Episode - JavaScript Functions — Every Type Explained (Complete Guide)

Episode - JavaScript Functions — Every Type Explained (Complete Guide)

Hey everyone! Welcome back to Namaste JavaScript. Today we cover one of the most important topics — All Types of Functions in JavaScript!

Functions are the building blocks of JavaScript. But JavaScript has many different kinds of functions — and each behaves differently. By the end of this blog, you will know ALL of them inside out!

What we will cover:

  • Function Declaration (Normal Function)
  • Function Expression
  • Named Function Expression
  • Arrow Function
  • Immediately Invoked Function Expression (IIFE)
  • Generator Function
  • Async Function
  • Higher-Order Function
  • Pure Function
  • Recursive Function
  • Constructor Function
  • Method (Function inside Object)
  • Anonymous Function
  • Big Comparison Table

1. Function Declaration (Normal Function)

The classic, traditional way. Uses the function keyword followed by a name.

function greet(name) {
    return "Hello, " + name + "!";
}

console.log(greet("Shreyesh"));

OUTPUT:
Hello, Shreyesh!
Key Characteristics:
=====================

✅ HOISTED — can be called BEFORE its definition in code!
✅ Has its own 'this' binding
✅ Has 'arguments' object
✅ Can be used as a constructor (with 'new')
✅ Has 'prototype' property

// Hoisting demo
sayHi();  // Works! Even before the function definition!

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

OUTPUT:
Hi!

// The entire function is hoisted to the top of scope.
// This is UNIQUE to function declarations.
When to use:
=============
- Main utility functions
- Recursive functions (needs a name to call itself)
- When you want it available anywhere in the scope
- Constructor functions

2. Function Expression

A function assigned to a variable. The function itself has no name.

const greet = function(name) {
    return "Hello, " + name + "!";
};

console.log(greet("Shreyesh"));

OUTPUT:
Hello, Shreyesh!
Key Characteristics:
=====================

❌ NOT hoisted — the variable is hoisted, but value is undefined
✅ Has its own 'this' binding
✅ Has 'arguments' object
✅ Can be used as constructor (if assigned to var — careful!)
✅ Function is anonymous (no name property)

// Hoisting demo — the difference from declarations!
console.log(greet);  // undefined (var is hoisted, but not value)
greet();             // TypeError: greet is not a function

var greet = function() {
    console.log("Hi!");
};

// With const/let:
greet();  // ReferenceError: Cannot access 'greet' before initialization
const greet = function() { console.log("Hi!"); };
When to use:
=============
- Assign function conditionally
- Pass as callback argument
- Assign to object properties
- When you want to prevent hoisting intentionally

// Conditional function assignment
let transform;
if (userPrefersDarkMode) {
    transform = function(color) { return invert(color); };
} else {
    transform = function(color) { return color; };
}

3. Named Function Expression

A function expression where the function ALSO has a name. The name is only accessible inside the function itself.

const factorial = function fact(n) {
    if (n <= 1) return 1;
    return n * fact(n - 1);  // Uses 'fact' to call itself!
};

console.log(factorial(5));   // 120
console.log(fact);           // ReferenceError! 'fact' not in outer scope

OUTPUT:
120
ReferenceError: fact is not defined
Key Characteristics:
=====================

✅ Has a name — shows up in stack traces (easier to debug!)
✅ Name is ONLY visible inside the function — not outside
✅ Perfect for recursion in expressions
✅ Behaves like function expression in all other ways

// Why named expressions are better for debugging:
// Anonymous function in stack trace:
//   at anonymous (app.js:12)   ← not helpful!

// Named function in stack trace:
//   at calculateTotal (app.js:12)  ← much more helpful!

const calculateTotal = function calculateTotal(items) {
    return items.reduce((sum, item) => sum + item.price, 0);
};

4. Arrow Function (ES6)

Shorter syntax introduced in ES6. BUT — very different behavior from normal functions!

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

// Implicit return (single expression — no curly braces needed)
const add = (a, b) => a + b;

// Single parameter — no parentheses needed
const double = n => n * 2;

// No parameters — empty parentheses required
const greet = () => "Hello!";

// Returning object — wrap in ()
const getUser = (id) => ({ id, role: "admin" });

console.log(add(3, 4));    // 7
console.log(double(5));    // 10
console.log(greet());      // Hello!
Key Characteristics:
=====================

