Learn JavaScript

Lesson 1 of 9 ¡ Arrays and the Array Methods: The map, filter, reduce Pipeline

Module 7 ¡ Arrays and the Array Methods: The map, filter, reduce Pipeline

Arrays, the Picture: Ordered Lists and the map, filter, reduce Pipeline

FreeReading

In this lesson

  • Store many values in one array, and read any of them by its index, the last one with at(-1).
  • Grow an array with push and read its size with length.
  • Picture map, filter and reduce as stages that elements flow through, one element at a time.

Maria teaches a quiz class of thirty students. After every quiz she has thirty marks to keep. With what you know so far, that means thirty variables: mark1, mark2, and so on to mark30. Finding the average means typing thirty names in one long line. Adding a thirty-first student means changing the program in four places.

You have already met the fix, quietly. The starter that reads your input keeps every word of it in tokens, and tokens is an array. In this module you learn arrays properly: what they are and every method they have. You also learn the pipeline of map, filter and reduce, which turns most list jobs into one readable line.

An array is an ordered list

An array is one value that holds a list of other values, in order. Each value in it is an element. You write an array with square brackets and commas between the elements. This is called an array literal, because you write its contents literally in the code.

const marks = [72, 45, 88, 91, 60];
console.log(marks);
console.log(marks.length);
[ 72, 45, 88, 91, 60 ]
5
Run in Compiler

The first line prints the whole array. The second prints length, the number of elements. Order matters: [72, 45] and [45, 72] are two different arrays.

An array can hold any values, even a mix: numbers, strings, booleans, other arrays. In practice, keep one kind per array. A list of marks should hold only marks, so every element can be treated the same way.

Indexes start at 0

Each element sits at a numbered position called its index. The first element is at index 0, the second at index 1, and the last at index length - 1. You read an element with the array's name and the index in square brackets.

const marks = [72, 45, 88, 91, 60];
console.log(marks[0]);
console.log(marks[2]);
console.log(marks[marks.length - 1]);
console.log(marks[5]);
72
88
60
undefined
Run in Compiler

The last line asks for index 5, one past the end. JavaScript does not stop the program. It gives undefined, the value that means "nothing here". That is kind, and also dangerous: a wrong index does not shout, it just quietly gives you nothing.

Counting from 0 feels odd for a day or two. Think of the index as "how many elements come before this one". The first element has zero elements before it.

at(-1) reads from the end

Reading the last element as marks[marks.length - 1] is long, and Bob gets the - 1 wrong about once a week. Since ES2022, arrays have at. With a positive number it works like the brackets. With a negative number it counts from the end: at(-1) is the last element, at(-2) the one before it.

const marks = [72, 45, 88, 91, 60];
console.log(marks.at(0));
console.log(marks.at(-1));
console.log(marks.at(-2));
72
60
91
Run in Compiler

Square brackets do not count from the end. marks[-1] is undefined, because -1 is not an index at all.

An array grows

An array is not fixed in size. push adds an element to the end and makes length one bigger. This is how the starter builds out: every line of your answer is pushed onto it, then joined at the end.

const marks = [];
console.log(marks.length);
marks.push(72);
marks.push(45);
marks.push(88);
console.log(marks);
console.log(marks.length);
0
[ 72, 45, 88 ]
3
Run in Compiler

Notice the const. It does not freeze the array. It only means the name marks always points to the same array. The contents of that array can still change, which is why push works on a const array. Module 8 explains this in detail, with references and copies.

You can also write to an index directly: marks[1] = 50 replaces the second element. Writing far past the end leaves gaps, and Lesson 02 shows why that is a trap.

The pipeline picture

Most list jobs are one of three shapes. You want a new value for every element, like a mark with 5 grace marks added. You want only some elements, like the marks of 80 or more. Or you want one answer from all of them, like a total or a count.

JavaScript gives each shape its own method:

  • map calls your function once per element and builds a new array of the answers. The new array has the same length.
  • filter calls your function once per element and keeps the elements for which it returned true. The new array can be shorter.
  • reduce walks the array with a running value, and returns that one value at the end.

None of the three changes the array you call it on. Each returns something new, so you can chain them: the output of one stage is the input of the next. That chain is the pipeline.

