JavaScript 中的设计模式

2025-05-28

JavaScript 中的设计模式

最近,一位开发人员朋友问我在工作中使用了哪些设计模式。这个问题让我有点困惑,因为我实际上并不知道答案,尽管我知道我确实使用过一些模式。这些模式有名字吗?为什么我用的那些模式比其他的更好?

今天的文章,我会记录我学习各种设计模式以及如何在 JavaScript 中实现它们的笔记和想法。欢迎大家一起来探索我的设计之路,或者嘲笑我的菜鸟身份,因为我发现了一些我感觉早就应该学的东西。(迟学总比不学好,对吧?)

仅供参考:这篇文章的主要资源之一是Lambda Test 的这篇博文

什么是设计模式?

根据维基百科,软件设计模式是......

在软件设计中,针对特定情境下常见问题的通用且可复用的解决方案。它并非可以直接转换为源代码或机器码的成品设计,而是一种可用于多种不同情况的解决问题的描述或模板。设计模式是形式化的最佳实践,程序员可以在设计应用程序或系统时用它来解决常见问题。

Lambda Test 文章还指出,它们“代表了面向对象软件开发人员随着时间的推移所采用的久经考验的解决方案和最佳实践”。

2021 JavaScript 设计模式

JavaScript 设计模式图 - 以上信息的视觉呈现
来源

根据 Lambda Test 的说法,并非所有模式在 JavaScript 中都是必需的,因为 JavaScript 本身就有一些原生特性可以帮我们实现它们。因此,本文我将只讨论一部分设计模式。如果您希望我补充一些我遗漏的模式,欢迎给我留言。

注意:以下所有引用的定义均来自维基百科的软件设计模式条目

构造函数/建造者模式 - 创建型设计

将复杂对象的构造与其表示分离,允许相同的构造过程创建各种表示。

好吧,我以前确实用构造函数设计模式写过 JavaScript 代码!我猜我只是以为它是 JavaScript 中类的一个特性;我从来不知道它也能算作一种“设计模式”。

class Person {
  constructor(name, age, mother) {
    this.name = name;
    this.age = age;
    this.mother = mother;
  }
}

const tim = new Person('Tim', 31, null);
const tina = new Person('Tina', 57, null);

tim.mother = tina;

console.log(tim);

const grandma = new Person('Sherry', 80, null);

tina.mother = grandma;

console.log(tim);
Enter fullscreen mode Exit fullscreen mode

工厂模式 - 创建型设计

定义一个用于创建单个对象的接口,但让子类决定实例化哪个类。工厂方法允许将类的实例化推迟到子类。

所以,基于 Lambda 测试文章中该模式的实现方式,这个名字似乎很完美。它实际上是一种模式,你只需设置一个函数,该函数可以接受各种参数并返回相应的对象。就像你问服装厂要一件衬衫,他们就会给你一件衬衫;问裤子,他们就会给你裤子;问鞋子,你就会得到鞋子。每个对象都有其自身的功能。

function animalFactory() {
  this.createAnimal = function(animalType) {
    let animal;

    switch(animalType) {
      case 'dog':
        animal = new Dog();
        break;
      case 'cat':
        animal = new Cat();
        break;
      case 'horse':
        animal = new Horse();
        break;
      default:
        animal = new Monkey();
        break;
    }

    return animal;
  }
}

const Dog = function() {
  this.makeSound = () => {
    console.log('woof woof!');
  }
}

const Cat = function() {
  this.makeSound = () => {
    console.log('prrr prrr meow!');
  }
}

const Horse = function() {
  this.makeSound = () => {
    console.log('neeeighhh!')
  }
}

const Monkey = function() {
  this.makeSound = () => {
    console.log('ooooh ahh oooh oooh!');
  }
}

const factory = new animalFactory();

const jojo = factory.createAnimal('dog');
jojo.makeSound();

const smokey = factory.createAnimal('cat');
smokey.makeSound();

const princess = factory.createAnimal('horse');
princess.makeSound();

const kong = factory.createAnimal('monkey');
kong.makeSound();
Enter fullscreen mode Exit fullscreen mode

原型模式 - 创造性设计

使用原型实例指定要创建的对象类型,并从现有对象的“骨架”创建新对象,从而提高性能并将内存占用降至最低。

