📌 The Ultimate Guide to Functions in JavaScript & TypeScript 🚀
A Deep Dive into Function Types, Overloading, Callbacks, Performance Optimization, and Best Practices.
Balaji Udayagiri
Frontend Lead Engineer
Technologies Used
Summary
Functions are the backbone of JavaScript & TypeScript. This guide explores every function type, best practices, and advanced concepts like higher-order functions, memoization, and decorators.
Key Points
- Understand function types: Regular, Arrow, Async, and more.
- Learn function overloading and higher-order functions.
- Optimize performance using memoization and throttling.
- Explore function decorators and metaprogramming.
Code Examples
📌 Function Declaration
Function declarations are hoisted, meaning they can be called before they are defined. They are useful for defining helper functions that can be reused throughout a program.
function greet(name: string): string {
return `Hello, ${name}!`;
}
console.log(greet("Alice")); // ✅ Works
📌 Function Expression
Function expressions are not hoisted and must be defined before use. They are useful for creating anonymous functions, assigning functions to variables, and passing functions as arguments.
const greet = function(name: string): string {
return `Hello, ${name}!`;
};
console.log(greet("Alice")); // ✅ Works
📌 Arrow Function
Arrow functions provide a concise syntax and do not bind their own `this`. They are commonly used for callbacks and functional programming.
const greet = (name: string): string => `Hello, ${name}!`;
console.log(greet("Alice")); // ✅ Works
📌 Immediately Invoked Function Expression (IIFE)
IIFE functions execute immediately after they are defined. They are used to avoid polluting the global scope and are often used in modular programming.
(function() {
console.log("This function runs immediately!");
})();
📌 Generator Function
Generator functions allow functions to pause and resume execution using the `yield` keyword. They are useful for handling large datasets and implementing iterators.
function* counter() {
yield 1;
yield 2;
yield 3;
}
const gen = counter();
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // 3
📌 Async Function
Async functions make it easier to work with Promises using the `await` keyword. They simplify asynchronous code and improve readability.
async function fetchData(): Promise<string> {
return new Promise(resolve => setTimeout(() => resolve("Data loaded"), 1000));
}
async function main() {
const data = await fetchData();
console.log(data);
}
main();
📌 Constructor Function
Constructor functions create object instances using the `new` keyword. They were widely used before the introduction of ES6 classes.
function Person(name: string) {
this.name = name;
}
const user = new (Person as any)("Alice");
console.log(user.name); // Alice
📌 Callback Function
Callbacks are functions passed as arguments to other functions. They are commonly used in asynchronous programming and event handling.
function processUser(id: number, callback: (name: string) => void): void {
const name = "Alice";
callback(name);
}
processUser(1, (userName) => {
console.log(`Processed user: ${userName}`);
});
📌 Higher-Order Function
Higher-order functions are functions that take other functions as arguments or return functions. They are widely used in functional programming.
function multiplier(factor: number) {
return (value: number) => value * factor;
}
const double = multiplier(2);
console.log(double(10)); // 20
📌 Recursive Function
A recursive function calls itself until a base condition is met. Recursion is commonly used for solving problems like tree traversal and factorial calculations.
function factorial(n: number): number {
return n === 0 ? 1 : n * factorial(n - 1);
}
console.log(factorial(5)); // 120
📌 Currying Function
Currying transforms a function with multiple arguments into a sequence of functions, each taking a single argument. It enhances code reusability and modularity.
const add = (a: number) => (b: number) => a + b;
console.log(add(2)(3)); // 5
📌 Memoization Function
Memoization is a caching technique that stores the results of function calls to avoid redundant calculations. It improves performance in computation-heavy applications.
function memoize(fn: Function) {
let cache: Record<string, any> = {};
return function (...args: any[]) {
let key = JSON.stringify(args);
return cache[key] || (cache[key] = fn(...args));
};
}
📌 Debounce Function
Debounce delays the execution of a function until a specified time has passed since the last time it was called. It is useful for optimizing event handling, such as search inputs.
function debounce(fn: Function, delay: number) {
let timer: any;
return function (...args: any[]) {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
📌 Throttle Function
Throttle ensures that a function is executed at most once within a specified time interval. It is commonly used for optimizing scroll and resize events.
function throttle(fn: Function, limit: number) {
let inThrottle: boolean;
return function (...args: any[]) {
if (!inThrottle) {
fn(...args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
📌 Proxy Function
Proxy functions allow interception of function calls to modify their behavior. They are used in logging, validation, and middleware systems.
const handler = {
apply: function(target: Function, thisArg: any, argumentsList: any) {
console.log(`Calling function with arguments: ${argumentsList}`);
return target(...argumentsList);
}
};
function sum(a: number, b: number): number {
return a + b;
}
const proxiedSum = new Proxy(sum, handler);
console.log(proxiedSum(5, 10)); // Logs function call and result
📌 Function Decorator
Function decorators allow for metaprogramming, modifying the behavior of functions at runtime. They are commonly used in TypeScript, Angular, and NestJS.
function Log(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${key} with`, args);
return original.apply(this, args);
};
}