Scope and closures

Introduction

If every variable could be used from anywhere, then any line of a long program could change any value, and bugs would be nearly impossible to trace. Scope limits where a name can be seen. Closures are what you get when scope meets functions as values, and they let a function keep private information between calls.

How it works

The scope of a variable is the part of the program where its name can be used:

  • Global scope: declared outside every function and block; visible everywhere.
  • Function scope: declared inside a function; visible only in that function.
  • Block scope: let and const inside a block (the braces of an if, a loop, and so on) are visible only inside it. var ignores blocks and belongs to the whole function.

When a name is used, JavaScript looks in the current scope first, then in the scope around it, and so on out to the global scope. A variable inside can have the same name as one outside: it shadows the outer one while the inner code runs.

// Global, function and block scope, and shadowing.
const course = "Web";             // global

function show(level) {
  const course = "JavaScript";    // shadows the global course
  if (level > 1) {
    const note = "advanced";      // only inside this block
    console.log(course, level, note);
  }
  return course;
}

console.log(show(2));
console.log(course);              // the global one is unchanged
Open in compiler

Hoisting. Before code runs, JavaScript sets up every declaration in a scope. Function declarations can be called before their line. let and const variables also exist from the start of their block but cannot be used until their line runs. The time between is called the temporal dead zone, and reading the variable there is a ReferenceError.

Closures. A function remembers the scope it was created in, even after that outer function has returned. A function together with the variables it remembers is a closure. Variables stay alive for as long as something can still use them, so the outer function's variable does not disappear when the function returns.

Syntax

function makeAdder(amount) {
  return (x) => x + amount;   // remembers amount
}
const addFive = makeAdder(5);
addFive(2);                   // 7

Example

// A closure: the inner function remembers count.
function makeCounter() {
  let count = 0;
  return function () {
    count += 1;
    return count;
  };
}

const a = makeCounter();
const b = makeCounter();
console.log(a(), a(), a());
console.log(b());                 // b has its own count
Open in compiler

Line by line

  1. makeCounter creates a variable count, and returns an inner function that uses it.
  2. const a = makeCounter() runs the outer function. It finishes, but a still holds the inner function, so count lives on.
  3. const b = makeCounter() runs it again, which makes a separate count.
  4. Each a() adds 1 to a's own count, giving 1, 2 and 3.
  5. b() uses its own count, which is still at 0, so it gives 1.

Expected result

1 2 3
1

In the real world

Closures are how JavaScript keeps private data and remembers state between button clicks. Every event handler that uses a variable from outside itself is a closure. Caches, rate limiters and React's useState hook are all built on this idea.

Common mistakes

  • Using var in a loop that creates functions. They all share one variable. let gives each pass its own.
  • Assigning to a name that was never declared. In strict mode this is a ReferenceError, not a new global variable.
  • Expecting an inner variable to be visible after its block ends.
  • Shadowing a variable by accident and changing the wrong one.

Best practices

  • Declare variables in the smallest scope that works.
  • Avoid global variables, which any part of the program can change.
  • Use a closure when a function needs private state that survives between calls.

Summary

Scope decides where a name is visible, and lookups go from the inside outward. let and const belong to their block. A closure is a function that keeps the variables of the place it was created, and each call of the outer function creates new ones.

Quick check

1. Where is a let variable declared inside an if block visible?
2. What is a closure?
3. What happens when you read a let variable before its declaration line?
4. const a = makeCounter(); const b = makeCounter(); a(); a(); b(); what does b() give next?

Key takeaways

  • JavaScript looks for a name in the current scope, and then outward.
  • A closure keeps the variables of the scope it was created in alive.
  • Each call of an outer function makes a new set of variables.

Practice problems for this lesson

My notes

Write down what you want to remember from this lesson. Notes are saved on this device as you type, and appear on your dashboard.