HulaHoop0331
HulaHoop0331

Reputation: 13

Rewrite _.invoke

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

Answers (1)

T.J. Crowder
T.J. Crowder

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

Related Questions