web-dev-qa-db-fra.com

Comment personnaliser l'égalité des objets pour JavaScript Set

La nouvelle ES 6 (Harmony) introduit un nouvel objet Set . L'algorithme d'identité utilisé par Set est similaire à l'opérateur === Et n'est donc pas très approprié pour comparer des objets:

var set = new Set();
set.add({a:1});
set.add({a:1});
console.log([...set.values()]); // Array [ Object, Object ]

Comment personnaliser l'égalité pour les objets Set afin d'effectuer une comparaison d'objet approfondie? Existe-t-il quelque chose comme Java equals(Object)]?

127
czerny

L'objet ES6 Set ne dispose d'aucune méthode de comparaison ni d'extensibilité de comparaison personnalisée.

Les méthodes .has(), .add() et .delete() fonctionnent uniquement si elles sont identiques au même objet ou à la même valeur pour une primitive et n'ont pas le moyen de s'y connecter ou remplacer simplement cette logique.

Vous pourriez probablement dériver votre propre objet d'un Set et remplacer les méthodes .has(), .add() et .delete() par quelque chose qui a d'abord comparé les objets en profondeur pour rechercher si l'élément est déjà dans l'ensemble, mais les performances ne seraient probablement pas bonnes, car l'objet Set sous-jacent ne serait d'aucune utilité. Vous devrez probablement effectuer une itération forcée sur tous les objets existants pour trouver une correspondance à l'aide de votre propre comparaison personnalisée avant d'appeler l'original .add().

Voici quelques informations de cet article et cette discussion sur les fonctionnalités de l'ES6:

5.2 Pourquoi ne puis-je pas configurer comment les cartes et les ensembles comparent les clés et les valeurs?

Question: Ce serait bien s’il y avait un moyen de configurer quelles clés de la carte et quels éléments de l’ensemble sont considérés égaux. Pourquoi n’est-ce pas?

Réponse: Cette fonctionnalité a été reportée car il est difficile à mettre en œuvre correctement et efficacement. Une option consiste à remettre des rappels aux collections qui spécifient l'égalité.

Une autre option, disponible en Java, consiste à spécifier l'égalité via une méthode que l'objet implémente (equals () en Java). Cependant, cette approche est problématique pour les objets mutables: en général, si un objet change, son "emplacement" dans une collection doit également changer. Mais ce n’est pas ce qui se passe en Java. JavaScript sera probablement le moyen le plus sûr de ne permettre que la comparaison par valeur pour des objets immuables spéciaux (appelés objets de valeur). La comparaison par valeur signifie que deux valeurs sont considérées égales si leur contenu est égal. Les valeurs primitives sont comparées par valeur en JavaScript.

86
jfriend00

Comme mentionné dans réponse de jfriend , la personnalisation de la relation d'égalité est probablement impossible.

Le code suivant présente un aperçu de l'efficacité des calculs (mais de la mémoire est coûteuse) solution de contournement:

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());
    }

    // ...
}

Chaque élément inséré doit implémenter la méthode toIdString() qui retourne une chaîne. Deux objets sont considérés égaux si et seulement si leurs méthodes toIdString renvoient la même valeur.

27
czerny

Pour ajouter aux réponses ici, je suis allé de l'avant et j'ai implémenté un wrapper de carte qui prend une fonction de hachage personnalisée, une fonction d'égalité personnalisée et stocke des valeurs distinctes qui ont des hachages équivalents (personnalisés) dans des compartiments.

Comme prévu, il s'est avéré être plus lent que méthode de concaténation de chaînes de czerny .

Source complète ici: https://github.com/makoConstruct/ValueMap

4
mako

Les comparer directement ne semble pas possible, mais JSON.stringify fonctionne si les clés ont été triées. Comme je l'ai souligné dans un commentaire

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

Mais nous pouvons contourner ce problème avec une méthode stringify personnalisée. D'abord nous écrivons la méthode

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);
}

L'ensemble

Maintenant, nous utilisons un ensemble. Mais nous utilisons un ensemble de chaînes au lieu d'objets

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

set.has({b:2, a:1}.stringifySorted());
// returns true

Obtenir toutes les valeurs