❌ NO own 'this' — inherits 'this' from lexical scope
❌ NO 'arguments' object — use rest params (...args)
❌ CANNOT be used as constructor (no 'new')
❌ NO 'prototype' property
✅ Implicit return for single expressions
✅ NOT hoisted (stored in variable)
✅ Perfect for callbacks and array methods

// 'this' behavior — most important difference!
const user = {
    name: "Shreyesh",
    normalGreet: function() {
        console.log("Normal:", this.name);  // ✅ "Shreyesh"
    },
    arrowGreet: () => {
        console.log("Arrow:", this.name);   // ❌ undefined
        // Arrow function inherits 'this' from module/global scope
        // NOT from the user object!
    }
};

user.normalGreet();  // Normal: Shreyesh
user.arrowGreet();   // Arrow: undefined
When to use:
=============
- Callbacks inside methods (forEach, map, filter)
- Promise chains (.then, .catch)
- Short utility functions
- React functional components and hooks
- When you want to preserve 'this' from outer scope

// Perfect use case
const user = {
    name: "Shreyesh",
    hobbies: ["coding", "reading"],
    showHobbies: function() {
        // Arrow function here inherits 'this' from showHobbies
        this.hobbies.forEach(hobby => {
            console.log(this.name + " likes " + hobby); // ✅ works!
        });
    }
};

5. IIFE — Immediately Invoked Function Expression

A function that is defined AND called immediately at the same time. Runs once and disappears!

// Basic IIFE syntax
(function() {
    console.log("I run immediately!");
})();

OUTPUT:
I run immediately!

// IIFE with parameters
(function(name) {
    console.log("Hello,", name);
})("Shreyesh");

OUTPUT:
Hello, Shreyesh

// Arrow function IIFE
(() => {
    console.log("Arrow IIFE!");
})();

// IIFE with return value
const result = (function(a, b) {
    return a + b;
})(10, 20);

console.log(result);  // 30
Key Characteristics:
=====================

✅ Creates its own private scope — variables don't pollute global
✅ Runs ONCE and immediately — no separate invocation needed
✅ Variables inside are NOT accessible from outside
✅ Used in the Module Pattern (before ES6 modules)

// Data privacy example
const counter = (function() {
    let count = 0;  // Private variable!

    return {
        increment: () => ++count,
        decrement: () => --count,
        getCount:  () => count
    };
})();

counter.increment();
counter.increment();
counter.increment();
console.log(counter.getCount());  // 3
console.log(count);               // ReferenceError! 'count' is private

When to use:
=============
- Initialize something once (DB setup, config)
- Create private scope to avoid global variable pollution
- Module pattern (older codebase)
- Wrapping code to avoid naming conflicts in scripts

6. Generator Function

A special function that can pause and resume its execution! Uses function* syntax and the yield keyword.

function* countUp() {
    console.log("Started!");
    yield 1;            // Pause here, return 1
    console.log("Resumed!");
    yield 2;            // Pause here, return 2
    console.log("Resumed again!");
    yield 3;            // Pause here, return 3
    console.log("Done!");
}

const gen = countUp();  // Does NOT run the function yet!

console.log(gen.next());  // { value: 1, done: false }
console.log(gen.next());  // { value: 2, done: false }
console.log(gen.next());  // { value: 3, done: false }
console.log(gen.next());  // { value: undefined, done: true }

OUTPUT:
Started!
{ value: 1, done: false }
Resumed!
{ value: 2, done: false }
Resumed again!
{ value: 3, done: false }
Done!
{ value: undefined, done: true }
Key Characteristics:
=====================

✅ Defined with function* (asterisk after 'function')
✅ Returns a Generator object (an iterator)
✅ yield keyword pauses execution and returns a value
✅ .next() resumes execution from where it paused
✅ .next() returns { value, done } object
✅ 'done: true' when function has no more yields
❌ Cannot be an arrow function (function* only)

// Infinite sequence generator!
function* infiniteIds() {
    let id = 1;
    while (true) {
        yield id++;  // Infinite loop is OK — yields pause it!
    }
}

const getId = infiniteIds();
console.log(getId.next().value);  // 1
console.log(getId.next().value);  // 2
console.log(getId.next().value);  // 3
// Never exhausts — only runs when you call .next()!

// Spread with generator
function* range(start, end) {
    for (let i = start; i <= end; i++) {
        yield i;
    }
}

console.log([...range(1, 5)]);  // [1, 2, 3, 4, 5]
Real-World Use Cases:
======================

