学完这篇你能得到:一套能直接抄进项目里的 JavaScript 装饰器写法,用在日志、耗时统计、缓存、重试、权限校验这些重复逻辑上,不改动原函数一行代码。
第一步:先搞清楚装饰器到底在做什么
装饰器模式的本质就一句话:接收一个函数,返回一个新函数,新函数在调用原函数前后插入额外逻辑。
function withLog(fn) {
return function (...args) {
console.log(`调用 ${fn.name},参数:`, args);
const result = fn.apply(this, args);
console.log('返回:', result);
return result;
};
}
function add(a, b) { return a + b; }
const loggedAdd = withLog(add);
loggedAdd(1, 2); // 打印两行日志,返回 3
原函数 add 完全没动,这就是“优雅”的来源:增强逻辑和业务逻辑分离,谁都能拆、能复用。
第二步:写成装饰器工厂,让装饰器能带参数
固定参数的装饰器用处有限。改成“先传配置、再传函数”的两层结构:
function withTiming(label) {
return function (fn) {
return function (...args) {
const start = performance.now();
try {
return fn.apply(this, args);
} finally {
console.log(`${label || fn.name} 耗时 ${(performance.now() - start).toFixed(2)}ms`);
}
};
};
}
const fastQuery = withTiming('查询用户')(queryUser);
注意:一定要用
fn.apply(this, args)而不是fn(...args)。否则方法被装饰后this会丢,类方法里访问this.xxx直接报 undefined。
第三步:处理异步函数
上面那版对 async 函数计时是错的——fn 立刻返回 Promise,finally 马上执行,测到的是“创建 Promise 的耗时”。
function withTiming(label) {
return function (fn) {
return async function (...args) {
const start = performance.now();
try {
return await fn.apply(this, args);
} finally {
console.log(`${label || fn.name} 耗时 ${(performance.now() - start).toFixed(2)}ms`);
}
};
};
}
注意:加了
async后,原本同步的返回值会变成 Promise。同步函数请用同步版装饰器,别混用。
第四步:几个高频实用装饰器
缓存(memoize)
function memoize(fn, resolver = (...args) => JSON.stringify(args)) {
const cache = new Map();
return function (...args) {
const key = resolver(...args);
if (cache.has(key)) return cache.get(key);
const value = fn.apply(this, args);
cache.set(key, value);
return value;
};
}
失败重试
function withRetry(times = 3, baseDelay = 300) {
return function (fn) {
return async function (...args) {
let lastErr;
for (let i = 0; i < times; i++) {
try {
return await fn.apply(this, args);
} catch (err) {
lastErr = err;
await new Promise(r => setTimeout(r, baseDelay * 2 ** i)); // 指数退避
}
}
throw lastErr;
};
};
}
权限校验
function requireRole(role) {
return function (fn) {
return function (...args) {
if (!currentUser.roles.includes(role)) {
throw new Error(`需要 ${role} 权限`);
}
return fn.apply(this, args);
};
};
}
注意:
memoize缓存的如果是 Promise,一次失败会被永久缓存住。要么只缓存成功结果,要么加过期时间;参数里含对象时,JSON.stringify的键顺序不同会被当成两个不同缓存。
第五步:组合多个装饰器
const fetchUser = requireRole('admin')(withRetry(3)(memoize(withTiming('fetchUser')(queryUser))));
这写法没法看。抽一个 compose:
const compose = (...decorators) => (fn) =>
decorators.reduceRight((acc, dec) => dec(acc), fn);
const fetchUser = compose(
requireRole('admin'),
withRetry(3),
memoize,
withTiming('fetchUser')
)(queryUser);
执行顺序是从下往上:请求 → 计时 → 缓存 → 重试 → 权限。想清楚每一层该在外面还是里面,比如权限校验必须在缓存外层,否则缓存命中就绕过了鉴权。
第六步:类方法装饰器语法 @
ECMAScript 装饰器提案已到 Stage 3,但浏览器和 Node 目前都没有原生支持,要跑得靠 TypeScript 或 Babel。
TypeScript 里打开 tsconfig.json:
{ "compilerOptions": { "experimentalDecorators": true } }
然后就能写:
class UserService {
@withTiming('getUser')
@memoize
async getUser(id) { /* ... */ }
}
注意:
experimentalDecorators是旧的 legacy 提案,和 2023 年定稿的 Stage 3 语义不兼容——新提案的装饰器接收(value, context)两个参数,返回新值取代直接改原型。团队里用哪套要统一,别一半 legacy 一半 stage 3。如果不想引编译器,就老老实实用第五步的函数组合写法,效果完全一样。
注意:装饰后的函数会丢
name和length。调试时堆栈里全是匿名函数很难受,可以在包装里补一句Object.defineProperty(wrapper, 'name', { value: fn.name }),日志和错误上报会清晰很多。
小结
- 装饰器 = 高阶函数,接收函数返回函数,原函数保持不变。
- 带参数的装饰器用两层闭包:
withXxx(config)(fn)。 - 必须用
fn.apply(this, args)保住this;异步函数要用async/await版计时。 - 常用四件套:日志计时、memoize 缓存、withRetry 重试(配指数退避)、权限校验。
- 多层组合用
reduceRight的 compose,注意权限类装饰器放在缓存外层。 @语法需要 TypeScript/Babel,且 legacy 与新提案不兼容;不引编译链就用函数组合。
正式会员
认证极客
见习用户