Fundamentals 28 min read

JavaScript Design Patterns: A Comprehensive Guide

This guide explains JavaScript design patterns—creational, structural, and behavioral—detailing common examples such as Singleton, Factory, Module, Decorator, Observer, Strategy, and more, and shows how applying these proven solutions can make code clearer, more flexible, maintainable, and scalable.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
JavaScript Design Patterns: A Comprehensive Guide

JavaScript design patterns are the wisdom of programming, providing excellent solutions to common problems. Whether you are a beginner or an experienced developer, mastering these patterns can make your code clearer and more flexible. This article introduces common design patterns to help improve code quality and build more maintainable applications.

What Are Design Patterns

Design patterns are solutions to problems that repeatedly occur in software design. They are proven, reusable code design experiences that can solve specific types of problems. These patterns help developers organize and design code more effectively, improving maintainability and scalability.

Categories of Design Patterns in JavaScript

Design patterns can be divided into the following categories:

Creational Patterns : Focus on object creation mechanisms, providing flexible and controlled ways to instantiate objects. Common patterns include Singleton, Factory, Constructor, Prototype, and more.

Structural Patterns : Focus on organizing and composing objects to form larger structures. They promote object composition, define relationships, and provide flexible ways to manipulate their structure. Common patterns include Adapter, Decorator, Composite, and more.

Behavioral Patterns : Focus on interaction between objects and responsibility allocation. They provide solutions for object communication, coordination, and collaboration. Common patterns include Observer, Iterator, Strategy, and more.

1. Creational Patterns

Creational patterns focus on object creation mechanisms, providing flexible and controllable ways to instantiate objects.

Singleton Pattern

The Singleton pattern ensures a class has only one instance and provides a global access point. It limits the number of times a class can be instantiated, ensuring only one instance exists throughout the application (like store in vuex and redux).

Implementation Example 1: Using Closures

var Singleton = (function () {
  var instance; // Store singleton instance

  function init() {
    // Private variables and methods
    var privateVariable = 'I am private';

    function privateMethod() {
      console.log('This is a private method');
    }

    return {
      // Public variables and methods
      publicVariable: 'I am public',
      publicMethod: function() {
        console.log('This is a public method');
      },
      getPrivateVariable: function() {
        return privateVariable;
      },
    };
  }

  return {
    // Method to get singleton instance
    getInstance: function() {
      if (!instance) {
        instance = init();
      }
      return instance;
    },
  };
})();

// Get singleton instance
var mySingleton = Singleton.getInstance();

Implementation Example 2: Using Class

class Singleton {
  static instance; // Static property to store singleton instance

  constructor() {}

  // Static method to get singleton instance
  static getInstance() {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton();
    }
    return Singleton.instance;
  }
}

// Get singleton instance
var mySingleton1 = Singleton.getInstance();
var mySingleton2 = Singleton.getInstance();

console.info(mySingleton1 === mySingleton2); // true

Factory Pattern

The Factory pattern is a creational pattern that encapsulates the object creation process, allowing code to not care about the specific instantiation process. It helps encapsulate object creation, improving code flexibility and maintainability.

class Car {
  constructor(type, model) {
    this.type = type;
    this.model = model;
  }
}

class CarFactory {
  createCar(type, model) {
    return new Car(type, model);
  }
}

const factory = new CarFactory();
const myCar = factory.createCar('SUV', 'Model 1');

Constructor Pattern

In JavaScript, the Constructor pattern is a way to create objects using constructors and the new keyword. It allows creating multiple objects with similar properties and methods.

// Constructor
function Animal(name, species) {
  this.name = name;
  this.species = species;

  this.eat = function() {
    console.info('eating');
  };
}

const cat = new Animal('Tom', 'Cat');
const dog = new Animal('Pluto', 'Dog');

Prototype Pattern

The Prototype pattern creates objects using a prototype object as a template, allowing property and method sharing through the prototype chain. In JavaScript, every object has a link to its prototype, through which it can inherit properties and methods.

function Animal(name, species) {
  this.name = name;
  this.species = species;
}
Animal.prototype.eat = function() {
  console.info('eating');
};

const cat = new Animal('Tom', 'Cat');
const dog = new Animal('Pluto', 'Dog');

Module Pattern

The Module pattern is a design pattern for encapsulating and organizing JavaScript code. It uses closures to create private scopes, enabling information hiding and modularization. This pattern helps organize code into maintainable and reusable units.

