Advanced JavaScript (ES6+)

Let, Const, and Block Scope

13 min Lesson 2 of 40

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!

ES
Edrees Salih
7 hours ago

We are still cooking the magic in the way!