Перейти к основному содержимому

Фронтенд собеседование 2025 | Junior Frontend | Реальные вопросы и задачи

· 48 мин. чтения

Сегодня мы разберём реальное собеседование на позицию фронтенд-разработчика, в ходе которого кандидат Роман с полутора годами опыта продемонстрировал уверенные практические навыки в создании проектов, но заметно уступил в глубине теоретической базы по HTML, CSS и JavaScript. Интервью выявило типичную для начинающих разработчиков проблему: сильный крен в сторону практики при недостаточном понимании фундаментальных концепций языка и веб-технологий, что в итоге не позволило ему пройти отбор.

Вопрос 1. Что такое DOCTYPE и зачем он нужен?

Таймкод: 00:03:45

Ответ собеседника: Неправильный. Не смог дать точного определения DOCTYPE, вспомнил только что это что-то с помощью чего открывается HTML.

Правильный ответ:

Определение

<!DOCTYPE> — это декларация (не тег!), которая сообщает браузеру, какую версию HTML/XHTML использует документ. Она должна быть первой строкой в HTML-файле, до тега <html>.

Зачем нужен

  • Определение режима рендеринга браузера — без DOCTYPE браузер переключается в "quirks mode" (режим совместимости), где он эмулирует старые баги для совместимости со старыми страницами. С DOCTYPE браузер работает в "standards mode" или "almost standards mode".
  • Валидация — указывает валидатору, по какому DTD (Document Type Definition) проверять документ.

Синтаксис для HTML5

<!DOCTYPE html>

Это самый короткий и рекомендуемый DOCTYPE для современных веб-приложений.

Примеры старых DOCTYPE

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

Ключевые моменты

  • <!DOCTYPE> — не HTML-тег, а инструкция для браузера
  • В HTML5 он нужен только для активации standards mode
  • Без него браузер может отобразить страницу неожиданно из-за quirks mode

Вопрос 2. Что такое HTML, из каких основных тегов состоит HTML-страница?

Таймкод: 00:04:10

Ответ собеседника: Неполный. Назвал теги: header, body, title, но сначала не вспомнил про корневой тег HTML. Внутри HTML назвал head (для метатегов и title) и body.

Правильный ответ:

Определение

HTMLHyperText Markup Language (язык гипертекстовой разметки). Стандартный язык разметки для создания веб-страниц.

Структура HTML-страницы

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Заголовок страницы</title>
<link rel="stylesheet" href="styles.css">
<script src="script.js"></script>
</head>
<body>
<header>
<nav>Навигация</nav>
</header>
<main>
<article>
<h1>Заголовок</h1>
<p>Содержимое</p>
<section>Секция</section>
</article>
<aside>Боковая панель</aside>
</main>
<footer>Подвал</footer>
</body>
</html>

Основные теги и их назначение

1. Корневой тег <html> — оборачивает весь документ. Атрибут lang указывает язык содержимого.

2. Секция <head> — метаданные документа:

  • <meta charset="UTF-8"> — кодировка
  • <meta name="viewport"> — адаптивность для мобильных устройств
  • <title> — заголовок вкладки
  • <link> — подключение CSS
  • <script> — подключение JavaScript

3. Секция <body> — видимое содержимое:

  • <header> — шапка страницы
  • <nav> — навигация
  • <main> — основное содержимое
  • <article> — независимая статья
  • <section> — тематическая секция
  • <aside> — боковая панель
  • <footer> — подвал страницы
  • <h1>-<h6> — заголовки
  • <p> — параграф
  • <div> — блочный контейнер
  • <span> — строчный контейнер

4. Семантические теги<header>, <nav>, <main>, <article>, <section>, <aside>, <footer> — появились в HTML5 для улучшения семантики и доступности (accessibility).

Ключевой момент: кандидат на Go-позицию должен знать базовую структуру HTML, особенно если работает с веб-сервисами и рендерит шаблоны.

Вопрос 3. Что такое HTML5 и чем эта версия отличается от предыдущих?

Таймкод: 00:05:55

Ответ собеседника: Неполный. Предположил, что до 5-й версии были обновления, а потом все устроило или обновления происходят внутри HTML5. Не смог назвать конкретные фичи HTML5.

Правильный ответ:

Что такое HTML5

HTML5 — это пятая и текущая основная версия HTML, ставшая рекомендацией W3C в октябре 2014 года. С 2019 года развитие продолжается под названием HTML Living Standard — стандарт живёт и обновляется непрерывно, без привязки к номерам версий.

Ключевые отличия от HTML 4.01 / XHTML 1.0

1. Семантические теги

<!-- HTML4: без семантики -->
<div id="header">...</div>
<div id="nav">...</div>
<div id="content">...</div>
<div id="footer">...</div>

<!-- HTML5: семантическая разметка -->
<header>...</header>
<nav>...</nav>
<main>
<article>...</article>
<section>...</section>
<aside>...</aside>
</main>
<footer>...</footer>

Новые теги: <header>, <nav>, <main>, <article>, <section>, <aside>, <footer>, <figure>, <figcaption>, <details>, <summary>, <mark>, <time>.

2. Нативная поддержка мультимедиа

<!-- HTML4: требовался Flash или плагины -->
<object data="movie.swf">...</object>

<!-- HTML5: нативные теги -->
<video src="movie.mp4" controls poster="preview.jpg">
<source src="movie.webm" type="video/webm">
<source src="movie.mp4" type="video/mp4">
Ваш браузер не поддерживает видео.
</video>

<audio src="music.mp3" controls>
<source src="music.ogg" type="audio/ogg">
</audio>

3. Графика и рисование

<canvas id="myCanvas" width="800" height="600"></canvas>
<script>
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#FF0000';
ctx.fillRect(10, 10, 100, 50);
</script>

Также поддержка SVG (Scalable Vector Graphics) встроена нативно.

4. Формы — новые типы полей и валидация

<!-- HTML4 -->
<input type="text" name="email">
<input type="text" name="date">

<!-- HTML5: встроенные типы и валидация -->
<input type="email" name="email" required>
<input type="date" name="birthday">
<input type="number" name="age" min="18" max="99">
<input type="range" name="volume" min="0" max="100">
<input type="color" name="favorite">
<input type="search" name="q" placeholder="Поиск...">
<input type="url" name="website">

Новые атрибуты: placeholder, required, autofocus, pattern, min, max, step, autocomplete.

5. API для JavaScript

APIНазначение
Local Storage / Session StorageХранение данных на клиенте (до 5-10 МБ)
Geolocation APIОпределение геолокации пользователя
Web WorkersФоновые потоки выполнения
WebSocketsДвунаправленное соединение с сервером
Drag and Drop APIНативный drag & drop
History APIМанипуляция историей браузера
File APIРабота с файлами на клиенте

6. Упрощённый DOCTYPE

<!-- HTML 4.01 Strict -->
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">

<!-- HTML5 -->
<!DOCTYPE html>

7. Другие важные изменения

  • Устаревшие теги: <font>, <center>, <frame>, <frameset>, <big>, <strike> — удалены, заменены CSS
  • Поддержка офлайн-приложений через Application Cache и Service Workers
  • Атрибут data-* для хранения пользовательских данных: <div data-user-id="123">
  • ContentEditable для WYSIWYG-редактирования
  • Геолокация, перетаскивание, веб-хранилище — всё нативно

Для Go-разработчика: при работе с шаблонизаторами Go (html/template) важно знать HTML5-семантику, чтобы генерировать валидный и доступный HTML. Например, при рендеринге форм использовать правильные типы input и атрибуты валидации.

Вопрос 4. Какие существуют способы подключения CSS к HTML?

Таймкод: 00:06:39

Ответ собеседника: Неполный. Назвал подключение через тег style, через тег link для внешнего CSS-файла, через атрибут style внутри тегов (inline-стили), а также упомянул подключение стилей через JavaScript. Однако забыл про директиву @import.

Правильный ответ:

1. Внешние стили через <link> (рекомендуемый способ)

<head>
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="https://cdn.example.com/lib.css">
<link rel="stylesheet" href="print.css" media="print">
<link rel="stylesheet" href="mobile.css" media="screen and (max-width: 768px)">
</head>

Преимущества: кэширование браузером, разделение ответственности, возможность подключения с CDN, медиа-запросы для условной загрузки.

2. Внутренние стили через <style>

<head>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
</style>
</head>

Используется для критического CSS (above-the-fold), чтобы ускорить первую отрисовку страницы.

3. Inline-стили через атрибут style

<p style="color: red; font-size: 16px; margin: 10px;">
Текст с инлайн-стилями
</p>

Применяется редко, имеет наивысший приоритет (кроме !important). Часто используется при динамическом добавлении стилей через JavaScript.

4. Директива @import

<!-- Внутри <style> -->
<style>
@import url('reset.css');
@import url('fonts.css') screen;

body { margin: 0; }
</style>

<!-- Или внутри внешнего CSS-файла -->
<!-- styles.css */
@import url('components/buttons.css');
@import url('components/forms.css') screen and (min-width: 768px);

.container { width: 100%; }

Недостатки @import: блокирует параллельную загрузку, может вызывать FOUC (Flash of Unstyled Content). Предпочтительнее использовать <link>.

5. Подключение через JavaScript

// Динамическое создание link-элемента
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'dynamic-styles.css';
document.head.appendChild(link);

// Или через инлайн-стили
document.getElementById('myElement').style.color = 'blue';

// Или через CSSOM
const style = document.createElement('style');
style.textContent = '.dynamic { color: green; }';
document.head.appendChild(style);

6. CSS-переменные и темизация

:root {
--primary-color: #3498db;
--font-size-base: 16px;
}

.button {
background-color: var(--primary-color);
font-size: var(--font-size-base);
}

Приоритет стилей (специфичность)

От низкого к высокому:

  1. Стили браузера (user agent stylesheet)
  2. Внешние стили (<link>, @import)
  3. Внутренние стили (<style>)
  4. Inline-стили (style="")
  5. !important (переопределяет всё)

