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