Dev.to WebDev 🛠 Dev 👁 0 📖 8 min read

Types of Functions in JavaScript — Differences Explained with Examples

Types of Functions in JavaScript — Differences Explained JavaScript has several ways to define functions. They look similar, but they differ in hoisting, this, arguments, and whether they can be used with new. If you’

Types of Functions in JavaScript — Differences Explained

JavaScript has several ways to define functions. They look similar, but they differ in hoisting, this, arguments, and whether they can be used with new.

If you’ve ever wondered why fn() works before its line for one function but throws for another — or why an arrow “method” prints undefined — this guide is for you.

Table of contents
  1. Function Declaration
  2. Function Expression
  3. Arrow Function
  4. Object Method
  5. Constructor Function & Class
  6. Generator Function
  7. Async Function
  8. IIFE (Immediately Invoked Function Expression)
  9. Quick Comparison Table
  10. Key Differences
  11. When to Use What
  12. Interview Gotchas

1. Function Declaration

function greet(name) {
  return `Hello, ${name}`;
}

greet("Amit"); // "Hello, Amit"

Key traits

  • Fully hoisted — you can call it before its definition
  • Has its own this (depends on how it’s called)
  • Has arguments
  • Can be used with new, but prefer class / dedicated constructors — don’t treat that as the normal pattern
  • Best for named, reusable top-level functions
sayHi(); // works — hoisted

function sayHi() {
  console.log("Hi");
}

2. Function Expression

const greet = function (name) {
  return `Hello, ${name}`;
};

Named function expression (useful for stack traces / recursion):

const factorial = function fact(n) {
  if (n <= 1) return 1;
  return n * fact(n - 1);
};

Key traits

  • The variable may be hoisted (var), but the function value is not available until the assignment runs
  • With const / let, accessing before the line is a TDZ ReferenceError
  • With var, calling before assignment is usually a TypeError (undefined is not a function)
  • Same this / arguments behavior as a declaration
  • Can be anonymous or named
  • Common when assigning functions to variables or passing as callbacks
// var → TypeError
sayHiVar(); // TypeError: sayHiVar is not a function
var sayHiVar = function () {
  console.log("Hi");
};

// const / let → ReferenceError (TDZ)
sayHiConst(); // ReferenceError: Cannot access 'sayHiConst' before initialization
const sayHiConst = function () {
  console.log("Hi");
};

3. Arrow Function

const greet = (name) => `Hello, ${name}`;

const add = (a, b) => {
  return a + b;
};

Key traits

  • Not hoisted
  • No own this — uses lexical this from the surrounding scope
  • No arguments object (use rest params: (...args) => {})
  • Cannot be used as a constructor (new throws)
  • No prototype
  • Perfect for callbacks, map / filter / reduce, and preserving outer this

Lexical this example

const user = {
  name: "Amit",
  regular() {
    setTimeout(function () {
      console.log(this.name); // undefined (or global) — lost this
    }, 0);
  },
  arrow() {
    setTimeout(() => {
      console.log(this.name); // "Amit" — lexical this
    }, 0);
  },
};

user.regular();
user.arrow();

Rest instead of arguments

const sum = (...nums) => nums.reduce((a, b) => a + b, 0);
sum(1, 2, 3); // 6

Guess the output?
const user = {
  name: "Amit",
  greet: () => this.name,
};
console.log(user.greet());

Answer: undefined in a typical module / strict runtime — the arrow does not take this from user.


4. Object Method

const user = {
  name: "Amit",
  greet() {
    return `Hello, ${this.name}`;
  },
};

user.greet(); // "Hello, Amit"

Key traits

  • this is usually the object that owns the method (when called as obj.method())
  • Shorthand method syntax is preferred over greet: function () {}
  • Avoid arrow functions as object methods if you need this to be the object
const user = {
  name: "Amit",
  greet: () => `Hello, ${this.name}`, // wrong — lexical this
};

user.greet(); // "Hello, undefined" (in modules / strict mode)

5. Constructor Function & Class

Constructor function

function Person(name) {
  this.name = name;
}

Person.prototype.greet = function () {
  return `Hello, ${this.name}`;
};

const p = new Person("Amit");
p.greet(); // "Hello, Amit"

Class (modern syntax)

class Person {
  constructor(name) {
    this.name = name;
  }

  greet() {
    return `Hello, ${this.name}`;
  }
}

const p = new Person("Amit");

