JavaScript Functions as Values

Context: FIT2102_MOC · the payoff of Programming Paradigms’ “avoid hand-coded loops” — how JS composes behaviour instead of writing indices · the language-neutral concept is Higher-Order Function; syntax basics in JavaScript Basics (Syntax, Types, Control Flow) Problem it solves: pass behaviour around as a value, so traversal is written once (by the language) and only the per-element work is yours.

Quick Revision

  • 🎯 Trigger: about to write for (let i = 0; …) over an array ➔ reach for forEach/map/filter/reduce with an arrow function instead.
  • ⚡ Key Constraint: a closure captures a reference to the enclosing variable, not a snapshot of its value — that capture is what makes add(9) a permanently-configured “add nine”.

🔧 Minimal Working Example

const tutors = ['tim', 'michael', 'yan', 'Yang', 'arthur', 'kelvin'];
 
tutors.forEach(person => console.log('hello ' + person));   // effect per element, returns undefined
tutors.map(person => 'hello ' + person);                    // ['hello tim', 'hello michael', ...]
tutors.filter(person => person[0] == 'y');                  // ['yan']  -- 'Yang' has a capital Y
tutors.map(p => p.length).reduce((t, s) => t + s, 0);       // 29

Expected output: six hello … lines logged; a 6-element mapped array; a 1-element filter result; 29 (). (The deck prints ['yan','yang'] for that filter while writing 'Yang' in the array — the slide is internally inconsistent. String comparison is case-sensitive, so as written the answer is ['yan'].)

  • Three ways to have a functionnamed function hi(person) { … } · anonymous assigned to a variable const hi = function(person) { … } · anonymous passed inline ['tim','sally'].forEach(hi).
  • Arrow syntaxfunction(x) { return <expr> } is (almost) equivalent to x => <expr>; multi-parameter function(a, b) { return <expr> } becomes (a, b) => <expr>. The return and braces vanish because the body is the expression.
  • Higher-order function ➔ one that takes a function as an argument and/or returns a function.
  • Closure ➔ a function plus the set of variables it accesses from its enclosing scope.
  • The four Array HOFsforEach (effect per element) · map (transform) · filter (select) · reduce (accumulate, with an initial value).

🧬 Evaluation Model

Type signatures make the difference between the four methods unambiguous, for arrays of :

MethodSignatureReturns
forEachnothing — it exists for the effect
mapa new array, same length
filtera new array, length original
reducea single value

Desugaring the arrow, and taking the function as a parameter:

function sumTo(n, f) { return n ? f(n) + sumTo(n - 1, f) : 0; }
 
sumTo(10, function square(x) { return x * x; });   // 385  -- named
sumTo(10, function (x) { return x * x; });         // 385  -- anonymous, verbose
sumTo(10, x => x * x);                             // 385  -- arrow: same value, same semantics
  • Reduction of the closure caseadd(9) returns the expression y => y + x with x captured:
function add(x) { return y => y + x; }   // add : number -> (number -> number)
const addNine = add(9);                  // the returned closure remembers x = 9
addNine(10);  // 19
addNine(1);   // 10   -- x is still 9; the capture outlived add()'s call

🔀 Variations

  • Effect vs valueforEach for side effects (logging), map when you want the results. Using map purely for its side effect builds and discards an array.
  • Chaining beats nestingtutors.map(p => p.length).reduce((t, s) => t + s, 0) reads left-to-right as transform then accumulate; the equivalent loop needs an index and an accumulator you must initialise correctly.
  • reduce subsumes the other twomap and filter are both expressible as a reduce that appends conditionally — useful to know, rarely worth writing.
  • Generating the source array ➔ the pipeline needs something to start from, and JS has no range. Two loop-free idioms, both producing :
const range = n => Array.from({ length: n }, (_, i) => i);   // preferred: builds AND fills
const range2 = n => [...Array(n).keys()];                    // spread the index iterator
range(5);    // [0, 1, 2, 3, 4]
range(0);    // []          -- the empty case falls out for free
  • The filter → reduce aggregation shapegenerate a domain, keep what qualifies, collapse to one number is the single most reusable pipeline in the unit: range(n).filter(pred).reduce((t, x) => t + x, 0). Naming each stage in an interview (“domain, predicate, fold”) is worth more than the answer.

⚖️ Core Decision Matrix

Read the loop body and ask what each line is doing; the answer names the method.

The loop body does…➔ becomesInitial value
out.push(f(a[i])).map(f)
if (p(a[i])) out.push(a[i]).filter(p)
acc = g(acc, a[i]).reduce(g, init)whatever acc was set to before the loop
count++ under a condition.filter(p).length or .reduce((c, x) => p(x) ? c + 1 : c, 0)0
track a running best.reduce((best, x) => x > best ? x : best, a[0])the first element, never 0
transform then select.map(f).filter(p) — order matters

When It Flips: a loop that both transforms and tests (tripled = a[i] * 3; if (tripled % 2)) tests the transformed value ➔ .map(x => x * 3).filter(x => x % 2 !== 0), in that order. Filter-then-map is cheaper (fewer elements transformed) and is the default — but only when the predicate reads the original value. Reading the loop backwards is how you tell.

✍️ Practice

⚠️ Common Mistakes

  • 💡 new Array(n).map(…) silently does nothing ➔ it creates a sparse array of holes, and map/filter/forEach skip holes, so you get [empty × n] back. Use Array.from({length: n}, (_, i) => i), which fills as it builds, or spread [...Array(n).keys()].
  • 💡 Seeding a max-reduce with 0reduce((m, x) => x > m ? x : m, 0) returns 0 for an all-negative array. Seed with array[0] — and accept that the empty array then has no answer, which is the honest result.
  • 💡 Arrow functions are only almost equivalent ➔ the slides say “(almost)” and do not explain why in Week 1. Do not claim exact equivalence in an interview; say the arrow form differs in how it binds context and that the unit covers it later.
  • 💡 A closure captures the variable, not a copy ➔ if the enclosing variable is a mutable let that changes later, the closure sees the new value. Capturing a const avoids the whole class of bug.
  • 💡 forEach returns undefined ➔ chaining off it (xs.forEach(…).map(…)) throws. Use map when you need the results.
  • 💡 reduce without an initial valuereduce((t, s) => t + s) on an empty array throws, and on a string array starts with a string accumulator. Always pass the initial 0.