最近开始给本身每周订个学习任务,学习结果反馈为一篇文章的输出,作好学习记录。
这一周(02.25-03.03)我定的目标是《JavaScript 模式》的第七章学习一遍,学习结果的反馈就是本篇文章啦。
因为内容实在太长,我将本文分为两部分:前端
本文内容中主要参考《JavaScript 模式》,其中也有些案例是来自网上资料,有备注出处啦,如形成不便,请联系我删改。 git
过两天我会把这篇文章收录到我整理的知识库 【Cute-JavaScript】 中,并已经同步到 【github】上面。github
单体模式(Singleton Pattern)的思想在于保证一个特定类仅有一个实例,即无论使用这个类建立多少个新对象,都会获得与第一次建立的对象彻底相同。 算法
它让咱们能将代码组织成一个逻辑单元,并能够经过单一变量进行访问。 segmentfault
单体模式有如下优势:设计模式
但在JavaScript中没有类,只有对象。当咱们建立一个新对象,它都是个新的单体,由于JavaScript中永远不会有彻底相等的对象,除非它们是同一个对象。
所以,咱们每次使用对象字面量建立对象的时候,实际上就是在建立一个单例。数组
let a1 = { name : 'leo' }; let a2 = { name : 'leo' }; a1 === a2; // false a1 == a2; // false
这里须要注意,单体模式有个条件,是该对象能被实例化,好比下面这样就不是单体模式,由于它不能被实例化:微信
let a1 = { b1: 1, b2: 2, m1: function(){ return this.b1; }, m2: function(){ return this.b2; } } new a1(); // Uncaught TypeError: a1 is not a constructor
下面展现一个单体模式的基本结构:闭包
let Singleton = function (name){ this.name = name; this.obj = null; } Singleton.prototype.getName = function(){ return this.name; } function getObj(name){ return this.obj || (this.obj = new Singleton(name)); } let g1 = getObj('leo'); let g2 = getObj('pingan'); g1 === g2; // true g1 == g2; // true g1.getName(); // 'leo' g2.getName(); // 'leo'
从这里能够看出,单体模式只能实例化一次,后面再调用的话,都是使用第一次实例化的结果。app
单例模式只容许实例化一次,能提升对象访问速度而且节约内存,一般被用于下面场景:
这里咱们要用单体模式,建立一个弹框,大概须要实现:元素值建立一次,使用的时候直接调用。
所以咱们这么作:
let create = (() => { let div; return () => { if(!div){ div = document.createElement('div'); div.innderHTML = '我是leo建立的弹框'; div.style.display = 'none'; div.setAttribute("id", "leo"); document.body.appendChild(div); } return div; } })(); // 触发事件 document.getElementById('otherBtn').onclick = () => { let first = create(); first.style.display = 'block'; }
因为JavaScript中没有类,但JavaScript有new
语法来用构造函数建立对象,并可使用这种方法实现单体模式。
当使用同一个构造函数以new
操做符建立多个对象,得到的是指向彻底相同的对象的新指针。
一般咱们使用new
操做符建立单体模式的三种选择,让构造函数总返回最初的对象:
function Leo(name){ if(typeof Leo.obj === 'object'){ return Leo.obj; } this.name = name; Leo.obj = this; return this; } let a1 = new Leo('leo'); let a2 = new Leo('pingan'); a1 === a2 ; // true a1 == a2 ; // true
惟一的缺点就是obj
属性是公开的,容易被修改。
咱们这经过重写上面的方法,加入闭包:
function Leo(name){ let obj; this.name = name; obj = this; // 1.存储第一次建立的对象 Leo = function(){ // 2.修改原来的构造函数 return obj; } } let a1 = new Leo('leo'); let a2 = new Leo('pingan'); a1 === a2 ; // true a1 == a2 ; // true
当咱们第一次调用构造函数,像往常同样返回this,然后面再调用的话,都将重写构造函数,并访问私有变量obj
并返回。
工厂模式的目的在于建立对象,实现下列目标:
经过工厂方法(或类)建立的对象,都继承父对象,下面一个简单工厂方法理解:
function Person(name, age, sex){ let p = {}; // 或 let p = new Object(); 建立一个初始对象 p.name = name; p.age = age; p.sex = sex; p.ask = function(){ return 'my name is' + this.name; } return p; } let leo = new Person('leo', 18, 'boy'); let pingan = new Person('pingan', 18, 'boy'); console.log(leo.name, leo.age, leo.sex); // 'leo', 18, 'boy' console.log(pingan.name, pingan.age, pingan.sex); // 'pingan', 18, 'boy'
经过调用Person
构造函数,咱们能够像工厂那样,生产出无数个包含三个属性和一个方法的对象。
能够看出,工厂模式能够解决建立多个相似对象的问题。
每次增长一个产品时,都须要增长一个具体类和对象实现工厂,使得系统中类的个数成倍增长,在必定程度上增长了系统的复杂度,同时也增长了系统具体类的依赖。这并非什么好事。
在复杂工厂模式中,咱们将其成员对象的实列化推迟到子类中,子类能够重写父类接口方法以便建立的时候指定本身的对象类型。
父类相似一个公共函数,只处理建立过程当中的问题,而且这些处理将被子类继承,而后在子类实现专门功能。
好比这里咱们须要实现这么一个实例:
CarMaker
;CarMaker
有个factor
静态方法,用于建立car
对象;CarMaker
;而后咱们但愿这么使用这个函数:
let c1 = CarMaker.factory('Car1'); let c2 = CarMaker.factory('Car2'); let c3 = CarMaker.factory('Car3'); c1.drirve(); // '个人编号是6' c2.drirve(); // '个人编号是3' c3.drirve(); // '个人编号是12'
能够看出,调用时接收以字符串形式指定类型,并返回请求类型的对象,而且这样使用是不须要用new
操做符。
下面看代码实现:
// 建立父构造函数 function CarMaker(){}; CarMaker.prototype.drive = function(){ return `个人编号是${this.id}`; } // 添加静态工厂方法 CarMaker.factory = function (type){ let types = type, newcar; // 若构造函数不存在 则发生错误 if(typeof CarMaker[types] !== 'function'){ throw{ name: 'Error', message: `${types}不存在`}; } // 若构造函数存在,则让原型继承父类,但仅继承一次 if(CarMaker[types].prototype.drive !== 'function'){ CarMaker[types].prototype = new CarMaker(); } // 建立新实例,并返回 newcar = new CarMaker[types](); return newcar; } // 调用 CarMaker.c1 = function(){ this.id = 6; } CarMaker.c2 = function(){ this.id = 3; } CarMaker.c3 = function(){ this.id = 12; }
定义完成后,咱们再执行前面的代码:
let c1 = CarMaker.factory('Car1'); let c2 = CarMaker.factory('Car2'); let c3 = CarMaker.factory('Car3'); c1.drirve(); // '个人编号是6' c2.drirve(); // '个人编号是3' c3.drirve(); // '个人编号是12'
就能正常打印结果了。
实现该工厂模式并不困难,主要是要找到可以穿件所需类型对象的构造函数。
这里使用简单的映射来建立该对象的构造函数。
内置的对象工厂,就像全局的Object()
构造函数,也是工厂模式的行为,根据输入类型建立不一样对象。
如传入一个原始数字,返回一个Number()
构造函数建立一个对象,传入一个字符串或布尔值也成立。
对于传入任何其余值,包括无输入的值,都会建立一个常规的对象。
不管是否使用new
操做符,均可以调用Object()
,咱们这么测试:
let a = new Object(), b = new Object(1), c = Object('1'), d = Object(true); a.constructor === Object; // true b.constructor === Number; // true c.constructor === String; // true d.constructor === Boolean; // true
事实上,Object()
用途不大,这里列出来是由于它是咱们比较常见的工厂模式。
迭代器模式(Iterator Pattern)是提供一种方法,顺序访问一个聚合对象中每一个元素,而且不暴露该对象内部。
这种模式属于行为型模式,有如下几个特色:
在迭代器模式中,一般包含有一个包含某种数据集合的对象,须要提供一种简单的方法来访问每一个元素。
这里对象须要提供一个next()
方法,每次调用都必须返回下一个连续的元素。
这里假设建立一个对象leo
,咱们经过调用它的next()
方法访问下一个连续的元素:
let obj; while(obj = leo.next()){ // do something console.log(obj); }
另外迭代器模式中,聚合对象还会提供一个更为渐变的hasNext()
方法,来检查是否已经到达数据末尾,咱们这么修改前面的代码:
while(leo.hasNext()){ // do something console.log(obj); }
因为迭代器模式将存储数据和遍历数据的职责分离,增长新的聚合类须要对应增长新的迭代器类,类的个数成对增长,这在必定程度上增长了系统的复杂性。
根据上面的介绍,咱们这里实现一个简单案例,将设咱们数据只是普通数组,而后每次检索,返回的是间隔一个的数组元素(即不是连续返回):
let leo = (function(){ let index = 0, data = [1, 2, 3, 4, 5], len = data.length; return { next: function(){ let obj; if(!this.hasNext()){ return null; }; obj = data[index]; index = index + 2; return obj; }, hasNext: function(){ return index < len; } } })()
而后咱们还要给它提供更简单的访问方式和屡次迭代数据的能力,咱们须要添加下面两个方法:
rewind()
重置指针到初始位置;current()
返回当前元素,由于当指针步前进时没法使用next()
操做;代码变成这样:
let leo = (function(){ //.. return { // .. rewind: function(){ index = 0; }, current: function(){ return data[index]; } } })();
这样这个案例就完整了,接下来咱们来测试:
// 读取记录 while(leo.hasNext()){ console.log(leo.next()); }; // 打印 1 3 5 // 回退 leo.rewind(); // 获取当前 console.log(leo.current()); // 回到初始位置,打印1
迭代器模式一般用于:对于集合内部结果经常变化各异,咱们不想暴露其内部结构的话,但又响让客户代码透明底访问其中的元素,这种状况下咱们可使用迭代器模式。
简单理解:遍历一个聚合对象。
jQuery中的$.each()
方法,可让咱们传入一个方法,实现对全部项的迭代操做:
$.each([1,2,3,4,5],function(index, value){ console.log(`${index}: ${value}`) })
each()
方法let myEach = function(arr, callback){ for(var i = 0; i< arr.length; i++){ callback(i, arr[i]); } }
迭代器模式是一种相对简单的模式,目前绝大多数语言都内置了迭代器。并且迭代器模式也是很是经常使用,有时候不经意就是用了。
装饰者模式(Decorator Pattern):在不改变原类和继承状况下,动态添加功能到对象中,经过包装一个对象实现一个新的具备原对象相同接口的新对象。
装饰者模式有如下特色:
实际上,装饰着模式的一个比较方便的特征在于其预期行为的可定制和可配置特性。从只有基本功能的普通对象开始,不断加强对象的一些功能,并按照顺序进行装饰。
咱们这里实现一个基本对象sale
,能够经过sale
对象获取不一样项目的价格,并经过调用sale.getPrice()
方法返回对应价格。而且在不一样状况下,用额外的功能来装饰它,会获得不一样状况下的价格。
这里咱们假设客户须要支付国家税和省级税。按照装饰者模式,咱们就须要使用国家税和省级税两个装饰者来装饰这个sale
对象,而后在对使用价格格式化功能的装饰者装饰。实际看起来是这样:
let sale = new Sale(100); sale = sale.decorate('country'); sale = sale.decorate('privince'); sale = sale.decorate('money'); sale.getPrice();
使用装饰者模式后,每一个装饰都很是灵活,主要根据其装饰者顺序,因而若是客户不须要上缴国家税,代码就能够这么实现:
let sale = new Sale(100); sale = sale.decorate('privince'); sale = sale.decorate('money'); sale.getPrice();
接下来咱们须要考虑的是如何实现Sale
对象了。
实现装饰者模式的其中一个方法是使得每一个装饰者成为一个对象,而且该对象包含了应该被重载的方法。每一个装饰者实际上继承了目前已经被前一个装饰者进行装饰后的对象,每一个装饰方法在uber
(继承的对象)上调用一样的方法并获取值,此外还继续执行一些操做。
uber
关键字相似Java的super
,它可让某个方法调用父类的方法,uber
属性指向父类原型。
即:当咱们调用sale.getPrice()
方法时,会调用money
装饰者的方法,而后每一个装饰方法都会先调用父对象的方法,所以一直往上调用,直到开始的Sale
构造函数实现的未被装饰的getPrice()
方法。理解以下图:
咱们这里能够先实现构造函数Sale()
和原型方法getPrice()
:
function Sale (price){ this.price = price || 100; } Sale.prototype.getPrice = function (){ return this.price; }
而且装饰者对象都将以构造函数的属性来实现:
Sale.decorators = {};
接下来实现country
这个装饰者并实现它的getPrice()
,改方法首先从父对象的方法获取值再作修改:
Sale.decorators.country = { getPrice: function(){ let price = this.uber.getPrice(); // 获取父对象的值 price += price * 5 / 100; return price; } }
按照相同方法,实现其余装饰者:
Sale.decorators.privince = { getPrice: function(){ let price = this.uber.getPrice(); price += price * 7 / 100; return price; } } Sale.decorators.money = { getPrice: function(){ return "¥" + this.uber.getPrice().toFixed(2); } }
最后咱们还须要实现前面的decorate()
方法,它将咱们全部装饰者拼接一块儿,而且作了下面的事情:
建立了个新对象newobj
,继承目前咱们所拥有的对象(Sale
),不管是原始对象仍是最后装饰后的对象,这里就是对象this
,并设置newobj
的uber
属性,便于子对象访问父对象,而后将全部装饰者的额外属性复制到newobj
中,返回newobj
,即成为更新的sale
对象:
Sale.prototype.decorate = function(decorator){ let F = function(){}, newobj, overrides = this.constructor.decorators[decorator]; F.prototype = this; newobj = new F(); newobj.user = F.prototype; for(let k in overrides){ if(overrides.hasOwnProperty(k)){ newobj[k] = overrides[k]; } } return newobj; }
这里咱们使用列表实现相同功能,这个方法利用JavaScript语言的动态性质,而且不须要使用继承,也不须要让每一个装饰方法调用链中前面的方法,能够简单的将前面方法的结果做为参数传递给下一个方法。
这样实现也有个好处,支持反装饰或撤销装饰,咱们仍是实现如下功能:
let sale = new Sale(100); sale = sale.decorate('country'); sale = sale.decorate('privince'); sale = sale.decorate('money'); sale.getPrice();
如今的Sale()
构造函数中多了个装饰者列表的属性:
function Sale(price){ this.price = (price > 0) || 100; this.decorators_list = []; }
而后仍是须要实现Sale.decorators
,这里的getPrice()
将变得更简单,也没有去调用父对象的getPrice()
,而是将结果做为参数传递:
Sale.decorators = {}; Sale.decorators.country = { getPrice: function(price){ return price + price * 5 / 100; } } Sale.decorators.privince = { getPrice: function(price){ return price + price * 7 / 100; } } Sale.decorators.money = { getPrice: function(price){ return "¥" + this.uber.getPrice().toFixed(2); } }
而这时候父对象的decorate()
和getPrice()
变得复杂,decorate()
用于追加装饰者列表,getPrice()
须要完成包括遍历当前添加的装饰者一级调用每一个装饰者的getPrice()
方法、传递从前一个方法得到的结果:
Sale.prototype.decorate = function(decorators){ this.decorators_list.push(decorators); } Sale.propotype.getPrice = function(){ let price = this.price, name; for(let i = 0 ;i< this.decorators_list.length; i++){ name = this.decorators_list[i]; price = Sale.decorators[name].getPrice(price); } return price; }
很显然,第二种列表实现方法会更简单,不用设计继承,而且装饰方法也简单。
案例中getPrice()
是惟一能够装饰的方法,若是想实现更多能够被装饰的方法,咱们能够抽一个方法,来将每一个额外的装饰方法重复遍历装饰者列表中的这块代码,经过它来接收方法并使其成为“可装饰”的方法。这样实现,sale
的decorators_list
属性会成为一个对象,且该对象每一个属性都是以装饰者对象数组中的方法和值命名。
策略模式(Strategy Pattern):封装一系列算法,支持咱们在运行时,使用相同接口,选择不一样算法。它的目的是为了将算法的使用与算法的实现分离开来。
策略模式一般会有两部分组成,一部分是策略类,它负责实现通用的算法,另外一部分是环境类,它用户接收客户端请求并委托给策略类。
咱们能够很简单的将策略和算法直接作映射:
let add = { "add3" : (num) => num + 3, "add5" : (num) => num + 5, "add10": (num) => num + 10, } let demo = (type, num) => add[type](num); console.log(demo('add3', 10)); // 13 console.log(demo('add10', 12)); // 22
而后咱们再把每一个策略的算法抽出来:
let fun3 = (num) => num + 3; let fun5 = (num) => num + 5; let fun10 = (num) => num + 10; let add = { "add3" : (num) => fun3(num), "add5" : (num) => fun5(num), "add10": (num) => fun10(num), } let demo = (type, num) => add[type](num); console.log(demo('add3', 10)); // 13 console.log(demo('add10', 12)); // 22
咱们须要使用策略模式,实现一个处理表单验证的方法,不管表单的具体类型是什么都会调用验证方法。咱们须要让验证器能选择最佳的策略来处理任务,并将具体的验证数据委托给适当算法。
咱们假设须要验证下面的表单数据的有效性:
let data = { name : 'pingan', age : 'unknown', nickname: 'leo', }
这里须要先配置验证器,对表单数据中不一样的数据使用不一样的算法:
validator.config = { name : 'isNonEmpty', age : 'isNumber', nickname: 'isAlphaNum', }
而且咱们须要将验证的错误信息打印到控制台:
validator.validate(data); if(validator.hasErrors()){ console.log(validator.msg.join('\n')); }
接下来咱们才要实现validator
中具体的验证算法,他们都有一个相同接口validator.types
,提供validate()
方法和instructions
帮助信息:
// 非空值检查 validator.types.isNonEmpty = { validate: function(value){ return value !== ''; } instructions: '该值不能为空' } // 数值类型检查 validator.types.isNumber = { validate: function(value){ return !isNaN(value); } instructions: '该值只能是数字' } // 检查是否只包含数字和字母 validator.types.isAlphaNum = { validate: function(value){ return !/[^a-z0-9]/i.test(value); } instructions: '该值只能包含数字和字母,且不包含特殊字符' }
最后就是要实现最核心的validator
对象:
let validator = { types: {}, // 全部可用的检查 msg:[], // 当前验证的错误信息 config:{}, // 验证配置 validate: function(data){ // 接口方法 let type, checker, result; this.msg = []; // 清空错误信息 for(let k in data){ if(data.hasOwnProperty(k)){ type = this.config[k]; checker = this.types[type]; if(!type) continue; // 不存在类型 则 不须要验证 if(!checker){ throw { name: '验证失败', msg: `不能验证类型:${type}` } } result = checker.validate(data[k]); if(!result){ this.msg.push(`无效的值:${k},${checker.instructions}`); } } } return this.hasErrors(); } hasErrors: function(){ return this.msg.length != 0; } }
总结这个案例,咱们能够看出validator
对象是通用的,须要加强validator
对象的方法只需添加更多的类型检查,后续针对每一个新的用例,只需配置验证器和运行validator()
方法就能够。
平常开发的时候,仍是须要根据实际状况来选择设计模式,而不能为了设计模式而去设计模式。经过上面的学习,咱们使用策略模式来避免多重条件判断,而且经过开闭原则来封装方法。咱们应该多在开发中,逐渐积累本身的开发工具库,便于之后使用。
Author | 王平安 |
---|---|
pingan8787@qq.com | |
博 客 | www.pingan8787.com |
微 信 | pingan8787 |
每日文章推荐 | https://github.com/pingan8787... |
JS小册 | js.pingan8787.com |
微信公众号 | 前端自习课 |