javascript设计模式(二)

代理模式

用户不方便直接访问一个对象或者不满足需要的时候,通过另一个对象B去进行代理控制访问。对用户来说,能操作的就是B,B再来转交请求给target,而不用用户来管B->target到底做了什么操作。

最朴素的代理模式就是下面的代码,只是做一个转发请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function(){
var Flower = function(){}
var xiaoming = {
sendFlower:function(receiver){
var flower = new Flower()
receiver.receiveFlower(flower)
}
},
var proxier = {
receiveFlower: function(){
//当然这边只是代理操作,实际上可能还会有保护检查,拒绝,性能优化等操作
target.receiveFlower(flower)
};
},
var target = {
receiveFlower: function(){
console.log(" i receive the flower, thx!");
};
}
})

保护代理和虚拟代理

代理模式分为两种,按照他们的功能来分类:

  • 一种是做保护target的用途,接受sender的请求,然后进行一些校验,即保护代理,比如上面的代码就是保护代理。
  • 另一种是把一些开销大的操作延迟到真正需要它做的时候再处理,而sender本身不动,依然做最朴素的操作,代理去做优化措施,这种是虚拟代理。

两者的调用不太一样,前者是sender.sendTo(proxy),后者是proxier.call(fn).

日常接触的基本都是虚拟代理,(需要注意的是它的本体和代理要实现同样的方法,即接口要一致,这样才可以对用户透明,也方便调用,但JavaScript不像静态类型语言,不会进行编译检查,因此,只能在最末端进行throw异常抛出,但这个时候已经是执行期了,有一丢丢晚。)

他的用途有比如实现图片预加载、合并http请求、

图片预加载

[备注]:
图片预加载的原理是预先设置一个占位图片,当真正的图片资源加载完后去进行替换操作。有一点点像ajax请求获取前放置一个loading层,数据返回后hide loading,显示最终的网页信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var myImage = (function(){
var imgNode = document.createElement( 'img' );
document.body.appendChild( imgNode );
return {
setSrc: function( src ){
imgNode.src = src;
}
}})();
var proxyImage = (function(){
var img = new Image;
img.onload = function(){
myImage.setSrc( this.src );
}
return {
setSrc: function( src ){
myImage.setSrc( 'file:// /C:/Users/svenzeng/Desktop/loading.gif' );
img.src = src;
}
}
})();
proxyImage.setSrc('realImage.png')

这边用代理的一个原因是遵守了单一职责原则,因为setSrc和设置预加载图片是两个职责,通过代理,让myimage只进行设置图片源的功能。做了任务分离。而且,后面我们会提到的性能优化(如缓存)的代理,也是这样,源请求者只做他本身要做的部分,后续的优化功能让代理来实现。(唔,也符合开放-封闭职责…)
确切的说,大多数情况下,若违反其他任何原则,同时将违反开放-封闭原则。

合并http请求

原理是设置一个代理函数,(利用闭包放置一个Cache[],存放给定时间段内的请求,当时间到达时,一同发送)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//本体
sendAjax(req);
//代理
var proxyAjax = (function(){
var reqQueue = [];
return function(req){
reqQueue.push(req);
if(timer) return; //1
timer = setTimeout(function(){
//把reqQueue的请求内容合并,比如原始req的post body是[obj1,obj2,obj3].现在要通过一定的方式去合并成一个大的bigObj,然后发送它。
var assignedObj = transform(reqQueue);
sendAjax(assignedObj);
reqQueue = [];
timer = null;//下一轮进来的入口条件,见1处
});
}
})();
for(var i=0;i<reqs.length;i++){
proxyAjax(reqs[i]);//楼主之前担心闭包,想了下,以前是在这边进行绑定事件,事件执行时间不定,所以要闭包把i穿进去,但是这边是直接进行操作。所以不要担心。。。。:)
}

上面的代码特别是proxy的内部代码要仔细体会下,跟楼主之前写节流的代码思想一致,但是比较巧妙,不需要进行gap时间的计算。po一下节流

还有一点是,这边的合并请求跟之前的节流有点思想相似,只不过这边是特定场景的优化,节流是通用的对函数的操作。合并请求的优点很多,可以缓解服务端压力,而且当用户发送请求后又取消时,合并以后实际上是不发送请求的。当然这也可以在前端判断…

其他优化场景

除此之外,代理还可以进行其他缓存操作,比如:

  • 动态规划里面的缓存数组= =
  • ajax请求的数据这样使得本体函数可以只进行自己的操作,在代理中做完优化后直接sourceFun.apply()就可以了。