Рекомендации

  • Основной CSS подключать через <link> в <head>
  • Критический CSS встраивать через <style> для быстрой первой отрисовки
  • @import избегать в пользу <link> — он блокирует рендеринг
  • Inline-стили использовать только для динамических значений через JS
  • Для продакшена использовать минификацию и объединение файлов

Вопрос 5. Как подключить TypeScript-файл к index.html при работе на чистом JS без фреймворков?

Таймкод: 00:08:02

Ответ собеседника: Неправильный. Признался, что не знает, так как работал только в React с TypeScript, а не на чистом JavaScript. Не знает, что TypeScript компилируется в JavaScript и подключается через тег script.

Правильный ответ:

Ключевой принцип: TypeScript нельзя подключить напрямую в браузер

Браузер не понимает TypeScript. TS-файл должен быть скомпилирован (транспилирован) в JavaScript, и именно JS-файл подключается к HTML.

Шаг 1: Установка TypeScript

npm init -y
npm install typescript --save-dev

Шаг 2: Инициализация конфигурации

npx tsc --init

Создаётся файл tsconfig.json:

{
"compilerOptions": {
"target": "ES2015",
"module": "ES2015",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"sourceMap": true,
"esModuleInterop": true
},
"include": ["src/**/*"]
}

Шаг 3: Структура проекта

project/
├── src/
│ └── app.ts
├── dist/
│ └── app.js ← скомпилированный файл
├── index.html
├── tsconfig.json
└── package.json

Шаг 4: Написание TypeScript-кода

// src/app.ts
interface User {
name: string;
age: number;
}

function greet(user: User): string {
return `Привет, ${user.name}! Тебе ${user.age} лет.`;
}

const currentUser: User = { name: 'Анна', age: 25 };

document.addEventListener('DOMContentLoaded', () => {
const output = document.getElementById('output');
if (output) {
output.textContent = greet(currentUser);
}
});

Шаг 5: Компиляция

# Однократная компиляция
npx tsc

# Режим наблюдения (перекомпилирует при изменениях)
npx tsc --watch

Шаг 6: Подключение скомпилированного JS к HTML

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>TypeScript Demo</title>
</head>
<body>
<div id="output"></div>

<!-- Подключаем СКОМПИЛИРОВАННЫЙ JavaScript, а не TypeScript! -->
<script src="dist/app.js"></script>
</body>
</html>

Альтернативный способ: подключение через модули

<!-- type="module" для ES-модулей -->
<script type="module" src="dist/app.js"></script>

Важные моменты

  • tsc --watch — удобен для разработки, автоматически перекомпилирует при сохранении файлов
  • sourceMap: true — генерирует .map-файлы, позволяя отлаживать TypeScript-код в DevTools браузера, хотя выполняется JavaScript
  • Для продакшена используется сборщик (Vite, esbuild, webpack), который делает то же самое, но с оптимизацией
  • Без сборщика в браузер всегда попадает только JavaScript

Связь с Go: если Go-бэкенд отдаёт статику, то в шаблонах подключается именно скомпилированный JS:

// Пример отдачи статики из Go
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("dist"))))

Вопрос 6. Какие файлы получаются на выходе после сборки фронтенд-проекта?

Таймкод: 00:09:39

Ответ собеседника: Правильный. Назвал три основных файла: HTML, CSS и JavaScript.

Правильный ответ:

Ответ верен, но можно дополнить деталями.

Основные типы файлов на выходе сборки

1. HTML-файлы — точка входа приложения:

  • index.html — главная страница
  • Могут быть дополнительные: 404.html, about.html и т.д.
  • Автоматически инъецируются ссылки на CSS и JS (через сборщики вроде Vite, Webpack)

2. CSS-файлы — стили:

  • main.css или styles.css — основной бандл стилей
  • Могут быть разделены на чанки: vendor.css, main.css
  • Минифицированы и оптимизированы

3. JavaScript-файлы — логика:

  • main.js или app.js — основной бандл
  • vendor.js — сторонние библиотеки (react, lodash и т.д.)
  • Чанки при код-сплиттинге: home.js, dashboard.js
  • Минифицированы и обфусцированы

4. Ассеты (статические ресурсы):

  • Изображения: .png, .jpg, .svg, .webp, .avif
  • Шрифты: .woff2, .woff, .ttf, .eot
  • Медиа: .mp4, .webm, .mp3
  • Другое: .ico, .webmanifest, .pdf

Типичная структура папки dist/ или build/:

dist/
├── index.html
├── assets/
│ ├── main-a1b2c3.css
│ ├── main-d4e5f6.js
│ ├── vendor-7890ab.js
│ ├── logo-1a2b3c.webp
│ └── fonts/
│ ├── Inter-Regular.woff2
│ └── Inter-Bold.woff2
├── favicon.ico
├── manifest.json
└── robots.txt

Важные особенности

  • Имена файлов содержат хеш (main-a1b2c3.css) — для инвалидации кэша
  • Файлы минифицированы — удалены пробелы, комментарии, имена переменных сокращены
  • Tree-shaking — удалён неиспользуемый код
  • Code splitting — код разделён на чанки для ленивой загрузки

Для Go-разработчика: при интеграции фронтенда с Go-бэкендом нужно корректно отдавать эту статику, учитывая хеши в именах файлов и настраивая заголовки кэширования.

Вопрос 7. В чём разница между visibility: hidden и display: none?

Таймкод: 00:10:25

Ответ собеседника: Правильный. visibility: hidden скрывает элемент, но он продолжает занимать место на странице. display: none полностью убирает элемент из потока документа, и он не занимает места.

Правильный ответ:

Ответ полностью корректен. Дополним деталями.

visibility: hidden

.hidden-element {
visibility: hidden;
}
  • Элемент невидим, но занимает место в макете
  • Потомки могут стать видимы через visibility: visible
  • Участвует в событиях мыши (в некоторых браузерах)
  • Не вызывает reflow (пересчёт позиций), только repaint

display: none

.removed-element {
display: none;
}
  • Элемент полностью удалён из потока документа
  • Не занимает место, соседние элементы «сдвигаются»
  • Все потомки тоже скрыты
  • Не реагирует на события
  • Вызывает reflow (пересчёт позиций всех элементов)

Дополнительные варианты скрытия

/* Прозрачность — элемент виден для событий */
.opacity-hidden {
opacity: 0;
pointer-events: none; /* отключаем события */
}

/* Сдвиг за пределы экрана — для accessibility */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}

/* Современный способ (CSS3) */
.clip-hidden {
clip-path: inset(100%);
clip: rect(0 0 0 0);
}

Сравнительная таблица

СвойствоЗанимает местоРеагирует на событияReflowПотомки видны
visibility: hiddenДаЧастичноНетДа (если visible)
display: noneНетНетДаНет
opacity: 0ДаДаНетНет

Когда что использовать

  • visibility: hidden — когда нужно скрыть элемент, но сохранить макет (например, placeholder в формах)
  • display: none — когда элемент нужно полностью убрать (табы, модальные окна, условный рендеринг)
  • opacity: 0 + pointer-events: none — для анимаций появления/исчезновения

Вопрос 8. Что такое специфичность CSS-селекторов и как она рассчитывается?

Таймкод: 00:10:47

Ответ собеседника: Неправильный. Первоначально не знал термина, но после подсказки о разных типах селекторов (по ID, классу, тегу) вспомнил, что ID имеет наивысший приоритет, затем класс, затем теги.

Правильный ответ:

Специфичность (Specificity) — это алгоритм, по которому браузер определяет, какой стиль применить, когда несколько правил претендуют на один элемент.

Система весов — четыре уровня

Специфичность записывается как кортеж (a, b, c, d):

(0, 0, 0, 0) — минимальная специфичность
(1, 0, 0, 0) — максимальная

Уровень a: Inline-стили

<p style="color: red;">Текст</p>
<!-- Специфичность: (1, 0, 0, 0) -->

Уровень b: Селекторы по ID

#header { color: blue; }
/* Специфичность: (0, 1, 0, 0) */

#nav .menu { color: green; }
/* Специфичность: (0, 1, 1, 0) */

Уровень c: Классы, псевдоклассы, атрибуты

.button { color: orange; }
/* Специфичность: (0, 0, 1, 0) */

.btn:hover { color: purple; }
/* Специфичность: (0, 0, 2, 0) */

[type="text"] { color: gray; }
/* Специфичность: (0, 0, 1, 0) */

:first-child { color: pink; }
/* Специфичность: (0, 0, 1, 0) */

Уровень d: Теги и псевдоэлементы

p { color: black; }
/* Специфичность: (0, 0, 0, 1) */

div p { color: brown; }
/* Специфичность: (0, 0, 0, 2) */

p::before { content: "→"; }
/* Специфичность: (0, 0, 0, 2) */

Особые случаи

/* Универсальный селектор — не влияет на специфичность */
* { margin: 0; }
/* Специфичность: (0, 0, 0, 0) */

/* :where() — обнуляет специфичность */
:where(.btn, .link) { color: red; }
/* Специфичность: (0, 0, 0, 0) */

