为什么理解 class 的本质很重要
ES6 引入的 class 关键字让 JavaScript 看起来像一门传统的面向对象语言。但 class 实际上是语法糖——它的底层仍然是 prototype 原型链机制。理解这一点,才能真正掌握 JavaScript 的对象模型。
很多面试者能写出 class,却说不清 class 到 prototype 的映射关系、分不清 super() 和 Parent.prototype.method() 的区别。这篇文章让你彻底理解这些问题。
一、class 是语法糖
1.1 class 到 prototype 的映射
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound`;
}
static create(name) {
return new Animal(name);
}
}
// 等价于:
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
return `${this.name} makes a sound`;
};
Animal.create = function(name) {
return new Animal(name);
};
1.2 class 与构造函数的区别
| 特性 | class | 构造函数 |
|---|---|---|
| 必须用 new 调用 | Yes | No(可以直接调用) |
| 方法不可枚举 | Yes | No |
| 默认启用严格模式 | Yes | No |
| 存在 TDZ | Yes | No |
| prototype 不可写 | Yes | No |
class A {
method() {}
}
// prototype 不可写
A.prototype = {}; // 在非严格模式下静默失败,严格模式报错
二、constructor 与实例属性
2.1 constructor 的本质
constructor 是 class 内部的方法,创建实例时自动调用:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
const alice = new Person('Alice', 30);
// new Person() 自动调用 constructor
2.2 实例属性的定义方式
class Counter {
// 实例属性
count = 0;
constructor() {
// 也可以在 constructor 中定义
this.id = Counter.nextId++;
}
static nextId = 1;
}
const c = new Counter();
console.log(c.count); // 0
console.log(c.id); // 1
三、extends 与原型链继承
3.1 extends 的机制
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound`;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // 调用父类 constructor
this.breed = breed;
}
speak() {
return `${this.name} barks`;
}
}
const dog = new Dog('Rex', 'German Shepherd');
console.log(dog.speak()); // "Rex barks"
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
3.2 super 的两种用法
class Dog extends Animal {
constructor(name, breed) {
// super() 作为函数调用 — 必须在 constructor 中先调用
super(name);
this.breed = breed;
}
speak() {
// super.method() — 调用父类原型上的方法
const parentVoice = super.speak();
return `${parentVoice} and barks`;
}
}
3.3 super 的 TDZ 约束
class Child extends Parent {
constructor() {
// 在 super() 调用之前,this 处于 TDZ 状态
// this.x = 1; // ReferenceError
super();
this.x = 1; // 现在可以了
}
}
四、静态属性与静态方法
4.1 static 关键字
class MathHelper {
static PI = 3.14159;
static add(a, b) {
return a + b;
}
// 静态块(ES2022)
static {
console.log('MathHelper initialized');
}
}
console.log(MathHelper.PI); // 3.14159
console.log(MathHelper.add(1, 2)); // 3
// 静态属性可以通过类本身访问,不能通过实例访问
const m = new MathHelper();
console.log(m.PI); // undefined
4.2 静态属性继承
class Parent {
static className = 'Parent';
}
class Child extends Parent {}
console.log(Child.className); // 'Parent'
// 子类的静态属性指向父类
console.log(Child.className === Parent.className); // true
五、私有字段
5.1 #privateField 语法
class BankAccount {
// 私有字段以 # 开头
#balance = 0;
#password;
constructor(password) {
this.#password = password;
}
#validatePassword(pwd) {
return pwd === this.#password;
}
deposit(amount, password) {
if (!this.#validatePassword(password)) {
throw new Error('Invalid password');
}
this.#balance += amount;
}
getBalance(password) {
if (!this.#validatePassword(password)) {
throw new Error('Invalid password');
}
return this.#balance;
}
}
const account = new BankAccount('secret');
// account.#balance // SyntaxError — 无法从外部访问
account.deposit(100, 'secret');
5.2 私有字段 vs Symbol
// Symbol 作为私有属性的替代方案(ES2022 之前)
const _private = Symbol('private');
class OldSchool {
constructor() {
this[_private] = 'secret';
}
getPrivate() {
return this[_private];
}
}
// 私有字段的优势
// 1. 语法层面强制私有,无法通过 Symbol 意外访问
// 2. 子类无法继承私有字段
// 3. 字段存在性可以在类外部检测(但仍无法访问值)
六、new.target 元属性
6.1 new.target 的作用
class Animal {
constructor() {
console.log(new.target); // 当前正在被构造的类
console.log(new.target === Animal); // 判断是否通过 new 调用
}
}
class Dog extends Animal {
constructor() {
super();
}
}
new Animal(); // [class Animal]
new Dog(); // [class Dog extends Animal]
6.2 抽象类模式
class Shape {
constructor() {
if (new.target === Shape) {
throw new Error('Shape is abstract and cannot be instantiated directly');
}
}
area() {
throw new Error('Subclass must implement area()');
}
}
class Circle extends Shape {
constructor(radius) {
super();
this.radius = radius;
}
area() {
return Math.PI * this.radius ** 2;
}
}
const s = new Shape(); // Error
const c = new Circle(5); // OK