对象相等性判断

原生 JavaScript 的比较问题

在原生 JavaScript 中,在判断 ab 的相等性时,面临如下的比较问题:

  • 0 === -0
  • null == undefined
  • NaN != NaN
  • NaN !== NaN

0 === -0:

对于该问题,我们可以借助如下等式解决:

1/0 === 1/0
1/-0 !== 1/0

null == undefined:

对于该问题,我们可以通过如下等式解决:

null === null;
null !== undefined;

NaN != NaNNaN !== NaN

如果我们要认为 NaN 等于 NaN(这更加符合认知和语义),我们只需要:

if(a !== a)
    return b !== b;

_.isEqual(a,b)

_.isEqual(a,b):判断 ab 是否相等

在 underscore 中提供了该函数用于判断两个变量是否相等,其源码如下:

_.isEqual = function (a, b) {
    return eq(a, b);
};

其内部是通过 eq 函数实现的:

eq = function (a, b, aStack, bStack)
    if (a === b) return a !== 0 || 1 / a === 1 / b;
    if (a == null || b == null) return a === b;
    if (a !== a) return b !== b;
    // Exhaust primitive checks
    var type = typeof a;
    if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;
    // 如果a,bfunction或者object(数组也是object)则意味着二者需要深度比较
    return deepEq(a, b, aStack, bStack);
};

eq 的源码中,我们也发现,当 ab 是数组或者对象时,会调用 deepEq 进行深度比较

在 1.8.3 版本中,没有区分 eqdeepEq 方法

deepEq:深度比较

对于如下几个类型(通过 Object.prototype.toString() 取到),需要进行深度比较:

  • [object RegExp]:正则表达式
  • [object String]:字符串
  • [object Number]:数字
  • [object Date]:日期
  • [object Boolean]:Bool值
  • [object Symbol]:Symbol

这些类型的比较方式在 deepEq 中源码中可以轻松的理解,希望大家能仔细阅读,掌握这些比较的 trick:

deepEq = function(a, b, aStack, bStack) {

// ...

    var className = toString.call(a);
    if (className !== toString.call(b)) return false;
    switch (className) {
        // Strings, numbers, regular expressions, dates, and booleans are compared by value.
        case '[object RegExp]':
        // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i')
        case '[object String]':
            // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
            // equivalent to `new String("5")`.
            return '' + a === '' + b;
        case '[object Number]':
            // `NaN`s are equivalent, but non-reflexive.
            // Object(NaN) is equivalent to NaN.
            if (+a !== +a) return +b !== +b;
            // An `egal` comparison is performed for other numeric values.
            return +a === 0 ? 1 / +a === 1 / b : +a === +b;
        case '[object Date]':
        case '[object Boolean]':
            // Coerce dates and booleans to numeric primitive values. Dates are compared by their
            // millisecond representations. Note that invalid dates with millisecond representations
            // of `NaN` are not equivalent.
            // var a = new Date();
            // +a; // => 1464867835038
            return +a === +b;
        case '[object Symbol]':
            return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b);
    }

// ...

}

对象及数组的深度比较

而对于对象(键值对)和数组的深度比较就较为繁琐了,需要逐个比较当中的元素,并且这些元素也可能是数组或者对象,所以将会用到递归。

由于递归是耗时的,当两个变量不是数组,而是一般的实例化对象时,可以先考虑二者的不等性:当二者的构造函数就不等时,则认为二者不等:

deepEq = function(a, b, aStack, bStack) {

    // ...
    var areArrays = className === '[object Array]';
    if (!areArrays) {
        if (typeof a != 'object' || typeof b != 'object') return false;

        // Objects with different constructors are not equivalent, but `Object`s or `Array`s
        // from different frames are.
        // 构造函数不同的对象必然不等,
        var aCtor = a.constructor, bCtor = b.constructor;
        if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor &&
            _.isFunction(bCtor) && bCtor instanceof bCtor)
            && ('constructor' in a && 'constructor' in b)) {
            return false;
        }
    }
    // ...    

}

