我经常遇到这样的问题:想要从库中扩展一个类(我不控制的类),但又想让该类具有 EventTarget/EventEmitter 的功能。
class Router extends UniversalRouter {
...
// Add functionality of EventTarget
}
我还想让这个类成为一个EventTarget,这样它就可以调度事件并监听事件。它是 EventTarget 的实例并不重要,重要的是它的功能可以直接在对象上调用。
我尝试合并原型,虽然这确实复制了原型函数,但在尝试添加事件侦听器时,我收到错误:
未捕获的类型错误:非法调用
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 模式,但我不知道如何使用它来扩展现有的类:
const eventTargetMixin = (superclass) => class extends superclass {
// How to mixin EventTarget?
}
我不希望建立一个 HAS-A 关系,在这种关系中我创建一个新的 EventTarget 作为对象内的属性:
class Router extends UniversalRouter {
constructor() {
this.events = new EventTarget();
}
}
const eventTargetMixin = superclass => class extends superclass { // How to mixin EventTarget? }
...不是混合模式,它是纯继承(可能是名称“动态子类”或“动态子类型”)。由于这一点和 JavaScript 只实现了单一继承,这种所谓的并广泛推广的“mixin”模式毫不奇怪地在OP描述的场景中失败了。
因此,由于 OP 不想依赖聚合(不想......
this.events = new EventTarget();
),必须想出一个真正的 mixin,以确保任何 OP 自定义的真正的 Web-API EventTarget
行为路由器实例。
但是第一个可能会查看OP已更改的代码,该代码已经实现了代理化
EventTarget
行为...
class UniversalRouter {
navigate(...args) {
console.log('navigate ...', { reference: this, args });
}
}
class ObservableRouter extends UniversalRouter {
// the proxy.
#eventTarget = new EventTarget;
constructor() {
// inheritance ... `UniversalRouter` super call.
super();
}
willNavigate(location) {
const canceled = this.dispatchEvent(
new Event('navigate', { cancelable: true })
);
if (canceled === false) {
this.navigate(location);
}
}
// the forwarding behavior.
removeEventListener(...args) {
return this.#eventTarget.removeEventListener(...args);
}
addEventListener(...args) {
return this.#eventTarget.addEventListener(...args);
}
dispatchEvent(...args) {
return this.#eventTarget.dispatchEvent(...args);
}
};
const router = new ObservableRouter;
router.addEventListener('navigate', evt => {
evt.preventDefault();
const { type, cancelable, target } = evt;
console.log({ type, cancelable, target });
});
router.willNavigate('somewhere');
.as-console-wrapper { min-height: 100%!important; top: 0; }
...它适用于 private 字段
#eventTarget
,其中后者是一个真正的 EventTarget
实例,可以通过人们期望事件目标具有的转发原型方法进行访问。
虽然上述实现按预期工作,但一旦开始遇到与OP解释的场景类似的场景,人们就会发现自己想要将基于代理的转发抽象出来......
我经常遇到这样的问题:想要从库中扩展一个类(我不控制的类),但又想让该类具有 EventTarget/EventEmitter 的功能。
我还想让这个类成为一个EventTarget,这样它就可以调度事件并监听事件。它是否是 EventTarget 的实例并不重要,重要的是它的功能可以直接在对象上调用。
由于函数(箭头函数除外)能够访问
this
上下文,因此可以将转发代理功能实现为我个人喜欢称为 基于函数的混合。
此外,尽管这样的 实现可以用作构造函数,但它不是 并且也不鼓励这样使用。相反,它总是必须应用于像这样的任何对象...
withProxyfiedWebApiEventTarget.call(anyObject)
...其中anyObject
随后具有事件目标的所有方法,例如dispatchEvent
,addEventListener
和removeEventListener
。
// function-based `this`-context aware mixin
// which implements a forwarding proxy for a
// real Web-API EventTarget behavior/experience.
function withProxyfiedWebApiEventTarget() {
const observable = this;
// the proxy.
const eventTarget = new EventTarget;
// the forwarding behavior.
function removeEventListener(...args) {
return eventTarget.removeEventListener(...args);
}
function addEventListener(...args) {
return eventTarget.addEventListener(...args);
}
function dispatchEvent(...args) {
return eventTarget.dispatchEvent(...args);
}
// apply behavior to the mixin's observable `this`.
Object.defineProperties(observable, {
removeEventListener: {
value: removeEventListener,
},
addEventListener: {
value: addEventListener,
},
dispatchEvent: {
value: dispatchEvent,
},
});
// return observable target/type.
return observable
}
class UniversalRouter {
navigate(...args) {
console.log('navigate ...', { reference: this, args });
}
}
class ObservableRouter extends UniversalRouter {
constructor() {
// inheritance ... `UniversalRouter` super call.
super();
// mixin ... apply the function based
// proxyfied `EventTarget` behavior.
withProxyfiedWebApiEventTarget.call(this);
}
willNavigate(location) {
const canceled = this.dispatchEvent(
new Event('navigate', { cancelable: true })
);
if (canceled === false) {
this.navigate(location);
}
}
};
const router = new ObservableRouter;
router.addEventListener('navigate', evt => {
evt.preventDefault();
const { type, cancelable, target } = evt;
console.log({ type, cancelable, target });
});
router.willNavigate('somewhere');
.as-console-wrapper { min-height: 100%!important; top: 0; }
这个答案与其他针对可观察或事件目标行为的问题密切相关。因此,除了OP提出的用例/场景之外的其他用例/场景将特此链接到...
扩展 Web-API
EventTarget
为 ES/JS 对象类型实现自己的/自定义的事件调度系统
编辑 ...进一步推动上述
EventTarget
特定代理转发器/转发混合的方法/模式,还有另一种实现,通常从传递的类构造函数创建此类混合...
const withProxyfiedWebApiEventTarget =
createProxyfiedForwarderMixinFromClass(
EventTarget, 'removeEventListener', 'addEventListener', 'dispatchEvent'
//EventTarget, ['removeEventListener', 'addEventListener', 'dispatchEvent']
);
class UniversalRouter {
navigate(...args) {
console.log('navigate ...', { reference: this, args });
}
}
class ObservableRouter extends UniversalRouter {
constructor() {
// inheritance ... `UniversalRouter` super call.
super();
// mixin ... apply the function based
// proxyfied `EventTarget` behavior.
withProxyfiedWebApiEventTarget.call(this);
}
willNavigate(location) {
const canceled = this.dispatchEvent(
new Event('navigate', { cancelable: true })
);
if (canceled === false) {
this.navigate(location);
}
}
};
const router = new ObservableRouter;
router.addEventListener('navigate', evt => {
evt.preventDefault();
const { type, cancelable, target } = evt;
console.log({ type, cancelable, target });
});
router.willNavigate('somewhere');
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
function isFunction(value) {
return (
'function' === typeof value &&
'function' === typeof value.call &&
'function' === typeof value.apply
);
}
function isClass(value) {
let result = (
isFunction(value) &&
(/class(\s+[^{]+)?\s*{/).test(
Function.prototype.toString.call(value)
)
);
if (!result) {
// - e.g. as for `EventTarget` where
// Function.prototype.toString.call(EventTarget)
// returns ... 'function EventTarget() { [native code] }'.
try { value(); } catch({ message }) {
result = (/construct/).test(message);
}
}
return result;
}
function createProxyfiedForwarderMixinFromClass(
classConstructor, ...methodNames
) {
// guards.
if (!isClass(classConstructor)) {
throw new TypeError(
'The 1st arguments needs to be a class constructor.'
);
}
methodNames = methodNames
.flat()
.filter(value => ('string' === typeof value));
if (methodNames.length === 0) {
throw new ReferenceError(
'Not even a single to be forwarded method name got provided with the rest parameter.'
);
}
// mixin implementation which gets created/applied dynamically.
function withProxyfiedForwarderMixin(...args) {
const mixIntoThisType = this;
const forwarderTarget = new classConstructor(...args) ?? {};
const proxyDescriptor = methodNames
.reduce((descriptor, methodName) =>
Object.assign(descriptor, {
[ methodName ]: {
value: (...args) =>
forwarderTarget[methodName]?.(...args),
},
}), {}
);
Object.defineProperties(mixIntoThisType, proxyDescriptor);
return mixIntoThisType;
}
return withProxyfiedForwarderMixin;
}
</script>