文章转自:https://juejin.cn/post/6860731239754792968
当我们谈论数组去重我们在谈论什么
一说到数组去重,大家可能就惯性地开始想去重的算法了。
- 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上找到我面试各大厂前端的面试题,我都分类分好了。希望对大家有所帮助。
作者:Liqiuyue
链接:https://juejin.cn/post/6860731239754792968
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。