学习underscore源码之在数组中查找指定元素

849 阅读3分钟

前言

学习的underscore.js 源码版本为1.13.1。
这一节学习如何在数组中查找指定元素,涉及的方法包括findIndex、findLastIndex、sortedIndex、indexOf、lastIndexOf

ES6 findIndex

ES6中数组新增了findIndex方法,会返回数组中满足提供的测试函数的第一个元素的索引。若没有找到对应元素则返回-1;

先来看下使用:

const arr = [5, 12, 8, 130, 44];

const isLargeNumber = (element) => element > 13;

console.log(arr.findIndex(isLargeNumber));  // 3

findIndex 会找出第一个大于 13 的元素的下标,所以最后返回 3

实现findIndex

function findIndex (array, predicate, context) {
  var length = array.length;
  for (var index = 0; index < length; index++) {
    if (predicate.call(context, array[index], index, array)) return index;
  }
  return -1;
}

const arr = [5, 12, 8, 130, 44];
findIndex(arr, function (item, index, array) {
    return item > 13;
});
// 3

实现findLastIndex

findIndex是正序查找, 同样我们也可以实现一个倒序查找的findLastIndex

function findLastIndex (array, predicate, context) {
  var length = array.length;
  for (var index = length - 1; index >= 0; index--) {
    if (predicate.call(context, array[index], index, array)) return index;
  }
  return -1;
}

const arr = [5, 12, 8, 130, 44];
findLastIndex(arr, function (item, index, array) {
    return item > 13;
});
// 4

createPredicateIndexFinder

从上面代码可以看出findIndex、findLastIndex中除了for循环内语句不一样外,其余都是一样的处理逻辑,underscore.js中利用传参的不同,返回不同的函数,此时高阶函数上场.

/**
 * 利用传参的不同,返回不同的函数
 * @param {number} dir 实现正序和倒序遍历的关键因子
 */
function createPredicateIndexFinder (dir) {
  return function (array, predicate, context) {
    var length = arr.length;
    var index = dir > 0 ? 0 : length - 1;
    for (; index >= 0 && index < length; index += dir) {
      if (predicate.call(context, array[index], index, array)) return index;
    }
    return -1;
  };
}
var findIndex = createPredicateIndexFinder(1);
var findLastIndex = createPredicateIndexFinder(-1);

sortedIndex

使用二分查找确定valuelist中的位置序号,value按此序号插入能保持list原有的排序。

使用:

_.sortedIndex([10, 20, 30, 40, 50], 35);  // 3

list既然是有序的,那我们可以使用二分法查找插入位置。

第一版实现

function sortedIndex (array, obj) {
  var low = 0, high = array.length;
  while (low < high) {
    var mid = Math.floor((low + high) / 2);
    if (array[mid] < obj) low = mid + 1; 
    else high = mid;
  }
  return low;
}
sortedIndex([10, 20, 30, 40, 50], 35);  // 3

但这样局限性大,我们还希望实现下面效果:

var stooges = [{name: 'moe', age: 40}, {name: 'curly', age: 60}];
_.sortedIndex(stooges, {name: 'larry', age: 50}, 'age');  // 1

所以我们需要加上一个参数iteratee函数对数组的每一个元素进行处理,此外还需要考虑this指向问题;
在上一篇我们学习了内部函数cb,用来处理不同类型的iteratee

