JS 总结之原型继承的几种方式
Category(分类): JavaScript Status: 已整理(2026)
本文保留原文的七种继承方式:原型链继承、借用构造函数、组合继承、原型式继承、寄生式继承、寄生组合继承和 ES6
extends。原文中的页面代码块格式标记、代码粘连、object(Chinese)拼写错误、重复调用父构造函数以及把 class 简单说成“语法糖”等内容已修正或补充现代边界。
一、先准备父类
下面的构造函数用于说明传统写法。age ?? 18 会在传入 0 时保留 0,比原文的 age || 18 更准确:
function Person(age) {
this.age = age ?? 18
}
Person.prototype.sleep = function () {
return 'sleeping'
}
JavaScript 没有强制的“类继承关键字”才能建立继承关系。常见做法是让子类实例的 [[Prototype]] 连接到父类的 prototype,并在构造阶段复用父类初始化逻辑。
需要区分三种关系:
实例 → 子类.prototype → 父类.prototype → Object.prototype → null
未显式建立静态继承时:
子类构造器 → Function.prototype → Object.prototype → null
使用 class extends 或本文 inherit() 建立静态继承时:
子类构造器 → 父类构造器 → Function.prototype → Object.prototype → null
prototype 是构造函数用于创建实例的属性;实例的真实内部原型是 [[Prototype]],读取它应使用 Object.getPrototypeOf()。
二、方式 1:原型链继承
function Programmer() {}
Programmer.prototype = new Person()
Programmer.prototype.constructor = Programmer
Programmer.prototype.code = function () {
return 'coding'
}
const jon = new Programmer()
console.log(jon.code()) // coding
console.log(jon.sleep()) // sleeping
console.log(jon instanceof Person) // true
console.log(jon instanceof Programmer) // true
console.log(Object.getPrototypeOf(jon) === Programmer.prototype) // true
优点:实现简单,实例可以访问父类原型上的方法。
缺点:
new Person()发生在设置原型时,不能为每个Programmer实例传递不同的父类构造参数;- 父类构造器创建的引用类型属性会被所有子类实例共享;
Programmer.prototype被替换后必须手动恢复constructor;- 创建一个额外的父类实例作为原型可能产生无意义的初始化副作用。
共享状态的例子:
function PersonWithTags() {
this.tags = []
}
function ProgrammerWithTags() {}
ProgrammerWithTags.prototype = new PersonWithTags()
ProgrammerWithTags.prototype.constructor = ProgrammerWithTags
const first = new ProgrammerWithTags()
const second = new ProgrammerWithTags()
first.tags.push('js')
console.log(second.tags) // ['js']:引用被共享
三、方式 2:借用构造函数(经典继承)
借用构造函数只在子类构造器中调用父类构造器:
function Programmer(age, name) {
Person.call(this, age)
this.name = name
}
const jon = new Programmer(18, 'jon')
console.log(jon.name, jon.age) // jon 18
console.log(jon.sleep) // undefined:没有继承 Person.prototype
console.log(jon instanceof Person) // false
console.log(jon instanceof Programmer) // true
优点:
- 可以向父类构造器传参;
- 父类构造器创建的自有属性属于当前实例,不会直接在实例之间共享。
缺点:
- 没有建立
Programmer.prototype到Person.prototype的关系; - 不能访问父类原型方法;
- 如果父类把方法写在构造器中,每次调用都会创建一份新函数。
原文把父构造器临时挂到 this.newMethod 再删除,这种“对象冒充”写法可以理解历史实现,但现代代码应直接使用 Person.call(this, age),避免可见的临时属性和异常时未清理的问题。
四、方式 3:组合继承
组合继承把“借用构造函数”和“原型链继承”组合起来:
function Programmer(age, name) {
Person.call(this, age)
this.name = name
}
// 这里会调用一次 Person,设置 Programmer.prototype 的初始属性
Programmer.prototype = new Person()
Programmer.prototype.constructor = Programmer
Programmer.prototype.code = function () {
return 'coding'
}
const jon = new Programmer(18, 'jon')
const flash = new Programmer(22, 'flash')
console.log(jon.age, jon.name) // 18 jon
console.log(flash.age, flash.name) // 22 flash
console.log(jon.sleep()) // sleeping
console.log(jon instanceof Person) // true
console.log(jon instanceof Programmer) // true
它解决了前两种方式的一部分问题,但父类构造器被调用两次:设置 Programmer.prototype 时调用一次,创建每个实例时又调用一次。父类自有属性可能先被写入子类原型,再被实例自有属性遮蔽,因此存在冗余。
五、方式 4:原型式继承
原型式继承直接让新对象以已有对象为原型:
const person = {
name: 'Ada',
skills: []
}
const jon = Object.create(person)
const flash = Object.create(person)
jon.name = 'Jon' // 自有属性,不影响 person.name
jon.skills.push('JavaScript') // 修改的是共享的数组
console.log(jon.name) // Jon
console.log(flash.name) // Ada
console.log(flash.skills) // ['JavaScript']
console.log(Object.getPrototypeOf(jon) === person) // true
原文的 create(o) 空构造函数实现:
function create(prototype) {
function Temporary() {}
Temporary.prototype = prototype
return new Temporary()
}
const object = { value: 1 }
const copy = create(object)
console.log(copy.value) // 1
console.log(Object.getPrototypeOf(copy) === object) // true
现代代码优先使用 Object.create(prototype),因为它直接表达意图,也可以创建 Object.create(null) 这样的无原型对象。
注意:Object.create 不是深复制,也不是把原对象的所有属性复制到新对象;新对象只是通过原型链访问原对象的属性。引用类型属性仍可能共享。
六、方式 5:寄生式继承
寄生式继承在原型式继承得到的对象上增加新能力,再返回该对象:
function createProgrammer(prototype, name) {
const programmer = Object.create(prototype)
programmer.name = name
programmer.code = function () {
return `${this.name} is coding`
}
return programmer
}
const programmer = createProgrammer(Person.prototype, 'Jon')
console.log(programmer.code()) // Jon is coding
console.log(programmer.sleep()) // sleeping
缺点是每次调用工厂函数都会创建新的 code 函数;如果方法可以共享,应放到一个明确的原型对象或 class 方法中。寄生式继承也不会自动提供构造器身份,programmer instanceof Person 是否成立取决于传入的原型链。
七、方式 6:寄生组合继承
寄生组合继承只调用一次父类构造器:
- 子类构造器通过
Person.call(this, age)复制父类自有状态; - 子类原型通过
Object.create(Person.prototype)连接到父类原型; - 不再使用
new Person()作为子类原型,因此没有多余的父类初始化。
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 Programmer(age, name) {
Person.call(this, age)
this.name = name
}
inherit(Programmer, Person)
Programmer.prototype.code = function () {
return 'coding'
}
const jon = new Programmer(18, 'jon')
console.log(jon.name, jon.age) // jon 18
console.log(jon.sleep()) // sleeping
console.log(jon.code()) // coding
console.log(jon instanceof Person) // true
console.log(jon instanceof Programmer) // true
console.log(Object.getPrototypeOf(Programmer) === Person) // true
这是传统构造函数中较完整的实现,但仍需要手动维护 super 调用、静态继承、属性描述符和异常边界。原文引用《JavaScript 高级程序设计》称它是引用类型的理想继承范式,这个评价属于 ES5 时代的历史背景;现代项目通常优先使用 class 或组合。
八、方式 7:ES6 class extends
class PersonClass {
constructor(age = 18) {
this.age = age
}
sleep() {
return 'sleeping'
}
}
class ProgrammerClass extends PersonClass {
constructor(age, name) {
super(age)
this.name = name
}
code() {
return 'coding'
}
}
const jon = new ProgrammerClass(18, 'jon')
console.log(jon.name, jon.age) // jon 18
console.log(jon.sleep()) // sleeping
console.log(jon.code()) // coding
console.log(jon instanceof PersonClass) // true
console.log(jon instanceof ProgrammerClass) // true
console.log(Object.getPrototypeOf(ProgrammerClass.prototype) === PersonClass.prototype) // true
console.log(Object.getPrototypeOf(ProgrammerClass) === PersonClass) // true
class 使用原型方法和构造器,但不是把 JavaScript 变成 Java/C++ 那种基于类的运行时。class 具有自己的语义:
- class 声明默认处于严格模式;
- class 构造器不能被普通调用,必须使用
new; - 派生构造器必须先调用
super()才能访问this; - 方法默认不可枚举;
extends同时建立实例原型链和构造器静态原型链;- 私有字段
#field、静态初始化块等能力不能简单等同于 ES5 的函数拷贝。
原文说“只要部分浏览器支持,需要转为 ES5”是历史兼容性说明。现代目标浏览器和 Node.js 普遍支持 class;是否转译应由项目的目标环境、构建配置和兼容性需求决定,而不是因为 class 本身不可靠。
九、非构造函数对象的继承
如果只是想让一个对象继承另一个对象,不需要人为创建空构造函数:
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
原文的空构造函数版本可以改成:
function inheritObject(prototype) {
function Temporary() {}
Temporary.prototype = prototype
return new Temporary()
}
const Chinese = { nation: '中国' }
const Doctor = inheritObject(Chinese)
Doctor.career = '医生'
console.log(Doctor.nation) // 中国
现代代码推荐 Object.create。如果对象需要封装状态、校验输入、共享方法或定义明确的 API,工厂函数、class 或组合通常比直接构造长原型链更清晰。
十、继承不等于必须使用继承
继承适合稳定的“is-a”关系,但业务代码经常更适合组合(has-a):
function canCode(target) {
return {
code() {
return `${target.name} is coding`
}
}
}
function createDeveloper(name) {
const developer = { name }
Object.assign(developer, canCode(developer))
return developer
}
console.log(createDeveloper('Ada').code()) // Ada is coding
组合可以减少脆弱的基类依赖和深层原型链。选择继承方式时,应考虑:共享行为的位置、状态是否隔离、是否需要 instanceof、是否需要静态继承,以及跨 Realm/序列化边界。
十一、总结
| 方式 | 主要优点 | 主要问题 | 现代建议 |
|---|---|---|---|
| 原型链继承 | 简单,可继承原型方法 | 引用属性共享,不能灵活传参 | 仅用于理解历史代码 |
| 借用构造函数 | 可传参,实例状态隔离 | 没有原型方法继承 | 作为底层步骤理解 |
| 组合继承 | 兼顾状态和方法 | 父构造器调用两次 | 了解 ES5 代码即可 |
| 原型式继承 | 直接基于对象创建对象 | 引用属性可能共享 | 使用 Object.create |
| 寄生式继承 | 可对原型对象进行增强 | 方法可能重复创建 | 谨慎使用 |
| 寄生组合继承 | ES5 中效率和语义较完整 | 手写细节多 | 维护旧代码时使用 |
class extends | 语义清晰,支持 super/私有字段 | 仍需理解原型和构造语义 | 新代码的常用选择 |
| 组合 | 低耦合,能力可复用 | 需要设计对象协作 | 业务代码优先考虑 |