探寻 ECMAScript 中的装饰器 Decorator

BurtonWagst 2年前
   <h2>前言</h2>    <p>如果曾经使用过 Python,尤其是 Django 的话,应该对 <strong>装饰器</strong> 的概念有些许的了解。在函数前加 @user_login 这样的语句就能判断出用户是否登录。</p>    <p>装饰器可以说是解决了不同类之间共享方法的问题(可以看做是弥补继承的不足)。</p>    <p>A Python decorator is a function that takes another function, extending the behavior of the latter function without explicitly modifying it.</p>    <p>这句话可以说是对装饰器的非常漂亮的解释。</p>    <p>在未来的 JavaScript 中也引入了这个概念,并且 babel 对他有很好的支持。如果你是一个疯狂的开发者,就可以借助 babel 大胆使用它。</p>    <h2>正文</h2>    <h3>工具准备</h3>    <p>装饰器目前在浏览器或者 Node 中都暂时不支持,需要借助 babel 转化为可支持的版本</p>    <p>安装 babel</p>    <p>按照官网的 <a href="/misc/goto?guid=4958871204393151840" rel="nofollow,noindex">说明</a> 安装:</p>    <pre>  <code class="language-javascript">npm install --save-dev babel-cli babel-preset-env</code></pre>    <p>在 .babelrc 中写入:</p>    <pre>  <code class="language-javascript">{    "presets": ["env"]  }</code></pre>    <p>安装 decorators 插件</p>    <p>如果不装插件执行 babel-node a.js 或者 babel a.js > b.js 的话都会提示:</p>    <pre>  <code class="language-javascript">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.</code></pre>    <p>按照说明,安装 babel-plugin-transform-decorators-legacy 插件:</p>    <pre>  <code class="language-javascript">npm install babel-plugin-transform-decorators-legacy --save-dev</code></pre>    <p>.babelrc :</p>    <pre>  <code class="language-javascript">{    "presets": ["env"],    "plugins": ["transform-decorators-legacy"]  }</code></pre>    <p>这样准备工作就完成了。</p>    <p>说明:</p>    <p>babel-cli 安装会有 babel 和 babel-node 的工具生成,通过 babel a.js > b.js 可以转化 JS 版本为低版本 JS,通过 babel-node a.js 可以直接执行 JS</p>    <h3>正式开始</h3>    <p>装饰 类的方法</p>    <pre>  <code class="language-javascript">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</code></pre>    <p>装饰器接收三个参数,这三个参数和 <a href="/misc/goto?guid=4959632029316817377" rel="nofollow,noindex">Object.defineProperty()</a> 基本保持一致,分别表示:</p>    <ul>     <li>需要定义属性的对象 —— 被装饰的类</li>     <li>需定义或修改的属性的名字 —— 被装饰的属性名</li>     <li>将被定义或修改的属性的描述符 —— 属性的描述对象</li>    </ul>    <p>再看上面的代码:</p>    <ul>     <li>target 是 Man {} 这个类</li>     <li>key 是被装饰的函数 init()</li>     <li>descriptor 和 Object.defineProperty() 一样: {value: [Function], writable: true, enumerable: false, configurable: true}</li>    </ul>    <p>descriptor.value = (...args)=> 中的 args 是一个数组,分别对应 def、atk、hp,给 def + 100,然后再执行 method (即被装饰的函数),最后返回 descriptor 。</p>    <p>这样就给 init 函数包装了一层。</p>    <p>带参数装饰 类的方法</p>    <p>有时候,需要给装饰器传参数:</p>    <pre>  <code class="language-javascript">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</code></pre>    <p>装饰 类</p>    <p>上面两个装饰器都是对类里面的函数进行装饰,改变了类的静态属性;除此之外,还可以对类进行装饰,给类添加方法或者修改方法(通过被装饰类的 prototype):</p>    <pre>  <code class="language-javascript">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</code></pre>    <p>装饰 普通函数</p>    <p>不建议装饰,因为变量提升会产生系列问题</p>    <h3>衍生</h3>    <p>装饰器的使用场景基本都是 <a href="/misc/goto?guid=4959737792990527241" rel="nofollow,noindex">AOP</a> 。大多数日志场景都可以使用此种模式,比如这里一个简单的 <a href="/misc/goto?guid=4959737793086365433" rel="nofollow,noindex">日志场景</a> 。</p>    <p>对于纯前端来说,也有很多用途,比如实现一个 react 的 lazyload,就可以使用装饰器修饰整个 class。</p>    <p>同时,也有一些库实现了常用的装饰器,比如: <a href="/misc/goto?guid=4958972612467736644" rel="nofollow,noindex">core-decorators.js</a></p>    <h2>参考文章</h2>    <ul>     <li><a href="/misc/goto?guid=4959737793192515605" rel="nofollow,noindex">Exploring EcmaScript Decorators</a></li>     <li><a href="/misc/goto?guid=4959643070687769896" rel="nofollow,noindex">javascript-decorators</a></li>     <li><a href="/misc/goto?guid=4959737793309502393" rel="nofollow,noindex">ES7 Decorator 装饰者模式</a></li>     <li><a href="/misc/goto?guid=4959737793421406854" rel="nofollow,noindex">ECMAScript 6 入门</a></li>    </ul>    <p> </p>    <p>来自:https://github.com/rccoder/blog/issues/23</p>    <p> </p>