When you look at both the array and object scenarios, there is a call shown that doesn't try to pass args
to the method:
newArr.push(element[methodName]());
Since args
is an array, the easiest way to pass them is by using apply
. The apply
function takes two arguments. The first argument represents what should be considered as this
inside the method call, which in this case is element
. The second argument is the array of arguments to be passed to the method. So here's how it looks after applying apply
:
newArr.push(element[methodName].apply(element, args));
Now that we have addressed the main part of your query, let's explore how we can enhance your implementation of invoke
. First, let's focus on the array scenario:
for (let index = 0; index < collection.length; index++) {
let keysArr = Object.keys(collection);
let element = collection[keysArr[index]];
newArr.push(element[methodName].apply(element, args));
};
The way you're determining element
here seems a bit inefficient. You are recalculating Object.keys(collection)
in each iteration, even though it remains constant. Also, you don't really need keysArr
; simply use collection[index]
to access the element. Therefore, a more efficient version would be:
for (let index = 0; index < collection.length; index++) {
let element = collection[index];
newArr.push(element[methodName].apply(element, args));
};
A similar issue exists in the object scenario as well:
for (let index = 0; index < Object.entries(collection).length; index++) {
let keysArr = Object.keys(collection);
let element = collection[keysArr[index]];
newArr.push(element[methodName].apply(element, args));
}
In this case, apart from recomputing Object.keys(collection)
, you are also redoing
Object.entries(collection)</code in every iteration. However, unlike in the array case, you do require <code>keysArr
here. The solution is to compute it once before the loop and reuse it like so:
let keysArr = Object.keys(collection);
for (let index = 0; index < keysArr.length; index++) {
let element = collection[keysArr[index]];
newArr.push(element[methodName].apply(element, args));
}
With these optimizations, our implementation of _.invoke
is now effective. But since we're dealing with Underscore, let's investigate if we can introduce more functional programming elements.
Functional style revolves around composing existing functions into new ones. In the case of _.invoke
, it is essentially a specialized form of _.map
. Given that _.map
is capable of iterating over arrays and objects while producing a new array just like _.invoke
, we can simplify our approach. Instead of repeating the "call a method with arguments" logic for the entire collection, we only need to figure out how to apply it to a single element and then combine that with _.map
.
We start by defining a function that performs the task for a single element:
function invokeElement(element, methodName, args) {
return element[methodName].apply(element, args);
}
However, this version of invokeElement
isn't directly compatible with _.map
. While _.map
knows about passing the element
, it lacks information regarding methodName
or
args</code. To address this, we predefine <code>methodName
and
args
using
_.partial
:
const partialInvoke = _.partial(invokeElement, _, methodName, args);
This snippet indicates that a modified version of invokeElement
is created, where the second and third arguments are set to methodName
and
args</code while retaining dependency on the forthcoming first argument. The underscore placeholder <code>_
used here aligns with Underscore's default export in functions like
_.map</code and <code>_.partial
.
Now, armed with the necessary components, we compose invoke
using _.map
and invokeElement
:
function invoke(collection, methodName) {
const args = Array.prototype.slice.call(arguments, 2);
const partialInvoke = _.partial(invokeElement, _, methodName, args);
return _.map(collection, partialInvoke);
}
Further refinement is possible. By leveraging _.restArguments
, we eliminate the need for explicitly computing args
:
const invoke = _.restArguments(function(collection, methodName, args) {
const partialInvoke = _.partial(invokeElement, _, methodName, args);
return _.map(collection, partialInvoke);
});
Alternatively, we can adopt modern spread syntax, which provides a cleaner alternative:
function invoke(collection, methodName, ...args) {
const partialInvoke = _.partial(invokeElement, _, methodName, args);
return _.map(collection, partialInvoke);
}
Regardless of the approach chosen, we now have our personalized implementation of invoke
encapsulated in just two lines of code. This showcases the power and elegance of functional programming!