Learn / JavaScript fundamentals
LESSON 11 OF 84

JavaScript fundamentals Foundation

Arrays

Keep ordered lists in arrays, add and remove items, search them, and transform them with map, filter, reduce and sort, then build a small student management system.

  • 40 min to read and try
  • You need: The lessons up to Functions
  • You build: A student management system that lists, adds, finds and ranks students
Test yourself

Creating and reading arrays

An array is an ordered list of values. You have already used a few: a list of titles in Loops, a list of numbers in Functions. Write one with square brackets and commas. Each item has a position, its index, counted from 0, and .length tells you how many items there are:

create.js
const titles = ["Buy milk", "Write report", "Call Ada"];
const empty = [];
const mixed = [1, "two", true, null];

console.log(titles);
console.log(titles.length, empty.length);
console.log(titles[0]);             // the first item
console.log(titles[titles.length - 1]);   // the last item
console.log(titles.at(-1));         // also the last item
console.log(titles[10]);            // no item there
console.log(mixed);
Output of node create.js and of the browser terminal
[ 'Buy milk', 'Write report', 'Call Ada' ]
3 0
Buy milk
Call Ada
Call Ada
undefined
[ 1, 'two', true, null ]

Reading an index that does not exist gives undefined, not an error. An array can hold any mix of values, but in practice you keep one kind of thing per array: all titles, or all tasks.

Updating items

Assign to an index to replace an item:

update.js
const titles = ["Buy milk", "Write report", "Call Ada"];

titles[1] = "Write the quarterly report";
console.log(titles);
console.log(Array.isArray(titles), typeof titles);
Output of node update.js and of the browser terminal
[ 'Buy milk', 'Write the quarterly report', 'Call Ada' ]
true object

Notice that titles is a const, and yet it changed. const only stops you from pointing the name at a different array; the array itself can still change. Changing a value in place is called mutating it. Arrays are objects, so typeof says "object"; use Array.isArray to check for an array.

Adding and removing items

Four methods add or remove at the ends. They all mutate the array:

push-pop.js
const queue = ["b", "c"];

queue.push("d");          // add to the end
queue.unshift("a");       // add to the start
console.log(queue);

const last = queue.pop();     // remove from the end
const first = queue.shift();  // remove from the start
console.log(last, first);
console.log(queue);
Output of node push-pop.js and of the browser terminal
[ 'a', 'b', 'c', 'd' ]
d a
[ 'b', 'c' ]

pop and shift give back the item they removed. An easy way to remember: push/pop work at the end, unshift/shift at the start.

slice versus splice

These two names look alike and do very different things:

  • slice(start, end) copies a part of the array into a new one. The original does not change. The item at end is not included.
  • splice(start, deleteCount, ...newItems) changes the array: it removes items, can insert new ones in their place, and gives back what it removed.
slice-splice.js
const letters = ["a", "b", "c", "d", "e"];

console.log(letters.slice(1, 3));   // copy positions 1 and 2
console.log(letters.slice(-2));     // copy the last two
console.log(letters);               // unchanged

const removed = letters.splice(1, 2);          // remove 2 items at position 1
console.log(removed, letters);
letters.splice(1, 0, "x", "y");               // remove 0, insert 2
console.log(letters);
Output of node slice-splice.js and of the browser terminal
[ 'b', 'c' ]
[ 'd', 'e' ]
[ 'a', 'b', 'c', 'd', 'e' ]
[ 'b', 'c' ] [ 'a', 'd', 'e' ]
[ 'a', 'x', 'y', 'd', 'e' ]

slice() with no arguments copies the whole array, which is useful when you want to change a copy and keep the original.

includes tells you whether a value is in the array. indexOf tells you where, or -1 if it is not there. Both compare with ===:

includes.js
const tags = ["home", "urgent", "shopping"];

console.log(tags.includes("urgent"), tags.includes("work"));
console.log(tags.indexOf("shopping"), tags.indexOf("work"));
Output of node includes.js and of the browser terminal
true false
2 -1

For anything more than an exact value, pass a callback. find gives the first item for which the callback returns something truthy, and findIndex gives its position. some asks "does at least one item match?" and every asks "do all items match?":

find.js
const scores = [42, 87, 65, 91, 73];

console.log(scores.find((s) => s > 80));        // first score above 80
console.log(scores.findIndex((s) => s > 80));   // its position
console.log(scores.find((s) => s > 100));       // no match
console.log(scores.findIndex((s) => s > 100));
console.log(scores.some((s) => s < 50));        // anyone failed?
console.log(scores.every((s) => s >= 40));      // everyone at least 40?
Output of node find.js and of the browser terminal
87
1
undefined
-1
true
true

Each of these calls your callback with one item at a time, just like applyToAll did in the lesson on functions. They stop as soon as they know the answer.

map, filter and reduce

