跳到主要内容

面向对象✅

什么是面向对象编程(OOP)?它与函数式编程有何区别?

答案

面向对象编程(Object-oriented programming OOP)是一种通过对象来封装数据与行为的编程范式。对象是具有状态(数据)和行为(方法)的实体,OOP 的核心思想在于将复杂问题分解为相互作用的对象,从而提高代码的模块化和可维护性。与函数式编程(FP)相比,OOP 更强调状态和可变性,而 FP 强调无副作用的纯函数和不可变数据。

在 OOP 中,开发者通过定义类(或构造函数)来创建对象,并利用继承、多态和封装来管理代码复杂度。通过对象实例来操作和维护状态。

OOP 通过封装、组合、继承、多态等特性促进代码复用和模块化。但在前端场景中,过度使用继承可能导致组件层级过深,增加维护成本。状态管理不当也容易引发 UI 的非预期变化。

在前端开发中,OOP 的典型场景如下:

  • 组件化开发:如 React、Vue 的组件定义等,基于组件模式实现 UI 的封装与复用。
  • 事件系统:利用发布-订阅模式(观察者模式的一种)实现组件间的解耦和灵活通信。
  • Express 中间件:采用了职责链模式,请求在多个中间件中依次处理,每个中间件负责特定的任务。

JavaScript 中的对象和类是如何定义的?请举例说明

答案

对象创建方法

方法语法示例优势局限性
对象字面量const obj = { prop: value }简洁易读,适合单例对象不支持创建多个相似结构的对象
new 关键字初始化function Person(name) { this.name = name }class A {}可创建多个实例,支持原型继承适用于复用的对象,简单对象创建建议采用字面量形式
Object.assign()const obj = Object.assign({}, sourceObj)可合并多个对象,实现浅拷贝只复制可枚举属性,嵌套对象仍共享引用
Object.create()const obj = Object.create(proto)直接设置原型,实现继承初始化属性较繁琐

类创建方法

方法语法示例优势局限性
ES6 类语法class Person { constructor(name) { this.name = name } }语法清晰,支持继承和静态方法本质仍是原型继承的语法糖
构造函数+原型function Person(name){this.name=name}; Person.prototype.greet=function(){}兼容性好分离实例属性和原型方法, 代码组织分散,不直观
类表达式const Person = class { constructor(name) { this.name = name } }可用于闭包和高阶函数与类声明类似,仅语法形式不同
IIFE类模式const Person = (function(){return class{}}())可创建私有状态和方法较复杂,可读性较差

示例说明如下

/**
* 1. 对象字面量
* 关键特性: 直接定义对象属性和方法
* 优势: 简洁易读,适合单例对象
* 局限性: 不支持创建多个相似结构的对象
* */
const objLiteral = { prop: 'value' }
console.log(objLiteral) // { prop: 'value' }

/**
* 2. new 关键字初始化
* 关键特性: 通过 new 关键字创建实例
* 优势: 可创建多个实例,支持原型继承
* 局限性: 适用于复用的对象,简单对象创建建议采用字面量形式
*/

// 2.1 使用构造函数模式
function Person (name) {
this.name = name
}
const person1 = new Person('Alice')
console.log(person1) // Person { name: 'Alice' }

// 2.2 使用类模式
class Animal {
constructor (type) {
this.type = type
}
}
const animal1 = new Animal('Dog')
console.log(animal1) // Animal { type: 'Dog' }
/**
* 3. Object.assign()
* 关键特性: 复制一个或多个源对象的属性到目标对象
* 优势: 可合并多个对象,实现浅拷贝
* 局限性: 只复制可枚举属性,嵌套对象仍共享引用
*/

//
const sourceObj = { a: 1, b: 2 }
const objAssign = Object.assign({}, sourceObj)
console.log(objAssign) // { a: 1, b: 2 }

/**
* 4. Object.create()
* 关键特性: 基于现有对象创建新对象
* 优势: 直接设置原型,实现继承
* 局限性: 初始化属性较繁琐
*/
const proto = { greet: function () { console.log('Hello!') } }
const objCreate = Object.create(proto)
console.log(objCreate) // {}
objCreate.greet() // Hello!
提示

注意类和对象的区别,对象是一个具体的实例,类是一个创建对象的模版,现实映射的话,类是工厂加工的模具用来批量产出同种类型的产品,而对象是模具加工出来的产品。

什么是继承,JavaScript 中的继承机制的实现方式?

答案

继承是面向对象编程中的一种机制,允许一个对象(子类)获取另一个对象(父类)的属性和方法。它使得代码复用成为可能,并且可以通过扩展父类的功能来实现多态。

JavaScript 中的使用原型链实现了继承功能,原型链原理如下

  1. 构造函数创建
    • 当你创建一个构造函数时,例如 function Person(name) { this.name = name; },JavaScript 会自动为 Person 函数创建一个 prototype 属性,Person.prototype 指向一个对象。
    • 默认情况下,Person.prototype 对象只有一个 constructor 属性,指向 Person 函数本身。
  2. 设置原型属性/方法
    • 你可以向 Person.prototype 对象添加属性和方法,例如 Person.prototype.sayHello = function() { console.log('Hello, ' + this.name); }
    • 这些属性和方法会被所有通过 Person 构造函数创建的实例所继承。
  3. 创建实例
    • 当你使用 new Person('Alice') 创建一个实例时,会发生以下事情:
      • 创建一个新的空对象。
      • 将新对象的 __proto__ 属性指向 Person.prototype
      • 以新对象为上下文(this)调用 Person 构造函数,执行构造函数中的代码(例如 this.name = name)。
      • 如果构造函数没有显式返回一个对象,则返回新创建的对象。
  4. 属性查找
    • 当你访问 alice.sayHello() 时,JavaScript 引擎首先检查 alice 对象自身是否有 sayHello 属性。
    • 如果没有,引擎会查找 alice.__proto__,也就是 Person.prototype,看它是否有 sayHello 属性。
    • 如果找到了,就调用该方法。如果 Person.prototype 也没有 sayHello 属性,引擎会继续向上查找 Person.prototype.__proto__,直到找到该属性或者到达原型链的顶端(Object.prototype.__proto__null
  5. ES6 extens 说明 在 JavaScript 中,由于没有传统的类继承概念(ES6 引入的 class 实际上是语法糖,本质还是基于原型),因此原型链是实现继承的主要方式。

继承的语法

方法语法示例优势局限性
ES6 class 继承class Child extends Parent {}语法简洁,更符合面向对象编程的习惯,支持 super 调用父类方法本质上仍然是原型继承的语法糖,需注意浏览器兼容性
es5 继承Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child; Parent.call(this, name);结合了原型链继承和构造函数继承的优点,既能继承实例属性,又能继承原型属性父类构造函数会被调用两次,产生不必要的性能开销
提示

虽然 JS 可以用 extends class 模式实现继承,但这个机制的本质还是基于对象的原型链,通过运行时查找确认对象的属性和方法。传统 OOP 语言的继承,比如 C++、JAVA 的继承特性是编译时确定的而不是运行时,而 JS 的继承是运行时动态绑定的。所以你也可以只通过动态的设置原型链来实现继承。而不需要拘泥于 class 的继承模式。

示例代码

function Parent (name) {
this.name = name
}

// 父类的实例方法
Parent.prototype.greet = function () {
console.log('Hello from Parent, my name is ' + this.name)
}

// 父类的静态方法
Parent.staticMethod = function () {
console.log('This is a static method in Parent')
}

const Child = (function () {
Object.setPrototypeOf(Child, Parent) // 设置子类的原型链
function Child (name, age) {
// 调用父类的构造函数
Parent.call(this, name)
this.age = age
}
return Child
})()

// 子类继承父类的原型方法
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child

// 子类重写父类的实例方法
Child.prototype.greet = function () {
Parent.prototype.greet.call(this) // 调用父类的实例方法
console.log('Hello from Child, I am ' + this.age + ' years old')
}

// 子类重写父类的静态方法
Child.staticMethod = function () {
Parent.staticMethod.call(this) // 调用父类的静态方法
console.log('This is a static method in Child')
}

// 实例化子类
const child = new Child('Alice', 10)
child.greet()

// 调用子类重写的静态方法
Child.staticMethod()

// 演示子类继承父类的静态方法
console.log('\n从子类调用父类的静态方法(未重写的情况下):')
delete Child.staticMethod // 删除子类重写的静态方法
Child.staticMethod() // 这将调用父类的静态方法

什么是访问控制, JavaScript 中如何实现访问控制?

答案

访问控制是限制对对象内部状态和行为的访问权限,以保护数据完整性和实现信息隐藏。

ES6 之前,JavaScript 通过闭包和 Object.defineProperty,Object.defineProperties 实现访问控制。 ES6 引入了 class 语法

类型描述支持版本
public公共属性和方法,可以被实例化对象和子类访问。任何版本
private、#私有属性和方法,只能在类内部访问,不能被实例化对象或子类访问。ES2022+
protected受保护属性和方法,只能在类内部和子类中访问。Typescript

示例代码

class Person {
// ===== ES2015 (ES6) =====
static species = 'Human' // Static properties

constructor (name) {
this.name = name
Person.#count++
}

greet () { // Public methods
console.log(`Hello, my name is ${this.name}`)
}

get profile () { // Getter
return `${this.name}, age: ${this.age}`
}

set profile (value) { // Setter
[this.name, this.age] = value.split(',')
}

static getCount () { // Static methods
return this.#count
}

// ===== ES2019 =====
#privateField = 'private value' // Private fields
static #count = 0 // Static private fields

// ===== ES2020 =====
#privateMethod () { // Private methods
return `${this.name}'s private data: ${this.#privateField}`
}

// ===== ES2022 =====
name // Public class fields
age = 0

// Static initialization block
static {
console.log('Class initialization')
}
}
const person = new Person('Alice')
console.log(person.name) // Alice

