js设计模式

93 阅读36分钟

设计模式的概念

设计模式(design)是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案

这个术语是由四人帮在1990年代从建筑设计领域引入到计算机科学的。

为什么要学习设计模式

第一个,设计模式可以让你的程序体现出更好的代码重用性、可靠性、可维护性、可以让程序体现出高内聚、低耦合的特性。

  • 代码重用性:即相同功能的代码,一次编写,多次使用
  • 可读性:即编码的规范性
  • 可扩展性:即可维护性,当需要增加新功能的时候,成本非常第,课扩展性就非常高

高内聚&低耦合

  • 内聚:主要描述的是模块内部
  • 耦合:主要描述的是模块内部之间的关系
  • 高内聚:一个功能模块内部的元素,关联性非常强
  • 低耦合:模块之间的耦合性非常低

第二个,掌握了设计模式,看一些源码会非常清晰
第三个,设计模式也是系统合理重构的指南针。

设计模式原则

何为设计模式的原则?

  • 程序员在编程时,应当遵守的准则
  • 设计模式的基础、依据
  • 不是孤立存在的相互依赖的、相互补充的,每一种都很重要
名称设计原则简介
单一职责原则类的职责要单一,不能将太多的职责放到一个类中
开闭原则软件实体对扩展是开放的,但对修改是关闭的,即在不修改一个软件实体的基础上去扩展其功能
里氏代换原则在软件系统中,一个可以接受基类对象的地方必然可以接受一个子类对象
依赖倒置原则要针对抽象层编程,二不要针对具体类编程
接口隔离原则使用多个专门的接口来取代一个统一的接口
合成复用原则在系统中应该尽量多使用组合和聚合关联关系,尽量少使用甚至不使用继承关系
迪米特法则一个软件实体对其他实体的引用越少越好,或者说如果两个类不必彼此之间通信,那么这两个类就不应当发生直接的互相作用,而是通过引入一个第三者发生间接交互

设计模式的分类

总体来说设计模式分为三大类:

创建型模式,共五种:工厂模式、单例模式、建造者模式、原型模式

结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式

行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式

单例模式

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

应用场景——创建全局唯一的div节点

使用 CreateDiv 单例类,它的作用是负责在页面中创建唯一的 div 节点

es5方式:

var CreateDiv = function( html ){
    this.html = html;
    this.init(); 
};
CreateDiv.prototype.init = function(){
    var div = document.createElement( 'div' ); 
    div.innerHTML = this.html; 
    document.body.appendChild( div );
};
var SingletonCreateDiv = (function(){
    var instance;
    return function( html ){
        if ( !instance ){
            instance = new CreateDiv( html ); // 实例未存在,就创建实例
        }
        return instance; // 实例已存在,就返回实例
    }
})();
var a = new SingletonCreateDiv( 'sven1' ); 
var b = new SingletonCreateDiv( 'sven2' );
alert( a === b );  //true

es6方式:


class SingletonCreateDiv {
    constructor(html){
        if (!SingletonCreateDiv.instance) {
            return SingletonCreateDiv.instance;
        }
        this.html = html
        this.init(); 
        SingletonCreateDiv.instance = this;
    }
    init(){
        const div = document.createElement( 'div' ); 
        div.innerHTML = this.html; 
        document.body.appendChild( div );
    };
}

惰性单例

指的是在需要的时候才创建对象实例。惰性单例是单例模式的重点

应用场景——创建唯一的浮窗

创建唯一的浮窗,登录窗

// 闭包存储result
const getSingle = function( fn ){
   let result;
   return function(){
       return result || ( result = fn .apply(this, arguments ) );
   } 
};
// 创建div
const createLoginLayer = function(){
    const div = document.createElement( 'div' );
    div.innerHTML = '我是登录浮窗';
    div.style.display = 'none';  
    document.body.appendChild( div );

    return div;
};
const createSingleLoginLayer = getSingle( createLoginLayer );
// 点击按钮展示div
document.getElementById( 'loginBtn' ).onclick = function(){ 
    const loginLayer = createSingleLoginLayer(); 
    loginLayer.style.display = 'block';
};

项目中使用场景:toast弹框,全局登录弹窗(知乎)

总结

由于在系统内存中只存在一个对象,因此可以减少不必要的内存开销,节约系统资源,当需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能

工厂模式

介绍

工厂模式定义了一个创建对象的抽象方法,由子类决定要实例化的类

工厂模式大体分为三类:简单工厂模式、工厂方法模式、抽象工厂模式

简单工厂模式

function bookShop (name, year, vs) {
  var book = new Object();
  book.name = name;
  book.year = year;
  book.vs = vs;
  book.price = '暂无标价';
  if (name === 'JS高级编程') {
    book.price = '79';
  }
  if (name === 'css世界') {
    book.price = '69';
  }
  if (name === 'VUE权威指南') {
    book.price = '89';
  }
  return book;
}
var book1 = bookShop('JS高级编程', '2013', '第三版');
var book2 = bookShop('ES6入门教程', '2017', '第六版');
var book3 = bookShop('css世界', '2015', '第一版');
console.log(book1)
console.log(book2)
console.log(book3)

book1 instanceof bookShop // false 
// 这就是与构造函数的区别

工厂方法模式

var BookShop = function (name) {
  // 如果外部直接调用了BookShop而不是new关键字调用,则返回new BookShop调用,否则直接调用
  // 这个产品类创建实例返给外部
  if (this instanceof BookShop) {
    var book = new this[name]()
    return book
  } else {
    return new BookShop(name)
  }
}
BookShop.prototype = {
  Programme: function () {
    this.books = ['css世界', 'JS高级编程', 'ES6入门教程']
  },
  Science: function () {
    this.books = ['人与自然', '大自然的奥秘', '走进科学']
  },
  Society: function () {
    this.books = ['精神小伙修炼手册', '摇花手', '豆豆鞋']
  }
}

var programme = new BookShop('programme');
var science = BookShop('science');
var society = BookShop('society');
console.log(programme) // books: (3) ["css世界", "JS高级编程", "ES6入门教程"]
console.log(science) // books: (3) ["人与自然", "大自然的奥秘", "走进科学"]
console.log(society) // books: (3) ["精神小伙修炼手册", "摇花手", "豆豆鞋"]

抽象工厂模式

实际上就是多个具体工厂类的集合,该模式的扩张需要修改源代码,违背了开闭原则

21646150360_.pic_hd.jpg

总结

以下几种情景下工厂模式特别有用:

  1. 对象的构建十分复杂
  2. 需要依赖具体环境创建不同实例
  3. 处理大量具有相同属性的小对象

好处: 解藕;减少代码量,易于维护

策略模式

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

应用场景1——表单校验

表单校验

  • 未使用策略模式
var registerForm = document.getElementById( 'registerForm' );
registerForm.onsubmit = function(){
    if ( registerForm.userName.value === '' ){
        alert ( '用户名不能为空' );
        return false; 
    }
    if ( registerForm.password.value.length < 6 ){ 
        alert ( '密码长度不能少于 6 位' );
        return false;
    }
    if ( !/(^1[3|5|8][0-9]{9}$)/.test( registerForm.phoneNumber.value ) ){
        alert ( '手机号码格式不正确' ); 
        return false;
    }
}
  • 使用策略模式
