js中的设计模式
首先了解一下设计原则:
单一职责原则(SRP):一个对象或一个方法只做一件事情。如果一个方法承担了过多的事情,那么在需求更改的时候,需要改写这个方法的可能性就越大。应该把对象或方法划分为更小的粒度。
最少知识原则(LKP):一个软件实体,应该尽可能少的与其他实体发生相互作用。应当尽量减少两个对象之间的交互,如果不是必要的直接关系,最好通过第三方进行处理。
开发-封闭原则(OCP):软件实体(类、函数、模块),只能为其扩展,不能更改。当需要改变一个程序的功能或为其增添新的功能时,可以通过增加代码的方式,尽量避免修改源代码,防止影响原系统的稳定。
如promise中,每一个then中做一件事,当有新的需求时,在后面添加更多的then,而不是修改之前的代码
下面介绍一些常用的设计模式
一、单例模式
确保一个类,仅有一个实例,且提供了一个全局访问点
如:有一个manager类,即使多次调用构造函数也仅创建一个manager
// 构造函数
function setManager(name) { this.manager = name; }
// 向原型上添加方法 setManager.prototype.getName = function () { console.log(this.manager); }
// 创建单例manager的方法,仅当manager不存在时,创建新的manager,最后返回 var singletonSetManager = (function () { var manager = null; return function (name) { if (!manager) { manager = new setManager(name); } return manager; }; })();
然而,以上的方法仅能实现manager单例的需求,如果此时需要实现一个hr单例需求呢?
因此,将单例的实现方法进行抽取
// 将创建单例的方法当作参数传入,单例不存在时,通过apply调用。最后将单例返回
function singletonSetInstance(fn) { var instance = null; return function () { if (!instance) { instance = fn.apply(this, arguments); } return instance; }; }
之后,对于想要创建单例的类,仅需调用以上方法,传入函数,得到关于该类的单例
function setManager(name) { this.manager = name; } setManager.prototype.getName = function () { console.log(this.manager); }; var getSingleManager = singletonSetInstance(function (name) { var manager = new setManager(name); return manager; }); function setHr(name) { this.hr = name; } setHr.prototype.getName = function () { console.log(this.hr); }; var getSingleHr = singletonSetInstance(function (name) { var hr = new setHr(name); return hr; }); getSingleManager("m1").getName(); // m1 getSingleManager("m2").getName(); // m1 getSingleHr("h1").getName(); // h1 getSingleHr("h2").getName(); // h1
二、策略模式
策略模式将一系列算法汇总到策略集中,根据不同情况进行调用。将算法的实现和使用分隔开来
一个基于策略模式的程序至少由两部分组成:
第一个部分是一组策略类,策略类封装了具体的算法,并负责具体的计算过程。
第二个部分是环境类Context,Context接受客户的请求,随后把请求委托给某一个策略类。要做到这点,说明Context 中要维持对某个策略对象的引用
例如需要根据学生的成绩等级,对应分数进行加权
var levelMap = { S: 10, A: 8, B: 6, C: 4, }; var setScore = { basicScore: 80, S: function () { return this.basicScore + levelMap["S"]; }, A: function () { return this.basicScore + levelMap["A"]; }, B: function () { return this.basicScore + levelMap["B"]; }, C: function () { return this.basicScore + levelMap["C"]; }, }; function getScore(level) { return setScore[level] ? setScore[level]() : 0; } console.log(getScore("S")); // 90 console.log(getScore("A")); // 88 console.log(getScore("B")); // 86 console.log(getScore("C")); // 84
策略模式经常用在对表单的验证中:
<script> var errMsgs = { default: "输入数据格式不正确", minLength: "输入数据长度不足", isNumber: "请输入数字", required: "内容不能为空", }; var rules = { minLength: function (value, length, errMsg) { if (value.length < length) { return errMsg || errMsgs["minLength"]; } }, isNumber: function (value, errMsg) { if (!/^\d+$/.test(value)) { return errMsg || errMsgs["isNumber"]; } }, required: function (value, errMsg) { if (value === "") { return errMsg || errMsgs["required"]; } }, }; function Validators() { this.items = []; } Validators.prototype = { constructor: Validators, add: function (value, rule, errMsg) { var arg = [value]; if (rule.indexOf("minLength") != -1) { var temp = rule.split(":"); arg.push(temp[1]); rule = temp[0]; } arg.push(errMsg); this.items.push(function () { return rules[rule].apply(this, arg); }); }, start: function () { for (var i = 0; i < this.items.length; ++i) { var ret = this.items[i](); if (ret) { console.log(ret); } } }, }; var validate = new Validators(); validate.add("111s", "isNumber", "输入内容只能是数字"); validate.add("1", "minLength:5"); validate.add("", "required"); validate.start(); </script>
三、代理模式
当客户不方便直接访问一个 对象或者不满足需要的时候,提供一个替身对象 来控制对这个对象的访问,客户实际上访问的是 替身对象。
替身对象对请求做出一些处理之后, 再把请求转交给本体对象
代理模式主要有三种:保护代理、虚拟代理、缓存代理
保护代理主要实现了访问主体的限制行为,以过滤字符作为简单的例子
// 主体,发送消息 function sendMsg(msg) { console.log(msg); } // 代理,对消息进行过滤 function proxySendMsg(msg) { // 无消息则直接返回 if (typeof msg === 'undefined') { console.log('deny'); return; } // 有消息则进行过滤 msg = ('' + msg).replace(/泥\s*煤/g, ''); sendMsg(msg); } sendMsg('泥煤呀泥 煤呀'); // 泥煤呀泥 煤呀 proxySendMsg('泥煤呀泥 煤'); // 呀 proxySendMsg(); // deny
它的意图很明显,在访问主体之前进行控制,没有消息的时候直接在代理中返回了,拒绝访问主体,这数据保护代理的形式
有消息的时候对敏感字符进行了处理,这属于虚拟代理的模式
虚拟代理在控制对主体的访问时,加入了一些额外的操作
在滚动事件触发的时候,也许不需要频繁触发,我们可以引入函数节流,这是一种虚拟代理的实现
// 函数防抖,频繁操作中不处理,直到操作完成之后(再过 delay 的时间)才一次性处理 function debounce(fn, delay) { delay = delay || 200; var timer = null; return function() { var arg = arguments; // 每次操作时,清除上次的定时器 clearTimeout(timer); timer = null; // 定义新的定时器,一段时间后进行操作 timer = setTimeout(function() { fn.apply(this, arg); }, delay); } }; var count = 0; // 主体 function scrollHandle(e) { console.log(e.type, ++count); // scroll } // 代理 var proxyScrollHandle = (function() { return debounce(scrollHandle, 500); })(); window.onscroll = proxyScrollHandle;
缓存代理可以为一些开销大的运算结果提供暂时的缓存,提升效率
来个栗子,缓存加法操作
// 主体 function add() { var arg = [].slice.call(arguments); return arg.reduce(function(a, b) { return a + b; }); } // 代理 var proxyAdd = (function() { var cache = []; return function() { var arg = [].slice.call(arguments).join(','); // 如果有,则直接从缓存返回 if (cache[arg]) { return cache[arg]; } else { var ret = add.apply(this, arguments); return ret; } }; })(); console.log( add(1, 2, 3, 4), add(1, 2, 3, 4), proxyAdd(10, 20, 30, 40), proxyAdd(10, 20, 30, 40) ); // 10 10 100 100
四、迭代器模式
1. 定义
迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
2. 核心
在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素
3. 实现
JS中数组的map forEach 已经内置了迭代器
[1, 2, 3].forEach(function(item, index, arr) { console.log(item, index, arr); });
不过对于对象的遍历,往往不能与数组一样使用同一的遍历代码
我们可以封装一下
function each(obj, cb) { var value; if (Array.isArray(obj)) { for (var i = 0; i < obj.length; ++i) { value = cb.call(obj[i], i, obj[i]); if (value === false) { break; } } } else { for (var i in obj) { value = cb.call(obj[i], i, obj[i]); if (value === false) { break; } } } } each([1, 2, 3], function(index, value) { console.log(index, value); }); each({a: 1, b: 2}, function(index, value) { console.log(index, value); }); // 0 1 // 1 2 // 2 3 // a 1 // b 2
再来看一个例子,强行地使用迭代器,来了解一下迭代器也可以替换频繁的条件语句
虽然例子不太好,但在其他负责的分支判断情况下,也是值得考虑的
function getManager() { var year = new Date().getFullYear(); if (year <= 2000) { console.log('A'); } else if (year >= 2100) { console.log('C'); } else { console.log('B'); } } getManager(); // B
将每个条件语句拆分出逻辑函数,放入迭代器中迭代
function year2000() { var year = new Date().getFullYear(); if (year <= 2000) { console.log('A'); } return false; } function year2100() { var year = new Date().getFullYear(); if (year >= 2100) { console.log('C'); } return false; } function year() { var year = new Date().getFullYear(); if (year > 2000 && year < 2100) { console.log('B'); } return false; } function iteratorYear() { for (var i = 0; i < arguments.length; ++i) { var ret = arguments[i](); if (ret !== false) { return ret; } } } var manager = iteratorYear(year2000, year2100, year); // B
五、发布-订阅模式
也称作观察者模式,定义了对象间的一种一对多的依赖关系,当一个对象的状态发 生改变时,所有依赖于它的对象都将得到通知
与传统的发布-订阅模式实现方式(将订阅者自身当成引用传入发布者)不同,在JS中通常使用注册回调函数的形式来订阅
小A在公司C完成了笔试及面试,小B也在公司C完成了笔试。他们焦急地等待结果,每隔半天就电话询问公司C,导致公司C很不耐烦。
一种解决办法是 AB直接把联系方式留给C,有结果的话C自然会通知AB
这里的“询问”属于显示调用,“留给”属于订阅,“通知”属于发布
<script> var observer = { // 订阅集合 subscribes: [], // 订阅 subscribe: function (type, fn) { // 如果不存在处理此类型事件的订阅数组,进行初始化 if (!this.subscribes[type]) { this.subscribes[type] = []; } typeof fn === "function" && this.subscribes[type].push(fn); }, // 发布 publish: function () { var type = [].shift.call(arguments); var fns = this.subscribes[type]; // 如果不存在该类型的处理函数 if (!fns || !fns.length) { return; } // 对于该类型的事件分别调用 for (var i = 0; i < fns.length; ++i) { fns[i].apply(this, arguments); } }, remove: function (type, fn) { // 删除全部 if (typeof type === "undefined") { this.subscribes = []; return; } var fns = this.subscribes[type]; if (!fns || !fns.length) { return; } //删除所有该类型的处理函数 if (typeof fn === "undefined") { this.subscribes[type] = []; return; } for (var i = 0; i < fns.length; ++i) { if (fns[i] === fn) { fns.splice(i, 1); } } }, }; function jobA(jobs) { console.log("jobList for A", jobs); } function jobB(jobs) { console.log("jobList for B", jobs); } observer.subscribe("job", jobA); observer.subscribe("job", jobB); observer.subscribe("examA", function () { console.log("100"); }); observer.subscribe("examB", function () { console.log("99"); }); observer.subscribe("interviewA", function () { console.log("通过"); }); observer.publish("job", ["前端开发", "设计师", "产品经理"]); observer.publish("examA"); observer.publish("examB"); observer.publish("interviewA"); observer.remove("job", jobA); observer.publish("job", ["咖啡师", "前台", "店长"]); </script>
六、组合模式
1. 定义
是用小的子对象来构建更大的 对象,而这些小的子对象本身也许是由更小 的“孙对象”构成的。
2. 核心
可以用树形结构来表示这种“部分- 整体”的层次结构。
调用组合对象 的execute方法,程序会递归调用组合对象 下面的叶对象的execute方法
但要注意的是,组合模式不是父子关系,它是一种HAS-A(聚合)的关系,将请求委托给 它所包含的所有叶对象。基于这种委托,就需要保证组合对象和叶对象拥有相同的 接口
此外,也要保证用一致的方式对待 列表中的每个叶对象,即叶对象属于同一类,不需要过多特殊的额外操作
3. 实现
使用组合模式来实现扫描文件夹中的文件
<script> function Folder(name) { this.name = name; this.files = []; this.parent = null; } Folder.prototype = { constructor: Folder, add: function (file) {
// 返回this,实现链式调用 file.parent = this; this.files.push(file); return this; }, scan: function () {
// 传给叶子执行 for (var i = 0; i < this.files.length; ++i) { this.files[i].scan(); } }, remove(file) { // 删除全部 if (typeof file === "undefined") { this.files = []; return; } for (var i = 0; i < this.files.length; ++i) { if (this.files[i] === file) { this.files.splice(i, 1); } } }, }; function File(name) { this.name = name; this.parent = null; } File.prototype = { constructor: File, add: function () { console.log("文件中不能添加文件"); }, scan: function () { var name = [this.name]; var parent = this.parent; while (parent) { name.unshift(parent.name); parent = parent.parent; } console.log(name.join(" / ")); }, }; var projects = new Folder("projects"); var mainweb = new Folder("mainweb"); var center = new Folder("center"); var src = new Folder("src"); var index = new File("index.html"); var app = new File("app.js"); var main = new File("main.js"); var home = new File("home.html"); var utils = new File("utils.js"); projects.add(mainweb).add(center); // projects / center / src / index.html center.add(src); // projects / center / src / utils.js src.add(index).add(utils); // projects / center / app.js center.add(app).add(main); // projects / center / main.js projects.add(home); // projects / home.html projects.scan(); </script>
七、命令模式
命令模式,就是将一系列命令添加到类中,通过包装实例对象的命令为对象,就可以随意通过实例对象发出命令,并按情况将命令压入栈中。通常命令模式都有Redo(重做)、undo(撤销)和execute(执行)三种命令
以下代码示例,实现了自增命令,包含撤销和重做
<script> function Increment() { // 自加栈为空 this.stack = []; // 初始时,指针指向-1 this.stackPosition = -1; // 初始值为0 this.val = 0; } Increment.prototype = { // 执行命令 execute: function () { this._cleanUedo(); // 定义自加命令 var command = function () { this.val += 2; }.bind(this); // 执行 command(); // 缓存 this.stack.push(command); // 指针后移 this.stackPosition++; this.getValue(); }, // 判断是否可以重做 canRedo: function () { return this.stackPosition < this.stack.length - 1; }, canUndo: function () { return this.stackPosition >= 0; }, redo: function () { if (!this.canRedo()) { return; } // 执行当前指针后一位的命令 this.stack[++this.stackPosition](); this.getValue(); }, undo: function () { if (!this.canUndo()) { return; } var command = function () { this.val -= 2; }.bind(this); command(); // 撤销命令不需缓存,指针向前移一位,自加命令依然在栈中 this.stackPosition--; this.getValue(); }, getValue() { console.log(this.val); }, _cleanUedo() { // 撤销的命令不再执行 this.stack = this.stack.slice(0, this.stackPosition + 1); }, }; var increment = new Increment(); var eventTriggle = { execute: function () { increment.execute(); }, undo: function () { increment.undo(); }, redo: function () { increment.redo(); }, }; eventTriggle.execute(); // 2 eventTriggle.execute(); // 4 eventTriggle.execute(); // 6 eventTriggle.execute(); // 8 eventTriggle.undo(); // 6 eventTriggle.undo(); // 4 eventTriggle.undo();// 2 eventTriggle.undo(); // 0 eventTriggle.undo(); // 无输出 eventTriggle.redo(); // 2 eventTriggle.redo(); // 4 eventTriggle.redo(); // 6 eventTriggle.redo(); // 8 eventTriggle.redo();// 无输出 </script>
当然,以上只针对自加命令做了处理,当命令增多时,可以将命令抽取,调用execute时,将命令当作参数传入,压入栈中。如下:
<script> var MacroCommand = { commands: [], add: function (command) { this.commands.push(command); return this; }, remove: function (command) { // 当不不传参时,删除所有命令 if (!command) { this.commands = []; return; } for (var i = 0; i < this.commands.length; i++) { if (this.commands[i] === command) { this.commands.splice(i, 1); } } }, execute: function () { if (this.commands.length === 0) { return; } for (var i = 0; i < this.commands.length; i++) { this.commands[i].execute(); } }, }; var showName = { execute: function () { console.log("ashen"); }, }; var showGendle = { execute: function () { console.log("female"); }, }; MacroCommand.add(showName).add(showGendle); MacroCommand.execute();
</script>
八、模板方法模式
模板方法模式由抽象父类和具体的实现子类组成
在抽象父类中封装子类的算法框架,它的 init方法可作为一个算法的模板,指导子类以何种顺序去执行哪些方法。
由父类分离出公共部分,要求子类重写某些父类的(易变化的)抽象方法
模板方法模式一般的实现方式为继承
以运动为例,如下
<script> function Sport() {} Sport.prototype = { init: function () { this.strech(); this.jog(); this.deepBreath(); this.start(); this.free = this.end(); if (this.free) { this.strech(); } }, strech: function () { console.log("先拉伸一下肌肉"); }, jog: function () { console.log("再慢跑一会,热热身"); }, deepBreath: function () { console.log("跑完深呼吸~"); }, start: function () { throw new Error("子类必须改写此方法"); }, end: function () { console.log("运动结束"); }, }; function Run() {} Run.prototype = new Sport(); Run.prototype.start = function () { console.log("每天跑个半小时"); }; Run.prototype.end = function () { console.log("跑完要回去虐腹,先走啦!"); return false; }; function Zumba() {} Zumba.prototype = new Sport(); var run = new Run(); var zumba = new Zumba(); run.init(); zumba.init(); </script>
执行结果如下:
九、享元模式
享元(flyweight)模式是一种用于性能优化的模式,“fly”在这里是苍蝇的意思,意为蝇量级。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。如果系统中因为创建了大量类似的对象而导致内存占用过高,享元模式就非常有用了。在javascript中,浏览器特别是移动端的浏览器分配的内存并不算多,如何节省内存就成了一件非常有意义的事情。
十、职责链模式
使多个对象都有机会处理请求,从而避免请求的发送者和请求的接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
请求发送者只需要知道链中的第一个节点,弱化发送者和一组接收者之间的强联系,可以便捷地在职责链中增加或删除一个节点,同样地,指定谁是第一个节点也很便捷。
如下,实现了一个简单的判断数据类型的职责链
<script> function ChainItem(fn) { this.fn = fn; this.next = null; } ChainItem.prototype = { constructor: ChainItem, setNext: function (next) { this.next = next; return next; }, start: function () { this.fn.apply(this, arguments); }, toNext: function () { if (this.next) { this.start.apply(this.next, arguments); } else { console.log("无匹配的执行项目"); } }, }; function showNumber(num) { if (typeof num === "number") { console.log("number", num); } else { this.toNext(num); } } function showString(str) { if (typeof str === "string") { console.log("string", str); } else { this.toNext(str); } } function showObject(obj) { if (typeof obj === "object") { console.log("object", obj); } else { this.toNext(obj); } } var numberItem = new ChainItem(showNumber); var stringItem = new ChainItem(showString); var objectItem = new ChainItem(showObject); numberItem.setNext(stringItem).setNext(objectItem); numberItem.start({ name: "ashen" }); // object {name: "ashen"}
objectItem.start("str"); // 无匹配的执行项目 </script>
当需要向其中加入判断是否undefined也很容易,如下
function showUndefined(un) { if (typeof un === "undefined") { console.log("undefined", un); } else { this.toNext(un); } } var undefinedItem = new ChainItem(showUndefined); objectItem.setNext(undefinedItem); // 可以添加到任意一个节点后 numberItem.start(); // 可以从任意节点开始 undefined undefined
十一、中介者模式
<script> var A = { score: 100, changeTo: function (score) { this.score = score; this.getRank(); }, getRank: function () { var scores = [this.score, B.score, C.score].sort((a, b) => { return a < b; }); console.log(scores.indexOf(this.score) + 1); }, }; var B = { score: 90, changeTo: function (score) { this.score = score; rankMediator(B); }, }; var C = { score: 80, changeTo: function (score) { this.score = score; rankMediator(C); }, }; function rankMediator(person) { var scores = [A.score, B.score, C.score].sort(); console.log(scores); console.log(scores.indexOf(person.score) + 1); } A.changeTo(120); B.changeTo(150); C.changeTo(130); </script>
以上例子中,A 通过自身的函数,拿到B、C的成绩进行排名,而B和C通过中介者rankMediator进行排名,减少了多对象间的相互引用
十二、装饰者模式
var person = { name: 'ashen', } function decorator(){ console.log(person.name + '1999'); }
还可以通过传统的面向对象实现
function Person() {} Person.prototype.skill = function () { console.log("唱歌"); }; function CodeDecorator(person) { this.person = person; } CodeDecorator.prototype.skill = function () { this.person.skill(); console.log("敲代码"); }; function DanceDecorator(person) { this.person = person; } DanceDecorator.prototype.skill = function () { this.person.skill(); console.log("跳舞"); }; var person = new Person(); var person1 = new Person(); person1 = new CodeDecorator(person1); person1 = new DanceDecorator(person1); person1.skill(); // 唱歌 敲代码 跳舞
在JS中,函数为一等对象,所以我们也可以使用更通用的装饰函数
function decorateBefore(fn, beforeFn) { return function () { var ret = beforeFn.apply(this, arguments); if (ret !== false) { fn.apply(this, arguments); } }; } function skill() { console.log("说话"); } function skillEat() { console.log("吃饭"); } function skillSleep() { console.log("睡觉"); } var getSkill = decorateBefore(skill, skillEat); getSkill = decorateBefore(getSkill, skillSleep); getSkill(); // 睡觉 吃饭 说话
十三、适配器模式
是解决两个软件实体间的接口不兼容的问题,对不兼容的部分进行适配。
例如下面的数据类型转换的适配器
<script> // 限制只能传入数组 function render(data) { data.forEach((item) => { console.log(item); }); } // 数据格式适配器 function adapter(data) { if (typeof data !== "object") { // 数据不可迭代 return []; } // 如果是数组,直接返回 if (Object.prototype.toString.call(data) === "[Object Array]") { return data; } // 如果是对象,进行迭代,转换为数组 var temp = []; for (var item in data) { if (data.hasOwnProperty(item)) { temp.push(data[item]); } } return temp; } var data = { name: "ashen", age: 21, gender: "female", }; var str = "asharren"; var arr = ["一小", "三中", "一中"]; render(adapter(data)); render(adapter(str)); render(adapter(arr)); </script>