You reach for Array.prototype.filter every time you want a smaller array that contains only the elements matching some condition — the unread messages, the in-stock products, the users over 18. Reimplementing it forces you to think about exactly what filter does on each step and what it hands the callback.
Implement arrayFilter(arr, predicate) as a standalone function. Walk the array once. For every element, call predicate(element, index, arr) — exactly those three arguments, in that order — and keep the element only when the return value is truthy. Return a brand-new array; never mutate the input.
function arrayFilter(arr, predicate) {
// returns a new array containing every element of `arr`
// for which predicate(element, index, arr) is truthy.
}
arrayFilter([1, 2, 3, 4, 5], (n) => n % 2 === 0);
// [2, 4]
arrayFilter(['apple', 'fig', 'banana'], (s) => s.length > 3);
// ['apple', 'banana']
// The predicate receives (element, index, array).
arrayFilter(['a', 'b', 'c', 'd'], (_, i) => i % 2 === 0);
// ['a', 'c']
// Truthy / falsy, not strictly boolean.
arrayFilter([0, 1, '', 'hi', null, 7], (x) => x);
// [1, 'hi', 7]
(element, index, array). Skipping the second or third argument is a real-world bug; tests check it.predicate(...) is truthy. 0, '', null, undefined, NaN, and false are all dropped.arrayFilter(arr, () => true) should produce a new array equal in contents but !== to arr.[1, , 3]) — the real spec skips them. For this question you may either skip or visit holes; tests don't pin this down, but the solution explains the trade-off.You'll walk the array once, ask a yes/no question about each element, and collect the yeses into a brand-new array.
You have an array. You want a smaller array — just the elements that pass some test. That test is a function (the predicate) you receive as the second argument. For every element of the input, you call the predicate, and if it returns a truthy value you keep that element in the output. Array.prototype.filter is the built-in version; you're recreating it as a free function arrayFilter(arr, predicate).
Picture a row of items moving past a checkpoint. At each cell, you ask the predicate one question: should this stay? If the answer is truthy, the element drops into a fresh output array. If it's falsy, it's discarded. The input is never touched; the output starts empty and grows as elements pass.
A reasonable first try is to write the smallest loop that works:
function arrayFilterBroken(arr, predicate) {
const result = [];
for (const el of arr) {
if (predicate(el)) {
result.push(el);
}
}
return result;
}
This passes test 1 (keeps elements where the predicate returns true) — arrayFilterBroken([1, 2, 3], (n) => n > 1) returns [2, 3]. But it fails any test where the predicate reads its second or third argument — like test 6, which passes the index as the second argument. The spec for Array.prototype.filter says the predicate is called with three arguments: (element, index, array). Real-world callbacks rely on that — think of users.filter((u, i) => i < 10) to keep the first ten. Our naive version only passes the element, so any predicate that reads i or arr sees undefined and behaves wrong.
function arrayFilter(arr, predicate) {
// Start with a fresh array. Never push into `arr` or return it directly —
// filter is non-mutating, and the caller expects a new reference.
const result = [];
// Indexed for-loop so we can pass `i` to the predicate.
// for...of and forEach hide the index.
for (let i = 0; i < arr.length; i++) {
const element = arr[i];
// Spec: predicate receives (element, index, array). All three. In that order.
// Tests with predicates like (_, i) => i % 2 === 0 only pass when `i` is wired up.
if (predicate(element, i, arr)) {
result.push(element);
}
}
return result;
}
module.exports = { arrayFilter };
Two shifts from the naive version. First, the loop uses an index counter i instead of for...of, because we need to hand that i to the predicate. Second, the call site is predicate(element, i, arr) — three arguments, not one — so any predicate that reads the index or the array gets a real value.
A quick word on what this version does not do: the real Array.prototype.filter is a method on the prototype, callable as arr.filter(...). You could ship that variant by writing Array.prototype.myFilter = function(predicate) { ... } and using this instead of arr. The shape of the loop is identical; the only difference is where the array comes from. We pick the free-function form here so the test harness can require it like any other module.
Take arrayFilter([1, 2, 3, 4, 5], (n) => n % 2 === 0). The predicate keeps even numbers.
result = [], i = 0.i = 0: element = 1. predicate(1, 0, arr) → 1 % 2 === 0 is false. Skip. result = [].i = 1: element = 2. predicate(2, 1, arr) → 2 % 2 === 0 is true. Push. result = [2].i = 2: element = 3. predicate(3, 2, arr) → false. Skip. result = [2].i = 3: element = 4. predicate(4, 3, arr) → true. Push. result = [2, 4].i = 4: element = 5. predicate(5, 4, arr) → false. Skip. result = [2, 4].[2, 4].predicate(element), the test arrayFilter(['a','b','c','d'], (_, i) => i % 2 === 0) returns [] instead of ['a', 'c'], because i is undefined inside the predicate and undefined % 2 === 0 is false.arrayFilter([1,2,3], () => true) must return a new array, not the original. If you accidentally return arr itself, mutating the output later will mutate the input — and the test expect(result).not.toBe(input) will fail.if (predicate(...) === true) rejects 1, 'hi', and other truthy values. Use if (predicate(...)) so any truthy value keeps the element, matching how the real filter behaves with (x) => x on [0, 1, '', 'hi'].arr.push(...) or arr.splice(...) inside arrayFilter would corrupt the iteration. The function is a read; never write back into arr.(el, i, full) => full.indexOf(el) === i (a dedupe trick) silently break if you call predicate(element, i) without the array. Always pass all three.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
You reach for Array.prototype.filter every time you want a smaller array that contains only the elements matching some condition — the unread messages, the in-stock products, the users over 18. Reimplementing it forces you to think about exactly what filter does on each step and what it hands the callback.
Implement arrayFilter(arr, predicate) as a standalone function. Walk the array once. For every element, call predicate(element, index, arr) — exactly those three arguments, in that order — and keep the element only when the return value is truthy. Return a brand-new array; never mutate the input.
function arrayFilter(arr, predicate) {
// returns a new array containing every element of `arr`
// for which predicate(element, index, arr) is truthy.
}
arrayFilter([1, 2, 3, 4, 5], (n) => n % 2 === 0);
// [2, 4]
arrayFilter(['apple', 'fig', 'banana'], (s) => s.length > 3);
// ['apple', 'banana']
// The predicate receives (element, index, array).
arrayFilter(['a', 'b', 'c', 'd'], (_, i) => i % 2 === 0);
// ['a', 'c']
// Truthy / falsy, not strictly boolean.
arrayFilter([0, 1, '', 'hi', null, 7], (x) => x);
// [1, 'hi', 7]
(element, index, array). Skipping the second or third argument is a real-world bug; tests check it.predicate(...) is truthy. 0, '', null, undefined, NaN, and false are all dropped.arrayFilter(arr, () => true) should produce a new array equal in contents but !== to arr.[1, , 3]) — the real spec skips them. For this question you may either skip or visit holes; tests don't pin this down, but the solution explains the trade-off.You'll walk the array once, ask a yes/no question about each element, and collect the yeses into a brand-new array.
You have an array. You want a smaller array — just the elements that pass some test. That test is a function (the predicate) you receive as the second argument. For every element of the input, you call the predicate, and if it returns a truthy value you keep that element in the output. Array.prototype.filter is the built-in version; you're recreating it as a free function arrayFilter(arr, predicate).
Picture a row of items moving past a checkpoint. At each cell, you ask the predicate one question: should this stay? If the answer is truthy, the element drops into a fresh output array. If it's falsy, it's discarded. The input is never touched; the output starts empty and grows as elements pass.
A reasonable first try is to write the smallest loop that works:
function arrayFilterBroken(arr, predicate) {
const result = [];
for (const el of arr) {
if (predicate(el)) {
result.push(el);
}
}
return result;
}
This passes test 1 (keeps elements where the predicate returns true) — arrayFilterBroken([1, 2, 3], (n) => n > 1) returns [2, 3]. But it fails any test where the predicate reads its second or third argument — like test 6, which passes the index as the second argument. The spec for Array.prototype.filter says the predicate is called with three arguments: (element, index, array). Real-world callbacks rely on that — think of users.filter((u, i) => i < 10) to keep the first ten. Our naive version only passes the element, so any predicate that reads i or arr sees undefined and behaves wrong.
function arrayFilter(arr, predicate) {
// Start with a fresh array. Never push into `arr` or return it directly —
// filter is non-mutating, and the caller expects a new reference.
const result = [];
// Indexed for-loop so we can pass `i` to the predicate.
// for...of and forEach hide the index.
for (let i = 0; i < arr.length; i++) {
const element = arr[i];
// Spec: predicate receives (element, index, array). All three. In that order.
// Tests with predicates like (_, i) => i % 2 === 0 only pass when `i` is wired up.
if (predicate(element, i, arr)) {
result.push(element);
}
}
return result;
}
module.exports = { arrayFilter };
Two shifts from the naive version. First, the loop uses an index counter i instead of for...of, because we need to hand that i to the predicate. Second, the call site is predicate(element, i, arr) — three arguments, not one — so any predicate that reads the index or the array gets a real value.
A quick word on what this version does not do: the real Array.prototype.filter is a method on the prototype, callable as arr.filter(...). You could ship that variant by writing Array.prototype.myFilter = function(predicate) { ... } and using this instead of arr. The shape of the loop is identical; the only difference is where the array comes from. We pick the free-function form here so the test harness can require it like any other module.
Take arrayFilter([1, 2, 3, 4, 5], (n) => n % 2 === 0). The predicate keeps even numbers.
result = [], i = 0.i = 0: element = 1. predicate(1, 0, arr) → 1 % 2 === 0 is false. Skip. result = [].i = 1: element = 2. predicate(2, 1, arr) → 2 % 2 === 0 is true. Push. result = [2].i = 2: element = 3. predicate(3, 2, arr) → false. Skip. result = [2].i = 3: element = 4. predicate(4, 3, arr) → true. Push. result = [2, 4].i = 4: element = 5. predicate(5, 4, arr) → false. Skip. result = [2, 4].[2, 4].predicate(element), the test arrayFilter(['a','b','c','d'], (_, i) => i % 2 === 0) returns [] instead of ['a', 'c'], because i is undefined inside the predicate and undefined % 2 === 0 is false.arrayFilter([1,2,3], () => true) must return a new array, not the original. If you accidentally return arr itself, mutating the output later will mutate the input — and the test expect(result).not.toBe(input) will fail.if (predicate(...) === true) rejects 1, 'hi', and other truthy values. Use if (predicate(...)) so any truthy value keeps the element, matching how the real filter behaves with (x) => x on [0, 1, '', 'hi'].arr.push(...) or arr.splice(...) inside arrayFilter would corrupt the iteration. The function is a read; never write back into arr.(el, i, full) => full.indexOf(el) === i (a dedupe trick) silently break if you call predicate(element, i) without the array. Always pass all three.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.