JS设计模式与开发实践

最近开始看曾探的《JavaScript设计模式与开发实践》一书,仅以此篇博客记录学习内容。


设计模式的定义是:在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案。通俗一点说,设计模式是在某种场合下针对某个问题的一种解决方案,给面向对象软件开发中的一些好的设计取个名字。

从某些角度来看,设计模式确实有可能带来代码量的增加,或许也会把系统的逻辑搞得更复杂。但软件开发的成本并非全部在开发阶段,设计模式的作用是让人们写出可复用可维护性高的程序。

一、基础知识

1、面向对象的JavaScript

1.1 动态类型语言
编程语言 静态类型语言 动态类型语言
区别 在编译时便已确定变量的类型 在程序运行的时候,待变量被赋予某个值之后,才会具有某种类型
优点 在编译时就能发现类型不匹配的错误,避免运行期间的某些错误 编写代码少,简洁
缺点 类型声明会增加更多的代码 无法保证变量的类型,在运行期间可能发生和类型相关的错误
举例 java、c++、c等 JavaScript
1.2 多态

多态的实际含义是:同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。换句话说,给不同的对象发送同一消息的时候,这些对象会根据这个消息分别给出不同的反馈。

//不同的动物接收到“叫”的命令后,发出不同的叫声
let makeSound = function(animal){
    if(animal instanceof Duck){
        console.log("嘎嘎嘎");
    }else if(animal instanceof Chicken){
        console.log("咯咯咯");
    }
};

let Duck = function(){};
let Chicken = function(){};

makeSound( new Duck() );  //嘎嘎嘎
makeSound( new Chicken() );  //咯咯咯

这段代码确实体现了“多态性”,当我们分别向鸭和鸡发出“叫唤”的消息时,它们根据此 消息作出了各自不同的反应。但这样的“多态性”是无法令人满意的,如果后来又增加了一只动 物,比如狗,显然狗的叫声是“汪汪汪”,此时我们必须得改动 makeSound 函数,才能让狗也发出 叫声。修改代码总是危险的,修改的地方越多,程序出错的可能性就越大,而且当动物的种类越 来越多时,makeSound 有可能变成一个巨大的函数。

多态背后的思想是将“做什么”和“谁去做以及怎么去做”分离开来,也就是将“不变的事物”与“可能改变的事物”分离开来。把不变的部分隔离出来,把可变的部分封装起来,这给予了我们扩展程序的能力,程序看起来是可生长的,也是符合开放-封闭原则的,相对于修改代码来说,仅仅增加代码就能完成同样的功能,这显然优雅和安全的多。依此对上面的代码进行修改:

//把不变的部分隔离,即所有动物都会发出叫声
let makeSound = function( animal ){
    animal.sound();
}

//把可变的地方各自封装
let Duck = function(){};

Duck.prototype.sound = function(){
    console.log('嘎嘎嘎');
};

let Chicken = function(){};

Chicken.prototype.sound = function(){
    console.log('咯咯咯');
};

makeSound( new Duck() );    //嘎嘎嘎
makeSound( new Chicken() );   //咯咯咯

//现在我们要听狗的叫声,可以仅增加代码,而不用改变makeSound函数
let Dog = function(){};
Dog.prototype.sound = function(){
    console.log('汪汪汪');
};
makeSound( new Dog() );   //汪汪汪

从前面的讲解可知,多态的思想实际上是把“做什么”和“谁去做”分离开来,要实现这一点,归根结底先要消除类型之间的耦合关系。如果类型之间的耦合关系没有被消除,那么我们在makeSound方法中指定了发出叫声的对象是某个类型,它就不可能再被替换为另外一个类型。而JavaScript的变量类型在运行期是可变的,JavaScript作为一门动态类型语言,它没有类型检查的过程,一个JavaScript对象,既可以表示Duck类型的对象,又可以表示Chicken类型的对象,这意味着JavaScript对象的多态性是与生俱来的。某一种动物能否发出叫声,只取决于它有没有makeSound方法,而不取决于它是否是某种类型的对象,这里不存在任何类型上的“类型耦合”。

1.3 封装

封装的目的是将信息隐藏。不仅包括封装数据和封装实现,还包括封装类型和封装变化。

1.3.1 封装数据

在许多语言的对象系统中,封装数据是由语法解析来实现的,这些语言也许提供了private、public、protected 等关键字来提供不同的访问权限。但 JavaScript 并没有提供对这些关键字的支持,我们只能依赖变量的作用域来实现封装特性,而且只能模拟出 public 和 private 这两种封装性。除了 ECMAScript 6 中提供的 let 之外,一般我们通过函数来创建作用域:

let myObject = (function(){
    let _name = 'sven';    //私有(private)变量
    return {
        getName: function(){    //公开(public)方法
            return _name;
        }   
    }
})();

console.log( myObject.getName() );    //输出:sven
console.log( myObject._name );    //输出:undefined

另外值得一提的是,在 ECAMScript 6 中,还可以通过 Symbol 创建私有属性。

1.3.2 封装实现

封装的目的是将信息隐藏,封装应该被视为“任何形式的封装”,也就是说,封装不仅仅是隐藏数据,还包括隐藏实现细节、设计细节以及隐藏对象的类型等。

从封装实现细节来讲,封装使得对象内部的变化对其他对象而言是透明的,也就是不可见的。对象对它自己的行为负责。其他对象或者用户都不关心它的内部实现。封装使得对象之间的耦合变松散,对象之间只通过暴露的 API 接口来通信。当我们修改一个对象时,可以随意地修改它的内部实现,只要对外的接口没有变化,就不会影响到程序的其他功能。

封装实现细节的例子非常之多。拿迭代器来说明,迭代器的作用是在不暴露一个聚合对象的内部表示的前提下,提供一种方式来顺序访问这个聚合对象。我们编写了一个 each 函数,它的作用就是遍历一个聚合对象,使用这个 each 函数的人不用关心它的内部是怎样实现的,只要它提供的功能正确便可以。即使 each 函数修改了内部源代码,只要对外的接口或者调用方式没有变化,用户就不用关心它内部实现的改变。

1.3.3 封装类型

封装类型是静态类型语言中一种重要的封装方式。一般而言,封装类型是通过抽象类和接口来进行的。把对象的真正类型隐藏在抽象类或者接口之后,相比对象的类型,客户更关心对象的行为。在许多静态语言的设计模式中,想方设法地去隐藏对象的类型,也是促使这些模式诞生的原因之一。比如工厂方法模式、组合模式等。

当然在 JavaScript 中,并没有对抽象类和接口的支持。JavaScript 本身也是一门类型模糊的语言。在封装类型方面,JavaScript 没有能力,也没有必要做得更多。对于JavaScript 的设计模式实现来说,不区分类型是一种失色,也可以说是一种解脱。在后面章节的学习中,我们可以慢慢了解这一点。

1.3.4 封装变化

从设计模式的角度出发,封装在更重要的层面体现为封装变化。

通过封装变化的方式,把系统中稳定不变的部分和容易变化的部分隔离开来,在系统的演变过程中,我们只需要替换那些容易变化的部分,如果这些部分是已经封装好的,替换起来也相对容易。这可以最大程度地保证程序的稳定性和可扩展性。

从《设计模式》副标题“可复用面向对象软件的基础”可以知道,这本书理应教我们如何编写可复用的面向对象程序。这本书把大多数笔墨都放在如何封装变化上面,这跟编写可复用的面向对象程序是不矛盾的。当我们想办法把程序中变化的部分封装好之后,剩下的即是稳定而可复用的部分了。

1.4 继承

JavaScript 就是使用原型 模式来搭建整个面向对象系统的。原型模式的实现关键,是语言本身是否提供了clone方法。ECMAScript 5提供了Object.create 方法,可以用来克隆对象。

