面试题总结

前言

主要是以答案以及自己的理解来描述

参考以下链接

「历时8个月」10万字前端知识体系总结(基础知识篇) - 掘金

「万字总结」动画 + 大白话讲清楚React渲染原理
2万字 | 前端基础拾遗90问 - 掘金

“老默我想吃鱼了”与五层网络模型 - 掘金

目录

前言​​​​​​​​​​​​​​

JS 基础

执行上下文和执行栈

什么是执行上下文?

执行上下文生命周期

变量对象

执行栈

作用域 

作用域

作用域类型

函数作用域

块作用域

var、let、const的区别

let 实现原理

变量提升

闭包

作用域链

this

call apply bind

闭包

Css 基础

position


JS 基础

执行上下文和执行栈

什么是执行上下文?


Javascript 代码都是在执行上下文中运行的(执行环境,就像一个屏障一样,划分界限)

执行上下文: 指当前执行环境中的变量、函数声明、作用域链、this等信息

执行上下文(Execution context)是JavaScript代码执行时的环境。它包括了代码执行时需要的所有信息,如变量、函数、作用域链、this指向等。

JavaScript中有三种执行上下文:全局执行上下文、函数执行上下文和eval执行上下文。

全局执行上下文是在代码执行前创建的,用于全局范围的代码执行。

函数执行上下文是在函数调用时创建的,用于函数内部的代码执行。

eval执行上下文是在eval()函数执行时创建的,用于eval()函数内部的代码执行。

例子:

1. 全局执行上下文

var a = 1;

function foo() {
  console.log(a); // 输出1
}


2. 函数执行上下文

function foo() {
  var a = 1;
  console.log(a); // 输出1
}

foo();

3. eval执行上下文

eval("var a = 1; console.log(a);"); // 输出1

全局、函数、Eval执行上下文

执行上下文分为全局、函数、Eval执行上下文

1)全局执行上下文(浏览器环境下,为全局的 window 对象)

2)函数执行上下文,每当一个函数被调用时, 都会为该函数创建一个新的上下文

3)Eval 函数执行上下文,如eval("1 + 2")

对于每个执行上下文,都有三个重要属性:变量对象、作用域链(Scope chain)、this

执行上下文的特点:

1)单线程,只在主线程上运行;

2)同步执行,从上向下按顺序执行;

3)全局上下文只有一个,也就是window对象;

4)函数每调用一次就会产生一个新的执行上下文环境。

执行上下文生命周期

1)创建阶段
生成变量对象、建立作用域链、确定this的指向

2)执行阶段
变量赋值、函数的引用、执行其他代码

面试题总结_第1张图片

举个例子,当我们在浏览器中打开一个网页时,JavaScript 引擎会对每个 JavaScript 文件和代码块都创建对应的执行上下文,然后开始执行代码。当我们关闭网页或者页面发生跳转时,JavaScript 引擎就会销毁所有执行上下文,释放内存空间

变量对象

变量对象是与执行上下文相关的数据作用域,存储了上下文中定义的变量和函数声明

变量对象是一个抽象的概念,在全局执行上下文中,变量对象就是全局对象。 在顶层js代码中,this指向全局对象,全局变量会作为该对象的属性来被查询。在浏览器中,window就是全局对象

function foo(a, b) {
  var c = a + b;
  function bar() {
    console.log(c);
  }
  bar();
}

foo(1, 2);

在上面的代码中,当foo()函数被调用时,会创建一个新的执行上下文,并在其中创建一个变量对象。这个变量对象包含了参数ab,以及在函数内部声明的变量c和函数bar()bar()函数内部引用了变量c,因此它会在执行时从变量对象中获取c的值并输出到控制台上。

总之,变量对象是JavaScript执行上下文中的一个内部对象,它存储了当前上下文中定义的所有变量和函数,以及对其外部环境变量对象的引用。

执行栈

是一种先进后出的数据结构,用来存储代码运行的所有执行上下文

1)当 JS 引擎第一次遇到js脚本时,会创建一个全局的执行上下文并且压入当前执行栈

2)每当JS 引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部

3)当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文

4)一旦所有代码执行完毕,JS 引擎从当前栈中移除全局执行上下文

var a = 1; // 1. 全局上下文环境
function bar (x) {
    console.log('bar')
    var b = 2;
    fn(x + b); // 3. fn上下文环境
}
function fn (c) {
    console.log(c);
}
bar(3); // 2. bar上下文环境

作者:海阔_天空
链接:https://juejin.cn/post/7146973901166215176
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

面试题总结_第2张图片

(看了这个图明白多了,就是看执行顺序麻,搞了一个高深的词) 

作用域 

作用域

作用域:可访问变量的集合

function foo(a) {
  var b = 2;
  function bar() {
    var c = 3;
    console.log(a, b, c);
  }
  bar();
}

foo(1); // 输出: 1 2 3

 在上面的代码中,函数foo()定义了一个参数a和一个变量b,并在其内部定义了另一个函数bar()bar()函数定义了一个变量c,并打印了所有三个变量的值。

在函数bar()中,变量ab可以被访问,因为它们是从外部作用域传递进来的。变量c可以在bar()函数中直接访问,因为它是在bar()函数内部定义的。

总之,可访问变量是指当前作用域中可以直接访问的变量,包括在当前作用域中定义的变量、函数参数、以及从外部作用域传入的变量。

作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突

作用域类型

全局作用域函数作用域、ES6中新增了块级作用域

全局作用域

全局作用域是指在程序中没有嵌套函数或块级作用域的情况下定义的变量和函数所在的作用域。在全局作用域中定义的变量和函数可以在整个程序中被访问和使用,因此全局作用域也被称为全局命名空间。

函数作用域


是指声明在函数内部的变量,函数的作用域在函数定义的时候就决定了

块作用域


1)块作用域由{ }包括,if和for语句里面的{ }也属于块作用域
2)在块级作用域中,可通过let和const声明变量,该变量在指定块的作用域外无法被访问

var、let、const的区别

1)var定义的变量,没有块的概念,可以跨块访问, 可以变量提升

2)let定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问,无变量提升,不可以重复声明。可以修改值。

3)const用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改,无变量提升,不可以重复声明

如果用const 声明一个数组,此时是可以修改的,数组本身是可以修改的

const content = []
content[1]= 1
content [0] = 0

content此时是有一个指针指向数组的。它们之间是引用的关系 

let和const声明的变量只在块级作用域内有效,示例

function func() {
  if (true) {
    let i = 3;
  }
  console.log(i); // 报错 "i is not defined"
}
func();

var与let的经典案例

经典案例之一:循环中的声明

在循环中使用var声明变量会导致变量的作用域超出了循环块,而使用let则不会。

例如:

// 使用var
for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}

// 输出:3 3 3

// 使用let
for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}

// 输出:0 1 2

在第一个例子中,使用var声明i会导致所有的setTimeout都共享同一个变量i,最终输出3 3 3。

The output of the above code will be 3, 3, 3 because the setTimeout function is asynchronous(异步的) and the for loop will finish executing(for已经被执行完了) before the setTimeout functions are called. 

这是因为setTimeout是一个异步函数,而js 是单线程的。所以每次循环都需要等待4秒后再执行settimeout这个函数就很麻烦,因此引入了任务队列。只要设置了settimeout函数,当你使用它了,即使秒数设置为0,也不会立刻执行该函数。而是由浏览器先进行处理,现将东西放在任务队列中,等待执行栈中的东西执行完毕,再处理任务队列中的东西

而在第二个例子中,使用let声明i会使每个循环迭代都有一个新的变量i,因此输出0 1 2。

The reason for using let instead of var in the for loop is to create a block-scoped variable for i. This ensures that each setTimeout function has access to its own unique value of i, rather than all setTimeout functions sharing the same value of i. As a result, the output of the second for loop will be 0, 1, 2 with a delay of 1 second between each number being logged to the console.