Key traits

  • Meant to be called with new
  • Creates instance objects
  • Methods usually live on the prototype (shared across instances)
  • Prefer class in modern code; behavior is still prototype-based under the hood
  • Classes are hoisted into the TDZ — you cannot use them before the class line (ReferenceError)
const user = new User(); // ReferenceError
class User {}

React / class-fields note

class Button {
  label = "Save";

  // Per-instance arrow — lexical this (handy for callbacks)
  onPress = () => this.label;

  // Prototype method — this depends on call site
  handle() {
    return this.label;
  }
}

6. Generator Function

function* idGenerator() {
  let id = 1;
  while (true) {
    yield id++;
  }
}

const gen = idGenerator();
gen.next().value; // 1
gen.next().value; // 2
gen.next().value; // 3

Key traits

  • Defined with function*
  • Can pause and resume with yield
  • Returns an iterator
  • Useful for lazy sequences, custom iteration, infinite lists

Also works as a generator method:

const obj = {
  *range(n) {
    for (let i = 1; i <= n; i++) yield i;
  },
};

[...obj.range(3)]; // [1, 2, 3]

There are also async generators (async function*) used with for await...of — same pause/resume idea for async streams.

7. Async Function

async function loadUser() {
  const res = await fetch("/api/user");
  return res.json();
}

// async arrow
const loadUserArrow = async () => {
  const res = await fetch("/api/user");
  return res.json();
};

Key traits

  • Always returns a Promise
  • Lets you write async code with await in a sync-looking style
  • Thrown errors become rejected promises (use try/catch or .catch())
  • await on a non-Promise still works — the value is wrapped
async function example() {
  return 42;
}

example(); // Promise that resolves to 42

8. IIFE (Immediately Invoked Function Expression)

Also called a self-calling or self-invoking function.

An IIFE is defined and executed in the same step — you don’t call it later by name.

(function () {
  console.log("I run immediately");
})();

// Arrow version
(() => {
  console.log("I also run immediately");
})();

Why wrap in parentheses?

Without wrapping, JS treats function as a declaration, and declarations can’t be invoked that way as a statement.

// Valid — force an expression, then invoke
(function () {
  console.log("works");
})();

Other (less common) IIFE forms
(function () {
  console.log("form 1");
})();

(function () {
  console.log("form 2");
}());

!function () {
  console.log("form 3");
}();

Prefer the (function () { ... })() or (() => { ... })() style in real code.


Pass arguments

(function (name) {
  console.log(`Hello, ${name}`);
})("Amit");

Create private scope

(function () {
  const secret = "hidden";
  // secret is not accessible outside
})();

// console.log(secret); // ReferenceError

Return a value / module-like API

const counter = (function () {
  let count = 0;

  return {
    increment() {
      count++;
      return count;
    },
    getCount() {
      return count;
    },
  };
})();

counter.increment(); // 1
counter.getCount(); // 1
// count itself is private

Async IIFE

Useful for top-level async setup when you can’t use top-level await:

(async function () {
  const res = await fetch("/api/user");
  console.log(await res.json());
})();

Key traits

  • Defined and executed immediately
  • Creates a private scope
  • Can take parameters and return values
  • Common before ES modules; still used in bundles, polyfills, one-off setup, and interviews

Guess the output?
const result = (function () {
  var x = 10;
  return x;
})();

console.log(result);
// console.log(x);

Answer: result is 10. Logging x throws ReferenceError — x stayed private inside the IIFE.


9. Quick Comparison Table

Feature Declaration Expression Arrow Method Constructor / Class Generator Async
Hoisting Yes (full) No (assignment); var/const differ No No Function ctor: yes · Class: TDZ Like its form Like its form
Own this Yes Yes No (lexical) Yes (usually) Yes (instance) Yes Yes (unless arrow)
arguments Yes Yes No Yes Yes Yes Yes (unless arrow)
Can use new Possible* Possible* No Not intended Yes Avoid No
Returns Any Any Any Any Instance Iterator Promise
Best for Named utils Assigned fns Callbacks Object behavior OOP / instances Lazy iteration Async / await

* Technically allowed for classic functions, but prefer class for constructors.

10. Key Differences

Declaration vs Expression

Point Function Declaration Function Expression
Syntax function fn() {} const fn = function () {}
Before definition Callable var → TypeError · const/let → ReferenceError
Name required Yes Optional
Use case Top-level helpers Assign to vars, pass as values
hello(); // works
function hello() {
  console.log("declaration");
}

