Імітація наборів у JavaScript?


220

Я працюю в JavaScript. Я хотів би зберегти список унікальних , не упорядкованих рядкових значень із такими властивостями:

  1. швидкий спосіб запитати "А в списку"?
  2. швидкий спосіб зробити "видалити A зі списку, якщо він існує у списку"
  3. швидкий спосіб зробити "додати А до списку, якщо його ще немає".

Те, що я дуже хочу, - це набір. Будь-які пропозиції щодо найкращого способу імітувати набір у JavaScript?

Це питання рекомендує використовувати Об'єкт , у якому ключі зберігають властивості та всі значення встановлені на істинні: це розумний спосіб?


2
можливий дублікат реалізації JavaScript заданої структури даних та stackoverflow.com/questions/4343746
Matt Ball

Відповіді:


262

Якщо ви програмуєте в середовищі, що підтримує ES6 (наприклад, node.js, певному браузері з необхідними можливостями ES6 або перекодує код ES6 для вашого середовища), ви можете використовувати Setоб'єкт, вбудований в ES6 . Він має дуже приємні можливості і може бути використаний як слід у вашому оточенні.


Для багатьох простих речей у середовищі 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: Додайте "А" до списку, якщо його ще не було

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" як окремі клавіші. І ви навіть можете використовувати Об'єкти безпосередньо в наборі без перетворення рядків. Ось стаття, яка описує деякі можливості та документацію MDN на об'єкт Set.

Зараз я написав поліфайл для встановленого об'єкта ES6, щоб ви могли почати використовувати це зараз, і він автоматично відкладеться до вбудованого об'єкта, якщо браузер його підтримує. Це має перевагу в тому, що ви пишете сумісний код ES6, який буде працювати до IE7. Але, є і деякі недоліки. Інтерфейс набору ES6 використовує переваги ітераторів ES6, щоб ви могли виконувати такі дії, for (item of mySet)і він автоматично повториться через встановлений для вас набір. Але цей тип мовної функції неможливо реалізувати за допомогою polyfill. Ви все ще можете ітератувати набір ES6, не використовуючи нові функції мов ES6, але, чесно кажучи, без нових мовних функцій, це не так зручно, як інший інтерфейс набору, який я включаю нижче.

Ви можете вирішити, який з них найкраще підійде, переглянувши обидва. Поліфункція набору ES6 тут: https://github.com/jfriend00/ES6-Set .

FYI, на своєму власному тестуванні я помітив, що реалізація Firefox v29 Set не є повністю актуальною для поточного проекту специфікації. Наприклад, ви не можете ланцюжок .add()викликів методів, як описує специфікацію, і підтримує мою поліфіл Ймовірно, це питання специфікації в русі, оскільки вона ще не завершена.


Попередньо вбудовані об'єкти набору: Якщо ви хочете вже вбудований об’єкт, який має методи роботи з набором, який ви можете використовувати в будь-якому браузері, ви можете використовувати ряд різних попередньо побудованих об'єктів, що реалізують різні типи наборів. Існує міні-набір, який є невеликим кодом, який реалізує основи заданого об'єкта. У нього також є більш багатий набір об'єктів та декілька похідних даних, включаючи словник (давайте зберігати / отримувати значення для кожного ключа) та 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

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), тому вам доведеться перевірити корисний набір набору (властивості, які ви додали) за допомогою hasOwnPropertyвif (A in set)
Thorben Croisé

6
Я не знав, що можна створити абсолютно порожній об’єкт. Дякую, ваше рішення дуже елегантне.
blueFast

1
Цікаво, але мінус цього, безумовно, у тому, що ви повинні мати set[A]=trueзаяви для кожного елемента, який ви хочете додати, а не лише одного ініціалізатора?
vogomatix

1
Не впевнений, що ви маєте на увазі, але якщо ви маєте на увазі ініціалізацію набору вже наявним набором, ви можете зробити щось по лініїs = Object.create(null);s["thorben"] = true;ss = Object.create(s)
Thorben Croisé

23

