Lo-Dash Essentials

5 (3 reviews total)
By Adam Boduch
  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies

About this book

Lo-Dash Essentials walks you through the Lo-Dash utility library, which promises consistency and performance in JavaScript development. This book looks into the most common functions and the various contexts in which they're used. You'll first start with object types and their properties, then you'll dive into larger development patterns, such as MapReduce, and how to chain functionality together. Following this, you'll learn how to make suitable builds for various environments, and discover how high-level patterns complement one another and how they lead to reusable building blocks for applications. Finally, you will gain some practical exposure to Lo-Dash by working alongside other libraries, and learn some useful techniques for improving performance.

Publication date:
January 2015
Publisher
Packt
Pages
232
ISBN
9781784398330

 

Chapter 1. Working with Arrays and Collections

Lo-Dash offers a wide variety of functions that operate on arrays and collections. This generally involves iterating over the collection in one form or another. Lo-Dash helps make iterative behavior easy to implement, including searching for data, as well as building new data structures.

Collections are at the heart of applicative programming. This is where we will successively apply functions to each element in the collection. This chapter introduces the concept of the collection, something Lo-Dash code uses extensively.

In this chapter, we will cover the following topics:

  • Iterating over collections

  • Sorting data

  • Searching for data

  • Slicing collections into smaller pieces

  • Transforming collections

 

The difference between arrays and collections


One of the initial sources of confusion for newcomers to Lo-Dash is the distinction between arrays and collections. The Lo-Dash API has a set of functions for arrays, and a set of functions for collections. But why? It would appear that these functions are interchangeable for any collection. Well, a better definition of what a collection actually is according to Lo-Dash might clear things up.

A collection is an abstract concept. That is, we can use the collection functions found in Lo-Dash on any JavaScript object that we'd like to iterate over. For example, the forEach() function will happily iterate over an array, a string, or an object. The subtle differences between these types, and what they mean when it comes to iterating, are hidden from the developer.

The array functions provided by Lo-Dash are less abstract, they do, in fact, expect an array. In a sense, even these functions are abstract because they don't explicitly check for the Array type. They require that the object supports numerical indices and that it has a numerical length property.

The takeaway is that, in the overwhelming majority of your days as a Lo-Dash programmer, the distinction between arrays and collections does not matter. Mainly, because the primary collection type will be an array anyway. In a small minority of cases, where the distinction does matter, just remember that the array functions have a slightly stricter criteria for what they consider acceptable data.

 

Iterating over collections


Lo-Dash does a lot of iterating over collections, whether it's done explicitly by the developer, or done implicitly through a higher level Lo-Dash function. Let's take a look at the basic forEach() function:

var collection = [
    'Lois',
    'Kathryn',
    'Craig',
    'Ryan'
];

_.forEach(collection, function(name) {
    console.log(name);
});
// → 
// Lois
// Kathryn
// Craig
// Ryan

This code doesn't do much, aside from logging the value of each item in the collection. It does, however, give you a general sense of what iterative functions look like in Lo-Dash. As the name implies, for each element in the array, apply the function callback. There's more than just the current element that's passed to the callback function. We also get the current index.

Tip

Downloading the example code

You can download the example code files from your account at http://www.packtpub.com for all the Packt Publishing books you have purchased. If you purchased this book elsewhere, you can visit http://www.packtpub.com/support and register to have the files e-mailed directly to you.

Take a look at the following code:

var collection = [
    'Timothy',
    'Kelly',
    'Julia',
    'Leon'
];

_.forEach(collection, function(name, index) {
    if (name === 'Kelly') {
        console.log('Kelly Index: ' + index);
        return false;
    }
});
// → Kelly Index: 1

Returning false tells forEach() that this iteration will be the last. The index, with each iteration, is incremented and passed to the callback function as the second argument.

The forEach() function iterates over the collection in the typical left-to-right fashion. If we want the inverse behavior, we can use the cousin function, forEachRight():

var collection = [
    'Carl',
    'Lisa',
    'Raymond',
    'Rita'
];

var result = [];

_.forEachRight(collection, function(name) {
    result.push(name);
});
// →
// [
//   "Rita",
//   "Raymond",
//   "Lisa",
//   "Carl"
// ]

This type of behavior is useful when we're working with sorted collections, as is the case in the preceding code. But, let's say we wanted to render this array data in the DOM in descending order. The preceding code shows that we can render each item in a given iteration. Using the forEachRight() function for this scenario has the advantage of not having to reverse-sort the array.

However, many times this shortcut will not suffice, and you have to sort your collections. We'll take a look at the Lo-Dash functions that assist with sorting next.

 

Sorting data


