JavaScript 词法环境、执行上下文

260 阅读20分钟

看了很多博客,一直也没清楚地理解执行下文到底是个啥,特此去看看官方文档,记录一下。

一、可执行代码(executable code)类型

ECMAScript可执行代码一共有三种:

  • 全局代码:是指被作为 ECMAScript Program 处理的源代码文本。一个特定 Program 的全局代码不包括作为函数体(FunctionBody)被解析的源代码文本。
  • Eval 代码:是指提供给 eval 内置函数的源代码文本。更精确地说,如果传递给 eval 内置函数的参数为一个字符串,该字符串将被作为 ECMAScript Program 进行处理。在特定的一次对 eval 的调用过程中,eval 代码作为该 Program 的全局代码部分。
  • 函数代码:是指作为 函数体(FunctionBody)被解析的源代码文本。一个 FunctionBody 的函数代码不包括作为其嵌套函数的 FunctionBody 被解析的源代码文本。函数代码同时还特指以构造器方式调用 FunctionBody 内置对象时所提供的源代码文本。更精确地说,调用 Function 构造器时传递的最后一个参数将被转换为字符串并作为 FunctionBody 使用。如果调用 Function 构造器时,传递了一个以上的参数,除最后一个参数以外的其他参数都将转换为字符串,并以逗号作为分隔符连接在一起成为一个字符串,该字符串被解析为 FormalParameterList 供由最后一个参数定义的 FunctionBody 使用。初始化 Function 对象时所提供的函数代码,并不包括作为其嵌套函数的 FunctionBody 被解析的源代码文本。

二、词法环境(lexical environment)

词法环境是一种规范类型,用于根据 ECMAScript 代码的词法嵌套结构定义标识符特定变量和函数的关系。一个词法环境由一个环境记录(environment record)和可能为空的外部词法环境(outer lexical environment)引用构成。
通常词法环境会与 ECMAScript 代码,诸如 FunctionDeclaration(函数声明)、WithStatement(with语句)或者 TryStatement 的 Catch 块这样的特定句法结构相联系,且类似代码每次执行都会有一个新的词法环境被创建出来。

2.1 环境记录(environment record)

环境记录项记录了在其 关联的词法环境范围中创建的标识符绑定
环境记录器分为两种:

  • 声明式环境记录 :用于定义那些将标识符与语言值直接绑定的 ECMAScript 语法元素。例如:函数声明、变量声明、catch语句。
  • 对象式环境记录 :用于定义那些将标识符与具体对象的属性绑定的ECMAScript元素,例如 程序 以及 with语句。 可以将环境记录理解为一个抽象类,并且有两个具体实现类,分别为声明式环境记录和对象式环境记录。

(1)环境记录的抽象方法

  • HasBinding(N):判断环境记录是否包含对某个标识符的绑定。其中字符串N是标识符
  • CreateMutableBinding(N, D):在环境记录中创建一个新的可变绑定。其中字符串 N 指定绑定名称。如果可选参数 D 的值为 true,则该绑定在后续操作中可以被删除。
  • SetMutableBinding(N, V, S):在环境记录项中设置一个已经存在的绑定的值。其中字符串 N 指定绑定名称。V 用于指定绑定的值,可以是任何 ECMAScript 语言的类型。S 是一个布尔类型的标记,当 S 为 true 并且该绑定不允许赋值时,则抛出一个 TypeError 异常。S 用于指定是否为严格模式。
  • GetBindingValue(N, S):返回环境记录中一个已经存在的绑定的值。其中字符串 N 指定绑定的名称。S 用于指定是否为严格模式。如果 S 的值为 true 并且该绑定不存在或未初始化,则抛出一个 ReferenceError 异常。
  • DeleteBinding(N):从环境记录中删除一个绑定。其中字符串 N 指定绑定的名称。如果 N 指定的绑定存在,将其删除并返回 true。如果绑定存在但无法删除则返回 false。如果绑定不存在则返回 true
  • ImplicitThisValue():当从该环境记录的绑定中获取一个函数对象并且调用时,该方法返回该函数对象使用的 this 对象的值。
    • 声明式环境记录永远将 undefined 作为其 ImplicitThisValue 的返回值
    • 对象式环境记录ImplicitThisValue 通常返回 undefined,但 provideThis 标识的值为 true时,返回该对象式环境记录绑定的对象。

(2)声明式环境记录

