Writing JavaScript can be a menace to both rookie and experienced developers alike due to some of its unorthodox implementations of popular programming concepts. This article tackles the scenario where two tricky concepts work hand in hand to frustrate the unsuspecting programmer:
this
(context)Each of these can already be a nightmare to work with, but it gets even trickier when the challenge is to access the correct this
within a callback. In this article, we will figure this out and see how we can explicitly force a context binding to point to our object of choice.
For us to tread gently, we have to recap what a callback is.
A callback is a function that is passed as an argument to another function. Usually, the callback is then invoked at some point within the outer function.
Note: The outer function that takes in a callback is called a higher-order function.
Since a callback is a function and functions are objects in JavaScript, a callback has its own set of methods and properties. When a callback is executed within a higher-order function, it gets assigned a this
property that is completely dependent on how it is invoked and not where/how/when it was defined.
We can trace the this
value within a callback by looking within the higher-order function where it is invoked. Most of the problems with this
in callbacks are due to the fact that the actual definition of the enclosing function might have locally scoped properties. When that property is accessed using a this
binding within the callback, however, it doesn’t exist because the context of the callback changes dynamically depending on how it is invoked.
Pro tip: When a function (callback) is invoked, the JavaScript interpreter creates an execution record (execution context), and this context contains information about the function. Amongst other things is the
this
reference, which is available for the duration of the function’s execution.
Here’s an example of a callback function:
function HOF(callback){ callback(); } function callback(){ console.log(this); } HOF(callback) // points to the global Window Object
In the example above, we have a function called HOF (higher-order function), and it takes in a callback function that logs its this
value to the console.
This is a great example of tracing down the this
value within the callback to see where it is invoked because the context of a callback changes, and its this
value is reassigned depending on how it is being invoked within the enclosing function.
Note: In a callback that is invoked by an enclosing function, the
this
context changes. The valuethis
holds is reassigned to the function that is calling the function — the call site.
In this case, the enclosing function — HOF
— is defined and called in the global scope so the this
binding within the callback will point to the Window
object.
Note: The
Window
object is a client object that represents an open window in the browser.
Let’s have a look at some of the behaviors of the this
value when used under different scenarios:
function bar() { console.log(this); } bar(); // points to the global Window Object
This is pretty straightforward. The bar()
function is in the global scope, so its this
value will point to the Window
object. If we took that same function and made it into a method on an object, however, we get a different binding:
let sample = {bar: bar}; sample.bar(); // points to the object above
The output of this code will point to the sample
object we just created. This is perhaps the most expected and intuitive binding; we tend to expect the this
value to refer to the object to the left-hand side of the dot, but this isn’t always the case in JavaScript.
And, finally, if used in a new
constructor:
new bar();
The output of this code will point to an object that inherits from bar.prototype
.
This is all fairly straightforward until we have situations with nested callbacks where it seems like a function should have a this
binding that refers to its lexical enclosing function that possesses all the properties defined at author time. But at this point, we tend to overlook the fact that a function’s context binding is completely independent of its lexical declaration and is determined by how it is invoked.
When this becomes the case, there are a few ways to resolve bugs that arise from being unable to access the correct this
in a callback.
this
inside a callbackJavaScript arrow functions were introduced in ECMAScript 6. They’re the compact alternative to a traditional function expression and do not have their own this
binding. This ensures that whenever a reference to this
is used within an arrow function, it is looked up in scope like a normal variable.
Let’s have a quick look at this Stack Overflow problem that’s centered around the this
binding in a callback:
function MyConstructor(data, transport) { this.data = data; transport.on('data', function () { console.log(this.data); }); } // Mock transport object let transport = { on: function(event, callback) { setTimeout(callback, 1000); } }; // called as let obj = new MyConstructor('foo', transport);
This is one of the trickier scenarios where the this
binding within the callback refers to the Window
object and seems difficult to trace and debug. When we run this code, it outputs undefined
, but we can easily solve this problem by changing the anonymous function expression to an arrow function. The code then becomes:
[...] transport.on('data', () => { console.log(this.data); }); } [...]
That’s it — it’s as easy as changing a few characters in the function declaration, and we’ve solved the this
binding problem.
this
objectMost times, when we try to access this
within a callback, what we really want access to is the object it points to. A way to achieve this is to create a variable and store the value of this
just before the callback scope (although some programmers would rather not because it seems messy).
I’ve seen some people call it that
or self
, but it really doesn’t matter what it’s called as long as it’s intuitive enough. This hack works because the variable obeys the rules of lexical scope and is therefore accessible inside the callback. An extra benefit to this method is that you still have access to whatever the dynamic this
binding of the callback is.
Here’s an example of what it would look like using the snippet above:
function MyConstructor(data, transport) { this.data = data; let that = this; transport.on('data', function() { alert(that.data); }); }
This, like the solution before it, solves the problem of accessing this
within a callback.
this
to an objectWe can explicitly specify what we want this
to be when we define a callback. Using the bind()
method, we can set the this
value and be certain that it’ll remain that way during its execution no matter how or where the function is called or passed.
Every function has the bind()
method that returns a new function with its this
property bound to a specified object. The returned function will have the exact behavior as the original function; the only difference is that you have complete control over what the this
property points to.
Let’s take the same code snippet for example:
function MyConstructor(data, transport) { this.data = data; let boundFunction = (function() { alert(this.data); }).bind(this); // we call bind with the `this` value of the enclosing function transport.on('data', boundFunction); }
This solves the problem and gives us great control over the this
binding of the callback.
We’ve had a superficial exploration of two of the trickiest and most daunting concepts in modern JavaScript. Whenever you are within a codebase that has callbacks and it seems to be accessing the wrong this
, try tracing the callback’s execution within the higher-order function to find a clue to what its this
binding might be, depending on how the higher-order function is called.
If that fails or proves difficult, remember your arsenal of techniques in rectifying this menace.
Debugging code is always a tedious task. But the more you understand your errors, the easier it is to fix them.
LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to see exactly what the user did that led to an error.
LogRocket records console logs, page load times, stack traces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]