🟨 JavaScript Q121 / 121

How do you implement a singleton pattern in JavaScript?

AI-Powered Answer ✓ Answered

The Singleton pattern is a creational design pattern that ensures a class has only one instance and provides a global point of access to that instance. It's useful when exactly one object is needed to coordinate actions across the system, such as a configuration manager, logger, or database connection pool.

Core Principles of a Singleton

  • One Instance Only: Guarantees that only a single instance of the class can exist.
  • Global Access Point: Provides a method to access that single instance from anywhere in the application.
  • Lazy Initialization: The instance is often created only when it's first requested, rather than at application startup.

Implementation Strategies in JavaScript

1. Using an ES6 Class with a Static Method

This is a common and relatively clean way to implement a singleton using modern JavaScript classes. We create a class and use a static method to control the instantiation process, ensuring only one instance is ever created.

javascript
class DatabaseConnection {
  constructor() {
    if (DatabaseConnection.instance) {
      return DatabaseConnection.instance;
    }

    this.connectionString = 'mongodb://localhost:27017/mydatabase';
    console.log('New database connection created.');
    DatabaseConnection.instance = this;
  }

  connect() {
    console.log(`Connecting to ${this.connectionString}...`);
    // Actual connection logic would go here
  }

  static getInstance() {
    if (!DatabaseConnection.instance) {
      DatabaseConnection.instance = new DatabaseConnection();
    }
    return DatabaseConnection.instance;
  }
}

// Usage:
const db1 = DatabaseConnection.getInstance();
const db2 = DatabaseConnection.getInstance();

console.log(db1 === db2); // true

db1.connect();
db2.connect();

// Trying to create directly via new will still return the existing instance
const db3 = new DatabaseConnection();
console.log(db1 === db3); // true

In this approach, the getInstance() static method acts as the gatekeeper. It checks if an instance already exists; if not, it creates one and stores it on the class itself (DatabaseConnection.instance). Subsequent calls return the stored instance. The constructor also includes a check to handle direct instantiation attempts, although relying solely on getInstance() is the intended usage.

2. Using an IIFE (Immediately Invoked Function Expression)

This method leverages closures and the module pattern to create a private scope for the instance, exposing only a public API. This was a very common pattern before ES6 modules became widespread.

javascript
const Logger = (function() {
  let instance;

  function init() {
    // Private methods and variables
    let logs = [];
    function privateLog(message) {
      const timestamp = new Date().toISOString();
      logs.push(`${timestamp}: ${message}`);
      console.log(`[LOG] ${timestamp}: ${message}`);
    }

    return {
      // Public methods
      log: function(message) {
        privateLog(message);
      },
      getLogs: function() {
        return [...logs]; // Return a copy to prevent external modification
      }
    };
  }

  return {
    getInstance: function() {
      if (!instance) {
        instance = init();
      }
      return instance;
    }
  };
})();

// Usage:
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();

console.log(logger1 === logger2); // true

logger1.log('Application started.');
logger2.log('User logged in.');

console.log(logger1.getLogs());

Here, the IIFE creates a closure. The instance variable is private to the IIFE. The init() function creates and returns the actual singleton object. The outer function then returns an object with a getInstance() method, which ensures init() is called only once.

Considerations and Alternatives

  • Global State: Singletons introduce global state, which can make testing harder and lead to tight coupling.
  • Testability: Mocking singletons for testing can be challenging, as their instance is globally accessible and tightly coupled.
  • Modern JavaScript Modules: With ES6 Modules, you can achieve a similar effect of a 'single instance' by simply exporting an instantiated object from a module. The module system guarantees that the module's code runs only once when imported, effectively creating a singleton for that specific module's export.
  • Dependency Injection: For more complex applications, dependency injection frameworks can manage object lifecycles, often providing singletons as a configuration option, which can be more flexible than manually implemented singletons.
javascript
// logger.js (ES6 Module Singleton)
class MyLogger {
  constructor() {
    if (MyLogger.instance) {
        return MyLogger.instance;
    }
    this.logs = [];
    console.log('Logger initialized via module.');
    MyLogger.instance = this;
  }

  log(message) {
    const timestamp = new Date().toISOString();
    this.logs.push(`${timestamp}: ${message}`);
    console.log(`[MODULE LOG] ${timestamp}: ${message}`);
  }

  getLogs() {
    return [...this.logs];
  }
}

export const logger = new MyLogger();


// app.js
import { logger } from './logger.js';
import { logger as anotherLogger } from './logger.js';

logger.log('App started.');
anotherLogger.log('Another event.');

console.log(logger === anotherLogger); // true
console.log(logger.getLogs());