人人都会AI编程

ES6 Class 继承的底层实现与本质

更新时间:2026-07-11

ES6 引入的 class 关键字让 JavaScript 的面向对象编程在写法上更贴近 Java、C++ 等传统语言,但它的底层并没有引入新的继承模型。Class 继承本质上仍然是原型继承,classextends 只是对原有原型链操作的一套更清晰、更易用的语法糖。

理解这一本质,能帮助开发者在遇到复杂继承场景时不被语法糖迷惑,直接看清原型链上的真实关系。

语法糖转换:从 class 到构造函数

下面是一个简单的类定义:

class Person {
  constructor(name) {
    this.name = name;
  }
  
  sayHi() {
    console.log(`Hi, I'm ${this.name}`);
  }
  
  static create(name) {
    return new Person(name);
  }
}

这段代码在底层等价于以下 ES5 写法:

function Person(name) {
  this.name = name;
}

Person.prototype.sayHi = function() {
  console.log(`Hi, I'm ${this.name}`);
};

Person.create = function(name) {
  return new Person(name);
};

要点:

  • constructor 就是构造函数本身。
  • 类中定义的方法会被添加到 Person.prototype 上,因此所有实例共享同一个方法,而不是每个实例都拷贝一份。
  • static 静态方法直接添加到构造函数对象上,实例无法访问。

extends 做了什么:连接两条原型链

使用继承时:

class Student extends Person {
  constructor(name, grade) {
    super(name);
    this.grade = grade;
  }
  
  study() {
    console.log(`${this.name} is studying`);
  }
}

extends 底层完成了两件事:

  1. 构造函数的原型链Student.proto 指向 Person,让 Student 可以继承 Person 的静态方法。
  2. 实例的原型链Student.prototype.proto 指向 Person.prototype,让 Student 的实例可以访问 Person 原型上的方法。

手写等效代码大约是这样:

function Student(name, grade) {
  // 调用父类构造函数
  Person.call(this, name);
  this.grade = grade;
}

// 实例原型继承
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;

// 构造函数原型继承(静态方法)
Object.setPrototypeOf(Student, Person);
// 或 Student.__proto__ = Person;

Student.prototype.study = function() {
  console.log(`${this.name} is studying`);
};

因此 extends 本质上是一种寄生组合式继承的标准化封装,比手工实现更简洁,且自动处理了 constructor 指向等细节。

super 关键字:定位父类原型并绑定 this

super 在两个场景下行为不同:

1. 构造函数中调用 super(...)
这相当于 Person.call(this, name),但必须在访问 this 之前调用。底层原因是子类实例的创建过程与父类绑定:子类的 constructor 实际上会调用父类的 constructor 来初始化 this,因此必须先执行 super() 才能拿到 this 引用。

2. 方法中调用 super.methodName()
这相当于在原型链上查找方法并执行,同时保持 this 为当前实例:

class Child extends Parent {
  foo() {
    super.foo(); // 等价于 Parent.prototype.foo.call(this)
  }
}

super 通过内部的 [[HomeObject]] 属性记住了方法定义时所在的对象(即当前类的原型),从而能准确定位父类原型,即使方法被单独取出调用也不会丢失父类上下文。

私有属性与私有方法

ES6 之后的提案(现已标准化)引入了私有属性和方法,用 # 前缀:

class MyClass {
  #privateField = 42;
  
  #privateMethod() {
    return this.#privateField;
  }
  
  publicMethod() {
    return this.#privateMethod();
  }
}

私有成员完全无法从外部访问,哪怕是子类也不行。这一特性在引擎层面强制执行,不像之前的 _ 下划线约定只靠“信任”。它补充了 JavaScript 封装机制的一块重要短板。

与原型继承相比,Class 继承带来了什么

| 对比维度 | 纯原型操作 | Class 语法 |
|---------|-----------|-----------|
| 代码直观度 | 需要手动操作 prototypeprotocall | 写法接近传统 OOP,可读性强 |
| 继承链完整性 | 开发者容易遗忘静态继承和 constructor 修正 | 自动处理两条原型链 |
| 内置方法 | instanceof 需要原型链正确 | instanceof 基于原型链,class 语法不会破坏它 |
| 封装能力 | 靠命名约定模拟私有 | 原生支持私有成员(#) |
| 错误提示 | 错误信息可能不够友好 | 引擎能给出更明确的类相关错误 |

实际应用中的注意事项

  • 不要混用类语法和直接原型操作:如果手动修改了 prototype,可能破坏 class 维护的原型链完整性。
  • 继承层级不宜过深:原型链过长会拖慢属性查找速度,也会让代码难以理解和维护。优先考虑组合模式而非深度继承。
  • class 不会被提升:与函数声明不同,类声明存在暂时性死区,必须先定义后使用。
  • 方法默认不可枚举:类中定义的方法在原型上的 enumerable 属性为 false,这一点与手工赋值的原型方法略有不同(手工赋值默认是可枚举的)。

本质把握:当你使用 classextends 时,心里应该始终清楚,JavaScript 仅仅是在你眼前铺了一层整洁的桌布,桌布下面依然是那条永不停歇的原型链。