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


167

新的ES 6(和谐)引入了新的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)


3
“定制平等”是什么意思?Javascript不允许运算符重载,因此没有办法使===运算符重载。ES6 set对象没有任何比较方法。该.has()方法和.add()方法仅在它是相同的实际对象或基元的值的情况下起作用。
jfriend00

12
“自定义相等性”是指开发人员如何定义某些对象对(无论是否相等)的任何方式。
czerny

Answers:


107

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

.has().add().delete()方法只关闭它是一个基本相同的实际物体或相同的值,没有办法插头插入或更换只是逻辑。

你大概可以从派生您自己的对象Set和替换.has().add().delete()与一些做了深刻的对象比较方法,先找到,如果该项目已经在设置,但性能可能不会很好,因为底层Set对象不会帮助完全没有 在调用原始对象之前,您可能必须对所有现有对象进行蛮力迭代才能找到匹配项.add()

以下是本文的一些信息以及对ES6功能的讨论

5.2为什么我不能配置映射和设置比较键和值的方式?

问题:如果有一种方法可以配置哪些映射键和哪些设置元素被认为是相等的,那就太好了。为什么不在那里?

答:由于难以正确有效地实施,该功能已被推迟。一种选择是将回调传递给指定相等性的集合。

Java中可用的另一个选项是通过对象实现的方法(Java中的equals())指定相等性。但是,这种方法对于可变对象是有问题的:通常,如果对象发生更改,则它在集合中的“位置”也必须更改。但这不是Java中发生的情况。JavaScript可能会走上一条更安全的途径,即仅对特殊的不可变对象(所谓的值对象)启用按值比较。按值比较意味着,如果两个值的内容相等,则认为这两个值相等。在JavaScript中按值比较原始值。


4
添加了有关此特定问题的文章参考。似乎面临的挑战是如何处理与添加到集合时的另一个对象完全相同但现在已更改且不再与该对象相同的对象。是否在Set
jfriend00 2015年

3
为什么不实现一个简单的GetHashCode或类似的东西呢?
Jamby

@Jamby-创建一个可处理所有类型的属性并按正确顺序哈希属性并处理循环引用等的哈希将是一个有趣的项目。
jfriend00

1
@Jamby即使使用哈希函数,您仍然必须处理冲突。您只是在推迟平等问题。
mpen

5
@mpen这是不对的,我允许开发人员为自己的特定类管理自己的哈希函数,这在几乎每种情况下都可以防止冲突问题,因为开发人员知道对象的性质并可以派出一个好的密钥。在任何其他情况下,请回退到当前比较方法。很多 语言 已经做到这一点,JS不是。
Jamby

28

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方法返回相同的值时,才认为两个对象相等。


您还可以让构造函数使用一个比较项是否相等的函数。如果希望将此相等性作为集合的功能而不是集合中使用的对象的功能,那么这很好。
Ben J

1
@BenJ生成字符串并将其放入Map的意义在于,这样,您的Javascript引擎将在本机代码中使用〜O(1)搜索来搜索对象的哈希值,同时接受相等函数将强制对集合进行线性扫描并检查每个元素。
Jamby

3
这种方法的一个挑战是,我认为它假设的值item.toIdString()是不变的并且不能改变。因为如果可以,则GeneralSet其中的“重复”项目很容易变得无效。因此,这样的解决方案将仅限于某些情况,在某些情况下,使用该集合时对象本身不会发生更改,或者变得无效的集合不会产生任何后果。所有这些问题都可能进一步解释了ES6集为何不公开此功能的原因,因为它实际上仅在某些情况下有效。
jfriend00

是否可以.delete()在此答案中添加正确的实现?
jlewkovich

1
@JLewkovich肯定
czerny19年

6

正如最高答案提到的那样,自定义相等性对于可变对象是有问题的。好消息是(令我惊讶的是,到目前为止还没有人提及),有一个非常流行的库称为immutable-js,该库提供了丰富的不可变类型集,这些类型提供了您正在寻找的深层价值相等语义

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

10
immutable-js Set / Map与本机Set / Map的性能相比如何?
frankster

5

为了在此处添加答案,我继续实现了Map包装器,该包装器接受了自定义哈希函数,自定义相等函数,并在存储桶中存储了具有等效(自定义)哈希值的不同值。

可以预见的是,它竟然是慢切尔尼的字符串连接方法

全文在这里:https : //github.com/makoConstruct/ValueMap


“字符串串联”?他的方法是否更像是“字符串替代”(如果您要给它起个名字)?还是您使用“串联”一词的原因?我很好奇;-)
宾基

@binki这是一个很好的问题,我认为答案提出了一个很好的观点,我花了一些时间来掌握。通常,在计算哈希码时,会执行类似于HashCodeBuilder的操作,该运算将各个字段的哈希码相乘,并且不能保证是唯一的(因此需要自定义相等函数)。但是,生成ID字符串时,您要串联各个字段的ID字符串,这些ID保证是唯一的(因此不需要相等函数)
Pace

因此,如果您有一个Pointas 的定义,{ x: number, y: number }id string可能是x.toString() + ',' + y.toString()
佩斯

我以前使用过一种策略,使您的平等比较建立一些价值,只有在事情被认为不平等时,这些价值才能保证有所变化。有时以这种方式思考事情会更容易。在这种情况下,您将生成密钥而不是哈希。只要您有一个密钥派生程序,它以现有工具支持的具有值样式相等性的形式输出密钥(这种情况几乎总是以)结束String,那么您可以跳过您所说的整个哈希和存储步骤,而直接使用Map甚至就派生键而言的旧式普通对象。
宾基

1
如果在密钥派生程序的实现中实际使用字符串连接,要注意的一件事是,如果允许字符串属性采用任何值,则可能需要对字符串属性进行特殊处理。例如,如果您有{x: '1,2', y: '3'}{x: '1', y: '2,3'},则将String(x) + ',' + String(y)为两个对象输出相同的值。假设您可以依靠JSON.stringify()确定性,一个更安全的选择是利用其字符串转义并JSON.stringify([x, y])改用它。
宾基

3

直接比较它们似乎是不可能的,但是如果仅对键进行排序,则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);
}

集合

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

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

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


“如果键已排序”则是一个很大的问题,尤其是对于复杂的对象
Alexander Mills

这正是我选择这种方法的原因;)
relief.melone19年

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


2
这可以正常工作,但不必作为JSON.stringify({a:1,b:2})!== JSON.stringify({b:2,a:1})如果所有对象都是由您的程序在同一位置创建的为了安全起见 但总的来说

1
是的,“将其转换为字符串”。Javascript的一切答案。
Timmmm

2

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

-1

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

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


2
这个答案和这个问题有关吗?问题是关于Set类实例的项是否相等。这个问题似乎在讨论两个Set实例的相等性。
czerny19年

@czerny你是正确的-我本来查看此计算器问题,在上面的方法可以用于:stackoverflow.com/questions/6229197/...
斯特凡Musarra

-2

对于某个在Google上(和我一样)想找到此问题的人,想要使用对象作为Key来获取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");

输出:

工作了

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.