JavaScript: функції, замикання й this
20 питань · ~20 хв · Версія v3.0
Увійдіть, щоб продовжити
Область видимості й замикання, this і стрілкові функції, bind/call/apply, прототипи й класи - питання всіх рівнів, від junior до senior.
- За спробу
- 20
- У пулі
- 82
- Проходжень
- 0
- Середній бал
- -
- Пройшли на 70%+
- -
Питання для підготовки
20 питаньКолбек (функція зворотного виклику) - функція, яку передають іншій функції, щоб та викликала її пізніше:
button.addEventListener('click', () => console.log('клік'));
[1, 2, 3].map((n) => n * 2); // теж колбек, але синхронний
До появи Promise асинхронний код писали лише колбеками: «зроби запит і, коли буде відповідь, виклич цю функцію».
Пекло колбеків з'являється, коли кроки залежать один від одного:
getUser(id, (error, user) => {
if (error) return showError(error);
getOrders(user.id, (error, orders) => {
if (error) return showError(error);
getInvoice(orders[0].id, (error, invoice) => {
if (error) return showError(error);
render(invoice);
});
});
});
Проблеми:
- код росте вправо і погано читається;
- помилку треба перевіряти на кожному рівні окремо - легко пропустити;
- немає простого способу запустити кілька операцій паралельно й дочекатися всіх;
- функція може викликати колбек двічі чи жодного разу - і нічого вас від цього не захистить.
Те саме з Promise і async/await:
try {
const user = await getUser(id);
const orders = await getOrders(user.id);
render(await getInvoice(orders[0].id));
} catch (error) {
showError(error);
}
Код читається зверху вниз, одна обробка помилок на всі кроки, а Promise гарантовано виконується чи відхиляється рівно один раз.
Колбеки нікуди не зникли: обробники подій, методи масивів, setTimeout - це колбеки, і для них вони доречні. Від колбеків відмовилися саме для одноразових асинхронних результатів.
Усі обходять масив і викликають функцію для кожного елемента:
map- новий масив тієї ж довжини з перетвореними елементами.filter- новий масив лише з тими елементами, для яких функція повернула істинне значення.reduce- згортає масив в одне значення: суму, об'єкт, інший масив.forEach- нічого не повертає (undefined), лише виконує дію для кожного елемента.
const orders = [{ total: 100, paid: true }, { total: 250, paid: false }, { total: 40, paid: true }];
orders.map((o) => o.total); // [100, 250, 40]
orders.filter((o) => o.paid); // два оплачені
orders.reduce((sum, o) => sum + o.total, 0); // 390
orders.forEach((o) => console.log(o.total)); // лише побічна дія
Типові помилки:
mapзаради побічної дії, без використання результату. Для цього єforEachчиfor...of.reduceбез початкового значення на порожньому масиві кидаєTypeError.- Перервати
forEachнеможливо (breakне працює). Якщо треба зупинитися, -for...of,some()чиfind().
Усі чотири не змінюють початковий масив (хіба що сам колбек зробить це навмисно), тож їх зручно з'єднувати в ланцюжок: orders.filter(...).map(...).
Замикання - це функція разом зі змінними того місця, де її створено. Внутрішня функція «пам'ятає» змінні зовнішньої навіть після того, як зовнішня вже завершилася.
function makeCounter() {
let count = 0;
return () => ++count;
}
const next = makeCounter();
next(); // 1
next(); // 2
// count недоступний ззовні, але живе, поки живе функція next
Де замикання трапляються щодня:
- Обробники подій і колбеки - вони бачать змінні з коду, що їх створив.
- Приватний стан - як
countвище: змінити його можна лише через функцію. - Фабрики функцій -
const add5 = makeAdder(5). - Хуки React - кожен рендер створює нові функції, що замикають значення стану саме цього рендеру.
Класична пастка з var у циклі:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i)); // 3, 3, 3
}
var має одну змінну на всю функцію, і до моменту спрацювання таймерів вона вже дорівнює 3. З let у кожної ітерації своя змінна, і виведеться 0, 1, 2.
Нюанс для пам'яті: поки замикання живе (наприклад, обробник не знято), живуть і всі змінні, які воно використовує, разом з великими об'єктами в них.
var |
let |
const |
|
|---|---|---|---|
| Область видимості | функція | блок {} |
блок {} |
| Повторне присвоєння | так | так | ні |
| Повторне оголошення | так | ні | ні |
| Доступ до оголошення | undefined |
помилка (TDZ) | помилка (TDZ) |
if (true) {
var a = 1;
let b = 2;
}
a; // 1 - var «протікає» з блоку
b; // ReferenceError
Підняття (hoisting): оголошення всіх трьох обробляються до виконання коду, але по-різному. var одразу отримує undefined, тож звернення до нього до рядка оголошення не дає помилки - лише тихий undefined. let і const до оголошення перебувають у «тимчасовій мертвій зоні» (TDZ): звернення кидає ReferenceError. Друге поведінка краща: помилка видна одразу.
const не робить значення незмінним - лише забороняє переприсвоїти змінну:
const user = { name: 'Оля' };
user.name = 'Іра'; // можна: змінюємо вміст об'єкта
user = {}; // TypeError
Для незмінності вмісту - Object.freeze() (поверхнево).
Сучасне правило: const за замовчуванням, let - коли значення справді змінюється, var - не використовувати.
У звичайній функції this визначається в момент виклику, а не там, де функцію написано:
obj.method()-thisцеobj;- просто
fn()-undefinedу строгому режимі (у модулях і класах він завжди строгий), глобальний об'єкт - у нестрогому; new Fn()- новий об'єкт;fn.call(ctx),fn.apply(ctx),fn.bind(ctx)- явно заданийctx;- в обробнику
addEventListener- елемент, на якому висить обробник.
Типова пастка - метод, переданий як колбек:
class Timer {
seconds = 0;
tick() { this.seconds++; }
}
const timer = new Timer();
setInterval(timer.tick, 1000); // this === undefined -> TypeError
Метод «відірвався» від об'єкта, і викликається вже як просто функція.
Стрілкова функція не має власного this - вона бере this з місця, де її створено (лексично), і змінити його не можна навіть через call чи bind:
setInterval(() => timer.tick(), 1000); // працює
// або поле-стрілка в класі:
class Timer2 { seconds = 0; tick = () => { this.seconds++; }; }
Ще відмінності стрілкових функцій: немає власного arguments, їх не можна викликати через new, вони не мають prototype.
Коли НЕ стрілка: методи об'єкта, яким потрібен this цього об'єкта ({ name: 'x', greet: () => this.name } не працює), і обробники, де потрібен this як елемент.
Спершу почитати
Прочитати - ще не значить знати
20 питань, по одному на екран, ~20 хв. Після завершення - розбір кожної помилки з посиланням на питання.