In Vanilla JavaScript, the approach to sorting involves arrays and two methods. The sort() method sorts the array in ascending order, using primitive comparison operations between the items. You can customize this behavior by passing sort(), a comparator function. For example, you use this callback function to sort an array in descending order. The other method, reverse(), simply reverses the order of the array. It's the inverse of the current order, whatever that might be.

The native array sort() method sorts the array in-place although you might not want that to happen. Immutable operations reduce side effects because they don't change the original collection. Specifically, you might have requested the API data in a specific order. A region of the UI wants to render this array in a different sort order. Well, you don't want to change the order from what was requested. In this case, it would be better to have a function that returns a new array that contains the items of the original, but in the expected sort order.

Using sortBy()

The sortBy() function is the Lo-Dash answer to the native Array.sort() method. Since it's an abstract collection function, it's not limited to arrays. Take a look at the following code:

_.sortBy('cba').join('');

While the function works just fine with strings as the input, the output is a sorted array of characters; hence, the call to join them back together. This is because the sortBy()function always returns an array as the result.

The sortBy() function is similar to the native Array.sort() method, in that it sorts collection items in ascending order by default. Also, similar to the native method, we can pass in a callback function to sortBy() that'll customize the sorting behavior, as follows:

var collection = [
    { name: 'Moe' },
    { name: 'Seymour' },
    { name: 'Harold' }, 
    { name: 'Willie' }
];

_.sortBy(collection, function(item) {
    return item.name;
});

The preceding callback function passed to sortBy() returns the value of an object property. By doing this, the sorting behavior will compare the property values—in this case, name—instead of the objects themselves. There's actually a shorter way to achieve the same result:

_.sortBy(collection, 'name');

This is what's referred to as the pluck style shorthand in Lo-Dash terminology. We pass in the name of the property we want to sort the collection by. The value of this property is then plucked from each object in the collection. There's actually a pluck() function we'll look at in more depth later on.

The last trick sortBy() has up its sleeve takes the pluck shorthand to the next level and allows sorting by multiple property names, as shown in the following code:

var collection = [
    { name: 'Clancy', age: 43 },
    { name: 'Edna', age: 32 },
    { name: 'Lisa', age: 10 },
    { name: 'Philip', age: 10 }
];

_.sortBy(collection, [ 'age', 'name' ]);
// →
// [
//   { name: "Lisa", age: 10 },
//   { name: "Philip", age: 10 },
//   { name: "Edna", age: 32 },
//   { name: "Clancy", age: 43 }
// ]

The primary determinant of order here is the age property. If we specify a second property name, this is used to determine the order of elements that have the same primary sort order. It serves as a tie breaker. Here, there are two objects where age equals 10. Since the name property is the secondary sort order, this is how these two objects are sorted. Multiple sort properties is a typical use case in web applications, which would require us to write a surprisingly large amount of JavaScript to achieve, if not for this Lo-Dash utility.

Maintaining the sort order

Using the sortBy() function is a great tool for changing the sort order of an existing collection, especially if we don't want to permanently alter the default sort order of that collection. Other times, however, you'll want to permanently keep a collection sorted. Sometimes, this is actually done for us by the backend API that sends us collections in the form of JSON data.

In these situations, sorting is easy because you don't actually have to sort anything. It's already done. The challenge lies in maintaining the sort order. Because, sometimes elements get added to collections in real time. The naive approach to maintain sort order here would be to simply add the new element to the collection then resort it. The Lo-Dash alternative is to figure out the insertion point that will keep the current collection sort order intact. This is shown in the following code:

var collection = [ 
    'Carl',
    'Gary',
    'Luigi',
    'Otto'
];

var name = 'Luke';

collection.splice(_.sortedIndex(collection, name), 0, name);
// → 
// [
//   "Carl",
//   "Gary",
//   "Luigi",
//   "Luke",
//   "Otto"
// ]

The new name variable gets inserted into the second-last position. This is really the only function needed to maintain the order of a sorted collection. The same splice() array method is used to remove items from the collection, which doesn't disrupt the order. Adding new items is a challenge because of the search that takes place to figure out the insertion index. The sortedIndex() function does a binary search on the collection to figure out where the new item fits.

 

Searching for data


Applications don't use entire collections. Rather, they iterate over a collection subset, or they look for a specific item in the collection. Lo-Dash has a number of functional tools to help the programmer find the data they need.

Filtering collections

The simplest way to perform a filter operation on a collection using Lo-Dash is to use the where() function. This function takes an object argument and will match its properties against each item in the collection, as shown in the following code:

var collection = [ 
    { name: 'Moe', age: 47, gender: 'm' },
    { name: 'Sarah', age: 32, gender: 'f' },
    { name: 'Melissa', age: 32, gender: 'f' },
    { name: 'Dave', age: 32, gender: 'm' }
];

