什么是装饰器
装饰器,顾名思义,就是在不影响原有功能的情况下,增加一些附属的东西。可以理解成抽象的一种实现,把通用的东西给抽象出来,独立去使用。
装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。 装饰器使用 @expression这种形式,expression 美 [ɪkˈsprɛʃən] 求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。
expression 美 [ɪkˈsprɛʃən] 表现,表示,表达;表情,脸色,态度,腔调,声调;[数]式,符号;词句,语句,措辞,说法
Decorator 美 [ˈdɛkəˌretɚ] 室内装饰师,油漆匠
目前装饰器还不属于标准,还在 建议征集的第二阶段,但这并不妨碍我们在ts中的使用。在 tsconfig.json
中开启 experimentalDecorators
编译器选项
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
复制代码
所以目前 @Decorators
仅是一个语法糖,装饰器可以理解成一种解决方案,我觉得这个跟 AOP 面向切面编程 的思想有点类似。
在非typescript环境中使用装饰器
vscode 解决装饰器 ESLint: Cannot read property 'type' of undefined 报错问题
"javascript.implicitProjectConfig.experimentalDecorators":true,
在VSCode中,转到File => Preferences => Settings(或Control +逗号)
,它将打开用户设置文件。添加“javascript.implicitProjectConfig.experimentalDecorators”:
对文件为true
执行时机
注意,修饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,修饰器能在编译阶段运行代码。也就是说,修饰器本质就是编译时执行的函数。
创建类和创建函数时立即执行装饰器
如果装饰器函数返回一个function,这个function会在所有的装饰器函数运行完毕后,继续运行
参考文章中的“装饰器执行顺序”
@decorator
class A {}
// 等同于
class A {}
A = decorator(A) || A;
为什么有这个执行时机呢?
编译前
function changeMood(isHappy) {
return function(target) {
target.isHappy = isHappy
}
}
function changeMoodStatic(target){
target.isHappyStatic = true;
}
@changeMoodStatic
@changeMood(true)
class Boy {}
console.log(Boy);
@changeMood(false)
class Girl {}
console.log(Girl);
// 编译后
var _dec, _class, _dec2, _class2;
function changeMood(isHappy) {
return function (target) {
target.isHappy = isHappy;
};
}
function changeMoodStatic(target) {
target.isHappyStatic = true;
}
let Boy = (_dec = changeMood(true), changeMoodStatic(_class = _dec(_class = class Boy {}) || _class) || _class);
console.log(Boy);
let Girl = (_dec2 = changeMood(false), _dec2(_class2 = class Girl {}) || _class2);
console.log(Girl);
使用方式
装饰器可以应用在如下几处地方
- Class
- 函数
- 函数参数
- 属性
- get set 访问器
使用的语法很简单,类似于java的注解
@sealed // 使用装饰器
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
// 定义装饰器
function sealed(constructor: Function) { // 此装饰器的作用是封闭一个对象
Object.seal(constructor);
Object.seal(constructor.prototype);
}
// mixins.js
export function mixins(...list) {
return function (target) {
Object.assign(target.prototype, ...list)
}
}
//########################################################################
// main.js
import { mixins } from './mixins'
const Foo = {
foo() { console.log('foo') }
};
@mixins(Foo)
class MyClass {}
let obj = new MyClass();
obj.foo() // 'foo'
装饰器的执行顺序
装饰器可以同时应用多个,所以在定义装饰器的时候应当每个装饰器都是相互独立的。举个官方的栗子
function f() {
console.log("f(): evaluated");
return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("f(): called");
}
}
function g() {
console.log("g(): evaluated");
return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("g(): called");
}
}
class C {
@f()
@g()
method() {}
}
执行结果
f(): evaluated
g(): evaluated
g(): called
f(): called
babel下使用@Decorator
利用@Decorator给类添加静态属性
npm install babel-cli -g
npm install babel-plugin-transform-decorators-legacy -D
babel --plugins transform-decorators-legacy decorator.js --out-file test.js
node test.js
// decorator.js
function changeMood(isHappy) {
return function(target) {
target.isHappy = isHappy
}
}
function changeMoodStatic(target){
target.isHappyStatic = true;
}
@changeMoodStatic
@changeMood(true)
class Boy {}
console.log(Boy);
@changeMood(false)
class Girl {}
console.log(Girl);
{ [Function: Boy] isHappy: true, isHappyStatic: true }
{ [Function: Girl] isHappy: false }
Class 修饰类,修饰类的方法
类的装饰器
在类中使用时,参数只有target一个
function testable(isTestable) {
return function(target) {
target.isTestable = isTestable;
}
}
类装饰器,在类定义前执行,在装饰器中我们可以重新定义构造函数,用来监视,修改或替换类定义。举个栗子
// 定义装饰器
const FTest = <T extends {new(...args:any[]):{}}>(constructor:T) => {
return class extends constructor {
newProperty = "new property";
hello = "override";
}
}
@FTest
class Test {
hello: string;
constructor(){
this.hello = 'test'
}
}
const test = new Test();
console.log(test.hello) // override
可以看到, hello
的值在构造器中被我们修改了。类装饰器只能有一个参数,即原本类的构造函数。
Mixin 的实现就可以使用类构造器。
类的方法装饰器
装饰函数时,可以有3个参数
// 接收参数的语法
function setHello(value) {
console.log(value)
return function(target, propKey, descriptor) {
console.log(target, propKey, descriptor)
}
}
// 不接收参数的语法
function setHello(target, propKey, descriptor) {
// descriptor 可以通过descriptor改变value的值
var oldValue = descriptor.value;
console.log('执行顺序1')
descriptor.value = function() {
console.log('执行顺序3')
// 在这里可以输出日志
console.log(`Calling ${name} with`, arguments);
// return 1; 这里可以改变math.add的返回值
return oldValue.apply(this, arguments);
};
console.log('执行顺序2')
return descriptor;
}
class Boy {
constructor(){
this.hello;
this._hello = this.hello;
}
@setHello('value')
setHello(){}
}
const boy = new Boy();
console.log(boy.hello);
boy.hello = '777';
console.log(boy.hello);
/* 以下是输出
value
Boy {}
'setHello'
{ value: [Function: setHello],
writable: true,
enumerable: false,
configurable: true }
undefined
777
*/
应用场景
自动绑定this @autobind
autobind
修饰器使得方法中的this
对象,绑定原始对象。
import { autobind } from 'core-decorators';
class Person {
@autobind
getPerson() {
return this;
}
}
let person = new Person();
let getPerson = person.getPerson;
getPerson() === person;
// true
检查子类是否正确复写父类方法@override
override
修饰器检查子类的方法,是否正确覆盖了父类的同名方法,如果不正确会报错。
import { override } from 'core-decorators';
class Parent {
speak(first, second) {}
}
class Child extends Parent {
@override
speak() {}
// SyntaxError: Child#speak() does not properly override Parent#speak(first, second)
}
// or
class Child extends Parent {
@override
speaks() {}
// SyntaxError: No descriptor matching Child#speaks() was found on the prototype chain.
//
// Did you mean "speak"?
}
该方法将要废弃@deprecate (别名@deprecated)
deprecate
或deprecated
修饰器在控制台显示一条警告,表示该方法将废除。
import { deprecate } from 'core-decorators';
class Person {
@deprecate
facepalm() {}
@deprecate('We stopped facepalming')
facepalmHard() {}
@deprecate('We stopped facepalming', { url: 'http://knowyourmeme.com/memes/facepalm' })
facepalmHarder() {}
}
let person = new Person();
person.facepalm();
// DEPRECATION Person#facepalm: This function will be removed in future versions.
person.facepalmHard();
// DEPRECATION Person#facepalmHard: We stopped facepalming
person.facepalmHarder();
// DEPRECATION Person#facepalmHarder: We stopped facepalming
//
// See http://knowyourmeme.com/memes/facepalm for more details.
//
@suppressWarnings
suppressWarnings
修饰器抑制deprecated
修饰器导致的console.warn()
调用。但是,异步代码发出的调用除外。
import { suppressWarnings } from 'core-decorators';
class Person {
@deprecated
facepalm() {}
@suppressWarnings
facepalmWithoutWarning() {
this.facepalm();
}
}
let person = new Person();
person.facepalmWithoutWarning();
// no warning is logged
使用修饰器实现自动发布事件
const postal = require("postal/lib/postal.lodash");
export default function publish(topic, channel) {
const channelName = channel || '/';
const msgChannel = postal.channel(channelName);
msgChannel.subscribe(topic, v => {
console.log('频道: ', channelName);
console.log('事件: ', topic);
console.log('数据: ', v);
});
return function(target, name, descriptor) {
const fn = descriptor.value;
descriptor.value = function() {
let value = fn.apply(this, arguments);
msgChannel.publish(topic, value);
};
};
}
上面代码定义了一个名为publish的修饰器,它通过改写descriptor.value,使得原方法被调用时,会自动发出一个事件。它使用的事件“发布/订阅”库是Postal.js。
它的用法如下。
// index.js
import publish from './publish';
class FooComponent {
@publish('foo.some.message', 'component')
someMethod() {
return { my: 'data' };
}
@publish('foo.some.other')
anotherMethod() {
// ...
}
}
let foo = new FooComponent();
foo.someMethod();
foo.anotherMethod();
以后,只要调用someMethod或者anotherMethod,就会自动发出一个事件。
$ bash-node index.js
频道: component
事件: foo.some.message
数据: { my: 'data' }
频道: /
事件: foo.some.other
数据: undefined
属性不可写
class Person {
@readonly
name() { return `${this.first} ${this.last}` }
}
function readonly(target, name, descriptor){
// descriptor对象原来的值如下
// {
// value: specifiedFunction,
// enumerable: false,
// configurable: true,
// writable: true
// };
descriptor.writable = false;
return descriptor;
}
readonly(Person.prototype, 'name', descriptor);
// 类似于
Object.defineProperty(Person.prototype, 'name', descriptor);
设置属性为不可枚举
class Person {
// children = [1,2,3,4];
constructor(){
this.x = 1;
this.y = 2;
this.children = [1,2,3,4];
this.kidCount = this.children.length;
}
@nonenumerable
get kidCount() { return this.children.length; }
set kidCount(value) { return value; }
add(x,y){
return x * y;
}
}
var a = new Person();
console.log(Object.keys(Person)); // 返回[],因为this作用域的缘故,他本身没有可枚举
console.log(Object.getOwnPropertyNames(Person)); // 返回 ["length", "prototype", "name"]
console.log(Object.keys(a)); // ["x", "y", "children"]
console.log(Object.getOwnPropertyNames(a)); // ["x", "y", "children"]
// 注意 add方法与kidCount 可枚举方法与不可枚举方法,都显示不出来
function nonenumerable(target, name, descriptor) {
descriptor.enumerable = false;
return descriptor;
}
打印日志
class Math {
@log
add(a, b) {
return a + b;
}
}
function log(target, name, descriptor) {
var oldValue = descriptor.value;
console.log('执行顺序1')
descriptor.value = function() {
console.log('执行顺序3')
// 在这里可以输出日志
console.log(`Calling ${name} with`, arguments);
// return 1; 这里可以改变math.add的返回值
return oldValue.apply(this, arguments);
};
console.log('执行顺序2')
return descriptor;
}
const math = new Math();
console.log(math.add(2, 4));
console.log(math.add(3, 4));
console.log(math.add(4, 4));
/*
结果打印
执行顺序1
执行顺序2
执行顺序3
Calling add with [Arguments] { '0': 2, '1': 4 }
6
执行顺序3
Calling add with [Arguments] { '0': 3, '1': 4 }
7
执行顺序3
Calling add with [Arguments] { '0': 4, '1': 4 }
8
*/
我觉得函数装饰器的使用场景会跟多一些,比如说函数的权限判断、参数校验、日志打点等一些通用的处理,因为这些都跟函数本身的业务逻辑相独立,所以就可以通过装饰器来实现。
在举栗子之前,我们想要介绍一个ts官方的库 reflect-metadatareflect-metadata
的作用就是在装饰器中类给类添加一些自定义的信息,然后在需要使用的地方通过反射定义的信息提取出来。举个栗子
const Custom = (value?: any): MethodDecorator => {
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
Reflect.defineMetadata('name', value, target, propertyKey);
}
}
class A{
@Custom('test')
method(){}
}
console.log(Reflect.getMetadata('name', new A(), 'method')) // test
复制代码
看下上面两个 Reflect APIReflect.defineMetadata(metadataKey, metadataValue, C.prototype, "method");
Reflect.getMetadata(metadataKey, obj, "method")
可见上面的栗子中,在Custom装饰器中,给元数据设置的值,可以在任何地方获取。
Reflect API
namespace Reflect {
// 用于装饰器
metadata(k, v): (target, property?) => void
// 在对象上面定义元数据
defineMetadata(k, v, o, p?): void
// 是否存在元数据
hasMetadata(k, o, p?): boolean
hasOwnMetadata(k, o, p?): boolean
// 获取元数据
getMetadata(k, o, p?): any
getOwnMetadata(k, o, p?): any
// 获取所有元数据的 Key
getMetadataKeys(o, p?): any[]
getOwnMetadataKeys(o, p?): any[]
// 删除元数据
deleteMetadata(k, o, p?): boolean
}
复制代码
再回到函数装饰器,装饰器有三个参数
- 如果装饰器挂载于静态成员上,则会返回构造函数,如果挂载于实例成员上则会返回类的原型
- 装饰器挂载的成员名称,函数名称或属性名
- 成员的描述符,也就是Object.getOwnPropertyDescriptor的返回值
我简单实现了几个装饰器
// 当前函数的请求方式
enum METHOD {
GET = 0
}
const Methor = (method: METHOD) => (value?: any): MethodDecorator => {
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
Reflect.defineMetadata('methodMetaData', method, target, propertyKey);
}
}
const Get = Methor(METHOD.GET)
复制代码
// 记录函数执行的耗时
const ConsumeTime = (target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<Function>) => {
let method = descriptor.value;
descriptor.value = function () {
let start = new Date().valueOf()
try {
return method.apply(this, arguments).then(() => {
let end = new Date().valueOf()
console.log(`${target.constructor.name}-${propertyKey} start: ${start} end: ${end} consume: ${end - start}`)
}, (e: any) => {
console.error(e)
});
} catch (e) {
console.error('error')
}
}
}
复制代码
// 函数参数校验,这里使用了 Joi
const ParamValidate = (value: any) => {
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
const schema = Joi.object().keys(value);
let method = descriptor.value;
descriptor.value = function () {
const { error, value } = Joi.validate(arguments[1], schema);
if (error) {
throw new Error("ParamValidate Error.");
}
return method.apply(this, arguments);
}
}
}
复制代码
使用如下
class Test {
@ConsumeTime
@Get()
@ParamValidate({
username: Joi.string(),
password: Joi.string(),
})
async userInfo(ctx: any, param: any) {
await this.sleep(1000)
}
async sleep(ms:number){
return new Promise((resolve:any)=>setTimeout(resolve,ms));
}
}
复制代码
函数、函数参数、属性、访问器
小结
reflect-metadata 我们想要介绍一个ts官方的库
core-decorators.js是一个第三方模块,提供了几个常见的修饰器,通过它可以更好地理解修饰器。
装饰器是个很方便的东西,在前端领域它算是个比较新的东西,但是它的思想在后端已经非常成熟了,也可看出,前端工程化是个大趋势,引入成熟的思想,完善前端工程的空缺,以后的前端可做的将越来越广。
作者:小黎也
链接:https://juejin.im/post/5c84c6afe51d453ac76c2d97
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。