从数组去重中学习 JavaScript 判断相等的方法

JavaScript 中数组去重的方法有很多,但是每种方法绕不过的就是判断元素的相等。由于 JS 动态数据类型与隐式转换的关系,判断相等时会有一些特性的不同。有时候生硬的去记忆效果不好,不如从数组去重的例子中学习会有更好的理解。

JavaScript 数据类型有:字符串(string),数值(number),布尔(boolean),undefined,null,引用类型,es6中还有 symbol。

判断相等的方法:=====,Object.is,SameValueZero

以下方法统一测试数组:

var array = [-0,-0,0,0,+0,+0,false,"0","0",false,undefined,null,true,"true",NaN,NaN,'NaN',{},{}];

方法一:indexOf

function unique(arr){
    var array = [];
    for(var i = 0;i<arr.length;i++){
        if(array.indexOf(arr[i])==-1){
            array.push(arr[i])
        }
    }
    return array;
}

unique(array);
//output:[-0, false, "0", undefined, null, true, "true", NaN, NaN, "NaN", {…}, {…}]

indexOf 按严格相等来查找元素在数组中的索引,也就是说与=== 是一样的。从以上输出的结果来看,严格相等不会区分0与+0,-0;不会有隐式转换等。

+0 === 0; //true
-0 === 0; //true
NaN === NaN; //false

这里需要注意的是判断对象的相等,在 JS 中,并不是判断判断对象内的属性值,而是判断在等式的两边是否是同一个对象引用,也可以说是判断引用地址是否相同。

{}==={}; //false

如果我一定要去除数组中重复的空对象,有没有办法呢?也是有办法的,后面我会解释。

因为 indexOf 是 es5 才出现的,更早版本的浏览器是不支持的。所以使用的更早的或最多是下面这种方法。

方法二:===

function unique(arr){
    for(var i=0;i<arr.length;i++){
        for(var j=i+1;j<arr.length;j++){
            if(arr[i]===arr[j]){
                arr.splice(j,1);
                j--;
            }
        }
    }
    return arr;
}
//output:[-0, false, "0", undefined, null, true, "true", NaN, NaN, "NaN", {…}, {…}]

这里 splice 方法要比 indexOf 实现的要早,但他并不是关键。用不用 splice 方法无关紧要,你可以在函数内部再创建一个空数组,在判断是否相等后将唯一值放入你创建的数组中。这里采用这种方法是为了减少循环次数。这里的=== 能不能换成== 呢?答案是不能的。

0 == false; //true
undefined == null; //true

== 是存在隐式转换的,js 是动态类型语言,隐式转换给 js 语言带来了很大的灵活性,但有的时候不注意也会带来很多的麻烦。对于隐式转换是一个需要去探讨的知识点,因为往往有 “when?” 和 “how?” 两大疑问,即什么时候需要转换,到底是怎么转换的?(此处只讨论相等,不讨论隐式转换)只有你在确定了要判断值的数据类型时才去用== ,比如上面的 indexOf ,确定了输出结果一定为一个 number 类型的值。

关于“0”和“NaN”

+0和-0是否相等?在有理数的四则运算中区分+0和-0是没有必要的,但在微积分的计算中是需要区分的。在计算机内部的机器码表示上,+0和-0也是不同的,因为机器码的符号位、反码和补码的缘故。如果你不需要区分+0与-0,可以使用===,也可以用 Object.is() 区分。

+0===-0; //true
Object.is(0,-0); //false 这里默认你输入的0就是+0

NaN 的数据类型是 number,虽然他是“not a number”的缩写,表示的是值不为一个数。

typeof NaN; //number

那为什么 NaN 和自身不相等呢?

NaN === NaN; //false

这是因为 NaN 在机器码中并不是一个确定的值,它是一类二进制码的统称。在IEEE 754 双精度表示中,每一个 number都一样表示为:

[V=(-1)^s imes M imes 2^E ]

s表示符号位,M表示有效数字,E表示指数位。

img

当 E 位全为1,M位不全为0时,表示 NaN。

以上双精度浮点数的表述是简化了的,详细的内容可以网上查阅 IEEE 754的标准文档。

方法三:Object.is

如果想要去重 NaN,可以使用 Object.is();

Object.is(NaN,NaN); //true

所以可以把方法改写为:

function unique(arr){
    for(var i=0;i<arr.length;i++){
        for(var j=i+1;j<arr.length;j++){
            if(Object.is(arr[i],arr[j])){
                arr.splice(j,1);
                j--;
            }
        }
    }
    return arr;
}
//output:[-0, 0, false, "0", undefined, null, true, "true", NaN, "NaN", {…}, {…}]

注意 Object.is 只在较高版本的浏览器中使用。

方法四:hasOwnProperty

前面说了0和NaN,还没有处理数组中的对象类型的数据。这里指的是引用类型的数据,包括 Array、Object,es6出现的 Map、Set 等。上面测试用例中的空对象是引用对象的一个代表。在 JS 中判断引用对象是否相等实际上是判断引用地址是否相同,也就是判断等式两边的操作数是否引用的内存中同一对象。

{} === {}; //false
var o1 = {};
var o2 = o1;
o1 === o2; //true

如果我的关注点不在对象本身,而在对象内存储的信息,比方说:

[1,2,3] ?== [1,2,3];
{a:1,b:2} ?== {a:1,b:2}

你可能会在有些博客上看到用这种方法去重空对象:

function unique1(arr){
    var obj = {};
    return arr.filter(function(item){
        return obj.hasOwnProperty(typeof item + item)?false:(obj[typeof item + item]=true);
    })
}

这种方法就是将数组中的值转换为字符串,再作为 key 传入到对象中,用 hasOwnproperty 判断存在否。因为 hasOwnproperty 方法判断的都是字符串,倒是不用繁琐的考虑数据类型带来的问题。但是这种方法是有问题的,并不推荐使用。问题就出在+ 号操作符所带来的隐式转换。

typeof item +item;

这个用法很巧妙

typeof NaN + NaN;  //"numberNaN"
typeof 'NaN' + 'NaN';  //"stringNaN"
typeof [1,2,3] + [1,2,3];  //"object1,2,3"
typeof {} + {}; //"object[object Object]"

当数组中出现多个 object 时,值都会是"object[object Object]",这样只有第一个会保留,后面的的会去除,显然这种行为是错误的。

所以+ 操作符数组中只有基本数据类型和数组是适用的,但是有普通对象或者 es6 出现的 map、set 是是不适用的。为什么数组和 map、set 类型的隐式转换不同?主要是因为重写的 toString 方法的不同。

JSON.stringify 方法能解决一些问题:

function unique2(arr){
    var obj = {};
    return arr.filter(function(item){
        return obj.hasOwnProperty(JSON.stringify(item))?false:(obj[JSON.stringify(item)]=true);
    })
}
//test:arr = [{a:1,b:2},{a:1,b:2},{},{}]
//output:[{a:1,b:2},{}]

JSON.stringify 似乎解决了我们想要的对象去重,但是还是有问题,比如 NaN,会被转换成 null,对象中的循环引用等,还有一堆其他需要注意的规则。

unique1 和 unique2 好像都不能完美的解决问题。当数组中不包含对象时,用 unique1,去重对象,可以用JSON.stringify。

其实后来我想想,去重空对象真的有意义吗?

或者说空对象真的是空吗?

var o1 = {};
var o2 = Object.create(null);
JSON.tringify(o1) === JSON.stringify(o2); //true

o2 是没有原型的空对象,o1是有原型的空对象,两个都是空对象,他们相等吗?

方法五:Map

function unique(arr){
    var map = new Map;
    return arr.filter(function(item){
        if(map.has(item)){
            return false;
        }else{
            map.set(item,true);
            return true;
        }
    })
}
//output:[-0, false, "0", undefined, null, true, "true", NaN, "NaN", {…}, {…}]

Map、Set 和 Array 的 includes 方法都是基于 sameValueZero 算法的。

sameValueZero:

  • NaN 是与 NaN 相等的(虽然 NaN !== NaN),剩下所有其它的值是根据 === 运算符的结果判断是否相等。

方法六:Set

[...new Set(array)];

简单方便。JavaScript 数组去重之所以有这么一大堆特殊情况,好像复杂许多,也就是 JS 动态语言的特性造成的。如果像 Java 那样支持类型化数组和泛型,那估计就没这么多麻烦事了。当然,JavaScript 的优点也是他这样的灵活性,编程时应尽量规避他的那些不好的特性,避免去踩那些坑。

参考文章

原文地址:https://www.cnblogs.com/arduka/p/13688689.html