Underscore w/D8

Todd Zebert @toddzebert

Lead Web Dev

Miles @meetmiles

v1.0.1

Underscore.js

A JS library that provides a whole mess of useful functional programming helpers without extending any built-in objects. It's the answer to the question: 'If I sit down in front of a blank HTML page, and want to start being productive immediately, what do I need?'

Underscore provides over 100 functions that support both your favorite workaday functional helpers...

What's it good for?

While jQuery helps you with the DOM, Underscore helps you with collections, arrays, object, and functions.

A number of modules use it (because they use Backbone): Contextual, Quickedit, Toolbar, CKEditor , Tour

At only 16KB minified, it will help make your code more readable. reliable, and easier to reason about.

To use, either include or make a dependency of core/underscore.

Past, Present Future

First introduced late 2009 by Jeremy Ashkenas.

The current version of 1.8.3 (Apr 2015) is included with D8.

There's still new commits but many of the core devs have moved on to Lodash - a rewritten, performant, enhanced (and at one point a drop-in replacement) - which started as alternative and has since taken over in popularity.

But don't let that stop you - it's still super useful!

Underscore and D7

Of course you can use Underscore with Drupal 7: just include underscore.js in your code.

As I mentioned, while Lodash is largely a replacement for Underscore, if you want to keep "compatible" between your D7 and D8 sites, then stick with Underscore.

The Methods

The following sections with describe the various categories of methods, briefly list them out, and delves into a few.

Many of these methods have aliases, sometimes more than one, so you may know them by other names.

Method compatibility

Since Underscore was originally released around the time of ES5, but based on ES3, many of it's functions either added methods JS didn't have, or smoothed out differences between implementations. Where reasonable replacements of these can be found, they're marked ES 5, ES 6, or ES 7.

Method Concepts

Chaining

Chaining (sometimes refered to as method cascading, or a fluent interface) allows multiple methods to be used directly without assignment to intermediate variables.

Simply, chain() wraps an array or collection, while value() unwraps.

Chaining Methods

_.chain(obj)

Returns a wrapped object. Calling methods on this object will continue to return wrapped objects until value is called.

.value()

Extracts the value of a wrapped object.

Superfluous chaining:


_.chain(quotes).where({source: 'E!'}).value();
[
  0: { source: 'E!', when: '2014', quote: 'Spetacular!' },
  1: { source: 'E!', when '2016', quote: 'Again Spectacular!' }
]
						

where() will be defined later.

Method context

A concept used across many of the methods, across categories, is support for an optional context parameter.


_.each([1, 2, 3],
  function(x) { console.log(x * this.a); },
  {a: '3'}
);
3
6
9
						

Iteratee

The iteratee is a function that gets called as a collection is iterated over; it usually gets passed three arguments:

  • if target is an array it gets: (element, index, list)
  • if it's an object, it gets: (value, key, list)

Interatee's use vary so be sure to consult the docs.

Predicates

Many functions take a predicate, which is just a function that returns true or false given it's input, and has the same function definition as iteratee's.

Collection Functions

Collections are generally defined, as far as Underscore is concerned, as:

  • arrays
  • objects
  • array-like objects*

*Array-like objects are data structures such as arguments, NodeList and similar.

The Collection Functions

Categorized by how many items of n returned:

1 item

  • reduce
  • reduceRight
  • find
  • findWhere
  • every
  • some
  • contains
  • max
  • min
  • sample
  • toArray
  • size

1 - n items

  • each
  • filter
  • where
  • reject
  • groupBy
  • countBy
  • partition

n items

  • map
  • invoke
  • pluck
  • sortBy
  • indexBy
  • shuffle

Replacement key: ES 5 ES 6 ES 7

.where()

_.where(list, properties)

Examines each collection item; returns array of all the values that contain all of the key-value pairs listed in properties.


var quotes = [
  { source: 'Hollywood Reporter', when: '2016', quote: 'Best ever!' },
  { source: 'E!', when: '2014', quote: 'Spectacular!' },
  { source: 'Entertainment Weekly', when: '2016', quote: 'Amazing!' },
  { source: 'E!', when: '2016', quote: 'Again Spectacular!' }
];

_.where(quotes, {source: 'E!'});
[
  0: { source: 'E!', when: '2014', quote: 'Spetacular!' },
  1: { source: 'E!', when '2016', 'Again Spectacular!' }
]

_.where(quotes, {source: 'E!', when: '2014'});
[ 0: { source: 'E!', when: '2014', quote: 'Spetacular!' } ]
						

.shuffle()

_.shuffle(list)

Returns a shuffled copy of the list, using a version of the Fisher-Yates shuffle*.

Continuing the previous example:


_.chain(quotes).where({source: 'E!'}).shuffle().value();
[
  0: { source: 'E!', when '2016', quote: 'Again Spectacular!' }
  1: { source: 'E!', when: '2014', quote: 'Spetacular!' },
]
						

