前端JS常用设计模式

话不多说,这里记录一些常见的设计模式,常看常新,也能提升JavaScript编程水平

一、设计原则

二、单例模式

单例模式的定义是,保证一个类仅有一个实例,并且要提供访问他的全局api

单例模式在前端是一种很常见的模式,一些对象我们往往就只需要一个,如VueX,React-redux等框架全局状态管理工具

1.Class语法简单实现

class Singleton {
  constructor (name) {
    this.name = name
  }
  // 静态方法
  static getInstance (name) {
    if (!this.instance) {
      this.instance = new Singleton(name)
    }
    return this.instance
  }
}
let a = Singleton.getInstance('a1')
let b = Singleton.getInstance('b2')
console.log(a == b)

2.闭包结合new关键字

var instance
function Singleton (name) {
  if (!instance) {
    return (instance = this)
  }
  return instance
}
let a = new Singleton('a1')
let b = new Singleton('b1')
console.log(a===b); // true

3.代理模式创建单例

// 定义代理
var CreateSingleton = (function () {
    let instance;
    // 接收一个普通类用于创建单例特性
    return function(Singleton,name) {
        if(!instance) {
            return instance = new Singleton(name)
        }
        return instance
    }
})()

var Singleton = function(name) {
    this.name = name
}

let a = new CreateSingleton(Singleton,'a')
let b = new CreateSingleton(Singleton, 'b')
console.log(a===b); // true
  • 代理只专注创建单例
  • 让Singleton变成普通类
  • 通过代理让Singleton普通类拥有单例特性

实际应用:如页面弹窗

惰性单例:

var createDiv = (function () {
  var instance
  return function () {
    if (!instance) {
      var div = document.createElement('div')
      div.innerHTML = '我是登录窗口'
      div.style.display = 'none'
      document.body.appendChild(div)
      return instance = div
    }
    return instance
  }
})()

button.addEventListener('click', function () {
  let div = createDiv()
  div.style.display = 'block'
})

三、观察者模式

 1.被观察者

 // 被观察者类
        class Subject {
        //未传值初始为空
            constructor(state = "") {
                // 初始状态
                this.state = state
                // 观察者方法队列 
                this.observsers = []
            }
            // 设置自己的状态
            setState(val) {
            // 告诉观察者目前改变成了什么状态
                this.state = val;
                // 同时需要把自己身上的观察者方法全部触发
                this.observsers.map(item => {
                    // item是每一个观察者,每一个观察者是一个对象
                    item.callback(this.state);
                })
            }
            // 添加观察者
            addObserver(observer) {
                // 把观察者传递进来
                this.observsers.push(observer)
            }
            // 删除观察者
            removeObserver(observer) {
                // 过滤出来非当前观察者的观察者
                this.observsers = this.observsers.filter(obs => obs !== observer);
            }
        }

2.观察者

    // 观察者类
        class Observer {
            // name需要观察的参数 
            // callback 观察的参数达到边界条件触发的事件
            constructor(name, callback = () => { }) {
                this.name = name;
                this.callback = callback;
            }
        }

3.使用

初始数据

   let obj = {
            name: "abc"
        }

创建观察者与被观察者

     // 创建观察者
        const ObserverName = new Observer('监听obj改变', () => { console.log('obj发生改变'); })
        // 创建一个被观察者
        const name = new Subject(obj.name)
        // 添加一个观察者
        name.addObserver(ObserverName)
        //触发观察者方法
        name.setState('123')

四、发布-订阅模式

 模拟报纸的订阅与发布过程:

