Decorator

装饰器主要用于:

  • 装饰类
  • 装饰方法或属性

装饰类

  1. @annotation
    class MyClass { }

  2. function annotation(target) {
    target.annotated = true;
    }

装饰方法或属性

  1. class MyClass {
  2. @readonly
  3. method() { }
  4. }
  5.  
  6. function readonly(target, name, descriptor) {
  7. descriptor.writable = false;
  8. return descriptor;
  9. }

Babel

安装编译

我们可以在 Babel 官网的 Try it out,查看 Babel 编译后的代码。

不过我们也可以选择本地编译:

  1. npm init
  2.  
  3. npm install --save-dev @babel/core @babel/cli
  4.  
  5. npm install --save-dev @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties

新建 .babelrc 文件

  1. {
  2. "plugins": [
  3. ["@babel/plugin-proposal-decorators", { "legacy": true }],
  4. ["@babel/plugin-proposal-class-properties", {"loose": true}]
  5. ]
  6. }

再编译指定的文件

  1. babel decorator.js --out-file decorator-compiled.js

装饰类的编译

编译前:

  1. @annotation
    class MyClass { }

  2. function annotation(target) {
    target.annotated = true;
    }

编译后:

  1. var _class;
  2.  
  3. let MyClass = annotation(_class = class MyClass {}) || _class;
  4.  
  5. function annotation(target) {
  6. target.annotated = true;
  7. }

我们可以看到对于类的装饰,其原理就是:

  1. @decorator
    class A {}

  2. // 等同于

  3. class A {}
    A = decorator(A) || A;

装饰方法的编译

编译前:

  1. class MyClass {
  2. @unenumerable
  3. @readonly
  4. method() { }
  5. }
  6.  
  7. function readonly(target, name, descriptor) {
  8. descriptor.writable = false;
  9. return descriptor;
  10. }
  11.  
  12. function unenumerable(target, name, descriptor) {
  13. descriptor.enumerable = false;
  14. return descriptor;
  15. }

编译后:

  1. var _class;
  2.  
  3. function _applyDecoratedDescriptor(target, property, decorators, descriptor, context ) {
  4. /**
  5. * 第一部分
  6. * 拷贝属性
  7. */
  8. var desc = {};
  9. Object["ke" + "ys"](descriptor).forEach(function(key) {
  10. desc[key] = descriptor[key];
  11. });
  12. desc.enumerable = !!desc.enumerable;
  13. desc.configurable = !!desc.configurable;
  14.  
  15. if ("value" in desc || desc.initializer) {
  16. desc.writable = true;
  17. }
  18.  
  19. /**
  20. * 第二部分
  21. * 应用多个 decorators
  22. */
  23. desc = decorators
  24. .slice()
  25. .reverse()
  26. .reduce(function(desc, decorator) {
  27. return decorator(target, property, desc) || desc;
  28. }, desc);
  29.  
  30. /**
  31. * 第三部分
  32. * 设置要 decorators 的属性
  33. */
  34. if (context && desc.initializer !== void 0) {
  35. desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
  36. desc.initializer = undefined;
  37. }
  38.  
  39. if (desc.initializer === void 0) {
  40. Object["define" + "Property"](target, property, desc);
  41. desc = null;
  42. }
  43.  
  44. return desc;
  45. }
  46.  
  47. let MyClass = ((_class = class MyClass {
  48. method() {}
  49. }),
  50. _applyDecoratedDescriptor(
  51. _class.prototype,
  52. "method",
  53. [readonly],
  54. Object.getOwnPropertyDescriptor(_class.prototype, "method"),
  55. _class.prototype
  56. ),
  57. _class);
  58.  
  59. function readonly(target, name, descriptor) {
  60. descriptor.writable = false;
  61. return descriptor;
  62. }

装饰方法的编译源码解析

我们可以看到 Babel 构建了一个 _applyDecoratedDescriptor 函数,用于给方法装饰。

Object.getOwnPropertyDescriptor()

在传入参数的时候,我们使用了一个 Object.getOwnPropertyDescriptor() 方法,我们来看下这个方法:

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

