DEV Community

Cover image for Fun Facts about JavaScript Arrays
Cooty
Cooty

Posted on • Edited on

Fun Facts about JavaScript Arrays

JavaScript has a lot of quirks and weird parts, some of these impact the daily work of the average web dev, while some of it belongs to the sort of dark arts, which's only practical application is to create tricky interview questions. Nonetheless, it's worth knowing these oddities in a language.

Today I was thinking to cover some lesser known behaviors involving JavaScripts's arrays, so let's dive in.

Arrays are actually Objects

We know that curly braces ({}) stand for objects and square brackets ([]) stand for arrays, right?
Well it turns out that arrays are just a special kind of object. You can prove this by checking the type of an array.

const dogBreeds = ["Labrador", "Poodle", "German Shepherd"]
console.log(typeof dogBreeds) // object
Enter fullscreen mode Exit fullscreen mode

Or an even better option is to log out an array in your browser's console and see that the [[Protype]] points to the built in Array object.

Screenshot from a JavaScript console showing an Array prototype

Observe all the built in array methods you know and love being listed on the prototype object (the faded color signals that they are read-only).

If you want to know that some unknown piece of data is indeed an array, the easiest way is to use the isArray static method on the Array prototype.

const somethingThatMightBeAnArray = ["dog", 42, {foo: 'bar'}]

console.log(Array.isArray(somethingThatMightBeAnArray)) // true

const somethingThatsNotAnArray = { id: 1 }

console.log(Array.isArray(somethingThatsNotAnArray)) // false
Enter fullscreen mode Exit fullscreen mode

Now when your accessing an item inside an array with the bracket notation (meaning myArray[0]), you are actually accessing a key of the object, but because object keys can't be integers, under the hood JS actually converts that number you put in between the brackets to a string. So actually myArray[0] and myArray["0"] would yield the same result.

Also note that dot notation won't work, so myArray.0 will fail just as trying to use dot notation to get the value of an object key that uses a "-" character or an integer.

const myObject = {"my-key": "A", "20": "B"};
console.log(myObject.20) // Uncaught SyntaxError: missing ) after argument list

console.log(myObject["20"]) // "B"
Enter fullscreen mode Exit fullscreen mode

Speaking of keys, - or as we call them in this case array indices - there's another odd behavior that a lot of people don't know about. Namely that if you set an item with an index that's bigger than the currently available last index, JS will "fill up" the items leading up to that index with empty values. Don't believe me? Try it for yourself!

const dogBreeds = ["Labrador", "Poodle", "German Shepherd"]
dogBreeds[100] = "Vizsla"
console.log(dogBreeds.length) // 101
Enter fullscreen mode Exit fullscreen mode

Observe that all the 97 items between the indices 2 and 100 will give undefined.

I don't really know the exact reason why this is, but if I had to guess it has to do with something relating to the fact that under the hood, JavaScript engines store arrays in some other data structure.

The practical implication is that if you want to dynamically insert new items to an array you should always use the built in push method instead of the bracket notation if you want to be safe and keep the consistency of your indexes.

Arrays are stored by Reference

There's something that tips off beginners and sometimes even experienced programmers coming from other languages.

When trying to compare arrays that look the same you run into a surprise.

const arr1 = [1, 2, 3]
const arr2 = [1, 2, 3]

console.log(arr1 === arr2); // false
Enter fullscreen mode Exit fullscreen mode

This seems odd…

In other programming languages this would be true. Say in PHP.

$arr1 = [1, 2, 3];
$arr2 = [1, 2, 3];

var_dump($arr1 === $arr2); // true
Enter fullscreen mode Exit fullscreen mode

Or in Python.

from array import*

a = array("i", [1, 2, 3])
b = array("i", [1, 2, 3])

print(a == b) # True
Enter fullscreen mode Exit fullscreen mode

So what's going on here? If you understand that arrays are actually objects, then this whole thing will make sense.

In JavaScript objects are stored by reference and not by value like primitive data types. So that means that even if the values (and keys) are the same, internally they are stored on a different chunk of memory, so when comparing them for equality they will always return false, unless you compare the same object reference with itself.

Now again this has a lot of real world implications. For instance if you just reassign an existing array to a new variable and then start mutating the values you are in for some surprises.

const dogBreeds = ["Poodle", "Labrador"]
const copiedDogBreeds = dogBreeds

copiedDogBreeds.push("Labradoodle")

console.log(copiedDogBreeds[2]) // "Labradoodle"
console.log(dogBreeds[2]) // "Labradoodle"
Enter fullscreen mode Exit fullscreen mode

You would initially expect that you've only modified the copiedDogBreeds array, but as you can see our original array also got "Labradoodle" as its third item.

To fix this you'll need to make an actual copy of the object. One way is to use the slice array method without any arguments

const dogBreeds = ["Poodle", "Labrador"]
const copiedDogBreeds = dogBreeds.slice()

copiedDogBreeds.push("Labradoodle")

console.log(copiedDogBreeds[2]) // "Labradoodle"
console.log(dogBreeds[2]) // undefined
Enter fullscreen mode Exit fullscreen mode

Or better yet use the spread syntax!

const dogBreeds = ["Poodle", "Labrador"]
const copiedDogBreeds = [...dogBreeds]

copiedDogBreeds.push("Labradoodle")

console.log(copiedDogBreeds[2]) // "Labradoodle"
console.log(dogBreeds[2])
Enter fullscreen mode Exit fullscreen mode

But wait! There's more…

So you would think that the above examples create a totally new JavaScript array object, right? Wrong! Both of these methods only create a so-called shallow copy, which still shares the same underlying values of the source object.

Now the practical consequence of this is that if we modify any nested value then we will still overwrite the values in the original array.

const numbersAndLetters = [[1, 2, 3], ["a", "b", "c"]]
const copiedNumbersAndLetters = [...numbersAndLetters]

copiedNumbersAndLetters[1][0] = "A"

console.log(numbersAndLetters[1][0]) // "A"
console.log(copiedNumbersAndLetters[1][0]) // "A"
Enter fullscreen mode Exit fullscreen mode

The only way around this is to make a deep copy, which is - you guessed it - a totally new array object with no connection whatsoever to the original.
The easiest way to do this is to serialize it to a JSON string then parse it back into a JavaScript object.

const numbersAndLetters = [[1, 2, 3], ["a", "b", "c"]]
const copiedNumbersAndLetters = JSON.parse(JSON.stringify(numbersAndLetters))

copiedNumbersAndLetters[1][0] = "A"

console.log(numbersAndLetters[1][0]) // a
console.log(copiedNumbersAndLetters[1][0]) // A

Enter fullscreen mode Exit fullscreen mode

Summary

In essence you need to remember that arrays in JavaScript are actually objects with the indices as their keys and as objects they are stored by reference.

I hope you've learned a thing or two while reading this, cause I sure did while writing it.

Top comments (2)

Collapse
 
steve_fallet profile image
Steve Fallet • Edited

Hello, I use the structuredClone method.

developer.mozilla.org/en-US/docs/W...

const numbersAndLetters = [[1, 2, 3], ["a", "b", "c"]]
const copiedNumbersAndLetters = structuredClone(numbersAndLetters)

copiedNumbersAndLetters[1][0] = "A"

console.log(numbersAndLetters[1][0]) // "a"
console.log(copiedNumbersAndLetters[1][0]) // "A"
Enter fullscreen mode Exit fullscreen mode
Collapse
 
cooty profile image
Cooty

Nice, I didn't know that this is a thing!