var MyModule = (function() {
  // Private variable
  var privateVariable = 'I am private';

  // Private method
  function privateMethod() {
    console.log('This is a private method');
  }

  // Public variables and methods
  return {
    publicVariable: 'I am public',
    publicMethod: function() {
      console.log('This is a public method');
    },
    getPrivateVariable: function() {
      return privateVariable;
    },
  };
})();

Builder Pattern

The Builder pattern is a creational design pattern used for constructing complex objects, separating the construction process from representation so the same construction process can create different representations. In JavaScript, it is typically implemented using object literals and method chaining.

class ComputerBuilder {
  constructor() {
    this.computer = {};
  }

  addCPU(cpu) {
    this.computer.cpu = cpu;
    return this; // Return builder instance for chaining
  }

  addRAM(ram) {
    this.computer.ram = ram;
    return this;
  }

  addStorage(storage) {
    this.computer.storage = storage;
    return this;
  }

  build() {
    return this.computer;
  }
}

// Use builder to create computer object
const myComputer = new ComputerBuilder()
  .addCPU('Intel i7')
  .addRAM('16GB')
  .addStorage('512GB SSD')
  .build();

2. Structural Patterns

Structural patterns focus on organizing and composing objects to form larger structures.

Decorator Pattern

The Decorator pattern is a structural design pattern that allows dynamically extending object behavior by wrapping objects in decorator class instances. In JavaScript, it is typically implemented using functions or classes.

class Car {
  drive() { console.log('Driving the car'); }
}

// Decorator class - Turbo
class TurboDecorator {
  constructor(car) {
    this.car = car;
  }

  drive() {
    this.car.drive();
    console.log('Turbo boost activated!');
  }
}

// Decorator class - Stereo
class StereoDecorator {
  constructor(car) {
    this.car = car;
  }

  drive() {
    this.car.drive();
    console.log('Enjoying music with the stereo system');
  }
}

// Create basic car object
const basicCar = new Car();

// Use decorators to extend functionality
const turboCar = new TurboDecorator(basicCar);
const luxuryCar = new StereoDecorator(turboCar);

// Call decorated car object's method
luxuryCar.drive();

Facade Pattern

The Facade pattern is a structural design pattern that provides a simplified interface for accessing one or more complex subsystems. It hides system complexity and provides a simpler, consistent interface.

// Subsystem - Player
class Player {
  play() { console.log('Playing music'); }
  stop() { console.log('Stopping music'); }
}

// Subsystem - Stereo
class Stereo {
  turnOn() { console.log('Turning on the stereo'); }
  turnOff() { console.log('Turning off the stereo'); }
}

// Facade class - Audio System
class AudioSystemFacade {
  constructor() {
    this.player = new Player();
    this.stereo = new Stereo();
  }

  playMusic() {
    this.stereo.turnOn();
    this.player.play();
  }

  stopMusic() {
    this.player.stop();
    this.stereo.turnOff();
  }
}

// Use Facade pattern to simplify interface
const audioFacade = new AudioSystemFacade();
audioFacade.playMusic(); // Turning on the stereo, Playing music
audioFacade.stopMusic(); // Stopping music, Turning off the stereo

Adapter Pattern

The Adapter pattern is a structural design pattern that allows objects with incompatible interfaces to cooperate. It creates a wrapper object (adapter) that makes incompatible interfaces compatible.

// Old calculator object
class OldCalculator {
  getTotal() {
    return 100;
  }
}

// New system expected interface
class NewCalculator {
  calculate() {
    return 200;
  }
}

// Adapter class
class CalculatorAdapter {
  constructor(oldCalculator) {
    this.oldCalculator = oldCalculator;
  }

  // Adapter method
  calculate() {
    return this.oldCalculator.getTotal();
  }
}

// Use adapter to connect old and new systems
const oldCalculator = new OldCalculator();
const adapter = new CalculatorAdapter(oldCalculator);

console.log('Total using adapter:', adapter.calculate()); // Total using adapter: 100

Bridge Pattern

The Bridge pattern is a structural design pattern that separates an abstraction from its implementation, allowing them to vary independently. It uses composition instead of inheritance to decouple abstraction and implementation.

// Color implementation class
class RedColor {
  applyColor() {
    console.log('Applying red color');
  }
}

class BlueColor {
  applyColor() {
    console.log('Applying blue color');
  }
}

// Shape abstraction class
class Shape {
  constructor(color) {
    this.color = color;
  }

  applyColor() {
    // Delegate to color implementation class
    this.color.applyColor();
  }

  draw() {
    // Specific shape drawing logic
  }
}

