javascript设计模式,想学吗?

54 阅读25分钟

设计模式原则

  • 单一职责原则
  • 开发封闭原则
  • 里式替换原则
  • 依赖倒转原则
  • 组合/聚合复用原则
  • 迪米特法则

创建型:工厂模式

专门负责对象的创建工作,封装对象,对外暴露接口

工厂模式在创建型模式中比较常用,它并不是一个独立的设计模式,而是三种功能接近的设计模式的统称。 这三种方法是 简单工厂模式、工厂方法模式、抽象工厂模式。

简单工厂模式

简单工厂模式有唯一的工厂类,工厂类的创建方法根据传入的参数做if-else条件判断,决定最终创建什么样式的产品对象 通过工厂类创建对象并且根据传入的参数决定具体子类对象


public interface IMask {
    void show();
}

public class HighEndMask implements IMask {
    @Override
    public void show() {
        System.out.println("我是高端口罩");
    }
}

public class LowEndMask implements IMask {
    @Override
    public void show(){
        System.out.println("我的低端口罩");
    }

}


public class MaskFactory{

    public IMask createMask(String type) {
        IMask mask = null;
        if("高端口罩".equals(type)){
            mask = new HighEndMask();
            // .....
            // HighEndMask的100行初始化代码
        }else if("低端口罩".equals(type)){
            mask =  new LowEndMask();
            // .....
            // LowEndMask的100行初始化代码
        }
        return mask;
    }
}

工厂方法模式

工厂方法模式由多个工厂类实现工厂接口,利用多态类创建不同的产品对象,从而避免了冗长的if-else条件判断

抽象工厂模式 (开放封闭原则)

抽象工厂模式把产品子类进行分组,同组中的不同产品由同一个工厂子类的不同方法负责创建,从而减少了工厂子类的数量

避免了工厂子类越来越多、系统越来越复杂的问题

示例代码

// 用户构造器
function User(name, age, career, work){
  this.name = name;
  this.age = age;
  this.career = career;
  this.work = work
}

const mapToArr = {
  coder: ['写代码', '画流程图', '修bug'],
  productManager: ['订会议室', '写PRD', '催更'],
  boss: ['喝茶','看报','见客户']
}
function factory({name, age, carerr}) {
  // 映射职责
  return new User(name, age, carerr, mapToArr[career]);
}

// 创建实例
const userInstance = factory({
  name: '张三',
  age: 18,
  career: 'coder'
})
console.log(userInstance)

创建型:单例模式

保证一个类仅有一个实例,并提供一个访问它的全局访问点

不管创建多少次,它都只会返回第一次所创建的那唯一的一个实例

示例代码

  • 原理实现
class SingleDog {
  show (){
    console.log('我是一个单例对象')
  }
  static getInstance(){
    // 判断是否已经new过1个实例
    if(!SingleDog.instance) {
      // 若这个唯一的实例不存在,那么创建它
      SingleDog.instance = new SingleDog()
    }
    // 如果这个唯一的实例已经存在,则直接返回
    return SingleDog.instance
  }
}

const s1 = SingleDog.getInstance()
const s2 = SingleDog.getInstance()

// true
s1 === s2
  • 用闭包实现
SingleDog.getInstance = (function() {
    // 定义自由变量instance,模拟私有变量
    let instance = null
    return function() {
        // 判断自由变量是否为null
        if(!instance) {
            // 如果为null则new出唯一实例
            instance = new SingleDog()
        }
        return instance
    }
})()

应用实践 Vuex-store

Vuex 插件是一个对象,它在内部实现了一个 install 方法,这个方法会在插件安装时被调用,从而把 Store 注入到Vue实例里去。也就是说每 install 一次,都会尝试给 Vue 实例注入一个 Store。

在install方法里,有一段逻辑和上面getInstance非常相似的逻辑:

let Vue // 这个Vue的作用和楼上的instance作用一样
...

export function install (_Vue) {
  // 判断传入的Vue实例对象是否已经被install过Vuex插件(是否有了唯一的state)
  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 实例(即一个 Vue 应用)只会被 install 一次 Vuex 插件,所以每个 Vue 实例只会拥有一个全局的 Store。

面试题: 实现一个Storage

描述

实现Storage,使得该对象为单例,基于localStorage进行封装。实现方法setItem(key.value)和getItem(key)

代码实现

  • 原理实现
// 定义Storage
class Storage {
  static getInstance(){
    // 判断是否已经new过1个实例
    if(!Storage.instance) {
      // 若这个唯一的实例不存在,那么先创建它
      Storage.instance = new Storage()
    }
    // 如果这个实例已经存在,则直接返回
    return Storage.instance
  }
  getItem(key) {
    return localStorage.getItem(key)
  }
  setItem(key) {
    return localStorage.setItem(key, value)
  }
}

const storage1 = Storage.getIntance()
const storage2 = Storage.getIntance()

storage1 === storage2
  • 闭包实现
// 先实现一个基础的Storage基础类,吧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) {
      instance = new StorageBase()
    }
    return instance
  }
})()

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

// true
storage2 === storage1

实现一个全局的模拟框

描述

实现一个全局唯一Modal弹窗

实现代码

<!DOCTYPE html>
<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) // appendChild() 方法向节点添加最后一个子节点
      }
      return modal;
    }
  })()
  
  // 点击打开按钮提示模拟框
  document.getElementById('open').addEventListener('click', function() {
    // 未点击则不创建modal实例,避免不必要的内存占用,此处不用 new Modal的形式调用也可以
    const modal = new Modal()
    modal.style.display = 'block'
  })
  
  // 点击关闭按钮提示模拟框
  document.getElementById('close').addEventListener('click', function() {
    // 未点击则不创建modal实例,避免不必要的内存占用,此处不用 new Modal的形式调用也可以
    const modal = new Modal()
    if(modal)  {
      modal.style.display = 'none'
    }
  })
</script>
</html>

创建型:原型模式

概述

原型编程范式的核心思想就是利用实例来描述对象,用实例作为定义对象和继承的基础。原型编程范式的体现是基于原型链的继承

原型

在JavaScript中,每个构造函数都拥有一个prototype属性,它指向构造函数的原型对象; 每个实例都有一个__proto__属性,使用构造函数创建实例时,实例的__proto__属性就会指向构造函数的原型对象

原型链

试图访问一个JavaScript实例的属性/方法时,它首先搜索这个实例本身; 当发现实例没有定义对应的属性/方法时,它会去搜索实例的原型对象; 若搜不到,则再去搜索实例的原型对象的原型对象,这个搜索轨迹就叫原型链

深拷贝

深拷贝 有一种取巧的方式——JSON.Stringify() 但是这个方法有一些局限性,比如无法处理function、无法处理正则等等

  • 深拷贝边界递归代码
function deepClone(obj) {
  // 如果是 值类型 或 null,则直接return
  if(typeof obj !== 'object' || obj === null) {
    return obj
  }
  
  // 定义结果对象
  let copy = {}
  
  // 如果对象是数组,则定义结果为数组
  if(obj.constructor === Array) {
    copy = []
  }
  
  // 遍历对象
  for(let key in obj) {
    // 如果key是对象的自有属性
    if(obj.hasOwnProperty(key)) {
      // 递归调用深拷贝方法
      copy[key] = deepClone(obj[key])
    }
  }
  
  return copy
}

结构型:装饰器模式

在不影响原有功能的基础上,新增功能

应用场景

