Command Palette

Search for a command to run...

Hectal
PHASE 3Beginner ~25 min· topic 4 of 9

Topic 3.4

Scope

In one line

Scope is the region of code where a variable can be seen: global scope is visible everywhere, function scope inside one function, and block scope (for let and const) inside one pair of braces; lookups go from the inside outwards, and an inner variable with the same name shadows the outer one.

Think of it like this

Rooms in a house with one-way windows. From your bedroom (a block) you can look out and see the hallway (the function) and the garden everyone shares (the global scope). But people in the garden can't see into your bedroom. If there's a ball in your room and another ball in the garden, when you say "the ball" you mean the one in your room: the nearer one shadows the farther one.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Scope
The part of the code where a name can be seen and used.
Global scope
The outermost scope. Names declared there are visible everywhere.
Function scope
Names declared inside a function, visible only inside it.
Block scope
Names declared with let, const or class inside { }, visible only inside those braces.
Scope chain
The list of scopes, from inner to outer, that the engine searches for a name.
Shadowing
An inner variable with the same name as an outer one, hiding it inside the inner scope.
Lexical scope
What a function can see is decided by where it is written, not where it is called.
Strict mode
A stricter set of rules (turned on by "use strict") that turns silent mistakes into errors.

Step by step

01Draw the boxes

Picture every function and every block as a box drawn around its code. Boxes nest inside each other. Code inside a box can see its own names and the names in every box around it.

Draw the boxesdiagram
Rendering diagram…

02Declare in each scope

Here is one name in each box. The innermost line can use all three; outside, only the global one exists.

scope.jswhole filejavascript
const site = "Hectal";            // global
function showScope() {
  const course = "JavaScript";    // function scope
  if (true) {
    const lesson = "Scope";       // block scope
    console.log(site, course, lesson); // all visible
  }
  // lesson is not visible here
}
// course and lesson are not visible here

03See var escape the block

Swap const lesson for var lesson and it's visible in the whole function, after the if too. With let/const it stays inside.

04Follow a lookup

When the engine meets site inside the if, it checks the block (not there), then the function (not there), then the global scope (found). If no box has it, you get a ReferenceError.

05Shadow on purpose, then by accident

Declaring let color inside a function creates a new variable that hides the outer color. Assigning without let changes the outer one instead. Read each line carefully: one keyword changes which variable you're touching.

shadow.jswhole filejavascript
let color = "red";
function paint() {
  let color = "blue";  // new variable, shadows the outer one
}
function repaint() {
  color = "green";     // no let: changes the outer variable
}

Try it yourself

  1. 1

    Move the console.log

    In the first example, move console.log(site, course, lesson) to just after the if block. Predict the error message, then run.

  2. 2

    Remove a let

    In the second example, delete the word let inside paint (keep color = "blue"). Which of the three lines changes, and why?

  3. 3

    Turn on strict mode everywhere

    In the third example, add "use strict"; as the very first line. Now oops() throws too. Which line in the console changes?

Code & diagrams

Global, function and block scope ES2015+ New tab

`typeof` on a name that isn't visible gives `"undefined"` instead of throwing, which makes it handy for these experiments.

Console

(nothing was printed)

Expected console output

Hectal JavaScript Scope
var ignores the block
undefined
undefined
undefined
Shadowing, and updating the outer variable New tab

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

inner block: green
function: blue
global: red
count: 2
Lexical scope and the global object New tab

`readX` sees the global `x`, not the caller's. The last call throws on purpose: strict mode refuses to create an accidental global.

Console

(nothing was printed)

Expected console output

global x
1 undefined
42
Uncaught ReferenceError: missing is not defined

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Use a block variable outside its block

Declare const lesson inside an if and log it after the }.

terminal
$ Console
── what you'll see ──
Uncaught ReferenceError: lesson is not defined

Break #2

Typo creates a hidden global

In a classic script without strict mode, declare let total = 0; then write totl = total + 5;.

terminal
$ console.log(total, window.totl)
── what you'll see ──
0 5

Myth vs fact

Myth

Every pair of braces creates a scope for every variable.

Fact

Braces create a scope for let, const and class. var only respects function boundaries.

Myth

A function can see the variables of whoever calls it.

Fact

