JavaScript Scope
Scope determines where a variable can be accessed. Think of it like an ID card — your city ID only works within your province, and your provincial ID won't help you abroad. Variables work the same way: once you leave their scope, they're unreachable.
📖 Summary
Global Scope
Variables declared with var, let, or const at the outermost level have global scope and can be accessed from anywhere.
<script>
var globalVar = "I'm a global variable";
function test() {
console.log(globalVar); // Accessible
}
test();
</script>
Global variables are convenient but can easily pollute the namespace — as projects grow, naming conflicts become inevitable. Minimize global variable usage.
Function Scope
Variables declared with var follow function scope: they only exist within the current function and disappear outside it.
<script>
function greet() {
var message = "Hello";
console.log(message); // Works fine
}
greet();
try {
console.log(message); // ReferenceError!
} catch(e) {
console.log("Access failed: " + e.message);
}
</script>
A classic gotcha with var's function scope: using var in a for loop means the variable "leaks" out after the loop ends.
Block Scope
Variables declared with let and const follow block scope: they only exist within the current {}.
<script>
if (true) {
let x = 10;
const y = 20;
var z = 30;
console.log("Inside block: x=" + x + ", y=" + y + ", z=" + z);
}
console.log("Outside block: z=" + z); // var isn't restricted by blocks
try {
console.log(x); // x is not accessible here
} catch(e) {
console.log("x not accessible: " + e.message);
}
</script>
This is why loop counters should use let instead of var.
Scope Chain
Inner scopes can access variables from outer scopes — like how you can use things from home at school (if you brought them). When looking up a variable, JavaScript searches from the current scope outward, layer by layer, until it reaches the global scope. This chain is called the scope chain.
<script>
const city = "Beijing";
function outer() {
const district = "Haidian";
function inner() {
const street = "Zhongguancun";
console.log(street, district, city); // All accessible
}
inner();
}
outer();
</script>
Outer scopes cannot access inner scope variables — just like you can't reach into a student's pocket from outside the classroom.
Hoisting
var declarations are "hoisted" to the top of their scope, but assignments are not:
<script>
console.log(a); // undefined (not an error!)
var a = 5;
// Actual execution order: var a; → console.log(a); → a = 5;
</script>
let and const are also hoisted, but accessing them before declaration throws an error — this region is called the Temporal Dead Zone (TDZ):
<script>
try {
console.log(b); // ReferenceError!
} catch(e) {
console.log("Temporal dead zone: " + e.message);
}
let b = 5;
</script>
Bottom line: always use let and const; avoid the hoisting pitfalls of var.
Closures Explained Intuitively
A closure = a function + the variable environment where it was born. It's like someone who left their hometown but still remembers what it looked like — even if the hometown changed later, their memory of it stays the same.
<script>
function createCounter() {
let count = 0; // This variable is "locked" inside the closure
return function() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// The count variable wasn't destroyed because the returned function still "remembers" it
</script>
createCounter has already finished executing — logically, count should be destroyed. But the returned function holds tightly onto count, so it survives. That's a closure.
Practical Closure Use Cases
- Counters: Record state without global variables (as shown above)
- Private variables: Variables inside closures can't be accessed directly from outside — only through the returned functions
- Callback functions: Maintain references to variables in event listeners and timers
Example: var vs let in Loops
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>var vs let</title>
<style>
body { font-family: sans-serif; padding: 20px; }
.block { margin: 16px 0; padding: 12px; background: #f5f5f5; border-radius: 6px; }
.block h3 { margin-top: 0; }
.var-result, .let-result { margin: 8px 0; font-weight: bold; }
.var-result { color: #d9534f; }
.let-result { color: #5cb85c; }
</style>
</head>
<body>
<h2>var vs let: Scope Differences in Loops</h2>
<div id="output"></div>
<script>
const varResults = [];
const letResults = [];
for (var i = 0; i < 3; i++) {
varResults.push(`Inside loop: i = ${i}`);
}
varResults.push(`Outside loop: i = ${i} (var leaked!)`);
for (let j = 0; j < 3; j++) {
letResults.push(`Inside loop: j = ${j}`);
}
letResults.push(`Accessing j outside loop → ReferenceError (let doesn't leak)`);
document.getElementById("output").innerHTML = `
<div class="block">
<h3>var Declaration</h3>
${varResults.map(r => `<div class="var-result">${r}</div>`).join("")}
</div>
<div class="block">
<h3>let Declaration</h3>
${letResults.map(r => `<div class="let-result">${r}</div>`).join("")}
</div>
`;
</script>
</body>
</html>
Example: Scope Chain Demo
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Scope Chain</title>
<style>
body { font-family: sans-serif; padding: 20px; }
.scope { margin: 12px 0; padding: 12px; border-left: 4px solid #4a90d9; background: #f0f7ff; border-radius: 0 6px 6px 0; }
.scope-inner { margin: 12px 0 12px 20px; padding: 12px; border-left: 4px solid #5cb85c; background: #f0fff0; border-radius: 0 6px 6px 0; }
.scope-innermost { margin: 12px 0 12px 20px; padding: 12px; border-left: 4px solid #f0ad4e; background: #fffdf0; border-radius: 0 6px 6px 0; }
code { background: rgba(0,0,0,0.06); padding: 2px 6px; border-radius: 3px; }
</style>
</head>
<body>
<h2>Scope Chain: Looking Up Variables from Inside Out</h2>
<div id="output"></div>
<script>
const country = "USA";
function outer() {
const state = "California";
function middle() {
const city = "San Francisco";
function inner() {
const district = "Mission";
const result = `${district}, ${city}, ${state}, ${country}`;
return result;
}
return inner();
}
return middle();
}
const final = outer();
document.getElementById("output").innerHTML = `
<div class="scope">
<strong>Global Scope</strong>: country = <code>"${country}"</code>
<div class="scope-inner">
<strong>outer Function Scope</strong>: state = <code>"California"</code>
<div class="scope-innermost">
<strong>middle Function Scope</strong>: city = <code>"San Francisco"</code>
<div style="margin-top:12px; padding:12px; border-left:4px solid #d9534f; background:#fff0f0; border-radius:0 6px 6px 0;">
<strong>inner Function Scope</strong>: district = <code>"Mission"</code>
<p>Innermost scope can access all outer variables → <strong>${final}</strong></p>
</div>
</div>
</div>
</div>
`;
</script>
</body>
</html>
Example: Closure Counter with Private Variables
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Closures</title>
<style>
body { font-family: sans-serif; padding: 20px; text-align: center; }
.counter { display: inline-block; padding: 20px; background: #f0f7ff; border-radius: 12px; margin: 12px; }
.count { font-size: 48px; font-weight: bold; color: #4a90d9; }
button { padding: 10px 24px; font-size: 16px; border: none; border-radius: 6px; cursor: pointer; background: #4a90d9; color: #fff; margin: 4px; }
button:hover { background: #357abd; }
.reset { background: #d9534f; }
.reset:hover { background: #c9302c; }
.info { color: #888; font-size: 14px; margin-top: 8px; }
</style>
</head>
<body>
<h2>Closure Counter</h2>
<div class="counter">
<div class="count" id="display">0</div>
<button id="increment">+1</button>
<button id="decrement">-1</button>
<button id="reset" class="reset">Reset</button>
<div class="info">The count variable is protected by the closure — it can't be modified directly from outside</div>
</div>
<script>
function createCounter() {
let count = 0;
return {
increment() { return ++count; },
decrement() { return --count; },
reset() { count = 0; return count; },
getCount() { return count; }
};
}
const counter = createCounter();
const display = document.getElementById("display");
function updateDisplay() {
display.textContent = counter.getCount();
}
document.getElementById("increment").addEventListener("click", function() {
counter.increment();
updateDisplay();
});
document.getElementById("decrement").addEventListener("click", function() {
counter.decrement();
updateDisplay();
});
document.getElementById("reset").addEventListener("click", function() {
counter.reset();
updateDisplay();
});
</script>
</body>
</html>
Example: Hoisting Comparison
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hoisting</title>
<style>
body { font-family: sans-serif; padding: 20px; }
.compare { display: flex; gap: 20px; margin: 16px 0; }
.col { flex: 1; padding: 16px; border-radius: 8px; }
.col-var { background: #fff0f0; border: 2px solid #d9534f; }
.col-let { background: #f0fff0; border: 2px solid #5cb85c; }
h3 { margin-top: 0; }
code { background: rgba(0,0,0,0.06); padding: 2px 6px; border-radius: 3px; }
pre { background: #fff; padding: 12px; border-radius: 6px; overflow-x: auto; }
</style>
</head>
<body>
<h2>Hoisting: var vs let</h2>
<div id="output"></div>
<script>
var varBefore = "Access before var declaration → " + typeof aVar;
var aVar = 5;
var varAfter = "Access after var declaration → " + aVar;
let letAfter = 5;
let letBefore = "Access before let declaration → throws ReferenceError!";
document.getElementById("output").innerHTML = `
<div class="compare">
<div class="col col-var">
<h3>var (hoisted but not assigned)</h3>
<pre><code>console.log(typeof aVar); // ${varBefore}
var aVar = 5;
console.log(aVar); // ${varAfter}</code></pre>
<p>No error, but you get <code>undefined</code> — a more subtle bug!</p>
</div>
<div class="col col-let">
<h3>let (temporal dead zone)</h3>
<pre><code>console.log(bVar); // ${letBefore}
let bVar = 5;</code></pre>
<p>Throws an error immediately — you catch the problem right away!</p>
</div>
</div>
`;
</script>
</body>
</html>
❓ FAQ
var in a for loop cause all callbacks to output the same value?var has function scope — after the loop ends, there's only one i, and all callbacks share it (which by then equals the loop's final value). With let, each iteration gets its own independent copy of i.null.let or const?const; only use let when you need to reassign. This habit prevents you from accidentally modifying variables that shouldn't change.📝 Exercises
- Write a function
makeGreeter(greeting)that returns a function which, when called with a name, returnsgreeting + ", " + name. Create two greeting functions with different greetings and verify they don't interfere with each other. - Write a
createWallet(initialBalance)function that returns three methods:deposit,withdraw, andgetBalance. Thebalanceshould not be directly accessible from outside (private variable) — it can only be manipulated through these methods. - Explain the output of the following code and why:
<script>
for (var i = 0; i < 3; i++) {
setTimeout(function() { console.log(i); }, 100);
}
</script>
Then modify it to output 0, 1, 2.



