1、对闭包的理解

闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。 闭包有两个常用的用途;

  • 闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
  • 闭包的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。

比如,函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包。

function A() {
  let a = 1
  window.B = function () {
      console.log(a)
  }
}
A()
B() // 1

在 JS 中,闭包存在的意义就是让我们可以间接访问函数内部的变量。经典面试题:循环中使用闭包解决 var 定义函数的问题

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

首先因为 setTimeout 是个异步函数,所以会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。解决办法有三种:

  • 第一种是使用闭包的方式
for (var i = 1; i <= 5; i++) {
  ;(function(j) {
    setTimeout(function timer() {
      console.log(j)
    }, j * 1000)
  })(i)}

在上述代码中,首先使用了立即执行函数将i传入函数内部,这个时候值就被固定在了参数j上面不会改变,当下次执行timer这个闭包的时候,就可以使用外部函数的变量 j,从而达到目的。

  • 第二种就是使用 setTimeout 的第三个参数,这个参数会被当成 timer 函数的参数传入。
for (var i = 1; i <= 5; i++) {
  setTimeout(
    function timer(j) {
      console.log(j)
    },
    i * 1000,
    i
  )
}
  • 第三种就是使用 let 定义i了来解决问题了,这个也是最为推荐的方式
for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

2、对作用域、作用域链的理解

1)全局作用域和函数作用域

(1)全局作用域

  • 最外层函数和最外层函数外面定义的变量拥有全局作用域
  • 所有未定义直接赋值的变量自动声明为全局作用域
  • 所有window对象的属性拥有全局作用域
  • 全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空间,容易引起命名冲突。

(2)函数作用域

  • 函数作用域声明在函数内部的变量,一般只有固定的代码片段可以访问到
  • 作用域是分层的,内层作用域可以访问外层作用域,反之不行

2)块级作用域

  • 使用ES6中新增的let和const指令可以声明块级作用域,块级作用域可以在函数中创建也可以在一个代码块中的创建(由{ }包裹的代码片段)
  • let和const声明的变量不会有变量提升,也不可以重复声明
  • 在循环中比较适合绑定块级作用域,这样就可以把声明的计数器变量限制在循环内部。

作用域链: 在当前作用域中查找所需变量,但是该作用域没有这个变量,那这个变量就是自由变量。如果在自己作用域找不到该变量就去父级作用域查找,依次向上级作用域查找,直到访问到window对象就被终止,这一层层的关系就是作用域链。 作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到外层环境的变量和函数。 作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。 当查找一个变量时,如果当前执行环境中没有找到,可以沿着作用域链向后查找。

3、对执行上下文的理解

1)执行上下文类型

(1)全局执行上下文

任何不在函数内部的都是全局执行上下文,它首先会创建一个全局的window对象,并且设置this的值等于这个全局对象,一个程序中只有一个全局执行上下文。

(2)函数执行上下文

当一个函数被调用时,就会为该函数创建一个新的执行上下文,函数的上下文可以有任意多个。

(3)eval函数执行上下文

执行在eval函数中的代码会有属于他自己的执行上下文

2)执行上下文栈

  • JavaScript引擎使用执行上下文栈来管理执行上下文
  • 当JavaScript执行代码时,首先遇到全局代码,会创建一个全局执行上下文并且压入执行栈中,每当遇到一个函数调用,就会为该函数创建一个新的执行上下文并压入栈顶,引擎会执行位于执行上下文栈顶的函数,当函数执行完成之后,执行上下文从栈中弹出,继续执行下一个上下文。当所有的代码都执行完毕之后,从栈中弹出全局执行上下文。
let a = 'Hello World!';
function first() {
  console.log('Inside first function');
  second();
  console.log('Again inside first function');
}
function second() {
  console.log('Inside second function');
}
first();
//执行顺序
//先执行second(),在执行first()
// 结果
// Inside first function
// Inside second function
// Again inside first function

3)创建执行上下文

创建执行上下文有两个阶段:创建阶段执行阶段

(1)创建阶段

this绑定

  • 在全局执行上下文中,this指向全局对象(window对象)
  • 在函数执行上下文中,this指向取决于函数如何调用。如果它被一个引用对象调用,那么 this 会被设置成那个对象,否则 this 的值被设置为全局对象或者 undefined

创建词法环境组件

  • 词法环境是一种有标识符——变量映射的数据结构,标识符是指变量/函数名,变量是对实际对象或原始数据的引用。
  • 词法环境的内部有两个组件:加粗样式:环境记录器:用来储存变量个函数声明的实际位置外部环境的引用:可以访问父级作用域

创建变量环境组件

  • 变量环境也是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。

(1)执行阶段

简单来说执行上下文就是指: 在执行一点JS代码之前,需要先解析代码。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。这一步执行完了,才开始正式的执行程序。 在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出this、arguments和函数的参数。

  • 全局上下文:变量定义,函数声明
  • 函数上下文:变量定义,函数声明,this,arguments

