Javascript set context this

Привязка контекста к функции

При передаче методов объекта в качестве колбэков, например для 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 всё время будет одинаков для нашей задачи, то мы можем создать частично применённую функцию и дальше работать с ней.

Источник

Читайте также:  Пример графиков на java
Оцените статью