ECMAScript 闭包

前言

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) 会在 申明环境记录 的前面, 这是因为

显然,函数表达式实例化时 为 函数对象 在前,只有有了函数调用,才能调用嘛。 所以,

  • 在实例化 函数对象时 , 函数对象的 [[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]],形成作用域链, 于是就能访问到 外部环境的标志符,也就是所谓的 "闭包"。
  • 函数表达式 实例化为函数对象时,可能会新建一个申明环境记录,用于保存函数名。
相关推荐
阿虎儿1 分钟前
MCP
前端
layman052814 分钟前
node.js 实战——(fs模块 知识点学习)
javascript·node.js
毕小宝14 分钟前
编写一个网页版的音频播放器,AI 加持,So easy!
前端·javascript
万水千山走遍TML15 分钟前
JavaScript性能优化
开发语言·前端·javascript·性能优化·js·js性能
Aphasia31115 分钟前
react必备JS知识点(一)——判断this指向👆🏻
前端·javascript·react.js
会飞的鱼先生31 分钟前
vue3中slot(插槽)的详细使用
前端·javascript·vue.js
小小小小宇1 小时前
一文搞定CSS Grid布局
前端
0xHashlet1 小时前
Dapp实战案例002:从零部署链上计数器合约并实现前端交互
前端
知心宝贝1 小时前
🔍 从简单到复杂:JavaScript 事件处理的全方位解读
前端·javascript·面试
安余生大大1 小时前
关于Safari浏览器在ios<16.3版本不支持正则表达式零宽断言的解决办法
前端