阅读 131

浅谈js中的装饰器

什么是装饰器?

装饰器模式(Decorator Pattern)是一种结构型设计模式,旨在促进代码复用,可以用于修改现有的系统,希望在系统中为对象添加额外的功能,同时又不需要大量修改原有的代码。

JS中的装饰器是ES7中的一个新语法,可以对方法属性进行修饰,从而进行一些相关功能定制, 它的写法与Java的注解(Annotation)类似,但是功能有比较大的区别。

大家可能听说过 组合函数 和 高阶函数 的概念,也可以这么理解。

我们先来看一下以下代码:

function doSomething(name) {
  console.log('Hi, I\'' + name);
}

funtion useLogging(func, name) {
    console.log('Starting');
    func(name);
    console.log('Finished');
}
复制代码

以上逻辑不难理解,给原有的函数加一个打日志的功能,但是这样的话,每次都要传参数给useLogging,而且破坏了之前的代码结构,之前直接doSomething就好了,现在要改成useLogging(doSomething, 'Jiang')。 那有没有更好的方式呢,当然有啦。

简单装饰器:

function useLogging(func) {
    return function() {
        console.log('Starting');
        const result = func.apply(this, arguments)
        console.log('Done');
        return result;
    }
}

const wrapped = useLogging(doSomething);
复制代码

以上代码返回了一个新的函数 wrapped , 调用方式和doSomething相同,在原来的基础上能做多一点事情。

doSomething('angry');
// Hi, I'angry

const wrapped = useLogging(doSomething);


wrapped('angry');
// Starting
// Hi, I'angry
// Done
复制代码

怎么使用装饰器?

装饰器主要有两种用法:

  • 装饰类方法或属性(类成员)
class MyClass {
  @readonly
  method() { }
}

function readonly(target, name, descriptor) {
  descriptor.writable = false;
  return descriptor;
}
复制代码
  • 装饰类
@annotation
class MyClass { }

function annotation(target) {
   target.annotated = true;
}
复制代码

类成员装饰器

类成员装饰器用来装饰类里面的属性、方法、gettersetter。这个装饰器函数调用三个参数调:

  • target: 被装饰的类的原型
  • name: 被装饰的类、属性、方法的名字
  • descriptor: 被装饰的类、属性、方法的descriptor,将传递给Object.defineProperty

我们来写几个装饰器,代码如下:

写一个@readonly装饰器,简单版实现:

class Example {
  @log
  add(a, b) {
    return a + b;
  }

  @unenumerable
  @readonly
  name = "alibaba"
}

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

function unenumerable(target, name, descriptor) {
  descriptor.enumerable = false;
  return descriptor;
}

function log(target, name, descriptor) {
  const original = descriptor.value;
  if (typeof original === 'function') {
    descriptor.value = function(...args) {
      console.log(`Arguments: ${args}`);
      try {
        const result = original.apply(this, args);
        console.log(`Result: ${result}`);
        return result;
      } catch (e) {
        console.log(`Error: ${e}`);
        throw e;
      }
    }
  }
  return descriptor;
}

const e = new Example();

// Calling add with [2, 4]
e.add(2, 4);
e.name = 'antd'; // Error
复制代码

我们可以通过Babel查看编译后的代码,也可以在本地编译。

npm i @babel/core @babel/cli
npm i @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties -D
复制代码

.babelrc文件

{
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "legacy": true }],
    ["@babel/plugin-proposal-class-properties", {"loose": true}]
  ]
}
复制代码

编译 ES6 语法输出到文件

因为没用全局安装@babel/cli, 建议用 npx 命令来执行,或者./node_modules/.bin/babel,关于npx命令,可以看下官方文档

npx babel decorator.js --out-file complied.js
复制代码

编译后的代码:

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;
}

_applyDecoratedDescriptor(_class.prototype, "add", [log], Object.getOwnPropertyDescriptor(_class.prototype, "add"), _class.prototype)
复制代码

Babel 构建了一个 _applyDecoratedDescriptor函数,用于装饰类成员

Object.getOwnPropertyDescriptor

Object.getOwnPropertyDescriptor()方法返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性),不是原型链上的这点很关键。

详情可以查看官方文档,这里就不细说了。

var desc = {};
  // 这里对 descriptor 属性做了一层拷贝
  Object['ke' + 'ys'](descriptor).forEach(function (key) {
    desc[key] = descriptor[key];
  });
  desc.enumerable = !!desc.enumerable;
  desc.configurable = !!desc.configurable;
  // 没有 value 或者 initializer 属性的,都是 get 和 set 方法
  if ('value' in desc || desc.initializer) {
    desc.writable = true;
  }
复制代码

这里的 initializer 是 Babel 为了配合 decorator 而产生的一个属性,就比方说对于上面代码中的 name 属性,被编译成:

_descriptor = _applyDecoratedDescriptor(_class.prototype, "name", [unenumerable, readonly], {
  configurable: true,
  enumerable: true,
  writable: true,
  initializer: function initializer() {
    return "alibaba";
  }
})
复制代码
desc = decorators.slice().reverse().reduce(function (desc, decorator) {
    return decorator(target, property, desc) || desc;
  }, desc);
复制代码

处理多个 decorator 的情况,这里执行了slice()和reverse(),所以我们可以得出,一个类成员有多个装饰器,会由内向外执行。

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;
复制代码

最后无论是装饰方法还是属性,都会执行:

Object["define" + "Property"](target, property, desc);
复制代码

由此可见,装饰方法本质上还是使用 Object.defineProperty() 来实现的。

类装饰器

类装饰器相对简单

function log(Class) {
  return (...args) => {
    console.log(args);
    return new Class(...args);
  };
}
复制代码
@log
class Example {
  constructor(name, age) {
  }
}

const e = new Example('Graham', 34);
// [ 'Graham', 34 ]
console.log(e);
// Example {}
复制代码

装饰器中传入参数:

function log(name) {
  return function decorator(Class) {
    return (...args) => {
      console.log(`Arguments for ${name}: args`);
      return new Class(...args);
    };
  }
}

@log('Demo')
class Example {
  constructor(name, age) {}
}

const e = new Example('Graham', 34);
// Arguments for Demo: args
console.log(e);
// Example {}
复制代码

应用

在 React 中,经常会用到 redux 或者高阶组件。

class A extends React.Component {}
export default connect()(A);
复制代码

装饰器写法:

@connect()
export default connect()(A);
复制代码

总结

Decorator 虽然原理非常简单,但是的确可以实现很多实用又方便的功能.

关注下面的标签,发现更多相似文章
评论