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

Питання на співбесіді: Сучасний синтаксис

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

12 питань

Деструктуризація - короткий запис, щоб витягти значення з об'єкта чи масиву в окремі змінні.

Об'єкти - за назвами властивостей:

const user = { id: 7, name: 'Оля', city: 'Львів', role: 'admin' };

const { name, city } = user;               // name = 'Оля', city = 'Львів'
const { name: userName } = user;           // перейменування
const { phone = 'не вказано' } = user;     // значення за замовчуванням
const { id, ...profile } = user;           // решта властивостей в об'єкт

Масиви - за позицією:

const [first, second] = ['php', 'laravel', 'vue'];
const [, , third] = ['php', 'laravel', 'vue'];   // пропуск елементів
const [head, ...tail] = [1, 2, 3, 4];           // tail = [2, 3, 4]

let a = 1, b = 2;
[a, b] = [b, a];                                // обмін значеннями

Вкладена деструктуризація:

const { address: { city } } = order;   // city з order.address

Якщо order.address - undefined, буде TypeError. Захист - значення за замовчуванням: const { address: { city } = {} } = order;.

Де це використовується найчастіше:

  • параметри функцій - іменовані опції:
function createUser({ name, email, role = 'user' }) { /* ... */ }
createUser({ email: 'a@b.ua', name: 'Оля' });
  • імпорти: import { ref, computed } from 'vue' - схожий синтаксис;
  • результат функції з кількома значеннями: const [count, setCount] = useState(0) у React, const { data, error } = await request();
  • перебір пар: for (const [key, value] of Object.entries(prices));
  • відповідь API: const { data, meta: { total } } = await response.json();.

Що варто знати:

  • значення за замовчуванням спрацьовує лише для undefined, не для null: const { limit = 10 } = { limit: null } дасть null;
  • деструктуризувати null чи undefined неможливо - TypeError;
  • деструктуризація масиву працює з будь-яким ітерованим значенням: рядком, Set, Map (const [[key, value]] = map);
  • деструктуризація створює поверхневі копії значень: вкладені об'єкти лишаються спільними з оригіналом.

Аналог у PHP - [$first, $second] = $array і ['name' => $name] = $user.

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

Три крапки ... означають протилежні речі залежно від місця:

  • spread (розгортання) - «розкласти» масив чи об'єкт на окремі елементи. Стоїть там, де використовують значення;
  • rest (решта) - «зібрати» кілька елементів в один масив чи об'єкт. Стоїть там, де оголошують змінні чи параметри.

Spread:

// масиви
const all = [...frontend, ...backend];          // об'єднання
const copy = [...items];                        // поверхнева копія
Math.max(...prices);                            // масив як аргументи функції
const unique = [...new Set(tags)];              // будь-який ітерований об'єкт у масив

// об'єкти
const defaults = { perPage: 20, sort: 'name' };
const options = { ...defaults, ...userOptions }; // пізніші властивості перекривають ранніші
const updated = { ...user, name: 'Іра' };        // копія зі зміненим полем

Rest:

function log(level, ...messages) {}             // решта аргументів у масив
const [first, ...others] = list;                // решта елементів
const { password, ...safeUser } = user;         // усе, крім password

Останній приклад - зручний спосіб прибрати поле з об'єкта без мутації.

Що варто знати про spread:

  • копія поверхнева. Вкладені об'єкти й масиви лишаються спільними:
const copy = { ...settings };
copy.limits.upload = 1;   // змінило й settings.limits

Для глибокої копії - structuredClone(settings).

  • порядок важливий: { ...user, role: 'admin' } перезапише роль, а { role: 'admin', ...user } - ні, якщо в user вона є;
  • spread об'єкта копіює лише власні перелічувані властивості - без прототипу, гетери обчислюються в значення. Тому { ...dateInstance } чи { ...classInstance } дасть простий об'єкт без методів;
  • spread null чи undefined в об'єкті нічого не додає (зручно для умовних полів: { ...(isAdmin && { role: 'admin' }) }), а в масиві - кидає TypeError;
  • дуже великі масиви як аргументи (Math.max(...hugeArray) на сотні тисяч елементів) можуть перевищити ліміт аргументів - для них reduce.

