由一张图来理解javascript中的原型和原型链

362 阅读5分钟

又到了金三银四的季节,各路码农又琢磨着该跳槽涨工资了,对前端的同学来说,面试时原型和原型链相关的知识几乎是必问的。不少新人甚至是工作一段时间的同学,对于这方面的知识了解还是不够甚至不怎么了解,如果你是这样的状态,那么看了这篇文章希望你能更深入的了解原型和原型链,如果有说得不对的地方,还忘各位大佬不吝赐教。

首先祭上这张经典的解释javascript原型和原型链的图,出处已不可考,但这不是本文重点。

我们可以从以下几点来分析原型与原型链

构造函数

从图片上部分开始分析,我们首先定义一个构造函数,并创建它的实例

function Foo() {}
var f1 = new Foo()

此时发生了几件事:

  1. 定义了一个函数,定义函数时会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向一个对象,即函数的原型,这个对象也会默认带有一个constructor属性,指向函数本身。
  2. 通过new操作符调用这个函数,并返回一个对象赋值给f1,f1就是Foo构造函数的实例,f1是一个对象,在javascript中,所有的对象创建时都会被添加上一个内部属性[[prototype]]属性,目前在大部分浏览器中都可以通过__proto__属性访问[[prototype]](在此处__proto__可视为与[[prototype]]是同一个东西),ES5新增了一个Object.getPrototypeOf可直接访问这个属性。
  3. f1对象的[[prototype]]指向了创建f1的构造函数Foo的原型属性Foo.prototype,这个属性连接的是f1和构造函数的原型,而不是连接了构造函数本身,我们可以通过以下方式验证。
f1.__proto__ === Foo.prototype // true
Object.getPrototypeOf(f1) === Foo.prototype // true

我们常说的实例可以调用构造函数的原型上的方法,就是因为[[prototype]]属性的存在。当实例f1访问自身身上不存在的属性或方法时,会尝试通过[[prototype]]属性去访问创建它的构造函数Foo的原型上即Foo.prototype是否存在对应的属性。我们可以开到上图的最上面的线,f1__proto__ -> Foo.prototype。

原型链

我们同样可以看到,如果当f1通过__proto__访问Foo.prototype后,发现Foo.prototype还是没有找到对应的属性或方法,那么此时f1还会继续查找。

别忘了Foo.prototype也是一个对象,既然是对象那么就是Object的实例,那么Foo.prototype也就自然的有一个[[prototype]]属性指向Object的原型了,可以用一下方式验证。

Foo.prototype.__proto__ === Object.prototype // true
Object.getPrototypeOf(Foo.prototype) === Object.prototype // true

所以,当f1通过__proto__访问Foo.prototype后发现找不到想要的属性或方法,那么会继续根据Foo.prototype.__proto__访问Object.prototype,如果找到了会访问该属性或调用该方法,且会停止查找。如果还是没有找到,根据图上来看,最后会查找到null。

f1 -> ___proto___ -> Foo.prototype -> __proto__ -> Object.prototype -> __proto__ -> null

这样看着像一条线的逐级查找,且是通过函数的原型来查找,这就是所谓的原型链。

为了验证原型链真的存在,我们还可以举个例子

f1.toString() // [object Object]
Object.prototype.toString.call(f1) // [object Object]
f1.toString === Object.prototype.toString // true

很明显,我们并没有在f1上定义一个toString方法,包括在创建它的构造函数的原型中也没有,而在Object.prototype中有一个toString方法,且二者是相等的,所以f1调用toString方法也就是通过原型链去查找最后在Object.prototype找到了对应的方法。

函数也有原型链

我们继续看图,在javascript中,函数也是对象的一种,自然函数也是有原型链的,而函数实际上都是Function的实例,自然酥油函数都会有一个[[prototype]]指向Function.prototype了,另外不管是普通函数还是内置的函数比如Function、Object、Array等,既然它们是函数,也会有一个[[prototype]]指向Function.prototype。

Foo.toString() // "function Foo() {}"
Object.prototype.toString.call(Foo) // [object Function]
Function.prototype.toString.call(Foo) // "function Foo() {}"
Foo.toString === Function.prototype.toString // true

此时调用Foo.toString方法,会发现与Object.prototype.toString方法调用的结果不一样,这是因为Foo通过原型链,在Foo.prototype上就找到了对应的方法,然后停止查找了。

另外细心的同学可能发现了,在这幅图中,有一个循环的指向

Object -> __proto__ -> Function.prototype -> __proto__ -> Object.prototype -> constructor -> Object

这也很好理解,Object是函数,所以会有原型链查找到Function.prototype再查找到Object自己的原型Object.prototype,而Object.prototype是对象,且在创建的时候会自动添加上一个constructor的属性指向Object函数自身,所以会有这么一个看似是“鸡生蛋,蛋生鸡”的结果。

原型链相关

如何判断两个对象是否通过原型链有关联呢,我们可以通过instanceof操作符和Object.prototype.isPrototypeOf来判断

f1 instanceof Foo // true
f1 instanceof Object // true
f1 instanceof Function // false

Foo.prototype.isPrototypeOf(f1) // true
Object.prototype.isPrototypeOf(f1) // true

当Foo.prototype自身没有isPrototypeOf时,通过原型链查找到了Object.prototype上找到了对应的方法。

instanceof和isPrototypeOf的区别:

  • instanceof是用原型链上查找的各个函数的原型,再通过该原型对应自身的函数判断,即instanceof右侧的值为函数且该函数的原型出现在左侧的值的原型链上
  • isPrototypeOf则是直接通过原型对象查找,不经过原型的函数本身了

二者比较而言还是isPrototypeOf更通用更直观一些。