首页 文章详情

与周爱民老师探讨了两天之久的一道题

全栈前端精选 | 424 2021-05-07 06:21 0 0 0
UniSMS (合一短信)

这道题目来源于社区的群聊,自己思考了很久,不明所以。于是乎找到了周爱民老师,和老师大概探讨了两天之久,在老师的一步步带领下,终于找到了答案。

文中内容是我根据题目的一系列思考过程和推测,如有错误,恳请斧正。

昨天的结论有些小问题,怕误导小伙伴们,重新发下。

注意:本文中所有示例,描述的都是非严格模式下的情况。

出题

最近群内一直在聊一道题,大概题目如下:

{
  a = 1
  function a({}
  a = 2
  console.log(a)
}
console.log(a)

这段代码输出什么?为什么?

常规思考

首先,按照自己的理解,来看下这个题目。

正常思路,抛开一切乱七八糟的内容

  1. 首先,这里涉及到变量和函数的提升
  2. 函数提升优于变量
  3. 函数提升时,会带着函数体一起
  4. 变量提升只会提升声明,而赋值操作则在运行时
  5. 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 中我可以看到如下信息:

B3.3

上面中提到了三种情况,第一种情况属于正常范畴

但是,第2种和第3种情况针对于我们所熟知的规范进行了修改调整:

  • FunctionDeclarationInstantiation
  • GlobalDeclarationInstantiation
  • EvalDeclarationInstantiation

而这些属于兼容性语义的范畴,因此,每个内核的实现可能存在差异。

其实出现本文开头题目输出和预期不符的问题的说明,主要出现在B3.3.2 GlobalDeclarationInstantiation

B3.3.2

其他大概含义是,全局中的 declaredFunctionNames 和 declaredVarNames 都会存储在 declaredFunctionOrVarNames 列表当中。

而直接包含在 script 中的 block,case 子句或者 default 子句的语句列表中的每个函数声明,都会进行上图中的操作。

大概意思是:

  1. 搞个变量 F 存储函数声明 f 标识符一致
  2. 如果 F 把函数声明 f 替换掉,不会对 script 造成影响,则继续后续操作
  3. 判断块中函数声明的名字是否可以在全局中定义,如果可以,则在全局中创建。
    • 注意:此时 block 块中还没有声明 F
  4. 当函数声明 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 等内核的相关实现

至此,已合理解释了这个问题。

总结

  1. 非严格模式下,不要在 block 中编写函数声明,可能会造成意想不到的 Bug
  2. 多看看标准,少踩坑
  3. 阅读 mdn 的话,英文为主,中文为辅。(中文更新不及时)
  4. 有能力的话,可以啃一啃 ecma
  5. 近期我对 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
good-icon 0
favorite-icon 0
收藏
回复数量: 0
    暂无评论~~
    Ctrl+Enter