MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript属性装饰器:实现属性监听与验证

2021-11-106.2k 阅读

TypeScript属性装饰器简介

在TypeScript中,装饰器是一种特殊类型的声明,它能够被附加到类声明、方法、属性或参数上,用来修改类的行为。属性装饰器是装饰器中的一种,它应用于类的属性声明。属性装饰器表达式会在运行时当作函数被调用,它接受两个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象:这个参数为我们提供了操作类相关对象的入口,我们可以通过它来访问类的其他成员或者修改类的行为。
  2. 属性的名称:知道属性名称有助于我们在装饰器逻辑中精准定位到特定的属性。

属性装饰器的基本语法如下:

function propertyDecorator(target: any, propertyKey: string) {
    // 装饰器逻辑
}

class MyClass {
    @propertyDecorator
    myProperty: string;
}

实现属性监听

  1. 基本原理 实现属性监听的核心思想是利用JavaScript的对象.defineProperty方法。通过这个方法,我们可以重新定义属性的访问器(getter和setter)。在属性装饰器中,我们可以获取到属性所在的类的原型对象和属性名称,然后使用Object.defineProperty方法来重新定义属性的访问器,在访问器中添加监听逻辑。

  2. 代码示例

function logProperty(target: any, propertyKey: string) {
    let value: any;
    const originalDescriptor = Object.getOwnPropertyDescriptor(target, propertyKey);

    const getter = function () {
        console.log(`Getting ${propertyKey}`);
        return value;
    };

    const setter = function (newValue: any) {
        console.log(`Setting ${propertyKey} to ${newValue}`);
        value = newValue;
    };

    if (originalDescriptor) {
        if (typeof originalDescriptor.get === 'function') {
            getter = originalDescriptor.get;
        }
        if (typeof originalDescriptor.set === 'function') {
            setter = originalDescriptor.set;
        }
    }

    Object.defineProperty(target, propertyKey, {
        get: getter,
        set: setter,
        enumerable: true,
        configurable: true
    });
}

class User {
    @logProperty
    name: string;

    constructor(name: string) {
        this.name = name;
    }
}

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

在上述代码中,logProperty装饰器实现了对name属性的监听。当获取name属性时,会打印Getting name,当设置name属性时,会打印Setting name to [新值]

  1. 深度分析 首先,我们定义了一个变量value来存储属性的实际值。然后通过Object.getOwnPropertyDescriptor获取属性的原始描述符,这样做是为了在不覆盖原始属性配置的情况下添加我们的监听逻辑。如果原始属性有自己的gettersetter,我们会保留它们,并在其基础上添加我们的打印逻辑。最后使用Object.defineProperty重新定义属性,将新的gettersetter方法应用到属性上,同时保持属性的可枚举性和可配置性。

实现属性验证

  1. 基本原理 属性验证就是在属性被设置时,检查传入的值是否符合特定的规则。我们可以在属性装饰器中利用Object.defineProperty方法的setter来实现这一功能。当属性被设置时,setter函数会被调用,我们在其中添加验证逻辑,如果验证失败,可以抛出错误或者采取其他处理措施。

  2. 代码示例

function validateNumber(target: any, propertyKey: string) {
    let value: any;
    const originalDescriptor = Object.getOwnPropertyDescriptor(target, propertyKey);

    const setter = function (newValue: any) {
        if (typeof newValue!== 'number') {
            throw new Error(`${propertyKey} must be a number`);
        }
        value = newValue;
    };

    if (originalDescriptor) {
        if (typeof originalDescriptor.set === 'function') {
            setter = originalDescriptor.set;
        }
    }

    Object.defineProperty(target, propertyKey, {
        set: setter,
        enumerable: true,
        configurable: true
    });
}

class Product {
    @validateNumber
    price: number;

    constructor(price: number) {
        this.price = price;
    }
}

try {
    const product = new Product(100);
    console.log(product.price);
    product.price = 200;
    product.price = 'not a number';
} catch (error) {
    console.error(error.message);
}

在上述代码中,validateNumber装饰器用于验证price属性是否为数字类型。如果尝试设置非数字值,就会抛出错误。

  1. 深度分析 与属性监听类似,我们首先定义了一个变量value来存储属性值,并获取属性的原始描述符。在setter函数中,我们检查传入的newValue是否为数字类型。如果不是,就抛出错误。这样就确保了price属性只能被设置为数字类型的值。同样,我们在处理原始描述符时,保留了原始的setter(如果存在),并将新的验证逻辑与之结合。

