JavaScript继承的六种方式及优缺点对比

作者:代码拾荒

时间:2026-11-04

浏览:605

深入解析JavaScript实现继承的六种经典模式:原型链继承、借用构造函数、组合继承、原型式继承、寄生式继承以及寄生组合式继承。通过代码示例、原型内存图解及优缺点全方位对比,理清JS面向对象演进逻辑。

文章目录
JavaScript继承的六种方式及优缺点对比

假设你正在封装一套通用的业务模型:基类 User 拥有姓名、ID以及权限校验方法,而子类 Admin 需要在此基础上扩展超级管理权限。在 ES6 出现 class 和 extends 之前,JavaScript 是通过原型链和构造函数来模拟面向对象继承特性的。即使在现代前端开发中,ES6 的类继承本质上依然是基于寄生组合式继承的语法糖。本文将依次剖析经典的六种继承实现方案,理清各方案背后的内存机制、演进动力与缺陷权衡。

JavaScript继承的六种方式及优缺点对比
图1:JavaScript 六种继承方式逻辑推导与演化关系图

一、原型链继承 (Prototype Chain)

原型链继承是最直观的继承思路:将子类的原型对象直接赋值为父类的一个新实例。如此一来,子类实例在自身查找不到某些属性或方法时,就会沿着隐式原型链 __proto__ 向父类实例以及父类原型检索。

1. 实现代码

function Parent() {
  this.name = 'Parent';
  this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function() {
  return this.name;
};

function Child() {
  this.type = 'Child';
}

// 核心:子类原型指向父类实例
Child.prototype = new Parent();
// 修正 constructor 指向
Child.prototype.constructor = Child;

const c1 = new Child();
const c2 = new Child();

c1.colors.push('black');
console.log(c2.colors); // ['red', 'blue', 'green', 'black'] (受到污染)
JavaScript继承的六种方式及优缺点对比
图2:原型链继承导致引用类型属性被所有实例共享修改

2. 优缺点对比

✔ 优点

  • 简单直接:符合原型链直觉,代码实现只有一行赋值。
  • 方法复用:父类原型上定义的方法能被所有子类实例复用,不会重复创建函数。

✖ 缺点

  • 引用属性共享篡改:父类构造函数中的引用属性变成了子类原型属性,任意一个实例修改该引用,所有实例都会受到影响。
  • 无法传参:在实例化子类时,无法在不影响其他实例的前提下向父类构造函数传递个性化参数。

二、借用构造函数继承 (Constructor Stealing)

为了解决原型链继承中“引用值共享”以及“无法向父类传参”的两大痛点,开发者提出了借用构造函数(也称经典继承)。核心思想是在子类构造函数内部通过 call() 或 apply() 调用父类构造函数。

1. 实现代码

function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}

function Child(name, age) {
  // 核心:在子类上下文中借调父类构造函数
  Parent.call(this, name);
  this.age = age;
}

const c1 = new Child('Tom', 18);
const c2 = new Child('Jerry', 20);

c1.colors.push('black');
console.log(c1.colors); // ['red', 'blue', 'green', 'black']
console.log(c2.colors); // ['red', 'blue', 'green'] (相互独立,互不影响)

2. 优缺点对比

✔ 优点

  • 独立属性:避免了引用类型属性被所有实例共享的问题,每个实例都有一份属于自己的独立拷贝。
  • 支持传参:子类可以在 Parent.call(this, ...args) 中灵活传递参数给父类构造函数。

✖ 缺点

  • 函数无法复用:所有方法都必须在构造函数中定义,每次 new Child() 都会重复开辟内存创建相同的函数对象。
  • 丢失父类原型方法:子类实例与父类原型之间没有建立链接,无法访问 Parent.prototype 上定义的方法。

三、组合继承 (Combination Inheritance)

组合继承(又称伪经典继承)结合了原型链继承与借用构造函数的各自长处:利用借用构造函数来继承实例私有属性并支持传参,再通过将子类原型重写为父类实例来继承原型上的共享方法。

1. 实现代码

function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.sayName = function() {
  console.log('My name is ' + this.name);
};

