JavaScript this Keyword
The this
keyword in JavaScript can really be confusing if you try to learn it without having a fundamental understanding of execution context. But if you do understand what an execution context is then it should be straight forward to understand how the this
keyword works. The first thing we need to know about this
is it is determined at call time.
Before getting started with learning the this
keyword, it's important to understand what execution context is and understanding scope also helps to clear up any confusion. Make sure to read these two articles first so the this
keyword makes sense.
this
can have many different values depending on how a function is called. The value of this
has everything to do with how a function is invoked, not where a function is declared.
When is the Value of this
Set?
Once a function object has been instantiated and the evalaution of code begins, when a function call expression is found foo()
the function object's internal [[Call]]
method will be called.
[[Call]]
On step 2, PrepareForOrdinaryCall
is where the execution context and function environment record or function scope are created. Step 7 is where the new global scope is created.
On step 5, OrdinaryCallBindThis
is where the value of this
is set.
- Let callerContext be the running execution context.
- Let calleeContext be PrepareForOrdinaryCall(F, undefined).
- Assert: calleeContext is now the running execution context.
- If F.[[IsClassConstructor]] is true, then
- Let error be a newly created TypeError object.
- NOTE: error is created in calleeContext with F's associated Realm Record.
- Remove calleeContext from the execution context stack and restore callerContext as the running execution context.
- Return ThrowCompletion(error).
- Perform OrdinaryCallBindThis(F, calleeContext, thisArgument).
- Let result be Completion(OrdinaryCallEvaluateBody(F, argumentsList)).
- Remove calleeContext from the execution context stack and restore callerContext as the running execution context.
- If result.[[Type]] is return, return result.[[Value]].
- ReturnIfAbrupt(result).
- Return undefined.
PrepareForOrdinaryCall
- Let callerContext be the running execution context.
- Let calleeContext be a new ECMAScript code execution context.
- Set the Function of calleeContext to F.
- Let calleeRealm be F.[[Realm]].
- Set the Realm of calleeContext to calleeRealm.
- Set the ScriptOrModule of calleeContext to F.[[ScriptOrModule]].
- Let localEnv be NewFunctionEnvironment(F, newTarget).
- Set the LexicalEnvironment of calleeContext to localEnv.
- Set the VariableEnvironment of calleeContext to localEnv.
- Set the PrivateEnvironment of calleeContext to F.[[PrivateEnvironment]].
- If callerContext is not already suspended, suspend callerContext.
- Push calleeContext onto the execution context stack; calleeContext is now the running execution context.
- NOTE: Any exception objects produced after this point are associated with calleeRealm.
- Return calleeContext.
OrdinaryCallBindThis
- Let thisMode be F.[[ThisMode]].
- If thisMode is lexical, return unused.
- Let calleeRealm be F.[[Realm]].
- Let localEnv be the LexicalEnvironment of calleeContext.
- If thisMode is strict, let thisValue be thisArgument.
- Else,
- If thisArgument is either undefined or null, then
- Let globalEnv be calleeRealm.[[GlobalEnv]].
- Assert: globalEnv is a Global Environment Record.
- Let thisValue be globalEnv.[[GlobalThisValue]].
- Else,
- Let thisValue be ! ToObject(thisArgument).
- NOTE: ToObject produces wrapper objects using calleeRealm.
- If thisArgument is either undefined or null, then
- Assert: localEnv is a Function Environment Record.
- Assert: The next step never returns an abrupt completion because localEnv.[[ThisBindingStatus]] is not initialized.
- Perform ! localEnv.BindThisValue(thisValue).
- Return unused.
How Can the Value of this
Be Determined?
The value of this
depends on how the function is invoked, not where it's declared. Here the greet
function is declared in the global scope and it's also invoked in the global scope, but notice how the value of this
is set to obj
using the call
method.
const obj = {
name: 'david'
}
function greet() {
console.log(`hi ${this.name}`)
}
greet.call(obj) // hi david
The value of this
can also be passed to another function.
const obj = {
name: 'david'
}
function greet() {
bar(this)
}
function bar(ctx) {
console.log(`hi ${ctx.name}`)
}
greet.call(obj) // hi david
Below we will go over the different ways that the value of this
is determined.
Default Binding
The default this
binding is created whenever a function is invoked in global scope. Note that in strict mode
you will get undefined
.
function foo() {
console.log(this) // window
}
foo() // here the scope is global
Implicit Binding and Object Methods
This can be confusing, it's often explained wrongly that an object owns a method, then adding to the confusion it's said that you should only use normal functions instead of arrow functions because arrow functions do not create their own this
.
The value of this
inside of an object is not the object itself, rather it's the same value as this
outside of the object.
// this === window
const obj = {
foo: this === window,
name: 'david',
// this === window
greet: function() {
// this === window
return `hi ${this.name}`
},
greetAgain: () => {
// this === window
return `hi ${this.name}`
}
}
console.log(obj)
/*
const obj = {
foo: true,
name: 'david',
greet: function() {
return `hi ${this.name}`
},
greetAgain: () => {
return `hi ${this.name}`
}
}
*/
The only difference is how the function is invoked as we already mentioned. To call the greet()
method we use dot notation. At the time of invocation, the value of this
is determined by what is on the left hand side of the dot, which in more technical terms is called a context object
.
Here when greet()
is called, it's preceded by a reference to a context object called obj
which will be used as the value of this
.
obj.greet() // logs 'hi david'
The above code could be rewritten as follows:
function greet() {
return `hi ${this.name}`
}
const obj = {
name: 'david',
greet
}
Now we can see there is nothing special about an object method.
Below note that if there is a chain, only the last object property reference matters to the function call.
function greet() {
console.log(`hi ${this.name}`)
}
const obj = {
name: 'john',
obj2
}
const obj2 = {
name: 'david',
greet
}
obj.obj2.greet() // `hi david`
Explicit Binding
Explicit binding happens when using call()
, apply()
, and bind()
.
This is when we state explicitly what the value of this
will be. call()
and apply()
are basically the same, just the way you pass arguments to them differs. call()
accepts a comma seperated list, whereas apply()
accepts an array.
function greet() {
return `hi ${this.name}`
}
const obj = {
name: 'john',
greet
}
const obj2 = {
name: 'david',
greet
}
obj.greet() // hi john
obj.greet.call(obj2) // hi david
Implicitly Lost
This is a common source of confusion for developers. Let's say you have the following code and you want to save an object's method so you can call it later.
const obj = {
name: 'david',
greet() {
return `hi ${this.name}`
}
}
const greetMethodForLater = obj.greet
greetMethodForLater() // 'hi '
// 'this' is lost
We stored a reference to the obj.greet
method in the greetMethodForLater
variable and when we invoked it, it was a normal undecorated function invocation in global scope. When greetMethodForLater()
is invoked, there is no context object and this
does not refer to obj
.
In this situation we need to use bind
, call
will invoke a function immediately with the specified this
value, bind
will return a new function with its this
value set for later use.
const obj = {
name: 'david',
greet() {
return `hi ${this.name}`
}
}
const greetMethodForLater = obj.greet.bind(obj)
greetMethodForLater() // 'hi david'
Callbacks
Callbacks are another way we can lose the value of this
. The below example doesn't work as we already mentioned because there is no context object, it's just a normal function invocation.
const obj = {
name: 'david',
greet() {
return `hi ${this.name}`
}
}
const greetMethodForLater = obj.greet
greetMethodForLater() // `hi `
This same thing happens with callbacks. Inside of foo
, obj.greet
is being assigned to callbackFn
.
function foo(callbackFn) {
callbackFn()
}
const obj = {
name: 'david',
greet() {
console.log(`hi ${this.name}`)
}
}
foo(obj.greet)
The reason this happens is because inside of the function foo
what is happening is that obj.greet
is passed in and assigned to callbackFn
.
function foo(callbackFn) {
// callbackFn = obj.greet
callbackFn()
}
const obj = {
name: 'david',
greet() {
console.log(`hi ${this.name}`)
}
}
foo(obj.greet)
Here is a more advanced example utilizing the same concept.
Many developers find this
confusing because they don't first learn the fundamentals, like execution context for example. So rather than trying to understand how this
actually works, you will often see code being written trying to make use of lexical scope rather than using bind
like we did above. The reason this happens is because anytime function() {}
is invoked it creates a new value of this
which normally is just the global value if a context object is not used or if call
, apply
, or bind
are not being used.
The callback below is a normal function and not an arrow function so it creates its own this
value which is the window
if you run this in a console. For that reason we need to capture the value of this
lexically with _this
or use bind
.
const obj = {
name: 'david',
tasks: ['eat', 'clean'],
listTasks() {
const _this = this
this.tasks.forEach(function(task) {
console.log(`${_this.name} must ${task}`)
})
}
}
Here is an example using bind
. This code is unnecessary because we can just use an arrow function which will inherit the this
value of the surrounding scope.
const obj = {
name: 'david',
tasks: ['eat', 'clean'],
listTasks() {
this.tasks.forEach(function(task) {
console.log(`${this.name} must ${task}`)
}.bind(this))
}
}
Arrow Functions
Arrow functions inherit their this
value from the surrounding lexical scope, they do not create their own this
value unlike regular functions. The value of this
inside the object is window
in the case of a browser. When obj.foo()
is called, the value of this
is set to the context object, obj
, and when obj.bar()
is called, because arrow functions do not create a new value of this
, the value of this
is the surrounding lexical scope which is the window
object. Regular functions do define their own this
value which is why code such as obj.foo()
works.
// this === window
const obj = {
// this === window
x: 1,
foo() {
console.log(this.i, this)
},
bar: () => console.log(this.i, this)
}
Here is another example.
const obj = {
name: 'david',
tasks: ['sleep', 'clean'],
listTasks() {
// obj is the context object at invocation time
// obj is the `this` value the arrow function inherits
this.tasks.forEach(task => {
console.log(`${this.name} must ${this}`)
})
}
}
obj.listTasks()
Remember, arrow functions do not create their own this
. A normal function() {}
has a this
value can that be changed implicitly with dot or bracket notation or explictly using call
, apply
, or bind
.
We noted that arrow functions capture this
at call time so lets take a look at another example.
function greet() {
return (name) => {
console.log(`hi ${this.name}`)
}
}
const obj = {
name: 'david'
}
const obj2 = {
name: 'john'
}
const foo = greet.call(obj)
foo() // 'hi david'
The reason this happens is because const foo = greet.call(obj)
saves the returned function with a captured this
which is set to obj
. When foo()
is invoked, it is invoking the arrow function that was returned from greet()
.
foo.call(obj2)
// 'hi david'
// not 'hi john' as we might expect
This is due to the fact that the arrow function captured the value of this
at call time. When we first called greet.call(obj)
the value of this
was captured as obj
and the arrow function was returned. Any attempt to change the this
value of the arrow function will fail.
Get Information About a Function While it's Executing
Variables only exist while a function is executing then they cease to exist, except in the case of closures. During function execution, you can use a debugger
statement in your code to pause execution at that point and see what the state of your program looks like.
Do call
, apply
and bind
have a higher priority than using a context object?
If there are multiple rules such as using an implicit binding and an explicit binding one must have a higher priority over the other. Below when invoking obj.greet()
'hi john
is logged to the console as we would expect, and invoking obj.greet.call(obj2)
logs 'hi david'
. This means when we inspect a call site using the execution context stack in our dev tools, if using call
, apply
or bind
, this has a higher priority than just using a context object, i.e. obj.greet()
.
const obj = {
name: 'john',
greet() {
return `hi ${this.name}`
}
}
const obj2 = {
name: 'david',
greet() {
return `hi ${this.name}`
}
}
obj.greet() // 'hi john'
obj.greet.call(obj2) // 'hi david'
new
Operator
What happens if we use the new operator with an object?
function setAge(age) {
this.age = age
}
const obj = {
name: 'david'
}
// bar is hard bound to obj
const foo = setAge.bind(obj)
foo(30)
console.log(obj.age) // 30
// here foo is called with the new keyword and baz.age === 40 not 30 as we might expect
const bar = new foo(40)
console.log(bar.age) // 40
The new
keyword has the highest precedence when determining the value of this
. When you examine the call site, ask yourself, is the function using the new binding? if not, is the function using explicit binding? if not, is the function called with implicit binding? If the answer is still no, then we know that the default binding applies.
Review
this
is determined at function invocation time.- Regular functions create a new value of
this
when they are invoked, this iswindow
in the case of the browser. - Arrow functions do not create a new
this
, they inherit thethis
of the surrounding lexical scope. - The
this
value is created for a regularfunction() {}
in three ways:- Implicitly -
contextObj.foo()
- Explictly -
foo.call(someObj)
- With the
new
operator -new someConstructorFunction()
- Implicitly -
- The order of precendence in determing the the value of
this
occurs in this order from highest to lowest.new
operatorcall
,apply
, andbind
- Using a context obj, i.e.
contextObj.foo()
- Default binding, ie.
foo()