每个声明式环境记录都有一个包含变量和函数声明的 ECMAScript 的程序作用域相关联。声明式环境记录用于绑定作用域内定义的一系列标识符。
声明式环境记录支持可变绑定不可变绑定。在不可变绑定中,一个标识符与它的值之间的关联关系建立之后,就无法改变。
声明式环境记录还支持以下方法:

  • CreateImmutableBinding(N):在环境记录中创建一个未初始化的不可变绑定
  • InitializeImmutableBinding:在环境记录中设置一个已经创建但未初始化的不可变绑定的值。 所以,创建和初始化不可变绑定是两个独立的过程。

(3)对象式环境记录

每一个对象式环境记录都有一个关联的对象,这个对象称为绑定对象。对象式环境记录直接将一系列标识符与其绑定对象的属性名建立一一对应的关系。
对象自身的和继承的属性都会作为绑定。
对象的属性可以动态增减,所以对象式环境记录所绑定的标识符集合也会隐匿地变化。而且,对象式环境记录没有不可变绑定

(4)词法环境的运算

1、GetIdentifierReference(lex, name, strict)
lex:词法环境
name:标识符字符串
strict:Boolean,标识是否为严格模式

lex === null => 返回一个引用类型的对象,基值为undefined,引用名为name  
lex !== null => envRec为lex的环境数据 => 调用envRec的HasBinding(N)具体方法:
    若 HasBinding(N) 结果为true => 返回一个引用类型的对象,基值为envRec,引用名为name  
    若 HasBinding(N) 结果为true => 令 outer 为 lex 的外部环境引用,返回GetIdentifierReference(outer, name, strict)的调用结果

个人理解:其实这个函数指明了,词法环境中式如何去获取标识符对应的引用的。
2、NewDeclarativeEnvironment(E) 新建一个词法环境env 和 一个声明式环境记录 envRec(不包含任何绑定),令 env 的环境记录为 envRec,外部环境引用为 E(可以为NULL),最后返回env
3、NewObjecteEnvironment(O, E) 新建一个词法环境env 和 一个对象式环境记录 envRec(包含 O 作为绑定对象),令 env 的环境记录为 envRec,外部环境引用为 E(可以为NULL),最后返回env

(5)全局环境(Global Environment)

全局环境是一个唯一的词法环境,它在任何ECMAScript脚本的代码执行前创建
全局环境的环境记录是一个对象式环境记录,该环境记录使用全局对象作为绑定对象,其外部环境引用为 null。 ECMAScript代码执行过程中,可能会像全局对象添加额外属性,或者修改初始属性的值。
个人理解:之所以全局环境的环境记录时一个对象式环境记录,时因为全局环境有一个唯一的全局对象,在全局环境中声明的变量和函数都是这个全局对象的属性。

2.2 外部环境引用(Outer Environment Reference)

用于表示词法环境的逻辑嵌套关系模型。(内部)词法环境的外部引用是逻辑上包含内部词法环境的词法环境。外部词法环境自然也可能有多个内部词法环境。
例如,如果一个 FunctionDeclaration 包含两个嵌套的 FunctionDeclaration,那么每个内嵌函数的词法环境都是外部函数本次执行所产生的词法环境。

2.3 词法环境实例理解

(1)变量

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

let phrase = "hello";
phrase = "Bye";

image.png 图中矩形代表环境记录器,箭头代表外部词法环境引用,演示了执行过程中全局词法环境的变化:

  1. 当脚本开始运行,词法环境预先填充了所有声明的变量。变量处于“未初始化(Uninitialized)”状态。这是一种特殊的内部状态,这意味着引擎知道变量,但是不能引用它。
  2. let phrase:尚未赋值,值为undefined
  3. phrase = "hello":phrase 赋值 "hello"
  4. phrase = "Bye":phrase 赋值"Bye"

(2)函数声明

函数其实也是一个变量,不同之处在于函数声明的初始化会被立即完成。 当创建了一个词法环境(Lexical Environment)时,函数声明会立即变为即用型函数(不像 let 那样直到声明处才可用)。这就是为什么我们可以在(函数声明)的定义之前调用函数声明。

image.png 但是这种行为只适用于函数声明,不适用于函数表达式。

(3)内部和外部的词法环境

一个函数运行时,在调用刚开始时,会自动创建一个新的词法环境以存储这个调用的局部变量和参数。

image.png 在这个函数调用期间,有两个词法环境::

  • 内部词法环境:与 say 的当前执行相对应。它具有一个单独的属性:name,函数的参数。我们调用的是 say("John"),所以 name 的值为 "John"
  • 外部词法环境:全局词法环境。它具有 phrase 变量和函数本身。

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

