前言
ECMAScript标准是深入学习JavaScript原理最好的资料,没有其二。
通过增加对ECMAScript语言的理解,理解javascript现象后面的逻辑,提升个人编码能力。
欢迎关注和订阅专栏 **[重学前端-ECMAScript协议上篇]
闭包的定义
闭包是一种 ECMAScript规范类型, 其在协议的第六章有定义 6.2.8 The Abstract Closure Specification Type。翻译为中文 就是 抽象闭包规范类型,其协议原文描述也是相当抽象。

该文章先通过看例子来理解闭包,这描述不看也罢。
为了方便,本章节全部是普通脚本,所以省略掉 <script>
相关标签。
函数对象 function object
函数对象可以理解为开发者编写的 function
, 理解闭包,本质只要理解函数对象的一个属性即可。 她就是 [Environment]]
其在协议里有详细的定义 10.2 ECMAScript Function Objects 。

翻译一下描述: 这是一个函数在其被创建时所封闭的环境记录。当执行函数内部的代码时,此环境记录被用作外部环境。
理解这句话,需要您知道一个点,就是 函数代码执行时,会新建一个函数函数环境记录。

- 第一步会新建一个 函数环境记录 env
- 第六步会把 env的
[[OuterEnv]]
指向函数的F.[[Environment]]
, 这就是 当执行函数内部的代码时,此环境记录被用作外部环境记录。
函数的 [[Environment]]
属性又是什么呢? 一般来说,函数对象在哪个环境记录创建的,就是哪个。
比如如下,函数对象 log 的 [[Environment]]
就是全局环境记录。
javascript
javascript
复制代码
function log(){
console.log(name);
}
函数对象访问外部标志符的机制
先来段简单的代码,热热身, 一起看看如何访问外部变量
javascript
javascript
复制代码
"use strict"
const name = 10;
function log(){
console.log(name);
}
log();
当代码执行到 log()
(未进入函数)
log标志符的对应的函数对象,是在全局环境记录中创建的, 所以其 [[Environment]]
指向的是全局环境记录。
关系图如下:

当函数评估执行时
会新建一个 函数执行上下文,也会新建一个 函数环境记录
- 新建函数环境记录 localEnv , 同时
[[OuterEnv]]
指向函数对象的的[[[Environment]]]
, 即全局环境记录 - 执行上下文的 LexicalEnvironment 和 VariableEnvironment 指向新的函数环境记录,即 localEnv


于是关系图变为

函数申明环境与全局环境记录链起来了,于是就能从全局环境记录中的 申明环境记录中找到 name
绑定关系,并进行取值输出了。
函数返回函数表达式
进入正题,击碎闭包吧,少年,看看函数返回函数表达式的情况,如何访问外部变量。
javascript
javascript
复制代码
"use strict";
function getFun(){
var name = "name";
return function(){
return name;
}
}
const getNameFun = getFun();
getNameFun();
getFun
返回的函数是函数表达式,对应协议描述 FunctionExpression, 先简单了解一下
FunctionExpression 函数表达式
全局顶层代码都还是比较好理解的,重点是理解 getFun 函数的 解析节点内容如下, 去掉location等信息如下:
json
javascript
复制代码
{
"type": "FunctionDeclaration",
"strict": true,
"BindingIdentifier": {
"type": "BindingIdentifier",
"strict": true,
"name": "getFun"
},
"FormalParameters": [],
"FunctionBody": {
"type": "FunctionBody",
"strict": true,
"directives": [],
"FunctionStatementList": [
{
"type": "VariableStatement",
"strict": true,
"VariableDeclarationList": [
{
"type": "VariableDeclaration",
"strict": true,
"BindingIdentifier": {
"type": "BindingIdentifier",
"strict": true,
"name": "name"
},
"Initializer": {
"type": "StringLiteral",
"strict": true,
"value": "name"
}
}
]
},
{
"type": "ReturnStatement",
"strict": true,
"Expression": {
"type": "FunctionExpression",
"strict": true,
"BindingIdentifier": null,
"FormalParameters": [],
"FunctionBody": {
"type": "FunctionBody",
"strict": true,
"directives": [],
"FunctionStatementList": [
{
"type": "ReturnStatement",
"strict": true,
"Expression": {
"type": "IdentifierReference",
"strict": true,
"escaped": false,
"name": "name"
}
}
]
}
}
}
]
}
}
完整走完之前 需要理解一下 getFun的 返回语句 ReturnStatement。

getFun 返回函数的表达式 对应的是 ReturnStatement
, 返回的匿名函数部分 对应 FunctionExpression
,实例化逻辑逻辑参见15.2.5 Runtime Semantics: InstantiateOrdinaryFunctionExpression。
看协议写的返回的是 closure,本质 return 语句返回的依旧是 函数对象 function object。

这里无非是说明两点
- return 语句后面的是 函数表达式 FunctionExpression
- return 最后的返回值 是 函数对象
明白这两点,那函数返回函数的闭包就很好理解了。
getFun 返回前
return 返回的匿名函数,不会在函数申明环境记录有绑定关系。

const getNameFun = getFun() 赋值后
getNameFun 赋值成功后
- getFun已经执行完后,getFun对应的上下文出栈
- 申明环境记录的 getNameFun 的值经是函数对象,其
[[Environment]]
属性依旧指向 getFun执行时创建的函数执行环境

