SECTION 1
Historical Context & Motivation
When Brendan Eich created JavaScript in just ten days in 1995, the language had only one way to declare a variable: the var keyword. At the time, JavaScript was meant to be a simple scripting language for adding small interactive features to web pages. Nobody expected it to power massive applications with thousands of lines of code. As projects grew larger, developers began running into strange bugs caused by the way var handled scope — the rules that determine where a variable can be accessed. The JavaScript community spent nearly twenty years looking for a better approach.
1995 • JavaScript Is Born
var keyword is the only way to declare variables, and it uses function scope.2005 • AJAX & Large-Scale JS
2009 • ES5 & Strict Mode
var for variable declarations.2015 • ES6 Introduces let and const
let and const, giving developers block-scoped variables and solving decades of frustration.2020+ • Modern Best Practice
const and using let only when reassignment is needed. var is considered legacy.The central question this lesson addresses is: Where exactly can a variable be seen and used in your code, and how do var, let, and const differ in answering that question?
SECTION 2
Core Principles & Definitions
Before diving into code, you need to understand a few foundational ideas. Scope is the region of your code where a particular variable is accessible. Think of it like the walls of a room: a variable declared inside a room can be used inside that room, but code outside the room cannot reach in and grab it. Different declaration keywords — var, let, and const — build rooms with different sizes.
Scope
Function Scope (var)
var is visible everywhere inside the function where it was created, regardless of any if, for, or while blocks around it.Block Scope (let / const)
let or const is visible only inside the nearest set of curly braces { } (the block) where it was declared.Block
{ }. This includes if statements, for loops, while loops, and even standalone { } pairs.Hoisting
var declarations to the top of their function before execution, initializing them as undefined. let and const are hoisted too, but remain in a "temporal dead zone" until their declaration line runs.KEY TAKEAWAY
let and const keep your variables whispered in the room where they belong, preventing accidental leaks.SECTION 3
Visual Explanation — Scope Boundaries
The diagram below shows a simple function that contains an if block. A variable is declared with var inside the if block, and another is declared with let inside the same block. Notice how the colored scope boundaries differ: var escapes the block and fills the entire function, while let stays contained within the curly braces.
if statement. Notice how var message (pink) leaks out to the function level, while let exclaim and const MAX_LEN (cyan) remain trapped inside the block.This visual makes the core difference concrete. When you use var, the variable's scope boundary is the nearest function (or the global scope if there is no function). When you use let or const, the scope boundary is the nearest block — any pair of curly braces. Blocks are smaller than functions, so block-scoped variables are more tightly controlled.
SECTION 4
How It Works — The Scope Lookup Chain
When the JavaScript engine encounters a variable name, it searches for that variable using a process called the scope chain. It starts in the innermost scope (the block or function where the code is currently executing) and works outward, level by level, until it either finds the variable or reaches the global scope. If the variable is not found anywhere, JavaScript throws a ReferenceError.
Hoisting: The Hidden First Step
Before your code runs, the JavaScript engine performs a preparation step called hoisting. It scans your code and moves all var declarations to the top of their enclosing function. The variable is initialized to undefined right away. This means you can accidentally use a var variable before its declaration line and get undefined instead of an error — a silent bug.
With let and const, the engine still hoists the declaration, but it places the variable in a temporal dead zone (TDZ) from the start of the block until the line where the declaration appears. If you try to access the variable during the TDZ, JavaScript throws a ReferenceError. This fail-fast behavior helps you catch mistakes immediately instead of debugging mysterious undefined values.
Scope Rules Summary
| Behavior | var | let | const |
|---|---|---|---|
| Scope | Function | Block | Block |
| Hoisted? | Yes, initialized to undefined | Yes, but in TDZ | Yes, but in TDZ |
| Can be reassigned? | Yes | Yes | No |
| Can be redeclared in same scope? | Yes (dangerous!) | No (error) | No (error) |
| Access before declaration | Returns undefined | Throws ReferenceError | Throws ReferenceError |
Comparison of var, let, and const behaviors
SECTION 5
The Classic Loop Bug — var vs let in for Loops
One of the most famous JavaScript gotchas involves using var inside a for loop. Because var is function-scoped, the loop counter variable is shared across every iteration. If you create functions inside the loop that reference the counter, all of them see the same variable — which has already reached its final value by the time those functions run. Replacing var with let fixes this because let creates a fresh copy of the counter for each iteration.
var, all three callbacks share the same i which ends at 3. Right: with let, each iteration gets its own i, producing the expected output 0, 1, 2.This loop bug has tripped up professional developers for years. It is one of the strongest practical reasons to avoid var by default. By using let in your loops, you get the behavior you naturally expect — each pass through the loop gets its own variable.
SECTION 6
Worked Example — Tracing Scope Step by Step
Let's trace through a small program and predict what each console.log will output. This exercise builds the habit of thinking about scope before you run your code.
Predicting Output: var, let, and const
- 1
Step 1 — Read the Code
Consider this function:function demo() { var a = 1; let b = 2; const c = 3; if (true) { var a = 10; // same a or new a? let b = 20; // same b or new b? // const c = 30; (would be new c in block) console.log(a, b, c); // Line A } console.log(a, b, c); // Line B }Our goal is to predict the output of Line A and Line B. - 2
Step 2 — Analyze the var Declaration
Inside theifblock,var a = 10does NOT create a new variable. Becausevaris function-scoped, this redeclares and reassigns the sameafrom the outer function. The original value of 1 is overwritten.ais now 10 in the entire function. - 3
Step 3 — Analyze the let Declaration
Inside theifblock,let b = 20creates a brand-newbthat only exists within the block. It "shadows" (temporarily hides) the outerbwhile inside the block. Once we leave the block, the outerb(still 2) becomes visible again.Inside block:b= 20. Outside block:b= 2. - 4
Step 4 — Predict Line A (inside the block)
At Line A, we are inside theifblock.ahas been reassigned to 10. The block-localbis 20.cwas never redeclared in the block, so it's still the outerc= 3.Line A prints: 10 20 3 - 5
Step 5 — Predict Line B (outside the block)
At Line B, we have left theifblock. The block-scopedb = 20is destroyed. Thevar achange persists because it affected the function-level variable.cis unchanged at 3.Line B prints: 10 2 3
Key Observation
a changed from 1 to 10 outside the block because var does not respect block boundaries. If the original programmer intended a to stay 1 after the if block, this is a bug. Using let would have prevented it.SECTION 7
var vs let vs const — When to Use Each
Now that you understand the mechanics, here is the practical decision framework that professional JavaScript developers use every day. The modern rule of thumb is simple: start with const, switch to let if you need to reassign, and never use var unless you're maintaining legacy code.
| Keyword | Strengths | Weaknesses | Use When |
|---|---|---|---|
const | Block-scoped; cannot be reassigned; signals intent ("this value won't change"); catches accidental reassignment at compile time | Cannot reassign the binding (but objects/arrays inside can still be mutated) | Default choice — most variables don't need to change |
let | Block-scoped; allows reassignment; great for loop counters and accumulators | None significant — it's the safer replacement for var | When you need to change the value later (counters, flags, totals) |
var | Supported in all JavaScript versions back to 1995 | Function-scoped (leaks out of blocks); hoisted with undefined; allows redeclaration; causes loop bugs | Only when maintaining old code that already uses it |
Comparison of JavaScript variable declaration keywords
KEY TAKEAWAY
const is a sealed container — you put the food in, close it, and it stays put. let is a container with a lid you can open — you can swap the food out, but only within its shelf (block). var is an open plate on the counter — anyone walking through the kitchen (the entire function) can bump into it or change what's on it. Use sealed containers whenever possible.SECTION 8
Connection to Closures and Modules
Understanding scope is the gateway to more advanced JavaScript concepts you'll encounter later. The most important one is closures — functions that "remember" the variables from the scope where they were created, even after that scope has finished executing. The classic loop bug from Section 5 is actually a closure problem: the callbacks close over the same var variable. With let, each closure captures its own copy.
| Concept in This Lesson | Advanced Extension | Why It Matters |
|---|---|---|
Block scope with let / const | Closures | Block scope ensures closures capture the correct value per iteration, preventing shared-variable bugs. |
| Scope boundaries | ES Modules (import/export) | Each module file creates its own scope, so variables are private unless explicitly exported. |
| Avoiding global leaks | IIFE & Module Patterns | Before ES6, developers wrapped code in Immediately Invoked Function Expressions to create scope boundaries. |
const for immutability intent | Object.freeze & Immutable Data | const prevents reassignment of the variable, but Object.freeze() prevents mutation of the object itself. |
How scope concepts connect to advanced JavaScript topics
Don't worry if closures and modules sound complex right now. The key insight is that mastering scope gives you a solid foundation. Every advanced pattern in JavaScript — from React state management to Node.js server code — relies on the same idea of variables being visible only within their designated boundaries.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
Show answer
if or for blocks. Block scope means a variable is accessible only inside the nearest pair of curly braces { } where it was declared. A useful analogy: function scope is like having a name badge that lets you into every room of a building, while block scope gives you a badge for only the specific room you're in.Question 2: BASIC CALCULATION
function test() {
let x = 5;
if (true) {
let x = 99;
console.log(x); // Line A
}
console.log(x); // Line B
}
test();Show answer
let x = 99 creates a new block-scoped variable inside the if block that shadows the outer x. Line B outputs 5 because once the if block ends, the inner x is destroyed and the original x = 5 becomes visible again. This demonstrates that let keeps variables confined to their block.Question 3: INTERMEDIATE
function getDiscount(price) {
if (price > 100) {
var discount = price * 0.1;
}
return price - discount;
}
What happens when getDiscount(50) is called?Show answer
getDiscount(50) is called, the if condition is false, so the assignment never runs. However, because var is hoisted, discount exists as undefined. The return becomes 50 - undefined which equals NaN — not an error, just a silent wrong answer. The fix: use let instead of var and initialize discount before the if: let discount = 0; so the function returns the full price when no discount applies.Question 4: APPLIED
const and let (no var). Decide which variables should be const and which should be let, and justify your choices.
function countCompleted(tasks) {
var count = 0;
var total = tasks.length;
for (var i = 0; i < total; i++) {
var task = tasks[i];
if (task.done) {
count = count + 1;
}
}
var message = count + " of " + total + " completed";
return message;
}Show answer
function countCompleted(tasks) {
let count = 0; // let: reassigned in the loop
const total = tasks.length; // const: never changes
for (let i = 0; i < total; i++) {
const task = tasks[i]; // const: new binding each iteration
if (task.done) {
count = count + 1;
}
}
const message = count + " of " + total + " completed";
return message;
}
count uses let because its value is updated on each completed task. total, task, and message use const because they are assigned once and never reassigned. The loop variable i uses let because it changes with each iteration. Note that task inside the loop can be const because each iteration creates a fresh block scope with a fresh binding.Question 5: CRITICAL THINKING
const makes things truly immutable, so if I write const colors = ['red', 'blue'], I cannot change that array at all." Are they correct? Write a short code example to support your answer, and explain the distinction between reassigning a variable and mutating a value.Show answer
const prevents reassignment — you cannot point the variable at a completely different value. However, it does NOT prevent mutation — you can still change the contents of an object or array. For example:
const colors = ['red', 'blue'];
colors.push('green'); // Allowed! Array is mutated.
colors[0] = 'yellow'; // Allowed! Contents changed.
colors = ['new']; // TypeError! Cannot reassign.
Think of const as a permanent label on a box. You can't peel the label off and stick it on a different box (reassignment), but you can open the box and rearrange what's inside (mutation). To make an object truly immutable, you would need Object.freeze().SUMMARY
Lesson Summary
Summary
JavaScript has three ways to declare variables: var (legacy, function-scoped), let (modern, block-scoped, reassignable), and const (modern, block-scoped, not reassignable). Scope determines where a variable is visible: function scope extends to the entire enclosing function, while block scope is limited to the nearest curly braces { }. Hoisting moves var declarations to the top of their function (initialized as undefined), while let and const remain in a temporal dead zone until their declaration line.
The modern best practice is to default to const, use let only when you need to reassign a value (like loop counters), and avoid var entirely in new code. This prevents classic bugs like loop variable leaking, accidental redeclaration, and mysterious undefined values from hoisting. Understanding scope is also the foundation for closures and modules, two powerful patterns you will explore next.