JavaScript 继承全方案详解:从原型链到 Class 语法
前言:为什么需要继承?
让我们从一个现实场景开始:
function User(name) {
this.name = name
}
User.prototype.login = function() {
console.log(`${this.name} logged in`)
}
// 现在需要创建 Admin 用户,拥有 User 的所有特性外加特殊权限
// 如何避免重复代码?这就是继承要解决的问题!
JavaScript 作为一门基于原型的语言,提供了多种实现继承的方式,每种方式都有其特点和适用场景。
1. 原型链继承
基本实现
function Parent() {
this.name = 'Tom'
this.like = [1, 2]
}
function Child(age) {
this.age = age
}
Child.prototype = new Parent()
const child1 = new Child(18)
const child2 = new Child(20)
特点分析
-
优点:
- 简单直观
- 父类原型方法可被继承
-
缺点:
- 多个实例共享同一个原型对象,引用类型属性会相互影响
child1.like.push(3) console.log(child2.like) // [1, 2, 3]
- 子类无法向父类构造函数传参
// 无法在创建 Child 时初始化 Parent 的 name
- 多个实例共享同一个原型对象,引用类型属性会相互影响
内存图示
child1 → Parent实例(作为原型) → Parent.prototype
child2 ↗
2. 构造函数继承(经典继承)
基本实现
function Parent(name) {
this.name = name
}
Parent.prototype.getName = function() {
return this.name
}
function Child(name) {
Parent.call(this, name) // 关键步骤
this.age = 18
}
const c1 = new Child('Tom')
特点分析
-
优点:
- 解决了原型链继承的两个问题:
- 每个实例都有独立的属性
- 可以向父类传参
- 解决了原型链继承的两个问题:
-
缺点:
- 无法继承父类原型上的方法
console.log(c1.getName) // undefined
- 方法只能在构造函数中定义,无法复用
- 无法继承父类原型上的方法
适用场景
- 只需要继承实例属性时
- 不关心父类原型方法的情况
3. 组合继承(伪经典继承)
基本实现
function Parent(name) {
this.name = name
}
Parent.prototype.getName = function() {
return this.name
}
function Child(name) {
Parent.call(this, name) // 第二次调用 Parent
this.age = 18
}
Child.prototype = new Parent() // 第一次调用 Parent
Child.prototype.constructor = Child
const c1 = new Child('Tom')
console.log(c1.getName()) // 'Tom'
特点分析
-
优点:
- 结合了原型链和构造函数的优点
- 实例属性独立,可继承原型方法
-
缺点:
- 父类构造函数被调用两次,导致:
- 子类原型上有多余的属性
- 性能开销
- 父类构造函数被调用两次,导致:
内存图示
c1 →
|- 自身属性: name, age
|- __proto__ → Parent实例(包含多余的name属性) → Parent.prototype
4. 原型式继承
基本实现
const person = {
name: 'Tom',
friends: ['Alice', 'Bob']
}
const p1 = Object.create(person)
p1.friends.push('Charlie')
const p2 = Object.create(person)
console.log(p2.friends) // ['Alice', 'Bob', 'Charlie']
特点分析
-
优点:
- 简洁的对象继承
- 不需要构造函数
-
缺点:
- 与原型链继承相同,引用类型属性共享
- 无法实现代码复用(没有构造函数)
与 Object.create()
的关系
// 等效于
function object(o) {
function F() {}
F.prototype = o
return new F()
}
适用场景
- 简单对象继承
- 不需要构造函数的场景
5. 寄生组合式继承(终极方案)
基本实现
Parent.prototype.getName = function() {
return this.name;
}
function Parent(name) {
this.name = name
}
// 关键步骤1:建立原型链连接
Child.prototype = Object.create(Parent.prototype)
// 关键步骤2:修复构造函数指向
Child.prototype.constructor = Child
function Child(name) {
// 关键步骤3:调用父类构造函数
Parent.call(this, name)
this.age = 18
}
let c1 = new Child('Tom')
代码解析:三步实现完美继承
第一步:原型链连接
Child.prototype = Object.create(Parent.prototype)
-
作用:创建一个以
Parent.prototype
为原型的新对象,作为Child.prototype
-
优势:
- 避免了
new Parent()
带来的多余属性问题 - 保持了纯净的原型链
- 避免了
-
对比原型链继承:
- 原型链继承:
Child.prototype = new Parent()
(会在原型上创建不必要的实例属性) - 寄生组合式:只继承原型方法,不包含实例属性
- 原型链继承:
第二步:修复构造函数
Child.prototype.constructor = Child
-
为什么需要:
- 上一步操作会导致
Child.prototype.constructor
指向Parent
- 这步修正确保实例的
constructor
属性正确指向Child
- 上一步操作会导致
-
重要性:
- 保持构造函数引用的正确性
- 某些库会依赖这个属性
第三步:调用父类构造函数
Parent.call(this, name)
-
作用:
- 在子类实例上初始化父类的属性
- 确保每个实例都有独立的属性副本
-
优势:
- 解决了原型链继承的属性共享问题
- 可以向父类传递参数
内存结构图解
c1 实例:
{
name: 'Tom', // 来自 Parent.call(this, name)
age: 18, // 子类自有属性
__proto__: Child.prototype {
constructor: Child,
__proto__: Parent.prototype {
getName: function...,
__proto__: Object.prototype
}
}
}
6. ES6 Class 继承
基本实现
class Parent {
constructor(name) {
this.name = name
}
getName() {
return this.name
}
}
class Child extends Parent {
constructor(name) {
super(name) // 相当于 Parent.call(this, name)
this.age = 18
}
}
const c = new Child('Tom')
特点分析
-
优点:
- 语法简洁
- 底层使用寄生组合式继承
- 内置
super
关键字访问父类
-
注意事项:
- 必须先调用
super()
才能使用this
- class 方法是不可枚举的
- 本质仍是原型继承的语法糖
- 必须先调用
Babel 转译结果
// 实际会被转译为类似寄生组合继承的代码
对比总结
继承方式 | 原型方法 | 实例属性独立 | 避免重复调用 | 语法简洁性 |
---|---|---|---|---|
原型链继承 | ✅ | ❌ | ✅ | ⭐⭐ |
构造函数继承 | ❌ | ✅ | ✅ | ⭐⭐ |
组合继承 | ✅ | ✅ | ❌ | ⭐⭐⭐ |
原型式继承 | N/A | ❌ | ✅ | ⭐⭐⭐ |
寄生组合式继承 | ✅ | ✅ | ✅ | ⭐ |
Class 继承 | ✅ | ✅ | ✅ | ⭐⭐⭐⭐ |
面试常见问题
Q1: 各种继承方式的优缺点?
回答策略:
- 按顺序介绍每种方式
- 重点对比原型链/构造函数/组合继承
- 最终引出寄生组合是最佳方案
- 说明 class 是语法糖
Q2: 实现一个继承函数?
function extend(Child, Parent) {
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
// 可选:保存父类引用
Child.super = Parent.prototype
}
Q3: super 关键字的原理?
回答要点:
- 在构造函数中指向父类构造函数
- 在方法中指向父类原型
- 内部通过
[[HomeObject]]
实现
最佳实践建议
- 现代项目:直接使用 class 语法
- 旧代码维护:使用寄生组合式继承
- 避免:
- 直接修改
__proto__
- 深度继承链(通常3层以上就难以维护)
- 直接修改
- 注意:
- 继承会增加代码耦合度
- 考虑组合优于继承的原则
终极方案代码
// 最健壮的继承实现
function inherit(Child, Parent) {
// 创建原型对象
const prototype = Object.create(Parent.prototype, {
constructor: {
value: Child,
enumerable: false,
writable: true,
configurable: true
}
})
// 设置静态属性继承
if (Object.setPrototypeOf) {
Object.setPrototypeOf(Child, Parent)
} else {
Child.__proto__ = Parent
}
Child.prototype = prototype
}
// 使用示例
function Parent(name) {
this.name = name
}
Parent.prototype.say = function() {
console.log(this.name)
}
function Child(name, age) {
Parent.call(this, name)
this.age = age
}
inherit(Child, Parent)
理解 JavaScript 继承机制是成为高级开发者的必经之路。掌握这些知识后,你将能够:
- 在面试中游刃有余
- 阅读框架源码更轻松
- 设计更合理的对象体系
- 避免常见的继承陷阱