W3docs

Декораторы и перенаправление вызовов в 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. Чтобы обернуть любую функцию, нужно перенаправить вызов.

Кеширующий декоратор

Распространённый реальный декоратор кеширует результаты, чтобы дорогостоящая функция выполнялась только один раз для каждого входного значения. Попробуйте:

javascript— editable

Это работает для обычной функции. Но как только slow становится методом, использующим this, вызов func(x) ломает его — обёртка теряет контекст объекта. Вот тут и нужны call и apply.

Перенаправление вызова: call и apply

call и apply оба вызывают функцию с явно заданным this. Они различаются только способом передачи аргументов:

  • func.call(thisArg, arg1, arg2, ...) — аргументы перечисляются по одному.
  • func.apply(thisArg, argsArray) — аргументы передаются в виде одного массива (или массивоподобного объекта).

call

javascript— editable

apply

javascript— editable

Эти два вызова эквивалентны:

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):

javascript— editable

Без func.call(this, x) вызов внутри обёртки был бы func(x), и this был бы потерян, поэтому this.someMethod() выбросило бы ошибку.

Перенаправление всех аргументов с помощью apply

Для метода с несколькими аргументами передаём все аргументы сразу. Обёртка не знает, сколько их, поэтому читает их из arguments и передаёт все через func.apply(this, arguments):

javascript— editable

Прямая передача 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 больше не будет указывать на него.

javascript— editable

Подробнее об исправлении контекста в обратных вызовах и о различии между bind, стрелочными функциями и call/apply можно прочитать в разделе Привязка функции.

Когда что использовать

ЗадачаИспользуйте
Вызвать немедленно с выбранным this, аргументы перечислены по отдельностиfunc.call(thisArg, a, b)
Вызвать немедленно с выбранным this, аргументы уже в массивеfunc.apply(thisArg, args)
Получить функцию для последующего вызова с зафиксированным thisfunc.bind(thisArg)
Переиспользовать встроенный метод на массивоподобном объектезаимствование: [].method.call(obj, …)

Заключение

Декораторы оборачивают функцию, добавляя поведение без её изменения. Чтобы обёртка работала с любой функцией — включая методы — нужно перенаправлять оригинальный вызов через func.call(this, ...) или func.apply(this, arguments), использовать bind при отложенном вызове и заимствовать встроенные методы, когда объект является лишь массивоподобным. Вместе эти приёмы дают переиспользуемые, контекстно-безопасные абстракции, например кеширующий декоратор из примеров выше.

Связанные разделы: Методы объекта, "this", Объект функции, NFE и Привязка функции.

Практика

Практика
Какие из утверждений точно описывают использование и различия между методами `call` и `apply` в JavaScript?
Какие из утверждений точно описывают использование и различия между методами `call` и `apply` в JavaScript?
Was this page helpful?