小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
这是我写的第三篇了,这一篇来写Array的相关扩展方法,今天主要来写如何在数组中寻找元素、去重(uniq)和扁平化(flatten),对应 underscore 中的 findIndex**, findLastIndex,indexOf**, lastIndexOf 以及sortIndex 等方法。
findIndex和findLastIndex
findIndex* 的作用就是从一个数组中找到第一个满足某个条件的元素,*findLastIndex 则是找到最后一个(或者说倒序查找)。当我们来到源码中,可以看到是这样的。
// Returns the first index on an array-like that passes a truth test.
var findIndex = createPredicateIndexFinder(1);
// Returns the last index on an array-like that passes a truth test.
var findLastIndex = createPredicateIndexFinder(-1);
我们发现两者都用了createPredicateIndexFinder函数,通过传入1和-1来返回不同的返回方法。直接看源码,注释已经写的非常清楚了。
// Internal function to generate `_.findIndex` and `_.findLastIndex`.
// dir === 1 => 从前往后找 dir === -1 => 从后往前找
function createPredicateIndexFinder(dir) {
// 返回一个函数,该函数接受三个参数,数组、筛选函数、上下文
return function (array, predicate, context) {
// cb 函数先不说,就是处理传入的 predicate,根据 predicate的值 返回不同的处理函数
predicate = cb(predicate, context);
// 获取传入数组的长度
var length = getLength(array);
var index = dir > 0 ? 0 : length - 1; // 初始化 index,根据 dir 不同的值,确定起始位置
// index >= 0 && index < length 组合判断,适用两种情况
for (; index >= 0 && index < length; index += dir) {
// 调用 predicate 函数,返回值为真时(满足用户的条件),立即返回 当前索引(index)
if (predicate(array[index], index, array)) return index;
}
// 没找到就返回 -1
return -1;
};
}
sortIndex
sortIndex 方法,这个方法无论使用还是实现都非常的简单。如果往一个有序数组中插入元素,使得数组继续保持有序,那么这个插入位置是?这就是这个方法的作用,有序,很显然用二分查找即可。我们先来看看它所要传入的参数_.sortedIndex(array, value, [iteratee], [context]),然后直接看源码
// Use a comparator function to figure out the smallest index at which
// an object should be inserted so as to maintain order. Uses binary search.
// _.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
function sortedIndex(array, obj, iteratee, context) {
// 处理传入的 iteratee 函数
// 一般我们没传,采用内部的 identity 函数,该函数就是传入什么值,返回什么值
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
接下来看看indexOf和lastIndexOf两个方法,indexOf作用是返回 value 在该 array 中的索引值,如果 value 不存在 array 中就返回-1。其需要参数是_.indexOf(array, value, [isSorted]);lastIndexOf作用是返回 value 在该 array 中的从最后开始的索引值,如果 value 不存在 array 中就返回-1。其需要参数是_.lastIndexOf(array, value, [fromIndex])。
在ES6中引入了这两个方法,所以其实这个实现就可以当作是Polyfill了,那么我们该如何实现呢?
// 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)
var indexOf = createIndexFinder(1, findIndex, sortedIndex);
// Return the position of the last occurrence of an item in an array,
// or -1 if the item is not included in the array.
// 和 _indexOf 相似
// 反序查找
// _.lastIndexOf(array, value, [fromIndex])
// [fromIndex] 参数表示从倒数第几个开始往前找
var lastIndexOf = createIndexFinder(-1, findLastIndex);
是不是似曾相识?对,和上面两个一样但是是使用了createIndexFinder函数,那么我们明白了这个函数怎么实现的就是知道了indexOf和lastIndexOf怎么做的,啊哈哈哈真聪明。
// Internal function to generate the `_.indexOf` and `_.lastIndexOf` functions.
function createIndexFinder(dir, predicateFind, sortedIndex) {
// 返回一个函数,该函数接受三个参数,数组、将要查找的那个值、从给定索引开始搜索或是否有序
return function (array, item, idx) {
var i = 0, length = getLength(array);
// 给定索引 查找,不能使用二分查找优化
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) {
// indexOf 支持 数组是否有序
// 如果正好插入的位置的值和 item 刚好相等 ,说明该位置就是 item 第一次出现的位置,
// 返回下标,否则即是没找到,返回 -1
idx = sortedIndex(array, item);
return array[idx] === item ? idx : -1;
}
// 如果要查找的元素是 NaN 类型
if (item !== item) {
// findIndex、findLastIndex 进行查找
idx = predicateFind(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;
};
}
range
range函数是一个用来创建整数灵活编号的列表的函数,_.range([start], stop, [step])(左闭右开)使用方法见这:www.underscorejs.com.cn/range。实现起来也是比较容易的。
// Generate an integer Array containing an arithmetic progression. A port of
// the native Python `range()` function. See
// [the Python documentation](https://docs.python.org/library/functions.html#range).
function range(start, stop, step) {
// stop 不存在(就传入了一个参数),就把 start 赋值给 stop,然后 start = 0
if (stop == null) {
stop = start || 0;
start = 0;
}
// 没有指定间隔,那么 默认是 1 或 -1
if (!step) {
step = stop < start ? -1 : 1;
}
// 向上取整, 因为包含 起始值start
var length = Math.max(Math.ceil((stop - start) / step), 0);
var range = Array(length);
for (var idx = 0; idx < length; idx++, start += step) {
range[idx] = start;
}
return range;
}
unique
_.uniq(array, [isSorted], [iteratee])返回 array 去重后的副本
// Produce a duplicate-free version of the array. If the array has already
// been sorted, you have the option of using a faster algorithm.
// The faster algorithm will not work with an iteratee if the iteratee
// is not a one-to-one function, so providing an iteratee will disable
// the faster algorithm.
// 数组去重,如果第二个参数 `isSorted` 为 true, 则说明事先已经知道数组有序
// 程序会跑一个更快的算法(一次线性比较,元素和数组前一个元素比较即可)
// 如果有第三个参数 iteratee,则对数组每个元素迭代,对迭代之后的结果进行去重
// 返回去重后的数组(array 的子数组)
// _.uniq(array, [isSorted], [iteratee])
function uniq(array, isSorted, iteratee, context) {
// isSorted 不是 boolean 值的时候
if (!isBoolean(isSorted)) {
context = iteratee;
iteratee = isSorted;
isSorted = false;
}
// 存在迭代函数,就对这个 迭代函数 进行处理返回正确的迭代函数
if (iteratee != null) iteratee = cb(iteratee, context);
var result = [];
var seen = [];
for (var i = 0, length = getLength(array); i < length; i++) {
var value = array[i], // 当前值
// 如果指定了迭代函数,则对数组每一个元素进行迭代
// 根据 当前值 计算出来的值,要是对象的时候可能是根据某个属性值去重,那么这 computed 就是 属性值
computed = iteratee ? iteratee(value, i, array) : value;
// 是有序的并且没有指定迭代函数
if (isSorted && !iteratee) {
// i===0 或者 相邻两个不相等的时候(有序的,相邻不相等说明不是重复的),第一个直接填加进去
if (!i || seen !== computed) result.push(value);
seen = computed; // 保存前一个值
} else if (iteratee) {
// 存在迭代函数,判断 seen 是否有该值,没有就添加 ==》 seen 保存的是 属性值
if (!contains(seen, computed)) {
seen.push(computed);
result.push(value);
}
} else if (!contains(result, value)) {
// 默认情况 result 中没有 value,就添加
result.push(value);
}
}
return result;
}
这是underscore实现的去重方法,对象也可以用,如果说就单纯数组去重的的话还有好几种思路大家可以想一想。
flatten
_.flatten(array, [shallow])将一个嵌套多层的数组 array(数组) (嵌套可以是任何层数)转换为只有一层的数组。 如果你传递 shallow(boolean、number) 参数,数组将只减少一维或指定维数的嵌套。
// _.flatten([1, [2], [3, [[4]]]]); => [1, 2, 3, 4];
// _.flatten([1, [2], [3, [[4]]]], true); => [1, 2, 3, [[4]]];
// Flatten out an array, either recursively (by default), or up to `depth`.
// Passing `true` or `false` as `depth` means `1` or `Infinity`, respectively.
function flatten(array, depth) {
return flatten$1(array, depth, false);
}
这里又引入了一个重要函数flatten$1,还有其他函数也用到了,我们就直接看代码注释吧
// Internal implementation of a recursive `flatten` function.
// flatten(array, depth) 使用 flatten$1(array, depth, false)
/**
* strict: 能过滤 input 参数元素中的非数组元素
var ans = flatten([5, 6, [1, 2], [3, 4]], true, true);
console.log(ans); // => [1, 2, 3, 4]
*/
function flatten$1(input, depth, strict, output) {
output = output || [];
// false 或者不传 的时候,全部展开 // '' null undefined NaN
if (!depth && depth !== 0) {
depth = Infinity;
} else if (depth <= 0) {
// 不展开,直接返回
return output.concat(input);
}
// 从哪往后加,因为展开后 output.length > input.length, 递归过程中添加到 output 的哪个位置
var idx = output.length;
// 递归遍历 input
for (var i = 0, length = getLength(input); i < length; i++) {
var value = input[i]; // 每一项
if (isArrayLike(value) && (isArray(value) || isArguments$1(value))) {
// Flatten current level of array or arguments object.
if (depth > 1) {
flatten$1(value, depth - 1, strict, output);
idx = output.length;
} else {
// depth === 1 或者 depth === true 一层的时候,直接把 value 展开
var j = 0, len = value.length;
while (j < len) output[idx++] = value[j++];
}
} else if (!strict) {
/**
* 进入该分支的条件: value 不是数组,并且 strict === false 的时候 ===> flatten传入的就是false
* 不是数组,就直接把这一项的值添加进 output
*/
output[idx++] = value;
}
}
return output;
}
union
_.union(*arrays)返回传入的 **arrays(数组)**并集:按顺序返回,返回数组的元素是唯一的,可以传入一个或多个 arrays (数组)。
// _.union([1, 2, 3], [101, 2, 1, 10], [2, 1]); => [1, 2, 3, 101, 10]
// Produce an array that contains the union: each distinct element from all of
// the passed-in arrays.
var union = restArguments(function(arrays) {
// 只展开一层,然后去重
return uniq(flatten$1(arrays, true, true));
});
Array的扩展方法我就介绍到这里了,还有一些其他的方法都比较简单,大家可以直接去看看源代码,都有注释。