手写JS寄生组合式继承代码教程

作者:木棉

时间:2026-10-30

浏览:460

本教程详细讲解JavaScript寄生组合式继承的实现原理、手写代码步骤、核心避坑要点与运行验证,带你彻底掌握前端经典继承模式。

文章目录
寄生组合式继承是 ECMAScript 5 时代被公认最理想的原型链继承方案,也是 ES6 中 class ... extends 语法的底层转译基石。本文先通过手写一段标准、健壮的 inheritPrototype 工具函数,直接解决组合继承调用两次父构造函数、原型存在冗余属性的问题;随后分步讲解各行代码的实现依据、原型链拓扑连接方式与执行验证步骤。
手写JS寄生组合式继承代码教程
图 0:寄生组合式继承技术概览与原型桥接设计

一、为什么需要寄生组合式继承

在探讨如何手写之前,我们需要明确它解决的具体痛点。在前端开发中,最常见的继承尝试往往始于“组合继承”(Combination Inheritance)。组合继承通过构造函数借用属性,并通过原型链继承方法:

// 组合继承常见写法
function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue'];
}
function Child(name, age) {
  Parent.call(this, name); // 第二次调用 Parent()
  this.age = age;
}
Child.prototype = new Parent(); // 第一次调用 Parent()
Child.prototype.constructor = Child;

组合继承虽然解决了引用类型属性共享与子类传参问题,但存在明显的缺陷:父构造函数被执行了两次。在 Child.prototype = new Parent() 阶段,Child.prototype 上会被挂载 name 和 colors 属性;而当 new Child() 实例化时,实例自身又通过 Parent.call(this) 创建了一份同名的 name 与 colors 实例属性。

这导致实例属性对原型同名属性产生属性遮蔽(Shadowing),不仅多占用了一份内存,而且在实例化时执行了两次父类构造逻辑,如果父构造函数包含复杂计算或副作用(如网络请求或事件注册),会带来严重的逻辑隐患。

手写JS寄生组合式继承代码教程
图 1:组合继承的属性冗余 vs 寄生组合式继承的精简原型结构对比

寄生组合式继承的核心目标非常单纯:保留 Parent.call(this) 借用属性,同时砍掉 new Parent() 这次多余的实例化调用,直接以一个纯净的空对象作为桥梁,建立子类原型与父类原型的连接。

二、核心手写实现:inheritPrototype 函数

寄生组合式继承的关键在于封装一个处理原型关系的辅助函数。现代 JavaScript 中通常命名为 inheritPrototype(原型寄生封装)。

完整工具函数代码

/**
 * 寄生组合式继承的核心工具函数
 * @param {Function} Child 子类构造函数
 * @param {Function} Parent 父类构造函数
 */
function inheritPrototype(Child, Parent) {
  // 1. 以父类原型为模板,创建一个干净的中间原型对象(不调用 Parent 构造函数)
  const prototype = Object.create(Parent.prototype);

  // 2. 将原型对象的 constructor 准确指向子类构造函数
  // 建议使用 Object.defineProperty 设置不可枚举,符合原生原型规范
  Object.defineProperty(prototype, 'constructor', {
    value: Child,
    writable: true,
    configurable: true,
    enumerable: false
  });

  // 3. 将修剪完善的原型对象赋值给子类的 prototype
  Child.prototype = prototype;
}
兼容性提示:如果运行环境不支持 ES5 的 Object.create(),可以用一个空函数 F 来实现同等效果: function create(proto) { function F() {} F.prototype = proto; return new F(); }。它的作用完全一致:产生一个 __proto__ 指向 proto 但内部没有任何自有属性的空对象。
手写JS寄生组合式继承代码教程
图 2:inheritPrototype 核心函数的四步执行逻辑

三、完整可运行的工程示例

下面给出一个包含属性定义、父类方法、子类方法以及多实例验证的完整工程代码。你可以直接将其放置在 Node.js 或浏览器控制台中执行:

// 1. 定义父类构造函数
function Parent(name) {
  this.name = name;
  this.skills = ['JavaScript', 'CSS'];
}

// 2. 在父类原型上定义公共方法
Parent.prototype.sayName = function() {
  return `Parent Name: ${this.name}`;
};

// 3. 定义子类构造函数:通过 Parent.call(this, ...) 借用父类属性
function Child(name, age) {
  // 继承父类的实例属性
  Parent.call(this, name);
  // 定义子类自有的实例属性
  this.age = age;
}