1. Lazy evaluation — only compute values when needed
2. Infinite sequences — IDs, pagination
3. Custom iterators (for...of loops)
4. Data streaming — process large datasets in chunks
5. Redux-Saga — managing side effects in React apps

function* paginate(data, pageSize) {
    for (let i = 0; i < data.length; i += pageSize) {
        yield data.slice(i, i + pageSize);
    }
}

const pages = paginate([1,2,3,4,5,6,7,8,9,10], 3);
console.log(pages.next().value);  // [1, 2, 3]
console.log(pages.next().value);  // [4, 5, 6]
console.log(pages.next().value);  // [7, 8, 9]
console.log(pages.next().value);  // [10]

7. Async Function (ES2017)

A function that always returns a Promise and lets you use the await keyword inside it.

// async function declaration
async function fetchUser(id) {
    const response = await fetch(`/api/users/${id}`);
    const user = await response.json();
    return user;  // Automatically wrapped in Promise.resolve()!
}

// async function expression
const fetchUser = async function(id) {
    const user = await fetch(`/api/users/${id}`).then(r => r.json());
    return user;
};

// async arrow function
const fetchUser = async (id) => {
    const user = await fetch(`/api/users/${id}`).then(r => r.json());
    return user;
};

// Calling an async function — ALWAYS returns a Promise!
fetchUser(1).then(user => console.log(user));

// Or with await inside another async function
async function main() {
    const user = await fetchUser(1);
    console.log(user);
}
Key Characteristics:
=====================

✅ Always returns a Promise (even if you return a plain value)
✅ await pauses execution until Promise resolves
✅ Can be declaration, expression, arrow, or method
✅ Error handling with try/catch (clean, synchronous-looking)
✅ Runs on microtask queue

async function example() {
    return 42;  // Becomes Promise.resolve(42)
}

example().then(v => console.log(v));  // 42

// Error handling
async function riskyOperation() {
    try {
        const result = await mightFail();
        return result;
    } catch (err) {
        console.log("Caught:", err.message);
    }
}

// Parallel execution — DON'T await one by one if independent!
// ❌ Sequential (slow — waits for each)
const a = await fetchA();
const b = await fetchB();

// ✅ Parallel (fast — both start at the same time)
const [a, b] = await Promise.all([fetchA(), fetchB()]);

8. Higher-Order Function

A function that either takes another function as argument OR returns a function.

// Higher-Order Function — takes a function as argument
function applyOperation(a, b, operation) {
    return operation(a, b);
}

const result1 = applyOperation(10, 5, (a, b) => a + b);  // 15
const result2 = applyOperation(10, 5, (a, b) => a * b);  // 50

console.log(result1);  // 15
console.log(result2);  // 50

// Higher-Order Function — returns a function
function multiplier(factor) {
    return function(number) {
        return number * factor;
    };
}

const triple = multiplier(3);
const quadruple = multiplier(4);

console.log(triple(5));     // 15
console.log(quadruple(5));  // 20

// Array methods are Higher-Order Functions!
[1, 2, 3].map(n => n * 2);        // map is HOF
[1, 2, 3].filter(n => n > 1);     // filter is HOF
[1, 2, 3].reduce((acc, n) => acc + n, 0);  // reduce is HOF

9. Pure Function

A function that: (1) given the same inputs always returns the same output, and (2) has NO side effects.

// ✅ PURE FUNCTION — no side effects, predictable
function add(a, b) {
    return a + b;  // Always same result for same inputs!
}

console.log(add(2, 3));  // Always 5
console.log(add(2, 3));  // Always 5
console.log(add(2, 3));  // Always 5

// ❌ IMPURE — uses external variable (side cause)
let taxRate = 0.1;
function calculateTax(price) {
    return price * taxRate;  // Depends on external 'taxRate'!
}
// If taxRate changes, same input gives different output!

// ❌ IMPURE — modifies external state (side effect)
let total = 0;
function addToTotal(n) {
    total += n;  // Modifies external variable!
    return total;
}

// Pure version
function addToTotal(currentTotal, n) {
    return currentTotal + n;  // No mutation — returns new value
}
Why Pure Functions Matter:
===========================
✅ Predictable — easy to reason about
✅ Testable — just check input vs output
✅ Cacheable (memoizable) — same inputs = same output
✅ Foundation of functional programming
✅ React relies on pure rendering — components should be pure!

10. Recursive Function

A function that calls itself until a base condition is met.

