萬物皆空之 JavaScript 原型

本文首發於我的 Github,歡迎 issue / fxxk。前端

前言

ES6 的第一個版本發佈於 156 月,而本文最先創做於 16 年,那也是筆者從事前端的早期。在那個時候,ES6 的衆多特性仍處於 stage 階段,也遠沒有如今這麼普及,爲了更輕鬆地寫JavaScript,筆者曾花費了整整一天,仔細理解了一下原型——這個對於一個成熟的JavaScript開發者必需要跨越的大山。git

ES6帶來了太多的語法糖,其中箭頭函數掩蓋了 this 的神妙,而 class 也掩蓋了本文要長篇談論的 原型github

最近,我重寫了這篇文章,經過本文,你將能夠學到:windows

  • 如何用 ES5 模擬類;
  • 理解 prototype__proto__
  • 理解原型鏈和原型繼承;
  • 更深刻地瞭解 JavaScript 這門語言。

引入:普通對象與函數對象

JavaScript 中,一直有這麼一種說法,萬物皆對象。事實上,在 JavaScript 中,對象也是有區別的,咱們能夠將其劃分爲 普通對象函數對象ObjectFunction 即是 JavaScript 自帶的兩個典型的 函數對象。而函數對象就是一個純函數,所謂的 函數對象,其實就是使用 JavaScript模擬類瀏覽器

那麼,究竟什麼是普通對象,什麼又是函數對象呢?請看下方的例子:函數

首先,咱們分別建立了三個 FunctionObject 的實例: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
複製代碼

在上述的例子中,ob1ob2ob3 爲普通對象(均爲 Object 的實例),而 fn1fn2fn3 均是 Function 的實例,稱之爲 函數對象this

如何區分呢?其實記住這句話就好了:spa

  • 全部Function的實例都是函數對象,而其餘的都是普通對象

說到這裏,細心的同窗會發表一個疑問,一開始,咱們已經提到,ObjectFunction 均是 函數對象,而這裏咱們又說:全部Function的實例都是函數對象,難道 Function 也是 Function 的實例?

先保留這個疑問。接下來,對這一節的內容作個總結:

image_1b4867lll1fqfiqt14o17gccjb1m.png-58.3kB

從圖中能夠看出,對象自己的實現仍是要依靠構造函數。那 原型鏈 究竟是用來幹嗎的呢?

衆所周知,做爲一門面向對象(Object Oriented)的語言,一定具備如下特徵:

  • 對象惟一性
  • 抽象性
  • 繼承性
  • 多態性

而原型鏈最大的目的, 就是爲了實現繼承


進階:prototype 和 __proto__

原型鏈到底是如何實現繼承的呢?首先,咱們要引入介紹兩兄弟:prototype__proto__,這是在 JavaScript 中無處不在的兩個變量(若是你常常調試的話),然而,這兩個變量並非在全部的對象上都存在,先看一張表:

對象類型 prototype __proto__
普通對象(NO)
函數對象(FO)

首先,咱們先給出如下結論:

  1. 只有 函數對象 具備 prototype 這個屬性;
  2. 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
複製代碼

既然是語言層面的預置屬性,那麼二者究竟有何區別呢?咱們依然從結論出發,給出如下兩個結論:

  1. prototype 被實例的 __proto__ 所指向(被動)
  2. __proto__ 指向構造函數的 prototype(主動)

哇,也就是說如下代碼成立:

console.log(fn.__proto__ === Function.prototype); // true
console.log(ob.__proto__ === Object.prototype); // true
複製代碼

看起來很酷,結論瞬間被證實,感受是否是很爽,那麼問題來了:既然 fn 是一個函數對象,那麼 fn.prototype.__proto__ 到底等於什麼?

