JavaScript обеспечивает исключительную гибкость при работе с функциями. Их можно передавать, использовать как объекты, и сейчас мы увидим, как перенаправлять вызовы между ними и украшать их.
Допустим, у нас есть функция slow(x)
, которая нагружает процессор, но ее результаты стабильны. Другими словами, для одного и того же x
он всегда возвращает один и тот же результат.
Если функция вызывается часто, мы можем захотеть кэшировать (запомнить) результаты, чтобы не тратить дополнительное время на перерасчеты.
Но вместо добавления этой функциональности в slow()
мы создадим функцию-оболочку, которая добавляет кеширование. Как мы увидим, у этого есть много преимуществ.
Вот код, и следуют пояснения:
функция медленная(х) { // здесь может быть тяжелая работа с интенсивным использованием процессора alert(`Вызывается с помощью ${x}`); вернуть х; } функция cachingDecorator(func) { пусть кэш = новая карта(); возвращаемая функция (х) { if (cache.has(x)) { // если в кеше есть такой ключ вернуть кэш.получить(х); // читаем из него результат } пусть результат = func(x); // иначе вызовем func кэш.set(х, результат); // и кэшируем (запоминаем) результат вернуть результат; }; } медленно = кэшированиеDecorator (медленно); предупреждение(медленно(1)); // медленный(1) кэшируется и возвращается результат alert("Опять: " + медленно(1)); // результат медленного(1) возвращается из кэша предупреждение(медленно(2)); // медленный(2) кэшируется и возвращается результат alert("Опять: " + медленно(2)); // результат медленного(2) возвращается из кэша
В приведенном выше коде cachingDecorator
— это декоратор : специальная функция, которая принимает другую функцию и изменяет ее поведение.
Идея состоит в том, что мы можем вызвать cachingDecorator
для любой функции, и она вернет оболочку кэширования. Это здорово, потому что у нас может быть множество функций, которые могли бы использовать такую возможность, и все, что нам нужно сделать, — это применить к ним cachingDecorator
.
Отделив кэширование от кода основной функции, мы также упрощаем основной код.
Результатом cachingDecorator(func)
является «обертка»: function(x)
, которая «обертывает» вызов func(x)
в логику кэширования:
Из внешнего кода обернутая slow
функция по-прежнему делает то же самое. В его поведение только что добавлен аспект кэширования.
Подводя итог, можно сказать, что использование отдельного cachingDecorator
вместо изменения самого slow
кода дает несколько преимуществ:
cachingDecorator
можно использовать повторно. Мы можем применить его к другой функции.
Логика кэширования отдельная, она не увеличивала сложность самого slow
(если оно было).
При необходимости мы можем объединить несколько декораторов (другие декораторы последуют за нами).
Упомянутый выше декоратор кэширования не подходит для работы с методами объекта.
Например, в приведенном ниже коде worker.slow()
перестает работать после оформления:
// сделаем кэширование worker.slow пусть рабочий = { некоторыйМетод() { возврат 1; }, медленно (х) { // здесь страшная задача, нагружающая процессор alert("Вызывается с помощью " + x); return x * this.someMethod(); // (*) } }; // тот же код, что и раньше функция cachingDecorator(func) { пусть кэш = новая карта(); возвращаемая функция (х) { если (cache.has(x)) { вернуть кэш.получить(х); } пусть результат = func(x); // (**) кэш.set(х, результат); вернуть результат; }; } оповещение(worker.slow(1)); // оригинальный метод работает worker.slow = cachingDecorator(worker.slow); // теперь сделаем это кэшированием оповещение(worker.slow(2)); // Упс! Ошибка: невозможно прочитать свойство someMethod неопределенного значения.
Ошибка возникает в строке (*)
, которая пытается получить доступ к this.someMethod
и терпит неудачу. Вы понимаете, почему?
Причина в том, что оболочка вызывает исходную функцию как func(x)
в строке (**)
. И при таком вызове функция получает this = undefined
.
Мы бы наблюдали аналогичный симптом, если бы попытались запустить:
пусть func = worker.slow; функция(2);
Итак, обертка передает вызов исходному методу, но без контекста this
. Отсюда и ошибка.
Давайте исправим это.
Существует специальный встроенный метод функции func.call(context, …args), который позволяет вызывать функцию, явно устанавливая this
.
Синтаксис:
func.call(контекст, arg1, arg2, ...)
Он запускает func
предоставляя первый аргумент в качестве this
, а следующий в качестве аргументов.
Проще говоря, эти два вызова делают почти одно и то же:
функция(1, 2, 3); func.call(obj, 1, 2, 3)
Они оба вызывают func
с аргументами 1
, 2
и 3
. Единственное отличие состоит в том, что func.call
также устанавливает this
в obj
.
Например, в приведенном ниже коде мы вызываем sayHi
в контексте разных объектов: sayHi.call(user)
запускает sayHi
предоставляя this=user
, а следующая строка устанавливает this=admin
:
функция SayHi() { оповещение(это.имя); } let user = { name: "Джон" }; пусть администратор = {имя: "Администратор"}; // используем вызов для передачи разных объектов как «это» SayHi.call(пользователь); // Джон SayHi.call(администратор); // Администратор
И здесь мы используем call
чтобы say
с данным контекстом и фразой:
функция сказать (фраза) { alert(this.name + ': ' + фраза); } let user = { name: "Джон" }; // пользователь становится этим, а «Привет» становится первым аргументом Say.call(пользователь, «Привет»); // Джон: Привет
В нашем случае мы можем использовать call
в обертке для передачи контекста исходной функции:
пусть рабочий = { некоторыйМетод() { возврат 1; }, медленно (х) { alert("Вызывается с помощью " + x); return x * this.someMethod(); // (*) } }; функция cachingDecorator(func) { пусть кэш = новая карта(); возвращаемая функция (х) { если (cache.has(x)) { вернуть кэш.получить(х); } пусть результат = func.call(this, x); // "это" теперь передается правильно кэш.set(х, результат); вернуть результат; }; } worker.slow = cachingDecorator(worker.slow); // теперь сделаем это кэшированием оповещение(worker.slow(2)); // работает оповещение(worker.slow(2)); // работает, не вызывает оригинал (кэшированный)
Теперь все в порядке.
Чтобы все было понятно, давайте посмотрим более подробно, как this
передается:
После оформления worker.slow
теперь является function (x) { ... }
.
Поэтому, когда worker.slow(2)
выполняется, оболочка получает 2
в качестве аргумента и this=worker
(это объект перед точкой).
Внутри оболочки, предполагая, что результат еще не кэширован, func.call(this, x)
передает текущий this
( =worker
) и текущий аргумент ( =2
) исходному методу.
Теперь сделаем cachingDecorator
еще более универсальным. До сих пор он работал только с функциями с одним аргументом.
Как теперь кэшировать метод worker.slow
с несколькими аргументами?
пусть рабочий = { медленный (мин, максимум) { вернуть мин + макс; // предполагается страшная нагрузка на процессор } }; // следует помнить вызовы с одним и тем же аргументом worker.slow = cachingDecorator(worker.slow);
Раньше для одного аргумента x
мы могли просто cache.set(x, result)
для сохранения результата и cache.get(x)
для его получения. Но теперь нам нужно запомнить результат для комбинации аргументов (min,max)
. Собственная Map
принимает в качестве ключа только одно значение.
Возможны множество решений:
Реализуйте новую (или используйте стороннюю) структуру данных, похожую на карту, которая является более универсальной и допускает использование нескольких ключей.
Используйте вложенные карты: cache.set(min)
будет Map
, в которой хранится пара (max, result)
. Таким образом, мы можем получить result
как cache.get(min).get(max)
.
Объедините два значения в одно. В нашем конкретном случае мы можем просто использовать строку "min,max"
в качестве ключа Map
. Для гибкости мы можем предоставить декоратору функцию хеширования , которая знает, как сделать одно значение из многих.
Для многих практических приложений достаточно хорош третий вариант, поэтому мы остановимся на нем.
Также нам нужно передать не только x
, но и все аргументы в func.call
. Напомним, что в function()
мы можем получить псевдомассив ее аргументов в качестве arguments
, поэтому func.call(this, x)
следует заменить на func.call(this, ...arguments)
.
Вот более мощный cachingDecorator
:
пусть рабочий = { медленный (мин, максимум) { alert(`Вызывается с помощью ${min},${max}`); вернуть мин + макс; } }; функция cachingDecorator(func, hash) { пусть кэш = новая карта(); функция возврата() { пусть ключ = хеш (аргументы); // (*) если (cache.has(ключ)) { вернуть кэш.получить(ключ); } пусть результат = func.call(this, ...arguments); // (**) кэш.set(ключ, результат); вернуть результат; }; } хеш функции (args) { вернуть args[0] + ',' + args[1]; } worker.slow = cachingDecorator(worker.slow, hash); предупреждение(worker.slow(3, 5)); // работает alert( "Опять " + worker.slow(3, 5)); // то же самое (кэшировано)
Теперь он работает с любым количеством аргументов (хотя хэш-функцию также необходимо будет настроить, чтобы она допускала любое количество аргументов. Интересный способ справиться с этим будет описан ниже).
Есть два изменения:
В строке (*)
он вызывает hash
для создания единственного ключа из arguments
. Здесь мы используем простую функцию «объединения», которая превращает аргументы (3, 5)
в ключ "3,5"
. В более сложных случаях могут потребоваться другие функции хеширования.
Затем (**)
использует func.call(this, ...arguments)
для передачи контекста и всех аргументов, полученных оболочкой (а не только первого), исходной функции.
Вместо func.call(this, ...arguments)
мы могли бы использовать func.apply(this, arguments)
.
Синтаксис встроенного метода func.apply:
func.apply(контекст, аргументы)
Он запускает func
, устанавливая this=context
и используя объект args
, подобный массиву, в качестве списка аргументов.
Единственное синтаксическое различие между call
и apply
заключается в том, что call
ожидает список аргументов, а apply
принимает с собой объект, подобный массиву.
Итак, эти два вызова почти эквивалентны:
func.call(контекст, ...args); func.apply(контекст, аргументы);
Они выполняют один и тот же вызов func
с заданным контекстом и аргументами.
Есть лишь небольшая разница в отношении args
:
Синтаксис распространения ...
позволяет передавать итерируемые args
в качестве списка для call
.
apply
принимает только args
, подобные массиву .
…А для объектов, которые одновременно являются итеративными и похожими на массивы, например, реального массива, мы можем использовать любой из них, но apply
, вероятно, будет быстрее, потому что большинство движков JavaScript лучше оптимизируют его внутри.
Передача всех аргументов вместе с контекстом в другую функцию называется переадресацией вызовов .
Это самая простая форма:
пусть оболочка = функция() { вернуть func.apply(это, аргументы); };
Когда внешний код вызывает такую wrapper
, это неотличимо от вызова исходной функции func
.
Теперь сделаем еще одно незначительное улучшение в функции хеширования:
хеш функции (args) { вернуть args[0] + ',' + args[1]; }
На данный момент он работает только с двумя аргументами. Было бы лучше, если бы он мог склеивать любое количество args
.
Естественным решением было бы использовать метод arr.join:
хеш функции (args) { вернуть args.join(); }
…К сожалению, это не сработает. Потому что мы вызываем hash(arguments)
, а объект arguments
является одновременно итеративным и похожим на массив, но не является настоящим массивом.
Таким образом, вызов join
не удастся, как мы видим ниже:
функция хэш() { оповещение(аргументы.join()); // Ошибка: аргументы.join не является функцией } хеш(1, 2);
Тем не менее, есть простой способ использовать объединение массивов:
функция хэш() { Предупреждение([].join.call(аргументы)); // 1,2 } хэш(1, 2);
Этот трюк называется заимствованием метода .
Мы берем (заимствуем) метод соединения из обычного массива ( [].join
) и используем [].join.call
для его запуска в контексте arguments
.
Почему это работает?
Это потому, что внутренний алгоритм собственного метода arr.join(glue)
очень прост.
Взято из спецификации практически «как есть»:
Пусть glue
будет первым аргументом или, если аргументов нет, то запятая ","
.
Пусть result
будет пустой строкой.
Добавьте this[0]
к result
.
Добавьте glue
и this[1]
.
Добавьте glue
и this[2]
.
…Делайте так, пока элементы this.length
не будут склеены.
Возврат result
.
Итак, технически он берет this
и объединяет this[0]
, this[1]
… и т. д. вместе. Он намеренно написан таким образом, чтобы можно было использовать любой массив, подобный this
(не совпадение, многие методы следуют этой практике). Вот почему это также работает с this=arguments
.
Заменять функцию или метод на декорируемую в целом безопасно, за исключением одной мелочи. Если исходная функция имела свойства, такие как func.calledCount
или что-то еще, то декорированная функция их не предоставит. Потому что это обертка. Поэтому нужно быть осторожным, если их использовать.
Например, в приведенном выше примере, если slow
функция имела какие-либо свойства, то cachingDecorator(slow)
является оболочкой без них.
Некоторые декораторы могут предоставлять свои собственные свойства. Например, декоратор может подсчитать, сколько раз вызывалась функция и сколько времени это заняло, и предоставить эту информацию через свойства оболочки.
Существует способ создания декораторов, которые сохраняют доступ к свойствам функции, но для этого требуется использовать специальный Proxy
объект для оболочки функции. Мы обсудим это позже в статье Proxy и Reflect.
Декоратор — это оболочка функции, которая изменяет ее поведение. Основная работа по-прежнему выполняется функцией.
Декораторы можно рассматривать как «функции» или «аспекты», которые можно добавить к функции. Мы можем добавить один или добавить несколько. И все это без изменения его кода!
Для реализации cachingDecorator
мы изучили методы:
func.call(context, arg1, arg2…) – вызывает func
с заданным контекстом и аргументами.
func.apply(context, args) – вызывает func
передавая context
как this
и args
, подобные массиву, в список аргументов.
Общая переадресация вызовов обычно выполняется с помощью apply
:
пусть оболочка = функция() { вернуть original.apply(это, аргументы); };
Мы также видели пример заимствования метода , когда мы берем метод из объекта и call
его в контексте другого объекта. Довольно часто методы массива применяются к arguments
. Альтернативой является использование объекта остальных параметров, который является реальным массивом.
В дикой природе есть много декораторов. Проверьте, насколько хорошо они у вас получились, решив задания этой главы.
важность: 5
Создайте декоратор spy(func)
, который должен возвращать оболочку, сохраняющую все вызовы функции в своем свойстве calls
.
Каждый вызов сохраняется как массив аргументов.
Например:
функция работа(а, б) { предупреждение (а + б); // работа — произвольная функция или метод } работа = шпион (работа); работа(1, 2); // 3 работа(4, 5); // 9 for (пусть аргументы work.calls) { alert('вызов:' + args.join()); // "вызов:1,2", "вызов:4,5" }
PS Этот декоратор иногда полезен при модульном тестировании. Его расширенная форма — sinon.spy
в библиотеке Sinon.JS.
Откройте песочницу с тестами.
Оболочка, возвращаемая spy(f)
должна сохранить все аргументы, а затем использовать f.apply
для переадресации вызова.
функция шпион (функ) { функция-обертка(...args) { // использование ...args вместо аргументов для хранения «настоящего» массива в обертке.calls обертка.calls.push(args); return func.apply(this, args); } оболочка.вызовы = []; возвратная обертка; }
Откройте решение с тестами в песочнице.
важность: 5
Создайте декоратор delay(f, ms)
который задерживает каждый вызов f
на миллисекунды в ms
.
Например:
функция f(x) { предупреждение (х); } // создаем обертки пусть f1000 = задержка (f, 1000); пусть f1500 = задержка (f, 1500); f1000("тест"); // показывает «тест» через 1000 мс f1500("тест"); // показывает «тест» через 1500 мс
Другими словами, delay(f, ms)
возвращает вариант f
«с задержкой на ms
».
В приведенном выше коде f
— это функция одного аргумента, но ваше решение должно передавать все аргументы и контекст this
.
Откройте песочницу с тестами.
Решение:
функция задержки(f, мс) { функция возврата() { setTimeout(() => f.apply(this, аргументы), мс); }; } пусть f1000 = задержка (предупреждение, 1000); f1000("тест"); // показывает «тест» через 1000 мс
Обратите внимание, как здесь используется функция стрелки. Как мы знаем, стрелочные функции не имеют собственных аргументов this
и arguments
, поэтому f.apply(this, arguments)
берет this
и arguments
из оболочки.
Если мы передаем обычную функцию, setTimeout
вызовет ее без аргументов и this=window
(при условии, что мы находимся в браузере).
Мы по-прежнему можем передать this
право, используя промежуточную переменную, но это немного более громоздко:
функция задержки(f, мс) { возвращаемая функция(...args) { пусть saveThis = это; // сохраняем это в промежуточной переменной setTimeout(функция() { f.apply(savedThis, args); // используем его здесь }, РС); }; }
Откройте решение с тестами в песочнице.
важность: 5
Результатом декоратора debounce(f, ms)
является оболочка, которая приостанавливает вызовы f
до тех пор, пока не пройдет ms
миллисекунд бездействия (нет вызовов, «период восстановления»), а затем вызывает f
один раз с последними аргументами.
Другими словами, debounce
похож на секретаря, который принимает «телефонные звонки» и ждет, пока не пройдет ms
миллисекунд молчания. И только потом передает «начальнику» информацию о последнем звонке (звонит фактическому f
).
Например, у нас была функция f
, и мы заменили ее на f = debounce(f, 1000)
.
Тогда, если обернутая функция вызывается в 0, 200 и 500 мс, а вызовов нет, то фактическая f
будет вызываться только один раз, в 1500 мс. То есть: после периода восстановления 1000мс от последнего звонка.
…И он получит аргументы самого последнего вызова, остальные вызовы игнорируются.
Вот его код (используется декоратор debounce из библиотеки Lodash):
пусть f = _.debounce(alert, 1000); е("а"); setTimeout( () => f("b"), 200); setTimeout( () => f("c"), 500); // функция с устранением дребезга ждет 1000 мс после последнего вызова, а затем запускается: alert("c")
Теперь практический пример. Допустим, пользователь что-то вводит, и мы хотели бы отправить запрос на сервер, когда ввод будет завершен.
Нет смысла отправлять запрос на каждый набранный символ. Вместо этого мы хотели бы подождать, а затем обработать весь результат.
В веб-браузере мы можем настроить обработчик событий — функцию, которая вызывается при каждом изменении поля ввода. Обычно обработчик событий вызывается очень часто для каждого набранного ключа. Но если мы debounce
дребезг на 1000 мс, то он будет вызван только один раз, через 1000 мс после последнего ввода.
В этом живом примере обработчик помещает результат в поле ниже, попробуйте:
Видеть? Второй ввод вызывает функцию устранения дребезга, поэтому ее содержимое обрабатывается через 1000 мс с момента последнего ввода.
Итак, debounce
— отличный способ обработки последовательности событий: будь то последовательность нажатий клавиш, движений мыши или чего-то еще.
Он ждет заданное время после последнего вызова, а затем запускает свою функцию, которая может обработать результат.
Задача — реализовать декоратор debounce
.
Подсказка: это всего лишь несколько строк, если вдуматься :)
Откройте песочницу с тестами.
функция debounce(func, мс) { дать тайм-аут; функция возврата() { ClearTimeout (тайм-аут); timeout = setTimeout(() => func.apply(this, аргументы), мс); }; }
Вызов debounce
возвращает оболочку. При вызове он планирует исходный вызов функции через заданную ms
и отменяет предыдущий такой тайм-аут.
Откройте решение с тестами в песочнице.
важность: 5
Создайте «регулирующий» декоратор throttle(f, ms)
— который возвращает обертку.
Когда он вызывается несколько раз, он передает вызов f
максимум один раз в ms
.
По сравнению с декоратором debounce поведение совершенно иное:
debounce
запускает функцию один раз после периода «перезарядки». Подходит для обработки конечного результата.
throttle
запускает его не чаще, чем заданное время ms
. Подходит для регулярных обновлений, которые не должны быть очень частыми.
Другими словами, throttle
похож на секретаря, который принимает телефонные звонки, но беспокоит начальника (звонит реальному f
) не чаще, чем раз в ms
миллисекунды.
Давайте проверим реальное приложение, чтобы лучше понять это требование и понять, откуда оно взялось.
Например, мы хотим отслеживать движения мыши.
В браузере мы можем настроить функцию, которая будет запускаться при каждом движении мыши и получать местоположение указателя при его движении. При активном использовании мыши эта функция обычно запускается очень часто, примерно 100 раз в секунду (каждые 10 мс). Мы хотели бы обновлять некоторую информацию на веб-странице при перемещении указателя.
…Но обновлять функцию update()
слишком сложно, чтобы делать это при каждом микродвижении. Обновляться чаще, чем раз в 100мс, тоже нет смысла.
Итак, мы обернем его в декоратор: используйте throttle(update, 100)
в качестве функции, которая будет запускаться при каждом движении мыши вместо исходного update()
. Декоратор будет вызываться часто, но вызов update()
пересылается не чаще одного раза в 100 мс.
Визуально это будет выглядеть так:
При первом движении мыши декорированный вариант сразу передает вызов update
. Это важно, пользователь сразу видит нашу реакцию на его шаг.
Затем, когда мышь движется дальше, до 100ms
ничего не происходит. Декорированный вариант игнорирует вызовы.
По истечении 100ms
происходит еще одно update
последних координат.
И вот, наконец, мышь где-то останавливается. Декорированный вариант ждет, пока истечет 100ms
, а затем запускает update
с последними координатами. Итак, что очень важно, обрабатываются окончательные координаты мыши.
Пример кода:
функция f(a) { console.log(а); } // f1000 передает вызовы f максимум один раз в 1000 мс пусть f1000 = дроссель(f, 1000); ф1000(1); // показывает 1 ф1000(2); // (регулирование, 1000 мс еще не истекло) ф1000(3); // (регулирование, 1000 мс еще не истекло) // когда истекает время 1000 мс... // ...выводит 3, промежуточное значение 2 игнорируется
PS Аргументы и контекст this
в f1000
должны быть переданы в исходный f
.
Откройте песочницу с тестами.
функция дросселя(func, мс) { пусть isThrottled = ложь, сохраненныеArgs, сохраненоЭто; функция-обертка() { если (isThrottled) { // (2) saveArgs = аргументы; сохраненоЭто = это; возвращаться; } isThrottled = правда; func.apply(это, аргументы); // (1) setTimeout(функция() { isThrottled = ложь; // (3) если (savedArgs) { обертка.apply(savedThis, saveArgs); saveArgs = saveThis = null; } }, РС); } возвратная обертка; }
Вызов throttle(func, ms)
возвращает wrapper
.
Во время первого вызова wrapper
просто запускает func
и устанавливает состояние восстановления ( isThrottled = true
).
В этом состоянии все вызовы сохраняются в savedArgs/savedThis
. Обратите внимание, что и контекст, и аргументы одинаково важны и их следует запомнить. Они нужны нам одновременно, чтобы воспроизвести вызов.
По прошествии ms
миллисекунд срабатывает setTimeout
. Состояние перезарядки удаляется ( isThrottled = false
) и, если мы игнорировали вызовы, wrapper
выполняется с последними запомненными аргументами и контекстом.
На третьем шаге запускается не func
, а wrapper
, потому что нам нужно не только выполнить func
, но еще раз войти в состояние восстановления и настроить таймаут для его сброса.
Откройте решение с тестами в песочнице.