Arrays

Objects are built for keyed data: you look things up by name. That covers a huge range of programs, but not everything.

A lot of the time you need order instead. There’s a first item, a second, a third, and the position carries meaning. A list of users. A row of DOM nodes. A queue of messages waiting to be drawn on screen. An object has no good answer for “put this one between those two” — its keys aren’t a sequence, and rearranging them is not something the syntax helps with.

For ordered collections JavaScript gives you a dedicated structure: the Array.

Declaration

There are two ways to make an empty array:

let arr = new Array();
let arr = [];

You’ll reach for the second one almost every time. The square brackets are shorter, and you can seed the array with values right inside them:

let cities = ["Cairo", "Oslo", "Lima"];

Elements are numbered from zero. Grab one by writing its position in square brackets:

let cities = ["Cairo", "Oslo", "Lima"];

alert( cities[0] ); // Cairo
alert( cities[1] ); // Oslo
alert( cities[2] ); // Lima
“Cairo”“Oslo”“Lima”
012
Each value lives in a numbered slot. The first slot is index 0, not 1 — the single most common off-by-one trap for newcomers.

You can overwrite a slot:

cities[2] = 'Perth'; // now ["Cairo", "Oslo", "Perth"]

Or add a fresh one by assigning to the next index:

cities[3] = 'Delhi'; // now ["Cairo", "Oslo", "Perth", "Delhi"]

The number of elements is the array’s length:

let cities = ["Cairo", "Oslo", "Lima"];

alert( cities.length ); // 3

You can also pass the whole array to alert, which shows it as a comma-joined string:

let cities = ["Cairo", "Oslo", "Lima"];

alert( cities ); // Cairo,Oslo,Lima

An array can hold values of any type, mixed freely — strings, numbers, objects, even functions:

// mix of values
let arr = [ 'Cairo', { name: 'Maya' }, true, function() { alert('hello'); } ];

// get the object at index 1 and then show its name
alert( arr[1].name ); // Maya

// get the function at index 3 and run it
arr[3](); // hello

That flexibility comes free because, under the hood, an array is a kind of object. More on that in a moment.

Get last elements with “at”

Say you want the final element.

Some languages let you write cities[-1] for that. JavaScript does not. You’ll get undefined, and the reason is worth understanding: square brackets do a literal property lookup. The value -1 is coerced to the string key "-1", your array has no property under that key, so the lookup comes back empty.

The manual route is to compute the index yourself:

let cities = ["Cairo", "Oslo", "Lima"];

alert( cities[cities.length-1] ); // Lima

It works, but you have to name the array twice, and long expressions like orders[orders.length - 1] get noisy fast.

The shorter form is cities.at(-1):

let cities = ["Cairo", "Oslo", "Lima"];

// same as cities[cities.length-1]
alert( cities.at(-1) ); // Lima

So arr.at(i):

  • behaves exactly like arr[i] when i >= 0.
  • for a negative i, counts back from the end.
arr[i]at(-i)“Cairo”“Oslo”“Lima”012-3-2-1
Positive indexes count from the front; at() also accepts negative indexes that count from the back, so at(-1) is always the last element regardless of length.

Type an index below — including a negative one — and watch how arr[i] and arr.at(i) diverge. For i >= 0 they agree; for a negative i, square brackets return undefined while at counts back from the end.

interactivearr[i] versus arr.at(i)

Methods pop/push, shift/unshift

A queue is one of the most common things you build out of an array. It’s an ordered collection with two operations:

  • push adds an element to the end.
  • shift removes an element from the beginning, so the second element becomes the first.
shift() ←msg1msg2msg3← push()
front is the left end · back is the right end · first in, first out
A queue is first-in, first-out. New items join at the back with push; the oldest item leaves from the front with shift.

You’ll use this constantly. A queue of on-screen notifications is the textbook case: draw the one at the front, shift it off, push new ones onto the back.

There’s a second classic use: the stack. It also has two operations, but both act on the same end:

  • push adds an element to the end.
  • pop removes an element from the end.

Picture a pack of cards. You add to the top and you take from the top:

card 4card 3card 2card 1
push() adds on top ↑pop() removes from top ↑last in, first out
A stack is last-in, first-out. push and pop both work at the top, so the most recently added card is the first one back out.

With a stack, the most recently pushed item comes back first. That’s the LIFO (Last-In-First-Out) rule. A queue is the opposite: FIFO (First-In-First-Out).

JavaScript arrays can act as either. They let you add and remove at both the front and the back. A structure that supports that is called a deque (double-ended queue), and an array is one.

Try it both ways below. Add and remove at either end and watch the array reshape — the log shows what each call returns, so notice that the removers hand back the element while the adders hand back the new length.

interactiveAn array as a deque: work both ends

Methods that work with the end of the array:

pop

Removes the last element and returns it:

let cities = ["Cairo", "Oslo", "Perth"];

alert( cities.pop() ); // remove "Perth" and alert it

alert( cities ); // Cairo, Oslo

Both cities.pop() and cities.at(-1) read the last element, but only pop changes the array — it takes the element out. Reach for at(-1) when you just want to look; reach for pop when you want to remove.

push

Adds an element to the end:

let cities = ["Cairo", "Oslo"];

cities.push("Perth");

alert( cities ); // Cairo, Oslo, Perth

The call cities.push(...) does the same job as cities[cities.length] = ... — it writes into the slot just past the current end.

Methods that work with the beginning of the array:

shift

Removes the first element and returns it:

let cities = ["Cairo", "Oslo", "Perth"];

alert( cities.shift() ); // remove Cairo and alert it

alert( cities ); // Oslo, Perth
unshift

Adds an element to the beginning:

let cities = ["Oslo", "Perth"];

cities.unshift('Cairo');

alert( cities ); // Cairo, Oslo, Perth

Both push and unshift accept more than one element at a time:

let cities = ["Cairo"];

cities.push("Oslo", "Quito");
cities.unshift("Tokyo", "Delhi");

// ["Tokyo", "Delhi", "Cairo", "Oslo", "Quito"]
alert( cities );

Internals

An array is a special kind of object. The square-bracket access arr[0] is the same machinery as obj[key]: arr is the object, and the numbers act as keys.

Arrays extend plain objects with methods tuned for ordered data, plus the auto-maintained length property. Strip those away and it’s still an object underneath.

Recall that there are only eight basic data types (see Data types for the full list). An array isn’t a ninth — it’s an object, and it behaves like one.

That has a concrete consequence: an array is copied by reference, never cloned.

let cities = ["Nome"]

let arr = cities; // copy by reference (two variables reference the same array)

alert( arr === cities ); // true

arr.push("Perth"); // modify the array by reference

alert( cities ); // Nome, Perth - 2 items now

Assigning cities to arr doesn’t duplicate anything. Both names point at one array in memory, so a change made through either name is visible through the other.

citiesarrone array (shared)“Nome”“Perth”
let arr = cities copies the reference, not the data. Pushing through arr mutates the one shared array, and cities sees the new element too.

What makes arrays genuinely special is how the engine lays them out. It tries to keep the elements packed together in one contiguous block of memory, one after another, matching the pictures in this chapter. There are more optimizations layered on top, all aimed at making array operations fast.

Those optimizations hold only while you treat the array as an ordered collection. Start using it like a general-purpose object and they fall apart.

For instance, this is legal:

let cities = []; // make an array

cities[99999] = 5; // assign a property with the index far greater than its length

cities.age = 25; // create a property with an arbitrary name

It works because arrays are objects at heart, and objects take any property you give them.

But the engine notices you’re no longer using it as a tidy ordered list. The array-specific optimizations don’t fit this shape, so they switch off and their benefits vanish.

The main ways to defeat them:

  • Add a non-numeric property like arr.test = 5.
  • Punch holes: set arr[0] and then arr[1000] with nothing in between.
  • Fill in reverse: assign arr[1000], then arr[999], and so on.

Treat arrays as what they’re built for: structures for ordered data, with methods to match. If you find yourself wanting arbitrary string keys, you almost certainly want a plain object {} instead.

Performance

push and pop are fast. shift and unshift are slow. Understanding why keeps you from accidentally putting a slow operation in a tight loop.

Start with shift:

cities.shift(); // take 1 element from the start

Removing the element at index 0 isn’t the whole job. Every other element has to be renumbered so the array stays a proper zero-based sequence.

shift does three things:

  1. Remove the element at index 0.
  2. Slide every remaining element one step left: index 1 becomes 0, 2 becomes 1, and so on.
  3. Update length.
beforeABCD
index0 ←out123
B, C, D each move one slot left ↓
afterBCD
index012
shift removes the front element, then every survivor slides left to a new index. The more elements there are, the more moves it takes.

The more elements in the array, the more of them have to move, and the more memory operations it takes.

unshift has the same problem in reverse. To make room at the front, every existing element must first shuffle right to a higher index.

So what about push and pop? They move nothing. To drop the last element, pop just clears the final slot and shortens length:

cities.pop(); // take 1 element from the end
beforeABCD
D leaves → A, B, C keep their index, length drops from 4 to 3
afterABC·
pop removes the last element and stops. Nothing else changes index, which is why it stays fast no matter how long the array is.

Because pop leaves every other index untouched, it’s fast. push is the same story at the other end.

Loops

The oldest way to walk an array is a for loop over its indexes:

let arr = ["Cairo", "Oslo", "Perth"];

for (let i = 0; i < arr.length; i++) {
  alert( arr[i] );
}

Arrays also support a cleaner form, for..of:

let cities = ["Cairo", "Oslo", "Lima"];

// iterates over array elements
for (let city of cities) {
  alert( city );
}

for..of hands you each value but not its index. Most of the time that’s all you need, and it reads better. When you do want the index alongside the value, arr.entries() pairs them up:

let cities = ["Cairo", "Oslo", "Lima"];

for (let [i, city] of cities.entries()) {
  alert( `${i}: ${city}` ); // 0: Cairo, 1: Oslo, 2: Lima
}

Since arrays are objects, for..in technically works too:

let arr = ["Cairo", "Oslo", "Perth"];

for (let key in arr) {
  alert( arr[key] ); // Cairo, Oslo, Perth
}

Avoid it. There are two real problems:

  1. for..in iterates over all enumerable properties, not just the numeric indexes.

    Browsers and other hosts hand you “array-like” objects that look like arrays — they carry a length and indexed entries, but they may also have extra non-numeric properties and methods you don’t care about. for..in walks those too. The moment you loop over an array-like value, those extras show up in your loop.

  2. for..in is optimized for general objects, not arrays, so it can run 10 to 100 times slower. It’s still fast in absolute terms, and the difference only bites in a bottleneck — but the gap is real.

As a rule, don’t use for..in on arrays.

A word about “length”

The length property updates itself whenever you change the array. Here’s the precise definition, which surprises people: it isn’t the count of values, it’s the greatest numeric index plus one.

So a single element at a large index produces a large length:

let cities = [];
cities[123] = "Cairo";

alert( cities.length ); // 124
empty · indexes 0 … 122“Cairo”1230length = 123 + 1 = 124
length is not a count of stored values — it is (highest index + 1). One element at index 123 makes length 124, even though 123 slots are empty.

You wouldn’t normally build an array this way, but it explains the rule.

The other surprising fact about length is that it’s writable.

Increase it by hand and nothing interesting happens — you just get some empty trailing slots. Decrease it and the array is truncated, permanently:

let arr = [1, 2, 3, 4, 5];

arr.length = 2; // truncate to 2 elements
alert( arr ); // [1, 2]

arr.length = 5; // return length back
alert( arr[3] ); // undefined: the values do not return

Cutting length down discards the elements past the new end for good. Growing it again only adds empty slots; the old values are gone.

Drag the slider to set length directly on one persistent array. Shrink it, then grow it back, and confirm the point for yourself: the recovered slots come back empty, never with their old values.

interactivelength is writable — and shrinking is permanent

That gives you the shortest way to empty an array: arr.length = 0;.

new Array()

There’s one more way to build an array:

let arr = new Array("Cairo", "Perth", "etc");

It’s rare, because [] is shorter. It also hides a trap.

Call new Array with a single number, and instead of an array holding that number, you get an empty array with that length:

let arr = new Array(2); // will it create an array of [2] ?

