淺談JS原型

CodeForBetter發表於2023-02-23

JS原型學習筆記,如有錯誤,還請留言~~

  • 全域性物件window
  • 什麼是原型
  • JS中一切皆為物件?

全域性物件window

在談window之前,試想一個簡單的問題,開啟瀏覽器在控制檯輸入console.log("Hello"); 並按下Enter鍵,我們理所應當看到了控制檯給我們返回的結果Hello,那麼,console.log()這個方法是怎麼來的呢?其實瀏覽器中已經為我們內建了一個全域性物件叫做window,window作為全域性物件,代表指令碼正在執行的瀏覽器視窗。console.log()是一種簡寫的格式,更加精確的寫法為:window.console.log()。window下則封裝了多種 多樣的屬性,這些屬性中,有些是瀏覽器自帶的屬性,有些則是ECMAScript標準規定的屬性。ECMAScript規定的全域性物件為global,但是在瀏覽器下則是window。

淺談JS原型

這裡需要注意的是,瀏覽器中的全域性物件是window,但是在其他的環境下,例如node.js就不一樣了。瀏覽器下,在控制檯輸入window可以檢視全域性物件的所有屬性。而在node.js環境中,則需要使用global,且瀏覽器中規定的屬性在node.js環境下自然是無效的。
當我們開啟瀏覽器,系統自然會為瀏覽器分配記憶體,瀏覽器將記憶體分給各個頁面,頁面中的HTML,CSS,JS,外掛等會分配到一部分記憶體空間。對於JS來說,一開始window物件就已經存在於堆記憶體中了。

淺談JS原型

什麼是原型

先上圖:

淺談JS原型

這是堆記憶體中window全域性物件的部分記憶體模型圖,紅色框部分為函式,黑色框部分為prototype物件,先不管函式中的prototype屬性以及物件中的__proto__屬性的含義。我們先宣告一個物件,並呼叫toString()方法,看一看記憶體中到底會發生什麼。

var obj = new Object();
複製程式碼

淺談JS原型

我們看到我們在宣告瞭一個空物件之後,內部自帶了一個__proto__屬性,且使用命令:obj.__proto__ === Object.prototype 結果會返回true
淺談JS原型

那麼也就是說:

淺談JS原型

obj物件的__proto__屬性指向了Object.prototype這樣一個物件。我們來看一看Object.prototype裡面有什麼。

淺談JS原型

在Object.prototype中的toString屬性指向一個函式,恰恰是我們想要使用的toString()這樣一個方法。現在我們在大概可以明白:在我們宣告一個物件obj時,這明明是一個空物件,但是這個物件卻可以呼叫toSting(),valueOf()等方法,是因為在我們宣告物件時,瀏覽器為我們這個物件新增了一個屬性__proto__,這個屬性指向了一個Object.prototype這樣一個物件,Object.prototype物件中有著Object所共有的一些屬性。其實這個prototype就是原型,比起原型,我更喜歡叫它共有屬性物件。那麼__proto__是什麼呢?__proto__是瀏覽器自動賦予物件的一個屬性,這個屬性指向著一個函式的原型,當然最後的root必然是Object.prototype。
例如:

var n = new Number(15);
n.toString(16);// "f"
複製程式碼

上述程式碼的含義是,將數值型別的n轉化為16進位制後,再將其轉化為字串,15在16進位制中對應的值為f,轉化為字串之後的結果為"f"。很顯然,Object原型中的toString()方法是沒有辦法將一個值按照進位制轉化,再變為字串的,也就是說,Number型別的toString(),不同於Object原型的toString()方法。倘若是Java,我們會想到方法的重寫,但是JS則不一樣,我們繼續從記憶體模型的角度來分析到底發生了什麼:

淺談JS原型

首先var n = new Number(15); n為一個物件,如果不明白為什麼n的值會在堆記憶體中,可以參考我的文章JS記憶體模型。我們可以看到,物件n的__proto__指向了Number.prototype,在Number.prototype中有什麼呢?

淺談JS原型

我們可以看到,Number.prototype也有一個toString的屬性,指向了toString()這樣一個函式。並且

淺談JS原型