var strategies = {
    isNonEmpty: function( value, errorMsg ){
        if ( value === '' ){ 
            return errorMsg ;
        } 
    },
    minLength: function( value, length, errorMsg ){ 
        if ( value.length < length ){
            return errorMsg;
        }
    },
    isMobile: function( value, errorMsg ){ // 手机号码格式
        if ( !/(^1[3|5|8][0-9]{9}$)/.test( value ) ){ 
            return errorMsg;
        } 
    }
};
var Validator = function(){
    this.cache = []; // 保存校验规则
};
Validator.prototype.add = function( 
    var ary = rule.split( ':' ); 
    this.cache.push(function(){ //
        var strategy = ary.shift(); 
        ary.unshift( dom.value ); 
        ary.push( errorMsg ); // 
        return strategies[strategy].apply(dom, ary);
    }); 
};
Validator.prototype.start = function(){
    for ( var i = 0, validatorFunc; validatorFunc = this.cache[ i++ ]; ){
        var msg = validatorFunc(); // 开始校验,并取得校验后的返回信息 
        if ( msg ){ // 如果有确切的返回值,说明校验没有通过
              return msg; 
        }
    }
};
var validataFunc = function(){
    var validator = new Validator(); // 创建一个 validator 对象
    /***************添加一些校验规则****************/
    validator.add( registerForm.userName, 'isNonEmpty', '用户名不能为空' );           
    validator.add( registerForm.password, 'minLength:6', '密码长度不能少于 6位');     
    validator.add( registerForm.phoneNumber, 'isMobile', '手机号码格式不正确' );
    var errorMsg = validator.start(); // 获得校验结果
    return errorMsg; // 返回校验结果 
}
var registerForm = document.getElementById( 'registerForm' ); registerForm.onsubmit = function(){
    var errorMsg = validataFunc(); // 如果 errorMsg 有确切的返回值,说明未通过校验 
    if ( errorMsg ){
        alert ( errorMsg );
        return false; // 阻止表单提交 
    }
};

应用场景2

奖金计算,绩效为 S 的人年 终奖有 4 倍工资,绩效为 A 的人年终奖有 3 倍工资,而绩效为 B 的人年终奖是 2 倍工资

  • 未使用策略模式:
var calculateBonus = function( performanceLevel, salary ){
    if ( performanceLevel === 'S' ){
         return salary * 4;
    }
    if ( performanceLevel === 'A' ){ 
         return salary * 3;
    }
    if ( performanceLevel === 'B' ){ 
         return salary * 2;
    } 
};
calculateBonus( 'B', 20000 ); // 输出:40000 
calculateBonus( 'S', 6000 ); // 输出:24000
  • 使用策略模式:
var strategies = {
    "S": function( salary ){
         return salary * 4;
     },
    "A": function( salary ){ 
         return salary * 3;
    },
    "B": function( salary ){
         return salary * 2;
    }
};
var calculateBonus = function( level, salary ){ 
    return strategies[ level ]( salary );
};
console.log( calculateBonus( 'S', 20000 ) );// 输出:80000
console.log( calculateBonus( 'A', 10000 ) );// 输出:30000

项目中使用场景
growth-public-console工程
例1:
/growth-publish-console/src/pages/page-center/addEdit/left/constants.tsx image.png /growth-publish-console/src/pages/page-center/addEdit/left/index.tsx 331646188849_.pic.jpg

例2:
/growth-publish-console/src/pages/page-center/addEdit/right/components/Actions/components/ClickAction/index.tsx image.png

总结

使代码调理清晰

代理模式

代理模式 是为一个对象提供一个代用品或占位符,以便控制对它的访问。可以这样理解:在出发点到目的地之间有一道中间层,意为代理

常见的代理模式:

  • 保护代理 用于控制不同权限的对象对目标对象的访问,但在 JavaScript 并不容易实现保护代理,因为我们无法判断谁访问了某个对象。
  • 虚拟代理 虚拟代理是把一些开销很大的对象,延迟到真正需要它的时候才去创建执行。

虚拟代理应用场景——实现图片预加载

  • 虚拟代理实现图片预加载
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 ); //img的src加载完成后,myImage的src被img的src替换
    }
    return {
        setSrc: function( src ){
             myImage.setSrc( 'file:// /C:/Users/svenzeng/Desktop/loading.gif' );//预加载占位图片
             img.src = src;  
        }
    } 
})();
proxyImage.setSrc('http://imgcache.qq.com/music/photo/k/000GGDys0yA0Nk.jpg' );
  • 虚拟代理合并HTTP请求

假设我们在做一个文件同步的功能,当我们选中一个 checkbox 的时候,它对应的文件就会被同 步到另外一台备用服务器上面。当一次选中过多时,会产生频繁的网络请求。将带来很大的开销。可以通过一个代理函数 proxySynchronousFile 来收集一段时间之内的请求, 最后一次性发送给服务器

var synchronousFile = function( id ){ 
    console.log( '开始同步文件,id 为: ' + id );
};
var proxySynchronousFile = (function(){
    var cache = [], // 保存一段时间内需要同步的 ID
    timer; // 定时器
    return function( id ){
        cache.push( id );
            if ( timer ){ // 保证不会覆盖已经启动的定时器
                 return; 
            }
        timer = setTimeout(function(){ 
            synchronousFile( cache.join( ',' ) ); 
            clearTimeout( timer ); // 清空定时器 
            timer = null;
            cache.length = 0; // 清空 ID 集合
        }, 2000 ); 
    }// 2 秒后向本体发送需要同步的 ID 集合
})();

var checkbox = document.getElementsByTagName( 'input' );
   for ( var i = 0, c; c = checkbox[ i++ ]; ){
      c.onclick = function(){
      if ( this.checked === true ){
          proxySynchronousFile( this.id ); }
      }
};

观察者模式

观察者模式 它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。

模式特征

  1. 一个目标者对象,拥有方法:添加 / 删除 / 通知观察者;
  2. 多个观察者对象,拥有方法:接收目标对象状态变更通知并处理;
  3. 目标对象状态变更时,通知所有观察者。

目标者对象添加一系列 观察者, 目标者对象负责维护与这些 观察者 之间的联系,“你对我有兴趣,我更新就会通知你”。

代码实现

// 目标者类
class Subject {
  constructor() {
    this.observers = [];  // 观察者列表
  }
  // 添加
  add(observer) {
    this.observers.push(observer);
  }
  // 删除
  remove(observer) {
    let idx = this.observers.findIndex(item => item === observer);
    idx > -1 && this.observers.splice(idx, 1);
  }
  // 通知
  notify() {
    for (let observer of this.observers) {
      observer.update();
    }
  }
}

// 观察者类
class Observer {
  constructor(name) {
    this.name = name;
  }
  // 目标对象更新时触发的回调
  update() {
    console.log(`目标者通知我更新了,我是:${this.name}`);
  }
}

// 实例化目标者
let subject = new Subject();

// 实例化两个观察者
let obs1 = new Observer('前端开发者');
let obs2 = new Observer('后端开发者');

// 向目标者添加观察者
subject.add(obs1);
subject.add(obs2);

// 目标者通知更新
subject.notify();  
// 输出:
// 目标者通知我更新了,我是前端开发者
// 目标者通知我更新了,我是后端开发者

优点

  1. 目标者与观察者,功能耦合度降低,专注自身功能逻辑;
  2. 观察者被动接收更新,时间上解耦,实时接收目标者更新状态。

缺点

