ECMAScript中的装饰器

201 阅读5分钟

装饰器的定义

  • 装饰器是一种与类相关的语法,主要用来包装、修改类或类的方法。更多相关内容可参考:github.com/tc39/propos…

装饰器示例

  • 在类上使用装饰器
@testable
class MyTestableClass {
    constructor(name) {
        this.name = name;
    }
}

function testable(target) {
    target.isTestable = true;
}

console.log(MyTestableClass.isTestable);//true

@testable就是一个装饰器。它修改了MyTestableClass这个类的行为,为它加上了静态属性isTestable。其中testable函数的参数targetMyTestableClass类本身。

  • 在类的方法上使用装饰器
class MyTestableClass {
    constructor(name) {
        this.name = name;
    }

    @log
    testSayName() {
        console.log(this.name);
    }
}

function testable(target) {
    target.isTestable = true;
}

new MyTestableClass('nameABC').testSayName();
//调用方法testSayName前打印
//nameABC
//调用方法testSayName后打印

/**
 * @param target 被装饰的属性方法
 * @param name  被装饰的属性方法的名称
 * @param descriptor  被装饰的属性方法的修饰符
 */
function log(target, name, descriptor) {
    console.log(target, name, descriptor);
    const fn = descriptor.value;
    descriptor.value = function(...rest) {
        console.log(`调用方法${name}前打印`);
        fn.call(this, ...rest);
        console.log(`调用方法${name}后打印`);
    };
}

@log也是一个装饰器。它修改了testSayName这个类的方法的行为,在该方法执行前后分别添加了两次输出日志。其中log函数的参数targetMyTestableClass类中testSayName这个属性方法本身,nametestSayName这个属性方法名称,descriptortestSayName这个属性方法的修饰符,包括writable,enumerable,configurable等。

  • 组合使用装饰器
class MyTestableClass {
    constructor(name) {
        this.name = name;
    }

    @time
    @log
    testSayName() {
        console.log(this.name);
    }
}

function testable(target) {
    target.isTestable = true;
}

new MyTestableClass('nameABC').testSayName();
// time
// 调用方法testSayName前打印
// nameABC
// 调用方法testSayName后打印
// time

function log(target, name, descriptor) {
    const fn = descriptor.value;
    descriptor.value = function(...rest) {
        console.log(`调用方法${name}前打印`);
        fn.call(this, ...rest);
        console.log(`调用方法${name}后打印`);
    };
}
function time(target, name, descriptor) {
    const fn = descriptor.value;
    descriptor.value = function(...rest) {
        console.log('time');
        fn.call(this, ...rest);
        console.log('time');
    };
}

组合使用装饰器时,会按照装饰器离被装饰属性的远近顺序依次执行。

注意:装饰器不能用于普通函数,只能用于类及类的方法,因为存在函数提升。

装饰器用途

装饰器的一般用途可大致分为以下几类:

  • 返回包裹函数
function log(target, key, descriptor) {
    return function(...args) {
        const confirmOperate = window.confirm('确定执行操作吗?');
        if (confirmOperate) {
            descriptor.value.call(this, ...args);
        }
    };
}
class MyTest {
    age=18;

    @log()
    btnClick() {
        fetch('/api/delete');
    }
}
  • 对descriptorwritable,enumerable 等属性的修改
function readonly(target, key, descriptor) {
    descriptor.writable = false;
}
class MyTest {
    age=18;

    @readonly
    name='123';
}
const t = new MyTest();
t.age = 20;
// t.name = 'aaa'; //尝试修改时会报错
console.log(t.age, t.name);
  • 对descriptor的value的修改
function log(myParam) {
    console.log('myParam', myParam);
    return function(target, key, descriptor) {
        const fn = descriptor.value;
        descriptor.value = fn.bind(this, myParam);
    };

}
class MyTest {
    constructor(age) {
        this.age = age;
    }

    @log('something')//在ts中,此种写法称为装饰器工厂,区别于普通装饰器,装饰器工厂可传参
    btnClick(a, b) {
        console.log(a, b);//something lall
        // fetch('/api/delete', a, b);
    }
}
console.log((new MyTest(18)).btnClick('lall'));

装饰器实践

以下代码均以在vue@2.x环境为例。

  • 防抖节流
