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

Junior: питання на співбесіді з теми «Сучасний синтаксис»

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

4 питання

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

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

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: класи