I was in a job interview pair programming session where I was asked to implement
JavaScript’s setInterval method without using setInterval itself. I did
poorly at that interview for many reasons, including not knowing how to
implement this.
The Question
Implement the setInterval function in such a way that executing new SetInterval.prototype.start will start the function passed to it and run the function at intervals interval milliseconds until the SetInterval.prototype.clear method is called.
For example:
const runner = new SetInterval(() => console.log("hello"), 1000);
runner.start();
setTimeout(() => runner.clear(), 5000);
will log “hello” 5 times on the console/terminal at an interval of 1000ms.
I was asked to use the following snippet as a template for my implementation:
function SetInterval(fn, interval) {}
SetInterval.prototype.start = function () {};
SetInterval.prototype.clear = function () {};
This question opened me up to a lot of my flaws in understanding JavaScript.
The Solution
After reading Michael Zheng’s implementation at Implement setInterval with setTimeout, I modified his implementation to fit the interview question.
function SetInterval(fn, interval) {
this.fn = fn.bind(this);
this.interval = interval;
this.toContinue = true;
}
SetInterval.prototype.start = function () {
const wrapper = () => {
if (this.toContinue) {
this.fn();
// returning the id in case it may be needed.
// Not necessary for this implementation
return setTimeout(wrapper, this.interval);
}
return;
};
wrapper();
};
SetInterval.prototype.clear = function () {
this.toContinue = false;
};
const theFn = new SetInterval(function () {
console.log("hello");
}, 1000);
theFn.start();
setTimeout(() => {
theFn.clear();
}, 5000);
The Explanation
Interpret the SetInterval function as a JavaScript class for easier understanding. See it as
class SetInterval {
constructor(fn, interval) {
this.fn = fn.bind(this);
this.interval = interval;
this.toContinue = true;
}
}
When a new SetInterval object is created via new SetInterval(fn, interval), the function fn that will run, and the interval are both passed in.
The value of this in the function argument fn passed in, is replaced with the object
instance that will be created by the SetInterval constructor function using
bind. This is done so that any reference to this in fn will refer to the
object instance created by SetInterval. The result of executing bind is
passed to this.fn, which is fn but with its this referring to the object
created by the SetInterval constructor.
The object’s toContinue property is set to track if executing this.fn at intervals should stop or not.
How does the start method work?
start () {
const wrapper = () => {
if(this.toContinue){
this.fn()
// returning the id in case it may be needed.
// Not necessary for this implementation
return setTimeout(wrapper, this.interval);
}
return;
};
wrapper()
}
start is a method on object instances created by SetInterval. When start
is executed, a reference to wrapper is first stored in start’s local memory,
and then wrapper is executed.
When wrapper is executed, it checks if its instance’s toContinue property has a value of true. If the condition evaluates to true, this.fn is executed, else, it isn’t executed.
wrapper is set to be executed on the next this.interval milliseconds using
setTimeout(wrapper, this.interval). setTimeout(wrapper, this.interval) stores the
setTimeout function in JavaScript’s memory, and schedules wrapper to run in the next this.interval milliseconds.
How does the clear method work?
clear () {
this.toContinue = false
}
When this.clear is executed, it sets the object’s toContinue property to a false. This causes the next execution of wrapper to find out that this.toContinue is now false, and
then doesn’t execute this.fn or store another wrapper function to be executed in another this.interval milliseconds.
That is why the code snippet below:
const theFn = new SetInterval(function () {
console.log("hello");
}, 1000);
theFn.start();
setTimeout(() => {
theFn.clear();
}, 5000);
will log “hello” five times.
