技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

一张图让自己搞懂(mēng)原型 & 原型链

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/70-一张图让自己搞懂(mēng)原型&原型链
本文目录10 个章节
  1. 写在前面
  2. 先上一张图再说
  3. prototype、[[Prototype]] 与 constructor
  4. Object、Function 与实例
  5. 函数也是对象
  6. 原型链如何查找属性
  7. 构造函数、类与原型方法
  8. 原型链的完整示意
  9. 小结
  10. 参考资料

一张图让自己搞懂(mēng)原型 & 原型链

写在前面

这篇博客来源于,有天 mentor 突然传给我一张“祖传的图片”,并且发誓一定要给我讲清楚,然而在一番激情讲解之后,大家都被绕懵了……于是我决定把这张图背后的关系重新整理一遍。

原文适合作为历史入门材料,但其中把 __proto__prototypeconstructor 混在一起的说法容易造成误解。现代 ECMAScript 规范使用内部槽 [[Prototype]] 描述对象的原型;__proto__ 只是浏览器中普遍存在的遗留访问器,不应作为新代码的主要术语。

先上一张图再说

原文的原型链关系图

图片来源:原文 SegmentFault 图片资源,已下载为本地附件;图中的标识以本文的规范化说明为准。

prototype[[Prototype]]constructor

先区分三个容易混淆的概念:

  1. [[Prototype]]:对象的内部原型槽。读取属性时,如果对象自身没有该属性,JavaScript 会继续沿着 [[Prototype]] 向上查找。
  2. prototype 属性:普通函数(尤其是可作为构造函数的函数)通常拥有的一个普通属性。使用 new Foo() 创建实例时,实例的 [[Prototype]] 默认取 Foo.prototype
  3. 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,共享同一个原型方法

prototype、实例与原型对象的关系

因此,原文中的三角关系可以改写为:

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() 可能让引擎丢失已有优化。能在创建对象时确定原型,就不要在运行过程中反复修改。

ObjectFunction 与实例

原文说 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,所以它是通常对象原型链的终点。ObjectFunction 本身也是函数对象,它们的 [[Prototype]] 通常是 Function.prototype;这并不意味着“所有对象都由手写的 new Object() 产生”。内置对象的创建过程由规范定义。

Object、Function 与原型链的关系

函数也是对象

函数同时具有两种身份:

  • 它可以被调用,因此具有 [[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'

函数对象与 Function.prototype

原型链如何查找属性

当执行 obj.key 时,JavaScript 大致会:

  1. 先查看 obj 自身的属性;
  2. 找不到时查看 Object.getPrototypeOf(obj)
  3. 继续向上查找,直到找到属性或遇到 null
  4. 全部找不到时返回 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 或组合、委托等清晰的设计。手写继承适合学习原型关系,不是完整模拟 classsuper、私有元素、派生构造器和 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.prototypeArray.prototype 来给项目增加“全局方法”,这会污染所有代码并带来命名冲突;只有明确的兼容性填充等少数场景才应考虑原型扩展。

小结

  • prototype 是函数的一个属性,常用于指定 new 创建实例的原型;
  • [[Prototype]] 是每个对象的内部原型关系,推荐用 Object.getPrototypeOf() 观察;
  • constructor 通常是原型对象上的普通属性,可以被重写,不能当成不可伪造的类型标签;
  • 函数也是对象,但不是所有函数都有 prototype,也不是所有函数都能被 new
  • 属性访问会沿 [[Prototype]] 链查找,直到 null
  • class 仍然建立在原型机制之上,但应使用现代语法表达意图;
  • 原文图片用于帮助建立直觉,判断具体行为时应以规范术语和可运行代码为准。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS