Skip to content

执行上下文/作用域链/闭包 ​

1. 对闭包的理解 ​

闭包是函数和它定义时所在词法环境的组合,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。

闭包有两个常用的用途;

  • 通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。

  • 使已经运行结束的函数上下文中的词法环境继续留在内存中,因为闭包函数保留了这个词法环境的引用,所以只要闭包还在,这个词法环境不会被回收。

    词法环境:记录“当前作用域里有哪些变量”的一个内部结构

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

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

在 JS 中,闭包存在的意义就是让我们可以间接访问函数内部的变量。 经典面试题:var 声明的循环变量,被多个闭包共享的问题

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

首先因为 setTimeout 注册一个定时器,到时间后把回调放到宏任务队列。所以整个流程是:

循环开始

第 1 次循环:调用 setTimeout,注册一个定时器(1 秒后触发)

第 2 次循环:调用 setTimeout,注册一个定时器(2 秒后触发)

...

第 5 次循环:调用 setTimeout,注册一个定时器(5 秒后触发)

循环结束,i 变成 6

主线程空了,开始处理定时器

1 秒后,第 1 个回调触发,输出 i(此时 i 是 6)

2 秒后,第 2 个回调触发,输出 i(还是 6)

...

所以会输出五次 6。解决办法有三种:

  • 第一种是使用闭包+立即执行函数的方式
javascript
for (var i = 1; i <= 5; i++) {
  (function(j) {
    setTimeout(function timer() {
      console.log(j)
    }, j * 1000)
  })(i)
}
//立即执行函数表达式(function(j) {...})(i) : 传入i作为实参立即调用function

使用了立即执行函数,每次循环创建新作用域,把 i 的值固定成参数 j(由于闭包使 j 在立即执行函数执行完后不会被销毁,),当下次执行 timer 这个闭包的时候,就可以使用外部函数的变量 j,从而达到目的。

  • 第二种就是使用 setTimeout 的第三个参数,这个参数会在延迟时间到了之后,作为实参传给 timer。
javascript
for (var i = 1; i <= 5; i++) {
  setTimeout(
    function timer(j) {
      console.log(j)
    },
    i * 1000,
    i //传参
  )
}
  • 第三种就是使用 let 定义 i 了来解决问题了,这个也是最为推荐的方式 let 是块级作用域,规范规定 for 循环中 let 每次迭代创建一个新绑定,所以每个 timer 捕获的是各自迭代的 i。
javascript
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声明的变量有变量提升,但是存在暂时性死区,声明前访问会报错;也不可以重复声明
  • 在循环中比较适合绑定块级作用域,这样就可以把声明的计数器变量限制在循环内部。

作用域链:

在当前作用域中查找所需变量,但是该作用域没有这个变量,那这个变量就是自由变量。如果在自己作用域找不到该变量就去上级作用域查找,依次向上级作用域查找,直到访问到全局词法环境就被终止,这一层层的关系就是作用域链。

作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到外层环境的变量和函数。

作用域链本质上是词法环境的链。每个词法环境包含环境记录和外层引用。

当查找一个变量时,如果当前执行环境中没有找到,可以沿着作用域链向后查找。

3. 对执行上下文的理解 ​

可以理解为: 执行上下文 = 代码执行时的环境,里面记录了变量、this、外层作用域。

1. 执行上下文类型 ​

(1)全局执行上下文

任何不在函数内部的代码,都在全局执行上下文中执行。一个程序中只有一个全局执行上下文

(2)函数执行上下文

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

(3)eval函数执行上下文(不常用

执行在eval函数中的代码,会创建属于它自己的执行上下文。

2. 执行上下文栈 ​

  • JavaScript引擎使用执行上下文栈来管理执行上下文
  • 当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();
//执行顺序
// 1. first 被调用,输出 'Inside first function'
// 2. first 内部调用 second,输出 'Inside second function'
// 3. second 执行完,回到 first,输出 'Again inside first function'
// 最终输出:
// Inside first function
// Inside second function
// Again inside first function

3. 创建执行上下文 ​

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

1)创建阶段

(1)this绑定

  • 在全局执行上下文中,this 指向全局对象window(浏览器)/global(Node)。
  • 在函数执行上下文中,this指向取决于函数如何调用。如果它被一个引用对象调用,那么 this 会指向那个对象,否则在非严格模式下 this 指向全局对象,严格模式下 this 指向 undefined

(2)创建词法环境

  • 词法环境用来记录当前作用域有哪些变量。
  • 词法环境:处理 let、const 声明

(3)创建变量环境

  • 变量环境只是词法环境的一种特例
  • 变量环境:专门处理 var 声明和函数声明

2)执行阶段

此阶段会按顺序执行代码,完成变量的赋值、函数的调用等。

简单来说执行上下文就是指:

在执行 JS 代码之前,引擎会先编译代码,然后创建全局执行上下文。创建时会把即将执行的变量、函数声明都拿出来,变量先赋值为 undefined,函数先声明好可使用。这一步完成后,才开始正式执行程序。

在一个函数执行之前,也会创建一个函数执行上下文,跟全局执行上下文类似。区别在于:

  • 全局上下文的 this 固定指向全局对象;函数上下文的 this 取决于调用方式
  • 函数上下文有 arguments,全局上下文没有

总结:

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

总结: ​

  1. 执行上下文是代码执行时的环境,每次执行都会创建。
  2. 作用域是变量能被访问的范围,由词法环境决定。
  3. 闭包是函数 + 它定义时的词法环境,让外层词法环境在函数执行完后不被回收。

三者关系:执行上下文里包含词法环境,词法环境决定作用域,闭包捕获并保留词法环境。

Code. Log. Share.