/* :is() — берёт максимальную специфичность из аргументов */
:is(#id, .class) { color: red; }
/* Специфичность: (0, 1, 0, 0) */

/* !important — переопределяет всё */
.button { color: red !important; }
/* Наивысший приоритет, игнорирует специфичность */

Примеры расчёта

/* (0, 0, 0, 1) */
p { }

/* (0, 0, 1, 1) */
div.container p { }

/* (0, 1, 1, 0) */
#sidebar .widget { }

/* (0, 1, 2, 3) */
#main article.post > h2.title::first-line { }

/* (0, 0, 2, 0) — два класса */
.btn.primary { }

/* (0, 0, 3, 1) — два класса + псевдокласс + тег */
.card:hover .title.text-large { }

Правила разрешения конфликтов

  1. Победитель — селектор с большей специфичностью
  2. При равной специфичности побеждает последний в коде (каскад)
  3. !important переопределяет всё (кроме другого !important с более высокой специфичностью)
  4. Inline-стили переопределяют всё, кроме !important
  5. Наследуемые стили имеют наименьший приоритет

Практические рекомендации

  • Избегать !important — делает код неподдерживаемым
  • Избегать чрезмерной вложенности — усложняет переопределение стилей
  • Использовать классы как основной способ стилизации
  • Использовать :where() для группировки без повышения специфичности
  • Методология BEM помогает держать специфичность на одном уровне

Вопрос 9. Что такое CSS и в чём заключается каскадность?

Таймкод: 00:11:53

Ответ собеседника: Неполный. CSS используется для стилизации DOM, включая flexbox. О каскадности сказал только что стили применяются сверху вниз, не раскрыл тему наследования и приоритетов.

Правильный ответ:

CSSCascading Style Sheets (каскадные таблицы стилей). Язык описания внешнего вида HTML-документа.

Каскадность — это механизм, определяющий, какой стиль будет применён к элементу, когда на него претендует несколько правил. Каскадность учитывает три фактора.

1. Источник стиля (Origin)

Приоритет от низкого к высокому:

1. Стили браузера (user agent stylesheet)
2. Стили пользователя (настройки браузера)
3. Стили автора (наш CSS) — внешние, внутренние, инлайн
4. !important в стилях автора
5. !important в стилях пользователя

2. Специфичность (Specificity)

При равном источнике побеждает более специфичный селектор:

/* Менее специфичный — (0, 0, 0, 1) */
p {
color: black;
}

/* Более специфичный — (0, 0, 1, 0) */
.text {
color: blue;
}

/* Ещё более специфичный — (0, 1, 0, 0) */
#content {
color: red;
}

/* Самый специфичный — (1, 0, 0, 0) */
<p style="color: green;">

3. Порядок объявления (Source Order)

При равной специфичности побеждает последнее правило:

.text {
color: blue;
}
.text {
color: red; /* Этот стиль победит */
}

Наследование (Inheritance)

Некоторые свойства передаются от родителя к потомкам:

/* Наследуемые свойства */
body {
font-family: Arial; /* наследуется */
color: #333; /* наследуется */
line-height: 1.5; /* наследуется */
}

/* Ненаследуемые свойства */
.container {
border: 1px solid black; /* НЕ наследуется */
margin: 20px; /* НЕ наследуется */
padding: 10px; /* НЕ наследуется */
}

Принудительное наследование:

.child {
border: inherit; /* наследует от родителя */
margin: initial; /* сбрасывает к начальному значению */
padding: unset; /* наследует если наследуемое, иначе initial */
}

Полный алгоритм каскада

1. Сортировка по источнику и !important
2. Сортировка по специфичности
3. Сортировка по порядку объявления
4. Применение наследования (если нет явного значения)
5. Применение начального значения (initial)

Пример работы каскада

/* Правило 1: (0, 0, 1, 1) */
div.text {
color: blue;
}

/* Правило 2: (0, 0, 1, 0) — менее специфичный */
.text {
color: green;
}

/* Правило 3: (0, 0, 0, 1) — наименее специфичный */
div {
color: red;
}
<div class="text">Текст</div>
<!-- Результат: blue (Правило 1 победило по специфичности) -->

Ключевой момент: «Каскад» в названии CSS означает именно этот многоуровневый механизм разрешения конфликтов, а не просто «применение сверху вниз».

Вопрос 10. Что такое БЭМ (Блок-Элемент-Модификатор)?

Таймкод: 00:12:32

Ответ собеседника: Неправильный. Слышал о БЭМ, но не смог объяснить, что это такая методология именования классов в CSS.

Правильный ответ:

БЭМ — методология именования CSS-классов, разработанная Яндексом. Аббревиатура расшифровывается как Блок-Элемент-Модификатор. Цель — создать понятную, модульную и масштабируемую систему стилей.

Блок (Block)

Самостоятельный, независимый компонент страницы. Может быть простым или составным.

<!-- Блок: кнопка -->
<button class="button">Купить</button>

<!-- Блок: форма поиска -->
<form class="search-form">
<input type="text" class="search-form__input">
<button class="search-form__button">Найти</button>
</form>

<!-- Блок: карточка товара -->
<div class="card">
<img class="card__image" src="product.jpg">
<h2 class="card__title">Товар</h2>
<p class="card__description">Описание</p>
</div>

Элемент (Element)

Часть блока, которая не имеет самостоятельного смысла. Привязана к своему блоку. Обозначается двумя подчёркиваниями __.

<!-- Элементы блока card -->
<div class="card">
<img class="card__image" src="photo.jpg">
<div class="card__body">
<h2 class="card__title">Заголовок</h2>
<p class="card__text">Текст</p>
</div>
<div class="card__footer">
<button class="card__button">Подробнее</button>
</div>
</div>
/* Стили элементов */
.card { }
.card__image { }
.card__body { }
.card__title { }
.card__text { }
.card__footer { }
.card__button { }

Модификатор (Модификатор)

Вариация блока или элемента — изменяет внешний вид или поведение. Обозначается двумя дефисами --.

<!-- Модификаторы кнопки -->
<button class="button button--primary">Основная</button>
<button class="button button--secondary">Второстепенная</button>
<button class="button button--large">Большая</button>
<button class="button button--disabled" disabled>Отключена</button>

<!-- Модификаторы карточки -->
<div class="card card--featured">
<img class="card__image card__image--rounded" src="photo.jpg">
<h2 class="card__title card__title--large">Заголовок</h2>
</div>

<!-- Модификаторы меню -->
<nav class="menu menu--vertical">
<a class="menu__item menu__item--active" href="#">Главная</a>
<a class="menu__item" href="#">О нас</a>
</nav>

Пример полной структуры

<!-- Форма поиска с модификаторами -->
<form class="search-form search-form--compact">
<div class="search-form__group">
<input class="search-form__input search-form__input--error"
type="text"
placeholder="Поиск...">
<span class="search-form__error-text">Введите запрос</span>
</div>
<button class="search-form__button search-form__button--loading"
type="submit">
Найти
</button>
</form>

Принципы БЭМ

  • Блок независим — можно переместить в любое место страницы без поломки
  • Элемент зависим от блока — не используется отдельно
  • Модификатор не используется без блока/элемента — всегда дополняет основной класс
  • Нет вложенности в именованииcard__title, а не card__body__title
  • Один блок — один класс — не цепляем классы друг на друга для наследования

Преимущества

  • Понятная структура — по имени класса видно назначение
  • Низкая специфичность — все селекторы одного уровня (один класс)
  • Легко поддерживать и рефакторить
  • Компонентный подход — блоки можно переиспользовать
  • Нет конфликтов имён

Сравнение с обычным подходом

<!-- Без БЭМ: непонятная вложенность -->
<div class="card">
<div class="card-body">
<h2 class="card-title text-bold">Заголовок</h2>
<p class="card-text text-muted">Текст</p>
<button class="btn btn-primary btn-lg">Кнопка</button>
</div>
</div>

<!-- С БЭМ: явная структура -->
<div class="card">
<div class="card__body">
<h2 class="card__title card__title--bold">Заголовок</h2>
<p class="card__text card__text--muted">Текст</p>
<button class="card__button card__button--primary card__button--large">
Кнопка
</button>
</div>
</div>

Для Go-разработчика: при генерации HTML в шаблонах Go (html/template) использование БЭМ-нотации делает шаблоны более читаемыми и предсказуемыми.

Вопрос 11. Какие подходы к стилизации использовал в проектах?

Таймкод: 00:12:47

Ответ собеседника: Правильный. Использовал CSS-in-JS с помощью Styled Components в React, хотел бы также изучить SCSS.

Правильный ответ:

Ответ корректен. Дополним обзором основных подходов к стилизации.

1. Обычный CSS

/* styles.css */
.button {
padding: 10px 20px;
border: none;
border-radius: 4px;
background-color: #3498db;
color: white;
cursor: pointer;
}

.button:hover {
background-color: #2980b9;
}
<link rel="stylesheet" href="styles.css">
<button class="button">Кнопка</button>

2. CSS-препроцессоры (SCSS/SASS, LESS, Stylus)

// SCSS
$primary-color: #3498db;
$border-radius: 4px;

@mixin flex-center {
display: flex;
justify-content: center;
align-items: center;
}

.button {
padding: 10px 20px;
border-radius: $border-radius;
background-color: $primary-color;

&:hover {
background-color: darken($primary-color, 10%);
}

&--large {
padding: 15px 30px;
font-size: 18px;
}
}

.card {
@include flex-center;
padding: 20px;
}

3. CSS Modules

/* Button.module.css */
.button {
padding: 10px 20px;
background-color: #3498db;
}

.primary {
background-color: #2ecc71;
}
// Button.jsx
import styles from './Button.module.css';

function Button() {
return <button className={`${styles.button} ${styles.primary}`}>
Кнопка
</button>;
}
// Результат: class="Button_button_x7ks2 Button_primary_a3b4c"

4. CSS-in-JS (Styled Components, Emotion)

// Styled Components
import styled from 'styled-components';

const Button = styled.button`
padding: 10px 20px;
background-color: ${props => props.primary ? '#2ecc71' : '#3498db'};
color: white;
border: none;
border-radius: 4px;
cursor: pointer;

&:hover {
opacity: 0.9;
}

${props => props.large && `
padding: 15px 30px;
font-size: 18px;
`}
`;

// Использование
<Button primary large>Кнопка</Button>

5. Utility-First CSS (Tailwind CSS)

<!-- Tailwind CSS -->
<button class="px-5 py-2 bg-blue-500 text-white rounded hover:bg-blue-600
transition-colors duration-200">
Кнопка
</button>

<div class="flex flex-col gap-4 p-6 bg-white rounded-lg shadow-md">
<h2 class="text-xl font-bold text-gray-800">Заголовок</h2>
<p class="text-gray-600">Текст карточки</p>
</div>

6. Сравнительная таблица

