JS 实现继承的几种方式
Category(分类): JavaScript Status: 已整理(2026)
原文以几何形状为引子,介绍对象冒充、原型链、混合继承、ES6
extends和对象之间的继承。本文保留这些历史方案,修复function/new粘连、构造函数指针、object(Chinese)拼写和错误的 ES5 转译代码,并补充Object.create、静态继承、组合优先和现代 class 语义。
原文:js 实现继承的几种方式
一、什么是 JavaScript 中的继承
继承通常表示:一个对象或类型可以复用另一个对象/类型的行为,并在此基础上增加或覆盖能力。几何形状可以形成直观的关系:圆是某种椭圆,矩形是多边形,正方形又可以是矩形的特化。
但 JavaScript 的继承不是“复制父类全部内容”:
- 对象通过
[[Prototype]]委托属性查找; - 构造函数负责初始化实例自有状态;
- 原型对象通常保存可共享的方法;
- class 是建立这些关系的一种语法和语义,不会消除底层原型机制。
原文中的“所有数据都是对象”“对象是通过克隆原型得到的”属于原型编程的历史观点,不是 ECMAScript 的完整规则。JavaScript 同时拥有 undefined、null、boolean、string、symbol、number、bigint 等原始值。
二、先理解构造函数和原型
function Person(name) {
this.name = name
}
Person.prototype.getName = function () {
return this.name
}
const person = new Person('Ada')
console.log(person.name) // Ada
console.log(person.getName()) // Ada
console.log(Object.getPrototypeOf(person) === Person.prototype) // true
console.log(Person.prototype.constructor === Person) // true
new Person('Ada') 的简化过程是:创建一个新对象,把它的 [[Prototype]] 设置为 Person.prototype,以新对象为 this 执行 Person,再根据构造器返回值决定最终结果。
函数能否作为构造器不是由首字母大小写决定的。普通函数通常可调用且可构造;箭头函数、对象方法、async 函数和 generator 函数通常不可构造:
function Ordinary() {}
const arrow = () => {}
const method = { run() {} }.run
console.log(typeof Ordinary) // function
console.log(typeof arrow) // function
console.log(Reflect.construct(Ordinary, []) instanceof Ordinary) // true
for (const value of [arrow, method]) {
try {
Reflect.construct(value, [])
} catch (error) {
console.log(error.name) // TypeError
}
}
三、方式 1:对象冒充(借用构造函数)
因为构造函数本质上也是函数,所以可以用 call/apply 在子类构造器中执行父类构造器:
function ClassA(color) {
this.color = color
this.sayColor = function () {
return this.color
}
}
function ClassB(color, name) {
ClassA.call(this, color)
this.name = name
this.sayName = function () {
return this.name
}
}
const objectA = new ClassA('blue')
const objectB = new ClassB('red', 'John')
console.log(objectA.sayColor()) // blue
console.log(objectB.sayColor()) // red
console.log(objectB.sayName()) // John
console.log(objectB instanceof ClassA) // false:还没有建立原型链
原文还展示了把 ClassA 临时挂到 this.newMethod 再删除的写法:
function LegacyClassB(color, name) {
this.newMethod = ClassA
this.newMethod(color)
delete this.newMethod
this.name = name
}
这种对象冒充写法只是历史代码。直接 ClassA.call(this, color) 更清晰,也避免临时属性被覆盖、构造过程中抛错后没有删除等问题。
优点:可以传参,父构造器创建的自有属性不会直接由不同实例共享。
缺点:不会自动继承 ClassA.prototype,并且如果父构造器内定义方法,每次创建实例都重复创建函数。现代写法通常把状态初始化和共享方法分开。
四、方式 2:原型链继承(prototype chaining)
function ClassA() {}
ClassA.prototype.color = 'blue'
ClassA.prototype.sayColor = function () {
return this.color
}
function ClassB() {}
ClassB.prototype = new ClassA()
ClassB.prototype.constructor = ClassB
ClassB.prototype.name = ''
ClassB.prototype.sayName = function () {
return this.name
}
const objectA = new ClassA()
const objectB = new ClassB()
objectA.color = 'blue'
objectB.color = 'red'
objectB.name = 'John'
console.log(objectA.sayColor()) // blue
console.log(objectB.sayColor()) // red
console.log(objectB.sayName()) // John
console.log(objectB instanceof ClassA) // true
console.log(objectB instanceof ClassB) // true
这里 ClassB.prototype 被设置为 ClassA 的实例,所以实例原型链大致是:
objectB → ClassB.prototype → ClassA.prototype → Object.prototype → null
缺点:
- 设置原型时调用了
new ClassA(),不能为每个子类实例传递父类参数; - 父类构造器创建的引用类型属性可能被放在
ClassB.prototype上并被所有实例共享; - 重新赋值后必须恢复
constructor; - 父类构造器可能产生不必要的初始化副作用。
现代代码通常用 Object.create(ClassA.prototype) 建立原型链,避免把父类实例作为原型。
五、方式 3:混合/组合继承
组合继承结合借用构造函数和原型链:
function ClassA(color) {
this.color = color
}
ClassA.prototype.sayColor = function () {
return this.color
}
function ClassB(color, name) {
ClassA.call(this, color) // 复制父类自有状态
this.name = name
}
ClassB.prototype = new ClassA() // 建立原型链,但会第二次调用父构造器
ClassB.prototype.constructor = ClassB
ClassB.prototype.sayName = function () {
return this.name
}
const objectB = new ClassB('red', 'John')
console.log(objectB.sayColor()) // red
console.log(objectB.sayName()) // John
console.log(objectB instanceof ClassA) // true
console.log(objectB instanceof ClassB) // true
它比单独使用前两种方式更完整,但 ClassA 会调用两次:设置 ClassB.prototype 时一次,构造实例时一次。父类自有属性可能先出现在子类原型,再被实例属性覆盖,形成冗余。
六、方式 4:寄生组合继承
寄生组合继承用 Object.create 只连接父类原型,不调用父类构造器来创建子类原型:
function inherit(Sub, Super) {
Sub.prototype = Object.create(Super.prototype)
Object.defineProperty(Sub.prototype, 'constructor', {
value: Sub,
enumerable: false,
writable: true,
configurable: true
})
// 完整模拟 class extends 时建立静态继承;不需要静态继承时可以省略
Object.setPrototypeOf(Sub, Super)
}
function Person(name) {
this.name = name
}
Person.prototype.getName = function () {
return this.name
}
function Programmer(name, language) {
Person.call(this, name)
this.language = language
}
inherit(Programmer, Person)
Programmer.prototype.code = function () {
return `${this.name} writes ${this.language}`
}
const programmer = new Programmer('Ada', 'JavaScript')
console.log(programmer.getName()) // Ada
console.log(programmer.code()) // Ada writes JavaScript
console.log(programmer instanceof Person) // true
console.log(Object.getPrototypeOf(Programmer) === Person) // true
它解决了传统组合继承重复调用父构造器的问题,是 ES5 时代常见的完整方案。缺点是需要手动处理原型、构造器属性、静态继承和父类初始化顺序,因此新代码一般使用 class extends 或组合。
七、方式 5:ES6 class extends
class Point {
constructor(x, y) {
this.x = x
this.y = y
}
getPosition() {
return [this.x, this.y]
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y)
this.color = color
}
getColor() {
return this.color
}
}
const point = new ColorPoint(1, 2, 'red')
console.log(point.getPosition()) // [1, 2]
console.log(point.getColor()) // red
console.log(point instanceof Point) // true
console.log(point instanceof ColorPoint) // true
console.log(Object.getPrototypeOf(ColorPoint.prototype) === Point.prototype) // true
console.log(Object.getPrototypeOf(ColorPoint) === Point) // true
class 的重要语义:
- class 构造器不能脱离
new直接调用; - 派生 class 构造器必须先调用
super()才能使用this; - 方法默认定义在原型上并且不可枚举;
extends同时建立实例原型链和构造器的静态原型链;- class 默认使用严格模式;
- 私有字段、静态块、
super和new.target等能力有自己的规范语义。
因此,class 不是简单的文本替换;它使用原型机制,但还提供了更完整的构造和继承语义。
八、ES6 class 的历史 ES5 转译
旧项目常使用 Babel 等工具把 class 转换为 ES5。转译器版本、目标环境和插件不同,生成代码也会不同,不能把某一份 helper 当作规范实现。下面是便于理解的简化版本:
function inherits(Sub, Super) {
Sub.prototype = Object.create(Super.prototype, {
constructor: {
value: Sub,
enumerable: false,
writable: true,
configurable: true
}
})
Object.setPrototypeOf(Sub, Super)
}
function Point(x, y) {
this.x = x
this.y = y
}
Point.prototype.getPosition = function () {
return [this.x, this.y]
}
function ColorPoint(x, y, color) {
Point.call(this, x, y)
this.color = color
}
inherits(ColorPoint, Point)
ColorPoint.prototype.getColor = function () {
return this.color
}
const point = new ColorPoint(1, 2, 'red')
console.log(point.getPosition()) // [1, 2]
console.log(point.getColor()) // red
这段代码只能模拟公开字段和原型方法;class 私有字段、派生构造器返回值、内建 subclassing、super 等语义并不能仅靠几行 call 完整复现。
九、非构造函数对象之间的继承
原文用“中国人”和“医生”说明对象继承:
const Chinese = {
nation: '中国'
}
const Doctor = Object.create(Chinese)
Doctor.career = '医生'
console.log(Doctor.nation) // 中国
console.log(Doctor.career) // 医生
console.log(Object.getPrototypeOf(Doctor) === Chinese) // true
原文的空构造函数写法中 var Doctor = object(Chinese) 是拼写错误,应该调用定义的函数:
function inheritObject(prototype) {
function Temporary() {}
Temporary.prototype = prototype
return new Temporary()
}
const Chinese = { nation: '中国' }
const Doctor = inheritObject(Chinese)
Doctor.career = '医生'
console.log(Doctor.nation) // 中国
console.log(Doctor.career) // 医生
现代代码直接使用 Object.create:
const Chinese = { nation: '中国' }
const Doctor = Object.create(Chinese, {
career: {
value: '医生',
enumerable: true,
writable: true,
configurable: true
}
})
console.log(Doctor.nation) // 中国
console.log(Doctor.career) // 医生
如果目标只是复用能力,组合通常比继承更灵活:
function createCanCode(name) {
return {
name,
code() {
return `${this.name} is coding`
}
}
}
const developer = createCanCode('Ada')
console.log(developer.code()) // Ada is coding
十、历史图示与来源
原文的几何形状继承图已下载到本地:

图片来源:原始图片。图示用于理解“is-a”关系;实际 JavaScript 代码还需要明确 [[Prototype]]、构造器和实例状态的边界。
十一、如何选择
| 需求 | 推荐方式 |
|---|---|
| 旧 ES5 代码维护 | 了解借用构造函数、组合继承和寄生组合继承 |
| 新的类型层次 | class extends,同时理解其原型语义 |
| 一个对象委托给另一个对象 | Object.create 或对象字面量原型语法 |
| 复用几个独立能力 | 组合、工厂函数或 mixin,并控制名称冲突 |
| 只想共享无状态方法 | 原型方法、class 方法或模块函数 |
| 需要跨窗口/Worker 传输 | 使用消息协议和可结构化克隆的数据,不依赖 instanceof |
继承层次过深会增加耦合;如果子类只是为了复用一两个方法,直接组合或导入函数往往更清晰。父类构造器应只负责初始化当前实例状态,原型方法应避免持有本应隔离的可变共享数据。
十二、总结
- 借用构造函数解决状态初始化,但不会自动继承原型方法;
- 原型链继承可以共享方法,但父类实例状态可能共享或产生初始化副作用;
- 组合继承兼顾两者,但会调用父构造器两次;
- 寄生组合继承通过
Object.create避免设置原型时调用父构造器; class extends建立实例和静态两条原型关系,并增加super、私有字段等语义;- 对象之间的委托优先使用
Object.create,不要把它误称为深复制; - 不要把 Babel 的 ES5 输出当作唯一的 class 实现;
- 继承不是唯一的复用手段,组合和模块函数经常更适合业务代码。