为什么装饰器是 JavaScript 的重要特性
装饰器(Decorators)是 TC39 正在推进的 ECMAScript 提案,提供了一种修改类、属性、方法行为的元编程方式。
TypeScript、Angular、Babel 等早已支持装饰器,但 JavaScript 原生支持还在 Stage 3。这篇文章解析装饰器的概念和用法。
一、装饰器提案状态
1.1 当前状态
装饰器提案目前处于 Stage 3(Candidate),是接近最终版本的阶段。使用时需要配置相应的打包工具或启用实验性特性。
// tsconfig.json (TypeScript)
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
1.2 Babel 配置
// babel.config.js
module.exports = {
plugins: [
['@babel/plugin-proposal-decorators', { legacy: true }]
]
};
二、类装饰器
2.1 基本类装饰器
function sealed(constructor) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class Person {
constructor(name) {
this.name = name;
}
}
// 等价于:
// class Person { ... }
// sealed(Person);
2.2 装饰器工厂
// 返回装饰器函数的函数
function version(version) {
return function(constructor) {
constructor.VERSION = version;
};
}
@version('1.0.0')
class App {
static VERSION; // 被装饰器设置
}
console.log(App.VERSION); // '1.0.0'
三、方法装饰器
3.1 方法装饰器签名
function readonly(target, propertyKey, descriptor) {
descriptor.writable = false;
return descriptor;
}
class Circle {
@readonly
pi() { return 3.14159; }
}
3.2 实用方法装饰器
// 自动绑定方法到实例
function autobind(_, __, descriptor) {
const original = descriptor.value;
return {
configurable: true,
enumerable: false,
get() {
return original.bind(this);
}
};
}
class Counter {
count = 0;
@autobind
increment() {
this.count++;
return this.count;
}
}
const counter = new Counter();
const increment = counter.increment;
console.log(increment()); // 1
四、属性装饰器
4.1 属性装饰器签名
function format(formatString) {
return function(target, propertyKey) {
// 存储元数据
Reflect.defineMetadata('format', formatString, target, propertyKey);
};
}
class User {
@format('YYYY-MM-DD')
birthday;
}
4.2 reflect-metadata
需要使用 reflect-metadata 库存储元数据:
import 'reflect-metadata';
const formatMetadataKey = Symbol('format');
function format(formatString) {
return Reflect.metadata(formatMetadataKey, formatString);
}
function getFormat(target, propertyKey) {
return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}
class User {
@format('YYYY-MM-DD')
birthday;
}
console.log(getFormat(User.prototype, 'birthday')); // 'YYYY-MM-DD'
五、访问器装饰器
5.1 getter/setter 装饰器
function logged(target, name, descriptor) {
const originalGet = descriptor.get;
const originalSet = descriptor.set;
descriptor.get = function() {
console.log(`Reading ${name}`);
return originalGet.call(this);
};
descriptor.set = function(value) {
console.log(`Setting ${name} to ${value}`);
return originalSet.call(this, value);
};
return descriptor;
}
class Point {
#x = 0;
#y = 0;
@logged
get x() { return this.#x; }
@logged
set x(value) { this.#x = value; }
}
六、装饰器组合
6.1 多个装饰器
function first() {
console.log('first(): evaluated');
return function(target, propertyKey, descriptor) {
console.log('first(): called');
};
}
function second() {
console.log('second(): evaluated');
return function(target, propertyKey, descriptor) {
console.log('second(): called');
};
}
class Example {
@first()
@second()
method() {}
}
// 输出:
// first(): evaluated
// second(): evaluated
// second(): called
// first(): called
6.2 装饰器执行顺序
装饰器从下到上(从离方法最近到最远)求值,从上到下(从最远到最近)调用。
七、实际应用场景
7.1 依赖注入
constInject.define = function(service) {
return function(target, propertyKey) {
Object.defineProperty(target, propertyKey, {
get: () => Inject.get(service)
});
};
};
class UserService {
@Inject('Database')
db;
}
7.2 验证装饰器
function validate(target, propertyKey, descriptor) {
const original = descriptor.value;
descriptor.value = function(...args) {
const rules = validationRules.get(propertyKey);
rules.forEach(rule => rule(args));
return original.apply(this, args);
};
return descriptor;
}
八、面试高频考点
考点 1:装饰器提案状态
装饰器目前处于 Stage 3,需要配置才能使用。
考点 2:装饰器类型
类装饰器、方法装饰器、属性装饰器、访问器装饰器各有不同的函数签名。
考点 3:reflect-metadata
用于在装饰器中存储和读取元数据。