const marks = [72, 45, 88, 91, 60];
const graced = marks.map((m) => m + 5);
const high = graced.filter((m) => m >= 80);
const count = high.reduce((c, m) => c + 1, 0);
console.log(graced);
console.log(high);
console.log(count);
console.log(marks);
[ 77, 50, 93, 96, 65 ]
[ 93, 96 ]
2
[ 72, 45, 88, 91, 60 ]
Run in Compiler

The last line proves the point: marks is exactly as it was. The function you pass to each stage is the arrow function of Module 5. (m) => m + 5 takes one element and returns its new value.

Watch the same pipeline one element at a time. Each step moves one element through one stage. The element 45 becomes 50 in map, then fails the test in filter, so it never reaches reduce.

The same steps as a table, one row per step.

StepStageElementOutKeptAccWhat happened
107277map adds 5 grace marks: 72 becomes 77.
2177false77 is under 80, so filter drops it.
304550map adds 5 grace marks: 45 becomes 50.
4150false50 is under 80, so filter drops it.
508893map adds 5 grace marks: 88 becomes 93.
6193true93 is 80 or more, so filter keeps it.
72931reduce counts 93; the count is now 1.
809196map adds 5 grace marks: 91 becomes 96.
9196true96 is 80 or more, so filter keeps it.
102962reduce counts 96; the count is now 2.
1106065map adds 5 grace marks: 60 becomes 65.
12165false65 is under 80, so filter drops it.

A reduce step is easier to trust when you see its running value. Here is a total of the same marks, one row per element.

The same steps as a table, one row per step.

indexmsum beforesum after
072072
14572117
288117205
391205296
460296356

The last row's "sum after" is what reduce returns. The 0 at the end of the call is the start value: the running sum before the first element.

Arrays and the three stages

const a = [x, y, z];                    // an array literal
a[0]          a.at(-1)      a.length    // first element, last element, size
a.push(v);                              // add v at the end
a.map((el) => newValue)                 // new array, same length
a.filter((el) => trueOrFalse)           // new array, the elements that passed
a.reduce((soFar, el) => next, start)    // one value
Example 1: the class report from the input

Maria's input is the number of students, then their marks. This report prints the class average, the highest mark, and the marks of 80 or more. It reads with the starter's first two lines, which you know from Module 1.

const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
const marks = tokens.slice(1).map(Number);

const total = marks.reduce((sum, m) => sum + m, 0);
const best = marks.reduce((hi, m) => (m > hi ? m : hi), marks[0]);
const high = marks.filter((m) => m >= 80);

console.log("average " + total / marks.length);
console.log("best " + best);
console.log("80 or more: " + high.join(" "));
average 73.33333333333333
best 91
80 or more: 88 91 84

Look at the third line of code. tokens.slice(1) drops the first token, the count, and map(Number) turns every remaining string into a number. That one line is a small pipeline of its own.

Run in Compiler
Example 2: how many marks each stage keeps

This program prints one line per stage: its name and the length of the array after it. The chart below draws those numbers.

const marks = [72, 45, 88, 91, 60, 84, 39, 77];
const graced = marks.map((m) => m + 5);
const high = graced.filter((m) => m >= 80);
console.log("input " + marks.length);
console.log("map " + graced.length);
console.log("filter " + high.length);
input 8
map 8
filter 4
Run in Compiler
Elements left after each stage of Example 2 elements input 8 map 8 filter 4
Elements left after each stage of Example 2

Where this is used

  • React: its documentation page "Rendering Lists" builds a list on screen by calling map over an array, and filter to show only some items.
  • Node.js: fs.readdirSync returns the names of the files in a folder as an array of strings.
  • V8: the engine inside Chrome and Node.js stores small arrays of whole numbers in a compact form; Lesson 05 looks inside.

Common mistakes

1. Reading one past the end

const marks = [72, 45, 88];
for (let i = 0; i <= marks.length; i++) {
  console.log(marks[i]);
}
72
45
88
undefined

Bob wrote <= where < belongs. The loop runs four times for three elements, and the fourth read is index 3, which does not exist. JavaScript prints undefined and carries on, so the bug can travel far. Use i < marks.length, or better, a for...of loop that has no index to get wrong.

2. Calling push on something that is not an array yet

