Imitando i set in JavaScript?


220

Sto lavorando in JavaScript. Vorrei memorizzare un elenco di valori di stringa univoci e non ordinati, con le seguenti proprietà:

  1. un modo rapido per chiedere "è A nell'elenco"?
  2. un modo rapido per fare 'eliminare A dall'elenco se esiste nell'elenco'
  3. un modo rapido per fare 'aggiungere A all'elenco se non è già presente'.

Quello che voglio davvero è un set. Qualche suggerimento per il modo migliore per imitare un set in JavaScript?

Questa domanda consiglia di utilizzare un oggetto , con le chiavi che memorizzano le proprietà e i valori impostati su true: è un modo ragionevole?



Risposte:


262

Se si sta programmando in un ambiente compatibile con ES6 (come node.js, un browser specifico con le funzionalità ES6 necessarie o la trasplicazione del codice ES6 per il proprio ambiente), è possibile utilizzare l' Setoggetto incorporato in ES6 . Ha delle ottime capacità e può essere usato come è nel tuo ambiente.


Per molte cose semplici in un ambiente ES5, l'utilizzo di un oggetto funziona molto bene. Se objè il tuo oggetto ed Aè una variabile che ha il valore su cui vuoi operare nel set, allora puoi farlo:

Codice di inizializzazione:

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

Domanda 1: È Anell'elenco:

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

Domanda 2: Elimina 'A' dall'elenco se è presente:

delete obj[A];

Domanda 3: aggiungi "A" all'elenco se non era già presente

obj[A] = true;

Per completezza, il test per stabilire se Aè nell'elenco è un po 'più sicuro con questo:

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

a causa del potenziale conflitto tra metodi integrati e / o proprietà sull'oggetto base come la constructorproprietà.


Barra laterale su ES6: l'attuale versione funzionante di ECMAScript 6 o qualcosa chiamato ES 2015 ha un oggetto Set incorporato . Ora è implementato in alcuni browser. Poiché la disponibilità del browser cambia nel tempo, è possibile cercare la riga Setin questa tabella di compatibilità ES6 per vedere lo stato corrente della disponibilità del browser.

Un vantaggio dell'oggetto Set incorporato è che non obbliga tutte le chiavi a una stringa come fa l'Oggetto, quindi puoi avere sia 5 che "5" come chiavi separate. Inoltre, puoi persino usare Oggetti direttamente nel set senza una conversione di stringhe. Ecco un articolo che descrive alcune delle funzionalità e la documentazione di MDN sull'oggetto Set.

Ora ho scritto un polyfill per l'oggetto set ES6 in modo da poter iniziare a usarlo ora e rimanderà automaticamente all'oggetto set incorporato se il browser lo supporta. Questo ha il vantaggio di scrivere codice compatibile ES6 che funzionerà fino a IE7. Ma ci sono alcuni aspetti negativi. L'interfaccia del set ES6 sfrutta gli iteratori ES6 in modo che tu possa fare cose del genere for (item of mySet)e itererà automaticamente attraverso il set per te. Tuttavia, questo tipo di funzionalità del linguaggio non può essere implementata tramite polyfill. È ancora possibile iterare un set ES6 senza utilizzare le nuove funzionalità delle lingue ES6, ma francamente senza le nuove funzionalità della lingua, non è conveniente come l'altra interfaccia impostata che includo di seguito.

Puoi decidere quale funziona meglio per te dopo aver visto entrambi. Il polyfill set ES6 è qui: https://github.com/jfriend00/ES6-Set .

Cordiali saluti, nei miei test ho notato che l'implementazione di Firefox v29 Set non è completamente aggiornata sulla bozza corrente delle specifiche. Ad esempio, non è possibile concatenare .add()chiamate di metodo come descritto nelle specifiche e supportate da my polyfill. Probabilmente si tratta di una specifica in movimento in quanto non ancora finalizzata.


