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
ποΈ 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?
Frequently Asked Questions
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.