设计模式之美-前端
Category(分类): JavaScript Status: 已审查(2025)
原文:整理自知乎专栏文章(原文导出格式损坏严重,本文重排了全部代码块),核心示例取自曾探《JavaScript 设计模式与开发实践》与极客时间专栏《设计模式之美》(王争)。原 TS 示例已按文中说明替换为 TypeScript 语法,JS/TS 代码块分别标注。
最近看完了《JavaScript 设计模式与开发实践》,也在学习极客时间的专栏《设计模式之美》,想着整理一下常用的设计模式。设计模式不仅受用于面试,更能增进代码水平,是每个程序员必须掌握的。
GoF 在《设计模式》中归纳了 23 种设计模式,而它们又属于三种类型的模式,分别是创建型模式、结构型模式和行为型模式。正如书中所说,其实每一种设计模式都来自真实项目,作者只是将它们取一个好听的名字,方便记忆与传播。所以设计模式可比数据结构算法上手要简单一点,因为很多设计模式你可能都似曾相识,学习的过程不过是一个接一个的顿悟。
本文中每一种设计模式,我都会搭配一个简单的例子,我会尽量用 JS 实现,如果 JS 不能很好的表达,就用 TS 实现。毕竟《设计模式》一书的子标题是"可复用面向对象软件的基础",它是为面向对象总结的设计模式。
大分类
- 创建型(5)
工厂方法,抽象工厂,单例,建造者,原型。
- 结构型(7)
适配器,装饰器,代理,外观,桥接,组合,享元。
- 行为型(11)
策略,模板,观察者,迭代器,中介者,状态,职责链,命令,访问者,备忘录,解释器。
创建型设计模式主要解决"对象的创建"问题,结构型设计模式主要解决"类或对象的组合或组装"问题,那行为型设计模式主要解决的就是"类或对象之间的交互"问题。
具体一点,设计模式要干的事情就是解耦,创建型模式是将创建和使用代码解耦,结构型模式是将不同功能代码解耦,行为型模式是将不同的行为代码解耦。

