原型式继承的核心思想是:不需要定义一个“类”或构造函数,而是直接以一个现有对象为模板,创建一个新对象,让新对象的原型指向这个模板对象。道格拉斯·克罗克福德在 2006 年提出这一模式,ES5 将它内置为 Object.create() 方法。
// 模拟 Object.create 的简单版本
function createObject(proto) {
function F() {}
F.prototype = proto;
return new F();
}
const parent = {
name: 'Parent',
greet() {
return `I am ${this.name}`;
}
};
const child = Object.create(parent);
child.name = 'Child';
console.log(child.greet()); // "I am Child"
工作流程:
- 创建一个临时构造函数,将其
prototype指向传入的原型对象。 - 用
new调用该临时构造函数,返回一个全新对象,该对象的proto指向原型对象。 - 新对象本身是空的,但可以访问原型链上的属性和方法。
优点
- 极其简洁:不用写构造函数,不需要
new关键字,非常适合“一个对象需要继承另一个对象”的场景。 - 基于实例而非类型:符合 JavaScript 的“对象间委托”思想,没有类的概念负担。
缺点
- 引用类型共享问题:如果原型对象中包含数组或对象等引用类型,所有实例会共享同一个引用,一个实例修改会影响到其他实例(这与原型链继承的缺点类似)。
- 无法初始化参数:原型式继承直接从对象创建,没有类似构造函数传递定制参数的过程,无法对每个实例进行差异化初始化,通常需要手动添加或覆盖属性。
实际应用
原型式继承最常见的就是 Object.create(null) 创建纯字典对象(没有原型链上的属性和方法干扰),以及需要轻量级对象扩展时。在 Vue.js 的响应式系统中,也使用 Object.create 来完成某些原型委托。
寄生式继承
寄生式继承可以看作是原型式继承的增强版:创建一个仅用于封装继承过程的函数,在内部用原型式继承得到一个对象,然后对这个对象进行扩充(添加方法或属性),最后返回这个对象。这种“寄生”在已有对象上、再添枝加叶后再交出去的思路,类似于工厂模式,但基础来自原型式继承。
function createParasitic(original) {
// 1. 通过原型式继承创建新对象
const clone = Object.create(original);
// 2. 在新对象上添加自有方法或属性
clone.sayHi = function() {
return `Hi, I am ${this.name}`;
};
return clone;
}
const person = {
name: 'Anonymous',
greet() {
return `Hello, I am ${this.name}`;
}
};
const student = createParasitic(person);
student.name = 'Tom';
console.log(student.greet()); // "Hello, I am Tom"
console.log(student.sayHi()); // "Hi, I am Tom"
优点
- 可以看作是带自增强功能的工厂函数:不必创建构造函数或类,用函数封装继承与扩展逻辑,代码组织比较清晰。
- 适合只想一次性扩展某个对象,且不需要建立通用“类型”的场景。
缺点
- 方法无法复用:
createParasitic每调用一次,都会在内部分配新的函数(如sayHi),不同实例的sayHi并不共享,造成内存浪费。这与构造函数模式类似。 - 基础的引用类型共享问题依然存在(因为原型式继承是基础)。
适用场景
寄生式继承通常不单独使用,更多是作为组合继承或寄生组合式继承中的一个中间步骤。如果场景很小,只需要一个临时扩展的对象,比如需要对某个配置对象做预处理,然后交给其他模块使用,寄生式继承就能以最小成本完成任务。
寄生组合式继承
寄生组合式继承被视为 JavaScript 中最理想的继承方式,它结合了原型式继承的优点,并解决了“组合继承”(构造函数继承 + 原型链继承)中“父构造函数被调用两次”的性能问题。ES6 的 class extends 在底层本质上也是用寄生组合式继承实现的。
先回顾一下组合继承的问题:
组合继承为了同时继承父类的自有属性和原型方法,会执行:
- 在子构造函数内执行
Parent.call(this, args)—— 第一次调用父构造函数,得到一组子实例的自有属性。 - 执行
Child.prototype = new Parent()—— 第二次调用父构造函数,这次会创建一组挂载在原型上的属性(这些属性会被子实例的自有属性屏蔽,形成冗余)。
寄生组合式继承的改进思路:用原型式继承创建一个干净的、指向父类原型的对象,把它作为子类的原型,从而避免“new Parent()”的第二次调用。
function inheritPrototype(Child, Parent) {
// 1. 创建以 Parent.prototype 为原型的对象
const prototype = Object.create(Parent.prototype);
// 2. 修正 constructor 指向
prototype.constructor = Child;
// 3. 将这个对象赋给子类的 prototype
Child.prototype = prototype;
}
// 父类
function Animal(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
Animal.prototype.sayName = function() {
return this.name;
};
// 子类
function Dog(name, breed) {
Animal.call(this, name); // 第一次(也是唯一一次)调用父构造函数,继承自有属性
this.breed = breed;
}
// 关键步骤:寄生组合式继承
inheritPrototype(Dog, Animal);
// 子类原型上添加方法必须在原型替换之后
Dog.prototype.bark = function() {
return `${this.name} barks!`;
};
const dog1 = new Dog('Buddy', 'Golden');
const dog2 = new Dog('Max', 'Husky');
console.log(dog1.sayName()); // 'Buddy'
console.log(dog1.bark()); // 'Buddy barks!'
console.log(dog1.colors); // ['red', 'blue']
dog1.colors.push('green');
console.log(dog2.colors); // ['red', 'blue'] ——互不影响
console.log(dog1 instanceof Dog); // true
console.log(dog1 instanceof Animal);// true
核心要点
- 只调用一次父构造函数
Animal.call(this),获取父类中的实例属性(避免原型上出现冗余属性)。 - 子类的原型通过
Object.create(Parent.prototype)创建,它完美地复制了父类原型上的方法,而不包含父类的实例属性。 - 必须修正
constructor指向为子类自身,否则实例的constructor会错误地指向父类。 - 添加子类原型方法要在原型替换之后进行。
为什么说它是最理想的?
- 性能高:父构造函数只执行一次,没有重复初始化实例属性。
- 原型链清晰:
Child.prototype → (干净对象) → Parent.prototype,避免在子类原型上挂载不必要的父类属性。 - 符合语义:完全实现了面向对象继承所需要的特性:子类继承父类属性和方法,原型方法与实例属性分离,且可以自由扩展。
与 ES6 class 的关系
class Animal {
constructor(name) { this.name = name; }
sayName() { return this.name; }
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
bark() { return `${this.name} barks!`; }
}
编译成 ES5 后,extends 的实现就是用寄生组合式继承。也就是说,寄生组合式继承是 JavaScript 继承的成熟范式,也是 modern JavaScript 引擎和写法背后的核心继承机制。
实际应用
今天你已经不必手工写 inheritPrototype,直接用 class ... extends 即可。但理解寄生组合式继承能帮你:
- 看懂经典库的继承代码。
- 在面试中深入解释“ES6 类的本质”。
- 在需要极致的控制力(如按需 mixin)时,用原型式技巧写出更灵活的方案。