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
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.
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
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"
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
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
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
Or in Python.
from array import*
a = array("i", [1, 2, 3])
b = array("i", [1, 2, 3])
print(a == b) # True
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"
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
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])
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"
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
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)
Hello, I use the
structuredClone
method.developer.mozilla.org/en-US/docs/W...
Nice, I didn't know that this is a thing!