美文网首页
浅谈JS中的装饰器

浅谈JS中的装饰器

作者: 一句话不说也不好啊 | 来源:发表于2019-04-11 09:27 被阅读0次

    什么是装饰器?

    装饰器模式(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 虽然原理非常简单,但是的确可以实现很多实用又方便的功能.

    相关文章

      网友评论

          本文标题:浅谈JS中的装饰器

          本文链接:https://www.haomeiwen.com/subject/gqyziqtx.html