
JavaScript中的Class在提升(Hoisting)与暂时性死区(Temporal Dead Zone, TDZ)行为上,与传统的函数声明和var声明存在显著区别。本文将深入剖析Class内部各类声明机制的细节,揭示其在变量提升和访问时机上的独特表现。
一、Class声明本身的特性
Class声明存在不完全提升,并具有暂时性死区:
// 错误:Cannot access 'Person' before initialization
const p = new Person();
class Person {
constructor(name) {
this.name = name;
}
}Class声明会被提升到作用域顶部,但不能在声明前使用(与let/const类似)。
声明前的区域称为Class的"暂时性死区",访问会抛出ReferenceError。
二、Class内部的变量声明
Class内部(包括构造函数、方法中)使 let/const 声明的变量存在暂时性死区,但不会提升:
class Person {
constructor() {
console.log(age); // 错误:Cannot access 'age' before initialization
let age = 18;
}
}与普通函数中的 let/const 行为一致。
必须先声明再使用。
三、Class内部的函数(方法)声明
Class内部的方法声明不会提升(不能在类定义前调用类方法或创建实例)。
类方法不遵循普通函数的提升机制。但这并不影响它们在构造函数中的调用,因为类方法的初始化时机(原型方法在类定义时,实例字段在构造函数前)确保了它们在构造函数执行时已经可用。
传统原型方法:在类定义时已添加到原型上,构造函数执行时可通过原型链访问。
实例字段方法:在构造函数执行前已完成初始化。
普通函数的提升:可以在函数声明前调用函数。
类中的方法分为两种类型,它们的行为有所不同:
3.1 传统原型方法(类方法声明)
class Person {
constructor() {
this.sayHello(); // 可以正常调用
}
sayHello() { // 传统方法声明
console.log("Hello");
}
}实际行为
类方法(如 sayHello )会在类定义时被添加到类的原型(Person.prototype)上。
当使用new Person() 创建实例时:
先创建新对象并建立原型链(此时原型方法已存在)。
再执行构造函数(因此可以通过 this 调用原型方法)。
类方法的声明顺序不影响构造函数中的调用,因为所有原型方法都在构造函数执行前完成初始化。
3.2 实例字段方法(箭头函数或函数赋值)
class Person {
constructor() {
this.sayGoodbye(); // 可以正常调用
}
sayGoodbye = () => { // 箭头函数作为实例字段
console.log("Goodbye");
}
}实际行为
实例字段(包括箭头函数)会在 构造函数执行前 被初始化。
因此,无论声明顺序如何,都可以在构造函数中通过 this 调用。
四、Class的属性声明
ES6+中,Class支持类字段声明(Class Field Declarations),包括实例属性和静态属性:
class Person {
// 实例属性(ES2022+)
age = 18;
// 静态属性(ES2022+)
static species = 'Human';
constructor() {
console.log(this.age); // 18(正确:属性已初始化)
console.log(Person.species); // Human(正确:静态属性已初始化)
}
}类字段(包括实例属性和静态属性)不会提升。
它们在构造函数执行前初始化,因此可以在构造函数中直接访问。
使用等号
=赋值的类字段不存在暂时性死区。
五、与传统函数的对比总结

