Understand “this” in JavaScript
Blog
Thought Leadership
Understand “this” in JavaScript
Yuruo Wang
I know it is 2018 already, but this keyword still manifests to be one of the most confusing parts of JavaScript. And it haunts me occasionally with some weird and unexpected bugs. So, I figured there is still some value to dive deep into how this works since it’s vital and is the foundation of more advanced concepts such as prototype chain and closure.
First, two basic important rules:
JavaScript has Lexical Scoping with function scope. This means that the variable scope is determined by its location of the code. We can infer the code by just reading the code instead of going through compilation, as opposed to Dynamic Scoping. Although we can’t change JavaScript’s essence of lexical scoping, we can control the context when a function is called. Context is an activation record that is created automatically when a function gets called. It carries information about the function such as where is the call-back, how it is called, etc. One of the attributes of this record is this, which points to the object during its execution context. (See details in the Implicitly rule.)Overall, there are four rules of how this works: globally, implicitly, explicitly and constructively with the new keyword.
GloballyWhen this is outside of a declared object, this refers to the global object, which is a window object in browsers and a global object in the Node environment.
1 2 console.log(this === window) // trueAlthough it looks like something we may want to use, this is actually a bad design, which can bring many issues if the developer accidentally misuses this and puts some variable on the global that everyone has access to, while meaning to keep the variable to a certain scope. For example:
1 2 3 4 5 6 function createNewPerson() { this.person = "Foo"; // this refers to global object } createNewPerson(); person // Foo - Oops, now we have access to person now globallyGenerally, the best practice is to declare all the global variables at the top of the code and set/update them in the functions at a later time. Declaring a global variable in a function scope is neither clean, nor easy to understand.
Thankfully, strict mode is introduced by ES5 to help us write better code. With strict mode being turned on by adding 'use strict' to the file, it is impossible to accidentally create global variables. When we are not inside a declared object, this is “undefined” instead of global scope.
1 2 3 4 5 6 7 8 9 'use strict' function createNewPerson() { this.person = "Foo"; // Cannot set property 'person' of undefined } createNewPerson(); personImplicitly
This rule means:
When function is called using object.foo(), this in the function foo refers to the calling object (the context).For example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var person = { name: "A", sayHi: function(){ console.log("Hi " + this.name); } } var sayHi = person.sayHi; var name = "B"; // first way of calling func person.sayHi(); // Hi A // second way of calling func sayHi(); // Hi BIn this example, although person.sayHi and sayHi point to the exactly same function, the results are different because the contexts are different. In the first example, the calling object is the person object, while in the second example the calling object is the global object. And in this case, the window object. That is why each function grabs the corresponding name variable of the context.
When this is inside of a declared object, the value of this will be the closest parent object.Things can get trickier in nested objects, but looking for the closest parent object can always help figure out the correct scope. For instance:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 var person = { name: "Mr.G", age: 18, sayHey: function () { return "hey " + this.name; }, isContextEqual: function () { return this === person; }, cat: { name: "Hello Kitty", sayHey: function () { return "hey " + this.name; }, sayAge: function () { return "your age is " + this.age; }, isContextEqual: function () { return this === person; } } } person.sayHey() // "hey Mr.G" person.isContextEqual() // true person.cat.sayHey() // "hey Hello Kitty" person.cat.isContextEqual() // false person.cat.sayAge() // your age is undefinedIn this example, the cat object is nested inside the person object. When calling sayAge() in cat, this is indeed inside a declared object, and the closest parent object is cat.Since cat doesn’t have an age variable in the object scope, it is returning undefined for this.age.
What if we want to borrow the implementation of the sayAge function but only want to change the value of this to person? That way our code can be drier. JavaScript offered three methods that enable us to change the value of this explicitly.
ExplicitlyThese three methods are call(), apply() and bind(). These are super powerful methods. which make JavaScript calls very flexible, and each has its own best use case. For what it’s worth, these methods can only be used on functions.
Method Syntax Invoked Immediately? call call(thisArg, …) Yes apply apply(thisArg, […]) Yes bind bind(thisArg, …) No, returns func definition call() and apply() are very similar and they are very common in JavaScript for borrowing methods by changing the value of this, except a syntax difference, call() method, takes all the parameters separated by comma while apply() takes an array of parameters, making it suitable for variadic functions. call and apply are invoked immediately with setting this, while bind just returns the function definition. This makes it great for function currying (We don’t want to invoke the function right away because not all arguments are ready yet.) or async method. bind takes arguments separated by commas just like call. ES6 introduced Arrow Functions, in which scopes are set lexically. It always refers to its execution context, and is no longer affected by call, apply and bind, so thisArg can be omitted.Example of call and apply:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var foo = { name: "Foo", addNumbers: function (a, b, c, d) { return this.name + " just calculated " + (a + b + c + d); } } var bar = { name: "Bar" } foo.addNumbers.apply(bar, [1, 2, 3, 4]); // Bar just calculated 10 foo.addNumbers.call(bar, 1, 2, 3, 4); // Bar just calculated 10How bind can be used with partial function application:
1 2 3 4 5 6 7 8 9 10 var foo = { name: "Foo", addNumbers: function (a, b, c, d) { return this.name + " just calculated " + (a + b + c + d); } } var barCal = foo.addNumbers.bind(bar, 1, 2); barCal(3, 4); // Bar just calculated 10bind is also commonly used in async methods where the context of this can be a little confusing:
1 2 3 4 5 6 7 8 9 10 11 var baz = { bazName: "Baz", sayHey: () => { setTimeout(() => { console.log("hey " + this.bazName); }, 1000) } } baz.sayHey() //hey undefinedIn this example, you may say, “Since this is inside a declared object, this refers to the closest object, which is the baz object in this case.” However, the callback function within the setTimeOut is not called instantly, but rather later. And by the time it is called, the context isn’t the baz object anymore. It’s the global object – window. In order to fix this, we can explicitly bind the this of the callback to baz object, which is what this refers when sayHey is invoked. So, this solution can be:
1 2 3 4 5 6 7 8 9 10 var baz = { bazName: "Baz", sayHey: function(){ setTimeout(function(){ console.log("hey " + this.bazName); }.bind(this),1000) } } baz.sayHey() //hey BazThe first parameter this in the bind method refers to the baz object when we declare the setTimeOut, thus setting the context of this in the callback to also be baz object.
ConstructivelyThis rule is pretty straight-forward: When using the new operator, this refers to the new object itself. For instance, when declaring function object Person
1 2 3 4 5 6 7 8 9 10 function Person(name, age) { this.name = name; this.age = age; } var kate = new Person('Kate', 10); kate.name // Kate kate.age // 10Reference and Extension Reading
What is the Execution Context & Stack in JavaScript?