Decorator 淺析與實踐

Decorator (裝飾器模式)

在面向對象(OOP)的設計模式中,Decorator 被稱爲裝飾模式。OOP 的裝飾模式須要經過繼承和組合來實現。javascript

經過裝飾器動態地給一個對象添加一些額外的職責,就增長功能來講,裝飾器模式比生成子類更爲靈活;它容許向一個現有的對象添加新的功能,同時又不改變其結構。java

Javascript 中的 Decorator 源於 python 之類的語言。node

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

def decorator(func):

    print("decorator")
    return func

def func():
    print('func')

func = decorator(func)  
func()                  

@decorator
def func2():
    print("func2")

func2()

複製代碼

點擊 python 在線運行環境 查看運行結果。git

這裏的 @decorator 就是裝飾器,利用裝飾器給目標方法執行前打印出" decorator",而且並無對原方法作任何的修改。es6

配置環境

decorator 還在草案階段,因此須要 babel 支持下面給出幾種方式github

babel-在線編輯環境 (須要打開 F12 )

使用 babel 編譯並執行

npm install --save-dev @babel/core \
@babel/cli \
@babel/preset-env \
@babel/plugin-proposal-decorators \
@babel/plugin-proposal-class-properties 
複製代碼
.babelrc

{
  "presets": ["@babel/preset-env"],
  "plugins": [
    [
      "@babel/plugin-proposal-decorators",
      {
        "legacy": true
      }
    ],
    ["@babel/plugin-proposal-class-properties", { "loose": true }]
  ]
}
複製代碼
npx babel test.js | node -
複製代碼

在 node 環境下運行

在上述配置的基礎上再執行如下命令
npm install --save-dev @babel/register @babel/polyfill

新建index.js
require("@babel/register")();
require("@babel/polyfill");
require('./test')


執行命令
node index
複製代碼

Javascript 中的 Decorator

從 Class 看起

es6 中的 class 可使用 Object.defineProperty 實現,代碼以下:npm

class Shopee {
  isWho() {
    console.log("One of the largest e-commerce companies in Southeast Asia");
  }
}

function Shopee() {}
Object.defineProperty(Shopee.prototype, "isWho", {
  value: function() {
    console.log("One of the largest e-commerce companies in Southeast Asia");
  },
  enumerable: false,
  configurable: true,
  writable: true
});

new Shopee().isWho();
複製代碼

ES7 Decorator

在 ES7 中的 Decorator 能夠用來裝飾 類 || 類方法 || 類屬性編程

修飾類

function isAnimal(target) {
  target.isAnimal = true;
  return target;
}

@isAnimal
class Cat {}

console.log(Cat.isAnimal); // true

複製代碼

若是把 decorator 做用到類上,則它的第一個參數 target類自己json

因此針對 class 的 decorator ,return 一個 target 便可。

那麼當一個類有多個裝飾器是怎麼樣的呢?

function dec_1(target) {
  target.value = 1;
  console.log("dec_1");
  return target;
}

function dec_2(target) {
  target.value = 2;
  console.log("dec_2");
  return target;
}

@dec_1
@dec_2
class Target {}

console.log(Target.value);

// dec_1 
// dec_2
// 1
複製代碼

decorator 的執行順序是 dec_2 -> dec_1 ,且修改的目標屬性是同一個屬性時最後執行的會覆蓋前一個,經過 babel 轉譯獲得以下代碼:

var _class;

function dec_1(target) {
  target.value = 1;
  console.log("dec_1");
  return target;
}

function dec_2(target) {
  target.value = 2;
  console.log("dec_2");
  return target;
}

let Target =
  dec_1((_class = dec_2((_class = class Target {})) || _class)) || _class;

console.log(Target.value);
複製代碼

decorator 修飾 class 的本質就是函數的嵌套,能夠從兩個方面來看:

  1. 若是代碼中函數的嵌套層級過多,致使相似 callback 或者 .then 時的死亡嵌套,可使用 decorator 展開,變成平級的結構。
  2. class 使用 extend ,在多個不一樣類之間共享或者擴展一些方法或者行爲的時候 ,層級結構會變得複雜,很難一眼就看出該 class 實際擁有了哪些方法,哪些行爲已經被擴展或修改。使用 decorator 能夠更加優雅地解決這個事情。

修飾類屬性 || 類方法

咱們利用修飾器使該方法不可寫

function readonly(target, name, descriptor) {
  descriptor.writable = false;
  return descriptor;
}

class FE {
  @readonly
  say() {
    console.log("javascipt");
  }
}

var leo = new FE();

leo.say = function() {
  console.log("C++");
};

leo.say();

// javascipt
複製代碼

咱們將以上代碼使用 ES5 實現 :

function readonly(target, name, descriptor) {
  descriptor.writable = false;
  return descriptor;
}

function FE() {}

let descriptor = {
  value: function() {
    console.log("javascipt");
  },
  enumerable: false,
  configurable: true,
  writable: true
};

descriptor = readonly(FE.prototype, "say", descriptor) || descriptor;

Object.defineProperty(FE.prototype, "say", descriptor);

var leo = new FE();

leo.say = function() {
  console.log("C++");
};

leo.say();


複製代碼

從上述代碼能夠看出,對於修飾類方法的 decorator 形參和 Object.defineProperty 的屬性值一致

Object.defineProperty(object, propertyname, descriptor)
/** * 裝飾者 * @param {Object} 類爲實例化的工廠類對象 * @param {String} name 修飾的屬性名 * @param {Object} desc 描述對象 * @return {descr} 返回一個新的描述對象 */
function decorator(target,name,desc){}
複製代碼

