How `this` keyword is determined in Javascript
One of the most confused things in Javascript is the this
keyword. It is a special identifier keyword and automatically defined and bind to a specific context at running-time of function call. Determining which context this
keyword is pointing to is a easy-to-make-mistake work even seasoned developer.
Keyword this
in Javascript acts in a different way.
If you come from another OOP (Object Oriented Programming) language like Java, C#, C++,...
, you may be very familiar with mentioning this
keyword in a class and treat it like a itself
object. It is very intuitive and this
point to the same context wherever your function is invoked.
Unfortunately, this
keyword in Javascript was not designed for OOP, it acts in a strange way and different from many other common languages. So, in my opinion, before learning this
in Javascript, do not try to find any relations, or common traits with others.
What is this
?
Keyword this
is not a thing which can be determined at writing-time (function declaration). Instead, we control it at running-time (function invocation) and know that which object this
is pointing to. At this time, this
acts like a normal object, it just looks like a replacement in your code.
So, this
is contextual based on the conditions of the function's invocation, this
binding has nothing to do with where a function is declared, but has instead everything to do with the manner in which the function is called. The hard part to work with this
is just to determine how function's execution will bind this
or which object this
is pointing to at that time. Let's see.
Determine the call-site
of function
To understand this
, we have to understand the call-site. Call-site is the location in code where a function is called (not where it's declared). Finding the call-site is generally go locate where a function is called from, it's not always easy, a complex codebase and high function call stack can obscure the true call-site.
Let's demonstrate call-site in code.
function baz() {
console.log('baz');
bar(); // <-- call-site for bar
}
function bar() {
console.log('bar');
foo(); // <-- call-site for foo
}
function foo() {
console.log('foo');
}
baz(); // <-- call-site for baz
Fortunately, we have rules to determine!
-
Explicit Binding
As the name said, this case is quite simple and clear, we specify the this
explicitly on function invocation.
All functions in Javascript can access some methods from their Prototype
to invoke with a specific this
object (or context).
For instance, the Function.prototype.call(...)
and Function.prototype.apply(...)
both take, as their first parameter, an object to use for the this
and then invoke the function with that this
specified.
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
};
foo.call(obj); // 2
foo.apply(obj); // 2
Another method Function.prototype.bind(thisArg)
allow us to bind this
of current function to a specified context. It returns a new function with hard-binding this
. We can call it many times without rebinding this
context. Technically, it behaves like a decorated function.
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
};
var bar = foo.bind(obj);
bar(); // 2
-
Implicit Binding
In Javascript, object can have a property linked to a function. So from an object perspective, it can access and point to function, then call it normally. On this case, the call-site of the invoked function is the object it belongs to. So this
context inside function will reference to parent object.
Let's see!
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo,
};
obj.foo(); // 2
-
Default Binding
This rule comes from the most common case of function calls: standalone function invocation. Consider this code!
function foo() {
console.log(this.a);
}
var a = 2;
foo(); // 2
Variable a
is declared in the global scope and synonymous with global-object properties of the same name. We see that when foo()
function is called, this.a
resolves to our global variable a
. In other words, this
is pointed to global object.
If strict mode
is in effect, the global object is not eligible for the default binding, so the this
instead set to undefined
function foo() {
'use strict';
console.log(this.a);
}
var a = 2;
foo(); // TypeError: `this` is `undefined`
-
new
Binding
When a function invoked with new
in front of it, the following things are done automatically:
- a brand new object is created
- the newly constructed object is et as the
this
binding for that function call - unless function return its own alternate object, the
new
-invoked function call will automatically return the newly constructed object
Consider this code!
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2
By calling foo(...)
with new
in front of it, we have constructed a new object and set that new object as this
for the call of foo(...)
. It's called new
binding.
What about precedence?
So, we have uncovered rules for binding this
in function calls, all we need to do is find the call-site and inspect it to see which rule applies. But, what if the found call-site can be applied many rules at the same time. There must be an order of precedence to these rules, let's see.
Obviously, Default binding
is the lowest priority rule.
Explicit binding
takes precedence over Implicit binding
. So, before Implicit binding
can be applied, we have to ask first if Explicit binding
can be.
function foo() {
console.log(this.a);
}
var obj1 = {
a: 2,
foo: foo,
};
var obj2 = {
a: 3,
foo: foo,
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call(obj2); // 3
obj2.foo.call(obj1); // 2
Next, new Binding
is more precedent than Implicit binding
.
function foo(something) {
this.a = something;
}
var obj1 = {
foo: foo,
};
var obj2 = {};
obj1.foo(2);
console.log(obj1.a); // 2
obj1.foo.call(obj2, 3);
console.log(obj2.a); // 3
var bar = new obj1.foo(4);
console.log(obj1.a); // 2
console.log(bar.a); // 4
The rest work is to figure out where new Binding
and Explicit binding
(bind()
function) take precedence.
Note:
new
andcall
/apply
can not be used together, e.g syntaxnew foo.call(obj1)
is not valid in Javascript. So, there's no need to compare precedence betweennew Binding
andcall
/apply
. Consider this code.
function foo(something) {
this.a = something;
}
var obj1 = {};
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a); // 2
var baz = new bar(3);
console.log(obj1.a); // 2
console.log(baz.a); // 3
Although bar
function is hard-bound against obj1
, but when it is invoked with new
keyword, this
is referenced to baz
object. It means that new Binding
rule can override the hard-binding behavior of bind
function. Interesting!
There are always special cases
As usual, there are some exceptions to the "rules"!
-
Ignored
this
If you pass nullish value like null
or undefined
as a this
binding parameter to call
, apply
or bind
, those values are totally ignored by the engine. On this case, Default Binding
will apply to the invocation.
For example.
function foo() {
console.log(this.a);
}
var a = 2;
foo.call(null); // 2
-
Arrow-functions
Arrow-function is signified not by the function
keyword, but by the =>
annotation. Instead of using these rules, arrow-functions adopt the this
binding from the enclosing function or global scope.
Let see!
function foo() {
return (a) => {
// `this` here is lexically adopted from `foo()`
console.log(this.a);
};
}
var obj1 = {
a: 2,
};
var obj2 = {
a: 3,
};
var bar = foo.call(obj1);
bar.call(obj2); // 2, not 3!
The arrow-function created in foo()
lexically captures whatever foo()
's this
is its call-time. Because foo()
was this
-bound to obj1
, bar
will also be this
-bound to obj1
. And importantly, the lexical binding of an arrow function can not be overridden (event with new
).
Developer commonly use this kind of binding in arrow-function to handle events timers.
Conclusion
Determining the this
binding for an executing function requires finding the direct call-site of that function. Then, four rules can be applied to the call-site with precedence:
- Called with
new
-> Use the newly constructed object. - Called with
call
,apply
orbind
-> Use the specified object. - Called with a context object owning the call? Use that context object.
- Default ->
undefined
instrict mode
, otherwise global object
Be aware of some exceptions such as Ignored "this"
and Arrow function
when determining also.
Happy tracing!