How JavaScript Works: deep dive into call, apply, and bind
--
This is post # 35 of the series, dedicated to exploring JavaScript and its building components. In the process of identifying and describing the core elements, we also share some rules of thumb we use when building SessionStack, a JavaScript tool for developers to identify, visualize, and reproduce web app bugs through pixel-perfect session replay.
Introduction:
Call, Apply, and Bind are three incredibly important JavaScript methods that are available to all JavaScript functions — out of the box.
Call, Apply, and Bind help keep our code clean. And they make possible some advanced design patterns in JavaScript.
Also, they are extremely powerful tools in functional programming in JavaScript. They all have a relationship with the this variable in JavaScript and they can be applied in concepts such as function currying, function borrowing, and function binding.
Call, Apply, and Bind are not beginner-friendly as they often require an understanding of some fundamental JavaScript concepts. Concepts such as function currying cannot be understood without knowledge of closure and closure requires knowledge of the scope and the scope chain.
Consequently, call, apply, and bind are dreaded by JavaScript newbies.
Also, many developers are often confused with their differences and find it puzzling to decide when to use which? We would cover all these and more in this article.
To better understand call, apply, and bind, a good understanding of JavaScript functions and some foundational JavaScript concepts such as the execution context, call stack, closure, scope, and the scope chain, is required.
So in this article, we would lay the groundwork by learning about these foundational JavaScript concepts — some of which have been mentioned above. Thereafter, we would take a deep dive into call, apply, and bind.
Let’s start by learning about JavaScript functions in the next section.
The Anatomy Of A JavaScript Function:
In JavaScript, everything is an object. So a function in JavaScript is a special object that has all the properties of a normal object and some special hidden properties, such as the code property and an optional name property — functions in JavaScript can be anonymous.
The name property refers to the function name as seen in the code below:
There is an internal property called code that holds all the function code that we write. So the code we write is not an actual function. A function is an object with different properties and methods and the code we write is in the code property of the function.
Also, like normal objects, JavaScript functions can have methods. And call, apply, and bind are built-in methods in all JavaScript functions.
Consider the image of a JavaScript function below:
The image above shows a JavaScript function object with three built-in methods: call, apply, and bind. Also, it shows two special properties: name and code. Other hidden properties of a JavaScript function object can be found here.
The Execution Context And The Call Stack
We have covered this topic in a previous article in this series. The execution context is a wrapper around the currently executing code. It consists of the following:
- The this variable. Every execution context provides the this variable which refers to an object to which the currently executing code belongs.
- The variable environment — a place in memory where variable lives and how they relate with each other. Each execution context has its variable environment.
- The outer environment. When we execute code within a function the outer environment is the code outside of that function — at the global level, it is null.
When the JavaScript engine starts executing our code, a base execution context — the global execution context is created. Also, anytime a function is invoked a new execution context is created and placed on top of the stack. And when a function returns its execution context is popped off the call stack. This stack of the execution contexts that are created during code execution is called the call stack.
Scopes And Closures
A scope is a place where a variable can be found. It refers to the current context in which variables and expressions can be referenced. Scopes have a layered hierarchy so that the variables and expressions in a parent scope can be accessible by the child scope but not vice-visa.
A function has a local scope — its variables and expressions can only be referenced within its scope — within the function code block, or from a child function.
In JavaScript, even if a function returns, the inner — child function is still able to access the variables and values of the outer — parent function. This phenomenon is called closure. Closures are possible in JavaScript because of the behavior of the language.
When the JavaScript engine does not see a variable in the variable environment of an execution context, it would go out to its outer environment to look for it. That is when the JavaScript engine cannot find a variable in a given function scope it would look in the outer environment of the parent scope.
Consider the code below:
From our code above, we see that even though the getName function has returned, its variables — the name variable is still accessible to the getBio — inner function. This is because in JavaScript when a function returns and its execution context gets popped off the execution stack, its variables are still available in its variable environment so an inner function can look up the scope chain and access these variables.
With the knowledge of these important foundational JavaScript concepts, we are now ready to take a deep dive into call, apply and bind.
Let’s get started in the next section.
Deep Dive Into Call, Apply, And Bind
The call, apply and bind functions are similar in terms that they enable us to set the this context. Consequently, they enable us to control the behavior of the this variable.
Consider the code below:
From our code above, we see that call and apply invokes the greet function immediately and set the this context to their first argument.
The bind method, however, returns a copy of the greet function and sets the this context to its first argument. The new function — greet2 is then invoked later.
When the first argument is null or undefined the this variable points to the global object but in strict mode, it would be undefined.
Consider the side the code below:
and
One of the biggest challenges with using call , apply, and bind lies in knowing their differences and when to use them.
Call vs Apply vs Bind
Consider the table below:
The limitations of call() quickly become apparent in cases where we do not know the amount of argument a function would take.
apply() shines in this scenario since it takes an array of arguments as its second argument.
Application of call, apply and bind
As seen above, call, apply and bind are similar methods and while they can be used interchangeably in many use cases, they still have their areas of specialization where they shine.
Below are some use cases of call, apply and bind:
Function currying and partial functions
The important design pattern called function currying is possible due to Closures.
Function currying refers to the process of transforming a variadic function — a function that takes a variable number of arguments, into a series of nested unary functions — functions that take a single argument.
Consider the code below:
In our code above, we have transformed the add binary function into the curriedAdd function. Things may be clearer if it is written in ES5.
Consider the code below:
In the code above, the add function returns an anonymous function that still has access to the variables of the add function even if it has returned — this is made possible by the closure.
Also, function currying enables us to create a copy of a function but with some preset parameters.
Consider the code below:
We can easily create a function that multiplies by a specific number (e.g 2) using the pattern below:
Since the inner function would have access to the variables of the outer function even if it has returned, we preset the value of a parameter to 2. This is used to multiply any argument passed to the inner function.
The bind() method in particular is suited for function currying because it creates a new copy of the function. We can simplify our code using the bind method as seen below:
Consider the code below:
Function currying is also very useful in mathematical situations. For example, when building a library that involves a lot of mathematical calculations, we can have some fundamental functions that we can build on with some other default parameters — a really good usage for the .bind method.
Consider the code below:
In the code above, calc.multiplyMany method is a variadic function that will multiply every argument passed to it. The ES6 spread operator returns all the arguments passed to the multiplyMany. And the call method binds the this variable to this array of arguments. This is then passed to the reduce with the multiply method as the reducer function.
Now we can create a partial function below out of the calc.multiplyMany method.
Partial function application is a concept where a function of multiple arity, operates on some of its arguments and returns a function with a lesser arity for further usage. Function currying is a special case of partial function application.
Consider the code below:
In the code above, .bind literally bound the three arguments 1, 2, 3 as default arguments. Now, we can pass n number of arguments and the multiplyManyBySix function will multiply the product of these arguments by six.
In our small contrived example above, the bind() method provides the opportunity for us to pass multiple default function arguments and preserves the context of this for future execution.
Function borrowing
Method or function borrowing is a way for an object to use the method of another object without redefining that method. This pattern allows an object to borrow the methods of other objects without inheriting their properties and methods. call, apply, and bind can all be used for method borrowing.
Function borrowing fosters code reuse and keeps our code clean and DRY.
Call
Consider the code below:
In the code above, we have invoked the start and stop Vehicle methods on the car object using call — even though the car object does not have a start or stop method.
Thus, the car object has borrowed these methods from the Vehicle object.
Also, function borrowing provides a great way to reuse existing functionality without having to make one object extend or inherit from another.
Consider the code below:
Above, we have successfully borrowed the built-in Array.prototype.slice() using call.
Apply
Similarly, we can employ the apply method to borrow and reuse built-in variadic functions such as the Math.max and the Math.min functions.
Consider the code below:
In the code above, we have successfully borrowed the Math.max method using apply.
Now getMaxOfArray([1, 2, 3]) is equivalent to Math.max(1, 2, 3), and it can be reused in our code.
Also, we can borrow and reuse the math.min method using apply as seen below:
Bind
We can also use the bind method for function borrowing.
Consider the code below:
In the code above, we can make the baby run by borrowing the run method from the man object as seen below:
Conclusion:
call, apply and bind are used in several other advanced JavaScript design patterns such as anonymous function binding, chaining constructors, and access the this variables in callbacks.
call, apply, and bind help make our code clean and DRY.
With the knowledge of the fundamental JavaScript concepts discussed in this article, and with our contrived examples and use cases of call, apply and bind, you should be ready to start using them in your code.
When using call, apply, and bind you need be to very careful and make sure that you are providing the right execution context If, however, issues related to providing the wrong execution context reach production and impact your users, it could be quite time-consuming to identify the cause and resolve the issue on a timely manner.
A solution like SessionStack will make the process much more efficient as it allows you to replay customer journeys as videos. This allows you to see how customers interacted with your web app and what happened on their screen when the issue occurred. Combining this visual information with all of the technical details from the browser, device, network, and environment will give you all of the context needed to reproduce the problem and resolve it efficiently.
There is a free trial if you’d like to give SessionStack a try.
If you missed the previous chapters of the series, you can find them here:
- An overview of the engine, the runtime, and the call stack
- Inside Google’s V8 engine + 5 tips on how to write optimized code
- Memory management + how to handle 4 common memory leaks
- The event loop and the rise of Async programming + 5 ways to better coding with async/await
- Deep dive into WebSockets and HTTP/2 with SSE + how to pick the right path
- A comparison with WebAssembly + why in certain cases it’s better to use it over JavaScript
- The building blocks of Web Workers + 5 cases when you should use them
- Service Workers, their life-cycle, and use cases
- The mechanics of Web Push Notifications
- Tracking changes in the DOM using MutationObserver
- The rendering engine and tips to optimize its performance
- Inside the Networking Layer + How to Optimize Its Performance and Security
- Under the hood of CSS and JS animations + how to optimize their performance
- Parsing, Abstract Syntax Trees (ASTs) + 5 tips on how to minimize parse time
- The internals of classes and inheritance + transpiling in Babel and TypeScript
- Storage engines + how to choose the proper storage API
- The internals of Shadow DOM + how to build self-contained components
- WebRTC and the mechanics of peer to peer connectivity
- Under the hood of custom elements + Best practices on building reusable components
- Exceptions + best practices for synchronous and asynchronous code
- 5 types of XSS attacks + tips on preventing them
- CSRF attacks + 7 mitigation strategies
- Iterators + tips on gaining advanced control over generators
- Cryptography + how to deal with man-in-the-middle (MITM) attacks
- Functional style and how it compares to other approaches
- Three types of polymorphism
- Regular expressions (RegExp)
- Introduction to Deno
- Creational, Structural, and Behavioural design patterns + 4 best practices
- Modularity and reusability with MVC
- Cross-browser testing + tips for prerelease browsers
- The “this” variable and the execution context
- High-performing code + 8 optimization tips
- Debugging overview + 4 tips for async code