JavaScript装饰器,你也能用上

592 阅读5分钟

在 JavaScript 里,装饰器(Decorator)是一种特殊的声明,它可以被附加到类声明、方法、属性或访问器上,用于修改类或其成员的行为。装饰器本质上是一个函数,它接收目标对象、属性名和属性描述符作为参数,并返回一个新的属性描述符或修改后的目标对象。装饰器在 ES2016 时以提案的形式提出,在 TypeScript 中得到了广泛支持,并且在现代 JavaScript 项目中也逐渐被应用。下面详细介绍其使用方法和应用场景。

基本语法

在 JavaScript 中使用装饰器,需要使用 @ 符号,后跟装饰器函数名。以下是一个简单的类装饰器示例:

function myDecorator(target) {
    // 对目标类进行修改
    target.prototype.newProperty = 'This is a new property added by decorator';
    return target;
}

@myDecorator
class MyClass {
    constructor() {}
}

const instance = new MyClass();
console.log(instance.newProperty); // 输出: This is a new property added by decorator

在上述代码中,myDecorator 是一个装饰器函数,它接收一个目标类作为参数,并在该类的原型上添加了一个新属性。通过 @myDecorator 将该装饰器应用到 MyClass 类上。

装饰器的参数

在 JavaScript 中,装饰器本质上是一个函数,根据其应用的目标不同(类、方法、属性等),接收的参数也有所不同。下面详细介绍不同类型装饰器函数所接收的参数。

类装饰器

类装饰器是应用在类声明上的装饰器,它接收一个参数,即被装饰的类的构造函数。

function classDecorator(target) {
    // target 是被装饰的类的构造函数
    console.log('类构造函数:', target);
    // 可以对类进行修改,例如添加静态属性
    target.newStaticProperty = 'This is a new static property';
    return target;
}

@classDecorator
class MyClass {
    constructor() {}
}

console.log(MyClass.newStaticProperty); // 输出: This is a new static property

在上述代码中,classDecorator 是一个类装饰器,它接收 target 参数,该参数就是 MyClass 类的构造函数。通过这个参数,可以对类进行修改,如添加静态属性。

方法装饰器

方法装饰器应用在类的方法上,它接收三个参数:

  1. target:对于静态方法,它是类的构造函数;对于实例方法,它是类的原型对象。
  2. propertyKey:被装饰方法的名称,是一个字符串或符号(Symbol)类型。
  3. descriptor:方法的属性描述符对象,包含 value(方法的实际实现)、writableenumerable 和 configurable 等属性。
function methodDecorator(target, propertyKey, descriptor) {
    console.log('目标对象:', target);
    console.log('方法名称:', propertyKey);
    console.log('属性描述符:', descriptor);

    const originalMethod = descriptor.value;
    descriptor.value = function (...args) {
        console.log(`调用方法 ${propertyKey} 前`);
        const result = originalMethod.apply(this, args);
        console.log(`调用方法 ${propertyKey} 后`);
        return result;
    };
    return descriptor;
}

class MyClass {
    @methodDecorator
    myMethod() {
        console.log('方法执行中');
    }
}

const instance = new MyClass();
instance.myMethod();

在这个例子中,methodDecorator 是一个方法装饰器,通过修改 descriptor.value 来包裹原始方法,实现了在方法调用前后添加日志的功能。

属性装饰器

属性装饰器应用在类的属性上,它接收两个参数:

  1. target:对于静态属性,它是类的构造函数;对于实例属性,它是类的原型对象。
  2. propertyKey:被装饰属性的名称,是一个字符串或符号(Symbol)类型。
function propertyDecorator(target, propertyKey) {
    console.log('目标对象:', target);
    console.log('属性名称:', propertyKey);
    // 可以在这里对属性进行一些操作,例如设置默认值等
}

class MyClass {
    @propertyDecorator
    myProperty;
}

const instance = new MyClass();

在上述代码中,propertyDecorator 是一个属性装饰器,它接收 target 和 propertyKey 参数,可用于对属性进行一些预处理操作。

访问器装饰器

访问器装饰器应用在类的 getter 或 setter 方法上,其参数与方法装饰器相同,即 targetpropertyKey 和 descriptor

function accessorDecorator(target, propertyKey, descriptor) {
    console.log('目标对象:', target);
    console.log('访问器名称:', propertyKey);
    console.log('属性描述符:', descriptor);

    const originalGetter = descriptor.get;
    descriptor.get = function () {
        console.log(`获取 ${propertyKey} 属性值前`);
        const result = originalGetter ? originalGetter.call(this) : undefined;
        console.log(`获取 ${propertyKey} 属性值后`);
        return result;
    };
    return descriptor;
}

class MyClass {
    private _myValue = 0;

    @accessorDecorator
    get myValue() {
        return this._myValue;
    }

    set myValue(value: number) {
        this._myValue = value;
    }
}

const instance = new MyClass();
console.log(instance.myValue);

在这个例子中,accessorDecorator 是一个访问器装饰器,通过修改 descriptor.get 来包裹原始的 getter 方法,实现了在获取属性值前后添加日志的功能。

传递参数

若要在装饰器里传递参数,可通过创建一个高阶函数来实现。高阶函数会返回一个装饰器函数,这样就能把参数传递给返回的装饰器函数。下面分别从类装饰器、方法装饰器和属性装饰器的角度,介绍如何在装饰器中传递参数。

类装饰器传递参数

类装饰器用于修改类的定义。要给类装饰器传递参数,可创建一个返回装饰器函数的函数。

// 定义一个接收参数的高阶函数,返回一个类装饰器
function classDecoratorWithParams(param1, param2) {
    return function (target) {
        // 打印传入的参数
        console.log(`Received parameters: ${param1}, ${param2}`);
        // 可以对目标类进行修改,这里只是简单添加一个静态属性
        target.newStaticProperty = `Combined: ${param1} - ${param2}`;
        return target;
    };
}

