Webbo3 Level 2 · JavaScript Development · Day 3 of 20
Arrays, Objects and Reshaping Data
Real applications are mostly data being filtered, transformed and totalled. Today you learn the four methods 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
- Transform a whole list with
mapinstead of writing a loop - Narrow a list with
filterand pull one item out withfind - Total anything with
reducewithout being frightened of it - Unpack values with destructuring and copy safely with the spread operator
Open any application you use. A feed is an array of posts. A cart is an array of items. A dashboard is an array 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. Today is the day that stops being intimidating.
1. Arrays: an ordered list
const courses = ['HTML', 'CSS', 'Tailwind', 'JavaScript'];
console.log(courses.length);
console.log(courses[0]); // counting starts at zero
console.log(courses[courses.length - 1]);
console.log(courses.at(-1)); // the same thing, said properlycourses[0] is the first item, not the second. Every off-by-one bug you will ever write starts here. at(-1) for the last item is newer and clearer than length - 1, and it works in every browser you need to care about.
Adding and removing
const cart = ['rice'];
cart.push('beans'); // add to the end
cart.unshift('oil'); // add to the front
console.log(cart);
const removed = cart.pop(); // take from the end
console.log(removed, cart);The method that surprises everyone
Most array methods you meet today return a new array and leave the original alone. But push, pop, shift, unshift, sort, reverse and splice change the original in place. Sorting a list you were also displaying elsewhere, and finding that the other place changed too, is a real bug with a real cause.
const scores = [30, 100, 25];
const sortedBadly = scores.sort(); // sort mutates, AND sorts as text
console.log('after sort:', scores);
console.log('same array?', scores === sortedBadly);
const safe = [30, 100, 25];
const sortedNumerically = [...safe].sort((a, b) => a - b);
console.log('copy sorted:', sortedNumerically, 'original:', safe);Two lessons in one output there. sort() with no arguments converts everything to text and sorts alphabetically, which is why 100 landed before 25 (because "1" comes before "2"). And it changed the original array rather than handing back a new one. Sorting numbers always needs the comparison function (a, b) => a - b.
2. Objects: a record with named fields
const student = {
name: 'Chidinma Okafor',
age: 22,
enrolled: true,
scores: [72, 65, 88],
contact: { email: '[email protected]', city: 'Lagos' }
};
console.log(student.name);
console.log(student.contact.city); // dot into a nested object
console.log(student.scores[2]); // index into a nested array
console.log(student.nickname); // a field that does not existAsking for a field that does not exist gives undefined rather than an error. That sounds forgiving until you go one level deeper:
const student = { name: 'Musa' };
console.log(student.contact.city);student.contact is undefined, and you cannot read a property of undefined. This is the most common runtime error in JavaScript, and the fix is optional chaining: ?. stops and returns undefined instead of throwing.
const student = { name: 'Musa' };
console.log(student.contact?.city); // undefined, no crash
console.log(student.contact?.city ?? 'Unknown'); // supply a fallbackTwo operators worth learning together
?. stops safely when something is missing. ?? supplies a fallback only when the value is null or undefined. Use ?? rather than || for fallbacks, because || also replaces 0 and '', which are often legitimate values.
3. A list of records: the shape of everything
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 }
];
console.log(students.length, 'records');
console.table(students);Hold that array in your head for the rest of the day. Every method below is asked of exactly this shape, and so is every API response you fetch next week.
4. forEach: do something with each one
const students = [
{ name: 'Chidinma', score: 88 },
{ name: 'Musa', score: 54 }
];
students.forEach((student, index) => {
console.log(`${index + 1}. ${student.name} scored ${student.score}`);
});forEach returns nothing. It is for side effects: printing, or later, putting things on the page. The moment you want a new list out the other end, you want map instead.
5. map: turn every item into something else
const students = [
{ name: 'Chidinma', score: 88 },
{ name: 'Musa', score: 54 },
{ name: 'Amaka', score: 39 }
];
const names = students.map((s) => s.name);
const passed = students.map((s) => `${s.name}: ${s.score >= 50 ? 'pass' : 'fail'}`);
console.log(names);
console.log(passed);
console.log('original untouched:', students.length);map always returns a new array of exactly the same length. One item in, one item out. If you find yourself wanting fewer items out than went in, you want filter, not map.
The mistake that produces a list of undefined
Writing students.map((s) => { s.name }) with braces and no return gives you an array of undefined, one for each student. Same trap as Day 2: braces mean you must return yourself.
const students = [{ name: 'Chidinma' }, { name: 'Musa' }];
console.log(students.map((s) => { s.name; })); // braces, no return
console.log(students.map((s) => s.name)); // no braces, implicit return6. filter and find
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 }
];
const passing = students.filter((s) => s.score >= 50);
const unpaid = students.filter((s) => !s.paid);
const tunde = students.find((s) => s.name === 'Tunde');
const missing = students.find((s) => s.name === 'Zainab');
console.log('passing:', passing.length);
console.log('unpaid:', unpaid.map((s) => s.name));
console.log('found:', tunde);
console.log('not found:', missing);| Method | Gives you back | When nothing matches |
|---|---|---|
filter | A new array of every match | An empty array [] |
find | The first matching item itself | undefined |
some | true if at least one matches | false |
every | true only if all match | true for an empty array |
filter gives an array even when there is one match
students.filter(s => s.name === 'Tunde').name is undefined, because you asked an array for a name. You wanted find. This is a weekly mistake for the first month and then never again.
7. reduce: the one people avoid
reduce takes a whole list and boils it down to a single value. It looks harder than it is, because it has two parameters instead of one: what you have built so far, and the current item.
const prices = [1500, 2000, 750];
// start at 0, add each price to the running total
const total = prices.reduce((runningTotal, price) => runningTotal + price, 0);
console.log(total);The 0 at the end is the starting value, and leaving it out is the classic reduce bug: on an empty array with no starting value, reduce throws instead of giving you 0.
console.log([].reduce((a, b) => a + b));console.log([].reduce((a, b) => a + b, 0)); // with a starting value: finereduce is not only for totals
const students = [
{ name: 'Chidinma', city: 'Lagos' },
{ name: 'Musa', city: 'Kano' },
{ name: 'Tunde', city: 'Lagos' }
];
// group a flat list into buckets: the shape every dashboard needs
const byCity = students.reduce((groups, student) => {
groups[student.city] = groups[student.city] ?? [];
groups[student.city].push(student.name);
return groups;
}, {});
console.log(byCity);8. Chaining: where it all comes together
Because filter and map each return a new array, you can hang them off each other. This is what professional data handling actually looks like.
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 }
];
const report = students
.filter((s) => s.paid) // only paid students
.filter((s) => s.score >= 50) // who passed
.map((s) => ({ ...s, grade: s.score >= 70 ? 'A' : 'C' }))
.map((s) => `${s.name} (${s.grade})`);
console.log(report);
const averageOfPassing = students
.filter((s) => s.score >= 50)
.reduce((sum, s, _i, list) => sum + s.score / list.length, 0);
console.log('average of passing:', averageOfPassing.toFixed(1));9. Destructuring and spread
const student = { name: 'Chidinma', score: 88, city: 'Lagos' };
// pull fields out into their own names
const { name, score } = student;
console.log(name, score);
// rename while unpacking, and supply a default
const { city, nickname = 'none' } = student;
console.log(city, nickname);
// arrays destructure by position
const [first, second] = ['gold', 'silver', 'bronze'];
console.log(first, second);const base = { name: 'Chidinma', score: 88 };
const updated = { ...base, score: 91 }; // copy, then override
const withGrade = { ...base, grade: 'A' }; // copy, then add
console.log(base);
console.log(updated);
console.log(withGrade);
console.log('different objects?', base !== updated);Spread copies one level deep, and only one
A spread copy shares any nested object or array with the original. Change the nested part through one and the other sees it too. For the flat records in this course that never bites, but you should know it before you meet it.
const original = { name: 'Chidinma', scores: [72, 65] };
const copy = { ...original };
copy.scores.push(99); // reaching into the SHARED array
console.log('original:', original.scores);Do this now
One dataset, six questions. Write each answer as a chain, print it, and check it by eye against the data.
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' }
];
console.log('1. total paid:', orders.filter(o => o.status === 'paid').reduce((s, o) => s + o.amount, 0));
console.log('2. customers:', [...new Set(orders.map(o => o.customer))]);
console.log('3. lagos orders:', orders.filter(o => o.city === 'Lagos').length);
console.log('4. biggest:', orders.reduce((top, o) => o.amount > top.amount ? o : top).customer);
console.log('5. any failed?', orders.some(o => o.status === 'failed'));
console.log('6. all above 1000?', orders.every(o => o.amount > 1000));- Rewrite question 1 as a
forloop, then look at both. Keep the one you would rather read in six weeks. - Add a question of your own: total per city, as an object.
reduceinto{}. - Break one deliberately: use
findwhere you neededfilterand read the error.
Checkpoint
Q1. What does `[30, 100, 25].sort()` give you, and why?
[100, 25, 30]. With no comparison function, sort converts each value to text and sorts alphabetically, so "100" comes before "25". Numbers need sort((a, b) => a - b). It also mutates the original array.
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?
With no starting value, reduce uses the first element as the start, and an empty array has none. Always pass the starting value: , 0 for a sum, , {} for grouping, , [] for building a list.
Q4. `const copy = { ...original }` then `copy.scores.push(99)`. What happened to `original.scores`?
It got the 99 too. Spread copies one level deep, so both objects point at the same nested array.
Tick before you move on
- ☐ I answered all six questions with chains and checked them by eye
- ☐ I hit the "cannot read properties of undefined" error and fixed it with
?. - ☐ I used
reducewith a starting value every time - ☐ I can say out loud what
map,filter,findandreduceeach return
Quick recap
Arrays index from zero, and at(-1) is the honest way to say "last" · push, sort and reverse mutate; map and filter return new arrays · map for same-length transforms, filter for fewer, find for one · reduce always takes a starting value · ?. to read safely, ?? to supply a fallback, spread to copy and override
Tomorrow, Day 4: the DOM. Everything you did today was invisible in a console. Tomorrow it lands on the page.