// 报社
class Publisher {
  constructor(name, channel) {
    this.name = name;
    this.channel = channel;
  }
  // 注册报纸
  addTopic(topicName) {
    this.channel.addTopic(topicName);
  }
  // 推送报纸
  publish(topicName) {
    this.channel.publish(topicName);
  }
}
// 订阅者
class Subscriber {
  constructor(name, channel) {
    this.name = name;
    this.channel = channel;
  }
  //订阅报纸
  subscribe(topicName) {
    this.channel.subscribeTopic(topicName, this);
  }
  //取消订阅
  unSubscribe(topicName) {
    this.channel.unSubscribeTopic(topicName, this);
  }
  //接收推送
  update(topic) {
    console.log(`${topic}已经送到${this.name}家了`);
  }
}
// 第三方平台
class Channel {
  constructor() {
    this.topics = {};
  }
  //报社在平台注册报纸
  addTopic(topicName) {
    this.topics[topicName] = [];
  }
  //报社取消注册
  removeTopic(topicName) {
    delete this.topics[topicName];
  }
  //订阅者订阅报纸
  subscribeTopic(topicName, sub) {
    if (this.topics[topicName]) {
      this.topics[topicName].push(sub);
    }
  }
  //订阅者取消订阅
  unSubscribeTopic(topicName, sub) {
    this.topics[topicName].forEach((item, index) => {
      if (item === sub) {
        this.topics[topicName].splice(index, 1);
      }
    });
  }
  //平台通知某个报纸下所有订阅者
  publish(topicName) {
    this.topics[topicName].forEach((item) => {
      item.update(topicName);
    });
  }
}

这里的报社我们可以理解为发布者(Publisher)的角色,订报纸的读者理解为订阅者(Subscriber),第三方平台就是事件中心;报社在平台上注册某一类型的报纸,然后读者就可以在平台订阅这种报纸;三个类准备好了,我们来看下他们彼此如何进行联系:

var channel = new Channel();

var pub1 = new Publisher("报社1", channel);
var pub2 = new Publisher("报社2", channel);

pub1.addTopic("晨报1");
pub1.addTopic("晚报1");
pub2.addTopic("晨报2");

var sub1 = new Subscriber("小明", channel);
var sub2 = new Subscriber("小红", channel);
var sub3 = new Subscriber("小张", channel);

sub1.subscribe("晨报1");
sub2.subscribe("晨报1");
sub2.subscribe("晨报2");
sub3.subscribe("晚报1");

sub3.subscribe("晨报2");
sub3.unSubscribe("晨报2");

pub1.publish("晨报1");
pub1.publish("晚报1");
pub2.publish("晨报2");

//晨报1已经送到小明家了
//晨报1已经送到小红家了
//晚报1已经送到小张家了
//晨报2已经送到小红家了

我们先定义了一个调度中心channel,然后分别定义了两个报社pub1、pub2,以及三个读者sub1、sub2和sub3;两家报社在平台注册了晨报1、晚报1和晨报2三种类型的报纸,三个读者各自订阅各家的报纸,也能取消订阅。

我们可以发现在发布者中并没有直接维护订阅者列表,而是注册了一个事件主题,这里的报纸类型相当于一个事件主题;订阅者订阅主题,发布者推送某个主题时,订阅该主题的所有读者都会被通知到;这样就避免了观察者模式无法进行过滤筛选的缺陷。

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

 

 观察者模式是一种紧耦合的状态,发布/订阅模式相比于观察者模式多了一个中间媒介,因为这个中间媒介,发布者和订阅者的关联为松耦合

  • 通知订阅者的方式不同
  • 内部维护的内容不同

观察者模式把观察者对象维护在目标对象中的,需要发布消息时直接发消息给观察者。在观察者模式中,目标对象本身是知道观察者存在的。发布/订阅模式中,发布者并不维护订阅者,也不知道订阅者的存在,所以也不会直接通知订阅者,而是通知调度中心,由调度中心通知订阅者。

Vue的基本原理即运用发布-订阅模式,具体可查看手写vue2.x原理:https://gitee.com/younghxp/vue2-data-response

五、策略模式

策略模式的目的就是使算法的使用与算法分离开来

