diff --git a/test/objects.js b/test/objects.js index 48971c079..f1cc7e1f6 100644 --- a/test/objects.js +++ b/test/objects.js @@ -82,6 +82,46 @@ $(document).ready(function() { ok(!_.isEqual({x: 1, y: undefined}, {x: 1, z: 2}), 'objects with the same number of undefined keys are not equal'); ok(!_.isEqual(_({x: 1, y: undefined}).chain(), _({x: 1, z: 2}).chain()), 'wrapped objects are not equal'); equals(_({x: 1, y: 2}).chain().isEqual(_({x: 1, y: 2}).chain()).value(), true, 'wrapped objects are equal'); + + // Objects with circular references. + var circularA = {'abc': null}, circularB = {'abc': null}; + circularA.abc = circularA; + circularB.abc = circularB; + ok(_.isEqual(circularA, circularB), 'objects with a circular reference'); + circularA.def = 1; + circularB.def = 1; + ok(_.isEqual(circularA, circularB), 'objects with identical properties and a circular reference'); + circularA.def = 1; + circularB.def = 0; + ok(!_.isEqual(circularA, circularB), 'objects with different properties and a circular reference'); + + // Arrays with circular references. + circularA = []; + circularB = []; + circularA.push(circularA); + circularB.push(circularB); + ok(_.isEqual(circularA, circularB), 'arrays with a circular reference'); + circularA.push('abc'); + circularB.push('abc'); + ok(_.isEqual(circularA, circularB), 'arrays with identical indices and a circular reference'); + circularA.push('hello'); + circularB.push('goodbye'); + ok(!_.isEqual(circularA, circularB), 'arrays with different properties and a circular reference'); + + // Hybrid cyclic structures. + circularA = [{'abc': null}]; + circularB = [{'abc': null}]; + circularA[0].abc = circularA; + circularB[0].abc = circularB; + circularA.push(circularA); + circularB.push(circularB); + ok(_.isEqual(circularA, circularB), 'cyclic structure'); + circularA[0].def = 1; + circularB[0].def = 1; + ok(_.isEqual(circularA, circularB), 'cyclic structure with identical properties'); + circularA[0].def = 1; + circularB[0].def = 0; + ok(!_.isEqual(circularA, circularB), 'cyclic structure with different properties'); }); test("objects: isEmpty", function() { diff --git a/underscore.js b/underscore.js index 5757609fa..6bc1ed9e4 100644 --- a/underscore.js +++ b/underscore.js @@ -391,7 +391,6 @@ return -1; }; - // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. _.lastIndexOf = function(array, item) { if (array == null) return -1; @@ -538,7 +537,6 @@ }; }; - // Object Functions // ---------------- @@ -596,44 +594,63 @@ }; // Perform a deep comparison to check if two objects are equal. - _.isEqual = function(a, b) { - // Check object identity. - if (a === b) return true; - // Different types? - var atype = typeof(a), btype = typeof(b); - if (atype != btype) return false; - // Basic equality test (watch out for coercions). - if (a == b) return true; - // One is falsy and the other truthy. - if ((!a && b) || (a && !b)) return false; - // Unwrap any wrapped objects. - if (a._chain) a = a._wrapped; - if (b._chain) b = b._wrapped; - // One of them implements an isEqual()? - if (a.isEqual) return a.isEqual(b); - if (b.isEqual) return b.isEqual(a); - // Check dates' integer values. - if (_.isDate(a) && _.isDate(b)) return a.getTime() === b.getTime(); - // Both are NaN? - if (_.isNaN(a) && _.isNaN(b)) return false; - // Compare regular expressions. - if (_.isRegExp(a) && _.isRegExp(b)) - return a.source === b.source && - a.global === b.global && - a.ignoreCase === b.ignoreCase && - a.multiline === b.multiline; - // If a is not an object by this point, we can't handle it. - if (atype !== 'object') return false; - // Check for different array lengths before comparing contents. - if (a.length && (a.length !== b.length)) return false; - // Nothing else worked, deep compare the contents. - var aKeys = _.keys(a), bKeys = _.keys(b); - // Different object sizes? - if (aKeys.length != bKeys.length) return false; - // Recursive comparison of contents. - for (var key in a) if (!(key in b) || !_.isEqual(a[key], b[key])) return false; - return true; - }; + _.isEqual = (function() { + function eq(a, b, stack) { + // Identical objects are equal. + if (a === b) return true; + // A strict comparison is necessary because `null == undefined`. + if (a == null) return a === b; + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className != toString.call(b)) return false; + // Compare functions by reference. + if (_.isFunction(a)) return _.isFunction(b) && a == b; + // Compare strings, numbers, dates, and booleans by value. + if (_.isString(a)) return _.isString(b) && String(a) == String(b); + if (_.isNumber(a)) return _.isNumber(b) && +a == +b; + if (_.isDate(a)) return _.isDate(b) && a.getTime() == b.getTime(); + if (_.isBoolean(a)) return _.isBoolean(b) && +a == +b; + // Compare RegExps by their source patterns and flags. + if (_.isRegExp(a)) return _.isRegExp(b) && a.source == b.source && + a.global == b.global && + a.multiline == b.multiline && + a.ignoreCase == b.ignoreCase; + // Recursively compare objects and arrays. + if (typeof a != 'object') return false; + // Unwrap any wrapped objects. + if (a._chain) a = a._wrapped; + if (b._chain) b = b._wrapped; + // Invoke a custom `isEqual` method if one is provided. + if (a.isEqual) return a.isEqual(b); + if (b.isEqual) return b.isEqual(a); + // Compare array lengths to determine if a deep comparison is necessary. + if (a.length && (a.length !== b.length)) return false; + // Assume equality for cyclic structures. + var length = stack.length; + while (length--) { + if (stack[length] == a) return true; + } + // Add the object to the stack of traversed objects. + stack.push(a); + var result = true; + // Deep comparse the contents. + var aKeys = _.keys(a), bKeys = _.keys(b); + // Ensure that both objects contain the same number of properties. + if (result = aKeys.length == bKeys.length) { + // Recursively compare properties. + for (var key in a) { + if (!(result = key in b && eq(a[key], b[key], stack))) break; + } + } + // Remove the object from the stack of traversed objects. + stack.pop(); + return result; + } + // Expose the recursive `isEqual` method. + return function(a, b) { + return eq(a, b, []); + }; + })(); // Is a given array or object empty? _.isEmpty = function(obj) {