探尋 ECMAScript 中的裝飾器 Decorator
前言
如果曾經使用過 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.</code></pre>
按照說明,安裝 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</code></pre>
裝飾器接收三個參數,這三個參數和 Object.defineProperty() 基本保持一致,分別表示:
- 需要定義屬性的對象 —— 被裝飾的類
- 需定義或修改的屬性的名字 —— 被裝飾的屬性名
- 將被定義或修改的屬性的描述符 —— 屬性的描述對象
再看上面的代碼:
- target 是 Man {} 這個類
- key 是被裝飾的函數 init()
- descriptor 和 Object.defineProperty() 一樣: {value: [Function], writable: true, enumerable: false, configurable: true}
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</code></pre>
裝飾 類
上面兩個裝飾器都是對類里面的函數進行裝飾,改變了類的靜態屬性;除此之外,還可以對類進行裝飾,給類添加方法或者修改方法(通過被裝飾類的 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</code></pre>
裝飾 普通函數
不建議裝飾,因為變量提升會產生系列問題
衍生
裝飾器的使用場景基本都是 AOP 。大多數日志場景都可以使用此種模式,比如這里一個簡單的 日志場景 。
對于純前端來說,也有很多用途,比如實現一個 react 的 lazyload,就可以使用裝飾器修飾整個 class。
同時,也有一些庫實現了常用的裝飾器,比如: core-decorators.js
參考文章
來自:https://github.com/rccoder/blog/issues/23