Skip to the chapter
←JavaScriptGGroundworkJSJavaScript
JavaScript/Intermediate/I1
18 / 41 written chapters
/

The cover
  1. B1Setup & mental model
  2. B2Execution context
  3. B3var, let & const
  4. B4Scope
  5. B5Single thread
  6. B6In the browser
  7. B7Types & values
  8. B8Strings & Math
  9. B9Operators & flow
  10. B10Functions
  11. B11Objects & arrays
  12. B12this
  13. B13Closures
  14. B14DOM & events
  15. B15Callbacks & promises
  16. B16Errors & tools
  17. B17Guided project
  18. ★Cheat page
  1. I1Scope & functions, properly
  2. I2Objects deeply
  3. I3Prototypes & OOP
  4. I4Async, properly
  5. I5Modules & tooling
  6. I6Regex & dates
  7. I7Error handling
  8. I8Real-time connections
  9. I9Offline & storage
  10. I10Browser APIs
  11. I11Modern JS
  12. ★Cheat page
  1. A1Engine & memory
  2. A2Advanced async
  3. A3Metaprogramming
  4. A4Types & data
  5. A5Patterns & architecture
  6. A6Performance
  7. A7Security
  8. A8Ecosystem
  9. A9Testing
  10. A10Observability
  11. A11Interview bank
  12. A12Build it yourself
  13. ★Cheat page

JSI1 · Part 2 · Intermediate

Scope & functions, properly

The corners of how a scope is built, new versus bind, and what you build once functions are values: currying, composition, recursion.

9 min read8 sections2 exercises
18 of 41

In this chapter

  • Where this picks up
  • Where the creation phase gets strange
  • Closures, and what this chapter adds to them
  • this, past the four rules
  • Two properties every function carries

and 3 more

about 9 min
08On this page
  1. Where this picks up
  2. Where the creation phase gets strange
  3. Closures, and what this chapter adds to them
  4. this, past the four rules
  5. Two properties every function carries
  6. Higher-order functions: currying, partial application, composition
  7. Callbacks, as higher-order functions
  8. Recursion

Where this picks up

Scope covered the lookup itself — every kind of scope, the chain walked outward, shadowing, and writes that find nothing. This chapter starts where those rules stop being enough on their own: the corners of how a scope gets built, then what you build once functions are ordinary values.

Where the creation phase gets strange

Execution context gave the normal rules: what each declaration holds before its line runs. These are the cases those rules do not settle on their own — the ones that separate "I know hoisting" from "I know how a scope is built".

A function declaration inside a block

let in a block is block-scoped. A function declaration in a block is, too — but only in strict mode. In old-style sloppy scripts, browsers kept a legacy behaviour the spec now writes down as a special case: the name is also declared on the enclosing function like a var, holding undefined until the block runs.

// sloppy script
f();                            // TypeError: f is not a function — f exists, holds undefined
{ function f() { return "ok"; } }
f();                            // "ok" — running the block copied it out

// strict mode, or any ES module
{ function g() { return "ok"; } }
g();                            // ReferenceError — g never left its block

Same source, two behaviours, depending on a mode flag you may not have set yourself (modules and class bodies are always strict). The practical rule: if a function has to be visible outside a block, declare it outside the block — or assign a function expression to a let that is.

Default parameters get their own scope

Functions showed the first rule: parameters are set up left to right, each in its TDZ until its turn, so function f(a = b, b = 1) throws a ReferenceError while b = a + 1 works.

Less well known: once any parameter has a default, the parameter list becomes a scope of its own, sitting between the outer scope and the body. A default cannot see vars declared in the body, even ones with the same name.

var y = "outer";
function g(read = () => y) {
  var y = "inner";
  return read();
}
console.log(g());   // what happens?

"outer". The arrow was created in the parameter scope, so its chain goes parameters → outer, skipping the body entirely. The body's var y is a different binding that the arrow can never reach.

A named function expression's name is private

const factorial = function fact(n) {
  return n <= 1 ? 1 : n * fact(n - 1);   // fact is visible in here
};
factorial(5);    // 120
typeof fact;     // "undefined" — and nowhere out here

The engine puts fact in a tiny scope wrapped around the function alone. That makes it the safe way to recurse — it still works if someone reassigns factorial — and the name shows up in stack traces. The binding is read-only: fact = 5 inside the function is silently ignored in sloppy mode and a TypeError in strict mode.

What the spec actually calls these boxes

The beginner chapter drew one "variable environment" per context. The spec splits it in two, and the split explains everything above:

  • VariableEnvironment — where vars and (sloppy) function declarations go. Set once when the function starts, never changes. That is why var ignores blocks.
  • LexicalEnvironment — where let, const and class go. Every time execution enters a block, a new environment is created and becomes the current LexicalEnvironment, with the old one as its outer reference; leaving the block restores it. That is block scope, and it is also how each loop iteration gets its own let i.

Each environment is an environment record plus that outer reference. The global one is a two-part record: an object part that is window (for var and function declarations) and a plain part beside it (for let and const). That one detail is why top-level let is not on window, yet two classic scripts declaring the same let still collide.

Closures, and what this chapter adds to them

The scope chain is the whole mechanism behind closures: a function keeps its outer reference, and if the function outlives the call, the environment on the other end of that reference cannot be collected. The closures chapter covers that properly — the live link rather than a snapshot, one closure per call, the five jobs they do, and the memory they hold on to.

What is worth adding here: a closure is not a different kind of scope. It is the same lookup you just read about, still working after the function that created it has returned. Shadowing behaves identically inside one, and a closure over a name that gets shadowed later still sees the binding that was in scope where it was written.

this, past the four rules

The beginner chapter on this has the four binding rules and the ways a method loses its object. What belongs here is the part that is really about scope: an arrow has no this of its own, so it resolves the name through the scope chain like any other variable — which is why an arrow written inside a method keeps that method's this, and an arrow written at the top level never can.

And one experiment worth running, because it settles the ranking for good — new against a this that was already welded on by bind:

const F = function () { return this; };
const bound = F.bind({ tag: "bound" });
const created = new bound();          // new vs bind — who wins?
console.log(created instanceof bound, created.tag);   // what happens?

true undefined — even a this locked in by bind gets overridden the moment the bound function is called with new. It still constructs a real, correctly-typed instance; the bound object is just discarded in favour of the newly created one. new beats explicit beats everything else, confirmed by experiment rather than by table.

Two properties every function carries

Functions are objects, and two of their properties matter below: length is what curry reads to decide whether it has enough arguments yet.

function labeled(a, b = 1, ...rest) {}
labeled.length;   // 1 — counts params up to the FIRST one with a default or rest
labeled.name;     // "labeled"

const anon = () => {};
anon.name;        // "anon" — inferred from the variable it's assigned to

Higher-order functions: currying, partial application, composition

A higher-order function just means: takes a function as an argument, returns one, or both. map/filter/reduce from earlier already qualify — this section is what you build with that idea once you're the one writing the higher-order function.

// Currying — one arg at a time, until there are enough
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) return fn.apply(this, args);
    return (...more) => curried.apply(this, args.concat(more));
  };
}
function volume(l, w, h) { return l * w * h; }
const curried = curry(volume);
console.log(curried(2)(3)(4));      // what happens?
console.log(curried(2, 3)(4));      // what happens?
console.log(curried(2, 3, 4));      // what happens?

All three print 24 — currying doesn't change what gets computed, only how many calls it takes to supply the arguments. Each call checks whether it has enough arguments yet (fn.length, from just above); if not, it returns another function waiting for the rest.

Dry run: curried(2)(3)(4), call by call

Callargs accumulated so farargs.length vs fn.length (3)What happens
curried(2)[2]1 < 3 — not enoughreturns a new function closing over [2]
(3)[2, 3]2 < 3 — not enoughreturns another function closing over [2, 3]
(4)[2, 3, 4]3 >= 3 — enoughfn.apply(this, [2, 3, 4]) — volume(2, 3, 4) runs, returns 24

Each intermediate call's args is a closure the next call reads and extends with .concat(more) — this is the exact same "environment survives the call that made it" mechanism from the closures chapter, just used to accumulate arguments instead of a counter.

