Understanding Block-Scoped Declarations: let vs const in JavaScript
Unlike var, identifiers declared with let are not initialized during the compilation phase. Accessing them before the declaration line triggers a ReferenceError. This behavior eliminates traditional hoisting and introduces the Temporal Dead Zone (TDZ).
// Traditional var hoisting
console.log(alpha); // undefined
var alpha = 10;
// let TDZ behavior
console.log(beta); // ReferenceError: Cannot access 'beta' before initialization
let beta = 20;
let enforces strict block-level scoping. Variables declared inside {} remain isolated from outer scopes, preventing accidental overwrites and loop counter leakage.
// Scope isolation example
function processItems() {
let counter = 5;
if (true) {
let counter = 15; // Shadows outer counter
console.log(counter); // 15
}
console.log(counter); // 5
}
Loop variables declared with let are recreated per iteration and do not pollute the enclosing scope:
for (let idx = 0; idx < 3; idx++) {
// iteration logic
}
console.log(typeof idx); // "undefined"
Redeclaring an identifier within the same lexical environment is strictly prohibited.
function setup() {
let config = true;
// let config = false; // SyntaxError: Identifier 'config' has already been declared
// var config = false; // SyntaxError
}
The const keyword creates a read-only reference to a value. Like let, its block-scoped, non-hoisted, subject to the TDZ, and forbids redeclaration. The critical distinction is that a const binding must be initialized at declaration and cannot be reassigned.
const MAX_RETRIES = 3;
// MAX_RETRIES = 5; // TypeError: Assignment to constant variable.
When const holds a reference type (objects, arrays), the binding itself is immutable, but the underlying data structure remains mutable. Reassignment fails, but property mutation succeeds.
const settings = { theme: 'dark' };
settings.theme = 'light'; // Valid
settings.version = 2; // Valid
// settings = {}; // TypeError: Invalid reassignment
Arrays behave identically:
const queue = [];
queue.push('task1', 'task2'); // Valid mutation
console.log(queue.length); // 2
// queue = ['new']; // TypeError
To prevent modification of the underlying data structure, apply Object.freeze(). This seals the object, blocking additions, deletions, and value changes.
const frozenConfig = Object.freeze({ mode: 'production' });
frozenConfig.mode = 'development'; // Silently fails (or throws in strict mode)
console.log(frozenConfig.mode); // 'production'
Modern JavaScript provides six declaration mechanisms: var, function, let, const, import, and class. When choosing between let and const, consider thier shared constraints and distinct behaviors:
- Shared: Block-scoped, TDZ-enforced, no hoisting initialization, no same-scope redeclaration.
- Distinct:
letallows reassignment and deferred initialization.constrequires immediate initialization and prohibits reassignment. For reference types,constonly locks the memory address, not the contents.