*Fisher-Yates shuffle is http://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle

.pluck()

_.pluck(list, propertyName)

A convenient version of what is perhaps the most common use-case for map: extracting a list of property values.

uniq() is a Array function and will be formally described in the next section.

Continuing our example - useful for populating select tags:


_.chain(quotes).pluck("source").uniq().value();
0: "Hollywood Reporter"
1: "E!"
2: "Entertainment Weekly"
						

Map, Reduce & Filter

Map, Reduce and Filter each have ES5 replacements but I'm going to cover them as they're important concepts and are common to this kind of coding, and Functional Programming.

Each gets an iterator, processes each item in the collection, and returns...

  • map: a transformation of the original collection
  • reduce: a single value
  • filter: portions of the original collection.

.map()

_.map(list, iteratee, [context])

Produces a new array of values by mapping each value in list through a transformation function (iteratee).

While map may seem just about looping through values in a list, it's really about composition.


wordCount = function(s) { return s.split(/\s+/).length; } // simplistic
_.map(quotes, value => {
  value.wordCount = wordCount(value.quote);
  return value;
});
[
  { /* ... */ quote: 'Best ever!', wordCount: 2 },
  { /* ... */ quote: 'Spectacular!', wordCount: 1 },
  { /* ... */ quote: 'Amazing!', wordCount: 1 },
  { /* ... */ quote: 'Again Spectacular!', wordCount: 2 }
];
						

.reduce()

_.reduce(list, iteratee, [memo], [context])

...reduce boils down a list of values into a single value. Memo is the initial state of the reduction, and each successive step of it should be returned by iteratee.

An import aspect depends on if memo is passed:

  • Passed: execution begins with first element
  • Absent: first element is passed as menu to second element

.reduce(), cont


_.reduce(quotes, (memo, value, i) => {
  return memo + (i ? '  ' : '') + '"' + value.quote + '"'},
  ""
);
"Best ever!"  "Spectacular!"  "Amazing!"  "Again Spectacular!"
						

"a single value" can also be an object (or even an array):


_.reduce(quotes, (memo, value) => {
  if (value.when < 2015) { memo.before2015++ } else { memo.after2015++ };
  return memo;
  },
  { before2015: 0, after2015: 0 }
);
{ before2015: 1, after2015: 3 }
						

.filter()

_.filter(list, predicate, [context])

Looks through each value in the list, returning an array of all the values that pass a truth test (predicate).


_.filter(quotes, value => value.when == '2014');
{ source: 'E!', when: '2014', quote: 'Spectacular!' }
						

_.filter(quotes, value => wordCount(value.quote) > 1);
[
  { source: 'Hollywood Reporter', when: '2016', quote: 'Best ever!' },
  { source: 'E!', when: '2016', quote: 'Again Spectacular!' }
];
						

Array Functions

All array functions will also work on the arguments object. However, Underscore functions are not designed to work on "sparse" arrays.

While Collection Functions work on arrays and other structures, Array Functions only work on Arrays*.

Many of the Array functions may seem trivial but they shine when chained.

The Array Functions

1 item

  • first
  • last
  • object
  • indexOf
  • lastIndexOf
  • sortedIndex
  • findIndex
  • findLastIndex

1 - n items

  • initial
  • rest
  • compact
  • without
  • union
  • intersection
  • difference
  • uniq
  • unzip

1 - n+ items

  • flatten
  • zip
  • range

Replacement key: ES 5 ES 6 ES 7

.first()

_.first(array, [n])

Returns the first element of an array. Passing n will return the first n elements of the array.

Continuing the previous example:


_.chain(quotes).where({source: 'E!'}).shuffle().first().value();
{ source: 'E!', when '2016', 'Again Spectacular!' }
						

.uniq()

_.uniq(array, [isSorted], [iteratee])

Produces a duplicate-free version of the array, using === to test object equality. ... If you want to compute unique items based on a transformation, pass an iteratee function.


_.uniq(quotes, false, element => element.source );
[
  { source: 'Hollywood Reporter', when: '2016', quote: 'Best ever!' },
  { source: 'E!', when: '2014', quote: 'Spectacular!' },
]
						

See also prior code example.

.findIndex()

_.findIndex(array, predicate, [context])

Similar to _.indexOf, returns the first index where the predicate truth test passes; otherwise returns -1.


_.findIndex(quotes, element => element.source == 'E!' );
1 // which is { source: 'E!', when: '2014', quote: 'Spectacular!' }
						

Function Functions

What's your conjunction? ;)

These methods primarily work on functions, although sometimes on methods.

The Function Functions

Context

  • bind
  • bindAll

Timing

  • delay
  • defer
  • wrap

Frequency

  • throttle
  • debounce
  • once
  • after
  • before

