JavaScript From Zero · 20 min · 150 XP

Scope, closures and modules

Know where a variable can be seen, let a function remember state privately, and split code into modules that share only what they export.

Scope is where a name can be seen. let and const are block-scoped: a variable declared inside { } (an if, a loop, a function) exists only inside those braces. When JavaScript meets a name, it looks in the current block, then the block around it, and so on outwards to the top of the file. It never looks inwards, so an outer block can't see a variable declared inside an inner one.

javascript
const taxRate = 0.2;              // visible everywhere below

function priceWithTax(price) {
  const tax = price * taxRate;     // taxRate found one level out
  if (tax > 100) {
    const note = "high tax";       // exists only inside this if
  }
  return price + tax;              // note doesn't exist here
}

console.log(tax);                  // ReferenceError: tax is not defined

Keep variables in the smallest scope that works. A variable at the top of a classic script is global: every other script on the page can read it, change it, or collide with it by using the same name. That's how two libraries that both define total break each other. var, which you'll meet in older code, ignores blocks and is scoped to the whole function, which is why it leaks out of loops.

A closure is what happens when a function uses a variable from the scope it was created in: the function keeps that variable alive, even after the outer function has returned. Each call to the outer function makes a fresh variable, so each closure has its own. It's the standard way to keep state private: nothing outside can reach count except through the functions you hand back.

Two counters, each with its own private count
function makeCounter() {
  let count = 0;                       // a new count for every call
  return {
    increment: () => ++count,
    value: () => count,
  };
}

const likes = makeCounter();
const views = makeCounter();
likes.increment(); likes.increment();
views.increment();
likes.value();   // 2
views.value();   // 1
likes.count;     // undefined: the variable isn't a property, and no one else can change it

You've already relied on closures: an event handler that changes a quantity variable declared outside it is one. React's hooks, later, are closures too, and one that captured an old value is behind a common kind of React bug.

Modules give every file its own scope. In a file loaded with type="module" (or imported by one), top-level variables are private to that file; nothing becomes global by accident. A file shares something by exporting it, and another file uses it by importing it by name. In the browser, the path needs the ./ and the .js. A module runs once, however many files import it, so they all share the same exported values.

cart.js and app.js
// cart.js
const items = [];                        // private to this file
export function addItem(item) {
  items.push(item);
}
export function itemCount() {
  return items.length;
}

// app.js, loaded with <script type="module" src="./app.js">
import { addItem, itemCount } from "./cart.js";
addItem({ id: "tent" });
itemCount();   // 1
items;         // ReferenceError: items is private to cart.js

The lesson editor runs a single script, so it can't show import and export between files. The challenge practises scope and closures; to see modules work, make the two files above in a folder, serve it with your editor's local server, and open the page with the console open.

Loading your workspace…