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.
- Function Declaration
- Function Expression
- Arrow Function
- Object Method
- Constructor Function & Class
- Generator Function
- Async Function
- IIFE (Immediately Invoked Function Expression)
- Quick Comparison Table
- Key Differences
- When to Use What
- Interview Gotchas
1. Function Declaration
function greet(name) {
return `Hello, ${name}`;
}
greet("Amit"); // "Hello, Amit"
Enter fullscreen mode Exit fullscreen mode
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 preferclass/ 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");
}
Enter fullscreen mode Exit fullscreen mode
2. Function Expression
const greet = function (name) {
return `Hello, ${name}`;
};
Enter fullscreen mode Exit fullscreen mode
Named function expression (useful for stack traces / recursion):
const factorial = function fact(n) {
if (n <= 1) return 1;
return n * fact(n - 1);
};
Enter fullscreen mode Exit fullscreen mode
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 TDZReferenceError - With
var, calling before assignment is usually aTypeError(undefinedis not a function) - Same
this/argumentsbehavior 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");
};
Enter fullscreen mode Exit fullscreen mode
3. Arrow Function
const greet = (name) => `Hello, ${name}`;
const add = (a, b) => {
return a + b;
};
Enter fullscreen mode Exit fullscreen mode
Key traits
- Not hoisted
-
No own
this— uses lexicalthisfrom the surrounding scope - No
argumentsobject (use rest params:(...args) => {}) - Cannot be used as a constructor (
newthrows) - No
prototype - Perfect for callbacks,
map/filter/reduce, and preserving outerthis
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();
Enter fullscreen mode Exit fullscreen mode
Rest instead of arguments
const sum = (...nums) => nums.reduce((a, b) => a + b, 0);
sum(1, 2, 3); // 6
Enter fullscreen mode Exit fullscreen mode
Guess the output?const user = {
name: "Amit",
greet: () => this.name,
};
console.log(user.greet());
Enter fullscreen mode Exit fullscreen mode
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"
Enter fullscreen mode Exit fullscreen mode
Key traits
-
thisis usually the object that owns the method (when called asobj.method()) - Shorthand method syntax is preferred over
greet: function () {} - Avoid arrow functions as object methods if you need
thisto be the object
const user = {
name: "Amit",
greet: () => `Hello, ${this.name}`, // wrong — lexical this
};
user.greet(); // "Hello, undefined" (in modules / strict mode)
Enter fullscreen mode Exit fullscreen 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"
Enter fullscreen mode Exit fullscreen mode
Class (modern syntax)
class Person {
constructor(name) {
this.name = name;
}
greet() {
return `Hello, ${this.name}`;
}
}
const p = new Person("Amit");
Enter fullscreen mode Exit fullscreen mode
Key traits
- Meant to be called with
new - Creates instance objects
- Methods usually live on the prototype (shared across instances)
- Prefer
classin modern code; behavior is still prototype-based under the hood -
Classes are hoisted into the TDZ — you cannot use them before the
classline (ReferenceError)
const user = new User(); // ReferenceError
class User {}
Enter fullscreen mode Exit fullscreen mode
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;
}
}
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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]
Enter fullscreen mode Exit fullscreen mode
There are also async generators (
async function*) used withfor 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();
};
Enter fullscreen mode Exit fullscreen mode
Key traits
- Always returns a Promise
- Lets you write async code with
awaitin a sync-looking style - Thrown errors become rejected promises (use
try/catchor.catch()) -
awaiton a non-Promise still works — the value is wrapped
async function example() {
return 42;
}
example(); // Promise that resolves to 42
Enter fullscreen mode Exit fullscreen mode
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");
})();
Enter fullscreen mode Exit fullscreen mode
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");
})();
Enter fullscreen mode Exit fullscreen mode
Other (less common) IIFE forms(function () {
console.log("form 1");
})();
(function () {
console.log("form 2");
}());
!function () {
console.log("form 3");
}();
Enter fullscreen mode Exit fullscreen mode
Prefer the (function () { ... })() or (() => { ... })() style in real code.
Pass arguments
(function (name) {
console.log(`Hello, ${name}`);
})("Amit");
Enter fullscreen mode Exit fullscreen mode
Create private scope
(function () {
const secret = "hidden";
// secret is not accessible outside
})();
// console.log(secret); // ReferenceError
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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());
})();
Enter fullscreen mode Exit fullscreen mode
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
const result = (function () {
var x = 10;
return x;
})();
console.log(result);
// console.log(x);
Enter fullscreen mode Exit fullscreen mode
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 Syntaxfunction 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");
};
Enter fullscreen mode Exit fullscreen mode
Arrow Function vs Normal Function
“Normal function” = declaration or classic expression.
Feature Normal Function Arrow Function Ownthis
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();
Enter fullscreen mode Exit fullscreen mode
Constructor usage
function Person(name) {
this.name = name;
}
new Person("Amit"); // OK
const PersonArrow = (name) => {
this.name = name;
};
new PersonArrow("Amit"); // TypeError
Enter fullscreen mode Exit fullscreen mode
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’sthis
Method vs Arrow as Method
Point Methodgreet() {}
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
};
Enter fullscreen mode Exit fullscreen mode
Constructor Function vs Class
Point Constructor Function Class Syntaxfunction 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;
}
}
Enter fullscreen mode Exit fullscreen mode
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 scopeconst greet = function (name) {
console.log(name);
};
greet("Amit");
(function (name) {
console.log(name);
})("Amit");
Enter fullscreen mode Exit fullscreen mode
An IIFE can be an arrow:
(() => {
console.log("arrow IIFE");
})();
Enter fullscreen mode Exit fullscreen mode
Normal vs Async vs Generator (quick)
Point Normal Async Generator Keywordfunction
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;
}
Enter fullscreen mode Exit fullscreen mode
11. When to Use What
Need Prefer Named reusable helper Function declaration Pass / assign a function Function expression or arrow Callback / array method / keep outerthis
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 () {};
Enter fullscreen mode Exit fullscreen mode
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);
},
};
Enter fullscreen mode Exit fullscreen mode
In the browser, a regular
addEventListenercallback gets the element asthis; 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"
Enter fullscreen mode Exit fullscreen mode
4. Async always returns a Promise
async function getValue() {
return 10;
}
getValue().then(console.log); // 10
Enter fullscreen mode Exit fullscreen mode
5. Don’t use arrow functions as constructors
const Person = (name) => {
this.name = name;
};
new Person("Amit"); // TypeError
Enter fullscreen mode Exit fullscreen mode
6. IIFE creates private scope
(function () {
var x = 10;
})();
// console.log(x); // ReferenceError
Enter fullscreen mode Exit fullscreen mode
Practical Rule of Thumb
- Declaration → top-level named functions
-
Arrow → callbacks and short logic where lexical
thishelps - Method / class → object behavior and OOP
- Async → anything that waits on promises
-
Generator → when you need
yieldor 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.
답글 남기기