Scope and Hoisting in JavaScript
Scope determines where variables are accessible. Hoisting is how JavaScript moves declarations to the top. Understanding both is crucial.
What is Scope?
Scope is the region where a variable is accessible. JavaScript has three types of scope:
- Global Scope - accessible everywhere
- Function Scope - accessible within a function
- Block Scope - accessible within a block (if, for, while, etc.)
Global Scope
Variables declared outside functions are global:
const globalVar = "I'm global";
function test() {
console.log(globalVar); // "I'm global"
}
test();
console.log(globalVar); // "I'm global"
Global Object
In browsers, global variables become properties of window:
var x = 5;
console.log(window.x); // 5
let y = 10;
console.log(window.y); // undefined (let doesn't attach to window)
In Node.js, use global:
var x = 5;
console.log(global.x); // 5
Function Scope
Variables declared inside a function are local to that function:
function myFunction() {
const localVar = "I'm local";
console.log(localVar); // "I'm local"
}
myFunction();
console.log(localVar); // ReferenceError: localVar is not defined
Function Scope with var
var is function-scoped:
function example() {
if (true) {
var x = 10;
}
console.log(x); // 10 (accessible outside if block)
}
example();
Block Scope
let and const are block-scoped:
function example() {
if (true) {
let x = 10;
const y = 20;
}
console.log(x); // ReferenceError
console.log(y); // ReferenceError
}
example();
Block Scope Examples
// if block
if (true) {
let x = 1;
}
console.log(x); // ReferenceError
// for loop
for (let i = 0; i < 3; i++) {
// i is scoped to the loop
}
console.log(i); // ReferenceError
// while loop
while (true) {
let x = 1;
break;
}
console.log(x); // ReferenceError
// try-catch
try {
throw new Error("test");
} catch (e) {
let error = e;
}
console.log(error); // ReferenceError
Scope Chain
Inner scopes can access outer scopes:
const global = "global";
function outer() {
const outerVar = "outer";
function inner() {
const innerVar = "inner";
console.log(innerVar); // "inner"
console.log(outerVar); // "outer"
console.log(global); // "global"
}
inner();
}
outer();
Scope Chain Lookup
JavaScript looks for variables from inner to outer scope:
const x = "global";
function test() {
const x = "function";
if (true) {
const x = "block";
console.log(x); // "block" (closest scope)
}
console.log(x); // "function"
}
test();
console.log(x); // "global"
Hoisting
Hoisting moves declarations to the top of their scope before execution.
var Hoisting
var declarations are hoisted and initialized with undefined:
console.log(x); // undefined (not an error!)
var x = 5;
console.log(x); // 5
This is equivalent to:
var x;
console.log(x); // undefined
x = 5;
console.log(x); // 5
Function Hoisting
Function declarations are fully hoisted:
console.log(greet("Alice")); // "Hello, Alice!"
function greet(name) {
return `Hello, ${name}!`;
}
This works because the entire function is hoisted.
let and const Hoisting
let and const are hoisted but not initialized (Temporal Dead Zone):
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 5;
console.log(y); // ReferenceError: Cannot access 'y' before initialization
const y = 10;
Function Expression Hoisting
Function expressions are NOT hoisted:
console.log(greet("Alice")); // TypeError: greet is not a function
const greet = function(name) {
return `Hello, ${name}!`;
};
Temporal Dead Zone (TDZ)
The period between entering a scope and reaching the declaration:
function example() {
console.log(x); // ReferenceError (in TDZ)
let x = 5; // TDZ ends here
console.log(x); // 5
}
example();
Practical Examples
Avoiding Global Pollution
// Bad - pollutes global scope
var globalCounter = 0;
function increment() {
globalCounter++;
}
// Good - encapsulated in function
function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
Module Pattern
const calculator = (function() {
let result = 0; // Private variable
return {
add(x) {
result += x;
return this;
},
subtract(x) {
result -= x;
return this;
},
getResult() {
return result;
}
};
})();
console.log(calculator.add(5).subtract(2).getResult()); // 3
console.log(calculator.result); // undefined (private)
Loop Variable Scope
// Bad - var leaks out
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 3, 3, 3
// Good - let is block-scoped
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 0, 1, 2
Closure with Block Scope
function createFunctions() {
const functions = [];
for (let i = 0; i < 3; i++) {
functions.push(() => i);
}
return functions;
}
const fns = createFunctions();
console.log(fns[0]()); // 0
console.log(fns[1]()); // 1
console.log(fns[2]()); // 2
Best Practices
Use const by Default
// Good
const x = 5;
let y = 10;
// Avoid
var z = 15;
Minimize Global Variables
// Bad
var globalConfig = { ... };
// Good
const config = (function() {
return { ... };
})();
Understand Hoisting
// Avoid relying on hoisting
function test() {
console.log(x); // Don't do this
var x = 5;
}
// Better - declare at top
function test() {
var x;
console.log(x); // undefined
x = 5;
}
Use Block Scope
// Good - block scope
if (condition) {
let x = 5;
}
// Avoid - function scope
if (condition) {
var x = 5;
}
Summary
- Global Scope: accessible everywhere
- Function Scope: accessible within function (var)
- Block Scope: accessible within block (let, const)
- Scope Chain: inner scopes access outer scopes
- Hoisting: declarations moved to top
- TDZ: period before let/const initialization
- Best practice: use const/let, avoid var
Related Resources
Official Documentation
Next Steps
- Closures: Understanding Function Scope
- The ’this’ Keyword and Context Binding
- Functions: Definition, Parameters, Return Values
Comments