// Concrete shape class
class Circle extends Shape {
  draw() {
    console.log('Drawing circle');
  }
}

class Square extends Shape {
  draw() {
    console.log('Drawing square');
  }
}

// Use bridge pattern to connect shapes and colors
const redCircle = new Circle(new RedColor());
const blueSquare = new Square(new BlueColor());

// Call concrete shape methods, delegate to color implementation
redCircle.draw(); // Drawing circle
redCircle.applyColor(); // Applying red color

blueSquare.draw(); // Drawing square
blueSquare.applyColor(); // Applying blue color

Composite Pattern

The Composite pattern is a structural design pattern that allows composing objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects and composite objects uniformly.

// Abstract graphic class
class Graphic {
  draw() {
    // Abstract method, implemented by concrete subclasses
  }
}

// Concrete graphic class - Circle
class Circle extends Graphic {
  constructor(name) {
    super();
    this.name = name;
  }

  draw() {
    console.log(`Drawing Circle: ${this.name}`);
  }
}

// Concrete graphic class - Rectangle
class Rectangle extends Graphic {
  constructor(name) {
    super();
    this.name = name;
  }

  draw() {
    console.log(`Drawing Rectangle: ${this.name}`);
  }
}

// Composite graphic class
class CompositeGraphic extends Graphic {
  constructor(name) {
    super();
    this.name = name;
    this.graphics = [];
  }

  add(graphic) {
    this.graphics.push(graphic);
  }

  draw() {
    console.log(`Drawing Composite: ${this.name}`);
    this.graphics.forEach((graphic) => {
      graphic.draw();
    });
  }
}

// Use composite pattern to create graphic structure
const circle1 = new Circle('Circle 1');
const circle2 = new Circle('Circle 2');
const rectangle1 = new Rectangle('Rectangle 1');

const composite = new CompositeGraphic('Composite 1');
composite.add(circle1);
composite.add(rectangle1);

const rootComposite = new CompositeGraphic('Root Composite');
rootComposite.add(composite);
rootComposite.add(circle2);

// Draw entire graphic structure
rootComposite.draw();

3. Behavioral Patterns

Behavioral patterns focus on interaction between objects and responsibility allocation.

Observer Pattern

The Observer pattern defines a one-to-many dependency relationship. When one object's state changes, all dependent objects are notified and automatically updated. In JavaScript, it is typically implemented using callbacks or event mechanisms.

// Subject class, maintains observer list, provides methods to add, remove, and notify observers
class Subject {
  constructor() {
    this.observers = [];
  }

  // Add observer
  addObserver(observer) {
    this.observers.push(observer);
  }

  // Remove observer
  removeObserver(observer) {
    this.observers = this.observers.filter((o) => o !== observer);
  }

  // Notify all observers
  notify() {
    this.observers.forEach((observer) => {
      observer.update();
    });
  }
}

// Observer class, has an update method to perform operations when notified
class Observer {
  constructor(name) {
    this.name = name;
  }

  // Update method
  update() {
    console.log(`${this.name} has been notified and updated.`);
  }
}

// Create subject and observers
const subject = new Subject();
const observer1 = new Observer('Observer 1');
const observer2 = new Observer('Observer 2');

// Add observers to subject
subject.addObserver(observer1);
subject.addObserver(observer2);

// Notify all observers
subject.notify();

Strategy Pattern

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It allows the algorithm to vary independently from clients that use it.

// Payment strategy interface
class PaymentStrategy {
  pay(amount) {
    // Strategy interface, concrete strategy classes need to implement this method
  }
}

// Concrete payment strategy - Alipay
class AlipayStrategy extends PaymentStrategy {
  pay(amount) {
    console.log(`Paid ${amount} using Alipay.`);
  }
}

// Concrete payment strategy - WeChat Pay
class WeChatPayStrategy extends PaymentStrategy {
  pay(amount) {
    console.log(`Paid ${amount} using WeChat Pay.`);
  }
}

// Concrete payment strategy - Credit Card
class CreditCardStrategy extends PaymentStrategy {
  pay(amount) {
    console.log(`Paid ${amount} using Credit Card.`);
  }
}

// Context class, responsible for receiving and executing concrete payment strategies
class PaymentContext {
  constructor(strategy) {
    this.strategy = strategy;
  }

  // Set payment strategy
  setPaymentStrategy(strategy) {
    this.strategy = strategy;
  }

  // Execute payment strategy
  executePayment(amount) {
    this.strategy.pay(amount);
  }
}

// Use strategy pattern
const paymentContext = new PaymentContext(new AlipayStrategy());
paymentContext.executePayment(1000); // Paid 1000 using Alipay

