探寻 ECMAScript 中的装饰器 Decorator
BurtonWagst
8年前
<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>