使用for..of迭代Set集合时删除元素是否安全?

52

在使用for..of迭代一个Set实例时,是否规定可以删除任意元素,并且:

  • 您不会对某个元素进行多次遍历
  • 除了您删除的元素之外,不会错过在迭代开始时存在于集合中的任何其他元素


1
@bjb568 规范并不等同于保证。我不反对改进措辞,但是失去问题的主要意图是不好的。我暂时回滚了。如果您有更好的措辞,同时保持意图清晰(即来自规范),请修改。 - Florian Margaine
1
我不知道你在说什么,@Florian。规范保证了行为。实现者通常不会遇到太多遵循规范的问题。 - bjb568
2个回答

51

是的,在迭代集合时添加或删除元素是完全可以的。在JavaScript 2015(ES6)中,已经考虑到并支持了这种用例,并且会保持集合处于一致的状态。请注意,这也适用于使用forEach进行迭代。

直观地说:

集合迭代算法基本上看起来像这样:

Set position to 0
While position < calculateLength() // note it's calculated on each iteration
    return the element at set.entryList[position]

加法的样子类似于这样:

If element not in set
   Add element to the _end_ of the set

因此,它不会干扰现有迭代-它们将迭代它。

删除的过程如下所示:

Replace all elements with are equal to `element` with a special empty value

用空值替换它而不是删除它可以确保不会破坏迭代器的位置。


形式化

添加

这里是来自% SetIteratorPrototype% .next 规范的相关部分:

当索引小于条目总数时重复。必须每次在计算此方法时重新确定元素数量。

集合迭代器逐个迭代条目。

来自Set.prototype.add

将值作为最后一个元素附加到条目中。

这确保了在向列表添加元素时,在迭代完成之前将进行迭代,因为它总是在条目列表中获得新插槽。因此,这将按照规范要求工作。

至于删除:

用一个值为空的元素替换值为e的元素。

用空元素替换而不是移除它可以确保现有迭代器的迭代顺序不会失序,并且它们将继续正确地迭代集合。

代码示例

这是一个演示这种能力的简短代码片段

var set = new Set([1]);
for(let item of set){
   if(item < 10) set.add(item+1);
   console.log(item);
}

记录数字1到10的日志。下面是一个不使用for...of循环的版本,您可以在浏览器中运行它:

var set = new Set([1]);
for (var _i = set[Symbol.iterator](), next; !(next = _i.next()).done;) {
   var item = next.value;
   if (item < 10) set.add(item + 1);
   document.body.innerHTML += " " + item;
}


我想快速指出Symbol在FF中尚未得到支持,因为消费者版本是35,而它需要36。 - ndugger
3
更新:我收到了Brendan Eich的回复,显然要查看esdiscuss中相关的主题是https://esdiscuss.org/topic/set-iterators。 - Benjamin Gruenbaum
1
@BenjaminGruenbaum 这个讨论非常有趣。 - Denys Séguret
1
所以,答案是否定的。不,你不能删除Set中的元素而不影响迭代器。... - Andrew
规范确实提到了在使用 forEach 迭代时插入和删除元素的情况。但是规范似乎并没有要求这样做。目前看来,JS 引擎已经一致地实现了它。 - Nathan

2
我的答案是肯定的,如果您同意在删除后的下一次迭代中继续到Set中的下一个值。似乎当前迭代过程中Set的哪个实例也并不重要。非常理想!

Here's my test code:

s = new Set([ { a: 0 }, { a: 1 }, { a: 2 }, { a: 3 } ]);
do {
  for (let x of s) {
    console.log(x.a);
    if (Math.random() < 0.2) {
      console.log('deleted ' + x.a);
      s.delete(x);
    }
  }
} while (s.size > 0);

在Firefox 75.0中,它正常工作。集合应该保持插入顺序,它确实如此,并且在迭代时按照该顺序打印出来。无论删除了什么,它都会按照插入顺序继续进行:
0
1
2
3
0
1
deleted 1
2
3
0
2
deleted 2
3
0
3
0
deleted 0
3
3
...
3
3
deleted 3

我也用类似的代码进行了测试,但它没有使用当前迭代过程的实例:

sCopy = [{ a: 0 }, { a: 1 }, { a: 2 }, { a: 3 }];
s = new Set(sCopy);
do {
  for (let x of s) {
    console.log(x.a);
    if (Math.random() < 0.2) {
      let deleteMe = Math.floor(Math.random() * s.size);
      console.log('deleted ' + sCopy[deleteMe].a);
      s.delete(sCopy[deleteMe]);
      sCopy.splice(deleteMe, 1);
    }
  }
} while (s.size > 0);

我不得不使用相邻数组,因为没有办法查找Set的随机索引,以删除随机实例。所以我只是从数组中创建了Set,这样它就使用相同的对象实例。
正如您所看到的,这也非常有效:
0
deleted 1
2
deleted 2
3
0
3
0
deleted 0
3
3
3
3
deleted 3

是的,我甚至测试了随机对象实例插入... 结果一样,这次我不会发布输出:

sCopy = [{ a: 0 }, { a: 1 }, { a: 2 } ];
s = new Set(sCopy);
do {
  for (let x of s) {
    console.log(x.a);
    if (Math.random() < 0.1) {
      let newInstance = { a: Math.random() * 100 + 100 };
      console.log('added ' + newInstance.a);
      s.add(newInstance);
      sCopy.push(newInstance);
    }
    if (Math.random() < 0.2) {
      let deleteMe = Math.floor(Math.random() * s.size);
      console.log('deleted ' + sCopy[deleteMe].a);
      s.delete(sCopy[deleteMe]);
      sCopy.splice(deleteMe, 1);
    }
  }
} while (s.size > 0);

这在Linux上的FF 75.0中进行了测试,我的朋友在MacOS上的Chrome中也进行了测试。 - Andrew

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