_.where(collection, { age: 32, gender: 'f' });
// →
// [
//   { name: "Sarah", age: 32, gender: "f" },
//   { name: "Melissa", age: 32, gender: "f" }
// ]

The preceding code filters the collection on both the age and the gender properties. The query translates to thirty-two year old females. The Moe object matches with neither property, while the Dave object matches with the age property, but not gender. A good way to think about where() filtering is that each object property you pass in as the filter will be logical and joined together. For example, match the age and the gender properties.

The where() function is great for its concise syntax and intuitive application to collections. With this simplicity comes a few limitations. First, the property values that we're comparing to each item in the collection must match exactly. Sometimes, we need comparisons a little more exotic than strict equality. Second, the logical and way that where() joins query conditions together isn't always desirable. Logical or conditions are just as common.

For these types of advanced filtering capabilities, you should turn to the filter() function. Here's a basic filter operation that's even simpler than the where() queries:

var collection = [ 
    { name: 'Sean', enabled: false },
    { name: 'Joel', enabled: true },
    { name: 'Sue', enabled: false },
    { name: 'Jackie', enabled: true }
];

_.filter(collection, 'enabled');
// →
// [
//   { name: "Joel", enabled: true },
//   { name: "Jackie", enabled: true }
// ]

Since the enabled property has truthy values for two objects in this collection, they're returned in a new array.

Note

Lo-Dash uses the notion of truthy values everywhere. This simply means that a value will test positive if used in an if statement or a ternary operator. Values don't need to be of Boolean type and true to be truthy. An object, an array, a string, a number—these are all truthy values. Whereas null, undefined, and 0— are all false.

As mentioned, the filter() function fills gaps in the where() function. Unlike where(),filter() accepts a callback function that's applied to each item in the collection, as shown in the following code:

var collection = [ 
    { type: 'shirt', size: 'L' },
    { type: 'pants', size: 'S' },
    { type: 'shirt', size: 'XL' },
    { type: 'pants', size: 'M' }  
];

_.filter(collection, function(item) {
    return item.size === 'L' || item.size === 'M';
});
// →
// [
//   { type: "shirt", size: "L" },
//   { type: "pants", size: "M" }
// ]

The callback function uses an or condition to satisfy the size constraint here—medium or large. This is simply not doable with the where function.

Note

The filter() function accepts an object argument as well. In Lo-Dash terminology, this is called a where style callback. There are many functions, not just filter(), that accept the filter criteria specified as an object and behave like where().

Filtering collections using the filter() function is good when we know what we're looking for. The callback function gives the programmer enough flexibility to compose elaborate criteria. But sometimes, we don't know what you need from a collection. Instead, you only know what you don't need, as shown in the following code:

var collection = [
    { name: 'Ryan', enabled: true },
    { name: 'Megan', enabled: false },
    { name: 'Trevor', enabled: false },
    { name: 'Patricia', enabled: true }
];

_.reject(collection, { enabled: false });
// →
// [
//   { name: "Ryan", enabled: true },
//   { name: "Patricia", enabled: true }
// ]

You can see here that only enabled items are returned that are equivalent to doing _.filter(collection, {enabled: true}),which is a simple inversion of filter(). Which function you use is a matter of personal preference and the context in which they're used. Go for the one that reads cleaner in your code.

Note

reject() actually uses the filter() function internally. It uses the negate() function to invert the result of the callback passed to filter().

Finding items in collections

Sometimes, we need a specific collection item. Filtering a collection simply generates a new collection with less items in it. Conversely, finding items in a collection means finding a specific item.

The function used to find items in a collection is aptly named find(). This function accepts the same arguments as the filter() function. You can pass the name of the property as a string, an object filled with property names and values to execute a where style search, or just a plain callback function to match against whatever you want. The following is an example of this:

var collection = [ 
    { name: 'Derek', age: 37 },
    { name: 'Caroline', age: 35 },
    { name: 'Malcolm', age: 37 },
    { name: 'Hazel', age: 62 } 
];

_.find(collection, { age:37 });
// → { name: "Derek", age: 37 }

There're actually two items matching this where style criteria in the collection—Derek and Malcolm. If we were to run this code though, we'd see that only Derek is returned. That's because find() returns as soon as a match is found. Collection order matters when searching for items in collections. It doesn't take into consideration duplicate values.

Let's look in the other direction and see what we find. Using the same collection and the same search criteria, you can search in the opposite direction:

_.findLast(collection, { age:37 });
// → { name: "Malcolm", age: 37 }

While find() searches for the first occurrence of the matching item in the collection, findLast() searches for the last occurrence. This is useful when we're working with sorted collections—you can better optimize your linear searches.

