【underscore 源碼解讀】如何優(yōu)雅地寫(xiě)一個(gè)『在數(shù)組中尋找指定元素』的方法

Why underscore

(覺(jué)得這部分眼熟的可以直接跳到下一段了...)

最近開(kāi)始看 underscore.js 源碼,并將 underscore.js 源碼解讀 放在了我的 2016 計(jì)劃中。

閱讀一些著名框架類庫(kù)的源碼懂衩,就好像和一個(gè)個(gè)大師對(duì)話锅知,你會(huì)學(xué)到很多阴幌。為什么是 underscore寄纵?最主要的原因是 underscore 簡(jiǎn)短精悍(約 1.5k 行)立美,封裝了 100 多個(gè)有用的方法碗降,耦合度低隘竭,非常適合逐個(gè)方法閱讀,適合樓主這樣的 JavaScript 初學(xué)者遗锣。從中货裹,你不僅可以學(xué)到用 void 0 代替 undefined 避免 undefined 被重寫(xiě)等一些小技巧 ,也可以學(xué)到變量類型判斷精偿、函數(shù)節(jié)流&函數(shù)去抖等常用的方法弧圆,還可以學(xué)到很多瀏覽器兼容的 hack,更可以學(xué)到作者的整體設(shè)計(jì)思路以及 API 設(shè)計(jì)的原理(向后兼容)笔咽。

之后樓主會(huì)寫(xiě)一系列的文章跟大家分享在源碼閱讀中學(xué)習(xí)到的知識(shí)搔预。

歡迎圍觀~ (如果有興趣,歡迎 star & watch~)您的關(guān)注是樓主繼續(xù)寫(xiě)作的動(dòng)力

題外話

先說(shuō)點(diǎn)題外話叶组。

自從 5 月 16 日開(kāi)始 underscore 系列解讀文章拯田,目前已經(jīng)收獲了 160+ star,在這里子遲也感謝大家的支持甩十,并將繼續(xù)努力分享源碼里的干貨船庇。有朋友私信我說(shuō)好幾天沒(méi)看到更新吭产,在此也請(qǐng)大家原諒,畢竟我把它當(dāng)成了今年的計(jì)劃之一鸭轮,而且平時(shí)也要上班工作臣淤,只能利用閑暇時(shí)間,而且樓主本人對(duì)文章的質(zhì)量要求比較高窃爷,如果是一律的流水文章邑蒋,讀者學(xué)不到什么東西,自己的那關(guān)都過(guò)不了按厘。其實(shí)如果有心医吊,應(yīng)該能發(fā)現(xiàn) underscore-1.8.3 源碼全文注釋 一直有在更新(注釋行數(shù)已經(jīng)快破 1000 了)。

Main

言歸正傳逮京,上一章 中我們結(jié)束了 Object 擴(kuò)展方法部分卿堂,今天開(kāi)始來(lái)解讀 Array 部分的擴(kuò)展方法。其實(shí) JavaScript 中的數(shù)組是我最喜歡的類型造虏,能模擬棧御吞、隊(duì)列等數(shù)據(jù)結(jié)構(gòu),還能隨意插入元素(splice)漓藕,非常的靈活陶珠,這點(diǎn)做過(guò) leetcode 的應(yīng)該都深有體會(huì)(這里也順便安利下我的 leetcode 題解 Repo https://github.com/hanzichi/leetcode)。

今天要講的是享钞,如何在數(shù)組中尋找元素揍诽,對(duì)應(yīng) underscore 中的 _.findIndex,_.findLastIndex栗竖,_.indexOf暑脆,_.lastIndexOf 以及 _.sortIndex 方法姆另。

等等闽撤,是不是有點(diǎn)眼熟,沒(méi)錯(cuò)瞬哼,JavaScript 中已經(jīng)部署了 indexOf 方法(ES5)以及 findIndex 方法(ES6)份名,這點(diǎn)不介紹了碟联,大家可以自行學(xué)習(xí)。

我們先來(lái)看 _.findIndex 和 _.findLastIndex 函數(shù)僵腺。如果了解過(guò) Array.prototype.findIndex() 方法鲤孵,會(huì)非常容易。_.findIndex 的作用就是從一個(gè)數(shù)組中找到第一個(gè)滿足某個(gè)條件的元素辰如,_.findLastIndex 則是找到最后一個(gè)(或者說(shuō)倒序查找)普监。

舉個(gè)簡(jiǎn)單的例子:

var arr = [1, 3, 5, 2, 4, 6];

var isEven = function(num) {
  return !(num & 1);
};

var idx = _.findIndex(arr, isEven);
// => 3

直接看源碼,注釋已經(jīng)寫(xiě)的非常清楚了。這里要注意這個(gè) predicate 函數(shù)凯正,其實(shí)就是把數(shù)組中的元素傳入這個(gè)參數(shù)毙玻,返回一個(gè)布爾值。如果返回 true漆际,則表示滿足這個(gè)條件淆珊,如果 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 變量來(lái)確定數(shù)組遍歷的起始位置
    var index = dir > 0 ? 0 : length - 1;

    for (; index >= 0 && index < length; index += dir) {
      // 找到第一個(gè)符合條件的元素
      // 并返回下標(biāo)值
      if (predicate(array[index], index, array)) return index;
    }

    return -1;
  };
}

// Returns the first index on an array-like that passes a predicate test
// 從前往后找到數(shù)組中 `第一個(gè)滿足條件` 的元素奸汇,并返回下標(biāo)值
// 沒(méi)找到返回 -1
// _.findIndex(array, predicate, [context]) 
_.findIndex = createPredicateIndexFinder(1);

