-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for comparing cyclic structures #244
Conversation
missing but Array.prototype.forEach exists.
Fixed any/some formatting to be consistent with the rest of underscore.js
if ((!a && b) || (a && !b)) return false; | ||
// `NaN` values are toxic. | ||
if (_.isNaN(a) || _.isNaN(b)) return false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be an egal comparison:
if (_.isNaN(a)) return _.isNaN(b);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't backward-compatible, but I agree nonetheless.
LGTM other than my comments. +1 from me. |
// Internal recursive comparison function. | ||
function eq(a, b, stack) { | ||
// Identical objects are equal. | ||
if (a === b) return a != 0 || 1 / a == 1 / b; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now it requires two divisions for any strictly equal falsey values a
and b
instead of just 0
or -0
as my suggestion would have done. Example: a = b = false
. Use !==
.
Edit: looks like @kflorence beat me to it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, the division would only be executed for the value false
...null !=0
and void 0 != 0
. I agree, though; it's unnecessary. The latest commit should fix this. Thank you for your other suggestions as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Empty string: '' == 0
. But you're right that not all falsey values would execute the division operations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, yes. I'd forgotten about the empty string. Thanks!
…d in the prototype chain.
for (var key in a) if (!(key in b) || !_.isEqual(a[key], b[key])) return false; | ||
return true; | ||
// Ensure that both objects contain the same number of properties. | ||
var result = aKeys.length == bKeys.length; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the point of comparing the relative lengths of just the own
keys? All keys, including prototypal ones, are going to be compared in the next step.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was in the original _.isEqual
function, so I assumed that it was intentional. If it wasn't, then we can have the function compare either own properties or all properties. The deepEqual
method in the CommonJS unit testing specification adopts the former approach, but this is clearly problematic for objects that have no own properties and different prototype chains. I use the latter approach in Spec and Maddy, since objects with different inherited properties are clearly not equivalent. This will be more performant as well, since it won't require a dependency on _.keys
, and won't loop twice for objects with different sizes. Want me to patch it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes ... but these sort of changes / tweaks are why I'm still uneasy with merging in a homebrew deep-equal comparison function. It would be awfully nice to have something like this tested in the wild for a while before merging. But so it goes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. I can see why you're uneasy, but I also think it'd be useful for other developers to have a deep comparison function to use, rather than writing their own.
// Internal recursive comparison function. | ||
function eq(a, b, stack) { | ||
// Identical objects are equal. | ||
if (a === b) return a !== 0 || 1 / a == 1 / b; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
0===0, so I don't quite understand the logic behind the rest of this line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
0 === -0
, but the two aren't identical. 1 / 0 == Infinity
; 1 / -0 == -Infinity
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting. Hadn't considered -0. I think it's confusing enough to warrant
a comment though.
On Wed, Jul 13, 2011 at 12:35 PM, kitgoncharov <
reply@reply.github.com>wrote:
@@ -595,44 +593,67 @@
return obj;
};
- // 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).
- // Internal recursive comparison function.
- function eq(a, b, stack) {
- // Identical objects are equal.
- if (a === b) return a !== 0 || 1 / a == 1 / b;
0 === -0
, but the two aren't identical.1 / 0 == Infinity
;1 / -0 == -Infinity
.Reply to this email directly or view it on GitHub:
https://github.com/documentcloud/underscore/pull/244/files#r64357
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. I'll add it in the next commit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should probably include a link to harmony:egal in the comment
This reverts commit 47fb3d7.
// Unwrap any wrapped objects. | ||
if (a._chain) a = a._wrapped; | ||
if (b._chain) b = b._wrapped; | ||
// One of them implements an isEqual()? | ||
// Invoke a custom `isEqual` method if one is provided. | ||
if (a.isEqual) return a.isEqual(b); | ||
if (b.isEqual) return b.isEqual(a); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a
does not have an isEqual
method (or it returns false
), then we should just return false
upon seeing this condition, right? Unless we foresee someone wanting non-commutative equality. But that's almost as crazy as irreflexive equality, and nobody would be crazy enough to do that...
edit: added links for @jdalton
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@michaelficarra: Like this? if (a.isEqual && !a.isEqual(b)) return false;
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kitgoncharov: like this:
if(typeof a.isEqual == 'function') return a.isEqual(b);
if(typeof b.isEqual == 'function') return false;
PS: I always make sure [[Call]] is implemented before attempting to call something that may not be a function. And any object with [[Call]] will respond to typeof
with 'function'
.
if (a == b) return true; | ||
// One is falsy and the other truthy. | ||
// Optimization; ensure that both values are truthy or falsy. | ||
if ((!a && b) || (a && !b)) return false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternative solution:
if (!a != !b) return false;
For more clarity, this also works:
if (!a == !!b) return false;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 first alternative solution
It would be worth making sure that whatever algorithm we want to adopt here also passes the QUnit suite of deep equality tests: https://github.com/jquery/qunit/blob/master/test/same.js ... and we should take a look at the implementation of |
// 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't a == b
guaranteed to be false at this point?
We already know that a !== b && typeof a != typeof b
. Doesn't that imply a != b
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bman654: no:
$ node > a = 0 0 > b = '' '' > a !== b true > typeof a != typeof b true > a == b true >
edit: also, sorry for taking so long to respond. I literally had this tab open since you asked that question and didn't have a chance to get back to you.
edit again: it looks like your "what we know" was mistyped and I found a counterexample to that instead of the real source. You meant "we already know that a !== b && typeof a == typeof b
. Hmm, let me think about this one. It looks like you're right, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@michaelficarra The only case I can think of where that may present a problem is an object with a custom valueOf
method, such as {valueOf: function () { return 1; }}
. Since the ==
algorithm uses an identity comparison if both operands are objects, however, a == b
is never true.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kitcambridge: I'm pretty confident that @bman654 was right. I'd make the change.
Merging @michaelficarra's additions...
@jashkenas (and other participants, collaborators, and maintainers): I think this pull is ready to be reviewed now. |
@michaelficarra Not quite; I need to fix some last-minute bugs and add more unit tests. Temporarily closing the pull request... |
@kitcambridge: whoops, sorry for jumping the gun. I thought it was ready. |
…. Ignore inherited properties when deep comparing objects. Use a more efficient `while` loop for comparing arrays and array-like objects.
var isNumberA = _.isNumber(a), isNumberB = _.isNumber(b); | ||
if (isNumberA || isNumberB) return isNumberA && isNumberB && +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]'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that this [[Class]]
check is used to compare boolean object wrappers, not primitives..._.isBoolean
returns false
for boolean objects. I'd suggest adding this check to _.isBoolean
instead:
_.isBoolean = function(obj) {
return obj === true || obj === false || typeof obj == 'object' && toString.call(obj) == '[object Boolean]';
};
Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed.
Okay, I think this is now ready for review. Thanks for your help, everyone. |
// Optimization; ensure that both values are truthy or falsy. | ||
if (!a != !b) return false; | ||
// `NaN` values are equal. | ||
if (_.isNaN(a)) return _.isNaN(b); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My previous comment proposed adding the expression if (a != b) return false;
below this line. This would have worked in one of my local branches, where I attempted to consolidate this series of checks into a single do...while
loop, but would have clearly failed in this context. My apologies for any confusion; I'm not sure why I even posted that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I was confused so I decided to temporarily ignore it. Good thing, I guess.
Two further requests on this ... Can we remove the change to |
Nevermind ... the change to the semantics is fine -- I'll tweak the boolean. |
@jashkenas Thanks. I'll be off to class shortly, so I'm afraid I won't be able to make any changes for the next hour and a half. When I return, though, would you like for me to update the documentation as well? |
No worries -- I'm working through it all now. |
@kitcambridge and @michaelficarra -- thanks for all the great work on this. Landed on master. |
Fantastic. Thank you! |
Awesome. I loved this pull. Many thanks to @kitcambridge, this wouldn't have been started without you. Underscore's |
As per the discussion in issue #240, I've added support for comparing cyclic structures to
_.isEqual
. All the unit tests, including my nine additions, pass.