user3871
user3871

Reputation: 12718

JavaScript call - I can just access object properties directly

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:


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

Answers (4)

rgthree
rgthree

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 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.

Upvotes: 2

sahbeewah
sahbeewah

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

Patrick Evans
Patrick Evans

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

Upvotes: 1

Chris Tavares
Chris Tavares

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

Related Questions