Scope Types
Scope is the region of code where a variable can be accessed. JavaScript has three scope levels.
// 1. GLOBAL SCOPE - accessible everywhere
const globalVar = "I am global";
var globalVar2 = "Also global"; // var in global scope -> window.globalVar2
// 2. FUNCTION SCOPE - only inside the function
function outerFn() {
const funcVar = "I am function-scoped";
console.log(globalVar); // OK - outer scope visible
console.log(funcVar); // OK - same scope
}
// console.log(funcVar); // ReferenceError
// 3. BLOCK SCOPE - let/const inside { } blocks
{
let blockLet = "only in this block";
const blockConst = "also only here";
var blockVar = "leaks out! (var is function-scoped)";
}
// console.log(blockLet); // ReferenceError
// console.log(blockConst); // ReferenceError
console.log(blockVar); // "leaks out!" (var escapes blocks)
// Block scope in conditionals and loops
if (true) {
let x = 10;
const y = 20;
}
// x and y are gone here
for (let i = 0; i < 3; i++) {
// i exists only inside this loop with let
}
Scope Chain
When JavaScript looks up a variable, it starts in the current scope and walks outward through each containing scope until it finds the variable or reaches global scope.
const level1 = "global";
function outer() {
const level2 = "outer function";
function inner() {
const level3 = "inner function";
// Scope chain lookup: inner -> outer -> global
console.log(level3); // found in inner (own scope)
console.log(level2); // found in outer (parent scope)
console.log(level1); // found in global (grandparent scope)
}
inner();
// console.log(level3); // ReferenceError - cannot look DOWN
}
outer();
// Shadowing: inner scope variable hides outer one
const color = "blue";
function getColor() {
const color = "red"; // shadows global 'color'
return color; // "red" (inner scope wins)
}
console.log(getColor()); // "red"
console.log(color); // "blue" (global unchanged)
Lexical Scope
JavaScript is lexically scoped: a function's scope is determined by where it is written in the code, not where it is called from.
const env = "production";
function checkEnv() {
// This function sees 'env' from where it was DEFINED (global)
// not from where it is called
console.log(env);
}
function runInTest() {
const env = "test"; // local variable, not visible to checkEnv
checkEnv(); // still logs "production"!
}
runInTest(); // "production" - lexical scope, not call-site scope
// This is why callbacks behave predictably
const greetings = ["Hello", "Hi", "Hey"];
const name = "Alice";
greetings.forEach(function(greeting) {
// 'name' is resolved lexically - where forEach callback was WRITTEN
console.log(`${greeting}, ${name}`);
});
// Hello, Alice / Hi, Alice / Hey, Alice
Closures
A closure is a function that retains access to its outer scope's variables even after the outer function has returned. It "closes over" the variables it references.
function makeCounter() {
let count = 0; // this variable is "closed over"
return function() {
count++;
return count;
};
}
const counter = makeCounter(); // makeCounter() has returned
console.log(counter()); // 1 - but 'count' still lives!
console.log(counter()); // 2
console.log(counter()); // 3
// Each call creates a separate closure (independent count)
const counterA = makeCounter();
const counterB = makeCounter();
console.log(counterA()); // 1
console.log(counterA()); // 2
console.log(counterB()); // 1 - separate 'count' variable
// How closures work:
// 1. makeCounter() creates a local 'count'
// 2. The returned function references 'count' from outer scope
// 3. JavaScript keeps 'count' alive because the returned function holds a reference
// 4. 'count' is NOT accessible from outside - true encapsulation
Closure Use Cases
// 1. Data privacy / encapsulation
function createBankAccount(initialBalance) {
let balance = initialBalance; // private
return {
deposit(amount) { balance += amount; },
withdraw(amount) { balance = Math.max(0, balance - amount); },
getBalance() { return balance; }
};
}
const account = createBankAccount(1000);
account.deposit(500);
account.withdraw(200);
console.log(account.getBalance()); // 1300
// balance is not accessible from outside
// 2. Factory functions with captured configuration
function createLogger(prefix) {
return function(msg) {
console.log(`[${prefix}] ${msg}`);
};
}
const infoLog = createLogger("INFO");
const errorLog = createLogger("ERROR");
infoLog("Server started"); // [INFO] Server started
errorLog("Connection failed"); // [ERROR] Connection failed
// 3. Memoization (caching expensive results)
function memoize(fn) {
const cache = {};
return function(n) {
if (n in cache) return cache[n];
cache[n] = fn(n);
return cache[n];
};
}
const fib = memoize(function(n) {
return n <= 1 ? n : fib(n - 1) + fib(n - 2);
});
console.log(fib(40)); // fast (cached intermediate results)
// 4. Loop closure gotcha - with var vs let
// PROBLEM with var (all share same 'i')
const fns = [];
for (var i = 0; i < 3; i++) {
fns.push(function() { return i; });
}
console.log(fns[0]()); // 3 (not 0!)
console.log(fns[1]()); // 3
console.log(fns[2]()); // 3
// FIXED with let (new binding per iteration)
const fns2 = [];
for (let j = 0; j < 3; j++) {
fns2.push(function() { return j; });
}
console.log(fns2[0]()); // 0
console.log(fns2[1]()); // 1
console.log(fns2[2]()); // 2
The classic closure bug: using var in a loop creates one shared variable. All callbacks close over the same reference and see the final value after the loop. Fix: use let (creates a new binding per iteration) or an IIFE around the callback body. This is one of the most common reasons to prefer let over var.
IIFE - Immediately Invoked Function Expression
An IIFE is a function that is defined and called in a single expression. It creates a private scope that does not leak variables into the surrounding context.
// Basic IIFE syntax
(function() {
const secret = "only inside here";
console.log("IIFE ran!");
})();
// console.log(secret); // ReferenceError
// Arrow IIFE
(() => {
console.log("Arrow IIFE");
})();
// IIFE with return value
const result = (function() {
const x = 10;
const y = 20;
return x + y;
})();
console.log(result); // 30
// IIFE with parameters
(function(name) {
console.log(`Hello, ${name}!`);
})("Alice"); // "Hello, Alice!"
// Real-world use: avoid polluting global scope in scripts
(function() {
// All code here is isolated
const config = { ... };
const init = () => { ... };
init();
})();
Module Scope
In ES modules (type="module"), every file has its own module scope.
Top-level variables are NOT global - they are local to the module.
// utils.js - module scope
const DB_URL = "mongodb://localhost"; // not global - private to this file
export const API_KEY = "abc123"; // explicitly exported
// app.js - another module
import { API_KEY } from "./utils.js";
// console.log(DB_URL); // ReferenceError - not exported!
// Module scope benefits:
// - No accidental global variable collisions
// - Explicit dependency declaration via imports
// - Each module is strict mode by default
// - IIFEs are not needed in module code