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.