1) 用var定义i变量,循环后打印i的值

// 案例1
// i是var声明的,在全局范围内都有效,全局只有一个变量i,输出的是最后一轮的i值,也就是 10

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function() {
    console.log(i);
  };
}
a[0]();  // 10
复制代码

这是因为在循环中创建的函数都共享同一个变量 i,当循环结束时,i 的值为 10。因此,当调用 a0 或 a6 时,实际上在打印 i 的值,而此时 i 的值都为 10。

2) 用let定义i变量,循环后打印i的值

// 案例2
// 用let声明i,for循环体内部是一个单独的块级作用域,相互独立,不会相互覆盖
var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function() {
    console.log(i);
  };
}
a[0](); // 0
a[6]();

经典案例之二:变量提升

使用var声明的变量会存在变量提升的现象,而使用let则不会。

例如:

// 使用var
console.log(a); // 输出undefined
var a = 1;

// 使用let
console.log(b); // 抛出ReferenceError
let b = 1;

在第一个例子中,由于变量a会被提升到函数的顶部,因此在console.log之前a已经被声明但尚未被赋值,所以输出undefined。而在第二个例子中,由于变量b不存在变量提升,所以在console.log之前b还未被声明,抛出ReferenceError异常。

综上所述,这些经典案例清晰地说明了var和let之间的区别。在使用变量时,我们应该根据具体情况选择合适的关键字,以确保变量的正确性和可读性。

let 实现原理

借助闭包和函数作用域来实现块级作用域的效果

// 用var实现案例2的效果
var a = [];

var _loop = function _loop(i) {
  a[i] = function() {
    console.log(i);
  };
};

for (var i = 0; i < 10; i++) {
  _loop(i);
}
a[0](); // 0

在 JavaScript 中,变量的作用域指的是在代码中可以访问该变量的区域。JavaScript 中有两种主要的变量作用域:函数作用域和块作用域。

函数作用域是指变量只能在声明它们的函数内部访问,外部的代码无法访问这些变量。这就是上面所提到的“函数作用域变量”的含义。在函数作用域内声明的变量称为局部变量,它们只能在该函数内部使用。

在这段代码中,迭代变量 i 是在 for 循环外部的函数作用域中声明的,因此所有在 for 循环内部创建的闭包共享相同的 i 变量。这意味着,在每次迭代结束后,i 变量的值会一直保留在内存中,因为闭包仍然在引用它。在使用 letconst 声明迭代变量时,i 会成为块作用域变量,并且在每次迭代后都会被销毁,因为每次迭代都会创建一个新的块级作用域。

在 JavaScript 中,由 var 声明的变量是函数作用域的,而不是块作用域的。即使在 {} 块内部声明变量,变量仍然只在当前函数作用域内可见。

这是因为在 JavaScript 中,var 关键字声明的变量会被提升到函数作用域的顶部,并且在整个函数中都可以访问。在上面的代码中,i 变量被声明在 for 循环的外部,因此它是函数作用域变量,而不是块作用域变量。

如果你希望变量在特定的块中可见并具有块级作用域,可以使用 letconst 关键字进行声明。这些关键字声明的变量会在其声明的块中具有作用域,并且不会被提升到函数作用域顶部。

在这段代码中,闭包 a[i] 持有对 _loop 函数的引用。在每次迭代中,_loop 函数被调用并将 i 的值作为参数传递。这个参数实际上是一个新的函数作用域内的变量,它的值被赋给闭包内的变量,因此闭包捕获的是一个新的变量,而不是迭代过程中 i 的引用。

虽然 i 的值在迭代过程中不断变化,但是由于闭包捕获的是新的函数作用域变量,而不是迭代过程中的 i 引用,所以每个闭包捕获的都是不同的变量,并且不会随着 i 的变化而变化。

这种现象被称为“闭包捕获变量的值而不是变量的引用”。这是因为在每次迭代中,闭包都会捕获一个新的函数作用域变量,而不是当前迭代中的 i 引用。

 两句话总结:

i使用的是var声明的,所以它的作用域不是块作用域,而是函数作用域。所以loop函数也是可以使用i变量的。然后因为在中间创建了闭包,因为闭包捕获变量的值而不是变量的引用,所以它捕获了新的函数作用域变量,而不是当前迭代中的i引用

变量提升

变量提升是指在JavaScript代码执行过程中,变量和函数声明会被提前到它们所在作用域的顶部,以便使用。

在变量提升中,变量和函数声明都会被提升,但是变量的赋值操作不会被提升。也就是说,在变量提升阶段,变量被声明但没有被赋值时,它的值为undefined。

例如:


console.log(a); // undefined
var a = 1;

上面的代码中,变量a在被赋值之前被使用了,但是由于变量提升的存在,a被声明,并且其值为undefined,所以不会报错。

函数声明也会被提升,但是函数表达式不会被提升。例如:


foo(); // "hello world"

function foo() {
  console.log("hello world");
}

bar(); // Uncaught TypeError: bar is not a function

var bar = function() {
  console.log("hello world");
}

上面的代码中,函数foo被声明并定义在调用之前,所以可以正常调用并输出结果。但是函数表达式bar的定义在调用之后,所以会报错。

闭包

闭包(closure)是指一个函数中包含另一个函数,并且内部函数可以访问外部函数的变量和参数。简单来说,就是内部函数“记住了”外部函数的执行环境。闭包可以用来实现一些高级的编程技巧,比如函数工厂、模块化等。

以下是一个例子:


function outerFunction() {
  var outerValue = "I am in outer function";
  
  function innerFunction() {
    console.log(outerValue);
  }
  
  return innerFunction;
}

var innerFunc = outerFunction();
innerFunc(); // 输出 "I am in outer function"

在这个例子中,outerFunction 中定义了一个内部函数 innerFunction,并将其作为返回值。在外部调用 outerFunction 后,将其返回值赋值给 innerFunc,此时 innerFunc 就是 innerFunction 函数。

当调用 innerFunc 时,它仍然能够访问 outerFunction 中的变量 outerValue,这是因为 innerFunction 形成了一个闭包,可以“记住”它的词法环境。这是一个非常简单的闭包实例,实际上,使用闭包的应用可以非常复杂。

作用域链

当查找变量的时候,首先会先从当前上下文的变量对象(作用域)中查找,如果没有找到,就会从父级的执行上下文的变量对象中查找,如果还没有找到,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链

自己-父亲-全局

最上层的执行上下文可以访问到其他层的变量对象,因为作用域链的缘故。

就像原味薯片一直在被加不同的味道。

this

this的5种绑定方式

1)默认绑定(非严格模式下this指向全局对象,严格模式下函数内的this指向undefined)

2)隐式绑定(当函数引用有上下文对象时, 如 obj.foo()的调用方式, foo内的this指向obj)

3)显示绑定(通过call或者apply方法直接指定this的绑定对象, 如foo.call(obj))

4)new构造函数绑定,this指向新生成的对象

5)箭头函数,this指向的是定义该函数时,外层环境中的this,箭头函数的this在定义时就决定了,不能改变

this 题目1

"use strict";
var a = 10; // var定义的a变量挂载到window对象上
function foo () {
  console.log('this1', this)  // undefined
  console.log(window.a)  // 10
  console.log(this.a)  //  报错,Uncaught TypeError: Cannot read properties of undefined (reading 'a')
}
console.log('this2', this)  // window
foo();

注意:开启了严格模式,只是使得函数内的this指向undefined,它并不会改变全局中this的指向。因此this1中打印的是undefined,而this2还是window对象。

JavaScript 严格模式是一种更为严格的编码规范,它在 JavaScript 语言的基础上增加了一些限制和规则,以帮助开发者编写更加安全、可靠、高效的代码。

