源本科技 | 码上会

JavaScript 变量提升

2025/12/30
23
0

学习目标

  • 理解 JavaScript 中“提升”的本质及其发生时机

  • 掌握 varletconst 在提升行为上的关键差异

  • 了解函数声明与函数表达式的提升规则

  • 掌握暂时性死区(Temporal Dead Zone, TDZ)的概念与影响

  • 能够避免因提升导致的常见运行时错误


什么是变量提升

提升是 JavaScript 引擎在编译阶段将变量、函数和类的声明移动到其作用域顶部的行为。

注意:只有声明被提升,初始化(赋值)不会被提升!

这意味着,无论你在代码何处声明变量或函数,JavaScript 都会“提前知道”它们的存在——但能否安全使用,取决于声明方式。


暂时性死区

Temporal Dead Zone, TDZ

在深入提升之前,必须理解 TDZ

  • TDZ 是指从进入作用域开始,到变量实际初始化完成之间的这段时间

  • 在此期间,访问 letconst 声明的变量会抛出 ReferenceError

  • var 没有 TDZ,它会被初始化为 undefined

console.log(x); // ReferenceError: 初始化前无法访问 'x'
let x = 10;

关键点let/const 虽然也被“提升”,但在初始化前处于“不可访问”状态。


不同声明方式的提升行为

1. var 的提升

声明提升 + 初始化为 undefined

console.log(a); // undefined
var a = 5;

等效于:

var a;          // 声明被提升
console ---> undefined
a = 5;          // 赋值留在原地

特点:

  • 不会报错,但值为 undefined

  • 允许重复声明(后声明覆盖前声明)

var a = 10;
var a = 20;
console.log(a); // 20

2. let / const 的提升

提升但处于 TDZ

console.log(b); // ReferenceError
let b = 10;

尽管 b 的声明被提升到块顶部,但在 let b = ... 执行前,它处于 TDZ,无法访问。

优势:防止在声明前意外使用变量,提高代码健壮性。


函数的提升

1. 函数声明

完整提升

greet(); // 正常输出:"Hello, Mahima!"
function greet() {
    console.log("Hello, Mahima!");
}

整个函数体(包括名称和实现)都被提升,可以在声明前调用。


2. 函数表达式

仅变量名提升

hello(); // TypeError: hello is not a function
var hello = function() {
    console.log("Hi!");
};

等效于:

var hello;        // 声明提升 → hello = undefined
hello();          // undefined() → TypeError
hello = function(){...}; // 赋值留在原地

即使使用 let,也会因 TDZ 报错:

hi(); // ReferenceError
let hi = function() { };

类的提升

类声明也会被提升,但受 TDZ 限制

const obj = new MyClass(); // ReferenceError
class MyClass {
    constructor() {
        this.name = "Mahima Bhardwaj";
    }
}

虽然 MyClass 被提升,但在 class 语句执行前无法访问,这是为了防止在类定义前实例化。


复杂场景中的表现

函数内部的提升

function test() {
    console.log(x); // ReferenceError(TDZ)
    let x = 50;
}
test();

let x 被提升到函数顶部,但仍在 TDZ 中,直到赋值语句执行。


嵌套函数中的提升

function outer() {
    console.log(a); // undefined(var 提升)
    var a = 5;
    function inner() {
        console.log(b); // undefined(var 提升)
        var b = 10;
    }
    inner();
}
outer();

每个函数作用域独立进行提升。


循环中的 var 问题

经典闭包陷阱

for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 100);
}
// 输出:3, 3, 3

原因:var i 被提升到函数 / 全局作用域,所有回调共享同一个 i,循环结束后 i === 3

解决方案:使用 let(块级作用域):

for (let i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 100); // 输出:0, 1, 2
}

带参数的函数提升

test(10); // 10
function test(num) {
    console.log(num);
}

函数整体(含参数签名)被提升,参数值由调用时传入决定,与提升无关。


提升行为总结

声明类型

是否提升

初始化值

是否有 TDZ

可否在声明前访问

var

undefined

✅(值为 undefined)

let

未初始化

❌(ReferenceError)

const

未初始化

❌(ReferenceError)

函数声明

完整函数体

函数表达式(var)

✅(变量名)

undefined

❌(TypeError)

类声明

未初始化

❌(ReferenceError)


最佳实践

  1. 始终在作用域顶部声明变量,避免依赖提升逻辑。

  2. 优先使用 letconst,它们的 TDZ 机制能帮助你发现潜在错误。

  3. 避免使用 var,除非需要兼容老旧环境。

  4. 函数尽量使用声明式写法function foo() {}),便于提升和可读性。

  5. 不要在声明前调用函数表达式或类


思考题

  1. 以下代码会输出什么?为什么?

    console.log(typeof foo); // ?
    var foo = function() {};
  2. 为什么下面的代码不会报错,而使用 let 会?

    console.log(x);
    var x = 1;
  3. 如何修改以下代码,使其输出 0, 1, 2 而不是 3, 3, 3?(不使用 let

    for (var i = 0; i < 3; i++) {
        setTimeout(function() {
            console.log(i);
        }, 100);
    }