Fun Facts about JavaScript Arrays
Tags: javascript, programming, fundamentals, arrays
Published: 2022-12-01 20:08:13

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.

```javascript
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]]`](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object_prototypes) points to the built in [`Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) object.

![Screenshot from a JavaScript console showing an Array prototype](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uqj7b0camvjlwwhcypao.jpg)

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.

```javascript
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.

```javascript
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!

```javascript
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`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/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.

```javascript
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.

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

var_dump($arr1 === $arr2); // true
```

Or in Python.

```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](https://en.wikipedia.org/wiki/Evaluation_strategy#Call_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`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_with_Objects#comparing_objects), 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.

```javascript
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"`](https://en.wikipedia.org/wiki/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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice) array method without any arguments

```javascript
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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax)!

```javascript
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](https://developer.mozilla.org/en-US/docs/Glossary/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.

```javascript
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](https://developer.mozilla.org/en-US/docs/Glossary/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.

```javascript
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.