Week 6: Functions as Values & Immutability

Week 5 used map/filter/reduce without dwelling on why passing a function into another function works at all. This week makes that explicit — functions in JavaScript are values, just like numbers or strings — and pairs it with the habit that makes functional-style code predictable: never changing data in place.

Phase 3 of 8 Week 6 of 14 ~3–4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Explain what "functions are first-class values" means, concretely
  • Use spread syntax to copy and update arrays/objects without mutating the original
  • Recognize a higher-order function, and write a simple one yourself

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:

functions, treated like any other value
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:

a higher-order function you write yourself
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.

A function returning a function is also higher-order

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:

the bug: accidental mutation
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:

the fix: spread into a new array/object
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.

Know which array methods mutate

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:

named callback vs. inline callback
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:

a curried function
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
simple composition
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

Hands-on

Build an immutable todo-list updater

Practice spread-based immutability and higher-order functions on one small, realistic dataset.

Requirements:

  1. Create an array of at least 5 todo objects, each with id, text and done (boolean).
  2. Write a pure function addTodo(todos, text) that returns a new array with a new todo appended, without mutating the original.
  3. Write a pure function toggleTodo(todos, id) that returns a new array where only the matching todo's done flips, using map and object spread — every other todo object should be the exact same object reference, unchanged.
  4. Write a higher-order function countBy(todos, predicate) that takes a predicate function and returns how many todos satisfy it (built on filter).
  5. Confirm none of your functions ever mutate the original array by logging the original array's done values before and after calling toggleTodo.
Hint

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?

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?

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?

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?

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.