🏹 Regular Function vs Arrow Function

Introduction

Understand the key differences between creating JavaScript functions with the regular function keyword and ES6 arrow functions.

Before getting into the differences, here are the different ways JavaScript functions can be created.

Functions in JavaScript can be created with the function keyword as function declarations, unnamed functions assigned to variables as expressions, arrow functions using the latest ES6 syntax, and finally with the constructor Function keyword (which is not advised).

1// Function declaration
2function greetingTypeOne(name){
3 return `hello ${name}`;
4}
5
6// Function expresion
7const greetingTypeTwo = function(name){
8 return `hello ${name}`;
9}
10
11// IFFE - anonymous function expression
12(function () {
13 console.log("yo!");
14})();
15
16// Function expression with constructor
17const greetingTypeThree = Function('name')
18
19// ES6 arrow function
20const greetingTypeFour = () => `hello ${name}`;
Definition

A function declaration must be created with a function name. In a function expression, the name can be omitted to create anonymous functions. Function expressions are most commonly used in callbacks and to create an IIFE (Immediately Invoked Function Expression). IIFE statements run as soon as they are defined.

The focus here is the differences between the traditional function keyword and ES6 arrow functions.

This value

The value of this is dynamic. The value of this will change depending on how a function in JavaScript is invoked.

Definition

Invoking a function simply means to "call it". We invoke a function to execute the code inside the function body.

We can invoke traditional functions using the function keyword in four different ways. Here's the full picture before we break each one down.

Regular functions decide this at call time. Arrow functions inherit this from where they were defined.

Simple function invocation

When invoking a function declaration the value of this will be a global object or undefined if run in strict mode.

1function greeting() {
2 console.log(this);
3}
4
5/*
6 Simple Invocation
7 - logs the global window object in the context of the browser.
8*/
9greeting();
Definition

In JavaScript, there's always a global object defined in the global scope. The default values of the global object are different depending on the runtime environment of the script. In the context of the browser, we have a global Window object containing the DOM document. In Node.js, we refer to the global object simply as global. Any variables or functions created in a global scope exist on the global object of the runtime environment.

Method invocation

When invoking a function inside an object, the value of this is the owning object. In the example below, the console logged object will contain both functions as properties and the object prototype chain properties.

1const greetingsObject = {
2 greetingOne() {
3 console.log('yo');
4 console.log(this);
5 },
6 greetingTwo() {
7 console.log('hi');
8 console.log(this);
9 },
10};
11
12// Invoke the functions on object
13greetingsObject.greetingOne(); // logs greetingsObject
14greetingsObject.greetingTwo(); // also logs greetingsObject

Indirect invocation

When using .call() or .apply() function prototype properties, the value of this is the first argument.

1function greeting() {
2 console.log(this);
3}
4
5const greetingObject = { greetingOne: 'yo', greetingTwo: 'hi' };
6
7greeting.call(greetingObject); // { greetingOne: 'yo', greetingTwo: 'hi' };
8greeting.apply(greetingObject); //{ greetingOne: 'yo', greetingTwo: 'hi' };

Constructor invocation

When using a constructor to create a function with the new keyword, the value of this is the newly created function instance.

1function greeting() {
2 console.log(this);
3}
4
5new greeting(); // logs an instance of MyFunction

Arrow Function

When using arrow functions, the behaviour is very different. The value of this always inherits the value of the parent scope. Arrow functions do not bind their own scope.

In the code snippet below, we have created an object greetingConfig with two properties assigned to functions. The first property greetUsers is assigned to an unnamed regular function expression. The second property, goodBye is assigned to an arrow function expression.

The value of this in both arrow functions will always be the value of the parent scope (lexically scoped). In this example, the value of the parent scope is the greetingConfig object.

1const greetingConfig = {
2 greetUsers: function (users) {
3 console.log(this); // greetingConfig {greetUsers: ƒ, goodBye: ƒ}
4 const logGreetingCallback = (user) => {
5 console.log(`hi ${user}`);
6 console.log(this); // greetingConfig {greetUsers: ƒ, goodBye: ƒ}
7 };
8 users.forEach(logGreetingCallback);
9 },
10 goodBye: (user) => {
11 console.log(`cya ${user}`);
12 console.log(this); // greetingConfig {greetUsers: ƒ, goodBye: ƒ}
13 },
14};
15
16greetingConfig.greetUsers(['johnny', 'sarah', 'mark']);

If we remove the outer object wrapping the parent scope, the value of this for both our arrow function expression and function declaration will now be the global window scope.

1// function declaration
2function greetUsers(users) {
3 console.log(this); // Window
4
5 // ES6 arrow function expression
6 const logGreetingCallback = (user) => {
7 console.log(`hi ${user}`);
8 console.log(this); // Window
9 };
10 users.forEach(logGreetingCallback);
11};
12
13greetUsers(['johnny', 'sarah', 'mark']);

Classes

With a regular function:

