目錄javascript
示例代碼託管在:http://www.github.com/dashnowords/blogshtml
博客園地址:《大史住在大前端》原創博文目錄前端
華爲雲社區地址:【你要的前端打怪升級指南】java
好的代碼都差很少,爛的代碼卻各有各的爛法。node
原型鏈是javascript很是重要的基礎知識。最近在閱讀node.js
,發現許多代碼乍一看會以爲很費解,但細細品味以後會以爲很是優雅,對於代碼細節的把控和性能的考量讓人以爲讚歎。不得不說看大師級的做品真的是一種享受。本篇中我將以cluster
模塊中子進程管理對象Worker類
的實現爲例,帶你一塊兒看看堪稱藝術的代碼是如何像手術同樣操做原型鏈,同時理解本節的知識點對於下一篇cluster
模塊的學習壓力。git
javascript中存在兩種原型概念——內置[[prototype]]
屬性指向的對象和prototype
原型對象,prototype
原型對象上掛載着實例上的公共方法和屬性,[[prototype]]
屬性能夠經過__proto__
屬性來訪問(雖然暴露了這個屬性但不推薦使用,平時更多使用Object.getPrototypeOf( )
方法來獲取,也能夠經過Object.setPrototypeOf( )
來修改,本文中爲了書寫方便繼續用__proto__
),所一個實例的[[prototype]]
屬性指向的並不必定是本身構造方法對應的prototype
原型對象。github
javascript中經過new
運算符來生成對象,生成的對象的[[prototype]]
屬性會以一種串聯的方式指向多個構造函數的原型對象,以即可以獲取可被共享使用的方法,以下所示:算法
當咱們須要實現功能繼承時,最簡單的作法就是在子類的構造函數裏生成一個父類的實例,而後令實例的__proto__
屬性指向這個實例,但這樣作會使得父類上一些本應被添加在實例上的屬性和方法被添加到了原型鏈上,而不是真正的子類實例上,而繼承的目的主要是爲了獲取父類的提供的公共的原型方法,因此ES6
的extends
語法糖實現的繼承效果就是下面這個樣子的,後文中咱們會看到Worker
的原型鏈也是按照這樣的方式來修剪的:函數
Worker
的源代碼在官方倉庫的lib/internal/worker.js
,代碼只有50行,用IDE摺疊起來先瀏覽一下:性能
咱們分析一下它的運做機制,首先聲明瞭Worker
這個類,此時它對應的原型鏈以下:
爲了Worker
擁有消息收發的能力,須要讓它從EventEmitter
類來繼承發佈訂閱能力,因此這裏將EventEmitter.prototype
對象添加到Worker
的原型鏈中:
Object.setPrototypeOf(Worker.prototype, EventEmitter.prototype);
這時的原型鏈就變成了下面的樣子,也就是和ES6
中extends
關鍵字的實現的繼承是一致的:
接下來的這句就有些費解,看起來好像沒起到什麼做用,你能夠本身思考一下,最後咱們再揭曉答案:
Object.setPrototypeOf(Worker,EventEmitter);
一圖勝千言,直接看原型鏈結果:
這裏的加工使得Worker
構造方法的__proto__
從Worker.prototype
改變到了EventEmitter
構造方法,這使得原型鏈直接變成一個三叉形,看起來很是奇怪,並且看起來Worker
和它的原型對象Worker.prototype
之間斷開了聯繫,若是此時讓你生成一個worker
實例,你能清楚地說出它的原型鏈是什麼樣子嗎?
咱們先繼續日後看,後面的代碼在Worker.prototype
上添加了一些原型方法,使得原型鏈再一次變形:
至此,原型鏈就調整結束了,下一節咱們開始看Worker
如何生成實例。
worker
的實例化是在lib/internal/cluster/master.js
中,也就是主線程中生成子線程時調用的,調用的語句是:
const worker = new Worker({ id: id, process: workerProcess });
也就是說它是經過new
操做符來生成實例的。Worker
構造方法中的核心語句以下:
function Worker(options){ if(!(this instanceof Worker)){ return new Worker(options) } EventEmitter.call(this); }
首先對於this
的判斷是用來限制Worker
只能做爲構造函數使用,由於此時this
會指向實例,若是this
並非Worker
的實例,就說明Worker是做爲方法調用的,此時會自動用new
操做符來生成實例,若是你它的機制還不清楚,能夠先閱讀如下Mozilla開發者文檔(【MDN中對於new算法的描述】),基本算法是這樣的:
1.生成一個新的空對象; 2.將空對象的.__proto__指向構造函數的原型對象; 3.將這個空對象綁定爲this指向而後傳入構造函數來運行; 4.若是構造函數有返回值,則將返回值做爲實例返回,若是沒有則將以前生成的空對象做爲實例返回。
按照上面的描述,當函數被執行到Worker
構造方法的函數體中時,原型鏈是下面這樣的:
接下來執行的是:
EventEmitter.call(this);
也就是將實例做爲this
透傳到EventEmitter
構造方法中去執行,在官方文檔中能夠找到它實際上執行的是EventEmitter.init
方法,語句只有幾行,但很是有意思:
EventEmitter.init = function(){ if (this._events === undefined || this._events === Object.getPrototypeOf(this)._events) { this._events = Object.create(null); this._eventsCount = 0; } }
若是實例上沒有_events
屬性,或者它的_events
屬性存在於本身的原型鏈上,那麼就使用Object.create(null)
生成一個空對象,就直接在實例上添加_events
屬性和_eventsCount
屬性並賦值。空對象字面量和Object.create(null)
生成的對象原型鏈是不同的:
後者生成的對象原型鏈更短,對象的本質是一種散列結構,你新生成的對象極可能只是用來存儲一些鍵值對的映射關係而並非爲了當作對象實例在使用,後一種結構在查找某個屬性時須要遍歷的屬性就更少,效率也會高一些。
至此實例就生成完畢了,它最終的原型鏈是下面這樣的:
能夠看到Worker
雖然繼承了EventEmitter
的消息收發能力,可是卻並無生成完整的EventEmitter
實例,而只是將必須擁有的實例屬性添加在了子類的實例對象上,在實現能力的同時也保持原型鏈結構的最小化,避免冗餘,這一波乾淨利落的原型鏈加工真的太秀了,不得不說node.js
的細節處理真的堪稱藝術。
前面咱們還遺留了一個問題,還記得嗎?
Object.setPrototypeOf(Worker,EventEmitter)
你能夠很清楚地看到實例的原型鏈和上面這條語句實現的功能沒什麼關係。事實上它的做用是爲了讓子類繼承父類的靜態方法,一張圖就能解決的問題,我就再也不多bibi了:
這裏的目的就是爲了儘量完整地實現面向對象的特性,使得你能夠直接經過Worker
構造函數來訪問到EventEmitter
上的靜態屬性和方法,你能夠在本文提供的demo中看到。
閱讀經典源碼是一個很是緩慢且吃力的事情,尤爲是沒人帶沒人交流時,可是若是開始了,就請必定保持耐心。好比上面的代碼僅僅是cluster
模塊中很小的一部分,只有短短50行,若是基礎薄弱可能要花好久才能消化其中的東西,可是它可以教給你的原型鏈知識和對開發細節的把控能力,是你讀5000行垃圾代碼也沒法學習到的。