天天看點

深入解析Underscore.js源碼架構

Underscore.js是很有名的一個工具庫,我也經常用他來處理對象,數組等,本文會深入解析Underscore源碼架構,跟大家一起學習下他源碼的亮點,然後模仿他寫一個簡單的架子來加深了解。他的源碼通讀下來,我覺得他的亮點主要有如下幾點:

不需要new的構造函數

同時支援靜态方法調用和執行個體方法調用

支援鍊式調用

本文的例子已經上傳到GitHub,同一個repo下還有我全部的博文和例子,求個star:

https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Architecture/Underscore

Underscore外層就是一個自執行函數,在自執行函數裡面将<code>_</code>挂載到了window上。這是很多第三方庫慣用的套路。如果你還不知道怎麼入手看源碼,不知道入口在哪裡,或者看不懂他的外層結構,請看從架構入手輕松讀懂架構源碼:以jQuery,Zepto,Vue和lodash-es為例,這篇文章詳細講解了怎麼入手看源碼。本文主要講解Underscore源碼架構裡面的亮點,怎麼入手就不再贅述了。

我們在使用第三方庫的時候,經常需要先拿一個他們的執行個體,有些庫需要用new來顯式的調用,比如原生的Promise,有些庫不需要new也可以拿到執行個體對象,比如jQuery。不用new就傳回一個執行個體原生JS肯定是不支援的,有這些特性的庫都是自己封裝了一層的。不同的庫在封裝的時候也有不同的思路,下面我們來講講其中兩種方案。

之前我在另一篇文章從架構入手輕松讀懂架構源碼:以jQuery,Zepto,Vue和lodash-es為例中詳細講解了jQuery是怎麼實作不用new就傳回一個執行個體的。另外還模仿jQuery的這種方案實作了我自己的一個工具庫:學以緻用:手把手教你撸一個工具庫并打包釋出,順便解決JS小數計算不準問題。這裡貼一段我工具庫文章的代碼簡單回顧下這種方案:

jQuery的方案是在構造函數裡面new了另外一個對象,然後将這個對象的原型指向jQuery的原型,以便傳回的執行個體能夠通路jQuery的執行個體方法。目的是能夠達到的,但是方案顯得比較冗長,Underscore的方案就簡潔多了:

上面代碼的輸出是:

可以看到constructor指向的是<code>_()</code>,說明這真的是一個_的執行個體,我們來分析下代碼執行流程:

調用<code>_()</code>,裡面的this指向外層的作用域,我們這裡是window,因為window不是_的執行個體,會走到if裡面去。關于this指向,如果你還不是很明白,請看這篇文章。 if裡面會調用<code>new _()</code>,這會拿到一個執行個體對象,并将這個對象<code>return</code>出去。<code>new _()</code>也會調到<code>_()</code>方法,但是因為使用new調用,裡面的this指向的就是new出來的執行個體,是以if進不去,執行結束。

Underscore巧妙應用了this的指向,通過檢測this的指向來判斷你是new調用的還是普通調用的,如果是普通調用就幫你new一下再傳回。

用過Underscore的朋友應該有注意到,對于同一個方法來說,Underscore既支援作為靜态方法調用,也支援作為執行個體方法調用,下面是官方的例子:

當我們把方法作為靜态方法調用的時候,需要處理的資料就是第一個參數;當把他作為執行個體方法調用的時候,待處理資料是作為參數傳給構造函數的。下面我們來講講這是怎麼實作的。

其實最簡單的方法就是寫兩個函數,一個是靜态方法,一個是執行個體方法。但是如果我們這樣做了,這兩個函數内部處理的邏輯其實是高度相似的,可能隻是參數稍微有點不同而已。這肯定不是一個優雅的程式員應該做的。Underscore給出的方法就是所有方法先寫成靜态方法,然後用一個統一的函數來将所有的靜态方法挂載到原型上,讓他成為一個執行個體方法。我們試着一步一步的來實作下。

我們先來寫一個簡單的map方法,将它挂載到_上成為靜态方法:

這個方法寫完其實就可以直接用了,用上面那個例子調用如下:

在Underscore裡面是用一個<code>mixin</code>方法來将靜态方法映射到原型上的,<code>mixin</code>方法接收一個對象作為參數,然後将這個對象上的方法全部複制到原型上。具體流程如下:

取出參數裡面的函數屬性,将其塞入一個數組 周遊這個數組,将裡面的每個項設定到原型上 設定原型的時候注意處理下執行個體方法和靜态方法的參數

下面來看看代碼:

上面的<code>_.mixin(_);</code>調用之後就會将<code>_</code>上的靜态方法全部映射到原型上,這樣<code>_()</code>傳回的執行個體也有了所有的靜态方法,這就讓<code>_</code>支援了兩種調用方式。可能有朋友注意到,我們上面的代碼還有<code>each</code>和<code>functions</code>兩個輔助方法,我們也來實作下這兩個方法:

