0

I am trying to understand the purpose of using call(). I read this example on what it's used for, and gathered examples from Mozilla, namely:

var animals = [
  { species: 'Lion', name: 'King' },
  { species: 'Whale', name: 'Fail' }
];

for (var i = 0; i < animals.length; i++) {
  (function(i) {
    this.print = function() {
      console.log('#' + i + ' ' + this.species
                  + ': ' + this.name);
    }
    this.print();
  }).call(animals[i], i);
}

I know their example is just for demonstration, but I could easily just access the properties of the object directly in the loop as such:

        var animals = [
          { species: 'Lion', name: 'King' },
          { species: 'Whale', name: 'Fail' }
        ];

        for (var i = 0; i < animals.length; i++) {
            console.log('#' + i + ' ' + animals[i].species  + ': ' + animals[i].name);        
        }

From this I'm left confused. I still don't see the reason when I'd need to pass a different this context to call().


Also, with that mozilla example, in the .call(animals[i], i); function, I have two questions:

  • the this here is the animals array. I'm guessing you need call here because animals is otherwise out of scope to the inner anonymous function?

  • what is the purpose of passing in the index i as the second argument in .call(animals[i], i);?


This question is spurred from the following code segment that I was trying to understand. This code is to gather all inner values from certain spans in a document and join them together. Why do I need to execute call here on map? Is it because the document.querySelectorAll is otherwise out of context?

        console.log([].map.call(document.querySelectorAll("span"), 
            function(a){ 
                return a.textContent;
            }).join(""));
Community
  • 1
  • 1
user3871
  • 12,432
  • 33
  • 128
  • 268
  • Yes, that example is really contrived. It just passed the animal for `this` and `i` for the `i` parameter. It would make more sense if `.print()` wasn't called directly, like in [this contrived example](http://stackoverflow.com/q/750486/1048572). – Bergi Feb 26 '15 at 00:47
  • @NetaMeta: Except `querySelectorAll` doesn't return an array. And that's why you NEED `call` there. – Bergi Feb 26 '15 at 00:49
  • Bergi, yaps removed the comment, i was wrong – Neta Meta Feb 26 '15 at 00:54

4 Answers4

3

I'm going to start at the bottom of your question, asking about why to use "call" in this code:

    console.log([].map.call(document.querySelectorAll("span"), 
        function(a){ 
            return a.textContent;
        }).join(""));

So, here's the issue. What this chunk of code really wanted to do was use the map method on an array. However, document.querySelectorAll returns a NodeList, not an array. Most importantly here, NodeList does not have a map function on it. So you'd be stuck with the classic for loop.

However, it turns out Array.map actually works on anything that has a length property and supports numeric indexing (nodes[i] for example). However, the implementation of map is using specifically this.length internally.

So, by using call here, you can borrow the implementation of Array.map, pass it a this reference that isn't actually an array but works enough like one to work for these purposes.

So basically, this is calling:

document.querySelectorAll("span").map( 
  function(a){ 
    return a.textContent;
  }).join("");

except that the return doesn't actually have a map method, so we borrow one from Array. This is often referred to as "duck typing".

The animal example is, like any example with animals in it, rather contrived. You're passing i in the call because the function you're calling (the anonymous one) is expecting a parameter (notice the function(i)) so you need to pass a value. As I said, it's contrived, and in real life you wouldn't do it this way.

So in general, call is most useful to borrow methods off one type to use on another.