function sortedIndex(array, obj, iteratee, context) {
  iteratee = cb(iteratee, context, 1);
  var value = iteratee(obj);
  var low = 0, high = array.length;
  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

根据以上findIndex 和 FindLastIndex 的实现方式,我们很容易写出第一版indexOf、lastIndexOf

第一版

function createIndexFinder(dir) {
  return function(array, item, idx) {
    var i = 0, length = array.length;
    for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {
      if (array[idx] === item) return idx;
    }
    return -1;
  };
}
var indexOf = createIndexFinder(1);
var lastIndexOf = createIndexFinder(-1);


indexOf([1, 2, 3], 2);    //  1
lastIndexOf([1, 2, 3, 1, 2, 3], 2);   // 4

第二版 支持 fromIndex

indexOf中fromIndex
MDN中介绍fromIndex:

开始查找的位置。如果该索引值大于或等于数组长度,意味着不会在数组里查找,返回-1。如果参数中提供的索引值是一个负值,则将其作为数组末尾的一个抵消,即-1表示从最后一个元素开始查找,-2表示从倒数第二个元素开始查找 ,以此类推。 注意:如果参数中提供的索引值是一个负值,并不改变其查找顺序,查找顺序仍然是从前向后查询数组。如果抵消后的索引值仍小于0,则整个数组都将会被查询。其默认值为0.

lastIndexOf中fromIndex
MDN中介绍fromIndex:

从此位置开始逆向查找。默认为数组的长度减 1(arr.length - 1),即整个数组都被查找。如果该值大于或等于数组的长度,则整个数组会被查找。如果为负值,将其视为从数组末尾向前的偏移。即使该值为负,数组仍然会被从后向前查找。如果该值为负时,其绝对值大于数组长度,则方法返回 -1,即数组不会被查找。

第二版实现:

function createIndexFinder(dir) {
  return function(array, item, idx) {
    var i = 0, length = array.length;
    if (typeof idx == 'number') {
      // indexOf就处理开始查找的位置
      if (dir > 0) {
        i = idx >= 0 ? idx : Math.max(idx + length, i);
      } else {
        // lastIndexOf就处理遍历数组的length
        length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1;
      }
    } 
    for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {
      if (array[idx] === item) return idx;
    }
    return -1;
  };
}

var indexOf = createIndexFinder(1);
var lastIndexOf = createIndexFinder(-1);


indexOf([1, 2, 3, 2], 2, 2);    //  3
lastIndexOf([1, 2, 3, 1, 2, 3], 2, 2);   // 1

第三版优化 考虑NaN

实现的indexOf中是使用 === 来进行判断的,我们知道NaN !== NaN, 那么就可以利用一开始写的 findIndex 从数组中找到符合条件的值的下标

var isNumber = function (num) {
  return Object.prototype.toString.call(num) === '[object Number]'
}
function isNaN$1(obj) {
  return isNumber(obj) && isNaN(obj);
}


function createIndexFinder(dir, predicateFind) {
  return function(array, item, idx) {
    var i = 0, length = array.length;
    if (typeof idx == 'number') {
      if (dir > 0) {
        i = idx >= 0 ? idx : Math.max(idx + length, i);
      } else {
        length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1;
      }
    } 
    // NaN !== NaN
    if (item !== item) {
      // 在截取好的数组中查找第一个满足isNaN$1函数的元素的下标
      idx = predicateFind(array.slice.call(array, i, length), isNaN$1);
      return idx >= 0 ? idx + i : -1;
    }
    for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {
      if (array[idx] === item) return idx;
    }
    return -1;
  };
}

var indexOf = createIndexFinder(1, findIndex);
var lastIndexOf = createIndexFinder(-1, findLastIndex);

indexOf([1, NaN, 3, NaN], NaN, 2);    //  3
lastIndexOf([1, NaN, 3, 1, NaN, 3], NaN, 2);   // 1

第四版优化 indexOf支持二分查找

支持对有序的数组进行更快的二分查找
如果 indexOf 第三个参数不传开始搜索的下标值,而是一个布尔值 true,就认为数组是一个排好序的数组,这时候,就会采用更快的二分法进行查找,这个时候,可以利用我们写的 sortedIndex 函数
最终版:

function createIndexFinder(dir, predicateFind, sortedIndex) {
  return function(array, item, idx) {
    var i = 0, length = array.length;
    // 处理查找位置
    if (typeof idx == 'number') {
      if (dir > 0) {
        i = idx >= 0 ? idx : Math.max(idx + length, i);
      } else {
        length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1;
      }
    } else if (sortedIndex && idx && length) {  // 采用更快的二分法进行查找
      idx = sortedIndex(array, item);
      return array[idx] === item ? idx : -1;
    }
    // 处理NaN
    if (item !== item) {
      // 在截取好的数组中查找第一个满足isNaN$1函数的元素的下标
      idx = predicateFind(array.slice.call(array, i, length), isNaN$1);
      return idx >= 0 ? idx + i : -1;
    }
    for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {
      if (array[idx] === item) return idx;
    }
    return -1;
  };
}
// 只有 indexOf 是支持有序数组使用二分查找,lastIndexOf 并不支持
var indexOf = createIndexFinder(1, findIndex, sortedIndex);
var lastIndexOf = createIndexFinder(-1, findLastIndex);

参考资料:
github.com/mqyqingfeng…
developer.mozilla.org/zh-CN/docs/…