一道刁钻的Annex B(Web 兼容语义)笔试题

50 阅读4分钟

代码示例及预期 vs 实际打印结果

以下是一段 JavaScript 代码,请先尝试预测它的打印结果:

var a = 0;
console.log(a, window.a); // 预期: 0 0, 实际: 0 0

if (true) {
  a = 2;
  console.log(a, window.a); // 预期: 2 2, 实际: 2 0
  function a() { }
  console.log(a, window.a); // 预期: function a() {} function a() {}, 实际: 2 2
  a = 3;
  console.log(a, window.a); // 预期: 3 3, 实际: 3 2
  console.log('里面', a); // 预期: 里面 3, 实际: 里面 3
}

console.log('外面', a); // 预期: 外面 3, 实际: 外面 2

为什么打印结果不符合预期?

这段代码的行为涉及到 JavaScript 中变量提升、作用域和块级函数声明的特殊行为(尤其是在非严格模式下)。以下是逐步解释:

关键机制

  1. 全局变量声明var a = 0 在全局作用域中声明变量 a,并赋值为 0。在浏览器环境中,全局变量自动成为 window 对象的属性,因此 window.a 也是 0。第一个 console.log 输出 0 0,符合预期。

  2. 进入 if 块:在非严格模式下,块内的函数声明(如 function a() {})会创建一个块级作用域的变量 a(类似于使用 let 声明)。这个变量在块内被提升,与全局变量 a 分离。因此,当进入 if 块时,实际上有一个新的变量 a 在块作用域内。

  3. 赋值操作 a = 2:这里赋值的是块级变量 a,而不是全局变量。所以块级变量 a 变为 2,而全局变量 a(即 window.a)仍然是 0。因此第二个 console.log 输出 2 0(块级 a2,全局 window.a0)。

  4. 执行函数声明 function a() {}:在非严格模式下,当执行到这一行时,JavaScript 会将当前块级变量 a 的值(此时是 2复制到全局变量 a 上。因此,全局变量 a 变为 2。注意,函数声明本身不会改变块级变量 a 的值(因为函数声明在提升阶段已经处理,执行到这行时只是同步值)。所以第三个 console.log 输出 2 2(块级 a2,全局 window.a2)。

  5. 再次赋值 a = 3:这里赋值的是块级变量 a,所以块级变量变为 3,但全局变量 a 保持不变(仍然是 2)。因此第四个 console.log 输出 3 2(块级 a3,全局 window.a2)。第五个 console.log 输出 里面 3,指的是块级变量 a

  6. 退出 if 块:当退出 if 块后,块级变量 a 不再可访问。此时访问的 a 是全局变量 a,其值为 2。所以最后一个 console.log 输出 外面 2

为什么预期错误?

  • 预期 a = 2 会修改全局变量:实际上,由于块级函数声明的存在,a = 2 修改的是块级变量,而不是全局变量。
  • 预期 function a() {} 会将变量设置为函数:实际上,它只同步块级变量的当前值到全局变量,并不会改变块级变量的值(函数声明在提升阶段已处理)。
  • 预期所有赋值都会影响全局变量:实际上,只有通过函数声明同步的那一次影响了全局变量,后续赋值只影响块级变量。

这种行为的根源

这种行为是 JavaScript 非严格模式下的历史遗留特性,旨在兼容旧代码。规范中的 Annex B(Web 兼容语义)定义了这种特殊处理:在非严格模式下,块级函数声明会同时创建块级变量和影响外层变量。

如何避免这种困惑?

  1. 使用严格模式:在脚本开头添加 'use strict',这会禁用这种特殊行为。在严格模式下,块级函数声明完全局限于块内,不会影响全局变量。
  2. 避免块级函数声明:在块内使用函数表达式(如 const a = function() {})而不是函数声明。
  3. 使用 let/const:优先使用 letconst 声明变量,它们具有清晰的块级作用域,不会出现这种混淆。

严格模式下的示例

如果添加 'use strict',代码行为会更直观:

'use strict';
var a = 0;
console.log(a, window.a); // 0 0

if (true) {
  a = 2; // 这里会抛出错误(严格模式下,a 未定义在块内),或者如果块内有函数声明,则 a 是块级变量。
  // ... 其他代码会不同
}
// 最终输出会改变

总之,这段代码的打印结果不符合预期是因为非严格模式下块级函数声明的特殊同步行为。理解这一点有助于避免类似陷阱。