Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Trying to understand underscore.js source - call and apply used in library

In Jeremy Ashkenas's awesome Underscore.js library, I tried to understand one thing about the source file. I do not understand this:

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

So that:

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

.call or .apply are the methods of the functions. But here, which functions do .call refer to? The first parameter should be the context, but arguments is context? The second parameter should be the params to pass in the functions. Here they are number 2. What does this mean? Sometimes in the library, it uses 1 or 0. Are they the number of the params to pass in the functions?

_.bind = function bind(func, context) {
    var bound, args;
    if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
    if (!_.isFunction(func)) throw new TypeError;
    args = slice.call(arguments, 2);
    return bound = function() {
      if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));
      ctor.prototype = func.prototype;
      var self = new ctor;
      var result = func.apply(self, args.concat(slice.call(arguments)));
      if (Object(result) === result) return result;
      return self;
    };
  };

Question 2: I do not quite understand the logic of this function. Need help to understand. An example should be very helpful.

  // Invoke a method (with arguments) on every item in a collection.
  _.invoke = function(obj, method) {
    var args = slice.call(arguments, 2);
    return _.map(obj, function(value) {
      return (method.call ? method || value : value[method]).apply(value, args);
    });
  };

Thank you for help.

like image 744
MMA Avatar asked Jan 15 '12 14:01

MMA


1 Answers

The "slice" function on the Array prototype expects that this will refer to the array on which it's supposed to operate. In other words, if you have a real array:

var myArray = [1, 2, 3];

and you call slice():

var sliced = myArray.slice(1);

Then in that call to slice(), this refers to the array "myArray". As Raynos notes in a comment:

myArray.slice(1)

is the same as

myArray.slice.call(myArray, 1);

Thus when you use call() to invoke the function, and pass it arguments as the context object, the slice() code operates on arguments. The other parameters passed via .call() are simply the parameter or parameters for slice() itself. In my example above, note that I passed 1 to the function.

Now as to your second question, that .invoke() function first isolates the arguments passed in after the first two. That means that when you use _.invoke() you pass it two or more arguments: the first is the list to operate on, the second is the method, and the (optional) subsequent arguments are passed to the method for each element of the list.

That call to _.map() is complicated (and in fact I think it's got a little nonsense in it). What it's doing is iterating over the list, calling a function for each value in the list. What that function does to first determine whether the "method" parameter really is a function. If it is, then it calls that function via .apply() with the element of the list as the context. If "method" is not a function, then it assumes it's the name of a property of each list element, and that the properties are functions.

So for example, with a simple list it's pretty simple:

var myList = [1, 2, 3];
var result = _.invoke(myList, function(n) { return this * n; }, 2);

That will give the result [2, 4, 6] because the function I passed multiplies its context object (this) by the parameter passed, and I passed 2 in the call to _.invoke().

With a more complicated list, I can use the second flavor of _.invoke() and call a method on each object in the list:

var getName = function(prefix) { return prefix + " " + this.name; };
var list = [
  { name: "Bob", getName: getName },
  { name: "Sam", getName: getName },
  { name: "Lou", getName: getName }
];

var result = _.invoke(list, "getName", "Congressman");

That will call the "getName" function on each object in the list and return a list made from the results. The effect will be the list ["Congressman Bob", "Congressman Sam", "Congressman Lou"].

Now about that nonsense. In the code for _.invoke():

return _.map(obj, function(value) {
  return (method.call ? method || value : value[method]).apply(value, args);
});

That subexpresion method || value will always return the value of "method", or at least almost always barring some exotic trick. If method.call is truthy, then a reference to method must also be truthy. Also, if it were my code, I'd inspect method outside the _.map() callback so that the decision doesn't have to be made over and over again. Maybe something like:

return _.map(obj, method.call ?
  function(value) { method.apply(value, args); } :
  function(value) { value[method].apply(value, args); }
);
like image 197
Pointy Avatar answered Dec 09 '22 22:12

Pointy