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

Vlad Poe
Web Standards
Published in
3 min readJun 13, 2018

Перевод «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

--

--