前端设计模式

903 阅读52分钟

面向对象 OOP 和 UML 类图 - 前端开发的必备编程思想

当谈到面向对象编程(Object-Oriented Programming,OOP)时,它是一种编程范式,其中程序的结构是基于对象的概念。在面向对象编程中,问题被分解为一组相互作用的对象,每个对象都有自己的状态(属性)和行为(方法)。这种方式使得代码更加模块化、可重用和易于理解。

面向对象编程中的核心概念包括:

  1. 类(Class):类是对象的蓝图或模板,描述了对象的属性和方法。它定义了对象的共同特征和行为。例如,可以定义一个名为"Person"的类,它具有属性(如姓名、年龄)和方法(如说话、行走)。
  2. 对象(Object):对象是类的实例,它具有类定义的属性和方法。通过实例化类,可以创建对象。例如,可以通过实例化"Person"类创建一个名为"John"的对象,它具有特定的姓名和年龄,并可以执行相应的方法。
  3. 封装(Encapsulation):封装是将数据和操作封装在一个单元(类)中,以实现信息隐藏和保护数据的安全性。通过定义类的公共接口(方法)来访问和操作对象的数据,而对于类的内部实现细节则是私有的。
  4. 继承(Inheritance):继承是一种机制,允许一个类继承另一个类的属性和方法。通过继承,子类可以重用父类的代码,并可以在不修改父类的情况下添加自己的特定行为。
  5. 多态(Polymorphism):多态是指同一操作对不同对象的不同响应方式。通过多态,可以使用统一的接口来处理不同类型的对象,从而提高代码的灵活性和可扩展性。

UML(Unified Modeling Language)类图是一种用于可视化和描述类、对象、关系和行为的图形化表示方法。它是一种常用的软件工程工具,用于设计和分析系统。在类图中,可以表示类之间的关系(如继承、关联、聚合等)和类的属性和方法。

类图中的一些常见元素包括:

  1. 类(Class):用矩形框表示类,包含类的名称、属性和方法。
  2. 属性(Attribute):表示类的状态或特征,通常以名称和类型的形式表示。
  3. 方法(Method):表示类的行为或操作,通常以名称和参数列表的形式表示。
  4. 关联关系(Association):表示类之间的关联关系,可以是单向或双向的。
  5. 继承关系(Inheritance):表示一个类继承另一个类的关系,通常用箭头指向父类。
  6. 聚合关系(Aggregation):表示一种弱的关联关系,表示整体与部分之间的关系。
  7. 组合关系(Composition):表示一种强的关联关系,表示整体与部分之间的关系,部分不能独立存在。

UML 类图提供了一种可视化的方式来描述系统的结构和行为,有助于开发人员和设计师更好地理解和沟通系统的设计。它在软件开发过程中起到了重要的指导和文档作用。

image.png

多态

多态性的关键在于继承和方法重写。通过继承,子类可以继承父类的方法,并且可以在子类中重新实现这些方法以适应子类的特定需求

class Shape {
  calculateArea() {
    console.log("This is the base class for shapes.");
  }
}

class Rectangle extends Shape {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
  }

  calculateArea() {
    console.log("Area of the rectangle:", this.width * this.height);
  }
}

class Circle extends Shape {
  constructor(radius) {
    super();
    this.radius = radius;
  }

  calculateArea() {
    console.log("Area of the circle:", Math.PI * this.radius * this.radius);
  }
}

// 多态性的体现
const shapes = [new Rectangle(5, 10), new Circle(3)];

shapes.forEach((shape) => {
  shape.calculateArea();
});

在这个示例中,我们有一个基类 Shape 和两个派生类 Rectangle 和 CircleShape 类中定义了一个 calculateArea() 方法,而 Rectangle 和 Circle 类都重写了这个方法以计算它们各自形状的面积。

在主程序中,我们创建了一个包含 Rectangle 和 Circle 对象的数组 shapes。然后,我们使用 forEach 方法遍历数组中的每个元素,并调用它们的 calculateArea() 方法。

由于 shapes 数组中的每个元素都是 Shape 类型的引用,但实际指向的对象是不同的,所以在调用 calculateArea() 方法时,会根据对象的实际类型调用相应的重写方法。

输出结果将根据每个对象的类型而不同,分别计算矩形和圆的面积。

这个例子展示了 JavaScript 中多态性的特性,允许使用相同的方法名在不同类型的对象上产生不同的行为。这提供了更灵活和可扩展的代码结构。

设计模式只是套路,设计原则是指导思想

设计模式可以被视为一种套路,可以帮助开发人员解决特定类型的问题,并提供了一种经过验证的方法。

而设计原则是指导思想,它们提供了关于如何设计良好的软件架构和代码的指导。设计原则是广泛适用的准则,可以指导开发人员做出合理的设计决策,以实现可维护、可扩展和可重用的代码

以下是一些常见的设计原则:

  1. 单一职责原则(Single Responsibility Principle,SRP):一个类应该只有一个引起它变化的原因,即一个类应该只有一个职责。
  2. 开放封闭原则(Open-Closed Principle,OCP):软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。即在不修改现有代码的情况下,通过添加新代码来扩展功能。
  3. 里氏替换原则(Liskov Substitution Principle,LSP):子类对象应该能够替换其父类对象,而不会影响程序的正确性。
  4. 依赖倒置原则(Dependency Inversion Principle,DIP):高层模块不应该依赖于低层模块,两者都应该依赖于抽象。抽象不应该依赖于具体实现细节,具体实现细节应该依赖于抽象。
  5. 接口隔离原则(Interface Segregation Principle,ISP):客户端不应该依赖于它不需要的接口。一个类不应该强迫它的客户端依赖于它们不使用的方法。
  6. 迪米特法则(Law of Demeter,LoD):一个对象应该对其他对象有尽可能少的了解,只与最直接的朋友通信。

这些设计原则提供了一些通用的指导原则,帮助开发人员构建具有良好设计和高内聚低耦合的软件系统。设计模式则是在实践中应用这些设计原则的具体实现方式。

场景设计模式

工厂模式、单例模式(全局只允许有一个实例)、观察者模式、迭代器模式、原型模式、装饰器模式、代理模式

工厂模式

什么是工厂模式,它主要解决什么问题

工厂模式是一种创建对象的设计模式,它主要解决了对象的实例化过程与使用过程之间的耦合问题。它通过引入一个工厂类或接口,将对象的创建逻辑封装起来,客户端代码只需要通过工厂来创建对象,而无需直接使用 new 关键字实例化对象

工厂模式的主要目标是将对象的创建与使用分离,以提供更好的灵活性和可维护性。它可以解决以下问题:

  1. 隐藏对象的创建过程:使用工厂模式,客户端代码无需了解具体的对象创建细节,只需要知道如何通过工厂来创建对象。这样可以将对象的创建逻辑封装在工厂中,隐藏起来,使客户端代码更加简洁和易读。
  2. 解耦合:工厂模式通过引入工厂类或接口,将客户端代码与具体的产品类解耦。客户端只需要与工厂进行交互,而不需要直接依赖具体的产品类。这样可以降低代码的耦合度,提高代码的可维护性和扩展性。
  3. 提供灵活性:通过工厂模式,可以根据需要选择不同的工厂来创建不同的产品对象。这样可以在不修改客户端代码的情况下,更换或新增具体的产品类。工厂模式提供了一种可扩展的机制,使系统更具灵活性。
// 抽象产品接口
interface Product {
  operation(): void
}
// 具体产品类A
class ConcreteProductA implements Product {
  operation(): void {
    console.log('具体产品A的操作')
  }
}
// 具体产品类B
class ConcreteProductB implements Product {
  operation(): void {
    console.log('具体产品B的操作')
  }
}

// 抽象工厂接口
interface Factory {
  creteProduct(): Product
}
// 具体工厂类A
class ConcreteFactoryA implements Factory {
  creteProduct(): Product {
    return new ConcreteProductA();
  }
}
// 具体工厂类B
class ConcreteFactoryB implements Factory {
  creteProduct(): Product {
    return new ConcreteProductB();
  }
}
// 创建具体工厂对象
const factoryA: Factory = new ConcreteFactoryA();
const factoryB: Factory = new ConcreteFactoryB();

// 使用工厂A创建产品A
const productA: Product = factoryA.creteProduct();
productA.operation(); // 输出:具体产品A的操作

// 使用工厂B创建产品B
const productB: Product = factoryB.creteProduct();
productB.operation(); // 输出:具体产品B的操作

这里,我们通过工厂对象的 createProduct() 方法来创建具体的产品对象,并调用产品对象的方法进行操作。通过工厂模式,我们可以根据需要选择不同的工厂来创建不同的产品,而不需要直接关注具体的产品类

工厂模式缺点

工厂模式作为一种常用的设计模式,虽然有很多优点,但也存在一些缺点,包括:

  1. 增加了代码复杂性:引入工厂模式会增加额外的类和接口,增加了代码的复杂性和理解难度。工厂模式需要定义抽象工厂、具体工厂和产品接口等,这些额外的结构和层级可能会使代码变得更加复杂。
  2. 增加了系统的抽象性:工厂模式通过引入抽象工厂和产品接口,将对象的创建过程进行了抽象和封装。这种抽象性可能会导致系统的理解和调试变得困难,特别是对于初学者或新加入的开发人员来说。
  3. 不易于扩展和变化:尽管工厂模式提供了一种灵活的方式来创建对象,但当需要添加新的产品类型时,需要修改工厂类和产品接口的定义,这可能导致代码的修改和重构。这种扩展性的局限性可能会增加代码的维护成本。
  4. 增加了系统的依赖性:使用工厂模式会增加系统中类之间的依赖关系。客户端代码必须依赖于工厂接口和产品接口,这种依赖关系可能会增加系统的耦合度,使得代码更难以理解和修改。
  5. 可能引入过多的工厂类:随着系统的复杂性增加,可能需要引入多个具体工厂类来创建不同类型的产品。这可能导致工厂类的数量增加,使得代码变得冗长和复杂。

尽管工厂模式存在一些缺点,但在适当的场景下仍然是一种有价值的设计模式。它可以提供灵活性、可维护性和可扩展性,尤其在需要将对象的创建过程与使用过程分离,并提供统一的接口来访问对象时,工厂模式是一个有用的选择。

工厂模式的场景-jQuery

在 jQuery 库中,工厂模式被广泛应用于创建和操作 DOM 元素。jQuery 提供了一个全局函数 $,它实际上是一个工厂函数,用于创建 jQuery 对象

下面是一个简单的示例,展示了如何使用 jQuery 的工厂模式来创建和操作 DOM 元素:

// 创建一个 div 元素
var div = $('<div></div>');

// 添加类名和文本内容
div.addClass('myDiv').text('Hello, jQuery!');

// 将元素添加到文档中的 body 元素中
$('body').append(div);

在上述示例中,$('<div></div>') 使用 $ 工厂函数创建了一个 jQuery 对象,它封装了一个新创建的 <div> 元素。然后,我们可以使用 jQuery 提供的方法来操作这个 jQuery 对象,例如 addClass() 和 text() 方法。最后,通过调用 $('body').append(div) 将这个元素添加到文档中的 <body> 元素中。

通过使用工厂模式,jQuery 提供了一种简洁而灵活的方式来创建和操作 DOM 元素。它隐藏了底层的 DOM 操作细节,使开发人员能够以更简洁的方式编写代码,并提供了丰富的方法和功能来操作 DOM 元素。

除了创建和操作 DOM 元素,jQuery 还使用工厂模式来创建和操作其他对象,例如 AJAX 请求对象、事件对象等。工厂模式使得 jQuery 能够提供一致的接口和易于使用的功能,成为了 Web 开发中常用的工具库之一。

工厂模式的场景-Vue和React的createElement

在 Vue 和 React 中,工厂模式被用于创建虚拟 DOM 元素。虚拟 DOM 元素是用于描述 UI 的 JavaScript 对象,它们最终会被渲染成实际的 DOM 元素。

在 Vue 中,使用 createElement 函数创建虚拟 DOM 元素。createElement 是一个工厂函数,它接受三个参数:标签名、属性对象和子元素。通过调用 createElement 函数,可以创建一个虚拟 DOM 元素。

以下是一个使用 Vue 的 createElement 创建虚拟 DOM 元素的示例:

new Vue({
  el: '#app',
  render: function(createElement) {
    return createElement('div', { class: 'myDiv' }, 'Hello, Vue!');
  }
});

在上述示例中,render 函数使用 createElement 工厂函数创建了一个虚拟 DOM 元素 <div>,它具有类名为 'myDiv',并且文本内容为 'Hello, Vue!'。这个虚拟 DOM 元素最终会被渲染成实际的 DOM 元素,并插入到具有 id 为 'app' 的元素中。

类似地,在 React 中,使用 React.createElement 函数创建虚拟 DOM 元素。React.createElement 也是一个工厂函数,它接受三个参数:标签名或组件、属性对象和子元素。通过调用 React.createElement 函数,可以创建一个虚拟 DOM 元素。

以下是一个使用 React 的 createElement 创建虚拟 DOM 元素的示例:

ReactDOM.render( 
    React.createElement('div', { className: 'myDiv' }, 'Hello, React!'),
    document.getElementById('app')
);

在上述示例中,React.createElement 工厂函数创建了一个虚拟 DOM 元素 <div>,它具有类名为 'myDiv',并且文本内容为 'Hello, React!'。通过调用 ReactDOM.render 将这个虚拟 DOM 元素渲染成实际的 DOM 元素,并插入到具有 id 为 'app' 的元素中。

通过使用工厂模式,Vue 和 React 提供了一种简洁而灵活的方式来创建虚拟 DOM 元素。工厂模式隐藏了底层的 DOM 操作细节,使开发人员能够以声明式的方式描述 UI,并提供了丰富的功能和组件来构建复杂的应用程序界面。

单例模式 - 全局只允许有一个实例,多则出错

什么是单例模式,它主要解决什么问题

单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来访问该实例。

单例模式解决的主要问题是控制对象的实例化过程,确保在整个应用程序中只有一个实例存在。这在某些情况下是很有用的,例如:

  1. 资源共享:单例模式可以用来管理共享的资源,例如数据库连接池、线程池等。通过使用单例模式,可以确保所有的请求都使用同一个资源实例,避免资源的重复创建和管理。
  2. 对象跨越多个模块的访问:单例模式可以提供一个全局访问点,使得不同模块中的代码可以方便地访问同一个对象实例。这在需要共享数据或协调不同模块之间操作时非常有用。
  3. 控制实例数量:有些情况下,我们需要限制一个类的实例数量,确保只有一个实例存在。例如,某些设备驱动程序只允许有一个实例,或者某些配置信息只需要加载一次。

单例模式通过将类的实例化过程封装在类内部,并提供一个静态方法或属性来访问该实例,确保只有一个实例被创建并全局可访问。这样可以简化代码的使用方式,避免重复创建实例,同时提供了一种集中管理和控制对象实例的方式。

然而,单例模式也有一些缺点,例如可能引入全局状态和共享状态的问题,使得代码的依赖关系变得复杂。因此,在使用单例模式时需要谨慎考虑,并确保它真正解决了问题并符合应用程序的设计需求。

单例模式缺点

当使用单例模式时,需要注意以下一些潜在的缺点:

  1. 难以扩展和测试:由于单例模式创建了一个全局唯一的实例,它可能会导致代码的扩展和测试变得困难。其他部分的代码依赖于单例对象,如果需要修改或替换该对象,可能需要修改大量的代码。
  2. 引入全局状态:单例模式将实例对象设为全局可访问,这可能导致全局状态的引入。全局状态的管理变得复杂,可能会增加代码的耦合性和维护难度。
  3. 增加代码的耦合性:使用单例模式可能会增加代码的耦合性。其他部分的代码可能会直接依赖于单例对象,这使得单例对象的修改变得困难,可能需要同时修改依赖于该对象的其他代码。
  4. 可能引入并发问题:如果在多线程或异步环境中使用单例模式,可能会引入并发问题。当多个线程或任务同时访问单例对象时,需要考虑线程安全性和同步机制,以避免数据竞争和不一致的状态。
  5. 难以进行单元测试:由于单例对象通常在整个应用程序中被共享和访问,它可能会导致单元测试变得困难。在单元测试中,我们通常希望能够独立地测试每个模块,但单例对象的全局可访问性可能会干扰测试的隔离性。

下面是一个例子,展示了单例模式的一些缺点:

var Singleton = (function() {
  var instance;

  function Singleton() {
    this.counter = 0;
  }

  Singleton.prototype.incrementCounter = function() {
    this.counter++;
  };

  return {
    getInstance: function() {
      if (!instance) {
        instance = new Singleton();
      }
      return instance;
    }
  };
})();

var singletonInstance1 = Singleton.getInstance();
var singletonInstance2 = Singleton.getInstance();

singletonInstance1.incrementCounter();
console.log(singletonInstance2.counter); // 输出: 1

singletonInstance2.incrementCounter();
console.log(singletonInstance1.counter); // 输出: 2

在上述示例中,我们使用单例模式创建了一个计数器对象。然而,由于单例对象是全局共享的,对计数器对象的操作会影响到其他部分的代码。这可能导致并发问题和难以预测的行为。此外,由于全局状态的引入,单元测试变得困难,无法独立地测试每个模块。

单例模式示例

// 立即执行函数创建单例对象
var Singleton = (function(){
  // 单例实例
  var instance;
  // 私有属性和方法
  function initialize() {
    var privateVariable = "私有属性"
    function privateMethod() {
      console.log("私有方法")
    }
    return {
      // 共有属性和方法
      publicMethod: function() {
        console.log("公有方法")
      },
      publicVariable: "公有属性",
      getPrivateVariable: function() {
        return privateVariable;
      }
    }
  }
  // 获取单例的实例和方法
  return {
    getInstance: function() {
      if(!instance) {
        instance = initialize()
      }
      return instance;
    }
  }
})()

- 当使用 TypeScript 实现单例模式时,我们可以通过一个示例来说明其用法。假设我们有一个日志记录器,我们希望在整个应用程序中共享同一个日志记录器实例。

class Logger {
  private static instance: Logger;
  private logs: string[];

  private constructor() {
    this.logs = [];
  }

  public static getInstance(): Logger {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }

  public log(message: string): void {
    this.logs.push(message);
  }

  public printLogs(): void {
    console.log(this.logs);
  }
}

// 使用单例日志记录器
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();

logger1.log("Message 1");
logger2.log("Message 2");

logger1.printLogs(); // 输出: ["Message 1", "Message 2"]
logger2.printLogs(); // 输出: ["Message 1", "Message 2"]

在上述示例中,我们定义了一个名为 Logger 的类,它实现了单例模式。

  • 类中的 instance 是一个私有的静态成员,用于存储单例实例。
  • 构造函数 constructor 被设为私有,以防止直接实例化 Logger 类。
  • getInstance 是一个公共的静态方法,用于获取单例实例。如果实例不存在,则创建一个新的实例并将其赋值给 instance,然后返回该实例。如果 instance 已经存在,直接返回它。
  • logs 是一个私有成员,用于存储日志消息。
  • log 是一个公共方法,用于向日志记录器添加消息。
  • printLogs 是一个公共方法,用于打印日志消息。

在示例中,我们通过调用 Logger.getInstance() 方法来获取单例实例。我们创建了两个变量 logger1 和 logger2,它们实际上引用的是同一个日志记录器实例。

然后,我们分别向 logger1 和 logger2 添加了两条日志消息。由于它们引用的是同一个实例,这些日志消息都会被添加到同一个日志记录器中。

最后,我们分别调用 logger1.printLogs() 和 logger2.printLogs() 方法来打印日志消息。可以看到,两个日志记录器实例中都包含了相同的日志消息。

这个示例展示了如何使用 TypeScript 实现单例模式,并通过共享单例实例在整个应用程序中进行日志记录。这种方式可以确保我们在应用程序中共享同一个日志记录器,方便统一管理和查看日志消息。

单例模式的场景-登录框

class LoginBox {
  private static instance: LoginBox;
  private loggedIn: boolean;

  private constructor() {
    this.loggedIn = false;
  }

  public static getInstance(): LoginBox {
    if (!LoginBox.instance) {
      LoginBox.instance = new LoginBox();
    }
    return LoginBox.instance;
  }

  public login(username: string, password: string): void {
    // 执行登录逻辑,验证用户名和密码
    // ...

    // 登录成功
    this.loggedIn = true;
    console.log("User logged in");
  }

  public logout(): void {
    // 执行登出逻辑
    // ...

    // 登出成功
    this.loggedIn = false;
    console.log("User logged out");
  }

  public isLoggedIn(): boolean {
    return this.loggedIn;
  }
}

// 使用登录框
const loginBox1 = LoginBox.getInstance();
const loginBox2 = LoginBox.getInstance();

loginBox1.login("username", "password");
console.log(loginBox1.isLoggedIn()); // 输出: true
console.log(loginBox2.isLoggedIn()); // 输出: true

loginBox2.logout();
console.log(loginBox1.isLoggedIn()); // 输出: false
console.log(loginBox2.isLoggedIn()); // 输出: false

在上述示例中,我们定义了一个名为 LoginBox 的类,它实现了单例模式。

  • 类中的 instance 是一个私有的静态成员,用于存储单例实例。
  • 构造函数 constructor 被设为私有,以防止直接实例化 LoginBox 类。
  • getInstance 是一个公共的静态方法,用于获取单例实例。如果实例不存在,则创建一个新的实例并将其赋值给 instance,然后返回该实例。如果 instance 已经存在,直接返回它。
  • loggedIn 是一个私有成员,用于表示用户的登录状态。
  • login 是一个公共方法,用于执行用户登录逻辑,并更新登录状态。
  • logout 是一个公共方法,用于执行用户登出逻辑,并更新登录状态。
  • isLoggedIn 是一个公共方法,用于检查用户的登录状态。

在示例中,我们通过调用 LoginBox.getInstance() 方法来获取单例实例。我们创建了两个变量 loginBox1 和 loginBox2,它们实际上引用的是同一个登录框实例。

然后,我们通过调用 loginBox1.login("username", "password") 方法进行用户登录。由于 loginBox1 和 loginBox2 引用的是同一个实例,因此它们的登录状态是相互影响的。

最后,我们分别调用 loginBox1.isLoggedIn() 和 loginBox2.isLoggedIn() 方法来检查登录状态。可以看到,无论是通过 loginBox1 还是 loginBox2 访问,它们都返回相同的登录状态。

这个示例展示了如何使用单例模式实现登录框,并确保在整个应用程序中只存在一个登录框实例。这样可以保证用户的登录状态在全局范围内的一致性,并且方便地管理用户的登录和登出操作。

观察者模式

什么是观察者模式,它解决什么问题

观察者模式(Observer Pattern)是一种行为设计模式,用于在对象之间建立一种一对多的依赖关系,当一个对象的状态发生改变时,它的所有依赖对象都会收到通知并自动更新。

观察者模式解决的问题是对象之间的解耦和通信问题。当多个对象之间存在一种依赖关系,一个对象的状态改变需要通知其他对象进行相应的处理时,使用观察者模式可以有效地实现这种通信机制。

以下是观察者模式的几个关键角色:

  • Subject(被观察者) :也称为被观察者或可观察对象,它维护一组观察者对象,并在状态发生改变时通知观察者。
  • Observer(观察者) :也称为订阅者或监听者,它定义了一个接口,用于接收主题的通知并进行相应的处理。
  • ConcreteSubject(具体Subject) :实现Subject接口,维护具体的状态,并在状态改变时通知观察者。
  • ConcreteObserver(具体观察者) :实现观察者接口,定义具体的处理逻辑,接收主题的通知并进行相应的操作。

观察者模式的工作原理如下:

  1. 被观察者对象(Subject)维护了一个观察者列表,并提供方法用于注册、注销和通知观察者。
  2. 观察者对象(Observer)通过注册方法将自身添加到主题对象的观察者列表中,以便接收通知。
  3. 当被观察者对象的状态发生改变时,它会遍历观察者列表,并调用每个观察者的通知方法,将状态改变的信息传递给观察者。
  4. 观察者收到通知后,根据接收到的信息进行相应的处理,可能会更新自身的状态或执行其他操作。

观察者模式的优点包括:

  • 解耦性:主题和观察者之间的关系是松散耦合的,它们可以独立地进行扩展和修改,而不会相互影响。
  • 可维护性:由于对象之间的依赖关系明确,代码的维护和调试更加容易。
  • 可扩展性:可以很方便地添加新的观察者对象,而不需要修改现有的代码。

观察者模式适用于以下情况:

  • 当一个对象的改变需要同时影响其他对象,并且不希望对象之间紧密耦合时。
  • 当一个对象的改变需要通知一组对象,而不知道具体有多少个对象需要通知时。
  • 当需要确保对象之间的一致性,避免手动维护对象之间的关联关系时。

总而言之,观察者模式提供了一种松散耦合的通信机制,使得对象之间的依赖关系更加灵活和可扩展。它解决了对象之间解耦和通信的问题,提高了代码的可维护性和可扩展性。

观察者模式示例

// 主题(被观察者)
class Newspaper {
  constructor() {
    this.subscribers = [] // 存储观察者(订阅者)
  }

  // 添加观察者
  subscribe(observer) {
    this.subscribers.push(observer)
  }

  // 移除观察者
  unsubscribe(observer) {
    this.subscribers = this.subscribers.filter(
      (subscriber) => subscriber !== observer
    )
  }

