Understanding “this” in JavaScript by focusing on “where” and “how” a function is invoked

diegolepore

Diego Palacios Lepore

Posted on September 20, 2020

Understanding “this” in JavaScript by focusing on “where” and “how” a function is invoked

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()
Enter fullscreen mode Exit fullscreen mode

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) 
Enter fullscreen mode Exit fullscreen mode

Alt Text

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 

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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'

Enter fullscreen mode Exit fullscreen mode

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 a const 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

Enter fullscreen mode Exit fullscreen mode

When we do a constructor call on the foo method, this is what happens:

  1. First, it creates and return a new object. Something like Object.create({}).

  2. this will point to the newly created object, which in this case has a reference to that object in bar.

  3. And lastly, the newly created object is linked to the function's prototype. In other words, the bar object delegates its [[Prototype]] / __proto__ to the foo's prototype object.

Alt Text

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
Enter fullscreen mode Exit fullscreen mode

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'

Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
diegolepore
Diego Palacios Lepore

Posted on September 20, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related