什么是多态?JS 实现多态有哪些方式?

答案

多态是指不同类的对象可以通过同一接口进行交互,具体的实现方式取决于对象的实际类型。多态使得代码更具灵活性和可扩展性。多态的核心思想就是一个接口,多种实现

实现方式示例说明
方法重写(原型/类)子类覆盖父类同名方法
Duck Typing对象只要有对应方法即可使用

示例

// 父类
class Animal {
speak () {
return 'Some sound'
}
}

// 子类重写父类方法
class Dog extends Animal {
speak () {
return 'Woof!'
}
}

class Cat extends Animal {
speak () {
return 'Meow!'
}
}

// 多态行为
const animals = [new Animal(), new Dog(), new Cat()]
animals.forEach(animal => {
console.log(animal.speak())
})

什么是组合?JS 实现组合有哪些方式?

答案

组合是一种面向对象编程的设计模式,它通过将不同的对象实例组合在一起,形成更复杂的对象,从而实现功能的复用和扩展。组合强调的是对象之间的“有一个 (has-a)”关系,而不是继承的“是一个 (is-a)”关系。组合有如下优点:

  • 高灵活性: 可以通过组合不同的对象来动态地改变一个对象的行为,更容易适应需求变化。
  • 低耦合: 组合的对象之间相互独立,修改一个对象通常不会影响到其他对象,降低了系统的耦合度。
  • 高复用性: 可以将小的、功能单一的对象在不同的上下文中进行组合,提高代码的复用率。
  • 避免继承的局限性: 避免了继承可能带来的“脆弱的基类问题”和“多重继承的复杂性”。
  • 更清晰的对象关系: “有一个”的关系比“是一个”的关系在某些场景下更符合现实世界的模型,使得对象之间的关系更加清晰。
