Intro
Recently, I got caught up in a discussion about Lodash and whether we really need to lean on third-party libraries as much as we do in the JavaScript world. It's clear we love our shortcuts and helpers, but it's also incredible to see how JavaScript itself has grown, turning from a basic scripting language into something much more powerful, complete with its own advanced features like array and map methods. This article is about putting Lodash side by side with vanilla JavaScript to see how they compare today. Feel free to follow along on a browser console for the Vanilla JS potion of the blog.
Deep Copy
Let's start with the concept of deep copying in JavaScript. It's a common necessity across programming tasks.
Lodash
Lodash provides a _.cloneDeep method to perform a deep copy:
const _ = require('lodash');
let obj = { a: 1, b: { c: 2 } };
let deepCopiedObj = _.cloneDeep(obj);
console.log(deepCopiedObj); // => { a: 1, b: { c: 2 } }
Vanilla Javascript
Vanilla JavaScript offers a couple of ways to achieve deep copying, each with its own considerations. Here's a look at two prevalent methods.
With modern JavaScript, you can achieve a deep copy using the structuredClone method.
let obj = { a: 1, b: { c: 2 } };
let deepCopiedObj = structuredClone(obj);
console.log(deepCopiedObj); // => { a: 1, b: { c: 2 } }
Vanilla JavaScript
Another way is to use the JSON.parse and JSON.stringify methods.
let obj = { a: 1, b: { c: 2 } };
let deepCopiedObj = JSON.parse(JSON.stringify(obj));
console.log(deepCopiedObj); // => { a: 1, b: { c: 2 } }
Note: This method works well for objects containing only primitive values, arrays, and plain objects but might not be suitable for objects with methods, circular references, or special types like Date, Set, Map, etc. I honestly hate seeing this, but I know it's prevalent in many code bases i and can attest to it working, so I have included it in this article.
Difference
Finding the difference between two arrays is a common task, where we identify elements present in one array but not in another.
Lodash
_.difference([2, 1], [2, 3]); // => [1]
Vanilla JavaScript
let firstArr = [1,2,3];
let secondArr = [2,4];
let difference = firstArr.filter(x => !secondArr.includes(x));
console.log(difference); // => [1, 3]
DifferenceBy
DifferenceBy introduces a twist to finding array differences by allowing comparisons based on a transformation function.
Lodash
_.differenceBy([2.1, 1.2], [2.3, 3.4], Math.floor); // => [1.2]
Vanilla JavaScript
let firstArr = [1,2,3, 4.2];
let secondArr = [2,4];
// Various approaches but they get more funtional
let difference = firstArr.filter(x => !secondArr.includes(Math.floor(x)));
console.log("difference:" + difference); // => [1, 3]
const diffBy = element => Math.floor(element);
let differenceWithFunc = firstArr.filter(element => !secondArr.includes(diffBy(element)));
console.log("differenceWithFunc:" + differenceWithFunc); // => [1, 3]
const transform = element => Math.floor(element);
const notIncluded = (element, transformFunc, secondArray) => !secondArray.includes(transformFunc(element));
let differenceEncapsulationVersion = firstArr.filter(element => notIncluded(element, transform, secondArr));
console.log("differenceEncapsulationVersion:" + differenceEncapsulationVersion); // => [1, 3]
Union
Union operations merge multiple arrays into a single array with unique elements. This common functionality showcases another aspect of array manipulation.
Lodash
_.union([2], [1, 2]); // => [2, 1]
Vanilla JavaScript
Using ES6's Set and spread operator:
let firstArr = [2];
let secondArr = [1,2];
let thirdArr = [2,3,4];
let union = new Set([...firstArr, ...secondArr, ...thirdArr]);
console.log([...union]); // => [2, 1, 3, 4]
Zip
Zipping arrays is a powerful method to combine arrays element-wise.
Lodash
_.zip(['a', 'b'], [1, 2], [true, false]); // => [['a', 1, true], ['b', 2, false]]
Vanilla JavaScript
Using Array.from, we can zip arrays of varying lengths:
function zip(...arrays) {
const maxLength = Math.max(...arrays.map(arr => arr.length));
return Array.from({ length: maxLength }, (_, index) => arrays.map(array => array[index]));
}
let abc = ['a', 'b', 'c'];
let num = [1, 2, 3];
let bool = [true, false, true];
let zipped = zip(abc, num, bool);
console.log(zipped);
A more functional approach:
const zip = (...arrays) => arrays[0].map((_, i) => arrays.map(array => array[i]));
let abc = ['a', 'b', 'c'];
let num = [1, 2, 3];
let bool = [true, false, true];
let zipped = zip(abc, num, bool);
console.log(zipped); // => [['a', 1, true], ['b', 2, false], ['c', 3, true]]
Currying
Currying transforms a function, so it can be invoked piecewise, with each call returning a new function that accepts the next argument.
Lodash
var abc = function(a, b, c) {
return [a, b, c];
};
var curried = _.curry(abc);
curried(1)(2)(3); // => [1, 2, 3]
curried(1, 2)(3); // => [1, 2, 3]
curried(1, 2, 3); // => [1, 2, 3]
curried(1)(_, 3)(2); // => [1, 3, 2]
Vanilla JavaScript
implementing currying with support for placeholders:
const _ = Symbol('_');
function curry(fn) {
return function currier(...args) {
const isComplete = args.length >= fn.length && !args.slice(0, fn.length).includes(_);
if (isComplete) {
return fn.apply(this, args);
} else {
return function(...newArgs) {
const combinedArgs = args.reduce((acc, arg) => {
if (arg === _ && newArgs.length) acc.push(newArgs.shift());
else acc.push(arg);
return acc;
}, []);
return currier(...combinedArgs, ...newArgs);
};
}
};
}
const abc = (a, b, c) => [a, b, c];
const curried = curry(abc);
console.log(curried(1)(2)(3)); // => [1, 2, 3]
let unfinishedCurry = curried(1,2);
console.log(unfinishedCurry(3)); // => [1, 2, 3]
console.log(curried(1, 2)(3)); // => [1, 2, 3]
console.log(curried(1)(_, 3)(2)); // => [1, 3, 2]
console.log(curried(1, _, _)(2)(3)); // => [1, 2, 3]
Conclusion
There's an argument to be made for using only specific parts of Lodash to minimize the footprint, yet the essence of my argument remains: As developers, our aim should be to deeply understand the functions we employ. Relying heavily on abstractions can, if not careful, erode our foundational skills. This isn't to undermine Lodash's value; on the contrary, I hope this article highlights its excellence. Lodash has played a pivotal role by providing robust functionalities during a time when such features were lacking in native browser environments.
Currently, I find myself not relying on Lodash as much, though I remain open to the possibility that it might offer indispensable solutions to complex problems in the future. My journey through this exploration was not just to question commonly held beliefs but to deepen my own grasp of JavaScript and to hone my skills. This reflection on JavaScript's growth celebrates Lodash's contribution to easing software development and acknowledges the language's continuous, albeit occasionally contentious, evolution. The path JavaScript is on is filled with promise and excitement for what's yet to come.
Top comments (0)