When working with TypeScript, it's important to understand some of the key differences between named and fat arrow functions. Let's have a look at a basic class with one of each type of function defined, and also the resulting ES5 that TypeScript transpiles to for a better understanding.
Here is a sample TypeScript class with the two different types of functions.
Here is the resulting transpiled ES5 JavaScript.
For the purpose of this post, we'll concentrate on (2) main aspects of the code
There are other use cases beyond the ones I'm mentioning here today in regards to the behavior of this with inheritance and calling the parent class as well as other differences. However these are two key areas that should be understood, as I see the usage flip-flop without intent, and developers should be aware of the performance and behavior differences and create the correct type where appropriate. They have separate purposes, so I'm not a fan of just one or the other. Using only fat arrow functions have their performance impact, but shouldn't be avoided all together as they are instrumental on capturing the correct context of this when needed.
Here is a sample TypeScript class with the two different types of functions.
class MyTsClass { private myValue: number = 0; myNamedFunction(): void { setTimeout(function () { console.log(`Inside myNamedFunction, this.myValue = ${this.myValue}`); }, 1000); } myFatArrowFunction = (someValue: number) => { this.myValue = someValue; console.log(`Inside myFatArrowFunction, this.myValue = ${this.myValue}`); } } const myClass = new MyTsClass(); myClass.myFatArrowFunction(1); myClass.myNamedFunction();
Here is the resulting transpiled ES5 JavaScript.
"use strict"; var MyTsClass = /** @class */ (function () { function MyTsClass() { var _this = this; this.myValue = 0; this.myFatArrowFunction = function (someValue) { _this.myValue = someValue; console.log("Inside myFatArrowFunction, this.myValue = " + _this.myValue); }; } MyTsClass.prototype.myNamedFunction = function () { var _this = this; setTimeout(function () { console.log("Inside myNamedFunction, this.myValue = " + _this.myValue); }, 1000); }; return MyTsClass; }()); var myClass = new MyTsClass(); myClass.myFatArrowFunction(1); myClass.myNamedFunction();
For the purpose of this post, we'll concentrate on (2) main aspects of the code
- Where the method is created on the object, and the resulting performance impact
- How the this keyword behaves
It's important to understand the differences because there is a performance impact. Notice how the named function on the class is created and attached on the object's prototype. In this manner, the method is stored in memory only one time, as objects created from the same constructor point to a single prototype object. This is the more memory efficient implementation.
Now inspect the fat arrow's function in the ES5 code. Because the method is declared and created in the object's constructor (instance member), it will be declared again per each new instance of this type of object created. This has a memory and performance overhead. As a note, this is the identical resulting behavior to explicitly creating methods within the class constructor in TypeScript.
At this point we might be thinking, "Well that's enough let's just use named functions which will be declared on the object's prototype and be done with it!" There is more than meets the eye and the fat arrow function is quite useful. One of the main advantages is that fat arrow functions lexically capture the context of this in TypeScript. This is critically important in functions that contain callbacks or where re-assignment of instance variables might occur. The former use case is one that we run into often with observable callbacks in say Angular components. You may have run into issues with the this keyword and noticed it wasn't the correct context. The issue is if not using the fat arrow syntax, the context of the this keyword will be undefined. If strict mode isn't enabled, the context of this will be of the window object. If you look in the ES5 tanspiled code, you'll notice this is captured from outside the function body allowing our context to be captured correctly.
var _this = this;
Let's look at the console output from the original code above. Notice in the setTimeout call the use of this. However in this instance it's not the proper context resulting in undefined when accessed.
The fix is to update the callback to use the fat arrow syntax which will capture the correct context of this.
If we run the code again, we get the console output we expect.
In a TypeScript project with the default configuration of "noImplicitThis": true set in the tsconfig.json file, you'll actually be warned in the IDE of this scenario, " 'this' implicitly has type 'any' because it does not have a type annotation." This would be an indication you need to use the fat arrow function instead.
myNamedFunction(): void { setTimeout(() => { console.log(`Inside myNamedFunction, this.myValue = ${this.myValue}`); }, 1000); }
If we run the code again, we get the console output we expect.
There are other use cases beyond the ones I'm mentioning here today in regards to the behavior of this with inheritance and calling the parent class as well as other differences. However these are two key areas that should be understood, as I see the usage flip-flop without intent, and developers should be aware of the performance and behavior differences and create the correct type where appropriate. They have separate purposes, so I'm not a fan of just one or the other. Using only fat arrow functions have their performance impact, but shouldn't be avoided all together as they are instrumental on capturing the correct context of this when needed.
No comments:
Post a Comment