第五十周总结——JavaScript设计模式

对该文章的学习总结https://juejin.cn/book/6844733790204461070

设计模式

设计模式

构造器模式

创建的多个实例对象都具有相同的属性。

function User(name , age, career) {
  this.name = name
  this.age = age
  this.career = career 
}
const user = new User(name, age, career)

简单工厂模式

创建的多个实例对象都既有有相同的属性又有不同的属性。

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

抽象工厂模式

抽象工厂不干活,抽象工厂里面的具体工厂干活。遵守开放封闭原则。让代码对拓展开放,对修改封闭。
例:我们创建一个手机工厂的流水线。

  1. 定义手机抽象工厂
class MobilePhoneFactory {
  // 提供操作系统的接口
  createOS(){
      throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!");
  }
  // 提供硬件的接口
  createHardWare(){
      throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!");
  }
}
  1. 定义手机具体工厂,具体工厂继承于抽象工厂
class FakeStarFactory extends MobilePhoneFactory {
  createOS() {
      // 提供安卓系统实例
      return new AndroidOS()
  }
  createHardWare() {
      // 提供高通硬件实例
      return new QualcommHardWare()
  }
}
  1. 定义手机操作系统的抽象产品
class OS {
    controlHardWare() {
        throw new Error('抽象产品方法不允许直接调用,你需要将我重写!');
    }
}
  1. 定义手机操作系统的具体产品
class AndroidOS extends OS {
    controlHardWare() {
        console.log('我会用安卓的方式去操作硬件')
    }
}
  1. 定义手机硬件的抽象产品
class HardWare {
  // 手机硬件的共性方法,这里提取了“根据命令运转”这个共性
  operateByOrder() {
      throw new Error('抽象产品方法不允许直接调用,你需要将我重写!');
  }
}
  1. 定义手机硬件的具体产品
class MiWare extends HardWare {
  operateByOrder() {
      console.log('我会用小米的方式去运转')
  }
}

如果我们想再创建一部新的手机,就可以直接重新创建一个类来继承不同的操作系统和硬件。这样就实遵循了开放封闭原则。

单例模式

让一个类的实例只存在一个,不管我们尝试去创建多少次,它都只给你返回第一次所创建的那唯一的一个实例。

  1. 使用ES6的class实现单例模式
class Requset {
  constructor(){}
  static instance;
  static getInstance(){
    if(!this.instance){
      this.instance = new Requset();
    }
    return this.instance;
  }
}

const r1:Requset = Requset.getInstance();
const r2:Requset = Requset.getInstance();

console.log(r1 === r2);//true
  1. 使用闭包实现单例模式
SingleDog.getInstance = (function() {
    // 定义自由变量instance,模拟私有变量
    let instance = null
    return function() {
        // 判断自由变量是否为null
        if(!instance) {
            // 如果为null则new出唯一实例
            instance = new SingleDog()
        }
        return instance
    }
})()
  1. Vuex中的单例模式
    Vuex内部实现了一个install方法,这个方法再被调用时会将Store注入到Vue实例中去,Vuex就是使用单例模式让全局Store唯一。
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)
}

单例模式练习

  1. 实现Storage,使得该对象为单例,基于 localStorage 进行封装。实现方法 setItem(key,value) 和 getItem(key)。
    使用ES6的class实现
class Storage {
    constructor(){};
    static instance;
    static getInstance(){
        if(!this.instance){
            this.instance = new Storage();
        }
        return Storage.instance;
    };
    getItem(key){
        return localStorage.getItem(key);
    };
    setItem(key, value){
        return localStorage.setItem(key, value);
    }
}

使用闭包实现

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
    }
})()
  1. 实现一个全局的模态框
<!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>

原型模式

在JavaScript中只存在原型模式,不存在类模式,JavaScript中的类是使用原型继承的语法糖。
例如:

class Dog {
    constructor(name, age){
        this.name = name;
        this.age = age;
    }
    eat(){
        console.log('吃骨头');
    }
}

可以将上面的代码写成下面的形式

function Dog(name, age){
    this.name = name;
    this.age = age;
}
Dog.prototype.eat = function(){
    console.log('吃骨头');
}
  1. 对象的深拷贝
    注意:深拷贝没有完美的方案,每一种方案都有它的边界
  • 第一种方法使用JSON.stringify和JSON.parse的方式
const data = {
  name: 'tom',
  fn: ()=>{
    console.log('我是一个函数');
  }
}
const dataStr = JSON.stringify(data);
const copy = JSON.parse(dataStr);
console.log(copy);

上面的方法只拷贝了name参数,没有拷贝fn参数,应为使用JSON.stringify和JSON.parse的方式进行深拷贝不能对函数和正则进行拷贝。

  • 第二种使用递归的方式
function deepClone(obj){
  if(typeof obj!=='object'||obj===null){
    return obj;
  }
  let ans={};
  if(obj.constructor===Array){
    ans=[];
  }
  for(let key in obj){
    console.log(obj.hasOwnProperty(key));
    if(obj.hasOwnProperty(key)){
      ans[key] = deepClone(obj[key]);
    }
  }
  return ans;
}

装饰器模式

装饰器模式为了不被已有的业务逻辑干扰,实现新的逻辑
例:我们现在点击按钮,弹窗会打开,我们想在弹窗打开后又将按钮变成灰色,然后文字变成“快去登录”

  1. ES5实现方法

//旧的逻辑
function openModal() {
    const modal = new Modal()
    modal.style.display = 'block'
}
//新的逻辑
function changeButtonText() {
    // 按钮文案修改逻辑
    const btn = document.getElementById('open')
    btn.innerText = '快去登录'
}
function disableButton() {
    // 按钮置灰逻辑
    const btn =  document.getElementById('open')
    btn.setAttribute("disabled", true)
}
function changeButtonStatus() {
    // 新版本功能逻辑整合
    changeButtonText()
    disableButton()
}

document.getElementById('open').addEventListener('click', function() {
    openModal()
    changeButtonStatus()
})
  1. ES6实现方法
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.changeButtonText()
        this.disableButton()
    }
    
    disableButton() {
        const btn =  document.getElementById('open')
        btn.setAttribute("disabled", true)
    }
    
    changeButtonText() {
        const btn = document.getElementById('open')
        btn.innerText = '快去登录'
    }
}

document.getElementById('open').addEventListener('click', function() {
    // openButton.onClick()
    decorator.onClick()
})

ES7中的装饰器

下面的代码可能有的浏览器不支持,可以在babeljs中转成js代码再运行。

function classDecorator(target){
  target.hasDecorator = true;
  return target;
}
@classDecorator
class Button {}


console.log('Button是否被装饰了', Button.hasDecorator);

适配器模式

适配器模式就是将一个类的接口变换成所期待的另一种接口。
例如:我有一个圆孔的耳机,需要插到方形耳机孔的手机上面,就需要一个适配器来让手机能够插上耳机。
封装一个fetch

export default class Http {
  //get方法
  static get(url){
    return new Promise((resolve, reject)=>{
      fetch(url)
      .then(response=>response.json())
      .then(result=>{
        resolve(result);
      })
      .catch(error=>{
        reject(error);
      })
    })
  }
  //post方法
  static post(url, data){
    return new Promise((resolve, reject)=>{
      fetch(url, {
        method: 'POST',
        headers: {
          Accept: 'application/json',
          'Content-Type': 'application/x-www-wform-urlencoded'
        },
        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(! === 0){
        str += prop + '=' + obj[prop];
      }else{
        str += '&' + prop + '=' + obj[prop];
      }
      return str;
    }
  }
}
  1. fetch适配ajax
    ajax的请求写法如下
Ajax('get', url, data, function(data){
    //成功的回调逻辑
},function(error){
    //失败的回调逻辑
});

如果我们想用上面的fetch修改ajax请求,如果一个一个修改的话十分麻烦,这时候我们就可以写一个适配器

async function AjaxAdapter(type, url, data, success, failed){
  const type = type.toUpperCase();
  let result;
  try {
    if(type === 'GET'){
      result = await Http.get(url) || {};
    }else if(type === 'POST'){
      result = await Http.post(url, data) || {};
    }
    //假设请求对应的状态码是200
    result.code === 200 && success ? success(result) : failed(result.code);
  } catch (error) {
    if(failed){
      failed(error.code);
    }
  }
}

async function Ajax(type, url, data, success, failed){
  await AjaxAdapter(type, url, data, success, failed);
}

代理模式

代理模式就是在某种情况下,一个对象不能直接访问另一个对象,需要一个第三者(代理)来间接达到访问目的

const girl = {
  a: 1
};
const proxy = new Proxy(girl, {
  get: (girl, key)=>{
    console.log(gril, 'key='+key);
  },
  set: (gril, key)=>{
    console.log(gril, key);
  }
});

const a = girl.a;
proxy.a = 2;

代理模式的实践

  1. 事件代理
    下面的代码实现了,点击哪个a标签就弹窗显示对应a标签上的字,但是a标签多的情况下,需要给每个a标签绑定事件,那么性能的开销就会更大。
<!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>事件代理</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>
    <a href="">7</a>
    <a href="">8</a>
    <a href="">9</a>
    <a href="">10</a>
  </div>
  <script>
    const aNodes = document.getElementById('father').getElementsByTagName('a');
    const len = aNodes.length;
    for(let i=0;i<len;i++){
      aNodes[i].addEventListener('click', function(e){
        e.preventDefault();
        alert(`我是${aNodes[i].innerText}`);
      });
    }
  </script>
</body>
</html>

我们使用事件代理来修改js代码

const father = document.getElementById('father');
    father.addEventListener('click', function(e){
      if(e.target.tagName === 'A'){
        e.preventDefault();
        alert(`我是${e.target.innerText}`);
      }
    });
  1. 虚拟代理-实现图片的懒加载
<!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>虚拟代理</title>
</head>
<body>
  <img class="img" src="" alt="11" width="100" height="100">
  <script>
    class PreLoadImage {
      constructor(imgNode){
        //获取真实的DOM节点
        this.imgNode = imgNode;
      }
      //操作img节点的src属性
      setSrc(imgUrl){
        this.imgNode.src = imgUrl;
      }
    }

    class ProxyImage {
      static LOADING_URL = 'https://p6-passport.byteacctimg.com/img/user-avatar/620a16ed8f1917ba1825537525a22dce~300x300.image'
      constructor(targetImage){
        //目标Image,即PreLoadImage实例
        this.targetImage = targetImage;
      }
      serSrc(targetUrl){//targetUrl就是真实的图片路径
        //真实img节点初始化时展示的是一个占位图
        this.targetImage.setSrc(ProxyImage.LOADING_URL);
        //创建一个帮我们加载图片的虚拟Image实例
        const virtualImage = new Image();
        //监听目标图片加载的情况,完成时再将DOM上的真实img节点的src属性设置为目标图片的url
        virtualImage.onload = () => {
          setTimeout(()=>{
            this.targetImage.setSrc(targetUrl);
          },1000)
        }
        //设置src属性,虚拟Image实例开始加载图片
        virtualImage.src = targetUrl;
      }
    }
    const img = document.querySelector('.img');
    const set = new ProxyImage(new PreLoadImage(img));
    set.serSrc('');//设置真实的图片地址
  </script>
</body>
</html>
  1. 缓存代理
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)
  }
})()

console.log(proxyAddAll(1,2,3));
console.log(proxyAddAll(1,2,3));
  1. 保护模式
    保护模式就是我们之前演示的代理模式,在getter和setter中进行拦截。

策略模式

例:如果输入的是a就执行a的逻辑,如果是b就执行b的逻辑,我们可以如下这样写。

function ans(tag){
  if(tag==='a'){
    //执行a的逻辑
  }else if(tag==='b'){
    //执行b的逻辑
  }
}

如果逻辑再多一点,上面的代码逻辑就太胖了,而且违背了“开放闭合原则”。可以修改成一下格式。

  1. 使用职责分离修改代码
function a(){
  //执行a的逻辑
}
function b(){
  //执行b的逻辑
}
function ans(tag){
  if(tag==='a'){
    a();
  }
  if(tag==='b'){
    b();
  }
}
  1. 上面的代码是经过开放封闭改造过的,如果想在ansObj里面写新的逻辑,就可以直接通过ansObj.属性,来添加
const ansObj = {
  a(){
    //执行a的逻辑
  },
  b(){
    //执行b的逻辑
  }
}