function Child(name, age) {
  // 第二次调用父类构造函数:继承实例属性并传递参数
  Parent.call(this, name);
  this.age = age;
}

// 第一次调用父类构造函数:继承原型上的方法
Child.prototype = new Parent();
Child.prototype.constructor = Child;

Child.prototype.sayAge = function() {
  console.log('Age: ' + this.age);
};

const c1 = new Child('Alice', 22);
c1.colors.push('purple');
console.log(c1.colors); // ['red', 'blue', 'green', 'purple']

const c2 = new Child('Bob', 24);
console.log(c2.colors); // ['red', 'blue', 'green'] (未受污染,且可调用 sayName)

2. 优缺点对比

✔ 优点

  • 属性独立且方法共享:既能保证实例拥有独立的属性与引用副本,又能让方法挂载在原型上实现内存复用。
  • 类型识别完整:instanceof 和 isPrototypeOf() 能正确识别子类及父类类型。

✖ 缺点

  • 性能开销:父类构造函数会被调用两次(一次在原型赋值 new Parent(),一次在实例创建 Parent.call(this))。
  • 属性冗余与屏蔽:子类原型对象上存在一份冗余的父类属性(被子类实例自身的同名属性屏蔽),造成轻微的内存浪费。

四、原型式继承 (Prototypal Inheritance)

由前端大师 Douglas Crockford 提出。如果你的目标不是构建复杂的类和类型层次,而仅仅是希望基于一个现有对象派生出一个结构相似的新对象,原型式继承便是最轻量的选择。ECMAScript 5 的 Object.create() 正是该模式的官方标准化实现。

1. 实现代码

// Crockford 的原始实现
function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

// 使用场景
const person = {
  name: 'Default',
  friends: ['Shelby', 'Court']
};