ПодходИзоляцияДинамичностьРазмер бандлаКривая обучения
CSSНетНетМинимальныйНизкая
SCSSНетОграниченаМинимальныйНизкая
CSS ModulesДаОграниченаМинимальныйСредняя
CSS-in-JSДаПолнаяБольшеСредняя
TailwindДа (классы)ОграниченаСредняя (purge)Средняя

Для Go-разработчика: при разработке веб-приложений на Go с рендерингом на сервере (html/template) чаще всего используется обычный CSS или SCSS. CSS Modules и Tailwind хорошо подходят для гибридных приложений, где Go отдаёт API, а фронтенд — отдельное SPA-приложение.

Вопрос 12. Что такое замыкание в JavaScript?

Таймкод: 00:13:46

Ответ собеседника: Правильный. Замыкание — это сохранение переменных родительской функции даже после завершения её работы. Вложенная функция имеет доступ к переменным внешней функции.

Правильный ответ:

Ответ корректен. Дополним деталями и примерами.

Замыкание (Closure) — это функция вместе с лексическим окружением, в котором она была создана. Функция «запоминает» переменные из области видимости, где была определена, даже после того как эта область видимости завершила работу.

Базовый пример

function createCounter() {
let count = 0; // переменная внешней функции

return function increment() {
count++; // замыкание имеет доступ к count
return count;
};
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// переменная count сохраняется между вызовами

Как это работает

  1. При вызове createCounter() создаётся контекст выполнения с переменной count
  2. Возвращаемая функция increment сохраняет ссылку на этот контекст
  3. Даже после завершения createCounter(), переменная count не удаляется сборщиком мусора, потому что на неё есть ссылка из замыкания

Практические применения

1. Приватные переменные (инкапсуляция)

function createUser(name) {
let _name = name; // приватная переменная

return {
getName: function() {
return _name;
},
setName: function(newName) {
_name = newName;
}
};
}

const user = createUser('Анна');
console.log(user.getName()); // "Анна"
user.setName('Мария');
console.log(user.getName()); // "Мария"
console.log(user._name); // undefined — нет доступа

2. Фабричные функции

function createMultiplier(multiplier) {
return function(number) {
return number * multiplier;
};
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

3. Мемоизация

function createMemoizer() {
const cache = {};

return function(key, fn) {
if (cache[key]) {
return cache[key];
}
const result = fn();
cache[key] = result;
return result;
};
}

const memoize = createMemoizer();
memoize('expensive', () => { /* тяжёлые вычисления */ });

4. Обработчики событий

function setupButton(buttonId, message) {
const button = document.getElementById(buttonId);

button.addEventListener('click', function() {
// замыкание сохраняет доступ к переменной message
alert(message);
});
}

setupButton('btn1', 'Привет!');
setupButton('btn2', 'Пока!');

Классическая ловшка с циклом

// НЕПРАВИЛЬНО — все функции ссылаются на одну переменную i
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 3, 3, 3
}, 100);
}

// ПРАВИЛЬНО — создаём замыкание для каждой итерации
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // 0, 1, 2
}, 100);
})(i);
}

// Или с let — создаётся новая область видимости на каждой итерации
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 0, 1, 2
}, 100);
}

Цепочка областей видимости (Scope Chain)

let global = 'глобальная';

function outer() {
let outerVar = 'внешняя';

function inner() {
let innerVar = 'внутренняя';

console.log(global); // доступно
console.log(outerVar); // доступно — замыкание
console.log(innerVar); // доступно
}

return inner;
}

const fn = outer();
fn(); // все три переменные доступны благодаря замыканию

Важные моменты

  • Замыкание создаётся автоматически — не нужно явно его «создавать»
  • Каждая функция в JavaScript является замыканием
  • Замыкания хранят ссылку на переменные, а не копии значений
  • Чрезмерное использование замыканий может привести к утечкам памяти, если ссылки не освобождаются

Вопрос 13. Можно ли привести пример замыкания без вложенных функций?

Таймкод: 00:14:19

Ответ собеседника: Неправильный. Не смог привести альтернативный пример, знает только паттерн с функцией внутри функции.

Правильный ответ:

Да, замыкание не требует вложенных функций. Замыкание возникает всякий раз, когда функция имеет доступ к переменным из внешней области видимости.

Замыкание через возврат объекта

function createCounter() {
let count = 0;

return {
increment: () => ++count,
decrement: () => --count,
getCount: () => count,
reset: () => { count = 0; }
};
}

const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.decrement(); // 1
console.log(counter.getCount()); // 1

Замыкание в обработчиках событий

function initLogger(prefix) {
document.addEventListener('click', function(event) {
// замыкание сохраняет prefix
console.log(`[${prefix}] Click at:`, event.clientX, event.clientY);
});
}

initLogger('DEBUG');
// Каждый клик логируется с префиксом "[DEBUG]"

Замыкание с колбэками

function fetchWithRetry(url, maxRetries) {
let attempts = 0;

function attempt() {
fetch(url)
.then(response => {
if (!response.ok) throw new Error('Failed');
return response.json();
})
.catch(error => {
attempts++;
if (attempts < maxRetries) {
console.log(`Retry ${attempts}/${maxRetries}`);
setTimeout(attempt, 1000); // замыкание сохраняет attempts
}
});
}

attempt();
}

fetchWithRetry('/api/data', 3);

Замыкание с модулями (IIFE)

const utils = (function() {
// приватные переменные — замыкание
const PI = 3.14159;
const E = 2.71828;

return {
circleArea: (r) => PI * r * r,
circleCircumference: (r) => 2 * PI * r,
exponential: (x) => Math.pow(E, x)
};
})();

console.log(utils.circleArea(5)); // 78.53975
console.log(utils.PI); // undefined — недоступно

Замыкание в функциях высшего порядка

function withLogging(fn) {
return function(...args) {
console.log('Calling with args:', args);
const result = fn(...args); // замыкание сохраняет fn
console.log('Result:', result);
return result;
};
}

const add = (a, b) => a + b;
const loggedAdd = withLogging(add);

loggedAdd(2, 3);
// Calling with args: [2, 3]
// Result: 5

Замыкание в промисах

function createDelayedMessage(message, delay) {
const startTime = Date.now();

return new Promise((resolve) => {
setTimeout(() => {
const elapsed = Date.now() - startTime;
resolve(`${message} (waited ${elapsed}ms)`); // замыкание сохраняет message и startTime
}, delay);
});
}

createDelayedMessage('Hello', 1000).then(console.log);
// "Hello (waited 1002ms)"

Ключевой момент: замыкание — это не про синтаксис (вложенные функции), а про механизм. Любая функция, которая ссылается на переменную из внешней области видимости, создаёт замыкание. Даже глобальные функции технически являются замыканиями по глобальному объекту.

Вопрос 14. Что такое лексическое окружение в JavaScript?

Таймкод: 00:14:54

Ответ собеседника: Неправильный. Не знаком с термином лексическое окружение.

Правильный ответ:

Лексическое окружение (Lexical Environment) — это внутренняя структура данных, которая хранит информацию о переменных, функциях и области видимости. Это фундаментальная концепция JavaScript, лежащая в основе замыканий, поднятия (hoisting) и цепочки областей видимости.

Структура лексического окружения

Каждое лексическое окружение состоит из двух частей:

LexicalEnvironment = {
EnvironmentRecord, // хранилище переменных и функций
OuterEnvironment // ссылка на внешнее окружение (родитель)
}

Environment Record — хранит:

  • Объявления переменных (let, const, var)
  • Объявления функций
  • Параметры функции
  • Значение this

Outer Environment Reference — ссылка на родительское лексическое окружение, формирующая цепочку областей видимости (scope chain).

Типы лексических окружений

1. Глобальное лексическое окружение

let name = 'Анна';
const age = 25;

function greet() {
console.log(`Привет, ${name}!`);
}

// Глобальное лексическое окружение:
// {
// EnvironmentRecord: { name: 'Анна', age: 25, greet: <function> },
// OuterEnvironment: null // нет родителя
// }

2. Функциональное лексическое окружение

function outer() {
let x = 10;

function inner() {
let y = 20;
console.log(x + y); // 30 — доступ к x через цепочку областей
}

inner();
}

// outer: {
// EnvironmentRecord: { x: 10, inner: <function> },
// OuterEnvironment: GlobalEnvironment
// }
//
// inner: {
// EnvironmentRecord: { y: 20 },
// OuterEnvironment: outerEnvironment
// }

3. Блочное лексическое окружение

function example() {
let a = 1;

if (true) {
let b = 2;
const c = 3;
console.log(a + b + c); // 6
}

// console.log(b); // ReferenceError — b недоступна вне блока
}

// example: {
// EnvironmentRecord: { a: 1 },
// OuterEnvironment: GlobalEnvironment
// }
//
// if-блок: {
// EnvironmentRecord: { b: 2, c: 3 },
// OuterEnvironment: exampleEnvironment
// }

Механизм работы

Шаг 1: Создание окружения

function createCounter() {
let count = 0; // добавляется в EnvironmentRecord

return function() {
return ++count; // замыкание сохраняет ссылку на это окружение
};
}

const counter = createCounter();

Когда интерпретатор встречает функцию createCounter:

  1. Создаётся новое лексическое окружение
  2. count записывается в EnvironmentRecord
  3. OuterEnvironment указывает на глобальное окружение

Шаг 2: Цепочка областей видимости

let global = 'глобальная';

function outer() {
let outerVar = 'внешняя';

function inner() {
let innerVar = 'внутренняя';

console.log(innerVar); // найдено в своём окружении
console.log(outerVar); // найдено в outer окружении
console.log(global); // найдено в глобальном окружении
}

inner();
}

// Цепочка поиска переменной:
// innerEnv → outerEnv → globalEnv → null

Лексическое окружение vs контекст выполнения

Лексическое окружениеКонтекст выполнения (Execution Context)
Статическая структураДинамическая сущность
Создаётся при написании кодаСоздаётся при выполнении
Хранит переменные и ссылкиХранит состояние выполнения
Не меняется во время выполненияМеняется при каждом вызове функции

Связь с замыканиями

Замыкание — это функция + её лексическое окружение на момент создания:

function makeCounter() {
let count = 0; // хранится в EnvironmentRecord

return {
increment: () => ++count,
get: () => count
};
}

const counter = makeCounter();
// counter.increment и counter.get сохраняют ссылку
// на лексическое окружение makeCounter, где count = 0

Ключевой момент: лексическое окружение определяется местом написания кода, а не местом вызова. Поэтому оно называется «лексическим» (от слова «лексика» — структура кода). Это отличает JavaScript от языков с динамической областью видимости.

Вопрос 15. Что такое this в JavaScript и как работает контекст вызова?

Таймкод: 00:15:05

Ответ собеседника: Правильный. this указывает на контекст вызова. Если функция вызывается отдельно от объекта, контекст теряется, нужно использовать bind или другие методы для привязки контекста.

Правильный ответ:

Ответ корректен. Дополним полным разбором всех правил определения this.

this — это специальное ключевое слово, которое ссылается на объект, являющийся контекстом текущего выполнения функции. Значение this определяется способом вызова функции, а не местом её объявления.

Правило 1: Обычный вызов функции

function show() {
console.log(this);
}

show(); // undefined (strict mode) или window (non-strict mode)

В обычном вызове this равен undefined (в strict mode) или глобальному объекту (window в браузере, global в Node.js).

Правило 2: Вызов как метод объекта

const user = {
name: 'Анна',
greet() {
console.log(this.name);
}
};

user.greet(); // "Анна" — this указывает на user

this указывает на объект, на котором вызван метод.

Правило 3: Вызов через new (конструктор)

function User(name) {
this.name = name;
this.greet = function() {
console.log(this.name);
};
}

const user = new User('Мария');
user.greet(); // "Мария" — this указывает на новый созданный объект

Правило 4: Явная привязка (call, apply, bind)

function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}

const user = { name: 'Анна' };

greet.call(user, 'Привет', '!'); // "Привет, Анна!"
greet.apply(user, ['Здравствуй', '.']); // "Здравствуй, Анна."

const boundGreet = greet.bind(user, 'Добрый день');
boundGreet('!'); // "Добрый день, Анна!"

Различия:

  • call — вызывает функцию с переданными аргументами по одному
  • apply — вызывает функцию с аргументами в виде массива
  • bind — возвращает новую функцию с привязанным контекстом

Правило 5: Стрелочные функции

const user = {
name: 'Анна',

// Обычная функция — this зависит от вызова
regularGreet() {
console.log(this.name);
},

// Стрелочная функция — this берётся из внешнего лексического окружения
arrowGreet: () => {
console.log(this.name); // undefined — this из внешнего окружения
},

// Пример с setTimeout
delayedGreet() {
// Обычная функция — контекст теряется
setTimeout(function() {
console.log(this.name); // undefined
}, 100);

// Стрелочная функция — контекст сохраняется
setTimeout(() => {
console.log(this.name); // "Анна"
}, 100);
}
};

user.regularGreet(); // "Анна"
user.arrowGreet(); // undefined

Стрелочные функции не имеют собственного this. Они захватывают this из окружения, где были созданы.

Правило 6: Обработчики событий

// DOM-элемент
button.addEventListener('click', function() {
console.log(this); // элемент button — this указывает на элемент-источник события
});

button.addEventListener('click', () => {
console.log(this); // undefined или window — стрелочная функция берёт this извне
});

Потеря контекста и решения

const user = {
name: 'Анна',
greet() {
console.log(this.name);
}
};

// Потеря контекста
const fn = user.greet;
fn(); // undefined — контекст потерян

// Решение 1: bind
const boundFn = user.greet.bind(user);
boundFn(); // "Анна"

// Решение 2: стрелочная функция-обёртка
const wrapper = () => user.greet();
wrapper(); // "Анна"

// Решение 3: стрелочный метод
const user2 = {
name: 'Мария',
greet: () => console.log(this.name) // осторожно! this не указывает на user2
};

// Решение 4: сохранение в переменную
const self = user;
function callGreet() {
self.greet(); // "Анна"
}

Приоритет правил (от высшего к низшему)

  1. new — новый объект
  2. call / apply / bind — явно указанный объект
  3. Вызов как метод — объект слева от точки
  4. Обычный вызов — undefined (strict) или window (non-strict)
  5. Стрелочные функции — this из внешнего лексического окружения (не подчиняются правилам выше)

Вопрос 16. Почему this.a возвращает undefined и как это исправить?

Таймкод: 00:16:28

Ответ собеседника: Неполный. Правильно объяснил причину: функция вызывается вне контекста объекта, поэтому this ссылается на window, где нет свойства a. Однако не смог предложить конкретное решение с использованием bind/call/apply.

Правильный ответ:

Дополним полным набором решений.

Проблема

const obj = {
a: 42,
getA() {
return this.a;
}
};

const fn = obj.getA;
console.log(fn()); // undefined — контекст потерян

При присваивании obj.getA переменной fn функция отрывается от объекта. При вызове fn() это обычный вызов, и this равен undefined (strict mode) или window (non-strict).

Решение 1: bind — создание функции с привязанным контекстом

const fn = obj.getA.bind(obj);
console.log(fn()); // 42

bind возвращает новую функцию, у которой this навсегда привязан к obj.

Решение 2: call и apply — немедленный вызов с контекстом

console.log(obj.getA.call(obj)); // 42
console.log(obj.getA.apply(obj)); // 42

Разница между call и apply — только в способе передачи аргументов:

function sum(a, b) {
return this.base + a + b;
}

console.log(sum.call(obj, 1, 2)); // 42 + 1 + 2 = 45
console.log(sum.apply(obj, [1, 2])); // 42 + 1 + 2 = 45

Решение 3: стрелочная функция-обёртка

const fn = () => obj.getA();
console.log(fn()); // 42

Стрелочная функция вызывает obj.getA() как метод объекта, поэтому this указывает на obj.

Решение 4: стрелочный метод в объекте

const obj = {
a: 42,
getA: () => this.a // this берётся из внешнего окружения, НЕ из obj
};

Осторожно: этот способ не работает для методов объекта, потому что стрелочная функция берёт this из внешнего лексического окружения, а не из объекта.

Решение 5: сохранение контекста в переменную

const obj = {
a: 42,
getA() {
const self = this;
return function() {
return self.a;
};
}
};

const fn = obj.getA();
console.log(fn()); // 42

Решение 6: использование стрелочной функции как метода

const obj = {
a: 42,
getA() {
const helper = () => this.a;
return helper();
}
};

console.log(obj.getA()); // 42

Стрелочная функция helper берёт this из getA(), где this указывает на obj.

Решение 7: классы с привязкой в конструкторе

class MyClass {
constructor() {
this.a = 42;
this.getA = this.getA.bind(this); // привязываем в конструкторе
}

getA() {
return this.a;
}
}

const instance = new MyClass();
const fn = instance.getA;
console.log(fn()); // 42

Решение 8: классовые поля со стрелочными функциями

class MyClass {
a = 42;

getA = () => this.a; // стрелочная функция как классовое поле
}

const instance = new MyClass();
const fn = instance.getA;
console.log(fn()); // 42

Сравнение решений

РешениеКогда использовать
bindНужна отдельная функция с фиксированным контекстом
call / applyОдноразовый вызов с нужным контекстом
Стрелочная обёрткаПростые случаи, колбэки
self = thisСтарый подход, до ES6
Классовое поле со стрелочнойReact-компоненты, классы

Ключевой момент: bind создаёт новую функцию, а call/apply вызывают функцию немедленно. Это важно понимать при выборе подхода.

Вопрос 17. Что такое прототипы в JavaScript?

Таймкод: 00:20:11

Ответ собеседника: Неполный. Прототипы — это родительские объекты, которые хранят функции и данные. JavaScript построен на объектах, и каждый объект имеет свой прототип. Не смог чётко объяснить механизм прототипного наследования.

Правильный ответ:

Прототип — это объект, от которого другой объект наследует свойства и методы. JavaScript использует прототипное наследование вместо классического.

Прототипная цепочка

Каждый объект в JavaScript имеет скрытое свойство [[Prototype]] (доступное через __proto__ или Object.getPrototypeOf()), которое указывает на другой объект — его прототип.

const animal = {
type: 'Животное',
eat() {
console.log(`${this.name} ест.`);
}
};

const cat = {
name: 'Мурка',
meow() {
console.log('Мяу!');
}
};

// Устанавливаем прототип
Object.setPrototypeOf(cat, animal);

cat.meow(); // "Мяу!" — собственный метод
cat.eat(); // "Мурка ест." — унаследованный метод от animal
cat.type; // "Животное" — унаследованное свойство

console.log(cat.hasOwnProperty('meow')); // true
console.log(cat.hasOwnProperty('eat')); // false — унаследовано

Как работает поиск свойств

cat.meow()

1. Ищем meow в самом cat → нашли → вызываем

cat.eat()

1. Ищем eat в cat → не нашли

2. Ищем eat в animal (прототип cat) → нашли → вызываем

cat.toString()

1. Ищем toString в cat → не нашли

2. Ищем toString в animal → не нашли

3. Ищем toString в Object.prototype → нашли → вызываем

Функции-конструкторы и prototype

function Animal(name) {
this.name = name;
}

// Методы добавляем в prototype — все экземпляры будут использовать одну копию
Animal.prototype.eat = function() {
console.log(`${this.name} ест.`);
};

Animal.prototype.sleep = function() {
console.log(`${this.name} спит.`);
};

const cat = new Animal('Мурка');
const dog = new Animal('Шарик');

cat.eat(); // "Мурка ест."
dog.eat(); // "Шарик ест."

// Оба используют одну и ту же функцию из prototype
console.log(cat.eat === dog.eat); // true

Структура при использовании конструктора

cat = {
name: 'Мурка',
__proto__: Animal.prototype = {
eat: function() { ... },
sleep: function() { ... },
__proto__: Object.prototype = {
toString: function() { ... },
hasOwnProperty: function() { ... },
__proto__: null
}
}
}

Прототипное наследование — полный пример

// Родительский конструктор
function Animal(name) {
this.name = name;
}

Animal.prototype.eat = function() {
console.log(`${this.name} ест.`);
};

// Дочерний конструктор
function Cat(name, color) {
Animal.call(this, name); // вызываем родительский конструктор
this.color = color;
}

// Наследование прототипов
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;

// Добавляем методы дочернего класса
Cat.prototype.meow = function() {
console.log(`${this.name} говорит: Мяу!`);
};

const murka = new Cat('Мурка', 'рыжий');

murka.meow(); // "Мурка говорит: Мяу!" — собственный метод
murka.eat(); // "Мурка ест." — унаследованный метод

console.log(murka instanceof Cat); // true
console.log(murka instanceof Animal); // true
console.log(murka instanceof Object); // true

Классы ES6 — синтаксический сахар над прототипами

class Animal {
constructor(name) {
this.name = name;
}

eat() {
console.log(`${this.name} ест.`);
}
}

class Cat extends Animal {
constructor(name, color) {
super(name); // вызов родительского конструктора
this.color = color;
}

meow() {
console.log(`${this.name} говорит: Мяу!`);
}
}

const murka = new Cat('Мурка', 'рыжий');
murka.meow(); // "Мурка говорит: Мяу!"
murka.eat(); // "Мурка ест."

Классы ES6 работают на тех же прототипах, просто предоставляют более привычный синтаксис.

Важные методы

const obj = { a: 1 };

// Получить прототип
Object.getPrototypeOf(obj); // возвращает прототип

// Установить прототип
Object.setPrototypeOf(obj, newProto);

// Проверить, является ли объект прототипом другого
Animal.prototype.isPrototypeOf(murka); // true

// Проверить наличие собственного свойства
murka.hasOwnProperty('name'); // true
murka.hasOwnProperty('eat'); // false

// Проверить наличие свойства (включая прототипы)
'name' in murka; // true
'eat' in murka; // true

Ключевые моменты

  • Каждый объект имеет прототип (кроме Object.prototype, у которого __proto__ = null)
  • Свойства ищутся вверх по цепочке прототипов
  • Методы лучше добавлять в prototype, а не в конструктор — экономия памяти
  • Классы ES6 — это синтаксический сахар над прототипным наследованием
  • instanceof проверяет всю цепочку прототипов

Вопрос 18. Что происходит при вызове функции с оператором new?

Таймкод: 00:21:17

Ответ собеседника: Неполный. Создаётся новый объект и новый конструктор, связанный с прототипом. Не смог детально описать все 4 шага процесса.

Правильный ответ:

При вызове new SomeFunction() происходит ровно 4 шага:

Шаг 1: Создаётся новый пустой объект

const newObj = {};

Шаг 2: Прототип нового объекта связывается с Function.prototype

// Внутри происходит:
Object.setPrototypeOf(newObj, SomeFunction.prototype);

// Теперь newObj.__proto__ === SomeFunction.prototype

Это означает, что newObj получает доступ ко всем методам и свойствам из SomeFunction.prototype.

Шаг 3: Конструктор вызывается с this, привязанным к новому объекту

// Внутри происходит:
const result = SomeFunction.apply(newObj, arguments);

Код конструктора выполняется, и this указывает на только что созданный объект.

Шаг 4: Возвращается объект (если конструктор не вернул объект)

// Если конструктор вернул не-объект (или ничего) — возвращается newObj
// Если конструктор вернул объект — возвращается этот объект
if (result !== null && typeof result === 'object') {
return result;
}
return newObj;

Полная реализация — полифилл для new

function myNew(constructor, ...args) {
// Шаг 1: Создаём новый объект
const obj = {};

// Шаг 2: Связываем прототип
Object.setPrototypeOf(obj, constructor.prototype);

// Шаг 3: Вызываем конструктор с this = obj
const result = constructor.apply(obj, args);

// Шаг 4: Возвращаем объект (или результат, если это объект)
return result instanceof Object ? result : obj;
}

Пример использования

function User(name, age) {
this.name = name;
this.age = age;

this.greet = function() {
console.log(`Привет, ${this.name}!`);
};
}

User.prototype.sayAge = function() {
console.log(`Мне ${this.age} лет.`);
};

// Обычный вызов с new
const user1 = new User('Анна', 25);

// Эквивалент с нашим полифиллом
const user2 = myNew(User, 'Мария', 30);

user1.greet(); // "Привет, Анна!"
user1.sayAge(); // "Мне 25 лет."

user2.greet(); // "Привет, Мария!"
user2.sayAge(); // "Мне 30 лет."

console.log(user1 instanceof User); // true
console.log(user2 instanceof User); // true

Особый случай: конструктор возвращает объект

function SpecialUser(name) {
this.name = name;

// Возвращаем другой объект — new вернёт ЕГО, а не this
return {
type: 'special',
label: name.toUpperCase()
};
}

const user = new SpecialUser('Анна');
console.log(user); // { type: 'special', label: 'АННА' }
console.log(user.name); // undefined — this был проигнорирован
console.log(user instanceof SpecialUser); // false

Если конструктор возвращает примитив — он игнорируется

function NumberUser(name) {
this.name = name;
return 42; // примитив — игнорируется
}

const user = new NumberUser('Анна');
console.log(user); // { name: 'Анна' }
console.log(user.name); // "Анна" — this работает

Визуализация процесса

function Animal(name, sound) {
// this = {} (новый объект)
// this.__proto__ = Animal.prototype

this.name = name;
this.sound = sound;

// return this (неявно)
}

Animal.prototype.speak = function() {
console.log(`${this.name} говорит ${this.sound}`);
};

const cat = new Animal('Мурка', 'Мяу');

// cat = {
// name: 'Мурка',
// sound: 'Мяу',
// __proto__: Animal.prototype = {
// speak: function() { ... },
// __proto__: Object.prototype
// }
// }

Ключевые моменты

  • new создаёт новый объект и связывает его с прототипом функции
  • this внутри конструктора указывает на новый объект
  • Если конструктор вернул объект — он заменит this
  • Если конструктор вернул примитив — он будет проигнорирован
  • instanceof проверяет, есть ли Constructor.prototype в цепочке прототипов объекта

Вопрос 19. Что такое CORS (Cross-Origin Resource Sharing)?

Таймкод: 00:21:52

Ответ собеседника: Неправильный. Не слышал о CORS и не смог объяснить, что это механизм безопасности браузера для контроля кросс-доменных запросов.

Правильный ответ:

CORS (Cross-Origin Resource Sharing) — это механизм безопасности браузера, который контролирует доступ веб-страниц к ресурсам с другого источника (origin). Это критически важная тема для любого разработчика, работающего с API.

Что такое Origin (источник)

Origin — это комбинация протокола, домена и порта:

https://example.com:443
|------|---------|----|
протокол домен порт

Два URL считаются разными origin, если отличается хотя бы один компонент:

https://example.com ← текущая страница
https://api.example.com ← другой домен — блокируется
http://example.com ← другой протокол — блокируется
https://example.com:8080 ← другой порт — блокируется
https://other.com ← другой домен — блокируется

Same-Origin Policy (SOP)

По умолчанию браузер блокирует кросс-доменные запросы:

// Страница на https://frontend.com
fetch('https://api.backend.com/data')
.then(response => response.json())
.then(data => console.log(data));

// Ошибка: Access to fetch at 'https://api.backend.com/data'
// from origin 'https://frontend.com' has been blocked by CORS policy

Как работает CORS

Браузер отправляет специальные заголовки, и сервер решает, разрешить ли запрос.

Простой запрос (Simple Request)

Условия: метод GET, POST или HEAD, только стандартные заголовки, тело — text/plain, application/x-www-form-urlencoded или multipart/form-data.

// Браузер отправляет:
// Origin: https://frontend.com
fetch('https://api.backend.com/data', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});

// Сервер должен ответить с заголовком:
// Access-Control-Allow-Origin: https://frontend.com

Предварительный запрос (Preflight Request)

Для «непростых» запросов браузер сначала отправляет OPTIONS-запрос:

// Браузер сначала отправляет:
// OPTIONS /data HTTP/1.1
// Origin: https://frontend.com
// Access-Control-Request-Method: PUT
// Access-Control-Request-Headers: Content-Type, Authorization

fetch('https://api.backend.com/data', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
},
body: JSON.stringify({ name: 'Анна' })
});

// Сервер отвечает на OPTIONS:
// Access-Control-Allow-Origin: https://frontend.com
// Access-Control-Allow-Methods: GET, POST, PUT, DELETE
// Access-Control-Allow-Headers: Content-Type, Authorization
// Access-Control-Max-Age: 86400 (кешировать ответ на 24 часа)

// Только после успешного ответа на OPTIONS браузер отправит основной PUT-запрос

Заголовки CORS

Запросные заголовки (отправляет браузер):

ЗаголовокНазначение
OriginИсточник запроса
Access-Control-Request-MethodМетод основного запроса (preflight)
Access-Control-Request-HeadersЗаголовки основного запроса (preflight)

Ответные заголовки (отправляет сервер):

ЗаголовокНазначение
Access-Control-Allow-OriginРазрешённые origin (* или конкретный)
Access-Control-Allow-MethodsРазрешённые методы
Access-Control-Allow-HeadersРазрешённые заголовки
Access-Control-Allow-CredentialsРазрешены ли cookies и авторизация
Access-Control-Max-AgeВремя кеширования preflight-ответа
Access-Control-Expose-HeadersЗаголовки, доступные клиенту

Настройка CORS на сервере

// Go с использованием gorilla/mux
import "github.com/rs/cors"

func main() {
router := mux.NewRouter()
router.HandleFunc("/api/data", handleData).Methods("GET", "POST", "PUT")

// Настройка CORS
c := cors.New(cors.Options{
AllowedOrigins: []string{"https://frontend.com", "https://admin.frontend.com"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Content-Type", "Authorization", "X-Requested-With"},
AllowCredentials: true,
MaxAge: 86400,
})

handler := c.Handler(router)
http.ListenAndServe(":8080", handler)
}
// Или вручную через middleware
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")

// Проверяем, разрешён ли origin
if isAllowedOrigin(origin) {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Max-Age", "86400")
}

// Preflight-запрос
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}

next.ServeHTTP(w, r)
})
}
// Node.js с Express
const cors = require('cors');

app.use(cors({
origin: ['https://frontend.com', 'https://admin.frontend.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400
}));

Credentials (куки и авторизация)

// Клиент — включаем отправку cookies
fetch('https://api.backend.com/data', {
credentials: 'include' // отправлять cookies
});

// Сервер — разрешаем credentials
// Access-Control-Allow-Credentials: true
// Access-Control-Allow-Origin: https://frontend.com (НЕЛЬЗЯ использовать *)

При credentials: true нельзя использовать Access-Control-Allow-Origin: * — нужно указать конкретный origin.

Распространённые ошибки и решения

Ошибка: No 'Access-Control-Allow-Origin' header
→ Сервер не настроил CORS-заголовки

Ошибка: The value of 'Access-Control-Allow-Origin' header must not be '*'
→ Используете credentials + wildcard, нужно указать конкретный origin

Ошибка: Request header field X-Custom is not allowed
→ Сервер не включил X-Custom в Access-Control-Allow-Headers

Ошибка: Method PUT is not allowed
→ Сервер не включил PUT в Access-Control-Allow-Methods

Для Go-разработчика: CORS — это одна из первых вещей, которую нужно настраивать при создании API. Библиотека rs/cors — стандарт де-факто в экосистеме Go. Важно не забывать про preflight-запросы (OPTIONS) и корректно обрабатывать их в роутере.

Вопрос 20. Что такое функция debounce и где она используется?

Таймкод: 00:22:08

Ответ собеседника: Неполный. Помнит, что debounce ограничивает количество вызовов функции, чтобы приложение не лагало. Знает, что есть throttle, но не смог чётко объяснить разницу между ними.

Правильный ответ:

Debounce — это техника, которая откладывает выполнение функции до тех пор, пока не пройдёт определённое время без новых вызовов. Если за это время поступает новый вызов — таймер сбрасывается.

Реализация debounce

function debounce(func, delay) {
let timeoutId;

return function(...args) {
// Сбрасываем предыдущий таймер
clearTimeout(timeoutId);

// Устанавливаем новый таймер
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}

// Вариант с немедленным первым вызовом (leading edge)
function debounceLeading(func, delay) {
let timeoutId;

return function(...args) {
const isFirstCall = !timeoutId;

clearTimeout(timeoutId);

timeoutId = setTimeout(() => {
timeoutId = null;
}, delay);

if (isFirstCall) {
func.apply(this, args);
}
};
}

Throttle — это техника, которая ограничивает частоту вызова функции: функция выполняется не чаще одного раза за указанный период.

function throttle(func, delay) {
let lastCall = 0;
let timeoutId;

return function(...args) {
const now = Date.now();
const timeSinceLastCall = now - lastCall;

if (timeSinceLastCall >= delay) {
lastCall = now;
func.apply(this, args);
} else {
// Запланировать вызов в конце периода
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
lastCall = Date.now();
func.apply(this, args);
}, delay - timeSinceLastCall);
}
};
}

Визуальное сравнение

События: * * * * * * * * *
| | | | | | | | |
0 1 2 3 4 5 6 7 8 сек

Debounce (500мс):
Результат: * * *
(ждём паузу 500мс) 3.5 6.5 8.5

Throttle (500мс):
Результат: * * * *
(каждые 500мс) 0 0.5 1.0 1.5

Практические примеры

1. Поиск с автодополнением (debounce)

const searchInput = document.getElementById('search');

function searchAPI(query) {
fetch(`/api/search?q=${query}`)
.then(response => response.json())
.then(results => displayResults(results));
}

// Ждём 300мс после окончания ввода
const debouncedSearch = debounce(searchAPI, 300);

searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});

2. Бесконечная прокрутка (throttle)

function checkScrollPosition() {
const scrollBottom = document.documentElement.scrollHeight
- window.innerHeight
- window.scrollY;

if (scrollBottom < 200) {
loadMoreContent();
}
}

// Проверяем позицию не чаще раза в 200мс
const throttledCheck = throttle(checkScrollPosition, 200);
window.addEventListener('scroll', throttledCheck);

3. Изменение размера окна (debounce)

function recalculateLayout() {
console.log('Пересчёт макета...');
}

// Ждём окончания изменения размера
const debouncedResize = debounce(recalculateLayout, 250);
window.addEventListener('resize', debouncedResize);

4. Клики по кнопке (throttle)

function handleSubmit() {
console.log('Отправка формы...');
}

// Не чаще раза в 1 секунду
const throttledSubmit = throttle(handleSubmit, 1000);
submitButton.addEventListener('click', throttledSubmit);

5. Движение мыши (throttle)

function updateTooltip(event) {
tooltip.style.left = event.clientX + 'px';
tooltip.style.top = event.clientY + 'px';
}

// Обновляем позицию не чаще 60 раз в секунду
const throttledMove = throttle(updateTooltip, 16);
document.addEventListener('mousemove', throttledMove);

Когда что использовать

СценарийТехникаПочему
Поиск по вводуDebounceЖдём окончания набора
Кнопка «Отправить»ThrottleПредотвращаем повторные клики
Resize окнаDebounceЖдём окончания изменения
ScrollThrottleРегулярные проверки
АвтосохранениеDebounceЖдём паузы в редактировании
Игровой вводThrottleФиксированная частота обновлений
Валидация формыDebounceЖдём окончания ввода
Аналитика (клики)ThrottleНе перегружать сервер

Использование с библиотеками

// Lodash
import { debounce, throttle } from 'lodash';

const debouncedFn = debounce(myFunction, 300);
const throttledFn = throttle(myFunction, 300);

// RxJS
import { fromEvent } from 'rxjs';
import { debounceTime, throttleTime, distinctUntilChanged } from 'rxjs/operators';

fromEvent(searchInput, 'input')
.pipe(
debounceTime(300),
distinctUntilChanged()
)
.subscribe(e => searchAPI(e.target.value));

Ключевое отличие

  • Debounce: «Подожди, пока не закончатся события, и выполни один раз»
  • Throttle: «Выполняй регулярно, но не чаще указанного интервала»

Вопрос 21. Напишите функцию debounce с паузой 3 секунды.

Таймкод: 00:22:56

Ответ собеседника: Неполный. Предложил идею использования setTimeout с его очисткой при каждом новом вызове. Правильно понял логику, но не смог написать рабочий код. Также не знает, что setTimeout возвращает числовой идентификатор, а не промис.

Правильный ответ:

Базовая реализация

function debounce(func, delay = 3000) {
let timeoutId;

return function(...args) {
// Сбрасываем предыдущий таймер, если он есть
clearTimeout(timeoutId);

// Устанавливаем новый таймер
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}

Как это работает

// Создаём версию функции с debounce
const debouncedLog = debounce((message) => {
console.log('Выполнено:', message);
}, 3000);

// Симуляция быстрых вызовов
debouncedLog('первый вызов'); // таймер установлен на 3 сек
debouncedLog('второй вызов'); // предыдущий таймер сброшен, новый на 3 сек
debouncedLog('третий вызов'); // предыдущий таймер сброшен, новый на 3 сек

// Через 3 секунды после последнего вызова:
// Выполнено: третий вызов

Расширенная версия с дополнительными возможностями

function debounce(func, delay = 3000, options = {}) {
let timeoutId;
let lastArgs;
let lastThis;
const { leading = false, trailing = true, maxWait } = options;
let lastCallTime = 0;
let lastInvokeTime = 0;

function invokeFunc(time) {
const args = lastArgs;
const thisArg = lastThis;
lastArgs = lastThis = undefined;
lastInvokeTime = time;
func.apply(thisArg, args);
}

function startTimer(pendingFunc, wait) {
timeoutId = setTimeout(pendingFunc, wait);
}

function remainingWait(time) {
const timeSinceLastCall = time - lastCallTime;
const timeSinceLastInvoke = time - lastInvokeTime;
const timeWaiting = delay - timeSinceLastCall;

return maxWait !== undefined
? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting;
}

function shouldInvoke(time) {
const timeSinceLastCall = time - lastCallTime;
const timeSinceLastInvoke = time - lastInvokeTime;

return (
lastCallTime === 0 ||
timeSinceLastCall >= delay ||
timeSinceLastInvoke >= (maxWait || Infinity)
);
}

function trailingEdge(time) {
timeoutId = undefined;
if (trailing && lastArgs) {
invokeFunc(time);
} else {
lastArgs = lastThis = undefined;
}
}

function timerExpired() {
const time = Date.now();
if (shouldInvoke(time)) {
return trailingEdge(time);
}
startTimer(timerExpired, remainingWait(time));
}

function debounced(...args) {
const time = Date.now();
const isInvoking = shouldInvoke(time);

lastArgs = args;
lastThis = this;
lastCallTime = time;

if (isInvoking) {
if (timeoutId === undefined) {
// Leading edge: вызываем сразу
lastInvokeTime = time;
startTimer(timerExpired, delay);
return leading ? invokeFunc(time) : undefined;
}
if (maxWait !== undefined) {
startTimer(timerExpired, delay);
return invokeFunc(time);
}
}

if (timeoutId === undefined) {
startTimer(timerExpired, delay);
}

return undefined;
}

// Метод для отмены
debounced.cancel = function() {
if (timeoutId !== undefined) {
clearTimeout(timeoutId);
}
lastInvokeTime = 0;
lastCallTime = 0;
lastArgs = lastThis = undefined;
timeoutId = undefined;
};

// Метод для немедленного вызова
debounced.flush = function() {
if (timeoutId === undefined) {
return;
}
trailingEdge(Date.now());
};

return debounced;
}

Использование расширенной версии

// Базовое использование
const debouncedSearch = debounce((query) => {
fetch(`/api/search?q=${query}`);
}, 3000);

searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});

// С leading edge (вызов на первом событии)
const debouncedClick = debounce(handleClick, 3000, { leading: true, trailing: false });

// С maxWait (максимальное время ожидания)
const debouncedSave = debounce(saveData, 3000, { maxWait: 10000 });

// Отложенное выполнение с отменой
const debouncedUpdate = debounce(updateUI, 3000);

// Пользователь начал вводить — таймер запустился
debouncedUpdate(data);

// Пользователь ушёл со страницы — отменяем
debouncedUpdate.cancel();

// Нужно выполнить немедленно
debouncedUpdate.flush();

Важные детали

  • setTimeout возвращает числовой идентификатор (в браузере) или объект таймера (в Node.js), а не промис
  • clearTimeout принимает этот идентификатор и отменяет запланированный вызов
  • func.apply(this, args) нужен для сохранения контекста и передачи аргументов
  • ...args (rest-параметр) собирает все аргументы в массив

Пример с сохранением контекста

const user = {
name: 'Анна',

updateName(newName) {
this.name = newName;
console.log('Имя обновлено:', this.name);
}
};

// Привязываем debounce к методу объекта
const debouncedUpdate = debounce(function(newName) {
this.updateName(newName);
}.bind(user), 3000);

// Или используем apply внутри debounce
const debouncedUpdate2 = debounce((newName) => {
user.updateName(newName);
}, 3000);

Ключевой момент: debounce — это замыкание. Переменная timeoutId сохраняется между вызовами возвращаемой функции, что позволяет отслеживать и сбрасывать таймер.

Таймкод: 00:38:24

Ответ собеседника: Неполный. После подсказки вспомнил принцип деления на два и рекурсивный подход, но не смог написать рабочий код.

Правильный ответ:

Бинарный поиск — алгоритм поиска элемента в отсортированном массиве с временной сложностью O(log n). На каждом шаге массив делится пополам, и поиск продолжается в соответствующей половине.

Принцип работы

Массив: [2, 5, 8, 12, 16, 23, 38, 56, 72, 91]
Ищем: 23

Шаг 1: left=0, right=9, mid=4 → arr[4]=16 < 23 → ищем в правой половине
Шаг 2: left=5, right=9, mid=7 → arr[7]=56 > 23 → ищем в левой половине
Шаг 3: left=5, right=6, mid=5 → arr[5]=23 == 23 → нашли! Индекс: 5

Итеративная реализация (рекомендуется)

function binarySearch(arr, target) {
let left = 0;
let right = arr.length - 1;

while (left <= right) {
const mid = Math.floor((left + right) / 2);

if (arr[mid] === target) {
return mid; // Нашли элемент
} else if (arr[mid] < target) {
left = mid + 1; // Ищем в правой половине
} else {
right = mid - 1; // Ищем в левой половине
}
}

return -1; // Элемент не найден
}

// Использование
const sortedArray = [2, 5, 8, 12, 16, 23, 38, 56, 72, 91];

console.log(binarySearch(sortedArray, 23)); // 5
console.log(binarySearch(sortedArray, 2)); // 0
console.log(binarySearch(sortedArray, 91)); // 9
console.log(binarySearch(sortedArray, 50)); // -1

Рекурсивная реализация

function binarySearchRecursive(arr, target, left = 0, right = arr.length - 1) {
// Базовый случай: элемент не найден
if (left > right) {
return -1;
}

const mid = Math.floor((left + right) / 2);

if (arr[mid] === target) {
return mid;
} else if (arr[mid] < target) {
return binarySearchRecursive(arr, target, mid + 1, right);
} else {
return binarySearchRecursive(arr, target, left, mid - 1);
}
}

Оптимизация: предотвращение переполнения

// При больших массивах (left + right) может переполнить Number
// Безопасный способ вычисления mid:
const mid = left + Math.floor((right - left) / 2);

// Или с использованием побитового сдвига:
const mid = (left + right) >>> 1;

Поиск с возвратом позиции для вставки

function binarySearchLowerBound(arr, target) {
let left = 0;
let right = arr.length; // 注意: не length - 1

while (left < right) {
const mid = Math.floor((left + right) / 2);

if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid;
}
}

return left; // Позиция, куда нужно вставить target
}

// Примеры
console.log(binarySearchLowerBound([1, 3, 5, 7], 4)); // 2 (между 3 и 5)
console.log(binarySearchLowerBound([1, 3, 5, 7], 5)); // 2 (найден)
console.log(binarySearchLowerBound([1, 3, 5, 7], 0)); // 0 (в начало)
console.log(binarySearchLowerBound([1, 3, 5, 7], 10)); // 4 (в конец)

Поиск первого и последнего вхождения (для дубликатов)

function findFirstOccurrence(arr, target) {
let left = 0;
let right = arr.length - 1;
let result = -1;

while (left <= right) {
const mid = Math.floor((left + right) / 2);

if (arr[mid] === target) {
result = mid; // Запоминаем позицию
right = mid - 1; // Продолжаем искать левее
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}

return result;
}

function findLastOccurrence(arr, target) {
let left = 0;
let right = arr.length - 1;
let result = -1;

while (left <= right) {
const mid = Math.floor((left + right) / 2);

if (arr[mid] === target) {
result = mid; // Запоминаем позицию
left = mid + 1; // Продолжаем искать правее
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}

return result;
}

// Пример с дубликатами
const arr = [1, 2, 4, 4, 4, 5, 6, 7];
console.log(findFirstOccurrence(arr, 4)); // 2
console.log(findLastOccurrence(arr, 4)); // 4

Бинарный поиск на ответе

// Задача: найти минимальное количество дней для выполнения работы
function minDays(work, workers) {
let left = 1;
let right = work; // Максимум — один работник всё делает

function canComplete(days) {
return workers * days >= work;
}

while (left < right) {
const mid = Math.floor((left + right) / 2);

if (canComplete(mid)) {
right = mid; // Можно попробовать меньше дней
} else {
left = mid + 1; // Нужно больше дней
}
}

return left;
}

console.log(minDays(10, 3)); // 4 (3 работника * 4 дня = 12 >= 10)

Сравнение с линейным поиском

ХарактеристикаЛинейный поискБинарный поиск
СложностьO(n)O(log n)
ТребованияЛюбой массивОтсортированный массив
n = 1000~1000 операций~10 операций
n = 1000000~1000000 операций~20 операций

Ключевые моменты

  • Массив должен быть отсортирован — иначе алгоритм не работает
  • Итеративная версия предпочтительнее рекурсивной — нет накладных расходов на стек вызовов
  • left + Math.floor((right - left) / 2) безопаснее, чем Math.floor((left + right) / 2) — предотвращает переполнение
  • Бинарный поиск широко используется в стандартных библиотеках: Arrays.binarySearch() в Java, bisect в Python

Вопрос 23. Напишите функцию, возвращающую элементы массива, делящиеся на 3 без остатка.

Таймкод: 00:46:27

Ответ собеседника: Неполный. Допустил синтаксические ошибки, после подсказки исправил, но код не заработал корректно. Задача не была доведена до рабочего результата.

Правильный ответ:

Решение с использованием filter

function getDivisibleByThree(arr) {
return arr.filter(num => num % 3 === 0);
}

// Использование
console.log(getDivisibleByThree([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]));
// [3, 6, 9]

console.log(getDivisibleByThree([12, 15, 7, 21, 8, 30]));
// [12, 15, 21, 30]

Решение с использованием цикла

function getDivisibleByThree(arr) {
const result = [];

for (let i = 0; i < arr.length; i++) {
if (arr[i] % 3 === 0) {
result.push(arr[i]);
}
}

return result;
}

Решение с использованием for...of

function getDivisibleByThree(arr) {
const result = [];

for (const num of arr) {
if (num % 3 === 0) {
result.push(num);
}
}

return result;
}

Решение с использованием reduce

function getDivisibleByThree(arr) {
return arr.reduce((acc, num) => {
if (num % 3 === 0) {
acc.push(num);
}
return acc;
}, []);
}

Для чётных чисел (как упомянуто в задаче)

function getEvenNumbers(arr) {
return arr.filter(num => num % 2 === 0);
}

console.log(getEvenNumbers([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]));
// [2, 4, 6, 8, 10]

Расширенная версия — универсальная функция

function filterByDivisor(arr, divisor) {
if (divisor === 0) {
throw new Error('Divisor cannot be zero');
}
return arr.filter(num => num % divisor === 0);
}

// Использование
console.log(filterByDivisor([1, 2, 3, 4, 5, 6], 3)); // [3, 6]
console.log(filterByDivisor([1, 2, 3, 4, 5, 6], 2)); // [2, 4, 6]
console.log(filterByDivisor([10, 15, 20, 25, 30], 5)); // [10, 15, 20, 25, 30]

Обработка крайних случаев

function getDivisibleByThree(arr) {
// Проверка на массив
if (!Array.isArray(arr)) {
return [];
}

return arr.filter(num => {
// Проверяем, что элемент — число и не NaN
if (typeof num !== 'number' || Number.isNaN(num)) {
return false;
}
return num % 3 === 0;
});
}

// Тесты с крайними случаями
console.log(getDivisibleByThree([3, 6, '9', null, undefined, NaN, 12]));
// [3, 6, 12] — только валидные числа

console.log(getDivisibleByThree([])); // []
console.log(getDivisibleByThree('not array')); // []

Ключевые моменты

  • % — оператор остатка от деления (modulo)
  • num % 3 === 0 — проверка делимости на 3 без остатка
  • filter — самый идиоматичный способ в JavaScript для фильтрации массивов
  • Не забывайте обрабатывать крайние случаи: пустой массив, нечисловые значения, NaN