Детальний розбір запитань на інтервʼю
Добірка питань та відповідей для підготовки до технічних співбесід. Включає JavaScript/Node.js, System Design, бази даних та хмарні технології.
Теги:
Що не так з 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…