假设需求:每个业务中的按钮在点击后都弹出【您还未登录哦】的弹窗

  • 需求代码
    // 按钮文案修改逻辑
    function changeButtonText() {
        const btn = document.getElementById('open')
        btn.innerText = '快去登录'
    }
    
    // 按钮置灰逻辑
    function disableButton() {
        const btn =  document.getElementById('open')
        btn.setAttribute("disabled", true)
    }
    
    // 新版本功能逻辑整合
    function changeButtonStatus() {
        changeButtonText()
        disableButton()
    }
    
    // 把三个操作逐个添加open按钮的监听函数里
    document.getElementById('open').addEventListener('click', function() {
        openModal()
        changeButtonStatus()
    }
    
  • 面向对象化方式实现
// 定义打开按钮
class OpenButton {
  // 点击后展时弹窗
  onclick() {
    const modal = new Modal()
    modal.style.display = 'block'
  }
}

// 定义按钮对应的装饰器
class Decorator {
  // 将按钮实例传入
  constructor(open_button) {
    this.open_button = open_button
  }
  
  onclick() {
    this.open_button.onClick()
    // “包装”了新逻辑
    this.changeButtonStatus()
  }
  
  changeButtonStatus() {
    this.chagneButtonText();
    this.disableButton();
  }
  
  chagneButtonText(){
    const btn = document.getElementById('open')
    btn.innewText = '快去登录'
  }
  
  disableButton(){
    const btn = document.getElementById('open')
    btn.setAttribute('disabled',true)
  }
}

const openButton = new OpenButton()
const decorator = new Decorator(openButton)

document.getElementById('open').addEventListener('click', function() {
  // openButton.onClick()
  // 此处可以分别尝试两个onClick方法,验证装饰器是否生效
  decorator.onClick()
})

前置知识: ES7中的装饰器

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

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

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

也可以用同样的语法糖去装饰类里面的方法

function funcDecorator(target, name, descript) {
  let originalMethod = descriptor.value
  descriptor.value = function() {
    console.log('我是Func的装饰器逻辑')
    return originalMethod.apply(this, arguments)
  }
  return desciptor
}

class Button {
  @funcDecorator
  onClick() {
    console.log('我是Func原有的逻辑')
  }
}

// 验证装饰器是否生效
const button = new Button()
button.onClick();

生产实践

React中的装饰器:HOC

HOC (Higher Order Component) 即高阶组件。高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件。

示例代码

import React, { Component } from 'react'

// 作用是把传入的组件丢进一个有红色边框的容器里(拓展其样式)
const BorderHoc = WrappedComponent => class extends Component {
  render() {
    return <div style={{ border: 'solid 1px red' }}>
      <WrappedComponent />
    </div>
  }
}

export default borderHoc

装饰目标组件

import React, { Component } from 'react'
import BorderHoc from './BorderHoc'

// 用BorderHoc装饰目标组件
@BorderHoc 
class TargetComponent extends React.Component {
  render() {
    // 目标组件具体的业务逻辑
  }
}

// export出去的其实是一个被包裹后的组件
export default TargetComponent

使用装饰器改写 Redux connect

想要引入 Redux 时,通常需要调用 connect 方法来把状态和组件绑在一起。

import React, { Component } from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import action from './action.js'

class App extends Component {
  render() {
    // App的业务逻辑
  }
}

function mapStateToProps(state) {
  // 假设App的状态对应状态树上的app节点
  return state.app
}

function mapDispatchToProps(dispatch) {
  // 这段看不懂也没关系,下面会有解释。重点理解connect的调用即可
  return bindActionCreators(action, dispatch)
}

// 把App组件与Redux绑在一起
export default connect(mapStateToProps, mapDispatchToProps)(App)

mapStateToProps 是一个函数,它可以建立组件和状态之间的映射关系;mapDispatchToProps也是一个函数,它用于建立组件和store.dispatch的关系,使组件具备通过 dispatch 来派发状态的能力。

能力的拓展

能用装饰器来改写, 把 connect 抽出来

import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import action from './action.js'

function mapStateToProps(state) {
  return state.app
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators(action, dispatch)
}

// 将connect调用后的结果作为一个装饰器导出
export default connect(mapStateToProps, mapDispatchToProps)

在组件文件里引入connect:

import React, { Component } from 'react'
import connect from './connect.js'   

@connect
export default class App extends Component {
  render() {
    // App的业务逻辑
  }
}

装饰器模式的优势在于其极强的灵活性和可复用性

优质的源码阅读材料——装饰模式库 core-decorators

结构型:适配器模式

适配器模式通过把一个类的接口变换成客户端所期待的另外一种接口,可以解决不兼容问题

业务场景 - 兼容接口

  • 封装基于fetch的http方法库
export default class HttpUtils {
  // get方法
  static get(url) {
    return new Promise((resolve, reject) => {
      // 调用fetch
      fetch(url)
        .then(response => response.json())
        .then(result => {
          resolve(result)
        })
        .catch(error => {
          reject(error)
        })
    })
  }
  
  // post方法,data以object形式传入
  static post(url, data) {
    return new Promise((resolve, reject) => {
      // 调用fetch
      fetch(url, {
        method: 'POST',
        headers: {
          Accept: 'application/json',
          'Content-Type': 'application/x-www-form-urlencoded'
        },
        // 将object类型的数据格式化为合法的body参数
        body: this.changeData(data)
      })
        .then(response => response.json())
        .then(result => {
          resolve(result)
        })
        .catch(error => {
          reject(error)
        })
    })
  }
  
  // body请求体的格式化方法
  static changeData(obj) {
    var prop,
      str = ''
    var i = 0
    for (prop in obj) {
      if (!prop) {
        return
      }
      if (i == 0) {
        str += prop + '=' + obj[prop]
      } else {
        str += '&' + prop + '=' + obj[prop]
      }
      i++
    }
    return str
  }
}

发起请求

// 定义目标url地址
const URL = "xxxxx"
// 定义post入参
const params = {
    ...
}

// 发起post请求
 const postResponse = await HttpUtils.post(URL,params) || {}
 
 // 发起get请求
 const getResponse = await HttpUtils.get(URL) || {}

基于 XMLHttpRequest 网络请求, 调用方式:

// 发送get请求
Ajax('get', url地址, post入参, function(data){
    // 成功的回调逻辑
}, function(error){
    // 失败的回调逻辑
})

用适配器模式适配2个差异性网络请求封装方法

// Ajax适配器函数,入参与旧接口保持一致
async function AjaxAdapter(type, url, data, success, failed) {
    const type = type.toUpperCase()
    let result
    try {
         // 实际的请求全部由新接口发起
         if(type === 'GET') {
            result = await HttpUtils.get(url) || {}
        } else if(type === 'POST') {
            result = await HttpUtils.post(url, data) || {}
        }
        // 假设请求成功对应的状态码是1
        result.statusCode === 1 && success ? success(result) : failed(result.statusCode)
    } catch(error) {
        // 捕捉网络错误
        if(failed){
            failed(error.statusCode);
        }
    }
}

// 用适配器适配旧的Ajax方法
async function Ajax(type, url, data, success, failed) {
    await AjaxAdapter(type, url, data, success, failed)
}

生产实践:axios中的适配器

常用的接口api示例:

// Make a request for a user with a given ID
axios.get('/user?ID=12345')
  .then(function (response) {
    // handle success
    console.log(response);
  })
  .catch(function (error) {
    // handle error
    console.log(error);
  })
  .then(function () {
    // always executed
  })   
     
axios.post('/user', {
    firstName: 'Fred',
    lastName: 'Flintstone'
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });   

axios({
  method: 'post',
  url: '/user/12345',
  data: {
    firstName: 'Fred',
    lastName: 'Flintstone'
  }
})

在 axios 的核心逻辑中,实际上派发请求的是 dispatchRequest 方法。该方法内部其实主要做了两件事:

  • 数据转换,转换请求体/响应体,可以理解为数据层面的适配;
  • 调用适配器。

调用适配器的逻辑如下:

// 若用户未手动配置适配器,则使用默认的适配器
var adapter = config.adapter || defaults.adapter;
  
  // dispatchRequest方法的末尾调用的是适配器方法
  return adapter(config).then(function onAdapterResolution(response) {
    // 请求成功的回调
    throwIfCancellationRequested(config);

    // 转换响应体
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );

    return response;
  }, function onAdapterRejection(reason) {
    // 请求失败的回调
    if (!isCancel(reason)) {
      throwIfCancellationRequested(config);

      // 转换响应体
      if (reason && reason.response) {
        reason.response.data = transformData(
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }

    return Promise.reject(reason);
  });

实际开发中,我们使用默认适配器的频率更高。默认适配器在axios/lib/default.js里是通过getDefaultAdapter方法来获取的:

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;
}

http 适配器:

module.exports = function httpAdapter(config) {
  return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) {
    // 具体逻辑
  }
}

