DEV Community

Cover image for A JavaScript interview question asked at Google
elisabethgross for Coderbyte

Posted on

A JavaScript interview question asked at Google

Hello and welcome back to Code Review, a series of coding interview challenges and career related content released weekly exclusively on Dev.to. I’m Elisabeth and I’ve been a software engineer for about 4+ years now. I’m passionate about sharing my knowledge, and best tips and tricks when it comes to acing that interview and or just leveling up your coding skills. If you want more content and challenges like these, subscribe to the Coderbyte newsletter here. That’s it for stand up - let’s get to challenge solving!

The Challenge

Write a class, EventEmitter that has three methods: on, emit, and removeListener.

  • on("eventName", callbackFn) - a function that takes an eventName and a callbackFn, should save the callbackFn to be called when the event with eventName is emitted.
  • emit("eventName", data) - a function that takes an eventName and data object, should call the callbackFns associated with that event and pass them the data object.
  • removeListener("eventName", callbackFn) - a function that takes eventName and callbackFn, should remove that callbackFn from the event.

For example:


let superbowl = new EventEmitter()

const cheer = function (eventData) {
  console.log('RAAAAAHHHH!!!! Go ' + eventData.scoringTeam)
}

const jeer = function (eventData) {
  console.log('BOOOOOO ' + eventData.scoringTeam)
}

superbowl.on('touchdown', cheer)
superbowl.on('touchdown', jeer)

superbowl.emit('touchdown', { scoringTeam: 'Patriots' }) // Both cheer and jeer should have been called with data

superbowl.removeListener('touchdown', jeer)

superbowl.emit('touchdown', { scoringTeam: 'Seahawks' }); // Only cheer should have been called

The solution:

This is a great opportunity to use ES6 classes. In case you haven’t used them before, check out their syntax here. We can start with a basic structure for the class EventEmitter and initialize it with an object events that we will use to track our events.

class EventEmitter {
  constructor () {
    this.events = {}
  }
}

On

Next we can start working on our methods. First up is on. Here is the code for that:

on (eventName, callbackFn) {
  if (!this.events[eventName])  {
    this.events[eventName] = []
  }
  this.events[eventName].push(callbackFn)
}

Because functions are first class objects in javascript, which basically means they can be stored in a variable, an object, or an array, we can just push the callback function to an array stored at the key eventName in our events object.

Emit

Now, for our emit function.

  emit (eventName, eventData) {
    if (!this.events[eventName]) return
    this.events[eventName].forEach(fn => fn(eventData))  
  }

This solution takes advantage of what is called closure in javascript. If you are coding in Javascript in your interview, understanding closure can be vital. A closure is essentially when a function has references to its surrounding state or its lexical environment. You can also think of this as a closure allowing you access to an outer function’s scope from inside an inner function. Using global variables is a great simple example of closure.

Here’s another great example of using closure to track how many times a function was called.

function tracker (fn) {
  let numTimesCalled = 0
  return function () {
    numTimesCalled++
    console.log('I was called', numTimesCalled)
    return fn()
  }
}

function hello () {
  console.log('hello')
}

const trackedHello = tracker(hello)

The inner function returned in tracker closes over the variable numTimesCalled and maintains a reference to it for the life of the trackedHello function. Cool stuff huh??

RemoveListener

The removeListener method is probably the easiest of the three. Here is a solution -

removeListener (eventName, callbackFn) {
  const idx = this.events[eventName].indexOf(callbackFn)
  if (idx === -1) return
  this.events[eventName].splice(idx, 1)
}

And that’s the class! Pun intended :) Seeing if you can implement methods that are part of the language is a great way to practice for interviews. See you all next week!

Discussion (32)

Collapse
briancodes profile image
Brian • Edited on

That's a good question, and the solution is really clear👍

I'm not sure I'd call the emit function a closure, it's not returning a function, and it's not being run outside it's lexical scope (although the callbacks are closures themselves I guess)

Collapse
elisabethgross profile image
elisabethgross Author

Not the emit function - the event callbacks themself close over the data object and gain access to that themselves!

Collapse
Sloan, the sloth mascot
Comment deleted
Collapse
theodesp profile image
Theofanis Despoudis

That's not so different than a typical implementation:

github.com/Olical/EventEmitter/blo...

Kudos

Collapse
Sloan, the sloth mascot
Comment deleted
Collapse
elisabethgross profile image
elisabethgross Author

Regardless of whatever you two decide is a better answer, its always nice to see other ways of solving the same problem! Play nicely :)

Collapse
elisabethgross profile image
elisabethgross Author

Good point that will definitely be a bug! Nice catch!

Collapse
sqlrob profile image
Robert Myers

There's a couple of things that bother me about this solution, both of them in emit.

