Функ­ции-де­ко­ра­то­ры, ко­то­рые мож­но на­пи­сать с нуля

Перевод «Here are a few function decorators you can write from scratch»

Кристи Сальсезку

Перевод Владислав Почепцов

Редактура Вадим Макеев

Фото Calum Lewis.

Декораторы — это функции высшего порядка, которые принимают в качестве аргумента одну функцию и возвращают другую. Возвращаемая функция является преобразованным вариантом функции-аргумента Javascript Allongé

Давайте самостоятельно напишем некоторые базовые дектораторы, представленные в таких библиотеках, как underscore.js, lodash.js, ramda.js.

once()#

  • once(fn) создает экземпляр функции, которая должна быть выполнена только один раз. Паттерн может быть использован, например, для инициализации, когда нужно быть уверенным в единичном запуске функциональности, даже если сама функция вызвана в нескольких местах.
function once(fn){
    let returnValue;
    let canRun = true;
    return function runOnce(){
        if(canRun) {
            returnValue = fn.apply(this, arguments);
            canRun = false;
        }
        return returnValue;
    }
}

var processonce = once(process);
processonce(); // process
processonce(); //

Функция once() возвращает другую функцию — runOnce(), использующую замыкание. Обратите также внимание, как осуществлен вызов оригинальной функции, а именно через передачу this и arguments в метод apply: fn.apply(this, arguments).

Если хотите узнать замыкания глубже, обратите внимание на статью «Why you should give the Closure function another chance».

after()#

  • after(count, fn) создает вариант функции, которая будет выполнена только после определенного количества вызовов. Функция полезна, например, если должна быть выполнена только по завершению асинхронных операций.
function after(count, fn) {
    let runCount = 0;
    return function runAfter() {
        runCount = runCount + 1;
        if (runCount >= count) {
            return fn.apply(this, arguments);
        }
    }
}

function logResult() { console.log('calls have finished'); }
let logResultAfter2Calls = after(2, logResult);

setTimeout(function logFirstCall() {
    console.log('1st call has finished');
    logResultAfter2Calls();
}, 3000);

setTimeout(function logSecondCall() {
    console.log('2nd call has finished');
    logResultAfter2Calls();
}, 4000);

В примере выше при помощи after() я создаю функцию logResultAfter2Calls(). Она в свою очередь выполняет logResult() только после второго вызова.

throttle()#

  • throttle(fn, wait) создает вариант функции, которая при повторяющихся вызовах выполняется через указанный временной интервал (аргумент wait). Декоратор эффективен для обработки быстро повторяющихся событий.
function throttle(fn, interval) {
    let lastTime;
    return function throttled() {
        let timeSinceLastExecution = Date.now() - lastTime;
        if(!lastTime || (timeSinceLastExecution >= interval)) {
            fn.apply(this, arguments);
            lastTime = Date.now();
        }
    };
}

let throttledProcess = throttle(process, 1000);
$(window).mousemove(throttledProcess);

Здесь движение мыши генерирует множество событий mousemove, тогда как оригинальная функция process() вызывается лишь раз в секунду.

debounce()#

  • debounce(fn, wait) создает вариант функции, которая выполняет оригинальную функцию спустя wait миллисекунд после предыдущего вызова декорированной функции. Паттерн также применяется в работе с повторяющимися событиями. Он полезен, если функциональность должна быть выполнена по завершению очереди событий.
function debounce(fn, interval) {
    let timer;
    return function debounced() {
        clearTimeout(timer);
        let args = arguments;
        let that = this;
        timer = setTimeout(function callOriginalFn() {
            fn.apply(that, args);
        }, interval);
    };
}

let delayProcess = debounce(process, 400);
$(window).resize(delayProcess);

Функция debounce() часто используется вместе с событиями scroll, resize, mousemove и keypress.

Частичное применение#

Частичное применение преобразует функцию за счет изменения количества параметров. Это один из примеров движения от общего к частному.

partial()#

На этот раз создадим метод partial() и сделаем его доступным для всех функций. В данном примере я использую синтаксис ECMAScript 6, а именно оператор rest. С его помощью набор аргументов функции преобразуется в массив ...leftArguments. Это нужно для конкатенации массивов, тогда как специальный объект arguments массивом не является.

function.prototype.partial = function(...leftArguments){
    let fn = this;
    return function partialFn(...rightArguments){
        let args = leftArguments.concat(rightArguments);
        return fn.apply(this, args);
    }
}

function log(level, message){
    console.log(level + ' : ' + message);
}

let logInfo = log.partial('Info');
logInfo('here is a message');

Обратите внимание, созданная таким образом logInfo() использует лишь один аргумент message.

Заключение#

Применение указанных функций помогает понять принципы работы декораторов и саму идею инкапсуляции логики внутри них.

Декораторы — мощный инструмент расширения функциональности без изменения исходной функции. Это отличный путь переиспользовать код, и он соответствует функциональной парадигме программирования.

Больше о ФП в JavaScript#