新的ES 6 (Harmony)引入了新的Set对象。Set使用的恒等算法类似于===运算符,所以不太适合比较对象:

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

如何自定义相等的集合对象,以做深度对象比较?有没有类似Java = (Object)的东西?


当前回答

对于TypedArray作为Set/Map键的特殊但常见的情况,使用一种很好的字符串化方法

const key = String.fromCharCode(...new Uint16Array(myArray.buffer));

它生成可以轻松转换回去的最短的惟一字符串。然而,对于低代理和高代理的显示,这并不总是一个有效的UTF-16字符串。Set和Map似乎忽略了代理有效性。 在Firefox和Chrome中,扩展操作符执行得比较慢。如果你的myArray有固定的大小,当你写的时候执行得更快:

const a = new Uint16Array(myArray.buffer);  // here: myArray = Uint32Array(2) = 8 bytes
const key = String.fromCharCode(a[0],a[1],a[2],a[3]);  // 8 bytes too

这种键构建方法最有价值的优点可能是:它适用于Float32Array和Float64Array,没有任何舍入副作用。注意+0和-0是不同的。无穷大也是一样的。静默nan也一样。信号nan根据它们的信号而不同(在普通JavaScript中从未见过)。

其他回答

也许你可以尝试使用JSON.stringify()来进行深度对象比较。

例如:

Const arr = [ {名称:“a”,值:10}, {名称:“a”,值:20}, {名称:“a”,值:20}, {名称:“b”,价值:30}, {名称:“b”,价值:40}, {名称:“b”,价值:40} ]; const names = new Set(); Const result = arr。name .has(JSON.stringify(item)) ?names.add(JSON.stringify(item)): false); console.log(结果);

对于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方法。在我的情况下,我stringify一些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

正如在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方法返回相同的值时,才认为它们相等。

更新3/2022

目前有一个提议将记录和元组(基本上是不可变的对象和数组)添加到Javascript中。在这个提议中,它提供了使用===或!==来比较值的记录和元组的直接比较,而不仅仅是对象引用,并且与这个答案相关的Set和Map对象都将在键比较/查找中使用记录或元组的值,这将解决这里所要求的问题。

由于记录和元组是不可变的(不能修改),并且因为它们很容易通过值(通过它们的内容,而不仅仅是它们的对象引用)进行比较,它允许map和set使用对象内容作为键,拟议的规范明确地为Sets和map命名了这一特性。

这个最初的问题要求Set比较的可定制性,以支持深度对象比较。这并没有提出Set比较的可定制性,但如果您使用新的Record或Tuple而不是object或Array,则它直接支持深度对象比较,从而解决了这里的原始问题。

请注意,该提案于2021年年中推进到第二阶段。最近一直在向前推进,但肯定还没有完成。

Mozilla在这个新提议上的工作可以在这里找到。


原来的答案

ES6 Set对象没有任何比较方法或自定义比较扩展性。

.has(), .add()和.delete()方法只能在原语是相同的实际对象或相同的值时起作用,而不能插入或替换该逻辑。

您可以假定从Set中派生出自己的对象,并将.has()、.add()和.delete()方法替换为首先进行深度对象比较的方法,以查找该项是否已经在Set中,但性能可能不会很好,因为底层Set对象根本没有帮助。在调用原始的.add()之前,您可能必须对所有现有对象进行蛮力迭代,使用您自己的自定义比较来查找匹配。

以下是本文和ES6特性讨论中的一些信息:

5.2 Why can’t I configure how maps and sets compare keys and values? Question: It would be nice if there were a way to configure what map keys and what set elements are considered equal. Why isn’t there? Answer: That feature has been postponed, as it is difficult to implement properly and efficiently. One option is to hand callbacks to collections that specify equality. Another option, available in Java, is to specify equality via a method that object implement (equals() in Java). However, this approach is problematic for mutable objects: In general, if an object changes, its “location” inside a collection has to change, as well. But that’s not what happens in Java. JavaScript will probably go the safer route of only enabling comparison by value for special immutable objects (so-called value objects). Comparison by value means that two values are considered equal if their contents are equal. Primitive values are compared by value in JavaScript.

为了补充这里的答案,我实现了一个Map包装器,它接受一个自定义哈希函数、一个自定义相等函数,并将具有等效(自定义)哈希值的不同值存储在存储桶中。

可以预见的是,它比czerny的字符串连接方法要慢。

完整源代码在这里:https://github.com/makoConstruct/ValueMap