Functions and arrays
Assignment
Martina Hostettler is preparing the next board meeting and has written down two wishes.
As a board member, I want to know how many things we have in each category, so that we can decide where to buy new things next.
As a volunteer at the counter, I want to see which tools are in right now, sorted by name, so that I can answer members on the phone quickly.
The acceptance criteria are:
- The overview shows the number of things for every category.
- It names the tools that are in, sorted alphabetically, with names that start with an umlaut in the right place too.
- The catalog itself stays unchanged.
Learning goals
After this chapter you can
- pass functions as arguments and read callbacks,
- analyze lists with
map,filter,find,someandevery, - compute sums and counts with
reduce, - sort lists with
toSortedandlocaleComparewithout changing the original, - write functions that build other functions.
You know arrow functions and closures from the unit Functions and scope of the JS workshop.
Functions as arguments
Section titled “Functions as arguments”A function that you pass to another function so that it calls it is called a callback. You know this from addEventListener. Arrays also have methods that call a callback for every element.
function forEachItem(items, callback) { for (const item of items) callback(item)}
const items = [{ name: 'Drill' }, { name: 'Ladder' }]
forEachItem(items, (item) => console.log(item.name))items.forEach((item, index) => console.log(index, item.name))forEachItem shows what happens. The loop sits inside the function, and the callback says what should happen with every element. forEach is the built-in version and passes the callback the position of the element as well. The following methods all work this way and differ in what they do with the results of the callback.
map and filter
Section titled “map and filter”map calls the callback for every element and builds a new list from the return values, as long as the old one. filter keeps the elements for which the callback returns true.
const items = [ { id: 1, name: 'Drill', category: 'Tools', deposit: 20, availableFrom: null }, { id: 2, name: 'Tent for four', category: 'Camping', deposit: 50, availableFrom: '2026-11-12' }, { id: 5, name: 'Jigsaw', category: 'Tools', deposit: 20, availableFrom: null }, { id: 11, name: 'Cordless screwdriver', category: 'Tools', deposit: 20, availableFrom: '2026-11-05' },]
const ids = items.map((item) => item.id)console.log(ids)
const available = items.filter((item) => item.availableFrom === null)console.log(available.length)
const toolsThatAreIn = items .filter((item) => item.category === 'Tools' && item.availableFrom === null) .map((item) => item.name)console.log(toolsThatAreIn)Neither method changes the old list, both return a new one. That's why you can chain them. The last statement first filters the tools that are in and then turns the remaining things into a list of names. This chain is half of Martina Hostettler's second wish.
find, some and every
Section titled “find, some and every”Sometimes you need just one element or a yes or no answer.
const items = [ { id: 1, name: 'Drill', category: 'Tools', deposit: 20, availableFrom: null }, { id: 2, name: 'Tent for four', category: 'Camping', deposit: 50, availableFrom: '2026-11-12' }, { id: 5, name: 'Jigsaw', category: 'Tools', deposit: 20, availableFrom: null }, { id: 11, name: 'Cordless screwdriver', category: 'Tools', deposit: 20, availableFrom: '2026-11-05' },]
const tent = items.find((item) => item.id === 2)console.log(tent.name, items.findIndex((item) => item.id === 2))console.log(items.find((item) => item.id === 99))console.log(items.some((item) => item.deposit > 40), items.every((item) => item.deposit >= 20))find returns the first element for which the callback returns true, or undefined if none matches. findIndex returns the position or -1. some asks whether at least one element matches, every whether all do. All four stop as soon as the answer is clear and check no more elements than necessary.
reduce
Section titled “reduce”reduce combines a list into a single value. The callback receives the result so far and the next element and returns the new result. The second argument of reduce is the starting value.
const items = [ { id: 1, name: 'Drill', category: 'Tools', deposit: 20, availableFrom: null }, { id: 2, name: 'Tent for four', category: 'Camping', deposit: 50, availableFrom: '2026-11-12' }, { id: 5, name: 'Jigsaw', category: 'Tools', deposit: 20, availableFrom: null }, { id: 11, name: 'Cordless screwdriver', category: 'Tools', deposit: 20, availableFrom: '2026-11-05' },]
const total = items.reduce((sum, item) => sum + item.deposit, 0)console.log(total)
const perCategory = items.reduce((counts, item) => { counts[item.category] = (counts[item.category] ?? 0) + 1 return counts}, {})console.log(perCategory)
const groups = Object.groupBy(items, (item) => item.category)console.log(Object.keys(groups), groups['Tools'].length)The first count starts at 0 and adds up all deposits. The second starts with an empty object and counts for every category how often it occurs. counts[item.category] ?? 0 returns 0 as long as a category is still missing. Since 2024 there is also Object.groupBy for grouping, which collects the things of a category in a list. For a count, reduce is enough.
Sorting without side effects
Section titled “Sorting without side effects”sort sorts an array in place and thereby changes the original. toSorted returns a sorted copy. A compare function says how to compare. It returns a negative number if a comes before b, a positive one if b comes before a, and 0 if both are equal.
const items = [ { name: 'Jigsaw', deposit: 20 }, { name: 'Tent for four', deposit: 50 }, { name: 'Cordless screwdriver', deposit: 20 }, { name: 'Projector', deposit: 80 },]
const byName = items.toSorted((a, b) => a.name.localeCompare(b.name, 'en-US'))console.log(byName.map((item) => item.name))
const byDeposit = items.toSorted((a, b) => b.deposit - a.deposit)console.log(byDeposit.map((item) => item.deposit))console.log(items[0].name)localeCompare compares text by the rules of a language and puts upper and lower case and umlauts in the right place. For numbers a subtraction is enough, and b.deposit - a.deposit sorts in descending order. The last line shows that the original kept its order.
Functions that build functions
Section titled “Functions that build functions”A function can return a function that remembers its arguments. That way you get a tailor-made callback for filter.
// Returns a function that remembers the category it was made forfunction byCategory(category) { return (item) => item.category === category}
const items = [ { name: 'Drill', category: 'Tools' }, { name: 'Tent for four', category: 'Camping' }, { name: 'Jigsaw', category: 'Tools' },]
const isTool = byCategory('Tools')console.log(items.filter(isTool).length, items.filter(byCategory('Camping')).length)byCategory('Tools') returns a new function that only needs a thing and knows the category from its closure. Such functions make code readable, and items.filter(isTool) says what it does. In the challenge you build a filter from several criteria this way.
In code
Section titled “In code”Work with the example on reduce. Read the code and write down what you expect before you run it.
const items = [ { id: 1, name: 'Drill', category: 'Tools', deposit: 20, availableFrom: null }, { id: 2, name: 'Tent for four', category: 'Camping', deposit: 50, availableFrom: '2026-11-12' }, { id: 5, name: 'Jigsaw', category: 'Tools', deposit: 20, availableFrom: null }, { id: 11, name: 'Cordless screwdriver', category: 'Tools', deposit: 20, availableFrom: '2026-11-05' },]
const total = items.reduce((sum, item) => sum + item.deposit, 0)console.log(total)
const perCategory = items.reduce((counts, item) => { counts[item.category] = (counts[item.category] ?? 0) + 1 return counts}, {})console.log(perCategory)
const groups = Object.groupBy(items, (item) => item.category)console.log(Object.keys(groups), groups['Tools'].length)- What does the first count print if you leave out the starting value 0? And the second one without
{}? - Change the second count so that it returns the sum of the deposits for every category instead of the number of things.
- What does the count look like if you write it with a
for...ofloop? Which is easier to read? - What does
[].reduce((sum, item) => sum + item.deposit)print, without a starting value and with an empty list?
Without a starting value, reduce takes the first element as the starting value. In the first count that's an object, and the sum turns into text. In the second, reduce counts into the drill, changes it in the process and leaves the drill itself out of the count. With an empty list and no starting value, reduce reports a TypeError. So always give the starting value.
Exercises
Section titled “Exercises”The exercises build the reports for the board meeting. You find the project in the course repo in the folder exercises/01-javascript/arrays/en, with the whole catalog in data/items.json. Copy the folder, install the packages and run the tests.
npm installnpm testnpm run overview shows the overview that uses your functions. At the start it is empty.
Exercise Getting started
Write the function availableNames(items, category) in src/catalog.js. It returns the names of the things of a category that are in, sorted by the rules of the language. For the tools these are eight names, from the circular saw to the toolbox.
Solution
// Names of the things of one category that are in, sorted by nameexport function availableNames(items, category) { return items .filter((item) => item.category === category && item.availableFrom === null) .map((item) => item.name) .toSorted((a, b) => a.localeCompare(b, 'en-US'))}filter keeps the tools that are in, map turns them into names, and toSorted sorts them with localeCompare. Without localeCompare, the oil can from the second test would come before the awl, because a capital letter has a lower code than a lowercase one.
Exercise Going further
Write the function countByCategory(items). It returns an object that gives the number of things for every category. In the catalog there are ten per category.
Solution
// How many things there are per category, as an object with the names of the categories as keysexport function countByCategory(items) { return items.reduce((counts, item) => { counts[item.category] = (counts[item.category] ?? 0) + 1 return counts }, {})}The starting value is an empty object, and every round adds one to the category of the thing. The return counts at the end of the callback matters. Without it, the result of the next round would be undefined.
Exercise Challenge
Write the function createFilter(criteria). It receives an object with the criteria category, query and onlyAvailable, each of them optional, and returns a function that filter can use. The search text should occur in the name regardless of upper and lower case.
Solution
// Builds a test for filter from the criteria category, query and onlyAvailable, each of them optionalexport function createFilter({ category, query, onlyAvailable = false } = {}) { const wanted = query?.trim().toLowerCase() return (item) => (!category || item.category === category) && (!wanted || item.name.toLowerCase().includes(wanted)) && (!onlyAvailable || item.availableFrom === null)}The function takes the criteria apart right in its parameter list. It prepares the search text once, not for every thing. The returned function checks each criterion only if it is given, and !category || lets every thing through as long as no category is set. You will need this filter again in part 3, when the catalog in Vue gets a search field.
Common mistakes
Section titled “Common mistakes”| Mistake | What helps |
|---|---|
map with curly braces returns a list full of undefined. | Write return in the block or leave out the braces. |
The result of forEach is used and is undefined. | forEach returns nothing. Use map or filter for a new list. |
find finds nothing, and accessing .name throws a TypeError. | Check the result or write ?.name. |
reduce without a starting value reports a TypeError for an empty list. | Always give the starting value. |
sort changes the catalog for everyone who uses it. | Use toSorted. |
Numbers are sorted as text with sort(), and 10 comes before 9. | Pass a compare function such as (a, b) => a - b. |
An AI assistant installs a library such as Lodash for groupBy or sortBy. | reduce, Object.groupBy and toSorted do this without a package. |
Self-check
Section titled “Self-check”Self-check
- What is the difference between
mapandforEach? - What does
findreturn if no element matches, and what doesfilterreturn? - What is the second argument of
reducefor? - Why does the course use
toSortedinstead ofsort?
Answers
mapreturns a new list with the return values of the callback.forEachonly calls the callback and returnsundefined.findreturnsundefined,filteran empty list.- It is the starting value. Without it,
reducetakes the first element and reports an error for an empty list. toSortedreturns a sorted copy and leaves the original unchanged.sortchanges the array itself, a side effect that surprises other parts of the code.
Assignment
The reports convinced the board. Martina Hostettler has good news. From now on the catalog lives on the association's server, and the volunteers maintain it there directly. The overview should load it from there. But a server doesn't answer right away, and sometimes not at all. The next chapter shows how JavaScript deals with this kind of waiting.
Text and figures are licensed under CC BY-SA 4.0, the code under the MIT License and the course data under CC0 1.0. The figures use the font Kalam under the SIL Open Font License 1.1. The sources are on GitHub.