Functions and this

Function forms, parameter environments, closures, call-site this binding, overloads, guards, and assertions.

High-level overview

JavaScript functions combine two independent mechanisms. A function can keep access to bindings from the scope where JavaScript created the function. A normal function also receives a this value from the expression that calls it.

The page changes function syntax, parameter lists, captured bindings, and call forms separately. Keeping those four parts separate makes most function code readable.

Things to focus on

// 02 — FUNCTIONS, PARAMETERS, CLOSURES, `this`
//
// A reading file. Read it straight down: the comment above each construct gives
// its NAME, and `// =>` gives what the expression on its left evaluates to, so
// nothing is left hanging. Each section uses only what the lines just above it
// introduced — you never have to hold a definition from three screens back.
//
// It still runs (`node 02-functions-and-this.ts`) and prints nothing; the `// =>`
// annotations are the output, already collected.
//
// Mark anything you can't explain out loud with  // ?

// ─── 1. THE FUNCTION FORMS ───────────────────────────────────────────────────
//
// These are not spellings of one thing. They differ in when the name exists,
// whether `new` works on them, and — the one you pay for later — where `this`
// comes from. Section 5 is where that last difference gets collected.

function declaration(a: number) { // FUNCTION DECLARATION — hoisted whole: the name
  return a; // and the finished function both exist before this line runs
}

const expression = function (a: number) { // FUNCTION EXPRESSION — anonymous, and the
  return a; // `const` rules apply, so the name is in the TDZ until this line
};

// NAMED FUNCTION EXPRESSION — `factorial` is a private binding visible only
// inside the body. It exists so a function can call itself without depending on
// the outer name, which anyone else could reassign out from under it.
const fact = function factorial(n: number): number {
  return n <= 1 ? 1 : n * factorial(n - 1); // resolves to the private name, not `fact`
};
const fourFactorial = fact(4); // => 24
// Naming `factorial` out here is a ReferenceError: nothing declared it in this scope.

const concise = (a: number) => a; // ARROW, CONCISE BODY — the body IS the returned value
const explicitReturn = (a: number) => { return a; }; // ARROW, BLOCK BODY — needs `return`

// `{` after `=>` opens a body, never an object, so a concise object literal takes
// parentheses. Without them you get a block containing a LABEL — `wrapped:` — on
// the expression statement `true`, and a function that returns nothing at all.
const parenthesised = () => ({ wrapped: true });
const parensResult = parenthesised(); // => { wrapped: true }
const braced = () => { wrapped: true };
const bracedResult = braced(); // => undefined

const literalWithMethods = {
  method() { // METHOD SHORTHAND — gets a `this`, and cannot be `new`ed
    return 1;
  },
  arrow: () => 1, // an arrow in a property is not a method: no `this` of its own
};

// The two modifiers, and the gap in the grid. `async` combines with every form
// above; `*` combines with all of them except the arrow. There is no generator
// arrow, because an arrow borrows its context and a generator needs a context of
// its own to suspend inside.
async function asyncDeclaration() { return 1; }
function* generatorDeclaration() { yield 1; }
async function* asyncGeneratorDeclaration() { yield 1; }
const asyncArrow = async (n: number) => n * 2;
const modifiedMethods = {
  async asyncMethod() { return 1; },
  *generatorMethod() { yield 1; },
  async *asyncGeneratorMethod() { yield 1; },
};

// Both modifiers change what the CALL gives back. Neither hands you the body's
// value: `async` wraps it, and `*` doesn't run the body at all until you ask.
const pending = asyncDeclaration(); // => Promise { 1 }
const suspended = generatorDeclaration(); // => Object [Generator] {} — body not yet run
const firstStep = suspended.next(); // => { value: 1, done: false }
const secondStep = suspended.next(); // => { value: undefined, done: true }

// FUNCTION CONSTRUCTOR — builds a function from a STRING at run time. It is the
// one form that closes over nothing but global scope, which makes it the
// exception to everything in section 4. You will almost never want it.
const fromSource = new Function("a", "return a");
const echoed = fromSource(1); // => 1
const blind = new Function("return typeof fromSource")(); // => "undefined"
// The string body is compiled in global scope, and a module's bindings are not
// global, so it cannot see even the line directly above it.

// ─── 2. PARAMETERS ───────────────────────────────────────────────────────────
//
// The parameter list is a small scope of its own, evaluated left to right on
// every call. Nearly everything in this section falls out of those two facts.

