Skip to content

设计模式

源代码下载地址

1、单例模式

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

  • 线程池、全局缓存、浏览器中的window对象

  • 登录浮窗

    • 当我们单击登录按钮的时候,页面中会出现一个登录浮窗,
    • 而这个登录浮窗是唯一的,无论单击多少次登录按钮,这个浮窗都只会被创建一次,
    • 那么这个登录浮窗就适合用单例模式来创建。
  • code

    • test1-1/test1-2
      • 不足(不透明)
        • 我们通过Singleton.getInstance来获取Singleton类的唯一对象,这种方式相对简单,
        • 但有一个问题,就是增加了这个类的“不透明性”,
        • Singleton类的使用者必须知道这是一个单例类,跟以往通过new XXX的方式来获取对象不同,
        • 这里偏要使用Singleton.getInstancevar a = Singleton.getInstance("sven1");来获取对象。
      • 本质
        • 是访问的同一变量,
        • 第一次访问设置值,
        • 再次访问返回上一次设置的值
    • test2
      • 优点:完成了一个透明的单例类的编写,但它同样有一些缺点。
      • 缺点:
        • 1、为了把instance封装起来,我们使用了自执行的匿名函数和闭包,并且让这个匿名函数返回真正的Singleton构造方法,这增加了一些程序的复杂度,阅读起来也不是很舒服。
        • 2、CreateDiv的构造函数实际上负责了两件事情。第一是创建对象和执行初始化init方法,第二是保证只有一个对象
        • 3、假设我们某天需要利用这个类,在页面中创建千千万万的div,即要让这个类从单例类变成一个普通的可产生多个实例的类,把控制创建唯一对象的那一段去掉if (instance) {return instance;},这种修改会给我们带来不必要的烦恼。
          js
            var CreateDiv = (function () {
              var instance;
              var CreateDiv = function (html) {
                if (instance) {
                  return instance;
                }
                this.html = html;
                this.init();
                return (instance = this);
              };
              CreateDiv.prototype.init = function () {
                var div = document.createElement("div");
                div.innerHTML = this.html;
                document.body.appendChild(div);
              };
              return CreateDiv;
            })();
      • test3
        • 优点
          • 引入代理
          • 把负责管理单例的逻辑移到了代理类proxySingletonCreateDiv中。
          • CreateDiv就变成了一个普通的类,
          • 它跟proxySingletonCreateDiv组合起来可以达到单例模式的效果。
      • test4
        • 重新分析:
          • 1、不适用
            • test3的做法:在传统面向对象语言,很自然的做法
            • JavaScript其实是一门无类(class-free)语言,生搬单例模式的概念并无意义
            • 在JavaScript中创建对象的方法非常简单,既然我们只需要一个“唯一”的对象,为什么要为它先创建一个“类”呢?
            • 这无异于穿棉衣洗澡,传统的单例模式实现在JavaScript中并不适用
            • 但是如果使用es6开发还是适用的
          • 2、本质/核心
            • 单例模式的核心是确保只有一个实例,并提供全局访问
            • 全局变量
              • 似乎是个不错的选择
              • 全局变量存在很多问题,它很容易造成命名空间污染
            • 其他选择
              • 使用命名空间
                • 适当地使用命名空间,并不会杜绝全局变量,但可以减少全局变量的数量。
                • 最简单的方法依然是用对象字面量的方式:
                • 动态地创建命名空间,即动态的创建对象属性
              • 使用闭包封装私有变量
                • 这种方法把一些变量封装在闭包的内部,只暴露一些接口跟外界通信:
      • test5
        • 惰性单例
          • 惰性单例指的是在需要的时候才创建对象实例
        • test5-1 第一种解决方案
          • 思路
            • 是在页面加载完成的时候便创建好这个div浮窗,
            • 这个浮窗一开始肯定是隐藏状态的,
            • 当用户点击登录按钮的时候,它才开始显示:
          • 不足
            • 也许我们进入WebQQ只是玩玩游戏或者看看天气,根本不需要进行登录操作,
            • 因为登录浮窗总是一开始就被创建好,那么很有可能将白白浪费一些DOM节点。
        • test5-2 第二种解决方案
          • 不足
            • 现在达到了惰性的目的,但失去了单例的效果
            • 当我们每次点击登录按钮的时候,都会创建一个新的登录浮窗div。
            • 虽然我们可以在点击浮窗上的关闭按钮时(此处未实现)把这个浮窗从页面中删除掉,
            • 但这样频繁地创建和删除节点明显是不合理的,也是不必要的。
        • test5-3 第三种解决方案
          • 完成了一个可用的惰性单例
          • 不足
            • 这段代码仍然是违反单一职责原则的,创建对象和管理单例的逻辑都放在createLoginLayer对象内部
            • 如果下次需要创建页面中唯一的iframe,或者script标签,用来跨域请求数据,就必须得如法炮制,把createLoginLayer函数几乎照抄一遍:
        • test5-4 (最终)
          • 1、把不变的部分隔离出来
            • 先不考虑创建一个div和创建一个iframe有多少差异
            • 管理单例的逻辑其实是完全可以抽象出来的,这个逻辑始终是一样的
              • 用一个变量来标志是否创建过对象,
              • 如果是,则在下次直接返回这个已经创建好的对象:
              js
                  var obj;
                  if ( ! obj ){
                      obj = xxx;
                  }
          • 2、成品
            • 把如何管理单例的逻辑从原来的代码中抽离出来,这些逻辑被封装在getSingle函数内部,创建对象的方法fn被当成参数动态传入getSingle函数
            js
              var getSingle = function( fn ){
                  var result;
                  return function(){
                    return result || ( result = fn .apply(this, arguments ) );
                  }
              };
            • 将用于创建登录浮窗的方法用参数fn的形式传入getSingle,我们不仅可以传入createLoginLayer,还能传入createScript、createIframe、createXhr等。
            • 之后再让getSingle返回一个新的函数,并且用一个变量result来保存fn的计算结果。
            • result变量因为身在闭包中,它永远不会被销毁。
            • 在将来的请求中,如果result已经被赋值,那么它将返回这个值
            js
                var createLoginLayer = function(){
                    var div = document.createElement( 'div' );
                    div.innerHTML = ’我是登录浮窗’;
                    div.style.display = 'none';
                    document.body.appendChild( div );
                    return div;
                };
                var createSingleLoginLayer = getSingle( createLoginLayer );
                document.getElementById( 'loginBtn' ).onclick = function(){
                    var loginLayer = createSingleLoginLayer();
                    loginLayer.style.display = 'block';
                };
          • 3、最终
            • 我们把创建实例对象的职责和管理单例的职责分别放置在两个方法里,
            • 这两个方法可以独立变化而互不影响,
            • 当它们连接在一起的时候,就完成了创建唯一实例对象的功能,看起来是一件挺奇妙的事情。
        • test5-5 (最终)
          • 这种单例模式的用途远不止创建对象,
          • jQuery选择给节点绑定one事件:
          • 模拟one实现只绑定一次事件

