Unexpected 'this': Binding Exceptions with this in JavaScript
#javascript, #this, #binding, #exceptions, #objects
Apr 03, 20245 min read
In the previous article, we delved into the four primary rules that dictate a this
binding in JavaScript functions. While these rules generally provide a solid framework for understanding this
behaviour, exceptions do exist. These exceptions can lead to unexpected binding behaviour, where the intended binding differs from the outcome dictated by the default binding rule.
Let’s explore these exceptions and learn how to identify and resolve them effectively.
This article is inspired by Kyle Simpson's book, "this & Object Prototypes," particularly Chapter 2: "this All Makes Sense Now." If you're interested in diving deeper into JavaScript's this
behaviour, I highly recommend reading this book.
Ignored this
If you pass null
or undefined
as a this
binding parameter to call()
, apply()
, or bind()
, those values are ignored, and instead default binding takes precedence. this
is substituted with the global object, undefined
in strict mode.
function foo() {
console.log(this.a);
}
var a = 2;
foo.call(null); // 2
Leveraging Null for this
Binding
Why would we want to intentionally pass something like null
for a this
binding, though?
Passing null
as the this
binding parameter in function calls might seem unusual, but it offers powerful functionalities like array spreading and partial application.
function foo(a, b) {
console.log('a:' + a + ', b:' + b);
}
// spreading out array as parameters
foo.apply(null, [2, 3]); // a:2, b:3
// partial application with `bind(..)`
var bar = foo.bind(null, 2);
bar(3); // a:2, b:3
Both methods require a this
binding for the first parameter. If the functions in question don’t care about this, you need a placeholder value, and null
might seem like a reasonable choice as shown in this snippet.
However, there’s a small risk in always using null
for this
binding. If a function (for instance, a third-party library function that you don’t control) makes a this
reference and you've passed null
, the default binding rule may cause it to unintentionally reference or modify the global object (like window
in the browser).
This can lead to hard-to-find bugs. And don’t we just love that.
Safer this
Instead of passing null
, it's safer to use a placeholder object as the this
binding. This ensures that any unexpected usage of this
is restricted to the placeholder object, preventing potential issues.
We can declare this placeholder object as ø
, or any other character, and create it using Object.create(null)
. This object acts as a "DMZ (demilitarized zone)" object, isolating the function call from any unintended side effects related to the this
binding. This DMZ is nothing more special than a completely empty, non-delegated object.
Object.create(null)
is similar to {}
, but without the delegation to Object.prototype
, so it’s more empty than just {}
.
function foo(a, b) {
console.log('a:' + a + ', b:' + b);
}
// our DMZ empty object
var ø = Object.create(null);
// spreading out array as parameters
foo.apply(ø, [2, 3]); // a:2, b:3
// partial application with `bind(..)`
var bar = foo.bind(ø, 2);
bar(3); // a:2, b:3
By using this approach, we ensure that the function's behaviour remains predictable and free from unexpected this
binding issues.
Indirection
Sometimes, assignments can lead to unexpected behaviour when it comes to function references. This is particularly evident when functions are indirectly referenced and then invoked.
Consider the following example:
function foo() {
console.log(this.a);
}
var a = 2;
var o = {
a: 3,
foo: foo,
};
var p = { a: 4 };
o.foo(); // 3
(p.foo = o.foo)(); // 2
At first glance, you might expect p.foo()
to output 4
, as p
has its own foo
property with a
set to 4
.
Let's break down what happens:
-
The assignment
p.foo = o.foo
assigns thefoo()
function from objecto
to thefoo
property of objectp
. -
After the assignment, the expression
(p.foo = o.foo)
evaluates to thefoo()
function itself.p.foo = o.foo; // ƒ foo()
-
The resulting function is then immediately invoked with
()
, without any binding context, wherethis
insidefoo()
refers to the global object (orundefined
in strict mode).
Conclusion
The behaviour of this
can sometimes create unexpected results, particularly when default binding takes precedence. Explicitly setting this
to null
can produce unintended consequences, since it defaults to the global object or undefined
in strict mode. However, by using a DMZ (demilitarized zone) object as a placeholder, we can ensure safer this
binding.
Also, remember that when functions are indirectly referenced and then invoked, we may encounter unintentional behaviour where this
is bound by default binding.
Curious about JavaScript's this
behaviour? Check out my previous articles where we explore this
and how it behaves in different contexts.
Back to Blogs