前端入門15-JavaScript進階之原型鏈

聲明

本系列文章內容全部梳理自以下幾個來源:

作為一個前端小白,入門跟着這幾個來源學習,感謝作者的分享,在其基礎上,通過自己的理解,梳理出的知識點,或許有遺漏,或許有些理解是錯誤的,如有發現,歡迎指點下。

PS:梳理的內容以《JavaScript權威指南》這本書中的內容為主,因此接下去跟 JavaScript 語法相關的系列文章基本只介紹 ES5 標準規範的內容、ES6 等這系列梳理完再單獨來講講。

正文-原型鏈

原型鏈也就是對象的繼承結構,舉個例子:

var a = []

那麼 a 對象的原型鏈:

a -> Array.prototype -> Object.prototype -> null

基本所有對象的原型鏈頂部都是 Object.prototype,而 Object.prototype 沒有原型,手動通過 Object.create(null) 創建的對象也沒有原型。但這兩點是特例。

原型的用途在於讓對象可繼承原型上的屬性,達到功能複用、代碼複用的目的。

面向對象的編程語言中,繼承是一大特性,所以在編寫 JavaScript 代碼時,要能夠很明確所創建的對象的一個原型鏈結構,這樣才便於更好的設計,更好的編寫代碼。

在編寫代碼過程中,使用的無非就是內置對象,或者自定義對象,所以下面來看看兩者的原型鏈結構:

內置對象的原型鏈結構

其實也就是之前有講過的默認的原型鏈結構:

  • 聲明的每個函數 -> Function.prototype –> Object.prototype -> null
  • 數組對象 -> Array.prototype -> Object.prototype -> null
  • 對象直接量創建的對象 -> Object.prototype -> null
  • 日期對象 -> Date.prototype -> Object.prototype -> null
  • 正則對象 -> RegExp.prototype -> Object.prototype -> null

    可以用對象的 __proto__.constructor.name 來測試:

Object.prototype 已經內置定義了一些屬性,如:toString(),isPrototypeOf(),hasOwnProperty() 等等;

同樣,Array.prototype 內置瞭如:forEach(),map() 等等。

其他內置原型也都有相對應的一些屬性。

所以使用內置對象時,才可以直接使用內置提供的一些屬性。

自定義對象的原型鏈結構

不手動修改自定義構造函數的 prototype 屬性的話,默認創建的對象的原型鏈結構:

  • 自定義構造函數創建的對象 -> {} -> Object.prototype -> null

比如:

function A() {}
var a = new A();

在首次使用構造函數 A 時,內部會去對 prototype 屬性賦值,所進行的工作類似於:A.prototype = new Object();

所以 A.prototype 會指向一個空對象,但這個空對象繼承了 Object.prototype。

那麼不修改這條原型鏈的話,默認通過自定義構造函數創建的對象的繼承結構也就是:{} –> Object.prototype –> null。

雖然這條原型鏈也可以這麼表示:A.prototype –> Object.prototype -> null

a 雖然確實繼承自 A.prototype,但我不傾向於這種寫法來表示,因為自定義構造函數的 prototype 屬性值會有很大的可能性被修改掉,當它的屬性值重新指向另一個對象後,此時也仍舊可以説 a 對象繼承自 A.prototype,個人感覺理解上會有點彆扭,無法區別前後原型的不同,畢竟 A.prototype 只是一個 key 值,所以我傾向於直接説 a 繼承的實際對象,也就是 key 值對應的 value 值。

雖然 Object.prototype 也是一個 key 值,實際指向的一個內置的對象,但手動修改這些內置構造函數的 prototype 的可能性不高,所以個人覺得對於內置構造函數,可以直接用類似 Object.prototype 來表示。

那麼這個時候,如果為這個構造函數的 prototype 添加一些屬性:

function A() {}
A.prototype.num = 0;
var a = new A();

那麼,對於對象 a 而言,它的原型鏈:

a -> {num:0} -> Object.prototype -> null

這是不修改原型鏈的場景,那麼如果手動破壞了默認的原型鏈呢?

var B = [];
B.num = 0; 
function A() {}   
A.prototype.num = 222; 
var a = new A();  //a 的原型鏈
A.prototype = B;
var b = new A();  //b 的原型鏈

此時對象 b 的原型鏈又是什麼呢?

首先看看對象 B,是一個數組對象,所以 B 對象的原型鏈:

B –> Array.prototype -> Object.prototype -> null

再來看看對象 a,創建它時,還並沒有修改構造函數的 prototype,所以它的原型鏈:

a -> {num:222} -> Object.prototype -> null

那麼這個時候,手動修改掉了構造函數的 prototype 指向,這之後再通過構造函數 A 創建的對象的原型鏈也就會跟隨着變化,所以對象 b 的原型鏈:

b -> B –> Array.prototype -> Object.prototype -> null

所以,修改構造函數的 prototype,其實相當於將另外一條原型鏈拿來替換掉原本的原型鏈。

原型鏈用途

對於對象,它的本質其實也就是一堆屬性的集合,所以對象的用途是用來操作對象內的屬性的,而當操作對象的屬性時,會有一種類似於作用域鏈機制來尋找屬性。