JavaScript is lexically scoped: a function sees the scopes around where it's written, not where it's called.

Myth

Shadowing changes the outer variable.

Fact

It creates a separate variable. The outer one keeps its value; it's just hidden inside the inner scope.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Internally the spec models scopes as Environment Records linked by an [[OuterEnv]] pointer; the scope chain is that linked list. Engines resolve most lookups at compile time to fixed slots, so deep nesting costs nothing at run time.

  • ▸

    Global let/const live in a separate declarative record of the global environment, which is why they don't appear on window yet are shared across all classic scripts on the page (two <script> tags declaring the same let name throw a SyntaxError in the second).

  • ▸

    eval and with can add names to a scope at run time, which forces engines to de-optimise lookups; both are banned or limited in strict mode, one more reason to stay strict.

  • ▸

    Minimising globals matters in real pages: every script, extension and third-party widget shares the same global scope, and named globals can even collide with element ids (an element with id="total" becomes window.total).

Remember this

  1. 1

    Scope is the set of places in your code where a name (a variable, constant or function) is visible and usable. JavaScript has three everyday kinds. Global scope: names declared outside every function and block, visible everywhere. Function scope: names declared inside a function (including its parameters), visible only inside that function. Block scope: names declared with let, const or class inside any { } block (an if, a loop, or bare braces), visible only inside that block. (Modules add a fourth, module scope, covered in the modules phase.)

  2. 2

    var ignores blocks. A var declared inside an if or a for belongs to the whole enclosing function (or to the global scope if there is no function). That's one big reason Topic 1.1 told you to prefer let and const: they stay inside the block where you wrote them, so a loop counter can't leak out and clash with another one.

  3. 3

    When code uses a name, the engine looks for it in the current scope first. If it isn't there, it looks in the scope around that one, then the one around that, all the way out to the global scope. This chain of nested scopes is the scope chain. If the name isn't found anywhere, reading it throws ReferenceError: x is not defined. The lookup only ever goes outwards, never inwards: outer code can't see inner variables.

  4. 4

    Shadowing happens when an inner scope declares a name that already exists in an outer scope. Inside the inner scope, the inner variable wins and the outer one is hidden (it is untouched, just not reachable by that name). Shadowing is legal and sometimes useful, but it's a common source of confusion: you change "count" and the outer count doesn't move, because you changed a different variable with the same name. Leaving out let does the opposite: count = count + 1 with no declaration updates the outer variable.

  5. 5

    JavaScript uses lexical scope (also called static scope): which variables a function can see is decided by where the function is written in the source code, not by where it is called from. A function defined at the top level sees the global variables, even when it's called from inside another function that has a local variable of the same name. This rule is the foundation of closures (Topic 3.6).

  6. 6

    Two global-scope gotchas. (1) In a classic (non-module) script, top-level var and function declarations become properties of the global object window, while top-level let and const don't. (2) In sloppy mode (the default for classic scripts), assigning to a name that was never declared, like total = 42, silently creates a new global variable. Strict mode ("use strict"; at the top of a script or function, and always on in modules and classes) turns that into a ReferenceError, which catches typos early.

Explain it without notes

01

Name the kinds of scope in JavaScript and which declarations belong to each.

02

What is the scope chain, and what happens when a name isn't found?

03

What is lexical scope? Give an example where it matters.

04

What is shadowing, and how does it differ from reassigning an outer variable?

Practice

01

Predict what this prints, then check: let a = 1; { let a = 2; console.log(a); } console.log(a);

02

Rewrite this so i can't be read after the loop: for (var i = 0; i < 3; i++) {} console.log(i);

03

Write a function makeLabel(text) that uses a local prefix constant, and show that prefix is not visible outside it.

Trade-offs

  • ↔

    Declaring variables in the smallest scope that needs them prevents clashes and makes code easier to move, at the cost of occasionally hoisting a declaration up when two blocks need to share it.

  • ↔

    Shadowing can make small blocks read naturally, but linters often warn about it (no-shadow) because it hides bugs in larger functions.

Done when you can

  • I can name global, function and block scope and what lives in each.

  • I know var ignores blocks and let/const don't.

  • I can trace a name lookup through the scope chain.

  • I can explain lexical scope with an example.

  • I can spot shadowing and accidental globals, and I know strict mode stops the latter.