结合属性监听与验证

  1. 基本思路 我们可以在一个属性装饰器中同时实现属性监听和验证的功能。这样在属性被设置时,既可以验证值的合法性,又可以进行监听操作。

  2. 代码示例

function validateAndLog(target: any, propertyKey: string) {
    let value: any;
    const originalDescriptor = Object.getOwnPropertyDescriptor(target, propertyKey);

    const getter = function () {
        console.log(`Getting ${propertyKey}`);
        return value;
    };

    const setter = function (newValue: any) {
        if (typeof newValue!== 'number') {
            throw new Error(`${propertyKey} must be a number`);
        }
        console.log(`Setting ${propertyKey} to ${newValue}`);
        value = newValue;
    };

    if (originalDescriptor) {
        if (typeof originalDescriptor.get === 'function') {
            getter = originalDescriptor.get;
        }
        if (typeof originalDescriptor.set === 'function') {
            setter = originalDescriptor.set;
        }
    }

    Object.defineProperty(target, propertyKey, {
        get: getter,
        set: setter,
        enumerable: true,
        configurable: true
    });
}

class Order {
    @validateAndLog
    quantity: number;

    constructor(quantity: number) {
        this.quantity = quantity;
    }
}

try {
    const order = new Order(5);
    console.log(order.quantity);
    order.quantity = 10;
    order.quantity = 'not a number';
} catch (error) {
    console.error(error.message);
}

在这个例子中,validateAndLog装饰器同时实现了对quantity属性的验证和监听。当获取属性时会打印获取信息,当设置属性时,先验证值是否为数字,然后打印设置信息。

  1. 深度分析 在这个装饰器中,我们结合了属性监听和验证的逻辑。getter函数实现了属性监听中的获取属性时的打印逻辑,setter函数首先进行属性值的验证,如果验证通过,再进行属性监听中的设置属性时的打印逻辑。通过这种方式,我们在一个装饰器中实现了两种功能,使得代码更加简洁和高效。

装饰器的组合使用

  1. 基本概念 在实际开发中,我们可能需要对一个属性应用多个装饰器,以实现更复杂的功能。装饰器是可以按照顺序依次应用的,这就允许我们将不同功能的装饰器组合起来。

  2. 代码示例

function log(target: any, propertyKey: string) {
    let value: any;
    const originalDescriptor = Object.getOwnPropertyDescriptor(target, propertyKey);

    const getter = function () {
        console.log(`Getting ${propertyKey}`);
        return value;
    };

    const setter = function (newValue: any) {
        console.log(`Setting ${propertyKey} to ${newValue}`);
        value = newValue;
    };

    if (originalDescriptor) {
        if (typeof originalDescriptor.get === 'function') {
            getter = originalDescriptor.get;
        }
        if (typeof originalDescriptor.set === 'function') {
            setter = originalDescriptor.set;
        }
    }

    Object.defineProperty(target, propertyKey, {
        get: getter,
        set: setter,
        enumerable: true,
        configurable: true
    });
}

function validateString(target: any, propertyKey: string) {
    let value: any;
    const originalDescriptor = Object.getOwnPropertyDescriptor(target, propertyKey);

    const setter = function (newValue: any) {
        if (typeof newValue!=='string') {
            throw new Error(`${propertyKey} must be a string`);
        }
        value = newValue;
    };

    if (originalDescriptor) {
        if (typeof originalDescriptor.set === 'function') {
            setter = originalDescriptor.set;
        }
    }

    Object.defineProperty(target, propertyKey, {
        set: setter,
        enumerable: true,
        configurable: true
    });
}

class Message {
    @log
    @validateString
    content: string;

    constructor(content: string) {
        this.content = content;
    }
}

try {
    const message = new Message('Hello');
    console.log(message.content);
    message.content = 'World';
    message.content = 123;
} catch (error) {
    console.error(error.message);
}

在上述代码中,Message类的content属性同时应用了logvalidateString装饰器。validateString装饰器先对属性值进行验证,确保其为字符串类型,然后log装饰器对属性的获取和设置进行监听。

  1. 深度分析 当多个装饰器应用于一个属性时,它们的执行顺序是从下往上(或者说从最靠近属性声明的装饰器开始依次执行)。在这个例子中,先执行validateString装饰器的逻辑,再执行log装饰器的逻辑。这种组合使用方式使得我们可以将不同功能的装饰器灵活搭配,以满足复杂的业务需求。

与类装饰器和方法装饰器的配合

  1. 类装饰器与属性装饰器的配合 类装饰器可以用来修改整个类的行为,而属性装饰器专注于类的属性。例如,类装饰器可以为类添加一些全局的功能,而属性装饰器可以对类的特定属性进行处理。
