Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

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(...).

Докладніше в документації: Array.prototype.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 - не використовувати.

Докладніше в документації: let

У звичайній функції 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 як елемент.

Докладніше в документації: this

Прочитати - ще не значить знати

20 питань, по одному на екран, ~20 хв. Після завершення - розбір кожної помилки з посиланням на питання.