Note

While Lo-Dash heavily optimizes the while loops used when iterating over collections, searches executed using functions such as find() are linear. It's important to remember that it's up to the programmer using Lo-Dash to consider the performance implications of their unique application data. Lo-Dash functions are optimized for the generic common case, they're not going to magically make your code faster by virtue of using them. They're tools to assist the programmer to make incredibly high-performance code.

 

Slicing collections into smaller pieces


So far we've seen how collections can be filtered, creating a new smaller collection. Lo-Dash supplies you with a number of functions that take existing arrays and produce one or more smaller arrays. For example, you might want a portion of the first part of any array—or a portion of the last part. Arrays can be divided into chunks of smaller arrays that are useful for batched processing. You can also use Lo-Dash array tools to remove duplicates, thus ensuring the uniqueness of your array.

First and last collection portions

With native JavaScript arrays, you can slice off the first portion of an array using the slice() array method. Lo-Dash provides abstractions on top of the native array slice() method that make it a little easier for the developer to write intuitive code—this isn't always the case with the native array approach. Further, the Lo-Dash take() function operates on collections, so it'll work with both arrays and strings, as shown in the following code:

var array = [ 
    'Steve',
    'Michelle',
    'Rebecca',
    'Alan'
];

_.take(array, 2);
// → [ "Steve", "Michelle" ]

_.take('lodash', 2).join('');
// → "lo"

There's a difference in the output when using take() on arrays and strings. When applying it to an array, it generates a new array, a subset of the original. However, when applying take() to strings, it returns a new array of individual characters. The preceding code will return [ 'l', 'o' ]. That's probably not what we're after most of the time, so we'll just join these characters back together with an empty string.

We can slice off the last portions of collections and strings using the takeRight() function. Using the same array and string, you can run the following code to get the last portions of the collections:

_.takeRight(array, 2);
_.takeRight(string, 4).join('');

The resulting array looks like [ 'Rebecca', 'Alan']. The resulting string looks like 'dash'.

Applying take() to a collection without any arguments will slice the first item. Likewise, applying takeRight() without any arguments slices off the last item. In both the cases, the returned value is a one item array, not the item itself. If you're just after the first or last collection items, use the first() and last() Lo-Dash functions respectively.

Splitting collections into chunks

Sometimes, we're faced with large collections. Really large collections. Especially when using API data, the frontend doesn't always have control over the size of the dataset that's returned. When the API does return a mountain of data, there's a good chance that our code that processes it will lock the UI. We can't exactly say give me less data to work with so that the UI doesn't freeze. Freezing the UI is also unacceptable.

Lo-Dash iterates through collections very efficiently. It doesn't, however, have any control over the potentially expensive operations carried out by your code. And this is what causes the UI to freeze – not the size of the collection by itself, and not by executing an expensive operation once – it's the two factors combined together that become lethal for UI responsiveness.

The chunk() function is an easy way to split the processing of a really large collection into several smaller tasks. This gives the chance for the UI to update—render the pending DOM updates and processes the pending events. The usage of this function can be seen in the following code:

function process(chunks, index) {
    var chunk = chunks[index];
    if (_.isUndefined(chunk)) {
        return;
    };  
    console.log('doing expensive work ' + _.last(chunk));
    _.defer(_.partial(process, chunks, ++index));
}

var collection = _.range(10000),
    chunks = _.chunk(collection, 50);

process(chunks, 0);
// → 
// doing expensive work 49
// doing expensive work 99
// doing expensive work 149

If the preceding code is a bit of a turn off, don't worry. There're a few new concepts introduced here that you might find confusing. Let's start by explaining at a high level what the code is actually doing. A large collection is created and it's split into chunks of smaller collections. The process() function does some work with each chunk, then calls itself again to process the next chunk, until there are no chunks left.

The collection itself is generated using the range() function, with 10000 integers in it. It's not the content that's important, but rather, the large size. The chunk() function is used to split the large collection into smaller ones. We specify the size we want each chunked collection to be and in this case, we get 20 smaller collections with 50 items each. The processing work is kicked off by the call to process(chunks, 0). The second argument is the first chunk to begin with.

The process() function itself grabs the next chunk to process based on the index argument. If the chunk is undefined, it means that the end has been reached and there are no more chunks to process. Otherwise, we can start doing expensive processing on the chunk, as illustrated in the example with the console.log() call. Finally, the defer() function will start processing the next chunk. The reason we're using defer() is so that the call stack has a chance to clear, and the DOM operations have a chance to run. If we don't do this, there wouldn't be any point in using chunk() to split the processing. The defer() function expects a callback, and we make one using partial(), which creates a new function, with arguments already supplied to it.

Note

