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]],形成作用域链, 于是就能访问到 外部环境的标志符,也就是所谓的 "闭包"。
  • 函数表达式 实例化为函数对象时,可能会新建一个申明环境记录,用于保存函数名。
相关推荐
OpenTiny社区5 分钟前
把 SearchBox 塞进项目,搜索转化率怒涨 400%?
前端·vue.js·github
编程猪猪侠34 分钟前
Tailwind CSS 自定义工具类与主题配置指南
前端·css
qhd吴飞38 分钟前
mybatis 差异更新法
java·前端·mybatis
YGY Webgis糕手之路1 小时前
OpenLayers 快速入门(九)Extent 介绍
前端·经验分享·笔记·vue·web
患得患失9491 小时前
【前端】【vueDevTools】使用 vueDevTools 插件并修改默认打开编辑器
前端·编辑器
ReturnTrue8681 小时前
Vue路由状态持久化方案,优雅实现记住表单历史搜索记录!
前端·vue.js
UncleKyrie1 小时前
一个浏览器插件帮你查看Figma设计稿代码图片和转码
前端
遂心_1 小时前
深入解析前后端分离中的 /api 设计:从路由到代理的完整指南
前端·javascript·api
你听得到111 小时前
Flutter - 手搓一个日历组件,集成单日选择、日期范围选择、国际化、农历和节气显示
前端·flutter·架构
风清云淡_A1 小时前
【REACT18.x】CRA+TS+ANTD5.X封装自定义的hooks复用业务功能
前端·react.js