Передаючи методи об’єкту в якості колбеків, наприклад в setTimeout
, існує відома проблема: “втрата this
”.
В цьому розділі ми розглянемо способи як це можливо виправити.
Втрата “this”
Ми вже розглядали приклади втрати this
. Якщо метод передати окремо від об’єкта – this
втрачається.
В прикладі наведено як це відбувається з setTimeout
:
let user = {
firstName: "Іван",
sayHi() {
alert(`Привіт, ${this.firstName}!`);
}
};
setTimeout(user.sayHi, 1000); // Привіт, undefined!
Як ми бачимо, в модальному вікні браузера відображається не "Іван"
як this.firstName
, а undefined
!
Це тому що setTimeout
отримав функцію user.sayHi
окремо від об’єкта. Останній рядок може бути переписаний наступним чином:
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(`Привіт, ${this.firstName}!`);
}
};
setTimeout(function() {
user.sayHi(); // Привіт, Іван!
}, 1000);
Тепер це працює, бо ми отримали user
з зовнішнього лексичного оточення, і потім викликали метод.
Аналогічний запис, тільки коротший:
setTimeout(() => user.sayHi(), 1000); // Привіт, Іван!
Виглядає чудово, але з’являється легка вразливість в структурі нашого коду.
Що якщо перед спрацюванням setTimeout
(з однією секундою затримки!) змінна user
змінить своє значення? Тоді, неочікувано, наша обгортка викличе неправильний об’єкт!
let user = {
firstName: "Іван",
sayHi() {
alert(`Привіт, ${this.firstName}!`);
}
};
setTimeout(() => user.sayHi(), 1000);
// ...значення user змінюється впродовж 1 секунди
user = {
sayHi() { alert("Інший user в setTimeout!"); }
};
// Інший user в setTimeout!
Наступне рішення гарантує, що така ситуація не трапиться.
Рішення 2: прив’язка (bind)
Функції надають нам вбудований метод 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);
}
// прив’язка до user
let funcUser = func.bind(user);
funcUser("Привіт"); // Привіт, Іван (переданий аргумент "Привіт" та this=user)
Тепер спробуємо з методом об’єкту:
let user = {
firstName: "Іван",
sayHi() {
alert(`Привіт, ${this.firstName}!`);
}
};
let sayHi = user.sayHi.bind(user); // (*)
// можемо викликати без об’єкту
sayHi(); // Привіт, Іван!
setTimeout(sayHi, 1000); // Привіт, Іван!
// навіть якщо значення user зміниться впродовж 1 секунди
// sayHi використовує прив’язане значення, яке посилається на старий об’єкт user
user = {
sayHi() { alert("Інший user в setTimeout!"); }
};
В рядку (*)
ми взяли метод user.sayHi
та прив’язали його до user
. sayHi
є “прив’язаною” функцією, що може бути викликана окремо або передана до setTimeout
. Не важливо де вона буде викликана, контекст буде заданий нами.
В цьому прикладі ми бачимо, що аргументи передані “як є”, тільки this
змінено за допомогою bind
:
let user = {
firstName: "Іван",
say(phrase) {
alert(`${phrase}, ${this.firstName}!`);
}
};
let say = user.say.bind(user);
say("Привіт"); // Привіт, Іван! (Аргумент "Привіт" переданий функції say)
say("Бувай"); // Бувай, Іван! ("Бувай" передане функції say)
bindAll
Якщо об’єкт містить багато методів та ми плануємо активно їх передавати, тоді ми можемо прив’язати їх всіх за допомогою циклу:
for (let key in user) {
if (typeof user[key] == 'function') {
user[key] = user[key].bind(user);
}
}
JavaScript бібліотеки також пропонують функції для зручної масової прив’язки, наприклад _.bindAll(object, methodNames) в бібліотеці lodash.
Часткове застосування (partial function)
До цього моменту ми говорили тільки про прив’язку this
. Зробимо крок далі.
Ми можемо прив’язати не тільки this
, а також аргументи. Це робиться рідко, проте може бути інколи дуже зручним.
Повний синтаксис bind
:
let bound = func.bind(context, [arg1], [arg2], ...);
Це дозволяє прив’язати context як this
та початкові аргументи функції.
Наприклад, ми маємо функцію множення mul(a, b)
:
function mul(a, b) {
return 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
вимагає першим аргументом щось, що буде прив’язане як this
, тому ми мусимо передати щось як заглушку – 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: "Іван",
say(time, phrase) {
alert(`[${time}] ${this.firstName}: ${phrase}!`);
}
};
// та метод partial з закріпленим часом
user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes());
user.sayNow("Привіт");
// Something like:
// [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
.
Коли ми прив’язуємо деякі аргументи існуючої функції, в результаті ми отримуємо нову (менш універсальну) функцію, яка називається частково застосованою або частковою (partial).
Часткові функції зручні, коли ми не хочемо повторно передавати одні й ті ж самі аргументи знову і знову. Наприклад, якщо ми маємо функцію send(from, to)
та аргумент from
постійно буде однаковим для нашої поточної задачі, ми можемо зробити частково застосовану функцію та використовувати її.
Коментарі
<code>
, для кількох рядків – обгорніть їх тегом<pre>
, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)