Привязка контекста к функции
При передаче методов объекта в качестве колбэков, например для setTimeout , возникает известная проблема – потеря this .
В этой главе мы посмотрим, как её можно решить.
Потеря «this»
Мы уже видели примеры потери this . Как только метод передаётся отдельно от объекта – this теряется.
Вот как это может произойти в случае с setTimeout :
let user = < firstName: "Вася", sayHi() < alert(`Привет, $!`); > >; setTimeout(user.sayHi, 1000); // Привет, undefined!
При запуске этого кода мы видим, что вызов this.firstName возвращает не «Вася», а undefined !
Это произошло потому, что setTimeout получил функцию sayHi отдельно от объекта user (именно здесь функция и потеряла контекст). То есть последняя строка может быть переписана как:
let f = user.sayHi; setTimeout(f, 1000); // контекст user потеряли
Метод setTimeout в браузере имеет особенность: он устанавливает this=window для вызова функции (в Node.js this становится объектом таймера, но здесь это не имеет значения). Таким образом, для this.firstName он пытается получить window.firstName , которого не существует. В других подобных случаях this обычно просто становится undefined .
Задача довольно типичная – мы хотим передать метод объекта куда-то ещё (в этом конкретном случае – в планировщик), где он будет вызван. Как бы сделать так, чтобы он вызывался в правильном контексте?
Решение 1: сделать функцию-обёртку
Самый простой вариант решения – это обернуть вызов в анонимную функцию, создав замыкание:
let user = < firstName: "Вася", sayHi() < alert(`Привет, $!`); > >; setTimeout(function() < user.sayHi(); // Привет, Вася! >, 1000);
Теперь код работает корректно, так как объект user достаётся из замыкания, а затем вызывается его метод sayHi .
То же самое, только короче:
setTimeout(() => user.sayHi(), 1000); // Привет, Вася!
Выглядит хорошо, но теперь в нашем коде появилась небольшая уязвимость.
Что произойдёт, если до момента срабатывания setTimeout (ведь задержка составляет целую секунду!) в переменную user будет записано другое значение? Тогда вызов неожиданно будет совсем не тот!
let user = < firstName: "Вася", sayHi() < alert(`Привет, $!`); > >; setTimeout(() => user.sayHi(), 1000); // . в течение 1 секунды user = < sayHi() < alert("Другой пользователь в 'setTimeout'!"); >>; // Другой пользователь в 'setTimeout'!
Следующее решение гарантирует, что такого не случится.
Решение 2: привязать контекст с помощью bind
В современном JavaScript у функций есть встроенный метод bind, который позволяет зафиксировать this .
// полный синтаксис будет представлен немного позже let boundFunc = func.bind(context);
Результатом вызова func.bind(context) является особый «экзотический объект» (термин взят из спецификации), который вызывается как функция и прозрачно передаёт вызов в func , при этом устанавливая this=context .
Другими словами, вызов boundFunc подобен вызову func с фиксированным this .
Например, здесь funcUser передаёт вызов в func , фиксируя this=user :
let user = < firstName: "Вася" >; function func() < alert(this.firstName); >let funcUser = func.bind(user); funcUser(); // Вася
Здесь func.bind(user) – это «связанный вариант» func , с фиксированным this=user .
Все аргументы передаются исходному методу func как есть, например:
let user = < firstName: "Вася" >; function func(phrase) < alert(phrase + ', ' + this.firstName); >// привязка this к user let funcUser = func.bind(user); funcUser("Привет"); // Привет, Вася (аргумент "Привет" передан, при этом this = user)
Теперь давайте попробуем с методом объекта:
let user = < firstName: "Вася", sayHi() < alert(`Привет, $!`); > >; let sayHi = user.sayHi.bind(user); // (*) sayHi(); // Привет, Вася! setTimeout(sayHi, 1000); // Привет, Вася!
В строке (*) мы берём метод user.sayHi и привязываем его к user . Теперь sayHi – это «связанная» функция, которая может быть вызвана отдельно или передана в setTimeout (контекст всегда будет правильным).
Здесь мы можем увидеть, что bind исправляет только this , а аргументы передаются как есть:
let user = < firstName: "Вася", say(phrase) < alert(`$, $!`); > >; let say = user.say.bind(user); say("Привет"); // Привет, Вася (аргумент "Привет" передан в функцию "say") say("Пока"); // Пока, Вася (аргумент "Пока" передан в функцию "say")
Если у объекта много методов и мы планируем их активно передавать, то можно привязать контекст для них всех в цикле:
Некоторые JS-библиотеки предоставляют встроенные функции для удобной массовой привязки контекста, например _.bindAll(obj) в lodash.
Частичное применение
До сих пор мы говорили только о привязывании this . Давайте шагнём дальше.
Мы можем привязать не только this , но и аргументы. Это делается редко, но иногда может быть полезно.
let bound = func.bind(context, [arg1], [arg2], . );
Это позволяет привязать контекст this и начальные аргументы функции.
Например, у нас есть функция умножения mul(a, b) :
Давайте воспользуемся bind , чтобы создать функцию double на её основе:
function mul(a, b) < return a * b; >let double = mul.bind(null, 2); alert( double(3) ); // = mul(2, 3) = 6 alert( double(4) ); // = mul(2, 4) = 8 alert( double(5) ); // = mul(2, 5) = 10
Вызов mul.bind(null, 2) создаёт новую функцию double , которая передаёт вызов mul , фиксируя null как контекст, и 2 – как первый аргумент. Следующие аргументы передаются как есть.
Это называется частичное применение – мы создаём новую функцию, фиксируя некоторые из существующих параметров.
Обратите внимание, что в данном случае мы на самом деле не используем this . Но для bind это обязательный параметр, так что мы должны передать туда что-нибудь вроде null .
В следующем коде функция triple умножает значение на три:
function mul(a, b) < return a * b; >let triple = mul.bind(null, 3); alert( triple(3) ); // = mul(3, 3) = 9 alert( triple(4) ); // = mul(3, 4) = 12 alert( triple(5) ); // = mul(3, 5) = 15
Для чего мы обычно создаём частично применённую функцию?
Польза от этого в том, что возможно создать независимую функцию с понятным названием ( double , triple ). Мы можем использовать её и не передавать каждый раз первый аргумент, т.к. он зафиксирован с помощью bind .
В других случаях частичное применение полезно, когда у нас есть очень общая функция и для удобства мы хотим создать её более специализированный вариант.
Например, у нас есть функция send(from, to, text) . Потом внутри объекта user мы можем захотеть использовать её частный вариант: sendTo(to, text) , который отправляет текст от имени текущего пользователя.
Частичное применение без контекста
Что если мы хотим зафиксировать некоторые аргументы, но не контекст this ? Например, для метода объекта.
Встроенный bind не позволяет этого. Мы не можем просто опустить контекст и перейти к аргументам.
К счастью, легко создать вспомогательную функцию partial , которая привязывает только аргументы.
function partial(func, . argsBound) < return function(. args) < // (*) return func.call(this, . argsBound, . args); >> // использование: let user = < firstName: "John", say(time, phrase) < alert(`[$] $: $!`); > >; // добавляем частично применённый метод с фиксированным временем user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes()); user.sayNow("Hello"); // Что-то вроде этого: // [10:00] John: Hello!
Результатом вызова partial(func[, arg1, arg2. ]) будет обёртка (*) , которая вызывает func с:
- Тем же this , который она получает (для вызова user.sayNow – это будет user )
- Затем передаёт ей . argsBound – аргументы из вызова partial ( «10:00» )
- Затем передаёт ей . args – аргументы, полученные обёрткой ( «Hello» )
Благодаря оператору расширения . реализовать это очень легко, не правда ли?
Также есть готовый вариант _.partial из библиотеки lodash.
Итого
Метод bind возвращает «привязанный вариант» функции func , фиксируя контекст this и первые аргументы arg1 , arg2 …, если они заданы.
Обычно bind применяется для фиксации this в методе объекта, чтобы передать его в качестве колбэка. Например, для setTimeout .
Когда мы привязываем аргументы, такая функция называется «частично применённой» или «частичной».
Частичное применение удобно, когда мы не хотим повторять один и тот же аргумент много раз. Например, если у нас есть функция send(from, to) и from всё время будет одинаков для нашей задачи, то мы можем создать частично применённую функцию и дальше работать с ней.