JAVASCRIPT / SCOPE, CLOSURES, AND THIS
Closures and functions that remember variables
Build functions that keep reading variables from an outer call that already returned, and predict which of them share state and which get their own.
What you will learn
- Return an inner function to keep an outer call's variables alive after that call ends
- Read closed-over variables at call time, so later reassignments are visible
- Call a factory once per instance to get independent remembered state
- Create sibling functions in one call when they must share a single binding
Understanding Closures and functions that remember variables
When an ordinary call finishes, the storage for its parameters and local variables is no longer reachable, so the engine is free to discard it. That stops being true the moment the call creates a function that outlives it: the inner function holds a hidden reference to the environment it was defined in, and while that reference exists the variables in it must stay alive. A closure is exactly that pair, the inner function plus the environment of the call that produced it, which is why the `warn` function below can still read `tag` long after `makeTagger` returned.
The mental model worth carrying is that every call to the outer function allocates a fresh environment, and every function created during that call points at that one environment. Two calls to the same factory therefore hand back functions with identical code but separate variables, while two functions created inside a single call share the same variables and see each other's writes. How many times you call the factory is how you decide how much state is shared.
Closures capture variables, not the values those variables happened to hold at creation time. The inner function reads the binding when it runs, so if anything reassigns that binding in between, the function reports the new value; a `const` capture only looks like a snapshot because nothing is allowed to reassign it. This is also why configuration through the outer function's parameters works so well: each produced function permanently remembers the arguments of the call that made it.
Nothing outside the closure can reach those variables by name, which is a consequence of where they live rather than a privacy feature you switched on.
function makeTagger(tag) {
let count = 0;
return function (text) {
count += 1;
return `[${tag} #${count}] ${text}`;
};
}
const warn = makeTagger('WARN');
const info = makeTagger('INFO');
console.log(warn('disk almost full'));
console.log(warn('disk full'));
console.log(info('backup started'));
console.log(typeof tag);A returned function carries the live variables of the call that created it, so those variables outlive the call and are shared by every function created in that same call.
Worked examples
Two functions, one remembered variable
Functions created in the same call share a single binding, while a second call gets its own.
function makeSharedPair(start) {
let n = start;
const bump = () => { n += 1; };
const read = () => n;
return [bump, read];
}
const [bumpA, readA] = makeSharedPair(10);
const [bumpB, readB] = makeSharedPair(10);
bumpA();
bumpA();
bumpB();
console.log(readA());
console.log(readB());Example explained
Line 1`let n = start` runs once per call to makeSharedPair, so there are two independent `n` variables in total.
Line 2`bump` and `read` were created in the same call, so both reference that call's `n` rather than a copy of it.
Line 3`readA()` returns 12 because it looks the variable up when it runs, after bumpA already changed it twice.
Line 4`readB()` is unaffected by bumpA because the second call allocated separate storage.
Callbacks that outlive their call
Each scheduled callback still knows its own argument after the function that created it has returned.
function later(name) {
setTimeout(function () {
console.log('hello ' + name);
}, 0);
console.log('scheduled ' + name);
}
later('ada');
later('grace');
console.log('done');Example explained
Line 1Both `later` calls have already returned before either timer runs, yet `name` is still readable inside the callbacks.
Line 2The parameter cannot be living on the call stack, because that frame is gone; it lives in the environment the callback references.
Line 3Each call to `later` created a separate binding for `name`, so one callback prints ada and the other grace.
Line 4The two timers share the same 0 ms delay, so they fire in the order they were registered.
A wrapper that remembers it already ran
Remembered variables can hold a flag and a cached result, not just a counter.
function once(fn) {
let called = false;
let result;
return function (...args) {
if (!called) {
called = true;
result = fn(...args);
}
return result;
};
}
const init = once((label) => {
console.log('running setup for ' + label);
return label.toUpperCase();
});
console.log(init('db'));
console.log(init('cache'));Example explained
Line 1`called` and `result` belong to the single `once(fn)` call and are re-examined on every later invocation of the wrapper.
Line 2Because the flag survives, the second invocation skips `fn` entirely, so the setup message prints only once.
Line 3The argument 'cache' is ignored: the wrapper returns the remembered `result` instead of calling `fn` again.
Line 4`fn` is itself a captured parameter, so the wrapper needs no other reference to the function it wraps.
Important notes
Closed-over variables are not properties of the function object, so `warn.count` is `undefined`; only code written inside the closure can read them.
A closure keeps its whole environment reachable, so capturing a large array or a DOM node keeps that object in memory until the function itself is dropped.
Common mistakes
Writing `return inner()` instead of `return inner`: the body runs once during the factory call and the caller receives its return value, so the next attempt to call it fails with "is not a function".
Calling the factory inline at every use, as in `makeTagger('WARN')('a')` each time: every call builds a fresh environment, so the remembered count restarts at 1 and the state looks like it was lost.
Assuming the closure copied the value when it was created: reassigning the outer variable later changes what every function closed over it reports, which looks like a working function silently changing behaviour.
Try it yourself
Change, predict, then run
In a browser console write `makeStepper(step)` that returns a function adding `step` to a running total and returning the total. Create `makeStepper(2)` and `makeStepper(10)`, call the first twice and the second once in any order, and confirm they report 4 and 10 rather than a single combined 14.
Open the JavaScript workspaceCheck your understanding
A function `build()` declares `let n = 0` and returns `{ inc, get }`, both defined in that body. You call `build()` twice and keep both results. What is true about `n`?
- All four functions share one `n`, because `n` is written only once in the source.
- Each of the four functions gets its own copy of `n`, so `get` never sees what `inc` changed.
- `inc` and `get` from the same call share one `n`, and the second `build()` call creates a separate `n`.
- `n` is discarded when `build()` returns, so `get` reports `undefined`.
Show answer
Storage for `n` is created per invocation, not per function and not per source declaration, so functions born in the same call point at the same binding while a second call allocates a new one. Option 0 is tempting because the source contains a single `let n = 0`, but that line is an instruction executed on every call, and each execution makes fresh storage.