ES6 引入的 class 关键字让 JavaScript 的面向对象编程在写法上更贴近 Java、C++ 等传统语言,但它的底层并没有引入新的继承模型。Class 继承本质上仍然是原型继承,class 和 extends 只是对原有原型链操作的一套更清晰、更易用的语法糖。
理解这一本质,能帮助开发者在遇到复杂继承场景时不被语法糖迷惑,直接看清原型链上的真实关系。
语法糖转换:从 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 底层完成了两件事:
- 构造函数的原型链:
Student.proto指向Person,让Student可以继承Person的静态方法。 - 实例的原型链:
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 语法 |
|---------|-----------|-----------|
| 代码直观度 | 需要手动操作 prototype、proto、call | 写法接近传统 OOP,可读性强 |
| 继承链完整性 | 开发者容易遗忘静态继承和 constructor 修正 | 自动处理两条原型链 |
| 内置方法 | instanceof 需要原型链正确 | instanceof 基于原型链,class 语法不会破坏它 |
| 封装能力 | 靠命名约定模拟私有 | 原生支持私有成员(#) |
| 错误提示 | 错误信息可能不够友好 | 引擎能给出更明确的类相关错误 |
实际应用中的注意事项
- 不要混用类语法和直接原型操作:如果手动修改了
prototype,可能破坏class维护的原型链完整性。 - 继承层级不宜过深:原型链过长会拖慢属性查找速度,也会让代码难以理解和维护。优先考虑组合模式而非深度继承。
class不会被提升:与函数声明不同,类声明存在暂时性死区,必须先定义后使用。- 方法默认不可枚举:类中定义的方法在原型上的
enumerable属性为false,这一点与手工赋值的原型方法略有不同(手工赋值默认是可枚举的)。
本质把握:当你使用 class 和 extends 时,心里应该始终清楚,JavaScript 仅仅是在你眼前铺了一层整洁的桌布,桌布下面依然是那条永不停歇的原型链。