MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript中的设计模式与应用场景

2021-06-274.6k 阅读

一、设计模式基础概念

(一)设计模式的定义

设计模式是指在软件开发过程中,针对反复出现的问题所总结归纳出的通用解决方案。这些解决方案并非具体的代码实现,而是一种经过实践验证的、可复用的设计思想。它们能够帮助开发者更高效地构建软件系统,提高代码的可维护性、可扩展性和可复用性。在 JavaScript 这门灵活多变的编程语言中,设计模式的应用尤为重要,它可以帮助开发者在面对复杂业务逻辑时,保持代码的清晰与简洁。

(二)设计模式的分类

设计模式通常分为三大类:创建型模式、结构型模式和行为型模式。

  1. 创建型模式
    • 主要用于对象的创建过程,隐藏对象的创建细节,使代码在创建对象时更加灵活。常见的创建型模式包括单例模式、工厂模式、抽象工厂模式、建造者模式和原型模式等。在 JavaScript 中,由于其独特的基于原型的继承机制,原型模式应用广泛,并且单例模式也有其独特的实现方式。
  2. 结构型模式
    • 关注如何将类或对象组合成更大的结构,以实现功能的增强和复用。比如代理模式、装饰器模式、适配器模式、桥接模式、组合模式、外观模式和享元模式等。这些模式能够帮助我们优化代码结构,使各个模块之间的关系更加清晰合理。例如,代理模式可以在不改变原始对象的情况下,为其提供额外的功能。
  3. 行为型模式
    • 着重于处理对象之间的交互和职责分配,以提高系统的灵活性和可维护性。常见的行为型模式有观察者模式、策略模式、状态模式、模板方法模式、迭代器模式、命令模式、备忘录模式、解释器模式、中介者模式和访问者模式等。在前端开发中,观察者模式常用于实现组件之间的通信,当一个对象的状态发生变化时,自动通知依赖它的其他对象。

二、JavaScript 中的创建型模式

(一)单例模式

  1. 概念
    • 单例模式确保一个类仅有一个实例,并提供一个全局访问点。在 JavaScript 中,由于没有类的传统概念,实现单例模式的方式有所不同。单例模式的核心思想是确保在整个应用程序中,某个特定对象只有一个实例存在,避免重复创建带来的资源浪费和数据不一致问题。
  2. 实现方式
    • 简单实现
// 简单的单例模式实现
const Singleton = (function () {
    let instance;
    function createInstance() {
        const object = new Object('I am the instance');
        return object;
    }
    return {
        getInstance: function () {
            if (!instance) {
                instance = createInstance();
            }
            return instance;
        }
    };
})();

// 使用单例
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true
- **ES6 类和闭包实现**
class Singleton {
    constructor() {
        if (Singleton.instance) {
            return Singleton.instance;
        }
        Singleton.instance = this;
        return this;
    }
}

// 使用闭包来模拟私有变量和方法
const singletonClosure = (function () {
    let instance;
    function getInstance() {
        if (!instance) {
            instance = new Singleton();
        }
        return instance;
    }
    return {
        getInstance: getInstance
    };
})();

const s1 = singletonClosure.getInstance();
const s2 = singletonClosure.getInstance();
console.log(s1 === s2); // true
  1. 应用场景
    • 全局状态管理:在一个大型的 JavaScript 应用程序中,可能需要一个全局的状态对象来管理应用的各种状态,如用户登录状态、主题设置等。使用单例模式可以确保这个状态对象在整个应用中唯一,避免状态不一致的问题。例如,在一个单页应用(SPA)中,我们可以创建一个单例的状态管理对象来存储用户的登录信息、购物车数据等。
    • 日志记录:在应用程序中,日志记录是一个常见的功能。为了避免重复创建日志记录器实例,造成资源浪费,我们可以使用单例模式。这样无论在应用的哪个部分需要记录日志,都可以通过同一个日志记录器实例来进行操作,确保日志记录的一致性和规范性。

(二)工厂模式

  1. 概念
    • 工厂模式是一种创建型设计模式,它提供了一种创建对象的方式,将对象的创建和使用分离。通过使用工厂模式,我们可以将对象的创建逻辑封装在一个工厂函数或类中,而不是在代码的多个地方直接使用 new 关键字创建对象。这样做的好处是可以降低代码的耦合度,提高可维护性和可扩展性。
  2. 实现方式
    • 简单工厂函数