let Plane = function(){
    this.blood = 100;
    this.attackLevel = 1;
    this.defenseLevel = 1;
};

let plane = new Plane();
plane.blood = 500;
plane.attackLevel = 10;
plane.defenseLevel = 7;

let clonePlane = Obiect.create(plane);
console.log(clonePlane);   //输出:

在 JavaScript语言中不存在类的概念(es6中已有,但只是语法糖),对象也并非从类中创建出来的,所有的 JavaScript对象都是从某个对象上克隆而来的。 JavaScript遵守原型编程的基本规则:

  • 所有的数据都是对象(所有对象都来源于Object.prototype这个根对象)
  • 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它
  • 对象会记住它的构造器的原型(通过 proto 这个属性)
  • 如果对象无法响应某个请求,它会把这个请求委托给它的构造器的原型

二、设计模式

1、单例模式

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

单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏 览器中的 window 对象等。

//使用代理实现单例模式
let CreateDiv = function(html){
    this.html = html;
    this.init();
};

CreateDiv.prototype.init = function(){
    let div = document.createElement('div');
    div.innerHTML = this.html;
    document.body.appendChild(div);
};

//接下来引入代理类proxySingletonCreateDiv:
let ProxySingletonCreateDiv = (function(){
    let instance;
    return function(html){
        if(!instance){
            instance = new CraeteDiv(html);
        }
        return instance;
    }
})();

let a = new ProxySingletonCreateDiv('sven1');
let b = new ProxySingletonCreateDiv('sven2');

console.log(a === b);   //输出:true

以上的单例模式的实现,更多的是接近传统面向对象语言中的实现,单例对象从 “类”中创建而来。

但 JavaScript 其实是一门无类(class-free)语言,也正因为如此,生搬单例模式的概念并无 意义。在 JavaScript 中创建对象的方法非常简单,既然我们只需要一个“唯一”的对象,为什 么要为它先创建一个“类”呢?这无异于穿棉衣洗澡,传统的单例模式实现在 JavaScript中并 不适用。 单例模式的核心是确保只有一个实例,并提供全局访问。

全局变量不是单例模式,但在 JavaScript开发中,我们经常会把全局变量当成单例来使用。但是全局变量存在很多问题,它很容易造成命名污染。 采用命名空间和闭包封装私有变量的方式可以相对降低全局变量带来的命名污染。

在 JavaScript开发中,单例模式的用途同样非常广泛。试想一下,当我 们单击登录按钮的时候,页面中会出现一个登录浮窗,而这个登录浮窗是唯一的,无论单击多少 次登录按钮,这个浮窗都只会被创建一次,那么这个登录浮窗就适合用单例模式来创建。 下面是一种通用的惰性单例的实现:


    <body>
        <button id="loginBtn">登录button>
    body>
    <script>
        //创建悬浮窗的函数
        let createLoginLayer = function(){
            let div = document.createElement('div');
            div.innerHTML = '我是登录浮窗';
            div.style.display = 'none';
            document.body.appendChild(div);
            return div;
        };

        //实现单例逻辑
        let getSingle = function(fn){
            let result;
            return function(){
                return result || (result = fn.apply(this, arguments));
            }
        };

        let createSingleLoginLayer = getSingle(createLoginLayer);

        document.getElementById('loginBtn').onclick = function(){
            let loginLayer = createSingleLoginLayer();
            loginLayer.style.display = 'block';
        };
    script>
html>

2、策略模式

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

策略模式有着广泛的应用。我们就以年终奖的计算为例进行介绍。 很多公司的年终奖是根据员工的工资基数和年底绩效情况来发放的。例如,绩效为S的人年 终奖有 4倍工资,绩效为 A的人年终奖有 3倍工资,而绩效为 B的人年终奖是 2倍工资。假设财 务部要求我们提供一段代码,来方便他们计算员工的年终奖。