我之前很少接触原型设计,所以很想深入研究一下,尤其因为我知道这是 JavaScript 的一个很棒的特性。说实话,一开始我对此很困惑,直到我重读了 Lambda 测试那篇文章中的相关章节,才意识到它其实就是关于 cloning 的。在尝试了下面的实现之后,我终于搞清楚了,并且真正理解了它。

const macBook = {
  color: 'silver',
  turnOn() {
    console.log('turning on...');
  },
  turnOff() {
    console.log('turning off...');
  }
}

// Proper prototype cloning
const myComputer = Object.create(macBook, { owner: { value: 'Tim'} });
console.log(myComputer.__proto__ === macBook);

// Not a prototype copy
const newComputer = {...macBook, owner: 'John'};
console.log(newComputer.__proto__ === macBook);

macBook.power = 'USB-C';

// The protoype gets the new value for 'power'
console.log(myComputer.power);
// But the non-prototype doesn't
console.log(newComputer.power);
Enter fullscreen mode Exit fullscreen mode

单例模式/严格模式 - 创造型设计

确保一个类只有一个实例,并提供对它的全局访问点。

所以这个方法确保只有一个实例存在。如果尝试创建另一个实例,它只会返回一个已存在实例的引用。很容易理解。

const Database = (function () {
  let instance;

  function createDatabaseInstance() {
    return new Object('Database Instance');
  }

  function getDatabaseInstance() {
    if (!instance) {
      instance = createDatabaseInstance();
    }

    return instance;
  }

  return { getDatabaseInstance }
})();

const databaseInstance1 = Database.getDatabaseInstance();
const databaseInstance2 = Database.getDatabaseInstance();

console.log(databaseInstance1 === databaseInstance2);
Enter fullscreen mode Exit fullscreen mode

适配器模式/包装器模式 - 结构设计

将一个类的接口转换为客户端期望的另一个接口。适配器使原本因接口不兼容而无法协同工作的类能够协同工作。企业集成模式中的对应角色是转换器。

我在国外生活了一年,适配器的概念很容易理解。它看起来可能是一种将遗留代码中的函数连接到新代码库中的函数的有效方法?

在 YouTube 上发现了一个非常棒的视频,它完美地解释了适配器模式的一个用例,并利用它创建了下面的实现,将两个不同的 NPM 库合并成一个实用程序。我写的代码不容易复制粘贴到这篇文章中,所以欢迎在我的 Github 上查看代码

我完全可以想象到这种设计模式的大量用例,并且非常期待将来用它来让我的代码更易于维护!这大概是我目前为止最喜欢的设计模式了 =)

复合模式 - 结构设计

将对象组合成树形结构,以表示“部分-整体”的层次结构。Composite 允许客户端统一处理单个对象及其组合。

嘿,只是节点和组件而已!我当然知道这个设计模式:一个父节点,多个子节点。而且这些子节点可以有多个子节点。这是 JSON 和 HTML 树中使用的一对多模式。现在我知道这个设计模式的正式名称了 =)

const Node = function(name) {
  this.children = [];
  this.name = name;
};

Node.prototype = {
  add: function(child) {
    this.children.push(child);
    return this;
  }
}

// recursive console log of what's inside of the tree
const log = (root) => {
  if (!root) return;

  console.log('');
  console.log(`---Node: ${root.name}---`);
  console.log(root);

  root.children.forEach(child => {
    if(child?.children.length) {
      log(child);
    }
  });
}

const init = () => {
  const tree = new Node('root');
  const [left, right] = [new Node("left"), new Node("right")];
  const [leftleft, leftright] = [new Node("leftleft"), new Node("leftright")];
  const [rightleft, rightright] = [new Node("rightleft"), new Node("rightright")];

  tree.add(left).add(right);
  left.add(leftleft).add(leftright);
  right.add(rightleft).add(rightright);

  log(tree);
}

init();
Enter fullscreen mode Exit fullscreen mode

模块模式 - 结构设计

将几个相关元素(例如类、单例、方法、全局使用)分组为单个概念实体。

这种设计模式让我想起了我学习 Java 时的情景:将函数私有化,只暴露必要的函数和变量。在我的职业生涯中,我确实多次使用过这种设计模式。很高兴知道它有个名字……

const userApi = () => {
  // private variables
  const users = [];

  // private function
  const addUser = (name) => {
    users.push(name);

    return users[users.length -1];
  }

  // private function
  const getAllUsers = () => {
    return users;
  }

  // private function
  const deleteUser = (name) => {
    const userIndex = users.indexOf(name);

    if (userIndex < 0) {
      throw new Error('User not found');
    }

    users.splice(userIndex, 1);
  }

  // private function
  const updateUser = (name, newName) => {
    const userIndex = users.indexOf(name);

    if (userIndex < 0) {
      throw new Error('User not found');
    }

    users[userIndex] = newName;

    return users[userIndex];
  }

  return {
    // public functions
    add: addUser,
    get: getAllUsers,
    del: deleteUser,
    put: updateUser
  }
}

const api = userApi();

api.add('Tim');
api.add('Hina');
api.add('Yasmeen');
api.add('Neeloo');

console.log(api.get());

api.del('Yasmeen');

console.log(api.get());

api.put('Tim', 'Tim Winfred');

console.log(api.get());
Enter fullscreen mode Exit fullscreen mode

装饰器模式 - 结构设计

在保持接口不变的情况下,动态地将附加职责附加到对象上。装饰器提供了一种灵活的替代子类化方法来扩展功能。

好吧,这个方法既有趣又容易理解。我可以设置一个函数,赋予一个对象通用的功能和特性,然后我可以用它自己的功能和特性来“装饰”该对象的每个实例。我绝对可以预见自己将来会用到这个方法。

const Animal = function(type) {
  this.type = type || 'dog';
}

const dog = new Animal('dog');
const cat = new Animal('cat');

dog.bark = function() {
  console.log('woof woof!');
  return this;
}

cat.meow = function() {
  console.log('meow meooooooow!');
  return this;
}

dog.bark();
cat.meow();
Enter fullscreen mode Exit fullscreen mode

外观模式 - 结构设计

为子系统中的一组接口提供统一的接口。Facade定义了一个更高级别的接口,使子系统更易于使用。

我其实对 Facade 的用法相当熟悉。我目前正在做的一个项目就用过一个预先构建好的 Facade。(它甚至以 为名导入了facade,但我当时根本不知道这是一种“设计模式”。)

我发现这个关于 JavaScript 外观设计的视频非常有帮助。(这个视频的主持人还有很多非常有用的视频。)

import axios from 'axios';

function getUsers() {
  return facade.get('https://jsonplaceholder.typicode.com/users');
}

function getUserById(id) {
  return facade.get('https://jsonplaceholder.typicode.com/users', { id });
}

const facade = {
  get: function(url, params) {
    return axios({
      url,
      params,
      method: 'GET'
    }).then(res => res.data);
  }
};

async function getData() {
  try {
    console.time('getUsers took');
    const users = await getUsers();
    console.timeEnd('getUsers took');
    console.log(`There are ${users.length} users`);

    console.time('getUserById took');
    const user1 = await getUserById(1);
    console.timeEnd('getUserById took');
    console.log(user1);
  } catch (error) {
    console.log(error);
  }
}

getData();
Enter fullscreen mode Exit fullscreen mode

代理模式 - 结构设计

为另一个对象提供代理或占位符以控制对它的访问。

作为《万智牌》玩家,这个在理论上是有道理的:如果可以使用代理卡,就不要使用昂贵的卡。

我发现这个评价很高的 YouTube 视频,它介绍了代理设计模式以及如何在 JavaScript 中使用它来创建缓存,以节省时间并减少访问外部 API 的次数。

// Mock External API
function CryptoCurrencyAPI() {
  this.getValue = function(coin) {
    console.log(`Calling Crypto API to get ${coin} price...`);

    switch(coin.toLowerCase()) {
      case 'bitcoin':
        return 38000;
      case 'ethereum':
        return 2775;
      case 'dogecoin':
        return 0.39;
    }
  }
}

function CryptoCurrencyAPIProxy() {
  this.api = new CryptoCurrencyAPI();
  this.cache = {};

  this.getValue = function(coin) {
    if(!this.cache[coin]) {
      console.log(`The value of ${coin} isn't stored in cache...`);
      this.cache[coin] = this.api.getValue(coin);
    }

    return this.cache[coin];
  }
}

const proxyAPI = new CryptoCurrencyAPIProxy();

console.log(proxyAPI.getValue('Bitcoin'));
console.log(proxyAPI.getValue('Bitcoin'));
console.log(proxyAPI.getValue('Ethereum'));
console.log(proxyAPI.getValue('Ethereum'));
console.log(proxyAPI.getValue('Dogecoin'));
console.log(proxyAPI.getValue('Dogecoin'));
Enter fullscreen mode Exit fullscreen mode

