又到了金三銀四的季節,各路碼農又琢磨著該跳槽漲工資了,對前端的同學來說,面試時原型和原型鏈相關的知識幾乎是必問的。不少新人甚至是工作一段時間的同學,對於這方面的知識瞭解還是不夠甚至不怎麼了解,如果你是這樣的狀態,那麼看了這篇文章希望你能更深入的瞭解原型和原型鏈,如果有說得不對的地方,還忘各位大佬不吝賜教。
首先祭上這張經典的解釋javascript原型和原型鏈的圖,出處已不可考,但這不是本文重點。
我們可以從以下幾點來分析原型與原型鏈
建構函式
從圖片上部分開始分析,我們首先定義一個建構函式,並建立它的例項
function Foo() {}
var f1 = new Foo()
複製程式碼
此時發生了幾件事:
- 定義了一個函式,定義函式時會根據一組特定的規則為該函式建立一個prototype屬性,這個屬性指向一個物件,即函式的原型,這個物件也會預設帶有一個constructor屬性,指向函式本身。
- 通過new操作符呼叫這個函式,並返回一個物件賦值給f1,f1就是Foo建構函式的例項,f1是一個物件,在javascript中,所有的物件建立時都會被新增上一個內部屬性[[prototype]]屬性,目前在大部分瀏覽器中都可以通過__proto__屬性訪問[[prototype]](在此處__proto__可視為與[[prototype]]是同一個東西),ES5新增了一個Object.getPrototypeOf可直接訪問這個屬性。
- f1物件的[[prototype]]指向了建立f1的建構函式Foo的原型屬性Foo.prototype,這個屬性連線的是f1和建構函式的原型,而不是連線了建構函式本身,我們可以通過以下方式驗證。
f1.__proto__ === Foo.prototype // true
Object.getPrototypeOf(f1) === Foo.prototype // true
複製程式碼
我們常說的例項可以呼叫建構函式的原型上的方法,就是因為[[prototype]]屬性的存在。當例項f1訪問自身身上不存在的屬性或方法時,會嘗試通過[[prototype]]屬性去訪問建立它的建構函式Foo的原型上即Foo.prototype是否存在對應的屬性。我們可以開到上圖的最上面的線,f1__proto__ -> Foo.prototype。
原型鏈
我們同樣可以看到,如果當f1通過__proto__訪問Foo.prototype後,發現Foo.prototype還是沒有找到對應的屬性或方法,那麼此時f1還會繼續查詢。
別忘了Foo.prototype也是一個物件,既然是物件那麼就是Object的例項,那麼Foo.prototype也就自然的有一個[[prototype]]屬性指向Object的原型了,可以用一下方式驗證。
Foo.prototype.__proto__ === Object.prototype // true
Object.getPrototypeOf(Foo.prototype) === Object.prototype // true
複製程式碼
所以,當f1通過__proto__訪問Foo.prototype後發現找不到想要的屬性或方法,那麼會繼續根據Foo.prototype.__proto__訪問Object.prototype,如果找到了會訪問該屬性或呼叫該方法,且會停止查詢。如果還是沒有找到,根據圖上來看,最後會查詢到null。
f1 -> ___proto___ -> Foo.prototype -> __proto__ -> Object.prototype -> __proto__ -> null
複製程式碼
這樣看著像一條線的逐級查詢,且是通過函式的原型來查詢,這就是所謂的原型鏈。
為了驗證原型鏈真的存在,我們還可以舉個例子
f1.toString() // [object Object]
Object.prototype.toString.call(f1) // [object Object]
f1.toString === Object.prototype.toString // true
複製程式碼
很明顯,我們並沒有在f1上定義一個toString方法,包括在建立它的建構函式的原型中也沒有,而在Object.prototype中有一個toString方法,且二者是相等的,所以f1呼叫toString方法也就是通過原型鏈去查詢最後在Object.prototype找到了對應的方法。
函式也有原型鏈
我們繼續看圖,在javascript中,函式也是物件的一種,自然函式也是有原型鏈的,而函式實際上都是Function的例項,自然酥油函式都會有一個[[prototype]]指向Function.prototype了,另外不管是普通函式還是內建的函式比如Function、Object、Array等,既然它們是函式,也會有一個[[prototype]]指向Function.prototype。
Foo.toString() // "function Foo() {}"
Object.prototype.toString.call(Foo) // [object Function]
Function.prototype.toString.call(Foo) // "function Foo() {}"
Foo.toString === Function.prototype.toString // true
複製程式碼
此時呼叫Foo.toString方法,會發現與Object.prototype.toString方法呼叫的結果不一樣,這是因為Foo通過原型鏈,在Foo.prototype上就找到了對應的方法,然後停止查詢了。
另外細心的同學可能發現了,在這幅圖中,有一個迴圈的指向
Object -> __proto__ -> Function.prototype -> __proto__ -> Object.prototype -> constructor -> Object
複製程式碼
這也很好理解,Object是函式,所以會有原型鏈查詢到Function.prototype再查詢到Object自己的原型Object.prototype,而Object.prototype是物件,且在建立的時候會自動新增上一個constructor的屬性指向Object函式自身,所以會有這麼一個看似是“雞生蛋,蛋生雞”的結果。
原型鏈相關
如何判斷兩個物件是否通過原型鏈有關聯呢,我們可以通過instanceof操作符和Object.prototype.isPrototypeOf來判斷
f1 instanceof Foo // true
f1 instanceof Object // true
f1 instanceof Function // false
Foo.prototype.isPrototypeOf(f1) // true
Object.prototype.isPrototypeOf(f1) // true
複製程式碼
當Foo.prototype自身沒有isPrototypeOf時,通過原型鏈查詢到了Object.prototype上找到了對應的方法。
instanceof和isPrototypeOf的區別:
- instanceof是用原型鏈上查詢的各個函式的原型,再通過該原型對應自身的函式判斷,即instanceof右側的值為函式且該函式的原型出現在左側的值的原型鏈上
- isPrototypeOf則是直接通過原型物件查詢,不經過原型的函式本身了
二者比較而言還是isPrototypeOf更通用更直觀一些。