Arrow Functions vs Regular Functions in JavaScript – What's the Difference?
https://www.freecodecamp.org/news/the-difference-between-arrow-functions-and-normal-functions/
In JavaScript, there are two types of functions. You have normal functions and arrow functions. Let's explore the difference between them in this article.
Arrow functions was introduced in ES6. And it introduced a simple and shorter way to create functions.
Here's how to create a normal function, with arguments, which returns something:
function multiply(num1, num2) {
const result = num1 * num2
return result
}
If you want to transform this into an arrow function, here's what you'll have:
const multiply = (num1, num2) => {
const result = num1 * num2
return result
}
If the return
statement is the only statement in the function, you can even have a shorter function expression. For example:
const multiply = (num1, num2) => {
return num1 * num2
}
This function only contains the return
statement. With arrow functions, we can have something shorter like this:
const multiply = (num1, num2) => num1 * num2
We skip the curly braces and the return
keyword. Shorter; one-liner.
But the syntax of writing both types of functions is not the only difference. There's more, so let's look at them.
I have a video version of this topic which you can also check out :)
1. No arguments
object in arrow functions
A normal function has an arguments
object which you can access in the function:
function print() {
console.log(arguments)
}
The arguments
object is a local variable that contains the arguments passed to the function when called. Let's try it out:
print("hello", 400, false)
// {
// '0': 'hello',
// '1': 400,
// '2': false
// }
As you can see here, the three arguments passed when calling print()
are contained in the arguments
object which we log to the console. We can access the first argument with arguments[0]
, the second with arguments[1]
and the third with arguments[2]
But this object does not exist in arrow functions. Let's try it out. Say we have print
using an arrow function:
const print = () => {
console.log(arguments)
}
print("hello", 400, false)
// Uncaught ReferenceError: arguments is not defined
Now we have a reference error: arguments is not defined. That's because the arguments
variable does not exist in arrow functions.
2. Arrow functions do not create their own this
binding
In normal functions, a this
variable is created which references the objects that call them. For example:
const obj = {
name: "deeecode",
age: 200,
print: function () {
console.log(this)
},
}
obj.print()
// {
// name: 'deeecode',
// age: 200,
// print: [Function: print]
// }
As you can see here, the this
in the print
method points to obj
, which is the object that calls the method.
Here's another example:
const obj = {
name: "deeecode",
age: 200,
print: function () {
function print2() {
console.log(this)
}
print2()
},
}
obj.print()
// Window
Here, we have two functions. The first one is print
which is a method of the obj
object. The second is print2
which is a function declared inside print
. print2()
is also called directly.
In this case, print
is called by obj
(obj.print()
) but no object calls print2
(print2()
). So the this
in print2
would reference the window object by default.
Now let's see what happens with an arrow function.
const obj = {
name: "deeecode",
age: 200,
print: () => {
console.log(this)
},
}
obj.print()
// Window
By using an arrow function for print
, this function does not automatically create a this
variable. As a result, any reference to this
would point to what this
was before the function was created.
As you see in the result, this
was pointing to the Window
object before print
was created.
Let's see another example:
const obj = {
name: "deeecode",
age: 200,
print: function () {
const print2 = () => {
console.log(this)
}
print2()
},
}
obj.print()
// {
// name: 'deeecode',
// age: 200,
// print: [Function: print]
// }
Here, we have print
as a normal function which means a this
variable is automatically created in it. Then we have print2
which is an arrow function.
Because obj
is calling print
(as in obj.print()
), the this
in print
would point to obj
.
Since print2
is an arrow function, it doesn't create its own this
variable. Therefore, any reference to this
would point to what the value of this
was before the function was created. In this case where obj
calls print
, this
was pointing to obj
before print2
was created. As you can see in the results, by logging this
from print2
, obj
is the result.
You can learn more about this
in my article here
3. Arrow functions cannot be used as constructors
With normal functions, you can create constructors which serve as a special function for instantiating an object from a class.
Here is an example of an Animal
class which we instantiate two objects from:
class Animal {
constructor(name, numOfLegs) {
this.name = name
this.numOfLegs = numOfLegs
}
sayName() {
console.log(`My name is ${this.name}`)
}
}
const Dog = new Animal("Bingo", 4)
const Bird = new Animal("Steer", 2)
Dog.sayName()
// My name is Bingo
Bird.sayName()
// My name is Bird
Here, we have the Animal
constructor which can be instantiated with different parameters. In the constructor, two arguments are required: name
and noOfLegs
.
In the case of Dog
, we create a new instance of Animal
using "Bingo" as the name
and 4 as the noOfLegs
.
In the case of Bird
, we create a new instance of Animal
using "Steer" as the name
and 2 as the noOfLegs
.
By calling sayName
on Dog
and Bird
, you can see how the method works currently with each object. The this
variable points to the objects and the name
field is gotten from each of them.
The this
variable is very important for classes and constructors. In point 2, we saw that arrow functions cannot create their own this
. For this reason, arrow functions also cannot be used as constructors.
Let's attempt it and see what happens:
class Animal {
constructor = (name, numOfLegs) => {
this.name = name
this.numOfLegs = numOfLegs
}
sayName() {
console.log(`My name is ${this.name}`)
}
}
// Uncaught SyntaxError: Classes may not have a field named 'constructor'
Here, we have an arrow function used for the constructor
. But, we get a SyntaxError: Classes may not have a field named 'constructor'.
Because arrow functions involve expressions that are assigned variables, JavaScript now sees constructor
as a field. And in classes, you cannot have a field named constructor
as that is a reserved name.
But, we can use arrow functions for the methods in the class without getting errors. For example:
class Animal {
constructor(name, numOfLegs) {
this.name = name
this.numOfLegs = numOfLegs
}
sayName = () => {
console.log(`My name is ${this.name}`)
}
}
const Dog = new Animal("Bingo", 4)
Dog.sayName()
// My name is Bingo
Here, we have a normal function for the constructor
, and an arrow function for the sayName
method. sayName
is a field. And we do not get errors.
By calling sayName()
on Dog
, we still get "My name is Bingo". Though sayName
as an arrow function does not create its own this
, remember that any reference to this
would point to the value of it before the arrow function was created. In this case, the value of this
pointed to Dog
before sayName
was created.
4. Arrow functions cannot be declared
When it comes to functions, you need to understand function declaration and function expression.
Function declarations involve the function
keyword and a name for the function. For example:
function printHello() {
console.log("hello")
}
printHello
is a declared function. But, check out this example:
const printHello = function () {
console.log("hello")
}
In this case, printHello
is not a declared function. We have an anonymous function (not named) on the right side of the assignment operator. This function is a function expression, which is assigned to the printHello
variable.
Though the function
keyword is used, there is no name assigned, which makes it an expression and not a declaration. To prove that it is not a declaration, try the following:
function() {
console.log("hello")
}
Because this expression is not assigned to a variable, you get an error: SyntaxError: Function statements require a function name
Back to arrow functions. Normal functions can be declared when you use the function keyword and a name, but arrow functions cannot be declared. They can only be expressed because they are anonymous:
const printHello = () => {
console.log("hello")
}
As you see here, we have an anonymous function (starting from () => ...
) which is assigned to the printHello
variable. printHello
is not a declared function here. It is a variable that holds the evaluated value from the function expression.
5 Arrow functions cannot be accessed before initialization
Hoisting is a concept where a variable or function is lifted to the top of its global or local scope before the whole code is executed. This makes it possible for such a variable/function to be accessed before initialization. Here's a function example:
printName()
console.log("hello")
function printName() {
console.log("i am dillion")
}
// i am dillion
// hello
As you can see here, we called printName
before it was actually declared in the code. But we don't get any errors. printName()
is executed (logging "i am dillion" to the console) before console.log("hello")
.
What happens here is hoisting.
The printName
function is raised to the top of the global scope (the scope it is declared in) before the whole code is executed, thereby making it possible to execute the function earlier.
But not all kinds of functions can be accessed before initialization. All functions and variables in JavaScript are hoisted, but only declared functions can be accessed before initialization.
Here's an example with an arrow function:
printName()
console.log("hello")
const printName = () => {
console.log("i am dillion")
}
// ReferenceError: Cannot access 'printName' before initialization
Running this code, you get an error: ReferenceError: Cannot access 'printName' before initialization.
As we saw in point 4, printName
is not a declared function. It is a variable, declared with const
which is assigned a function expression. Variables declared with let
and const
are hoisted, but they cannot be accessed before the line they are initialized.
Let's say we use var
for our arrow function:
printName()
console.log("hello")
var printName = () => {
console.log("i am dillion")
}
// TypeError: printName is not a function
Here, we have declared the printName
variable with var
. The error we get now is TypeError: printName is not a function. The reason for this is that variables declared with var
are hoisted and accessible, but they have a default value of undefined
. So attempting to access printName
before the line it was initialized with the function expression is interpreted as undefined()
, and as you know, "undefined is not a function".
You can learn more about the hoisting differences between var, let, and const here
Wrap up
Although arrow functions allow you to write functions more concisely, they also have limitations. As we have seen in this article, some of the behaviors that occur in normal functions do not happen in arrow functions.
If you want to create constructors, retain the normal behavior of this
or have your functions hoisted, then arrow functions are not the right approach.