如何在JavaScript中对Set进行映射/约简/过滤操作?

257

在JavaScript中,有没有办法对Set进行map/reduce/filter/等操作,还是我必须自己编写代码?

这里有一些合理的Set.prototype扩展。

Set.prototype.map = function map(f) {
  var newSet = new Set();
  for (var v of this.values()) newSet.add(f(v));
  return newSet;
};

Set.prototype.reduce = function(f,initial) {
  var result = initial;
  for (var v of this) result = f(result, v);
  return result;
};

Set.prototype.filter = function filter(f) {
  var newSet = new Set();
  for (var v of this) if(f(v)) newSet.add(v);
  return newSet;
};

Set.prototype.every = function every(f) {
  for (var v of this) if (!f(v)) return false;
  return true;
};

Set.prototype.some = function some(f) {
  for (var v of this) if (f(v)) return true;
  return false;
};

让我们来看一个小集合

let s = new Set([1,2,3,4]);

还有一些愚蠢的小函数

const times10 = x => x * 10;
const add = (x,y) => x + y;
const even = x => x % 2 === 0;

看看它们如何工作

s.map(times10);    //=> Set {10,20,30,40}
s.reduce(add, 0);  //=> 10
s.filter(even);    //=> Set {2,4}
s.every(even);     //=> false
s.some(even);      //=> true

这不是很好吗?是的,我也这么认为。将它与丑陋的迭代器用法进行比较。

// puke
let newSet = new Set();
for (let v in s) {
  newSet.add(times10(v));
}

// barf
let sum = 0;
for (let v in s) {
  sum = sum + v;
}

在 JavaScript 中,是否有更好的方法使用 Set 来实现 mapreduce 功能?


Set进行map-reduce的问题在于Sets不是Functors。 - Bartek Banachewicz
5
考虑一下 var s = new Set([1,2,3,4]); s.map((a) => 42);。它改变了元素的数量,而 map 通常不应该这样做。如果你只比较保留对象的部分,情况更糟,因为从技术上讲,你会得到未指定的结果。 - Bartek Banachewicz
我认为这是一个疏忽。对于一个Set来说,折叠(reduce)是完全可以的。 - Bartek Banachewicz
4
一些相关阅读:https://esdiscuss.org/topic/set-some-every-reduce-filter-map-methods - CodingIntrigue
everysome不应该使用filter来实现,这样它们可以提前return - Bergi
显示剩余7条评论
5个回答

234

通过ES6的展开运算符将其转换为数组是一种简便的方法。

然后,您就可以使用所有数组函数了。

const mySet = new Set([1,2,3,4]);
[...mySet].reduce(...);

1
因为Set没有提供这些功能,所以无法使用函数! 这是一个完整、有指导性并且易于理解的解决方法,但目前在这个主题中还不存在。 尽管“花费更长时间”是一种令人遗憾的代价,但这是一个解决方法,直到Set实现这些功能! - ZephDavies
1
这和Array.from有什么区别? - pete
31
至少对我来说,这与Array.from的区别在于Array.from可以和TypeScript一起使用。而使用[...mySet]则会产生错误:TS2461: Type 'Set<number>' is not an array type. - Mikal Madsen
2
请参考 https://dev59.com/mFkR5IYBdhLWcg3w_B5d#40549565 了解spread和Array.from()的区别。基本上,这两个都可以在这里使用。 Array.from()还可以处理没有实现@@iterator方法的类似数组的对象。 - ZephDavies
3
在V8中,另一个区别是对于大型Set,[...mySet]会导致堆栈溢出失败。 Array.from()不会为每个元素使用堆栈内存,因此当元素数量可能很大时,它的风险较小。 - peterflynn
显示剩余2条评论

30

总结来自评论的讨论:虽然没有技术上的原因不支持 reduce,但目前还没有提供,我们只能希望在 ES7 中它会有所改变。

至于 map,仅调用它可能会违反 Set 的限制,因此它在这里的存在可能是有争议的。

考虑使用函数 (a) => 42 进行映射 - 它将把集合的大小更改为 1,而这可能是您想要的,也可能不是。

如果您可以接受这种违反规则的情况,例如,您将要进行折叠操作,那么您可以在传递给 reduce 之前对每个元素应用 map 部分,从而接受中间集合(在这一点上不再是 Set)可能具有重复元素。这本质上等同于转换为数组进行处理。


