web-dev-qa-db-ja.com

JavaScriptセットのオブジェクトの等価性をカスタマイズする方法

新しいES 6(Harmony)では、新しい Set オブジェクトが導入されました。 Setで使用されるIDアルゴリズムは===演算子に似ているため、オブジェクトの比較にはあまり適していません。

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

深いオブジェクト比較を行うためにSetオブジェクトの等値性をカスタマイズする方法は? Java equals(Object)のようなものはありますか?

127
czerny

ES6 Setオブジェクトには、比較メソッドやカスタム比較拡張機能はありません。

.has().add()、および.delete()メソッドは、同じ実際のオブジェクトまたはプリミティブの同じ値である場合にのみ機能し、そのロジックのみにプラグインまたは置換する手段はありません。

おそらく、Setから独自のオブジェクトを派生させ、.has().add()、および.delete()のメソッドを、オブジェクトがすでに深いかどうかを調べるために最初に深いオブジェクト比較を行ったものに置き換えることができますセットではありますが、基になるSetオブジェクトはまったく役に立たないため、パフォーマンスはおそらく良くありません。おそらく、元の.add()を呼び出す前に、独自のカスタム比較を使用して、既存のすべてのオブジェクトをブルートフォースで繰り返して一致を見つける必要があります。

ES6機能の この記事とディスカッション からの情報を以下に示します。

5.2マップとセットがキーと値を比較する方法を設定できないのはなぜですか?

質問:どのマップキーとどのセット要素が等しいと見なされるかを構成する方法があればいいでしょう。なぜないの?

回答:適切かつ効率的に実装することが難しいため、この機能は延期されました。 1つのオプションは、等価性を指定するコレクションにコールバックを渡すことです。

Javaで利用できる別のオプションは、オブジェクトが実装するメソッド(Javaのequals())を介して同等性を指定することです。ただし、このアプローチは可変オブジェクトには問題があります。一般に、オブジェクトが変更された場合、コレクション内の「場所」も変更する必要があります。しかし、それはJavaで起こることではありません。 JavaScriptはおそらく、特別な不変オブジェクト(いわゆる値オブジェクト)の値による比較のみを有効にするという、より安全な方法になります。値による比較とは、内容が等しい場合に2つの値が等しいと見なされることを意味します。プリミティブ値は、JavaScriptの値によって比較されます。

86
jfriend00

jfriend00's answer で述べたように、等値関係のカスタマイズはおそらく不可能です

次のコードは、計算効率の高い(ただし、メモリが高い)回避策の概要を示しています。

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()メソッドを実装する必要があります。 2つのオブジェクトは、toIdStringメソッドが同じ値を返す場合にのみ等しいと見なされます。

27
czerny

ここで答えを追加するために、カスタムハッシュ関数、カスタム等値関数を受け取り、バケットに同等の(カスタム)ハッシュを持つ個別の値を保存するMapラッパーを実装しました。

予想通り、それは 結果が遅くなったczernyの文字列連結方法 よりも。

完全なソースはこちら: https://github.com/makoConstruct/ValueMap

4
mako

それらを直接比較することは不可能に思えますが、キーがソートされただけであればJSON.stringifyは機能します。コメントで指摘したように

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

しかし、カスタムstringifyメソッドを使用して、この問題を回避できます。最初にメソッドを書きます

カスタム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を使用します。しかし、オブジェクトの代わりに文字列のセットを使用します

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

ここにすべてが1つのファイルにあるリンクがあります http://tpcg.io/FnJg2i

3
relief.melone

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
GuaHsu

トップアンサー が言及しているように、等値のカスタマイズは可変オブジェクトにとって問題です。良いニュースは(そして、これについて誰もまだ言及していないことに驚いています) immutable-js と呼ばれる非常に人気のあるライブラリがあり、 deep value等価セマンティクス 探しています。

次に、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}]
1
Russell Davis

両方のセットの組み合わせから新しいセットを作成し、長さを比較します。

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 = true

set1 = set4 = false

0
Stefan Musarra

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
0
Jan Dolejsi