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

JavaScript: Junior

20 питань · ~15 хв · Версія v3.0

Увійдіть, щоб продовжити

Базові знання JavaScript: типи, область видимості, функції, масиви, асинхронність.

За спробу
20
У пулі
100
Проходжень
0
Середній бал
-
Пройшли на 70%+
-

Питання для підготовки

37 питань

Promise - об'єкт-обіцянка результату, який з'явиться пізніше: відповідь сервера, прочитаний файл, таймер. Замість того щоб передавати колбек, функція одразу повертає Promise, а результат на нього «підписують».

Три стани:

  • pending - очікування, результату ще немає;
  • fulfilled - виконано успішно, є значення;
  • rejected - відхилено, є причина (зазвичай об'єкт Error).

Перехід з pending відбувається один раз і назавжди: виконаний Promise не стане відхиленим.

fetch('/api/user')
  .then((response) => response.json())
  .then((user) => console.log(user.name))
  .catch((error) => console.error('Не вдалося завантажити', error))
  .finally(() => hideSpinner());

Що варто знати:

  • then() повертає новий Promise, тому виклики можна з'єднувати в ланцюжок. Значення, повернене з then, стає результатом наступного кроку.
  • Один catch() наприкінці ловить помилку з будь-якого попереднього кроку.
  • Promise без обробника помилки, який відхилився, дає unhandledrejection - у Node.js це за замовчуванням завершує процес.
  • fetch відхиляє Promise лише при мережевій помилці. Відповідь 404 чи 500 - це виконаний Promise з response.ok === false.

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

По суті нічим: async/await - зручніший запис тих самих Promise. Асинхронний код читається зверху вниз, як синхронний.

// then()
function loadUser(id) {
  return fetch(`/api/users/${id}`)
    .then((r) => r.json())
    .then((user) => user.name);
}

// async/await
async function loadUser(id) {
  const response = await fetch(`/api/users/${id}`);
  const user = await response.json();
  return user.name;
}

Правила:

  • async-функція завжди повертає Promise. return 5 у ній - це Promise, що виконається з 5.
  • await чекає Promise і повертає його значення. Якщо Promise відхилено, await кидає виняток.
  • Помилки ловлять звичайним try/catch.
try {
  const user = await loadUser(1);
} catch (error) {
  showError(error.message);
}

Пастка - зайва послідовність:

const user = await loadUser();     // чекаємо
const posts = await loadPosts();   // і лише тоді починаємо друге

Якщо запити незалежні, їх запускають разом: const [user, posts] = await Promise.all([loadUser(), loadPosts()]);.

await на верхньому рівні (поза функцією) працює в ES-модулях.

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

setTimeout(fn, 0) означає не «виконай зараз», а «постав у чергу, щойно буде можливість». JavaScript у браузері виконується в одному потоці: поки працює поточний код, ніщо інше не запуститься.

console.log('1');
setTimeout(() => console.log('3'), 0);
Promise.resolve().then(() => console.log('2б'));
console.log('2');
// 1, 2, 2б, 3

Порядок такий:

  1. виконується весь синхронний код до кінця;
  2. виконуються всі мікрозадачі (колбеки Promise, queueMicrotask);
  3. браузер може перемалювати сторінку;
  4. береться наступна макрозадача - зокрема колбек таймера.

Тому колбек Promise спрацьовує раніше за таймер з нульовою затримкою.

Затримка - мінімальна, а не точна. setTimeout(fn, 100) гарантує лише, що колбек запуститься не раніше ніж за 100 мс. Якщо головний потік зайнятий важким обчисленням 2 секунди, таймер спрацює через 2 секунди.

Інші обмеження:

  • вкладені таймери після п'яти рівнів отримують мінімальну затримку 4 мс;
  • у фонових вкладках браузер пригальмовує таймери до разу на секунду і рідше - не можна покладатися на них для точного відліку часу;
  • для анімацій є requestAnimationFrame, синхронізований з перемальовуванням екрана.

Навіщо setTimeout(fn, 0) на практиці: відкласти роботу до моменту, коли поточна подія оброблена й DOM оновлено, або розбити довгу роботу на шматки, даючи браузеру перемальовувати сторінку між ними.

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

Колбек (функція зворотного виклику) - функція, яку передають іншій функції, щоб та викликала її пізніше:

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

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

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