054: Array Methods II: Transform, Filter, and Find
Learning outcomes
By the end of this lesson, you can:
- choose
map(),filter(),find(), orfindIndex()from the result they need; - write callbacks using the element, index, and source-array parameters;
- chain methods in a readable data pipeline;
- explain which methods create arrays and which return one value;
- explain that copying array methods do not change the source array's slots.
Retrieval warm-up
Before coding, answer from memory:
- What is the first valid index of an array?
- Which 042 method returns a section without changing the original:
slice()orsplice()? - What must an arrow function with braces do to produce a value?
Checkpoint: expect 0, slice(), and an explicit return. Try to predict before running code.
Vocabulary
- Callback: Function passed to a method and called per element. — Source: MDN: Callback function
- Transform: map-style operation converting each element into a new output value. — Source: MDN: map
- Predicate: Callback returning truthy/falsy used to test each element. — Source: MDN: Array.prototype.filter()
- Selection: filter/find/findIndex choosing subset or first match. — Source: MDN: find
- Chain: Sequencing array methods where each returns a new array. — Source: MDN: map
- Source array: Original array consumed by a non-mutating pipeline step. — Source: MDN: map
- Dense array: Array holding a value at every index from 0 through length-1. — Source: MDN: Array
- Predicate (official): "A predicate is a function that returns true or false to test a condition." — Source: MDN: Array.prototype.filter()
- Data pipeline (official): "Chaining array methods (map, filter, find) to transform data step by step." — Source: MDN: Array methods — Iterative methods
Beginner mental model
Imagine values moving along a conveyor belt. The callback inspects one value at a time.
map()gives every visited value one output ticket. For a dense three-element input, it produces a result of length three. Use it to transform.filter()opens or closes a gate. Only values whose predicate is truthy enter the new array.find()stops at the first matching value and returns that element. It returnsundefinedwhen none matches.findIndex()stops at the first match but returns its position. It returns-1when none matches.
For the dense arrays used in this lesson, map() calls the callback once per element and preserves length. On a sparse array, it skips empty slots and leaves corresponding empty slots in its result, so "every element" does not mean "every numeric index." Prefer dense arrays while learning these methods.
These methods do not add, remove, or reorder slots in the source array. A callback can still cause unrelated side effects, so keep callbacks focused:
const prices = [20, 16, 45];
const salePrices = prices.map((price) => price - 2);
console.log(prices); // [20, 16, 45]
console.log(salePrices); // [18, 14, 43]
Object references and object copying are not required today. They are introduced in later lessons. Every callback can receive (element, index, array). Use only parameters that make the intent clearer. The third parameter is the array currently being processed, not the result being built.
Worked beginner example: build a product shelf
Represent today's shelf as product-name strings. The example needs display labels and two kinds of search.
const productNames = ["Notebook", "Desk Lamp", "Water Bottle", "Backpack"];
const affordableNames = ["Notebook", "Water Bottle"];
const shelfLabels = affordableNames.map(
(name, index) => `${index + 1}. ${name}`,
);
console.log(shelfLabels);
const requestedName = productNames.find((name) => name === "Water Bottle");
if (requestedName === undefined) {
console.log("Product not found");
} else {
console.log(requestedName);
}
const lampIndex = productNames.findIndex((name) => name === "Desk Lamp");
console.log(lampIndex);
const longNames = productNames.filter((name) => name.length > 8);
console.log(longNames);
Walk-through:
map()converts each of the two selected names to exactly one label. Itsindexstarts at zero, so the display number usesindex + 1.find()returns the actual first matching string, not an array.- The explicit
undefinedcheck handles failed search without syntax from later lessons. findIndex()returns1, the position of Desk Lamp. Do not test it withif (lampIndex): index0is falsy and-1is truthy. Compare with-1explicitly.filter()can return any number of matching values, including an empty array.
Output:
["1. Notebook", "2. Water Bottle"] Water Bottle 1 ["Water Bottle"]
Selection and transformation can become a chain:
const shortUppercaseNames = productNames
.filter((name) => name.length <= 8)
.map((name) => name.toUpperCase());
console.log(shortUppercaseNames); // ["NOTEBOOK", "BACKPACK"]
Read chains top to bottom: start with names, keep short ones, then transform each kept name. Store an intermediate value when a chain becomes difficult to explain.
Intermediate example: reusable catalog queries
Functions can package a primitive-array search without introducing records:
const normalize = (text) => text.trim().toLowerCase();
function searchNames(names, query, minimumLength = 0) {
const term = normalize(query);
return names
.filter((name) => name.length >= minimumLength)
.filter((name) => normalize(name).includes(term))
.map((name) => name.toUpperCase());
}
console.log(searchNames(productNames, " bottle ", 8));
// ["WATER BOTTLE"]
console.log(productNames);
// ["Notebook", "Desk Lamp", "Water Bottle", "Backpack"]
The function returns new presentation strings without changing the source array. Multiple filters are acceptable when they make rules easy to name and debug. One combined predicate may traverse fewer times, but clarity matters first for a small list.
Optional advanced extension
Use the callback's third argument to compare a number with its neighbor after filtering:
const prices = [4, 28, 16, 45];
const changes = prices
.filter((price) => price <= 30)
.map((price, index, affordable) =>
index === 0 ? 0 : price - affordable[index - 1]
);
console.log(changes); // [0, 24, -12]
Here affordable is the intermediate filtered array. It is not prices and not changes. This parameter is useful occasionally; capturing a clearly named intermediate array is often simpler.
Object mutation and spread are a preview only, not required today. After objects are taught, this pattern will matter:
const previewProducts = [{ name: "Mouse", price: 20 }];
const previewSale = previewProducts.map((product) => ({
...product,
price: 18,
}));
Changing product.price inside the callback would mutate a shared object. The spread copy avoids that, but 046 explains the syntax and shallow-copy rules. Do not assign this preview as a core exercise.
Common mistakes and debugging
- Missing
return:map(name => { name.toUpperCase() })producesundefinedentries. Addreturn, or remove braces. - Using
map()to filter: returning conditionally still creates one result slot per input. Usefilter(). - Expecting an array from
find(): it returns one element orundefined. Usefilter()for all matches. - Confusing
findIndex()with an ID: an index is an array position and may change when data changes. - Testing an index as a boolean: use
index !== -1orindex === -1. - Side effects in a callback: changing outside state makes a pipeline harder to reason about. Return the transformed value instead.
- Over-chaining: name intermediate results and log their shape after each stage.
- Passing a multi-parameter function blindly:
map(parseInt)also passes the index asparseInt's radix. Preferstrings.map(text => Number.parseInt(text, 10)).
Best practices
- Choose by result shape: transform with
map, select many withfilter, search one withfind, locate withfindIndex. - Name callback parameters after the domain (
nameorprice), not vague letters. - Keep predicates free of unrelated side effects.
- Remember that these methods preserve source slots but cannot prevent callback side effects.
- Break a chain when an intermediate name explains a business concept.
- Handle
find()failure deliberately before using the returned value.
Checkpoint
Use four result requests without naming methods: "one label per name," "all prices under $20," "the first name containing Bottle," and "the position of Desk Lamp." Identify the method, expected result type, and failure value before writing code. Then change the order and determine which answers remain stable. Finally, point at every callback return and describe what that return means to its particular method.
Exercises
Core
From productNames, create uppercaseNames. Then find "Backpack" and print the found string or "Missing" using an explicit undefined check.
const uppercaseNames = productNames.map((name) => name.toUpperCase());
const foundName = productNames.find((name) => name === "Backpack");
console.log(uppercaseNames);
if (foundName === undefined) {
console.log("Missing");
} else {
console.log(foundName);
}
Output:
["NOTEBOOK", "DESK LAMP", "WATER BOTTLE", "BACKPACK"] Backpack
Practice
Write getPassingLabels(scores, minimum) that keeps scores at least minimum, then returns labels like "Passing score: 82". Confirm the source is unchanged.
function getPassingLabels(scores, minimum) {
return scores
.filter((score) => score >= minimum)
.map((score) => `Passing score: ${score}`);
}
const scores = [45, 82, 67, 91];
console.log(getPassingLabels(scores, 70));
console.log(scores);
Output:
["Passing score: 82", "Passing score: 91"] [45, 82, 67, 91]
Professional Extension
Write replaceFirst(values, target, replacement). Return a new array in which only the first matching primitive value is replaced. If the target is missing, return an unchanged array copy.
function replaceFirst(values, target, replacement) {
const targetIndex = values.findIndex((value) => value === target);
return values.map((value, index) =>
index === targetIndex ? replacement : value
);
}
const names = ["plan", "code", "test", "code"];
const updated = replaceFirst(names, "code", "build");
console.log(updated);
console.log(names);
console.log(replaceFirst(names, "deploy", "ship") === names);
Output:
["plan", "build", "test", "code"] ["plan", "code", "test", "code"] false
Even when the target is absent, map() returns a different array reference. Its primitive values are safe to reuse directly.
Recap
Explain without notes: For a dense array, which method returns an array of the same length? What happens to sparse holes? Which method returns zero or more matches? What are the failure values of find() and findIndex()? Why should callbacks avoid unrelated side effects?
