underscore 源码解读:如何优雅地写一个「在数组中寻找指定元素」的方法

题外话
git

先说点题外话。github

自从 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 则是找到最后一个(或者说倒序查找)。ide

举个简单的例子:函数

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

相关文章
相关标签/搜索