Mastering Closures, IIFEs, and the Singleton Pattern: A JavaScript Case Study
JavaScript Essencial Fundamentals | The Series
In the realm of vanilla JavaScript, closures stand out as both crucial and challenging concepts for many developers. Alongside closures, Immediately Invoked Function Expressions (IIFEs), factory functions, and arrow functions play pivotal roles in efficient scripting. This case study delves into the Singleton Pattern, a notable design pattern recognized by the Gang of Four (GoF), to demystify these concepts through practical application and clear educational insights.
Singleton Pattern
DEFINITION The Singleton Pattern is a software design pattern that falls under the category of creational patterns. Its main objective is to ensure that a particular class has only a single instance and provide a global point of access to that instance throughout the system.
CHARACTERISTICS The key characteristics of the Singleton Pattern include:
Single Instance: It ensures that only one instance of the Singleton class is created during program execution.
Global Access: It provides a means to globally access this single instance, allowing other objects to obtain a reference to it.
Access Control: Typically, the Singleton Pattern restricts the creation of new instances of the Singleton class, often by defining a static method to obtain the existing instance.
USAGE This pattern is useful in situations where it is crucial to have exactly one instance of a class that coordinates actions across the entire system. A typical example is creating a configuration manager or a database connection pool, where having multiple instances could lead to consistency issues or excessive resource consumption.
IMPLEMENTATION The implementation of the Singleton Pattern varies from language to language but generally involves defining a private static variable to store the single instance of the class, as well as creating a public static method to access it.
JS IMPLEMENTATION In JavaScript, the Singleton Pattern is typically implemented using a combination of Closures, Anonymous Functions, Immediately Invoked Function Expressions (IIFE), arrow notation, and factory functions. A closure encapsulates private variables and methods, ensuring they are accessible only within the function scope. An IIFE creates an immediate and isolated execution context. Arrow notation allows for concise function definitions. Meanwhile, a factory function is used as a public method to create and return the Singleton instance, which is stored within a private variable. This approach guarantees a single instance of the object throughout the application.
CODE Here's an example of implementing the Singleton Pattern in JavaScript, incorporating all mentioned concepts:
const Singleton = (() => {
let instance;
let privateVar = 0;
function createInstance() {
return {
getPrivateVar: () => privateVar,
incrementPrivateVar: () => { privateVar++ },
}
}
return {
getInstance: () => {
if (!instance) {
instance = createInstance();
}
return instance;
},
};
})();
STUDY From this example, we will explore all the JavaScript concepts used for implementing this Singleton. Therefore, if you haven't fully grasped all the intricacies of this code, everything will become clearer as we progress through our study. This whole example will be detailed explained later.
CLASS NOTATION As a side note, it's important to know this is not the only way to implement a Singleton. There are several other variations, one of them using Class Notation (ES6+), which is often considered simpler by programmers coming from object-oriented languages like Java.
NEXT Before delving into the Singleton pattern, let's grasp the concept of closures.
Closures
DEFINITION Closure is a feature in some programming languages that occurs when a function is able to "remember" and access its lexical scope even when it is being executed outside of its lexical scope.
ETYMOLOGY It can be said that a closure "closes over" variables from its surrounding environment, encapsulating them.
CREATION In JavaScript, closure is formed at the moment a function is instantiated and retains the references to variables from its lexical scope that are used within the function.
ELEMENTS Okay, let's delve deeper into this concept. A closure comprises two key components:
The Function: This refers to a function that can access its lexical environment by reference.
Lexical Environment: The lexical environment is an internal data structure that holds all the variables, functions, and references available in a specific scope during the execution of the program.
STORAGE The variables of a closure are not stored directly within the closure itself but rather in the associated lexical environment (data structure) of that closure. The lexical environment includes not only the local variables of the function where it was created but also a reference to the outer lexical environment. This chain of references forms the "scope chain" that allows a function to access variables from its outer lexical environments.
REFERENCE When a closure is created, it retains a reference to this lexical environment, enabling it to access and utilize the variables and references from that specific context, even after this lexical environment that created the closure has completed its execution or even when the closure function is being executed outside of its lexical scope.
COUNTERINTUITIVE The concept of closures can seem counterintuitive, especially to those familiar with programming languages where functions do not first-class citizens or where lexical scoping and closures are not integral parts of the language's design. In such languages, the local variables of a function typically cease to exist once execution completes, and special mechanisms or keywords are required to achieve similar effects.
DYNAMISM However, JavaScript's treatment of functions as first-class objects, combined with its lexical scoping rules, allows closures to capture and maintain references to outer scope variables even after the outer function has finished executing. This unique feature enables more dynamic and flexible programming patterns, such as module creation, currying, and private data encapsulation, which might be more complex to implement or less intuitive in languages without native support for closures.
CODE Here's a didatical example of a closure in JavaScript:
const globalVar = 1
function outer()
{
console.log("2| >> globalVar:", globalVar )
const outerVar = 10;
function inner()
{
console.log("4| >> globalVar + outerVar:", outerVar + globalVar)
}
return inner
}
console.log("1| Creating closureFunc" )
const closureFunc = outer(); //Closure's creation
console.log("3| Calling closureFunc()" )
closureFunc();
// Creating closureFunc
// >> globalVar: 1
// Calling closureFunc()
// >> globalVar + outerVar: 11
EXPLANATION In the code above, inner
function is a closure that has access to outerVar
and to globalVar
. Outer
function is also a closure with access to globalVar
.
EXECUTION As we can see above, steps 2
(related to the execution of the outer
function) only occur at the time of closure creation, when the closureFunc
variable is assigned the inner
function after the execution of the outer()
function.
AVAILABILITY This means that always when we execute the closure function stored in the closureFunc
variable, not just globalVar
variable remains accessible but also outerVar
, even though it belongs to the outer
function that has not been reprocessed.
PRIVACY Now, We have found a way to keep the instance variable private and directally inaccessible using closure in our Singleton. So let's try to code our Singleton function:
// Define a Constructor Function createSingleton
// that creates a Singleton instance
function createSingleton() {
// Initialize a private variable
let privateVar = 0;
// Create an object with methods to access
// and increment the private variable
const instance = {
//indirect property access
getPrivateVar: () => privateVar, //this is a closure
//indirect property manipulation method
incrementPrivateVar: () => { privateVar++ }, //this is also a closure
};
// Return the Singleton instance
return instance;
}
// Create the Singleton instance by calling createSingleton
const Singleton1 = createSingleton();
// Example of usage
// Access and log the value of the privateVar for Singleton1
console.log(Singleton1.getPrivateVar()); // OUTPUT:0
// Increment the privateVar for Singleton1
Singleton1.incrementPrivateVar();
// Access and log the updated value of the privateVar for Singleton1
console.log(Singleton1.getPrivateVar()); // OUTPUT:1
NON-SINGLENESS However, the use of this only key concept in JS like the closure is not enouth to create our Singleton object. We can continue to add new variables to the code, which will result in new instances of the same closure.
// Create the second Singleton instance by calling createSingleton again
const Singleton2 = createSingleton();
// Increment the privateVar for Singleton2
Singleton2.incrementPrivateVar();
// Access and log the value of the privateVar for Singleton2
console.log(Singleton2.getPrivateVar()); //OUTPUT:1
// Check if Singleton2 is the same as Singleton1 and log the result
console.log(Singleton2 === Singleton1); // OUTPUT: false
NEXT In order to prevent potential instantiations in our Singleton code, we need to find a way to disable or replace the createSingleton()
Constructor Function. To achieve this, we will use another concept from JavaScript: IIFE (Immediately Invoked Function Expression).
IIFE (Immediately Invoked Function Expression)
DEFINITION An Immediately Invoked Function Expression (IIFE) is a function that is executed immediately after it is defined. Here's an example of how to create an IIFE in JavaScript:
//using anonymous function
(function() {
// function code here
})();
//or using a simpler anonymous function with arrow notation
(() => {
// function code here
})();
DETAILING As we can see above, the IIFE consists of three elements:
Surrounding (): The anonymous function is defined inside parentheses () to turn it into a function expression.
Executor (): The outer parentheses () around the entire function expression cause it to be executed immediately.
Code: Any code inside the function will be executed as soon as the script is loaded in the browser.
COMBINATION Now, let's study an example of a closure combined with an IIFE. To better understand the execution sequence within a closure, let's introduce some step-by-step console.log() statements into the code:
const outer = (() => {
console.log("1: Initiating the only outer function execution.");
const outerVar = 10;
function inner()
{
console.log('>> This is inner function execution.');
console.log(">> This is the closure's outerVar:", outerVar);
}
console.log(`2: Concluding outer function execution
before returning the inner function.`);
return inner;
})();
console.log("3:Before outer call.");
outer()
console.log(`--------------------------------------------
-------------------------------`)
const outer1 = outer;
console.log("4: After assigning to a outer1.");
console.log("5: Type of outer1:", typeof outer1)
console.log("6: Before outer1 call.");
outer1();
console.log("7: Is outer the same reference as outer1 ? ", outer1 === outer)
// 1: Initiating the only outer function execution.
// 2: Concluding outer function execution before returning the inner function.
// 3: Before outer call.
// >> This is inner function execution.
// >> This is the closure's outerVar: 10
// ---------------------------------------------------------------------------
// 4: After assigning to a outer1.
// 5: Type of outer1: function
// 6: Before outer1 call.
// >> This is inner function execution.
// >> This is the closure's outerVar: 10
// 7: Is outer the same reference as outer1 ? true
IMMEDIATE The provided code defines an IIFE assigned to the variable outer
, which encapsulates the inner function inner
and a local variable outerVar
creating a closure. Upon invocation, the IIFE logs a message indicating the initiation of the outer function execution (message #1), assigns the value 10
to outerVar
, and logs a message indicating the conclusion of the outer function execution (message #2). The inner function inner
captures the variable outerVar
through closure, allowing it to access the value even after the outer function has concluded.
CALLING After the IIFE is executed, the outer function is invoked (message #3), resulting in the execution of the inner function and the logging of its messages.
SINGLENESS Additionally, a reference to the outer
function is assigned to outer1
(message #4). Subsequent logs display the type of outer1
(#5), followed by the invocation of outer1
and its corresponding messages (#6). Finally, a comparison between outer
and outer1
verifies that they are referencing the same function (#7).
ROBUSTNESS This showcases the combination of an IIFE and a closure enables the restriction of object instantiation, providing a robust implementation for our Singleton Pattern.
CODE Now, we have found a way to keep the instance variable private and directally inaccessible using closure and also a way to ensure that only one instance of the object will be created, let's code our Singleton:
// Immediately Invoked Function Expression (IIFE) begins
const Singleton = (() => {
// Declaring a private variable
let privateVar = 0;
// Creating an object containing methods (closures)
// to access and manipulate privateVar
// Declaring a variable to hold the singleton instance
const instance = {
// Method to retrieve privateVar
getPrivateVar: () => privateVar,
// Method to increment privateVar
incrementPrivateVar: () => { privateVar++ },
};
// Returning the object containing the methods
return instance;
})(); // IIFE ends, and the object is assigned to the Singleton constant
// Example of usage
// It's not possible to create a new instance of Singleton:
// const singleton1 = new Singleton() // TypeError: Singleton is not a constructor
// const singleton1 = Singleton() // TypeError: Singleton is not a function
// Accessing the initial value of privateVar
console.log(Singleton.getPrivateVar());
// Incrementing the value of privateVar
Singleton.incrementPrivateVar();
// Accessing the updated value of privateVar
console.log(Singleton.getPrivateVar());
// Creating a new reference for Singleton
const singleton1 = Singleton
// Incrementing the value of privateVar
singleton1.incrementPrivateVar();
// Accessing the updated value of privateVar
console.log(singleton1.getPrivateVar());
// Comparing singleton references
console.log(Singleton === singleton1);
// OUTPUT:
// 0
// 1
// 2
// true
ACHIEVEMENTS With the above code, we have already met the most important prerequisites of a Singleton Pattern:
Encapsulation: The code keeps
privateVar
truly private, accessible only through thegetPrivateVar
andincrementPrivateVar
methods.Singleness: By defining
instance
as a constant within the IIFE, the code ensures that only one instance of the Singleton object is created. This meets the main goal of the Singleton pattern.
Full Singleton Implementation
IMPROVEMENTS The code implements the Singleton pattern in a basic and effective manner for the presented scenario, ensuring a single "instance" of the object with controlled access to a private variable. However, to adhere more strictly to the Singleton pattern and to enhance the security and integrity of the object some improvement are recommended:
Full Immutability: To ensure that the Singleton object and its methods are not modified after creation, we could use
Object.freeze()
on theinstance
object before returning it. This would prevent any modifications to the methods and properties of the object.Demand-Driven Initialization: If you have a Singleton instance that is costly to initialize or rarely accessed, it might be beneficial to more closely adhere to the Singleton Pattern by implementing a technique known as a lazy initialization. This method utilizes a the getter function called
getInstance
to create the Singleton instance on-demand, only when it is truly needed for the first time, rather than initializing it the moment the script is loaded.
IMPLEMENTATION Let's see how all these improvements appear in the code below:
const Singleton = (() => {
// Stores the Singleton instance
let instance;
// Private variable accessible only through Singleton's methods
let privateVar = 0;
// Factory Function (a function that returns an object)
// to initialize the Singleton instance,
// called internally only.
function createInstance() {
// Object containing the Singleton's public methods and properties
return {
// Method to access the value of the private variable
getPrivateVar: () => { return privateVar;},
// Method to increment the value of the private variable
incrementPrivateVar: () => { privateVar++;}
};
}
// Publicly exposed object
return {
// Method to get the Singleton instance
getInstance: function() {
if (!instance) {
// Only create the instance if it doesn't exist
instance = createInstance();
// Optionally, you can freeze the instance
// to prevent modifications
Object.freeze(instance);
}
return instance;
}
};
})();
// Using the Singleton
// Obtaining the Singleton instance
const reference1 = Singleton.getInstance();
console.log(reference1.getPrivateVar()); // OUTPUT: 0
// Trying to get another reference to the Singleton instance
const reference2 = Singleton.getInstance();
// Modifying the value of the private variable
reference2.incrementPrivateVar();
console.log(reference1.getPrivateVar()); // OUTPUT: 1
// Comparing both references.
// Both references point to the same instance.
console.log(reference1 === reference2); // OUTPUT: true
// Attempts to directly modify the instance will fail
// If `Object.freeze(instance)` is being used,
// the instance is fully immutable.
reference1.property = 0
console.log(reference1.property) // OUTPUT: undefined
//Priting objects
console.log('Singleton creator:', Singleton)
// OUTPUT:{ getInstance: [Function: getInstance] }
console.log("Singleton instance:", reference1)
// OUTPUT:
//{
// getPrivateVar: [Function: getPrivateVar],
// incrementPrivateVar: [Function: incrementPrivateVar]
//}
// Attempts to create a new instance will result in error.
// const instance2 = Singleton()
// OUTPUT:TypeError: Singleton is not a function
// const instance2 = new Singleton()
// OUTPUT:TypeError: Singleton is not a constructor
CODE The code above implements a Singleton pattern in JavaScript, ensuring that only one instance of a particular object is created throughout the lifetime of the application. It leverages closures to encapsulate a private variable (privateVar
) and provides two methods (getPrivateVar
and incrementPrivateVar
) for interacting with this private state. The Singleton instance is "lazily initialized" upon the first call to getInstance
, which also optionally employs Object.freeze
to make the instance immutable, preventing any further modification to its properties or methods.
EFFICIECY This implementation demonstrates an efficient use of the Singleton pattern to manage global state in a controlled manner. The code guarantees both the uniqueness of the instance and the integrity of its state. Such a pattern is particularly useful in scenarios where a single, shared resource or configuration object is needed across different parts of an application.
Conclusion
DIGEST Through this study, we learned how closures allow for the creation of privately inaccessible variables, maintaining state between function invocations, and how IIFE is used to immediately execute functions, encapsulating logic within an isolated scope to avoid global conflicts. Additionally, the use of factory functions and arrow function notation to create and manage instances in a more concise and readable manner was demonstrated. The final implementation of the Singleton pattern, with improvements like on-demand initialization and full object immutability, illustrates a robust and secure application of this pattern, emphasizing efficiency in managing global state in a controlled manner.
APPRENTICESHIP This blog post provides a clear understanding of advanced JavaScript concepts, demonstrates practical implementations of the Singleton Pattern, and offers insights into programming techniques that promote encapsulation, singleness, and state integrity. Moreover, the step-by-step approach and detailed code examples serve as an excellent educational resource for programmers at all levels, facilitating the assimilation of complex design patterns and their implementations in JavaScript.
FINAL This case study on the Singleton pattern in JavaScript showcases the importance of understanding closures and IIFE for effective software design. It highlights how advanced JavaScript features can lead to more secure, maintainable, and efficient applications. Such insights are crucial for developers aiming to harness JavaScript's full potential in real-world scenarios, emphasizing the value of deep conceptual knowledge in programming.
We'd Love to Hear from You!
Found this dive into JavaScript's Singleton Pattern, closures, and IIFEs intriguing? Have a question, suggestions or a unique perspective to share? Drop a comment below and let's spark a vibrant discussion.
Your insights could inspire our next topic or help fellow readers navigate their coding journey. Keep the conversation going!