文章

【施工中】JS 闭包

说一说 JS 的闭包?(要求你从零开始介绍你对闭包的简要理解)

什么是闭包?闭包的优缺点?

首先我们要搞明白 闭包 的概念,然后从。。。展开。

闭包概念

闭包可以用来在一个函数与一组“私有”变量之间建立关联关系。

让我们先从词法作用域开始。

作用域

作用域- MDN Web 文档术语表

JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。

因为 JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了。

而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。

……

——JavaScript深入之词法作用域和动态作用域#3

绑定

深度理解for循环的作用域let 的「创建」过程被提升了

首先我们来看一个代码例子:

1
2
3
4
5
6
7
8
9
10
11
12
/**
 * 最后输出结果:
 * 3 3 3
 * 0 1 2
 */
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i));
}

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i));
}

setTimeOut 是异步函数,回调要等到主线程同步代码执行完才会进入任务队列。且 JS 闭包捕获的不是变量值,而是变量引用(内存地址)

for(var …) → 所有迭代共享一个 i

  • 这个 i 绑定在外层函数作用域(如果在全局就是全局作用域)。
  • 每次 ()=>console.log(i) 形成闭包时,并不会复制 i 的值,而是记录“到外层作用域里 i 的引用”。
  • 所以整个循环过程只有一个 i,三个闭包都引用这同一个。

for(let …) → 每次迭代都会创建一个新的块作用域,新的 i

关于异步回调函数何时执行?

从这里可以看出:是在所有同步代码执行完后,才开始执行异步队列中的函数。

image.png

ES6 对于 for 循环中 let、const 的特殊规则

javascript - Am I right about LexicalEnvironment and …

当我搞不清楚 “为何 for 循环中,let 和 var 会有如此大的差别” 时,我在 GPT 的回答中得知:

……但对于 let/const 声明的变量,会有一个额外的机制。

规范规定:当循环头里用了 letconst 声明时,每次迭代都会新建一个环境记录 (environment record)。

……底层发生的是:

  • for 初始化时,创建了一个“循环环境”。
  • 每次进入一次迭代,JS 引擎会 克隆一份上一次迭代的环境,得到一个新的环境记录。
  • 在新的环境里,i 绑定到一个新的存储槽(slot)。
    • 如果是第一次迭代,存储槽里就是初始化的 0
    • 如果是后续迭代,会把上一次迭代结束时的 i 的值复制进来(比如 1, 2…)。

这样,每次迭代的 i 虽然名字相同,但其实指向不同的内存槽。

这就是为什么 闭包能“记住”当时那一轮的 i。 而:

  • var 声明不创建迭代环境,所有迭代共用一个环境记录
  • 所以闭包里捕获到的 i 始终是那个唯一的槽。

然后我在 mdn 关于 for 的描述文档中找到了相关资料:

for - JavaScript | MDN - Mozilla

更准确地说,let 声明是 for 循环特有的——如果 initialization 是 let 声明,那么每次循环体执行完毕后,都会发生以下事情:

  1. 使用 let 声明新的变量会创建一个新的词法作用域
  2. 上次迭代的绑定值用于重新初始化新变量
  3. afterthought 在新的作用域中执行

因此,在 afterthought 中重新分配新变量不会影响上一次迭代的绑定。

新的词法作用域会在 initialization 之后、condition 第一次被判定之前创建。

JS 闭包与早期语言闭包の对比

本文由作者按照 CC BY 4.0 进行授权