JavaScript engines process source code in two distinct phases: compilation and execution. During the compilation phase, the engine scans for declarations and registers them in their respective lexical environments. This behavior, commonly known as hoisting, allows identifeirs to be referenced before their physical location in the codebase.
Variable Hoisting Behavior
When utilizing var, the declaration is moved to the top of the enclosing function or global scope, while the initialization remains at its original position. Accessing the identifier before the assignment line executes results in undefined rather than a reference error.
console.log(sessionState); // undefined
var sessionState = 'authenticated';
console.log(sessionState); // 'authenticated'
Modern let and const declarations are also hoisted but are placed in a Temporal Dead Zone (TDZ). Any attempt to read them before execution reaches their definition triggers a ReferenceError, enforcing stricter variable lifecycle management.
Function Declarations vs. Expressions
Function declarations are fully hoisted. Both the identifier and the complete function body are registered during compilation, making them callable anywhere within their scope.
processInventory(15); // Executes successfully
function processInventory(quantity) {
return quantity * 2.5;
}
Function expressions, however, adhere to standard variable hoisting rules. Only the container variable is hoisted, not the assigned function logic. Invoking them before assignment results in a runtime error.
// applyTax(100); // TypeError: applyTax is not a function
var applyTax = function(amount) {
return amount * 1.08;
};
Scope Boundaries: Function vs. Block
Scope dictates the accessibility and lifetime of identifiers. JavaScript historically relied exclusively on function scope, but the introduction of block scope provided granular control over variable visibility.
Function Scope Mechanics
Identifiers declared with var inside a function are strictly confined to that function's execution context. They remain inaccessible to outer scopes, effectively preventing namespace pollution.
function configurePanel() {
var panelWidth = 320;
console.log(panelWidth); // 320
}
configurePanel();
// console.log(panelWidth); // ReferenceError: panelWidth is not defined
Block Scope Implementation
Block scope restricts identifier access to the nearest enclosing curly braces {}. This boundary applies to control flow statements like if, for, switch, and explicit block statements. Leveraging let and const enforces this isolation.
let themeConfig = 'default';
{
let themeConfig = 'high-contrast';
console.log(themeConfig); // 'high-contrast'
}
console.log(themeConfig); // 'default'
Iterative structures and conditional branches benefit significantly from block scoping. It eliminates legacy closure pitfalls commonly encountered in asynchronous callbacks.
for (let step = 0; step < 3; step++) {
setTimeout(() => console.log(step), 50);
}
// Outputs: 0, 1, 2
Substituting var with let in loops guarantees that each iteration creates a fresh lexical binding. This prevents the classic anomaly where deferred calllbacks uniformly capture the final loop counter value.