6

开门见山,我们来看看下面这个有趣的例子

 对于上面这种用var的声明方式,无论x的默认值为什么,只要形参中出现了默认值,zzz都会被当作块级作用域中的值。

 这是我偶然间遇到的一个问题,起初我认为这是chrome的bug,我将我的想法请教了一位朋友,他告诉我说这不是bug,并让我先看看这篇params default value & params environment & TDZ

 看完后我将我的想法进一步告诉了他,我的想法可以用下面这5张图来概括。

我认为这是chrome的bug,如果说是block,那么出了这个块就不该被访问到,但是事实是能访问到

 而且从本身的语法来讲,他也不应该是block,而是function scope。

 他回答说你没看懂,并告知我没看规范是很难理解,那么没办法了,读读规范吧,对规范已经不陌生了,在我的前两篇文章中,已经引用了规范中的很多内容。下面我们先来解释下规范中对于这一问题相关的解释,然后根据这些去解释我们遇到的这一问题。

 注:以下为ES6规范,ES6规范,ES6规范,重要的事情说三遍,不是ES5噢~

8.1 词法环境(LexicalEnvironment)

 一个词法环境是一种规范的类型,用作定义基于JS代码的嵌套词法结构中标识符与变量或者函数间的关联。一个词法环境包括一个Environment Records(即作用域记录,以下我们也简称ER)和一个可能为null的指向外部词法环境的引用。

 通常一个词法环境与JS代码一些特殊的语法结构想关联,如函数声明,块级语句,或者try语句中的catch从句。当每次这些代码被解析的时候,都会创建一个新的词法环境。

 一个ER记录了与它关联的词法环境的作用域中的标识符绑定。所以称之为作词法环境的ER。

 外部的词法环境引用用作模拟逻辑上的词法环境嵌套。一个词法环境的外部引用也是一个引用,它指向围绕或者说包括当前这个词法环境的词法环境。当然,外部的词法环境又有它自己的外部词法环境,这就是我们常说的作用域链。

 一个词法环境可能作为多个内部词法环境共同的外部词法环境。例如,一个函数声明中有两个内嵌的函数声明。一个语句块中有两个内嵌的语句块。

 一个全局环境是特殊的词法环境,它没有外部词法环境,它的外部词法环境引用为null。一个全局环境的ER也许会被用标识符绑定进行预填充,包含一些相关的全局对象,它的属性提供一些全局环境下的标识符绑定,即内置对象,不同的JS宿主环境,内置对象不同。

 这个全局对象就是全局环境下this的值。当JS代码运行的时候,其他的属性也许会被加入到全局对象中,最初的属性可能会被修改。

 一个模块环境是一个词法环境,它包括对于一个模块顶部声明的绑定。它也包括对于通过模块显式导入(通过import)的模块的绑定。一个模块环境的外部环境为全局环境。

 调用一个函数的时候,一个函数环境也是一个词法环境,与函数对象想对应。一个函数环境也许会建立一个新的this绑定(比如构造函数,对象中的函数),注意这里的也许二字,因为this只有调用时才能确定。一个函数环境也会捕获必要的状态以支持调用父级方法。

 词法环境和ER值是纯粹的规范,它们不需要对应于任何特定的ECMAScript实现。在ECMAScript程序中不可能直接访问或者操作它们。

8.1.1 Environment Records

 在规范中,有两种类型的ER,声明式ER(declarative Environment Records)和对象式ER(object Environment Records)。

 声明式ER(declarative Environment Records)被用作定义ECMAScript(以下简称ES)语言中语法元素的作用,例如函数声明,变量声明,以及catch语句中把绑定的标识符与ES语言中的值(Undefined, Null, Boolean, String, Symbol,Number, and Object中的一种,以下简称ES合法值)联系在一起。

 对象式ER(object Environment Records)被用作定义例如with语句这类把绑定的标识符与某些对象联系起来的ES元素。

 全局ER(Global Environment Records)和函数ER(function Environment Records)是专门用作全局脚本声明和函数内的顶部声明(也就是我们常说的声明提升)。

 为了规范ER的值是Record规范类型并且能够存在于简单的面向对象层次结构中。可以认为ER是一个抽象类,他有三个子类-声明式ER,对象式ER,全局ER。函数ER和模块ER(module Environment Records)是声明式ER的子类。ER这个抽象类包含许多抽象方法(见下表),这些抽象方法在不同的子类中有不同的实现(既然是抽象方法,那么这是必然的)

                                             表1:ER中的抽象方法

Method Purpose
HasBinding(N) 判断ER中是否绑定有N(即是否有标识符N),有返回true,否则返回false
CreateMutableBinding(N, D) 在ER中创建一个新的未初始化的且可变的绑定(可以理解为声明一个变量),N为标识符名,D是可选参数,如果为true,这个绑定随后可能会被删除。
CreateImmutableBinding(N, S) 在ER中创建一个新的未初始化的且不可变的绑定,N为标识符名。如果S为true,无论是否在严格模式下,在它初始化之前尝试去访问它的值或者在他初始化后设置它的值都会抛出异常(就是我们用到的const)。S是可选参数,默认为false。
InitializeBinding(N,V) 设置ER中已经存在但是未初始化的绑定的值。N为标识符名,V为ES合法值。
SetMutableBinding(N,V, S) 设置ER中已经存在但是未初始化的绑定的值。N为标识符名,V为ES合法值。S为一个boolean类型标志,如果为true并且无法设置成你传入的值,将抛出一个TypeError错误。
GetBindingValue(N,S) 返回一个ER中已经存在的绑定。N为标识符名。S被用作识别原始引用是否在严格模式中或者需要使用严格模式语义。如果S为true且绑定不存在,将抛出一个ReferenceError异常。如果绑定存在但是未初始化,无论S为何值,一个ReferenceError异常将被抛出。
DeleteBinding(N) 从ER中删除一个绑定。N为标识符名,如果N存在,删除并返回true。如果N存在但是不能被删除返回false。如果N不存在,返回true。
HasThisBinding() 判断ER是否绑定了this。(就是我们常用的call和apply)。如果是返回true,否则返回false。
HasSuperBinding() 判断是否有父类方法绑定。如果是返回true,否则返回false。
WithBaseObject () 如果ER与with语句有关联,返回with的对象。否则,返回undefined

8.1.1.1 声明式ER(Declarative Environment Records)

 每个声明式ER都与一个作用域想关联,这个作用域包含var,const,let,class,module,import或者function声明。一个声明式ER绑定它的作用域中定义的标识符的集合。

有了上面的基本解释,我们下面来看与提问有关的地方:

9.2.12 函数声明实例化(FunctionDeclarationInstantiation(func, argumentsList)

请记住这里的func和argumentsList,在后面描述过程的时候我们会多次提到

 当为了解析一个JS函数建议执行上下文的时候,一个新的函数ER就被建立,并且绑定这个ER中每个实例化了的形参(这里的实例化应该是指在执行函数的时候,形参才能有值,有值之后就代表实例化了)。同时在函数体中的每个声明也被实例化了。

 如果函数的形参不包含任何默认值,那么函数体内的声明将与形参在同一ER中实例化。

 如果形参有设置默认值,第二个ER就被建立,他针对的是函数体内的声明(我们可以形象的理解为这是一个除了函数作用域和块级作用域之外的"第三作用域")。形参和本身的函数声明是函数声明实例化的一部分。所有其他的声明在解析函数体的才会被实例化。

 其实到这里,我们就已经能解释我们提出的问题了,用var声明的变量在chrome中显示为Block,并不是代表他为块级作用域中的值,而仅仅是为了区分形参的ER和函数体的ER,形参的ER中的变量只能读取形参ER中的变量或者函数外的变量,而函数体内的变量可以读取函数体内,形参,外部的变量。这里摘抄下上面提到的文章中的代码片段:

let y =1;
function foo(x = function(){console.log(y)},y=2) {
  x(); // 2
  var y = 3; // if use let, then throw error: y is already declared, which is much more clear.
  console.log(y); //3
  x(); // 2
}
foo();
console.log(y); //1

这便是我们chrome为什么要区分形参的ER和函数体的ER的原因,是为了让我们看得更加清晰。

问题虽然解决了,但是规范却还意犹未尽,有兴趣的同学可以接着往下将这规则中这一节的内容看完。

 函数声明实例化按照如下过程进行。其中func为函数对象,argumentsList为参数列表

 1. Let calleeContext 作为运行时上下文/运行时环境(Execution Contexts,见下)

Execution Contexts(原文为8.3节内容,但是这里提到了,所以我们在这里就一并解释了):

一个运行时上下文或者说运行时环境是用来跟踪一个ECMAScript实现(注意ES实现不止JS一种)的代码的运行时解析。在运行时的任意时间点,最多只存在一个运行时上下文,即当前执行的代码。

一个栈被用作跟踪运行时环境,运行时环境总是指向栈顶的元素(也就是我们常说的调用栈,chrome调试时的call stack)。无论何时,只要运行时环境从当前运行的代码转移到非当前运行时环境的代码,就会创建一个新的运行时环境,并将这个新的运行时环境push到栈顶,成为当前的运行时环境。

为了跟踪代码的执行过程,一个运行时环境包含实现具体的状态是有必要的。每一个运行时环境都至少有下表列出的这几种元素。

Component Purpose
code evaluation state 包含与运行时环境相关的代码所需的任何状态,如执行中,暂停,继续解析
Function 如果运行时环境正在解析一个函数对象,那么这个值就为那个函数对象。如果正在解析一个脚本(script)或者模块(module),那么这个值为null
Realm(域) 来自相关代码可以访问的ECMAScript resources的域。注:ECMAScript resources包含客户端ECMAScript,ECMAScript核心标准库,扩展自ECMAScript核心标准库的服务端ECMAScript。域包括全局对象和内置对象

运行时环境的代码解析可能会被各种各样的情况打断而导致暂停或者说挂起。一旦运行时环境切换到另一个不同的运行时环境,那么这个不同的环境就可能成为当前运行时环境,并开始解析代码。一段时间过后,一个暂停的执行环境也许会成为运行时环境并且从之前的暂停点继续解析代码。运行时环境的这种来回切换的状态是通过类栈结构来过渡的。然而,一些ES特性需要非栈的过渡。

运行时环境的Realm的值也被称作当前域。运行时环境的Function的值也被成为活动函数对象。

ECMAScript的运行时环境有额外的state元素(见下表)

Component Purpose
LexicalEnvironment 标记用作解析当前运行时环境中代码里的标识符引用的词法环境
VariableEnvironment 标记在当前运行时环境中词法环境的ER包括var声明创建的绑定的词法环境

上表中的词法环境(LexicalEnvironment)和变量环境(VariableEnvironment),在一个运行时环境中总是表现为词法环境。当一个运行时环境被创建的时候,它的词法环境和变量环境初始化为相同的值。

可以参考下stackoverflow上的解释1以及stackoverflow上的解释2

// VariableEnvironment (global) = { __outer__: null }
// LexicalEnvironment = VariableEnvironment (global)

(function foo() {
   
 // VariableEnvironment (A) = { x: undefined, __outer__: global }
 // LexicalEnvironment = VariableEnvironment (A)
   
 var x;
   
 (function bar(){
   
   // VariableEnvironment (B) = { y: undefined, __outer__: A }
   // LexicalEnvironment = VariableEnvironment (B)
   
   var y;
   
   x = 2;
   
   // VariableEnvironment (A) = { x: 2, __outer__: global }
   // LexicalEnvironment is still the same as VariableEnvironment (B)
   
 })(); 
})();

对于构造器的运行时上下文,有额外的的state元素(见下表)

Component Purpose
Generator 当前运行时环境正在解析的构造器对象

在大多数情况下,只有当前运行时环境(即运行时环境栈的栈顶元素)直接被规范中的算法操作。

 2. Let env 作为calleeContext(当前的运行时上下文,也就是运行时上下文的栈顶元素)的词法环境(LexicalEnvironment)

 3. Let envRec 作为env的ER

 4. Let code 等于[[ECMAScriptCode]]这个func的内嵌属性的值(内嵌属性(两个中括号包裹的属性)并不是ES的一部分,由ES的具体实现来定义,它们纯粹是为了展示,更重要的一点,它们具有多态性。下面再看到中括号就不再解释内嵌属性了)

  • [[ECMAScriptCode]]:类型为Node。值为源代码文件解析后的函数体,即函数对象有一个属性[[ECMAScriptCode]]可以指向自身的函数体。

 5. Let strict 等于[[Strict]]的值

  • [[Strict]]: 类型为boolean。如果为true代表这是一个严格模式下的函数

 6. Let formals 等于[[FormalParameters]]的值

  • [[FormalParameters]]:类型为Node。指向函数的形参列表。

 7. Let parameterNames 等于formalsBoundNames,即如果形参为x, y那么parameterNames['x', 'y']

 8. 如果parameterNames里有重复的,将hasDuplicates置为true,否则置为false

 9. Let simpleParameterList等于formalsIsSimpleParameterList

  • IsSimpleParameterList:如果形参为空或者只是普通的标识符则返回true,其他的如形参为rest参数(...x),普通参数加rest参数(x, ...y),参数有默认值,参数有解构赋值等等,都返回false

 10. Let hasParameterExpressions等于formalsContainsExpression的值

  • ContainsExpression:形参含有默认值则为true,否则为false

 11. Let varNames等于函数的VarDeclaredNames(只包含函数体里的变量,不包含形参)的值

 12. Let varDeclarations等于函数的VarScopedDeclarations的值

  • VarDeclaredNamesVarScopedDeclarations的区别:VarDeclaredNames是一个类型为NameSetName只包含标识符名,作用域等等)。而VarScopedDeclarations是一个类型为StatementListItemListStatementListItem代表的是语句元素,ES一共有14种语句),就这里的语句而言,指的是VariableStatement,对于我们解析而已,是把语句(也就是Statement)当作一个语法树节点

 13. Let lexicalNames等于函数的LexicallyDeclaredNames(不包含var和function声明)

 14. Let functionNames等于一个空的List

 15. Let functionsToInitialize等于一个空的List

 16. 对于变量varDeclarations其中的每个元素d,如果d既不是VariableDeclaration也不是ForBinding(for in或者for of结构里面进行声明)。那么:

  • 进行Assert(断言),判断d是否是函数声明或者构造器声明

  • Let fn等于dBoundNames

  • 如果fn不是functionNames里的元素,那么

  • fn用头插法插入functionNames

  • 注意如果fn有多次重复出现,则以最后一次为准

  • d用头插法插入functionsToInitialize

 17. 声明一个argumentsObjectNeeded,赋值为true

 18. 如果func的内嵌属性[[ThisMode]]的值为lexical,那么

  • argumentsObjectNeeded赋值为false(注意箭头函数没有arguments对象)

  • [[ThisMode]]:作用是定义在函数形参和函数体内如何解析this引用。值为lexical代表this指向词法闭包的this值(词法闭包就是我们常说的闭包,具体可以看我的上一篇文章),strict代表this值完全由函数调用提供。global代表this值为undefined

 19. 否则(接上)如果argumentsparameterNames(在第7步声明)的一个元素(也就是形参里面我们使用了arguments作为标识符), 那么将argumentsObjectNeeded赋值为false

 20. 否则(接上)如果hasParameterExpressions(在第10步声明)等于false,那么

  • 如果argumentsfunctionNames(在第14步声明)的一个元素,或者是lexicalNames(在第13步声明)的一个元素,那么将argumentsObjectNeeded赋值为false

 21. 对于parameterNames(在第7步声明)中每个元素paramName

  • a.Let alreadyDeclared等于envRec.HasBinding(paramName)的值(即判断当前环境中是否绑定过paramName)

  • b.注意:早期的错误检查确保了多个重复的形参参数数名只可能出现在形参没有默认值和rest参数的非严格模式下的函数中:

    1. function func(x, x = 2) {} // 报错
    2. function func(x, ...x) {} // 报错
    3. function func(x, x) {} // 不报错
    4. 'use strict';
       function func(x, x) {} // 报错
  • c.如果alreadyDeclared等于false,那么:

  • c.1 Let status等于envRec.CreateMutableBinding(paramName)(表1中有这个方法)的值(即将声明的参数绑定到函数的作用域中)

  • c.2 如果hasDuplicates(在第8步声明)等于true,那么:

  • Let status等于envRec.InitializeBinding(paramName, undefined)(表1中有这个方法)的值

  • c.3 断言:在上面两步操作中(c.1和c.2),status不可能是一个 abrupt completion(可以简单的理解为break,continue,return和throw操作)

 22. 如果argumentsObjectNeeded(第17-20步改变)等于true,那么:

  • a.如果strict(第5步声明)等于true或者simpleParameterList(第9步声明)等于false,那么:

  • a.1 Let ao等于CreateUnmappedArgumentsObject(argumentsList)的值

  • b.否则(接上面的a步骤):

  • b.1 注意:mapped argument(与上面的Unmapped对应)对象仅在非严格模式下且形参没有rest参数,默认值,解构赋值的函数中提供。(满足这三个条件其实simpleParameterList就为true了)

  • b.2 Let ao等于CreateMappedArgumentsObject(func, formals, argumentsList, env)的值

  • 注:CreateUnmappedArgumentsObject和CreateMappedArgumentsObject简单来说就是根据参数形式的不同创建不同的arguments`对象

  • c.ReturnIfAbrupt(ao)

  • d.如果strict等于true,那么:

  • d.1 Let status等于envRec.CreateImmutableBinding("arguments")(表1中有介绍)的值

  • e.否则(接上面的c步骤),Let status等于envRec.CreateMutableBinding("arguments")(表1中有介绍)的值

  • f.断言:status不可能是一个 abrupt completion

  • g.执行envRec.InitializeBinding("arguments", ao)(表1中有介绍)

  • h.向parameterNames(第7步中声明)中appendarguments

 23. Let iteratorRecord等于Record {[[iterator]]: CreateListIterator(argumentsList), [[done]]: false}(即建立一个内置迭代器属性,让arguments变成可迭代的)

 24. 如果hasDuplicates(第8步中声明)等于true,那么:

  • a.Let formalStatus等于formals去调用IteratorBindingInitialization,用iteratorRecordundefined作为参数的返回值

 25. 否则(接上面的24步骤):

  • a.Let formalStatus等于formals去调用IteratorBindingInitialization,用iteratorRecordenv作为参数的返回值(可以看到只有最后一个参数和24步不一样)

  • IteratorBindingInitialization(iteratorRecord,environment):当environmentundefined的时候,这意味着应该用一个PutValue(即将一个值放入一个对象)操作去初始化值。这是针对非严格模式情况下的一个考虑(因为严格模式下在24步应该是false)。在这种情况下,形参被预初始化,目的是解决多个参数名相同的问题。

 26. ReturnIfAbrupt(formalStatus)

 27. 如果hasParameterExpressions(第10步声明)等于false,那么:

  • a.注意:对于形参和声明提取的变量,仅仅只需要一个单一的词法环境

  • b.Let instantiatedVarNames等于parameterNames的一个副本

  • c.对于varNames(第11步中声明)的每个元素n

  • c.1 如果n不是instantiatedVarNames里的元素,那么:

  • c.1.1 appendninstantiatedVarNames

  • c.1.2 Let status等于envRec.CreateMutableBinding(n)

  • c.1.3 断言:status不可能是一个 abrupt completion

  • c.1.4 执行envRec.InitializeBinding(n, undefined)

  • d.Let varEnv等于env

  • e.Let varEnvRec等于envRec

 28. 否则(接上面的27步骤):

  • a.注意:一个单独的ER是有必要的,目的是确保形参中的表达式创建的闭包对函数体的变量不具有可访问性(即我们提到的"第三作用域")

  • b.Let varEnv等于NewDeclarativeEnvironment(env)的值(即创建一个新的词法环境,它的ER里没有任何绑定,这个ER的外部或者说父级词法环境在这里就是env)

  • c.Let varEnvRec等于varEnv的ER

  • d.将calleeContext(第1步中声明)的VariableEnvironment设为varEnv

  • e.Let instantiatedVarNames等于一个空的List

  • f.对于varNames中的每个元素n

  • f.a 如果n不是instantiatedVarNames中的元素,那么:

  • f.a.1 appendninstantiatedVarNames

  • f.a.2 Let status等于varEnvRec.CreateMutableBinding(n)varEnvRec在27.e步或者28.c步中声明,CreateMutableBinding参考表1)的值

  • f.a.3 断言:status不可能是一个 abrupt completion

  • f.a.4 如果n不是parameterNames(第7步中声明)中的元素,或者nfunctionNames(第14步中声明)中的元素,Let initialValue等于undefined

  • f.a.5 否则(接上面的f.a.4步骤):

  • f.a.5.1 Let initialValue等于envRec.GetBindingValue(n, false)envRec在第3步中声明,GetBindingValue参考表1)

  • f.a.5.2 ReturnIfAbrupt(initialValue)

  • f.a.6 执行varEnvRec.InitializeBinding(n, initialValue)varEnvRec在27.e步或者28.c步中声明,InitializeBinding参考表1)

  • f.a.7 注意:形参中相同标识符的变量,当它们对应的形参初始化的时候,它们的值是一样的。(意思就是比如function func(x, x) {},调用时func(111),那么当第二个x初始化的时候,第一个x也就变成undefined了,因为它们的值要保持一致,所以最后x为undefined)

 29. 注意:附录B.3.3在这一点有额外的步骤(有兴趣可以去看看,主要是介绍了浏览器宿主环境对于块级函数声明的解析和规范的差异)

 30. 如果strict等于false,那么:

  • a.Let lexEnv等于NewDeclarativeEnvironment(varEnv)的值(即创建一个新的词法环境,它的ER里没有任何绑定,这个ER的外部或者说父级词法环境在这里就是varEnv)

  • b.注意:非严格模式下的函数对于顶层声明采用的是一个单独的词法作用域,因此直接调用evalvar a = eval; a(xx)这叫间接调用)能够对那些已经声明过的会导致冲突。在严格模式下这是不需要的,因为严格模式下的eval总是把声明放到一个新的ER中

    function qq(){var a = 1; eval('var a = 55;'); console.log(a);} // 输出55
    
    "use strict";
    function qq(){var a = 1; eval('var a = 55;'); console.log(a);} // 输出1
    

 31.否则(接上面的30步骤),Let lexEnv 等于varEnv(在27.d或者28.b中声明)

 32. Let lexEnvRec等于lexEnv的ER

 33. 将calleeContext(第1步中声明)的ER设置为lexEnv

 34. Let lexDeclarations等于函数的LexicallyScopedDeclarations

 35. 对于lexDeclarations中的每个元素d

  • a.注意:一个词法声明的标识符不能和函数,产生器函数,形参或者其他变量名相同。词法声明的标识符只会在这里实例化而不是初始化。

  • b.对于BoundNames中的每个元素dn

  • b.1 如果d是常量声明,那么:

  • b.1.1 Let status等于lexEnvRec.CreateImmutableBinding(dn, true)

  • b.1.2 Let status等于lexEnvRec.CreateMutableBinding(dn, false)

  • c.断言:status不可能是一个 abrupt completion

 36. 对于functionsToInitialize中的每个解析过的语法短语(这里的短语指的是编译原理里的短语)f

  • a.Let fn作为fBoundNames的唯一元素

  • b.Let fo等于执行InstantiateFunctionObject(f, lexEnv)的结果

  • InstantiateFunctionObject(f, lexEnv)

  • c.Let status等于varEnvRec.SetMutableBinding(fn, fo, false)

  • d.断言:status不可能是一个 abrupt completion

 37. 返回NormalCompletion(empty)(即返回 Completion{[[type]]: normal, [[value]]: empty, [[target]]:empty}

注意:附录B.3.3关于上面的算法提供了一种扩展,这种扩展对于浏览器在ES2015之前实现ECMAScript向后兼容是有必要的。(也就是我们常说的ployfill)

注意:形参的Initializers(即默认值)也许包含eval表达式。任何在这个eval里面声明的变量只能在这个eval内才能访问。

写在结尾

 在探索和翻译的过程中,确实是遇到了一些困难,包括到现在也还有一些困惑仍未解决。经过这次探索,想到一位大牛曾回答过"作为程序员,哪些网站是必须了解的"的问题,他的回答是"除了github和stackoverflow,应该没有其他是必须的",算是比较深刻的体会到了一这点,很多东西google和wiki都是找不到的,只能求助于so,没有的话还需要自己提问和gh上提issue。

 一条评论可能又会提到其他地方,其他地方又会链接到不同的人,不同的技术,不同的想法。这样都去浏览或者了解一番,便能开阔眼界,从一个单一知识点入手,不单单是解决这一个问题。或许我们还能学到很多新的知识,方式,想法,了解一些新的工具,认识一些有趣的人。


ne_smalltown
1.9k 声望38 粉丝

一个致力于电影,音乐,历史,自然,社会,美好事物的程序员