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.
Where this lesson lives in the cluster: this lesson canonically owns the rules for what this refers to — default, implicit, explicit (.call / .apply / .bind), new, and lexical (arrow) binding. For the broader question of which function form to use — declaration vs expression, arrow vs regular, IIFE, parameter syntax — see Function Types. For hoisting differences between function declarations and expressions, see Scope and Hoisting.
Understanding the this Keyword
What is this?
- Definition:
thisis a value available in an execution context. It is often an object, but in a strict function it can also beundefined,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 lexicalthisinstead.
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 context | Top-level this | this in a plain call to a function defined there |
|---|---|---|
| Browser classic script, non-strict | window | window |
Browser classic script with 'use strict' | window | undefined |
| Node.js CommonJS file, non-strict | module.exports | globalThis |
Node.js CommonJS file with 'use strict' | module.exports | undefined |
| ES module, browser or Node.js | undefined | undefined |
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:
- Default Binding
- Implicit Binding
- Explicit Binding
- New Binding
- Arrow Functions
Default Binding
- Context: When a function is called without any context (standalone function invocation).
- Non-Strict Mode:
thisrefers to the global object (windowin browsers,globalin Node.js). - Strict Mode:
thisisundefined.
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:
thisrefers 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:
thisrefers topersonbecausegreetis called asperson.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 substitutesglobalThisfornull/undefinedand boxes other primitives. Arrows and already-bound functions do not accept a replacementthis.
Using .call() and .apply():
.call(thisArg, arg1, arg2, ...): Invokes the function withthisset tothisArgand arguments passed individually..apply(thisArg, [argsArray]): Invokes the function withthisset tothisArgand 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 withnewignores 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
newkeyword. - Binding Rule:
thisrefers 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
thisrefers to that object. - The properties and methods are added to the new object.
Arrow Functions and this
- Definition: Arrow functions (
()=>{}) do not have their ownthisbinding. - Binding Rule:
thisin an arrow function refers tothisin 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 lexicalthis.
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
innerFuncinheritsthisfromgreet, which isperson.
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 emptywindow.name; in non-strict CommonJS, with no globalname, it printsHello, my name is undefined..- If
innerFuncis strict (including in an ES module),thisisundefinedand readingthis.namethrows aTypeErrorbefore 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.
- Arrow target, including an arrow behind
.bind()? Use its enclosing lexicalthis.newon an arrow (even a bound one) throws; method syntax and explicit binding cannot change itsthis. The bound-receiver rule below applies to non-arrow targets. - 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 internalthis; derived-classthisis initialized bysuper(). - Already bound? An ordinary call uses the bound receiver, even through another object or a later
.call(). .call()or.apply()? Use the supplied receiver, subject to the called function's strict/sloppy conversion rules.- Method invocation such as
person.greet()orperson['greet']()? Use the base object (person). - Plain call?
thisisundefinedin a strict function, orglobalThisin 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;thisrefers toperson1.showName.call(person2)uses explicit binding;thisrefers toperson2.new showName()uses new binding;thisrefers to the new object created bynew.
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:
regularFuncuses implicit binding;thisrefers toobj.arrowFuncinherits top-levelthis, notobj. In a classic browser script (even a strict one), it readswindow.value; in CommonJS it readsmodule.exports.value. Both are absent in this standalone example. In an ES module, top-levelthisisundefined, soobj.arrowFunc()throws aTypeError.
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:
greetis assigned toobj2.- When
obj2.greet()is called,thisrefers toobj2.
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:
getNameFunctionis assignedobj.getName, but when called, it uses default binding.thisisundefinedin strict mode or the global object in non-strict mode.- Thus a strict function throws on
this.name; a non-strict CommonJS function returnsundefinedwhenglobalThis.nameis absent. A browser's existingwindow.namecan 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
thisfrom 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
thisoverridden. - If you need
thisto 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 withnewis 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
thiscontext 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:
| Context | Output, in order |
|---|---|
| Non-strict browser script | Alice, Global |
| Strict browser script or strict CommonJS | Alice, then TypeError |
| Non-strict CommonJS | Alice, undefined (no global name) |
| ES module | Alice, 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'
| Context | Original detached call | Bound call |
|---|---|---|
| Non-strict browser script | Empty string | Bob |
| Non-strict CommonJS | undefined | Bob |
| Strict script, strict CommonJS or ES module | TypeError | Bob |
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:
| Context | Result |
|---|---|
| Browser classic script, strict or non-strict | Hello, (empty window.name) |
| CommonJS, strict or non-strict | Hello, undefined (no name on module.exports) |
| ES module | TypeError 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:
| Context | Result |
|---|---|
| Non-strict browser script or CommonJS | Dave, Eve |
| Strict browser script, strict CommonJS or ES module | Person('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:
| Context | First call | Does execution continue? |
|---|---|---|
| Non-strict browser script | Hello, (empty window.name) | Yes |
| Non-strict CommonJS | Hello, undefined (no global name) | Yes |
| Strict script, strict CommonJS or ES module | TypeError | No; 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.