方法优势局限性
引用/委托实现了真正的“有一个”关系,对象之间耦合度最低,职责分离清晰,易于测试和维护。被引用的对象可以独立演化。可能需要编写更多的委托代码。
工厂/组合函数灵活地创建具有特定行为的对象,行为以函数形式组织,易于复用和组合。可以更好地管理状态和行为之间的关系。创建的对象不是特定类的实例,可能在类型检查和 instanceof 操作上有所不同。如果组合的行为依赖于共享的内部状态,需要小心处理。
类混入 (原型)可以将行为添加到现有类的所有实例中,实现代码复用。对于希望在类层面共享行为的场景比较适用。可能导致命名冲突,混入的行为可能与类自身的方法产生意外的相互影响。如果混入过多,可能会使原型链变得复杂,降低可读性。
对象混入 (直接扩展)简单直接,易于理解和使用,适用于快速组合现有对象。仅为浅拷贝,如果源对象包含引用类型的值,修改后会互相影响。如果混入的属性名冲突,后面的会覆盖前面的。对于需要创建多个具有相同组合行为的对象,代码复用性不高。

示例

// 定义一个负责日志记录的类
class Logger {
log (message) {
console.log(`[LOG]: ${message}`)
}
}

// 定义一个用户服务类,它依赖于 Logger 类来记录日志
class UserService {
constructor () {
this.logger = new Logger() // UserService "有一个" Logger
}

createUser (username) {
console.log(`Creating user: ${username}`)
this.logger.log(`User "${username}" created successfully.`) // 将日志记录的职责委托给 Logger 实例
return { id: Math.random(), username }
}
}

const userService = new UserService()
const newUser = userService.createUser('Bob')
console.log(newUser)
提示

在有些面向对象的语言中还支持类似 Trait 的概念,Trait 是一种轻量级的类,可以被多个类共享。Trait 允许你将行为分离到独立的单元中,从而实现更好的代码复用和组合。示例如下

trait Walkable {
fn walk(&self);
}

trait Swimmable {
fn swim(&self);
}

struct Dog {
name: String,
}

impl Walkable for Dog {
fn walk(&self) {
println!("{} is walking.", self.name);
}
}

impl Swimmable for Dog {
fn swim(&self) {
println!("{} is swimming.", self.name);
}
}

fn main() {
let dog = Dog { name: String::from("Buddy") };
dog.walk(); // 输出: Buddy is walking.
dog.swim(); // 输出: Buddy is swimming.
}

什么是 SOLID 原则?

答案

SOLID 是面向对象设计的五项基本原则,它们分别为:

原则描述实际应用
S (Single Responsibility Principle)每个模块/类只应负责一种功能。使得每个组件或服务职责单一,便于维护和扩展。
O (Open/Closed Principle)模块对扩展开放,对修改封闭。通过继承、接口或组合来扩展功能,而不修改已有代码。
L (Liskov Substitution Principle)子类对象应能够替换父类对象而不破坏系统行为。在组件库中确保继承关系合理,保证组件可替换性。
I (Interface Segregation Principle)不应强迫客户端依赖它们不使用的方法。在前端开发中设计 API 或服务接口时,尽量精简,避免臃肿。
D (Dependency Inversion Principle)高层模块不应依赖低层模块,两者都应该依赖于抽象。通过依赖注入、接口设计实现模块独立性,提升测试与复用能力。
// 单一职责原则要求每个类或模块只负责一种功能。
// 拆分用户认证与日志记录逻辑
class AuthService {
login (username, password) {
// 验证用户信息,返回认证结果
return username === 'admin' && password === '1234'
}
}

class Logger {
log (message) {
console.log('LOG:', message)
}
}

// 前端调用时分别使用不同的服务
const authService = new AuthService()
const logger = new Logger()

if (authService.login('admin', '1234')) {
logger.log('User logged in successfully.')
}

什么是依赖注入?它和依赖倒置原则原则区别是什么,在实际开发中如何使用?

答案
对比点依赖注入(DI)依赖倒置原则(DIP)
定义一种设计模式,外部注入依赖一种设计原则,高层依赖抽象,不依赖具体
是否抽象必须可以注入具体类或接口强调依赖抽象(接口)
目的降低耦合,提升可测试性构建稳定系统架构,使高层和低层依赖同一接口
举例constructor(logger) 外部注入constructor(ILogger) 只依赖抽象
关系DI 是实现 DIP 的一种手段DIP 是一种架构设计思维指导原则

前端典型使用场景为

