let, var, const β Variable Declaration Differences
After completing this topic, you will:
Be able to explain the differences between var, let, and const in terms of scope and reassignment, and be able to determine which one to use in different situations.
Three Declaration Keywords
In JavaScript, there are three ways to declare variables:
var name = "μ² μ"; // ES5 (legacy)
let age = 25; // ES6 (2015~)
const PI = 3.14159; // ES6 (2015~)All three accomplish the same thing: "creating a named container for a value." The differences lie in scope (how far it's visible) and reassignment (whether the value can be changed).
Reassignment
let count = 0;
count = 1; // β
`let` allows reassignment
const MAX = 100;
MAX = 200; // β TypeError: Assignment to constant variableconst prevents reassignment after declaration. You cannot change the value once it's assigned. let and var allow reassignment.
Note: const means the "variable binding is immutable," not that the "value is immutable." The internal state of objects or arrays can still be modified:
const user = { name: "μ² μ" };
user.name = "μν¬"; // β
Modifying the object's internal state is allowed
user = {}; // β Changing the variable itself to a different value is not allowedScope β var vs. let/const
var has function scope. It ignores the blocks ({}) of if statements or for loops:
function example() {
if (true) {
var x = 10;
}
console.log(x); // 10 β accessible outside the `if` block!
}let/const have block scope. They only exist within the block ({}) where they are declared:
function example() {
if (true) {
let y = 10;
}
console.log(y); // ReferenceError: y is not defined
}Block scope is more intuitive. It only lives within the curly braces where it's declared and disappears outside of it.
Differences in for Loops
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 3, 3, 3 β all refer to the same `i`Because var ignores the for loop block, by the time the setTimeout functions execute, i has already reached 3.
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 0, 1, 2 β each iteration has its own `i`let creates a new block scope for each iteration, so each i exists independently. This is the most significant practical difference between let and var.
Hoisting
Hoisting is the behavior where declarations are conceptually "moved" to the top of their scope.
console.log(a); // undefined (not an error!)
var a = 5;
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 5;var declarations are hoisted and initialized to undefined. Therefore, accessing them before declaration doesn't cause an error β which can make debugging harder.
let and const are also hoisted, but accessing them before initialization results in an error. This period is called the TDZ (Temporal Dead Zone). Raising an error is more helpful for debugging.
Practical Guidelines
// 1. `const` by default
const API_URL = "https://api.example.com";
const users = [];
// 2. Use `let` only when the value needs to change
let count = 0;
count++;
// 3. Don't use `var`
// (you might see it in legacy code)| Keyword | Scope | Reassignment | Hoisting | When to Use |
|---|---|---|---|---|
| var | Function | β | Initialized to undefined | Don't use |
| let | Block | β | TDZ (error) | When the value changes |
| const | Block | β | TDZ (error) | Default |
In modern JavaScript, use const by default, and switch to let only when reassignment is necessary. var exists for historical reasons, but there's no reason to use it in new code.
A Common Bug with Closures and var
The most famous bug caused by var's function scope:
// We want 5 buttons, each displaying a different number
for (var i = 0; i < 5; i++) {
document.getElementById("btn" + i).onclick = function() {
alert(i); // All alert 5
};
}All buttons display 5. By the time the buttons are clicked, i has already reached 5, and all the callbacks refer to the same i.
// Fix it with `let`
for (let i = 0; i < 5; i++) {
document.getElementById("btn" + i).onclick = function() {
alert(i); // 0, 1, 2, 3, 4
};
}let creates a new scope for each iteration, so each callback has its own i. Before ES6, you would use an IIFE (Immediately Invoked Function Expression) to work around this, but let makes that pattern unnecessary.
var/let/const in Function Declarations
There are also differences when creating functions:
// Function declaration β hoisted
greet(); // β
works
function greet() {
console.log("Hello");
}
// `const` function expression β not hoisted
hello(); // β ReferenceError
const hello = () => {
console.log("Hello");
};Function declarations can be called from anywhere in the code, but a function declared with const can only be called after its declaration. It's best to be consistent with one style in a project.
Differences in Global Scope
// In a browser environment
var globalVar = "I'm var";
let globalLet = "I'm let";
console.log(window.globalVar); // "I'm var" β added to the `window` object
console.log(window.globalLet); // undefined β not added to `window`Variables declared with var in the global scope become properties of the window object. This can cause naming conflicts between libraries. let and const do not add variables declared in the global scope to the window object.
Common Mistakes and Solutions
// Mistake 1: Modifying the internal state of a `const` object is allowed
const config = { debug: true };
config.debug = false; // β
Allowed β modifying the object's internal state
config = {}; // β Not allowed β reassigning the variable
// Mistake 2: Using `const` in a `for` loop
for (const i = 0; i < 3; i++) { // β Error: Cannot reassign in `i++`
console.log(i);
}
// `for...of` allows `const` (new binding on each iteration)
for (const item of [1, 2, 3]) { // β
console.log(item);
}Object.freeze β Making Truly Immutable Objects
If you want to prevent even internal modification of a const object:
const config = Object.freeze({
apiUrl: "https://api.example.com",
maxRetries: 3
});
config.apiUrl = "changed"; // Silently ignored (error in strict mode)
console.log(config.apiUrl); // "https://api.example.com"Object.freeze() prevents adding, modifying, or deleting properties of the object. However, it's a shallow freeze, meaning that the internal state of nested objects can still be modified.
When you see const in code, you immediately know that the value won't change. This predictability becomes increasingly important as a project grows.