Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

一道题引发的一系列思考🤔 #60

Open
QC-L opened this issue Mar 26, 2021 · 0 comments
Open

一道题引发的一系列思考🤔 #60

QC-L opened this issue Mar 26, 2021 · 0 comments

Comments

@QC-L
Copy link
Owner

QC-L commented Mar 26, 2021

一道题引发的一系列思考🤔

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

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

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

出题

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

{
  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 均不相同。

哪里出了问题?

那究竟是哪里的问题?变量?函数?

社区解释

于是乎,查阅资料。。。

大概会有如下的机制

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 
  a = function () {}
  a = 1 // 赋值给了词法环境中的 a
  a = a // 运行时 函数声明 执行时,会将词法环境与变量环境绑定
  a = 2 // 赋值给词法环境中的 a
  console.log(a) // 输出词法环境中的 a
}
console.log(a) // 输出变量环境中的 a

如需进一步验证,需深入阅读 ecma 标准以及 v8 等内核的相关实现。

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

总结

  1. 非严格模式下,不要在 block 中编写函数声明,可能会造成意想不到的 Bug
  2. 多看看标准,少踩坑
  3. 阅读 mdn 的话,英文为主,中文为辅。(中文更新不及时)
  4. 有能力有时间,可以啃一啃 ecma 标准。

如有错误,恳请斧正。

参考链接

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant