最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • JavaScript 装饰器原理

    正文概述 掘金(DjangoXiang)   2020-12-18   440

    装饰器是一个以@开头的描述性词语。英语的decorator动词是decorate,装饰的意思。其中词根dek(dec发音)原始印欧语系中意思是“接受”。即,原来的某个事物接受一些新东西(而变得更好)。从另外一个角度描述,装饰器主要是在被装饰对象的外部起作用,而非入侵其内部发生什么改变。装饰器模式同时也是一种开发模式,其地位虽然弱于MVC、IoC等,但不失为一种优秀的模式。

    JavaScript的装饰器可能是借鉴自Python也或许是Java。较为明显的不同的是大部分语言的装饰器必须是一行行分开,而JS的装饰器可以在一行中。

    装饰器存在的意义

    举个例子:我拿着员工卡进入公司总部大楼。因为每个员工所属的部门、级别不同,并不能进入大楼的任何房间。每个房间都有一扇门;那么,公司需要安排每个办公室里至少一个人关于验证来访者的工作:

    1. 先登记来访者
    2. 验证是否有权限进入,如果没有则要求其离开
    3. 记录其离开时间

    还有一个选择方式,就是安装电子门锁,门锁只是将员工卡的信息传输给机房,由特定的程序验证。

    前者暂且称之为笨模式,代码如下:

    function A101(who){
      record(who,new Date(),'enter');
      if (!permission(who)) {
        record(who,new Date(),'no permission')
        return void;
      }
      // 继续执行
      doSomeWork();
      record(who,new Date(),'leave')
    }
    
    function A102(who){
    record(who,new Date(),'enter');
      if (!permission(who)) {
        record(who,new Date(),'no permission')
        return void;
      }
      // 继续执行
      doSomeWork();
      record(who,new Date(),'leave')
    }
    
    // ... 
    

    有经验的大家肯定第一时间想到了,把那些重复语句封装为一个方法,并统一调用。是的,这样可以解决大部分问题,但是还不够“优雅”。同时还有另外一个问题,如果“房间”特别多,又或者只有大楼奇数号房间要验证偶数不验证,那岂不是很“变态”?如果使用装饰器模式来做,代码会如下面这样的:

    @verify(who)
    class Building {
      @verify(who)
      A101(){/*...*/}
      @verify(who)
      A102(){/*...*/}
      //...
    }
    

    verify是验证的装饰器,而其本质就是一组函数。

    JavaScript装饰器

    正如先前的那个例子,装饰器其实本身就是一个函数,它在执行被装饰的对象之前先被执行。

    在JavaScript中,装饰器的类型有:

    • 存取方法(属性的get和set)
    • 字段
    • 方法

    由于目前装饰器概念还处于提案阶段,不是一个正式可用的JS功能,所以想要使用这个功能,不得不借助翻译器工具,例如Babel工具或者TypeScript编译JS代码转后才能被执行。我们需要先搭建运行环境,配置一些参数。(以下过程,假设已经正确安装了NodeJS开发环境以及包管理工具)

    cd project && npm init
    npm i -D @babel/cli @babel/core @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators @babel/preset-env babel-plugin-parameter-decorator
    

    创建一个.babelrc配置文件,如下:

    {
      "presets": ["@babel/preset-env"],
      "plugins": [
        ["@babel/plugin-proposal-decorators", { "legacy": true }],
        ["@babel/plugin-proposal-class-properties", { "loose": true }],
        "babel-plugin-parameter-decorator"
      ]
    }
    

    利用下面的转换命令,我们可以得到ES5的转换程序:

    类装饰器

    创建一个使用装饰器的JS程序decorate-class.js

    @classDecorator
    class Building {
      constructor() {
        this.name = "company";
      }
    }
    
    const building = new Building();
    
    function classDecorator(target) {
      console.log("target", target);
    }
    

    以上是最最简单的装饰器程序,我们利用babel将其“翻译”为ES5的程序,然后再美化一下后得到如下程序

    "use strict";
    
    var _class;
    
    function _classCallCheck(instance, Constructor) {
      if (!(instance instanceof Constructor)) {
        throw new TypeError("Cannot call a class as a function");
      }
    }
    
    var Building =
      classDecorator(
        (_class = function Building() {
          _classCallCheck(this, Building);
    
          this.name = "company";
        })
      ) || _class;
    
    var building = new Building();
    
    function classDecorator(target) {
      console.log("target", target);
    }
    

    第12行就是在类生成过程中,调用函数形态的装饰器,并将构造函数(类本身)送入其中。同样揭示了装饰器的第一个参数是类的构造函数的由来。

    方法 (method)装饰器

    稍微修改一下代码,依旧是尽量保持最简单:

    class Building {
      constructor() {
        this.name = "company";
      }
      @methodDecorator
      openDoor() {
        console.log("The door being open");
      }
    }
    
    const building = new Building();
    
    function methodDecorator(target, property, descriptor) {
      console.log("target", target);
      if (property) {
        console.log("property", property);
      }
      if (descriptor) {
        console.log("descriptor", descriptor);
      }
      console.log("=====end of decorator=========");
    }
    

    然后转换代码,可以发现,这次代码量突然增大了很多。排除掉_classCallCheck_defineProperties_createClass三个函数,关注_applyDecoratedDescriptor函数:

    function _applyDecoratedDescriptor(
      target,
      property,
      decorators,
      descriptor,
      context
    ) {
      var desc = {};
      Object.keys(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.defineProperty(target, property, desc);
        desc = null;
      }
      return desc;
    }
    

    它在生成构造函数之后,执行了这个函数,特别注意,这个装饰器函数是以数组形式的参数传递的。然后到上述代码的17~22行,将装饰器逐个应用,其中对装饰器的调用就在第21行。它发送了3个参数,target指类本身。property指方法名(或者属性名),desc是可能被先前装饰器被处理过的descriptor,如果是第一次循环或只有一个装饰器,那么就是方法或属性本身的descriptor。

    存取器(accessor)装饰

    JS关于类的定义中,支持get和set关键字针对设置某个字段的读写操作逻辑,装饰器也同样支持这类方法的操作。

    class Building {
      constructor() {
        this.name = "company";
      }
      @propertyDecorator
      get roomNumber() {
        return this._roomNumber;
      }
    
      _roomNumber = "";
      openDoor() {
        console.log("The door being open");
      }
    }
    

    有心的读者可能已经发现了,存取器装饰的代码与上面的方法装饰代码非常接近。关于属性 get和set方法,其本身也是一种方法的特殊形态。所以他们之间的代码就非常接近了。

    属性装饰器

    继续修改源代码:

    class Building {
      constructor() {
        this.name = "company";
      }
      @propertyDecorator
      roomNumber = "";
    }
    
    const building = new Building();
    
    function propertyDecorator(target, property, descriptor) {
      console.log("target", target);
      if (property) {
        console.log("property", property);
      }
      if (descriptor) {
        console.log("descriptor", descriptor);
      }
      console.log("=====end of decorator=========");
    }
    
    

    转换后的代码,还是与上述属性、存取器的代码非常接近。但除了_applyDecoratedDescriptor外,还多了一个_initializerDefineProperty函数。这个函数在生成构造函数时,将声明的各种字段绑定给对象。

    参数装饰器

    参数装饰器的使用位置较之前集中装饰器略有不同,它被使用在行内。

    class Building {
      constructor() {
        this.name = "company";
      }
      openDoor(@parameterDecorator num, @parameterDecorator zoz) {
        console.log(`${num} door being open`);
      }
    }
    
    const building = new Building();
    
    function parameterDecorator(target, property, key) {
      console.log("target", target);
      if (property) {
        console.log("property", property);
      }
      if (key) {
        console.log("key", key);
      }
      console.log("=====end of decorator=========");
    }
    

    转换后的代码区别就比较明显了,babel并没有对其生成一个特定的函数对其进行特有的操作,而只在创建完类(构造函数)以及相关属性、方法后直接调用了开发者自己编写的装饰器函数:

    var Building = /*#__PURE__*/function () {
      function Building() {
        _classCallCheck(this, Building);
    
        this.name = "company";
      }
    
      _createClass(Building, [{
        key: "openDoor",
        value: function openDoor(num, zoz) {
          console.log("".concat(num, " door being open"));
        }
      }]);
    
      parameterDecorator(Building.prototype, "openDoor", 1);
      parameterDecorator(Building.prototype, "openDoor", 0);
      return Building;
    }();
    

    装饰器参数的解决方案——闭包

    以上所有的案例,装饰器本身均没有使用任何参数。然实际应用中,经常会需要有特定的参数需求。我们再回到一开头的例子中verify(who),其中需要传入一个身份变量。哪又怎么做?我们少许改变一下类装饰器的代码:

    const who = "Django";
    @classDecorator(who)
    class Building {
      constructor() {
        this.name = "company";
      }
    }
    

    转换后得到

    // ...
    var who = "Django";
    var Building =
      ((_dec = classDecorator(who)),
      _dec(
        (_class = function Building() {
          _classCallCheck(this, Building);
    
          this.name = "company";
        })
      ) || _class);
    // ...
    

    请注意第4第5行,它先执行了装饰器,然后再用返回值将类(构造函数)送入。相对应的,我们就应该将构造函数写成下面这样:

    function classDecorator(people) {
      console.log(`hi~ ${people}`);
      return function (target) {
        console.log("target", target);
      };
    }
    

    同样的,方法、存取器、属性和参数装饰器均是如此。

    装饰顺序

    通过阅读转换后的代码,我们知道装饰器工作的时刻是在类被实例化之前,在生成之中完成装饰函数的动作。那么,如果不同类型的多个装饰器同时作用,其过程是怎样的?我们将先前的案例全部整合到一起看看:

    const who = "Django";
    @classDecorator(who)
    class Building {
      constructor() {
        this.name = "company";
      }
    
      @propertyDecorator
      roomNumber = "";
    
      @methodDecorator
      openDoor(@parameterDecorator num) {
        console.log(`${num} door being open`);
      }
    
      @accessorDecorator
      get roomNumber() {
        return this._roomNumber;
      }
    }
    
    const building = new Building();
    
    function classDecorator(people) {
      console.log(`class decorator`);
      return function (target) {
        console.log("target", target);
      };
    }
    
    function methodDecorator(target, property, descriptor) {
      console.log("method decorator");
    }
    
    function accessorDecorator(target, property, descriptor) {
      console.log("accessor decorator");
    }
    
    function propertyDecorator(target, property, descriptor) {
      console.log("property decoator");
    }
    
    function parameterDecorator(target, property, key) {
      console.log("parameter decorator");
    }
    

    还可以通过阅读转换后的源代码得到执行顺序:

    1. 类装饰器(在最外层)
    2. 参数装饰器(在生成构造函数最里层)
    3. 按照出现的先后顺序的:属性、方法和存取器

    总结

    装饰器是一种优雅的开发模式,极大的方便了开发者编码过程,同时提升了代码的可读性。我们在使用装饰器开发时,还是非常有必要了解其运行机理。

    另外,都看到这里了,赏个“赞”吧……


    起源地下载网 » JavaScript 装饰器原理

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元