getNameFun() 代码执行
- getNameFun 代码评估会创建新的执行上下文
- getNameFun 代码评估会创建新的函数环境记录,并且会把当前函数执行上下文的
[[outerEnv]]
指向 getNameFun 函数对象的[[Environment]]
,即getFun 执行时创建的函数环境记录。
下面蓝色的线,就是执行上下文和环境记录的变化。
这时候 getNameFun
就能从 LexicalEnvironment
开始查找,在 getFun
环境记录上找到 name 标志符绑定关系,并进行取值了。

小结
从函数返回函数的例子,可以一些总结
- 闭包本质就是 环境记录的链, 即常会说的作用域链。
- 函数执行时会创建函数执行上下文,执行完毕后会销毁该上下文。
- 函数(getFun)执行时会创建函数环境记录,该函数顶层代码 实例化的内部函数对象时,函数对象[[Environment]] 会指向该环境记录。 当其被调用时,会通过该属性把环境记录链接起来,形成所谓的闭包。
具名函数表达式 和 匿名函数表达式
现在将上面的代码做一点点更改, 返回不再是匿名函数,而是具名(getNameFunInner)函数 。

javascript
javascript
复制代码
"use strict";
function getFun(){
var name = "name";
return function getNameFunInner(){
return name;
}
}
const getNameFun = getFun();
getNameFun();
那么,这和返回匿名函数有区别嘛?
答 : 有区别。
FunctionExpression 实例化逻辑逻辑参见15.2.5 Runtime Semantics: InstantiateOrdinaryFunctionExpression。

如图具名的 FunctionExpression 会 新建一个申明环境记录(红色圈出),用于保存 函数名的 绑定关系,这是为什么呢?

翻译: 在FunctionExpression中的BindingIdentifier(绑定标识符,即函数名)可以从该FunctionExpression的FunctionBody(函数体)内部引用,这样允许函数自我递归调用。然而,与FunctionDeclaration不同的是,FunctionExpression中的BindingIdentifier不能从包围它的作用域中被引用,也不会影响这个外部作用域。
再简化就是为了,能在 函数表达式中 访问自己。
javascript
javascript
复制代码
"use strict";
function getFun(){
var name = "name";
return function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
}
const factorial = getFun();
factorial(10);
javascript
javascript
javascript
复制代码
"use strict";
function getFun(){
var name = "name";
return function ??(n) {
if (n <= 1) return 1;
return n * ??(n - 1);
}
}
const factorial = getFun();
factorial(10);
所以下面代码的
javascript
javascript
复制代码
"use strict";
function getFun(){
var name = "name";
return function getNameFunInner(){
return name;
}
}
const getNameFun = getFun();
debugger
getNameFun();
getNameFun
被调用前,即运行到 debugger位置:
- 虽然 getFun 已经执行完毕了,但是对应的环境记录依然是存在的

在getNameFun
代码执行时,执行上下文,环境记录,标志符绑定关系的关系图如下:

为什么 函数申明环境记录(getNameFun) 会在 申明环境记录 的前面, 这是因为
- 申明环境记录是在函数表达式实例化时 为 函数对象 时创建的, 参见15.2.5 Runtime Semantics: InstantiateOrdinaryFunctionExpression
- 函数申明环境记录是 函数对象 准备调用时 创建的 ,参见 10.2.1.1 PrepareForOrdinaryCall ( F, newTarget )
显然,函数表达式实例化时 为 函数对象 在前,只有有了函数调用,才能调用嘛。 所以,
- 在实例化 函数对象时 , 函数对象的
[[Environment]]
指向的是 申明环境记录 - 函数对象调用时,新建 函数申明环境记录,其
[[outerEnv]]
指向 函数对象的[[Environment]]
,即申明环境记录。于是链路形成。
于是,具名函数表达式 就比 匿名函数表达式 多了一个申明环境记录,链路自然也变成长了。
那么,下次你会写具名还是匿名函数表达式呢?

思考
如果先进行函数申明,然后再返回函数标志符,当代码执行到 return name
,执行上下文,环境记录,标志符绑定关系图又是怎么呢?
javascript
javascript
复制代码
"use strict";
function getFun(){
var name = "name";
function getNameFunInner(){
return name;
}
return getNameFunInner;
}
const getNameFun = getFun();
getNameFun();
如果是如下代码,当代码执行到 return name
,执行上下文,环境记录,标志符绑定关系图又是怎么呢?
javascript
javascript
复制代码
"use strict";
function getFun(){
var name = "name";
const getNameFunInner = function getNameFunInnerExression(){
return name;
}
return getNameFunInner;
}
const getNameFun = getFun();
getNameFun();
提示:
- getFun函数 本质最后返回的值 都是 函数对象
- 函数对象的
[[Environment]]
保存着 相关的环境记录,通常是实例化自己的环境记录。 - 函数代码执行时,会新建执行上下文 和 函数环境记录,并且把 函数环境记录的
outerEnv
指向 函数对象的[[Environment]]
,形成作用域链, 于是就能访问到 外部环境的标志符,也就是所谓的 "闭包"。 - 函数表达式 实例化为函数对象时,可能会新建一个申明环境记录,用于保存函数名。