人人都会AI编程

原型链继承、构造函数继承、组合继承

更新时间:2026-07-11

原型链继承是 JavaScript 中最纯粹的继承方式,直接利用了原型链的查找机制:让子类的原型对象指向父类的实例,从而子类实例可以通过原型链访问父类的方法和属性。

function Parent() {
  this.names = ['张三', '李四'];
}
Parent.prototype.sayHello = function() {
  console.log('你好,我是父类');
};

function Child() {}
// 关键一步:将子类原型指向父类实例
Child.prototype = new Parent();

const child1 = new Child();
const child2 = new Child();
child1.names.push('王五');
console.log(child2.names); // ['张三', '李四', '王五'],共享了引用类型
child1.sayHello(); // '你好,我是父类'

原理分析

  • Child.prototype 现在是 Parent 的一个实例,该实例的内部 [[Prototype]](即 proto)指向 Parent.prototype
  • 当访问 child1.sayHello 时,先在自身属性中找,找不到就去 Child.prototype(即父类实例)上找,还找不到就继续沿着原型链到 Parent.prototype 上找,最终找到方法。

核心缺点

  • 引用类型属性被所有实例共享:如上面的 names 数组,当原型对象上存在引用类型值时,所有子类实例会共享同一个引用,一处修改处处受影响。
  • 无法向父类构造函数传参:在创建 Child.prototype = new Parent() 这一步时,无法为不同子类实例传递不同的参数,所有子类实例共享同一个父类实例的属性初始值。
  • 子类原型 constructor 指向错误Child.prototype.constructor 会指向 Parent,需要手动修复。

适用场景

几乎没有单独使用的价值,通常作为组合继承的一部分出现,或用于极简单的单例式继承。

4.5.2 构造函数继承

构造函数继承通过在子类构造函数中直接调用父类构造函数,并利用 callapply 方法将父类构造函数内的属性绑定到子类实例上。这种方式本质上只是复制属性,并不涉及原型链。

function Parent(name) {
  this.name = name;
  this.colors = ['红', '蓝'];
}
Parent.prototype.sayHello = function() {
  console.log('你好');
};

function Child(name, age) {
  // 在子类构造函数中调用父类构造函数,并传入当前实例
  Parent.call(this, name);
  this.age = age;
}

const child1 = new Child('小明', 10);
const child2 = new Child('小红', 12);
child1.colors.push('绿');
console.log(child1.name);    // '小明'
console.log(child1.colors);  // ['红', '蓝', '绿']
console.log(child2.colors);  // ['红', '蓝'],互不影响

// 但无法继承父类原型上的方法
child1.sayHello(); // TypeError: child1.sayHello is not a function

原理分析

  • Parent.call(this, name)Parent 内部的 this 强制绑定为当前的 Child 实例,所以 Parent 内部通过 this.xxx 定义的属性都会被直接挂载到子类实例上。
  • 每个子类实例都拥有父类属性的独立副本,解决了原型链继承中引用类型共享的痛点。

核心优点

  • 可以向父类构造函数传递参数,每个实例可以有不同的初始值。
  • 引用类型属性不再共享,安全可靠。

核心缺点

  • 无法继承父类原型上的方法:因为构造函数的 call 只复制实例属性,子类实例和父类原型之间没有任何联系。
  • 方法必须定义在构造函数内部(每次实例化都会重新创建函数),无法实现函数复用,内存消耗大。

适用场景

单独使用的情况很少,更多的是与其他方式配合,解决引用类型共享和传参问题。

4.5.3 组合继承

组合继承(有时也称“伪经典继承”)将原型链继承构造函数继承两者的优点结合起来:用原型链继承方法,用构造函数继承属性。它曾经是 JavaScript 中最常用的继承模式。

function Parent(name) {
  this.name = name;
  this.colors = ['红', '蓝'];
}
Parent.prototype.sayHello = function() {
  console.log('我是' + this.name);
};

function Child(name, age) {
  // 第二次调用 Parent:继承属性
  Parent.call(this, name);
  this.age = age;
}
// 第一次调用 Parent:继承原型方法
Child.prototype = new Parent();
// 修复 constructor 指向
Child.prototype.constructor = Child;

const child1 = new Child('小明', 10);
const child2 = new Child('小红', 12);

child1.colors.push('绿');
console.log(child1.colors); // ['红', '蓝', '绿']
console.log(child2.colors); // ['红', '蓝'],属性独立
child1.sayHello();          // '我是小明',可继承方法

原理分析

  • 属性继承:通过 Parent.call(this, name),每个子类实例都拥有父类实例属性的独立副本,避免了引用类型共享。
  • 方法继承:通过 Child.prototype = new Parent(),子类原型成为父类实例,从而能通过原型链访问 Parent.prototype 上定义的方法,实现函数复用。
  • 修复 constructor 指向,保证类型判断不出错。

核心优点

  • 既能向父类传参,又能共享原型方法,解决了前两种继承模式各自的硬伤。
  • 是 ES6 之前最成熟、最通用的继承实现方式。

核心缺点

  • 父类构造函数被调用了两次:一次是设置原型 new Parent(),另一次是在子类构造函数内部 Parent.call(this)。这导致子类原型上多了一套父类实例属性,虽然被实例属性屏蔽(因为实例属性同名时会覆盖原型属性),但造成了一定的内存浪费和逻辑冗余。
  • 实现稍显繁琐,需要手动修复 constructor 和维护原型链。

现实地位

在 ES6 引入 class 语法之前,组合继承几乎是所有严肃项目中处理继承问题的标配方案。理解它的运行机制,对于阅读和维护老代码、深入理解原型链仍然至关重要。下一节将介绍如何用寄生组合继承来修复“两次调用”的问题,以及 ES6 Class 如何用更优雅的方式完成同样的工作。