Передаючи методи об’єкту в якості колбеків, наприклад в 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, що дозволяє зберегти правильний 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.
Часткове застосування
До цього моменту ми говорили тільки про прив’язку 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
вимагає цього, тому ми маємо передати щось як заглушку – 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)
повертає a “прив’язаний варіант” функції func
який виправляє контекст this
та аргументи, якщо вони передані.
Зазвичай ми застосовуємо bind
, щоб виправити this
для методу об’єкта та передати його деінде. Наприклад, в setTimeout
.
Коли ми виправляємо деякі аргументи існуючої функції, в результаті ми отримуємо (менш універсальну) функцію, яка називається частково застосованою або частковою.
Часткові функції зручні, коли ми не хочемо повторно передавати одні й ті ж самі аргументи знову і знову. Наприклад, якщо ми маємо функцію send(from, to)
та аргумент from
постійно буде однаковим для нашої поточної задачі, ми можемо зробити частково застосовану функцію та використовувати її.
Коментарі
<code>
, для кількох рядків – обгорніть їх тегом<pre>
, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)