JavaScript Arrays — The Basics
Build a real mental model of JavaScript arrays: how indexing and length work, why arrays are shared by reference, how iteration leads to map/filter/reduce, and the sharp edge of sparse arrays.
Introduction
An array is an ordered collection of values that lives in a single variable. Instead of juggling first, second and third as separate names, you keep the whole sequence together and reach any element by its position. That position is called the index, and — this trips up almost everyone at first — it counts from 0, not 1. The first element sits at index 0, the second at index 1, and so on up the row.
Why does the language need a dedicated structure for this? Because most real programs deal in *lists*: the items in a cart, the rows returned from a database, the characters in a name, the scores in a game. Without arrays you would invent a new variable for every value and instantly lose the ability to count, loop over, or transform them as a group. An array turns *many related values* into one thing you can pass around, measure, and reshape.
The mental model to hold onto is a row of numbered slots. The slots are ordered, you can read or overwrite any of them by number, and the row can grow or shrink while the program runs. By the end of this guide you'll be able to reason about how an array stores its elements, how its length stays in step with them, why copying needs care, and how the transforming methods all fit together.
const fruits = ['apple', 'pear', 'plum'];
// 0 1 2 ← indexes
Lessons
1. Indexing: reaching elements by position
Every element in an array has an index — its numeric position, starting at 0. You read or replace an element by putting its index in square brackets. The single most common beginner surprise is that the *last* element is at index length - 1, not length, precisely because counting starts at zero. Reading an index that doesn't exist is not an error in JavaScript: it quietly returns undefined, which is why a typo in an index shows up as a mysterious undefined far away rather than a crash at the point of the mistake.
const fruits = ['apple', 'pear', 'plum'];
fruits[0]; // 'apple' (first)
fruits[fruits.length - 1]; // 'plum' (last)
fruits.at(-1); // 'plum' (last, counting from the end)
fruits[10]; // undefined — no error for a missing index
Because a missing index is silent, guard with length or a membership check when the position might not be there. Treat undefined as the array telling you "nothing lives at that slot," not as a value you put there yourself.
2. Length: a live count, not a stored label
It is tempting to think of length as a number the array remembers and you occasionally update. It is the opposite: length is always one greater than the highest index in use, and the engine keeps it in sync for you. Add an element and length grows; remove one and it shrinks. You can even assign to length directly — setting it smaller truncates the array, dropping the elements beyond the new end. This tight coupling between length and the indices is what makes an array feel like a single, coherent row rather than a loose bag of numbered variables.
const nums = [1, 2, 3];
console.log(nums.length); // 3
nums.push(4);
console.log(nums.length); // 4 — grew automatically
nums.length = 2;
console.log(nums); // [1, 2] — truncated by setting length
So length is a *derived* fact about the array's contents, not a separate setting. Once that clicks, adding, removing and looping all stop feeling like special cases — they are just changes to the row, and length follows.
3. Growing and shrinking from either end
Four methods change the length of an array by adding or removing a single element, and the only thing to remember is *which end* each one touches. Adding or removing at the end is cheap. Adding or removing at the start has to renumber every remaining element, because element 1 must become element 0, and so on — worth knowing when a list gets large.
| Method | What it does | Returns |
|---|---|---|
push(x) | Add to the end | the new length |
pop() | Remove from the end | the removed element |
unshift(x) | Add to the start (reindexes the rest) | the new length |
shift() | Remove from the start (reindexes the rest) | the removed element |
const stack = ['a', 'b'];
stack.push('c'); // ['a', 'b', 'c'] → returns 3
stack.pop(); // returns 'c' → ['a', 'b']
stack.unshift('z'); // ['z', 'a', 'b'] → returns 3
Notice each of these mutates the array in place. That is fine — until the array is shared, which is the next and most important idea.
4. Arrays are shared by reference
An array variable does not hold the array itself; it holds a reference to it — a pointer to one row of slots living in memory. When you assign one array variable to another, you copy the pointer, not the row. Both names now refer to the *same* array, so a change made through one name is visible through the other. This single fact is behind a large share of confusing JavaScript bugs: you "copied" a list, changed the copy, and the original changed too.
const original = [1, 2];
const alias = original; // same array, second name
alias.push(3);
console.log(original); // [1, 2, 3] — changed through 'alias'
console.log(alias === original); // true — one array, two references
To get an array you can change without disturbing the source, make a real copy. Spreading ([...arr]) is the usual way. Keep in mind it is a shallow copy: the outer row is new, but any objects *inside* it are still shared references.
const source = ['apple', 'pear'];
const copy = [...source];
copy.push('kiwi');
console.log(source); // ['apple', 'pear'] — untouched
console.log(copy); // ['apple', 'pear', 'kiwi']
5. Iterating: from "do this to each" to "describe the change"
To iterate is to visit each element in turn. The plainest way is for...of, which hands you the values one at a time without any index bookkeeping. It is perfect when you want to *do* something for each element — log it, send it, draw it — and don't need a result back.
const fruits = ['apple', 'pear', 'plum'];
for (const fruit of fruits) {
console.log(fruit);
}
// apple → pear → plum
But the moment you find yourself building a *new* array while you iterate — collecting transformed values, or keeping only some — there is a clearer way to express it. Instead of writing the loop and managing the new array by hand, you describe the transformation and let the array method run the loop for you. That shift in thinking is what the composition methods are for.
6. Composition: map, filter and reduce
Three methods cover the vast majority of array work, and they share two properties that make them easy to trust: each one returns a new array or value, and each leaves the original untouched. Because they return fresh results, they chain — the output of one flows straight into the next, so a pipeline reads top-to-bottom like a sentence describing what happens to the data.
map()— turn every element into a new one; the result is the same length as the input.filter()— keep only the elements that pass a test; the result is shorter or equal.reduce()— fold the whole array down into a single value, such as a sum or a lookup object.
You don't need to master all three at once. Reach for map when you want a transformed list of the same length, filter when you want a subset, and reduce when you want one value out of many. The dedicated reference pages above cover each method's rules and edge cases in full.
7. Sparse arrays and holes (the sharp edge)
JavaScript lets an array have gaps. Skip an index while assigning, or leave a comma empty in a literal, and you get a sparse array — one whose length counts positions that were never actually filled. A hole is not the same as an element holding undefined: the slot simply doesn't exist, and several methods (map, forEach) *skip* holes entirely rather than visiting them. This is a genuine footgun, and the reason to know about it is so you recognise the symptom rather than reproduce it.
const holes = [1, , 3]; // note the empty middle slot
console.log(holes.length); // 3 — the hole still counts
console.log(holes[1]); // undefined — but the slot doesn't really exist
holes.map(x => x * 2); // [2, <hole>, 6] — map skips the hole
The practical advice is short: don't create holes on purpose. Build arrays with push, literals without gaps, or the composition methods, and you'll never trip over sparseness — but now you'll recognise it if a library or older code hands you one.
That's the whole model. An array is a row of numbered slots; length tracks the slots; the variable holds a reference, not the row; you iterate to read and compose to transform; and holes are the one edge to steer around. Everything else — every method, every recipe — is a detail hung on that frame.
Practice
Check your model before moving on. Given const nums = [1, 2, 3], what does nums.length = 1 leave in nums, and why? (Answer: [1] — assigning a smaller length truncates the row.)
And a reference-semantics check: if const b = a and you call b.push(9), what happens to a? (Answer: it also ends in 9 — a and b are two names for one array.) Now experiment for real — change the numbers, add a filter, watch what stays untouched:
Next steps
You have the mental model — now put it to work. Browse runnable JavaScript array examples for copy-paste snippets, follow how to sort an array for a common task done correctly, or read the focused references for map(), filter() and reduce(). Every snippet on this site runs in the online JavaScript compiler or the full editor.
Frequently asked questions
How do I get the last element of an array?
array[array.length - 1], or the modern array.at(-1), which reads from the end directly. The last index is length - 1 because indexing starts at 0.Why did changing my copied array also change the original?
b = a copies the reference, not the array — both names point at the same row of slots. To get an independent array, copy it with [...a] (a shallow copy) before changing it.What's the difference between an array and an object?
length and list methods like push/map/filter. An object is an unordered collection accessed by named keys. Use an array for a sequence of similar items, an object for a labelled record.Why doesn't reading a missing index throw an error?
undefined for an out-of-range read rather than throwing, so a bad index surfaces as a stray undefined later. Guard with length or includes() when the position might not exist.What is a sparse array?
length that were never filled. Holes differ from undefined values and are skipped by methods like map and forEach. Avoid creating them; build arrays with push, gap-free literals, or the composition methods.