JavaScript 原型链机制深度解析:从构造函数到隐式委托

65 阅读6分钟

在 JavaScript 的设计哲学中,原型(Prototype)  是其面向对象系统的基石。与 Java 或 C++ 等基于类(Class)的语言不同,JavaScript 并不通过复制“蓝图”来创建对象,而是通过委托(Delegation)  机制将对象连接在一起。

本文将基于一系列实战代码,从内存管理的角度出发,深入剖析构造函数、原型对象与实例之间的三角关系,揭示原型链查找的底层逻辑,并探讨继承机制的演变。

这张图先放在这里,现在不懂没关系,待会儿你会划上来的 屏幕截图 2026-01-25 174707.png

第一章:起源——构造函数的内存困境

在 ES5 时代,我们通过构造函数来模拟“类”的概念。构造函数的核心作用是初始化新对象的属性。

1.1 私有属性与方法的内存开销

请看以下代码示例

JavaScript

function Car(color) {
    // 实例属性:每个对象独享一份内存
    this.color = color; 
    
    // 假设我们将方法直接写在构造函数内部(不推荐)
    // this.drive = function() {
    //     console.log('drive, 下赛道');
    // }
}

const car1 = new Car('霞光紫');
const car2 = new Car('海湾蓝');

如果在构造函数内部定义 drive 方法,每次执行 new Car() 时,JavaScript 引擎都会在内存中创建一个新的函数实例。这意味着 car1.drive 和 car2.drive 虽然功能相同,但在内存中是两个完全不同的地址。对于成千上万个实例而言,这是极大的内存浪费。

1.2 Prototype:共享内存的解决方案

为了解决上述问题,JavaScript 引入了 prototype 属性。它是函数对象特有的属性,用于存放所有实例共享的方法和属性。

JavaScript

// 资料来源:1.js
// 将公共方法挂载到 prototype 上
Car.prototype = {
    drive: function() {
        console.log('drive, 下赛道');
    },
    name: 'su7',
    long: 4800
};

const car1 = new Car('霞光紫');
car1.drive(); // 调用的是 Car.prototype 上的方法

此时,无论创建多少个 Car 实例,drive 方法在内存中只存在一份。实例通过一种隐式的链接访问该方法。这便是原型机制诞生的初衷:通过共享来优化资源


第二章:三角关系——构造函数、实例与原型对象

理解 JavaScript 面向对象的关键,在于理清构造函数(Constructor)、原型(Prototype)和实例(Instance)三者之间的关系。

2.1 核心概念界定

  1. prototype(显式原型)

    • 归属:只有函数(构造函数)才拥有此属性。
    • 作用:作为新创建实例的公共祖先(基因库)。
  2. proto(隐式原型)

    • 归属:所有对象(包括实例、函数、原型对象本身)都拥有此属性(现代标准推荐使用 Object.getPrototypeOf,但 proto 依然广泛存在于调试与旧代码中)。
    • 作用:它是连接对象与原型的“链条”,指向创建该对象的构造函数的 prototype。
  3. constructor

    • 归属:原型对象默认拥有的属性。
    • 作用:记录“我是由谁创造的”,指向构造函数本身。

2.2 终极公式与关系图

我们可以得出一个铁律公式:

JavaScript

// 实例的隐式原型 === 构造函数的显式原型
instance.__proto__ === Constructor.prototype

我们可以通过以下字符图来可视化这种关系:

Text

构造函数 (Person)
              +-----------------+
              | prototype       | ---------------------> +-----------------------+
              |                 |                        | Person.prototype      |
              +-----------------+ <--------------------- | constructor           |
                       ^                                 | sayHi() / species     |
                       | (new 操作)                       +-----------------------+
                       |                                           ^
              +-----------------+                                  |
              | 实例 (su)        |                                  |
              | __proto__       | ---------------------------------+
              +-----------------+

代码验证:

JavaScript

