JavaScript设计模式与开发实践(二)--设计模式篇

将不变的部分和变化的部分隔开是每个设计模式的主题

单例模式 – 性能优化,减少创建的开销

定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点
实现:用一个变量来标志当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象
惰性单例:惰性单例指的是在需要的时候才创建对象实例

var getSingle = function(fn){
    
      // 创建实例的方法用参数 fn 的形式传入
  var result;
  return function(){
    
    
    return result || result = fn.apply(this, arguments) // 保证只有一个实例
  }
}

策略模式 – 知道所有策略,不同场景使用不同的策略

定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。(定义一系列的算法,把它们各自封装成策略类,算法被封装在策略类内部的方法里,在定义一个环境类/函数(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

代理模式 – 需要在访问本体前操作

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

  • 在编写业务代码的时候,往往不需要去预先猜测是否需要使用代理模式。当真正发现不方便直接访问某个对象的时候,再编写代理也不迟
  • 代理的作用:1. 可以过滤请求;2. 把一些开销很大的对象,延迟到真正需要它的时候才去创建。
  • 保护代理和虚拟代理
    保护代理:
    代理B可以帮助A过滤掉一些请求,比如送花的人中年龄太大的或者没有宝马的,这种请求就可以直接在代理 B处被拒绝掉。这种代理叫作保护代理。(保护代理用于控制不同权限的对象对目标对象的访问,但在JavaScript并不容易实现保护代理,因为我们无法判断谁访问了某个对象。)
    虚拟代理:
    虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建(把一些开销很大的事情,延迟到真正需要做的时候再去执行)。
  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() {
    
    
      myImage.setSrc(this.src);  // this指向img
  }
  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' );

// proxyImage 控制了客户对 MyImage 的访问,并且在此过程中加入一些额外的操作,比如在真正的图片加载好之前,先把 img 节点的 src 设置为一张本地的 loading图片。
  1. 虚拟代理合并http请求
// 通过一个代理函数 proxySynchronousFile 来收集一段时间之内的请求,最后一次性发送给服务器。比如我们等待 2 秒之后才把这 2 秒之内需要同步的文件 ID 打包发给服务器,如果不是对实时性要求非常高的系统,2秒的延迟不会带来太大副作用,却能大大减轻服务器的压力。
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);
      }
  }
};
  1. 缓存代理
// 先创建一个用于求乘积的函数:
var mult = function() {
    
    
  console.log('开始计算乘积');
  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

迭代器模式 – 知道所有方法,依次迭代,取出能用的方法

无论是内部迭代器还是外部迭代器,只要被迭代的聚合对象拥有 length 属性而且可以用下标访问,那它就可以被迭代

  1. 内部迭代器(内部迭代器的迭代规则已经被提前规定)
// 内部迭代器
var each = function(ary, callback) {
    
    
    for (var i = 0, l = ary.length; i < l; i++) {
    
    
        callback.call(ary[i], i, ary[i]); // 把下标和元素当作参数传给 callback 函数
    }
};
each([1, 2, 3], function(i, n) {
    
    
    alert([i, n]);
});
  1. 外部迭代器
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
    }
};

应用举例:

// 好比我们有一个钥匙串,其中共有 3把钥匙,我们想打开一扇门但是不知道该使用哪把钥匙,于是从第一把钥匙开始,迭代钥匙串进行尝试,直到找到了正确的钥匙为止
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);

发布-订阅模式 – 订阅的函数存在对象数组中,发布时取出来执行

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

var Event = (function() {
    
    
    var clientList = {
    
    },
        listen,
        trigger,
        remove;
    listen = function(key, fn) {
    
    
        if (!clientList[key]) {
    
    
            clientList[key] = [];
        }
        clientList[key].push(fn);
    };
    trigger = function() {
    
    
        var key = Array.prototype.shift.call(arguments),
            fns = clientList[key];
        if (!fns || fns.length === 0) {
    
    
            return false;
        }
        for (var i = 0, fn; fn = fns[i++];) {
    
    
            fn.apply(this, arguments);
        }
    };
    remove = function(key, fn) {
    
    
        var fns = clientList[key];
        if (!fns) {
    
    
            return false;
        }
        if (!fn) {
    
    
            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);
                }
            }
        }
    };
    return {
    
    
        listen: listen,
        trigger: trigger,
        remove: remove
    }
})();
Event.listen('squareMeter88', function(price) {
    
     // 小红订阅消息
    console.log('价格= ' + price); // 输出:'价格=2000000'
});
Event.trigger('squareMeter88', 2000000); // 售楼处发布消息
  • 升级版事件对象 – 添加离线缓存功能(可以先发布后订阅)和使用命名空间(减少命名冲突)
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][i] === 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') {
    
    
                            offlineStack.length && offlineStack.pop()();
                        } 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;
})();

