The let Declaration
Basic Usage
The let keyword introduces block-scoped variables. Unlike var, variables declared with let are not accessible outside their containing block.
// Example with var
{
var globalVar = 'leaks';
}
console.log(globalVar); // Outputs: leaks
// Example with let
{
let blockScoped = 'does not leak';
}
console.log(blockScoped); // ReferenceError: blockScoped is not defined
Hoisting Differences
Variables declared with var are hoisted to the top of their scope and initialized with undefined. let declarations are also hoisted but not initialized, leading to a ReferenceError if accessed before declaration (Temporal Dead Zone).
// var hoisting
console.log(varExample); // Outputs: undefined
var varExample = 'hoisted';
// let hoisting (TDZ)
console.log(letExample); // ReferenceError: Cannot access 'letExample' before initialization
let letExample = 'not hoisted';
Temporal Dead Zone (TDZ)
The TDZ is the period from the start of a block until a let variable is declared. Accessing the variable in this zone results in an error.
{
// Entering TDZ for 'tdzVar'
// console.log(tdzVar); // Would throw ReferenceError
let tdzVar = 'safe';
// Exiting TDZ
console.log(tdzVar); // Outputs: safe
}
No Redeclarasion
let does not allow redeclaring a variable within the same scope, even with var.
let uniqueId = 10;
// let uniqueId = 20; // SyntaxError: Identifier 'uniqueId' has already been declared
var anotherId = 30;
// let anotherId = 40; // SyntaxError: Identifier 'anotherId' has already been declared
Block-Level Scope in ES6
Issues Without Block Scope
Prior to ES6, JavaScript had function and global scopes. This led to common issues like variable leakage and unexpected hoisting.
// Loop variable leakage
var collectedFuncs = [];
for (var i = 0; i < 3; i++) {
collectedFuncs.push(function() { return i; });
}
console.log(i); // Outputs: 3 (leaked!)
console.log(collectedFuncs[0]()); // Outputs: 3, not 0
// Hoisting in conditional
var testValue = 5;
function checkScope() {
if (false) {
var testValue = 10;
}
console.log(testValue); // Outputs: undefined (due to hoisting)
}
checkScope();
Implementing Block Scope with let
Using let in loops and conditionals creates a new binding for each iteration, preventing leakage.
function demonstrateBlockScope() {
let outerVar = 'visible outside';
if (true) {
let innerVar = 'only inside';
console.log(outerVar); // Outputs: visible outside
}
// console.log(innerVar); // ReferenceError: innerVar is not defined
console.log(outerVar); // Outputs: visible outside
}
demonstrateBlockScope();
The const Declaration
Basic Usage
const declares a read-only reference to a value. The varible identifier cannot be reassigned. It must be initialized upon declaration and is block-scoped.
// Correct usage
const MAX_SIZE = 100;
console.log(MAX_SIZE); // Outputs: 100
// Attempting reassignment
// MAX_SIZE = 200; // TypeError: Assignment to constant variable.
// Must be initialized
// const UNINITIALIZED_CONST; // SyntaxError: Missing initializer in const declaration
Underlying Principle
const prevents reassignment of the variable identifier, but does not make the assigned value immutable. For objects and arrays, their contents can be modified.
// With objects
const userProfile = { name: 'Alice' };
userProfile.name = 'Bob'; // Allowed
console.log(userProfile); // Outputs: { name: 'Bob' }
// userProfile = { name: 'Charlie' }; // TypeError: Assignment to constant variable.
// With arrays
const numberList = [1, 2, 3];
numberList.push(4); // Allowed
console.log(numberList); // Outputs: [1, 2, 3, 4]
numberList[0] = 99; // Allowed
console.log(numberList); // Outputs: [99, 2, 3, 4]
// numberList = [5, 6, 7]; // TypeError: Assignment to constant variable.
Deep Freezing
Object.freeze() can make an object immutable, but it only performs a shallow freeze. Nested objects remain muttable.
const shallowFrozen = Object.freeze({ key: 'value' });
// shallowFrozen.key = 'new value'; // Fails silently in non-strict mode, throws in strict mode