场景 | 实现说明 Angular | 内建依赖注入系统(如 @Injectable()),控制反转容器自动注入服务 Vue 3 | provide/inject 实现组件树中的依赖注入 React | 使用 Context 实现依赖注入(如主题、store、i18n 等)

class Container {
constructor () {
this.services = new Map()
}

register (name, implementation) {
this.services.set(name, implementation)
}

resolve (name) {
const Service = this.services.get(name)
return new Service()
}
}

// 使用
class Logger {
log (msg) {
console.log(msg)
}
}
class UserService {
constructor (logger) {
this.logger = logger
}

static inject = ['logger']
createUser (name) {
this.logger.log(`创建用户:${name}`)
}
}

// 模拟注入
const container = new Container()
container.register('logger', Logger)

// 自动注入依赖
function inject (cls) {
const deps = cls.inject.map(dep => container.resolve(dep))
return new cls(...deps)
}

const userService = inject(UserService)
userService.createUser('Alice')

什么是设计模式?为什么在开发中使用设计模式?

答案

设计模式(Design Pattern) 是一套经过总结、可复用、经过验证的编程经验,它描述了在特定情境下,如何优雅地解决某类常见软件设计问题的通用方案。 设计模式不是代码模板,而是一种设计思想和结构方案,用于提升系统的可维护性、可扩展性和灵活性。

设计模式在开发中的作用主要包括:

作用点说明
提高复用性提供通用的解决方案,避免重复造轮子
增强可读性使用大家熟悉的模式能让代码更容易被团队理解
提升可维护性模块职责清晰,结构可控,便于维护和扩展
应对变化更好地应对需求变更,实现“对扩展开放,对修改封闭”
降低耦合通过抽象和封装减少模块之间的依赖,提高系统稳定性

根据设计模式:可复用面向对象软件的基础分为三大类:创建型(Creational)结构型(Structural)行为型(Behavioral)。 每类下可再按对象模式类模式划分:

类型模式名类/对象前端解释前端典型场景
创建型单例模式(Singleton)对象全局唯一实例Vuex、Redux Store
工厂方法(Factory Method)统一创建对象的接口动态组件生成
抽象工厂(Abstract Factory)对象创建一组相关对象UI 组件库中统一主题生成
建造者模式(Builder)对象分步骤构建复杂对象表单构造器
原型模式(Prototype)对象克隆已有对象DOM 节点或状态深拷贝
结构型适配器模式(Adapter)对象接口转换兼容旧系统axios 请求结果适配旧结构
装饰器模式(Decorator)对象动态扩展对象功能React HOC、高阶函数增强
外观模式(Facade)对象提供统一接口屏蔽复杂实现API 接口统一封装
组合模式(Composite)对象树形结构统一处理Vue、React 组件树
代理模式(Proxy)对象控制访问权限或增强行为虚拟滚动、懒加载、权限组件
桥接模式(Bridge)对象分离抽象和实现主题系统与业务逻辑解耦
享元模式(Flyweight)对象共享对象减少内存虚拟列表复用 item DOM
行为型观察者模式(Observer)对象订阅-发布模型Vue 响应式系统、事件监听
策略模式(Strategy)对象替代 if-else 逻辑表单验证策略切换
状态模式(State)对象状态切换影响行为登录状态管理、流程控制
命令模式(Command)对象封装请求为对象撤销、重做功能
责任链模式(Chain of Responsibility)对象多个处理器链式处理请求中间件体系,如 Redux middleware
模板方法(Template Method)定义算法骨架,子类实现UI 组件渲染流程扩展
中介者模式(Mediator)对象控制组件间通信前端事件总线、组件解耦
迭代器模式(Iterator)对象顺序访问集合元素for-of、生成器遍历数据源
解释器模式(Interpreter)解释某种语言表达式模板引擎、表达式计算器
访问者模式(Visitor)对象对结构中元素进行操作AST 转换器,如 Babel 插件
备忘录模式(Memento)对象保存对象状态以便恢复表单撤销、可视化编辑器历史记录
提示