// DEFAULT PARAMETER — a default may read any EARLIER parameter, because that
// binding is already initialized by the time this one is evaluated.
function withDefaults(a: number, b = a * 2, c = b + 1) {
  return [a, b, c];
}
const allDefaulted = withDefaults(1); // => [1, 2, 3]
const oneSupplied = withDefaults(1, 5); // => [1, 5, 6]

// The trigger is exactly one value: `undefined`. Not falsy, not nullish. `null`
// is a real value that someone chose, so it skips the default, lands in `b`, and
// then coerces to 0 in the arithmetic that builds `c`.
const undefinedTriggersIt = withDefaults(1, undefined); // => [1, 2, 3]
// @ts-expect-error TS2345: null is not a number. JS accepts it and skips the default.
const nullDoesNot = withDefaults(1, null); // => [1, null, 1]

// Defaults are evaluated PER CALL, not once at definition, so a default of `[]`
// hands out a fresh array every time. The mutable-default bug other languages
// have cannot happen here.
function freshDefault(items: number[] = []) {
  items.push(1);
  return items.length;
}
const firstCall = freshDefault(); // => 1
const secondCall = freshDefault(); // => 1 — a second array, not the first one grown

// The list being its own scope is why a later default can read an earlier name.
const moduleWide = "module scope";
function paramScope(a = moduleWide, b = a) {
  return [a, b];
}
const bothFilled = paramScope(); // => ["module scope", "module scope"]

// Referring to a LATER parameter is file 01's temporal dead zone, one scope down.
// Defined and never called, because calling it throws
// `ReferenceError: Cannot access 'b' before initialization`.
// @ts-expect-error TS2373: parameter 'a' cannot reference identifier 'b' declared after it
function backwardsDefault(a = b, b = 1) { return [a, b]; }

// REST PARAMETER — gathers the remaining arguments into a REAL array; must be last.
function restParameter(first: number, ...others: number[]) {
  return { first, others };
}
const gathered = restParameter(1, 2, 3); // => { first: 1, others: [2, 3] }

// DESTRUCTURING in the list, which is how a parameter ends up with no name of
// its own: this function declares `x`, `y`, `head` and `third`, and nothing that
// names either argument as a whole.
function patterns(
  { x, y = 0, ...remaining }: { x: number; y?: number; [key: string]: unknown }, // OBJECT PATTERN
  [head, , third]: number[], // ARRAY PATTERN — the bare comma is an ELISION, skipping index 1
) {
  return { x, y, remaining, head, third };
}
const unpacked = patterns({ x: 1, z: 9 }, [1, 2, 3]);
// => { x: 1, y: 0, remaining: { z: 9 }, head: 1, third: 3 }

// RENAMING IN A PATTERN — read `a`, bind `stored`. It reads backwards until you
// read the colon as "source : target".
function renaming({ a: stored }: { a: number }) {
  return stored;
}
const renamedOut = renaming({ a: 1 }); // => 1

// ARGUMENTS OBJECT — array-LIKE: it has `length` and index properties, and none
// of the array methods. It reports the arguments actually passed, whatever the
// parameter list declared. Arrows have no `arguments` at all, which is one more
// reason rest parameters won.
function usesArguments(..._declared: number[]) {
  return { count: arguments.length, copied: Array.from(arguments) };
}
const seen = usesArguments(1, 2, 3); // => { count: 3, copied: [1, 2, 3] }

// ─── 3. FUNCTIONS ARE OBJECTS ────────────────────────────────────────────────
//
// Not a metaphor. They carry properties, you can assign to them, and `typeof`
// answers "function" only because the language special-cases the one kind of
// object you are allowed to call.

function arityDemo(a: number, b = 1, ...rest: number[]) { return [a, b, rest]; }
const arity = arityDemo.length; // => 1 — .length is the DECLARED parameter count, and
// counting stops at the first default or rest rather than at the end of the list
const inferredName = arityDemo.name; // => "arityDemo" — .name is inferred from the
// assignment when the function itself is anonymous
const ownSource = arityDemo.toString();
// => "function arityDemo(a        , b = 1, ...rest          ) { return [a, b, rest]; }"
// The literal source text back — which is how some libraries read parameter names
// out of a function, and why minifying one breaks them. Those gaps are where the
// annotations were: Node erases types by overwriting them with spaces so every
// remaining character keeps its position. Compiled with `tsc` instead, the same
// line comes back closed up, with no gap at all.

const withProperty = Object.assign(function tick() {}, { calls: 0 });
withProperty.calls += 1; // functions take properties like any other object
const callCount = withProperty.calls; // => 1
const tickName = withProperty.name; // => "tick"

