2

I am trying to make timer in javascript using a prototype. Each time a new timer is created, a object of prototype is created. There are methods to increase time and print each second. The whole code snippet is as follows:

function Timer(elem) {

  this.interval = null;
  this.currentTime = {
    sec: 0,
    min: 0,
    hr: 0
  };
  this.elem = elem;
};

Timer.prototype.start = function() {
  var self = this;
  if (!self.interval) {
    self.interval = setInterval(update, 1000);
  }

  function update() {
    incrementTime();
    render();
  }

  function render() {
    self.elem.innerText = getPrintableTime();
  }

  function incrementTime() {
    self.currentTime["min"] += Math.floor((++self.currentTime["sec"]) / 60);
    self.currentTime["hr"] += Math.floor(self.currentTime["min"] / 60);
    self.currentTime["sec"] = self.currentTime["sec"] % 60;
    self.currentTime["min"] = self.currentTime["min"] % 60;
  }

  function getPrintableTime() {
    var text = getTwoDigitNumber(self.currentTime["hr"]) + ":" + getTwoDigitNumber(self.currentTime["min"]) + ":" + getTwoDigitNumber(self.currentTime["sec"]);
    return text;
  }

  function getTwoDigitNumber(number) {
    if (number > 9) {
      return "" + number;
    } else {
      return "0" + number;
    }
  }
};
module.exports = Timer;

I have all methods in start function. The problem is that for each new object of Timer, new space for each method will be used which is very inefficient. But when I try to put methods outside of start function, they lose access to self variable. You can see that there is setInterval function used which will be calling these methods per second. I cannot use this also as this will be instance of Window in subsequent calls.

How can I solve this situation by only keeping one instance of all the interior methods?

dubs
  • 6,511
  • 4
  • 19
  • 35
Shashwat Kumar
  • 5,159
  • 2
  • 30
  • 66

3 Answers3

1

You don't need to have all methods in the start function. Yes, for each new Timer instance, new space for each function will be used, but that is necessary when you want to work with setInterval as you need a function which closes over the instance. However, you need only one such closure, the other methods can be standard prototype methods.

function getTwoDigitNumber(number) {
    return (number > 9 ? "" : "0") + number;
}

function Timer(elem) {
    this.interval = null;
    this.currentTime = {
        sec: 0,
        min: 0,
        hr: 0
    };
    this.elem = elem;
};

Timer.prototype.start = function() {
    var self = this;
    if (!this.interval) {
        this.interval = setInterval(function update() {
            self.incrementTime();
            self.render();
        }, 1000);
    }
};
Timer.prototype.render() {
    this.elem.innerText = this.getPrintableTime();
};
Timer.prototype.incrementTime = function() {
    this.currentTime.sec += 1;
    this.currentTime.min += Math.floor(this.currentTime.sec / 60);
    this.currentTime.hr += Math.floor(this.currentTime.min / 60);
    this.currentTime.sec = this.currentTime.sec % 60;
    this.currentTime.min = this.currentTime.min % 60;
};
Timer.prototype.getPrintableTime = function() {
    var text = getTwoDigitNumber(this.currentTime.hr) + ":"
             + getTwoDigitNumber(this.currentTime.min) + ":"
             + getTwoDigitNumber(self.currentTime.sec);
    return text;
};

module.exports = Timer;

Btw, regarding your incrementTime pattern, you should have a look at How to create an accurate timer in javascript?.

Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • If only `start` method needs to be "public", exposing other methods in prototype is breaking encapsulation – Stubb0rn Feb 28 '17 at 19:51
  • @Stubb0rn and doing what the OP wants to do is the same thing, breaking encapsulation.... – epascarello Feb 28 '17 at 19:53
  • @Stubb0rn If you want "private" methods, you can only use static functions that get passed the instance as an argument. Or use an underscore naming convention. All methods are public in JS, but that wasn't the question anyway - it was about putting the common functions on the prototype. – Bergi Feb 28 '17 at 19:54
  • @Bergi Is there any way to avoid making each function prototype of Timer. Though I didn't want to break encapsulation but just to avoid multiple instances of each method. How to implement other option, using static function getting instance as argument? – Shashwat Kumar Feb 28 '17 at 20:05
  • @ShashwatKumar Just like I did with `getTwoDigitNumber` - and instead of passing `number`, pass `self`. – Bergi Feb 28 '17 at 20:13
  • @Bergi I can pass `this`? But when javascript calls each function after interval, it passes `this` as Window – Shashwat Kumar Feb 28 '17 at 20:16
  • @ShashwatKumar I said `self`. Given that the "methods" aren't real methods any more (outside of the prototype) but just plain functions, they don't have a `this`. – Bergi Feb 28 '17 at 20:19
  • @Bergi thanks. I tried and it worked. I unnecessarily was getting confused. Also thanks for your incrementing suggestion. – Shashwat Kumar Feb 28 '17 at 20:20
