观感度:🌟🌟🌟🌟🌟javascript
口味:话梅排骨css
烹饪时间:20min
html
有人问我坚持写博客的缘由是什么?前端
借用著名小说家斯蒂芬·金
的一句话:“开始写吧!年轻人。”vue
当你把消费级兴趣
升级为生产型兴趣
时,你才会渐渐发现之前没有窥见的门道和妙处。java
咳咳,天凉了请喝鸡汤~node
可能本系列文章中所讲的设计模式你在工做中常常应用它们,可是并不知道它们的名字。jquery
一旦咱们将这些设计模式整理学习并融会贯通后,即可以大大加强咱们的编程功底,在遇到实际业务需求时,给咱们提供更好的解决问题的思路。webpack
毕竟咱们的口号是:不加班!es6
书接上文,本文给你们介绍的是结构型设计模式
,包括外观模式
、适配器模式
、代理模式
、装饰者模式
、桥接模式
、组合模式
以及享元模式
。
概念:能够对复杂的子系统接口提供一个更高级的统一接口,对底层结构兼容性作统一封装来简化用户使用。
这种模式比较简单也比较容易理解,在平常的开发中你必定遇到过如下的场景。
那些年咱们对ie浏览器作过的兼容。。
这些常见的兼容方案属于外观模式。
var getEvent = function (event) {
return event || window.event;
}
var getTarget = function (event) {
var event = getEvent(event);
return event.target || event.srcElement;
}
var preventDefault = function (event) {
var event = getEvent(event);
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
}
var stopBubble = function (event) {
var event = getEvent(event);
if (event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}
}
复制代码
在团队开发中,为了不添加点击事件时出现的重名覆盖问题,咱们会使用DOM2级事件处理程序绑定事件,并添加对浏览器的兼容。
function addEvent (dom, type, fn) {
if (dom.addEventListener) {
dom.addEventListener (type, fn, false);
} else if (dom.attachEvent) {
// 兼容IE(低于9)
dom.attachEvent('on' + type, fn);
} else {
dom['on' + type] = fn;
}
}
复制代码
你固然也能够经过外观模式,来封装多个功能来简化底层操做方法。
var T = {
g : function (id) {
return document.getElementById(id);
},
css : function (id, key, value) {
document.getElementById(id).style[key] = value;
},
attr : function (id, key, value) {
document.getElementById(id)[key] = value;
},
html : function (id, html) {
document.getElementById(id).innerHTML = html;
},
on : function (id, type, fn) {
document.getElementById(id)['on' + type] = fn;
}
};
T.css('box', 'background', 'red');
T.attr('box', 'className', 'box');
T.html('box', '这是新添加的内容');
T.on('box', 'click', function() {
T.css('box', 'width', '500px');
})
复制代码
外观模式对接口进行了包装,咱们使用时无须知道接口实现的具体细节,按照规则使用便可。
概念:将一个类(对象)的接口(方法或者属性)转化成另一个接口,以知足用户需求,使类(对象)之间接口的不兼容问题经过适配器得以解决。
适配器在咱们的平常生活中很常见,好比出国旅行时,有的国家只有三项的插座,这时候咱们须要三项转两项插头电源适配器。再好比iPhone7之后耳机接口变成了lightning接口,为了适配圆孔耳机苹果为咱们提供了适配器。
// 为两个代码库所写的代码兼容运行而书写的额外代码是适配器的一种。
window.A = A = jQuery;
复制代码
jQuery.fn.css()
jQuery核心cssHook
// (为了控制文数,此处不贴代码,阅读完后你们可自行去官网查看)
复制代码
function doSomeThing(name, title, age, color, size, prize) {}
// 咱们记住这些参数的顺序是很困难的,因此咱们常常是以一个参数对象方式来传入
/** * obj.name : name * obj.title : title * obj.age : age * obj.color : color * obj.size : size * obj.prize : prize **/
// 当调用它的时候咱们要考虑到传递的参数是否完整的问题,若是有一些必须参数没有传入
// 一些参数是有默认值的等等。这时咱们能够用适配器来适配传入的参数对象
function doSomeThing (obj) {
var _adapter = {
name : '前端食堂',
title : '设计模式',
age : 24,
color : 'blue',
size : 100,
prize : 99
};
for (var i in _adapter) {
_adapter[i] = obj[i] || _adapter[i];
}
// 或者extend(_adapter, obj) 注: 此时可能会多添加属性
// do things
}
复制代码
不少插件对于参数配置都是这么作的。
var arr = ['前端食堂', 'restaurant', '记得按时吃饭', '9月30日'];
// 咱们发现数组中每一个成员表明的意义不一样,因此这种数据结构 语义很差,咱们将其适配成对象。
function arrToObjAdapter (arr) {
return {
name : arr[0],
type : arr[1],
title : arr[2],
data : arr[3]
}
var adapterData = arrToObjAdapter(arr);
console.log(adapterData);
// {name:"前端食堂", type:"restaurant", title:"记得按时吃饭",data:"9月30日"}
}
复制代码
如今主流的方案是用node.js
写中间层-BFF层(Backend for Frontend)
,获取到后台返回的数据后进行处理, 处理为前端想要的数据,这样也不失为一种适配器模式。
与外观模式的区别:
相比于外观模式,适配器模式要了解适配对象的内部结构。
概念:因为一个对象不能直接引用另外一个对象,因此须要经过代理对象在这两个对象之间起到中介的做用。
现实生活中好比咱们租房子时回去自如,自如是房屋中介机构,也就是咱们的代理。
在javaScript
中使用最多的就是虚拟代理和缓存代理。
图片loading预加载
// 建立本体对象,生成img标签,对外提供setSrc接口
var myImage = (function() {
var imgNode = document.createElement('img');
document.body.appendChild(imgNode);
return {
setSrc: function (src) {
imgNode.src = src;
}
}
})();
// 引入代理对象,图片加载以前会有个loading.gif
var proxyImage = (function () {
var img = new Image();
img.onload = function () {
myImage.setSrc(this.src);
}
return {
setSrc: function (src) {
myImage.setSrc('loading.gif');
img.src= src;
}
}
})();
// 经过代理获得图片
proxyImg.setSrc('http://...');
复制代码
能够为一些开销很大的运算结果提供暂时的储存,若下次传递的参数一致则直接返回以前的结果,大大提升效率和节省开销。
var computedFn = function () {
console.log('开始');
var a = 1;
for (var i = 0; l = arguments.length; i < l; i++) {
a = a * arguments[i];
}
return a;
}
var proxyComputed = (function() {
var cache = {};
return function() {
var args = Array.prototype.join.call(arguments, ',');
if (args in cache) {
return cache[args];
}
return cache[args] = computedFn.apply(this, arguments);
}
})();
proxyComputed(1, 2, 3, 4); // 24
proxyComputed(1, 2, 3, 4); // 24
复制代码
如上所示,代理模式能够解决系统之间的耦合度以及系统资源开销大的问题,
ES6所提供的Proxy构造函数可以让咱们轻松的使用代理模式。
// target所要代理的对象
// handler设置对所代理的对象的行为
var proxy = new Proxy(target, handler);
复制代码
vue3.0中的双向数据绑定原理用了es6中的Proxy,并优雅的解决了Proxy细节上的一些问题,从而完美的实现双向绑定,你们能够去阅读源码,或是社区中的这篇文章,这里不做展开。
概念:在不改变原对象的基础上,经过对其进行包装拓展(添加属性或者方法)使原有对象能够知足用户的更复杂需求。
装饰器是一项实验性特性,在将来的版本中可能会发生改变。
具体使用方法请移步官方文档
假设咱们在开发中有这样一个需求,在不影响原有事件的前提下,新增事件实现业务,就能够经过装饰者来实现。
// 装饰者
var decorator = function (input, fn) {
// 获取事件源
var input = document.getElementById(input);
// 若事件源已经绑定事件
if (typeof input.onclick === 'function') {
// 缓存事件源原有回调函数
var oldClickFn = input.onclick;
// 为事件源定义新的事件
input.onclick = function () {
// 事件源原有回调函数
oldClickFn();
// 执行事件源新增回调函数
fn();
}
} else {
// 事件源未绑定事件,直接为事件源添加新增回调函数
input.onclick = fn;
}
}
复制代码
与适配者模式的区别:
在适配者模式中要了解原有方法实现的具体细节,而在装饰者模式只有当咱们调用方法时才会知道其内部细节,这是对原有功能完整性的一种保护。
概念:在系统沿着多个维度变化的同时,又不增长其复杂度并已达到解耦。
var spans = document.getElementsByTagName('span');
// 为用户名绑定特效
spans[0].onmouseover = function () {
this.style.color = 'red';
this.style.background = '#ddd';
}
spans[0].onmouseout = function () {
this.style.color = '#333';
this.style.background = '#f5f5f5';
}
// 为等级绑定特效
spans[1].onmouseover = function () {
this.getELementsByTagName('strong')[0].style.color = 'red';
this.getElementsByTagName('strong')[0].style.background = '#ddd';
}
spans[1].onmouseout = function () {
this.getELementsByTagName('strong')[0].style.color = '#333';
this.getElementsByTagName('strong')[0].style.background = '#f5f5f5';
}
复制代码
// 提取共同点,解除与事件中的this的耦合
function changeColor (dom, color, bg) {
dom.style.color = color;
dom.style.background = bg;
}
// 使用匿名函数,事件与业务逻辑之间的桥梁
var spans = document.getElementsByTagName('span');
spans[0].onmouseover = function () {
changeColor(this, 'red', '#ddd';)
}
// changeColor方法中的dom实质上是事件回调函数中的this,解除它们之间的耦合
// 咱们使用了一个桥接方法-匿名回调函数。经过这个匿名回调函数
// 咱们将获取到的this传递到changeColor函数中,便可实现需求
spans[0].onmouseout = function () {
changeColor(this, '#333', '#f5f5f5');
}
// 一样的道理应用在用户等级上
spans[1].onmouseover = function () {
changeColor(this.getElementsByTagName('strong')[0], 'red', '#ddd');
}
spans[1].onmouseout = function () {
changeColor(this.getElementsByTagName('strong')[0], '#333', '#f5f5f5');
}
// 若是需求再有变化,只须要修改changeColor的内容就能够了
// 而没必要去到每一个事件回调函数中去修改,以新增一个桥接函数为代价
复制代码
将实现层(如元素绑定的事件)与抽象层(如修饰页面UI逻辑)解耦分离,使两部分能够独立变化。
注意,有些时候,对于桥梁的添加,会形成开发成本增长,性能上也会受影响。
概念:又称部分-总体模式,将对象组合成树形结构以表示“部分总体”的层次结构,组合模式使得用户对单个对象和组合对象的使用具备一致性。
中餐厅:套餐服务
webpack构建项目的目录结构
公司部门组织架构
组合模式可以给咱们提供一个清晰的组成结构。组合对象类经过继承同一个父类使其具备统一的方法,这样也方便了咱们统一管理和使用。
// jQuery 3.4.1
addClass: function( value ) {
var classes, elem, cur, curValue, clazz, j, finalValue,
i = 0;
if ( isFunction( value ) ) {
return this.each( function( j ) {
jQuery( this ).addClass( value.call( this, j, getClass( this ) ) );
} );
}
classes = classesToArray( value );
if ( classes.length ) {
while ( ( elem = this[ i++ ] ) ) {
curValue = getClass( elem );
cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " );
if ( cur ) {
j = 0;
while ( ( clazz = classes[ j++ ] ) ) {
if ( cur.indexOf( " " + clazz + " " ) < 0 ) {
cur += clazz + " ";
}
}
// Only assign if different to avoid unneeded rendering.
finalValue = stripAndCollapse( cur );
if ( curValue !== finalValue ) {
elem.setAttribute( "class", finalValue );
}
}
}
}
return this;
}
复制代码
咱们无须知道addClass的内部结构和实现细节,就可使用addClass来对标签添加类。
概念:运用共享技术有效地支持大量的细粒度的对象,避免对象间拥有相同内容形成多余的开销。
核心思想
:共享细粒度对象
最终目标
:尽可能减小共享对象的数量
现实生活中,LOL、农药段位之分是享元模式的思想,咖啡厅的咖啡种类也是享元模式的思想。
在咱们熟知的原型链继承中,当子类实例不少的时候,子类能够经过原型来复用父类的方法和属性来优化内存,这也是享元模式的思想。
除此以外,Node.js中的线程池、数据库的链接池、HTTP链接池以及字符常量池都是享元模式或其升级版。
参考:
《JavaScript设计模式》张容铭
欢迎关注个人我的公众号,文章将同步发送,后台回复【福利】便可免费领取海量学习资料。
你的前端食堂,记得按时吃饭。