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

别来无恙 提交于 2020-11-30 17:56:25

问题


Is it specified that you can delete any element in an instance of Set while iterating using for..of and that

  • you won't iterate more than once on an element
  • you won't miss any other element that was in the set at the start of the iteration other than the ones you remove

?


回答1:


Yes, it is perfectly fine to add elements and remove elements to a set while iterating it. This use case was considered and is supported in JavaScript 2015 (ES6). It will leave it in a consistent state. Note this also applies to itearting with forEach.

Intuitively:

The set iteration algorithm basically looks something like this:

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

Addition just looks something like this:

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

So it does not interfere with existing iterations - they will iterate it.

Deletion looks something like this:

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

Replacing it with an empty value rather than deleting it ensures it will not mess up with iterators' positions.


Formally

Addition

Here is the relevant part of the specification from %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.

The set iterator proceeds to iterate the entries one by one.

From Set.prototype.add:

Append value as the last element of entries.

This ensures that when adding elements to the list it will be iterated before the iteration completes since it always gets a new slot in the entries list. Thus this will work as the spec mandates.

As for deletion:

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

Replacing it with an empty element rather than removing it ensures that the iteration order of existing iterators will not get out or order and they will continue iterating the set correctly.

With code

Here is a short code snippet that demonstrates this ability

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

Which logs the numbers 1 to 10. Here is a version not using for... of you can run in your browser today:

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



回答2:


My answer is yes, if you're okay with it continuing onto the next value in the Set in the next iteration after the deletion. It doesn't even seem to matter which instance of the Set you're currently on in the iteration process. Pretty ideal!


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

In Firefox 75.0, it works just fine. Sets are supposed to maintain their insertion order, and it does, it prints it out in that order as it iterates through. Regardless of what gets deleted, it continues in the insertion order:

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

I also tested with similar code but that doesn't use the current instance of the iteration process:

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

I had to use an adjacent array because there's no way to look up a random index of a Set, to delete a random instance. So I just created the Set from the array so it uses the same object instances.

That works great as well, as you can see:

0
deleted 1
2
deleted 2
3
0
3
0
deleted 0
3
3
3
3
deleted 3

And yes... I even tested with random object instance insertion as well... Same deal, I won't post the output this time:

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


来源:https://stackoverflow.com/questions/28306756/is-it-safe-to-delete-elements-in-a-set-while-iterating-with-for-of

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!