ES7 Decorator 入门解析

范文杰

Decorator 提供了一种独特的抽象逻辑,可在原有代码基础上,零侵入添加新功能特性。商业代码总是多种交织并存的,在日常开发中,除了实现业务功能之外,我们还需要考虑诸如:异常处理、性能分析、日志等额外的需求。未经设计的的开发方法会倾向于将各种需求耦合组成一个功能模块,比如:

class Math{
  static add(num1,num2){
    try{
      console.time('some label');
      log('log for something');
      const result= num1+num2;
      console.timeEnd('some label');
      return result;
    }catch(e){
      error('something had broken');
    }
  }
}

上述简单的两数相加功能,在添加各类需求之后,已经变的面目全非。Decorator 语法通过描述,可将功能特性叠加到原有功能中:

class Math{
  @log
  @error
  @time
  static add(num1,num2){
    return num1+num2;
  }
}

Decorator 是什么

Decorator 就是一个的包裹函数,运行时在编译阶段调用该函数,修改目标对象的行为、属性。我们先来看一个简单实例:

const log = (target,prop)=>console.log(`Wrap function: '${prop}'`);

const tec={
  @log
  say(){
    console.log('hello world')
  }
}

// => Wrap function 'say'

Decorator 函数签名如下:

// @param   target  作用对象
// @param   prop    作用的属性名
// @param   descriptor  属性描述符
// @return  descriptor  属性描述符
function decorator(target,prop,descriptor){}

参数详解:

  1. target : 作用的对象,有如下情况:

    • 作用于 class 时,target 为该 class 函数

    • 作用于 class 中的函数、属性 时,target 为该 class 的 prototype 对象

    • 作用于 对象字面量中的函数、属性 时,target 为该对象

  2. prop : 描述的属性名,若decorator作用于class时,该参数为空

  3. descriptor : 属性原本的描述符,该描述符可通过 Object.getOwnPropertyDescriptor() 获取,若decorator作用于class时,该参数为空

decorator 函数支持返回描述符undefined,当返回值为描述符时,运行时会调用Object.defineProperty()修改原有属性。

Decorator 的ES5实现

理解 Decorator 机制,最佳方式是使用ES5实现该过程。
class装饰器机制比较简单,仅做一层包装,伪代码:

// 调用实例
@log 
class Person{}
// 实现代码
const Person = log(Person);

属性装饰器机制则比较复杂,babel 就此提供了一个参考范例:

// decorator 处理
function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) {
  var desc = {};
  Object['ke' + 'ys'](descriptor).forEach(function (key) {
    desc[key] = descriptor[key];
  });
  desc.enumerable = !!desc.enumerable;
  desc.configurable = !!desc.configurable;

  if ('value' in desc || desc.initializer) {
    desc.writable = true;
  }

  desc = decorators.slice().reverse().reduce(function (desc, decorator) {
    return decorator(target, property, desc) || desc;
  }, desc);

  if (context && desc.initializer !== void 0) {
    desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
    desc.initializer = undefined;
  }

  if (desc.initializer === void 0) {
    Object['define' + 'Property'](target, property, desc);
    desc = null;
  }

  return desc;
}

// 调用实例
class Person{
  @log
  say(){}
}

// 实现代码
_applyDecoratedDescriptor(
  Person.prototype, 
  'say', 
  [log],
  Object.getOwnPropertyDescriptor(Person.prototype, 'say'),
  Person.prototype)
)

用例

Decorator 主要应用于如下几类对象:

  1. class

  2. class 中,除构造函数外的方法

  3. class 中的属性

  4. 对象字面量中的函数

  5. 对象字面量中的属性


// 类
@log
class Person{
  // 函数
  @log
  say(){}
  
  // 属性
  @log
  name = 'tec';
}

// 同样适用于对象字面量的方法、属性
const tec = {
  @log
  name:'tec',
  
  @log
  walk(){}
};

Decorator 实践

在JS中,Decorator 是一个新概念,对于多数没有接触过诸如python、C#的开发者而言,很难理解实际应用场景。幸运的是github已经有人封装了常用Decorator。笔者分析该库,总结如下几种定义模式:

  1. 通过 descriptor 的 value 值修改:

function decorate(target, key, descriptor) {
  const fn = descriptor.value;

  return {
    ...descriptor,
    value() {
      return fn.apply(this, arguments);
    }
  }
}
  1. 通过 descriptor 的 getset 函数修改:

function decorate(target, key, descriptor) {
  let value = descriptor.value;

  return {
    ...descriptor,
    get() {
      return value;
    }
    set(v) {
      value=v;
    }
  }
}
  1. 通过 descriptor 的 writableenumerable 等属性修改:

function readonly(target, key, descriptor) {
  return {
    ...descriptor,
    writable:false
  }
}
  1. 针对 class ,返回包裹函数

function log(target){
  let initTimes=0;
  return (...arg)=>{
    console.log(++initTimes);
    target.call(this,...arg);
  };
}

在实际开发中,还需要注意以下事项:

  1. Decorator 的目标是在原有功能基础上,添加功能,切忌覆盖原有功能

  2. Decorator 不是管道模式,decorator之间不存在交互,所以必须注意保持decorator独立性、透明性

  3. Decorator 更适用于非业务功能需求

  4. 确定 decorator 的用途后,切记执行判断参数类型

  5. decorator 针对每个装饰目标,仅执行一次

参考资料:

  1. babel 装饰器插件

  2. Object.defineProperty

  3. 属性描述符

阅读 5.2k

avatar
范文杰
字节跳动 前端工程师
812 声望
3.8k 粉丝
0 条评论
你知道吗?

avatar
范文杰
字节跳动 前端工程师
812 声望
3.8k 粉丝
宣传栏