Oggetti set predefiniti: se si desidera un oggetto già costruito con metodi per operare su un set che è possibile utilizzare in qualsiasi browser, è possibile utilizzare una serie di diversi oggetti precostruiti che implementano diversi tipi di set. C'è un miniSet che è un piccolo codice che implementa le basi di un oggetto set. Ha anche un oggetto set più ricco di funzionalità e diverse derivazioni tra cui un dizionario (memorizziamo / recuperiamo un valore per ogni chiave) e un ObjectSet (manteniamo un insieme di oggetti - oggetti JS o oggetti DOM in cui fornire il funzione che genera una chiave univoca per ognuno o ObjectSet genererà la chiave per te).

Ecco una copia del codice per il miniSet (il codice più aggiornato è qui su 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
Questo risolve la domanda, ma per essere chiari, questa implementazione non funzionerà per insiemi di cose oltre a interi o stringhe.
mkirk,

3
@mkirk: sì, l'elemento che stai indicizzando nel set deve avere una rappresentazione di stringa che può essere la chiave di indice (ad esempio, è una stringa o ha un metodo toString () che descrive in modo univoco l'elemento).
jfriend00,

4
Per ottenere gli elementi nell'elenco, è possibile utilizzare Object.keys(obj).
Blixt,

3
@Blixt: Object.keys()necessita di IE9, FF4, Safari 5, Opera 12 o versioni successive. C'è un polyfill per i browser più vecchi qui .
jfriend00,

1
Non utilizzare obj.hasOwnProperty(prop)per i controlli di appartenenza. Usa Object.prototype.hasOwnProperty.call(obj, prop)invece, che funziona anche se il "set" contiene il valore "hasOwnProperty".
davidchambers,

72

È possibile creare un oggetto senza proprietà come

var set = Object.create(null)

che può agire come un set ed elimina la necessità di utilizzare 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

Bello, ma non so perché dici che "elimina la necessità di utilizzare hasOwnProperty"
blueFast

13
Se lo usi set = {}, erediterai tutte le proprietà dall'oggetto (ad es. toString), Quindi dovrai controllare il payload del set (proprietà che hai aggiunto) con hasOwnPropertyinif (A in set)
Thorben Croisé,

6
Non sapevo che fosse possibile creare un oggetto completamente vuoto. Grazie, la tua soluzione è molto elegante.
blueFast

1
Interessante, ma il rovescio della medaglia di questo è sicuramente che devi avere delle set[A]=truedichiarazioni per ogni elemento che desideri aggiungere invece di un solo inizializzatore?
vogomatix,

1
Non sei sicuro di cosa intendi, ma se ti riferisci all'inizializzazione di un set da un set già presente, puoi fare qualcosa del generes = Object.create(null);s["thorben"] = true;ss = Object.create(s)
Thorben Croisé,

23

A partire da ECMAScript 6, la struttura di dati Set è una funzionalità integrata . La compatibilità con le versioni node.js è disponibile qui .


4
Ciao, solo per chiarezza - è il 2014 ora, è ancora sperimentale in Chrome? In caso contrario, potresti modificare la tua risposta? Grazie
Karel Bílek,

1
Sì, è ancora sperimentale per Chrome. Credo che entro la fine del 2014, quando ECMAScript dovrebbe essere rilasciato "ufficialmente", sarà supportato. Quindi aggiornerò la mia risposta di conseguenza.
hymloth,

OK, grazie per aver risposto! (Le risposte JavaScript diventano obsolete abbastanza rapidamente.)
Karel Bílek,

1
@Val innon funziona perché gli Setoggetti non hanno i suoi elementi come proprietà, il che sarebbe male perché gli insiemi possono avere elementi di qualsiasi tipo, ma le proprietà sono stringhe. Puoi usare has:Set([1,2]).has(1)
Oriol il

1
La risposta di Salvador Dalì è più completa e aggiornata.
Dan Dascalescu,

14

