# 【nodejs原理&原始碼賞析(3)】欣賞手術級的原型鏈加工藝術

大史不說話發表於2019-05-24

# 【nodejs原理&原始碼賞析(3)】欣賞手術級的原型鏈加工藝術

示例程式碼託管在: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]]屬性會以一種串聯的方式指向多個建構函式的原型物件,以便可以獲取可被共享使用的方法,如下所示:

# 【nodejs原理&原始碼賞析(3)】欣賞手術級的原型鏈加工藝術

當我們需要實現功能繼承時,最簡單的做法就是在子類的建構函式裡生成一個父類的例項,然後令例項的__proto__屬性指向這個例項,但這樣做會使得父類上一些本應被新增在例項上的屬性和方法被新增到了原型鏈上,而不是真正的子類例項上,而繼承的目的主要是為了獲取父類的提供的公共的原型方法,所以ES6extends語法糖實現的繼承效果就是下面這個樣子的,後文中我們會看到Worker的原型鏈也是按照這樣的方式來修剪的:

# 【nodejs原理&原始碼賞析(3)】欣賞手術級的原型鏈加工藝術

三. Worker類的原型鏈加工

Worker的原始碼在官方倉庫的lib/internal/worker.js,程式碼只有50行,用IDE摺疊起來先瀏覽一下:

# 【nodejs原理&原始碼賞析(3)】欣賞手術級的原型鏈加工藝術

我們分析一下它的運作機制,首先宣告瞭Worker這個類,此時它對應的原型鏈如下:

# 【nodejs原理&原始碼賞析(3)】欣賞手術級的原型鏈加工藝術

為了Worker擁有訊息收發的能力,需要讓它從EventEmitter類來繼承釋出訂閱能力,所以這裡將EventEmitter.prototype物件新增到Worker的原型鏈中:

Object.setPrototypeOf(Worker.prototype, EventEmitter.prototype);

這時的原型鏈就變成了下面的樣子,也就是和ES6extends關鍵字的實現的繼承是一致的:

# 【nodejs原理&原始碼賞析(3)】欣賞手術級的原型鏈加工藝術

接下來的這句就有些費解,看起來好像沒起到什麼作用,你可以自己思考一下,最後我們再揭曉答案:

Object.setPrototypeOf(Worker,EventEmitter);

一圖勝千言,直接看原型鏈結果:

# 【nodejs原理&原始碼賞析(3)】欣賞手術級的原型鏈加工藝術

這裡的加工使得Worker構造方法的__proto__Worker.prototype改變到了EventEmitter構造方法,這使得原型鏈直接變成一個三叉形,看起來非常奇怪,而且看起來Worker和它的原型物件Worker.prototype之間斷開了聯絡,如果此時讓你生成一個worker例項,你能清楚地說出它的原型鏈是什麼樣子嗎?

我們先繼續往後看,後面的程式碼在Worker.prototype上新增了一些原型方法,使得原型鏈再一次變形:

# 【nodejs原理&原始碼賞析(3)】欣賞手術級的原型鏈加工藝術

至此,原型鏈就調整結束了,下一節我們開始看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構造方法的函式體中時,原型鏈是下面這樣的:

# 【nodejs原理&原始碼賞析(3)】欣賞手術級的原型鏈加工藝術

接下來執行的是:

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)生成的物件原型鏈是不一樣的:

# 【nodejs原理&原始碼賞析(3)】欣賞手術級的原型鏈加工藝術

後者生成的物件原型鏈更短,物件的本質是一種雜湊結構,你新生成的物件很可能只是用來儲存一些鍵值對的對映關係而並不是為了當做物件例項在使用,後一種結構在查詢某個屬性時需要遍歷的屬性就更少,效率也會高一些。

至此例項就生成完畢了,它最終的原型鏈是下面這樣的:

# 【nodejs原理&原始碼賞析(3)】欣賞手術級的原型鏈加工藝術

可以看到Worker雖然繼承了EventEmitter的訊息收發能力,但是卻並沒有生成完整的EventEmitter例項,而只是將必須擁有的例項屬性新增在了子類的例項物件上,在實現能力的同時也保持原型鏈結構的最小化,避免冗餘,這一波乾淨利落的原型鏈加工真的太秀了,不得不說node.js的細節處理真的堪稱藝術。

五. 最後一個問題

前面我們還遺留了一個問題,還記得嗎?

Object.setPrototypeOf(Worker,EventEmitter)

你可以很清楚地看到例項的原型鏈和上面這條語句實現的功能沒什麼關係。事實上它的作用是為了讓子類繼承父類的靜態方法,一張圖就能解決的問題,我就不再多bibi了:

# 【nodejs原理&原始碼賞析(3)】欣賞手術級的原型鏈加工藝術

這裡的目的就是為了儘可能完整地實現物件導向的特性,使得你可以直接通過Worker建構函式來訪問到EventEmitter上的靜態屬性和方法,你可以在本文提供的demo中看到。

六. 一些心得

閱讀經典原始碼是一個非常緩慢且吃力的事情,尤其是沒人帶沒人交流時,但是如果開始了,就請一定保持耐心。比如上面的程式碼僅僅是cluster模組中很小的一部分,只有短短50行,如果基礎薄弱可能要花很久才能消化其中的東西,但是它能夠教給你的原型鏈知識和對開發細節的把控能力,是你讀5000行垃圾程式碼也無法學習到的。

相關文章