Splicing a Javascript array from within the callback passed to forEach

I have this code which is supposed to iterate over each item in an array, removing items based on some condition:

//iterate over all items in an array
//if the item is "b", remove it.

var array = ["a", "b", "c"];

array.forEach(function(item) {
    if(item === "b") {
        array.splice(array.indexOf(item), 1);
    }

    console.log(item);
});

Desired output:

a
b
c

Actual output:

a
b

Obviously the native forEach method doesn't check after each iteration whether the item has been deleted, so if it is then the next item is skipped. Is there a better way of doing this, aside from overriding the forEach method or implementing my own class to use instead of an array?

Edit - further to my comment, I suppose the solution is to just use a standard for loop. Feel free to answer if you have a better way.

Answers:

Answer

Lets see why JavaScript behaves like this. According to the ECMAScript standard specification for Array.prototype.forEach,

when you delete an element at index 1, the element at index 2 becomes the element at index 1 and index 2 doesn't exist for that object.

Now, JavaScript looks for element 2 in the object, which is not found, so it skips the function call.

That is why you see only a and b.


The actual way to do this, is to use Array.prototype.filter

var array = ["a", "b", "c"];

array = array.filter(function(currentChar) {
    console.log(currentChar);   // a, b, c on separate lines
    return currentChar !== "b";
});
console.log(array);             // [ 'a', 'c' ]
Answer

One possibility would be to use the array.slice(0) function, which creates a copy (clone) of the array and thus the iteration is separated from the deletion.

Then the only change to the original approach using array.forEach would be to change it to array.slice(0).forEach and it will work:

array.slice(0).forEach(function(item) {
    if(item === "b") {
        array.splice(array.indexOf(item), 1);
    }
    alert(item)
});

After the forEach, the array will contain only a and b.

A jsFiddle demo can be found here.

Answer

Using Array.prototype.filter as in thefourtheye's answer is a good way to go, but this could also be done with a while loop. E.g.:

const array = ["a", "b", "c"];
let i = 0;

while (i < array.length) {
    const item = array[i];

    if (item === "b") {
        array.splice(i, 1);
    } else {
        i += 1;
    }

    console.log(item);
});
Answer

All the above mentioned answers just fail or not retain the original array to be passed elsewhere, if we were to remove two elements at a specific index and continue iterating from the immediate element. for suppose, I have an array

vehicles = [{make: ford, model: mustang}, 
            {make: chevy, model: camaro}, 
            {make: chevy, model: camaro},
            {make: ford, model: mustang},
            {make: chevy, model: camaro}]

I want to splice away two elements if there is successive combination of ford and chevy.

vehicles.forEach(function (vehicle) {
         if (vehicle) {
              var index = vehicles.indexOf(vehicle);
              var flag = vehicle.make=== "ford" && vehicles[index + 1].make=== "chevy";
              if (flag) {
                  //Array.Prototype.forEach() wouldn't update the iteration index after splice
                  vehicles.splice(index, 2, null);
              }
          }
});

So this way I am replacing the couple spliced elements with a null so that I could adapt to non updating iteration index of forEach(). Then I can clean the array of any inserted nulls once the iteration is complete, and array is ready to be handed off.

//After all the iteration is done, we clear all the inserted null
vehicles = [].concat(vehicles.filter(Boolean));

This can be a better way of not hampering anything and absoloutely solving this spooky behavior of javascript.

Answer

Another possibility would be to use the array.reduceRight function to avoid the skip:

//iterate over all items in an array from right to left
//if the item is "b", remove it.

const array = ["a", "b", "c"];

array.reduceRight((_, item, i) => {
    if(item === "b") {
        array.splice(i, 1);
    }

});

console.log(array);

After the reduceRight, the array will contain only a and c.

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.