Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

执行环境(执行上下文)/ 词法环境 #5

Open
yangdui opened this issue May 16, 2020 · 0 comments
Open

执行环境(执行上下文)/ 词法环境 #5

yangdui opened this issue May 16, 2020 · 0 comments

Comments

@yangdui
Copy link
Owner

yangdui commented May 16, 2020

在 ECMAScript 中,对执行环境有明确的解释:

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

执行环境包含所有用于追踪与其相关的代码的执行进度的状态。精确地说,每个执行环境包含:

  • 词法环境组件(指定一个词法环境对象,用于解析该执行环境内的代码创建的标识符引用。)
  • 变量环境组件(指定一个词法环境对象,其环境数据用于保存由该执行环境内的代码通过 VariableStatementFunctionDeclaration 创建的绑定。)
  • this 绑定(指定该执行环境内的 ECMA 脚本代码中 this 关键字所关联的值。)

其中执行环境的词法环境组件变量环境组件始终为词法环境对象。当创建一个执行环境时,其词法环境组件变量环境组件最初是同一个值。在该执行环境相关联的代码的执行过程中,变量环境组件永远不变,而词法环境组件有可能改变。

在本标准中,通常情况下,只有正在运行的执行环境(执行环境栈里的最顶层对象)会被算法直接修改。因此当遇到“词法环境组件”、“变量环境组件”、“this 绑定组件”这三个术语时,指的是正在运行的执行环境的对应组件。

执行环境是一个纯粹的标准机制,并不代表任何 ECMA 脚本实现的工件。在 ECMA 脚本程序中是不可能访问到执行环境的。

只要进入可执行代码时,就会进入一个执行环境。javascript 有三种可执行代码:全局代码,函数代码,eval 代码。所以执行环境也只能在这三种代码下产生。

从一个执行环境到另一个执行环境(进入可执行代码)会在逻辑上形成栈结构(调用栈)。当前执行环境永远在栈的最顶层。

其中执行环境的词法环境组件变量环境组件始终为词法环境对象。当创建一个执行环境时,其词法环境组件变量环境组件最初是同一个值。在该执行环境相关联的代码的执行过程中,变量环境组件永远不变,而词法环境组件有可能改变。

在本标准中,通常情况下,只有正在运行的执行环境(执行环境栈里的最顶层对象)会被算法直接修改。因此当遇到“词法环境组件”、“变量环境组件”、“this 绑定组件”这三个术语时,指的是正在运行的执行环境的对应组件。

词法环境在 catch 和 with 中会改变词法环境组件,这里只说 catch。

catch 词法环境

在上面提到执行环境的词法环境组件和变量环境组件在 catch 语句中是不一样的。我们知道变量是需要在作用域(执行环境)中查找的。在执行之前,变量已经存在词法环境中了。但是在 catch (err) 语句中,err 是在运行时产生的,是动态的。所以在进入 catch 语句时需要生成词法环境。

catch 执行过程

产生式 Catch : catch ( Identifier ) Block 按照下面的过程执行 :

  1. 令 C 为传给这个产生式的参数。
  2. 令 oldEnv 为运行中执行上下文的词法环境组件
  3. 令 catchEnv 为以 oldEnv 为参数调用 NewDeclarativeEnvironment 的结果。
  4. Identifier 字符串值为参数调用 catchEnv 的 CreateMutableBinding 具体方法。
  5. Identifier、C、false 为参数调用 catchEnv 的 SetMutableBinding 具体方法。注:这种情况下最后一个参数无关紧要。
  6. 设定运行中执行上下文的词法环境组件为 catchEnv。
  7. 令 B 为解释执行 Block 的结果。
  8. 设定运行中执行上下文的词法环境组件为 oldEnv。
  9. 返回 B。

注: 不管控制是怎样退出 Block 的,词法环境组件总是会恢复到其之前的状态。

令 C 为传入的参数(这个参数是 try 语句中产生的错误信息对象),oldEnv 为执行环境的词法环境组件。

第三点,创建了新的词法环境catchEnv,并且 catchEnv 外部词法环境引用就是 oldEnv。

第四五点,在 catchEnv 创建和初始化可变绑定 err。

第六点,将执行环境中的词法环境组件设为 catchEnv。在这里词法环境组件就修改为新创建的词法环境 catchEnv,和变量环境组件不一样了。

第七点,执行 Block。

第八点及注释,执行完 catch 语句之后,词法环境组件都会恢复到之前的状态。

词法环境

词法环境组件(变量环境组件)都提到了词法环境对象。在 ECMAScript 中对词法环境有说明:

词法环境是一个用于定义特定变量和函数标识符在 ECMAScript 代码的词法嵌套结构上关联关系的规范类型。一个词法环境由一个环境记录项和可能为空的外部词法环境引用构成。通常词法环境会与 ECMAScript 代码诸如 FunctionDeclarationWithStatement 或者 TryStatementCatch 块这样的特定句法结构相联系,且类似代码每次执行都会有一个新的词法环境被创建出来。

环境记录项记录了在其关联的词法环境范围中创建的标识符绑定。

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

--Undefined (talk) 04:46, 28 January 2014 (UTC)词法环境和环境记录项是纯粹的规范机制,而不需要 ECMAScript 的实现保持一致。ECMAScript 程序不可能直接访问或者更改这些值。

词法环境是规范类型,它的作用是:一是表明词法嵌套关联关系,二是记录词法环境中创建的标识符绑定。要理解嵌套关系,我们首先明白词法环境存在哪些代码结构中(词法环境本身就是和我们书写源程序结构相对应的):

  • 全局代码
  • 函数代码
  • eval
  • catch 结构
  • with 结构

在这些结构中,会创建一个新的词法环境。从一个代码结构到另一个代码结构时,词法环境也就从一个词法环境到新创建的词法环境中了。在词法环境中会记录这种嵌套关系。如果重复进入同一段代码,那么每次进入时都会产生新的词法环境。

词法环境的结构和它的作用是相对应的,词法环境是由一个环境记录项和可能为空的外部词法环境引用组成的。这里重点介绍环境记录项。

环境记录项

环境记录项记录了在其关联的词法环境范围中创建的标识符绑定。

环境记录项又分为:声明式环境记录项和对象式环境记录项。

声明式环境记录项

声明式环境记录项用于定义那些将标识符与语言值直接绑定的 ECMA 脚本语法元素。声明式环境记录项存在函数、catch语句中。比如:

function foo(a) {
	var b = 2;
	 
	console.log(a); // 1
	console.log(b); // 2
}

foo(1)

foo 函数中的声明式环境记录项包括: a、b。因为标识符直接与语言值绑定,所以我们可以直接通过标识符取值。

声明式环境记录项提供可变绑定不可变绑定,可变绑定在所有环境记录项都支持。不可变绑定分为创建和初始化两个独立过程,分别通过 CreateImmutableBindingInitializelmmutableBingding 两个内部方法完成。

具名函数表达式中 Identifier 就是不可变绑定

var foo = function _foo() {
	_foo = 1;
	console.log(_foo);
}

foo(); // ƒ _foo() {...}

在严格模式下,函数的 arguments 对象也是不可变绑定

'use strict';

function foo() {
	arguments = 1;
}

foo(); // SyntaxError: Unexpected eval or arguments in strict mode
对象式环境记录项

每一个对象式环境记录项都有一个关联的对象,这个对象被称作绑定对象对象式环境记录项直接将一系列标识符与其绑定对象的属性名称建立一一对应关系。

对象式环境记录项存在全局代码、 with语句中。而且对象式环境记录项中没有不可变绑定。比如说:

var a = 0;
function func() {
	console.log(1)
}
// a 和 func 是全局对象的属性

在 with 语句中,变量和 with 对象的属性关联起来:

with({a: 1}) {
	console.log(a);
}
// with 对象式环境记录项为 a

建立执行环境

在以下三种情况会建立执行代码:

  • 进入全局代码
  • 进入eval 代码
  • 进入函数代码

在这三种代码中,大概步骤实际是差不多:

  1. 设置变量环境组件、词法环境组件、this 绑定。
  2. 执行声明式绑定初始化。

这里就只介绍在进入全局代码建立执行环境,至于函数的执行环境在介绍函数时说明

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

  1. 10.4.1.1 描述的方案,使用全局代码初始化执行环境。
  2. 10.5 描述的方案,使用全局代码执行声明式绑定初始化化步骤。

使用全局代码初始化执行环境采用以下步骤:

以下步骤描述 ECMA 脚本的全局执行环境 C 的创建过程:

  1. 变量环境组件设置为全局环境
  2. 词法环境组件设置为全局环境
  3. this绑定设置为全局对象

一二点会将变量环境组件、词法环境组件都设为全局环境。

全局环境的定义:

全局环境是一个唯一的词法环境,它在任何 ECMA 脚本的代码执行前创建。全局环境的环境数据是一个对象环境数据,该环境数据使用全局对象15.1)作为绑定对象。全局环境的外部环境引用null

在 ECMA 脚本的代码执行过程中,可能会向全局对象添加额外的属性,也可能修改其初始属性的值。

从这个定义可以得出以下几点

  • 全局环境是一个词法环境,并且是在代码执行前创建。
  • 全局环境的环境数据是对象环境数据。使用全局对象作为绑定对象,而且全局环境的外部环境引用为 null
  • 全局对象可以动态添加属性和修改初始属性的值。

第三点将 this 设置为全局对象

在 HTML 文档对象模型中全局对象可以理解为 window

10.5 描述的方案,使用全局代码执行声明式绑定初始化化步骤,只需要查看最后一点就好:

按源码顺序遍历 code,对于每一个 VariableDeclarationVariableDeclarationNoIn 表达式作为 d 执行:Note.png

  1. 令 dn 为 d 中的标识符
  2. 以 dn 为参数,调用 env 的 HasBinding 具体方法,并令 varAlreadyDeclared 为调用的结果。
  3. 如果 varAlreadyDeclared 为 false,则:
    1. 以 dn 和 configurableBindings 为参数,调用 env 的 CreateMutableBinding 具体方法。
    2. 以 dn、undefined 和 strict 为参数,调用 env 的 SetMutableBinding 具体方法。

作用域(作用域链/静态词法作用域)

理解了词法环境,也就很好理解作用域。实际上在 ECMAScript 规范中没有明确提出作用域,有明确的概念的是词法环境。

= 运算符为例进行说明

var a;
a = b; // ReferenceError: b is not defined

= 运算符的步骤

产生式 AssignmentExpression : LeftHandSideExpression = AssignmentExpression 按照下面的过程执行 :

  1. 令 lref 为解释执行 LeftHandSideExpression 的结果。
  2. 令 rref 为解释执行 AssignmentExpression 的结果。
  3. 令 rval 为 GetValue(rref)。
  4. 抛出一个 SyntaxError 异常,当以下条件都成立 :
  5. 调用 PutValue(lref, rval)。
  6. 返回 rval。

第一二点都是解释执行等号两边的标识符,也就是标识符解析

标识符解析是指使用正在运行的执行环境中的词法环境组件,通过一个 Identifier 获得其对应的绑定的过程。在 ECMA 脚本代码执行过程中,PrimaryExpression : Identifier 这一语法产生式将按以下算法进行解释执行:

  1. 令 env 为正在运行的执行环境的 词法环境组件
  2. 如果正在解释执行的语法产生式处在严格模式下的代码中,则仅 strict 的值为 true,否则令 strict 的值为 false
  3. 以 env、Identifier 和 strict 为参数,调用 GetIdentifierReference 函数,并返回调用的结果。

解释执行一个标识符得到的结果必定是引用类型的对象,且其引用名属性的值与 Identifier 字符串相等。

标识符解析的结果都是引用类型的对象。引用类型在 this 章节做过介绍。

直接看第三点,标识符解析结果是通过 GetIdentifierReference 函数返回的。

GetIdentifierReference

当调用 GetIdentifierReference 抽象运算时,需要指定一个词法环境 lex,一个标识符字符串 name 以及一个布尔型标识 strict。lex 的值可以为 null。当调用该运算时,按以下步骤进行:

  1. 如果 lex 的值为 null,则:
    1. 返回一个类型为引用的对象,其基值为 undefined,引用的名称为 name,严格模式标识的值为 strict。
  2. 令 envRec 为 lex 的环境数据
  3. 以 name 为参数 N,调用 envRec 的 HasBinding(N) 具体方法,并令 exists 为调用的结果。
  4. 如果 exists 为 true,则:
    1. 返回一个类型为引用的对象,其基值为 envRec,引用的名称为 name,严格模式标识的值为 strict。
  5. 否则:
    1. 令 outer 为 lex 的外部环境引用
    2. 以 outer、name 和 strict 为参数,调用 GetIdentifierReference,并返回调用的结果。

关注引用类型的基值部分。

如果在 lex(词法环境)的环境数据(环境记录项)中绑定有参数 name ,那么引用类型的基值为词法环境的环境记录项。否则继续查找词法环境的外用环境引用,直到外部环境引用值为 null。外部环境值为 null 时,基值为 undefined。

回到 = 运算符步骤第三点,通过 GetValue(rref) 获取右值 rval。

:这里 GetIdentifierReference 函数参数 lex 是指定的一个词法环境,但是在传入函数的实参,也就是在标识符解析时候的词法环境组件。在介绍执行环境的时候,词法环境组件就是一个词法环境。

GetValue

  1. 如果 Type(V) 不是引用,返回 V。
  2. 令 base 为调用 GetBase(V) 的返回值。
  3. 如果 IsUnresolvableReference(V),抛出一个 ReferenceError 异常。
  4. 如果 IsPropertyReference(V),那么
    1. 如果 HasPrimitiveBase(V) 是 false,那么令 get 为 base 的 [[Get]] 内部方法 , 否则令 get 为下面定义的特殊的 [[Get]] 内部方法。
    2. 将 base 作为 this 值,传递 GetReferencedName(V) 为参数,调用 get 内部方法,返回结果。
  5. 否则,base 必须是一个环境记录项
  6. 传递 GetReferencedName(V) 和 IsStrictReference(V) 为参数调用 base 的 GetBindingValue 具体方法,返回结果。

