做过前端的都知道,两个必会的知识就是原型和原型链,如果有人问你,原型是什么?你是不是回答对象中都有一个默认的属性叫prototype,指向的就是原型。如果再追问你,那原型链是什么呢?你是不是回答如果在当前对象中找不到某个属性,就会去父对象的原型中去查找,这样一层一层的向上查找,一直到顶层null,这样形成的一条链就叫原型链。
那到底什么是原型与原型链呢,先不说上面的回答对不对,这样的答案肯定是小伙伴们听别人说的之后,似懂非懂的记在了心里,并没有去验证,只是在工作中觉得确实好像是这么回事。
下面我们来一起看一下,相信看完之后你能对它们有更深的了解。
prototype
所有的js对象都会继承原型对象上面的属性和方法。其中原型对象就是prototype所指向的那个对象。我们一般叫它原型属性。
而原型属性,是只有函数才有的,或者说是只有typeof为function的对象才有的(箭头函数除外),在js里面,函数可以作为构造函数使用,可以生成自己对应的实例化对象,而它所生成的这些实例,就会共享这个函数的原型对象里面的属性和方法,也就是我们所说的继承。
从下面的例子我们来看一下:
我们可以看到,不同的函数都拥有自己的原型属性,而非函数不具有prototype属性,因此返回undefined,其中Symbol不是构造函数,但是本身具有原型属性,箭头函数也不是构造函数,但其本身并无原型属性。
对于字符串、数值、布尔类型与其对应的构造函数也是一样的:
上面的输出是否有跟你想的不一样的呢?
我们通过一个简单的例子来理解一下原型对象:
function PaperNovel(author, title, date, pages, content) {
this.author = author
this.title = title
this.date = date
this.pages = pages
this.content = content
}
PaperNovel.prototype.medium = "纸质"
PaperNovel.prototype.category = "小说"
PaperNovel.prototype.wordsNum = function () {
return this.content.length
}
let 三国演义 = new PaperNovel("罗贯中", "三国演义", "2022-07-31", "327", "东汉末年,皇帝昏聩,宦官专权,民不聊生。爆发了大型农民起义——黄巾起义。乱世之中,一代英雄人物竞相涌现。")
let 西游记 = new PaperNovel("吴承恩", "西游记", "2022-07-30", "465", "东胜神州傲来国海边有一花果山,山顶一石,受日月精华,产下一个石猴。石猴在花果山做了众猴之王,为求长生,出海求仙,在西牛贺州拜菩提祖师为师。")
console.log(三国演义.author)//罗贯中
console.log(三国演义.medium)//纸质
console.log(西游记.author)//吴承恩
console.log(西游记.medium)//纸质
console.log(西游记.wordsNum())//69
我们定义了函数PaperNovel,当它被当做构造函数来调用的时候,实例化了两个对象:三国演义和西游记。其中author属于实例属性,不同的实例拥有各自对应的值,medium属于原型属性,各个实例之间共用这个值。
用图形来表述上面的关系的话,大概长成这样:
构造函数与实例
其中的author、title、date、pages、content属于实例属性,每个实例对象都会有自己的实例属性值,存储在当前的实例对象中。medium、category、wordsNum属于原型属性,每个实例对象都共有这些原型属性,存储在构造函数的原型中,实例对象只保存一个对它们的引用。
因此原型中的属性改变的时候,所有的实例对象都会受到影响,请看如下结果:
console.log(三国演义.category)//小说
console.log(西游记.category)//小说
PaperNovel.prototype.category = "散文"
console.log(三国演义.category)//散文
console.log(西游记.category)//散文
三国演义.__proto__.category = "科普"
console.log(西游记.category)//科普
在实例对象访问一个值的时候,会先在实例属性中查找,如果没有找到,那么将会去它对应的构造函数的原型中去查找,还是以上面的代码为例,我们来看一下效果:
console.log(三国演义.category)//小说
三国演义.category = "散文"
console.log(三国演义.category)//散文
console.log(三国演义.__proto__.category)//小说
console.log(西游记.category)//小说
至此我们知道了,prototype是函数的原型对象,当函数被当做构造函数调用的时候,区别于实例属性,原型属性会被所有实例所共用,实现的方式就是所有实例对象保存一个指向该原型对象的指针。
原型属性大概先介绍这些。
proto
我们上文中说到,实例对象没有prototype属性,只有构造函数才有,实例对象会有一个指针来指向构造函数的原型对象,而这个指针就是用__proto__来存储和表示的。
也就是说实例对象的__proto__指向它的构造函数的原型。
知道了这一点,我们很容易得出:
到这就为止了吗?,我们还可以深挖一下,其实看到这里,有眼尖的小伙伴可能会问:PaperNovel.prototype也是一个对象,那它有没有__proto__属性,有的话指向哪里呢?
很好,提出这个问题说明你的求知欲很强,我们对待技术就是要充满好奇心。给你鼓掌[手动鼓掌]。
我们先来简单思考一下,PaperNovel.prototype是一个对象,它应该也是被实例化出来的,那么它应该有__proto__属性,并且指向它的构造函数的原型。
好了,对象的构造函数是什么呢?没错!就是Object,已经很清晰了,我们来下是否像你想的样子:
这个时候小伙伴们又会问了:Object.prototype也是一个对象,它有__proto__属性吗?有的话指向哪里呢?
我们直接来看:
呀!我们发现虽然有这个属性,但是它为null了,其实这也就是我们平常所说的,循着原型链查找,一直查找到null为止,那么那个null是什么它又在哪呢?就是这个,已经到达了原型链顶端,发现是null,就不会再继续往上查找了。
至此我们知道了,对象的__proto__属性指向它的构造函数的原型,通过它可以把一系列的原型连接起来,我们在访问一个对象的属性的时候,如果当前对象不存在这个实例属性,那么它就会去从它的__proto__指向的对象中去查找,层层往上,一直到null。
我们可以通过一个示例来感受一下它的魔力:
__proto__先大概介绍这些。
原型与原型链扩展
相信看到这里,大家已经知道了什么是原型以及原型的存在形式,也知道了__proto__是做什么用的,它是如何把各个原型对象连接起来的,也能明白了对象属性访问时对于原型链的查找机制。
下面我们来扩展一些内容,理解既有一些构造函数他们之间的关系。
如下示例:
① 数组的__proto__指向构造函数Array的原型
② 函数的原型对象的__proto__指向构造函数Object的原型
③ 函数的__proto__指向构造函数Function的原型
④ Function的__proto__指向构造函数Function(也就是它自己)的原型
⑤ Object的__proto__指向Function的原型
⑥ Function的原型的__proto__指向Object的原型
我们一切操作基本都离不开上面的这些关联关系,只要了解prototype和__proto__,相信你会很快明白上面的这些行为。
主要总结为两点:
- prototype为函数特有属性,只有函数才具有原型属性,以供继承,对象也能继承主要是通过__proto__的连接
- __proto__表示一个对象指向它的构造函数的原型的指针,也就是说一个对象的__proto__指向它的构造函数的原型
好好理解一下上面的这两点内容,结合之前的例子,基本就对原型与原型链有了深刻的认识,相信你也能很好的回答了文章开始的问题。
常用方法解析
① Object.prototype.toString.call
相信你看到这种形式的写法不会感到陌生,这是调用Object原型上面的toString方法,通过call指定了它的执行作用域,也就是改变了this的指向。
通过上面的学习我们知道Object的原型,其实就是它的实例化对象的__proto__,因此我们换一种方式书写也是可以的:
//注意,下面的结果是false
//因此这两个方法转化的字符串规则是不一样的
//我们在例子中使用的是Object.prototype.toString
Object.prototype.toString === Object.toString
//通过上面的讲解,我们知道下面的结果是true
//因此这两种写法是等价的
Object.prototype.toString === ({}).__proto__.toString
Array.prototype.slice.call
//下面的结果为true
//因此这两种写法是等价的
Array.prototype.slice === [].__proto__.slice
总结
理解原型与原型链,对我们书写代码、构造高级函数、理解深层次的执行机制等,都是非常有帮助的,利用这些属性和它们的特点能创造出很多优雅的代码。
现在是不是对于原型和原型链有了更好的理解了呢?对于面试官给出的问题也能不慌不忙的解释清楚了呢?
希望本文对你有所帮助,不胜欣慰。