从babel的编译结果来学习装饰器

1,408 阅读8分钟

1、先决条件

装饰器只能用于类和类的方法,不能用于函数,因为存在函数提升。 因此本文只阐述类和方法的装饰器

2、项目配置

本文的项目配置将会采用笔者在之前的文章中所用到的配置。从babel编译结果来学习ES6-class

3、类的装饰

3.1、 基础

装饰器是一个对类进行处理的函数。装饰器函数的第一个参数,就是所要装饰的目标类 我们来编写一个简单的装饰器

function Wrap(target) {
  return target;
}

@Wrap
class Polygon {}

运行

npm run build 

我们将会得到如下结果:

"use strict";

var _class;

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

function Wrap(target) {
  return target;
}

var Polygon =
  Wrap(
    (_class = function Polygon() {
      _classCallCheck(this, Polygon);
    })
  ) || _class; 

从babel转码的结果,我们可以知道,如果类装饰器不返回内容,将不会对类进行修改。并且我们还可以知道,如果在别的地方引用此类的话,类装饰器已经执行了。如果类装饰器返回内容的话(且为真),那么我们的这个类就被改写了。实际例子:vue-class-component的Component装饰器。

3.2、多个装饰器装饰器同一个类

后续的操作如上,将不会赘述。 源码:


function Wrap(target) {
  return target;
}

function Component(target) {
  return target;
}

function Decs(target) {
  return target;
}

@Wrap
@Component
@Decs
class Polygon {}

转码结果:


function Wrap(target) {
  return target;
}

function Component(target) {
  return target;
}

function Decs(target) {
  return target;
}

var Polygon =
  Wrap(
    (_class =
      Component(
        (_class =
          Decs(
            (_class = function Polygon() {
              _classCallCheck(this, Polygon);
            })
          ) || _class)
      ) || _class)
  ) || _class;

从这个结果,我们可以得出结论,离class越近的装饰器越先执行。

3.3 接受参数的装饰器

当我们的类装饰器需要获得参数的时候,在装饰器外面再封装一层函数。 我们先来一个错误的示范,各位读者请看,此时Component并没有写成函数的执行形式,我们来看转码结果: 源码:


function Wrap(target) {
  return target;
}

function Component(options = {}) {
  return function (target) {
    return target;
  };
}

function Decs(target) {
  return target;
}

@Wrap
@Component
@Decs
class Polygon {}

转码结果:

function Wrap(target) {
  return target;
}

function Component() {
  var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  return function (target) {
    return target;
  };
}

function Decs(target) {
  return target;
}

var Polygon =
  Wrap(
    (_class =
      Component(
        (_class =
          Decs(
            (_class = function Polygon() {
              _classCallCheck(this, Polygon);
            })
          ) || _class)
      ) || _class)
  ) || _class;

从转码结果来看,因为我们的粗心,此时Component返回的是function (target) { return target; };后面的装饰器所接收的结果均会是这个function,这样,我们的原始类已经被修改了。如果我们的类装饰器需要接收参数,一定要写成函数的执行形式,否则会给程序造成潜在的bug,这个小坑大家一定要注意。 笔者喜欢一个设计API的原则——恶心自己,成全别人。这句话的意思就是说,在设计API的时候给用户尽可能的提供提供便利性,而在设计API的时候增加很多额外的判断,这大概是老祖宗流传下来的哲学吧。


function anonymous(target) {
	return target;
}

function Component() {
	var isFunc = arguments.length > 0 && typeof arguments[0] === 'function';
    return isFunc ? anonymouse(arguments[0]) : anonymous;
}

容错能容的了一时,但是容不了一世,如:

 @Component(function(){ })
 class Polygon {}

此时我们的程序就傻眼了,反而还出现了bug,因此,对于我们开发者来说,更多的还是要仔细阅读API才是最终的解决方案。 正确编写可接受参数的装饰器之后,转码的结果:

var Polygon =
  ((_dec = Component({
    age: "26",
    name: "hsuyang",
  })),
  Wrap(
    (_class =
      _dec(
        (_class =
          Decs(
            (_class = function Polygon() {
              _classCallCheck(this, Polygon);
            })
          ) || _class)
      ) || _class)
  ) || _class);

4、 方法或属性装饰器

方法或属性装饰器需要接收3个参数,第一个参数是target,这个参数是类的原形对象,第二个参数是要装饰的字段,第三个参数是descriptor, 其中descriptor的描述如下:

interface PropertyDescriptor {
    configurable?: boolean;
    enumerable?: boolean;
    value?: any;
    writable?: boolean;
    get?(): any;
    set?(v: any): void;
}

由于装饰器在装饰属性和方法上还是有一定的区别的,因此笔者把这里单独拆开讲

4.1 属性装饰器

属性装饰器babel对于属性的初始化,运用了一个叫initializer的方法(笔者最开始学习的时候也是比较懵逼,难道阮一峰老师错了,结果阮一峰老师没有错,只是自己的见识还不够🙄)。 源码:

function readonly(target, name, descriptor) {
  descriptor.writable = false;
  descriptor.value = function () {
    console.log("hsuyang");
  };
  return descriptor;
}

class Person {
  @readonly
  name1;

  @readonly
  name2 = "I love microsoft";
}

转码结果:

"use strict";

var _class, _descriptor, _descriptor2, _temp;

function _initializerDefineProperty(target, property, descriptor, context) {
  if (!descriptor) return;
  Object.defineProperty(target, property, { enumerable: descriptor.enumerable, configurable: descriptor.configurable, writable: descriptor.writable, value: descriptor.initializer ? descriptor.initializer.call(context) : void 0 });
}

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

function _defineProperty(obj, key, value) {
  if (key in obj) {
    Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true });
  } else {
    obj[key] = value;
  }
  return obj;
}

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

function _initializerWarningHelper(descriptor, context) {
  throw new Error("Decorating class property failed. Please ensure that " + "proposal-class-properties is enabled and runs after the decorators transform.");
}

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

  descriptor.value = function () {
    console.log("hsuyang");
  };

  return descriptor;
}

var Person =
  ((_class =
    ((_temp = function Person() {
      _classCallCheck(this, Person);

      _initializerDefineProperty(this, "name1", _descriptor, this);

      _initializerDefineProperty(this, "name2", _descriptor2, this);
    }),
    _temp)),
  ((_descriptor = _applyDecoratedDescriptor(_class.prototype, "name1", [readonly], {
    configurable: true,
    enumerable: true,
    writable: true,
    initializer: null,
  })),
  (_descriptor2 = _applyDecoratedDescriptor(_class.prototype, "name2", [readonly], {
    configurable: true,
    enumerable: true,
    writable: true,
    initializer: function initializer() {
      return "I love microsoft";
    },
  }))),
  _class);

代码虽然多,但是核心就在于_applyDecoratedDescriptor和_initializerDefineProperty这两个方法。

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) {
      // 如果装饰器不返回任何东西,则默认采用这个descriptor,因为JS语言的特性,desc是一个对象,当我们把这个对象传递到函数内部执行之后,这个对象已经被修改了,因此,属性装饰器,其实不管是否return descriptor,效果都是一样的。
      return decorator(target, property, desc) || desc;
    }, desc);
  // initializer 是专门用来处理在class中对 属性或者方法进行赋值语句操作的
  // 如果赋值语句存在,则属性的值等于赋值语句的结果
  if (context && desc.initializer !== void 0) {
    //并且把赋值上下文的this绑定进去
    desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
    //当处理完initializer之后,被处理成了undefined。这很重要
    desc.initializer = undefined;
  }
  //如果,没有赋值语句,则按照普通的Object.defineProperty处理。对于Object.defineProperty不懂的读者请移步MDN。
  if (desc.initializer === void 0) {
    Object.defineProperty(target, property, desc);
    desc = null;
  }
  return desc;
}

function _initializerDefineProperty(target, property, descriptor, context) {
  if (!descriptor) return;
    Object.defineProperty(target, property, { 
    enumerable: descriptor.enumerable, 
    configurable: descriptor.configurable, 
    writable: descriptor.writable, 
    //请各位看官看仔细了,对,你确定你没有看错,如果对于没有赋值的属性,你如果想通过装饰器赋值,是不能赋值的。
    value: descriptor.initializer ? descriptor.initializer.call(context) : void 0
  });
}

各位尤其要注意这个_initializerDefineProperty方法,想要通过属性装饰器,通过修改descriptor.value,给属性设置值,是行不通的。因为之前在处理initializer的时候,处理完之后,已经被设置成了undefined,你想要通过initializer去改变属性的值,也是徒劳的。

还有另外一个奇葩操作,笔者在之前的开发中也是这样干过🤪,但是其实是不推荐的。


function Autowired(target, name, descriptor) {
  //想给类的某个字段设置一个属性值。
  target[name] = {};
}

class Person {
  @Autowired
  name1;
}

笔者在前文讲解babel转码class的时候说过,属性,不管是在构造器里面,还是外面,其都是定义在类的实例上的,而以上的赋值语句,我们其实是赋值在类的原形对象上的,当我们的类初始化之后,这个对象是拥有name1这个属性的,只不过是undefined罢了,而虽然我们在这个对象的原形上定义了name1,但是因为自身拥有name1属性,则自动忽略了原形对象上的name1属性。

因此,我们也可以得出一个小小的结论,想要通过属性装饰器去修改属性的值,是不可取的手段,自欺欺人而已😩。

4.2 方法装饰器

相对于属性装饰,方法装饰的坑要少的多。 源码:

function log(target, name, descriptor) {
  var oldValue = descriptor.value;

  descriptor.value = function () {
    console.log(`Calling ${name} with`, arguments);
    return oldValue.apply(this, arguments);
  };

  return descriptor;
}

class Person {
  @log
  calc() {}
}

转码结果:

//多余的代码就不必要贴出来了,关键就是,我们的方法装饰器,不会经过_initializerDefineProperty,因此,方法就可以通过value修改,而增加一些额外的操作(比如在方法执行器做什么事,方法执行完成之后做什么事)。
var Person =
  ((_class = /*#__PURE__*/ (function () {
    function Person() {
      _classCallCheck(this, Person);
    }

    _createClass(Person, [
      {
        key: "calc",
        value: function calc() {},
      },
    ]);

    return Person;
  })()),
  _applyDecoratedDescriptor(_class.prototype, "calc", [log], Object.getOwnPropertyDescriptor(_class.prototype, "calc"), _class.prototype),
  _class);

4.3 多个属性或者方法叠加使用

源码:

function log(target, name, descriptor) {
  var oldValue = descriptor.value;

  descriptor.value = function () {
    console.log(`Calling ${name} with`, arguments);
    return oldValue.apply(this, arguments);
  };

  return descriptor;
}

function log2(target, name, descriptor) {
  return descriptor;
}

class Person {
  @log
  @log2
  calc() {}
}

转码结果:

//...省略代码
// 将装饰器的顺序颠倒,并且迭代执行
desc = decorators
    .slice()
    .reverse()
    .reduce(function (desc, decorator) {
      return decorator(target, property, desc) || desc;
    }, desc);
// ...省略代码

// 依次读取装饰器
_applyDecoratedDescriptor(_class.prototype, "calc", [log, log2], Object.getOwnPropertyDescriptor(_class.prototype, "calc"), _class.prototype),

由此可以得出结论,属性或方法装饰器跟类的装饰器的执行顺序是一致的,离的近装饰器的先执行。

4.4 带参数的属性或方法装饰器

带参数的属性或方法装饰器,和带参数的类的装饰器类似,如:

function Prop(options = {}) {
    //做一些操作
    return function(target, prop, descriptor){
    	//做一些操作
        return descriptor;
    }
}

此处将不再赘述。

5、总结

1、装饰器总是在类定义的时候就已经执行了;

2、类装饰器只接收一个参数,这个参数是被装饰的类本身,装饰器如果有为真的返回值,则这个值用作覆盖类,如果没有的话,就是类的本身。

3、属性或方法装饰器接收三个参数,分别是target,prop,descriptor。对于babel转码的结果,属性装饰器还有一个额外的临时属性叫作initializer,用来初始属性的值。属性装饰器可以选择返回或不返回descriptor效果是一致的,因为JS对象在传递进入函数的时候,修改之后的效果将会反应在对象上,但是建议返回descriptor。

4、永远不要尝试用属性装饰器去改写属性的值。

5、带参数的装饰器一定要写成函数的执行形式。

由于笔者水平有限,写作过程中难免出现错误,若有纰漏,请各位读者指正,你们的意见将会帮助我更好的进步。本文乃笔者原创,若转载,请联系作者本人,邮箱404189928@qq.com🥰