Overview

this is a reference to some object whose properties can be accessed inside the function call. This this is the execution context.

First, let's remember how we can basically execute an instruction in code.

There are 4 ways to execute something in JS:

  • By calling a function;
  • By calling an object method;
  • By using a constructor function;
  • By an indirect function call.

The first and easiest way to execute something is to call a function.

function hello(whom) {
  console.log(`Hello, ${whom}!`)
}

hello('World') // Hello, World!

To execute the function, we use the hello expression and parentheses with arguments.

When we call a function, the value of this can only be a global object or undefined when using 'use strict'.

Global Object

A global object is, so to say, the root object in a programme.

If we run JS code in a browser, the global object will be window. If we run code in Node-environment, then global.

Strict Mode

You could say that a strict mode is a non-punitive way of dealing with legacy.

Strict mode is enabled by using the 'use strict' directive at the beginning of a block that is to be executed in strict mode:

function nonStrict() {
  // To be carried out in a non-strict mode.
}

function strict() {
  'use strict'
  // To be carried out in a strict mode.
}

You can also set strict mode for the entire file if you specify 'use strict' at the beginning.

Meaning of this

Let's get back to this. In the non-strict mode when executed in a browser, this will be equal to window when the function is called:

function whatsThis() {
  console.log(this === window)
}

whatsThis() // true

The same - if the function is declared inside a function:

function whatsThis() {
  function whatInside() {
    console.log(this === window)
  }

  whatInside()
}

whatsThis() // true

And the same - if the function will be anonymous and, for example, called immediately:

;(function () {
  console.log(this === window)
})() // true

In the above example, you may notice ; before the anonymous function. The point is that the existing auto-separated semicolon insertion (ASI) mechanism works only in certain cases, while a string starting with ( is not included in the list of these cases. That's why experienced developers often add ; in those cases when their code can be copied and added to the existing code.

In strict mode - the value will be undefined:

'use strict'

function whatsThis() {
  console.log(this === undefined)
}

whatsThis() // true

Object Method

If a function is stored in an object, it is a method of that object.

const user = {
  name: 'Alice',
  greet() {
    console.log('Hello, my name is Alice')
  },
}

user.greet() // Hello, my name is Alice

user.greet() is a method of the user object.

In this case, the value of this is this object.

const user = {
  name: 'Alice',
  greet() {
    console.log(`Hello, my name is ${this.name}`)
  },
}

user.greet() // Hello, my name is Alice

Note that this is defined at the time the function is called. If you write an object method to a variable and call it, the value of this will change.

const user = {
  name: 'Alice',
  greet() {
    console.log(`Hello, my name is ${this.name}`)
  },
}

const greet = user.greet
greet() // Hello, my name is

When user.greet() is called through a point, this equals the object before the point (user). Without this object, this equals the global object (in normal mode). In strict mode we would get the error "Cannot read properties of undefined".

To prevent this from happening, you should use bind().

Constructor Call

A constructor is a function we use to create objects of the same type. Such functions are like a printing press that creates LEGO parts. Same-type objects are parts, and a constructor is a machine. It sort of constructs these objects, hence the name.

By convention, constructors are invoked with the keyword new, and also named with a capital letter, and usually with a noun rather than a verb. The noun is the entity that the constructor creates.

For example, if the constructor is going to create user objects, we can call it User, and use it like this:

function User() {
  this.name = 'Alica'
}

const firstUser = new User()
firstUser.name === 'Alica' // true

When the constructor is called, this equals the freshly created object.

In the User example, the value of this would be the object that the constructor creates:

function User() {
  console.log(this instanceof User) // true
  this.name = 'Alice'
}

const firstUser = new User()
firstUser instanceof User // true

In fact, there is a lot going on "behind the scenes":

  • When called, a new empty object is first created and assigned to this.
  • The function code is executed. (Usually it modifies this, adds new properties to it).
  • The value of this is returned.

If you schedule all the implicit steps, then:

function User() {
  // Happens implicitly:
  // this = {};

  this.name = 'Alice'

  // Happens implicitly:
  // return this;
}

The same thing happens in ES6 Classes:

class User {
  constructor() {
    this.name = 'Alice'
  }

  greet() {
    // ...
  }
}

const firstUser = new User()

How not to Forget about the new

When working with constructor functions, it's easy to forget about new and call them incorrectly:

const firstUser = new User() // ✅
const secondUser = User() // ❌

Although at first glance there is no difference, and it works as if it is correct. But in reality, there is a difference:

console.log(firstUser) // User { name: 'Alice' }
console.log(secondUser) // undefined

To avoid falling into such a trap, you can write a check in the constructor that a new object has been created:

function User() {
  if (!(this instanceof User)) {
    throw Error('Error: Incorrect invocation!')
  }

  this.name = 'Alice'
}

// Or:

function User() {
  if (!new.target) {
    throw Error('Error: Incorrect invocation!')
  }

  this.name = 'Alice'
}

const secondUser = User() // Error: Incorrect invocation!

Indirect Call

An indirect call is a function call via call() or apply().

Both take this as their first argument. That is, they allow you to set the context externally, moreover - explicitly.

function greet() {
  console.log(`Hello, ${this.name}`)
}

const user1 = { name: 'Alice' }
const user2 = { name: 'James' }

greet.call(user1) // Hello, Alice
greet.call(user2) // Hello, James

greet.apply(user1) // Hello, Alice
greet.apply(user2) // Hello, James

In both cases, in the first call this === user1, in the second call user2.

The difference between call() and apply() is how they take arguments for the function itself after this.

call() accepts arguments in a comma-separated list, apply() accepts an array of arguments. Otherwise they are identical:

function greet(greetWord, emoticon) {
  console.log(`${greetWord} ${this.name} ${emoticon}`)
}

const user1 = { name: 'Alice' }
const user2 = { name: 'James' }

greet.call(user1, 'Hello,', ':-)') // Hello, Alice :-)
greet.call(user2, 'Good morning,', ':-D') // Good morning, James :-D
greet.apply(user1, ['Hello,', ':-)']) // Hello, Alice :-)
greet.apply(user2, ['Good morning,', ':-D']) // Good morning, James :-D

Linking Functions

A standout is bind(). This is a method that allows you to bind the execution context to a function to determine "in advance and precisely" what value this will have.

function greet() {
  console.log(`Hello, ${this.name}`)
}

const user1 = { name: 'Alice' }

const greetAlex = greet.bind(user1)
greetAlex() // Hello, Alice

Note that bind(), unlike call() and apply(), does not call a function immediately. Instead, it returns another function - bound to the specified context forever. The context of this function cannot be changed.

function getAge() {
  console.log(this.age)
}

const howOldAmI = getAge.bind({age: 20}).bind({age: 30})

howOldAmI() // 20

Arrow Functions

Arrow functions do not have their own execution context. They are linked to the closest hierarchical context in which they are defined.

This is useful when we need to pass in an arrow function, for example, a parent context without using bind().

function greetWaitAndAgain() {
  console.log(`Hello, ${this.name}!`)

  setTimeout(() => {
    console.log(`Hello again, ${this.name}!`)
  })
}

const user = { name: 'Alice' }

user.greetWaitAndAgain = greetWaitAndAgain;
user.greetWaitAndAgain()

// Hello, Alice!
// Hello again, Alice!

With a normal function inside, the context would be lost and we would have to use call(), apply() or bind() to get the same result.

Support ❤️

It took a lot of time and effort to create this material. If you found this article useful or interesting, please support my work with a small donation. It will help me to continue sharing my knowledge and ideas.

Make a contribution or Subscription to the author's content: Buy me a Coffee, Patreon, PayPal.