Jak dostosować równość obiektów dla zestawu JavaScript


167

Nowy ES 6 (Harmony) wprowadza nowy obiekt Set . Algorytm tożsamości używany przez Set jest podobny do ===operatora, więc nie nadaje się zbytnio do porównywania obiektów:

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

Jak dostosować równość dla obiektów Set, aby przeprowadzić głębokie porównanie obiektów? Czy jest coś takiego jak Java equals(Object)?


3
Co rozumiesz przez „dostosowywanie równości”? JavaScript nie zezwala na przeciążanie operatora, więc nie ma możliwości przeciążenia ===operatora. Obiekt set ES6 nie ma żadnych metod porównawczych. .has()Metoda i .add()sposób nowy utwór tylko na off to jest to samo rzeczywisty przedmiot lub sama wartość dla prymitywne.
jfriend00

12
Przez „dostosowuj równość” rozumiem każdy sposób, w jaki programista może zdefiniować kilka obiektów, które mają być traktowane jako równe lub nie.
czerny

Odpowiedzi:


107

Obiekt ES6 Setnie ma żadnych metod porównywania ani niestandardowej rozszerzalności porównywania.

Metody .has(), .add()i .delete()działają tylko wtedy, gdy są tym samym rzeczywistym obiektem lub tą samą wartością dla prymitywu i nie mają środków do podłączenia lub zastąpienia tylko tej logiki.

Można przypuszczalnie wyprowadzić własny obiekt z a Seti replace .has(), .add()a także .delete()metody z czymś, co najpierw wykonało głębokie porównanie obiektów, aby dowiedzieć się, czy element jest już w zestawie, ale wydajność prawdopodobnie nie byłaby dobra, ponieważ podstawowy Setobiekt nie pomagałby w ogóle. Prawdopodobnie musiałbyś po prostu wykonać iterację brutalnej siły przez wszystkie istniejące obiekty, aby znaleźć dopasowanie za pomocą własnego niestandardowego porównania przed wywołaniem oryginału .add().

Oto kilka informacji z tego artykułu i omówienie funkcji ES6:

5.2 Dlaczego nie mogę skonfigurować sposobu, w jaki mapy i zestawy porównują klucze i wartości?

Pytanie: Byłoby miło, gdyby istniał sposób na skonfigurowanie, które klucze mapy i które elementy zestawu są uważane za równe. Dlaczego nie ma?

Odpowiedź: Ta funkcja została odłożona, ponieważ trudno jest ją prawidłowo i wydajnie zaimplementować. Jedną z opcji jest przekazanie wywołań zwrotnych do kolekcji, które określają równość.

Inną opcją dostępną w Javie jest określenie równości za pomocą metody implementowanej przez obiekt (equals () w Javie). Jednak takie podejście jest problematyczne w przypadku obiektów podlegających zmianom: ogólnie rzecz biorąc, jeśli obiekt się zmieni, jego „lokalizacja” w kolekcji również musi się zmienić. Ale to nie jest to, co dzieje się w Javie. JavaScript prawdopodobnie pójdzie bezpieczniejszą drogą, umożliwiając porównywanie tylko według wartości dla specjalnych niezmiennych obiektów (tak zwanych obiektów wartości). Porównanie według wartości oznacza, że ​​dwie wartości są uważane za równe, jeśli ich zawartość jest równa. Wartości pierwotne są porównywane według wartości w JavaScript.


4
Dodano informację o artykule na temat tego konkretnego problemu. Wygląda na to, że wyzwanie polega na tym, jak poradzić sobie z obiektem, który był dokładnie taki sam jak inny w momencie dodania go do zestawu, ale został teraz zmieniony i nie jest już tym samym obiektem. Czy to jest w środku, Setczy nie?
jfriend00

3
Dlaczego nie zaimplementować prostego GetHashCode lub podobnego?
Jamby

@Jamby - To byłby interesujący projekt, aby utworzyć skrót, który obsługuje wszystkie typy właściwości i haszuje właściwości we właściwej kolejności, a także zajmuje się odwołaniami cyklicznymi i tak dalej.
jfriend00

1
@Jamby Nawet z funkcją skrótu nadal musisz radzić sobie z kolizjami. Po prostu odkładasz problem równości.
mpen

5
@mpen To nie w porządku, pozwalam programistom zarządzać własną funkcją skrótu dla jego określonych klas, co prawie w każdym przypadku zapobiega problemowi kolizji, ponieważ programista zna naturę obiektów i może uzyskać dobry klucz. W każdym innym przypadku skorzystaj z aktualnej metody porównania. Lot z języków już zrobić, JS nie.
Jamby

28

Jak wspomniano w odpowiedzi jfriend00, dostosowanie relacji równości prawdopodobnie nie jest możliwe .

Poniższy kod przedstawia zarys wydajnego obliczeniowo (ale kosztownego w pamięci) obejścia :

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());
    }

    // ...
}

Każdy wstawiany element musi zaimplementować toIdString()metodę, która zwraca ciąg. Dwa obiekty są uważane za równe wtedy i tylko wtedy, gdy ich toIdStringmetody zwracają tę samą wartość.


Możesz również użyć konstruktora do wykonania funkcji porównującej elementy pod kątem równości. Jest to dobre, jeśli chcesz, aby ta równość była cechą zestawu, a nie obiektów w nim używanych.
Ben J

1
@BenJ Celem generowania ciągu znaków i umieszczania go w mapie jest to, że w ten sposób twój silnik Javascript będzie używał wyszukiwania ~ O (1) w kodzie natywnym do wyszukiwania wartości skrótu twojego obiektu, podczas gdy zaakceptowanie funkcji równości wymusi wykonać liniowe skanowanie zestawu i sprawdzić każdy element.
Jamby

3
Jednym z wyzwań związanych z tą metodą jest to, że moim zdaniem zakłada ona, że ​​wartość item.toIdString()jest niezmienna i nie może się zmienić. Bo jeśli może, to GeneralSetłatwo może stać się nieważne z „zduplikowanymi” elementami. Tak więc takie rozwiązanie byłoby ograniczone tylko do pewnych sytuacji, w których same obiekty nie są zmieniane podczas używania zestawu lub gdy zbiór, który staje się nieważny, nie ma znaczenia. Wszystkie te kwestie prawdopodobnie dodatkowo wyjaśniają, dlaczego zestaw ES6 nie udostępnia tej funkcji, ponieważ działa ona tylko w określonych okolicznościach.
jfriend00

Czy można dodać poprawną implementację .delete()do tej odpowiedzi?
jlewkovich

1
@JLewkovich sure
czerny

6

Jak wspomniano w pierwszej odpowiedzi , dostosowywanie równości jest problematyczne w przypadku obiektów zmiennych. Dobrą wiadomością jest to, że (i jestem zaskoczony, że nikt jeszcze o tym nie wspomniał) istnieje bardzo popularna biblioteka o nazwie immutable-js, która zapewnia bogaty zestaw niezmiennych typów, które zapewniają głęboką semantykę równości wartości , której szukasz.

Oto przykład użycia immutable-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
Jak wypada wydajność immutable-js Set / Map w porównaniu z natywnym Set / Map?
frankster

5

Aby dodać do odpowiedzi tutaj, poszedłem dalej i zaimplementowałem otokę mapy, która przyjmuje niestandardową funkcję skrótu, niestandardową funkcję równości i przechowuje różne wartości, które mają równoważne (niestandardowe) skróty w zasobnikach.

Jak można się było spodziewać , okazał się wolniejszy niż metoda konkatenacji strun czernego .

Pełne źródło tutaj: https://github.com/makoConstruct/ValueMap


„Konkatenacja ciągów”? Czy jego metoda nie przypomina bardziej „zastępowania ciągów znaków” (jeśli zamierzasz nadać jej nazwę)? A może jest powód, dla którego używasz słowa „konkatenacja”? Jestem ciekawy ;-)
binki

@binki To dobre pytanie i myślę, że odpowiedź prowadzi do dobrego punktu, którego zrozumienie zajęło mi trochę czasu. Zwykle podczas obliczania kodu skrótu wykonuje się coś w rodzaju HashCodeBuilder, który mnoży kody skrótów poszczególnych pól i nie gwarantuje, że będzie on niepowtarzalny (stąd potrzeba niestandardowej funkcji równości). Jednak podczas generowania ciągu identyfikatora łączysz ciągi identyfikatorów poszczególnych pól, które JEST gwarantowane jako unikalne (a zatem nie jest wymagana funkcja równości)
Pace

Więc jeśli masz Pointzdefiniowane jako { x: number, y: number }wówczas id stringprawdopodobnie x.toString() + ',' + y.toString().
Tempo

Dokonywanie porównania równości buduje pewną wartość, która z pewnością będzie się zmieniać tylko wtedy, gdy rzeczy należy uznać za nierówne, jest strategią, której używałem wcześniej. Czasami łatwiej o tym myśleć w ten sposób. W takim przypadku generujesz klucze, a nie skróty . Tak długo, jak masz pochodną klucza, która generuje klucz w formie, którą istniejące narzędzia obsługują z równością w stylu wartości, co prawie zawsze kończy się byciem String, możesz pominąć cały krok haszowania i grupowania, jak powiedziałeś i po prostu bezpośrednio użyj Maplub nawet zwykły obiekt w starym stylu pod względem klucza pochodnego.
binki

1
Jedną rzeczą, na którą należy uważać, jeśli faktycznie używasz konkatenacji ciągów w swojej implementacji pochodnej klucza, jest to, że właściwości łańcuchów mogą wymagać specjalnego traktowania, jeśli wolno im przyjmować jakąkolwiek wartość. Na przykład, jeśli masz {x: '1,2', y: '3'}i {x: '1', y: '2,3'}, to String(x) + ',' + String(y)wyświetli tę samą wartość dla obu obiektów. Bezpieczniejszą opcją, zakładając, że możesz liczyć na JSON.stringify()bycie deterministą, jest skorzystanie z funkcji ucieczki ciągu i użycie JSON.stringify([x, y])zamiast tego.
binki

3

Bezpośrednie porównanie ich wydaje się niemożliwe, ale JSON.stringify działa, jeśli klucze zostały właśnie posortowane. Jak wskazałem w komentarzu

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

Ale możemy to obejść za pomocą niestandardowej metody stringify. Najpierw piszemy metodę

Niestandardowe 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);
}

Zestaw

Teraz używamy zestawu. Ale zamiast obiektów używamy zestawu ciągów

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

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

Uzyskaj wszystkie wartości

Po utworzeniu zestawu i dodaniu wartości, możemy uzyskać wszystkie wartości według

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

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

Oto link ze wszystkim w jednym pliku http://tpcg.io/FnJg2i


„jeśli klucze są posortowane” to duże „jeśli”, zwłaszcza w przypadku złożonych obiektów
Alexander Mills,

właśnie dlatego wybrałem takie podejście;)
relief.melone

2

Może możesz spróbować użyć JSON.stringify()do przeprowadzenia głębokiego porównania obiektów.

na przykład :

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
To może działać, ale nie musi, ponieważ JSON.stringify ({a: 1, b: 2})! == JSON.stringify ({b: 2, a: 1}) Jeśli wszystkie obiekty są tworzone przez Twój program w tym samym porządek jesteś bezpieczny. Ale ogólnie nie jest to naprawdę bezpieczne rozwiązanie
ulga. Melone

1
Ach tak, „zamień to na ciąg”. Odpowiedź JavaScript na wszystko.
Timmmm

2

W przypadku użytkowników maszynopisu odpowiedzi innych osób (zwłaszcza czerny ) można uogólnić na ładną, bezpieczną dla typów i wielokrotnego użytku klasę bazową:

/**
 * 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;
}

Przykładowa implementacja jest więc taka prosta: po prostu zastąp stringifyKeymetodę. W moim przypadku określam pewną uriwłasność.

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

Przykładowe użycie jest wtedy tak, jakby to było zwykłe 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

Utwórz nowy zestaw z połączenia obu zestawów, a następnie porównaj długość.

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))

zestaw1 równa się zestaw2 = prawda

set1 równa się set4 = false


2
Czy ta odpowiedź jest związana z pytaniem? Pytanie dotyczy równości przedmiotów w odniesieniu do instancji klasy Set. Wydaje się, że to pytanie dotyczy równości dwóch instancji zbioru.
czerny

@czerny, masz rację - pierwotnie przeglądałem to pytanie stackoverflow, w którym można zastosować powyższą metodę: stackoverflow.com/questions/6229197/ ...
Stefan Musarra

-2

Do kogoś, kto znalazł to pytanie w Google (tak jak ja), chcąc uzyskać wartość mapy przy użyciu obiektu jako klucza:

Ostrzeżenie: ta odpowiedź nie będzie działać dla wszystkich obiektów

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");

Wynik:

Pracował

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.