Part 9 — Functions
Ways to define, Arrow vs regular — the differences that matter 🎯, Parameters: defaults, rest, destructuring, arguments, Higher-order functions, callbacks, pure/impure
Functions are first-class values: assign them, pass them, return them.
Ways to define#
function decl(a, b) { return a + b; } // declaration — hoisted
const expr = function (a, b) { return a + b; }; // expression — not hoisted
const arrow = (a, b) => a + b; // arrow — concise, lexical this
const named = function fac(n){ return n<2?1:n*fac(n-1); }; // named expr (self-ref)
(function(){ console.log("IIFE"); })(); // runs immediately
Arrow vs regular — the differences that matter 🎯#
| Regular function | Arrow function | |
|---|---|---|
this |
dynamic (depends how it's called) | lexical (inherits from enclosing scope) |
arguments object |
yes | no (use rest ...args) |
Usable as constructor (new) |
yes | no |
Has own prototype |
yes | no |
| Hoisted | if declaration | no |
| Can be generator | yes | no |
const obj = {
name: "X",
regular() { return this.name; }, // "X"
arrow: () => this.name, // undefined — this is the outer scope
};
🟢 Use arrows for callbacks and when you want to keep the surrounding this (e.g., inside a class method's setTimeout). Use regular functions for object methods and anything used with new.
Parameters: defaults, rest, destructuring#
function connect({ host = "localhost", port = 5432 } = {}) { /* ... */ }
connect(); // uses all defaults thanks to `= {}`
function log(level, ...msgs) { } // rest gathers remaining args
⚠️ Default params are evaluated at call time, left to right, and can reference earlier params: (a, b = a * 2) => {}.
arguments#
Legacy array-like (not a real array) available in non-arrow functions. 🟢 Prefer rest params ...args, which give a real array.
Higher-order functions, callbacks, pure/impure#
- Higher-order — takes and/or returns functions (
map,setTimeout,debounce). - Callback — a function passed to be called later.
- Pure — same input → same output, no side effects. Easy to test and cache.
- Impure — depends on or changes outside state (I/O,
Date.now(), mutation).
// pure
const double = x => x * 2;
// impure (reads external clock, logs)
const stampedLog = msg => console.log(Date.now(), msg);
Closures — the crown jewel 🎯#
ELI12. A closure is a function that remembers the variables from where it was born, even after that place has finished running. Like a backpack the function carries around with the outer variables inside.
function counter() {
let count = 0; // private state
return () => ++count; // inner fn closes over `count`
}
const next = counter();
next(); // 1
next(); // 2 — `count` survived because the returned fn still references it
Why it exists / when to use: data privacy (module pattern), function factories, memoization, event handlers with retained state, currying.
graph LR
subgraph "counter() scope (kept alive)"
C["count = 2"]
end
N["next (the returned function)"] -->|closes over| C
⚠️ Closure traps:
- The loop bug in Part 4 —
varshares one binding. - Closures keep their entire scope reachable → memory leaks if a long-lived closure holds a big object it no longer needs.
Lexical environment#
Each execution creates a lexical environment: a record of local variables + a reference to the parent environment. The chain of these is the scope chain; closures are simply functions holding onto their lexical environment.
this — the four rules 🎯#
this is decided by how a function is called, not where it's defined (except arrows). In order:
newbinding —new Foo()→thisis the new object.- Explicit binding —
f.call(obj),f.apply(obj),f.bind(obj)→thisisobj. - Implicit binding —
obj.f()→thisisobj. - Default — plain
f()→thisisundefined(strict) or global (sloppy). - Arrow — ignores all the above; uses the enclosing lexical
this.
function who() { return this?.name; }
const a = { name: "A", who };
a.who(); // "A" (implicit)
const loose = a.who;
loose(); // undefined (default) ⚠️ lost `this`
who.call({ name: "C" }); // "C" (explicit)
⚠️ Passing a method as a callback (setTimeout(a.who, 0)) loses this. Fix with .bind(a), an arrow wrapper, or a class field arrow.
call, apply, bind#
f.call(thisArg, a, b); // invoke now, args listed
f.apply(thisArg, [a, b]); // invoke now, args as array
const g = f.bind(thisArg, a); // returns a NEW function, partially applied
Currying, partial application, composition, memoization#
// currying: f(a)(b)(c)
const curry = f => a => b => c => f(a, b, c);
// partial application: fix some args now
const add = (a, b) => a + b;
const add10 = add.bind(null, 10); // add10(5) → 15
// composition: pipe data through functions
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const shout = pipe(s => s.trim(), s => s.toUpperCase(), s => s + "!");
shout(" hi "); // "HI!"
// memoization: cache by args
function memoize(fn) {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const val = fn(...args);
cache.set(key, val);
return val;
};
}
Generators & async functions (intro; more in Part 20/16)#
function* ids() { let i = 0; while (true) yield i++; }
const gen = ids();
gen.next().value; // 0
gen.next().value; // 1 — generators pause at yield and resume on next()
async function load() { const r = await fetch("/api"); return r.json(); }
Interview questions (Part 9):
- Define a closure and give a real use (privacy, factory, memoize).
- What are the four
thisbinding rules? How do arrows differ? - Difference between
call,apply,bind? - Predict output when a method is detached and called standalone.
- Implement
once(fn)sofnruns at most once (uses a closure).
Practice:
- Implement
curry,debounce,throttle, andmemoizefrom scratch. - Build a private counter module using closures with
increment,decrement,value.