Lập trình · 21/10/2026

JavaScript splice in a Forward Loop: Why Elements Get Skipped

A loop completes but leaves some targeted elements behind. splice shifts later elements left while a forward loop still increments its index. An item can move past the position being examined without being checked.

JavaScript splice trong vòng lặp tiến: vì sao xóa sót phần tử?

A loop completes but leaves some targeted elements behind. splice shifts later elements left while a forward loop still increments its index. An item can move past the position being examined without being checked.

1. Reproduce the bug with adjacent even numbers

const values = [2, 4, 5, 6, 8];
const visited = [];
for (let index = 0; index < values.length; index += 1) {
  visited.push(values[index]);
  if (values[index] % 2 === 0) values.splice(index, 1);
}
console.log(JSON.stringify(values)); // [4,5,8]
console.log(visited.join('|')); // 2|5|6

Removing 2 moves 4 into index 0, but the next iteration examines index 1. Removing 6 similarly shifts 8 into the deleted position and skips it. splice mutates the source array; see MDN splice.

Using the original length as the loop bound does not fix index shifting. The visited log shows which values were actually inspected rather than merely counting iterations.

2. Create a new list when selection is the goal

const values = [2, 4, 5, 6, 8];
const remaining = values.filter(value => value % 2 !== 0);
console.log(JSON.stringify(remaining)); // [5]
console.log(JSON.stringify(values)); // [2,4,5,6,8]
console.log(remaining === values); // false

filter expresses which elements to keep and creates a new array; see MDN filter. This predicate only checks numbers without modifying the source.

These are dense arrays of already validated numbers. For object records, filter does not deep-copy each retained object. Other owners of those objects can still observe field mutations. A new list and deeply independent data are separate requirements.

3. Iterate backward when the same array must change

function removeEvensInPlace(values) {
  for (let index = values.length - 1; index >= 0; index -= 1) {
    if (values[index] % 2 === 0) values.splice(index, 1);
  }
  return values;
}
const values = [2, 4, 5, 6, 8];
const shared = values;
const remaining = removeEvensInPlace(values);
console.log(JSON.stringify(values)); // [5]
console.log(remaining === shared); // true
console.log(shared.length); // 1

This sample helper expects a writable dense array of previously validated numbers. After removing the current item, all unexamined positions have smaller indices. Moving backward avoids skipping them in this algorithm.

shared observes the new contents because it is still the same array. This violates a source-preservation requirement even though the retained values are correct. Conversely, assigning a variable to a new filtered array does not update other references to the original.

Backward traversal also reverses inspection order. Adding API calls or other side effects changes their sequence compared with a forward loop. Separate data selection from ordered work if action order matters.

4. Choose according to data ownership

RequirementApproach shown here
Preserve the source listfilter with a non-mutating predicate
Update the same shared arrayBackward traversal with indexed splice
Preserve side-effect orderSeparate selection from execution

Test adjacent removals, first and last entries, all removed, none removed and empty input. Assert values, order and identity according to the chosen contract.

All six bilingual blocks ran with Node.js, alongside boundary cases for backward removal. These approaches were not benchmarked or integrated with UI components.

Discussion

Comments 0

Sign in to comment

You need an account to join the discussion and reply to other readers.

Sign inRegister

No comments yet. Be the first to share your thoughts.