Skip to content

Instantly share code, notes, and snippets.

@darrentorpey
Last active September 11, 2017 16:19
Show Gist options
  • Save darrentorpey/5482962 to your computer and use it in GitHub Desktop.
Save darrentorpey/5482962 to your computer and use it in GitHub Desktop.
An underscore.js primer

An Underscore.js Primer

Written by Darren Torpey and presented to Rue La La in May 2013.

This guide was written based upon Underscore.js version 1.4.4.

What is Underscore.js? How does it help us?

From the official website:

Underscore is a utility-belt library for JavaScript that provides a lot of the functional programming support that you would expect in Prototype.js (or Ruby), but without extending any of the built-in JavaScript objects.

It effectively extends the "standard lib" for JavaScript, allowing you to easily manipulate data, query the logical type and size of objects, get the keys from objects, etc. It also allows for robust, idiomatic, functional-programming-inspired code.

Key underscore.js links

At a glance

Underscore saves us from browser-compatibility hell

A few examples...

// Want an object's keys, but not the attributes of its parent/ancestor prototypes?
Object.keys(obj) // Oops; won't work in IE < 9. Gotcha!
     _.keys(obj) // All good, all the time. +1 FTW

// For example:
var developer = { name: 'Darren', age: 31, role: 'Developer' };
_.keys(developer) // =>['name', 'age', 'role']

// _.each on an array
_.each(items, function(item) {
    item.initialize();
    item.prepareForTotalAwesomeness();
});

// _.each on an object
var buttonMap = { R: 'right', L: 'left', U: 'up', D: 'down' };
_.each(buttonMap, function(direction, button) {
  console.log('Press ' + button + ' button to move "' + direction + '"');
});
/*
    Logs:
    Press R button to move "right"
    Press L button to move "left"
    Press U button to move "up"
    Press D button to move "down"
*/

Note: Delegates to ECMAScript 5's native forEach if available.

Data manipulation functions

_.map

_.map([{ id: 1, name: 'Alex' }, { id: 2, name: 'Darren' }, { id: 3, name: 'Audrey' }], function(developer) {
    return developer.name;
}
// => ['Alex', 'Darren', 'Audrey']

_.pluck

_.pluck([{ id: 1, name: 'Alex' }, { id: 2, name: 'Darren' }, { id: 3, name: 'Audrey' }], 'name')
// => ['Alex', 'Darren', 'Audrey'];

_.reduce

_.reduce([1, 2, 3, 4], function(sum, int) {
    return sum + int;
}, 0);
// => 10

Object helper functions

_.keys

_.keys({ name: 'Darren', age: 31, role: 'Developer' })
// => ['name', 'age', 'role']

_.pick

_.pick({ name : 'Darren', age: 31, role : 'Developer' }, 'name', 'role')
// => { name : 'Darren', role : 'Developer' }

_.omit

_.omit({ name : 'Darren', age: 31, role : 'Developer' }, 'age')
// => { name : 'Darren', role : 'Developer' }

_.invert

_.invert({ right: 'R', left: 'L', up: 'U', down: 'D' })
// => { R: 'right', L: 'left', U: 'up', D: 'down' }

_.pairs

Generates an array of key/value pair arrays for each key/value pair in the object

_.pairs({ name: 'Darren', role: 'Developer', home: 'Cambridge' })
// => [['name', 'Darren'], ['role', 'Developer'], ['home', 'Cambridge']]

_.object

Generates an array of objects from the keys and values given

_.object(['Moe', 'Larry', 'Curly'], [30, 40, 50])
// => { Moe: 30, Larry: 40, Curly: 50 }
_.object([['Moe', 30], ['Larry', 40], ['Curly', 50]])
// => { Moe: 30, Larry: 40, Curly: 50 }

_.extend

_.extend({ kind: true }, { honest: true, kind: false }, { honest: false })
// => { kind: false, honest: false }
_.extend(
    { repeat: true, volume: 1 },
    { volume: 2, repeat: false },
    { volume: 4, mutable: true }
)
// => { repeat: false, volume: 4, mutable: true }

_.defaults

_.defaults({ kind: true }, { honest: true, kind: false }, { honest: false })
// => { kind: true, honest: true }
_.defaults(
    { repeat: true, volume: 1 },
    { volume: 2, repeat: false },
    { volume: 4, mutable: true }
)
// => { repeat: true, volume: 1, mutable: true }

Set logic functions

_.contains

_.contains([1, 2, 3, 4], 2); // true
_.contains([1, 2, 3, 4], 5); // false

_.all

a.k.a every

_.every([1, 2, 3, 4], function(int) { return int > 3 }); // false
_.every([1, 2, 3, 4], function(int) { return int < 5 }); // true

_.any

a.k.a some

_.any([1, 2, 3, 4], function(int) { return int > 3 }); // true
_.any([1, 2, 3, 4], function(int) { return int < 5 }); // true
_.any([1, 2, 3, 4], function(int) { return int > 4 }); // false

Note: a significant benefit of this function is that it stops checking if/when it finds one that returns true. This cab be a significant efficiency gain when you're dealing with heavy operations for each check.

Here's an example, slightly modified from our code base:

// The "select" operation here is expensive, so it's good that
//  we stop checking the other attributes as soon as one is
//  found to have an empty result set
var disableBasedOnSubsetQuery = _.any(attributes, function(attribute) {
    results = self.select(_.omit(choices, attribute));
    return 0 === results.length;
});

_.without

_.without([1, 2, 1, 0, 3, 1, 4], 0, 1)
// => [2, 3, 4]

_.union

_.union([1, 2, 3], [101, 2, 1, 10], [2, 1, 5])
// => [1, 2, 3, 101, 10, 5]

_.union([1, 2, 3], [101, 2, 1, 101, 10], [2, 5, 1, 5])
// => [1, 2, 3, 101, 10, 5]

_.intersection

_.intersection([1, 2, 3], [101, 2, 1, 10], [2, 1])
// => [1, 2]

_.difference

_.difference([1, 2, 3, 4, 5], [5, 2, 10])
// => [1, 3, 4]

_.uniq

_.uniq([1, 2, 1, 3, 1, 4])
// => [1, 2, 3, 4]

Type- and size-checking functions

_.size

_.size([]);                                 // 0
_.size({});                                 // 0
_.size({ propOne: 'one', propTwo: 'two' }); // 2
_.size([1, 2, 3]);                          // 3

_.isEmpty

_.isEmpty({})                // true
_.isEmpty([])                // true
_.isEmpty('')                // true

_.isEmpty({ key: 'value' }); // false
_.isEmpty(' ');              // false
_.isEmpty([1, 2, 3])         // false
_.isEmpty([''])              // false
_.isEmpty([{}])              // false

Other is functions, mostly self-explanatory

isElement | isArray | isObject | isArguments | isFunction | isString | isNumber | isBoolean | isDate

Careful, though:

// An array is an object, too!
_.isObject({}) // true
_.isObject([]) // true <-- don't forget this
_.isArray([])  // true
_.isArray({})  // false

Guideline: always try it out in a JS console first to know for sure how the method works! The source is easy to read, too -- especially the annotated source code.

Other really useful functions

_.range

_.range(1, 11); // => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

_.invoke

Calls the specified function on each item in the given collection

var people = [
    {
        id:         11,
        name:       'Darren',
        getTagText: function() { return this.name + ' - ' + this.id  }
    },
    {
        id:         2,
        name:       'Alex',
        getTagText: function() { return this.name + ' - ' + this.id; }
    }
];
_.invoke(people, 'getTagText');
// ['Darren - 11', 'Alex - 2']

_.identity

Useful for other methods like _.sortBy that require you to give a comparison function but where you sometimes just want elements to be compared directly to one another. See the _.sortBy example below.

_.sortBy

_.sortBy([1, 204, 3, 10, 99, 2031], _.identity)
// => [1, 3, 10, 99, 204, 2031]

_.sortBy([{ id: 2, name: 'Darren' }, { id: 1, name: 'Alex' }, { id: 3, name: 'Audrey' }], 'name')
// => [{ id: 1, name: 'Alex' }, { id: 3, name: 'Audrey' }, { id: 2, name: 'Darren' }]

_.groupBy

var widgets = [{ id: 1, tag: 'A' }, { id: 2, tag: 'B' }, { id: 3, tag: 'A' }, { id: 4, tag: 'B' }, { id: 5, tag: 'A' }];
_.groupBy(widgets, 'tag')
/*
=>
{
"A": [
    {
       "id":  1,
       "tag": "A"
    }, {
       "id":  3,
       "tag": "A"
    }, {
       "id":  5,
       "tag": "A"
    }
],
"B": [
    {
       "id":  2,
       "tag": "B"
    }, {
       "id":  4,
       "tag": "B"
    }
]
}
*/

Chaining

Often times the data manipulation operation you need to perform can be expressed as a composition of the simple, idiomatic operations that underscore's functions help you with.

In these cases, _.chain can help you write this code in an idiomatic, easy-to-read fashion.

Take this example:

// This is rather awkward and unclear...
var commaListAwk = function(array) {
    return _.uniq(
                _.flatten(array)
            )
            .join(', ');
}
commaListAwk([[1, 2], [1, 3, [5, 11]], [101, 2, 0], [6, 7]]);
// => '1, 2, 3, 5, 11, 101, 0, 6, 7'

// Much better now with chaining!
var commaListBetter = function (array) {
    return _.chain(array)
        .flatten()
        .uniq()
        .value()
        .join(', ');
};
commaListBetter([[1, 2], [1, 3, [5, 11]], [101, 2, 0], [6, 7]]);
// => '1, 2, 3, 5, 11, 101, 0, 6, 7'

// Watch how easy it is to now add one more step, where we sort the values
var commaListSorted = function (array) {
    return _.chain(array)
        .flatten()
        .uniq()
        .sortBy(_.identity)
        .value()
        .join(', ');
};
commaListSorted([[1, 2], [1, 3, [5, 11]], [101, 2, 0], [6, 7]]);
// => '0, 1, 2, 3, 5, 6, 7, 11, 101'

Thanks to @dariusk for the initial code example, which I have adapted slightly from his comment on this Gist.

Advanced underscore that you will want to use

reduce

a.k.a fold, accumulate, aggregate, compress, or inject

"...a family of higher-order functions that analyze a recursive data structure and recombine through use of a given combining operation the results of recursively processing its constituent parts, building up a return value."

Full Wikipedia entry

In other words: It turns a collection into a single aggregate object.

Example:

_.reduce(list, iterator, memo)

// Builds a lookup (dictionary) object for the given array of items based on their "id" property
var items = [{ id: 1, name: 'laptop' }, { id: 2, name: 'book' }, { id: 3, name: 'mouse' }]
_.reduce(items, function(itemLookup, item) {
    itemLookup[item.id] = item;
    return itemLookup;
}, {});
// {
//     1: { id: 1, name: 'laptop' },
//     2: { id: 2, name: 'book' },
//     3: { id: 3, name: 'mouse' }
// };

Idiomatic JavaScript with underscore

Okay:

// This is okay, but we have to read the loop very carefully to have any idea
//  of what it's doing. Plus, it's easy for side-effects to appear as a
//  result of the loop's work, especially since the loop shares scope with
//  the rest of the surrounding code
var items = [{ id: 1, name: 'laptop' }, { id: 2, name: 'book' }, { id: 3, name: 'mouse' }];
var itemLookup = {};
for (var i = 0; items.length; i++) {
    var item = items[i];
    itemLookup[item.id] = item;
}

Better:

// The usage of _.each here helps us manage scope and promotes the encapsulation of
//  the behavior being done each time as a separate method
var items = [{ id: 1, name: 'laptop' }, { id: 2, name: 'book' }, { id: 3, name: 'mouse' }];
var itemLookup = {};
_.each(items, function(item) {
    itemLookup[item.id] = item;
});

Good:

// The very use of "reduce" signals to the reader that we're taking a collection
//  and turning it into an object of the type demonstrated by the third
//  parameter passed to the function call
var items = [{ id: 1, name: 'laptop' }, { id: 2, name: 'book' }, { id: 3, name: 'mouse' }];
_.reduce(items, function(itemLookup, item) {
    itemLookup[item.id] = item;
    return itemLookup;
}, {});

A longer-form, real-worldy example

I put this in its own Gist here.

When to use underscore vs. jQuery

Generally, the guideline is this: "If it's a purely jQuery-oriented operation -- and especially if it's in a jQuery chain -- use jQuery; otherwise use underscore"

Another way of looking at it: "If it's basically just a utility/convenience function in jQuery, use the underscore equivalent instead."

Examples:

// This is a good place to use jQuery's "each" because it lets us keep chaining
// and it is a purely DOM reading and writing operation
this.find('.sometimes-cool-elements')
    .initialize()
    .each(function() {
        var personalityData = $(this).data('personality');
        if (personalityData.isCool) {
            $(this).addClass('cool');
        }
    })
    .show()

Codepens

@dariusk
Copy link

dariusk commented May 22, 2013

This is great! I think you should add _.unique to your list of set functions. In my experience that is the most commonly used set function after _.contains.

@dariusk
Copy link

dariusk commented May 22, 2013

Example:

var commaList = function (array) {
  return _.chain( array )
    .flatten()
    .uniq()
    .value()
    .toString()
    .replace(/,/g,', ');
};

Try running:

commaList([[1,2],[1,3],[2,0],[6,7]]);

@darrentorpey
Copy link
Author

Thanks, @dariusk.

I'm adapting your code and adding it to my Gist.

I modified it to use .join(', ') instead of .toString().replace(/,/g,', '); and I added a sort step for the fun of it. Here's my result:

var commaListSorted = function (array) {
    return _.chain(array)
        .flatten()
        .uniq()
        .sortBy(_.identity)
        .value()
        .join(', ');
};
commaListSorted([[1, 2], [1, 3, [5, 11]], [101, 2, 0], [6, 7]]);
// => '0, 1, 2, 3, 5, 6, 7, 11, 101'

@darrentorpey
Copy link
Author

I just updated this to include content covering _.extend, _.defaults, _.object, _.without, _.union, _.intersection, _.difference, _.uniq, _.invoke and chaining with _.chain and _.value

@darrentorpey
Copy link
Author

Just added info for _. identity, .sortBy, and.groupBy`

@darrentorpey
Copy link
Author

Just added syntax highlighting. Hope it helps!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment