一道看似简单但是90%的人都答错的js题目

1,194 阅读5分钟

直接上题目:

var a = 10
{
    a = 99
    function a(){}
    a = 30
}
console.log(a); // ?

答案: 99

如果你能答对并且知道原因,就不用往下看了(好像有点 听君一席话,如听一席话 的意思)

可能答案会让你感到诧异,Why?

按理来说,代码块中的 函数a 如果存在 块级作用域,那么因为 函数提升 的缘故,无论 a=99 还是 a=30 都是在修改 块级作用域 中的 函数a,为何会出现 a=99 作用到了外部但是 a=30 却没有的情况?

为了跟踪代码块中a的变化,我在每一行代码之间都打印了一下a

var a = 10

{
    console.log(1,a);// 1 [Function: a]
    a = 99
    console.log(2,a);// 2 99
    function a(){}
    console.log(3,a);// 3 99
    a = 30
    console.log(4,a);// 4 30
}

console.log(a);

通过打印结果来看,代码块中 函数a 的确提升了,后序 函数a 的值也随着 a 的赋值而改变,很合理。但这仅仅是代码块作用域内部 a 变量的变化。那么代码块外部全局作用域的 a 变量发生了什么变化呢?为何会被修改?又为何只有第一次 a=99 修改生效?

我们继续打印一下外部的a的变化

var a = 10
console.log('global:'+window.a);
// global:10
{
    console.log('block:'+a, 'global:'+window.a); 
    // block:[Function: a] global:10
    a = 99
    console.log('block:'+a, 'global:'+window.a);
    // block:99 global:10
    function a(){}
    console.log('block:'+a, 'global:'+window.a);
    // block:99 global:99
    a = 30
    console.log('block:'+a, 'global:'+window.a);
    // block:30 global:99
}
console.log('global:'+window.a);
// global:99

console.log(a);

我们发现外部作用域的a是在 function a(){} 语句执行时被修改为99的,Why? function a(){} 到底发生了什么事?

我在 Function declaration in block moving temporary value outside of block? 找到了答案。

我们将代码块外部 全局作用域的变量a 记为 ,代码块的 块级作用域的变量a 记为 ,代码的大致执行流程如下

var a¹ = 10;
 {
   function a²() {} // 函数提升
   a² = 99;
   a¹ = a²; // function a²() {}函数的声明语句执行,将内部的a值同步到外部的a(变量突破)此时外部的a和内部的a值都为99a² = 30;
}
console.log(a¹);
  • 最开始执行 var a = 10 被赋值为 10
  • 遇到一个可执行代码块,产生一个块级作用域,初始化时将函数a 提升 到作用域顶部,所以此时为声明的函数
  • 执行到 var a = 99 被赋值为 99
  • 执行到 function a() {},重点来了,执行到函数的声明语句时,会将块级作用域中函数对应的变量同步到外层作用域(这个行为目前的官方名词叫什么不知道,我的同事称之为:变量突破),所以此时 被修改为和 一样的值,都是 99
  • 然后执行到 var a = 30 被赋值为 30
  • 最后执行 console.log(a),输出 ,为 99

也就是说:在块级作用域中声明的函数,会函数提升到作用域顶部,并且执行到函数的声明语句时,会将其值同步到外层作用域。

然后我们通过以下代码来验证

var a = 10
console.log(window.a);// 10
{
    console.log(window.a);// 10
    a = 99
    console.log(window.a);// 10
    function a(){}// 变量突破
    console.log(window.a); // 99
    a = 10
    console.log(window.a);// 99
    function a(){}// 变量突破
    console.log(window.a);// 10
    a = 12
}
console.log(a);// 10

上面代码中 函数a 存在两处声明语句,在第一次声明时,将内部 a 的值 99 同步到了外部,第二次同理将 10 同步到了外部。所以最终外部的 a 值为 10

为什么要出现这种现象?

对于在块中声明的函数,在 es6 之前,函数的声明会被提升到外层的作用域(当时还不存在块级作用域,也不算外层),这是早已存在的特性,就不赘述了。

以下代码是IE7的执行结果,完全避开的块级作用域的影响

typeof foo; // "function"
if (true) {
  // 一旦解析了块,`foo`将被声明并可用于整个范围
  function foo(){ return 1; }
}
typeof foo; // "function"

即使没有命中 if 的条件,foo 依然会被提升,这可能不太符合逻辑,我们更加期望只有 if 的条件为 true 时外部才能调用 foo,但是没办法,es6 以前就是这样。

到了 es6,由于块级作用域的诞生,这导致块中的函数由于块级作用域的阻挡无法提升到外层,这种无法支持以往特性的大变动当然是不被 ECMAScript规范 所允许的。

var 关键字没有被添加块级作用域的特性,所以不会产生这个问题

所以他们使用了一个折中的方案,在执行函数声明语句时将函数值突破块级作用域同步到外部的方式来兼容旧浏览器。这虽然导致 function 的声明无法被提升到外部,只有当 function 确实被声明时,才会使其在上级作用域中可用,这使其变得更合理,尤其是分支语句中声明函数时。

typeof foo; // "undefined"
if (true) {
  // 一旦执行了声明语句,`foo`将可用于整个范围
  function foo(){ return 1; }
}
typeof foo; // "function"

总的来说就是:为了es6的块级作用域 与 旧特性 做的折中兼容。

总结

es6以后,在块级作用域中声明的函数,会函数提升到作用域顶部,并且执行到函数的声明语句时,会将其值同步到外层作用域。