设计模式的本质其实是在 OOP 编程范式下,提供了一系列的可复用的设计元语。但是模式本身并不拘泥于 OOP 也不仅局限于上面的分类。 重点是在日常开发中能够识别这些典型的模式,同时基于上面的思想沉淀其他的模式来实现复用。

解释以下创建型模式单例模式、工厂模式、建造者模式、抽象工厂模式的区别,说明实际开发中如何使用?

答案

模式之间的区别

模式名称定义使用场景与其他模式的区别
单例模式保证一个类只有一个实例,并提供全局访问点全局状态管理、缓存、唯一服务实例(如 EventBus)强调**“唯一性”全局访问**,不关注对象的创建过程复杂度
工厂方法模式定义一个用于创建对象的接口,由子类决定要实例化的类创建过程相对简单,但需要根据类型动态创建不同对象,如图标组件、表单字段等强调延迟到子类决定创建哪种对象,相比抽象工厂只创建一个对象
建造者模式将一个复杂对象的构建与表示分离,同样构建过程可以创建不同表示多步骤构建复杂对象(如 UI 表单、复杂配置),构建流程固定但参数可定制适用于构建过程复杂的对象,强调按步骤组装;而工厂注重类型选择
抽象工厂模式提供一个接口用于创建一系列相关或相互依赖的对象,无需指定具体类UI 主题风格切换(按钮、输入框风格统一)、跨平台组件创建等场景能创建一组相关对象,比工厂方法更宏观、更全面(如创建整个 UI 套件,而不是一个组件)
/**
* 典型场景:Vuex / Redux Store、全局 EventBus、Modal 管理器**
*
*/
// EventBus 单例
class EventBus {
constructor () {
if (!EventBus.instance) {
this.listeners = {}
EventBus.instance = this
}
return EventBus.instance
}

on (event, cb) {
(this.listeners[event] ||= []).push(cb)
}

emit (event, data) {
(this.listeners[event] || []).forEach(cb => cb(data))
}
}

// 使用
const bus = new EventBus()
bus.on('login', data => console.log('User logged in', data))
bus.emit('login', { user: 'Alice' })

解释以下结构型模式适配器模式、装饰器模式、外观模式,说明实际开发中如何使用?

答案
模式名称定义使用场景与其他模式的区别
适配器模式将一个接口转换成客户端期望的另一个接口,解决接口不兼容问题第三方库适配、老代码兼容新接口、统一数据格式(如后端返回数据与组件 props 不一致)关注接口转换,原有结构不变,强调“兼容”
装饰器模式动态地给对象添加额外的功能,而不影响原对象结构高阶组件(HOC)、Vue/React 自定义指令、增强函数功能强调在不修改原对象的前提下增强功能,适用于可选性增强逻辑
外观模式提供一个统一接口,隐藏系统内部复杂性,简化调用对外暴露简化 API(如封装复杂图表、动画库等),页面初始化流程封装关注简化复杂系统的使用接口,提供一站式访问门面
/**
* 典型场景:将后端数据结构适配成前端组件所需格式
* 统一后端返回值,适配不同组件要求,避免直接修改第三方接口或组件源代码。
*/
// 原始数据(来自后端)
const backendUser = {
uid: 1001,
username: 'Alice',
gender: 1
}

// 前端组件需要的数据结构
// { id: number, name: string, genderText: string }

function userAdapter (rawUser) {
return {
id: rawUser.uid,
name: rawUser.username,
genderText: rawUser.gender === 1 ? '女' : '男'
}
}

// 使用
const adaptedUser = userAdapter(backendUser)
console.log(adaptedUser) // { id: 1001, name: 'Alice', genderText: '女' }

解释以下行为型模式观察者模式、策略模式、命令模式、状态模式,说明实际开发中如何使用?