alert( arr[0] ); // undefined! no elements.

alert( arr.length ); // length 2

So new Array(2) is a two-slot array of holes, not [2]. This is a real footgun, which is exactly why [] is the default recommendation — with square brackets, [2] means what it looks like.

Multidimensional arrays

Array elements can themselves be arrays. That gives you multidimensional structures — a matrix, for example:

let matrix = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
];

alert( matrix[0][1] ); // 2, the second value of the first inner array

matrix[0] selects the first inner array, and [1] selects its second element. You chain the brackets, outer index first.

123456789
row 0 is the top; column 1 is the middle → matrix[0][1] === 2
matrix[0][1]: the first bracket picks row 0, the second picks column 1 within that row, landing on the value 2.

toString

Arrays carry their own toString that returns the elements joined by commas:

let arr = [1, 2, 3];

alert( arr ); // 1,2,3
alert( String(arr) === '1,2,3' ); // true

Now try this:

alert( [] + 1 ); // "1"
alert( [1] + 1 ); // "11"
alert( [1,2] + 1 ); // "1,21"

Arrays have no Symbol.toPrimitive and no usable valueOf; the only primitive conversion they implement is toString. So [] becomes the empty string, [1] becomes "1", and [1,2] becomes "1,2".

When binary + sees a string on either side, it converts the other operand to a string as well. After the array turns into a string, each line above is really:

alert( "" + 1 ); // "1"
alert( "1" + 1 ); // "11"
alert( "1,2" + 1 ); // "1,21"

Don’t compare arrays with ==

Unlike some languages, JavaScript arrays should not be compared with ==.

The operator has no special case for arrays. It treats them like any other object.

Recall the rules:

  • Two objects are equal under == only when both sides are references to the same object.
  • If one side is an object and the other a primitive, the object is converted to a primitive first, as covered in Object to primitive conversion.
  • The pair null and undefined equal each other under == and nothing else.

Strict === is even simpler: it never converts types.

Put those together, and comparing two arrays with == is always false unless the two variables literally reference one and the same array:

alert( [] == [] ); // false
alert( [0] == [0] ); // false

Each literal builds a distinct object, so the references differ and the result is false. == does no element-by-element check.

Comparing an array against a primitive gets weird for the same reason:

alert( 0 == [] ); // true

alert('0' == [] ); // false

Both lines compare a primitive with an array object, so [] is converted to a primitive — the empty string ''. The comparison then continues between primitives, following the rules from Type Conversions:

// after [] was converted to ''
alert( 0 == '' ); // true, as '' becomes converted to number 0

alert('0' == '' ); // false, no type conversion, different strings

So how do you compare arrays?

Don’t use ==. Walk them element by element in a loop, or use the iteration methods from the next chapter.

Summary

An array is a special kind of object built for storing and managing ordered items.

Declaration:

// square brackets (usual)
let arr = [item1, item2...];

// new Array (exceptionally rare)
let arr = new Array(item1, item2...);

Calling new Array(number) creates an array with that length but no elements.

  • length is the array’s length, or more precisely its highest numeric index plus one. Array methods keep it in sync automatically.
  • Shortening length by hand truncates the array, irreversibly.

Getting elements:

  • read by index, like arr[0].
  • or use at(i), which accepts negative indexes. For a negative i it steps back from the end; for i >= 0 it matches arr[i].

You can use an array as a deque with these operations:

  • push(...items) adds items to the end.
  • pop() removes the element from the end and returns it.
  • shift() removes the element from the beginning and returns it.
  • unshift(...items) adds items to the beginning.

Operations on the end (push/pop) are fast; operations on the front (shift/unshift) are slow because everything has to be renumbered.

To loop over elements:

  • for (let i=0; i<arr.length; i++) — fastest, works everywhere.
  • for (let item of arr) — the modern syntax for values.
  • for (let i in arr) — never use.

To compare arrays, don’t use == (or >, <, and the rest). They give arrays no special treatment, handling them as plain objects, which is almost never what you want. Compare element by element with a for..of loop instead.

We’ll keep going with arrays in the next chapter, Array methods, which covers adding, removing, searching, sorting, and transforming.