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

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.

Докладніше в документації: 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: регулярні вирази