一道数组去重面试题引发的“血案”

文章转自: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自己的规范。

所以arrSet去重之后-0没了,NaN的重复也没了。只有object类型没有去重。

Map的key也不能重复,如果重复的话后面的会覆盖前面的值。重复的规则和Set一样。

2. for循环去重

不管是用哪种算法形式去解决数组去重,都会涉及到判断数组中的元素是否相等。

(1) indexOf/splice

一次for循环+indexOfindexOf可以解决基本数据类型,不能解决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循环+includesincludes可以解决基本数据类型和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是否相等返回布尔值,在这个新方法里-00返回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
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值