DEV Community

Cover image for JavaScript evolution: From Lodash and Underscore to vanilla
Megan Lee for LogRocket

Posted on • Originally published at blog.logrocket.com

JavaScript evolution: From Lodash and Underscore to vanilla

Written by Kapeel Kokane✏️

Imagine this scenario – you’re planning to start a new project. Regardless of whether it’s a frontend project or a backend repo, what’s the first thing you do?

Well, first you look for a starter kit or a boilerplate that can help you get started quickly instead of doing all the heavy lifting. For our example, let’s say you are starting a new frontend project.

In that case, you use Next.js because who uses create-react-app anymore? Not even the official React docs recommend it. What next? You install Tailwind CSS right away, because who wants to write CSS from scratch? Then comes state management. You can use Redux, MobX, Recoil, or even the new kid on the block, Zustand. I would do the same thing.

But what do we do next? We usually install a utility package that can help us with some common tasks. Who wants to write their mapping, sorting functions, debounce utilities, or even their deep clone methods?

But which utility package should we use? There are so many options: Lodash, Underscore, Ramda, etc. The two most popular ones are Lodash and Underscore, which is what we’ll explore today. We’ll compare their functionalities and explore whether these packages are even necessary these days.

Comparing Lodash and Underscore

Underscore was created by Jeremy Ashkenas (the creator of Backbone.js) in 2009 to provide a set of utility functions that JavaScript lacked at the time. It was also created to work with Backbone.js, but it slowly became a favorite among developers who needed utility functions that they could just call and get stuff done with without having to worry about the inner implementations and browser compatibility.

Lodash was created by John-David Dalton in 2012 as a fork of Underscore. It was created to provide a more consistent API and better performance. It also provided some additional utilities that are not a part of Underscore.

Bundle sizes

If we compare the npm repositories for both packages, we can see that Lodash has a larger bundle size (1.41 MB) compared to Underscore (906kB). This means that when used in an npm project, Lodash would take an additional 500 kilobytes of network bandwidth while installing the node modules. But, with that additional size, we get some extra features. Let’s talk about them next.

Functionalities

Lodash provides some additional capabilities when compared to Underscore, including:

  • _.clone-- Used for deep cloning objects
  • _.merge-- Can be used to merge two objects with common keys
  • _.set -- Sets a value for any path we want

