How to customize object equality for JavaScript Set

后端 未结 9 1272
臣服心动
臣服心动 2020-11-22 16:48

New ES 6 (Harmony) introduces new Set object. Identity algorithm used by Set is similar to === operator and so not much suitable for comparing objects:

相关标签:
9条回答
  • 2020-11-22 16:51

    Comparing them directly seems not possible, but JSON.stringify works if the keys just were sorted. As I pointed out in a comment

    JSON.stringify({a:1, b:2}) !== JSON.stringify({b:2, a:1});

    But we can work around that with a custom stringify method. First we write the method

    Custom Stringify

    Object.prototype.stringifySorted = function(){
        let oldObj = this;
        let obj = (oldObj.length || oldObj.length === 0) ? [] : {};
        for (let key of Object.keys(this).sort((a, b) => a.localeCompare(b))) {
            let type = typeof (oldObj[key])
            if (type === 'object') {
                obj[key] = oldObj[key].stringifySorted();
            } else {
                obj[key] = oldObj[key];
            }
        }
        return JSON.stringify(obj);
    }
    

    The Set

    Now we use a Set. But we use a Set of Strings instead of objects

    let set = new Set()
    set.add({a:1, b:2}.stringifySorted());
    
    set.has({b:2, a:1}.stringifySorted());
    // returns true
    

    Get all the values

    After we created the set and added the values, we can get all values by

    let iterator = set.values();
    let done = false;
    while (!done) {
      let val = iterator.next();
    
      if (!done) {
        console.log(val.value);
      }
      done = val.done;
    }
    

    Here's a link with all in one file http://tpcg.io/FnJg2i

    0 讨论(0)
  • 2020-11-22 16:52

    As mentioned in jfriend00's answer customization of equality relation is probably not possible.

    Following code presents an outline of computationally efficient (but memory expensive) workaround:

    class GeneralSet {
    
        constructor() {
            this.map = new Map();
            this[Symbol.iterator] = this.values;
        }
    
        add(item) {
            this.map.set(item.toIdString(), item);
        }
    
        values() {
            return this.map.values();
        }
    
        delete(item) {
            return this.map.delete(item.toIdString());
        }
    
        // ...
    }
    

    Each inserted element has to implement toIdString() method that returns string. Two objects are considered equal if and only if their toIdString methods returns same value.

    0 讨论(0)
  • 2020-11-22 16:54

    The ES6 Set object does not have any compare methods or custom compare extensibility.

    The .has(), .add() and .delete() methods work only off it being the same actual object or same value for a primitive and don't have a means to plug into or replace just that logic.

    You could presumably derive your own object from a Set and replace .has(), .add() and .delete() methods with something that did a deep object comparison first to find if the item is already in the Set, but the performance would likely not be good since the underlying Set object would not be helping at all. You'd probably have to just do a brute force iteration through all existing objects to find a match using your own custom compare before calling the original .add().

    Here's some info from this article and discussion of ES6 features:

    5.2 Why can’t I configure how maps and sets compare keys and values?

    Question: It would be nice if there were a way to configure what map keys and what set elements are considered equal. Why isn’t there?

    Answer: That feature has been postponed, as it is difficult to implement properly and efficiently. One option is to hand callbacks to collections that specify equality.

    Another option, available in Java, is to specify equality via a method that object implement (equals() in Java). However, this approach is problematic for mutable objects: In general, if an object changes, its “location” inside a collection has to change, as well. But that’s not what happens in Java. JavaScript will probably go the safer route of only enabling comparison by value for special immutable objects (so-called value objects). Comparison by value means that two values are considered equal if their contents are equal. Primitive values are compared by value in JavaScript.

    0 讨论(0)
  • 2020-11-22 17:02

    Create a new set from the combination of both sets, then compare the length.

    let set1 = new Set([1, 2, 'a', 'b'])
    let set2 = new Set([1, 'a', 'a', 2, 'b'])
    let set4 = new Set([1, 2, 'a'])
    
    function areSetsEqual(set1, set2) {
      const set3 = new Set([...set1], [...set2])
      return set3.size === set1.size && set3.size === set2.size
    }
    
    console.log('set1 equals set2 =', areSetsEqual(set1, set2))
    console.log('set1 equals set4 =', areSetsEqual(set1, set4))
    

    set1 equals set2 = true

    set1 equals set4 = false

    0 讨论(0)
  • 2020-11-22 17:07

    To add to the answers here, I went ahead and implemented a Map wrapper that takes a custom hash function, a custom equality function, and stores distinct values that have equivalent (custom) hashes in buckets.

    Predictably, it turned out to be slower than czerny's string concatenation method.

    Full source here: https://github.com/makoConstruct/ValueMap

    0 讨论(0)
  • 2020-11-22 17:10

    To someone who found this question on Google (as me) wanting to get a value of a Map using an object as Key:

    Warning: this answer will not work with all objects

    var map = new Map<string,string>();
    
    map.set(JSON.stringify({"A":2} /*string of object as key*/), "Worked");
    
    console.log(map.get(JSON.stringify({"A":2}))||"Not worked");
    

    Output:

    Worked

    0 讨论(0)
提交回复
热议问题