Object.isEqual

# tyler clark (7 years ago)

Hello All!

I am reaching out today to get some feedback on my proposal and hopefully find a champion that will represent me and my idea.

Multiple times throughout my javascript projects I have found it necessary to compare two objects. More recently, while working in React I ran into an issue where I needed to compare responses from an API and had to use LoDash's isEqual to do it.

As we all know Javascript takes into count memory reference when comparing objects, not the individual keys and values. I am proposing we create a method on the Object constructor that takes in objects and returns true or false if the provided arguments have the exact same keys and values.

For example, this returns false:

const one = {test: 'test'}

const two = {test: 'test'}

const one === const two

The new method would look like this and will return true:

Object.isEqual(one, two)

I have the rough draft code to propose here:

jsfiddle.net/pdoutbnm/2

Thank you,

Tylerc

# Oriol _ (7 years ago)

This is not easy to generalize. Comparing objects is one thing lots of people want, but not everybody needs the same kind of comparison. For example, you compare own property strings. But what about symbols? Somebody might consider two objects to be different if they have different symbol properties. Or the opposite, somebody may think that checking enumerable properties is enough, and non-enumerable ones can be skipped. Then some property values might be objects. Are they compared with === or recursively with this algorithm (be aware of cycles)? Similarly, for the [[Prototype]]. Do inherited properties matter? Should [[Prototype]]s be compared with === or recursively? There is also the problem of getters: each time you read a property, it might give a different value! You might want to get the property descriptor and compare the values or the getter functions. And then there are proxies. Taking them into account, I don't think there is any reasonable way to compare objects.

So I think it's better if each person writes the code that compares objects according to their needs.

# Alexander Jones (7 years ago)

I hear this argument a lot but it strikes me with cognitive dissonance! JSON defines a very intuitive notion of object value-semantics - whether the serialized JSON is an equivalent string. Granted that many value types are not supported by JSON, but it's a trivial generalisation.

Let's just give the above a name and get on with it. For 99% of use cases it would be ideal, no?

Thoughts?

# tyler clark (7 years ago)

I believe you are right Oriol, that is is not easy to generalize. However I think that there is a definite need for something like this and we can come to a general conclusion that will work for most instances. It is hard to get a lot of people to come to a conclusion on what should or should not be the right method. People can find reasons to argue one way or another. In the end, it should come down to what the majority of people feel is appropriate. An example of this would be the argument of which properties the for in loop should take into count. I could see people arguing for and against using inherited properties or not.

As Alexander mentions I can see the majority of use cases would be simple string keys and values. However, I believe we should make the function robust enough to handle nested data types. We could go the route of stringifing the objects and then comparing (which would be quicker) than a recursion function. However as mentioned, will cause problems with types.

Another option would be to add a parameter that determines whether inherited properties are taken into count or not. As far as getters, symbols, and proxies go, I think there can be viable work rounds for those cases. I understand that this might make things more complicated but just because something is complicated, it doesn't mean that we shouldn't create it.

# Steve Fink (7 years ago)

It would be nice to have something for this. Some remaining problems I see with using JSON serialization, let's call it JSON.isEqual:

  • JS has order, JSON does not
  • JSON lacks NaN, +/-Infinity (and JSON.stringify maps these to null, which means JSON.isEqual({x: 0/0}, {x: 1/0}))
  • cycles
  • ...and everything under your "trivial generalisation"

It still seems like it'd be unfortunate if !JSON.isEqual({foo: val1}, {foo: val2}) where val1 === val2 (because val1/2 is not serializable, eg it has a cycle).

Also, what is

 var x = 4;
 JSON.isEqual({get foo() { return x++; }}, {foo: 4})

? If you went purely by "whatever JSON.stringify would return", then this would be true once and false afterwards.

This may seem like nitpicking, but if you don't nail down the exact semantics, then engines will end up doing the JSON serialization and a string compare, which rather defeats the purpose. If you stick to something simple like comparing JSON.stringify output, then they will pretty much have to do this, since there are so many observable side effects like getter invocation and proxy traps. You could define semantics that cover a large percentage of the interesting cases, but JSON isn't going to be of much help.

And for the record, JSON does not have an intuitive semantics at all. It has intuitive semantics for a small subset of values, a subset that is rarely adhered to except near interchange points where JSON makes sense. (And even then, it's common to accidentally step outside of it, for example by having something overflow to Infinity or accidentally produce a NaN.)

# Alexander Jones (7 years ago)

No I was not proposing that it actually be stringified-comparison... or in any way be related to the JSON global.

  • Property order would not be important for equality. Precedent is set by common sense.
  • Reference semantics (i.e. visitation) would be exactly as you'd expect - look at Python's dict implementation for IMO the only obvious answer.
  • I acknowledged specifically that JSON does not support all types, just use SameValueZero for primitives and be done with it.
  • Cycles are a subset of all possible reference semantics.
  • Getters should be called. It already works for JSON and I don't hear anyone complaining about it. (Maybe I wasn't listening?) - same for prototype chain questions...

That said, most of these questions evaporate if people would just start using Map for things. AFAIAC the main reason people don't is due to the absence of a Map literal and JSON parsing tending to give you Object back instead of a more appropriate type without such weirdness as prototypes and property descriptors (yes, I went there!).

# doodad-js Admin (7 years ago)

Maybe operator overloading?

Symbol.IsEqual {

            return obj.a  === this.a;

}

Symbol.IsGreater {

            return obj.a  > this.a;

}

...

# Steve Fink (7 years ago)

On 05/01/2017 03:15 PM, Alexander Jones wrote:

No I was not proposing that it actually be stringified-comparison... or in any way be related to the JSON global.

  • Property order would not be important for equality. Precedent is set by common sense.
  • Reference semantics (i.e. visitation) would be exactly as you'd expect - look at Python's dict implementation for IMO the only obvious answer.
  • I acknowledged specifically that JSON does not support all types, just use SameValueZero for primitives and be done with it.
  • Cycles are a subset of all possible reference semantics.
  • Getters should be called. It already works for JSON and I don't hear anyone complaining about it. (Maybe I wasn't listening?) - same for prototype chain questions...

If there is any possibility that anything has a getter, anywhere in the tree you're comparing, then you will have to do a full tree traversal. Even if you're comparing an object to itself. Both

 isEqual({'foo': giantHugeObject}, {'foo': giantHugeObject});

and

 isEqual(giantHugeObject, giantHugeObject);

have to traverse through giantHugeObject in order to find any getters to invoke, which will be far slower than a simple JS function that short-circuits if things are ===. But then what's the point?

Arguably, an engine could know that neither of the objects has any nontrivially-comparable children, but that requires fancy type tracking (eg if someone does a setPrototypeOf, you'd better be able to find everything affected and invalidate that compares-fast bit.)

As you point out, Maps are simpler. Map keys can't have getters (though I guess a Proxy of a Map could?). Maybe there's more hope for a Map.isEqual?