Other

  • partial
  • memoize
  • negate
  • compose

Replacement key: ES 5 ES 6 ES 7

.debounce()

Debouncing is essential when dealing with verbose browser events, most commonly resize.

_.debounce(function, wait, [immediate])

Creates and returns a new debounced version of the passed function which will postpone its execution until after wait milliseconds have elapsed since the last time it was invoked.


var debouncedResize = _.debounce(function() {
  console.log('resized!');
  }, 75);
$(window).resize(debouncedResize);
						

.once()

Once'ing is helpful for ensuring something only happens once. Don't you hate those payment pages that warn you about clicking pay button only once?

_.once(function)

Creates a version of the function that can only be called one time. Repeated calls to the modified function will have no effect, returning the value from the original call.


var processPayNow = function() { console.log('paid!') };
var payNowOnce = _.once(processPayNow);
document.getElementById("pay-now").addEventListener("click", payNowOnce);
						

Composition

Composition is a core concept of functional programming.

_.compose(*functions)

Returns the composition of a list of functions, where each function consumes the return value of the function that follows.


// First lets define some small functions:
var stripLeadingSlash = function (s) { return (s.indexOf('/') === 0) ? s.substring(1) : s; };
var prefixHost = function (s) { return '//' + window.location.host + s; };
var makeLink = function (s) { return '' + s + ''; };
						

Example continued on next slide.

Composition continued.

The long, wordy way:


var s = prefixHost('/node/3');
s = stripLeadingSlash(s);
s = makeLink(s);
<a href="//my.domain/node/3">//node/3</a>
						

Or deeply nest them:


// you "read" from inside to out
var s = makeLink(stripLeadingSlash(prefixHost('/node/3')));
<a href="//my.domain/node/3">//node/3</a>
						

Or use composition, and be able to reuse elsewhere:


// you "read" right to left
var processLinks = _.compose(makeLink, stripLeadingSlash, prefixHost);
var s = processLinks('/node/3');
<a href="//my.domain/node/3">//node/3</a>
						

Object Functions

In JavaScript Function, Array, and Object are objects*.

* also RegExp and Date.

The Object Functions

Component related

  • keys
  • allKeys
  • values
  • pairs
  • invert
  • functions
  • findKey
  • defaults
  • has
  • property
  • propertyOf
  • mapObject
  • matcher

Returns Object

  • create
  • extend
  • extendOwn
  • pick
  • omit
  • clone
  • tap

Replacement key: ES 5 ES 6 ES 7

The Object Functions, II

"IS"

  • isEqual
  • isMatch
  • isEmpty
  • isElement
  • isArray
  • isObject
  • isArguments
  • isFunction
  • isString
  • isNumber
  • isFinite
  • isBoolean
  • isDate
  • isRegExp
  • isError
  • isNaN
  • isNull
  • isUndefined

Replacement key: ES 5 ES 6 ES 7

.extend()

_.extend(destination, *sources)

Shallowly copy all of the properties in the source objects over to the destination object, and return the destination object.

"Shallow copy" means nested objects will be copied by reference.

sources are processed in-order so subsequent properties overwrite earlier ones.

See my Backbone.js presentation for an example.

.defaults()

_.defaults(object, *defaults)

Fill in undefined properties in object with the first value present in the following list of defaults objects.

In this example we've defined site-wide Slick Slider defaults, that we can override easily:


var slickDefaults = { autoplay: false, arrows: false, dots: true, mobileFirst: true };
var doSlick = function (selector, settings) {
  $(selector).slick(_.defaults(settings, slickDefaults));
}
doSlick($('#slider-a'), { dots: false });
// passed settings are: {dots: false, autoplay: false, arrows: false, mobileFirst: true}
						

.clone()

_.clone(object)

Create a shallow-copied* clone of the provided plain object.

Like with .extend(), "shallow copy" means nested objects will be copied by reference.


var a = { item: "blue", arr: [1,2,3] };
var c = _.clone(a);
a.item = "green";
a.arr[0] = 9;
console.log(c); // {item: "blue", arr: [9, 2, 3] }
						

.clone(), cont.

Since Objects (including Function, Array, and Object) in JS are assigned by reference, if you want to actually copy an object you need a function like this. Consider:


var a = { item: "blue", arr: [1,2,3] };
var b = a;
a.item = "green";
a.arr[0] = 9;
console.log(b); // {item: "green", arr: [9, 2, 3] }
						

.tap()

_.tap(object, interceptor)

Invokes interceptor with the object, and then returns object. The primary purpose of this method is to "tap into" a method chain, in order to perform operations on intermediate results within the chain.

Used in chaining, to perform operations on the object itself, not directly object items. You must modify the object itself, not return or create a new object.


_.chain([3,6,8,2,4,0,9,6]).tap(obj => obj.sort()).value();
[0, 2, 3, 4, 6, 6, 8, 9]
// this example could also just be done via array.prototype.sort, or _.invoke()
						