下面是一个传统函数提升示例:
// 普通函数声明会被提升
hello(); // 可以正常执行,输出 "Hello"
function hello() { console.log("Hello"); }
// var变量声明会被提升,但赋值不会
console.log(x); // 输出 undefined
var x = 5;六、关于提升
要理解 JavaScript 中 Class 声明的 "不完全提升",我们需要先明确:提升(Hoisting)的基本概念,再对比不同声明方式的提升行为。
6.1 提升的基本概念
提升 是 JavaScript 引擎的一种编译机制:在代码执行前,引擎会将变量和函数声明"移动"到其作用域的顶部。但不同类型的声明,提升的 程度和行为 完全不同。
6.2 三种声明的提升对比
为了理解 Class 的"不完全提升",我们将其与另外两种典型声明(var 和 let/const)进行对比:
声明类型 | 提升程度 | 初始化状态 | 声明前访问 |
|---|---|---|---|
var | 完成提升 | 初始化为 undefined | 返回 undefined (不报错) |
let/const | 不完全提升 | 未初始化(TDZ) | 抛出 ReferenceError |
class | 不完全提升 | 未初始化(TDZ) | 抛出 ReferenceError |
6.3 抛出 ReferenceError
Class 声明的"不完全提升"可以拆解为两个关键特征:
「声明被提升」但「未被初始化」
声明被提升:引擎在编译阶段会识别 Class 声明,并将其标识符(类名)添加到当前作用域中,避免了"未定义标识符"的错误。
未被初始化:提升后,Class 不会像 var 那样被初始化为 undefined,而是处于一种"等待初始化"的状态。
存在「暂时性死区(TDZ)」
• Class 声明前的区域称为 暂时性死区,在这个区域内访问 Class 会抛出 ReferenceError。
// Class 声明前的区域:暂时性死区
console.log(Person); // 错误:Cannot access 'Person' before initialization
const p = new Person(); // 错误:Cannot access 'Person' before initialization
class Person { // Class 声明语句(初始化点)
constructor() {}
}
// Class 声明后的区域:可用
const p = new Person(); // 正确6.4 为什么设计成"不完全提升"?
JavaScript 引擎将 Class 设计为"不完全提升",主要是为了:
保持代码可读性:避免开发者在声明前使用 Class,导致逻辑混乱。
确保初始化顺序:Class 可能依赖其他 Class 或变量,强制声明后使用可以避免依赖错误。
与 let / const 保持一致:Class 声明的行为与块级作用域变量(let/const)保持一致,减少语言的复杂性。
下面通过代码对比,更清晰地理解 Class的提升行为:
Class 与 var 的对比示例
// var 的完全提升
console.log(name); // undefined(不报错)
var name = "水月";
// Class 的不完全提升
console.log(Person); // ReferenceError(报错)
class Person {}Class 与 let 的相似性
// let 的不完全提升
console.log(age); // ReferenceError: age is not defined
let age = 18;
// Class 的不完全提升
console.log(Person); // ReferenceError: Person is not defined
class Person {}6.5 "不完全提升"的核心
Class 声明的"不完全提升"可以简单理解为:
声明被识别,但不能在声明语句执行前使用。
引擎知道 Class 标识符的存在(已提升)。
但在声明语句执行前,Class 无法被访问或实例化(未初始化,处于 TDZ)。
总结
Class声明:存在不完全提升和暂时性死区,声明前不可使用。
Class内部方法:无提升,但声明顺序不影响调用顺序。类方法的可用性取决于类的初始化阶段(类定义时或实例创建时),与声明顺序无关。
Class字段:无提升,但在构造函数执行前初始化,可在构造函数中直接访问。
Class内部变量:使用 let/const 声明时,遵循与普通作用域相同的暂时性死区规则。
理解:类方法的可用性取决于类的初始化阶段(类定义时或实例创建时),与声明顺序无关。
这句话需要从类方法的两种类型和JavaScript类的初始化机制两个维度来理解,先回顾上面提到的两种类型的类方法:
1. 传统原型方法(格式:method() { ... })
语法: 直接在类内部定义的方法。
初始化时机:在类定义时,(即 class Person { ... } 这段代码执行时)进行初始化。
存储位置:类的原型对象( Person.prototype )上。
2. 箭头函数作为实例字段( 格式:method = () => { ... } )
语法:使用赋值语法定义的箭头函数。
初始化时机:在实例创建时、构造函数执行前,(即 new Person() 时)。
存储位置:每个类实例对象本身(不是原型)。
“类方法的可用性取决于类的初始化阶段”的具体含义
类方法的“可用”状态,取决于它们是否已经被 初始化到正确的位置(原型或实例),而这又与类的两个关键初始化阶段相关:
阶段1:类定义时(Class Definition Phase)
当JavaScript引擎执行 class Person { ... } 这段代码时:
会创建类的构造函数。
会将所有传统原型方法(如 sayHello() )添加到原型 Person.prototype 上。
此时,类的原型已经包含了所有传统方法,这些方法对所有实例“潜在可用”。
阶段2:实例创建时(Instance Creation Phase)
当使用 new Person() 创建实例时:
先创建一个新的空对象(实例)。
建立原型链(将实例的 __proto__ 指向 Person.prototype )。
初始化所有实例字段(包括箭头函数作为实例字段)。
最后执行构造函数体(constructor() { ... })。
此时,实例对象本身包含了所有箭头函数方法,这些方法对当前实例“实际可用”。
“与声明顺序无关”的原因
类内部方法的 声明顺序不影响初始化时机,因此也不影响它们在构造函数中的可用性。
传统原型方法:声明顺序无关
所有传统原型方法都在 类定义时被同时添加到原型上,无论它们在类中声明的先后顺序如何。因此,在构造函数中可以随意调用这些方法。
class Person {
constructor() {
this.sayGoodbye(); // 后声明的方法(可用,因为已在类定义时添加到原型)
this.sayHello(); // 先声明的方法(可用,同样已添加到原型)
}
sayHello() { console.log('Hello'); }
sayGoodbye() { console.log('Goodbye'); }
}箭头函数作为实例字段:声明顺序影响初始化顺序,但不影响可用性
箭头函数作为实例字段的 初始化顺序与声明顺序一致 ,但由于它们都在 构造函数执行前 完成初始化,因此在构造函数中可以随意调用,不受声明顺序影响。
class Person {
constructor() {
this.method3(); // 最后声明的箭头函数(可用,因为已在构造函数前初始化)
this.method1(); // 先声明的箭头函数(可用,同样已初始化)
}
method1 = () => console.log('Method 1');
method2 = () => console.log('Method 2');
method3 = () => console.log('Method 3');
}关键要点
类定义时:传统原型方法被添加到类的原型上。
实例创建时、构造函数执行前:箭头函数作为实例字段被初始化到实例对象上。
构造函数执行时:所有类方法(无论是传统方法还是箭头函数)都已经可用,因此可以按任意顺序调用。
声明顺序无关:因为所有方法的初始化都在构造函数执行前完成,与它们在类中声明的先后顺序无关。
最终理解
这句话的核心是:类方法的可用性不取决于它们在类中的书写顺序,而是取决于JavaScript类的初始化机制——传统方法在类定义时就绪,箭头函数在实例创建时就绪,两者都在构造函数执行前完成初始化,因此可以在构造函数中随意调用。