在 JavaScript 的设计哲学中,原型(Prototype) 是其面向对象系统的基石。与 Java 或 C++ 等基于类(Class)的语言不同,JavaScript 并不通过复制“蓝图”来创建对象,而是通过委托(Delegation) 机制将对象连接在一起。
本文将基于一系列实战代码,从内存管理的角度出发,深入剖析构造函数、原型对象与实例之间的三角关系,揭示原型链查找的底层逻辑,并探讨继承机制的演变。
这张图先放在这里,现在不懂没关系,待会儿你会划上来的
第一章:起源——构造函数的内存困境
在 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 核心概念界定
-
prototype(显式原型) :
- 归属:只有函数(构造函数)才拥有此属性。
- 作用:作为新创建实例的公共祖先(基因库)。
-
proto(隐式原型) :
- 归属:所有对象(包括实例、函数、原型对象本身)都拥有此属性(现代标准推荐使用 Object.getPrototypeOf,但 proto 依然广泛存在于调试与旧代码中)。
- 作用:它是连接对象与原型的“链条”,指向创建该对象的构造函数的 prototype。
-
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 查找规则:向上委托
查找过程如下:
- 自身查找:检查对象实例本身(su)是否拥有该属性。
- 原型委托:如果没有,顺着 su.proto 找到 Person.prototype 继续查找。
- 层层上溯:如果 Person.prototype 也没有,继续顺着 Person.prototype.proto(即 Object.prototype)查找。
- 终点:如果 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) 的操作。
技术总结
- 构造函数负责处理对象的差异性(私有属性),原型对象负责处理对象的共性(共享方法)。
- proto 是对象寻找属性的“地图导线”,prototype 是函数提供的“公共资源库”。
- 原型查找遵循就近原则:读取属性时沿链向上查找,写入属性时只操作实例自身(遮蔽效应)。
- 慎重重写 Prototype:直接赋值 {...} 给 prototype 会导致 constructor 指向丢失,需手动修正。
- 继承的本质是委托:JavaScript 的对象之间是通过引用链接的,而非传统面向对象语言中的代码复制。su 能访问 species,不是因为它拥有了 species,而是因为它被委托了访问权限。
如果这篇文章有助于你理解原型链机制,就请您点个赞吧,谢谢