DEV Community

loading...
Cover image for Gamify! - A Gamified Approach to Named vs Arrow Functions

Gamify! - A Gamified Approach to Named vs Arrow Functions

bkbranden profile image Branden Kim ・14 min read

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!


drawing

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

Basic Bindings Asset

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.

Level 1 Basic This 001

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:

Level 1 Basic This 002

This allows us to access members of the level1obj.

Level 1 Basic This 003

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.

Level 1 Basic This 004

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.

Level 1 Dynamic Context

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!


drawing

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.

Arguments Binding Image

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.

Constructable and Callable Image

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.

Generator Functions Gif

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:

  1. 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.

New Context Gif

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.

  1. 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.

Callbacks Gif

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.

  1. 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.

Nested Objs Gif

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.


drawing

Level 3

Here I wanted to cover several examples of using named vs arrow functions and explaining the results of each example.

  1. Asynchronous Functions

Asynchronous Functions Gif

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.

  1. Classes

Classes Gif

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.

  1. 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().

Objects Prototypes Gif

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)?

  1. 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.

Callback This Binding Gif

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.

  1. Use bind(), call(), or apply()

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.


drawing

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).

Named Function AST

Looking at the AST node output for a named function, we can see that it is of type FunctionDeclaration.

Arrow Function AST

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.

Declare Function Source Code

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.

Declare Scope Source Code

On the other hand for variable declarations, there is no changing of the this variable within its declaration.

Declare Variable Source Code

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.

Discussion (2)

pic
Editor guide
Collapse
shinesanthosh profile image
Shine Santhosh

The comparison with Mario and Luigi, that's amazing🔥🔥.
I wish I knew these when I was learning this 😂

Collapse
bkbranden profile image
Branden Kim Author

Thanks! I hope it isn't too out there, but I honestly couldn't think of a better analogy