Reputation: 12718
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(""));
Upvotes: 0
Views: 136
Reputation: 7273
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 theanimals
array. I'm guessing you need call here becauseanimals
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.
Upvotes: 2
Reputation: 2690
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.
Upvotes: 0
Reputation: 42736
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);
}
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
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.
Upvotes: 1
Reputation: 30411
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.
Upvotes: 3