Vous imitez des ensembles en JavaScript?


220

Je travaille en JavaScript. Je voudrais stocker une liste de valeurs de chaîne uniques et non ordonnées, avec les propriétés suivantes:

  1. un moyen rapide de demander «est A dans la liste»?
  2. un moyen rapide de «supprimer A de la liste s'il existe dans la liste»
  3. un moyen rapide de faire «ajouter A à la liste s'il n'est pas déjà présent».

Ce que je veux vraiment, c'est un ensemble. Des suggestions sur la meilleure façon d'imiter un ensemble en JavaScript?

Cette question recommande d'utiliser un objet , avec les clés stockant les propriétés et les valeurs toutes définies sur true: est-ce une manière raisonnable?



Réponses:


262

Si vous programmez dans un environnement compatible ES6 (tel que node.js, un navigateur spécifique avec les capacités ES6 dont vous avez besoin ou transpilant du code ES6 pour votre environnement), vous pouvez utiliser l' Setobjet intégré à ES6 . Il a de très belles capacités et peut être utilisé tel quel dans votre environnement.


Pour de nombreuses choses simples dans un environnement ES5, l'utilisation d'un objet fonctionne très bien. Si objest votre objet et Aest une variable qui a la valeur sur laquelle vous souhaitez opérer dans l'ensemble, vous pouvez le faire:

Code d'initialisation:

// create empty object
var obj = {};

// or create an object with some items already in it
var obj = {"1":true, "2":true, "3":true, "9":true};

Question 1: Est Adans la liste:

if (A in obj) {
    // put code here
}

Question 2: supprimez «A» de la liste s'il y en a un:

delete obj[A];

Question 3: ajoutez 'A' à la liste si ce n'est déjà fait

obj[A] = true;

Pour être complet, le test de A est dans la liste est un peu plus sûr avec ceci:

if (Object.prototype.hasOwnProperty.call(obj, A))
    // put code here
}

en raison d'un conflit potentiel entre les méthodes et / ou les propriétés intégrées sur l'objet de base comme le constructor propriété.


Barre latérale sur ES6: La version de travail actuelle d' ECMAScript 6 ou quelque chose appelé ES 2015 a un objet Set intégré . Il est désormais implémenté dans certains navigateurs. Étant donné que la disponibilité du navigateur change au fil du temps, vous pouvez consulter la ligneSet dans ce tableau de compatibilité ES6 pour voir l'état actuel de la disponibilité du navigateur.

L'un des avantages de l'objet Set intégré est qu'il ne contraint pas toutes les clés à une chaîne comme le fait l'objet, vous pouvez donc avoir à la fois 5 et "5" comme clés distinctes. Et, vous pouvez même utiliser des objets directement dans l'ensemble sans conversion de chaîne. Voici un article qui décrit certaines des capacités et la documentation de MDN sur l'objet Set.

J'ai maintenant écrit un polyfill pour l'objet set ES6 afin que vous puissiez commencer à l'utiliser maintenant et il se reportera automatiquement à l'objet set intégré si le navigateur le prend en charge. Cela a l'avantage d'écrire du code compatible ES6 qui fonctionnera jusqu'à IE7. Mais, il y a quelques inconvénients. L'interface de l'ensemble ES6 profite des itérateurs ES6 pour que vous puissiez faire des choses comme for (item of mySet)et il itérera automatiquement l'ensemble pour vous. Mais, ce type de fonctionnalité de langage ne peut pas être implémenté via polyfill. Vous pouvez toujours itérer un ensemble ES6 sans utiliser les nouvelles fonctionnalités linguistiques ES6, mais franchement sans les nouvelles fonctionnalités linguistiques, ce n'est pas aussi pratique que l'autre interface d'ensemble que j'inclus ci-dessous.

Vous pouvez décider lequel vous convient le mieux après avoir examiné les deux. Le polyfill ES6 set est ici: https://github.com/jfriend00/ES6-Set .

Pour info, lors de mes propres tests, j'ai remarqué que l'implémentation de Firefox v29 Set n'est pas entièrement à jour par rapport à la version actuelle de la spécification. Par exemple, vous ne pouvez pas enchaîner les .add()appels de méthode comme le décrit la spécification et mon support polyfill. Il s'agit probablement d'une spécification en mouvement car elle n'est pas encore finalisée.


