news 2026/4/15 21:58:47

JavaScript 中基于原型和原型链的继承方式详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaScript 中基于原型和原型链的继承方式详解

引言

在 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 引擎会执行以下查找过程:

  1. 先在对象自身查找;
  2. 如果找不到,则沿着__proto__向上查找其原型;
  3. 继续向上,直到找到该属性,或到达原型链顶端(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.sayChild实例不可见;
  • 方法无法复用:若在构造函数内定义方法,每个实例都会创建一份新函数,浪费内存。
适合只关心属性继承、不依赖原型方法的场景。

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"

优点:

  • 属性独立(不共享引用类型);
  • 方法复用(通过原型);
  • 支持传参;
  • instanceofisPrototypeOf正常工作。

缺点:

  • 父构造函数被调用了两次:
    • 第一次: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 面向对象编程的基石。掌握“空对象作为中介”的寄生组合式继承,不仅能写出高效、健壮的代码,更能深入理解这门语言的设计哲学——万物皆对象,继承靠委托

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!