Instanceof与原型链
原型和原型链,是JavaScript中的一个重要知识点,也是面试中的高频考点,理解并掌握原型和原型链,对于前端开发者来说是重中之重。下面就通过Instanceof运算符来复习一下相关知识吧。
知识点
- 理解JavaScript对象原型
- 原型链如何工作
- instanceof运算符的作用与实现
理解JavaScript对象原型与原型链
JavaScript常被描述为一种基于原型的语言——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链。
JavaScript中规定,每个函数都有一个特殊的属性prototype
,这个属性的值是一个对象,通常被称为这个函数的原型。通俗的讲,我们叫它显示原型。通过new一个函数,我们可以得到一个实例对象,在创建这个实例对象的过程中,会将实例对象内部的__proto__
指向构造它的函数的显示原型上。通过__proto__
我们可以获取对象内部的原型,我们叫它隐式原型。有点绕对不对,下面我们通过代码和图像来加深理解:
(注:__proto__
为浏览器提供的属性(新版浏览器已被废弃,改为<prototype>
,这里为了方便还是以__proto__
来表示),并非官方标准,ECMAScript标准用[[prototype]]
来表示)
对象的隐式原型可以通过Object.getPrototypeOf(obj)
来获取。
1 |
|
通过代码和图像,我们对原型对象、构造函数、实例之间的关系有了更深一步的了解。我们都知道,函数其实也是对象的一种,那函数是谁创建的呢?函数也有__proto__
吗?答案是:函数是由Function来创建的,函数也有__proto__
。我们来验证一下:
跟函数Foo是被Function创建的一样,obj本质是也是由Object创建而来的。
1 |
|
而Function和Object是函数,所以他们也是对象。那Function的隐式原型__proto__
指向谁呢?我们通过控制台打印来观察一下:
我们惊奇的发现,Function.__proto__
与Function.prototype
一抹一样,所以我创造了我自己?
1 |
|
那Object.__proto__
应该指向Object.prototype
吗?答案是与上面一样,指向Function.prototype
。
1 |
|
那Object.prototype
跟Function.prototype
是一个原型对象吗?答案是false,惊不惊喜,意不意外。
1 |
|
我们接着来看,因为Function.prototype
是对象,所以他也有有隐式原型__proto__
,那它指向谁呢?答案是Object.prototype
,我们来验证一下:
1 |
|
越来越有趣了对不对,那我们好奇一下,Object.prototype
的隐式原型指向谁呢?答案是null。
1 |
|
通过上面的一系列小测验,我们得出:
- 所有的函数都有一个prototype属性,指向这个函数的原型对象(可以俗称显示原型、函数原型)
- 所有的实例对象,都有一个隐式原型
__proto__
,它指向这个对象的构造函数的原型对象(可以俗称隐式原型、对象原型) - 对象的隐式原型可以通过
Object.getPrototypeOf(obj)
来获取 - 函数也是对象,所以函数也有隐式原型
- Function的隐式原型指向它自己的显示原型
- Object的隐式原型指向Function的显示原型
- Function.prototype的隐式原型指向Object的显示原型
- Object.prototype是顶级,所以它的隐式原型指向null
看完上面头还是晕晕的?来一张图给你标明它们之间的关系:
原型链是如何工作的
通过上面的学习,我们已经掌握了什么是原型,而像上图这样把原型和对象一层一层链接起来,就叫做原型链。那它是如何工作的呢?我们还是通过代码来理解:
1 |
|
我们先是在Foo函数的原型对象上添加了一个name属性,然后通过new Foo生成foo对象,这时我们打印foo.name,我们明明没有给foo定义name属性,但是却能得到值XiaoDai。接着我们给foo定义name属性,再次打印,值变成了XiaoMeng。这是为什么呢?我们打印foo来观察:
我们发现,在foo对象上,有一个name属性,在foo的隐式原型上,也有一个name属性,当我们调用name属性时,会先从自身查找,如果自身有,直接返回查到的值。如果没有,就顺着__proto__
去隐式原型上查,如果隐式原型上也没有(比如上面代码调用的toString方法,foo的隐式原型上并没有),就会顺着隐式原型的隐式原型上去查,直到查到Object.prototype
,因为再往上查是null,就停止了。如下图:
1 |
|
需要注意的一点是:原型链上的属性和方法并没有被复制到实例对象上,通过上图也能观察到有2个name属性。
constructor属性
实际上,每个原型对象都有一个constructor属性,这个属性是在生成prototype时自动生成的,它指向构造函数本身。这里我们了解就好,所以上面的原型链图里没有标明,不然脑子会很乱。感兴趣的同学可以看着图写一下。
1 |
|
instanceof运算符的作用与实现
理解了原型与原型链,我们要如何判断一个构造函数的prototype属性是否出现在某个实例对象的原型链上呢?答案是instanceof运算符
1 |
|
接下来我们实现一下instanceof:
1 |
|
与typeof运算符的的区别
作用 | 缺点 | |
---|---|---|
typeof | 检测基本类型的值:string、boolean、number、null、undefined,以及引用类型中的function是精准的 | 对于object,array,null来说,都会返回object,无法精准区分 |
instanceof | 判断一个构造函数的prototype属性是否出现在某个实例对象的原型链上,从原型的角度上可以判断某引用属于哪个构造函数,从而判定它的数据类型 |
小结
通过这篇文章,我们理解了什么是原型和原型链,以及原型链的工作原理。其次,我们学习到了instanceof运算符的作用以及如何实现,并与typeof做了对比,希望这篇文章能够帮助大家快速掌握相关知识。