Master JavaScript “this” keyword to pass job interview

skorphil

Philipp Rich

Posted on June 25, 2024

Master JavaScript “this” keyword to pass job interview

Glossary

Lexical scope aka lexical context or scope.

Determines which variables and functions can be accessed by a part of a code. For example, const variable, defined inside a code block can’t be accessed outside that block.

{  
  const blockScopedVar = 1  
}  
console.log(blockScopedVar) // Error. blockScopedVar is not defined
Enter fullscreen mode Exit fullscreen mode

In this example, lexical scope of console.log() does not contain blockScopedVar.

Under the hood, lexical scope is defined by lexical Environment chain.

Execution context
In simplified terms, it is an environment, in which the current function is running. JavaScript code is executed by dividing the code into ‘pieces’ and running them sequentially. These ‘pieces’ are execution contexts. Execution context created for global context and per each function call. When a function being invoked, execution context is created. Execution context contains a value of this, lexical scope, and other internal stuff required to run this function.

"this" keyword
this keyword is usually used in methods or classes, making it easy to point the function to an object’s properties we want to use. this in JS is just a reference to an object. This reference can be set:

  • implicitly (when a new execution context is being created)
  • explicitly (with call(), apply(), bind() methods).

The implicit value of this is determined by where in the code this is being used (in what context). This is causing most confusion and the way to sort this out is to know what this contexts exist and how they affect the value of this.

Implicit contexts

At the creation phase of execution context, the value of this is assigned automatically, based on where this in code is being used. Sometimes this is called this context (not to be confused with execution context or lexical context)

function executionContext1 () {  
  console.log(this.a) // `this` is used in globally invoked function context  
}  
const obj1 = {  
  a: 1,  
  executionContext2() {console.log(this.a)} // `this` is used in method context  
};  

executionContext1() // execution context with `this` pointing to global object being created to execute this function  
executionContext2() // execution context with `this` pointing to `obj1` being created to execute this function
Enter fullscreen mode Exit fullscreen mode

this contexts, determining its value:

  • Global context (when this placed outside of functions or classes): this -> global object
  • Function context (when this placed inside the function)
    • Globally invoked function: this -> global object
    • Method: this -> object, on which method being called
    • Constructor function: this -> instance object
    • Arrow function: Exception. Do not have this. this -> inherited from outer function scope
  • Event handler context
  • Class context

Global context

If this is used outside of functions or classes, it will be pointing to the global environment (window in a browser or global in NodeJS)

console.log(this) // [object Window]
Enter fullscreen mode Exit fullscreen mode

Function context

this is determined by how the function is called, not how it is declared.

Except for an arrow function, when a value of this is determined by where an arrow function is declared.

Globally invoked function

If a function is called In a global context, this refers to the global object.

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

printThis() // Window // or undefined in strict mode
Enter fullscreen mode Exit fullscreen mode

Notice that the value of this in a globally invoked function can be altered by the "use strict" directive.

Method

If a function is called as a method of an object, this will refer to that object (on the left side of the dot before a method). For a.b.c.func() this will be c

const obj1 = {  
  a: 1,  
  log() {console.log(this.a)}  
};  
const obj2 = {  
  a: 2,  
  log: obj1.log  
};  

obj2.log(); // 2, because method is being called on `obj2`
Enter fullscreen mode Exit fullscreen mode

Constructor Function

If this is used inside a constructor function, it points to the created instance of this constructor.

function Person(name, birthYear) {  
  this.name = name // `this` will reference the instanse, so `name` will be added to `beatrix`  
  this.birthYear = birthYear // `this` will reference the instanse, so `birthYear` will be added to `beatrix`  

  this.describe = function() { // `describe()` will be added to `beatrix`  
    console.log(`${this.name} was born in ${this.birthYear}.`)   
    // `this` will be pointing to the object on which `describe` will be called (method context)  
  }  
}  
const beatrix = new Person('Beatrix', 2005)  

beatrix.describe() // "Beatrix was born in 2005."  
console.log(beatrix.name) // "Beatrix"
Enter fullscreen mode Exit fullscreen mode

Arrow Function

Arrow functions are a special case, which makes this more tricky. They do not have their own this and inherit this value from the closest enclosing function scope. this value depends on how a function is declared, not how it is called (like with “normal” functions). In other words, this in arrow functions will point to the ‘owner’ of a function, despite where this function is being invoked. It means, that calling arrow functions as methods might result in an unexpected behavior:

const obj = {  
  a: 1,  
  arrow: () => console.log(this.a), // there is no enclosing function, so this will point to global object  
  arrowNested:  
    function() {  
      return () => console.log(this.a) // will inherit the value of this from arrowNested() function, so it will point to `obj`  
  }  
}  

