在前兩章,為了友善調試,我們寫了一個非常簡單的 jQuery.fn.init 方法:
是以我們在 demo 裡執行 $('div') 時可以取得這麼一個類數組對象:
在完整的 jQuery 中通過 $(selector) 的形式擷取的對象也基本如此 —— 它是一個對象而非數組,但可以通過下标(如 $div[index] )或 .get(index) 接口來擷取到相應的 DOM 對象,也可以直接通過 .length 來擷取比對到的 DOM 對象總數。
這麼實作的原因是 —— 友善,該對象畢竟是 jQuery 執行個體,繼承了所有的執行個體方法,同時又直接是所檢索到的DOM集合(而不需要通過 $div.getDOMList() 之類的方法來擷取),簡直一石二鳥。
如下圖所示便是一個很尋常的 JQ 類數組對象(初始化執行的代碼是 $('div') ):

1. Sizzle 引入
在 jQuery 中,檢索DOM的能力來自于 Sizzle 引擎,它是 JQ 最核心也是最複雜的部分,在後續有機會我們再對其作詳細介紹,目前階段,我們隻需要直接“擷取”并“使用”它即可。
Sizzle 是開源的選擇器引擎,其官網是 http://sizzlejs.com/ ,直接在首頁便能下載下傳到最新版本。
我們在 src 目錄下新增一個 /sizzle 檔案夾,并把下載下傳到的 sizzle.js 放進去(即存放為 src/sizzle/sizzle.js ),接着得對其做點小修改,使其得以适應我們 rollup 的打包模式。
其原先代碼為:
将這段代碼的頭和尾替換為:
同時新增一個初始化檔案 src/sizzle/init.js ,用于把 Sizzle 賦予靜态接口 jQuery.find:
别忘了在打包的入口檔案裡引入該子產品并執行:

打包後我們就能愉快地通過 jQuery.find 接口來使用 Sizzle 的各種能力了(使用方式可以參考 Sizzle 的API文檔):
留意 $.find(XXX) 傳回的是一個比對到的 DOM 集合的數組(注意類型直接就是Array,不是 document.querySelectorAll 那樣傳回的 nodeList )。
我們需要多做一點處理,來将這個數組轉換為前頭提到的類數組JQ對象。
另外,雖然現在 JQ 的工具方法有了檢索DOM的能力,但其執行個體方法是木有的,鑒于構造器的靜态屬性不會繼承給執行個體,會導緻我們沒法鍊式地來支援 find,比如:
很明顯,這可以在 jQuery.fn.extend 裡多加一個 find 接口來實作,不過不着急,咱們一步一步來。

2. $.merge 方法
針對上述的第一個需求點,我們修改下 src/core.js ,往 jQuery.extend 裡新增一個 jQuery.merge 靜态方法,友善把檢索到的 DOM 集合數組轉換為類數組對象:
merge 的代碼段太好了解了,其實作的能力為:
運作輸出:
是以,如果我們在 jQuery.fn.init 中,把 this 傳入為 $.merge 的 first 參數(留意這裡this為JQ執行個體對象自身,預設 length 執行個體屬性為0),再把檢索到的 DOM 集合數組作為 second 參數傳入,那麼就能愉快地得到我們想要的 JQ 類數組對象了。
我們簡單地修改下 src/init.js :
我們打包後執行:
輸出正是我們所想要的類數組對象:

3. 擴充 $.fn.find
針對第二個需求點 —— 鍊式支援 find 接口,我們需要給 $.fn 擴充一個 find 方法:
這裡我們依舊直接使用了 Sizzle 接口 —— 當帶上了第三個參數(數組類型)時,Sizzle 會把檢索到的 DOM 集合注入到該參數中去(API文檔)。
我們打包後執行下方代碼:
效果如下:
可以看到,我們要的子元素是出來了,不過呢,這裡擷取到的是純數組,而非 JQ 對象,處理方法很簡單 —— 直接調用前面剛加上的 $.merge 方法即可。
另外也有個問題,一旦咱們擷取到了子孫元素(如上方代碼中的span),那麼如果我們需要重新取到其祖先元素(如上方代碼中的div),就又得重新去走 $('div') 來檢索了,這樣麻煩且效率不高。
而我們知道,在 jQuery 中是有一個 $.fn.end 方法可以傳回上一次檢索到的 JQ 對象的:
處理方法也很簡單,參考浏覽器的曆史記錄棧,我們也來寫一個遵循後進先出的棧操作方法。
可能你在第一時間會想到,是否使用一個數組,通過 push 和 pop 來實作入棧和出棧的功能。
事實上我們有更簡單的形式 —— 給新的 JQ 對象新增一個 .prevObject 屬性并指向舊 JQ 對象,這樣一來,我們想擷取目前 JQ 對象之前的一次 JQ 對象,通過該屬性就能直接取到了:
這樣通過 pushStack 接口包裝下,就解決了上面說的兩個問題,我們改下 $.fn.find 代碼:
從性能上考慮,我們這樣寫會更好一些(減少一些merge裡的周遊):

4. $.fn.end、$.fn.eq 和 $.fn.get
鑒于我們在 pushStack 中加上了 oldJQ.prevObject 的關系鍊,那麼 $.fn.end 接口的實作就太簡單了:
直接傳回上一次檢索到的JQ對象(如果木有,則傳回一個空的JQ對象)。
這裡順便再多添加兩個大家熟悉的不能再熟悉的 $.fn.eq 和 $.fn.get 工具方法,代碼非常的簡單:
通過 eq 接口我們可以知道,後續任何方法,如果要傳回一個 JQ 對象,基本都需要裹一層 pushStack 做處理,來確定 prevObject 的正确引用。
當然,這也輕松衍生了 $.fn.first 和 $.fn.last 兩個工具方法:

本章就先寫到這裡,避免太多内容難消化。事實上,我們的 $.fn.init 、$.find 和 $.fn.find 都還有一些不完善的地方:
1. $.fn.init 方法沒有兼顧到各種參數類型的情況,也還沒有加上第二個參數 context 來做上下文預設;
2. 同上,$.fn.find 也未對兼顧到各種參數類型的情況;
3. $.fn.find 傳回結果有可能帶有重複的 DOM,例如:
這些存在的問題我們都會在後面的篇章做進一步的優化。
另外提幾個點:
1. 部分讀者是從公衆号上閱讀本系列文章的,建議也要同時關注本人部落格好一些 —— 有時我會對文章做一些更改,讓其更易讀懂;
2. 對于前兩篇文章,部分基礎較差的讀者貌似不太好了解,我其實有考慮寫個番外篇來幫你們梳理這塊(特别是原型鍊的)知識點,如果覺得有需要的話可以留言給我,要求的人多的話我就動筆了;
3. 工作較忙,發文頻率大約是1到2周一篇文章。近期其實蠻多讀者催我更文的,但為了保持文章品質,需要多點時間,不希望數量上來了品質卻下去了。
本文的代碼挂在我的github上,有需要的同學可以自行下載下傳調試。共勉~