本文首發於我的 Github,歡迎 issue / fxxk。前端
ES6
的第一個版本發佈於 15
年 6
月,而本文最先創做於 16
年,那也是筆者從事前端的早期。在那個時候,ES6
的衆多特性仍處於 stage
階段,也遠沒有如今這麼普及,爲了更輕鬆地寫JavaScript
,筆者曾花費了整整一天,仔細理解了一下原型——這個對於一個成熟的JavaScript
開發者必需要跨越的大山。git
ES6
帶來了太多的語法糖,其中箭頭函數掩蓋了 this
的神妙,而 class
也掩蓋了本文要長篇談論的 原型
。github
最近,我重寫了這篇文章,經過本文,你將能夠學到:windows
ES5
模擬類;prototype
和 __proto__
;JavaScript
這門語言。在 JavaScript
中,一直有這麼一種說法,萬物皆對象。事實上,在 JavaScript
中,對象也是有區別的,咱們能夠將其劃分爲 普通對象
和 函數對象
。Object
和 Function
即是 JavaScript
自帶的兩個典型的 函數對象
。而函數對象就是一個純函數,所謂的 函數對象
,其實就是使用 JavaScript
在 模擬類
。瀏覽器
那麼,究竟什麼是普通對象
,什麼又是函數對象
呢?請看下方的例子:函數
首先,咱們分別建立了三個 Function
和 Object
的實例:post
function fn1() {}
const fn2 = function() {}
const fn3 = new Function('language', 'console.log(language)')
const ob1 = {}
const ob2 = new Object()
const ob3 = new fn1()
複製代碼
打印如下結果,能夠獲得:ui
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
的實例,稱之爲 函數對象
。this
如何區分呢?其實記住這句話就好了:spa
Function
的實例都是函數對象
,而其餘的都是普通對象
。說到這裏,細心的同窗會發表一個疑問,一開始,咱們已經提到,Object
和 Function
均是 函數對象
,而這裏咱們又說:全部Function
的實例都是函數對象
,難道 Function
也是 Function
的實例?
先保留這個疑問。接下來,對這一節的內容作個總結:
從圖中能夠看出,對象自己的實現仍是要依靠構造函數。那 原型鏈
究竟是用來幹嗎的呢?
衆所周知,做爲一門面向對象(Object Oriented)的語言,一定具備如下特徵:
而原型鏈最大的目的, 就是爲了實現繼承
。
原型鏈到底是如何實現繼承的呢?首先,咱們要引入介紹兩兄弟: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)
複製代碼
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
用來指向引用它的函數對象。
Person.prototype.constructor === Person //true
console.log(Person.prototype.constructor.prototype.constructor === Person) //true
複製代碼
這是一種循環引用。固然你也能夠在上一節的原型鏈圖中畫上去,這裏就不贅述了。
經過前文的論述,結合相應的代碼驗證,整理出如下原型鏈圖:
因而可知,咱們更增強化了這兩個觀點:
- 任何內置函數對象(類)自己的
__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,感謝~