xhr 适配器:

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    // 具体逻辑
  }
}

结构型:代理模式

ES6中的proxy

“婚介所”的实现

// 普通私密信息
const baseInfo = ['age', 'career']
// 最私密信息
const privateInfo = ['avatar', 'phone']

// 用户(同事A)对象实例
const user = {
    ...(一些必要的个人信息)
    isValidated: true,
    isVIP: false,
}

// 掘金婚介所登场了
const JuejinLovers = new Proxy(girl, {
  get: function(girl, key) {
      if(baseInfo.indexOf(key)!==-1 && !user.isValidated) {
          alert('您还没有完成验证哦')
          return
      }
      
      //...(此处省略其它有的没的各种校验逻辑)
    
      // 此处我们认为只有验证过的用户才可以购买VIP
      if(user.isValidated && privateInfo.indexOf(key) && !user.isVIP) {
          alert('只有VIP才可以查看该信息哦')
          return
      }
  }
})




// 规定礼物的数据结构由type和value组成
const present = {
    type: '巧克力',
    value: 60,
}

// 为用户增开presents字段存储礼物
const girl = {
  // 姓名
  name: '小美',
  // 自我介绍
  aboutMe: '...'(大家自行脑补吧)
  // 年龄
  age: 24,
  // 职业
  career: 'teacher',
  // 假头像
  fakeAvatar: 'xxxx'(新垣结衣的图片地址)
  // 真实头像
  avatar: 'xxxx'(自己的照片地址),
  // 手机号
  phone: 123456,
  // 礼物数组
  presents: [],
  // 拒收50块以下的礼物
  bottomValue: 50,
  // 记录最近一次收到的礼物
  lastPresent: present,
}

// 掘金婚介所推出了小礼物功能
const JuejinLovers = new Proxy(girl, {
  get: function(girl, key) {
    if(baseInfo.indexOf(key)!==-1 && !user.isValidated) {
        alert('您还没有完成验证哦')
        return
    }
    
    //...(此处省略其它有的没的各种校验逻辑)
  
    // 此处我们认为只有验证过的用户才可以购买VIP
    if(user.isValidated && privateInfo.indexOf(key)!==-1 && !user.isVIP) {
        alert('只有VIP才可以查看该信息哦')
        return
    }
  }
  
  set: function(girl, key, val) {
 
    // 最近一次送来的礼物会尝试赋值给lastPresent字段
    if(key === 'lastPresent') {
      if(val.value < girl.bottomValue) {
          alert('sorry,您的礼物被拒收了')
          return
      }
    
      // 如果没有拒收,则赋值成功,同时并入presents数组
      girl.lastPresent = val
      girl.presents = [...girl.presents, val]
    }
  }
 
})

事件代理

  • 场景: 一个父元素下有多个子元素
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>事件代理</title>
</head>
<body>
  <div id="father">
    <a href="#">链接1号</a>
    <a href="#">链接2号</a>
    <a href="#">链接3号</a>
    <a href="#">链接4号</a>
    <a href="#">链接5号</a>
    <a href="#">链接6号</a>
  </div>
</body>
</html>
  • 需求:

希望鼠标点击每个 a 标签,都可以弹出“我是xxx”这样的提示

// 假如不用代理模式,我们将循环安装监听函数
const aNodes = document.getElementById('father').getElementsByTagName('a')
  
const aLength = aNodes.length

for(let i=0;i<aLength;i++) {
    aNodes[i].addEventListener('click', function(e) {
        e.preventDefault()
        alert(`我是${aNodes[i].innerText}`)                  
    })
}
  • 事件代理的实现
// 获取父元素
const father = document.getElementById('father')

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

图片懒加载

使用代理模式前

class PreLoadImage {
    // 占位图的url地址
    static LOADING_URL = 'xxxxxx'
    
    constructor(imgNode) {
        // 获取该实例对应的DOM节点
        this.imgNode = imgNode
    }
    
