1. 前言
知道装饰器还是在 Mobx 中见过类似的语法,对某个类或属性或方法进行包装修饰,是一种与类(class)相关的语法,用来注释或修改类和类方法,是实现面向切面编程(AOP)的一种重要模式。
import { Controller, Get, Post } from '@nestjs/common';
@Controller('cats');
export class CatsController {
@Post();
create(): string {
return 'this action adds a new cat';
}
@Get();
findAll(): string {
return 'this action returns all cats';
}
}
下面是一个使用装饰器修饰属性的简单例子,@readonly 可以将 count 属性设置为只读,通过此类方式,装饰器可以大大提高代码的简洁性和可读性。
class Person {
@readonly count = 0;
}
由于目前所有浏览器暂未支持装饰器语法,如果要看到运行效果,可以通过去 babel 官网进行验证。
2. 装饰器模式
经典的装饰器模式是一种结构型设计模式,它允许向一个现有的对象中添加新的功能,同时又保证不改变它的结构,是对现有类的一个包装修饰。
通常来说,在代码设计中,应该遵循 「多用组合,少用继承」的原则。通过装饰器模式可以动态地给一个对象添加额外的职责。就增加新的功能而言,装饰器模式比生成子类更加灵活,简单。
2.1 一个英雄的例子
在游戏中设计一个特定的英雄的类。
class Hero {
attack() {}
}
class SpecialHero extends Hero {
attack() {
console.log('斩钢闪');
}
}
当另外一个英雄具有上述英雄的一些技能(属性或方法)时,就需要第二次继承,此时需要继承 SpecialHero。
class FirstHero extends SpecialHero {}
如果有第二个英雄、第三个英雄时,就要继承四次 SpecialHero 类,岂不是有多少个英雄就要继承多少次 SpecialHero 类。
class SecondHero extends SpecialHero {}
class ThirdHero extends SpecialHero {}
...
可以换一种思路来思考这个问题,把英雄身上的 Skills 当做英雄身上的衣服。在不同的季节就换上不同的衣服,到了冬天,甚至会叠加多件衣服。当 Skills 都没有了,相当于把这件衣服脱了下来。
衣服对人来说起到修饰作用,Skills 对于英雄来说也只是增强效果。想到这里,你是不是有思路了呢?没错,可以创建 Skills 类,传入英雄后获得一个新的增强后的英雄类。
class FirstHero extends Skills {
construtor(hero) {
this.hero = hero;
}
extraDamage() {}
attack() {
return this.hero.attack() + this.extraDamage();
}
}
class SecondHero extends Skills {
construtor(hero) {
this.hero = hero;
}
// 技能 (减10%)
skillDecrese() {
return this.hero.skillDecrese() * 0.9;
}
}
class ThirdHero extends Skills {
construtor(hero) {
this.hero = hero;
}
// 回城速度缩短一半
backSpeed() {
return this.hero.backSpeed() * 0.5;
}
}
定义好所有的 Skills 类后,就可以直接套用到英雄身上,这样看起来是不是清爽了许多呢?这种写法看起来很像函数的组合。
const specialhero = new specialHero(); // 特殊的英雄
const firsthero = new FirstHero(specialhero); // 第一个英雄
const secondhero = new FirstHero(specialhero); // 第二个英雄
const thirdhero = new FirstHero(specialhero); // 第三个英雄
3. ES7 装饰器
decorator (装饰器)是 ES7 中的一个提案,目前处于 stage-2 阶段。装饰器与函数组合(compose)以及高阶函数很相似,使用 @ 符号作为标识符,放置在被装饰的代码前面。在 Python 语言中,早就已经有了非常成熟的装饰器方案,下面就来看看 Python 中的一个装饰器的例子。
3.1 Python 中的装饰器
def auth(func):
def inner(request,*args,**kwargs):
v = request.COOKIES.get('user')
if not v:
return redirect('/login')
return func(request,*args,**kwargs)
return inner
@auth
def index(request):
v = request.COOKIES.get('user')
return render(request,"index.html",{"current_user":v})
auth 装饰器是通过检查 cookie 来判断用户是否登录的。 auth 函数是一个高阶函数,它接受了一个 func 函数作为参数,返回了一个新的 inner 函数。在 inner 函数中进行 cookie 的检查,由此来判断跳回登录页面还是继续执行 func 函数。在所有需要权限验证的函数上,都可以使用这个 auth 装饰器,简洁明了且无侵入。
3.2 JavaScript中的装饰器
JavaScript 中的装饰器和 Python 中的装饰器类似,依赖于 Object.defineProperty,一般是用来装饰类、类属性、类方法。使用装饰器可以做到不直接修改代码,就实现某些功能,做到真正的面向切面编程。这在一定程度上和 Proxy 很相似,但使用起来比 Proxy 会更加简洁。
3.3 类装饰器
装饰类的时候,装饰器方法一般会接收一个目标类作为参数。下面是一个给目标类增加静态属性 test 的例子:
const decoratorClass = (targetClass) => {
targetClass.test = "123"
}
@decoratorClass
class Test {}
Test.test; // "123"
除了修改类本身,还可以通过修改原型,给实例增加新属性。下面是给目标类增加 speak 方法的例子:
const withSpeak = (targetClass) => {
const prototype = targetClass.prototype;
prototype.speak = function() {
console.log('I can speak ', this.language);
}
}
@withspeak
class Student {
constructor(language) {
this.language = language;
}
}
const student1 = new Student('Chinese');
const student2 = new Studnent('English');
student1.speak(); // "I can speak Chinese"
student2.speak(); // "I can speak English"
利用高阶函数的属性,还可以给装饰器传参,通过参数来判断对类进行什么处理。
const withLanguage = (language) => (targetClass) =>{
targetClass.prototype.language = language;
}
@withLanguage('Chinese')
class Student {}
const student = new Student();
student.language; // 'Chinese'
如果你经常编写 react-redux 的代码,会经常遇到需要将 store 中的数据映射到组件的情况。 其中 connect 就是一个高阶组件,它接收两个函数 mapStateToProps 和 mapDispatchToProps 以及一个组件 App , 最终返回一个增强版的组件。
class App extends React.Component {}
connect(mapStateToProps,mapDispatchToProps)(App)
有了装饰器之后, connect 的写法可以变得更加优雅。
@connect(mapStateToProps,mapDispatchToProps)
class App extends React.Component {}
3.4 类属性装饰器
类属性装饰器可以用在类的属性、方法、get/set 函数中,一般会接收三个参数;
(1) .target : 被修饰的类;
(2). name:类成员的名字;
(3). decriptor:属性描述符,对象会将这个参数传给 Object.defineProperty , 使用类属性可以做很多有趣的事情,比如最开始的那个 readonly
的例子:
function readonly(target,name,descriptor) {
descriptor.writable = false;
return descriptor;
}
class Person = {
@readonly name = 'person'
}
const person = new Person();
person.name = 'tom';
还可以统计一个函数的执行时间,以便后期做一些性能优化。
function time(target, name, descriptor) {
const func = descriptor.value;
if (typeof func === "function") {
descriptor.value = function(...args) {
console.time();
const results = func.apply(this, args);
console.timeEnd();
return results;
}
}
}
class Person {
@time
say() {
console.log('hello');
}
}
const person = new Person();
person.say();
在 react 知名的状态管理库 mobx 中,也通过装饰器将类属性设置为可观察属性,以此来实现响应式编程。
import { observale, action, autorun } from 'mobx'
class Store {
@observale count = 1;
@action
changeCount(count) {
this.count = count;
}
}
const store = new Store();
autorun(() => {
console.log('count is ', store.count);
})
store.changeCount(10); // 修改 count 的值,会引起 autorun 中的函数自动执行
3.5 装饰器组合
如果需要多个装饰器,那该怎么办呢?装饰器是可以叠加的,根据与被装饰类或属性的距离,由近到远依次执行。(就近原则)
class Person {
@time
@log
say() {}
}
除此之外在,在装饰器的提案中,还出现了一种组合多种装饰器的装饰器例子。目前还没有见到被使用。通过使用 decorato
r 来声明一个组合装饰器 xyz
, 这个装饰器组合了多种装饰器。
decorator @xyz (arg,arg2) {
@foo @bar(arg) @baz(arg2)
}
@xyz(1,2) class C {}
上面的写法和下面的是类似的:
@foo @bar(1) @bar(2)
class C {}
4.装饰器可以用来干啥
4.1 多重继承
通过 mixin
可以实现多重继承,如果使用装饰器可以进步一简化 mixin
的使用。 mixin
方法将会接收一个父类列表,通过它来装饰目标类。我们理想中的用法应该是这样:
@mixin (Parent1,Parent2,Parent3)
class Childe {}
这里只需要拷贝父类的原型属性和实例属性就可以实现多重继承。这里创建了一个新的 Mixin
类,来将 mixin
和 targetClass
上面的所有属性都拷贝过去。
const mixin = (...mixins) => (targetClass) => {
mixins = [targetClass, ...mixins];
function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if (key !== 'construtor' && key !== 'prototype' && key !== 'name') {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}
class Mixin {
constructor(...args) {
for (let mixin of mixins) {
copyProperties(this, new mixin(...args)); // 拷贝实例属性
}
}
}
for (let mixin of mixins) {
copyProperties(Mixin, mixin); // 拷贝静态属性
copyProperties(Mixin.prototype, mixin.prototype); // 拷贝原型属性
}
return Mixin;
}
export default mixin;
可以来测试一下这个 mixin 方法是否能够正常工作吧。
class Parent1 {
p1() { console.log('this is parent1') }
}
class Parent2 {
p2() { console.log('this is parent2') }
}
class Parent3 {
p3() { console.log('this is parent3') }
}
@mixin(parent1, parent2, parent3);
class Child {
c1 = () => {
console.log('this is child')
}
}
const child = new Child();
console.log(child);
最终在浏览器中打印出来的 child 对象是这样的,证明这个 mixin 是可以正常工作的,注意这里的 Child
就是前面的 Mixin
类。
也许你会问,为什么要创建一个多余的 Mixin
类呢?为什么不直接修改 targetClass 的 constructor 呢?
原因是 Proxy 会拦截 constructor。 这里是 Proxy 的一种使用场景,使用 Proxy 会更优雅。
const mixin = (...mixins) => (targetClass) => {
function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if (key !== "construtor" && key !== "name" && key !== "prototype") {
let desc = Object.getOwnProperyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}
for (let mixin of mixins) {
copyProperties(targetClass, mixin); // 拷贝静态属性
copyProperties(targetClass.prototype, mixin.prototype); // 拷贝原型属性
}
// 拦截 construct 方法, 进行实例属性的拷贝
return new Proxy(targetClass, {
construct(target, args) {
const obj = new target(...args);
for (let mixin of mixins) {
copyProperties(obj, new mixin()); // 拷贝实例属性
}
return obj;
}
});
}
4.2 防抖和节流
通常我们在频繁出发的场景下,为了优化性能,经常会使用到节流函数。下面以 React 组件绑定滚动事件为例:
const App extends React.Component {
componentDidMount() {
this.handleScroll = _.throttle(this.srcoll, 500);
window.addEventListener('srcoll', this.handleScroll)
}
componentWillUnmount() {
window.removeEventListener('srcoll', this.handleScroll)
}
srcoll() {}
}
在组件中绑定事件需要注意在组件销毁时进行解绑。而由于节流函数返回了一个新的匿名函数,所以为了之后能够有效解绑,不得不将这个匿名函数存起来,以便之后使用。但是有了装饰器之后,我们就不必在每个绑定事件的地方都手动设置 throttle 方法,只需要在 scroll 函数添加 一个 throttle 的装饰器就行了。
const throttle = (wait) => {
let prev = new Date();
return (target, name, descriptor) => {
const func = descriptor.value;
if (typeof func === 'function') {
descriptor.value = function(...args) {
const now = new Date();
if (now - prev > wait) {
fn.apply(this, args);
prev = new Date();
}
}
}
}
}
使用起来要比原来简介许多。
class App extends React.Component {
componentDidMount() {
window.addEventListener('srcoll',this.srcoll);
}
componentWillUnmount() {
window.removeEventListener('srcoll',this.srcoll);
}
@throttle(50)
srcoll() {}
}
而实现防抖 (debounce)函数装饰器和节流函数类似,这里不多说了。
const debounce = (wait) => {
let timer;
return (target, name, descriptor) => {
const func = descriptor.value;
if (typeof func === 'function') {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, wait);
}
}
}
4.3 数据格式验证
通过类属性修饰器来对类的属性进行类型的校验。
const validate = (type) => (target, name) => {
if (typeof target[name] !== type) {
throw new Error(`attribute ${name} must be ${type} type`)
}
class Form {
@validate('string')
name = 111 // Error: attribute name must be string type
}
}
如果你觉得对属性一个一个手动去校验太过麻烦,也可以通过编写校验规则,来对整个类进行校验。
const rules = {
name: 'string',
password: 'string',
age: 'number'
}
const validator = rules => targetClass => {
return new Proxy(target, {
construct(target, args) {
const obj = new target(...args);
for (let [name, type] of Object.entries(rules)) {
if (typeof rules[name] !== type) {
throw new Error(`${name} must be ${type}`);
}
}
return obj;
}
})
}
@validator(rules);
class Person = { name: 'tom', password = '123', age = '21' };
const person = new Person(); // Error:
4.4 core-decorators.js
core-decorators
是一个封装了常用装饰器的 JS 库,它归纳了下面这些装饰器(只列举了部分)。
(1). autobind
:自动绑定 this , 告别箭头函数和 bind ;
(2). readonly
:将类属性设置为可读;
(3). override
:检查子类的方法是否覆盖了父类的同名方法 ;
(4). debounce
:防抖函数 ;
(5). throttle
:节流函数 ;
(6). enumerable
:让一个类方法变得可枚举;
(7). nonenumerable
:让一个类方法变得不可枚举 ;
(8). time
:打印函数执行耗时 ;
(9). mixin
:将多个对象混入类(和上面的 mixin 不太一样) ;
5.总结
JavaScript 装饰器是高级 JavaScript 编程语法中的一个较难的知识点,刚开始会理解不透彻,但随着在实际场景以及实际项目中开始使用,会是代码简洁许多,这里推荐尝试 React 的状态管理工具 Mobx 库,因为里面有多种使用装饰器的实际场景和案例。