JavaScript 原型继承的三种经典模式

32 阅读3分钟

在 JavaScript 的面向对象编程中,继承并非通过类定义实现,而是依托于**原型链(Prototype Chain)**机制。由于早期语言设计未引入 class 语法,开发者需手动构建父子对象之间的“血缘关系”。这一过程虽略显繁琐,却深刻体现了 JavaScript 灵活而动态的本质。本文将围绕三种主流的原型继承模式——构造函数继承、原型链继承与组合式中介继承——剖析其原理、优劣及适用场景。

构造函数继承:复用实例属性

最直接的继承方式是在子类构造函数中调用父类构造函数,并通过 callapply 显式绑定 this

function Animal(name, age) {
  this.name = name;
  this.age = age;
}
function Cat(name, age, color) {
  Animal.call(this, name, age);
  this.color = color;
}

此方法确保每个 Cat 实例都能拥有独立的 nameage 属性,避免了引用类型属性被共享的问题。同时,call 允许逐个传递参数,而 apply 则接受参数数组,两者在功能上等价,仅调用形式不同。然而,这种模式仅继承了实例属性,无法访问父类原型上的方法或属性,导致 cat instanceof Animal 返回 false

原型链继承:共享原型方法

为了让子类也能使用父类原型上的成员,需将子类的 prototype 指向父类的原型或其实例:

Cat.prototype = Animal.prototype;
// 或
Cat.prototype = new Animal();

前者通过引用赋值直接共享同一原型对象,内存效率高,但存在严重副作用:对 Cat.prototype 的任何修改(如添加 eat 方法)都会污染 Animal.prototype,破坏封装性。

后者则通过创建父类实例作为子类原型,既保留了原型方法,又实现了隔离。但问题在于,若父类构造函数依赖参数初始化,new Animal() 可能因缺少必要参数而产生无效状态。此外,子类原型的 constructor 默认指向父类,需手动修正:

Cat.prototype.constructor = Cat;

否则 cat.constructor 将错误地返回 Animal,影响类型识别。

中介空对象继承:安全的原型桥接

为兼顾隔离性与原型复用,一种更稳健的做法是引入空函数作为中介

function extend(Child, Parent) {
  var F = function() {};
  F.prototype = Parent.prototype;
  Child.prototype = new F();
  Child.prototype.constructor = Child;
}
extend(Cat, Animal);

这里,F 是一个不执行任何逻辑的空构造函数。将其原型指向 Parent.prototype 后,再以 new F() 创建子类原型。由于 F 无自身属性,其实例纯粹作为桥梁,既继承了父类原型链,又避免了直接修改 Parent.prototype。此时,Cat.prototype 是一个独立对象,其 __proto__ 指向 Animal.prototype,形成干净的继承链。

这种模式被广泛应用于早期类库(如 jQuery)中,成为事实上的标准继承方案。它完美解决了引用污染问题,同时保持了原型方法的可访问性。

组合继承:属性与方法的完整复用

实际开发中,往往需要同时继承实例属性和原型方法。因此,构造函数继承 + 中介原型继承的组合模式最为实用:

function Cat(name, age, color) {
  Animal.apply(this, [name, age]); // 继承属性
  this.color = color;
}
extend(Cat, Animal); // 继承方法
Cat.prototype.eat = function() {
  console.log(this.name + '在吃');
};

如此,Cat 实例既拥有独立的 nameagecolor,又能调用 specieseat 方法。通过 instanceof 判断也完全符合预期:

const cat = new Cat('加菲猫', 2, '黄色');
console.log(cat instanceof Cat);     // true
console.log(cat instanceof Animal);  // true

总结

JavaScript 的原型继承虽无现代 class extends 语法那般简洁,但其底层机制揭示了语言的核心设计理念:对象之间通过原型链动态关联,而非静态类定义。理解 call/apply 的上下文绑定、prototype 的引用特性,以及中介空对象的隔离作用,不仅能帮助我们写出健壮的继承代码,也为深入掌握 Vue、React 等框架的响应式原理打下基础。

尽管 ES6 已提供 class 语法糖,但其本质仍是原型链的封装。在阅读源码或调试复杂继承关系时,回归原始模式仍不可或缺。掌握这三种继承方式,意味着你已真正踏入 JavaScript 面向对象编程的大门。