天天看點

prototype是原型對象,那__proto__又是什麼呢,原型深度解析

做過前端的都知道,兩個必會的知識就是原型和原型鍊,如果有人問你,原型是什麼?你是不是回答對象中都有一個預設的屬性叫prototype,指向的就是原型。如果再追問你,那原型鍊是什麼呢?你是不是回答如果在目前對象中找不到某個屬性,就會去父對象的原型中去查找,這樣一層一層的向上查找,一直到頂層null,這樣形成的一條鍊就叫原型鍊。

prototype是原型對象,那__proto__又是什麼呢,原型深度解析

那到底什麼是原型與原型鍊呢,先不說上面的回答對不對,這樣的答案肯定是小夥伴們聽别人說的之後,似懂非懂的記在了心裡,并沒有去驗證,隻是在工作中覺得确實好像是這麼回事。

下面我們來一起看一下,相信看完之後你能對它們有更深的了解。

prototype

所有的js對象都會繼承原型對象上面的屬性和方法。其中原型對象就是prototype所指向的那個對象。我們一般叫它原型屬性。

而原型屬性,是隻有函數才有的,或者說是隻有typeof為function的對象才有的(箭頭函數除外),在js裡面,函數可以作為構造函數使用,可以生成自己對應的執行個體化對象,而它所生成的這些執行個體,就會共享這個函數的原型對象裡面的屬性和方法,也就是我們所說的繼承。

從下面的例子我們來看一下:

prototype是原型對象,那__proto__又是什麼呢,原型深度解析

我們可以看到,不同的函數都擁有自己的原型屬性,而非函數不具有prototype屬性,是以傳回undefined,其中Symbol不是構造函數,但是本身具有原型屬性,箭頭函數也不是構造函數,但其本身并無原型屬性。

對于字元串、數值、布爾類型與其對應的構造函數也是一樣的:

prototype是原型對象,那__proto__又是什麼呢,原型深度解析

上面的輸出是否有跟你想的不一樣的呢?

我們通過一個簡單的例子來了解一下原型對象:

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屬于原型屬性,各個執行個體之間共用這個值。

用圖形來表述上面的關系的話,大概長成這樣:

prototype是原型對象,那__proto__又是什麼呢,原型深度解析

構造函數與執行個體

其中的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,已經很清晰了,我們來下是否像你想的樣子:

prototype是原型對象,那__proto__又是什麼呢,原型深度解析

這個時候小夥伴們又會問了:Object.prototype也是一個對象,它有__proto__屬性嗎?有的話指向哪裡呢?

我們直接來看:

prototype是原型對象,那__proto__又是什麼呢,原型深度解析

呀!我們發現雖然有這個屬性,但是它為null了,其實這也就是我們平常所說的,循着原型鍊查找,一直查找到null為止,那麼那個null是什麼它又在哪呢?就是這個,已經到達了原型鍊頂端,發現是null,就不會再繼續往上查找了。

至此我們知道了,對象的__proto__屬性指向它的構造函數的原型,通過它可以把一系列的原型連接配接起來,我們在通路一個對象的屬性的時候,如果目前對象不存在這個執行個體屬性,那麼它就會去從它的__proto__指向的對象中去查找,層層往上,一直到null。

我們可以通過一個示例來感受一下它的魔力:

prototype是原型對象,那__proto__又是什麼呢,原型深度解析

__proto__先大概介紹這些。

原型與原型鍊擴充

相信看到這裡,大家已經知道了什麼是原型以及原型的存在形式,也知道了__proto__是做什麼用的,它是如何把各個原型對象連接配接起來的,也能明白了對象屬性通路時對于原型鍊的查找機制。

下面我們來擴充一些内容,了解既有一些構造函數他們之間的關系。

如下示例:

① 數組的__proto__指向構造函數Array的原型

prototype是原型對象,那__proto__又是什麼呢,原型深度解析

② 函數的原型對象的__proto__指向構造函數Object的原型

prototype是原型對象,那__proto__又是什麼呢,原型深度解析

③ 函數的__proto__指向構造函數Function的原型

prototype是原型對象,那__proto__又是什麼呢,原型深度解析

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

prototype是原型對象,那__proto__又是什麼呢,原型深度解析

⑤ Object的__proto__指向Function的原型

prototype是原型對象,那__proto__又是什麼呢,原型深度解析

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

prototype是原型對象,那__proto__又是什麼呢,原型深度解析

我們一切操作基本都離不開上面的這些關聯關系,隻要了解prototype和__proto__,相信你會很快明白上面的這些行為。

主要總結為兩點:

  1. prototype為函數特有屬性,隻有函數才具有原型屬性,以供繼承,對象也能繼承主要是通過__proto__的連接配接
  2. __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      

總結

繼續閱讀