Objets Set prédéfinis: Si vous voulez un objet déjà construit qui possède des méthodes pour fonctionner sur un ensemble que vous pouvez utiliser dans n'importe quel navigateur, vous pouvez utiliser une série d'objets différents prédéfinis qui implémentent différents types d'ensembles. Il existe un miniSet qui est un petit code qui implémente les bases d'un objet set. Il a également un objet d'ensemble plus riche en fonctionnalités et plusieurs dérivations, y compris un dictionnaire (vous permet de stocker / récupérer une valeur pour chaque clé) et un ObjectSet (vous permet de conserver un ensemble d'objets - soit des objets JS ou des objets DOM où vous fournissez le fonction qui génère une clé unique pour chacun ou l'ObjectSet générera la clé pour vous).

Voici une copie du code du miniSet (le code le plus récent est ici sur github ).

"use strict";
//-------------------------------------------
// Simple implementation of a Set in javascript
//
// Supports any element type that can uniquely be identified
//    with its string conversion (e.g. toString() operator).
// This includes strings, numbers, dates, etc...
// It does not include objects or arrays though
//    one could implement a toString() operator
//    on an object that would uniquely identify
//    the object.
// 
// Uses a javascript object to hold the Set
//
// This is a subset of the Set object designed to be smaller and faster, but
// not as extensible.  This implementation should not be mixed with the Set object
// as in don't pass a miniSet to a Set constructor or vice versa.  Both can exist and be
// used separately in the same project, though if you want the features of the other
// sets, then you should probably just include them and not include miniSet as it's
// really designed for someone who just wants the smallest amount of code to get
// a Set interface.
//
// s.add(key)                      // adds a key to the Set (if it doesn't already exist)
// s.add(key1, key2, key3)         // adds multiple keys
// s.add([key1, key2, key3])       // adds multiple keys
// s.add(otherSet)                 // adds another Set to this Set
// s.add(arrayLikeObject)          // adds anything that a subclass returns true on _isPseudoArray()
// s.remove(key)                   // removes a key from the Set
// s.remove(["a", "b"]);           // removes all keys in the passed in array
// s.remove("a", "b", ["first", "second"]);   // removes all keys specified
// s.has(key)                      // returns true/false if key exists in the Set
// s.isEmpty()                     // returns true/false for whether Set is empty
// s.keys()                        // returns an array of keys in the Set
// s.clear()                       // clears all data from the Set
// s.each(fn)                      // iterate over all items in the Set (return this for method chaining)
//
// All methods return the object for use in chaining except when the point
// of the method is to return a specific value (such as .keys() or .isEmpty())
//-------------------------------------------


// polyfill for Array.isArray
if(!Array.isArray) {
    Array.isArray = function (vArg) {
        return Object.prototype.toString.call(vArg) === "[object Array]";
    };
}

function MiniSet(initialData) {
    // Usage:
    // new MiniSet()
    // new MiniSet(1,2,3,4,5)
    // new MiniSet(["1", "2", "3", "4", "5"])
    // new MiniSet(otherSet)
    // new MiniSet(otherSet1, otherSet2, ...)
    this.data = {};
    this.add.apply(this, arguments);
}

MiniSet.prototype = {
    // usage:
    // add(key)
    // add([key1, key2, key3])
    // add(otherSet)
    // add(key1, [key2, key3, key4], otherSet)
    // add supports the EXACT same arguments as the constructor
    add: function() {
        var key;
        for (var i = 0; i < arguments.length; i++) {
            key = arguments[i];
            if (Array.isArray(key)) {
                for (var j = 0; j < key.length; j++) {
                    this.data[key[j]] = key[j];
                }
            } else if (key instanceof MiniSet) {
                var self = this;
                key.each(function(val, key) {
                    self.data[key] = val;
                });
            } else {
                // just a key, so add it
                this.data[key] = key;
            }
        }
        return this;
    },
    // private: to remove a single item
    // does not have all the argument flexibility that remove does
    _removeItem: function(key) {
        delete this.data[key];
    },
    // usage:
    // remove(key)
    // remove(key1, key2, key3)
    // remove([key1, key2, key3])
    remove: function(key) {
        // can be one or more args
        // each arg can be a string key or an array of string keys
        var item;
        for (var j = 0; j < arguments.length; j++) {
            item = arguments[j];
            if (Array.isArray(item)) {
                // must be an array of keys
                for (var i = 0; i < item.length; i++) {
                    this._removeItem(item[i]);
                }
            } else {
                this._removeItem(item);
            }
        }
        return this;
    },
    // returns true/false on whether the key exists
    has: function(key) {
        return Object.prototype.hasOwnProperty.call(this.data, key);
    },
    // tells you if the Set is empty or not
    isEmpty: function() {
        for (var key in this.data) {
            if (this.has(key)) {
                return false;
            }
        }
        return true;
    },
    // returns an array of all keys in the Set
    // returns the original key (not the string converted form)
    keys: function() {
        var results = [];
        this.each(function(data) {
            results.push(data);
        });
        return results;
    },
    // clears the Set
    clear: function() {
        this.data = {}; 
        return this;
    },
    // iterate over all elements in the Set until callback returns false
    // myCallback(key) is the callback form
    // If the callback returns false, then the iteration is stopped
    // returns the Set to allow method chaining
    each: function(fn) {
        this.eachReturn(fn);
        return this;
    },
    // iterate all elements until callback returns false
    // myCallback(key) is the callback form
    // returns false if iteration was stopped
    // returns true if iteration completed
    eachReturn: function(fn) {
        for (var key in this.data) {
            if (this.has(key)) {
                if (fn.call(this, this.data[key], key) === false) {
                    return false;
                }
            }
        }
        return true;
    }
};