    // 该方法用于设置真实的图片地址
    setSrc(targetUrl) {
        // img节点初始化时展示的是一个占位图
        this.imgNode.src = PreLoadImage.LOADING_URL
        // 创建一个帮我们加载图片的Image实例
        const image = new Image()
        // 监听目标图片加载的情况,完成时再将DOM上的img节点的src属性设置为目标图片的url
        image.onload = () => {
            this.imgNode.src = targetUrl
        }
        // 设置src属性,Image实例开始加载图片
        image.src = targetUrl
    }
}

使用代理模式后

class PreLoadImage {
    constructor(imgNode) {
        // 获取真实的DOM节点
        this.imgNode = imgNode
    }
     
    // 操作img节点的src属性
    setSrc(imgUrl) {
        this.imgNode.src = imgUrl
    }
}

class ProxyImage {
    // 占位图的url地址
    static LOADING_URL = 'xxxxxx'

    constructor(targetImage) {
        // 目标Image,即PreLoadImage实例
        this.targetImage = targetImage
    }
    
    // 该方法主要操作虚拟Image,完成加载
    setSrc(targetUrl) {
       // 真实img节点初始化时展示的是一个占位图
        this.targetImage.setSrc(ProxyImage.LOADING_URL)
        // 创建一个帮我们加载图片的虚拟Image实例
        const virtualImage = new Image()
        // 监听目标图片加载的情况,完成时再将DOM上的真实img节点的src属性设置为目标图片的url
        virtualImage.onload = () => {
            this.targetImage.setSrc(targetUrl)
        }
        // 设置src属性,虚拟Image实例开始加载图片
        virtualImage.src = targetUrl
    }
}

缓存代理

对传入的参数进行求和:

// addAll方法会对你传入的所有参数做求和操作
const addAll = function() {
    console.log('进行了一次新计算')
    let result = 0
    const len = arguments.length
    for(let i = 0; i < len; i++) {
        result += arguments[i]
    }
    return result
}

// 为求和方法创建代理
const proxyAddAll = (function(){
    // 求和结果的缓存池
    const resultCache = {}
    return function() {
        // 将入参转化为一个唯一的入参字符串
        const args = Array.prototype.join.call(arguments, ',')
        
        // 检查本次入参是否有对应的计算结果
        if(args in resultCache) {
            // 如果有,则返回缓存池里现成的结果
            return resultCache[args]
        }
        return resultCache[args] = addAll(...arguments)
    }
})()

行为型:策略模式

###概念

定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换

业务场景

马上大促要来了,我们本次大促要做差异化询价。啥是差异化询价?就是说同一个商品,我通过在后台给它设置不同的价格类型,可以让它展示不同的价格。具体的逻辑如下:

  • 当价格类型为“预售价”时,满 100 - 20,不满 100 打 9 折
  • 当价格类型为“大促价”时,满 100 - 30,不满 100 打 8 折
  • 当价格类型为“返场价”时,满 200 - 50,不叠加
  • 当价格类型为“尝鲜价”时,直接打 5 折

四种价格标签化

  • 预售价 pre
  • 大促价 onSale
  • 返场价 back
  • 尝鲜价 fresh

业务代码:

// 询价方法,接受价格标签和原价为入参
function askPrice(tag, originPrice) {

  // 处理预热价
  if(tag === 'pre') {
    if(originPrice >= 100) {
      return originPrice - 20
    } 
    return originPrice * 0.9
  }
  
  // 处理大促价
  if(tag === 'onSale') {
    if(originPrice >= 100) {
      return originPrice - 30
    } 
    return originPrice * 0.8
  }
  
  // 处理返场价
  if(tag === 'back') {
    if(originPrice >= 200) {
      return originPrice - 50
    }
    return originPrice
  }
  
  // 处理尝鲜价
  if(tag === 'fresh') {
     return originPrice * 0.5
  }
}

缺点:

  • 单一职责原则: 一个function有4个逻辑,如果其中一行代码出bug,这个function就坏了
  • 开放封闭原则: 如果新增一个需求 100-50的“新人价”,只能继续if-else 优化后:
// 定义一个询价处理器对象
const priceProcessor = {
  pre(originPrice) {
    if (originPrice >= 100) {
      return originPrice - 20;
    }
    return originPrice * 0.9;
  },
  onSale(originPrice) {
    if (originPrice >= 100) {
      return originPrice - 30;
    }
    return originPrice * 0.8;
  },
  back(originPrice) {
    if (originPrice >= 200) {
      return originPrice - 50;
    }
    return originPrice;
  },
  fresh(originPrice) {
    return originPrice * 0.5;
  },
};

想使用其中某个询价算法的时候,通过标签名去定位就好了

// 询价函数
function askPrice(tag, originPrice) {
  return priceProcessor[tag](originPrice)
}

如果要新增一个业务

priceProcessor.newUser = function (originPrice) {
  if (originPrice >= 100) {
    return originPrice - 50;
  }
  return originPrice;
}

行为型:状态模式

概念

