JavaScript 继承:从原型链到 ES6 类的全方位解析

JavaScript 作为一门原型-based的语言,其继承机制与 Java、C++ 等传统面向对象语言的"类继承"有本质区别。理解 JS 的继承,核心是理解原型链(Prototype Chain)——这是 JS 中对象之间共享属性和方法的底层逻辑。

在 ES6 引入class语法之前,JS 开发者通过构造函数原型对象实现继承;ES6 的class则是原型继承的语法糖,让代码更接近传统 OOP 风格,但底层仍依赖原型链。

本文将从基础概念到高级实践,全面覆盖 JS 继承的方方面面:

  • 原型链的底层逻辑
  • 三种经典继承实现方式(构造函数+原型、Object.create、ES6 类)
  • 关键概念(构造函数窃取、方法重写、super 关键字)
  • 常见陷阱与避坑指南
  • 最佳实践(类 vs 组合)
  • 真实场景案例

目录#

  1. JS 继承的核心:原型链
    • 1.1 原型的三个关键概念
    • 1.2 原型链的工作原理
    • 1.3 自身属性 vs 原型属性
  2. JS 继承的三种实现方式
    • 2.1 构造函数+原型(经典继承)
    • 2.2 Object.create:显式原型继承
    • 2.3 ES6 类:语法糖的优雅实现
  3. 继承的关键概念深化
    • 3.1 构造函数窃取:继承自身属性
    • 3.2 方法重写与super关键字
    • 3.3 多级继承:原型链的延伸
  4. 常见陷阱与避坑指南
    • 4.1 忘记修复构造函数指向
    • 4.2 误修改原型导致的"Live 原型"问题
    • 4.3 原型链断裂引发的属性查找失败
    • 4.4 滥用__proto__(已废弃)
  5. 最佳实践:从继承到组合
    • 5.1 优先使用 ES6 类
    • 5.2 用 Object.create 实现轻量级继承
    • 5.3 组合优于继承:更灵活的代码组织
  6. 真实场景案例
    • 6.1 UI 组件库的继承体系
    • 6.2 用组合实现可复用行为
  7. 调试与排错技巧
  8. 总结
  9. 参考资料

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); // true

1.2 原型链的工作原理#

当访问对象的某个属性时,JS 会按以下顺序查找:

  1. 先在对象自身属性中查找(用Object.hasOwn(obj, key)判断);
  2. 如果没找到,就沿着原型链向上查找(依次访问__proto__);
  3. 直到找到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 → null

1.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 之前最常用的继承方式,核心是构造函数窃取(继承自身属性)+ 原型赋值(继承原型方法)。

实现步骤#

  1. 定义父类构造函数(如Animal),并在其原型上定义共享方法;
  2. 定义子类构造函数(如Dog),通过父类.call(this, ...)窃取父类的自身属性;
  3. 将子类的原型指向父类原型的实例(Dog.prototype = Object.create(Animal.prototype));
  4. 修复子类原型的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。这种方式更接近原型继承的本质,无需构造函数。

实现步骤#

  1. 定义父类原型对象(如animalProto);
  2. 定义子类原型对象,继承父类原型(dogProto = Object.create(animalProto));
  3. 定义工厂函数,创建子类实例并初始化自身属性。

示例代码#

// 父类原型: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); // true

2.3 ES6 类:语法糖的优雅实现#

ES6 引入的class语法是原型继承的语法糖,让代码更接近传统 OOP 风格,但底层仍依赖原型链。

实现步骤#

  1. class定义父类(如Animal),通过constructor初始化自身属性;
  2. extends关键字让子类继承父类(如Dog extends Animal);
  3. 子类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 构造函数窃取:继承自身属性#

为什么需要构造函数窃取?因为原型继承只能继承原型上的方法,无法继承父类的自身属性(如Animalnametype)。

例如,若不使用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 → null

4. 常见陷阱与避坑指南#

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.getPrototypeOfObject.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 类的语法更简洁、可读性更高,且内置superextends等关键字,避免了手动处理原型的麻烦。现代项目优先使用类

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可以同时有SpeakWalkCat可以有SpeakClimb);
  • 行为模块可独立维护,不依赖类的继承关系。

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. 调试与排错技巧#

  1. 查看原型链:用console.dir(obj)在浏览器控制台查看对象的原型链(展开__proto__属性);
  2. 检查构造函数:用obj.constructor确认对象的构造函数是否正确;
  3. 验证继承关系:用instanceof判断对象是否属于某个类(dog instanceof Animal);
  4. 查找属性来源:用Object.getPrototypeOf(obj)逐层向上查找属性的定义位置。

8. 总结#

JS 继承的核心是原型链,三种实现方式各有优劣:

  • 构造函数+原型:ES6 之前的经典方案,需手动处理原型和构造函数;
  • Object.create:显式原型继承,适合轻量级场景;
  • ES6 类:语法糖,现代项目的首选。

最佳实践

  • 优先使用 ES6 类,保持代码可读性;
  • 避免修改原生对象的原型;
  • 组合优于继承:当需要灵活复用行为时,用组合代替继承;
  • 理解原型链的工作原理,避免常见陷阱(如构造函数指向错误、原型链断裂)。

9. 参考资料#

  1. MDN 文档:原型链
  2. MDN 文档:ES6 类
  3. MDN 文档:Object.create
  4. 《你不知道的 JavaScript(上卷)》:Chapter 5(原型)
  5. MDN 文档:super
  6. MDN 文档:Object.hasOwn