Decorator——JS装饰器

996 阅读1分钟

深入解析如何在 JavaScript 中实现装饰器,详细介绍类装饰器、属性装饰器、方法装饰器(包括普通方法和访问器方法)、以及参数装饰器。

装饰器允许在不增加代码复杂性的情况下向类和属性添加新功能。

JS实现装饰器

// 透明缓存
function cachingDecorator(fn){
    let cache = new Map();
    return function (x) {
        if(cache.has(x)){
            console.log(`fn${x}的结果已经缓存了`);
            return cache.get(x);
        }else {
            let result = fn(x)
            cache.set(x, result);
            return result;
        }
    }
}
const squire = x => x ** 2
const cachingSquire = cachingDecorator(squire)
// 传递多个参数
function cachingDecorator(fn){
    let cache = new Map();
    return function (...args) {
        let key = hash(args)
        if(cache.has(key)){
            console.log(`fn${args}的结果已经缓存了`);
            return cache.get(key);
        }else {
            let result = fn.apply(this, args)
            cache.set(key, result);
            return result;
        }
    }
}
const show1 = (...args)=> {
    console.log(args);
    return args
}
const show2 = cachingDecorator(show1)
// 延时装饰器
function delayDecorator(fn, ms){
    return function (...args) {
        console.log(`我是装饰后的函数,延时了${ms / 1000}秒`);
        setTimeout(() => fn.apply(this, args), ms)
    }
}
// 防抖装饰器
function debounceDecorator(fn, ms){
    let timeout;
    return function (...args) {
        clearTimeout(timeout)
        timeout = setTimeout(() => fn.apply(this, args), ms)
    }
}
// 节流装饰器
function throttleDecorator(fn, ms){
    let isThrottled = false,
        savedArgs,
        savedThis;
    function wrapper(...args){
        if(isThrottled){
            savedArgs = args
            savedThis = this
            return
        }
        fn.apply(this, args)
        isThrottled = true
        setTimeout(() => {
            isThrottled = false
            if(savedArgs){
                wrapper.apply(savedThis, savedArgs)
                savedArgs = savedThis = null
            }
        }, ms)
    }
    return wrapper
}

尽管装饰器在 TypeScript 和 Python 等语言中被广泛使用,但是 JavaScript 装饰器的支持仍处于第 2 阶段提案中(stage 2 proposal)。但是,我们可以借助 Babel 和 TypeScript 编译器使用 JavaScript 装饰器。

装饰器有一种特殊的语法。它们以 @ 符号为前缀,放置在我们需要装饰的代码之前。另外,可以一次使用多个装饰器

类装饰器

类装饰器应用于整个类。因此,我们所做的任何修改都会影响整个类。对类装饰器所做的任何事情都需要通过返回一个新的构造函数来替换类构造函数

function addTimestamp(target) {
  // 只有一个参数,target就是被装饰的类
  console.log(target) // [Function: MyClass]
  // 给类的原型添加一个timestamp属性,初始化为当前时间
  target.prototype.timestamp = new Date()
}

@addTimestamp
class MyClass {}

const instance = new MyClass()
console.log(instance.timestamp) // 输出当前日期和时间:2024-08-07T08:50:01.234Z
// 使用高阶函数进行传参
function addTimestamp(options) {
  return function(target) {
    // 给类的原型添加一个timestamp属性,初始化为当前时间
    target.prototype.timestamp = options?.initialDate || new Date();
  }
}

@addTimestamp({ initialDate: new Date('2024-01-01') })
class MyClass {}

const instance = new MyClass();
console.log(instance.timestamp); // 输出: 2024-01-01T00:00:00.000Z

属性装饰器

最常见的就是设置属性只读

function readOnly(prototype, propertyKey, descriptor) {
    // 三个参数分别是:类的原型对象,属性名,属性描述符
    console.log(prototype);
    console.log(propertyKey);
    console.log(descriptor);
    descriptor.writable = false;
}

class Car {
    @readOnly
    make = 'Toyota';
}

const myCar = new Car();
console.log(myCar.make); // 输出: Toyota
try {
    myCar.make = 'Honda'; // 此操作将静默失败或在严格模式下抛出错误
} catch(error) {
    console.log(error.message); // 输出: Cannot assign to read only property 'make' of object '#<Car>'
}
console.log(myCar.make); // 仍输出: Toyota

方法装饰器

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

普通方法装饰器

function logMethod(prototype, propertyKey, descriptor) {
    // 三个参数分别是:类的原型对象,方法名,方法描述符
    console.log(prototype); // {}
    console.log(propertyKey); // greet
    console.log(descriptor); 
    /*
    {
        value: [Function: greet],
        writable: true,
        enumerable: false,
        configurable: true
    }
    */
    const originalMethod = descriptor.value;

    descriptor.value = function (...args) {
        console.log(`调用方法${propertyKey},参数为:`, args); // 调用方法greet,参数为: [ 'World' ]
        return originalMethod.apply(this, args);
    };
    return descriptor;
}

class MyClass {
    @logMethod
    greet(name) {
        return `Hello, ${name}!`; // Hello, World!
    }
}

const instance = new MyClass();
console.log(instance.greet('World'));

访问器装饰器

function capitalize(prototype, propertyKey, descriptor) {
    console.log(prototype); // {}
    console.log(propertyKey); // name
    console.log(descriptor); 
    /*
    {
        get: [Function: get],
        set: undefined,
        enumerable: false,
        configurable: true
    }
    */
    // 三个参数分别是:类的原型对象,方法名,方法描述符
    const originalGetter = descriptor.get;
    descriptor.get = function () {
        const result = originalGetter.call(this);
        return result.toUpperCase();
    };
    return descriptor;
}

class Person {
    constructor(name) {
        this._name = name;
    }
    @capitalize
    get name() {
        return this._name;
    }
}

const person = new Person('john');
console.log(person.name); // 输出: JOHN

参数装饰器

暂时还未支持

function logParameter(prototype, propertyKey, parameterIndex) {
    // 三个参数分别是:类的原型对象,方法名,参数在参数列表中的索引
    const originalMethod = prototype[propertyKey];
    prototype[propertyKey] = function (...args) {
        console.log(`方法${propertyKey}的第${parameterIndex}个参数值为:`, args[parameterIndex]);
        return originalMethod.apply(this, args);
    };
}

class User {
    greet(@logParameter name) {
        return `Hello, ${name}!`;
    }
}

const user = new User();
console.log(user.greet('Alice'));