JS设计模式——装饰者模式

给对象动态地增加职责的方式称作装饰者模式,它是在程序运行期间给对象动态添加职责。

一、定义

给对象动态地增加职责的方式称作装饰者模式,它是在程序运行期间给对象动态添加职责。

传统OOP编程中的继承方式存在的问题:

  1. 不够灵活,每次增加功能可能都需要新派生一个类;
  2. 基类和派生类之间容易存在强耦合性,基类的改变会影响派生类;
  3. 基类的实现细节对派生类可见,一定程度上破坏了封装性;

而通过装饰者模式我们可以动态地给某个对象添加一些额外的职责(功能),而不会影响这个类本身及其派生类。

二、面向对象方式的实现

核心思想:通过一个新的对象去包装原始类,新的类和原始类拥有相同接口。

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*OOP方式实现*/
function A(){ //原始类
}
A.prototype.func=function(){
//类本身的功能
};

function B(obj){ //装饰类
this.obj=obj;
}
B.prototype.func=function(){
this.obj.func(); //执行原始类的功能
//do something new
};

/*使用方式*/
var a=new A();
a = new B(a);
a.func(); //沿着包装链向上回溯,先执行原始类的功能,然后执行装饰类的功能

三、JavaScript的实现

JavaScript相对于强类型语言来说,它支持动态地修改对象属性,这使得装饰着模式实现更容易。

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*通过保存原引用方式实现装饰者模式*/

var a=function(){ //原始类
console.log("原始功能");
};

var b=function(){ //待添加的新功能
console.log("新功能");
};

var _a=a; //保存原引用

a=function(){
_a(); //原始功能
b(); //新功能
};

a(); //先执行原始功能,后执行新功能

技巧

使用onEventType方式给元素绑定事件时会覆盖以往的绑定函数,可以用以下方式绑定:

1
2
3
4
5
6
7
/*安全地通过onEventType绑定事件*/

var _ontype=Element.ontype||function(){};
Element.ontype=function(){
_ontype();
//do something new
};

以上的实现方式存在两个问题:

  1. 需要维护中间变量,如果装饰链较长,中间变量会变得很多;
  2. this被劫持,_ontype中的this指向全局变量window;

四、装饰者模式与AOP

AOP:

面向切面编程,把一些与核心业务逻辑模块无关的功能抽离出来,再通过“动态织入”的方式掺入业务逻辑中。AOP可以让核心业务模块保持纯净和高内聚性,抽离出来的模块还能较好地复用。(codepen)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*装饰者模式实现AOP*/

Function.prototype.before=function(before_fn){ //前织入函数before_fn
var _self=this; //保存原函数的引用
//this是调用before方法的对象,一般为原函数
return function(){
before_fn.apply(this,arguments); //执行新函数,this指向window
return _self.apply(this,arguments); //返回原函数的执行结果
};
};

Function.prototype.after=function(after_fn){ //后织入函数after_fn
var _self=this;
return function(){
var ret=_self.apply(this,arguments);
after_fn.apply(this,arguments);
return ret;
};
};

注意:

当多个before和after级联调用时,函数执行顺序是:先执行before传入的函数,后执行after函数,这与before和after的顺序无关。级联的before中先执行靠后出现的调用,after中先执行先出现的调用。

现在我们来重写安全绑定事件的方式:

1
2
3
4
5
/*安全地通过onEventType绑定事件*/

Element.ontype=(Element.ontype||function(){}).before(function(){
//do something new
});

技巧:

使用img的src属性上报数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*使用img的src属性上报数据*/
<!DOCTYPE html>
<html>
<input type="text" id="search">
<iframe src="" frameborder="0" id="res"></iframe>
<script>
var ele=document.getElementById("search");
var img_arr=[];
ele.oninput=function(){
var img=new Image();
img.src="https://www.baidu.com/s?wd="+this.value;
document.getElementById("res").src=img.src; //非必须
};
</script>

</html>