Senior: питання на співбесіді з теми «Сучасний синтаксис»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
3 питання
Proxy - обгортка над об'єктом, що перехоплює базові операції з ним: читання й запис властивостей, видалення, перевірку in, перебір ключів, виклик функції. Перехоплювачі називають «пастками» (traps).
const target = { name: 'Оля' };
const logged = new Proxy(target, {
get(obj, key, receiver) {
console.log(`читання ${String(key)}`);
return Reflect.get(obj, key, receiver);
},
set(obj, key, value, receiver) {
console.log(`запис ${String(key)} = ${value}`);
return Reflect.set(obj, key, value, receiver);
},
});
logged.name; // читання name
logged.age = 30; // запис age = 30
Reflect - набір функцій, що виконують ту саму базову операцію «за замовчуванням». У пастці його використовують, щоб після власної логіки виконати стандартну поведінку правильно - зокрема з параметром receiver, від якого залежить this для гетерів.
Пастки: get, set, has (in), deleteProperty, ownKeys (Object.keys, for...in), apply (виклик функції), construct (new), defineProperty, getPrototypeOf та інші - для кожної внутрішньої операції мови.
Реактивність Vue 3:
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
track(target, key); // запам'ятати: поточний ефект залежить від key
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
trigger(target, key); // перезапустити ефекти, що залежать від key
return result;
},
});
}
Під час рендеру компонента кожне читання реактивної властивості реєструє залежність. Зміна властивості перезапускає лише ті рендери й computed, що від неї залежать.
Чому Vue 3 перейшов з гетерів/сетерів (Vue 2) на Proxy:
- нові властивості відстежуються автоматично - у Vue 2 доводилося викликати
Vue.set; - індекси масиву й
length- змінаitems[3] = xтеж реактивна; Map,Set- через перехоплення їхніх методів;- не треба заздалегідь обходити весь об'єкт: вкладені об'єкти обгортаються ліниво, при доступі.
Інші застосування: валідація записів, значення за замовчуванням для відсутніх ключів, логування й налагодження, «віртуальні» об'єкти (клієнт API, де api.users.list() будує запит з назв властивостей), моки в тестах.
Обмеження:
- тотожність:
proxy !== target. Порівняння реактивного об'єкта з оригіналом (toRawу Vue) - часте джерело плутанини; - приватні поля
#і внутрішні слоти (Map,Date) не проходять крізь проксі: методи, викликані на проксі, отримуютьthis= проксі й падають. Тому для вбудованих колекцій Vue має окремі обробники; - продуктивність: кожна операція йде через пастку - для гарячих циклів по великих структурах це відчутно (звідси
shallowRef,markRaw); - інваріанти: пастка не може «збрехати» про незмінювану властивість (наприклад, заморожену) - рушій кине
TypeError.
Декоратор - функція, що отримує елемент класу (метод, поле, аксесор чи весь клас) і змінює чи доповнює його поведінку. Записується через @ перед оголошенням:
function logged(method, context) {
return function (...args) {
console.log(`виклик ${String(context.name)}`, args);
return method.apply(this, args);
};
}
class OrderService {
@logged
place(order) { /* ... */ }
}
Стан стандарту. Декоратори - пропозиція TC39 на стадії 3: синтаксис і семантика узгоджені, але рушії браузерів і Node.js нативно їх ще не виконують. Використовуються через компіляцію:
- TypeScript 5.0+ підтримує стандартні декоратори без прапорців;
- Babel - через плагін.
Дві несумісні версії. Це головне джерело плутанини:
- «експериментальні» (legacy) декоратори - TypeScript з
experimentalDecorators: true. На них побудовані Angular, NestJS, TypeORM, MobX (старі версії). Вони отримують дескриптор властивості й підтримують декоратори параметрів; - стандартні декоратори (стадія 3) - інший API: отримують значення й об'єкт
context(kind,name,addInitializer,access,metadata). Декораторів параметрів у них немає.
Декоратор, написаний для однієї версії, не працює з іншою. Перехід фреймворків на стандартні декоратори - поступовий.
Що можна декорувати:
- методи - обгортки: логування, кешування, повтор при помилці, перевірка прав;
- поля - перетворення початкового значення;
- аксесори з
accessor- нове ключове слово, що створює пару гетер/сетер з прихованим сховищем. Основа для реактивних полів (Lit, MobX); - класи - реєстрація, додавання поведінки.
class Counter {
@reactive accessor count = 0; // гетер/сетер, які декоратор може перехопити
}
Метадані (окрема пропозиція, теж стадія 3): context.metadata - об'єкт, куди декоратори записують інформацію про клас. Замінює reflect-metadata, на якому побудовані DI-контейнери NestJS і Angular.
Порівняння з PHP: атрибути PHP 8 (#[Route('/orders')]) - лише метадані, які читає фреймворк через рефлексію. Декоратори JavaScript - виконуваний код, що змінює елемент у момент визначення класу.
Чи варто використовувати в застосунку: якщо фреймворк побудований на них (Angular, NestJS, Lit) - так, у тій версії, яку він вимагає. У власному коді - обережно: компіляція обов'язкова, а стандарт ще не в рушіях. Звичайна функція вищого порядку (const place = logged(placeImpl)) дає той самий ефект без нового синтаксису.
Допоміжні методи ітераторів (ES2025) - map, filter, take, drop, flatMap, reduce, some, every, find, forEach, toArray - тепер є не лише в масивах, а й у будь-якому ітераторі.
Різниця з методами масиву - лінивість. Ланцюжок методів масиву на кожному кроці створює новий масив цілком:
const result = hugeArray
.filter((n) => n % 2) // новий масив на сотні тисяч елементів
.map((n) => n * 10) // ще один
.slice(0, 2); // а потрібно було лише 2 елементи
Методи ітератора нічого не обчислюють наперед. Кожен елемент проходить увесь ланцюжок по одному, і лише коли його запитали:
const result = hugeArray
.values() // ітератор масиву
.filter((n) => n % 2)
.map((n) => n * 10)
.take(2) // зупиниться після двох
.toArray();
Оброблено рівно стільки елементів, скільки потрібно, щоб знайти два, - без проміжних масивів.
Нескінченні послідовності стають природними:
function* naturals() {
let n = 1;
while (true) yield n++;
}
naturals().filter((n) => n % 7 === 0).take(3).toArray(); // [7, 14, 21]
З масивом таке неможливо в принципі.
Ітератори всюди:
map.keys().filter((key) => key.startsWith('user:')).toArray();
set.values().map((tag) => tag.toLowerCase());
document.querySelectorAll('li').values().filter((li) => li.dataset.active).toArray();
Iterator.from(obj) перетворює будь-який ітерований об'єкт (чи об'єкт з методом next) на ітератор з цими методами.
Що варто знати:
- ітератор одноразовий: після проходу він вичерпаний. Повторний
toArray()поверне порожній масив. Масив можна перебирати скільки завгодно; - немає довжини й доступу за індексом - якщо потрібні
length,sort,at, перетворюйте на масив; - на невеликих масивах виграшу немає, а ланцюжок методів масиву звичніший;
- асинхронних версій (для асинхронних ітераторів) поки немає в стандарті - це окрема пропозиція.
Коли використовувати: великі чи потенційно нескінченні послідовності, ранній вихід (take, find), обробка Map/Set без проміжних масивів, генератори даних.
Підтримка: Chrome 122+, Firefox 131+, Safari 18.4+, Node.js 22+.
Аналогія з Laravel: LazyCollection проти Collection - те саме розрізнення між лінивою й «жадібною» обробкою, і cursor()/lazy() для великих вибірок з бази.