Variables & Scoping
Understand the differences between var, let, and const, and how block scoping, hoisting, and the temporal dead zone work.
var, let, and const
JavaScript has three keywords for declaring variables, each with different scoping and reassignment rules. The 'var' keyword is function-scoped and was the original way to declare variables, but it has quirks that make it error-prone. The 'let' keyword was introduced in ES6 and provides block-scoped variable declarations, making it the preferred choice for variables that need to be reassigned. The 'const' keyword also provides block scoping but prevents reassignment of the binding, though objects and arrays declared with const can still have their contents modified.
// var is function-scoped
function example() {
var x = 10;
if (true) {
var x = 20; // Same variable!
console.log(x); // 20
}
console.log(x); // 20 - changed!
}
// let is block-scoped
function betterExample() {
let x = 10;
if (true) {
let x = 20; // Different variable
console.log(x); // 20
}
console.log(x); // 10 - unchanged
}
// const prevents reassignment
const PI = 3.14159;
// PI = 3; // TypeError!
// But objects/arrays can be mutated
const user = { name: "Alice" };
user.name = "Bob"; // This is fine
// user = {}; // TypeError!
Block Scope
Block scope means a variable is only accessible within the pair of curly braces where it is declared. This includes if statements, for loops, while loops, and standalone blocks. Block scoping with let and const eliminates many common bugs, especially in for loops where var would create a single shared variable across all iterations. Understanding block scope is essential for writing predictable, bug-free JavaScript code.
// Block scope with let
{
let message = "hello";
console.log(message); // "hello"
}
// console.log(message); // ReferenceError!
// Classic loop problem with var
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log("var:", i), 100);
}
// Prints: var: 3, var: 3, var: 3
// Fixed with let
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log("let:", j), 100);
}
// Prints: let: 0, let: 1, let: 2
// Block scope in switch
switch (action) {
case "greet": {
let greeting = "Hello";
console.log(greeting);
break;
}
case "farewell": {
let greeting = "Goodbye"; // No conflict
console.log(greeting);
break;
}
}
Hoisting
Hoisting is JavaScript's behavior of moving declarations to the top of their scope during the compilation phase. Variable declarations with 'var' are hoisted and initialized to undefined, which means you can reference them before their declaration without getting an error. Function declarations are fully hoisted, including their body, which is why you can call functions before they appear in the code. Understanding hoisting helps explain some of JavaScript's more surprising behaviors.
// var is hoisted and initialized to undefined
console.log(name); // undefined (not an error!)
var name = "Alice";
// Function declarations are fully hoisted
greet(); // "Hello!" - works fine
function greet() {
console.log("Hello!");
}
// Function expressions are NOT hoisted
// sayBye(); // TypeError: sayBye is not a function
var sayBye = function () {
console.log("Bye!");
};
// Arrow functions are NOT hoisted either
// add(1, 2); // ReferenceError
const add = (a, b) => a + b;
// Hoisting in practice
function processData() {
// All var declarations are hoisted here
console.log(result); // undefined
if (true) {
var result = "done";
}
console.log(result); // "done"
}
Temporal Dead Zone
The temporal dead zone (TDZ) is the period between entering a block scope and the point where a let or const variable is declared and initialized. During this zone, any attempt to access the variable throws a ReferenceError, unlike var which simply returns undefined. The TDZ exists to catch programming errors where variables are used before they are properly initialized. This behavior makes let and const safer than var because it surfaces bugs immediately rather than producing silent undefined values.
// Temporal Dead Zone example
{
// TDZ starts for 'value'
// console.log(value); // ReferenceError!
// typeof value; // ReferenceError! (even typeof)
let value = 42; // TDZ ends
console.log(value); // 42
}
// TDZ with const
function example() {
// console.log(config); // ReferenceError - TDZ
const config = { debug: true };
console.log(config.debug); // true
}
// TDZ in function parameters
function greet(name = defaultName) {
// const defaultName = "World"; // Would be in TDZ!
}
// Safe pattern: declare before use
function processItems(items) {
const count = items.length;
const results = [];
for (let i = 0; i < count; i++) {
const processed = items[i].toUpperCase();
results.push(processed);
}
return results;
}
console.log(processItems(["hello", "world"]));