如果在任何地方都找不到这个变量:

  • 在严格模式下就会报错
  • 在非严格模式下,为了向下兼容,给未定义的变量赋值会创建一个全局变量。

(4)返回函数

image.png 在每次 makeCounter() 调用的开始,都会创建一个新的词法环境对象,以存储该 makeCounter 运行时的变量。
所有的函数在“诞生”时都会记住创建它们的词法环境。所有函数都有名为 [[Environment]] 的隐藏属性,该属性保存了对创建该函数的词法环境的引用 所以,counter.[[Environment]] 有对 {count: 0} 词法环境的引用。这就是函数记住它创建于何处的方式,与函数被在哪儿调用无关。[[Environment]] 引用在函数创建时被设置并永久保存。 当调用 counter() 时,会为该调用创建一个新的词法环境,并且其外部词法环境引用获取于 counter.[[Environment]]

image.png 执行counter()函数后,词法环境如下所示:

image.png

function makeCounter() {
    let count = 0;
  
    return function() {
      return count++;
    };
  }
  
  let counter = makeCounter();
  console.log(counter());  // 0
  console.log(counter());  // 1
  console.log(counter());  // 2
  console.log(counter());  // 3

三、执行上下文(Execution Contexts)

首先看官方文档中的一段话:

When control is transferred to ECMAScript executable code, control is entering an execution context. Active execution contexts logically form a stack. The top execution context on this logical stack is the running execution context. A new execution context is created whenever control is transferred from the executable code associated with the currently running execution context to executable code that is not associated with that execution context. The newly created execution context is pushed onto the stack and becomes the running execution context.

当控制器转入 ECMAScript 的可执行代码时,控制器就会进入一个执行上下文。
当前活动的多个执行上下文在逻辑上形成一个栈结构。该逻辑栈的最顶层的执行环境称为当前运行的执行上下文
任何时候,当控制器从当前运行的执行环境相关的可执行代码转入与该执行环境无关的可执行代码时,会创建一个新的执行环境。新建的这个执行环境会推入栈中,成为当前运行的执行环境。

执行上下文包含所有用于追踪其相关的代码的执行进度的状态。一般,执行上下文中包含如下组件:

  • 词法环境(LexicalEnvironment):一个词法环境对象,用于解析该执行上下文内代码创建的标识符引用
  • 变量环境(VariableEnvironment):一个词法环境对象,其环境记录用于保存由该执行上下文内的代码通过 VariableStatementFunctionStatement 绑定的值
  • this 绑定(ThisBinding):该执行上下文内的ECMAScript代码中 this 关键字所关联的值。 当创建一个新的执行上下文时,词法环境和变量环境最初是同一个值,但是在执行上下文关联的代码在执行过程中,变量组件永远不变,词法环境有可能改变
    通常情况下,只有正在运行的执行环境(执行环境栈里的最顶层对象)会被算法直接修改。因此当遇到“词法环境组件”、“变量环境组件”、“this 绑定组件”这三个术语时,指的是正在运行的执行环境的对应组件。
    注意:\color{red}{注意:}执行上下文只是一个标准机制,所以在ECMASript的代码中是无法访问执行上下文的。

3.1 标识符解析

标识符解析指的是使用正在运行的执行上下文中的词法环境,通过一个标识符获取其对应绑定的过程。
即:调用CetIdentifierReference(env, Identifier, strict) 并返回调用结果。

  • env:正在运行的执行上下文的词法环境
  • Identifier:标识符
  • strict: 正在解释执行的语法产生式是否处于严格模式下 标识符解析的结果必定是引用类型的对象,且其引用名属性的值与 标识符 字符串相等。

3.2 建立执行上下文

如第一节所述,ECAMScript中可执行代码总共分为三类,当执行这三类可执行代码时都会创建并进入一个新的执行上下文。即:

  • 解释执行全局代码 —— 全局上下文
  • 解释执行 eval 函数输入的代码 —— eval 上下文
  • 调用 ECMAScript 代码定义的函数 —— 函数上下文 每次 return 都会退出一个执行上下文,抛出异常也可退出一个或多个执行上下文
    当控制流进入一个执行上下文时,会设置该执行上下文的this绑定、定义变量环境和初始词法环境,并执行声明式绑定初始化过程。

(1)进入全局代码

