天天看點

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

【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__

屬性指向這個執行個體,但這樣做會使得父類上一些本應被添加在執行個體上的屬性和方法被添加到了原型鍊上,而不是真正的子類執行個體上,而繼承的目的主要是為了擷取父類的提供的公共的原型方法,是以

ES6

extends

文法糖實作的繼承效果就是下面這個樣子的,後文中我們會看到

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);           

複制

這時的原型鍊就變成了下面的樣子,也就是和

ES6

extends

關鍵字的實作的繼承是一緻的:

【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行垃圾代碼也無法學習到的。