1 什么是继承
JavaScript 中的继承是一种对象间共享属性和方法的机制。与基于类的语言(如 Java、C++)不同,JavaScript 采用基于原型(Prototype) 的继承模型,这是其最核心的特征之一。
1.1 原型继承的本质
在 JavaScript 中,每个对象都有一个内部属性 [[Prototype]](可通过 __proto__ 或 Object.getPrototypeOf() 访问),它指向另一个对象(即其原型)。当访问对象的属性时,如果对象自身没有该属性,JavaScript 引擎会沿着原型链向上查找,直到找到该属性或到达链的末端(null)。
1.2 发展历程
想要详细了解的宝子可以去看这篇文章:从 Object.create 到 class:JS 原型的进化论原型 JS 是一门基于原型的语言,其他语言通过类描述和 - 掘金
JavaScript 继承机制经历了从简单到复杂、从功能实现到语法糖封装的发展过程:
- 早期阶段:仅通过构造函数和原型实现简单的继承
- 模式探索期:开发者创造了多种继承模式(原型链、构造函数、组合继承等)
- ES5 规范化:
Object.create()方法提供了标准化的原型继承 - ES6 现代化:引入
class和extends关键字,提供更直观的类式继承语法
2 实现方式详解
2.1 原型链继承
原理:通过将子类的原型对象指向父类的实例,实现原型链的连接。
function Parent() {
this.name = 'parent'
this.like = [12, 13, 14]
}
function Child() {
this.age = 19
}
// 核心:建立原型链
Child.prototype = new Parent()
let c = new Child()
let d = new Child()
c.like.push(199)
console.log(d.like) // [12, 13, 14, 199] - 共享引用属性
特点:
- 实例共享原型上的引用属性(如数组、对象)
- 无法向父类构造函数传参
- 简单易实现,但存在共享问题
2.2 构造函数继承
原理:在子类构造函数中通过 call() 或 apply() 调用父类构造函数。
function Parent(name) {
this.name = name || "parent"
this.like = [12, 13, 14]
}
function Child() {
// 核心:借用父类构造函数
Parent.call(this, "child") // 可传参
this.age = 19
}
let c = new Child()
let d = new Child()
c.like.push(199)
console.log(d.like) // [12, 13, 14] - 不共享
特点:
- 解决了引用属性共享问题
- 可以向父类传参
- 无法继承父类原型上的属性和方法
2.3 组合式继承
原理:结合原型链继承和构造函数继承的优点。
function Parent(name) {
this.name = name || "parent"
this.like = [12, 13, 14]
}
Parent.prototype.getName = function() {
console.log(this.name)
}
function Child(name) {
// 第二次调用 Parent
Parent.call(this, name) // 继承实例属性
this.age = 19
}
// 第一次调用 Parent
Child.prototype = new Parent() // 继承原型方法
Child.prototype.constructor = Child // 修复构造函数指向
let c = new Child("child1")
let d = new Child("child2")
c.like.push(199)
console.log(d.like) // [12, 13, 14] - 不共享
c.getName() // child1 - 可访问原型方法
特点:
- 结合了两种继承方式的优点
- 实例属性不共享,原型方法可复用
- 父类构造函数被调用两次,效率较低
2.4 原型式继承
原理:基于现有对象创建新对象,使用 Object.create() 方法。
const parent = {
name: 'parent',
like: ['one', 'two']
}
// 核心:以 parent 为原型创建新对象
const child = Object.create(parent)
console.log(child.__proto__ === parent) // true
child.like.push('three')
console.log(parent.like) // ['one', 'two', 'three'] - 共享引用属性
特点:
- 无需构造函数即可实现对象继承
- 与原型链继承有相同的共享问题
- ES5 标准化了这种模式
2.5 寄生式继承
原理:在原型式继承基础上增强对象,添加额外方法。
const parent = {
name: "parent",
like: ["one", "two"],
}
function createChild(obj) {
// 原型式继承
let clone = Object.create(obj)
// 增强对象
clone.getLike = function() {
return this.like
}
return clone
}
const child1 = createChild(parent)
console.log(child1.getLike()) // ["one", "two"]
child1.like.push('three')
const child2 = createChild(parent)
console.log(child2.getLike()) // ["one", "two", "three"] - 共享
特点:
- 为对象添加自定义方法
- 方法不能复用,每个实例都有单独的方法副本
- 仍然存在引用属性共享问题
2.6 寄生组合式继承
原理:通过寄生方式修复组合继承中父类构造函数被调用两次的问题。
function Parent(name) {
this.name = name || "parent"
this.like = ["one", "two"]
}
Parent.prototype.getName = function() {
console.log(this.name)
}
function Child(name) {
// 只调用一次 Parent 构造函数
Parent.call(this, name)
this.age = '18'
}
// 核心:不调用 Parent 构造函数,直接继承原型
Child.prototype = Object.create(Parent.prototype)
// 修复构造函数指向
Child.prototype.constructor = Child
let child1 = new Child("child1")
child1.getName() // child1
console.log(Child.prototype.constructor === Child) // true
console.log(child1 instanceof Child) // true
console.log(child1 instanceof Parent) // true
特点:
- 只调用一次父类构造函数
- 原型链保持正确
- 是 ES6 之前最理想的继承方式
2.7 ES6 类继承
原理:使用 class 和 extends 关键字实现更直观的继承。
class Parent {
constructor(name) {
this.name = name || "parent"
this.like = ["one", "two"]
}
// 实例方法
getName() {
console.log(this.name)
}
// 静态方法
static getHello() {
console.log("hello world!")
}
}
class Child extends Parent {
constructor(name) {
super(name) // 必须先调用 super()
this.age = "18"
}
}
// 调用静态方法
Parent.getHello() // "hello world!"
let child1 = new Child("child1")
child1.getName() // "child1"
let child2 = new Child("child2")
child1.like.push("three")
console.log(child2.like) // ["one", "two"] - 不共享
特点:
- 语法更清晰,更接近传统面向对象语言
- 内置了
super关键字用于调用父类方法 - 支持静态方法的继承
- 本质仍是基于原型的语法糖
3 总结对比
| 继承方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 原型链继承 | 简单易实现 | 引用属性共享,无法传参 | 简单对象继承 |
| 构造函数继承 | 可传参,不共享引用属性 | 无法继承原型方法 | 需要隔离实例属性的场景 |
| 组合继承 | 结合两者优点 | 父类构造函数调用两次 | 通用场景 |
| 原型式继承 | 无需构造函数 | 共享引用属性 | 对象字面量继承 |
| 寄生式继承 | 可增强对象 | 方法不能复用 | 需要添加额外方法的场景 |
| 寄生组合继承 | 最优的 ES5方案 | 实现稍复杂 | 需要高效继承的场景 |
| ES6类继承 | 语法清晰,功能完善 | 需要转换工具支持旧浏览器 | 现代 JavaScript 开发 |