JavaScript Kümesi için nesne eşitliği nasıl özelleştirilir


167

Yeni ES 6 (Uyum) yeni Set nesnesini sunar. Set tarafından kullanılan kimlik algoritması ===operatöre benzer ve nesneleri karşılaştırmak için çok uygun değildir:

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

Derin nesne karşılaştırması yapmak için Set nesneleri için eşitlik nasıl özelleştirilir? Java gibi bir şey var mı equals(Object)?


3
"Eşitliği özelleştir" ile ne demek istiyorsun? Javascript, operatörün aşırı yüklenmesine izin vermediğinden, operatörü aşırı yüklemenin bir yolu yoktur ===. ES6 set nesnesinin karşılaştırma yöntemi yoktur. .has()Yöntem ve .add()sadece ilkel aynı gerçek nesne ya da aynı değerini kapalı yöntem çalışması.
jfriend00

12
"Eşitliği özelleştir" ile, geliştiricinin eşit kabul edilecek ya da edilmeyecek bazı nesneleri nasıl tanımlayabileceği anlamına gelir.
czerny

Yanıtlar:


107

ES6 Setnesnesinin karşılaştırma yöntemleri veya özel karşılaştırma genişletilebilirliği yoktur.

.has(), .add()Ve .delete()yöntemler sadece ilkel aynı gerçek nesne ya da aynı değerini kapalı çalışma içine fişi için bir araç ya da sadece mantığı yerine geçemez.

Sen muhtemelen bir kendi nesne elde edebileceğini Setve değiştirme .has(), .add()ve .delete()madde Set zaten mevcutsa derin nesne karşılaştırma yaptı şeyle yöntemleri ilk bulmaya, ancak altta yatan beri performans olasılıkla iyi olmaz Setnesne yardımcı olmaz hiç. Muhtemelen orijinali çağırmadan önce kendi özel karşılaştırmanızı kullanarak bir eşleşme bulmak için mevcut tüm nesneler boyunca kaba bir kuvvet yinelemesi yapmanız gerekir .add().

Bu makaleden bazı bilgiler ve ES6 özelliklerinin tartışılması :

5.2 Haritaların ve kümelerin anahtarları ve değerleri karşılaştırma şeklini neden yapılandıramıyorum?

Soru: Hangi harita anahtarlarını ve hangi set öğelerinin eşit kabul edileceğini yapılandırmanın bir yolu olsaydı iyi olurdu. Neden yok?

Yanıt: Düzgün ve verimli bir şekilde uygulanması zor olduğu için bu özellik ertelendi. Seçeneklerden biri, eşitliği belirleyen koleksiyonlara geri arama yapmaktır.

Java'da kullanılabilen başka bir seçenek de nesnenin uyguladığı bir yöntemle eşitliği belirtmektir (Java'da () eşittir). Ancak, bu yaklaşım değiştirilebilir nesneler için sorunludur: Genel olarak, bir nesne değişirse, koleksiyon içindeki “konumu” da değişmelidir. Ama Java'da olan bu değil. JavaScript büyük olasılıkla yalnızca özel sabit nesneler (değer nesneleri olarak adlandırılan) için değere göre karşılaştırmayı etkinleştirmek için daha güvenli bir yol izleyecektir. Değere göre karşılaştırma, içerikleri eşitse iki değerin eşit kabul edildiği anlamına gelir. İlkel değerler JavaScript'teki değerle karşılaştırılır.


4
Bu sorunla ilgili makale referansı eklendi. Zorluk, sete eklendiğinde başka biriyle tamamen aynı olan, ancak şimdi değiştirilen ve artık o nesne ile aynı olmayan bir nesneyle nasıl başa çıkılacağı gibi görünüyor. İçinde Setmi , değil mi?
jfriend00

3
Neden basit bir GetHashCode veya benzeri bir uygulama yapmıyorsunuz?
Jamby

@Jamby - Bu, her tür özelliği ve hash özelliklerini doğru sırada işleyen ve dairesel referanslar vb. İle ilgilenen bir karma yapmak ilginç bir proje olacaktır.
jfriend00

1
@ Jamby Bir karma işleviyle bile, hala çarpışmalarla uğraşmak zorundasınız. Sadece eşitlik sorununu erteliyorsun.
mpen

5
@mpen Bu doğru değil, geliştiricinin kendi sınıfları için kendi hash işlevini yönetmesine izin veriyorum, çünkü geliştirici nesnelerin doğasını biliyor ve iyi bir anahtar türetebildiğinden hemen hemen her durumda çarpışma problemini önlüyor. Başka bir durumda, geçerli karşılaştırma yöntemine geri dönüş. Çok ait dilleri zaten , js o yok.
Jamby

28

Jfriend00'ün cevabında belirtildiği gibi , eşitlik ilişkisinin özelleştirilmesi muhtemelen mümkün değildir .

Aşağıdaki kod, hesaplama açısından verimli (ancak bellek açısından pahalı) bir geçici çözümün ana hatlarını sunar :

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

    // ...
}

Eklenen her öğenin toIdString()dize döndüren yöntemi uygulaması gerekir . İki nesne, yalnızca toIdStringyöntemleri aynı değeri döndürürse eşit kabul edilir .


Ayrıca, yapıcıya öğeleri eşitlikle karşılaştıran bir işlev almasını da sağlayabilirsiniz. Bu eşitliğin, içinde kullanılan nesneler yerine kümenin bir özelliği olmasını istiyorsanız, bu iyidir.
Ben J

1
@BenJ Bir dize oluşturma ve bir Haritaya koyma noktası, Javascript motorunuzun, bir eşitlik işlevini kabul ederken, nesnenizin karma değerini aramak için yerel kodda bir ~ O (1) araması kullanmasıdır. setin doğrusal olarak taranması ve her elemanın kontrol edilmesi.
Jamby

3
Bu yöntemle ilgili bir zorluk, bence, değerinin item.toIdString()değişmez olduğunu ve değişemeyeceğini varsayar . Çünkü yapabiliyorsa, içindeki GeneralSet"yinelenen" öğelerle kolayca geçersiz hale gelebilir. Dolayısıyla, bunun gibi bir çözüm sadece set kullanılırken nesnelerin kendilerinin değişmediği veya geçersiz hale gelen bir setin sonuç olmadığı belirli durumlarla sınırlı olacaktır. Tüm bu sorunlar, muhtemelen ES6 Set'in neden bu işlevi göstermediğini açıklamaktadır çünkü gerçekten sadece belirli durumlarda çalışır.
jfriend00

.delete()Bu cevaba doğru uygulanmasını eklemek mümkün müdür ?
jlewkovich

1
@JLewkovich sure
czerny

6

En iyi yanıttan bahsedildiği gibi, eşitliği özelleştirmek değişebilir nesneler için sorunludur. İyi haber şu ki (ve henüz kimsenin bundan bahsetmediğine şaşırdım) , aradığınız derin değer eşitliği anlambilimini sağlayan zengin bir dizi değişmez tür sağlayan immutable-js adlı çok popüler bir kütüphane var .

İmmutable-js kullanarak örnek :

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
İmmutable-js Set / Map'in performansı yerel Set / Map ile nasıl karşılaştırılır?
frankster

5

Buradaki yanıtlara eklemek için, devam ettim ve özel bir karma işlevi, özel bir eşitlik işlevi alan ve eşdeğer (özel) karmaları olan farklı değerleri kovalarda saklayan bir Harita sarmalayıcısı uyguladım.

Tahmin edileceği gibi, czerny'nin string birleştirme yönteminden daha yavaş olduğu ortaya çıktı .

Tam kaynak burada: https://github.com/makoConstruct/ValueMap


“Dize birleştirme”? Yöntemi daha çok “string surrogating” gibi değil mi? Yoksa “birleştirme” kelimesini kullanmanız için bir neden var mı? Merak ediyorum ;-)
binki

@binki Bu iyi bir soru ve bence cevap, kavramamın biraz zaman aldığı iyi bir noktaya geldi. Tipik olarak, bir karma kodu hesaplanırken, HashCodeBuilder gibi , tek tek alanların karma kodlarını çoğaltan ve benzersiz olması garanti edilmeyen bir şey yapar (bu nedenle özel bir eşitlik fonksiyonuna ihtiyaç vardır). İd dizesi oluştururken Ancak, benzersiz olması garanti (ve dolayısıyla hiçbir eşitlik işlevi gerekli) IS bireysel alanların kimliği dizeleri bitiştirmek
Pace

Eğer bir varsa Yani Pointolarak tanımlanan { x: number, y: number }sonra senin id stringmuhtemelen x.toString() + ',' + y.toString().
Pace

Eşitlik karşılaştırmanızın, yalnızca işler eşit olmadığı düşünüleceği zaman değişeceği garanti edilen bir değer inşa etmek, daha önce kullandığım bir stratejidir. Bazen işleri bu şekilde düşünmek daha kolay. Bu durumda, karmalar yerine anahtarlar üretiyorsunuz . Mevcut araçların değer stili eşitliği ile desteklediği bir formda bir anahtar çıkaran bir anahtar türeviniz olduğu sürece, neredeyse her zaman sonuçlanır , o zaman tüm karma ve kovalama adımını atladığınız gibi atlayabilir ve doğrudan bir hatta türetilmiş anahtar açısından eski stil düz nesne. StringMap
binki

1
Bir anahtar türetici uygulamanızda aslında dize birleştirme kullanıyorsanız dikkatli olmanız gereken bir şey, dize özelliklerinin herhangi bir değer almasına izin verilirse özel olarak ele alınması gerekebilir. Örneğin, varsa {x: '1,2', y: '3'}ve {x: '1', y: '2,3'}daha sonra, String(x) + ',' + String(y)her iki nesneler için çıktısı aynı değeri. JSON.stringify()Deterministik olmaya güvenebileceğiniz varsayıldığında daha güvenli bir seçenek, dizeden kaçmak ve JSON.stringify([x, y])bunun yerine kullanmaktır .
binki

3

Bunları doğrudan karşılaştırmak mümkün görünmüyor, ancak JSON.stringify anahtarlar sadece sıralanmışsa çalışır. Bir yorumda belirttiğim gibi

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

Ancak özel bir stringify yöntemiyle bu sorunu çözebiliriz. İlk önce yöntemi yazıyoruz

Özel 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);
}

Set

Şimdi bir Set kullanıyoruz. Ama nesneler yerine bir dizi dizgi kullanıyoruz

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

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

Tüm değerleri alın

Kümeyi oluşturduktan ve değerleri ekledikten sonra, tüm değerleri

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

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

İşte hepsi bir dosyada bir bağlantı http://tpcg.io/FnJg2i


"Anahtarlar sıralanırsa", özellikle karmaşık nesneler için büyükse
Alexander Mills

tam da bu yüzden bu yaklaşımı seçtim;)
rölyef.melone

2

Belki JSON.stringify()derin nesne karşılaştırması yapmak için kullanmayı deneyebilirsiniz .

Örneğin :

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
Bu işe yarayabilir, ancak JSON.stringify ({a: 1, b: 2})! == JSON.stringify ({b: 2, a: 1}) gibi tüm programların aynı şekilde oluşturulmuş olması gerekmez. güvende ol. Ama genel olarak gerçekten güvenli bir çözüm değil
relief.melone

1
Ah evet, "bir dizeye dönüştür". Her şey için Javascript'in cevabı.
Timmmm

2

Typescript kullanıcıları için, başkalarının cevapları (özellikle czerny ) güzel tipte güvenli ve tekrar kullanılabilir bir temel sınıfa genelleştirilebilir:

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

Örnek uygulama bu kadar basittir: sadece stringifyKeyyöntemi geçersiz kılın . Benim durumumda bazı uriözellikleri dize .

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

Örnek kullanım o zaman bu normal bir durumdur 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

Her iki kümenin birleşiminden yeni bir küme oluşturun, ardından uzunluğu karşılaştırın.

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 eşittir set2 = true

set1 eşittir set4 = yanlış


2
Bu cevap soru ile ilgili mi? Soru, Set sınıfının bir örneğine göre öğelerin eşitliği ile ilgilidir. Bu soru iki Set örneğinin eşitliğini tartışıyor gibi görünüyor.
czerny

@czerny haklısın - başlangıçta yukarıdaki yöntemin kullanılabileceği bu
yığın akışı sorusunu görüntülüyordum

-2

Bu soruyu Google'da (benim gibi) Anahtar olarak bir nesne kullanarak bir Harita değeri almak isteyen birine:

Uyarı: bu cevap tüm nesnelerle çalışmaz

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

Çıktı:

işlenmiş

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.