原型链

Posted by violetks on March 2, 2023

一、原型(★)

1、函数中 prototype 指向原型对象

当我们创建一个函数时,函数都会有一个默认属性prototype,该属性指向一个原型对象

function fun() {}
fun.prototype // 原型对象

原型链1.webp

2、对象中 __proto__ 指向原型对象

(1)当函数作为普通函数进行调用时,该属性不会有任何作用。
(2)当函数作为构造函数进行调用时,构建出来的实例对象会有一个属性__proto__指向原型对象

function fun(name) {
  this.name = name;
}

fun.prototype // 原型对象

const obj = new fun('张三') // 调用构造函数创建一个实例对象 obj
obj.__proto__ === fun.prototype // true,实例对象.__proto__ 指向 构造函数.prototype

原型链2.webp

3、原型对象中 constructor 指向构造函数

原型对象默认会有一个特殊的属性constructor,该属性又指向了函数本身。

function fun(name) {
  this.name = name;
}

fun.prototype // 原型对象

const obj = new fun('张三') // 调用构造函数创建一个实例对象 obj
obj.__proto__ === fun.prototype // true,实例对象.__proto__ 指向 构造函数.prototype

fun.prototype.constructor === fun // true

原型链3.webp

4、__proto__ 与 [[Prototype]]

实例对象中没有__proto__属性,但是有[[Prototype]]属性。

原型链4.png

__proto__[[Prototype]]的关系:
(1)__proto__并不是 ECMAScript 语法规范的标准,它只是大部分浏览器厂商实现或说是支持的一个属性,通过该属性方便我们访问、修改原型对象
(2)遵循 ECMAScript 标准,[[Prototype]]才是正统,[[Prototype]]无法被直接修改、引用。
(3)从 ECMAScript 6 开始,可通过Object.getPrototypeOf()Object.setPrototypeOf()来访问、修改原型对象
(4)简单理解:__proto__[[Prototype]]是同一个东西,__proto__是非标准的,[[Prototype]]才是标准的,但是它们都是指向原型对象
(5)实际上,我们访问的__proto__是被添加在Object.prototype上,然后通过原型链我们就能够访问到该属性。

5、所有非空类型数据,都具有原型对象

任何非空数据,本质上都是通过对应构造函数构建出来的,它们都具有__proto__属性,指向构造函数原型对象
所以要判断某个值的原型对象,只需要确认该值是通过哪个构造函数构建的即可,只要确认了构造函数那么该值的__proto__必然指向该构造函数prototype

// 数字
const num = 1
// 数字是通过 Number 构建的,那么其原型对象等于 Number.prototype
num.__proto__ === Number.prototype // true

// 字符串
const str = 'str'
// 字符串是通过 String 构建的,那么其原型对象等于 String.prototype
str.__proto__ === String.prototype // true

// 布尔类型
const bool = false
// 布尔值是通过 Boolean 构建的,那么其原型对象等于 Boolean.prototype
bool.__proto__ === Boolean.prototype // true

// Symbol
const sym = Symbol('symbol')
// sym 是通过 Symbol 构建的,那么其原型对象等于 Symbol.prototype
sym.__proto__ === Symbol.prototype // true

// BigInt
const big = BigInt(1)
// big 是通过 BigInt 构建的,那么其原型对象等于 BigInt.prototype
big.__proto__ === BigInt.prototype // true

// 对象
const obj = { age: 18 }
// 对象是通过 Object 构建的,那么其原型对象等于 Object.prototype
obj.__proto__ === Object.prototype // true

// 函数
const fun = () => {}
// 函数是通过 Function 构建的,那么其原型对象等于 Function.prototype
fun.__proto__ === Function.prototype // true

// 数组
const arr = [1, 2, 3]
// 数组是通过 Array 构建的,那么其原型对象等于 Array.prototype
arr.__proto__ === Array.prototype // true

6、补充:new 运算符做了哪些事情

(1)创建一个新的空对象 A。
(2)往空对象挂载构造函数 Com原型对象:对象 A 创建__proto__属性,并将构造函数prototype属性赋值给__proto__
(3)执行构造函数 Com:改变构造函数 this 指向,指向空对象 A,并执行构造函数,往空对象注入属性。
(4)判断构造函数是否返回一个对象。如果构造函数也返回了一个对象 B,则最终 new 出来的对象则为返回的对象 B;否则最终 new 出来的对象为最初创建的对象 A。
因此当我们执行:

var o = new Foo();

实际上执行的是:

// 1. 创建一个新的空对象 A
let A = {};

// 2. 往空对象挂载,挂载构造函数 Com 的原型对象: obj.__proto__ === Com.prototype;
Object.setPrototypeOf(A, Com.prototype);

// 3. 执行构造函数: 改变构造函数 this 指向,指向对象 A,往 A 注入属性
let B = Com.apply(A, args);

// 4. 判断构造函数是否返回对象: 是则取返回值、否则取最初创建的对象 A
const newObj = B instanceof Object ? res : A;

二、原型链(★)

根据上文,所有非空数据,都可以通过__proto__指向原型对象,故而如果原型对象非空,那么必然会有__proto__指向它自己的原型对象,如此一层层往上追溯,以此类推,就形成了一整条链路,一直到某个原型对象为 null,才到达最后一个链路的最后环节,而原型对象之间这种链路关系被称之为原型链(prototype chain)。

1、几个例子

(1)直接创建一个对象。