0

You can use apply to use functions defined outside of prototype with correct this context.

function Timer(elem) {

  this.interval = null;
  this.currentTime = {
    sec: 0,
    min: 0,
    hr: 0
  };
  this.elem = elem;
};

function update() {
  incrementTime.apply(this);
  render.apply(this);
}

function render() {
  this.elem.innerText = getPrintableTime.apply(this);
}

function incrementTime() {
  this.currentTime["min"] += Math.floor((++this.currentTime["sec"]) / 60);
  this.currentTime["hr"] += Math.floor(this.currentTime["min"] / 60);
  this.currentTime["sec"] = this.currentTime["sec"] % 60;
  this.currentTime["min"] = this.currentTime["min"] % 60;
}

function getPrintableTime() {
  var text = getTwoDigitNumber(this.currentTime["hr"]) + ":" + getTwoDigitNumber(this.currentTime["min"]) + ":" + getTwoDigitNumber(this.currentTime["sec"]);
  return text;
}

function getTwoDigitNumber(number) {
  if (number > 9) {
    return "" + number;
  } else {
    return "0" + number;
  }
}

Timer.prototype.start = function() {
  var self = this;
  if (!self.interval) {
    self.interval = setInterval(function() {
      update.apply(self);
    }, 1000);
  }
};

document.addEventListener('DOMContentLoaded', function() {
  var timer = new Timer(document.getElementById('timer'));
  timer.start();
}, false);
<div id="timer"></div>
Stubb0rn
  • 1,269
  • 12
  • 17
0

If I understand correctly, you're wanting to only create one interval.

One possible solution would be to create a static method and variable to manage the setInterval. I would note that while this may be more performance friendly, the timers will always start and run on the same count...not from the moment each timer is created. (See example)

Of course, you could capture the current timestamp and calculate the elapsed time from there. But, that's another thread ;)

function Timer(elem) {
  this.interval = null;
  this.currentTime = {
    sec: 0,
    min: 0,
    hr: 0
  };
  this.elem = elem;
};

Timer.subscribe = function(timer) {
  Timer.subscribers = Timer.subscribers || [];
  if (Timer.subscribers.indexOf(timer) === -1) {
    Timer.subscribers.push(timer);
    timer.update.call(timer);
  }
  Timer.checkInterval();
};

Timer.unsubscribe = function(timer) {
  Timer.subscribers = Timer.subscribers || [];
  if (Timer.subscribers.indexOf(timer) !== -1) {
    Timer.subscribers.splice(Timer.subscribers.indexOf(timer), 1);
  }
  Timer.checkInterval();
};

Timer.checkInterval = function() {
  if (!Timer.interval && Timer.subscribers.length > 0) {
    Timer.interval = setInterval(function() {
      Timer.subscribers.forEach(function(item) {
        item.update.call(item);
      });
    }, 1000);
  } else if (Timer.interval && Timer.subscribers.length === 0) {
    clearInterval(Timer.interval);
    Timer.interval = null;
  }
};

Timer.prototype = {
  start: function() {
    Timer.subscribe(this);
  },
  
  stop: function() {
    Timer.unsubscribe(this);
  },
  
  update: function() {
    this.incrementTime();
    this.render();
  },
  
  incrementTime: function() {
    this.currentTime["min"] += Math.floor((++this.currentTime["sec"]) / 60);
    this.currentTime["hr"] += Math.floor(this.currentTime["min"] / 60);
    this.currentTime["sec"] = this.currentTime["sec"] % 60;
    this.currentTime["min"] = this.currentTime["min"] % 60;
  },
  
  render: function() {
    var self = this;

    function getPrintableTime() {
      var text = getTwoDigitNumber(self.currentTime["hr"]) + ":" + getTwoDigitNumber(self.currentTime["min"]) + ":" + getTwoDigitNumber(self.currentTime["sec"]);
      return text;
    }

    function getTwoDigitNumber(number) {
      if (number > 9) {
        return "" + number;
      } else {
        return "0" + number;
      }
    }
    
    this.elem.innerText = getPrintableTime();
  }
};

/**
 *
 */
var timers = document.getElementById('timers');

function addTimer() {
  var el = document.createElement('div');
  var tmr = document.createElement('span');
  var btn = document.createElement('button');
  var t = new Timer(tmr);
  
  btn.innerText = 'Stop';
  btn.onclick = function() {
    t.stop();
  };
  
  el.appendChild(tmr);
  el.appendChild(btn);
  timers.appendChild(el);
  
  t.start();
};
<div id="timers"></div>

<button onclick="addTimer()">Add Timer</button>
Corey
  • 5,818
  • 2
  • 24
  • 37