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

Junior: питання на співбесіді з теми «Масиви й об'єкти»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

4 питання

Усі обходять масив і викликають функцію для кожного елемента:

  • 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

Об'єкти й масиви в змінних зберігаються за посиланням. const b = a - це не копія, а друге ім'я для того самого об'єкта.

Поверхнева копія копіює лише перший рівень. Вкладені об'єкти лишаються спільними:

const user = { name: 'Оля', address: { city: 'Київ' } };
const copy = { ...user };       // або Object.assign({}, user)

copy.name = 'Іра';              // user.name не змінився
copy.address.city = 'Львів';    // user.address.city теж став 'Львів'!

Глибока копія копіює всі рівні:

const deep = structuredClone(user);
deep.address.city = 'Одеса';    // user не зачеплено

structuredClone (вбудований у браузери й Node.js 17+) коректно копіює Date, Map, Set, вкладені масиви й навіть циклічні посилання. Не копіює функції, DOM-вузли й прототипи класів: екземпляр класу стане звичайним об'єктом.

Старий прийом JSON.parse(JSON.stringify(obj)) має купу обмежень: Date перетворюється на рядок, undefined, функції й Map зникають, циклічні посилання дають помилку.

На практиці глибока копія потрібна рідше, ніж здається: у React, Vue чи Redux змінений стан зазвичай будують поверхневими копіями лише тих гілок, що змінилися, - це й дешевше, і дозволяє швидко порівнювати посилання.

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

Метод залежить від того, що шукаємо і що потрібно отримати.

includes(value) - чи є значення в масиві, true/false. Для примітивів (рядки, числа):

const roles = ['admin', 'editor'];
roles.includes('admin');   // true

indexOf(value) - позиція першого входження або -1. Колись це був єдиний спосіб перевірки (indexOf(x) !== -1), тепер для перевірки є includes. Відмінність у краєвому випадку: includes знаходить NaN, indexOf - ні.

some(predicate) - чи є хоча б один елемент, що задовольняє умову. Для об'єктів, де шукаємо за властивістю:

const users = [{ id: 1, active: false }, { id: 2, active: true }];
users.some((user) => user.active);   // true

find(predicate) - сам елемент (перший відповідний) або undefined; findIndex - його позиція або -1. Є й варіанти з кінця: findLast, findLastIndex.

const user = users.find((user) => user.id === 2);

every(predicate) - протилежність some: чи всі елементи задовольняють умову.

Типова помилка - includes з об'єктами:

users.includes({ id: 1, active: false });   // false

Об'єкти порівнюються за посиланням, а не за вмістом. Новий об'єкт з тими самими полями - інший об'єкт. Для об'єктів використовують some/find з умовою.

Продуктивність. Усі ці методи перебирають масив, тобто працюють за O(n). Якщо перевірок багато (наприклад, у циклі по іншому масиву), краще один раз побудувати Set чи Map:

const allowed = new Set(roles);
items.filter((item) => allowed.has(item.role));   // has - O(1)

Докладніше в документації: Array.prototype.includes()

Object.keys, Object.values, Object.entries повертають масиви власних перелічуваних властивостей об'єкта:

const prices = { coffee: 50, tea: 35 };

Object.keys(prices);     // ['coffee', 'tea']
Object.values(prices);   // [50, 35]
Object.entries(prices);  // [['coffee', 50], ['tea', 35]]

for (const [name, price] of Object.entries(prices)) {
  console.log(`${name}: ${price} грн`);
}

Оскільки це масиви, до них можна застосувати map, filter, reduce. А назад у об'єкт - через Object.fromEntries:

const withVat = Object.fromEntries(
  Object.entries(prices).map(([name, price]) => [name, price * 1.2]),
);

for...in перебирає ключі, але разом з успадкованими перелічуваними властивостями з ланцюжка прототипів:

for (const key in prices) {
  if (Object.hasOwn(prices, key)) { /* ... */ }
}

Для звичайних об'єктів успадкованих перелічуваних властивостей зазвичай немає, але якщо якась бібліотека додала метод в Object.prototype, for...in його побачить. Тому в сучасному коді частіше пишуть Object.entries + for...of.

Не плутати for...in і for...of:

  • for...in - ключі (для масиву - індекси як рядки '0', '1');
  • for...of - значення ітерованих об'єктів (масиви, рядки, Map, Set). Звичайний об'єкт не ітерований, for...of по ньому кине TypeError.

Порядок ключів визначений: спершу цілочисельні ключі за зростанням, потім рядкові в порядку додавання.

Object.keys({ b: 1, 2: 1, a: 1, 1: 1 });   // ['1', '2', 'b', 'a']

Це пастка для об'єктів, де ключами є id: { 10: ..., 2: ... } перебереться як 2, 10, а не в порядку додавання. Якщо порядок важливий, краще Map чи масив.

Символьні ключі жоден з цих способів не повертає - для них є Object.getOwnPropertySymbols чи Reflect.ownKeys.

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