顺便注意这是一个 ES5 的方法。

举个例子:

  1. const foo = { value: 1 };
  2. const bar = Object.getOwnPropertyDescriptor(foo, "value");
  3. // bar {
  4. // value: 1,
  5. // writable: true
  6. // enumerable: true,
  7. // configurable: true,
  8. // }
  9.  
  10. const foo = { get value() { return 1; } };
  11. const bar = Object.getOwnPropertyDescriptor(foo, "value");
  12. // bar {
  13. // get: /*the getter function*/,
  14. // set: undefined
  15. // enumerable: true,
  16. // configurable: true,
  17. // }

第一部分源码解析

在 _applyDecoratedDescriptor 函数内部,我们首先将 Object.getOwnPropertyDescriptor() 返回的属性描述符对象做了一份拷贝:

  1. // 拷贝一份 descriptor
  2. var desc = {};
  3. Object["ke" + "ys"](descriptor).forEach(function(key) {
  4. desc[key] = descriptor[key];
  5. });
  6. desc.enumerable = !!desc.enumerable;
  7. desc.configurable = !!desc.configurable;
  8.  
  9. // 如果没有 value 属性或者没有 initializer 属性,表明是 getter 和 setter
  10. if ("value" in desc || desc.initializer) {
  11. desc.writable = true;
  12. }

那么 initializer 属性是什么呢?Object.getOwnPropertyDescriptor() 返回的对象并不具有这个属性呀,确实,这是 Babel 的 Class 为了与 decorator 配合而产生的一个属性,比如说对于下面这种代码:

  1. class MyClass {
  2. @readonly
  3. born = Date.now();
  4. }
  5.  
  6. function readonly(target, name, descriptor) {
  7. descriptor.writable = false;
  8. return descriptor;
  9. }
  10.  
  11. var foo = new MyClass();
  12. console.log(foo.born);

Babel 就会编译为:

  1. // ...
  2. (_descriptor = _applyDecoratedDescriptor(_class.prototype, "born", [readonly], {
  3. configurable: true,
  4. enumerable: true,
  5. writable: true,
  6. initializer: function() {
  7. return Date.now();
  8. }
  9. }))
  10. // ...

此时传入 _applyDecoratedDescriptor 函数的 descriptor 就具有 initializer 属性。

第二部分源码解析

接下是应用多个 decorators:

  1. /**
  2. * 第二部分
  3. * @type {[type]}
  4. */
  5. desc = decorators
  6. .slice()
  7. .reverse()
  8. .reduce(function(desc, decorator) {
  9. return decorator(target, property, desc) || desc;
  10. }, desc);

对于一个方法应用了多个 decorator,比如:

  1. class MyClass {
  2. @unenumerable
  3. @readonly
  4. method() { }
  5. }

Babel 会编译为:

  1. _applyDecoratedDescriptor(
  2. _class.prototype,
  3. "method",
  4. [unenumerable, readonly],
  5. Object.getOwnPropertyDescriptor(_class.prototype, "method"),
  6. _class.prototype
  7. )

在第二部分的源码中,执行了 reverse() 和 reduce() 操作,由此我们也可以发现,如果同一个方法有多个装饰器,会由内向外执行。

第三部分源码解析

  1. /**
  2. * 第三部分
  3. * 设置要 decorators 的属性
  4. */
  5. if (context && desc.initializer !== void 0) {
  6. desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
  7. desc.initializer = undefined;
  8. }
  9.  
  10. if (desc.initializer === void 0) {
  11. Object["define" + "Property"](target, property, desc);
  12. desc = null;
  13. }
  14.  
  15. return desc;

如果 desc 有 initializer 属性,意味着当装饰的是类的属性时,会将 value 的值设置为:

  1. desc.initializer.call(context)

而 context 的值为 _class.prototype,之所以要 call(context),这也很好理解,因为有可能

  1. class MyClass {
  2. @readonly
  3. value = this.getNum() + 1;
  4.  
  5. getNum() {
  6. return 1;
  7. }
  8. }

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

  1. Object["define" + "Property"](target, property, desc);

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