//普通实现
let calculateBonus = function(performanceLevel, salary){
    if( performanceLevel === 'S'){
        return salary*4;
    }
    if(performanceLevel === 'A'){
        return salary*3;
    }
    if(performanceLevel === 'B'){
        return salary*2;
    }
};

console.log(calculateBonus('B', 20000));    //输出:40000
console.log(calculateBonus('S', 6000));    //输出: 24000

//上述算法缺点:calculateBonus函数比较庞大,包含太多if语句,如果新等级C,函数缺乏弹性,算法复用性差

//使用JavaScript版本的策略模式实现上述过程
let strategies = {
    "S": function(salary){
        return salary*4;
    },
    "A": function(salary){
        return salary*3;
    },
    "B": function(salary){
        return salary*2;
    }
};

let calculateBonus = function(level, salary){
    return strategies[level](salary);
};

console.log(calculateBonus("S", 20000));   //输出:80000
console.log(calculateBonus("A", 10000));   //输出:30000

//在 JavaScript语言的策略模式中,策略类往往被函数所代替,这时策略模式就 成为一种“隐形”的模式。
//JS去掉strategies的策略模式
let S = function(salary){
    return salary*4;
};

let A = function(salary){
    return salary*3;
};

let B = function(salary){
    return salary*2;
};

let calculateBonus = function(func, salary){
    return func(salary);
};

calculateBonus(S, 10000);  //输出:40000

策略模式是一种常用且有效的设计模式,例如缓动动画、表单校验中的validator类的应用,总结策略模式的一些优点:

  • 策略模式利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句。
  • 策略模式提供了对开放—封闭原则的完美支持,将算法封装在独立的 strategy 中,使得它 们易于切换,易于理解,易于扩展。
  • 策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制粘贴工作。
  • 在策略模式中利用组合和委托来让 Context(环境类,例如calculateBonus )拥有执行算法的能力,这也是继承的一种更轻 便的替代方案。

当然,策略模式也有一些缺点,但这些缺点并不严重。

  • 使用策略模式会在程序中增加许多策略类或者策略对象,但实际上这比把它们负责的 逻辑堆砌在 Context中要好。
  • 要使用策略模式,必须了解所有的 strategy,必须了解各个 strategy 之间的不同点, 这样才能选择一个合适的 strategy。比如,我们要选择一种合适的旅游出行路线,必须先了解选 择飞机、火车、自行车等方案的细节。此时 strategy 要向客户暴露它的所有实现,这是违反少 知识原则的。

3、代理模式

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

代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身 对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之 后,再把请求转交给本体对象。

代理分为保护代理和虚拟代理,保护代理用于控制不同权限的对象对目标对象的访问,但在 JavaScript并不容易实现保护代 理,因为我们无法判断谁访问了某个对象。而虚拟代理是常用的一种代理模式,我们主要讨论 的也是虚拟代理。

//不用代理的图片预加载
var MyImage = (function(){     
    var imgNode = document.createElement( 'img' );          
    document.body.appendChild( imgNode );
    var img = new Image; 

    img.onload = function(){        
         imgNode.src = img.src;     
     }; 

    return {         
        setSrc: function( src ){             
            imgNode.src = 'file:///C:/Users/svenzeng/Desktop/loading.gif';    
            img.src = src;                 
         }      
    } 
})(); 

MyImage.setSrc( 'http:// imgcache.qq.com/music/photo/k/000GGDys0yA0Nk.jpg' ); 

//虚拟代理实现图片预加载
/*
引入代理对象 proxyImage,通过这个代理对象,在图片被真正加载好之前,
页面中将出现一张占位的菊花图 loading.gif, 来提示用户图片正在加载。
*/

let myImage = (function(){
    let imgNode = document.createElement('img');
    document.body.appendChild(imgNode);

    return {
        setSrc: function(src){
            imgNode.src = src;
        }
    }
})();

let proxyImage = (function(){
    let img = new Image;
    img.onload = function(){
        myImage.setSrc(this.src);
    }
    return {
        setSrc: function(src){
            //比如在真正的图片加载好之前,先把 img 节点的 src 设置为 一张本地的 loading图片。
            myImage.setSrc('file:// /C:/Users/xxx/Desktop/loading.gif');
            img.src = src;
        }
    }
})();

proxyImage.setSrc('http://imgcache.qq.com/photo/xxx.jpg');

上述程序使用代理对象的好处,符合单一职责原则。单一职责原则指的是,就一个类(通常也包括对象和函数等)而言,应该仅有一个引起它变 化的原因。如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可 能会有多个。面向对象设计鼓励将行为分布到细粒度的对象之中,如果一个对象承担的职责过多, 等于把这些职责耦合到了一起,这种耦合会导致脆弱和低内聚的设计。当变化发生时,设计可能 会遭到意外的破坏。

纵观整个程序,我们并没有改变或者增加 MyImage 的接口,但是通过代理对象,实际上给系 统添加了新的行为。这是符合开放—封闭原则的。给 img 节点设置 src 和图片预加载这两个功能, 被隔离在两个对象里,它们可以各自变化而不影响对方。何况就算有一天我们不再需要预加载, 那么只需要改成请求本体而不是请求代理对象即可。

代理模式包括许多小分类,在 JavaScript开发中常用的是虚拟代理和缓存代理。还有防火墙代理、智能引用代理等。

4、迭代器模式

迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象 的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即 使不关心对象的内部构造,也可以按顺序访问其中的每个元素。 迭代器模式是一种相对简单的模式,简单到很多时候我们都不认为它是一种设计模式。目前 的绝大部分语言都内置了迭代器。

//根据不同的浏览器获取相应的上传组件对象
let getUploadObj = function(){
    try{
        return new ActiveXObject("TXFTNActiveX.FTNUpload"); //IE上传控件
    }catch(e){
        if(supportFlash()){
            let str = '';  //使用 Flash上传
            return $(str).appendTo($('body'));
        }else{
            let str =  '';  // 表单上传 
            return $(str).appendTo($('body'));
        }
    }
};
//上述代码的缺点:第一是很难阅读,第二是严重违反开闭原则。 

//使用迭代器模式的改进
let getActiveUploadObj = function(){
    try{
        return new ActiveXObject("TXFTNActiveX.FTNUpload");  //IE上传控件
    }catch(e){
        return false;
    }
};

let getFlashUploadObj = function(){
    if(supportFlash()){
        let str =  ''; 
        return $(str).appendTo($('body'));
    }
    return false;
};

let getFormUploadObj = function(){
    let str = '';  // 表单上传
    return $(str).appendTo($('body'));
};

//迭代器
let iteratorUploadObj = function(){
    for(let i=0, fn; fn = arguments[i++]; ){
        let uploadObj = fn();
        if(uploadObj !== false){
            return uploadObj;
        }
    }
};

let uploadObj = iteratorUploadObj(getActiveUploadObj, getFlashUploadObj, getFormUploadObj);

使用迭代器模式之后,我们可以看到,获取不同上传对象的方法被隔离在各自的函数里互不干扰, try、catch 和 if 分支不再纠缠在一起,使得我们可以很方便地的维护和扩展代码。比如,后来 我们又给上传项目增加了Webkit控件上传和 HTML5上传,我们要做的仅仅是增加上传对象的函数,并依照优先级把他们添加进迭代器。

5、发布—订阅 模式

发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状 态发生改变时,所有依赖于它的对象都将得到通知。在 JavaScript开发中,我们一般用事件模型 来替代传统的发布—订阅模式。

