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
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]wheni >= 0. - for a negative
i, counts back from the end.
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.
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:
pushadds an element to the end.shiftremoves an element from the beginning, so the second element becomes the first.
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:
pushadds an element to the end.popremoves an element from the end.
Picture a pack of cards. You add to the top and you take from the top:
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.
Methods that work with the end of the array:
popRemoves the last element and returns it:
let cities = ["Cairo", "Oslo", "Perth"]; alert( cities.pop() ); // remove "Perth" and alert it alert( cities ); // Cairo, OsloBoth
cities.pop()andcities.at(-1)read the last element, but onlypopchanges the array — it takes the element out. Reach forat(-1)when you just want to look; reach forpopwhen you want to remove.
pushAdds an element to the end:
let cities = ["Cairo", "Oslo"]; cities.push("Perth"); alert( cities ); // Cairo, Oslo, PerthThe call
cities.push(...)does the same job ascities[cities.length] = ...— it writes into the slot just past the current end.
Methods that work with the beginning of the array:
shiftRemoves the first element and returns it:
let cities = ["Cairo", "Oslo", "Perth"]; alert( cities.shift() ); // remove Cairo and alert it alert( cities ); // Oslo, Perth
unshiftAdds 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.
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 thenarr[1000]with nothing in between. - Fill in reverse: assign
arr[1000], thenarr[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:
- Remove the element at index
0. - Slide every remaining element one step left: index
1becomes0,2becomes1, and so on. - Update
length.
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
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:
-
for..initerates 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
lengthand indexed entries, but they may also have extra non-numeric properties and methods you don’t care about.for..inwalks those too. The moment you loop over an array-like value, those extras show up in your loop. -
for..inis 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
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.
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.
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
nullandundefinedequal 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.
lengthis the array’s length, or more precisely its highest numeric index plus one. Array methods keep it in sync automatically.- Shortening
lengthby hand truncates the array, irreversibly.
Getting elements:
- read by index, like
arr[0]. - or use
at(i), which accepts negative indexes. For a negativeiit steps back from the end; fori >= 0it matchesarr[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.