Набор объектов в JavaScript

Я хотел бы иметь набор объектов в Javascript. То есть структура данных, которая содержит только уникальные объекты.

Обычно рекомендуется использовать свойства, например myset["key"] = true, Тем не менее, мне нужны ключи, чтобы быть объектами. Я читал, что Javascript переводит имена свойств в строки, поэтому я думаю, что я не могу использовать myset[myobject] = true,

Я мог бы использовать массив, но мне нужно что-то лучше, чем производительность O(n) для добавления, поиска и удаления элементов.

Он должен уметь различать объекты только по ссылке, поэтому с учетом:

var a = {};
var b = {};

тогда оба a а также b должны быть в состоянии быть добавлены, потому что они являются отдельными объектами.

По сути, я после чего-то вроде C++ std::set, который может хранить объекты Javascript. Есть идеи?

13 ответов

Решение

ES6 предоставляет родную Set:

let s = new Set();
let a = {};
let b = {};

s.add(a);

console.log(s.has(a));  // true
console.log(s.has(b));  // false

Вот сумасшедшее предположение... введите его в результате JSON.stringify(object)

Я отвечаю на свой вопрос, но я нашел альтернативное решение, которое мне показалось интересным, и подумал, что было бы полезно поделиться им.

Ответ Кволков дал мне идею. Предоставление объекта toString() Метод однозначно идентифицирует экземпляр, свойства объекта могут использоваться для хранения набора объектов. По сути, для хранения объекта x, ты можешь использовать items[x.toString()] = x;, Обратите внимание, что значением является сам объект, поэтому набор объектов можно извлечь, посмотрев на все itemсвойства и сброс всех значений в массив.

Вот класс, который я называю ObjectSet, в полном объеме. Это требует, чтобы объекты были однозначно определены их toString() метод, который подходит для моих целей. add, remove а также contains все должны работать быстрее, чем за O(n) - независимо от эффективности доступа к свойствам javascript, которая, как мы надеемся, равна либо O(1), либо O(n log n).

// Set of objects.  Requires a .toString() overload to distinguish objects.
var ObjectSet = function ()
{
    this.items = {};
    this.item_count = 0;
};

ObjectSet.prototype.contains = function (x)
{
    return this.items.hasOwnProperty(x.toString());
};

ObjectSet.prototype.add = function (x)
{
    if (!this.contains(x))
    {
        this.items[x.toString()] = x;
        this.item_count++;
    }

    return this;
};

ObjectSet.prototype.remove = function (x)
{
    if (this.contains(x))
    {
        delete this.items[x.toString()];
        this.item_count--;
    }

    return this;
};

ObjectSet.prototype.clear = function ()
{
    this.items = {};
    this.item_count = 0;

    return this;
};

ObjectSet.prototype.isEmpty = function ()
{
    return this.item_count === 0;
};

ObjectSet.prototype.count = function ()
{
    return this.item_count;
};

ObjectSet.prototype.values = function ()
{
    var i, ret = [];

    for (i in this.items)
    {
        if (this.items.hasOwnProperty(i))
            ret.push(this.items[i]);
    }

    return ret;
};

Это возможно не для всех объектов, но если ваш объект имеет .toString() Метод реализован, это:

var x = {toString: function(){ return 'foo'; }};
var y = {toString: function(){ return 'bar'; }};
var obj = {};
obj[x] = 'X';
obj[y] = 'Y';
console.log(obj);
// { foo: 'X', bar: 'Y' }

Если вы хотите сделать это проще, сделайте это классом:

function myObj(name){
   this.name = name;
}
myObj.prototype.toString = function(){ return this.name; }

var obj = {};
obj[new myObj('foo')] = 'X';
obj[new myObj('bar')] = 'Y';

Я использовал карту, решил свой случай

      const objectsMap = new Map();
const placesName = [
  { place: "here", name: "stuff" },
  { place: "there", name: "morestuff" },
  { place: "there", name: "morestuff" },
];
placesName.forEach((object) => {
  objectsMap.set(object.place, object);
});
console.log(objectsMap);

ECMAScript6Set должен вести себя так:

Рабочий пример для Firefox 32 (но не реализован в Chromium 37):

if (Set) {
  var s = new Set()
  var a = {}
  var b = {}
  var c = {}
  s.add(a)
  s.add(b)
  s.add(b)
  assert(s.size === 2)
  assert(s.has(a))
  assert(s.has(b))
  assert(!s.has(c))
}

Это не удивительно, так как {} != {}: равенство сравнивает адреса объектов по умолчанию.

Модуль, который реализует его для браузеров без поддержки: https://github.com/medikoo/es6-set

Для того, что вы пытаетесь сделать (наборы объектов), не существует нативной реализации Javascript. Вы должны будете реализовать это самостоятельно. Один из способов сделать это - реализовать функцию хеширования для ваших объектов. Поддерживающим типом данных набора будет ассоциативный массив, где ключом массива является значение, которое вы получаете при вызове хеш-функции объекта, а значением массива является сам объект.

Конечно, это не решает проблему, которую вы выдвинули на первый план, поэтому вам нужно будет также учитывать равенство (возможно, реализовать функцию равенства)?

Вместо того, чтобы делать хеш-функцию свойством самого объекта, вы можете иметь отдельную хеш-функцию, которая принимает объект в качестве входных данных и генерирует хеш-значение (предположительно, путем перебора его свойств).

Используя этот метод, вы сможете получить O(1) для вставки, поиска и удаления (не считая порядка хэш-функции, которая не должна быть хуже, чем O(n)особенно если вы перебираете его свойства, чтобы создать хешированное значение).

Javascript Set не выполняет глубокое сравнение объектов.

Используя lodash, это уникальный массив с глубоким сравнением объектов:

      const objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 2 }];
 
_.uniqWith(objects, _.isEqual);

Дан массив следующего типа:

      Array<{ foo: T1, bar: T2 }>

Вы можете построить соответствующий словарь типа:

      { [foo: T1]: Set<T2> }

Стремление к{ foo: fooValue, bar: barValue }можно выполнить следующим образом:

      if (fooValue in dictionary && dictionary[fooValue].has(barValue))

Таким образом, мы можем построить то, что будетObjectSet<T1, T2>. Если у вас теперь есть три элемента, вы можете построить следующий словарь:

      { [foo: T1]: ObjectSet<T2, T3> }

и расширить свой ObjectSet до любого количества свойств по индукции.

Это предполагает, что ваши типы могут использоваться в качестве подписей индекса.

ну, не существует собственного класса ecmascript, такого как std::set, куда вы передаете функцию сравнения. Конечно, вы можете реализовать эту структуру данных, как и в любом другом языке программирования. Но для большинства случаев использования я предлагаю что-то проще:

  • Если ваши объекты имеют какое-то поле ключа или идентификатора и вам нужны операции O(1) для таких операций, как поиск/добавление/обновление/удаление, вам следует использовать карту, использующую это поле идентификатора.
  • Если у вас нет ключа, и вы просто хотите избежать дублирования в коллекции, вы можете использовать карту или даже установить, используякак идентификатор. Если у вас есть вложенные объекты или массивы, вам также следует сократить их элементы (я не писал эту часть).

Я написал класс, который может соответствовать обоим:

      class ObjectSet {
    constructor(iterable) {
        this.map = new Map()
        if(!iterable) {
            return;
        }
        for(o of iterable) {
            this.map.set(this._getId(o), o);
        }
    }

    _getId(o) {
        return o.id ? o.id :
            JSON.stringify(o, Object.keys(o).sort());
    }

    get(id) {
        return this.map.get(id);
    }

    has(o) {
        return this.map.has(this._getId(o));
    }

    //add or update. Call "add" if you want to match ES6 Set interface
    set(o) {
        this.map.set(this._getId(o), o);
    }

    delete(o) {
        this.map.delete(this._getId(o));
    }

    values() {
        return this.map.values();
    }

    [Symbol.iterator] () {
        return this.values();
    }

    get size() {
        return this.map.size;
    }
}

//first use case example, fast find and update:
let objSet = new ObjectSet();

//add
objSet.set({
    id: 1,
    name: "John",
    age: 30
});
const eric = {
    id: 2,
    name: "Eric",
    age: 27
}
//add
objSet.set(eric)
console.log(objSet.size); //2

//update object of id 2
objSet.set({
    id: 2,
    name: "Eric",
    age: 28
})
console.log(objSet.size); // still 2


for(o of objSet) {
    console.log(o);
}
/*
prints:

{ id: 1, name: 'John Abbot', age: 30 }
{ id: 2, name: 'Eric', age: 28 }
*/

objSet.delete({id: 1});//deletes john
objSet.delete(eric);
console.log(objSet.size);//0


//second use case, lets remove duplicated objects using this class
let points = [
    {
        x: 10,
        y: 40
    },
    {
        x: 10,
        y: 40
    },
    {
        x: 10,
        y: 35
    }
]


//lets remove duplicated objects using ObjectSet:
objSet = new ObjectSet(points);
console.log(o.size);//2
for(o of objSet) {
    console.log(o);
}

//same with set method
objSet.set({
    x: 10,
    y: 35
});
console.log(objSet.size);//still 2

Кажется, что внутренний вызов функции работает с префиксом этого. Exemple:

var put;
this.put = put = function(x) {
    if (!this.contains(x))
        list.push(x);

    return this;
}

Просто напечатали это, это только кратко проверено:

var Set = function Set()
{
    var list = [];

    var contains;
    this.contains = contains = function(x) {
        return list.indexOf(x) >= 0;
    }

    var put;
    this.put = put = function(x) {
        if (!contains(x))
            list.push(x);

        return this;
    }

    var remove;
    this.remove = remove = function(x)
    {
        var idx = list.indexOf(x);
        if (idx >= 0)
            list.splice(idx,1);

        return this;
    }

    var all;
    this.all = all = function()
    {
        return list.concat();
    }

    return this;
}

Пожалуйста, используйте этот код в качестве ссылки.

Другие вопросы по тегам