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

JS实现装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 透明缓存
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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 传递多个参数
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)
1
2
3
4
5
6
7
// 延时装饰器
function delayDecorator(fn, ms){
return function (...args) {
console.log(`我是装饰后的函数,延时了${ms / 1000}秒`);
setTimeout(() => fn.apply(this, args), ms)
}
}
1
2
3
4
5
6
7
8
// 防抖装饰器
function debounceDecorator(fn, ms){
let timeout;
return function (...args) {
clearTimeout(timeout)
timeout = setTimeout(() => fn.apply(this, args), ms)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 节流装饰器
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 装饰器。

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

类装饰器

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

1
2
3
4
5
6
7
8
9
10
11
12
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
1
2
3
4
5
6
7
8
9
10
11
12
13
// 使用高阶函数进行传参
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

属性装饰器

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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

方法装饰器

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

普通方法装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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'));

访问器装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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

参数装饰器

暂时还未支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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'));