Underscore的<code>mixin</code>不僅讓他支援了靜态和執行個體方法兩種調用方式,同時因為他自己也是<code>_</code>的一個靜态方法,我們也是可以拿來用的。官方支援自定義插件就是用的這個方法,下面是官方例子:

其實我們前面寫的那個<code>mixin</code>方法已經支援将自定義方法作為執行個體方法了,但是還差一點,還差靜态方法,是以我們再加一行代碼,同時将接收到的參數指派給<code>_</code>就行了:

鍊式調用也很常見,比如jQuery的點點點,我在另一篇文章學以緻用:手把手教你撸一個工具庫并打包釋出,順便解決JS小數計算不準問題詳細講解過這種執行個體方法的鍊式調用怎麼實作,關鍵是每個執行個體方法計算完成後都傳回目前執行個體,對于執行個體方法來說,目前執行個體就是this。這種方式也适用于Underscore,但是Underscore因為自身需求和API結構的原因,他的鍊式調用需要支援更多場景:

Underscore的執行個體方法還支援直接調用傳回結果,不能簡單的傳回執行個體 Underscore的靜态方法也要支援鍊式調用

我們一步一步來,先來解決執行個體方法支援鍊式調用的問題,我們前面已經實作了将靜态方法映射成執行個體方法,前面實作的執行個體方法的傳回值就是靜态方法的傳回值。為了實作鍊式調用,我們還需要執行個體方法計算完後還能夠傳回目前執行個體(也就是this),是以我們需要一個依據來判斷應該傳回計算結果還是目前執行個體。這個依據在Underscore裡面是要使用者給的,也就是顯式調用<code>chain</code>方法。依據我們的分析,<code>chain</code>應該很簡單,給一個依據來判斷執行個體方法應該傳回啥,也就是給目前執行個體設定一個标志位:

<code>chain</code>就是這麼簡單,兩行代碼,然後我們的執行個體方法裡面根據_chain來判斷傳回計算結果還是目前執行個體:

我們再來寫個<code>unique</code>方法來驗證下鍊式調用:

試下鍊式調用:

我們發現結果是對的,但是輸出的是一個執行個體,不是我們想要的,是以我們還要一個方法來輸出真正的計算結果,這個方法隻能挂在原型上,不能寫成靜态方法,不然還會走到我們的mixin,會傳回執行個體:

再來試一下呢:

靜态方法也要支援鍊式調用,我們必須要讓他的傳回值也能夠通路到執行個體方法才行。一般情況下靜态方法的傳回值是不能傳回執行個體的,但是我們現在已經有了<code>chain</code>方法,我們直接讓這個方法構造一個<code>_</code>執行個體傳回就行了,上面的執行個體方法支援鍊式調用是利用了現成的執行個體,傳回的this,但是如果<code>chain</code>傳回一個新執行個體,也是相容上面的,于是<code>chain</code>改為:

這樣我們的靜态方法<code>chain</code>也可以鍊式調用了,資料跟其他靜态方法一樣作為參數傳給<code>chain</code>:

到這裡我們的功能基本實作了,但是<code>mixin</code>函數還有需要優化的地方:

<code>var res = func.apply(this, args);</code>這裡的this指向的是目前執行個體,但是一個方法作為靜态方法調用時,比如<code>_.map()</code>,方法裡面的this指向的是<code>_</code>,是以這裡應該改成<code>_</code>。之前這裡傳this是因為<code>chain</code>裡面操作的是this,現在已經改成建立執行個體,就不用傳this,是以改為正确的<code>_</code>。

對<code>item</code>進行了特異性判斷,前面之是以這麼做,也是因為<code>chain</code>裡面操作的是this,是以在apply裡面其實已經設定了<code>this._chain</code>為true,是以會走到if裡面去,現在建立執行個體了,走到apply的時候,設定的其實是<code>res._chain</code>,是以不會進到if,要調下一個執行個體方法的時候,<code>this._chain</code>才會是true,是以這個if可以直接去掉了。

Underscore裡面還将<code>isChain</code>的判斷單獨提成了一個方法,我這裡沒這麼做了,放在一起看着還直覺點。

本文主要講解了Underscore源碼的架構,并自己實作了一個簡單的架子,部分變量名字和方法的具體實作可能不一樣,但是原理是一樣的。通過搭建這個簡單的架子,其實我們學會了:

不用new構造執行個體對象

<code>mixin</code>怎麼擴充靜态方法到原型上

通過顯式的調用<code>chain</code>來支援靜态方法和執行個體方法的鍊式調用

文章的最後,感謝你花費寶貴的時間閱讀本文,如果本文給了你一點點幫助或者啟發,請不要吝啬你的贊和GitHub小星星,你的支援是作者持續創作的動力。

歡迎關注我的公衆号進擊的大前端第一時間擷取高品質原創~

“前端進階知識”系列文章:https://juejin.im/post/5e3ffc85518825494e2772fd

“前端進階知識”系列文章源碼GitHub位址: https://github.com/dennis-jiang/Front-End-Knowledges

深入解析Underscore.js源碼架構

繼續閱讀