// 简单工厂函数创建不同类型的动物对象
function AnimalFactory(type) {
    if (type === 'dog') {
        return {
            speak: function () {
                console.log('Woof!');
            }
        };
    } else if (type === 'cat') {
        return {
            speak: function () {
                console.log('Meow!');
            }
        };
    }
}

const dog = AnimalFactory('dog');
const cat = AnimalFactory('cat');
dog.speak(); // Woof!
cat.speak(); // Meow!
- **工厂类实现**
class AnimalFactoryClass {
    createAnimal(type) {
        if (type === 'dog') {
            return {
                speak: function () {
                    console.log('Woof!');
                }
            };
        } else if (type === 'cat') {
            return {
                speak: function () {
                    console.log('Meow!');
                }
            };
        }
    }
}

const factory = new AnimalFactoryClass();
const dogFromClass = factory.createAnimal('dog');
const catFromClass = factory.createAnimal('cat');
dogFromClass.speak(); // Woof!
catFromClass.speak(); // Meow!
  1. 应用场景
    • 对象创建逻辑复杂时:当创建对象的过程涉及到复杂的初始化逻辑、参数验证或依赖注入时,使用工厂模式可以将这些复杂逻辑封装在工厂中,使调用者只需要关心获取对象,而无需了解创建细节。例如,在一个游戏开发中,创建不同类型的角色可能需要加载不同的资源、设置初始属性等复杂操作,使用工厂模式可以将这些操作封装起来。
    • 根据不同条件创建不同类型对象:在很多应用场景中,我们需要根据不同的条件或用户输入创建不同类型的对象。工厂模式可以很好地满足这一需求,通过传入不同的参数,工厂可以创建出相应类型的对象。比如在一个图形绘制库中,根据用户选择的图形类型(圆形、矩形、三角形等),使用工厂模式创建对应的图形对象。

(三)原型模式

  1. 概念
    • 在 JavaScript 中,原型模式是其基于原型继承的核心机制。每个对象都有一个原型对象,对象可以从其原型对象继承属性和方法。通过原型模式,我们可以在原型对象上定义共享的属性和方法,从而避免在每个实例对象上重复创建相同的内容,提高内存利用效率。
  2. 实现方式
// 使用原型模式创建对象
function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.sayHello = function () {
    console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
};

const person1 = new Person('Alice', 30);
const person2 = new Person('Bob', 25);

person1.sayHello(); // Hello, my name is Alice and I'm 30 years old.
person2.sayHello(); // Hello, my name is Bob and I'm 25 years old.
  1. 应用场景
    • 对象继承与复用:当我们需要创建多个具有相似属性和方法的对象时,使用原型模式可以将这些共享的部分定义在原型对象上,从而实现代码的复用。例如,在一个电商系统中,可能有多个商品对象,它们都具有一些共同的属性(如名称、价格)和方法(如计算总价),可以通过原型模式来实现这些共享部分的复用。
    • 模拟类的继承:虽然 JavaScript 从 ES6 开始引入了类的概念,但本质上仍然是基于原型的继承。在 ES6 之前,开发者广泛使用原型模式来模拟类的继承,即使在 ES6 之后,了解原型模式对于深入理解 JavaScript 的继承机制仍然非常重要。

三、JavaScript 中的结构型模式

(一)代理模式

  1. 概念
    • 代理模式为其他对象提供一种代理以控制对这个对象的访问。代理对象可以在客户端和目标对象之间起到中介作用,在不改变目标对象的情况下,为其添加额外的功能,如访问控制、缓存、日志记录等。
  2. 实现方式
// 目标对象
const realImage = {
    display: function () {
        console.log('Displaying real image');
    }
};

// 代理对象
const imageProxy = (function () {
    let isLoaded = false;
    return {
        display: function () {
            if (!isLoaded) {
                console.log('Loading image...');
                // 模拟加载过程
                setTimeout(() => {
                    realImage.display();
                    isLoaded = true;
                }, 2000);
            } else {
                realImage.display();
            }
        }
    };
})();

imageProxy.display(); // Loading image... 两秒后 Displaying real image
imageProxy.display(); // Displaying real image
  1. 应用场景
    • 远程代理:当需要访问远程对象时,由于网络延迟等原因,直接访问可能效率较低。可以使用代理对象在本地进行一些预处理,如缓存数据、验证请求等,然后再将请求转发给远程对象。例如,在一个前端应用中调用后端 API 获取数据时,可以通过代理对象缓存经常请求的数据,减少对后端的重复请求。
    • 虚拟代理:对于一些创建开销较大的对象,可以使用代理对象在需要时才创建实际对象,从而提高系统的性能。比如在网页中加载大图片时,先使用代理对象占位,当图片真正需要显示时再加载实际图片。