// Classic recursion — factorial
function factorial(n) {
    if (n <= 1) return 1;     // Base case — STOPS recursion
    return n * factorial(n - 1);  // Recursive call
}

console.log(factorial(5));  // 5 * 4 * 3 * 2 * 1 = 120

// How it works:
// factorial(5)
//   = 5 * factorial(4)
//         = 4 * factorial(3)
//               = 3 * factorial(2)
//                     = 2 * factorial(1)
//                           = 1  (base case!)
//                     = 2 * 1 = 2
//               = 3 * 2 = 6
//         = 4 * 6 = 24
//   = 5 * 24 = 120

// Tree traversal — recursion shines here!
function traverseTree(node) {
    if (!node) return;            // Base case
    console.log(node.value);
    traverseTree(node.left);      // Recurse left
    traverseTree(node.right);     // Recurse right
}

// Flatten nested array recursively
function flatten(arr) {
    return arr.reduce((acc, item) =>
        acc.concat(Array.isArray(item) ? flatten(item) : item), []);
}

console.log(flatten([1, [2, [3, [4]]]]));  // [1, 2, 3, 4]

11. Constructor Function

A regular function used with the new keyword to create objects. The predecessor to ES6 classes.

function Person(name, age) {
    this.name = name;
    this.age = age;
    // 'this' refers to the new object being created!
}

// Adding methods via prototype (shared across all instances)
Person.prototype.greet = function() {
    return "Hi, I am " + this.name;
};

const p1 = new Person("Shreyesh", 25);
const p2 = new Person("Arjun", 30);

console.log(p1.greet());  // Hi, I am Shreyesh
console.log(p2.greet());  // Hi, I am Arjun
console.log(p1 instanceof Person);  // true

// What 'new' does internally:
// 1. Creates empty object: {}
// 2. Sets 'this' to that object
// 3. Runs the constructor function
// 4. Sets the prototype: obj.__proto__ = Person.prototype
// 5. Returns 'this' (the new object)

// ES6 class is just syntactic sugar over this!
class PersonClass {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    greet() {
        return "Hi, I am " + this.name;
    }
}
// Internally: same prototype-based mechanism!

12. Anonymous Function

A function with no name. Used inline as callbacks or assigned to variables.

// Anonymous function as callback
setTimeout(function() {
    console.log("Anonymous callback!");
}, 1000);

// Anonymous arrow function
[1, 2, 3].map(function(n) { return n * 2; });
[1, 2, 3].map(n => n * 2);  // Anonymous arrow function

// Anonymous function assigned to variable
// (it's now a function expression)
const greet = function(name) {
    return "Hello, " + name;
};

// Downside: anonymous functions show as "(anonymous)"
// in stack traces — harder to debug!
// Prefer named function expressions in production code.

Quick Recap — All Function Types

Type Syntax Hoisted Own 'this' arguments Constructor Best Use
Declaration function name() {} ✅ Yes ✅ Yes ✅ Yes ✅ Yes Main functions, recursion
Expression const f = function() {} ❌ No ✅ Yes ✅ Yes ✅ Yes Callbacks, conditional assign
Named Expression const f = function name() {} ❌ No ✅ Yes ✅ Yes ✅ Yes Self-referential, debuggable
Arrow const f = () => {} ❌ No ❌ Lexical ❌ No ❌ No Callbacks, array methods
IIFE (function() {})() N/A ✅ Yes ✅ Yes Rarely Private scope, run-once
Generator function* name() {} ✅ Yes ✅ Yes ✅ Yes ❌ No Lazy sequences, iterators
Async async function() {} Depends Depends Depends ❌ No Async/await operations
Constructor function Name() {} ✅ Yes ✅ Yes ✅ Yes ✅ Yes Creating objects (pre-class)

Key Points to Remember

  • Function declarations are hoisted — expressions and arrows are NOT
  • Arrow functions have no own 'this' — they inherit from lexical scope
  • Generator functions pause with yield and resume with .next()
  • async functions always return a Promise — even if you return a plain value
  • IIFE runs immediately — creates private scope, used for module pattern
  • Pure functions — same input, same output, no side effects
  • Higher-order functions take or return other functions (map, filter, reduce)
  • Named function expressions are better than anonymous for debugging
  • Constructor functions are used with 'new' — ES6 classes are sugar over this
  • Recursive functions call themselves — always need a base case to stop!

Keep coding, keep learning! See you in the next one!