bye(); // ReferenceError with const
const bye = function () {
  console.log("expression");
};

Arrow Function vs Normal Function

“Normal function” = declaration or classic expression.

Feature Normal Function Arrow Function
Own this Yes — depends on how it’s called No — lexical
arguments Yes No — use ...args
Can use new Yes No (TypeError)
prototype Yes No
Implicit return No Yes — () => value
Best for Methods, constructors, own this Callbacks, keep outer this

this binding

const user = {
  name: "Amit",
  normal: function () {
    console.log(this.name); // "Amit"
  },
  arrow: () => {
    console.log(this.name); // undefined (not the object)
  },
};

user.normal();
user.arrow();

Constructor usage

function Person(name) {
  this.name = name;
}
new Person("Amit"); // OK

const PersonArrow = (name) => {
  this.name = name;
};
new PersonArrow("Amit"); // TypeError

When to use which

  • Normal function → object methods, constructors, handlers where you need call-site this
  • Arrow function → callbacks (map, filter, setTimeout) where you want the parent’s this

Method vs Arrow as Method

Point Method greet() {} Arrow property greet: () => {}
this Owning object (usual call) Outer / lexical this
Recommended for objects? Yes Usually no
const user = {
  name: "Amit",
  method() {
    return this.name; // "Amit"
  },
  arrow: () => this.name, // wrong for object method
};

Constructor Function vs Class

Point Constructor Function Class
Syntax function Person() {} class Person {}
Style Older / prototype style Modern syntactic sugar
Before definition Function is hoisted TDZ — ReferenceError
Internals Prototype model Same prototype model
function PersonFn(name) {
  this.name = name;
}

class PersonClass {
  constructor(name) {
    this.name = name;
  }
}

Function Expression vs IIFE

Point Function Expression IIFE
When it runs When you call it later Immediately
Stored in variable? Usually yes Often not needed
Purpose Reuse later One-time run + private scope
const greet = function (name) {
  console.log(name);
};
greet("Amit");

(function (name) {
  console.log(name);
})("Amit");

An IIFE can be an arrow:

(() => {
  console.log("arrow IIFE");
})();

Normal vs Async vs Generator (quick)

Point Normal Async Generator
Keyword function async function function*
Returns Any value Always a Promise An iterator
Special — await yield / .next()
function syncFn() {
  return 10; // number
}

async function asyncFn() {
  return 10; // Promise → 10
}

function* gen() {
  yield 1;
  yield 2;
}

11. When to Use What

Need Prefer
Named reusable helper Function declaration
Pass / assign a function Function expression or arrow
Callback / array method / keep outer this Arrow function
Behavior on an object Object method / class method
Create many similar objects class
Pause / resume / lazy values Generator
Fetch / timers / promises Async function
Run once + private variables IIFE (or an ES module)

12. Interview Gotchas

1. Hoisting: declaration vs expression

fnDecl(); // works

function fnDecl() {}

fnVar(); // TypeError
var fnVar = function () {};

fnConst(); // ReferenceError (TDZ)
const fnConst = function () {};

2. Arrow functions don’t bind their own this

const button = {
  id: "save",
  bindRegular() {
    setTimeout(function () {
      console.log(this.id); // undefined — lost this
    }, 0);
  },
  bindArrow() {
    setTimeout(() => {
      console.log(this.id); // "save" — lexical this
    }, 0);
  },
};

In the browser, a regular addEventListener callback gets the element as this; an arrow does not.

3. Losing this when extracting a method

const user = {
  name: "Amit",
  greet() {
    return this.name;
  },
};

const fn = user.greet;
fn(); // undefined (this is lost)

fn.call(user); // "Amit"

4. Async always returns a Promise

async function getValue() {
  return 10;
}

getValue().then(console.log); // 10

5. Don’t use arrow functions as constructors

const Person = (name) => {
  this.name = name;
};

new Person("Amit"); // TypeError

6. IIFE creates private scope

(function () {
  var x = 10;
})();

// console.log(x); // ReferenceError

Practical Rule of Thumb

  • Declaration → top-level named functions
  • Arrow → callbacks and short logic where lexical this helps
  • Method / class → object behavior and OOP
  • Async → anything that waits on promises
  • Generator → when you need yield or custom iterators
  • IIFE → run once immediately with private scope (or just use a module)

Master these differences and you can explain not just how to write a function, but why one form is better than another — in interviews and in real projects.

If this helped, leave a ❤️ and comment which function type still confuses you the most.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.