使用严格模式,可以避免一些潜在的错误,同时也可以让代码运行更快,具有更好的可维护性。

严格模式的特点包括:

  1. 变量必须先声明后使用,否则会抛出错误。
  2. 禁止使用 with 语句。
  3. 函数中的 this 关键字的值不再是全局对象,而是 undefined
  4. 禁止删除变量或函数等。
  5. 禁止使用一些保留字作为变量名或函数名。

要在 JavaScript 中使用严格模式,需要在代码的开头添加 "use strict"; 字符串。例如:

"use strict";
var x = 1;

这样就可以开启严格模式,对代码进行更加严格的限制。

this 题目2

let a = 10
const b = 20
function foo () {
  console.log(this.a)  // undefined
  console.log(this.b)  // undefined
}
foo();
console.log(window.a) // undefined  

如果把 var 改成了 let 或 const,变量是不会被绑定到window上的,所以此时会打印出三个undefined

详细解释

在 JavaScript 中,在全局作用域中声明的变量会成为全局对象(在浏览器中通常是 window 对象)的属性,因此可以通过全局对象来访问这些变量。但是,在严格模式下,声明变量时未使用关键字 varletconst,则不会成为全局对象的属性,也就无法通过全局对象来访问这些变量。

在你的代码中,变量 a 是通过使用 let 关键字声明的,因此它不会成为全局对象的属性。foo 函数中,使用 this.a 访问变量 a 时,因为没有给 this 指定一个明确的对象,所以 this 默认指向全局对象,但是全局对象并没有属性 a,因此返回 undefined

变量 b 是通过使用 const 关键字声明的,它也不会成为全局对象的属性。foo 函数中,同样使用 this.b 访问变量 b 时,也返回 undefined

需要注意的是,使用全局对象来访问变量是一种不好的编程习惯,因为它会破坏变量的封装性和作用域规则,容易导致不必要的错误和难以维护的代码。建议尽可能地避免使用全局变量和全局对象。

window.a为什么是undefined. let a =10 不是window.a?

在 JavaScript 中,使用 var、let 或 const 声明的变量在全局作用域中都会被添加到全局对象(比如浏览器环境中的 window 对象)上,但是这些变量并不是全局对象的直接属性,而是被封装在全局作用域中的一个词法环境(lexical environment)中。

因此,对于全局作用域中使用 let 或 const 声明的变量,虽然它们在全局作用域中可见,但是并不会成为全局对象的属性,因此访问 window.a 会返回 undefined。

需要注意的是,对于使用 var 声明的变量,它们会成为全局对象的属性,因此访问 window.a 会返回它们的值。但是,由于 var 存在变量提升的特性,因此在函数内部声明的 var 变量会被提升到函数顶部,所以在函数内部访问 var 变量时可能会出现意料之外的结果。

this 题目3

var a = 1
function foo () {
  var a = 2
  console.log(this)  // window
  console.log(this.a) // 1
}
foo()

foo()函数内的this指向的是window,因为是window调用的foo,打印出的this.a是window下的a

严格模式下指向的是undefined

怎么修改让this.a 为2呢?

要让 this.a 的值为 2,可以将变量 a 的值赋给 this.a,可以使用函数的 call 或 apply 方法来改变函数执行时的上下文,即 this 对象。具体操作如下:

var a = 1
function foo () {
  var a = 2
  console.log(this)  // window
  console.log(this.a) // 1
  this.a = a  // 将变量 a 的值赋给 this.a
}
foo.call(this)  // 使用 call 方法将函数的上下文设置为 window 对象
console.log(a)  // 1
console.log(this.a)  // 2

在上面的代码中,我们将函数 foo 的上下文设置为 window 对象,并在函数内部将变量 a 的值赋给 this.a。最后,我们输出全局变量 a 和 this.a 的值,可以看到 this.a 的值已经被修改为 2。 

this 题目4

var obj2 = {
    a: 2,
    foo1: function () {
      console.log(this.a) // 2
    },
    foo2: function () {
      setTimeout(function () {
        console.log(this) // window
        console.log(this.a) // 3
      }, 0)
    }
  }
  var a = 3
  
  obj2.foo1()
  obj2.foo2() 

对于setTimeout中的函数,这里存在隐式绑定的this丢失,也就是当我们将函数作为参数传递时,会被隐式赋值,回调函数丢失this绑定,因此这时候setTimeout中函数内的this是指向window

在这段代码中,定义了一个对象 obj2,它有两个属性 a 和两个方法 foo1foo2。同时,全局作用域中也定义了一个变量 a

当调用 obj2.foo1() 时,方法 foo1 内部的 this 指向调用它的对象 obj2,因此 this.a 返回对象的属性 a,也就是 2

当调用 obj2.foo2() 时,方法 foo2 内部的 this 同样指向调用它的对象 obj2。但是,foo2 方法中有一个 setTimeout 函数,该函数会在下一个事件循环中执行,并且在执行时 this 默认指向全局对象(在浏览器环境中是 window)。因此,在 setTimeout 函数内部,使用 this 访问对象的属性 a 时,实际上是访问了全局作用域中的变量 a,其值为 3

要解决这个问题,可以使用 bind 方法来显式地绑定 this 的值,例如:

foo2: function () {
  setTimeout(function () {
    console.log(this.a) // 2
  }.bind(this), 0)
}

使用 bind 方法将 setTimeout 函数内部的 this 绑定到调用 foo2 方法的对象 obj2 上,这样就可以正确地访问对象的属性 a

this 题目5

var obj = {
 name: 'obj',
 foo1: () => {
   console.log(this.name) // window
 },
 foo2: function () {
   console.log(this.name) // obj
   return () => {
     console.log(this.name) // obj
   }
 }
}
var name = 'window'
obj.foo1()
obj.foo2()()

这道题非常经典,它证明了箭头函数( => )内的this是由外层作用域决定的

题目5解析:
1)对于obj.foo1()函数的调用,它的外层作用域是window,对象obj当然不属于作用域了(作用域只有全局作用域、函数作用域、块级作用域),所以会打印出window

2)obj.foo2()(),首先会执行obj.foo2(),这不是个箭头函数,所以它里面的this是调用它的obj对象,因此第二个打印为obj,而返回的匿名函数是一个箭头函数,它的this由外层作用域决定,那也就是它的this会和foo2函数里的this一样,第三个打印也是obj

再来40道this面试题酸爽继续(1.2w字用手整理)

还没看,上面的连接

call apply bind

三者的区别

1)三者都可以显式绑定函数的this指向

2)三者第一个参数都是this要指向的对象,若该参数为undefined或null,this则默认指向全局window

3)传参不同:apply是数组、call是参数列表(比如说单独的参数),而bind可以分为多次传入,实现参数的合并

4)call、apply是立即执行,bind是返回绑定this之后的函数,如果这个新的函数作为构造函数被调用,那么this不再指向传入给bind的第一个参数,而是指向新生成的对象

function Person(name, age) {
  this.name = name;
  this.age = age;
}

const person1 = new Person('Alice', 25);
console.log(person1.name); // 输出:Alice
console.log(person1.age); // 输出:25

const person2 = Person.bind(null, 'Bob', 30);
const bob = new person2();
console.log(bob.name); // 输出:Bob
console.log(bob.age); // 输出:30

onst person3 = Person.bind(null, 'Cat', 40);
person3();
console.log(person3.name); // undefined 
console.log(person3.age); // undefined

在这个例子中,我们定义了一个 Person 构造函数,它有两个参数 nameage。然后我们使用 new 关键字创建一个名为 person1 的对象,并传递了 Alice25 作为参数。