First, there's no error handling, so a called function could break the forEach with a throw.

The second is a little more subtle, and I'm not sure if, or how it should be fixed. The object is mutable, and one of the callbacks could change it. Is it canonical to not worry about this? How would you go about fixing it? Deep copy? Freezing? Since it's a parameter, not sure the best way.

Collapse
briancodes profile image
Brian • Edited on

Making the eventData immutable is probably not the job of the EventEmitter

The Node.js EventEmitter doesn't alter the data in its emit implementation

Collapse
sqlrob profile image
Robert Myers

That's why I don't know the solution to this. In C++, passing along const references solves it from anything but something very intentional. If it's not immutable (or rather, some callback decides to mutate it) you're going to run into all sorts of potential bugs. Things can change based on the order of the callbacks and on the implementation of the emitter. That just really grates on me and feels wrong.

I wonder if that's part of the point of the question. When I did C++ interviews, one of my standard questions was "implement memcpy". I was more interested in how they handled edge cases than the actual copying.

Collapse
jdmg94 profile image
José Muñoz

while I like the content, Google has not been a good place for developers for a while and we should stop idolizing the "anything related to google must be good" label

Collapse
elisabethgross profile image
elisabethgross Author

Couldn't agree more. Idolizing any company is a recipe for disappointment. However, they are KNOWN for the caliber of interview questions they ask so its really nice to see some real questions they've asked in the past!

Collapse
costinmanda profile image
Costin Manda • Edited on

I would say that the implementation is straight forward, but my brain focused on the events member and how it is being used. I have seen this pattern many times and I am surprised there are no common specialized classes for it.

So, first of all, without changing any types, I would optimize the on (and similarly emit) methods like this:

on (eventName, callbackFn) {
  let list = this.events[eventName];
  if (!list) {
    list = [];
    this.events[eventName] = list;
  }
  list.push(callbackFn)
}

It's a minor improvement for small numbers, but it hurts me to see repeated key access to the same reference.

Second of all, why not write first a class that we could call Lookup to handle all those string indexed lists?

class Lookup {
    constructor() {
        this._items=new Map();
    }

    add (key,obj) {
        let list = this._items.get(key);
        if (!list) {
            list=[];
            this._items.set(key,list);
        }
        list.push(obj);
    }

    remove (key, obj) {
        const list = this._items.get(key);
        if (!list) return;
        const index = list.indexOf(obj);
        if (index>=0) {
            list.splice(index,1);
        }
    }

    get(key) {
        // can return an empty array, too, but one could then try to push items into it
        return this._items.get(key) || function*(){};
    }

    clear(key) {
        if (typeof key === 'undefined') {
            this._items.clear();
            return;
        }
        this._items.delete(key);
    }
}

Now, if events is a Lookup class, the code is cleaner and safer:

on (eventName, callbackFn) {
  this.events.add(eventName, callbackFn);
}
emit (eventName, eventData) {
  for (const fn of this.events.get(eventName)) {
    fn(eventData);
  }
}
removeListener (eventName, callbackFn) {
  this.events.remove(eventName, callbackFn);
}

In fact, most of the code in your EventEmitter class is actually Lookup code. One could add to the Lookup class a forEach method and you wouldn't even need an EventEmitter class:

forEach(key,func) {
  for (const item of this.get(key)) {
    func(item);
  }
}

Also, being all modern and not ending statements in semicolon feels obnoxious to me. It's probably me being me and old, but I had to say it.

Collapse
anuraghazra profile image
Anurag Hazra

PubSub

Collapse
cmelgarejo profile image
Christian Melgarejo Bresanovich

The bases for p/s, yes

Collapse
sunitk profile image
Sunit Katkar

Very good post. Thanks.

Collapse
pavanmehta profile image
Pavan Maheshwari

how about doing a filter for removing a listener i.e filtering in objects which need not be removed.

Collapse
eavichay profile image
Avichay Eyal

Sorry to poop the party, but I hope google asks better questions in interviews for their sake.

Collapse
elisabethgross profile image
elisabethgross Author • Edited on

Anyone smell that..? ;) Sometimes you get lucky with an "easier" problem - not the worst thing in the world!

Collapse
lucasjg profile image
Lucas

Hi, Elisabeth.

I want to share my solution to your series in the Korean community.
Are there any rules I must follow?

Collapse
mateiadrielrafael profile image
Matei Adriel

I see a lot of comments related to ts, but the article dowsnt seem to use ts, did it get edited?

Collapse
greencoder profile image
Vincent Cantin

You could also have used a Set, assuming that functions are added only one.

Collapse
hannadrehman profile image
hannad rehman

Holy shit. I ask the same question in my interviews. Its create a simple EventBus with fire,listen and remove methods 😅