在 JavaScript 的面向对象编程中,继承并非通过类定义实现,而是依托于**原型链(Prototype Chain)**机制。由于早期语言设计未引入 class 语法,开发者需手动构建父子对象之间的“血缘关系”。这一过程虽略显繁琐,却深刻体现了 JavaScript 灵活而动态的本质。本文将围绕三种主流的原型继承模式——构造函数继承、原型链继承与组合式中介继承——剖析其原理、优劣及适用场景。
构造函数继承:复用实例属性
最直接的继承方式是在子类构造函数中调用父类构造函数,并通过 call 或 apply 显式绑定 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 实例都能拥有独立的 name 和 age 属性,避免了引用类型属性被共享的问题。同时,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 实例既拥有独立的 name、age、color,又能调用 species 和 eat 方法。通过 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 面向对象编程的大门。