Code Review: Underscore

27 Jun 2011 | By Alex Young | Tags code-review functional

Code Review is a series on DailyJS where I take a look at an open source project to see how it’s built. Along the way we’ll learn patterns and techniques by JavaScript masters. If you’re looking for tips to write better apps, or just want to see how they’re structured in established projects, then this is the tutorial series for you.

Underscore provides functional programming tools for JavaScript. Let’s take a look inside to see how key functions are implemented.

About Underscore

Underscore (GitHub: documentcloud / underscore, License, npm: underscore) from DocumentCloud provides a rich set of functions that help when dealing with arrays and objects. Many of these functions are available in other languages, and some can even be found natively in JavaScript depending on the browser or interpreter.

Underscore’s API is accessed through an object rather than changing Array or Object. That means it’s easy to drop into a project without affecting existing code.

Usage

The canonical example of Underscore is the each function:

_.each([1, 2, 3], function(num) {
  alert(num);
});

The map function is similar, but allows the list to be changed:

_.map([1, 2, 3], function(num) {
  return num * 3;
});

// Returns [3, 6, 9]

I really like reduce:

_.reduce([1, 2, 3], function(memo, num) { return memo + num; }, 0);
// Returns 6

Underscore quickly became popular for client-side programming, but an increasing number of Node packages depend on it.

Structure

Like most JavaScript modules, Underscore is distributed in an anonymous wrapper:

(function() {
  // Establish the root object, `window` in the browser, or `global` on the server.
  var root = this;

  // Save the previous value of the `_` variable.
  var previousUnderscore = root._;

  // Establish the object that gets returned to break out of a loop iteration.
  var breaker = {};

  // Save bytes in the minified (but not gzipped) version:
  var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
  
  // ... snip
})();

The author (Jeremy Ashkenas) has written detailed comments through the project.

Underscore will attempt to use native JavaScript methods where available. We see this intention straight away at around line 35:

// All **ECMAScript 5** native function implementations that we hope to use
// are declared here.
var
  nativeForEach      = ArrayProto.forEach,
  nativeMap          = ArrayProto.map,
  nativeReduce       = ArrayProto.reduce,
  nativeReduceRight  = ArrayProto.reduceRight,
  nativeFilter       = ArrayProto.filter,
  nativeEvery        = ArrayProto.every,
  nativeSome         = ArrayProto.some,
  nativeIndexOf      = ArrayProto.indexOf,
  nativeLastIndexOf  = ArrayProto.lastIndexOf,
  nativeIsArray      = Array.isArray,
  nativeKeys         = Object.keys,
  nativeBind         = FuncProto.bind;

The most important function in Underscore is each, because so many other methods are built on it. Because it delegates to ECMAScript 5’s native forEach it has to be compatible. It’s also defined in the each variable, which makes using it throughout the rest of the file more straightforward (and save bytes):

var each = _.each = _.forEach = function(obj, iterator, context) {
  if (obj == null) return;
  if (nativeForEach && obj.forEach === nativeForEach) {
    obj.forEach(iterator, context);
  } else if (_.isNumber(obj.length)) {
    for (var i = 0, l = obj.length; i < l; i++) {
      if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return;
    }
  } else {
    for (var key in obj) {
      if (hasOwnProperty.call(obj, key)) {
        if (iterator.call(context, obj[key], key, obj) === breaker) return;
      }
    }
  }
};

A guard against null values is used, then nativeForEach is tested. If this isn’t available and the object looks like an array, a simple for loop is used to iterate over each value and run the callback. If the object looks like an Object, a for... in loop is used instead.

However, because the nativeForEach && obj.forEach === nativeForEach test is used, nativeForEach will only ever be used for arrays. The Mozilla Developer Network has some guidance on implementing forEach-compatible methods, in the Array forEach documentation:

if (!Array.prototype.forEach)
{
  Array.prototype.forEach = function(fun /*, thisp */)
  {
    "use strict";

    if (this === void 0 || this === null)
      throw new TypeError();

    var t = Object(this);
    var len = t.length >>> 0;
    if (typeof fun !== "function")
      throw new TypeError();

    var thisp = arguments[1];
    for (var i = 0; i < len; i++)
    {
      if (i in t)
        fun.call(thisp, t[i], i, t);
    }
  };
}

Since forEach is defined on Array.prototype, Underscore has to provide its own functionality for objects. That means this will work as expected:

_.each({ a: '1', b: '2' }, function(a, b) { console.log(a, b) });

Building on each

The map function works in a very similar way, testing for native support then calling each if required:

_.map = function(obj, iterator, context) {
  var results = [];
  if (obj == null) return results;
  if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
  each(obj, function(value, index, list) {
    results[results.length] = iterator.call(context, value, index, list);
  });
  return results;
};

If the native method can be used, the results are returned right away. Otherwise, the results are collected up and returned by using each.

The context parameter is used throughout Underscore. This is used by call to bind the callback. The first parameter of call is “thisArg”:

If thisArg is null or undefined, this will be the global object. Otherwise, this will be equal to Object(thisArg)

The MDN documentation on call has more information and examples.

Tests

The tests are written with QUnit, so they can be run in a browser very easily: Underscore tests.

Conclusion

Underscore’s source reads almost like JavaScript documentation for the methods it implements. And it’s nice to see that a library that can give so many productivity gains is actually simple and easy to follow.

Do you fancy building your own functional programming library? Start with each and see how far you can get!


blog comments powered by Disqus