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