Arrays are objects in JavaScript

Arrays are objects in JavaScript

Arrays are objects

First, let's confirm that arrays are objects.

This is key to understanding how they work:

const arr = ['a', 'b', 'c']

// Under the hood, it's like this:
const arrAsObject = {
    '0': 'a',
    '1': 'b',
    '2': 'c',
    length: 3
}

// We can prove this
typeof []  // "object"
[] instanceof Object  // true

Arrays special behaviors

Arrays have some special behaviors that regular objects don't:

// 1. Length is automatically managed
const arr = ["a"];
arr[1] = "b";
console.log(arr.length); // 2

// 2. Indices are converted to strings
arr["2"] = "c"; // Same as arr[2] = 'c'
console.log(arr); // ['a', 'b', 'c']

// 3. You can add non-index properties
arr.customProp = "hello";
console.log(arr.customProp); // "hello"
console.log(arr.length); // Still 3 (properties don't count)

Array length

The length property is special:

const arr = ["a", "b", "c"];
arr.length = 1; // Truncates the array!
console.log(arr); // ['a']

arr.length = 3; // Extends with empty slots
console.log(arr); // ['a', empty × 2]

Sparse arrays

Sparse arrays (arrays with holes) are possible because of this object nature:

const sparse = [];
sparse[0] = "first";
sparse[2] = "third";
console.log(sparse); // ['first', empty, 'third']
console.log(sparse.length); // 3

// Empty slots are different from undefined
sparse[1] = undefined;
console.log(0 in sparse); // true
console.log(1 in sparse); // true
console.log(3 in sparse); // false

Prototype

Array methods exist on Array.prototype:

const arr = ["a", "b"];
console.log(arr.__proto__ === Array.prototype); // true
console.log(arr.__proto__.__proto__ === Object.prototype); // true

This object-like nature affects performance:

// V8 can optimize sequential, numeric properties
const optimized = ["a", "b", "c"];

// But mixing types or adding non-index properties can deoptimize
optimized.prop = "bad";
optimized[0.5] = "worse"; // Non-integer index

If you wanna learn more about JS internals, read: How to write performant JavaScript code.

And finally, this is why array-like objects exist.

They mimic this structure:

const arrayLike = {
  0: "a",
  1: "b",
  length: 2,
};

// We can convert it to a real array
const realArray = Array.from(arrayLike);
// Or
const alsoReal = [...arrayLike]; // If it's iterable

This object nature of arrays explains many JavaScript behaviors:

  • Why we can add properties to arrays

  • Why array lengths can be manipulated

  • Why sparse arrays are possible

  • Why array-like objects work

  • How prototype inheritance works for arrays

The JavaScript engine optimizes common array patterns, but knowing that arrays are objects helps us understand their flexibility and limits.

More on the JS engine

// V8 has different ways it stores arrays internally based on how we use them

// 1. The fastest: packed elements
const packed = [1, 2, 3]  // Sequential, same type (all numbers)
// V8 can store this in continuous memory blocks
// Direct memory access, very fast

// 2. Still fast: holey elements
const holey = [1, , 3]    // Has a hole
// V8 needs extra checks when accessing elements
// Must check if element exists

// 3. Slower: mixed elements
const mixed = [1, "string", 3]  // Different types
// V8 can't optimize as well
// Must handle different types

// Even slower patterns:
const bad1 = []
bad1[1000] = 1  // Creates sparse array
// V8 switches to dictionary mode (hash table)

const bad2 = [1, 2, 3]
bad2.foo = 'bar'  // Non-index property
// Can force V8 to switch to a slower mode

V8 has specific array modes:

  • PACKED_SMI: All small integers

  • PACKED_DOUBLE: All doubles

  • PACKED_ELEMENTS: All same type

  • HOLEY_SMI: Small integers with holes

  • HOLEY_ELEMENTS: Mixed with holes

Every time we do something that doesn't match the current mode, V8 has to switch to a more general (slower) mode. It can't switch back to a faster mode.

const arr = [1, 2, 3]  // Starts as PACKED_SMI
arr.push(4.5)          // Transitions to PACKED_DOUBLE
arr.push("string")     // Transitions to PACKED_ELEMENTS
delete arr[1]          // Transitions to HOLEY_ELEMENTS

This is why performance-critical code often:

  • Sets the array size in advance

  • Keeps arrays full (no holes)

  • Uses the same types

  • Avoids adding properties that aren't indexed