obj.arrow() // undefined  
obj.arrowNested()() // 1  
// value of `this` in arrow function is determined by where function being declared
Enter fullscreen mode Exit fullscreen mode

In some cases, such a behavior of arrow functions might be handy, i.e. when used in event handlers.

Event Handler

In the browser, there is a special this context for event handlers. In an event handler called by addEventListener, this will refer to the element, on which a listener is placed. In other words, this is pointing to event.currentTarget

const button = document.createElement('button')  
button.textContent = 'Click me'  
document.body.append(button)  

button.addEventListener('click', function(event) {  
  console.log(this) // <button>Click me</button>  
})
Enter fullscreen mode Exit fullscreen mode

Sometimes it is not convenient and arrow functions being used to ‘preserve’ this value, no matter what ‘caller’ is:

const person = {  
  name: "Beatrix",  
  sayHello: function() {  
    document.getElementById("myButton").addEventListener("click", () => {  
      console.log("Hello, " + this.name); // `this` inherited from `sayHello` and always will point to `person`, no matter where it will be invoked (arrow function context)  
    });  
  }  
};  

person.sayHello(); // "Hello, Beatrix"
Enter fullscreen mode Exit fullscreen mode

Class

When this is used inside a class, its value depends on what type of methods and field initializer it’s being used in:

  • In public and private methods and field initializers this behaves like in constructor functions: pointing to an instance
  • Static methods cannot be accessed via instance’s this. It must be called on class itself
class C {  
  instanceField = this;  
  static staticField = this;  
}  

const instance = new C();  

console.log(instance.instanceField === instance); // true  
console.log(instance.staticField); // undefined because static methods cannot be accessed via instance's `this`   
console.log(C.staticField === C); // true
Enter fullscreen mode Exit fullscreen mode

Explicit Context of this

The value of this can be set manually by using special function methods. Note, that Arrow functions do not have their own this, so these methods can’t be used with them.

.call() and .apply() are used to call the function with a specified value of this:

const obj1 = {  
    a: 1,  
    log() {console.log(this.a)}  
};  
const obj2 = {  
    a: 2,  
};  

obj1.log.apply(obj2); // 2, because `this` explicitely set to `obj2`  
obj1.log() // 1
Enter fullscreen mode Exit fullscreen mode

.bind() is used to create a constant binding of this to a specified object:

const obj1 = {  
    a: 1,  
    log() {console.log(this.a)}  
};  
const obj2 = {  
    a: 2,  
};  
const getObj2Var = obj1.log.bind(obj2);  

getObj2Var() // 2, because getObj2Var `this` binded to obj2
Enter fullscreen mode Exit fullscreen mode

Self-check

const obj = {  
  a: 1,  
  b: function() {  
    console.log(this.a)  
  },  
  c: () => {  
    console.log(this.a)  
  },  
  d: function() {  
    return () => {  
      console.log(this.a);  
    }  
  }  
}  

// what the output will be?  

console.log(obj.a)  

obj.b()  
const b = obj.b  
b()  
obj.b.apply({a: 2})  

obj.c()  
obj.c.apply({a:2})  

obj.d()()  
obj.d.call({a:2})()  
obj.d().call({a:2})
Enter fullscreen mode Exit fullscreen mode

Answer

console.log(obj.a)  // 1  

obj.b()             // 1           // method context  
const b = obj.b  
b()                 // undefined   // This is globally invoked function, not a method, so `this` is pointing to global object  
obj.b.apply({a: 2}) // 2  

obj.c()             // undefined   // Arrow function do not have outer function, so `this` reference global object  
obj.c.apply({a:2})  // undefined   // Arrow functions can't use .call() .apply() or .bind()  

obj.d()()           // 1           // first obj.d() being executed (method context)   
                                   // and returned arrow function. Then arrow function being executed in global context,   
                                   // but `this` in arrow function always points to the `this` of the encapsulating function.   
                                   // In this case `this` of `d()` pointing to `obj`, so `this` in arrow function   
                                   // will always reference `obj`, no matter where it called  

obj.d.call({a:2})() // 2           // same as above, but here `this` of `d()` was explicitely set   
obj.d().call({a:2}) // 1           // `call()` method was applied to arrow function, which is not making any sense.
Enter fullscreen mode Exit fullscreen mode

References

  1. this — JavaScript | MDN
  2. Object methods, this

Happy coding! Feedback is appreciated.

💖 💪 🙅 🚩
skorphil
Philipp Rich

Posted on June 25, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related