而对于数组或者对象这样包含元素或者属性的结构,需要用如下的递归流程来进行比较:

deepEq = function(a, b, aStack, bStack) {

    // ...
    aStack = aStack || [];
    bStack = bStack || [];
    var length = aStack.length;
    while (length--) {
      // Linear search. Performance is inversely proportional to the number of
      // unique nested structures.
      if (aStack[length] === a) return bStack[length] === b;
    }

    // Add the first object to the stack of traversed objects.
    aStack.push(a);
    bStack.push(b);

    // Recursively compare objects and arrays.
    if (areArrays) {
      // Compare array lengths to determine if a deep comparison is necessary.
      length = a.length;
      if (length !== b.length) return false;
      // Deep compare the contents, ignoring non-numeric properties.
      while (length--) {
        if (!eq(a[length], b[length], aStack, bStack)) return false;
      }
    } else {
      // Deep compare objects.
      var keys = _.keys(a), key;
      length = keys.length;
      // Ensure that both objects contain the same number of properties before comparing deep equality.
      if (_.keys(b).length !== length) return false;
      while (length--) {
        // Deep compare each member
        key = keys[length];
        if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false;
      }
    }
    // Remove the first object from the stack of traversed objects.
    aStack.pop();
    bStack.pop();
    return true;
    // ...    

}

很多 underscore 分析文章中对 aStackbStack 的作用一笔带过,说是 暂存 a,b 的值,这么说,和不分析也区别不大。

实际上,作者在这里的 aStackbStack 是用于检测循环引用的,算法参考自 ES 5.1 15.12.3 章节中的 JO。假定我有如下的数组或者对象,当中包含循环引用:

var aArr = [1,2,3];
aArr[3] = aArr;
var bArr = [1,2,3];
bArr[3] = bArr;

var aObj = {
    title: 'underscore'
};
aObj.toSelf = aObj;

var bObj = {
    title: 'underscore'
};
bObj.toSelf = bObj;

如果不借助于上面提到的算法,不借助于辅助的空间 aStack 以及 bStack,仅只是使用普通的递归,那么遇到上述的循环引用结构(cyclic structure),将陷入无穷递归,由于js会对递归调用次数进行限制,所以会报错:

// 递归...

// 比较: aArr[3], bArr[3]
// 比较: aArr[3], bArr[3]
// 比较: aArr[3], bArr[3]
// 比较: aArr[3], bArr[3]
// Uncaught RangeError: Maximum call stack size exceeded

// ....

而借助于 aStack 以及 bStack,我们将能避开无限递归,过程如下图所示:

// 递归...
aStack PUSH: [aArr]
bStack PUSH: [bArr]

// 比较: aArr[3], bArr[3]
// 因为:
aStack[0] === aArr[3]
bStack[0] === bArr[3]
// 所以:
aArr[3] === bArr[3]
return true

// ...

从中可以看到,aStackbStack 这两个栈的目的是:

记录了上级(父级)的引用,当子元素指向父元素时(循环引用),可以通过引用比较跳出无限递归。

测试

var aArr = [1,2,3];
aArr[3] = aArr;
var bArr = [1,2,3];
bArr[3] = bArr;

var aObj = {
    title: 'a'
};
aObj.toSelf = aObj;

var bObj = {
    title: 'b'
};
bObj.toSelf = bObj;
_.isEqual(aObj, bObj); // => false

var aObj = {
    title: 'underscore'
};
aObj.toSelf = aObj;

var bObj = {
    title: 'underscore'
};
bObj.toSelf = bObj;

_.isEqual(aArr, bArr); // => true
_.isEqual(aObj, bObj); // => true
_.isEqual(0, -0); // => false
_.isEqual(NaN, NaN); // => true

results matching ""

    No results matching ""