在使用 for..of 迭代时删除 Set 中的元素是否安全?

Is it safe to delete elements in a Set while iterating with for..of?

是否指定在使用 for..of

迭代时可以删除 Set 实例中的任何元素

?

,在迭代集合的同时添加元素和移除元素是完全没问题的。 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规范的相关部分:

Repeat while index is less than the total number of elements of entries. The number of elements must be redetermined each time this method is evaluated.

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

来自Set.prototype.add

Append value as the last element of entries.

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

至于删除:

Replace the element of entries whose value is e with an element whose value is empty.

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

有代码

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

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

它记录数字 1 到 10。这是一个不用于...的版本,您今天可以在浏览器中运行:

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;
}

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


这是我的测试代码:

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

是的...我什至还测试了随机对象实例插入...同样,这次我不会 post 输出:

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);