新的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)的东西?
新的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)的东西?
当前回答
直接比较它们似乎是不可能的,但是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
其他回答
更新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.
正如其他人所说,当前版本的Set没有办法做到这一点。 我的建议是使用数组和映射的组合。
下面的代码将基于您自己定义的键创建唯一键的映射,然后将唯一项的映射转换为一个数组。
Const数组= [ {"name": "Joe", "age": 17}, {"name": "Bob", "age": 17}, {"name": "Carl", "age": 35} ] Const key = 'age'; const arrayUniqueByKey =[…]新地图(数组。地图(项= > (项目(关键),项目))). values ()]; console.log (arrayUniqueByKey); / *输出 [ {"name": "Bob", "age": 17}, {"name": "Carl", "age": 35} ] * / //注意:这将选择列表中最后一个重复的项。
对于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
也许你可以尝试使用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(结果);
对于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中从未见过)。