IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    如何在JavaScript中判断两个值相等

    颜海镜发表于 2022-07-10 00:00:00
    love 0

    本文已收录到《面试知识系列》

    在 JavaScript 中如何判断两个值相等,这个问题看起来非常简单,但并非如此,在 JavaScript 中存在 4 种不同的相等逻辑,如果你不知道他们的区别,或者认为判断相等非常简单,那么本文非常适合你阅读。

    ECMAScript 是 JavaScript 的语言规范,在ECMAScript 规范中存在四种相等算法,如下图所示:

    上图中四种算法对应的中文名字如下,大部分前端应该熟悉严格相等和非严格相等,但对于同值零和同值却不熟悉,下面我们分别介绍这四种算法。

    • 同值
    • 同值零
    • 非严格相等
    • 严格相等

    非严格相等

    非严格相等使用两个等号,也就是我们熟悉的双等,非严格相等表示语义相等,不要求类型一样,非严格相等在比较前会先将比较参数类型转换为一致,再进行比较,代码示例如下:

    1 == 1; // true
    1 == '1'; // true 类型不同,不影响比较结果
    

    非严格相等有非常复杂的转换规则,非常难以记忆,社区中有人将上面的规则总结成了图片,一图胜千言,如下图所示:

    为了方便记住非严格相等的的转换逻辑,作者将非对象值,可以总结为如下三条规则:

    • Undefined 只和 Null 相等
    • 和 Number 比较时,另一个值会自动转换为 Number
    • 和 Boolean 比较时,另一个值会转换为 Number

    如果值为对象,会使用内部的 ToPrimitive 转换,可以通过自定义 Symbol.toPrimitive 改变返回值,需要注意的是在相等的判断中 Symbol.toPrimitive 接受的 hint 参数都是 default。

    const obj = {
      [Symbol.toPrimitive](hint) {
        console.log(hint);
        if (hint == 'number') {
          return 1;
        }
        if (hint == 'string') {
          return 'yan';
        }
        return true;
      },
    };
    
    console.log(obj == 1); // obj 返回 true
    console.log(obj == '1'); // obj 返回 true
    console.log(obj == true); // obj 返回 true
    

    非严格相等并非带来了很多便利,通过隐式的自动转换,简化了部分场景的工作,比如 Number 和 String 的自动转换,简化了前端从表单,url 参数中获取值的比较问题,但自动转换带来的问题比便利还多。

    隐式转换的规则,大部分情况下难以驾驭,现在主流的观点已经不建议使用,作者建议只在判断 undefined 和 null 的场景下可以使用非严格相等。

    严格相等

    严格相等是另一种比较算法,其和非严格想等的区别是不会进行类型转换,类型不一致时直接返回 false,严格相等对应===操作符,因为使用三个等号,也被称作三等或者全等,严格相等示例如下:

    1 === 1; // true
    1 === '1'; // false 类型不同,影响比较结果
    

    不同类型值判断规则如下,和前面的非严格相等对比,严格相等更符合直觉。

    严格相等解决了非严格相等中隐式转换带来的问题,但也丢失了隐式转换带来的便利,对于类型可能不一致的情况下,比如从表单中获取的值都是字符串,保险的做法是,在比较前手动类型转换,代码示例如下:

    1 === Number('1'); // true 手动类型转换,类型防御
    

    严格相等几乎总是正确的,但也有例外情况,比如 NaN 和正负 0 的问题。

    Number 类型有个特殊的值 NaN,用来表示计算错误的情概况,比较常见是非 Number 类型和 Number 类型计算时,会得到 NaN 值,代码示例如下所示,这是从表单和接口请求获取数据时很容易出现的问题。

    const a = 0 / 0; // NaN
    const b = 'a' / 1;
    const c = undefined + 1; // NaN
    

    在严格相等中,NaN 是不等于自己的,NaN 是(x !== x) 成立的唯一情况,在某些场景下其实是希望能够判断 NaN 的,可以使用 isNaN 进行判断,ECMAScript 2015 引入了新的 Number.isNaN,和 isNaN 的区别是不会对传入的参数做类型转换,建议使用语义更清晰的 Number.isNaN,但是要注意兼容性问题,判断 NaN 代码示例如下:

    NaN === NaN; // false
    
    isNaN(NaN); // true
    Number.isNaN(NaN); // true
    
    isNaN('aaa'); // true 自动转换类型 'aaa'转换为Number为NaN
    Number.isNaN('aaa'); // false 不进行转换,类型不为Number,直接返回false
    

    严格相等另一个例外情况是,无法区分+0 和-0,代码示例如下,在一些数学计算场景中是要区分语义的。

    +0 === -0; // true
    

    JavaScript 中很多系统函数都使用严格相等,比如数组的 indexOf,lastIndexOf 和 switch-case 等,需要注意,这些对于 NaN 无法返回正确结果,代码示例如下:

    [NaN].indexOf(NaN); // -1 数组中其实存在NaN
    [NaN].lastIndexOf(NaN); // -1
    

    同值零

    同值零是另一种相等算法,名字来源于规范的直译,规范中叫做 SameValueZero,同值零和严格相等功能一样,除了处理 NaN 的方式,同值零认为 NaN 和 NaN 相等,这在判断 NaN 是否在集合中的语义下是非常合理的。

    ECMAScript 2016 引入的 includes 使用此算法,此外 Map 的键去重和 Set 的值去重,使用此算法,代码示例如下:

    [NaN].incdudes(NaN); // true 注意和indexOf的区别,incdudes的语义更合理
    
    new Set([NaN, NaN]); // [NaN] set中只会有个一个NaN,如果 NaN !== NaN的话,应该是[NaN, NaN]
    
    new Map([
      [NaN, 1],
      [NaN, 2],
    ]); // {NaN => 2} 如果 NaN !== NaN的话,应该是 {NaN => 1, NaN => 2}
    

    同值

    同值是最后一种相等算法,其和同值零类似,但认为 +0 不等于 -0,ECMAScript 2015 带来的 Object.is 使用同值算法,代码示例如下:

    Object.is(NaN, NaN); // true
    Object.is(+0, -0); // false 📢 注意这里
    

    同值算法的使用场景是,确定两个值是否在任何情况下功能上是相同的,比较不常用,defineProperty 使用此算法确认键是否存在,例如,将存在的只读属性值-0 修改为+0 时会报错,如果设置为同样的-0 将执行正常,代码示例如下:

    function test() {
      'use strict'; // 需要开启严格模式
      var a = {};
    
      Object.defineProperty(a, 'a1', {
        value: -0,
        writable: false,
        configurable: false,
        enumerable: false,
      });
    
      Object.defineProperty(a, 'a1', {
        value: -0,
      }); // 正常执行
    
      Object.defineProperty(a, 'a1', {
        value: 0,
      }); // Uncaught TypeError: Cannot redefine property: a1
    }
    test();
    

    对于数组判断是否存在的场景,如果想区分+0 和-0,可以使用 ECMAScript 2015 引入的 find 方法,自行控制判断逻辑,代码示例如下:

    [0].includes(-0); // 不能区分-0
    [0].find((val) => Object.is(val, -0)); // 能区分+0和-0
    

    对比四种算法

    下面对比下四种算法的区别,区别如下表所示:

      隐式转换 NaN 和 NaN +0 和 -0
    非严格相等(==) 是 false true
    严格相等(===) 否 false true
    同值零(includes 等) 否 true true
    同值(Object.is) 否 true false

    Number 类型的坑

    Number 类型真的有太多坑了,除了上面提到的 NaN 和正负零的问题,还存在其他语言都存在的小数问题,小数问题是前端比较容易踩坑的地方,如果想对比两个小数是否相同,可能会违反直觉,比如 0.1+0.2 并不和 0.3 全等,代码示例如下:

    const a = 0.1 + 0.2; // 0.30000000000000004
    
    a === 0.3; // false
    

    如果要理解上面的原因,需要知道 JavaScript 是如何存储小数的,我之前曾经写个两篇文章,专门介绍这个问题:

    • 聊聊 JavaScript 中的二进制数
    • 每一个 JavaScript 开发者应该了解的浮点知识

    简单来说 JavaScript 使用 IEEE-754 规范存储浮点数,这意味着每个浮点数占 64 位,具体含义如下图所示:

    因此 JavaScript 中的最小数字 2-52,对应的十进制约等于 2.2204460492503130808472633361816E-16,这个数字比较难以记忆,ECMAScript 2015 引入了 Number.EPSILON 常量表示这个数字,使用方法如下

    console.log(Number.EPSILON); // 2.220446049250313e-16
    

    对于小数的比较,一般都是让两个数字做减法,如果其差值,小于 Number.EPSILON,就认为其相等,代码示例如下:

    var a = 0.1 + 0.2; // 0.30000000000000004
    
    a - 0.3 < Number.EPSILON; // true 可认为 a === 0.3
    

    结构相等

    前面介绍了各种判断相等的办法,都只能用于基本类型,如果有两个内容一样的对象,使用上面的方法都会返回 false,在 JavaScript 中缺少判断两个引用类型结构相等的功能,比如如下的 a1 和 a2,并不相等:

    const a1 = { a: 1 };
    const a2 = { a: 1 };
    
    a1 == a2; // false
    a1 === a2; // false
    Object.is(a1, a2); // false
    

    通过将对象序列化,可以将结构相等,转换为字符串相等,在 JavaScript 中序列化需要用到 JSON.stringify,使用 JSON.stringify 判断结构相等的示例代码如下:

    const a1 = { a: 1 };
    const a2 = { a: 1 };
    
    JSON.stringify(a1) === JSON.stringify(a2); // true
    

    大部分同学可能就是使用的,但其实这种方法是有缺陷的,比如某些值,在序列化后会丢失,从而导致判断逻辑错误,比如下面的值都会有问题:

    • NaN 序列化后和 null 无法区分;
    • +0 和-0 在序列化后也无法区分;
    • 溢出的数字和 null 无法区分;

    比如如下两个对象,结构并不相等,但序列化后值是一样的:

    const a1 = {
      a: NaN,
    };
    
    const a2 = {
      a: null,
    };
    
    JSON.stringify(a1); // '{"a":null}'
    JSON.stringify(a2); // '{"a":null}'
    

    此外,还有一些值不能被序列化,比如 undefined 和 symbol,序列化后就丢失了,代码示例如下:

    const a = {
      a: undefined,
      b: Symbol(''),
    };
    
    JSON.stringify(a); // '{}' 值丢失了
    

    由于 JSON.stringify 的方法走不通,另一种思路是自己写代码判断结构相等,其原理是依次遍历递归比较两个树是否相等,也可以使用社区中别人写好的库,比如isequal。

    鉴于篇幅,本文不在给出递归判断结构相等的代码,在下一篇文章中,给大家带来 isequal 的源码分析。

    最后,欢迎大家阅读本文,如果对本文有任何疑问,欢迎在评论区交流。



沪ICP备19023445号-2号
友情链接