// 使用带参数的类装饰器
@classDecoratorWithParams('value1', 'value2')
class MyClass {
    constructor() {}
}

// 访问添加的静态属性
console.log(MyClass.newStaticProperty); 

在上述代码中,classDecoratorWithParams 是一个高阶函数,它接收 param1 和 param2 作为参数,并返回一个类装饰器函数。这个装饰器函数会对目标类 MyClass 进行修改,添加一个静态属性。

方法装饰器传递参数

方法装饰器用于修改类的方法。要给方法装饰器传递参数,同样可以创建一个返回装饰器函数的高阶函数。

// 定义一个接收参数的高阶函数,返回一个方法装饰器
function methodDecoratorWithParams(message) {
    return function (target, propertyKey, descriptor) {
        const originalMethod = descriptor.value;
        descriptor.value = function (...args) {
            // 打印传入的消息
            console.log(`Message from decorator: ${message}`);
            // 调用原始方法
            return originalMethod.apply(this, args);
        };
        return descriptor;
    };
}

class MyClass {
    // 使用带参数的方法装饰器
    @methodDecoratorWithParams('This is a custom message')
    myMethod() {
        console.log('Method is executing');
    }
}

const instance = new MyClass();
instance.myMethod();

在这段代码中,methodDecoratorWithParams 是一个高阶函数,它接收 message 作为参数,并返回一个方法装饰器函数。这个装饰器函数会在调用原始方法之前打印传入的消息。

属性装饰器传递参数

属性装饰器用于修改类的属性。要给属性装饰器传递参数,也采用创建高阶函数返回装饰器函数的方式。

// 定义一个接收参数的高阶函数,返回一个属性装饰器
function propertyDecoratorWithParams(defaultValue) {
    return function (target, propertyKey) {
        let value = defaultValue;
        const getter = function () {
            return value;
        };
        const setter = function (newValue) {
            value = newValue;
        };
        Object.defineProperty(target, propertyKey, {
            get: getter,
            set: setter,
            enumerable: true,
            configurable: true
        });
    };
}

class MyClass {
    // 使用带参数的属性装饰器
    @propertyDecoratorWithParams('default value')
    myProperty;
}

const instance = new MyClass();
console.log(instance.myProperty); 
instance.myProperty = 'new value';
console.log(instance.myProperty); 

在这个例子中,propertyDecoratorWithParams 是一个高阶函数,它接收 defaultValue 作为参数,并返回一个属性装饰器函数。这个装饰器函数会为属性设置默认值,并定义其 getter 和 setter 方法。

应用场景

1. 日志记录

可以使用装饰器在方法执行前后记录日志,方便调试和监控。

function logMethod(target, name, descriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args) {
        console.log(`Calling method ${name} with arguments:`, args);
        const result = originalMethod.apply(this, args);
        console.log(`Method ${name} returned:`, result);
        return result;
    };
    return descriptor;
}

class Calculator {
    @logMethod
    add(a, b) {
        return a + b;
    }
}

const calculator = new Calculator();
const result = calculator.add(2, 3);

在这个例子中,logMethod 装饰器用于记录 add 方法的调用参数和返回值。

2. 权限验证

在方法执行前进行权限验证,只有具备相应权限的用户才能调用该方法。

function checkPermission(requiredRole) {
    return function (target, name, descriptor) {
        const originalMethod = descriptor.value;
        descriptor.value = function (...args) {
            const userRole = this.userRole; // 假设 this 上有 userRole 属性
            if (userRole === requiredRole) {
                return originalMethod.apply(this, args);
            } else {
                console.log('You do not have permission to call this method.');
            }
        };
        return descriptor;
    };
}

class UserService {
    userRole = 'admin';

    @checkPermission('admin')
    deleteUser() {
        console.log('User deleted.');
    }
}

const userService = new UserService();
userService.deleteUser();

这里的 checkPermission 装饰器会检查调用者的角色是否符合要求,如果符合则执行原方法,否则给出提示。

3. 自动绑定 this

在 JavaScript 中,类方法的 this 指向可能会出现问题。可以使用装饰器自动绑定 this

function autobind(target, name, descriptor) {
    const originalMethod = descriptor.value;
    return {
        configurable: true,
        get() {
            const boundMethod = originalMethod.bind(this);
            Object.defineProperty(this, name, {
                value: boundMethod,
                configurable: true,
                writable: true
            });
            return boundMethod;
        }
    };
}

class Button {
    constructor() {
        this.text = 'Click me';
    }

    @autobind
    handleClick() {
        console.log(`Button text: ${this.text}`);
    }
}

const button = new Button();
const clickHandler = button.handleClick;
clickHandler();

autobind 装饰器确保 handleClick 方法在被调用时,this 始终指向 Button 实例。

4. 性能监控

使用装饰器来监控方法的执行时间,帮助分析性能瓶颈。

function performanceMonitor(target, name, descriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = async function (...args) {
        const start = performance.now();
        const result = await originalMethod.apply(this, args);
        const end = performance.now();
        console.log(`Method ${name} took ${end - start} milliseconds to execute.`);
        return result;
    };
    return descriptor;
}

class DataFetcher {
    @performanceMonitor
    async fetchData() {
        // 模拟异步数据获取
        await new Promise(resolve => setTimeout(resolve, 1000));
        return { data: 'Some data' };
    }
}

const fetcher = new DataFetcher();
fetcher.fetchData();

performanceMonitor 装饰器记录了 fetchData 方法的执行时间。

总之,装饰器提供了一种简洁而强大的方式来修改类和其成员的行为,在很多场景下都能提高代码的可维护性和复用性。