1class User {
2 constructor(firstname, lastname, username) {
3 this.name = `${firstname} ${lastname}`;
4 this.username = username;
5 }
6
7 logName() {
8 console.log(this, this.name, this.username);
9 }
10}
11
12const user = new User('Johnny', 'McFadden', 'thejoecodes');
13
14user.logName(); // User {name: 'Johnny McFadden', username: 'thejoecodes'} 'Johnny McFadden' 'thejoecodes'

What if we try and assign the method to a variable and call it, surely everything will behave the same and the value of this will still be our class instance?

1class User {
2 constructor(firstname, lastname, username) {
3 this.name = `${firstname} ${lastname}`;
4 this.username = username;
5 }
6
7 logName() {
8 console.log(this, this.name, this.username);
9 }
10}
11
12const user = new User('Johnny', 'McFadden', 'thejoecodes');
13
14let newLogNameFunctionAssignment = user.logName;
15
16newLogNameFunctionAssignment(); // Uncaught TypeError: Cannot read properties of undefined (reading 'name')

The resulting console log from calling newLogNameFunctionAssignment(): Uncaught TypeError: Cannot read properties of undefined (reading 'name')

Hmm... and what about if we try the same thing but this time with an arrow function as our class method.

1class User {
2 constructor(firstname, lastname, username) {
3 this.name = `${firstname} ${lastname}`;
4 this.username = username;
5 }
6
7 logName = () => {
8 console.log(this, this.name, this.username);
9 }
10}
11
12const user = new User('Johnny', 'McFadden', 'thejoecodes');
13
14let newLogNameFunctionAssignment = user.logName;
15
16newLogNameFunctionAssignment(); // User {name: 'Johnny McFadden', username: 'thejoecodes', logName: ƒ} 'Johnny McFadden' 'thejoecodes'

đŸ¤¯ It works! We can call our class method just fine, but why??

With a traditional function, the property is defined on the constructor functions prototype, but not the instance of the class. Without the reassignment, we can access the function through prototypal inheritance. However, the moment we reassign our traditional class function we lose the context.

When we use an arrow function, the method property is not defined on the constructor functions prototype. Instead, it's defined on the instance of the class with the value of this lexically bound to class instance.

This is the exact reason React class components originally required you to bind methods in the constructor with this.handleClick = this.handleClick.bind(this). Arrow function class fields were the fix that made that boilerplate go away.

With the class fields feature, we can also use arrow functions for our class methods. The value of this inside our arrow function binds lexically to the class instance.

1class User {
2 constructor(firstname, lastname, username) {
3 this.name = `${firstname} ${lastname}`;
4 this.username = username;
5 }
6
7 logName = () => {
8 console.log(this, this.name, this.username);
9 }
10}
11
12const user = new User('Johnny', 'McFadden', 'thejoecodes');
13
14user.logName(); // User {name: 'Johnny McFadden', username: 'thejoecodes'} 'Johnny McFadden' 'thejoecodes'
Tip

If you ever need to reference the global object reliably across environments (browser, Node, workers), use globalThis. It always points to the global object regardless of context, so you don't need to check for window vs global vs self.

Function Returns

Arrow function

If we have a single expression, we can implicitly return the value by removing the curly braces.

1const greeting = (name) => `hi ${name}`;

If additional processing is needed inside the body of the function, curly braces are required.

1const greeting = (name) => {
2 if (typeof name !== 'string') return 'Name must be a string';
3 if (name.length >= 20) return 'Sorry... name must be under 20 characters';
4 const formatted = name.toUpperCase();
5 return `hi ${formatted}`;
6};

One gotcha: if you want to implicitly return an object literal, you need to wrap it in parentheses. Without the parentheses, JavaScript sees the opening curly brace as the start of the function body, not an object: const getUser = () => ({ name: 'Joe' }). You'll see this pattern a lot in React when using array methods like .map().

Regular function

With regular functions, we need to explicitly use the return keyword when returning a value.

1function greeting(name) {
2 return `hi ${name}`;
3}

If we don't add a return statement or an expression after it, undefined is returned implicitly. This is the same for arrow functions.

Function Arguments

Arrow function

No local arguments variable is available. Attempting to access arguments will result in an error: Uncaught ReferenceError: arguments is not defined.

Regular function

Any arguments passed to a traditional function can be accessed via a local arguments variable.

1function greeting() {
2 console.log(arguments);
3}
4
5greeting('hi', 'there', [1, 2, 3], { name: 'johnny' });
6
7/*
8Arguments(4) ['hi', 'there', Array(3), {â€Ļ}, callee: ƒ, Symbol(Symbol.iterator): ƒ]
90: "hi"
101: "there"
112: (3) [1, 2, 3]
123: {name: 'johnny'}
13*/

What if we used an arrow function inside a traditional, regular function and tried to access arguments?

1function greetingRegularFunction() {
2 const greetingArrowFunction = () => {
3 console.log(arguments);
4 };
5
6 greetingArrowFunction('👾', 'đŸ‘Ŋ');
7}
8
9greetingRegularFunction('🤖', '🎃');
10
11/*
12Arguments(2) ['🤖', '🎃', callee: ƒ, Symbol(Symbol.iterator): ƒ]
130: "🤖"
141: "🎃"
15*/

