Is it safe to delete elements in a Set while iterating with for..of?
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;
}
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);