冴羽 Blog JavaScript 深入系列:从原型到原型链——prototype、\_\_proto\_\_ 与 constructor 全解析
冴羽 Blog JavaScript 深入系列:从原型到原型链——prototype、__proto__ 与 constructor 全解析
本指南源自冴羽 Blog 仓库中 JavaScript深入之从原型到原型链 一文,是该「JavaScript 深入系列」的开篇。它以构造函数、实例、原型三者之间的关系为主线,逐层回答「实例的原型是什么、原型的原型是什么、原型链的终点在哪里」这一连串问题。读完本篇,你将彻底掌握
prototype、__proto__、constructor三个核心属性的指向关系,理解属性查找的委托机制,并为后续学习new的模拟实现、闭包、继承等底层知识打下坚实基础。
构造函数创建对象
我们先使用构造函数创建一个对象:
function Person() {
}
var person = new Person();
person.name = 'Kevin';
console.log(person.name) // Kevin
在这个例子中,Person 就是一个构造函数,我们使用 new 创建了一个实例对象 person。new 在这里到底做了什么,我们会在系列第十二篇 JavaScript深入之new的模拟实现 中详细拆解,但有一点从现在就值得记住:new 创建出来的实例,会和构造函数的 prototype 建立关联,这正是原型与原型链的起点。
prototype:函数特有的属性
每个函数都有一个 prototype 属性,也就是我们在各种例子中经常见到的那个 prototype:
function Person() {
}
// 虽然写在注释里,但是你要注意:
// prototype是函数才会有的属性
Person.prototype.name = 'Kevin';
var person1 = new Person();
var person2 = new Person();
console.log(person1.name) // Kevin
console.log(person2.name) // Kevin
那这个函数的 prototype 属性到底指向的是什么呢?是这个函数的原型吗?
其实,函数的 prototype 属性指向了一个对象,这个对象正是调用该构造函数而创建的实例的原型,也就是这个例子中的 person1 和 person2 的原型。
那什么是原型呢?可以这样理解:每一个 JavaScript 对象(null 除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型“继承”属性。
在上图中,我们用 Person.prototype 表示实例原型:构造函数 Person 通过 prototype 属性指向它,而 person1、person2 这两个实例都与它关联。那么我们该怎么表示实例与实例原型(person 与 Person.prototype)之间的关系呢?这就要讲到第二个属性:
__proto__:实例指向原型的桥梁
每一个 JavaScript 对象(除了 null)都具有一个属性 __proto__,这个属性会指向该对象的原型。
为了证明这一点,我们可以在火狐或者谷歌浏览器中输入:
function Person() {
}
var person = new Person();
console.log(person.__proto__ === Person.prototype); // true
于是我们更新下关系图:
从图中可以清晰地看到:构造函数 Person 通过 prototype 指向实例原型 Person.prototype,实例 person 通过 __proto__ 也指向 Person.prototype,两者殊途同归。
既然实例对象和构造函数都可以指向原型,那么原型是否有属性指向构造函数或者实例呢?
constructor:原型指回构造函数
指向实例倒是没有,因为一个构造函数可以生成多个实例,但是原型指向构造函数倒是有的,这就要讲到第三个属性:constructor——每个原型都有一个 constructor 属性指向关联的构造函数。
为了验证这一点,我们可以尝试:
function Person() {
}
console.log(Person === Person.prototype.constructor); // true
所以再更新下关系图:
至此,构造函数、实例原型、实例三者之间的三角关系已经完整建立。我们把前面学到的三条规则汇总成一段可验证的代码:
function Person() {
}
var person = new Person();
console.log(person.__proto__ == Person.prototype) // true
console.log(Person.prototype.constructor == Person) // true
// 顺便学习一个ES5的方法,可以获得对象的原型
console.log(Object.getPrototypeOf(person) === Person.prototype) // true
这里额外补充一个 ES5 标准方法 Object.getPrototypeOf(obj),它与 __proto__ 等价,但它是规范定义的正式 API,更推荐在生产代码中使用。
实例与原型:属性查找的委托机制
了解了构造函数、实例原型和实例之间的关系,接下来我们讲讲实例和原型的关系:
当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。
举个例子:
function Person() {
}
Person.prototype.name = 'Kevin';
var person = new Person();
person.name = 'Daisy';
console.log(person.name) // Daisy
delete person.name;
console.log(person.name) // Kevin
在这个例子中,我们给实例对象 person 添加了 name 属性,打印 person.name 时,实例自有属性优先,结果自然为 Daisy。
但是当我们删除了 person 的 name 属性时,读取 person.name,从 person 对象中找不到 name 属性,就会从 person 的原型——也就是 person.__proto__,即 Person.prototype 中查找,幸运的是我们在原型上找到了 name 属性,结果为 Kevin。
注意这里的措辞:实例并没有复制原型的属性,它只是在自身找不到属性时,沿着原型向上「委托」查找。关于这种「委托」而非「复制」的语义,我们在后文「真的是继承吗」一节中会进一步展开。
原型的原型:原型本身也是对象
万一实例自身和原型上都找不到属性呢?原型的原型又是什么呢?
在前面,我们已经讲了原型也是一个对象,既然是对象,我们就可以用最原始的方式创建它,那就是:
var obj = new Object();
obj.name = 'Kevin'
console.log(obj.name) // Kevin
所以原型对象是通过 Object 构造函数生成的。结合之前所讲——实例的 __proto__ 指向构造函数的 prototype——那么 Person.prototype 作为 Object 构造函数的实例,它的 __proto__ 自然指向 Object.prototype。我们更新下关系图:
从图中可以看到,原型链第一次出现了向下的延伸:Person.prototype 通过 __proto__ 指向 Object.prototype,而 Object 与 Object.prototype 之间同样存在 prototype / constructor 的双向关联。
原型链:以 null 为终点
那 Object.prototype 的原型呢?
是 null,我们可以打印验证:
console.log(Object.prototype.__proto__ === null) // true
然而 null 究竟代表了什么呢?
null 表示“没有对象”,即该处不应该有值。所以 Object.prototype.__proto__ 的值为 null 与 Object.prototype 没有原型,其实表达的是一个意思。
所以查找属性的时候查到 Object.prototype 就可以停止查找了。最后一张关系图也可以更新为:
顺便还要说一下,图中由相互关联的原型组成的链状结构就是原型链,也就是图中蓝色的这条线:从实例 person 出发,经由 Person.prototype、Object.prototype,最终终止于 null。
补充:三个容易被忽视的细节
最后,补充三点大家可能不会注意的地方:
constructor
首先是 constructor 属性,我们看个例子:
function Person() {
}
var person = new Person();
console.log(person.constructor === Person); // true
当获取 person.constructor 时,其实 person 中并没有 constructor 属性,当不能读取到 constructor 属性时,会从 person 的原型也就是 Person.prototype 中读取,正好原型中有该属性,所以:
person.constructor === Person.prototype.constructor
也就是说,person.constructor 之所以能访问到,靠的正是前文讲的「属性查找委托」机制,而不是实例自己拥有 constructor。
这个细节在实际开发中有一个重要推论:如果你用对象字面量整体重写 Person.prototype(例如 Person.prototype = { ... }),新原型对象将不再自动拥有 constructor 属性,此时 person.constructor 会继续沿着原型链找到 Object.prototype.constructor,导致指向 Object 而非 Person。关于这个陷阱的完整讨论,可以阅读同仓库的 JavaScript深入之创建对象的多种方式以及优缺点,其中「原型模式优化」一节专门演示了如何通过手动补回 constructor: Person 来修复。
__proto__
其次是 __proto__,绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于 Person.prototype 中,实际上,它是来自于 Object.prototype。与其说是一个属性,不如说是一个 getter/setter:当使用 obj.__proto__ 时,可以理解成返回了 Object.getPrototypeOf(obj)。
正因为它只是一个访问器,所以严格意义上它是「读取/设置对象原型」的入口,而真正的原型关系由引擎内部维护。这也是为什么 ES5 规范提供了标准的 Object.getPrototypeOf 和 Object.setPrototypeOf,在正式代码中应当优先使用它们。
真的是继承吗?
最后是关于继承。前面我们讲到“每一个对象都会从原型‘继承’属性”,实际上,继承是一个十分具有迷惑性的说法。继承意味着复制操作,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数。所以与其叫继承,委托的说法反而更准确些。
这个视角贯穿了整个系列:在 JavaScript深入之new的模拟实现 中,objectFactory 的核心步骤之一就是 obj.__proto__ = Constructor.prototype——它做的正是「建立关联」而非「复制属性」;而在 JavaScript深入之继承的多种方式和优缺点 中,各种继承模式的本质,也无非是围绕这条原型链做不同的关联与增强。
总结:一条链,三个属性,一次遍历
把本篇的全部知识点压缩成一句话:每个函数都有 prototype(指向实例原型),每个对象都有 __proto__(指向自己的原型),每个原型都有 constructor(指回构造函数);属性查找沿着 __proto__ 逐级委托,直到 Object.prototype.__proto__ === null 为止,这条由相互关联的原型组成的链就是原型链。
从源码验证的角度,这套关系并非只是理论推演:本仓库 Images 目录下保存了本篇配套的五张关系图(prototype1.png 至 prototype5.png),完整记录了三者关系从单点指向到完整链路的演化过程;而系列后续文章如 JavaScript深入之new的模拟实现、JavaScript深入之创建对象的多种方式以及优缺点、JavaScript深入之继承的多种方式和优缺点、JavaScript深入之闭包 均以本篇为地基,反复运用 __proto__ 指向与作用域链委托的思想。理解了原型链,你就拿到了读懂整个 JavaScript 对象系统的钥匙。
延伸阅读
- 下一篇:JavaScript深入之词法作用域和动态作用域
- JavaScript深入之new的模拟实现:从
obj.__proto__ = Constructor.prototype出发,模拟new的完整行为 - JavaScript深入之创建对象的多种方式以及优缺点:工厂、构造函数、原型、组合等模式的权衡
- JavaScript深入之继承的多种方式和优缺点:基于原型链的各种继承范式的演进
- JavaScript深入之作用域链:与原型链并列的另一条「链」——作用域链




