Josué Hernández
In modern JavaScript, understanding scope and closures is essential for writing efficient and maintainable code. These concepts govern how variables are accessed and retained, enabling powerful patterns and practices.
Scope defines the visibility and lifetime of variables within your code. It determines which variables you can access and where. JavaScript has several types of scope, including global, local, function, and block scope, all of which serve specific purposes.
Variables in the global scope are declared outside any function or block. They are accessible from anywhere in your code. However, using global variables is discouraged because they can lead to conflicts or unexpected behavior.
Example:
const isStudent = true;
function checkStudent() {
console.log(isStudent); // true
}
checkStudent();
console.log(isStudent); // true
In this example, isStudent is a global variable, accessible both inside the checkStudent function and outside of it. However, global variables can lead to unexpected results if multiple parts of your code inadvertently modify them.
Local scope applies to variables declared inside a function or block. They are only accessible within that specific function or block.
Example:
function myFunction() {
const carName = "Volvo";
console.log(carName); // Volvo
}
myFunction();
// console.log(carName); // Error: carName is not defined
The variable carName exists only inside myFunction. Trying to access it outside the function results in an error because its scope is limited to the function.
Variables declared with var have function scope, meaning they are accessible throughout the entire function, even before their declaration (due to hoisting). Variables declared with let or const are block-scoped.
Example:
function example() {
if (true) {
var a = 10;
let b = 20;
const c = 30;
}
console.log(a); // 10
// console.log(b); // Error: b is not defined
// console.log(c); // Error: c is not defined
}
example();
Here, a is declared with var, so it is accessible throughout the function, including outside the if block. In contrast, b and c, declared with let and const, are confined to the if block.
let and const introduced block scope in ECMAScript 6. This limits the scope of a variable to the block (delimited by {}) in which it was declared.
Example:
{
let blockScoped = "I'm inside a block!";
console.log(blockScoped); // I'm inside a block!
}
// console.log(blockScoped); // Error: blockScoped is not defined
The variable blockScoped only exists within the {} block. Attempting to access it outside the block results in an error.
Hoisting is JavaScript's default behavior of moving declarations to the top of their containing scope during compilation. This means variables and functions are registered in memory before the code is executed, allowing you to reference them before they are declared.
Key Points:
Example 1: var and Hoisting
console.log(a); // undefined
var a = 10;
console.log(a); // 10
Here, the declaration of a is hoisted to the top of the scope, but the assignment a = 10 remains in place. Therefore, console.log(a) before initialization returns undefined.
Example 2: let and const
console.log(b); // Error: Cannot access 'b' before initialization
let b = 20;
console.log(c); // Error: Cannot access 'c' before initialization
const c = 30;
Although b and c are hoisted, they remain in the temporal dead zone until the execution reaches their declaration. Accessing them before this point results in a ReferenceError.
Example 3: Function Hoisting
sayHello(); // Hello, world!
function sayHello() {
console.log("Hello, world!");
}
Functions are hoisted with their complete definition, so they can be called before their declaration.
A closure is the combination of a function and the scope in which it was created. Closures allow functions to "remember" variables from their enclosing scope even after that scope has finished executing.
function outerFunction() {
const outerVariable = "I'm from the outer scope!";
function innerFunction() {
console.log(outerVariable);
}
return innerFunction;
}
const closureExample = outerFunction();
closureExample(); // Output: I'm from the outer scope!
The innerFunction retains access to the outerVariable from the outerFunction, even though outerFunction has completed execution. This is the essence of a closure.
function createCounter() {
let count = 0;
return function () {
count++;
console.log(count);
};
}
const counter = createCounter();
counter(); // 1
counter(); // 2
counter(); // 3
The count variable is preserved between function calls because of the closure created by the returned function. This allows you to maintain and manipulate state.
function buildCounter(start = 0) {
let count = start;
return {
increment: () => ++count,
decrement: () => --count,
reset: () => (count = start),
};
}
const counter = buildCounter(10);
console.log(counter.increment()); // 11
console.log(counter.decrement()); // 10
counter.reset();
console.log(counter.increment()); // 11
Here, the count variable is encapsulated within the buildCounter function, accessible only through the methods increment, decrement, and reset.
Closures are one of the most versatile tools in JavaScript, enabling developers to write expressive and efficient code. They form the backbone of many advanced patterns and functionalities in modern programming. Here's how closures can empower your code:
Closures allow you to retain and manage state across function calls without relying on global variables. This makes your code more predictable and encapsulated.
Example:
function userTracker() {
let loggedIn = false;
return {
logIn: () => {
loggedIn = true;
console.log("User logged in:", loggedIn);
},
logOut: () => {
loggedIn = false;
console.log("User logged out:", loggedIn);
},
status: () => {
console.log("User is logged in:", loggedIn);
}
};
}
const user = userTracker();
user.status(); // User is logged in: false
user.logIn(); // User logged in: true
user.status(); // User is logged in: true
user.logOut(); // User logged out: false
Here, the loggedIn variable is private to the userTracker function, but its state is maintained across calls through the closure.
In JavaScript, closures are often used to emulate private variables, as the language lacks built-in privacy controls.
Example:
function bankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit: (amount) => {
balance += amount;
console.log(`Deposited $${amount}. New balance: $${balance}`);
},
withdraw: (amount) => {
if (amount > balance) {
console.log("Insufficient funds!");
} else {
balance -= amount;
console.log(`Withdrew $${amount}. New balance: $${balance}`);
}
},
getBalance: () => {
console.log(`Current balance: $${balance}`);
}
};
}
const myAccount = bankAccount(100);
myAccount.deposit(50); // Deposited $50. New balance: $150
myAccount.withdraw(30); // Withdrew $30. New balance: $120
myAccount.getBalance(); // Current balance: $120
The balance variable is accessible only through the returned methods, ensuring it cannot be directly modified from outside the bankAccount function.
Closures are foundational for functional programming patterns like currying and partial application.
Example:
function multiplyBy(multiplier) {
return function (value) {
return value * multiplier;
};
}
const double = multiplyBy(2);
const triple = multiplyBy(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
The multiplyBy function generates specialized functions (double and triple) that retain their own multiplier values via closures, showcasing their utility in building reusable functional components.
Closures are fundamental to JavaScript's flexibility and power. Their ability to encapsulate data, manage state, and create reusable components makes them invaluable in modern web development.
Closures are more than a concept; they are a practical tool that empowers developers to write scalable, efficient, and elegant code. Understanding and mastering closures will significantly elevate your JavaScript expertise.