/************** 先发布后订阅 ********************/
Event.trigger('click', 1);
Event.listen('click', function(a) {
    
    
    console.log(a); // 输出:1
});
/************** 使用命名空间 ********************/
Event.create('namespace1').listen('click', function(a) {
    
    
    console.log(a); // 输出:1
});
Event.create('namespace1').trigger('click', 1);
Event.create('namespace2').listen('click', function(a) {
    
    
    console.log(a); // 输出:2
});
Event.create('namespace2').trigger('click', 2);

命令模式 – 把请求发送者和请求接收者解耦开(没有接收者的命令模式与策略模式类似)、记录命令信息

定义:命令模式中的命令(command)指的是一个执行某些特定事情的指令命
令模式最常见的应用场景是:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。
tip:所有事情都要执行命令(不变的地方),执行命令的内容不同(变的地方)

// 特点:有receiver和command.execute
var RefreshMenuBarCommand = function(receiver) {
    
    
    return {
    
    
        execute: function() {
    
    
            receiver.refresh();
        }
    }
};
var setCommand = function(button, command) {
    
    
    button.onclick = function() {
    
    
        command.execute();
    }
};
var MenuBar = {
    
    
    refresh: function(){
    
    
        console.log( '刷新菜单界面' );
    }
};
var refreshMenuBarCommand = RefreshMenuBarCommand(MenuBar);
setCommand(button1, refreshMenuBarCommand);  // button1就是请求者,refreshMenuBarCommand就是接收者

作用:

  1. 撤销命令(撤销操作的实现一般是给命令对象增加一个名为 unexecude 或者 undo 的方法,在该方法里执行 execute 的反向操作)
  2. 撤销和重做(使用堆栈保存命令,如果无法撤销,则重做)
  3. 命令排队
  4. 请求发送者和请求接收者解耦(各干各的活)
  5. 宏命令(一连串的命令放在堆栈中一个一个执行)
    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();
    

组合模式

定义:组合模式就是用小的子对象来构建更大的对象,而这些小的子对象本身也许是由更小的“孙对象”构成的(宏命令)
使用:a. 表示对象的部分整体层次结构。组合模式可以方便地构造一棵树来表示对象的部分-整体结构。特别是我们在开发期间不确定这棵树到底存在多少层次的时候。在树的构造最终完成之后,只需要通过请求树的最顶层对象,便能对整棵树做统一的操作。在组合模式中增加和删除树的节点非常方便,并且符合开放封闭原则。b. 客户希望统一对待树中的所有对象。组合模式使客户可以忽略组合对象和叶对象的区别,客户在面对这棵树的时候,不用关心当前正在处理的对象是组合对象还是叶对象,也就不用写一堆 if 、 else 语句来分别处理它们。组合对象和叶对象会各自做自己正确的事情,这是组合模式最重要的能力。

  1. 扫描文件夹例子
/******************************* 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++];) {
    
    
        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);
};

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

folder.scan();
  1. 引用父对象
// Folder类
var Folder = function(name) {
    
    
    this.name = name;
    this.parent = null; //增加 this.parent 属性
    this.files = [];
};
Folder.prototype.add = function(file) {
    
    
    file.parent = this; //设置父对象
    this.files.push(file);
};
Folder.prototype.scan = function() {
    
    
    console.log('开始扫描文件夹: ' + this.name);
    for (var i = 0, file, files = this.files; file = files[i++];) {
    
    
        file.scan();
    }
};
Folder.prototype.remove = function() {
    
    
    if (!this.parent) {
    
     //根节点或者树外的游离节点
        return;
    }
    for (var files = this.parent.files, l = files.length - 1; l >= 0; l--) {
    
    
        var file = files[l];
        if (file === this) {
    
    
            files.splice(l, 1);
        }
    }
};

// File类
var File = function(name) {
    
    
    this.name = name;
    this.parent = null;
};
File.prototype.add = function() {
    
    
    throw new Error('不能添加在文件下面');
};
File.prototype.scan = function() {
    
    
    console.log('开始扫描文件: ' + this.name);
};
File.prototype.remove = function() {
    
    
    if (!this.parent) {
    
     //根节点或者树外的游离节点
        return;
    }
    for (var files = this.parent.files, l = files.length - 1; l >= 0; l--) {
    
    
        var file = files[l];
        if (file === this) {
    
    
            files.splice(l, 1);
        }
    }
};
var folder = new Folder('学习资料');
var folder1 = new Folder('JavaScript');
var file1 = new Folder('深入浅出 Node.js');
folder1.add(new File('JavaScript 设计模式与开发实践'));
folder.add(folder1);
folder.add(file1);
folder1.remove(); //移除文件夹
folder.scan();

猜你喜欢

转载自blog.csdn.net/qq_36303110/article/details/114290710