Scope and hoisting

JavaScript · Practical · 10 min

Knowing where a name is visible explains a whole class of 'undefined' and ReferenceError bugs.

JavaScript uses lexical scope: where a variable is visible is decided by where it is written in the source, not by how functions are called. Scopes nest, and a lookup walks outward from the innermost scope until it finds the name — or throws ReferenceError.

The kinds of scope:

  • Global — names visible everywhere. globalThis is the global object in every environment.
  • Module — each ES module has its own top-level scope; nothing leaks to the global unless you put it there.
  • Function — parameters and everything declared inside.
  • Block — let, const and class declarations inside { }.

Hoisting describes the fact that declarations are processed when a scope is entered, before any code in it runs:

  • Function declarations are hoisted completely; you can call them before their line.
  • var declarations are hoisted and initialised to undefined, so reading one early silently gives undefined. var also ignores blocks and is scoped to the whole function.
  • let, const and class are hoisted but not initialised. Touching them before the declaration throws a ReferenceError; that window is the temporal dead zone (TDZ).

An inner declaration with the same name shadows the outer one within its scope. That is legal but can be confusing; linters can flag it.

In non-module scripts, a top-level var or function declaration becomes a property of the global object, and assigning to an undeclared name creates a global in sloppy mode. Modules and strict mode turn the latter into an error — another reason to write modules.

Example

console.log(hoisted());   // "works": declarations are hoisted
function hoisted() { return "works"; }

console.log(v);           // undefined (var hoisted, initialised to undefined)
var v = 1;

try {
  console.log(l);         // TDZ
} catch (e) {
  console.log(e.name);    // ReferenceError
}
let l = 2;

function scopes() {
  if (true) {
    var fnScoped = "var";     // visible in the whole function
    let blockScoped = "let";  // only inside this block
  }
  console.log(fnScoped);                // "var"
  console.log(typeof blockScoped);      // "undefined" (not in scope)
}
scopes();

const name = "outer";
{ const name = "inner"; console.log(name); } // inner (shadowing)
console.log(name);                           // outer

Remember

  • Scope is lexical: decided by where code is written
  • var is function-scoped and reads as undefined early; let/const throw in the TDZ
  • Function declarations are fully hoisted; function expressions are not

Watch out for

  • Relying on var inside an if or loop and expecting it to stay in that block.

Check yourself

  1. Inside a function: console.log(x); let x = 1; — what happens?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: developer.mozilla.org/en-US/docs/Glossary/Hoisting