Cara menyesuaikan kesetaraan objek untuk Kumpulan JavaScript

167

ES 6 Baru (Harmoni) memperkenalkan objek Set baru . Algoritma identitas yang digunakan oleh Set mirip dengan ===operator sehingga tidak terlalu cocok untuk membandingkan objek:

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

Bagaimana cara menyesuaikan kesetaraan untuk objek Set untuk melakukan perbandingan objek yang mendalam? Apakah ada yang seperti Java equals(Object)?

czerny
sumber
3
Apa yang Anda maksud dengan "sesuaikan kesetaraan"? Javascript tidak memungkinkan operator overloading sehingga tidak ada cara untuk membebani ===operator. Objek set ES6 tidak memiliki metode perbandingan apa pun. The .has()Metode dan .add()metode kerja hanya off itu menjadi objek yang sebenarnya sama atau nilai yang sama untuk primitif.
jfriend00
12
Dengan "menyesuaikan kesetaraan" Maksud saya bagaimana pengembang dapat mendefinisikan beberapa objek tertentu untuk dianggap sama atau tidak.
czerny

Jawaban:

107

SetObjek ES6 tidak memiliki metode perbandingan apa pun atau kustom membandingkan ekstensibilitas.

Metode .has(), .add()dan .delete()metode hanya berfungsi sebagai objek aktual yang sama atau nilai yang sama untuk primitif dan tidak memiliki sarana untuk menyambungkan atau mengganti hanya logika itu.

Anda mungkin bisa mendapatkan objek Anda sendiri dari a Setdan mengganti .has(), .add()dan .delete()metode dengan sesuatu yang melakukan perbandingan objek dalam terlebih dahulu untuk menemukan apakah item tersebut sudah di Set, tetapi kinerjanya kemungkinan tidak akan baik karena Setobjek yang mendasarinya tidak akan membantu sama sekali. Anda mungkin harus melakukan iterasi kasar melalui semua objek yang ada untuk menemukan kecocokan menggunakan perbandingan kustom Anda sendiri sebelum memanggil yang asli .add().

Berikut beberapa info dari artikel ini dan diskusi tentang fitur ES6:

5.2 Mengapa saya tidak bisa mengonfigurasi bagaimana peta dan set membandingkan kunci dan nilai?

Pertanyaan: Alangkah baiknya jika ada cara untuk mengonfigurasi kunci peta apa dan elemen set apa yang dianggap sama. Kenapa tidak ada?

Jawab: Fitur itu telah ditunda, karena sulit untuk diimplementasikan dengan baik dan efisien. Salah satu opsi adalah menyerahkan panggilan balik ke koleksi yang menentukan kesetaraan.

Pilihan lain, tersedia di Jawa, adalah untuk menentukan kesetaraan melalui metode yang objek implementasikan (equals () di Java). Namun, pendekatan ini bermasalah untuk objek yang bisa berubah: Secara umum, jika suatu objek berubah, "lokasi" di dalam koleksi harus berubah juga. Tapi bukan itu yang terjadi di Jawa. JavaScript mungkin akan menempuh rute yang lebih aman dengan hanya memungkinkan perbandingan berdasarkan nilai untuk objek tidak berubah khusus (yang disebut objek nilai). Perbandingan dengan nilai berarti bahwa dua nilai dianggap sama jika isinya sama. Nilai-nilai primitif dibandingkan dengan nilai dalam JavaScript.

pacar00
sumber
4
Referensi artikel ditambahkan tentang masalah khusus ini. Sepertinya tantangannya adalah bagaimana berurusan dengan objek yang sama persis dengan yang lain pada saat ditambahkan ke set, tetapi sekarang telah diubah dan tidak lagi sama dengan objek itu. Apakah di dalam Setatau tidak?
jfriend00
3
Mengapa tidak menerapkan GetHashCode sederhana atau serupa?
Jamby
@Jamby - Itu akan menjadi proyek yang menarik untuk membuat hash yang menangani semua jenis properti dan hash properti dalam urutan yang benar dan berkaitan dengan referensi melingkar dan sebagainya.
jfriend00
1
@Jamby Bahkan dengan fungsi hash Anda masih harus berurusan dengan tabrakan. Anda hanya menunda masalah kesetaraan.
buka
5
@mpen Itu tidak benar, saya mengizinkan pengembang untuk mengelola fungsi hash sendiri untuk kelas spesifiknya yang dalam hampir setiap kasus mencegah masalah tabrakan karena pengembang tahu sifat objek dan dapat memperoleh kunci yang baik. Dalam kasus lain, mundur ke metode perbandingan saat ini. Banyak dari bahasa sudah melakukan itu, js tidak.
Jamby
28

Seperti yang disebutkan dalam jawaban jfriend00, kustomisasi relasi kesetaraan mungkin tidak mungkin .

Berikut kode hadiah garis besar komputasi yang efisien (tapi memori mahal) solusi :

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

    // ...
}

Setiap elemen yang dimasukkan harus menerapkan toIdString()metode yang mengembalikan string. Dua objek dianggap sama jika dan hanya jika toIdStringmetode mereka mengembalikan nilai yang sama.

czerny
sumber
Anda juga bisa meminta konstruktor mengambil fungsi yang membandingkan item untuk kesetaraan. Ini bagus jika Anda ingin kesetaraan ini menjadi fitur set, bukan objek yang digunakan di dalamnya.
Ben J
1
@ BENJ Titik menghasilkan string dan meletakkannya di Peta adalah bahwa dengan cara itu mesin Javascript Anda akan menggunakan pencarian ~ O (1) dalam kode asli untuk mencari nilai hash objek Anda, sementara menerima fungsi kesetaraan akan memaksa untuk melakukan pemindaian linear dari set dan memeriksa setiap elemen.
Jamby
3
Salah satu tantangan dengan metode ini adalah bahwa saya pikir itu mengasumsikan bahwa nilai item.toIdString()invarian dan tidak dapat berubah. Karena jika bisa, maka GeneralSetdapat dengan mudah menjadi tidak valid dengan item "duplikat" di dalamnya. Jadi, solusi seperti itu akan dibatasi hanya pada situasi tertentu di mana objek itu sendiri tidak berubah saat menggunakan set atau di mana set yang menjadi tidak valid tidak ada konsekuensinya. Semua masalah ini mungkin lebih jauh menjelaskan mengapa Perangkat ES6 tidak memaparkan fungsi ini karena hanya berfungsi dalam keadaan tertentu.
jfriend00
Apakah mungkin untuk menambahkan implementasi yang benar .delete()untuk jawaban ini?
jlewkovich
1
@JLewkovich yakin
czerny
6

Seperti yang dijawab oleh jawaban teratas , menyesuaikan kesetaraan merupakan masalah untuk objek yang dapat diubah. Berita baiknya adalah (dan saya terkejut belum ada yang menyebutkan ini) ada perpustakaan yang sangat populer yang disebut immutable-js yang menyediakan serangkaian tipe abadi yang memberikan semantik kesetaraan nilai mendalam yang Anda cari.

Inilah contoh Anda menggunakan 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}]
Russell Davis
sumber
10
Bagaimana kinerja Set / Map immutable-js dibandingkan dengan Set / Map asli?
Frankrank
5

Untuk menambah jawaban di sini, saya melanjutkan dan mengimplementasikan pembungkus Peta yang menggunakan fungsi hash khusus, fungsi kesetaraan khusus, dan menyimpan nilai berbeda yang memiliki hash (kustom) setara dalam ember.

Bisa ditebak, ternyata lebih lambat daripada metode penggabungan string czerny .

Sumber lengkap di sini: https://github.com/makoConstruct/ValueMap

mako
sumber
"String concatenation"? Bukankah metodenya lebih seperti "string surrogating" (jika Anda akan memberikan nama)? Atau adakah alasan Anda menggunakan kata "concatenation"? Saya ingin tahu ;-)
binki
@binki Ini adalah pertanyaan yang bagus dan saya pikir jawabannya memunculkan poin yang baik sehingga saya perlu waktu untuk memahami. Biasanya, ketika menghitung kode hash, seseorang melakukan sesuatu seperti HashCodeBuilder yang mengalikan kode hash dari masing-masing bidang dan tidak dijamin unik (karenanya diperlukan fungsi kesetaraan khusus). Namun, saat membuat string id, Anda menggabungkan string id dari masing-masing bidang yang dijamin unik (dan karenanya tidak diperlukan fungsi kesetaraan)
Laju
Jadi jika Anda memiliki Pointdefinisi { x: number, y: number }maka Anda id stringmungkin x.toString() + ',' + y.toString().
Laju
Membuat perbandingan kesetaraan Anda membangun beberapa nilai yang dijamin bervariasi hanya ketika hal-hal yang dianggap tidak setara adalah strategi yang telah saya gunakan sebelumnya. Terkadang lebih mudah untuk memikirkan hal-hal seperti itu. Dalam hal ini, Anda menghasilkan kunci daripada hash . Selama Anda memiliki deriver kunci yang menampilkan kunci dalam bentuk yang didukung oleh alat yang ada dengan kesetaraan gaya nilai, yang hampir selalu berakhir String, maka Anda dapat melewati seluruh langkah hashing dan bucket seperti yang Anda katakan dan langsung saja menggunakan Mapatau bahkan objek biasa gaya lama dalam hal kunci yang diturunkan.
binki
1
Satu hal yang perlu diperhatikan jika Anda benar-benar menggunakan penggabungan string dalam implementasi Anda terhadap kunci deriver adalah bahwa properti string mungkin perlu diperlakukan secara khusus jika mereka diizinkan untuk mengambil nilai apa pun. Misalnya, jika Anda memiliki {x: '1,2', y: '3'}dan {x: '1', y: '2,3'}, maka String(x) + ',' + String(y)akan menampilkan nilai yang sama untuk kedua objek. Opsi yang lebih aman, dengan asumsi Anda dapat mengandalkan JSON.stringify()deterministik, adalah mengambil keuntungan dari string yang keluar dan digunakan JSON.stringify([x, y])sebagai gantinya.
binki
3

Membandingkannya secara langsung tampaknya tidak mungkin, tetapi JSON.stringify berfungsi jika kunci hanya diurutkan. Seperti yang saya tunjukkan dalam komentar

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

Tetapi kita dapat mengatasinya dengan metode stringifikasi khusus. Pertama kita menulis metodenya

Stringify Kustom

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

Sekarang kita menggunakan Set. Tapi kami menggunakan Set of Strings sebagai ganti objek

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

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

Dapatkan semua nilainya

Setelah kami membuat set dan menambahkan nilai, kami bisa mendapatkan semua nilai dengan

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

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

Berikut ini tautan dengan semua dalam satu file http://tpcg.io/FnJg2i

relief.melone
sumber
"Jika kunci diurutkan" adalah besar jika, terutama untuk benda
Alexander Mills
itulah sebabnya saya memilih pendekatan ini;)
relief.melone
2

Mungkin Anda bisa mencoba menggunakan JSON.stringify()untuk melakukan perbandingan objek yang dalam.

sebagai contoh :

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

GuaHsu
sumber
2
Ini bisa berfungsi tetapi tidak harus seperti JSON.stringify ({a: 1, b: 2})! == JSON.stringify ({b: 2, a: 1}) Jika semua objek dibuat oleh program Anda di memesan Anda aman. Tapi bukan solusi yang benar-benar aman secara umum
relief.melone
1
Ah ya, "ubah menjadi string". Jawaban Javascript untuk semuanya.
Timmmm
2

Untuk pengguna naskah, jawaban oleh orang lain (terutama czerny ) dapat digeneralisasi ke kelas dasar yang aman dan dapat digunakan kembali:

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

Contoh penerapannya adalah ini sederhana: cukup timpa stringifyKeymetode. Dalam kasus saya, saya merangkai beberapa uriproperti.

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

Contoh penggunaan kemudian seolah-olah ini biasa 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
Jan Dolejsi
sumber
-1

Buat set baru dari kombinasi kedua set, lalu bandingkan panjangnya.

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 sama dengan set2 = true

set1 sama dengan set4 = false

Stefan Musarra
sumber
2
Apakah jawaban ini terkait dengan pertanyaan? Pertanyaannya adalah tentang kesetaraan item sehubungan dengan instance kelas Set. Pertanyaan ini tampaknya membahas kesetaraan dua instance Set.
czerny
@czerny Anda benar - saya awalnya melihat pertanyaan stackoverflow ini, di mana metode di atas dapat digunakan: stackoverflow.com/questions/6229197/…
Stefan Musarra
-2

Untuk seseorang yang menemukan pertanyaan ini di Google (seperti saya) yang ingin mendapatkan nilai Peta menggunakan objek sebagai Kunci:

Peringatan: jawaban ini tidak akan bekerja dengan semua objek

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

Keluaran:

Bekerja

DigaoParceiro
sumber