责任链模式 - 行为设计

通过让多个对象都有机会处理请求,避免请求的发送者和接收者之间的耦合。将接收对象串联起来,并沿着这个链条传递请求,直到有一个对象处理它。

这是另一种我从理论上肯定理解并且可以与许多现实世界场景联系起来的模式,但除了 Lambda Test 文章中给出的示例之外,我无法立即想到编码的实现。

玩了一会儿之后,我的确很喜欢这个设计模式,并且享受方法链的便捷。你觉得我写的东西怎么样?我不太确定它是否符合维基百科上“对象”的定义……

const ATM = function() {
  this.withdrawl = function(amount) {
    console.log(`Requesting to withdrawl $${amount.toFixed(2)}`);

    if (amount % 1 !== 0) {
      console.log('Sorry, this ATM can\'t dispense coins. Please request another amount.');
      return;
    }

    const dispenseOutput = {};

    // chain or responsibility function
    function get(bill) {
      dispenseOutput[bill] = Math.floor(amount / bill);
      amount -= dispenseOutput[bill] * bill;

      return { get };
    }

    get(100).get(50).get(20).get(10).get(5).get(1);

    this.dispense(dispenseOutput);
  };

  this.dispense = function(bills) {
    console.log('--- Dispensing cash ---')
    Object.entries(bills).forEach(([key, value]) => {
      console.log(`- Dispensing ${value} $${key} bills`);
    });
  }
};

const myATM = new ATM();

myATM.withdrawl(126.10);
myATM.withdrawl(1287);
myATM.withdrawl(879);
Enter fullscreen mode Exit fullscreen mode

命令模式 - 行为设计

将请求封装为对象,从而允许对具有不同请求的客户端进行参数化,并对请求进行排队或记录。它还支持可撤消的操作。

这个看起来相当简单,而且我非常喜欢命令对象和接收器的分离。这使得代码简洁。我最近一直在尝试用 React 创建一个计算器,所以这真是个完美的解决方案!

const calculationMethods = {
  add: function(x, y) {
    return x + y;
  },
  subtract: function(x, y) {
    return x - y;
  },
  multiply: function(x, y) {
    return x * y;
  },
  divide: function(x, y) {
    return x / y;
  }
};

const calculator = {
  execute: function(method, num1, num2) {
    if (!(method in calculationMethods)) return null;

    return calculationMethods[method](num1, num2);
  }
};

console.log(calculator.execute('add', 1, 2));
console.log(calculator.execute('subtract', 5, 2));
console.log(calculator.execute('multiply', 11, 2));
console.log(calculator.execute('divide', 10, 2));
console.log(calculator.execute('square root', 20));
Enter fullscreen mode Exit fullscreen mode

观察者(发布/订阅)模式 - 行为设计

定义对象之间的一对多依赖关系,其中一个对象的状态变化会导致其所有依赖对象都自动收到通知和更新。

我立刻想到,这让我想到了 JavaScript 中的“可观察对象”,我之前很少(至少有一段时间了)接触它。这种模式的发布/订阅理念也让我想到了 WebSocket 和 Redux。

在下面的实现中,我使用了 freeCodeCamp 视频中的示例,该视频对观察者模式进行了很好的解释。我稍微调整了一下,实现了一些方法链:

function Subject() {
  this.observers = [];
}

Subject.prototype = {
  subscribe: function(observer) {
    this.observers.push(observer);

    return this;
  },

  unsubscribe: function(observer) {
    const indexOfObserver = this.observers.indexOf(observer);
    if (indexOfObserver > -1) {
      this.observers.splice(indexOfObserver, 1);
    }

    return this;
  },

  notifyObserver: function(observer) {
    const indexOfObserver = this.observers.indexOf(observer);
    if (indexOfObserver > -1) {
      this.observers[indexOfObserver].notify();
    }

    return this;
  },

  notifyAllObservers: function() {
    this.observers.forEach(observer => {
      observer.notify();
    });

    return this;
  }
}

function Observer(name) {
  this.name = name;
}

Observer.prototype = {
  notify: function() {
    console.log(`Observer ${this.name} has been notified`);
  }
};

const subject = new Subject();