.isEqual()

_.isEqual(object, other)

Performs an optimized deep comparison between the two objects, to determine if they should be considered equal.

JS objects (Function, Array, and Object*) are never "equal" even for seemingly trivial "equivalant" objects.


[1,2,3]==[1,2,3]
false
_.isEqual([1,2,3],[1,2,3]);
true
						

Utility Functions

The Utility Functions are mostly those used internally by other Underscore methods, or don't fall into any other category.

The Utility Functions

Meta

  • noConflict
  • mixin

Internal

  • identity
  • constant
  • noop
  • iteratee

DOM/HTML

  • uniqueId
  • escape
  • unescape

Other

  • times
  • random
  • result
  • now
  • template

Replacement key: ES 5 ES 6 ES 7

.uniqueId()

_.uniqueId([prefix])

Generate a globally-unique id for client-side models or DOM elements that need one. If prefix is passed, the id will be appended to it.


_.chain(quotes).pluck("source").uniq() // this is the same as prior
  .map(
    (value, key) => ({ value: value, id: _.uniqueId('source-select-') }) )
  .value();
[
  { id: "source-select-1", value: "Hollywood Reporter" }
  { id: "source-select-2", value: "E!" }
  { id: "source-select-3", value: "Entertainment Weekly" }
]
						

See the prior example on pluck(). We'll use this result later in another example.

.mixin()

_.mixin(object)

Allows you to extend Underscore with your own utility functions.


_.mixin({
  sum(arr) { return _.reduce(arr, (memo, num) => memo + num) },
  mean(arr) { return _.sum(arr) / arr.length; },
  trimmedMean(arr, perc) {
    var s = Math.round(arr.length * (perc / 100));
    if ((s * 2) >= arr.length) return undefined;
    if (!s) return _.mean(arr);
    arr.sort();
    return _.mean(arr.slice(s, s * -1));
  }
});
_.trimmedMean([3,6,8,2,4,0,9,6], 5); // 4.75
_.trimmedMean([3,6,8,2,4,0,9,6], 10); // 4.833333333333333
_.trimmedMean([3,6,8,2,4,0,9,6], 40); // 5
						

.escape() & .unescape()

_.escape(string)

Escapes a string for insertion into HTML, replacing &, <, >, ", `, and ' characters.

_.unescape(string)

The opposite of escape, replaces &amp;, &lt;, &gt;, &quot;, &#96; and &#x27; with their unescaped counterparts.

This should be obvious so I'm skipping an example.

.template()

_.template(templateString, [settings])

Compiles JavaScript templates into functions that can be evaluated for rendering. Useful for rendering complicated bits of HTML from JSON data sources...

ES6 has Template Strings which offers multi-line and value interpolation, but you'll probably need to transpile for broad compatibility.

Templating, 1

Underscore by default uses ERB-style deliminters which is basd on Embedded Ruby template syntax. This can be changed but we won't get into it.

Interpolation deliminters:

  • Values: <%= someVar %>
  • Values w/escaping: <%- someVar %>
  • Arbitrary JS: <% someCode %>

Templating, 2

By default Underscore templating uses the dreaded with* statment so the solution is to define the setting's variable property, and access vars relative to a container object.


var p = _.template('<%= data.myVar %>', { variable: 'data' });
p({ myVar: 'No with! '});
						

{variable: 'data'} tell Bb to assign the passed properties as properties of variable and not individual variables.

*with is discouraged, as it's tough for the compilier to optimise, it's slower, and can be tough to troubleshoot.

Templating, 3

Building off of .uniqueId() by setting quotesSources to the example output.


var menuT = _.template([
  '
    ', '<% _.each(data, el => { %>', '
  • <%= el.value %>
  • ', '<% }); %>', '
'].join(''), {variable: 'data'}); menuT(quotesSources);
  • Hollywood Reporter
  • E!
  • Entertainment Weekly

Templating, 4

If you don't like using JavaScript strings, or it doesn't work for your implementation, you can include the template "string" in your html.



						

var menuT = _.template($('#tmpl-filter-options').html(), {variable: 'data'});
menuT(quotesSources);
  • Hollywood Reporter
  • E!
  • Entertainment Weekly

Underscore-contrib

These contributed libraries are not included with D8 but are available for download. They include:

  • array.builders
  • array.selectors
  • collections.walk
  • function.arity
  • function.combinators
  • function.iterators
  • function.predicates
  • object.builders
  • object.selectors
  • util.existential
  • util.operators
  • util.strings
  • util.trampolines

Ported Underscore

Underscore has been ported to pretty much every other popular languages:

  • PHP!
  • Lua
  • Swift
  • Objective-C
  • Perl
  • Coldfusion
  • Java
  • Ruby
  • Python