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,constorclassinside{ }, 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.
02Declare in each scope
Here is one name in each box. The innermost line can use all three; outside, only the global one exists.
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 here03See 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.
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
Move the console.log
In the first example, move
console.log(site, course, lesson)to just after theifblock. Predict the error message, then run. - 2
Remove a let
In the second example, delete the word
letinsidepaint(keepcolor = "blue"). Which of the three lines changes, and why? - 3
Turn on strict mode everywhere
In the third example, add
"use strict";as the very first line. Nowoops()throws too. Which line in the console changes?
Code & diagrams
`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
undefinedPreview
Console
Nothing logged yet.Expected console output
inner block: green
function: blue
global: red
count: 2`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 definedBreak 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 }.
Break #2
Typo creates a hidden global
In a classic script without strict mode, declare let total = 0; then write totl = total + 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/constlive in a separate declarative record of the global environment, which is why they don't appear onwindowyet are shared across all classic scripts on the page (two<script>tags declaring the sameletname throw aSyntaxErrorin the second). - ▸
evalandwithcan 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"becomeswindow.total).
Remember this
- 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,constorclassinside any{ }block (anif, a loop, or bare braces), visible only inside that block. (Modules add a fourth, module scope, covered in the modules phase.) - 2
varignores blocks. Avardeclared inside anifor aforbelongs 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 preferletandconst: they stay inside the block where you wrote them, so a loop counter can't leak out and clash with another one. - 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
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 outercountdoesn't move, because you changed a different variable with the same name. Leaving outletdoes the opposite:count = count + 1with no declaration updates the outer variable. - 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
Two global-scope gotchas. (1) In a classic (non-module) script, top-level
varand function declarations become properties of the global objectwindow, while top-levelletandconstdon't. (2) In sloppy mode (the default for classic scripts), assigning to a name that was never declared, liketotal = 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 aReferenceError, which catches typos early.
Explain it without notes
Name the kinds of scope in JavaScript and which declarations belong to each.
What is the scope chain, and what happens when a name isn't found?
What is lexical scope? Give an example where it matters.
What is shadowing, and how does it differ from reassigning an outer variable?
Practice
Predict what this prints, then check: let a = 1; { let a = 2; console.log(a); } console.log(a);
Rewrite this so i can't be read after the loop: for (var i = 0; i < 3; i++) {} console.log(i);
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
varignores blocks andlet/constdon'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.