За станом на ECMAScript 6, структура даних Set - це вбудована функція . Сумісність з версіями node.js можна знайти тут .


4
Привіт, просто для наочності - це 2014 рік, це все ще експериментально в Chrome? Якщо це не так, чи можете ви відредагувати свою відповідь? Спасибі
Карел Білек

1
Так, це все ще експериментально для Chrome. Я вважаю, що до кінця 2014 року, коли ECMAScript повинен бути офіційно випущений, він буде підтриманий. Тоді я відповідно оновлю відповідь.
гімлет

Добре, дякую за відповідь! (Відповіді на JavaScript доволі швидко застарівають.)
Карел Білек

1
@Val inне працює, оскільки Setоб'єкти не мають своїх елементів як властивостей, що було б погано, оскільки множини можуть мати елементи будь-якого типу, але властивості - це рядки. Ви можете скористатися has:Set([1,2]).has(1)
Oriol

1
Відповідь Сальвадора Далі є більш вичерпною та актуальною.
Дан Даскалеску

14

У ES6 версії Javascript ви вбудували тип для набору ( перевірте сумісність із браузером ).

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(), що повертає істину, якщо елемент знаходиться у наборі, а неправдиво - інакше. Також працює в O(1).

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

Окрім бажаних методів, є кілька додаткових:

  • numbers.clear(); просто видалить усі елементи з набору
  • numbers.forEach(callback); ітерація через значення набору в порядку вставки
  • numbers.entries(); створити ітератор усіх значень
  • numbers.keys(); повертає ключі набору, які є такими ж, як numbers.values()

Існує також слабкий набір, який дозволяє додавати лише значення типів об'єкта.


Ви можете вказати посилання на .add()прогони в O (1)? Мене це заінтригує,
Зелений

10

Я розпочав реалізацію Sets, яка в даний час працює досить добре з числами та рядками. Моя головна увага була різницевою операцією, тому я намагався зробити її максимально ефективною. Огляди вилок та кодів вітаються!

https://github.com/mcrisc/SetJS


Уау цей клас горіхи! Я б цілком використовував це, якби я не писав JavaScript всередині карти CouchDB / зменшував!
portforwardpodcast

9

Я щойно помітив, що бібліотека d3.js має реалізацію наборів, карт та інших структур даних. Я не можу сперечатися про їхню ефективність, але судячи з того, що це популярна бібліотека, вона повинна бути тим, що вам потрібно.

Документація тут

Для зручності я копіюю посилання (перші 3 функції цікавлять)


  • d3.set ([масив])

Побудова нового набору. Якщо масив вказаний, додає заданий масив значень рядків до повернутого набору.

  • set.has (значення)

Повертає істину, якщо і лише тоді, коли цей набір має запис для вказаного рядка значення.

  • set.add (значення)

Додає до цього набору вказаний рядок значення.

  • set.remove (значення)

Якщо набір містить вказаний рядок значення, видаляє його та повертає true. В іншому випадку цей метод нічого не робить і повертає помилкове.

  • set.values ​​()

Повертає масив значень рядків у цьому наборі. Порядок повернених значень довільний. Може використовуватися як зручний спосіб обчислення унікальних значень для набору рядків. Наприклад:

d3.set (["foo", "bar", "foo", "baz"]). значення (); // "foo", "bar", "baz"

  • set.forEach (функція)

Викликає вказану функцію для кожного значення в цьому наборі, передаючи значення як аргумент. Цей контекст функції - це безліч. Повертається невизначено. Порядок ітерації є довільним.

  • set.empty ()

Повертає істину тоді і лише тоді, коли цей набір має нульові значення.

  • set.size ()

Повертає кількість значень у цьому наборі.


4

Так, це розумний спосіб - це все, що об'єкт є (ну, для цього випадку використання) - купа ключів / значень з прямим доступом.

Вам потрібно буде перевірити, чи він уже є перед тим, як додати його, чи вам просто потрібно вказати присутність, "додавання" його знову насправді нічого не змінює, він просто встановлює його на об'єкт знову.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.