单例模式的定义是,保证一个类仅有一个实例,并提供一个访问它的全局访问点。javascript
有一些对象,好比线程池/全局缓存/浏览器中的 window
对象等等,咱们就只须要一个实例。html
下面将根据实际场景进行介绍。java
当咱们单击登陆按钮时,页面中会出现一个登陆的浮窗,而这个登陆浮窗是惟一的,不管单击多少次登陆按钮,这个浮窗都只会被建立一次,那么这个登陆浮窗就适合用单例模式来建立。git
传统作法在页面加载完成时,就建立好登陆浮窗,当用户点击登陆按钮时,显示登陆浮窗,实现代码以下:github
<button id="loginBtn">登陆</button>
var loginLayer = (() => { let div = document.createElement('div') div.innerHTML = '我是登陆弹窗' div.style.display = 'none' document.body.appendChild(div) return div })() document.getElementById('loginBtn').onclick = () => { loginLayer.style.display = 'block' }
上述代码有如下缺点:web
DOM
节点,浪费性能。如今优化一下,将代码改成,在用户点击登陆按钮后,才新增登陆浮窗的 DOM
节点。ajax
代码以下:算法
var createLoginLayer = () => { let div = document.createElement('div') div.innerHTML = '我是登陆弹窗' div.style.display = 'none' document.body.appendChild(div) return div } document.getElementById('loginBtn').onclick = () => { const loginLayer = createLoginLayer() loginLayer.style.display = 'block' }
上述代码也存在缺陷,具体以下:设计模式
DOM
节点更加浪费性能。实际上,咱们只须要建立一次登陆浮窗。浏览器
经过单例模式,重构上述代码。
const createLoginLayer = () => { const div = document.createElement('div') div.innerHTML = '我是登陆弹窗' div.style.display = 'none' console.log(123) document.body.appendChild(div) return div } const createSingle = (function () { var instance = {} return function (fn) { if (!instance[fn.name]) { instance[fn.name] = fn.apply(this, arguments) } return instance[fn.name] } })() const createIframe = function () { const iframe = document.createElement('iframe') document.body.appendChild(iframe) iframe.style.display = 'none' return iframe } const createSingleLoginLayer = createSingle(createLoginLayer) const createSingleIframe = createSingle(createIframe) document.getElementById('loginBtn').onclick = () => { const loginLayer = createSingleLoginLayer const iframe = createSingleIframe loginLayer.style.display = 'block' iframe.style.display = 'block' }
通过重构,代码作了如下优化:
createLoginLayer
/ createIframe
的职责和管理单例对象 createSingle
的职责分离,符合单一职责原则;iframe
/ script
等其余标签时,能够直接复用该逻辑。单例模式是一种简单但很是实用的模式,特别是惰性单例技术,在合适的时候才建立对象,而且只建立惟一的一个。更奇妙的是,建立对象和管理单例的职责被分布在两个不一样的方法中,这两个方法组合起来才具备单例模式的威力。
当咱们计划国庆出去游玩时,在交通方式上,咱们能够选择贵而快的飞机、价格中等但稍慢的动车、便宜但超级慢的火车,根据不一样的人,选择对应的交通方式,且能够随意更换交通方式,这就是策略模式。
策略模式的定义是,定义一系列算法,把它们一个个封装起来,而且使它们能够相互替换。
有一个计算员工年终奖的需求,假设,绩效为 S
的员工年终奖是 4
倍工资,绩效为 A
的员工年终奖是 3
倍工资,绩效为 B
的员工年终奖是 2
倍工资,下面咱们来计算员工的年终奖。
var calculateBonus = function(performanceLevel, salary) { if (performanceLevel === 'S') { return salary * 4; } if (performanceLevel === 'A') { return salary * 3; } if (performanceLevel === 'B') { return salary * 2; } }; calculateBonus('B', 20000); // 输出:40000 calculateBonus( 'S', 6000 ); // 输出:24000
上述代码有如下缺点:
if-else
语句描述逻辑,代码庞大;S
的奖金系数,必须修改 calculateBonus
函数,违反了开放-封闭原则;使用策略模式改良后
const strategies = { S: salary => { return salary * 4 }, A: salary => { return salary * 3 }, B: salary => { return salary * 2 } } const calculateBonus = (level, salary) => { return strtegies[level](salary) } console.log(calculateBonus('s', 20000)) console.log(calculateBonus('a', 10000))
能够看到上述代码作了如下改动:
strategies
封装了具体的算法和计算过程(每种绩效的计算规则);calculateBonus
接受请求,把请求委托给策略类 strategies
(员工的绩效和工资;if-else
语句。策略模式使代码可读性更高,易于拓展更多的策略算法。当绩效系数改变,或者绩效等级增长,咱们只须要为 strategies
调整或新增算法,符合开放-封闭原则。
当网页上的表单须要校验输入框/复选框等等规则时,如何去实现呢?
如今有一个注册用户的表单需求,在提交表单以前,须要验证如下规则:
使用 if-else
语句判断表单输入是否符合对应规则,如不符合,提示错误缘由。
<!DOCTYPE html> <html> <head> <title></title> </head> <body> <form id='registerForm' action="xxx" method="post"> 用户名:<input type="text" name="userName"> 密码:<input type="text" name="password"> 手机号:<input type="text" name="phone"> <button>提交</button> </form> <script type="text/javascript"> let registerForm = document.getElementById('registerForm') registerForm.onsubmit = () => { if (registerForm.userName.value) { alert('用户名不能为空') return false } if (registerForm.password.value.length < 6) { alert('密码长度不能少于6') return false } if (!/(^1[3|5|8][0-9]$)/.test(registerForm.phone.value)) { alert('手机号码格式不正确') return false } } </script> </body> </html>
上述代码有如下缺点:
onsubmit
函数庞大,包含大量 if-else
语句;onsubmit
缺少弹性,当有规则须要调整,或者须要新增规则时,须要改动 onsubmit
函数内部,违反开放-封闭原则;使用策略模式重构上述代码。
<!DOCTYPE html> <html> <head> <title></title> </head> <body> <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> <script type="text/javascript" src="index.js"> </script> </body> </html>
// 表单dom const registerForm = document.getElementById('registerForm') // 表单规则 const rules = { userName: [ { strategy: 'isNonEmpty', errorMsg: '用户名不能为空' }, { strategy: 'minLength:10', errorMsg: '用户名长度不能小于10位' } ], password: [ { strategy: 'minLength:6', errorMsg: '密码长度不能小于6位' } ], phoneNumber: [ { strategy: 'isMobile', errorMsg: '手机号码格式不正确' } ] } // 策略类 var strategies = { isNonEmpty: function(value, errorMsg) { if (value === '') { return errorMsg; } }, minLength: function(value, errorMsg, length) { console.log(length) if (value.length < length) { return errorMsg; } }, isMobile: function(value, errorMsg) { if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) { return errorMsg; } } }; // 验证类 const Validator = function () { this.cache = [] } // 添加验证方法 Validator.prototype.add = function ({ dom, rules}) { rules.forEach(rule => { const { strategy, errorMsg } = rule console.log(rule) const [ strategyName, strategyCondition ] = strategy.split(':') console.log(strategyName) const { value } = dom this.cache.push(strategies[strategyName].bind(dom, value, errorMsg, strategyCondition)) }) } // 开始验证 Validator.prototype.start = function () { let errorMsg this.cache.some(cacheItem => { const _errorMsg = cacheItem() if (_errorMsg) { errorMsg = _errorMsg return true } else { return false } }) return errorMsg } // 验证函数 const validatorFn = () => { const validator = new Validator() console.log(validator.add) Object.keys(rules).forEach(key => { console.log(2222222, rules[key]) validator.add({ dom: registerForm[key], rules: rules[key] }) }) const errorMsg = validator.start() return errorMsg } // 表单提交 registerForm.onsubmit = () => { const errorMsg = validatorFn() if (errorMsg) { alert(errorMsg) return false } return false }
上述代码经过 strategies
定义规则算法,经过 Validator
定义验证算法,将规则和算法分离,咱们仅仅经过配置的方式就能够完成表单的校验,这些校验规则也能够复用在程序的任何地方,还能做为插件的形式,方便的被移植到其余项目中。
策略模式是一种经常使用且有效的设计模式,经过上述例子,能够总结出策略模式的一些优势:
Context
拥有执行算法的能力,这也是继承的一种更轻便的代替方案。代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。
代理模式的关键是,当客户不方便直接访问一个对象或者不知足须要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。
传统作法是小明直接把花送给小白,小白接收到花,代码以下:
const Flower = function () { return '玫瑰🌹' } const xiaoming = { sendFlower: target => { const flower = new Flower() target.receiveFlower(flower) } } const xiaobai = { receiveFlower: flower => { console.log('收到花', flower) } } xiaoming.sendFlower(xiaobai)
可是,小明并不认识小白,他想要经过小代,帮他打探小白的状况,在小白心情好的时候送花,这样成功率更高。代码以下:
const Flower = function () { return '玫瑰🌹' } const xiaoming = { sendFlower: target => { const flower = new Flower() target.receiveFlower(flower) } } const xiaodai = { receiveFlower: flower => { xiaobai.listenGoodMood().then(() => { xiaobai.receiveFlower(flower) }) } } const xiaobai = { receiveFlower: flower => { console.log('收到花', flower) }, listenGoodMood: fn => { return new Promise((reslove, reject) => { // 10秒后,心情变好 reslove() }) } } xiaoming.sendFlower(xiaodai)
以上,小明经过小代,监听到小白心情的心情变化,选择在小白心情好时送花给小白。不只如此,小代还能够作如下事情:
图片预加载时一种常见的技术,若是直接给 img 标签节点设置 src 属性,因为图片过大或网络不佳,图片的位置每每有一段时间时空白。
const myImage = (() => { const imgNode = document.createElement('img') document.body.appendChild(imgNode) return { setSrc: src => { imgNode.src = src } } })() myImage.setSrc('https://img30.360buyimg.com/ling/jfs/t1/187775/5/8271/435193/60c8117eE7d79ef41/1d21db2c4dca9a90.png')
经过开发者工具把网速设置为 5kb/s 时,会发如今很长一段时间内,图片位置是空白的。
下面用虚拟代理优化该功能,把加载图片的操做交给代理函数完成,在图片加载时,先用一张loading 图占位,当图片加载成功后,再把它填充进 img 节点。
代码以下:
const myImage = (() => { const imgNode = document.createElement('img') document.body.appendChild(imgNode) return { setSrc: src => { imgNode.src = src } } })() const loadingSrc = '../../../../img/loading.gif' const imgSrc = 'https://img30.360buyimg.com/ling/jfs/t1/187775/5/8271/435193/60c8117eE7d79ef41/1d21db2c4dca9a90.png' const proxyImage = (function () { const img = new Image() img.onload = () => { myImage.setSrc(img.src) } return { setSrc: src => { myImage.setSrc(loadingSrc) img.src = src } } })() proxyImage.setSrc(imgSrc)
上述代码有如下优势:
proxyImage
控制了对 MyImage
的访问,在 MyImage
未加载成功以前,使用 loading
图占位;img
节点设置 src
的函数 MyImage
,预加载图片的函数 proxyImage
,都只有一个职责;img
节点设置 src
和预加载图片的功能,被隔离在两个对象里,它们能够各自变化不影响对方。假设咱们要实现一个同步文件的功能,经过复选框,当复选框选中的时候,将该复选框对应的 id 传给服务器,告诉服务器须要同步 id 对应的文件。
思考一下,会发现,若是每选中一个复选框,就请求一次接口,假设 1s 内选中了 10 个复选框,那么就要发送 10 次请求。
能够经过虚拟代理来优化上述作法,新增一个代理,帮助复选框发起同步文件的请求,收集在这 1s 内的请求,1s 后再一块儿把这些文件 id 发送到服务器。
代码以下:
<!DOCTYPE html> <html> <meta charset="utf-8" /> <head> <title></title> </head> <body> a <input type="checkbox" value="a" /> b <input type="checkbox" value="b" /> c <input type="checkbox" value="c" /> d <input type="checkbox" value="d" /> <script type="text/javascript" src="index.js"> </script> </body> </html>
const synchronousFile = cache => { console.log('开始同步文件,id为:'+ cache.join('/')) } const proxySynchronousFile = (() => { const cache = [] let timer return id => { console.log(id) cache.push(id) if (timer) { return } timer = setTimeout(() => { synchronousFile(cache) clearTimeout(timer) timer = null cache.length = 0 }, 2000) } })() const checkbox = document.getElementsByTagName('input') Array.from(checkbox).forEach(i => { console.log(i) i.onclick = () => { if (i.checked) { proxySynchronousFile(i.value) } } })
在列表须要分页时,同一页的数据理论上只须要去后台拉取一次,能够把这些拉取过的数据缓存下来,下次请求时直接使用缓存数据。
使用缓存代理实现上述功能,代码以下:
(async function () { function getArticle (currentPage, pageSize) { console.log('getArticle', currentPage, pageSize) // 模拟一个ajax请求 return new Promise((resolve, reject) => { resolve({ ok: true, data: { list: [], total: 10, params: { currentPage, pageSize } } }) }) } const proxyGetArticle = (() => { const caches = [] return async (currentPage, pageSize) => { const cache = Array.prototype.join.call([currentPage, pageSize],',') if (cache in caches) { return caches[cache] } const { data, ok } = await getArticle(currentPage, pageSize) if (ok) { caches[cache] = data } return caches[cache] } })() // 搜索第一页 await proxyGetArticle(1, 10) // 搜索第二页 await proxyGetArticle(2, 10) // 再次搜索第一页 await proxyGetArticle(1, 10) })()
经过缓存代理,在第二次请求第一页的数据时,直接在缓存数据中拉取,无须再次从服务器请求数据。
上面根据实际场景介绍了虚拟代理和缓存代理的作法。
当咱们不方便直接访问某个对象时,找一个代理方法帮咱们去访问该对象,这就是代理模式。
可经过 github源码 进行实操练习。
但愿本文能对你有所帮助,感谢阅读❤️~
欢迎关注凹凸实验室博客:aotu.io
或者关注凹凸实验室公众号(AOTULabs),不定时推送文章。