Why underscore
(覺得這部分眼熟的可以直接跳到下一段了...)
最近開始看 underscore.js 源碼,并將 underscore.js 源碼解讀 放在了我的 2016 計劃中。
閱讀一些著名框架類庫的源碼,就好像和一個個大師對話,你會學到很多。為什么是 underscore?最主要的原因是 underscore 簡短精悍(約 1.5k 行),封裝了 100 多個有用的方法,耦合度低,非常適合逐個方法閱讀,適合樓主這樣的 JavaScript 初學者。從中,你不僅可以學到用 void 0 代替 undefined 避免 undefined 被重寫等一些小技巧 ,也可以學到變量類型判斷、函數(shù)節(jié)流&函數(shù)去抖等常用的方法,還可以學到很多瀏覽器兼容的 hack,更可以學到作者的整體設(shè)計思路以及 API 設(shè)計的原理(向后兼容)。
之后樓主會寫一系列的文章跟大家分享在源碼閱讀中學習到的知識。
- underscore-1.8.3 源碼解讀項目地址 https://github.com/hanzichi/underscore-analysis
- underscore-1.8.3 源碼全文注釋 https://github.com/hanzichi/underscore-analysis/blob/master/underscore-1.8.3.js/underscore-1.8.3-analysis.js
- underscore-1.8.3 源碼解讀系列文章 https://github.com/hanzichi/underscore-analysis/issues
歡迎圍觀~ (如果有興趣,歡迎 star & watch~)您的關(guān)注是樓主繼續(xù)寫作的動力
題外話
先說點題外話。
自從 5 月 16 日開始 underscore 系列解讀文章,目前已經(jīng)收獲了 160+ star,在這里子遲也感謝大家的支持,并將繼續(xù)努力分享源碼里的干貨。有朋友私信我說好幾天沒看到更新,在此也請大家原諒,畢竟我把它當成了今年的計劃之一,而且平時也要上班工作,只能利用閑暇時間,而且樓主本人對文章的質(zhì)量要求比較高,如果是一律的流水文章,讀者學不到什么東西,自己的那關(guān)都過不了。其實如果有心,應該能發(fā)現(xiàn) underscore-1.8.3 源碼全文注釋 一直有在更新(注釋行數(shù)已經(jīng)快破 1000 了)。
Main
言歸正傳,上一章 中我們結(jié)束了 Object 擴展方法部分,今天開始來解讀 Array 部分的擴展方法。其實 JavaScript 中的數(shù)組是我最喜歡的類型,能模擬棧、隊列等數(shù)據(jù)結(jié)構(gòu),還能隨意插入元素(splice),非常的靈活,這點做過 leetcode 的應該都深有體會(這里也順便安利下我的 leetcode 題解 Repo https://github.com/hanzichi/leetcode)。
今天要講的是,如何在數(shù)組中尋找元素,對應 underscore 中的 _.findIndex,_.findLastIndex,_.indexOf,_.lastIndexOf 以及 _.sortIndex 方法。
等等,是不是有點眼熟,沒錯,JavaScript 中已經(jīng)部署了 indexOf 方法(ES5)以及 findIndex 方法(ES6),這點不介紹了,大家可以自行學習。
我們先來看 _.findIndex 和 _.findLastIndex 函數(shù)。如果了解過 Array.prototype.findIndex() 方法,會非常容易。_.findIndex 的作用就是從一個數(shù)組中找到第一個滿足某個條件的元素,_.findLastIndex 則是找到最后一個(或者說倒序查找)。
舉個簡單的例子:
var arr = [1, 3, 5, 2, 4, 6];
var isEven = function(num) {
return !(num & 1);
};
var idx = _.findIndex(arr, isEven);
// => 3
直接看源碼,注釋已經(jīng)寫的非常清楚了。這里要注意這個 predicate 函數(shù),其實就是把數(shù)組中的元素傳入這個參數(shù),返回一個布爾值。如果返回 true,則表示滿足這個條件,如果 false 則相反。
// Generator function to create the findIndex and findLastIndex functions
// dir === 1 => 從前往后找
// dir === -1 => 從后往前找
function createPredicateIndexFinder(dir) {
// 經(jīng)典閉包
return function(array, predicate, context) {
predicate = cb(predicate, context);
var length = getLength(array);
// 根據(jù) dir 變量來確定數(shù)組遍歷的起始位置
var index = dir > 0 ? 0 : length - 1;
for (; index >= 0 && index < length; index += dir) {
// 找到第一個符合條件的元素
// 并返回下標值
if (predicate(array[index], index, array)) return index;
}
return -1;
};
}
// Returns the first index on an array-like that passes a predicate test
// 從前往后找到數(shù)組中 `第一個滿足條件` 的元素,并返回下標值
// 沒找到返回 -1
// _.findIndex(array, predicate, [context])
_.findIndex = createPredicateIndexFinder(1);
// 從后往前找到數(shù)組中 `第一個滿足條件` 的元素,并返回下標值
// 沒找到返回 -1
// _.findLastIndex(array, predicate, [context])
_.findLastIndex = createPredicateIndexFinder(-1);
接下來看 _.sortIndex 方法,這個方法無論使用還是實現(xiàn)都非常的簡單。如果往一個有序數(shù)組中插入元素,使得數(shù)組繼續(xù)保持有序,那么這個插入位置是?這就是這個方法的作用,有序,很顯然用二分查找即可。不多說,直接上源碼。
// _.sortedIndex(list, value, [iteratee], [context])
_.sortedIndex = function(array, obj, iteratee, context) {
// 注意 cb 方法
// iteratee 為空 || 為 String 類型(key 值)時會返回不同方法
iteratee = cb(iteratee, context, 1);
// 經(jīng)過迭代函數(shù)計算的值
var value = iteratee(obj);
var low = 0, high = getLength(array);
while (low < high) {
var mid = Math.floor((low + high) / 2);
if (iteratee(array[mid]) < value) low = mid + 1; else high = mid;
}
return low;
};
最后我們說說 _.indexOf 和 _.lastIndexOf 方法。
ES5 引入了 indexOf 和 lastIndexOf 方法,但是 IE < 9 不支持,面試時讓你寫個 Polyfill,你會怎么做(可以把 underscore 的實現(xiàn)看做 Polyfill)?如何能讓面試官滿意?首先如果分開來寫,即兩個方法相對獨立地寫,很顯然代碼量會比較多,因為兩個方法功能相似,所以可以想辦法調(diào)用一個方法,將不同的部分當做參數(shù)傳入,減少代碼量。其次,如果數(shù)組已經(jīng)有序,是否可以用更快速的二分查找算法?這點會是加分項。
源碼實現(xiàn):
// Generator function to create the indexOf and lastIndexOf functions
// _.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex);
// _.lastIndexOf = createIndexFinder(-1, _.findLastIndex);
function createIndexFinder(dir, predicateFind, sortedIndex) {
// API 調(diào)用形式
// _.indexOf(array, value, [isSorted])
// _.indexOf(array, value, [fromIndex])
// _.lastIndexOf(array, value, [fromIndex])
return function(array, item, idx) {
var i = 0, length = getLength(array);
// 如果 idx 為 Number 類型
// 則規(guī)定查找位置的起始點
// 那么第三個參數(shù)不是 [isSorted]
// 所以不能用二分查找優(yōu)化了
// 只能遍歷查找
if (typeof idx == 'number') {
if (dir > 0) { // 正向查找
// 重置查找的起始位置
i = idx >= 0 ? idx : Math.max(idx + length, i);
} else { // 反向查找
// 如果是反向查找,重置 length 屬性值
length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1;
}
} else if (sortedIndex && idx && length) {
// 能用二分查找加速的條件
// 有序 & idx !== 0 && length !== 0
// 用 _.sortIndex 找到有序數(shù)組中 item 正好插入的位置
idx = sortedIndex(array, item);
// 如果正好插入的位置的值和 item 剛好相等
// 說明該位置就是 item 第一次出現(xiàn)的位置
// 返回下標
// 否則即是沒找到,返回 -1
return array[idx] === item ? idx : -1;
}
// 特判,如果要查找的元素是 NaN 類型
// 如果 item !== item
// 那么 item => NaN
if (item !== item) {
idx = predicateFind(slice.call(array, i, length), _.isNaN);
return idx >= 0 ? idx + i : -1;
}
// O(n) 遍歷數(shù)組
// 尋找和 item 相同的元素
// 特判排除了 item 為 NaN 的情況
// 可以放心地用 `===` 來判斷是否相等了
for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {
if (array[idx] === item) return idx;
}
return -1;
};
}
// Return the position of the first occurrence of an item in an array,
// or -1 if the item is not included in the array.
// If the array is large and already in sort order, pass `true`
// for **isSorted** to use binary search.
// _.indexOf(array, value, [isSorted])
// 找到數(shù)組 array 中 value 第一次出現(xiàn)的位置
// 并返回其下標值
// 如果數(shù)組有序,則第三個參數(shù)可以傳入 true
// 這樣算法效率會更高(二分查找)
// [isSorted] 參數(shù)表示數(shù)組是否有序
// 同時第三個參數(shù)也可以表示 [fromIndex] (見下面的 _.lastIndexOf)
_.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex);
// 和 _indexOf 相似
// 反序查找
// _.lastIndexOf(array, value, [fromIndex])
// [fromIndex] 參數(shù)表示從倒數(shù)第幾個開始往前找
_.lastIndexOf = createIndexFinder(-1, _.findLastIndex);
這里有一點要注意,_.indexOf 方法的第三個參數(shù)可以表示 [fromIndex] 或者 [isSorted],而 _.lastIndexOf 的第三個參數(shù)只能表示 [fromIndex],我們從代碼中便可以輕易看出:
_.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex);
_.lastIndexOf = createIndexFinder(-1, _.findLastIndex);
關(guān)于這點我也百思不得其解,不知道做這個限制是為了什么考慮,歡迎探討~
最后給出本文涉及的五個方法的源碼位置 https://github.com/hanzichi/underscore-analysis/blob/master/underscore-1.8.3.js/src/underscore-1.8.3.js#L613-L673