// Partial application — curry's simpler cousin: some args now, the rest later, ONE split
function partial(fn, ...preset) {
  return (...rest) => fn(...preset, ...rest);
}
function greet(greeting, name) { return greeting + ", " + name + "!"; }
const hiTo = partial(greet, "Hi");
hiTo("Ana");   // "Hi, Ana!"
// Composition — chain small functions into one, right to left
function compose(...fns) {
  return (x) => fns.reduceRight((acc, fn) => fn(acc), x);
}
const double = (x) => x * 2;
const inc = (x) => x + 1;
const incThenDouble = compose(double, inc);   // double(inc(x))
incThenDouble(5);   // (5 + 1) * 2 = 12

compose reads right to left because that's the order a nested call double(inc(x)) actually runs in — the rightmost function touches x first. Some libraries offer a pipe instead, which is the identical idea left to right — purely a readability choice, same reduce/reduceRight underneath.

Callbacks, as higher-order functions

A callback is the same idea as everything above it in this chapter — a function treated as a value and handed to other code. The only difference is who calls it and when. arr.map(fn) calls yours immediately; setTimeout(fn) parks it.

The historical shape — Node's error-first convention, the nested staircase it produced, and why try/catch could not reach inside it — is covered where it belongs, next to the promises that replaced it, in Callbacks, then promises. Worth knowing here: a promise does not remove the callback. It just gives you somewhere to hand it that can be chained and can fail in one place.

Recursion

A function that calls itself, always working toward a base case — the condition that stops it. Skip the base case, or get the shrinking step wrong, and it never stops on its own.

function factorial(n) {
  if (n <= 1) return 1;         // base case — where it stops
  return n * factorial(n - 1);  // recursive step — smaller problem, same shape
}
factorial(5);   // 120
function countDown(n) {
  if (n <= 0) return "done";
  return countDown(n - 1);
}
try {
  console.log(countDown(100000));   // what happens?
} catch (e) {
  console.log("threw:", e.constructor.name, "-", e.message);
}

On most engines, a RangeError: Maximum call stack size exceeded — each pending call sits on the call stack waiting for the one below it to return, and the stack has a hard size limit. The spec technically allows tail-call optimization (reusing the current frame when the recursive call is the very last thing a function does), which would make this run in constant stack space — but outside Safari, no major engine actually implements it. In practice: deep, unbounded recursion is a real risk in JS, not just a theoretical one. A loop has no such ceiling.

Say it like this → "Recursion needs a base case that's actually reachable and a step that provably shrinks toward it. If I'm not sure the depth is bounded, I either convert it to a loop or add an explicit depth guard — I don't rely on TCO, because V8 doesn't have it."
Interview answer, one sentence

"Scope is fixed by where code is written, a closure keeps that scope alive, this is decided by how a function is called, and higher-order functions — currying, partial application, composition — build new functions out of old ones."

Before you move on
  • Explain why a function declared inside a block is callable outside it in sloppy mode but not in strict mode or a module.
  • Say why a default parameter can't see a var declared later in the function body, in terms of the parameter scope.
  • Trace curry(volume) through curried(2)(3)(4), writing down the accumulated args at each call.
  • Explain why new bound() overrides a this already welded on by bind, and what that says about the binding rule ranking.
  • Say why unbounded recursion is a real risk in JS specifically, given that most engines don't implement tail-call optimization.

02Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 8 tests3 3 3 → 0 1 2beginner
  • 8 testsA function that returns a functionintermediate
All 33 for this level →
I1

Finished reading?

0 of 41 chapters read in this series

→Next stop

  1. B17Previous · B17Guided project: build a to-do app
  2. I1you are here
  3. Next · I2Objects deeplyThe rest of the object/array toolbox — past what the beginner chapter covered.8 min read

01On this page

8 sections · 0%

  1. 01Where this picks up
  2. 02Where the creation phase gets strange
  3. 03Closures, and what this chapter adds to them
  4. 04this, past the four rules
  5. 05Two properties every function carries
  6. 06Higher-order functions: currying, partial application, composition
  7. 07Callbacks, as higher-order functions
  8. 08Recursion

[] chapters · t top