In this article I talk about what I've learned about how to know where this
points to in a given function. Basically this is me sharing with you, in my own words how to do so.
And yes, I did that weird drawing at the top 😀
Firstly, it is important to understand that the this binding
is not determined when a function is declared, instead, it is determined by where that function is invoked, and also based on how that function was invoked.
Step 1: WHERE
The first thing we need to do is find where the function was invoked in our program. It could have been invoked from either the global execution context or from a local execution context , and the only way to find our function's call-site (besides watching directly in our code) is by looking at the call stack. Here's a very simple example that you can try in the console in order to see the stack.
First, copy and paste the following code in your browser's console:
function baz() {
bar()
}
function bar() {
foo()
}
function foo() {
debugger
}
baz()
Then, in the devtools, under the sources tab, and then under the Call Stack section, you will see a list of functions. This way we can know for sure that foo() call-site
is bar() , and bar() call-site
is baz(), and finally baz() call-site
is the global execution context, which in this case is shown as anonymous.
foo (VM431:10)
bar (VM431:6)
baz (VM431:2)
(anonymous) (VM431:13)
Now that we know how to find our function's call-site (where) , let's talk about the set of rules that determine the this binding
(how) .
Step 2: HOW
When a function is invoked, a new Local Execution Context is created. That Local Execution Context has information about the function (its place in the call stack, the arguments length and - among other things - a property called this
).
The value of this
(what object is it pointing to) is determined based on how the function is invoked.
We can invoke our functions in 4 different ways, following 4 different rules, namely:
- Default Binding
- Implicit Binding
- Explicit Binding
- New Binding
Extra: I will also talk about how the this binding
is determined on arrow functions.
Default Binding
var x = 20
function foo() {
console.log(this.x)
}
foo.x = 40
foo() // 20
A default binding
is made when we do a regular function call, like we did here with foo()
. In non-strict
mode the this binding
will reference the global object, but on strict mode
it will be undefined
.
It's worth mentioning that in the first line we declare a variable x
and assign the value of 20. And this is like doing window.x = 20
. Long story short, a property is created in the global object, and this is the reason why this.x
is 20.
When foo
is invoked, something like this happens under the hood:
foo.call(window) // non-strict
foo.call(undefined) // strict
Even though we will revisit this subject later in one of the 4 rules, I'll briefly explain what's the call()
method doing here: The call()
method is explicitly setting to what object this
will be bound to.
Implicit Binding
When we invoke a function in the context of an object, this
will point to that object. Let's take a look at the following code:
var x = 20
const myObj = {
x: 50,
foo: function() {
console.log(this.x)
}
}
myObj.foo() // 50
I would like to clarify that the anonymous function declaration in myObj.foo
(aka method, since it is declared inside an object) does not belong to myObj
. Remember that since functions are callable objects
, they are assigned by reference (like all objects are), unlike the primitive values, which are assigned by copy.
In order to illustrate my point, consider the following code:
var x = 20
const myObj = {
x: 50,
foo: function() {
console.log(this.x)
}
}
myObj.foo() // 50
const foo = myObj.foo
foo() // 20
When we declare const foo
, we assign a reference to the same function myObj.foo
is pointing to, and then, by doing a stand-alone invocation of foo
, the default binding rule is applied, and since we're not using strict-mode
, this
will point to the global object
, in this case, the window
.
As you can see, and like I said before, the binding of this
is not determined when the function is declared, but when the function is invoked and most importantly on how that function is invoked.
Explicit Binding
All functions have access to three different methods that allow us to invoke them and explicitly set the object that this
will be bound to. I'm talking about the call()
, apply()
and bind()
methods.
Consider the following code:
const obj = {
x: 'Hi there'
}
function foo(name, age) {
console.log(
`${this.x}, my name is ${name}, and I'm ${age} years old`
)
}
foo.call(obj, 'Diego', 31)
// 'Hi there, my name is Diego, and I'm 31 years old'
foo.apply(obj, ['Diego', 31])
// 'Hi there, my name is Diego, and I'm 31 years old'
const bar = foo.bind(obj, 'Diego', 31)
bar() // 'Hi there, my name is Diego, and I'm 31 years old'
Let's talk about each of the call methods in our snippet:
call() : Invokes and receives (as its first argument) an object that will explicitly be bound to
this
. It also receives the function's arguments separated by a comma.apply() : It does the same thing as call(), but the only difference is that the arguments are passed inside an array.
bind() : It's also similar to call() but instead of immediately invoking the function, it returns a function with
this
bound to the object passed as its first argument. In this snippet we store the returned function in aconst
and below that we make the invocation.
New Binding
A function invocation with the new
keyword at the beginning is referred to as a constructor call
. Let's now consider the following code snippet:
function foo(name, age) {
this.name = name
this.age = age
}
const bar = new foo('Diego', 31)
console.log(
`My name is ${bar.name}, and I'm ${bar.age} years old`
)
// My name is Diego, and I'm 31 years old
When we do a constructor call
on the foo method, this is what happens:
First, it creates and return a new object. Something like
Object.create({})
.this
will point to the newly created object, which in this case has a reference to that object inbar
.And lastly, the newly created object is linked to the function's prototype. In other words, the
bar
object delegates its[[Prototype]] / __proto__
to thefoo
'sprototype
object.
Just as a refresher, all functions have a prototype
object. It only has one property, constructor
, which happens to be a reference to the function itself.
foo.prototype
/*
Output:
{ constructor: ƒ foo(name, age), __proto__: Object.prototype }
*/
bar.__proto__
// or
Object.getPrototypeOf(bar)
/*
Output:
{ constructor: ƒ foo(name, age), __proto__: Object.prototype }
*/
foo.prototype === bar.__proto__ // true
foo.prototype === Object.getPrototypeOf(bar) // true
These are the 4 rules that will determine the this binding
of a function. So now we know the questions we need to ask ourselves in order to know where this
is pointing, namely:
- where has the function been invoked?
- how the function was invoked?
Arrow functions and this
But there's one more thing to consider...
Unlike the 4 rules above, the this binding
in arrow functions is determined by its parent scope. In other words, the this binding
of an arrow function is the same as its container function:
var name = 'Global'
function foo() {
const bar = () => {
console.log(this.name)
}
return bar
}
const obj = {
name: 'Diego'
}
const fn = foo()
fn() // 'Global'
const fn2 = foo.call(obj)
fn2() // 'Diego'
When the foo
function is invoked, the arrow function will inherit the this
from foo
.
In const fn = foo()
since foo()
invocation is a regular/normal function call, the Default Binding rule is applied, so in this case the foo's this
points to the window
object (if we are on strict mode
it will be undefined
).
But, in const fn2 = foo.call(obj)
, the Explicit Binding rule is applied, since we're explicitly setting the obj that will be bound to foo's this
, which is the obj
object.
And even if we do a fn2()
(invoking our returned arrow function) which according to the 4 rules is a Default Binding, it will ignore those rules, and use the this binding
of foo's invocation, in this case obj
.
Final words
Like I said at the begging, this post is me writing in my own words what I learned from the YDKJS book series, specifically the from the this & Object Prototypes
book from Kyle Simpson. I fully recommend all the books from the series.
Top comments (0)