  // 发布新闻
  publishNews(news) {
    console.log(`Breaking News: ${news}`)
    this.notifySubscribers(news)
  }

  // 通知所有观察者
  notifySubscribers(news) {
    this.subscribers.forEach((observer) => observer.update(news))
  }
}

// 观察者(订阅者)
class Subscriber {
  constructor(name) {
    this.name = name
  }

  // 更新方法,当接收到通知时调用
  update(news) {
    console.log(`${this.name} received the news: ${news}`)
  }
}

// 使用例子
const newspaper = new Newspaper()

const subscriber1 = new Subscriber('John')
const subscriber2 = new Subscriber('Alice')

// 订阅者订阅报纸
newspaper.subscribe(subscriber1)
newspaper.subscribe(subscriber2)

// 发布新闻
newspaper.publishNews('COVID-19 Vaccine Found')

// 输出:
// Breaking News: COVID-19 Vaccine Found
// John received the news: COVID-19 Vaccine Found
// Alice received the news: COVID-19 Vaccine Found

// John取消订阅
newspaper.unsubscribe(subscriber1)

// 发布新闻
newspaper.publishNews('Tech Company IPO')

// 输出:
// Breaking News: Tech Company IPO
// Alice received the news: Tech Company IPO

在这个例子中,Newspaper 是被观察者,它维护了一个订阅者列表。Subscriber 是观察者,它可以订阅、取消订阅并在接收到通知时执行相应的操作。当报纸发布新闻时,所有订阅的观察者都会收到通知并执行更新操作。

观察者模式的场景-Vue 组件生命周期

当我们说 Vue 组件的生命周期使用了观察者模式时,主要指的是 Vue 的响应式系统和生命周期钩子之间的关系。Vue 的响应式系统使用了观察者模式来追踪依赖项并在数据变化时通知相关的观察者。

在 Vue 中,每个组件实例都有一个对应的响应式数据对象。当数据对象的属性被访问时,Vue 将该属性的访问添加到依赖项列表中,当属性被修改时,通知所有依赖项进行更新。

下面是一个简化的例子:

class Dep {
  constructor() {
    this.subscribers = [];
  }

  depend() {
    if (currentObserver) {
      this.subscribers.push(currentObserver);
    }
  }

  notify() {
    this.subscribers.forEach(subscriber => subscriber());
  }
}

let currentObserver = null;

function observe(fn) {
  currentObserver = fn;
  fn();
  currentObserver = null;
}

const data = { count: 0 };
const dep = new Dep();

observe(() => {
  dep.depend();
  console.log(`Count is now: ${data.count}`);
});

// 修改数据并触发更新
data.count++;
dep.notify(); // Count is now: 1

在这个例子中,Dep 类代表一个数据的依赖项,observe 函数用于创建一个观察者并执行相应的逻辑。当 data.count 被修改时,dep.notify() 通知所有依赖项进行更新。

总的来说,Vue 的响应式系统和生命周期钩子之间的关系体现了观察者模式的思想,通过观察数据的变化和组件的生命周期阶段,我们能够实现更灵活、响应式的前端开发。

观察者模式的场景-Vue watch

在 Vue 中,watch 是一个观察者模式的应用场景,它允许你监听数据的变化并执行相应的回调函数。watch 提供了一种声明式的方式来响应数据的变化,而不必手动去轮询或在数据变化时手动执行某些逻辑。

观察者模式的场景-各种异步回调函数

观察者模式在处理各种异步回调函数时可以发挥很大的作用。异步回调函数通常包括事件处理、HTTP 请求、定时器等场景。观察者模式可以使得对象之间的关系更加松散,使得在事件发生时能够通知到多个观察者,从而实现更灵活的异步编程。

以下是观察者模式在异步回调函数中的应用场景:

  1. 事件处理:

    • 场景: 在前端开发中,用户交互、DOM 事件等通常采用事件监听器来处理。一个事件触发时,多个事件监听器(观察者)可以响应该事件。
    • 观察者模式: 事件作为主题,事件监听器作为观察者,当事件发生时,通知所有的事件监听器进行相应的处理。
  2. HTTP 请求:

    • 场景: 发起 HTTP 请求是一个异步操作,通常通过回调函数来处理请求的成功或失败。
    • 观察者模式: 发起 HTTP 请求的对象是主题,成功和失败的回调函数是观察者,当请求完成时,通知所有的观察者执行相应的回调。
  3. 定时器:

    • 场景: 使用 setTimeoutsetInterval 设置定时器,等待一段时间后执行回调函数。
    • 观察者模式: 定时器是主题,定时器结束时执行的回调函数是观察者,当定时器时间到达时,通知所有的观察者执行相应的回调。
  4. Promise:

    • 场景: 使用 Promise 处理异步操作,通过 then 方法注册回调函数。
    • 观察者模式: Promise 是主题,then 方法注册的回调函数是观察者,当 Promise 的状态发生变化时,通知所有观察者执行相应的回调。
  5. WebSocket:

    • 场景: WebSocket 是一种双向通信协议,可以接收服务端推送的消息。
    • 观察者模式: WebSocket 是主题,接收消息时执行的回调函数是观察者,当有新消息时,通知所有观察者执行相应的回调。

通过观察者模式,可以实现异步编程中的解耦和灵活性,让异步操作更容易理解和维护。在现代的 JavaScript 开发中,Promise、async/await 等语法糖也是基于观察者模式的思想。

观察者模式的场景-MutationObserver

MutationObserver 是一个现代 Web API,用于监测 DOM 树的变化。它可以应用于观察者模式的场景,用于监听和响应 DOM 的改变。

MutationObserver 的应用场景包括但不限于:

  1. 动态内容加载:当页面中的内容是通过异步加载或动态生成的时候,MutationObserver 可以用来监测这些内容的添加、修改或删除。例如,在无限滚动加载数据的情况下,可以使用 MutationObserver 监测新的数据是否被添加到了页面中。
  2. 表单验证:当用户在表单中输入内容时,可以使用 MutationObserver 监测表单元素的值是否发生了变化,从而实时验证用户输入的有效性。
  3. DOM 结构变化:当页面中的 DOM 结构发生变化时,MutationObserver 可以用来捕捉这些变化并进行相应的处理。例如,当某个元素的子元素被添加或删除时,可以使用 MutationObserver 来监听这些变化并执行相应的操作。
  4. UI 组件的状态变化:当 UI 组件的状态发生变化时,可以使用 MutationObserver 来监听这些变化并触发相应的回调函数。例如,当一个弹窗组件的显示状态发生变化时,可以使用 MutationObserver 来监听该组件的样式属性是否发生变化,并在变化发生时执行相应的动画效果或其他操作。

MutationObserver 提供了一种高效且灵活的方式来监测和响应 DOM 的变化,可以用于实现各种观察者模式的场景。它在现代 Web 开发中被广泛应用,特别适用于需要实时监测和处理 DOM 变化的情况。

检测子元素数量变化

下面是一个使用 MutationObserver 的示例,用于监测一个元素的子元素数量变化:

<!DOCTYPE html>
<html>
<head>
  <title>MutationObserver 示例</title>
</head>
<body>
  <div id="container">
    <ul id="list">
      <li>Item 1</li>
      <li>Item 2</li>
      <li>Item 3</li>
    </ul>
  </div>

  <script>
    // 目标元素
    const targetElement = document.getElementById('list');

    // 创建 MutationObserver 实例
    const observer = new MutationObserver((mutationsList) => {
      for (let mutation of mutationsList) {
        if (mutation.type === 'childList') {
          console.log('子元素数量发生变化');
          console.log('新的子元素数量:', targetElement.childElementCount);
        }
      }
    });

    // 配置项
    const config = { childList: true };

    // 开始观察目标元素
    observer.observe(targetElement, config);

    // 在一定时间后修改目标元素的子元素数量
    setTimeout(() => {
      targetElement.innerHTML += '<li>Item 4</li>';
    }, 2000);
  </script>
</body>
</html>

在上面的示例中,我们创建了一个 MutationObserver 实例,并通过 observe 方法开始观察目标元素 list 的子元素变化。当目标元素的子元素数量发生变化时,MutationObserver 的回调函数会被触发,并输出相关信息到控制台。

在示例中,我们通过 setTimeout 函数在 2 秒后向目标元素添加了一个新的子元素。当子元素数量发生变化时,MutationObserver 的回调函数会被触发,并输出新的子元素数量到控制台。

你可以在浏览器中运行这个示例,打开开发者工具的控制台,可以看到相关的输出信息。这个示例演示了如何使用 MutationObserver 监测 DOM 的变化并进行相应的处理。

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

观察者模式(Observer Pattern):
  1. 定义:

    • 观察者模式定义了一种一对多的依赖关系,当一个对象的状态发生变化时,所有依赖于它的对象都得到通知并被自动更新。
  2. 结构:

    • 主题(Subject)维护了一个观察者(Observer)的列表,包括注册、移除和通知等操作。
  3. 耦合性:

    • 高耦合性,观察者和主题直接交互。
  4. 同步性:

    • 通常是同步的,主题状态变化时,观察者立即得到通知并执行相应的操作。

示例代码:

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

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

  removeObserver(observer) {
    this.observers = this.observers.filter(o => o !== observer);
  }

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

class Observer {
  update() {
    console.log('Observer has been updated!');
  }
}

// Usage
const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();

subject.addObserver(observer1);
subject.addObserver(observer2);

subject.notify();
// Output:
// Observer has been updated!
// Observer has been updated!

发布-订阅模式(Publish-Subscribe Pattern):
  1. 定义:

    • 发布-订阅模式定义了一种对象间一对多的关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知。
  2. 结构:

    • 引入了一个事件通道(Event Channel)或者中介者(Mediator),发布者(Publisher)通过事件通道发布事件,订阅者(Subscriber)通过事件通道订阅感兴趣的事件。
  3. 耦合性:

    • 低耦合性,发布者和订阅者通过事件通道进行间接通信。
  4. 同步性:

    • 可以是同步或异步的,取决于具体的实现。订阅者可以选择订阅同步或异步的事件。

示例代码:

class EventChannel {
  constructor() {
    this.subscribers = {};
  }

  subscribe(event, callback) {
    if (!this.subscribers[event]) {
      this.subscribers[event] = [];
    }
    this.subscribers[event].push(callback);
  }

  publish(event, data) {
    if (this.subscribers[event]) {
      this.subscribers[event].forEach(callback => callback(data));
    }
  }
}

// Usage
const eventChannel = new EventChannel();

function subscriber1(data) {
  console.log('Subscriber 1 received:', data);
}

function subscriber2(data) {
  console.log('Subscriber 2 received:', data);
}

eventChannel.subscribe('eventA', subscriber1);
eventChannel.subscribe('eventA', subscriber2);

eventChannel.publish('eventA', 'Some data');
// Output:
// Subscriber 1 received: Some data
// Subscriber 2 received: Some data

区别总结:
  1. 耦合性:

    • 观察者模式中,高耦合性,观察者和主题直接交互。
    • 发布-订阅模式中,低耦合性,发布者和订阅者通过事件通道进行间接通信。
  2. 实现:

    • 观察者模式更加简单直接,主题直接通知观察者。
    • 发布-订阅模式更加灵活,引入了事件通道或中介者,支持多对多的关系。
  3. 同步性:

    • 观察者模式通常是同步的。
    • 发布-订阅模式可以是同步或异步的,取决于具体的实现方式。

发布订阅模式的场景-自定义事件-Vue3 推荐使用 mitt

在 Vue 3 中,自定义事件很常见,而发布-订阅模式(Publish-Subscribe Pattern)是一种用于实现自定义事件的强大方式。Vue 3 中推荐使用 mitt 库,它是一个小巧且功能强大的事件总线库,非常适合在 Vue 3 项目中使用。

下面是一个简单的示例,演示了在 Vue 3 中使用 mitt 进行自定义事件的场景:

  1. 首先,你需要安装 mitt
npm install mitt
  1. 在你的 Vue 3 项目中,使用 mitt
// main.js
// main.js 或其他入口文件
import { createApp } from 'vue';
import mitt from 'mitt';
import App from './App.vue';

const app = createApp(App);

// 在 app.config.globalProperties 上挂载事件总线
app.config.globalProperties.$eventBus = mitt();

app.mount('#app');

  1. 在组件中使用事件总线:
<!-- ChildComponent.vue -->
<template>
  <div>
    <button @click="triggerEvent">触发自定义事件</button>
  </div>
</template>

<script>
export default {
  methods: {
    triggerEvent() {
      // 触发自定义事件
      this.$eventBus.emit('custom-event', 'Hello from ChildComponent!');
    },
  },
};
</script>

<!-- ParentComponent.vue -->
<template>
  <div>
    <p>父组件接收到的消息: {{ message }}</p>
    <ChildComponent />
  </div>
</template>

<script>
import { ref, onMounted, onBeforeUnmount } from 'vue';

export default {
  setup() {
    const message = ref('');

    // 订阅自定义事件
    const handleCustomEvent = (data) => {
      message.value = data;
    };

    // 在组件挂载时订阅事件,在组件销毁前取消订阅
    onMounted(() => {
      this.$eventBus.on('custom-event', handleCustomEvent);
    });

    onBeforeUnmount(() => {
      this.$eventBus.off('custom-event', handleCustomEvent);
    });

    return { message };
  },
};
</script>

这个示例中,ChildComponent 组件触发了一个自定义事件,而 ParentComponent 组件订阅了这个事件,接收到消息后更新了界面。这种模式在组件之间的通信中非常有用,尤其是在复杂的组件结构中,避免了直接通过 props 或 emit 传递事件的繁琐操作。

发布订阅模式的场景-postMessage 通讯

在嵌套的 iframe 场景中,可以使用 postMessage 结合发布订阅模式进行跨 iframe 通信。下面是一个简单的示例,演示了如何在一个包含嵌套 iframe 的页面中使用 postMessage 和发布订阅模式进行通信。

<!-- parent.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Parent Window</title>
</head>
<body>
  <h1>Parent Window</h1>
  <iframe id="childFrame" src="child.html" width="300" height="200"></iframe>

  <script>
    const childFrame = document.getElementById('childFrame').contentWindow;

    // 发布订阅模式的简单实现
    const eventBus = {
      subscribers: {},
      subscribe(event, callback) {
        if (!this.subscribers[event]) {
          this.subscribers[event] = [];
        }
        this.subscribers[event].push(callback);
      },
      publish(event, data) {
        if (this.subscribers[event]) {
          this.subscribers[event].forEach(callback => {
            callback(data);
          });
        }
      }
    };

    // 监听消息
    window.addEventListener('message', (event) => {
      const { type, payload } = event.data;
      console.log('Parent Window received message:', type, payload);
      
      // 发布消息给订阅者
      eventBus.publish(type, payload);
    });

    // 订阅事件
    eventBus.subscribe('messageFromChild', (data) => {
      console.log('Parent Window received message from child:', data);
    });

    // 发送消息给嵌套的 iframe
    function sendMessageToChild() {
      const message = 'Hello from Parent Window!';
      childFrame.postMessage({ type: 'messageFromParent', payload: message }, '*');
    }

    // 触发发送消息
    sendMessageToChild();
  </script>
</body>
</html>

<!-- child.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Child Window</title>
</head>
<body>
  <h1>Child Window</h1>

  <script>
    // 发布订阅模式的简单实现
    const eventBus = {
      subscribers: {},
      subscribe(event, callback) {
        if (!this.subscribers[event]) {
          this.subscribers[event] = [];
        }
        this.subscribers[event].push(callback);
      },
      publish(event, data) {
        if (this.subscribers[event]) {
          this.subscribers[event].forEach(callback => {
            callback(data);
          });
        }
      }
    };

    // 监听消息
    window.addEventListener('message', (event) => {
      const { type, payload } = event.data;
      console.log('Child Window received message:', type, payload);

      // 发布消息给订阅者
      eventBus.publish(type, payload);
    });

    // 订阅事件
    eventBus.subscribe('messageFromParent', (data) => {
      console.log('Child Window received message from parent:', data);
    });

    // 发送消息给父窗口
    function sendMessageToParent() {
      const message = 'Hello from Child Window!';
      window.parent.postMessage({ type: 'messageFromChild', payload: message }, '*');
    }

    // 触发发送消息
    sendMessageToParent();
  </script>
</body>
</html>

在这个示例中,两个窗口通过 postMessage 实现了跨 iframe 的通信,同时使用发布订阅模式,父窗口和子窗口都能订阅并接收彼此发送的消息。

请注意,为了安全起见,postMessage第二个参数 '*' 表示消息可以被任何窗口接收。在实际应用中,最好明确指定接收消息的窗口的 origin,以防止恶意攻击。

迭代器模式

普通的 for 循环并不是迭代器

普通的 for 循环并不是迭代器,而是一种基础的循环结构,通过索引来遍历数组或类数组对象的元素。迭代器是一种更为抽象的概念,它提供了一种统一的接口来访问一个对象的元素,无论这个对象是数组、集合、映射还是其他数据结构。

在 JavaScript 中,有一些内置的迭代器和可迭代对象的概念,比如:

for...of 循环: 用于遍历可迭代对象(数组、字符串、Map、Set 等)的语法糖,提供了更简洁的语法。

const iterable = [1, 2, 3];

for (const item of iterable) {
  console.log(item);
}

Iterator 接口: JavaScript 中的迭代器是一个拥有 next 方法的对象,每次调用 next 方法都会返回一个包含 valuedone 属性的对象。

const iterable = [1, 2, 3];
const iterator = iterable[Symbol.iterator]();

console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

这表示你在创建一个数组 iterable,其中包含值 1、2、3。在 JavaScript 中,数组是一种可迭代对象,即可以通过迭代器来循环遍历。

接着,通过 iterable[Symbol.iterator](),你获取了数组的默认迭代器。在 JavaScript 中,迭代器是一个带有 next() 方法的对象,该方法返回一个包含 valuedone 属性的对象。数组默认具有一个通过 Symbol.iterator 方法获得的迭代器。

可迭代对象(Iterable): 实现了 Symbol.iterator 方法的对象被认为是可迭代的,可以在 for...of 循环中使用。

const iterable = {
  items: [1, 2, 3],
  [Symbol.iterator]: function () {
    let index = 0;
    return {
      next: () => ({
        value: this.items[index++],
        done: index > this.items.length
      })
    };
  }
};

for (const item of iterable) {
  console.log(item);
}

for...in 循环: 用于遍历可枚举的对象(数组、字符串、对象)的语法糖,提供了更简洁的语法。

const person = {
  name: 'John',
  age: 30,
  gender: 'male'
};

for (let key in person) {
  if (person.hasOwnProperty(key)) {
    console.log(key + ': ' + person[key]);
  }
}

在这个例子中,for...in 循环用于遍历 person 对象的属性。使用 hasOwnProperty 来确保只输出对象自身的属性。输出将是:

name: John
age: 30
gender: male

需要注意的是,for...in 不仅遍历对象的可枚举属性,还遍历原型链上的属性。如果只想遍历对象自身的属性,最好使用 Object.keysObject.valuesObject.entries 等方法。

迭代器模式的场景-JS 内置迭代器 Symbol.iterator

在 JavaScript 中,使用内置迭代器 Symbol.iterator 的场景非常常见,它是迭代器模式的一种具体应用。下面是一些使用 Symbol.iterator 的场景:

for...of 循环: for...of 循环是使用 Symbol.iterator 的典型场景。它允许你遍历实现了 Symbol.iterator 的可迭代对象。

const array = [1, 2, 3];

for (const item of array) {
  console.log(item);
}

在这里,array 是一个可迭代对象,而 for...of 循环内部会自动调用 array[Symbol.iterator]() 获取迭代器,并遍历其中的元素。

扩展运算符 (...): 扩展运算符也使用了 Symbol.iterator。当你使用扩展运算符时,它会自动调用对象的 Symbol.iterator 方法来获取可迭代对象的元素。

const array1 = [1, 2, 3];
const array2 = [...array1];

在这里,array2 获取了 array1 的迭代器,通过迭代器遍历并复制了数组的元素。

解构赋值: 解构赋值语法也使用了 Symbol.iterator。当你对可迭代对象进行解构赋值时,它会调用对象的 Symbol.iterator 方法。

const array = [1, 2, 3];
const [first, second, third] = array;

在这里,解构赋值语法使用了数组的迭代器来提取元素。

Map 和 Set: MapSet 是 JavaScript 中的集合对象,它们都实现了 Symbol.iterator 方法,允许你使用 for...of 循环或扩展运算符来遍历它们的元素。

const map = new Map([
  ['key1', 'value1'],
  ['key2', 'value2']
]);

for (const [key, value] of map) {
  console.log(key, value);
}

  1. 在这里,Map 对象使用了 Symbol.iterator 来支持迭代。

总体而言,使用 Symbol.iterator 是 JavaScript 中迭代器模式的一个具体实现,它提供了一种统一的方法来遍历集合对象,使得不同类型的对象能够以相似的方式进行迭代。

迭代器的场景-自定义简易迭代器

自定义简易迭代器可以在处理特定场景时提供灵活性和可读性。以下是一些可能适用的场景:

  1. 特定数据结构的迭代: 当你有一种特殊的数据结构,想要以自定义的方式进行迭代时,自定义迭代器是一个合适的选择。例如,你可能有一个存储学生信息的对象,想要按照一定的顺序迭代学生信息。
const studentDatabase = {
  Alice: { age: 22, grade: 'A' },
  Bob: { age: 21, grade: 'B' },
  // ...
};

studentDatabase[Symbol.iterator] = function () {
  const students = Object.entries(this);
  let index = 0;

  return {
    next: function () {
      if (index < students.length) {
        const [name, info] = students[index++];
        return { value: { name, info }, done: false };
      } else {
        return { value: undefined, done: true };
      }
    }
  };
};

// 使用自定义迭代器遍历学生信息
for (const student of studentDatabase) {
  console.log(student);
}

如果 studentDatabase 是一个实现了迭代器协议的可迭代对象(即具有 Symbol.iterator 方法),那么 for...of 循环会自动调用该迭代器的 next 方法,并在每次迭代中获取迭代器返回的 value 属性。

异步迭代: 如果你处理的是异步数据,可能需要自定义异步迭代器。这在处理大量异步数据时是非常有用的。

const asyncData = {
  data: [1, 2, 3],
  [Symbol.asyncIterator]: async function* () {
    for (const item of this.data) {
      // 模拟异步操作
      await new Promise(resolve => setTimeout(resolve, 1000));
      yield item;
    }
  }
};

// 使用异步迭代器
(async () => {
  for await (const value of asyncData) {
    console.log(value);
  }
})();

  • data 数组包含了要遍历的数据 [1, 2, 3]
  • 使用 [Symbol.asyncIterator] 符号创建了一个异步迭代器。迭代器通过 async function* () 定义,表示这是一个异步生成器函数。

生成器函数: 使用生成器函数创建自定义迭代器是一种简便的方法。生成器函数使用 yield 关键字,允许你按需生成值,而不需要一次性生成整个序列。

function* simpleIterator() {
  yield 1;
  yield 2;
  yield 3;
}

// 使用生成器函数创建自定义迭代器
const iterator = simpleIterator();

console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

解构赋值

数组结构赋值

// 基本的数组解构赋值
const numbers = [1, 2, 3];
const [a, b, c] = numbers;
console.log(a, b, c); // 输出:1 2 3

// 跳过某些元素
const [first, , third] = numbers;
console.log(first, third); // 输出:1 3

// 剩余运算符(rest operator)获取剩余的元素
const [head, ...tail] = numbers;
console.log(head, tail); // 输出:1 [2, 3]

对象解构赋值

// 基本的对象解构赋值
const person = { name: 'Alice', age: 25, country: 'Wonderland' };
const { name, age, country } = person;
console.log(name, age, country); // 输出:Alice 25 Wonderland

// 重命名变量
const { name: personName, age: personAge } = person;
console.log(personName, personAge); // 输出:Alice 25

// 默认值
const { name: newName, age: newAge, occupation: newOccupation = 'Unknown' } = person;
console.log(newName, newAge, newOccupation); // 输出:Alice 25 Unknown

函数参数结构赋值

// 函数参数解构赋值
function printPersonInfo({ name, age }) {
  console.log(`Name: ${name}, Age: ${age}`);
}

const person = { name: 'Bob', age: 30 };
printPersonInfo(person); // 输出:Name: Bob, Age: 30

迭代器模式的场景-使用Generator遍历DOM树

在前端开发中,Generator 函数可以用于遍历 DOM 树的场景,特别是当处理深度嵌套的 DOM 结构时。通过递归地使用 Generator,可以更清晰地表达对 DOM 树的深度优先遍历。

以下是一个简单的例子,演示如何使用 Generator 遍历 DOM 树:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>DOM Tree Traversal</title>
</head>
<body>
  <div id="root">
    <h1>Hello, DOM!</h1>
    <ul>
      <li>Item 1</li>
      <li>Item 2</li>
      <li>Item 3</li>
    </ul>
    <div>
      <p>Nested Paragraph</p>
      <span>Another Span</span>
    </div>
  </div>

  <script>
    // Generator 函数用于深度优先遍历 DOM 树
    function* traverseDOM(node) {
      yield node; // 返回当前节点

      // 遍历子节点
      const children = node.children;
      for (const child of children) {
        yield* traverseDOM(child);
      }
    }

    // 获取根节点
    const root = document.getElementById('root');

    // 使用 Generator 迭代器遍历 DOM 树
    const domIterator = traverseDOM(root);

    // 输出节点信息
    for (const node of domIterator) {
      console.log(node.tagName);
    }
  </script>
</body>
</html>

在这个例子中,traverseDOM 是一个 Generator 函数,它接受一个 DOM 节点作为参数,并通过 yield 返回当前节点,然后递归遍历所有子节点。通过 yield* traverseDOM(child),实现了深度优先遍历。

这种方法使得代码更具可读性和可维护性,特别是在处理复杂的嵌套结构时。Generator 的暂停和继续执行的特性为 DOM 树的遍历提供了一种清晰而灵活的方式。

yield*

yield* 是在 Generator 函数内部使用的一种语法,用于委托(delegate)给另一个 Generator 函数、可迭代对象或类数组对象。它实现了在一个 Generator 函数中调用另一个 Generator 函数,使得可以在当前 Generator 函数中迭代另一个 Generator 函数的值。

function* generator1() {
  yield 1;
  yield 2;
  yield 3;
}

function* generator2() {
  yield 'a';
  yield 'b';
  yield 'c';
}

function* combinedGenerator() {
  yield* generator1();
  yield* generator2();
}

const combinedIterator = combinedGenerator();

console.log(combinedIterator.next().value); // 输出:1
console.log(combinedIterator.next().value); // 输出:2
console.log(combinedIterator.next().value); // 输出:3
console.log(combinedIterator.next().value); // 输出:'a'
console.log(combinedIterator.next().value); // 输出:'b'
console.log(combinedIterator.next().value); // 输出:'c'

深度优先遍历

深度优先遍历(Depth-First Search, DFS)是一种树(Tree)或图(Graph)结构的遍历算法。在深度优先遍历中,我们从根节点出发,首先访问根节点,然后递归地访问每个子节点。具体来说,深度优先遍历会一直往下访问,直到达到树的最底层,然后再回溯到上一层,继续访问其他节点。

深度优先遍历通常使用递归的方式实现,也可以使用栈(Stack)来辅助实现。

装饰器模式

装饰器模式是一种结构型设计模式,其主要目的是通过在不改变原始对象结构的情况下,动态地给对象添加新的功能或职责。装饰器模式通过将对象包装在一个装饰器类的实例中,以实现对原始对象的透明扩展。

主要角色:

  1. Component(组件): 定义一个对象接口,可以动态地给这些对象添加新的职责。
  2. ConcreteComponent(具体组件): 实现了 Component 接口的具体对象,是被装饰的对象。
  3. Decorator(装饰器): 持有一个指向 Component 对象的引用,并定义一个与 Component 接口一致的接口。
  4. ConcreteDecorator(具体装饰器): 实现了 Decorator 接口的具体装饰器类,负责给 ConcreteComponent 添加新的职责。

问题解决:

  1. 动态添加功能: 装饰器模式允许动态地给对象添加新的功能,而无需修改其代码。这使得系统更加灵活,能够在运行时动态地组合对象的行为。
  2. 避免使用子类扩展: 通过装饰器模式,可以避免创建大量的子类来扩展对象的功能。相比之下,装饰器模式更具灵活性,可以按需组合不同的装饰器。

示例:

考虑一个咖啡店的例子,原始咖啡是一个具体组件,而各种调料(牛奶、糖、摩卡等)可以作为具体装饰器,通过动态组合这些装饰器,可以创建不同口味的咖啡。

// Component(咖啡)
class Coffee {
  cost() {
    return 5;
  }
}

// Decorator(装饰器)
class CoffeeDecorator {
  constructor(coffee) {
    this._coffee = coffee;
  }

  cost() {
    return this._coffee.cost();
  }
}

// ConcreteDecorator(具体装饰器)
class MilkDecorator extends CoffeeDecorator {
  cost() {
    return super.cost() + 2;
  }
}

class SugarDecorator extends CoffeeDecorator {
  cost() {
    return super.cost() + 1;
  }
}

// 使用装饰器组合不同口味的咖啡
const plainCoffee = new Coffee();
console.log(plainCoffee.cost()); // 输出:5

const milkCoffee = new MilkDecorator(plainCoffee);
console.log(milkCoffee.cost()); // 输出:7

const sweetMilkCoffee = new SugarDecorator(milkCoffee);
console.log(sweetMilkCoffee.cost()); // 输出:8

在这个例子中,Coffee 是具体组件,CoffeeDecorator 是装饰器,MilkDecoratorSugarDecorator 是具体装饰器。通过组合这些装饰器,可以创建不同调料的咖啡,而不需要修改原始咖啡类的代码。这就是装饰器模式的灵活性和可扩展性。

代理模式

什么是代理模式

当我们谈论代理模式时,可以将其比喻为一种“替身”或“中间人”设计。代理模式允许我们使用一个代理对象来控制对另一个对象的访问。这个代理对象担任了两者之间的沟通角色,可以在实际对象执行前后进行一些额外的操作。

比如,你去购物,不一定直接找厂家购买商品,而是通过零售商(代理)来完成购买。零售商可能会帮你做一些额外的事情,比如检查货物、提供包装等。在这个例子中,零售商就是代理,你和厂家之间的沟通通过零售商来进行。

解决哪些问题

  1. 访问控制: 控制对对象的访问,实现对敏感操作的权限控制。
  2. 延迟加载(Lazy Loading): 可以推迟创建或加载实际对象,提高系统性能,特别是在需要时才真正创建实际对象。
  3. 增加额外功能: 代理可以在调用实际对象的方法前后执行额外的操作,如日志记录、性能监控、缓存等。

代理模式的场景有哪些

1、虚拟代理(Virtual Proxy): 当对象创建开销较大时,可以使用虚拟代理来推迟对象的实际创建,只有在需要时才进行实际的创建。这可以提高系统的性能。例如,图片加载前使用一个虚拟代理,实际的图片对象只在用户浏览到需要显示的位置时才被真正加载。

2、保护代理(Protection Proxy): 用于控制对对象的访问权限,确保只有具有足够权限的用户才能访问某些敏感操作。例如,在一个文件管理系统中,使用保护代理来限制用户对文件的访问。

3、缓存代理(Caching Proxy): 在访问实际对象前,检查是否已经有相同请求的结果缓存,以提高性能。例如,Web 请求的结果可以被缓存,下一次相同的请求可以直接返回缓存的结果。

4、远程代理(Remote Proxy): 用于在不同地址空间中代表对象。例如,远程方法调用(RMI)中的远程代理,允许在不同的 JVM 中调用对象的方法。

5、智能(引用)代理(Smart/Reference Proxy): 在访问实际对象时添加一些额外的逻辑,如引用计数、懒加载等。例如,可以使用引用代理来跟踪一个对象被引用的次数,以便在没有引用时释放资源。

这些场景只是代理模式应用的一小部分,实际上代理模式非常灵活,可以根据具体的需求设计出更多的变体。

Nginx反向代理

当我们谈论 Nginx 反向代理时,我们通常是指 Nginx 作为一个中间层,接收客户端的请求并将其转发给一个或多个后端服务器。这种反向代理的模式有很多用途,包括负载均衡、SSL 终结、安全性等。

1. 配置 Nginx