Chris Tavares
  • 29,165
  • 4
  • 46
  • 63
  • just so I understand... 1) ideally you'd just use `[].map` to create a new array of the joined spans' inner text values found by the querySelector, but need `call` so that `map` can handle the returned nodeList as an array? 2) I tried your second example, `document.querySelectorAll("span").map( function(a){ etc...` without `[].map.call` and it said `undefined is not a function`, why? and 3) I still don't understand what arguments `call` takes? Is the first argument always something that returns an Array? What are the arguments after the first arguments? Are they always anonymous functions? – user3871 Feb 26 '15 at 01:00
  • A couple things. 1) `[].map` is not calling map. It's retrieving the function object for the map method so that you can call `call` on it. 2) The second one fails because you call a function which returns an object (`querySelectorAll` in this case). Then you access a property on the result (`map` in this case). The result object doesn't have a `map` property, so you get the `undefined` value. You then try to call it (using the `()` operator), but you can't do a function invoke on `undefined`. Thus the error. Hope this helps. – Chris Tavares Feb 26 '15 at 01:06
  • `It's retrieving the function object for the map method so that you can call call on it.` I'm very confused by this. `Array.prototype.map` creates a new array with the results of calling a provided function on every element in this array, right? What is the `function object` I'm retrieving in this case, and why do I need to get the function object to call `call` on it? – user3871 Feb 26 '15 at 01:14
  • Remember - Javascript functions ARE objects. A method is just a property who's value happens to be a function. When you do `[].map` (note there's no `()` in this expression), you're not invoking `map`, you're retrieving the map property from the array. `call` is a method on function objects. So you're retrieving a property of an object (map) and invoking a method on it (call). Would it be easier to understand if instead of writing `[].map` it was `Array.prototype.map` instead (that works too)? – Chris Tavares Feb 26 '15 at 05:36
  • Okay, last thing then I'll accept, I promise :). I need to understand this... so the `Array.prototype.call(` function 1) get all spans with tag "hidden" from `querySelectorAll`, 2) pass query selector values as arguments (a) to anonymous function (where `a` = the span), 3) create a new array with returned results from anonymous function, 4) when array of all `hidden` spans is full, use `join` to turn array into a string, 5) `call` returns that string – user3871 Feb 27 '15 at 03:39
  • Almost. I'm not sure where the "hidden" thing came from? We're just querying spans. There are actually two function calls here. The `map.call` is the first one - it's return value is the result of the map operation, which is an array containing the return values from the anonymous function. The second function call is the call to "join", which is a method on arrays, which concatenates every element in the array it's called on into a string. – Chris Tavares Feb 27 '15 at 05:55
  • Okay, so I'm retrieving "map" property from array prototype object, and invoking method (call() ) on it. I guess I'm confused because in MDN docs, Array.prototype "properties" section doesn't have a `map` property, only a `map()` function, which creates a new array. – user3871 Feb 27 '15 at 16:51
  • map is the name of the property which contains the map function object. That's just core javascript. – Chris Tavares Feb 27 '15 at 21:04
2

That example is correct, but poor in that it is completely unnecessary to achieve their result.

Also, with that mozilla example, in the .call(animals[i], i); function, I have two questions:

  • the this here is the animals array. I'm guessing you need call here because animals is otherwise out of scope to the inner anonymous function?

Actually, the this is each item. In .call(animals[i], i); the first item is the object that will be mapped to this, so this inside the function is the current animal item, not the animals array.

Also, the inner function can access animals just fine in the current closure. Again, proving that their example is poor.

  • what is the purpose of passing in the index i as the second argument in .call(animals[i], i);?

The second parameter is the of the call is the first argument passed to the function being called (and the third parameter is the second argument, etc.). This, opposed to apply which takes an array as it's second parameter and apply's that as individual arguments. But, their called function has access to the current i once again showing that their example is unecessary.


Now, the second part. You need to use call because document.querySelectorAll is a NodeList not an Array and, unfortunately, NodeList does not have a map method, while Array does.

For a very simplified version, assume that we have Array.prototype.map defined something like this:

Array.prototype.map = function(fn) {
  var copy = [];
  for (var i = 0; i < this.length; i++) {
    copy.push(fn(this[i]));
  }
  return copy;
};

You can see here that when we call:

var timesTwo = [1,2,3].map(function(n) {
  return n * 2;
});
// timesTwo is [2,4,6];

You can see in our defined Array.prototype.map that we are referring to our array instance as this. Now, back to our NodeList from above: We don't have a map method available but we can still call the Array.prototype.map using call and force this to refer to our NodeList. This is is fine because it does have a length property, as we are using.