【summary】代理分保护和虚拟代理,虚拟代理的使用场景是一些大型开销操作或者优化措施,case有预加载,合并请求,ajax数据缓存等等。


迭代器模式

只要被迭代的对象有length属性并且可以用下标访问,就可以被迭代。

书里面说迭代器分为两种:

  • 一种是内部迭代器,也就是说函数内部定义好了如何去进行迭代,如js原声的forEach,map等等,underscore里面也有实现

    1
    2
    3
    4
    5
    6
    7
    8
    var each = function(arr,fn){
    for(var i=0;i<arr.length;i++){
    fn.call(null,arr[i],i,arr);
    }
    }
    each([1,2,3],function(v,index){
    console.log(v)
    });
  • 另一种是外部迭代器,即把所有遍历操作等等都放权给程序员,比如遍历规则是什么,比如step,何时终止等等,有点像java里面的iterator。之前看到es6里面也有简易版本的iterator,只要实现遍历器生成方法就行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    const obj = {
    [Symbol.iterator] : function () {
    return {
    next: function () {
    return {
    value: 1,
    done: true
    };
    }
    };
    }
    };
    //[Symbol.iterator]()返回的是一个遍历器对象。如果一个对象实现了它,并且index是数字,那么它也可以被用iterator遍历。可以用for of来访问值。
    var $iterator = ITERABLE[Symbol.iterator]();
    var $result = $iterator.next();
    while (!$result.done) {
    var x = $result.value;
    // ...
    $result = $iterator.next();
    }

使用场景

如代替if-else,上传方式的遍历:(浏览器上传控件、flash上传、原生表单上传input file)
示例代码:

1
2
3
4
5
6
7
8
9
10
var iteratorUploadObj = function(){
for ( var i = 0, fn; fn = arguments[ i++]; ){
var uploadObj = fn();
if ( uploadObj !== false ){
return uploadObj;
}
}
};
var uploadObj = iteratorUploadObj( getActiveUploadObj, getFlashUploadObj, getFormUpladObj );

【summary】
迭代器分为内部、外部两种类型。
迭代器遵循了单一职责模式,将遍历data的职责与其他的操作分离开(用回调)。


模版方法模式

广义的模版方法模式主要是由抽象父类和具体实现父类方法的子类组成,父类中定义了方法,主要有两种:

  • 抽象方法: 需要字累实现的方法,包括模版方法:子类方法的执行顺序
  • 具体方法: 从子类中抽取的公共的方法,作复用
1
2
3
4
5
6
Coffee.prototype.init = function(){
this.boilWater();
this.brewCoffeeGriends();
this.pourInCup();
this.addSugarAndMilk();
};

这边的init方法就是模版方法,因为它规定了子类需要实现的方法及执行顺序。对于javscript来说,没有抽象类的概念,一般用原型链来模拟父类子类。

1
2
3
4
5
6
7
var Rectangle = function (id, x, y, width, height) {
Shape.call(this, id, x, y);
this.width = width;
this.height = height;
};
Rectangle.prototype = Object.create(Shape.prototype);//继承父类的方法
Rectangle.prototype.constructor = Rectangle;

注意点

  • 但是有一个问题是,由于javascript是动态语言,没有编译器检查子类是否实现了父类的方法,解决方案一种是在父类里面抛出异常,子类没有实现的话,会顺着原型链调用到父类方法,抛出异常就会发现。
  • 钩子函数: 有时候并不一定按照父类的顺序或者定义的模版方法来执行,需要有所变化,这个时候需要在父类挂一些钩子函数,让这些子类决定的钩子函数来决定子类真正的实现流程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Beverage.prototype.init = function(){
this.boilWater();
this.brew();
this.pourInCup();
if ( this.customerWantsCondiments() ){
// 如果挂钩返回true,则需要调料
this.addCondiments();
}
}
CoffeeWithHook.prototype.customerWantsCondiments =
function(){
return window.confirm( '请问需要调料吗?' );
};
var coffeeWithHook = new CoffeeWithHook();
coffeeWithHook.init();

实际上可以用高阶函数的方法,子类把要覆盖的方法作为param传给父类的函数,然后类似于object.assign去生成一个子类的方法集合对象。

嗯,react中的HOC(高阶组件)有点像这边的父类,好吧,本质上跟继承没差啦。

外观模式

外观模式对客户提供一个简单易用的高层接口,高层接口会把客户的请求转发给子系统来完成具体的功能实现。
概况来说就是:
客户->高层接口-> 子系统
(内心OS,感觉有点像反向代理…)