While going through Jasmine’s source code, I came across a weird idiom:
function attemptAsync(queueableFn) {
var clearTimeout = function () {
Function.prototype.apply.apply(self.timer.clearTimeout, [j$.getGlobal(), [timeoutId]]);
},
next = once(function () {
clearTimeout(timeoutId);
self.run(queueableFns, iterativeIndex + 1);
}),
timeoutId;
...
}
What in the world is Function.prototype.apply.apply
? I decided to find out for myself.
As a refresher, let’s write a function coolFx
that simply prints out its this
and arguments
. When invoked as a method on a function, apply
executes its caller with its first parameter as the caller’s this
, and its second parameter as an array of arguments as the caller’s argument list. The parameters to apply
can be anything, even primitives, as demonstrated below:
function coolFx() {
console.log("My cool function is running!!");
console.log("coolFx's this:", this);
console.log("coolFx's arguments:", arguments);
}
var anything = 1;
coolFx.apply(anything, [anything, anything, anything]);
prints:
My cool function running!!
coolFx's this: Number {[[PrimitiveValue]]: 1}
coolFx's arguments: [1,1,1]
To preserve our sanity later on, let’s see apply
in more detail by defining a wrapper around apply
:
Function.prototype.firstApply = function (x, y) {
console.log("This is firstApply's this: ", this);
console.log("This is firstApply's first parameter:", x);
console.log("This is firstApply's second parameter:", y);
this.apply(x, y);
};
Function.prototype.secondApply = function (x, y) {
console.log("This is secondApply's this: ", this);
console.log("This is secondApply's first parameter:", x);
console.log("This is secondApply's second parameter:", y);
this.apply(x, y);
};
Running coolFx.firstApply
with this modified version of apply
prints:
This is firstApply's this: coolFx()
This is firstApply's first parameter: 1
This is firstApply's second parameter: [1]
My cool function is running!!
coolFx's this: Number {[[PrimitiveValue]]: 1}
coolFx's arguments: [1]
So what does it mean when you call apply
again on itself, as in coolFx.firstApply.secondApply
?
coolFx.firstApply.secondApply(anything, [anything, anything]);
This is the secondApply's this: Function.firstApply(x, y)
This is the secondApply's first parameter: 1
This is the secondApply's second parameter: [1, 1]
This is the firstApply's this: Number {[[PrimitiveValue]]: 1}
This is the firstApply's first parameter: 1
This is the firstApply's second parameter: 1
Uncaught TypeError: this.apply is not a function
Calling secondApply
on firstApply
means that secondApply
will execute firstApply
, and since firstApply
needs to be executed in the context of a function, the first parameter must be a function. (I only know this from working backwards from the interpreter’s errors, go figure).
In this case, since this
in firstApply
is the primitive 1
, 1.apply
is obviously not a function since it doesn’t inherit from Function.prototype
.
Now that we know that the secondApply
expects a function as its first parameter, let’s try putting coolFx
there:
coolFx.firstApply.secondApply(coolFx, [anything, anything]);
This is the secondApply's this: Function.firstApply(x, y)
This is the secondApply's first parameter: coolFx()
This is the secondApply's second parameter: [1, 1]
This is the firstApply's this: coolFx()
This is the firstApply's first parameter: 1
This is the firstApply's second parameter: 1
Uncaught TypeError: Function.prototype.apply: Arguments list has wrong type
Okay, so at least it’s a different error now. Why is firstApply
complaining that its argument list (the second parameter) has the wrong type? That’s because its trying to call coolFx.apply(1,1)
! Don’t forget that apply
expects an array as its second parameter.
So I guess we can wrap the second anything
in an array:
coolFx.firstApply.secondApply(coolFx, [anything, [anything]]);
This is the secondApply's this: Function.firstApply(x, y)
This is the secondApply's first parameter: coolFx()
This is the secondApply's second parameter: [1, 1]
This is the firstApply's this: coolFx()
This is the firstApply's first parameter: 1
This is the firstApply's second parameter: [1]
My cool function is running!!
coolFx's this: Number {[[PrimitiveValue]]: 1}
coolFx's arguments: [1]
So it finally works! Wait, doesn’t this look… familiar? It should, because what its doing is exactly what coolFx.apply(anything, [anything])
is doing (see above) - executing coolFx
with this
as anything
and its arguments as [anything]
.
And isn’t it stupid that we’re mentioning coolFx
twice? What exactly does the first coolFx
even do? Let’s get rid of it:
Function.prototype.firstApply.secondApply(coolFx, [anything, [anything]]);
This is the secondApply's this: Function.firstApply(x, y)
This is the secondApply's first parameter: coolFx()
This is the secondApply's second parameter: [1, 1]
This is the firstApply's this: coolFx()
This is the firstApply's first parameter: 1
This is the firstApply's second parameter: [1]
My cool function is running!!
coolFx's this: Number {[[PrimitiveValue]]: 1}
coolFx's arguments: [1]
It turns out that it doesn’t matter which function calls firstApply
, because the only function that is going to be executed is secondApply
’s. firstApply
’s original context is irrelevant (imagine it as behaving as a static method). It’s just there to facilitate this process by helping “promote” the first parameter (the function) into the executing function itself.
Maybe its a little clearer if you remove secondApply
from the equation:
This is the firstApply's this: Empty()
This is the firstApply's first parameter: coolFx()
This is the firstApply's second parameter: [1, Array[1]]
So there you have it. And after all this trouble, I still don’t actually know why Jasmine uses this idiom. Instead of
Function.prototype.apply.apply(self.timer.clearTimeout, [
j$.getGlobal(),
[timeoutId],
]);
It seems the following is equivalent:
self.time.clearTimeout.apply(j$.getGlobal(), [timeoutId]);
#Bonus
What happens if we chain more than 2 apply
s together?
Function.prototype.firstApply.secondApply.thirdApply(coolFx, [
anything,
[anything],
]);
This is the thirdApply's this: Function.secondApply(x, y)
This is the thirdApply's first parameter: coolFx()
This is the thirdApply's second parameter: [1, Array[1]]
This is the secondApply's this: coolFx()
This is the secondApply's first parameter: 1
This is the secondApply's second parameter: [1]
My cool function is running!!
coolFx's this: Number {[[PrimitiveValue]]: 1}
coolFx's arguments: [1]
Looks like firstApply
doesn’t even get run at all. This is puzzling at first, but makes sense when you think about it - secondApply
’s original this
, firstApply
, has been “diverted” to coolFx
(remember when I said above that firstApply
’s original context is irrelevant?) Thus, when secondApply
does its job, its coolFx
that it executes. After its done, firstApply
is all but forgotten. Of course, this remains the same regardless of how many apply
s you chain.
#More Bonus
So you’ve decided against your better self to use this idiom in your code, but you really don’t like how you have nest the eventual arguments in another array. There’s actually a way to work around that - use call
1 instead!
randomFunction.call.secondApply(coolFx, [
anything,
anything,
anything,
anything,
anything,
]);
This is secondApply's this: call()
This is secondApply's first parameter: coolFx()
This is secondApply's second parameter: [1, 1, 1, 1, 1]
My cool function is running!!
coolFx's this: Number {[[PrimitiveValue]]: 1}
coolFx's arguments: [1, 1, 1, 1]
(Okay, this post turned out to be way longer than I had anticipated.)
#Footnotes
-
In case you’re too lazy to follow through on the link, I’ll paste it here for your convenience, courtesy of MDN:
While the syntax of this function is almost identical to that of
apply()
, the fundamental difference is thatcall()
accepts an argument list, whileapply()
accepts a single array of arguments.