let marks;
marks.push(72);
console.log(marks);
TypeError: Cannot read properties of undefined (reading 'push')

Node stops with TypeError: Cannot read properties of undefined (reading 'push'). The variable was declared but never given an array, so it holds undefined, and undefined has no push. Start the list as an empty array: const marks = [];.

3. A missing comma between elements

const marks = [72 45 88];
console.log(marks.length);

Node refuses to run the file and says SyntaxError: Unexpected number. After 72 the parser expects a comma or the closing bracket. A space is not enough to separate two elements.

Brain teaser

The array with a gap

Zara likes to test edges, so she writes to an index far past the end.

const a = [10, 20, 30];
a[6] = 70;
console.log(a.length);

What does it print: 4 or 7? Think about what length really measures.

Exercise 1Easy

Maria runs the school library. When a book comes back, the desk writes how many days late it is. Zero or a negative number means the book came back on time or early. Every late day costs 5 taka. Maria wants the fine of each late book, in the order they came back, and the total of all fines.

Input. The first line holds n, the number of returned books. The second line holds n whole numbers: the days late of each book, in the order they came back.

Output. Two lines. The first line holds the fine of every late book, in their order, separated by single spaces, or the word none if no book was late. The second line holds the total of the fines.

Constraints. 1 <= n <= 100000; -30 <= each number of days <= 60.

Sample.

6
3 0 -2 10 1 0
15 50 5
70
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];

// your code: read with next() and nextInt(), push every line of output to out

console.log(out.join("\n"));
Run in Compiler

Graded as late-returns, a free problem in this module's problem set.

Exercise 2Medium

David sells bus tickets. A return ticket costs twice the one-way fare. He wants every return fare on one line, then how many return fares are over 100 taka, then their total.

Input. n, then n whole one-way fares.

Output. three lines: the return fares in order separated by spaces, the count of return fares over 100, and the total of all return fares.

Constraints. 1 <= n <= 1000. 1 <= each fare <= 500.

Sample. the input below.

5
40 75 50 120 30
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];

// your code: read with next() and nextInt(), push every line of output to out

console.log(out.join("\n"));

Sample output.

80 150 100 240 60
2
630
Run in Compiler
Exercise 3Hard

Kenji logs the temperature at noon each day. He wants the biggest change between two neighbouring days, up or down. He also wants the day it happened: the later of the two days, counting from 1. If two changes tie, report the earlier day.

Input. n, then n whole temperatures.

Output. one line: the biggest change, a space, and the day.

Constraints. 2 <= n <= 1000. -20 <= each temperature <= 50.

Sample. the input below.

6
21 25 19 19 30 27
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];

// your code: read with next() and nextInt(), push every line of output to out

console.log(out.join("\n"));

Sample output.

11 5
Run in Compiler

Common doubts

  • Why does the index start at 0 and not 1?

    Most languages that JavaScript grew from, like C, count from 0, because the index says how far an element is from the start. The first element is zero steps away. You get used to it within a week of practice.

  • Can one array hold numbers and strings together?

    Yes, JavaScript allows it. But a mixed list is hard to process, because every stage must check what each element is. Keep one kind of value per array, and use an object (Module 8) when a record needs several kinds.

  • Is at(-1) available everywhere?

    It joined the language in ES2022 and runs on Node 22 and every current browser. Very old browsers lack it, and there a[a.length - 1] does the same job.

  • Do I still need loops now that I have map, filter and reduce?

    Yes. A loop is still the clearest tool when you must stop early or carry state from one element to the next. Lesson 04 gives the rule for choosing.

Key takeaways

  • An array is an ordered list of elements, written [a, b, c], with length as its size.
  • Indexes start at 0; a read past the end gives undefined, not an error.
  • at(-1) reads the last element; square brackets never count from the end.
  • push adds to the end, and works on a const array because const fixes only the name.
  • map, filter and reduce return something new and leave the array as it was, so they chain into a pipeline.
  • Go deeper: Under the Hood, Arrays as Objects, V8's Elements Kinds, and What Holes, shift and Spread Cost (Pro).

Next: every array method, one by one, with what it returns and whether it changes the array.

End of lesson 1

Mark it done, and your progress moves with you.

Next: Every Array Method, One by One, With What It Returns