The defer() and partial() functions are covered in much more depth in Chapter 3, Working with Functions.

How do we know what size to make our array chunks? In the previous code, we chose 50 as the chunk size. But is that an arbitrary decision, or is it based on the typical datasets used in the application? The short answer is that we have to tinker a little and optimize for the common case. This might mean doing something such as figuring out the chunk size based on a percentage of the overall collection size, as shown in the following code:

var collection = _.range(10),
    size = Math.ceil(0.25 * collection.length);
_.chunk(collection, size);
// → 
// [
//   [ 0, 1, 2 ],
//   [ 3, 4, 5 ],
//   [ 6, 7, 8 ],
//   [ 9 ]
// ]

The chunk size here turns out to be 3. The actual size is 2.5, but you take the ceiling of that since there's no such thing as 2.5 collection elements. Besides, what you're interested in is not the exactness of the chunk size, but rather, the proximity to the 25 percent.

Note

You might have noticed that 3 doesn't divide evenly into 10. The chunk() function is smart enough to not leave out items. Any remaining items that do not fill the chunk size are still included.

Building unique arrays

Collections sometimes have unwanted duplicates in them. This could be a result of the API data itself that contains the duplicates, or as a side effect of other computations you're performing in the frontend. Regardless of the cause, Lo-Dash provides the tools necessary to quickly generate unique collections.

The uniq() function takes a collection as the input and generates a new collection as output, with any duplicates removed:

var collection = [ 
    'Walter',
    'Brenda',
    'Arthur',
    'Walter'
];

_.uniq(collection);
// → [ "Walter", "Brenda", "Arthur" ]

By default, the potential duplicates are compared against one another using the strict equality operator. In the preceding collection, the duplicate is found and removed because of 'Walter' === 'Walter'. You can specify, in more detail, how you want uniq() to compare values. For example, if we had a collection of objects and we only want unique objects based on the name property, we could write _.uniq(collection, 'name'). The function also accepts a callback, which is used to compute the values before they're compared. This is useful in situations where the uniqueness of an object isn't so straightforward, as in the following code:

var collection = [ 
    { first: 'Julie', last: 'Sanders' },
    { first: 'Craig', last: 'Scott' },
    { first: 'Catherine', last: 'Stewart' },
    { first: 'Julie', last: 'Sanders' },
    { first: 'Craig', last: 'Scott' },
    { first: 'Janet', last: 'Jenkins' }
];

_.uniq(collection, function(item) {
    return item.first + item.last;
});
// →
// [
//   { first: "Julie", last: "Sanders" },
//   { first: "Craig", last: "Scott" },
//   { first: "Catherine", last: "Stewart" },
//   { first: "Janet", last: "Jenkins" }
// ]

This code ensures that the uniqueness of each object in the collection is based on the full name. There's no full name property, perhaps it's not needed anywhere else in the application. So, the uniq() function can just construct one on-the-fly, which is used for the sole purpose of validating this constraint.

 

Transforming collections


Lo-Dash has a number of tools for transforming collections into new data structures. Additionally, there are tools that can take two or more collections and combine them into a single collection. These functions focus on the most common, yet most burdensome programming tasks faced by frontend developers. Instead of focusing on boilerplate collection transformations, you can get back to making a great application—users don't care about awesome compact code as much as you do.

Grouping collection items

Items in collections are sometimes grouped implicitly. For example, let's say there's a size property for a given class of objects whose allowable values are 'S', 'M', or 'L'. The code in your frontend application might need to round up the items that contain these various groups for display purposes. Rather than writing our own code, we'll let the groupBy() function handle the intricacies of constructing such a grouping:

var collection = [ 
    { name: 'Lori', size: 'S' },
    { name: 'Johnny', size: 'M' },
    { name: 'Theresa', size: 'S' },
    { name: 'Christine', size: 'S' }
];

_.groupBy(collection, 'size');
// →
// {
//   S: [
//     { name: "Lori", size: "S" },
//     { name: "Theresa", size: "S" },
//     { name: "Christine", size: "S" }
//   ],
//   M: [
//     { name: "Johnny", size: "M" }
//   ]
// }

The groupBy() function, as you might have noticed by now, doesn't return a collection—it takes a collection as the input, but transforms it into an object. This object that groupBy() returns contains the original items of the input collection, they're just organized differently. The properties of the object are the values you want to group by. A majority of collection items in the preceding code will reside in the S property.

Note

You'll also see that transformative functions such as groupBy() don't actually modify the items themselves—just the collections they're in. That's why, in the resulting object from the preceding code, each item still has its size property, despite not really being needed.