例子:不同的输入需要产生不同的输出,比如现在需要根据员工的等级来发最后的年终奖,这时候最简便的方法莫过于写一大堆 if···else··· 语句了。就像这样:

var calculateBonus = function( level, salary ){ 
    if ( level === 'S' ){ 
         return salary * 4; 
    } 
    if ( level === 'A' ){ 
         return salary * 3; 
    } 
    if ( level === 'B' ){ 
        return salary * 2; 
    } 
}; 
calculateBonus( 'B', 20000 ); // 输出:40000 
calculateBonus( 'S', 6000 ); // 输出:24000

 缺点明显:

  • 整个calculateBouns函数太庞大了,包含了非常多的if-else语句,这些语句需要覆盖所有的语句。
  • 违背了开放封闭原则。如果需要临时增加员工等级或者修改分配方式,那么只能直接去修改原来的代码,增加判断条件,这种侵入是很大的。
  • 重用性低,这样的代码无法高效复用,只能无脑CV。
策略模式就是定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换,那么这是比较官方的说法。代入我们这个计算奖金的例子里面来讲的话,每个if语句里面的逻辑就相当于算法,我们要把这些算法用一些手段封装起来,让它们之间独立,保持一个平等的关系也就相当于它们之间可以相互替换

 改写策略模式:

首先,策略模式至少由两部分组成,第一个是一组策略类,策略类封装了具体的算法,并负责具体的计算过程。第二个是Context(上下文),Context负责接收请求,之后把请求委托给某一个具体的策略类。
其实很好理解,Context就相当于一个中转站,接收不同的计算请求,然后把具体的计算任务转发给某一个具体策略类

1.定义一组策略类

let Bouns = function () {
  this.salary = null;
  this.strategy = null
}
Bouns.prototype.setSalary = function (salary) {
  // 设置员工的原始工资
  this.salary = salary;
}
Bouns.prototype.setStrategy = function (strategy) {
  // 设置策略对象
  this.strategy = strategy;
}
Bouns.prototype.getBouns = function () {
  return this.strategy.calculate(this.salary);
}

2.实现中转站

let Bouns = function () {
  this.salary = null;
  this.strategy = null
}
Bouns.prototype.setSalary = function (salary) {
  // 设置员工的原始工资
  this.salary = salary;
}
Bouns.prototype.setStrategy = function (strategy) {
  // 设置策略对象
  this.strategy = strategy;
}
Bouns.prototype.getBouns = function () {
  return this.strategy.calculate(this.salary);
}

3.实现策略模式年终奖计算

let bouns = new Bouns()
bouns.setSalary(20000);
bouns.setStrategy(new LevelS())
console.log(bouns.getBouns())   // 输出: 80000

JavaScript版本的策略模式

 上面的策略模式是模仿一些传统的面向对象编程语言的策略模式的实现,而在JavaScript中对象不一定需要构造函数实例化,而且函数也是对象。完全可以将那一组组的策略类变成一个普通对象的属性,把中转站变成一个普通函数

1.创建策略对象

let strategies = {
  'S': function (salary) {
    return salary * 4
  },
  'A': function (salary) {
    return salary * 3
  },
  'B': function (salary) {
    return salary * 2
  },
}

2.创建一个计算函数

function calculateBouns (level, salary) {
  return strategies[level](salary);
}
console.log(calculateBouns('S', 20000))

采用策略模式优点:

  1. 可以有效地避免多重条件选择语句
  2. 代码复用性高,避免了很多粘贴复制的操作。
  3. 策略模式提供了对开放封闭原则的支持,将算法独立封装在strategies中,使得它们易于切换,易于扩展。

六、代理模式

为一个对象提供一个代用品或占位符,以便控制对它的访问;

该模式场景需要三类角色,分别为使用者、目标对象和代理者,使用者的目的是直接访问目标对象,但却不能直接访问,而是要先通过代理者。因此该模式非常像明星代理人的场景。其特征为:

  • 使用者无权访问目标对象;
  • 中间加代理,通过代理做授权和控制

