You don't need Lodash
Let me start with this: Lodash is a great library.
It is a library I have used a bunch of times. It has some of the best documentation for the most widly used functions in JavaScript, and it can make things a lot easier.
The real issue I have with lodash is the bloat.
Lodash complexity
For example, take a look at lodash’s groupBy function. I have extracted some of the code below, but if you want to see the full thing you can find it on npm.
It is 64kB and 2370 lines long! Now to be fair that includes comments, but still…
/* Here is a small subset of the code */
function objectToString(value) {
return Object.prototype.toString.call(value);
}
function getRawTag(value) {
var isOwn = hasOwnProperty.call(value, symToStringTag),
tag = value[symToStringTag];
try {
value[symToStringTag] = undefined;
var unmasked = true;
} catch (e) {}
var result = nativeObjectToString.call(value);
if (unmasked) {
if (isOwn) {
value[symToStringTag] = tag;
} else {
delete value[symToStringTag];
}
}
return result;
}
function baseGetTag(value) {
if (value == null) {
return value === undefined ? undefinedTag : nullTag;
}
return (symToStringTag && symToStringTag in Object(value))
? getRawTag(value)
: objectToString(value);
}
function isObject(value) {
var type = typeof value;
return value != null && (type == 'object' || type == 'function');
}
function isFunction(value) {
if (!isObject(value)) {
return false;
}
var tag = baseGetTag(value);
return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag;
}
function isLength(value) {
return typeof value == 'number' &&
value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER;
}
function isArrayLike(value) {
return value != null && isLength(value.length) && !isFunction(value);
}
function createBaseEach(eachFunc, fromRight) {
return function(collection, iteratee) {
if (collection == null) {
return collection;
}
if (!isArrayLike(collection)) {
return eachFunc(collection, iteratee);
}
var length = collection.length,
index = fromRight ? length : -1,
iterable = Object(collection);
while ((fromRight ? index-- : ++index < length)) {
if (iteratee(iterable[index], index, iterable) === false) {
break;
}
}
return collection;
};
}
var baseEach = createBaseEach(baseForOwn);
function arrayAggregator(array, setter, iteratee, accumulator) {
var index = -1,
length = array == null ? 0 : array.length;
while (++index < length) {
var value = array[index];
setter(accumulator, value, iteratee(value), array);
}
return accumulator;
}
function baseAggregator(collection, setter, iteratee, accumulator) {
baseEach(collection, function(value, key, collection) {
setter(accumulator, value, iteratee(value), collection);
});
return accumulator;
}
function getIteratee() {
var result = lodash.iteratee || iteratee;
result = result === iteratee ? baseIteratee : result;
return arguments.length ? result(arguments[0], arguments[1]) : result;
}
function createAggregator(setter, initializer) {
return function(collection, iteratee) {
var func = Array.isArray(collection) ? arrayAggregator : baseAggregator,
accumulator = initializer ? initializer() : {};
return func(collection, setter, getIteratee(iteratee, 2), accumulator);
};
}
function baseAssignValue(object, key, value) {
if (key == '__proto__' && defineProperty) {
Object.defineProperty(object, key, {
'configurable': true,
'enumerable': true,
'value': value,
'writable': true
});
} else {
object[key] = value;
}
}
var groupBy = createAggregator(function(result, value, key) {
if (Object.prototype.hasOwnProperty.call(result, key)) {
result[key].push(value);
} else {
baseAssignValue(result, key, [value]);
}
});
sheesh…
There is a lot of complexity in there to make sure it handles all scenarios no matter what a developer throws at it. While that can be useful, it seems like a waste of time when tools like Typescript can remove those issues for us. Take a look at my implementation:
function groupBy<T extends object,K extends keyof T>(arr: T[], key: K): Map<T[K], T> {
return arr.reduce((acc, obj) => {
if (!acc.has(obj[key])) {
acc.set(obj[key], []);
}
acc.get(obj[key]).push(obj);
return acc;
}, new Map<T[K], T>());
}
If we compare the iterated function between lodash and my implementation, they are very similar.
// Lodash
function(result, value, key) {
if (Object.prototype.hasOwnProperty.call(result, key)) {
result[key].push(value);
} else {
baseAssignValue(result, key, [value]);
}
}
// Mine
// `key` comes from the parent scope
function(result, obj) {
if (!result.has(obj[key])) {
result.set(obj[key], []);
}
result.get(obj[key]).push(obj);
return result;
}
Both implementations check to see if the key already exists, and if not create a new array for it. Then both push the object to the array. The big difference is that Lodash has to handle a wide variety of input types and edge cases, so includes a huge amount of extra code.
Now yes there are a few minor differences here such as using an object vs a map, but the core concept is the same. Some things I don’t handle:
- Non-object values in the array
- Objects that don’t have the key specified
- Non-array inputs
but Typescript will handle these for me, and even if I ignore that how many of these edge cases will I realistically encounter in my day to day work?
So next time you reach for Lodash, think about whether you really need it, or if you can implement the functionality you need without the bloat.
Want to read more? Check out more posts below!