For a layman who is in hurry to read this blog, the answer is.
The difference between call and apply can be visualised as, you have a stick and you call your dog’s name and throw that stick towards him and dog grabs it, no hassle, simple task isn’t it? That’s call(). Another way could be, you call the name, let him come to you and then you can give him as many sticks as you want or none at all. Now here sticks refer to the 2nd arguments that you pass to the call() and apply() function i.e. list of arguments
Lets have a look at this small code snippet.
Now some might question about the GC cycles, but trust me you can explicitly call GC by either calling delete somObj which is the worst way of doing it, rather you can reuse the same variable by either assigning null or undefined but you will find an equal variation of time for both call() and apply().
Okay, lets get to the problem, you can try running the benchmarks and you will find out that call() is far more optimised then apply(). You will find similar benchmarks all over the internet but everyone says that “JS internally optimise it” but being a programmer you should be able to answer the question, How? In this blog I am gonna do the same, answering the quest of the solid proof as why they two happen to be in JS and why one is more optimised then another.
JS Invoke Steps
- Load the Code in memory
- Generate an intermediate binary code(JS OpCode generation)
- Interpret that code
- Bailout, and initialise environment variables, context, stack and other necessary things required to execute that block.
- Execute the block, save the return states onto the stack, call the callee and jump out of Bailout.
A typical JS block/Script/Function is executed until the opcode JSOP_RETRVAL, JSOP_RETURN is encountered. So what are OpCodes?
An OpCode is basically generated by JIT(Just-In-Time) compiler of JS Engine, these are symbols which converts the given code into an engine understandable format, an example of a JIT is CrankShaft that v8 uses. So yeah, the code is compiled first, and then interpreted, but don’t worry a JS compiler never throws any error for your script except Syntax Errors. These compilers perform the primary level of optimisation for your code, it converts your code into tokens which have specific meanings for the Interpreter to understand, an example of optimisation are Atomic values e.g. accessing the length of an Array or an Object i.e. JSOP_LENGTH, it is by default optimised for faster execution. So our dear apply() and call() have their opcodes too i.e. JSOP_FUNAPPLY and JSOP_FUNCALL both are parts of JS_INVOKE opcode. A JS_INVOKE opcode means that this statement need to be invoked separately out of the current running context.
Now what happens with call() is pretty basic, do you remember the C language’s variable length argument declaration? Something that looked like this.
Yeah, I know you might be laughing now, because we cracked it, almost!
Whenever you invoke the call() function of Function.prototype, where Function could be any JS function(anonymous, named) that call to function by default gets converted to (…) a pretty basic optimised implementation. The interpreter now don’t have to worry to expand the arguments when calling that function instead what it does is just point it to the list of arguments that you provided to someFunction.call(), which are easily available on the stack itself.
A Bailout of call when triggered, picks the first argument as the pointer to the *jscontext for the next bailout(actual execution of the function which is been called) and all other arguments following it which are already available on the stack are directly passed to the function for which the call() is invoked.
Now the only drawback with call() is the number of arguments, whenever you invoke call(), you are passing a predefined length of arguments. You can’t pass arbitrary number of arguments using call and as I said its your homework to figure out that limit. So, when Douglas Crockford and team were developing JS they thought of this use case when the programmer can have an arbitrary length of arguments which they might not know about when they want to invoke a function, so they found a workaround for it, and that is the apply() function. An apply function takes the second parameter as an array of arguments. Now when the Bailout is triggered by the invocation of apply() function, JS Engine picks the second parameter, expand it and apply those to whichever function you have triggered apply on. This task has its own cost which is, traversing the array, picking each element, pushing onto the next calling stack of Bailout one by one and then invoking it.
An apply() function too has the same limit on length of arguments, so don’t think that you can pass the whole list of unique ID’s of Penguins that lives on Antarctica(C’mon atleast for next 5–10 years until we make them reach near to extinction)(:angryface:).
Function.prototype.call() and Function.prototype.apply() both will give you a Stack Range Limit Error if you try to pass a very long number of arguments to a function.
With ❤ ! Cheers!