为什么写这篇文章
在掘金上每过一段时间就会出现一次题材相似的讨论,例如面试,手写函数,而此题材也不例外——数组去重。我看到过论坛上出现过的一次次的数组去重的文章,但是个人觉得可能都不太完善,所以,我期望尝试对 JS 数组去重进行一次 “系统性” 的总结。
资料
前提
测试用例
该用例包含了所有简单基本类型以及对象,基本可以涵盖所有常见数据的可能
let a = {};
let b = { a: 1 };
let testArr = [1, 1, "1", "1", "a", "a", {}, {}, { a: 1 }, a, a, b, [], [], [1], undefined, undefined, null, null, NaN, NaN];
去重标准
以下的代码以 lodash 中默认去重函数的去重结果为标准,lodash uniq
函数去重结果如下
// lodash@4.17.15
_.uniq(testArr);
// [1, "1", "a", {}, {}, { a: 1 }, {}, { a: 1 }, [], [], [1], undefined, null, NaN];
去重 4 大类型
对于 JS 数组去重来说,其实万变不离其中,我简单的总结了以下 4 种去重类型,而围绕着各种类型,由于 JS 丰富的函数以及工具能力,可以有多种实现方法,不管是采用 forEach,reduce,filter
等来循环并拼装新数组,还是用最基础的 for
循环以及 push
来循环并组装新数组,暂时都可以归纳到以下的 4 个类型
数组元素比较型
该方法是将数组的值取出与其他值比较并修改数组
双层 for 循环
取出一个元素,将其与其后所有元素比较,若在其后发现相等元素,则将后者删掉
function uniq(arr) {
for (let i = 0; i < arr.length; i++) {
for (let j = i + 1; j < arr.length; j++) {
if (arr[i] === arr[j]) {
arr.splice(j, 1);
j--;
}
}
}
return arr;
}
// 运行结果
// [1, "1", "a", {}, {}, { a: 1 }, {}, { a: 1 }, [], [], [1], undefined, null, NaN, NaN]
// 与 lodash 结果相比 NaN 没有去掉
由于 NaN === NaN
等于 false
,所以重复的 NaN 并没有被去掉
排序并进行相邻比较
先对数组内元素进行排序,再对数组内相邻的元素两两之间进行比较,经测试,该方法受限于 sort
的排序能力,所以若数组不存在比较复杂的对象(复杂对象难以排序),则可尝试此方法
function uniq(arr) {
arr.sort();
for (let i = 0; i < arr.length - 1; i++) {
arr[i] === arr[i + 1] && arr.splice(i + 1, 1) && i--;
}
return arr;
}
// 运行结果
//[[], [], 1, "1", [1], NaN, NaN, {}, {}, { a: 1 }, {}, { a: 1 }, "a", null, undefined];
// 与 lodash 结果相比 NaN 没有去掉,且对象的去重出现问题
同样由于 NaN === NaN
等于 false
,所以重复的 NaN 并没有被去掉,并且由于 sort
没有将对象很好的排序,在对象部分,会出现一些去重失效
查找数组元素位置型
该类型即针对每个数组元素进行一次查找其在数组内的第一次出现的位置,若第一次出现的位置等于该元素此时的索引,即收集此元素
indexOf
以 indexOf
来查找元素在数组内第一次出现的位置,若位置等于当前元素的位置,则收集
function uniq(arr) {
let res = [];
for (let i = 0; i < arr.length; i++) {
if (arr.indexOf(arr[i]) === i) {
res.push(arr[i]);
}
}
return res;
}
// 运行结果
// [1, "1", "a", {}, {}, { a: 1 }, {}, { a: 1 }, [], [], [1], undefined, null];
// 与 lodash 结果相比 少了 NaN
indexOf
采用与 ===
相同的值相等判断规则,所以在数组内没有元素与 NaN 相等,包括它自己,所以 NaN 一个都不会被留下
findIndex
以 findIndex
方法来查找元素在数组内第一次出现的位置
function uniq(arr) {
let res = [];
for (let i = 0; i < arr.length; i++) {
if (arr.findIndex(item => item === arr[i]) === i) {
res.push(arr[i]);
}
}
return res;
}
// 运行结果
// [1, "1", "a", {}, {}, { a: 1 }, {}, { a: 1 }, [], [], [1], undefined, null];
// 与 lodash 结果相比 少了 NaN
结果原理和 indexOf
相同,因为用了 ===
的规则来判断元素是否相等,但此方法相当于双层 for
循环
查找元素是否存在型
该方法基本依托 includes
方法来判断对应元素是否在新数组内存在,若不存在则收集
function uniq(arr) {
let res = [];
for (let i = 0; i < arr.length; i++) {
if (!res.includes(arr[i])) {
res.push(arr[i]);
}
}
return res;
}
// 运行结果
// [1, "1", "a", {}, {}, { a: 1 }, {}, { a: 1 }, [], [], [1], undefined, null, NaN];
// 与 lodash 结果相同
includes
方法采用 SameValueZero 判断规则,所以可以判断出并去重 NaN
依托数据类型特性型
该方案依托于数据类型的不重复特性,以达到去重效果
Map
Map 类型的数据可以像 Object 一样,在设定元素键值对的时候可以保证键的唯一,并且将键的类型拓展到了基本所有元素,包括对象,在设定好一个唯一键的 Map 数据类型后,再用其自带的 Map.prototype.keys()
方法取到相应的键类数组,最后将类数组进行一次转化即可
function uniq(arr) {
let map = new Map();
for (let i = 0; i < arr.length; i++) {
!map.has(arr[i]) && map.set(arr[i], true);
}
return [...map.keys()];
}
// 运行结果
// [1, "1", "a", {}, {}, { a: 1 }, {}, { a: 1 }, [], [], [1], undefined, null, NaN];
// 与 lodash 结果相同
Set
与 Map 类似,运用数据类型的特性完成去重,这个方法也是最热门的方法
function uniq(arr) {
return [...new Set(arr)];
}
// 运行结果
// [1, "1", "a", {}, {}, { a: 1 }, {}, { a: 1 }, [], [], [1], undefined, null, NaN];
// 与 lodash 结果相同
性能测试
测试用例
这里简单(不严谨)的将刚才使用的测试例子拼接 100 万次
let a = {};
let b = { a: 1 };
let testArr = [1, 1, "1", "1", "a", "a", {}, {}, { a: 1 }, a, a, b, [], [], [1], undefined, undefined, null, null, NaN, NaN];
let arr = [];
for (let i = 0; i < 1000000; i++) {
arr.push(...testArr);
}
uniq(arr);
先说结果,在简单的测试用例大概 2000 万条数据下,indexOf
的方案速度相对最快
以上,就是我针对数组去重的总结,希望可以给大家一点 JS 数组去重的灵感,有不足的地方,欢迎指出一起讨论 😋
我之前还写过两个小工具,希望可以帮助到大家,一起学习一起进步。