Урок 11

Практика: утилиты на функциях

Собрали набор утилит, которые часто нужны в реальных проектах.

debounce

Откладывает вызов до паузы:

function debounce(fn, delay) {
    let timer = null;

    return function(...args) {
        clearTimeout(timer);
        timer = setTimeout(() => fn.apply(this, args), delay);
    };
}

const onSearch = debounce((q) => fetchResults(q), 300);

throttle

Не чаще раза в интервал:

function throttle(fn, delay) {
    let last = 0;
    let timer = null;

    return function(...args) {
        const now = Date.now();
        const remaining = delay - (now - last);

        if (remaining <= 0) {
            last = now;
            fn.apply(this, args);
        } else if (!timer) {
            timer = setTimeout(() => {
                last = Date.now();
                timer = null;
                fn.apply(this, args);
            }, remaining);
        }
    };
}

once

function once(fn) {
    let called = false;
    let result;

    return function(...args) {
        if (!called) {
            called = true;
            result = fn.apply(this, args);
        }
        return result;
    };
}

memoize

function memoize(fn, keyFn = JSON.stringify) {
    const cache = new Map();

    return function(...args) {
        const key = keyFn(args);
        if (cache.has(key)) return cache.get(key);
        const result = fn.apply(this, args);
        cache.set(key, result);
        return result;
    };
}

curry

function curry(fn) {
    return function curried(...args) {
        if (args.length >= fn.length) return fn.apply(this, args);
        return (...next) => curried.apply(this, [...args, ...next]);
    };
}

compose

const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);

retry

async function retry(fn, { attempts = 3, delay = 1000, backoff = 2 } = {}) {
    let wait = delay;

    for (let i = 0; i < attempts; i++) {
        try {
            return await fn();
        } catch (error) {
            if (i === attempts - 1) throw error;
            await new Promise((r) => setTimeout(r, wait));
            wait *= backoff;
        }
    }
}

timeout

function withTimeout(promise, ms) {
    return Promise.race([
        promise,
        new Promise((_, reject) =>
            setTimeout(() => reject(new Error('Timeout')), ms)
        ),
    ]);
}

sleep

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

await sleep(1000);

chunk

function chunk(arr, size) {
    const result = [];
    for (let i = 0; i < arr.length; i += size) {
        result.push(arr.slice(i, i + size));
    }
    return result;
}

groupBy

function groupBy(items, keyFn) {
    return items.reduce((acc, item) => {
        const key = typeof keyFn === 'function' ? keyFn(item) : item[keyFn];
        acc[key] = acc[key] ?? [];
        acc[key].push(item);
        return acc;
    }, {});
}

uniqueBy

function uniqueBy(items, keyFn) {
    const seen = new Set();
    return items.filter((item) => {
        const key = typeof keyFn === 'function' ? keyFn(item) : item[keyFn];
        if (seen.has(key)) return false;
        seen.add(key);
        return true;
    });
}

sortBy

function sortBy(items, keyFn, direction = 'asc') {
    return [...items].sort((a, b) => {
        const av = typeof keyFn === 'function' ? keyFn(a) : a[keyFn];
        const bv = typeof keyFn === 'function' ? keyFn(b) : b[keyFn];
        if (av < bv) return direction === 'asc' ? -1 : 1;
        if (av > bv) return direction === 'asc' ? 1 : -1;
        return 0;
    });
}

Итоги

  • debounce, throttle — для событий
  • once, memoize, curry — для оптимизации
  • compose, pipe — для композиции
  • retry, withTimeout, sleep — для асинхронности
  • chunk, groupBy, uniqueBy, sortBy — для массивов
  • Такие утилиты удобно выносить в utils/