Reputation: 13
I am trying to rewrite the _.invoke function from scratch and can only half get it to work. My function should take a collection, a methodName and optionally additional arguments and return an array with the results of calling the method indicated by methodName on each value in the collection. Any extra arguments passed to invoke will be forwarded on to the method invocation.
Here's my code:
_.invoke = function (collection, methodName) {
let res = [];
if (Array.isArray(collection)) {
for (let i = 0; i < collection.length; i++) {
res.push(collection[i][methodName](arguments));
}
} else {
for (const [key, value] of Object.entries(collection)) {
res.push(value[methodName](arguments));
}
}
return res;
};
It is currently failing due to not passing the arguments correctly. Can anybody give me any advice or recommendations why this is happening?
Upvotes: 1
Views: 551
Reputation: 1074268
You're passing the arguments
pseudo-array rather than its contents, and you're passing the whole thing, but you probably didn't want to pass the collection of method name.
Instead, use a rest parameter on the function signature, and then spread it out when calling the method:
_.invoke = function (collection, methodName, ...args) {
// ^^^^^^^^^−−−−−−− rest param
let res = [];
if (Array.isArray(collection)) {
for (let i = 0; i < collection.length; i++) {
res.push(collection[i][methodName](...args));
// ^^^−−−−−−−−−−−−−−− spread notation
}
} else {
for (const [key, value] of Object.entries(collection)) {
res.push(value[methodName](...args));
// ^^^−−−−−−−−−−−−−−−−−−−−−−− spread notation
}
}
return res;
};
Side note: You might want to allow any iterable rather than specifically just an array, and you can use Object.values
to get an array of an object's values.
_.invoke = function (collection, methodName, ...args) {
const iterable = isIterable(collection) ? collection : Object.values(collection);
const res = Array.from(iterable).map(entry => entry[methodName](...args));
return res;
};
...where isIterable
is:
function isIterable(obj) {
return obj && typeof obj[Symbol.iterator] === "function";
}
Or if doing a shallow copy of the array unnecessarily bothers you:
_.invoke = function (collection, methodName, ...args) {
const array = Array.isArray(collection)
? collection
: isIterable(collection) ? Array.from(collection) : Object.values(collection);
const res = array.map(entry => entry[methodName](...args));
return res;
};
If you need to do that three-step on collection
repeatedly, you could use a helper function for it:
function arrayForAny(x) {
const array = Array.isArray(x) ? x : isIterable(x) ? Array.from(x) : Object.values(x);
}
You could also check for array-likes and pass those into Array.from
rather than using Object.values
on them. But I've already gone over the top here, so... ;-)
Upvotes: 1