发布—订阅模式的优点:

  • 可以广泛应用于异步编程中,这是一种替代传递回调函数的方案。 比如,我们可以订阅 ajax请求的 error、succ 等事件。 或者如果想在动画的每一帧完成之后做一 些事情,那我们可以订阅一个事件,然后在动画的每一帧完成之后发布这个事件。在异步编程中 使用发布—订阅模式,我们就无需过多关注对象在异步运行期间的内部状态,而只需要订阅感兴 趣的事件发生点。
  • 可以取代对象之间硬编码的通知机制,一个对象不用再显式地调 用另外一个对象的某个接口。发布—订阅模式让两个对象松耦合地联系在一起,虽然不太清楚彼 此的细节,但这不影响它们之间相互通信。当有新的订阅者出现时,发布者的代码不需要任何修 改;同样发布者需要改变时,也不会影响到之前的订阅者。只要之前约定的事件名没有变化,就 可以自由地改变它们。

如果看过Vue的源码,了解了Vue中数据双向绑定的原理,其中的Object.definePrototype 就利用了发布—订阅模式。

6、命令模式

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

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

拿订餐来说,客人需要向厨师发送请求,但是完全不知道这些厨师的名字和联系方式,也不 知道厨师炒菜的方式和步骤。 命令模式把客人订餐的请求封装成 command 对象,也就是订餐中的 订单对象。这个对象可以在程序中被四处传递,就像订单可以从服务员手中传到厨师的手中。这 样一来,客人不需要知道厨师的名字,从而解开了请求调用者和请求接收者之间的耦合关系。

另外,相对于过程化的请求调用,command 对象拥有更长的生命周期。对象的生命周期是跟 初始请求无关的,因为这个请求已经被封装在了 command 对象的方法中,成为了这个对象的行为。 我们可以在程序运行的任意时刻去调用这个方法,就像厨师可以在客人预定 1个小时之后才帮他 炒菜,相当于程序在 1个小时之后才开始执行 command 对象的方法。

除了这两点之外,命令模式还支持撤销、排队等操作。

举例:让某个程序员负责绘制这些按钮,而另外一些程序员则负责编写点击按钮后的具体行为,这些行为都将被封装在对象里。

//利用闭包实现的命令模式

    <button id="button1">点击按钮1button>
    <button id="button2">点击按钮2button>
body>

<script>
    let button1 = document.getElementById("button1");
    let button2 = document.getElementById("button2");

    //setCommand函数负责在按钮上安装命令
    let setCommand = function(button, command){
        button.onclick = function(){
            command.execute();
        }
    };

    //点击按钮后的具体行为
    let MenuBar = {
        refresh: function(){
            console.log('刷新菜单目录');
        }
    };

    //把命令封装进命令类
    let RefreshMenuBarCommand = function(receiver){
        return {
            execute: function(){
                receiver.refresh();
            }
        }
    };

    //执行命令
    let refreshMenuBarCommand = RefreshMenuBarCommand(MenuBar);
    setCommand(button1, refreshMenuBarCommand);
script>

跟许多其他语言不同,JavaScript 可以用高阶函数非常方便地实现命令模式。命令模式在 JavaScript 语言中是一种隐形的模式。

7、组合模式

组合模式将对象组合成树形结构,以表示“部分—整体”的层次结构。 除了用来表示树形结构之外,组合模式的另一个好处是通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性,下面分别说明。

  • 表示树形结构。通过宏命令,我们很容易找到组合模式的一个优点:提供了一种遍历树形结构的方案,通过调用组合对象的 execute 方法,程序会递归调用组合对象下面的叶对象的 execute 方法,所以我们的万能遥控器只需要一次操作,便能依次完成关门、打开电脑、登录 QQ 这几件事情。组合模式可以非常方便地描述对象部分—整体层次结构。
  • 利用对象多态性统一对待组合对象和单个对象。利用对象的多态性表现,可以使客户端忽略组合对象和单个对象的不同。在组合模式中,客户将统一地使用组合结构中的所有对象,而不需要关心它究竟是组合对象还是单个对象。

8、模板方法模式

模板方法模式是一种只需使用继承就可以实现的非常简单的模式。

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

假如我们有一些平行的子类,各个子类之间有一些相同的行为,也有一些不同的行为。如果相同和不同的行为都混合在各个子类的实现中,说明这些相同的行为会在各个子类中重复出现。但实际上,相同的行为可以被搬移到另外一个单一的地方,模板方法模式就是为解决这个问题而生的。在模板方法模式中,子类实现中的相同部分被上移到父类中,而将不同的部分留待子类来实现。这也很好地体现了泛化的思想。

//例如将泡咖啡和泡茶引用模板方法模式,在 JavaScript 中,
//我们很多时候都不需要依样画瓢地去实现一个模版方法模式,高阶函数
是更好的选择。

//Beverage高阶函数可以达到和继承一样的效果
let Beverage = function(param){
    //相同方法公用,不同方法重写
    let boilWater = function(){
        console.log("把水煮沸");
    };

    let brew = param.brew || function(){
        throw new Error('必须传递brew方法');
    };

    let pourInCup = param.pourInCup || function(){
        throw new Error('必须传递pourInCup方法');
    };

    let addCondiments = param.addCondiments || function(){
        throw new Error('必须传递addCondiments 方法');
    };

    let F = function(){};

    F.prototype.init = function(){
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    };

    return F;
};

let Coffee = Beverage({
    brew: function(){
        console.log("用沸水冲泡咖啡");
    },
    pourInCup: function(){
        console.log("把咖啡倒进杯子");
    },
    addCondiments: function(){
        console.log("加糖和牛奶");
    }
});

let Tea = Beverage({
    brew: function(){
        console.log("用沸水浸泡茶叶");
    },
    pourInCup: function(){
        console.log("把茶倒进杯子");
    },
    addCondiments: function(){
        console.log("加柠檬");
    }
});

let coffee = new Coffee();
coffee.init();

let tea = new Tea();
tea.init();

9、享元模式

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

使用享元模式的关键是如何区别内部状态和外部状态。可以被对象共享的属性通常被划分为内部状态。而外部状态取决于具体的场景,并根据场景而变化,它们不能被一些对象共享,因此只能被划分为外部状态。

享元模式是一种很好的性能优化方案,但它也会带来一些复杂性的问题,使用了享元模式之后,我们需要分别多维护一个共享对象和一个给共享对象增加外部状态的对象,在大部分不必要使用享元模式的环境下,这些开销是可以避免的。

享元模式带来的好处很大程度上取决于如何使用以及何时使用,一般来说,以下情况发生时便可以使用享元模式。

  • 一个程序中使用了大量的相似对象。
  • 由于使用了大量对象,造成很大的内存开销。
  • 对象的大多数状态都可以变为外部状态。
  • 剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象。

10、职责链模式

职责链模式的定义是:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。

职责链模式的名字非常形象,一系列可能会处理请求的对象被连接成一条链,请求在这些对象之间依次传递,直到遇到一个可以处理它的对象,我们把这些对象称为链中的节点。

//用职责链模式来获取文件上传对象
let getActiveUploadObj = function(){
    try{
        return new ActiveXObject("TXFTNActiveX.FTNUpload"); //IE上传控件
    }catch(e){
        return "nextSuccessor";
    }
};

let getFlashUploadObj = function(){
    if(supportFlash()){
        let str = '';
        return $(str).appendTo($('body'));
    }
    return "nextSuccessor";
};

let getFormUploadObj = function(){
    return $('
'
).appendTo($('body')); }; //使用after函数,使得第一个函数返回'nextSuccessor'时,将请求继续传递给下一个函数 Function.prototype.after = function(fn){ let self = this; return function(){ let ret = self.apply(this, arguments); if(ret === 'nextSuccessor'){ return fn.apply(this, arguments); } return ret; } }; let getUploadObj = getActiveUploadObj.after(getFlashUploadObj ).after( getFormUpladObj ); console.log( getUploadObj() );

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

无论是作用域链、原型链,还是 DOM 节点中的事件冒泡,我们都能从中找到职责链模式的影子。职责链模式还可以和组合模式结合在一起,用来连接部件和父部件,或是提高组合对象的效率。

11、中介者模式

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