状态模式(State Pattern):允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类 状态模式主要解决的是当控制一个独享状态的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类中,可以把复杂的判断逻辑简化

业务场景:咖啡机

  • 单一职责原则
class CoffeeMaker {
  constructor() {
    /**
    这里略去咖啡机中与咖啡状态切换无关的一些初始化逻辑
  **/
    // 初始化状态,没有切换任何咖啡模式
    this.state = 'init';
  }
  changeState(state) {
    // 记录当前状态
    this.state = state;
    if(state === 'american') {
      // 这里用 console 代指咖啡制作流程的业务逻辑
      this.americanProcess();
    } else if(state === 'latte') {
      this.latteProcress();
    } else if(state === 'vanillaLatte') {
      this.vanillaLatteProcress();
    } else if(state === 'mocha') {
      this.mochaProcress();
    }
  }
  
  americanProcess() {
    console.log('我只吐黑咖啡');    
  }
  
  latteProcress() {
    this.americanProcess();
    console.log('加点奶');  
  }
  
  vanillaLatteProcress() {
    this.latteProcress();
    console.log('再加香草糖浆');
  }
  
  mochaProcress() {
    this.latteProcress();
    console.log('再加巧克力');
  }
}

const mk = new CoffeeMaker();
mk.changeState('latte');
  • 开放封闭原则
const stateToProcessor = {
  american() {
    console.log('我只吐黑咖啡');    
  },
  latte() {
    this.american();
    console.log('加点奶');  
  },
  vanillaLatte() {
    this.latte();
    console.log('再加香草糖浆');
  },
  mocha() {
    this.latte();
    console.log('再加巧克力');
  }
}

class CoffeeMaker {
  constructor() {
    /**
    这里略去咖啡机中与咖啡状态切换无关的一些初始化逻辑
  **/
    // 初始化状态,没有切换任何咖啡模式
    this.state = 'init';
  }
  
  // 关注咖啡机状态切换函数
  changeState(state) {
    // 记录当前状态
    this.state = state;
    // 若状态不存在,则返回
    if(!stateToProcessor[state]) {
      return ;
    }
    stateToProcessor[state]();
  }
}

const mk = new CoffeeMaker();
mk.changeState('latte');

再优化,把状态-行为映射对象作为主体类对应实例的一个属性添加进去

class CoffeeMaker {
  constructor() {
    /**
    这里略去咖啡机中与咖啡状态切换无关的一些初始化逻辑
  **/
    // 初始化状态,没有切换任何咖啡模式
    this.state = 'init';
    // 初始化牛奶的存储量
    this.leftMilk = '500ml';
  }
  stateToProcessor = {
    that: this,
    american() {
      // 尝试在行为函数里拿到咖啡机实例的信息并输出
      console.log('咖啡机现在的牛奶存储量是:', this.that.leftMilk)
      console.log('我只吐黑咖啡');
    },
    latte() {
      this.american()
      console.log('加点奶');
    },
    vanillaLatte() {
      this.latte();
      console.log('再加香草糖浆');
    },
    mocha() {
      this.latte();
      console.log('再加巧克力');
    }
  }

  // 关注咖啡机状态切换函数
  changeState(state) {
    this.state = state;
    if (!this.stateToProcessor[state]) {
      return;
    }
    this.stateToProcessor[state]();
  }
}

const mk = new CoffeeMaker();
mk.changeState('latte');

策略模式和状态模式

相似点: 都封装行为、都通过委托来实现行为分发 不同点:

  • 策略模式中的行为函数不依赖调用主题、互相平行。
  • 状态模式中的行为函数,首先是和状态主体之间存在关联,由状态将它们串联在一起;另一个方面,因为和状态主体存在关联,所以不同状态对应的行为函数可能不会特别割裂

行为型:观察者模式(重点面试)

概念

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新

业务场景: 钉钉群发消息

观察者模式: 角色划分 --> 状态变化 --> 发布者通知到订阅者

定义发布者类

// 定义发布者类
class Publisher {
  constructor() {
    this.observers = []
    console.log('Publisher created')
  }
  // 增加订阅者
  add(observer) {
    console.log('Publisher.add invoked')
    this.observers.push(observer)
  }
  // 移除订阅者
  remove(observer) {
    console.log('Publisher.remove invoked')
    this.observers.forEach((item, i) => {
      if (item === observer) {
        this.observers.splice(i, 1)
      }
    })
  }
  // 通知所有订阅者
  notify() {
    console.log('Publisher.notify invoked')
    this.observers.forEach((observer) => {
      observer.update(this)
    })
  }
}

订阅者 : 被通知、去执行,接受发布者的调用

// 定义订阅者类
class Observer() {
  constructor() {
    console.log('Observer created')
  }
  
  update() {
    console.log('Observer.update invoked')
  }
}

通过拓展发布者类,来使所有的订阅者来监听某个特定状态的变化

// 定义一个具体的需求文档(prd)发布类
class PrdPublisher extends Publish {
  constructor() {
    super()
    // 初始化需求文档
    this.prdState = null
    // 
  }
}

拓展发布者类,来使所有的订阅者来监听某个特定状态的变化

