随着 ES6 和 TypeScript 中类的引入,使得我们在多个不同类之间共享或者扩展一些方法或者行为的时候,变得并不是那么优雅。在某些场景需要在不改变原有类和类属性的基础上扩展些功能,这也是装饰器出现的原因。
装饰器简介
装饰器接收一个参数,也就是我们被装饰的目标方法,处理完扩展的内容后再返回一个方法,供以后调用,同时也失去了对原方法对象的访问。
当我们对某个方法应用了装饰之后,其实就是改变了被装饰方法的入口引用,使其重新指向了装饰器返回的方法的入口点,从而来实现对原函数的扩展、修改等操作
不过装饰器模式仍处于第 2 阶段提案中,使用它之前需要使用 babel 模块 transform-decorators-legacy 编译成 ES5 或 ES6。
.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、数据描述符:描述属性的值和值是否可被赋值运算符改变
2、存取描述符:由getter、setter函数对属性的描述
属性描述符必须是上述两者之一;且不可同时是两者
属性描述符通用键值(即数据描述符和存取描述符都有的键值):
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 = 30
Object.defineProperty(obj, 'id', {
configurable: true,
enumerable: true,
get: () => num,
set: (newValue) => {
num = newValue
}
})
console.info(obj.id, num) // 30 30
obj.id = 20
console.info(obj.id, num) // 20 20
num = 40
console.info(obj.id, num) // 40 40
装饰器
一、作用于类的装饰器
当装饰的对象是类时,我们操作的就是这个类本身。
类的装饰器函数的第一个参数,就是所有装饰的目标类
装饰器对类的行为的改变是代码编译时发生的,而不是在运行时。这意味着,装饰器能够在编译阶段运行代码。也就是说,装饰器本质就是编译时执行的函数
例子🌰:
1、simple class decorator
in decorator.js
// 类的装饰器
export const classDecorator = (target) => {
// 此处的target为类本身
target.a = true // 给类添加一个静态属性
}
in index.js
@classDecorator
export 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) // false
const classB = new ClassB()
console.info('new ClassB().a: ', classB.a) // 1
classB.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.prototype
ps:装饰器的本意是要装饰类的实例,但此时实例还未生成,所以只能装饰类的原型
*/
/*
prototypeKey为要装饰的方法(属性名)
*/
/*
descriptor为要修饰的方法(属性名)的描述符,即(默认值为):
{
value: specifiedFunction,
enumerable: false,
configurable: true,
writable: true
}
*/
// 实现一个传参的readonly,修改描述符的writable
descriptor.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 = 2
console.info(`this.a ${tag}`, this.a)
}
}
const classD = new ClassD()
classD.fun('first')
// 报错,无法改变classD.fun,因为他的描述符descriptor.writable已经被装饰器修改为false
try {
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) => {
// 默认需要showLoading
const { showLoading = true } = params
const oldValue = descriptor.value
descriptor.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 tests
export 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.value
let { prefix } = params
const { console } = params
if (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 += 1
console.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 } = params
const { options } = params
if (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.value
return {
...descriptor,
value(...args) {
console.log('test1')
oldValue.apply(this, args)
}
}
}
export const testSequence2 = (params = {}) => (target, prototypeKey, descriptor) => {
const oldValue = descriptor.value
return {
...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更有助于你理解装饰器