Global Scope
Variables declared outside any function or block live in the global scope. In a browser, globals are properties of the window object.
// Global variable β accessible anywhere in the file
const appName = "ylearner";
let userCount = 0;
function incrementUsers() {
userCount++; // Accesses and modifies global variable
}
incrementUsers();
incrementUsers();
console.log(userCount); // 2
console.log(appName); // ylearner
Every global variable can be accidentally overwritten by any other script or library. Keep globals minimal. In modules (type="module"), variables are module-scoped by default, not global.
Function (Local) Scope
Variables declared inside a function are scoped to that function β invisible outside it.
function calculateTax(price) {
const taxRate = 0.15; // Local β only visible inside calculateTax
const tax = price * taxRate;
return tax;
}
console.log(calculateTax(100)); // 15
try {
console.log(taxRate); // ReferenceError!
} catch (e) {
console.log("Error: taxRate is not accessible outside the function.");
}
// Each function call gets its own scope
function counter() {
let count = 0; // Fresh for each call
count++;
return count;
}
console.log(counter()); // 1
console.log(counter()); // 1 β fresh count each time
Block Scope (let and const)
let and const are block-scoped β they live only inside the {} block where they are declared.
// Block scope with if statement
if (true) {
let blockVar = "I'm in the if block";
const blockConst = "Me too";
console.log(blockVar); // Works
}
// blockVar is gone here
try {
console.log(blockVar);
} catch (e) {
console.log("blockVar not accessible outside block");
}
// Block scope with for loops
for (let i = 0; i < 3; i++) {
const msg = `iteration ${i}`;
console.log(msg);
}
// i and msg are not accessible here
var and Function Scope Gotcha
var ignores block scope β it is function-scoped. This leads to surprising behavior with loops and conditionals.
// var leaks out of blocks
if (true) {
var leaked = "I escaped the if block!";
}
console.log(leaked); // "I escaped the if block!"
// var in a for loop leaks into the function scope
for (var j = 0; j < 3; j++) { /* ... */ }
console.log(j); // 3 β j is still accessible!
// let is properly block-scoped
for (let k = 0; k < 3; k++) { /* ... */ }
try {
console.log(k); // ReferenceError
} catch (e) {
console.log("k not accessible outside loop");
}
Scope Chain
When JavaScript looks up a variable, it starts in the current scope and moves outward through parent scopes until it finds the variable or reaches the global scope.
const globalVar = "global";
function outer() {
const outerVar = "outer";
function inner() {
const innerVar = "inner";
// Inner can access all parent scopes
console.log(innerVar); // "inner" β own scope
console.log(outerVar); // "outer" β parent scope
console.log(globalVar); // "global" β global scope
}
inner();
// Outer cannot access inner's scope
try {
console.log(innerVar);
} catch (e) {
console.log("innerVar not accessible in outer");
}
}
outer();
Lexical Scope
Lexical scope means that scope is determined by where a function is written in the source code, not where it is called from. JavaScript is lexically scoped.
const lang = "JavaScript";
function getLanguage() {
return lang; // Reads 'lang' from where getLanguage is defined (global)
}
function demo() {
const lang = "Python"; // Local shadowing variable
console.log(getLanguage()); // "JavaScript" β NOT "Python"!
// getLanguage sees the scope where it was defined, not where it's called
}
demo();
Some older languages use dynamic scope β a function sees the variables of its caller. JavaScript (like most modern languages) uses lexical scope β a function sees the variables of its definition site. This is why closures work.
Variable Shadowing
A variable in an inner scope with the same name as an outer one shadows the outer variable within that scope.
const x = 10; // Outer x
function example() {
const x = 20; // Shadows outer x inside this function
console.log(x); // 20 β inner x
}
example();
console.log(x); // 10 β outer x unchanged
// Block-level shadowing
let count = 0;
if (true) {
let count = 100; // Shadows outer count in this block
console.log(count); // 100
}
console.log(count); // 0 β outer count unchanged
Scope in Loops β The Classic var Bug
// Bug: var creates one shared variable for all iterations
const funcsVar = [];
for (var i = 0; i < 3; i++) {
funcsVar.push(() => i);
}
console.log(funcsVar[0]()); // 3 β not 0!
console.log(funcsVar[1]()); // 3
console.log(funcsVar[2]()); // 3
// Fix: let creates a new binding per iteration
const funcsLet = [];
for (let j = 0; j < 3; j++) {
funcsLet.push(() => j);
}
console.log(funcsLet[0]()); // 0
console.log(funcsLet[1]()); // 1
console.log(funcsLet[2]()); // 2
IIFE for Scope Isolation
// Before ES modules, IIFEs isolated library code
const MyLibrary = (function() {
// Private variables β not accessible outside
let privateCounter = 0;
const privateConfig = { version: "1.0" };
function increment() {
privateCounter++;
}
// Public API
return {
getCount: () => privateCounter,
bump: () => { increment(); return MyLibrary; },
version: privateConfig.version
};
})();
MyLibrary.bump().bump().bump();
console.log(MyLibrary.getCount()); // 3
console.log(MyLibrary.version); // 1.0
Scope, Hoisting, and Closures
Scope decides where a variable is visible. The key modern shift: var is function-scoped, while let/const are block-scoped (limited to the nearest { }). This one difference fixes a whole category of loop bugs.
// var leaks out of the block
for (var i = 0; i < 3; i++) {}
console.log(i); // 3 β var escaped the loop
// let stays inside
for (let j = 0; j < 3; j++) {}
console.log(j); // ReferenceError β block-scoped
Closures: functions remember their birthplace
A closure is a function that keeps access to variables from the scope where it was defined, even after that scope has returned:
function counter() {
let count = 0; // "private" β survives via closure
return () => ++count;
}
const c = counter();
c(); c(); // 1, 2 β count persists between calls
Hoisting: var and function declarations are moved to the top of their scope (var initialized to undefined); let/const are hoisted but stay in the "temporal dead zone" until declared, so using them early throws. The classic loop-closure bug (all callbacks logging the final i) simply disappears when you use let.
ποΈ Practical Exercise
Without running the code, predict the output of each console.log and explain why:
var a = 1;
let b = 2;
function test() {
var a = 10;
if (true) {
var a = 20; // var β function scoped
let b = 30; // let β block scoped
console.log(a); // A
console.log(b); // B
}
console.log(a); // C
console.log(b); // D
}
test();
console.log(a); // E
console.log(b); // F
π₯ Challenge Exercise
Build a createNamespace(name) function using an IIFE that returns an object with set(key, value), get(key), and list() methods. The internal storage must be private (not accessible from outside). Create two separate namespaces and show they don't interfere with each other.
π Summary
- Global scope: accessible everywhere β minimize globals to avoid collisions.
- Function scope:
varand function parameters β only visible inside the function. - Block scope:
let/constβ restricted to the{}block they're declared in. - Scope chain: inner scopes can read outer variables; outer cannot read inner.
- Lexical scope: where a function is defined β not called β determines its variable access.
- Variable shadowing: same name in inner scope hides the outer one.
- Always prefer
let/constovervarto avoid scope leaks.
Interview Questions
- What is the difference between function scope and block scope?
- Why does
varcause problems in for loops with closures? How doesletfix it? - Explain the scope chain with an example of nested functions.
- What is lexical scope? How does it differ from dynamic scope?
- What is variable shadowing and when can it cause bugs?
Related Topics
FAQ
The TDZ is the period from when a let or const binding is created (hoisted) to when it is initialized. Accessing the variable during TDZ throws a ReferenceError. This is why let/const can't be used before their declaration line, unlike var (which initializes to undefined).
JavaScript modules (<script type="module"> or ESM files) have their own scope. Variables declared at the top of a module are module-scoped β they don't become global even without let/const/var. This eliminates the global pollution problem.
Yes, completely fine. Each function has its own scope. function a() { const x = 1; } and function b() { const x = 2; } have independent x variables that never conflict.

