You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

TypeScript装饰器绑定addEventListener时this指向异常问题

错误原因

方法装饰器的执行时机是类定义阶段,此时拿到的target参数是类的原型对象,而非类的实例对象:

  • 第一版实现中直接把方法绑定到target.constructor(也就是Canvas类本身),实例属性width是挂载在实例上的,类本身不存在这个属性,所以回调执行时读取为undefined。
  • 第二版实现中在装饰器执行时直接调用target[key]触发getter,此时getter里的this指向的是类原型,绑定的是原型对象而非实例,且addEventListener仅在类定义阶段执行一次,后续生成的类实例对应的绑定方法根本没有被注册到事件监听器中,自然无法获取到实例属性。

修复方案

核心思路:将事件绑定逻辑延迟到类实例化阶段执行,绑定到当前生成的实例上,同时记录绑定的函数引用用于后续解绑,避免内存泄漏。

修复后的装饰器代码

// 依赖reflect-metadata用于存储元数据,也可以自定义属性挂载到原型上省略该依赖
import "reflect-metadata";

const LISTENER_META_KEY = Symbol("listener_config");

export function Listener(listenerLocation: string) {
  return (target: any, key: string, descriptor: PropertyDescriptor) => {
    // 先存储事件配置到类元数据,不直接执行绑定
    const existListeners = Reflect.getMetadata(LISTENER_META_KEY, target) || [];
    existListeners.push({
      methodName: key,
      eventConfig: listenerLocation,
      originalMethod: descriptor.value
    });
    Reflect.defineMetadata(LISTENER_META_KEY, existListeners, target);
  };
}

// 提供通用基类,继承该类的子类会自动绑定所有@Listener装饰的方法
export abstract class EventBindable {
  // 存储已绑定的事件信息,用于后续销毁解绑
  #boundEvents: Array<{ target: EventTarget; event: string; handler: EventListener }> = [];

  constructor() {
    // 实例化时读取元数据,绑定事件到当前实例
    const listeners = Reflect.getMetadata(LISTENER_META_KEY, Object.getPrototypeOf(this)) || [];
    for (const { methodName, eventConfig, originalMethod } of listeners) {
      const [targetName, eventName] = eventConfig.split(":");
      let eventTarget: EventTarget;
      if (targetName === "window") eventTarget = window;
      else if (targetName === "document") eventTarget = document;
      else continue;

      // 绑定当前实例
      const boundHandler = originalMethod.bind(this) as EventListener;
      eventTarget.addEventListener(eventName, boundHandler);
      this.#boundEvents.push({ target: eventTarget, event: eventName, handler: boundHandler });
      // 把绑定后的方法挂载到实例,和原方法调用逻辑一致
      Object.defineProperty(this, methodName, {
        value: boundHandler,
        writable: true,
        configurable: true
      });
    }
  }

  // 实例销毁时主动调用该方法,解绑所有事件避免内存泄漏
  destroy() {
    for (const { target, event, handler } of this.#boundEvents) {
      target.removeEventListener(event, handler);
    }
    this.#boundEvents = [];
  }
}

调整后的Canvas类代码

export class Canvas extends EventBindable {
  constructor(
    public readonly width: number,
    public readonly height: number,
    public readonly canvas: HTMLCanvasElement | string
  ) {
    // 先执行父类构造完成事件绑定
    super();
    console.log(this.width);
    // 其余初始化逻辑
  }

  @Listener('window:resize')
  windowResize() {
    console.log('w', this.width);
  }
}

内容的提问来源于stack exchange,提问作者Get Off My Lawn

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.06 15:45:03