function classLogger(target: Function) {
    return class extends target {
        constructor(...args: any[]) {
            super(...args);
            console.log('Class instantiated');
        }
    };
}

function propertyValidator(target: any, propertyKey: string) {
    let value: any;
    const originalDescriptor = Object.getOwnPropertyDescriptor(target, propertyKey);

    const setter = function (newValue: any) {
        if (typeof newValue!== 'number') {
            throw new Error(`${propertyKey} must be a number`);
        }
        value = newValue;
    };

    if (originalDescriptor) {
        if (typeof originalDescriptor.set === 'function') {
            setter = originalDescriptor.set;
        }
    }

    Object.defineProperty(target, propertyKey, {
        set: setter,
        enumerable: true,
        configurable: true
    });
}

@classLogger
class Counter {
    @propertyValidator
    count: number;

    constructor(count: number) {
        this.count = count;
    }
}

try {
    const counter = new Counter(10);
    console.log(counter.count);
    counter.count = 20;
    counter.count = 'not a number';
} catch (error) {
    console.error(error.message);
}

在这个例子中,classLogger类装饰器在类实例化时打印一条日志,而propertyValidator属性装饰器对count属性进行验证。

  1. 方法装饰器与属性装饰器的配合 方法装饰器主要用于修改类方法的行为,而属性装饰器关注属性。它们可以配合使用,例如,方法可能依赖于经过验证和监听的属性。
function logProperty(target: any, propertyKey: string) {
    let value: any;
    const originalDescriptor = Object.getOwnPropertyDescriptor(target, propertyKey);

    const getter = function () {
        console.log(`Getting ${propertyKey}`);
        return value;
    };

    const setter = function (newValue: any) {
        console.log(`Setting ${propertyKey} to ${newValue}`);
        value = newValue;
    };

    if (originalDescriptor) {
        if (typeof originalDescriptor.get === 'function') {
            getter = originalDescriptor.get;
        }
        if (typeof originalDescriptor.set === 'function') {
            setter = originalDescriptor.set;
        }
    }

    Object.defineProperty(target, propertyKey, {
        get: getter,
        set: setter,
        enumerable: true,
        configurable: true
    });
}

function logMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        console.log(`Calling method ${propertyKey}`);
        const result = originalMethod.apply(this, args);
        console.log(`Method ${propertyKey} returned ${result}`);
        return result;
    };
    return descriptor;
}

class MathOperation {
    @logProperty
    base: number;

    constructor(base: number) {
        this.base = base;
    }

    @logMethod
    multiply(factor: number) {
        return this.base * factor;
    }
}

const operation = new MathOperation(5);
console.log(operation.multiply(3));

在这个例子中,logProperty属性装饰器对base属性进行监听,logMethod方法装饰器对multiply方法进行监听,当调用multiply方法时,会打印方法调用和返回的相关信息,而multiply方法依赖于经过监听的base属性。

在实际项目中的应用场景

  1. 数据模型验证 在前端开发中,数据模型通常需要进行验证。例如,在一个用户注册表单中,用户输入的年龄、邮箱等信息需要进行格式和类型的验证。我们可以使用属性装饰器来对数据模型类的属性进行验证。
function validateEmail(target: any, propertyKey: string) {
    let value: any;
    const originalDescriptor = Object.getOwnPropertyDescriptor(target, propertyKey);

    const setter = function (newValue: any) {
        const emailRegex = /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/;
        if (!emailRegex.test(newValue)) {
            throw new Error(`${propertyKey} must be a valid email`);
        }
        value = newValue;
    };

    if (originalDescriptor) {
        if (typeof originalDescriptor.set === 'function') {
            setter = originalDescriptor.set;
        }
    }

    Object.defineProperty(target, propertyKey, {
        set: setter,
        enumerable: true,
        configurable: true
    });
}

class UserModel {
    @validateEmail
    email: string;

    constructor(email: string) {
        this.email = email;
    }
}

try {
    const user = new UserModel('valid@email.com');
    console.log(user.email);
    user.email = 'invalidemail';
} catch (error) {
    console.error(error.message);
}

在这个用户模型中,validateEmail装饰器确保email属性为有效的邮箱格式。

  1. 状态管理中的属性监听 在状态管理库(如Redux或MobX)中,属性监听可以用于跟踪状态的变化。例如,在一个购物车状态管理中,我们可以监听购物车商品数量的变化,并在数量变化时更新界面。
