Як налаштувати рівність об'єктів для JavaScript Set


167

Новий ES 6 (Гармонія) представляє новий об'єкт Set . Алгоритм ідентичності, який використовується Set, схожий з ===оператором і не дуже підходить для порівняння об'єктів:

var set = new Set();
set.add({a:1});
set.add({a:1});
console.log([...set.values()]); // Array [ Object, Object ]

Як налаштувати рівність для заданих об'єктів, щоб зробити глибоке порівняння об'єктів? Чи є щось на кшталт Java equals(Object)?


3
Що ви маєте на увазі під «налаштувати рівність»? Javascript не дозволяє перевантажувати оператора, тому немає можливості перевантажувати ===оператора. Об'єкт набору ES6 не має методів порівняння. .has()Метод і .add()методи робота тільки з неї бути тим же самим реальним об'єктом або ж значенням для примітивного.
jfriend00

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

Відповіді:


107

Об'єкт ES6 Setне має жодних методів порівняння або розширення спеціальної порівняльності.

І .has(), .add()і .delete()методи працюють лише за те, що вони є тим самим фактичним об'єктом або тим самим значенням для примітиву і не мають засобів підключення або заміни саме цієї логіки.

Ви, мабуть, можете отримати власний об'єкт від a Setі замінити .has(), .add()а також .delete()методами з чимось, що спершу зробив глибоке порівняння об'єкта, щоб виявити, чи є елемент вже в Наборі, але ефективність, ймовірно, не буде хорошою, оскільки базовий Setоб'єкт не допоможе зовсім. Вам, мабуть, доведеться просто зробити грубу силу ітерації через усі існуючі об'єкти, щоб знайти збіг за допомогою власного користувацького порівняння перед тим, як викликати оригінал.add() .

Ось деяка інформація з цієї статті та обговорення особливостей ES6:

5.2 Чому я не можу налаштувати, як карти та набори порівнюють ключі та значення?

Питання: Було б непогано, якби був спосіб налаштувати, які клавіші карти та які елементи набору вважаються рівними. Чому його немає?

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

Інший варіант, доступний на Java, - це визначити рівність методом, який об'єкт реалізує (equals () у Java). Однак такий підхід є проблематичним для змінних об'єктів: загалом, якщо об'єкт змінюється, його "розташування" всередині колекції також має змінюватися. Але це не те, що відбувається в Java. JavaScript, ймовірно, піде більш безпечним шляхом, лише дозволяючи порівнювати значення за спеціальними незмінними об'єктами (так звані об'єкти значення). Порівняння за значенням означає, що два значення вважаються рівними, якщо їх зміст є рівним. Примітивні значення порівнюються за значенням у JavaScript.


4
Додано посилання на статтю з цього питання. Схоже, виклик полягає в тому, як поводитися з об'єктом, який був точно таким же, як інший на той момент, коли він був доданий до набору, але тепер він був змінений і вже не такий, як цей об’єкт. Це в Setчи ні?
jfriend00

3
Чому б не реалізувати простий GetHashCode або подібне?
Джамбі

@Jamby - Це був би цікавий проект зробити хеш, який обробляє всі типи властивостей і хеширує властивості в потрібному порядку і має справу з круговими посиланнями тощо.
jfriend00

1
@Jamby Навіть з хеш-функцією вам все одно доведеться стикатися зі зіткненнями. Ви просто відкладаєте проблему рівності.
mpen

5
@mpen Це неправильно, я дозволяю розробнику керувати власною хеш-функцією для його конкретних класів, які майже у кожному випадку запобігають проблему зіткнення, оскільки розробник знає природу об'єктів і може отримати хороший ключ. У будь-якому іншому випадку, повернення до поточного методу порівняння. Лот з мов уже зробити це, JS немає.
Джембі

28

Як зазначено у відповіді jfriend00, налаштування відношення рівності, ймовірно, неможливо .

Наступний код представляє контур обчислювально ефективного (але пам'яті дорогого) рішення :

class GeneralSet {

    constructor() {
        this.map = new Map();
        this[Symbol.iterator] = this.values;
    }

    add(item) {
        this.map.set(item.toIdString(), item);
    }

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

    delete(item) {
        return this.map.delete(item.toIdString());
    }

    // ...
}

Кожен вставлений елемент повинен реалізувати toIdString()метод, який повертає рядок. Два об'єкти вважаються рівними тоді і тільки тоді, коли їхні toIdStringметоди повертають однакове значення.


Ви також можете змусити конструктора взяти функцію, яка порівнює елементи для рівності. Це добре, якщо ви хочете, щоб ця рівність була особливістю множини, а не об'єктів, які в ній використовуються.
Бен Дж

1
@BenJ Суть у створенні рядка та розміщенні його на карті полягає в тому, що таким чином ваш Javascript-движок буде використовувати пошук ~ O (1) у власному коді для пошуку хеш-значення вашого об'єкта, а прийняття функції рівності змусить зробити лінійну перевірку набору і перевірити кожен елемент.
Джамбі

3
Одна з проблем цього методу полягає в тому, що, на мою думку, він передбачає, що значення item.toIdString()інваріантне і воно не може змінитися. Тому що якщо це можливо, то GeneralSetможе легко стати недійсним із "дублюючими" елементами в ньому. Таким чином, таке рішення було б обмежене лише певними ситуаціями, ймовірно, коли самі об'єкти не змінюються під час використання набору або коли набір, який стає недійсним, не має наслідку. Усі ці питання, ймовірно, додатково пояснюють, чому набір ES6 не відкриває цю функціональність, оскільки він дійсно працює лише в певних обставинах.
jfriend00

Чи можна додати правильну реалізацію .delete()цієї відповіді?
jlewkovich

1
@JLewkovich впевнений
czerny

6

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

Ось ваш приклад використання незмінного js :

const { Map, Set } = require('immutable');
var set = new Set();
set = set.add(Map({a:1}));
set = set.add(Map({a:1}));
console.log([...set.values()]); // [Map {"a" => 1}]

10
Як порівняння продуктивності непорушного js Set / Map порівнюється з рідною програмою Set / Map?
франкстер

5

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

Передбачувано він виявився повільніше, ніж метод струнної конкатенації czerny .

Повне джерело тут: https://github.com/makoConstruct/ValueMap


"Рядкове з'єднання"? Чи не його метод більше схожий на "строкове сурогання" (якщо ви збираєтесь дати йому ім'я)? Або є причина, коли ви використовуєте слово "конкатенація"? Мені цікаво ;-)
binki

@binki Це гарне запитання, і я думаю, що відповідь підводить хороший момент, щоб зрозуміти, що мені знадобився певний час. Зазвичай під час обчислення хеш-коду робиться щось на зразок HashCodeBuilder, що множує хеш-коди окремих полів і не гарантується, що він унікальний (отже, необхідність користувацької функції рівності). Однак, створюючи рядок id, ви об'єднуєте рядки id окремих полів, які гарантовано є унікальними (і, таким чином, не потрібна функція рівності)
Pace

Отже, якщо у вас Pointвизначено, як { x: number, y: number }тоді id string, можливо, ваш x.toString() + ',' + y.toString().
Темп

Здійснення порівняння рівності будує певну цінність, яка гарантовано змінюється лише тоді, коли речі слід вважати нерівними - це стратегія, яку я використовував раніше. Іноді легше думати про речі. У цьому випадку ви генеруєте ключі, а не хеші . Поки у вас є ключовий дериватор, який виводить ключ у формі, яку існуючі інструменти підтримують із рівністю стильового значення, що майже завжди закінчується String, ви можете пропустити весь крок хешування та з’єднання, як ви сказали, і просто безпосередньо використовувати Mapабо навіть звичайний об’єкт старого стилю з точки зору похідного ключа.
бінкі

1
Слід бути обережним, якщо ви реально використовуєте конкатенацію рядків у своїй реалізації ключового дериватора, - це те, що властивості рядків, можливо, потребують особливої ​​обробки, якщо їм дозволяється приймати будь-яке значення. Наприклад, якщо у вас є {x: '1,2', y: '3'}і {x: '1', y: '2,3'}, то String(x) + ',' + String(y)буде виводити однакове значення для обох об'єктів. Більш безпечним варіантом, якщо припустити, що ви можете розраховувати на JSON.stringify()детермінованість, є скористатися можливістю його виходу з рядка та використовувати JSON.stringify([x, y])замість цього.
бінкі

3

Порівняння їх безпосередньо здається неможливим, але JSON.stringify працює, якщо ключі були просто відсортовані. Як я зазначив у коментарі

JSON.stringify ({a: 1, b: 2})! == JSON.stringify ({b: 2, a: 1});

Але ми можемо обійти це за допомогою спеціального методу stringify. Спочатку пишемо метод

Спеціальна строкованість

Object.prototype.stringifySorted = function(){
    let oldObj = this;
    let obj = (oldObj.length || oldObj.length === 0) ? [] : {};
    for (let key of Object.keys(this).sort((a, b) => a.localeCompare(b))) {
        let type = typeof (oldObj[key])
        if (type === 'object') {
            obj[key] = oldObj[key].stringifySorted();
        } else {
            obj[key] = oldObj[key];
        }
    }
    return JSON.stringify(obj);
}

Набір

Тепер ми використовуємо набір. Але ми використовуємо набір рядків замість об'єктів

let set = new Set()
set.add({a:1, b:2}.stringifySorted());

set.has({b:2, a:1}.stringifySorted());
// returns true

Отримайте всі значення

Після того, як ми створили набір і додали значення, ми можемо отримати всі значення за

let iterator = set.values();
let done = false;
while (!done) {
  let val = iterator.next();

  if (!done) {
    console.log(val.value);
  }
  done = val.done;
}

Ось посилання з усіма в одному файлі http://tpcg.io/FnJg2i


"якщо ключі відсортовані" є великим, якщо, особливо для складних об'єктів,
Олександр Міллс

саме тому я обрав такий підхід;)
relief.melone

2

Можливо, ви можете спробувати використовувати JSON.stringify()для глибокого порівняння об'єктів.

наприклад :

const arr = [
  {name:'a', value:10},
  {name:'a', value:20},
  {name:'a', value:20},
  {name:'b', value:30},
  {name:'b', value:40},
  {name:'b', value:40}
];

const names = new Set();
const result = arr.filter(item => !names.has(JSON.stringify(item)) ? names.add(JSON.stringify(item)) : false);

console.log(result);


2
Це може працювати, але не потрібно як JSON.stringify ({a: 1, b: 2})! == JSON.stringify ({b: 2, a: 1}) Якщо всі об'єкти створені вашою програмою в одній і тій же щоб ти в безпеці. Але взагалі не дуже безпечне рішення
relief.melone

1
Ага так, "перетворити його в рядок". Відповідь Javascript на все.
Timmmm

2

Для користувачів Typescript відповіді інших (особливо czerny ) можуть бути узагальнені до хорошого базового класу, безпечного для використання та багаторазового використання:

/**
 * Map that stringifies the key objects in order to leverage
 * the javascript native Map and preserve key uniqueness.
 */
abstract class StringifyingMap<K, V> {
    private map = new Map<string, V>();
    private keyMap = new Map<string, K>();

    has(key: K): boolean {
        let keyString = this.stringifyKey(key);
        return this.map.has(keyString);
    }
    get(key: K): V {
        let keyString = this.stringifyKey(key);
        return this.map.get(keyString);
    }
    set(key: K, value: V): StringifyingMap<K, V> {
        let keyString = this.stringifyKey(key);
        this.map.set(keyString, value);
        this.keyMap.set(keyString, key);
        return this;
    }

    /**
     * Puts new key/value if key is absent.
     * @param key key
     * @param defaultValue default value factory
     */
    putIfAbsent(key: K, defaultValue: () => V): boolean {
        if (!this.has(key)) {
            let value = defaultValue();
            this.set(key, value);
            return true;
        }
        return false;
    }

    keys(): IterableIterator<K> {
        return this.keyMap.values();
    }

    keyList(): K[] {
        return [...this.keys()];
    }

    delete(key: K): boolean {
        let keyString = this.stringifyKey(key);
        let flag = this.map.delete(keyString);
        this.keyMap.delete(keyString);
        return flag;
    }

    clear(): void {
        this.map.clear();
        this.keyMap.clear();
    }

    size(): number {
        return this.map.size;
    }

    /**
     * Turns the `key` object to a primitive `string` for the underlying `Map`
     * @param key key to be stringified
     */
    protected abstract stringifyKey(key: K): string;
}

Приклад реалізації тоді такий простий: просто перекрийте stringifyKeyметод. У моєму випадку я впорядкую деяку uriвластивість.

class MyMap extends StringifyingMap<MyKey, MyValue> {
    protected stringifyKey(key: MyKey): string {
        return key.uri.toString();
    }
}

Приклад використання - це так, ніби це було звичайним Map<K, V>.

const key1 = new MyKey(1);
const value1 = new MyValue(1);
const value2 = new MyValue(2);

const myMap = new MyMap();
myMap.set(key1, value1);
myMap.set(key1, value2); // native Map would put another key/value pair

myMap.size(); // returns 1, not 2

-1

Створіть новий набір із комбінації обох наборів, а потім порівняйте довжину.

let set1 = new Set([1, 2, 'a', 'b'])
let set2 = new Set([1, 'a', 'a', 2, 'b'])
let set4 = new Set([1, 2, 'a'])

function areSetsEqual(set1, set2) {
  const set3 = new Set([...set1], [...set2])
  return set3.size === set1.size && set3.size === set2.size
}

console.log('set1 equals set2 =', areSetsEqual(set1, set2))
console.log('set1 equals set4 =', areSetsEqual(set1, set4))

set1 дорівнює set2 = вірно

set1 дорівнює set4 = false


2
Чи пов’язана ця відповідь з питанням? Питання стосується рівності елементів стосовно екземпляра класу Set. Це питання, схоже, обговорює рівність двох встановлених випадків.
czerny

@czerny ви правильно - я був спочатку перегляд StackOverflow питання, де вище метод може бути використаний: stackoverflow.com/questions/6229197 / ...
Стефан Musarra

-2

Користувачеві, який знайшов це запитання в Google (як я), бажаючи отримати значення Карти за допомогою об’єкта як ключа:

Увага: ця відповідь працюватиме не з усіма об'єктами

var map = new Map<string,string>();

map.set(JSON.stringify({"A":2} /*string of object as key*/), "Worked");

console.log(map.get(JSON.stringify({"A":2}))||"Not worked");

Вихід:

Працювали

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