The keyword this
works differently for javascript language. It can change its value depending on how it is called. After facing many this
issues , I wrote this article explaining how this
works with just 4 rules to simplify the understanding and avoid the typical this
issues. Also, I explain how it works with callback functions, different platforms like nodejs and browser, and how to prevent bugs on testing with tools like jest
.
Table of Contents
- Introduction
- The problem
- The 4 rules
- Rule 1
- Arrow functions
- Nested functions
- Issues with testing
- Explaining the first example
Introduction
According to Mozilla, the keyword this
works like below:
In most cases, the value of this is determined by how a function is called (runtime binding). It can’t be set by assignment during execution, and it may be different each time the function is called.
The problem below shows the value of this
is different in two cases
The problem
See the two examples below:
1)
const a = [1,2,3].map(function(n){
return n*this;
}, 2);
[2,4,6]
2)
const b = [1,2,3].map( n => {
return n*this;
}, 2);
[NaN,NaN,NaN]
This first example returns [2,4,6], while the second one, gets [NaN, NaN, NaN]. At the end of this article you will understand why they have different results.
The 4 rules
There are 4 rules that helps to understand how the keyword this
works:
- Inside a function,
this
refers to a global context(window for browser, global for nodejs); - Inside a method, refers to an object;
- The value of
this
can be modified bycall
andapply
functions and also when converts a method to a function and vice-versa; - The value of
this
will never change if is in anarrow function
or abind
function, therefore invalidating the rules 1, 2 and 3;
Rule 1
Within a function, this
refers to a global object. In a browser, the global object is windows
, while in NodeJs is global
.
function print(){
console.log(this == window);
}
print() //true
Even nested functions refers to a global context
function print(){
console.log(this == window);
function print2(){
console.log(this == window);
}
print2(); //true
}
print() //true
Rule 2
Within a method, this
refers to an object. The code below this
refers to object account
. This applies to object literal, class constructor and function contructor:
Object literal
const account = {
balance: 100,
showBalance: function(value){
console.log(this.balance)
}
}
account.showBalance(); //100
Class constructor
class Account{
constructor(){
this.balance = 100;
}
showBalance(value){
console.log(this.balance)
}
}
const account = new Account();
account.showBalance(); //100
Function constructor
function Account(){
this.balance = 100;
this.showBalance = function(value){
console.log(this.balance)
}
}
const account = new Account();
account.showBalance(); //100
Rule 3
The value of this
can be modified by using call
, apply
and converting a method to a function and vice-versa.
Converting a method to a function
The code below has two variables:
- the global variable
balance
. Line 1 - the variable
balance
at object account. Line 2
On line 9 is doing aliasing
which means, converting the method
showBalance to function
showBalance. The line 10 is calling a function(Rule 1), then the value of this
will be global. On line 11, is a method(Rule 2), then it refers to object account.
var balance = 500;
const account = {
balance: 100,
showBalance: function(value){
console.log(this.balance);
}
}
const showBalance = account.showBalance; //aliasing
showBalance(); //500
account.showBalance(); //100
Converting a function to a method
The opposite way, by assigning a function to a method, has also the same effect:
var balance = 500;
function showBalance(value){
console.log(this.balance);
}
const account = {
balance: 100
}
account.showBalance = showBalance;
showBalance(); //500
account.showBalance(); //100
call and apply
Another way to change the value of this is by using call
or apply
. The first parameter of call/apply is the value that sets this
.
function showBalance(value){
console.log(this.balance);
}
const account1 = {
balance: 100
}
const account2 = {
balance: 50
}
showBalance.call(account1); //100
showBalance.apply(account2); //50
Rule 4
The value of this
will never change if is in an arrow function
or a bind
function, therefore invalidating the rules 1, 2 and 3;
Arrow function
As the method account.showBalance
is a arrow function
, then the value of this
will always refer to account
object, the showBalance
will always return the same value regardless how it has been called.
function Account() {
this.balance = 100;
this.showBalance = () => {
console.log(this.balance);
};
}
const account = new Account();
const showBalance = account.showBalance;
account.showBalance(); //100
showBalance(); //100
showBalance.call(); //100
showBalance.apply(); //100
bind
The same behavior for bind
.
function Account() {
this.balance = 100;
}
const account = new Account();
function showBalance(){
console.log(this.balance);
}
accountShowBalance = showBalance.bind(account);
accountShowBalance(); //100
accountShowBalance.call(); //100
accountShowBalance.apply(); //100
showBalance(); //undefined
The accountShowBalance
is a bind
function, then the this
refers to account
object. The showBalance
is just a normal function, then the this
value refers to global context, that’s why it displayed undefined
.
Callback function
Depending how a callback function is called and created the this
will have different values.
called as a function - Rule 1
See the code below:
var price = 200;
const product = {
price: 100,
showPrice: function(callback){
callback(); // Rule 1
}
}
product.showPrice(function(){
console.log(this.price); //200
});
The result will be 200. Why? As the callback has been created as function
at line 9 and it was called as function
at line 5, the this
will refer to window
that contains the price 200.
called as a method - Rule 2
var price = 200;
const product = {
price: 100,
showPrice: function(callback){
this.callback = callback; // converting to method - Rule 3
this.callback(); // method - Rule 2
}
}
product.showPrice(function(){
console.log(this.price);
});
At the line 5, the callback function is assigned into a method, it is available now by product.callback. At the line 6, as it is been called by this
which refers product
object, so the line 11 returns 100.
callback called by apply/call - Rule 3
The context of this
of callback can be modified by apply
or call
.
const product2 = {
price: 300
}
const product = {
price: 100,
showPrice: function(callback){
callback.apply(product2); //Rule 3
}
}
product.showPrice(function(){
console.log(this.price); //300
});
At the line 7, the contenxt of this
is changed to product2
, then line 12 will return 300.
callback created by arrow function or bind - Rule 4
If the callback is created by arrow function or bind, the value of this
never changes.
var value = 'value from global';
function App(){
this.value = 'value from app';
this.addCallback = function(callback){
this.callback = callback;
}
this.triggerCallback = function(){
const callback = this.callback;
this.callback();
callback();
}
}
function System(){
this.value = 'value from system';
function callback(){
console.log(this.value);
}
this.run = function(){
var app = new App();
app.addCallback(()=>{ //Rule 4
console.log(this.value);
});
app.triggerCallback();
//value from system
//value from system
app.addCallback(callback.bind(this)); //Rule 4
app.triggerCallback();
//value from system
//value from system
app.addCallback(callback); //Rule 3
app.triggerCallback();
//value from app
//value from global
}
}
var system = new System();
system.run();
Arrow functions
The arrow functions will access the enclosing lexical context’s this
, in another words, it will access the this
from closest constructor function or class that surrounds it.
function Person(){ //contructor
this.name = 'Foo';
const getName = () => {
return this.name;
}
this.show = function(){
console.log(getName());
}
}
const p = new Person();
p.show();
class PersonClass{ //contructor
constructor(){
this.name = 'Foo';
}
show(){
const getName = () => {
return this.name;
}
console.log(getName());
}
}
const pc = new PersonClass();
pc.show();
output
foo
foo
The line 4 declares an arrow function. As it is in the constructor function Person
, then this
value will be the instance of Person
;
If a nested arrow function is created, the this’ value will still be the same.
function Person(){
this.name = 'Foo';
const getName = () => {
const nestedGetName = () => {
return this.name;
}
return nestedGetName();
}
this.show = function(){
console.log(getName());
}
}
const p = new Person();
p.show();
output
foo
The arrow function set this
value from the closest constructor function or class.
function Person(){
this.name = 'Foo';
const getParentName = () => {
return this.name;
}
function Child(){
this.name = 'bar';
this.surname = ' surname';
const getSurname = ()=>{
return this.surname;
}
this.getName = () =>{
return this.name+getSurname();
}
}
var child = new Child();
this.showChild = function(){
console.log(child.getName());
}
this.show = function(){
console.log(getParentName());
}
}
const p = new Person();
p.show();
p.showChild();
Output
Foo
bar surname
The line 4, the variable getParentName
is inside of Person
function. When it creates an arrow function, the lexical context is Person
, so the this
value belong to Person
instance.
The line 10 and 14 create an arrow function, but the lexical context now is Child
which is a constructor function as it will be instantiated on line 19.
Arrow function won’t access object literal’s context like a method.
function Person(){
this.name = 'Parent';
this.child = {
name: 'child 1',
show: () =>{
console.log(this.name);
}
}
this.child2 = {
name: 'child 2',
show: function(){
console.log(this.name);
}
}
}
var p = new Person();
p.child.show();
p.child2.show();
Output
Parent
child 2
Nested functions
On object literal, contructor or classes, the value of this
refers to the context of the object.
const person = { //object literal
name: 'foo',
showName: function(){
console.log(this.name); // this == person
},
child: {
name: 'bar',
showName: function(){
console.log(this.name); // this == child
}
}
}
function PersonConstructor(){ //constructor
this.name = 'foo';
this.showName = function(){
console.log(this.name); // this == PersonContructor instance
}
}
class PersonClass{ //class
constructor(){
this.name = 'foo'; // this == PersonClass instance
}
showName(){
console.log(this.name); // this == PersonClass instance
}
}
const p = new PersonConstructor();
const p2 = new PersonClass();
person.showName();
person.child.showName();
p.showName();
p2.showName();
Output
foo
bar
foo
foo
On line 4, the function showName is assigned inside of person
object, then its this
will access the object. On line 6, the child
is another object literal, then the line 9 this
will accesss child
context.
After instantiated with keyword new
on lines 29 and 30, the lines 17 and 26 will access the context of the object instantiate from PersonContructor and PersonClass.
Value of this by Platforms
The value of this
is different by plataform (browser or nodejs), strict mode or if is a ES6 module.
Browser
In browser the value of this
in function(Rule 1) refers to window
. Look the code below:
var a = 'world';
window.b = 'hello';
function hello(){
console.log(this.a)
console.log(this.b)
}
hello();
The lines 1 and 2 create global variables a
and b
. The individual function hello
can access the global object by this
. The lines 5 and 6 prints the hello
and world
.
hello
world
Browser with strict mode
If the code has been changed to strict mode, the this
will no longer be window
value. To change to strict mode, just insert the use strict
at beggining of the code. The code below will fail on both lines 6 and 7 as the this
will be undefined
.
'use strict'
var a = 'world';
window.b = 'hello';
function hello(){
console.log(this.a) // fail
console.log(this.b) // fail
}
hello();
Browser with module
ES6 Introduced the “module”. It doesn’t even have global/window context. So, the code below won’t work either.
Create a module with the code below:
Create the file hello.js
with content below:
var a = 'world';
function hello(){
console.log(this.a)// it will fail
}
hello();
Create a file index.html then add within the tag <head>
the line below:
<script type="module" src="hello.js"></script>
Open the file in browser. The code will fail on line 4, this
can’t access window
.
I would recommend to use “modules”, because it can be transpiled with webpack or similar, and work in both environments browser and nodejs. If you want to learn to write a testable javascript code with jquery to run in both environment, read my article.
Nodejs
In nodejs works a little bit different. The this
is a global
value.
var a = 'hello';
global.b = 'world';
function hello(){
console.log(this.a) // fail
console.log(this.b) // works
}
hello();
The variable a
can’t be access from global.a
. The this
access only global
object.
Issues with testing
Sometimes a javascript code can work on browser but its unit test fails. This happens quite often in legacy code that contains global variables spread among several files and consumed by this
in somewhere. If this legacy code has been tested in tools like jest
and karma
they might have different results.
The example below is a sample legacy app that contains two javascript files that are loaded in browser manually by “script” tags. The first one “globalState.js” creates global variables, the second one “legacy.js” will access those global variables through “this”. You can get the whole code here.
globalState.js
var globalValue = 10;
window.globalValue2 = 5;
legacy.js
function getGlobalValue() {
return this.globalValue;//jest ❌ karma ✔️
}
function getGlobalValue2() {
return this.globalValue2;//jest ✔️ karma ✔️
}
window.getGlobalValue = getGlobalValue;
window.getGlobalValue2 = getGlobalValue2;
Below are test files to verify if the functions “getGlobalValue” and getGlobalValue2” returns right values. The first one is written in jest
while the second in karma
.
jest test
test("result is 10", () => {
expect(getGlobalValue()).toBe(10);
});
test("result is 5", () => {
expect(getGlobalValue2()).toBe(5);
});
karma test
describe("getGlobalValue test", function() {
it("result is 10", function() {
expect(getGlobalValue()).toBe(10);
});
it("result is 5", function() {
expect(getGlobalValue2()).toBe(5);
});
});
Both tests in karma
will all pass, while the jest
will fail on the first test result is 10
. Why? The issue is in the line 1 of globalState.js
.
var globalValue = 10;
The variable globalValue = 10
will available on window
if runs on browser, but won’t be available on global
if it runs on nodejs
. This is deeply explained on “Value of this by plataforms” item.
jest
is testing framework that runs the javascript in nodejs plataform, while the Karma
runs the code in a browser, so that’s why it works only on Karma
as it runs on browser environment. If the line 1 is replace by window
like below, the code will work on both jest
and karma
globalState.js modified
window.globalValue = 10; //fixed
window.globalValue2 = 5;
jest
will automatically copy all window
values into nodejs global
object. Then the jest
test will pass.
Explaining the first example
Before to explain why the Problem at the beggining of the article, let’s see first the next example with callback. The code below has some similarities with the first problem:
- it will iterate all elements;
- has a function as parameter(callback);
- has
thisArg
parameter; - It modifies the
this
; - It will return
NaN
if use arrow function;
function modifyArray(array, callback ,thisArg){
var newArray = [];
for(var i=0;i<array.length;i++){
newArray[i] = callback.call(thisArg, array[i]);
}
return newArray;
}
Let’s multiply all the elements and return a new array:
const array = [1,2,3];
const newArray = modifyArray(array,function(element){
return element*this;
}, 2);
console.log(newArray);
The result will be [2,4,6]
.
Let’s use the same but with arrow function:
const array = [1,2,3];
const newArray = modifyArray(array, element => {
return element*this;
}, 2);
console.log(newArray);
The result will be [NaN, NaN, NaN]
.
- The first example uses callback as a function, then when it calls
callback.call(thisArg, array[i])
, it will change the value ofthis
, according to rule 3. -
The second example uses an
arrow function
. Thecall
method will not work because it will not change the value ofthis
, but it will keep its original value. In this case,this
refers to the global context. So, when it callselement*this
it tries to multiply a number with theglobal
value. This line could be translated to this:element*window
By doing so, it returns a Not A Number(NaN).
Now, by understanding the previous example, the same idea may apply to the function map
.
const a = [1,2,3].map(function(n){
return n*this;
}, 2);
const b = [1,2,3].map( n => {
return n*this;
}, 2);
The map
function has two parameters function and thisArg.
map(function callback( currentValue[, index[, array]]) {
// return element for new_array
}[, thisArg])
The thisArg
will be the value of this
inside a function callback. In arrow function
the value of this
won’t be thisArg
. Inside the map
function it does something similar to our function modifyArray
. It might use the call
or apply
method to change the value of this
.
Summary
4 Rules
- Inside a function,
this
refers to a global context(window for browser, global for nodejs); - Inside a method, refers to an object;
- The value of
this
can be modified bycall
andapply
functions and also when converts a method to a function and vice-versa; - The value of
this
will never change if is in anarrow function
or abind
function, therefore invalidating the rules 1, 2 and 3;
Callback
- If the callback is a bind function or an
arrow function
, the value ofthis
will never change regardless of how it is called. Rule 4 - if the callback is just a function, the value of
this
depends on how will be called then could apply the rules 1, 2 or 3.
Browser
this
iswindow
- declare
var
outside any function is accessible viathis
Browser with strict mode
this
doesn’t accesswindow
Browser with module
this
doesn’t accesswindow
Nodejs
this
isglobal
- declare
var
outside any function is NOT accessible viathis
All cases in one code
var name = 'Foo global';
function getNameGlobalFunc(){
console.log(this.name);
}
const getNameGlobalVar = function(){
console.log(this.name);
}
const personObjectLiteral = {
name: 'Foo object literal',
getName: function(){
console.log(this.name);
}
}
function PersonConstructor(){
this.name = 'Foo constructor';
this.setCallback = function( callback ){
this.callback = callback;
}
this.callCallback = function(){
this.callback();
}
function getNameGlobal(){
console.log(this.name);
}
const getNameArrow = () => {
console.log(this.name);
};
const getNameBind = getNameGlobal.bind(this);
this.getName = function(){
console.log(this.name);
}
this.getNameReassigned = getNameGlobal;
const reasignAsGlobal = this.getName;
this.callThisGlobals = function(){
getNameGlobal()
reasignAsGlobal();
}
this.callThisPersonContext = function(){
getNameArrow();
getNameBind();
this.getName();
this.getNameReassigned();
}
}
const person = new PersonConstructor();
getNameGlobalFunc();
getNameGlobalVar();
person.callThisGlobals();
personObjectLiteral.getName();
person.callThisPersonContext();
console.log(getNameGlobalVar.call(personObjectLiteral))
console.log(getNameGlobalVar.apply(person))