Day 24: Higher-Order Functions, Debounce & Throttle
✅ Today you will learn
- Higher-order functions: take or return functions
- Composition, pipe and currying
- Immutability: update data without mutating it
- Debounce: wait until the user stops typing
- Throttle: run at most once every N ms
🏢 In the real world
The live search box calls the API on every keystroke: typing “kaju katli” fires 10 requests, results flicker, and on a shared server the shop starts hitting rate limits. Debounce fixes it with 10 lines – and “implement debounce” is one of the most common front-end interview coding questions in India.
Higher-order functions
// Takes a function
function repeat(times, action) { for (let i = 1; i <= times; i++) action(i); }
repeat(3, n => console.log(`Box ${n} packed`));
// Returns a function
const discount = percent => price => Math.round(price * (1 - percent / 100));
const festival = discount(15);
console.log([900, 650, 500].map(festival));
// Wraps a function to add behaviour (a "decorator")
function logged(fn) {
return (...args) => {
const result = fn(...args);
console.log(`${fn.name}(${args.join(', ')}) → ${result}`);
return result;
};
}
const addGst = function addGst(amount) { return Math.round(amount * 1.05); };
logged(addGst)(1800);Composition, pipe and currying
const pipe = (...fns) => input => fns.reduce((value, fn) => fn(value), input);
const subtotal = cart => cart.reduce((s, i) => s + i.price * i.qty, 0);
const applyCoupon = code => amount => (code === 'SWEET10' ? amount * 0.9 : amount);
const addDelivery = amount => (amount >= 1500 ? amount : amount + 60);
const round = amount => Math.round(amount);
const checkoutTotal = pipe(subtotal, applyCoupon('SWEET10'), addDelivery, round);
console.log(checkoutTotal([{ price: 900, qty: 2 }])); // 1800 → 1620 → 1620
console.log(checkoutTotal([{ price: 500, qty: 1 }])); // 500 → 450 → 510
// currying: f(a, b, c) → f(a)(b)(c)
const curry = fn => function c(...args) { return args.length >= fn.length ? fn(...args) : (...more) => c(...args, ...more); };
const price = curry((rate, kg, gst) => Math.round(rate * kg * (1 + gst)));
const kajuPrice = price(900);
console.log(kajuPrice(2)(0.05), kajuPrice(0.5, 0.05));Immutability: never change data in place
const cart = { customer: 'Aarti', items: [{ id: 1, qty: 1 }, { id: 4, qty: 2 }] };
// ❌ mutating – other parts of the app holding "cart" change silently
// cart.items[0].qty = 5;
// ✅ create new objects/arrays for what changed
const updated = {
...cart,
items: cart.items.map(item => (item.id === 1 ? { ...item, qty: item.qty + 1 } : item)),
};
const removed = { ...cart, items: cart.items.filter(i => i.id !== 4) };
const added = { ...cart, items: [...cart.items, { id: 7, qty: 1 }] };
console.log(cart.items[0].qty, updated.items[0].qty);
console.log(removed.items.length, added.items.length, cart.items.length);
console.log(cart.items[1] === updated.items[1]); // unchanged parts are shared (cheap)
const deepCopy = structuredClone(cart); // full independent copy when needed
deepCopy.items[0].qty = 99;
console.log(cart.items[0].qty);React decides whether to re-render by comparing references (old === new). If you mutate an array, the reference stays the same and the screen does not update. Immutable updates are therefore a must for React and Redux.
Debounce: wait for a pause
function debounce(fn, delay) {
let timer; // closure (Day 16)
return function (...args) {
clearTimeout(timer); // every new call cancels the previous one
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
const search = debounce(q => console.log(`🔎 API call for "${q}"`), 300);
// simulate typing "kaju" quickly, a pause, then " katli"
const typed = ['k', 'ka', 'kaj', 'kaju', 'kaju ', 'kaju k', 'kaju ka', 'kaju katli'];
const times = [0, 80, 160, 240, 700, 780, 860, 940];
typed.forEach((text, i) => setTimeout(() => search(text), times[i]));
// only 2 API calls instead of 8Throttle: at most once per interval
function throttle(fn, interval) {
let last = 0, timer;
return function (...args) {
const now = Date.now();
const remaining = interval - (now - last);
if (remaining <= 0) {
last = now;
fn.apply(this, args); // run immediately
} else {
clearTimeout(timer);
timer = setTimeout(() => { last = Date.now(); fn.apply(this, args); }, remaining); // and once at the end
}
};
}
let calls = 0;
const onScroll = throttle(y => console.log(`update sticky header at y=${y} (call ${++calls})`), 200);
for (let i = 0; i <= 20; i++) setTimeout(() => onScroll(i * 50), i * 30); // 21 scroll events over 600 ms| Debounce | Throttle | |
|---|---|---|
| Runs | Once, after events stop for N ms | Regularly, at most once per N ms |
| Like | A lift door waiting until nobody else enters | A traffic signal letting cars through every N seconds |
| Use for | Search boxes, auto-save, resize-end, form validation | Scroll position, mousemove, drag, analytics pings |
| Typical delay | 250–400 ms | 100–250 ms |
Real task: debounced live search against the API
<input id="q" placeholder="Type fast: kaju, ghewar, box…" aria-label="Search sweets" style="width:260px">
<p>API calls: <b id="calls">0</b> · keystrokes: <b id="keys">0</b></p>
<ul id="results" aria-live="polite"></ul>
<script>
const API = 'https://codingeasily.in/api/sweets/';
let calls = 0, keys = 0, latest = 0;
function debounce(fn, delay) {
let t;
return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), delay); };
}
async function search(q) {
const id = ++latest; // ignore out-of-order responses
document.querySelector('#calls').textContent = ++calls;
const res = await fetch(`${API}?q=${encodeURIComponent(q)}`);
const data = await res.json();
if (id !== latest) return; // a newer search already started
document.querySelector('#results').innerHTML =
data.map(p => `<li>${p.name} – ₹${p.price}</li>`).join('') || '<li>No match</li>';
}
const debounced = debounce(q => { if (q.trim()) search(q.trim()); }, 300);
document.querySelector('#q').addEventListener('input', e => {
document.querySelector('#keys').textContent = ++keys;
debounced(e.target.value);
});
</script>🏅 Job skills unlocked today
Practice tasks
🏋️ Practice – do it yourself
Show solution
function memoizeAsync(fn) {
const cache = new Map();
return arg => {
if (!cache.has(arg)) cache.set(arg, fn(arg).catch(err => { cache.delete(arg); throw err; }));
return cache.get(arg);
};
}
let apiCalls = 0;
const fakeApi = q => new Promise(r => setTimeout(() => r(`results for ${q} (call ${++apiCalls})`), 50));
const cachedSearch = memoizeAsync(fakeApi);
(async () => {
console.log(await cachedSearch('kaju'));
console.log(await cachedSearch('kaju'));
console.log(await cachedSearch('ghewar'));
})();Show solution
const order = { id: 1042, customer: { name: 'Aarti' }, items: [{ id: 1, qty: 1 }, { id: 4, qty: 1 }] };
const next = {
...order,
gift: true,
items: order.items.map(i => (i.id === 4 ? { ...i, qty: 3 } : i)),
};
console.log(JSON.stringify(order));
console.log(JSON.stringify(next));
console.log(order.customer === next.customer);Quick quiz
🧠 Quick Quiz – test yourself
Interview corner
💼 Interview Corner
Implement debounce.
function debounce(fn, ms) { let t; return function (...args) { clearTimeout(t); t = setTimeout(() => fn.apply(this, args), ms); }; } – each call resets the timer so fn runs only after calls stop for ms; mention leading/trailing options and cancel().
Debounce vs throttle with a real example?
Debounce: autocomplete search – fire once after typing stops. Throttle: infinite-scroll position or window resize handling – fire at a steady maximum rate while the event continues.
Summary
- Functions are values: pass them, return them, wrap them.
- pipe/compose and currying build logic from small pieces.
- Update with spread/map/filter; structuredClone for deep copies.
- Debounce for “after the pause”, throttle for “at most every N ms”.
Day 25: DOM performance and patterns – fast rendering, event delegation at scale, requestAnimationFrame and lazy loading.
