概念
装修器是一个在代码运行时动态增加功用的方式,它能够用来修正类或函数的行为,通常用于:
- 扩展已有的类或函数功用;
- 修正类或函数的特点;
- 将类或函数转化为不同的方式,例如,将类转化为单例形式。
在 JavaScript 中,装修器的完成有很多种方式,其间比较常用的方式是运用装修器函数和类装修器。
运用装修器函数
能够运用一个装修器函数来修正类或函数的行为,装修器函数接纳传入的类或函数作为参数,并将修正后的类或函数返回。例如,下面的比如演示了怎么运用装修器函数来给一个类增加 log 函数:
function addLogFunction(cls) {
cls.prototype.log = function(msg) {
console.log(`[${new Date().toISOString()}] ${msg}`);
};
return cls;
}
@addLogFunction
class MyClass {
constructor() {}
}
const myObj = new MyClass();
myObj.log('hello');
在这个比如中,addLogFunction
函数接纳一个类作为参数,在该函数中将类的原型(prototype
)目标上增加一个 log
办法。然后返回修正后的类。在声明 MyClass 时运用了装修器函数 @addLogFunction
,相当于履行 MyClass = addLogFunction(MyClass)
。当实例化 MyClass 的目标之后,调用 myObj.log('hello')
能够输出 log 信息。
运用类装修器
类装修器是一个润饰类的类,它能够修正类的行为、静态特点、原型特点等。一个类装修器能够接纳三个参数:
- 构造函数;
- 类的称号;
- 类的描绘目标。
下面是一个比如,运用类装修器为类增加一个静态特点:
function addVersion(cls) {
cls.version = '1.0';
return cls;
}
@addVersion
class MyClass {}
console.log(MyClass.version); // 输出 1.0
在这个比如中,addVersion
类装修器接纳一个构造函数作为参数,它在该构造函数上增加了一个静态特点 version,并将修正后的构造函数返回。在声明 MyClass 时运用了装修器函数 @addVersion
,相当于履行 MyClass = addVersion(MyClass)
。这样,就能够通过调用 MyClass.version
拜访静态特点 version。
常见的装修器应用
下面是一些常见的运用装修器的场景:
路由恳求办法装修器
function routeMethod(method) {
return function(target, key, descriptor) {
target.routes = target.routes || {};
target.routes[key] = method;
return descriptor;
};
}
class UserController {
@routeMethod('GET')
getUser(id) {
// ...
}
@routeMethod('DELETE')
deleteUser(id) {
// ...
}
}
console.log(UserController.routes);
// 输出 {getUser: "GET", deleteUser: "DELETE"}
这个比如中,运用了 routeMethod
装修器润饰了 getUser
和 deleteUser
函数,给同一个类中的两个办法增加了路由恳求办法类型。
单例形式装修器
function singleton(cls) {
let instance;
return function() {
if (!instance) {
instance = new cls(...arguments);
}
return instance;
};
}
@singleton
class MyClass {
constructor(val) {
this.val = val;
}
}
const a = new MyClass(1);
const b = new MyClass(2);
console.log(a === b); // 输出 true
这个比如中,运用了 singleton
装修器润饰了 MyClass
类,使得该类实例化后一直返回同一个实例,从而完成了单例形式。
主动绑定 this 装修器
function autobind(_, _2, descriptor) {
const { value: fn, configurable, enumerable } = descriptor;
return {
configurable,
enumerable,
get() {
const boundFn = fn.bind(this);
Object.defineProperty(this, key, {
value: boundFn,
configurable: true,
writable: true,
});
return boundFn;
},
};
}
class MyComponent {
constructor(props) {
this.props = props;
}
@autobind
handleClick() {
console.log(this.props);
}
}
这个比如中,运用了 autobind
装修器润饰了 handleClick
函数,使得该函数在被调用时主动绑定 this,并返回一个新的函数。这样,在实例化 MyComponent
后,调用 this.handleClick()
函数时,不需要再手动绑定 this。
日志记载
装修器能够用于记载日志,包括打印函数调用,函数履行时间等信息。
function log(target, name, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args) {
console.log(`Function ${name} called with ${args}`);
const start = performance.now();
const result = originalMethod.apply(this, args);
const duration = performance.now() - start;
console.log(`Function ${name} completed in ${duration}ms`);
return result;
};
return descriptor
}
class MyClass {
@log
myMethod(arg1, arg2) {
return arg1 + arg2;
}
}
const obj = new MyClass();
obj.myMethod(1, 2); // Output:
// Function myMethod called with 1,2
// Function myMethod completed in 0.013614237010165215ms
认证鉴权
装修器还能够用于查看用户的认证状况和权限,以防止未授权的用户拜访敏感数据或定期履行操作。
function authorization(target, name, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args) {
if (!this.isAuthenticated()) {
console.error('Access denied! Not authenticated');
return;
}
if (!this.hasAccessTo(name)) {
console.error(`Access denied! User does not have permission to ${name}`);
return;
}
return originalMethod.apply(this, args);
};
return descriptor;
}
class MyApi {
isAuthenticated() {
// perform authentication check
return true;
}
hasAccessTo(endpoint) {
// perform authorization check
return true;
}
@authorization
getUsers() {
// return users data
}
@authorization
deleteUser(id) {
// delete user with id
}
}
缓存
装修器还能够用于缓存函数的履行成果,以防止重复核算。
function memoize(target, name, descriptor) {
const originalMethod = descriptor.value;
const cache = new Map();
descriptor.value = function (...args) {
const cacheKey = args.toString();
if (cache.has(cacheKey)) {
console.log(`cache hit: ${cacheKey}`);
return cache.get(cacheKey);
}
const result = originalMethod.apply(this, args);
console.log(`cache miss: ${cacheKey}`);
cache.set(cacheKey, result);
return result;
};
return descriptor;
}
class MyMath {
@memoize
calculate(num) {
console.log('calculate called');
return num * 2;
}
}
const math = new MyMath();
console.log(math.calculate(10)); // Output:
// calculate called
// cache miss: 10
// 20
console.log(math.calculate(10)); // Output:
// cache hit: 10
// 20
面向切面编程
装修器能够用于完成面向切面编程,即在不修正原始代码的情况下,在运行时增加功用。
function validate(target, name, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args) {
const isValid = args.every(arg => typeof arg === 'string' && arg.length > 0);
if (!isValid) {
console.error('Invalid arguments');
return;
}
return originalMethod.apply(this, args);
};
return descriptor;
}
class MyForm {
@validate
submit(name, email, message) {
// submit the form
}
}
const form = new MyForm();
form.submit('', 'john@example.com', 'Hello world'); // Output: Invalid arguments
可逆装修器
装修器还能够应用在可逆的场景中,例如能够增加一个可逆的装修器来修正函数行为。
function reverse(target, name, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args) {
args.reverse();
return originalMethod.apply(this, args);
};
return descriptor;
}
class MyMath {
@reverse
calculate(num1, num2) {
return num1 + num2;
}
}
const math = new MyMath();
console.log(math.calculate(1, 2)); // Output: 3
console.log(math.calculate.reversed(1, 2)); // Output: 3
主动类型查看
装修器能够应用在主动类型查看上,例如能够增加一个装修器来确保函数参数的类型是正确的。
function checkType(expectedType) {
return function(target, name, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args) {
const invalidArgs = args.filter(arg => typeof arg !== expectedType);
if (invalidArgs.length > 0) {
console.error(`Invalid arguments: ${invalidArgs}`);
return;
}
return originalMethod.apply(this, args);
};
return descriptor;
}
}
class MyMath {
@checkType('number')
add(num1, num2) {
return num1 + num2;
}
}
const math = new MyMath();
math.add(1, '2'); // Output: Invalid arguments: 2