Spread - основа незмінних оновлень стану у React, Pinia, Redux: замість зміни об'єкта створюється новий з потрібними змінами.

У PHP ті самі три крапки: ...$args у параметрах (rest) і [...$a, ...$b] чи f(...$args) (spread).

Докладніше в документації: Spread-синтаксис

Шаблонні рядки записують у зворотних лапках `. Вони підтримують підстановку виразів і багаторядковість:

const name = 'Оля';
const count = 3;

const message = `Привіт, ${name}! У вас ${count} нових ${count === 1 ? 'повідомлення' : 'повідомлень'}.`;

const html = `
  <div class="card">
    <h3>${title}</h3>
  </div>
`;

Усередині ${...} - будь-який вираз: виклик функції, тернарний оператор, арифметика. Результат перетворюється на рядок.

Чим кращі за склеювання через +:

  • читається як готовий текст, без нагромадження лапок і плюсів;
  • переноси рядків зберігаються як є;
  • не треба екранувати одинарні й подвійні лапки всередині.

Пастки:

  • HTML з даними користувача через шаблонний рядок і innerHTML - XSS. Шаблонний рядок нічого не екранує;
  • відступи багаторядкового шаблону потрапляють у рядок;
  • undefined і null підставляться як текст 'undefined'/'null' - видно в інтерфейсі, якщо дані не прийшли.

Теговані шаблони - функція перед шаблоном отримує окремо статичні частини й підставлені значення:

function highlight(strings, ...values) {
  return strings.reduce(
    (result, part, i) => result + part + (i < values.length ? `<mark>${escapeHtml(values[i])}</mark>` : ''),
    '',
  );
}

highlight`Знайдено ${count} результатів для «${query}»`;

Функція бачить, де закінчується довірений текст розробника і починаються дані, - і може їх обробити: екранувати, параметризувати.

Де теговані шаблони використовують:

  • безпечний SQL: бібліотеки на кшталт postgres (sql`SELECT * FROM users WHERE id = ${id}`) перетворюють значення на параметри запиту, а не вставляють у текст - захист від SQL-ін'єкцій;
  • HTML-шаблони з екрануванням: lit-html (html`<p>${text}</p>`);
  • CSS-in-JS: styled-components;
  • GraphQL-запити: gql`...`;
  • String.raw - вбудований тег, що не обробляє екранування: String.raw`C:\new\path` лишає \n як два символи. Зручно для регулярних виразів і шляхів Windows.

Масив статичних частин має властивість raw - ті самі частини без обробки екранування.

Докладніше в документації: Шаблонні літерали

Клас - синтаксис для створення об'єктів з однаковою структурою й поведінкою. Під капотом це звичайна функція-конструктор з прототипом, але запис читабельніший.

class Product {
  static count = 0;          // статичне поле: належить класу, а не екземпляру
  currency = 'UAH';          // поле екземпляра зі значенням за замовчуванням
  #discount = 0;             // приватне поле

  constructor(name, price) {
    this.name = name;
    this.price = price;
    Product.count++;
  }

  get finalPrice() {         // гетер: читається як властивість
    return this.price - this.#discount;
  }

  applyDiscount(amount) {    // метод: спільний для всіх екземплярів через прототип
    this.#discount = amount;
    return this;
  }

  static fromJson(data) {    // статичний метод: фабрика
    return new Product(data.name, data.price);
  }
}

const p = new Product('Книга', 500).applyDiscount(50);
p.finalPrice;       // 450
Product.count;      // 1

Успадкування:

class DigitalProduct extends Product {
  constructor(name, price, url) {
    super(name, price);      // обов'язково до звернення до this
    this.url = url;
  }

  toString() {
    return `${this.name} (${this.url})`;
  }
}

Правила, на яких часто помиляються:

  • super() перед this у конструкторі класу-нащадка - інакше ReferenceError: Must call super constructor ... before accessing 'this';
  • клас без new викликати не можна: Product('x') дає TypeError: Class constructor Product cannot be invoked without 'new';
  • класи не піднімаються як функції: використати клас до його оголошення - ReferenceError (тимчасова мертва зона, як у let);
  • тіло класу завжди в суворому режимі;
  • методи не прив'язані до екземпляра: const fn = p.applyDiscount; fn(10) втратить this. Для обробників подій використовують стрілкову функцію в полі класу чи bind.

Поля чи присвоєння в конструкторі: поля класу оголошують структуру об'єкта в одному місці й створюються до виконання тіла конструктора (для нащадка - одразу після super()).

Статичний блок static { ... } виконується один раз при оголошенні класу - для складної ініціалізації статичних полів.

Коли клас доречний: об'єкти з власним станом і поведінкою, ієрархії, з якими працює бібліотека (веб-компоненти, класи помилок). Для простих даних достатньо об'єктного літерала, а для поведінки без стану - звичайних функцій.

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

Оператор ?. звертається до властивості чи викликає метод, лише якщо значення ліворуч не null і не undefined. Інакше весь вираз повертає undefined замість TypeError.

const city = order?.customer?.address?.city;

// раніше
const city = order && order.customer && order.customer.address && order.customer.address.city;

Три форми:

user?.name             // властивість
user?.['first-name']   // обчислена властивість чи індекс
callback?.()           // виклик, якщо функція існує
user.save?.()          // виклик методу, якщо він є

Коротке замикання. Якщо ?. зустрів null/undefined, решта ланцюжка не обчислюється:

null?.a.b.c;               // undefined, а не TypeError на .b
user?.profile.update(log()); // log() не викликається, якщо user - null

Поєднання з ?? - значення за замовчуванням:

const theme = settings?.ui?.theme ?? 'light';
const total = response?.meta?.total ?? 0;

Обмеження:

  • ліворуч від присвоєння не працює: user?.name = 'Оля' - синтаксична помилка;
  • з new не працює: new Foo?.();
  • ?.() захищає лише від null/undefined, а не від «не функції»: { f: 42 }.f?.() кине TypeError: f is not a function.

Де ?. приховує помилки:

function renderInvoice(order) {
  const total = order?.items?.reduce((sum, item) => sum + item.price, 0);
  return `Разом: ${total} грн`;   // 'Разом: undefined грн'
}

Якщо order не повинен бути порожнім, ?. ховає баг: замість зрозумілої помилки в місці, де дані зламалися, отримуємо undefined у інтерфейсі чи NaN у розрахунку - далеко від справжньої причини.

Правило: ?. - для значень, відсутність яких очікувана й нормальна (необов'язкові поля API, опційні колбеки, елемент, якого може не бути на сторінці). Для обов'язкових даних краще явна перевірка з помилкою на вході:

if (!order) throw new Error('Замовлення не завантажено');

TypeScript робить це правило природним: для необов'язкових типів (address?: Address) він вимагає ?., а для обов'язкових - не дозволяє безпідставно сподіватися на «а раптом там null».

Аналог у PHP 8 - nullsafe-оператор $order?->customer?->address.

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

Префікс # робить поле, метод чи аксесор класу справді приватним: доступ до нього можливий лише з коду всередині тіла класу.

class Cart {
  #items = [];
  static #instances = 0;

  constructor() {
    Cart.#instances++;
  }

  add(product, qty = 1) {
    this.#validate(qty);
    this.#items.push({ product, qty });
  }

  get total() {
    return this.#items.reduce((sum, { product, qty }) => sum + product.price * qty, 0);
  }

  #validate(qty) {
    if (qty <= 0) throw new RangeError('Кількість має бути додатною');
  }
}

const cart = new Cart();
cart.add({ price: 100 }, 2);
cart.total;     // 200
cart.#items;    // SyntaxError - навіть не помилка під час виконання, а помилка розбору

Чим відрізняється від конвенції _items:

  • _items - лише домовленість: властивість доступна, видна в Object.keys, у JSON.stringify, її можна змінити ззовні;
  • #items - приватність гарантована мовою: не видно в Object.keys, не серіалізується в JSON, не доступна через obj['#items'], проксі й рефлексію.

Корисні можливості:

  • перевірка «бренду» - чи об'єкт створено цим класом, без instanceof (якого можна обдурити підміною прототипу):
class Money {
  #amount;
  static isMoney(value) {
    return #amount in value;
  }
}
  • статичні приватні поля й методи (static #instances);
  • статичні блоки ініціалізації (static { ... }) - мають доступ до приватних елементів класу.

Обмеження й підводні камені:

  • приватні поля не наслідуються в доступі: підклас не бачить #items батьківського класу. Для «захищених» (як protected у PHP) окремого синтаксису немає;
  • Proxy над об'єктом з приватними полями ламає методи: метод, викликаний через проксі, отримує this = проксі, а в проксі немає приватних полів - TypeError. Це помітно в реактивних системах: Vue робить об'єкти реактивними через Proxy, тож класи з # полями в reactive() можуть падати. Для таких класів - markRaw чи shallowRef;
  • приватні поля оголошуються в тілі класу - додати їх динамічно неможливо;
  • structuredClone і JSON.stringify їх не копіюють.

TypeScript має і модифікатор private, і #. private - лише перевірка компілятора, після компіляції поле звичайне; # - справжня приватність під час виконання.

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

Symbol - примітивний тип для унікальних ідентифікаторів. Кожен виклик Symbol() створює значення, що не дорівнює жодному іншому, навіть з тим самим описом:

const id1 = Symbol('id');
const id2 = Symbol('id');
id1 === id2;   // false

Опис ('id') - лише для налагодження, на унікальність не впливає.

Символи як ключі властивостей - не конфліктують з жодними іншими ключами:

const internal = Symbol('internal');

const user = { name: 'Оля', [internal]: { loadedAt: Date.now() } };

Object.keys(user);        // ['name'] - символьний ключ прихований
JSON.stringify(user);     // '{"name":"Оля"}'
user[internal];           // доступ лише з посиланням на сам символ

Бібліотека може додати службові дані до чужого об'єкта, не ризикуючи зіткнутися з його полями і не потрапляючи в серіалізацію чи for...in. Але символьні ключі не приватні: Object.getOwnPropertySymbols() чи Reflect.ownKeys() їх покажуть.

Глобальний реєстр - Symbol.for(key) повертає той самий символ для того самого ключа в усьому застосунку (навіть між iframe):

Symbol.for('app.cache') === Symbol.for('app.cache');   // true

Найважливіше практичне застосування - вбудовані (well-known) символи. Через них ваші об'єкти підключаються до механізмів мови:

  • Symbol.iterator - об'єкт стає ітерованим (for...of, spread):
class Range {
  constructor(from, to) { this.from = from; this.to = to; }
  *[Symbol.iterator]() {
    for (let n = this.from; n <= this.to; n++) yield n;
  }
}
[...new Range(1, 3)];   // [1, 2, 3]
  • Symbol.asyncIterator - для for await...of;
  • Symbol.toPrimitive - перетворення на число чи рядок;
  • Symbol.toStringTag - що показує Object.prototype.toString ([object Money]);
  • Symbol.hasInstance - поведінка instanceof;
  • Symbol.dispose / Symbol.asyncDispose - для явного керування ресурсами (using).

Ще застосування:

  • значення-«константи», які гарантовано не збігаються з даними: const NOT_FOUND = Symbol('not found') замість null чи -1, які можуть бути реальними значеннями;
  • ключі для provide/inject у Vue - щоб різні бібліотеки не перезаписали одна одній залежності.

Обмеження: символ не перетворюється неявно на рядок (`${sym}` - TypeError), лише явно через String(sym) чи sym.description.

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

Гетер - метод, який викликається при читанні властивості. Сетер - при записі. Ззовні це виглядає як звичайна властивість, без дужок.

class Temperature {
  #celsius = 0;

  get fahrenheit() {
    return this.#celsius * 9 / 5 + 32;
  }

  set fahrenheit(value) {
    this.#celsius = (value - 32) * 5 / 9;
  }

  get celsius() {
    return this.#celsius;
  }

  set celsius(value) {
    if (value < -273.15) throw new RangeError('Нижче абсолютного нуля');
    this.#celsius = value;
  }
}

const t = new Temperature();
t.celsius = 25;
t.fahrenheit;       // 77
t.fahrenheit = 32;
t.celsius;          // 0

У об'єктних літералах так само:

const user = {
  first: 'Оля',
  last: 'Коваль',
  get fullName() { return `${this.first} ${this.last}`; },
};

Де доречні:

  • обчислювані властивості - значення, що виводиться з інших (fullName, total, isEmpty);
  • валідація при записі - сетер відкидає некоректні значення;
  • зворотна сумісність - була звичайна властивість, стала обчислюваною, а код, що її використовує, не змінився;
  • властивість лише для читання - гетер без сетера.

Підводні камені:

  • гетер без сетера - запис мовчки ігнорується в нестрогому режимі й кидає TypeError у строгому (модулі й класи завжди строгі);
  • дорогий гетер виглядає як дешеве поле. list.total у циклі на тисячу ітерацій, що щоразу перераховує суму, - прихована проблема продуктивності. Важкі обчислення краще робити явним методом чи кешувати;
  • побічні ефекти в гетері - погана практика: читання властивості не повинно нічого змінювати;
  • рекурсія: сетер set name(v) { this.name = v; } викличе сам себе нескінченно - зберігати значення треба в іншому полі (#name);
  • серіалізація: JSON.stringify викликає гетери власних властивостей об'єкта-літерала, але гетери класу живуть у прототипі й не потрапляють у JSON. Для класу потрібен toJSON().

Низькорівнево гетери й сетери - це дескриптори властивостей: Object.defineProperty(obj, 'x', { get() {...}, set(v) {...} }). Саме так працювала реактивність Vue 2 - кожна властивість даних перетворювалася на пару гетер/сетер, що відстежувала читання й записи.

У Vue 3 аналог - computed, у Laravel - аксесори й мутатори моделі (Attribute::make(get: ..., set: ...)).

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

Прапорці:

Прапорець Що робить
g шукати всі збіги, а не перший
i без урахування регістру
m ^ і $ - початок і кінець кожного рядка тексту
s . збігається й з переносом рядка
u режим Unicode: коректні символи поза BMP, \p{...}
v розширений Unicode-режим: операції над множинами в класах, властивості рядків
y «липкий»: збіг лише точно з позиції lastIndex
d індекси груп у match.indices

Іменовані групи:

const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const { year, month } = '2026-10-04'.match(re).groups;

'2026-10-04'.replace(re, '$<day>.$<month>.$<year>');   // "04.10.2026"

matchAll - усі збіги з групами й позиціями:

for (const m of 'a1 b22 c333'.matchAll(/(?<letter>[a-z])(?<digits>\d+)/g)) {
  console.log(m.groups.letter, m.groups.digits, m.index);
}

Вимагає прапорець g, інакше кидає TypeError.

Lookbehind і lookahead - умова на сусідній текст, яка не входить у збіг:

'ціна: 100 грн'.replace(/(?<=ціна: )\d+/, '200');   // "ціна: 200 грн"
/\d+(?= грн)/.exec('100 грн')[0];                   // "100"
/(?<!\$)\b\d+/;                                     // число, перед яким немає $

Заміна функцією:

'привіт світ'.replace(/\p{L}+/gu, (word) => word[0].toUpperCase() + word.slice(1));
// "Привіт Світ"

Unicode і кирилиця: \w - лише [A-Za-z0-9_], українські літери в нього не входять. Для літер будь-якої мови - \p{L} з прапорцем u чи v; \p{Script=Cyrillic} - лише кирилиця.

Пастка з g і test(): регулярний вираз з прапорцем g зберігає lastIndex між викликами:

const re = /a/g;
re.test('aa');   // true
re.test('aa');   // true
re.test('aa');   // false - пошук почався з кінця рядка

Для перевірки «чи є збіг» прапорець g не потрібен.

Безпека: вирази з вкладеними квантифікаторами ((a+)+$) на зловмисному введенні виконуються експоненційно довго (ReDoS) і блокують головний потік. Регулярні вирази з користувацького введення будувати не можна без екранування, а для складного розбору краще парсер.

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

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