The this Keyword and Binding Rules in JavaScript

The this keyword is a fundamental concept in JavaScript that often confuses developers due to its dynamic nature. It plays a crucial role in how functions access their execution context, and understanding its binding rules is essential for writing accurate and efficient code. This lesson provides an in-depth exploration of how this is determined in various scenarios, including different function invocation patterns and the use of arrow functions.

Understanding the this Keyword

What is this?

  • Definition: this is a value available in an execution context. It is often an object, but in a strict function it can also be undefined, null, or a primitive.
  • Ordinary functions: The invocation supplies the receiver. Bound functions retain their bound receiver for ordinary calls.
  • Arrow functions: They have no own this; they use the enclosing lexical this instead.

State the Execution Context First

Run each example independently. Unless marked otherwise, ordinary functions below are non-strict. A file loaded as an ES module is strict automatically.

File contextTop-level thisthis in a plain call to a function defined there
Browser classic script, non-strictwindowwindow
Browser classic script with 'use strict'windowundefined
Node.js CommonJS file, non-strictmodule.exportsglobalThis
Node.js CommonJS file with 'use strict'module.exportsundefined
ES module, browser or Node.jsundefinedundefined

Strictness belongs to the called function, not its caller. A strict classic script still has top-level this === window; this is different from an ES module. In a browser classic script, top-level var becomes a global-object property. In CommonJS and ES modules it is module-scoped. Browser window.name initially contains an empty string, not undefined; assume a fresh, unnamed window in the examples below.

Why is this Important?

  • Accessing Object Properties: Methods can access the object they belong to using this.
  • Dynamic Context: Functions can be reused with different contexts.
  • Object-Oriented Programming: Essential for implementing classes and constructors.

Binding Rules for this

These are the binding categories, not their precedence. The decision checklist below shows which rule wins:

  1. Default Binding
  2. Implicit Binding
  3. Explicit Binding
  4. New Binding
  5. Arrow Functions

Default Binding

  • Context: When a function is called without any context (standalone function invocation).
  • Non-Strict Mode: this refers to the global object (window in browsers, global in Node.js).
  • Strict Mode: this is undefined.

Example (Non-Strict Mode):

function showThis() {
  console.log(this)
}

showThis() // Browser classic script: Window; non-strict CommonJS: globalThis

Example (Strict Mode):

'use strict'

function showThis() {
  console.log(this)
}

showThis() // Outputs: undefined

Implicit Binding

  • Context: When a function is called as a method of an object.
  • Binding Rule: this refers to the object before the dot notation used to invoke the function.

Example:

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

person.greet() // Outputs: Hello, my name is Alice.

Explanation:

  • this refers to person because greet is called as person.greet().

Explicit Binding

  • Context: .call() and .apply() invoke a function; .bind() creates a new callable without invoking it.
  • Binding Rule: For an unbound ordinary function, .call() and .apply() supply the receiver. A strict function keeps that exact value. A non-strict function substitutes globalThis for null/undefined and boxes other primitives. Arrows and already-bound functions do not accept a replacement this.

Using .call() and .apply():

  • .call(thisArg, arg1, arg2, ...): Invokes the function with this set to thisArg and arguments passed individually.
  • .apply(thisArg, [argsArray]): Invokes the function with this set to thisArg and arguments passed as an array.

Example:

function introduce(language1, language2) {
  console.log(`I'm ${this.name} and I speak ${language1} and ${language2}.`)
}

const person = { name: 'Bob' }

introduce.call(person, 'English', 'Spanish')
// Outputs: I'm Bob and I speak English and Spanish.

introduce.apply(person, ['French', 'German'])
// Outputs: I'm Bob and I speak French and German.

Using .bind():

  • Definition: Creates a function that supplies a fixed receiver on ordinary calls. Later .call(), .apply(), or .bind() calls cannot replace that receiver. If the target is constructible, invoking the bound function with new ignores the bound receiver.
  • Usage:
const boundFunction = introduce.bind(person, 'Italian', 'Portuguese')
boundFunction() // Outputs: I'm Bob and I speak Italian and Portuguese.

New Binding

  • Context: When a function is invoked as a constructor using the new keyword.
  • Binding Rule: this refers to the newly created object.

Example:

function Person(name) {
  this.name = name
  console.log(this)
}

const charlie = new Person('Charlie')
// Outputs: Person { name: 'Charlie' }

Explanation:

  • A new object is created, and this refers to that object.
  • The properties and methods are added to the new object.

Arrow Functions and this

  • Definition: Arrow functions (()=>{}) do not have their own this binding.
  • Binding Rule: this in an arrow function refers to this in the enclosing (lexical) scope.
  • Cannot be used as constructors: Arrow functions cannot be used with new.
  • Cannot rebind this: .call()/.apply() can pass arguments and .bind() can prefill arguments, but none changes an arrow's lexical this.

Example:

const person = {
  name: 'Diana',
  greet: function () {
    const innerFunc = () => {
      console.log(`Hello, my name is ${this.name}.`)
    }
    innerFunc()
  },
}

person.greet() // Outputs: Hello, my name is Diana.

Explanation:

  • The arrow function innerFunc inherits this from greet, which is person.

Contrast with Regular Function:

const person = {
  name: 'Eve',
  greet: function () {
    function innerFunc() {
      console.log(`Hello, my name is ${this.name}.`)
    }
    innerFunc()
  },
}

person.greet() // Non-strict browser script: Hello, my name is .

Explanation:

  • innerFunc() is a plain call, not a method call. In a fresh non-strict browser script it reads the empty window.name; in non-strict CommonJS, with no global name, it prints Hello, my name is undefined..
  • If innerFunc is strict (including in an ES module), this is undefined and reading this.name throws a TypeError before anything is logged.

Determining this Value: Precedence

First identify the function, then the invocation. Read the decision checklist downward and stop at the first matching rule.

  1. Arrow target, including an arrow behind .bind()? Use its enclosing lexical this. new on an arrow (even a bound one) throws; method syntax and explicit binding cannot change its this. The bound-receiver rule below applies to non-arrow targets.
  2. Construction with new? A constructible function receives a new instance, even if it was bound. Non-constructible functions throw instead. A constructor's explicitly returned object can differ from its internal this; derived-class this is initialized by super().
  3. Already bound? An ordinary call uses the bound receiver, even through another object or a later .call().
  4. .call() or .apply()? Use the supplied receiver, subject to the called function's strict/sloppy conversion rules.
  5. Method invocation such as person.greet() or person['greet']()? Use the base object (person).
  6. Plain call? this is undefined in a strict function, or globalThis in a non-strict function.

Interview check: obj.arrow() does not bind the arrow to obj, and bound.call(other) does not replace the bound receiver. Callback APIs may supply their own receivers; inspect how the API invokes the callback rather than assuming every callback is a plain call.

Practical Examples and Code Analysis

Example 1: Combining Binding Rules

function showName() {
  console.log(this.name)
}

const person1 = { name: 'Frank', showName: showName }
const person2 = { name: 'Grace' }

person1.showName() // Outputs: Frank (Implicit binding)
showName.call(person2) // Outputs: Grace (Explicit binding)
new showName() // Outputs: undefined (New binding)

Explanation:

  • person1.showName() uses implicit binding; this refers to person1.
  • showName.call(person2) uses explicit binding; this refers to person2.
  • new showName() uses new binding; this refers to the new object created by new.

Example 2: Arrow Functions and this

const obj = {
  value: 42,
  regularFunc: function () {
    console.log(this.value)
  },
  arrowFunc: () => {
    console.log(this.value)
  },
}

obj.regularFunc() // Outputs: 42
obj.arrowFunc() // Browser classic script or CommonJS: undefined, if value is absent

Explanation:

  • regularFunc uses implicit binding; this refers to obj.
  • arrowFunc inherits top-level this, not obj. In a classic browser script (even a strict one), it reads window.value; in CommonJS it reads module.exports.value. Both are absent in this standalone example. In an ES module, top-level this is undefined, so obj.arrowFunc() throws a TypeError.

Example 3: Methods Borrowed from Other Objects

const obj1 = {
  name: 'Heidi',
  greet: function () {
    console.log(`Hi, I'm ${this.name}.`)
  },
}

const obj2 = { name: 'Ivan' }

obj2.greet = obj1.greet
obj2.greet() // Outputs: Hi, I'm Ivan.

Explanation:

  • greet is assigned to obj2.
  • When obj2.greet() is called, this refers to obj2.

Example 4: Losing this Context

const obj = {
  name: 'Jack',
  getName: function () {
    return this.name
  },
}

const getNameFunction = obj.getName
console.log(getNameFunction()) // Non-strict browser script: "" (empty window.name)

Explanation:

  • getNameFunction is assigned obj.getName, but when called, it uses default binding.
  • this is undefined in strict mode or the global object in non-strict mode.
  • Thus a strict function throws on this.name; a non-strict CommonJS function returns undefined when globalThis.name is absent. A browser's existing window.name can change the non-strict result.

Solution: Use .bind()

const boundGetName = obj.getName.bind(obj)
console.log(boundGetName()) // Outputs: Jack

Best Practices

Use Arrow Functions for Lexical this

  • Use arrow functions when you need to access this from the enclosing scope.
  • Avoid using arrow functions as methods in objects intended to use this.

Example:

const obj = {
  count: 0,
  increment: function () {
    const innerFunc = () => {
      this.count++
    }
    innerFunc()
  },
}

obj.increment()
console.log(obj.count) // Outputs: 1

Avoid Arrow Functions as Object Methods

  • Arrow functions cannot have their this overridden.
  • If you need this to refer to the object, use regular functions.

Example:

const obj = {
  value: 10,
  getValue: () => {
    console.log(this.value)
  },
}

obj.getValue() // Classic script or CommonJS: undefined; ES module: TypeError

Solution:

const obj = {
  value: 10,
  getValue: function () {
    console.log(this.value)
  },
}

obj.getValue() // Outputs: 10

Use .bind() to Keep a Callback Receiver

  • Use .bind() when ordinary calls must use the same receiver. Construction with new is the exception, as shown in the binding rules.

Example:

function logValue() {
  console.log(this.value)
}

const obj = { value: 5 }
const boundLogValue = logValue.bind(obj)

boundLogValue() // Outputs: 5

Be Careful with Callbacks and Event Handlers

  • When passing methods as callbacks, the this context may be lost.

Example:

const obj = {
  value: 'Hello',
  getValue: function () {
    console.log(this.value)
  },
}

setTimeout(obj.getValue, 1000) // Outputs: undefined, not Hello

The timer does not invoke obj.getValue() as a method. Browsers supply window as the timer callback receiver; Node.js supplies a Timeout object. Neither has a value property in this example. This is host-API behavior, not a claim that all callbacks use default binding.

Solution: Use Arrow Function or .bind()

setTimeout(() => obj.getValue(), 1000)
// or
setTimeout(obj.getValue.bind(obj), 1000)

Exercises

Exercise 1: Identifying this

Question:

What will be the output of the following code?

var name = 'Global'

const person = {
  name: 'Alice',
  getName: function () {
    return this.name
  },
}

const getName = person.getName

console.log(person.getName()) // Output?
console.log(getName()) // Output?

Answer:

ContextOutput, in order
Non-strict browser scriptAlice, Global
Strict browser script or strict CommonJSAlice, then TypeError
Non-strict CommonJSAlice, undefined (no global name)
ES moduleAlice, then TypeError

The method call uses person. The detached call uses default binding. In CommonJS, var name stays in the module, not on globalThis. In strict functions, reading this.name throws; it does not return undefined.

Exercise 2: Binding with .bind()

Question:

Modify the following code so that getName() always returns the correct name property.

const person = {
  name: 'Bob',
  getName: function () {
    return this.name
  },
}

