JavaScript 继承:从原型链到 ES6 类的全方位解析
JavaScript 作为一门原型-based的语言,其继承机制与 Java、C++ 等传统面向对象语言的"类继承"有本质区别。理解 JS 的继承,核心是理解原型链(Prototype Chain)——这是 JS 中对象之间共享属性和方法的底层逻辑。
在 ES6 引入class语法之前,JS 开发者通过构造函数和原型对象实现继承;ES6 的class则是原型继承的语法糖,让代码更接近传统 OOP 风格,但底层仍依赖原型链。
本文将从基础概念到高级实践,全面覆盖 JS 继承的方方面面:
- 原型链的底层逻辑
- 三种经典继承实现方式(构造函数+原型、Object.create、ES6 类)
- 关键概念(构造函数窃取、方法重写、super 关键字)
- 常见陷阱与避坑指南
- 最佳实践(类 vs 组合)
- 真实场景案例
目录#
- JS 继承的核心:原型链
- 1.1 原型的三个关键概念
- 1.2 原型链的工作原理
- 1.3 自身属性 vs 原型属性
- JS 继承的三种实现方式
- 2.1 构造函数+原型(经典继承)
- 2.2 Object.create:显式原型继承
- 2.3 ES6 类:语法糖的优雅实现
- 继承的关键概念深化
- 3.1 构造函数窃取:继承自身属性
- 3.2 方法重写与super关键字
- 3.3 多级继承:原型链的延伸
- 常见陷阱与避坑指南
- 4.1 忘记修复构造函数指向
- 4.2 误修改原型导致的"Live 原型"问题
- 4.3 原型链断裂引发的属性查找失败
- 4.4 滥用__proto__(已废弃)
- 最佳实践:从继承到组合
- 5.1 优先使用 ES6 类
- 5.2 用 Object.create 实现轻量级继承
- 5.3 组合优于继承:更灵活的代码组织
- 真实场景案例
- 6.1 UI 组件库的继承体系
- 6.2 用组合实现可复用行为
- 调试与排错技巧
- 总结
- 参考资料
1. JS 继承的核心:原型链#
要理解 JS 继承,必须先搞懂原型链(Prototype Chain)——这是 JS 中对象之间共享属性和方法的底层机制。
1.1 原型的三个关键概念#
JS 中每个对象(除了null)都有一个**[[Prototype]]**(内部插槽,无法直接访问),指向其"原型对象"。以下三个 API 用于操作原型:
Object.getPrototypeOf(obj):获取对象的原型(推荐)Object.setPrototypeOf(obj, proto):设置对象的原型(性能低,谨慎使用)obj.__proto__:访问原型的非标准方法(已废弃,仅浏览器环境支持)
此外,构造函数(如function Animal())有一个prototype属性,指向其创建的实例的原型对象。例如:
function Animal(name) {
this.name = name;
}
// Animal.prototype 是所有 Animal 实例的原型
const cat = new Animal('Mimi');
console.log(Object.getPrototypeOf(cat) === Animal.prototype); // true1.2 原型链的工作原理#
当访问对象的某个属性时,JS 会按以下顺序查找:
- 先在对象自身属性中查找(用
Object.hasOwn(obj, key)判断); - 如果没找到,就沿着原型链向上查找(依次访问
__proto__); - 直到找到
null(原型链的终点),返回undefined。
例如:
// 原型对象:Animal.prototype
Animal.prototype.speak = function() {
console.log(`${this.name} 发出声音`);
};
const cat = new Animal('Mimi');
cat.speak(); // 输出:Mimi 发出声音
// 查找过程:cat → Animal.prototype → Object.prototype → null1.3 自身属性 vs 原型属性#
- 自身属性:通过
this或对象字面量定义的属性(如cat.name); - 原型属性:定义在原型对象上的属性(如
Animal.prototype.speak)。
可以用以下方法区分:
console.log(Object.hasOwn(cat, 'name')); // true(自身属性)
console.log(Object.hasOwn(cat, 'speak')); // false(原型属性)
console.log('speak' in cat); // true(原型链中存在)2. JS 继承的三种实现方式#
JS 有三种主流继承方式,各有适用场景。
2.1 构造函数+原型:经典继承#
这是 ES6 之前最常用的继承方式,核心是构造函数窃取(继承自身属性)+ 原型赋值(继承原型方法)。
实现步骤#
- 定义父类构造函数(如
Animal),并在其原型上定义共享方法; - 定义子类构造函数(如
Dog),通过父类.call(this, ...)窃取父类的自身属性; - 将子类的原型指向父类原型的实例(
Dog.prototype = Object.create(Animal.prototype)); - 修复子类原型的
constructor指向(避免指向父类)。
示例代码#
// 父类:Animal
function Animal(name) {
this.name = name; // 自身属性:名字
this.type = '动物'; // 自身属性:类型
}
// 父类原型方法
Animal.prototype.speak = function() {
console.log(`${this.name} 发出声音`);
};
// 子类:Dog
function Dog(name, breed) {
// 1. 构造函数窃取:继承父类的自身属性
Animal.call(this, name);
this.breed = breed; // 子类自身属性:品种
}
// 2. 继承父类原型方法
Dog.prototype = Object.create(Animal.prototype);
// 3. 修复constructor指向(关键!)
Dog.prototype.constructor = Dog;
// 子类重写父类方法
Dog.prototype.speak = function() {
console.log(`${this.name}(${this.breed})汪汪叫`);
};
// 使用示例
const dog = new Dog('Buddy', '金毛寻回犬');
console.log(dog.name); // Buddy(继承自Animal)
console.log(dog.breed); // 金毛寻回犬(Dog自身属性)
dog.speak(); // 输出:Buddy(金毛寻回犬)汪汪叫2.2 Object.create:显式原型继承#
Object.create(proto)会创建一个新对象,其原型指向proto。这种方式更接近原型继承的本质,无需构造函数。
实现步骤#
- 定义父类原型对象(如
animalProto); - 定义子类原型对象,继承父类原型(
dogProto = Object.create(animalProto)); - 定义工厂函数,创建子类实例并初始化自身属性。
示例代码#
// 父类原型:animalProto
const animalProto = {
speak() {
console.log(`${this.name} 发出声音`);
}
};
// 子类原型:dogProto(继承自animalProto)
const dogProto = Object.create(animalProto);
// 子类原型方法
dogProto.speak = function() {
console.log(`${this.name}(${this.breed})汪汪叫`);
};
// 工厂函数:创建Dog实例
function createDog(name, breed) {
const dog = Object.create(dogProto); // 绑定原型
dog.name = name; // 初始化自身属性
dog.breed = breed;
return dog;
}
// 使用示例
const dog = createDog('Buddy', '金毛寻回犬');
dog.speak(); // 输出:Buddy(金毛寻回犬)汪汪叫
console.log(Object.getPrototypeOf(dog) === dogProto); // true2.3 ES6 类:语法糖的优雅实现#
ES6 引入的class语法是原型继承的语法糖,让代码更接近传统 OOP 风格,但底层仍依赖原型链。
实现步骤#
- 用
class定义父类(如Animal),通过constructor初始化自身属性; - 用
extends关键字让子类继承父类(如Dog extends Animal); - 子类
constructor中用super()调用父类构造函数(必须先调用super才能访问this)。
示例代码#
// 父类:Animal
class Animal {
constructor(name) {
this.name = name; // 自身属性
this.type = '动物';
}
// 原型方法(等价于Animal.prototype.speak)
speak() {
console.log(`${this.name} 发出声音`);
}
}
// 子类:Dog(继承自Animal)
class Dog extends Animal {
constructor(name, breed) {
// 必须先调用super(),否则无法访问this
super(name); // 调用父类构造函数,继承name和type
this.breed = breed; // 子类自身属性
}
// 重写父类方法
speak() {
console.log(`${this.name}(${this.breed})汪汪叫`);
}
}
// 使用示例
const dog = new Dog('Buddy', '金毛寻回犬');
dog.speak(); // 输出:Buddy(金毛寻回犬)汪汪叫
console.log(dog instanceof Animal); // true(原型链中存在Animal)2.4 三种方式对比#
| 方式 | 可读性 | 灵活性 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| 构造函数+原型 | 中等 | 高 | 高 | ES6 之前的项目 |
| Object.create | 低 | 极高 | 中 | 轻量级、无构造函数的场景 |
| ES6 类 | 高 | 中 | 低 | 现代项目(推荐) |
3. 继承的关键概念深化#
3.1 构造函数窃取:继承自身属性#
为什么需要构造函数窃取?因为原型继承只能继承原型上的方法,无法继承父类的自身属性(如Animal的name和type)。
例如,若不使用Animal.call(this, name):
function Dog(name, breed) {
// 未窃取父类构造函数
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
const dog = new Dog('Buddy', '金毛寻回犬');
console.log(dog.name); // undefined(未继承父类的name属性)通过父类.call(this, ...),可以在子类构造函数中重复执行父类的自身属性初始化逻辑,从而继承父类的自身属性。
3.2 方法重写与super关键字#
- 方法重写:子类可以重写父类的原型方法(如
Dog.prototype.speak覆盖Animal.prototype.speak); - super关键字:在子类方法中调用父类的同名方法(仅 ES6 类支持)。
示例:用super调用父类方法#
class Dog extends Animal {
speak() {
// 先调用父类的speak方法
super.speak();
// 再执行子类逻辑
console.log(`它是一只${this.breed},汪汪叫`);
}
}
const dog = new Dog('Buddy', '金毛寻回犬');
dog.speak();
// 输出:
// Buddy 发出声音
// 它是一只金毛寻回犬,汪汪叫3.3 多级继承:原型链的延伸#
JS 支持多级继承(子类继承自子类),原型链会随之延伸。例如:
// 一级父类:Animal
class Animal { /* ... */ }
// 二级父类:Mammal(继承自Animal)
class Mammal extends Animal {
constructor(name, hasFur) {
super(name);
this.hasFur = hasFur; // 哺乳动物特征:有毛
}
nurse() {
console.log(`${this.name} 哺乳幼崽`);
}
}
// 子类:Dog(继承自Mammal)
class Dog extends Mammal {
constructor(name, breed) {
super(name, true); // 哺乳动物有毛
this.breed = breed;
}
}
const dog = new Dog('Buddy', '金毛寻回犬');
dog.nurse(); // 输出:Buddy 哺乳幼崽
// 原型链:dog → Dog.prototype → Mammal.prototype → Animal.prototype → Object.prototype → null4. 常见陷阱与避坑指南#
4.1 忘记修复构造函数指向#
当用Object.create继承原型时,子类原型的constructor会默认指向父类。若不修复,会导致instanceof判断错误。
错误示例:
Dog.prototype = Object.create(Animal.prototype);
console.log(Dog.prototype.constructor); // Animal(错误,应指向Dog)
const dog = new Dog('Buddy', '金毛寻回犬');
console.log(dog.constructor === Dog); // false(错误)修复方法:
Dog.prototype = Object.create(Animal.prototype);
// 手动设置constructor指向
Dog.prototype.constructor = Dog;
console.log(dog.constructor === Dog); // true(正确)4.2 误修改原型导致的"Live 原型"问题#
原型对象是Live的——修改原型会影响所有已创建的实例(因为实例的__proto__指向原型对象)。
示例:
const dog = new Dog('Buddy', '金毛寻回犬');
// 后期修改父类原型
Animal.prototype.walk = function() {
console.log(`${this.name} 走路`);
};
dog.walk(); // 输出:Buddy 走路(已创建的实例也能访问新方法)风险:如果误修改了原生对象的原型(如Array.prototype),会影响所有数组实例,导致不可预期的问题。严禁修改原生对象的原型!
4.3 原型链断裂引发的属性查找失败#
若子类原型被赋值为null或错误对象,会导致原型链断裂,属性查找失败。
错误示例:
Dog.prototype = null; // 断裂原型链
const dog = new Dog('Buddy', '金毛寻回犬');
dog.speak(); // 报错:dog.speak is not a function避免方法:确保子类原型始终指向父类原型的实例(Object.create(父类.prototype))。
4.4 滥用__proto__(已废弃)#
__proto__是非标准属性(仅浏览器支持),且性能低下。推荐使用Object.getPrototypeOf和Object.setPrototypeOf代替。
错误示例:
const dog = new Dog('Buddy', '金毛寻回犬');
dog.__proto__ = Cat.prototype; // 不推荐正确示例:
const dog = new Dog('Buddy', '金毛寻回犬');
Object.setPrototypeOf(dog, Cat.prototype); // 推荐(但性能低,尽量避免)5. 最佳实践:从继承到组合#
5.1 优先使用 ES6 类#
ES6 类的语法更简洁、可读性更高,且内置super、extends等关键字,避免了手动处理原型的麻烦。现代项目优先使用类。
5.2 用 Object.create 实现轻量级继承#
当需要无构造函数的轻量级继承(如工具函数、简单对象)时,Object.create是更好的选择(无需处理构造函数和constructor修复)。
5.3 组合优于继承:更灵活的代码组织#
继承会导致紧耦合(子类依赖父类的实现细节),而组合(将功能拆分为独立的行为模块,通过Object.assign或混入整合)更灵活。
组合 vs 继承的对比#
| 特性 | 继承 | 组合 |
|---|---|---|
| 关系 | 是...的一种(Is-A) | 有...的功能(Has-A) |
| 灵活性 | 低(父类变化影响子类) | 高(可自由组合行为) |
| 代码复用 | 父类方法共享 | 行为模块共享 |
组合示例:用混入实现可复用行为#
// 独立行为模块:说话
const SpeakBehavior = {
speak(sound) {
console.log(`${this.name} ${sound}`);
}
};
// 独立行为模块:走路
const WalkBehavior = {
walk() {
console.log(`${this.name} 走路`);
}
};
// 类:Dog(组合行为模块)
class Dog {
constructor(name, breed) {
this.name = name;
this.breed = breed;
// 混入行为模块(将方法添加到实例)
Object.assign(this, SpeakBehavior, WalkBehavior);
}
}
const dog = new Dog('Buddy', '金毛寻回犬');
dog.speak('汪汪叫'); // 输出:Buddy 汪汪叫
dog.walk(); // 输出:Buddy 走路优势:
- 可自由组合多个行为(如
Dog可以同时有Speak和Walk,Cat可以有Speak和Climb); - 行为模块可独立维护,不依赖类的继承关系。
6. 真实场景案例#
6.1 UI 组件库的继承体系#
在 React/Vue 组件库中,通常会定义一个基础组件(BaseComponent),封装通用逻辑(如状态管理、事件处理),然后子类组件继承自基础组件。
// 基础组件:BaseComponent
class BaseComponent {
constructor(el) {
this.el = el; // 组件根元素
this.state = {}; // 组件状态
}
// 通用方法:更新状态并重新渲染
setState(newState) {
this.state = { ...this.state, ...newState };
this.render();
}
// 抽象方法:子类必须实现
render() {
throw new Error('子类必须实现render方法');
}
}
// 子类组件:Button(继承自BaseComponent)
class Button extends BaseComponent {
constructor(el, text) {
super(el);
this.text = text;
this.render(); // 初始化渲染
}
// 实现render方法
render() {
this.el.innerHTML = `<button>${this.text}</button>`;
this.el.addEventListener('click', () => {
this.setState({ clicked: true });
console.log('按钮被点击');
});
}
}
// 使用示例
const buttonEl = document.getElementById('btn');
const button = new Button(buttonEl, '点击我');6.2 用组合实现可复用行为#
在游戏开发中,角色的行为(如移动、攻击、跳跃)可以拆分为独立模块,通过组合赋予不同角色。
// 移动行为
const MoveBehavior = {
move(direction) {
console.log(`${this.name} 向${direction}移动`);
}
};
// 攻击行为
const AttackBehavior = {
attack(target) {
console.log(`${this.name} 攻击${target.name}`);
}
};
// 角色:战士(组合移动+攻击)
class Warrior {
constructor(name) {
this.name = name;
Object.assign(this, MoveBehavior, AttackBehavior);
}
}
// 角色:法师(组合移动+法术攻击)
class Mage {
constructor(name) {
this.name = name;
Object.assign(this, MoveBehavior);
this.castSpell = function(target) {
console.log(`${this.name} 对${target.name}释放法术`);
};
}
}
// 使用示例
const warrior = new Warrior('Arthur');
warrior.move('前'); // 输出:Arthur 向前移动
warrior.attack({ name: 'Goblin' }); // 输出:Arthur 攻击Goblin
const mage = new Mage('Merlin');
mage.move('后'); // 输出:Merlin 向后移动
mage.castSpell({ name: 'Dragon' }); // 输出:Merlin 对Dragon释放法术7. 调试与排错技巧#
- 查看原型链:用
console.dir(obj)在浏览器控制台查看对象的原型链(展开__proto__属性); - 检查构造函数:用
obj.constructor确认对象的构造函数是否正确; - 验证继承关系:用
instanceof判断对象是否属于某个类(dog instanceof Animal); - 查找属性来源:用
Object.getPrototypeOf(obj)逐层向上查找属性的定义位置。
8. 总结#
JS 继承的核心是原型链,三种实现方式各有优劣:
- 构造函数+原型:ES6 之前的经典方案,需手动处理原型和构造函数;
- Object.create:显式原型继承,适合轻量级场景;
- ES6 类:语法糖,现代项目的首选。
最佳实践:
- 优先使用 ES6 类,保持代码可读性;
- 避免修改原生对象的原型;
- 组合优于继承:当需要灵活复用行为时,用组合代替继承;
- 理解原型链的工作原理,避免常见陷阱(如构造函数指向错误、原型链断裂)。
9. 参考资料#
- MDN 文档:原型链
- MDN 文档:ES6 类
- MDN 文档:Object.create
- 《你不知道的 JavaScript(上卷)》:Chapter 5(原型)
- MDN 文档:super
- MDN 文档:Object.hasOwn