UPDATE: 2015.11.28 = I have since written a new debounce/throttle library named odis.

As part of building my scrollEvents library, we needed a way to limit the amount of times scroll events fired, so my first solution was to use a very simple throttle function a la jonathansampson:

// thanks to @jonathansampson
function throttle(callback, limit) {
  // don't wait initially
  var wait = false;
  // return a throttled function
  return function() {
    // if not waiting, invoke function
    if (!wait) {
      callback.call();
      // prevent future invocations
      wait = true;
      // after a period of time allow function to be invoked again
      setTimeout(function() {
        wait = false;
      }, limit);
    }
  };
}

The problem with this throttle(), as pointed out by my friend Tarabyte, is that it will not call a function on both ends.

In other words, we needed a function that would invoke on what is sometimes referred to as the ‘trailing edge’. Ben Alman made an excellent diagram showing the difference in this blog post.

throttle diagram

Our old throttle function (by @jonathansampson) wouldn’t call the function on the ‘trailing edge’ (behaving like the no_trailing equal to true in the diagram above).

Enter underscore.js’s throttle:

// taken from underscore 1.8.3
_.throttle = function(func, wait, options) {
  var context, args, result;
  var timeout = null;
  var previous = 0;
  if (!options) options = {};
  var later = function() {
    previous = options.leading === false ? 0 : _.now();
    timeout = null;
    result = func.apply(context, args);
    if (!timeout) context = args = null;
  };
  return function() {
    var now = _.now();
    if (!previous && options.leading === false) previous = now;
    var remaining = wait - (now - previous);
    context = this;
    args = arguments;
    if (remaining <= 0 || remaining > wait) {
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      previous = now;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    } else if (!timeout && options.trailing !== false) {
      timeout = setTimeout(later, remaining);
    }
    return result;
  };
};

For anyone keeping score, I also looked at the throttle function for lodash, but that returns a debounce function, so making it modular would have required quite a bit more surgery:

// taken from lodash 3.10.1
function throttle(func, wait, options) {
  var leading = true,
    trailing = true;
  if (typeof func != 'function') {
    throw new TypeError(FUNC_ERROR_TEXT);
  }
  if (options === false) {
    leading = false;
  } else if (isObject(options)) {
    leading = 'leading' in options ? !!options.leading : leading;
    trailing = 'trailing' in options ? !!options.trailing : trailing;
  }
  return debounce(func, wait, {
    'leading': leading,
    'maxWait': +wait,
    'trailing': trailing
  });
}

Since our own function was only used internally, we could simplify a few things, and we didn’t really need the options argument, so we ended up with this for our 1.0.0 release of scrollEvents:

// slightly modified/simplified version of underscore.js's throttle (v1.8.3)
function throttle(func, wait) {
  var timeout = null,
    previous = 0,
    later = function() {
      previous = Date.now();
      timeout = null;
      func();
    };
  return function() {
    var now = Date.now();
    if (!previous) previous = now;
    var remaining = wait - (now - previous);
    if (remaining <= 0 || remaining > wait) {
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      previous = now;
      func();
    } else if (!timeout) {
      timeout = setTimeout(later, remaining);
    }
  };
}

The only other underscore function we had to implement to make this work was _.now() which is either Date.now() or new Date().getTime(). Since we aren’t trying to support ancient browsers (read as pre-IE9), we went with the simpler Date.now(). Here is underscore’s _.now():

_.now = Date.now || function() {
  return new Date().getTime();
};

Feel free to use our throttle in your own project if you find it useful, or help us improve it in the comments below!

P.S. Our code is GPLv2 licensed, and the underscore & lodash code is MIT licensed.