【问题标题】:Is it safe to delete elements in a Set while iterating with for..of?使用 for..of 迭代时删除 Set 中的元素是否安全?
【发布时间】:2015-04-03 02:41:03
【问题描述】:

是否指定您可以在使用for..of 进行迭代时删除Set 实例中的任何元素以及

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

?

【问题讨论】:

  • @bjb568 规范!= 保证。我不反对改进措辞,但失去问题的主要意图是不好的。我现在已经回滚了。如果您有更好的措辞,同时保持意图清晰(即来自规范),请这样做。
  • IDK 你的意思@Florian,规范保证 行为。无论如何,实施者通常不会有太多遵循规范的问题。

标签: javascript iterator set ecmascript-6


【解决方案1】:

是的,在迭代时向集合添加元素和删除元素是非常好的。 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 迭代器继续逐个迭代条目。

来自Set.prototype.add

将值附加为条目的最后一个元素。

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

至于删除:

将条目中值为e的元素替换为值为空的元素。

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

有代码

这是一个演示此能力的短代码 sn-p

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

【讨论】:

  • 我想快速指出,Symbol 在 FF 中尚不支持,因为消费者版本是 35,而它需要 36。
  • 更新:我收到了 Brendan Eich 的回复,显然 esdiscuss 中要查看的相关主题是 esdiscuss.org/topic/set-iterators
  • @BenjaminGruenbaum 这个讨论读起来很有趣。
  • 所以,就是答案。不,您不能在不使用迭代器的情况下从 Set 中删除元素。 ...
  • The spec 确实特别提到了在使用forEach 进行迭代时插入和删除。该规范似乎并不需要它。目前看来,JS 引擎似乎一致地实现了它。
【解决方案2】:

我的答案是肯定的,如果您同意在删除后的下一次迭代中继续使用 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

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

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 中测试过。
猜你喜欢
  • 2019-11-04
  • 1970-01-01
  • 2011-02-21
  • 2014-09-22
  • 1970-01-01
  • 1970-01-01
  • 2014-01-04
  • 2019-09-26
相关资源
最近更新 更多