JavaScript中如何模拟集合?

220

我在使用JavaScript。我想存储一个唯一、无序的字符串值列表,具有以下属性:

  1. 快速查询'A'是否在列表中?
  2. 快速删除列表中存在的'A'
  3. 如果不存在,则快速将'A'添加到列表中。

我真正想要的是一个集合。有没有关于如何在JavaScript中模拟集合的最佳建议?

这个问题建议使用对象,其中键存储属性,而所有值都设置为true:这是一个明智的做法吗?


2
可能是一个重复问题,JavaScript实现的集合数据结构,和https://dev59.com/rm855IYBdhLWcg3wbzxl。请注意,这些链接指向Stack Overflow上的相关讨论。 - Matt Ball
1
ES6拥有原生的集合(Set)。 - Salvador Dali
7个回答

263
如果您正在使用支持ES6的编程环境(例如node.js,具有所需ES6功能的特定浏览器或为您的环境转换ES6代码),则可以使用内置于ES6中的Set对象。它具有非常好的功能,可以直接在您的环境中使用。
在ES5环境中,对于许多简单的事情,使用一个对象可以很好地解决。如果obj是您要操作的对象,A是一个具有所需值的变量,则可以执行以下操作:
初始化代码:
// 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};

问题1:在列表中是否存在A

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

问题2:如果列表中存在'A',则将其删除:

delete obj[A];

问题3:如果列表中没有'A',则将其添加到列表中

obj[A] = true;

为了完整起见,检查A是否在列表中的测试更加安全:

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

由于基本对象(如constructor属性)上的内置方法和/或属性可能存在冲突,因此需要注意。


ES6 侧边栏: 当前工作版本的 ECMAScript 6 或者有些人称之为 ES 2015,有一个内置的 Set 对象。它现在已经在一些浏览器中实现了。由于浏览器可用性随着时间的推移而改变,您可以查看 这个 ES6 兼容性表 中关于 Set 的行以查看当前浏览器可用性的状态。
内置 Set 对象的一个优点是,它不像 Object 一样将所有键强制转换为字符串,因此您可以将 5 和 "5" 作为单独的键。甚至可以直接在集合中使用对象,而无需进行字符串转换。这里有 一篇文章 描述了一些功能和 MDN 的 Set 对象文档
我现在已经为ES6 set对象编写了一个polyfill,因此您现在可以开始使用它,如果浏览器支持,它将自动转换为内置的set对象。 这有一个优点,即您正在编写符合ES6标准的代码,可以在IE7上运行。 但是,也存在一些缺点。 ES6 set接口利用ES6迭代器,因此您可以执行诸如 for(item of mySet)之类的操作,它会自动迭代整个集合。 但是,这种语言特性无法通过polyfill实现。 您仍然可以在不使用新的ES6语言功能的情况下迭代ES6 set,但是没有新语言功能,使用起来不如我在下面包含的另一个set接口方便。
您可以查看两者后决定哪一个最适合您。 ES6 set polyfill在此处:https://github.com/jfriend00/ES6-Set
FYI,在我的测试中,我注意到Firefox v29 Set实现与当前规范的最新草案不完全一致。 例如,您无法像规范描述的那样链接 .add()方法调用,而我的polyfill则支持。 这可能是规范的问题,因为它尚未最终确定。
预构建的Set对象:如果您想要一个已经构建好的对象,其中包含用于操作集合的方法,您可以使用一系列不同的预构建对象,这些对象实现了不同类型的集合。其中有一个miniSet,它是一个小型代码,实现了集合对象的基本功能。它还有一个更丰富功能的set对象和几个派生对象,包括Dictionary(允许您为每个键存储/检索值)和ObjectSet(允许您保留一组对象-无论是JS对象还是DOM对象,您都可以提供生成每个对象唯一键的函数或ObjectSet将为您生成键)。