So, we can do so by using:

var spanNodeList = document.querySelectorAll('span');
Array.prototype.map.call(spanNodeList, function(span) { /* ... */ });

// Or, as MDN's example, use an array instance as "[]"
[].map.call(spanNodeList, function(span) { /* ... */ });

Hope that helps.

rgthree
  • 7,217
  • 17
  • 21
  • Excellent description. Thanks for diving into `Array.prototype`. So I'm retrieving "map" property from array prototype object, and invoking method (call() ) on it. I guess I'm confused because in MDN docs, Array.prototype "properties" section doesn't have a map property, only a map() function, which creates a new array. – user3871 Feb 27 '15 at 17:22
  • @Growler Yup. `Array.prototype.map` is a method, not a _property_. When we execute the `Array.prototype.map.call` and pass in our `NodeList` in as the binding all we are saying: "Hey, please invoke the map method on the Array prototype, but use this NodeList when it is referring to `this`." The `call` doesn't really matter if it's called on a prototype, a method of an instance, a global or anonymous function, etc. It's simply saying "make `this` refer to my first parameter inside the function call" – rgthree Feb 27 '15 at 20:23
1

document.querySelectorAll("span") returns a NodeList and not an Array, so if you tried document.querySelectorAll("span").map(...) you would get an error about map not being defined (same if you tried to a .join)

[].map.call(document.querySelectorAll("span"), 
    function(a){ 
        return a.textContent;
    }
)

This is simply using call to make it so the NodeList is used as an Array for the map function


for (var i = 0; i < animals.length; i++) {
  (function(i) {
    this.print = function() {
      console.log('#' + i + ' ' + this.species
                  + ': ' + this.name);
    }
    this.print();
  }).call(animals[i], i);
}
  1. the this here is the animals array. I'm guessing you need call here because animals is otherwise out of scope to the inner anonymous function?

    Actually the this here will refer to the object in the animals array at index i. But animals is not out of scope, it is just easier to do this.species than animals[i].species. They could have just as easily done )(i) and done animals[i].species instead of ).call(...) and this.species

  2. what is the purpose of passing in the index i as the second argument in .call(animals[i], i);?

    Since they are using an anonymous function, if they did not use the IIFE and pass in the index i argument, the anonymous function would use whatever value the for loop's increment variable i was last set to. So the print function would be logging the same info instead of each objects info.

Patrick Evans
  • 41,991
  • 6
  • 74
  • 87
0

Your example is different to Mozilla's. In Mozilla's, it creates a function. Without the anonymous function in the loop to create that function, you will encounter a closure problem. Effectively meaning that function won't be behave as you might expect. For example, try to recreate the function without the anonymous function and call animals[0].print()

the this here is the animals array. I'm guessing you need call here because animals is otherwise out of scope to the inner anonymous function?

This this value is an individual animal object. The animals array is NOT out of scope to the inner anonymous function - this is the nature of JavaScript, you will always have access to an outer function's variables unless another variable is shadowing it (has the same name and thereby covers an outer variable).

Consider the example without the call method:

var animals = [
    { species: 'Lion', name: 'King' },
    { species: 'Whale', name: 'Fail' }
];

for (var i = 0; i < animals.length; i++) {
    (function(i) {
        this.print = function() {
            console.log('#' + i + ' ' + this.species
              + ': ' + this.name);
        }
        this.print();
    })(i);
}

In this example, the this variable will refer to the window object, or null depending on some factors such as whether use strict is used. But what we want to do is attach the print method to the animal object, so we need to use the call method.

what is the purpose of passing in the index i as the second argument in .call(animals[i], i);?

As above, it is because of the closure problem, you don't want to be referring to the i variable in the outer closure, but you want to localize it to inside the anonymous function so that the print method is sane.

Community
  • 1
  • 1
sahbeewah
  • 2,690
  • 1
  • 12
  • 18