1. Functions as First-Class Values
Saying JavaScript treats functions as first-class values means exactly what it says: a function can be stored in a variable, passed as an argument, returned from another function, and put inside an array or object — anything you can do with a number or a string:
function double(n) { return n * 2; }
const stored = double; // stored in a variable
const list = [double, (n) => n + 1]; // stored inside an array
const obj = { transform: double }; // stored as an object property
console.log(stored(5)); // 10
console.log(list[0](5)); // 10
console.log(obj.transform(5)); // 10
This is precisely why numbers.map(double) from Week 5 works at all:
double isn't special syntax passed into map, it's a value —
the exact same function reference that stored holds above — being
handed to another function as an ordinary argument.
2. Higher-Order Functions
A higher-order function is any function that either takes another
function as an argument, returns a function, or both. You've already used several —
map, filter, reduce and
setTimeout are all higher-order functions built into JavaScript. Writing
your own is the same idea:
function repeat(n, action) {
const results = [];
for (let i = 0; i < n; i++) {
results.push(action(i)); // action is a function, passed in as an argument
}
return results;
}
const squares = repeat(5, (i) => i * i);
console.log(squares); // [0, 1, 4, 9, 16]
repeat doesn't know or care what action actually does — it
just calls whatever function it was handed, once per iteration. This separation is
the whole value of higher-order functions: repeat's looping logic stays
fixed and reusable, while the behavior on each iteration is supplied fresh by the
caller.
Week 3's createCounter() and makeMultiplier(factor) both qualify too — a function that returns another function is just as much a higher-order function as one that accepts one. Closures and higher-order functions overlap constantly in practice.
3. Immutability Patterns
Mutation means changing a value in place, rather than creating a new one — and it's a common source of subtle bugs, because other code that still holds a reference to the same array or object gets changed out from under it:
function addItem(cart, item) {
cart.push(item); // mutates the array the caller passed in!
return cart;
}
const original = ["pen", "notebook"];
const updated = addItem(original, "eraser");
console.log(original); // ["pen", "notebook", "eraser"] -- changed, even though we didn't ask for that
console.log(original === updated); // true -- same array, not a new one
Whoever still holds a reference to original now silently sees the extra
item too — that's rarely what's intended, and it's exactly the kind of bug that's
hard to trace back to its source. The fix is to build and return a
new array or object instead of changing the existing one, using
spread syntax:
function addItem(cart, item) {
return [...cart, item]; // a brand-new array: everything from cart, plus item
}
const original = ["pen", "notebook"];
const updated = addItem(original, "eraser");
console.log(original); // ["pen", "notebook"] -- untouched
console.log(updated); // ["pen", "notebook", "eraser"] -- a separate, new array
console.log(original === updated); // false
// The same pattern works on objects:
const user = { name: "Priya", age: 29 };
const olderUser = { ...user, age: 30 }; // copy every field, then override age
console.log(user.age, olderUser.age); // 29 30
[...cart, item] spreads every element of cart into a new
array literal, then adds item at the end — the original
cart is never touched. The object version works identically:
{'{'} ...user, age: 30 {'}'} copies every field from user,
then the explicit age: 30 after it overrides just that one field in the
new object.
push, pop, shift, unshift, splice and sort all mutate the original array. map, filter, slice, and spread syntax all return a new one. Reaching for the wrong half of that list inside a function that receives an array as a parameter is the single most common source of this bug.
4. Callback Patterns in Practice
A callback is simply a function passed into another function to be
called later — every callback you've written for map,
filter or setTimeout already qualifies. Two patterns worth
recognizing as your own code starts using more of them:
function isEven(n) {
return n % 2 === 0;
}
// Named: reads clearly, and isEven is reusable elsewhere
const evens1 = [1, 2, 3, 4].filter(isEven);
// Inline: fine for short, one-off logic that won't be reused
const evens2 = [1, 2, 3, 4].filter((n) => n % 2 === 0);
Both produce the same result. A named function is worth extracting once the logic is reused elsewhere, needs a descriptive name to stay readable, or is complex enough that inlining it would clutter the call site — an inline arrow function is perfectly fine for short, single-use logic.
5. A First Look at Currying & Composition
Two ideas worth recognizing at a glance now, even without going deep yet: currying turns a multi-argument function into a chain of single-argument functions, and composition chains small functions into a pipeline where each one's output feeds the next one's input:
function add(a) {
return function (b) {
return a + b; // b is captured together with a, via a closure
};
}
const addFive = add(5);
console.log(addFive(3)); // 8
console.log(add(10)(20)); // 30 -- called all at once, same underlying logic
const double = (n) => n * 2;
const addOne = (n) => n + 1;
const result = addOne(double(5)); // 5 -> double -> 10 -> addOne -> 11
console.log(result); // 11
Notice add is built on exactly the closure pattern from Week 3 — the
returned inner function captures a and adds b to it
whenever it's eventually called. Both currying and composition come up constantly in
functional-style JavaScript once functions are fully comfortable as values;
Coding Practice's Module 9 (in the site's separate practice problem
set) goes considerably deeper on both, including a reusable pipe() and
compose() helper, if you want more repetitions.
6. Hands-on Exercise
Build an immutable todo-list updater
Practice spread-based immutability and higher-order functions on one small, realistic dataset.
Requirements:
- Create an array of at least 5 todo objects, each with
id,textanddone(boolean). - Write a pure function
addTodo(todos, text)that returns a new array with a new todo appended, without mutating the original. - Write a pure function
toggleTodo(todos, id)that returns a new array where only the matching todo'sdoneflips, usingmapand object spread — every other todo object should be the exact same object reference, unchanged. - Write a higher-order function
countBy(todos, predicate)that takes a predicate function and returns how many todos satisfy it (built onfilter). - Confirm none of your functions ever mutate the original array by logging the original array's
donevalues before and after callingtoggleTodo.
For step 3, the shape is: todos.map(todo => todo.id === id ? {'{'} ...todo, done: !todo.done {'}'} : todo) — every non-matching item passes straight through unchanged, and only the matching one gets rebuilt as a new object.
7. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
What does it mean, concretely, to say "functions are first-class values" in JavaScript?
What does it mean, concretely, to say "functions are first-class values" in JavaScript?
A function can be stored in a variable, put inside an array or object, passed as an argument to another function, and returned from a function — the same set of things you can do with a number or string. There's nothing special or restricted about how functions can be used compared to any other kind of value.
Q2
Why is cart.push(item) risky inside a function that received cart as a parameter?
Why is cart.push(item) risky inside a function that received cart as a parameter?
push mutates the array in place rather than creating a new one — since arrays are passed by reference, whatever array the caller passed in gets changed too, even though nothing in the function's signature suggested that would happen. Any other code still holding a reference to that same array sees the change unexpectedly. Returning a new array with spread ([...cart, item]) avoids this entirely.
Q3
Is Array.prototype.map a higher-order function? Why?
Is Array.prototype.map a higher-order function? Why?
Yes — a higher-order function is any function that takes a function as an argument, returns one, or both. map takes a callback function as its argument, which is enough to qualify on its own.
Q4
Given {'{'} ...user, age: 30 {'}'}, what happens if age: 30 is written before the spread instead of after?
Given {'{'} ...user, age: 30 {'}'}, what happens if age: 30 is written before the spread instead of after?
The override would be lost. Object literals apply properties left to right, with later ones winning on a collision — so {'{'} age: 30, ...user {'}'} spreads user's own age back in after the explicit 30, overwriting it. The override property must come after the spread to actually take effect.