You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
在 ECMAScript 中,对执行环境有明确的解释:
只要进入可执行代码时,就会进入一个执行环境。javascript 有三种可执行代码:全局代码,函数代码,eval 代码。所以执行环境也只能在这三种代码下产生。
从一个执行环境到另一个执行环境(进入可执行代码)会在逻辑上形成栈结构(调用栈)。当前执行环境永远在栈的最顶层。
其中执行环境的词法环境组件和变量环境组件始终为词法环境对象。当创建一个执行环境时,其词法环境组件和变量环境组件最初是同一个值。在该执行环境相关联的代码的执行过程中,变量环境组件永远不变,而词法环境组件有可能改变。
在本标准中,通常情况下,只有正在运行的执行环境(执行环境栈里的最顶层对象)会被算法直接修改。因此当遇到“词法环境组件”、“变量环境组件”、“this 绑定组件”这三个术语时,指的是正在运行的执行环境的对应组件。
词法环境在 catch 和 with 中会改变词法环境组件,这里只说 catch。
catch 词法环境
在上面提到执行环境的词法环境组件和变量环境组件在 catch 语句中是不一样的。我们知道变量是需要在作用域(执行环境)中查找的。在执行之前,变量已经存在词法环境中了。但是在 catch (err) 语句中,err 是在运行时产生的,是动态的。所以在进入 catch 语句时需要生成词法环境。
catch 执行过程
令 C 为传入的参数(这个参数是 try 语句中产生的错误信息对象),oldEnv 为执行环境的词法环境组件。
第三点,创建了新的词法环境catchEnv,并且 catchEnv 外部词法环境引用就是 oldEnv。
第四五点,在 catchEnv 创建和初始化可变绑定 err。
第六点,将执行环境中的词法环境组件设为 catchEnv。在这里词法环境组件就修改为新创建的词法环境 catchEnv,和变量环境组件不一样了。
第七点,执行 Block。
第八点及注释,执行完 catch 语句之后,词法环境组件都会恢复到之前的状态。
词法环境
词法环境组件(变量环境组件)都提到了词法环境对象。在 ECMAScript 中对词法环境有说明:
词法环境是规范类型,它的作用是:一是表明词法嵌套关联关系,二是记录词法环境中创建的标识符绑定。要理解嵌套关系,我们首先明白词法环境存在哪些代码结构中(词法环境本身就是和我们书写源程序结构相对应的):
在这些结构中,会创建一个新的词法环境。从一个代码结构到另一个代码结构时,词法环境也就从一个词法环境到新创建的词法环境中了。在词法环境中会记录这种嵌套关系。如果重复进入同一段代码,那么每次进入时都会产生新的词法环境。
词法环境的结构和它的作用是相对应的,词法环境是由一个环境记录项和可能为空的外部词法环境引用组成的。这里重点介绍环境记录项。
环境记录项
环境记录项记录了在其关联的词法环境范围中创建的标识符绑定。
环境记录项又分为:声明式环境记录项和对象式环境记录项。
声明式环境记录项
声明式环境记录项用于定义那些将标识符与语言值直接绑定的 ECMA 脚本语法元素。声明式环境记录项存在函数、catch语句中。比如:
foo 函数中的声明式环境记录项包括: a、b。因为标识符直接与语言值绑定,所以我们可以直接通过标识符取值。
声明式环境记录项提供可变绑定和不可变绑定,可变绑定在所有环境记录项都支持。不可变绑定分为创建和初始化两个独立过程,分别通过 CreateImmutableBinding、InitializelmmutableBingding 两个内部方法完成。
具名函数表达式中 Identifier 就是不可变绑定
在严格模式下,函数的 arguments 对象也是不可变绑定
对象式环境记录项
对象式环境记录项存在全局代码、 with语句中。而且对象式环境记录项中没有不可变绑定。比如说:
在 with 语句中,变量和 with 对象的属性关联起来:
建立执行环境
在以下三种情况会建立执行代码:
在这三种代码中,大概步骤实际是差不多:
这里就只介绍在进入全局代码建立执行环境,至于函数的执行环境在介绍函数时说明
使用全局代码初始化执行环境采用以下步骤:
一二点会将变量环境组件、词法环境组件都设为全局环境。
全局环境的定义:
从这个定义可以得出以下几点
第三点将 this 设置为全局对象。
在 HTML 文档对象模型中全局对象可以理解为 window。
按 10.5 描述的方案,使用全局代码执行声明式绑定初始化化步骤,只需要查看最后一点就好:
作用域(作用域链/静态词法作用域)
理解了词法环境,也就很好理解作用域。实际上在 ECMAScript 规范中没有明确提出作用域,有明确的概念的是词法环境。
以 = 运算符为例进行说明
= 运算符的步骤
第一二点都是解释执行等号两边的标识符,也就是标识符解析:
标识符解析的结果都是引用类型的对象。引用类型在 this 章节做过介绍。
直接看第三点,标识符解析结果是通过 GetIdentifierReference 函数返回的。
GetIdentifierReference
关注引用类型的基值部分。
如果在 lex(词法环境)的环境数据(环境记录项)中绑定有参数 name ,那么引用类型的基值为词法环境的环境记录项。否则继续查找词法环境的外用环境引用,直到外部环境引用值为 null。外部环境值为 null 时,基值为 undefined。
回到 = 运算符步骤第三点,通过 GetValue(rref) 获取右值 rval。
注:这里 GetIdentifierReference 函数参数 lex 是指定的一个词法环境,但是在传入函数的实参,也就是在标识符解析时候的词法环境组件。在介绍执行环境的时候,词法环境组件就是一个词法环境。
GetValue
直接看第三点,在 IsUnresolvableReference(V) 为 true 的时候,抛出 ReferenceError 异常。
在基值为 undefined 时,返回 true。根据 GetIdentifierReference 函数的描述,只有在词法环境的外部环境引用为 null 时,基值才为 undefined。
这意味着:变量在词法环境的环境记录项中没有绑定,在外部环境引用中也没有绑定。所以此时变量是没有定义的。
上面的例子中,b 变量抛出 “ReferenceError: b is not defined” 错误。b 在全局环境中是没有定义的,全局环境的外部引用是 null。所以取 b 的值时会抛出错误。实际作用域链也就是不断的查找外部环境引用形成的路径。
所以变量的查找是在词法环境中进行的,准确的说是在词法环境的环境记录项中。
上面的例子是在全局环境中,在函数中也是一样的,确定词法环境(执行环境的词法环境组件)就确定了变量的查找路径。
在进入函数代码:
函数中的词法环境组件是通过 NewDeclarativeEnvironment 新创建的词法环境 localEnv ,并且 localEnv 的外部词法环境引用为函数的 [[Scope]] 内部属性。在介绍函数时候,详细介绍过 [[Scope]] 内部属性。这里 foo 函数的 [[Scope]] 内部属性就是全局环境的变量环境组件( VariableEnvironment)。
函数 foo 自身的词法环境没有变量 a,外部词法环境引用中存在 a 变量。
静态词法作用域是和词法环境有关,于代码执行位置无关。
闭包
MDN 中对闭包有明确概念:
其它语言中,函数执行完成之后,函数体中的变量会被释放。但是在 JavaScript 中,只要有对变量、函数声明的引用,即使函数执行完成,也不会释放。
我们知道函数调用会在栈中形成调用栈,当函数执行完成返回时,栈顶弹出,所有资源释放。所以如果存在闭包,那么资源应该是存在堆中的,至于具体怎样实现的,以后在探究。
上例中,muFunc 是 makeFunc 函数执行时创建的 displayName 函数的引用。即使 makeFunc 执行完成之后,displayName 函数声明也不会释放,displayName 函数可以访问自己的词法环境。所以调用 myFunc 函数时,可以访问 name 变量。
需要注意的是,每次进入函数都会生成新的词法环境
两次调用 foo 函数时,foo 函数中都是新的词法环境。func1 和 func2 中词法环境的外部词法环境引用是不同的,所以变量 a 是公用。
参考资料
ECMAScript 英文文档
ECMAScript 维基百科 中文文档
彻底搞懂javascript-运行上下文(Execution Context)
Clarity on the difference between “LexicalEnvironment” and “VariableEnvironment” in ECMAScript/JavaScript
The text was updated successfully, but these errors were encountered: