代码示例及预期 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 中变量提升、作用域和块级函数声明的特殊行为(尤其是在非严格模式下)。以下是逐步解释:
关键机制
-
全局变量声明:
var a = 0在全局作用域中声明变量a,并赋值为0。在浏览器环境中,全局变量自动成为window对象的属性,因此window.a也是0。第一个console.log输出0 0,符合预期。 -
进入 if 块:在非严格模式下,块内的函数声明(如
function a() {})会创建一个块级作用域的变量a(类似于使用let声明)。这个变量在块内被提升,与全局变量a分离。因此,当进入 if 块时,实际上有一个新的变量a在块作用域内。 -
赋值操作
a = 2:这里赋值的是块级变量a,而不是全局变量。所以块级变量a变为2,而全局变量a(即window.a)仍然是0。因此第二个console.log输出2 0(块级a是2,全局window.a是0)。 -
执行函数声明
function a() {}:在非严格模式下,当执行到这一行时,JavaScript 会将当前块级变量a的值(此时是2)复制到全局变量a上。因此,全局变量a变为2。注意,函数声明本身不会改变块级变量a的值(因为函数声明在提升阶段已经处理,执行到这行时只是同步值)。所以第三个console.log输出2 2(块级a是2,全局window.a是2)。 -
再次赋值
a = 3:这里赋值的是块级变量a,所以块级变量变为3,但全局变量a保持不变(仍然是2)。因此第四个console.log输出3 2(块级a是3,全局window.a是2)。第五个console.log输出里面 3,指的是块级变量a。 -
退出 if 块:当退出 if 块后,块级变量
a不再可访问。此时访问的a是全局变量a,其值为2。所以最后一个console.log输出外面 2。
为什么预期错误?
- 预期
a = 2会修改全局变量:实际上,由于块级函数声明的存在,a = 2修改的是块级变量,而不是全局变量。 - 预期
function a() {}会将变量设置为函数:实际上,它只同步块级变量的当前值到全局变量,并不会改变块级变量的值(函数声明在提升阶段已处理)。 - 预期所有赋值都会影响全局变量:实际上,只有通过函数声明同步的那一次影响了全局变量,后续赋值只影响块级变量。
这种行为的根源
这种行为是 JavaScript 非严格模式下的历史遗留特性,旨在兼容旧代码。规范中的 Annex B(Web 兼容语义)定义了这种特殊处理:在非严格模式下,块级函数声明会同时创建块级变量和影响外层变量。
如何避免这种困惑?
- 使用严格模式:在脚本开头添加
'use strict',这会禁用这种特殊行为。在严格模式下,块级函数声明完全局限于块内,不会影响全局变量。 - 避免块级函数声明:在块内使用函数表达式(如
const a = function() {})而不是函数声明。 - 使用 let/const:优先使用
let或const声明变量,它们具有清晰的块级作用域,不会出现这种混淆。
严格模式下的示例
如果添加 'use strict',代码行为会更直观:
'use strict';
var a = 0;
console.log(a, window.a); // 0 0
if (true) {
a = 2; // 这里会抛出错误(严格模式下,a 未定义在块内),或者如果块内有函数声明,则 a 是块级变量。
// ... 其他代码会不同
}
// 最终输出会改变
总之,这段代码的打印结果不符合预期是因为非严格模式下块级函数声明的特殊同步行为。理解这一点有助于避免类似陷阱。