Learning Objectives

  • Understand the differences between var, let, and const
  • Master function scope vs block scope
  • Learn about lexical scope and the scope chain
  • Understand the temporal dead zone
  • Apply best practices for variable declarations

Understanding Variables in JavaScript

Variables are containers for storing data values. In JavaScript, you can declare variables using three keywords: var, let, and const. Each has different characteristics and use cases.

The Three Ways to Declare Variables

var - The Old Way

var was the original way to declare variables in JavaScript. It has some quirks that can lead to bugs:

var name = 'Alice';
var age = 30;

// var can be redeclared
var name = 'Bob'; // No error!

// var is function-scoped, not block-scoped
if (true) {
    var message = 'Hello';
}
console.log(message); // 'Hello' - accessible outside the block!

let - Block-Scoped Variables

let was introduced in ES6 (2015) and provides block-level scoping:

let name = 'Alice';
let age = 30;

// let cannot be redeclared in the same scope
// let name = 'Bob'; // SyntaxError!

// let is block-scoped
if (true) {
    let message = 'Hello';
    console.log(message); // 'Hello'
}
// console.log(message); // ReferenceError: message is not defined

// But let can be reassigned
age = 31; // This works

const - Constants

const declares constants that cannot be reassigned:

const PI = 3.14159;
const MAX_SIZE = 100;

// const cannot be reassigned
// PI = 3.14; // TypeError: Assignment to constant variable

// const must be initialized
// const VALUE; // SyntaxError: Missing initializer

// But const objects can be mutated
const person = { name: 'Alice' };
person.name = 'Bob'; // This works!
person.age = 30;     // This works!

// You just can't reassign the variable itself
// person = {}; // TypeError!

Function Scope vs Block Scope

Function Scope (var)

var is function-scoped, meaning it's accessible anywhere within the function where it's declared:

function example() {
    var x = 1;
    
    if (true) {
        var x = 2; // Same variable!
        console.log(x); // 2
    }
    
    console.log(x); // 2 - modified by the if block
}

example();

Block Scope (let and const)

let and const are block-scoped, meaning they're only accessible within the block where they're declared:

function example() {
    let x = 1;
    
    if (true) {
        let x = 2; // Different variable!
        console.log(x); // 2
    }
    
    console.log(x); // 1 - unchanged
}

example();

// Block scope works with any block
{
    let blockScoped = 'I exist only in this block';
    console.log(blockScoped); // Works
}
// console.log(blockScoped); // ReferenceError

// Useful in loops
for (let i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 100);
}
// Prints: 0, 1, 2

// Compare with var
for (var j = 0; j < 3; j++) {
    setTimeout(() => console.log(j), 100);
}
// Prints: 3, 3, 3 (all reference the same variable!)

Lexical Scope and the Scope Chain

JavaScript uses lexical scoping, meaning inner functions have access to variables in their outer scopes:

const globalVar = 'global';

function outer() {
    const outerVar = 'outer';
    
    function inner() {
        const innerVar = 'inner';
        
        // Can access all three variables
        console.log(globalVar); // 'global'
        console.log(outerVar);  // 'outer'
        console.log(innerVar);  // 'inner'
    }
    
    inner();
    // console.log(innerVar); // ReferenceError - can't access inner scope
}

outer();

The scope chain is the hierarchy of scopes that JavaScript searches when looking for a variable:

let a = 1;

function first() {
    let b = 2;
    
    function second() {
        let c = 3;
        
        function third() {
            let d = 4;
            console.log(a + b + c + d); // 10
            // Searches: third scope → second scope → first scope → global scope
        }
        
        third();
    }
    
    second();
}

first();

The Temporal Dead Zone (TDZ)

Variables declared with let and const are in a "temporal dead zone" from the start of the block until the declaration is reached:

// With var - hoisted and initialized to undefined
console.log(varVariable); // undefined
var varVariable = 'var';

// With let - hoisted but NOT initialized (TDZ)
// console.log(letVariable); // ReferenceError: Cannot access before initialization
let letVariable = 'let';

// With const - same as let
// console.log(constVariable); // ReferenceError
const constVariable = 'const';

// TDZ in action
{
    // TDZ starts here for 'x'
    // console.log(x); // ReferenceError
    
    let x = 10; // TDZ ends here
    console.log(x); // 10
}

Global Scope and Its Pitfalls

Variables declared outside any function or block are in the global scope:

// Global variables
var globalVar = 'I am global';
let globalLet = 'Also global';
const globalConst = 'Global too';

// In browsers, var creates a property on window
console.log(window.globalVar); // 'I am global'
console.log(window.globalLet); // undefined (let doesn't create window property)

// Avoid polluting global scope
// Bad
var count = 0;
function increment() {
    count++;
}

// Better - use modules or IIFE
(function() {
    let count = 0;
    function increment() {
        count++;
    }
})();

// Best - use ES6 modules
// export function increment() { ... }

Variable Shadowing

Inner scopes can declare variables with the same name as outer scopes, "shadowing" them:

let name = 'Global';

function outer() {
    let name = 'Outer';
    console.log(name); // 'Outer'
    
    function inner() {
        let name = 'Inner';
        console.log(name); // 'Inner'
    }
    
    inner();
    console.log(name); // 'Outer'
}

outer();
console.log(name); // 'Global'

// Be careful with shadowing - it can be confusing
function confusing() {
    let x = 1;
    
    if (true) {
        let x = 2; // Different x
        console.log(x); // 2
    }
    
    console.log(x); // 1 - might expect 2!
}

Common Pitfalls and How to Avoid Them

1. Accidental Global Variables

// Bad - creates global variable
function bad() {
    accidentalGlobal = 'oops'; // No var/let/const!
}
bad();
console.log(accidentalGlobal); // 'oops' - leaked to global!

// Good - always declare variables
function good() {
    let intentional = 'safe';
}

// Use strict mode to catch this
'use strict';
function strict() {
    // undeclared = 'error'; // ReferenceError in strict mode
}

2. Loop Variable Leakage with var

// Bad - var leaks outside loop
for (var i = 0; i < 5; i++) {
    // loop body
}
console.log(i); // 5 - still accessible!

// Good - let is block-scoped
for (let j = 0; j < 5; j++) {
    // loop body
}
// console.log(j); // ReferenceError

3. Closures with var in Loops

// Bad - all callbacks reference the same i
var funcs = [];
for (var i = 0; i < 3; i++) {
    funcs.push(function() {
        console.log(i);
    });
}
funcs[0](); // 3
funcs[1](); // 3
funcs[2](); // 3

// Good - let creates a new binding for each iteration
var funcs2 = [];
for (let i = 0; i < 3; i++) {
    funcs2.push(function() {
        console.log(i);
    });
}
funcs2[0](); // 0
funcs2[1](); // 1
funcs2[2](); // 2

4. Const Doesn't Mean Immutable

// const prevents reassignment, not mutation
const obj = { count: 0 };
obj.count = 1; // Works!
obj.newProp = 'new'; // Works!

const arr = [1, 2, 3];
arr.push(4); // Works!
arr[0] = 99; // Works!

// To make truly immutable, use Object.freeze()
const frozen = Object.freeze({ count: 0 });
frozen.count = 1; // Silently fails (throws in strict mode)
console.log(frozen.count); // 0

// For deep immutability, freeze recursively or use libraries
const deepFrozen = Object.freeze({
    nested: Object.freeze({ value: 1 })
});

Best Practices

1. Prefer const by Default

// Use const for values that won't be reassigned
const API_KEY = 'abc123';
const MAX_RETRIES = 3;
const user = { name: 'Alice', age: 30 };

// This makes your intent clear and prevents accidental reassignment

2. Use let When You Need to Reassign

// Use let for values that will change
let count = 0;
count++;

let message = 'Loading...';
message = 'Complete!';

3. Avoid var

// Don't use var in modern JavaScript
// var has confusing scoping rules and no benefits over let/const

4. Declare Variables at the Top of Their Scope

// Good - clear what variables exist in this scope
function process(data) {
    const result = [];
    let total = 0;
    let average = 0;
    
    // ... use variables
    
    return { result, total, average };
}

5. Use Descriptive Names

// Bad
let x = 10;
let y = 20;
let z = x + y;

// Good
let width = 10;
let height = 20;
let area = width + height;

6. Minimize Scope

// Bad - wider scope than needed
function process() {
    let temp = 0;
    
    // ... lots of code
    
    if (condition) {
        temp = calculate(); // Only used here
    }
}

// Good - narrower scope
function process() {
    // ... lots of code
    
    if (condition) {
        const temp = calculate(); // Scoped to where it's used
    }
}

Real-World Example: Counter Module

// Using closures and proper scoping
function createCounter(initialValue = 0) {
    let count = initialValue; // Private variable
    
    return {
        increment() {
            count++;
            return count;
        },
        
        decrement() {
            count--;
            return count;
        },
        
        getValue() {
            return count;
        },
        
        reset() {
            count = initialValue;
            return count;
        }
    };
}

const counter1 = createCounter(0);
const counter2 = createCounter(100);

console.log(counter1.increment()); // 1
console.log(counter1.increment()); // 2
console.log(counter2.increment()); // 101

// Each counter has its own private count variable
console.log(counter1.getValue()); // 2
console.log(counter2.getValue()); // 101

Key Takeaways

  • Use const by default, let when you need to reassign, avoid var
  • var is function-scoped, let and const are block-scoped
  • Lexical scope means inner functions can access outer variables
  • The scope chain is searched from inner to outer scopes
  • Temporal Dead Zone prevents accessing let/const before declaration
  • const prevents reassignment, not mutation of objects/arrays
  • Always declare variables to avoid accidental globals
  • Use block scope to limit variable visibility
  • Variable shadowing can be confusing - use carefully
  • Minimize scope and use descriptive names