内容概要:
需要去重数组模板:
const arr = [
0,0,0,
'str','str','str',
true,true,true,'true',
undefined,undefined,undefined,'undefined',
null,null,null,'null',
NaN,NaN,NaN,'NaN',
{val: 1},{val: 1},
{},{},
[1,2,3],[1,2,3],
[],[],
];
const set = new Set(arr);
// 不能用 Array.prototype.slice.call 方法转换set为数组
// const uniqueArr = Array.prototype.slice.call(set);
// const uniqueArr = [...set]; // 可行
const uniqueArr = Array.from(set); // 可行
console.log('isArray:', Array.isArray(uniqueArr))
console.log(uniqueArr);
通过 Set 的无重复性可以得到一个 set 实例对象,再转换成数组即可。
得到的 uniqueArr 是:
[
0,
'str',
true,'true',
undefined,'undefined',
null,'null',
NaN,'NaN',
{val: 1},{val: 1},
{},{},
[1,2,3],[1,2,3],
[],[],
];
需要注意的几点:
1.虽然使用 NaN === NaN 判断出来为 false,但是在 set 去重的过程中会去掉重复的 NaN;
2.set 去重并不能直接去掉引用类型,因为判断的是地址是否相同,可以看下面一个例子:
const obj = { val: 1 };
const copyObj = obj; // 引用类型,浅拷贝其地址
const arr = [obj, obj, copyObj]
const set = new Set(arr);
const uniqueArr = Array.from(set);
console.log(uniqueArr); // [{val: 1}]
3.set 去重不会改变原数组
本部分利用各种 API 进行去重,本质上都是遍历数组,取值进行对比
采用 SameValueZero 算法,相关内容可以查阅 ES 标准中的相等比较算法
const unique = (arr) => {
for (let i = 0; i < arr.length; i += 1) {
const iValue = arr[i];
const iValueIsNaN = Number.isNaN(iValue);
for (let j = i + 1; j < arr.length;) {
let jValue = arr[j];
if (iValueIsNaN && Number.isNaN(jValue)) {
arr.splice(j, 1);
continue;
}
if (iValue === jValue) {
arr.splice(j, 1);
continue;
}
// 由于去除数组元素会导致索引改变,因此只能在未调用splice时进行索引增加
j += 1;
}
}
return arr;
}
const uniqueArr = unique(arr);
console.log(uniqueArr);
得到的去重结果与使用 set 方法去重一致
需要注意的几点:
针对上面第三点,如果使用该方法又不想改变原数组,可以进行一次深拷贝即可,对应内容可查阅 JS 浅拷贝与深拷贝
includes 方法基于 SameValueZero 算法,因而可以判断重复的 NaN ,而 indexOf 基于 严格相等比较算法(===) 无法去重 NaN。
const a = [NaN];
console.log(a.includes(NaN)); // true
console.log(a.indexOf(NaN)); // false
代码实现:
const unique = (arr) => {
const newArr = [];
for (let i = 0; i < arr.length; i += 1) {
const iValue = arr[i];
// includes 方法
if (!newArr.includes(iValue)) {
newArr.push(iValue);
}
// indexOf 方法
// if (newArr.indexOf(iValue) === -1) {
// newArr.push(iValue);
// }
}
return newArr;
}
const uniqueArr = unique(arr);
console.log(uniqueArr);
可自行打印结果,可以看到,includes 方法去重效果与 set 一致,而 indexOf 方法无法去重 NaN 。
需要注意的几点:
使用 sort 方法,保证数组有序,对比相邻两个元素是否相等,由于数组排序会导致顺序错乱,非数值型不推荐使用,这里也不再赘述,实现方式与上面类似。
在循环遍历的方式中,比较常用的是 2.1 ,2.2 中的方法,当然也还有其他方式,感兴趣的可以自己去实现
const unique = (arr) => {
const map = new Map();
let newArr = [];
for(let i = 0; i < arr.length; i += 1) {
const iValue = arr[i];
if (!map.has(iValue)) {
newArr.push(iValue);
map.set(iValue, true)
}
}
return newArr;
}
const uniqueArr = unique(arr);
console.log(uniqueArr);
结果与使用 set 方法去重一致
需要注意的几点:
补充,利用对象属性不会重复的特点去重,实现上与 map 去重类似。实际上,利用该方法是有缺陷的,举个例子:
const obj = {};
obj[{ a: 1 }] = 1;
obj[{ b: 2 }] = 2;
console.log(obj); // {[object Object]: 2}
可以看到,{ a: 1 } 和 { b: 2 } 是两个不同的对象作为对象属性,然而后面的对象会覆盖前面的对象,如此存放引用类型的时候是会有遗漏的。
实际上,也不推荐使用 map 键不可重复特性与 object 属性不可重复性进去去重,相比前面几种方式,并没有特别的优势。
需要提前说明的是,虽然函数同样是引用类型,但是其本身并不常用于存储数据,因此这儿指的引用类型只有对象与数组。
前面的方法中,最常用的是 set 方法,splice 方法,includes 和 indexOf 方法,都实现了 NaN 的去重,却也都没有实现引用类型的去重。因此接下来会实现一个完整版去重方法,包括引用类型的去重。
function deepEquals (val1, val2) {
// NaN 对比情况判断为相同
if (Number.isNaN(val1) && Number.isNaN(val2)) return true;
// 是否为对象
let valIsObject = function (val) {
return typeof val === 'object' && val !== null;
}
if (!valIsObject(val1) || !valIsObject(val2)) return val1 === val2;
// 当val1和val2都为对象时,若地址相同则相等
if (val1 === val2) return true;
// 到这一步可以判断 val1 和 val2 都是对象,为了区分 {} 和 [],如果没有此判断,会导致 deepEquals({}, []) 返回 true
let isEmptyObj = (Array.isArray(val1) && !Array.isArray(val2)) || (Array.isArray(val2) && !Array.isArray(val1));
if (isEmptyObj) return false;
// 获取键的长度,长度不等则不同,也是为了下面遍历时以短的为基准
if (Object.keys(val1).length !== Object.keys(val2).length) return false;
for (let key in val1) {
if (val1.hasOwnProperty(key)) {
const isEqual = deepEquals(val1[key], val2[key])
if (!isEqual) return isEqual;
}
}
return true;
}
想了解更全面的深比较,可以查阅 lodash 的 isEqual 源码。
const unique = (arr) => {
const newArr = [];
for (let i = 0; i < arr.length; i += 1) {
const iValue = arr[i];
if (newArr.findIndex((item) => deepEquals(item, iValue)) === -1) {
newArr.push(iValue);
}
}
return newArr;
}
const uniqueArr = unique(arr);
console.log(uniqueArr);
uniqueArr 结果:
[
0,
'str',
true,'true',
undefined,'undefined',
null,'null',
NaN,'NaN',
{val: 1},
{},
[1,2,3],
[],
];
与 set 方法去重做对比,可以看到,引用类型也实现去重。