JavaScript functions are not limited to performing a task when called. They can also be passed as arguments, returned from other functions, created dynamically, and used to store previously calculated results.
These capabilities make JavaScript functions powerful and flexible. They are widely used in event handling, asynchronous programming, functional programming, and application development.
In this tutorial, you will learn about higher-order functions, callback functions, function factories, currying, immediately invoked function expressions (IIFEs), and memoization.
Higher-Order Functions in JavaScript
A higher-order function is a function that accepts another function as an argument, returns a function, or does both.
JavaScript supports this pattern because functions are first-class values. You can store functions in variables, pass them to other functions, and return them from functions.
Example of a Higher-Order Function
function calculate(a, b, operation) {
return operation(a, b);
}
function add(x, y) {
return x + y;
}
console.log(calculate(10, 20, add));
Output:
30
In this example, calculate() accepts operation as an argument. The add() function is passed to calculate(), which executes it with the supplied numbers.
Therefore, calculate() is a higher-order function.
Using Built-in Higher-Order Functions
JavaScript array methods such as map(), filter(), and reduce() commonly accept callback functions.
For example, map() transforms each element of an array.
const numbers = [1, 2, 3, 4];
const doubled = numbers.map(function (number) {
return number * 2;
});
console.log(doubled);
Output:
[2, 4, 6, 8]
The map() method receives a function and applies it to each element of the array.
You can also use an arrow function:
const numbers = [1, 2, 3, 4];
const doubled = numbers.map(number => number * 2);
console.log(doubled);
Output:
[2, 4, 6, 8]
Practical Uses of Higher-Order Functions
Higher-order functions are useful for:
- Transforming arrays with
map(). - Selecting values with
filter(). - Calculating results with
reduce(). - Reusing common logic across different functions.
- Creating configurable behavior by passing functions as arguments.
Learn more about JavaScript higher-order functions.
Callback Functions in JavaScript
A callback function is a function passed to another function so that it can be invoked by that function.
Callbacks are commonly used in event handling, array methods, timers, and asynchronous operations.
Example of a Callback Function
function greet(name, callback) {
console.log("Hello, " + name);
callback();
}
function sayWelcome() {
console.log("Welcome to Web4Learners!");
}
greet("Karim", sayWelcome);
Output:
Hello, Karim
Welcome to Web4Learners!
The sayWelcome function is passed to greet() as a callback. After printing the greeting, greet() invokes the callback.
Notice that sayWelcome is passed without parentheses. This passes the function itself instead of immediately executing it.
Callback Functions with setTimeout()
Callbacks can also execute after a delay.
console.log("Start");
setTimeout(function () {
console.log("This message appears later");
}, 1000);
console.log("End");
Output:
Start
End
This message appears later
The timer callback runs after the current synchronous code has finished and the timer delay has elapsed. The delay is a minimum waiting time, not a guarantee of exact execution timing.
Callback Functions with Array Methods
const numbers = [10, 15, 20, 25, 30];
const evenNumbers = numbers.filter(function (number) {
return number % 2 === 0;
});
console.log(evenNumbers);
Output:
[10, 20, 30]
The function passed to filter() is a callback. It determines whether each element should be included in the resulting array.
Callback vs Higher-Order Function
These concepts are related but describe different things.
| Concept | Meaning | Example |
|---|---|---|
| Callback function | A function passed to another function | sayWelcome |
| Higher-order function | A function that accepts or returns another function | greet |
A single operation can involve both concepts: the outer function is the higher-order function, while the function passed to it is the callback.
For more examples, read the JavaScript callback functions tutorial.
Function Factories in JavaScript
A function factory is a function that creates and returns another function.
Function factories allow you to generate customized functions based on values supplied when the factory is called.
Example of a Function Factory
function createMultiplier(multiplier) {
return function (number) {
return number * multiplier;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5));
console.log(triple(5));
Output:
10
15
The createMultiplier() function returns a new function that remembers the supplied multiplier value through a closure.
The double function remembers 2, while the triple function remembers 3.
Creating Greeting Functions
function createGreeting(greeting) {
return function (name) {
return greeting + ", " + name + "!";
};
}
const sayHello = createGreeting("Hello");
const sayWelcome = createGreeting("Welcome");
console.log(sayHello("Karim"));
console.log(sayWelcome("Ahmed"));
Output:
Hello, Karim!
Welcome, Ahmed!
Each generated function uses its own greeting value.
Practical Uses of Function Factories
Function factories are useful for:
- Creating reusable validation functions.
- Generating customized formatting functions.
- Creating functions with preset configuration values.
- Building reusable logging or calculation utilities.
- Encapsulating state with closures.
For example, a factory can create a reusable percentage calculator:
function createPercentageCalculator(percentage) {
return function (amount) {
return amount * percentage / 100;
};
}
const calculateGST = createPercentageCalculator(18);
console.log(calculateGST(1000));
Output:
180
The returned function remembers the percentage value of 18 and applies it to the supplied amount.
Currying in JavaScript
Currying is a technique that transforms a function accepting multiple arguments into a sequence of functions, each accepting one argument.
Instead of calling a function with several arguments at once, you call one function at a time.
Regular Function vs Curried Function
A regular function may accept two arguments:
function add(a, b) {
return a + b;
}
console.log(add(5, 10));
Output:
15
A curried version returns another function:
function add(a) {
return function (b) {
return a + b;
};
}
console.log(add(5)(10));
Output:
15
The first call, add(5), returns a function that remembers a as 5. The second call supplies b as 10, producing the result 15.
Currying with Arrow Functions
Arrow functions can make curried functions more concise.
const multiply = a => b => a * b;
console.log(multiply(4)(5));
Output:
20
The outer function accepts a and returns another function that accepts b. The final function multiplies both values.
Practical Example of Currying
Currying can help create reusable functions with preset values.
const addTax = taxRate => price => {
return price + price * taxRate / 100;
};
const addGST = addTax(18);
console.log(addGST(1000));
console.log(addGST(2000));
Output:
1180
2360
The addTax(18) call creates a function that remembers the tax rate. You can then use addGST() with different prices.
Currying vs Function Factories
Currying and function factories can look similar because both can return functions. However, their purposes are different.
| Feature | Function Factory | Currying |
|---|---|---|
| Main purpose | Create customized functions | Transform multi-argument functions into sequences of functions |
| Argument handling | Depends on the design | Arguments are supplied through successive function calls |
| Example | createMultiplier(2) | multiply(2)(3) |
| Typical use | Generate configured behavior | Build reusable functions through partial application |
A function factory can use currying, and a curried function can act as a function factory. The concepts are related but not identical.
IIFE in JavaScript
IIFE stands for Immediately Invoked Function Expression. It is a function expression that is executed immediately after it is defined.
IIFEs were commonly used to create private scopes and prevent variables from becoming global before JavaScript modules became widely available.
Syntax of an IIFE
(function () {
console.log("This function runs immediately");
})();
Output:
This function runs immediately
The parentheses around the function make it a function expression. The final () immediately invokes it.
IIFE with Variables
(function () {
const message = "Hello, JavaScript!";
console.log(message);
})();
Output:
Hello, JavaScript!
The message variable belongs to the function’s local scope. It is not available as a variable outside the IIFE.
IIFE with Parameters
You can pass arguments to an IIFE just like any other function.
(function (name) {
console.log("Hello, " + name);
})("Karim");
Output:
Hello, Karim
The string "Karim" is passed to the IIFE’s name parameter.
Arrow Function IIFE
An IIFE can also be written using an arrow function.
(() => {
console.log("Arrow function IIFE");
})();
Output:
Arrow function IIFE
Practical Uses of IIFEs
IIFEs can be useful when you want to:
- Create a temporary local scope.
- Execute initialization code immediately.
- Avoid introducing temporary variables into an outer scope.
- Encapsulate variables and functions in older JavaScript code.
For example:
const result = (function () {
const price = 1000;
const discount = 10;
return price - price * discount / 100;
})();
console.log(result);
Output:
900
The calculation runs immediately, and the resulting value is stored in result. The local variables price and discount are not directly accessible outside the IIFE.
In modern JavaScript, ES modules and block scope often remove the need for IIFEs, but they remain useful to understand when working with existing code.
Memoization in JavaScript
Memoization is an optimization technique that stores the results of previous function calls so that the same calculations do not need to be repeated.
When a function receives an input it has processed before, a memoized version can return the stored result instead of performing the calculation again.
Memoization is most useful for functions that are expensive to execute and produce the same result for the same input.
Example of Memoization
Consider a function that squares a number:
function square(number) {
console.log("Calculating...");
return number * number;
}
console.log(square(5));
console.log(square(5));
Output:
Calculating...
25
Calculating...
25
The function calculates the result every time it is called.
We can improve this example by storing previously calculated results.
function memoizedSquare() {
const cache = {};
return function (number) {
if (number in cache) {
console.log("Returning cached result");
return cache[number];
}
console.log("Calculating...");
const result = number * number;
cache[number] = result;
return result;
};
}
const square = memoizedSquare();
console.log(square(5));
console.log(square(5));
console.log(square(6));
Output:
Calculating...
25
Returning cached result
25
Calculating...
36
The first call with 5 calculates the square and stores the result in cache.
The second call with 5 retrieves the stored result. The call with 6 performs a new calculation because that input is not yet in the cache.
The cache object remains accessible to the returned function through a closure.
Memoization with Recursive Functions
Memoization can also improve recursive calculations, such as the Fibonacci sequence.
A simple recursive Fibonacci function repeats many calculations:
function fibonacci(n) {
if (n <= 1) {
return n;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
console.log(fibonacci(10));
Output:
55
A memoized version stores previously calculated results:
function createFibonacci() {
const cache = {};
function fibonacci(n) {
if (n <= 1) {
return n;
}
if (n in cache) {
return cache[n];
}
cache[n] = fibonacci(n - 1) + fibonacci(n - 2);
return cache[n];
}
return fibonacci;
}
const fibonacci = createFibonacci();
console.log(fibonacci(10));
console.log(fibonacci(40));
Output:
55
102334155
The cache stores Fibonacci results so that previously calculated values can be reused. For the standard Fibonacci recurrence, memoization reduces the number of repeated calculations substantially.
Advantages of Memoization
Memoization can provide several benefits:
- Reduces repeated calculations.
- Improves performance for expensive, repeatable computations.
- Works well with recursive algorithms.
- Uses closures to preserve cached results between calls.
However, memoization also consumes memory. It is most suitable when caching is appropriate for the function’s inputs and outputs. Functions that depend on changing external state, current time, or other side effects require additional care.
Difference Between the Advanced Function Concepts
The following table summarizes the concepts covered in this tutorial.
| Concept | Main Purpose | Example |
|---|---|---|
| Higher-order function | Accepts or returns functions | numbers.map(callback) |
| Callback function | Passed to another function for invocation | setTimeout(callback, 1000) |
| Function factory | Creates customized functions | createMultiplier(2) |
| Currying | Converts multi-argument calls into chained calls | add(5)(10) |
| IIFE | Defines and invokes a function immediately | (function () {})() |
| Memoization | Caches previous results | square(5) using a cache |
These concepts often work together. For example, a function factory can return a closure, a higher-order function can accept a callback, and a memoized function can use a closure to retain its cache.
Frequently Asked Questions
What is a higher-order function in JavaScript?
A higher-order function accepts another function as an argument, returns a function, or does both. Examples include custom functions that accept callbacks and array methods such as map() and filter().
What is a callback function?
A callback is a function passed to another function so that the receiving function can invoke it. Callbacks are widely used in event handling, timers, and array operations.
What is a function factory?
A function factory is a function that returns another function. It can create customized functions that remember configuration values through closures.
What is currying in JavaScript?
Currying transforms a function that accepts multiple arguments into a sequence of functions, each accepting one argument. For example, add(5)(10) can return 15.
What is an IIFE?
An IIFE is an Immediately Invoked Function Expression. It executes immediately after being defined and can create a local scope for variables and initialization code.
What is memoization?
Memoization stores results from previous function calls. When the same input is used again, a memoized function can reuse the cached result instead of recalculating it.
Are closures required for memoization?
Closures are a common way to implement memoization because they allow a function to preserve access to a cache between calls. However, memoization can also use other storage mechanisms.
Conclusion
Advanced JavaScript functions provide powerful ways to organize code, reuse logic, preserve state, and improve performance.
Higher-order functions and callbacks allow functions to work together. Function factories create customized functions, while currying organizes arguments across multiple calls. IIFEs provide immediate execution with a local scope, and memoization reduces repeated calculations by caching results.
Understanding these concepts will help you write more flexible JavaScript applications and prepare you for functional programming, asynchronous programming, and advanced JavaScript development.