跳到主要内容

装饰器与调用转发

装饰器是什么?​

  • 定义: 接收一个函数并返回「行为被改写」的新函数, 新函数称为包装器;
  • 结构: 包装器把原调用裹进额外逻辑, 主要工作仍由原函数完成;
  • 好处: 逻辑可复用、与原函数代码解耦、多个装饰器可叠加;
  • 用途与状态: 包装器靠闭包保存缓存与调用记录, 典型如单元测试的 spy (对应 sinon.spy);
function spy(func) {
function wrapper(...args) {
wrapper.calls.push(args); // 记录每次调用的参数
return func.apply(this, args); // 转发, 保留 this 与全部参数
}
wrapper.calls = [];
return wrapper;
}

为什么装饰后对象方法里的 this 会丢失?​

  • 现象: 装饰后调用报 Cannot read property 'someMethod' of undefined, 因为包装器内部写 func(x), 等于把方法摘出来裸调用, 严格模式下 this 为 undefined;
let worker = {
someMethod: () => 1,
slow(x) {
return x * this.someMethod();
},
};
// 包装器内部:
const bad = func(x); // 裸调用: this 丢失
const good = func.call(this, x); // 转交包装器收到的 this (点号前的对象)
  • 链路: worker.slow(2) 让包装器拿到 this = worker, 再由 func.call(this, 2) 转交;

如何把调用转发给不定参数函数?​

  • 调用转发: 把 this 与全部参数原样交给原函数, 外部调用无法区分包装器与原函数;
  • 取参数: 普通 function 内可用伪数组 arguments, 或用剩余参数 (...args);
  • 合键: 多参数需先压成单键, 键要涵盖全部参数, 否则不同调用会撞键;
function cachingDecorator(func, hash) {
const cache = new Map();
return function () {
const key = hash(arguments); // (3, 5) → "3,5"
if (cache.has(key)) return cache.get(key);
const result = func.call(this, ...arguments); // this 与全部参数一起转发
cache.set(key, result);
return result;
};
}
  • 等价写法: func.call(this, ...arguments) 可换成 func.apply(this, arguments);
  • 选择: call 用展开符逐个传参, 要求可迭代; apply 只接受类数组; 真数组两者皆可, 且引擎对 apply 优化更好, 通用骨架是 return func.apply(this, arguments);

透明缓存装饰器如何实现?​

  • 前提与思路: 函数是纯的 (同参数必然同结果), 首次调用算出来存进 Map, 之后同键直接读, 外部无感知;
function cachingDecorator(func) {
const cache = new Map();
return function (x) {
if (cache.has(x)) return cache.get(x); // 命中: 不再计算
const result = func(x); // 未命中: 真正执行
cache.set(x, result);
return result;
};
}
  • 键的局限: 原生 Map 只接受单个键, 多参数需自行合并成一个键 (即上面 hash(arguments) 的作用);

借用数组方法处理 arguments 的原理是什么?​

  • 问题与办法: arguments 是类数组, 没有 join 等方法, 需从数组借方法并用 call 换掉 this: [].join.call(arguments);
  • 原理: 原生 arr.join(glue) 算法只读 this.length 与 this[0..length-1], 不检查 this 是否真是数组;
function hash() {
return [].join.call(arguments);
}
hash(1, 2); // "1,2"

装饰器会丢失什么, 有什么代价?​

  • 属性丢失: 包装器是新函数, 原函数自有属性 (func.calledCount 等) 不会带到包装器上;
  • 形参个数: wrapper.length 由包装器自己的形参决定, 用 ...args 时为 0, 与 func.length 无关;
  • 额外开销: 每次调用多一层函数与闭包查表, 缓存装饰器还要计算键;
  • 取舍: 依赖函数属性或形参数量的代码不能直接换成装饰版; 但装饰器可以自己挂属性 (如 wrapper.calls、耗时统计);
  • 保住属性: 需用 Proxy 包装函数才能透明转发属性, 见 Proxy 与 Reflect;