The arguments object is resolved lexically via lexical function scope. The arrow function we defined accesses the arguments from the outer regular function, despite having called greetingArrowFunction with 👾 and đŸ‘Ŋ.

Function Lexical Scope

Lexical scope is where an item got created.

Lexical scope allows function scope to access variables from the parent scope. A child function is said to be lexically bound by that of the parent function. Functions may access variables from their parent scope, all the way up to the global scope, this is called the scope chain.

Definition

JavaScript uses a scope chain to determine the accessibility of variables. When we use a variable, JavaScript searches for the declaration starting at the current scope and continues to any parent scopes until it reaches the global scope. Traversing all scopes is called the scope chain.

Note: functions and objects in JavaScript are also variables.

Tip

In practice, the arguments object is rarely used in modern JavaScript. Rest parameters (...args) work in both regular and arrow functions and give you a proper array instead of an array-like object, so you can use .map(), .filter() and friends directly.

Function Hoisting

Definition

Hoisting is JavaScript's default behaviour of moving declarations to the top. A traditional function can be used before its declaration. This is because regular functions defined using the function keyword are hoisted. ES6 arrow functions are not hoisted.

Regular function

1greeting()
2
3console.log("whats up?")
4
5function greeting() {
6 console.log("yo!")
7}
8
9// yo!
10// whats up?

The greeting() function is called before it is declared. With hoisting, we get no errors calling this function. Our custom function will get called before the console.log().

Arrow function

1greeting()
2
3console.log("whats up?")
4
5const greeting = () => {
6 console.log("yo!")
7}

Arrow functions are not hoisted; calling a function before it is declared will result in an error: Uncaught ReferenceError: greeting is not defined.

If you're somewhat familiar with the concept of hoisting in JavaScript, you might think of defining your arrow function with the var keyword and expect it to work (although is anyone actually using var in modern FE development??) , given var is hoisted. Let's see what would happen.

1greeting()
2
3console.log("whats up?")
4
5var greeting = () => {
6 console.log("yo!")
7}

We would actually get an error: Uncaught TypeError: greeting is not a function.

Remember, var variables are indeed hoisted, but they're hoisted and initialised with a value of undefined, not as a function type in the example above.

Learn more about hoisting and scope in: đŸ“Ļ var, let & const

Function Prototype

Definition

JavaScript is a prototype-based language, meaning object properties and methods can be shared through generalised objects that have the ability to be cloned and extended. This is known as prototypical inheritance and differs from class inheritance. Source: digitalocean - Understanding Prototypes and Inheritance in JavaScript

Arrow function

Do not have their own this binding or prototype and cannot be used as a constructor.

1const greeting = () => {
2 this.value = 'hi'
3}
4
5console.log(greeting.prototype)

Will result in undefined being logged out.

1const greeting = () => {
2 this.value = 'hi'
3}
4
5console.log(greeting.prototype)
6
7const instance = new greeting()
8
9console.log(instance.value)

Will result in: Uncaught TypeError: greeting is not a constructor

Regular function

Regular functions do have a prototype chain. We can use the new keyword to create a new instance of our function.

1function greeting() {
2 this.value = 'hi'
3}
4
5console.log(greeting.prototype) // {constructor: ƒ}
6
7const instance = new greeting()
8
9console.log(instance.value) // 'hi'

If we don't add a return statement or an expression after it, undefined is returned implicitly. This is the same for arrow functions.

Final Thoughts

Featurefunction() => {}
this bindingDynamic (depends on caller)Lexical (inherits from parent)
HoistingYes (declarations)No
arguments objectOwn argumentsInherits from parent
PrototypeHas .prototypeNo .prototype
Constructor (new)Can be used with newCannot be used with new
Implicit returnNo (must use return)Yes (single expression)
Syntaxfunction name() {}() => {}

If you want to work with constructors and classes, keep the "normal" behaviour of this value, care about the prototype chain of a function, or want your functions hoisted, then you should use a regular function, not an arrow function. Having said that, over the past few years I can't recall caring about those things too much and usually always reach for arrow functions given their implicit returns and nicer syntax.

Was this useful?

Thanks for reading! Time for one more?

export const resources = {
en, fr,
} as const
type Locale = keyof typeof resources

🌐 React i18n TS Support

17 Dec 2023 â€ĸ 📖 5 min read â€ĸ Updated 15 Mar 2026
⚡ Lightning read

Get full TypeScript intellisense for your i18n translations using "as const".

0
0
0
TYPESCRIPT REACT
if (true) {
var x = 1
}
console.log(x) // 1, var leaks out!

đŸ“Ļ var, let & const

23 Feb 2023 â€ĸ 📖 9 min read â€ĸ Updated 15 Mar 2026
🔰 Beginner friendly

Learn about scope chain, hoisting, lexical scoping and the key differences between var, let and const.

0
0
0
JAVASCRIPT