中介者模式是迎合迪米特法则的一种实现。迪米特法则也叫最少知识原则,是指一个对象应该尽可能少地了解另外的对象(类似不和陌生人说话)。如果对象之间的耦合性太高,一个对象发生改变之后,难免会影响到其他的对象,跟“城门失火,殃及池鱼”的道理是一样的。而在中介者模式里,对象之间几乎不知道彼此的存在,它们只能通过中介者对象来互相影响对方。

12、装饰者模式

给对象动态地增加职责的方式称为装饰者(decorator)模式。装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。跟继承相比,装饰者是一种更轻便灵活的做法,这是一种“即用即付”的方式,比如天冷了就多穿一件外套,需要飞行时就在头上插一支竹蜻蜓。

JavaScript 语言动态改变对象相当容易,我们可以直接改写对象或者对象的某个方法,并不需要使用“类”来实现装饰者模式。

装饰者模式和代理模式的结构看起来非常相像,这两种模式都描述了怎样为对象提供一定程度上的间接引用,它们的实现部分都保留了对另外一个对象的引用,并且向那个对象发送请求。

代理模式和装饰者模式最重要的区别在于它们的意图和设计目的。代理模式的目的是,当直接访问本体不方便或者不符合需要时,为这个本体提供一个替代者。本体定义了关键功能,而代理提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情。装饰者模式的作用就是为对象动态加入行为。换句话说,代理模式强调一种关系(Proxy 与它的实体之间的关系),这种关系可以静态的表达,也就是说,这种关系在一开始就可以被确定。而装饰者模式用于一开始不能确定对象的全部功能时。代理模式通常只有一层代理—本体的引用,而装饰者模式经常会形成一条长长的装饰链。

13、状态模式

状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变

状态模式的优点如下。

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

状态模式的缺点是会在系统中定义许多状态类,编写 20 个状态类是一项枯燥乏味的工作,而且系统中会因此而增加不少对象。另外,由于逻辑分散在状态类中,虽然避开了不受欢迎的条件分支语句,但也造成了逻辑分散的问题,我们无法在一个地方就看出整个状态机的逻辑。

策略模式和状态模式的相同点是,它们都有一个上下文、一些策略或者状态类,上下文把请求委托给这些类来执行。它们之间的区别是策略模式中的各个策略类之间是平等又平行的,它们之间没有任何联系,所以客户必须熟知这些策略类的作用,以便客户可以随时主动切换算法;而在状态模式中,状态和状态对应的行为是早已被封装好的,状态之间的切换也早被规定成,“改变行为”这件事情发生在状态模式内部。对客户来说,并不需要了解这些细节。这正是状态模式的作用所在。

14、适配器模式

适配器模式的作用是解决两个软件实体间的接口不兼容的问题。使用适配器模式之后,原本由于接口不兼容而不能工作的两个软件实体可以一起工作。

适配器的别名是包装器(wrapper),这是一个相对简单的模式。在程序开发中有许多这样的场景:当我们试图调用模块或者对象的某个接口时,却发现这个接口的格式并不符合目前的需求。这时候有两种解决办法,第一种是修改原来的接口实现,但如果原来的模块很复杂,或者我们拿到的模块是一段别人编写的经过压缩的代码,修改原接口就显得不太现实了。第二种办法是创建一个适配器,将原接口转换为客户希望的另一个接口,客户只需要和适配器打交道。

三、JavaScript 开发的设计原则

原则 定义 应用
单一职责原则(SRP) 一个对象(方法)只做一件事情,提高复用性 代理模式、迭代器模式、装饰者模式等
最少知识原则(LKP) 一个软件实体应当尽可能少地与其他实体发生相互作用,减少对象之间的交互 中介者模式、外观模式等
开放—封闭原则(OCP) 软件实体(类、模块、函数)等应该是可以扩展的,但是不可修改。 发布—订阅模式、模板方法模式等

有些模式没时间详细记录,以后补吧!

你可能感兴趣的:(web知识点详解)