Skip to content

Arrays

docs.scrimba.com

You have a shopping cart, a list of scores, the names of everyone in a group chat. One variable holds one value, so storing a list this way means a pile of separate names: price1, price2, price3, and no clean way to loop over them or ask how many there are. An array is the fix. It holds an ordered list of values under a single name, and it knows its own length, so you can add to it, read any item, and walk the whole thing in a few lines.

Making and reading an array

An array is an ordered list of values. You write one with square brackets, with the values separated by commas:

js
const prices = [4, 9, 2, 7];

That is one variable, prices, holding four numbers. To read a single value you use its position, called its index, in square brackets. The catch that trips up almost everyone at first: counting starts at zero, not one.

js
const names = ["Mara", "Kwame", "Yuki"];
console.log(names[0]); // Mara
console.log(names[1]); // Kwame

To ask how many items an array has, use .length:

js
console.log(names.length); // 3

Because counting starts at zero, the last item sits at length - 1, not at length. So the last name here is names[2], and names[3] would be off the end.

An array is an ordered collection of values under one name. You create one with a square-bracket literal, and you read any item by its index, its position counted from zero:

js
const prices = [4, 9, 2, 7];
console.log(prices[0]); // 4
console.log(prices[2]); // 2

Zero-based indexing is the source of most early off-by-one bugs, so it is worth fixing in your head now: the first item is at index 0, and the last is at length - 1.

js
console.log(prices.length); // 4
console.log(prices[prices.length - 1]); // 7, the last item

Reading an index that does not exist does not throw an error. prices[10] gives you undefined, which is JavaScript's way of saying "there is nothing here". That quiet result is convenient and occasionally a trap, because a typo in an index gives you undefined rather than a loud failure.

An array is an ordered, index-addressed collection created with a bracket literal. Items are read by index counted from zero, and .length reports the count:

js
const prices = [4, 9, 2, 7];
console.log(prices[0]); // 4
console.log(prices.length); // 4
console.log(prices[prices.length - 1]); // 7, the last item

Two properties are worth naming precisely. First, .length is not a fixed size you declared up front; it tracks the highest index in use and updates as you add and remove items. Second, an out-of-range read returns undefined rather than throwing, so an off-by-one error surfaces later as undefined flowing through your logic, not as an exception at the point of the mistake. When you want the failure to be loud, guard the index against length yourself.

Arrays can hold mixed value types ([1, "two", true]), but a working codebase almost always keeps one array to one kind of thing, because every piece of code that reads the array then knows what it is getting. Consistency here is a readability decision, not a language rule.

JunoMaking and reading an array An array is a list of values in square brackets under one name. Read an item by its position with names[0], and remember counting starts at zero, so the first item is [0], not [1]. That zero threw me for a week, so check it whenever your list is off by one. Use .length to count the items, and the last one lives at length - 1.
JunoMaking and reading an array An array holds an ordered list under one name, and you read items by index counted from zero. The last item is at length - 1, which is where most early off-by-one bugs come from. Reading past the end returns undefined instead of erroring, so a typo'd index goes quiet rather than loud. Keep that in mind when a value shows up as undefined for no clear reason.
JunoMaking and reading an array Bracket literal, zero-based index, and .length that tracks the highest index in use rather than a size you declared. An out-of-range read hands back undefined instead of throwing, so an off-by-one error travels downstream before it bites. Guard the index against length yourself when you want that failure to be loud. And keep one array to one type of thing, not because the language forces it, but because every reader of the array then knows what it holds.

Changing an array

Arrays are made to change. The two you reach for most add or remove at the end: push adds an item, pop removes the last one.

js
const cart = ["apples", "bread"];
cart.push("milk");
console.log(cart); // ["apples", "bread", "milk"]
cart.pop();
console.log(cart); // ["apples", "bread"]

The matching pair works at the start: unshift adds to the front, shift removes the first item.

js
cart.unshift("eggs");
console.log(cart); // ["eggs", "apples", "bread"]

You can also replace an item directly by assigning to its index:

js
cart[0] = "bananas";
console.log(cart); // ["bananas", "apples", "bread"]

To find something, includes tells you yes or no, and indexOf tells you where it is (or -1 if it is not there):

js
console.log(cart.includes("apples")); // true
console.log(cart.indexOf("bread")); // 2

One thing worth knowing early: a const array can still be changed. const stops you reassigning the name cart to a whole new array, but the list it points at is still open for push, pop, and index assignment.

Arrays are built to be modified in place. Two pairs of methods do most of the work, one at each end:

js
const cart = ["apples", "bread"];
cart.push("milk"); // add to the end
cart.pop(); // remove from the end
cart.unshift("eggs"); // add to the start
cart.shift(); // remove from the start

push and pop are the cheap, common pair; you reach for them constantly. shift and unshift touch the front, which means every other item has to move up or down a slot, so on a large array they do more work. Prefer the end when the order is yours to choose.

You can overwrite any position by assigning to its index, and you can find items two ways:

js
const names = ["Mara", "Kwame", "Yuki"];
names[1] = "Priya";
console.log(names.indexOf("Yuki")); // 2, or -1 if absent
console.log(names.includes("Mara")); // true

Reach for includes when you only care whether something is present, and indexOf when you need the position too. Both compare with ===, so indexOf("2") will not find the number 2.

A const array is still fully mutable: const locks the name, not the contents. You cannot point cart at a new array, but you can push into the one it holds all day.

Arrays are mutable, and the core methods change the array in place rather than returning a fresh one. push/pop operate on the end, shift/unshift on the front:

js
const cart = ["apples", "bread"];
cart.push("milk"); // returns the new length, mutates cart
const removed = cart.pop(); // returns the removed item, mutates cart

The end and the front are not equal in cost. push and pop touch a single slot. shift and unshift reindex every remaining element, which is linear work on the array's length, so a hot loop that repeatedly shifts a large array is quietly doing far more than it looks. When order is under your control, keep the churn at the end.

Assigning to an index writes in place, and searching splits by need: includes for presence, indexOf for the position (with -1 for absent). Both use strict equality (===), so type matters:

js
const ids = [1, 2, 3];
console.log(ids.indexOf("2")); // -1, string does not match number
console.log(ids.includes(2)); // true

The const point is worth stating exactly, because it is the first place the reference model shows through: const binds the name to one array and forbids rebinding it, but says nothing about the array's contents. cart.push(...) mutates the object the name points at, which is a different operation from cart = [...]. That distinction is the whole subject of the next section.

JunoChanging an arraypush and pop add and remove at the end; unshift and shift do the same at the start. Assign to an index like cart[0] = "bananas" to replace an item, and use includes or indexOf to find one. The part that surprised me: a const array can still change, because const only stops you swapping the whole array for a new one.
JunoChanging an array Two pairs cover most edits: push/pop at the end, shift/unshift at the start. The front pair moves every other item over, so favour the end when the order is yours. includes checks presence, indexOf gives the position, and both match with ===, so "2" will not find the number 2. And const locks the name, never the contents, so you can push into a const array all day.
JunoChanging an array These methods mutate in place: push/pop touch one slot, shift/unshift reindex the whole array, so a loop that keeps shifting a big array is doing linear work each time. includes and indexOf both use ===, so a string index will not match a number. And const only forbids rebinding the name; cart.push() mutating the array is a different thing from cart = []. That gap is exactly the reference story coming up next.

Looping over an array

Once you have a list, you usually want to do something with every item. The cleanest way to read through an array is for...of, which hands you one item at a time:

js
const prices = [4, 9, 2, 7];
for (const price of prices) {
  console.log(price);
}
// 4
// 9
// 2
// 7

You do not manage an index or a counter. for...of gives you each value in order, and you name it (price) however you like. If you have met loops already, this is the friendliest one for arrays.

There is also forEach, which runs a small function once for each item:

js
const names = ["Mara", "Kwame", "Yuki"];
names.forEach(function (name) {
  console.log("Hi " + name);
});

Both walk the whole array. Reach for for...of when you want to read cleanly, and you will meet forEach and its cousins again when you learn to transform arrays.

The modern default for reading an array is for...of. It gives you each value directly, with no index to manage and no chance of an off-by-one slip:

js
const prices = [4, 9, 2, 7];
let total = 0;
for (const price of prices) {
  total = total + price;
}
console.log(total); // 22

Use for...of when you want the values in order and clean code. If you need the index too (say, to number a list), the classic counting for loop or the second argument of forEach gives it to you.

forEach runs a callback, a function you hand it, once per item:

js
const names = ["Mara", "Kwame", "Yuki"];
names.forEach((name) => {
  console.log("Hi " + name);
});

forEach reads well for a side effect per item, like logging. What it cannot do is break out early, so when you need to stop partway, use for...of. Building a new array out of an old one is a different job with its own tools, covered in array methods.

For a plain read, for...of is the idiomatic choice: it iterates values directly, sidesteps index arithmetic, and supports break and continue:

js
const prices = [4, 9, 2, 7];
let total = 0;
for (const price of prices) {
  if (price > 8) continue;
  total = total + price;
}
console.log(total); // 13, skipped the 9

forEach takes a callback per element and reads cleanly for side effects, but it comes with two constraints worth knowing before you standardise on it. It cannot break, so it always runs to the end, and it ignores a returned value, so it is the wrong tool when you are trying to produce a result. For that, the transforming methods (map, filter, reduce) exist, and array methods is where they belong.

js
const names = ["Mara", "Kwame", "Yuki"];
names.forEach((name, index) => {
  console.log(index + ": " + name);
});

The callback's second argument is the index, which is how you recover position without a manual counter. The rule of thumb: for...of when you need control flow or are accumulating into an outside variable, forEach when a per-item side effect reads more clearly, and a transforming method when you want a new array back rather than a mutation or a log.

