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

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

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

4 питання

Ітератор - об'єкт з методом next(), який щоразу повертає { value, done }. Ітерований об'єкт має метод [Symbol.iterator](), що повертає ітератор.

Саме на цьому протоколі працюють for...of, spread ([...x]), деструктуризація, Array.from, Promise.all, конструктори Map і Set. Масиви, рядки, Map, Set, NodeList - ітеровані з коробки; звичайні об'єкти - ні.

Власний ітерований об'єкт найпростіше зробити генератором:

class Range {
  constructor(from, to) {
    this.from = from;
    this.to = to;
  }

  *[Symbol.iterator]() {
    for (let i = this.from; i <= this.to; i++) {
      yield i;
    }
  }
}

[...new Range(1, 5)];          // [1, 2, 3, 4, 5]
for (const n of new Range(1, 3)) console.log(n);

Навіщо це на практиці:

  • Ліниві послідовності: значення обчислюються лише тоді, коли їх просять. Можна описати навіть нескінченну послідовність і взяти з неї 10 елементів.
  • Однаковий інтерфейс для власних колекцій - їх можна передавати скрізь, де очікується ітероване.
  • Асинхронні ітератори (Symbol.asyncIterator, for await...of) - для потоків даних: сторінки API, рядки великого файлу, потік відповіді fetch.

Нові «ітераторні хелпери» (Iterator.prototype.map, filter, take, drop) дозволяють працювати з ітераторами ліниво, не перетворюючи їх на масив. Вони вже є в сучасних браузерах і Node.js 22+.

Докладніше в документації: Ітератори й генератори

Збирач сміття звільняє об'єкт, коли на нього немає посилань. Звичайний Map тримає свої ключі: поки об'єкт лежить у Map як ключ, його не звільнять, навіть якщо більше ніде він не потрібен. Звідси витоки пам'яті в кешах і реєстрах.

WeakMap тримає ключі слабко: запис не заважає збирачу сміття. Коли на об'єкт-ключ не лишилося інших посилань, запис зникає разом з ним.

const metadata = new WeakMap();

function track(element) {
  metadata.set(element, { clicks: 0, addedAt: Date.now() });
}
// DOM-елемент видалили зі сторінки й забули - запис у metadata зникне сам

Обмеження WeakMap: ключі - лише об'єкти (і незареєстровані символи), немає size, обходу й clear(). Інакше результат залежав би від того, коли саме пройшов збирач сміття.

Де застосовують:

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

WeakSet - те саме для множини: «чи бачили ми цей об'єкт».

WeakRef - слабке посилання на окремий об'єкт: ref.deref() повертає об'єкт або undefined, якщо його вже зібрано. Разом з FinalizationRegistry дозволяє дізнатися про збирання. Але момент збирання непередбачуваний, тож MDN прямо радить уникати WeakRef там, де можна без нього, і ніколи не будувати на ньому логіку, від якої залежить коректність.

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

const захищає лише змінну від переприсвоєння, але не вміст об'єкта. Для захисту самого об'єкта є три рівні:

Метод Додати властивість Видалити Змінити значення
Object.preventExtensions ні так так
Object.seal ні ні так
Object.freeze ні ні ні
const config = Object.freeze({ apiUrl: '/api', retries: 3 });
config.retries = 10;     // тихо ігнорується
delete config.apiUrl;    // тихо ігнорується
config.retries;          // 3

Тихо - лише в нестрогому режимі. У строгому режимі (а ES-модулі та класи завжди строгі) спроба змінити заморожений об'єкт кидає TypeError. Це добре: помилка видна одразу.

Заморожування поверхневе:

const settings = Object.freeze({ theme: 'dark', limits: { upload: 10 } });
settings.limits.upload = 1000;   // працює!

Вкладені об'єкти лишаються змінюваними. Для глибокого заморожування потрібна рекурсія:

function deepFreeze(object) {
  for (const value of Object.values(object)) {
    if (value && typeof value === 'object' && !Object.isFrozen(value)) {
      deepFreeze(value);
    }
  }
  return Object.freeze(object);
}

Інші нюанси:

  • масиви теж можна заморозити: push, sort, присвоєння елемента кидають помилку в строгому режимі;
  • Map, Set, Date заморожування не захищає: їхні дані зберігаються у внутрішніх слотах, а не у властивостях. Object.freeze(new Map()).set('a', 1) спрацює;
  • розморозити неможливо - лише створити змінювану копію ({ ...frozen }, structuredClone);
  • перевірки: Object.isFrozen, Object.isSealed, Object.isExtensible.

Де це корисно:

  • константи й конфігурація, що не повинні змінюватися випадково;
  • «перелічення» без TypeScript: const Status = Object.freeze({ Paid: 'paid', New: 'new' });
  • спільний стан, який мають змінювати лише через визначені функції - заморожування ловить випадкові мутації в розробці.

Що не варто робити: заморожувати все підряд «для іммутабельності». Реальну незмінність у застосунках зазвичай забезпечують підходом - нові об'єкти замість зміни старих (toSorted, spread), - а TypeScript з readonly ловить мутації ще на етапі компіляції без витрат під час виконання.

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

Без компаратора sort() перетворює елементи на рядки і порівнює їх за кодами символів UTF-16:

[10, 9, 1].sort();   // [1, 10, 9] - '10' < '9', бо '1' < '9'

Для чисел потрібна функція порівняння, що повертає від'ємне, нуль чи додатне число:

[10, 9, 1].sort((a, b) => a - b);        // [1, 9, 10]
users.sort((a, b) => b.age - a.age);     // за спаданням віку

Українські рядки. Порівняння за кодами UTF-16 ламає абетку: літери є, і, ї, ґ мають коди більші за я, бо в Unicode стоять поза основним блоком:

['яблуко', 'їжак', 'ґанок', 'гора', 'єнот'].sort();
// ['гора', 'яблуко', 'єнот', 'їжак', 'ґанок'] - неправильно

Правильно - Intl.Collator з українською локаллю:

const collator = new Intl.Collator('uk');
words.sort(collator.compare);
// ['гора', 'ґанок', 'єнот', 'їжак', 'яблуко']

users.sort((a, b) => collator.compare(a.name, b.name));

Корисні опції колатора:

  • numeric: true - «природне» сортування чисел у рядках: файл2 перед файл10;
  • sensitivity: 'base' - ігнорувати регістр і діакритику при порівнянні (зручно для пошуку збігів);
  • caseFirst: 'upper' - великі літери першими.

a.localeCompare(b, 'uk') дає той самий результат, але створює колатор при кожному виклику. Для сортування великих масивів один Intl.Collator значно швидший.

Інші факти про sort:

  • сортування змінює масив на місці і повертає той самий масив. Для копії - toSorted() (ES2023);
  • сортування стабільне (гарантовано з ES2019): елементи з однаковим ключем зберігають взаємний порядок. Тому сортування за кількома ключами можна робити послідовно, але простіше одним компаратором:
items.sort((a, b) => a.category.localeCompare(b.category, 'uk') || b.price - a.price);
  • компаратор має бути узгодженим: повертати true/false ((a, b) => a > b) - помилка. Результат залежить від рушія і може бути неправильним;
  • undefined завжди йдуть у кінець, компаратор для них не викликається.

Сортування на сервері й на клієнті має збігатися. Якщо список посторінково сортує база (з колацією uk), а клієнт досортовує сторінку через sort() без колатора, порядок «стрибатиме».

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