Срез 1
Изучение теории. Неделя 1
1. Common principles
1.1. Именование переменных в React
- UpperCamelCase для компонент, классов и интерфейсов — в React компоненты обязаны начинаться с заглавной буквы, чтобы JSX отличал их от обычных HTML-тегов
- kebab-case для папок
- lowerCamelCase (myVar) для переменных и функций
- SCREAMING_SNAKE_CASE для констант и глобальных переменных
- Префикс “use” and lowerCamelCase для кастомных хуков
1.2. Принципы переиспользования кода в React
В React переиспользование кода достигается в основном за счет:
- Создания переиспользуемых компонентов путем передачи в них пропсов с разными данными. Также есть пропс
children, с помощью которого можно передавать одни компоненты внутрь других - Использования составных компонентов — этот паттерн позволяет создавать группу компонентов, которые работают вместе, разделяя общее состояние
- Использования кастомных хуков для переиспользования логики
- Рендер-пропсов — это техника, при которой компонент принимает функцию в качестве пропса и использует её для рендеринга части интерфейса
- Компонентов высшего порядка (HOC) — это функции, которые принимают компонент и возвращают его же, но с дополнительными данными или поведением
1.3. Принципы DRY, KISS и YAGNI в контексте разработки на React
- DRY (Don’t Repeat Yourself) — Не дублируй свой код
- Создание переиспользуемых компонентов
- Вынос общей логики в кастомные хуки
KISS (Keep It Simple, Stupid) — Сохраняй код простым и тупым. Нужно писать код, который решает задачу, используя минимальное количество абстракций, при этом вложенность этих абстракций друг в друга также минимальна
- YAGNI (You Ain’t Gonna Need It) — Тебе это не понадобится. Код должен уметь делать только то, для чего он написан. Мы не создаем никакой функционал, который может понадобиться потом. Делаем только то, что нужно конкретно для реализации поставленной задачи
1.4. Принципы написания чистого кода по Мартину в контексте React
Чистый код по Роберту Мартину — это концепция написания кода, который легко читать, понимать и поддерживать. Согласно этой концепции имена переменных, функций и классов должны быть семантически понятными и отражать их назначение. Важно избегать избыточности кода и его дублирования, использовать комментарии только для пояснения сложной логики, сам код должен быть самодокументирующимся, всегда настраивать обработку ошибок.
Несколько конкретных примеров в контексте React:
- Избегайте прямых манипуляций с DOM
- Используйте тернарные операторы и логическое «И» для условного рендеринга
- Компоненты и вспомогательные утилиты должны стремиться к чистоте, т.е. отсутствию побочных эффектов
- Используйте кастомные хуки для выноса сложной бизнес-логики из UI-компонентов
- Компоненты должны быть маленькими: если часть JSX можно логически выделить, выносите её
1.5. Процесс и значение код-ревью в проектах на React
Code Review — это процесс проверки и анализа исходного кода для выявления ошибок, улучшения качества и обучения команды.
Типичный цикл ревью в команде выглядит следующим образом:
- Написание кода — автор завершает работу над задачей
- Отправка на проверку — автор создаёт Pull Request или Merge Request в системе контроля версий, добавляет описание изменений и указывает ревьюверов
- Анализ — ревьюверы изучают код, оставляют комментарии, задают вопросы и предлагают улучшения
- Исправление замечаний — автор вносит правки на основе комментариев
- Утверждение и слияние — после устранения всех правок ревьюверы одобряют Merge Request, код сливается с основной веткой проекта
1.6. Принципы SOLID
SOLID — это акроним, обозначающий пять принципов объектно-ориентированного программирования, разработанных для создания более понятного, гибкого, и поддерживаемого программного кода.
- Принцип единственной ответственности (Single Responsibility Principle) — модуль (в контексте React - компонент, хук или функция) должен иметь одну причину для изменения
- Принцип открытости/закрытости (Open/Closed Principle) — программные сущности должны быть открыты для расширения, но закрыты для изменения. Это значит, что можно добавлять новую функциональность, не изменяя существующий код
- Принцип подстановки Барбары Лисков (Liskov Substitution Principle) — объекты класса наследника должны быть взаимозаменяемы с объектами родительского класса без изменения ожидаемого поведения программы. Проще говоря, если есть базовый класс, то любой его наследник должен соответствовать ожиданиям, заданным им, и не нарушать его контракт
- Принцип разделения интерфейсов (Interface Segregation Principle) — программные сущности не должны зависеть от методов, которые они не используют. Лучше разделить интерфейсы на более мелкие, специфичные
- Принцип инверсии зависимостей (Dependency Inversion Principle) — модули высокого уровня не должны зависеть от модулей низкого уровня. И те, и другие должны зависеть от абстракций.
1.7. Значение и правильное использование комментариев в коде React
Комментарии в коде React служат для пояснения логики, облегчения понимания кода и документации. Неправильно использовать комментарии для описания очевидной работы кода. Правильно использовать комментарии при объяснении сложных логических участков, назначении переменных и функций, временном отключение фрагментов кода или документировании API.
1.8. Антипаттерны в разработке на React и способы их избежания
- Прямое обращение к DOM через
refдля получения данных из инпутов, минуя стейт. Это ломает реактивность и усложняет логику.- Как избежать:
useStateдля управления состоянием инпутов. При каждом изменении инпута обновлять состояние
- Как избежать:
- Один огромный компонент, который управляет слишком большим количеством состояний и логики.
- Как избежать: следуйте принципу единственной ответственности. Выносите логику в кастомные хуки, а верстку делите на более мелкие компоненты
- Использование индексов массива как
keyпри его рендеринге. React может некорректно реагировать на изменения элементов.- Как избежать: всегда используйте уникальные ID из ваших данных
- Props Drilling — передача пропсов через несколько уровней компонентов, которым эти данные не нужны, только чтобы доставить их “внуку”:
- Как избежать: для глобальных данных (тема, пользователь) используйте Context или стейт-менеджеры
- Избыточное состояние (Derived State) — хранение в useState данных, которые можно вычислить на основе других пропсов или стейта.
- Как избежать: вычисляйте значения прямо во время рендера. Если вычисления тяжелые, используйте
useMemo
- Как избежать: вычисляйте значения прямо во время рендера. Если вычисления тяжелые, используйте
- Прямая мутация стейта — попытка изменить объект или массив в стейте напрямую, без создания копии.
- Как избежать: всегда используйте деструктуризацию для создания копий:
setState([...oldArray, newItem]).
- Как избежать: всегда используйте деструктуризацию для создания копий:
- Выполнение запросов, подписок или прямое манипулирование DOM вне хуков useEffect.
- Как избежать: используйте
useEffectдля загрузки данных, подписок и других операций. Правильно настройте массив зависимостей, чтобы эффект срабатывал только тогда, когда это необходимо, а не при каждом ререндере
- Как избежать: используйте
2. Programming paradigms (imperative and declarative)
2.1. Определение и виды программных парадигм
Парадигма программирования — совокупность идей и понятий, определяющих стиль написания компьютерных программ, структурирование их работы и организацию вычислений. Основные виды включают императивную, декларативную, объектно-ориентированную и функциональную парадигмы.
2.2. Примеры императивных и декларативных языков программирования
Императивные ЯП:
- Fortran
- Pascal
- C/C++
- Java
- Python
- Go
Декларативные ЯП:
- SQL
- HTML/CSS
- XML
- LaTeX
Большинство современных языков (JavaScript, Swift, Kotlin, Rust) — мультипарадигменные. На них можно писать как в императивном стиле (через циклы и условия), так и в декларативном (используя методы массивов map, filter, reduce).
2.3. Основные различия между императивным и декларативным подходами
- Императивный подход — сосредоточен на точном описании алгоритма (как сделать).
- Есть четкая последовательность команд
- Есть состояние программы, циклы, условия и операция присваивания
- Оптимизация кода для повышения производительности — это задача программиста
- Декларативный подход — сосредоточен на результате (что получить).
- Программисту не нужно думать о низкоуровневых деталях (например, об управлении памятью или оптимизации)
- Состояние программы управляется автоматически
2.4. Преимущества императивного и декларативного подходов
Императивный подход дает разработчику полный контроль над выполнением программы, что особенно ценно для задач, требующих оптимизации производительности или работы с низкоуровневыми операциями.
Декларативный подход предлагает более высокий уровень абстракции, делая код компактным и выразительным. Он особенно эффективен при работе со сложными преобразованиями данных, где важен результат, а не процесс его получения.
2.5. Управление состоянием в императивном и декларативном программировании
Сравнение управления состоянием:
- Императивное: состояние явно модифицируется и отслеживается, разработчик вручную создаёт и изменяет переменные, отслеживая их текущие значения на каждом шаге выполнения программы
- Декларативное: состояние управляется автоматически, фокус смещается на описание целевого состояния. В библиотеках типа React вы описываете, как должен выглядеть UI, а React сам обновляет DOM, управляя состоянием за вас
2.6. Примеры решения задач с использованием обеих парадигм
Рассмотрим задачу сортировки массива чисел. В случае декларативного подхода мы бы вызвали метод sort:
1
2
let arr = [3, 1, 2];
arr.sort((a, b) => a - b);
При этом нас как разработчика не волнует, как именно реализован алгоритм сортировки и какая у него сложность.
В случае императивного подхода нам бы пришлось реализовать алгоритм сортировки самостоятельно (ниже реализован bubble sort):
1
2
3
4
5
6
7
8
9
let arr = [3, 1, 2];
for (let i = 0; i < arr.length; i++)
for (var j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
let temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
2.7. Основные концепции декларативного программирования и их применение в сложных задачах
Декларативное программирование — это парадигма программирования, где разработчик описывает, что нужно сделать, а не как это сделать.
Основные концепции включают в себя описание желаемого результата, а не процесса, отсутствие явного управления состоянием, использование встроенных функций или абстракций для достижения результата и возможность параллельной обработки.
3. Functional programming (functors, key features)
3.1. Основы функционального программирования
Функциональное программирование — это парадигма программирования, в которой программы строятся путем композиции чистых функций, избегая изменения состояния и мутации данных.
Основные концепции:
- Чистые функции (Pure Functions)
- Детерминированность — одинаковые входные данные дают одинаковый результат
- Не имеют побочных эффектов — они ничего не меняют вне себя
- Иммутабельность (Immutability)
- Данные не изменяются, а создаются новые на основе старых
- Функции — объекты первого класса (First-Class Functions)
- С функциями можно работать как с любыми другими значениями: передавать их аргументами, возвращать из других функций, присваивать переменным
- Функции высшего порядка (Higher-Order Functions, HOF)
- Это функции, которые либо принимают другие функции в качестве аргументов, либо возвращают функции как результат. Примеры:
map,filter,reduceв JavaScript
- Это функции, которые либо принимают другие функции в качестве аргументов, либо возвращают функции как результат. Примеры:
3.2. Особенности и преимущества функционального программирования
Преимущества:
- Предсказуемость и надёжность: чистые функции легче тестировать и отлаживать
- Параллелизм: отсутствие изменяемого состояния позволяет безопасно запускать операции в разных потоках, избегая конфликтов
- Модульность: код легко разбивается на мелкие, переиспользуемые функции, которые можно комбинировать
Особенности:
- Сложность управления побочными эффектами: работа с I/O, исключениями, случайными числами в чисто функциональной среде требует специальных подходов (монады, функторы)
- Проблемы с производительностью из-за неизменяемости данных
- Функции — объекты первого класса
- Функции высшего порядка
3.3. Примеры простых функциональных программ
1
2
3
// сумма элементов массива
let arr = [1, 2, 3];
const sum = arr.reduce((acc, x) => acc + x, 0);
1
2
3
// сортировка массива
let arr = [1, 3, 2];
arr.sort((a, b) => a - b);
3.4. Понятие функторов и монад в функциональном программировании
Функтор — это объект, реализующий метод map и соответствующий законам:
Закон тождества — если к значению внутри функтора применить тождественную функцию, результат должен быть таким же, как исходный функтор.
1
numbers.map((x) => x); // [1, 2, 3] - тот же массив
Закон композиции — если применять композицию двух функций через
map, это должно быть эквивалентно последовательному применениюmapдля каждой функции.1 2 3 4 5 6 7
const numbers = [1, 2, 3]; const f = (x) => x + 1; const g = (x) => x * 2; numbers.map(f).map(g); // [4, 6, 8] numbers.map((x) => g(f(x))); // [4, 6, 8] - то же самое
Монада — функтор с дополнительными методами (bind, chain, flatMap), решающий проблему композиции вычислений в контексте — то есть возможность последовательно соединять операции, каждая из которых работает не с «чистыми» значениями, а с данными, обёрнутыми в какой-то контекст (неопределённость, ошибка, асинхронность, побочный эффект и т.д.).
Promise — не монада в строгом смысле. Тем не менее, в сообществе его часто называют «монадоподобным».
3.5. Примеры использования монад в программировании
Монады — это паттерн функционального программирования, а не встроенный синтаксис. Они помогают управлять контекстом вычислений (ошибки, асинхронность) через цепочки операций.
Promise — условный пример монады:
1
2
3
4
5
fetch("/api/user")
.then((res) => res.json())
.then((user) => fetch(`/api/posts?user=${user.id}`))
.then((res) => res.json())
.catch((err) => console.error("Ошибка:", err));
3.6. Преимущества использования функторов
- Универсальность: единый интерфейс
mapдля разных структур данных - Безопасность: позволяют избегать мутаций и побочных эффектов
- Композируемость: позволяют строить цепочки операций
3.7. Различия между функторами и монадами с примерами
Функторы и монады — это абстракции функционального программирования, различающиеся уровнем контроля над вложенными вычислениями. Функтор реализует метод map, позволяющий применять функцию к значению внутри контейнера, но не решает проблему вложенных структур.
Монада расширяет функтор методом flatMap (или chain), который автоматически “разворачивает” вложенность.
3.8. Реализация монад в JavaScript
В JavaScript монады можно реализовать через классы или фабричные функции, инкапсулирующие значение и предоставляющие методы для безопасных преобразований. Простейший пример — создание монады Maybe для обработки потенциально отсутствующих значений (null/undefined). Она включает методы map для применения функций к значению (если оно существует) и flatMap для цепочек преобразований. Например, Promise в JavaScript — это встроенная монадоподобная конструкция, где then работает как flatMap, последовательно связывая асинхронные операции.
3.9. Монадические законы в функциональном программировании
Монадические законы — это три фундаментальных правила, которым должна удовлетворять любая монада, для её корректного поведения.
- Закон левой идентичности (Left Identity) — если поместить значение
aв монаду и применить функциюf, результат должен быть таким же, как простое применениеf(a).of(a).chain(f) === f(a)
- Закон правой идентичности (Right Identity) — если взять монаду
mи применитьofчерезchain, результат должен быть исходной монадойm.m.chain(of) === m
- Закон ассоциативности (Associativity) — при последовательном связывании нескольких монадических функций порядок группировки операций не должен влиять на результат.
m.chain(f).chain(g) === m.chain(x => f(x).chain(g))
4. JS. Common (function, hoisting, work with objects, etc)
4.1. Управляющие конструкции в JavaScript: циклы и условные операторы
В JavaScript используются стандартные управляющие конструкции:
if/elseдля ветвления логикиswitchдля множественных условий- тернарный оператор
? - циклы
for,whileиdo...while for...ofдля итерируемых объектов (массивы, строки)for...inдля перебора свойств объектов
Управление выполнением циклов:
break— прерывание циклаcontinue— прерывание текущей итерации и переход к следующей
4.2. Основы работы с функциями в JavaScript
Функции в JS бывают трёх видов:
- function declaration (подвержен всплытию “hoisting”)
- function expression (присваивается переменной)
- стрелочная функция (не имеет своего
thisиarguments, не может быть вызвана сnew)
Кроме того, есть IIFE (Immediately Invoked Function Expression) — это функция, которая объявляется и сразу же выполняется. Основная цель — изолировать область видимости. Базовый синтаксис выглядит так:
1
2
3
(function () {
// код внутри
})();
Функции — это объекты первого класса: их можно передавать аргументами, возвращать из других функций и присваивать переменным.
Область видимости переменных внутри функции:
- Переменные, объявленные внутри функции, видны только внутри этой функции.
- Функция обладает полным доступом к внешним переменным и может изменять их значение.
- Внешняя переменная используется, только если внутри функции нет такой локальной. Если одноимённая переменная объявляется внутри функции, тогда она перекрывает внешнюю.
4.3. Типы данных и операторы в JavaScript
Типы данных:
- string
- number
- bigInt
- boolean
- null
- undefined
- symbol
- object
Операторы:
- Арифметические:
+-*/%** - Сравнения:
==(с приведением типов)===(строгое)!=!==><>=<= - Логические:
&&||!(возвращают значения, не толькоboolean) - Присваивания:
=+=-=*=/= - Тернарный:
условие ? значение_если_true : значение_если_false
4.4. Основы управления объектами и массивами в JavaScript
Объект — это коллекция данных в формате «ключ: значение». Ключ — это всегда строка (или символ), а значение может быть чем угодно.
1
2
3
4
5
6
7
8
9
let someObj = { one: 1, two: 2 };
someObj.one;
someObj["one"];
someObj.newProp = "something";
delete someObj.newProp;
"one" in someObj;
Массив — это упорядоченный список элементов. В JS массивы могут содержать данные разных типов одновременно.
1
2
3
4
5
6
7
8
9
10
let someArr = [1, 2, 3];
someArr[0];
someArr.length;
someArr.filter / map / sort / reduce;
someArr.push(4); // Добавляет в конец
someArr.pop(); // Удаляет последний элемент
someArr.shift(); // Удаляет первый элемент
someArr.unshift(0); // Добавляет в начало
Объекты и массивы передаются по ссылке, а не по значению. Для создания поверхностной (shallow) копии можно использовать оператор spread: [...someArr] или {...someObj}.
4.5. Различия между объявлениями переменных: var, let, const
Ключевые различия:
- Область видимости (Scope):
var: функциональная область видимостиlet/const: блочная область видимости
- Всплытие (Hoisting):
var: всплывает с инициализациейundefined. Нет TDZ (Temporal Dead Zone, временная мертвая зона) — это период времени с момента входа в область видимости до строки, где переменная фактически объявляетсяlet/const: всплывают, но попадают в TDZ
- Повторное объявление:
var: можно переобъявлятьlet: нельзя переобъявлять в той же областиconst: нельзя переобъявлять и изменять значение
- Инициализация:
var/let: можно объявить без значенияconst: требует инициализации при объявлении
4.6. Поднятие (hoisting) и области видимости переменных
- Hoisting — это поведение JavaScript, при котором объявления переменных и функций перемещаются вверх своей области видимости на этапе компиляции
varимеет функциональную область видимости и hoisting с инициализациейundefinedletиconstимеют блочную область видимости и hoisting с TDZ (временной мертвой зоной)- Область видимости определяет доступность переменных: глобальная, функциональная, блочная
4.7. Работа с шаблонными строками (template strings)
Шаблонные строки, созданные через обратные кавычки `someString`, поддерживают многострочность без символа переноса строки \n, интерполяцию переменных через ${expression}, а также вложенные выражения.
Они удобны для создания динамических строк (HTML-шаблонов, URL), так как автоматически конвертируют выражения в строки. В отличие от обычных строк, шаблонные сохраняют пробелы и переносы, что полезно для форматирования.
4.8. Способы сравнения в JavaScript: строгое и нестрогое сравнение
- Нестрогое сравнение
==приводит типы данных перед проверкой: например,5 == '5'вернетtrue - Строгое
===проверяет равенство без приведения, включая тип:5 === '5'вернетfalse - Особые случаи:
null == undefined(true),NaN !== NaN(true).
4.9. Концепция временной мертвой зоны (Temporary Dead Zone)
TDZ — это состояние переменных let/const от начала блока до их объявления, при котором обращение к ним вызовет ReferenceError. В отличие от var, который при всплытии инициализируется как undefined, let/const в TDZ вообще недоступны.
4.10. Поведение необъявленных переменных
При обращении к необъявленной переменной в строгом режиме 'use strict' возникнет ReferenceError. В нестрогом — JS создаст её как свойство глобального объекта window, что может приводить к утечкам памяти и ошибкам.
4.11. Сборка мусора в JavaScript и её важность
Сборка мусора (Garbage Collection, GC) — это автоматический процесс освобождения памяти, занятой объектами, которые больше не используются программой и на которые нет ссылок.
4.12. Методы работы с объектами: keys, values, entries, fromEntries
Object.keys(obj)— возвращает массив ключейObject.values(obj)— возвращает массив значенийObject.entries(obj)— возвращает массив пар [ключ, значение]Object.fromEntries([[key, value]])— создает объект из массива пар
4.13. Методы работы с массивами: flat, flatMap, includes, Array.from()
flat(depth)— “выравнивает” вложенные массивы на указанную глубинуflatMap()объединяет функцииmap()иflat(), применяя функцию ко всем элементам массива и затем выравнивая результат на глубину. ПреимуществоflatMap()в том, что он упрощает процесс, делая код более читаемым и кратким при работе с вложенными структурами данныхincludes(item)— проверяет наличие элементаArray.from(iterable)— создаёт массив из итерируемого объекта
Эти методы упрощают обработку данных: flat полезен для работы с ответами API, flatMap — для фильтрации + трансформации, Array.from — для конвертации DOM-коллекций.
5. JS. Data types
5.1. Типы данных в JavaScript
Типы данных:
- string
- number
- bigInt
- boolean
- null
- undefined
- symbol
- object
5.2. Примитивные и ссылочные типы данных
В JavaScript примитивный тип данных — это данные, которые не являются объектами и не имеют методов (хотя у них есть объекты-обёртки). Они представляют собой простые, неделимые значения.
- Иммутабельны (неизменяемы)
- Сравниваются по значению
- Не имеют методов или свойств (хотя JavaScript временно оборачивает их в объекты для доступа к методам)
Все типы данных кроме object в JS являются примитивами.
Объект — это ссылочный тип данных, который хранит коллекцию ключ-значение.
- Мутабельны (изменяемы)
- Сравниваются по ссылке
- Обладают методами
Для создания поверхностных (shallow) копий объектов можно использовать spread оператор ... или Object.assign().
Для глубоких копий SON.parse(JSON.stringify(obj)).
5.3. Упаковка и распаковка (Boxing / Unboxing)
Упаковка — это когда примитивное значение автоматически оборачивается в соответствующий объект, чтобы можно было использовать методы этого объекта. JS временно создаёт объект-обёртку (String, Number, Boolean, BigInt, Symbol), чтобы вызвать метод, после чего удаляет его.
Распаковка — это обратный процесс: получение примитивного значения из объекта-обертки.
У null и undefined нет соответствующих им объектов-оберток, попытка вызвать методы приведет к ошибке.
5.4. Преобразование типов в JavaScript
Существует 3 наиболее широко используемых преобразования: строковое, численное и логическое.
Строковое– может быть вызвано с помощьюString(value). Для примитивных значений работает очевидным образом.Численное– может быть вызвано с помощьюNumber(value).Подчиняется правилам:
Значение Становится… undefinedNaNnull0true / false1 / 0stringПробельные символы по краям обрезаются.
Далее, если остаётся пустая строка, то получаем0,
иначе из непустой строки «считывается» число.
При ошибке результатNaN.Логическое– может быть вызвано с помощьюBoolean(value).Подчиняется правилам:
Значение Становится… 0,null,undefined,NaN,""falseлюбое другое значение true
Большую часть из этих правил легко понять и запомнить. Особые случаи, в которых часто допускаются ошибки:
undefinedпри численном преобразовании становитсяNaN, не0."0"и строки из одних пробелов типа" "при логическом преобразовании всегдаtrue.
Коротко из файлика:
JS автоматически преобразует типы в операциях (например, при применении оператора + к числу и строке число преобразуется в строку и происходит конкатенация). Явное приведение типов делается через функции String(), Number(), Boolean() или операторы (+ для строк, !! для булева значения). Особые случаи: null становится 0 при численном преобразовании, undefined — NaN, а пустая строка "" — false. Неявное преобразование часто вызывает ошибки, поэтому рекомендуется использовать строгое сравнение === и явные приведения.
5.5. Оператор ‘typeof’ и его использование
Оператор typeof возвращает строку с названием типа данных: number, string, boolean, undefined, object (для null и объектов), function, symbol, bigint. Его ключевая особенность — ошибочное определение null как object (историческая особенность JS). Для точной проверки типов объектов используют instanceof или Object.prototype.toString.call().
5.6. Работа с операторами ‘==’ и ‘===’ в JavaScript
- Нестрогое сравнение
==приводит типы данных перед проверкой: например,5 == '5'вернетtrue - Строгое
===проверяет равенство без приведения, включая тип:5 === '5'вернетfalse - Особые случаи:
null == undefined(true), ноnull === undefined(false)NaN === NaN(false) —NaNне равно ничему, даже самому себе
Рекомендуется всегда использовать ===, кроме случаев явного приведения (например, проверка на null/undefined через value == null).
5.7. Преобразование строк в числа и булев тип
Преобразование строк в числа:
string | Пробельные символы по краям обрезаются. Далее, если остаётся пустая строка, то получаем 0,иначе из непустой строки «считывается» число. При ошибке результат NaN. |
Number(str), унарный плюс (+str)parseInt(str, 10)parseFloat()
Преобразование в булев тип:
false:0""nullundefinedNaNtrue: все остальные значения, включая"0"" "[]{}
Явное приведение: Boolean(value) или двойное отрицание !!value.
6. JS. Closure
6.1. Определение и основные принципы замыканий в JavaScript
Замыкание — это комбинация функции и лексического окружения, в котором она была объявлена. Оно позволяет функции запоминать и получать доступ к переменным из внешней области видимости даже после того, как внешняя функция завершила выполнение.
6.2. Примеры использования замыканий для решения базовых задач
Создание функции-счетчика:
1
2
3
4
5
6
7
8
9
10
11
function createCounter() {
let count = 0;
return function () {
return ++count;
};
}
const counter = createCounter(); // создаем замыкание
counter(); // 1
counter(); // 2
6.3. Проблемы и ограничения, связанные с замыканиями
- Утечки памяти: Замыкания удерживают ссылки на внешние переменные
- Сложность отладки: Поскольку переменные захватываются из внешних областей видимости, бывает трудно отследить, какое значение они содержат в конкретный момент, особенно в асинхронном коде
- Производительность: Создание множества замыканий, особенно в циклах, может замедлять работу из-за дополнительных расходов на хранение окружения для каждого экземпляра
- Неочевидное поведение: Замыкания могут сохранять устаревшие значения переменных, если те изменяются после создания замыкания, что приводит к неожиданным результатам
- Сложность тестирования: Замкнутые приватные переменные труднее тестировать извне, так как к ним нет прямого доступа
6.4. Примеры использования замыканий для решения специфических задач
1
2
3
4
5
6
function createUniqueId(prefix) {
let count = 0;
return () => `${prefix}_${++count}`;
}
const getUserId = createUniqueId("user");
getUserId(); // "user_1"
Преимущества такого подхода:
- Переменная
countполностью приватная - Каждый генератор имеет свое собственное состояние
6.5. Разработка сложных решений с использованием замыканий без подсказок
Тот же самый пример:
1
2
3
4
5
6
function createUniqueId(prefix) {
let count = 0;
return () => `${prefix}_${++count}`;
}
const getUserId = createUniqueId("user");
getUserId(); // "user_1"
6.6. lexical env
Лексическое окружение — это внутренняя структура данных, которая хранит связь между идентификаторами (именами переменных, функций) и их значениями в определённом участке кода.
Основные компоненты:
- Запись окружения (environment record) — объект, хранящий фактические переменные и функции
- Ссылка на внешнее лексическое окружение (outer reference) — ссылка на окружение, в котором был создан текущий код
Каждый вызов функции создает новое лексическое окружение, образуя цепочку (scope chain). Именно эта цепочка позволяет замыканиям получать доступ к переменным из внешних функций даже после их завершения. Движок оптимизирует доступ к переменным через эту цепочку, но избыточная вложенность может влиять на производительность.
7. JS. Context
7.1. Основы контекста в JavaScript
Контекст выполнения (this) — это специальная переменная, которая ссылается на объект, в контексте которого выполняется функция.
Его значение динамично и зависит от способа вызова:
- В глобальной области
thisссылается на глобальный объект (windowв браузере илиglobalв Node.js) - В обычной функции
thisссылается на глобальный объект (нестрогий режим) илиundefined(строгий режим) - В методе объекта
this— сам объект (при вызове через точку:obj.method()) - В стрелочных функциях
thisберётся из внешнего лексического окружения
7.2. Глобальный контекст в JavaScript
Глобальный контекст выполнения в JavaScript определяется при запуске программы и является базовым контекстом.
В глобальном контексте находится всё, что объявлено не внутри функций, блоков или модулей:
- Глобальные переменные
- Глобальные функции
- Встроенные объекты (
Math,JSON,Dateи другие) - Глобальные API (
document,console,navigator)
В глобальном контексте this ссылается на:
- window (в браузере)
- global (в Node.js)
undefinedв строгом режиме
Есть globalThis — современный способ доступа к глобальному объекту (window), который работает везде.
7.3. Определение контекста во время выполнения программы
Контекст (this) определяется в момент вызова функции, а не её объявления. Это приводит к потере контекста, если функция передаётся как колбэк: const obj = { name: ‘Alice’, greet() { console.log(this.name); } }; setTimeout(obj.greet, 100); // Ошибка: this === window Решение — явная привязка через bind, call/apply или стрелочные функции.
7.4. Привязка контекста к функции и возможности множественной привязки
JavaScript предоставляет три метода для управления контекстом:
bind(context)— создаёт новую функцию с жёстко привязаннымthis.call(context, ...args)— вызывает функцию с заданнымthisи аргументами.apply(context, [args])— аналогиченcall, но аргументы передаются массивом.
7.5. Потеря контекста в функциях
Контекст теряется, когда:
- Функция передаётся как колбэк (
setTimeout, обработчики событий) - Метод объекта присваивается переменной
Решение — явная привязка через bind, call/apply.
Пример:
1
2
3
4
5
6
7
8
9
const obj = {
name: "Kate",
log() {
console.log(this.name);
},
};
const log = obj.log;
log(); // Ошибка: this === undefined (в strict mode)
7.6. Решение задач на потерю и восстановление контекста
Жёсткая привязка через
bind:1
setTimeout(obj.log.bind(obj), 100); // "Kate"
Стрелочные функции:
1 2
const log = () => obj.log(); log(); // "Kate"
Паттерн “мягкая привязка”:
1 2 3 4 5 6
function foo() { console.log(this.name); } const context = { name: "Soft" }; foo.call(context); // "Soft"
7.7. Особенности работы контекста с хуками в React
В React хуки (например, useState, useEffect) не имеют собственного this, так как работают внутри функциональных компонентов. Проблемы контекста возникают из-за:
- неправильного обращения с состоянием и эффектами
- при передаче колбэков в дочерние компоненты (решение —
useCallback)
7.8. Использование контекста для управления состоянием в React-приложениях
Контекст React (createContext) позволяет передавать данные через дерево компонентов без явной передачи пропсов:
Создание контекста:
1
const ThemeContext = createContext("light");
Обёртка провайдером:
1 2 3
<ThemeContext.Provider value="dark"> <App /> </ThemeContext.Provider>
Получение значения через
useContext:1 2 3 4
function Button() { const theme = useContext(ThemeContext); return <button className={theme}>Click</button>; }
Это удобно для глобальных данных (темы, настройки, авторизация).
8. JS. Asynchronous programming
8.1. Блокирующий код и его влияние
Блокирующий код — это синхронные операции, которые останавливают выполнение программы до своего завершения (например, сложные вычисления или синхронные HTTP-запросы). В JavaScript, который работает в одном потоке, это приводит к “замораживанию” интерфейса, так как цикл событий (event loop) не может обрабатывать другие задачи (анимации, клики) до завершения блокирующей операции. Для избежания этого используют асинхронные подходы: промисы, async/await, Web Workers.
8.2. Цикл событий (event loop)
Event loop — это механизм, который управляет выполнением асинхронного кода в JavaScript. Он постоянно проверяет две очереди: стек вызовов (для синхронного кода) и очередь задач (для асинхронных колбэков). Когда стек пуст, event loop берёт первую задачу из очереди (например, колбэк setTimeout) и помещает её в стек.
8.3. Использование setTimeout и setInterval
setTimeout(fn, delay)выполняет функциюfnодин раз после указанной задержкиdelay(в миллисекундах)setInterval(fn, delay)вызываетfnповторно с интерваломdelay
Особенности:
- Фактическая задержка может быть больше из-за занятости основного потока
setIntervalне учитывает время выполнения самой функции, что может приводить к наложению вызовов
8.4. Остановка setTimeout и setInterval
Для отмены асинхронных операций используют:
clearTimeout(id)— отменяетsetTimeoutпо его идентификаторуclearInterval(id)— останавливаетsetInterval
8.5. Понимание setTimeout с нулевой задержкой
setTimeout(fn, 0) не выполняет функцию мгновенно, а помещает её в конец очереди макрозадач. Это позволяет:
- Завершить текущий синхронный код
- Дать браузеру обновить UI (например, перед тяжёлыми вычислениями)
8.6. Макро- и микрозадачи в асинхронном программировании
- Микрозадачи (
Promise,queueMicrotask,MutationObserver) выполняются сразу после текущего синхронного кода, перед следующей макрозадачей - Макрозадачи (
setTimeout,setInterval, события DOM (клики, загрузка)) — на следующей итерации event loop
Порядок выполнения:
- Синхронный код
- Все микрозадачи
requestAnimationFrame(callback)- Рендеринг
- Одна макрозадача
- Повтор
8.7. render, requestAnimationFrame
requestAnimationFrame(callback)выполняетcallbackперед следующей отрисовкой кадра. Оптимален для анимаций, так как синхронизирован с частотой обновления экрана- Рендеринг (перерисовка UI) имеет приоритет над макрозадачами, но ждёт завершения всех микрозадач
8.8. workers
Web Workers позволяют выполнять код в отдельном потоке, не блокируя основной. Используются для:
- Тяжёлых вычислений
- Обработки больших данных
Ограничения:
- Нет доступа к DOM
- Общение через
postMessageиonmessage
8.9. observers
Наблюдатели (например, MutationObserver, IntersectionObserver) реагируют на изменения в DOM или видимости элементов:
MutationObserverотслеживает изменения в дереве DOMIntersectionObserverопределяет, когда элемент появляется вviewport
8.10. queueMicrotask
queueMicrotask(fn) добавляет функцию в очередь микрозадач (как .then у промиса). Используется для выполнения кода сразу после текущего синхронного задания, но перед макрозадачами.
9. JS. Garbage collector
9.1. Основы сборки мусора в JavaScript
Сборка мусора (Garbage Collection, GC) — это автоматический процесс освобождения памяти, занятой объектами, которые больше не используются программой. В JavaScript разработчик не управляет памятью вручную — движок (V8, SpiderMonkey) самостоятельно определяет “мусор” и очищает его. Это избавляет от утечек памяти.
9.2. Принципы работы сборщика мусора
Сборщик мусора в JS работает по алгоритму маркировки и очистки (Mark-and-Sweep):
- Маркировка: движок обходит все достижимые объекты, начиная с корневых (глобальные переменные, стек вызовов), и помечает их как “живые”
- Очистка: память, занятая неотмеченными (“мертвыми”) объектами, освобождается
Современные движки (например, V8) дополняют этот подход поколенческой сборкой (Generational GC), разделяя объекты на “молодые” (часто меняющиеся) и “старые” (долгоживущие).
9.3. Механизмы определения ‘мусора’ в памяти
Объект считается “мусором”, если он недостижим из корневых точек:
- Нет ссылок из глобальных переменных
- Нет ссылок из стека вызовов функций
- Нет ссылок из других достижимых объектов
9.4. Примеры освобождения памяти сборщиком мусора
1
2
3
4
5
function process() {
const temp = new Array(1000);
// После вызова функции `temp` станет недостижим и будет удалён
}
process();
Обрыв ссылок:
1
2
let cache = { data: "Важные данные" };
cache = null; // Объект удаляется при следующем цикле GC
9.5. Значение сборщика мусора для управления памятью
GC избавляет разработчиков от ручного управления памятью (как в C/C++), предотвращая:
- Утечки памяти (например, забытые обработчики событий)
- Висячие ссылки (ссылки на уже ненужные объекты)
Однако автоматизация не исключает необходимости оптимизации — плохие практики (например, глобальные кэши) могут перегружать GC
9.6. Механизм отслеживания достижимости
Достижимость определяется через алгоритм трёхцветной маркировки:
- Белый (не посещённый) – объект не проверен, изначально все объекты белые
- Серый (в обработке) – объект достижим, но его ссылки ещё не проверены
- Чёрный (обработанный) – объект достижим, и все его ссылки проверены
Как работает алгоритм:
Все объекты помечаются белыми. Корневые ссылки (глобальные переменные, стек вызовов) помечаются серыми. Пока есть серые объекты, сборщик берёт один из них и проверяет его ссылки: все найденные белые объекты становятся серыми. После проверки исходный объект помечается чёрным.
Когда серых объектов не остаётся, все белые объекты считаются недостижимыми (мусор) и удаляются.
9.7. Влияние стека вызовов на сборщик мусора
Стек вызовов — одна из корневых точек для GC. Локальные переменные функций считаются достижимыми, пока функция выполняется. После завершения функции её контекст (и связанные объекты) могут быть удалены, если нет других ссылок.
9.8. Типы сборщиков мусора и их отличия
- Mark-and-Sweep (метка и очистка) — базовый алгоритм
- Generational GC — разделяет объекты на поколения (young/new-space и old-space)
- Incremental GC — разбивает сборку на фазы для уменьшения лагов
- Concurrent GC — работает параллельно с основным потоком
V8 использует Generational Incremental Mark-and-Sweep для баланса между скоростью и эффективностью.
9.9. Влияние сборщика мусора на производительность приложения
Проблемы:
- Stop-the-World: GC иногда останавливает выполнение кода (особенно при сборке старого поколения)
- Лаги в анимациях/интерактивах при частых циклах сборки
Оптимизации:
- Уменьшение количества временных объектов
- Избегание глобальных переменных
9.10. Стратегии оптимизации работы с памятью
- Переиспользование объектов: пулы объектов вместо создания/удаления
- Избегание утечек: удаление обработчиков событий, таймеров
- Осторожность с замыканиями: они сохраняют ссылки на внешние переменные
- Использование типизированных массивов для больших данных
9.11. Поколенческий сборщик мусора и его принципы работы
Поколенческий сборщик мусора (Generational GC) минимизирует задержки в интерактивных приложениях за счёт разделения объектов по “поколениям” (молодые, старые) и частой очистки только молодых объектов.
Большинство объектов становятся мусором быстро (например, временные переменные в функциях), поэтому GC в первую очередь сканирует молодое поколение. Реже проверяются старые объекты, которые живут дольше. Такой подход сокращает общее время остановок.
9.12. Минимизация задержек сборщика мусора в интерактивных приложениях
К способам минимизации задержек, вызванных сборкой мусора в интерактивных JavaScript-приложениях, относятся:
- Управление потреблением памяти: предотвращение резких скачков путём постепенной загрузки данных, использования инкрементальной сборки мусора или применения слабых ссылок (WeakRef, WeakMap, WeakSet) для снижения нагрузки на сборщик
- Оптимизация работы с DOM: отказ от частых прямых манипуляций с DOM в пользу пакетного обновления (batch updates), что уменьшает количество создаваемых и удаляемых объектов
9.13. Управление памятью с помощью сборщика мусора в Node.js
Node.js использует тот же GC, что и Chrome (V8), но с доп. настройками:
--max-old-space-size— лимит памяти для старого поколения--incremental-gc— включает режим инкрементальной сборки мусора
9.14. Различие между автоматической и ручной управлением памятью
- Автоматическая (JS): удобна, но менее предсказуема (задержки GC)
- Ручная (C/C++): полный контроль, но риск утечек и висячих ссылок
В JS ручное управление имитируется через:
WeakMap/WeakSet(не препятствуют сборке)- Обнуление ссылок (
ref = null)
9.15. Ручное управление памятью при наличии сборщика мусора
Несмотря на автоматизацию, в JS можно оптимизировать память:
WeakRef для кэшей:
1 2
const cache = new WeakMap(); cache.set(obj, data);
Отписка от событий:
1
window.removeEventListener("resize", handler);
Очистка больших структур:
1
largeArray.length = 0; // Освобождение памяти
10. JS. Promise, async/await
10.1. Основы промисов и колбэков
Промисы (Promises) и колбэки — это механизмы для работы с асинхронным кодом в JavaScript.
// Колбэки — это функции, передаваемые в качестве аргументов, которые вызываются после завершения операции (например, fs.readFile в Node.js).
// Промисы предоставляют более удобный способ обработки асинхронности, избегая “ада колбэков” (callback hell). Промис — это объект, представляющий результат асинхронной операции.
10.2. Примеры асинхронных функций
Пример с fetch:
1 2 3 4 5 6
async function fetchData() { const response = await fetch("https://api.example.com"); const data = await response.json(); return data; } fetchData().then((data) => console.log(data));
Пример с setTimeout:
1 2 3 4 5 6 7 8
function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function logAfterDelay() { await delay(1000); console.log("Done"); } logAfterDelay();
10.3. Состояния промисов
Промис может находиться в одном из трёх состояний:
pending— начальное состояние, операция не завершена.fulfilled— операция завершена успешно (вызываетсяthen).rejected— операция завершена с ошибкой (вызываетсяcatch).
После перехода в fulfilled или rejected промис становится неизменяемым.
10.4. Метод finally в промисах
Метод finally выполняется после then или catch. Он выполняется вне зависимости от того, завершился промис успехом или ошибкой. Метод finally используют для очистки ресурсов (закрытие соединений, сброс состояния).
10.5. Обработка ошибок и исключений в промисах
Ошибки в промисах можно обрабатывать через:
- .catch() — перехватывает ошибки в цепочке промисов.
- try/catch в async/await.
Пример с
.catch():1 2 3 4 5 6
fetch("<https://api.example.com>") .then((response) => { if (!response.ok) throw new Error("Ошибка сети"); return response.json(); }) .catch((err) => console.error(err));
Пример с
async/await:1 2 3 4 5 6 7 8
async function loadData() { try { const response = await fetch("<https://api.example.com>"); return await response.json(); } catch (err) { console.error(err); } }
10.6. Использование метода then для обработки ошибок
Второй аргумент .then() может обрабатывать ошибки, но это менее удобно, чем .catch(), потому что такой подход не перехватывает ошибки внутри первого обработчика
1
2
3
4
fetch("<https://api.example.com>").then(
(response) => response.json(),
(err) => console.error("Ошибка:", err), // Аналог .catch()
);
10.7. Обработка ошибок и исключений в async/await
Обработка ошибок в async/await работает синхронно и использует привычные try/catch блоки, что делает код более линейным и читаемым. В промисах же ошибки перехватываются через метод .catch() или второй аргумент then(), что создает цепочки обработчиков.
Главное отличие - async/await позволяет писать асинхронный код, который выглядит и обрабатывается как синхронный, включая механизмы ошибок, тогда как промисы требуют более декларативного подхода с отдельными обработчиками.
10.8. Переписывание кода с промисов на async/await
Исходный код (промисы):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fetchData()
.then(data => processData(data))
.catch(error => handleError(error));
Аналог с async/await:
async function handleData() {
try {
const data = await fetchData();
const processedData = await processData(data);
return processedData;
} catch (error) {
handleError(error);
}
}
handleData(); // Вызов функции
10.9. Статические методы
Promise.resolve(value)— возвращает успешный промис сvaluePromise.reject(error)— возвращает отклонённый промис сerrorPromise.all([...promises])— ожидает все промисы; если один отклонён — вся группа отклоненаPromise.race([...promises])— возвращает первый завершённый промис (успешный или ошибку)Promise.allSettled([...promises])— ждёт все промисы, возвращает их статусы
11. JS. Error handling
11.1. Основные понятия ошибок в JavaScript
Ошибки в JavaScript — это механизм прерывания нормального выполнения программы при возникновении исключительных ситуаций (например, обращение к несуществующей переменной, сбой сети). Они могут быть синхронными (выбрасываются сразу при выполнении кода) или асинхронными (например, в промисах). Ошибки времени выполнения — ошибки, которые возникают во время выполнения программы, когда она уже запущена.
11.2. Типы ошибок в JavaScript
Error— базовая ошибка (например,new Error("Сообщение"))SyntaxError— синтаксическая ошибка в кодеeval("1 +")ReferenceError— обращение к несуществующей переменнойx = 1TypeError— недопустимая операцияnull.f()RangeError— выход за допустимые пределыnew Array(-1)- Пользовательские ошибки — создаются через
class MyError extends Error
11.3. Обработка ошибок без использования try-catch
В JavaScript для обработки ошибок, помимо классической конструкции try...catch, существуют и другие подходы: обработка ошибок в callback функциях, использование операторов “или” || и “и” &&, проверка условий перед выполнением операций, использование библиотек для обработки ошибок.
11.4. Различие между синтаксическими ошибками и ошибками времени выполнения
- Синтаксические (например, пропущенная скобка) обнаруживаются при парсинге кода и не могут быть перехвачены
try..catch - Ошибки времени выполнения (например,
undefined()) возникают при выполнении и могут быть перехвачены
11.5. Последствия ошибок в выполнении кода
- Необработанные ошибки прерывают выполнение скрипта (в браузере) или завершают процесс (Node.js)
- В асинхронном коде (промисы, таймеры) ошибки могут “теряться”, если нет обработчика
- В React необработанные ошибки приводят к “падению” компонента (решается с помощью ErrorBoundary)
11.6. Принципы работы конструкции try-catch
В блоке try размещается потенциально опасный код, который может вызвать ошибку. Если внутри try возникает исключение, выполнение немедленно прерывается и управление передаётся в блок catch, куда попадает объект ошибки для обработки.
try..catch перехватывает только синхронные ошибки. Для асинхронного кода требуется дополнительная обработка: либо вложенный try..catch внутри асинхронной функции с await, либо обработчик .catch() для промисов.
Блок finally выполняется в любом случае, не важно, было ли выброшено исключение или нет, что полезно для очистки ресурсов.
11.7. Использование множественных блоков catch
В JavaScript нельзя использовать несколько catch для разных типов ошибок (как в Java). Вместо этого нужно проверять тип ошибки внутри одного блока, используя if/else:
1
2
3
4
5
6
7
8
try {
// Код с возможными ошибками
} catch (err) {
if (err instanceof TypeError) {
console.log('TypeError');
} else if (err instanceof ReferenceError) {
} else {...}
}
11.8. Цель и использование блока finally
Блок finally выполняется в любом случае, даже если в try или catch был return, или если ошибка не перехвачена, что полезно для очистки ресурсов.
11.9. Передача ошибок из блока catch
В JavaScript блок catch используется для обработки ошибок, возникающих в блоке try. Если в блоке try происходит исключение, выполнение кода в этом блоке прерывается, и управление передается в соответствующий блок catch, где происходит обработка ошибки.
11.10. Применение try-catch в асинхронных функциях
В асинхронных функциях, как и в обычных, для обработки ошибок используется конструкция try...catch. блок try содержит код, который может вызвать исключение, а блок catch обрабатывает это исключение. Ключевое слово await используется внутри async функций для ожидания результатов асинхронных операций, и его также можно обернуть в try...catch для перехвата ошибок, возникающих в этих операциях.
11.11. Создание собственных типов ошибок
Создание пользовательских ошибок осуществляется с помощью класса Error и extends для наследования. Они полезны для детализации проблем. + п.12 + п.13
11.12. Правила выбрасывания исключений
При выбрасывании исключений (оператор throw) важно следовать определенным правилам, чтобы обеспечить надежность и понятность кода. Исключения должны использоваться для обозначения исключительных ситуаций, а не для управления обычным потоком выполнения. Важно также избегать выбрасывания общих исключений и предоставлять конкретную информацию о причине ошибки.
- Используйте
throw new Error(), а неthrow "строка"(теряется стек вызовов) - Выбрасывайте ошибки только для исключительных ситуаций (не для управления потоком)
- Документируйте типы ошибок в API
11.13. Перехват необработанных исключений
Для перехвата необработанных исключений в JavaScript используется конструкция try...catch. Она позволяет обернуть блок кода, который может вызвать ошибку, и обработать исключение, если оно произойдет.
В браузере:
1
2
3
window.addEventListener("error", (event) => {
console.log(event.error); // Перехватывает все ошибки
});
В Node.js:
1
2
3
process.on("uncaughtException", (err) => {
console.error("Необработанная ошибка:", err);
});
11.14. Использование исключений для управления потоком выполнения
Преимущества исключений:
- Не “забываются” (в отличие от проверки кода ошибки)
- Автоматически прерывают выполнение
- Стек вызовов помогает в отладке
Пример антипаттерна (коды возврата):
1
2
3
4
function readFile() {
if (!fileExists) return { error: "File not found" }; // Легко пропустить проверку
return { data: "..." };
}
11.15. Преимущества исключений перед кодами возврата
Совпадает с п.14