const observer1 = new Observer('user001');
const observer2 = new Observer('user002');
const observer3 = new Observer('user003');
const observer4 = new Observer('user004');
const observer5 = new Observer('user005');

subject.subscribe(observer1).subscribe(observer2).subscribe(observer3).subscribe(observer4).subscribe(observer5);
subject.notifyObserver(observer4);
subject.unsubscribe(observer4);
subject.notifyAllObservers();
Enter fullscreen mode Exit fullscreen mode

模板方法模式 - 行为设计

定义操作中算法的框架,将某些步骤推迟到子类。模板方法允许子类重新定义算法的某些步骤,而无需更改算法的结构。

好吧,根据这个描述我肯定没理解,哈哈。听起来像是在扩展类,然后给新的子类添加新的方法,但我肯定得看看能不能找到更好的描述……

与这种模式的定义不同,微软对模板方法模式的解释帮助我理解了发生了什么(尽管他们没有使用 JavaScript),这篇博客文章也是如此。


class HouseTemplate {
  constructor(name, address) {
    this.name = name;
    this.address = address;
  }

  buildHouse() {
    this.buildFoundation();
    this.buildPillars();
    this.buildWalls();
    this.buildWindows();
    console.log(`${ this.name } has been built successfully at ${ this.address }`);
  }

  buildFoundation() {
    console.log('Building foundation...');
  }

  buildPillars() {
    throw new Error('You have to build your own pillars');
  }

  buildWalls() {
    throw new Error('You have to build your own walls');
  }

  buildWindows() {
    console.log('Building windows');
  }
}

class WoodenHouse extends HouseTemplate {
  constructor(name, address) {
    super(name, address);
  }

  buildPillars() {
    console.log('Building pillars for a wooden house');
  }

  buildWalls() {
    console.log('Building walls for a wooden house');
  }
}

class BrickHouse extends HouseTemplate {
  constructor(name, address) {
    super(name, address);
  }

  buildPillars() {
    console.log('Building pillars for a brick house');
  }

  buildWalls() {
    console.log('Building walls for a brick house');
  }
}

const woodenHouse = new WoodenHouse('Wooden house', '123 Maple Road');
const brickHouse = new BrickHouse('Brick house', '456 Stone Lane');

woodenHouse.buildHouse();
brickHouse.buildHouse();
Enter fullscreen mode Exit fullscreen mode

策略模式 - 行为设计

定义一系列算法,将每个算法封装起来,并使它们可以互换。策略允许算法根据使用它的客户端而独立变化。

好了,这是最后一个了!而且,谢天谢地,多亏了 Lambda 测试文章,它超级容易理解 =) 我欣赏任何允许 DRY 编码的方法,并且完全可以想象未来会有各种各样的实现。

function Regal() {
  this.getTicketPrice = function(quantity) {
    return quantity * 11.99;
  }
}

function AMC() {
  this.getTicketPrice = function(quantity) {
    return quantity * 10.99;
  }
}

function Cinemark() {
  this.getTicketPrice = function(quantity) {
    return quantity * 9.99;
  }
}

function TicketPrice() {
  this.theaterChain;

  this.setTheaterChain = function(chain) {
    this.theaterChain = chain;
  }

  this.calculate = function(quantity) {
    return this.theaterChain.getTicketPrice(quantity);
  }
}

const regal = new Regal();
const amc = new AMC();
const cinemark = new Cinemark();

const ticketPrice = new TicketPrice();
ticketPrice.setTheaterChain(regal);
console.log(ticketPrice.calculate(2));

ticketPrice.setTheaterChain(amc);
console.log(ticketPrice.calculate(3));

ticketPrice.setTheaterChain(cinemark);
console.log(ticketPrice.calculate(4));
Enter fullscreen mode Exit fullscreen mode

最后的想法

这就是全部了(目前)!

说实话,深入研究这些设计模式让我非常开心,也非常开心。虽然我跳过了一些用 JavaScript 实现 ES6 的模式,但总的来说,我确实学到了很多东西。

如果你读到这里,希望我的旅程和代码对你有所帮助。期待下次再见!

访问我的 Github repo 即可在一个地方获取上述所有代码。

文章来源:https://dev.to/twinfred/design-patterns-in-javascript-1l2l
PREV
如何免费为学生启用 GitHub Copilot
NEXT
31 个前端开发学习主题,助您提升代码质量