一、什么是提升?

提升(Hoisting)是 JavaScript 引擎的一种特性:在代码执行前,JavaScript 引擎会将变量和函数的声明提升到其作用域的顶部。这种机制允许在声明前使用某些变量或函数,但具体行为因语法结构而异。

二、提升的类型:完全提升 vs 不完全提升

2.1 完全提升

完全提升特指 普通函数声明 的提升行为,其核心特征包括:

  • 声明部分:(函数名)被提升。

  • 定义部分:(函数体)也被提升。

  • 可以在声明前直接调用函数并执行其功能。

简言之:函数的 声明、定义和初始化 会被同时提升到作用域顶部,因此可以在声明前直接调用并执行。

代码示例

// 完全提升:普通函数声明
hello(); // 可以直接调用,输出 "Hello from normal function"

function hello() {
    console.log('Hello from normal function');
}

执行顺序(引擎实际处理过程):

  1. 提升 function hello() { console.log('Hello from normal function'); } 到作用域顶部。

  2. 执行 hello(); → 正常调用函数。

  3. 执行函数声明(已提升,实际被忽略)

2.2 不完全提升

不完全提升 指的是:只有 声明部分 被提升,定义/赋值/初始化 部分留在原地,因此在声明前无法正常使用。

不完全提升又可细分为两种情况:

  • 无暂时死区:如 var 变量。

  • 有暂时死区:如 let 、const 和 类声明。

三、各类语法结构的提升行为

3.1 var 变量声明(不完全提升,无暂时死区)

// var 提升但未赋值
console.log(x); // 输出 undefined(声明被提升,赋值留在原地)
var x = 5;

执行顺序

  1. 提升 var x; 到作用域顶部(默认值 undefined )。

  2. 执行 console.log(x); → 输出 undefined。

  3. 执行 x = 5; → 完成赋值。

3.2 let 变量声明(不完全提升,有暂时死区)

// let 提升但处于暂时死区
// console.log(x); // 抛出 ReferenceError
// typeof x; // 同样抛出 ReferenceError
let x = 5;

执行顺序

  1. 提升 let x; 到作用域顶部(进入暂时死区,未初始化)。

  2. 执行 console.log(x);  → 抛出 ReferenceError。

  3. 执行 let x = 5; 完成初始化和赋值。

3.3 const 常量声明(不完全提升,有暂时死区)

// const 提升但处于暂时死区
// console.log(y); // 抛出 ReferenceError

const y = 10;

执行顺序

  1. 提升 const y; 到作用域顶部(进入暂时死区,未初始化).

  2. 执行 console.log(y); → 抛出 ReferenceError。

  3. 执行 const y = 10; → 完成初始化和赋值。

3.4. 类声明(不完全提升,有暂时死区)

// 类提升但处于暂时死区
// const person = new Person(); // 抛出 ReferenceError

class Person {
    constructor() {
        this.name = "Alice";
    }
}

执行顺序

  1. 提升 class Person; 到作用域顶部(进入暂时死区,未初始化)。

  2. 执行 const person = new Person(); → 抛出 ReferenceError。

  3. 执行 class Person { ... } → 完成类定义。

3.5 类内部的方法(无提升)

类内部的方法 完全不会被单独提升,它们的可用性依赖于类的初始化顺序:

3.5.1 原型方法(普通方法声明)

class Person {
    constructor() {
        this.sayHello(); // 可以正常调用(原型方法已添加到原型)
    }
    
    sayHello() { // 原型方法
        console.log("Hello from prototype method");
    }
}

关键机制

  • 类定义时,所有原型方法(如 sayHello)被添加到类的原型上。

  • 实例化时,先创建对象并建立原型链,再执行构造函数。

  • 因此,构造函数中可以通过 this 访问所有原型方法。

3.5.2 实例字段方法(箭头函数)

class Person {
    constructor() {
        this.sayGoodbye(); // 可以正常调用(实例字段已初始化)
    }
    
    sayGoodbye = () => { // 实例字段方法
        console.log("Goodbye from arrow function");
    }
}

关键机制

  • 实例化时,先初始化所有实例字段(包括箭头函数)。

  • 再执行构造函数。

  • 因此,构造函数中可以通过 this 访问所有实例字段方法。

四、完全提升 vs 不完全提升对比表

根据 JavaScript 规范,所有声明(包括 var、let、const、function、class)都会在代码执行前被提升到作用域顶部。但不同声明方式的提升行为有明显区别:

五、暂时死区(Temporal Dead Zone, TDZ)

暂时死区是 ES6 引入的概念:变量从声明开始到初始化完成前的区域,在此区域内访问变量会抛出 ReferenceError。

let/const 的暂时死区示例

function testTDZ() {
    console.log("Start of function");
    
    // TDZ 开始:let x 声明被提升
    // console.log(x); // 抛出 ReferenceError
    
    let x = 5; // TDZ 结束:初始化完成
    
    console.log(x); // 输出 5
}

六、类内部方法的特殊行为

类内部的方法(无论是原型方法还是箭头函数)没有提升行为,但:

  1. 原型方法:在类定义时添加到原型,构造函数执行时可通过原型链访问。

  2. 箭头函数:在实例化时初始化,构造函数执行前已可用。


关键结论

类内部方法的 声明顺序不影响构造函数中的调用 ,这不是因为提升,而是因为类的初始化机制。

七、最佳实践

  1. 总是在作用域顶部声明变量和函数,避免依赖提升机制。

  2. 优先使用 let / const, 而非 var,利用暂时死区避免意外错误。

  3. 先定义类,再使用类,不要在类定义前创建实例。

总结

JavaScript 的提升机制是其核心特性之一,但不同语法结构的提升行为差异较大:

  • 只有普通函数声明是 完全提升

  • var 、let、const和类声明都是 不完全提升

  • 类内部方法 没有提升,但其可用性依赖于类的初始化顺序。

理解这些差异有助于编写更安全、更可预测的 JavaScript 代码。