При передаче методов объекта в качестве обратных вызовов, например, в setTimeout
, возникает известная проблема: «потеря this
».
В этой главе мы увидим способы исправить это.
Мы уже видели примеры потери this
. Если метод передается куда-то отдельно от объекта — this
теряется.
Вот как это может произойти с setTimeout
:
пусть пользователь = { Имя: «Джон», сказатьПривет() { alert(`Привет, ${this.firstName}!`); } }; setTimeout(user.sayHi, 1000); // Привет, неопределенный!
Как мы видим, в выводе отображается не «Джон» как this.firstName
, а undefined
!
Это потому, что setTimeout
получил функцию user.sayHi
отдельно от объекта. Последнюю строку можно переписать так:
пусть f = user.sayHi; setTimeout (ф, 1000); // потерян пользовательский контекст
Метод setTimeout
в браузере немного особенный: он устанавливает this=window
для вызова функции (для Node.js this
становится объектом таймера, но здесь это не имеет большого значения). Итак, для this.firstName
он пытается получить window.firstName
, которого не существует. В других подобных случаях обычно this
просто становится undefined
.
Задача вполне типичная — мы хотим передать метод объекта куда-то ещё (здесь — в планировщик), где он будет вызываться. Как убедиться, что он будет вызываться в правильном контексте?
Самое простое решение — использовать функцию переноса:
пусть пользователь = { Имя: «Джон», сказатьПривет() { alert(`Привет, ${this.firstName}!`); } }; setTimeout(функция() { пользователь.sayHi(); // Привет, Джон! }, 1000);
Теперь это работает, потому что получает user
из внешнего лексического окружения, а затем нормально вызывает метод.
То же самое, но короче:
setTimeout(() => user.sayHi(), 1000); // Привет, Джон!
Выглядит нормально, но в структуре нашего кода появляется небольшая уязвимость.
Что, если до того, как сработает setTimeout
(задержка составляет одну секунду!), user
изменит значение? И вдруг он вызовет не тот объект!
пусть пользователь = { Имя: «Джон», сказатьПривет() { alert(`Привет, ${this.firstName}!`); } }; setTimeout(() => user.sayHi(), 1000); // ...значение пользователя меняется в течение 1 секунды пользователь = { SayHi() { alert("Другой пользователь в setTimeout!"); } }; // Другой пользователь в setTimeout!
Следующее решение гарантирует, что такого не произойдет.
Функции предоставляют встроенную привязку метода, которая позволяет this
исправить.
Основной синтаксис:
// более сложный синтаксис появится чуть позже пустьboundFunc = func.bind(контекст);
Результатом func.bind(context)
является специальный «экзотический объект», подобный функции, который вызывается как функция и прозрачно передает вызов func
устанавливая this=context
.
Другими словами, boundFunc
аналогичен вызову func
с фиксированным this
.
Например, здесь funcUser
передает вызов func
с помощью this=user
:
пусть пользователь = { Имя: «Джон» }; функция func() { оповещение(this.firstName); } пусть funcUser = func.bind(пользователь); funcПользователь(); // Джон
Здесь func.bind(user)
— «связанный вариант» func
с фиксированным this=user
.
Все аргументы передаются исходной func
«как есть», например:
пусть пользователь = { Имя: «Джон» }; функция func(фраза) { alert(phrase + ', ' + this.firstName); } // привязываем это к пользователю пусть funcUser = func.bind(пользователь); funcUser("Привет"); // Привет, Джон (передан аргумент «Hello», и this=user)
Теперь давайте попробуем использовать метод объекта:
пусть пользователь = { Имя: «Джон», сказатьПривет() { alert(`Привет, ${this.firstName}!`); } }; пусть SayHi = user.sayHi.bind(пользователь); // (*) // можем запустить его без объекта сказатьПривет(); // Привет, Джон! setTimeout (скажем Привет, 1000); // Привет, Джон! // даже если значение пользователя изменится в течение 1 секунды // SayHi использует предварительно привязанное значение, которое является ссылкой на старый объект пользователя пользователь = { SayHi() { alert("Другой пользователь в setTimeout!"); } };
В строке (*)
мы берем метод user.sayHi
и привязываем его к user
. sayHi
— это «связанная» функция, которую можно вызвать отдельно или передать в setTimeout
— не имеет значения, контекст будет правильным.
Здесь мы видим, что аргументы передаются «как есть», только this
исправляется bind
:
пусть пользователь = { Имя: «Джон», сказать (фраза) { alert(`${phrase}, ${this.firstName}!`); } }; пусть говорят = user.say.bind(пользователь); сказать("Привет"); // Привет, Джон! (аргумент «Привет» передается, чтобы сказать) сказать("Пока"); // Пока, Джон! («Пока» передается, чтобы сказать)
Удобный метод: bindAll
Если у объекта много методов и мы планируем его активно передавать, то мы можем связать их все в цикле:
for (введите пользователя) { if (typeof user[key] == 'функция') { пользователь[ключ] = пользователь[ключ].bind(пользователь); } }
Библиотеки JavaScript также предоставляют функции для удобной массовой привязки, например _.bindAll(object, MethodNames) в lodash.
До сих пор мы говорили только об this
. Давайте сделаем еще один шаг вперед.
Мы можем связать не только this
, но и аргументы. Это делается редко, но иногда может быть удобно.
Полный синтаксис bind
:
letbound = func.bind(context, [arg1], [arg2], ...);
Это позволяет связать контекст как this
и начальные аргументы функции.
Например, у нас есть функция умножения mul(a, b)
:
функция mul(a, b) { вернуть а * б; }
Давайте воспользуемся bind
, чтобы создать на ее основе функцию double
:
функция mul(a, b) { вернуть а * б; } пусть двойной = mul.bind(null, 2); предупреждение(двойной(3)); // = mul(2, 3) = 6 предупреждение(двойной(4)); // = mul(2, 4) = 8 предупреждение(двойной(5)); // = mul(2, 5) = 10
Вызов mul.bind(null, 2)
создает новую функцию double
, которая передает вызовы mul
, фиксируя null
в качестве контекста и 2
в качестве первого аргумента. Дальнейшие аргументы передаются «как есть».
Это называется применением частичной функции — мы создаем новую функцию, исправляя некоторые параметры существующей.
Обратите внимание, что на самом деле мы здесь this
не используем. Но для этого требуется bind
, поэтому мы должны указать что-то вроде null
.
Функция triple
в приведенном ниже коде утраивает значение:
функция mul(a, b) { вернуть а * б; } пусть тройка = mul.bind(null, 3); предупреждение(тройной(3)); // = mul(3, 3) = 9 предупреждение(тройной(4)); // = mul(3, 4) = 12 предупреждение(тройной(5)); // = mul(3, 5) = 15
Почему мы обычно создаем частичную функцию?
Преимущество в том, что мы можем создать независимую функцию с читаемым именем ( double
, triple
). Мы можем использовать его и не предоставлять первый аргумент каждый раз, поскольку это исправлено с помощью bind
.
В других случаях частичное применение полезно, когда у нас есть очень общая функция и для удобства нам нужен менее универсальный ее вариант.
Например, у нас есть функция send(from, to, text)
. Затем внутри объекта user
мы можем использовать его частичный вариант: sendTo(to, text)
который отправляет данные от текущего пользователя.
Что, если мы хотим исправить некоторые аргументы, но не контекст this
? Например, для метода объекта.
Родная bind
этого не позволяет. Мы не можем просто опустить контекст и перейти к аргументам.
К счастью, можно легко реализовать partial
функции для связывания только аргументов.
Так:
функция частичная (функ, ...argsBound) { return function(...args) { // (*) return func.call(this, ...argsBound, ...args); } } // Использование: пусть пользователь = { Имя: «Джон», сказать(время, фраза) { alert(`[${time}] ${this.firstName}: ${phrase}!`); } }; // добавляем частичный метод с фиксированным временем user.sayNow = parts(user.say, new Date().getHours() + ':' + new Date().getMinutes()); user.sayNow("Привет"); // Что-то вроде: // [10:00] Джон: Привет!
Результатом вызова partial(func[, arg1, arg2...])
является оболочка (*)
, которая вызывает func
с помощью:
То же this
, что и есть (для user.sayNow
назовите его user
)
Затем выдает ...argsBound
— аргументы partial
вызова ( "10:00"
)
Затем передает ...args
— аргументы, переданные обертке ( "Hello"
)
Так легко это сделать с помощью синтаксиса распространения, не так ли?
Также есть готовая реализация _.partial из библиотеки lodash.
Метод func.bind(context, ...args)
возвращает «связанный вариант» функции func
который фиксирует контекст this
и первые аргументы, если они заданы.
Обычно мы применяем bind
, чтобы исправить this
для метода объекта, чтобы мы могли передать его куда-нибудь. Например, чтобы setTimeout
.
Когда мы фиксируем некоторые аргументы существующей функции, результирующая (менее универсальная) функция называется частично прикладной или частичной .
Частичные аргументы удобны, когда мы не хотим повторять один и тот же аргумент снова и снова. Например, если у нас есть функция send(from, to)
, и from
всегда должна быть одинаковой для нашей задачи, мы можем получить частичную функцию и продолжить ее.
важность: 5
Каков будет результат?
функция е() { предупреждение(это); // ? } пусть пользователь = { г: f.bind(ноль) }; пользователь.г();
Ответ: null
.
функция е() { предупреждение(это); // нулевой } пусть пользователь = { г: f.bind(ноль) }; пользователь.г();
Контекст связанной функции жестко фиксирован. Дальнейшего изменения просто нет.
Таким образом, даже когда мы запускаем user.g()
, исходная функция вызывается с this=null
.
важность: 5
Можем ли мы изменить this
с помощью дополнительной привязки?
Каков будет результат?
функция е() { оповещение(это.имя); } f = f.bind( {name: "Джон"} ).bind( {name: "Энн" } ); е();
Ответ: Джон .
функция е() { оповещение(это.имя); } f = f.bind( {name: "Джон"}).bind( {name: "Пит"} ); е(); // Джон
Объект экзотической связанной функции, возвращаемый f.bind(...)
запоминает контекст (и аргументы, если они предоставлены) только во время создания.
Функция не может быть перепривязана.
важность: 5
В свойстве функции есть значение. Изменится ли оно после bind
? Почему или почему бы и нет?
функция SayHi() { предупреждение(это.имя); } SayHi.test = 5; letbound =sayHi.bind({ имя: «Джон» }); оповещение (bound.test); // какой будет результат? почему?
Ответ: undefined
.
Результатом bind
является другой объект. Он не имеет свойства test
.
важность: 5
Вызов askPassword()
в приведенном ниже коде должен проверить пароль, а затем вызвать user.loginOk/loginFail
в зависимости от ответа.
Но это приводит к ошибке. Почему?
Исправьте выделенную строку, чтобы все заработало правильно (остальные строки менять нельзя).
функция AskPassword(ок, неудачно) { let пароль = приглашение("Пароль?", ''); if (пароль == "рок-звезда") ок(); иначе неудача(); } пусть пользователь = { имя: 'Джон', логинОК() { alert(`${this.name} вошел в систему`); }, логинФейл() { alert(`${this.name} не удалось войти в систему`); }, }; AskPassword(user.loginOk, user.loginFail);
Ошибка возникает из-за того, что askPassword
получает функции loginOk/loginFail
без объекта.
Когда он их вызывает, они, естественно, предполагают, this=undefined
.
Давайте bind
контекст:
функция AskPassword(ок, неудачно) { let пароль = приглашение("Пароль?", ''); if (пароль == "рок-звезда") ок(); иначе неудача(); } пусть пользователь = { имя: 'Джон', логинОК() { alert(`${this.name} вошел в систему`); }, логинФейл() { alert(`${this.name} не удалось войти в систему`); }, }; AskPassword(user.loginOk.bind(пользователь), user.loginFail.bind(пользователь));
Теперь это работает.
Альтернативным решением может быть:
//... AskPassword(() => user.loginOk(), () => user.loginFail());
Обычно это тоже работает и выглядит хорошо.
Однако это немного менее надежно в более сложных ситуациях, когда user
переменная может измениться после вызова askPassword
, но до того, как посетитель ответит и вызовет () => user.loginOk()
.
важность: 5
Задача представляет собой немного более сложный вариант исправления функции, которая теряет «это».
user
объект был изменен. Теперь вместо двух функций loginOk/loginFail
у него есть одна функция user.login(true/false)
.
Что нам следует передать в приведенном ниже коде askPassword
, чтобы он вызывал user.login(true)
как ok
, а user.login(false)
как fail
?
функция AskPassword(ок, неудачно) { let пароль = приглашение("Пароль?", ''); if (пароль == "рок-звезда") ок(); иначе неудача(); } пусть пользователь = { имя: 'Джон', вход (результат) { alert( this.name + (результат? 'вошел в систему': 'не удалось войти в систему')); } }; спроситьПароль(?, ?); // ?
Ваши изменения должны изменить только выделенный фрагмент.
Либо используйте функцию-обертку, если быть кратким, стрелку:
AskPassword(() => user.login(true), () => user.login(false));
Теперь он получает user
из внешних переменных и запускает его обычным способом.
Или создайте частичную функцию из user.login
, которая использует user
в качестве контекста и имеет правильный первый аргумент:
AskPassword(user.login.bind(пользователь, правда), user.login.bind(пользователь, ложь));