Decorator:从原理到实践
前言
es6 已经不必在过多介绍,在 es6 之前,装饰器可能并没有那么重要,因为你只需要加一层 wrapper 就好了,但是现在,由于语法糖 class 的出现,当我们想要去在多个类之间共享或者扩展一些方法的时候,代码会变得错综复杂,难以维护,而这,也正式我们 decorator 的用武之地。
object.defineproperty
关于 object.defineproperty 简单的说,就是该方法可以精准的添加和修改对象的属性
语法
object.defineproperty(obj,prop,descriptor)
- ojb:要在其上定义属性的对象
- prop:要定义或修改的属性的名称
- descriptor:将被定义或修改的属性描述符
该方法返回被传递给函数的对象
在es6中,由于 symbol类型的特殊性,用symbol类型的值来做对象的key与常规的定义或修改不同,而object.defineproperty 是定义key为symbol的属性的方法之一。
通过赋值操作添加的普通属性是可枚举的,能够在属性枚举期间呈现出来(for...in 或 object.keys 方法), 这些属性的值可以被改变,也可以被删除。这个方法允许修改默认的额外选项(或配置)。默认情况下,使用 object.defineproperty() 添加的属性值是不可修改的
属相描述符
对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者。
数据描述符和存取描述符均具有以下可选键值:
configurable
当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false
enumerable
当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中。默认为 false。
数据描述符同时具有以下可选键值:
value
该属性对应的值。可以是任何有效的 javascript 值(数值,对象,函数等)。默认为 undefined。
writable
当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false
存取描述符同时具有以下可选键值:
get
一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。默认为 undefined。
set
一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。默认为 undefined。
如果一个描述符不具有value,writable,get 和 set 任意一个关键字,那么它将被认为是一个数据描述符。如果一个描述符同时有(value或writable)和(get或set)关键字,将会产生一个异常
更多使用实例和介绍,参看:mdn
装饰者模式
在看decorator之前,我们先看下装饰者模式的使用,我们都知道,装饰者模式能够在不改变对象自身基础上,在程序运行期间给对象添加指责。特点就是不影响之前对象的特性,而新增额外的职责功能。
like...this:
这段比较简单,直接看代码吧:
let monkey = function () {} monkey.prototype.say = function () { console.log('目前我只是个野猴子'); } let tensionmonkey = function (monkey) { this.monkey = monkey; } tensionmonkey.prototype.say = function () { this.monkey.say(); console.log('带上紧箍咒,我就要忘记世间烦恼!'); } let monkey = new tensionmonkey(new monkey()); monkey.say();
执行结果:
decorator
decorator其实就是一个语法糖,背后其实就是利用es5的object.defineproperty(target,name,descriptor)
,了解object.defineproperty请移步这个链接:mdn文档
其背后原理大致如下:
class monkey{ say(){ console.log('目前,我只是个野猴子'); } }
执行上面的代码,大致代码如下:
object.defineproperty(monkey.prototype,'say',{ value:function(){console.log('目前,我只是个野猴子')}, enumerable:false, configurable:true, writable:true })
如果我们利用装饰器来修饰他
class monkey{ @readonly say(){console.log('现在我是只读的了')} }
在这种装饰器的属性,会在object.defineproperty为monkey.prototype注册say属性之前,执行以下代码:
let descriptor = { value:specifiedfunction, enumerable:false, configurable:true, writeable:true }; descriptor = readonly(monkey.prototype,'say',descriptor)||descriptor; object.defineproperty(monkey.prototype,'say',descriptor);
从上面的伪代码我们可以看出,decorator只是在object.defineproperty为monkey.prototype注册属性之前,执行了一个装饰函数,其属于一个类对object.defineproperty的拦截。所以它和object.defineproperty具有一致的形参:
- obj:作用的目标对象
- prop:作用的属性名
- descriptor:针对该属性的描述符
下面看下简单的使用
在class中的使用
- 创建一个新的class继承自原有的class,并添加属性
@name class person{ sayhello(){ console.log(`hello ,my name is ${this.name}`) } } function name(constructor) { return class extends constructor{ name="nealyang" } } new person().sayhello() //hello ,my name is nealyang
- 针对当前class修改(类似mixin)
@name @seal class person { sayhello() { console.log(`hello ,my name is ${this.name}`) } } function name(constructor) { object.defineproperty(constructor.prototype,'name',{ value:'一凨' }) } new person().sayhello() //若修改一个属性 function seal(constructor) { let descriptor = object.getownpropertydescriptor(constructor.prototype, 'sayhello') object.defineproperty(constructor.prototype, 'sayhello', { ...descriptor, writable: false }) } new person().sayhello = 1;// cannot assign to read only property 'sayhello' of object '#<person>'
上面说到mixin,那么我就来模拟一个mixin吧
class a { run() { console.log('我会跑步!') } } class b { jump() { console.log('我会跳!') } } @mixin(a, b) class c {} function mixin(...args) { return function (constructor) { for (const arg of args) { for (let key of object.getownpropertynames(arg.prototype)) { if (key === 'constructor') continue; object.defineproperty(constructor.prototype, key, object.getownpropertydescriptor(arg.prototype, key)); } } } } let c = new c(); c.jump(); c.run(); // 我会跳! // 我会跑步!
截止目前我們貌似写了非常多的代码了,对。。。这篇,为了彻底搞投decorator,这。。。只是开始。。。
在class成员中的使用
这类的装饰器的写法应该就是我们最为熟知了,会接受三个参数:
- 如果装饰器挂载在静态成员上,则会返回构造函数,如果挂载在实例成员上,则返回类的原型
- 装饰器挂载的成员名称
- object.getownpropertydescriptor的返回值
首先,我们明确下静态成员和实例成员的区别
class model{ //实例成员 method1(){} method2 = ()=>{} // 靜態成員 static method3(){} static method4 = ()=>{} }
method1 和method2 是实例成员,但是method1存在于prototype上,method2只有实例化对象以后才有。
method3和method4是静态成员,两者的区别在于是否可枚举描述符的设置,我们通过babel转码可以看到:
上述代码比较乱,简单的可以理解为:
function model () { // 成员仅在实例化时赋值 this.method2 = function () {} } // 成员被定义在原型链上 object.defineproperty(model.prototype, 'method1', { value: function () {}, writable: true, enumerable: false, // 设置不可被枚举 configurable: true }) // 成员被定义在构造函数上,且是默认的可被枚举 model.method4 = function () {} // 成员被定义在构造函数上 object.defineproperty(model, 'method3', { value: function () {}, writable: true, enumerable: false, // 设置不可被枚举 configurable: true })
可以看出,只有method2是在实例化时才赋值的,一个不存在的属性是不会有descriptor的,所以这就是为什么在针对property decorator不传递第三个参数的原因,至于为什么静态成员也没有传递descriptor,目前没有找到合理的解释,但是如果明确的要使用,是可以手动获取的。
就像上述的示例,我们针对四个成员都添加了装饰器以后,method1和method2第一个参数就是model.prototype,而method3和method4的第一个参数就是model。
class model { // 实例成员 @instance method1 () {} @instance method2 = () => {} // 静态成员 @static static method3 () {} @static static method4 = () => {} } function instance(target) { console.log(target.constructor === model) } function static(target) { console.log(target === model) }
函数、访问器、属性 三者装饰器的使用
- 函数装饰器的返回值会默认作为属性的value描述符的存在,如果返回为undefined则忽略
class model { @log1 getdata1() {} @log2 getdata2() {} } // 方案一,返回新的value描述符 function log1(tag, name, descriptor) { return { ...descriptor, value(...args) { let start = new date().valueof() try { return descriptor.value.apply(this, args) } finally { let end = new date().valueof() console.log(`start: ${start} end: ${end} consume: ${end - start}`) } } } } // 方案二、修改现有描述符 function log2(tag, name, descriptor) { let func = descriptor.value // 先获取之前的函数 // 修改对应的value descriptor.value = function (...args) { let start = new date().valueof() try { return func.apply(this, args) } finally { let end = new date().valueof() console.log(`start: ${start} end: ${end} consume: ${end - start}`) } } }
- 访问器的decorator就是
get
set
前缀函数了,用于控制属性的赋值、取值操作,在使用上和函数装饰器没有任何区别
class modal { _name = 'niko' @prefix get name() { return this._name } } function prefix(target, name, descriptor) { return { ...descriptor, get () { return `wrap_${this._name}` } } } console.log(new modal().name) // wrap_niko
- 对于属性装饰器是没有descriptor返回的,并且装饰器函数的返回值也会被忽略,如果我们需要修改某一个静态属性,则需要自己获取descriptor
class modal { @prefix static name1 = 'niko' } function prefix(target, name) { let descriptor = object.getownpropertydescriptor(target, name) object.defineproperty(target, name, { ...descriptor, value: `wrap_${descriptor.value}` }) } console.log(modal.name1) // wrap_niko
对于一个实例的属性,则没有直接修改的方案,不过我们可以结合着一些其他装饰器来曲线救国。
比如,我们有一个类,会传入姓名和年龄作为初始化的参数,然后我们要针对这两个参数设置对应的格式校验
const validateconf = {} // 存储校验信息 @validator class person { @validate('string') name @validate('number') age constructor(name, age) { this.name = name this.age = age } } function validator(constructor) { return class extends constructor { constructor(...args) { super(...args) // 遍历所有的校验信息进行验证 for (let [key, type] of object.entries(validateconf)) { if (typeof this[key] !== type) throw new error(`${key} must be ${type}`) } } } } function validate(type) { return function (target, name, descriptor) { // 向全局对象中传入要校验的属性名及类型 validateconf[name] = type } } new person('niko', '18') // throw new error: [age must be number]
函数参数装饰器
const parseconf = {} class modal { @parsefunc addone(@parse('number') num) { return num + 1 } } // 在函数调用前执行格式化操作 function parsefunc (target, name, descriptor) { return { ...descriptor, value (...arg) { // 获取格式化配置 for (let [index, type] of parseconf) { switch (type) { case 'number': arg[index] = number(arg[index]) break case 'string': arg[index] = string(arg[index]) break case 'boolean': arg[index] = string(arg[index]) === 'true' break } return descriptor.value.apply(this, arg) } } } } // 向全局对象中添加对应的格式化信息 function parse(type) { return function (target, name, index) { parseconf[index] = type } } console.log(new modal().addone('10')) // 11
decorator 用例
log
为一个方法添加 log 函数,检查输入的参数
let log = type => { return (target,name,decorator) => { const method = decorator.value; console.log(method); decorator.value = (...args) => { console.info(`${type} 正在进行:${name}(${args}) = ?`); let result; try{ result = method.apply(target,args); console.info(`(${type}) 成功 : ${name}(${args}) => ${result}`); }catch(err){ console.error(`(${type}) 失败: ${name}(${args}) => ${err}`); } return result; } } } class math { @log('add') add(a, b) { return a + b; } } const math = new math(); // (add) 成功 : add(2,4) => 6 math.add(2, 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); } } } } }
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() } }; } }
更多关于 core-decorators 的例子后面再
nealyang/personalblog中补充,再加注释说明。
参考
学习交流
关注公众号: 【全栈前端精选】 每日获取好文推荐。
公众号内回复 【1】,加入全栈前端学习群,一起交流。