These three methods replace most loops you would otherwise write. None of them changes the original array; each gives back something new.

  • map makes a new array of the same length, with each item transformed by your callback.
  • filter makes a new array with only the items for which your callback returns something truthy.
  • reduce combines all items into one value, such as a total.
map-filter-reduce.js
const prices = [1200, 450, 3000, 800];   // in cents

const inEuros = prices.map((cents) => cents / 100);
const expensive = prices.filter((cents) => cents >= 1000);
const total = prices.reduce((sum, cents) => sum + cents, 0);

console.log(inEuros);
console.log(expensive);
console.log(total);
console.log(prices);          // unchanged
Output of node map-filter-reduce.js and of the browser terminal
[ 12, 4.5, 30, 8 ]
[ 1200, 3000 ]
5450
[ 1200, 450, 3000, 800 ]

reduce needs two things: a callback that takes the result so far (sum) and the current item, and a starting value (0). Step by step: 0 + 1200 = 1200, then + 450 = 1650, then + 3000 = 4650, then + 800 = 5450.

Always give reduce a starting value

Without the 0, reduce starts from the first item, and on an empty array it throws an error. With it, the total of an empty list is simply 0.

forEach

forEach calls your callback once per item and gives back nothing. Use it when you only want a side effect, such as printing. The callback also receives the index as a second argument; so do map, filter and the others:

for-each.js
const titles = ["Buy milk", "Write report"];

titles.forEach((title, index) => {
  console.log(`${index + 1}. ${title}`);
});
Output of node for-each.js and of the browser terminal
1. Buy milk
2. Write report

A for...of loop does the same job and also allows break, which forEach does not. Pick whichever reads better.

Sorting

sort puts the items in order. It has a famous trap: by default it converts every item to a string and sorts them as text.

sort-trap.js
const numbers = [10, 9, 1, 100, 25];
numbers.sort();
console.log(numbers);
Output of node sort-trap.js and of the browser terminal
[ 1, 10, 100, 25, 9 ]

As text, "100" comes before "25" because "1" comes before "2", the same rule you saw with "10" < "9" in Operators. To sort numbers, pass a comparator: a function that takes two items a and b and returns a negative number if a should come first, a positive number if b should, and 0 if they are equal. a - b does exactly that:

sort.js
const numbers = [10, 9, 1, 100, 25];

console.log(numbers.toSorted((a, b) => a - b));   // smallest first
console.log(numbers.toSorted((a, b) => b - a));   // largest first
console.log(numbers);                             // unchanged

const names = ["bola", "Ada", "émile", "Chen"];
console.log(names.toSorted());
console.log(names.toSorted((a, b) => a.localeCompare(b)));
Output of node sort.js and of the browser terminal
[ 1, 9, 10, 25, 100 ]
[ 100, 25, 10, 9, 1 ]
[ 10, 9, 1, 100, 25 ]
[ 'Ada', 'Chen', 'bola', 'émile' ]
[ 'Ada', 'bola', 'Chen', 'émile' ]
  • sort mutates the array. toSorted does the same job but gives back a sorted copy. Prefer toSorted unless you really want to change the original.
  • The default string order puts all capital letters before all small letters, and accented letters after both. a.localeCompare(b) sorts text the way a person expects.

Chaining methods

Because map, filter, slice and toSorted give back a new array, you can call the next method straight on the result. This is called chaining. Put each step on its own line so it reads like a recipe:

chaining.js
const words = ["  milk ", "", "BREAD", "eggs", "  ", "Milk"];

const shoppingList = words
  .map((w) => w.trim().toLowerCase())    // clean each word
  .filter((w) => w !== "")               // drop the empty ones
  .filter((w, i, all) => all.indexOf(w) === i)   // drop repeats
  .toSorted();

console.log(shoppingList);
console.log(shoppingList.join(", "));
Output of node chaining.js and of the browser terminal
[ 'bread', 'eggs', 'milk' ]
bread, eggs, milk

The third filter uses all three callback arguments: the item, its index, and the whole array. It keeps an item only if this is the first place it appears. join is the opposite of the string method split: it glues the items into one string.

Build: a student management system

A school needs a small program to keep track of students and their grades. Each student is an object with a name and a grade: { name: "Ada", grade: 91 }. Objects get the next lesson; here you only need student.name and student.grade to read the two values.

The system is a set of small functions over one array: list, add (with validation), find, average and top students.

students.js
const students = [
  { name: "Ada", grade: 91 },
  { name: "Bola", grade: 78 },
  { name: "Chen", grade: 85 },
];

function addStudent(name, grade) {
  const clean = String(name ?? "").trim();
  if (clean === "") return "rejected: name is required";
  if (!Number.isInteger(grade) || grade < 0 || grade > 100) return "rejected: grade must be 0 to 100";
  if (students.some((s) => s.name.toLowerCase() === clean.toLowerCase())) return `rejected: ${clean} exists`;
  students.push({ name: clean, grade });
  return `added ${clean}`;
}

