前端设计模式

0 阅读11分钟

前言

前端开发中的设计模式就像是“代码模版”,它们提供了一套经过验证的解决方案,用于解决常见的设计问题,是为了解决开发中反复出现的特定问题而总结出的最佳实践。接下来会介绍一些常见的前端设计模式,并提供示例代码。

常见设计模式

1. 模块模式(Module Pattern)

模块模式是一种常见的设计模式,用于创建具有私有和公共成员的模块。它通过闭包来实现数据封装,避免了全局变量的污染。

const MyModule = (function () {
  // 私有变量
  let privateVariable = "I am private";
  // 私有函数
  function privateFunction() {
    console.log(privateVariable);
  }
  // 公共接口
  return {
    publicMethod: function () {
      privateFunction();
    },
  };
})();
MyModule.publicMethod(); // 输出: I am private

2. 单例模式(Singleton Pattern)

单例模式确保一个类只有一个实例,并提供一个全局访问点。它常用于管理全局状态或资源。

const Singleton = (function () {
  let instance;
  function createInstance() {
    return { name: "I am the only instance" };
  }
  return {
    getInstance: function () {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    },
  };
})();
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // 输出: true

3. 观察者模式(Observer Pattern)

观察者模式是一种设计模式,其中一个对象(称为“主题(Subject)”)维护一系列依赖于它的对象(称为“观察者(Observer)”),并在状态发生变化时通知它们。它常用于事件处理系统。

class Subject {
  constructor() {
    this.observers = [];
  }
  subscribe(observer) {
    this.observers.push(observer);
  }
  unsubscribe(observer) {
    this.observers = this.observers.filter((obs) => obs !== observer);
  }
  notify(data) {
    this.observers.forEach((observer) => observer.update(data));
  }
}
class Observer {
  update(data) {
    console.log("Observer received data:", data);
  }
}
const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();
subject.subscribe(observer1);
subject.subscribe(observer2);
subject.notify("Hello Observers!"); // 输出: Observer received data: Hello Observers!

4. 发布-订阅模式(Publish-Subscribe Pattern)

发布-订阅模式是一种设计模式,其中发布者(Publisher)发布事件,订阅者(Subscriber)订阅事件,并在事件发生时接收通知。它常用于解耦组件之间的通信。

class PubSub {
  constructor() {
    this.events = {};
  }
  subscribe(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
  }
  publish(event, data) {
    if (this.events[event]) {
      this.events[event].forEach((callback) => callback(data));
    }
  }
}
const pubSub = new PubSub();
pubSub.subscribe("event1", (data) =>
  console.log("Subscriber 1 received:", data),
);
pubSub.subscribe("event1", (data) =>
  console.log("Subscriber 2 received:", data),
);
pubSub.publish("event1", "Hello PubSub!");
// 输出: Subscriber 1 received: Hello PubSub!
// 输出: Subscriber 2 received: Hello PubSub!

5. 工厂模式(Factory Pattern)

工厂模式是一种创建对象的设计模式,它提供一个接口用于创建对象,但允许子类决定实例化哪个类。它常用于需要根据条件创建不同类型对象的场景。

class Car {
  constructor(model) {
    this.model = model;
  }
}
class CarFactory {
  createCar(model) {
    return new Car(model);
  }
}
const factory = new CarFactory();
const car1 = factory.createCar("Tesla Model S");
console.log(car1.model); // 输出: Tesla Model S

6. 策略模式(Strategy Pattern)

策略模式是一种设计模式,它定义了一系列算法,并将每个算法封装起来,使它们可以互换。它常用于需要在运行时选择算法的场景。

class Strategy {
  execute() {
    throw new Error("Strategy#execute must be overridden");
  }
}
class ConcreteStrategyA extends Strategy {
  execute() {
    console.log("Executing strategy A");
  }
}
class ConcreteStrategyB extends Strategy {
  execute() {
    console.log("Executing strategy B");
  }
}
class Context {
  constructor(strategy) {
    this.strategy = strategy;
  }
  setStrategy(strategy) {
    this.strategy = strategy;
  }
  executeStrategy() {
    this.strategy.execute();
  }
}
const context = new Context(new ConcreteStrategyA());
context.executeStrategy(); // 输出: Executing strategy A
context.setStrategy(new ConcreteStrategyB());
context.executeStrategy(); // 输出: Executing strategy B

7. 装饰器模式(Decorator Pattern)

装饰器模式是一种设计模式,它允许向一个对象添加新的功能,而不改变其结构。它常用于需要动态地为对象添加功能的场景。

function decorator(func) {
  return function (...args) {
    console.log("Before executing the function");
    const result = func(...args);
    console.log("After executing the function");
    return result;
  };
}
function originalFunction() {
  console.log("Executing original function");
}
const decoratedFunction = decorator(originalFunction);
decoratedFunction();
// 输出: Before executing the function
// 输出: Executing original function
// 输出: After executing the function

单例模式实现方式

单例模式可以通过多种方式实现,以下是两种常见的实现方式:

1. 使用闭包实现单例模式

const Singleton = (function () {
  let instance;
  function createInstance() {
    return { name: "I am the only instance" };
  }
  return {
    getInstance() {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    },
  };
})();

const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // 输出: true

2. 使用类实现单例模式

class Singleton {
  constructor(name) {
    if (Singleton.instance) {
      return Singleton.instance;
    }
    this.name = name;
    this.startTime = new Date();
    Singleton.instance = this;
  }

  static getInstance(name) {
    if (!this.instance) {
      this.instance = new Singleton(name);
    }
    return this.instance;
  }

  showTime() {
    console.log(`Instance created at: ${this.startTime}`);
  }
}

const instance1 = new Singleton("First Instance");
const instance2 = new Singleton("Second Instance");

console.log(instance1 === instance2); // 输出: true
console.log(instance1.name); // 输出: First Instance
console.log(instance2.name); // 输出: First Instance(因为 instance2 实际上是 instance1 的引用,instance2并没有被创建)

为什么单例模式用 class 的方式实现更合适?

  1. 命名空间与组织性:使用class可以将属性和方法组织在一个命名空间中,可以清晰地看到单例的结构和行为,而闭包方式可能会导致代码分散,难以维护。

  2. 延迟初始化:使用class的getInstance方法可以在有需要的时候才创建实例,而如果直接定义一个全局对象const singleton = new Singleton(),则在模块加载时就会创建实例,可能会导致不必要的资源浪费。

  3. 继承和扩展性:使用class支持extends,这样可以让单例可以拥有父类的通用能力,比如EventEmitter等,而闭包方式则不太方便实现原型链继承。

单例模式如何防止他人强行创建实例?

在Java或C++中,可以把构造函数设为private来防止外部直接创建实例,但在JavaScript中没有private构造函数的概念。JS虽然提供了private class fields(以#开头的字段)来实现私有属性,但它们并不能完全阻止外部通过new操作符创建实例,而我们可以通过直接抛出错误来强制限制。

class StrictSingleton {
    static #instance = null;

    constructor() {
        if(StrictSingleton.#instance) {
            throw new Error("请使用 StrictSingleton.getInstance() 获取实例");
        }
        StrictSingleton.#instance = this;
    }

    static getInstance() {
        if(!StrictSingleton.#instance) {
            StrictSingleton.#instance = new StrictSingleton();
        }

        return StrictSingleton.#instance;
    }
}

发布订阅模式和观察者模式的区别

发布订阅模式和观察者模式看起来都是在某一对象发生变化时通知其他对象,但它们之间实际上有一些区别,接下来会结合代码示例来说明它们的区别。

1. 核心对象

  • 观察者模式:核心对象是“主题(Subject)”,它维护一系列依赖于它的对象(称为“观察者(Observer)”),并在状态发生变化时通知它们。

  • 发布订阅模式:核心对象是“事件中心(Event Bus)”,它负责管理事件的订阅和发布,发布者(Publisher)发布事件,订阅者(Subscriber)订阅事件,并在事件发生时接收通知,在上面的代码中,PubSub类就是事件中心。

2. 耦合度

  • 观察者模式:主题和观察者之间存在直接的依赖关系,主题需要知道观察者的存在,并且在状态变化时直接调用观察者的方法。

  • 发布订阅模式:发布者和订阅者之间没有直接的依赖关系,它们通过事件中心进行通信,发布者只需要发布事件,而订阅者只需要订阅事件,彼此之间是完全解耦的。

3. 通信方式

  • 观察者模式:主题直接调用观察者的方法进行通信。

  • 发布订阅模式:发布者通过事件中心发布事件,订阅者通过事件中心接收事件进行通信,由事件中心决定通知的时间和方式。

4. 应用场景

  • 观察者模式:在基础库的内部逻辑中比较常见,比如Vue.js中的响应式系统就是基于观察者模式实现的。

ref 函数接受一个初始值,并返回一个包含该值的响应式对象。其核心原理如下:

function ref(initialValue) {
  const r = {
    get value() {
      // 依赖收集
      track(r, 'value')
      return initialValue
    },
    set value(newValue) {
      initialValue = newValue
      // 触发更新
      trigger(r, 'value')
    }
  }
  return r
}

reactive 函数接受一个对象,并返回该对象的响应式代理。其核心原理如下:

function reactive(target) {
  return new Proxy(target, {
    get(target, key) {
      // 依赖收集
      track(target, key)
      return Reflect.get(target, key)
    },
    set(target, key, value) {
      const result = Reflect.set(target, key, value)
      // 触发更新
      trigger(target, key)
      return result
    }
  })
}
  • 发布订阅模式:在跨组件通信、事件驱动的系统中比较常见,比如前端框架中的事件总线就是基于发布订阅模式实现的。如Vuex和Redux中的状态管理也是基于发布订阅模式实现的。

装饰器模式的优缺及使用场景

装饰器模式的核心理念是在不改变对象代码,不使用继承的情况下,动态地为对象添加额外的功能。

1. 与传统类继承的对比

  • 灵活性:装饰器模式允许在运行时动态地为对象添加功能,而不需要在编译时就确定对象的行为。这使得代码更加灵活,易于维护和扩展。

  • 遵循开闭原则:装饰器模式遵循开闭原则,即对扩展开放,对修改关闭。可以在不修改现有代码的情况下,通过添加新的装饰器来扩展对象的功能。

  • 避免了类的数量爆炸:使用类继承来组合各种功能,随着功能排列组合的增加,派生类的数量会呈指数级增长,而装饰器模式通过组合装饰器来实现功能的扩展,只需要少量装饰类即可完成复杂的组合。

  • 职责分离:装饰器模式将对象的功能分解为独立的装饰器,每个装饰器只负责一个特定的功能,使得代码更加模块化,易于维护和测试。

2. 适用场景

  • 动态添加功能:当需要在运行时动态地为对象添加功能时,装饰器模式是一个很好的选择,如在一个支付系统中,基础功能是“支付”。你可以根据用户需求,动态地叠加上“短信通知”、“积分奖励”、“多币种转换”等功能。

  • 处理多种功能的组合:如果一个对象有5个独立的功能扩展,使用继承可能需要实现2的五次方个子类,而装饰器模式只需要5个装饰器类。

  • 无法通过继承扩展的类:当类被定义为final或者无法修改时,装饰器模式可以通过组合的方式来扩展其功能,而不需要修改原有类的代码。

3. 具体示例

class Coffee {
  cost() {
    return 5;
  }
}

class MilkDecorator {
  constructor(coffee) {
    this.coffee = coffee;
  }
  cost() {
    return this.coffee.cost() + 2; // 加牛奶的费用
  }
}

class SugarDecorator {
  constructor(coffee) {
    this.coffee = coffee;
  }
  cost() {
    return this.coffee.cost() + 1; // 加糖的费用
  }
}

const myCoffee = new SugarDecorator(new MilkDecorator(new Coffee()));
console.log(myCoffee.cost()); // 输出: 8 (5 + 2 + 1)

在这个例子中,Coffee是基础类,MilkDecorator和SugarDecorator是装饰器类,通过组合的方式为Coffee对象动态地添加了牛奶和糖的功能,而不需要修改Coffee类的代码。

4. 装饰器的缺点

  • 增加了系统的复杂性:装饰器模式引入了更多的类和对象,可能会使系统变得更加复杂,尤其是在装饰器层次较深时,可能会导致代码难以理解和维护。

  • 调试困难:由于装饰器模式涉及多个对象的组合,调试时可能需要跟踪多个对象的状态和行为,增加了调试的难度。

  • 性能开销:每个装饰器都需要创建一个新的对象,这可能会导致性能开销,尤其是在装饰器层次较深时,可能会影响系统的性能。

5. 一些扩展

  • Python中的装饰器:Python内置了装饰器语法,可以直接使用@符号来装饰函数或类,极大地简化了装饰器的使用。

def decorator(func):
    def wrapper(*args, **kwargs):
        print("Before executing the function")
        result = func(*args, **kwargs)
        print("After executing the function")
        return result
    return wrapper

@decorator
def original_function():
    print("Executing original function")

original_function()

# 输出:
# Before executing the function
# Executing original function
# After executing the function
  • Java中的装饰器:Java中可以通过接口和抽象类来实现装饰器模式,常见的例子是Java IO库中的InputStream和OutputStream类。
public interface Coffee {
    double cost();
}

public class SimpleCoffee implements Coffee {
    @Override
    public double cost() {
        return 5;
    }
}

public class MilkDecorator implements Coffee {
    private Coffee coffee;

    public MilkDecorator(Coffee coffee) {
        this.coffee = coffee;
    }

    @Override
    public double cost() {
        return coffee.cost() + 2; // 加牛奶的费用
    }
}

public class SugarDecorator implements Coffee {
    private Coffee coffee;

    public SugarDecorator(Coffee coffee) {
        this.coffee = coffee;
    }

    @Override
    public double cost() {
        return coffee.cost() + 1; // 加糖的费用
    }
}

public class Main {
    public static void main(String[] args) {
        Coffee myCoffee = new SugarDecorator(new MilkDecorator(new SimpleCoffee()));
        System.out.println(myCoffee.cost()); // 输出: 8 (5 + 2 + 1)
    }
}

结语

设计模式实在太多了,以上只是提到了几个比较重要和常见的设计模式,个人感觉不用过于苛求每个设计模式都要熟练掌握,了解它们的核心思想和基本适用场景就足够了,在实际开发中,根据具体问题选择合适的设计模式来解决问题才是最重要的。设计模式是工具,不是目的,过度使用设计模式可能会导致代码过于复杂,反而不利于维护和理解,所以在使用设计模式时要根据实际情况进行权衡和选择。