前言

面试必问的原型和原型链问题,先来开具一张图,后面就全靠想了
原型图.jpg

1.构造函数

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

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

上述代码发生了几件事
1.定义了一个函数,定义函数时会根据特定的规则为该函数创建一个prototype属性,这个属性指向一个对象,即函数的原型。这个对象也会默认带有一个constructor属性,指向函数本身;

2.通过new操作符调用这个函数,并返回赋值给f1,f1就是构造函数Foo的实例。f1是一个对象,在js中,所有的对象在创建时都会被添加一个[[prototype]]属性,目前大部门浏览器可以通过proto属性访问[[prototype]]属性(此处可以认为是同一个东西),es5中可以使用Object.getPrototypeOf可以直接访问这个属性;

3.f1对象的[[prototype]]指向了创建f1的构造函数Foo的原型属性Foo.prototype,这个属性连接的是f1和构造函数原型,而不是连接的构造函数本身,可以通过以下方式验证

1
2
f1.__proto__ === Foo.prototype //true
Object.getPrototypeOf(f1) === Foo.prototype // true

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

2.原型链

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

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

1
2
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

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

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

1
2
3
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找到了对应的方法。

2.函数也有原型链

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

1
2
3
4
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函数自身,所以会有这么一个看似是“鸡生蛋,蛋生鸡”的结果。

4.原型链相关

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

1
2
3
4
5
6
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更通用更直观一些。