-
Notifications
You must be signed in to change notification settings - Fork 4
Description
一道题引发的一系列思考🤔
这道题目来源于群聊,自己思考了很久,不明所以。于是乎找到了周爱民老师,和老师大概探讨了两天之久,在老师的一步步带领下,终于找到了答案。
文中内容是我根据题目的一系列思考过程和推测,如有错误,恳请斧正。
注意:本文中所有示例,描述的都是非严格模式下的情况。
出题
最近群内一直在聊一道题,大概题目如下:
{
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
1WTF!!! 发生了什么?
从执行结果看,可以看出 v8 和 SpiderMonkey 实现一致,但与 JavaScriptCore、ChakraCore 均不相同。
哪里出了问题?
那究竟是哪里的问题?变量?函数?
社区解释
于是乎,查阅资料。。。
大概会有如下的机制
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
a = function () {}
a = 1 // 赋值给了词法环境中的 a
a = a // 运行时 函数声明 执行时,会将词法环境与变量环境绑定
a = 2 // 赋值给词法环境中的 a
console.log(a) // 输出词法环境中的 a
}
console.log(a) // 输出变量环境中的 a如需进一步验证,需深入阅读 ecma 标准以及 v8 等内核的相关实现。
至此,已合理解释了这个问题。
总结
- 非严格模式下,不要在 block 中编写函数声明,可能会造成意想不到的 Bug
- 多看看标准,少踩坑
- 阅读 mdn 的话,英文为主,中文为辅。(中文更新不及时)
- 有能力有时间,可以啃一啃 ecma 标准。
如有错误,恳请斧正。
参考链接
- js 关于函数声明提升的问题?
- 如何理解 ES6 以后的 block-level function declaration 和 Web Legacy Compatibility Semantics
- What are the precise semantics of block-level functions in ES6?
- Block-level functions and web extensions
- Block-level function declarations Web Legacy Compatibility bug
- ecma262 B 3.3 Block-Level Function Declarations Web Legacy Compatibility Semantics
- ecma262 B 3.3.2 GlobalDeclarationInstantiation