Webbo3 Level 2 · JavaScript Development · Day 3 of 20
Arrays, Objects and Reshaping Data
Real applications are mostly lists of records being filtered, transformed and totalled. Today you learn the handful of tools that do almost all of it.
By the end of today you can
- Hold a list in an array and a record in an object, and nest one inside the other
- Read nested data safely without crashing your program
- Go through every item in a list with a loop and with forEach
- Transform a whole list with
map, narrow it with filter and pick one item with find
- Total and group anything with
reduce without being frightened of it
- Unpack values with destructuring and copy safely with the spread operator
Open any application you use. A feed is a list of posts. A cart is a list of items. A dashboard is a list of records with a total underneath.
Almost everything you will build from here is a list of records, reshaped: filtered down, transformed into something else, or added up.
Yesterday you analysed three students by passing their scores in one at a time. Today you will hold a whole class in one value and run your functions over all of them at once.
1. Arrays: An Ordered List
Definition
An array is an ordered list of values stored under one name. Each value has a position, called its index, and the positions start at 0, not 1.
Real-Life Example
Think of a queue at a bank. Everyone is in order, and each person has a position. If you want the person at the front, you ask for position one.
An array is that queue. The only surprise is that JavaScript calls the front of the queue position 0.
Types of Things You Do With an Array
- Read an item by its position.
- Add items to the end or the front.
- Remove items from the end or the front.
- Sort the items into an order.
Type 1: Reading Items
Put the index in square brackets to read an item. length tells you how many items there are, and at(-1) reads the last item without any arithmetic.
What this code doesWe want to read items out of a list by position, including the first and the last, and count how many there are.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// A list of four courses, in order
const courses = ['HTML', 'CSS', 'Tailwind', 'JavaScript'];
// How many items are in the list?
console.log(courses.length);
// The first item is at position 0
console.log(courses[0]);
// The last item: length - 1, the older way
console.log(courses[courses.length - 1]);
// The last item: at(-1), the clearer way
console.log(courses.at(-1));
OUTPUT TERMINAL
➜ webbo3 node app.js
4
HTML
JavaScript
JavaScript
courses[0] is the first item, not the second. Almost every "off by one" bug you will ever write starts here.
Type 2 and 3: Adding and Removing
| Method | What it does |
push(item) | Adds to the end |
unshift(item) | Adds to the front |
pop() | Removes from the end, and gives you the removed item |
shift() | Removes from the front, and gives you the removed item |
What this code doesWe want to change a shopping cart: add items at both ends, then take one off the end and see what was removed.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
// A cart with one item
const cart = ['rice'];
// Add to the end, then to the front
cart.push('beans');
cart.unshift('oil');
console.log(cart);
// Remove from the end, keeping hold of what was removed
const removed = cart.pop();
console.log(removed);
console.log(cart);
OUTPUT TERMINAL
➜ webbo3 node app.js
[ 'oil', 'rice', 'beans' ]
beans
[ 'oil', 'rice' ]
Type 4: Sorting
sort() puts the items in order. For numbers it needs a small function that tells it how to compare two values. You will understand that function fully in section 5; for now, read (a, b) => a - b as "smallest first".
What this code doesWe want to put a list of scores in order from lowest to highest.
app.js
1
2
3
4
5
6
7
8
// Three scores in no particular order
const scores = [30, 100, 25];
// Sort them as numbers, smallest first
scores.sort((a, b) => a - b);
// Show the sorted list
console.log(scores);
OUTPUT TERMINAL
➜ webbo3 node app.js
[ 25, 30, 100 ]
Common Mistake: sort() With Nothing Inside
With no comparison function, sort() turns every value into text and sorts it alphabetically. Predict the output before you read it:
What this code doesWe want to see what sort() does to numbers when it is not told how to compare them.
app.js
1
2
3
4
5
6
7
8
// The same three scores
const scores = [30, 100, 25];
// Sort with no comparison function
scores.sort();
// Show the result
console.log(scores);
OUTPUT TERMINAL
➜ webbo3 node app.js
[ 100, 25, 30 ]
Alphabetically, "1" comes before "2" and "3", so 100 lands first. Numbers always need (a, b) => a - b.
Important Rule
push, pop, shift, unshift and sort change the original array. Most of the methods you meet later today, such as map and filter, leave the original alone and give you a new array instead.
Overall Example
What this code doesWe want to manage a class register: add a student, sort the names and report who is first and last.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
// A class register
const register = ['Chidinma', 'Musa', 'Tunde'];
// A new student joins
register.push('Amaka');
// Put the names in alphabetical order (text sorts correctly as text)
register.sort();
// Report the class
console.log(`${register.length} students`);
console.log(`First: ${register[0]}, last: ${register.at(-1)}`);
OUTPUT TERMINAL
➜ webbo3 node app.js
4 students
First: Amaka, last: Tunde
Try This
Create an array of three foods you like. Add one to the end, remove the first one, and print the list and its length.
What this code doesPractise: add to and remove from a list, then check what is left.
app.js
1
2
3
4
5
6
7
8
9
10
// Three foods (use your own)
const foods = ['jollof', 'suya', 'plantain'];
// Add one to the end, remove one from the front
foods.push('puff-puff');
foods.shift();
// Show the list and how long it is
console.log(foods);
console.log(foods.length);
OUTPUT TERMINAL
➜ webbo3 node app.js
[ 'suya', 'plantain', 'puff-puff' ]
3
Picture This
Picture a queue at the bank with numbered spots on the floor, starting at spot 0. push lets someone join at the back, shift serves the person at the front, and sort rearranges the whole queue. at(-1) points at whoever is last, however long the queue gets.
2. Objects: A Record With Named Fields
Definition
An object stores related values together, each under a name called a property (or field). You read a property with a dot: object.property.
Real-Life Example
A student's registration form has labelled boxes: Name, Age, City, Email. Each box holds one answer, and the label tells you what the answer means.
An object is that form. The property names are the labels, and the values are the answers. A box can even hold another small form, such as a Contact section with its own email and city.
Reading Properties, Including Nested Ones
An object can hold arrays and other objects. You follow the path with dots for objects and square brackets for arrays.
What this code doesWe want to read values out of a student record, including values inside a nested object and a nested array, and see what a missing property gives back.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// One student record, with an array and an object inside it
const student = {
name: 'Chidinma Okafor',
age: 22,
enrolled: true,
scores: [72, 65, 88],
contact: { email: '[email protected]', city: 'Lagos' }
};
// A simple property
console.log(student.name);
// Into the nested object, then its city
console.log(student.contact.city);
// Into the nested array, then position 2
console.log(student.scores[2]);
// A property that does not exist
console.log(student.nickname);
OUTPUT TERMINAL
➜ webbo3 node app.js
Chidinma Okafor
Lagos
88
undefined
Asking for a property that does not exist gives undefined, not an error. That sounds forgiving, until you go one level deeper.
Common Mistake: Reading Through Something That Is Missing
What this code doesWe want to see what happens when code reads a property inside something that does not exist.
app.js
1
2
3
4
5
// This record has no contact section at all
const student = { name: 'Musa' };
// Try to read the city inside the missing contact
console.log(student.contact.city);
OUTPUT TERMINAL
➜ webbo3 node app.js
TypeError: Cannot read properties of undefined (reading 'city')
Why This Happens
student.contact is undefined, and you cannot read a property of undefined. This is the most common runtime error in JavaScript. You will meet it constantly with data from APIs, where some fields are sometimes missing.
The Fix: ?. and ??
| Operator | Name | What it does |
?. | Optional chaining | Stops and gives undefined if the thing before it is missing, instead of crashing |
?? | Nullish coalescing | Gives a fallback, but only when the value is null or undefined |
What this code doesWe want to read the city safely: no crash when contact is missing, and a sensible fallback to show instead.
app.js
1
2
3
4
5
6
7
8
// The same record with no contact section
const student = { name: 'Musa' };
// Read safely: stop at the missing part instead of crashing
console.log(student.contact?.city);
// Read safely AND supply a fallback for the missing value
console.log(student.contact?.city ?? 'Unknown');
OUTPUT TERMINAL
➜ webbo3 node app.js
undefined
Unknown
Important Rule
Use ?? rather than || for fallbacks. As you saw on Day 2, || also replaces 0 and '', which are often real values. ?? only replaces null and undefined.
Overall Example
What this code doesWe want one line of code that reads a city safely from a complete record and from an incomplete one.
app.js
1
2
3
4
5
6
7
// Two records: one complete, one with a missing section
const ada = { name: 'Ada', contact: { city: 'Abuja' } };
const bayo = { name: 'Bayo' };
// The same safe line works for both
console.log(`${ada.name} lives in ${ada.contact?.city ?? 'an unknown city'}`);
console.log(`${bayo.name} lives in ${bayo.contact?.city ?? 'an unknown city'}`);
OUTPUT TERMINAL
➜ webbo3 node app.js
Ada lives in Abuja
Bayo lives in an unknown city
Try This
Create an object for yourself with a name and a nested contact object holding your city. Print the city, then print a property that does not exist with a fallback.
What this code doesPractise: read a nested value, then read a missing one safely.
app.js
1
2
3
4
5
6
7
8
// Your record (use your own details)
const me = { name: 'Joshua', contact: { city: 'Ibadan' } };
// Read the nested city
console.log(me.contact.city);
// Read a missing property safely, with a fallback
console.log(me.contact?.phone ?? 'No phone saved');
OUTPUT TERMINAL
➜ webbo3 node app.js
Ibadan
No phone saved
Picture This
Picture a filing cabinet. Each drawer is labelled, and some drawers hold smaller folders. The dot is you opening a drawer. ?. is you checking the drawer exists before reaching in, so your hand never closes on thin air.
3. A List of Records: The Shape of Everything
Definition
A list of records is an array whose items are objects that all have the same properties. It is the shape of almost all real data.
Real-Life Example
A class register in a spreadsheet: every row is one student, and every column is one property (name, score, city, paid). The spreadsheet is the array; each row is an object.
What this code doesWe want to hold a whole class in one value and look at it the way you would look at a spreadsheet.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
// Four students: an array of objects with the same properties
const students = [
{ name: 'Chidinma', score: 88, city: 'Lagos', paid: true },
{ name: 'Musa', score: 54, city: 'Kano', paid: false },
{ name: 'Tunde', score: 71, city: 'Lagos', paid: true },
{ name: 'Amaka', score: 39, city: 'Enugu', paid: true }
];
// How many records?
console.log(students.length, 'records');
// Show them as a table, like a spreadsheet
console.table(students);
OUTPUT TERMINAL
➜ webbo3 node app.js
4 records
┌─────────┬────────────┬───────┬─────────┬───────┐
│ (index) │ name │ score │ city │ paid │
├─────────┼────────────┼───────┼─────────┼───────┤
│ 0 │ 'Chidinma' │ 88 │ 'Lagos' │ true │
│ 1 │ 'Musa' │ 54 │ 'Kano' │ false │
│ 2 │ 'Tunde' │ 71 │ 'Lagos' │ true │
│ 3 │ 'Amaka' │ 39 │ 'Enugu' │ true │
└─────────┴────────────┴───────┴─────────┴───────┘
Hold this array in your head for the rest of the day. Every tool below works on exactly this shape, and so will every API response you fetch next week.
Picture This
Picture a spreadsheet. The whole sheet is the array, each row is an object, and each column heading is a property name.
4. Going Through Every Item
Definition
A loop runs the same code once for each item in a list, so you do not have to write it out again for every item.
Real-Life Example
A teacher calling the register reads each name in turn and does the same thing for every student: says the name and waits for "present". The teacher does not need a different instruction for each student.
Types of Loops for Lists
for...of: a loop written with the keyword for.
forEach: an array method that runs a function for each item.
Type 1: for...of
for (const student of students) means: take each item in turn, call it student, and run the block.
What this code doesWe want to print one line for every student without writing a separate line for each of them.
app.js
1
2
3
4
5
6
7
8
9
10
// Two students
const students = [
{ name: 'Chidinma', score: 88 },
{ name: 'Musa', score: 54 }
];
// Run the block once for each student
for (const student of students) {
console.log(`${student.name} scored ${student.score}`);
}
OUTPUT TERMINAL
➜ webbo3 node app.js
Chidinma scored 88
Musa scored 54
Type 2: forEach
forEach does the same job as a method. You hand it a function, and it calls that function once for every item. It also gives you each item's position.
What this code doesWe want the same register, numbered, by handing a function to forEach.
app.js
1
2
3
4
5
6
7
8
9
10
// The same two students
const students = [
{ name: 'Chidinma', score: 88 },
{ name: 'Musa', score: 54 }
];
// For each student, run this function with the student and its position
students.forEach((student, index) => {
console.log(`${index + 1}. ${student.name} scored ${student.score}`);
});
OUTPUT TERMINAL
➜ webbo3 node app.js
1. Chidinma scored 88
2. Musa scored 54
Important Rule
forEach gives you nothing back. It is for doing something with each item, such as printing or, from tomorrow, putting things on the page. The moment you want a new list out the other end, you want map instead.
Try This
Loop through an array of three city names and print "Welcome to" before each one.
What this code doesPractise: run the same line for every item in a list.
app.js
1
2
3
4
5
6
7
// Three cities
const cities = ['Lagos', 'Kano', 'Enugu'];
// Print a greeting for each one
for (const city of cities) {
console.log(`Welcome to ${city}`);
}
OUTPUT TERMINAL
➜ webbo3 node app.js
Welcome to Lagos
Welcome to Kano
Welcome to Enugu
Picture This
Picture the teacher calling the register: one instruction, repeated for every name, from the top of the list to the bottom.
5. map: Turn Every Item Into Something Else
First, the Idea of a Callback
A callback is a function you hand to another function, so that function can call it for you. You have already written one: the arrow function you gave to forEach.
Everything in the rest of this lesson works the same way. You write a small function that handles one item, and the array method calls it for every item.
Definition
map builds a new array by calling your function on every item and collecting what it returns. One item in, one item out, so the new array is always the same length.
Real-Life Example
A printing shop takes a stack of photos and hands you back a stack of framed photos. Same number of photos, each one changed the same way, and your originals are untouched.
Examples
What this code doesWe want to turn a list of student records into a list of names, and into a list of sentences, without changing the original list.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Three student records
const students = [
{ name: 'Chidinma', score: 88 },
{ name: 'Musa', score: 54 },
{ name: 'Amaka', score: 39 }
];
// Turn each record into just its name
const names = students.map((student) => student.name);
// Turn each record into a sentence
const lines = students.map((student) => `${student.name} scored ${student.score}`);
// Show both new arrays, then check the original is still there
console.log(names);
console.log(lines);
console.log(students.length);
OUTPUT TERMINAL
➜ webbo3 node app.js
[ 'Chidinma', 'Musa', 'Amaka' ]
[ 'Chidinma scored 88', 'Musa scored 54', 'Amaka scored 39' ]
3
Yesterday's functions work as callbacks too. Pass the grading logic to map and every student is graded at once:
What this code doesWe want to reuse Day 2's gradeFor function on a whole class in one line, instead of calling it once per student.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Day 2's grading function, unchanged
function gradeFor(score) {
if (score >= 70) return 'A';
if (score >= 60) return 'B';
if (score >= 50) return 'C';
return 'F';
}
// The class
const students = [
{ name: 'Chidinma', score: 88 },
{ name: 'Musa', score: 54 },
{ name: 'Amaka', score: 39 }
];
// Grade every student by calling gradeFor inside map
const grades = students.map((student) => `${student.name}: ${gradeFor(student.score)}`);
console.log(grades);
OUTPUT TERMINAL
➜ webbo3 node app.js
[ 'Chidinma: A', 'Musa: C', 'Amaka: F' ]
Common Mistake: A List of undefined
What this code doesWe want to see the Day 2 arrow-function trap again, this time inside map.
app.js
1
2
3
4
5
6
7
8
// Two students
const students = [{ name: 'Chidinma' }, { name: 'Musa' }];
// Braces and no return: the callback gives back nothing
console.log(students.map((student) => { student.name; }));
// No braces: the value is returned automatically
console.log(students.map((student) => student.name));
OUTPUT TERMINAL
➜ webbo3 node app.js
[ undefined, undefined ]
[ 'Chidinma', 'Musa' ]
Why This Happens
Braces mean you must write return yourself (Day 2, section 5). With no return, every call hands back undefined, and map faithfully collects one undefined per student.
Try This
You have prices in naira. Use map to add 7.5% VAT to every price.
What this code doesPractise: transform every number in a list the same way.
app.js
1
2
3
4
5
6
7
// Prices before VAT
const prices = [1000, 2500, 400];
// Add 7.5% to each one
const withVat = prices.map((price) => price * 1.075);
console.log(withVat);
OUTPUT TERMINAL
➜ webbo3 node app.js
[ 1075, 2687.5, 430 ]
Picture This
Picture a conveyor belt through a machine. Every item that goes in comes out changed, in the same order, and the number of items never changes. That machine is your callback, and map is the belt.
6. Searching a List: filter, find, some and every
Definition
These four methods each ask a yes-or-no question about the items. Your callback returns true or false for each item, and the method uses those answers in its own way.
Real-Life Example
A head teacher asks four different questions about the same class: "Show me everyone who passed." "Find me Tunde." "Did anyone fail?" "Did everybody pay?" Same class, same yes-or-no test, four different kinds of answer.
Types
| Method | Gives you back | When nothing matches |
filter | A new array of every item that passed | An empty array [] |
find | The first item that passed, itself | undefined |
some | true if at least one passed | false |
every | true only if all passed | true (for an empty list) |
Type 1: filter
What this code doesWe want two smaller lists from the same class: everyone who passed, and everyone who has not paid.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// The class
const students = [
{ name: 'Chidinma', score: 88, paid: true },
{ name: 'Musa', score: 54, paid: false },
{ name: 'Tunde', score: 71, paid: true },
{ name: 'Amaka', score: 39, paid: true }
];
// Keep only the students who scored 50 or more
const passing = students.filter((student) => student.score >= 50);
// Keep only the students who have not paid
const unpaid = students.filter((student) => !student.paid);
console.log(passing.length);
console.log(unpaid);
OUTPUT TERMINAL
➜ webbo3 node app.js
3
[ { name: 'Musa', score: 54, paid: false } ]
Type 2: find
What this code doesWe want to pick out one particular student, and see what happens when the student is not there.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// The class
const students = [
{ name: 'Chidinma', score: 88, paid: true },
{ name: 'Musa', score: 54, paid: false },
{ name: 'Tunde', score: 71, paid: true },
{ name: 'Amaka', score: 39, paid: true }
];
// Find the one student called Tunde
const tunde = students.find((student) => student.name === 'Tunde');
// Look for someone who is not in the class
const zainab = students.find((student) => student.name === 'Zainab');
console.log(tunde);
console.log(zainab);
OUTPUT TERMINAL
➜ webbo3 node app.js
{ name: 'Tunde', score: 71, paid: true }
undefined
Type 3 and 4: some and every
What this code doesWe want quick yes-or-no answers about the whole class, without building a new list.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
// The class
const students = [
{ name: 'Chidinma', score: 88, paid: true },
{ name: 'Musa', score: 54, paid: false },
{ name: 'Tunde', score: 71, paid: true },
{ name: 'Amaka', score: 39, paid: true }
];
// Did at least one student fail?
console.log(students.some((student) => student.score < 50));
// Did every student pay?
console.log(students.every((student) => student.paid));
OUTPUT TERMINAL
➜ webbo3 node app.js
true
false
Common Mistake: filter When You Wanted find
filter always gives you an array, even when only one item matches. Asking that array for
name gives
undefined, because an array has no name.
What this code doesWe want to see why reading a property off the result of filter does not work, and how find fixes it.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
// The class
const students = [
{ name: 'Chidinma', score: 88, paid: true },
{ name: 'Musa', score: 54, paid: false },
{ name: 'Tunde', score: 71, paid: true },
{ name: 'Amaka', score: 39, paid: true }
];
// filter returns an ARRAY holding Tunde
console.log(students.filter((student) => student.name === 'Tunde').name);
// find returns Tunde himself
console.log(students.find((student) => student.name === 'Tunde').name);
OUTPUT TERMINAL
➜ webbo3 node app.js
undefined
Tunde
Overall Example
What this code doesWe want an end-of-term report: who is cleared (passed and paid), and whether anyone still owes fees.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// The class
const students = [
{ name: 'Chidinma', score: 88, paid: true },
{ name: 'Musa', score: 54, paid: false },
{ name: 'Tunde', score: 71, paid: true },
{ name: 'Amaka', score: 39, paid: true }
];
// Everyone who passed AND paid
const cleared = students.filter((student) => student.score >= 50 && student.paid);
// Report
console.log(`${cleared.length} students are cleared`);
console.log(`Anyone owing fees? ${students.some((student) => !student.paid)}`);
OUTPUT TERMINAL
➜ webbo3 node app.js
2 students are cleared
Anyone owing fees? true
Try This
From a list of product prices, keep only the ones under N2,000, then check whether every price is above zero.
What this code doesPractise: filter a list, then ask a question about all of it.
app.js
1
2
3
4
5
6
7
8
// Some prices
const prices = [1500, 3200, 800, 2500];
// Cheap items only
console.log(prices.filter((price) => price < 2000));
// Are all prices positive?
console.log(prices.every((price) => price > 0));
OUTPUT TERMINAL
➜ webbo3 node app.js
[ 1500, 800 ]
true
Picture This
Picture four sieves. filter keeps everything that passes through. find stops at the first thing that passes. some just rings a bell if anything passes. every rings it only if nothing gets stuck.
7. reduce: Boil a List Down to One Value
Definition
reduce walks through the list and builds up one result, such as a total. Its callback receives two things: what you have built so far (often called the accumulator) and the current item. Whatever the callback returns becomes "what you have built so far" for the next item.
Real-Life Example
A cashier adding up your shopping. They start at zero, then for each item they add its price to the running total. At the end, one number is left: what you owe.
How It Works
| Part | Meaning |
(total, price) => total + price | The callback: the running total and the current item |
0 | The starting value, written after the callback |
| The result | Whatever the callback returned for the last item |
Example 1: A Total
What this code doesWe want to add up every price in a cart into one total, the way a cashier would.
app.js
1
2
3
4
5
6
7
// Three prices in a cart
const prices = [1500, 2000, 750];
// Start at 0, and add each price to the running total
const total = prices.reduce((runningTotal, price) => runningTotal + price, 0);
console.log(total);
OUTPUT TERMINAL
➜ webbo3 node app.js
4250
Common Mistake: Leaving Out the Starting Value
What this code doesWe want to see what reduce does with an empty list when nobody told it where to start.
app.js
1
2
3
4
5
// An empty cart
const prices = [];
// No starting value
console.log(prices.reduce((runningTotal, price) => runningTotal + price));
OUTPUT TERMINAL
➜ webbo3 node app.js
TypeError: Reduce of empty array with no initial value
What this code doesWe want to see the same empty list handled correctly once reduce has a starting value.
app.js
1
2
3
4
5
// The same empty cart
const prices = [];
// With a starting value of 0
console.log(prices.reduce((runningTotal, price) => runningTotal + price, 0));
OUTPUT TERMINAL
➜ webbo3 node app.js
0
Important Rule
Always give reduce a starting value: 0 for a total, {} for grouping into an object, [] for building a list.
Example 2: Grouping
reduce is not only for totals. The "something built so far" can be an object, which lets you sort a flat list into groups.
What this code doesWe want to turn a flat list of students into groups by city: the shape every dashboard needs.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Students and their cities
const students = [
{ name: 'Chidinma', city: 'Lagos' },
{ name: 'Musa', city: 'Kano' },
{ name: 'Tunde', city: 'Lagos' }
];
// Start with an empty object and add each student to their city's list
const byCity = students.reduce((groups, student) => {
// Make an empty list for this city the first time we see it
groups[student.city] = groups[student.city] ?? [];
// Add the student's name to their city's list
groups[student.city].push(student.name);
// Hand the object on to the next student
return groups;
}, {});
console.log(byCity);
OUTPUT TERMINAL
➜ webbo3 node app.js
{ Lagos: [ 'Chidinma', 'Tunde' ], Kano: [ 'Musa' ] }
groups[student.city] uses square brackets to read a property whose name is stored in a variable. The dot only works when you type the property name yourself.
Try This
Use reduce to count how many items are in stock across a warehouse list.
What this code doesPractise: total one property across a list of records.
app.js
1
2
3
4
5
6
7
8
9
10
11
// Stock levels for three products
const stock = [
{ item: 'Rice', quantity: 40 },
{ item: 'Beans', quantity: 25 },
{ item: 'Garri', quantity: 60 }
];
// Start at 0 and add each quantity
const totalItems = stock.reduce((sum, product) => sum + product.quantity, 0);
console.log(totalItems);
OUTPUT TERMINAL
➜ webbo3 node app.js
125
Picture This
Picture the cashier again. The running total on the screen is the accumulator. Each item on the belt is the current value. When the belt is empty, the number on the screen is your answer, and if nobody switched the till on at zero, there is nothing to add to.
8. Chaining: Where It All Comes Together
Definition
Chaining means calling one method straight after another. Because filter and map each give back a new array, you can call the next method on that array immediately.
Real-Life Example
An assembly line: one station removes damaged items, the next paints what is left, the last packs it. Each station works on whatever the previous station passed along.
What this code doesWe want a report of paid students who passed, built in steps: narrow the list, narrow it again, then format it.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// The class
const students = [
{ name: 'Chidinma', score: 88, paid: true },
{ name: 'Musa', score: 54, paid: false },
{ name: 'Tunde', score: 71, paid: true },
{ name: 'Amaka', score: 39, paid: true }
];
// Paid students, who passed, as "name: score" lines
const report = students
.filter((student) => student.paid)
.filter((student) => student.score >= 50)
.map((student) => `${student.name}: ${student.score}`);
console.log(report);
OUTPUT TERMINAL
➜ webbo3 node app.js
[ 'Chidinma: 88', 'Tunde: 71' ]
Read a chain from top to bottom. Each line takes the list the line above produced.
Overall Example
What this code doesWe want the average score of only the students who passed, combining filter and reduce.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// The class
const students = [
{ name: 'Chidinma', score: 88, paid: true },
{ name: 'Musa', score: 54, paid: false },
{ name: 'Tunde', score: 71, paid: true },
{ name: 'Amaka', score: 39, paid: true }
];
// The students who passed
const passing = students.filter((student) => student.score >= 50);
// Add up their scores, then divide by how many there are
const total = passing.reduce((sum, student) => sum + student.score, 0);
const average = total / passing.length;
// toFixed(1) rounds to one decimal place and gives back text
console.log(`Average of the ${passing.length} who passed: ${average.toFixed(1)}`);
OUTPUT TERMINAL
➜ webbo3 node app.js
Average of the 3 who passed: 71.0
Try This
Chain filter and map to list the names of every student from Lagos.
What this code doesPractise: narrow a list, then transform what is left.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
// Students and cities
const students = [
{ name: 'Chidinma', city: 'Lagos' },
{ name: 'Musa', city: 'Kano' },
{ name: 'Tunde', city: 'Lagos' }
];
// Only Lagos, then only the names
const lagosNames = students
.filter((student) => student.city === 'Lagos')
.map((student) => student.name);
console.log(lagosNames);
OUTPUT TERMINAL
➜ webbo3 node app.js
[ 'Chidinma', 'Tunde' ]
Picture This
Picture the assembly line: every station does one job, and the product moves along to the next. When a chain gets confusing, read it one station at a time.
9. Destructuring and Spread
Definition
Destructuring pulls values out of an object or array into their own variables in one line. Spread, written ..., copies everything out of an object or array into a new one.
Real-Life Example
Unpacking a delivery box: you take the items out and put each one where it belongs. That is destructuring. Photocopying a form and then changing one answer on the copy, leaving the original alone: that is spread.
Types
- Object destructuring: take properties out by name.
- Array destructuring: take items out by position.
- Spread: copy an object or array, then add or change things on the copy.
Type 1: Object Destructuring
What this code doesWe want to unpack properties from a record into their own variables, with a default for one that is missing.
app.js
1
2
3
4
5
6
7
8
9
10
// A student record
const student = { name: 'Chidinma', score: 88, city: 'Lagos' };
// Take name and score out into variables of the same names
const { name, score } = student;
console.log(name, score);
// Take city out, and give nickname a default because it is missing
const { city, nickname = 'none' } = student;
console.log(city, nickname);
OUTPUT TERMINAL
➜ webbo3 node app.js
Chidinma 88
Lagos none
Type 2: Array Destructuring
What this code doesWe want to unpack the first two items of a list by their position.
app.js
1
2
3
4
5
6
7
// Medals in finishing order
const medals = ['gold', 'silver', 'bronze'];
// Take the first two out by position
const [first, second] = medals;
console.log(first, second);
OUTPUT TERMINAL
➜ webbo3 node app.js
gold silver
Type 3: Spread
What this code doesWe want to make changed copies of a record while leaving the original exactly as it was.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
// The original record
const base = { name: 'Chidinma', score: 88 };
// Copy it, then change score on the copy
const updated = { ...base, score: 91 };
// Copy it, then add a new property on the copy
const withGrade = { ...base, grade: 'A' };
console.log(base);
console.log(updated);
console.log(withGrade);
OUTPUT TERMINAL
➜ webbo3 node app.js
{ name: 'Chidinma', score: 88 }
{ name: 'Chidinma', score: 91 }
{ name: 'Chidinma', score: 88, grade: 'A' }
Common Mistake: Spread Copies Only One Level
A spread copy shares any nested object or array with the original. Change the nested part through one, and the other sees it too.
What this code doesWe want to see what happens to the original when a nested array is changed through a spread copy.
app.js
1
2
3
4
5
6
7
8
9
10
11
// A record with a nested array
const original = { name: 'Chidinma', scores: [72, 65] };
// Copy the record
const copy = { ...original };
// Change the nested array through the copy
copy.scores.push(99);
// Look at the ORIGINAL
console.log(original.scores);
OUTPUT TERMINAL
➜ webbo3 node app.js
[ 72, 65, 99 ]
For the flat records in this course it rarely matters, but you should recognise it when you meet it.
Overall Example
What this code doesWe want to update a score safely with spread, then unpack the new values to report them.
app.js
1
2
3
4
5
6
7
8
9
10
// A student with a new score to record
const student = { name: 'Tunde', score: 71, city: 'Lagos' };
// Make an updated copy with the new score
const updated = { ...student, score: 76 };
// Unpack what we want to report
const { name, score } = updated;
console.log(`${name}'s new score is ${score}. Old record still says ${student.score}.`);
OUTPUT TERMINAL
➜ webbo3 node app.js
Tunde's new score is 76. Old record still says 71.
Try This
Copy a product record, change its price on the copy, and destructure the name and new price.
What this code doesPractise: copy with a change, then unpack.
app.js
1
2
3
4
5
6
7
8
9
// The product
const product = { name: 'Headphones', price: 15000 };
// A discounted copy
const sale = { ...product, price: 12000 };
// Unpack and report
const { name, price } = sale;
console.log(name, price, product.price);
OUTPUT TERMINAL
➜ webbo3 node app.js
Headphones 12000 15000
Picture This
Picture photocopying a form. The copy is yours to scribble on, and the original stays clean. But if the form has a photo stapled to it, both copies share that one photo, so scribble on the photo and both forms change.
Do This Now
One dataset, four questions that get harder. Write each answer yourself, predict it, then run it and check it against the data by eye.
1. How Many Orders Came From Lagos? (Very Easy)
What this code doesExercise 1: count the orders from one city.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
// Five orders from a small online shop
const orders = [
{ id: 1, customer: 'Chidinma', city: 'Lagos', amount: 12500, status: 'paid' },
{ id: 2, customer: 'Musa', city: 'Kano', amount: 3200, status: 'pending' },
{ id: 3, customer: 'Tunde', city: 'Lagos', amount: 45000, status: 'paid' },
{ id: 4, customer: 'Amaka', city: 'Enugu', amount: 8700, status: 'paid' },
{ id: 5, customer: 'Chidinma', city: 'Lagos', amount: 2100, status: 'failed' }
];
// Keep the Lagos orders, then count them
const lagosOrders = orders.filter((order) => order.city === 'Lagos');
console.log(lagosOrders.length);
OUTPUT TERMINAL
➜ webbo3 node app.js
3
2. Has Any Order Failed? (Simple Application)
What this code doesExercise 2: answer a yes-or-no question about the whole list.
app.js
1
2
3
4
5
6
7
8
9
10
11
// Five orders from a small online shop
const orders = [
{ id: 1, customer: 'Chidinma', city: 'Lagos', amount: 12500, status: 'paid' },
{ id: 2, customer: 'Musa', city: 'Kano', amount: 3200, status: 'pending' },
{ id: 3, customer: 'Tunde', city: 'Lagos', amount: 45000, status: 'paid' },
{ id: 4, customer: 'Amaka', city: 'Enugu', amount: 8700, status: 'paid' },
{ id: 5, customer: 'Chidinma', city: 'Lagos', amount: 2100, status: 'failed' }
];
// Ask whether at least one order has the status failed
console.log(orders.some((order) => order.status === 'failed'));
OUTPUT TERMINAL
➜ webbo3 node app.js
true
3. Total Money Actually Received (Combination)
What this code doesExercise 3: chain filter and reduce to total the paid orders.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Five orders from a small online shop
const orders = [
{ id: 1, customer: 'Chidinma', city: 'Lagos', amount: 12500, status: 'paid' },
{ id: 2, customer: 'Musa', city: 'Kano', amount: 3200, status: 'pending' },
{ id: 3, customer: 'Tunde', city: 'Lagos', amount: 45000, status: 'paid' },
{ id: 4, customer: 'Amaka', city: 'Enugu', amount: 8700, status: 'paid' },
{ id: 5, customer: 'Chidinma', city: 'Lagos', amount: 2100, status: 'failed' }
];
// Only the paid orders, then add up their amounts
const received = orders
.filter((order) => order.status === 'paid')
.reduce((total, order) => total + order.amount, 0);
console.log(`Received: N${received}`);
OUTPUT TERMINAL
➜ webbo3 node app.js
Received: N66200
4. Takings Per City (Small Real-World Problem)
What this code doesExercise 4: build a small dashboard figure: money received, grouped by city.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Five orders from a small online shop
const orders = [
{ id: 1, customer: 'Chidinma', city: 'Lagos', amount: 12500, status: 'paid' },
{ id: 2, customer: 'Musa', city: 'Kano', amount: 3200, status: 'pending' },
{ id: 3, customer: 'Tunde', city: 'Lagos', amount: 45000, status: 'paid' },
{ id: 4, customer: 'Amaka', city: 'Enugu', amount: 8700, status: 'paid' },
{ id: 5, customer: 'Chidinma', city: 'Lagos', amount: 2100, status: 'failed' }
];
// Group the paid amounts by city into one object
const perCity = orders
.filter((order) => order.status === 'paid')
.reduce((totals, order) => {
// Start this city at 0 the first time we see it
totals[order.city] = (totals[order.city] ?? 0) + order.amount;
return totals;
}, {});
console.log(perCity);
OUTPUT TERMINAL
➜ webbo3 node app.js
{ Lagos: 57500, Enugu: 8700 }
Extend It Tonight
Break one on purpose: in exercise 3, use find where you needed filter. Read what happens, and explain it in one sentence.
Checkpoint
Q1. What does [30, 100, 25].sort() give you, and why?
[100, 25, 30]. With no comparison function, sort compares the values as text, and "1" comes before "2". Numbers need sort((a, b) => a - b). It also changes the original array.
What this code doesCheck your answer: run the sort.
app.js
1
2
// Sort numbers with no comparison function
console.log([30, 100, 25].sort());
OUTPUT TERMINAL
➜ webbo3 node app.js
[ 100, 25, 30 ]
Q2. When do you use find rather than filter?
When you want the item itself and expect at most one. filter always hands back an array, so reading .name off it gives undefined.
Q3. Why does [].reduce((a, b) => a + b) throw an error?
With no starting value, reduce uses the first item as the start, and an empty array has no first item. Always pass a starting value, such as , 0.
What this code doesCheck your answer: run it.
app.js
1
2
// reduce an empty list with no starting value
console.log([].reduce((a, b) => a + b));
OUTPUT TERMINAL
➜ webbo3 node app.js
TypeError: Reduce of empty array with no initial value
Q4. What does student.contact?.city ?? "Unknown" give when contact is missing?
?. stops at the missing contact and gives undefined, and ?? replaces undefined with "Unknown".
What this code doesCheck your answer: read the missing city.
app.js
1
2
3
4
// A record with no contact
const student = { name: 'Musa' };
console.log(student.contact?.city ?? 'Unknown');
OUTPUT TERMINAL
➜ webbo3 node app.js
Unknown
Q5. After const copy = { ...original } and copy.scores.push(99), what is in original.scores?
The 99 is there too. Spread copies one level deep, so both objects share the same nested array.
Tick Before You Move On
-
☐
I can read the first and last item of an array
-
☐
I know which array methods change the original
-
☐
I sorted numbers with (a, b) => a - b
-
☐
I hit the "cannot read properties of undefined" error and fixed it with ?.
-
☐
I can explain what a callback is
-
☐
I can say what map, filter, find and reduce each give back
-
☐
I used reduce with a starting value every time
-
☐
I chained filter, map and reduce
-
☐
I can destructure an object and copy it with spread
-
☐
I answered the four order questions and checked them by eye
Quick Recap
Arrays hold ordered lists and count from 0; at(-1) is the clearest way to say "last".
Objects hold named properties, and ?. with ?? reads missing data safely.
A callback is a function you hand to another function, which calls it for every item.
map transforms every item, filter keeps some, find returns one, some and every answer yes or no.
reduce builds one value, and always needs a starting value.
push, pop, shift, unshift and sort change the original; map and filter make new arrays.
Destructuring unpacks values, and spread copies one level deep.
Final Scenario
Yesterday you built a Student Result Analyzer that handled one student at a time. Today you can run it over a whole class.
The data is a list of records. The grading rules are functions. map applies them to everyone, filter picks out who passed, and reduce totals the class.
What this code doesWe want to grade a whole class with Day 2's function and summarise the results in a few lines.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Day 2's grading rule
function gradeFor(score) {
if (score >= 70) return 'A';
if (score >= 60) return 'B';
if (score >= 50) return 'C';
return 'F';
}
// The whole class as a list of records
const students = [
{ name: 'Chidinma', score: 88 },
{ name: 'Musa', score: 54 },
{ name: 'Tunde', score: 71 },
{ name: 'Amaka', score: 39 }
];
// One report line per student
const lines = students.map((student) => `${student.name}: ${gradeFor(student.score)}`);
// How many passed, and the class average
const passed = students.filter((student) => student.score >= 50).length;
const average = students.reduce((sum, student) => sum + student.score, 0) / students.length;
console.log(lines);
console.log(`${passed} of ${students.length} passed. Class average: ${average}`);
OUTPUT TERMINAL
➜ webbo3 node app.js
[ 'Chidinma: A', 'Musa: C', 'Tunde: A', 'Amaka: F' ]
3 of 4 passed. Class average: 63
That is the shape of real work from here on: data as a list of records, rules as functions, and array methods to apply the rules to everything at once.
Tomorrow, Day 4: the DOM and events. Everything you did today happened quietly in a console. Tomorrow the same lists land on the page, and the page starts reacting when people click.