装饰器接收一个参数,也就是我们被装饰的目标方法,处理完扩展的内容后再返回一个方法,供以后调用,同时也失去了对原方法对象的访问。
当我们对某个方法应用了装饰之后,其实就是改变了被装饰方法的入口引用,使其重新指向了装饰器返回的方法的入口点,从而来实现对原函数的扩展、修改等操作
不过装饰器模式仍处于第 2 阶段提案中,使用它之前需要使用 babel 模块 transform-decorators-legacy 编译成 ES5 或 ES6。
babel配置
.babelrc中
"plugins": ["transform-decorators-legacy"*]
ES7的装饰器decorator是依赖于ES5的Object.defineProperty方法
相关知识:Object.defineProperty
Object.defineProperty()在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。
语法:
Object.defineProperty(obj, prop, descriptor)
obj:操作的对象prop:被定义或者修改的属性名称descriptor:将被定义或修改的属性描述符返回值:被传递给函数的对象
属性描述符:descriptor
对象中目前存在的属性描述符有2种:数据描述符和存取描述符
1、数据描述符:描述属性的值和值是否可被赋值运算符改变<br /> 2、存取描述符:由getter、setter函数对属性的描述<br />** 属性描述符必须是上述两者之一;且不可同时是两者**
属性描述符通用键值(即数据描述符和存取描述符都有的键值):
1、configurable:configurable特性表示对象的属性是否可以被删除,以及除value和writable特性外的其他特性是否可以被修改。默认值false,即不可改变
2、enumerable:定义了当前操作的这个属性是否可以for…in和Object.key()中被枚举。设为true时,该属性才能出现在对象的枚举属性中。默认值false,即不可被枚举
数据描述符特有的键值:
1、value:该属性对应的值,可以是任意有效的javascript值(string,number,object,function等等)。默认值undefined
2、writable:当且仅当writable为true时,value才能被赋值运算符改变。默认值false,即不可被改变
let o = {};o.a = 1;// 等同于 :Object.defineProperty(o, "a", {value: 1,writable: true,configurable: true,enumerable: true});
// 另一方面,Object.defineProperty(o, "a", {value: 1});// 等同于 :Object.defineProperty(o, "a", {value: 1,writable: false,configurable: false,enumerable: false});
存取描述符特有的键值:
1、get:一个给属性提供getter的方法,如果没有getter则为undefined。当访问该属性时get方法会执行,方法执行时没有参数传入,但会传入this对象(由于继承关系,此this不一定是定义改属性的对象)
2、set:一个给属性提供setter的方法,如果没有setter则为undefined。当属性值修改时set方法会执行,该方法将接收唯一参数,即该属性新的参数值
let obj = {}let num = 30Object.defineProperty(obj, 'id', {configurable: true,enumerable: true,get: () => num,set: (newValue) => {num = newValue}})console.info(obj.id, num) // 30 30obj.id = 20console.info(obj.id, num) // 20 20num = 40console.info(obj.id, num) // 40 40
装饰器的简单应用
1. 类的装饰
当装饰的对象是类时,我们操作的就是这个类本身,即装饰器函数的第一个参数,就是所要装饰的目标类。
@decoratorclass A {}// 等同于class A {}A = decorator(A) || A;
示例:添加一个日志装饰器
@logclass MyClass { }function log(target) { // 这个 target 在这里就是 MyClass 这个类target.prototype.logger = () => `${target.name} 被调用`}const test = new MyClass()test.logger() // MyClass 被调用
由于装饰器是表达式,我们也可以在装饰器后面再添加个参数:
@log('hi')class MyClass { }function log(text) {return function(target) {target.prototype.logger = () => `${text},${target.name} 被调用`}}const test = new MyClass()test.logger() // hello,MyClass 被调用
2. 属性或方法的装饰
对于类属性或方法的装饰本质是操作其描述符,可以把此时的装饰器理解成是 Object.defineProperty(obj, prop, descriptor)的语法糖。
class C {@readonly(false)method() { console.log('cat') }}function readonly(value) {return function (target, key, descriptor) {/*** 此处 target 为 C.prototype;* key 为 method;* 原 descriptor 为:{ value: f, enumarable: false, writable: true, configurable: true }*/descriptor.writable = valuereturn descriptor}}const c = new C()c.method = () => console.log('dog')c.method() // cat
装饰器的复杂应用
一、作用于类的装饰器
当装饰的对象是类时,我们操作的就是这个类本身。
类的装饰器函数的第一个参数,就是所有装饰的目标类
装饰器对类的行为的改变是代码编译时发生的,而不是在运行时。这意味着,装饰器能够在编译阶段运行代 码。也就是说,装饰器本质就是编译时执行的函数
例子:
1、simple class decorator
in decorator.js
// 类的装饰器export const classDecorator = (target) => {// 此处的target为类本身target.a = true // 给类添加一个静态属性}
in index.js
@classDecoratorexport class ClassA {constructor() {this.a = 1}a = 2}console.info('ClassA.a: ', ClassA.a) // true
2、class decorator with params 传参的类装饰器
in decorator.js
// 传参的类的装饰器export const classDecoratorWithParams = (params = true) => (target) => {target.a = params}
in index.js
@classDecoratorWithParams(false)export class ClassB {constructor() {this.a = 1}fun = () => {console.info('fun中ClassB.a: ', this.a, ClassB.a) // 1, false}}console.info('ClassB.a: ', ClassB.a) // falseconst classB = new ClassB()console.info('new ClassB().a: ', classB.a) // 1classB.fun()
3、class decorator add prototype 给修饰类添加实例属性
in decorator.js
// 类的装饰器(给类添加实例属性)export const classDecoratorAddPrototype = prototypeList => (target) => {target.prototype = { ...target.prototype, ...prototypeList }target.prototype.logger = () => console.info(`${target.name} 被调用`) // target.name即获得类的名}
in index.js
@classDecoratorAddPrototype({ fn() { console.info('fnfnfn') } }) // 此处不能使用箭头函数?export class ClassC {constructor() {this.a = 1}}// console.info('ClassC.fn: ', ClassC.fn()) // 报错,fn不在ClassC的静态属性上const classC = new ClassC()classC.fn()classC.logger()
例子github:https://github.com/zzsscc/decorators
在redux中我们经常使用react-redux的connect装饰器即为作用于类的装饰器
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])export default class MyComponent extends React.Component {}相当于class MyComponent extends React.Component {}export default connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])(MyComponent)
二、作用于类方法的装饰器
与装饰类不同,对类方法的装饰本质是操作其描述符
可以把此时的装饰器理解成是 Object.defineProperty(obj, prop, descriptor) 的语法糖
例子:
1、class function decorator
in decorator.js
// 方法的装饰器export const funDecorator = (params = { readonly: true }) => (target, prototypeKey, descriptor) => {/*此处target为类的原型对象,即方法Class.prototypeps:装饰器的本意是要装饰类的实例,但此时实例还未生成,所以只能装饰类的原型*//*prototypeKey为要装饰的方法(属性名)*//*descriptor为要修饰的方法(属性名)的描述符,即(默认值为):{value: specifiedFunction,enumerable: false,configurable: true,writable: true}*/// 实现一个传参的readonly,修改描述符的writabledescriptor.writable = !params.readonly// 返回这个新的描述符return descriptor}/*调用funDecorator(Class.prototype, prototypeKey, descriptor)相当于Object.defineProperty(Class.prototype, prototypeKey, descriptor)*/
in index.js
export class ClassD {constructor() {this.a = 1}@funDecorator()fun = (tag) => {this.a = 2console.info(`this.a ${tag}`, this.a)}}const classD = new ClassD()classD.fun('first')// 报错,无法改变classD.fun,因为他的描述符descriptor.writable已经被装饰器修改为falsetry {classD.fun = (tag) => {console.info(`this.a changed ${tag}`)}classD.fun('sec')} catch (err) {throw new Error(err)}
2、fun enhance(front/end) decorator
in decorator.js
// 方法的装饰器(在方法执行的前后添加操作:如show/hide loading)export const funEnhanceDecorator = (params = {}) => (target, prototypeKey, descriptor) => {// 默认需要showLoadingconst { showLoading = true } = paramsconst oldValue = descriptor.valuedescriptor.value = async function A(...args) {try {showLoading && console.info('加载中')const result = await oldValue.apply(this, args)console.info('hide')return result} catch (err) {console.info('hide')console.error(err)return null}};return descriptor}
in index.js
export class ClassE {constructor() {this.result = {}}afun = (params) => {return new Promise((resolve, reject) => {setTimeout(() => {resolve(params.id)}, 2000)})}@funEnhanceDecorator()async fun(params = {}) { // 不能使用箭头函数?const result = await this.afun(params)console.info(result)}}const classE = new ClassE()classE.fun({ id: 100 })
3、test decorators sequence多个装饰器的包装顺序
in decorator.js
// time => 计数and计时const labels = {};// Exported for mocking in testsexport const defaultConsole = {time: console.time ? console.time.bind(console) : (label) => {labels[label] = new Date();},timeEnd: console.timeEnd ? console.timeEnd.bind(console) : (label) => {const timeNow = new Date();const timeTaken = timeNow - labels[label];delete labels[label];console.info(`${label}: ${timeTaken}ms`);}};let count = 0;export const time = (params = { prefix: null, console: defaultConsole }) => (target, prototypeKey, descriptor) => {const fn = descriptor.valuelet { prefix } = paramsconst { console } = paramsif (prefix === null) {prefix = `${target.constructor.name}.${prototypeKey}`}if (typeof fn !== 'function') {throw new SyntaxError(`@time can only be used on functions, not: ${fn}`)}return {...descriptor,async value(...args) {const label = `${prefix}-${count}`count += 1console.time(label)try {return await fn.apply(this, args)} finally {console.timeEnd(label)}}}}// deprecate => 标记废弃const DEFAULT_MSG = 'This function will be removed in future versions.'export const deprecate = (params = { options: {} }) => (target, prototypeKey, descriptor) => {if (typeof descriptor.value !== 'function') {throw new SyntaxError('Only functions can be marked as deprecated')}const methodSignature = `${target.constructor.name}#${prototypeKey}`let { msg = DEFAULT_MSG } = paramsconst { options } = paramsif (options.url) {msg += `\n\n See ${options.url} for more details.\n\n`;}return {...descriptor,value(...args) {console.warn(`DEPRECATION ${methodSignature}: ${msg}`)return descriptor.value.apply(this, args)}}}// test sequence 测试顺序export const testSequence1 = (params = {}) => (target, prototypeKey, descriptor) => {const oldValue = descriptor.valuereturn {...descriptor,value(...args) {console.log('test1')oldValue.apply(this, args)}}}export const testSequence2 = (params = {}) => (target, prototypeKey, descriptor) => {const oldValue = descriptor.valuereturn {...descriptor,value(...args) {console.log('test2')oldValue.apply(this, args)}}}
in index.js
export class ClassF {constructor() {this.result = {}}@time()@deprecate({ options: { url: 'https://github.com/zzsscc' } })@testSequence1()@testSequence2()fun() {return new Promise((resolve, reject) => {setTimeout(() => {resolve(this.result)}, 3000)})}}const classf = new ClassF()classf.fun()classf.fun()
三、core-decorators.js
提供了一些常用的装饰器方法
code view更有助于你理解装饰器
跳转github查看
—————————————————————————————————————————————————
原文链接:https://www.jianshu.com/p/c3cbea4f86c6
原文链接:https://segmentfault.com/a/1190000014495089
扩展学习1:点击跳转
扩展学习2:https://www.sitepoint.com/javascript-decorators-what-they-are/
扩展学习3:https://es6.ruanyifeng.com/#docs/decorator#core-decorators-js
