Lesson 2·M1 Core Mechanics
Scope & the Scope Chain
OpenLearning Objectives
By the end of this lesson you'll understand:
- What scope is and why it exists
- Global, function and block scope
- Lexical scope and the scope chain
- Variable shadowing
- How scope keeps large codebases maintainable
What is Scope?
Scope determines where a variable is accessible in your program. Think of it as visibility.
Imagine a company:
- The CEO can access everything
- A department manager can access only their department
- An employee can access only their own tasks
Variables work the same way.
Why Do We Need Scope?
Imagine this:
let username = 'Ali';
// 5000 lines later…
username = 'Ahmed';
Without scope, any part of the application could modify any variable. Scope:
- Prevents accidental modification
- Avoids naming conflicts
- Organises code and improves readability
- Keeps variables alive only where they're needed
Global Scope
A variable declared outside any function or block is global.
const appName = 'My App';
function start() {
console.log(appName);
}
start();
console.log(appName);
Output:
My App
My App
Both the function and the top level can see it. Too many globals is a problem, because they can be changed from anywhere, they collide by name, and they make debugging harder. Modern applications keep the global scope as small as possible.
Function Scope
Variables declared inside a function exist only inside it.
function login() {
const token = 'abc123';
console.log(token);
}
login();
console.log(token);
Output:
abc123
💥 ReferenceError: token is not defined
Think of a function as a room. People inside can see everything in it; people outside can't see in.
Block Scope
A block is anything inside { } — if, for, while, or a bare block. Variables declared
with let and const are block-scoped.
if (true) {
const message = 'Hello';
}
console.log(message);
Output:
💥 ReferenceError: message is not definedvar Is Not Block Scoped
This is one of the biggest reasons var is discouraged.
if (true) {
var age = 28;
}
console.log(age);
Output:
28
Most people expect age to disappear with the block. It doesn't — var is function-scoped,
so it escapes to the whole function. Compare:
if (true) {
let age = 28;
}
console.log(age);
Output:
💥 ReferenceError: age is not defined
Safer, and far more predictable.
Lexical Scope
A favourite interview topic. Lexical scope means a function can access variables from the place where it was written — not where it's called.
const company = 'OpenAI';
function developer() {
const team = 'Frontend';
function intern() {
console.log(company);
console.log(team);
}
intern();
}
developer();
Output:
OpenAI
Frontend
intern can see its own variables, developer's, and the global ones. That's the chain.
Now the version that catches people out — predict it before running:
const value = 'global';
function show() {
console.log(value);
}
function run() {
const value = 'local';
show(); // what prints?
}
run();
It prints global. show was written next to the global value, so that's the chain
it carries — forever. Calling it from inside run doesn't re-point it. A function remembers
the neighbourhood it was born in, not the one it's visiting.
Get this and closures (Lesson 5) become obvious, because a closure is just this chain outliving the function that created it.
The Scope Chain
When JavaScript looks for a name it searches outward, stopping at the first match:
current scope → parent → parent's parent → global → ReferenceError
Visually:
┌─ Global ────────────────────────┐
│ const app = 'roadmap' │
│ │
│ ┌─ outer() ──────────────────┐ │
│ │ const user = 'Ada' │ │
│ │ │ │
│ │ ┌─ inner() ─────────────┐ │ │
│ │ │ const id = 7 │ │ │
│ │ │ │ │ │
│ │ │ id → found here │ │ │
│ │ │ user → found ───────┼─┘ │
│ │ │ app → found ───────┼───┘
│ │ │ nope → ReferenceError
│ │ └───────────────────────┘
│ └────────────────────────────┘
└─────────────────────────────────┘
The search only goes outward, never inward. outer() cannot see id.
const app = 'roadmap';
function outer() {
const user = 'Ada';
function inner() {
const id = 7;
console.log(id, user, app); // reaches out three levels
}
inner();
// console.log(id); ← would throw: inner's scope is invisible from here
}
outer();Variable Shadowing
An inner scope can declare a name that already exists outside. The inner one wins locally; the outer one is untouched.
const language = 'JavaScript';
function demo() {
const language = 'TypeScript';
console.log(language);
}
demo();
console.log(language);
Output:
TypeScript
JavaScript
Compare that with not redeclaring — then you're reaching out and modifying the original:
let count = 1;
{
count = 99; // no `let` → this is the outer count
}
console.log(count);
Output:
99
One let is the entire difference between "create a new variable" and "modify the existing
one".
Common Interview Questions
Q1. What is scope?
The set of names visible from a given point in the code. JavaScript has global scope, function scope and block scope.
The one that trips people is that
varonly respects function boundaries, so it leaks out ofifandforblocks.letandconstrespect any{ }.
Q2. What is the scope chain?
Every scope holds a reference to the scope that contains it. When you use a name, the engine checks the current scope, then walks outward link by link until it finds it — otherwise you get
is not defined.The search only goes outward. An outer scope can never see into an inner one.
Q3. What does it mean that scope is lexical?
The chain is fixed by where a function is written in the source, not by where it's called from. A function carries the neighbourhood it was born in.
So if
show()is defined at the top level and readsvalue, calling it from inside a function that has its own localvaluestill prints the global one.
Q4. Why is var considered unsafe?
It's function-scoped rather than block-scoped, so it escapes blocks people expect to contain it. It can also be redeclared silently, and it's hoisted as
undefinedrather than throwing.
Q5. What is variable shadowing, and how is it different from reassigning?
Shadowing is declaring a name in an inner scope that already exists outside — the inner one hides the outer for that block, and the outer is untouched. Without a declaration keyword you're not shadowing at all: the engine walks the chain and modifies the outer variable.
Q6. What actually creates a new scope?
A function call, any
{ }block forletandconst, a module, and the parameter of acatchclause. An object literal's braces don't — they aren't a block.
Senior follow-up: Lexical versus dynamic scope — and what does that have to do with closures?
Lexical means the chain comes from source position, which is what JavaScript does. Dynamic means it would come from the call stack — what shell scripts and some older Lisps do — so the same function could see different variables depending on the caller.
Closures fall straight out of the lexical rule: since a function keeps the chain it was written in, it still has that chain after the outer function has returned.
Best Practices
✅ Keep variables scoped as narrowly as they can be
✅ Minimise globals — prefer modules and function parameters
✅ Use const by default, let only when reassignment is required
✅ Avoid reusing a name in a nested scope unless the shadowing is deliberate
❌ Don't rely on var escaping a block. If you need it outside, declare it outside
Coding Exercise
Predict each output before running.
// 1
const city = 'Riyadh';
function showCity() { console.log(city); }
showCity();
// 2
function demo() { const age = 28; }
// console.log(age); ← uncomment: what happens?
// 3
if (true) { var x = 10; }
console.log(x);
// 4
if (true) { let y = 20; }
// console.log(y); ← uncomment: what happens?
// 5
const name = 'Ali';
function printName() {
const name = 'Ahmed';
console.log(name);
}
printName();
console.log(name);
Answers
Riyadh— lexical scope:showCitysees the globalcityReferenceError—ageis function-scoped todemo10—varescapes the blockReferenceError—letdoesn'tAhmedthenAli— shadowing, with the outer untouched
Mini Challenge
Write a small program that demonstrates all four:
- A global variable
- A function-scoped variable
- A block-scoped variable
- Variable shadowing
Then explain, out loud, which variables are reachable from each part of it.
Lesson Summary
Recap
- Scope is the set of names visible from a point in the code
- Three kinds: global, function (
varstops here) and block (let/conststop here) - Lookup walks outward only — current scope, parent, parent, global, then
ReferenceError - Lexical scope: the chain is set by where you wrote the function, not where you called it
- Shadowing creates a new variable; dropping the keyword modifies the outer one
- Minimise globals and keep every variable as narrowly scoped as you can
Check your understanding
Answer each one out loud. Then rate yourself honestly.
0/6From inside a doubly-nested function, describe the exact order the engine searches for a name — and what happens when it runs out.
Why can inner() see outer's variables, but never the reverse?
Predict the output when a top-level show() is called from inside a run() that declares its own value — and say why.
What is the difference between writing let count = 99 and count = 99 inside a block?
Which of if, for, a function call, and catch (err) create a scope that stops a var? Which stop a let?
Explain why lexical scoping is what makes closures possible.
What's Next
Lesson 3 — Hoisting. What hoisting actually is, whether JavaScript really "moves"
declarations (it doesn't), why var, let, const and functions behave differently, and
how it all relates to the execution context.