接下来,我们使用 bind() 方法创建一个新函数 person2,它将 Person 构造函数绑定到第一个参数为 null(或 undefined)上,并传递了 Bob30 作为参数。此时 person2 函数作为构造函数并没有立即执行,而是返回一个绑定了参数的函数。

最后,我们使用 new 关键字创建一个新对象 bob,并将 person2 作为构造函数调用。此时,bob 对象的 name 值为 Bobage 值为 30。这是因为 person2 函数在这里作为构造函数被调用时,它的执行上下文已被绑定到新创建的对象上,而不是 null(或 undefined)。

希望这个例子可以帮助您理解使用 bind() 方法创建的函数作为构造函数时 this 值的变化。

apply:

在 JavaScript 中,您可以使用 apply() 方法来更改函数的执行上下文(即 this 关键字的值)。

apply() 方法是 JavaScript 中的一个内置方法,它是所有函数对象的原型方法。该方法接收两个参数:

  1. 一个对象,将作为函数执行时的上下文对象。
  2. 个可选参数数组,它包含函数执行时传递给函数的参数。

下面是一个示例代码,它使用 apply() 方法来更改函数的执行上下文:

function greeting() {
  console.log(`Hello, ${this.name}!`);
}

const person = {
  name: 'Alice'
};

greeting.apply(person);
// 输出:Hello, Alice!

在这个例子中,我们定义了一个 greeting() 函数,它使用 console.log() 方法输出一条问候信息。然后我们定义了一个名为 person 的对象,它具有一个 name 属性。最后,我们使用 apply() 方法将 person 对象作为 greeting() 函数的上下文对象,这样在函数中使用 this.name 就会输出 Alice

需要注意的是,如果第一个参数传递了一个 null 或者 undefined,则默认上下文将是全局对象(在浏览器中为 window 对象)。如果传递的第一个参数不是一个对象,则会将其转换为对象。如果您不需要传递任何参数,则可以将第二个参数设置为一个空数组。

bind

在 JavaScript 中,您可以使用 bind() 方法来绑定一个函数的执行上下文,从而创建一个新的函数。这个新函数的 this 值将始终保持绑定的值。

bind() 方法返回一个新函数,该函数与原始函数具有相同的代码体,但具有新的执行上下文。它不会改变原始函数的执行上下文。

下面是一个示例代码,它使用 bind() 方法来绑定函数的执行上下文:

const person = {
  name: 'Alice'
};

function greeting() {
  console.log(`Hello, ${this.name}!`);
}

const boundGreeting = greeting.bind(person);
boundGreeting(); // 输出:Hello, Alice!

在这个例子中,我们首先定义了一个 person 对象,它有一个 name 属性。然后我们定义了一个 greeting() 函数,它使用 console.log() 方法输出一条问候信息。接下来,我们使用 bind() 方法创建一个新函数 boundGreeting,该函数的执行上下文将是 person 对象。最后,我们调用 boundGreeting(),它将输出 Hello, Alice!

需要注意的是,当您调用 bind() 方法时,您可以传递参数给该方法。这些参数将被添加到绑定函数的参数列表中,并传递给函数作为参数。例如:

function multiply(a, b) {
  return a * b;
}

const double = multiply.bind(null, 2);
console.log(double(5)); // 输出:10

在这个例子中,我们定义了一个 multiply() 函数,它将两个参数相乘并返回结果。然后我们使用 bind() 方法创建一个新函数 double,该函数将第一个参数设置为 2。最后,我们调用 double(5),它将返回 10,因为它等于 2 * 5

希望这可以帮助您了解如何在 JavaScript 中使用 bind() 方法来绑定函数的执行上下文。

手写call apply bind

// 手写call
Function.prototype.Call = function(context, ...args) {
  // context为undefined或null时,则this默认指向全局window
  if (context === undefined || context === null) {
    context = window;
  }
  // 利用Symbol创建一个唯一的key值,防止新增加的属性与obj中的属性名重复
  let fn = Symbol();
  // this指向调用call的函数
  context[fn] = this; 
  // 隐式绑定this,如执行obj.foo(), foo内的this指向obj
  let res = context[fn](...args);
  // 执行完以后,删除新增加的属性
  delete context[fn]; 
  return res;
};

// apply与call相似,只有第二个参数是一个数组,
Function.prototype.Apply = function(context, args) {
  if (context === undefined || context === null) {
    context = window;
  }
  let fn = Symbol();
  context[fn] = this;
  let res = context[fn](...args);
  delete context[fn];
  return res;
};

// bind要考虑返回的函数,作为构造函数被调用的情况
Function.prototype.Bind = function(context, ...args) {
  if (context === undefined || context === null) {
    context = window;
  }
  let fn = this;
  let f = Symbol();
  const result = function(...args1) {
    if (this instanceof fn) {
      // result如果作为构造函数被调用,this指向的是new出来的对象
      // this instanceof fn,判断new出来的对象是否为fn的实例
      this[f] = fn;
      let res = this[f](...args, ...args1);
      delete this[f];
      return res;
    } else {
      // bind返回的函数作为普通函数被调用时
      context[f] = fn;
      let res = context[f](...args, ...args1);
      delete context[f];
      return res;
    }
  };
  // 如果绑定的是构造函数 那么需要继承构造函数原型属性和方法
  // 实现继承的方式: 使用Object.create
  result.prototype = Object.create(fn.prototype);
  return result;
};

闭包

闭包:就是函数引用了外部作用域的变量

闭包常见的两种情况:
一是函数作为返回值; 另一个是函数作为参数传递

闭包的作用:
可以让局部变量的值始终保持在内存中;对内部变量进行保护,使外部访问不到
最常见的案例:函数节流和防抖

闭包的垃圾回收:
副作用:不合理的使用闭包,会造成内存泄露(就是该内存空间使用完毕之后未被回收)
闭包中引用的变量直到闭包被销毁时才会被垃圾回收

闭包的示例

// 原始题目
for (var i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i); // 1s后打印出5个5
  }, 1000);
}

// ⬅️利用闭包,将上述题目改成1s后,打印0,1,2,3,4

// 方法一:
for (var i = 0; i < 5; i++) {
  (function(j) {
    setTimeout(function timer() {
      console.log(j);
    }, 1000);
  })(i);
}

// 方法二:
// 利用setTimeout的第三个参数,第三个参数将作为setTimeout第一个参数的参数
for (var i = 0; i < 5; i++) {
  setTimeout(function fn(i) {
    console.log(i);
  }, 1000, i); // 第三个参数i,将作为fn的参数
}

// ⬅️将上述题目改成每间隔1s后,依次打印0,1,2,3,4
for (var i = 0; i < 5; i++) {
  setTimeout(function fn(i) {
    console.log(i);
  }, 1000 * i, i);
}
复制代码

发现 JavaScript 中闭包的强大威力https://juejin.cn/post/6844903769646317576
破解前端面试(80% 应聘者不及格系列):从闭包说起

for (var i = 0; i < 5; i++) {
  (function(j) {
    setTimeout(function timer() {
      console.log(j);
    }, 1000);
  })(i);
}

这段代码中,使用了立即执行函数表达式(IIFE)来创建了一个闭包。在每次循环中,都会调用一个新的 IIFE,传递当前迭代的变量 i 作为参数 j,这样在 IIFE 内部就可以通过 j 访问到每次迭代的 i 值,而不会受到变量提升的影响。

在 IIFE 内部,调用了一个 setTimeout 函数,该函数会在指定的延迟时间后执行一个函数,这里的函数是一个匿名函数,用于输出参数 j 的值。因为 setTimeout 函数是异步的,会在循环结束后才开始执行,所以输出的结果是从 04,每隔一秒输出一个数值。

如果不使用闭包,直接在 setTimeout 函数内部使用循环变量 i,由于 JavaScript 中的变量是函数级别的作用域,而不是块级别的作用域,所以在循环结束后,i 的值会是 5,而不是我们期望的 04。因此,使用闭包可以避免这个问题,确保输出的结果符合预期。