const obj = { age: 18 }

从对象obj视角来看:
obj本质上是通过Object构建出来的,那么obj.__proto__等于Object.prototype
Object.prototype原型对象为 null,原型链到此结束。

原型链5.webp

从数据上来看(看[[Prototype]]):
原型链6.webp

(2)数字、字符串、数组等类型数据,下面以数字为例,其他类型大同小异。

const num = 1

num视角来看:
num本质上是通过Number构建出来的,那么num.__proto__等于Number.prototype
Number.prototype本质上是个对象,是通过Object构建出来了,那么Number.prototype.__proto__等于Object.prototype
Object.prototype原型对象为 null,原型链到此结束。

原型链7.webp

从数据上来看(看[[Prototype]]):
原型链8.webp

(3)一个复杂的例子。

function Person(age) {
  this.age = age
}
var person = new Person(100)

从对象person视角来看:
person是通过Person构建出来的,那么person.__proto__等于Person.prototype
Person.prototype是个对象,是通过Object构建出来了,那么Person.prototype.__proto__等于Object.prototype
Object.prototype原型对象为 null,原型链到此结束。

原型链9.webp

从构造函数Person视角来看:
Person本质上是个函数,是通过Function构建出来的,那么Person.__proto__等于Function.prototype
Function.prototype本质上是个对象,是通过Object构建出来了,那么Function.prototype.__proto__等于Object.prototype
Object.prototype原型对象为 null,原型链到此结束。

原型链10.webp

同时,构造函数Object又是Function构建出来的,那么如果从构造函数Object视角来看:
Object本质上是个函数,是通过Function构建出来的,那么Object.__proto__等于Function.prototype
Function.prototype本质上是个对象,是通过Object构建出来了,那么Function.prototype.__proto__等于Object.prototype
Object.prototype原型对象为 null,原型链到此结束。

原型链11.webp

再有,构造函数Function是个函数,它自己构建了自己,那么从构造函数Function的视角来看:
Function是个函数,是通过自己构建出来的,那么Function.__proto__等于Function.prototype
Function.prototype本质上是个对象,是通过Object构建出来了,那么Function.prototype.__proto__等于Object.prototype
Object.prototype原型对象为 null,原型链到此结束。

原型链12.webp

(4)总结:所有原型链最后都会到Object.prototype,因为原型对象本质上就是个对象,由Object进行创建,其 __proto__指向Object.prototypeObject.prototype.__proto__等于 null,所以原型链的终点必然是:Object.prototype => null

2、原型链的作用

(1)查找属性:当我们试图访问对象属性时,它会先在当前对象上进行搜寻,搜寻没有结果时会继续搜寻该对象的原型对象,以及该对象的原型对象原型对象,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。

function Person(age) {
  this.age = age
}

Person.prototype.name = 'klx'
Person.prototype.age = 18

const person = new Person(28)

person // 当前对象: { age: 28 }

person.name // klx,取自原型对象 Person.prototype
person.age // 28,取自当前对象

person.toString() // [object Object],取自原型对象 Object.prototype

person.address // undefined,沿着原型链找不到 address

根据代码,得到如下简化的原型链示意图,在访问person属性时,是按照下图链路一层层往下搜寻。

原型链13.webp

(2)属性屏蔽:原型对象中的属性,如果在实例对象中重新声明,根据属性查找规则,在查找该属性时会直接返回实例中声明的值。这时原型对象中的属性可以简单理解为被屏蔽了,在很多文章中称该现象为属性覆盖,但个人认为说覆盖是不准确的,因为原型对象中属性并没有被覆盖,用屏蔽或许更为准确。
如下代码,在实例对象p2中,屏蔽了原型对象Person.prototypename属性。

function Person() {}

Person.prototype.name = '张三'
Person.prototype.age = 18

const p1 = new Person()
const p2 = new Person()
p2.name = '李四' // p2 声明 name 属性,屏蔽原型对象 Person.prototype 中 name 属性

p1.name // 张三,取自原型对象 Person.prototype
p2.name // 李四,取自实例对象

(3)原型对象中的函数被调用时,this 指向是当前对象,而不是函数所在的原型对象

// 1. 调用「普通对象」中的方法
const obj = {
  a: 10,
  name: {
    a: 1,
    printA: function(){
      console.log(this.a + 1)
    }
  }
}
obj.name.printA() // 2,printA 函数 this 指向函数所在的对象

// 2. 调用「原型对象」中的方法
function Person() {
  this.a = 10
}

Person.prototype.a = 1
Person.prototype.printA = function(){
  console.log(this.a + 1)
}

const person = new Person()

person.printA() // 11,printA 函数 this 指向当前对象(而不是函数所在的原型对象)

三、总结(★)

每个构造函数都有一个原型对象原型有一个属性指回构造函数,而实例有一个内部指针指向原型。如果原型是另一个类型的实例呢?那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链
1、所有函数都有一个属性prototype指向原型对象(所有函数都有原型对象)。
2、所有原型对象都有一个constructor属性,指向原型对象所属的函数。
3、所有非空数据都有__proto__指向其原型对象
4、要判断一个数据的原型对象,只需要确认该数据是通过哪个构造函数构建出来的,那么这个数据的原型对象等于构造函数的原型对象。
5、所有原型链的终点都是Object.prototype => null
6、原型、原型链的优点:为同类型对象提供共享属性、将通用属性抽离大大节约内存。