const su = new Person('舒老板', 19);
console.log(su.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true

第三章:链式查找——原型链的本质

当我们访问一个对象的属性时,JavaScript 引擎会启动一套严格的查找机制(Look-up)。

3.1 查找规则:向上委托

查找过程如下:

  1. 自身查找:检查对象实例本身(su)是否拥有该属性。
  2. 原型委托:如果没有,顺着 su.proto 找到 Person.prototype 继续查找。
  3. 层层上溯:如果 Person.prototype 也没有,继续顺着 Person.prototype.proto(即 Object.prototype)查找。
  4. 终点:如果 Object.prototype 也没有,查找 Object.prototype.proto,结果为 null,停止查找并返回 undefined。

JavaScript

var obj = {}; 
console.log(obj.__proto__.toString()); 
// 输出 [object Object],该方法来自于 Object.prototype

3.2 属性遮蔽(Shadowing)

一个常见的误区是认为修改实例属性会影响原型。实际上,JavaScript 的操作在原型链上的行为是不同的。

JavaScript

Person.prototype.species = '人类';
var su = new Person('舒老板', 19);

// "写"操作:直接在实例 su 上创建新属性,不会向上查找
su.species = 'LOL达人'; 

console.log(su.species); // 输出 'LOL达人' (来自实例)
console.log(su.__proto__.species); // 输出 '人类' (原型保持不变)

这种现象称为属性遮蔽(Shadowing) 。实例上的同名属性会“挡住”原型上的属性,但不会修改原型本身。这体现了 JS 原型继承的哲学:它不是基于复制的(像传统类继承那样),而是基于委托的。  如果我自己有,我就用自己的;如果我没有,我才去问我的原型。


第四章:进阶陷阱——重写原型与 Constructor 的丢失

在定义原型方法时,有两种常见的写法。新手往往因为图方便而陷入“重写原型”的陷阱。

4.1 增量添加(推荐)

采用了安全的写法:

JavaScript

Person.prototype.species = '人类';
// 此时 Person.prototype.constructor 依然指向 Person

4.2 重写对象(危险)

采用了覆盖写法:

JavaScript

// 危险操作:直接赋值一个新对象
Person.prototype = {
    species: '人类',
    sayHi: function() {}
};

后果
原始的 Person.prototype 对象被完全丢弃,取而代之的是一个新的对象字面量 {}。
这个新对象的 constructor 属性不再指向 Person,而是指向根构造函数 Object。

JavaScript

// 资料来源:3.html
var su = new Person('舒老板', 19);
// 结果为 false,因为 constructor 链条断裂
console.log(Person == Person.prototype.constructor); 
console.log(Person.prototype.constructor); // 输出 Object

修正方案
如果在实际开发中必须使用对象字面量赋值,必须手动修复 constructor:

JavaScript

Person.prototype = {
    constructor: Person, // 手动指回
    species: '人类'
};

第五章:继承的演变——从手动委托到 ES6 语法糖

原型链最强大的应用在于实现继承。展示了在 ES6 class 出现之前,开发者是如何手动构建继承链的。

5.1 手动构建原型链

我们希望 Person 继承 Animal 的属性。

JavaScript

var obj = new Object();
obj.species = '动物';

function Animal() {}
Animal.prototype = obj; // Animal 继承自 obj

function Person() {}
// 关键步骤:建立连接
// Person.prototype 变成了一个 Animal 的实例
// 因此 Person.prototype.__proto__ === Animal.prototype
Person.prototype = new Animal(); 

var su = new Person();
console.log(su.species); // 输出 '动物'

原理解析
查找路径为:su (无) -> su.proto (即 Person.prototype, 无) -> Person.prototype.proto (即 Animal.prototype/obj) -> 找到 '动物'

5.2 ES6 Class:语法的现代化

提到了 ES6 的 class 写法。必须明确的是,JavaScript 的 class 只是语法糖(Syntactic Sugar)

JavaScript

class Person {
    constructor(name) { this.name = name; }
    sayHi() { ... }
}

其底层依然遵循原型链机制:

  • class 声明的本质是构造函数。
  • 类的方法依然被挂载在 prototype 上。
  • extends 关键字实际上是在执行类似 Person.prototype = Object.create(Animal.prototype) 的操作。

技术总结

  1. 构造函数负责处理对象的差异性(私有属性),原型对象负责处理对象的共性(共享方法)。
  2. proto  是对象寻找属性的“地图导线”,prototype 是函数提供的“公共资源库”。
  3. 原型查找遵循就近原则:读取属性时沿链向上查找,写入属性时只操作实例自身(遮蔽效应)。
  4. 慎重重写 Prototype:直接赋值 {...} 给 prototype 会导致 constructor 指向丢失,需手动修正。
  5. 继承的本质是委托:JavaScript 的对象之间是通过引用链接的,而非传统面向对象语言中的代码复制。su 能访问 species,不是因为它拥有了 species,而是因为它被委托了访问权限。

如果这篇文章有助于你理解原型链机制,就请您点个赞吧,谢谢