Scope & Hoisting

Scope decides where a variable can be seen. Hoisting describes what JavaScript does with declarations before your code runs. Together they explain a lot of errors that look mysterious at first — "is not defined", "cannot access before initialization" — and they're the foundation for closures in the next lesson.

Three kinds of scope

  • Global scope — variables declared outside any function or block. Visible everywhere.
  • Function scope — variables declared inside a function. Visible only inside that function.
  • Block scope — let and const declared inside { } (an if, a loop, or a bare block). Visible only inside that block.
JS scopes.js
const appName = "Notes";

function showTitle() {
  const prefix = "★";
  if (true) {
    const suffix = "!";
    console.log(prefix, appName, suffix);
  }
  console.log(typeof suffix);
}

showTitle();
console.log(typeof prefix);
Output
★ Notes !
undefined
undefined

typeof is used here because it doesn't throw for undeclared names. Reading suffix directly outside its block would be a ReferenceError.

The scope chain

When code uses a name, JavaScript looks in the current scope, then the enclosing scope, then the next one out, all the way to the global scope. Inner code can see outer variables; outer code can't see inner ones:

JS chain.js
const level = "global";

function outer() {
  const level = "outer";
  function inner() {
    console.log("inner sees:", level);
  }
  inner();
}

outer();
console.log("global sees:", level);
Output
inner sees: outer
global sees: global

The inner level shadows the global one: the nearest declaration wins. Shadowing is legal but can be confusing, so avoid reusing names unnecessarily.

Scope is decided by where code is written, not where it's called from. This is called lexical scope.

Avoid global variables

Every script on a page shares the global scope. Global variables can be overwritten by other scripts or browser extensions, and make it hard to know what depends on what. Keep variables inside functions and modules (lesson 55) — module files have their own scope, so their top-level variables aren't global at all.

Accidental globals: assigning to a name that was never declared (total = 5 with no let/const) creates a global variable in sloppy mode. Strict mode, which modules and classes use automatically, turns this into an error instead (lesson 56).

Hoisting

Before running a scope's code, JavaScript scans it for declarations and sets them up first. This is called hoisting, and different declarations are hoisted differently:

DeclarationBefore its line runs…
function f() {}Fully available — you can call it
var xExists, with the value undefined
let x, const x, class XExists, but any access is an error (the "temporal dead zone")
JS hoisting.js
console.log(sayHello());
console.log(oldVar);

function sayHello() {
  return "hello";
}
var oldVar = "set later";
Output
hello
undefined
JS tdz.js
console.log(total);
let total = 10;
Output
Uncaught ReferenceError: Cannot access 'total' before initialization

The error message gives it away: total exists (it was hoisted), but it can't be used yet. This "temporal dead zone" is deliberate — it turns silent undefined bugs from the var era into clear errors.

Function expressions aren't hoisted like declarations

JS expression-hoisting.js
console.log(typeof square);
console.log(square(3));

const square = n => n * n;
Output
Uncaught ReferenceError: Cannot access 'square' before initialization

Only the const square declaration is hoisted (into the dead zone), not the function assigned to it. Function declarations are the only thing you can use before their line.

Block scope in loops

With let, each loop iteration gets its own copy of the loop variable. This matters when the loop creates functions that run later:

JS loop-scope.js
const withLet = [];
for (let i = 0; i < 3; i++) {
  withLet.push(() => i);
}

const withVar = [];
for (var j = 0; j < 3; j++) {
  withVar.push(() => j);
}

console.log(withLet.map(f => f()));
console.log(withVar.map(f => f()));
Output
[ 0, 1, 2 ]
[ 3, 3, 3 ]

All three var functions share a single j, which is 3 by the time they run. This classic bug is one more reason to avoid var.

Try it yourself

Try it yourself
Console output