2、策略模式

js
// 第一步
    var performanceS = function(){};
    performanceS.prototype.calculate = function( salary ){
        return salary * 4;
    };
    var performanceA = function(){};
    performanceA.prototype.calculate = function( salary ){
        return salary * 3;
    };
    var performanceB = function(){};
    performanceB.prototype.calculate = function( salary ){
        return salary * 2;
    };
    var Bonus = function(){
        this.salary = null;      // 原始工资
        this.strategy = null;    // 绩效等级对应的策略对象
    };
    Bonus.prototype.setSalary = function( salary ){
        this.salary = salary;    // 设置员工的原始工资
    };
    Bonus.prototype.setStrategy = function( strategy ){
        this.strategy = strategy;    // 设置员工绩效等级对应的策略对象
    };
    Bonus.prototype.getBonus = function(){    // 取得奖金数额
        return this.strategy.calculate( this.salary );    // 把计算奖金的操作委托给对应的策略对象
    };
    // 段代码是基于传统面向对象语言的模仿
// 第二步
    // 所有跟计算奖金有关的逻辑不再放在Context中,而是分布在各个策略对象中
    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
// 第三步 小球动起来
    <body>
        <div style="position:absolute; background:blue" id="div">我是div</div>
    </body>
    var tween = {
        linear: function( t, b, c, d ){
            return c*t/d + b;
        },
        easeIn: function( t, b, c, d ){
            return c * ( t /= d ) * t + b;
        },
        strongEaseIn: function(t, b, c, d){
            return c * ( t /= d ) * t * t * t * t + b;
        },
        strongEaseOut: function(t, b, c, d){
            return c * ( ( t = t / d -1) * t * t * t * t + 1 ) + b;
        },
        sineaseIn: function( t, b, c, d ){
            return c * ( t /= d) * t * t + b;
        },
        sineaseOut: function(t, b, c, d){
            return c * ( ( t = t / d -1) * t * t + 1 ) + b;
        }
    };
    var Animate = function( dom ){
        this.dom = dom;                   // 进行运动的dom节点
        this.startTime = 0;               // 动画开始时间
        this.startPos = 0;                // 动画开始时,dom节点的位置,即dom的初始位置
        this.endPos = 0;                  // 动画结束时,dom节点的位置,即dom的目标位置
        this.propertyName = null;         // dom节点需要被改变的css属性名
        this.easing = null;               // 缓动算法
        this.duration = null;             // 动画持续时间
    };
    Animate.prototype.start = function( propertyName, endPos, duration, easing ){
        this.startTime = +new Date;        // 动画启动时间
        this.startPos = this.dom.getBoundingClientRect()[ propertyName ];  // dom节点初始位置
        this.propertyName = propertyName;  // dom节点需要被改变的CSS属性名
        this.endPos = endPos;  // dom节点目标位置
        this.duration = duration;   // 动画持续时间
        this.easing = tween[ easing ];  // 缓动算法
        var self = this;
        var timeId = setInterval(function(){      // 启动定时器,开始执行动画
            if ( self.step() === false ){           // 如果动画已结束,则清除定时器
                clearInterval( timeId );
            }
        }, 19 );
    };
    Animate.prototype.step = function(){
        var t = +new Date;        // 取得当前时间
        if ( t >= this.startTime + this.duration ){       // (1)
            this.update( this.endPos );   // 更新小球的CSS属性值
            return false;
        }
        var pos = this.easing( t - this.startTime, this.startPos,
            this.endPos - this.startPos, this.duration );
        // pos为小球当前位置
        this.update( pos );    // 更新小球的CSS属性值
    };
    Animate.prototype.update = function( pos ){
        this.dom.style[ this.propertyName ] = pos + 'px';
    };                                                                                     
    var div = document.getElementById( 'div' );
    var animate = new Animate( div );
    animate.start( 'left', 500, 1000, 'strongEaseOut' );
    // animate.start( 'top', 1500, 500, 'strongEaseIn' );
// 第四步
    // <form action="http:// xxx.com/register" id="registerForm" method="post">
	// 	请输入用户名:<input type="text" name="userName"/ >
	// 	请输入密码:<input type="text" name="password"/ >
	// 	请输入手机号码:<input type="text" name="phoneNumber"/ >
	// 	<button>提交</button>
	// </form>
    /***********************策略对象**************************/
    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;
            }
        }
    };
    /***********************Validator 类**************************/
    var Validator = function(){
        this.cache = [];
    };
    Validator.prototype.add = function( dom, rules ){
        var self = this;
        for ( var i = 0, rule; rule = rules[ i++ ]; ){
            (function( rule ){
                var strategyAry = rule.strategy.split( ':' );
                var errorMsg = rule.errorMsg;
                self.cache.push(function(){
                    var strategy = strategyAry.shift();
                    strategyAry.unshift( dom.value );
                    strategyAry.push( errorMsg );
                    return strategies[ strategy ].apply( dom, strategyAry );
                });
            })( rule )
        }
    };
    Validator.prototype.start = function(){
        for ( var i = 0, validatorFunc; validatorFunc = this.cache[ i++ ]; ){
            var errorMsg = validatorFunc();
            if ( errorMsg ){
                return errorMsg;
            }
        }
    };
    /***********************客户调用代码**************************/
    var registerForm = document.getElementById( 'registerForm' );
    var validataFunc = function(){
        var validator = new Validator();
        validator.add( registerForm.userName, [{
            strategy: 'isNonEmpty',
            errorMsg: '用户名不能为空'
        }, {
            strategy: 'minLength:6',
            errorMsg: '用户名长度不能小于10 位'
        }]);
        validator.add( registerForm.password, [{
            strategy: 'minLength:6',
            errorMsg: '密码长度不能小于6 位'
        }]);
        var errorMsg = validator.start();
        return errorMsg;
    }
    registerForm.onsubmit = function(){
        var errorMsg = validataFunc();
        if ( errorMsg ){
            alert ( errorMsg );
            return false;
        }

    };

3、代理模式

js
// 1
    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(){
            // onload成功之后,替换为rc
            myImage.setSrc( this.src );
        }
        return {
            setSrc: function( src ){
                // 首先设置一个loading 图片
                myImage.setSrc( 'file:// /C:/Users/svenzeng/Desktop/loading.gif' );
                // new Image 设置src
                img.src = src;
            }
        }
    })();
    proxyImage.setSrc( 'http://imgcache.qq.com/music/photo/k/000GGDys0yA0Nk.jpg' );
// 2
// 只返回方法
// 如果代理对象和本体对象都为一个函数(函数也是对象),函数必然都能被执行,则可以认为它们也具有一致的“接口”
    var myImage = (function(){
        var imgNode = document.createElement( 'img' );
        document.body.appendChild( imgNode );
        return function( src ){
            imgNode.src = src;
        }
    })();
    var proxyImage = (function(){
        var img = new Image;
        img.onload = function(){
            myImage( this.src );
        }
        return function( src ){
            myImage( 'file:// /C:/Users/svenzeng/Desktop/loading.gif' );
            img.src = src;
        }
    })();
    proxyImage( 'http://imgcache.qq.com/music// N/k/000GGDys0yA0Nk.jpg' );
// 3
// 节流也是一种代理
// 虚拟代理合并http请求
    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( ', ' ) );    // 2秒后向本体发送需要同步的ID集合
                clearTimeout( timer );    // 清空定时器
                timer = null;
                cache.length = 0; // 清空ID集合
            }, 2000 );
        }
    })();
    var checkbox = document.getElementsByTagName( 'input' );
    for ( var i = 0, c; c = checkbox[ i++ ]; ){
        c.onclick = function(){
            if ( this.checked === true ){
                proxySynchronousFile( this.id );
            }
        }
    };
// 4 脚本记载
    var miniConsole = (function(){
        var cache = [];
        var handler = function( ev ){
            if ( ev.keyCode === 113 ){
                var script = document.createElement( 'script' );
                script.onload = function(){
                    // keydown以后 负责真正打印
                    for ( var i = 0, fn; fn = cache[ i++ ]; ){
                        fn();
                    }
                };
                script.src = 'miniConsole.js';
                document.getElementsByTagName( 'head' )[0].appendChild( script );
                document.body.removeEventListener( 'keydown', handler ); // 只加载一次miniConsole.js
            }
        };
        document.body.addEventListener( 'keydown', handler, false );
        return {
            log: function(){
                var args = arguments;
                // 负责存储
                    cache.push(function(){
                        // 这里是miniConsole.js的miniConsole
                        return miniConsole.log.apply( miniConsole, args );
                    });
            }
        }
    })();
    miniConsole.log( 11 );      // 开始打印log
    // miniConsole.js 文件的代码
    miniConsole = {
        log: function(){
            // 真正代码略
            console.log( Array.prototype.join.call( arguments ) );
        }
    };
// 5 代理缓存
    var mult = function(){
        var a = 1;
        for ( var i = 0, l = arguments.length; i < l; i++ ){
            a = a * arguments[i];
        }
        return a;
    };
    mult( 2, 3 );    // 输出:6
    mult( 2, 3, 4 );    // 输出:24
    var proxyMult = (function(){
        var cache = {};
        return function(){
            var args = Array.prototype.join.call( arguments, ', ' );
            if ( args in cache ){
                // 有缓存直接返回
                return cache[ args ];
            }
            return cache[ args ] = mult.apply( this, arguments );// 计算结果进行缓存
        }
    })();
    proxyMult( 1, 2, 3, 4 );    // 输出:24
    proxyMult( 1, 2, 3, 4 );    // 输出:24
// 动态创建代理
    /**************** 计算乘积 *****************/
    var mult = function(){
        var a = 1;
        for ( var i = 0, l = arguments.length; i < l; i++ ){
            a = a * arguments[i];
        }
        return a;
    };
    /**************** 计算加和 *****************/
    var plus = function(){
        var a = 0;
        for ( var i = 0, l = arguments.length; i < l; i++ ){
            a = a + arguments[i];
        }
        return a;
    };
    /**************** 创建缓存代理的工厂 *****************/
    var createProxyFactory = 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 ); // fn
        }
    };
    var proxyMult = createProxyFactory( mult ),
    proxyPlus = createProxyFactory( plus );
    alert ( proxyMult( 1, 2, 3, 4 ) );    // 输出:24
    alert ( proxyMult( 1, 2, 3, 4 ) );    // 输出:24
    alert ( proxyPlus( 1, 2, 3, 4 ) );    // 输出:10
    alert ( proxyPlus( 1, 2, 3, 4 ) );    // 输出:10

4、迭代器模式

js
function createIterator(items){
    var i = 0;
    return {
        next:function (){
            var done = (i >= items.length);
            var value = !done ? items[i++]:undefined
        }
        return {
            done:done,
            value:value
        }
    }
}
js
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,
        length:obj.length,
    }
}
  • es 与设计模式 迭代器函数原理基本相同,就是将遍历从代码中分离,不再手动控制
js
var each = function( ary, callback ){
    for ( var i = 0, l = ary.length; i < l; i++ ){
        callback.call( ary[i], i, ary[ i ] );  // 把下标和元素当作参数传给callback函数
        // if ( callback( i, ary[ i ] ) === false ){    // callback的执行结果返回false,提前终止迭代
        //     break;
        // }
    }
};
each( [ 1, 2, 3 ], function( i, n ){
    alert ( [ i, n ] );
});
js
var getActiveUploadObj = function(){
    try{
        return new ActiveXObject( "TXFTNActiveX.FTNUpload" );    // IE上传控件
    }catch(e){
        return false;
    }
};

var getFlashUploadObj = function(){
    if ( supportFlash() ){     // supportFlash函数未提供
        var str = '<object type="application/x-shockwave-flash"></object>';
        return $( str ).appendTo( $('body') );
    }
    return false;
};
var getFormUpladObj = function(){
    var str = '<input name="file" type="file" class="ui-file"/>';  // 表单上传
    return $( str ).appendTo( $('body') );
};

var iteratorUploadObj = function(){
    for ( var i = 0, fn; fn = arguments[ i++ ]; ){
        var uploadObj = fn();
        if ( uploadObj ! == false ){
            return uploadObj;
        }
    }
};

var uploadObj = iteratorUploadObj( getActiveUploadObj, getFlashUploadObj, getFormUpladObj );

// var uploadObj = null
// for(const fn of [getActiveUploadObj, getFlashUploadObj, getFormUpladObj]){
//     var uploadObj = fn();
//         if ( uploadObj ! == false ){
//             uploadObj = uploadObj //获取
//             break //关闭
//         }
// }
js
Object.prototype.objCustom = function() {};
Array.prototype.arrCustom = function() {};

let iterable = [3, 5, 7];
iterable.foo = 'hello';

for (let i in iterable) {
  console.log(i); // logs 0, 1, 2, "foo", "arrCustom", "objCustom"
}

for (let i in iterable) {
  if (iterable.hasOwnProperty(i)) {
    console.log(i); // logs 0, 1, 2, "foo"
  }
}

for (let i of iterable) {
  console.log(i); // logs 3, 5, 7
}

1、for of 实际就是调用的迭代器函数 2、[1,2,3] 数组使用内建的迭代器函数 3、for...of 可以由break, throw, continue 迭代器关闭 (或return终止 ?) 4、生成器 4.1 调用生成 迭代器函数

js
function * fibonacci() { // 一个生成器函数
    let [prev, curr] = [0, 1];
    for (;;) { // while (true) {
        [prev, curr] = [curr, prev + curr];
        yield curr;
    }
}
// 调用生成 迭代器函数 
for (let n of fibonacci()) {
     console.log(n);
    // 当n大于1000时跳出循环
    if (n >= 1000)
        break;
}

4.2、生成器不应该重用

js
// 此处 gen是一个固定的迭代器
var gen = (function *(){
    yield 1;
    yield 2;
    yield 3;
})();
for (let o of gen) {
    console.log(o);
    break;//关闭生成器
}
//生成器不应该重用,以下没有意义!
for (let o of gen) {
    console.log(o);
}

5、订阅发布者模式.md

  • 发布—订阅模式又叫观察者模式

  • 它定义对象间的一种一对多的依赖关系,

  • 当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。

  • 在JavaScript开发中,我们一般用事件模型来替代传统的发布—订阅模式

  • 优点:

    • 广泛应用于异步编程中,这是一种替代传递回调函数的方案,无需过多关注对象在异步运行期间的内部状态,而只需要订阅感兴趣的事件发生点
    • 取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口,取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口
  • addEventListener 典型的订阅发布

js
        var salesOffices = {};    // 定义售楼处

        salesOffices.clientList = {};    // 缓存列表,存放订阅者的回调函数

        salesOffices.listen = function( key, fn ){
            if ( ! this.clientList[ key ] ){    // 如果还没有订阅过此类消息,给该类消息创建一个缓存列表
              this.clientList[ key ] = [];
            }
            this.clientList[ key ].push( fn );    // 订阅的消息添加进消息缓存列表
        };

        salesOffices.trigger = function(){    // 发布消息
            var key = Array.prototype.shift.call( arguments ),    // 取出消息类型
              fns = this.clientList[ key ];    // 取出该消息对应的回调函数集合

            if ( ! fns || fns.length === 0 ){    // 如果没有订阅该消息,则返回
              return false;
            }

            for( var i = 0, fn; fn = fns[ i++ ]; ){
              fn.apply( this, arguments );    // (2) // arguments是发布消息时附送的参数
            }
        };

        salesOffices.listen( 'squareMeter88', function( price ){    // 小明订阅88平方米房子的消息
            console.log( '价格= ' + price );    // 输出: 2000000
        });

        salesOffices.listen( 'squareMeter110', function( price ){     // 小红订阅110平方米房子的消息
            console.log( '价格= ' + price );    // 输出: 3000000
        });

        salesOffices.trigger( 'squareMeter88', 2000000 );     // 发布88平方米房子的价格
        salesOffices.trigger( 'squareMeter110', 3000000 );    // 发布110平方米房子的价格
