Background
This is one part in a series called Gamify! where I try to create Gamified versions of the typical tutorial. I try to gamify learning since I believe its the best way for all skill and passion levels to get what they want out of the tutorial as well as been fun and informative. When going through this tutorial, there is a level that corresponds to how much and how in-depth you want to learn about the topic. If you just want to know what the topic is about Level 0 should be enough, but if you care about the nitty gritty details, Level 4 might be of interest.
Table of Contents
Introduction
Within Javascript you have probably seen something like:
const fun = () => {
// statements...
}
When encountering this syntax for the first time, it can really confuse you (it did for me) and it took me a while to get used to what it mean and why it was used.
Well fret no further because I am going to demystify this for you!
Level 0
What are "Arrow Functions"?
Arrow functions are another syntactical method to declare functions in Javacript (and Typescript). Basically its another form of function declarations with the following syntax:
(param1, param2, param3, ..., paramN) => { statements }
However with arrow functions, they must be assigned to a variable.
Here is an example:
// Declaration
const func = (a) => {
return a * a;
}
// invocation
func(10) // returns 100
This as opposed to the regular function declaration:
// Declaration
function namedFunction(a) {
return a*a;
}
// Invocation
namedFunction(10) // returns 100
Notice how the two functions had the exact same result with the same input! Basically, whenever you encounter this syntax, just read it as a regular function in your head!
If you want to learn more, progress to the next level!
Level 1
Differences between Named vs Arrow Functions
Out of all the differences, there is one really important difference between Named and Arrow functions:
"This" context
Arrow functions do not redefine the context of the this
keyword when created. This is different from that of named functions which do redefine the this
context based on what scope it is in.
Back when I first encountered arrow functions and read about their differences, I still didn't understand what the difference was. To help you avoid frustration and understand better, I have created a quick analogy:
Think of Named functions (ie. when using "function" keyword) as Mario and Arrow functions (ie. "() =>" syntax) as Luigi. Named functions and Arrow functions have the same end goal: defining a function similar to how Mario and Luigi have the same end goal of defeating Bowser and saving Princess Peach. However, Mario's fireball ability and Luigi's fireball ability differ in that Mario's fireball adheres to the rules of gravity while Luigi's fireball does not and is independent of the rules of gravity. Named functions and Arrow functions have a similar pattern. Named functions always follow the rule of defining the "this" context to its outer scope, while Arrow functions do not follow this rule. Basically, Named functions similar to Mario's fireballs follow rules while Arrow functions and Luigi's fireballs do not follow the rules, even though the overall goals for both are the same.
How "this" changes
Above is a basic demonstration of the this
binding in action. At a high level, we can see that when this
is returned within the arrow function, it is not pointing to the level1obj
but rather to the global window context. On the other hand, the named function return this
points to level1obj
.
We can see here that calling the named function and returning the this
value results in this
referring to our level1obj
which allows us to do things like:
This allows us to access members of the level1obj
.
On the other hand, when we access the arrowFunctions this
that gets returned, we actually get the global window
object. This is because the arrow function does not change the this
context.
Hence, accessing testParam
with this
will not work.
When to use Named vs Arrow
Now that you know some basic on how the Arrow function changes this
, here are some general guidelines for when to use Named functions vs Arrow functions.
1. Don't use arrow functions as members of an object
For reasons that we can see above. In the example given above, if for some reason within the function we have to access the members of the object (level1obj
in the example), then we can't access them from within the function which will make things quite difficult.
2. Use arrow functions within callbacks
There is a deeper explanation for why this rule should be adhered to in the higher levels, but as a general guideline, arrow functions should be used in callbacks as you will be able to preserve your this
.
3. Use arrow functions within dynamic contexts
By dynamic contexts, I mean anytime you are trying access or modify an object and its methods after the program runs. This usually appears when using events with some kind of event handler. When a callback function is passed to the event handler, the this
reference points to the object that is listening for the event rather than the object that created the callback. Most times, it is advantageous to have the this
reference point to the object that created it to modify its member variables or state. This is a common problem in React that arises when developers first learn about passing functions as props.
Here we can see that when the Named Function is called within the class, the this
context is not pointing to the class but rather the global window.
On the other hand, the arrow function preserves the this
context and can access the Dynamic
classes's member variables within the callback function.
If you want to go more in-depth in the differences go to the next level!
Level 2
Arrow functions have more differences than just the this
context and for simplification, I spared the explanation on why the differences occur.
Arguments Binding
Named functions have this feature called arguments binding. Utilizing the new
keyword, you can create an instance of a function and store the arguments to a function within a variable.
Here we can see that when utilizing a named function, we are able to bind the arguments by utilizing the arguments
keyword within the function.
However, in the arrow function, it doesn't keep that reference to the arguments
keyword.
Constructable and Callable
Named functions are constructable and callable meaning that they are able to be called utilizing the new
keyword, creating a new instance of the function, and are able to be called as regular functions.
Arrow functions, on the other hand, are only callable. This means that arrow functions cannot be called utilizing the new
keyword.
In the screenshot above, we can see that new
could be used with named functions to create a new object. However, when utilized with the arrow function, the compiler gives an error: "TypeError: y is not a constructor".
Generators
Named functions have access to a special keyword yield
. This keyword along with a special syntax on the function declaration allows the function to become a Generator function
. A generator function is one that can be exited and later re-entered where the information within the function context is saved even after exiting the function. If this sounds a bit confusing don't worry! What generator functions are, how they work, and use cases will be covered in another Gamify! series post.
While named functions have access to yield
, arrow functions do not, meaning arrow functions cannot be generator functions.
Above we can see that when using the named function, we were able to create generator functions and utilize them with yield
. However, when the same syntax was made the arrow function, the parser couldn't figure out what yield
was.
In-depth explanation of "this" context
In the previous level, we found several use cases of named and arrow functions based on how the this
context changes. While I explained the "what" I haven't yet explained the "why".
When generalized, the rules of how the this
context switches are fairly simple:
-
new
keyword
The new
keyword changes the context of the outermost this
context for everything within that scope. This means that any functions defined within the object that gets created using new
will have its this
reference pointing to that new object. Let's see a very simple example of how this changes.
Normally in the global scope, this
refers to either the window or undefined. If we were to create a new object with new
, then if any of the functions within that new object reference this
, they will point to the new object that was created.
Here we can see that we create a new obj1
that logs its this
reference and it points to itself. Within its member functions, it creates a new instance of obj2
which logs it own this
reference which points to its own member variables in both the named function and arrow function.
The new
keyword changes all of the this
contexts of functions (both named and arrow) defined in its scope to point to instance of the newly instantiated object.
- Callbacks
Callbacks makes things a bit messy. When encountering a function declaration to find the this
context, the outer scope needs to be identified. While the scope of normal variables are determined by the lexical scope, the this
scope is determined by where it is called. Generally, the way callbacks work is that the compiler stores the context of where the callback function was passed as the callback's scope.
let obj = {
name: "test",
cb() {
return ("Hi", this.name)
}
}
setTimeout(obj.cb, 1000)
In this example, this it will print out "Hi undefined". In this case, the callback "obj.cb" was defined in the global scope and as such the this
reference will be lost and not set to obj
.
Unlike named functions, arrow functions are treated as variables and thus are subject to the compiler's lexical scope. This means that within callbacks, there will be a difference in functionality with the this
keyword.
We can see in the above example that when a named function is used within the callback, the this
context becomes global as when setTimeout is invoked, where the callback gets defined and run is in the global context not in obj
, hence the this
context points to the window.
On the other hand, when an arrow function is used, since it is treated as a variable, it doesn't redefine the this
context which is why it still points to obj
.
- Nested objects within classes
The simplest way to handle how named and arrow functions differ is to treat named functions as redefining this
to the parent context of where it is defined and arrow functions as not redefining this
.
In this nested objects example, the named function this
reference points to the innermost nested object while the arrow function this
reference points to the outermost object.
Thats all for this level, in the next one we will cover different instances and common patterns for fixing losing this
context.
Level 3
Here I wanted to cover several examples of using named vs arrow functions and explaining the results of each example.
- Asynchronous Functions
With asynchronous functions, the binding of this
follows the same rules as for regular functions and callbacks. In the example above, we can see that when using named functions for the callback to the Promise, we lose the context of this
and it gets sent to the window. However, when we use arrow functions, we retain our context to the object. One aspect to note is that because our "arrowFunction" member variable is a named function, the this
context within it points to the obj
. If we had used an arrow function instead, it this
would point to the window instead.
A takeaway we can note here is that, asynchronous functions don't change any differences between named and arrow functions, they both retain the same rules when used as regular functions and callbacks.
- Classes
Within classes, the context of this
changes due to the use of the new
keyword. Because new
is an identifier for detecting the start of a new context, both namedFunction
and arrowFunc
have their this
context pointing to class Testing
.
Following the rule for callbacks mentioned previously, when we call namedFunction
because of the use of named functions within the callbacks, the this
context is lost within the Promise.
On the other hand, arrowFunc
uses arrow functions in the callback handlers, so the this
context is kept.
- Object.create() and Prototypes
Prototypes are the method in which Javascript objects inherit base and additional features from one another. Using Object.create()
syntax, you are able to create the equivalent of classes
using prototypes in Javascript with Objects.create().
In the example above, using the prototype of the object proto
I created a new object using Object.create()
. Here it just creates a new object with the prototype that gets passed in meaning, p
is a new object with the member variables and methods of proto
.
In this scenario, namedFunc
has a this
reference to the member variables of proto
but just returning this
by itself shows an empty object. This is probably due to the fact that Javascript cannot determine whether this
is referring to proto
or the prototype for objects as Object.create() creates an object with the existing object as the prototype of the newly created object.
When using arrowFunc
there is no new
keyword used here, even though we are creating a new object. This combined with the rules for arrow functions never change the this
context, thus not changing it from pointing to the window.
Patterns for fixing losing this
So how do we not lose this
(nice pun)?
- Using arrow functions
Arrow functions in Javascript are actually treated as variables that are bound to the lexical scope as opposed to functions (more on this in the next level). This is why arrow functions don't change the this
context when created.
const arrowFunc = () => {
console.log(this)
}
function higherOrder(callback) {
let obj = {
name: "some new object"
}
obj.callback = callback
obj.callback()
}
function namedFunction() {
console.log(this)
}
higherOrder(namedFunction)
higherOrder(arrowFunc)
What do you think is going to be printed to the console in both cases?
Here namedFunction
actually prints the obj
that was defined within the higherOrder
function while arrowFunc
prints the global window.
The reason this happens is because when arrowFunc
was defined, it was treated as a variable meaning where this
was referring to was already known as the lexer was able to scope the variable to the outermost scope.
However, with namedFunction
, it is treated as a function and when it got passed into higherOrder
, there was no way it could know what this
was referring to until it was tied as a member function to obj
within higherOrder
Because of this effect within callbacks, it is generally preferred to pass arrow functions within callbacks as the this
context doesn't change as much and cause confusion.
- Use
bind()
,call()
, orapply()
When using bind()
on a function, this returns a copy of the function with this
pointing to the object passed into the bind function.
let obj = {
aProp: "this is a property",
namedFunction() {
console.log(this)
}
}
let obj2 = {
message: "When passed to bind, this object will be referenced by 'this'"
}
let funcBind = obj.namedFunction.bind(obj2)
obj.namedFunction() // returns obj
funcBind() // returns obj2
Here we can see that by using bind()
we were able to bind the this
reference to another object. When using bind()
it expects a parameter that is an object to bind the this
reference to and then returns a copy of the function with the this
reference changed. Also, the original function is not changed as above, obj.namedFunction()
still has its this
pointing to itself.
A common pattern is for an object to pass itself in bind()
so that its member function can be passed to another function as a callback, but still modify properties in the original object.
class ChangeMe {
constructor() {
this.state = []
}
handleChange() {
this.state = [1, 2, 3]
}
}
Commonly used in React components, if handleChange()
is passed as a prop to a child component without calling bind()
, this
will point towards the child component and will change the child state not the parent.
Using bind, however, we can fix this!
class ChangeMe {
constructor() {
this.state = []
this.bindHandleChange = this.handleChange.bind(this)
}
handleChange() {
this.state = [1, 2, 3]
}
}
There are two other functions: apply()
and call()
that have a similar functionality as bind()
except, they call and run the function immediately.
let obj = {
aProp: "this is a property",
namedFunction(param1, param2) {
console.log(param1)
console.log(param2)
console.log(this)
}
}
let obj2 = {
message: "When passed bind, this object will be referenced by 'this'"
}
obj.namedFunction.apply(obj2, ["test", "test2"])
obj.namedFunction.call(obj2, "test", "test2")
Both apply and call takes the object to bind this
to as the first parameter and run the function immediately. However, apply()
takes an array of parameters, while call()
takes parameters separated by commas.
Bind()
, call()
, and apply()
all bind this
to the object that gets passed in. In common cases, the class that owns that function usually binds its own this
reference to the function in case that the function is used in a callback.
Level 4
I know what some of you are thinking at this level, exactly why does Javascript treat named and arrow functions differently?
In this level lets take a look at the AST that gets generated from Javascript compilers, specifically Node.
const { Parser } = require('acorn')
const namedAst = Parser.parse("function namedFunction() { return 1}")
console.log(namedAst)
const arrowAst = Parser.parse("const arrowFunction = () => {return 1}")
console.log(arrowAst)
I am just passing a very simple named function and arrow function in the form of a string to a package called acorn which is a package for a small Javascript parser that can generate the AST for a given Javascript program (for those that are not familiar, AST is abstract syntax tree).
Looking at the AST node output for a named function, we can see that it is of type FunctionDeclaration.
On the other hand, an arrow function is treated as a node of type VariableDeclaration.
FunctionDeclaration and VariableDeclaration types are interesting, but we don't know what they are yet. After digging into the source code for the Node compiler,
I was able to pin down some files where these types were referenced.
From the Node compiler, this is the source code within scopes.cc to generatee the scope for default function variables.
Highlighted is a function within the same file that checks if the function is derived from an object and then assigns the this
variable as a function local variable.
In addition there is a function called DeclareDynamicGlobal
that gets used within the declaration of the scope that references this
, most likely to change it dynamically based on the current scope.
On the other hand for variable declarations, there is no changing of the this
variable within its declaration.
There is more to this function, however, there was nothing reeferencing the two methods, DeclareThis
and DeclareDynamicGlobal
within this function for declaring the scope of variables.
While I am not too familiar with this source code as I haven't written or contributed to it, I think I was able to make a reasonable assumption as to why functions reassign this
but variables do not.
Conclusion
If you made it this far congrats! 🎉
This was a part in the series of Gamify! where I try to write gamified tutorials that go in-depth (to the best of my ability) into a topic while also providing simplifications and steps towards learning more advanced knowledge within the topic. This time we covered Named vs Arrow functions, specifically, how they are the same, but also how they differ as well as providing solutions to common problems faced when handling those differences. Furthermore, we went in-depth into the AST of a Javascript compiler to figure out why and how the compiler made those differences happen.
Top comments (2)
The comparison with Mario and Luigi, that's amazing🔥🔥.
I wish I knew these when I was learning
this
😂Thanks! I hope it isn't too out there, but I honestly couldn't think of a better analogy