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

如何为已继承其他类的类实现Web-API EventTarget功能?

给第三方类混入EventTarget功能

需求与问题

需要继承一个不可修改的第三方类(比如UniversalRouter),让子类直接具备EventTarget的事件监听、分发能力,不能用组合方式(也就是不在实例内部创建EventTarget属性)。

尝试直接合并原型的方式时,触发了错误:

Uncaught TypeError: Illegal invocation

错误代码示例:

class Router extends UniversalRouter { 
  willNavigate(location) {
    const cancelled = this.dispatchEvent(new Event('navigate', { cancellable: true }));
    if(cancelled === false) {
      this.navigate(location);
    }
  }
}
Object.assign(Router.prototype, EventTarget.prototype);

想通过Mixin模式实现,但不知道怎么正确混入EventTarget:

const eventTargetMixin = (superclass) => class extends superclass {
  // 如何混入EventTarget?
}

解决方案

直接复制EventTarget.prototype的方法会报错,是因为这些内置方法依赖实例的内部[[EventTarget]]槽位——只有EventTarget原生实例才拥有这个槽。要让子类实例同时具备父类特性和EventTarget的事件能力,得在Mixin里正确初始化EventTarget的内部状态。

方法1:用Reflect.construct混合构造

这种方式利用Reflect.construct在子类构造时初始化EventTarget的内部槽,完全复用原生EventTarget的逻辑:

const eventTargetMixin = (superclass) => class extends superclass {
  constructor(...args) {
    // 先执行父类构造
    super(...args);
    // 初始化EventTarget的内部槽位
    Reflect.construct(EventTarget, [], this.constructor);
  }
};

// 用Mixin创建Router类
class Router extends eventTargetMixin(UniversalRouter) {
  willNavigate(location) {
    const event = new Event('navigate', { cancelable: true });
    const cancelled = this.dispatchEvent(event);
    if (!cancelled) {
      this.navigate(location);
    }
  }
}

方法2:手动模拟EventTarget逻辑

如果环境不支持Reflect.construct,可以手动实现EventTarget的核心方法,绕开原生槽位的限制:

const eventTargetMixin = (superclass) => class extends superclass {
  constructor(...args) {
    super(...args);
    // 初始化事件监听存储
    this._eventListeners = new Map();
  }

  addEventListener(type, listener, options = {}) {
    if (!this._eventListeners.has(type)) {
      this._eventListeners.set(type, []);
    }
    this._eventListeners.get(type).push({ listener, options });
  }

  removeEventListener(type, listener) {
    if (!this._eventListeners.has(type)) return;
    const listeners = this._eventListeners.get(type);
    const targetIndex = listeners.findIndex(item => item.listener === listener);
    if (targetIndex !== -1) {
      listeners.splice(targetIndex, 1);
    }
  }

  dispatchEvent(event) {
    if (!this._eventListeners.has(event.type)) return true;
    // 复制一份监听列表,避免遍历中删除导致的问题
    const listeners = [...this._eventListeners.get(event.type)];
    for (const { listener, options } of listeners) {
      listener.call(this, event);
      // 处理once选项
      if (options.once) {
        this.removeEventListener(event.type, listener);
      }
    }
    return !event.defaultPrevented;
  }
};

// 用Mixin创建Router类
class Router extends eventTargetMixin(UniversalRouter) {
  willNavigate(location) {
    const event = new Event('navigate', { cancelable: true });
    const cancelled = this.dispatchEvent(event);
    if (!cancelled) {
      this.navigate(location);
    }
  }
}

问题根源

EventTarget的原生方法(比如dispatchEvent)在调用时会检查this是否是合法的EventTarget实例(即拥有[[EventTarget]]内部槽)。直接把这些方法复制到其他类的原型上,调用时this是子类实例,不满足检查条件,就会抛出Illegal invocation错误。

内容的提问来源于stack exchange,提问作者Joe Jankowiak

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 04:45:29