Decorator 浅析与实践

1,946 阅读6分钟

Decorator (装饰器模式)

在面向对象(OOP)的设计模式中,Decorator 被称为装饰模式。OOP 的装饰模式需要通过继承和组合来实现。

通过装饰器动态地给一个对象添加一些额外的职责,就增加功能来说,装饰器模式比生成子类更为灵活;它允许向一个现有的对象添加新的功能,同时又不改变其结构。

Javascript 中的 Decorator 源于 python 之类的语言。

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

def decorator(func):

    print("decorator")
    return func

def func():
    print('func')

func = decorator(func)  
func()                  

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

func2()

点击 python 在线运行环境 查看运行结果。

这里的 @decorator 就是装饰器,利用装饰器给目标方法执行前打印出" decorator",并且并没有对原方法做任何的修改。

配置环境

decorator 还在草案阶段,所以需要 babel 支持下面给出几种方式

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 实现,代码如下:

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类本身

所以针对 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 ,然后使用上述"配置环境" 中的 2、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