Regular Functions
Regular functions, also known as traditional functions, are the most commonly used way to define functions in JavaScript using the function
keyword.
They are easy to understand and support features like hoisting, the arguments
object, and their own this
binding.
Syntax of Regular Functions
function greet(name) {
console.log("Hello, " + name + "!");
}
Here,
function
is the keyword that tells JavaScript you’re making a function.greet
is the name of the function.name
is a parameter, a placeholder for any value you pass.- Inside the
{}
, the function prints a message to the console.
How to call it:
greet("Shefali"); // Output: Hello, Shefali!
Here,
"Shefali"
is the argument, the actual value passed to the function.- This will print
Hello, Shefali!
to the console.
Example:
function add(a, b) {
return a + b;
}
console.log(add(2, 3)); // Output: 5
Here, the regular function add
takes two parameters a
, b
, and returns their sum.
Features of Regular Functions
-
Hoisted: You can use the function before it's defined in your code.
-
Own
this
value: Thethis
keyword inside a regular function depends on how the function is called. -
arguments object: Available inside regular functions, which lets you access all passed arguments even if they’re not defined as parameters.
Example: Using the
arguments
Objectfunction showAll() { for (let i = 0; i < arguments.length; i++) { console.log(arguments[i]); } } showAll("apple", "banana", "cherry"); // Output: // apple // banana // cherry
Here,
function showAll()
is defined with no parameters.- But when you call
showAll("apple", "banana", "cherry")
, it still receives 3 values. - JavaScript stores those values in a special built-in object called
arguments
. - The loop
for (let i = 0; i < arguments.length; i++)
goes through each value. console.log(arguments[i])
prints each one.
Use Cases of Regular Functions
Use regular functions when:
- You need reusable logic in different parts of your code.
- You want to access all arguments using the
arguments
object. - You need to use
this
inside the function.
Advantages of Regular Functions
- Simple and readable syntax
- Can be hoisted (you can call before defining)
- Supports
arguments
and has its ownthis
Limitations of Regular Functions
- For short one-liner tasks, regular functions feel long (arrow functions are more concise).
this
binding can sometimes be confusing, especially inside callbacks or nested functions.
Anonymous Functions
An anonymous function is a function that does not have any name.
These functions are often defined inline and can be assigned to a variable or passed as an argument to another function.
Anonymous functions are useful when you need a quick function definition, and there’s no intention of reusing it elsewhere in the code.
Syntax of Anonymous Functions
// Anonymous function assigned to a variable
const myFunction = function () {
console.log("This is an example of an anonymous function.");
};
// Invoking the anonymous function
myFunction();
Here,
function () { ... }
is an anonymous function.- It is saved in the variable
myFunction
. - You can call it using
myFunction();
.
Use Cases of Anonymous Functions
-
Callback Functions:
// Using an anonymous function as a callback setTimeout(function () { console.log("This is invoked after 2 seconds"); }, 2000);
Here,
- You pass an anonymous function to
setTimeout
. - That function runs after 2 seconds.
- It’s not reused, so naming it isn’t necessary.
- You pass an anonymous function to
-
Event Handlers:
When you want to attach an event handler dynamically, you can use an anonymous function.
// Anonymous function as an event handler document.querySelector("Button").addEventListener("click", function () { console.log("Button clicked!"); });
Here,
- This adds a click listener to a button.
- The function runs only when the button is clicked.
- No need to reuse it, perfect for an anonymous function.
-
Immediately Invoked Function Expressions (IIFE):
If you want to create a function and execute it immediately after the declaration, then you can use an anonymous function like in the below example.
// IIFE using an anonymous function (function () { console.log("This is an example of IIFE."); })();
Here,
- The function is created and called immediately.
- It runs once and is gone.
- Often used to avoid polluting global variables.
Polluting global variables means accidentally adding too many variables or functions to the global scope, which can cause conflicts or bugs.
-
Array Methods:
You can use the anonymous functions with array methods like map, filter, and reduce.
// Using an anonymous function with array map method const numbers = [1, 2, 3]; const doubledNumbers = numbers.map(function (num) { return num * 2; }); console.log(doubledNumbers); // [2, 4, 6]
Advantages of Anonymous Functions
- Anonymous functions are often more concise in scenarios where a function is simple and won’t be reused.
- When anonymous functions are used in IIFE patterns, they reduce the risk of variable conflicts in the global scope.
Limitations of Anonymous Functions
- Anonymous functions can decrease code readability.
- With anonymous functions, debugging can be more challenging, as they lack meaningful names.
- Anonymous functions have their own
this
binding, which may lead to unexpected behavior in certain contexts.
Callback Functions
A callback function is a function passed as an argument to another function. It gets called later inside the outer function.
Callbacks are often used when a task is asynchronous (like fetching data or waiting for a timeout) or when you want to run some code after something else happens.
Any function type (regular, arrow, anonymous) can be used as a callback.
Syntax of Callback Functions
function greet(name, callback) {
console.log("Hello, " + name);
callback();
}
You call it by passing another function as the second argument.
greet("Shefali", function () {
console.log("Welcome!");
});
Output:
Hello, Shefali
Welcome!
Here,
greet
is the main function.name
is the first parameter.callback
is the second parameter, a function passed from outside.- Inside
greet
, it says hello and then calls the callback function. - The callback function prints "Welcome!"
Example: setTimeout (Asynchronous Callback)
setTimeout(function () {
console.log("This runs after 2 seconds.");
}, 2000);
Here,
setTimeout
is a built-in function that waits.- It takes a callback (anonymous function) and a delay (2000 ms = 2 seconds).
- After 2 seconds, the callback runs.
Use Cases of Callback Functions
-
Event Handling:
document.querySelector("Button").addEventListener("click", function () { console.log("Button clicked!"); });
Here, the function is a callback that runs when the button is clicked.
-
Array Methods:
const numbers = [1, 2, 3]; const doubled = numbers.map(function (num) { return num * 2; }); console.log(doubled); // [2, 4, 6]
Here,
.map()
is a method that takes a callback.- It runs the callback on each element of the array.
- The function returns a new array with the updated values.
Advantages of Callback Functions
- Make your code modular and reusable.
- Allow you to control timing (when to run something).
- Essential for handling asynchronous code.
Limitations of Callback Functions
- Can lead to callback hell (many nested callbacks).
- Harder to manage in complex logic.
- Promises and async/await are now preferred for complex async logic.
Nested Functions
A nested function is simply a function defined inside another function.
The inner (nested) function can access variables from the outer function’s scope. This is useful for breaking complex logic into smaller parts or creating closures.
Syntax of Nested Functions
function outerFunction() {
function innerFunction() {
console.log("Hello from inner function");
}
innerFunction();
}
Here,
outerFunction
is the main function.- Inside it, you create another function called
innerFunction
. innerFunction
just prints a message.- To run the inner function, you call it from inside the outer function.
Call it like this:
outerFunction(); // Output: Hello from inner function
Example:
function greetUser(name) {
function formatName(userName) {
return userName.toUpperCase();
}
console.log("Hello, " + formatName(name));
}
greetUser("shefali"); // Output: Hello, SHEFALI
Here,
greetUser
is the main (outer) function.- It takes a name, in this case, "shefali".
- Inside
greetUser
, you define another function calledformatName
. formatName
takes the name and converts it to uppercase.- Then you print the greeting using the result of
formatName
. - This keeps the formatting logic inside
greetUser
, so your code stays clean and organized.
Use Cases of Nested Functions
- Helper functions: For small tasks inside a larger function.
- Closures: The inner function remembers variables from the outer function.
- Encapsulation: To hide logic from the outside and avoid polluting global variables.
Advantages of Nested Functions
- Can access variables from the outer functions.
- Keep your code clean and modular.
- Avoid naming conflicts in global scope.
Limitations of Nested Functions
- You can’t use inner functions outside the outer functions.
- If you nest too much, your code might become hard to read.
Arrow Functions
Arrow functions were introduced in ECMAScript 6 (ES6) and offer a shorter way to write functions.
They are particularly useful for short and one-liner functions.
Syntax of Arrow Functions
// Basic arrow function
const add = (a, b) => {
return a + b;
};
If you have a single expression in the function body, then you can omit the curly braces {}
and the return
keyword as shown in the example below.
const add = (a, b) => a + b;
If you have a single parameter in the function, then you can omit the parentheses around the parameter as shown in the example below.
const square = x => x * x;
For functions, in which you have no parameters, you still need to include empty parentheses as shown in the example below.
const randomNumber = () => Math.random();
Lexical this in Arrow Functions
One of the most significant features of arrow functions is that they do not have their own this
binding. Instead, they inherit this
from their parent scope. This behavior can be especially helpful when working with event handlers or callbacks within methods.
const obj = {
name: "John",
greet: () => {
console.log(`Hello, ${this.name}`); // Lexical 'this' refers to the global scope, not obj
},
};
obj.greet(); // Output: Hello, undefined
Here, the arrow function greet
does not have its own this
binding, so it uses the this
value from its parent scope, which is the global scope. Since name
is not defined globally, it outputs undefined
.
Note: Don’t use arrow functions if you need the function to use its own this.
Use Cases of Arrow Functions
-
Array Manipulation:
const numbers = [1, 2, 3, 4, 5]; // Using regular function const squared = numbers.map(function (num) { return num * num; }); // Using arrow function const squaredArrow = numbers.map((num) => num * num);
-
Callback Functions:
const numbers = [1, 2, 3, 4, 5, 6]; //Using regular function const evenNumbers = numbers.filter(function (num) { return num % 2 === 0; }); //Using arrow function const evenNumbersArrow = numbers.filter((num) => num % 2 === 0);
-
Asynchronous Operations:
const fetchFromAPI = () => { return new Promise((resolve, reject) => { fetch("https://api.example.com/data") .then((response) => response.json()) .then((data) => resolve(data)) .catch((error) => reject(error)); }); }; fetchFromAPI().then((data) => console.log(data));
Advantages of Arrow Functions
- Arrow functions have a more concise syntax, especially for short, one-liner functions.
- Arrow functions do not have their own
this
binding. Instead, they inheritthis
from their parent scope. - Arrow functions often result in cleaner and more readable code, especially when used with array methods like map, filter, and reduce.
Limitations of Arrow Functions
- Arrow functions do not have their own arguments object.
- The concise syntax of arrow functions may lead to less descriptive function names, which can sometimes affect code readability.
- Arrow functions cannot be used as constructors, attempting to use
new
with an arrow function will result in an error.
Immediately Invoked Function Expressions (IIFE)
An Immediately Invoked Function Expression (IIFE) is a function that runs immediately after it is defined.
It is useful to run some code right away without leaving extra variables or functions in the global space.
Syntax of IIFE
(function () {
console.log("This runs immediately!");
})();
Here,
- The function is wrapped in parentheses to convert it into an expression.
- The last
()
invokes it immediately. - You don’t have to call it separately, it runs once on its own.
Example:
(function () {
const message = "Hello from IIFE!";
console.log(message);
})(); // Output: Hello from IIFE!
Here,
- The function is created and run right away.
- Inside it, a variable
message
is defined. console.log(message)
prints the message.- After this runs, the variable
message
does not exist outside the function. - This prevents cluttering the global space with temporary variables.
You can also write IIFE with arrow functions:
(() => {
console.log("Arrow IIFE!");
})();
Use Cases of IIFE
-
Encapsulation (Private Scope):
(function () { let counter = 0; console.log("Counter inside IIFE:", counter); })();
The variable
counter
is not accessible outside the IIFE, which avoids polluting the global scope. -
Running Setup Code Once:
(function () { console.log("Setup is done."); })();
You can use IIFEs to run configuration or startup code once when your script loads.
Advantages of IIFE
- Keeps variables and code private inside the function.
- Avoids polluting or overwriting global variables.
- Runs code immediately without extra calls.
Limitations of IIFE
- The syntax can be slightly harder to read for beginners due to the extra parentheses.
- Runs only once and cannot be reused or called again.
Higher-Order Functions
A Higher-Order Function is a function that does one of the following:
- Takes another function as an argument
- Returns another function
This concept is common in JavaScript and is widely used in array methods like map
, filter
, and reduce
.
Syntax of Higher-Order Functions
function doSomething(callback) {
callback();
}
Here, doSomething
is a higher-order function because it takes another function (callback
) as an argument.
Example: Passing a Function as an Argument
function greet() {
console.log("Hello!");
}
function processUserInput(callback) {
callback();
}
processUserInput(greet);
Here,
greet()
just prints a message.processUserInput()
takes a function as input and calls it.- When
processUserInput(greet)
runs, it callsgreet()
inside.
Example: Returning a Function
function multiplier(factor) {
return function (number) {
return number * factor;
};
}
const double = multiplier(2);
console.log(double(5)); // Output: 10
Here,
multiplier
is a function that takes one input,factor
.- Inside it, you're returning a new function that takes another input,
number
. - This inner function multiplies
number * factor
. - The outer function remembers the value of factor due to closure.
- You call
multiplier(2)
, this returns a function that will multiply anything by 2. - So now
double
is a function that doubles any number. double(5)
means5 * 2
, which gives10
.
Use Cases of Higher-Order Functions
- Array methods: Like
map
,filter
, andreduce
let you pass functions to work with each item in the array. - Event handling: You can pass a function to run when something happens (like a button click).
- Functional programming: Helps write clean, reusable, and modular code using functions.
- Dynamic logic: You can create flexible functions that return or accept other functions to control behavior.
Advantages of Higher-Order Functions
- Improve code reusability.
- Help create more abstract and powerful logic.
- Reduce repetition.
Limitations of Higher-Order Functions
- It might feel confusing at first for beginners.
- Can make debugging slightly harder if overused.
Recursion and Recursive Functions
Recursion is when a function calls itself to solve smaller parts of the same problem.
A recursive function is a function that keeps calling itself until it reaches a stopping point.
Syntax of Recursive Functions
function recurse() {
// some condition to stop recursion
recurse(); // function calls itself
}
Note: Every recursive function must have a base case, a condition that stops the recursion. Otherwise, it will keep calling itself like this👇
Example: Counting Down
function countDown(n) {
if (n <= 0) {
console.log("Done!");
return;
}
console.log(n);
countDown(n - 1);
}
countDown(3);
// Output:
// 3
// 2
// 1
// Done!
Here,
- If
n
is0
or less, it prints "Done!" and stops. - Otherwise, it prints
n
and calls itself withn - 1
.
Example: Factorial with Recursion
function factorial(n) {
if (n === 0) {
return 1;
}
return n * factorial(n - 1);
}
console.log(factorial(5)); // Output: 120
Here,
factorial(5)
returns5 * factorial(4)
- Which returns
5 * 4 * factorial(3)
and so on... - Until it reaches
factorial(0)
which returns1
- Final result is
5 * 4 * 3 * 2 * 1 = 120
Use Cases of Recursive Functions
- Solving problems that have smaller sub-problems (divide and conquer).
- Working with tree-like structures (DOM, file systems, etc.).
- Mathematical operations like factorial, Fibonacci, etc.
Advantages of Recursive Functions
- Code is clean and matches the natural logic of the problem.
- Useful for problems where iteration is difficult or awkward.
Limitations of Recursive Functions
- Can cause stack overflow if not written carefully.
- Usually slower than loops due to function call overhead.
- Must always include a base case to avoid infinite recursion.
Generator Functions
A generator function is a special type of function that can pause and resume its execution using the yield
keyword.
Instead of returning all values at once, it produces a sequence of values over time, one at a time.
Syntax of Generator Functions
function* generatorName() {
yield value1;
yield value2;
}
- Notice the
*
after thefunction
keyword, this marks it as a generator function. yield
is used to pause and send a value back each time the function runs.
Example:
function* countUp() {
yield 1;
yield 2;
yield 3;
}
const counter = countUp();
console.log(counter.next().value); // Output: 1
console.log(counter.next().value); // Output: 2
console.log(counter.next().value); // Output: 3
Here,
countUp
is a generator function that yields three values: 1, 2, and 3.- Calling
countUp()
does not run the function right away, it returns a generator objectcounter
. - Calling
counter.next()
:- Runs the function until the next yield.
- Returns an object
{ value: X, done: false }
where value is the yielded value.
counter.next().value
gets the current yielded value.- Each call to
.next()
resumes where it last stopped. - After the last yield,
.next()
would return{ value: undefined, done: true }
indicating completion.
Use Cases of Generator Functions
-
Custom Iteration Logic:
Generators are useful when you want to:
- Create custom iterators
- Generate sequences step-by-step
- Handle async flows (when used with async/await or Promises)
Example: Iterating with
for...of
function* names() { yield "Alice"; yield "Bob"; yield "Charlie"; } for (let name of names()) { console.log(name); }
Here,
-
function* names() { ... }
- This defines a generator function named
names
. - The
*
after function tells JavaScript that it’s a generator.
- This defines a generator function named
-
yield "Alice";, yield "Bob";, yield "Charlie";
- Each
yield
gives out one value when the generator runs. - But instead of giving all values at once, it pauses after each yield.
- Each
-
names()
- This doesn't run the code immediately.
- It returns a generator object that can be used to get values one by one.
-
for (let name of names()) { ... }
-
The
for...of
loop that works well with generators. It automatically:- Starts the generator,
- Gets the first yield value ("Alice"),
- Then resumes to get "Bob",
- Then resumes again to get "Charlie",
- Finally, it stops when the generator is done.
-
The
-
console.log(name);
- This line runs inside the loop each time a value is yielded.
- It prints each name one by one.
Output:
Alice Bob Charlie
Advantages of Generator Functions
- Pause and resume execution whenever needed.
- Use less memory by generating values on demand.
- Can simplify complex iteration or async logic.
Limitations of Generator Functions
- Slightly more advanced, not needed for simple tasks.
- Debugging can be tricky if used incorrectly.
- Cannot be used with arrow function syntax.
JavaScript Function Types Comparison
Feature | Regular | Anonymous | Arrow | IIFE | Higher-Order | Recursive | Generator |
---|---|---|---|---|---|---|---|
Syntax | function name(){} | function () {} | const fn = () => {} | (function () { })() | function (fn) {} | function recurse() {} | function* gen() { yield } |
Named | Yes | No (but can assign) | Yes (via variable) | No | Yes or No | Yes | Yes |
Hoisted | Yes | No | No | No | No | Yes | No |
Reusable | Yes | Yes (if assigned) | Yes | No (runs once) | Yes | Yes | Yes |
this binding | Own this | Own this | No (inherits this) | Own this | Depends on passed fn | Yes | Yes |
arguments object | Yes | Yes | No | Yes | Yes | Yes | No |
Can be used as constructor | Yes | Yes | No | No | Yes | Yes | No |
Executes immediately | No | No | No | Yes | No | No | No |
Common Use Cases | General purpose | Callbacks, one-time use | Short logic, array methods | Setup, private scope | Array methods, event handling | Divide & conquer, tree traversal | Lazy iteration, custom sequences |