面试常问的常用设计模式的原理和实践

前言

以下内容主要是笔者的笔记内容,有些设计模式在前端里特别常见的就省略了。

JavaScript 设计模式核心原理与应用实践

  • 创建型
    • 构造器模式
    • 工厂模式
    • 抽象工厂模式
    • 原型模式
  • 结构型
    • 装饰器模式
    • 适配器模式
    • 代理模式
  • 行为型
    • 观察者模式
    • 策略模式
    • 状态模式
    • 迭代器模式
  • 主要用到的设计模式基本都围绕单一功能开放封闭这两个原则来展开
  • 将变与不变分离,确保变化的部分灵活、不变的部分稳定,这样的代码,就是我们所谓的健壮的代码,它可以经得起变化的考验

工厂模式

简单工厂模式

先介绍一下构造器模式:

js 复制代码
function User(name , age, career) {
    this.name = name
    this.age = age
    this.career = career 
}

楼上个这 User,就是一个构造器

简单的调用:

javascript 复制代码
const user = new User(name, age, career)

从此再也不用手写字面量。

工厂模式 其实就是将创建对象的过程单独封装

这个承载了共性的 User 类和个性化的逻辑判断写入同一个函数:

javascript 复制代码
function User(name , age, career, work) {
    this.name = name
    this.age = age
    this.career = career 
    this.work = work
}

function Factory(name, age, career) {
    let work
    switch(career) {
        case 'coder':
            work =  ['写代码','写系分', '修Bug'] 
            break
        case 'product manager':
            work = ['订会议室', '写PRD', '催更']
            break
        case 'boss':
            work = ['喝茶', '看报', '见客户']
        case 'xxx':
            // 其它工种的职责分配
            ...
            
    return new User(name, age, career, work)
}

这样一来,我们要做事情是不是简单太多了?不用自己时刻想着我拿到的这组数据是什么工种、我应该怎么给它分配构造函数,更不用手写无数个构造函数------Factory已经帮我们做完了一切,而我们只需要像以前一样无脑传参就可以了!

抽象工厂模式

抽象类:

js 复制代码
class MobilePhoneFactory {
    // 提供操作系统的接口
    createOS(){
        throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!");
    }
    // 提供硬件的接口
    createHardWare(){
        throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!");
    }
}

具体工厂:

js 复制代码
// 具体工厂继承自抽象工厂
class FakeStarFactory extends MobilePhoneFactory {
    createOS() {
        // 提供安卓系统实例
        return new AndroidOS()
    }
    createHardWare() {
        // 提供高通硬件实例
        return new QualcommHardWare()
    }
}

抽象操作系统类:

js 复制代码
// 定义操作系统这类产品的抽象产品类
class OS {
    controlHardWare() {
        throw new Error('抽象产品方法不允许直接调用,你需要将我重写!');
    }
}

// 定义具体操作系统的具体产品类
class AndroidOS extends OS {
    controlHardWare() {
        console.log('我会用安卓的方式去操作硬件')
    }
}

class AppleOS extends OS {
    controlHardWare() {
        console.log('我会用🍎的方式去操作硬件')
    }
}
...

硬件类产品同理:

javascript 复制代码
// 定义手机硬件这类产品的抽象产品类
class HardWare {
    // 手机硬件的共性方法,这里提取了"根据命令运转"这个共性
    operateByOrder() {
        throw new Error('抽象产品方法不允许直接调用,你需要将我重写!');
    }
}

// 定义具体硬件的具体产品类
class QualcommHardWare extends HardWare {
    operateByOrder() {
        console.log('我会用高通的方式去运转')
    }
}

class MiWare extends HardWare {
    operateByOrder() {
        console.log('我会用小米的方式去运转')
    }
}
...

好了,如此一来,当我们需要生产一台FakeStar手机时,我们只需要这样做:

javascript 复制代码
// 这是我的手机
const myPhone = new FakeStarFactory()
// 让它拥有操作系统
const myOS = myPhone.createOS()
// 让它拥有硬件
const myHardWare = myPhone.createHardWare()
// 启动操作系统(输出'我会用安卓的方式去操作硬件')
myOS.controlHardWare()
// 唤醒硬件(输出'我会用高通的方式去运转')
myHardWare.operateByOrder()

关键的时刻来了------假如有一天,FakeStar过气了,我们需要产出一款新机投入市场,这时候怎么办?我们是不是不需要对抽象工厂MobilePhoneFactory做任何修改,只需要拓展它的种类:

javascript 复制代码
class newStarFactory extends MobilePhoneFactory {
    createOS() {
        // 操作系统实现代码
    }
    createHardWare() {
        // 硬件实现代码
    }
}

这么个操作,对原有的系统不会造成任何潜在影响 所谓的对拓展开放,对修改封闭就这么圆满实现了。前面我们之所以要实现抽象产品类,也是同样的道理。

单例模式

闭包实现:

js 复制代码
SingleDog.getInstance = () => {
    let instance = null;
    return function() {
        if(!instance){
            instance = new SingleDog();
        }
        return instance;
    }
}

一个 Vue 应用只能对应一个 Store,在Vuex中的使用:

  • 保证在同一个Vue应用里只能有一个Vuex实例
js 复制代码
let Vue // 这个Vue的作用和楼上的instance作用一样

export function install (_Vue) {
  // 判断传入的Vue实例对象是否已经被install过Vuex插件(是否有了唯一的 store)
  if (Vue && _Vue === Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  // 若没有,则为这个Vue实例对象install一个唯一的Vuex
  Vue = _Vue
  // 将Vuex的初始化逻辑写进Vue的钩子函数里
  applyMixin(Vue)
}
  • 保证了在同一个 Vue 应用中只存在一个 Vuex 实例。
js 复制代码
function vuexInit () {
  const options = this.$options
  // 将 store 实例挂载到 Vue 实例上
  if (options.store) {
    this.$store = typeof options.store === 'function'
      ? options.store()
      : options.store
  } else if (options.parent && options.parent.$store) {
    this.$store = options.parent.$store
  }
}

这段逻辑意味着,$store实例在 Vue 组件树中是被层层继承 下来的------当子组件自身不具备 $store 时,会查找父组件的 $store 并继承。这样,整个 Vue 组件树中的所有组件都会访问到同一个 Store 实例------那就是根组件的Store实例。

尽管 Vuex 并不是严格意义上的单例模式,但它却很大程度上从单例模式的思想中受益,也为我们在实践中应用单例模式提供了全新的思路。

实现一个 Storage

js 复制代码
// 先实现一个基础的StorageBase类,把getItem和setItem方法放在它的原型链上
function StorageBase () {}
StorageBase.prototype.getItem = function (key){
    return localStorage.getItem(key)
}
StorageBase.prototype.setItem = function (key, value) {
    return localStorage.setItem(key, value)
}

// 以闭包的形式创建一个引用自由变量的构造函数
const Storage = (function(){
    let instance = null
    return function(){
        // 判断自由变量是否为null
        if(!instance) {
            // 如果为null则new出唯一实例
            instance = new StorageBase()
        }
        return instance
    }
})()

// 这里其实不用 new Storage 的形式调用,直接 Storage() 也会有一样的效果 
const storage1 = new Storage()
const storage2 = new Storage()

storage1.setItem('name', '李雷')
// 李雷
storage1.getItem('name')
// 也是李雷
storage2.getItem('name')

// 返回true
storage1 === storage2

实现一个全局的模态框

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>单例模式弹框</title>
</head>
<style>
    #modal {
        height: 200px;
        width: 200px;
        line-height: 200px;
        position: fixed;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
        border: 1px solid black;
        text-align: center;
    }
</style>
<body>
	<button id='open'>打开弹框</button>
	<button id='close'>关闭弹框</button>
</body>
<script>
    // 核心逻辑,这里采用了闭包思路来实现单例模式
    const Modal = (function() {
    	let modal = null
    	return function() {
            if(!modal) {
            	modal = document.createElement('div')
            	modal.innerHTML = '我是一个全局唯一的Modal'
            	modal.id = 'modal'
            	modal.style.display = 'none'
            	document.body.appendChild(modal)
            }
            return modal
    	}
    })()
    
    // 点击打开按钮展示模态框
    document.getElementById('open').addEventListener('click', function() {
        // 未点击则不创建modal实例,避免不必要的内存占用;此处不用 new Modal 的形式调用也可以,和 Storage 同理
    	const modal = new Modal()
    	modal.style.display = 'block'
    })
    
    // 点击关闭按钮隐藏模态框
    document.getElementById('close').addEventListener('click', function() {
    	const modal = new Modal()
    	if(modal) {
    	    modal.style.display = 'none'
    	}
    })
</script>
</html>

原型模式

  • 谈原型模式,其实是谈原型范式,原型编程范式的体现就是基于原型链的继承

装饰器模式

在 ES7 中,我们可以像写 python 一样通过一个@语法糖轻松地给一个类装上装饰器:

javascript 复制代码
// 装饰器函数,它的第一个参数是目标类
function classDecorator(target) {
    target.hasDecorator = true
  	return target
}

// 将装饰器"安装"到Button类上
@classDecorator
class Button {
    // Button类的相关逻辑
}

// 验证装饰器是否生效
console.log('Button 是否被装饰了:', Button.hasDecorator)

我们可以联想记忆函数的作用,就能理解装饰器了:

js 复制代码
// 创建一个函数 memoize,接受一个函数作为参数
function memoize(func) {
  // 创建一个空对象 cache,用于存储计算结果
  const cache = {};
  // 返回一个新的函数,此处使用了 rest parameter 操作符 ...args,
  // 它可以让我们将传入的参数转换成一个数组
  return function(...args) {
    // 使用 JSON.stringify 将参数列表 args 转换成字符串,作为缓存对象 cache 的 key 值
    const key = JSON.stringify(args);
    // 如果缓存对象中存在该 key 值,则直接返回缓存值
    if (cache[key]) {
      console.log('从缓存中获取结果');
      return cache[key];
    } else {
      // 否则,执行原始函数,将结果存入缓存对象中,并返回结果
      console.log('进行计算');
      const result = func.apply(this, args);
      cache[key] = result;
      return result;
    }
  }
}

function fib(n) {
  if (n < 2) return n;
  return fib(n - 1) + fib(n - 2);
}

console.time('fib(40)');
console.log(fib(40));
console.timeEnd('fib(40)');

const memoizedFib = memoize(fib);// 高阶函数

console.time('memoizedFib(40)');
console.log(memoizedFib(40));
console.timeEnd('memoizedFib(40)');

适配器模式

  • 兼容代码就是一把梭
  • 生产实践中:axios 中的适配器
js 复制代码
function getDefaultAdapter() {
  var adapter;
  // 判断当前是否是node环境
  if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // 如果是node环境,调用node专属的http适配器
    adapter = require('./adapters/http');
  } else if (typeof XMLHttpRequest !== 'undefined') {
    // 如果是浏览器环境,调用基于xhr的适配器
    adapter = require('./adapters/xhr');
  }
  return adapter;
}