// ─── 4. CLOSURES ─────────────────────────────────────────────────────────────
//
// A closure is a function together with the scopes it can still reach. It
// captures the BINDING, not a copy of the value in it. Everything interesting
// about closures follows from that one word.

function makeCounter() {
  let count = 0; // outlives the call, because the returned functions still refer to it
  return {
    increment: () => ++count,
    read: () => count, // both arrows share the SAME binding — that is the point
  };
}
const counter = makeCounter();
counter.increment();
counter.increment();
const counted = counter.read(); // => 2 — one binding, two writers, one answer
const independent = makeCounter().read(); // => 0 — a second call, a second binding

// IIFE — immediately invoked function expression: define and call in one
// expression. Before modules this was the only way to get a private scope, which
// is why old code is full of them.
const iife = (function () {
  return "runs immediately";
})(); // => "runs immediately"
const arrowIife = (() => "same shape")(); // => "same shape"

// CURRYING — one argument at a time, each call returning the next function. Each
// returned arrow closes over the argument above it.
const curried = (a: number) => (b: number) => (c: number) => a + b + c;
const summed = curried(1)(2)(3); // => 6 — three calls, because there are three functions
const partiallyApplied = curried(1)(2); // => a function, still waiting for `c`

// HIGHER-ORDER FUNCTION — takes a function, returns a function. It works only
// because the returned arrow remembers `f` from where it was created.
const twice = <T>(f: (x: T) => T) => (x: T) => f(f(x));
const incrementTwice = twice((n: number) => n + 1);
const applied = incrementTwice(0); // => 2

// ─── 5. `this` — THE FIVE BINDING RULES ──────────────────────────────────────
//
// The model to carry out of this file: `this` is not part of the function. It is
// decided by the CALL, and it is whatever was to the left of the dot. One body,
// five call shapes, five answers — so read the call, never the definition.

const host = {
  label: "host",
  whoAmI() {
    return this?.label; // this body does not change anywhere below. Only the calls do.
  },
};

// 1. METHOD CALL — `this` is whatever is to the left of the dot. That is the
// whole rule; the rest of this section is what happens when there is no dot.
const viaMethod = host.whoAmI(); // => "host"

// 2. PLAIN CALL — storing the method DROPS the receiver, because the receiver was
// never part of the function. Strict code (and every module is strict) gives
// `undefined` here rather than the global object, so the `?.` in the body is all
// that stands between this line and a TypeError.
const detached = host.whoAmI;
const viaPlainCall = detached(); // => undefined

// 3. EXPLICIT BINDING — name the receiver yourself.
const viaCall = host.whoAmI.call({ label: "call" }); // => "call" — arguments listed
const viaApply = host.whoAmI.apply({ label: "apply" }, []); // => "apply" — arguments in an array
const bound = host.whoAmI.bind({ label: "bind" }); // `bind` returns a NEW function
const viaBound = bound(); // => "bind"
const boundThenRebound = bound.call({ label: "ignored" }); // => "bind" — a later `call`
// cannot override what `bind` fixed. This is why `bind` is the one that sticks.

// PARTIAL APPLICATION — `bind` also presets leading arguments, for free.
function volume(length: number, width: number, height: number) {
  return length * width * height;
}
const fixedLength = volume.bind(null, 2);
const fixedLengthAndWidth = volume.bind(null, 2, 3);
const boxA = fixedLength(3, 4); // => 24
const boxB = fixedLengthAndWidth(4); // => 24

// 4. ARROW — no `this` of its own, so the name resolves outward like any other.
const arrowHost = {
  label: "arrowHost",
  arrow: () => typeof this, // reaches MODULE scope, not `arrowHost`, and module-level
  // `this` in an ES module is `undefined`. An arrow can never be a method.
  method() {
    const inner = () => this?.label; // inside a real method, though, the arrow captures
    return inner(); // that method's receiver — and this is the useful direction
  },
};
const fromArrowProperty = arrowHost.arrow(); // => "undefined"
const fromArrowInsideMethod = arrowHost.method(); // => "arrowHost"

// 5. `new` — builds a fresh object and makes it the receiver, ignoring everything
// the other four rules would have said.
function Tagged(this: { tag: string }, tag: string) {
  this.tag = tag;
}
// The cast is needed because a plain function typed with a `this` parameter has
// no construct signature. `new` also beats `bind`; page 24 has the combination.
const constructed = new (Tagged as any)("built"); // => Tagged { tag: "built" }

