JavaScript (ES2026) Module 6 – Arrays and array methods
Transforming arrays with map, filter and forEach
Transform JavaScript arrays with map, keep items with filter, know when forEach or a loop is clearer, and see what a long chain of methods costs.
What you will learn
- Transform and select items with map and filter
- Decide when forEach or a plain loop is clearer
- Combine methods in chains without hiding performance problems
Before you start
What you’ll learn · Before you start
What you will learn
- Transform and select items with map and filter
- Decide when forEach or a plain loop is clearer
- Combine methods in chains without hiding performance problems
Before you start
On this page
map(), filter() and forEach() each call a function you give them once for every item of an array, and they
differ in what they hand back. map(fn) returns a new array of the same length, holding what fn returned for
each item: marks in, grades out. filter(fn) returns a new array of the items for which fn returned a truthy
value: all the results, then only the passes. forEach(fn) returns undefined; it is for what fn does, such as
printing or adding to a total. None of the three changes the array it is called on. Because map() and filter()
return arrays, they chain: results.filter(passed).map(toName). This lesson runs all three, shows when a plain loop
is clearer, explains the classic map(parseInt) surprise and measures what a long chain costs on a million items.
map and filter: new arrays from old ones
// A class's marks out of 100. map() turns each result into something new; filter() keeps the results that pass a test.
const results = [
{ name: 'asha verma', marks: 72 },
{ name: 'ravi kumar', marks: 35 },
{ name: 'meena iyer', marks: 91 },
{ name: 'kiran rao', marks: 48 },
{ name: 'arjun das', marks: 60 },
];
const grade = (marks) => (marks >= 90 ? 'A' : marks >= 60 ? 'B' : marks >= 40 ? 'C' : 'F');
const grades = results.map((r) => grade(r.marks));
console.log('grades:', grades);
const passed = results.filter((r) => r.marks >= 40);
console.log('passed:', passed.length, 'of', results.length, '| the same objects:', passed[0] === results[0]);
const firstClass = results
.filter((r) => r.marks >= 60)
.map((r) => `${r.name}: ${r.marks} (${grade(r.marks)})`);
console.log(firstClass);
console.log('results is unchanged:', results.length, results[1].marks); Output
grades: [ 'B', 'F', 'A', 'C', 'B' ] passed: 4 of 5 | the same objects: true [ 'asha verma: 72 (B)', 'meena iyer: 91 (A)', 'arjun das: 60 (B)' ] results is unchanged: 5 35
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node marks-report.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
results.map((r) => grade(r.marks)) calls the arrow function for each of the five results and collects the five
grades it returns into a new array, in the same order. map() always returns exactly as many items as it was given,
so it is the method for “turn each item into something else”: a price into a price with tax, a record into a line of
text, a name into its initials.
results.filter((r) => r.marks >= 40) keeps the results whose test returns true, four of the five here. The kept
items are the same objects, not copies, which passed[0] === results[0] shows: change a mark through passed and it
changes in results too. filter() never changes the order, and when nothing passes it returns an empty array, never
undefined.
Since both return arrays, one can feed the other: filter() first, to keep the first-class results, then map(), to
turn each into a line of the report. Putting filter() first means map() does less work. The last line of output
shows that results itself was left alone, as the specification promises: neither method changes the array it is
called on, although your function still could.
What map(), filter() and forEach() give back
Text description of the diagram
The diagram starts from five marks: 72, 35, 91, 48 and 60, and shows three calls on them, one below the other.
- marks.map(grade) returns a new array with one result for each item, so it always has 5 items: 'B', 'F', 'A', 'C' and 'B'.
- marks.filter((m) => m >= 40) returns a new array with only the items for which the function returned true, here 4 of them: 72, 91, 48 and 60.
- marks.forEach(print) calls print once for each of the 5 items and returns undefined. Its use is what the function does, such as printing, not a value.
None of the three changes the marks array.
forEach: for what the function does
forEach() is the odd one out. It calls the function for every item and then returns undefined, whatever the
function returned:
// forEach() calls a function for each item and always returns undefined. A return inside that function ends one
// call, not the loop, so forEach() cannot stop early or hand back an answer.
const orders = [120, 0, 450, 75];
console.log('forEach returned:', orders.forEach((amount) => amount * 2));
function hasZeroWrong(list) {
list.forEach((amount) => {
if (amount === 0) return true; // returns from the arrow function, not from hasZeroWrong
});
return false;
}
console.log('hasZeroWrong:', hasZeroWrong(orders), '| some():', orders.some((amount) => amount === 0));
let visited = 0;
orders.forEach((amount) => {
visited++;
if (amount === 0) return;
});
console.log('forEach visited', visited, 'orders');
visited = 0;
for (const amount of orders) {
visited++;
if (amount === 0) break;
}
console.log('for...of with break visited', visited, 'orders');
let total = 0;
orders.forEach((amount) => {
total += amount;
});
console.log('a side effect is what forEach is for: total', total); Output
forEach returned: undefined hasZeroWrong: false | some(): true forEach visited 4 orders for...of with break visited 2 orders a side effect is what forEach is for: total 645
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node foreach-return.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
hasZeroWrong() is a bug that runs without any error. Its return true returns from the arrow function, and forEach()
ignores that value and moves to the next order; hasZeroWrong() then reaches its own return false. The visit count
shows the other half of the problem: forEach() went through all four orders, although the second one was already
the answer. There is no way to stop it early short of throwing an error.
So the choice follows the job:
- To build a new array, use
map()orfilter(), notforEach()withpush(). - To answer a question, use the searching methods of the previous lesson:
some(),every(),find(), which stop as soon as they know. - To stop part-way, to
awaitinside the loop (the module on asynchronous code shows whyforEach()does not wait), or when the body is long, usefor…of, which hasbreakandcontinue. forEach()fits short side effects on every item, such as adding to a total or printing each line.
The function gets three arguments
map(), filter(), forEach() and the searching methods call your function with three arguments: the item, its
index and the whole array. Most functions use only the first, but a function that accepts more parameters gets them
all:
// map(), filter() and forEach() call their function with three arguments: the item, its index and the array.
console.log(['a', 'b'].map((item, index, array) => `${item} at ${index} of ${array.length}`));
// parseInt(text, radix) takes a second argument, so map() hands it the index as the radix.
const inputs = ['10', '10', '10', '10'];
console.log('map(parseInt):', inputs.map(parseInt));
console.log('one argument only:', inputs.map((text) => parseInt(text, 10)), inputs.map(Number));
// filter() keeps an item when the function returns a truthy value, so filter(Boolean) drops the falsy ones.
console.log('filter(Boolean):', [0, 'tea', '', null, 'rice', undefined].filter(Boolean)); Output
[ 'a at 0 of 2', 'b at 1 of 2' ] map(parseInt): [ 10, NaN, 2, 3 ] one argument only: [ 10, 10, 10, 10 ] [ 10, 10, 10, 10 ] filter(Boolean): [ 'tea', 'rice' ]
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node callback-arguments.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
inputs.map(parseInt) looks like “turn each string into a number” and gives [10, NaN, 2, 3]. parseInt() has a
second parameter, the radix, the base of the number system, and map() fills it with the index. parseInt('10', 0) is 10, because 0 means “work it out”; base 1 does not exist, so the second call gives NaN; and '10' read in
base 2 and base 3 is 2 and 3. Wrap the function so that it receives only what you mean, (text) => parseInt(text, 10), or use Number, which takes a single argument. The same care applies to any function that you pass by name.
The last line passes a function by name on purpose: filter(Boolean) calls Boolean(item) for each item and keeps the
truthy ones, which drops 0, '', null and undefined in one go. Be sure that is what you want: it also drops a
real 0 from a list of numbers.
What a chain costs
A chain reads well, and each step builds a whole new array before the next step starts. On small arrays that does not matter. On large ones it costs memory and time:
// One million orders: the paid ones, in rupees, of at least 1,000. Three ways to count them. Times are the fastest
// of five runs, in milliseconds.
const orders = Array.from({ length: 1_000_000 }, (_, i) => ({
id: i,
amountPaise: (i * 7919) % 500_000,
status: i % 10 === 0 ? 'refunded' : 'paid',
}));
function chained() {
const paid = orders.filter((o) => o.status === 'paid');
const rupees = paid.map((o) => o.amountPaise / 100);
const large = rupees.filter((r) => r >= 1000);
return { count: large.length, built: [paid.length, rupees.length, large.length] };
}
function loop() {
let count = 0;
for (const o of orders) {
if (o.status !== 'paid') continue;
if (o.amountPaise / 100 >= 1000) count++;
}
return { count };
}
function helpers() {
const large = orders
.values()
.filter((o) => o.status === 'paid')
.map((o) => o.amountPaise / 100)
.filter((r) => r >= 1000);
let count = 0;
for (const _ of large) count++;
return { count };
}
function fastest(task) {
let best = Infinity;
let result;
for (let run = 0; run < 5; run++) {
const start = performance.now();
result = task();
best = Math.min(best, performance.now() - start);
}
return [result, best];
}
const [a, chainMs] = fastest(chained);
const [b, loopMs] = fastest(loop);
const [c, helperMs] = fastest(helpers);
const fmt = (n) => n.toLocaleString('en-US');
console.log(`orders: ${fmt(orders.length)}; paid and at least 1,000 rupees: ${fmt(a.count)} (all three agree: ${a.count === b.count && b.count === c.count})`);
console.log(`filter, map, filter: ${chainMs.toFixed(1).padStart(6)} ms, building arrays of ${a.built.map(fmt).join(', ')} items`);
console.log(`one for...of loop: ${loopMs.toFixed(1).padStart(6)} ms, building no array`);
console.log(`iterator helpers: ${helperMs.toFixed(1).padStart(6)} ms, building no array`); Output
orders: 1,000,000; paid and at least 1,000 rupees: 720,000 (all three agree: true) filter, map, filter: 106.2 ms, building arrays of 900,000, 900,000, 720,000 items one for...of loop: 16.2 ms, building no array iterator helpers: 113.0 ms, building no array
This output changes from run to run: the times depend on the computer and on what else it is running
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node chain-cost.mjs
The chain built three arrays, 2,520,000 slots in all, about 20 MB at 8 bytes a slot, only to count the items of the
last one. The single for…of loop did the same work with no array at all and was several times faster in the run
above. When a chain runs on a large array in a hot path, measure it, and if the cost shows up, rewrite it as one loop
or as one reduce(), the subject of the next lesson.
Iterator helpers, added in ECMAScript 2025, are a third way. orders.values() gives an iterator over the array,
and iterators now have their own map(), filter(), take() and more, which work lazily: each item goes through
the whole pipeline before the next one is read, and nothing is collected until you ask for it. In the run above they
built no arrays but were still several times slower than the plain loop, about as slow as the chain: in Node.js 24
they save memory, not time. Laziness pays off most when you need only the first few results:
// Array methods finish each step for every item before the next step starts. Iterator helpers (ECMAScript 2025)
// pass each item through the whole pipeline, and take(2) stops it as soon as there are two results.
let calls = 0;
const square = (n) => {
calls++;
return n * n;
};
const eager = [1, 2, 3, 4, 5, 6].map(square).filter((n) => n > 4).slice(0, 2);
console.log('array methods: ', eager, 'after', calls, 'calls of square');
calls = 0;
const lazy = [1, 2, 3, 4, 5, 6].values().map(square).filter((n) => n > 4).take(2).toArray();
console.log('iterator helpers:', lazy, 'after', calls, 'calls of square'); Output
array methods: [ 9, 16 ] after 6 calls of square iterator helpers: [ 9, 16 ] after 4 calls of square
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node lazy-pipeline.mjs
The array methods squared all six numbers before slice() kept two of them; the iterator helpers squared four and
stopped, because take(2) closes the pipeline as soon as it has its two results. The iterators module covers them in
depth.
Version note
Iterator helpers need Node.js 22 or later, Chrome 122, Firefox 131 or Safari 18.4. The Run button’s engine, QuickJS
0.32.0, stops with a TypeError on the pipeline of lazy-pipeline.mjs, so that example runs in Node.js only.
Exercises
Exercise · Easy · JavaScript
List the students who passed, with their names in title case
Write passedStudents(results, passMark) in report.mjs. results is an array of objects such as { name: 'asha verma', marks: 72 }, typed in by hand, so the names come in any mix of capitals and small letters.
- Return the names of the students whose
marksare at leastpassMark, in the order ofresults, each name in title case:'asha verma'and'ASHA VERMA'both become'Asha Verma'. Names are words separated by single spaces. - Leave out a result whose
nameis missing or empty: there is nobody to list. - Marks may have decimals: 39.5 is below a pass mark of 40.
- Do not change
results. When nobody passed, return an empty array.
The starter drops students who scored exactly the pass mark and returns the names as they were typed. Run the sample tests when you are done.
Starter code · report.mjs
// The report of lesson javascript/map-filter-foreach.
export function passedStudents(results, passMark) {
// Drops the students exactly at the pass mark, and returns the names as they were typed.
return results.filter((r) => r.marks > passMark).map((r) => r.name);
} The sample tests · report.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { passedStudents } from './report.mjs';
const results = [
{ name: 'asha verma', marks: 72 },
{ name: 'ravi kumar', marks: 35 },
{ name: 'meena iyer', marks: 91 },
];
test('names of the students who passed, in title case and in order', () => {
assert.deepEqual(passedStudents(results, 40), ['Asha Verma', 'Meena Iyer']);
});
test('a mark equal to the pass mark passes', () => {
assert.deepEqual(passedStudents([{ name: 'kiran rao', marks: 40 }], 40), ['Kiran Rao']);
});
test('capitals in the input become title case', () => {
assert.deepEqual(passedStudents([{ name: 'ARJUN DAS', marks: 60 }, { name: 'divya NAIR', marks: 88 }], 50), ['Arjun Das', 'Divya Nair']);
});
test('decimal marks count as they are', () => {
assert.deepEqual(passedStudents([{ name: 'farah khan', marks: 39.5 }, { name: 'john mathew', marks: 40.25 }], 40), ['John Mathew']);
});
test('results without a name are left out', () => {
assert.deepEqual(passedStudents([{ marks: 95 }, { name: '', marks: 80 }, { name: 'lata pillai', marks: 70 }], 40), ['Lata Pillai']);
});
test('nobody passed, and results is unchanged', () => {
const copy = results.map((r) => ({ ...r }));
assert.deepEqual(passedStudents(results, 95), []);
assert.deepEqual(results, copy);
}); A hint
Do it in two steps: filter() the results you want to keep (a name that is a non-empty string and enough marks), then map() each one to its name in title case. For one name, split it into words with split(' '), change each word with word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(), and join(' ') the words again.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
7 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
Fresher · What is the difference between map() and forEach() in JavaScript?
Both call a function once for each item of an array, in index order, and neither changes the array by itself. map()
collects what the function returns into a new array of the same length and returns it, so it is for transforming
data: prices.map((p) => p * 2). forEach() ignores what the function returns and always returns undefined, so it
is only for side effects such as printing or updating a total, and it cannot be chained. Neither can stop early: a
return inside the function only ends that one call. Use map() to build a new array, filter() to keep some items,
some(), every() or find() to answer a question, and a for…of loop when you need break, continue or await.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
function allValid(rows) { rows.forEach((r) => { if (!r.email) return false; }); return true; } should reject an upload with any row that has no email, but it accepts every file. Explain why, and give two correct versions, saying which one you would ship.
-
Mid to senior
A nightly report runs orders.filter(isPaid).map(toRupees).filter(isLarge).length on 5 million orders, and the Node.js process now runs close to its memory limit. Explain where the memory goes, with rough numbers, and rewrite the code, saying what each rewrite trades.
Key takeaways
map(fn)returns a new array of the same length, one result per item.filter(fn)returns a new array of the items for whichfnreturned a truthy value. Neither changes the original.forEach(fn)always returnsundefinedand cannot stop early: areturninsidefnends one call only. Use it for side effects, a search method to answer a question, andfor…ofto stop part-way.- The function receives the item, its index and the array. Passing
parseIntby name gives it the index as a radix:['10', '10', '10'].map(parseInt)is[10, NaN, 2]. - Each step of a chain builds a new array. On large arrays in hot paths, measure and consider one loop.
- Iterator helpers (
array.values().map(…).filter(…)) build no arrays in between and stop early withtake(); in Node.js 24 they save memory rather than time.
References
- ECMAScript 2026 Language Specification: Array.prototype.map (Ecma International)
- ECMAScript 2026 Language Specification: Array.prototype.filter (Ecma International)
- ECMAScript 2026 Language Specification: Array.prototype.forEach (Ecma International)
- ECMAScript 2026 Language Specification: Iterator.prototype.map (Ecma International)
- ECMAScript 2026 Language Specification: parseInt (Ecma International)
- Array.prototype.map() (MDN Web Docs)
- Array.prototype.forEach() (MDN Web Docs)
- Iterator (MDN Web Docs)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress