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

引言

在 JavaScript 的世界中,继承是通过原型(prototype) 原型链(prototype chain)机制实现的。与传统面向对象语言(如 Java、C++)不同,JavaScript 并没有真正意义上的"类继承"概念------即使 ES6 引入了 class 语法,它本质上也只是对原型继承的语法糖封装。JavaScript 的核心继承模型是基于对象的委托机制 ,即一个对象可以通过其内部的 [[Prototype]] 链访问另一个对象的属性和方法。

本文将系统、深入地介绍 JavaScript 中几种常见的基于原型和原型链的继承方式,并重点解析其中"使用空对象作为中介"的经典模式------寄生组合式继承,帮助你彻底掌握 JS 面向对象编程的底层逻辑。


一、基本概念回顾

1. 原型(Prototype)

在 JavaScript 中,每个函数(function)都有一个 prototype 属性 ,该属性指向一个对象。当这个函数被用作构造函数(通过 new 调用)时,所创建的实例对象会自动将其内部 [[Prototype]](可通过 __proto__ 访问)链接到该 prototype 对象上。

javascript 复制代码
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)为止。
arduino 复制代码
const obj = {};
console.log(obj.toString); 
// obj 自身没有 toString,但:
// obj.__proto__ → Object.prototype → 找到 toString 方法
// 最终输出:function toString() { [native code] }

这种链式查找机制就是原型链,它是 JavaScript 实现继承的核心。


二、常见的基于原型的继承方式

1. 原型链继承(Prototype Chain Inheritance)

这是最基础的继承方式:让子类的 prototype 指向父类的一个实例

javascript 复制代码
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()),实现属性的"复制式"继承。

ini 复制代码
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)

结合前两种方式的优点:用构造函数继承属性,用原型链继承方法

ini 复制代码
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 构造函数,从而消除冗余属性。

实现步骤详解

javascript 复制代码
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;
}

完整使用示例

javascript 复制代码
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 上没有 namecolors 等实例属性
完整保留原型链 child → Child.prototype → Parent.prototype → Object.prototype → null
内存高效 避免了组合继承中在原型上存储无用属性的问题
语义正确 属性归实例,方法归原型,职责分明

关键理解new F() 创建的对象是一个"空壳代理",它唯一的使命是作为桥梁,将 Child.prototype__proto__ 指向 Parent.prototype,而不携带任何由 Parent 构造函数初始化的数据。


四、现代替代方案:Object.create()

ES5 标准引入了 Object.create(proto, [propertiesObject]) 方法,可以直接创建一个以指定对象为原型的新对象。这使得寄生组合式继承的实现更加简洁:

ini 复制代码
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),但由引擎原生实现,更安全、更高效。

此外,还可以封装一个通用继承函数:

ini 复制代码
function extend(Child, Parent) {
  Child.prototype = Object.create(Parent.prototype);
  Child.prototype.constructor = Child;
}

五、补充:原型式继承(Prototypal Inheritance)

虽然不属于"类式继承",但 Object.create() 也支持直接基于现有对象创建新对象,这体现了 JavaScript 真正的原型继承思想

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 语法,它在底层正是基于寄生组合式继承实现的:

    scala 复制代码
    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 面向对象编程的基石。掌握"空对象作为中介"的寄生组合式继承,不仅能写出高效、健壮的代码,更能深入理解这门语言的设计哲学------万物皆对象,继承靠委托

相关推荐
用户600071819102 小时前
【翻译】如何在Vue中使用Suspense处理异步渲染?
前端
acaiEncode2 小时前
nvm use xxx 报错: exit status 145: The directory is not empty.
前端·node.js
三秦赵哥2 小时前
Prompt 优化教程
前端
光影少年2 小时前
react怎么实现响应式?
前端·react.js·前端框架
奋斗吧程序媛2 小时前
Vue Router的路由模式
前端·javascript·vue.js
by__csdn2 小时前
Vue.js 生命周期全解析:从创建到销毁的完整指南
前端·javascript·vue.js·前端框架·ecmascript·css3·html5
m0_471199632 小时前
【JavaScript】前端如何处理服务端部分接口加解密
开发语言·前端·javascript
盐焗西兰花2 小时前
鸿蒙学习实战之路-Web 页面适配最佳实践
前端·学习·harmonyos
离别又见离别3 小时前
vue使用js渲染组件案例(公用打印组件动态渲染)及静默打印实现
前端·javascript·vue