From 6f62f258cb4c1c45248c30c56cfd0ceea7051086 Mon Sep 17 00:00:00 2001 From: Kit Cambridge Date: Mon, 5 Sep 2011 12:25:59 -0600 Subject: [PATCH] Add support for comparing string, number, and boolean object wrappers. Ignore inherited properties when deep comparing objects. Use a more efficient `while` loop for comparing arrays and array-like objects. --- underscore.js | 75 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 26 deletions(-) diff --git a/underscore.js b/underscore.js index 7615b5d5e..825e9405d 100644 --- a/underscore.js +++ b/underscore.js @@ -603,23 +603,29 @@ // Compare object types. var typeA = typeof a; if (typeA != typeof b) return false; - // The type comparison above prevents unwanted type coercion. - if (a == b) return true; // Optimization; ensure that both values are truthy or falsy. if (!a != !b) return false; - // `NaN` values are equal. - if (_.isNaN(a)) return _.isNaN(b); + // Compare string objects by value. + var isStringA = _.isString(a), isStringB = _.isString(b); + if (isStringA || isStringB) return isStringA && isStringB && String(a) == String(b); + // Compare number objects by value. `NaN` values are equal. + var isNumberA = toString.call(a) == '[object Number]', isNumberB = toString.call(b) == '[object Number]'; + if (isNumberA || isNumberB) return isNumberA && isNumberB && (_.isNaN(a) ? _.isNaN(b) : +a == +b); + // Compare boolean objects by value. The value of `true` is 1; the value of `false` is 0. + var isBooleanA = toString.call(a) == '[object Boolean]', isBooleanB = toString.call(b) == '[object Boolean]'; + if (isBooleanA || isBooleanB) return isBooleanA && isBooleanB && +a == +b; // Compare dates by their millisecond values. var isDateA = _.isDate(a), isDateB = _.isDate(b); if (isDateA || isDateB) return isDateA && isDateB && a.getTime() == b.getTime(); // Compare RegExps by their source patterns and flags. var isRegExpA = _.isRegExp(a), isRegExpB = _.isRegExp(b); if (isRegExpA || isRegExpB) - return isRegExpA && isRegExpB && - a.source == b.source && - a.global == b.global && - a.multiline == b.multiline && - a.ignoreCase == b.ignoreCase; + // Ensure commutative equality for RegExps. + return isRegExpA && isRegExpB && + a.source == b.source && + a.global == b.global && + a.multiline == b.multiline && + a.ignoreCase == b.ignoreCase; // Ensure that both values are objects. if (typeA != 'object') return false; // Unwrap any wrapped objects. @@ -627,30 +633,47 @@ if (b._chain) b = b._wrapped; // Invoke a custom `isEqual` method if one is provided. if (typeof a.isEqual == 'function') return a.isEqual(b); - if (typeof b.isEqual == 'function') return b.isEqual(a); - // Compare array lengths to determine if a deep comparison is necessary. - if ('length' in a && (a.length !== b.length)) return false; - // Assume equality for cyclic structures. + // If only `b` provides an `isEqual` method, `a` and `b` are not equal. + if (typeof b.isEqual == 'function') return false; + // Assume equality for cyclic structures. The algorithm for detecting cyclic structures is + // adapted from ES 5.1 section 15.12.3, abstract operation `JO`. var length = stack.length; while (length--) { + // Linear search. Performance is inversely proportional to the number of unique nested + // structures. if (stack[length] == a) return true; } // Add the first object to the stack of traversed objects. stack.push(a); - // Deep compare the two objects. - var size = 0, sizeRight = 0, result = true, key; - for (key in a) { - // Count the expected number of properties. - size++; - // Deep compare each member. - if (!(result = key in b && eq(a[key], b[key], stack))) break; - } - // Ensure that both objects contain the same number of properties. - if (result) { - for (key in b) { - if (++sizeRight > size) break; + var size = 0, result = true; + if (a.length === +a.length || b.length === +b.length) { + // Compare object lengths to determine if a deep comparison is necessary. + size = a.length; + result = size == b.length; + if (result) { + // Deep compare array-like object contents, ignoring non-numeric properties. + while (size--) { + // Ensure commutative equality for sparse arrays. + if (!(result = size in a == size in b && eq(a[size], b[size], stack))) break; + } + } + } else { + // Deep compare objects. + for (var key in a) { + if (hasOwnProperty.call(a, key)) { + // Count the expected number of properties. + size++; + // Deep compare each member. + if (!(result = hasOwnProperty.call(b, key) && eq(a[key], b[key], stack))) break; + } + } + // Ensure that both objects contain the same number of properties. + if (result) { + for (key in b) { + if (hasOwnProperty.call(b, key) && !size--) break; + } + result = !size; } - result = size == sizeRight; } // Remove the first object from the stack of traversed objects. stack.pop();