专栏首页腾讯NEXT学位干货 | ES6 系列之我们来聊聊装饰器

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

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

Decorator

装饰器主要用于:

1. 装饰类

2. 装饰方法或属性

1 .装饰类

@annotationclass MyClass { }
function annotation(target) {   target.annotated = true;}

2. 装饰方法或属性

class MyClass {  @readonly  method() { }}
function readonly(target, name, descriptor) {  descriptor.writable = false;  return descriptor;}

Babel

安装编译

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

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

npm init
npm install --save-dev @babel/core @babel/cli
npm install --save-dev @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties

新建 .babelrc 文件

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

再编译指定的文件

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

装饰类的编译

编译前:

@annotationclass MyClass { }
function annotation(target) {   target.annotated = true;}

编译后:

var _class;
let MyClass = annotation(_class = class MyClass {}) || _class;
function annotation(target) {  target.annotated = true;}

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

@decoratorclass A {}
// 等同于
class A {}A = decorator(A) || A;

装饰方法的编译

编译前:

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

编译后:

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 的方法。

举个例子:

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() 返回的属性描述符对象做了一份拷贝:

// 拷贝一份 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 配合而产生的一个属性,比如说对于下面这种代码:

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 就会编译为:

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

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

第二部分源码解析

接下是应用多个 decorators:

/** * 第二部分 * @type {[type]} */desc = decorators    .slice()    .reverse()    .reduce(function(desc, decorator) {        return decorator(target, property, desc) || desc;    }, desc);

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

class MyClass {  @unenumerable  @readonly  method() { }}

Babel 会编译为:

_applyDecoratedDescriptor(    _class.prototype,    "method",    [unenumerable, readonly],    Object.getOwnPropertyDescriptor(_class.prototype, "method"),    _class.prototype)

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

第三部分源码解析

/** * 第三部分 * 设置要 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 的值设置为:

desc.initializer.call(context)

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

class MyClass {  @readonly  value = this.getNum() + 1;
  getNum() {    return 1;  }}

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

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

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

应用

1.log

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

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

再完善点:

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

class Person {  @autobind  getPerson() {    return this;  }}
let person = new Person();let { getPerson } = person;
getPerson() === person;// true

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

class Toggle extends React.Component {
  @autobind  handleClick() {      console.log(this)  }
  render() {    return (      <button onClick={this.handleClick}>        button      </button>    );  }}

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

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

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

class Toggle extends React.Component {
  @debounce(500, true)  handleClick() {    console.log('toggle')  }
  render() {    return (      <button onClick={this.handleClick}>        button      </button>    );  }}

我们来实现一下:

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

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

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 中:

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 的一个简单实现如下:

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 库结合使用时,常常需要写成下面这样。

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

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

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

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

7.注意

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

const method = descriptor.value;

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

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

参考

1.  ECMAScript 6 入门

2. core-decorators

3. ES7 Decorator 装饰者模式

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

原文作者:冴羽

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

扫描二维码

关注我们吧

本文分享自微信公众号 - 腾讯NEXT学院(Next_Academy),作者:努力学习的小N

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-02-10

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 游戏《蔚蓝山》教我的编程道理

    如果有这么一款游戏,你操作的角色平均每 20 秒就会死亡一次,正常通关一次,总共需要死掉超过 2000 次。你猜这是一款神作还是垃圾?

    腾讯NEXT学位
  • 插秧、搬砖、线上卖艺……在家的大学生已经"疯"了!

    近日,一条有人欢喜有人愁的微博悄然登上热搜: ? 虽然微博官方配的柠檬脸,表达了众多网友的羡慕嫉妒恨,但奇怪的是,主角大学生们并没有我们想象的那么高兴,反而...

    腾讯NEXT学位
  • 2018 年最流行的 100 个前端项目

    ? 作为一名开发工程师,当接手一个全新的项目任务时,如何选用并设计合适的技术架构?面对日新月异的技术更新,如何保持一份积极而又淡定的心态,持续高效地跟进学习这...

    腾讯NEXT学位
  • 微信小程序的生命周期函数

    *** 里面的生命周期 *监听页面加载 onLoad:function(options) { } 常用语发送请求,因为这里的options 会携带参数。

    用户4344670
  • 【nodejs】让nodejs像后端mvc框架(asp.net mvc)一样处理请求--请求处理函数装饰器注册篇(5/8)【controller+action】

    上篇文章把action的注册讲完了,但是我们的处理函数没有指定可接受的httpmethod,也没有别名上面的。下面我们使用typescript的特性之一装饰器来...

    旺财的城堡
  • 函数模板之compare比较大小—C++

    汐楓
  • Angularjs 通过asp.net web api认证登录

    Angularjs 通过asp.net web api认证登录 Angularjs利用asp.net mvc提供的asp.net identity,member...

    阿新
  • 物理挖洞!涂抹地形! 小鳄鱼爱洗澡!百战天虫 !Cocos Creator !

    整体思路是先使用 PolyBool 计算多边形,接着使用 cc.PhysicsChainCollider 将多边形围起来,最后使用 cc.Graphics 将整...

    白玉无冰
  • JavaScript设计模式(3)——Revealing Module(揭示模块)模式

    这种模式能够在私有范围内定义所有的函数和变量,并返回一个匿名对象,它拥有指向私有函数的指针,该私有函数是希望展示为公有的方法。

    悠扬前奏
  • LeakCanary看这一篇文章就够了

    LeakCanary是Square公司基于MAT开源的一个内存泄漏检测工具,在发生内存泄漏的时候LeakCanary会自动显示泄漏信息。

    用户1269200

扫码关注云+社区

领取腾讯云代金券