《javascript设计模式与开发实践》阅读笔记(15)—— 装饰者模式
装饰者模式
可以动态地给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象。在程序开发中,许多时候都并不希望某个类天生就非常庞大,一次性包含许多职责。那么我们就可以使用装饰者模式。
代码例子
1 var Plane = function(){}
2 Plane.prototype.fire = function(){
3 console.log( '发射普通子弹' );
4 }
5 var one = function( plane ){ //装饰类 one
6 this.plane = plane;
7 }
8 one.prototype.fire = function(){
9 this.plane.fire();
10 console.log( '发射导弹' );
11 }
12 var two = function( plane ){ //装饰类 two
13 this.plane = plane;
14 }
15 two.prototype.fire = function(){
16 this.plane.fire();
17 console.log( '发射原子弹' );
18 }
19
20 var plane = new Plane();
21 plane = new one( plane ); //装饰对象会覆盖原来的对象,这么做是因为装饰者模式是为了添加职能,可能这时候的代码量已经很大了,原对象的方法可能在很多地方都调用过,所以我们通过这种方式,追加了功能而不用修改太多代码。
22 plane = new two( plane );
23
24 /**装饰对象调用**/
25 plane.fire(); /* 发射普通子弹
26 发射导弹
27 发射原子弹
28 */
从代码我们可以看出,装饰者模式这种给对象动态增加职责的方式,并没有真正地改动对象自身。
从形式上来看,装饰者模式就好像给原来的对象包裹了一层,产生一个新的对象,同时,保留了接口统一。装饰类的作用就是接受一个对象,然后把它变成功能更多的对象。
装饰类接收原来的对象,会把它保存下来,调用装饰类时,会将本来的对象放入另一个对象之中,这些对象以一条链的方式进行引用,形成一个聚合对象。这些对象都拥有相同的接口(fire方法),当请求达到链中的某个对象时,这个对象会执行自身的操作,随后把请求转发给链中的下一个对象。
从传递请求这一点来看有点类似于职责链,但是职责链是传递请求给正确执行的对象,而这个不但自身会执行,通过装饰类添加的方法也会执行,它这种类似于链子一样的请求调用与其说是链子倒不如说是包裹更合适。装饰类会把原来的对象包裹起来,调用时会一层一层的调用。
更js的实现方法
因为js是动态语言,修改类型,改变对象的属性和方法对它来说太简单了,所以还有一种思路,我们拿到需要调用的方法的引用,然后直接对它进行改造即可。不过这种方式等于直接修改原对象的方法,和上面的方法哪个更好,我说不上来。
1 var plane = { //原对象 2 fire: function(){ 3 console.log( '发射普通子弹' ); 4 } 5 } 6 var add_one = function(){ //想要添加的功能 7 console.log( '发射导弹' ); 8 } 9 var add_two = function(){ 10 console.log( '发射原子弹' ); 11 } 12 13 var fire1 = plane.fire; //存储对象方法的引用 14 plane.fire = function(){ //改写对象方法 15 fire1(); //原对象的方法 16 add_one(); //添加的方法 17 } 18 19 var fire2 = plane.fire; //同上 20 plane.fire = function(){ 21 fire2(); 22 add_two(); 23 } 24 25 plane.fire(); /* 发射普通子弹 26 发射导弹 27 发射原子弹 28 */
另外我还有一个不成熟的想法,像第一种实现装饰者的方法里,我们覆盖了原来对象的变量。使得所有调用对应方法的位置都会执行添加的函数,但是如果我们不是全都需要而是有的地方需要有的地方不需要呢?
我有两个思路,一个是在装饰类里重写方法时进行判断,另一个是把原来的对象单独给个变量,和装饰类加工过的对象有所区分,不需要执行新功能的地方,就用原来的对象调用。
感觉好像第一个思路好一点?
装饰函数
在js中,函数也是对象,只要是对象自然可以实现装饰者。
1 var a=function(){
2 alert(1);
3 }
4
5 var _a=a; //保存函数引用
6
7 a=function(){ //重写函数
8 _a(); //原本的函数引用
9 alert(2); //新添加的功能
10 }
又比如我们想给window绑定onload事件,但是又不确定这个事件是不是已经被其他人绑定过,为了避免覆盖掉之前的window.onload函数中的行为,我们一般都会先保存好原先的window.onload,把它放入新的window.onload 里执行:
1 window.onload = function(){
2 alert (1);
3 }
4 var _onload = window.onload || function(){}; //没有绑过onload,让变量为空的函数
5
6 window.onload = function(){ //重写
7 _onload();
8 alert (2); //追加的代码
9 }
可能的坑:this
保存引用再重写,可能会带来this绑定错误的问题,因为我们保存的引用中的this和当前执行环境的this可能完全不同了,重写的函数有时候上下文其实已经改变了,如果上下文改变,this的指向也就改变了,所以它有可能没有指向正确的对象。
1 var _getElementById = document.getElementById; //这个是document这个对象的方法,其内部this,指向document,我们把它的引用保存到_getElementById这个全局变量里
2 document.getElementById = function( id ){ //重写函数
3 alert (1);
4 return _getElementById( id ); // 返回了保留的函数,但是返回的函数是全局函数,this指向window,没有指向正确的document对象
5 }
6 var button = document.getElementById( 'button' ); //报错,Uncaught TypeError: Illegal invocation
又比如:
1 var name="大白";
2 var a={
3 name:"如花",
4 getname:function(){
5 console.log(this.name);
6 }
7 }
8
9 var get=a.getname; //保存引用
10
11 get(); //大白
12 a.getname(); //如花
可以看到,调用函数时的环境改变,虽然保存了引用,却没得到我们想要的结果。(我们想获得如花)
如果遇到这种情况,也就只能手动更改(apply),或者控制在同一个上下文环境中添加职责。
比如上面的代码,我们改成:
get.apply(a); //如花
就可以获得正确的结果。
第一个例子也是同理
1 var _getElementById = document.getElementById;
2 document.getElementById = function(){
3 alert (1);
4 return _getElementById.apply( document, arguments ); //apply大显身手,指定上下文为document
5 }
6 var button = document.getElementById( 'button' );
优缺点
装饰者模式可以说是AOP思想的实例。优点:解耦,追加功能不用修改原本函数,更为灵活方便。缺点:会叠加函数的作用域,如果装饰的链条过长,性能上也会受到一些影响。