// 從后往前找到數(shù)組中 `第一個(gè)滿足條件` 的元素,并返回下標(biāo)值
// 沒(méi)找到返回 -1
// _.findLastIndex(array, predicate, [context]) 
_.findLastIndex = createPredicateIndexFinder(-1);

接下來(lái)看 _.sortIndex 方法往声,這個(gè)方法無(wú)論使用還是實(shí)現(xiàn)都非常的簡(jiǎn)單擂找。如果往一個(gè)有序數(shù)組中插入元素,使得數(shù)組繼續(xù)保持有序浩销,那么這個(gè)插入位置是贯涎?這就是這個(gè)方法的作用,有序慢洋,很顯然用二分查找即可塘雳。不多說(shuō),直接上源碼普筹。

// _.sortedIndex(list, value, [iteratee], [context]) 
_.sortedIndex = function(array, obj, iteratee, context) {
  // 注意 cb 方法
  // iteratee 為空 || 為 String 類型(key 值)時(shí)會(huì)返回不同方法
  iteratee = cb(iteratee, context, 1);

  // 經(jīng)過(guò)迭代函數(shù)計(jì)算的值
  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;
};

最后我們說(shuō)說(shuō) _.indexOf 和 _.lastIndexOf 方法败明。

ES5 引入了 indexOf 和 lastIndexOf 方法,但是 IE < 9 不支持太防,面試時(shí)讓你寫(xiě)個(gè) Polyfill妻顶,你會(huì)怎么做(可以把 underscore 的實(shí)現(xiàn)看做 Polyfill)?如何能讓面試官滿意蜒车?首先如果分開(kāi)來(lái)寫(xiě)讳嘱,即兩個(gè)方法相對(duì)獨(dú)立地寫(xiě),很顯然代碼量會(huì)比較多酿愧,因?yàn)閮蓚€(gè)方法功能相似沥潭,所以可以想辦法調(diào)用一個(gè)方法,將不同的部分當(dāng)做參數(shù)傳入嬉挡,減少代碼量钝鸽。其次,如果數(shù)組已經(jīng)有序棘伴,是否可以用更快速的二分查找算法寞埠?這點(diǎn)會(huì)是加分項(xiàng)。

源碼實(shí)現(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ī)定查找位置的起始點(diǎn)
      // 那么第三個(gè)參數(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 剛好相等
        // 說(shuō)明該位置就是 item 第一次出現(xiàn)的位置
        // 返回下標(biāo)
        // 否則即是沒(méi)找到仁连,返回 -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 的情況
      // 可以放心地用 `===` 來(lái)判斷是否相等了
      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)的位置
  // 并返回其下標(biāo)值
  // 如果數(shù)組有序,則第三個(gè)參數(shù)可以傳入 true
  // 這樣算法效率會(huì)更高(二分查找)
  // [isSorted] 參數(shù)表示數(shù)組是否有序
  // 同時(shí)第三個(gè)參數(shù)也可以表示 [fromIndex] (見(jiàn)下面的 _.lastIndexOf)
  _.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex);

  // 和 _indexOf 相似
  // 反序查找
  // _.lastIndexOf(array, value, [fromIndex]) 
  // [fromIndex] 參數(shù)表示從倒數(shù)第幾個(gè)開(kāi)始往前找
  _.lastIndexOf = createIndexFinder(-1, _.findLastIndex);

這里有一點(diǎn)要注意饭冬,_.indexOf 方法的第三個(gè)參數(shù)可以表示 [fromIndex] 或者 [isSorted]使鹅,而 _.lastIndexOf 的第三個(gè)參數(shù)只能表示 [fromIndex],我們從代碼中便可以輕易看出:

_.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex);
_.lastIndexOf = createIndexFinder(-1, _.findLastIndex);

關(guān)于這點(diǎn)我也百思不得其解昌抠,不知道做這個(gè)限制是為了什么考慮患朱,歡迎探討~

最后給出本文涉及的五個(gè)方法的源碼位置 https://github.com/hanzichi/underscore-analysis/blob/master/underscore-1.8.3.js/src/underscore-1.8.3.js#L613-L673

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市炊苫,隨后出現(xiàn)的幾起案子裁厅,更是在濱河造成了極大的恐慌,老刑警劉巖侨艾,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件执虹,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡唠梨,警方通過(guò)查閱死者的電腦和手機(jī)袋励,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)当叭,“玉大人茬故,你說(shuō)我怎么就攤上這事∫媳睿” “怎么了磺芭?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)才睹。 經(jīng)常有香客問(wèn)我徘跪,道長(zhǎng),這世上最難降的妖魔是什么琅攘? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任垮庐,我火速辦了婚禮,結(jié)果婚禮上坞琴,老公的妹妹穿的比我還像新娘哨查。我一直安慰自己,他們只是感情好剧辐,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布寒亥。 她就那樣靜靜地躺著,像睡著了一般荧关。 火紅的嫁衣襯著肌膚如雪溉奕。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,772評(píng)論 1 290
  • 那天忍啤,我揣著相機(jī)與錄音加勤,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛鳄梅,可吹牛的內(nèi)容都是我干的叠国。 我是一名探鬼主播,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼戴尸,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼粟焊!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起孙蒙,我...
    開(kāi)封第一講書(shū)人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤项棠,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后马篮,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體沾乘,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年浑测,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片歪玲。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡迁央,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出滥崩,到底是詐尸還是另有隱情岖圈,我是刑警寧澤,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布钙皮,位于F島的核電站蜂科,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏短条。R本人自食惡果不足惜导匣,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望茸时。 院中可真熱鬧贡定,春花似錦、人聲如沸可都。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)渠牲。三九已至旋炒,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間签杈,已是汗流浹背瘫镇。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人汇四。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓接奈,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親通孽。 傳聞我的和親對(duì)象是個(gè)殘疾皇子序宦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容