引言
在 JavaScript 的世界中,继承是通过
原型(prototype)和
原型链(prototype chain)机制实现的。与传统面向对象语言(如 Java、C++)不同,JavaScript 并没有真正意义上的“类继承”概念——即使
ES6引入了class语法,它本质上也只是对原型继承的语法糖封装。JavaScript 的核心继承模型是基于对象的委托机制,即一个对象可以通过其内部的[[Prototype]]链访问另一个对象的属性和方法。
本文将系统、深入地介绍 JavaScript 中几种常见的基于原型和原型链的继承方式,并重点解析其中“使用空对象作为中介”的经典模式——
寄生组合式继承,帮助你彻底掌握 JS 面向对象编程的底层逻辑。
一、基本概念回顾
1. 原型(Prototype)
在 JavaScript 中,每个函数(function)都有一个prototype属性,该属性指向一个对象。当这个函数被用作构造函数(通过new调用)时,所创建的实例对象会自动将其内部[[Prototype]](可通过__proto__访问)链接到该prototype对象上。
function Parent() {} console.log(Parent.prototype); // { constructor: Parent } const p = new Parent(); console.log(p.__proto__ === Parent.prototype); // true注意:普通对象没有prototype属性,只有函数才有。但所有对象(包括函数)都有__proto__(或可通过Object.getPrototypeOf()获取),用于构成原型链。
2. 原型链(Prototype Chain)
当访问一个对象的属性(如obj.prop)时,JavaScript 引擎会执行以下查找过程:
- 先在对象自身查找;
- 如果找不到,则沿着
__proto__向上查找其原型; - 继续向上,直到找到该属性,或到达原型链顶端(
null)为止。
const obj = {}; console.log(obj.toString); // obj 自身没有 toString,但: // obj.__proto__ → Object.prototype → 找到 toString 方法 // 最终输出:function toString() { [native code] }这种链式查找机制就是原型链,它是 JavaScript 实现继承的核心。
二、常见的基于原型的继承方式
1. 原型链继承(Prototype Chain Inheritance)
这是最基础的继承方式:让子类的prototype指向父类的一个实例。
function Parent() { this.name = 'parent'; this.colors = ['red', 'blue']; // 引用类型属性 } Parent.prototype.say = function() { console.log('Hello from parent'); }; function Child() {} // 关键:Child.prototype 指向 Parent 的一个实例 Child.prototype = new Parent(); const child1 = new Child(); const child2 = new Child(); child1.colors.push('green'); console.log(child2.colors); // ['red', 'blue', 'green'] 被污染! child1.say(); // "Hello from parent"问题分析:
- 引用属性共享:
colors数组存在于Child.prototype上,所有子实例共享同一份数据。 - 无法传参:创建
Child实例时无法向Parent构造函数传递参数。 - 语义不清晰:
Child.prototype包含了本应属于实例的属性(如name),造成冗余。
此方式仅适用于无状态、纯方法复用的场景,实际开发中极少单独使用。
2. 构造函数继承(借用构造函数 / Classical Inheritance)
通过在子构造函数内部调用父构造函数(使用.call()或.apply()),实现属性的“复制式”继承。
function Parent(name) { this.name = name; this.colors = ['red', 'blue']; } function Child(name) { Parent.call(this, name); // 借用父构造函数,this 指向新创建的 Child 实例 } const child1 = new Child('Alice'); const child2 = new Child('Bob'); child1.colors.push('green'); console.log(child2.colors); // ['red', 'blue'] 独立副本优点:
- 每个实例拥有独立的属性,避免引用类型污染;
- 支持向父类传参。
缺点:
- 无法继承父类原型上的方法。例如
Parent.prototype.say对Child实例不可见; - 方法无法复用:若在构造函数内定义方法,每个实例都会创建一份新函数,浪费内存。
适合只关心属性继承、不依赖原型方法的场景。
3. 组合继承(Combination Inheritance)
结合前两种方式的优点:用构造函数继承属性,用原型链继承方法。
function Parent(name) { this.name = name; this.colors = ['red', 'blue']; } Parent.prototype.say = function() { console.log('Hi, I am ' + this.name); }; function Child(name, age) { Parent.call(this, name); // 继承属性(可传参,不共享) this.age = age; } // 继承方法:设置 Child.prototype 为 Parent 实例 Child.prototype = new Parent(); // 问题:这里会无参调用 Parent() Child.prototype.constructor = Child; // 修复 constructor 指向 const child = new Child('Tom', 10); child.say(); // "Hi, I am Tom"优点:
- 属性独立(不共享引用类型);
- 方法复用(通过原型);
- 支持传参;
instanceof和isPrototypeOf正常工作。
缺点:
- 父构造函数被调用了两次:
- 第一次:
Parent.call(this, name)—— 正确初始化实例属性; - 第二次:
new Parent()—— 在设置原型时无意义地创建了一个冗余的父实例,其属性(如name: undefined)被挂在Child.prototype上,造成内存浪费。
- 第一次:
尽管有缺陷,组合继承曾是 ES5 时代最常用的继承模式。
三、重点解析:空对象作为中介的继承方式(寄生组合式继承)
为了解决组合继承中父构造函数被调用两次的问题,寄生组合式继承(Parasitic Combination Inheritance)应运而生。这是《JavaScript 高级程序设计》作者 Nicholas C. Zakas 推荐的最高效、最理想的 ES5 继承方式。
核心思想
不通过new Parent()创建子类原型,而是创建一个“干净”的空对象,让这个空对象的原型指向Parent.prototype。
这样既能建立正确的原型链,又避免执行Parent构造函数,从而消除冗余属性。
实现步骤详解
function inheritPrototype(Child, Parent) { // Step 1: 创建一个空的构造函数 F(中介) function F() {} // Step 2: 将 F 的 prototype 指向 Parent.prototype // 这样 F 的实例就能“继承” Parent.prototype 上的所有方法 F.prototype = Parent.prototype; // Step 3: 将 Child.prototype 设置为 F 的一个实例 // new F() 是一个空对象,其 __proto__ 指向 Parent.prototype // 它不包含 Parent 构造函数初始化的任何实例属性(如 name、colors) Child.prototype = new F(); // Step 4: 修复 constructor,确保 Child.prototype.constructor 指向 Child // 否则会错误地指向 Parent(因为 F.prototype = Parent.prototype) Child.prototype.constructor = Child; }完整使用示例
function Parent(name) { this.name = name; this.colors = ['red', 'blue']; } Parent.prototype.say = function() { console.log('Parent says:', this.name); }; function Child(name, age) { Parent.call(this, name); // 借用构造函数继承属性(仅调用一次!) this.age = age; } // 使用空对象中介实现原型继承 inheritPrototype(Child, Parent); const child = new Child('Lucy', 8); child.say(); // "Parent says: Lucy" // 验证继承关系 console.log(child instanceof Parent); // true console.log(child instanceof Child); // true console.log(child.constructor === Child); // true console.log(child.colors); // ['red', 'blue'](来自实例,非原型) // 检查原型链 console.log(child.__proto__ === Child.prototype); // true console.log(Child.prototype.__proto__ === Parent.prototype); // true为什么这种方式更优?
| 优势 | 说明 |
|---|---|
| ✅ 只调用一次父构造函数 | 仅在 Parent.call(this, name) 中执行,无冗余 |
| ✅ 子类原型干净 | Child.prototype 上没有 name、colors 等实例属性 |
| ✅ 完整保留原型链 | child → Child.prototype → Parent.prototype → Object.prototype → null |
| ✅ 内存高效 | 避免了组合继承中在原型上存储无用属性的问题 |
| ✅ 语义正确 | 属性归实例,方法归原型,职责分明 |
关键理解:new F()创建的对象是一个“空壳代理”,它唯一的使命是作为桥梁,将Child.prototype的__proto__指向Parent.prototype,而不携带任何由Parent构造函数初始化的数据。
四、现代替代方案:Object.create
()
ES5 标准引入了Object.create(proto, [propertiesObject])方法,可以直接创建一个以指定对象为原型的新对象。这使得寄生组合式继承的实现更加简洁:
function Child(name, age) { Parent.call(this, name); this.age = age; } // 使用 Object.create 替代中介函数 Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child;Object.create(Parent.prototype)的效果等同于new F()(其中F.prototype = Parent.prototype),但由引擎原生实现,更安全、更高效。
此外,还可以封装一个通用继承函数:
function extend(Child, Parent) { Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child; }五、补充:原型式继承(Prototypal Inheritance)
虽然不属于“类式继承”,但Object.create()也支持直接基于现有对象创建新对象,这体现了 JavaScript真正的原型继承思想:
const person = { name: 'Anonymous', friends: ['Alice'], greet() { console.log(`Hi, I'm ${this.name}`); } }; const me = Object.create(person); me.name = 'John'; me.friends.push('Bob'); console.log(me.name); // "John" console.log(person.friends); // ['Alice', 'Bob'] 共享引用! // 若需深拷贝属性,可配合属性描述符或后续赋值此方式适用于无需构造函数、只需对象复用的场景,如配置模板、默认选项等。
六、总结对比表
| 继承方式 | 是否共享引用属性 | 能否传参 | 能否继承原型方法 | 父构造函数调用次数 | 是否推荐 |
|---|---|---|---|---|---|
| 原型链继承 | 是 | 否 | 是 | 1(设置原型时) | ❌ |
| 构造函数继承 | 否 | 是 | 否 | 1(子构造中) | ⚠️ 局限 |
| 组合继承 | 否 | 是 | 是 | 2 | ⚠️ 可用但非最优 |
| 寄生组合式继承(空对象中介) | 否 | 是 | 是 | 1 | ✅ 强烈推荐(ES5) |
| 原型式继承(Object.create) | 是 | 否 | 是(来自源对象) | 0 | ✅ 特定场景适用 |
七、最佳实践建议
- 在 ES5 环境中:优先使用寄生组合式继承,可通过
Object.create(Parent.prototype)简化实现。 - 在 ES6+ 环境中:直接使用
class extends语法,它在底层正是基于寄生组合式继承实现的:
class Parent { constructor(name) { this.name = name; } say() { console.log('Hi from', this.name); } } class Child extends Parent { constructor(name, age) { super(name); // 相当于 Parent.call(this, name) this.age = age; } }- 永远记住:JavaScript 的继承不是“复制”,而是“委托”。理解
[[Prototype]]链的查找机制,比死记语法更重要。
结语:原型和原型链是 JavaScript 面向对象编程的基石。掌握“空对象作为中介”的寄生组合式继承,不仅能写出高效、健壮的代码,更能深入理解这门语言的设计哲学——万物皆对象,继承靠委托。