// 定义一个具体的需求文档(prd)发布类
class PrdPublisher extends Publisher {
    constructor() {
        super()
        // 初始化需求文档
        this.prdState = null
        // 韩梅梅还没有拉群,开发群目前为空
        this.observers = []
        console.log('PrdPublisher created')
    }
    
    // 该方法用于获取当前的prdState
    getState() {
        console.log('PrdPublisher.getState invoked')
        return this.prdState
    }
    
    // 该方法用于改变prdState的值
    setState(state) {
        console.log('PrdPublisher.setState invoked')
        // prd的值发生改变
        this.prdState = state
        // 需求文档变更,立刻通知所有开发者
        this.notify()
    }
}

作为订阅方,开发者的任务也变得具体起来:接收需求文档、并开始干活:

class DeveloperObserver extends Observer {
    constructor() {
        super()
        // 需求文档一开始还不存在,prd初始为空对象
        this.prdState = {}
        console.log('DeveloperObserver created')
    }
    
    // 重写一个具体的update方法
    update(publisher) {
        console.log('DeveloperObserver.update invoked')
        // 更新需求文档
        this.prdState = publisher.getState()
        // 调用工作函数
        this.work()
    }
    
    // work方法,一个专门搬砖的方法
    work() {
        // 获取需求文档
        const prd = this.prdState
        // 开始基于需求文档提供的信息搬砖。。。
        ...
        console.log('996 begins...')
    }
}

目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。

// 创建订阅者:前端开发李雷
const liLei = new DeveloperObserver()
// 创建订阅者:服务端开发小A(sorry。。。起名字真的太难了)
const A = new DeveloperObserver()
// 创建订阅者:测试同学小B
const B = new DeveloperObserver()
// 韩梅梅出现了
const hanMeiMei = new PrdPublisher()
// 需求文档出现了
const prd = {
    // 具体的需求内容
    ...
}
// 韩梅梅开始拉群
hanMeiMei.add(liLei)
hanMeiMei.add(A)
hanMeiMei.add(B)
// 韩梅梅发送了需求文档,并@了所有人
hanMeiMei.setState(prd)

Vue数据双向绑定(响应式系统)的实现原理

vue是什么

Vue 框架是热门的渐进式 JavaScript框架。在 Vue 中,当我们修改状态时,视图会随之更新,这就是Vue的数据双向绑定(又称响应式原理)。 数据双向绑定是Vue 最独特的特性之一。

在Vue数据双向绑定的实现逻辑里,有这样三个关键角色:

  • observer(监听器):在Vue数据双向绑定的角色结构里,所谓的 observer 不仅是一个数据监听器,它还需要对监听到的数据进行转发——也就是说它同时还是一个发布者。
  • watcher(订阅者):observer 把数据转发给了真正的订阅者——watcher对象。watcher 接收到新的数据后,会去更新视图。
  • compile(编译器):MVVM 框架特有的角色,负责对每个节点元素指令进行扫描和解析,指令的数据初始化、订阅者的创建这些“杂活”也归它管~

核心代码

实现observer

// observe方法遍历并包装对象属性
function observe(target) {
    // 若target是一个对象,则遍历它
    if(target && typeof target === 'object') {
        Object.keys(target).forEach((key)=> {
            // defineReactive方法会给目标属性装上“监听器”
            defineReactive(target, key, target[key])
        })
    }
}

// 定义defineReactive方法
function defineReactive(target, key, val) {
    // 属性值也可能是object类型,这种情况下需要调用observe进行递归遍历
    observe(val)
    // 为当前属性安装监听器
    Object.defineProperty(target, key, {
         // 可枚举
        enumerable: true,
        // 不可配置
        configurable: false, 
        get: function () {
            return val;
        },
        // 监听器函数
        set: function (value) {
            console.log(`${target}属性的${key}属性从${val}值变成了了${value}`)
            val = value
        }
    });
}

实现订阅者 Dep:

// 定义订阅者类Dep
class Dep {
    constructor() {
        // 初始化订阅队列
        this.subs = []
    }
    
    // 增加订阅者
    addSub(sub) {
        this.subs.push(sub)
    }
    
    // 通知订阅者
    notify() {
        this.subs.forEach((sub)=>{
            sub.update()
        })
    }
}

现在我们可以改写 defineReactive 中的 setter 方法,在监听器里去通知订阅者了:

function defineReactive(target, key, val) {
    const dep = new Dep()
    // 监听当前属性
    observe(val)
    Object.defineProperty(target, key, {
        set: (value) => {
            // 通知所有订阅者
            dep.notify()
        }
    })
}

实现一个Event Bus/ Event Emitter (全局事件总线)

在Vue中使用Event Bus来实现组件间的通讯

我们所有事件的订阅/发布都不能由订阅方和发布方“私下沟通”,必须要委托这个事件中心帮我们实现。 A 组件和 B 组件之间能够通信,可以通过 Event Bus 来实现

  • 创建一个 Event Bus(本质上也是 Vue 实例)并导出:
const EventBus = new Vue()
export default EventBus
  • 在主文件里引入EventBus,并挂载到全局:
 import bus from 'EventBus的文件路径'
 Vue.prototype.bus = bus
  • 订阅事件:
// 这里func指someEvent这个事件的监听函数
this.bus.$on('someEvent', func)
  • 发布(触发)事件:
// 这里params指someEvent这个事件被触发时回调函数接收的入参
this.bus.$emit('someEvent', params)
  • 实现一个Event Bus
class EventEmitter {
  constructor() {
    // handler是一个map,用于存储时间与回调之间对应关系
    this.handlers =  {}
  }
  
  // on方法用于安装事件监听器,它接受目标事件名和回调函数作为参数
  on(eventName, cb) {
    // 先检查一下目标事件名有没有对应的函数队列
    if(!this.handlers[eventName]) {
      // 如果没有,则初始化一个监听函数队列
      this.handlers[eventName] = []
    }
    
    // 把回调函数加入目标时间的监听函数队列里
    this.handlers[eventName].push(cb)
  }
  
  // emit方法用于触发目标事件,它接受事件名和监听函数入参作为参数
   emit(eventName, ...args) {
     // 检查目标事件是否有监听函数队列
     if (this.handlers[eventName]) {
       // 这里需要对 this.handlers[eventName] 做一次浅拷贝,主要目的是为了避免通过 once 安装的监听器在移除的过程中出现顺序问题
       const handlers = this.handlers[eventName].slice()
       // 如果有,则逐个调用队列里的回调函数
       handlers.forEach((callback) => {
         callback(...args)
       })
     }
   }
 
   // 移除某个事件回调队列里的指定回调函数
   off(eventName, cb) {
     const callbacks = this.handlers[eventName]
     const index = callbacks.indexOf(cb)
     if (index !== -1) {
       callbacks.splice(index, 1)
     }
   }
 
   // 为事件注册单次监听器
   once(eventName, cb) {
     // 对回调函数进行包装,使其执行完毕自动被移除
     const wrapper = (...args) => {
       cb(...args)
       this.off(eventName, wrapper)
     }
     this.on(eventName, wrapper)
   }
}

观察者模式与发布-订阅模式的区别是什么?

发布者不直接触及到订阅者、而是由统一的第三方来完成实际的通信的操作,叫做发布-订阅模式。 发布者直接触及到订阅者的操作,叫观察者模式 区别在于是否存在第三方、发布者能否直接感知订阅者 观察者模式解决的其实是模块间的耦合问题,观察者模式仅仅是减少了耦合,并没有完全地解决耦合问题——被观察者必须去维护一套观察者的集合; 而发布者完全不用感知订阅者,事件的注册和触发都发生在独立于双方的第三方平台(事件总线)上。 完全解耦

行为型:迭代器模式

概念

迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露改对象的内部表示 迭代器模式是目的性极强的模式

ES6的迭代器

ES6约定,任何数据结构只要具备Symbol.iterator属性(这个属性就是Iterator的具体实现,它本质上是当前数据结构默认的迭代器生成函数),就可以被遍历——准确地说,是被for...of...循环和迭代器的next方法遍历。 事实上,for...of...的背后正是对next方法的反复调用。 在ES6中,针对Array、Map、Set、String、TypedArray、函数的 arguments 对象、NodeList 对象这些原生的数据结构都可以通过for...of...进行遍历 我们借助数组的Symbol.iterator生成了它对应的迭代器对象,通过反复调用迭代器对象的next方法访问了数组成员,像这样:

const arr = [1, 2, 3]
// 通过调用iterator,拿到迭代器对象
const iterator = arr[Symbol.iterator]()

// 对迭代器对象执行next,就能逐个访问集合的成员
iterator.next()
iterator.next()
iterator.next()

基本等价于下面这通操作:

// 通过调用iterator,拿到迭代器对象
const iterator = arr[Symbol.iterator]()

// 初始化一个迭代结果
let now = { done: false }

// 循环往外迭代成员
while(!now.done) {
    now = iterator.next()
    if(!now.done) {
        console.log(`现在遍历到了${now.value}`)
    }
}

实现代器生成函数

// 编写一个迭代器生成函数
function *iteratorGenerator() {
    yield '1号选手'
    yield '2号选手'
    yield '3号选手'
}

const iterator = iteratorGenerator()

iterator.next()
iterator.next()
iterator.next()

ES5去写一个能够生成迭代器对象的迭代器生成函数

// 定义生成器函数,参数可以是任意集合
function iteratorGenerator (list) {
  // idx记录当前访问的索引
  var idx = 0
  // len 记录传入集合的长度
  var len = list.length
  return {
    // 自定义next方法
    next: function() {
      // 如果索引还没有超出集合长度,done为false
      var done = idx >= len
      // 如果done为false,则可以继续取值
      var value = !done ? list[idx++] : undefined
      
      // 将当前值与遍历是否完毕(done)返回
      return {
        done: done,
        value: value
      }
    }
  }
}

 var iterator = iteratorGenerator(['1号选手','2号选手','3号选手'])
 iterator.next()
 iterator.next()
 iterator.next()