本文首發於個人 Github,歡迎 issue / fxxk。
前言
ES6
的第一個版本釋出於 15
年 6
月,而本文最早創作於 16
年,那也是筆者從事前端的早期。在那個時候,ES6
的眾多特性仍處於 stage
階段,也遠沒有現在這麼普及,為了更輕鬆地寫JavaScript
,筆者曾花費了整整一天,仔細理解了一下原型——這個對於一個成熟的JavaScript
開發者必須要跨越的大山。
ES6
帶來了太多的語法糖,其中箭頭函式掩蓋了 this
的神妙,而 class
也掩蓋了本文要長篇談論的 原型
。
最近,我重寫了這篇文章,通過本文,你將可以學到:
- 如何用
ES5
模擬類; - 理解
prototype
和__proto__
; - 理解原型鏈和原型繼承;
- 更深入地瞭解
JavaScript
這門語言。
引入:普通物件與函式物件
在 JavaScript
中,一直有這麼一種說法,萬物皆物件。事實上,在 JavaScript
中,物件也是有區別的,我們可以將其劃分為 普通物件
和 函式物件
。Object
和 Function
便是 JavaScript
自帶的兩個典型的 函式物件
。而函式物件就是一個純函式,所謂的 函式物件
,其實就是使用 JavaScript
在 模擬類
。
那麼,究竟什麼是普通物件
,什麼又是函式物件
呢?請看下方的例子:
首先,我們分別建立了三個 Function
和 Object
的例項:
function fn1() {}
const fn2 = function() {}
const fn3 = new Function('language', 'console.log(language)')
const ob1 = {}
const ob2 = new Object()
const ob3 = new fn1()
複製程式碼
列印以下結果,可以得到:
console.log(typeof Object); // function
console.log(typeof Function); // function
console.log(typeof ob1); // object
console.log(typeof ob2); // object
console.log(typeof ob3); // object
console.log(typeof fn1); // function
console.log(typeof fn2); // function
console.log(typeof fn3); // function
複製程式碼
在上述的例子中,ob1
、ob2
、ob3
為普通物件(均為 Object
的例項),而 fn1
、fn2
、fn3
均是 Function
的例項,稱之為 函式物件
。
如何區分呢?其實記住這句話就行了:
- 所有
Function
的例項都是函式物件
,而其他的都是普通物件
。
說到這裡,細心的同學會發表一個疑問,一開始,我們已經提到,Object
和 Function
均是 函式物件
,而這裡我們又說:所有Function
的例項都是函式物件
,難道 Function
也是 Function
的例項?
先保留這個疑問。接下來,對這一節的內容做個總結:
從圖中可以看出,物件本身的實現還是要依靠建構函式。那 原型鏈
到底是用來幹嘛的呢?
眾所周知,作為一門物件導向(Object Oriented)的語言,必定具有以下特徵:
- 物件唯一性
- 抽象性
- 繼承性
- 多型性
而原型鏈最大的目的, 就是為了實現繼承
。
進階:prototype 和 __proto__
原型鏈究竟是如何實現繼承的呢?首先,我們要引入介紹兩兄弟:prototype
和 __proto__
,這是在 JavaScript
中無處不在的兩個變數(如果你經常除錯的話),然而,這兩個變數並不是在所有的物件上都存在,先看一張表:
物件型別 | prototype |
__proto__ |
---|---|---|
普通物件(NO) | ❎ | ✅ |
函式物件(FO) | ✅ | ✅ |
首先,我們先給出以下結論:
- 只有
函式物件
具有prototype
這個屬性; prototype
和__proto__
都是JavaScript
在定義一個函式或物件時自動建立的預定義屬性
。
接下來,我們驗證上述的兩個結論:
function fn() {}
console.log(typeof fn.__proto__); // function
console.log(typeof fn.prototype); // object
const ob = {}
console.log(typeof ob.__proto__); // function
console.log(typeof ob.prototype); // undefined,哇!果然普通物件沒有 prototype
複製程式碼
既然是語言層面的預置屬性,那麼兩者究竟有何區別呢?我們依然從結論出發,給出以下兩個結論:
prototype
被例項的__proto__
所指向(被動)__proto__
指向建構函式的prototype
(主動)
哇,也就是說以下程式碼成立:
console.log(fn.__proto__ === Function.prototype); // true
console.log(ob.__proto__ === Object.prototype); // true
複製程式碼
看起來很酷,結論瞬間被證明,感覺是不是很爽,那麼問題來了:既然 fn
是一個函式物件,那麼 fn.prototype.__proto__
到底等於什麼?
這是我嘗試去解決這個問題的過程:
- 首先用
typeof
得到fn.prototype
的型別:"object"
- 哇,既然是
"object"
,那fn.prototype
豈不是 Object 的例項?根據上述的結論,快速地寫出驗證程式碼:
console.log(fn.prototype.__proto__ === Object.prototype) // true
複製程式碼
接下來,如果要你快速地寫出,在建立一個函式時,JavaScript
對該函式原型的初始化程式碼,你是不是也能快速地寫出:
// 實際程式碼
function fn1() {}
// JavaScript 自動執行
fn1.protptype = {
constructor: fn1,
__proto__: Object.prototype
}
fn1.__proto__ = Function.prototype
複製程式碼
到這裡,你是否有一絲恍然大悟的感覺?此外,因為普通物件就是通過 函式物件
例項化(new
)得到的,而一個例項不可能再次進行例項化,也就不會讓另一個物件的 __proto__
指向它的 prototype
, 因此本節一開始提到的 普通物件沒有 prototype 屬性
的這個結論似乎非常好理解了。從上述的分析,我們還可以看出,fn1.protptype
就是一個普通物件,它也不存在 protptype
屬性。
再回顧一下上一節,我們還遺留一個疑問:
- 難道
Function
也是Function
的例項?
是時候去掉應該
讓它成立了。那麼此刻,please show me your code!
檢視答案
console.log(Function.__proto__ === Function.prototype) // true
複製程式碼
重點:原型鏈
上一節我們詳解了 prototype
和 __proto__
,實際上,這兩兄弟主要就是為了構造原型鏈而存在的。
先上一段程式碼:
const Person = function(name, age) {
this.name = name
this.age = age
} /* 1 */
Person.prototype.getName = function() {
return this.name
} /* 2 */
Person.prototype.getAge = function() {
return this.age
} /* 3 */
const ulivz = new Person('ulivz', 24); /* 4 */
console.log(ulivz) /* 5 */
console.log(ulivz.getName(), ulivz.getAge()) /* 6 */
複製程式碼
解釋一下執行細節:
- 執行
1
,建立了一個建構函式Person
,要注意,前面已經提到,此時Person.prototype
已經被自動建立,它包含constructor
和__proto__
這兩個屬性; - 執行
2
,給物件Person.prototype
增加了一個方法getName()
; - 執行
3
,給物件Person.prototype
增加了一個方法getAge()
; - 執行
4
, 由建構函式Person
建立了一個例項ulivz
,值得注意的是,一個建構函式在例項化時,一定會自動執行該建構函式。 - 在瀏覽器得到
5
的輸出,即ulivz
應該是:
{
name: 'ulivz',
age: 24
__proto__: Object // 實際上就是 `Person.prototype`
}
複製程式碼
結合上一節的經驗,以下等式成立:
console.log(ulivz.__proto__ == Person.prototype) // true
複製程式碼
- 執行
6
的時候,由於在ulivz
中找不到getName()
和getAge()
這兩個方法,就會繼續朝著原型鏈向上查詢,也就是通過__proto__
向上查詢,於是,很快在ulviz.__proto__
中,即Person.prototype
中找到了這兩個方法,於是停止查詢並執行得到結果。
這便是 JavaScript
的原型繼承。準確的說,JavaScript
的原型繼承是通過 __proto__
並藉助 prototype
來實現的。
於是,我們可以作如下總結:
- 函式物件的
__proto__
指向Function.prototype
;(複習) - 函式物件的
prototype
指向instance.__proto__
;(複習) - 普通物件的
__proto__
指向Object.prototype
;(複習) - 普通物件沒有
prototype
屬性;(複習) - 在訪問一個物件的某個屬性/方法時,若在當前物件上找不到,則會嘗試訪問
ob.__proto__
, 也就是訪問該物件的建構函式的原型obCtr.prototype
,若仍找不到,會繼續查詢obCtr.prototype.__proto__
,像依次查詢下去。若在某一刻,找到了該屬性,則會立刻返回值並停止對原型鏈的搜尋,若找不到,則返回undefined
。
為了檢驗你對上述的理解,請分析下述兩個問題:
- 以下程式碼的輸出結果是?
console.log(ulivz.__proto__ === Function.prototype)
複製程式碼
檢視結果
falsePerson.__proto__
和Person.prototype.__proto__
分別指向何處?
檢視分析
前面已經提到,在 JavaScript
中萬物皆物件。Person
很明顯是 Function
的例項,因此,Person.__proto__
指向 Function.prototype
:
console.log(Person.__proto__ === Function.prototype) // true
複製程式碼
因為 Person.prototype
是一個普通物件,因此 Person.prototype.__proto__
指向Object.prototype
console.log(Person.prototype.__proto__ === Object.prototype) // true
複製程式碼
為了驗證 Person.__proto__
所在的原型鏈中沒有 Object
,以及 Person.prototype.__proto__
所在的原型鏈中沒有 Function
, 結合以下語句驗證:
console.log(Person.__proto__ === Object.prototype) // false
console.log(Person.prototype.__proto__ == Function.prototype) // false
複製程式碼
終極:原型鏈圖
上一節,我們實際上還遺留了一個疑問:
- 原型鏈如果一個搜尋下去,如果找不到,那何時停止呢?也就是說,原型鏈的盡頭是哪裡?
我們可以快速地利用以下程式碼驗證:
function Person() {}
const ulivz = new Person()
console.log(ulivz.name)
複製程式碼
很顯然,上述輸出 undefined
。下面簡述查詢過程:
ulivz // 是一個物件,可以繼續
ulivz['name'] // 不存在,繼續查詢
ulivz.__proto__ // 是一個物件,可以繼續
ulivz.__proto__['name'] // 不存在,繼續查詢
ulivz.__proto__.__proto__ // 是一個物件,可以繼續
ulivz.__proto__.__proto__['name'] // 不存在, 繼續查詢
ulivz.__proto__.__proto__.__proto__ // null !!!! 停止查詢,返回 undefined
複製程式碼
哇,原來路的盡頭是一場空。
最後,再回過頭來看看上一節的那演示程式碼:
const Person = function(name, age) {
this.name = name
this.age = age
} /* 1 */
Person.prototype.getName = function() {
return this.name
} /* 2 */
Person.prototype.getAge = function() {
return this.age
} /* 3 */
const ulivz = new Person('ulivz', 24); /* 4 */
console.log(ulivz) /* 5 */
console.log(ulivz.getName(), ulivz.getAge()) /* 6 */
複製程式碼
我們來畫一個原型鏈圖,或者說,將其整個原型鏈圖畫出來?請看下圖:
PS:手賤把chl(我的中文名縮寫)改成了 ulivz(Github名),所以這張圖中的chl實際上就是ulivz,畫這張圖的時候, 我還在用windows = =
畫完這張圖,基本上所有之前的疑問都可以解答了。
與其說萬物皆物件, 萬物皆空似乎更形象。
調料:constructor
前面已經有所提及,但只有原型物件才具有 constructor
這個屬性,constructor
用來指向引用它的函式物件。
Person.prototype.constructor === Person //true
console.log(Person.prototype.constructor.prototype.constructor === Person) //true
複製程式碼
這是一種迴圈引用。當然你也可以在上一節的原型鏈圖中畫上去,這裡就不贅述了。
補充: JavaScript中的6大內建(函式)物件的原型繼承
通過前文的論述,結合相應的程式碼驗證,整理出以下原型鏈圖:
由此可見,我們更加強化了這兩個觀點:
- 任何內建函式物件(類)本身的
__proto__
都指向Function
的原型物件;- 除了
Oject
的原型物件的__proto__
指向null
,其他所有內建函式物件的原型物件的__proto__
都指向object
。
為了減少讀者敲程式碼的時間,特給出驗證程式碼,希望能夠促進你的理解。
Array:
console.log(arr.__proto__)
console.log(arr.__proto__ == Array.prototype) // true
console.log(Array.prototype.__proto__== Object.prototype) // true
console.log(Object.prototype.__proto__== null) // true
複製程式碼
RegExp:
var reg = new RegExp;
console.log(reg.__proto__)
console.log(reg.__proto__ == RegExp.prototype) // true
console.log(RegExp.prototype.__proto__== Object.prototype) // true
複製程式碼
Date:
var date = new Date;
console.log(date.__proto__)
console.log(date.__proto__ == Date.prototype) // true
console.log(Date.prototype.__proto__== Object.prototype) // true
複製程式碼
Boolean:
var boo = new Boolean;
console.log(boo.__proto__)
console.log(boo.__proto__ == Boolean.prototype) // true
console.log(Boolean.prototype.__proto__== Object.prototype) // true
複製程式碼
Number:
var num = new Number;
console.log(num.__proto__)
console.log(num.__proto__ == Number.prototype) // true
console.log(Number.prototype.__proto__== Object.prototype) // true
複製程式碼
String:
var str = new String;
console.log(str.__proto__)
console.log(str.__proto__ == String.prototype) // true
console.log(String.prototype.__proto__== Object.prototype) // true
複製程式碼
總結
來幾句短總結:
- 若
A
通過new
建立了B
,則B.__proto__ = A.prototype
; __proto__
是原型鏈查詢的起點;- 執行
B.a
,若在B
中找不到a
,則會在B.__proto__
中,也就是A.prototype
中查詢,若A.prototype
中仍然沒有,則會繼續向上查詢,最終,一定會找到Object.prototype
,倘若還找不到,因為Object.prototype.__proto__
指向null
,因此會返回undefined
; - 為什麼萬物皆空,還是那句話,原型鏈的頂端,一定有
Object.prototype.__proto__ ——> null
。
最後,給你留下一個疑問:
- 如何用
JavaScript
實現類的繼承呢?
請看我的原型系列的下一篇《深入JavaScript繼承原理》 分曉。
以上,全文終。
注:此外本文屬於個人總結,部分表達可能會有疏漏之處,如果您發現本文有所欠缺,為避免誤人子弟,請放心大膽地在評論中指出,或者給我提 issue,感謝~