When you pass in the property name as a string, groupBy() will use a pluck style callback to grab the value of that property from each item in the collection. The unique property values form the keys of the group object. As is often the case, object properties aren't clear-cut and need to be computed at runtime. In the context of grouping items, function callbacks can be used to group collection items in cases where grouping isn't a matter of a simple comparison, as in the following code:

var collection = [ 
    { name: 'Andrea', age: 20 },    
    { name: 'Larry', age: 50 },  
    { name: 'Beverly', age: 67 },
    { name: 'Diana', age: 39 }
];

_.groupBy(collection, function(item) {
    return item.age > 65 ? 'retired' : 'working';
});
// →
// {
//   working: [
//     { name: "Andrea", age: 20 },
//     { name: "Larry", age: 50 },
//     { name: "Diana", age: 39 }
//   ],
//   retired: [
//     { name: "Beverly", age: 67 }
//   ]
// }

Rather than test for equality, this callback function tests for approximations. That is, anything greater than 65 in the age property is assumed to be retired. And we return that string as the group label. Keep in mind that it's best if these callback functions return primitive types for the keys. For any other values, the string working is returned. What's nice about these callback functions is that they can be used to quickly generate reports on the API data you're working with. The preceding example illustrates this with a one-liner callback function passed to groupBy().

Note

Although the groupBy() function will accept a where style object as the second parameter, this might not be what you're after. For example, if an item in the collection passes the test, it'll end up in the true group. Otherwise, it's a part of the false group. Be careful before going too far down the road with a pluck or where style callback—they might not do what you expect. Fiddle around and get quick results to sanity check your approach.

Counting collection items

Lo-Dash helps us find the minimum and maximum values of a collection. We might not need any help if we're working with a lot of arrays that contain only numbers. If that's the case, Math.min() is our friend. In nearly any other scenario, the min() and max() functions are the way to go, if for no other reason than the callback support. Let's take a look at the following example:

var collection = [ 
    { name: 'Douglas', age: 52, experience: 5 }, 
    { name: 'Karen', age: 36, experience: 22 },
    { name: 'Mark', age: 28, experience: 6 }, 
    { name: 'Richard', : age: 30, experience: 16 }
];

_.min(collection, 'age'),
// → { name: "Mark", age: 28, experience: 6 }

_.max(collection, function(item) {
    return item.age + item.experience;
});
// → { name: "Karen", age: 36, experience: 22 }

The first call is to min() and it gets a string argument—the name of the property we want the minimum value of in the collection. This uses the pluck style callback shorthand and produces concise code where you know the property you're working with. The second call in the preceding code is to max(). This function supports the same callback shorthand as min(), but here, there's no pre-existing property value for you to work with. Since what you want is the age property plus the experience property, the callback function supplied to max() computes this for us and figures out the maximum.

Note that the min() and max() functions return the actual collection item and not the minimum or maximum value. This makes sense because we're probably going to want to do something with the item itself, and not just the min/max value.

Beyond locating the minimum and maximum values of collections is finding the actual size of collections. This is easy if you're working with arrays because they already have the built-in length property. It is the same with strings. However, objects don't always have a length property. The Lo-Dash size() function tells you how many keys an object has, which is the intuitive behavior you'd expect from an object, but isn't there, by default. Take a look at the following code:

var collection = [ 
    { name: 'Gloria' },
    { name: 'Janice' },
    { name: 'Kathryn' },
    { name: 'Roger' }
];  

var first = _.first(collection);
_.size(collection); // → 4
_.size(first); // → 1
_.size(first.name); // → 6

The first call to size() returns the length of the collection. It'll look for a length property, and if the collection has one, this is the value that's returned. Since it's an array, the length property exists, and has a value of 4. This is what's returned. The first variable is an object, so it has no length property. It'll count the number of keys in the object and return this value—in this case, 1. Lastly, size() is called on a string. This has a length value of 6.

We can see from all three uses of size() that there's little guessing involved. Where the default JavaScript behavior is inconsistent and unintuitive, Lo-Dash provides a single function to address common use cases.

Flattening and compacting

Arrays can nest to arbitrary depth and sometimes contain falsey values that are of no practical use. Lo-Dash has functions to deal with both these situations. For example, a component of our UI might get passed as an array that has arrays nested inside it. But our component doesn't make use of this structure, and it's, in fact, more of a hindrance than it is helpful. We can flatten the array to extract and throw away the unnecessary structure your component does not need, as shown in the following code:

var collection = [ 
    { employer: 'Lodash', employees: [
        { name: 'Barbara' },
        { name: 'Patrick' },
        { name: 'Eugene' }
    ]}, 
    { employer: 'Backbone', employees: [
        { name: 'Patricia' },
        { name: 'Lillian' },
        { name: 'Jeremy' }
    ]}, 
    { employer: 'Underscore', employees: [
        { name: 'Timothy' },
        { name: 'Bruce' },
        { name: 'Fred' }
    ]}  
];  
var employees = _.flatten(_.pluck(collection, 'employees'));

_.filter(employees, function(employee) {
    return (/^[bp]/i).test(employee.name);
});
// → 
// [
//   { name: "Barbara" },
//   { name: "Patrick" },
//   { name: "Patricia" },
//   { name: "Bruce" }
// ]

Of course, we don't actually alter the structure of the original collection, we build a new one on the fly, better suited for the current context. In the preceding example, the collection consists of employer objects. However, our component is more concerned with the employee objects. So, the first step is to pluck those out of their objects using pluck(). This gets us an array of arrays. Because what we're actually plucking is the employee array from each employer array.

The next step is to flatten this employee array into an array of employee objects, which flatten() handles easily. The point of doing all this, which isn't really a lot, is now we have an easy structure to filter. Particularly, this code uses the flattened collection structure to filter out the employee names that start with b or p.

Note

There's another flatten function called flattenDeep(), which goes to arbitrary nested array depths to create a flattened structure. This is handy when you need to go beyond the one level of nesting that flatten() looks in. However, it's not a good idea to flatten arrays of unknown size and depth, simply due to the performance implications. There's a good chance that large array structures can lock the UI for your users.

A close cousin to flatten() is the compact() function, often used in conjunction with one another. We'll use compact() to remove the falsey values from a flattened array, to just use it on a plain array that already exists, or just to take out the falsey values before it's filtered. This is shown in the following code:

var collection = [ 
    { name: 'Sandra' },
    0,  
    { name: 'Brandon' },
    null,
    { name: 'Denise' },
    undefined,
    { name: 'Jack' }
    ];
var letters = [ 's', 'd' ],
    compact = _.compact(collection),
    result = [];

_.each(letters, function(letter) {
    result = result.concat(
        _.filter(compact, function(item) {
            return _.startsWith(item.name.toLowerCase(), letter);
        })
    );  
});
// → 
// [
//   { name: "Sandra" },
//   { name: "Denise" }
// ]

We can see that this collection has some values in it that we clearly don't want to deal with. But, the hopefully-not-so-sad reality is that doing frontend development in a dynamically-typed language with backend data means that you have no control over a lot of sanity checking. All that the preceding code does with the compact() function is remove any of the falsey values from the collection. These are things such as 0, null, and undefined. In fact, this code wouldn't even run without compacting the collection since it makes the implicit assumption about the name property being defined on each object in the collection.

Not only can compact() be used for safety purposes—removing items that violate contracts—but also for performance purposes. You'll see that the preceding code searches the collection, inside a loop. Therefore, any items removed from the collection before the outer loop is entered, the greater the performance gain.

Going back to the preceding code, there's one issue that can catch Lo-Dash programmers off guard. Let's say that we don't want anything that doesn't have a name property. Well, we're only shaving off falsey values—objects without name properties are still valid, and the compact() function lets them through. For example, {} doesn't have a name property, and neither does 2, but they're both allowed through in the previous approach. A safer approach might be to pluck then compact, as shown in the following code:

var collection = [ 
    { name: 'Sandra' },
    {}, 
    { name: 'Brandon' },
    true,
    { name: 'Denise' },
    1,  
    { name: 'Jack' }
    ];
var letters = [ 's', 'd' ],
    names = _.compact(_.pluck(collection, 'name')),
    result = [];

_.each(letters, function(letter) {
    result = result.concat(
        _.filter(names, function(name) {
            return _.startsWith(name.toLowerCase(),
                                letter);
        })  
    );  
});

Here, we're faced with a similar filtering task but with a slightly different collection. It has objects that will cause our code to fail because they don't have a name key with a string value. The quick-and-dirty workaround is to pluck the name property from all items in the collection before performing the compact() call. This will yield undefined values for objects that don't have a name property. But that's exactly what we're after, since compact() has no trouble excluding these values. Moreover, our code is actually simpler now. The caveat being, sometimes the simple approach doesn't work. Sometimes, you need the full object and not just the name. Cheat only when you can get away with it.

Validating some or all items

Sometimes, sections of our code hinge on the validity of all, or some collection items. Lo-Dash provides you with two complementary tools for the job. The every() function returns true if the callback returns true for every item in the collection. The some() function is a lazy brother of every()—it gives and returns true as soon as the callback returns true for an item, as shown in the following code:

var collection = [ 
    { name: 'Jonathan' },
    { first: 'Janet' },
    { name: 'Kevin' },
    { name: 'Ruby' }
];

if (!_.every(collection, 'name')) {
    return 'Missing name property';
}
// → "Missing name property"

