JavaScript 变量提升(Hoisting)深度解析

4 阅读4分钟

JavaScript 变量提升(Hoisting)深度解析

最近深入学习了JS的变量提升底层逻辑,写下此篇和大家分享。


一、从一个反直觉的例子开始

showName();               // 函数showName被执行了
console.log(myName);      // undefined
var myName = '极客时间';
console.log(myName);      // 极客时间
console.log(add);         // undefined

function showName() {
    console.log('函数showName被执行了');
}

var add = function(x, y) {
    return x + y;
};

按"代码一行一行执行"的直觉,第一行 showName() 应该报错,myName 也应该报错——但事实是:

  • 函数在声明之前调用,正常执行
  • 变量在声明之前使用,不报错,但值是 undefined

这就是变量提升(Hoisting)


二、什么是变量提升

变量提升是指:在 JS 代码执行过程中,JS 引擎把变量的声明部分和函数的声明部分提升到作用域开头的行为。变量提升后,会给变量设置默认值 undefined。

总结三条规则:

情况结果
使用未声明的变量ReferenceError: xxx is not defined
var 声明之前使用变量不报错,值为 undefined
在函数声明之前调用函数不报错,正常执行

后两条说明:JS 代码不是一行一行执行的,在执行之前还有一个编译阶段。


三、变量提升的细分情况

3.1 普通变量(var)

// 编译阶段:var myName = undefined;  (只有声明被提升)
// 执行阶段:才执行赋值
console.log(myName);   // undefined
var myName = '极客时间';
console.log(myName);   // 极客时间

3.2 函数声明

// 编译阶段:整个函数声明被提升(函数是一等公民)
showName();            // 正常执行

function showName() {
    console.log('执行了');
}

3.3 函数表达式

console.log(add);     // undefined

var add = function(x, y) {
    return x + y;
};

函数表达式本质上是一个变量赋值——编译阶段只有 var add = undefined;,赋值操作在执行阶段才发生。所以在赋值之前调用 add() 会报错。


四、变量提升的"物理移动"是误解

很多人认为"变量提升就是把声明移动到作用域最前面",但这是不准确的

事实是:变量和函数声明的代码位置不会改变,而是在编译阶段被 JS 引擎放入内存中。

// 源代码不会变成这样:
// var myName = undefined;
// function showName() { ... }
// showName();
// myName = '极客时间';

// 而是在编译阶段,JS 引擎将声明存入执行上下文,执行阶段再按顺序执行。

五、JS 代码的执行流程

源代码  →  [编译阶段][执行阶段]
              ↓
     生成两部分内容:
     1. 执行上下文(Execution Context)
     2. 可执行代码(字节码)

执行上下文

执行上下文是 JS 执行一段代码的运行环境,由两部分组成:

  • 变量环境(Variable Environment):存放 var 声明的变量和函数声明
  • 词法环境(Lexical Environment):存放 letconst 声明的变量

每次调用一个函数,都会创建一个新的执行上下文,压入调用栈。


六、let / const 为什么"没有"提升

console.log(myName);                          // 报错!
let myName = 'adofa';
// ReferenceError: Cannot access 'myName' before initialization

letconst 实际上也提升了(在编译阶段分配了内存),但它们的存储位置是词法环境,而不是变量环境。

在声明之前,词法环境中的变量处于暂时性死区(TDZ, Temporal Dead Zone),访问会直接报错。

对比维度varlet / const
是否提升
存储位置变量环境词法环境
声明前访问undefined报错(TDZ)
可重复声明允许不允许

七、用"模拟提升"来理解

理解了编译/执行两阶段后,可以手动模拟编译阶段的效果。比如原始代码:

showName();
console.log(myName);
var myName = 'aaaf';

function showName() {
    console.log('执行showName');
}

编译阶段"等价于":

// === 编译阶段完成的动作 ===
var myName = undefined;
function showName() {
    console.log('执行showName');
}

// === 执行阶段按顺序执行 ===
showName();              // 执行showName
console.log(myName);     // undefined
myName = 'aaaf';

八、完整的内存模型

以这段代码为例:

showName();
console.log(myName);
var myName = '...';

function showName() {
    var a = 1;
    console.log('showName');
}

编译阶段 1(全局)

GlobalExecutionContext = {
    变量环境: {
        myName: undefined,
        showName: function() { ... }
    },
    词法环境: {}
}

执行阶段:调用 showName() → 编译阶段 2

ShowNameExecutionContext = {
    变量环境: {
        a: undefined
    },
    词法环境: {}
}

每次进入一个新的函数调用,都会创建新的执行上下文。这正是"调用栈"的基础。


九、一句话总结

变量提升的本质,是 JS 引擎在编译阶段完成变量和函数声明的内存分配。var 和函数声明存入变量环境(可在声明前访问),let/const 存入词法环境(声明前为 TDZ,访问即报错)。代码位置不变,变的只是内存中"有没有"这个东西。


基于 V8 引擎原理整理,适合配合动手调试加深理解。