什么是闭包?

171 阅读4分钟

这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战

当面试时被问及什么是闭包时,我总会说:内部函数被外部函数包裹时,内部函数可以访问外部变量,这种情况叫做闭包。这么回答总觉得有点不够得劲,可能是因为回答只是对于闭包现象的描述而没有解释其本质,那么闭包是什么东西呢?

遇事不决,先翻翻MDN,其中对于闭包的描述就更专业一些。

当对于js的运行逻辑有疑问,不要猜,去看看ECMAScript的文档。关于闭包,可以重点关注8.11和10这两个章节。

明确定义

  1. Global code: 直观地说,就是一段js代码。定性地说,被处理成_ECMAScript Program_的代码是Global Code。(不包含Function code)
  2. Eval code: eval函数中所包含的代码
  3. Function code: 直观地说,就是函数中的代码。定性地说,是被处理成_ECMAScript FunctionBody_的函数(不包括内部嵌套的FunctionBody对应的Function code)。

这里提到了_ECMAScript Program_和_ECMAScript FunctionBody_,是编译流程中会涉及到的概念,两者都是语法树(Syntax Tree)中节点。

例如,这样一段代码被解析成语法树的话,如下图所示

function foo(a, b) {
    return a + b;
}

0.png


js的底层运行,会涉及到一些特定的数据结构,例如Lexical EnvironmentEnvironment Records,接下来一一来分析。当然,这些概念都来自于ECMAScript文档,推荐读者自行翻阅。

Lexical Environment

Lexical Environment是一种数据结构,其中主要包含Environment Records和outer两部分内容,outer指向另外一个Lexical Environment。多个Lexical Environment通过outer变量组成树状结构。

1.png

Environment Records

在Environment Records内记录了identifier和variable/function的关联关系。

2.png

Environment Records(以下简写为ER)分为两种:一种是Declarative Environment Records(以下简写为DER),另一种是Object Environment Records(以下简写为OER),两者在不同的情况下使用,DER主要在FunctionDeclarationVariableDeclarationCatch下使用,而OER主要在ProgramWithStatement下使用。

Environment Records的公有方法

ES5中的方法有:(在ES6中会有所不同)

  • HasBinding(name)
  • CreateMutableBinding(name, deleted)
  • SetMutableBinding(name, value, strict)
  • GetBindingValue(name, strict)
  • DeleteBinding(name)
  • ImplicitThisValue():返回在this所代表的value

基本上就是增删改查,比较特别的方法是ImplicitThisValue,具体实现在不同类型的ER中会有所不同。

DER

DER包含了声明时所生成的identifier,例如FunctionDeclaration、VariableDeclaration。

DER特有的方法:

  • CreateImmutableBinding(name)
  • InitializeImmutableBinding(name, value)

注意的是

ImplicitThisValue()将一直返回undefined

ECMA文档中,没有明确说明DER中内容的存储形式,为了方便理解和记忆,可以和下文OER一样都可以将其想象成object形式。

OER

拥有一个名为binding object的对象,identifier关联关系直接在这个对象,可以随意改动。

其ImplicitThisValue() 在provideThis=true情况下将返回binding object

Lexical Environment的操作

  • GetIdentifierReference(lex?, name, strict) 从lex的ER中获取内容,或者从递归从lex.outer中获取
  • NewDeclarativeEnvironment(lex?) 创建新LE(包含DER),指定outer
  • NewObjectEnvironment(object, lex?) 创建新LE(包含OER,使用object),指定outer

Global Environment

Global Environment是一个OER,在代码运行前创建。在不同的js环境下,Global Environment中的binding object有所不同,例如在浏览器环境下是window,在Nodejs环境下是Global

Execution Context

说到这就不得不提起计算机原理课上所提到的函数执行栈的相关知识点。Exection Context(以下简写EC)在js运行的过程中不断创建和销毁,并且也以栈的形式进行组织。栈顶的EC一般称为running EC

EC中由以下三部分组成:

  • LexicalEnvironment: 一个LE
  • VariableEnvironment: 一个LE
  • ThisBinding: 当前EC所关联的代码中的this指向。

3.png

具体的EC包括

  • Global EC
    • 在Global Code运行前被创建
    • LexicalEnvironment和VariableEnvironment初始时都是Global LE
    • ThisBinding是Global LE的binding object,例如,在浏览器上就是window
  • Eval EC: 略过
  • Function EC
    • 每次进入一个Function都会创建
    • 会调用NewDeclarativeEnvironment创建一个LE,该LE的outer是Function执行时所在的LE,就是running EC的LE

EC的操作包括:

  • Identifier Resolution: 对EC.LE调用GetIdentifierReference()

总结

执行栈结构帮助将LE串起来,通过LE链可以递归查询变量,所以闭包在我看来实际上是LE链,当然回过头来看MDN中的说明,大致上也可以看得懂了。