Debounce with me bounce with me
So got a fun interview question today. Didn’t actually end up solving it, but felt like it was a cool question that I wanted to write about. Here it is:
“Write your own debounce function”
Before I jump into the solution, if you’re unfamiliar with what debouncing is as a concept, it’s essentially a way to prevent the over-invocation of any given function (I totally made the word over-invocation up, but I think it works here).
For example, for a typeahead search, rather than making a call to get new search results upon every key stroke, debouncing is often used in order to limit the rate at which API calls are made to retrieve those results. E.g. if you type 5 letters in a matter of 2 seconds, we'd only call the API endpoint once when 2 seconds has passed from the last keystroke as opposed to five times.
So back to the question. I was given this function definition and an example of its usage as the following:
const debounce = (fn, delay) => { }; const sayHi = () => { console.log('yo!'); }; const debouncedSayHi = debounce(sayHi, 100);
So what should debounce look like? At first glance, I knew in the back of my head this was going to involve a closure somehow... The first clue was the the return value of debounce was another function, which implies that a closure exists, whether there is state held outside of the function that gets used within the returned function or not.
Before we get started it might help to know that whenever you call setTimeout(), a reference to that specific timeout is returned. You can later cancel that specific timeout using that object that was returned, i.e.
const timeout = setTimeout(someFunction, 100) clearTimeout(timeout)
someFunction will never be invoked.
So what did the solution end up looking like? Check this out:
const debounce = (fn, delay) => { let timeout; return (...args) => { setTimeout(() => { clearTimeout(timeout); timeout = setTimeout(() => { fn(...args); }, delay); }, delay) } }; const sayHi = () => { console.log('yo!'); }; const debouncedSayHi = debounce(sayHi, 100);
You'll notice that we're constantly overriding our timeout variable declared in the closure scope of the debounced function that we're wrapping. Pretending that timeout's have ids for a second, lets look at what happens when you call the debouncedSayHi function four times:
debouncedSayHi(); // => adds a timeout with id = 1 debouncedSayHi(); // => removes timeout with id = 1 and adds one with id = 2 debouncedSayHi(); // => removes timeout with id = 2 and adds one with id = 3 debouncedSayHi(); // => removes timeout with id = 3 and adds one with id = 4
The magic lies in adding and removing timeouts using the closure scope variable, timeout. Keeping track of the timeout variable in the closure scope allows us to constantly remove the last timeout that was set and effectively restart our 100ms timer every time that we invoke the function again. This is the behavior we're looking for, since debouncing a function makes it so that function is called only once X seconds after the LAST call.
This is where I got tripped up.
In my mind I was thinking that we were trying to invoke sayHi only once after 100ms from the first invocation of debouncedSayhi().
This, is actually a concept called throttling, which is effectively limiting the number of times you can invoke a function. within a time-frame.
The funny thing is that both of my friendly interviewers warned me of this misconception before we started and I STILL fell for it! Sigh...
Oh, also, there's some interesting places to improve this function. If your debounced function uses the keyword this at all, you're kinda screwed depending on where you end up calling debounce(). For example, if defined within an object:
const sayAge = () => { console.log(`I am ${this.age}); } const cooper = { age: 22 sayAge: debounce(sayAge, 100) }
when we invoke cooper.sayAge(), you'll get:
I am undefined
It's cool that Cooper is trying hard to be artsy and unique by saying he's undefined---were definitely all our own unique lil snowflakes---but this isn't exactly the behavior that we want.
This is due to the fact that using the new arrow definition syntax () => {} actually automatically binds this to the location of where the function is defined, as opposed to where it is invoked. This means two things for us:
our debounce function actually needs to use the old function definition syntax to get the desired behavior we'd want (i.e. to bind to this where debounce is invoked)
We need to make sure to grab this within debounce and manually bind it via .apply to the passed in function fn when it gets called
Check it out:
const debounce = (fn, delay) => { let timeout; return function() { setTimeout(() => { const context = this; clearTimeout(timeout); timeout = setTimeout(() => { fn.apply(context, ...args); }, delay); }, delay) } };
Now when we invoke
cooper.sayAge()
we get
I am 22
Thanks for being a sport Coop!
Anyway, I've learned a lot coming out of this interview and I hope my recap did it justice! Til next time.











