đš 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 declaration2function greetingTypeOne(name){3 return `hello ${name}`;4}56// Function expresion7const greetingTypeTwo = function(name){8 return `hello ${name}`;9}1011// IFFE - anonymous function expression12(function () {13 console.log("yo!");14})();1516// Function expression with constructor17const greetingTypeThree = Function('name')1819// ES6 arrow function20const greetingTypeFour = () => `hello ${name}`;
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.
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}45/*6 Simple Invocation7 - logs the global window object in the context of the browser.8*/9greeting();
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};1112// Invoke the functions on object13greetingsObject.greetingOne(); // logs greetingsObject14greetingsObject.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}45const greetingObject = { greetingOne: 'yo', greetingTwo: 'hi' };67greeting.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}45new 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};1516greetingConfig.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 declaration2function greetUsers(users) {3 console.log(this); // Window45 // ES6 arrow function expression6 const logGreetingCallback = (user) => {7 console.log(`hi ${user}`);8 console.log(this); // Window9 };10 users.forEach(logGreetingCallback);11};1213greetUsers(['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 }67 logName() {8 console.log(this, this.name, this.username);9 }10}1112const user = new User('Johnny', 'McFadden', 'thejoecodes');1314user.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 }67 logName() {8 console.log(this, this.name, this.username);9 }10}1112const user = new User('Johnny', 'McFadden', 'thejoecodes');1314let newLogNameFunctionAssignment = user.logName;1516newLogNameFunctionAssignment(); // 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 }67 logName = () => {8 console.log(this, this.name, this.username);9 }10}1112const user = new User('Johnny', 'McFadden', 'thejoecodes');1314let newLogNameFunctionAssignment = user.logName;1516newLogNameFunctionAssignment(); // 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 }67 logName = () => {8 console.log(this, this.name, this.username);9 }10}1112const user = new User('Johnny', 'McFadden', 'thejoecodes');1314user.logName(); // User {name: 'Johnny McFadden', username: 'thejoecodes'} 'Johnny McFadden' 'thejoecodes'
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}45greeting('hi', 'there', [1, 2, 3], { name: 'johnny' });67/*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 };56 greetingArrowFunction('đž', 'đŊ');7}89greetingRegularFunction('đ¤', 'đ');1011/*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.
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.
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
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()23console.log("whats up?")45function greeting() {6 console.log("yo!")7}89// 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()23console.log("whats up?")45const 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()23console.log("whats up?")45var 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
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}45console.log(greeting.prototype)
Will result in undefined being logged out.
1const greeting = () => {2 this.value = 'hi'3}45console.log(greeting.prototype)67const instance = new greeting()89console.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}45console.log(greeting.prototype) // {constructor: Æ}67const instance = new greeting()89console.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
| Feature | function | () => {} |
|---|---|---|
| this binding | Dynamic (depends on caller) | Lexical (inherits from parent) |
| Hoisting | Yes (declarations) | No |
| arguments object | Own arguments | Inherits from parent |
| Prototype | Has .prototype | No .prototype |
| Constructor (new) | Can be used with new | Cannot be used with new |
| Implicit return | No (must use return) | Yes (single expression) |
| Syntax | function 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 consttype Locale = keyof typeof resources
đ React i18n TS Support
17 Dec 2023 âĸ đ 5 min read âĸ Updated 15 Mar 2026Get full TypeScript intellisense for your i18n translations using "as const".
if (true) {var x = 1}console.log(x) // 1, var leaks out!
đĻ var, let & const
23 Feb 2023 âĸ đ 9 min read âĸ Updated 15 Mar 2026Learn about scope chain, hoisting, lexical scoping and the key differences between var, let and const.