this: execution context of functions

About context and this is often asked in interviews. Let's answer in detail and investigate the nuances.

Time to read: 9 min

Briefly

Roughly speaking, this is a reference to some object, the properties of which can be accessed within the function call. This this is the execution context.

But to better understand what this and execution context are in JavaScript, we need to take a roundabout way.

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

We can execute something in JS in 4 ways:

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

Function

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

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

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

        
        
          
        
      

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

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

Global Object

The global object is, so to speak, the root object in the program.

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

Strict Mode

It can be said that strict mode is an ungraceful way of fighting legacy.

Strict mode is enabled with the directive 'use strict' at the beginning of a block that must run in strict mode:

        
          
          function nonStrict() {  // Will run in non-strict mode}function strict() {  'use strict'  // Will run in strict mode}
          function nonStrict() {
  // Will run in non-strict mode
}

function strict() {
  'use strict'
  // Will run in strict mode
}

        
        
          
        
      

You can also set strict mode for the entire file by specifying 'use strict' at the beginning.

The Value of this

Let's return to this. In non-strict mode when executed in a browser, this when calling a function will be equal to window:

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

whatsThis()
// true

        
        
          
        
      

The same is true if the function is declared inside another function:

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

  whatInside()
}

whatsThis()
// true

        
        
          
        
      

And the same goes if the function is anonymous and, for example, called immediately:

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

        
        
          
        
      

In the example above, you may notice the ; before the anonymous function. This is because the existing mechanism of automatic semicolon insertion (ASI) only triggers in specific cases, while a line starting with ( is not included in that list. Therefore, experienced developers often add ; in situations where their code might be copied and appended to existing code.

In strict mode, the value will be undefined:

        
          
          'use strict'function whatsThis() {  console.log(this === undefined)}whatsThis()// true
          '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: 'Alex',  greet() {    console.log('Hello, my name is Alex')  },}user.greet()// Hello, my name is Alex
          const user = {
  name: 'Alex',
  greet() {
    console.log('Hello, my name is Alex')
  },
}

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

        
        
          
        
      

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

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

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

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

        
        
          
        
      

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

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

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

        
        
          
        
      

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

To prevent this, you should use bind(), which we will discuss a bit later.

Constructor Call

A constructor is a function used to create similar objects. Such functions are like a printing press that creates LEGO pieces. Similar objects are the pieces, and the constructor is the machine. It "constructs" these objects, hence the name.

According to conventions, constructors are called using the keyword new and are named with a capital letter, usually as a noun rather than a verb. The noun is the entity created by the constructor.

For example, if the constructor creates user objects, we can name it User, and use it like this:

        
          
          function User() {  this.name = 'Alex'}const firstUser = new User()firstUser.name === 'Alex'// true
          function User() {
  this.name = 'Alex'
}

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

        
        
          
        
      

When calling a constructor, this is equal to the newly created object.

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

        
          
          function User() {  console.log(this instanceof User)  // true  this.name = 'Alex'}const firstUser = new User()firstUser instanceof User// true
          function User() {
  console.log(this instanceof User)
  // true
  this.name = 'Alex'
}

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

        
        
          
        
      

In fact, a lot happens "behind the scenes":

  • When calling, a new empty object is created and assigned to this.
  • The function code executes. (Typically, it modifies this, adding new properties.)
  • The value of this is returned.

If we were to detail all the implicit steps:

        
          
          function User() {  // Happens implicitly:  // this = {};  this.name = 'Alex'  // Happens implicitly:  // return this;}
          function User() {
  // Happens implicitly:
  // this = {};

  this.name = 'Alex'

  // Happens implicitly:
  // return this;
}

        
        
          
        
      

The same occurs with ES6 classes; you can learn more about them in the article on object-oriented programming.

        
          
          class User {  constructor() {    this.name = 'Alex'  }  greet() {    /*...*/  }}const firstUser = new User()
          class User {
  constructor() {
    this.name = 'Alex'
  }

  greet() {
    /*...*/
  }
}

const firstUser = new User()

        
        
          
        
      

How to Remember new

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

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

        
        
          
        
      

Although at first glance there is no difference, and it seems to work correctly. But in reality, there is a difference:

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

console.log(secondUser)
// undefined

        
        
          
        
      

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

        
          
          function User() {  if (!(this instanceof User)) {    throw Error('Error: Incorrect invocation!')  }  this.name = 'Alex'}// orfunction User() {  if (!new.target) {    throw Error('Error: Incorrect invocation!')  }  this.name = 'Alex'}const secondUser = User()// Error: Incorrect invocation!
          function User() {
  if (!(this instanceof User)) {
    throw Error('Error: Incorrect invocation!')
  }

  this.name = 'Alex'
}

// or

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

  this.name = 'Alex'
}

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

        
        
          
        
      

Indirect Call

An indirect call refers to calling functions through call() or apply().

Both accept this as the first argument. That is, they allow the context to be set from the outside, and also — explicitly.

        
          
          function greet() {  console.log(`Hello, ${this.name}`)}const user1 = { name: 'Alex' }const user2 = { name: 'Ivan' }greet.call(user1)// Hello, Alexgreet.call(user2)// Hello, Ivangreet.apply(user1)// Hello, Alexgreet.apply(user2)// Hello, Ivan
          function greet() {
  console.log(`Hello, ${this.name}`)
}

const user1 = { name: 'Alex' }
const user2 = { name: 'Ivan' }

greet.call(user1)
// Hello, Alex
greet.call(user2)
// Hello, Ivan

greet.apply(user1)
// Hello, Alex
greet.apply(user2)
// Hello, Ivan

        
        
          
        
      

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

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

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

        
          
          function greet(greetWord, emoticon) {  console.log(`${greetWord} ${this.name} ${emoticon}`)}const user1 = { name: 'Alex' }const user2 = { name: 'Ivan' }greet.call(user1, 'Hello,', ':-)')// Hello, Alex :-)greet.call(user2, 'Good morning,', ':-D')// Good morning, Ivan :-Dgreet.apply(user1, ['Hello,', ':-)'])// Hello, Alex :-)greet.apply(user2, ['Good morning,', ':-D'])// Good morning, Ivan :-D
          function greet(greetWord, emoticon) {
  console.log(`${greetWord} ${this.name} ${emoticon}`)
}

const user1 = { name: 'Alex' }
const user2 = { name: 'Ivan' }

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

        
        
          
        
      

Function Binding

bind() stands out on its own. It is a method that allows binding the execution context to a function to "pre-determine" exactly what value this will have.

        
          
          function greet() {  console.log(`Hello, ${this.name}`)}const user1 = { name: 'Alex' }const greetAlex = greet.bind(user1)greetAlex()// Hello, Alex
          function greet() {
  console.log(`Hello, ${this.name}`)
}

const user1 = { name: 'Alex' }

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

        
        
          
        
      

Note that bind(), unlike call() and apply(), does not invoke the function immediately. Instead, it returns another function — permanently bound to the specified context. 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
          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 bound to the closest context in which they are defined.

This is convenient when we need to pass a parent context to the arrow function without using bind().

        
          
          function greetWaitAndAgain() {  console.log(`Hello, ${this.name}!`)  setTimeout(() => {    console.log(`Hello again, ${this.name}!`)  })}const user = { name: 'Alex' }user.greetWaitAndAgain = greetWaitAndAgain;user.greetWaitAndAgain()// Hello, Alex!// Hello again, Alex!
          function greetWaitAndAgain() {
  console.log(`Hello, ${this.name}!`)
  setTimeout(() => {
    console.log(`Hello again, ${this.name}!`)
  })
}

const user = { name: 'Alex' }

user.greetWaitAndAgain = greetWaitAndAgain;
user.greetWaitAndAgain()

// Hello, Alex!
// Hello again, Alex!

        
        
          
        
      

When using a regular function inside, the context would be lost, and to achieve the same result, we would have to use call(), apply(), or bind().

In practice

Advice 1

🛠 Flexible, non-fixed context in JavaScript is both convenient and dangerous at the same time.

It is convenient because we can write very abstract functions that will use the execution context for their work. Thus, we can achieve polymorphism.

However, at the same time, the flexible this can also be the cause of errors, for example, if we use a constructor without new or simply confuse the execution context.

🛠 Always use 'use strict'.

This refers more to writing code in general rather than specifically to context 🙂

However, with strict mode, errors that creep in can be detected earlier. For example, in non-strict mode, if we forget new, name will become a property on the global object.

        
          
          function User() {  this.name = 'Alex'}const user = User()// window.name === 'Alex';// user === window
          function User() {
  this.name = 'Alex'
}

const user = User()
// window.name === 'Alex';
// user === window

        
        
          
        
      

In strict mode, we will get an error because initially, the context inside the function in strict mode is undefined:

        
          
          function User() {  'use strict'  this.name = 'Alex'}const user = User()// Uncaught TypeError:// Cannot set property 'name' of undefined.
          function User() {
  'use strict'
  this.name = 'Alex'
}

const user = User()
// Uncaught TypeError:
// Cannot set property 'name' of undefined.

        
        
          
        
      

🛠 Always use new and put checks in the constructor.

When using constructors, always use new. This will protect you from errors and will not mislead developers who will read the code afterwards.

And to protect "against stupidity," it is advisable to put checks inside the constructor:

        
          
          function User() {  if (!(this instanceof User)) {    throw Error('Error: Incorrect invocation!')  }  this.name = 'Alex'}const secondUser = User()// Error: Incorrect invocation!
          function User() {
  if (!(this instanceof User)) {
    throw Error('Error: Incorrect invocation!')
  }

  this.name = 'Alex'
}

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

        
        
          
        
      

🛠 Auto-bind for class methods.

In ES6, classes appeared, but they do not work in older browsers. Usually, developers transpile code— that is, translate it using various tools into ES5.

It may happen that during transpilation, if it is set up incorrectly, class methods will not recognize this as an instance of the class.

        
          
          class User {  name: 'Alex'  greet() {    console.log(`Hello ${this.name}`)  }}// this.name may be undefined;// this may be undefined
          class User {
  name: 'Alex'
  greet() {
    console.log(`Hello ${this.name}`)
  }
}

// this.name may be undefined;
// this may be undefined

        
        
          
        
      

To defend against this, you can use arrow functions to create class fields.