这里是miniSet的代码副本(最新代码在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
这解决了问题,但需要明确一点的是,这种实现只适用于整数或字符串之类的东西的集合,对于其他类型的集合是行不通的。 - mkirk
3
@mkirk - 是的,你在集合中索引的项必须具有可用作索引键的字符串表示形式(例如,它是一个字符串或具有toString()方法,可以唯一描述该项)。 - jfriend00
4
要获取列表中的项目,您可以使用 Object.keys(obj) - Blixt
3
@Blixt - Object.keys()需要IE9,FF4,Safari 5,Opera 12或更高版本。对于旧版本浏览器可以使用一个polyfill 在这里 - jfriend00
1
在成员检查中不要使用 obj.hasOwnProperty(prop)。而是使用 Object.prototype.hasOwnProperty.call(obj, prop),即使“集合”包含值 "hasOwnProperty" 也可以正常工作。 - davidchambers
显示剩余19条评论

72

您可以创建一个没有属性的对象,如下:

var set = Object.create(null)

该对象可用作集合,并且无需使用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

不错,但我不确定你为什么说“它消除了使用hasOwnProperty的必要性”。 - blueFast
13
如果只使用set = {},它会继承Object的所有属性(例如toString),因此您需要在if(A in set)中使用hasOwnProperty检查集合的有效负载(即您添加的属性)。 - Thorben Croisé
6
我不知道可以创建一个完全空的对象。谢谢,你的解决方案非常优雅。 - blueFast
1
有趣的是,但这样做的缺点肯定是你必须为每个要添加的元素都有一个set[A]=true语句,而不仅仅是一个初始化程序? - vogomatix
1
不确定您的意思,但如果您是指通过已经存在的集合初始化一个集合,您可以执行类似于s = Object.create(null);s["thorben"] = true;ss = Object.create(s)的操作。 - Thorben Croisé
显示剩余6条评论

23

从ECMAScript 6开始,Set数据结构是一个内置feature。可在此处找到与node.js版本的兼容性here


4
你好,为了清晰起见 - 现在是2014年,这个在Chrome上还处于实验阶段吗?如果不是,请编辑你的回答。谢谢。 - Karel Bílek
1
是的,对于Chrome来说它仍然是实验性的。我相信到2014年底,当ECMAScript被“正式”发布时,它将得到支持。届时我会相应地更新我的答案。 - hymloth
好的,谢谢您的回答!(JavaScript的答案很快就会过时。) - Karel Bílek
' ' in new Set(['\t', ' ']) 产生 false,同样地,1 in new Set([1,2]) 也会产生 false - Val
1
@Val in 不起作用是因为 Set 对象没有将其元素作为属性,这是不好的,因为集合可以有任何类型的元素,但属性必须是字符串。您可以使用 hasSet([1,2]).has(1) - Oriol
1
Salvador Dali的回答 更加全面和最新。 - Dan Dascalescu

15

在JavaScript的ES6版本中,你可以使用内置类型set检查兼容性)。

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

要向集合中添加元素,你只需使用.add(),它在O(1)的时间内运行,如果元素不存在,则将其添加到集合中,如果已存在,则不执行任何操作。您可以向其中添加任何类型的元素(数组、字符串、数字等)。

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

要检查集合中元素的数量,您可以简单地使用.size。 这也在O(1)时间内运行。

numbers.size; // 4

使用.delete()从集合中删除元素。如果该值存在(并且已被删除),则它返回true,如果该值不存在,则返回false。同时,运行时间复杂度为O(1)

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

使用.has()来检查集合中是否存在元素,如果元素在集合中则返回true,否则返回false。时间复杂度为O(1)

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

除了你想要的方法,还有几个额外的方法:

  • numbers.clear(); 只会从集合中删除所有元素
  • numbers.forEach(callback); 以插入顺序迭代集合中的值
  • numbers.entries(); 创建所有值的迭代器
  • numbers.keys(); 返回集合的键,与 numbers.values() 相同

另外还有一个 WeakSet,只允许添加对象类型的值。



你能指出一个关于 .add() 在 O(1) 时间内运行的参考吗?我对此很感兴趣。 - Green

10
我已经开始实现了一组集合,目前对数字和字符串有很好的支持。 我的主要关注点是差异操作,所以我尝试让它尽可能高效。欢迎 Forks 和代码审查!

https://github.com/mcrisc/SetJS


哇,这个类太棒了!如果我不是在CouchDB的map/reduce函数中编写JavaScript,我肯定会使用它! - benathon

9
我刚刚注意到d3.js库中有集合、映射和其他数据结构的实现。 我不能否认它们的效率,但考虑到它是一个流行的库,它一定是你需要的。
文档在这里
为了方便起见,我从链接中复制(前三个函数是感兴趣的)。
  • d3.set([array])
构造一个新的集合。如果指定了数组,则将给定的字符串值数组添加到返回的集合中。
  • set.has(value)
仅当该集合具有指定值字符串的条目时,返回true。
  • set.add(value)
将指定的值字符串添加到此集合中。
  • set.remove(value)
如果集合包含指定的值字符串,则删除它并返回true。否则,此方法不执行任何操作并返回false。
  • set.values()
返回此集合中字符串值的数组。返回值的顺序是任意的。可用作计算一组字符串的唯一值的方便方法。例如: d3.set(["foo", "bar", "foo", "baz"]).values(); //“ foo”,“ bar”,“ baz”
  • set.forEach(function)

对这个集合中的每个值调用指定的函数,并将该值作为参数传递。该函数的上下文是这个集合。返回未定义。迭代顺序是任意的。

  • set.empty()
如果集合中没有任何值,返回true。
  • set.size()
返回此集合中值的数量。

4
是的,这是一种合理的方式 - 对于这个用例来说,对象就是一堆具有直接访问的键/值。在添加之前,您需要检查是否已经存在,或者如果您只需要指示存在,则再次“添加”它实际上并不会改变任何内容,而只是将其再次设置在对象上。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接