Детальний розбір запитань на інтервʼю
Добірка питань та відповідей для підготовки до технічних співбесід. Включає JavaScript/Node.js, System Design, бази даних та хмарні технології.
Теги:
Що таке WeakMap, WeakSet, WeakRef?
Це питання ви навряд чи почуєте напряму, бо на технічних інтерв’ю зазвичай питають класичні структури типу Map/Set. Але якщо ви ще й згадаєте WeakMap, WeakSet, WeakRef, то це майже гарантовано додасть вам кілька балів. Принаймні ви запам’ятаєтеся і дуже ймовірно, почуєте щось на кшталт: “Класно, що згадали ще й ці штуки”. Тому давайте розберемося, що це таке.
WeakMap
WeakMap - це мапа ключ → значення, схожа на звичайний Map, але з важливою різницею:
- ключами можуть бути тільки об’єкти
- ці ключі зберігаються як слабкі посилання
Що таке “слабке посилання” і навіщо воно? - У звичайному Map легко зробити ситуацію, коли об’єкт вже не потрібен, але він все одно “живе” в пам’яті, бо на нього тримається посилання як на ключ. GC (Garbage Collector) може подумати: “на цей об’єкт ще є посилання - значить, прибирати не можна”.
У WeakMap цього немає: якщо на об’єкт-ключ більше ніде немає сильних посилань, GC може прибрати об’єкт, і запис у WeakMap також зникне автоматично. Тобто, це менше шансів на memory leak. Цю структуру не можна ітерувати, тому немає: for...of, .keys(), .entries(), і навіть .size. API мінімальний: set / get / has / delete.
Типовий кейс: зберігати “приватні” дані/метадані для об’єктів (кеш, стан, підрахунки), не створюючи memory leak.
WeakSet
WeakSet - це як Set (унікальні значення), але: всередині тільки об’єкти, і вони теж зберігаються як слабкі посилання. Така сама, як і попередня структура, Set ітерувати не можна.
Типовий кейс: позначити об’єкти (наприклад, “вже оброблено”), не заважаючи GC їх прибирати.
WeakRef
WeakRef - це обгортка, яка тримає слабке посилання на об’єкт (його ще називають target або referent). Тут логіка така: ви ніби “тримаєте ручку” на об’єкт, але не гарантуєте, що він існуватиме завжди. У будь-який момент GC може його прибрати.
Для чого: дуже обережне кешування/оптимізації, коли ви хочете “підглянути” об’єкт, але не тримати його в пам’яті насильно. Також, важливо що на WeakRef не можна покладатися як на стабільне збереження даних — deref() може повернути undefined у будь-який момент.
Що таке referential actions в SQL?
Одне з запитань, яке вводить Node.js розробника в легкий ступор. Чомусь так склалося, що SQL і розуміння реляційних баз даних - це ахіллесова пʼята в світі backend-розробки на JavaScript.
Referential actions - це правила, які визначають, що має відбутися з рядками в дочірній таблиці, коли в батьківській таблиці оновлюється або видаляється рядок, на який вони посилаються через foreign key.
В основному є три способи контролю, і зараз ми розберемо їх на прикладі. Уявимо, що в нас є таблички users і orders. В таблиці orders є user_id, який представляє собою foreign key і звʼязує orders з конкретним користувачем.
Якщо ми спробуємо видалити користувача з id=1, на який посилаються рядки з orders, то спрацює механізм референційної цілісності (referential integrity), який визначає, чи можливе видалення і, якщо так, то за якими правилами. Далі ми по черзі розберемо правила, і ви зрозумієте, що це дуже просто і зрозуміло.
RESTRICT
Якщо на рядок з users посилається хоча б один рядок з orders, ми не зможемо видалити. Єдиний спосіб видалити такий запис - це спочатку видалити дані з orders, а тільки після того referential integrity дозволить видалити користувача.
CASCADE
Найбільш небезпечне правило, оскільки воно означає, що при видаленні рядка з таблиці users будуть видалені всі рядки з orders, які посилаються на користувача.
SET NULL
При видаленні користувача з таблиці users всім orders, які на нього посилаються, буде записано NULL в foreign key (user_id).
Всі ті самі правила застосовуються і для UPDATE-операції, але так як це використовується доволі рідко, на інтервʼю вас будуть запитувати саме про DELETE.
Як склонувати обʼєкт в JS?
Це бородате питання, яке досі задають на інтервʼю - зазвичай в компаніях, де люди явно засиділися і пропустили безліч оновлень в JS. В цілому є три найпоширеніші способи склонувати обʼєкт в JavaScript.
Spread оператор
Це найпростіший і найелегантніший спосіб. Єдиний нюанс в тому, що spread виконує shallow (поверхневе) клонування. Це означає, що всі вкладені обʼєкти не будуть склоновані і будуть посилатися на оригінальний обʼєкт.
JSON.stringify → JSON.parse
Старий дідівський метод, який робить deep clone (глибоке клонування) обʼєкта. Це означає, що всі вкладені обʼєкти будуть склоновані. В цілому цей спосіб робочий, але має одну проблему - він працює тільки з простими типами даних.
Якщо обʼєкт має поля з Map/Set, undefined, Infinity, BigInt і т.д., ми або отримаємо помилку, або поля будуть загублені чи конвертовані в рядок. Тому використовувати такий спосіб в реальному проєкті не найкраща ідея, і краще використовувати third party або спосіб, який я опишу далі.
structuredClone
Це відносно новий метод в JS, який дозволяє робити глибоке клонування, в тому числі складних типів, з якими не справляється попередній метод. Він гарно розписаний в MDN, тому для детального розбору рекомендую прочитати документацію.
Яка різниця між Promise.all i Promise.allSettled?
Обидва методи працюють із масивом промісів: вони запускають їх паралельно та очікують на їх завершення. Проте ці методи поводяться по-різному, тому і на співбесідах, і в реальній роботі важливо розуміти різницю між ними та знати, у яких ситуаціях який метод доречніший.
Promise.all
Цей метод приймає масив промісів і повертає масив з результатами в тому ж порядку, в якому ми передали проміси. Якщо всі вони завершилися успішно, то Promise.all завершується успіхом (fulfilled), але якщо хоча б один був відхилений (rejected), Promise.all відразу завершується з помилкою і не очікує проміси, яка в процесі виконання (pending).
Promise.allSettled
Теж запускає масив промісів паралельно, але очікує доки всі завершаться fulfilled/rejected, після чого повертає масив обʼєктів з даними або причиною помилки. Як можна побачити з відповіді від API, Promise.allSettled завершився успішно, навіть маючи rejected проміс. Це і є різниця між Promise.all і Promise.allSettled.
Коли і що використовувати
Promise.all найкраще використовувати для ідемпотентних операцій. Так як у випадку rejected ми без всяких проблем зможемо зробити Retry і перезапустити всі проміси. Найчастіше це отримання або оновлення даних. Якщо ви не знаєте, що таке ідемпотентність, то рекомендую почитати тут, так як це теж запитання на Node.js інтервʼю.
Promise.allSettled найкраще використовувати при створенні сутностей. Так як стан системи змінюється, ми не можемо зробити простий Retry для всіх промісів. А за допомогою allSettled ми зможемо вибрати проміси, які завершилися rejected, і перезапустити їх окремо.
Простим прикладом використання allSettled є завантаження багатьох картинок в галерею. Наприклад, ви завантажуєте одночасно 10 фотографій, і 7 з них завантажилися успішно, а три - з помилкою. Тоді ми можемо показати користувачу, що є проблеми з трьома картинками, і він зможе або видалити їх, або спробувати завантажити знову.
Що таке Promisification?
На дворі вже 2026 рік, але це запитання про промісифікацію і досі гуляє по різних співбесідах, здебільшого на Junior-інтервʼю. Щоб зрозуміти, як працює даний костиль, потрібно розібрати проблематику.
Callback hell
Це ситуація, коли через багато асинхронних викликів на колбеках код перетворюється на піраміду з вкладених функцій. Через це його важко читати, дебажити і нормально обробляти помилки.
В Node.js callbacks будуються наступним чином - першим аргументом приходить помилка або null, якщо її немає. А другим аргументом приходять дані і типовий метод, який використовує callback, виглядає наступним чином:
Якщо взяти цю функцію окремо, вона виглядає адекватно. Але як тільки наблизимося до реального проєкту, де може бути багато взаємозалежних викликів, починається callback hell.
Тут приходить на допомогу промісифікація
Promisification - це коли callback-based функція огортається в Promise. Тоді замість вкладених колбеків можна зручно писати .then/.catch або async/await. Зазвичай ця техніка застосовується в двох випадках:
Коли Node.js тільки зарелізив Promise - звісно, після релізу не всі бібліотеки швидко змінили своє API з callback-based до Promise, тому доводилося огортати їх таким способом, щоб було зручніше працювати.
Legacy - зараз це найчастіше використовується для обгортання різного legacy. Наприклад, в системі є метод, який писався вашим прадідом, який звичайно використовував callback-based підхід.
Це метод має 2000 рядків коду, і переписувати його ніхто не буде, а от промісифікувати, щоб уникнути колбеків, цілком можна. І дуже часто це була частина рефакторингу, так як працювати з Promise набагато простіше і приємніше. Повернемося до методу getUser, який ми розглядали на початку. Ми можемо дуже швидко зробити з нього Promise.
А тепер подивимось, як буде виглядати callback hell, якщо його промісифікувати:
Яка різниця між null i undefined в JavaScript?
Різниця між null i undefined - це класичне запитання на позицію junior-розробника. По своїй суті вони схожі і означають, що значення немає , але якщо копнути трошки глибше, різниця між ними стає очевидною. Отже, розберемо їх по черзі:
undefined
Цей тип даних означає, що значення не задано. Тобто, коли ми створюємо змінну і не присвоюємо значення, доступаємося до поля обʼєкта, якого не існує, чи банально - функція, яка не повертає значення явно, завжди повертає undefined.
null
Він означає порожнє значення, і в цілому він схожий на undefined, з однією важливою відмінністю - null не існує в природі JS і може зʼявитися як значення тільки тоді, коли розробник явно його задав.
Наприклад, в базі даних не знайдено продукту за ID, і ви явно повертаєте null, тим самим даєте зрозуміти, що ви шукали і нічого не знайшли, тому в якості значення підставили null.
Яка різниця між Process i Thread в Node.js?
Саме запитання більше відноситься до Computer Science, але його досить часто задають на Node.js інтервʼю, тому має сенс розібрати його саме в цій категорії. Для початку розберемося з визначенням процесу і потоку.
Процес - це окремо запущена програма, якій виділяє ресурси операційна система. Ключова ідея процесу полягає в ізоляції, тому проблеми в одному процесі напряму не впливають на інші, а комунікація відбувається через IPC (inter-process communication), наприклад через pipes, sockets…
Потік - це послідовність інструкцій, що виконується в межах процесу і має власний контекст виконання (Thread Execution Context). Так як потоки існують в межах процесу, вони використовують спільну памʼять, що робить їх чудовим рішенням для паралельних задач. Також, через відсутність необхідності в IPC, потоки працюють набагато швидше, ніж процеси.
Нижче наведена схема типового Node.js API, який використовує декілька процесів. Тут чітко видно, що потоки знаходяться в середині процесів, а сам процес - це ізольований контейнер.
Також, зверніть увагу на іконки Libuv - вони є як в процесі, так і в дочірніх потоках. Це означає, що при створенні потоку/процесу буде створено окремий Event Loop.
Підсумуємо основні відмінності
- Процес це так званий top-level контейнер
- Кожен процес має свою памʼять тоді як потоки використовують спільну
- Життєвий цикл процеса незалежний, тоді як потік залежить від процеса
- Комунікація між процесами повільніша і не така ефективна, як між потоками.
Що таке теорема CAP?
Одне з обовʼязкових питань на Senior-інтервʼю в проєктах з розподіленими системами. Питання теоретичне, але показує, наскільки глибоко ви зайшли в System Design та архітектуру мікросервісів.
CAP-теорема описує фундаментальний компроміс у розподілених системах зберігання даних, де дані реплікуються між кількома вузлами. Вона стверджує, що за наявності мережевого розділення (partition) система не може одночасно гарантувати всі три властивості: узгодженість (Consistency), доступність (Availability) і стійкість до розділення (Partition tolerance).
- Consistency: кожен запит на читання повертає найактуальніше значення, тобто всі вузли поводяться як єдине джерело істини.
- Availability: кожен запит отримує відповідь, система завжди доступна і не блокується повністю.
- Partition tolerance: система продовжує працювати, навіть якщо між частинами кластера втрачено зв’язок або з’явилися значні затримки.
Практичний сенс теореми полягає в тому, що в розподіленій системі мережеві збої неминучі, тому Partition tolerance ми приймаємо як даність. У таких умовах доводиться обирати пріоритет між Partition tolerance + Consistency (PC) і Partition tolerance + Availability (PA).
CP: система зберігає коректність і актуальність даних, але може тимчасово блокувати операції, щоб не повертати суперечливі дані. Відповідно, деколи система може бути недоступна і повертати помилку чи таймаут, щоб зберегти узгодженість даних.
AP: система продовжує відповідати завжди, але допускає, що під час розділення різні вузли можуть повертати застарілі значення. При цьому підході узгодження відбудеться пізніше, і для цього підходу теж є назва - Eventual Consistency.
Важливо розуміти, що CAP змушує до компромісу тільки при виникненні проблем. В звичному режимі система повинна мати як високу доступність, так і прийнятну узгодженість даних.
Що таке контекст виконання (Execution Context)?
Це питання можна почути майже на кожному Frontend чи Backend інтервʼю рівня junior/middle.
Якщо дуже коротко, контекст виконання (Execution Context) - це середовище, де прямо зараз виконується наш код, простіше кажучи, значення this в момент виконання. Ми можемо мати 3 типи контексту виконання:
1. Глобальний контекст (Global Execution Context)
Цей контекст створюється один раз, коли завантажується файл і до нього належить весь код, який не всередині функцій: глобальні змінні, глобальні функції, this на верхньому рівні.
У браузері глобальний об’єкт зазвичай window.
2. Контекст виконання функції (Function Execution Context)
Тут важливо запамʼятати: кожен виклик функції - це новий контекст. Навіть якщо функція одна й та сама, контекст щоразу буде інший. У ньому JS зберігає: параметри функції, локальні змінні (ті, що всередині {}), this для конкретного виклику.
3. Контекст модуля (Module Execution Context)
Якщо код запускається як ES module, то він виконується в контексті модуля. Він схожий на глобальний, але є 2 важливі відмінності:
- Змінні не стають глобальними автоматично
thisна верхньому рівні будеundefined
Що таке AbortController?
AbortController - це контролер, який дозволяє скасувати одну або кілька асинхронних операцій, якщо ці операції підтримують AbortSignal. Він створює об’єкт signal, який ви передаєте в операцію, а потім у потрібний момент викликаєте controller.abort() і операція отримує сигнал, щоб зупинитися.
Найкращий спосіб розібратися - це глянути на приклад з fetch. Уявимо, що в нас є React-додаток і на одній сторінці є запит, який може довго виконуватися, і якщо користувач покине сторінку, запит все одно виконається і поламає UI.
Це стандартна проблема в React, коли оновлення стану відбувається після смерті компонента, і AbortController чудово її вирішує. В прикладі нижче ми створюємо контролер і передаємо його в fetch. Таким чином, ми встановлюємо контроль над процесом виконання і можемо скасувати його в потрібний нам момент.
Якщо користувач покинув сторінку і компонент був видалений з DOM, нам нема ніякого сенсу продовжувати виконання HTTP-запиту, і ми його скасовуємо через controller.abort() перед тим, як компонент буде видалений з дерева.
Якщо ви хочете детальніше ознайомитися з усіма можливостями AbortController, найкращий спосіб - почитати документацію.
До AbortController було інше запитання в цій темі. Звучало воно так: Чи можна скасувати Promise?
Найчастіше його задавали на Angular-інтервʼю і очікували, що розробник розуміє неможливість скасування Promise і необхідність використання Observable для контролю над процесом виконання.
Що таке Circuit Breaker?
Одне з тих запитань, за яким визначають, чи працювали ви з складними системами. Найперше, що треба розуміти - Circuit Breaker це патерн, який зупиняє спроби системи виконати операцію, яка, скоріше за все, завершиться невдачею. Це, в свою чергу, збереже ресурси і зупинить безкінечний потік однакових логів.
Приклад використання
В нас є сервіс А, який робить запит в сервіс B. Він, своєю чергою, викликає стороннє API і зберігає щось в базу даних. Circuit Breaker може виступити в якості проксі між сервісами А і B. Цей проксі буде працювати за принципом запобіжника і в разі виникнення критичної для сервісу помилки зможе перекрити трафік до B. Умови, за яких буде спрацьовувати запобіжник, можуть бути різні. Найбільш розповсюджений варіант - це поріг у кількості помилок певного типу (наприклад, 500/502 кодів) в конкретний момент часу (наприклад, 60 секунд).
Після цього він буде перевіряти, чи проблема була виправлена, і коли сервіс знову запрацює, відновить трафік до нього. Технічно, Circuit Breaker має декілька станів, де він визначає, що робити з запитами до сервісу.
Closed
Цей стан означає, що з сервісом все добре і запити потрапляють куди потрібно. У випадку появи критичних помилок він починає рахувати їх, і якщо їхня кількість перетинає заданий поріг, проксі переходить в стан Open. Окрім цього, запускається таймер очікування, який після закінчення переходить в стан Half-Open, де відбувається перевірка життєздатності сервісу.
Open
В цьому стані, Circuit Breaker не буде направляти запити до сервісу. Замість цього він буде повертати помилку або fallback. Тут все залежить від реалізації, але суть цього стану - це не пропустити трафік до сервісу.
Half-Open
В цьому стані ми пропускаємо обмежену кількість запитів до сервісу. Якщо всі вони завершуються успішно, ми переводимо Circuit Breaker в стан Closed і відновлюємо трафік до сервісу. Якщо хоча б один запит завершився з помилкою, ми переводимо стан в Open і перезапускаємо таймер.
Що таке Currying?
Це запитання доволі часто звучить на Frontend інтервʼю і часто його задають на проєктах, які почали створювати 5 - 7 років тому. Так як саме в той час функціональне програмування було на своєму піку в світі JS і дуже часто використовували бібліотеки по типу Ramda.js.
Currying (карування) — це техніка у функціональному програмуванні, коли функція, що приймає кілька аргументів, перетворюється на ланцюжок функцій, які приймають по одному(або більше) аргументу і повертають наступну функцію.
Розберемо приклад
В нас є функція sum і якщо не використовувати Currying, ми маємо відразу передати всі три аргументи, щоб функція спрацювала коректно. Але з Currying ми можемо передати аргументи частково. Наприклад, спочатку передати a, після чого функція не виконається, а поверне іншу функцію, яка прийме b i c. Далі, ми можемо або передати b i c або тільки b, тоді повернеться функція, яка прийме c. І вже після передачі c, функція виконається.
Прикладом нативної реалізації Currying є функція bind з JavaScript. Хоч і технічно це не зовсім він, а його простіша версія - partial application(часткове використання). Тим не менш, bind наглядно показує, наскільки сильна ця техніка і які проблеми може вирішувати.
Для тих, хто хоче копнути глибше в тему функціонального програмування, рекомендую розпочати з Ramda.js - https://ramdajs.com/. Паралельно до читання документації варто розбирати з ChatGPT різні поняття, які будуть зустрічатися в процесі.
Чи слідкуєш ти за OWASP Top 10?
Це питання доволі часто ставлять на Senior інтервʼю і воно є стандартною перевіркою на те чи кандидат слідкує за трендами в безпеці. Зазвичай, вас запитають про саму суть проєкту, після чого можуть запитати про найбільш критичні уразливості на даний момент.
OWASP Top 10 - це список 10 найпоширеніших і найнебезпечніших ризиків для безпеки в WEB, який публікує OWASP (Open Worldwide Application Security Project). Слідкувати за ним потрібно для того, щоб розуміти як можна захистити ваші застосунки чи API.
Також, більшість людей за ним не слідкує і навряд чи заходили далі XSS тому ви можете використовувати OWASP Top 10, як козирь на інтервʼю, який значно збільшує ваші шанси на успіх.
Яку проблему вирішують Hooks в React?
Це запитання часто ставлять на Junior/Middle interview і воно показує як глибоко ви розбирались з тим, навіщо взагалі придумали React Hooks.
Якщо ми повернемось в часи класових компонентів, то побачимо очевидну проблему - стан жорстко привʼязаний до конкретного компонента і перевикористати його ми не можемо. Звісно умільці знаходили різні шляхи і використовували наслідування чи міксини, але це радше погіршувало ситуацію, ніж вирішувало проблему.
Розберемо приклад
Уявимо, що в нас є так званий Toggle, який переключає стан кнопки. Наразі ми його використовуємо в одному компоненті і він не створює проблем.
Але пізніше в нас зʼявляється необхідність додати такий самий функціонал для модальних вікон. І якраз тут заключається основна проблема класових компонентів. Так як this.state привʼязаний до конкретного класу, ми не можемо перевикористати бізнес логіку в іншому компоненті. Тоді нам доводиться писати костилі. Найпоширеніші це: render props, наслідування i HOC. Останнє є найбільш адекватним з даного списку і доволі часто виручало, але сильно ускладнювало кодову базу.
З хуками ситуація стала набагато кращою і ми можемо винести всю логіку переключення в Custom Hook і перевикористовувати як в кнопці так і в модальному вікні.
Що таке симетричний і асиметричний ключ в шифруванні?
Це питання часто виникає під час розмов про автентифікацію/авторизацію, різницю між HTTP та HTTPS, а також про TLS. Нижче коротко розберемо, чим відрізняються симетричні та асиметричні ключі, а також їхні переваги та недоліки.
Симетричний ключ
Симетричне шифрування - це коли один і той самий ключ використовується як для шифрування так і для розшифрування даних. Так як ми використовуємо тільки один ключ, такий підхід дуже простий в використанні і процес шифрування/розшифрування даних набагато швидший, ніж при асиметричному шифруванні.
Очевидним недоліком такого підходу є безпека. Обидві сторони(шифрування і розшифрування) мають отримати один і той самий ключ, але якщо передати його небезпечно зловмисник зможе читати всі повідомлення, зашифровані цим ключем. З цим можна боротись і найчастіше використовують 2 підходи:
- Використання асиметрії, щоб отримати спільний симетричний ключ.
- Використання сторонніх сервісів для керування ключами, наприклад AWS KMS, GCP KMS, Azure Key Vault…
Асиметричний ключ
Асиметричне шифрування - це коли ми використовуємо пару ключів для шифрування(публічний) і розшифрування(приватний) даних. Він має рід переваг над симетричним шифруванням, завдяки кращій безпеці та гнучкості.
Те як працює пара public/private ключів - це окреме запитання яке ви точно почуєте на інтервʼю тому ми винесли його в окреме запитання. Ось тут можеш ознайомитись з ним.
Недоліки в такого підходу теж є, наприклад:
- З точки зору CPU, асиметричне шифрування набагато дорожче і це проблема при великих обʼємах даних тому зазвичай асиметрію використовують для обміну секретом. А для самого процесу шифрування/розшифрування використовують симетрію.
- Key management при такому підході доволі складна задача. Тому реалізацію ротації, відкликання ключів, контролю доступу…здебільшого перекидають на third party по типу AWS KMS.
Що таке dead letter queue?
Це запитання часто звучить на backend або full-stack інтервʼю, особливо на проєкти з розподіленими системами. Кожна конкретна технологія(SQS, RabbitMQ, Kafka…), має свої підходи до Dead Letter Queue(DLQ) тому краще зосередитись на розумінні того, як це працює і які проблеми вирішує.
З точки зору системного дизайну, це патерн в message-driven архітектурі, який ізолює невдалі повідомлення в окрему чергу. Це дозволяє не блокувати основний потік, не втрачати дані і дати можливість знайти причину помилки та повторно відпрацювати повідомлення.
Важливо розуміти, що DLQ не вирішує корінь проблеми, по якій стався збій. Його задача в тому щоб контрольовано зберегти невдалі повідомлення, проаналізувати їх і прийняти рішення про подальшу обробку таких повідомлень.
Розберемо приклад
У нас є мікросервіс Payments, який після успішного платежу публікує подію: PaymentSucceeded { paymentId, userId, planId, amount, occurredAt }. Інший мікросервіс Subscriptions, слухає її і після отримання створює підписку в базі даних і надсилає welcome email через сторонній провайдер.
Найпростіший сценарій де буде корисний DLQ це відмова стороннього API, яке займається відправкою емейлів. Після того, як отримуємо 502 помилку з third party сервісу, ми пробуємо ще 3 рази з інтервалом в 30 секунд(retry + backoff). Якщо ми і далі отримуємо 502, відправляємо повідомлення в DLQ. Після того як провайдер відновився, ми пробуємо обробити ці повідомлення повторно - це називається DLQ re-drive.
Що таке private i public ключ?
Одне з частих запитань на позицію Node.js розробника. Найчастіше його запитують в розмові про різницю між HTTP i HTTPS або в контексті авторизації, особливо JWT.
Для початку потрібно розібрати, що таке приватний і публічний ключі. Публічний ключ не секретний і ти можеш вільно його розповсюджувати, тоді як приватний ключ ти маєш надійно зберігати і нікому не показувати.
Шифрування
В прикладі, простими словами розберемо як працює шифрування. Уявіть, що у тебе є друг і ти хочеш, щоб він передав тобі якусь секретну інформацію в зашифрованому вигляді, а ти її розшифрував після отримання.
Для того, щоб це зробити потрібно згенерувати пару ключів(публічний і приватний). Приватний ключ тримаєш в секреті, а от публічний даєш своєму другу. За допомогою публічного ключа він зможе зашифрувати повідомлення і відправити тобі. А от розшифрувати його зможе тільки той хто має приватний ключ.
Підпис
Продовжуючи наш приклад із повідомленням, уявіть, що тепер ти хочеш надіслати другу секретне повідомлення. Він генерує пару ключів і дає свій публічний ключ. Тоді ви шифруєте повідомлення його публічним ключем, щоб розшифрувати його зміг лише він.
Саме цю проблему вирішує цифровий підпис. Ти підписуєш повідомлення своїм приватним ключем, а друг перевіряє підпис твоїм публічним. Тоді навіть якщо його публічний ключ мають багато людей і можуть зашифрувати для нього повідомлення, підробити підпис вони не зможуть - бо для цього потрібен твій приватний ключ.
Якщо ти зараз пробуєш згадати де ще ти це бачив, то такий самий принцип використовується в авторизації з JWT токенами😁
Що таке Noisy tenants?
Noisy tenants це одна з типових проблем в Multi-tenant системах, коли один шумний tenant(клієнт) починає споживати непропорційно багато ресурсів, наприклад CPU, памʼять, черги чи ліміти в API. Таким чином, це впливає на інших клієнтів: росте latency, зʼявляються timeouts, проблеми з сесіями чи поява downtime.
Як це виглядає на практиці
Наприклад, ви використовуєте брокер повідомлень(SQS, RabbitMQ…) і один tenant генерує велику кількість подій. Так як брокер і обробники подій спільні вони здебільшого обробляють повідомлення від noisy tenant, а інші клієнти страждають від збільшеного latency.
Ще один приклад, повʼязаний з сторонніми інтеграціями. Ви не встановили чітких Rate limits для клієнтів і noisy tenant викликає стороннє API настільки часто, що дуже швидко вибиває всі ліміти і інші клієнти не можуть повноцінно використовувати інтеграції.
Основні причини виникнення:
- Спільна інфраструктура: брокери повідомлень, бази даних, сервери і тд.
- Спільні ліміти зовнішніх сервісів або відсутність Rate limiting
- Нерівномірний розподіл трафіку
Приклади вирішення проблеми
- Ліміти і справедливе розподілення ресурсів
- Запровадження квот на важкі та затратні операції
- Виділення окремих ресурсів для “жирних” клієнтів
Що таке Throttling?
Throttling - це техніка в програмуванні, яка обмежує частоту виконання. Простими словами, ми вказуємо, що код може запускатись не частіше, ніж раз на N мілісекунд. Найкращий спосіб зрозуміти throttling це розібрати приклад.
Throttling в HTTP API
Наприклад, ви розробляєте систему яка працює з багатьма tenants(клієнтами), які можуть обрати собі тарифні плани згідно з своїх потреб в вашому API.
Типова проблема в таких системах це - Noisy Tenants. Уявимо, що клієнт обрав максимальний тарифний план, але частота запитів які він робить настільки велика, що він перебирає на себе значну частину ресурсів. Таким чином інші клієнти мають проблеми з затримкою у відповіді чи проблеми з повільним завантаженням даних.
В такому випадку, використовуючи throttling ви можете вказати, що максимальна частота запитів це 1000 в секунду. Це потенційно розвантажить ваші ресурси і дозволить всім клієнтам повноцінно використовувати систему.
Що таке Debounce?
Debounce - це дуже поширена техніка, яка відкладає виконання функції, поки подія не затихне на певний час. Найпростіший приклад використання, це поле для пошуку. Уявіть input де користувач може вводити пошуковий запит. По замовчуванню при будь якій зміні значення, буде викликано подію, яка спровокує відправку на backend пошукового запиту.
Уявіть, що ви шукаєте імʼя Alex
Таким чином, в нас зʼявляється 2 проблеми:
- Неефективне використання ресурсів - якщо користувачів багато, це потенційно може перевантажити backend
- Race condition -
name=Aмає нижчу селективність, ніжname=Alexтому теоретичноname=Aможе завершитись пізніше і користувач побачить зовсім не ту відповідь, яку очікував.
Використовуючи техніку Debounce, ми можемо відправляти запити не на кожну зміну input, а наприклад через 300 мілісекунд після того, як він перестав друкувати. Таким чином, ми відправимо тільки один запит з останніми змінами.
ACID - Consistency
Consistency (узгодженість) в ACID означає, що кожна транзакція переводить базу з одного валідного стану в інший валідний стан, не ломаючи правил і логіку системи.
Якщо заглибитись в технічну реалізацію Consitency, то найбільш ефективний спосіб це зробити - перенести максимум правил на рівень бази даних, а не тримати їх на рівні бізнес логіки.
Наприклад, замість того щоб валідувати дані на рівні сервісів ми можемо перенести валідацію в Constraints або Triggers.
Головне, що вам треба запамʼятати - це те що найбільш надійний спосіб забезпечити Consistency це використовувати правила, саме на рівні даних. Але тим не менше, зловживання нативними інструментами DB не завжди добре, так як веде до ускладнення системи. Тому в реальному житті, узгодження даних це завжди про компроміси між простотою і надійністю.
Що таке Destructuring в JavaScript?
Деструктуризація - це синтаксичний цукор, який дозволяє витягнути значення з масивів або об’єктів у окремі змінні, що робить код коротшим і читабельнішим. Це сухе запитання тому здебільшого вам треба розуміти визначення і навести декілька прикладів використання.
Деструктуризація масивів
Використовуючи літерал [], ми можемо витягнути елементи масиву присвоївши їх в змінні.
Деструктуризація обʼєктів
В цілому тут вона працює так само як і для масивів тому коротко розберемо приклади, для закріплення даного питання.
Розкажи про методи call, apply і bind
Це питання доволі часто зустрічається на інтервʼю. Воно може звучати саме так як в назві, а може бути поставлене так: "Чи можемо ми змінювати this?" І тут потрібно буде відповісти, що так, можемо і для цього в JS існують такі вбудовані методи як call, apply і bind.
Усі три методи роблять одне й те саме - вони дозволяють явно вказати, яким буде this усередині функції. Тобто, простими словами, ми самі кажемо функції, з яким об’єктом вона має працювати.
Різниця між ними полягає в наступному:
- У способі передачі аргументів
- У тому, чи викликається функція одразу
Наприклад, метод call - буде приймати аргументи через кому і викликати функцію одразу. Тут ми явно вказуємо, що this всередині greet буде об’єкт { title: "Hello" }.
Метод apply, робить теж саме, що і call - функція викликається одразу, а аргументи передаються у вигляді масиву.
Метод bind, навідміну від попередніх, не викликає цю функцію одразу, а повертає нову функцію, у якої вже є this. Стосовно аргументів, вони передаються через кому, але в новій функції. Його зазвичай використовують, коли потрібно передати функцію кудись далі, але зберегти правильний this.
Гарною практикою буде додати, що метод bind є реалізацією підходу часткового використання, який в свою є основою для такої техніки як Currying.
Яка різниця між useEffect i useLayoutEffect?
Це доволі типове питання на інтервʼю, якщо в стеку проєкта використовується React. Варто почати з того, що обидва хуки - useEffect і useLayoutEffect використовуються для роботи з side effects, а найголовніша різниця між ними - момент виконання.
useEffect
Він спрацьовує після того, як React відрендерив компонент і браузер вже відмалював зміни на екрані, саме тому цей хук не блокує відмальовку UI. З точки зору lifecycle, useEffect схожий на сomponentDidMount, який теж спрацьовує тільки після того, як компонент вмонтований в DOM і повністю доступний.
Типові кейси застосування:
- Запити до API, на отримання даних;
- Підписки (event listeners, WebSocket);
useLayoutEffect
Він спрацьовує після того, як React оновив DOM, але до того, як браузер відмалює сторінку. Через це він блокує відмальовку UI, поки код всередині хука не виконається. Цей хук використовують тоді, коли потрібно синхронно працювати з DOM. Простими словами: useLayoutEffect потрібен, коли треба щось поправити в DOM так, щоб користувач цього не побачив.
Типові кейси застосування:
- Вимірювання ширини або висоти елементів;
- Зміна scroll-позиції;
- Синхронна зміна стилів;
- Уникнення миготіння або стрибків інтерфейсу (найчастіший кейс);
ACID - Atomicity
Atomicity - це характеристика, яка означає, що транзакція або виконається повністю, або не виконається взагалі. Якщо в процесі виконання стається помилка, але транзакція вже змінила щось в базі даних, то ці зміни НЕ будуть застосовані, а стан повернеться до початкового - це називається ROLLBACK. Якщо ж транзакція успішна, то буде виконаний COMMIT і зміни будуть застосовані до бази даних.
Розглянемо пиклад
Тут ми вираховуємо кошти з балансу користувача id=1 і зараховуємо їх на баланс користувача id=2. Це дві окремі операції, але якщо хоча б одна з них не буде виконана - це порушить ціліснісь даних.
Так як ми огорнули ці операції в одну транзакцію, то згідно Atomicity всі вони мають завершитись успішно. Якщо кошти були зняті в id=1, але при зарахуванні їх для id=2 сталась помилка, відбудеться ROLLBACK і стан бази даних буде повернутий в BEGIN(Початковий).
Що таке React.lazy?
Коли вас питатимуть, які варіанти оптимізацій ви знаєте, не згадати про React.lazy буде злочином.
React.lazy() - це один із підходів оптимізації першого завантаження сторінки в React. Простими словами, це API яке дозволяє завантажувати компоненти тільки тоді, коли це нам потрібно. Також, це дозволяє завантажувати компоненти паралельно, що значно зменшує розмір основного bundle.
Фактично, React.lazy() є реалізацією code splitting в React. Основна ідея цього підходу - відкласти завантаження важких або рідко використовуваних компонентів.
Як це працює?
React.lazy() приймає функцію, яка повертає динамічний import(), і компонент завантажується асинхронно. Цей компонент буде завантажений лише один раз - у момент, коли він стане потрібним. Після завантаження модуль кешується і повторно не завантажується.
Що таке Suspense в React?
Швидше за все, на інтервʼю це питання буде звучати так: чи чули ви щось про Suspense. Окрім цього, його часто запитують в контексті розмови про оптимізацію і зокрема React.lazy(). Якщо вас не запитали це напряму, хорошим тоном буде логічно доповнити lazy() згадкою про Suspense.
Отже, що ж це таке?
Suspense - це механізм, для керування очікуванням. Він дозволяє показувати fallback UI у той момент, коли частина застосунку ще не готова до рендеру. Простими словами, він відповідає за контент, який буде показуватись, поки React чекає на щось асинхронне. Це може бути loader, skeleton або будь-який інший тимчасовий контент.
Основне завдання - дати користувачу зрозуміти, що за кілька секунд тут з’явиться потрібний UI. Таким чином, в користувача не підгорає при поганому інтернет зʼєднанні, що в свою чергу позитивно впливає на UX.
Давайте розглянемо базовий приклад Suspense, разом з React.lazy().
У цьому прикладі за допомогою lazy loading ми обгортаємо компонент Profile і таким чином повідомляємо React, що його потрібно завантажити асинхронно. Ця техніка називається code splitting і про неї ви можете почитати тут.
Поки компонент ще не готовий, користувач буде бачити skeleton. Після завершення завантаження, React автоматично замінить його на компонент Profile.
Що таке code splitting?
Запитання про code splitting можуть задати в контексті оптимізації Frontend аплікацій. Цей підхід використовується щоденно тому його необхідно розуміти не тільки для інтервʼю.
Code splitting - це техніка оптимізації завантаження сторінки, при якому ми ділимо JavaScript-код на кілька менших частин (bundles) і замість завантаження одного великого файлу, ми паралельно підвантажуємо декілька невеликих частин.
Окрім паралельного завантаження, code splitting дозволяє завантажувати код не відразу, а тоді коли він нам дійсно потрібен. Таким чином ми можемо підвантажувати код на фоні, щоб це не впливало на UX.
Наприклад, у нас є один великий JavaScript файл, який використовується на HTML сторінці. Без використання code splitting браузеру доведеться витратити більше часу, на його завантаження. Якщо в користувача погане інтернет зʼєднання, ви можете ненароком підпалити йому пʼяту точку і він не дочекається завантаження вашого сайту.
А тепер уявіть, якщо це велика CRM i більшість цього коду не потрібна для першого завантаження. Саме тому використовується code splitting. Він дозволяє зменшити розмір початкового JavaScript bundle, швидше показати перший контент користувачу, а додатковий код підвантажувати фоново чи за потреби.
Що таке React.memo?
Зазвичай це питання задають в контексті розмови про оптимізацію ресурсів в SPA і гібридиних додатках, які часто не ефективно використовують ресурси. Якщо вас не запитали про React.memo на пряму, хорошою практикою буде згадати цю техніку самостійно.
React.memo - це один з способів, завдяки якому ми можемо оптимізувати наш додаток, уникаючи непотрібних нам ререндерів компонентів, якщо їхні props не змінилися.
Як це працює?
Зазвичай, коли відбувається re-render батьківського компонента, React за замовчуванням перемальовує сам компонент і все його дерево(дочірні компоненти).
Щоб уникнути непотрібних нам ререндерів в важких компонентах, ми можемо огорнути його в React.memo і при кожному оновлені батьківського компонента, React буде перевіряти чи змінились props. До важких компонентів можна віднести ті, які виконують CPU intensive задачі: великий і функціональний список, багато графіків, калькуляцій і тд.
За замовчуванням порівняння відбувається через Object.is, тобто:
- Примітиви порівнюються за значенням
- Об’єкти, масиви та функції - за посиланням
Якщо змін в props не відбулось - компонент не буде перемальовуватись. Найголовніше, треба памʼятати, що потрібно обгортати лише ті компоненти, де React.memo точно допоможе. Найгірше, що можна зробити це огортати все підряд, або оптимізовувати продуктивність без аналізу проблеми.
Яка різниця між Functions і Procedures в SQL?
Як і в інших мовах програмування, в SQL функція має повертати значення. Концептуально, вона використовується для інкапсуляції бізнес логіки з подальшим перевикористанням. Також, функція може використовуватись в запиті.
Розберемо приклад функції
Тут ми інкапсулюємо бізнес логіку, яку потім можемо перевикористовувати. В цілому, концепція SQL функцій нічим не відрізняється від звичайних.
Процедури найчастіше використовується для різних jobs(наприклад пофіксити криві дані), міграцій або реалізації простих workflows, які використовуються BA(business analyst) і їх немає сенсу додавати в кодову базу проекту.
Важливо розуміти, що процедури не повертають значення тому не використовуються в SQL запитах, а викликаються через ключове слово CALL.
Що таке never в TypeScript?
Простими словами, never означає те що ніколи не станеться. Скоріше за все, ви зараз подумали про тип void і задались запитанням, яка між ними різниця?
Якщо розглянути ці типи з точки зору функцій, то технічно void все таки повертає значення, бо в JavaScript навіть при відсутності return, неявно повертається undefined. Тоді як тип never означає те, що виконання ніколи не дійде до return.
Розберемо глибше
Гарний приклад реального використання never це Nest.js і його обгортки над обʼєктом Error, які додають трохи цукру в обробку помилок. Код в середині цих функцій, ніколи не дійде до ключового слова return, так як вони завжди завершуються з помилкою.
Ще один приклад never це цикл, який ніколи не завершиться. В функції infiniteLoop виконання коду, ніколи не дійде до return тому тип також буде never.
Що таке Generics в TypeScript?
Generics - це спосіб забезпечити повторне використання коду через узагальнення типів. Якщо в вас є компонент системи(клас, функція, інтерфейс…), який працює з одним типом, то Generics дозволяють йому працювати з декількома типами.
Розберемо приклад
Ми розробляємо REST API і хочемо уніфікувати відповіді від бекенду додавши дані в обʼєкт data. Так як в нас багато сутностей, для кожної з них ми будемо дублювати інтерфейси. А якщо ми захочемо змінити error: string на щось інше, наприклад на клас ApiError - це доведеться робити для кожного інтерфейсу окремо.
Якраз цю проблему вирішують Generics. Замість того, щоб дублювати код, ми створюємо один загальний тип і розширюємо його.
Теж саме можна робити з класами, інтерфейсами і функціями. Але для розуміння ідеї і проблеми яку вирішують Generics, цього прикладу буде цілком достатньо.
Що таке Type Narrowing в TypeScript?
Це дуже поширене запитання в контексті розмови про TypeScript. Якщо ви почули його на інтервʼю, це може бути хорошим сигналом того, що TS в проєкті використовується не просто для галочки.
Type Narrowing(звуження) означає конкретизацію типу для TypeScript. Наприклад, в нас є змінна з типом string | number i narrowing в цьому випадку буде означати перевірку змінної на те чи її значення має string чи number. В залежності від реального значення, ми будемо мати різну імплементацію, для різних типів даних.
Існує декілька способів звузити тип. В контексті цього запитання ми розберемо тільки базові, які зазвичай запитують на інтервʼю:
typeof
Це найпростіший спосіб, який використовується для примітивів. Якщо TypeScript зустрічається з Union типом, він не дозволяє викликати методи чи доступатись до полів, які присутні тільки в одному з типів. В такому випадку, вам потрібно явно перевірити, що значення є конкретним типом. Або використати as, але це вважається Bad Practice в TypeScript 👹
instanceof
Це звуження типу, через перевірку чи є об’єкт екземпляром певного класу. Він використовується не так часто, як наприклад typeof, але для розуміння Type Narrowing потрібно розібрати і його. Типовий приклад використання, це керування помилками:
Зазвичай, розуміння typeof i instanceof цілком достатньо в контексті інтервʼю. Але якщо ви хочете копнути трохи глибше, ось інші способи Type Narrowing:
- Discriminated unions
- Operator in
- Type guards
- Assertion functions
Що таке Hoisting в JavaScript?
Зазвичай, це запитання можуть задавати в контексті розмови про функції. А саме різниці між Arrow Function та звичайними або при обговорення контексту виконання функцій(this). Тож давайте розберемо, що таке hoisting. Простими словами це механізм JavaScript, який спочатку зчитує файл перед виконанням і реєструє всі змінні та функції. Це дає змогу звертатись до них в коді перед оголошенням. Розглянемо як працює hoisting з різними сутностями в JS:
Var
В реальному проєкті ви скоріше за все не зіштовхнетесь з var(якщо це не древній legacy), але розібравшись як працює hoisting в цьому випадку, дасть ширше розуміння концепції. В прикладі, під час першого зчитування файлу змінна a була зареєстрована і якщо ви хочете доступитись до неї перед оголошенням, вона буде мати значення undefined.
Function declaration
В цьому випадку при hoisting, реєструється як назва функції так і її тіло тому вона доступна для використання, перед оголошенням. Важливо наголосити, що це працює тільки з Function Declaration і не буде працювати з Function Expression.
Let і const
Фактично, змінні чи константи піддаються hoisting. Вони реєструються так само як і змінні оголошені через var, але замість присвоєння undefined, let i const потрапляють в TDZ(Temporal Dead Zone). Тому при спробі доступу до них перед оголошенням, ви отримаєте помилку - ReferenceError.
Інформації вище, цілком достатньо для того, щоб ваша відповідь на інтервʼю звучала впевнено і інтервʼювер зрозумів, що ви розбирається в даному питанні.
Які відмінності Arrow function від звичайної?
Питання про функцію звичайну та стрілкову(Arrow Function) ви можете почути майже на кожному інтервʼю. Існує декілька відмінностей і зараз ми детально їх розберемо:
Syntactic sugar
Найбільш очевидна, але не сама важлива відмінність - це зручність використання і лаконічність Arrow Function. Нижче, я приведу декілька прикладів того, як можна спрощувати невеликі функції.
Arguments
Arrow Function не має свого псевдомасиву arguments. Це повʼязано з тим що стрілкові функції використовують філософію Lambda і створені, щоб бути легковісними і максимально простими. Окрім цього, arguments вважається застарілим API i зараз ви можете використовувати більш сучасний підхід - Rest parameters.
Hoisting
Звичайна функція, оголошена через ключове слово function, хоститься наверх. Це означає, що навіть якщо ви напише цю функцію з самого низу - звернутись до неї буде можна в будь-якому місці.
Arrow Function створюється як значення змінної і не хоститься наверх. Тому звернутись до неї можна тільки після того, як ми її визначили. Якщо ж ви звернетесь до неї перед оголошенням, ви отримаєте помилку.
New
Звичайні функції можна викликати як конструктор з new, тоді як з Arrow Function це не спрацює, і буде помилка. Це повʼязано з тим що стрілкова функція не має внутрішнього поля [[Construct]] тому і не може бути конструктором.
Контекст виконання(this)
Остання і найважливіша відмінність це відсутність контексту виконання в Arrow Function. Це зроблено по тій самі причині, як і відсутність arguments і робить функцію максимально простою, згідно з філософією Lambda.
В звичайній функції оголошеній через ключове слово function, this буде залежати від місця її виклику(call-site). Тоді як в Arrow Function, this береться ззовні - де вона була оголошена(лексична область). Це робить стрілкову функцію більш безпечною з точки зору передбачуваності контексту виконання.
Яка різниця між map і forEach в Array
Доволі багато запитань на інтервʼю стосується методів масиву. Вас можуть запитати, які методи ви, до прикладу, знаєте. І коли ви закінчите перелік, одне з питань може звучати так: “Яка різниця між методами map і forEach в масиві і в яких випадках краще використовувати кожний з них? ”
На технічній співбесіді, варто сказати, що вони обоє перебирають масив, але різниця полягає в тому, що метод map імутабельний і він буде створювати новий масив на основі вихідного, а метод forEach просто його перебере, без повернення якогось результату.
Розберемо приклад
Ви будете перемножувати кожний елемент масиву і потім повертати результат. У випадку з map - ви отримаєте новий масив, з forEach - вам доведеться використовувати замкнення для того, щоб зберігати результат.
Що таке n + 1 проблема в GraphQL?
Проблема N + 1 в GraphQL - це класичне запитання в контексті розмови про дизайн GraphQL API або порівнянні його з REST. Вона виникає, коли один GraphQL запит призводить до великої кількості дрібних запитів до бази даних.
Розберемо простий приклад з інтернет магазином. Ми робимо запит на отримання продуктів і хочемо отримати коментарі до них. Якщо б ми використовували REST API, то могли зробити JOIN на рівні ORM і витягнути всі дані одним запитом. Але в GraphQL ми витягнемо всі продукти одним запитом і після того почнемо викликати Resolvers окремо для кожного продукту.
Для кожного продукту буде запущений окремий SQL запит. Якщо в нас 100 продуктів, то буде запущено 100 додаткових запитів в базу даних, які витягнуть коментарі для кожного окремого продукту.
Як вирішити цю проблему?
Вирішується вона доволі просто, за допомогою DataLoader, основна задача якого - Batching. В нашому випадку DataLoader буде збирати всі product ids в один масив і викликати функцію, яка витягне коментарі більш оптимізовано. Наприклад, замість 100 запитів в базу даних ми можемо отримати все за один запит, використовуючи WHERE IN([…productIds]).
Що таке мемоізація?
Мемоізація - це одна з технік оптимізації при якій ми запамʼятовуємо результат виконання функції для конкретних вхідних даних і при наступних викликах з тими ж аргументами повертаємо вже готовий результат, замість того щоб знову все перераховувати.
Розберемо простий приклад
Уяви, що працюєш над криптобіржою і тобі потрібно перераховувати ціну монет в USD і показувати для користувача.
Якщо баланс користувача і ціни монет не змінилися, ми можемо мемоізувати результат обчислень і повертати його з кешу, замість того щоб кожен раз знову перемножувати всі монети на їхню ціну. Наприклад, ціна USDT практично не змінюється і ми будемо брати її з кешу до тих пір поки клієнт не поповнить свій рахунок.
В контексті інтервʼю, важливо підкреслити, що ця техніка особливо важлива в контексті оточень з обмеженими CPU ресурсами. Наприклад мобільна версія сайту чи різні гібридні дотатки на React Native, Expo, Ionic i тд.
Що таке імутабельність?
Імутабельність (immutability) - це підхід у програмуванні, за якого дані не змінюються після їх створення. Замість зміни існуючих структур даних створюється нова копія з оновленими значеннями.
Таким чином, в нас не буде неочікуваних оновлень змінних чи обʼєктів. Ця техніка особливо корисна, якщо код складний і заплутаний. Імутабельний код набагато простіше тестувати і він в цілому більш передбачуваний.
Простий приклад використання
Ця техніка широко поширена в світі JS, особливо на стороні Frontend, наприклад в більшості бібліотек і фреймворків. Також, в самому JS зʼявляються імутабельні альтернативи методів. Наприклад toSorted, toReversed, with, toSpliced i тд.
Яка різниця між useCallback i useMemo?
Почнемо з того, що вони мають однакову ціль - зменшити кількість непотрібних рендерів компонента, що зменшить використання CPU і в теорії збереже вас від потенційних проблем з продуктивність. Особливо, якщо користувач відкриває вебсайт з телефону або ваш додаток запускається на Ionic, React Native, etc.
Коли використовувати useMemo
Простими словами, він мемоізує результат обчислення. Уявіть, в вас трейдинг платформа, яка в реальному часі отримує дані про ціну різних активів. Вам на стороні фронтенду потрібно перераховувати ці значення і відносно них, оновлювати різні графіки, таблиці, портфель користувача і тд. Але не всі оновлення потребують повторного рендеру. Наприклад, якщо ціна активу не помінялась, то і графік з ним перемальовувати не потрібно. Мемоізація(useMemo) дозволяє закешувати значення і оновлювати компонент тільки тоді, коли вхідні параметри змінились.
Коли використовувати useCallback
Якщо в нас відбувається рендер компонента, то функції, які були в ньому обʼявлені також будуть перестворюватись. В теорії, це може мати вплив на продуктивність, особливо при використанні різних супер абстрактних фреймворків, по типу Ionic. useCallback - дозволяє повертати ту ж саму функцію між рендерами, поки залежності не зміняться. Таким чином він мемоізує функцію і допомагає нам уникнути зайвих перестворень.
На інтервʼю, хорошим тоном буде сказати, що будь які оптимізації продуктивності повинні грунтуватись на поточних або потенційних проблемах і найгірше, що можна робити, це витрачати час на оптимізацію в якій нема ніякого сенсу.
Що таке чиста функція?
Чиста функція - це функція, яка при однакових вхідних даних, завжди повертає однаковий результат і не має побічних ефектів. Визначення може плутати тому по розберемо приклади цих характеристик:
Однаковий результат при однакових аргуметах
Відсутність побічних ефектів (Side Effects)
Функція не має побічних ефектів, якщо вона не змінює зовнішні змінні, обʼєкти, файли, DOM, БД, мережеві ресурси.
Для того, щоб зробити вашу відповідь більш професіональною ви можете додати, що ця техніка широко використовується в програмуванні як на бекенді так і на фронтенді. Хорошим тоном буде привести 1 - 2 приклади з фреймворка яким ви користуєтесь, наприклад React.js, Nest.js, Angular, etc. Також, варто згадати, що використання цієї концепції спрощує написання Unit i Integration тестів.
Що не так з var у циклі та setTimeout?
Одне з бородатих запитань, яке на даний час можна зустріти тільки на інтервʼю в так звані треш-галери. Найчастіше, питання поділяється на 2 частини:
Спочатку вас запитають який вивід в консолі
Ми отримали такий вивід через те що var має функціональну область видимості тому фактично, змінна і спільна для всіх ітерацій. Вона перезаписується на кожній ітерації і на момент завершення циклу, її значення буде 3. Так як setTimeout це макрозадача, вона буде чекати завершення синхронного коду і на момент виклику всіх трьох console.log і=3.
Далі вас можуть запитати як це виправити
Самий очевидний спосіб, це використати let. Він має блочну область видимості і на кожну ітерацію буде створюватись нова область видимості. Окрім let, можна використати IIFE (Immediately Invoked Function Expression), яка теж буде створювати нову область видимості на кожну ітерацію.
Яка різниця між async/await i chaining?
Це запитання дуже часто задають в контексті розмови про Promise чи Event Loop. Якщо дуже коротко, то різниці з технічної точки зору немає. Можна сказати, що async/await це синтаксичний цукор поверх chaining, який дозволяє писати асинхронний код в синхронному стилі, як це прийнято в більшості мов програмування, наприклад C#.
Вибір між chaining i async/await - це справа смаку, але як показує практика, синхронний стиль написання набагато зрозуміліший і зазвичай, вибір більшості розробників - це async/await.
Розглянемо приклад
Як працює RIGHT JOIN?
RIGHT JOIN - це тип JOIN, який повертає всі рядки з правої таблиці, навіть якщо у лівій таблиці немає відповідних збігів. Якщо збіг у лівій таблиці існує - дані з неї додаються, а якщо немає - замість значень з лівої таблиці буде NULL.
Розберемо приклад
У нас є дві таблиці: users і orders. Наша задача - отримати всі замовлення разом з інформацією про користувача, навіть якщо користувача немає (наприклад, замовлення створено, але користувач був видалений чи не існує).
Запит буде мати наступний вигляд. Розберемо його детальніше:
В останньому рядку, ім’я буде NULL, але замовлення Monitor буде в результаті, тому що RIGHT JOIN гарантує, що всі рядки з таблиці orders (правої таблиці) будуть включені.
Яка різниця між async та defer?
Це питання часто задають в контексті оптимізації frontend частини продукту. Async і Defer - це атрибути, які використовуються в тегу для підключення зовнішніх JS файлів. Обидва дозволяють НЕ блокувати парсинг HTML під час завантаження скриптів, але вони по-різному поводяться під час виконання. Розберемо, як завантажуються скрипти без додаткових атрибутів і з атрибутами async/defer:
Без атрибутів
Завантаження зовнішніх скриптів блокує парсинг HTML, і все виконується синхронно. Якщо підключити кілька скриптів у <head>, браузер буде чекати, поки не завантажить і не виконає кожен із них.
Async
При такому підході, завантаження скриптів не буде блокувати парсинг HTML і відбуватиметься паралельно. Скрипт виконається одразу після завантаження, навіть якщо парсинг HTML ще НЕ завершено.
Defer
Так само як async, defer не блокує парсинг HTML і завантажується паралельно. Але скрипти завжди виконуються в порядку підключення і тільки після завершення побудови DOM.
Як можна визначити складність алгоритму?
Найперше, розберемось з тим що таке складність алгоритму. Це спосіб оцінити, скільки ресурсів алгоритм споживає в залежності від розміру вхідних даних. Існує два способи, щоб виміряти складність алгоритму.
Time Complexity
Часова складність алгоритму - це спосіб зрозуміти, наскільки він повільнішає, коли даних стає більше. Для вимірювання використовується Big O нотація, яка показує верхню межу вимірювання.
Розберемо детальніше
O(1) - Яскравим прикладом константної складності є операція присвоювання, або доступ до поля обʼєкта по ключу.
O(log n) - При цій складності, час виконання росте дуже повільно, навіть при значному збільшенні вхідних даних. Прикладів його застосування дуже багато і найпростіший це бінарний пошук.
O(n) - При цій складності, час виконання росте прямо пропорційно збільшенню кількості елементів. Наприклад, ми перебираємо масив з 100 елементів. При використанні алгоритму з логарифмічною складність, при збільшенню кількості елементів до 100 000, ми майже не відчуємо змін в часі виконання алгоритму, тоді як при використанні O(n) алгоритм значно сповільниться.
O(n log n) - Простими словами це поєднання лінійної і логарифмічної складності. Наприклад наш алгоритм використовує стратегію divide and conquer O(log n), щоб розділяти масив і використовує перебір O(n), щоб обробити усі елементи.
O(n²) - Це відбувається тоді, коли для кожного елемента алгоритм перевіряє всі інші елементи. Найчастіше, коли є два вкладені цикли.
O(2ⁿ) - Експоненційна складність є найповільнішою серед всіх. Якщо описати її простими словами, то вона подвоює кількість операції. Наприклад, на 10 елементів, потрібно буде зробити орієнтовно 1024 операції.
Простий приклад визначення складності алгоритму
В нас є функція, яка повертає мінімальне значення масиву. Фактично ми маємо 2 операції, це присвоєння в let min = arr[0] i перебір масиву arr . Операція присвоєння завжди займає константний час O(1), так як не залежить від кількості елементів, тоді як при переборі масиву ми залежимо від кількості елементів в цьому масиві тому складність буде O(n). Визначення складності алгоритму, відбувається по найскладнішій операції. В нас це перебір масиву тому складність функції findMin буде O(n).
Space Complexity
Це кількість додаткової пам’яті, яку алгоритм потребує, окрім вхідних даних. Наприклад, створення додаткових змінних, масивів, обʼєктів і тд. Для визначення space complexity ми використовуємо ту саму Big O нотацію, але оперуємо не кількістю операцій, а кількістю задіяних структур.
Після того як ви відповіли на питання, буде дуже великим плюсом наголосити на важливість розуміння складності, так як це має пряме відношення до розуміння структур даних, а стркутури є фундаментом для розуміння сучасних розподілених систем, баз даних, кешування і багатьох інших важливих тем.
Як працює LEFT JOIN?
LEFT JOIN - це тип JOIN, який повертає всі рядки з лівої таблиці, навіть якщо у правій таблиці немає відповідних збігів. Якщо збіг у правій таблиці є - дані з неї додаються а якщо збігу немає - замість значень з правої таблиці буде NULL.
Розберемо приклад
Ми маємо дві таблиці users i orders. Наша задача полягає в тому щоб отримати всіх користувачів разом з замовленнями. Якщо в користувача немає замовлення, ми все одно маємо включити його в список тому ми будемо використовувати LEFT JOIN.
Запит буде мати наступний вигляд. Давайте розберемо його детальніше:
В результаті виконання запиту, ми отримаємо такий результат:
Чи можна відправляти body в GET-запиті?
Це запитання можна віднести до так званих Tricky Questions. Задають його доволі рідко і найчастіше - коли бачать в твоєму CV ElasticSearch. Просто щоб перевірити, чи дійсно ти працював з ним.
Якщо говорити коротко - ТАК, body в GET-запиті можна відправляти. Але хорошим тоном буде дати розгорнуту відповідь на це запитання. В цілому, відправка body в GET-запиті суперечить семантиці HTTP i якщо ви будете це робити, будьте готові, що різні бібліотеки, які працюють з парсингом HTTP-запитів, можуть його не бачити.
Але є винятки, наприклад ElasticSearch, який повністю підтримує і використовує body в GET-запитах. Це зроблено для того, щоб дати змогу простіше описувати складні пошукові запити і уникнути використання POST методів для читання даних.
Яка різниця між PUT i PATCH?
Скоріше за все, ви почуєте це запитання в контексті розмови про дизайн REST API. Варто розуміти, що ці методи використовуються для оновлення ресурсу, але мають відмінність в семантиці.
Спосіб оновлення ресурсу
PUT замінює весь ресурс новим представленням. Простими словами, він очікує, що ви будете відправляти всі поля, які описують ресурс, включаючи ті, що не потребують оновлення. PATCH в свою чергу очікує, що ви передасте тільки ті поля, які потребують оновлення.
Ідемпотентність
Згідно семантики HTTP, PUT - це ідемпотентний метод. Якщо ви відправите його 10 разів з таким самим тілом, то він дасть такий ж результат, якщо б ви відправили його один раз. PATCH не обов’язково ідемпотентний і все залежить від реалізації.
Що таке ідемпотентність?
Отже, почнемо з визначення того, що таке ідемпотентність. Це концепція, яка гарантує, що багаторазове повторення дає той самий ефект, як і одноразове.
Найчастіше це запитання звучить в контексті розмови про дизайн REST API, тому доцільно буде розібрати цю концепцію в контексті HTTP-методів.
Наприклад, розберемо метод PUT. Згідно з REST, він займається повним оновленням ресурсу. І якщо ми відправимо його 10 разів з однаковим body, то всі ці виклики дадуть такий самий результат, як і один виклик цього методу.
Те саме стосується GET, DELETE і PATCH. Щодо PATCH, то, згідно зі специфікацією, він може бути як ідемпотентним, так і ні.
Але якщо розібрати метод POST, то згідно концепції REST, кожен виклик буде створювати новий запис в базі даних і таким чином, повторний виклик НЕ дасть однаковий ефект - отже, метод не ідемпотентний.
Додатково, я би рекомендував почитати про ідемпотентність в розподілених системах і розповісти про це на інтервʼю. Також, хорошою практикою буде згадати про Idempotency Key i навести приклад його використання.
Яка різниця між any i unknown в TypeScript?
Окрім того, що це запитання часто звучить на інтервʼю, різницю корисно розуміти і для роботи.
Any
Отже, спочатку розберемось з any - простими словами, коли ми його використовуємо, ми відключаємо TypeScript.
Unknown
Його часто порівнюють з безпечною альтернативою any. Якщо ми вказуємо тип unknown, то ми можемо записувати будь що, але при використанні ми забовʼязані виконати type narrowing і перевірити тип.
Якщо підсумувати, то з точки зору безпеки типів - потрібно завжди використовувати unknown і уникати використання any.
Що таке композитні індекси?
Скоріше за все, вам зададуть це запитання в контексті розмови про індекси і від вас будуть очікувати розуміння того, що послідовність при створенні і використанні індексу має значення.
Почнемо зі створення:
При створенні композитного індекса, потрібно керуватись селективністю. Спочатку має йти поле з найбільшою селективністю(максимально обрізаємо результати), наприклад customer_id, після чого беремо поле з меньшою селективністю(status) і останнє поле має найменшу селективність(created_at).
Причина такої послідовності - це структура індекса. Якщо спростити, то можна сказати, що вона вкладена і ми можемо доступитись до status, тільки знаючи customer_id.
Давай розберемо на прикладі:
Індекс буде створений коректно, але тобі потрібно буде пояснити правило. При пошуку по цьому індексу, він буде ефективний тільки в випадку передачі значень зліва на право. Отже, якими правилами буде керуватись Query Planner при пошуку:
- Очевидно, якщо ми передамо
customer_id,statusicreated_atіндекс спрацює так як ми очікуємо. - Якщо ми передамо тільки
customer_idіstatus, індекс все ще буде працювати і шукати дані по цим двом полям. - Якщо ми передамо
customer_idicreated_at, індекс буде працювати, але Query Planner не врахуєcreated_at, бо ми пропустили status і таким чином розірвали індекс. Це можна побачити запустившиEXPLAIN. - Якщо ми передамо тільки
created_at, індекс не спрацює і буде виконаний full scan, так як ми не передалиcustomer_id, який стоїть на першому місці(префікс індексу).
Що таке замикання в JS?
Це напевно маст хев питання на інтервʼю, особливо для Junior i Middle розробників. Якщо вас запитали його, то ваша задача відповісти в неочікуваному ключі.
Найперше, розкажіть простими словами, що таке замикання.
Простими словами - це здатність функції запамʼятовувати область видимості, де вона була оголошена.
Розберемо на простому прикладі:
Після того як ви відповіли на питання, скажіть АЛЕ тут є нюанс.
Якщо розглядати замикання в контексті high-load чи навіть data-intensive в Node.js проектах, то його необережне використання, може привести до Memory Leaks.
Це відбувається тому що Garbage Collector не очищає обʼєкти на які є посилання. І якщо зберігати в замиканні великі обʼєкти, цілком імовірно отримати проблеми з памʼяттю. Тому варто памʼятати про WeakMap, WeakSet, WeakRef.
Яка різниця між let i var?
Це питання задають не часто, але воно все ще актуальне тому варто розуміти основні відмінності:
Область видимості
var має функціональну область видимості (function scope) тому якщо ми оголошуємо змінну var в блоці {}, вона все одно буде глобальна в рамках функції де була оголошена або в рамках глобального обʼєкту.
Hoisting
В цілому, як var так і let піднімаються на початок області видимості, але є суттєва різниця при спробі доступу до них.
var піднімається і присвоюється значення undefined, яке ми отримуємо при доступі let піднімається, але при спробі доступу, отримаємо помилку.
Повторне оголошення
var можна оголосити повторно в межах однієї області.
let не можна повторно оголосити в межах одного блоку, бо отримаємо помилку.
Що таке Clustered і Non-Clustered індекси?
Класичне питання на знання теорії баз даних і на мою думку з родзинкою. А родзинка заключається в тому, що ви і так працювали з clustered/non-clustered індексами, і точно знаєте їх. Просто не знаєте, що вони так називаються 😁
Найперше дайте відповідь на запитання:
Clustered
Він визначає в якому порядку дані будуть ФІЗИЧНО зберігатись на диску. З цього витікає, що таблиця може мати тільки один clustered index, це майже завжди primary key.
Non-Clustered
Він не змінює дані на диску, а створює окрему структуру для зберігання даних (B-Tree, Hash….). Простими словами, це звичайні індекси, які ми створюємо для оптимізації бази даних.
Після того як ви відповіли на запитання, в вас є можливість повести розмову в потрібне вам русло. Найкраще, після цього запитання почати розповідати про види Non-Clustered індексів.
Яка різниця між type i interface в TypeScript?
Це часте запитання на інтервʼю, особливо на Junior i Middle позиції. В цілому, те, що зазвичай хочуть почути на інтервʼю - це merge інтерфейсів.
Розберемо на прикладі:
Якщо ви спробуєте такий підхід з type, то отримаєте помилку. При відповіді на це питання буде бонусом додати випадки, коли краще використовувати type, коли interface:
- interface краще використовувати для опису структури обʼєктів і класів, як це і задумано в ООП.
- для всього іншого доцільніше використовувати type, особливо в контексті побудови складних типів: union, utility types, function signatures…