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 там, де можна без нього, і ніколи не будувати на ньому логіку, від якої залежить коректність.
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 ловить мутації ще на етапі компіляції без витрат під час виконання.
Без компаратора 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() без колатора, порядок «стрибатиме».