为什么setTimeout 函数是异步的,会在循环结束后才开始执行?

setTimeout 函数是异步的,因为它会在指定的延迟时间后将回调函数添加到任务队列(task queue)中,而不会立即执行该回调函数。当任务队列中没有其他任务时,JavaScript 引擎会从任务队列中取出下一个任务,执行其对应的回调函数。

在这个例子中,每次循环中都会调用 setTimeout 函数,并将回调函数添加到任务队列中。因为延迟时间是相同的,所以这些任务会按照添加的顺序依次排队执行。但由于任务是在循环结束后才开始执行的,所以最终的结果是从 04,每隔一秒输出一个数值。

需要注意的是,setTimeout 的延迟时间并不是精确的,而是一个最小值。实际上,延迟时间可能会比指定的值更长,因为在添加任务到队列中时会考虑到其他任务的执行时间和 JavaScript 引擎的负载等因素。

 使用立即执行函数,执行上下文中的变量对象是x,所以,它会看到全局变量对象i。这时候就会拿到i的值,放入任务队列中(i++),等到for循环结束后,再执行任务队列中的值。

原型/原型链

原型的作用

原型被定义为给其它对象提供共享属性的对象,函数的实例可以共享原型上的属性和方法

原型对象 

基本数据类型包括数字、字符串、布尔值、null 和 undefined。除此之外,其他所有东西都是对象,包括数组、函数、对象等等。

引用类型就是对象。

 

 

 

 

面试题详解  

面试题总结_第3张图片 

普通函数是不会放回一个新对象的

面试题总结_第4张图片 

 

原型链

它的作用就是当你在访问一个对象上属性的时候,如果该对象内部不存在这个属性,那么就会去它__proto__属性所指向的对象(原型对象)上查找。如果原型对象依旧不存在这个属性,那么就会去其原型的__proto__属性所指向的原型对象上去查找。以此类推,直到找到nul,而这个查找的线路,也就构成了我们常说的原型链

面试题总结_第5张图片

创建的实例,查看shop原型对象是否有属性,没有,继续往上找,查找supermarket是否有相关的属性。找到了就返回 

面试题

问a,b属性存在在f的属性中吗

let F = function(){}
Function.propotype.a = function(){}
Object.propotype.b = function(){}
let f = new F()

答案是a。因为f有object的propotype

原型链和作用域链的区别: 原型链是查找对象上的属性,作用域链是查找当前上下文中的变量

对象(Object)是 JavaScript 中的一种数据类型,它可以包含多个属性和方法,是 JavaScript 编程中的基本单元。属性(Property)是对象中的一个键值对,它描述了对象的某个特征或行为。变量(Variable)是用于存储值的标识符,可以存储各种类型的值,例如数字、字符串、对象等。当前上下文(Current context)是指代码当前执行的环境,例如函数执行时的上下文、全局环境等。

// 创建一个对象,包含一个属性和一个方法
const person = {
  name: "Alice",
  greet: function() {
    console.log(`Hello, my name is ${this.name}.`);
  }
};

// 调用对象的方法
person.greet(); // 输出 "Hello, my name is Alice."

// 创建一个函数,内部包含一个变量
function sayHello() {
  const message = "Hello, world!";

  // 内部函数可以访问外部函数的变量
  function logMessage() {
    console.log(message);
  }

  logMessage();
}

// 调用函数
sayHello(); // 输出 "Hello, world!"

 在这个例子中,person 是一个对象,它有一个 name 属性和一个 greet 方法。sayHello 是一个函数,它有一个内部变量 message 和一个内部函数 logMessage,后者可以访问前者的变量。通过这个例子,我们可以看到对象和属性之间的关系,函数和变量之间的关系,以及作用域链是如何在函数内部查找变量的。

proto、prototype、constructor属性介绍

1)js中对象分为两种,普通对象和函数对象

2)__proto__constructor是对象独有的。prototype属性是函数独有的,它的作用是包含可以给特定类型的所有实例提供共享的属性和方法;但是在 JS 中,函数也是对象,所以函数也拥有__proto__constructor属性

3)constructor属性是对象所独有的,它是一个对象指向一个函数,这个函数就是该对象的构造函数
构造函数.prototype.constructor === 该构造函数本身

4)一个对象的__proto__指向其构造函数的prototype
函数创建的对象.__proto__ === 该函数.prototype

5)特殊的ObjectFunction

console.log(Function.prototype === Function.__proto__); // true
console.log(Object.__proto__ === Function.prototype); // true
console.log(Function.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true

作者:海阔_天空
链接:https://juejin.cn/post/7146973901166215176
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处

下是一个使用原型继承的例子:

// 定义一个Animal构造函数
function Animal(name) {
  this.name = name;
}

// 添加一个Animal的原型方法
Animal.prototype.sayName = function() {
  console.log("My name is " + this.name);
};

// 定义一个Dog构造函数
function Dog(name, breed) {
  Animal.call(this, name); // 调用父类构造函数,并传递参数
  this.breed = breed;
}

// 使用原型继承来继承Animal的原型,让Dog实例可以访问Animal的原型方法
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 重设Dog的构造函数

// 添加一个Dog的原型方法
Dog.prototype.bark = function() {
  console.log("Woof!");
};

// 创建一个Dog实例
const myDog = new Dog("Buddy", "Golden Retriever");

// 调用继承自Animal的原型方法
myDog.sayName(); // 输出 "My name is Buddy"

// 调用Dog自己的方法
myDog.bark(); // 输出 "Woof!"

instanceof

instanceof 的基本用法,它可以判断一个对象的原型链上是否包含该构造函数的原型,经常用来判断对象是否为该构造函数的实例。

实例是什么?

在 JavaScript 中,我们可以使用构造函数来创建对象。一个构造函数就是一个类的定义,而构造函数被调用时会创建该类的一个实例对象。new 一个对象的时候,会自动调用构造函数。

特殊示例

console.log(Object instanceof Object); //true
console.log(Function instanceof Function); //true
console.log(Function instanceof Object); //true
console.log(function() {} instanceof Function); //true

作者:海阔_天空
链接:https://juejin.cn/post/7146973901166215176
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 instanceof是什么,可以举一个例子?

// 定义一个Animal构造函数
function Animal(name) {
  this.name = name;
}

// 创建一个Animal实例
const myAnimal = new Animal("Leo");

// 判断myAnimal是否是Animal的实例
console.log(myAnimal instanceof Animal); // true
console.log(myAnimal instanceof Object); // true

// 定义一个Dog构造函数
function Dog(name, breed) {
  Animal.call(this, name);
  this.breed = breed;
}

// 使用原型继承继承Animal的原型
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

// 创建一个Dog实例
const myDog = new Dog("Buddy", "Golden Retriever");

// 判断myDog是否是Dog和Animal的实例
console.log(myDog instanceof Dog); // true
console.log(myDog instanceof Animal); // true
console.log(myDog instanceof Object); // true

 手写instanceof方法

function instanceOf(obj, fn) {
  let proto = obj.__proto__;
  if (proto) {
    if (proto === fn.prototype) {
      return true;
    } else {
      return instanceOf(proto, fn);
    }
  } else {
    return false;
  }
}

// 测试
function Dog() {}
let dog = new Dog();
console.log(instanceOf(dog, Dog), instanceOf(dog, Object)); // true true

作者:海阔_天空
链接:https://juejin.cn/post/7146973901166215176
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

instanceof与typeof的区别

1)typeof一般被用于来判断一个变量的类型
typeof可以用来判断number、undefined、symbol、string、function、boolean、object 这七种数据类型,特殊情况:typeof null === 'object'

2)instanceof判断一个对象的原型链上是否包含该构造函数的原型

