示例程式碼託管在:http://www.github.com/dashnowords/blogs
部落格園地址:《大史住在大前端》原創博文目錄
華為雲社群地址:【你要的前端打怪升級指南】
好的程式碼都差不多,爛的程式碼卻各有各的爛法。
一. 概述
原型鏈是javascript非常重要的基礎知識。最近在閱讀node.js
,發現許多程式碼乍一看會覺得很費解,但細細品味之後會覺得非常優雅,對於程式碼細節的把控和效能的考量讓人覺得讚歎。不得不說看大師級的作品真的是一種享受。本篇中我將以cluster
模組中子程式管理物件Worker類
的實現為例,帶你一起看看堪稱藝術的程式碼是如何像手術一樣操作原型鏈,同時理解本節的知識點對於下一篇cluster
模組的學習壓力。
二. 原型鏈基礎知識
javascript中存在兩種原型概念——內建[[prototype]]
屬性指向的物件和prototype
原型物件,prototype
原型物件上掛載著例項上的公共方法和屬性,[[prototype]]
屬性可以通過__proto__
屬性來訪問(雖然暴露了這個屬性但不推薦使用,平時更多使用Object.getPrototypeOf( )
方法來獲取,也可以通過Object.setPrototypeOf( )
來修改,本文中為了書寫方便繼續用__proto__
),所一個例項的[[prototype]]
屬性指向的並不一定是自己構造方法對應的prototype
原型物件。
javascript中通過new
運算子來生成物件,生成的物件的[[prototype]]
屬性會以一種串聯的方式指向多個建構函式的原型物件,以便可以獲取可被共享使用的方法,如下所示:
當我們需要實現功能繼承時,最簡單的做法就是在子類的建構函式裡生成一個父類的例項,然後令例項的__proto__
屬性指向這個例項,但這樣做會使得父類上一些本應被新增在例項上的屬性和方法被新增到了原型鏈上,而不是真正的子類例項上,而繼承的目的主要是為了獲取父類的提供的公共的原型方法,所以ES6
的extends
語法糖實現的繼承效果就是下面這個樣子的,後文中我們會看到Worker
的原型鏈也是按照這樣的方式來修剪的:
三. 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行垃圾程式碼也無法學習到的。