闭包和词法环境
闭包是个老生常谈的问题,所以写个记录下什么是闭包,闭包怎么来的,这里就从代码块开始说起。
代码块
{...}
花括号中声明的变量对于外部是不可见的。- 其中函数
function () {...}
;if(){...}
;for(let i;;){...}
;while{...}
// 例 1 if (true) { const testA = 1; } console.log(testA); //testA is not defined; // 例 2 { const testB = 2; }; console.log(testAB); //testB is not defined; // 例 3 for (let i = 0; i < 5; i++) { console.log(i); } console.log(i); // i is not defined; let testC; console.log(testC); // undefined
for
循环从视觉上看,let i
位于{...}
之外。但是 for 构造在这里很特殊:在其中声明的变量被视为块的一部分。
- 其中函数
块级作用域
var
没有块级作用域且会进行变量声明提升,但赋值不会。function ku() { // 变量提升 // var phrase; // 第一行(a) phrase = 'hello'; console.log(phrase); var phrase; // hello 因为会变量提升 提升到第一行(a) } ku(); // 没有块级作用域 无视代码块 say = 'world'; if (false) { var say; const habit = 1; } console.log(say); // world ---> 没有块级作用域 // const let 有块级作用域 console.log(habit); // habit is not a defined;
- 模拟块级作用域:使用自执行函数;
- 所有
function开头
都会被认为是函数声明:所以可以使用(), +, !
形式写函数表达式。
!function(){}( console.log(1) ); +function(){}( console.log(2) ); (function(){ console.log(3) }()); (function(){ console.log(4) })();
- 所有
词法环境
-
在 JavaScript 中,每个运行的函数,代码块 {...} 以及整个脚本,都有一个被称为 词法环境(Lexical Environment) 的内部(隐藏)的关联对象。
-
词法环境对象由两部分组成:
- 环境记录(Environment Record) —— 一个存储所有局部变量作为其属性(包括一些其他信息,例如 this 的值)的对象。
- 对 外部词法环境 的引用,与外部代码相关联。
-
一个“变量”只是 环境记录 这个特殊的内部对象的一个属性。“获取或修改变量”意味着“获取或修改词法环境的一个属性”。
-
函数声明
- 一个函数其实也是一个值,就像变量一样。
- 不同之处在于函数声明的初始化会被立即完成。
- 当创建了一个词法环境(Lexical Environment)时,函数声明会立即变为即用型函数(不像 let 那样直到声明处才可用)。
- 这就是为什么我们可以在(函数声明)的定义之前调用函数声明。
- 一个变量从技术的角度来讲是存在的,但是在 let 之前还不能使用。
- 从程序执行进入代码块(或函数)的那一刻起,变量就开始进入“未初始化”状态。它一直保持未初始化状态,直至程序执行到相应的
let
“死区”语句。
- 从程序执行进入代码块(或函数)的那一刻起,变量就开始进入“未初始化”状态。它一直保持未初始化状态,直至程序执行到相应的
let x = 1; function func() { // 引擎从函数开始就知道局部变量 x, // 但是变量 x 一直处于“未初始化”(无法使用)的状态,直到结束 let(“死区”) // 因此答案是 error console.log(x); // ReferenceError: Cannot access 'x' before initialization let x = 2; } func();
-
函数每次调用时都会创建一个新的词法环境。以存储改函数运行时的变量。
- 所有的函数在“诞生”时都会记住创建它们的词法环境。从技术上讲,这里没有什么魔法:所有函数都有名为 [[Environment]] 的隐藏属性,该属性保存了对创建该函数的词法环境的引用。
function makeCounter() { let count = 0; return function() { return count++; }; } let counter = makeCounter(); // 新的词法环境 let counter2 = makeCounter(); // 新的词法环境
- counter.[[Environment]] 有对 {count: 0} 词法环境的引用。这就是函数记住它创建于何处的方式,与函数被在哪儿调用无关。[[Environment]] 引用在函数创建时被设置并永久保存。稍后,当调用 counter() 时,会为该调用创建一个新的词法环境,并且其外部词法环境引用获取于 counter.[[Environment]]:
-
当代码要访问一个变量时 —— 首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境。
-
Remark:
- “词法环境”是一个规范对象(specification object):它仅仅是存在于 编程语言规范 中的“理论上”存在的,用于描述事物如何运作的对象。我们无法在代码中获取该对象并直接对其进行操作。
- 但 JavaScript 引擎同样可以优化它,比如清除未被使用的变量以节省内存和执行其他内部技巧等,但显性行为应该是和上述的无差。
垃圾收集
- 通常,函数调用完成后,会将词法环境和其中的所有变量从内存中删除。因为现在没有任何对它们的引用了。与 JavaScript 中的任何其他对象一样,词法环境仅在可达时才会被保留在内存中。
- 但是,如果有一个嵌套的函数在函数结束后仍可达,则它将具有引用词法环境的 [[Environment]] 属性。
- 请注意,如果多次调用 f(),并且返回的函数被保存,那么所有相应的词法环境对象也会保留在内存中。下面代码中有三个这样的函数:
function f() { let value = Math.random(); return function() { alert(value); }; } // 数组中的 3 个函数,每个都与来自对应的 f() 的词法环境相关联 let arr = [f(), f(), f()];
+ 当词法环境对象变得不可达时,它就会死去(就像其他任何对象一样)。换句话说,它仅在至少有一个嵌套函数引用它时才存在。在下面的代码中,嵌套函数被删除后,其封闭的词法环境(以及其中的 value)也会被从内存中删除:
```javascript
function f() {
let value = 123;
return function() {
alert(value);
}
}
let g = f(); // 当 g 函数存在时,该值会被保留在内存中
g = null; // ……现在内存被清理了
- 实际中的优化
- 正如我们所看到的,理论上当函数可达时,它外部的所有变量也都将存在。但在实际中,JavaScript 引擎会试图优化它。它们会分析变量的使用情况,如果从代码中可以明显看出有未使用的外部变量,那么就会将其删除。
- 在 V8(Chrome,Edge,Opera)中的一个重要的副作用是,此类变量在调试中将不可用。
- 当代码执行暂停时,在控制台中输入
alert(value)
。
- 当代码执行暂停时,在控制台中输入
function f() { let value = Math.random(); function g() { debugger; // 在 Console 中:输入 alert(value); No such variable! } return g; } let g = f(); g();
闭包
-
闭包 是指内部函数总是可以访问其所在的外部函数中声明的变量和参数,即使在其外部函数被返回(寿命终结)了之后。在某些编程语言中,这是不可能的,或者应该以特殊的方式编写函数来实现。但是如上所述,在 JavaScript 中,所有函数都是天生闭包的(只有一个例外,将在 "new Function" 语法 中讲到)。
-
也就是说:JavaScript 中的函数会自动通过隐藏的 [[Environment]] 属性记住创建它们的位置,所以它们都可以访问外部变量。
-
在面试时,前端开发者通常会被问到“什么是闭包?”,正确的回答应该是闭包的定义,并解释清楚为什么 JavaScript 中的所有函数都是闭包的,以及可能的关于 [[Environment]] 属性和词法环境原理的技术细节。
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter = makeCounter(); // counter.[[Environment]] 存储了对相应 makeCounter() 调用的词法环境的引用
let counter2 = makeCounter();
alert( counter() ); // 0
alert( counter() ); // 1
alert( counter2() ); // ? ----> 0
alert( counter2() ); // ? ----> 1
counter = null; // ……现在内存被清理了