ES6帶來了太多的語法糖,其中箭頭函數掩蓋了 this 的神妙,而 class 也掩蓋了本文要長篇談論的 原型。
最近,我重寫了這篇文章,通過本文,你將可以學到:
1. 如何用 ES5 模擬類;
2. 理解 prototype 和 __proto__;
3. 理解原型鏈和原型繼承;
4. 更深入地瞭解 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)的語言,必定具有以下特徵:
1. 對象唯一性
2. 抽象性
3. 繼承性
4. 多態性
而原型鏈最大的目的, 就是為了實現繼承。
進階:prototype 和 proto
原型鏈究竟是如何實現繼承的呢?首先,我們要引入介紹兩兄弟:prototype 和 __proto__,這是在 JavaScript 中無處不在的兩個變量(如果你經常調試的話),然而,這兩個變量並不是在所有的對象上都存在,先看一張表:
| 對象類型 | prototype |
__proto__ |
|---|---|---|
| 普通對象 | × | √ |
| 函數對象 | √ | √ |
首先,我們先給出以下結論:
只有 函數對象 具有 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 的實例?
是時候去掉應該讓它成立了。那麼此刻,just 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;(複習)
- instance.__proto__ 指向函數對象的 prototype ;(複習)
- 普通對象的 proto 指向 Object.prototype;(複習)
- 普通對象沒有 prototype 屬性;(複習)
- 在訪問一個對象的某個屬性/方法時,若在當前對象上找不到,則會嘗試訪問 ob.__proto__, 也就是訪問該對象的構造函數的原型 obCtr.prototype,若仍找不到,會繼續查找 obCtr.prototype.__proto__,像這樣依次查找下去。若在某一刻,找到了該屬性,則會立刻返回值並停止對原型鏈的搜索,若找不到,則返回 undefined。
為了檢驗你對上述的理解,請分析下述兩個問題:
1. 以下代碼的輸出結果是?
console.log(ulivz.__proto__ === Function.prototype)
答案: false
2. `Person.__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。
原文作者