ES6 系列之我们来聊聊装饰器

摘要: ## Decorator 装饰器主要用于: 1. 装饰类 2. 装饰方法或属性 ### 装饰类 ```js @annotation class MyClass { } function annotation(target) { target.annotated = true; } ``` ### 装饰方法或属性 ```js class

Decorator

装饰器主要用于:

  1. 装饰类
  2. 装饰方法或属性

装饰类

1@annotation 2class MyClass { } 3 4function annotation(target) { 5 target.annotated = true; 6}

装饰方法或属性

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

Babel

安装编译

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

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

1npm init 2 3npm install --save-dev @babel/core @babel/cli 4 5npm 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}

再编译指定的文件

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

装饰类的编译

编译前:

1@annotation 2class MyClass { } 3 4function annotation(target) { 5 target.annotated = true; 6}

编译后:

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

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

1@decorator 2class A {} 3 4// 等同于 5 6class A {} 7A = decorator(A) || A;

装饰方法的编译

编译前:

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

编译后:

1var _class; 2 3function _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 47let 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 59function readonly(target, name, descriptor) { 60 descriptor.writable = false; 61 return descriptor; 62}

装饰方法的编译源码解析

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

Object.getOwnPropertyDescriptor()

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

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

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

举个例子:

1const foo = { value: 1 }; 2const bar = Object.getOwnPropertyDescriptor(foo, "value"); 3// bar { 4// value: 1, 5// writable: true 6// enumerable: true, 7// configurable: true, 8// } 9 10const foo = { get value() { return 1; } }; 11const 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 2var desc = {}; 3Object["ke" + "ys"](descriptor).forEach(function(key) { 4 desc[key] = descriptor[key]; 5}); 6desc.enumerable = !!desc.enumerable; 7desc.configurable = !!desc.configurable; 8 9// 如果没有 value 属性或者没有 initializer 属性,表明是 getter 和 setter 10if ("value" in desc || desc.initializer) { 11 desc.writable = true; 12}

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

1class MyClass { 2 @readonly 3 born = Date.now(); 4} 5 6function readonly(target, name, descriptor) { 7 descriptor.writable = false; 8 return descriptor; 9} 10 11var foo = new MyClass(); 12console.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 */ 5desc = decorators 6 .slice() 7 .reverse() 8 .reduce(function(desc, decorator) { 9 return decorator(target, property, desc) || desc; 10 }, desc);

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

1class 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 */ 5if (context && desc.initializer !== void 0) { 6 desc.value = desc.initializer ? desc.initializer.call(context) : void 0; 7 desc.initializer = undefined; 8} 9 10if (desc.initializer === void 0) { 11 Object["define" + "Property"](target, property, desc); 12 desc = null; 13} 14 15return desc;

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

desc.initializer.call(context)

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

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

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

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

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

应用

1.log

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

1class Math { 2 @log 3 add(a, b) { 4 return a + b; 5 } 6} 7 8function 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 19const math = new Math(); 20 21// Calling add with [2, 4] 22math.add(2, 4);

再完善点:

1let 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

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

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

1class 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 函数:

1const { defineProperty, getPrototypeOf} = Object; 2 3function 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 13function 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 26function 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

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

1class 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}

我们来实现一下:

1function _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 25function 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

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

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

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

mixin 的一个简单实现如下:

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

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

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

1@connect(mapStateToProps, mapDispatchToProps) 2export 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)场景实战

ES6 系列

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

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

原文链接

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

swap空间的增减方法

(1)增大swap空间去激活swap交换区:swapoff v /dev/vg00/lvswap扩展交换lv:lvextend L 10G /dev/vg00/lvswap重新生成swap交换区:mkswap /dev/vg00/lvswap激活新生成的交换区:swapon v /dev/vg00/lvswap

Java获得今日零时零分零秒的时间(Date型)

publicDatezeroTime()throwsParseException{    DatetimenewDate();    SimpleDateFormatsimpnewSimpleDateFormat("yyyyMMdd00:00:00");    SimpleDateFormatsimp2newS

ES6 系列之我们来聊聊装饰器 - HelloWorld