模仿 JavaScript 中的集合?

IT技术 javascript
2021-01-14 17:15:02

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

  1. 一种快速询问“列表中是否有 A”的方法?
  2. 一种快速执行“如果列表中存在 A 从列表中删除它”的方法
  3. 一种快速的方法来执行“如果 A 尚不存在,则将其添加到列表中”。

我真正想要的是一套。关于在 JavaScript 中模仿集合的最佳方式有什么建议吗?

这个问题建议使用 Object,键存储属性,值都设置为 true:这是一种明智的方式吗?

6个回答

如果您在支持 ES6 的环境中编程(例如 node.js,具有您需要的 ES6 功能的特定浏览器或为您的环境转译 ES6 代码),那么您可以使用SetES6 中内置对象它具有非常好的功能,可以在您的环境中直接使用。


对于 ES5 环境中的许多简单事情,使用 Object 效果很好。如果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 对象它现在在一些浏览器中实现。由于浏览器的可用性随时间变化的,你可以看看线Set此ES6兼容性表,查看浏览器可用性的当前状态。

内置 Set 对象的一个​​优点是它不会像 Object 那样将所有键强制转换为字符串,因此您可以将 5 和“5”作为单独的键。而且,您甚至可以直接在集合中使用对象,而无需进行字符串转换。是一篇描述Set 对象的一些功能和MDN 文档的文章

我现在已经为 ES6 set 对象编写了一个 polyfill,所以你现在可以开始使用它,如果浏览器支持它,它会自动遵循内置的 set 对象。这样做的好处是您正在编写 ES6 兼容的代码,这些代码将一直工作到 IE7。但是,也有一些缺点。ES6 集合接口利用了 ES6 迭代器,因此您可以执行诸如此类的操作for (item of mySet),它会自动为您遍历集合。但是,这种语言特性不能通过 polyfill 实现。您仍然可以在不使用新的 ES6 语言功能的情况下迭代 ES6 集,但坦率地说,如果没有新的语言功能,它不如我在下面包含的其他集界面方便。

您可以在查看两者后决定哪一个最适合您。ES6 set polyfill 在这里:https : //github.com/jfriend00/ES6-Set

仅供参考,在我自己的测试中,我注意到 Firefox v29 Set 实现在规范的当前草案中并不完全是最新的。例如,您不能.add()像规范描述和我的 polyfill 支持那样链接方法调用。这可能是一个正在制定的规范问题,因为它尚未最终确定。


Pre-Built Set 对象:如果您想要一个已经构建的对象,该对象具有可以在任何浏览器中使用的操作集合的方法,您可以使用一系列不同的预构建对象来实现不同类型的集合。有一个 miniSet,它是实现集合对象基础的小代码。它还有一个功能更丰富的集合对象和几个派生,包括一个字典(让你存储/检索每个键的值)和一个 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;
@Blixt -Object.keys()需要 IE9、FF4、Safari 5、Opera 12 或更高版本。有旧版本浏览器一个填充工具在这里
2021-03-13 17:15:02
不要obj.hasOwnProperty(prop)用于会员资格检查。使用Object.prototype.hasOwnProperty.call(obj, prop)替代,即使“集”包含值工作"hasOwnProperty"
2021-03-16 17:15:02
这解决了问题,但要清楚的是,除了整数或字符串之外,此实现不适用于其他事物集。
2021-03-18 17:15:02
要获取列表中的项目,您可以使用Object.keys(obj).
2021-03-29 17:15:02
@mkirk - 是的,您在集合中索引的项目必须具有可以作为索引键的字符串表示形式(例如,它要么是字符串,要么具有唯一描述该项目的 toString() 方法)。
2021-04-07 17:15:02

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

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
如果您只是使用set = {}它,它将继承 Object(例如toString)的所有属性,因此您必须使用hasOwnPropertyin检查该集合的有效负载(您添加的属性)if (A in set)
2021-03-15 17:15:02
不确定你的意思,但如果你指的是通过已经存在的集合初始化一个集合,你可以做一些类似的事情 s = Object.create(null);s["thorben"] = true;ss = Object.create(s)
2021-03-19 17:15:02
很好,但不知道你为什么说“消除了使用 hasOwnProperty 的需要”
2021-03-22 17:15:02
很有趣,但这样做的缺点肯定是您必须为set[A]=true要添加的每个元素都添加语句,而不仅仅是一个初始化程序?
2021-03-27 17:15:02
我不知道可以创建一个完全空的对象。谢谢,您的解决方案非常优雅。
2021-04-03 17:15:02

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

你好,为了清楚起见 - 现在是 2014 年,这在 Chrome 中仍然是实验性的吗?如果不是,您能否编辑您的答案?谢谢
2021-03-13 17:15:02
是的,Chrome 仍处于试验阶段。我相信到 2014 年底,当 ECMAScript 应该“正式”发布时,它会得到支持。然后我会相应地更新我的答案。
2021-03-13 17:15:02
好的,谢谢解答!(JavaScript 答案很快就会过时。)
2021-03-16 17:15:02
@Valin不起作用,因为Set对象没有其元素作为属性,这很糟糕,因为集合可以包含任何类型的元素,但属性是字符串。您可以使用hasSet([1,2]).has(1)
2021-03-26 17:15:02
2021-04-02 17:15:02

在 ES6 版本的 Javascript 中,您已经为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(),如果元素是集合,否则为false返回true。也运行在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) 中运行的引用吗?我对这个很感兴趣
2021-04-09 17:15:02

我已经开始了一个 Sets 的实现,它目前在数字和字符串上工作得很好。我的主要关注点是差异操作,所以我试图让它尽可能高效。欢迎分叉和代码审查!

https://github.com/mcrisc/SetJS

哇这门课太疯狂了!如果我不在 CouchDB map/reduce 函数中编写 JavaScript,我会完全使用它!
2021-03-22 17:15:02