Après avoir créé le jeu et ajouté les valeurs, nous pouvons obtenir toutes les valeurs en

let iterator = set.values();
let done = false;
while (!done) {
  let val = iterator.next();

  if (!done) {
    console.log(val.value);
  }
  done = val.done;
}

Voici un lien avec tout dans un fichier http://tpcg.io/FnJg2i

3
relief.melone

Peut-être pouvez-vous essayer d'utiliser JSON.stringify() pour comparer les objets en profondeur.

par exemple :

const arr = [
  {name:'a', value:10},
  {name:'a', value:20},
  {name:'a', value:20},
  {name:'b', value:30},
  {name:'b', value:40},
  {name:'b', value:40}
];

const names = new Set();
const result = arr.filter(item => !names.has(JSON.stringify(item)) ? names.add(JSON.stringify(item)) : false);

console.log(result);
2
GuaHsu

Comme le réponse du haut le mentionne, la personnalisation de l'égalité est problématique pour les objets mutables. La bonne nouvelle est (et je suis étonné que personne ne l’ait encore mentionné). Il existe une bibliothèque très populaire appelée immutable-js , qui fournit un riche ensemble de types immuables qui fournissent la valeur deep value sémantique d’égalité vous cherchez.

Voici votre exemple utilisant immutable-js :

const { Map, Set } = require('immutable');
var set = new Set();
set = set.add(Map({a:1}));
set = set.add(Map({a:1}));
console.log([...set.values()]); // [Map {"a" => 1}]
1
Russell Davis

Créez un nouvel ensemble à partir de la combinaison des deux ensembles, puis comparez la longueur.

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 est égal à set2 = true

set1 est égal à set4 = false

0
Stefan Musarra

Pour les utilisateurs de TypeScript, les réponses des autres (en particulier czerny ) peuvent être généralisées à une classe de base de type Nice et réutilisable:

/**
 * Map that stringifies the key objects in order to leverage
 * the javascript native Map and preserve key uniqueness.
 */
abstract class StringifyingMap<K, V> {
    private map = new Map<string, V>();
    private keyMap = new Map<string, K>();

    has(key: K): boolean {
        let keyString = this.stringifyKey(key);
        return this.map.has(keyString);
    }
    get(key: K): V {
        let keyString = this.stringifyKey(key);
        return this.map.get(keyString);
    }
    set(key: K, value: V): StringifyingMap<K, V> {
        let keyString = this.stringifyKey(key);
        this.map.set(keyString, value);
        this.keyMap.set(keyString, key);
        return this;
    }

    /**
     * Puts new key/value if key is absent.
     * @param key key
     * @param defaultValue default value factory
     */
    putIfAbsent(key: K, defaultValue: () => V): boolean {
        if (!this.has(key)) {
            let value = defaultValue();
            this.set(key, value);
            return true;
        }
        return false;
    }

    keys(): IterableIterator<K> {
        return this.keyMap.values();
    }

    keyList(): K[] {
        return [...this.keys()];
    }

    delete(key: K): boolean {
        let keyString = this.stringifyKey(key);
        let flag = this.map.delete(keyString);
        this.keyMap.delete(keyString);
        return flag;
    }

    clear(): void {
        this.map.clear();
        this.keyMap.clear();
    }

    size(): number {
        return this.map.size;
    }

    /**
     * Turns the `key` object to a primitive `string` for the underlying `Map`
     * @param key key to be stringified
     */
    protected abstract stringifyKey(key: K): string;
}

Voici un exemple d'implémentation simple: écrasez simplement la méthode stringifyKey. Dans mon cas, je chaîne quelques propriétés uri.

class MyMap extends StringifyingMap<MyKey, MyValue> {
    protected stringifyKey(key: MyKey): string {
        return key.uri.toString();
    }
}

Exemple d'utilisation est alors comme s'il s'agissait d'un Map<K, V>.

const key1 = new MyKey(1);
const value1 = new MyValue(1);
const value2 = new MyValue(2);

const myMap = new MyMap();
myMap.set(key1, value1);
myMap.set(key1, value2); // native Map would put another key/value pair

myMap.size(); // returns 1, not 2
0
Jan Dolejsi