Singleton Design Pattern in JavaScript

Christopher T.

July 10th, 2022

Share This Post

In this post, we will be going over the Singleton Design Pattern in JavaScript.

Singletons are a necessary component in software where only one instance of an object is necessary but is instantiated many times.

For example, if we were to implement LocalStorage in JavaScript, it would look something like this:

class LocalStorage {
  #data = {}

  constructor(options) {
    this.options = options || {}
  }

  get(key) {
    return this.#data[key]
  }

  set(key, value) {
    this.#data[key] = value
  }

  remove(key) {
    delete this.#data[key]
  }

  clear() {
    this.#data = {}
  }
}

Let's now think about our choices in code decision making here whenever a function requests to use this interface.

Every time we instantiate a new instance of LocalStorage, the 4 methods get, set, remove and clear will exist in all instances. However, new instances will not copy these methods from the original instance. Since they are included as part of the prototype of LocalStorage they are shared methods, meaning every new instance of LocalStorage will have the methods become references to the ones in the original instance.

This is crucial to understand when making decisions between factory functions and classes. You probably read one or more tutorials about factories before where they recommend them over classes. It's important to know that tips like those are not a one-size-fits-all solution because some situations suffer different problems.

I agree with the recommendation to prefer factories over classes however when your program also creates instances of an object often then it's time to consider classes because they have the advantage of sharing its methods throughout all object creations.

Here is the factory function equivalent of our LocalStorage class:

function LocalStorage() {
  let _data = {}
  let _options = {}

  return {
    get(key) {
      return _data[key]
    },
    set(key, value) {
      _data[key] = value
    },
    remove(key) {
      delete _data[key]
    },
    clear() {
      _data = {}
    },
  }
}

Instantiating this would look something like this:

const localStorage = LocalStorage()

This is not a good decision since the returned object is a newly created object everytime LocalStorage produces the object.

So if a program were to create hundreds of these it's wasteful because the implementation details of get, set, remove and clear never changes, so there is really no point in having them copied and take up memory space.

At this moment its best to use the Singleton Design Pattern on LocalStorage so that get, set, remove and clear will be shared amongst all constructed instances.

We can actually use factories to implement a singleton if we were to encapsulate the prototype class:

function LocalStorage(options) {
  let _data = {}
  let _options = options || {}

  return {
    get(key) {
      return _data[key]
    },
    set(key, value) {
      _data[key] = value
    },
    remove(key) {
      delete _data[key]
    },
    clear() {
      _data = {}
    },
  }
}

const createLocalStorage = (function () {
  return function createLocalStorage(options) {
    if (!ls) {
      ls = LocalStorage(options)
    }
    return ls
  }
})()

Whenever our createLocalStorage factory function is called it will check if a previously instantiated one exists on the ls variable. If it does, it returns the existing one. If not, it will create a new one and slap that right on the ls variable:

factory-singleton-design-pattern-in-javascript.png

Other implementations

So far i've covered the factory function way of maintaining a Singleton. Here are other ways that can achieve this:

Class static method

This implementation uses a class object and implements two static properties (technically it's one property and one method). Notice how it's equivalent to a global variable:

Name Description
_instance Holds the global instance of LocalStorage. This is where it will be stored by instantiators
getInstance Performs a check on LocalStorage._instance. If an instance was created previously, that will be returned every time. If it is empty, a new instance will be created and attached onto it
class LocalStorage {
  #data = {}

  static _instance = null

  static getInstance(options) {
    if (!LocalStorage._instance) {
      LocalStorage._instance = new LocalStorage(options)
    }

    return LocalStorage._instance
  }

  constructor(options) {
    this.options = options || {}
  }

  get(key) {
    return this.#data[key]
  }

  set(key, value) {
    this.#data[key] = value
  }

  remove(key) {
    delete this.#data[key]
  }

  clear() {
    this.#data = {}
  }
}

Variable Declaration

This is the simplest way to create a Singleton. It works because it's declared in the global scope and cannot be re-written due to the const keyword. However this is not the best way to create singletons when you have more efficient ways to do it:

const MySingleton = {
  sayHello() {
    console.log('hello')
  },
}

Freeze It

In this approach we keep the LocalStorage class. However, this time we don't create any static properties onto the class. We freeze the instance instead:

class LocalStorage {
  #data = {}

  static _instance = null

  static getInstance(options) {
    if (!LocalStorage._instance) {
      LocalStorage._instance = new LocalStorage(options)
    }

    return LocalStorage._instance
  }

  constructor(options) {
    this.options = options || {}
  }

  get(key) {
    return this.#data[key]
  }

  set(key, value) {
    this.#data[key] = value
  }

  remove(key) {
    delete this.#data[key]
  }

  clear() {
    this.#data = {}
  }
}

const localStorage = new LocalStorage()

Object.freeze(localStorage)

This way we are ensured that nothing in the class can get written. However, maintaining the singleton works a little bit differently.

By using Object.freeze we must ensure to never attempt to create or instantiate a LocalStorage because our program will throw an error. This can mean something as simple as renaming createLocalStorage to getLocalStorage and utilizing it throughout the lifetime of the app.

Common practices in Singleton implementations

  • It's not uncommon to work with Singletons that never free up from the memory until the program is terminated
  • Global variables are scoped lexically while the intent of a Singleton is to never be redefined
  • Singletons are modified through methods

Conclusion

Thank you for reading and look forward for more quality posts coming from me in the future!

Subscribe to my posts :)


Top online courses in Web Development

Tags


Read every story from jsmanifest (and thousands of other writers on medium)

Your membership fee directly supports jsmanifest and other writers you read. You'll also get full access to every story on Medium.

Subscribe to the Newsletter

Get continuous updates

Mediumdev.toTwitterGitHubrss

© jsmanifest 2022