单例模式
限制类实例化次数只能一次,一个类只有一个实例,并提供一个访问它的全局访问点。
单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏览器中的window对象等。在JavaScript开发中,单例模式的用途同样非常广泛。试想一下,当我们单击登录按钮的时候,页面中会出现一个登录浮窗,而这个登录浮窗是唯一的,无论单击多少次登录按钮,这个浮窗都只会被创建一次,那么这个登录浮窗就适合用单例模式来创建。
使用场景 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function initBaiduMap ( ) { return new Promise (function (resolve, reject ) { if (typeof BMap !== 'undefined' ) { resolve (); return ; } const script = document .createElement ('script' ); script.type = 'text/javascript' ; script.onerror = function ( ) { reject (); }; script.onload = function ( ) { resolve (); }; script.src = `https://api.map.baidu.com/api?v=2.0&ak=ak` ; document .head .appendChild (script); }); }
策略模式
定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
就是把看似毫无联系的代码提取封装、复用,使之更容易被理解和拓展。常见的用于if判断、switch枚举、数据字典等流程判断语句中。
使用场景 使用策略模式计算等级 在游戏中,我们每玩完一局游戏都有对用户进行等级评价,比如S级4倍经验,A级3倍经验,B级2倍经验,其他1倍经验,用函数来表达如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 // 不使用任何模式 // 拓展性差,如果增加了一种新的等级C,或者想把S的经验系数改为5 则需要修改函数内容实现function getExperience (level, experience) { if (level == 'S') { return 4 * experience; } if (level == 'A' ) { return 3 * experience; } if (level == 'B' ) { return 2 * experience; } return experience; } // 策略模式 const strategy = { S: function (experience) { return 4 * experience; }, A: function (experience) { return 3 * experience; }, B: function (experience) { return 2 * experience; }, };function getExperience (strategy, level, experience) { return level in strategy ? strategy[level](experience) : experience ; } var s = getExperience(strategy, 'S' , 100 ); var a = getExperience(strategy, 'A' , 100 ); console.log(s, a); // 400 300
表单校验 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 <form action="http://xxx.com/register" id="registerForm" method="post" > 请输入用户名:<input type="text" name="userName" / ><br /> 请输入密码:<input type="text" name="password" / > <br /> 请输入手机号码:<input type="text" name="phoneNumber" / ><br /> <button > 提交</button > </form><script > var registerForm = document .getElementById ("registerForm" ) registerForm.onsubmit = function ( ) { console .log (registerForm.userName ) 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 } } </script > 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 } }, } class Validator { constructor ( ) { this .cache = [] } add (dom, rules ) { var self = this for (var i = 0 , rule; (rule = rules[i++]); ) { console .log (rule); ;(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) } } start ( ) { 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 位" , }, ]) validator.add (registerForm.phoneNumber , [ { strategy : "isMobile" , errorMsg : "手机号码格式不正确" , }, ]) var errorMsg = validator.start () return errorMsg } registerForm.onsubmit = function ( ) { var errorMsg = validataFunc () if (errorMsg) { alert (errorMsg) return false } }
代理模式
为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。
代理模式是一种非常有意义的模式,在生活中可以找到很多代理模式的场景。比如,明星都有经纪人作为代理。如果想请明星来办一场商业演出,只能联系他的经纪人。经纪人会把商业演出的细节和报酬都谈好之后,再把合同交给明星签。
在面向对象的编程中,代理模式的合理使用能够很好的体现下面两条原则:
单一职责原则 : 面向对象设计中鼓励将不同的职责分布到细粒度的对象中,Proxy 在原对象的基础上进行了功能的衍生而又不影响原对象,符合松耦合高内聚的设计理念。
开放-封闭原则 :代理可以随时从程序中去掉,而不用对其他部分的代码进行修改,在实际场景中,随着版本的迭代可能会有多种原因不再需要代理,那么就可以容易的将代理对象换成原对象的调用
使用场景 虚拟代理实现图片预加载 1 2 3 4 5 6 7 8 9 10 11 const myImage = (function () { const imgNode = document.createElement("img" ) ; document.body.appendChild(imgNode ) ; return { setSrc: function (src) { imgNode.src = src; }, }; })() ; myImage.setSrc('http :/ / pic .616pic.com / bg_w1180 / 00/ 09/ 75/ 6eISyvLqRL.jpg !/ fw / 1120')
在chrome控制台设置Slow 3G
,然后通过MyImage.setSrc给该img节点设置src,可以看到,在图片被加载好之前,页面中有一段长长的空白时间。
现在开始引入代理对象proxyImage,通过这个代理对象,在图片被真正加载好之前,页面中将出现一张占位的菊花图loading.gif,来提示用户图片正在加载。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const proxyImage = (function () { const img = new Image() ; img.onload = function () { myImage.setSrc(this .src ) ; }; return { setSrc: function (src) { myImage.setSrc('https :/ / media1 .giphy .com / media / 58Y1tQU8AAhna/ giphy .gif ') ; img.src = src; }, }; })() ; proxyImage.setSrc("http://pic.616pic.com/bg_w1180/00/09/75/6eISyvLqRL.jpg!/fw/1120" ) ;
缓存代理 缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 function factorial_recursion (n ) { if (n <= 1 ) return 1 ; return n * factorial_recursion(n - 1 ); } function factorial_recursion (n ) { console .log ("计算了哦" ); if (n <= 1 ) return 1 ; return n * factorial_recursion(n - 1 ); } var proxy = (function ( ) { const cache = {}; return function (key ) { if (key in cache) { return cache[key]; } else { return (cache[key] = factorial_recursion(key)); } }; })(); proxy(2 ); proxy(2 ); proxy(2 ); proxy(2 );
发布—订阅模式(观察者模式)
发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在JavaScript开发中,我们一般用事件模型来替代传统的发布—订阅模式。
实际上,只要我们曾经在DOM节点上面绑定过事件函数,那我们就曾经使用过发布—订阅模式。
1 2 3 4 5 6 7 8 // 在这里需要监控用户点击document.body 的动作,但是我们没办法预知用户将在什么时候点击。所以我们订阅document.body 上的click事件,当body 节点被点击时,body 节点便会向订阅者发布这个消息。 document.body .addEventListener( "click" , function () { alert(1); }, false );
使用场景 小明最近看上了一套房子,到了售楼处之后才被告知,该楼盘的房子早已售罄。好在售楼MM告诉小明,不久后还有一些尾盘推出,开发商正在办理相关手续,手续办好后便可以购买。小明离开之前,把电话号码留在了售楼处。售楼MM答应他,新楼盘一推出就马上发信息通知小明。小红、小强和小龙也是一样,他们的电话号码都被记在售楼处的花名册上,新楼盘推出的时候,售楼MM会翻开花名册,遍历上面的电话号码,依次发送一条短信来通知他们。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 ); var fns = this .clientList [key]; if (!fns || fns.length === 0 ) { return false ; } for (let i = 0 ; i < fns.length ; i++) { fns[i].apply (this , arguments ); } }, };var installEvent = function (obj ) { for (const key in event) { obj[key] = event[key]; } };var salesOffices = {};installEvent (salesOffices); salesOffices.listen ("小红" , function (price ) { console .log ("价格是" + price); }); salesOffices.listen ("小明" , function (price ) { console .log ("价格是" + price); }); salesOffices.trigger ("小红" , 2000 ); salesOffices.trigger ("小明" , 12000 );
比如现在有两个模块,a模块里面有一个按钮,每次点击按钮之后,b模块里的div中会显示按钮的总点击次数,我们用全局发布—订阅模式完成下面的代码,使得a模块和b模块可以在保持封装性的前提下进行通信。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <button id="btn" >click</button> <input type="text" id="input" />var a = (function ( ) { var count = 0 ; var btn = document.querySelector("button" ); btn.onclick = function ( ) { event.trigger("add" , count ++); }; })();var b = (function ( ) { var input = document.querySelector("input" ); event.listen("add" , function (count ) { input.value = count ; }); })();
设计原则和编程技巧 单一职责原则(SRP)
修改代码总是一件危险的事情,特别是当两个职责耦合在一起的时候,一个职责发生变化可能会影响到其他职责的实现,造成意想不到的破坏,这种耦合性得到的是低内聚和脆弱的设计。因此,SRP原则体现为:一个对象(方法)只做一件事情。
举个现实例子: 如果一个电视机内置了DVD机,当电视机坏了的时候,DVD机也没法正常使用,那么一个DVD发烧友通常不会选择这样的电视机。但如果我们的客厅本来就小得夸张,或者更在意DVD在使用上的方便,那让电视机和DVD机耦合在一起就是更好的选择。
1 2 3 4 5 6 7 8 9 const appendDiv = function (data ) { for (let i = 0 , l = data.length ; i < l; i++) { const div = document .createElement ('div' ); div.innerHTML = data[i]; document .body .appendChild (div); } };appendDiv ([1 , 2 , 3 , 4 , 5 , 6 ]);
这其实是一段很常见的代码,经常用于ajax请求之后,在回调函数中遍历ajax请求返回的数据,然后在页面中渲染节点。appendDiv函数本来只是负责渲染数据,但是在这里它还承担了遍历聚合对象data的职责。我们想象一下,如果有一天cgi返回的data数据格式从array变成了object,那我们遍历data的代码就会出现问题,必须改成for ( var i in data )的方式,这时候必须去修改appendDiv里的代码,否则因为遍历方式的改变,导致不能顺利往页面中添加div节点。
正确写法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 const each = function (obj, callback) { let value, i = 0 , length = obj.length, isArray = isArraylike(obj ) ; if (isArray) { for (; i < length; i++) { callback.call(obj[i ] , i, obj[i ] ); } } else { for (i in obj) { value = callback.call(obj[i ] , i, obj[i ] ); } } return obj; }; const appendDiv = function (data) { each(data, function (i, n) { const div = document.createElement('div ') ; div.innerHTML = n; document.body.appendChild(div ) ; }); }; appendDiv([1, 2, 3, 4, 5, 6]) ; appendDiv({ a : 1, b : 2, c : 3, d : 4 }) ;
SRP原则的优缺点 SRP原则的优点是降低了单个类或者对象的复杂度,按照职责把对象分解成更小的粒度,这有助于代码的复用,也有利于进行单元测试。当一个职责需要变更的时候,不会影响到其他的职责。但SRP原则也有一些缺点,最明显的是会增加编写代码的复杂度。当我们按照职责把对象分解成更小的粒度之后,实际上也增大了这些对象之间相互联系的难度。
最少知识原则(LKP)
最少知识原则要求我们在设计程序时,应当尽量减少对象之间的交互。如果两个对象之间不必彼此直接通信,那么这两个对象就不要发生直接的相互联系。常见的做法是引入一个第三者对象,来承担这些对象之间的通信作用。如果一些对象需要向另一些对象发起请求,可以通过第三者对象来转发这些请求。
最简单的外观模式应该是类似下面的代码:
1 2 3 4 5 const facade = function () { A() B() } facade()
开放-封闭原则(OCP) 1 2 3 4 5 6 7 所谓开放封闭原则就是软件实体应该对扩展开放,而对修改封闭。开放封闭原则是所有面向对象原则的核心。软件设计本身所追求的目标就是封装变化,降低耦合,而开放封闭原则正是对这一目标的最直接体现。 开放封闭原则主要体现在两个方面: 对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。 对修改封闭,意味着类一旦设计完成,就可以独立其工作,而不要对类尽任何修改。
在项目需求变迁的过程中,我们经常会找到相关代码,然后改写它们。这似乎是理所当然的事情,不改动代码怎么满足新的需求呢?想要扩展一个模块,最常用的方式当然是修改它的源代码。如果一个模块不允许修改,那么它的行为常常是固定的。然而,改动代码是一种危险的行为,也许我们都遇到过bug越改越多的场景。刚刚改好了一个bug,但是又在不知不觉中引发了其他的bug。此时就需要使用开放-封闭原则。
示例1:扩展window.onload函数 假如我们接到了一个新的需求,即在window.onload函数中打印出页面中的所有节点数量。这当然难不倒我们了。于是我们打开文本编辑器,搜索出window.onload函数在文件中的位置,在函数内部添加代码,如果是更复杂的需求呢。
如果目前的window.onload函数是一个拥有500行代码的巨型函数,里面密布着各种变量和交叉的业务逻辑,而我们的需求又不仅仅是打印一个log这么简单。那么“改好一个bug,引发其他bug”这样的事情就很可能会发生。我们永远不知道刚刚的改动会有什么副作用,很可能会引发一系列的连锁反应。
1 2 3 4 5 6 7 8 9 10 11 12 13 Function .prototype .after = function (afterfn ) { let _self = this ; return function ( ){ let ret = _self.apply (this , arguments ); afterfn.apply (this , arguments ); return ret; } }window .onload = (window .onload || function ( ) {}).after (function ( ) { console .log (document .getElementsByTagName ('*' ).length ); });
通过动态装饰函数的方式,我们完全不用理会从前window.onload函数的内部实现,无论它的实现优雅或是丑陋。就算我们作为维护者,拿到的是一份混淆压缩过的代码也没有关系。只要它从前是个稳定运行的函数,那么以后也不会因为我们的新增需求而产生错误。新增的代码和原有的代码可以井水不犯河水。