JavaScript Scope and Closures: Lexical Scope Explained with Runnable Examples

Learn to trace JavaScript bindings through global, function and block scope. Build independent closures, repair the classic var loop bug and check your understanding with exact outputs.

KnowledgeGate Team

Exam prep & CS education

Updated 27 Oct 20265 min read

Nested functions resolve names from where they are defined, not from who calls them. Block boundaries can remove a binding from view, while a returned function can keep a defining environment reachable after the outer call finishes. Trace each environment and predict every output before running the code.

1. JavaScript scope decides where a binding can be read

Scope determines where a binding can be accessed. Lexical scope fixes lookup from the structure of the written code. JavaScript uses global, function and block environments.

Braces form blocks for let and const. A var belongs to the surrounding function, not an if or for block. Run this trace:

js
const topic = "global";

function showResult(score) {
  const topic = "scope";

  if (score >= 60) {
    let result = "pass";
    var attempts = 2;
    console.log(topic, result, attempts);
  }

  console.log(topic, attempts);
  // console.log(result); // ReferenceError if uncommented
}

showResult(72);
console.log(topic);

The global environment has topic: "global". The call creates score: 72, shadowing topic: "scope" and function-scoped attempts: 2; the block creates result: "pass". It prints scope pass 2, scope 2, then global. After the block, attempts remains available but result does not.

Declaration choice, temporal-dead-zone behaviour, const-object mutation and loop-binding rules are worked through in JavaScript Variables: let, const and var Scope with Runnable Examples. Closure tracing asks a different question: which environment owns a name, and which bindings remain reachable?

Nested Global, Function and if Block scope boxes for the showResult(72) trace, with the printed output scope pass 2, scope 2, global.

2. Place let, const and var in the correct environment

A let or const declared inside braces belongs to that block. A var declared there belongs to the surrounding function. Lexical lookup can use a binding in the current environment or follow outward links, but it never searches an inner or sibling environment.

Setup-time states and early-access errors are worked through in Hoisting in JavaScript: Predict Output with var, let, const and Functions. Closure analysis starts with a narrower operation: place each declaration in its block, function or global environment before resolving any name.

3. The scope chain follows code structure, not the caller

Identifier resolution walks outwards from the current environment to its enclosing environments and global scope, stopping at the first match. A nearer binding shadows, rather than mutates, an outer name.

js
const value = 10;

function readValue() {
  return value;
}

function run() {
  const value = 99;
  return readValue();
}

console.log(run()); // 10

readValue was defined globally, so value resolves to global 10. Calling it inside run, beside value: 99, does not change its lexical parent. Trace from the function's definition site, not its call site.

4. A closure connects a function to its lexical environment

A closure is a function with access to bindings surrounding its definition. A reachable inner function can still read and update them after the outer call returns. It retains live bindings, not frozen copies of their values.

javascript
function makeQuizSession(limit) {
  let answered = 0;

  return function record(count = 1) {
    answered = Math.min(answered + count, limit);
    return `${answered}/${limit}`;
  };
}

const morning = makeQuizSession(5);
const evening = makeQuizSession(3);

console.log(morning(2)); // "2/5"
console.log(evening());  // "1/3"
console.log(morning(2)); // "4/5"

The morning function retains answered and limit from one factory call. Its first call changes answered from 0 to 2, and its next call changes 2 to 4. The evening function reaches a different environment, so its default count changes its own answered value from 0 to 1. The output is 2/5, 1/3, 4/5.

Function syntax, parameters, lexical this and a step-counter factory are compared in Functions and Arrow Functions in JavaScript. The quiz-session closure exposes a different mechanism: two retained environments after both factory calls have returned.

Closures support factories, event handlers and small stateful APIs. Each retained environment stays independent unless the functions deliberately close over the same outer binding.

5. Closure mistakes: shared state, retention and explicit capture

Several callbacks can accidentally share one loop binding. One repair is to create the value that each closure should retain inside the iteration:

javascript
const labels = ["alpha", "beta", "gamma"];
const readers = [];

for (var index = 0; index < labels.length; index += 1) {
  const capturedLabel = labels[index];
  readers.push(() => capturedLabel);
}

console.log(readers.map((read) => read()));
// ["alpha", "beta", "gamma"]

The control variable index is function scoped, but each iteration creates a new block-scoped capturedLabel. The callbacks retain those three separate bindings. A let loop variable or a for...of binding provides the same per-iteration separation without the extra constant.

A closure does not copy the whole outer function. Capturing an object can keep it reachable while the callback is reachable. Remove finished listeners or subscriptions, and capture a small field instead of a large object when possible. JavaScript does not promise a garbage-collection time.

Closures and this solve different problems. An arrow can preserve lexical this; an ordinary nested function does not gain outer this merely by closing over local variables.

6. Output questions require environment tracing

Common tasks ask you to trace shadowing, identify temporal-dead-zone access, repair a var loop, draw a scope chain or build independent closure state. Solve this before reading on:

js
let n = 1;

function outer() {
  let n = 5;
  return function add(amount = 2) {
    n += amount;
    return n;
  };
}

const addToClosedN = outer();
console.log([
  addToClosedN(),
  addToClosedN(3),
  n
]);

The answer is [7, 10, 1]: 5 + 2 = 7, then 7 + 3 = 10. Global n remains 1.

Circle each declaration, draw its environment, connect functions to their definition environments, then update one binding per call in order.

7. Practise scope and closures with three acceptance checks

  1. Shadowing: Set global let score = 1. Inside puzzle, set function-scoped let score = 2; inside an if, set block-scoped let score = 3. Print in the block, inside puzzle after the block, then after calling puzzle. Expect 3, 2, 1.

  2. Independent closure state: Implement makeToggle(initial). One toggle created with false must return true and then false. A second toggle created with true must return false without changing the first toggle's retained state.

  3. Definition-site lookup: Set global const mode = "global" and define readMode beside it. Call readMode from a function whose local mode is "caller". The result must remain "global"; explain the lexical link that supplies it.

8. JavaScript scope and closures: the short version and next step

  • Scope limits where a binding can be accessed.

  • Lexical code structure controls identifier lookup.

  • The nearest matching binding shadows outer names.

  • let and const respect block boundaries.

  • var respects the surrounding function boundary.

  • A closure keeps access to live outer bindings.

Rerun showResult(72), makeQuizSession and the explicit-capture loop after changing one value in each. To keep learning the language in order, work through the Complete JavaScript Course. To pair JavaScript with front-end and server-side tools, follow the MERN Stack Course: Full Stack Development.

Your mastery check is concrete: explain why run() returns 10, why morning(2) reaches 4/5, and why each callback retains a different capturedLabel. If you can do that without running the code, you are tracing lexical scope and closure state instead of guessing.