当我们谈论数组去重我们在谈论什么
一说到数组去重,大家可能就惯性地开始想去重的算法了。
- Set
- 两次for循环暴力去重
- Map + 一次for循环
- 数组的filter方法
- .....
但是我为什么说这次数组去重会引发“血案”,是因为我们都忘记考虑另外一件事了——数组里的元素不一定都是数字(number类型)。
这一次我遇到的面试官让我对这样一个数组去重。
arr=[undefined, undefined, null, null, true, false, 'true', NaN, NaN, 'NaN', {}, {}, [], [], -0, 1, 0]
ok,下面来看看我是怎么成功地掉入陷阱。没有做好这道题,让我顺利地挂了这次面试😭
去重方法
1. Set
众所周知,在JavaScript语言中谈论数组去重一定少不了Set这个数据结构。但是Set能完美解决arr数组去重吗?答案是:NO!。
var arr=[undefined, undefined, null, null, true, false, 'true',
NaN, NaN, 'NaN', {}, {}, [], [], -0, 1, 0]
function uniqueBySet(arr) {
return [... new Set(arr)]
}
uniqueBySet(arr) // [undefined, null, true, false, 'true', NaN, 'NaN', {}, {}, [], [], 0, 1]
咦,两个{}和[]还在,-0怎么不在了...Set里到底发生了什么?
看一看Set在MDN的解释:Set 对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用。
什么意思呢,先看一个例子:
var a = {name:a}, b = a
var c = {name:a}
var d = null, e = d
var f = null
uniqueBySet([a, b, c, d, e, f]) // [{name: a}, {name: a}, null] -->[a , c, d]
在这个例子中,c虽然与a看起来相同,但是堆内存地址绝对是不同的;a通过直接赋值定义b是一个浅拷贝,赋值的其实是a的引用,所以指向的堆内存地址相同。null是基本数据类型,赋值操作是深拷贝。
知识点1:根据MDN的解释和例子说明:基本数据类型会根据值去重,只要值和类型相同就会去重(深拷贝);引用数据类型如果指向的堆内存地址相同(浅拷贝)就会去重。
这个也说明了为什么arr里的{}和[]还在,因为两个{}和[]看起来是一样的,但是堆内存地址不一样,所以在Set里没有去重。不管是undefined还是null都是基础数据类型,只要值相等就会去重。
知识点2:
Set里的-0===0遵循ES5规范返回true,所以-0没了;本来NaN!==NaN,但是这个又不遵守规范返回true。
这一点就没有为什么,就是Set自己的规范。
所以arr用Set去重之后-0没了,NaN的重复也没了。只有object类型没有去重。
Map的key也不能重复,如果重复的话后面的会覆盖前面的值。重复的规则和Set一样。
2. for循环去重
不管是用哪种算法形式去解决数组去重,都会涉及到判断数组中的元素是否相等。
(1) indexOf/splice
一次for循环+indexOf。indexOf可以解决基本数据类型,不能解决object类型、NaN!==NaN和-0===0.
function uniqueByIndexOf(arr) {
var res = []
for (var i = 0; i < arr.length; i++) {
if (res.indexOf(arr[i]) === -1) {
res.push(arr[i])
}
}
return res
}
uniqueByIndexOf(arr) //[undefined, null, true, false, 'true', NaN, NaN, 'NaN', {}, {}, [], [], -0, 1]
通过双重for循环+splice可以在原数组上更改,但是splice可以解决的类型和indexOf一样。这里就不写代码了。
(2) includes
一次for循环+includes。includes可以解决基本数据类型和NaN!==NaN,不能解决object类型和-0===0。
function uniqueByIncludes(arr) {
var res = []
for (var i = 0; i < arr.length; i++) {
if (!res.includes(arr[i])) {
res.push(arr[i])
}
}
return res
}
uniqueByIncludes(arr) //[undefined, null, true, false, 'true', NaN, 'NaN', {}, {}, [], [], -0, 1]
(3) 对象的key属性
因为对象的key是一个变量名,一个对象里只能有唯一的变量名。这个方法是可以解决{}和[],但是不能解决-0===0(因为-0通过toString转化为字符串的结果为'0'),并且对象的key属性里'NaN'和NaN是算同一个,会把string类型都去掉。
function uniqueByKey(arr) {
var res = [],
obj = {}
for (var i = 0; i < arr.length; i++) {
console.log(obj[arr[i]])
if (!obj[arr[i]]) {
// 设定arr中每个元素为obj的key
obj[arr[i]] = 1
res.push(arr[i])
}
}
return res
}
uniqueByKey(arr) //[undefined, null, true, false, NaN, {}, [], -0, 1]
(4) 综合
基于上面的情况,为了能够给arr去重我们需要综合一下。
第一步:把-0挑出来,解决-0===0
第二步:把object类型挑出来去重,解决{}和[],避免基本数据类型string被去掉
第三步:剩下的基本数据类型用includes去重,解决NaN!==NaN
为了挑出-0需要用到ES6新增的一个函数叫Object.is(p1, p2),比较p1和p2是否相等返回布尔值,在这个新方法里-0与0返回false、两个NaN返回true。
function unique(arr) {
var res = [],
obj = {},
flag = false // 表示res数组中没有-0
for (var i = 0; i < arr.length; i++) {
if (!Object.is(-0, arr[i])) { // 第一步:将-0挑出来
if (typeof arr[i] == "object") { // 第二步:去重object类型
if (!obj[arr[i]]) {
// 设定arr中每个元素为obj的key
obj[arr[i]] = 1
res.push(arr[i])
}
} else { // 第三步:去重基本数据类型
if (!res.includes(arr[i])) {
res.push(arr[i])
}
}
} else if (!flag) { // 如果有-0的话就将flag置为true
flag = true
}
}
return flag ? res.concat([-0]) : res
}
unique(arr) //[undefined, null, true, false, 'true', NaN, 'NaN', {}, [], 1, 0, -0]
👌,最后终于完美解决了这一道数组去重的问题。
希望大家以后不要因为这个而引发“血案”了 😂 。。。。。。
最后说一下,大家可以在github上找到我面试各大厂前端的面试题,我都分类分好了。希望对大家有所帮助。