常用设计模式
常用的设计模式有,工厂模式,单例模式,适配器模式,装饰器模式,代理模式,享元模式,策略模式,观察者模式,迭代器模式。如果你还没准备系统的了解设计模式,可以先看这些模式。
创建型
工厂方法
在 JS 中,工厂方法是创建对象的一种方式。它像工厂一样,生产出来的函数都是标准件(拥有相同的属性)。它和单例模式有一点像,缓存了对象,避免重复重新构造相同的对象。下面是创建不同角色的工厂类。
function createPeopleFactory(id, name, age) {
const obj = new Object();
obj.id = id;
obj.name = name;
obj.age = age;
return obj;
}
const child = createPeopleFactory(1, 'baby', 1);
const father = createPeopleFactory(2, 'peter', 25);
抽象工厂
在工厂方法的基础上再抽象一层,用来管理多个工厂类。平时使用场景很少。
abstract class AbstractFactory {
public abstract getColor(color: string);
public abstract getShape(shape: string);
}
// 通过传递形状或颜色信息来获取工厂
class FactoryProducer {
public static getFactory(choice: string) {
if (choice === 'SHAPE') {
return new ShapeFactory();
} else if (choice === 'COLOR') {
return new ColorFactory();
}
return null;
}
}
class ColorFactory extends AbstractFactory {
public getColor(color) {
// do something
}
public getShape() {
return null;
}
}
class ShapeFactory extends AbstractFactory {
public getColor() {
return null;
}
public getShape(shape) {
// do something
}
}
const shape = FactoryProducer.getFactory('SHAPE');
shape.getShape('CIRCLE');
const color = FactoryProducer.getFactory('COLOR');
color.getColor('RED');
单例
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
const singleton = function(fn) {
let result = null;
return function() {
return result || (result = fn.apply(this, arguments));
};
};
const getScript = singleton(function() {
return document.createElement('script');
});
const script1 = getScript();
const script2 = getScript();
console.log(script1 === script2); // true
注:示例依赖
document,需在浏览器环境运行。ESM 时代模块本身就是天然单例——一个模块只会被求值一次,直接export const store = new Store()即可,见文末「2025 视角」。
建造者
用来参数需要在构造函数中初始化,但是参数又过多的场景。
未使用建造者模式:
class Shape {
constructor(width, height, color, opacity, borderWidth) {
if (typeof width !== 'number') {
throw new Error('width should be a number');
}
this.width = width;
if (typeof height !== 'number') {
throw new Error('width should be a number');
}
this.height = height;
this.color = color;
this.opacity = opacity;
if (width < borderWidth) {
throw new Error('width should be greater than borderWidth');
}
this.borderWidth = borderWidth;
}
}
const shape = new Shape(10, 10, 'red', 1, 10);
使用建造者模式改造后:
class ShapeConfig {
constructor(builder) {
this.width = builder.width;
this.height = builder.height;
this.color = builder.color;
this.opacity = builder.opacity;
this.borderWidth = builder.borderWidth;
}
}
class Shape {
constructor() {
this.width = 0;
this.height = 0;
this.color = '';
this.opacity = 1;
this.borderWidth = 0;
}
// 可以把多个值的比较逻辑都放在构建函数中
build() {
if (this.width < this.borderWidth) {
throw new Error('width should be greater than borderWidth');
}
return new ShapeConfig(this);
}
setWidth(width) {
if (typeof width !== 'number') {
throw new Error('width should be a number');
}
this.width = width;
return this;
}
setHeight(height) {
if (typeof height !== 'number') {
throw new Error('height should be a number');
}
this.height = height;
return this;
}
setColor(color) {
this.color = color;
return this;
}
setOpacity(opacity) {
this.opacity = opacity;
return this;
}
setBorderWidth(borderWidth) {
this.borderWidth = borderWidth;
return this;
}
}
const shape = new Shape();
// 可以通过return this方便的实现链式调用
// 译注:原文此处 setBorderWidth(11) 会使 build() 中 width(10) < borderWidth(11) 抛错,
// 与后续 console.log(shape) 的意图矛盾,应为笔误,此处改为 10
shape
.setWidth(10)
.setHeight(10)
.setColor('red')
.setOpacity(1)
.setBorderWidth(10)
.build();
console.log(shape);
原型
原型模式是用于创建对象的一种模式,通过克隆来创建对象。JavaScript 就是一种基于原型的语言。但就 JavaScript 的真正实现来说,其实并不能说对象有原型,而只能说对象的构造器有原型。我们可以通过 Object.create 克隆对象。
const Plane = function() {
this.blood = 100;
this.attackLevel = 1;
this.defenseLevel = 1;
};
const plane = new Plane();
plane.blood = 500;
plane.attackLevel = 10;
plane.defenseLevel = 7;
const clonePlane = Object.create(plane);
console.log(clonePlane); // 原型链上能取到 blood: 500 等属性
结构型
适配器
适配器英文是 Adapter。顾名思义它就是做适配用的,将一个不可用的接口转成可用的接口。适配器模式是一种"亡羊补牢"的模式,没有人会在程序的设计之初就使用它。前端比较典型的应用是跨端框架,mpvue 和 taro,它们都是在应用和各个小程序以及终端之间建立了一层适配器。
下面举一个支付的例子,我们只需要调用 pay 函数,适配器帮我们抹平平台之间的差异:
// wx / alipay / jd / xxx 为各 SDK 注入的全局对象,此处为示意
function pay(id, price) {
const platform = window.platform;
switch (platform) {
case 'wechat': wx.pay({ id, price });
break;
case 'alipay': alipay.pay({ id, price });
break;
case 'jd': jd.pay({ id, price });
break;
case 'xxx': xxx.toPay({ goodsId: id, price });
break;
}
}
pay(101, 1000);
装饰者
写代码的时候,我们总遵循"组合优于继承",而装饰者模式就是一种用组合关系来组织代码。而我们平时所说的装饰器就是装饰者的一种应用。
这个人原先普普通通,经过装饰者模式改造,瞬间变成人见人爱的帅哥。
function people(height, weight, character) {
this.height = 170;
this.weight = 80;
this.character = 'normal';
return this;
}
const xiaowang = people();
console.log(xiaowang.character); // normal
function decorate(ctr) {
ctr.height = 180;
ctr.weight = 70;
ctr.character = 'handsome';
return ctr;
}
const wang = decorate(people);
console.log(wang.character); // handsome
注:这个例子演示的是"往对象上附加职责"的思路。
people()以普通函数调用时this指向全局对象(非严格模式),实际项目中请传参或返回新对象,避免污染全局。
代理
它在不改变原始类代码的情况下,通过引入代理类来给原始类附加功能。前端最常听到的代理就是 nginx 代理,它其实是代理的一个应用,把自身作为代理服务器将资源请求转发到终端服务器。在 JS 中比较典型的代理有图片懒加载,合并 http 请求,以及缓存计算结果。
下面是一个图片懒加载的例子,我们先加载默认图片,等真实图片加载完之后再替换默认图片。
const createImage = (function() {
const img = document.createElement('img');
document.body.appendChild(img);
return function(src) {
img.src = src;
};
})();
const proxyImage = function(fn) {
const image = new Image();
const defaultImg = 'https://rs.vip.miui.com/vip-resource/prod/mio/v136/static/media/lazyLoad.a10ffbd7.png';
return function(src) {
fn(defaultImg);
// 这里加一个延迟,可以更好的看到图片替换的过程。
setTimeout(function() {
image.src = src;
image.onload = function() {
fn(src);
};
}, 2000);
};
};
const proxy = proxyImage(createImage);
proxy('https://pic1.zhimg.com/80/v2-ec33fcec249a9cabab61b14436432bf0_r.jpg');
注:需浏览器环境。现代浏览器已有原生
loading="lazy"属性和IntersectionObserver实现懒加载,代理模式的思路则适用于一切"在访问前后包一层"的场景——JS 还提供了语言级的Proxy对象(见文末)。
外观
也叫门面模式,GoF 上的定义是,外观模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。它和适配器模式类似:适配器是做接口转换,解决的是原接口和目标接口不匹配的问题,而外观模式做接口整合,解决多接口带来的调用问题。
比如下面的获取用户信息的接口。
// API 为示意对象
const getUserBaseInfo = () => API.getUserBaseInfo();
const getUserPriority = () => API.getUserPriority();
const getUserCustomContent = () => API.getUserCustomContent();
// 对外提供一个总的接口
async function getUserInfo() {
const baseInfo = await getUserBaseInfo();
const priority = await getUserPriority();
const customContent = await getUserCustomContent();
return { ...baseInfo, ...priority, ...customContent };
}
桥接
在《设计模式》中解释为将抽象和实现解耦,让它们可以独立变化。比如 H5 开发过程中,就通过 JsBridge 桥,实现 JS 和 Native 的双向通信,这里桥不做逻辑处理,只是收集双方的请求,再转发,桥实现了抽象,具体实现交给 JS 和 Native。
举一个简单的例子,比如某个男生想要某个女生的号码,但是他不好意思,就通过共同朋友将他们桥接起来,最后那个男生对共同好友说了谢谢,很有礼貌。
function getNumber(number, callback) {
callback('thank you');
}
function sendNumber() {
return '13***********';
}
function bridge() {
const number = sendNumber();
getNumber(number, (feedback) => {
console.log(feedback);
});
}
bridge(); // thank you
我们也可以进行抽象,通过一个 bridge 函数,实现双方通信。
function bridge(send, receiveFn, sendFn) {
const receive = receiveFn(send);
sendFn(receive);
}
const send = 'hello';
function receiveFn(send) {
if (send === 'hello') {
return '我们不合适';
}
return '我们可以聊聊';
}
bridge(send, receiveFn, receive => {
console.log(receive);
}); // 我们不合适
组合
不是我们平时说的组合关系。它规定了数据类型必须是树型结构,并且表示"部分-整体"的层次结构,是用来处理单个对象和组合对象之间的关系。
我们通过一个扫描文件的例子来说明。
class Folder {
constructor(name) {
this.name = name;
this.files = [];
}
add(file) {
this.files.push(file);
}
scan() {
console.log('开始扫描文件夹: ' + this.name);
this.files.forEach(file => {
file.scan();
})
}
};
class File {
constructor(name) {
this.name = name;
}
add(file) {
throw new Error('不能往文件里添加东西,' + file);
}
scan() {
console.log('开始扫描文件: ' + this.name);
}
}
const rootFolder = new Folder('根目录');
const folder1 = new Folder('一级目录');
const folder2 = new Folder('二级目录');
const file = new File('根文件');
const file1 = new File('一级文件');
const file2 = new File('二级文件');
const file3 = new File('也是二级文件');
rootFolder.add(folder1);
folder1.add(folder2);
rootFolder.add(file);
folder1.add(file1);
folder2.add(file2);
folder2.add(file3);
rootFolder.scan();
注:
File是浏览器的内置全局类名,真实项目中请换个名字避免遮蔽。
享元
应用于大量相似对象的系统。一般是借用工厂模式,新建一个对象,然后其他对象共享这个工厂对象,避免新建对象。享元模式是一种用时间换空间的优化模式,避免性能损耗。
享元模式的代码比较好理解,因为衣服的型号就那么几种,我们可以通过身高判断衣服类型(忽略贾玲和宋一茜),所以衣服类型就可以作为一个共享对象。
const selectClothes = (function() {
const clothesType = {
160: 's',
170: 'l',
175: 'xl',
180: 'xxl',
181: 'xxxl'
};
return function(height) {
if (height < 170) {
return clothesType[160];
} else {
// 后面的代码省略
return clothesType[175];
}
}
})();
class People {
constructor(height, weight) {
this.height = height;
this.weight = weight;
this.clothesType = '';
}
}
const people1 = new People(160, 100);
const people2 = new People(170, 150);
people1.clothesType = selectClothes(people1.height);
people2.clothesType = selectClothes(people2.height);
行为型
策略
定义一系列的算法,把它们一个个封装起来,并且可以相互替换,这就是策略模式。要使用策略模式,必须了解所有的 strategy,必须了解各个 strategy 之间的不同点,这样才能选择一个合适的 strategy。比如,我们要选择一种合适的旅游出行路线,必须先了解选择飞机、火车、自行车等方案的细节。此时 strategy 要向客户暴露它的所有实现,这是违反最少知识原则的。
我直接使用《JavaScript 设计模式和开发实战》中的一个例子。这是一个计算不同绩效的人对应不同的奖金(奖金 = 工资 × 对应的绩效算法)。
const strategies = {
S: function(salary) {
return salary * 4;
},
A: function(salary) {
return salary * 3;
},
B: function(salary) {
return salary * 2
}
}
const calculateBonus = function(level, salary) {
return strategies[level](salary);
}
const staff1 = calculateBonus('S', 10000);
const staff2 = calculateBonus('A', 20000);
console.log(staff1, staff2); // 40000 60000
模板
将公共的代码抽成一个抽象类,子类继承抽象类,并重写相应的方法。模板方法模式是为数不多的基于继承的设计模式。
因为 JS 中无法实现抽象类,我直接用 TS 实现这个例子。
如果你对 TS 还不了解,可以看这篇文章:《TypeScript开发实战》总结。
abstract class Beverage {
init() {
this.boilWater();
this.brew();
this.pourInCup();
this.addCondiments();
}
boilWater() {
console.log('把自来水煮沸');
}
abstract brew(): void;
abstract pourInCup(): void;
abstract addCondiments(): void;
}
class Tea extends Beverage {
brew() {
console.log('用沸水浸泡茶叶');
}
pourInCup() {
console.log('把茶倒进杯子里');
}
addCondiments() {
console.log('加点糖');
}
}
class Coffee extends Beverage {
brew() {
console.log('用沸水浸泡咖啡');
}
pourInCup() {
console.log('把咖啡倒进杯子里');
}
addCondiments() {
console.log('加点牛奶');
}
}
const tea = new Tea();
tea.init();
const coffee = new Coffee();
coffee.init();
观察者
又称发布-订阅模式,它定义对象间的一种一对多的依赖关系。主要用于异步编程。JavaScript 本身也是一门基于事件驱动的语言,也利用了发布订阅模式。
下面是 JS 中自定义事件,它就是一个典型的观察者模式。
const msg = new Event('message');
window.addEventListener('message', function() {
console.log('我接收到了消息');
});
window.dispatchEvent(msg);
我们来手写一个观察者模式,下面这个实现比较简陋,边界处理很粗糙。
class MyEvent {
constructor() {
this.events = [];
}
on(fn) {
this.events.push(fn);
}
emit(data) {
this.events.forEach(fn => {
fn(data);
})
}
// off方法我这里就不实现了,也比较简单
off () {}
}
const event = new MyEvent();
event.on(function(data) {
console.log('我是第一个注册事件', data);
});
event.on(function(data) {
console.log('我是第二个注册事件', data);
});
event.emit('已发送');
注:重命名为
MyEvent以避免遮蔽浏览器内置的Event构造器。
迭代器
是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。分为内部迭代器和外部迭代器。
像我们大部分迭代器都是内部迭代器。比如 forEach,map,filter 等。而外部迭代器,迭代的控制权在外部。
const Iterator = function(obj){
let current = 0;
const next = function(){
console.log(obj[current]);
current++;
};
const isDone = function(){
return current >= obj.length;
};
const getCurItem = function(){
return obj[current];
};
return {
next: next,
isDone: isDone,
getCurItem: getCurItem
}
};
const iterator = Iterator([1, 2, 3]);
iterator.next(); // 1
iterator.next(); // 2
iterator.next(); // 3
注:ES2015 起迭代器已标准化为语言协议(
Symbol.iterator+ 生成器函数),for...of、展开运算符背后都是它,见文末。
中介者
定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。主要解决各个类之间关系复杂,且每个类都需要知道它要交互的类。这个时候就可以引入中介者,把脏活累活,耦合关系全放到中介者类中,我们其他类负责貌美如花。
下面是一个机场指挥塔的例子,它就是一个中介者,所有飞机想要获取航道信息都需要向指挥塔请求。
class PlaneCommandTower {
constructor(channel) {
this.channel = new Array(channel).fill(false);
}
enter(i) {
if (this.channel[i] === true || (i + 1 > this.channel.length)) {
throw new Error('这个航道已经被占用了');
}
console.log('航道预占成功');
this.channel[i] = true;
}
leave(i) {
this.channel[i] = false;
}
isBusy(i) {
return this.channel[i];
}
getChannelCnt() {
return this.channel.length;
}
}
class Plane {
constructor(commander) {
this.commander = commander;
this.channelCnt = this.commander.getChannelCnt();
}
land() {
let i = 0;
while(this.commander.isBusy(i)) {
++i;
if (i > this.channelCnt) {
break;
}
}
if (i < this.channelCnt) {
this.commander.enter(i);
} else {
console.log('航道已经被占满了');
return 'wait';
}
}
}
const commander = new PlaneCommandTower(3);
const plane1 = new Plane(commander);
const plane2 = new Plane(commander);
const plane3 = new Plane(commander);
const plane4 = new Plane(commander);
plane1.land();
plane2.land();
plane3.land();
plane4.land();
状态
状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变。举个例子,有一个电灯,电灯上面只有一个开关。当电灯开着的时候,此时按下开关,电灯会切换到关闭状态;再按一次开关,电灯又将被打开。同一个开关按钮,在不同的状态下,表现出来的行为是不一样的。
我们以灯的例子,扩展一下,比较高级的灯,不止开关两种状态,它有弱光,正常光,环保光,关四种状态。
class Light {
constructor() {
this.status = ['opened', 'closed'];
this.curStatus = -1;
}
setStatus(status) {
this.status = status;
}
press() {
this.curStatus = (this.curStatus + 1 === this.status.length) ? 1 : this.curStatus + 1;
console.log(this.status[this.curStatus]);
}
}
const light = new Light();
light.setStatus(['weak', 'common', 'environmental', 'closed']);
light.press(); // weak
light.press(); // common
light.press(); // environmental
light.press(); // closed
注:这个简化实现里,走完一轮后下一次
press()会从下标 1(common)重新开始而不是回到 weak,因为三目分支写死回了 1;生产实现请用this.curStatus = (this.curStatus + 1) % this.status.length。
职责链
定义:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。举个例子,如果早高峰能顺利挤上公交车的话,那么估计这一天都会过得很开心。因为公交车上人实在太多了,经常上车后却找不到售票员在哪,所以只好把两块钱硬币往前面递。除非你运气够好,站在你前面的第一个人就是售票员,否则,你的硬币通常要在 N 个人手上传递,才能最终到达售票员的手里。
在 JS 中,无论是作用域链、原型链,还是 DOM 节点中的事件冒泡,我们都能从中找到职责链模式的影子。
假设我们负责一个售卖手机的电商网站,经过分别交纳 500 元定金和 200 元定金的两轮预定后(订单已在此时生成),现在已经到了正式购买的阶段。公司针对支付过定金的用户有一定的优惠政策。在正式购买后,已经支付过 500 元定金的用户会收到 100 元的商城优惠券,200 元定金的用户可以收到 50 元的优惠券,而之前没有支付定金的用户只能进入普通购买模式。
注:原文漏掉了
order500、order200、orderNormal三个处理函数的定义,下面按《JavaScript 设计模式与开发实践》原书的语义补全(参数依次为订单类型、是否已支付定金、库存)。
const order500 = function (orderType, pay, stock) {
if (orderType === 1 && pay === true) {
console.log('500 元定金预购,得到 100 元优惠券');
} else {
return 'nextSuccessor';
}
};
const order200 = function (orderType, pay, stock) {
if (orderType === 2 && pay === true) {
console.log('200 元定金预购,得到 50 元优惠券');
} else {
return 'nextSuccessor';
}
};
const orderNormal = function (orderType, pay, stock) {
if (stock > 0) {
console.log('普通购买,无优惠券');
} else {
console.log('手机库存不足');
}
};
class Chain {
constructor(fn) {
this.fn = fn;
this.successor = null;
}
setNextSuccessor(successor) {
return this.successor = successor;
}
passRequest() {
const ret = this.fn.apply(this, arguments);
if (ret === 'nextSuccessor'){
return this.successor && this.successor.passRequest.apply(this.successor, arguments);
}
return ret;
}
}
const chainOrder500 = new Chain(order500);
const chainOrder200 = new Chain(order200);
const chainOrderNormal = new Chain(orderNormal);
chainOrder500.setNextSuccessor(chainOrder200);
chainOrder200.setNextSuccessor(chainOrderNormal);
chainOrder500.passRequest(1, true, 500); // 500 元定金预购,得到 100 元优惠券
chainOrder500.passRequest(2, true, 500); // 200 元定金预购,得到 50 元优惠券
chainOrder500.passRequest(3, true, 500); // 普通购买,无优惠券
chainOrder500.passRequest(1, false, 0); // 手机库存不足
命令
命令模式中的命令指的是一个执行某些特定事情的指令。命令模式最常见的应用场景是:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。
下面通过一个按钮的点击事件,触发页面的刷新事件(需浏览器环境运行)。
const button1 = document.createElement('button');
document.body.appendChild(button1);
const setCommand = function(button, func) {
button.onclick = function() {
func();
};
};
const MenuBar = {
refresh: function() {
console.log("刷新菜单界面");
}
};
const RefreshMenuBarCommand = function(receiver) {
return function() {
receiver.refresh();
};
};
const refreshMenuBarCommand = RefreshMenuBarCommand(MenuBar);
setCommand(button1, refreshMenuBarCommand);
button1.click(); // 刷新菜单界面
访问者
在访问者模式中,我们使用了一个访问者类,它改变了元素类的执行算法。通过这种方式,元素的执行算法可以随着访问者改变而改变。其次,必须定义一个访问者类,并且内部有 visit 方法。元素的执行算法实现 accept 方法,而内部通常都是 visitor.visit(this)。
function Visitor() {
this.visit = function (v) {
console.log('the computer type is ' + v.type);
}
}
function ComputerTypeVisitor(type) {
this.type = type;
this.accept = function (visitor) {
visitor.visit(this);
}
}
const visitor = new ComputerTypeVisitor('dell');
visitor.accept(new Visitor()); // the computer type is dell
备忘录
定义:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样就可以将该对象恢复到原先保存的状态。
// Ajax 与 render 为示意,表示发请求与渲染页面,此处未实现
const Page = function () {
let page = 1,
cache = {},
data;
return function (page) {
if (cache[page]) {
data = cache[page];
render(data);
} else {
Ajax.send('cgi.xx.com/xxx', function (data) {
cache[page] = data;
render(data);
});
}
}
}();
解释器
它是一种特殊的设计模式,它建立一个解释器,对于特定的计算机程序设计语言,用来解释预先定义的文法。通俗点,你通过函数名的定义就能知道程序即将要执行的过程。
function Context() {
let sum;
let list = [];
this.getSum = function () {
return sum;
}
this.setSum = function (_sum) {
sum = _sum;
}
this.add = function (eps) {
list.push(eps);
}
this.getList = function () {
return list;
}
}
function PlusExpression() {
this.interpret = function (context) {
let sum = context.getSum();
sum++;
context.setSum(sum);
}
}
function MinusExpression() {
this.interpret = function (context) {
let sum = context.getSum();
sum--;
context.setSum(sum);
}
}
const context = new Context();
context.setSum(20);
// 运行加法三次
context.add(new PlusExpression());
context.add(new PlusExpression());
context.add(new PlusExpression());
// 运行减法两次
context.add(new MinusExpression());
context.add(new MinusExpression());
const list = context.getList();
for (let i = 0; i < list.length; i++) {
const expression = list[i];
expression.interpret(context);
}
console.log("打印输出结果:" + context.getSum()); // 打印输出结果:21
2025 视角:语言进化吃掉了哪些模式
写于多年前的这篇文章里的示例,有一部分今天已经有原生替代品,面试和实际编码时可以补充这些说法:
- 单例:ESM 模块本身就是单例(模块只求值一次),
export const instance = createXxx()是最常见的写法;不再需要手写singleton高阶函数。 - 代理:ES2015 的
Proxy+Reflect是语言级代理,Vue 3 响应式系统(reactive)、各种校验/日志/懒加载中间件都用它实现,比手写代理函数通用得多。 - 装饰者/装饰器:JS 的装饰器提案历经多次设计,现处于 TC39 Stage 3(
@decorator语法),Angular、NestJS、TypeScript(5.0 起支持标准版)中已大量使用;在语法落地前,高阶函数/高阶组件(HOC)一直是 JS 里最自然的"装饰"方式。 - 观察者:浏览器有
EventTarget/addEventListener,Node 有EventEmitter;响应式编程库 RxJS 是观察者+迭代器(Observer + Iterator)的组合;近几年各框架的 Signal/响应式原语(Vue 的ref、Solid 的 signal、TC39 Signals 提案)本质仍是"依赖收集 + 通知更新"的观察者模型。 - 迭代器:
Symbol.iterator协议 + 生成器函数(function*)把外部迭代器标准化了,for...of、展开、解构、Array.from全部基于该协议;ES2025 又补上了迭代器帮助方法(.map()/.filter()/.take()等)。 - 策略:在 JS 里一个对象字面量或
Map就是策略表,配合函数是一等公民,策略模式几乎"消失"在语言惯用法里——这也正是 GoF 模式在动态语言中的普遍命运:模式没有被抛弃,而是变得不再需要仪式感。 - 职责链:中间件架构(Koa/Express/Redux 的
compose)是职责链的现代形态;Promise 链式then也带有它的影子。
结束语
文章中的内容如果有错误,请及时指出。毕竟将面向对象标准的设计模式迁移到 JS 中,可能会存在使用不当的情况。文章中的好几个例子都来自《JavaScript 设计模式与开发实践》。
参考资料:《JavaScript 设计模式与开发实践》(曾探),《设计模式之美》专栏(王争,极客时间),《设计模式:可复用面向对象软件的基础》(GoF)