// 4. 关键步骤:执行原型链绑定(必须在子类挂载自身原型方法之前调用)
inheritPrototype(Child, Parent);

// 5. 在子类原型上添加子类专属方法
Child.prototype.sayAge = function() {
  return `Child Age: ${this.age}`;
};
手写JS寄生组合式继承代码教程
图 3:寄生组合继承形成的对象、原型与构造函数内存拓扑结构

四、运行结果验证与判断依据

编写完继承代码后,必须通过以下几个关键维度检验继承关系是否正确、健康:

// --- 验证 1:实例独立性检查(引用类型不串号)---
const child1 = new Child('Alice', 20);
const child2 = new Child('Bob', 22);

child1.skills.push('TypeScript');
console.log(child1.skills); // 输出示例: ['JavaScript', 'CSS', 'TypeScript']
console.log(child2.skills); // 输出示例: ['JavaScript', 'CSS'] (互不影响)

// --- 验证 2:方法继承与自身方法调用 ---
console.log(child1.sayName()); // 输出示例: "Parent Name: Alice"
console.log(child1.sayAge());  // 输出示例: "Child Age: 20"

// --- 验证 3:原型链与 instanceof 识别 ---
console.log(child1 instanceof Child);  // 输出示例: true
console.log(child1 instanceof Parent); // 输出示例: true

// --- 验证 4:构造器指针修正与不可枚举性 ---
console.log(child1.constructor === Child); // 输出示例: true
console.log(Object.keys(Child.prototype));  // 输出示例: ['sayAge'] (不包含 constructor)

// --- 验证 5:子类原型上不存在冗余的父类实例属性 ---
console.log(Child.prototype.name);   // 输出示例: undefined (彻底根除了组合继承的冗余属性)
console.log(Child.prototype.skills); // 输出示例: undefined
验证项目 预期结果 技术意义
child1.skills !== child2.skills true 确保各个子类实例持有的引用类型数组互相独立,杜绝原型污染。
child1 instanceof Parent true 原型链完整连接,多态与类型检测均正常工作。
Child.prototype.name undefined 原型上纯净无实例属性,不存在多余的属性遮蔽。
child1.constructor === Child true 构造函数指针准确指向子类,对象识别逻辑正确。

五、手写代码必须防范的三个隐蔽陷阱

在面试或实际编写该模式时,有几个非常容易出错的细节需要格外警惕:

  • 时序陷阱:挂载子类原型方法的先后顺序
    在执行 inheritPrototype(Child, Parent) 时,Child.prototype 会被完全覆写为新创建的对象。因此,子类自身的原型方法必须在 inheritPrototype 执行完毕之后添加。如果在调用前就挂载了方法,这些方法会直接随旧原型对象一起丢失。
  • 直接赋值陷阱:严禁 Child.prototype = Parent.prototype
    有些初学者试图省略 Object.create,直接将父类原型赋值给子类原型。这种做法会让 Child.prototype 和 Parent.prototype 指向同一个内存地址。一旦后续修正 Child.prototype.constructor = Child,就会导致父类原型的 constructor 变成 Child,父类的原型机制被彻底破坏。
  • constructor 可枚举性陷阱
    直接使用 prototype.constructor = Child 会让 constructor 成为可枚举属性(enumerable: true)。而在 JavaScript 原生规范中,原型上的 constructor 是不可枚举的。推荐使用 Object.defineProperty 进行精确配置,确保 for...in 遍历与 Object.keys() 得到符合规范的结果。
手写JS寄生组合式继承代码教程
图 4:寄生组合式继承工程检查清单与关键约束全景图

六、总结与现代 ES6 Class 的映射关系

寄生组合式继承之所以被奉为经典,是因为它在机制上达到了极致的平衡:通过构造函数继承保证了属性的独立与参数的灵活传递,通过原型副本桥接保证了公共方法的复用与原型链的连续性,同时避免了任何形式的代码冗余与无谓开销。

现代 JavaScript 的 class SubClass extends SuperClass 语法糖,在被 Babel 或 TypeScript 转译为 ES5 代码时,其原型链接与构造借用逻辑的核心实质上就是寄生组合式继承(辅以静态属性继承 Object.setPrototypeOf(Child, Parent) 与 super() 检查)。因此,深入掌握并能徒手写出寄生组合式继承,不仅是应对前端底层原理考查的必备功底,更是透彻理解现代面向对象 JavaScript 设计原理的必经之路。

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章