Let, Const, and Block Scope
One of the most significant improvements in ES6 is the introduction of let and const for variable declarations. In this lesson, we'll explore how they differ from var, understand block scope, and learn best practices for modern variable declarations.
var vs let vs const
ES5 only had var for variable declarations, which had confusing scoping rules. ES6 introduced let and const to solve these problems:
var (ES5 - Function Scope):
var name = "John";
name = "Jane"; // ✓ Can reassign
var name = "Bob"; // ✓ Can redeclare (problematic!)
let (ES6+ - Block Scope):
let age = 25;
age = 26; // ✓ Can reassign
let age = 27; // ✗ Cannot redeclare (error)
const (ES6+ - Block Scope):
const PI = 3.14159;
PI = 3.14; // ✗ Cannot reassign (error)
const PI = 3.14; // ✗ Cannot redeclare (error)
Key Difference: var is function-scoped, while let and const are block-scoped (limited to {curly braces}).
Block Scope vs Function Scope
Understanding scope is crucial for writing bug-free code:
Function Scope (var):
function example() {
if (true) {
var x = 10;
}
console.log(x); // 10 - accessible outside if block
}
Block Scope (let/const):
function example() {
if (true) {
let y = 20;
const z = 30;
}
console.log(y); // Error: y is not defined
console.log(z); // Error: z is not defined
}
Block scope prevents variables from leaking outside their intended context:
Loops with var (problematic):
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 3, 3, 3 (not what we expected!)
Loops with let (correct):
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 0, 1, 2 (creates new binding each iteration)
Tip: Always use let or const in loops. Each iteration creates a new block scope, preventing common closure bugs.
Hoisting Behavior Differences
Hoisting is JavaScript's behavior of moving declarations to the top of their scope:
var Hoisting (initialized with undefined):
console.log(name); // undefined (not an error!)
var name = "John";
console.log(name); // "John"
Equivalent to:
var name; // Hoisted and initialized with undefined
console.log(name);
name = "John";
console.log(name);
let/const Hoisting (Temporal Dead Zone):
console.log(age); // ReferenceError: Cannot access before initialization
let age = 25;
console.log(PI); // ReferenceError: Cannot access before initialization
const PI = 3.14;
Important: let and const are hoisted but not initialized, creating a "Temporal Dead Zone" from the start of the block until the declaration is reached.
Temporal Dead Zone (TDZ)
The TDZ is the period between entering scope and the variable declaration:
{
// TDZ starts here for 'name'
console.log(name); // ReferenceError
let name = "John"; // TDZ ends, name is initialized
console.log(name); // "John" - works fine
}
The TDZ prevents using variables before they're declared, catching potential bugs:
function example(x = y, y = 2) {
return [x, y];
}
example(); // ReferenceError: Cannot access 'y' before initialization
// y is in TDZ when used as default for x
When to Use let vs const
Modern JavaScript best practices for choosing between let and const:
Use const (default choice):
✓ For values that won't be reassigned
✓ For object/array references (even if contents change)
✓ For function expressions
✓ Makes code more predictable and easier to reason about
Use let (when necessary):
✓ For variables that will be reassigned
✓ For loop counters
✓ For accumulator variables
✓ When you need to reassign the entire reference
Practical examples:
const for objects (contents can change):
const user = { name: "John" };
user.name = "Jane"; // ✓ Works - modifying contents
user.age = 25; // ✓ Works - adding properties
user = {}; // ✗ Error - cannot reassign the reference
const for arrays (contents can change):
const numbers = [1, 2, 3];
numbers.push(4); // ✓ Works - modifying contents
numbers[0] = 10; // ✓ Works - changing elements
numbers = []; // ✗ Error - cannot reassign the reference
let for reassignment:
let count = 0;
count++; // ✓ Works - reassigning value
count = 100; // ✓ Works - reassigning value
Remember: const prevents reassignment of the variable itself, not mutation of its contents. Objects and arrays declared with const can still be modified.
Best Practices for Variable Declarations
✓ DO:
1. Use const by default
2. Use let only when you need to reassign
3. Never use var in modern JavaScript
4. Declare variables at the top of their scope
5. Use meaningful, descriptive names
✗ DON'T:
1. Don't use var (causes scope confusion)
2. Don't reassign const variables
3. Don't declare multiple variables on one line
4. Don't use variables before declaration
5. Don't use single-letter names (except i, j in loops)
Common Pitfalls and How to Avoid Them
Pitfall 1: var in loops creating closures
// Problem:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 3, 3, 3
// Solution: Use let
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 0, 1, 2
Pitfall 2: Confusing const with immutability
// const doesn't make objects immutable:
const config = { api: "https://api.example.com" };
config.api = "https://new-api.example.com"; // Works!
// To make immutable, use Object.freeze():
const config = Object.freeze({ api: "https://api.example.com" });
config.api = "https://new-api.example.com"; // Fails silently (strict mode: error)
Pitfall 3: Accidental global variables
function example() {
x = 10; // Accidentally creates global variable!
}
// Solution: Use let/const and strict mode
"use strict";
function example() {
let x = 10; // Properly scoped variable
}
Practice Exercise:
Fix this code by replacing var with let/const appropriately:
var PI = 3.14159;
var radius = 5;
var area = PI * radius * radius;
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log("Loop: " + i);
}, 100);
}
var user = { name: "John" };
user = { name: "Jane" };
Solution:
const PI = 3.14159; // Never changes
let radius = 5; // Might be reassigned
const area = PI * radius * radius; // Calculated value
for (let i = 0; i < 3; i++) { // Use let for loop counter
setTimeout(function() {
console.log("Loop: " + i);
}, 100);
}
let user = { name: "John" }; // Will be reassigned
user = { name: "Jane" }; // Reassignment happens
Real-World Scenarios
Scenario 1: Configuration objects
const config = {
apiUrl: "https://api.example.com",
timeout: 5000,
retries: 3
};
// Properties can be modified, but reference stays constant
Scenario 2: Event handlers
const button = document.querySelector("#myButton");
button.addEventListener("click", () => {
console.log("Button clicked!");
});
// const prevents accidental reassignment of button reference
Scenario 3: Loop counters
for (let i = 0; i < items.length; i++) {
const item = items[i]; // const inside loop
processItem(item);
}
// Each iteration gets fresh i and item bindings
Summary
In this lesson, you learned:
var is function-scoped and has confusing hoisting behavior
let and const are block-scoped (limited to {curly braces})
const prevents reassignment but allows mutation of contents
- Temporal Dead Zone prevents using variables before declaration
- Use
const by default, let when reassignment needed
- Never use
var in modern JavaScript
- Block scope prevents variable leaks and closure bugs
Next Up: In the next lesson, we'll explore arrow functions and how they simplify function syntax while solving the this binding problem!