Mimicking sets in JavaScript?

前端 未结 7 2043
栀梦
栀梦 2020-11-22 13:01

I\'m working in JavaScript. I\'d like to store a list of unique, unordered string values, with the following properties:

  1. a fast way to ask \'is A in t
相关标签:
7条回答
  • 2020-11-22 13:25

    I just noticed that d3.js library has implementation of sets, maps and other data structures. I can't argue about their efficiency but judging by the fact that it is a popular library it must be what you need.

    The documentation is here

    For convenience I copy from the link (the first 3 functions are those of interest)


    • d3.set([array])

    Constructs a new set. If array is specified, adds the given array of string values to the returned set.

    • set.has(value)

    Returns true if and only if this set has an entry for the specified value string.

    • set.add(value)

    Adds the specified value string to this set.

    • set.remove(value)

    If the set contains the specified value string, removes it and returns true. Otherwise, this method does nothing and returns false.

    • set.values()

    Returns an array of the string values in this set. The order of the returned values is arbitrary. Can be used as a convenient way of computing the unique values for a set of strings. For example:

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

    • set.forEach(function)

    Calls the specified function for each value in this set, passing the value as an argument. The this context of the function is this set. Returns undefined. The iteration order is arbitrary.

    • set.empty()

    Returns true if and only if this set has zero values.

    • set.size()

    Returns the number of values in this set.

    0 讨论(0)
  • 2020-11-22 13:26

    You can create an Object with no properties like

    var set = Object.create(null)
    

    which can act as a set and eliminates the need to use 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
    
    0 讨论(0)
  • 2020-11-22 13:31

    I have started an implementation of Sets that currently works pretty well with numbers and strings. My main focus was the difference operation, so I tried to make it as efficient as I could. Forks and code reviews are welcome!

    https://github.com/mcrisc/SetJS

    0 讨论(0)
  • 2020-11-22 13:34

    Yes, that's a sensible way--that's all an object is (well, for this use-case)--a bunch of keys/values with direct access.

    You'd need to check to see if it's already there before adding it, or if you just need to indicate presence, "adding" it again doesn't actually change anything, it just sets it on the object again.

    0 讨论(0)
  • 2020-11-22 13:37

    If you are programming in an ES6-capable environment (such as node.js, a specific browser with the ES6 capabilities you need or transpiling ES6 code for your environment), then you can use the Set object built into ES6. It has very nice capabilities and can be used as is right in your environment.


    For many simple things in an ES5 environment, using an Object works very well. If obj is your object and A is a variable that has the value you want to operate on in the set, then you can do these:

    Initialization code:

    // 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: Is A in the list:

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

    Question 2: Delete 'A' from the list if it's there:

    delete obj[A];
    

    Question 3: Add 'A' to the list if it wasn't already there

    obj[A] = true;
    

    For completeness, the test for whether A is in the list is a little safer with this:

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

    because of potential conflict between built-in methods and/or properties on the base Object like the constructor property.


    Sidebar on ES6: The current working version of ECMAScript 6 or somethings called ES 2015 has a built-in Set object. It is implemented now in some browsers. Since browser availability changes over time, you can look at the line for Set in this ES6 compatibility table to see the current status for browser availability.

    One advantage of the built-in Set object is that it doesn't coerce all keys to a string like the Object does so you can have both 5 and "5" as separate keys. And, you can even use Objects directly in the set without a string conversion. Here's an article that describes some of the capabilities and MDN's documentation on the Set object.

    I have now written a polyfill for the ES6 set object so you could start using that now and it will automatically defer to the built-in set object if the browser supports it. This has the advantage that you're writing ES6 compatible code that will work all the way back to IE7. But, there are some downsides. The ES6 set interface takes advantage of the ES6 iterators so you can do things like for (item of mySet) and it will automatically iterate through the set for you. But, this type of language feature cannot be implemented via polyfill. You can still iterate an ES6 set without using the new ES6 languages features, but frankly without the new language features, it isn't as convenient as the other set interface I include below.

    You can decide which one works best for you after looking at both. The ES6 set polyfill is here: https://github.com/jfriend00/ES6-Set.

    FYI, in my own testing, I've noticed that the Firefox v29 Set implementation is not fully up-to-date on the current draft of the spec. For example, you can't chain .add() method calls like the spec describes and my polyfill supports. This is probably a matter of a specification in motion as it is not yet finalized.


    Pre-Built Set objects: If you want an already built object that has methods for operating on a set that you can use in any browser, you can use a series of different pre-built objects that implement different types of sets. There is a miniSet which is small code that implements the basics of a set object. It also has a more feature rich set object and several derivations including a Dictionary (let's you store/retrieve a value for each key) and an ObjectSet (let's you keep a set of objects - either JS objects or DOM objects where you either supply the function that generates a unique key for each one or the ObjectSet will generate the key for you).

    Here's a copy of the code for the miniSet (most up-to-date code is here on 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;
    
    0 讨论(0)
  • 2020-11-22 13:41

    As of ECMAScript 6, the Set data-structure is a built-in feature. Compatibility with node.js versions can be found here.

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