MiniSet.prototype.constructor = MiniSet;

16
Cela résout la question, mais pour être clair, cette implémentation ne fonctionnera pas pour des ensembles de choses en plus d'entiers ou de chaînes.
mkirk

3
@mkirk - oui, l'élément que vous indexez dans l'ensemble doit avoir une représentation sous forme de chaîne qui peut être la clé d'index (par exemple, c'est une chaîne ou une méthode toString () qui décrit l'élément de manière unique).
jfriend00

4
Pour obtenir les éléments de la liste, vous pouvez utiliser Object.keys(obj).
Blixt

3
@Blixt - Object.keys()nécessite IE9, FF4, Safari 5, Opera 12 ou supérieur. Il existe un polyfill pour les anciens navigateurs ici .
jfriend00

1
Ne pas utiliser obj.hasOwnProperty(prop)pour les chèques d'adhésion. Utilisez à la Object.prototype.hasOwnProperty.call(obj, prop)place, qui fonctionne même si le "set" contient la valeur "hasOwnProperty".
davidchambers

72

Vous pouvez créer un objet sans propriétés comme

var set = Object.create(null)

qui peut agir comme un ensemble et élimine la nécessité d'utiliser hasOwnProperty.


var set = Object.create(null); // create an object with no properties

if (A in set) { // 1. is A in the list
  // some code
}
delete set[a]; // 2. delete A from the list if it exists in the list 
set[A] = true; // 3. add A to the list if it is not already present

Sympa, mais je ne sais pas pourquoi vous dites que "élimine le besoin d'utiliser hasOwnProperty"
blueFast

13
Si vous l'utilisez, set = {}il héritera de toutes les propriétés de Object (par exemple toString), vous devrez donc vérifier la charge utile de l'ensemble (propriétés que vous avez ajoutées) avec hasOwnPropertyinif (A in set)
Thorben Croisé

6
Je ne savais pas qu'il était possible de créer un objet complètement vide. Merci, votre solution est très élégante.
blueFast

1
Intéressant, mais l'inconvénient est que vous devez avoir des set[A]=trueinstructions pour chaque élément que vous souhaitez ajouter au lieu d'un seul initialiseur?
vogomatix

1
Je ne sais pas ce que vous voulez dire, mais si vous faites référence à l'initialisation d'un set par un set déjà présent, vous pouvez faire quelque chose commes = Object.create(null);s["thorben"] = true;ss = Object.create(s)
Thorben Croisé

23

Depuis ECMAScript 6, la structure de données Set est une fonction intégrée . La compatibilité avec les versions de node.js peut être trouvée ici .


4
Bonjour, pour plus de clarté - nous sommes en 2014, est-ce encore expérimental dans Chrome? Si ce n'est pas le cas, pourriez-vous modifier votre réponse? Merci
Karel Bílek

1
Oui, c'est encore expérimental pour Chrome. Je pense que d'ici la fin de 2014, lorsque ECMAScript devrait être «officiellement» publié, il sera soutenu. Je mettrai ensuite ma réponse à jour en conséquence.
hymloth

OK, merci d'avoir répondu! (Les réponses JavaScript deviennent obsolètes assez rapidement.)
Karel Bílek

1
@Val inne fonctionne pas car les Setobjets n'ont pas leurs éléments en tant que propriétés, ce qui serait mauvais car les ensembles peuvent avoir des éléments de tout type, mais les propriétés sont des chaînes. Vous pouvez utiliser has:Set([1,2]).has(1)
Oriol

1
La réponse de Salvador Dali est plus complète et à jour.
Dan Dascalescu

14

