通俗易懂的JS原型链

506 阅读5分钟

什么是原型

JS没有类的概念,所以设计了构造函数用于模拟类,生成具有相同属性、方法的对象。但如果只是生成,每一个新对象都需要分配一块新的内存存储对象中的方法object.assign、object.create、object.keys等等等...,极大的浪费资源。所以JS又为构造函数设计了一个prototype属性,它指向一个对象,用于存储实例对象的共享属性和方法。

规范的构造函数以大写字母开头,但实际上任何函数都可以当作构造函数(箭头函数除外)。所以我们知道了第一点:

函数都有一个prototype属性,指向一个对象,这个对象就是原型

通过在浏览器控制台打印,可以验证上面的说法:

constructor

同时我们还能看出原型对象中有一个constructor属性,从函数的名字我们就能看出constructor函数指向的就是构造函数自身,我们可以继续验证:

验证的同时我们还新生成了一个a构造函数的实例。可以发现实例e中是没有constructor属性的,但能够访问到,且同样指向构造函数本身,这也证实了实例对象能够共享原型对象中的属性和方法。我们得出第二点:

原型对象都有一个constructor属性,指向构造函数

什么是原型链

上面知道了实例对象能够共享构造函数原型对象的数据,这里称作儿子能够访问爸爸的数据。如果原型对象也是由另一个构造函数生成的,那理应能够访问另一个构造函数原型的数据,也就是爸爸能够访问爷爷的数据,所以爸爸理所当然也能把爷爷给他的数据再传给儿子。

构造函数的原型对象可能是另一个构造函数的实例,所以实例对象能够通过prototype属性逐层获取每一个原型对象的属性和方法,这就形成了原型链

通过观察上面打印的实例对象e,可以看到对象内只有一个[[prototype]]属性,以[[]]包裹的是JS的内置属性,不能手动操作。那我们该如何获取并操作原型链呢,JS给我们提供了__proto__属性

proto

对象的__proto__指向其构造函数的原型对象
Object.create(null)除外,这样创建的对象没有原型(什么都没有)

打印构造函数Object的原型Object.protptype可以看到有一个__proto__属性,它是一个getter、setter。因为JS中所有对象都是Object的实例(后面会讲到),所以普通对象能够通过原型链访问到__proto__

__proto__的实际操作就是:

  • 读取时调用Object.getPrototypeOf()
  • 修改时调用Object.setPrototypeOf()

因为构造函数的属性和方法都是定义在原型对象上的,所以就能通过链的形式层层获取数据。虽然构造函数说到底也是对象,也能添加属性,但构造函数上的属性,实例是获取不到的,可以测试一下:

所以我们也可以说:

原型链就是__proto__获取到的对象链

对象链的源头是null,即Object原型对象的__proto__指向null

有了上面的知识,我们对原型链的认识就已经比较深刻了,下面我们再来深入一下

JS对象

大家应该都听过,JS中万物皆对象。这是因为JS中一切类型(null、undefined除外)都是通过Object派生而来的(即Object的子类),我们可以通过__proto__验证:

即使是基础类型也能通过new Number()new String()的方式创建。实际中我们使用例如'abc'.toUpperCase()其实也是语言内部将字符串转换为对象,再调用String原型上的方法。

这里我们可以通过面向对象的方式思考下: JS首先开发了一个Object的父类,然后通过class String extends Object(){ toUpperCase(){...} }的方式生成了一个String子类。当我们声明一个字符串的时候,其实就是用String生成一个了一个实例,调用方法时就是通过实例的原型链找到String的原型对象,或者Object的原型对象中的方法并执行

但JS中的类型也不都是这么简单的,下面我们来看一段三角关系

ObjectFunction都是构造函数,按理来说是Function的实例(即用Function构造函数生成Object);而Object又是所有对象的父类,按理来说Function的原型应该是由Object生成的。这看起来有种先有鸡还是先有蛋的感觉

实际中JS也确实是按上面的道理设计的,我们不必纠结这混乱的关系,就当作为达成目的做的设计就行了。上面的关系参照下图:

模拟构造函数生成实例

  1. 声明函数

    • Function构造函数生成一个构造函数实例Foo
  2. 为构造函数添加原型对象

    • Object构造函数生成一个对象实例obj1
    • 将构造函数Fooprototype属性指向新对象实例obj1
  3. 通过new生成构造函数Foo的实例对象obj2

    • Object构造函数生成obj2
    • obj2__proto__指向obj1
    • Foo函数中的this绑定至obj2
    • 执行函数,为obj2添加自身属性(this.xxx = xxx;
    • obj2prototype指向Foo的原型对象obj1
    • 返回obj2(构造函数显示的返回对象的话,忽略obj2,因为没有变量引用obj2,下次垃圾回收时会被回收)
  4. 实例创建完成,现在obj2就能够访问obj1中的数据以及object原型对象中的数据

第三步其实就是new的内部逻辑,相信你现在稍加思考就能实现手写new操作符了。

总结

  1. 原型的目的是为了共享属性和方法,节约内存
  2. 原型就是为构造函数开辟的一个对象空间prototype,这些空间通过__proto__联接起来就形成了原型链,原型链的尽头是null
  3. __proto__是通过原型链从Object原型中读取的,本质是getter、setter
  4. 原型对象中有个constructor属性,指向构造函数;实例读取constructor就是读取实例原型中的属性