Regular function vs arrow function in JavaScript

In this article, we will learn about the differences between a regular and an arrow function. We will explore the differences in syntax, the value of the this keyword, the value of the arguments keyword, creating constructor functions, and hoisting behavior. Let's dive into these details.

Differences in syntax

A regular function can be created using two different syntaxes: function declaration and function expression. Following is an example of a function declaration:

// Function declaration
function logger() {}

As you can see, a function declaration starts with the function keyword, followed by the function name, a set of parentheses, and ends with a set of curly braces.

On the other hand, a function expression starts by creating a variable with var, let, or const and assigning an anonymous function to that variable. Following is an example of a function expression:

// Function expression
const logger = function () {};

Notice that an anonymous regular function starts with the function keyword, followed by a set of parentheses, and ends with a set of curly braces.

Similar to a function expression, an arrow function starts by creating a variable and assigning an anonymous arrow function to that variable, as shown in the following example:

// Arrow function
const logger = () => {};

Notice that an anonymous arrow function starts with a set of parentheses followed by an arrow => and ends with a set of curly braces.

An arrow function can also be written without the closing curly braces if the return statement is a single line of code, as you can see in the below example:

const logger = () => console.log('Hello world!');

Differences in the value of the ‘this’ keyword

The next significant difference between a regular and an arrow function is that a regular function gets its own this keyword, whereas an arrow function doesn't. Instead, the this keyword in an arrow function points to its parent scope.

// Regular function
function logger() {
  console.log(this); // undefined
}
logger();

// Arrow function
const greet = () => {
  console.log(this); // Window
};
greet();

As you can see in the above examples, the this keyword in the regular function results in undefined. On the other hand, the this keyword in the arrow function points to the global Window object (parent scope).

It's worth noting that in the browser environment, where the this keyword points to the global Window object, in the Node.js environment, it points to an empty object, except for a function declaration or expression in non-strict mode, where it points to the Node.js Global object.

Good to know that variables created with var are placed in the browser's global Window object (aren't placed in the Node.js Global object). This practice, when combined with the use of arrow functions in object methods, can lead to unexpected behavior. Let's examine this with an example.

var name = 'Matt';

const john = {
  name: 'John',
  greet: () => {
    console.log(`Good morning ${this.name}`); // Good morning Matt
  },
};

john.greet();

In the above code, as an object literal doesn't create a scope in JavaScript, the parent scope of the greet method is the global Window object, which has a variable name created with var. So, this.name within the greet method points to that variable and results in 'Good morning Matt'.

This is weird, right? This behavior can introduce hard-to-find bugs in applications. This is why it's highly recommended to avoid using var and to use regular functions in object methods instead of arrow functions.

If you are interested in learning more about how the this keyword behaves when used in different places, I have an article on Understanding the dynamic nature of the ‘this’ keyword in JavaScript that provides further insights.

Differences in the value of the ‘arguments’ keyword

Similar to the this keyword, a regular function gets its own arguments keyword, while an arrow function doesn't. Instead, the arguments keyword in an arrow function points to the arguments keyword of its parent scope.

const john = {
  name: 'John',
  greet() {
    console.log(arguments); // [Arguments] { '0': 1, '1': 2, '2': 3 }

    const logger = () => {
      console.log(arguments); // [Arguments] { '0': 1, '1': 2, '2': 3 }
    };
    logger(4, 5, 6);
  },
};

john.greet(1, 2, 3);

In the above example, notice that the arguments keyword within the greet method refers to the arguments provided to it. However, the arguments keyword within the logger function also refers to the arguments provided to the greet method (parent scope), instead of the arguments provided to the logger function itself.

Differences in creating constructor function

Another distinction between a regular and an arrow function is that a constructor function can be created using a regular function, whereas it cannot be created using an arrow function. Trying to create a constructor function with an arrow function results in a type error, as you can see in the code below:

// Regular function constructor
function Person(name, age) {
  this.name = name;
  this.age = age;
}
const john = new Person('John', 30); // Person { name: 'John', age: 30 }

// Arrow function constructor
const Person = (name, age) => {
  this.name = name;
  this.age = age;
};
const john = new Person('John', 30); // TypeError: Person is not a constructor

Differences in hoisting behavior

Hoisting works differently for function declarations, function expressions, and arrow functions.

A function declaration is hoisted with its initial value set to the actual function. So, we can call a function declaration before it's created in the code, as shown in the following example:

greet();

function greet() {
  console.log('Good morning pal!'); // Good morning pal!
}

On the other hand, a function expression or an arrow function created with var is not hoisted. So, trying to call such a function before it's created results in a type error.

// Function expression
logger(); // TypeError: logger is not a function
var logger = function () {};

// Arrow function
greet(); // TypeError: greet is not a function
var greet = () => {};

However, a function expression or an arrow function created with let and const is technically hoisted and placed in the temporal dead zone with its initial value set to uninitialized. So, trying to call such a function before it's created results in a reference error.

// Function expression
logger(); // ReferenceError: Cannot access 'logger' before initialization
let logger = function () {};

// Arrow function
greet(); // ReferenceError: Cannot access 'greet' before initialization
const greet = () => {};

If you want to familiarize yourself with the concept of hoisting and temporal dead zone, I have an article on Understanding the variable environment and hoisting in JavaScript that provides further insights.

This concludes our exploration of the differences between a regular and an arrow function. Writing some code and observing these differences in action will solidify your understanding further.

Share this article with your friends

Copy URL

Elevate your JavaScript and freelance journey

Supercharge your JavaScript skills and freelance career. Subscribe now for expert tips and insights!