// Switch payment strategy
paymentContext.setPaymentStrategy(new WeChatPayStrategy());
paymentContext.executePayment(800); // Paid 800 using WeChat Pay

Command Pattern

The Command pattern encapsulates a request as an object, allowing parameterization of clients with different requests, queuing of requests, cancellation of requests, and support for undoable operations.

// Command interface
class Command {
  execute() {
    // Command interface, concrete command classes need to implement this method
  }
}

// Concrete command class - Turn on TV
class TurnOnTVCommand extends Command {
  constructor(tv) {
    super();
    this.tv = tv;
  }

  execute() {
    this.tv.turnOn();
  }
}

// Concrete command class - Turn off TV
class TurnOffTVCommand extends Command {
  constructor(tv) {
    super();
    this.tv = tv;
  }

  execute() {
    this.tv.turnOff();
  }
}

// Receiver class - TV
class TV {
  turnOn() {
    console.log('TV is turned on.');
  }

  turnOff() {
    console.log('TV is turned off.');
  }
}

// Invoker class - Remote control
class RemoteControl {
  constructor() {
    this.command = null;
  }

  setCommand(command) {
    this.command = command;
  }

  pressButton() {
    this.command.execute();
  }
}

// Use command pattern
const tv = new TV();
const turnOnCommand = new TurnOnTVCommand(tv);
const turnOffCommand = new TurnOffTVCommand(tv);

const remoteControl = new RemoteControl();

// Configure remote control buttons
remoteControl.setCommand(turnOnCommand);
remoteControl.pressButton(); // TV is turned on.

remoteControl.setCommand(turnOffCommand);
remoteControl.pressButton(); // TV is turned off.

Iterator Pattern

The Iterator pattern provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation. In JavaScript, it is typically implemented using iterator objects or built-in iterator interfaces.

// Custom iterator object
class ArrayIterator {
  constructor(array) {
    this.array = array;
    this.index = 0;
  }

  hasNext() {
    return this.index < this.array.length;
  }

  next() {
    return this.hasNext() ? this.array[this.index++] : null;
  }
}

// Use iterator pattern to traverse array
const array = [1, 2, 3, 4, 5];
const iterator = new ArrayIterator(array);

while (iterator.hasNext()) {
  console.log(iterator.next());
}

Mediator Pattern

The Mediator pattern defines an object that encapsulates how a set of objects interact. It prevents objects from communicating directly with each other, reducing coupling between objects.

// Chat room class
class ChatMediator {
  constructor() {
    this.users = [];
  }

  addUser(user) {
    this.users.push(user);
  }

  sendMessage(message, sender) {
    this.users.forEach((user) => {
      if (user !== sender) {
        user.receiveMessage(message);
      }
    });
  }
}

// User class
class User {
  constructor(name, mediator) {
    this.name = name;
    this.mediator = mediator;
  }

  sendMessage(message) {
    console.log(`${this.name} sending message: ${message}`);
    this.mediator.sendMessage(message, this);
  }

  receiveMessage(message) {
    console.log(`${this.name} received message: ${message}`);
  }
}

// Use mediator pattern
const mediator = new ChatMediator();

const user1 = new User('User 1', mediator);
const user2 = new User('User 2', mediator);
const user3 = new User('User 3', mediator);

mediator.addUser(user1);
mediator.addUser(user2);
mediator.addUser(user3);

user1.sendMessage('Hello, everyone!'); // User 1 sending message: Hello, everyone!
// User 2 received message: Hello, everyone!
// User 3 received message: Hello, everyone!

user3.sendMessage('Hi there!'); // User 3 sending message: Hi there!
// User 1 received message: Hi there!
// User 2 received message: Hi there!

Summary

In JavaScript development, design patterns are powerful tools that help us better organize and structure code, improving maintainability and scalability. This article introduced some common JavaScript design patterns, including Singleton, Factory, Observer, Strategy, and more. Design patterns are not rigid rules but rather best practices in specific contexts. In real projects, choosing the appropriate design pattern based on requirements and context is crucial. Understanding and mastering the principles of design patterns helps us better design and organize code structure, improving code quality. In summary, design patterns are an important topic in JavaScript development. By learning and applying design patterns, we can write clearer, more maintainable, and more extensible code.

design patternsJavaScriptcode architectureFactory PatternsingletonObserver PatternBehavioral PatternsCreational PatternsModule PatternStructural Patterns
Sohu Tech Products
Written by

Sohu Tech Products

A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.