rccoder / blog

😛 个人博客 🤐 订阅是 watch 是 watch 是 watch 是 watch
584 stars 36 forks source link

探寻 ECMAScript 中的装饰器 Decorator #23

Open rccoder opened 7 years ago

rccoder commented 7 years ago

2017021888257QQ20170217-175455@2x.png

前言

如果曾经使用过 Python,尤其是 Django 的话,应该对 装饰器 的概念有些许的了解。在函数前加 @user_login 这样的语句就能判断出用户是否登录。

装饰器可以说是解决了不同类之间共享方法的问题(可以看做是弥补继承的不足)。

A Python decorator is a function that takes another function, extending the behavior of the latter function without explicitly modifying it.

这句话可以说是对装饰器的非常漂亮的解释。

在未来的 JavaScript 中也引入了这个概念,并且 babel 对他有很好的支持。如果你是一个疯狂的开发者,就可以借助 babel 大胆使用它。

正文

工具准备

装饰器目前在浏览器或者 Node 中都暂时不支持,需要借助 babel 转化为可支持的版本

安装 babel

按照官网的 说明 安装:

npm install --save-dev babel-cli babel-preset-env

.babelrc 中写入:

{
  "presets": ["env"]
}

安装 decorators 插件

如果不装插件执行 babel-node a.js 或者 babel a.js > b.js 的话都会提示:

SyntaxError: a.js: Decorators are not officially supported yet in 6.x pending a proposal update.
However, if you need to use them you can install the legacy decorators transform with:

npm install babel-plugin-transform-decorators-legacy --save-dev

and add the following line to your .babelrc file:

{
  "plugins": ["transform-decorators-legacy"]
}

{

The repo url is: https://github.com/loganfsmyth/babel-plugin-transform-decorators-legacy.

按照说明,安装 babel-plugin-transform-decorators-legacy 插件:

npm install babel-plugin-transform-decorators-legacy --save-dev 

.babelrc :

{
  "presets": ["env"],
  "plugins": ["transform-decorators-legacy"]
}

这样准备工作就完成了。

说明:

babel-cli 安装会有 babel 和 babel-node 的工具生成,通过 babel a.js > b.js 可以转化 JS 版本为低版本 JS,通过 babel-node a.js 可以直接执行 JS

正式开始

装饰 类的方法

function decorateArmour(target, key, descriptor) {
  const method = descriptor.value;
  let moreDef = 100;
  let ret;
  descriptor.value = (...args)=>{
    args[0] += moreDef;
    ret = method.apply(target, args);
    return ret;
  }
  return descriptor;
}

class Man{
  constructor(def = 2,atk = 3,hp = 3){
    this.init(def,atk,hp);
  }

  @decorateArmour
  init(def,atk,hp){
    this.def = def; // 防御值
    this.atk = atk;  // 攻击力
    this.hp = hp;  // 血量
  }
  toString(){
    return `防御力:${this.def},攻击力:${this.atk},血量:${this.hp}`;
  }
}

var tony = new Man();

console.log(`当前状态 ===> ${tony}`);
// 输出:当前状态 ===> 防御力:102,攻击力:3,血量:3

装饰器接收三个参数,这三个参数和 Object.defineProperty() 基本保持一致,分别表示:

再看上面的代码:

descriptor.value = (...args)=> 中的 args 是一个数组,分别对应 def、atk、hp,给 def + 100,然后再执行 method(即被装饰的函数),最后返回 descriptor

这样就给 init 函数包装了一层。

带参数装饰 类的方法

有时候,需要给装饰器传参数:

function decorateArmour(num) {
  return function(target, key, descriptor) {
    const method = descriptor.value;
    let moreDef = num || 100;
    let ret;
    descriptor.value = (...args)=>{
      args[0] += moreDef;
      ret = method.apply(target, args);
      return ret;
    }
    return descriptor;
  }
}

class Man{
  constructor(def = 2,atk = 3,hp = 3){
    this.init(def,atk,hp);
  }

  @decorateArmour(20)
  init(def,atk,hp){
    this.def = def; // 防御值
    this.atk = atk;  // 攻击力
    this.hp = hp;  // 血量
  }
  toString(){
    return `防御力:${this.def},攻击力:${this.atk},血量:${this.hp}`;
  }
}

var tony = new Man();
console.log(`当前状态 ===> ${tony}`);
// 输出:当前状态 ===> 防御力:22,攻击力:3,血量:3

装饰 类

上面两个装饰器都是对类里面的函数进行装饰,改变了类的静态属性;除此之外,还可以对类进行装饰,给类添加方法或者修改方法(通过被装饰类的 prototype):

function decorateArmour(num) {
  return function(target, key, descriptor) {
    const method = descriptor.value;
    let moreDef = num || 100;
    let ret;
    descriptor.value = (...args)=>{
      args[0] += moreDef;
      ret = method.apply(target, args);
      return ret;
    }
    return descriptor;
  }
}

function addFunc(target) {
  target.prototype.addFunc = () => {
    return 'i am addFunc'
  }
  return target;
}

@addFunc
class Man{
  constructor(def = 2,atk = 3,hp = 3){
    this.init(def,atk,hp);
  }

  @decorateArmour(20)
  init(def,atk,hp){
    this.def = def; // 防御值
    this.atk = atk;  // 攻击力
    this.hp = hp;  // 血量
  }
  toString(){
    return `防御力:${this.def},攻击力:${this.atk},血量:${this.hp}`;
  }
}

var tony = new Man();
console.log(`当前状态 ===> ${tony}`)
console.log(tony.addFunc());

// 输出:当前状态 ===> 防御力:22,攻击力:3,血量:3
// 输出:i am addFunc

装饰 普通函数

不建议装饰,因为变量提升会产生系列问题

衍生

装饰器的使用场景基本都是 AOP。大多数日志场景都可以使用此种模式,比如这里一个简单的 日志场景

对于纯前端来说,也有很多用途,比如实现一个 react 的 lazyload,就可以使用装饰器修饰整个 class。

同时,也有一些库实现了常用的装饰器,比如:core-decorators.js

参考文章