在js中常用到的是缓存代理虚拟代理

1.虚拟代理

例子:图片懒加载场景

不使用代理模式:

// 不使用代理的预加载图片函数如下
var myImage = (function(){
    var imgNode = document.createElement("img");
    document.body.appendChild(imgNode);
    var img = new Image();
    img.onload = function(){
        imgNode.src = this.src;
    };
    return {
        setSrc: function(src) {
            imgNode.src = "loading.gif";
            img.src = src;
        }
    }
})();
// 调用方式
myImage.setSrc("pic.png");

缺点:

  • 代码耦合度比较大,一个函数内负责做了几件事情。未满足面向对象设计原则中单一职责原则;
  • 当某个时候不需要图片预加载的时候,需要从myImage 函数内把代码删掉,这样代码耦合性太高。

使用代理模式:

var myImage = (function(){
    var imgNode = document.createElement("img");
    document.body.appendChild(imgNode);
    return {
        setSrc: function(src) {
            imgNode.src = src;
        }
    }
})();
// 代理模式
var ProxyImage = (function(){
    var img = new Image();
    img.onload = function(){
        myImage.setSrc(this.src);
    };
    return {
        setSrc: function(src) {
            myImage.setSrc("loading.gif");
            img.src = src;
        }
    }
})();
// 调用方式
ProxyImage.setSrc("pic.png");

优点:

  • myImage 函数只负责做一件事。创建img元素加入到页面中,其中的加载loading图片交给代理函数ProxyImage 去做。
  • 加载成功以后,代理函数ProxyImage 会通知及执行myImage 函数的方法。
  • 当以后不需要代理对象的话,我们直接可以调用本体对象的方法即可

2.缓存代理

var mult = function(){
    var a = 1;
    for(var i = 0,ilen = arguments.length; i < ilen; i+=1) {
        a = a*arguments[i];
    }
    return a;
};
// 计算加法
var plus = function(){
    var a = 0;
    for(var i = 0,ilen = arguments.length; i < ilen; i+=1) {
        a += arguments[i];
    }
    return a;
}
// 代理函数
var proxyFunc = function(fn) {
    var cache = {};  // 缓存对象
    return function(){
        var args = Array.prototype.join.call(arguments,',');
        if(args in cache) {
            return cache[args];   // 使用缓存代理
        }
        return cache[args] = fn.apply(this,arguments);
    }
};
var proxyMult = proxyFunc(mult);
console.log(proxyMult(1,2,3,4)); // 24
console.log(proxyMult(1,2,3,4)); // 缓存取 24

var proxyPlus = proxyFunc(plus);
console.log(proxyPlus(1,2,3,4));  // 10
console.log(proxyPlus(1,2,3,4));  // 缓存取 10

实际应用:HTML元素事件代理;ES6 Proxy等

七、装饰器模式

装饰器,顾名思义,就是在原来方法的基础上去装饰一些针对特别场景所适用的方法,即添加一些新功能。因此其特征主要有两点:

  • 为对象添加新功能;
  • 不改变其原有的结构和功能,即原有功能还继续会用,且场景不会改变。

有点类似代理模式

class Circle {
    draw() {
        console.log('画一个圆形');
    }
}

class Decorator {
    constructor(circle) {
        this.circle = circle;
    }
    draw() {
        this.circle.draw();
        this.setRedBorder(circle);
    }
    setRedBorder(circle) {
        console.log('画一个红色边框');
    }
}

let circle = new Circle();
let decorator = new Decorator(circle);
decorator.draw(); //画一个圆形,画一个红色边框

装饰器模式,让对象更加稳定,且易于复用。而不稳定的功能,则可以在个性化定制时进行动态添加。

实际使用:ajax请求的拦截器等

posted @ 2022-06-22 16:09  盼星星盼太阳  阅读(316)  评论(0编辑  收藏  举报