应用

1.log

为一个方法添加 log 函数,检查输入的参数:

  1. class Math {
  2. @log
  3. add(a, b) {
  4. return a + b;
  5. }
  6. }
  7.  
  8. function log(target, name, descriptor) {
  9. var oldValue = descriptor.value;
  10.  
  11. descriptor.value = function(...args) {
  12. console.log(`Calling ${name} with`, args);
  13. return oldValue.apply(this, args);
  14. };
  15.  
  16. return descriptor;
  17. }
  18.  
  19. const math = new Math();
  20.  
  21. // Calling add with [2, 4]
  22. math.add(2, 4);

再完善点:

  1. let log = (type) => {
  2. return (target, name, descriptor) => {
  3. const method = descriptor.value;
  4. descriptor.value = (...args) => {
  5. console.info(`(${type}) 正在执行: ${name}(${args}) = ?`);
  6. let ret;
  7. try {
  8. ret = method.apply(target, args);
  9. console.info(`(${type}) 成功 : ${name}(${args}) => ${ret}`);
  10. } catch (error) {
  11. console.error(`(${type}) 失败: ${name}(${args}) => ${error}`);
  12. }
  13. return ret;
  14. }
  15. }
  16. };

2.autobind

  1. class Person {
  2. @autobind
  3. getPerson() {
  4. return this;
  5. }
  6. }
  7.  
  8. let person = new Person();
  9. let { getPerson } = person;
  10.  
  11. getPerson() === person;
  12. // true

我们很容易想到的一个场景是 React 绑定事件的时候:

  1. class Toggle extends React.Component {
  2.  
  3. @autobind
  4. handleClick() {
  5. console.log(this)
  6. }
  7.  
  8. render() {
  9. return (
  10. <button onClick={this.handleClick}>
  11. button
  12. </button>
  13. );
  14. }
  15. }

我们来写这样一个 autobind 函数:

  1. const { defineProperty, getPrototypeOf} = Object;
  2.  
  3. function bind(fn, context) {
  4. if (fn.bind) {
  5. return fn.bind(context);
  6. } else {
  7. return function __autobind__() {
  8. return fn.apply(context, arguments);
  9. };
  10. }
  11. }
  12.  
  13. function createDefaultSetter(key) {
  14. return function set(newValue) {
  15. Object.defineProperty(this, key, {
  16. configurable: true,
  17. writable: true,
  18. enumerable: true,
  19. value: newValue
  20. });
  21.  
  22. return newValue;
  23. };
  24. }
  25.  
  26. function autobind(target, key, { value: fn, configurable, enumerable }) {
  27. if (typeof fn !== 'function') {
  28. throw new SyntaxError(`@autobind can only be used on functions, not: ${fn}`);
  29. }
  30.  
  31. const { constructor } = target;
  32.  
  33. return {
  34. configurable,
  35. enumerable,
  36.  
  37. get() {
  38.  
  39. /**
  40. * 使用这种方式相当于替换了这个函数,所以当比如
  41. * Class.prototype.hasOwnProperty(key) 的时候,为了正确返回
  42. * 所以这里做了 this 的判断
  43. */
  44. if (this === target) {
  45. return fn;
  46. }
  47.  
  48. const boundFn = bind(fn, this);
  49.  
  50. defineProperty(this, key, {
  51. configurable: true,
  52. writable: true,
  53. enumerable: false,
  54. value: boundFn
  55. });
  56.  
  57. return boundFn;
  58. },
  59. set: createDefaultSetter(key)
  60. };
  61. }

3.debounce

有的时候,我们需要对执行的方法进行防抖处理:

  1. class Toggle extends React.Component {
  2.  
  3. @debounce(500, true)
  4. handleClick() {
  5. console.log('toggle')
  6. }
  7.  
  8. render() {
  9. return (
  10. <button onClick={this.handleClick}>
  11. button
  12. </button>
  13. );
  14. }
  15. }