不能对事件通知进行细分管控,如 “筛选通知”,“指定主题事件通知” 。

比如上面的例子,仅通知 “前端开发者” ?观察者对象如何只接收自己需要的更新通知?上例中,两个观察者接收目标者状态变更通知后,都执行了 update(),并无区分。

这就引出我们的下一个模式。进阶版的观察者模式“发布订阅模式”,部分文章对两者是否一样都存在争议。

仅代表个人观点:两种模式很类似,但是还是略有不同,就是多了个第三者,因 JavaScript 非正规面向对象语言,且函数回调编程的特点,使得 “发布订阅模式” 在 JavaScript 中代码实现可等同为 “观察模式”。

发布订阅模式(Publisher && Subscriber)

发布订阅模式:基于一个事件(主题)通道,希望接收通知的对象 Subscriber 通过自定义事件订阅主题,被激活事件的对象 Publisher 通过发布主题事件的方式通知各个订阅该主题的 Subscriber 对象。

发布订阅模式观察者模式的不同,“第三者” (事件中心)出现。目标对象并不直接通知观察者,而是通过事件中心来派发通知。

代码实现

// 事件中心
let pubSub = {
  list: {},
  subscribe: function (key, fn) {   // 订阅
    if (!this.list[key]) {
      this.list[key] = [];
    }
    this.list[key].push(fn);
  },
  publish: function(key, ...arg) {  // 发布
    for(let fn of this.list[key]) {
      fn.call(this, ...arg);
    }
  },
  unSubscribe: function (key, fn) {     // 取消订阅
    let fnList = this.list[key];
    if (!fnList) return false;

    if (!fn) {
      // 不传入指定取消的订阅方法,则清空所有key下的订阅
      fnList && (fnList.length = 0);
    } else {
      fnList.forEach((item, index) => {
        if (item === fn) {
          fnList.splice(index, 1);
        }
      })
    }
  }
}

// 订阅
pubSub.subscribe('onwork', time => {
  console.log(`上班了:${time}`);
})
pubSub.subscribe('offwork', time => {
  console.log(`下班了:${time}`);
})
pubSub.subscribe('launch', time => {
  console.log(`吃饭了:${time}`);
})

// 发布
pubSub.publish('offwork', '18:00:00'); 
pubSub.publish('launch', '12:00:00');

// 取消订阅
pubSub.unSubscribe('onwork');

发布订阅模式中,订阅者各自实现不同的逻辑,且只接收自己对应的事件通知。实现你想要的 “不一样”。

DOM 事件监听也是 “发布订阅模式” 的应用:

let loginBtn = document.getElementById('#loginBtn');

// 监听回调函数(指定事件)
function notifyClick() {
    console.log('我被点击了');
}

// 添加事件监听
loginBtn.addEventListener('click', notifyClick);
// 触发点击, 事件中心派发指定事件
loginBtn.click();             

// 取消事件监听
loginBtn.removeEventListener('click', notifyClick);

发布订阅的通知顺序:

  1. 先订阅后发布时才通知(常规)
  2. 订阅后可获取过往以后的发布通知 (QQ离线消息,上线后获取之前的信息)

流行库的应用

  1. Vue 的双向数据绑定;
  2. Vue 的组件通信 $on/$emit
Vue 的双向数据绑定

Vue双向数据绑定

利用 Object.defineProperty() 对数据进行劫持,设置一个监听器 Observer,用来监听数据对象的属性,如果属性上发生变化了,交由 Dep 通知订阅者 Watcher 去更新数据,最后指令解析器 Compile 解析对应的指令,进而会执行对应的更新函数,从而更新视图,实现了双向绑定。

  1. Observer (数据劫持,监听数据变化)
  2. Dep (发布订阅)
  3. Watcher (订阅者,更新数据)
  4. Compile (模版编译)

关于 Vue 双向数据绑定原理,可自行参考其它文章,或推荐本篇 《 vue双向数据绑定原理》

Vue 的父子组件通信

Vue 中,父组件通过 props 向子组件传递数据(自上而下的单向数据流)。父子组件之间的通信,通过自定义事件即 $on , $emit 来实现(子组件 $emit,父组件 $on)。

原理其实就是 $emit 发布更新通知,而 $on 订阅接收通知。Vue 中还实现了 $once(一次监听),$off(取消订阅)。

// 订阅
vm.$on('test', function (msg) {
    console.log(msg)
})

// 发布
vm.$emit('test', 'hi')

优点

  1. 对象间功能解耦,弱化对象间的引用关系;
  2. 更细粒度地管控,分发指定订阅主题通知

缺点

  1. 对间间解耦后,代码阅读不够直观,不易维护;
  2. 额外对象创建,消耗时间和内存(很多设计模式的通病)

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

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

类似点

都是定义一个一对多的依赖关系,有关状态发生变更时执行相应的通知。

区别点

发布订阅模式更灵活,是进阶版的观察者模式,指定对应事件分发。

  1. 观察者模式维护单一事件对应多个依赖该事件的对象关系;
  2. 发布订阅维护多个事件(主题)及依赖各事件(主题)的对象之间的关系;
  3. 观察者模式是目标对象直接触发通知(全部通知),观察对象被迫接收通知。发布订阅模式多了个中间层(事件中心),由其去管理通知广播(只通知订阅对应事件的对象);
  4. 观察者模式对象间依赖关系较强,发布订阅模式中对象之间实现真正的解耦。

装饰者模式

装饰者模式 允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。

这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。

场景举例——实现警告

在使用第三方库的过程中,我们会时不时的在控制台遇见一些警告,这些警告用来提醒开发者所调用的方法会在下个版本中被弃用。这样的一行打印信息也许我们的常规做法是在方法内部添加一行代码即可,这样其实在源码阅读上并不友好,也不符合单一职责原则。如果在需要抛出警告的方法前面加一个 @deprecate的装饰器来实现警告,会友好得多。

下面我们来实现一个 @deprecate的装饰器,其实这类的装饰器也可以扩展成为打印日志装饰器 @log,上报信息装饰器 @fetchInfo等。

function deprecate(deprecatedObj) {
  return function(target, key, descriptor) {
    const deprecatedInfo = deprecatedObj.info;
    const deprecatedUrl = deprecatedObj.url;
    // 警告信息
    const txt = `DEPRECATION ${target.constructor.name}#${key}: ${deprecatedInfo}. ${deprecatedUrl ? 'See '+ deprecatedUrl + ' for more detail' : ''}`;
    
    return Object.assign({}, descriptor, {
      value: function value() {
        // 打印警告信息
        console.warn(txt);
        descriptor.value.apply(this, arguments);
      }
    })
  }
}

上面的deprecate函数接受一个对象参数,该参数分别有infourl两个键值,其中info填入警告信息,url为选填的详情网页地址。下面我们来为一个名为MyLib的库的deprecatedMethod方法添加该装饰器吧!

class MyLib {
  @deprecate({
    info: 'The methods will be deprecated in next version', 
    url: 'http://www.baidu.com'
  })
  deprecatedMethod(txt) {
    console.log(txt)
  }
}

const lib = new MyLib();
lib.deprecatedMethod('调用了一个要在下个版本被移除的方法');
// DEPRECATION MyLib#deprecatedMethod: The methods will be deprecated in next version. See http://www.baidu.com for more detail
// 调用了一个要在下个版本被移除的方法

总结

添加功能,装饰者和被装饰者之间松耦合,可维护性好

