0

I am trying to recreate .invoke() function.

I am able to call the function, but struggling with passing the arguments. I tried using call and apply, but couldn't make it work.

Here is my code:

_.invoke = function (collection, methodName) {
  let newArr = [];

  var args = Array.prototype.slice.call(arguments, 2);

  if (collection instanceof Array) {
    for (let index = 0; index < collection.length; index++) {

      let keysArr = Object.keys(collection);
      let element = collection[keysArr[index]];

      newArr.push(element[methodName]());
    };

  } else if (collection instanceof Object) {
    for (let index = 0; index < Object.entries(collection).length; index++) {

      let keysArr = Object.keys(collection);
      let element = collection[keysArr[index]];

      newArr.push(element[methodName]());
    }
  }

  return newArr;
};

Thank you.

1 Answer 1

0

In both the array and the object case, you show a call that doesn't attempt 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. apply takes two arguments. The first is whatever should be considered this inside the call to the method, which is element in this case. The second argument is the array of arguments that should be passed to the method. We end up with this form:

newArr.push(element[methodName].apply(element, args));

Having answered the core of your question, let's see how else we could make your version of invoke better. First, we will take a look at the array case:

    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 in which you determine element here is a bit inefficient. You recompute Object.keys(collection) on every iteration, even though it never changes. Moreover, you do not actually need keysArr; element is just collection[index]. So we can change the array part into this:

    for (let index = 0; index < collection.length; index++) {
      let element = collection[index];
      newArr.push(element[methodName].apply(element, args));
    };

We have a similar problem in the object part:

    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));
    }

Besides Object.keys(collection), you are also recomputing Object.entries(collection) on every iteration, which also never changes. However, in this case, you do need keysArr. The solution is to compute it once before the loop and reuse it:

    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));
    }

At this point, we have an efficient implementation of _.invoke that works. However, since this is Underscore, let us also try whether we can introduce more functional style.

Functional style is all about how we can compose existing functions into new functions. In the specific case of _.invoke, we can see that it is essentially a special case of _.map. Since _.map already knows how to iterate over arrays as well as objects and it already returns a new array, just like _.invoke, this means that we can reduce our challenge. Instead of doing the "call a method with arguments" thing for the whole collection, we just need to figure out how to do it for a single element, and then compose that somehow with _.map.

Starting with just a function that does the job for a single element, we already know that it goes like this:

function invokeElement(element, methodName, args) {
    return element[methodName].apply(element, args);
}

Now, this version of invokeElement is not quite ready to be passed to _.map. _.map will know which element to pass, but it knows nothing about methodName or args. Somehow, we need to pass methodName and args "ahead of time", so that _.map will be able to suffice just by passing element. _.partial lets us do exactly that:

const partialInvoke = _.partial(invokeElement, _, methodName, args);

This line means: take invokeElement and create a new version of the function, where the second and third argument are already set to methodName and args, respectively, but the first argument still depends on what will be passed in the future. The underscore _ used here as a placeholder is the very same underscore as in _.map and _.partial, i.e., the default export of the Underscore library.

Now, we have everything we need to compose invoke out of _.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);
}

We can still do better. Using _.restArguments, we no longer need to compute args:

const invoke = _.restArguments(function(collection, methodName, args) {
    const partialInvoke = _.partial(invokeElement, _, methodName, args);
    return _.map(collection, partialInvoke);
});

Alternatively, we can use modern spread syntax, which didn't exist yet when _.restArguments was invented:

function invoke(collection, methodName, ...args) {
    const partialInvoke = _.partial(invokeElement, _, methodName, args);
    return _.map(collection, partialInvoke);
}

Regardless, we have our own implementation of invoke in just two lines of code. That's the power of functional style!

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.