Основы JavaScript для будущих фронтенд-разработчиков

Курс формирует фундаментальные навыки программирования на JavaScript, необходимые для перехода к React и Vue. Студенты освоят современный синтаксис, методы обработки данных и принципы асинхронного взаимодействия с интерфейсом.

1. Основы синтаксиса, переменные и типы данных в современном JavaScript

Основы синтаксиса, переменные и типы данных в современном JavaScript

В 1995 году Брендан Эйк создал первую версию JavaScript всего за десять дней. Тогда никто не предполагал, что этот «игрушечный» язык для создания анимаций со снегом на веб-страницах станет фундаментом для сложнейших интерфейсов Facebook, Google Maps и Netflix. Сегодня JavaScript — это не просто скрипты, а мощная экосистема. Однако даже самый опытный разработчик на React может столкнуться с трудноуловимым багом, если он не до конца понимает, как язык обрабатывает типы данных или почему переменная внезапно «всплывает» в коде. Начнем погружение с того, как компьютер интерпретирует наши команды.

Анатомия синтаксиса и культура написания кода

JavaScript — это язык с «си-подобным» синтаксисом. Если вы когда-либо видели код на C++, Java или C#, многие конструкции покажутся вам знакомыми. Но у JS есть свои капризы. Основной единицей выполнения является инструкция (statement). Каждая инструкция — это законченная команда для интерпретатора.

Исторически сложилось, что инструкции разделяются точкой с запятой ;. В современном стандарте ECMAScript (стандарт, по которому живет JS) существует механизм Automatic Semicolon Insertion (ASI). Он позволяет опускать точку с запятой в большинстве случаев, так как среда выполнения расставит их сама. Однако это «магическое» поведение иногда приводит к ошибкам. Например, если вы решите перенести возвращаемое значение функции на новую строку после ключевого слова return, интерпретатор может вставить ; сразу после return, и ваша функция вернет undefined вместо ожидаемых данных. Поэтому в профессиональном сообществе принято либо всегда ставить точки с запятой, либо использовать инструменты автоматического форматирования (например, Prettier), которые делают код единообразным.

