EventEmitter Polyfill
Implement an EventEmitter class with four methods.
on(eventName, listener) — register a listener, return this
off(eventName, listenerToRemove) — remove a specific listener, return this
emit(eventName, ...args) — call all listeners for the event, return true if any existed
once(eventName, listener) — register a listener that auto-removes itself after one call, return this
Examples
const e = new EventEmitter();
e.on('msg', m => console.log(m));
e.emit('msg', 'hello');
e.emit('msg', 'world');hello
worldconst e = new EventEmitter();
e.once('login', user => console.log('logged in:', user));
e.emit('login', 'Alice');
e.emit('login', 'Bob');logged in: AliceConstraints
- In emit(), iterate over a copy of the listener array (`.slice()`) so a listener that calls off() on itself does not break the loop
Notes
- once() stores a `wrapper` function in the listeners array — off() must remove `wrapper`, not the original listener
Hints
Editorial: EventEmitter Polyfill
Implementing EventEmitter from scratch
An EventEmitter is the pub/sub pattern: register listeners with on, remove them with off, fire them with emit, and register one-time listeners with once. This is the pattern behind Node's events module and most front-end event buses.
What the interviewer checks
- A clean data structure (object mapping event names to arrays of listeners)
- Correct removal in
off— filter by reference, not position - The
oncewrapper that unregisters itself after one fire
Implementation
class EventEmitter { constructor() { this.events = {}; } on(eventName, listener) { if (!this.events[eventName]) { this.events[eventName] = []; } this.events[eventName].push(listener); return this; // allow chaining } off(eventName, listenerToRemove) { if (!this.events[eventName]) return this; this.events[eventName] = this.events[eventName].filter( (listener) => listener !== listenerToRemove ); return this; } emit(eventName, ...args) { if (!this.events[eventName]) return false; // copy the array so a listener removing itself does not break iteration this.events[eventName].slice().forEach((listener) => listener.apply(this, args)); return true; } once(eventName, listener) { const wrapper = (...args) => { listener.apply(this, args); this.off(eventName, wrapper); }; this.on(eventName, wrapper); return this; } }
The .slice() in emit
This is the detail most candidates miss. A listener might call off on itself during emit. If you iterate over the live array while modifying it, you skip listeners or get index errors:
const handler = () => { emitter.off("data", handler); // removes itself mid-iteration console.log("fired"); }; emitter.on("data", handler); emitter.on("data", () => console.log("second")); emitter.emit("data"); // Without .slice(): "second" may be skipped // With .slice(): both fire correctly
The once wrapper
once registers a wrapper function, not the original listener. The wrapper fires the original, then calls off(eventName, wrapper) to remove itself. This is why off must reference wrapper, not listener:
once(eventName, listener) { const wrapper = (...args) => { listener.apply(this, args); this.off(eventName, wrapper); // removes wrapper, not listener }; this.on(eventName, wrapper); return this; }
If you stored listener in the events array, off(eventName, wrapper) would not find a match because wrapper !== listener.
Edge cases to mention
emiton an event with no listeners returnsfalse— useful for detecting dead eventsoffis a no-op if the event name doesn't exist- Chaining: all four methods return
this - Multiple listeners for the same event fire in registration order