JunoLooping over an arrayfor...of is the clean way to walk an array: it hands you each item in order and you never touch an index. forEach runs a little function once per item, which is handy too. Both go through the whole list, so pick for...of when you want to read straight through it. You will see more array tools once you get to transforming them.
JunoLooping over an arrayfor...of is the default for reading an array: values in order, no index to manage, and break works when you need to stop early. forEach runs a callback per item and reads nicely for side effects like logging, but it cannot break, so it always runs to the end. Building a new array from an old one is a separate job with its own methods. When in doubt, for...of.
JunoLooping over an arrayfor...of iterates values directly and supports break and continue, so it wins whenever you need control flow or are accumulating into an outside variable. forEach reads clean for a per-item side effect but cannot break and ignores return values, which makes it the wrong tool for producing a result. When you want a new array back, reach for the transforming methods instead of a loop. Its second callback argument gives you the index if you need position.

Arrays are held by reference

Here is behaviour that surprises people, so it is worth seeing early. When you copy an array variable into another name, you do not get a second array. Both names point at the same one.

js
const original = [1, 2, 3];
const copy = original;
copy.push(4);
console.log(original); // [1, 2, 3, 4]

Pushing to copy changed original, because there was only ever one array. The = handed the second name the same list, not a fresh copy of it. This is what people mean when they say arrays are held by reference: the variable holds a pointer to the list, not the list itself.

To get a real, separate copy, spread the array into a new one with ...:

js
const original = [1, 2, 3];
const copy = [...original];
copy.push(4);
console.log(original); // [1, 2, 3], untouched

A number or a string is held by value: copying the variable copies the value. An array is different. It is held by reference, so copying the variable copies the pointer, and both names end up aimed at the same array.

js
const original = [1, 2, 3];
const shared = original;
shared.push(4);
console.log(original); // [1, 2, 3, 4], the same array

This is the same fact behind the const note earlier: an array is an object, and a variable holds a reference to that object, not the object itself. Mutating through either name mutates the one array they share.

When you want an independent copy, spread it into a new array. The ... copies each item across:

js
const original = [1, 2, 3];
const copy = [...original];
copy.push(4);
console.log(original); // [1, 2, 3], separate

This is a shallow copy: the new array is its own list, but if the items were themselves arrays, both copies would still share those inner arrays. With plain numbers and strings, shallow is all you need.

Primitives (numbers, strings, booleans) are held by value; arrays are objects and held by reference. A variable that "contains" an array actually holds a reference to it, so assignment copies the reference, and every name pointing at that array sees every mutation:

js
const original = [1, 2, 3];
const alias = original;
alias.push(4);
console.log(original); // [1, 2, 3, 4]
console.log(original === alias); // true, same array

This is why const and mutability are separate concerns: const fixes the reference, mutation changes what the reference points at. It is also the argument for preferring immutability, producing a new array instead of changing one in place. Shared mutable state means a function that mutates an array it received quietly changes it for every other holder of that reference, which is a class of bug that is hard to trace. Returning a new array keeps each caller's data its own.

The spread operator makes a shallow copy: a new outer array with the same items copied in.

js
const original = [1, 2, 3];
const copy = [...original];
console.log(copy === original); // false, different arrays

Shallow is the word that matters. The new array is independent, but nested arrays inside it are still shared by reference, so a deep structure needs more than a spread. With arrays of primitives, which is all you have so far, a spread is a complete copy.

As for choosing the structure at all: an array is the right tool when you have an ordered list of similar things and position or count matters, a queue of prices, a sequence of names. When you instead want to look values up by a meaningful key rather than a position, that is what an object is for, and a function is where a lot of this array work ends up living.

JunoArrays are held by reference Copying an array variable with = does not make a second array; both names point at the same one, so a change through either shows up in both. This one bit me more than once. When you want a real separate copy, spread it with [...original], which builds a fresh array you can change on its own.
JunoArrays are held by reference Numbers and strings copy by value; arrays copy by reference, so const b = a gives you two names for one array, and mutating through either hits both. That is the same reason a const array is still mutable. Use [...original] for an independent copy, keeping in mind it is shallow, which is exactly what you want for arrays of numbers and strings.
JunoArrays are held by reference Arrays are objects held by reference, so assignment copies the pointer and every name sees every mutation. That splits const (fixes the reference) from mutability (changes the target), and it is the case for preferring a new array over an in-place change: a function that mutates a received array changes it for everyone holding that reference. Spread makes a shallow copy, independent at the top level but sharing any nested arrays. Reach for an array when order and count matter, and an object when you want to look things up by key.

Where arrays go from here

An array gives you an ordered list under one name: make it with [], read it by index from zero, change it with push and pop, and walk it with for...of. The one idea to carry past this chapter is that an array is held by reference, so copying the variable shares the list rather than duplicating it.

The natural next step is turning one array into another, filtering a list, transforming every item, adding it all up, without writing the loop by hand. That is array methods. When you need to look values up by a name instead of a position, objects are the structure for that.