• 从数组去重中学习 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 的优点也是他这样的灵活性,编程时应尽量规避他的那些不好的特性,避免去踩那些坑。

    参考文章

  • 相关阅读:
    Set存储元素为啥是唯一的(以HashSet为例源码分析)
    HashTable原理与源码分析
    手写spring(简易版)
    java--String equals方法
    [java]创建一个默认TreeMap() key为什么不能为null
    [java]类初始化挺有意思的题目
    [java] 为什么重写equals()必须要重写hashCode()
    java --Integer 学习
    减少重复代码的书写--Lombok
    JavaScript随笔
  • 原文地址:https://www.cnblogs.com/arduka/p/13688689.html
Copyright © 2020-2023  润新知