当你输入 var
后,JavaScript 引擎做了什么?
在 JavaScript 中,使用 var
声明变量时,背后涉及 编译阶段 和 执行阶段 的复杂流程。以下是其底层行为的完整解析:
一、编译阶段:变量对象与提升
当代码执行前,JavaScript 引擎会先进行 编译(解析和准备工作)。此时,所有 var
声明会被处理:
-
创建变量对象(Variable Object, VO)
每个执行上下文(全局或函数)都有一个关联的变量对象,用于存储变量和函数声明。 -
变量声明提升(Hoisting)
var
声明的变量会被添加到变量对象中,并初始化为undefined
。- 声明的位置在代码中的位置被“提升”到作用域顶部,但 赋值操作保留在原始位置。
// 原始代码
console.log(a); // undefined
var a = 10;
// 编译后的伪代码逻辑
var a = undefined; // 变量提升到作用域顶部
console.log(a); // 输出 undefined
a = 10; // 执行阶段赋值
二、执行阶段:作用域与赋值
当代码开始逐行执行时,引擎会做以下操作:
-
变量赋值
var
声明的变量在编译阶段已存在,此时按顺序进行赋值:var a = 10; // 编译阶段 a = undefined → 执行阶段 a = 10
-
作用域链查找
- 当访问变量时,引擎会从当前作用域的变量对象开始查找,沿作用域链向外层逐级搜索。
- 若变量未找到,在非严格模式下会隐式创建全局变量(严格模式报错)。
function foo() {
b = 20; // 非严格模式下,b 成为全局变量
}
foo();
console.log(b); // 20
三、函数作用域 vs 块级作用域
var
的作用域由其所在的 函数或全局作用域 决定,而非代码块(如 if
、for
):
-
函数作用域示例
function test() { var x = 1; if (true) { var x = 2; // 同一作用域,覆盖外层 x } console.log(x); // 2 }
-
块级作用域的缺失
for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i)); // 输出 3, 3, 3 }
- 所有异步回调共享同一个
i
,循环结束后i
的值为 3。
- 所有异步回调共享同一个
四、重复声明与覆盖
var
允许在同一作用域内重复声明变量,可能导致意外覆盖:
var x = 1;
var x = 2; // 合法,x 被重新赋值为 2
五、var
的底层存储机制
-
变量对象(VO)与活动对象(AO)
- 在全局上下文中,变量对象即
window
(浏览器环境)。 - 在函数上下文中,变量对象称为活动对象(Activation Object, AO),存储参数、局部变量等。
- 在全局上下文中,变量对象即
-
内存分配
- 编译阶段:变量被注册到变量对象,内存中分配空间并初始化为
undefined
。 - 执行阶段:根据代码逻辑对变量进行赋值,内存中的值被更新。
- 编译阶段:变量被注册到变量对象,内存中分配空间并初始化为
六、与 let
/const
的对比
特性 | var | let /const |
---|---|---|
作用域 | 函数作用域 | 块级作用域 |
提升 | 声明提升,初始化为 undefined | 声明提升,但存在暂时性死区(TDZ) |
重复声明 | 允许 | 禁止 |
全局声明 | 成为 window 的属性 | 不属于 window (模块作用域) |
七、总结:var
的全流程
-
编译阶段
- 解析代码,收集所有
var
声明。 - 在变量对象中注册变量,初始化为
undefined
。
- 解析代码,收集所有
-
执行阶段
- 按代码顺序执行赋值操作。
- 作用域链决定变量的查找路径,函数作用域限制变量可见性。
-
特殊行为
- 变量提升、重复声明、无块级作用域是
var
的核心特征。 - 隐式全局变量(非严格模式)可能导致难以追踪的 Bug。
- 变量提升、重复声明、无块级作用域是
八、最佳实践
- 优先使用
let
和const
:避免变量提升和污染全局作用域。 - 启用严格模式:通过
'use strict'
禁止隐式全局变量。 - 使用工具检查:通过 ESLint 的
no-var
规则强制代码规范。
// 现代代码示例
'use strict';
const MAX_SIZE = 100; // 常量
let count = 0; // 可变量
function processData() {
for (let i = 0; i < 5; i++) { // 块级作用域
setTimeout(() => console.log(i)); // 0,1,2,3,4
}
}
理解 var
的底层行为,不仅是掌握 JavaScript 历史的关键,更是深入理解作用域、闭包等高级概念的基石。尽管现代开发中已转向 let
/const
,但对 var
的透彻理解仍不可或缺。