Dans la version ES6 de Javascript, vous avez intégré un type pour set ( vérifiez la compatibilité avec votre navigateur ).

var numbers = new Set([1, 2, 4]); // Set {1, 2, 4}

Pour ajouter un élément à l'ensemble que vous utilisez .add(), il s'exécute O(1)et ajoute l'élément à définir (s'il n'existe pas) ou ne fait rien s'il est déjà là. Vous pouvez y ajouter des éléments de tout type (tableaux, chaînes, nombres)

numbers.add(4); // Set {1, 2, 4}
numbers.add(6); // Set {1, 2, 4, 6}

Pour vérifier le nombre d'éléments dans l'ensemble, vous pouvez simplement utiliser .size. Fonctionne également dansO(1)

numbers.size; // 4

Pour supprimer l'élément de l'ensemble, utilisez .delete(). Il renvoie vrai si la valeur était là (et a été supprimée) et faux si la valeur n'existait pas. Exécute également O(1).

numbers.delete(2); // true
numbers.delete(2); // false

Pour vérifier si l'élément existe dans un ensemble, utilisez .has()ce qui retourne vrai si l'élément est dans l'ensemble et faux sinon. Exécute également O(1).

numbers.has(3); // false
numbers.has(1); // true

En plus des méthodes que vous vouliez, il y en a quelques autres:

  • numbers.clear(); supprimerait simplement tous les éléments de l'ensemble
  • numbers.forEach(callback); itération à travers les valeurs de l'ensemble dans l'ordre d'insertion
  • numbers.entries(); créer un itérateur de toutes les valeurs
  • numbers.keys(); renvoie les clés de l'ensemble qui est le même que numbers.values()

Il existe également un Weakset qui permet d'ajouter uniquement des valeurs de type objet.


pourriez-vous indiquer une référence aux .add()exécutions dans O (1)? Je suis intrigué par cela,
Vert

10

J'ai commencé une implémentation de Sets qui fonctionne actuellement assez bien avec les nombres et les chaînes. Mon objectif principal était l'opération de différence, j'ai donc essayé de la rendre aussi efficace que possible. Les tests de Forks et de code sont les bienvenus!

https://github.com/mcrisc/SetJS


wow cette classe est folle! J'utiliserais totalement cela si je n'écrivais pas JavaScript dans les fonctions de réduction / cartographie de CouchDB!
portforwardpodcast

9

Je viens de remarquer que la bibliothèque d3.js a implémenté des ensembles, des cartes et d'autres structures de données. Je ne peux pas discuter de leur efficacité, mais à en juger par le fait que c'est une bibliothèque populaire, ce doit être ce dont vous avez besoin.

La documentation est ici

Pour plus de commodité, je copie à partir du lien (les 3 premières fonctions sont celles qui nous intéressent)


  • d3.set ([tableau])

Construit un nouvel ensemble. Si tableau est spécifié, ajoute le tableau de valeurs de chaîne donné à l'ensemble renvoyé.

  • set.has (valeur)

Renvoie vrai si et seulement si cet ensemble a une entrée pour la chaîne de valeur spécifiée.

  • set.add (valeur)

Ajoute la chaîne de valeur spécifiée à cet ensemble.

  • set.remove (valeur)

Si l'ensemble contient la chaîne de valeur spécifiée, la supprime et renvoie true. Sinon, cette méthode ne fait rien et renvoie false.

  • set.values ​​()

Renvoie un tableau des valeurs de chaîne de cet ensemble. L'ordre des valeurs renvoyées est arbitraire. Peut être utilisé comme un moyen pratique de calculer les valeurs uniques d'un ensemble de chaînes. Par exemple:

d3.set (["foo", "bar", "foo", "baz"]). values ​​(); // "foo", "bar", "baz"

  • set.forEach (fonction)

Appelle la fonction spécifiée pour chaque valeur de cet ensemble, en passant la valeur comme argument. Le contexte de la fonction est cet ensemble. Renvoie undefined. L'ordre d'itération est arbitraire.

  • set.empty ()

Renvoie vrai si et seulement si cet ensemble a des valeurs nulles.

  • set.size ()

Renvoie le nombre de valeurs de cet ensemble.


4

Oui, c'est une façon sensée - c'est tout ce qu'un objet est (enfin, pour ce cas d'utilisation) - un tas de clés / valeurs avec accès direct.

Vous auriez besoin de vérifier si elle est déjà là avant de l'ajouter, ou si vous avez juste besoin d'indiquer la présence, "l'ajouter" à nouveau ne change rien en fait, il le redéfinit simplement sur l'objet.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.