const findStudent = (name) => students.find((s) => s.name.toLowerCase() === name.toLowerCase());

function averageGrade() {
  if (students.length === 0) return 0;
  const total = students.reduce((sum, s) => sum + s.grade, 0);
  return Math.round((total / students.length) * 10) / 10;
}

function topStudents(count) {
  return students
    .toSorted((a, b) => b.grade - a.grade)
    .slice(0, count)
    .map((s) => `${s.name} (${s.grade})`);
}

function listStudents() {
  students.forEach((s, i) => console.log(`${i + 1}. ${s.name.padEnd(6)} ${s.grade}`));
}

console.log(addStudent("  Dayo ", 95));
console.log(addStudent("ada", 50));
console.log(addStudent("", 70));
console.log(addStudent("Emeka", 101));
console.log(addStudent("Femi", "88"));
listStudents();
console.log("Find chen:", findStudent("chen"));
console.log("Find zara:", findStudent("zara"));
console.log("Average:", averageGrade());
console.log("Top 2:", topStudents(2));
Output of node students.js and of the browser terminal
added Dayo
rejected: ada exists
rejected: name is required
rejected: grade must be 0 to 100
rejected: grade must be 0 to 100
1. Ada    91
2. Bola   78
3. Chen   85
4. Dayo   95
Find chen: { name: 'Chen', grade: 85 }
Find zara: undefined
Average: 87.3
Top 2: [ 'Dayo (95)', 'Ada (91)' ]

Read the output from top to bottom:

  • " Dayo " was trimmed before it was saved. "ada" was refused because some found an existing Ada; the check ignores case.
  • 101 and the string "88" were both refused. A grade from a form arrives as text; the caller must convert it with Number first, and the function does not guess.
  • find returned the whole student object, or undefined for a name that does not exist. A caller must check for undefined before using the result.
  • The average is (91 + 78 + 85 + 95) / 4 = 87.25, rounded to one decimal. The students.length === 0 guard avoids dividing by zero, which would give NaN.
  • topStudents uses toSorted, so the list order printed by listStudents is not disturbed.

Practice

TRY IT YOURSELF

Who passed?

Using the same kind of student list, print the names of the students with a grade of 80 or more, as one comma-separated string.

Show a solution
passed.js
const students = [
  { name: "Ada", grade: 91 },
  { name: "Bola", grade: 78 },
  { name: "Chen", grade: 85 },
];

const passed = students
  .filter((s) => s.grade >= 80)
  .map((s) => s.name)
  .join(", ");

console.log(passed);
Output of node passed.js and of the browser terminal
Ada, Chen

TRY IT YOURSELF

Remove a student

Write removeStudent(name) that removes the student with that name from the array and returns true, or returns false when there is no such student.

Show a solution
remove.js
const students = [
  { name: "Ada", grade: 91 },
  { name: "Bola", grade: 78 },
];

function removeStudent(name) {
  const index = students.findIndex((s) => s.name === name);
  if (index === -1) return false;
  students.splice(index, 1);
  return true;
}

console.log(removeStudent("Ada"), removeStudent("Zara"));
console.log(students);
Output of node remove.js and of the browser terminal
true false
[ { name: 'Bola', grade: 78 } ]

Always check for -1 before you use the index. students.splice(-1, 1) would silently remove the last student.

TRY IT YOURSELF

Count by grade band

With reduce, count how many grades in [91, 78, 85, 95, 62, 70] are 80 or more, and how many are below 80. Print both numbers.

Show a solution
bands.js
const grades = [91, 78, 85, 95, 62, 70];

const high = grades.reduce((count, g) => (g >= 80 ? count + 1 : count), 0);
const low = grades.length - high;

console.log(`80 or more: ${high}, below 80: ${low}`);
Output of node bands.js and of the browser terminal
80 or more: 3, below 80: 3

grades.filter((g) => g >= 80).length gives the same number and may read more clearly. Both are fine.

Recap

  • An array is an ordered list. Items are read by index from 0; .length counts them; at(-1) is the last one.
  • push/pop work at the end, unshift/shift at the start. slice copies; splice changes the array.
  • includes and indexOf look for a value; find, findIndex, some and every take a callback.
  • map transforms, filter keeps some items, reduce combines them into one value. Give reduce a starting value.
  • Default sort compares as text. Pass (a, b) => a - b for numbers, and prefer toSorted, which does not mutate.

Test yourself

Five questions, picked at random from this lesson's question bank. Some ask you to choose an answer, some to predict what code prints, and some to write code and run it in the terminal. Get 4 of 5 right to pass. If you don't, read the explanations and try again: you get 5 different questions.