Важную роль играют комментарии. В JS они бывают двух видов:

  • Однострочные: // текст комментария.
  • Многострочные: / текст комментария /.
  • Хорошим тоном считается писать код так, чтобы он был самодокументированным. Вместо того чтобы писать комментарий «здесь мы вычисляем налог», лучше назвать переменную taxAmount.

    Эволюция переменных: от var к let и const

    Переменная — это именованное хранилище для данных. В JavaScript способ объявления переменных радикально изменился с выходом стандарта ES6 в 2015 году. До этого существовало только ключевое слово var.

    Основная проблема var заключается в отсутствии блочной области видимости. Если вы объявите переменную через var внутри цикла или условия if, она будет доступна и за их пределами. Это часто приводило к конфликтам имен и непредсказуемому поведению программы. Кроме того, var подвержен эффекту «всплытия» (hoisting): вы можете обратиться к переменной до того, как она объявлена в коде, и не получите ошибку (она просто будет содержать undefined).

    Современный стандарт ввел let и const, которые работают иначе: * Область видимости: Они ограничены блоком {...}. Если переменная создана внутри if, она «умрет» вместе с окончанием этого блока. * Временная мертвая зона: Вы не можете обратиться к let или const до их объявления. Попытка сделать это вызовет ReferenceError.

    Различие между let и const фундаментально для чистоты кода. * let используется, когда значение переменной будет меняться в процессе работы программы (например, счетчик в цикле). * const (константа) используется для значений, которые не должны переназначаться.

    > Важный нюанс: const запрещает только переприсваивание самой переменной. Если в константе хранится объект или массив, вы все еще можете изменять их внутреннее содержимое (добавлять свойства или элементы). Это часто путает новичков, ожидающих полной неизменяемости.

    При выборе между ними всегда следуйте правилу «минимальных привилегий»: используйте const по умолчанию, и только если вы уверены, что значение должно измениться, меняйте его на let. Это защищает ваш код от случайных мутаций.

    Динамическая типизация и примитивы

    JavaScript — это язык с динамической типизацией. Это значит, что переменная не привязана к конкретному типу данных навсегда. В одну и ту же переменную вы можете сначала записать число, а затем строку.

    Типы данных в JS делятся на две большие группы: примитивы и объекты. Примитивы передаются по значению и являются неизменяемыми (immutable). Если вы меняете строку, JS на самом деле создает новую строку в памяти.

    Числа (Number и BigInt)

    Тип Number представляет собой 64-битное число с плавающей точкой в формате IEEE 754. В JS нет разделения на целые числа и числа с запятой (как int и float в других языках). Существуют специальные числовые значения: * Infinity и -Infinity: результат деления на ноль. * NaN (Not a Number): результат ошибочной математической операции (например, деления строки на число).

    > Особенность NaN в том, что он не равен самому себе. Проверка ` вернет false. Для проверки используется функция Number.isNaN().

    Для работы с очень большими целыми числами, которые выходят за пределы безопасного диапазона Number (), используется тип BigInt. Он создается путем добавления буквы n в конце числа: 100n.

    Строки (String)

    Строки в JS можно оборачивать в три вида кавычек:
  • Одинарные '...' и двойные "..." идентичны.
  • Обратные кавычки (бэктики) ... позволяют использовать шаблонные строки. В них можно вставлять выражения с помощью конструкции {user}!); // Интерполяция
  • javascript let a = 10; let b = a; // b теперь 10, они независимы javascript let user = { name: "Ivan" }; let admin = user; // Копируется ссылка admin.name = "Petr"; console.log(user.name); // "Petr", так как объект один и тот же javascript console.log(5 + "5"); // "55" (строка) console.log(10 - "5"); // 5 (число, так как минус не определен для строк) `

    Чтобы код был предсказуемым, лучше использовать явное преобразование: * Number(value) — к числу. * String(value) — к строке. * Boolean(value) — к логическому типу.

    Особое внимание стоит уделить преобразованию в число. Если строка содержит не только цифры, результатом будет NaN. Исключением является функция parseInt(), которая пытается «вытащить» число из начала строки, пока не встретит посторонний символ.

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

    В JavaScript принят стиль camelCase для переменных и функций (например, calculateTotalPrice). Имена классов обычно пишутся в PascalCase (UserAccount). Константы, значения которых известны до начала выполнения программы и не меняются (так называемые «жесткие константы»), пишутся в UPPER_SNAKE_CASE (API_KEY).

    Имя переменной должно отвечать на вопрос «что это?», а не «какого это типа?». Плохо: userArray. Хорошо: users. Избегайте сокращений вроде data, item, val`, если из контекста не очевидно, что именно там хранится.

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

    2. Функции и логика управления: от классических объявлений до стрелочного синтаксиса

    Функции и логика управления: от классических объявлений до стрелочного синтаксиса

    Представьте, что вы строите сложный механизм, где каждая деталь должна срабатывать только при определенных условиях и строго по заданному алгоритму. В программировании такими деталями являются функции — изолированные блоки кода, которые можно вызывать многократно, передавая им разные входные данные. Без функций написание кода превратилось бы в бесконечное копирование одних и тех же строк, что сделало бы поддержку любого интерфейса невозможной. В JavaScript функции — это не просто вспомогательный инструмент, а «граждане первого класса», обладающие огромной гибкостью.

    Управляющие конструкции: ветвление и логический выбор

    Прежде чем переходить к архитектуре функций, необходимо понять, как программа принимает решения. Логика управления определяет, какой участок кода будет выполнен, а какой проигнорирован. В основе этого лежат условные операторы.

    Классическая конструкция if...else проверяет условие на истинность. Если результат выражения внутри скобок приводится к true, выполняется первый блок кода. В противном случае управление переходит к блоку else.

    Однако, когда вариантов становится слишком много, цепочка else if превращается в «спагетти-код», который трудно читать. Для таких случаев в JavaScript предусмотрен оператор switch. Он сравнивает значение переменной с набором константных вариантов (case). Важно помнить об операторе break: если его пропустить, JavaScript продолжит выполнять все последующие блоки case независимо от того, совпадают ли они с условием. Это поведение называется «проваливанием» (fall-through).

    Особое место занимает тернарный оператор. Это единственный оператор в языке, принимающий три операнда. Его часто используют в React-разработке для условного рендеринга компонентов внутри JSX. Его структура: условие ? выражение_1 : выражение_2. Если условие истинно, возвращается результат первого выражения, если ложно — второго.

    Анатомия функции: Declaration против Expression

    В JavaScript существует несколько способов создания функций, и выбор между ними напрямую влияет на то, как ваш код будет вести себя при запуске.

    Function Declaration (Объявление функции)

    Это самый старый и привычный способ. Функция объявляется с помощью ключевого слова function, за которым следует имя.

    Главная особенность Declaration — это hoisting (всплытие), которое мы упоминали в контексте переменных. Интерпретатор JavaScript находит все объявления функций перед выполнением кода. Это означает, что вы можете вызвать функцию calculateTotal() в первой строке файла, а описать её тело в самом конце. С точки зрения архитектуры это позволяет располагать логику высокого уровня в начале файла, а детали реализации прятать ниже.

    Function Expression (Функциональное выражение)

    Здесь функция создается как часть выражения и обычно присваивается переменной.

    В отличие от Declaration, такие функции не «всплывают». Вы не можете вызвать multiply() до того, как интерпретатор дойдет до строки с её определением. Это делает поток выполнения программы более предсказуемым. Кроме того, функциональные выражения позволяют создавать анонимные функции — функции без имени, которые часто используются как аргументы для других функций (callback-функции).

    Параметры и возвращаемые значения

    Функция — это черный ящик. На вход поступают аргументы, внутри происходит магия, а на выходе мы получаем результат через оператор return.

    Если вызвать return без значения или если функция завершится без этого оператора, она вернет undefined. Это критически важный нюанс: JavaScript-функция всегда что-то возвращает.

    Современный стандарт ES6 привнес удобный механизм — параметры по умолчанию. Раньше, если аргумент не был передан, он становился undefined, что приводило к ошибкам вроде NaN при арифметических операциях. Теперь мы можем задать страховку прямо в заголовке:

    Этот подход позволяет отделять логику «когда сделать» от логики «что именно сделать». Например, браузер может ждать клика пользователя (событие), и только в момент клика вызвать ваш коллбэк.

    Рекурсия: когда функция вызывает саму себя

    Иногда решение задачи требует повторения одного и того же действия на разных уровнях вложенности (например, обход папок на диске или элементов в сложном меню). В таких случаях применяется рекурсия.

    Для успешной рекурсии критически важны два элемента:

  • Базовый случай (условие выхода) — точка, в которой функция перестает вызывать себя и начинает возвращать результат. Без него возникнет бесконечный цикл и ошибка Maximum call stack size exceeded.
  • Рекурсивный шаг — вызов функции с измененными параметрами, приближающими нас к базовому случаю.
  • Пример вычисления факториала числа :

    В коде это выглядит так:

    Здесь при каждом вызове создается новый кадр в стеке вызовов (call stack). Если будет слишком большим, стек переполнится. Поэтому в высокопроизводительных вычислениях рекурсию иногда заменяют обычными циклами, но для работы со структурами данных вроде деревьев (которым является и DOM в браузере) рекурсия незаменима.

    Чистые функции и побочные эффекты

    При подготовке к React важно понимать концепцию чистых функций (pure functions). Функция считается чистой, если:

  • Она всегда возвращает один и тот же результат при одних и тех же входных аргументах.
  • У неё нет побочных эффектов (side effects). Она не меняет глобальные переменные, не пишет в консоль, не делает сетевых запросов и не меняет объекты, переданные по ссылке.
  • Чистые функции легче тестировать и отлаживать. Современный фронтенд стремится к тому, чтобы компоненты вели себя как чистые функции: получили данные (props) — отобразили интерфейс.

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

    3. Объекты и массивы: структуры данных и современные методы их перебора

    Объекты и массивы: структуры данных и современные методы их перебора

    Если представить JavaScript как живой организм, то переменные — это его клетки, а объекты и массивы — сложные органы и системы, которые организуют эти клетки в работающее целое. В реальной разработке вы почти никогда не работаете с одиночными числами или строками. Данные приходят с сервера в виде массивов пользователей, каждый из которых представлен объектом с десятком свойств. Умение эффективно упаковывать, извлекать и трансформировать эти структуры — это 90% ежедневной работы фронтенд-разработчика.

    Анатомия объектов: гибкость и ассоциативность

    Объект в JavaScript — это неупорядоченная коллекция пар «ключ-значение». Если массив — это пронумерованный список, то объект — это словарь или картотека. Ключом (именем свойства) почти всегда выступает строка или символ, а значением может быть что угодно: от примитива до другого объекта или функции.

    Создание объекта через литерал {} является стандартом де-факто. Несмотря на то что объекты кажутся простыми, их поведение таит важные нюансы, связанные с доступом к данным.

    Точечная нотация против квадратных скобок

    Существует два способа обратиться к свойству объекта: user.name и user['name']. Новичкам часто кажется, что точка удобнее, и это правда, но у неё есть жесткие ограничения. Точка требует, чтобы имя свойства было валидным идентификатором (не начиналось с цифры, не содержало пробелов и тире). Квадратные скобки позволяют использовать динамические ключи.

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

    Ссылка на контекст и сокращенная запись

    Современный стандарт ES6+ привнес «синтаксический сахар», который делает код чище. Если имя переменной совпадает с именем ключа объекта, мы можем опустить дублирование:

    Важно помнить, что объекты являются ссылочными типами данных. Когда вы копируете объект в новую переменную, вы копируете не «коробку с данными», а «адрес этой коробки». Это приводит к тому, что изменение свойства в одной переменной мгновенно отражается во всех остальных, указывающих на тот же объект. Для создания независимой копии сегодня чаще всего используют spread-оператор (распыление): const clone = { ...original };. Однако стоит учитывать, что это поверхностное копирование (shallow copy): если внутри объекта был другой объект, ссылка на него останется прежней.

    Массивы: упорядоченность и индексация

    Массивы в JavaScript — это особый тип объектов, где ключами автоматически становятся индексы (целые числа, начиная с нуля), а сама структура оптимизирована для хранения последовательностей. Главное отличие массива от «просто объекта» — наличие магического свойства length и огромного набора встроенных методов для манипуляции данными.

    Длина массива в JS ведет себя специфично. Это не просто количество элементов, а «максимальный индекс плюс один». Если вы вручную установите array.length = 0, вы полностью очистите массив. Если установите значение больше текущего, массив станет «дырявым» (sparse array), заполнившись пустыми слотами.

    Деструктуризация: извлечение данных с хирургической точностью

    Деструктуризация — это способ «разобрать» объект или массив на части прямо при объявлении переменных. Это избавляет код от бесконечных цепочек data.user.profile.name.

    В React-разработке деструктуризация используется повсеместно: при получении пропсов в компонентах или при работе с хуками. Она позволяет задавать значения по умолчанию прямо в процессе извлечения: const { role = 'guest' } = user;. Если свойства role нет в объекте, переменная примет значение 'guest', а не undefined.

    Эволюция перебора: от циклов к методам высшего порядка

    Традиционный цикл for (let i = 0; i < arr.length; i++) сегодня встречается во фронтенд-коде редко. Он императивен: вы диктуете компьютеру, как именно шагать по массиву. Современный JavaScript тяготеет к декларативности, где мы описываем, что хотим получить.

    Базовый перебор: for...of и for...in

    Для массивов идеален цикл for...of. Он перебирает именно значения, он лаконичен и поддерживает прерывание через break. Для объектов существует for...in, который перебирает ключи. Однако с ним нужно быть осторожным: он заглядывает в прототипы объекта, поэтому часто требует проверки hasOwnProperty или замены на связку Object.keys(obj).forEach(...).

    Большая тройка: map, filter, reduce

    Эти методы — фундамент функционального стиля в JS. Они не мутируют (не изменяют) исходный массив, а возвращают новый результат, что критично для предсказуемости кода.

  • map(callback): Трансформирует каждый элемент. Если у вас есть массив ID, а нужен массив объектов с этими ID — используйте map.
  • где — ваша функция-преобразователь.

  • filter(callback): Оставляет только те элементы, для которых функция вернула true.
  • Пример: отсеять из списка товаров те, которых нет в наличии.

  • reduce(callback, initialValue): Самый мощный и сложный метод. Он «схлопывает» массив в одно единственное значение (число, строку, объект или даже другой массив).
  • Здесь acc (аккумулятор) — это накопленный результат, а item — текущий элемент.

    Рассмотрим практический пример. У нас есть массив заказов, и нам нужно посчитать общую сумму только доставленных товаров.

    Цепочки методов (chaining) делают код читаемым как книгу. Мы буквально описываем этапы конвейера обработки данных.

    Поиск и проверка: find, includes, some, every

    Иногда нам не нужно менять массив, нужно лишь извлечь информацию: * find возвращает первый элемент, подходящий под условие. Если ничего не найдено — undefined. * findIndex — то же самое, но возвращает индекс (или -1). * some проверяет, есть ли хотя бы один элемент, подходящий под условие (возвращает true/false). * every проверяет, все ли элементы соответствуют условию.

    Нюанс: методы some и every эффективны. some прекратит работу, как только найдет первый подходящий элемент, а every — как только встретит первый неподходящий. Это называется «короткое замыкание» вычислений.

    Работа с объектами как с массивами

    Часто возникает задача применить методы массивов к объекту. Напрямую это сделать нельзя, но есть три статических метода класса Object, которые создают «мостик»:

  • Object.keys(myObj) — возвращает массив строк (ключей).
  • Object.values(myObj) — возвращает массив значений.
  • Object.entries(myObj) — возвращает массив массивов, где каждый вложенный массив — это пара [ключ, значение].
  • Последний метод особенно удобен для деструктуризации в цикле:

    Грань между мутацией и иммутабельностью

    Одна из самых частых ошибок начинающих — случайное изменение исходных данных. Методы push, pop, shift, unshift, splice и sort изменяют массив на месте. В современной разработке (особенно в React) принят принцип иммутабельности: вместо изменения старого объекта мы создаем новый.

    Почему это важно? Если вы измените свойство внутри объекта, ссылка на него в памяти останется прежней. Фреймворк, сравнив старую ссылку и новую, решит, что ничего не изменилось, и не обновит интерфейс.

    Вместо push используйте spread: const newArr = [...oldArr, newItem];. Вместо splice для удаления элемента используйте filter: const filtered = arr.filter(el => el.id !== targetId);.

    Глубокие структуры данных и опциональная цепочка

    В реальных приложениях объекты бывают многослойными. Попытка обратится к user.profile.address.city, если у пользователя нет профиля, приведет к ошибке TypeError: Cannot read property 'address' of undefined. Это «смерть» для приложения — белый экран.

    Раньше приходилось писать громоздкие проверки: user && user.profile && user.profile.address.... Сегодня нас спасает оператор опциональной цепочки ?..

    const city = user?.profile?.address?.city;

    Если на любом этапе цепочки встретится null или undefined, выполнение прервется и в city запишется undefined, но программа не упадет. В сочетании с оператором нулевого слияния ?? это позволяет изящно задавать запасные варианты:

    const city = user?.profile?.address?.city ?? 'Город не указан';

    Здесь ?? сработает только если слева null или undefined, в отличие от ||, который сработал бы и на пустую строку или ноль.

    Когда использовать объекты, а когда массивы?

    Выбор структуры данных определяет производительность и читаемость. * Массивы идеальны, если важен порядок элементов или если вы планируете выполнять массовые трансформации (фильтрацию, сортировку). Поиск элемента в массиве по значению — это операция , то есть время поиска растет линейно с ростом количества элементов. * Объекты (или их аналоги Map) идеальны для быстрого доступа по ключу. Достать значение из объекта по ключу — это операция , она выполняется мгновенно независимо от размера объекта.

    Если вы часто ищете пользователя в списке по его ID, возможно, стоит один раз преобразовать массив пользователей в объект, где ключами будут ID, чтобы в дальнейшем получать данные мгновенно.

    Манипуляция структурами данных — это искусство баланса между краткостью кода и его производительностью. Понимание того, как работают методы перебора «под капотом» и как JavaScript управляет ссылками в памяти, отделяет простого кодера от инженера, способного строить надежные интерфейсы.

    4. Манипуляция DOM-деревом и механизмы обработки пользовательских событий

    Манипуляция DOM-деревом и механизмы обработки пользовательских событий

    Когда вы открываете веб-страницу, браузер превращает сухой HTML-код в живую структуру, с которой можно взаимодействовать. Этот процесс напоминает превращение чертежа в реальное здание, где каждая стена, окно или дверь становятся доступными для изменений. Если HTML — это скелет, а CSS — кожа, то JavaScript через манипуляции с DOM становится нервной системой, позволяющей странице реагировать на действия пользователя. Почему нажатие кнопки «Купить» мгновенно обновляет счетчик в корзине без перезагрузки всей страницы? Ответ кроется в умении JavaScript «общаться» с объектной моделью документа.

    Анатомия DOM: интерфейс между кодом и визуалом

    Document Object Model (DOM) — это не просто копия HTML-кода. Это программный интерфейс, который представляет документ в виде древовидной структуры, где каждый элемент, атрибут и даже текстовый фрагмент является объектом. Важно понимать принципиальное различие: HTML — это текстовый формат передачи данных, а DOM — это живое объектное представление этих данных в оперативной памяти браузера.

    Корнем этого дерева является объект window.document. От него отходят ветви: элемент <html>, внутри которого располагаются <head> и <body>, и так далее до самых мелких тегов вроде <span> или <img>. Каждое звено в этой цепи называется узлом (node). Существует несколько типов узлов, но во фронтенд-разработке мы чаще всего работаем с тремя:

  • Узлы-элементы (Element nodes) — сами HTML-теги.
  • Текстовые узлы (Text nodes) — текст внутри тегов.
  • Узлы-комментарии — то, что скрыто в <!-- -->.
  • Особенность DOM заключается в том, что он динамичен. Мы можем не только считывать данные из него, но и на лету перестраивать структуру: удалять старые блоки, добавлять новые и менять стили.

    Навигация и поиск: как найти иголку в стоге сена

    Прежде чем изменить цвет заголовка или скрыть рекламный баннер, JavaScript должен «найти» нужный узел. В современном стандарте разработки для этого используются два основных метода, которые поддерживают синтаксис CSS-селекторов:

    * querySelector(selector) — возвращает первый найденный элемент, соответствующий селектору. Если совпадений нет, вернет null. * querySelectorAll(selector) — возвращает статичную коллекцию NodeList, содержащую все найденные элементы.

    Использование CSS-селекторов делает поиск интуитивно понятным. Например, чтобы найти кнопку с классом .submit-btn, находящуюся внутри формы с идентификатором #login-form, мы напишем: const btn = document.querySelector('#login-form .submit-btn');

    Существует и «старая школа» методов: getElementById, getElementsByClassName и getElementsByTagName. Однако они менее гибкие и возвращают «живые» коллекции (HTMLCollection), которые автоматически обновляются при изменении DOM. В большинстве сценариев при разработке на React или Vue вы будете сталкиваться с логикой, близкой к querySelector, поэтому акцент стоит делать именно на нем.

    Навигация по связям

    Иногда нам не нужно искать элемент по всему документу, достаточно перейти к нему от уже известного узла. Для этого используются свойства навигации: * parentNode — переход к родителю. * children — коллекция дочерних узлов-элементов. * nextElementSibling и previousElementSibling — переход к «соседям» по уровню.

    Важно помнить, что свойства вроде childNodes или firstChild включают в себя текстовые узлы (в том числе пробелы и переносы строк в коде), что часто приводит к ошибкам. Поэтому в 99% случаев профессионалы используют версию свойств с приставкой Element (например, firstElementChild), чтобы работать только с тегами.

    Изменение содержимого и атрибутов

    Найдя нужный элемент, мы получаем доступ к его внутренностям. Здесь кроется одна из самых частых ловушек для новичков — выбор между innerHTML и textContent.

  • textContent — возвращает или устанавливает чистый текст внутри элемента, игнорируя HTML-разметку. Это безопасный метод, так как он не интерпретирует строку как код.
  • innerHTML — позволяет получать и вставлять полноценный HTML-код.
  • > Важное правило безопасности: Никогда не используйте innerHTML для вставки данных, полученных от пользователя (например, из полей ввода). Это открывает уязвимость для XSS-атак (Cross-Site Scripting), когда злоумышленник может внедрить вредоносный скрипт <script>stealCookies()</script> прямо в вашу страницу. Для простого текста всегда выбирайте textContent.

    Помимо содержимого, мы управляем атрибутами. У объектов DOM большинство стандартных атрибутов (id, src, value, href) доступны как обычные свойства. Но для работы с кастомными атрибутами или специфическими случаями используются методы: * getAttribute(name) * setAttribute(name, value) * removeAttribute(name) * hasAttribute(name)

    Особое место занимает атрибут class. Поскольку слово class зарезервировано в JavaScript для создания классов объектов, доступ к классам элемента осуществляется через свойство className (строка) или, что предпочтительнее, через объект classList. Объект classList предоставляет удобные методы: add(), remove(), toggle() (переключить) и contains() (проверить наличие). Это основа для создания анимаций: нажали на кнопку — добавили класс .is-active, CSS подхватил это и запустил анимацию.

    Создание и удаление элементов

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

  • Создание: const comment = document.createElement('div');
  • Наполнение: comment.classList.add('comment-item'); comment.textContent = 'Новый текст';
  • Монтирование в дерево: Чтобы элемент появился на экране, его нужно прикрепить к родителю.
  • Для вставки элементов современный стандарт предлагает метод append() (добавляет в конец) и prepend() (в начало). Также существует универсальный метод before() и after() для вставки относительно соседа.

    Пример создания структуры:

    Удаление элемента производится методом remove(). Это гораздо проще, чем в старых версиях языка, где приходилось обращаться к родителю и вызывать removeChild.

    События: как JavaScript узнает о действиях пользователя

    Событие — это сигнал от браузера о том, что что-то произошло: клик мышкой, нажатие клавиши, окончание загрузки картинки или прокрутка страницы. Обработка событий превращает статичную страницу в интерактивное приложение.

    Назначение обработчиков

    Существует три способа «подписаться» на событие, но в современной разработке признан только один — addEventListener.

    Почему addEventListener лучше других?

  • Он позволяет вешать несколько разных функций на одно и то же событие.
  • Он дает возможность тонкой настройки (через третий аргумент — объект опций).
  • Он позволяет легко удалять обработчик через removeEventListener.
  • Объект события (event)

    Когда срабатывает обработчик, браузер автоматически передает в функцию первым аргументом объект события. В нем содержится масса полезной информации: * event.type — тип события (например, 'click'). * event.target — ссылка на элемент, на котором произошло событие (например, конкретная кнопка, на которую нажали). * event.clientX / event.clientY — координаты клика. * event.key — какая клавиша была нажата (для событий клавиатуры).

    Жизненный цикл события: погружение и всплытие

    Это одна из самых важных и сложных тем для понимания в DOM. Когда вы кликаете по кнопке, которая находится внутри <div>, который в свою очередь находится в <body>, событие не просто возникает на кнопке. Оно проходит три фазы:

  • Фаза погружения (Capture phase): Событие идет от window вниз к цели через всех предков.
  • Фаза цели (Target phase): Событие достигло элемента, на который кликнули.
  • Фаза всплытия (Bubbling phase): Событие начинает подниматься вверх от цели к window.
  • Почти все события в JavaScript всплывают. Это означает, что если вы кликнули по ссылке внутри меню, обработчик клика сработает сначала на ссылке, потом на элементе списка <li>, потом на самом меню <ul> и так далее до самого верха.

    Остановка всплытия

    Иногда всплытие мешает. Например, у вас есть модальное окно, и вы хотите, чтобы оно закрывалось при клике на «подложку» (фон), но не закрывалось при клике на само окно. В этом случае внутри обработчика клика по окну нужно вызвать: event.stopPropagation(); Это приказывает событию: «Остановись здесь, не поднимайся к родителям».

    Делегирование событий: оптимизация производительности

    Понимание механизма всплытия позволяет использовать мощный паттерн — делегирование. Представьте таблицу из 1000 строк, в каждой из которых есть кнопка «Удалить». Если мы повесим 1000 обработчиков через addEventListener, это ударит по памяти и производительности.

    Вместо этого мы вешаем один обработчик на родительский элемент (таблицу). Благодаря всплытию, любой клик по кнопке внутри таблицы «дойдет» до родителя. Внутри родительского обработчика мы просто проверяем, откуда пришло событие:

    Метод closest(selector) — это незаменимый помощник при делегировании. Он ищет ближайшего родителя (или сам элемент), подходящего под селектор. Это позволяет не бояться, что пользователь попал не по самой кнопке, а по иконке внутри неё.

    Стандартное поведение браузера

    Многие события влекут за собой действия браузера по умолчанию: * Клик по ссылке инициирует переход на другой URL. * Нажатие Enter в форме отправляет данные на сервер и перезагружает страницу. * Правый клик вызывает контекстное меню.

    В современных Single Page Applications (SPA) мы часто хотим отменить это поведение, чтобы обработать действие через JavaScript. Для этого используется метод: event.preventDefault();

    Например, при работе с формой в React или Vue вы почти всегда увидите конструкцию:

    Производительность и перерисовки (Reflow/Repaint)

    Манипуляции с DOM — это самая «дорогая» операция в браузере. Каждый раз, когда вы меняете размеры элемента, его положение или добавляете новый узел, браузер вынужден пересчитывать геометрию всей страницы (Reflow) и заново отрисовывать пиксели (Repaint).

    Чтобы приложение работало плавно (60 кадров в секунду), следует придерживаться нескольких правил:

  • Группируйте изменения. Вместо того чтобы в цикле 100 раз добавлять li в список, создайте их в памяти, соберите в специальный объект document.createDocumentFragment() (невидимый контейнер) и вставьте его в DOM один раз.
  • Меняйте классы, а не стили напрямую. Изменение element.style.width заставляет браузер нервничать. Смена одного класса, в котором описано 10 изменений стилей, оптимизируется браузером лучше.
  • Помните о «тяжелых» свойствах. Обращение к свойствам, требующим расчета координат (например, offsetHeight или getBoundingClientRect()), заставляет браузер немедленно выполнить Reflow, чтобы дать вам актуальное значение. Если делать это внутри цикла, страница начнет «тормозить».
  • Работа с DOM и событиями — это фундамент. Даже если в будущем вы будете использовать фреймворки, которые берут на себя управление деревом (как React с его Virtual DOM), понимание того, как события всплывают и как работают атрибуты, останется вашим главным инструментом в отладке и создании сложных интерфейсов.

    5. Асинхронность в JavaScript: Promise, async/await и подготовка к компонентному подходу

    Асинхронность в JavaScript: Promise, async/await и подготовка к компонентному подходу

    Почему, когда вы нажимаете кнопку «Загрузить ленту» в социальной сети, интерфейс не замирает в ожидании ответа сервера, а позволяет вам продолжать скроллить или открывать уведомления? В мире синхронного программирования выполнение кода напоминает очередь в кассу: пока первый покупатель не расплатится, второй не может даже выложить продукты на ленту. Если бы JavaScript работал только так, любой сетевой запрос длительностью в две секунды превращал бы веб-страницу в «кирпич». Решение кроется в асинхронности — способности языка инициировать длительную операцию и продолжать выполнение других задач, не дожидаясь финала.

    Природа однопоточности и событийный цикл

    JavaScript по своей сути является однопоточным языком. Это означает, что у него есть только один стек вызовов (Call Stack), и он может выполнять только одну инструкцию в конкретный момент времени. Однако браузер, в котором живет JS, — это сложная многопоточная система. Когда мы просим JavaScript отправить запрос на сервер или установить таймер, выполнение этой задачи делегируется внешним API браузера (Web APIs).

    Механизм, который связывает однопоточный JS и многопоточные возможности браузера, называется Event Loop (событийный цикл). Его работа проста, но гениальна: он постоянно проверяет, пуст ли стек вызовов. Если стек пуст, Event Loop берет первую задачу из очереди задач (Callback Queue) и отправляет её на выполнение.

    Представьте ситуацию: вы вызываете setTimeout(() => console.log("Прошло 0 секунд"), 0). Несмотря на нулевую задержку, это сообщение появится в консоли после всего синхронного кода. Почему? Потому что setTimeout — это Web API. Браузер ставит таймер, по истечении которого коллбэк попадает в очередь. Но Event Loop не пустит его в стек, пока там выполняется основной сценарий. Это понимание критически важно для работы с данными: асинхронный код всегда выполняется «позже», даже если это «позже» измеряется миллисекундами.

    Эволюция подходов: от коллбэков к промисам

    На заре JavaScript единственным способом обработки асинхронности были функции обратного вызова (callbacks). Вы передавали функцию в качестве аргумента, и она вызывалась, когда операция завершалась. Проблема возникала, когда одна асинхронная операция зависела от другой, та от третьей, и так далее. Это приводило к «адскому кольцу коллбэков» (Callback Hell) — коду, который растет вправо быстрее, чем вниз, и становится практически нечитаемым.

    На смену им пришли промисы (Promises). Промис — это объект, представляющий результат асинхронной операции, который может быть доступен сейчас, в будущем или никогда.

    Промис всегда находится в одном из трех состояний:

  • Pending (ожидание) — начальное состояние.
  • Fulfilled (исполнено) — операция завершена успешно.
  • Rejected (отклонено) — произошла ошибка.
  • Синтаксически создание промиса выглядит так:

    Здесь resolve и reject — это функции-коллбэки, которые предоставляет сам движок JS. Вызов resolve(value) переводит промис в состояние успеха, а reject(error) — в состояние ошибки. Главное преимущество промисов — возможность строить цепочки (chaining) с помощью методов .then(), .catch() и .finally(). Каждый вызов .then() возвращает новый промис, что позволяет избежать вложенности.

    Fetch API: реальное взаимодействие с сервером

    Для современного фронтенд-разработчика основным источником промисов является функция fetch(). Она пришла на смену устаревшему XMLHttpRequest и предоставляет лаконичный интерфейс для сетевых запросов.

    Рассмотрим типичный кейс: получение списка пользователей.

    Этот императивный код закладывает фундамент для декларативного программирования. В React вы не будете писать .innerHTML, вы просто скажете: «Если isLoading истинно, покажи Spinner, иначе покажи List». Но под капотом все равно будет работать тот же async/await.

    Микрозадачи и макрозадачи: тонкий нюанс

    Для глубокого понимания JS важно знать, что не все асинхронные задачи равны. Существует две очереди:

  • Микрозадачи (Microtasks): сюда попадают коллбэки промисов (.then, await).
  • Макрозадачи (Macrotasks): сюда попадают setTimeout, setInterval, события кликов.
  • Event Loop работает по правилу: после выполнения каждой макрозадачи (или основного скрипта) он обязан выполнить все накопившиеся микрозадачи, прежде чем переходить к следующей макрозадаче или отрисовке страницы (Repaint).

    Что это значит на практике? Промисы имеют приоритет над таймерами. Если вы одновременно запустите setTimeout и Promise.resolve().then(), сначала выполнится код из промиса. Это позволяет браузеру быстрее обрабатывать логику данных, не отвлекаясь на менее приоритетные задачи рендеринга или таймеров.

    Замыкание мысли

    Асинхронность — это не просто «способ не тормозить сайт». Это философия современного веба, где клиент (браузер) является активным участником обмена данными, а не пассивным зрителем. Переход от коллбэков к async/await сделал код чище, но не отменил необходимости понимать, как работает событийный цикл.

    Овладение промисами открывает дверь к архитектуре современных приложений. Когда вы начнете работать с React, вы увидите, что вся работа с API, жизненными циклами компонентов и эффектами пронизана асинхронностью. Умение правильно обрабатывать состояния ожидания и ошибок, параллельно запускать запросы и понимать приоритеты выполнения задач — это те навыки, которые отличают любителя от профессионального фронтенд-разработчика.