const getName = person.getName
console.log(getName()) // What happens in each file context?

Answer:

Use .bind() to keep person as the receiver on ordinary calls:

const getName = person.getName.bind(person)
console.log(getName()) // Outputs: 'Bob'
ContextOriginal detached callBound call
Non-strict browser scriptEmpty stringBob
Non-strict CommonJSundefinedBob
Strict script, strict CommonJS or ES moduleTypeErrorBob

Exercise 3: Arrow Functions and this

Question:

Predict the result in each file context. Why does the arrow not use user.name, and how would you fix it?

const user = {
  name: 'Carol',
  greet: () => {
    console.log(`Hello, ${this.name}`)
  },
}

user.greet() // Output or error?

Answer:

ContextResult
Browser classic script, strict or non-strictHello, (empty window.name)
CommonJS, strict or non-strictHello, undefined (no name on module.exports)
ES moduleTypeError before logging

The object literal does not create a this binding. The arrow captures top-level this, which differs across these file contexts. Adding 'use strict' to a classic browser script does not make its top-level this undefined.

Fix: Use a regular function:

const user = {
  name: 'Carol',
  greet: function () {
    console.log(`Hello, ${this.name}`)
  },
}

user.greet() // Outputs: Hello, Carol

Exercise 4: Understanding new Binding

Question:

What will be the output of the following code?

function Person(name) {
  this.name = name
  this.getName = function () {
    return this.name
  }
}

const person1 = Person('Dave')
console.log(name) // Output?

const person2 = new Person('Eve')
console.log(person2.getName()) // Output?

Answer:

ContextResult
Non-strict browser script or CommonJSDave, Eve
Strict browser script, strict CommonJS or ES modulePerson('Dave') throws; neither log executes

In a non-strict function, the first call writes name onto the global object and returns undefined (person1 is not an instance). In a strict function, assigning through this === undefined throws. If run independently, new Person('Eve') constructs an instance in either mode and its method returns Eve. The non-strict version intentionally demonstrates global pollution; run it in isolation.

Exercise 5: Combining Binding Rules

Question:

Predict the output of the following code:

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

const obj1 = { name: 'Frank' }
const obj2 = { name: 'Grace' }

sayHello() // Output?
sayHello.call(obj1) // Output?
sayHello.apply(obj2) // Output?
const boundSayHello = sayHello.bind({ name: 'Heidi' })
boundSayHello() // Output?
new sayHello() // Output?

Answer:

ContextFirst callDoes execution continue?
Non-strict browser scriptHello, (empty window.name)Yes
Non-strict CommonJSHello, undefined (no global name)Yes
Strict script, strict CommonJS or ES moduleTypeErrorNo; nothing is logged

In the non-strict runs, the remaining output is Hello, Frank, Hello, Grace, Hello, Heidi, then Hello, undefined. In the strict runs the first call stops execution; these are not additional lines of output.

If each later invocation is evaluated independently, .call(obj1) uses Frank, .apply(obj2) uses Grace, the bound call uses Heidi, and new sayHello() uses a fresh object with no name. These receiver choices work in both strict and non-strict functions.

Follow-up: boundSayHello.call(obj1) still prints Hello, Heidi. new boundSayHello() ignores the bound receiver and prints Hello, undefined. Use the binding rules to explain why.

Understanding how this works in JavaScript, including its binding rules and behavior in different contexts, is crucial for writing reliable and maintainable code. By mastering the intricacies of this, you'll be better equipped to handle object-oriented programming patterns, avoid common pitfalls, and confidently tackle this-related questions in technical interviews.

Practice Problems

TheoryEasy

The `new` Keyword and Constructor `this`

TheoryMedium

`this` Binding Rules

TheoryMedium

`.call` vs `.apply` vs `.bind`

TheoryMedium

Why Arrow Functions Have No Own `this`

TheoryHard

`this` Execution Output Prediction

TheoryHard

Polyfill `bind`, `call`, and `apply`

Lesson completed?

Found a bug, typo, or have feedback?

Let me know

Continue in this section