JavaScript class 与面向对象

深入解析 ES6 class 语法糖背后的真相:prototype 原型链继承机制、constructor、静态属性、私有字段,以及 super 的调用规则。

为什么理解 class 的本质很重要

ES6 引入的 class 关键字让 JavaScript 看起来像一门传统的面向对象语言。但 class 实际上是语法糖——它的底层仍然是 prototype 原型链机制。理解这一点,才能真正掌握 JavaScript 的对象模型。

很多面试者能写出 class,却说不清 classprototype 的映射关系、分不清 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

七、延展阅读