装饰器模式,又名装饰者模式。它的定义是“在不改变原对象的基础上,通过对其进行包装拓展,使原有对象可以满足用户的更复杂需求”。

装饰函数

在JavaScript中,几乎一切都是对象,其中函数又被称为一等对象。在平时的开发工作中,也许大部分时间都在和函数打交道。在JavaScript中可以很方便地给某个对象扩展属性和方法,但却很难在不改动某个函数源代码的情况下,给该函数添加一些额外的功能。在代码的运行期间,我们很难切入某个函数的执行环境。

要想为函数添加一些功能,最简单粗暴的方式就是直接改写该函数,但这是最差的办法,直接违反了开放-封闭原则:

  1. const func = function() {
  2. console.log('我是Func的原有逻辑');
  3. }
  4. // 改成
  5. const func = function() {
  6. console.log('我是Func的原有逻辑');
  7. console.log('我是Func的装饰器逻辑');
  8. }

很多时候我们不想去碰原函数,也许原函数是由其他同事编写的,里面的实现非常杂乱。甚至在一个古老的项目中,这个函数的源代码被隐藏在一个我们不愿碰触的阴暗角落里。现在需要一个办法,在不改变函数源代码的情况下,能给函数增加功能,这正是开放-封闭原则给我们指出的光明道路。

用AOP装饰函数

首先给出Function.prototype.before方法和Function.prototype.after方法:

  1. Function.prototype.before = function( beforefn ){
  2. let __self = this; // 保存原函数的引用
  3. return function(){ // 返回包含了原函数和新函数的"代理"函数
  4. beforefn.apply( this, arguments ); // 执行新函数,且保证this不被劫持,新函数接受的参数
  5. // 也会被原封不动地传入原函数,新函数在原函数之前执行
  6. return __self.apply( this, arguments ); // 执行原函数并返回原函数的执行结果,
  7. // 并且保证this不被劫持
  8. }
  9. }
  10. Function.prototype.after = function( afterfn ){
  11. let __self = this;
  12. return function(){
  13. let ret = __self.apply( this, arguments );
  14. afterfn.apply( this, arguments );
  15. return ret;
  16. }
  17. };

Function.prototype.before接受一个函数当作参数,这个函数即为新添加的函数,它装载了新添加的功能代码。

接下来把当前的this保存起来,这个this指向原函数,然后返回一个“代理”函数,这个“代理”函数只是结构上像代理而已,并不承担代理的职责(比如控制对象的访问等)。它的工作是把请求分别转发给新添加的函数和原函数,且负责保证它们的执行顺序,让新添加的函数在原函数之前执行(前置装饰),这样就实现了动态装饰的效果。

我们注意到,通过Function.prototype.apply来动态传入正确的this,保证了函数在被装饰之后,this不会被劫持。

用AOP动态改变函数的参数

现在有一个用于发起request请求的函数,这个函数负责项目中所有的request异步请求:

  1. const request = function(type, url, params) {
  2. // 发送请求代码...
  3. }
  4. request('get', 'http://xxx.com/userinfo', {name: 'gosnails'});

request函数在项目中一直运转良好。直到有一天,我们的网站遭受了CSRF攻击。解决CSRF攻击最简单的一个办法就是在HTTP请求中带上一个Token参数。

假设我们已经有一个用于生成Token的函数:

  1. const getToken = function() {
  2. return 'Token';
  3. }

现在的任务是给每个request请求都加上Token参数:

  1. const request = function( type, url, param ){
  2. param = param || {};
  3. Param.Token = getToken();
  4. // 发送请求代码...
  5. };

虽然已经解决了问题,但我们的request函数相对变得僵硬了,每个从request函数里发出的请求都自动带上了Token参数,虽然在现在的项目中没有什么问题,但如果将来把这个函数移植到其他项目上,或者把它放到一个开源库中供其他人使用,Token参数都将是多余的。

为了解决这个问题,把Token参数通过Function.prototyte.before装饰到request函数的参数param对象中:

  1. request = request.before(function( type, url, param ){
  2. param.Token = getToken();
  3. });

用AOP的方式给request函数动态装饰上Token参数,保证了request函数是一个相对纯净的函数,提高了request函数的可复用性,它在被迁往其他项目的时候,不需要做任何修改。

装饰类

在项目开发的结尾阶段难免要加上很多统计数据的代码,假如页面中有一个Button,点击这个Button除了业务代码,与此同时还要进行数据上报,来统计有多少用户点击了这个Button。

传统面向对象装饰器

  1. class Button {
  2. onClick() {
  3. console.log('我是业务逻辑');
  4. }
  5. }
  6. class classDecorator {
  7. constructor(button) {
  8. this.button = button;
  9. }
  10. onClick() {
  11. this.button.onClick();
  12. console.log('我是数据上报逻辑');
  13. }
  14. }
  15. // 验证装饰器是否生效
  16. let button = new Button();
  17. button = new classDecorator(button);
  18. button.onClick();

装饰类的构造函数接受参数button对象,并且保存好这个参数,在它的onClick方法中,除了执行自身的数据上报逻辑之外,还调用button对象的onClick方法。

这种给对象动态增加职责的方式,并没有真正地改动对象自身,而是将对象放入另一个对象之中,这些对象以一条链的方式进行引用,形成一个聚合对象。

ES7 中的装饰器

在 ES7 中,我们可以通过一个@语法糖轻松地给一个类装上装饰器:

  1. // 装饰器函数,它的第一个参数是目标类
  2. function classDecorator(target) {
  3. target.hasDecorator = true;
  4. return target;
  5. }
  6. // 将装饰器“安装”到Button类上
  7. @classDecorator
  8. class Button {
  9. // Button类的相关逻辑
  10. }
  11. // 验证装饰器是否生效
  12. console.log('Button 是否被装饰了:', Button.hasDecorator);

也可以用同样的语法糖去装饰类里面的方法:

  1. function funcDecorator(target, name, descriptor) {
  2. let originalMethod = descriptor.value;
  3. descriptor.value = function() {
  4. let ret = originalMethod.apply(this, arguments);
  5. console.log('我是数据统计逻辑');
  6. return ret;
  7. }
  8. return descriptor;
  9. }
  10. class Button {
  11. @funcDecorator
  12. onClick() {
  13. console.log('我是业务逻辑');
  14. }
  15. }
  16. // 验证装饰器是否生效
  17. const button = new Button();
  18. button.onClick();

总结

通过动态改变函数参数、数据上报这两个例子,我们了解了装饰函数和装饰类,这种模式在实际开发中非常有用,装饰器模式可以避免为了让框架拥有更多的功能,而去使用一些if、else语句预测用户的实际需要。