This code checks every item in the collection for a name property before doing anything with it. Since one of the items is using an incorrect property name, the code will return early. The code that runs below the if statement can assume that each item has a name property.

On the other hand, we might only want to know whether any items have a necessary value. You can use this technique to greatly increase performance. For example, say that you have a loop that performs expensive operations on each collection item. You can do a preflight check, which is relatively inexpensive, to determine whether the expensive loop is worth running. An example for this is as follows:

var collection = [ 
    { name: 'Sean' },
    { name: 'Aaron' },
    { name: 'Jason' },
    { name: 'Lisa' }
];
if (_.some(collection, 'name')) {
    // Perform expensive processing...
}

If the some() call makes it all the way through the collection without any true callback return values, it means that we can skip the more expensive processing. For example, if we have a potentially large collection and we need to filter it using some nontrivial comparison operators, perhaps some function calls as well, the overhead really starts to add up. Using some() is a cheap way to avoid this heavy processing if it's needless.

Unions, intersections, and differences

The last section of this chapter looks at Lo-Dash functions that compare two or more arrays and yield a resulting array. In a way, we're combining several collections into a single collection. The union() function concatenates collections, with duplicate values removed. The intersection() function builds a collection with values common to all the provided collections. Lastly, the xor() function builds a collection that contains the differences between all provided collections. It's kind of like the inverse of intersection().

You can use the union() function when there are several overlapping collections that contain similar items—possibly the same items. Rather than iterate through each collection individually, it's easier to combine the collections, while at the same time removing duplicates, as you can see in the following code:

var css = [ 
    'Philip',
    'Donald',
    'Mark'
];  
var sass = [ 
    'Gary',
    'Michelle',
    'Philip'
];  
var less = [ 
    'Wayne',
    'Ruth',
    'Michelle'
];

_.union(css, sass, less);
// →
// [
//   "Philip",
//   "Donald",
//   "Mark",
//   "Gary",
//   "Michelle",
//   "Wayne",
//   "Ruth"
// ]

This code takes three arrays and transforms them into a single array. You can see in the resulting array that there's no overlap. That is, any items that exist in more than one of the input arrays are only included in the resulting array once. Let's see what the overlap looks like using intersection():

var css = [ 
    'Rachel',
    'Denise',
    'Ernest'
];  

var sass = [ 
    'Lisa',
    'Ernest',
    'Rachel'
];  

var less = [ 
    'Ernest',
    'Rachel',
    'William'
];

_.intersection(css, sass, less);
// → [ "Rachel", "Ernest" ]

Here, the intersection is Ernest and Rachel, since these strings exist in all three collections that were passed into intersection(). Now it's time to look at comparing the differences between two collections using xor():

var sass = [ 
    'Lisa',
    'Ernest',
    'Rachel'
];
var less = [ 
    'Ernest',
    'Rachel',
    'William'
];

return _.xor(sass, less);
// → [ "Lisa", "William" ]

Passing these two arrays to xor() will generate a new array that contains the difference between the two. In this case, the difference is Lisa and William. Everything else is the intersection.

Note

The xor() function accepts an arbitrary number of collections to compare with. Exercise caution, however, when comparing more than two collections. The most common case is to compare two collections to figure out the difference between the two. Going beyond that is venturing into set theory, and you might not get the results you'd expect.

 

Summary


This chapter introduced you to the concept of a collection and how they can be compared to arrays. Lo-Dash treats collections as an abstract concept—all JavaScript arrays are collections, but not all collections are arrays. We were introduced to the concept of iterating over collections using the tools provided by Lo-Dash—a fundamental concept in applicative programming and will be touched upon frequently throughout this book.

Collections can be filtered and items can be fetched from the collection. Lo-Dash also gives you the tools needed to transform collections into other structures you need when implementing frontend UI components.

We've been given a taste of some common themes in Lo-Dash programming—like the idea that callback functions are central to almost everything, and the various shorthands that can save on coding effort, such as the pluck and where callbacks. Now it's time to look at how Lo-Dash works with objects, and the various functions at our disposal there, which will be covered in the next chapter.

About the Author

  • Adam Boduch

    Adam Boduch has been involved in large-scale JavaScript development for nearly 10 years. Before moving to the frontend, he worked on several large-scale cloud computing products using Python and Linux. No stranger to complexity, Adam has practical experience with real-world software systems and the scaling challenges they pose. He is the author of several JavaScript and React books and is passionate about innovative user experiences and high performance.

    Browse publications by this author

Latest Reviews

(3 reviews total)
Great value for what I needed.
I have enjoyed and learned from the books i've purchased.
Great book, Lo-Dash is an essential tool for any web developer.
Book Title
Unlock this full book FREE 10 day trial
Start Free Trial