Nella versione ES6 di Javascript hai inserito il tipo per set ( controlla la compatibilità con il tuo browser ).

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

Per aggiungere un elemento al set che usi semplicemente .add(), che viene eseguito O(1)e aggiunge l'elemento al set (se non esiste) o non fa nulla se è già lì. È possibile aggiungere elementi di qualsiasi tipo lì (matrici, stringhe, numeri)

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

Per controllare il numero di elementi nel set, puoi semplicemente usare .size. Funziona anche dentroO(1)

numbers.size; // 4

Per rimuovere l'elemento dal set usare .delete(). Restituisce vero se il valore era presente (ed è stato rimosso) e falso se il valore non esisteva. Funziona anche dentro O(1).

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

Per verificare se l'elemento esiste in un set usare .has(), che restituisce true se l'elemento è nel set e false in caso contrario. Funziona anche dentro O(1).

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

Oltre ai metodi desiderati, ce ne sono alcuni aggiuntivi:

  • numbers.clear(); rimuoverebbe semplicemente tutti gli elementi dal set
  • numbers.forEach(callback); iterando attraverso i valori dell'insieme nell'ordine di inserimento
  • numbers.entries(); crea un iteratore di tutti i valori
  • numbers.keys(); restituisce le chiavi del set che è uguale a numbers.values()

C'è anche un Weakset che consente di aggiungere solo valori di tipo oggetto.


potresti indicare un riferimento alle .add()corse in O (1)? Sono incuriosito da questo,
Green,

10

Ho iniziato un'implementazione di set che attualmente funziona abbastanza bene con numeri e stringhe. Il mio obiettivo principale era l'operazione di differenza, quindi ho cercato di renderlo il più efficiente possibile. Forks e recensioni di codici sono benvenuti!

https://github.com/mcrisc/SetJS


wow questa classe è pazza! Lo userei totalmente se non stessi scrivendo JavaScript nella mappa / riduzione delle funzioni di CouchDB!
portforwardpodcast,

9

Ho appena notato che la libreria d3.js ha l'implementazione di set, mappe e altre strutture di dati. Non posso discutere della loro efficienza, ma a giudicare dal fatto che si tratta di una biblioteca popolare deve essere ciò di cui hai bisogno.

La documentazione è qui

Per comodità copio dal link (le prime 3 funzioni sono quelle di interesse)


  • d3.set ([matrice])

Crea un nuovo set. Se viene specificato un array, aggiunge il set di valori stringa specificato al set restituito.

  • set.has (valore)

Restituisce vero se e solo se questo set ha una voce per la stringa di valore specificata.

  • set.add (valore)

Aggiunge la stringa di valore specificata a questo set.

  • set.remove (valore)

Se il set contiene la stringa del valore specificato, la rimuove e restituisce true. Altrimenti, questo metodo non fa nulla e restituisce false.

  • set.values ​​()

Restituisce un array di valori stringa in questo set. L'ordine dei valori restituiti è arbitrario. Può essere usato come un modo conveniente per calcolare i valori univoci per un set di stringhe. Per esempio:

d3.set (["pippo", "bar", "pippo", "baz"]). valori (); // "foo", "bar", "baz"

  • set.forEach (funzione)

Chiama la funzione specificata per ciascun valore in questo set, passando il valore come argomento. Questo contesto della funzione è questo insieme. Restituisce indefinito. L'ordine di iterazione è arbitrario.

  • set.empty ()

Restituisce vero se e solo se questo set ha valori zero.

  • set.size ()

Restituisce il numero di valori in questo set.


4

Sì, è un modo sensato - tutto ciò che un oggetto è (beh, per questo caso d'uso) - un gruppo di chiavi / valori con accesso diretto.

Dovresti controllare per vedere se è già lì prima di aggiungerlo, o se devi solo indicare la presenza, "aggiungerlo" di nuovo non cambia nulla, lo imposta di nuovo sull'oggetto.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.