prototype是原型对象,那-proto-又是什么呢,总不能是别名吧?

做过前端的都知道,两个必会的知识就是原型和原型链,如果有人问你,原型是什么?你是不是回答对象中都有一个默认的属性叫prototype,指向的就是原型。如果再追问你,那原型链是什么呢?你是不是回答如果在当前对象中找不到某个属性,就会去父对象的原型中去查找,这样一层一层的向上查找,一直到顶层null,这样形成的一条链就叫原型链。

是的,没错

那到底什么是原型与原型链呢,先不说上面的回答对不对,这样的答案肯定是小伙伴们听别人说的之后,似懂非懂的记在了心里,并没有去验证,只是在工作中觉得确实好像是这么回事。

下面我们来一起看一下,相信看完之后你能对它们有更深的了解。

prototype

所有的js对象都会继承原型对象上面的属性和方法。其中原型对象就是prototype所指向的那个对象。我们一般叫它原型属性。

而原型属性,是只有函数才有的,或者说是只有typeof为function的对象才有的(箭头函数除外),在js里面,函数可以作为构造函数使用,可以生成自己对应的实例化对象,而它所生成的这些实例,就会共享这个函数的原型对象里面的属性和方法,也就是我们所说的继承。

从下面的例子我们来看一下:

不同类型的prototype

我们可以看到,不同的函数都拥有自己的原型属性,而非函数不具有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__指向它的构造函数的原型。

知道了这一点,我们很容易得出:

prototype与__proto__

到这就为止了吗?,我们还可以深挖一下,其实看到这里,有眼尖的小伙伴可能会问:PaperNovel.prototype也是一个对象,那它有没有__proto__属性,有的话指向哪里呢?

很好,提出这个问题说明你的求知欲很强,我们对待技术就是要充满好奇心。给你鼓掌[鼓掌]。

我们先来简单思考一下,PaperNovel.prototype是一个对象,它应该也是被实例化出来的,那么它应该有__proto__属性,并且指向它的构造函数的原型。

好了,对象的构造函数是什么呢?没错!就是Object,已经很清晰了,我们来下是否像你想的样子:

函数的原型的原型

这个时候小伙伴们又会问了:Object.prototype也是一个对象,它有__proto__属性吗?有的话指向哪里呢?

我们直接来看:

Object的原型的原型

呀!我们发现虽然有这个属性,但是它为null了,其实这也就是我们平常所说的,循着原型链查找,一直查找到null为止,那么那个null是什么它又在哪呢?就是这个,已经到达了原型链顶端,发现是null,就不会再继续往上查找了。

至此我们知道了,对象的__proto__属性指向它的构造函数的原型,通过它可以把一系列的原型连接起来,我们在访问一个对象的属性的时候,如果当前对象不存在这个实例属性,那么它就会去从它的__proto__指向的对象中去查找,层层往上,一直到null。

我们可以通过一个示例来感受一下它的魔力:

对象属性的查找

__proto__先大概介绍这些。

原型与原型链扩展

相信看到这里,大家已经知道了什么是原型以及原型的存在形式,也知道了__proto__是做什么用的,它是如何把各个原型对象连接起来的,也能明白了对象属性访问时对于原型链的查找机制。

下面我们来扩展一些内容,理解既有一些构造函数他们之间的关系。

如下示例:

① 数组的__proto__指向构造函数Array的原型

数组的__proto__

② 函数的原型对象的__proto__指向构造函数Object的原型

函数的原型的__proto__

③ 函数的__proto__指向构造函数Function的原型

函数的__proto__

④ Function的__proto__指向构造函数Function(也就是它自己)的原型

Function的__proto__

⑤ Object的__proto__指向Function的原型

Object的__proto__

⑥ Function的原型的__proto__指向Object的原型

Function的原型的__proto__

我们一切操作基本都离不开上面的这些关联关系,只要了解prototype和__proto__,相信你会很快明白上面的这些行为。

主要总结为两点:

  1. prototype为函数特有属性,只有函数才具有原型属性,以供继承,对象也能继承主要是通过__proto__的连接
  2. __proto__表示一个对象指向它的构造函数的原型的指针,也就是说一个对象的__proto__指向它的构造函数的原型

好好理解一下上面的这两点内容,结合之前的例子,基本就对原型与原型链有了深刻的认识,相信你也能很好的回答了文章开始的问题。

常用方法解析

① Object.prototype.toString.call

相信你看到这种形式的写法不会感到陌生,这是调用Object原型上面的toString方法,通过call指定了它的执行作用域,也就是改变了this的指向。

通过上面的学习我们知道Object的原型,其实就是它的实例化对象的__proto__,因此我们换一种方式书写也是可以的:

//注意,下面的结果是false//因此这两个方法转化的字符串规则是不一样的//我们在例子中使用的是Object.prototype.toStringObject.prototype.toString === Object.toString//通过上面的讲解,我们知道下面的结果是true//因此这两种写法是等价的Object.prototype.toString === ({}).__proto__.toString

Array.prototype.slice.call

//下面的结果为true//因此这两种写法是等价的Array.prototype.slice === [].__proto__.slice

总结

理解原型与原型链,对我们书写代码、构造高级函数、理解深层次的执行机制等,都是非常有帮助的,利用这些属性和它们的特点能创造出很多优雅的代码。

现在是不是对于原型和原型链有了更好的理解了呢?对于面试官给出的问题也能不慌不忙的解释清楚了呢?

希望本文对你有所帮助,不胜欣慰。

发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章