被装饰者可以使用装饰者动态地增加和撤销功能,可以在运行时选择不同的装饰器,实现不同的功能,灵活性好;

适配器模式

适配器模式 的作用是解决两个软件实体间的接口不兼容的问题。

场景举例

现在使用谷歌地图和百度地图在页面展现地图

var googleMap = { 
    show: function(){
        console.log( '开始渲染谷歌地图' ); 
    }
};
var baiduMap = { 
    show: function(){
        console.log( '开始渲染百度地图' ); 
    }
};
var renderMap = function( map ){
    if ( map.show instanceof Function ){
        map.show(); 
    }
};
renderMap( googleMap ); // 输出:开始渲染谷歌地图
renderMap( baiduMap ); // 输出:开始渲染百度地图

场景问题假设

这段程序得以顺利运行的关键是 googleMap 和 baiduMap 提供了一致的 show 方法,但第三方的 接口方法并不在我们自己的控制范围之内,假如 baiduMap 提供的显示地图的方法不叫 show 而叫 display 呢

baiduMap 这个对象来源于第三方,正常情况下我们都不应该去改动它。此时我们可以通过增 加 baiduMapAdapter 来解决问题

var googleMap = { 
    show: function(){
        console.log( '开始渲染谷歌地图' ); 
    }
};
var baiduMap = {
    display: function(){
        console.log( '开始渲染百度地图' ); 
    }
};
var baiduMapAdapter = { 
    show: function(){
        return baiduMap.display();
    } 
};
renderMap( googleMap ); // 输出:开始渲染谷歌地图
renderMap( baiduMapAdapter ); // 输出:开始渲染百度地图

总结

  • 适配器模式主要用来解决两个已有接口之间不匹配的问题,它不考虑这些接口是怎样实 现的,也不考虑它们将来可能会如何演化。适配器模式不需要改变已有的接口,就能够 使它们协同作用。
  • 装饰者模式和代理模式也不会改变原有对象的接口,但装饰者模式的作用是为了给对象 增加功能。装饰者模式常常形成一条长的装饰链,而适配器模式通常只包装一次。代理 模式是为了控制对对象的访问,通常也只包装一次。
  • 外观模式的作用倒是和适配器比较相似,有人把外观模式看成一组对象的适配器,但外 观模式最显著的特点是定义了一个新的接口。

中介者模式

中介者模式 的作用就是解除对象与对象之间的紧耦合关系。增加一个中介者对象后,所有的 相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知 中介者对象即可。中介者使各对象之间耦合松散,而且可以独立地改变它们之间的交互。中介者模式使网状的多对多关系变成了相对简单的一对多关系

用途:解除对象与对象之间的紧耦合关系

现实中的中介者模式:机场指挥塔就是中介者,它控制着飞机的起飞和降落,因为所有的沟通都是从飞机向塔台汇报来完成的,而不是飞机之前相互沟通。

使用场景:如果对象之间的复杂耦合确实导致调用和维护出现了困难,而且这些耦合度随项目的变化呈指数增长曲线,那我们就可以考虑用中介者模式来重构代码。

优点:中介者模式使各个对象之间得以解耦,以中介者和对象之间的一对多关系取代了对象之间的网状多对多关系。各个对象只需关注自身功能的实现,对象之间的交互关系交给了中介者对象来实现和维护。

缺点:系统中会新增一个中介者对象,因为对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者对象经常是巨大的。中介者对象自身往往就是一个难以维护的对象。

场景举例

假如在玩泡泡堂的游戏,使用泡泡击败对方所有玩家才能获得胜利。现在将队伍分成两个组进行游戏

代码实现(未使用中介者模式)

/*玩家*/
function Player( name, teamColor ){ 
    this.partners = []; // 队友列表 
    this.enemies = []; // 敌人列表 
    this.state = 'live'; // 玩家状态 
    this.name = name; // 角色名字 
    this.teamColor = teamColor; // 队伍颜色
};
Player.prototype.win = function(){ // 玩家团队胜利 
    console.log( 'winner: ' + this.name );
};
Player.prototype.lose = function(){ // 玩家团队失败
    console.log( 'loser: ' + this.name );
};

/*玩家死亡的方法*/
Player.prototype.die = function(){ // 玩家死亡 
    var all_dead = true;
    this.state = 'dead'; // 设置玩家状态为死亡
    for ( var i = 0, partner; partner = this.partners[ i++ ]; ){ // 遍历队友列表
        if ( partner.state !== 'dead' ){ // 如果还有一个队友没有死亡,则游戏还未失败
            all_dead = false; break;
        } 
    }
    if( all_dead === true ){// 如果队友全部死亡
        this.lose(); // 通知自己游戏失败
        for ( var i = 0, partner; partner = this.partners[ i++ ]; ){
            partner.lose(); 
        }
        for ( var i = 0, enemy; enemy = this.enemies[ i++ ]; ){ 
            enemy.win();
        } 
    }
};
/*定义一个工厂来创建玩家*/
var playerFactory = function( name, teamColor ){ 
    var newPlayer = new Player( name, teamColor );
    for ( var i = 0, player; player = players[ i++ ]; ){ // 通知所有的玩家,有新角色加入 
        if ( player.teamColor === newPlayer.teamColor ){ // 如果是同一队的玩家
            player.partners.push( newPlayer ); // 相互添加到队友列表
            newPlayer.partners.push( player ); 
        } else{
            player.enemies.push( newPlayer ); // 相互添加到敌人列表
            newPlayer.enemies.push( player ); 
        }
    }
    players.push( newPlayer );
    return newPlayer; 
};
/*创建玩家*/
//红队:
var player1 = playerFactory( '皮蛋', 'red' ),
    player2 = playerFactory( '小乖', 'red' ), 
    player3 = playerFactory( '宝宝', 'red' ), 
    player4 = playerFactory( '小强', 'red' );
//蓝队:
var player5 = playerFactory( '黑妞', 'blue' ),
    player6 = playerFactory( '葱头', 'blue' ), 
    player7 = playerFactory( '胖墩', 'blue' ), 
    player8 = playerFactory( '海盗', 'blue' );

让红队玩家全部死亡:
player1.die(); 
player2.die();
player4.die();  
player3.die();

执行结果

重构思路

如果玩家不止8个,有成百上千个。一个玩家如果掉线或者更换队伍,上面的代码完全无法解决。所以需要一个中间对象去管理每个玩家之间的状态与关系。如下图所示

代码重构(使用中介者模式)

  • 步骤

    • 定义Player构造函数和player对象的原型方法
    • 定义中介者对象playerDirector
    • 把操作转交给中介者对象
