In this article, we will explore a runtime construct for JavaScript language called this
. Compared to other languages, this
behaves differently in JavaScript.
this
keyword is a way to implicitly pass information of an execution context to running execution context. If you are not sure what those terms mean, checkout out my other articles on JavaScript Runtime and JavaScript Scopes in-depth.
It is a way to provide data implicitly between different scopes.
The value of this
depends on which object it is bound to in runtime. This binding is decided using a set of rules which are always decided on the function call-site, and not where function is declared.
Let us try to understand this with a very simple example:
var me = {name: 'Writer'}
var you = {name: 'Reader'}
function printNameImplicit(){. // declaration site
console.log(this.name)
}
printNameImplicit.call(me) // call-site 1
printNameImplicit.call(you) // call-site 2
Using call
(more on it later), we bind this
of printNameImplicit
function (or of the scope created by the function) to any object we want. this
value then becomes that object, and name
is displayed accordingly.
In a way, we "imported" me
and you
object in printNameImplicit
function's scope at runtime.
Consider another example:
var person = {name: 'Adam'}
function addAge(age){
this.age = age;
}
function addAddress(address){
this.place = address
}
addAge.call(person,20);
addAddress.call(person,'House 1, Street A')
console.log(person) //{ name: 'Adam', age: 20, place: 'House 1, Street A' }
Using this
we can build up an object or modify its existing properties.
Bindings
As we have seen above, call
helps us bind this
of a function to an object. It takes two parameters, the first being the object we need this
to be bound to, the second is the argument required by the function whose this
is being bound.
But what happens when we have multiple arguments for the function? We can pass subsequent parameters to call
or
we can use a very similar construct called apply
. It is almost identical to call
except that its second parameter is an array of values, representing multiple parameters of the function. If we modify our above example, by combining the functions:
var person = {name: 'Adam'}
function addAddressAndAge(address, age){
this.place = address
this.age = age;
}
addAddressAndAge.apply(person, ['House 1, Street A', 20]);
// or addAddressAndAge.call(person, 'House 1, Street A', 20);
console.log(person) // { name: 'Adam', age: 20, place: 'House 1, Street A' }
As mentioned above call
and apply
are methods are a way to bind this
to an object. By default, in non-strict mode, this
is bound to the Global Object. In strict mode, this
is bound to nothing and hence its value is undefined
. This scenario is called as default binding.
// non-strict mode
var a = 10;
function print(){
console.log(this.a)
}
print() // 10
In the above example, this
is bound to global scope. In strict mode, the result would be undefined
instead.
Implicit Binding
Implicit binding refers to this
being bound to the object decorated on the call-site of the function. Consider the following example:
function printName(){
console.log(this.name)
}
const person = {
name: 'Adam',
printName: printName // implicit 'this' binding
}
person.printName(); // call site decorated with person object
When we modify object person
to include printName inside it, the this
of printName
is implicitly bound to person
if the function call is made using the person
object.
Also note that only the closest object reference at call site is bound to this
. For eg. if we do:
function printName(){
console.log(this.name)
}
const person = {
name: 'Adam',
printName: printName // implicit 'this' binding
}
const anotherPerson = {
name: 'Eve',
friend: person
}
anotherPerson.friend.printName(); // Adam
friend
, i.e. person
object will be bound to 'this' because it is closer to printName
function call.
Implicit bindings are lost when the call-site is undecorated. When implicit bindings are lost, they fallback to default binding.
let name = 'Eve'
function printName(){
console.log(this.name)
}
const person = {
name: 'Adam',
printName: printName // implicit 'this' binding
}
printName() // Eve, in non-strict mode, undefined in strict
Implicit bindings are also easily lost or re-bound in-case of assignments and callbacks.
let name = 'Eve'
function printName(){
console.log(this.name)
}
const person = {
name: 'Adam',
printName: printName
}
setTimeout(person.printName, 1000) // Eve, implicit binding lost
In other words it do not automatically forms a closure with the bound object.
Explicit Binding
Explicit binding, as the name suggests, is when we explicitly bind this
to an object using constructs like call
, apply
or bind
which we saw above. Unlike implicit binding, explicit binding constructs allow us to bind this
without actually altering the object its bound to.
function printName(){
console.log(this.name)
}
const person = {
name: 'Adam',
age: '32'
}
printName.call(person); // Adam
printName.apply(person); // Adam
However, explicit binding does not cure the problem of losing our bindings and is fragile like implicit binding. To tackle this, we can perform a certain type of explicit binding called as hard explicit binding or just hard binding.
Hard binding wraps the call-site in a wrapper function, this makes sure that the scope in which a function's this
is bound to the target object is not influenced by other scope.
function printName(){
console.log(this.name)
}
const person = {
name: 'Adam',
age: '32'
}
let hardBind = function (targetFunction, targetObject){
targetFunction.call(targetObject)
}
setTimeout(() => hardBind(printName, person), 1000); // Adam
Here, we provide a wrapping function, so that our calling code (which is setTimeout
) is forced to perform binding through that wrapper function only, making sure that whenever printName
is called, its this
is bound to the personObject
.
The above example utilises Closures. If you do not understand how? Take a look at my article on closure: JavaScript Closures with examples
The wrapper function is a common binding design pattern in JavaScrip and hence it is supplied as a default construct by the language in the name of bind
. The bind
returns a function which is called every-time we want to perform hard binding.
function printName(){
console.log(this.name)
}
const person = {
name: 'Adam',
age: '32'
}
let hardBind = printName.bind(person)
setTimeout(() => hardBind(printName, person), 1000); // Adam
new
Binding
To understand this
binding created by new
operator, let us quickly refresh how new
works.
Calling any function with new
makes it a constructor call. A constructor calls returns a newly created object, unless the function being called returns its own object.
The new
operator binds the constructor function's this
to this newly created object.
function Person(name, age){
this.name = name;
this.age = age;
}
let person = new Person('Adam', 32)
console.log(person) // Person { name: 'Adam', age: 32 }
Arrow Functions
this
behaves differently from arrow functions. Unlike regular functions arrow functions do not have their own this
, they form a closure with the this
value of enclosing scope. This behaviour is sometimes useful when we do not want to shadow this
value by creating our own for each function declared.
const obj = {
// implicitly bound this
getThisGetter() {
const getter = () => this;
return getter;
},
};
const fn = obj.getThisGetter();
console.log(fn() === obj); // true
const fn2 = obj.getThisGetter;
console.log(fn2()() === globalThis); // true in non-strict mode
this
keyword in JavaScript plays a crucial role in determining the runtime binding of functions to objects. Unlike many other programming languages, this
in JavaScript is dynamic and relies on the rules set during the function call-site, not the declaration. We explored how this can be explicitly bound using methods like call, apply, and bind, allowing for flexibility in managing the context in which functions are executed.
That's all for now. Until next time :)
Top comments (0)