当你输入 var 后,JavaScript 引擎做了什么?

在 JavaScript 中,使用 var 声明变量时,背后涉及 编译阶段执行阶段 的复杂流程。以下是其底层行为的完整解析:


一、编译阶段:变量对象与提升

当代码执行前,JavaScript 引擎会先进行 编译(解析和准备工作)。此时,所有 var 声明会被处理:

  1. 创建变量对象(Variable Object, VO)
    每个执行上下文(全局或函数)都有一个关联的变量对象,用于存储变量和函数声明。

  2. 变量声明提升(Hoisting)

    • var 声明的变量会被添加到变量对象中,并初始化为 undefined
    • 声明的位置在代码中的位置被“提升”到作用域顶部,但 赋值操作保留在原始位置
// 原始代码
console.log(a); // undefined
var a = 10;
 
// 编译后的伪代码逻辑
var a = undefined; // 变量提升到作用域顶部
console.log(a);    // 输出 undefined
a = 10;            // 执行阶段赋值

二、执行阶段:作用域与赋值

当代码开始逐行执行时,引擎会做以下操作:

  1. 变量赋值
    var 声明的变量在编译阶段已存在,此时按顺序进行赋值:

    var a = 10; // 编译阶段 a = undefined → 执行阶段 a = 10
  2. 作用域链查找

    • 当访问变量时,引擎会从当前作用域的变量对象开始查找,沿作用域链向外层逐级搜索。
    • 若变量未找到,在非严格模式下会隐式创建全局变量(严格模式报错)。
function foo() {
  b = 20; // 非严格模式下,b 成为全局变量
}
foo();
console.log(b); // 20

三、函数作用域 vs 块级作用域

var 的作用域由其所在的 函数或全局作用域 决定,而非代码块(如 iffor):

  1. 函数作用域示例

    function test() {
      var x = 1;
      if (true) {
        var x = 2; // 同一作用域,覆盖外层 x
      }
      console.log(x); // 2
    }
  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 的底层存储机制

  1. 变量对象(VO)与活动对象(AO)

    • 在全局上下文中,变量对象即 window(浏览器环境)。
    • 在函数上下文中,变量对象称为活动对象(Activation Object, AO),存储参数、局部变量等。
  2. 内存分配

    • 编译阶段:变量被注册到变量对象,内存中分配空间并初始化为 undefined
    • 执行阶段:根据代码逻辑对变量进行赋值,内存中的值被更新。

六、与 let/const 的对比

特性varlet/const
作用域函数作用域块级作用域
提升声明提升,初始化为 undefined声明提升,但存在暂时性死区(TDZ)
重复声明允许禁止
全局声明成为 window 的属性不属于 window(模块作用域)

七、总结:var 的全流程

  1. 编译阶段

    • 解析代码,收集所有 var 声明。
    • 在变量对象中注册变量,初始化为 undefined
  2. 执行阶段

    • 按代码顺序执行赋值操作。
    • 作用域链决定变量的查找路径,函数作用域限制变量可见性。
  3. 特殊行为

    • 变量提升、重复声明、无块级作用域是 var 的核心特征。
    • 隐式全局变量(非严格模式)可能导致难以追踪的 Bug。

八、最佳实践

  • 优先使用 letconst:避免变量提升和污染全局作用域。
  • 启用严格模式:通过 '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 的透彻理解仍不可或缺。