DEV Community

Travis van der F.
Travis van der F.

Posted on

The Confusing World of Array Deletion in JavaScript

One of JavaScript's most controversial, yet quckly discussed subjects that goes beyond implicit type coercion (e.g., [] + {}) is handling arrays natively, specifically for programmers of other languages learning with the goal of actually understanding the language. While it's one of the most popular programming languages in the world, its design choices around array manipulation have frustrated developers for decades, including newcomers. This is especially clear when trying to do something simple, such as removing an item from an array.

Let's get into the different approaches!
Consider a simple basket of fruit:

const fruits = ['apple', 'banana', 'orange', 'grape', 'mango'];
Enter fullscreen mode Exit fullscreen mode

Now, imagine wanting to remove 'orange' from this basket. JavaScript offers several ways to do this, each with its own quirks, return values, and gotchas.

Approach 1: Using the Splice Function

The most common way to remove an element is using splice():

const fruits = ['apple', 'banana', 'orange', 'grape', 'mango'];
const index  = fruits.indexOf('orange'); // returns 2

if (index > -1)
{
  const removed = fruits.splice(index, 1);
  console.log(removed);  // returns an array ['orange']
  console.log(fruits);   // ['apple', 'banana', 'grape', 'mango']
}
Enter fullscreen mode Exit fullscreen mode

And here's where things get interesting. The splice() function doesn't just remove the element, it also mutates the original array AND returns an array of the removed elements. Not the element itself, but an array containing it. This is where the second parameter (set to 1 in this example) comes into play: if you increase it (e.g., to 2 or 3), the array elements will also be returned, such as 'grape' and/or 'mango'.

This return value makes sense when removing multiple items:

const fruits  = ['apple', 'banana', 'orange', 'grape', 'mango'];
const removed = fruits.splice(1, 3); // remove 3 items starting at index 1

console.log(removed);  // ['banana', 'orange', 'grape']
console.log(fruits);   // ['apple', 'mango']
Enter fullscreen mode Exit fullscreen mode

But for a single element, getting back a one-item array feels unnecessarily wrapped.

The splice() vs. slice() Naming Confusion

Perhaps the most infamous naming confusion in JavaScript, particularly within arrays, is between splice() and slice(). They differ by just one letter, however do completely opposite things. Let's take a look:

const fruits = ['apple', 'banana', 'orange', 'grape', 'mango'];

// slice sounds destructive but it isn't
const sliced = fruits.slice(1, 3);
console.log(sliced);  // new array: ['banana', 'orange']
console.log(fruits);  // unchanged array: ['apple', 'banana', 'orange', 'grape', 'mango']

// splice sounds like joining but does is destructive
const spliced = fruits.splice(1, 3);
console.log(spliced);  // new array: ['banana', 'orange', 'grape'] 
console.log(fruits);   // changed array: ['apple', 'mango']
Enter fullscreen mode Exit fullscreen mode

This mockery is painful. "Slice" sounds like cutting or removing, yet it leaves the original array intact, whereas "Splice" sounds like joining things together (like splicing rope or film). Yet, it actually removes elements and mutates the original array. Also, note that the returned arrays are different when both functions are called!

This single-letter difference has caused countless bugs and hours of debugging frustration. It's considered one of the worst naming decisions in JavaScript's history.

Removing Multiple Elements

When removing multiple elements with the same value using splice(), the approach changes significantly because splice() only removes elements at specific index positions and not by value. As an example, we now have orange twice:

const fruits = ['apple', 'orange', 'banana', 'orange', 'grape', 'orange'];

const index = fruits.indexOf('orange');
// indexOf() only finds the FIRST occurrence

fruits.splice(index, 1);  // ['apple', 'banana', 'orange', 'grape', 'orange']
console.log(fruits);      // only removed the first 'orange'
Enter fullscreen mode Exit fullscreen mode

To remove multiple occurrences, the code needs to loop backwards through the array:

const fruits = ['apple', 'orange', 'banana', 'orange', 'grape', 'orange'];

// loop backwards to avoid index shifting issues
for (let i = fruits.length - 1; i >= 0; i--)
{
  if (fruits[i] === 'orange')
  {
    fruits.splice(i, 1);
  }
}

console.log(fruits);  // ['apple', 'banana', 'grape']
Enter fullscreen mode Exit fullscreen mode

Unfortunately, its readability is confusing, so the question arises:
Why loop backwards?

When removing elements while looping forward, the indices shift, and some elements get skipped. Let's see how this would look forward (which is wrong):

const fruits = ['orange', 'orange', 'banana'];

for (let i = 0; i < fruits.length; i++) // normal forward iteration
{
  if (fruits[i] === 'orange')
   {
     fruits.splice(i, 1);
     // after removing index 0, the second 'orange' moves to index 0
     // but i becomes 1, so it gets skipped
  }
}

console.log(fruits);  // still has an orange: ['orange', 'banana']
Enter fullscreen mode Exit fullscreen mode

This complexity is one reason filter() is often the preferred native solution for removing multiple values, though it operates differently (more on that later).

Method 2: The Delete Operator

This one is a trap, and isn't so clear at first glance! Some developers try using the delete operator:

const fruits = ['apple', 'banana', 'orange', 'grape', 'mango'];
const index = fruits.indexOf('orange');

delete fruits[index];

console.log(fruits);        // ['apple', 'banana', empty, 'grape', 'mango']
console.log(fruits.length); // still contains 5 elements
console.log(fruits[2]);     // undefined
Enter fullscreen mode Exit fullscreen mode

As noticed (or not), the delete operator doesn't actually remove the element; it sets that position to undefined, leaving a hole in the array. The length remains the same, but iteration becomes problematic, resulting in what's known as a "sparse array."

Using this method with arrays is best avoided. It lingers from JavaScript's object-oriented roots and often surprises developers with its unexpected behavior.

Method 3: The Filter Function

This alternative, easily promising and for a completely different approach, the filter() creates a new array without the unwanted element:

const fruits   = ['apple', 'banana', 'orange', 'grape', 'mango'];
const filtered = fruits.filter(fruit => (fruit !== 'orange'));
                 // arrow function keeps it simple (low logic)

console.log(filtered);  // ['apple', 'banana', 'grape', 'mango']
console.log(fruits);    // ['apple', 'banana', 'orange', 'grape', 'mango'] 
Enter fullscreen mode Exit fullscreen mode

As noticed, fruits is unchanged!
This method follows an entirely different design. Instead of mutating the original array, it returns a brand new one. This immutable approach is safer and more predictable, but it comes with a performance cost for large arrays.

Another key difference is that filter() removes multiple occurrences of 'orange', not just the first one as previously seen with splice().

const fruits   = ['apple', 'orange', 'banana', 'orange', 'grape'];
const filtered = fruits.filter(fruit => (fruit !== 'orange'));

console.log(filtered);  // ['apple', 'banana', 'grape']
Enter fullscreen mode Exit fullscreen mode

And if one is wondering, how about a mutation desugb of this straightforward filter function? JavaScript doesn't have a built-in mutable version of filter(). If mutation of the original array is needed, then here are some options,

Mutable Option 1: Reassign after filtering

let fruits = ['apple', 'orange', 'banana', 'orange', 'grape'];
fruits     = fruits.filter(fruit => (fruit !== 'orange'));

console.log(fruits);  // ['apple', 'banana', 'grape']
Enter fullscreen mode Exit fullscreen mode

This reassigns the variable but technically creates a new array rather than mutating the original, yet it's readable and straightforward.

Mutable Option 2: Backwards loop with splice()

const fruits = ['apple', 'orange', 'banana', 'orange', 'grape'];

for (let i = fruits.length - 1; i >= 0; i--)
{
  if (fruits[i] === 'orange')
  {
    fruits.splice(i, 1);
  }
}

console.log(fruits);  // ['apple', 'banana', 'grape']
Enter fullscreen mode Exit fullscreen mode

This is truly mutable, as it actually mutates the original array in place. But as previously pointed out, backwards iteration isn't readable at first glance.

Mutable Option 3: Clear and refill

const fruits   = ['apple', 'orange', 'banana', 'orange', 'grape'];
const filtered = fruits.filter(fruit => (fruit !== 'orange'));

fruits.length = 0;         // clear the array
fruits.push(...filtered);  // refill it

console.log(fruits);  // ['apple', 'banana', 'grape']
Enter fullscreen mode Exit fullscreen mode

This is mutable but clunky; its readability is fair, but it requires additional external steps and is unnecessary unless memory becomes the primary constraint.

Additional Methods: The Pop & Shift Functions

Although these additional methods do not remove a specific element, they do, however, remove elements from the end or beginning of an array, which should be recognized:

const fruits = ['apple', 'banana', 'orange', 'grape', 'mango'];

const lastFruit = fruits.pop();
console.log(lastFruit);  // returns the removed  element 'mango'
console.log(fruits);     // ['apple', 'banana', 'orange', 'grape']

const firstFruit = fruits.shift();
console.log(firstFruit); // returns the removed  element  'apple'
console.log(fruits);     // ['banana', 'orange', 'grape']
Enter fullscreen mode Exit fullscreen mode

Notice the inconsistency? Unlike splice(), these methods return the actual element, not an array containing it. Both methods mutate the original array, but their return values follow a different pattern.

The naming is also strange. While pop() makes intuitive sense (like popping something off a stack), shift() is less clear. The term "shift" suggests moving elements around, not removing the first one. Many developers wish it had been called popFront() or removeFirst() instead, as seen in languages like C++ and Swift.

And, so, as a final conclusion:

The filter function always returns a new array, leaving the original unchanged. JavaScript doesn’t have a native method that filters in place while mutating the original array, which is why the backwards loop with the splice function is the go-to solution when true mutation is required.

  • In most cases, the filter() is the best choice.
    • It’s clear, predictable, removes all matching elements in one pass, and avoids the pitfalls of array mutation.
    • The immutable approach also prevents bugs in larger applications where unexpected mutations can cause problems.
  • The backwards loop with splice() is the right tool when working with very large arrays where performance matters.
    • Or when the original array reference must be preserved (for example, when other parts of the code hold references to it).
  • And delete, never use it for arrays.

#HappyCoding

Top comments (0)