前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >干货 | ES6 系列之我们来聊聊装饰器

干货 | ES6 系列之我们来聊聊装饰器

作者头像
腾讯NEXT学位
发布2020-02-11 09:40:33
5660
发布2020-02-11 09:40:33
举报
文章被收录于专栏:腾讯NEXT学位腾讯NEXT学位

       点击上方“腾讯NEXT学院”关注我们

Decorator

装饰器主要用于:

1. 装饰类

2. 装饰方法或属性

1 .装饰类

代码语言:javascript
复制
@annotationclass MyClass { }
function annotation(target) {   target.annotated = true;}

2. 装饰方法或属性

代码语言:javascript
复制
class MyClass {  @readonly  method() { }}
function readonly(target, name, descriptor) {  descriptor.writable = false;  return descriptor;}

Babel

安装编译

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

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

代码语言:javascript
复制
npm init
npm install --save-dev @babel/core @babel/cli
npm install --save-dev @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties

新建 .babelrc 文件

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

再编译指定的文件

代码语言:javascript
复制
babel decorator.js --out-file decorator-compiled.js

装饰类的编译

编译前:

代码语言:javascript
复制
@annotationclass MyClass { }
function annotation(target) {   target.annotated = true;}

编译后:

代码语言:javascript
复制
var _class;
let MyClass = annotation(_class = class MyClass {}) || _class;
function annotation(target) {  target.annotated = true;}

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

代码语言:javascript
复制
@decoratorclass A {}
// 等同于
class A {}A = decorator(A) || A;

装饰方法的编译

编译前:

代码语言:javascript
复制
class MyClass {  @unenumerable  @readonly  method() { }}
function readonly(target, name, descriptor) {  descriptor.writable = false;  return descriptor;}
function unenumerable(target, name, descriptor) {  descriptor.enumerable = false;  return descriptor;}

编译后:

代码语言:javascript
复制
var _class;
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;    }
    /**     * 第二部分     * 应用多个 decorators     */    desc = decorators        .slice()        .reverse()        .reduce(function(desc, decorator) {            return decorator(target, property, desc) || desc;        }, desc);
    /**     * 第三部分     * 设置要 decorators 的属性     */    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;}
let MyClass = ((_class = class MyClass {    method() {}}),_applyDecoratedDescriptor(    _class.prototype,    "method",    [readonly],    Object.getOwnPropertyDescriptor(_class.prototype, "method"),    _class.prototype),_class);
function readonly(target, name, descriptor) {    descriptor.writable = false;    return descriptor;}

装饰方法的编译源码解析

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

Object.getOwnPropertyDescriptor()

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

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

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

举个例子:

代码语言:javascript
复制
const foo = { value: 1 };const bar = Object.getOwnPropertyDescriptor(foo, "value");// bar {//   value: 1,//   writable: true//   enumerable: true,//   configurable: true,// }
const foo = { get value() { return 1; } };const bar = Object.getOwnPropertyDescriptor(foo, "value");// bar {//   get: /*the getter function*/,//   set: undefined//   enumerable: true,//   configurable: true,// }

第一部分源码解析

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

代码语言:javascript
复制
// 拷贝一份 descriptorvar desc = {};Object["ke" + "ys"](descriptor).forEach(function(key) {    desc[key] = descriptor[key];});desc.enumerable = !!desc.enumerable;desc.configurable = !!desc.configurable;
// 如果没有 value 属性或者没有 initializer 属性,表明是 getter 和 setterif ("value" in desc || desc.initializer) {    desc.writable = true;}

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

代码语言:javascript
复制
class MyClass {  @readonly  born = Date.now();}
function readonly(target, name, descriptor) {  descriptor.writable = false;  return descriptor;}
var foo = new MyClass();console.log(foo.born);

Babel 就会编译为:

代码语言:javascript
复制
// ...(_descriptor = _applyDecoratedDescriptor(_class.prototype, "born", [readonly], {    configurable: true,    enumerable: true,    writable: true,    initializer: function() {        return Date.now();    }}))// ...

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

第二部分源码解析

接下是应用多个 decorators:

代码语言:javascript
复制
/** * 第二部分 * @type {[type]} */desc = decorators    .slice()    .reverse()    .reduce(function(desc, decorator) {        return decorator(target, property, desc) || desc;    }, desc);

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

代码语言:javascript
复制
class MyClass {  @unenumerable  @readonly  method() { }}

Babel 会编译为:

代码语言:javascript
复制
_applyDecoratedDescriptor(    _class.prototype,    "method",    [unenumerable, readonly],    Object.getOwnPropertyDescriptor(_class.prototype, "method"),    _class.prototype)

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

第三部分源码解析

代码语言:javascript
复制
/** * 第三部分 * 设置要 decorators 的属性 */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;

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

代码语言:javascript
复制
desc.initializer.call(context)

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

代码语言:javascript
复制
class MyClass {  @readonly  value = this.getNum() + 1;
  getNum() {    return 1;  }}

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

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

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

应用

1.log

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

代码语言:javascript
复制
class Math {  @log  add(a, b) {    return a + b;  }}
function log(target, name, descriptor) {  var oldValue = descriptor.value;
  descriptor.value = function(...args) {    console.log(`Calling ${name} with`, args);    return oldValue.apply(this, args);  };
  return descriptor;}
const math = new Math();
// Calling add with [2, 4]math.add(2, 4);

再完善点:

代码语言:javascript
复制
let log = (type) => {  return (target, name, descriptor) => {    const method = descriptor.value;    descriptor.value =  (...args) => {      console.info(`(${type}) 正在执行: ${name}(${args}) = ?`);      let ret;      try {        ret = method.apply(target, args);        console.info(`(${type}) 成功 : ${name}(${args}) => ${ret}`);      } catch (error) {        console.error(`(${type}) 失败: ${name}(${args}) => ${error}`);      }      return ret;    }  }};

