getify / You-Dont-Know-JS

A book series on JavaScript. @YDKJS on twitter.
Other
179.37k stars 33.48k forks source link

this & Object Prototypes - Ch 2 (Add explanation regarding Function.call() inside Function.call() call) #982

Closed Konrud closed 7 years ago

Konrud commented 7 years ago

IMHO, there is some mislead behavior using Function.call. That is when you use function.call inside other function.call and pass this as the first parameter to the call function, e.g.

var obj = { name: "Sam" };

function foo() { 

   function boo() { 
         console.log("Hi my name is ", this.name)
   };
   boo.call(this);  // works as expected
};

foo.call(obj); // works as expected

The code above works fine and as expected, but if you try to do the same using, for example, Array.prototype method inside another Array.prototype method the this becomes undefined (in the strict mode).

Example:

 Array.prototype.unique = function() {

       /// this -> will be the calling array 
      //// now let's try to pass "this" as the first argument to the "call" function
      return Array.prototype.filter.call(this, function(v, i, arr) {
        var thisVal = this; /// will be "undefined"
        return Array.prototype.indexOf.call(arr, v) === i;
      });

 };

  var arr = [1, 2, 1, 3, 2, 4, 5, 4, 6, 5, 7, 8, 9, 10, 6, 5, 1];

  var uniqueArr = arr.unique();

I think this "trap/behavior" should be explained/mentioned.

getify commented 7 years ago

In your example, the "nested" call to Array.prototype.filter(..) via call(..) does in fact get the this passed along to it, which is how it's able to filter on the array in question. So that's not actually broken.

What you're pointing out is a separate behavior, which is that when a third nesting of call happens, when filter(..) itself calls your predicate function, that this binding isn't automatically passed along. The design of those utilities is to not by-default leak the parent this to those user-function delegated calls. You could argue they should automatically do that, in the same way that event handlers do. But they intentionally chose not to, for some various reasons.

However, those built-in utilities all include a third optional argument, which lets you override and tell them to pass the this binding along. Change your code to:

Array.prototype.unique = function() {

       /// this -> will be the calling array 
      //// now let's try to pass "this" as the first argument to the "call" function
      return Array.prototype.filter.call(this, function(v, i, arr) {
        var thisVal = this; /// will be `arr`
        return Array.prototype.indexOf.call(this, v) === i;
      }, this );   // <---- *** inserted `this` ***

 };

 var arr = [1, 2, 1, 3, 2, 4, 5, 4, 6, 5, 7, 8, 9, 10, 6, 5, 1];

 var uniqueArr = arr.unique();

Now your predicate function is getting the this binding as "expected", so you can use it instead of arr for the indexOf(..) call. :)

Konrud commented 7 years ago

Wow, thank you for your rapid and explanatory answer. But still, don't you think this is a bit misleading? First of all the "trick" that the call function may receive the third argument and we may use it as this value is not documented in any place, not even in MDN. I do know that call function may receive arguments that can be sent to the function when it is invoked. But nonetheless I think it's a misleading design, I mean if the first argument is intended to be the this argument why should I use some trick in order to use is as it should be?

Why in the first example (using plain functions) it does work as expected? I mean I set the first argument to be this in the called function and it's work fine, but when we use something more complex like Array.prototype.filter we need to do some tricks.

getify commented 7 years ago

But still, don't you think this is a bit misleading?

Misleading? No. Confusing? Well, maybe a tiny bit, but not in relation to what I said, rather this separate orthogonal topic of how certain built-in's methods work. I say it's separate because it would be true even if there was no nesting inside another function at all.

First of all the "trick" that the call function may receive the third argument

I think you have misunderstood the code. It's not that call(..) takes a third parameter. fn.call(..) takes as its first parameter a this to use for the fn only (not whatever fn may call). Everything after the first argument is just passed along to fn, one at a time. So, in this case, the this we're passing along ends up as the second argument to Array.prototype.filter(..). That's not a trick, it's just that call(..) is designed to pass along arguments to its callee.

is not documented in any place, not even in MDN.

It's definitely well documented, especially on MDN, but for filter(..) etc; it has nothing to do with call(..). See the "thisArg" here:

https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Array/filter#Syntax

I thought I had covered this behavior somewhere in the YDKJS series, but I did a search just now and couldn't find any explicit references to it. TBH, I've never once used it, and probably wouldn't recommend it, so it doesn't surprise or bother me that it's not specifically in there.

Why in the first example (using plain functions) it does work as expected?

Again, this misunderstanding you seem to have is that you're forgetting that Array.prototype.filter.call( someThisObjHere, ...) does in fact invoke filter(..) itself with someThisObjHere as its this. That's how filter(..) itself knows which array you want it to filter on.

It's a separate issue that you then provide another function to filter(..) that you want filter(..) to call. That's why filter(..) takes another parameter (its second parameter) if you want filter to make sure the predicate is invoked with some particular this.

Frankly, this design makes sense to me. I don't think, if you properly understand it, it will seem unreasonable. I think here the reason you think it's unreasonable is that you're misunderstanding what's actually going on.


Now, let's take a step back and analyze something for a minute, because I think this discussion is missing the forrest for the trees.

Array.prototype.unique = function() {
   return Array.prototype.filter.call(this, ..., this );
};

That is fine, but it's also unnecessary. Since this here is an array, we don't need to indirectly invoke filter(..), we can directly invoke it:

Array.prototype.unique = function() {
   return this.filter( ..., this );
};

You'll notice here that we still can pass along this as the second argument to filter(..), so that the predicate function will also be invoked in the context of this.

Konrud commented 7 years ago

Thank you very much for your clarification, patience and time. In the example, I've used it via Array.prototype because I wanted it to be generic not only for arrays but also for array-like objects.

BTW, I really enjoyed your books, I've finished all the books from the series.

getify commented 7 years ago

because I wanted it to be generic not only for arrays but also for array-like objects.

Good point! :)