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.
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:
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);
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:
const titles = ["Buy milk", "Write report", "Call Ada"];
titles[1] = "Write the quarterly report";
console.log(titles);
console.log(Array.isArray(titles), typeof titles);
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:
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);
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 atendis 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.
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);
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.
Searching
includes tells you whether a value is in the array. indexOf tells you where, or -1 if it is not there. Both compare with ===:
const tags = ["home", "urgent", "shopping"];
console.log(tags.includes("urgent"), tags.includes("work"));
console.log(tags.indexOf("shopping"), tags.indexOf("work"));
node includes.js and of the browser terminaltrue 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?":
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?
node find.js and of the browser terminal87 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.
mapmakes a new array of the same length, with each item transformed by your callback.filtermakes a new array with only the items for which your callback returns something truthy.reducecombines all items into one value, such as a total.
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
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 the0, 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:
const titles = ["Buy milk", "Write report"];
titles.forEach((title, index) => {
console.log(`${index + 1}. ${title}`);
});
node for-each.js and of the browser terminal1. 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.
const numbers = [10, 9, 1, 100, 25];
numbers.sort();
console.log(numbers);
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:
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)));
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' ]
sortmutates the array.toSorteddoes the same job but gives back a sorted copy. PrefertoSortedunless 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:
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(", "));
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.
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));
node students.js and of the browser terminaladded 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 becausesomefound an existing Ada; the check ignores case.101and the string"88"were both refused. A grade from a form arrives as text; the caller must convert it withNumberfirst, and the function does not guess.findreturned the whole student object, orundefinedfor a name that does not exist. A caller must check forundefinedbefore using the result.- The average is (91 + 78 + 85 + 95) / 4 = 87.25, rounded to one decimal. The
students.length === 0guard avoids dividing by zero, which would giveNaN. topStudentsusestoSorted, so the list order printed bylistStudentsis 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
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);
node passed.js and of the browser terminalAda, 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
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);
node remove.js and of the browser terminaltrue 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
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}`);
node bands.js and of the browser terminal80 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;
.lengthcounts them;at(-1)is the last one. push/popwork at the end,unshift/shiftat the start.slicecopies;splicechanges the array.includesandindexOflook for a value;find,findIndex,someandeverytake a callback.maptransforms,filterkeeps some items,reducecombines them into one value. Givereducea starting value.- Default
sortcompares as text. Pass(a, b) => a - bfor numbers, and prefertoSorted, 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.