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

相关文章
相关标签/搜索