Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at CFUNITED 2008 (Washington, D.C.) with: Yaron Kohn
Ben Nadel at CFUNITED 2008 (Washington, D.C.) with: Yaron Kohn

The Philosophy Of Extending Lodash In JavaScript

By Ben Nadel on

At work, I sometimes get into a philosophical debate about our use of the Lodash library. I love lodash and use it in our AngularJS applications all the time. But, I will often extend the lodash library with additional utility functions. This is where things gets heated. Some of the engineers question my desire to extend lodash when I could implement the same functionality using existing lodash features. To me, this mentality misses the point and often leads to a duplication of effort in the code.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

The more code it takes to implement a feature, the harder it is to understand and maintain. As such, if we can remove or encapsulate complexity, it's inherently a win for our application (and our engineering team). This is why I'll try to factor out common lodash use-cases into their own extensions. This provides a smaller, more intuitive interface that is less brittle and easier to maintain.

To make this concrete, consider the task of setting a value in each item of a collection. Certainly, you can do this using existing lodash functionality, which can range from simple and verbose (forEeach) to compact and cryptic (partial application). But, by creating a lodash extension, we can create a beautiful marriage between simple and compact.

In the following code, I'm going to use three different approaches to setting a value on each item in a collection. The first two approaches attempt to use existing lodash features, with subjective levels of success. The third approach still uses existing lodash functionality; but, it encapsulates the complexity and verbosity behind a lodash extension:

  • <!doctype html>
  • <html>
  • <head>
  • <title>The Philosophy Of Extending Lodash In JavaScript</title>
  • </head>
  • <body>
  •  
  • <h1>
  • The Philosophy Of Extending Lodash In JavaScript
  • </h1>
  •  
  • <p>
  • <em>All the magic is in the console</em>.
  • </p>
  •  
  • <script type="text/javascript" src="../../vendor/lodash/lodash-3.9.3.min.js"></script>
  • <script type="text/javascript">
  •  
  • // Start out with our initial collection. We'll be augmenting it using the
  • // lodash library.
  • var friends = [
  • {
  • id: 1,
  • name: "Kim"
  • },
  • {
  • id: 2,
  • name: "Sarah"
  • },
  • {
  • id: 3,
  • name: "Tricia"
  • }
  • ];
  •  
  •  
  • // ---
  • // APPROACH 1: Use explicit for-each method so you can see exactly what the code
  • // is doing. Easy to understand, easy to maintain, but verbose.
  • // ---
  •  
  •  
  • // Use an inline operator.
  • _.forEach(
  • friends,
  • function operator( friend, i ) {
  •  
  • friend.createdAt = _.now();
  •  
  • }
  • );
  •  
  •  
  • // ---
  • // APPROACH 2: Using unfortunate complexity. Hard to understand. Personally, when
  • // I see code like this, I think the developer was trying to be too clever. It's
  • // not about "you" bro, it's about the "next guy" trying to maintain your code.
  • // ---
  •  
  •  
  • // Using partial functions.
  • _.forEach( friends, _.partial( _.set, _, "isActive", true ) );
  •  
  •  
  • // ---
  • // APPROACH 3: Factor out common use-cases into an actual extension for lodash
  • // itself. This way, the complexity and verbosity of common tasks can be
  • // encapsulated behind well-named, easy-to-understand intentional methods.
  • // ---
  •  
  •  
  • // Extend lodash with some custom functions.
  • _.mixin({
  • setEach: function( collection, path, value ) {
  •  
  • var result = _.forEach(
  • collection,
  • function operator( item, index ) {
  •  
  • _.set( item, path, value );
  •  
  • }
  • );
  •  
  • return( result );
  •  
  • },
  •  
  • extendEach: function( collection, source ) {
  •  
  • var result = _.forEach(
  • collection,
  • function operator( item, index ) {
  •  
  • _.extend( item, source );
  •  
  • }
  • );
  •  
  • return( result );
  •  
  • }
  • });
  •  
  • // Now that we've extended lodash, we have a very simple function that is
  • // intuitive and reusable. And, if it's ever not enough, you can fall back to
  • // using a more explicit, inline approach.
  • _.setEach( friends, "isBFF", true );
  •  
  • // Use our other custom function.
  • _.extendEach(
  • friends,
  • {
  • foo: "bar",
  • hello: "world"
  • }
  • );
  •  
  •  
  • // CAUTION: The fact that I am using _.ary() here to apply the console.log()
  • // function is a bit tongue-in-cheek. This is more complex that I would like it
  • // to be. But, you can't help but enjoy the challenge of making overly-complex
  • // algorithms.
  • _.forEach( friends, _.ary( console.log, 1 ) );
  •  
  • </script>
  • </body>
  • </html>

The first approach, using the forEach() method, is very easy to understand and is very flexible. But, it's somewhat verbose.

The second approach, using forEach() in conjunction with partial application, is compact, but extremely cryptic. Like a Regular Expression, it might make sense to you when you write this code; but, for the engineer that has to come after you and maintain it, such function composition can often be hard to decipher. I would suggest, when possible, avoiding code like this that attempts to be too clever.

The third approach, extending lodash, creates the most compact code and provides a method name and signature that is easy to understand and consume. In my opinion, in this particular use case, the third approach makes for the most elegant solution.

I am not saying that you should factor out every possible lodash use case. In many cases, having an inline forEach() call is absolutely the right way to go. But, when you see common patterns emerge, moving them into a lodash extension will help you keep your code DRY (Do not Repeat Yourself).




Reader Comments

Yay for mixins! In node-land you can combo `_.mixin` with `_.runInContext` to ensure extension are added to a pristine lodash instance and not augmenting the `lodash` package itself:

```js
var _ = require('lodash').runInContext();
_.mixin(...);
```

Reply to this Comment

@Jdalton,

Very interesting. That reminds of jQuery's old .sub() method which, from what I recall, allowed you to sub-class jQuery for use in a particular context. Do you have any thoughts on the right approach to this in Node? Shooting from the hip, I would probably want to create a local lodash module that turns around includes the node-based module. So, something like this:

./lodash.js:
=========
module.exports = require( "lodash" ).runInContext();

module.exports.mixing( .... )
=========

... then, in the rest of my app, my other core modules would include my version of lodash (ie, require( "./lib/util/lodash" )) and not the underlying library?

Or am I over-thinking it?

Reply to this Comment

Good article ben, I haven't ever looked into lodash's mixin functionality - good stuff.

Also, just wanted to throw out that you could also consider using map() for your purposes:

friends = _.map(friends, function(item) {
item.createdAt = _.now();
return item;
});

Reply to this Comment

@Ryan,

As an aside, how great is the "_.now()" method?! I can't tell you how many times I've had to create that method before I discovered it in lodash :D

Reply to this Comment

Yeah, its great - I sing the praises of lodash on a regular basis, and always finding something new. Have you ever used _.template() ? Might not be as useful for you with angular, but its great.

Reply to this Comment

@Ryan,

I think I looked at it once a few years ago; or rather, I looked at the Underscore implementation - I am not sure if it's any different. Right now, on the client, I use AngularJS pretty heavily; but, this could be really useful on the server (in Node.js).

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.