Apart from that, Lodash also provides some additional string utilities like [_.kebabCase](https://lodash.com/docs/4.17.15#kebabCase) and [_.camelCase](https://lodash.com/docs/4.17.15#camelCase), that convert any string supplied to the particular case styles. There is also a [_.capitalize](https://lodash.com/docs/4.17.15#capitalize) method that capitalizes the first letter of any string.

Here are a few examples using these utilities that are currently not possible to achieve with Underscore:

// _.clone example
const user = {
  name: 'John Doe',
  age: 30,
  email: 'test@gmail.com',
  address: {
    city: 'New York',
    country: {
      code: 'US',
      name: 'United States'
    }
  }
};
const clonedUser = _.clone(user);
// _.set example
const user = {
  name: 'John Doe',
  age: 30,
};
_.set(user, 'address.city', 'New York');
_.set(user, 'address.country.code', 'US');
_.kebabCase('Hello World'); // hello-world
_.camelCase('Hello World'); // helloWorld
_.capitalize('hello world'); // Hello world
Enter fullscreen mode Exit fullscreen mode

We’ll look at the merge example later in the article when we look at vanilla JS alternatives of common utility methods.

Popularity

If we look at the npm downloads, Lodash has more downloads at 70.3 million per week compared to Underscore, which has far less, at 14.5 million per week. This makes sense because once Lodash was available, people chose the "more recent" version of the utility package compared to the older one, which automatically became "stale.”

Do we even need utility packages?

Now, let’s look at the main use case for which these libraries were initially created – utility functions. In the past, the JavaScript spec lacked some basic functionality. And even if it did introduce new capabilities by adding them to the spec, they were not implemented by all browsers. Let’s look at some of them.

Filtering an array based on a condition

The Array.prototype.filter method was introduced in ES5. It creates a new array with all elements that pass the test implemented by the provided function. However, it only became generally available in all major browsers in 2015. Before that, if you wanted to write functional code to filter an array, you would have to use the utility methods from Underscore or Lodash like so:

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const evenNumbers = _.filter(numbers, (number) => number % 2 === 0);
Enter fullscreen mode Exit fullscreen mode

But after ES2015, it can easily be implemented in vanilla JavaScript like so:

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const evenNumbers = numbers.filter((number) => number % 2 === 0);
Enter fullscreen mode Exit fullscreen mode

Reducing an array down to a single result

The same case happened with the Array.prototype.reduce method. Before 2015, if you wanted to write functional code to reduce an array, you would have to use the utility methods from Underscore or Lodash like so:

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const sum = _.reduce(numbers, (acc, number) => acc + number, 0);
Enter fullscreen mode Exit fullscreen mode

After ES2015, it can easily be implemented in vanilla JavaScript:

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const sum = numbers.reduce((acc, number) => acc + number, 0);
Enter fullscreen mode Exit fullscreen mode

Performing an operation on every array element

Let’s now look into the Array.prototype.forEach method. If we wanted to perform an operation on each element of an array, we would have to use the for loop or the utility methods from Underscore or Lodash like so:

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
_.forEach(numbers, (number) => console.log(number));
Enter fullscreen mode Exit fullscreen mode

But, after ES2015, it can be implemented in vanilla JavaScript like this:

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
numbers.forEach((number) => console.log(number));
Enter fullscreen mode Exit fullscreen mode

Checking whether a variable is an array

If we wanted to check if a variable is an array, we would have to use the utility methods from Underscore or Lodash like so:

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const isNumbersArray = _.isArray(numbers);
Enter fullscreen mode Exit fullscreen mode

With ES5, we can easily implement it in vanilla JavaScript like so:

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const isNumbersArray = Array.isArray(numbers);
Enter fullscreen mode Exit fullscreen mode

Picking properties from an object

Now let’s look at some object operations. If we wanted to pick some keys from an object, we would have to use the utility methods from Underscore or Lodash like so:

const user = {
  name: 'John Doe',
  age: 30,
  email: '
};
const pickedUser = _.pick(user, ['name', 'age']);
Enter fullscreen mode Exit fullscreen mode

This creates a new object by using the keys from the user object, which, in this case, would be name and age. But, from ES5 onwards, we can easily implement it in vanilla JavaScript like so:

const user = {
  name: 'John Doe',
  age: 30,
  email: 'test@gmail.com'
};
const pickedUser = Object.fromEntries(Object.entries(user).filter(([key]) => ['name', 'age'].includes(key)));
Enter fullscreen mode Exit fullscreen mode

Merging two objects

If we wanted to merge two objects so that the new object would have the superset of keys from the two objects, we would have to use the utility methods from Lodash like so:

const user = {
  name: 'John Doe',
  age: 30,
  email: 'test@gmail.com'
}
const newUser = {
  age: 31
}
const mergedUser = _.merge({}, user, newUser);
Enter fullscreen mode Exit fullscreen mode

Note that Underscore doesn’t have this functionality, but in ES5, we can easily implement it in vanilla JavaScript using the spread operator like so:

const mergedUser = {...user, ...newUser};
Enter fullscreen mode Exit fullscreen mode

We could also use the [Object.assign](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) method like so:

const mergedUser = Object.assign({}, user, newUser);
Enter fullscreen mode Exit fullscreen mode

Deep cloning an object

If we wanted to deep clone an object, we would have to use the utility methods from Underscore or Lodash like so:

const user = {
  name: 'John Doe',
  age: 30,
  email: 'test@gmail.com'
};
const clonedUser = _.cloneDeep(user);
Enter fullscreen mode Exit fullscreen mode

But, modern JavaScript has JSON utilities like parse and stringify can be used to deep clone an object like so:

const clonedUser = JSON.parse(JSON.stringify(user));
Enter fullscreen mode Exit fullscreen mode

Now, let's move on to some more complex use cases.

Throttling a function call

If we wanted to throttle a function, which is to limit the number of function executions to just one in a given timeframe, we would have to use the utility methods from Underscore or Lodash like so:

const throttledFunction = _.throttle(() => console.log('Throttled function'), 1000);
Enter fullscreen mode Exit fullscreen mode

In this case, the function would get executed only once in a second, i.e., 1000 milliseconds.

The same can be implemented in vanilla JavaScript like so:

function throttle(func, delay) {
  let lastTime = 0;

  return function() {
    const now = Date.now();
    if (now - lastTime >= delay) {
      lastTime = now;
      func.apply(this, arguments);
    }
  };
}

const throttledFunction = throttle(() => console.log('Throttled function'), 1000);
Enter fullscreen mode Exit fullscreen mode

Debouncing a function call

If we wanted to debounce a function, which is to stop the execution of a function for a particular time since the last function execution, we would have to use the utility methods from Underscore or Lodash like so:

const debouncedFunction = _.debounce(() => console.log('Debounced function'), 1000);
Enter fullscreen mode Exit fullscreen mode

But the same can be implemented in vanilla JavaScript like so:

function debounce(func, delay) {
  let timeoutId;

  return function() {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      func.apply(this, arguments);
    }, delay);
  };
}

const debouncedFunction = debounce(() => console.log('Debounced function'), 1000);
Enter fullscreen mode Exit fullscreen mode

Conclusion

So, do we even need utility libraries like Lodash or Underscore? Not in the year 2024.

With the introduction of ES2015 and later versions of JavaScript, the language has come a long way. It has introduced a lot of utility methods that can be used to perform common tasks. And even if you need some additional functionalities, you can easily implement them in vanilla JavaScript using the latest features of the language as we did with the spread operator in this article.

So, the next time you find yourself reaching out for Lodash or Underscore while setting up a new project, think again. Save yourself some network bandwidth (from the npm module) and write your own utility functions. Not only will it help you understand the language better but it’ll also make you a better developer.


Are you adding new JS libraries to build new features or improve performance? What if they’re doing the opposite?

There’s no doubt that frontends are getting more complex. As you add new JavaScript libraries and other dependencies to your app, you’ll need more visibility to ensure your users don’t run into unknown issues.

LogRocket is a frontend application monitoring solution that lets you replay JavaScript errors as if they happened in your own browser so you can react to bugs more effectively.

LogRocket with JS Libraries Demo

LogRocket works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Build confidently — start monitoring for free.

Top comments (0)