代理模式

  • 事件代理

用代理模式实现多个子元素的事件监听,代码会简单很多:

javascript 复制代码
// 获取父元素
const father = document.getElementById('father')

// 给父元素安装一次监听函数
father.addEventListener('click', function(e) {
    // 识别是否是目标子元素
    if(e.target.tagName === 'A') {
        // 以下是监听函数的函数体
        e.preventDefault()
        alert(`我是${e.target.innerText}`)
    }
} )

在这种做法下,我们的点击操作并不会直接触及目标子元素,而是由父元素对事件进行处理和分发、间接地将其作用于子元素,因此这种操作从模式上划分属于代理模式。

  • 保护代理
    • proxy 代理模式

策略模式

实际上常见在弹窗组件里,策略模式适用于需要根据不同情况采取不同行为的场景,它能够减少大量的条件语句,并提高代码的可读性和可维护性。同时,通过将算法封装成独立的策略函数,可以方便地扩展和添加新的算法。

js 复制代码
// 策略对象
const strategies = {
  add: function(a, b) {
    return a + b;
  },
  subtract: function(a, b) {
    return a - b;
  },
  multiply: function(a, b) {
    return a * b;
  },
  divide: function(a, b) {
    return a / b;
  }
};

// 上下文对象
function calculate(strategy, a, b) {
  if (typeof strategies[strategy] === 'function') {
    return strategies[strategy](a, b);
  } else {
    throw new Error('Invalid strategy');
  }
}

