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

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.

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

Декоратор - функція, що отримує елемент класу (метод, поле, аксесор чи весь клас) і змінює чи доповнює його поведінку. Записується через @ перед оголошенням:

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)) дає той самий ефект без нового синтаксису.

Докладніше в документації: Пропозиція TC39: декоратори

Допоміжні методи ітераторів (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() для великих вибірок з бази.

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