JavaScript 实现继承的方式
Category(分类): JavaScript Status: 已整理(2026)
本文保留原文介绍“类式继承、构造函数继承、组合继承、寄生组合式继承和
extends继承”的结构,同时把原文中被复制按钮挤在同一行的代码重新排版。JavaScript 本质上是原型对象之间的委托;class是建立在原型机制之上的语法和语义抽象,不是另一种完全不同的对象模型。
一、原型链是什么
当读取对象自身没有的属性时,JavaScript 会继续沿着该对象的内部 [[Prototype]] 链查找,直到找到属性或到达 null。方法通常放在原型上,让同一类实例共享方法函数:
const animal = {
greet() {
return '汪汪或喵喵'
}
}
const dog = Object.create(animal)
dog.name = '旺财'
console.log(dog.name) // 旺财:对象自身属性
console.log(dog.greet()) // 汪汪或喵喵:从原型链找到
console.log(Object.getPrototypeOf(dog) === animal) // true
Object.getPrototypeOf() 是读取原型的标准 API。__proto__ 是历史访问器,不建议作为新代码的主要 API;设置原型应优先使用对象字面量的 __proto__ 初始化语法、Object.create() 或 Object.setPrototypeOf(),并注意运行时改变原型可能影响性能。
二、类式继承:Child.prototype = new Parent()
早期 JavaScript 教材常把构造函数称为“类”,并直接把父类实例赋给子类原型:
function Animal(color) {
this.name = 'animal'
this.type = ['pig', 'cat']
this.color = color
}
Animal.prototype.greet = function (sound) {
return sound
}
function Dog() {
this.name = 'dog'
}
// 旧式类式继承:这里会立即调用一次 Animal
Dog.prototype = new Animal('白色')
Dog.prototype.constructor = Dog
const dog = new Dog()
console.log(dog.greet('汪汪')) // 汪汪
console.log(dog.color) // 白色
这种写法有两个典型问题:
Dog.prototype本身是一个Animal实例,Animal构造函数在定义继承关系时就被调用了一次;- 如果父构造函数把数组或对象放在实例上,这些值会成为所有没有自身同名属性的子类实例共享的原型属性。
dog.type.push('dog')
const dog2 = new Dog()
console.log(dog2.type) // ['pig', 'cat', 'dog']:共享了原型上的数组
此外,父构造函数中的 color 只能在设置原型时确定,不能根据每个 Dog 实例分别传入。把 constructor 修复为 Dog 只修复了元信息,不会消除这些共享和初始化问题。因此这种写法主要用于理解历史代码,不建议作为新代码的继承方案。
三、构造函数继承:调用父构造函数
构造函数继承在子构造函数中调用父构造函数,并把 this 指向新建的子实例。这样父构造函数创建的数组和对象会在每个实例上单独创建:
function Animal(color) {
this.name = 'animal'
this.type = ['pig', 'cat']
this.color = color
}
Animal.prototype.greet = function (sound) {
return sound
}
function Dog(color) {
Animal.call(this, color)
this.name = 'dog'
}
const dog = new Dog('白色')
const dog2 = new Dog('黑色')
dog.type.push('dog')
console.log(dog.color) // 白色
console.log(dog.type) // ['pig', 'cat', 'dog']
console.log(dog2.color) // 黑色
console.log(dog2.type) // ['pig', 'cat']
console.log(typeof dog.greet) // undefined:没有继承 Animal.prototype
call/apply 只是调用函数时指定 this 的机制,并不是“复制父类的方法”。因此该方案解决了实例属性共享,却没有建立原型链,也无法直接使用父原型上的方法。
四、组合继承:实例属性 + 原型方法
组合继承把上面两种方式结合起来:在子构造函数中调用父构造函数负责实例属性,再把父实例放进子原型以获得方法:
function Animal(color) {
this.name = 'animal'
this.type = ['pig', 'cat']
this.color = color
}
Animal.prototype.greet = function (sound) {
return sound
}
function Dog(color) {
// 第一次:每个 Dog 实例各自初始化实例属性
Animal.call(this, color)
this.name = 'dog'
}
// 第二次:设置原型时又调用一次 Animal
Dog.prototype = new Animal()
Dog.prototype.constructor = Dog
const dog = new Dog('白色')
const dog2 = new Dog('黑色')
dog.type.push('dog')
console.log(dog.greet('汪汪')) // 汪汪
console.log(dog.color, dog2.color) // 白色 黑色
console.log(dog.type, dog2.type) // ['pig', 'cat', 'dog'] ['pig', 'cat']
由于实例属性会遮蔽原型上的同名属性,所以数组不再被不同实例共享。但父构造函数被调用两次,设置原型时那次调用创建的 type 和 color 通常不会被直接使用,属于多余开销。
五、寄生组合式继承:使用 Object.create
寄生组合式继承保留“调用父构造函数初始化实例属性”,但使用 Object.create(Animal.prototype) 建立原型链,而不是创建一个父实例:
function Animal(color) {
this.name = 'animal'
this.type = ['pig', 'cat']
this.color = color
}
Animal.prototype.greet = function (sound) {
return sound
}
function Dog(color) {
Animal.call(this, color)
this.name = 'dog'
}
Dog.prototype = Object.create(Animal.prototype, {
constructor: {
value: Dog,
writable: true,
configurable: true,
enumerable: false
}
})
Dog.prototype.getName = function () {
return this.name
}
const dog = new Dog('白色')
const dog2 = new Dog('黑色')
dog.type.push('dog')
console.log(dog.greet('汪汪')) // 汪汪
console.log(dog.getName()) // dog
console.log(dog.type, dog2.type) // ['pig', 'cat', 'dog'] ['pig', 'cat']
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype) // true
Object.create(proto) 创建一个新对象,并把它的原型设置为 proto;它不是把原型对象“浅拷贝”成一个普通对象。constructor 属性需要显式恢复,因为新建的子原型默认没有一个指向 Dog 的自有 constructor 属性。
等价的教学实现如下:
function inheritPrototype(child, parent) {
child.prototype = Object.create(parent.prototype)
child.prototype.constructor = child
}
这种写法在 class 出现之前很常见。现在维护旧构造函数代码时仍可能遇到它,但新代码通常优先使用 class 或组合而不是手工拼接继承链。
六、ES2015 class 与 extends
class 提供了更清晰的构造函数和方法语法。类方法仍然位于原型上,实例字段则通常在每个实例上创建:
class Animal {
constructor(color) {
this.color = color
this.type = ['pig', 'cat']
}
greet(sound) {
return sound
}
}
class Dog extends Animal {
constructor(color, name) {
super(color)
this.name = name
}
bark() {
return this.greet('汪汪')
}
}
const dog = new Dog('黑色', '旺财')
console.log(dog.bark()) // 汪汪
console.log(dog.color, dog.name) // 黑色 旺财
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype) // true
在派生类构造函数中,通常必须先调用 super(),才能访问 this。super(color) 会调用父类构造函数;super.greet() 则会从父类原型上查找方法。若派生类没有写构造函数,JavaScript 会提供一个转发参数的默认构造函数。
extends 不只可以继承普通类,也可以继承构造函数或具有构造能力的内置类。class 声明不会被提升到可直接使用的状态,类方法默认不可枚举,类体默认处于严格模式。
私有字段与实例字段
现代类可以使用私有字段。私有字段不是普通字符串属性,不能通过 obj['#value'] 访问:
class Counter {
#value = 0
increment() {
this.#value += 1
return this.#value
}
get value() {
return this.#value
}
}
const counter = new Counter()
console.log(counter.increment()) // 1
console.log(counter.value) // 1
如果目标环境不支持私有字段,需要通过构建工具或闭包实现等价封装;不要把 #field 当作可以被任意代码读取的普通属性。
七、继承关系与 instanceof
instanceof 检查右侧构造函数的 prototype 是否出现在左侧对象的原型链中:
class Parent {}
class Child extends Parent {}
const child = new Child()
console.log(child instanceof Child) // true
console.log(child instanceof Parent) // true
console.log(child instanceof Object) // true
它不适合判断跨 Realm 的数组或对象,也可能被自定义的 Symbol.hasInstance 改变。数组应使用 Array.isArray(value);跨窗口对象的类型判断要结合具体 API 和协议。
八、现代项目是否应该优先继承
继承适合表达稳定的“是一个”关系,例如 Dog 是一种 Animal,并且子类确实需要替换或扩展父类行为。以下情况更适合组合:
- 组件需要复用能力,但不需要共享同一条原型链;
- 功能可以独立开关,例如日志、缓存、重试;
- 业务模型变化频繁,继承层级会持续增长;
- 需要把依赖注入到对象,而不是让子类隐式依赖父类。
function withLogger(service, logger = console) {
return {
...service,
request(...args) {
logger.log('request', args)
return service.request(...args)
}
}
}
const service = {
request(url) {
return `GET ${url}`
}
}
const loggedService = withLogger(service)
console.log(loggedService.request('/users'))
组合不是“永远优于继承”,但它可以减少脆弱的基类问题。选择时应以职责、变化方向、测试成本和运行时约束为依据。
九、常见错误总结
- 把
Dog.prototype = new Animal()当成现代 JavaScript 的唯一继承方式;它会共享原型上的引用属性并多调用一次父构造函数。 - 以为
Animal.call(this)会继承Animal.prototype的方法;它只是在当前实例上执行父构造函数。 - 使用
Object.create()后忘记修复constructor;这会让调试和基于构造函数的元信息产生误导。 - 派生类构造函数在
super()之前访问this;这会抛出ReferenceError。 - 用
__proto__作为通用类型判断或频繁修改原型;优先使用标准原型 API。 - 认为
instanceof在所有窗口、Worker 和 iframe 中都可靠;跨 Realm 时构造函数身份可能不同。