Advanced Functions
Advanced function techniques enable reusable, composable, and performant code. This chapter builds on scope and closures from the intermediate level.
Higher-Order Functions
Functions that take other functions as arguments or return functions:
function repeat(n, action) {
for (let i = 0; i < n; i++) {
action(i);
}
}
repeat(3, i => console.log(i)); // 0, 1, 2
function greaterThan(n) {
return x => x > n;
}
[1, 5, 10].filter(greaterThan(4)); // [5, 10]
// Built-in HOFs: map, filter, reduce, forEach, sort, find
[1, 2, 3].map(x => x * 2); // [2, 4, 6]
Higher-order functions are the foundation of functional programming in JavaScript.
Currying
Transform a multi-argument function into a chain of single-argument functions:
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return (...more) => curried(...args, ...more);
};
}
const add = curry((a, b, c) => a + b + c);
console.log(add(1)(2)(3)); // 6
console.log(add(1, 2)(3)); // 6
console.log(add(1, 2, 3)); // 6
Useful for partial configuration: const addTax = curry((rate, price) => price * (1 + rate)).
Partial Application
Fix some arguments upfront:
function partial(fn, ...fixedArgs) {
return (...remainingArgs) => fn(...fixedArgs, ...remainingArgs);
}
function greet(greeting, name) {
return `${greeting}, ${name}!`;
}
const sayHello = partial(greet, 'Hello');
console.log(sayHello('Alice')); // 'Hello, Alice!'
// Native: bind
const sayHi = greet.bind(null, 'Hi');
Partial application vs currying: partial fixes N arguments at once; currying transforms one argument at a time.
Debounce
Delay execution until after a pause in calls — ideal for search input:
function debounce(fn, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
}
const search = debounce((query) => {
fetch(`/api/search?q=${encodeURIComponent(query)}`)
.then(r => r.json())
.then(renderResults);
}, 300);
input.addEventListener('input', (e) => search(e.target.value));
Only fires 300ms after user stops typing — reduces API calls dramatically.
Debounce with Immediate Option
function debounce(fn, delay, immediate = false) {
let timeoutId;
return function(...args) {
const callNow = immediate && !timeoutId;
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
timeoutId = null;
if (!immediate) fn.apply(this, args);
}, delay);
if (callNow) fn.apply(this, args);
};
}
Throttle
Limit execution to at most once per interval — ideal for scroll/resize:
function throttle(fn, limit) {
let inThrottle = false;
return function(...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
window.addEventListener('scroll', throttle(() => {
updateScrollIndicator();
}, 200), { passive: true });
| Pattern | Use when |
|---|---|
| Debounce | Wait for pause (search, resize end) |
| Throttle | Regular sampling (scroll, mousemove) |
Function Composition
Combine functions into a pipeline:
const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);
const addOne = x => x + 1;
const double = x => x * 2;
const square = x => x * x;
console.log(compose(square, double, addOne)(3)); // ((3+1)*2)^2 = 64
console.log(pipe(addOne, double, square)(3)); // same: 64
Real-world example:
const processUser = pipe(
trimStrings,
normalizeEmail,
validateSchema,
saveToDatabase
);
Memoization
Cache function results for repeated inputs:
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
const fib = memoize(function fib(n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
});
console.log(fib(40)); // fast due to caching
Limit cache size in production to prevent memory growth.
Once
Execute function only once — useful for initialization:
function once(fn) {
let called = false;
let result;
return function(...args) {
if (!called) {
called = true;
result = fn.apply(this, args);
}
return result;
};
}
const init = once(() => {
console.log('Initialized once');
return { config: true };
});
Best Practices
- Pure functions when possible — same input, same output, no side effects
- Name returned functions for clearer stack traces:
return function curried(...args) - Document arity for curried functions
- Use lodash/debounce in production if battle-tested behavior needed
- Test edge cases — rapid calls,
thisbinding, argument changes
Troubleshooting
Debounce never fires
- Delay too long; function recreated each render (React) — wrap in
useCallback/useMemo
Throttle misses final call
- Combine throttle with debounced trailing call for scroll-end detection
this lost in HOF
- Use
fn.apply(this, args)or arrow functions carefully
Memoize stale results
- Cache invalidation needed when dependencies change
These patterns appear throughout production JavaScript — in frameworks, utilities, and performance-critical code paths.