参数默认值引发的第三做用域

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

 对于上面这种用var的声明方式,不管x的默认值为何,只要形参中出现了默认值,zzz都会被看成块级做用域中的值。git

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

 看完后我将个人想法进一步告诉了他,个人想法能够用下面这5张图来归纳。github

我认为这是chrome的bug,若是说是block,那么出了这个块就不应被访问到,可是事实是能访问到算法

 并且从自己的语法来说,他也不该该是block,而是function scope。chrome

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

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

8.1 词法环境(LexicalEnvironment)

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

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

 一个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。

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

相关文章
相关标签/搜索