一张图让自己搞懂(mēng)原型 & 原型链
写在前面
这篇博客来源于,有天 mentor 突然传给我一张“祖传的图片”,并且发誓一定要给我讲清楚,然而在一番激情讲解之后,大家都被绕懵了……于是我决定把这张图背后的关系重新整理一遍。
原文适合作为历史入门材料,但其中把 __proto__、prototype 和 constructor 混在一起的说法容易造成误解。现代 ECMAScript 规范使用内部槽 [[Prototype]] 描述对象的原型;__proto__ 只是浏览器中普遍存在的遗留访问器,不应作为新代码的主要术语。
先上一张图再说

图片来源:原文 SegmentFault 图片资源,已下载为本地附件;图中的标识以本文的规范化说明为准。
prototype、[[Prototype]] 与 constructor
先区分三个容易混淆的概念:
[[Prototype]]:对象的内部原型槽。读取属性时,如果对象自身没有该属性,JavaScript 会继续沿着[[Prototype]]向上查找。prototype属性:普通函数(尤其是可作为构造函数的函数)通常拥有的一个普通属性。使用new Foo()创建实例时,实例的[[Prototype]]默认取Foo.prototype。constructor属性:通常存在于构造函数默认创建的prototype对象上,值指回构造函数。它只是一个普通属性,不是引擎保存的“真实构造来源”,也可以被覆盖。
下面用代码把它们分开:
function Foo(value) {
this.value = value
}
Foo.prototype.getValue = function () {
return this.value
}
const f1 = new Foo(1)
const f2 = new Foo(2)
console.log(Object.getPrototypeOf(f1) === Foo.prototype) // true
console.log(Object.getPrototypeOf(f2) === Foo.prototype) // true
console.log(Foo.prototype.constructor === Foo) // true
console.log(f1.getValue()) // 1
console.log(f2.getValue()) // 2
console.log(f1.getValue === f2.getValue) // true,共享同一个原型方法

因此,原文中的三角关系可以改写为:
Foo.prototype --作为实例的 [[Prototype]]--> f1 / f2
Foo.prototype --constructor 属性------------> Foo
f1 / f2 --[[Prototype]]----------------> Foo.prototype
实例对象和构造函数之间没有一个由规范保证的“直接指针”。实例能够通过 Object.getPrototypeOf(instance) 找到 Foo.prototype,而 Foo.prototype.constructor 只是原型对象上的约定属性。
__proto__ 与规范 API
历史代码常写成:
function Foo() {}
const f1 = new Foo()
f1.__proto__ === Foo.prototype // true(遗留访问器)
更推荐使用:
function Foo() {}
const f1 = new Foo()
const anotherPrototype = { kind: 'another' }
console.log(Object.getPrototypeOf(f1) === Foo.prototype) // true
Object.setPrototypeOf(f1, anotherPrototype) // 修改已有对象的原型,通常应谨慎使用
console.log(f1.kind) // another
对象字面量中的 { __proto__: value } 是另一种标准语法,它在创建对象时指定原型;这和 obj.__proto__ 遗留访问器不是同一件事:
const parent = { kind: 'parent' }
const child = { name: 'child', __proto__: parent }
console.log(Object.getPrototypeOf(child) === parent) // true
console.log(child.kind) // parent
频繁对已经创建的对象调用 Object.setPrototypeOf() 可能让引擎丢失已有优化。能在创建对象时确定原型,就不要在运行过程中反复修改。
Object、Function 与实例
原文说 Object 是“最基本的对象”,这个说法可以作为直觉,但需要把几个关系写准确:
const object = {}
console.log(Object.getPrototypeOf(object) === Object.prototype) // true
console.log(Object.getPrototypeOf(Object.prototype) === null) // true
console.log(Object.getPrototypeOf(Object) === Function.prototype) // true
console.log(Object.getPrototypeOf(Function) === Function.prototype) // true
console.log(Function.prototype.constructor === Function) // true
console.log(Object.getPrototypeOf(Function.prototype) === Object.prototype) // true
Object.prototype 的 [[Prototype]] 是 null,所以它是通常对象原型链的终点。Object 和 Function 本身也是函数对象,它们的 [[Prototype]] 通常是 Function.prototype;这并不意味着“所有对象都由手写的 new Object() 产生”。内置对象的创建过程由规范定义。

