underscore 源碼解讀:如何優雅地寫一個「在數組中尋找指定元素」的方法

題外話

先說點題外話。

自從 5 月 16 日開始 underscore 系列解讀文章,目前已經收穫了 160+ star,在這裏子遲也感謝大家的支持,並將繼續努力分享源碼裏的乾貨。有朋友私信我說好幾天沒看到更新,在此也請大家原諒,畢竟我把它當成了今年的計劃之一,而且平時也要上班工作,只能利用閒暇時間,而且樓主本人對文章的質量要求比較高,如果是一律的流水文章,讀者學不到什麼東西,自己的那關都過不了。其實如果有心,應該能發現 underscore-1.8.3 源碼全文註釋 一直有在更新(註釋行數已經快破 1000 了)。

Main

言歸正傳,上一章 中我們結束了 Object 擴展方法部分,今天開始來解讀 Array 部分的擴展方法。其實 JavaScript 中的數組是我最喜歡的類型,能模擬棧、隊列等數據結構,還能隨意插入元素(splice),非常的靈活,這點做過 leetcode 的應該都深有體會(這裏也順便安利下我的 leetcode 題解 Repo https://github.com/hanzichi/leetcode)。

今天要講的是,如何在數組中尋找元素,對應 underscore 中的 .findIndex,.findLastIndex,.indexOf,.lastIndexOf 以及 _.sortIndex 方法。

等等,是不是有點眼熟,沒錯,JavaScript 中已經部署了 indexOf 方法(ES5)以及 findIndex 方法(ES6),這點不介紹了,大家可以自行學習。

我們先來看 .findIndex 和 .findLastIndex 函數。如果瞭解過 Array.prototype.findIndex() 方法,會非常容易。.findIndex 的作用就是從一個數組中找到第一個滿足某個條件的元素,.findLastIndex 則是找到最後一個(或者說倒序查找)。

舉個簡單的例子:


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

var isEven = function(num) {

  return !(num & 1);

};

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

// => 3

直接看源碼,註釋已經寫的非常清楚了。這裏要注意這個 predicate 函數,其實就是把數組中的元素傳入這個參數,返回一個布爾值。如果返回 true,則表示滿足這個條件,如果 false 則相反。


// Generator function to create the findIndex and findLastIndex functions

// dir === 1 => 從前往後找

// dir === -1 => 從後往前找

function createPredicateIndexFinder(dir) {

  // 經典閉包

  return function(array, predicate, context) {

    predicate = cb(predicate, context);

    var length = getLength(array);

    // 根據 dir 變量來確定數組遍歷的起始位置

    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

// 從前往後找到數組中 `第一個滿足條件` 的元素,並返回下標值

// 沒找到返回 -1

// _.findIndex(array, predicate, [context])

_.findIndex = createPredicateIndexFinder(1);

// 從後往前找到數組中 `第一個滿足條件` 的元素,並返回下標值

// 沒找到返回 -1

// _.findLastIndex(array, predicate, [context])

_.findLastIndex = createPredicateIndexFinder(-1);

接下來看 _.sortIndex 方法,這個方法無論使用還是實現都非常的簡單。如果往一個有序數組中插入元素,使得數組繼續保持有序,那麼這個插入位置是?這就是這個方法的作用,有序,很顯然用二分查找即可。不多說,直接上源碼。


// _.sortedIndex(list, value, [iteratee], [context])

_.sortedIndex = function(array, obj, iteratee, context) {

  // 注意 cb 方法

  // iteratee 爲空 || 爲 String 類型(key 值)時會返回不同方法

  iteratee = cb(iteratee, context, 1);

  // 經過迭代函數計算的值

  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 的實現看做 Polyfill)?如何能讓面試官滿意?首先如果分開來寫,即兩個方法相對獨立地寫,很顯然代碼量會比較多,因爲兩個方法功能相似,所以可以想辦法調用一個方法,將不同的部分當做參數傳入,減少代碼量。其次,如果數組已經有序,是否可以用更快速的二分查找算法?這點會是加分項。

源碼實現:


// Generator function to create the indexOf and lastIndexOf functions

  // _.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex);

  // _.lastIndexOf = createIndexFinder(-1, _.findLastIndex);

  function createIndexFinder(dir, predicateFind, sortedIndex) {

    // API 調用形式

    // _.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 類型

      // 則規定查找位置的起始點

      // 那麼第三個參數不是 [isSorted]

      // 所以不能用二分查找優化了

      // 只能遍歷查找

      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 找到有序數組中 item 正好插入的位置

        idx = sortedIndex(array, item);

        // 如果正好插入的位置的值和 item 剛好相等

        // 說明該位置就是 item 第一次出現的位置

        // 返回下標

        // 否則即是沒找到,返回 -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) 遍歷數組

      // 尋找和 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])

  // 找到數組 array 中 value 第一次出現的位置

  // 並返回其下標值

  // 如果數組有序,則第三個參數可以傳入 true

  // 這樣算法效率會更高(二分查找)

  // [isSorted] 參數表示數組是否有序

  // 同時第三個參數也可以表示 [fromIndex] (見下面的 _.lastIndexOf)

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

  // 和 _indexOf 相似

  // 反序查找

  // _.lastIndexOf(array, value, [fromIndex])

  // [fromIndex] 參數表示從倒數第幾個開始往前找

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

這裏有一點要注意,.indexOf 方法的第三個參數可以表示 [fromIndex] 或者 [isSorted],而 .lastIndexOf 的第三個參數只能表示 [fromIndex],我們從代碼中便可以輕易看出:


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

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

關於這點我也百思不得其解,不知道做這個限制是爲了什麼考慮,歡迎探討~

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章