一文吃透所有JS原型相关知识点

new 关键字

new一个对象,到底发生什么?

1)创建一个对象,该对象的原型指向构造函数的原型

2)调用该构造函数,构造函数的this指向新生成的对象

3)判断构造函数是否有返回值,如果有返回值且返回值是一个对象或一个方法,则返回该值;否则返回新生成的对象

构造函数有返回值的案例

function Dog(name) {
  this.name = name;
  return { test: 1 };
}
let obj = new Dog("ming");
console.log(obj); // {test:1} 

手写new

function selfNew(fn, ...args) {
  // 创建一个instance对象,该对象的原型是fn.prototype
  let instance = Object.create(fn.prototype);
  // 调用构造函数,使用apply,将this指向新生成的对象
  let res = fn.apply(instance, args);
  // 如果fn函数有返回值,并且返回值是一个对象或方法,则返回该对象,否则返回新生成的instance对象
  return typeof res === "object" || typeof res === "function" ? res : instance;
}

继承

多种继承方式

1)原型链继承,缺点:引用类型的属性被所有实例共享
2)借用构造函数(经典继承)
3)原型式继承
4)寄生式继承
5)组合继承
6)寄生组合式继承

  1. 原型链继承:这种方式通过让子类的原型对象指向父类的实例来实现继承。例如:
function Animal(name) {
  this.name = name;
}

Animal.prototype.sayName = function() {
  console.log("My name is " + this.name);
}

function Cat() {}

Cat.prototype = new Animal();

var cat = new Cat();
cat.sayName(); // "My name is undefined"

这种方式的缺点是,父类的引用类型属性会被子类的所有实例共享。

  1. 借用构造函数(经典继承):这种方式通过在子类构造函数中调用父类构造函数来实现继承。例如:
function Animal(name) {
  this.name = name;
}

Animal.prototype.sayName = function() {
  console.log("My name is " + this.name);
}

function Cat(name) {
  Animal.call(this, name);
}

var cat = new Cat("Kitty");
cat.sayName(); // "My name is Kitty"

这种方式解决了父类引用类型属性共享的问题,但是无法继承父类的原型对象上的属性和方法。

  1. 原型式继承:这种方式通过创建一个临时的构造函数来实现继承。例如:
function createObject(obj) {
  function F() {}
  F.prototype = obj;
  return new F();
}

var animal = {
  name: "Animal",
  sayName: function() {
    console.log("My name is " + this.name);
  }
};

var cat = createObject(animal);
cat.name = "Kitty";
cat.sayName(); // "My name is Kitty"

在这个函数中,使用了原型继承的方式创建了一个新的对象。函数createObject(obj) 接受一个参数 obj,用作新对象的原型。该函数创建了一个空的构造函数F,并将 obj 对象设置为 F 的原型对象。最后,通过 new F() 创建了一个新的对象并返回。

这个新对象将具有 obj 对象的所有属性和方法,因为它的原型指向了 obj 对象。当我们在新对象上访问属性或方法时,如果在对象本身上找不到该属性或方法,它会在其原型对象上查找该属性或方法。这就是 JavaScript 中的原型继承。

在示例中,我们创建了一个名为 animal 的对象,并将其作为参数传递给 createObject 函数。然后,我们创建了一个名为 cat 的新对象,并将其原型设置为 animal 对象。因此,cat 对象继承了 animal 对象的属性和方法,包括 sayName 方法。最后,我们将 cat 对象的 name 属性设置为 "Kitty",并调用其 sayName 方法,输出 "My name is Kitty"。

通过这种方式,我们可以使用原型继承的方式创建新对象,而无需使用构造函数或类。这种方法有助于简化代码并使其更易于理解。

这种方式的缺点和原型链继承相同,父类的引用类型属性会被子类的所有实例共享。

  1. 寄生式继承:这种方式与原型式继承类似,只是在创建临时构造函数的时候加入了一些额外的属性和方法。例如:
function createCat(obj) {
  var cat = createObject(obj);
  cat.sayName = function() {
    console.log("My name is " + this.name);
  };
  return cat;
}

var animal = {
  name: "Animal"
};

var cat = createCat(animal);
cat.name = "Kitty";
cat.sayName(); // "My name is Kitty"

这种方式的缺点也和原型链继承相同,父类的引用类型属性会被子类的所有实例共享。

  1. 组合继承:这种方式结合了原型链继承和借用构造函数的优点。例如:
function Animal(name) {
  this.name = name;
}

Animal.prototype.sayName = function() {
  console.log("My name is " + this.name);
}

function Cat(name) {
  Animal.call(this, name);
}

Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

var cat = new Cat("Kitty");
cat.sayName(); // "My name is Kitty"

寄生组合式继承:在组合继承的基础上,使用寄生式继承优化构造函数的继承方式。通过创建一个空对象作为中介,减少了父类构造函数的调用次数。

以下是一个使用寄生组合式继承的例子:

function Parent(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

Parent.prototype.sayName = function() {
  console.log(this.name);
};

function Child(name, age) {
  Parent.call(this, name);
  this.age = age;
}

// 借用构造函数实现属性的继承
// 在子类的构造函数中调用父类的构造函数,实现属性的继承

// 组合继承实现方法的继承
// 将子类的原型指向父类的实例,实现方法的继承

function inheritPrototype(child, parent) {
  const prototype = Object.create(parent.prototype); // 创建一个空对象,使用父类的原型对象作为新对象的原型
  prototype.constructor = child; // 修正构造函数指向子类
  child.prototype = prototype; // 将子类的原型指向新对象
}

inheritPrototype(Child, Parent);

// 寄生组合式继承实现构造函数的优化
// 在新创建的对象上添加属性和方法,最终返回该对象

寄生组合式继承的优势

优势:借用父类的构造函数,在不需要生成父类实例的情况下,继承了父类原型上的属性和方法

手写寄生组合式继承

// 精简版
class Child {
  constructor() {
    // 调用父类的构造函数
    Parent.call(this);
    // 利用Object.create生成一个对象,新生成对象的原型是父类的原型,并将该对象作为子类构造函数的原型,继承了父类原型上的属性和方法
    Child.prototype = Object.create(Parent.prototype);
    // 原型对象的constructor指向子类的构造函数
    Child.prototype.constructor = Child;
  }
}

// 通用版
function Parent(name) {
  this.name = name;
}
Parent.prototype.getName = function() {
  console.log(this.name);
};
function Child(name, age) {
  // 调用父类的构造函数
  Parent.call(this, name); 
  this.age = age;
}
function createObj(o) {
  // 目的是为了继承父类原型上的属性和方法,在不需要实例化父类构造函数的情况下,避免生成父类的实例,如new Parent()
  function F() {}
  F.prototype = o;
  // 创建一个空对象,该对象原型指向父类的原型对象
  return new F(); 
}

// 等同于 Child.prototype = Object.create(Parent.prototype)
Child.prototype = createObj(Parent.prototype); 
Child.prototype.constructor = Child;

let child = new Child("tom", 12);
child.getName(); // tom

一文吃透所有JS原型相关知识点
最详尽的 JS 原型与原型链终极详解

Class 类

1) Class 类可以看作是构造函数的语法糖

class Point {}
console.log(typeof Point); // "function"
console.log(Point === Point.prototype.constructor); // true

 2) Class 类中定义的方法,都是定义在该构造函数的原型上

class Point {
  constructor() {}
  toString() {}
}
// 等同于
Point.prototype = { constructor() {}, toString() {} };

自定义方法的时候不需要写function toString,只需要写上toString 

3)使用static关键字,作为静态方法(静态方法,只能通过类调用,实例不能调用) 

class Foo {
  static classMethod() {
    return "hello";
  }
}
Foo.classMethod(); // 'hello'

 在 JavaScript 中,static 关键字用于定义一个类的静态方法或静态属性。静态方法和属性是不需要实例化类即可使用的方法和属性,它们是属于类本身的,而不是属于实例的。

4)实例属性的简写写法 

class Foo {
  bar = "hello";
  baz = "world";
}
// 等同于
class Foo {
  constructor() {
    this.bar = "hello";
    this.baz = "world";
  }
}


作者:海阔_天空
链接:https://juejin.cn/post/7146973901166215176
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

5)extends 关键字,底层也是利用的寄生组合式继承 

class Parent {
  constructor(age) {
    this.age = age;
  }
  getName() {
    console.log(this.name);
  }
}
class Child extends Parent {
  constructor(name, age) {
    super(age);
    this.name = name;
  }
}
let child = new Child("li", 16);
child.getName(); // li

作者:海阔_天空
链接:https://juejin.cn/post/7146973901166215176
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 使用extends 继承父类的时候,需要需要添加新的属性,首先需要创建constructor,constructor里面需要使用super,super里面的属性是父类的属性

手写Class类

ES6的 Class 内部是基于寄生组合式继承,它是目前最理想的继承方式
ES6的 Class 允许子类继承父类的静态方法和静态属性

// Child 为子类的构造函数, Parent为父类的构造函数
function selfClass(Child, Parent) {
  // Object.create 第二个参数,给生成的对象定义属性和属性描述符/访问器描述符
  Child.prototype = Object.create(Parent.prototype, {
    // 子类继承父类原型上的属性和方法
    constructor: {
      enumerable: false,
      configurable: false,
      writable: true,
      value: Child
    }
  });
  // 继承父类的静态属性和静态方法
  Object.setPrototypeOf(Child, Parent);
}

// 测试
function Child() {
  this.name = 123;
}
function Parent() {}
// 设置父类的静态方法getInfo
Parent.getInfo = function() {
  console.log("info");
};
Parent.prototype.getName = function() {
  console.log(this.name);
};
selfClass(Child, Parent);
Child.getInfo(); // info
let tom = new Child();
tom.getName(); // 123

作者:海阔_天空
链接:https://juejin.cn/post/7146973901166215176
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Promise

Promise的底层原理:callback回调函数 + 发布订阅模式​​​​​​​​​​​​​​

回调函数

回调函数(Callback Function)是指一种函数,它作为参数传递给另一个函数,并且在该函数的内部被调用。回调函数常常用于异步编程中,以便在异步操作完成后执行某些操作。

例如,假设我们有一个函数getData,它从服务器异步获取数据,并且需要在数据返回后执行某些操作。我们可以使用回调函数来处理这种情况,如下所示:

function getData(callback) {
  // 异步获取数据
  const data = ...; // 假设这里是从服务器获取到的数据

  // 调用回调函数
  callback(data);
}

// 定义回调函数
function processData(data) {
  // 处理数据
  console.log(data);
}

// 调用函数,并传递回调函数作为参数
getData(processData);

在上面的代码中,我们定义了一个名为getData的函数,该函数接受一个回调函数作为参数,并在数据返回后调用该函数。我们还定义了另一个名为processData的函数,该函数接受数据作为参数,并对其进行处理。最后,我们调用getData函数,并将processData函数作为回调函数传递给它。当getData函数完成异步操作后,它会调用processData函数,并将获取到的数据作为参数传递给它。

所以他的重点是,它作为参数传递给另一个函数,并且在该函数内部被调用。那不是跟闭包有点像吗,一个函数嵌套另一个函数,并且内部参数可以访问外部参数?

是的,回调函数和闭包确实有一些相似之处。它们都涉及到一个函数作为另一个函数的参数,且内部可以访问外部的变量。

但是回调函数和闭包的主要目的是不同的。回调函数通常用于异步编程中,以便在异步操作完成后执行某些操作。而闭包则通常用于在函数内部创建私有变量,并且可以让这些变量在函数外部被访问。

虽然它们有一些相似之处,但是它们的使用场景和主要目的是不同的。

他们的主要使用场景和主要目的都是什么?

回调函数和闭包的主要使用场景和主要目的如下:

回调函数:

使用场景:

  • 异步编程,如处理事件、读取文件、网络请求等操作。
  • 模块化编程,如在模块中传递一个函数作为参数,以便在模块加载完成后执行该函数。

主要目的:

  • 在异步操作完成后执行某些操作。
  • 实现模块化编程,使模块之间可以互相调用并协作工作。

闭包:

使用场景:

  • 封装代码,避免全局变量污染。
  • 创建私有变量和方法,以保护数据安全性。
  • 缓存计算结果,提高程序性能。

主要目的:

  • 在函数内部创建私有变量和方法,使这些变量和方法在函数外部不可访问。
  • 提供更好的封装性和数据安全性。
  • 提高程序性能和效率,避免重复计算和重复执行代码。

需要注意的是,回调函数和闭包并不是完全独立的概念,它们常常会在一起使用,以实现更加复杂的功能。

发布订阅模式

发布-订阅模式(Publish-Subscribe Pattern),也称为观察者模式(Observer Pattern),是一种消息传递模式。在该模式中,消息的发送方(称为“发布者”)不会直接将消息发送给特定的接收方(称为“订阅者”),而是将消息发布到一个中间组件(称为“消息代理”或“主题”),订阅者可以选择订阅特定类型的消息,并在消息发布时自动接收通知。

在发布-订阅模式中,发布者和订阅者之间是松散耦合的,即它们彼此独立,可以相互交互而不需要知道彼此的具体实现细节。这种松散耦合的架构有助于提高系统的可扩展性、可维护性和可重用性,因为可以轻松地添加或删除发布者和订阅者,而不会对整个系统造成过多的影响。

发布-订阅模式在许多应用程序中得到广泛使用,包括消息队列系统、事件驱动架构、UI设计和分布式系统等。

发布订阅模式,为什么在promise底层原理?

发布订阅模式和 Promise 并不是同一层次的概念,它们之间并没有直接的联系。发布订阅模式是一种设计模式,它用于实现松耦合的组件之间的通信机制。而 Promise 是一种异步编程的解决方案,它用于解决回调地狱等问题。

然而,在实现 Promise 的过程中,发布订阅模式确实被用到了。Promise 的实现中,使用了一个叫做“事件队列”的机制来处理异步操作的结果。在这个事件队列中,每个异步操作都会产生一个事件,当该异步操作完成时,会触发该事件,然后事件队列会按照一定的顺序执行这些事件对应的处理函数,从而实现异步编程。

这个事件队列的实现就是基于发布订阅模式的。具体来说,Promise 对象会维护一个“等待队列”,当异步操作完成时,会遍历这个等待队列,依次执行其中的回调函数。这个过程其实就是一个发布订阅的过程:Promise 对象相当于“发布者”,而等待队列中的回调函数相当于“订阅者”。当异步操作完成时,Promise 对象就会向等待队列中的所有订阅者发布事件,告诉它们该事件已经发生了,然后订阅者就会执行自己的回调函数。

因此,尽管发布订阅模式和 Promise 并不是同一层次的概念,但在 Promise 的实现中,确实使用了发布订阅模式来处理异步操作的结果。

链式调用

1)promise的回调只能被捕获一次
2)在then函数加上return,后面的then函数才能继续捕获到

在 Promise 中,then 方法返回的是一个新的 Promise 对象,因此我们可以通过链式调用 then 方法来依次处理异步操作的结果。而关于 Promise 链式调用中的两个问题,可以分别进行如下解释:

  1. Promise 的回调只能被捕获一次

