Understanding Design Patterns in JavaScript
This article explains why and how to use various JavaScript design patterns—such as module, revealing module, ES6 modules, singleton, factory, and decorator—by describing their purpose, benefits, and providing clear code examples while warning against over‑use.
Using appropriate design patterns helps you write clearer, more maintainable code, but it is essential to avoid over‑using them and to ensure that a problem truly fits a pattern before applying one.
When starting a new project you should first define its purpose, scope, and specifications; only then should you begin coding or select the most suitable design pattern for a complex project.
A design pattern is a reusable solution to a common software‑design problem, representing best practices that experienced developers use and can be thought of as programming templates.
Design patterns improve code readability, maintainability, and provide a shared vocabulary that lets new developers quickly grasp the intent of the code.
Module Pattern – Modules encapsulate code, avoid namespace pollution, and enable reuse. Because JavaScript lacks access modifiers, the module pattern simulates encapsulation using an IIFE and closures.
const myModule = (function() {
const privateVariable = 'Hello World';
function privateMethod() {
console.log(privateVariable);
}
return {
publicMethod: function() {
privateMethod();
}
};
})();
myModule.publicMethod();The IIFE runs immediately, returning an object whose public methods can access the private variables via closure.
Revealing Module Pattern – An improvement that maps private functions directly to public properties, making the public API clearer.
const myRevealingModule = (function() {
let privateVar = 'Peter';
const publicVar = 'Hello World';
function privateFunction() {
console.log('Name: ' + privateVar);
}
function publicSetName(name) {
privateVar = name;
}
function publicGetName() {
privateFunction();
}
return {
setName: publicSetName,
greeting: publicVar,
getName: publicGetName
};
})();
myRevealingModule.setName('Mark');
myRevealingModule.getName();This pattern makes it easy to see which members are public and improves readability.
ES6 Modules – Modern JavaScript provides native module support. Each file is a module; everything is private by default and can be exported with the export keyword. Modules always run in strict mode.
// utils.js
export const greeting = 'Hello World';
export function sum(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
function privateLog() { console.log('Private'); }
// main.js
import { sum, greeting } from './utils.js';
console.log(greeting);
console.log(sum(3, 7));Exports can also be renamed, and imports can be aliased to avoid naming conflicts.
// utils.js
export { sum as add, multiply };
// main.js
import { add, multiply as mult } from './utils.js';
console.log(add(3, 7));
console.log(mult(3, 7));Singleton Pattern – Guarantees a class has only one instance. It can be implemented with an object literal, a constructor that caches the instance, or via a module.
// Constructor version
let instance = null;
function User() {
if (instance) return instance;
instance = this;
this.name = 'Peter';
return instance;
}
const u1 = new User();
const u2 = new User();
console.log(u1 === u2); // trueOr using a module:
const singleton = (function() {
let instance;
function init() { return { name: 'Peter', age: 24 }; }
return {
getInstance: function() {
if (!instance) instance = init();
return instance;
}
};
})();
const a = singleton.getInstance();
const b = singleton.getInstance();
console.log(a === b); // trueFactory Pattern – Creates objects without exposing the concrete classes. The example shows a VehicleFactory that produces Car or Truck instances based on an options object.
class Car { constructor(opts) { this.doors = opts.doors || 4; this.state = opts.state || 'new'; this.color = opts.color || 'white'; } }
class Truck { constructor(opts) { this.doors = opts.doors || 4; this.state = opts.state || 'used'; this.color = opts.color || 'black'; } }
class VehicleFactory {
createVehicle(opts) {
if (opts.vehicleType === 'car') return new Car(opts);
if (opts.vehicleType === 'truck') return new Truck(opts);
}
}
const factory = new VehicleFactory();
const car = factory.createVehicle({ vehicleType: 'car', color: 'silver' });
const truck = factory.createVehicle({ vehicleType: 'truck', color: 'white' });
console.log(car);
console.log(truck);Decorator Pattern – Extends an object’s behavior without modifying its class. The example decorates a Car object with additional features such as AC, automatic transmission, and power locks, updating the cost method each time.
function carWithAC(car) { car.hasAC = true; const prev = car.cost(); car.cost = () => prev + 500; }
function carWithAutoTransmission(car) { car.hasAutoTransmission = true; const prev = car.cost(); car.cost = () => prev + 2000; }
function carWithPowerLocks(car) { car.hasPowerLocks = true; const prev = car.cost(); car.cost = () => prev + 500; }
const car = new Car();
carWithAC(car);
carWithAutoTransmission(car);
carWithPowerLocks(car);
console.log(car.cost()); // total costIn summary, JavaScript offers many design patterns that improve code structure and collaboration, but developers should evaluate each pattern’s suitability and avoid unnecessary complexity.
Qunar Tech Salon
Qunar Tech Salon is a learning and exchange platform for Qunar engineers and industry peers. We share cutting-edge technology trends and topics, providing a free platform for mid-to-senior technical professionals to exchange and learn.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.