Middle: питання на співбесіді з теми «Сучасний синтаксис»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
5 питань
Оператор ?. звертається до властивості чи викликає метод, лише якщо значення ліворуч не 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) і блокують головний потік. Регулярні вирази з користувацького введення будувати не можна без екранування, а для складного розбору краще парсер.