/*******************玩家死亡*****************/
Player.prototype.die = function(){
    this.state = 'dead';
    playerDirector.reciveMessage( 'playerDead', this );// 给中介者发送消息,玩家死亡
};
/*******************移除玩家*****************/
Player.prototype.remove = function(){ // 给中介者发送消息,移除一个玩家
    playerDirector.reciveMessage('removePlayer', this );
};
/*******************玩家换队*****************/
Player.prototype.changeTeam = function( color ){
    playerDirector.reciveMessage( 'changeTeam', this, color ); // 给中介者发送消息,玩家换队
};
/*******************定义中介者对象*****************/
var playerDirector= ( function(){
    var players = {}, // 保存所有玩家
    operations = {}; // 中介者可以执行的操作
    /****************新增一个玩家***************************/ 
    operations.addPlayer = function( player ){
        var teamColor = player.teamColor; // 玩家的队伍颜色
        players[ teamColor ] = players[ teamColor ] || []; // 如果该颜色的玩家还没有成立队伍,则新成立一个队伍 
        players[ teamColor ].push( player ); // 添加玩家进队伍
    };
    /****************移除一个玩家***************************/ 
    operations.removePlayer = function( player ){
        var teamColor = player.teamColor, // 玩家的队伍颜色
        teamPlayers = players[ teamColor ] || []; // 该队伍所有成员
        for ( var i = teamPlayers.length - 1; i >= 0; i-- ){ // 遍历删除
            if ( teamPlayers[ i ] === player ){ 
                teamPlayers.splice( i, 1 );
            } 
        }
    };
    /****************玩家换队***************************/ 
    operations.changeTeam = function( player, newTeamColor ){ // 玩家换队
        operations.removePlayer( player ); // 从原队伍中删除
        player.teamColor = newTeamColor; // 改变队伍颜色
        operations.addPlayer( player );// 增加到新队伍中
    };
    /****************玩家死亡***************************/ 
    operations.playerDead = function( player ){ // 玩家死亡
        var teamColor = player.teamColor,
        teamPlayers = players[ teamColor ]; // 玩家所在队伍
        var all_dead = true;
        for ( var i = 0, player; player = teamPlayers[ i++ ]; ){ 
            if ( player.state !== 'dead' ){
                all_dead = false;
                break; 
            }
        }
        if ( all_dead === true ){// 全部死亡
            for ( var i = 0, player; player = teamPlayers[ i++ ]; ){ 
                player.lose(); // 本队所有玩家 lose
            }
            for ( var color in players ){ 
                if ( color !== teamColor ){
                    var teamPlayers = players[ color ];  // 其他队伍的玩家
                    for( var i = 0, player; player = teamPlayers[ i++ ]; ){
                        player.win(); // 其他队伍所有玩家 win
                    } 
                }
            } 
        }
    };

    var reciveMessage = function() {
        var message = Array.prototype.shift.call( arguments ); 
        operations[ message ].apply( this, arguments );
    };
    return {
        reciveMessage: reciveMessage
    } 
})();
/*******************设置工厂函数*****************/
var playerFactory = function( name, teamColor ){
    var newPlayer = new Player( name, teamColor ); // 创造一个新的玩家对象     
    playerDirector.reciveMessage( 'addPlayer', newPlayer ); // 给中介者发送消息,新增玩家
    return newPlayer; 
};
//红队
var player1 = playerFactory('皮蛋', 'red' ), 
    player2 = playerFactory('小乖', 'red' ),
    player3 = playerFactory( '宝宝', 'red' ), 
    player4 = playerFactory('小强', 'red' );
// 蓝队:
var player5 = playerFactory('黑妞', 'blue' ),
    player6 = playerFactory( '葱头', 'blue' ),  
    player7 = playerFactory( '胖墩', 'blue' ),
    player8 = playerFactory( '海盗', 'blue' );
player1.die(); 
player2.die();
player3.die(); 
player4.die();

执行结果

现在可以随时的进行掉线或者换队操作,玩家之间的耦合基本上消失了。

总结

中介者模式是迎合迪米特法则的一种实现。迪米特法则也叫最少知识原则,是指一个对象应该尽可能少地了解另外的对象。如果对象之间的耦合性太高,一个对象 发生改变之后,难免会影响到其他的对象,而在中介者模式里,对象之间几乎不知道彼此的存在,它们只能通过中介者对象来互相影响对方。

优点 中介者模式使各个对象之间解耦,以中介者和对象之间的一对多关系取代了对象之间的网状多对多关系。各个对象只需关注自身功能的实现,对象之间的交互关系交给了中介者对象来实现和维护。

缺点

最大的缺点是系统中会新增一个中介者对象,因 为对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者对象经常是巨大的。中介 者对象自身往往就是一个难以维护的对象。

迭代器模式

迭代器模式 是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。

在 JavaScript 中,迭代器是一个对象,它定义一个序列,并在终止时可能返回一个返回值。 更具体地说,迭代器是通过使用 next() 方法实现 Iterator protocol 的任何一个对象,该方法返回具有两个属性的对象: value,这是序列中的 next 值,和 done ,如果已经迭代到序列中的最后一个值,则它为 true 。如果 value 和 done 一起存在,则它是迭代器的返回值。

常用迭代器

map()filter()for...of

  • 内部迭代器
var each = function( ary, callback ){
    for ( var i = 0, l = ary.length; i < l; i++ ){
         callback.call( ary[i], i, ary[ i ] );
    }
};
each( [ 1, 2, 3 ], function( i, n ){ 
    alert ( [ i, n ] );
});
  • 外部迭代器
var Iterator = function( obj ){ 
    var current = 0;
    var next = function(){ 
        current += 1;
    };
    var isDone = function(){
        return current >= obj.length;
    };
    var getCurrItem = function(){ 
        return obj[ current ];
    };
    return {
        next: next,
        isDone: isDone,
        getCurrItem: getCurrItem 
    };
}
var compare = function( iterator1, iterator2 ){
    while( !iterator1.isDone() && !iterator2.isDone() ){
    if ( iterator1.getCurrItem() !== iterator2.getCurrItem() ){ 
        throw new Error ( 'iterator1 和 iterator2 不相等' );
    } 
    iterator1.next(); 
    iterator2.next();
}
alert ( 'iterator1 和 iterator2 相等' ); }
var iterator1 = Iterator( [ 1, 2, 3 ] );
var iterator2 = Iterator( [ 1, 2, 3 ] );
compare( iterator1, iterator2 ); // 输出:iterator1 和 iterator2 相等
  • 中止迭代器
var each = function( ary, callback ){
    for ( var i = 0, l = ary.length; i < l; i++ ){
        if ( callback( i, ary[ i ] ) === false ){ 
            break;
        } 
    }
};
each( [ 1, 2, 3, 4, 5 ], function( i, n ){
    if ( n > 3 ){ 
        return false;
    }
    console.log( n );
});

命令模式

命令模式 是最简单和优雅的模式之一,命令模式中的命令(command)指的是一个执行某些特定事情的指令。

应用场景

    有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。

  • 菜单案例

    假设我们正在编写一个用户界面程序,该用户界面上至少有数十个 Button 按钮。因为项目比较复杂,所以我们决定让某个程序员负责绘制这些按钮,而另外一些程序员则负责编写点击按钮后的具体行为,这些行为都将被封装在对象里。

    在大型项目开发中,这是很正常的分工。对于绘制按钮的程序员来说,他完全不知道某个按钮未来将用来做什么,可能用来刷新菜单界面,也可能用来增加一些子菜单,他只知道点击这个 按钮会发生某些事情。那么当完成这个按钮的绘制之后,应该如何给它绑定onclick 事件呢?

    我们很快可以找到在这里运用命令模式的理由:点击了按钮之后,必须向某些负责具体行为的对象发送请求,这些对象就是请求的接收者。但是目前并不知道接收者是什么对象,也不知道接收者究竟会做什么。此时我们需要借助命令对象的帮助,以便解开按钮和负责具体行为对象之间的耦合。

  • 代码实现
 var RefreshMenuBarCommand = function( receiver ){
     return {
         execute: function(){ 
             receiver.refresh();
         }
     } 
 };
 var setCommand = function( button, command ){ 
     button.onclick = function(){
         command.execute(); 
     }
 };
 var refreshMenuBarCommand = RefreshMenuBarCommand( MenuBar );         
 setCommand( button1, refreshMenuBarCommand );
  • 撤销命令

    命令模式的作用不仅是封装运算块,而且可以很方便地给命令对象增加撤销操作。现在页面上有个小球,点击移动按钮和撤销按钮可以实现移动和撤销的操作。

  • 代码实现
