Search code examples
arraysfunctional-programmingecmascript-6unique

Array of literal Objects without duplicates in ES6 using Set


The code to get an array without repeated items has become elegant since ES6:

[...new Set(array)];

That's it!

However, this is only removing duplicates if the array has elements with a primitive data type (string, boolean, number, ...).

What about a Set of object literals? How to make that work without getting duplicates, using a syntax close to the syntax used above?

var array=["aaa","bbb","aaa","cc","aaa","bbb"];
var out=[...new Set(array)];
console.log(out)

//----Literal Object 

array=[{n:"J",last:"B"},{n:"J",last:"B"}];
out=[...new Set(array)];
console.log(out)

The code above produces a set with 2 elements, yet I want it to only have one in this case.

I could use serialize/de-serialize methodology to achieve this:

[...new Set(array.map(
    //-- SERIALIZE:
    (e) => `${e.n}:${e.last}`
))].map(
    //-- DE-SERIALIZE:
    (e) => ({ n: `${e.split(':')[0]}`, last: `${e.split(':')[1]}` })
)

However, I am looking for an ES6 built-in.


Solution

  • In JavaScript two objects are different if they are not the same reference, even when they look the same:

    var a = {};
    var b = {};
    console.log(a === b); // false
    b = a;
    console.log(a === b); // true
    

    Sets work similarly: unless objects added to it are referring to the same thing, they will be distinct.

    A Custom Set

    One idea to make it work like you want, is to create your own flavour of Set, i.e. MySet, giving it all the methods and properties you need for it to work as a Set.

    Then in its implementation you would keep a Map in its internals, where you give a key to everything you store in it. You could then make sure that objects that you consider the same, get the same key in that map, and so are only stored once.

    A non-efficient, but straightforward way of doing that, is to use JSON.stringify(item) as key. This has its limitations (e.g. adding self-referencing objects will make JSON.stringify(item) give up), but for the rest it does the job.

    We could make MySet to accept an additional argument: the function to invoke to get an item's key value. Given the above idea, we would give it the default value JSON.stringify.

    Consider this implementation, with your testing code added to it:

    // Implementation of special Set:
    class MySet {
        constructor(values = [], keyFunc = JSON.stringify) {
            // Use a map to store the values 
            this._map = new Map();
            // Which function to use for generating an item's key
            this._keyFunc = keyFunc;
            // Add the initial values
            for (var value of [...values]) this.add(value);
        }
        get size() {
            return this._map.size;
        }
        add(item) {
            // Key items by the given function
            this._map.set(this._keyFunc(item), item);
        }
        has(item) {
            return this._map.has(this._keyFunc(item));
        }
        *[Symbol.iterator] () {
            for (var pair of this._map) {
                yield pair[1]; // return the item
            }
        }
        // etc...
    }
    
    // Test it:
    array = [{n:"J",last:"B"}, {n:"J",last:"B"}];
    out = [...new MySet(array)];
    console.log(out);

    As you can see, although two objects were added to the set, it has only stored one.