js
    var event = {
        clientList: [],
        listen: function( key, fn ){
            if ( ! this.clientList[ key ] ){
                this.clientList[ key ] = [];
            }
            this.clientList[ key ].push( fn );    // 订阅的消息添加进缓存列表
        },
        trigger: function(){
            var key = Array.prototype.shift.call( arguments ),    // (1);
                fns = this.clientList[ key ];

            if ( ! fns || fns.length === 0 ){    // 如果没有绑定对应的消息
                return false;
            }

            for( var i = 0, fn; fn = fns[ i++ ]; ){
                fn.apply( this, arguments );    // (2) // arguments是trigger时带上的参数
            }
        },
        remove : function( key, fn ){
            var fns = this.clientList[ key ];

            if ( ! fns ){    // 如果key对应的消息没有被人订阅,则直接返回
                return false;
            }
            if ( ! fn ){    // 如果没有传入具体的回调函数,表示需要取消key对应消息的所有订阅
                fns && ( fns.length = 0 );
            }else{
                for ( var l = fns.length -1; l >=0; l-- ){    // 反向遍历订阅的回调函数列表
                    var _fn = fns[ l ];
                    if ( _fn === fn ){
                        fns.splice( l, 1 );    // 删除订阅者的回调函数
                    }
                }
            }
        };
    };

    var salesOffices = {};
    var installEvent = function( obj ){
        for ( var i in event ){
            obj[ i ] = event[ i ];
        }
    }

    installEvent( salesOffices );

    salesOffices.listen( 'squareMeter88', fn1 = function( price ){    // 小明订阅消息
        console.log( ’价格= ' + price );
    });

    salesOffices.listen( 'squareMeter88', fn2 = function( price ){    // 小红订阅消息
        console.log( ’价格= ' + price );
    });

    salesOffices.remove( 'squareMeter88', fn1 );    // 删除小明的订阅
    salesOffices.trigger( 'squareMeter88', 2000000 );     // 输出:2000000
js
    var Event = (function(){
		var global = this,
		Event,
		_default = 'default';
		Event = function(){
			var _listen,
			_trigger,
			_remove,
			_slice = Array.prototype.slice,
			_shift = Array.prototype.shift,
			_unshift = Array.prototype.unshift,
			namespaceCache = {},
			_create,
			find,
			each = function( ary, fn ){
				var ret;
				for ( var i = 0, l = ary.length; i < l; i++ ){
					var n = ary[i];
					ret = fn.call( n, i, n);
				}
				return ret;
			};
			_listen = function( key, fn, cache ){
				if ( !cache[ key ] ){
					cache[ key ] = [];
				}
				cache[key].push( fn );
			};
			_remove = function( key, cache ,fn){
				if ( cache[ key ] ){
					if( fn ){
						for( var i = cache[ key ].length; i >= 0; i-- ){
							if( cache[ key ] === fn ){
								cache[ key ].splice( i, 1 );
							}
						}
					}else{
						cache[ key ] = [];
					}
				}
			};
			_trigger = function(){
				var cache = _shift.call(arguments),
				key = _shift.call(arguments),
				args = arguments,
				_self = this,
				ret,
				stack = cache[ key ];
				if ( !stack || !stack.length ){
					return;
				}
				return each( stack, function(){
					return this.apply( _self, args );
				});
			};
			_create = function( namespace ){
				var namespace = namespace || _default;
				var cache = {},
				offlineStack = [], // 离线事件
				ret = {
					listen: function( key, fn, last ){
						_listen( key, fn, cache );
						if ( offlineStack === null ){
							return;
						}
						if ( last === 'last' ){
						}else{
							each( offlineStack, function(){
								this();
							});
						}
						offlineStack = null;
					},
					one: function( key, fn, last ){
						_remove( key, cache );
						this.listen( key, fn ,last );
					},
					remove: function( key, fn ){
						_remove( key, cache ,fn);
					},
					trigger: function(){
						var fn,
						args,
						_self = this;
						_unshift.call( arguments, cache );
						args = arguments;
						fn = function(){
							return _trigger.apply( _self, args );
						};
						if ( offlineStack ){
							return offlineStack.push( fn );
						}
						return fn();
					}
				};
				return namespace ?
				( namespaceCache[ namespace ] ? namespaceCache[ namespace ] :
					namespaceCache[ namespace ] = ret )
				: ret;
			};
			return {
				create: _create,
				one: function( key,fn, last ){
					var event = this.create( );
					event.one( key,fn,last );
				},
				remove: function( key,fn ){
					var event = this.create( );
					event.remove( key,fn );
				},
				listen: function( key, fn, last ){
					var event = this.create( );
					event.listen( key, fn, last );
				},
				trigger: function(){
					var event = this.create( );
					event.trigger.apply( this, arguments );
				}
			};
		}();
		return Event;
	})();

6、命令模式

命令

命令(command)指的是一个执行某些特定事情的指令 请求发送者和请求接收者能够消除彼此之间的耦合关系

  • 请求封装成command对象,可以在程序中被四处传递

  • command对象拥有更长的生命周期

  • 支持撤销、排队等操作

  • 实质就是 封装中间处理层,

  • 约定好,并执行

命令模式的由来,其实是回调(callback)函数的一个面向对象的替代品

撤销和重做

  • canvas
    • 因为在Canvas画图中,擦除一条线相对不容易实现
    • 先清除画布,然后把刚才执行过的命令全部重新执行一遍,这一点同样可以利用一个历史列表堆栈办到
  • 游戏录像
    • 把用户在键盘的输入都封装成命令,执行过的命令将被存放到堆栈中

排队

  • 命令执行排队进行,一个完成,再做第二个
  • 压进一个队列堆栈,当动画执行完,通知队列
    • 可以选择发布-订阅模式

宏命令

  • 宏命令是命令模式与组合模式的联用产物

智能式

傻瓜式 - 保存一个接收者来负责真正执行客户的请求,这种情况下命令对象是“傻瓜式”的

智能式 - “聪明”的命令对象可以直接实现请求,这样一来就不再需要接收者的存在 - 保存一个接收者来负责真正执行客户的请求,这种情况下命令对象是“傻瓜式”的

智能式与策略模式对比 - 策略模式指向的问题域更小,所有策略对象的目标总是一致的,它们只是达到这个目标的不同手段,它们的内部实现是针对“算法”而言的。 - 而智能命令模式指向的问题域更广,command对象解决的目标更具发散性。命令模式还可以完成撤销、排队等功能

7、组合模式

特点

树结构(组合对象、叶对象) - 只需要一次操作 对单个对象和组合对象的使用具有一致性 - 统一地使用组合结构中的所有对象,而不需要关心它究竟是组合对象还是单个对象

实质为深度优先搜索 每当对最上层的对象进行一次请求时,实际上是在对整个树进行深度优先的搜索

安全性问题

给叶对象也增加add方法,并且在调用这个方法时,抛出一个异常来及时提醒客户

符合开闭原则

我们改变了树的结构,增加了新的数据,却不用修改任何一句原有的代码,这是符合开放-封闭原则的

注意点

1、组合模式不是父子关系 - 一种聚合 - 组合对象把请求委托给它所包含的所有叶对象 - 能够合作的关键是拥有相同的接口

2、对叶对象操作的一致性 - 除了要求组合对象和叶对象拥有相同的接口之外 - 全体员工发放元旦的过节费1000块 - 过生日则不行

3、双向映射关系 - 比如某位架构师既隶属于开发组,又隶属于架构组 - 这种复合情况下我们必须给父节点和子节点建立双向映射关系 - 引入中介者模式来管理这些对象

4、用职责链模式提高组合模式性能 在实际操作中避免遍历整棵树,有一种现成的方案是借助职责链模式

使用

1、对象的部分-整体层次结构 2、统一对待树中的所有对象

8、模板方法模式

案例

  • 咖啡与茶 组成
  • 抽象父类(封装子类算法框架:公共方法、子类所有方法执行顺序)
  • 具体实现子类(继承整个算法结构,可以选择重写父类方法)

模板方法 是 Beverage.prototype.init

如果我们的Coffee类或者Tea类忘记实现这4个方法中的一个呢?

  • 让Beverage.prototype.brew等方法直接抛出一个异常

钩子方法 (应对变化)

  • 隔离变化的一种常见手段
  • 我们在父类中容易变化的地方放置钩子,
  • 钩子可以有一个默认的实现,究竟要不要“挂钩”,这由子类自行决定。
  • 钩子方法的返回结果决定了模板方法后面部分的执行步骤,也就是程序接下来的走向

好莱坞原则 - 模板方法模式:子类放弃了对自己的控制权,而是改为父类通知子类,哪些方法应该在什么时候被调用

js
    var Beverage = function( param ){
        var boilWater = function(){
            console.log( ’把水煮沸’ );
        };
        var brew = param.brew || function(){
            throw new Error( ’必须传递brew方法’ );
        };
        var pourInCup = param.pourInCup || function(){
            throw new Error( ’必须传递pourInCup方法’ );
        };
        var addCondiments = param.addCondiments || function(){
            throw new Error( ’必须传递addCondiments方法’ );
        };
        var F = function(){};
        F.prototype.init = function(){
            boilWater();
            brew();
            pourInCup();
            addCondiments();
        };
        return F;
    };
    var Coffee = Beverage({
        brew: function(){
            console.log( ’用沸水冲泡咖啡’ );
        },
        pourInCup: function(){
            console.log( ’把咖啡倒进杯子’ );
        },
        addCondiments: function(){
            console.log( ’加糖和牛奶’ );
        }
    });
    var Tea = Beverage({
        brew: function(){
            console.log( ’用沸水浸泡茶叶’ );
        },
        pourInCup: function(){
            console.log( ’把茶倒进杯子’ );
        },
        addCondiments: function(){
            console.log( ’加柠檬’ );
        }
    });
    var coffee = new Coffee();
    coffee.init();
    var tea = new Tea();
    tea.init();

9、享元模式

案例

  • 500模特

性能优化模式

  • 蝇量级
  • 运用共享技术来有效支持大量细粒度的对象

内部状态 外部状态

目标:运用共享技术来有效支持大量细粒度的对象

划分:

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

想要实现享元模式,解决两个问题:

1、对象工厂:只有当某种共享对象被真正需要时,它才从工厂中被创建出来;

js
    var UploadFactory = (function(){
        var createdFlyWeightObjs = {};
        return {
            create: function( uploadType){
                if ( createdFlyWeightObjs [ uploadType] ){
                    return createdFlyWeightObjs [ uploadType];
                }
                return createdFlyWeightObjs [ uploadType] = new Upload( uploadType);
            }
        }
    })();

2、管理器来记录:外部状态通过某个钩子和共享对象联系起来;

js
    var uploadManager = (function(){
        var uploadDatabase = {};
        return {
            add: function( id, uploadType, fileName, fileSize ){
                var flyWeightObj = UploadFactory.create( uploadType );

                var dom = document.createElement( 'div' );
                dom.innerHTML =
                        '<span>文件名称:'+ fileName +',文件大小: '+ fileSize +'</span>' +
                        '<button class="delFile">删除</button>';
                dom.querySelector( '.delFile' ).onclick = function(){
                    flyWeightObj.delFile( id );
                }
                document.body.appendChild( dom );
                uploadDatabase[ id ] = {
                    fileName: fileName,
                    fileSize: fileSize,
                    dom: dom
                };
                return flyWeightObj ;
            },
            setExternalState: function( id, flyWeightObj ){
                var uploadData = uploadDatabase[ id ];
                for ( var i in uploadData ){
                    flyWeightObj[ i ] = uploadData[ i ];
                }
            }
        }
    })();

何时使用

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

对象池 维护一个装载空闲对象的池子,如果需要对象的时候,不是直接new,而是转从对象池里获取。 如果对象池里没有空闲对象,则创建一个新的对象, 当获取出的对象完成它的职责之后,再进入池子等待被下次获取

10、职责链模式

案例

  • 高峰公交硬币传递

特点

  • 一系列可能会处理请求的对象被连接成一条链,
  • 请求在这些对象之间依次传递,
  • 直到遇到一个可以处理它的对象

最大优点

  • 请求发送者只需要知道链中的第一个节点,从而弱化了发送者和一组接收者之间的强联系

改进结果

  • 每种订单都有各自的处理函数而互不影响
  • 使用了职责链模式之后,链中的节点对象可以灵活地拆分重组
  • 可以手动指定起始节点

弊端

  • 链尾增加一个保底的接受者节点来处理这种即将离开链尾的请求
  • 大部分节点并没有起到实质性的作用,它们的作用仅仅是让请求传递下去

11、中介者模式

作用:接触对象与对象之间的紧耦合关系

案例:机场指挥塔、泡泡堂游戏

泡泡堂游戏 问题:每个玩家和其他玩家都是紧紧耦合在一起的

中介者模式是

  • 迎合迪米特法则的一种实现。
  • 迪米特法则也叫最少知识原则,
  • 是指一个对象应该尽可能少地了解另外的对象

缺点:

  • 系统中会新增一个中介者对象, 因为对象之间交互的复杂性,转移成了中介者对象的复杂性,且巨大

12、装饰者模式

给对象动态地增加职责的方式称为装饰者(decorator)模式

js装饰者

js
    var plane = {
        fire: function(){
            console.log( ’发射普通子弹’ );
        }
    }
    var missileDecorator = function(){
        console.log( ’发射导弹’ );
    }
    var atomDecorator = function(){
        console.log( ’发射原子弹’ );
    }
    var fire1 = plane.fire;
    plane.fire = function(){
        fire1();
        missileDecorator();
    }
    var fire2 = plane.fire;
    plane.fire = function(){
        fire2();
        atomDecorator();
    }
    plane.fire();
        // 分别输出:发射普通子弹、发射导弹、发射原子弹

思路: 想为函数添加一些功能,最简单粗暴的方式就是直接改写该函数,

不足: 但这是最差的办法,直接违反了开放-封闭原则

解决: 保存引用:通过保存原引用的方式就可以改写某个函数

两个问题: 1、中间变量的数量也会越来越多; 2、中间变量的数量也会越来越多

解决:AOP Function.prototype.before接受一个函数当作参数, 这个函数即为新添加的函数,它装载了新添加的功能代码 把当前的this保存起来,这个this指向原函数 返回一个代理函数 请求分别转发给新添加的函数和原函数 实现了动态装饰效果

应用: 数据统计上报 用AOP动态改变函数的参数 插件式的表单验证

对比:

代理模式:

1、直接访问本体不方便或者不符合需要时,为这个本体提供一个替代者 本体定义了关键功能,而代理提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情

2、代理模式强调一种关系(Proxy与它的实体之间的关系),这种关系可以静态的表达,也就是说,这种关系在一开始就可以被确定

3、一层代理-本体的引用

装饰者模式:

1、为对象动态加入行为 2、用于一开始不能确定对象的全部功能时 3、形成一条长长的装饰链

13、状态模式

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

案例:电灯开关

状态模式的关键是

  • 把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部
  • 把状态的切换规则事先分布在状态类中,这样就有效地消除了原本存在的大量条件分支语句

优点:

  • 状态与行为之间的关系,并将它们封装在一个类里。通过增加新的状态类,很容易增加新的状态和转换;
  • 避免Context无限膨胀
  • 对象代替字符串来记录当前状态
  • Context中的请求动作和状态类中封装的行为可以非常容易地独立变化而互不影响。

缺点:

  • 定义许多状态类
  • 造成了逻辑分散的问题,我们无法在一个地方就看出整个状态机的逻辑

对比策略模式:

相同: 它们都有一个上下文、一些策略或者状态类,上下文把请求委托给这些类来执行

不同:意图上有很大不同 策略模式各个策略类之间是平等又平行的,它们之间没有任何联系, 所以客户必须熟知这些策略类的作用,以便客户可以随时主动切换算法;

状态模式中,状态和状态对应的行为是早已被封装好的,状态之间的切换也早被规定完成, “改变行为”这件事情发生在状态模式内部。对客户来说,并不需要了解这些细节。

状态机 javascript 状态机

  • call 请求委托给某个字面量对象来执行
  • 利用下面的delegate函数来完成这个状态机编写(闭包)

表驱动有限状态机 https://github.com/jakesgordon/javascript-state-machine

状态机在游戏开发中也有着广泛的用途,特别是游戏AI的逻辑编写

14、适配器模式

解决两个软件实体间的接口不兼容的问题

案例:港式插头转换器、电源适配器、usb转接头

对比:

适配器模式主要用来解决两个已有接口之间不匹配的问题

对象增加功能;装饰者模式常常形成一条长的装饰链;

代理模式是为了控制对对象的访问,通常也只包装一次

外观模式最显著的特点是定义了一个新的接口