图解 js 原型链
参考 JavaScript 高级程序设计 (第四版)
参考 https://chamikakasun.medium.com/javascript-prototype-and-prototype-chain-explained-fdc2ec17dd04
# 一、为什么需要原型?
假设我们代码里需要新建一个 person 对象
let person = {}
person.name = 'Leo'
person.age = 20
person.eat = function () {
console.log(`${this.name} is eating.`)
}
person.sleep = function () {
console.log(`${this.name} is sleeping.`)
}
2
3
4
5
6
7
8
9
看起来没啥问题
假设我们需要多个 person 对象呢? 你可能想到了,用工厂函数
function createPerson(name, age) {
let person = {}
person.name = name
person.age = age
person.eat = function () {
console.log(`${this.name} is eating.`)
}
person.sleep = function () {
console.log(`${this.name} is sleeping.`)
}
return person
}
// 工厂函数批量生成person对象
const person1 = createPerson('Mike', 23)
const person2 = createPerson('Alis', 34)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
工厂函数解决了批量生成的问题,但是如果业务代码中需要对同一类 person 做某种操作,问题出现了
不能判断 person1 和 person2 同一种类型,因为 person1 和 person2 没有任何关联。
或者你也可以用构造函数模式
function Person(name, age) {
this.name = name
this.age = age
this.eat = function () {
console.log(`${this.name} is eating.`)
}
this.sleep = function () {
console.log(`${this.name} is sleeping.`)
}
}
const person1 = new Person('Mike', 23)
const person2 = new Person('Alis', 34)
2
3
4
5
6
7
8
9
10
11
12
13
Person()构造函数代替了 createPerson()工厂函数。实际上,Person()内部 的代码跟 createPerson()基本是一样的,只是有如下区别。
- 没有显式地创建对象。
- 属性和方法直接赋值给了 this。
- 没有 return。
而反过来 这三点正是 new 关键字所做的事。
使用 new 操作符调用构造函数会将新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性
这步操作就标识了 person 实例对象类型为 Person
person1 instanceof Person // true
person2 instanceof Person // true
2
标识问题完美解决了!
构造函数虽然有用,但也不是没有问题。它主要问题在于,其定义的方法会在每个实例上 都创建一遍。
如果需要创建几万个 person,那么将会创建几万个 sleep 和 eat 方法。 😵💫😵😵💫
# 二、原型模式
为了解决上述问题,Javascript 引入了原型模式。
每个函数都会创建 prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例 共享的属性和方法。
原来在构造函数中直接赋给对象实例的值或方法,可以直接赋值给它们的原型
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.eat = function () {
console.log(`${this.name} is eating.`)
}
Person.prototype.sleep = function () {
console.log(`${this.name} is sleeping.`)
}
let person1 = new Person()
let person2 = new Person()
2
3
4
5
6
7
8
9
10
11
12
13
14
这样 person 的原型定义的属性和方法可以被对象实例共享,不管创建多少个 person 对象, sleep 和 eat 方法永远只创建一次。
console.log(person1.eat == person2.eat) // true
console.log(person1.sleep == person2.sleep) // true
2
生成的实例是怎么和 Person 构造函数关联呢? ‘__proto__’
这里可以看到 chrome devtool 里 显示的是 [[Prototype]], proto 是一个获取 [[Prototype]] 内部插槽值的 getter/setter,出于兼容性原因而存在。
引入 prototype 后, prototype、构造函数 Person、实例对象 person 之间的关系如下图:
为了让多个实例的情况更加清晰,我们 new 两个对象
let Alex = new Person('Alex', 24)
let Bob = new Person('Bob', 25)
2
他们的原型关系图如下
我们回顾一下原型模式做了些什么:
当我们创建一个函数时,JavaSctipt 引擎会自动为该函数创建一个 prototype 对象,并将原型属性添加到可用于访问该 Prototype 对象的 Function 对象,并添加指向 Function 对象的构造函数属性 constructor。
然后当我们从 Function 对象创建实例时,JavaScript 引擎再次将 getter/setter(即 __proto__)添加到对象的实例中,可用于访问 Function 对象的相同原型对象。
构造函数的这个 prototype 对象在使用构造函数创建的所有对象之间共享。 我们可以将方法和属性添加到这个原型对象中,然后这些方法和属性将自动可供其构造函数的实例使用。
# 三、原型链
理清楚 Person 原型后, 我们来看一个问题
Person.prototype 的 __proto__ 又指向哪里呢?
答案是 Object.prototype : JavaScript 内置对象 Object 的原型对象
那 Object.prototype.__proto__ 又指向哪里呢? null
为了了解其原因;首先,我们需要了解 JavaScript 中的对象查找行为。
当我们查找对象的属性(包括函数声明)时,JavaScript 引擎将首先检查对象本身是否存在该属性。
如果没有找到,它将转到对象的原型对象并检查该对象。如果找到,它将使用该属性。
如果在该对象中没有找到它,那么它将查找该原型对象的原型对象,如果在那里找到它,它将使用该属性,否则查找将继续,直到它找到一个 __proto__ 属性等于 null 的对象。
那是 JavaScript 内置对象的原型对象。这里它设置为 null 以终止原型查找链。这称为原型链。
这就是为什么我们在任何原型类型链对象中看到未定义的值的原因。它不应该与函数的 func.prototype 属性混淆,而是指定要分配给的 [[Prototype]]当用作构造函数时,由给定函数创建的对象的所有实例。
我们加入 null 后重新画一张原型链图
其他内置对象(如 Array、Date、Function 等)也具有关联的原型,并且具有特定于该类型的所有附加方法。
例如,如果我们创建一个简单的数字数组 [1, 2, 3],默认的 new Array() 构造函数会在内部被 JavaScript 引擎从幕后调用。
JavaScript 还会添加一个 proto 指针,指向新创建的数组实例,该实例指向数组。
原型(即 Array.prototype)对象,它具有与 Array 操作相关的所有方法,例如 concat、reduce、map、forEach 等等。
我们在调用数组的这些方法时,实际是调用的 Array.prototype 上的公共的操作方法。
可以注意到 Array.prototype.__proto__依然指向 Object.prototype
我们来看一下加入 array 后的原型链
其它内置对象的工作原理也和 Array 类似, 比如 Function, Date 等等
简单数据类型呢?
我们知道简单数据不是 JavaScript 中的对象,但是当我们尝试访问它们的属性时,JavaScript 引擎会自动创建一个临时包装对象,使用内置的构造函数创建,例如 String、Number、Boolean、Symbol(ES6),除了空且未定义。这些将提供与原语一起使用的其他方法。
值 null 和 undefined 没有对象包装器。由于它们没有对象包装器,因此它们不会有额外的方法和属性,而且它们也没有任何关联的原型对象。
# 四、总结
我们已经了解了不少关于 JavaScript 原型和原型链的知识,现在我们把这些知识串在一起放到一张大图中来回顾一下。