Rewrite _.isEqual and add support for comparing cyclic structures.

This commit is contained in:
Kit Goncharov
2011-07-12 19:42:36 -06:00
parent 727db393d5
commit 5c2c3ce464
2 changed files with 97 additions and 40 deletions

View File

@@ -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}, {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'); 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'); 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() { test("objects: isEmpty", function() {

View File

@@ -391,7 +391,6 @@
return -1; return -1;
}; };
// Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
_.lastIndexOf = function(array, item) { _.lastIndexOf = function(array, item) {
if (array == null) return -1; if (array == null) return -1;
@@ -538,7 +537,6 @@
}; };
}; };
// Object Functions // Object Functions
// ---------------- // ----------------
@@ -596,44 +594,63 @@
}; };
// Perform a deep comparison to check if two objects are equal. // Perform a deep comparison to check if two objects are equal.
_.isEqual = function(a, b) { _.isEqual = (function() {
// Check object identity. function eq(a, b, stack) {
if (a === b) return true; // Identical objects are equal.
// Different types? if (a === b) return true;
var atype = typeof(a), btype = typeof(b); // A strict comparison is necessary because `null == undefined`.
if (atype != btype) return false; if (a == null) return a === b;
// Basic equality test (watch out for coercions). // Compare `[[Class]]` names.
if (a == b) return true; var className = toString.call(a);
// One is falsy and the other truthy. if (className != toString.call(b)) return false;
if ((!a && b) || (a && !b)) return false; // Compare functions by reference.
// Unwrap any wrapped objects. if (_.isFunction(a)) return _.isFunction(b) && a == b;
if (a._chain) a = a._wrapped; // Compare strings, numbers, dates, and booleans by value.
if (b._chain) b = b._wrapped; if (_.isString(a)) return _.isString(b) && String(a) == String(b);
// One of them implements an isEqual()? if (_.isNumber(a)) return _.isNumber(b) && +a == +b;
if (a.isEqual) return a.isEqual(b); if (_.isDate(a)) return _.isDate(b) && a.getTime() == b.getTime();
if (b.isEqual) return b.isEqual(a); if (_.isBoolean(a)) return _.isBoolean(b) && +a == +b;
// Check dates' integer values. // Compare RegExps by their source patterns and flags.
if (_.isDate(a) && _.isDate(b)) return a.getTime() === b.getTime(); if (_.isRegExp(a)) return _.isRegExp(b) && a.source == b.source &&
// Both are NaN? a.global == b.global &&
if (_.isNaN(a) && _.isNaN(b)) return false; a.multiline == b.multiline &&
// Compare regular expressions. a.ignoreCase == b.ignoreCase;
if (_.isRegExp(a) && _.isRegExp(b)) // Recursively compare objects and arrays.
return a.source === b.source && if (typeof a != 'object') return false;
a.global === b.global && // Unwrap any wrapped objects.
a.ignoreCase === b.ignoreCase && if (a._chain) a = a._wrapped;
a.multiline === b.multiline; if (b._chain) b = b._wrapped;
// If a is not an object by this point, we can't handle it. // Invoke a custom `isEqual` method if one is provided.
if (atype !== 'object') return false; if (a.isEqual) return a.isEqual(b);
// Check for different array lengths before comparing contents. if (b.isEqual) return b.isEqual(a);
if (a.length && (a.length !== b.length)) return false; // Compare array lengths to determine if a deep comparison is necessary.
// Nothing else worked, deep compare the contents. if (a.length && (a.length !== b.length)) return false;
var aKeys = _.keys(a), bKeys = _.keys(b); // Assume equality for cyclic structures.
// Different object sizes? var length = stack.length;
if (aKeys.length != bKeys.length) return false; while (length--) {
// Recursive comparison of contents. if (stack[length] == a) return true;
for (var key in a) if (!(key in b) || !_.isEqual(a[key], b[key])) return false; }
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? // Is a given array or object empty?
_.isEmpty = function(obj) { _.isEmpty = function(obj) {