GetValue 中的 V 是原始基值的属性引用时使用下面的 [[Get]] 内部方法。它用 base 作为他的 this 值,其中属性 P 是它的参数。采用以下步骤:

  1. 令 O 为 ToObject(base)。
  2. 令 desc 为用属性名 P 调用 O 的 [[GetProperty]] 内部方法的返回值。
  3. 如果 desc 是 undefined,返回 undefined
  4. 如果 IsDataDescriptor(desc) 是 true,返回 desc。[[Value]]
  5. 否则 IsAccessorDescriptor(desc) 必须是 true,令 getter 为 desc。[[Get]]
  6. 如果 getter 是 undefined,返回 undefined
  7. 提供 base 作为 this 值,无参数形式调用 getter 的 [[Call]] 内部方法,返回结果。

注: 上述方法之外无法访问在第一步创建的对象。实现可以选择不真的创建这个对象。使用这个内部方法给实际属性访问产生可见影响的情况只有在调用访问器函数时。

直接看第三点,在 IsUnresolvableReference(V) 为 true 的时候,抛出 ReferenceError 异常。

IsUnresolvableReference(V):如果值是 undefined 那么返回 true,否则返回 false

在基值为 undefined 时,返回 true。根据 GetIdentifierReference 函数的描述,只有在词法环境的外部环境引用为 null 时,基值才为 undefined。

这意味着:变量在词法环境的环境记录项中没有绑定,在外部环境引用中也没有绑定。所以此时变量是没有定义的。

上面的例子中,b 变量抛出 “ReferenceError: b is not defined” 错误。b 在全局环境中是没有定义的,全局环境的外部引用是 null。所以取 b 的值时会抛出错误。实际作用域链也就是不断的查找外部环境引用形成的路径。

所以变量的查找是在词法环境中进行的,准确的说是在词法环境的环境记录项中。

上面的例子是在全局环境中,在函数中也是一样的,确定词法环境(执行环境的词法环境组件)就确定了变量的查找路径。

var a = 1;

function foo() {
	console.log(a);
}

foo();// 1

进入函数代码

以 F 的 [[Scope]] 内部属性为参数调用 NewDeclarativeEnvironment,并令 localEnv 为调用的结果。

词法环境组件 为 localEnv。

函数中的词法环境组件是通过 NewDeclarativeEnvironment 新创建的词法环境 localEnv ,并且 localEnv 的外部词法环境引用为函数的 [[Scope]] 内部属性。在介绍函数时候,详细介绍过 [[Scope]] 内部属性。这里 foo 函数的 [[Scope]] 内部属性就是全局环境的变量环境组件( VariableEnvironment)。

函数 foo 自身的词法环境没有变量 a,外部词法环境引用中存在 a 变量。

静态词法作用域是和词法环境有关,于代码执行位置无关。

var a = 1;

function func() {
	console.log(a);
}

function foo() {
	var a = 2;
	func();
}

foo(); // 1

闭包

MDN 中对闭包有明确概念:

闭包是函数和声明该函数的词法环境的组合。

function makeFunc() {
    var name = "Mozilla";
    function displayName() {
        console.log(name);
    }
    return displayName;
}

var myFunc = makeFunc();
myFunc(); // Mozilla

其它语言中,函数执行完成之后,函数体中的变量会被释放。但是在 JavaScript 中,只要有对变量、函数声明的引用,即使函数执行完成,也不会释放。

我们知道函数调用会在栈中形成调用栈,当函数执行完成返回时,栈顶弹出,所有资源释放。所以如果存在闭包,那么资源应该是存在堆中的,至于具体怎样实现的,以后在探究。

上例中,muFunc 是 makeFunc 函数执行时创建的 displayName 函数的引用。即使 makeFunc 执行完成之后,displayName 函数声明也不会释放,displayName 函数可以访问自己的词法环境。所以调用 myFunc 函数时,可以访问 name 变量。

需要注意的是,每次进入函数都会生成新的词法环境

function foo() {
  var a = 0;

  return function func() {
    a++;
    console.log(a);
  }
};

var func1 = foo();
var func2 = foo();

func1(); // 1
func1(); // 2
func2(); // 1
func2(); // 2

两次调用 foo 函数时,foo 函数中都是新的词法环境。func1 和 func2 中词法环境的外部词法环境引用是不同的,所以变量 a 是公用。

参考资料

ECMAScript 英文文档

ECMAScript 维基百科 中文文档

彻底搞懂javascript-运行上下文(Execution Context)

Clarity on the difference between “LexicalEnvironment” and “VariableEnvironment” in ECMAScript/JavaScript

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant