如何为JavaScript Set自定义对象相等性

时间:2015-04-20 22:22:26

标签: javascript set ecmascript-harmony

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

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

如何自定义Set对象的相等性以进行深层对象比较?有没有类似Java equals(Object)

的东西

9 个答案:

答案 0 :(得分:81)

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

.has().add().delete()方法仅适用于基元的相同实际对象或相同值,并且没有方法可以插入或替换那个逻辑。

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

以下是来自this article and discussion ES6功能的一些信息:

  

5.2为什么我无法配置地图和集合比较键和值的方式?

     

问题:如果有办法配置哪个地图会很好   键和什么设置元素被认为是相等的。为什么不存在?

     

答案:该功能已被推迟,因为很难   正确有效地实施。一种选择是将回调交给   指定相等的集合。

     

Java中提供的另一个选项是通过方法指定相等性   该对象实现(Java中的equals())。但是,这种方法是   对于可变对象有问题:一般来说,如果一个对象发生了变化,那就是它   集合内的“位置”也必须改变。但事实并非如此   Java中会发生什么。 JavaScript可能会走更安全的路线   仅允许按特殊不可变对象的值进行比较   (所谓的价值对象)。按值比较意味着两个值   如果它们的内容相同则被认为是平等的。原始值是   比较JavaScript中的值。

答案 1 :(得分:25)

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

    // ...
}

每个inserted元素都必须实现返回string的toIdString()方法。当且仅当它们的toIdString方法返回相同的值时,才认为两个对象相等。

答案 2 :(得分:4)

为了在这里添加答案,我继续实现了一个Map包装器,它采用自定义哈希函数,自定义相等函数,并存储在桶中具有等效(自定义)哈希值的不同值。

可以预见,它turned out to be slowerczerny's string concatenation method

完整来源:https://github.com/makoConstruct/ValueMap

答案 3 :(得分:3)

似乎不可能直接比较它们,但是如果仅对键进行排序,则JSON.stringify可以工作。正如我在评论中指出的

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

但是我们可以使用自定义的stringify方法解决该问题。首先我们编写方法

自定义字符串化

res <- mapply(rep, repeat.it, repeat.times)
res <- unlist(res)

集合

现在我们使用Set。但是我们使用一组字符串代替对象

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

获取所有值

创建集合并添加值之后,我们可以通过

获得所有值
let set = new Set()
set.add({a:1, b:2}.stringifySorted());

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

这里是一个包含所有文件的链接 http://tpcg.io/FnJg2i

答案 4 :(得分:2)

也许您可以尝试使用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);

答案 5 :(得分:0)

正如top answer所述,自定义相等性对于可变对象是有问题的。好消息是(令我惊讶的是,还没有人提及),有一个非常受欢迎的库叫做immutable-js,它提供了一组丰富的不可变类型,它们提供了您要寻找的deep value equality semantics

这是您使用 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}]

答案 6 :(得分:0)

从两个集合的组合中创建一个新集合,然后比较长度。

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

答案 7 :(得分:0)

对于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

答案 8 :(得分:-1)

对于在Google上(和我一样)发现此问题的某人想要使用对象作为键的Map值:

警告:此答案不适用于所有对象

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

输出:

  

工作