I wrote recently about finding a modular throttle function. I wasn’t happy using that one, primarily because it is a rewrite of the Underscore.js throttle. The Underscore project does great work, but I really wanted to understand this ‘low-level’ function. For me, doing is understanding. Furthermore, Underscore’s throttle relies on Date.now(), which gives it a kind of ‘memory’. Its behavior isn’t consistent between the first time you fire it, and firing it after waiting a while. A throttle’s throttling should always be relative.

So I have been researching and sampling various throttle functions since then. I appreciated some from a readability angle, but they had issues. They wouldn’t fire on the trailing edge. Or they modified the user’s function (and weren’t even a throttle, Zakas’ throttle is actually a debounce).

So I have been going back and forth on throttle ideas. Writing them out on my whiteboard, on notebook paper before I fall asleep, testing them in zero gravity, et cetera. I have meditated on all things throttle. Why? Because a throttle is rudimentary to so many libraries, I could not continue writing JavaScript libs until I had a throttle I could call my own. Until I had a throttle I could easily write and understand from memory. The other day I realized that the issue with Sampson’s throttle() could be solved by combining it with Zakas’ debounce(). So after weeks of trying things that didn’t work at all, I came up with this:

(function() {
  'use strict';
  var modThrottle = function(func, delay) {
    var waiting = false;
    return function() {
      if (!waiting) {
        waiting = true;
        clearTimeout(func.MODTHROTTLE___TIMEOUT___ID);
        func.MODTHROTTLE___TIMEOUT___ID = setTimeout(function() {
          func.call();
          waiting = false;
        }, delay);
      }
    };
  };
  window.modThrottle = modThrottle;
}());

What I don’t like is that it modify’s the user’s function, then I realized that could be worked around by cloning the function with bind().

(function() {
  'use strict';
  var modThrottle = function(func, delay) {
    var waiting = false,
      funcClone = func.bind();
    return function() {
      if (!waiting) {
        waiting = true;
        clearTimeout(funcClone.MODTHROTTLE___TIMEOUT___ID);
        funcClone.MODTHROTTLE___TIMEOUT___ID = setTimeout(function() {
          funcClone.call();
          waiting = false;
        }, delay);
      }
    };
  };
  window.modThrottle = modThrottle;
}());

Much better! Now we aren’t modifying the function we bring in. That was just messy. But wait, if you’re thinking, why not just use a var, you’re right on the money!

(function() {
  'use strict';
  var modThrottle = function(func, delay) {
    var waiting = false,
      funcTimeoutId;
    return function() {
      if (!waiting) {
        waiting = true;
        clearTimeout(funcTimeoutId);
        funcTimeoutId = setTimeout(function() {
          func.call();
          waiting = false;
        }, delay);
      }
    };
  };
  window.modThrottle = modThrottle;
}());

Now the code is simple, lightweight, readable, and isn’t violating any best practices. This throttle() could be used internally like so:

function modThrottle(func, delay) {
  'use strict';
  var waiting = false,
    funcTimeoutId;
  return function() {
    if (!waiting) {
      waiting = true;
      clearTimeout(funcTimeoutId);
      funcTimeoutId = setTimeout(function() {
        func.call();
        waiting = false;
      }, delay);
    }
  };
}

You can also easily reduce this to a debounce():

function modDebounce(func) {
  'use strict';
  var funcTimeoutId;
  return function() {
    clearTimeout(funcTimeoutId);
    funcTimeoutId = setTimeout(function() {
      func.call();
    }, 210);
  };
}

Now this is pretty much complete, but I think code should have a bit of attitude. So I decided to have my throttle tell a story. Enter Odis. Odis is the Latin god of modular limitation, let’s take a short look at the reusable version of Odis you can drop right inside of your library today:

You’ll notice that we have defaults for both delay arguments now (the last argument for a function should be optional), and we are only creating one global object (or none if you use this internally).

Find out more about Odis from his README.