In a web page, say we create an object that adds event listeners to certain elements on the DOM. Specifically we want to add a click event to a li element.
1 2 3 4 5 |
<ul> <li class='green'>green</li> <li class='red'>red</li> <li class='blue'>blue</li> </ul> |
So we create an object with a function called addGreenClickEvent. This function uses document to select our li element via class name green and proceeds to add the click handler.
In this handler, we attempt to access some data in the other function addGreenClickEvent because that is where we can access color and position by using this.
Note that when a function is within an object, the ‘this’ in that function will reference that object.
If its a standalone function, the ‘this’ within the function will reference the global object.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<script> var eventAdder = { color: 'green', position: 1, addGreenClickEvent: function() { console.log('box5 - clickMe executed'); console.log(this.color); // green console.log(this.position; // 1 document.querySelector('.green').addEventListener('click', function() { console.log('you clicked on green!'); // 'this' here is undefined because it is standalone callback. // It does not belong to any calling objects. var str = 'This box has color ' + this.color + ', with position ' + this.position; console.log(str); }); } } eventAdder.addGreenClickEvent(); </script> |
So now when you run the web page, in your console, you get:
box5 - clickMe executed
green
1
Now when you click on the li element, you’ll get this:
you clicked on green!
This box has color undefined, with position undefined
As you can see in the handler function, the properties color and position is undefined. If you try to log the ‘this’, you’ll see the HTML element you clicked on:
1 |
<li class='green'>green</li> |
Obviously this isn’t what we want. What we’re trying to do is to get the ‘this’ reference in addGreenClickEvent.
self hack
The ‘self’ hack basically declares a self property and point it to addGreenClickEvent ‘this’ reference.
By using scope, we call self in the event handler function and it works.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var eventAdder = { color: 'green', position: 1, addGreenClickEvent: function() { var self = this; document.querySelector('.green').addEventListener('click', function() { var str = 'This box has color ' + self.color + ', with position ' + self.position; console.log(str); }); } } eventAdder.addGreenClickEvent(); |
output:
This box has color green, with position 1
arrow function
If you use the arrow function, it uses the parent context, which is the ‘this’ inside the method addGreenClickEvent.
Since addGreenClickEvent is a method of object eventAdder, its ‘this’ correctly points to eventAdder.
Hence by using arrow function here, we point the ‘this’ inside the anonymous function to the parent context, which is the ‘this’ inside method addGreenClickEvent, that points to eventAdder obj.
1 2 3 4 5 6 7 8 9 10 11 12 |
var eventAdder = { color: 'green', position: 1, addGreenClickEvent: function() { // this reference obj eventAdder document.querySelector('.green').addEventListener('click', () => { var str = 'This box has color ' + this.color + ', with position ' + this.position; console.log(str); }); } } eventAdder.addGreenClickEvent(); |
Double Arrow
But what we made the outer function into an arrow function?
1 2 3 4 5 6 7 8 9 10 11 |
var eventAdder = { color: 'green', position: 1, addGreenClickEvent: () => { document.querySelector('.green').addEventListener('click', () => { var str = 'This box has color ' + this.color + ', with position ' + this.position; console.log(str); }); } } eventAdder.addGreenClickEvent(); |
You’ll see that our properties are undefined again.
The reason why this is, is because arrow functions points to the parent context. We’re not taking about the calling object anymore, which is what function does. We’re using the arrow function, which looks for the parent ‘this’. The parent context, in this case (‘this’ of eventAdder) is the windows object.
All you have to do is log the ‘this’ reference in addGreenClickEvent function and you’ll see that the Window object gets printed. Here’s why.
When we use the arrow function at addGreenClickEvent, it applies the self hack in the background like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// addGreenClickEvent's self = this, windows object var eventAdder = { color: 'green', position: 1, addGreenClickEvent: () => { console.log(this) // windows object // handler's self = this // windows object document.querySelector('.green').addEventListener('click', () => { // 'this' is windows object var str = 'This box has color ' + this.color + ', with position ' + this.position; console.log(str); }); } } eventAdder.addGreenClickEvent(); |
So as you can see by using the arrow function for addGreenClickEvent, the this references in that function becomes the global object.
Naturally, the handler function with the arrow function will also apply the self hack, which results in getting the global object for the ‘this’ reference as well.
Hence, in this situation, the right thing to do is to make sure the topmost function’s ‘this’ reference is correct. Then you can keep using arrow functions and it will chain correctly.
But when in doubt, start from the top and use self hack methodology to see what is assigned to each function.
Double function
We first create a function constructor.
1 2 3 |
function Person(name) { this.name = name; } |
We then attach a function called myFriends to its prototype object.
We iterate over an array and then call map on that array. We supply a function for it.
1 2 3 4 5 6 7 8 9 |
Person.prototype.myFriends = function(friends) { console.log(this); // Person var arr = friends.map(function(el) { console.log(this); // window object return this.name + ' is friends with ' + el; }); console.log(arr); } |
Then we run it.
1 2 |
var friends = ['Bob', 'Jane', 'Mark']; let p = new Person('John').myFriends(friends); |
The problem you will see is that even though our prototype function myFriends has its ‘this’ reference pointing to instance p.
Then when we run myFriends function, we get:
(3) [" is friends with Bob", " is friends with Jane", " is friends with Mark"]
We see that this.name does not display. Let’s simply log the this reference to check.
We see that the ‘this’ reference supplied to map is of the window object.
This is a behavior of es5, where if a function is not attached to an object, it automatically gets set to the global object.
As mentioned earlier, you can simply change the inner function to an arrow function and it will work.
But in our case, we can also use bind:
1 2 3 4 5 6 7 8 9 10 11 12 |
function Person(name) { this.name = name; } Person.prototype.myFriends = function(friends) { console.log(this); // Person var arr = friends.map(function(el) { return this.name + ' is friends with ' + el; }.bind(this)); console.log(arr); } var friends = ['Bob', 'Jane', 'Mark']; new Person('John').myFriends(friends); |
Unlike call or apply, bind does not execute the function. It simply binds an object to the function for future calls.
When in the future the inner function gets executed by map, its object bound to it will be the ‘this’ in myFriends function.