我们来实现一下:

  1. function _debounce(func, wait, immediate) {
  2.  
  3. var timeout;
  4.  
  5. return function () {
  6. var context = this;
  7. var args = arguments;
  8.  
  9. if (timeout) clearTimeout(timeout);
  10. if (immediate) {
  11. var callNow = !timeout;
  12. timeout = setTimeout(function(){
  13. timeout = null;
  14. }, wait)
  15. if (callNow) func.apply(context, args)
  16. }
  17. else {
  18. timeout = setTimeout(function(){
  19. func.apply(context, args)
  20. }, wait);
  21. }
  22. }
  23. }
  24.  
  25. function debounce(wait, immediate) {
  26. return function handleDescriptor(target, key, descriptor) {
  27. const callback = descriptor.value;
  28.  
  29. if (typeof callback !== 'function') {
  30. throw new SyntaxError('Only functions can be debounced');
  31. }
  32.  
  33. var fn = _debounce(callback, wait, immediate)
  34.  
  35. return {
  36. ...descriptor,
  37. value() {
  38. fn()
  39. }
  40. };
  41. }
  42. }

4.time

用于统计方法执行的时间:

  1. function time(prefix) {
  2. let count = 0;
  3. return function handleDescriptor(target, key, descriptor) {
  4.  
  5. const fn = descriptor.value;
  6.  
  7. if (prefix == null) {
  8. prefix = `${target.constructor.name}.${key}`;
  9. }
  10.  
  11. if (typeof fn !== 'function') {
  12. throw new SyntaxError(`@time can only be used on functions, not: ${fn}`);
  13. }
  14.  
  15. return {
  16. ...descriptor,
  17. value() {
  18. const label = `${prefix}-${count}`;
  19. count++;
  20. console.time(label);
  21.  
  22. try {
  23. return fn.apply(this, arguments);
  24. } finally {
  25. console.timeEnd(label);
  26. }
  27. }
  28. }
  29. }
  30. }

5.mixin

用于将对象的方法混入 Class 中:

  1. const SingerMixin = {
  2. sing(sound) {
  3. alert(sound);
  4. }
  5. };
  6.  
  7. const FlyMixin = {
  8. // All types of property descriptors are supported
  9. get speed() {},
  10. fly() {},
  11. land() {}
  12. };
  13.  
  14. @mixin(SingerMixin, FlyMixin)
  15. class Bird {
  16. singMatingCall() {
  17. this.sing('tweet tweet');
  18. }
  19. }
  20.  
  21. var bird = new Bird();
  22. bird.singMatingCall();
  23. // alerts "tweet tweet"

mixin 的一个简单实现如下:

  1. function mixin(...mixins) {
  2. return target => {
  3. if (!mixins.length) {
  4. throw new SyntaxError(`@mixin() class ${target.name} requires at least one mixin as an argument`);
  5. }
  6.  
  7. for (let i = 0, l = mixins.length; i < l; i++) {
  8. const descs = Object.getOwnPropertyDescriptors(mixins[i]);
  9. const keys = Object.getOwnPropertyNames(descs);
  10.  
  11. for (let j = 0, k = keys.length; j < k; j++) {
  12. const key = keys[j];
  13.  
  14. if (!target.prototype.hasOwnProperty(key)) {
  15. Object.defineProperty(target.prototype, key, descs[key]);
  16. }
  17. }
  18. }
  19. };
  20. }

6.redux

实际开发中,React 与 Redux 库结合使用时,常常需要写成下面这样。

  1. class MyReactComponent extends React.Component {}
  2.  
  3. export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);

有了装饰器,就可以改写上面的代码。

  1. @connect(mapStateToProps, mapDispatchToProps)
    export default class MyReactComponent extends React.Component {};

相对来说,后一种写法看上去更容易理解。

7.注意

以上我们都是用于修饰类方法,我们获取值的方式为:

  1. const method = descriptor.value;

但是如果我们修饰的是类的实例属性,因为 Babel 的缘故,通过 value 属性并不能获取值,我们可以写成:

  1. const value = descriptor.initializer && descriptor.initializer();

参考

ES6 系列

ES6 系列目录地址:https://github.com/mqyqingfeng/Blog

ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。