【underscore 源码解读】如何优雅地写一个『在数组中寻找指定元素』的方法

Why underscore(觉得这部分眼熟的可以直接跳到下一段了...)最近开始看 underscore.js 源码,并将 underscore.js 源码解读 放在了我的 2016 计划中。阅读一些著名框架类库的源码,就好像和一个个大师对话,你会学到很多。为什么是 underscore?最主要的原因是 underscore 简短精悍(约 1.5k 行),封装了 100

Why underscore

(觉得这部分眼熟的可以直接跳到下一段了...)

最近开始看 underscore.js 源码,并将 underscore.js 源码解读 放在了我的 2016 计划中。

阅读一些著名框架类库的源码,就好像和一个个大师对话,你会学到很多。为什么是 underscore?最主要的原因是 underscore 简短精悍(约 1.5k 行),封装了 100 多个有用的方法,耦合度低,非常适合逐个方法阅读,适合楼主这样的 JavaScript 初学者。从中,你不仅可以学到用 void 0 代替 undefined 避免 undefined 被重写等一些小技巧 ,也可以学到变量类型判断、函数节流&函数去抖等常用的方法,还可以学到很多浏览器兼容的 hack,更可以学到作者的整体设计思路以及 API 设计的原理(向后兼容)。

之后楼主会写一系列的文章跟大家分享在源码阅读中学习到的知识。

  1. underscore-1.8.3 源码解读项目地址 https://github.com/hanzichi/underscore-analysis

  2. underscore-1.8.3 源码全文注释 https://github.com/hanzichi/underscore-analysis/blob/master/underscore-1.8.3.js/underscore-1.8.3-analysis.js

  3. underscore-1.8.3 源码解读系列文章 https://github.com/hanzichi/underscore-analysis/issues

欢迎围观~ (如果有兴趣,欢迎 star & watch~)您的关注是楼主继续写作的动力

题外话

先说点题外话。

自从 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 

接下来看 _.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
最后我们说说 .indexOf 和 .lastIndexOf 方法。

ES5 引入了 indexOf 和 lastIndexOf 方法,但是 IE // 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 

这里有一点要注意,.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

关键字:JavaScript, idx, array, item