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
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
, 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.