这是因为 Promise 的状态一旦确定了就不会再改变,因此 Promise 的回调也只会被触发一次。在 Promise 中,我们可以通过 then 方法来注册回调函数,在 Promise 对象状态改变时触发这些回调函数。当 Promise 对象状态从“等待”变为“已完成”时,会触发 onFulfilled 回调函数;当 Promise 对象状态从“等待”变为“已拒绝”时,会触发 onRejected 回调函数。在注册回调函数时,我们可以选择只注册 onFulfilled 回调函数,也可以同时注册 onFulfilled 和 onRejected 回调函数,但无论如何,这些回调函数只会被触发一次。

  1. 在 then 函数加上 return,后面的 then 函数才能继续捕获到

这是因为 then 方法返回的是一个新的 Promise 对象,如果没有在 then 方法中显式返回一个值,那么后续的 then 方法就无法接收到前面 then 方法的返回值。具体来说,在 Promise 链式调用中,每个 then 方法都会返回一个新的 Promise 对象,如果我们在 then 方法中显式返回一个值,那么这个新的 Promise 对象的状态就会被设置为“已完成”,并且值就是我们返回的那个值;如果我们在 then 方法中抛出一个异常,那么这个新的 Promise 对象的状态就会被设置为“已拒绝”,并且值就是我们抛出的异常。如果我们没有在 then 方法中显式返回一个值,那么后续的 then 方法就会接收到一个值为 undefined 的 Promise 对象,它的状态为“已完成”。因此,如果我们希望后续的 then 方法能够接收到前面 then 方法的返回值,就需要在前面的 then 方法中显式返回一个值。

链式调用示例

// 只有第一个then函数能捕获到结果,第二个then打印undefined
let pro = new Promise((resolve, reject) => resolve(1));
pro.then(res => {
    console.log(res);
  })
  .then(res => {
    console.log(res);
  });


作者:海阔_天空
链接:https://juejin.cn/post/7146973901166215176
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

手写promise

class Promise {
  constructor(fn) {
    // resolve时的回调函数列表
    this.resolveTask = [];
    // reject时的回调函数列表
    this.rejectTask = [];
    // state记录当前状态,共有pending、fulfilled、rejected 3种状态
    this.state = "pending";
    let resolve = value => {
      // state状态只能改变一次,resolve和reject只会触发一种
      if (this.state !== "pending") return;
      this.state = "fulfilled";
      this.data = value;
      // 模拟异步,保证resolveTask事件先注册成功,要考虑在Promise里面写同步代码的情况
      setTimeout(() => {
        this.resolveTask.forEach(cb => cb(value));
      });
    };
    let reject = err => {
      if (this.state !== "pending") return;
      this.state = "rejected";
      this.error = err;
      // 保证rejectTask事件注册成功
      setTimeout(() => {
        this.rejectTask.forEach(cb => cb(err));
      });
    };

    // 关键代码,执行fn函数
    try {
      fn(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }

  then(resolveCallback, rejectCallback) {
    // 解决链式调用的情况,继续返回Promise
    return new Promise((resolve, reject) => {
      // 将then传入的回调函数,注册到resolveTask中
      this.resolveTask.push(() => {
        // 重点:判断resolveCallback事件的返回值
        // 假如用户注册的resolveCallback事件又返回一个Promise,将resolve和reject传进去,这样就实现控制了链式调用的顺序
        const res = resolveCallback(this.data);
        if (res instanceof Promise) {
          res.then(resolve, reject);
        } else {
          // 假如返回值为普通值,resolve传递出去
          resolve(res);
        }
      });

      this.rejectTask.push(() => {
        // 同理:判断rejectCallback事件的返回值
        // 假如返回值为普通值,reject传递出去
        const res = rejectCallback(this.error);
        if (res instanceof Promise) {
          res.then(resolve, reject);
        } else {
          reject(res);
        }
      });
    });
  }
}

// 测试
// 打印结果:依次打印1、2
new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1);
  }, 500);
}).then(
    res => {
      console.log(res);
      return new Promise(resolve => {
        setTimeout(() => {
          resolve(2);
        }, 1000);
      });
    }
  ).then(data => {
      console.log(data);
    });

作者:海阔_天空
链接:https://juejin.cn/post/7146973901166215176
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

手写race、all

race:返回promises列表中第一个执行完的结果
all:返回promises列表中全部执行完的结果

class Promise {
  // race静态方法,返回promises列表中第一个执行完的结果
  static race(promises) {
    return new Promise((resolve, reject) => {
      for (let i = 0; i < promises.length; i++) {
        // Promise.resolve包一下,防止promises[i]不是Promise类型
        Promise.resolve(promises[i])
          .then(res => {
            resolve(res);
          })
          .catch(err => {
            reject(err);
          });
      }
    });
  }

  // all静态方法, 返回promises列表中全部执行完的结果
  static all(promises) {
    let result = [];
    let index = 0;
    return new Promise((resolve, reject) => {
      for (let i = 0; i < promises.length; i++) {
        Promise.resolve(promises[i])
          .then(res => {
            // 输出结果的顺序和promises的顺序一致
            result[i] = res; 
            index++;
            if (index === promises.length) {
              resolve(result);
            }
          })
          .catch(err => {
            reject(err);
          });
      }
    });
  }
}


作者:海阔_天空
链接:https://juejin.cn/post/7146973901166215176
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

手写retry

retry的作用,当接口请求失败后,每间隔几秒,再重发几次

/* 
* @param {function} fn - 方法名
* @param {number} delay - 延迟的时间
* @param {number} times - 重发的次数
*/
function retry(fn, delay, times) {
  return new Promise((resolve, reject) => {
    function func() {
      Promise.resolve(fn()).then(res => {
          resolve(res);
        })
        .catch(err => {
          // 接口失败后,判断剩余次数不为0时,继续重发
          if (times !== 0) {
            setTimeout(func, delay);
            times--;
          } else {
            reject(err);
          }
        });
    }
    func();
  });
}


作者:海阔_天空
链接:https://juejin.cn/post/7146973901166215176
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

史上最最最详细的手写Promise教程

async、await

作用:用同步方式,执行异步操作

总结

1)async函数是generator(迭代函数)的语法糖

2)async函数返回的是一个Promise对象,有无值看有无return值

3)await关键字只能放在async函数内部,await关键字的作用 就是获取Promise中返回的resolve或者reject的值

4)async、await要结合try/catch使用,防止意外的错误

generator

1)generator函数跟普通函数在写法上的区别就是,多了一个星号*

2)只有在generator函数中才能使用yield,相当于generator函数执行的中途暂停点

3)generator函数是不会自动执行的,每一次调用它的next方法,会停留在下一个yield的位置

async、await示例

作者:海阔_天空
链接:https://juejin.cn/post/7146973901166215176
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Css 基础

position

CSS中的position属性有4个值:static、relative、absolute和fixed。其中,static是默认值,不会改变元素的位置。而absolute和relative则会改变元素的位置。

- absolute:绝对定位,相对于最近的已定位的父元素来定位(如果没有已定位的父元素,则相对于文档定位)。绝对定位的元素不会保留任何空间。
- relative:相对定位,相对于元素原来的位置来定位。相对定位的元素仍然会占据原来的空间,不会影响其他元素的位置

要区分它们,可以通过以下方法:

1. 父元素是否有定位属性:如果父元素有定位属性(如position: relative/absolute/fixed),那么子元素使用的定位属性就是相对于父元素而言的。如果父元素没有定位属性,则子元素的定位属性相对于文档而言。

2. 是否占据空间:绝对定位的元素不会占据原来的空间而相对定位的元素会占据原来的空间

3. 是否改变元素原来的位置:相对定位是相对于元素原来的位置来定位的,而绝对定位是相对于最近的已定位的父元素来定位的。

总的来说,absolute和relative都是用来控制元素位置的,但是表现不同,需要根据具体的需求和场景选择使用哪种定位属性。

你可能感兴趣的:(前端知识查缺补漏,前端)