Фронтенд собеседование 2025 | Junior Frontend | Реальные вопросы и задачи
Сегодня мы разберём реальное собеседование на позицию фронтенд-разработчика, в ходе которого кандидат Роман с полутора годами опыта продемонстрировал уверенные практические навыки в создании проектов, но заметно уступил в глубине теоретической базы по 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.
Правильный ответ:
Определение
HTML — HyperText 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);
}
Приоритет стилей (специфичность)
От низкого к высокому:
- Стили браузера (user agent stylesheet)
- Внешние стили (
<link>,@import) - Внутренние стили (
<style>) - Inline-стили (
style="") !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 { }
Правила разрешения конфликтов
- Победитель — селектор с большей специфичностью
- При равной специфичности побеждает последний в коде (каскад)
!importantпереопределяет всё (кроме другого!importantс более высокой специфичностью)- Inline-стили переопределяют всё, кроме
!important - Наследуемые стили имеют наименьший приоритет
Практические рекомендации
- Избегать
!important— делает код неподдерживаемым - Избегать чрезмерной вложенности — усложняет переопределение стилей
- Использовать классы как основной способ стилизации
- Использовать
:where()для группировки без повышения специфичности - Методология BEM помогает держать специфичность на одном уровне
Вопрос 9. Что такое CSS и в чём заключается каскадность?
Таймкод: 00:11:53
Ответ собеседника: Неполный. CSS используется для стилизации DOM, включая flexbox. О каскадности сказал только что стили применяются сверху вниз, не раскрыл тему наследования и приоритетов.
Правильный ответ:
CSS — Cascading 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 сохраняется между вызовами
Как это работает
- При вызове
createCounter()создаётся контекст выполнения с переменнойcount - Возвращаемая функция
incrementсохраняет ссылку на этот контекст - Даже после завершения
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:
- Создаётся новое лексическое окружение
countзаписывается в EnvironmentRecord- 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(); // "Анна"
}
Приоритет правил (от высшего к низшему)
new— новый объектcall/apply/bind— явно указанный объект- Вызов как метод — объект слева от точки
- Обычный вызов —
undefined(strict) илиwindow(non-strict) - Стрелочные функции —
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 | Ждём окончания изменения |
| Scroll | Throttle | Регулярные проверки |
| Автосохранение | 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 сохраняется между вызовами возвращаемой функции, что позволяет отслеживать и сбрасывать таймер.
Вопрос 22. Напишите функцию бинарного поиска (Binary Search).
Таймкод: 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