2.autobind

代码语言:javascript
复制
class Person {  @autobind  getPerson() {    return this;  }}
let person = new Person();let { getPerson } = person;
getPerson() === person;// true

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

代码语言:javascript
复制
class Toggle extends React.Component {
  @autobind  handleClick() {      console.log(this)  }
  render() {    return (      <button onClick={this.handleClick}>        button      </button>    );  }}

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

代码语言:javascript
复制
const { defineProperty, getPrototypeOf} = Object;
function bind(fn, context) {  if (fn.bind) {    return fn.bind(context);  } else {    return function __autobind__() {      return fn.apply(context, arguments);    };  }}
function createDefaultSetter(key) {  return function set(newValue) {    Object.defineProperty(this, key, {      configurable: true,      writable: true,      enumerable: true,      value: newValue    });
    return newValue;  };}
function autobind(target, key, { value: fn, configurable, enumerable }) {  if (typeof fn !== 'function') {    throw new SyntaxError(`@autobind can only be used on functions, not: ${fn}`);  }
  const { constructor } = target;
  return {    configurable,    enumerable,
    get() {
      /**       * 使用这种方式相当于替换了这个函数,所以当比如       * Class.prototype.hasOwnProperty(key) 的时候,为了正确返回       * 所以这里做了 this 的判断       */      if (this === target) {        return fn;      }
      const boundFn = bind(fn, this);
      defineProperty(this, key, {        configurable: true,        writable: true,        enumerable: false,        value: boundFn      });
      return boundFn;    },    set: createDefaultSetter(key)  };}

3.debounce

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

代码语言:javascript
复制
class Toggle extends React.Component {
  @debounce(500, true)  handleClick() {    console.log('toggle')  }
  render() {    return (      <button onClick={this.handleClick}>        button      </button>    );  }}

我们来实现一下:

代码语言:javascript
复制
function _debounce(func, wait, immediate) {
    var timeout;
    return function () {        var context = this;        var args = arguments;
        if (timeout) clearTimeout(timeout);        if (immediate) {            var callNow = !timeout;            timeout = setTimeout(function(){                timeout = null;            }, wait)            if (callNow) func.apply(context, args)        }        else {            timeout = setTimeout(function(){                func.apply(context, args)            }, wait);        }    }}
function debounce(wait, immediate) {  return function handleDescriptor(target, key, descriptor) {    const callback = descriptor.value;
    if (typeof callback !== 'function') {      throw new SyntaxError('Only functions can be debounced');    }
    var fn = _debounce(callback, wait, immediate)
    return {      ...descriptor,      value() {        fn()      }    };  }}

4.time

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

代码语言:javascript
复制
function time(prefix) {  let count = 0;  return function handleDescriptor(target, key, descriptor) {
    const fn = descriptor.value;
    if (prefix == null) {      prefix = `${target.constructor.name}.${key}`;    }
    if (typeof fn !== 'function') {      throw new SyntaxError(`@time can only be used on functions, not: ${fn}`);    }
    return {      ...descriptor,      value() {        const label = `${prefix}-${count}`;        count++;        console.time(label);
        try {          return fn.apply(this, arguments);        } finally {          console.timeEnd(label);        }      }    }  }}

5.mixin

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

代码语言:javascript
复制
const SingerMixin = {  sing(sound) {    alert(sound);  }};
const FlyMixin = {  // All types of property descriptors are supported  get speed() {},  fly() {},  land() {}};
@mixin(SingerMixin, FlyMixin)class Bird {  singMatingCall() {    this.sing('tweet tweet');  }}
var bird = new Bird();bird.singMatingCall();// alerts "tweet tweet"

mixin 的一个简单实现如下:

代码语言:javascript
复制
function mixin(...mixins) {  return target => {    if (!mixins.length) {      throw new SyntaxError(`@mixin() class ${target.name} requires at least one mixin as an argument`);    }
    for (let i = 0, l = mixins.length; i < l; i++) {      const descs = Object.getOwnPropertyDescriptors(mixins[i]);      const keys = Object.getOwnPropertyNames(descs);
      for (let j = 0, k = keys.length; j < k; j++) {        const key = keys[j];
        if (!target.prototype.hasOwnProperty(key)) {          Object.defineProperty(target.prototype, key, descs[key]);        }      }    }  };}

6.redux

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

代码语言:javascript
复制
class MyReactComponent extends React.Component {}
export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);

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

代码语言:javascript
复制
@connect(mapStateToProps, mapDispatchToProps)export default class MyReactComponent extends React.Component {};

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

7.注意

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

代码语言:javascript
复制
const method = descriptor.value;

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

代码语言:javascript
复制
const value = descriptor.initializer && descriptor.initializer();

参考

1.  ECMAScript 6 入门

2. core-decorators

3. ES7 Decorator 装饰者模式

4. JS 装饰器(Decorator)场景实战

原文作者:冴羽

原文链接:https://zhuanlan.zhihu.com/p/49843870

扫描二维码

关注我们吧

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-02-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯NEXT学院 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Decorator
  • 1 .装饰类
  • 2. 装饰方法或属性
  • Babel
  • 安装编译
  • 装饰类的编译
  • 装饰方法的编译
  • 装饰方法的编译源码解析
  • Object.getOwnPropertyDescriptor()
  • 第一部分源码解析
  • 第二部分源码解析
  • 第三部分源码解析
  • 应用
  • 1.log
  • 2.autobind
  • 3.debounce
  • 4.time
  • 5.mixin
  • 6.redux
  • 7.注意
  • 参考
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档