函数也是对象
函数同时具有两种身份:
- 它可以被调用,因此具有
[[Call]]能力; - 它也是对象,可以拥有属性和
[[Prototype]];部分函数还具有[[Construct]]能力,可以被new调用。
function normalFunction() {}
const arrowFunction = () => {}
const boundFunction = normalFunction.bind(null)
console.log(typeof normalFunction) // 'function'
console.log(Object.getPrototypeOf(normalFunction) === Function.prototype) // true
console.log(normalFunction.prototype) // 默认的实例原型对象
console.log(arrowFunction.prototype) // undefined:箭头函数没有默认 prototype 属性
console.log(boundFunction.prototype) // undefined:bound function 通常也没有 prototype 属性
这里要特别纠正一个常见误导:并不是所有函数都有自己的 prototype 属性。箭头函数、对象方法、类方法和绑定函数都可能没有可用于构造实例的 prototype 属性;能否使用 new 应以是否具有构造能力为准,而不是只看 typeof fn === 'function'。

原型链如何查找属性
当执行 obj.key 时,JavaScript 大致会:
- 先查看
obj自身的属性; - 找不到时查看
Object.getPrototypeOf(obj); - 继续向上查找,直到找到属性或遇到
null; - 全部找不到时返回
undefined。
const parent = {
value: 2,
method() {
return this.value + 1
},
}
const child = Object.create(parent)
console.log(child.method()) // 3,调用形式决定 this 为 child
child.value = 4 // 在 child 上创建自有属性,遮蔽 parent.value
console.log(child.method()) // 5
console.log('value' in child) // true:自身或原型链上存在
console.log(Object.hasOwn(child, 'value')) // true:自身存在
console.log(Object.hasOwn(parent, 'value')) // true
原型上的方法被继承对象调用时,this 通常是实际调用它的对象,而不是存放该方法的原型对象。这也是原型方法可以读取每个实例自己的属性的原因。

构造函数、类与原型方法
原文把 ES2015 class 说成“最理想的寄生组合式继承”并不准确。class 是更易读的构造和继承语法,实例方法仍然放在原型上;它并不等于所有语言中的类,也不保证某种绝对的性能优势。
class Animal {
constructor(name) {
this.name = name
}
speak() {
return `${this.name} makes a sound`
}
static category() {
return 'animal'
}
}
class Dog extends Animal {
speak() {
return `${this.name} barks`
}
}
const dog = new Dog('Lucky')
console.log(dog.speak()) // Lucky barks
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype) // true
console.log(Object.getPrototypeOf(Dog) === Animal) // true,静态继承关系
console.log(Dog.category()) // animal
使用传统构造函数也可以表达同样的实例原型关系:
function AnimalOld(name) {
this.name = name
}
AnimalOld.prototype.speak = function () {
return `${this.name} makes a sound`
}
function DogOld(name) {
AnimalOld.call(this, name)
}
Object.setPrototypeOf(DogOld.prototype, AnimalOld.prototype)
Object.setPrototypeOf(DogOld, AnimalOld)
const oldDog = new DogOld('Lucky')
console.log(oldDog.speak()) // Lucky makes a sound
实际项目中优先使用 class extends 或组合、委托等清晰的设计。手写继承适合学习原型关系,不是完整模拟 class 的 super、私有元素、派生构造器和 new.target 语义。
原型链的完整示意
普通构造函数创建的实例通常类似下面的链:
instance
↓ [[Prototype]]
Constructor.prototype
↓ [[Prototype]]
Object.prototype
↓ [[Prototype]]
null
而构造函数本身是函数对象:
Constructor
↓ [[Prototype]]
Function.prototype
↓ [[Prototype]]
Object.prototype
↓ [[Prototype]]
null
这两条链不要混为一谈:
Object.getPrototypeOf(instance) === Constructor.prototype讨论的是实例的原型;Object.getPrototypeOf(Constructor) === Function.prototype讨论的是构造函数对象本身的原型;Constructor.prototype是构造实例时使用的对象,不是Constructor自身的[[Prototype]]。
此外,Object.create(null) 可以创建没有 Object.prototype 的字典对象:
const dictionary = Object.create(null)
dictionary.key = 'value'
console.log(Object.getPrototypeOf(dictionary) === null) // true
console.log(dictionary.toString) // undefined
因此不能笼统地说所有对象的原型链最终都是 Object.prototype。也不要直接修改 Object.prototype 或 Array.prototype 来给项目增加“全局方法”,这会污染所有代码并带来命名冲突;只有明确的兼容性填充等少数场景才应考虑原型扩展。
小结
prototype是函数的一个属性,常用于指定new创建实例的原型;[[Prototype]]是每个对象的内部原型关系,推荐用Object.getPrototypeOf()观察;constructor通常是原型对象上的普通属性,可以被重写,不能当成不可伪造的类型标签;- 函数也是对象,但不是所有函数都有
prototype,也不是所有函数都能被new; - 属性访问会沿
[[Prototype]]链查找,直到null; class仍然建立在原型机制之上,但应使用现代语法表达意图;- 原文图片用于帮助建立直觉,判断具体行为时应以规范术语和可运行代码为准。