function ans(tag){
  return ansObj[tag]();
}

状态模式

状态模式和策略模式十分的相识。我们还拿策略模式的例子举例。

  1. 使用职责分离修改代码
class ans {
  constructor(){
    this.state = 'init';
  }
  changeState(state){
    this.state = state;
    if(state==='a'){
      this.a();
    }else if(state==='b'){
      this.b();
    }
  }
  a(){
    //执行a的逻辑
  }
  b(){
    //执行b的逻辑
  }
}
  1. 使用开放封闭修改代码
const ansObj = {
  a(){
    //执行a的逻辑
  },
  b(){
    //执行b的逻辑
  }
}

class Ans {
  constructor(){
    this.state = 'init';
  }
  changeState(state){
    this.state = state;
    if(!ansObj[state])return;
    ansObj[state]();
  }
}

const test = new Ans();
test.changeState('a');
  1. 修改开放封闭的代码,让Ans于ansObj建立联系
class Ans {
  constructor(){
    this.state = 'init';
  }
  const ansObj = {
    that:this,//在对象中使用this.that指向Ans中的this
    a(){
      //执行a的逻辑
    },
    b(){
      //执行b的逻辑
    }
}
  changeState(state){
    this.state = state;
    if(!this.ansObj[state])return;
    this.ansObj[state]();
  }
}

const test = new Ans();
test.changeState('a');

观察者模式(发布-订阅模式)

观察者模式是一种一对多的依赖关系,让多个观察者同时监听一个目标对象,如果目标对象状态发生变化时,回通知所有观察者,使观察者可以自动更新。

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

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

//定义一个具体的发布类
class PrdPublicher extends Publisher {
  constructor(props) {
    super(props);
    this.prdState = null;
    console.log('PrdPublicher created');
  }
  //获取当前的prdState
  getState(){
    console.log('PrdPublicher get');
    return this.prdState;
  }
  //设置prdState的值
  setState(state){
    this.prdState = state;
    this.notify();
    console.log('PrdPublicher set');
  }
}

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 aim = new PrdPublicher();
//观察者
const view1 = new DeveloperObserver();
const view2 = new DeveloperObserver();

//一对多,让一个目标值对应多个观察者
aim.add(view1);
aim.add(view2);

//当目标值中的值改变,观察者对应的值也都自动更新
aim.setState('1');

迭代器模式

迭代器模式提供一种顺序访问一个对象中的各个元素,但是又不暴露该对象内部的表示。

  1. 通过forEach方法遍历一个数组
const arr = [1,2,3];
arr.forEach((item,index)=>{
  console.log(`索引为${index}的元素是${item}`);
})

forEach不是万能的,如果用forEach遍历伪数组则会报错。
2. ES6对迭代器的实现
在ES6中不仅有Array(数组)和Object(对象),还新增了Map和Set。所以ES6也推出了一套统一的接口机制——迭代器(Iterator)。

const arr = [1,2,3];
const iterator = arr[Symbol.iterator]();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
  1. for…of…就是反复调用迭代器对象的next方法。
const arr = [1,2,3];
for(item of arr) {
    console.log(`当前元素是${item}`)
}
  1. ES6实现一个迭代器生成函数
function *iteratorGenerator(){
  yield '1'
  yield '2'
  yield '3'
}
const iterator2 = iteratorGenerator();
console.log(iterator2.next());
console.log(iterator2.next());
console.log(iterator2.next());
  1. ES5实现一个迭代器生成函数
const arr = [1,2,3];
function iteratorGenerator2(list){
  //记录当前的索引
  var idx = 0;
  //集合的长度
  var len = list.length;
  return {
    next: function(){
      //如果索引值没有超过集合长度,done为false
      var done = idx >= len;
      //如果done为false则可以继续取值
      var value = !done ? list[idx++] : undefined;
      //将当前值和是否完毕(done)返回
      return {
        done: done,
        value: value
      }
    }
  }
}

var iterator3 = iteratorGenerator2(arr);
console.log(iterator3.next());
console.log(iterator3.next());
console.log(iterator3.next());

猜你喜欢

转载自blog.csdn.net/qq_51965698/article/details/126471544