根據 ES7 decorate-constructor ,Decorator function 能夠不須要 return target/descriptor, 可是建議在書寫中帶上默認的 return。

decorator 爲何沒有支持 function

在 babel 中嘗試使用 decorator 裝飾方法會的到如下報錯。

看一段代碼

var decorator = function(){
    conslo.log(decorator)
}

@decorator
function target(){}


// js 存在變量的提高,會獲得一下代碼

var decorator

@decorator
function target(){}

decorator = function(){
    conslo.log(decorator)
}

// 當 decorator 執行時,decorator 仍是 undefined 

複製代碼

因爲 Javascript 中的變量提高問題,致使 decorator 的實現會變得比較複雜。 尤爲在使用模塊化編程時, var some-decorator = required('./some-decorator') 使用這個 some-decorator 修飾 function ,必然存在變量提高。

也許後續會出現修正 js 中變量提高的寫法,相似於:

@deco let f() {}
@deco const f() {}

......
複製代碼

對 Decorator 傳參

const dec = skill => target => {
  target.skill = skill;
  return target;
};

@dec("nodejs")
class FE {}

console.log(FE.skill);
複製代碼

實踐

React-redux

class MyReactComponent extends Component {}

export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);
複製代碼

connect(mapStateToProps, mapDispatchToProps) 的調用會 return 一個 function (target){}

因此咱們能夠將 connect 函數簡寫成 decorator

@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends Component {}
複製代碼

core-decorators.js

使用 npm install core-decorators --save ,而後使用上述"配置環境" 中的 二、3 點。

  • @autobind
class Person {
  getPerson() {
    return this;
  }
}

let person = new Person();
const { getPerson } = person;

console.log(getPerson() === person); // false
console.log(person.getPerson() === person); // true
複製代碼

因爲 const { getPerson } = person;getPerson 指向了全局,因此 getPerson() === personfalse, 咱們使用 autobind

import { autobind } from "core-decorators";

@autobind
class Person {
  getPerson() {
    return this;
  }
}

let person = new Person();
const { getPerson } = person;

console.log(getPerson() === person); // true
複製代碼
  • @readonly 可使 property or method 只讀。

  • @override 能夠檢測改方法是不是重寫的方法,方法名和參數名與父級保持一致,爲重寫。

import { override } from 'core-decorators';

class Parent {
  speak(first, second) {}
}

class Child extends Parent {
  @override
  speak() {}
  // SyntaxError: Child#speak() does not properly override Parent#speak(first, second)
}

// or

class Child extends Parent {
  @override
  speaks() {}
  // SyntaxError: No descriptor matching Child#speaks() was found on the prototype chain.
  //
  // Did you mean "speak"?
}
複製代碼
  • @deprecate 可標記該方法已被丟棄
import { deprecate } from 'core-decorators';

class Person {
  @deprecate
  facepalm() {}

  @deprecate('We stopped facepalming')
  facepalmHard() {}

  @deprecate('We stopped facepalming', { url: 'http://knowyourmeme.com/memes/facepalm' })
  facepalmHarder() {}
}

let person = new Person();

person.facepalm();
// DEPRECATION Person#facepalm: This function will be removed in future versions.

person.facepalmHard();
// DEPRECATION Person#facepalmHard: We stopped facepalming

person.facepalmHarder();
// DEPRECATION Person#facepalmHarder: We stopped facepalming
//
// See http://knowyourmeme.com/memes/facepalm for more details.
//
複製代碼

mixin-decorator

koa2 decorator

提供 github 使用 koa-with-decorator

登陸檢驗

// decorate 和 convert 會被複用 只寫一次
const decorate = (args, middleware) => {
  let [target, key, descriptor] = args;
  target[key].unshift(middleware);

  return descriptor;
};

const convert = middleware => (...args) => decorate(args, middleware);

export const auth = convert(async (ctx, next) => {
  if (!ctx.session.user) {
    return (ctx.body = {
      success: false,
      code: 401,
      err: "登陸信息失效,從新登陸"
    });
  }

  await next();
});

複製代碼

路由裝飾

const isArray = c => (_.isArray(c) ? c : [c]);
const symbolPrefix = Symbol("prefix");

// 存儲全部路由信息
const routerMap = new Map()

// 爲何使用 target[key] ?
const router = conf => (target, key, descriptor) => {
  routerMap.set({
    target: target,
    ...conf
  }, target[key])
}

const controller = path => target => (target.prototype[symbolPrefix] = path)

const get = path => router({
  method: 'get',
  path: path
})

複製代碼

@get @auth 的使用

const router = new Router();
const app = new Koa();

@controller('/admin')
export class adminController {
  @get('/movie/list')
  @auth
  async getMovieList (ctx, next) {
    console.log('admin movie list')
    const movies = await getAllMovies()

    ctx.body = {
      success: true,
      data: movies
    }
  }
  
  
for (let [conf, controller] of routerMap) {
      const controllers = isArray(controller);
      let prefixPath = conf.target[symbolPrefix];
      const routerPath = prefixPath + conf.path;
      router[conf.method](routerPath, ...controllers);
    }

app.use(router.routes());
app.use(router.allowedMethods());

複製代碼

總結

  • decorator 的用法使無限嵌套的函數的寫法變得優雅。使代碼變成了平級的狀態。
  • 改變多個class extend 的問題(相似 mixin)。
  • 在 core-decorators 的使用能夠看出,decorator 還有一個注註解的做用,代碼一目瞭然。
  • 不會對原有代碼進行侵入,減小修改代碼地成本。

參考

ES7 Decorator 裝飾者模式

理解Object.defineProperty的做用

阮一峯ES7 Decorator

相關文章
相關標籤/搜索