当控制流进入全局代码的执行上下文时,会执行以下步骤:

  1. 使用全局代码初始化执行上下文
    1. 变量环境 设置为 全局环境
    2. 词法环境 设置为 全局环境
    3. this绑定 设置为 全局对象
  2. 使用全局代码执行声明式绑定初始化。 注意:全局环境在代码执行之前就已经创建\color{red}{注意:全局环境在代码执行之前就已经创建}

(2)进入 eval 代码

当控制流进入 eval 代码的执行上下文时,执行以下步骤:

  1. 初始化执行上下文
    1. 如果没有调用上下文(calling context)或者 eval 代码并非通过直接调用 eval 函数来执行的:
      1. 那就使用eval代码来初始化执行环境
    2. 否则:
      1. this绑定 设置为 当前执行环境下的 this 绑定
      2. 词法环境 设置为 当前执行环境下的词法环境
      3. 变量环境 设置为 当前执行环境下的变量环境
    3. 如果 eval 代码是在严格模式下执行的:
      1. 令 strictVarEnv 为以词法环境为参数调用的 NewDeclarativeEnvironment 得到的结果。
      2. 设置 词法环境strictVarEnv
      3. 设置 变量环境strictVarEnv
  2. 使用eval 代码执行声明式绑定初始化

注意:严格模式下\color{red}{注意:严格模式下},eval 代码不能在调用环境的 变量组件 中初始化变量及函数绑定。与之相对的,变量和函数绑定在一个新的变量环境中被初始化,该 变量环境 仅可被 eval 代码访问

(3)进入函数代码

当控制流根据一个函数对象F调用者提供的 thisArgargumentList,进入函数代码的执行上下文时,会执行以下步骤:

  1. this 绑定
    1. 如果函数代码是严格模式下的代码,直接 this 绑定为 thisArg。
    2. 如果函数代码是非严格模式下的代码:
      1. thisArg 为 undefinednull,则设 this 绑定为全局对象
      2. 否则,如果 Type(thisArg) 的结果不为 Object ,则设 this 绑定 为 ToObject(thisArg)
      3. 否则,this 绑定为 thisArg
  2. 以 F 的 [[scope]] 内部属性为参数,调用 NewDeclarativeEnvironment,并令 localEnv 为调用的结果。
    1. 设置 词法环境strictVarEnv
    2. 设置 变量环境strictVarEnv
  3. 令 函数代码(code) 为 F 的[[code]] 内部属性值
  4. 使用函数代码 和 argumentList 执行声明式绑定初始化

注意\color{red}{注意}:执行上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数。全局上下文在应用程序退出前被销毁。(但是闭包会影响这个过程,这一点对理解闭包挺重要的)

3.3 声明式绑定初始化

