前言
学习的underscore.js 源码版本为1.13.1
这一节学习与数组有关的功能函数,涉及的函数包括flatten、uniq、intersection、union、difference
数组的扁平化
如何实现数组的扁平化?
1. 利用ES6的flat方法
ES6中数组新增了flat方法,会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。
其中参数表示想要拉平的层数,默认为1。如果不管有多少层嵌套,都要转成一维数组,可以用Infinity关键字作为参数。
var arr = [1, 2, [3, [4, 5, [6]]]];
arr.flat(); // [1, 2, 3, [4, 5, [6]]]
arr.flat(2); // [1, 2, 3, 4, 5, [6]]
arr.flat(Infinity); // [1, 2, 3, 4, 5, 6]
var arr1 = [1, 2, , 4, 5];
// 会将empty元素过滤掉
arr1.flat(); // [1, 2, 4, 5]
2. 利用栈/队列 + ES6 拓展运算符
// 也可以使用shift/unshift, 但使用pop/push速度更快
function flatten(input) {
const stack = [...input];
const res = [];
while (stack.length) {
const next = stack.pop();
if (Array.isArray(next)) {
stack.push(...next);
} else {
res.push(next);
}
}
// 反转
return res.reverse();
}
测试下:
// 利用underscore中range函数创建整数灵活编号的列表
function range(start, stop, step) {
if (stop == null) {
stop = start || 0;
start = 0;
}
if (!step) {
step = stop < start ? -1 : 1;
}
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;
}
// 使用shift/unshift
function flatten1(input) {
const stack = [...input];
const res = [];
while (stack.length) {
const next = stack.shift();
if (Array.isArray(next)) {
stack.unshift(...next);
} else {
res.push(next);
}
}
return res;
}
var arr = range(10000);
// 需要扁平化的数组
var list = [arr, [arr], [arr, [arr]]]
console.time('使用pop/push耗时');
flatten(list)
console.timeEnd('使用pop/push耗时');
console.time('使用shift/unshift耗时');
flatten1(list)
console.timeEnd('使用shift/unshift耗时');
或者使用下面代码:
var arr = [1, 2, [3, [4, 5, [6]]]];
function flatten(arr) {
while (arr.some(Array.isArray)) {
arr = [].concat(...arr);
}
return arr;
}
flatten(arr); // [1, 2, 3, 4, 5, 6]
3. 使用reduce + concat + 递归
var arr = [1, 2, [3, [4, 5, [6]]]];
function flatten(arr, deep){
return deep > 0 ? arr.reduce((prev, next) => prev.concat(Array.isArray(next) ? flatten(next, deep - 1) : next), []) : arr.slice();
}
flatten(arr, Infinity); // [1, 2, 3, 4, 5, 6]
4. 使用Generator函数
function* flatten(array, depth) {
if (depth === undefined) {
depth = 1;
}
for (const item of array) {
if (Array.isArray(item) && depth > 0) {
yield* flatten(item, depth - 1);
} else {
yield item;
}
}
}
const arr = [1, 2, [3, 4, [5, 6]]];
const flattened = [...flatten(arr, Infinity)];
// [1, 2, 3, 4, 5, 6]
5. 使用toString + split方法(只适用于都是数字组成的数组,不推荐)
var arr = [1, 2, [3, [4, 5, [6]]]];
function flatten(arr){
return arr.toString().split(',')
}
console.log(flatten(arr));
6. 正则(不推荐)
function flatten(arr) {
let str = JSON.stringify(arr);
str = str.replace(/(\[|\])/g, '');
str = '[' + str + ']';
return JSON.parse(str);
}
underscore.js中的_.flatten(array, [shallow])
先来看使用效果:
var list = [1, [2], [3, [[[4]]]]];
_.flatten(list); // [1, 2, 3, 4]
// shallow为true则数组将只减少一维的嵌套
_.flatten(list, true); // [1, 2, 3, [4]]
// 处理arguments
var result = (function(){ return _.flatten(arguments); }(1, [2], [3, [[[4]]]]));
console.log(result); // [1, 2, 3, 4]
_.flatten([[1,2,3], [4, 5, 6], 5, 1, 3], true); // [1, 2, 3, 4, 5, 6, 5, 1, 3]
如何实现?
/**
*
* @param {array|arguments} input 需要展开的数组或arguments
* @param {?boolean|number} depth
* @param {?boolean} strict strict === true,通常和 depth === true 配合使用 表示只展开一层,但是不保存非数组元素(即无法展开的基础类型)
* @param {?array} output 输出结果
*/
function flatten$1(input, depth, strict, output) {
output = output || [];
// depth为undefined、false
if (!depth && depth !== 0) {
depth = Infinity;
} else if (depth <= 0) {
return output.concat(input);
}
var idx = output.length;
for (var i = 0, length = input.length; i < length; i++) {
var value = input[i];
// 数组 或者 arguments且length 属性值是不大于 Number.MAX_SAFE_INTEGER 的自然数
if (isArrayLike(value) && (isArray(value) || isArguments$1(value))) {
if (depth > 1) {
// 递归展开
flatten$1(value, depth - 1, strict, output);
idx = output.length;
} else {
var j = 0,
len = value.length;
// 将 value 数组的元素添加到 output 数组中
while (j < len) output[idx++] = value[j++];
}
} else if (!strict) {
output[idx++] = value;
}
}
return output;
}
function flatten(array, depth) {
return flatten$1(array, depth, false);
}
数组去重
如何实现数组去重?
1. 利用ES6的Set 和 Map
var arr = [1, 2, 1, 4, 1, 3, '1'];
function unique(arr) {
// return [...new Set(array)]
return Array.from(new Set(arr));
}
unique(arr); // [1, 2, 4, 3, '1']
2. 利用双层for循环
var arr = [1, 2, 1, 4, 1, 3, '1'];
function unique(arr) {
// 用于存放结果
var result = [];
var arrLength = arr.length;
for (var i = 0; i < arrLength; i++) {
for (var j = 0, resLen = result.length; j < resLen; j++) {
if (arr[i] === result[j]) {
break;
}
}
// 如果array[i]是唯一的,那么执行完循环,j等于resLen
if (j === resLen) {
result.push(arr[i]);
}
}
return result;
}
unique(arr); // [1, 2, 4, 3, '1']
3. 使用indexOf
使用 indexOf 简化内层的循环:
var arr = [1, 2, 1, 4, 1, 3, '1'];
function unique(arr) {
// 用于存放结果
var result = [];
var arrLength = arr.length;
for (var i = 0; i < arrLength; i++) {
var current = arr[i];
if (result.indexOf(current) === -1) {
result.push(current)
}
}
return result;
}
unique(arr); // [1, 2, 4, 3, '1']
4. 先排序再去重
先进行排序,然后就可以只判断当前元素与上一个元素是否相同,相同就说明重复,不相同就添加进 result
var arr = [1, 2, 1, 4, 1, 3, '1'];
function unique(arr) {
// 用于存放结果
var result = [];
var arrLength = arr.length,
// 先利用slice或者concat进行浅拷贝一份数组然后进行排序
sortedArray = arr.slice().sort((a, b) => a - b),
prev;
for (var i = 0; i < arrLength; i++) {
var current = sortedArray[i];
// 如果是第一个元素或者相邻的元素不相同
if (!i || prev !== current) {
result.push(current)
}
prev = current;
}
return result;
}
unique(arr); // [1, '1', 2, 3, 4]
自己实现
第一版实现
function uniq(array, isSorted) {
var result = [];
var seen = [];
for (var i = 0, length = array.length; i < length; i++) {
var value = array[i];
if (isSorted) {
if (!i || seen !== value) result.push(value);
seen = value;
} else if (result.indexOf(value) === -1) {
result.push(value);
}
}
return result;
}
var arr = [1, 2, 1, 4, 1, 3, '1']
uniq(arr); // [1, 2, 4, 3, '1']
var sortedArr = [1, 1, 1, '1', 2, 3, 4]
uniq(sortedArr, true); // [1, '1', 2, 3, 4]
第二版优化 支持传迭代函数
function uniq(array, isSorted, iteratee) {
var result = [];
var seen = [];
for (var i = 0, length = array.length; i < length; i++) {
var value = array[i],
// 如果指定了迭代函数
// 则对数组每一个元素进行迭代
computed = iteratee ? iteratee(value, i, array) : value;
if (isSorted && !iteratee) {
if (!i || seen !== computed) result.push(value);
seen = computed;
} else if (iteratee) {
if (seen.indexOf(computed) === -1) {
seen.push(computed);
result.push(value);
}
} else if (result.indexOf(value) === -1) {
result.push(value);
}
}
return result;
}
var arr1 = [1, 'A', 'a', '1', 1, 3];
uniq(arr1, false, function (item) {
return typeof item == 'string' ? item.toLowerCase() : item
})
// [1, 'A', '1', 3]
underscore.js中的_.uniq(array, [isSorted], [iteratee])
返回 array去重后的副本, 使用 === 做相等测试. 如果您确定 array 已经排序, 那么给 isSorted 参数传递 true值, 此函数将运行的更快的算法(直接相邻元素进行比较). 如果要处理对象元素, 传递 iteratee函数来获取要对比的属性。
源码如下:
/**
* 如果obj包含指定的item则返回true(使用===检测)。如果obj 是数组,内部使用indexOf判断。使用fromIndex来给定开始检索的索引位置。
* @param {array|object} obj
* @param {*} item 包含项
* @param {*} fromIndex 开始检索的索引位置
* @param {*} guard
*/
function contains(obj, item, fromIndex, guard) {
// 如果是对象,返回 values 组成的数组
if (!isArrayLike(obj)) obj = values(obj);
// 如果没有指定该参数,则默认从头找起
if (typeof fromIndex != 'number' || guard) fromIndex = 0;
// 使用indexOf寻找
return indexOf(obj, item, fromIndex) >= 0;
}
/**
* 数组去重
* @param {array} array
* @param {?boolean} isSorted 是否已经排序, 如果排序则直接相邻元素比较
* @param {?function} iteratee 迭代函数
* @param {?obj} context 暴露的 API 中没 context 参数
*/
function uniq(array, isSorted, iteratee, context) {
// 处理参数
// 没有传入 isSorted 参数
if (!isBoolean(isSorted)) {
context = iteratee;
iteratee = isSorted;
isSorted = false;
}
// 如果有迭代函数, 则根据 this 指向二次返回新的迭代函数
if (iteratee != null) iteratee = cb(iteratee, context);
var result = [];
// 用来过滤重复值
var seen = [];
for (var i = 0, length = array.length; i < length; i++) {
var value = array[i],
// 如果指定了迭代函数
// 则对数组每一个元素进行迭代
computed = iteratee ? iteratee(value, i, array) : value;
if (isSorted && !iteratee) {
// 如果是第一个元素或者相邻的元素不相同
if (!i || seen !== computed) result.push(value);
seen = computed;
} else if (iteratee) {
// 如果 seen[] 中没有 computed 这个元素值
if (!contains(seen, computed)) {
seen.push(computed);
result.push(value);
}
} else if (!contains(result, value)) { // 迭代函数不存在
result.push(value);
}
}
return result;
}
数组的交集(去重)
LeedCode 349. 两个数组的交集
获取两数组的交集(去重)
var intersection = function(nums1, nums2) {
return [...new Set(nums1.filter((num) => nums2.includes(num)))]
};
underscore.js中的 _.intersection(*arrays)
返回传入 arrays(数组)交集。结果中的每个值是存在于传入的每个arrays(数组)里。
_.intersection([1, 2, 3], [101, 2, 1, 10], [2, 1]); // [1, 2]
实现:
// 获取一个对象上自有属性(不包括原型链上的)的 values 值
function values(obj) {
var _keys = keys(obj);
var length = _keys.length;
var values = Array(length);
for (var i = 0; i < length; i++) {
values[i] = obj[_keys[i]];
}
return values;
}
// 返回传入 arrays(数组)交集。结果中的每个值是存在于传入的每个arrays(数组)里
// 注意: 结果为去重的
function intersection(array) {
var result = [];
var argsLength = arguments.length;
for (var i = 0, length = array.length; i < length; i++) {
var item = array[i];
// 返回的 result 是去重的
if (contains(result, item)) continue;
var j;
// 判断其他参数数组中是否都有 item 这个元素
for (j = 1; j < argsLength; j++) {
if (!contains(arguments[j], item)) break;
}
if (j === argsLength) result.push(item);
}
return result;
}
数组的并集
_.union(*arrays)
返回传入的 arrays(数组)并集:按顺序返回,返回数组的元素是唯一的,可以传入一个或多个 arrays (数组)
_.union([1, 2, 3], [101, 2, 1, 10], [2, 1]); // [1, 2, 3, 101, 10]
如何实现?
// 先用 flatten 方法将传入的数组展开成一个数组,再使用 _.uniq 方法
var union = restArguments(function(arrays) {
return uniq(flatten$1(arrays, true, true));
});
数组的差集
_.difference(array, *others)
返回的值来自array参数数组,并且不存在于other 数组
_.difference([1, 2, 3, 4, 5], [5, 2, 10]); // [1, 3, 4]
如何实现?
var difference = restArguments(function(array, rest) {
// 先用 flatten 方法将传入的数组展开成一个数组
rest = flatten$1(rest, true, true);
// 遍历 array 过滤
return filter(array, function(value){
// 如果 value 存在在 rest 中,则过滤掉
return !contains(rest, value);
});
});