function debounce(time){
    let timer = null;
    return function(target,name,descriptor){
        const fn = descriptor.value;
        descriptor.value = function(...args){
            if(timer) clearTimeout(timer);
            timer = setTimeout(()=>{
                fn.apply(this,args)
            },time)
        }
    }
}
var app = new Vue({ 
    el: '#app', 
    data: { message: 'Hello Vue!' },
    mounted(){
        document.body.addEventListener('scroll',this.handleScroll)
    },
    methods:{
        @debounce(200)
        handleScroll(){
            console.log('scroll')
        }
    }
})

与传统的防抖截流函数相比较,使用装饰器可以简化调用过程。

  • 操作确认
  • 手动埋点
  • 数据校验 不论是操作确认、手动埋点还是数据校验,其基本原理与防抖节流都是相同的,即在用户进行某项操作后的回调函数前增加特定功能的装饰器,执行一些通用的逻辑。
  • 继承(mixin)
function myMixin(...fns) {
    return function(target) {
        fns.forEach((item) => {
            console.log(item, item.length);
            Reflect.ownKeys(item).forEach((i) => {
                if (i !== 'prototype' && i !== 'name' && i !== 'constructor') {
                    const desc = Object.getOwnPropertyDescriptor(item, i);
                    Object.defineProperty(target, i, desc);
                }
            });
            Reflect.ownKeys(item.prototype).forEach((i) => {
                if (i !== 'prototype' && i !== 'name' && i !== 'constructor') {
                    const desc = Object.getOwnPropertyDescriptor(item.prototype, i);
                    Object.defineProperty(target.prototype, i, desc);
                }
            });
        });
    };
}
class P1 {
    constructor() {}

    ha() {
        console.log('ha');
    }
}
class P2 {
    constructor(age) {
        this.age = 18;
    }

    hi() {
        console.log('hi');
    }
}
@myMixin(P1, P2)
class Person {
    constructor(name) {
        this.name = name;
    }
}
const p = new Person('me');
console.log(p.name, p.age);
console.log(p.ha());
console.log(p.hi());

这种方式实现了多重继承时的原型和静态属性的继承,单不能解决constructor中实例属性的继承和传参问题。故将原有的返回值由target改成另一个类可解决此问题。

function myMixin(...fns) {
    return function(target) {
        const list = [...fns, target];
        class M {
            constructor(...args) {
                const tempArguments = args;
                const that = this;
                list.forEach((Item) => {
                    const len = Item.length;
                    const temp = new Item(tempArguments.splice(0, len));
                    Reflect.ownKeys(temp).forEach((i) => {
                        const desc = Object.getOwnPropertyDescriptor(temp, i);
                        Object.defineProperty(that, i, desc);
                    });
                });
            }
        }
        list.forEach((item) => {
            Reflect.ownKeys(item).forEach((i) => {
                if (i !== 'prototype' && i !== 'name' && i !== 'constructor') {
                    const desc = Object.getOwnPropertyDescriptor(item, i);
                    Object.defineProperty(M, i, desc);
                }
            });
            Reflect.ownKeys(item.prototype).forEach((i) => {
                if (i !== 'prototype' && i !== 'name' && i !== 'constructor') {
                    const desc = Object.getOwnPropertyDescriptor(item.prototype, i);
                    Object.defineProperty(M.prototype, i, desc);
                }
            });
        });
        return M;
    };
}
class P1 {
    constructor() {}

    ha() {
        console.log('ha');
    }
}
class P2 {
    constructor(age) {
        this.age = age;
    }

    hi() {
        console.log('hi');
    }
}
@myMixin(P1, P2)
class Person {
    constructor(name) {
        this.name = name;
    }
}
const p = new Person(18, 'me');
console.log(p.name, p.age);
console.log(p.ha());
console.log(p.hi());

以上基本实现了多个类的混合继承,但仍然需要注意构造函数的传参顺序及数量,父类与子类存在同名变量时,父类的属性会被子类覆盖。

拓展-装饰者模式

给对象动态地增加职责的方式称为装饰者(decorator)模式。装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。跟继承相比,装饰者是一种更轻便灵活的做法

在JavaScript中,几乎一切都是对象,其中函数又被称为一等对象。在JavaScript中可以很方便的为某个对象添加属性或方法,最简单粗暴的就是直接改写该对象,但却违反了函数的开放-封闭原则。

开放封闭原则:核心思想是软件实体应该是可扩展,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。

对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。

对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对类进行任何修改。

Function.prototype与装饰器是实现装饰者模式的重要方案,装饰者模式常用于数据统计上报、动态修改函数参数及表单校验等场景。