(二)装饰器模式

  1. 概念
    • 装饰器模式允许向一个现有的对象添加新的功能,同时又不改变其结构。它通过将功能封装在装饰器对象中,并将装饰器对象与原始对象进行组合,从而实现功能的动态添加。
  2. 实现方式
// 原始对象
function Coffee() {
    this.getCost = function () {
        return 5;
    };
    this.getDescription = function () {
        return 'Coffee';
    };
}

// 装饰器
function MilkDecorator(coffee) {
    this.coffee = coffee;
    this.getCost = function () {
        return this.coffee.getCost() + 2;
    };
    this.getDescription = function () {
        return this.coffee.getDescription() + ', Milk';
    };
}

function SugarDecorator(coffee) {
    this.coffee = coffee;
    this.getCost = function () {
        return this.coffee.getCost() + 1;
    };
    this.getDescription = function () {
        return this.coffee.getDescription() + ', Sugar';
    };
}

const basicCoffee = new Coffee();
console.log(basicCoffee.getCost()); // 5
console.log(basicCoffee.getDescription()); // Coffee

const coffeeWithMilk = new MilkDecorator(basicCoffee);
console.log(coffeeWithMilk.getCost()); // 7
console.log(coffeeWithMilk.getDescription()); // Coffee, Milk

const coffeeWithMilkAndSugar = new SugarDecorator(coffeeWithMilk);
console.log(coffeeWithMilkAndSugar.getCost()); // 8
console.log(coffeeWithMilkAndSugar.getDescription()); // Coffee, Milk, Sugar
  1. 应用场景
    • 动态添加功能:在开发过程中,有时候需要在运行时为对象添加新的功能,而不希望修改对象的原有代码。装饰器模式可以很好地满足这一需求。例如,在一个电商系统中,对于商品对象,在不同的促销活动中可能需要动态添加不同的折扣、赠品等功能。
    • 功能复用:装饰器模式将功能封装在独立的装饰器对象中,这些装饰器可以被多个对象复用。比如在一个游戏开发中,不同的角色可能需要添加相同的 buff(如加速、无敌等),可以通过装饰器模式来实现这些 buff 的复用。

(三)适配器模式

  1. 概念
    • 适配器模式用于将一个类的接口转换成客户希望的另一个接口。它使得原本由于接口不兼容而不能一起工作的类可以协同工作。在 JavaScript 中,适配器模式常用于处理第三方库或旧代码的接口与当前系统接口不匹配的情况。
  2. 实现方式
// 目标接口
function Target() {
    this.request = function () {
        return 'Target request';
    };
}

// 现有接口
function Adaptee() {
    this.specificRequest = function () {
        return 'Specific request';
    };
}

// 适配器
function Adapter(adaptee) {
    this.adaptee = adaptee;
    this.request = function () {
        return this.adaptee.specificRequest().replace('Specific', 'Target');
    };
}

const target = new Target();
console.log(target.request()); // Target request

const adaptee = new Adaptee();
const adapter = new Adapter(adaptee);
console.log(adapter.request()); // Target request
  1. 应用场景
    • 整合第三方库:在项目开发中,经常会引入第三方库来实现特定的功能。但这些第三方库的接口可能与我们项目的整体接口风格不一致。通过适配器模式,可以将第三方库的接口适配成我们项目所需的接口,从而实现无缝集成。例如,引入一个图表绘制库,但其数据格式与我们项目中的数据格式不同,使用适配器模式可以将数据进行转换,使其能够正常使用。
    • 兼容旧代码:在维护旧项目时,可能会遇到旧代码中的接口与新的业务需求不匹配的情况。使用适配器模式可以在不修改旧代码的前提下,将旧接口适配成新接口,确保系统的兼容性和稳定性。

四、JavaScript 中的行为型模式

(一)观察者模式

  1. 概念
    • 观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。当主题对象的状态发生变化时,会自动通知所有的观察者对象,使它们能够及时做出相应的反应。
  2. 实现方式
class Subject {
    constructor() {
        this.observers = [];
    }

    attach(observer) {
        this.observers.push(observer);
    }