1
这个大部分是正确的,除了(使用上面的代码),s.map(a => 42) 将会得到 Set { 42 },所以映射结果将会有不同的长度,但是不会有“重复”的元素。也许更新措辞,我会接受这个答案。 - Mulan
@naomik 哦,我在写这个的时候刚喝完第一杯咖啡。第二次看的时候,如果你接受它不是一个集合,传递给reduce的中间集合可能有立即元素 - 这就是我的意思。 - Bartek Banachewicz
2
哦,我明白了 - map必须映射到相同的类型,因此在目标集中可能会发生冲突。当我看到这个问题时,我认为map会从一个集合映射到一个数组。(就好像你做set.toArray().map()一样) - Simon_Weaver
2
在Scala和Haskell中,集合支持映射操作 - 它可以减少集合中的元素数量。 - Velizar Hristov

14

Map/Set集合缺少map/reduce/filter的原因似乎主要是概念上的问题。在Javascript中,每种集合类型是否应该实际指定其自己的迭代方法以允许此操作?

const mySet = new Set([1,2,3]);
const myMap = new Map([[1,1],[2,2],[3,3]]);

mySet.map(x => x + 1);
myMap.map(([k, x]) => [k, x + 1]);

替代

new Set(Array.from(mySet.values(), x => x + 1));
new Map(Array.from(myMap.entries(), ([k, x]) => [k, x + 1]));

另一个选择是将map/reduce/filter指定为可迭代/迭代器协议的一部分,因为entries/values/keys返回Iterator。虽然可以想象并非每个可迭代对象也都是“可映射”的。另一种选择是为此目的指定单独的“集合协议”。
但是,我不知道ES上关于这个话题的当前讨论情况。

2
每个 JavaScript 中的集合类型是否都应该指定自己的迭代方法以允许这样做?是的。所有使用 Array.fromnew Set 的方法都只是一种变通方法,而且比 myArray.filter(isBiggerThan6Predicate); 更难读懂。现在我必须编写自己的 filterSet 函数,这样我就可以编写干净的代码:filterSet(setWithNumbers, biggerThan6Predicate); - KulaGGin
性能方面如何?我想像新的 Set (Array.from(mySet...)) 与自己的迭代方法相比有很多开销。 - Cadoiz

0

关于速度,如果你在 Set 方法 forEach()(为每个元素调用回调)和 values()(返回 Set 中所有值的迭代器)之间进行选择,值得一提。

例如,让我们从一个 Set 中过滤出偶数:

const generateSet = (n, m) => {
  // Generate list of length n with random numbers between 0 and m
  let arr = Array.from({ length: n }, () =>
    Math.floor(Math.random() * m)
  );
  // Convert to Set
  var set = new Set(arr);
  return set;
};

我们的两个过滤函数:
const filterValues = (set) => {
  // Using Iterator
  const it = set.values();
  let result = it.next();
  while (!result.done) {
    if (result.value % 2 === 0) {
      set.delete(result.value);
    }
    result = it.next();
  }
};

const filterForEach = (set) => {
  // invokes a callback
  set.forEach((item) => {
    if (item % 2 === 0) {
      set.delete(item);
    }
  });
};

为了计时,我们使用 Timing 这些包含来自范围 [0, 10,000,000] 的数字的随机数组,其中包含 5,000,000 个项目:

let setValues = generateSet(5000000, 10000000);
console.time("Filter with values()");
filterValues(setValues);
console.timeEnd("Filter with values()");

let setForEach = generateSet(5000000, 10000000);
console.time("Filter with forEach()");
filterForEach(setForEach);
console.timeEnd("Filter with forEach()");

结果是:

Filter with values(): 399.456ms
Filter with forEach(): 374.698ms

或者你可以继续使用数组方法:

const arrMethod = (set) => {
  // Using Array method
  const filter = [...set].filter((item) => item % 2 === 0);
  return filter;
};

let setArray = generateSet(5000000, 10000000);
console.time("Filter with array");
filterForEach(setArray);
console.timeEnd("Filter with array");

看起来一直更快...

Filter with values(): 356.486ms
Filter with forEach(): 386.825ms
Filter with array: 342.358ms

请注意,数组方法返回的是一个数组而不是一个集合。 - undefined

-2
const set = new Set([1,2,3,4,5]);

function filterSet(index) {
    set.delete([...set][index]);
}

filterSet(3); // Set { 1, 2, 3, 5, [size]: 4 }

我认为这是一个相当不错的解决方案,可以对集合进行“过滤”。


1
大家好,很抱歉但我必须警告 - 不,禁止 - 这样做。转换为和从数组中获取数据是常见的,但Set不能保证其顺序不变。在你这个例子中 碰巧是相同的,但一般情况下并非如此。一个很容易破坏你代码的方法如下:const set = new Set([1, 1, 2]); filterSet(2); // 现在这句话是什么意思? - colinta
RIP,是的,我猜这会出错。 - Tjerk Pietersma

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接