Variable Scope Categories
Scope defines the region where a variable is accessible. Prior to ES6, JavaScript primarily utilized two scope types:
- Global Scope: Variables declared outside any function are accessible anywhere in the code.
- Function Scope: Variables declared inside a function using
varare local to that function.
Consider the implications of variable hoisting within function scope:
var globalValue = 50;
function calculate() {
// Variable 'localValue' is hoisted to the top of the function scope
console.log(localValue); // undefined
var result = localValue * 2; // NaN because localValue is undefined
var localValue = 100;
console.log(result); // NaN
}
calculate();In the example above, variable declarations are hoisted, but initializations are not. Consequently, localValue exists when the function runs but holds the value undefined until the assignment line is reached.
Block Scope and ES6
ES6 introduced block scope using let and const. Variables declared this way exist only within the specific block {} where they are defined.
if (true) {
var accessibleOutside = "I am var";
let trappedInside = "I am let";
}
console.log(accessibleOutside); // "I am var"
console.log(trappedInside); // ReferenceError: trappedInside is not definedThe Scope Chain and Lexical Scope
When a variable is required, the JavaScript engine searches the current scope. If not found, it moves up to the parent scope, continuing until the global scope is reached. This hierarchy is the scope chain.
JavaScript relies on lexical scope (or static scope). This means the scope of a variable is determined by its position in the source code during authoring, not by where the function is invoked.
Closure Mechanics
A closure is the combination of a function and the lexical environment in which it was declared. It allows an inner function to access variables from its outer (enclosing) function even after the outer function has finished execution.
While theoretically all JavaScript functions are closures, the practical definition refers to scenarios where a function maintains access to its surrounding state persistently.
const context = "Global";
function outerWrapper() {
const context = "Local";
function innerFunction() {
return context;
}
return innerFunction;
}
const myClosure = outerWrapper();
console.log(myClosure()); // Output: "Local"Here, myClosure returns "Local" because innerFunction was defined within the scope where context was "Local". The lexical environment is preserved.
Classic Interview Scenario
A common demonstration of closure behavior involves loop variable capture.
var functions = [];
for (var k = 0; k < 3; k++) {
functions[k] = function() {
console.log(k);
};
}
functions[0](); // 3
functions[1](); // 3
// The variable 'k' is function-scoped and shared, ending at 3.To capture the specific value for each iteration, an IIFE (Immediately Invoked Function Expression) can create a new scope:
var functions = [];
for (var k = 0; k < 3; k++) {
(function(index) {
functions[index] = function() {
console.log(index);
};
})(k);
}
functions[0](); // 0
functions[1](); // 1Closures maintain references to outer variables, which keeps them in memory. While useful for state preservation, developers must be cautious of potential memory leaks if these references are not managed properly.