// 使用示例
console.log(calculate('add', 5, 2));       // 输出: 7
console.log(calculate('subtract', 5, 2));  // 输出: 3
console.log(calculate('multiply', 5, 2));  // 输出: 10
console.log(calculate('divide', 5, 2));    // 输出: 2.5

状态模式

状态模式是一种行为设计模式,它允许对象在内部状态发生变化时改变其行为。

js 复制代码
// 定义不同的状态对象
const states = {
  idle: {
    handleInput: function() {
      console.log('当前状态为:空闲');
      console.log('处理输入事件');
      // 处理空闲状态的逻辑
    },
    update: function() {
      console.log('当前状态为:空闲');
      console.log('更新状态');
      // 更新空闲状态的逻辑
    }
  },
  running: {
    handleInput: function() {
      console.log('当前状态为:运行');
      console.log('处理输入事件');
      // 处理运行状态的逻辑
    },
    update: function() {
      console.log('当前状态为:运行');
      console.log('更新状态');
      // 更新运行状态的逻辑
    }
  }
};

// 上下文对象
function Game() {
  let currentState = states.idle;

  this.changeState = function(state) {
    currentState = state;
  };

  this.handleInput = function() {
    currentState.handleInput();
  };

  this.update = function() {
    currentState.update();
  };
}

// 使用示例
const game = new Game();

game.handleInput();  // 输出: 当前状态为:空闲 / 处理输入事件
game.update();       // 输出: 当前状态为:空闲 / 更新状态

game.changeState(states.running);

game.handleInput();  // 输出: 当前状态为:运行 / 处理输入事件
game.update();       // 输出: 当前状态为:运行 / 更新状态

状态模式可以很好地将对象的行为和内部状态解耦,使得对象能够根据内部状态的变化自动改变行为。这样可以避免大量的条件语句和分支判断,提高代码的可读性和可维护性。同时,通过将状态封装成独立的对象,可以方便地扩展和添加新的状态。

两者区别和联系:

  • 状态模式和策略模式都是行为设计模式,它们允许对象根据不同情况采取不同的行为。

  • 状态模式通过改变对象的内部状态来改变行为,每个状态对应一个对象。对象的行为取决于当前状态,适用于需要根据不同状态采取不同行为的场景。

  • 策略模式将算法或行为封装成独立的对象,对象的行为取决于传入的策略对象。适用于需要在运行时根据不同条件选择不同算法的场景

  • 状态模式的上下文对象维护状态变量,根据变量调用对应状态对象的方法。策略模式的上下文对象接受策略对象作为参数,调用对应算法或行为函数。

  • 总的来说,状态模式和策略模式在目的、实现方式和上下文对象角色上有所不同,根据具体需求选择适合的模式。

订阅发布者模式与观察者模式

迭代模式

相关推荐
腾讯TNTWeb前端团队5 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
uhakadotcom8 小时前
视频直播与视频点播:基础知识与应用场景
后端·面试·架构
范文杰8 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪9 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪9 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy9 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom10 小时前
快速开始使用 n8n
后端·面试·github
uhakadotcom10 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom10 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom10 小时前
React与Next.js:基础知识及应用场景
前端·面试·github