webproblem/Blog

从underscore源码看如何判断两个对象相等

Opened this issue · 3 comments

首先要清楚 JavaScript 中的相等分为宽松相等(==)和严格相等(===)。宽松相等在比较值的时候会先进行类型的隐式转换,严格相等下如果比较值的类型不一致,那么就判定比较值不全等。如果比较值是引用类型,宽松相等和严格相等就不能直接判断出值是否相等了(引用类型浅拷贝比较值除外,也就是比较值指向的是同一引用地址),原因是对于任意两个不同的非原始对象,即便他们有相同的结构,都会计算得到 false 。

var num = 1;
var str = '1';
console.log(num == str); // true
console.log(num === str); // false

var obj1 = {name: '白展堂'};
var obj2 = {name: '白展堂'};
var obj3 = obj1;
console.log(obj1 == obj2); // false
console.log(obj1 === obj2); // false
console.log(obj1 == obj3); // true
console.log(obj1 === obj3); // true

var arr1 = [1];
var arr2 = [1];
console.log(arr1 == arr2); // false
console.log(arr1 === arr2); // false

JSON.stringify

如何判断对象是否相等?

一种解决方案就是使用 JSON.stringify 序列化成字符串再做比较。

var obj1 = {name: '白展堂', age: 25};
var obj2 = {name: '白展堂', age: 25};
JSON.stringify(obj1) === JSON.stringify(obj2); // true

var arr1 = ['a', 'b', 'c', 'd'];
var arr2 = ['a', 'b', 'c', 'd'];
JSON.stringify(arr1) === JSON.stringify(arr2); // true

这种方案看似可以判断出对象是否相等,但是会不会存在问题呢?看过 underscore 源码的都知道,isEqual 函数的实现有多复杂,很多种情况显然不是通过 JSON.stringify 序列化就能解决的。

先来分析下 JSON.stringify 方案存在的问题,假设比较对象中的属性值存在 RegExp 对象,判定结果是怎样的呢?

function eq(a, b) {
    return JSON.stringify(a) === JSON.stringify(b);
}
var obj1 = {name: '白展堂', reg: /test1/i};
var obj2 = {name: '白展堂', reg: /test2/i};
eq(obj1, obj2); // true

结果为 true,也就是说 obj1 和 obj2 序列化的字符串是一致的。

var obj1 = {name: '白展堂', reg: /test1/i};
var obj2 = {name: '白展堂', reg: /test2/i};
JSON.stringify(obj1); // "{"name":"白展堂","reg":{}}"
JSON.stringify(obj2); // "{"name":"白展堂","reg":{}}"

可以看到,JSON.stringify 将 RegExp 对象序列化成了 '{}',也就是说 JSON.stringify 序列化对于某些情况会存在问题,比如 undefined 和 Function 函数在序列化过程中会被忽略。

function test() {}
JSON.stringify(undefined) === JSON.stringify(test); // true

_.isEqual

那么如何完美的判断对象或值相等?现在来看看 underscore 中的 isEqual 函数是如何针对不同的比较值进行处理的。

区分 +0 与 -0 之间的差异

ECMAScript 中,认为 0 与 -0 是相等的,其实不然。

1 / 0 // Infinity
1 / -0 // -Infinity
1 / 0 === 1 / -0 // false

原因是因为 JavaScript 中的 Number 是64位双精度浮点数,采用了IEEE_754 浮点数表示法,这是一种二进制表示法,按照这个标准,最高位是符号位(0 代表正,1 代表负),剩下的用于表示大小。而对于零这个边界值 ,1000(-0) 和 0000(0)都是表示 0 ,这才有了正负零的区别。

那么如何区分 0 与 -0?

function eq(a, b) {
    // 比较值a,b相等且值不是0和-0的情况
    if(a === b) {
        return a !== 0 || 1 / a === 1 / b; 
    }
    return false;
}
eq(0, 0); // true
eq(0, -0); // false

判断值是否为 NaN

判断某个值是否为 NaN 时不能直接比较这个值是否等于 NaN,因为 ECMAScript 中 NaN 不等于自身,可以使用原生函数 Number.isNaN() 或 isNaN()。

var a = NaN;
a === NaN; // false
isNaN(a); // true

那么自己如何实现判断 NaN 值的方法?利用 NaN 不等于自身的原理。

function eq(a, b) {
    if(a !== a) return b !== b; 
}
eq(NaN, NaN); //true
eq(NaN, 'test'); // false

隐式类型转换

对于 RegExp,String,Number,Boolean 等类型的值,假设一个比较值是字面量赋值,另一个比较值的通过构造函数生成的,ECMAScript 会认为两个值并不相等。

var s1 = 'test';
var s2 = new String('test');
console.log(s1 === s2); // false
typeof s1; // 'string'
typeof s2; // 'object'

var n1 = 100;
var n2 = new Number(100);
console.log(n1 === n2); // false
typeof n1; // 'number'
typeof n2; // 'object'

原因是因为字面量赋值的变量和构造函数生成的变量之间的类型不同,前面说过,严格相等下不同类型的值是不全等的,那么如何处理这种情况?答案是对比较值进行隐式转换。

image

递归遍历

对于 toString() 是 Array 和 Object 类型的比较值,则循环遍历里面的元素或属性进行比较,只有length 属性值相等且里面的元素或属性都相等的情况下,就说明两个比较值是相等的了。存在一种情况就是比较值里的元素或者属性值是一个嵌套的对象,这就需要使用递归遍历。

image

PS: underscore 源码中的 _.isEqual 源码注释地址: 源码注释

参考

用心了,讲的很细致!

隐式类型转换第二段,有几个错别字😁

@YoungLightMing 多谢指出,已更正