linqinghao/blog

使用ES7中的Decorator来简化代码

Opened this issue · 0 comments

之前偶然在浏览网上的代码中发现了很多类的开头有这么一个@符号,出于好奇心,查阅了一番网上的资料,发现这是 ES7 中的新特性,还处于不稳定的状态,但是它却能帮助我们写出很简洁的代码。所以,我就把我对它的理解写下来。

在面向对象编程中,我们经常使用封装,继承,多态的特性。当我们需要为类新添一个打印日志的功能的时候,并且这个打印日志的功能可能很多地方都需要用到,我们就会抽象一个日志类,让其他类继承于它。但是这种方式不够灵活,通过继承的方式可能会导致子类繁多,仅仅为了增加一个单一的功能,就会显得多余。在js中,我们可以用包装对象的方式将打印日志的功能注入到类中,在不影响原有类功能的基础上,这也就是装饰器模式。这里的装饰器模式跟 es7 中的装饰器的概念是相同的,只是实现的方式不同而已。

装饰器模式

首先,先介绍下js中的装饰器模式,它的实现方式是用包装对象的形式,类似于函数复合或高阶组件。先来看个例子:

function cat() {
  console.log('my name is kitty');
}

function bell(wrapper) {
  return function() {
    const result = wrapper.apply(this, arguments);
    console.log('ding ding ding');
    return result;
  };
}

const kittyCat = bell(cat);
kittyCat();

上面的代码中,我为这只猫加了一个铃铛,每当猫走路时,铃铛就会响。bell方法通过返回函数的形式实现装饰器模式。关键代码在wrapper.apply(this, arguments)上,不改变原函数的行为。执行结果如下:

js-decorator-demo1-2019-12-7.jpg

上面代码用class的方式也很容易实现。

class Cat {
  constructor(name) {
    console.log(`my name is ${name}`);
  }
}

class Bell {
  constructor(cat) {
    console.log('ding ding ding');
  }
}
const kittyCat = new Bell(new Cat('kitty'));

这就是js实现的装饰器模式,当然,还有其他方法可以实现。

ES7 中的装饰器

ES7 中的装饰器能够让我们动态扩展类的功能、修改类的行为、参数等等。

使用@操作符

Decorator 装饰器是 ES7 中的提案,它目前还只是一个提案,在浏览器或者nodejs中都暂时还不支持,需要借助babel转换。

  1. 安装babel
npm install -g babel-cli
npm install --save-dev  babel-preset-env babel-plugin-transform-decorators-legacy
  1. 创建.babelrc文件,并添加:
{ "presets": ["env"], "plugins": ["transform-decorators-legacy"] }

这样准备工作就做好了。

实现

ES7 中的装饰器使用了 ES5 种的Object.defineProperty(target, name, descriptor)这一新特性。如果还不了解Object.defineProperty,请参考MDN 文档

Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。该方法具有三个参数:

  • target: 目标对象
  • name: 要定义或修改的属性的名称
  • descriptor:将被定义或修改的属性描述符

这里最重要的是descriptor这个参数,它是一个数据或访问器的属性描述对象。在对数据和访问器属性描述时,它们都具有 configurableenumerable 属性可用。而在数据描述时,value、writable 属性则是数据所特有的。getset 属性则是访问器属性描述所特有的。属性描述器中的属性决定了对象 prop 属性的一些特性。比如 enumerable,它决定了目标对象是否可被枚举,能够在 for…in 循环中遍历到,或者出现在 Object.keys 法的返回值中;writable 则决定了目标对象的属性是否可以被更改。

对于descriptor中的属性,它们可以被我们在 Decorator 中使用,或者修改的,以达到我们标注或者拦截的目的。

举个例子:

class Cat {
  constructor(name) {
    this.name = name;
    this.meow();
  }

  meow() {
    console.log(`my name is ${this.name}`);
  }
}
const kittyCat = new Cat('kitty');

1. 对方法的修饰

我想让这只猫出生的时候伴随着一声叫声,要怎么办呢~

function meowDecorator(msg) {
  return function(target, key, descriptor) {
    const method = descriptor.value; // 保留原有函数调用
    descriptor.value = function(...args) {
      const ret = method.apply(this, args); // 调用原有函数声明
      console.log(msg); // 打印出小猫的叫声~
      return ret;
    };
    return descriptor; // 注意:最后要返回descriptor
  };
}

class Cat {
  constructor(name) {
    this.name = name;
    this.meow();
  }

  @meowDecorator('ao uuu~~~') // 给类的方法添加装饰器
  meow() {
    console.log(`my name is ${this.name}`);
  }
}
const kittyCat = new Cat('kitty');

上面代码中,声明了一个 meowDecorator 的装饰器,并且返回了一个匿名函数,target指向了Cat类,keymeow方法,descriptormeow的装饰符,类似下方的对象。

{ value: [Function: meow],
  writable: true,
  enumerable: false,
  configurable: true,
}

最后的执行结果如下:

js-decorator-demo2-2019-12-7.jpg

注意:在调用原有函数方法时,上下文没有指向target原有的类,而是指向了调用该方法的this,也就是Cat的实例对象,这样才能正确访问this.name实例属性。

2. 对类的修饰

上面主要是对类的方法的修饰,装饰器也可以对类进行修饰。只是参数稍有区别,只有target参数指向当前类对象。举个例子,我们为这只 kitty 赋予飞行的能力~

+function flyDecorator(target) {
+  target.prototype.fly = function() { // 添加飞行能力
+    console.log('i can fly~');
+  }
+  return target;
+}
+

+@flyDecorator
class Cat {
  constructor(name) {
    this.name = name;
}
const kittyCat = new Cat('kitty');
+kittyCat.fly();

这里我添加了一个flyDecorator的装饰器,为类的原型添加了fly方法,这样猫就能飞起来了~

js-decorator-demo3-2019-12-7.jpg

注意: 装饰器不能用于函数声明,因为函数声明会函数提升。

总结一下,ES7 中的装饰器使我们能够动态改变运行时类的行为,因此适用于 AOP(面向切面编程)的场景,譬如需要为类增加打印日志这些无需侵入类的行为的功能的时候,这样就不违反设计模式中的单一职责原则。AOP 为传统的 OOP(面向对象编程)提供了横向的扩展,使得这种编程模型更加立体,弥补了 OOP 纵向扩展的不足~功力未深,还望指正~

完整代码可以在github下的decorator文件夹查看~