如何自定义对象等价JavaScript集

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

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

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

83577 次浏览

更新3/2022

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

由于记录和元组是不可变的(不能修改),并且因为它们很容易通过值(通过它们的内容,而不仅仅是它们的对象引用)进行比较,它允许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为什么我不能配置map和set如何比较键和值?

问题:如果有一种方法来配置什么映射就好了 键和set元素被认为是相等的。为什么没有呢?< / p > 答:该功能已被推迟,因为它很难 正确有效地执行。一种选择是将回调函数传递给

Java中可用的另一个选项是通过方法指定相等性 该对象实现(equals()在Java中)。然而,这种方法是 可变对象的问题:一般来说,如果一个对象改变了,它的 集合中的“location”也必须改变。但那不是 在Java中发生了什么。JavaScript可能会走更安全的路线 仅支持特殊不可变对象的值比较 (所谓的值对象)。按值比较指的是两个值 如果它们的内容相等,则认为它们相等。基本值包括

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

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

可以预见,它结果是变慢了Czerny的字符串连接方法

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

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

直接比较它们似乎是不可能的,但是JSON。如果键刚刚排序,Stringify就可以工作。正如我在评论中指出的那样

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

但我们可以用自定义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;
}

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

对那些在谷歌上发现这个问题的人(像我一样)想要使用对象作为键来获得一个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");

输出:

工作

正如上面的回答提到的,自定义相等性对于可变对象是有问题的。好消息是(我很惊讶还没有人提到这一点)有一个非常流行的库,名为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}]

对于Typescript用户,其他人(特别是车尔尼)的答案可以概括为一个良好的类型安全且可重用的基类:

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

对于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中从未见过)。

正如其他人所说,到目前为止还没有本地方法可以做到这一点。 但是如果你想用你的自定义比较器来区分一个数组,你可以尝试使用reduce方法
function distinct(array, equal) {
// No need to convert it to a Set object since it may give you a wrong signal that the set can work with your objects.
return array.reduce((p, c) => {
p.findIndex((element) => equal(element, c)) > -1 || p.push(c);
return p;
}, []);
}


// You can call this method like below,
const users = distinct(
[
{id: 1, name: "kevin"},
{id: 2, name: "sean"},
{id: 1, name: "jerry"}
],
(a, b) => a.id === b.id
);
...