High-level summary for the bosses: call
, apply
and bind
are methods we can call on functions to make us less subject to the cruel whims of the this
keyword.
If you're in the UK and you tell someone to take the lift to the second floor, they'll know you're talking about using the machine that transports people between different floors of a building. In the US, someone hearing the same instruction might initially think someone is about to give them a piggyback ride up a flight of stairs.
The same is true when writing code — reserved keywords have different meanings depending on the scope, or execution context, in which they're used. In JavaScript, one of the trickiest examples of a keyword that changes its meaning at the drop of a hat is this
.
What is this
?
The this
keyword is a pointer that refers to a scope, or execution context, in your program. Which scope it refers to depends on where it is used. When used outside of a declared object, this
points to the global object. (In the browser, the global object is actually the window
object. If you open up the console in the inspector and type console.log(this)
, you will see the window
object and all of its properties and methods logged).
Used inside of a declared object, this
refers to the closest parent object. For example:
const jim = {
name: "Jim",
age: 24,
printAge: function() {
return this.age
}
}
const bob = {
name: "Bob",
age: 35,
printAge: function() {
return this.age
}
}
jim.printAge() // returns 24
bob.printAge() // returns 35
Like the rest of us, Jim is getting older. Let's add another method to the jim
object that will take the number of years he has aged, and return his new age:
const jim = {
name: "Jim",
age: 24,
printAge: function() {
return this.age
},
increaseAge: function increaseAge(years){
return this.name + " is now " + (this.age + years) + " years old."
}
}
const bob = {
name: "Bob",
age: 35,
printAge: function() {
return this.age
}
}
When we call jim.increaseAge(5)
, it will return Jim is now 29 years old
. But what if we want to use this same method on the bob
object? We could write it out again inside of bob
, but that would be redundant. Instead, we'll use some trickery to redefine this
when we call increaseAge
so that it refers to the bob
object, and the name
and age
properties therein.
call
to the rescue
call
is a method that we can use to specify what the keyword this
refers to in the function to which we're adding it. The first argument that we pass into call
is known as the thisArg
. This is the object we want this
in the function to refer to. The subsequent arguments are simply the regular arguments that we want to pass into the function.
Using call
, we can access the properties and methods of the bob
object when we invoke jim.increaseAge
:
const jim = {
name: "Jim",
age: 24,
printAge: function() {
return this.age
},
increaseAge: function increaseAge(years){
return this.name + " is now " + (this.age + years) + " years old."
}
}
const bob = {
name: "Bob",
age: 35,
printAge: function() {
return this.age
}
}
jim.increaseAge.call(bob, 5) // returns "Bob is now 40 years old."
apply
allows us to control what this
refers to inside of a function when we call it from outside of the context in which that function was defined. This is the sort of sorcery that allows us to write more versatile and reusable code.
apply
is call
's slightly higher achieving older sibling
Like call
, apply
specifies the context that this
in the function will take in its first argument (the thisArg
). call
can only pass a predetermined number of arguments to a function, however, whereas apply
passes an array of arguments to a function that are then unpacked and passed as parameters to the function.
Let's take a look at how this works:
const obj1 = {
num: 5
}
const obj2 = {
num: 2
}
const addToObj = function(a, b, c) {
return this.num + a + b + c
}
let arr1 = [2, 3, 5, 6]
let arr2 = [4, 6, 3, 9]
console.log(addToObj.apply(obj1, arr1)) // logs 15 to the console
console.log(addToObj.apply(obj1, arr2)) // logs 18 to the console
console.log(addToObj.apply(obj2, arr1)) // logs 12 to the console
console.log(addToObj.apply(obj2, arr2)) // logs 15 to the console
Above, we declare arrays as variables then use these variables as apply
's second argument to pass them into the function as separate parameters. With call
, we would not be able to do this, as call
requires the functions arguments to be passed as regular comma-separated parameters.
The bind
method: delayed gratification
bind
is the third in this family of methods that allow us to redefine the context of this
when we call functions. While the difference between call
and apply
is subtle, the difference between bind
and call
and apply
is more significant.
Rather than immediately invoking the function on which it's called, bind
returns a function definition with the keyword this
set to the value of the first argument passed into it (the thisArg
). This is very useful when we know we want to redefine this
in a function, but we don't know which arguments we want to pass into the function.
Let's go back to bob
and jim
to take a look at how bind
works:
var bob = {
firstName: "Bob",
sayHi: function(){
return "Hi " + this.firstName
},
addNumbers: function(a,b,c,d){
return this.firstName + " just calculated " + (a+b+c+d)
}
}
var jim = {
firstName: "Jim"
}
var jimAdd = bob.addNumbers.bind(jim, 1, 2, 3, 4)
jimAdd() // Jimm just calculated 10
Here, we took the method addNumbers
on the bob
object and called the bind
method on it to create another function in which this
is redefined as the jim
object. We stored this new function in the variable jimAdd
, then subsequently called jimAdd
. This works, because unlike call
and apply
, bind
doesn't immediately invoke the function it's used on.
Don't know your arguments yet? Get out of your bind with bind
.
When we use bind
, we do not always need to know the parameters to the function we are creating — the only argument we need to pass into bind
is the thisArg
.
Let's return once again to Jim and Bob:
var bob = {
firstName: "Bob",
sayHi: function(){
return "Hi " + this.firstName
},
addNumbers: function(a,b,c,d){
return this.firstName + " just calculated " + (a+b+c+d)
}
}
var jim = {
firstName: "Jim"
}
var jimAdd2 = bob.addNumbers.bind(jim, 1,2)
jimAdd2(3,7) // returns 'Jim just added 13'
When we create the jimAdd2
function with bind
, we only pass two arguments into the function. When we call it on the following line, we pass two additional arguments, and the function is called with all four arguments.
Using bind
with asynchronous code:
Another common application of the bind
method is is asynchronous code.
var bob = {
firstName: "Bob",
sayHi: function(){
setTimeout(function(){
console.log("Hi " + this.firstName)
},1000)
}
}
bob.sayHi() // Hi undefined (1000 miliseconds later)
Since the closest parent object to the sayHi
method is bob
, it would make sense that sayHi
returns Hi Bob
. However, because setTimeout
is called at a later point in time, the object that it is attached to at the point of execution is not the bob
object, but the window
object.
If we still want the context of this
in the setTimeout
function to be the bob
object when it is called, bind
provides a perfect solution. It allows us to explicitly set the context of this
without immediately invoking the function:
var bob = {
firstName: "Bob",
sayHi: function(){
setTimeout(function(){
console.log("Hi " + this.firstName)
}.bind(this),1000)
}
}
bob.sayHi() // "Hi Bob"
In this example, we can actually pass this
in as the thisArg
of bind
on the setTimeout
function, because in this case, this
is equal to the bob
object itself. (We could just as easily have passed in bob
, but often, we will see code that uses this
in cases such as this instead of the name of the object.
Top comments (0)