var MoveCommand = function( receiver, pos ){ 
   this.receiver = receiver;
   this.pos = pos;
   this.oldPos = null;
};
MoveCommand.prototype.execute = function(){
   this.receiver.start( 'left', this.pos, 1000, 'strongEaseOut' );
   this.oldPos = this.receiver.dom.getBoundingClientRect([this.receiver.propertyName ];  // 记录小球开始移动前的位置
};
MoveCommand.prototype.undo = function(){
   this.receiver.start( 'left', this.oldPos, 1000, 'strongEaseOut' ); // 回到小球移动前记录的位置
};
var moveCommand;
moveBtn.onclick = function(){
   var animate = new Animate( ball );
   moveCommand = new MoveCommand( animate, pos.value );       
   moveCommand.execute();
};
cancelBtn.onclick = function(){ 
   moveCommand.undo();
};
  • 宏命令

    宏命令是一组命令的集合,通过执行宏命令的方式,可以一次执行一批命令。想象一下,家里有一个万能遥控器,每天回家的时候,只要按一个特别的按钮,它就会帮我们关上房间门,顺便打开电脑并登录 QQ。

  • 代码实现
var closeDoorCommand = { 
    execute: function(){
        console.log( '关门' ); 
    }
};
var openPcCommand = { 
    execute: function(){
        console.log( '开电脑' ); 
    }
};
var openQQCommand = { 
    execute: function(){
        console.log( '登录 QQ' ); 
    }
};
var MacroCommand = function(){ 
    return {
           commandsList: [],
           add: function( command ){
              this.commandsList.push( command ); 
           },
           execute: function(){
              for ( var i = 0, command; command = this.commandsList[ i++ ]; ){
                  command.execute(); 
              }
          } 
      }
};
var macroCommand = MacroCommand();

macroCommand.add( closeDoorCommand );  
macroCommand.add( openPcCommand );
macroCommand.add( openQQCommand );

macroCommand.execute();

状态模式

状态模式 允许一个对象在其内部状态改变时来改变它的行为。

场景举例

我们来想象这样一个场景:有一个电灯,电灯上面只有一个开关。当电灯开着的时候,此时 按下开关,电灯会切换到关闭状态;再按一次开关,电灯又将被打开。同一个开关按钮,在不同 的状态下,表现出来的行为是不一样的

代码实现(未使用状态模式)

var Light = function(){ 
    this.state = 'off'; // 给电灯设置初始状态 off
    this.button = null;// 电灯开关按钮
};
Light.prototype.init = function(){
    var button = document.createElement( 'button' ),
    self = this;
    button.innerHTML = '开关';
    this.button = document.body.appendChild( button ); 
    this.button.onclick = function(){
        self.buttonWasPressed(); 
    }
};
Light.prototype.buttonWasPressed = function(){ 
    if ( this.state === 'off' ){
        console.log( '开灯' );
        this.state = 'on';
    } else if ( this.state === 'on' ){
        console.log( '关灯' );
        this.state = 'off'; 
    }
};
var light = new Light(); 
light.init();

存在的问题: 假如现在电灯的状态多了一种,第一次按下打开弱光,第二次按下打开强光,第三次才是关闭电灯。以上的代码就无法满足这种电灯的情况

分析: 状态模式的关键是把事物的 每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部,所以 button 被按下的的时候,只需要在上下文中,把这个请求委托给当前的状态对象即可,该状态对象会负责渲染它自身的行为。如下图所示

重构思路

  • 定义 3 个状态类
  • 改写 Light 类,使用状态对象记录当前的状态
  • 提供一个 方法来切换 light 对象的状态
/******************** 定义 3 个状态类 ************************/
// OffLightState:
var OffLightState = function( light ){ 
    this.light = light;
};
OffLightState.prototype.buttonWasPressed = function(){ 
    console.log( '弱光' ); // offLightState 对应的行为 
    this.light.setState( this.light.weakLightState );// 切换状态到 weakLightState
};
// WeakLightState:
var WeakLightState = function( light ){ 
    this.light = light;
};
WeakLightState.prototype.buttonWasPressed = function(){ 
    console.log( '强光' ); // weakLightState 对应的行为 
    this.light.setState( this.light.strongLightState ); //切换状态到 strongLightState
};
// StrongLightState:
var StrongLightState = function( light ){ 
    this.light = light;
};
StrongLightState.prototype.buttonWasPressed = function(){
    console.log( '关灯' ); // strongLightState 对应的行为
    this.light.setState( this.light.offLightState ); // 切换状态到 offLightState
};
/******************* 改写 Light 类,使用状态对象记录当前的状态 ******************/
var Light = function(){
    this.offLightState = new OffLightState( this ); 
    this.weakLightState = new WeakLightState( this ); 
    this.strongLightState = new StrongLightState( this ); 
    this.button = null;
};
/******************** 提供一个 方法来切换 light 对象的状态 ************************/
Light.prototype.init = function(){
    var button = document.createElement( 'button' ),
    self = this;
    this.button = document.body.appendChild( button ); 
    this.button.innerHTML = '开关';
   this.currState = this.offLightState;
    this.button.onclick = function(){ 
        self.currState.buttonWasPressed();
    } 
};
Light.prototype.setState = function( newState ){
    this.currState = newState; 
};
var light = new Light(); 
light.init();

改进效果

执行结果跟之前的代码一致,但是使用状态模式的好处很明显,它可以使每 一种状态和它对应的行为之间的关系局部化,这些行为被分散和封装在各自对应的状态类之中, 便于阅读和管理代码。 另外,状态之间的切换都被分布在状态类内部,这使得我们无需编写过多的 if、else 条件 分支语言来控制状态之间的转换。

总结

  • 状态模式定义了状态与行为之间的关系,并将它们封装在一个类里。通过增加新的状态 类,很容易增加新的状态和转换。
  • 避免 Context 无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了 Context 中原本过 5 多的条件分支。
  • 用对象代替字符串来记录当前状态,使得状态的切换更加一目了然。
  • Context 中的请求动作和状态类中封装的行为可以非常容易地独立变化而互不影响

组合模式

组合模式 又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。

当我们想执行一个宏大的任务时,这个任务可以被细分为多层结构,组合模式可以让我们只发布一次执行命令,就能完成整个复杂的任务,屏蔽层级关系差异化问题

组合模式应树形结构而生,所以组合模式的使用场景就是出现树形结构的地方。

应用场景 —— 扫描文件夹

文件夹和文件之间的关系,非常适合用组合模式来描述。文件夹里既可以包含文件,又可以 包含其他文件夹,最终可能组合成一棵树 当使用用杀毒软件扫描该文件夹时,往往不会关心里面有多少文件和子文件夹,组合模式使得我们只需要操作最外层的文件夹进行扫描。

代码实现

/* Folder */ 
var Folder = function( name ){
    this.name = name;
    this.files = []; 
};
Folder.prototype.add= function( file ){ 
    this.files.push(file );
};
Folder.prototype.scan = function(){
    console.log( '开始扫描文件夹: ' + this.name );
    for ( var i = 0, file, files = this.files; file = files[ i++ ]; ){
        files file.scan();
    } 
};
/*File*/
var File = function( name ){
    this.name = name; 
};
File.prototype.add = function(){
    throw new Error( '文件下面不能再添加文件' );
};
File.prototype.scan = function(){
    console.log( '开始扫描文件: ' + this.name );
};
/*创建一些文件夹和文件对象, 并且让它们组合成一棵树,这棵树就是我们 F 盘里的 现有文件目录结构*/
var folder = new Folder( '学习资料' ); 
var folder1 = new Folder( 'JavaScript' ); 
var folder2 = new Folder ( 'jQuery' );
var file1 = new File( 'JavaScript 设计模式与开发实践' );
var file2 = new File( '精通 jQuery' );
var file3 = new File('重构与模式' );
folder1.add( file1 ); 
folder2.add( file2 );
folder.add( folder1 ); 
folder.add( folder2 ); 
folder.add( file3 );

/*现在的需求是把移动硬盘里的文件和文件夹都复制到这棵树中,假设我们已经得到了这些文件对象*/
var folder3 = new Folder( 'Nodejs' );
var file4 = new File( '深入浅出 Node.js' ); 
folder3.add( file4 );
var file5 = new File( 'JavaScript 语言精髓与编程实践' );

/*接下来就是把这些文件都添加到原有的树中*/
folder.add( folder3 ); 
folder.add( file5 );

小结

组合模式可以让我们使用树形方式创 建对象的结构。我们可以把相同的操作应用在组合对象和单个对象上。在大多数情况下,我们都 可以忽略掉组合对象和单个对象之间的差别,从而用一致的方式来处理它们。 然而,组合模式并不是完美的,它可能会产生一个这样的系统:系统中的每个对象看起来都 与其他对象差不多。它们的区别只有在运行的时候会才会显现出来,这会使代码难以理解。此外,如果通过组合模式创建了太多的对象,那么这些对象可能会让系统负担不起。

模板方法模式

模板方法模式 是一种只需使用继承就可以实现的非常简单的模式。模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。

抽象父类定义了子类需要重写的相关方法。并且这些方法,仍然是通过父类方法调用的。

根据描述,父类提供了“模板”并决定是否调用,子类进行具体实现。

场景举例

Animal是抽象类,DogCat分别具体实现了eat()sleep()方法。

DogCat实例可以通过live()方法调用eat()sleep()

注意CatDog实例会被自动添加live()方法。不暴露live()是为了防止live()被子类重写,保证父类的控制权

class Animal {
    constructor() {
        // this 指向实例
        this.live = () => {
            this.eat();
            this.sleep();
        };
    }

    eat() {
        throw new Error("模板类方法必须被重写");
    }

    sleep() {
        throw new Error("模板类方法必须被重写");
    }
}

class Dog extends Animal {
    constructor(...args) {
        super(...args);
    }
    eat() {
        console.log("狗吃粮");
    }
    sleep() {
        console.log("狗睡觉");
    }
}

class Cat extends Animal {
    constructor(...args) {
        super(...args);
    }
    eat() {
        console.log("猫吃粮");
    }
    sleep() {
        console.log("猫睡觉");
    }
}

/********* 以下为测试代码 ********/

// 此时, Animal中的this指向dog
let dog = new Dog();
dog.live();

// 此时, Animal中的this指向cat
let cat = new Cat();
cat.live();

总结

在传统的面向对象语 言中,一个运用了模板方法模式的程序中,子类的方法种类和执行顺序都是不变的,所以我们把这部分逻辑抽象到父类的模板方法里面。而子类的方法具体怎么实现则是可变的,于是我们把这 部分变化的逻辑封装到子类中。通过增加新的子类,我们便能给系统增加新的功能,并不需要改动抽象父类以及其他子类,这也是符合开放封闭原则的。

享元模式

享元(flyweight)模式 是一种用于性能优化的模式,“fly”在这里是苍蝇的意思,意为蝇量级。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。 如果系统中因为创建了大量类似的对象而导致内存占用过高,享元模式就非常有用了。在 JavaScript 中,浏览器特别是移动端的浏览器分配的内存并不算多,如何节省内存就成了一件非常有意义的事情。

场景举例

假设有个内衣工厂,目前的产品有 50 种男式内衣和 50 种女士内衣,为了推销产品,工厂决定生产一些塑料模特来穿上他们的内衣拍成广告照片。 正常情况下需要 50个男模特和50个女模特,然后让他们每人分别穿上一件内衣来拍照。

代码实现(未使用享元模式)

var Model = function( sex, underwear){
    this.sex = sex;
    this.underwear = underwear;
};
Model.prototype.takePhoto = function(){
    console.log( 'sex= ' + this.sex + ' underwear=' + this.underwear);
};
for ( var i = 1; i <= 50; i++ ){
    var maleModel = new Model( 'male', 'underwear' + i );     
    maleModel.takePhoto();
 };
for ( var j = 1; j <= 50; j++ ){
    var femaleModel= new Model( 'female', 'underwear' + j );
    femaleModel.takePhoto(); 
};

代码重构(享元模式)

/*只需要区别男女模特
那我们先把 underwear 参数从构造函数中 移除,构造函数只接收 sex 参数*/
var Model = function( sex ){ 
    this.sex = sex;
};
Model.prototype.takePhoto = function(){
    console.log( 'sex= ' + this.sex + ' underwear=' + this.underwear);
};
/*分别创建一个男模特对象和一个女模特对象*/
var maleModel = new Model( 'male' ), 
    femaleModel = new Model( 'female' );
/*给男模特依次穿上所有的男装,并进行拍照*/
for ( var i = 1; i <= 50; i++ ){ 
    maleModel.underwear = 'underwear' + i; 
    maleModel.takePhoto();
};
/*给女模特依次穿上所有的女装,并进行拍照*/
for ( var j = 1; j <= 50; j++ ){ 
    femaleModel.underwear = 'underwear' + j; 
    femaleModel.takePhoto();
};
//只需要两个对象便完成了同样的功能

如何使用享元模式

享元模式要求将对象的属性划分为内部状态与外部 状态(状态在这里通常指属性)。享元模式的目标是尽量减少共享对象的数量,关于如何划分内部状态和外部状态,下面的几条经验提供了一些指引

  • 内部状态存储于对象内部
  • 内部状态可以被一些对象共享
  • 内部状态独立于具体的场景,通常不会改变
  • 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享

在上面的例子中,性别是内部状态,内衣是外部状态,通过区分这两种状态,大大减少了系 统中的对象数量。通常来讲,内部状态有多少种组合,系统中便最多存在多少个对象,因为性别 通常只有男女两种,所以该内衣厂商最多只需要 2 个对象。

享元模式的替代方案 —— 对象池

对象池是另外一种性能优化方案,它跟享元模式有一些相似之处,但没有分离内部状态和外 部状态这个过程。对象池维护一个装载空闲对象的池子,如果需要对象的时候,不是直接 new,而是转从对象池里获取。如 果对象池里没有空闲对象,则创建一个新的对象,当获取出的对象完成它的职责之后, 再进入 池子等待被下次获取。

通用对象池代码实现

/*通用的对象池*/
var objectPoolFactory = function( createObjFn ){ 
    var objectPool = [];
    return {
        create: function(){
            var obj = objectPool.length === 0 ? createObjFn.apply( this, arguments ) : objectPool.shift();
            return obj; 
        },
        recover: function( obj ){ 
            objectPool.push( obj );
        }
    } 
};
var iframeFactory = objectPoolFactory( function(){ 
    var iframe = document.createElement( 'iframe' );
    document.body.appendChild( iframe );
    iframe.onload = function(){
        iframe.onload = null; // 防止 iframe 重复加载的 bug
        iframeFactory.recover( iframe );// iframe 加载完成之后回收节点
    }
    return iframe;
});
var iframe1 = iframeFactory.create(); 
iframe1.src = 'http:// baidu.com';
var iframe2 = iframeFactory.create(); 
iframe2.src = 'http:// QQ.com';
setTimeout(function(){
    var iframe3 = iframeFactory.create();
    iframe3.src = 'http:// 163.com'; 
}, 3000 );

职责链模式

职责链模式 的定义是:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。 职责链模式的名字非常形象,一系列可能会处理请求的对象被连接成一条链,请求在这些对 象之间依次传递,直到遇到一个可以处理它的对象,我们把这些对象称为链中的节点

场景举例

假设我们负责一个售卖手机的电商网站,经过分别交纳 500 元定金和 200 元定金的两轮预定后(订单已在此时生成),现在已经到了正式购买的阶段。 公司针对支付过定金的用户有一定的优惠政策。在正式购买后,已经支付过 500 元定金的用 户会收到 100 元的商城优惠券,200 元定金的用户可以收到 50 元的优惠券,而之前没有支付定金的用户只能进入普通购买模式,也就是没有优惠券,且在库存有限的情况下不一定保证能买到。

代码实现(未使用职责链模式)

var order = function( orderType, pay, stock ){
    if ( orderType === 1 ){ // 500 元定金购买模式
        if ( pay === true ){ // 已支付定金
            console.log( '500 元定金预购, 得到 100 优惠券' );
        } else{ // 未支付定金,降级到普通购买模式
            if ( stock > 0 ){ // 用于普通购买的手机还有库存
                console.log( '普通购买, 无优惠券' );
            }else{
                console.log( '手机库存不足' );
            } 
        }
    } else if ( orderType === 2 ){ 
        if ( pay === true ){ // 200 元定金购买模式
            console.log( '200 元定金预购, 得到 50 优惠券' ); 
        }else{
            if ( stock > 0 ){
                console.log( '普通购买, 无优惠券' );
            }else{
                console.log( '手机库存不足' );
            } 
        }
    } else if (orderType === 3) {
        if ( stock > 0 ){
            console.log( '普通购买, 无优惠券' ); 
        } else{
            console.log( '手机库存不足' ); 
        }
    } 
};
order( 1 , true, 500); // 输出: 500 元定金预购, 得到 100 优惠券

重构思路

现在我们采用职责链模式重构这段代码,先把 500 元订单、200 元订单以及普通购买分成 3 个函数。 接下来把 orderType、pay、stock 这 3 个字段当作参数传递给 500 元订单函数,如果该函数不符合处理条件,则把这个请求传递给后面的 200 元订单函数,如果 200 元订单函数依然不能处理该请求,则继续传递请求给普通购买函数。

代码重构(使用职责链模式)

var order500 = function( orderType, pay, stock ){ 
    if ( orderType === 1 && pay === true ){
        console.log( '500 元定金预购,得到 100 优惠券' ); 
    } else{
        return 'nextSuccessor'; // 我不知道下一个节点是谁,反正把请求往后面传递 
    }
};
var order200 = function( orderType, pay, stock ){ 
    if ( orderType === 2 && pay === true ){
        console.log( '200 元定金预购,得到 50 优惠券' ); 
    } else{
        return 'nextSuccessor'; // 我不知道下一个节点是谁,反正把请求往后面传递 
    }
};
var orderNormal = function( orderType, pay, stock ){
    if ( stock > 0 ){ 
        console.log( '普通购买,无优惠券' ); 
    } else{
        console.log( '手机库存不足' ); 
    }
};

// Chain.prototype.setNextSuccessor 指定在链中的下一个节点
// Chain.prototype.passRequest 传递请求给某个节点
var Chain = function( fn ){
    this.fn = fn;
    this.successor = null; 
};
Chain.prototype.setNextSuccessor = function( successor ){ 
    return this.successor = successor;
};
Chain.prototype.passRequest = function(){
    var ret = this.fn.apply( this, arguments );
    if ( ret === 'nextSuccessor' ){
        return this.successor && this.successor.passRequest.apply( this.successor, arguments );
    }
    return ret; 
};
var chainOrder500 = new Chain( order500 );
var chainOrder200 = new Chain( order200 );
var chainOrderNormal = new Chain( orderNormal );

chainOrder500.setNextSuccessor( chainOrder200 ); 
chainOrder200.setNextSuccessor( chainOrderNormal);

chainOrder500.passRequest( 1, true, 500 );   // 输出:500 元定金预购,得到 100 优惠券
chainOrder500.passRequest( 2, true, 500 );   // 输出:200 元定金预购,得到 50 优惠券
chainOrder500.passRequest( 3, true, 500 );   // 输出:普通购买,无优惠券
chainOrder500.passRequest( 1, false, 0 );    // 输出:手机库存不足

通过改进,我们可以自由灵活地增加、移除和修改链中的节点顺序,假如某天网站运营人员 又想出了支持 300 元定金购买,那我们就在该链中增加一个节点即可

var order300 = function(){
 // 具体实现略
};
chainOrder300= new Chain( order300 ); 
chainOrder500.setNextSuccessor( chainOrder300); chainOrder300.setNextSuccessor( chainOrder200);

异步的职责链

在现实开发中,我们经常会遇到一些异步的问题,比如我们要在 节点函数中发起一个 ajax 异步请求,异步请求返回的结果才能决定是否继续在职责链中 passRequest。 这时候让节点函数同步返回"nextSuccessor"已经没有意义了,所以要给 Chain 类再增加一个原型方法 Chain.prototype.next,表示手动传递请求给职责链中的下一个节点

Chain.prototype.next= function(){
    return this.successor && this.successor.passRequest.apply( this.successor, arguments );
};
/* 异步职责链 */
var fn1 = new Chain(function(){
   console.log( 1 );
   return 'nextSuccessor';
});
var fn2 = new Chain(function(){ 
    console.log( 2 );
    var self = this; 
    setTimeout(function(){
        self.next(); 
    }, 1000 );
});
var fn3 = new Chain(function(){
    console.log( 3 );
});
fn1.setNextSuccessor( fn2 ).setNextSuccessor( fn3 ); 
fn1.passRequest();

现在我们得到了一个特殊的链条,请求在链中的节点里传递,但节点有权利决定什么时候把 请求交给下一个节点。可以想象,异步的职责链加上命令模式(把 ajax 请求封装成命令对象),我们可以很方便地创建一个异步 ajax 队列库。

小结

在 JavaScript 开发中,职责链模式是最容易被忽视的模式之一。实际上只要运用得当,职责链模式可以很好地帮助我们管理代码,降低发起请求的对象和处理请求的对象之间的耦合性。职责链中的节点数量和顺序是可以自由变化的,我们可以在运行时决定链中包含哪些节点。