Home
Nacho's Blog

JS Array method image re-explained

JS Array methods meme

Introduction

We have all probably seen the image on the top where all the array methods are clearly explained in one simple image, right? Well, yes, every JR dev has shared or liked it on LinkedIn. And that's fine, but what gets me is that this image hides one crucial concept in its simplicity.

In order to see the issue, let's first code it!

To start because we are great devs we are going to write a simple loop to get all the methods at once:

const methods = [
  {
    base: new Array(4).fill("🟠"), //["🟠","🟠","🟠","🟠"]
    method: "push",
    args: ["🟢"],
  },
];

methods.forEach(({ method, base, args }) => {
  const res = base[method](...args);
  const outString = `${JSON.stringify(base)}.${method}(${args})`;
  const spacer = new Array(40 - outString.length).fill(" ").join("");
  console.log(`${outString}${spacer}->\t${JSON.stringify(res)}`);
});

Here we are using the fact that classes in JS are once defined an object. Because of that, we can access their methods dynamically with square brackets and arguments can be passed as a spread-ed array, so each item of the array will take place of the positional argument in the function.

But here lays the deception of the image, if we run this code as is, we get this: ["🟠","🟠","🟠","🟠","🟢"].push(🟢) -> 5

Now we can see that our original array has been changed, and our result is a 5.

Explanation

A lot of you already know the explanation. This happens because the push method for arrays is "mutable" this means that it changes the array it's called on. So we think ok, easy, let's just make a simple change and get rid of the problem:

methods.forEach(({ method, base, args }) => {
  const baseCopy = base; // Added this line
  const res = base[method](...args);
  const outString = `${JSON.stringify(baseCopy)}.${method}(${args})`; // and changed the shown array here
  const spacer = new Array(40 - outString.length).fill(" ").join("");
  console.log(`${outString}${spacer}->\t${JSON.stringify(res)}`);
});

And now if we run it, we realize we get the exact same result!

This is because when we declared baseCopy so be base JS takes that literally and just gives both variables the same reference! So when push changed the values of base, then when reading baseCopy we are just reading the exact same array from 2 different place (yes, this is just like pointers because it's the same thing).

So we need to create a brand-new array with the same values we could just spread the array but that just makes a 1-level deep copy, and then it shallows copy (I could explain this in another article). We will just do Array.from() this is a static method from the Array class that creates a new array from any iterable.

const originalBase = Array.from(base);

Now the code finally works, but the result is still 5. So we just change res for base and write the rest of the cases and get the results that will be right, right?!

Not Yet...

not yet meme

Mutable vs Immutable Array Methods

While this code works great for push, shift, unshift, pop and sort. It won't work for the rest of the methods that just returned the original.

This is because some methods are mutable and others are immutable

MutableImmutable
pushflat
shiftmap
unshiftreduce
popfilter
sortconcat
slice
  • Mutable methods will alter the original array
  • Immutable methods create a new array with a new reference

Immutable methods allow for much cleaner code and less surface for bugs as all references are new and the chances of changing one array changing data somewhere else are greatly reduced!

At this point, there are multiple solutions, so we have to think of some of them and discard the ones that won't work:

  1. Add a dictionary to determine which methods are mutable and which aren't and handle the result accordingly.
  2. Add a property to our method's array that indicates if it's mutable or not.
  3. Have 2 distinct loops. This is too repetitive codewise, as the code difference is a little too short.

In my opinion, both 1 and 2 are great. The first option has the edge in my opinion, as it allows saving data on the list at the cost of the check the function type. Option 2 is acceptable, but it required manually adding data to the list, so it's only viable for proof of concepts or very short lists.

["🟠","🟠","🟠","🟠"].push(🟢)                    ->    ["🟠","🟠","🟠","🟠","🟢"]
["🟠","🟠","🟠","🟠"].unshift(🟢)                 ->    ["🟢","🟠","🟠","🟠","🟠"]
["🟠","🟠","🟠","🟢"].pop()                       ->    ["🟠","🟠","🟠"]
["🟢","🟠","🟠","🟠"].shift()                     ->    ["🟠","🟠","🟠"]
["🟠","🟢","🟢","🟠"].filter((d) => d === "🟢")   ->    ["🟢","🟢"]
["🟠","🟣","🔴","🟢"].join(-)                     ->    "🟠-🟣-🔴-🟢"
["🟠","🟣"].concat(🔴,🟢)                         ->    ["🟠","🟣","🔴","🟢"]
["🟠","🟣",["🔴","🟢"]].flat()                    ->    ["🟠","🟣","🔴","🟢"]
["🟠","🟣","🔴","🟢"].slice(1,3)                  ->    ["🟣","🔴"]
// bonus negative args in slice
["🟠","🟣","🔴","🟢"].slice(0,-2)                 ->    ["🟠","🟣"]
["🟠","🟣","🔴","🟢"].slice(2)                    ->    ["🔴","🟢"]

Now It does work, even though I did ramble a bit. I do think that knowing the difference in array methods is quite important, specially since shift, push and unshift can be replaced with spread syntax, allowing for immutable implementations.

Thanks for reading.

Published: Monday, Oct 23, 2023
Privacy Policy© 2023 Ignacio Degregori. All rights Reserved.