In this article, you will have a good understanding of JavaScript closures . You will walk through the fundamentals of closures, functionality, and their practical applications. By the end of this article, you will have a clear understanding of closures, along with the skills to implement, optimize, and apply them efficiently in JavaScript development.
JavaScript Closure
A closure is a function that retains access to the variables of its surrounding lexical scope, even after the outer function has completed its execution. This allows functions to maintain a connection to the environment in which they were created, enabling them to recall and utilize variables from that scope as needed.
Closures are a foundational concept in JavaScript and play a vital role in functional programming, event handling, and data encapsulation. By leveraging closures, developers can create more efficient, modular, and maintainable code.
Exploring Lexical Scope
JavaScript employs lexical scoping, which enables a function to access and manipulate variables that are declared within its outer scope. In other words, a function has access to the variables of its parent scope, allowing it to utilize and modify those variables as needed
Example of lexical scope:
function outer(){ let name = "Saswat"; function inner(){ console.log("inner", name); // can access outer variable } inner(); } outer(); // Output : "inner Saswat"
In above mentioned program, outer( ) created a local variable known as
name and function known as inner( ). The inner( ) function is an inner function which is defined inside outer( ) and is available only within the body of the outer( ) function. If you noted that the inner( ) function doesn’t have local variable of its own. Since inner( ) can access the variable name which is declared in the parent function, outer( ).
Step-by-Step explanation of creating a closure
The process of creating a closure involves the following steps:
- Step 1 Nested Function Definition: A function is defined within the another function, creating a nested structure.
function createMusicPlayer(){ function playSong(songName){ // Nested function console.log(`Playing song:${songName}`); } function stopSong(){ // Nested function console.log("Stopping song."); } playSong("Happy Birthday"); stopSong(); } createMusicPlayer(); // output: // Playing song: Happy Birthday // Stopping song.
In this example :
The createMusicPlayer function is the outer function. The playSong and stopSong functions are nested inside the createMusicPlayer function. The playSong function takes a songName parameter and logs a message to the console. The stopSong function logs a message to the console. - Step 2 Variable Access: The inner function accesses and utilizes variables from the outer function’s scope.
function create bank account(){ let account balance = 1000; let account holder = "Saswat Rout"; function deposit amount(amount){ console.log(`depositing $${amount} into ${account holder}'s account.`); account balance += amount; console.log(`new balance: $${account balance}`); } function withdraw amount(amount){ console.log(`withdrawing $${amount} from ${accountholder}'s account.`); if(account balance >= amount){ account balance -= amount; console.log(`new balance: $${account balance}`);} else{ console.log("insufficient funds."); } } deposit amount(500); withdraw amount(200); } create bank account(); // output: // depositing $500 into Saswat Rout's account. // new balance: $1500 // withdrawing $200 from Saswat Rout's account. // new balance: $1300
In this example:
The create bank account function is the outer function. The deposit amount and withdraw amount functions are nested inside the create bank account function. The deposit amount and withdraw amount functions access the account balance and account holder variables from the outer function’s scope. The inner functions will modify the account balance variable, demonstrating lexical scope. - Step 3 Outer Function Returns Inner Function: The outer function returns the inner function, effectively creating a closure.
function create personalized greeter(name){ let greeting message = `hello, ${name}!`; return function greeter(){ console.log(greeting message); }; } const greet Saswat = create personalized greeter("Saswat"); const greet Varun = create personalized greeter("Varun"); greet Saswat(); // output: "hello, Saswat!" greet Varun();// output: "hello, Varun!"
- Step 4 Persistent Memory: Even after the outer function has executed and its scope has been closed, the inner function “remembers” the variables of the outer function, retaining access to its surrounding environment.
function createBankAccount(initialBalance) { let balance = initialBalance; let transactionHistory = []; return function performTransaction(amount, type) { if (type === 'deposit') { balance += amount; transactionHistory.push(`Deposited $${amount}`); } else if (type === 'withdrawal') { if (balance >= amount) { balance -= amount; transactionHistory.push(`Withdrew $${amount}`); } else { transactionHistory.push('Insufficient funds'); } } console.log(`Current balance: $${balance}`); console.log('Transaction history:'); console.log(transactionHistory); }; } const myAccount = createBankAccount(1000); myAccount(500, 'deposit'); myAccount(200, 'withdrawal'); myAccount(300, 'withdrawal'); myAccount(100, 'withdrawal');
In this example:
The createBankAccount function is the outer function. The performTransaction function is the inner function, which is returned by the outer function. The inner function has access to the balance and transactionHistory variables from the outer function’s scope. Even after the outer function has finished execution, the inner function remembers the balance and transactionHistory variables. The inner function can update balance and transactionHistory variables, and these changes are persisted across multiple calls to the inner function.