每个执行上下文都有一个关联的 变量环境(VariableEnvironment),在执行上下文下评估一段ECMAScript 代码时,变量和函数会以绑定的形式添加到这个变量环境的环境记录中。此外,对于函数代码,参数也会以绑定形式添加。
当进入一个执行上下文时,会按如下步骤在变量环境上创建绑定。假设调用者提供的代码为 code,函数代码参数为 args

  1. env 为当前运行的执行上下文的变量环境 的 环境记录。
  2. 若 code 为 eval代码configurableBindings = true;否则:configurableBindings = false
  3. 若 code 是在严格模式下:strict = true;否则:strict = false
  4. 如果 code 为函数代码
    1. 令 func 为通过 [[Call]] 内部属性初始化 code 的执行的函数对象。令 namesfunc[[FormalParameters]] 内部属性值。
    2. argCountargs 中元素的数量
    3. n = 0
    4. 按列表顺序遍历 names ,对于每一个字符串 argName:
      1. n ++
      2. 若 n > argCount:v = undefined;否则:v = args 中的第n个元素
      3. argName 调用 env 中的 HasBinding(),调用结果赋值给 argAlreadyDeclared
      4. 如果 argAlreadyDeclared === false,则以 argName 为参数调用 envCreateMutableBinding 具体方法
      5. 以 argNamev 和 strict 为参数,调用 env 的 SetMutableBinding 具体方法。
  5. 按源码顺序遍历 code ,对于每一个函数声明 f
    1. fn 为函数声明 f 中的标识符
    2. 初始化函数声明 f,并令 fo 为初始化结果。
    3. fn 为参数,调用 env 中的 HasBinding(),调用结果赋值给 argAlreadyDeclared
    4. 如果 argAlreadyDeclared === false,则以 fnconfigurableBindings 为参数调用 envCreateMutableBinding 具体方法
    5. 如果 argAlreadyDeclared === trueenv 为全局环境的环境记录:
      1. 令 go 为全局对象
      2. 以 fn 为参数,调用 go 和 [[GetProperty]] 内部方法,并令 existingProp 为调用的结果。
      3. 如果 existingProp.[[Configurable]] 的值为 true,则:以 fn、{ [[Value]]: undefined, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: configurableBindings } 组成的属性描述符和 true 为参数,调用 go 的 [[DefineOwnProperty]] 内部方法。
      4. 否则如果 IsAccessorDescriptor(existingProp) 的结果为真,或 existingProp 的特性中没有 {[[Writable]]: true, [[Enumerable]]: true},则:抛出一个 TypeError 异常。
    6. 以 fnfo 和 strict 为参数,调用 env 的 SetMutableBinding 具体方法
  6. 以 argments 为参数,调用 envHasBinding 具体方法,并令argumentsAlreadyDeclared 为调用的结果。
  7. 如果 code 是函数代码,并且 argumentsAlreadyDeclared 为 false,则:
    1. 以 fnnamesargsenv 和 strict 为参数,调用 CreateArgumentsObjec t抽象运算函数,并令 argsObj 为调用的结果。
    2. 如果 strict 为 true,则:
      1. 以字符串 "arguments" 为参数,调用 env 的 CreateImmutableBinding  具体方法。
      2. 以字符串  "arguments"  和 argsObj 为参数,调用envInitializeImmutableBinding 具体函数
    3. 否则: 2. 以字符串  "arguments" 为参数,调用 env 的 CreateMutableBinding 具体方法。
      1. 以字符串 "arguments"argsObj 和 false 为参数,调用 envSetMutableBinding 具体函数
  8. 按源码顺序遍历 code,对于每一个 VariableDeclaration 表达式作为 d 执行:
    1. 令 dn 为 d 中的标识符。
    2. 以 dn 为参数,调用 env 的 HasBinding 具体方法,并令 varAlreadyDeclared 为调用的结果。
    3. 如果 varAlreadyDeclared 为 false,则:
      1. 以 dn 和 configurableBindings 为参数,调用 env 的 CreateMutableBinding 具体方法。
      2. 以 dnundefined 和 strict 为参数,调用 env 的 SetMutableBinding 具体方法。 从上述步骤中可以看出,在执行声明式绑定初始化的步骤中,变量环境中函数声明的创建绑定和设置绑定的值是先于变量声明的。这让我明白了为啥函数提升的优先级比变量提升的优先级高了。

3.4 创建 Argument 对象

当控制器进入到函数代码的执行上下文时,将创建一个 arguments 对象,除非标识符 arguments 出现在该函数的形参列表中,或者是该函数的代码内部的变量声明标识符或函数声明标识符。
arguments 对象通过调用抽象方法 CreateArgumentsObject 创建,调用时将以下参数传入:funcnamesargsenvstrict

  • func:将要执行的函数对象
  • names: 函数的所有形参名
  • args:所有传给内部方法[[Call]]的实际参数
  • env:该函数代码的变量环境
  • strict:函数代码是否为严格代码

四、作用域和执行上下文的区别

这里说点题外话,之前看《JavaScript 高级程序设计(第四版)》时,有一章节是作用域和执行上下文,但是对这两个概念有点分不清楚,翻看了很多大佬的博客和书籍,特此记录一下两者的区别。

(1)作用域是静态的,执行上下文是动态的

JavaScript中的作用域是词法作用域,是由代码中书写的位置确定的。词法作用域是指在编译阶段就产生的有关标识符的访问规则。
而执行上下文是运行可执行代码时产生的,由词法环境、变量环境和this绑定组成,在代码执行完毕后对应的执行上下文会被销毁。此外执行上下文是一种标准机制,在ECMASript的代码中是无法访问的。

(2)执行上下文从属于作用域

这样说或许不是那么确切,但是确实一个作用域可以有多个执行上下文

个人总结

  • 能否访问到一个变量,是由作用域规则来确定的。而访问到的变量的值是什么由执行上下文来决定
  • 作用域的规则是基于作用域链,而执行上下文基于上下文栈。 (可能理解有误,欢迎大佬们批评指正)

此外,有关作用域和执行上下文的区别这篇文章解释的挺不错:segmentfault.com/a/119000001…

以上就是我看官网文档学习的有关词法环境和执行上下文的一些知识,了解了这些好像对我去理解this的指向,闭包等问题更清晰了一些。文章也主要是对官方文档的一些摘抄。

主要参考:
ES5/可执行代码与执行上下文