const anotherPerson = object(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Rob');

const yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Barbie');

console.log(person.friends); // ['Shelby', 'Court', 'Rob', 'Barbie'] (浅拷贝共享)

2. 优缺点对比

✔ 优点

  • 极度轻量:无需自定义繁琐的构造函数,适合快速在已有对象基础上实现浅层次的原型委托。
  • 语言内置原生支持:Object.create() 原生 API 性能优秀且语法简洁。

✖ 缺点

  • 引用值依然共享:与原型链继承一样,原型对象中的引用类型属性仍然在所有派生对象之间共享。
  • 无法像类那样定制构造过程:缺乏构造函数参数校验和独立的内部状态初始化环节。

五、寄生式继承 (Parasitic Inheritance)

寄生式继承与原型式继承紧密相连。其核心策略是:创建一个仅用于封装继承过程的工厂函数,在函数内部使用原型式继承克隆对象,然后直接对该对象进行属性或方法“增强”,最后将其返回。

1. 实现代码

function createAnother(original) {
  // 通过任意可以返回新对象的函数克隆一个对象
  const clone = Object.create(original);
  
  // 寄生扩展:为新对象挂载专属方法(增强对象)
  clone.sayHi = function() {
    console.log('Hi, I am ' + this.name);
  };
  
  return clone;
}

const user = {
  name: 'Kobe',
  hobbies: ['basketball']
};

const athlete = createAnother(user);
athlete.sayHi(); // "Hi, I am Kobe"

2. 优缺点对比

✔ 优点

  • 封装性良好:将对象的克隆与功能增强统一封装在工厂函数中,使用便利。
  • 适合纯对象模式:针对无需构造函数的场景,能够灵活追加业务特性。

✖ 缺点

  • 方法无法复用:每次调用 createAnother() 为对象添加的方法都是独立创建的闭包函数,降低了内存利用率。
  • 引用属性共享隐患:底层基于原型式继承,原型上的引用属性依然存在跨对象污染风险。

六、寄生组合式继承 (Parasitic Combination Inheritance)

前面提到,组合继承几乎是理想的继承模式,唯一的瑕疵在于:为了让子类原型继承父类原型上的方法,执行了一次多余的 Child.prototype = new Parent(),不仅造成了多余的构造函数开销,还在 Child.prototype 上挂载了一组无用的实例属性。

寄生组合式继承的核心突破在于:我们根本不需要为了继承父类原型而实例化父类。我们只需要一个干净的、以父类原型为原型的中间空对象即可!这被公认为 ES6 之前最完美、最成熟的继承模式,也是 Babel 将 ES6 class ... extends 转译为 ES5 时的底层实现方案。

JavaScript继承的六种方式及优缺点对比
图3:组合继承冗余属性遮蔽 vs 寄生组合继承轻量桥接对比

1. 标准实现代码

// 核心工具函数:继承原型的标准化步骤
function inheritPrototype(subType, superType) {
  // 1. 创建父类原型的一个纯净副本(无多余自身属性)
  const prototype = Object.create(superType.prototype);
  // 2. 增强对象:修正由于重写原型导致丢失的 constructor 指向
  prototype.constructor = subType;
  // 3. 将新创建的副本赋值给子类的原型
  subType.prototype = prototype;
}

// 父类
function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.sayName = function() {
  console.log('Parent Name: ' + this.name);
};

// 子类
function Child(name, age) {
  // 仅在子类实例化时调用一次父类构造函数
  Parent.call(this, name);
  this.age = age;
}

// 核心调用:建立原型链接
inheritPrototype(Child, Parent);

Child.prototype.sayAge = function() {
  console.log('Child Age: ' + this.age);
};

// 验证
const c1 = new Child('Jack', 19);
c1.colors.push('yellow');
console.log(c1.colors); // ['red', 'blue', 'green', 'yellow']

const c2 = new Child('Rose', 18);
console.log(c2.colors); // ['red', 'blue', 'green'] (独立私有)
console.log(c1 instanceof Child);  // true
console.log(c1 instanceof Parent); // true

2. 优缺点对比

✔ 优点 (最优解)

  • 只调用一次父类构造函数:极大优化了运行期性能与内存占用。
  • 避免原型冗余属性:子类原型是干净的空对象桥接,不会出现重复同名属性的遮蔽。
  • 原型链保持完整:类型检测完全兼容 instanceof 和 isPrototypeOf。

✖ 缺点

  • 实现复杂度略高:需要额外的辅助函数 inheritPrototype 或显式处理原型对象的桥接与 constructor 修复。

七、六种继承方式综合对比与选型维度

为了让不同场景下的技术选型更加清晰,下表从多个工程关键维度对上述六种方案进行了全方位的横向对比:

继承方式 属性独立性 方法复用度 向父类传参 父构造调用次数 原型冗余属性 推荐程度
原型链继承 差 (引用共享) 支持 (原型共享) 不支持 1 次 无 不推荐
借用构造函数 优 (实例独享) 无法复用 支持 1 次 无 不推荐
组合继承 优 (实例独享) 支持 (原型共享) 支持 2 次 (开销) 存在冗余 可用 (经典)
原型式继承 差 (引用共享) 支持 无构造概念 0 次 无 特定轻量场景
寄生式继承 差 (引用共享) 无法复用 无构造概念 0 次 无 较少使用
寄生组合式继承 优 (实例独享) 支持 (原型共享) 支持 仅 1 次 无冗余 最强推荐 (标准)
JavaScript继承的六种方式及优缺点对比
图4:JavaScript 继承演化机制与工程实践选型决策

八、总结与现代实践

回顾 JavaScript 继承的演化史,本质上是社区围绕“属性隔离”、“方法复用”以及“调用成本最小化”三大目标反复平衡的过程:

  • 原型链继承 实现了方法复用,却栽在引用属性共享与传参缺失上;
  • 借用构造函数 实现了独立属性与个性化传参,却丢掉了原型方法的共享优势;
  • 组合继承 兼收并蓄,但留下了“执行两次父构造”与“原型冗余属性遮蔽”的瑕疵;
  • 原型式与寄生式 开辟了对象浅拷贝与轻量增强的便捷路径;
  • 寄生组合式继承 以简洁巧妙的空对象桥接(Object.create)消除了组合继承的最后一道性能瓶颈,成为当之无愧的典范。

在日常业务开发中,我们应当优先采用语法清晰、内建完备的 class extends;而在设计大型框架插件架构、深入排查原型链遮蔽 Bug 或编写底层代码时,深刻掌握寄生组合继承的思想将成为你的核心技术护城河。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章