// new.target — how a function tells which way it was called, and the basis of
// every "you forgot the new keyword" guard.
function detectsNew() {
  return new.target === undefined ? "plain call" : "new call";
}
const calledPlainly = detectsNew(); // => "plain call"
const calledWithNew = new (detectsNew as any)(); // => detectsNew {} — `new` returns the
// object it built, not the string the body returned. Returning a primitive under
// `new` discards it; only returning an object would replace the receiver.

// ─── 6. RECURSION AND MUTUAL RECURSION ───────────────────────────────────────

// MUTUAL RECURSION. `isEven` names `isOdd` a line before `isOdd` exists, which is
// legal because the name is not resolved until the function RUNS — by then both
// consts are initialized. Definition order and execution order are different
// questions. The return annotations are required: without them the two would try
// to infer their types from each other forever.
const isEven = (n: number): boolean => (n === 0 ? true : isOdd(n - 1));
const isOdd = (n: number): boolean => (n === 0 ? false : isEven(n - 1));
const evenCheck = isEven(10); // => true
const oddCheck = isOdd(10); // => false

// ─── 7. GETTERS, SETTERS, COMPUTED NAMES ─────────────────────────────────────

const computedKey = "describe";

const temperature = {
  celsius: 0,
  get fahrenheit() { // GETTER — reads like a field, runs like a function
    return this.celsius * 1.8 + 32;
  },
  set fahrenheit(value: number) { // SETTER — the pair makes one ACCESSOR PROPERTY, and the
    this.celsius = (value - 32) / 1.8; // setter may take a different type than the getter gives
  },
  [computedKey]() { // COMPUTED KEY — the property name is worked out from an expression
    return "named when the object was built";
  },
};
const freezing = temperature.fahrenheit; // => 32 — a call, not a field read
temperature.fahrenheit = 212; // looks like an assignment; runs the setter
const readBack = temperature.celsius; // => 100 — the setter wrote through to `celsius`
const viaComputedKey = temperature.describe(); // => "named when the object was built"

// ─── 8. THE TYPE LAYER OVER FUNCTIONS ────────────────────────────────────────
//
// Everything below is ERASED before the code runs. It changes what callers are
// allowed to write, and nothing else.

// OVERLOAD SET — several public signatures over one implementation. The
// implementation signature is deliberately invisible to callers, so you can
// accept a loose union inside while promising something precise outside.
function identity(x: string): string;
function identity(x: number): number;
function identity(x: string | number): string | number {
  return x;
}
const stringOut = identity("s"); // => "s", and typed `string` — not `string | number`
const numberOut = identity(1); // => 1, typed `number`
// @ts-expect-error TS2769: no public overload accepts a boolean, though the body would
const rejected = identity(true); // return it happily. The implementation signature is
// not one of the choices.

// TYPE PREDICATE — turns a boolean return into a fact the checker acts on. Nobody
// verifies that the body tests for what it claims; the checker takes your word
// (see 36.5).
function isString(value: unknown): value is string {
  return typeof value === "string";
}
const probablyText: unknown = "hello";
if (isString(probablyText)) {
  const shouted = probablyText.toUpperCase(); // => "HELLO" — narrowed to `string`, but
} // only inside this branch

// ASSERTION SIGNATURE — narrows everything AFTER the call, on the grounds that if
// it didn't throw, the claim holds.
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== "string") throw new TypeError("not a string");
}
const alsoText: unknown = "hello";
assertIsString(alsoText);
const measured = alsoText.length; // => 5 — narrowed from the call to the end of the scope

// `this` PARAMETER — not a parameter at all. It types the receiver, which already
// travels separately from the arguments, so it doesn't shift them.
function describe(this: { id: number }, suffix: string) {
  return this.id + suffix;
}
const described = describe.call({ id: 1 }, "!"); // => "1!" — still a one-argument call

// `never` RETURN TYPE — this function does not finish normally: it throws, or
// loops forever. The checker can then treat what follows a call to it as dead.
function fail(message: string): never {
  throw new Error(message);
}
const parsed = Number("1");
const checked = Number.isNaN(parsed) ? fail("not a number") : parsed; // => 1, typed
// `number` — a `never` branch drops out of the union instead of widening it.

// Two questions now answer most of what a function does, and they are
// independent: what did it close over, and how was it called. The first is fixed
// when the function is written; the second is decided fresh at every call site by
// whoever is holding it. File 03 replaces the plain property lookup that
// `host.whoAmI()` quietly assumed with a chain.