Декораторы и перенаправление вызовов в JavaScript: call и apply
Декораторы (обёртки) в JavaScript, перенаправление вызовов через func.call и func.apply, кеширующий декоратор, bind и заимствование методов.
Декоратор — это функция-обёртка: она принимает другую функцию и возвращает новую, которая добавляет поведение — логирование, кеширование, измерение времени, проверку прав — вокруг оригинала, не затрагивая его код. Чтобы создавать декораторы, работающие с любой функцией, нужен надёжный способ вызвать функцию с нужным this и нужным набором аргументов. Именно это и предоставляют func.call и func.apply.
Эта глава посвящена декораторам (функциям-обёрткам), перенаправлению this и аргументов через call/apply, восстановлению потерянного контекста с помощью bind и заимствованию методов.
Примечание: Речь идёт о функциональных декораторах — распространённом паттерне, доступном в обычном JavaScript сегодня. Более новые классовые декораторы с префиксом
@— это отдельная, более продвинутая возможность (на данный момент предложение Stage 3, требующее транспилятора), и здесь они не рассматриваются.
Что такое декоратор
Декоратор — это функция, которая оборачивает целевую функцию и возвращает замену с дополнительным поведением. Поскольку обёртка имеет тот же внешний вид, вызывающему коду ничего менять не нужно.
function sum(a, b) {
return a + b;
}
function logged(func) {
return function (a, b) {
console.log(`calling with ${a}, ${b}`);
return func(a, b);
};
}
const loggedSum = logged(sum);
console.log(loggedSum(2, 3));
// calling with 2, 3
// 5Обёртка переиспользуема, сохраняет оригинал нетронутым и может комбинироваться. Ограничение приведённого примера в том, что он работает только с функцией, принимающей ровно два аргумента и не использующей this. Чтобы обернуть любую функцию, нужно перенаправить вызов.
Кеширующий декоратор
Распространённый реальный декоратор кеширует результаты, чтобы дорогостоящая функция выполнялась только один раз для каждого входного значения. Попробуйте:
Это работает для обычной функции. Но как только slow становится методом, использующим this, вызов func(x) ломает его — обёртка теряет контекст объекта. Вот тут и нужны call и apply.
Перенаправление вызова: call и apply
call и apply оба вызывают функцию с явно заданным this. Они различаются только способом передачи аргументов:
func.call(thisArg, arg1, arg2, ...)— аргументы перечисляются по одному.func.apply(thisArg, argsArray)— аргументы передаются в виде одного массива (или массивоподобного объекта).
call
apply
Эти два вызова эквивалентны:
func.call(obj, 1, 2, 3);
func.apply(obj, [1, 2, 3]);Используйте call, когда аргументы известны по отдельности; используйте apply, когда они уже находятся в массиве. С синтаксисом spread (func.call(obj, ...args)) различие часто исчезает — см. Остаточные параметры и синтаксис spread.
Перенаправление this с помощью call
Теперь можно исправить кеширующий декоратор для методов. Внутри обёртки this — это объект, на котором был вызван метод, поэтому мы передаём его с помощью func.call(this, x):
Без func.call(this, x) вызов внутри обёртки был бы func(x), и this был бы потерян, поэтому this.someMethod() выбросило бы ошибку.
Перенаправление всех аргументов с помощью apply
Для метода с несколькими аргументами передаём все аргументы сразу. Обёртка не знает, сколько их, поэтому читает их из arguments и передаёт все через func.apply(this, arguments):
Прямая передача this и arguments называется перенаправлением вызова: обёртка ведёт себя в точности как оригинал, но с дополнительной логикой вокруг него.
Заимствование метода
Функция hash выше использует один приём. arguments — массивоподобный объект (у него есть индексы и length), но он не является настоящим массивом, поэтому у него нет join. Вместо конвертации мы заимствуем метод массива:
function hash(args) {
return [].join.call(args, ',');
}
console.log(hash([3, 5])); // "3,5"[].join — это Array.prototype.join. Вызов его с args в качестве this выполняет логику join на массивоподобном значении. Заимствование методов позволяет повторно использовать встроенные методы на объектах, не являющихся массивами.
bind и потеря контекста
call и apply вызывают функцию немедленно. bind же возвращает новую функцию с постоянно зафиксированным this — это удобно, когда вызов происходит позже (обратный вызов, обработчик события, setTimeout).
Проблема, которую решает bind — это потеря контекста: отсоедините метод от его объекта, и this больше не будет указывать на него.
Подробнее об исправлении контекста в обратных вызовах и о различии между bind, стрелочными функциями и call/apply можно прочитать в разделе Привязка функции.
Когда что использовать
| Задача | Используйте |
|---|---|
Вызвать немедленно с выбранным this, аргументы перечислены по отдельности | func.call(thisArg, a, b) |
Вызвать немедленно с выбранным this, аргументы уже в массиве | func.apply(thisArg, args) |
Получить функцию для последующего вызова с зафиксированным this | func.bind(thisArg) |
| Переиспользовать встроенный метод на массивоподобном объекте | заимствование: [].method.call(obj, …) |
Заключение
Декораторы оборачивают функцию, добавляя поведение без её изменения. Чтобы обёртка работала с любой функцией — включая методы — нужно перенаправлять оригинальный вызов через func.call(this, ...) или func.apply(this, arguments), использовать bind при отложенном вызове и заимствовать встроенные методы, когда объект является лишь массивоподобным. Вместе эти приёмы дают переиспользуемые, контекстно-безопасные абстракции, например кеширующий декоратор из примеров выше.
Связанные разделы: Методы объекта, "this", Объект функции, NFE и Привязка функции.