Table of Contents
1. Introduction
This book is about the popular JavaScript utilities library lodash. Before we discuss lodash, we should understand why we need a JavaScript utilities library. With the prevalence of Web 2.0, Ajax and NodeJS, JavaScript has become a very important programming language in both browser-side and server-side. Besides the bad parts1 of JavaScript language, JavaScript itself doesn’t have a rich set of high-level API for developers to use, which makes common programming tasks hard to complete.
For example, it’s a very common task to iterate an array and process all elements in this array in sequence. In some old browsers, the JavaScript Array
object doesn’t have the method forEach()
. To iterate an array, for
loop is required as in Listing 1.1. process
is the function to process elements in the array.
When using the method forEach()
, the code in Listing 1.1 can be simplified as in Listing 1.2.
Comparing code snippets in Listing 1.1 and Listing 1.2, it’s obvious that Listing 1.2 is much simpler to understand and easier to write and maintain than the code in Listing 1.1. That’s why developers want more high-level APIs. JavaScript itself is evolving to add more language features and APIs, but the process is not fast enough. ECMAScript, the specification behind JavaScript, includes nine new methods for searching and manipulating array contents in 5th edition. This means developers can use the method forEach()
when the JavaScript engine supports ECMAScript 5. But some old browsers, like IE 8, don’t support ECMAScript 5, which means developers need to consider cross-platform compatibility issues if supporting old browsers is a must. ECMAScript 6 specification was published in June 2015 with a lot of new features and enhancements.
Developers rely on JavaScript libraries to make daily development easier. The goal of libraries is to become the bridge between JavaScript runtime and developers. Developers can enjoy the high-level APIs provided by those libraries. Libraries are responsible for handling implementation details about how to use the low-level JavaScript APIs efficiently.
You may have heard about or even used another JavaScript utilities library Underscore. Underscore provides a rich set of common APIs in the namespace _
. Lodash also uses namespace _
and it’s a drop-in replacement of Underscore with more features and performance improvements. If you already use Underscore, you can simply replace the Underscore with lodash, everything should just work.
This book is for the latest lodash 4.17.21 version.
1.1 Installation
Lodash is just a plain old JavaScript library, so it’s very easy to install and use.
1.1.1 Web
In a web application, we can just download the lodash release JavaScript file and include it in the HTML page, then use _
in the JavaScript code.
We can also use links provided by CDN servers to load lodash. CDN servers usually have different versions of lodash to choose from. Listing 1.4 shows how to use cdnjs to load lodash. cdnjs also provides the minified JavaScript version with source mapping file.
1.1.2 NodeJS
In NodeJS, we can install lodash using npm or yarn; see Listing 1.5 and Listing 1.6.
Then we can use require
to import lodash package, see Listing 1.7.
It’s recommended to only install NodeJS modules of actually used modules. For example, if the code only uses _.forEach
method, then install the lodash.foreach
module only.
1.2 Lodash features
Lodash focuses on providing core features that are frequently used for JavaScript built-in objects, including:
- Arrays
- Objects
- Functions
- Strings
Some of those features may have been included in the latest version of ECMAScript specification. Some platforms may have also implemented extra features. If the underlying platform already supports a certain feature, lodash just uses the native implementation to improve performance.
1.3 Code sample convention
All code samples in this book are written in ECMAScript 6 JavaScript syntax and tested on NodeJS 6.9.4. Most of the code is written as Jest test cases to verify the result. For those code that are not written as Jest code, the result of execution is provided below the actual code as a comment; see Listing 1.9.
As in the Listing 1.9 above, _.min([1, 2, 3]);
is the actual code, 1
after // ->
is the execution result.
The complete source code of this book can be found on GitHub.
1.4 About this book
Lodash is a well-documented JavaScript library with comprehensive official documentation. This book is a simple and concise guide on how to use lodash in practice. It covers core features and most frequently used functions.
2. Common concepts
Before diving into details of lodash functions, we start from some common concepts in lodash.
2.1 Truthy and falsy
Truthy and falsy values are very important when using lodash predicates. false
, 0
, ""
(empty string), null
, undefined
and NaN
are falsy values in JavaScript. All other values are truthy values.
2.2 SameValueZero
SameValueZero is the algorithm of how to compare two values in lodash. It’s similar to JavaScript “strict equality comparison” (===
), except the handling of NaN
. It always makes developers confused as NaN === NaN
returns false
. SameValueZero removes that confusion, so NaN
is the same to NaN
in SameValueZero algorithm.
2.3 Predicates
Predicate functions only return truthy or falsy values. They are used frequently in lodash. For example, when filtering a collection, a predicate function is required to determine what kind of elements should be kept.
Predicate functions can be written as plain old JavaScript functions. Lodash also provides some helper functions to generate predicate functions for common use cases.
2.3.1 matches
_.matches(source)
takes a source object and creates a new function which performs a deep comparison between the given object and the source object. _.matches
supports comparison of different types of data, including booleans, numbers, strings, Date
objects, RegExp
objects, Object
objects and arrays. Listing 2.1 shows how _.matches
works by comparing strings and objects.
2.3.2 matchesProperty
_.matchesProperty(path, value)
takes a property path and the expected value of this property path to create a new function that checks if the given object’s value of the same property path matches the expected value. Listing 2.2 shows how _.matchesProperty
works by matching simple property name, built-in property and nested property path.
2.3.3 property
_.property(path)
takes a property path and creates a new function which returns the value of this property path in the given object. _.property
can be used to create predicate functions with property values converted to truthy or falsy values.
For lodash functions which accept predicates, e.g. _.find
and _.filter
, predicates can be specified using functions, strings, and objects.
- If a function is provided, it’s used directly. The predicate matches if the function returns a truthy value.
- If only a string is provided, it’s used to create a function using
_.property
as the predicate. - If an array that contains a string and a value is provided, the string and the value are used to create a function using
_matchesProperty
as the predicate. - If an object is provided, it’s used to create a function using
_.matches
as the predicate.
For example, given an array shown in Listing 2.4,
_.find
returns the first matching element in the array. A JavaScript function can be used as the predicate to _.find
. In Listing 2.5, we find the first element with age
greater than 18
in the array users
. The result is the first element with the name Alex
.
If a string is passed as the predicate, it’s treated as a property name of objects in the array. In Listing 2.6, we find the first element with truthy value of the property is_premium
in the array. The actual used predicate is _.property('is_premium')
. The result is the second element with the name Bob
.
If an object is passed as the predicate, it’s treated as a search example. Objects in returned results must have exactly the same values for all the corresponding properties provided in the search example. In Listing 2.7, we find the first element with the value of the property name
equals to Alex
in the array. The actual used predicate is _.matches({ name: 'Alex' })
.
2.4 Iteratees
Iteratees are used by lodash functions which require iterating through a collection. Iteratee is invoked for each element in the collection and the result is used instead of the original element. Iteratees are typically used to transform collections. A typical usage of iteratee is in the function _.map
. The second argument of _.map
is the iteratee. The result of applying iteratee to each element in the collection is collected and returned. In Listing 2.8, we use a function to transform input array [1, 2, 3]
to [3, 6, 9]
.
2.4.1 Iteratee shorthand
When iteratee functions are required, we can also use the similar syntax as predicate functions to quickly create them. These iteratee shorthands use methods _.matches
, _.matchesProperty
or _.property
behind the scene.
In the second invocation of _.map
in Listing 2.9, the second argument of _.map
must be an array to indicate that it uses _.matchesProperty
.
2.5 this
binding
In Lodash 3, we can use the argument thisArg
to specify the value of this
binding. In Lodash 4, thisArg
has been removed in most methods. To specify the binding object, _.bind
should be used explicitly. In Listing 2.10, when the function add
is invoked, this
value is bound to obj
.
3. Collections
A collection is an object that contains iterable elements. In lodash, collections can be arrays, objects, and strings. Lodash has a rich set of functions to work with collections.
In this chapter, we use the following JSON array as the sample data fruits
for some code samples.
3.1 Each
_.each(collection, [iteratee=_.identity])
and _.eachRight(collection, [iteratee=_.identity])
iterate over elements in the collection and invoke the iteratee function. The difference is that _.eachRight
iterates from right to left. _.forEach
is an alias of _.each
, while _.forEachRight
is an alias of _.eachRight
.
3.2 Every and some
_.every(collection, [predicate=_.identity])
checks if all elements in the collection match the given predicate.
_.some(collection, [predicate=_.identity])
is the opposite of _.every
which checks if any element in the collection matches the given predicate. _.some
doesn’t need to iterate the entire collection and the iteration exits as soon as a matching element is found.
3.3 Filter and reject
_.filter(collection, [predicate=_.identity])
filters a collection by returning elements matching the given predicate. _.reject(collection, [predicate=_.identity])
is the opposite of _.filter
that returns elements not matching the given predicate. When _.filter
is used to filter objects, only values of matching properties are returned. If you want to keep the original object structure, use _.pick
or _.omit
instead. When _.filter
is used on strings, matching characters are returned in an array.
Listing 3.6 shows the examples of _.reject
.
3.4 Size
_.size(collection)
gets the size of a collection. For arrays, the size is the array’s length, same as the array’s property length
. For objects, the size is the number of own enumerable properties, i.e. the length of the array returned by _.keys
. For strings, the size is the string’s length.
3.5 Includes
_.includes(collection, value, [fromIndex=0])
checks if a collection contains the given value. An optional index can be provided as the starting position to search. If the collection is an object, values of this object’s properties, i.e. the result of _.values
, are searched instead. _.includes
uses the SameAsZero
algorithm to check equality.
3.6 Sample
_.sample(collection)
gets a single random element from a collection. _.sampleSize(collection, [n=1])
gets n
random elements with unique keys from a collection.
3.7 Shuffle
_.shuffle(collection)
shuffles a collection by generating a random permutation. Lodash uses the Fisher-Yates shuffle algorithm to shuffle the collection. For objects, the return value of _.shuffle
is a random permutation of the property values.
3.8 Partition
_.partition(collection, [predicate=_.identity])
splits a collection into two groups based on the result of invoking the predicate on each element. The first group contains elements for which the predicate returns a truthy value, while the second group contains elements for which the predicate returns a falsy value.
3.9 Count by
_.countBy(collection, [iteratee=_.identity])
applies a function to each element in the collection and counts the number of occurrences of each result. The counting result is returned as an object with the applied result as the keys and the count as the corresponding values.
3.10 Group by and key by
_.groupBy(collection, [iteratee=_.identity])
applies a function to each element in the collection and groups the elements by the result. Elements that have the same result will be in the same group. The grouping result is returned as an object. The keys in the object are the applied results, while the values are arrays of elements which generate the corresponding result.
The difference between _.countBy
and _.groupBy
is that _.countBy
only returns the number of grouped elements.
_.keyBy(collection, [iteratee=_.identity])
’s behavior is similar to _.groupBy
, but _.keyBy
only keeps the last element for each key.
3.11 invokeMap
_.invokeMap(collection, path, [args])
invokes a method on each element in the collection and returns the results in an array. The method to invoke is specified by the path, can be the function’s name or the function itself. Additional arguments can also be provided for the method invocation. In Listing 3.14, when the function is invoked, this
references the current element.
3.12 Map and reduce
Map and reduce are common operations when processing collections. Map transforms a collection into another collection by applying an operation to each element in the collection. Reduce transforms a collection into a single value by accumulating results of applying an operation to each element. The result of the last operation is used as the input of the current operation.
3.12.1 Map
_.map(collection, [iteratee=_.identity])
is the generic map function. We can use the different iteratee syntax.
3.12.2 Reduce
_.reduce(collection, [iteratee=_.identity], [accumulator])
has similar arguments list with _.map
, except that it accepts an optional value as the initial input of the first reduce operation. If the initial value is not provided, the first element in the collection is used instead. The provided iteratee function will be invoked with four arguments, accumulator
, value
, index
/key
and collection
. accumulator
is the current reduced value, while value
is the current element in the collection. The returned result of the iteratee function invocation is passed as the accumulator
value of the next invocation.
_.reduceRight(collection, [iteratee=_.identity], [accumulator]
is similar to_.reduce)
except _.reduceRight
iterates all the elements from right to left.
3.13 Search
Search is a very common task in programming. Search is performed on iterable collections with given conditions. The return result is the first element in the collection matching the condition, or undefined
if no matching element is found.
3.13.1 find
_.find(collection, [predicate=_.identity], [fromIndex=0])
is the generic function to search in collections. When invoking _.find
, the collection itself and the search condition should be provided. We can also provide an optional starting index for the search. _.find
supports the same predicate syntax. If a function is provided as the predicate, the function is invoked for each element in the array until the function returns a truthy value. The function is invoked with three arguments: the currently iterated element, index or key of the element and the collection itself.
3.13.2 findLast
_.findLast(collection, [predicate=_.identity], [fromIndex=collection.length-1])
is similar to _.find
, but _.findLast
iterates over all elements of the collection in reverse order. For arrays, it searches from the last element. For strings, it searches from the last character. For objects, it searches from the last element of the array of property names returned by _.keys
.
3.14 Sort
_.sortBy(collection, [iteratee=_.identity])
sorts a collection in ascending order with results after applying the iteratee function to each element in the collection. The sort is stable, which means it preserves original order for elements with equality. We can use multiple iteratees as sort conditions. If multiple elements in the collection have the same value for the first property name, those elements are sorted using the second property name, and so on.
3.15 flatMap
_.flatMap(collection, [iteratee=_.identity])
invokes an iteratee function to each element in a collection. The result of each iteratee function invocation is an array. All result arrays are concatenated and flattened into a single array as the final result.
_.flatMapDeep(collection, [iteratee=_.identity])
is similar to _.flatMap
except that _.flatMapDeep
recursively flattens the result array until it’s completely flattened.
_.flatMapDepth(collection, [iteratee=_.identity], [depth=1])
is similar to _.flatMapDeep
except that it only flattens the result at the given times. The default value of depth
is 1
, so _.flatMapDepth(array, iteratee)
is the same as _.flatMap(array, iteratee)
.
4. String Templates
If you want to generate strings from a template, _.template([string=''], [options={}])
is a simple yet powerful function to do that. It can be used by libraries and applications to avoid long string concatenations. Grunt uses _.template
to support templates in configuration files. It can also be used in applications to generate HTML markups, messages, emails and more.
In Listing 10.1, the input argument of _.template
is the template itself. In the template, <%=
and %>
are the delimiters to wrap variables to be evaluated at runtime. _.template
returns a new function. After invoking the returned function with a context object that contains actual values of template variables, it returns the generated string. If no value is assigned to a variable, an empty string will be used.
_.template
supports three types of delimiters, interpolate, escape and evaluate.
4.1 interpolate
interpolate delimiters allow to interpolate variables. The default regular expression pattern to declare interpolated variables is /<%=([\s\S]+?)%>/g
. Simple variables and complex expressions are both supported. The regular expression pattern of interpolate delimiters can be customized by the property interpolate
of the options object.
4.2 escape
It’s common to use _.template
to generate HTML markups. escape delimiters allow to interpolate variables and escape the result values. The default regular expression pattern to declare escaped variables is /<%-([\s\S]+?)%>/g
. The pattern can be customized by the property escape
of the options object.
4.3 evaluate
evaluate delimiters allow executions of JavaScript code. This kind of delimiters is useful when adding logic to templates, e.g. adding condition checks or loops to the template. The default regular expression pattern to declare JavaScript code is /<%([\s\S]+?)%>/g
. The pattern can be customized by the property evaluate
of the options object.
4.4 imports
Besides from the context object passed to the function created by _.template
, a default object can also be passed to _.template
as an additional source when evaluating variables. The object is specified using the property imports
of the options object. The default values in the imported object can be overridden by values in the context object.
The default imports object contains only the lodash object itself with the key _
, so lodash methods can be used directly in the expressions.
4.5 Data object name
By default, when evaluating variables in the template, variables use the same names as in the context object. The property variable
of the options object sets a name to the context object, then variable names should be changed accordingly.
In Listing 10.6, the context object’s name is set to user
, so user.name
accesses the property name
of the context object.
5. Recipes
This chapter gives some recipes about how to do common tasks using Lodash.
5.1 Filter an object’s properties
5.1.1 Scenario
Filter a given object by removing certain properties.
5.1.2 Solution
Although _.filter
and _.reject
can be applied to objects, they cannot be used for this scenario, because _.filter
and _.reject
return an array of property values after filtering. _.pick
, _.pickBy
, _.omit
and _.omitBy
should be used instead.
When a predicate function is passed to _.pickBy
or _.omitBy
, it’s invoked with three arguments: property value, property name and the object itself.
5.2 Push an array of elements into an array
5.2.1 Scenario
Given an array of elements, push those elements into another array.
5.2.2 Solution
If using Array
’s push
method, the whole array will be pushed as a single element.
The first solution is to use _.spread
to wrap the push
method to accept arrays as arguments.
The second solution is to push the array first, then use _.flatten
to flatten the array.
5.3 Process data for C3.js pie chart
5.3.1 Scenario
C3.js is a popular chart library based on d3.js. C3.js can create pie chart based on data input. But when there are many items in the data set, the pie chart itself becomes very hard to read.
Listing 11.5 is the basic code to create a pie chart with 100
items.
Below is how this chart looks like.
5.3.2 Solution
One solution is to process the data set first by limiting the number of items. For example, we can only get top 20
items from the data set and all the rest items are summed into a new item called Others
. By doing this, the created chart will be more readable.
In Listing 11.6, use _.sortBy
to sort the data
array first based on the second element of the item array. Items in the data
array are all arrays, [1]
can be used to access the second element in the item array. After the data
array is sorted, use _.take
to find the top 20
items in the sorted array, then use _.last
to find the last one. This last item is used as the threshold to partition the data
array. Then we use partition
to divide the data
array into two groups. The first group groups[0]
contains items we want to keep, the second group groups[1]
contains items we want to merge. For the second group, we use _.sum
to calculate the sum of all the items to merge. The merged item is pushed to the result data array with name Others
and sum.
After using Listing 11.6 code to process the data first, the chart is much easier to read, see below.
5.4 Create a unique array of objects
5.4.1 Scenario
Given an array of objects in Listing 11.7, remove duplicate values from the array.
5.4.2 Solution
_.uniq
and uniqBy
functions can be used to remove duplicate values from an array, but it only uses SameAsZero algorithm to compare values. To perform the deep comparison for elements in the array of Listing 11.7, we need to convert each element into a single value. For example, if the property name
is the unique key for each element, use _.uniqBy(array, 'name')
. If there is no unique key, you can convert the element into a JSON string.
JSON serialization may generate different results for objects with the same value due to the undermined property enumeration order. For a more consistent result, we should create our own object serialization format. In Listing 11.9, we concatenate name
and age
properties as the serialization format to determine uniqueness.
5.5 Convert an array to an object
5.5.1 Scenario
Given an array of objects with IDs, convert the array to an object with IDs as the keys and array elements as the values.
For example, given an array in Listing 11.10, convert it to an object shown in Listing 11.11.
5.5.2 Solution
One solution is to use _.each
to iterate the array and set each property in the result object, see Listing 11.12.
A better solution is to use _.reduce
, see Listing 11.13.
6. Thank you
Thank you for reading sample chapters of this book. You can purchase the complete book at Leanpub.
Notes
1Find out the good parts of JavaScript in Douglas Crockford’s excellent book JavaScript: The Good Parts↩