server {
    listen 80;  # 监听端口 80
    server_name example.com;  # 设置域名

    location / {
        proxy_pass http://backend_server;  # 反向代理配置,将请求转发给后端服务器
        proxy_set_header Host $host;  # 设置请求头信息,传递客户端的信息给后端服务器
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

upstream backend_server {
    server backend1.example.com;  # 后端服务器1
    server backend2.example.com;  # 后端服务器2
    # 可以添加更多的后端服务器
}

2. 工作原理

  • 客户端发起请求: 客户端发送请求到 Nginx,这个请求可以是 HTTP 或 HTTPS 请求。
  • Nginx 接收请求: Nginx 接收到客户端的请求,根据配置文件中的 location 块找到匹配的规则,然后根据 proxy_pass 指令将请求转发给后端服务器。
  • 后端服务器处理请求: 后端服务器接收到 Nginx 转发的请求,处理请求并生成响应。
  • Nginx 返回响应: 后端服务器将响应发送回 Nginx。
  • Nginx 返回给客户端: Nginx 将后端服务器的响应返回给客户端。

3. 配置解释

  • proxy_pass: 指定要转发请求的后端服务器的地址。
  • proxy_set_header: 设置要传递给后端服务器的请求头信息,包括客户端的 IP 地址等。
  • upstream: 定义一组后端服务器,可以包含多个服务器,用于负载均衡。

4. 使用场景

  • 负载均衡: 将流量分发到多个后端服务器,提高系统的可用性和性能。
  • SSL 终结: Nginx 可以处理 SSL/TLS 连接,将加密和解密的负担从后端服务器卸载。
  • 安全性: 隐藏后端服务器的真实 IP 地址,增加系统的安全性。
  • 缓存: Nginx 可以缓存后端服务器的响应,减轻后端服务器的负载,提高访问速度。

总体而言,Nginx 反向代理是一个强大而灵活的工具,用于优化和保护 Web 应用程序。

webpack-dev-server的代理proxy

webpack-dev-server 的代理功能属于开发阶段的代理设置,主要用于解决跨域问题Mock 数据(更多地是解决网络请求的问题)

// webpack.config.js 或 webpack.dev.js

module.exports = {
  // ...其他配置

  devServer: {
    proxy: {
      '/api': {
        target: 'http://localhost:8000',  // 后端服务地址
        pathRewrite: { '^/api': '' },     // 路径重写,将 /api 前缀去掉
        changeOrigin: true,                // 支持跨域
      },
    },
  },
};

Proxy

Proxy 是 JavaScript 提供的一种元编程特性,用于创建一个对象的代理,可以拦截并定义对象上的基本操作。这使得我们能够在对象上添加自定义行为,例如拦截属性的读取、设置、删除,函数调用,以及其他操作。

Proxy 的语法:

const proxy = new Proxy(target, handler);
  • target: 要代理的目标对象。
  • handler: 包含一组拦截器的对象,用于定义代理的行为。

Proxy 的基本使用:

const target = {
  name: 'John',
  age: 30,
};

const handler = {
  get(target, key, receiver) {
    console.log(`Getting property ${key}`);
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) {
    console.log(`Setting property ${key} to ${value}`);
    return Reflect.set(target, key, value, receiver);
  },
};

const proxy = new Proxy(target, handler);

// 使用代理对象
console.log(proxy.name);   // 输出: Getting property name, John
proxy.age = 31;            // 输出: Setting property age to 31

在上述示例中,Proxy 对象 proxy 代理了目标对象 target,并通过 handler 定义了拦截器。当通过代理对象读取或设置属性时,拦截器中的对应方法会被触发,允许我们自定义行为。

Reflect 是 JavaScript 中的一个内建方法,用于获取对象的属性值。与直接使用对象的属性访问运算符(.)或方括号运算符([])不同,Reflect.get 允许以函数的形式进行属性的读取操作。

  • target: 要从中获取属性的目标对象。
  • key: 要获取的属性的键名。
  • receiver(可选): 如果 target 对象中指定属性的值是一个 getter 函数,receiver 将成为该函数调用时的 this 值。

Proxy 的拦截器方法

  • get(target, prop, receiver) 拦截对象属性的读取操作。
  • set(target, prop, value, receiver) 拦截对象属性的设置操作。
  • deleteProperty(target, prop) 拦截对象属性的删除操作。
  • apply(target, thisArg, argumentsList) 拦截函数的调用操作。
  • construct(target, argumentsList, newTarget) 拦截 new 操作符,用于创建实例。
  • 其他拦截方法如 hasownKeysdefineProperty 等。

Reflect 对象: Reflect 是一个内建对象,提供了一组与操作对象相关的方法,这些方法与一些 Object 的方法功能相似,但是更具有操作性。

Reflect.getReflect.set:

const target = {
  name: 'John',
  age: 30,
};

const nameValue = Reflect.get(target, 'name');
const ageValue = Reflect.get(target, 'age');

console.log(nameValue);  // 输出: John
console.log(ageValue);   // 输出: 30

Reflect.set(target, 'age', 31);
console.log(target.age); // 输出: 31

Reflect.getReflect.set 分别用于获取和设置对象的属性值,与 Proxy 中的拦截器方法对应。

Vue3响应式实现(简化版)
// 依赖追踪的 Map
const targetMap = new WeakMap();

// 当前活跃的 effect 函数栈
const effectStack = [];

// 获取当前活跃的 effect 函数
function getCurrentEffect() {
  return effectStack[effectStack.length - 1];
}

// 依赖追踪函数
function track(target, key) {
  const effect = getCurrentEffect();

  if (effect) {
    let depsMap = targetMap.get(target);
    if (!depsMap) {
      depsMap = new Map();
      targetMap.set(target, depsMap);
    }

    let dep = depsMap.get(key);
    if (!dep) {
      dep = new Set();
      depsMap.set(key, dep);
    }

    dep.add(effect);
  }
}

// 触发依赖更新函数
function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (depsMap) {
    const dep = depsMap.get(key);
    if (dep) {
      dep.forEach(effect => effect());
    }
  }
}

// 创建响应式数据
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      track(target, key);
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const oldValue = target[key];
      const result = Reflect.set(target, key, value, receiver);
      trigger(target, key);
      return result;
    },
  });
}

// 创建 effect 函数
function effect(callback) {
  const wrappedEffect = function () {
    try {
      effectStack.push(wrappedEffect);
      return callback();
    } finally {
      effectStack.pop();
    }
  };

  // 初始化执行一次,进行依赖收集
  wrappedEffect();

  return wrappedEffect;
}

// 使用响应式数据
const state = reactive({
  message: 'Hello Vue!',
});

// 创建 effect 函数,它会在 message 发生变化时执行
const reactiveEffect = effect(() => {
  console.log(state.message);
});

// 修改 message 的值,触发 effect 函数执行
state.message = 'Vue 3 is awesome!';

  1. targetMapeffectStack

    • targetMap 是一个 WeakMap,用于存储目标对象和它们的依赖映射关系。
    • effectStack 是一个数组,用于维护当前活跃的 effect 函数栈。这是因为一个 effect 函数可能嵌套调用,形成一个栈的结构。
  2. getCurrentEffect 函数:

    • 用于获取当前活跃的 effect 函数。它从 effectStack 中取出最后一个函数,即当前正在执行的 effect
  3. track 函数:

    • 用于依赖追踪。当 effect 函数执行时,会在访问响应式数据的属性时调用 track,将当前 effect 添加到依赖映射中。
  4. trigger 函数:

    • 用于触发依赖更新。当响应式数据的属性发生变化时,会调用 trigger,找到相应的依赖集合,然后逐个执行依赖中的 effect 函数。
  5. reactive 函数:

    • 创建一个响应式数据的 Proxy。在访问和修改数据时,会调用 tracktrigger 实现依赖追踪和更新。
  6. effect 函数:

    • 创建一个 effect 函数。这个函数会被包装,确保在执行时能够正确地将自己添加到和从 effectStack 中推出,以及在初始化时执行一次进行依赖收集。
  7. 使用响应式数据和 effect 函数:

    • 创建了一个响应式对象 state,并创建了一个 effect 函数 reactiveEffect,它在 state.message 发生变化时执行。
  8. 修改数据触发更新:

    • 当执行 state.message = 'Vue 3 is awesome!' 时,会触发 trigger 函数,找到依赖集合中的 reactiveEffect,然后执行它,最终在控制台打印新的消息。

Map、WeakMap、Set、WeakSet有什么区别

  1. Map:

    • Map 对象是一组键值对的集合,其中的键和值可以是任意类型的。
    • Map 中的键是唯一的,每个键只能出现一次。
    • Map 可以使用任意类型的值作为键或值。
    • Map 是一个可迭代的数据结构,可以轻松遍历其中的键值对。
  2. WeakMap:

    • WeakMap 与 Map 类似,也是一组键值对的集合。
    • 与 Map 不同的是,WeakMap 中的键必须是对象,而值可以是任意类型的。
    • WeakMap 中的键是弱引用的,这意味着如果键不再被其他对象引用,那么键值对会被垃圾回收。
    • 由于键是弱引用的,WeakMap 不支持迭代和枚举,因此没有办法获取其中的所有键值对。
  3. Set:

    • Set 对象是一组唯一值的集合,其中的值可以是任意类型的。
    • Set 中的值是唯一的,每个值只能出现一次。
    • Set 可以用来检查某个值是否存在于集合中,也可以用来删除某个值。
    • Set 是一个可迭代的数据结构,可以轻松遍历其中的值。
  4. WeakSet:

    • WeakSet 与 Set 类似,也是一组唯一值的集合。
    • 与 Set 不同的是,WeakSet 中的值必须是对象。
    • WeakSet 中的值是弱引用的,这意味着如果值不再被其他对象引用,那么该值会被垃圾回收。
    • 由于值是弱引用的,WeakSet 不支持迭代和枚举,因此没有办法获取其中的所有值。

总体来说,Map 和 Set 是常规的集合类型,而 WeakMap 和 WeakSet 则更适合需要进行对象存储的情况,并且它们的特性使得它们在处理对象引用时更加灵活。