操作無非分兩種場景,一是讀取對象屬性,二是寫對象屬性,兩種所涉及的處理不一樣。

當讀取對象屬性時,是依靠對象的原型鏈來輔助工作,如果對象內部含有該屬性,則直接讀取,否則沿着原型鏈去尋找這個屬性。

也就是説,對象繼承原型的機制,並不是説,將原型的所有屬性拷貝一份到對象內部,而只是簡單對對象創建一條原型鏈而已。這條原型鏈中保存着各個原型對象的引用,當讀取繼承的屬性時,就可以根據這條原型鏈上的引用訪問到其他原型對象內的屬性了。

因為讀取繼承屬性,本質上是讀取其他對象的屬性,那麼,這些原型屬性發生變化時,也才會影響到繼承他們的子對象。

那麼,對於寫對象屬性的操作:

這點就由對象的特性決定了:當對一個對象的屬性進行賦值操作時,如果對象內沒有該屬性,那麼會動態為該對象添加一個屬性,如果對象內部有該屬性,那麼修改屬性值。

對象的屬性寫操作會影響到後續的讀操作,因為如果是讀取對象的某個繼承屬性,本來對象內部沒有該屬性,所以是去讀取的原型內的屬性值。但經過寫操作後,對象內部創建了同名的內部屬性,之後再讀取時,發現內部已經有了,自然不會再去原型鏈中讀取。

獲取對象的原型鏈

掌握了原型鏈的相關理論,對於代碼中某個對象的原型鏈也就能夠很清楚的知道了。無外乎內置對象的默認原型鏈,或者自定義構造函數手動修改的原型鏈。

但,初學階段,如果想借助瀏覽器的開發者工具的 console 來測試、查看對象的原型鏈以便驗證猜想,可以這麼處理:

var a = []

雖然 __proto__ 可以獲取原型,但拿到的是對象,所以可以藉助對象的某些標識,比如原型的 constructor 的 name 函數名屬性標識。

實例

網上關於原型鏈的文章經常會出現這麼一張圖片,首先我承認,這張圖很高級,也基本把原型鏈的相關理論表示出來了,但我很不喜歡它。因為對於新手來説,很難看懂這張圖,我第一次看到也一臉懵逼。

就算現在能夠看懂了,我也還是不喜歡它,因為這張圖表達的內容太多了:它不僅表示了某個對象的原型鏈結構,同時,也表示出了實例對象、原型、構造函數三者間的函數,而構造函數本質上也是對象,所以也順便表示它的原型鏈結構。

我們一步步來看,它首先定義了一個構造函數 Foo,然後通過它創建了 f1,f2對象,然後從 f1,f2開始出發,先求他們的原型鏈。

用代碼來説,其實也就是:

function Foo() {}
var f1 = new Foo();
//求f1對象的原型鏈

根據我們上述梳理的理論,很簡單了吧,原型鏈其實也就是:

f1 -> {} -> Object.prototype -> null

接着,它表達了可以用 __proto__ 獲取對象的原型,然後每個原型、構造函數、實例對象三者間的關係它也表達出來了,原型的constructor指向構造函數,而構造函數的prototype指向原型。

而這三個角色本質上也都是對象,既然是對象,那麼它們本身也有原型,所以也再順便畫出它們的原型鏈。

總之,就是從 f1 實例對象出發,先找它的原型,通過原型再找構造函數,然後再分別將原型和構造函數看成實例對象,重複之前f1的工作。

另外,又通過 new Object() 創建了對象 o1,求它的原型鏈。

所以,這張圖上,其實表達了一共 5 條原型鏈,分別是:

  • f1 的原型鏈
  • f1 的原型的constructor指向的構造函數Foo對象的原型鏈
  • 函數對象Foo的原型的constructor指向的構造函數Function對象的原型鏈
  • f1 的原型的原型即Object.prototype的constructor指向的構造函數Object 對象的原型鏈。
  • o1 的原型鏈

如果你能從這張圖看出這5條原型鏈,那麼原型鏈的理論你就基本掌握了。

而且,建議看這張圖時,每次都將某條原型鏈跟蹤到底,再去看另一條,這過程不要過多關注在分支上,否則很容易混亂。

對於新手,如果能夠對這張稍作備註,而不是直接將這張圖放出來,我覺得會更好,如下:


大家好,我是 dasu,歡迎關注我的公眾號(dasuAndroidTv),公眾號中有我的聯繫方式,歡迎有事沒事來嘮嗑一下,如果你覺得本篇內容有幫助到你,可以轉載但記得要關注,要標明原文哦,謝謝支持~
dasuAndroidTv2.png

關鍵詞:對象 原型 prototype 屬性 函數 object 構造 所以 null 繼承

相關推薦:

前端入門14-JavaScript進階之繼承

詳解js原型,構造函數以及class之間的原型關係

JavaScript原型鏈和繼承

js 原型鏈(轉)

[JS] Topic - Object.create vs new

javaScript系列 [04]-javaScript的原型鏈

原型對象和原型鏈