Number.prototype.toString === Object.prototype.toString返回的結果為false,也就是說,Number.prototype中的toString是一個“重寫”的屬性,當我們宣告瞭物件n,並呼叫toString()方法時,首先,瀏覽器會在n這個物件中尋找toString這個屬性,如果沒有它就會在n這個物件的__proto__屬性所指向的原型中尋找,n.__proto__指向了Number.prototype。 因為Numebr.prototype即Number原型也是一個物件,Number.prototype.__proto__則指向了root即Object.prototype。如果在Number原型中沒有toString這個屬性,那麼順其自然地就會在Object原型中尋找這個屬性。當然本例中,在Number的原型中Number.prototype找到了toString這個屬性,自然會呼叫它所指向的Number獨有的toString()方法。剛剛描述的過程中,這種指向的關係好似連結串列,而在JS中,我們可以形象地稱作為“原型鏈”。
接下來,我們再思考一個問題:函式是物件嗎 ?雖然typeof一個函式返回的結果為“function”,但是我們一再強調JavaScript裡的資料型別只有七種,無論是陣列還是函式,它們的本質都是物件。既然明確了函式是一個物件,那麼在上文中,我曾經暗示prototype是一個函式所擁有的屬性,__proto__是一個物件所擁有的屬性,那麼函式既然是物件,那麼換言之,一個函式中自然擁有兩種屬性了。我們再將記憶體模型圖完善一些:

淺談JS原型

在上圖中我們看到,如果一個函式作為一個函式而言,它自身的屬性為prototype,這個屬性指向它所對應的原型。如果將一個函式作為物件來看,它的屬性__proto__則指向了Function.prototype,這種指向的關係仔細想一想也合情合理,一個函式會提供自己的共有屬性,但是一個函式本質上也是一個物件,它被構造出來需要一個建構函式。值得一提的是Funcion.__proto__ === Funciton.prototype。 這可以形象地理解為:自己造自己,自己賦予自己了屬性。其實理解不了上圖也無關緊要,我們只需要記住原型裡面的一個規則即可:

內建函式.__proto__ === Function.prototype;
複製程式碼

一切皆為物件?

JS中一切皆為物件?很顯然這句話是錯的,用最簡單的程式碼就可以證明:

var n = 15;
typeof n;// "number"
複製程式碼

JS中,有七種資料型別,在上面的程式碼中,我們宣告瞭var n = 15;,在使用typeof n時,我們也可以看到返回的型別是number。但是,我們卻可以這樣做:

n.toString(16);// "f"
複製程式碼

和上面介紹的var n = new Number(15)宣告n的方式不同。n的typeof 返回的是一個“number”,如果使用var n = new Number(15)宣告n,那麼使用typeof n返回的結果就會是“object”。在宣告一個物件時,瀏覽器會為這個物件自動新增__proto__這樣一個屬性指向原型,好呼叫相應的方法,既然我們宣告瞭var n = 15;且可以呼叫toString()這樣一個方法,那不還是說明n實際上是一個物件嗎?實際上,並不是這樣的,n自然是一個number型別的數字,只不過這裡面另有蹊蹺。

var n = 15;
n.toString(16);
複製程式碼

當我們呼叫toString()方法時,在堆記憶體中會生成一個臨時物件,我們暫時叫它temp。也就是說這個過程是這樣的:

var n = 15;
// 呼叫toString()方法時,會在堆記憶體中產生一個臨時變數temp
// var temp = new Number(n);
// 將temp.toString(16)的結果記錄下來
// 臨時變數temp隨即被"抹殺掉"
複製程式碼

一個數值型,字串型等普通型別的變數可以呼叫原型中的方法,並不能說明它們型別的本質是物件,因為“建立臨時變數機制”,讓許多人對此有了誤解,實際上數值型即是數值型,字串型是字串型,JS當中一切皆物件很顯然是一個謬論。再看一個例子:

var a = 1;
// 問:執行此條語句 a.xxx = 2 是否會執行成功? 
複製程式碼

其實只要理解了"臨時物件"這個概念,就不難回答這個問題,在宣告var a = 1;後,我們已經確定了a的型別是Number,在執行語句a.xxx = 2;時,在堆記憶體中會生成一個臨時物件,對於這個臨時物件來說,執行a.xxx = 2;就相當於新增了一個屬性xxx其值為2,所以這條語句自然能執行成功。當然這個臨時物件會立刻被“抹殺”,當我們再次在控制檯輸入a.xxx檢視這個值時,返回的結果自然是undefined。

淺談JS原型

相關文章