模式名称定义使用场景区别
观察者模式对象间的一对多依赖关系,当一个对象状态变化时,所有依赖它的对象都会收到通知并自动更新。事件处理系统、发布订阅系统、全局状态管理(如 Redux、Vuex)。关注对象状态变化时的通知机制,强调数据与视图的同步。
策略模式定义一系列算法,将它们封装起来,并使它们可以互相替换。多算法切换(如排序策略)、多支付方式处理、表单验证策略。关注算法的动态替换,客户端决定使用哪种策略,与具体实现解耦。
命令模式将请求封装成对象,允许参数化客户端、请求排队或记录日志,支持撤销操作。菜单交互、事务处理、宏命令、撤销/重做功能(如编辑器)。关注请求的封装与扩展,将调用者与接收者解耦,支持历史操作管理。
状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。状态机管理(如游戏角色状态)、UI 组件状态驱动(如按钮禁用/加载中/成功)。关注对象内部状态的行为变化,状态转换逻辑集中在状态类中。
答案
/**
* 观察者模式
*/
class EventEmitter {
constructor () {
this.events = {}
}

on (event, listener) {
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push(listener)
}

off (event, listener) {
if (!this.events[event]) return
this.events[event] = this.events[event].filter(l => l !== listener)
}

once (event, listener) {
const onceWrapper = (...args) => {
listener(...args)
this.off(event, onceWrapper)
}
this.on(event, onceWrapper)
}

emit (event, ...args) {
if (this.events[event]) {
this.events[event].forEach(listener => listener(...args))
}
}
}

const eventEmitter = new EventEmitter()
eventEmitter.on('event1', (data) => {
console.log(`Event 1 triggered with data: ${data}`)
})
eventEmitter.once('event2', (data) => {
console.log(`Event 2 triggered with data: ${data}`)
})
eventEmitter.emit('event1', 'Hello World!')
eventEmitter.emit('event1', 'Hello World!')
eventEmitter.emit('event2', 'Hello World!')
eventEmitter.emit('event2', 'Hello Again!') // Won't trigger

解释职责链模式,说明实际开发中如何使用?

职责链模式是一种行为型设计模式,允许将请求沿着处理链传递,直到有对象能够处理它为止。通过解耦请求的发送者和接收者,使多个对象都有机会处理请求,但具体由哪个对象处理在运行时动态决定。

典型使用场景如下

答案
/**
* 前端表单验证
*/
// 抽象处理者
class Validator {
constructor () {
this.nextValidator = null
}

setNext (validator) {
this.nextValidator = validator
return validator // 返回下一个验证器,便于链式调用
}

validate (input) {
if (this.nextValidator) {
return this.nextValidator.validate(input)
}
return true // 如果没有下一个验证器,则验证通过
}
}

// 具体处理者:必填字段验证器
class RequiredValidator extends Validator {
validate (input) {
if (!input.value) {
return { field: input.name, error: '此字段为必填项' }
}
return super.validate(input)
}
}

// 具体处理者:邮箱验证器
class EmailValidator extends Validator {
validate (input) {
if (input.type === 'email' && input.value) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(input.value)) {
return { field: input.name, error: '请输入有效的电子邮件地址' }
}
}
return super.validate(input)
}
}

// 具体处理者:密码长度验证器
class PasswordLengthValidator extends Validator {
validate (input) {
if (input.type === 'password' && input.value && input.value.length < 8) {
return { field: input.name, error: '密码长度必须至少为8个字符' }
}
return super.validate(input)
}
}

// 客户端代码
function validateForm (formData) {
// 创建验证链
const requiredValidator = new RequiredValidator()
const emailValidator = new EmailValidator()
const passwordValidator = new PasswordLengthValidator()

requiredValidator
.setNext(emailValidator)
.setNext(passwordValidator)

const errors = []

// 对每一个表单字段进行验证
for (const field of formData) {
const result = requiredValidator.validate(field)
if (result !== true) {
errors.push(result)
}
}

return errors
}

// 使用示例
const formData = [
{ name: 'username', type: 'text', value: '' },
{ name: 'email', type: 'email', value: 'invalid-email' },
{ name: 'password', type: 'password', value: 'pass' }
]

const validationErrors = validateForm(formData)
console.log(validationErrors)
// 输出:
// [
// { field: 'username', error: '此字段为必填项' },
// { field: 'email', error: '请输入有效的电子邮件地址' },
// { field: 'password', error: '密码长度必须至少为8个字符' }
// ]
22%