    detach(observer) {
        this.observers = this.observers.filter(obs => obs!== observer);
    }

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

class Observer {
    constructor(name) {
        this.name = name;
    }

    update() {
        console.log(`${this.name} has been notified.`);
    }
}

const subject = new Subject();
const observer1 = new Observer('Observer 1');
const observer2 = new Observer('Observer 2');

subject.attach(observer1);
subject.attach(observer2);

subject.notify();
// Observer 1 has been notified.
// Observer 2 has been notified.
  1. 应用场景
    • 前端组件通信:在前端开发中,组件之间经常需要进行通信。例如,一个购物车组件和商品列表组件,当商品列表中的商品数量或价格发生变化时,需要通知购物车组件更新总价等信息。使用观察者模式可以实现这种组件之间的解耦,使它们能够独立变化,同时保持数据的一致性。
    • 事件驱动编程:JavaScript 是一种事件驱动的编程语言,许多 DOM 事件(如点击、滚动等)都可以看作是观察者模式的应用。当事件发生时(主题状态变化),绑定在该事件上的回调函数(观察者)会被触发执行。

(二)策略模式

  1. 概念
    • 策略模式定义了一系列算法,将每个算法都封装起来,并且使它们可以相互替换。策略模式使得算法的变化不会影响到使用算法的客户。在 JavaScript 中,策略模式可以通过对象字面量和函数来实现。
  2. 实现方式
const strategies = {
    add: function (a, b) {
        return a + b;
    },
    subtract: function (a, b) {
        return a - b;
    },
    multiply: function (a, b) {
        return a * b;
    }
};

function calculate(strategy, a, b) {
    return strategies[strategy](a, b);
}

console.log(calculate('add', 5, 3)); // 8
console.log(calculate('subtract', 5, 3)); // 2
console.log(calculate('multiply', 5, 3)); // 15
  1. 应用场景
    • 业务规则变化频繁:在业务开发中,经常会遇到业务规则变化频繁的情况。例如,在一个电商系统中,不同的促销活动可能有不同的计算折扣的规则。使用策略模式可以将这些不同的规则封装成不同的策略函数,当业务规则发生变化时,只需要修改相应的策略函数,而不会影响到其他部分的代码。
    • 条件判断复杂:当代码中存在大量的条件判断语句来选择不同的算法或行为时,可以使用策略模式将这些条件判断逻辑封装到策略对象中,使代码更加清晰、可维护。

(三)状态模式

  1. 概念
    • 状态模式允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。它将对象的状态封装成独立的状态类,每个状态类都实现了相同的接口,使得对象在不同状态下可以有不同的行为表现。
  2. 实现方式
// 状态基类
class State {
    handle(context) {
        console.log('Default state behavior');
    }
}

// 具体状态类
class ConcreteStateA extends State {
    handle(context) {
        console.log('ConcreteStateA handling');
        context.setState(new ConcreteStateB());
    }
}

class ConcreteStateB extends State {
    handle(context) {
        console.log('ConcreteStateB handling');
        context.setState(new ConcreteStateA());
    }
}

// 上下文类
class Context {
    constructor() {
        this.state = new ConcreteStateA();
    }

    setState(state) {
        this.state = state;
    }

    request() {
        this.state.handle(this);
    }
}

const context = new Context();
context.request(); // ConcreteStateA handling
context.request(); // ConcreteStateB handling
  1. 应用场景
    • 对象状态复杂多变:当一个对象的行为随着其内部状态的改变而发生显著变化,并且状态的转换逻辑较为复杂时,使用状态模式可以将状态相关的行为封装到各个状态类中,使代码更加清晰、易于维护。例如,在一个游戏角色的状态管理中,角色可能有奔跑、跳跃、攻击等不同状态,每个状态下的行为不同,状态之间的转换也有特定规则,使用状态模式可以很好地实现这种状态管理。
    • 避免大量条件判断:如果不使用状态模式,处理对象不同状态下的行为可能需要大量的条件判断语句。而状态模式通过将状态和行为封装,避免了这种复杂的条件判断,提高了代码的可扩展性和可维护性。

通过对以上多种设计模式在 JavaScript 中的深入探讨和示例展示,我们可以看到设计模式在优化代码结构、提高代码可维护性和可扩展性方面的强大作用。在实际的 JavaScript 项目开发中,根据不同的业务场景合理选择和应用设计模式,能够大大提升项目的质量和开发效率。