Питання на співбесіді: Сучасний синтаксис
Питання з реальних співбесід з відповідями: 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).
Шаблонні рядки записують у зворотних лапках `. Вони підтримують підстановку виразів і багаторядковість:
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 { ... } виконується один раз при оголошенні класу - для складної ініціалізації статичних полів.
Коли клас доречний: об'єкти з власним станом і поведінкою, ієрархії, з якими працює бібліотека (веб-компоненти, класи помилок). Для простих даних достатньо об'єктного літерала, а для поведінки без стану - звичайних функцій.
Оператор ?. звертається до властивості чи викликає метод, лише якщо значення ліворуч не 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.
Префікс # робить поле, метод чи аксесор класу справді приватним: доступ до нього можливий лише з коду всередині тіла класу.
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.
Гетер - метод, який викликається при читанні властивості. Сетер - при записі. Ззовні це виглядає як звичайна властивість, без дужок.
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: ...)).
Прапорці:
| Прапорець | Що робить |
|---|---|
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) і блокують головний потік. Регулярні вирази з користувацького введення будувати не можна без екранування, а для складного розбору краще парсер.
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() для великих вибірок з бази.