function cartQuantityListener(target: any, propertyKey: string) {
    let value: any;
    const originalDescriptor = Object.getOwnPropertyDescriptor(target, propertyKey);

    const getter = function () {
        return value;
    };

    const setter = function (newValue: any) {
        if (typeof newValue!== 'number' || newValue < 0) {
            throw new Error(`${propertyKey} must be a non - negative number`);
        }
        console.log(`Cart quantity changed to ${newValue}`);
        // 这里可以添加更新界面的逻辑
        value = newValue;
    };

    if (originalDescriptor) {
        if (typeof originalDescriptor.get === 'function') {
            getter = originalDescriptor.get;
        }
        if (typeof originalDescriptor.set === 'function') {
            setter = originalDescriptor.set;
        }
    }

    Object.defineProperty(target, propertyKey, {
        get: getter,
        set: setter,
        enumerable: true,
        configurable: true
    });
}

class Cart {
    @cartQuantityListener
    itemCount: number;

    constructor(itemCount: number) {
        this.itemCount = itemCount;
    }
}

const cart = new Cart(3);
cart.itemCount = 5;
cart.itemCount = -1;

在这个购物车状态管理示例中,cartQuantityListener装饰器监听itemCount属性的变化,并在变化时进行验证和打印日志,同时可以添加更新界面的逻辑。

  1. 表单输入验证与监听 在表单开发中,我们需要对用户输入进行验证,并监听输入的变化。例如,在一个密码输入框中,我们可能需要验证密码长度,并在密码变化时实时显示密码强度提示。
function passwordValidator(target: any, propertyKey: string) {
    let value: any;
    const originalDescriptor = Object.getOwnPropertyDescriptor(target, propertyKey);

    const getter = function () {
        return value;
    };

    const setter = function (newValue: any) {
        if (typeof newValue!=='string' || newValue.length < 6) {
            throw new Error(`${propertyKey} must be at least 6 characters long`);
        }
        console.log(`Password changed, length: ${newValue.length}`);
        // 这里可以添加密码强度提示的逻辑
        value = newValue;
    };

    if (originalDescriptor) {
        if (typeof originalDescriptor.get === 'function') {
            getter = originalDescriptor.get;
        }
        if (typeof originalDescriptor.set === 'function') {
            setter = originalDescriptor.set;
        }
    }

    Object.defineProperty(target, propertyKey, {
        get: getter,
        set: setter,
        enumerable: true,
        configurable: true
    });
}

class LoginForm {
    @passwordValidator
    password: string;

    constructor(password: string) {
        this.password = password;
    }
}

try {
    const form = new LoginForm('123456');
    console.log(form.password);
    form.password = '12345';
} catch (error) {
    console.error(error.message);
}

在这个登录表单示例中,passwordValidator装饰器验证密码长度,并在密码变化时打印日志,同时可以添加密码强度提示的逻辑。

注意事项

  1. 装饰器兼容性 虽然TypeScript支持装饰器,但在不同的JavaScript运行环境中,装饰器的支持情况可能有所不同。在使用装饰器时,需要确保目标运行环境支持或者使用转译工具(如Babel)将装饰器代码转换为兼容的JavaScript代码。

  2. 性能考虑 在属性装饰器中,每次获取或设置属性时都会执行装饰器中的逻辑,这可能会对性能产生一定的影响。尤其是在频繁访问属性的场景下,需要谨慎设计装饰器逻辑,避免复杂的计算或大量的日志输出。

  3. 装饰器顺序 当多个装饰器应用于一个属性时,它们的执行顺序是从下往上。在编写代码时,需要根据业务逻辑确保装饰器的顺序正确,以避免出现意外的结果。

  4. 与其他设计模式的结合 属性装饰器虽然强大,但在实际项目中,应该与其他设计模式(如单例模式、工厂模式等)结合使用,以构建更健壮和可维护的代码结构。例如,在一个大型的前端应用中,可能会使用单例模式来管理全局状态,同时使用属性装饰器对状态属性进行验证和监听。

  5. 错误处理 在属性装饰器中进行验证或其他操作时,要注意合理的错误处理。例如,在验证失败时,应该抛出有意义的错误信息,以便在调用处能够正确捕获并处理错误,避免程序出现未处理的异常导致崩溃。

通过深入理解和灵活运用TypeScript属性装饰器,我们可以在前端开发中实现更强大、优雅和可维护的代码,无论是在数据验证、状态管理还是表单处理等方面,属性装饰器都能发挥重要的作用。同时,在使用过程中要注意各种细节和潜在问题,以确保代码的质量和性能。