如何为已继承其他类的类实现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
相关产品推荐
相关产品推荐