4、函数this的指向

this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。多数情况下this指向调用它的对象。其实,this 是在函数被调用时确定的,它的指向取决于函数调用的地方,而不是它被声明的地方(除箭头函数外)。当函数被调用时,会创建一个执行上下文,它包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息,this 就是这个记录的一个属性,它会在函数执行的过程中被用到。

1)this的绑定规则

(1)默认绑定

独立函数调用

function foo() {
  console.log(this.a)
}
var a = 2
foo() // 2
  1. 我们可以看到当调用 foo() 时,this.a 被解析成了全局变量 a。为什么?因为在本例中,函数调用时应用了 **this** 的默认绑定,因此 this 指向全局对象
  2. 那么我们怎么知道这里应用了默认绑定呢?可以通过分析调用位置来看看foo()是如何调 用的。在代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则
  3. 这条规则也解释了上面 count 的代码中,为什么函数里面的this指向了window,因为 foo属于独立函数调用的,触发了默认绑定,从而指向全局**window**。(浏览器中全局是 window对象,node 中是空对象{}
  4. 属于默认绑定规则的还有:
    • 函数调用链(一个函数又调用另外一个函数)
    • 将函数作为参数,传入到另一个函数中

(扩展:如果使用严格模式(strict mode),则不能将全局对象用于默认绑定,因此 this 会绑定到 undefined:)

(2)隐式绑定

一般的对象调用

这一条需要考虑的规则是调用位置是否有上下文对象,或者说是通过某个对象发起的函数调用

function foo() {
  console.log(this.a)
}

const obj = {
  a: 2,
  foo: foo
}

// 通过 obj 对象调用 foo 函数
obj.foo() // 2
  • 调用位置会使用 obj 上下文来引用函数,因此你可以说函数被调用时 obj 对象“拥 有”或者“包含”它。
  • foo() 被调用时,它的前面确实加上了对 obj 的引用。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。因为调用 foo() this 被绑定到 obj 上,因此 this.aobj.a 是一样的。

对象属性引用链

对象属性引用链中只有上一层或者说最后一层在调用位置中起作用。举例来说:

function foo() {
  console.log(this.a)
}

var obj2 = {
  a: 2,
  foo: foo
}

var obj1 = {
  a: 1,
  obj2: obj2
}

obj1.obj2.foo() // 2

隐式丢失

一个最常见的 this 绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上(取决于是否是严格模式) 第一种情况:将对象里的函数赋值给一个变量

function foo() {
  console.log(this.a)
}

var obj = {
  a: 2,
  foo: foo
}

var bar = obj.foo // 函数别名!

var a = 'global' // a 是全局对象的属性
bar() // "global"

虽然 barobj.foo 的一个引用,但是实际上,它引用的是 foo 函数本身,因此此时的 bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。 第二种情况:传入回调函数时

function foo() {
  console.log(this.a)
}

function doFoo(fn) {
  // fn 其实引用的是 foo
  fn() // <-- 调用位置!
}

var obj = {
  a: 2,
  foo: foo
}

var a = 'global'  // a 是全局对象的属性
doFoo(obj.foo)    // "global"

参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一 个例子一样。

结论:隐式绑定的 this,指向调用函数的上下文对象。

(3)显式绑定

使用 call(...) 和 apply(...)

如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢? **可以使用函数的 ****call(..) ****apply(..)** 方法

  • JavaScript 提供的绝大多数函数以及你自己创建的所有函数都可以使用 call(..)apply(..) 方法。
  • apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数。
  • call 传入的参数数量不固定,跟 apply 相同的是,第一个参数也是代表函数体内的 this 指向,从第二个参数开始往后,每个参数被依次传入函数。

这两个方法是如何工作的呢?它们的第一个参数是一个对象,是给 this 准备的,接着在调用函数时将其绑定到 this因为你可以直接指定 **this** 的绑定对象,因此我们称之为显式绑定。 思考以下代码:

function foo() {
  console.log(this.a)
}

var obj = {
  a: 2
}

foo.call(obj) // 2
  • 通过 foo.call(..),我们可以在调用 foo 时强制把它的 this 绑定到 obj 上。
  • 如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作 this 的绑定对象,这个原始值会被转换成它的对象形式(也就是 new String(..)new Boolean(..) 或者 new Number(..))。这通常被称为“装箱”。
手写call(...)
  • 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  • 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
  • 处理传入的参数,截取第一个参数后的所有参数。
  • 将函数作为上下文对象的一个属性。
  • 使用上下文对象来调用这个方法,并保存返回结果。
  • 删除刚才新增的属性。
  • 返回结果。
Function.prototype.myCall = function(context) {
  // 判断调用对象
  if (typeof this !== "function") {
    console.error("type error");
  }
  // 获取参数
  let args = [...arguments].slice(1),
    result = null;
  // 判断 context 是否传入,如果未传入则设置为 window
  context = context || window;
  // 将调用函数设为对象的方法
  context.fn = this;
  // 调用函数
  result = context.fn(...args);
  // 将属性删除
  delete context.fn;
  return result;
};
手写apply(...)
  • 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  • 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
  • 将函数作为上下文对象的一个属性。
  • 判断参数值是否传入
  • 使用上下文对象来调用这个方法,并保存返回结果。
  • 删除刚才新增的属性
  • 返回结果
Function.prototype.myApply = function(context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  let result = null;
  // 判断 context 是否存在,如果未传入则为 window
  context = context || window;
  // 将函数设为对象的方法
  context.fn = this;
  // 调用方法
  if (arguments[1]) {
    result = context.fn(...arguments[1]);
  } else {
    result = context.fn();
  }
  // 将属性删除
  delete context.fn;
  return result;
};

硬绑定(一个函数总是显示的绑定到一个对象上)

由于硬绑定是一种非常常用的模式,所以 ES5 提供了内置的方法 Function.prototype.bind, 它的用法如下

function foo(num) {
  console.log(this.a, num)
  return this.a + num
}

var obj = {
  a: 2
}

// 调用 bind() 方法,返回一个函数
var bar = foo.bind(obj)

var b = bar(3) // 2 3

console.log(b) // 5

调用 bind(...) 方法,会返回一个新函数,那么这个新函数的 this,永远指向我们传入的obj对象

手写bind(...)
  1. 边界判断(this, context)
  2. 将调用的函数设置为对象(传入的context)的方法(改变this指向)
  3. 返回一个函数
  4. 函数里面:处理参数,调用函数,返回结果
Function.prototype.binBind = function(context, ...args) {
  if (typeof this !== 'function') return console.error('Error');
  context = (context!==null && context!==undefined) ? Object(context) : window

  context.fn = this // 这一步不能放在返回的函数里面

  // 返回一个函数
  return function Fn(...args2) {
    // 处理参数,调用函数,返回结果
    const newArr = [...args, ...args2]
    const result = context.fn(...newArr)
    delete context.fn
    return result
  }
}

API调用的 “上下文(内置函数)

第三方库的许多函数,以及 JavaScript 语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和 bind(..) 一样,确保你的回调函数使用指定的 this。例如: **数组方法 ****forEach()**

function foo(el) {
  console.log(el, this.id)
}

var obj = {
  id: 'bin'
};

[1, 2, 3].forEach(foo, obj)

// 输出:
// 1 bin 
// 2 bin 
// 3 bin
  • 调用 foo(..) 时把 this 绑定到 obj 身上

**setTimeout()**

setTimeout(function() {
  console.log(this); // window
}, 1000);
  • 在使用 setTimeout 时会传入一个回调函数,而这个回调函数中的this一般指向 window

结论:显式绑定的 this,指向我们指定的绑定对象。

(4)new 绑定

在 JavaScript 中,普通函数可以使用 new 操作符去调用,此时的普通函数则被称为 “构造函数”。没错,凡是由 new 操作符调用的函数,都称为 “构造函数” 使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

  1. 在内存中创建一个新对象。
  2. 这个新对象内部的[[Prototype]] 特性被赋值为构造函数的 prototype 属性。
  3. 构造函数内部的this 被赋值为这个新对象(即this 指向新对象)。
  4. 执行构造函数内部的代码(给新对象添加属性)。
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
function foo(a) {
  this.a = a
}

var bar = new foo(2)

console.log(bar.a) // 2

使用 new 来调用 foo(..) 时,我们会构造一个新对象并把它绑定到 foo(..) 调用中的 this 上。new 是最后一种可以影响函数调用时 this 绑定行为的方法,我们称之为 new 绑定。

结论:new 绑定的 this,都指向通过 new 调用的函数的实例对象(就是该函数)

2)绑定规则的优先级

默认绑定 < 隐式绑定 < 显示绑定(bind) < new绑定

3)判断this指向

image.png

4)特殊的this指向

(1)箭头函数

箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this。 箭头函数会根据其声明的地方来决定this:

const foo = {
    fn: function () {
    setTimeOut(() => {
      console.log(this)
    }
  }
}

console.log(foo.fn())
// { fn: f }

在箭头函数中,如果多层嵌套。像下面这种情况

function a() {
  return () => {
    return () => {
      console.log(this)
    }
  }
}
console.log(a()()())

由于箭头函数没有this,箭头函数中的this只取决包裹箭头函数的第一个普通函数的this。在这个例子中,应为包裹箭头函数的第一个普通函数是a,所以此时的thiswindow

需要注意,箭头函数的**this**绑定是无法通过**call****apply****bind**方法修改的。且因为箭头函数没有构造函数**constructor**,所以也不可以使用**new**调用,即不能作为构造函数,否则会报错

浙ICP备2021014928号 2023-PRESENT © Eric