Skip to content
Plurafy

Variable Scope and Hoisting

Understanding variable scope and hoisting behavior

Understand why code behaves unexpectedly with var, let, and const. Predict execution order and scope.

JavaScript has function scope (var), block scope (let, const), and hoisting which affects when variables are accessible.

// Global scope
const globalVar = 'global';
function testScope() {
// Function scope
const functionVar = 'function';
if (true) {
// Block scope (let, const only)
const blockVar = 'block';
}
// blockVar not accessible here (if using let/const)
}
// globalVar accessible everywhere
// var: Function scoped, redeclarable
function testVar() {
if (true) {
var x = 1;
}
console.log(x); // 1 (accessible outside if block)
}
// let: Block scoped, not redeclarable
function testLet() {
if (true) {
let y = 1;
}
// console.log(y); // Error: y is not defined
}
// const: Block scoped, immutable reference
const obj = { name: 'John' };
// obj = {}; // Error: Assignment to constant variable
obj.name = 'Jane'; // OK: Mutating object is allowed
const arr = [1, 2, 3];
arr.push(4); // OK
// arr = []; // Error
// var hoisting
console.log(x); // undefined (not ReferenceError)
var x = 5;
console.log(x); // 5
// Equivalent to:
var x; // Declaration hoisted
console.log(x); // undefined
x = 5;
console.log(x); // 5
// Function hoisting
console.log(foo()); // 'Hello' (entire function hoisted)
function foo() {
return 'Hello';
}
// Function expression not hoisted
// console.log(bar()); // TypeError: bar is not a function
const bar = () => 'Hello';
console.log(bar()); // 'Hello'
// TDZ: let/const not accessible before declaration
// console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 5;
console.log(y); // 5
// Even if outer scope has same variable
const z = 'outer';
{
// console.log(z); // ReferenceError (in TDZ)
const z = 'inner';
console.log(z); // 'inner'
}
const x = 'outer';
function test() {
// Shadowing: inner variable hides outer
const x = 'inner';
console.log(x); // 'inner'
}
test();
console.log(x); // 'outer'
function createCounter() {
let count = 0;
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
// count is not directly accessible but persists
// This is a closure retaining access to count
// JavaScript modules have file scope
// Variables declared at top are private to module
const privateVar = 'private';
export const publicVar = 'public';
// Only publicVar is accessible when imported
// privateVar remains hidden
// 1. Prefer const by default
const value = 5; // Immutable reference
// 2. Use let for mutable values
let counter = 0;
counter++;
// 3. Avoid var (legacy)
// var legacy = 'avoid';
// 4. Minimize scope
if (condition) {
const tempVar = compute(); // Scoped to if block
console.log(tempVar);
}
// tempVar not accessible here
// 5. Avoid hoisting confusion
function process() {
// Declare at top to make intent clear
const result = calculate();
return result;
}