与周爱民老师探讨了两天之久的一道题
这道题目来源于社区的群聊,自己思考了很久,不明所以。于是乎找到了周爱民老师,和老师大概探讨了两天之久,在老师的一步步带领下,终于找到了答案。
文中内容是我根据题目的一系列思考过程和推测,如有错误,恳请斧正。
昨天的结论有些小问题,怕误导小伙伴们,重新发下。
注意:本文中所有示例,描述的都是非严格模式下的情况。
出题
最近群内一直在聊一道题,大概题目如下:
{
a = 1
function a() {}
a = 2
console.log(a)
}
console.log(a)
这段代码输出什么?为什么?
常规思考
首先,按照自己的理解,来看下这个题目。
正常思路,抛开一切乱七八糟的内容
首先,这里涉及到变量和函数的提升 函数提升优于变量 函数提升时,会带着函数体一起 变量提升只会提升声明,而赋值操作则在运行时 block 环境外,应无法访问函数
按照这种思想,那输出的结果为:
2
ReferenceError
但是!!!,事实并非如此。
意外发生
我在 Chrome 中运行这段代码时,发现了与预想截然不同的结果:
2
1
为了排除内核差异,在各内核中的执行结果如下:
#### ChakraCore
2
function a() {}
#### JavaScriptCore
2
2
#### Moddable XS
2
ReferenceError: ?: get a: undefined variable
#### SpiderMonkey
2
1
#### V8
2
1
WTF!!! 发生了什么?
从执行结果看,可以看出 v8 和 SpiderMonkey 实现一致,但与 JavaScriptCore、ChakraCore 均不相同,只有 Moddable 表现与猜想一致。
哪里出了问题?
那究竟是哪里的问题?函数?
社区解释
于是乎,查阅资料。。。
大概会有如下的机制
function enclosing(…) {
…
{
…
function compat(…) { … }
…
}
…
}
类似于
function enclosing(…) {
var compat₀ = undefined; // function-scoped
…
{
let compat₁ = function compat(…) { … }; // block-scoped
…
compat₀ = compat₁;
…
}
…
}
那我们的代码,如果按照这种思考方式来改写的话,我觉得转换后的代码应该是这样滴:
// 可以按照此代码来理解本地,基本无误 ✅
var a1
{
let a2 = function a() {};
a2 = 1;
a1 = a2; // 原来函数声明的位置
a2 = 2;
console.log(a2);
}
console.log(a1);
看下输出,符合 v8 和 SpiderMonkey 的结果:
2
1
注意:这种解释是社区开发者为了帮助大家易于理解,所提供的伪代码的形式。
真相
在查阅了大量资料后,以及爱民老师的指导下,最终接近了真相。
这道题主要原因出在块级作用域(block)中的 function
:
MDN
MDN 中关于 block 的解释是:
Variables declared with var or created by function declarations in non-strict mode do not have block scope. Variables introduced within a block are scoped to the containing function or script, and the effects of setting them persist beyond the block itself. In other words, block statements do not introduce a scope.
解释下,就是当使用 var 进行声明或创建函数声明时,在非严格模式下不具有块级作用域。
但是看了文章开头的代码,你就会觉得这段描述并不全面。
然后继续查看 mdn 的话,就会发现一句短小精悍的话:
In non-strict code, function declarations inside blocks behave strangely. Do not use them.
函数声明在 block 中的表现会很奇怪,应该避免使用它们!
虽然在 MDN 中没有找到答案,但是我们得到一个关键信息,就是非严格模式下,不要在 block 中声明函数。
嗯,MDN 没找到答案,只能去 ecma 中找答案了。
ecma262
我们知道,在 ES5 以及之前,ECMAScript 并没有定义块级函数这种语法:函数声明作为 block 语句中的一个元素出现。但是当时很多浏览器内核中 ECMAScript 实现将其作为一种扩展进行了各自的支持,而这带来的结果是不同的实现中相同语法的语义却不同。
而我们这里主要参考 ecma262 的标准附录 B3.3 Block-Level Function Declarations Web Legacy Compatibility Semantics。
从规范 B3.3 中我可以看到如下信息:
上面中提到了三种情况,第一种情况属于正常范畴
但是,第2种和第3种情况针对于我们所熟知的规范进行了修改调整:
FunctionDeclarationInstantiation GlobalDeclarationInstantiation EvalDeclarationInstantiation
而这些属于兼容性语义的范畴,因此,每个内核的实现可能存在差异。
其实出现本文开头题目输出和预期不符的问题的说明,主要出现在B3.3.2 GlobalDeclarationInstantiation
其他大概含义是,全局中的 declaredFunctionNames 和 declaredVarNames 都会存储在 declaredFunctionOrVarNames 列表当中。
而直接包含在 script 中的 block,case 子句或者 default 子句的语句列表中的每个函数声明,都会进行上图中的操作。
大概意思是:
搞个变量 F 存储函数声明 f 标识符一致 如果 F 把函数声明 f 替换掉,不会对 script 造成影响,则继续后续操作 判断块中函数声明的名字是否可以在全局中定义,如果可以,则在全局中创建。 注意:此时 block 块中还没有声明 F 当函数声明 f 被执行时,会执行与我们日常理解的运行时语义环境不同的操作, 将 F 与执行上下文的变量环境和词法环境绑定
用代码解释:
// 块中的函数会在全局定义一个 var a
console.log(a) // 由函数提升上来的变量
{
// 函数提升,并且在词法环境声明了 a
let a = function a() {}
a = 1 // 赋值给了词法环境中的 a
function a() {} // 函数声明执行时,会绑定变量环境(var)与词法环境(let)
a = 2 // 赋值给词法环境中的 a
console.log(a) // 输出词法环境中的 a
}
console.log(a) // 输出变量环境中的 a
如需进一步验证,深入阅读 ecma 标准以及 v8 等内核的相关实现。
至此,已合理解释了这个问题。
总结
非严格模式下,不要在 block 中编写函数声明,可能会造成意想不到的 Bug 多看看标准,少踩坑 阅读 mdn 的话,英文为主,中文为辅。(中文更新不及时) 有能力的话,可以啃一啃 ecma 近期我对 ecma 中文文档进行重构,有兴趣的可以联系小助手
如有错误,恳请斧正。
参考链接
js 关于函数声明提升的问题?— https://www.zhihu.com/question/53191567) 如何理解 ES6 以后的 block-level function declaration 和 Web Legacy Compatibility Semantics — https://www.bruceyj.com/front-end-interview-summary/front-end/JavaScript/7-block-level-function.html What are the precise semantics of block-level functions in ES6? — https://stackoverflow.com/questions/31419897/what-are-the-precise-semantics-of-block-level-functions-in-es6 Block-level functions and web extensions — https://github.com/estools/escope/issues/73 Block-level function declarations Web Legacy Compatibility bug — https://esdiscuss.org/topic/block-level-function-declarations-web-legacy-compatibility-bug ecma262 B 3.3 Block-Level Function Declarations Web Legacy Compatibility Semantics — https://tc39.es/ecma262/#sec-block-level-function-declarations-web-legacy-compatibility-semantics ecma262 B 3.3.2 GlobalDeclarationInstantiation — https://tc39.es/ecma262/#sec-web-compat-globaldeclarationinstantiation