Courses Learn Blog Contact
Get Started →
Lesson 3

Day 3: Arrays, Objects and Reshaping Data

map, filter, find and reduce on a real list of records, plus the mutation, optional chaining and spread traps that catch everyone once.

Webbo3 Level 2: JavaScript Development 20 min read Free

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 map instead of writing a loop
  • Narrow a list with filter and pull one item out with find
  • Total 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 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

JSapp.js
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 properly
Console

courses[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

JSapp.js
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);
Console

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.

JSapp.js
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);
Console

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

JSapp.js
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 exist
Console

Asking for a field that does not exist gives undefined rather than an error. That sounds forgiving until you go one level deeper:

JSapp.js
const student = { name: 'Musa' };
console.log(student.contact.city);
Console

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.

JSapp.js
const student = { name: 'Musa' };

console.log(student.contact?.city);              // undefined, no crash
console.log(student.contact?.city ?? 'Unknown'); // supply a fallback
Console

Two 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

JSapp.js
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);
Console

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

JSapp.js
const students = [
  { name: 'Chidinma', score: 88 },
  { name: 'Musa', score: 54 }
];

students.forEach((student, index) => {
  console.log(`${index + 1}. ${student.name} scored ${student.score}`);
});
Console

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

JSapp.js
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);
Console

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.

JSapp.js
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 return
Console

6. filter and find

JSapp.js
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);
Console
MethodGives you backWhen nothing matches
filterA new array of every matchAn empty array []
findThe first matching item itselfundefined
sometrue if at least one matchesfalse
everytrue only if all matchtrue 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.

JSapp.js
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);
Console

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.

JSapp.js
console.log([].reduce((a, b) => a + b));
Console
JSapp.js
console.log([].reduce((a, b) => a + b, 0));   // with a starting value: fine
Console

reduce is not only for totals

JSapp.js
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);
Console

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.

JSapp.js
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));
Console

9. Destructuring and spread

JSapp.js
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);
Console
JSapp.js
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);
Console

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.

JSapp.js
const original = { name: 'Chidinma', scores: [72, 65] };
const copy = { ...original };

copy.scores.push(99);                 // reaching into the SHARED array
console.log('original:', original.scores);
Console

Do this now

One dataset, six questions. Write each answer as a chain, print it, and check it by eye against the data.

JSapp.js
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));
Console
  1. Rewrite question 1 as a for loop, then look at both. Keep the one you would rather read in six weeks.
  2. Add a question of your own: total per city, as an object. reduce into {}.
  3. Break one deliberately: use find where you needed filter and 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 reduce with a starting value every time
  • I can say out loud what map, filter, find and reduce each 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.

Learn this with a real instructor

Free lessons take you a long way on your own. The full programmes add live classes, assignments, Skill Points and a certificate — built for African students.