Singleton Design Pattern in JavaScript

Christopher T.

July 10th, 2022

Share This Post

Singletons are a necessary component in software where only one instance of an object is necessary but may eventually become instantiated multiple times.

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 = {}
  }
}

Every time we instantiate a new instance of LocalStorage, the methods get, set, remove and clear will not be copied from the original instance and will instead hold the original by inheritance via the prototype chain. Since they are included as part of the prototype of LocalStorage they are shared.. This is crucial to remember when it comes to factory functions and classes. You probably read tutorials about factories being preferred over the classes approach. It's worth mentioning that they aren't a one-size-fits-all solution because some situations actually endure different problems.

I agree prefering factories over classes most of the time, however it depends most on when and how our factories become used. When our program creates instances of a factory often then it's time to consider classes since they have the strength of the prototype chain allowing its methods to be shared amongst all instances.

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()

The returned object is newly created everytime LocalStorage produces the object. This is an example where we'd like to have the methods become shared.

If a program were to create hundreds of them it would be incredibly wasteful. If we take a closer look at the methods get, set, remove and clear, their implementation suggests that they never actually change in the runtime, so there is really no point in having them copied taking 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 proceed to check if a previously instantiated one exists on the ls variable. If it does, it returns the one that already exists. 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 approach in maintaining a Singleton. Here are other ways we 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
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 rewritten due to the const keyword. However I don't recommend doing this to create singletons when there are 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. Instead we freeze the instance:

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)

By doing this we can ensure that nothing in the class can get written. However, there's a caveat. Maintaining the singleton works a little bit differently. By using Object.freeze we must be sure to never attempt to create or instantiate a LocalStorage because our program will throw an error. This may mean something as simple as renaming createLocalStorage to getLocalStorage and utilizing that 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 2023