好的,我们来彻底讲清楚 JavaScript 中的原型 (prototype) 和原型链 (prototype chain)。
我会用一个尽量形象的方式,从根本原因讲起,让你不仅知道“是什么”,更理解“为什么是这样”。
1. 从一个问题开始:JavaScript 如何实现“共享”与“继承”?
想象一下,你正在用一个构造函数(可以暂时理解为“类”的早期雏形)来创建很多个“人”的对象:
JavaScript
function Person(name, age) {
this.name = name;
this.age = age;
// 假设我们想让每个人都能打招呼
// this.sayHello = function() {
// console.log("你好,我是" + this.name);
// };
}
let person1 = new Person("张三", 30);
let person2 = new Person("李四", 25);
现在,我们希望 person1 和 person2 都有一个 sayHello 的方法。最直接的想法是把 sayHello 放在 Person 函数内部(如上面注释掉的代码)。
但这会带来一个严重的问题:每 new 一个 Person 实例,就会在内存里创建一个新的 sayHello 函数。如果有一万个实例,内存里就有一万个功能完全相同的函数。这显然是巨大的资源浪费。
核心问题: 如何让所有 Person 的实例都能“共享”同一个 say-Hello 函数,而不是各自持有一个?
为了解决这个问题,JavaScript 的设计者引入了 原型 (prototype) 机制。
2. 核心概念:原型 (Prototype)
JavaScript 的规定是:每个函数(Function)都天生自带一个 prototype 属性,这个属性指向一个对象。这个对象,我们就称之为 原型对象。
这个原型对象有什么用呢?
它的作用就是:作为由这个函数创建出来的所有实例的“公共仓库”。
我们可以把需要共享的属性和方法,直接添加到这个原型对象上。
JavaScript
function Person(name, age) {
this.name = name;
this.age = age;
}
// 不在构造函数内部定义,而是加在它的原型对象上
Person.prototype.sayHello = function() {
console.log("你好,我是" + this.name);
};
Person.prototype.species = "人类";
let person1 = new Person("张三", 30);
let person2 = new Person("李四", 25);
person1.sayHello(); // 输出: 你好,我是张三
person2.sayHello(); // 输出: 你好,我是李四
console.log(person1.species); // 输出: 人类
console.log(person2.species); // 输出: 人类
console.log(person1.sayHello === person2.sayHello); // 输出: true
看,现在 sayHello 和 species 这两个属性被所有实例共享了,内存中只存在一份。问题解决了。
那么,新的问题来了:person1 明明自己身上没有 sayHello 方法,它是怎么找到并调用 Person.prototype 上的 sayHello 方法的呢?
这就引出了下一个概念:__proto__ 和 原型链。
3. 连接的桥梁:__proto__ 与原型链 (Prototype Chain)
__proto__ (隐式原型)
JavaScript 的规定是:每个对象(Object)都天生自带一个 __proto__ 属性,这个属性指向创建它的那个构造函数的原型对象。
-
person1是由new Person()创建的,所以person1的__proto__属性指向Person.prototype。 -
person2也是由new Person()创建的,所以person2的__proto__属性也指向Person.prototype。
我们可以验证一下:
console.log(person1.proto === Person.prototype); // 输出: true
这个 __proto__ 属性就是实例对象和它的“公共仓库”(原型对象)之间连接的桥梁。
注意:
__proto__是一个非标准的、但被广泛实现的属性,用于我们理解和调试。在现代 JavaScript 中,推荐使用Object.getPrototypeOf(obj)来获取一个对象的原型。
原型链 (Prototype Chain)
现在我们来完整地描述一下,当执行 person1.sayHello() 时,JavaScript 引擎到底做了什么:
-
首先,在
person1对象自身上查找,看有没有sayHello属性。——没有。 -
然后,顺着
person1的__proto__属性,找到了Person.prototype这个对象。 -
在
Person.prototype对象上查找,看有没有sayHello属性。——找到了!于是就执行它。
这个 “顺着 __proto__ 不断向上查找属性的过程” ,形成的这个链式结构,就是 原型链。
链的终点是什么?
我们继续这个查找过程。如果在 Person.prototype 上也找不到呢?比如,我们调用一个不存在的方法 person1.toString()(假设我们没在 Person.prototype 上定义过它)。
-
在
person1自身上查找toString。——没有。 -
顺着
person1.__proto__找到Person.prototype,在它上面查找。——没有。 -
查找不会停止! 引擎会继续查找
Person.prototype这个对象自身的__proto__。
Person.prototype 本身也是一个普通对象,它的构造函数是 Object。所以,Person.prototype.__proto__ 指向的是 Object.prototype。
- 在
Object.prototype对象上查找toString。——找到了!JavaScript 内置的toString方法就在这里。于是执行它。
如果连 Object.prototype 上都找不到呢?那引擎就会去看 Object.prototype 的 __proto__,而 Object.prototype.__proto__ 是 null。
当查找到 null 时,整个查找过程结束,如果还没找到,就返回 undefined。
所以,原型链的终点是 null。
完整的查找链条就像这样:
person1 -> Person.prototype -> Object.prototype -> null
4. 梳理关系图与关键等式
为了彻底清晰,我们总结一下几个关键角色的关系:
-
构造函数 (Constructor):比如
Person函数。 -
原型对象 (Prototype Object):比如
Person.prototype。 -
实例 (Instance):比如
person1。
它们之间存在一个“黄金三角”关系:
-
person1.__proto__ === Person.prototype- 实例的
__proto__指向构造函数的prototype。
- 实例的
-
Person.prototype.constructor === Person- 原型对象上有一个
constructor属性,指回它自己的构造函数。这是一个“回程票”。
- 原型对象上有一个
这个关系图可以帮助你理解一切:
+----------------+ .prototype +--------------------+
| | -------------------> | |
| Person | | Person.prototype |
| (构造函数) | <------------------- | (原型对象) |
| | .constructor | |
+----------------+ +--------------------+
^ ^
| .constructor (间接) | .__proto__
| |
+-----|----------+ +--------|---------+
| | | |
| person1 | | person2 |
| (实例) | | (实例) |
| | | |
+----------------+ +------------------+
5. 现代 JavaScript 中的 class
你可能会说,现在写代码都用 class 了,还需要懂这些吗?
非常需要。 因为 ES6 的 class 语法,本质上就是原型继承的 语法糖 (Syntactic Sugar)。它的底层实现原理,完完全全就是我们上面讲的这一套。
我们用 class 重写上面的例子:
JavaScript
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
// 写在这里的方法,其实就是加在了 Person.prototype 上
sayHello() {
console.log("你好,我是" + this.name);
}
}
let person1 = new Person("张三", 30);
person1.sayHello();
// 底层原理完全一样
console.log(person1.__proto__ === Person.prototype); // 输出: true
console.log(typeof Person); // 输出: "function",证明 class 本质还是个函数
class 语法让代码看起来更像传统的面向对象语言,更清晰,更易于组织,但它并没有创造新的继承模型。理解原型链,才能真正理解 class 的行为,也才能在遇到复杂继承问题时,知道如何去调试和解决。
总结与核心观点
-
原型 (Prototype) 的本质目的:是为了 高效地共享属性和方法,解决 JavaScript 中对象创建的内存浪费问题。它是一种实现继承的机制。
-
两个关键属性:
-
prototype:函数独有,指向一个对象(原型对象),这个对象是未来由该函数创建的所有实例的“公共仓库”。 -
__proto__:对象独有,指向创建该对象的构造函数的prototype。它是连接实例和原型对象的桥梁。
-
-
原型链 (Prototype Chain) 的本质:一个 属性查找机制。当你访问一个对象的属性时,如果对象自身没有,它就会沿着
__proto__这条链一直向上查找,直到找到为止,或者到达链的终点null。 -
最终选择:在现代开发中,你应该优先使用
class语法。它更简洁、清晰,且不易出错。但是,你必须深刻理解其背后的原型和原型链机制,这能让你写出更健壮、更高性能的代码,并且在面对看似“诡异”的 bug 时,能够从容地从语言底层原理去分析和定位问题。不理解原型链而直接使用class,就像一个会开车但完全不懂发动机原理的司机,日常驾驶没问题,一旦车子出状况就束手无策。
我们来深入地拆解一下 Person.prototype.constructor === Person 这句代码,以及它背后“回程票”这个精妙的比喻。
为了彻底搞明白,我们不能只看这一个点,而是要从 JavaScript 创建对象的核心机制出发,一步步推导出这个结论。
第一步:理解两个关键角色:构造函数和原型对象
首先,我们有两个主角:
-
构造函数 (Constructor Function): 在这个例子里就是
Person。它本质上就是一个普通的 JavaScript 函数,但我们约定俗成地用new关键字来调用它,目的是为了创建特定类型的对象实例。JavaScript
function Person(name) { this.name = name; } -
原型对象 (Prototype Object): 在你定义
Person这个函数的时候,JavaScript 引擎会“偷偷地”帮你做一件事:创建一个与之配对的普通对象,并把它赋值给Person函数的一个特殊属性——prototype。这个对象,我们称之为
Person的原型对象。也就是Person.prototype。JavaScript
// 即使你只写了一行 function Person() {} // JS 引擎在背后已经帮你创建了 Person.prototype 这个对象 console.log(typeof Person.prototype); // "object"
所以,在一切开始之前,我们就有了这样一种关系:构造函数 Person 通过自身的 prototype 属性,单向指向了原型对象 Person.prototype。
.prototype
Person ---------------> Person.prototype
(构造函数) (原型对象)
第二步:建立“回程票” —— constructor 属性的诞生
光有单程票还不够。为了让关系更紧密,JavaScript 引擎在创建 Person.prototype 这个对象时,又多做了一件至关重要的事:
在这个原型对象(Person.prototype)上,自动添加一个名为 constructor 的属性,让它反过来指向构造函数 Person 本身。
这就是“回程票”的由来。
我们可以把这个关系可视化:
.prototype
Person <---------------> Person.prototype
(构造函数) (原型对象)
.constructor
现在,Person.prototype 这个原型对象,通过它的 constructor 属性,就能准确地“知道”自己是由哪个构造函数所创建和管理的。
所以,Person.prototype.constructor === Person 这句代码的结果为 true,就变得理所当然了。它验证的正是这条“回程”的链接是否建立。
“回程票”这个比喻的精妙之处在于:
-
出发点是
Person:我们从Person出发,通过.prototype这张“去程票”,到达了原型对象Person.prototype这个目的地。 -
目的地有回程信息:在
Person.prototype这个目的地,我们找到了一张名为.constructor的“回程票”。 -
回程票的目的地是出发点:这张回程票的目的地,恰好就是我们最初的出发点
Person。
第三步:这张“回程票”有什么用?
你可能会问,为什么要设计这么一个看似“绕圈子”的机制?这张“回程票”在实际开发中非常有用。
它的核心价值体现在 实例对象 上。
当我们创建一个实例时:
JavaScript
const p1 = new Person('Alice');
new 命令会做几件事,其中一件关键的就是:将实例 p1 的内部 [[Prototype]] 链接(在浏览器中常表现为 __proto__)指向构造函数的原型对象 Person.prototype。
p1.__proto__ === Person.prototype; // true
现在,我们来看整个图景:
// 构造函数与原型对象的双向链接
Person <---- .constructor ---- Person.prototype
^
| .__proto__
|
p1
(实例对象)
由于 p1 自身并没有 constructor 属性,当代码访问 p1.constructor 时,JavaScript 的原型链查找机制会启动:
-
在
p1自身上查找constructor。没找到。 -
沿着
p1的__proto__链接,找到Person.prototype。 -
在
Person.prototype上查找constructor。找到了!它的值是Person函数。 -
返回这个值。
所以,p1.constructor === Person 的结果也是 true。
这带来了两个非常强大的应用场景:
-
动态识别对象类型: 如果你只有一个实例
p1,你可以通过p1.constructor来得知它是由哪个构造函数创建的。这比instanceof在某些复杂场景下(例如跨窗口或iframe)更为可靠。JavaScript
console.log(p1.constructor.name); // "Person" -
基于现有实例创建新实例: 这是最能体现“回程票”价值的地方。假设你有一个对象,但不知道它的具体构造函数是什么,但你就是想创建一个和它同类型的新对象。
JavaScript
function createAnother(someInstance) { // 我不需要知道 someInstance 是 new Person() 还是 new Car() // 我只需要用它的 "回程票" (constructor) 就能创建新的同类实例 return new someInstance.constructor('new name'); } const p1 = new Person('Alice'); const p2 = createAnother(p1); // p2 是一个新的 Person 实例 console.log(p2 instanceof Person); // true console.log(p2.name); // "new name"
一个常见的陷阱:重写原型
需要特别注意的是,如果你用一个新对象完全覆盖了 prototype,那么这张“回程票”就会丢失。
JavaScript
function Car() {}
// 错误的做法:直接赋值一个新对象
Car.prototype = {
drive: function() {
console.log('Vroom!');
}
};
const myCar = new Car();
// 回程票丢失了!
console.log(Car.prototype.constructor === Car); // false
// 它指向了新对象的默认构造函数 Object
console.log(Car.prototype.constructor === Object); // true
// 实例也受到了影响
console.log(myCar.constructor === Car); // false
console.log(myCar.constructor === Object); // true
正确的做法是,在重写原型时,手动将“回程票”重新贴上:
JavaScript
function Bike() {}
Bike.prototype = {
constructor: Bike, // 手动修正 constructor 指向,把回程票贴回来!
ride: function() {
console.log('Riding...');
}
};
const myBike = new Bike();
console.log(Bike.prototype.constructor === Bike); // true
console.log(myBike.constructor === Bike); // true
总结
Person.prototype.constructor === Person 之所以成立,是因为 JavaScript 在创建函数 Person 时,会同步创建一个原型对象 Person.prototype,并在这个原型对象上设置一个 constructor 属性,让它指回到 Person 函数本身。
这个设计形成了一个闭环,它:
-
建立了构造函数和其原型对象之间的双向引用。
-
允许实例对象通过原型链访问到自己的构造函数。
-
提供了一种强大的、与具体类型解耦的编程范式,使得我们可以基于一个实例来创建它的同类。
“回程票”的比喻非常贴切地描绘了 constructor 属性的角色:它是一个从原型对象返回其构造函数的路径,是 JavaScript 原型体系中一个基础且优雅的设计。