這是我嘗試去解決這個問題的過程:

  1. 首先用 typeof 獲得 fn.prototype 的類型:"object"
  2. 哇,既然是 "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. 執行 1,建立了一個構造函數 Person,要注意,前面已經提到,此時 Person.prototype 已經被自動建立,它包含 constructor__proto__這兩個屬性;
  2. 執行2,給對象 Person.prototype 增長了一個方法 getName()
  3. 執行3,給對象 Person.prototype 增長了一個方法 getAge()
  4. 執行4, 由構造函數 Person 建立了一個實例 ulivz,值得注意的是,一個構造函數在實例化時,必定會自動執行該構造函數。
  5. 在瀏覽器獲得 5 的輸出,即 ulivz 應該是:
{
     name: 'ulivz',
     age: 24
     __proto__: Object // 實際上就是 `Person.prototype`
}
複製代碼

結合上一節的經驗,如下等式成立:

console.log(ulivz.__proto__ == Person.prototype)  // true
複製代碼
  1. 執行6的時候,因爲在 ulivz 中找不到 getName()getAge() 這兩個方法,就會繼續朝着原型鏈向上查找,也就是經過 __proto__ 向上查找,因而,很快在 ulviz.__proto__ 中,即 Person.prototype 中找到了這兩個方法,因而中止查找並執行獲得結果。

這即是 JavaScript 的原型繼承。準確的說,JavaScript 的原型繼承是經過 __proto__ 並藉助 prototype 來實現的。

因而,咱們能夠做以下總結:

  1. 函數對象的 __proto__ 指向 Function.prototype;(複習)
  2. 函數對象的 prototype 指向 instance.__proto__;(複習)
  3. 普通對象的 __proto__ 指向 Object.prototype;(複習)
  4. 普通對象沒有 prototype 屬性;(複習)
  5. 在訪問一個對象的某個屬性/方法時,若在當前對象上找不到,則會嘗試訪問 ob.__proto__, 也就是訪問該對象的構造函數的原型 obCtr.prototype,若仍找不到,會繼續查找 obCtr.prototype.__proto__,像依次查找下去。若在某一刻,找到了該屬性,則會馬上返回值並中止對原型鏈的搜索,若找不到,則返回 undefined

爲了檢驗你對上述的理解,請分析下述兩個問題:

  1. 如下代碼的輸出結果是?
console.log(ulivz.__proto__ === Function.prototype)
複製代碼
查看結果 false

  1. 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 */
複製代碼

咱們來畫一個原型鏈圖,或者說,將其整個原型鏈圖畫出來?請看下圖:

原型鏈.png-41.2kB

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大內置(函數)對象的原型繼承

經過前文的論述,結合相應的代碼驗證,整理出如下原型鏈圖:

image_1b496ie7el7m1rvltoi17he1b459.png-52.6kB

因而可知,咱們更增強化了這兩個觀點:

  1. 任何內置函數對象(類)自己的 __proto__ 都指向 Function 的原型對象;
  2. 除了 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 
複製代碼

總結

來幾句短總結:

  1. A 經過new建立了B,則 B.__proto__ = A.prototype
  2. __proto__是原型鏈查找的起點;
  3. 執行B.a,若在B中找不到a,則會在B.__proto__中,也就是A.prototype中查找,若A.prototype中仍然沒有,則會繼續向上查找,最終,必定會找到Object.prototype,假若還找不到,由於Object.prototype.__proto__指向null,所以會返回undefined
  4. 爲何萬物皆空,仍是那句話,原型鏈的頂端,必定有Object.prototype.__proto__ ——> null

最後,給你留下一個疑問:

  • 如何用 JavaScript 實現類的繼承呢?

請看個人原型系列的下一篇《深刻JavaScript繼承原理》 分曉。

以上,全文終。

注:此外本文屬於我的總結,部分表達可能會有疏漏之處,若是您發現本文有所欠缺,爲避免誤人子弟,請放心大膽地在評論中指出,或者給我提 issue,感謝~

相關文章
相關標籤/搜索