开门见山,我们来看看下面这个有趣的例子
对于上面这种用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
等于formals
的BoundNames
,即如果形参为x, y那么parameterNames
为['x', 'y']
8. 如果parameterNames
里有重复的,将hasDuplicates
置为true
,否则置为false
9. Let simpleParameterList
等于formals
的IsSimpleParameterList
IsSimpleParameterList
:如果形参为空或者只是普通的标识符则返回true,其他的如形参为rest参数(...x),普通参数加rest参数(x, ...y),参数有默认值,参数有解构赋值等等,都返回false
10. Let hasParameterExpressions
等于formals
的ContainsExpression
的值
ContainsExpression
:形参含有默认值则为true,否则为false
11. Let varNames
等于函数的VarDeclaredNames
(只包含函数体里的变量,不包含形参)的值
12. Let varDeclarations
等于函数的VarScopedDeclarations
的值
VarDeclaredNames
与VarScopedDeclarations
的区别:VarDeclaredNames
是一个类型为Name
的Set
(Name
只包含标识符名,作用域等等)。而VarScopedDeclarations
是一个类型为StatementListItem
的List
(StatementListItem
代表的是语句元素,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
等于d
的BoundNames
如果
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. 否则(接上)如果arguments
是parameterNames
(在第7步声明)的一个元素(也就是形参里面我们使用了arguments
作为标识符), 那么将argumentsObjectNeeded
赋值为false
20. 否则(接上)如果hasParameterExpressions
(在第10步声明)等于false
,那么
如果
arguments
是functionNames
(在第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
,用iteratorRecord
和undefined
作为参数的返回值
25. 否则(接上面的24步骤):
a.Let
formalStatus
等于formals
去调用IteratorBindingInitialization
,用iteratorRecord
和env
作为参数的返回值(可以看到只有最后一个参数和24步不一样)IteratorBindingInitialization(iteratorRecord,environment)
:当environment
为undefined
的时候,这意味着应该用一个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 append
n
到instantiatedVarNames
中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
的ERd.将
calleeContext
(第1步中声明)的VariableEnvironment
设为varEnv
e.Let
instantiatedVarNames
等于一个空的List
f.对于
varNames
中的每个元素n
:f.a 如果
n
不是instantiatedVarNames
中的元素,那么:f.a.1 append
n
到instantiatedVarNames
中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步中声明)中的元素,或者n
是functionNames
(第14步中声明)中的元素,LetinitialValue
等于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.注意:非严格模式下的函数对于顶层声明采用的是一个单独的词法作用域,因此直接调用
eval
(var 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
作为f
的BoundNames
的唯一元素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。
一条评论可能又会提到其他地方,其他地方又会链接到不同的人,不同的技术,不同的想法。这样都去浏览或者了解一番,便能开阔眼界,从一个单一知识点入手,不单单是解决这一个问题。或许我们还能学到很多新的知识,方式,想法,了解一些新的工具,认识一些有趣的人。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。