学习链接

深入理解JavaScript执行上下文和执行栈

【译】理解 JavaScript 执行上下文和执行栈

现代 JavaScript 教程:变量作用域,闭包

汤姆大叔:深入理解JavaScript系列(11-16,可能有语句不准确,可选择性对照原文 (俄) Google 翻译)

JavaScript作用域原理

对比两种解释作出的猜测

这节都是对比不同解释之后作出的猜测,并没有逐字逐句的查询规范进行验证,如有错误,望不吝赐教!

(👉仅表示相关或者说类似,两种类型也是自己对比之后划分的)

变量对象/活动对象 👉 环境记录:

  • 主要都是用来存储各种变量声明、函数声明
  • 前者是上下文的一个属性且变量声明中只考虑 var 变量
  • 后者则是上下文的词法环境或变量环境中的一个属性
    • 词法环境中的环境存储记录函数声明和变量声明(let、const
    • 变量环境中的环境记录则只记录 var 变量声明

[[Scope]] 👉 [[Environment]]

  • 主要都是用来记忆函数诞生时候的外部词法环境,也就是指向外部,全局上下文的指向 null

Scope 👉 对外部词法环境的引用

  • 其实都可理解为作用域链,只不过前者基于[[Scope]] ,后者基于 [[Environment]]

this(无👉):

  • ECMAScript 使用词法作用域,this 进入上下文时确定,一旦确定就不可更改
  • 而不是使用动态作用域,动态作用域的 this 应该是执行后也可更改的

一种解释

两种执行上下文,一种是全局上下文,一种是函数上下文。

函数上下文是在函数被调用的时候才生成

执行上下文中有些属性(应该是不止,但下文只考虑这几个)

  • 变量对象 VO (Variable Object)/ 活动对象 AO(Activation Object)
  • 作用域链 Scope(Scope Chain)
  • this 属性
  1. GlobalContext = {
  2. VO: {...}, // global
  3. this: thisValue
  4. };
  5. FunctionContext = { // 函数func为例
  6. VO: {...},
  7. AO: VO
  8. Scope: [ func.AO, ...func.[[Scope]] ]
  9. this: thisValue
  10. };

变量对象

变量对象在进入上下文时生成

  1. 变量对象中都存储着在上下文中声明的以下内容:
  2. 变量 (var, 变量声明);
  3. 函数声明 (FunctionDeclaration, 缩写为FD);
  4. 全局上下文变量对象 GlobalContextVO
  5. (VO === this === global)
  6. 函数上下文变量对象 FunctionContextVO
  7. (VO === AO, 并且添加了<arguments>和<formal parameters>)

给对应的变量声明赋值或修改,也就是在赋值或修改对应上下文中变量对象的属性值

  • 只有全局上下文的变量对象允许通过 VO 的属性名称来间接访问
    • 因为在全局上下文里,全局对象自身就是变量对象
  • 在其它上下文中是不能直接访问 VO 对象的,因为它只是内部机制的一个实现
    • 函数执行上下文中由活动对象 AO(Activation Object)来扮演 VO 的角色。

全局对象(Global Object) 是在进入任何执行上下文之前就已经创建了的对象;
这个对象只存在一份,它的属性在程序中任何地方都可以访问,全局对象的生命周期终止于程序退出那一刻。

作用域链

作用域链(Scope Chain)是与执行上下文相关的变量对象链,解析标识符名称时会在其中搜索变量(即用于变量查询)。

作用域链用于解析变量。当被要求解析变量时,JavaScript 始终从代码嵌套的最内层开始,如果最内层没有找到变量,就会跳转到上一层父作用域中查找,直到找到该变量。

函数上下文的作用域链在函数调用时创建的,包含活动对象和这个函数内部的 **[[Scope]]** 属性

[[Scope]] 是所有父变量对象(VO)的层级链,处于当前函数上下文之上,在函数创建时存于其中。

注意几点:

  • [[Scope]] 在函数创建时被写入函数,是静态的(不变的),直至函数被销毁。
    函数可能永不调用,但 [[Scope]] 属性已经写入,并存储在函数对象中。
  • 与作用域链相比[[Scope]]函数的一个属性而不是上下文的属性
  • 同一个父上下文中创建的闭包是共用一个 **[[Scope]]** 属性的。也就是说,某个闭包对其中 [[Scope]] 的变量做修改会影响到其他闭包对其变量的读取

this

this 值在进入上下文时确定,并且在上下文运行期间永久不变

规则

  1. 如果没有显示标明 this 的值(call、apply),JavaScript 引擎就会根据调用语句去推断 this 的值。
  2. 引擎会试图将调用语句格式化为 [对象名].[函数名]()。如果能够格式化,this 就为上述对象。否则,this 只好取 window(非严格模式)。
  3. 对于 [函数名]() 的调用,引擎会先根据作用域链找到隐式的对象(变量对象)。
    • 由于全局上下文的变量对象、动态(with、catch)变量对象是可以给用户访问的,因此格式化成功。
    • 函数上下文的变量对象不能给用户直接访问,因此格式化失败。
  4. 对于 (表达式)() 的调用,由于表达式不可能属于任何对象,因此格式化失败。

另外一种解释

在任意的 JavaScript 代码被执行前,执行上下文处于创建阶段。在创建阶段中总共发生了三件事情:

  1. 确定 this 的值,也被称为 This Binding
  2. LexicalEnvironment(词法环境) 组件被创建。
  3. VariableEnvironment(变量环境) 组件被创建。

因此,执行上下文可以在概念上表示如下:

  1. ExecutionContext = {
  2. ThisBinding = <this value>,
  3. LexicalEnvironment = { ... },
  4. VariableEnvironment = { ... },
  5. }

结构介绍

词法环境(Lexical Environment)

词法环境对象由两部分组成:

  1. 环境记录(Environment Record): 一个存储所有局部变量作为其属性的对象,是存储变量和函数声明的实际位置。
  2. 外部词法环境的引用,与外部代码相关联,意味着可以访问其外部词法环境。

一个“变量”只是 环境记录 这个特殊的内部对象的一个属性。“获取或修改变量”意味着“获取或修改环境记录的一个属性”。

词法环境的两种类型:

  • 全局环境(在全局执行上下文中)
    • 它拥有一个全局对象(window 对象)及其关联的方法和属性(例如数组方法)以及任何用户自定义的全局变量,this 的值指向这个全局对象。
    • 一个没有外部环境的词法环境。全局环境的外部环境引用为 **null**
  • 函数环境(在函数执行上下文中)
    • 用户在函数中定义的变量被存储在环境记录中。
    • 对外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境

注意:  对于函数环境而言,环境记录 还包含了一个 arguments 对象,该对象包含了索引和传递给函数的参数之间的映射以及传递给函数的参数的长度(**length**

环境记录的两种类型

  • 对象环境记录 用于定义在全局执行上下文中出现的变量和函数的关联。
    • 全局环境包含对象环境记录。(Object)
  • 声明性环境记录 用于存储函数执行上下文中出现的变量、函数和参数(arguments)。
    • 函数环境包含声明性环境记录。(Declarative)

变量环境(Variable Environment)

它也是一个词法环境,其 EnvironmentRecord 包含了由 VariableStatements 在此执行上下文创建的绑定。

如上所述,变量环境也是一个词法环境,因此它具有上面定义的词法环境的所有属性。

在 ES6 中,LexicalEnvironment 组件和 VariableEnvironment 组件的区别在于前者用于存储函数声明和变量( **let****const** )绑定,而后者仅用于存储变量( **var** )绑定

词法环境细节补充

变量声明

当脚本开始运行,词法环境预先填充了所有声明的变量

  • 最初,它们处于“未初始化(Uninitialized)”状态。
  • 这是一种特殊的内部状态,这意味着引擎知道变量,但是在用 let 声明前,不能引用它。(暂时性死区

函数声明

与变量不同,函数声明的初始化会被立即完成。

创建了一个词法环境时,函数声明会立即变为即用型函数(不像 let 那样直到声明处才可用)。

所有的函数在“诞生”时都会记住创建它们的词法环境。

所有函数都有名为 **[[Environment]]** 的隐藏属性,该属性保存了对创建该函数的词法环境的引用

这就是函数记住它创建于何处的方式,与函数被在哪儿调用无关[[Environment]] 引用在函数创建时被设置永久保存

函数调用

在一个函数运行时,在调用刚开始时,会自动创建一个新的词法环境以存储这个调用的局部变量和参数,并且其外部词法环境引用获取于 函数的 **[[Environment]]** 属性

访问修改变量

当代码要访问一个变量时 —— 首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境

在变量所在的词法环境中更新变量,这也就意味着多次调用同一内部函数,使用的也是同一个外部函数的变量。

补充

这是去规范里面直接检索到的内容,只在(5.1)中搜索到了 [[Scope]] 但其中没有 [[Environment]],后续的版本中则可以搜到 [[Environment]] 却找不到 [[Scope]],对比介绍来看,猜测基本上是替换 + 一定的修改关系。

Internal Properties Only Defined for Some Objects

(5.1)

Internal Property Value Type Domain Description
[[Scope]] Lexical Environment A lexical environment
that defines the environment in which a Function object is executed. Of the standard built-in ECMAScript objects, only Function objects implement [[Scope]].

Internal Slots of ECMAScript Function Objects

(6.0/7.0/8.0/9.0/10.0/11.0/12.0)

Internal Slot Type Description
[[Environment]] Lexical Environment The Lexical Environment
that the function was closed over. Used as the outer environment when evaluating the code of the function.

Internal Slots of ECMAScript Function Objects

(13.0)

Internal Slot Type Description
[[Environment]] an Environment Record The Environment Record
that the function was closed over. Used as the outer environment when evaluating the code of the function.