Answers:
ES6 Set
对象没有任何比较方法或自定义比较可扩展性。
的.has()
,.add()
而.delete()
方法只关闭它是一个基本相同的实际物体或相同的值,没有办法插头插入或更换只是逻辑。
你大概可以从派生您自己的对象Set
和替换.has()
,.add()
并.delete()
与一些做了深刻的对象比较方法,先找到,如果该项目已经在设置,但性能可能不会很好,因为底层Set
对象不会帮助完全没有 在调用原始对象之前,您可能必须对所有现有对象进行蛮力迭代才能找到匹配项.add()
。
5.2为什么我不能配置映射和设置比较键和值的方式?
问题:如果有一种方法可以配置哪些映射键和哪些设置元素被认为是相等的,那就太好了。为什么不在那里?
答:由于难以正确有效地实施,该功能已被推迟。一种选择是将回调传递给指定相等性的集合。
Java中可用的另一个选项是通过对象实现的方法(Java中的equals())指定相等性。但是,这种方法对于可变对象是有问题的:通常,如果对象发生更改,则它在集合中的“位置”也必须更改。但这不是Java中发生的情况。JavaScript可能会走上一条更安全的途径,即仅对特殊的不可变对象(所谓的值对象)启用按值比较。按值比较意味着,如果两个值的内容相等,则认为这两个值相等。在JavaScript中按值比较原始值。
Set
?
如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
方法返回相同的值时,才认为两个对象相等。
item.toIdString()
是不变的并且不能改变。因为如果可以,则GeneralSet
其中的“重复”项目很容易变得无效。因此,这样的解决方案将仅限于某些情况,在某些情况下,使用该集合时对象本身不会发生更改,或者变得无效的集合不会产生任何后果。所有这些问题都可能进一步解释了ES6集为何不公开此功能的原因,因为它实际上仅在某些情况下有效。
.delete()
在此答案中添加正确的实现?
正如最高答案提到的那样,自定义相等性对于可变对象是有问题的。好消息是(令我惊讶的是,到目前为止还没有人提及),有一个非常流行的库称为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}]
为了在此处添加答案,我继续实现了Map包装器,该包装器接受了自定义哈希函数,自定义相等函数,并在存储桶中存储了具有等效(自定义)哈希值的不同值。
可以预见的是,它竟然是慢比切尔尼的字符串连接方法。
Point
as 的定义,{ x: number, y: number }
则id string
可能是x.toString() + ',' + y.toString()
。
String
,那么您可以跳过您所说的整个哈希和存储步骤,而直接使用Map
甚至就派生键而言的旧式普通对象。
{x: '1,2', y: '3'}
和{x: '1', y: '2,3'}
,则将String(x) + ',' + String(y)
为两个对象输出相同的值。假设您可以依靠JSON.stringify()
确定性,一个更安全的选择是利用其字符串转义并JSON.stringify([x, y])
改用它。
直接比较它们似乎是不可能的,但是如果仅对键进行排序,则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
也许您可以尝试使用它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);
对于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
从两个集合的组合中创建一个新集合,然后比较长度。
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
===
运算符重载。ES6 set对象没有任何比较方法。该.has()
方法和.add()
方法仅在它是相同的实际对象或基元的值的情况下起作用。