Глава 1. Введение в JavaScript

📜 История создания JavaScript

JavaScript был создан в 1995 году программистом Бренданом Айком (Brendan Eich) всего за 10 дней, когда он работал в компании Netscape Communications. Изначально язык назывался Mocha, затем был переименован в LiveScript, и наконец получил название JavaScript в рамках маркетинговой стратегии, чтобы воспользоваться популярностью Java (хотя это совершенно разные языки).

💡 Цель создания

JavaScript был разработан для того, чтобы сделать веб-страницы интерактивными и динамичными. До его появления HTML-страницы были статичными и не могли реагировать на действия пользователя без перезагрузки.

Основные этапы развития:

  • 1995 — Создание Бренданом Айком
  • 1997 — Стандартизация ECMA (ECMAScript)
  • 2009 — ES5: добавлены JSON, strict mode
  • 2015 — ES6 (ES2015): революция (let/const, классы, стрелочные функции, промисы)
  • 2016-2024 — Ежегодные обновления (async/await, optional chaining, и др.)

📜 Что такое JavaScript?

JavaScript (JS) — интерпретируемый язык программирования, выполняющийся в браузере. Позволяет создавать интерактивные веб-страницы.

💡 JavaScript используется для:
  • Изменения содержимого и стилей страницы
  • Реакции на действия пользователя (клики, ввод)
  • Отправки запросов на сервер (AJAX)
  • Создания анимаций и интерактивных элементов
  • Валидации форм

📜 Первая программа

Для начала определим для нашего приложения какой-нибудь каталог. Например, создадим на диске C: папку app. В этой папке создадим файл под названием index.html. То есть данный файл будет представлять веб-страницу с кодом HTML.

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <script> document.write("<h2>Первая программа на JavaScript</h2>"); </script> </body> </html>

Здесь мы определяем стандартные элементы html. В элементе head определяется кодировка utf-8 и заголовок (элемент title). В элементе body определяется тело веб-страницы, которое в данном случае состоит только из одного элемента <script>

Подключение кода javascript на html-страницу осуществляется с помощью тега <script>. Данный тег следует размещать либо в заголовке (между тегами <head> и </head>), либо в теле веб-странице (между тегами <body> и </body>). Нередко подключение скриптов происходит перед закрывающим тегом </body> для оптимизации загрузки веб-страницы.

Раньше надо было в теге <script> указывать тип скрипта, так как данный тег может использоваться не только для подключения инструкций javascript, но и для других целей. Так, даже сейчас вы можете встретить на некоторых веб-страницах такое определение элемента script:

<script type="text/javascript">

Но в настоящее время предпочтительнее опускать атрибут type, так как браузеры по умолчанию считают, что элемент script содержит инструкции javascript.

Используемый нами код javascript содержит одно выражение:

document.write("<h2>Первая программа на JavaScript</h2>");

Код javascript может содержать множество инструкций и каждая инструкция завершается точкой с запятой. Наша инструкция вызывает метод document.write(), который выводит на веб-страницу некоторое содержимое, в данном случае это заголовок <h2>Первая программа на JavaScript</h2>.

// Вывод в консоль console.log('Hello, World!'); // Окно с сообщением alert('Привет, JavaScript!'); // Запрос у пользователя let name = prompt('Как тебя зовут?'); console.log('Привет, ' + name);

📜 Выполнение кода javascript

Когда браузер получает веб-страницу с кодом html и javascript, то он ее интерпретирует. Результат интерпретации в виде различных элементов - кнопок, полей ввода, текстовых блоков и т.д., мы видим перед собой в браузере. Интерпретация веб-страницы происходит последовательно сверху вниз.

Когда браузер встречает на веб-странице элемент <script> с кодом javascript, то вступает в действие встроенный интерпретатор javascript. И пока он не закончит свою работу, дальше интерпретация веб-страницы не идет.

Рассмотрим небольшой пример и для этого определим файл index.html со следующим кодом:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> <script> document.write("Начальный текст"); </script> </head> <body> <h2>Первый заголовок</h2> <script> document.write("Первый текст"); </script> <h2>Второй заголовок</h2> <script> document.write("Второй текст"); </script> </body> </html>

Здесь три вставки кода javascript - один в секции <head> и по одному после каждого заголовка.

Браузер последовательно выполняет код веб-страницы:

                    
                        
                    
                

После этого браузер закончит интерпретацию веб-страницы, и веб-страница окажется полностью загружена. Данный момент очень важен, поскольку может влиять на производительность. Поэтому нередко вставки кода javascript идут перед закрывающим тегом </body>, когда основная часть веб-страницы уже загружена в браузере.

📜 Основы синтаксиса javascript

Код javascript состоит из инструкций. Каждая инструкция представляет некоторое действие. И для отделения инструкций друг от друга в javascript после инструкции ставится точка с запятой:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <script> document.write("2 + 5 = "); const sum = 2 + 5; document.write(sum); </script> </body> </html>

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

<body> <script> document.write("2 + 5 = ") const sum = 2 + 5 document.write(sum) </script> </body>

Но чтобы улучшить читабельность кода и снизить число возможных ошибок, рекомендуется определять каждую инструкцию javascript на отдельной строчке и завершать ее точкой с запятой, как в первом варианте.

Блоки кода

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

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <script> document.write("Привет! "); { document.write("Как дела? "); document.write("Че не отвечаешь, ты че спишь? "); } document.write("Ну пока..."); </script> </body> </html>

Здесь программа на JavaScript состоит из 4-х инструкций. Причем две из инструкций помещены в блок кода:

{ document.write("Как дела? "); document.write("Че не отвечаешь, ты че спишь? "); }

Комментарии

В коде javascript могут использоваться комментарии. Комментарии не обрабатываются интерпретатором javascript и никак не учитываются в работе программы. Они предназначены для ориентации по коду, чтобы указать, что делает тот или иной код.

Комментарии могут быть однострочными, для которых используется двойной слеш:

<body> <script> // выводим сообщение document.write("2 + 5 = ") // объявляем константу и присваиваем ей сумму чисел 2 и 5 const sum = 2 + 5 // выводим значение константы sum document.write(sum) </script> </body>

Кроме однострочных комментариев могут использоваться и многострочные. Такие комментарии заключаются между символами /*текст комментария*/. Например:

<body> <script> /* Пример арифметической операции и определения константы в коде JavaScript */ document.write("2 + 5 = ") const sum = 2 + 5 document.write(sum) </script> </body>

📜 Способы подключения JavaScript

1. Внутри HTML (inline)

<script> alert('Привет!'); </script>

2. Внешний файл (рекомендуется)

<!-- В конце body --> <script src="script.js"></script> <!-- С defer (выполнится после загрузки HTML) --> <script src="script.js" defer></script> <!-- С async (параллельная загрузка) --> <script src="script.js" async></script>

Асинхронная загрузка и отложенное выполнение

Нередко веб-страницы имеют сложную структуру, какие-то блоки на html-странице, где подключаются файлы javascript, формируются динамически, что может усложнять управление файлами javascript. И для управления загрузкой файла с кодом JavaScript браузер предоставляет два атрибута: async и defer.

Атрибут async гарантирует, что обработка HTML-кода не будет приостановлена, когда браузер встретит элемент <script>. Файл JavaScript загружается асинхронно (отсюда и название атрибута - async). В этом случае HTML-код продолжает обрабатываться до тех пор, пока не будет загружен соответствующий файл JavaScript. Когда будет загружен файл JavaScript, обработка HTML останавливается, и начинает выполняться загруженный файл JS. После выполнения кода JavaScript продолжается обработка HTML.

Пример применения атрибута async:

<script async src="js/main.js"></script>

Атрибут defer также гарантирует, что обработка HTML-кода не будет приостановлена. С другой стороны, исходный код JavaScript выполняется только после полной обработки HTML-кода. Таким образом, выполнение кода JavaScript откладывается (отсюда и название - defer (в переводе на английский).

Пример применения атрибута defer:

<script defer src="js/main.js"></script>

📜 Консоль браузера

Консоль — главный инструмент разработчика для отладки.

✅ Как открыть консоль:
  • Windows: F12 или Ctrl+Shift+I
  • Mac: ⌘+⌥+I
  • Или: ПКМ на странице → "Inspect" → вкладка "Console"

Мы можем напрямую вводить в консоль браузера выражения JavaScript, и они будут выполняться. Например, введем в консоли следующий текст:

alert("Привет мир");

Методы console

// Обычное сообщение console.log('Сообщение'); // Ошибка (красным) console.error('Ошибка!'); // Предупреждение (желтым) console.warn('Внимание!'); // Таблица console.table([ {name: 'Alice', age: 25}, {name: 'Bob', age: 30} ]); // Замер времени console.time('myTimer'); // ... код ... console.timeEnd('myTimer'); // "myTimer: 123ms"

Также последовательно вводим инструкции и после ввода каждой инструкции нажимаем на Enter.

Если нам надо, чтобы код в консоли переносился на новую строку без выполнения, то в конце выражения javascript нажимаем на комбинацию клавиш Shift + Enter. После ввода последней инструкции для выполнения введенного кода javascript нажимаем на Enter.

Глава 2. Основы JavaScript

📜 Переменные и константы

Для хранения данных в программе используются переменные и константы. Переменные предназначены для хранения каких-нибудь временных данных или таких данных, которые в процессе работы могут менять свое значение. Константы, наоборот, предназначены для хранения неизменных данных, значение которых должно оставаться постоянным в течение всей программы.

// let - изменяемая переменная (ES6+) let age = 25; age = 26; // OK // const - константа (нельзя изменить) const PI = 3.14159; // PI = 3; // Ошибка! // var - старый способ (не рекомендуется) var name = 'Alice';
⚠️ Правила именования переменных:
  • Начинаются с буквы, $ или _
  • Не могут начинаться с цифры
  • Чувствительны к регистру (age ≠ Age)
  • Не используйте зарезервированные слова (let, const, if, for...)
⚠️ Список зарезервированных слов в JavaScript:

await, break, case, catch, class, const, continue, debugger, default, delete, do, else, enum, export, extends, false, finally, for, function, if, import, in, instanceof, new, null, return, super, switch, this, throw, true, try, typeof, var, void, while, with, yield

Через запятую можно определить сразу несколько переменных:

var username, age, height; let a, b, c;

Присвоение переменной значения

После определения переменной ей можно присвоить какое-либо значение. Для этого применяется оператор присваивания (=):

var username; username = "Tom";

Можно сразу присвоить переменной значение при ее определении:

var username = "Tom"; let userage = 37;

Процесс присвоения переменной начального значения называется инициализацией.Можно обратить внимание, что одна переменная определена с помощью let, а другая - с помощью var. Конкретно в данном случае это не имеет значения, и мы могли бы определить обе переменных либо с помощью let, либо с помощью var. Но в целом между let и var, есть различия, которые будут рассмотрены следующих статьях.

Можно сразу инициализировать несколько переменных:

let name1 = "Tom", name2 = "Bob", name3 = "Sam"; console.log(name1); // Tom console.log(name2); // Bob console.log(name3); // Sam

JavaScript позволяет НЕ использовать ключевые слова let или var. Но, лучше так не делать.

<script> username = "Tom"; // ошибки нет, норм console.log(username); // Tom </script>

Изменение переменных

Отличительной чертой переменных является то, что мы можем изменить их значение:

<script> let username = "Tom"; console.log("username до изменения:", username); username = "Bob"; console.log("username после изменения:", username); </script>

Константы

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

const username = "Tom";

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

const username; // ошибка, username не инициализирована

📜 Типы данных в JavaScript

В JavaScript существует 8 типов данных: 7 примитивных и 1 сложный (Object).

  • String: представляет строку
  • Number: представляет числовое значение
  • BigInt: предназначен для представления очень больших целых чисел
  • Boolean: представляет логическое значение true или false
  • Undefined: представляет одно специальное значение - undefined и указывает, что значение не установлено
  • Null: представляет одно специальное значение - null и указывает на отсутствие значения
  • Symbol: представляет уникальное значение, которое часто применяется для обращения к свойствам сложных объектов
  • Object: представляет комплексный объект

Примитивные типы (7 типов)

// 1. Number (числа) let integer = 42; let float = 3.14; let negative = -10; // 2. String (строки) let str1 = "Hello"; let str2 = 'World'; let str3 = `Hello ${name}`; // template literal (ES6) // 3. Boolean (логический) let isTrue = true; let isFalse = false; // 4. Undefined (неопределённое значение) let notDefined; console.log(notDefined); // undefined // 5. Null (намеренное отсутствие значения) let empty = null; // 6. Symbol (уникальный идентификатор) - ES6 let id = Symbol('id'); // 7. BigInt (большие целые числа) - ES2020 let bigNumber = 1234567890123456789012345678901234567890n;

8-й тип: Object (Объекты)

Тип Object — это сложный тип данных, который включает в себя три основных подтипа:

// 1. Object (объект) - коллекция пар ключ-значение let user = { name: 'Alice', age: 25, isAdmin: false }; // 2. Array (массив) - упорядоченная коллекция элементов let numbers = [1, 2, 3, 4, 5]; let mixed = [1, 'text', true, null]; // 3. Date (дата и время) - работа с датами let now = new Date(); let specificDate = new Date('2024-01-01');
💡 Важно понимать
  • Array и Date технически являются объектами, но имеют специальное поведение
  • typeof [] вернёт "object"
  • typeof new Date() вернёт "object"
  • Для проверки массива используйте: Array.isArray([]) → true
  • Для проверки даты: now instanceof Date → true

Проверка типа

console.log(typeof 42); // "number" console.log(typeof "Hello"); // "string" console.log(typeof true); // "boolean" console.log(typeof undefined); // "undefined" console.log(typeof null); // "object" (баг JS!) console.log(typeof []); // "object" console.log(typeof {}); // "object" console.log(typeof new Date());// "object" // Правильная проверка массива console.log(Array.isArray([])); // true // Правильная проверка даты let now = new Date(); console.log(now instanceof Date); // true

Number

Тип Number представляет числа в JavaScript, которые могут быть целыми или дробными:

  • Целые числа, например, 35. Мы можем использовать как положительные, так и отрицательные числа. Диапазон используемых чисел: от -253 до 253
  • Дробные числа (числа с плавающей точкой). В качестве разделителя дробной и целой части применяется точка, например, 3.5575. Можно использовать как положительные, так и отрицательные числа. Для чисел с плавающей точкой используется тот же диапазон: от -253 до 253

JavaScript поддерживает возможность определять числа в двоичной, восьмеричной и шестнадцатеричной системах.

// Для определения числа в двоичной системе, перед числом указывается префикс 0b: const num1 = 0b1011; // число 11 в двоичной системе console.log(num1); // 11 // Для определения числа в восьмеричной системе, перед числом указывается префикс 0o: const num1 = 0o11; // число 9 в восьмеричной системе console.log(num1); // 9 // Для определения числа в шестнадцатеричной системе, перед числом указывается префикс 0x: const num1 = 0xff; // число 255 в шестнадцатеричной системе console.log(num1); // 255 const num2 = 0x1A; // число 26 в шестнадцатеричной системе console.log(num2); // 26

Начиная со стандарта ECMA2021 в JavaScript для увеличения читабельности в качестве разделителя между разрядами можно использовать символ подчеркивания _:

const num1 = 1234567; const num2 = 123_4567; // число равное num1 const num3 = 1234567890; const num4 = 12_3456_7890; // число равное num3

Тип BigInt

Тип BigInt добавлен в последних стандартах JavaScript для представления очень больших целых чисел, которые выходят за пределы диапазона типа number. Это не значит, что мы не можем совсем работать с большими числами с помощью типа number, но работа с ними в случае с типом number будет сопряжена с проблемами. Рассмотрим небольшой пример:

let num = 9007199254740991 console.log(num); // 9007199254740991 console.log(num + 1); // 9007199254740992 console.log(num + 2); // 9007199254740992

Здесь переменной num присваивается максимальное значение. И далее прибавляем к ней некоторые значения и выводим на консоль результат. И результаты могут нас смутить, особенно в случае прибавления числа 2.

Стоит отметить, что тип Number ограничен, хотя и позволяет оперировать довольно большим диапазоном чисел. В частности, мы можем использовать специальные константы Number.MIN_VALUE и Number.MAX_VALUE для проверки минимального и максимального возможных значений для типа Number:

console.log(Number.MIN_VALUE); // 5e-324 console.log(Number.MAX_VALUE); // 1.7976931348623157e+308

Например, рассмотрим следующий пример:

const num = 9223372036854775801; console.log(num); // 9223372036854776000

В силу ограничений типа Number на консоли мы увидим несколько другое число, нежели мы присвоили константе num. Это может негативно влиять на точность в вычислениях. И для подобных подобных чисел как раз предназначен тип BigInt. Для определения числа как значения типа BigInt в конце числа добавляется суффикс n:

let dimension = 19007n; const value = 2545n;

Например, изменим из предыдущего примера тип number на bigint:

const num = 9223372036854775801n; console.log(num); // 9223372036854775801n

Тип Boolean

Тип Boolean представляет булевые или логические значения true (верно) и false (ложно):

const isAlive = true; const isDead = false;

Строки String

Тип String представляет строки. Для определения строк применяются кавычки, причем, можно использовать как двойные, так одинарные, так и косые кавычки. Единственно ограничение: тип закрывающей кавычки должен быть тот же, что и тип открывающей, то есть либо обе двойные, либо обе одинарные.

const user = "Tom"; const company = 'Microsoft'; const language = `JavaScript`; console.log(user); console.log(company); console.log(language);

Если внутри строки встречаются кавычки, то мы их должны экранировать слешем. Например, пусть у нас есть текст "Бюро "Рога и копыта"". Теперь экранируем кавычки:

const company = "Бюро \"Рога и копыта\"";

Также мы можем внутри стоки использовать другой тип кавычек:

const company1 = "Бюро 'Рога и копыта'"; const company2 = 'Бюро "Рога и копыта"';

Также строка может содержать специальные символы - управляющие последовательности, которые интерпретируются определенным образом. Самые распространенные последовательности - это "\n" (перевод на другую строку) и "\t" (табуляция). Например:

const text = "Hello METANIT.COM\nHello\tWorld"; console.log(text);
Hello METANIT.COM
Hello	World

Интерполяция

Использование косых кавычек позволяет нам применять такой прием как интерполяция - встраивать данные в строку. Например:

const user = "Tom"; const text = `Name: ${user}`; console.log(text); // Name: Tom

Для встраивания значений выражений (например, значений других переменных и констант) в строку перед выражением ставится знак доллара $, после которого в фигурных скобках указывается выражение. Так, в примере выше ${user} означает, что в этом месте строки надо встроить значение переменной user.

Подобным образом можно встраивать и больше количество данных:

const user = "Tom"; const age = 37; const isMarried = false; const text = `Name: ${user} Age: ${age} IsMarried: ${isMarried}`; console.log(text); // Name: Tom Age: 37 IsMarried: false

Кроме интерполяции косые кавычки позволяют определять многострочный текст:

const text = `Мы все учились понемногу Чему-нибудь и как-нибудь, Так воспитаньем, слава богу, У нас немудрено блеснуть.`; console.log(text);

Консольный вывод браузера:

Мы все учились понемногу
Чему-нибудь и как-нибудь,
Так воспитаньем, слава богу,
У нас немудрено блеснуть.

null и undefined

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

let email; console.log(email); // выведет undefined

Присвоение значение null означает, что у переменной отсутствует значение:

let email; console.log(email); // undefined email = null; console.log(email); // null

Стоит отметить, что хотя в принципе можно переменной присвоить значение undefined, как в следующем случае:

let email = "tome@mimimail.com"; email = undefined; // установим тип undefined console.log(email); // undefined

Но основной смысл undefined состоит в том, что переменная неинициализирована, что обычно происходит до первого присвоения ей какого-либо значения. Поэтому обычно не предполагается, что переменной явным образом будет присваиваться значение undefined. В тоже время этот тип может быть полезен - мы можем использовать его на проверку инициализации переменной. Но если же нам надо указать, что у переменной нет никакого значения, то ей присваивается null, а не undefine.

object

Тип object представляет сложный объект. Простейшее определение объекта представляют фигурные скобки:

const user = {};

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

const user = {name: "Tom", age:24}; console.log(user.name);

В данном случае объект называется user, и он имеет два свойства: name и age, которые в качестве значения принимают данные других типов.

Слабая/динамическая типизация

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

let id; // тип undefined console.log(id); id = 45; // тип number console.log(id); id = "45"; // тип string console.log(id);

Несмотря на то, что во втором и третьем случае консоль выведет нам число 45, но во втором случае переменная id будет представлять число, а в третьем случае - строку.

Оператор typeof

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

let id; console.log(typeof id); // undefined id = 45; console.log(typeof id); // number id = 45n; console.log(typeof id); // bigint id = "45"; console.log(typeof id); // string

Стоит отметить, что для значения null оператор typeof возвращает значение "object", несмотря на то, что согласно спецификации JavaScript значение null представляет отдельный тип.

📜 Арифметические операции

let a = 10; let b = 3; console.log(a + b); // 13 (сложение) console.log(a - b); // 7 (вычитание) console.log(a * b); // 30 (умножение) console.log(a / b); // 3.333... (деление) console.log(a % b); // 1 (остаток от деления) console.log(a ** b); // 1000 (возведение в степень) // Инкремент/декремент let x = 5; x++; // x = 6 (постфиксный инкремент) ++x; // x = 7 (префиксный инкремент) x--; // x = 6 (декремент)

Оператор инкремента ++ увеличивает значение переменной на единицу. Существует префиксный инкремент, который сначала увеличивает переменную на единицу, а затем возвращает ее значение. И есть постфиксный инкремент, который сначала возвращает значение переменной, которое было до увеличения его на единицу:

// префиксный инкремент let x = 5; let z = ++x; console.log(x); // 6 console.log(z); // 6 // постфиксный инкремент let a = 5; let b = a++; console.log(a); // 6 console.log(b); // 5

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

Арифметические операции с присваиванием

  • +=

    Сложение с последующим присвоением результата. Например:

    let a = 23; a += 5; // аналогично a = a + 5 console.log(a); // 28
  • -=

    Вычитание с последующим присвоением результата. Например:

    let a = 28; a -= 10; // аналогично a = a - 10 console.log(a); // 18
  • *=

    Умножение с последующим присвоением результата:

    let x = 20; x *= 2; // аналогично x = x * 2 console.log(x); // 40
  • **=

    Возведение в степень с последующим присвоением результата:

    let x = 5; x **= 2; console.log(x); // 25
  • /=

    Деление с последующим присвоением результата:

    let x = 40; x /= 4; // аналогично x = x / 4 console.log(x); // 10
  • %=

    Получение остатка от деления с последующим присвоением результата:

    let x = 10; x %= 3; // аналогично x = x % 3 console.log(x); // 1, так как 10 - 3*3 = 1

📜 Поразрядные операции

Поразрядные операции выполняются над отдельными разрядами или битами чисел. Данные операции производятся только над целыми числами. Но сначала вкратце рассмотрим, что представляют собой разряды чисел.

Двоичное представление чисел

На уровне компьютера все данные представлены в виде набора бит. Каждый бит может иметь два значения: 1 (есть сигнал) и 0 (нет сигнала). И все данные фактически представляют набор нулей и единиц. 8 бит представляют 1 байт. Подобную систему называют двоичной.

Например, число 13 в двоичной системе будет равно 11012. Как мы это получили:

// перевод десятичного числа 13 в двоичную систему 13 / 2 = 6 // остаток 1 (13 - 6 *2 = 1) 6 / 2 = 3 // остаток 0 (6 - 3 *2 = 0) 3 / 2 = 1 // остаток 1 (3 - 1 *2 = 1) 1 / 2 = 0 // остаток 1 (1 - 0 *2 = 1)

Общий алгоритм состоит в последовательном делении числа и результатов деления на 2 и получение остатков, пока не дойдем до 0. Затем выстраиваем остатки в линию в обратном порядке и таким образом формируем двоичное представление числа. Конкретно в данном случае по шагам:

  1. Делим число 13 на 2. Результат деления - 6, остаток от деления - 1 (так как 13 - 6 *2 = 1)
  2. Далее делим результат предыдущей операции деления - число 6 на 2. Результат деления - 3, остаток от деления - 0
  3. Делим результат предыдущей операции деления - число 3 на 2. Результат деления - 1, остаток от деления - 1
  4. Делим результат предыдущей операции деления - число 1 на 2. Результат деления - 0, остаток от деления - 1
  5. Последний результат деления равен 0, поэтому завершаем процесс и выстраиваем остатки от операций делений, начиная с последнего - 1101

При обратном переводе из двоичной системы в десятичную умножаем значение каждого бита (1 или 0) на число 2 в степени, равной номеру бита (нумерация битов идет от нуля):

// перевод двоичного числа 1101 в десятичную систему
1(3-й бит)1(2-й бит)0(1-й бит)1(0-й бит)
1 * 23 + 1 * 22 + 0 * 21 + 1 * 20
=
1 * 8 + 1 * 4 + 0 * 2 + 1 * 1
=
8 + 4 + 0 + 1 
=
13

В JavaScript для определения чисел в двоичном формате перед числом применяется префикс 0b:

const num = 0b1101; // 13 в десятичной системе console.log(num); // 13

Представление отрицательных чисел

Для записи чисел со знаком в JavaScript применяется дополнительный код (two's complement), при котором старший разряд является знаковым. Если его значение равно 0, то число положительное, и его двоичное представление не отличается от представления беззнакового числа. Например, 0000 0001 в десятичной системе 1.

Если старший разряд равен 1, то мы имеем дело с отрицательным числом. Например, 1111 1111 в десятичной системе представляет -1. Соответственно, 1111 0011 представляет -13.

Чтобы получить из положительного числа отрицательное, его нужно инвертировать и прибавить единицу:

Например, получим число -3. Для этого сначала возьмем двоичное представление числа 3:

310 = 0000 00112

Инвертируем биты

~0000 0011 = 1111 1100

И прибавим 1

1111 1100 + 1 = 1111 1101

Таким образом, число 1111 1101 является двоичным представлением числа -3.

Сложение числа со знаком и без знака.

Например, сложим 12 и -8:

1210 = 000011002
+
-810 = 111110002 (8 - 00001000, после инверсии - 11110111, после +1 = 11111000)
=
410 = 000001002

Мы видим, что в двоичной системе получилось число 000001002 или 410 в десятичной системе.

Мы можем это увидеть на практике:

let num = 0b1100; // 12 в десятичной системе num = ~num; // инверсия разрядов num = num + 1; console.log(num); // -12

Операции сдвига

Каждое целое число в памяти представлено в виде определенного количества разрядов. И операции сдвига позволяют сдвинуть битовое представление числа на несколько разрядов вправо или влево. Операции сдвига применяются только к целочисленным операндам. Есть две операции:

  • << (сдвиг влево)

    Сдвигает битовое представление числа, представленного первым операндом, влево на определенное количество разрядов, которое задается вторым операндом.
    const res = 2 << 2; // 10 на два разрядов влево = 1000 - 8 console.log(res); // 8
    Число 2 в двоичном представлении 00102. Если сдвинуть число 0010 на два разряда влево, то получится 1000, что в десятичной системе равно число 8.
  • >> (арифметический сдвиг вправо)

    Сдвигает битовое представление числа вправо на определенное количество разрядов.
    const res = 16 >> 3; // 10000 на три разряда вправо = 10 или 2 в десятичной системе console.log(res); // 2

    Число 16 в двоичном представлении 100002. Если сдвинуть число 10000 на три разряда вправо (три последних разряда отбрасываются), то получится 00010, что в десятичной системе представляет число 2.

    Стоит отметить, что это так называемый арифметический сдвиг, при котором сдвинутые влевой части разряды заполняются знаковым битом - 0 для положительных и 1 для отрицательных чисел. Таким образом, при сдвиге отрицательных чисел не будет опасения, что после сдвига они станут положительными. Например:

    const res = -16 >> 3; // 11111110000 на три разряда вправо = 1111111111111110 console.log(res); // -2

    Так, при сдвиге вправо -16 на 3 разряда мы получим -2, что вполне естественно.

  • >>> (логический сдвиг вправо)

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

    const res = -16 >>> 3; console.log(res); // 536870910

Можно заметить, что сдвиг на один разряд влево фактически аналогично умножению на 2, тогда как сдвиг вправо на один раз эквивалентно делению на два. Мы можем обобщить: сдвиг влево на n аналогичен умножению числа на 2n, а сдвиг вправо на n разрядов аналогичен делению на 2n, что можно использовать вместо умножения/деления на степени двойки:

const res1 = 8 << 2; // эквивалентно 8 * 4 console.log(res1); // 32 const res2 = 64 >> 4; // эквивалентно 64 / 16 console.log(res2); // 4

Поразрядные операции

Поразрядные операции также проводятся только над соответствующими разрядами чисел:

  • & : поразрядная конъюнкция (операция И или поразрядное умножение). Возвращает 1, если оба из соответствующих разрядов обоих чисел равны 1
  • | : поразрядная дизъюнкция (операция ИЛИ или поразрядное сложение). Возвращает 1, если хотя бы один из соответствующих разрядов обоих чисел равен 1
  • ^ : поразрядное исключающее ИЛИ. Возвращает 1, если только один из соответствующих разрядов обоих чисел равен 1
  • ~ : поразрядное отрицание или инверсия. Инвертирует все разряды операнда. Если разряд равен 1, то он становится равен 0, а если он равен 0, то он получает значение 1.

Применение операций:

const a = 5 | 2; // 101 | 010 = 111 - 7 const b = 6 & 2; // 110 & 010 = 10 - 2 const c = 5 ^ 2; // 101 ^ 010 = 111 - 7 const d = ~9; // -10

Например, выражение 5 | 2 равно 7. Число 5 в двоичной записи равно 101, а число 2 - 10 или 010. Сложим соответствующие разряды обоих чисел. При сложении если хотя бы один разряд равен 1, то сумма обоих разрядов равна 1. Поэтому получаем:

101
010
111

В итоге получаем число 111, что в десятичной записи представляет число 7.

Возьмем другое выражение 6 & 2. Число 6 в двоичной записи равно 110, а число 2 - 10 или 010. Умножим соответствующие разряды обоих чисел. Произведение обоих разрядов равно 1, если оба этих разряда равны 1. Иначе произведение равно 0. Поэтому получаем:

110
010
010

Получаем число 010, что в десятичной системе равно 2.

📜 Условные выражения

Условные выражения работают с условиями - выражениями, которые возвращают значение типа Boolean - true (условие не верно) или false (условие не верно). Есть два типа условных операций: операции сравнения и логические операции.

Операции сравнения

Операторы сравнения сравнивают два значения и возвращают значение true или false:

  • ==

    Оператор равенства сравнивает два значения, и если они равны, возвращает true, иначе возвращает false: x == 5

  • ===

    Оператор тождественности также сравнивает два значения и их тип, и если они равны, возвращает true, иначе возвращает false: x === 5

  • !=

    Сравнивает два значения, и если они не равны, возвращает true, иначе возвращает false: x != 5

  • !==

    Сравнивает два значения и их типы, и если они не равны, возвращает true, иначе возвращает false: x !== 5

  • >

    Сравнивает два значения, и если первое больше второго, то возвращает true, иначе возвращает false: x > 5

  • <

    Сравнивает два значения, и если первое меньше второго, то возвращает true, иначе возвращает false: x < 5

  • >=

    Сравнивает два значения, и если первое больше или равно второму, то возвращает true, иначе возвращает false: x >= 5

  • <=

    Сравнивает два значения, и если первое меньше или равно второму, то возвращает true, иначе возвращает false: x <= 5

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

const income = 100; const strIncome = "100"; const result = income == strIncome; console.log(result); // true

Константа result здесь будет равна true, так как фактически и income, и strIncome представляют число 100.

Но оператор тождественности возвратит в этом случае false, так как данные имеют разные тип:

const income = 100; const strIncome = "100"; const result = income === strIncome; console.log(result); // false

Аналогично работают операторы неравенства != и !==.

// == (нестрогое равенство - приводит типы) console.log(5 == '5'); // true console.log(0 == false); // true // === (строгое равенство - сравнивает и тип) console.log(5 === '5'); // false console.log(0 === false); // false // != и !== (неравенство) console.log(5 != '5'); // false console.log(5 !== '5'); // true // Больше, меньше console.log(5 > 3); // true console.log(5 < 3); // false console.log(5 >= 5); // true console.log(5 <= 4); // false
💡 Всегда используйте ===

Рекомендуется всегда использовать строгое сравнение (=== и !==), чтобы избежать неожиданных преобразований типов.

Логические операции

Логические операции обычно применяются к значениям типа Boolean - true и false. Результат логических операций также обычно представляет значение типа Boolean - true или false. Часто этот вид операций используется проверки условий. В JavaScript есть следующие логические операции:

Логическое И

Операция && возвращает true, если оба операнда возвращают true, иначе возвращает false:

console.log(true && true); // true console.log(true && false); // false console.log(false && false); // false

Нередко этот тип операций применяется, если надо проверить истинность двух условий, причем оба условия должны быть истины:

const money = 1000; const age = 21; // проверяем, что age больше 18 и money больше 100 const access1 = age > 18 && money > 100; console.log(access1); // true // проверяем, что age больше 18 и money больше 1000 const access2 = age > 18 && money > 1000; console.log(access2);

В данном случае константа access1 будет равна true, если одновременно и age больше 18, и money больше 100. То есть условно говоря, если челевеку больше 18 лет, и у него больше 100 денежных единиц, то открываем ему доступ. Здесь оба эти условий истинны, поэтому и access1 равна true.

А вот константа access2 будет равна true, если одновременно и age больше 18, и money больше 1000. Здесь второе условие не соблюдается, оно ложно, поэтому и access2 равна false.

Логическое ИЛИ

Операция || возвращает true, если хотя бы один из операндов равен true, иначе операция || возвращает false:

console.log(true || true); // true console.log(true || false); // true console.log(false || false); // false

Эта операция также нередко применяется для проверки двух условий, когда достаточно, чтобы только одно условий было истинным, например:

const money = 1000; const age = 21; // проверяем, что age больше 18 или money больше 1000 const access1 = age > 18 || money > 1000; console.log(access1); // true // проверяем, что age больше 22 или money больше 1000 const access2 = age > 22 || money > 1000; console.log(access2); // false

В данном случае константа access1 будет равна true, если одновременно или age больше 18, или money больше 1000. То есть условно говоря, если человеку либо больше 18 лет, либо у него больше 1000 денежных единиц, то открываем ему доступ. То есть достаточно истинности одного условия. Здесь первое условие истинно, поэтому и access1 равна true.

А вот константа access2 будет равна true, если одновременно и age больше 22, и money больше 1000. Здесь ни первое, ни второе условие не соблюдаются, они ложны, поэтому и access2 равна false.

Логическое отрицание

Операция ! возвращает true, если операнд равен false, и возвращает false, если операнд равен true:

console.log(!true); // false console.log(!false); // true const isAlive = true; const isDead = !isAlive; console.log(isDead); // false

Логические операции с произвольными значениями

Выше писалось, что логические операции обычно в качестве операндо принимают значения Boolean - true или false, а результатом операций также обычно являются значения Boolean. Но это обычно, в рельности же операндами и результатами этих операций могут быть произвольные значения. Иногда JavaScript может автоматически преобразовать определенные значения в тип Boolean:

Значение Во что преобразуется
null false
undefined false
0 (в том числе значение NaN) false
1 (любое ненулевое значение) true
"" (пустая строка) false
"некоторый текст" (непустая строка) true
{} (любой объект) true

И если какие-то операнды не представляют значение Boolean, то в этом случае логические операции действуют в соответствии с некоторыми плавилами.

Если как минимум один операнд операции && не является значением типа Boolean, то выполняются следующие действия:

  • если первый операнд возвращает false (например, число 0, пустая строка, null, undefined), то операция возвращает первый операнд.
  • в остальных случаях возвращается второй операнд.
let isAlive; // undefined let name = "Tom"; const result = isAlive && name; console.log(result);

Здесь первый операнд операции && - переменная isAlive равна undefined (так как переменная не инициализирована), что при преобразовании к Boolean даст false, поэтому операция возвратит значение переменной isAlive.

Еще несколько примеров:

console.log(false && "Tom"); // false console.log("Tom" && null); // null console.log(true && "Tom"); // Tom

Если один или оба операнда операции || не являются значениями Boolean, то операция выполняет следующие действия:

  • Если первый операнд оценивается как true (то есть не равен 0, пустой строки, null или undefined), то возвращается первый операнд
  • Во всех остальных случаях возвращается второй операнд

Примеры:

let isAlive; // undefined console.log(!isAlive); // true console.log(!null); // true console.log(!0); // true console.log(!10); // false console.log(!""); // true (пустая строка) console.log(!"Tom"); // false

Еще примеры:

// && (И) - все условия должны быть true let age = 25; let hasLicense = true; if (age >= 18 && hasLicense) { console.log('Можно водить'); } // || (ИЛИ) - хотя бы одно условие true let isWeekend = true; let isHoliday = false; if (isWeekend || isHoliday) { console.log('Выходной!'); } // ! (НЕ) - инверсия let isRaining = false; if (!isRaining) { console.log('Можно идти гулять'); }

📜 Условные конструкции

Выше мы рассмотрели, что логические операции могут принимать значения произвольных типов, а не только Boolean. Казалось бы в этом нет особого смысла. Тем не менее подобные конструкции могут быть очень удобны, когда нам надо проверить условие и в зависимости от проверки выполнить то или иное действие. Например:

const age = 22; age <= 17 && console.log("Вам меньше 18 лет. Доступ запрещен."); age > 17 && console.log("Вам больше 17 лет. Доступ разрешен.");

Здесь в зависимости от значения константы age (которая представляет условный возраст) выводим на консоль ту или иную строку (условно в зависимости от возраста разрешаем или запрещаем доступ). Напомню, что && возвращает второй операнд, если первый равен true.
Сначала выполняется первая операция &&:

age <= 17 && console.log("Вам меньше 18 лет. Доступ запрещен.");

Здесь сначала проверяется первый операнд - выражение age <= 17. Если оно истинно (то есть если age меньше 18), то выполняем метод console.log(). Однако поскольку условие из первого операнда НЕ верно (так как age больше 17), поэтому не будет выполнять второй операнд, и операция возвратит false.

Аналогично работает вторая операция && за тем исключением, что она проверяет истинность выражения age > 17 (то есть age должно быть больше 17)

age > 17 && console.log("Вам больше 17 лет. Доступ разрешен.");

Здесь же условие из первого операнда верно, поэтому будет выполняться второй операнд. В итоге на консоль браузера будет выведена строка

Вам больше 17 лет. Доступ разрешен.

Аналогично для построения условных конструкций можно использовать операцию ||, но она возвращает второй операнд, если первый операнд равен false. То есть получается наоброт: операция && возвращает второй операнд, если условие первого операнда верно, а || - если условие не верно. И мы могли бы переписать предыдущий пример с помощью операции || следующим образом:

const age = 12; age <= 17 || console.log("Вам больше 17 лет. Доступ разрешен."); age > 17 || console.log("Вам меньше 18 лет. Доступ запрещен.");

Операции присваивания

В JavaScript также есть ряд операций, которые сочетают логические операции с присвоением:

  • &&=

    Аналогично выполнению a = a && b:
    let x = true; let y = false; y &&= x; console.log(y); // false let c = false; let d = true; c &&= d; console.log(c); // false let a = true; let b = true; a &&= b; console.log(a); // true let e = false; let f = false; e &&= f; console.log(e); // false
  • ||=

    a ||= b эквивалентно выражению a = a || b:
    let x = true; let y = false; y ||= x; console.log(y); // true let a = true; let b = true; a ||= b; console.log(a); // true let c = false; let d = true; c ||= d; console.log(c); // true let e = false; let f = false; e ||= f; console.log(e); // false

📜 Условные операторы ?: и ??

Условные операторы позволяют проверить некоторое условие и в зависимости от результата проверки выполнить определенные действия. Здесь мы рассмотрим оператор ?: или так называемый тернарный оператор и операцию ??.

Тернарная операция

Тернарная операция состоит из трех операндов и имеет следующее определение:

[первый операнд - условие] ? [второй операнд] : [третий операнд]

В зависимости от условия в первом операнде тернарная операция возвращает второй или третий операнд. Если условие в первом операнде равно true, то возвращается второй операнд; если условие равно false, то третий. Например:

const a = 1; const b = 2; const result = a < b ? a: b; console.log(result); // 1

Здесь первый операнд представляет следующее условие a < b. Если значение константы a меньше значения константы b, то возвращается второй операнд - a, то есть константа result будет равна a.

Если значение константы a больше или равно значению константы b, то возвращается третий операнд - b, поэтому константа result будет равна значению b.

В качестве операндов также могут выступать выражения:

const a = 1; const b = 2; const result = a < b ? a + b : a - b; console.log(result); // 3

В этом примере кода первый операнд представляет то же самое условие, что и в предыдущем примере, однако второй и третий операнды представляют арифметические операции. Если значение константы a меньше значения константы b, то возвращается второй операнд - a + b. Соответственно константа result будет равна сумме a и b.

Если значение константы a больше или равно значению константы b, то возвращается третий операнд - a - b. Соответственно константа result будет равна разности a и b.

// условие ? значение_если_true : значение_если_false let age = 20; let status = age >= 18 ? 'adult' : 'minor'; // Можно вкладывать let grade = score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C' : 'D';

Оператор ??

Оператор ?? (nullish coalescing operator) позволяет проверить значение на null и undefined. Он принимает два операнда:

левый_операнд ?? правый_операнд

Оператор возвращает значение левого операнда, если оно НЕ равно null и undefined. Иначе возвращается значение правого операнда. Например:

const result = "hello" ?? "world"; console.log(result); // hello console.log(0 ?? 5); // 0 console.log("" ?? "javascript"); // "" - пустая строка console.log(false ?? true); // false console.log(null ?? "not null"); // not null console.log(undefined ?? "defined"); // defined console.log(null ?? null); // null console.log(undefined ?? undefined); // undefined

Оператор ??=

Оператор ?? имеет модификацию в виде оператора ??=, который также позволяет проверить значение на null и undefined. Он принимает два операнда:

левый_операнд ??= правый_операнд
const message = "Hello JavaScript"; let text = "Hello work!" text ??= message; console.log(text); // Hello work!

Если левый операнд равен null и undefined, то ему присваивается значение правого операнда. Иначе левый операнд сохраняет свое значение. Например:

const message = "Hello JavaScript"; let text = "Hello work!" text ??= message; console.log(text); // Hello work!

Здесь переменная text не равна null или undefined, поэтому она сохраняет свое значение. Обратный пример:

const message = "Hello JavaScript"; let text = null; text ??= message; console.log(text); // Hello JavaScript

Здесь переменная text равна null, поэтому при посредстве оператора ??= она получает значение переменной message.

📜 Преобразования данных

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

const number1 = "56"; const number2 = 4; cont result = number1 + number2; console.log(result); // 564

Здесь константа number1 представляет строку, а точнее строковое представление числа. А константа number2 представляет число. И в итоге мы получим не число 60, а строку 564.

При сложении преобразования в JavaScript производятся по принципу:

  • Если оба операнда представляют числа, то происходит обычное арифметическое сложение
  • Если предыдущее условие не выполняется, то оба операнда преобразуются в строки и производится объединение строк.

Соответственно в примере выше, поскольку первый операнд - строка, то второй операнд - число также преобразуется в строку, и в итоге получаем строку "564", а не число 60. Фактически мы получаем:

const number1 = "56"; const number2 = 4; const result = number1 + String(number2); console.log(result); // 564

Выражение String(number2) позволяет получить строковое представление константы number2, то есть из числа 4 получает строку "4".

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

parseInt()

Для преобразования строки в целое число применяется функция parseInt():

const number1 = "56"; const number2 = 4; const result = parseInt(number1) + number2; console.log(result); // 60

При этом строка может иметь смешанное содержимое, например, "123hello", то есть в данном случае есть цифры, но есть и обычные символы. Функция parseInt() все равно попытается выполнить преобразование - она последовательно, начиная с первого символа, считывает цифры, пока не встретит первый нецифровой символ:

const num1 = "123hello"; const num2 = parseInt(num1); console.log(num2); // 123

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

const number1 = parseInt(0.000005); // 0; console.log(number1); const number2 = parseInt(0.0000005); // 5 console.log(number2);

Выше в функцию parseInt передаются дробные числа, и мы ожидаем в обоих случаях получить число 0. Однако при преобразовании number2 мы получаем число 5. Почему?

Пример выше будет эквивалентен следующему:

const number1 = parseInt(String(0.000005)); // 0; console.log(number1); const number2 = parseInt(String(0.0000005)); // 5 console.log(number2);

Для дробных чисел меньше 10-6 (0.000001) применяется экспоненциональная запись, то есть число 0.0000005 представляется как 5e-7:

console.log(0.0000005); // 5e-7

Далее число 5e-7 преобразуется в строку "5e-7", и эту строку parseInt пытается преобразовать в число. Соответственно на выходе получается число 5.

То же самое касается очень больших чисел типа 999999999999999999999, которые также представляются к экспоненциальной форме.

NaN и isNaN

Если функции parseInt() не удастся выполнить преобразование, то он возвращает значение NaN (Not a Number), которое говорит о том, что строка не представляет число и не может быть преобразована.

console.log(parseInt("abc")); // NaN cont type = typeof NaN; console.log(type); // number

Что интересно, само значение NaN (не число) представляет тип number, то есть число.

С помощью специальной функции isNaN() можно проверить, представляет ли строка число. Если строка не является числом, то функция возвращает true, если это число - то false:

const num1 = "javascript"; const num2 = "22"; let result = isNaN(num1); console.log(result); // true - num1 не является числом result = isNaN(num2); console.log(result); // false - num2 - это число

parseFloat

Для преобразования строк в дробные числа применяется функция parseFloat(), которая работает аналогичным образом:

const number1 = "46.07"; const number2 = "4.98"; let result = parseFloat(number1) + parseFloat(number2); console.log(result); // 51.05

Преобразование из строки в число и оператор +

Стоит отметить, что для преобразования строки в число в JavaScript мы можем использовать оператор унарного плюса + перед преобразуемым значением:

const number1 = "56"; const number2 = 4; const result = +number1 - number2; console.log(result); // 52

Здесь выражение +number1 преобразует строку "56" в число 56.

Если надо получить отрицательное представление числа, то аналогичным образом применяется знак унарного минуса (как перед отрицательными числами):

const number1 = "56"; const number2 = 4; const result = -number1 - number2; // -56 - 4 = -60 console.log(result); // -60

Однако в примерах выше строка number1 могла быть преобразована в числа. Если это не возможно, то результатом преобразования будет значение NaN:

const number1 = "56hek"; console.log(+number1); // NaN

📜 Введение в массивы

Для работы с наборами данных предназначены массивы. Для создания массива применяются квадратные скобки []. Внутри квадратных скобок определяются элементы массива. (Далее в отдельной главе, посвященной массивам, мы рассмотрим дополнительные способы создания массивов и вцелом более подробно работу с массивами)

Простейшее определение массива:

const myArray = [];

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

const people = ["Tom", "Alice", "Sam"]; console.log(people); // ["Tom", "Alice", "Sam"];

Для обращения к отдельным элементам массива используются индексы. Отсчет начинается с нуля, то есть первый элемент будет иметь индекс 0, а последний - 2:

const people = ["Tom", "Alice", "Sam"]; console.log(people[0]); // Tom const person3 = people[2]; // Sam console.log(person3); // Sam

Если мы попробуем обратиться к элементу по индексу больше размера массива, то мы получим undefined:

const people = ["Tom", "Alice", "Sam"]; console.log(people[7]); // undefined

Также по индексу осуществляется установка значений для элементов массива:

const people = ["Tom", "Alice", "Sam"]; console.log(people[0]); // Tom people[0] = "Bob"; console.log(people[0]); // Bob

Причем в отличие от других языков, как C# или Java, можно установить элемент, который изначально не установлен:

const people = ["Tom", "Alice", "Sam"]; console.log(people[7]); // undefined - в массиве только три элемента people[7] = "Bob"; console.log(people[7]); // Bob

Также стоит отметить, что в отличие от ряда языков программирования в JavaScript массивы не являются строго типизированными, один массив может хранить данные разных типов:

const objects = ["Tom", 12, true, 3.14, false]; console.log(objects);

Многомерные массивы

Массивы могут быть одномерными и многомерными. Каждый элемент в многомерном массиве может представлять собой отдельный массив. Выше мы рассматривали одномерный массив, теперь создадим многомерный массив:

const numbers1 = [0, 1, 2, 3, 4, 5 ]; // одномерный массив const numbers2 = [[0, 1, 2], [3, 4, 5] ]; // двумерный массив

Визуально оба массива можно представить следующим образом:

Одномерный массив numbers1

012345

Двухмерный массив numbers2

012
345

Поскольку массив numbers2 двухмерный, он представляет собой простую таблицу. Каждый его элемент может представлять отдельный массив.

Рассмотрим еще один двумерный массив:

const people = [ ["Tom", 25, false], ["Bill", 38, true], ["Alice", 21, false] ]; console.log(people[0]); // ["Tom", 25, false] console.log(people[1]); // ["Bill", 38, true]

Массив people можно представить в виде следующей таблицы:

Tom25false
Bill38true
Alice21false

Чтобы получить отдельный элемент массива, также используется индекс:

const tomInfo = people[0];

Только теперь переменная tomInfo будет представлять массив. Чтобы получить элемент внутри вложенного массива, нам надо использовать его вторую размерность:

console.log("Имя: ", people[0][0]); // Tom console.log("Возраст: ", people[0][1]); // 25

То есть если визуально двумерный массив мы можем представить в виде таблицы, то элемент people[0][1] будет ссылаться на ячейку таблицы, которая находится на пересечении первой строки и второго столбца (первая размерность - 0 - строка, вторая размерность - 1 - столбец).

Также мы можем выполнить присвоение:

const people = [ ["Tom", 25, false], ["Bill", 38, true], ["Alice", 21, false] ]; people[0][1] = 56; // присваиваем отдельное значение console.log(people[0][1]); // 56 people[1] = ["Bob", 29, false]; // присваиваем массив console.log(people[1][0]); // Bob

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

const numbers = []; numbers[0] = []; // теперь numbers - двумерный массив numbers[0][0]=[]; // теперь numbers - трехмерный массив numbers[0][0][0] = 5; // первый элемент трехмерного массива равен 5 console.log(numbers[0][0][0]);

📜 Условные конструкции

Условные конструкции позволяют выполнить те или иные действия в зависимости от определенных условий.

Конструкция if..else

Конструкция if..else проверяет некоторое условие и если это условие верно, то выполняет некоторые действия. Простейшая форма конструкции if..else:

if(условие){ /* ... некоторые действия ... */ }

После ключевого слова if в круглых скобках идет условие, а после условия - блок кода с некоторыми действиями. Если это условие истинно, то затем выполняются действия, которые помещены в блоке кода

Например:

const income = 100; if(income > 50) { console.log("доход больше 50"); }

Здесь в конструкции if используется следующее условие: income > 50. Если это условие возвращает true, то есть если константа income имеет значение больше 50, то браузер отображает сообщение. Если же значение income меньше 50, то никакого сообщения не отображается.

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

const income = 100; if(income > 50) console.log("доход больше 50");

или перенести действия на следующую строку

const income = 100; if(income > 50) console.log("доход больше 50");

Причем условия могут быть сложными:

const income = 100; const age = 19; if(income > 50 && age > 18){ console.log("доход больше 50"); console.log("возраст больше 18"); }

Проверка наличия значения

let myVar = 89; if(myVar){ console.log(`Переменная myVar имеет значение: ${myVar}`); }

Если переменная myVar имеет значение, как в данном случае, то в условной конструкции она возвратит значение true.

Противоположный вариант:

let myVar; if(myVar){ console.log(`Переменная myVar имеет значение: ${myVar}`); }

Здесь переменная myVar не имеет значения. (В реальности она равна undefined) Поэтому условие в конструкии if возвратит false, и действия в блоке конструкции if не будут выполняться.

Но нередко для проверки значения переменной используют альтернативный вариант - проверяют на значение undefined и null:

if (myVar !== undefined && myVar !== null) { console.log(`Переменная myVar имеет значение: ${myVar}`); }

Выражение else

Выше мы рассмотрели, как определить действия, которые выполняются, если условие после if истинно. Но что, если мы хотим также выполнять еще один набор инструкций, если условие ложно? В этом случае можно использовать блок else. Данный блок содержит инструкции, которые выполняются, если условие после if ложно, то есть равно false:

if(условие){ действия, если условие истинно } else{ действия, если условие ложно }

То есть если условие после if истинно, выполняется блок if. Если условие ложно, выполняется блок else. Например:

const income = 45; if(income > 50){ console.log("Доход больше 50"); } else{ console.log("Доход меньше или равен 50"); }

Здесь константа income равна 45, поэтому условие после оператора if возвратит false, и управление перейдет к блоку else.

Также если блок else содержит одну инструкцию, то можно сократить конструкцию:

const income = 45; if(income > 50) console.log("Доход больше 50"); else console.log("Доход меньше или равен 50");

Альтернативные условия и else if

С помощью конструкции else if мы можем добавить альтернативное условие к блоку if. Например, выше в условие значение income может быть больше определенного значению может быть меньше, а может быть равно ему. Отразим это в коде:

const income = 50; if(income > 50) { console.log("Доход больше 50"); } else if(income === 50){ console.log("Доход равен 50"); } else{ console.log("Доход меньше 50"); }

В данном случае выполнится блок else if. При необходимости мы можем использовать несколько блоков else if с разными условиями:

const income = 500; if(income < 200){ console.log("Доход ниже среднего"); } else if(income >= 200 && income < 300) { console.log("Чуть ниже среднего"); } else if(income >= 300 && income < 400){ console.log("Средний доход"); } else{ console.log("Доход выше среднего"); }

При этом блок else применять необязательно:

let age = 20; if (age >= 18) { console.log('Совершеннолетний'); } else { console.log('Несовершеннолетний'); } // else if let score = 85; if (score >= 90) { console.log('Отлично'); } else if (score >= 70) { console.log('Хорошо'); } else if (score >= 50) { console.log('Удовлетворительно'); } else { console.log('Неудовлетворительно'); }

True или false

В javascript любая переменная может применяться в условных выражениях, но не любая переменная представляет тип boolean. И в этой связи возникает вопрос, что возвратит та или иная переменная - true или false? Много зависит от типа данных, который представляет переменная:

Тип данных Что возвращает Пример
undefined false
null false
Boolean false - если константа/переменная равна false.
true - если константа/переменная равна true.
Number false - если число равно 0 или NaN (Not a Number),
true - в остальных случаях
следующая переменная вернет false:
let x = NaN; if(x){ // false }
String false - если константа/переменная равна пустой строке, то есть ее длина равна 0,
true - в остальных случаях
// false - так как пустая строка const emptyText = ""; // true - строка не пустая const someText = "javascript";
Object

Всегда возвращает true

const user = {name:"Tom"}; // true const car = {}; // true

Конструкция switch..case

Конструкция switch..case является альтернативой использованию конструкции if..else и также позволяет обработать сразу несколько условий:

let day = 'Monday'; switch(day) { case 'Monday': console.log('Понедельник'); break; case 'Tuesday': console.log('Вторник'); break; case 'Wednesday': console.log('Среда'); break; case 'Thursday': console.log('Четверг'); break; case 'Friday': console.log('Пятница'); break; case 'Saturday': case 'Sunday': console.log('Выходной!'); break; default: console.log('Неизвестный день'); }
const income = 200; switch(income){ case 100 : console.log("Доход равен 100"); break; case 200 : console.log("Доход равен 200"); break; case 500 : console.log("Доход равен 500"); break; }

После ключевого слова switch в скобках идет сравниваемое выражение. Значение этого выражения последовательно сравнивается со значениями, помещенными после оператора сase. И если совпадение будет найдено, то будет выполняться определенный блок сase.

В конце каждого блока сase ставится оператор break, чтобы избежать выполнения других блоков. В данном случае константа income равна 200, поэтому будет выполняться блок

case 200 : console.log("Доход равен 200"); break;

Оператор break

Оператор break служит для того, чтобы избежать выполнения других условий.

Благодаря оператору break после выполнения блока произойдет выход из конструкции switch, и никакие другие блоки case не будут выполняться.

Но теперь уберем оператор break:

let income = 200; switch(income){ case 100 : console.log("Доход равен 100"); income +=100; case 200 : console.log("Доход равен 200"); income +=100; case 500 : console.log("Доход равен 500"); income +=100; } console.log("Финальный доход равен", income);

Результат выполнения:

Доход равен 200
Доход равен 500
Финальный доход равен 400

Здесь изначально переменная income опять равна 200, соответственно будет выполняться блок

case 200 : console.log("Доход равен 200"); income +=100;

Значение income увеличивается на 100, однако в конце блока нет оператора break, поэтому управление перейдет к проверке условия в следующий блок:

case 500 : console.log("Доход равен 500"); income +=100;

И не важно, что income не равно 500 (а лишь 300 на данный момент), этот блок также будет выполняться.

⚠️ Не забывайте break!

Без break выполнение "проваливается" в следующий case (fall-through). Это может быть полезно (как в примере с Saturday/Sunday), но чаще всего это ошибка.

Объединение условий

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

const income = 200; switch(income){ case 100 : case 200 : console.log("Доход равен 100 или 200"); break; case 500 : console.log("Доход равен 500"); break; }

В данном случае для условия, когда income равно 100 и 200, выполняются одни и те же действия.

Условие по умолчанию - default

Если мы хотим также обработать ситуацию, когда совпадения не будет найдено, то можно добавить необязательный блок default:

const income = 700; switch(income){ case 100 : console.log("Доход равен 100"); break; case 200 : console.log("Доход равен 200"); break; case 500 : console.log("Доход равен 500"); break; default: console.log("Доход неизвестной величины"); break; }

📜 Циклы

Циклы позволяют в зависимости от определенных условий выполнять некоторое действие множество раз. В JavaScript имеются следующие виды циклов:

  • for
  • for..in
  • for..of
  • while
  • do..while

Цикл for

Цикл for имеет следующее формальное определение:

for ([инициализация счетчика]; [условие]; [изменение счетчика]){ // действия }

Например, используем цикл for для перебора чисел от 0 до 4:

for(let i = 0; i < 5; i++){ console.log(i); } console.log("Конец работы");

Первая часть объявления цикла - let i = 0 - создает и инициализирует счетчик - переменную i. И перед выполнением цикла ее значение будет равно 0. По сути это то же самое, что и объявление переменной.

Вторая часть - условие, при котором будет выполняться цикл: i<5. В данном случае цикл будет выполняться, пока значение i не достигнет 5.

Третья часть - i++ - приращение счетчика на единицу.

Каждое отдельное повторение цикла называется итерацией. Таким образом, в данном случае сработают 5 итераций.

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

for(let i = 10; i > 5; i--){ console.log(i); // 10, 9, 8, 7, 6 }

При этом можно опускать различные части объявления цикла:

let i = 0; for(; i < 60;){ console.log(i); i = i + 10; }

Счетчик удобно использовать как индекс элементов массива и таким образом перебирать массив:

const people = ["Tom", "Sam", "Bob"]; for(let i=0; i < 3; i++){ console.log(people[i]); }

Консольный вывод браузера:

Tom
Sam
Bob

Применение нескольких счетчиков в цикле

for(let i = 1, j=1; i < 5, j < 4; i++, j++){ console.log(i + j); } // 1 итерация: i=1, j=1; i + j = 2 // 2 итерация: i=2, j=2; i + j = 4 // 3 итерация: i=3, j=3; i + j = 6

Выполнение действий в объявлении цикла

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

for(let i = 0; i < 5; console.log(i++)); console.log("Конец работы");

Здесь не определено блока цикла, а сами действия цикла определены в третьей части заголовка цикла - console.log(i++)

Аналогично в первой части определения цикла - инициализации мы можем выполнять некоторые действия, а не обязательно только объявление счетчика:

let i=0; for(console.log("Init"); i < 5; i++){ console.log(i); }

Здесь определение счетчика вынесено вне цикла, а в инициализационной части цикла на консоль выводится строка. Вывод браузера:

Init
0
1
2
3
4

Вложенные циклы

for(let i=1; i <= 5; i++){ for(let j = 1; j <=5; j++){ console.log(i * j); } }

Используя вложенные циклы и несколько счетчиков можно перебирать многомерные массивы:

const people = [["Tom", 39], ["Sam", 28],["Bob", 42]]; for(let i=0; i < 3; i++){ // перебираем двухмерный массив for(let j=0; j < 2; j++){ // перебираем вложенные массивы console.log(people[i][j]); } console.log("================="); // для разделения элементов }

Здесь массив people представляет двухмерный массив из 3-х элементов, где каждый элемент представляет, в свою очередь, подмассив из 2-х элементов - условно имени и возраста пользователя. Во внешнем цикле определяем счетчик i для прохода по всем подмассивам в двухмерном массиве people, а во внутреннем цикле определяем счетчик j для прохода по всем элементам каждого подмассива. Консольный вывод:

Tom
39
=================
Sam
28
=================
Bob
42
=================

Цикл while

Цикл while выполняется до тех пор, пока некоторое условие истинно. Его формальное определение:

while(условие){ // действия }

Опять же выведем с помощью while числа от 1 до 5:

let i = 1; while(i <=5){ console.log(i); i++; }

Цикл while здесь будет выполняться, пока значение i не станет равным 6.

do..while

В цикле do сначала выполняется код цикла, а потом происходит проверка условия в инструкции while. И пока это условие истинно, цикл повторяется. Например:

let i = 1; do{ console.log( i ); i++; } while (i <= 5)

Здесь код цикла сработает 5 раз, пока i не станет равным 5. При этом цикл do гарантирует хотя бы однократное выполнение действий, даже если условие в инструкции while не будет истинно.

Операторы continue и break

Иногда бывает необходимо выйти из цикла до его завершения. В этом случае мы можем воспользоваться оператором break:

for(let i = 1; i <= 6; i++){ if(i === 4) break; console.log(i); } console.log("Конец работы");

Данный цикл увеличивает переменную i c 1 до 6 включая, то есть согласно условию цикла блок цикла должен выполняться 6 раз, то есть поизвести 6 итераций. Однако поскольку в блоке цикла происходит поверка if(i===4) break;, то, когда значение переменной i достигнет 4, то данное условие прервет выполнение цикла с помощью оператора break. И цикл заершит работу.

1
2
3
Конец работы

Если нам надо просто пропустить итерацию, но не выходить из цикла, мы можем применять оператор continue. Например, изменим предыдущий пример, только вместо break используем оператор continue:

for(let i = 1; i <= 6; i++){ if(i === 4) continue; console.log(i); } console.log("Конец работы");

В этом случае, когда значение переменная i станет равной 4 , то выражение i===4 возвратит true, поэтому будет выполняться конструкция if(i===4) continue;. С помощью оператора continue она завершит текущую итерацию, далее идущие инструкции цикла не будут выполняться, а произойдет переход к следующей итерации:

1
2
3
5
6
Конец работы

for..in

Цикл for..in предназначен главным образом для перебора объектов. Его формальное определение:

for (свойство in объект) { // действия }

Этот цикл перебирает все свойства объекта. Например:

const person = {name: "Tom", age: 37}; for(prop in person){ console.log(prop); }

Здесь перебирается объект person, который имеет два свойства - name и age. Соответственно на консоли мы увидим:

name
age

Получив свойтсва и используя специальный синтаксис объект[свойство], мы можем получить значение каждого свойства:

const person = {name: "Tom", age: 37}; for(prop in person){ console.log(prop, person[prop]); }

Консольный вывод:

name Tom
age 37

Цикл for...of

Цикл for...of предназначен для перебора наборов данных. Например, строка представляет фактически набор символов. И мы можем перебрать ее с помощью данного цикла:

const text = "Hello"; for(char of text){ console.log(char); }

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

H
e
l
l
o

Другим примером может быть перебор массива:

const people = ["Tom", "Sam", "Bob"]; for(const person of people) { console.log(person); }

В данном случае цикл перебирает элементы массива people. Каждый элемент последовательно помещается в константу person. И далее мы можем вывести ее значение на консоль:

Tom
Sam
Bob

for...(of, in)

// Классический for for (let i = 0; i < 5; i++) { console.log(i); // 0, 1, 2, 3, 4 } // for...of (для массивов) let fruits = ['apple', 'banana', 'orange']; for (let fruit of fruits) { console.log(fruit); } // for...in (для объектов) let user = {name: 'Alice', age: 25}; for (let key in user) { console.log(key + ': ' + user[key]); }

while

let i = 0; while (i < 5) { console.log(i); i++; } // do...while (выполнится хотя бы 1 раз) let j = 0; do { console.log(j); j++; } while (j < 5);

break и continue

// break - выход из цикла for (let i = 0; i < 10; i++) { if (i === 5) break; // остановка на 5 console.log(i); // 0, 1, 2, 3, 4 } // continue - пропуск итерации for (let i = 0; i < 5; i++) { if (i === 2) continue; // пропускаем 2 console.log(i); // 0, 1, 3, 4 }

📜 Отладка и отладчик

В общем случае отладка представляет процесс поиска ошибок и багов в программе. Для выполнения отладки применяется специальный инструмент - отладчик. Но кроме поиска багов и ошибок отладка и отладчик предоставляют прекрасный способ понять поток и выполнение программы, происходящие в ней действия, как эти действия выполняются в программе и в конечном счете обогатить навыки и понимание разработки.

Что касательно отладчиков для программ на языке JavaScript, то мы можем использоваться отладчиками, встроенными в веб-браузеры. Многие распространенные веб-браузеры предоставляют возможность отладки. В данном же случае для отладки мы будем использовать встроенный отладчик в Chrome DevTools, так как Google Chrome представляет наиболее распростраенный браузер. Однако работа с отладчиками в других браузерах будет во многом похожей.

Рассмотрим следующую программу:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <script> const numbers = [5, 3, 6, 2, 5, 1]; for(let i=0; i < numbers.length-1; i++){ for(let j=i+1; j < numbers.length; j++){ if(numbers[i] > numbers[j]){ const temp = numbers[j]; numbers[j] = numbers[i]; numbers[i] = temp; } } } console.log("numbers:", numbers); </script> </body> </html>

Здесь в коде JavaScript определена простейшая сортировка массива чисел. Сначала в цикле по i проходим по всем элементам массива вплоть до последнего (не включая), и сравниваем текущий элемент по i со всеми последующими элементами в цикле по j. Если один из последующих элементов (numbers[j]) меньше текщего элемента (numbers[i]), то меняем их местами. После того, как мы сравнивали текущий элемент numbers[i] со всеми последующими (numbers[j]) и поместили в numbers[i] наименьший элемент, увеличиваем значение i и переходим к сравнению следующего элемента со всеми последующими.

И чтобы детально посмотреть, как программа сортируем массив, воспользуемся отладкой. Для этого загрузим страницу с кодом JavaScript в Google Chrome и перейдем к инструментам разработчика (можно сделать с помощью комбинации клавиш Ctrl+Shift+I). Далее в панели инструментов разработчика откроем вкладку Sources

А в левом древовидном меню найдем файл, где у нас расположен код JavaScript (в моем случае это веб-страница index.html). И этот файл будет открыт в центральной части.

Установка точек прерывания

Чтобы иметь возможность инспектировать программу в определенной строке кода, надо на эту строку установить точку прерывания (breakpoint). В Chrome DevTools для этого достаточно нажать на номер нужной строки. После установки точки прерывания строка кода будет отмечена синий меткой:

В качестве альтернативы можно нажать на номер строки кода и в появившемся контекстном меню выбрать пункт "Add breakpoint" для установки на указанную строку точки прерывания.

Чтобы удалить точку прерывания, достаточно нажать на синюю метку на номере строки.

Если мы обновим страницу в браузере, то при выполнении кода JavaScript программа остановится на установленной точке прерывания:

И в этой точке мы можем исследовать состояние программы на текущий момент.

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

Например, из вышеприведенного скриншота видно, что при первом выполнении 17-й строки значение переменной i равно 0, а значение переменной j равно 1.

С помощью кнопок над правой колонкой можно управлять ходом отладки:

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

Кроме обычных точек прерывания Chrome DevTools позволяет устанавливать и другие типы точек прерывания:

  • условные точки прерывания - позволяют остановить выполнение программы, если соблюдается некоторое условие
  • точки прерывания DOM - позволяют остановить выполнение при динамическом изменении содержимого веб-страницы
  • точки преырвания обработчиков событий - позволяют остановить выполнение, если сработало определенное событие веб-страницы
  • точки прерывания XHR - позволяют остановить программу при выполнении Ajax-запроса. И таким образом, мы можем детально исследовать различные аспекты работы программы

Глава 3. Функции

📜 Определение функций

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

function имя_функции(параметры){ // Инструкции }

Определение функции начинается с ключевого слова function, после которого следует имя функции. Наименование функции подчиняется тем же правилам, что и наименование переменной: оно может содержать только цифры, буквы, символы подчеркивания и доллара ($) и должно начинаться с буквы, символа подчеркивания или доллара.

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

function hello(){ console.log("Hello Metanit.com"); }

Данная функция называется hello(). Она не принимает никаких параметров и все, что она делает, это выводит на консоль браузера строку "Hello Metanit.com".

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

имя_функции(параметры)

При вызове после имени вызываемой функции в скобках указывается список параметров. Если функция не имеет параметров, то указывются пустые скобки.

Например, определим и вызовем простейшую функцию:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <script> // определение функции function hello(){ console.log("Hello Metanit.com"); } // вызов функции hello(); </script> </body> </html>

В данном случае функция hello не принимает параметров, поэтому при ее вызове указываются пустые скобки:

hello();

Отличительной чертой функций является то, что их можно многократно вызывать в различных местах программы:

// определение функции function hello(){ console.log("Hello Metanit.com"); } // вызов функции hello(); hello(); hello();

Переменные и константы в качестве функций

Подобно тому, как константам и переменным присваиваются простейшие значения (числа, строки и т.д.), также им можно присваивать функции. Затем через такую переменную или константу можно вызвать присвоенную ей функцию:

<script> // определение функции function hello(){ console.log("Hello from Metanit.com"); } // передача константе message ссылки на функцию hello const message = hello; message(); // вызываем функцию, ссылка на которую хранится в константе message </script>

Присвоив константе или переменной функцию:

const message = hello;

затем мы можем по имени константы/переменной вызывать эту функцию:

message();

Также мы можем динамически менять функции, которые хранятся в переменной:

function goodMorning(){ console.log("Доброе утро"); } function goodEvening(){ console.log("Добрый вечер"); } let message = goodMorning; // присваиваем переменной message функцию goodMorning message(); // Доброе утро message = goodEvening; // меняем функцию в переменной message message(); // Добрый вечер

Функции-выражения и анонимные функции

Необязательно давать функциям определенное имя. Можно использовать анонимные функции. Такие функции при определении присваиваются константе или переменной. Эти функции еще называют функции-выражения (function expression):

const message = function(){ console.log("Hello JavaScript"); } message();

Используя имя константы или переменной, которой присвоена функция, можно вызывать эту функцию.

Локальные функции

JavaScript позволяет определять локальные функции - функции внутри других функций. Локальные функции видно только в рамках внешней функции, в которой они определены. Например:

function print(){ printHello(); printHello(); printHello(); function printHello(){ console.log("Hello"); } } print(); printHello(); // Uncaught ReferenceError: printHello is not defined //- локальную функцию можно вызвать только из ее окружающей функции

Здесь внутри функции print определена локальная функция printHello, которая просто выводит строку "Hello". И внутри функции print мы можем вызвать локальную функцию printHello, однако вне окружающей функции локальную функцию вызвать нельзя.

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

Объявление функций

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

// Классическое объявление function greet(name) { console.log('Привет, ' + name + '!'); } greet('Alice'); // "Привет, Alice!" // С возвратом значения function sum(a, b) { return a + b; } let result = sum(5, 3); // 8

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

// Присваиваем функцию переменной const greet = function(name) { console.log('Привет, ' + name + '!'); }; greet('Bob'); // "Привет, Bob!"

Arrow Functions (стрелочные функции) - ES6

// Краткая запись const greet = (name) => { console.log('Привет, ' + name + '!'); }; // Если один параметр, скобки можно опустить const greet = name => { console.log('Привет, ' + name + '!'); }; // Если одно выражение, можно опустить return и {} const sum = (a, b) => a + b; // Без параметров const sayHi = () => console.log('Hi!'); // Один параметр и одно выражение const double = x => x * 2;
💡 Разница между Function Declaration и Arrow Function
  • Hoisting: Function Declaration поднимается (можно вызвать до объявления)
  • this: Arrow Functions не имеют собственного this
  • arguments: Arrow Functions не имеют объекта arguments

📜 Параметры функций

Функция в JavaScript может принимать параметры. Параметры представляют способ передачи в функцию данных. Параметры указываются в скобках после названия функции.

Например, определим простейшую функцию, которая принимает один параметр:

function print(message){ console.log(message); } print("Hello JavaScript"); print("Hello METANIT.COM"); print("Function in JavaScript");

Функция print() принимает один параметр - message. Поэтому при вызове функции мы можем передать для него значение, например, некоторую строку:

print("Hello JavaScript");

Передаваемые параметрам значения еще называют аргументами.

При этом в отличие от ряда других языков программирования мы в принципе можем не передавать значения параметрам. Например:

function print(message){ console.log(message); } print();

Если параметру не передается значение, тогда он будет иметь значение undefined.

Если функция принимает несколько параметров, то они перечисляются через запятую:

function sum(a, b){ const result = a + b; console.log(result); } sum(2, 6); // 8 sum(4, 5); // 9 sum(109, 11); // 120

При вызове функции с несколькими параметрами значения передаются параметрам по позиции. То есть первое значение передается первому параметру, второе значение - второму и так далее. Например, в вызове:

sum(2, 6);

Число 2 передается параметру a, а число 6 - параметру b.

Передача массива в качестве параметра и spread-оператор

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

function printPerson(person) { console.log("Name:", person[0]); console.log("Age:", person[1]); console.log("Email:", person[2]); console.log("========================="); } const tom = ["Tom", 39, "tom@example.com"]; const bob = ["Bob", 43, "bob@example.com"]; printPerson(tom); printPerson(bob);

В данном случае функция printPerson принимает массив, который, как предполагается, имеет три элемента. И внутри функции происходит обращение к этим элементам.

Но рассмотрим другую ситуацию, когда функция принимает данные в виде отдельных параметров. И в этом случае удобнее использовать spread-оператор ...:

function printPerson(username, age, email) { console.log("Name:", username); console.log("Age:", age); console.log("Email:", email); console.log("========================="); } const tom = ["Tom", 39, "tom@example.com"]; const bob = ["Bob", 43, "bob@example.com"]; printPerson(...tom); printPerson(...bob);

Чтобы последовательно передать элементы массива параметрам функции перед именем массива указывается spread-оператор ...:

printPerson(...tom);

Это все равно, что если бы мы написали

printPerson(tom[0], tom[1], tom[2]);

только spread-оператор позволяет сделать передачу значений лаконичней.

Необязательные параметры и значения по умолчанию

Функция может принимать множество параметров, но при этом часть или все параметры могут быть необязательными. Если для параметров не передается значение, то по умолчанию они имеют значение "undefined". Однако иногда бывает необходимо, чтобы параметры обязательно имели какие-то значения, например, значения по умолчанию. До стандарта ES6 необходимо было проверять значения параметров на undefined:

function sum(x, y){ if(y === undefined) y = 5; if(x === undefined) x = 8; const z = x + y; console.log(z); } sum(); // 13 sum(6); // 11 sum(6, 4) // 10

Здесь функция sum() принимает два параметра. При вызове функции мы можем проверить их значения. При этом, вызывая функцию, необязательно передавать для этих параметров значения. Для проверки наличия значения параметров используется сравнение со значением undefined.

Также мы можем напрямую определять для параметров значения по умолчанию:

function sum(x = 8, y = 5){ const z = x + y; console.log(z); } sum(); // 13 sum(6); // 11 sum(6, 4) // 10

Если параметрам x и y не передаются значения, то они получаются в качестве значений числа 5 и 10 соответствено. Такой способ более лаконичен и интуитивен, чем сравнение с undefined.

При этом значение параметра по умолчанию может быть производным, представлять выражение:

function sum(x = 8, y = 10 + x){ const z = x + y; console.log(z); } sum(); // 26 sum(6); // 22 sum(6, 4) // 10

В данном случае значение параметра y зависит от значения x.

// ES6: значения по умолчанию function greet(name = 'Гость') { console.log('Привет, ' + name + '!'); } greet(); // "Привет, Гость!" greet('Alice'); // "Привет, Alice!"

Функции с произвольным количеством параметров

JavaScript позволяет определять так называемые variadic function или функции с произвольным количеством параметров. Для этого можно использовать ряд инструментов.

Объект arguments

При необходимости мы можем получить все переданные параметры через доступный внутри функции объект arguments:

function sum(){ let result = 0; for(const n of arguments) result += n; console.log(result); } sum(6); // 6 sum(6, 4) // 10 sum(6, 4, 5) // 15

При этом даже не важно, что при определении функции мы не указали никаких параметров, мы все равно можем их передать и получить их значения через arguments.

function sum(a, b, c){ console.log("a =", a); console.log("b =", b); console.log("c =", c); let result = 0; for(const n of arguments) result += n; console.log("result =", result); } sum(6, 4, 5, 8) // 23

Несмотря на то, что функция формально принимает 3 параметра, мы можем передать ей большее количество аргументов и получить их через массив arguments. Консольный вывод программы:

a = 6
b = 4
c = 5
result = 23

Хотя мы можем перебрать объект arguments как массив, тем не менее это не массив. Но, мы можем получить его длину с помощью свойства length и обращаться к переданным в функцию аргументам, используя индекс:

function sum(nums){ let result = 0; // результат функции if(arguments.length >=1) // если передан как минимум один параметр { result = result + arguments[0]; // обращаемся к первому параметру } if(arguments.length >=2) // если передано как минимум два параметра { result = result + arguments[1]; // обращаемся ко второму параметру } console.log("result =", result); } sum(6) // result = 6 sum(6, 5) // result = 11 sum(6, 5, 4) // result = 11 - третий параметр не учитывается

Rest-оператор/параметры (...)

С помощью оператора ... (rest-оператор) также можно передать переменное количество значений:

function sum(...numbers){ let result = 0; for(const n of numbers) result += n; console.log(result); } sum(6, 4, 5) // 15

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

Подобные rest-параметры можно комбинировать с обычными параметрами:

function display(season, ...temps){ console.log(season); for(index in temps){ console.log(temps[index]); } } display("Весна", -2, -3, 4, 2, 5); display("Лето", 20, 23, 31);

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

Консольный вывод:

Весна
-2
-3
4
2
5
Лето
20
23
31

Массив как параметр

Третий способ передачи неопределенного количества данных представляет передача их через параметр-массив. Хотя формально параметр у нас один (количество параметров определено), тем не менее количество конкретных данных в этом массиве неопределенно (как и в случае с массивом arguments):

function sum(numbers){ let result = 0; for(const n of numbers) result += n; console.log("result =", result); } const nums = [6, 4, 5, 8]; sum(nums) // result = 23

Другой пример:

// Собираем все аргументы в массив function sum(...numbers) { return numbers.reduce((total, num) => total + num, 0); } console.log(sum(1, 2, 3)); // 6 console.log(sum(1, 2, 3, 4, 5)); // 15

Функции в качестве параметров

Функции могут выступать в качестве параметров других функций:

function sum(x, y){ return x + y; } function subtract(x, y){ return x - y; } function operation(x, y, func){ const result = func(x, y); console.log(result); } console.log("Sum"); operation(10, 6, sum); // 16 console.log("Subtract"); operation(10, 6, subtract); // 4

Функция operation принимает три параметра: x, y и func. func - представляет функцию, причем на момент определения operation не важно, что это будет за функция. Единственное, что известно, что функция func может принимать два параметра и возвращать значение, которое затем отображается в консоли браузера. Поэтому мы можем определить различные функции (например, функции sum и subtract в данном случае) и передавать их в вызов функции operation.

Деструктуризация параметров

// Объект как параметр function printUser({name, age}) { console.log(`${name}, ${age} лет`); } printUser({name: 'Alice', age: 25}); // "Alice, 25 лет" // Массив как параметр function printCoordinates([x, y]) { console.log(`X: ${x}, Y: ${y}`); } printCoordinates([10, 20]); // "X: 10, Y: 20"

📜 Результат функции

Функция может возвращать результат. Для этого используется оператор return, после которого указывается возвращаемое значение:

function sum (a, b) { const result = a + b; return result; }

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

После получения результата функции мы можем присвоить его какой-либо другой переменной или константе:

function sum (a, b) { return a + b; } let num1 = sum(2, 4); console.log(num1); // 6 const num2 = sum(6, 34); console.log(num2); // 40

Функция может возвратить только одно значение. Если же нам надо возвратить несколько значений, то мы можем возвратить их в виде массива:

function rectangle(width, height){ const perimeter = width *2 + height * 2; const area = width * height; return [perimeter, area]; } const rectangleData = rectangle(20, 30); console.log(rectangleData[0]); // 100 - периметр прямоугольника console.log(rectangleData[1]); // 600 - площадь прямоугольника

В данном случае в функцию rectangle передаются ширина и высота прямоугольника, а внутри функции по этим данным вычисяем периметр и площадь прямоугольника и в виде массива возвращаем из функции.

В качестве альтернативы можно поместить многочисленные возвращаемые данные в один объект:

function rectangle(width, height){ const rectPerimeter = width *2 + height * 2; const rectArea = width * height; return {perimeter: rectPerimeter, area: rectArea}; } const rectangleData = rectangle(20, 30); console.log("Perimeter:", rectangleData.perimeter); // 100 - периметр прямоугольника console.log("Area:", rectangleData.area); // 600 - площадь прямоугольника

Возвращение функции из функции

Одна функция может возвращать другую функцию:

function menu(n){ if(n==1) return function(x, y){ return x + y;} else if(n==2) return function(x, y){ return x - y;} else if(n==3) return function(x, y){ return x * y;} return function(){ return 0;} } const action = menu(1); // выбираем первый пункт - сложение const result = action(2, 5); // выполняем функцию и получаем результат в константу result console.log(result); // 7

В данном случае функция menu() в зависимости от переданного в нее значения возвращает одну из трех функций или пустую функцию, которая просто возвращает число 0.

Далее мы вызываем функцию menu и получаем результат этой функции - другую функцию в константу action.

const action = menu(1);

То есть здесь action будет представлять функцию, которая принимает два параметра и возвращает число. Затем через имя константы мы можем вызвать эту функцию и получить ее результат в константу result:

const result = action(2, 5);

Подобным образом мы можем получить и другие возвращаемые функции:

function menu(n){ if(n==1) return function(x, y){ return x + y;} else if(n==2) return function(x, y){ return x - y;} else if(n==3) return function(x, y){ return x * y;} return function(){ return 0;}; } let action = menu(1); console.log(action(2, 5)); // 7 action = menu(2); console.log(action(2, 5)); // -3 action = menu(3); console.log(action(2, 5)); // 10 action = menu(190); console.log(action(2, 5)); // 0

Аналогичным образом можно возвращать функции по имени:

function sum(x, y){ return x + y;} function subtract(x, y){ return x - y;} function multiply(x, y){ return x * y;} function zero(){ return 0;} function menu(n){ switch(n){ case 1: return sum; case 2: return subtract; case 3: return multiply; default: return zero; } } let action = menu(1); console.log(action(5, 4)); // 9 action = menu(2); console.log(action(5, 4)); // 1 action = menu(3); console.log(action(5, 4)); // 20 action = menu(190); console.log(action(5, 4)); // 0

📜 Стрелочные функции

Стрелочные функции (arrow functions) позоляют сократить определение обычных функций. Стрелочные функции определяются с помощью оператора =>, перед которым в скобках идут параметры функции, а после - собственно тело функции.

(параметры) => действия_функции

Для примера возьмем сначала обычную примитивную функцию, которая выводит сообщение на консоль:

function hello(){ console.log("Hello"); } hello(); // вызываем функцию

Теперь переделаем ее в стрелочную функцию:

const hello = ()=> console.log("Hello"); hello();

В данном случае стрелочная функция присваивается константе hello, через которую затем можно вызвать данную функцию.

Здесь мы не используем параметры, поэтому указываются пустые скобки ()=> console.log("Hello");

Далее через имя переменной мы можем вызвать данную функцию.

Передача параметров

Теперь определим стрелочную функцию, которая принимает один параметр:

const print = (mes)=> console.log(mes); print("Hello Metanit.com"); print("Welcome to JavaScript");

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

Если стрелочная функция имеет только один параметр, то скобки вокруг списка параметров можно опустить:

const print = mes=> console.log(mes); print("Hello Metanit.com"); print("Welcome to JavaScript");

Другой пример - передадим два параметра:

const sum = (x, y)=> console.log("Sum =", x + y); sum(1, 2); // Sum = 3 sum(4, 3); // Sum = 7 sum(103, 2); // Sum = 105

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

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

const sum = (x, y)=> x + y; console.log(sum(1, 2)); // 3 console.log(sum(4, 3)); // 7 console.log(sum(102, 5)); // 107

Другой пример - возвратим отфарматированную строку:

const hello = name => `Hello, ${name}`; console.log(hello("Tom")); // Hello, Tom console.log(hello("Bob")); // Hello, Bob console.log(hello("Frodo Baggins")); // Hello, Frodo Baggins

В данном случае функция hello принимает один параметр name - условное имя и создает на его основе сообщение с приветствием.

Возвращение объекта

Особо следует остановиться на случае, когда стрелочная функция возвращает объект:

const user = (userName, userAge) => ({name: userName, age: userAge}); let tom = user("Tom", 34); let bob = user("Bob", 25); console.log(tom.name, tom.age); // "Tom", 34 console.log(bob.name, bob.age); // "Bob", 25

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

Функция из нескольких инструкций

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

const square = n => { const result = n * n; console.log(result); } square(5); // 25 square(6); // 36

А если надо возвратить результат, применяется оператор return, как в обычной функции:

const square = n => { const result = n * n; return result; } console.log(square(5)); // 25

📜 Область видимости (Scope)

Все переменные и константы в JavaScript имеют определенную область видимости, в пределах которой они могут действовать.

Глобальные переменные

Все переменные и константы, которые объявлены вне функций, являются глобальными:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <script> var a = 5; let b = 8; const c = 9; function displaySum(){ var d = a + b + c; console.log(d); } displaySum(); // 22 </script> </body> </html>

Здесь переменные a и b и константа c являются глобальными. Они доступны из любого места программы.

А вот переменная d глобальной не является, так как она определена внутри функции и видна только в этой функции.

Определение локальной области видимости

Для определения локальной области видимости в JavaScript используются фигурные скобки { }, которые создают блок кода. Этот блок кода может быть безымянным, может быть именнованным, например, функция, либо может представлять условную или циклическую конструкцию. Например, определение переменных в безымянном блоке кода:

{ var a = 5; let b = 8; const c = 9; }

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

Переменные и константы функции

Переменные и константы, определенные внутри функции, видны (то есть могут использоваться) только внутри этой функции:

function print(){ var a = 5; let b = 8; const c = 9; console.log("Function print: a =", a); console.log("Function print: b =", b); console.log("Function print: c =", c); } print(); console.log("Global: a =", a); // Uncaught ReferenceError: a is not defined

Переменные a и b и константа c являются локальными, они существуют только в пределах функции. Вне функции их нельзя использовать, поэтому мы получим следующий консольный вывод:

Function print: a= 5
Function print: b= 8
Function print: c= 9
Uncaught ReferenceError: a is not defined

Здесь мы видим, что при попытке обратиться к переменной a вне функции print(), браузер выводит ошибку. При этом подобное поведение не зависит от того, что это за переменная - var или let, либо это константа. Подобное поведение для всех переменных и констант одинаково.

Локальные переменные в блоках кода, условиях и циклах

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

// безымянный блок { var a = 5; } console.log("a =", a); // a = 5 // условная конструкция if(true){ var b = 6; } console.log("b =", b); // b = 6 // цикл for(let i = 0; i < 5; i++){ var c = 7; } console.log("c =", c); // c = 7

Единственное условие, что блок кода должен срабатывать, чтобы инициализировать переменную. Так, в примере выше условие в конструкции if и в цикле for установлено так, что блок этих конструкций будет выполняться. Однако, что если условие будет иным, и блок не будет выполняться?

if(false){ var b = 6; } console.log("b =", b); // b = undefined // цикл for(let i = 1; i < 0; i++){ var c = 7; } console.log("c =", c); // c = undefined

В таком случае мы опять же сможем обращаться к переменным, только они будут иметь значение undefined.

Переменная let и константы

Теперь посмотрим, как будут вести себя в подобной ситуации переменные, определенные с помощью let:

{ let a = 5; } console.log("a =", a); // Uncaught ReferenceError: a is not defined

В данном случае мы получим ошибку. Мы можем использовать переменные let, определенные внутри блока кода, только внутри этого блока кода.

Тоже самое относится и к константам:

{ const b = 5; } console.log("b =", b); // Uncaught ReferenceError: b is not defined

Скрытие переменных

Что если у нас есть две переменных - одна глобальная, а другая локальная, которые имеют одинаковое имя:

var z = 89; function print(){ var z = 10; console.log(z); // 10 } print(); // 10

В этом случае в функции будет использоваться та переменная z, которая определена непосредственно в функции. То есть локальная переменная скроет глобальную. Однако конкретное поведение при скрытии зависит от того, как определяется переменная.

Скрытие переменной var

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

function displayZ(){ var z = 20; { // Не определяет новую переменную, а изменяет значение переменной z уровня функции var z = 30; console.log("Block:", z); } console.log("Function:", z); } displayZ();

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

Block: 30
Function: 30

Скрытие переменной let

Как писалось выше, оператор let определяет переменную уровня блока кода. То есть каждый блок кода определяет новую область видимости, в которой существует переменная. Вне блока кода, где определена переменная, она не существует. Соответственно мы можем одновременно определить переменную на уровне блока и на уровне функции (в отличие от var):

let z = 10; function displayZ(){ let z = 20; { let z = 30; console.log("Block:", z); } console.log("Function:", z); } displayZ(); console.log("Global:", z);

Здесь внутри функции displayZ определен блок кода, в котором определена переменная z (вместо безымянного блока это мог быть и блок условной конструкции или цикла). Она скрывает глобальную переменную и переменную z, определенную на уровне функции.

И в данном случае мы получим следующий консольный вывод:

Block: 30
Function: 20
Global: 10

Константы

Все, что относится к оператору let, относится и к оператору const, который определяет константы уровня блока кода. Блоки кода задают область видимости констант, а константы, определенные на вложенных блоках кода, скрывают внешние константы с тем же именем:

const z = 10; function displayZ(){ const z = 20; { const z = 30; console.log("Block:", z); // 30 } console.log("Function:", z); // 20 } displayZ(); console.log("Global:", z); // 10

Scope chain / Цепочка областей видимости

При выполнении кода, когда интерпретатор сталкивается с каким-то идентификатором (название переменной, константы, функции), то вначале он ищет опеределение этого идентификатора в текущей области видимости, благодаря чему собственно и работает скрытие переменных и констант. Например,

const z = 10; function displayZ(){ const z = 20; console.log(z); // 20 } displayZ(); // 20

Здесь интепретатор увидит, что в функции displayZ идет обращение к идентификатору z, и будет искать определение этого идентификатора внутри функции displayZ. И поскольку в этой функции есть определение константы const z = 20, то именно эта константа и будет использоваться.

Другой пример:

const z = 10; function displayZ(){ console.log(z); // 10 } displayZ(); // 10

Теперь внутри функции displayZ нет определения идентификатора z, поэтому для его поиска применяется scope chain - интерпретатор обращается к окружающей области видимости и выполняет поиск там. То есть смотри области видимости по цепочке от текущей - к внешним вплоть до глобальной области видимости.

Необъявленные переменные

При определении переменных в JavaScript мы можем не использовать ключевое слово let или var. Например:

<script> { username = "Tom"; } console.log(username); // ошибки нет { console.log(username); // ошибки нет, доступна внутри других блоков кода } </script>

Если мы не используем это ключевое слово let/var при определении переменной в функции, то такая переменная будет глобальной. Например::

function setAge(){ userage = 39; } setAge(); console.log(userage); // 39

Несмотря на то, что вне функции setAge переменная userage нигде не определяется, тем не менее она доступна вне функции во внешнем контексте. Единственное условие - мы вызываем функцию, где определена такая переменная..

Однако если мы не вызовем функцию, переменная будет не определена:

function setAge(){ userage = 39; } // setAge(); Функция НЕ вызывается console.log(userage); // ошибка - Uncaught ReferenceError: userage is not defined

Ту же ошибку мы получили бы, если бы мы не только присваивали значение переменной, но и определяли бы ее как локальную по отношению к функции:

function setAge(){ var userage = 39; } setAge(); console.log(userage); // ошибка - Uncaught ReferenceError: userage is not defined

strict mode

Определение глобальных переменных в функциях может вести к потенциальным ошибкам. Чтобы их избежать используется строгий режим или strict mode. Установить режим strict mode можно двумя способами:

  • добавить выражение "use strict" в начало кода JavaScript, тогда strict mode будет применяться для всего кода
  • добавить выражение "use strict" в начало тела функции, тогда strict mode будет применяться только для этой функции.

Глобальное применение strict mode:

<script> "use strict"; // используем строгий режим username = "Tom"; // Uncaught ReferenceError: username is not defined console.log(username); </script>

В этом случае мы получим ошибку SyntaxError: Unexpected identifier, которая говорит о том, что переменная username не определена.

Аналогичную ошибку мы получим при определении глобальной переменной в функции:

<script> "use strict"; // используем строгий режим function setAge(){ userage = 39; // Uncaught ReferenceError: userage is not defined } setAge(); console.log(userage); </script>

Пример использования строгого режима на уровне функции:

<script> username = "Tom"; // норм console.log(username); // Tom function setAge(){ "use strict"; // используем строгий режим на уровне функции userage = 39; // Uncaught ReferenceError: userage is not defined } setAge(); console.log(userage); </script>

Глобальная и локальная область видимости

// Глобальная переменная let globalVar = 'Я глобальная'; function test() { // Локальная переменная let localVar = 'Я локальная'; console.log(globalVar); // OK - доступна console.log(localVar); // OK - доступна } test(); console.log(globalVar); // OK // console.log(localVar); // Ошибка! localVar не определена

Блочная область видимости (let, const)

if (true) { let blockVar = 'Я в блоке'; const PI = 3.14; console.log(blockVar); // OK } // console.log(blockVar); // Ошибка! // console.log(PI); // Ошибка! // var не имеет блочной области видимости if (true) { var oldVar = 'Я доступна снаружи'; } console.log(oldVar); // OK (но это плохо!)

📜 Замыкания (Closures)

Замыкание (closure) представляют собой конструкцию, когда функция, созданная в одной области видимости, запоминает свое лексическое окружение даже в том случае, когда она выполняет вне своей области видимости.

Для организации замыкания необходимы три компонента:

  • внешняя функция, которая определяет некоторую область видимости и в которой определены некоторые переменные - лексическое окружение
  • переменные (лексическое окружение), которые определены во внешней функции
  • вложенная функция, которая использует эти переменные
function outer(){ // внешняя функция let n; // некоторая переменная return inner(){ // вложенная функция // действия с переменной n } }

Грубо говоря, функция inner и представляет в данном случае замыкание, которое запонимание свое лексического окружение - переменную х.

Рассмотрим замыкания на простейшем примере:

function outer(){ let x = 5; function inner(){ x++; console.log(x); }; return inner; } const fn = outer(); // fn = inner, так как функция outer возвращает функцию inner // вызываем внутреннюю функцию inner fn(); // 6 fn(); // 7 fn(); // 8

Здесь функция outer задает область видимости, в которой определены внутренняя функция inner и переменная x. Переменная x представляет лексическое окружение для функции inner. В самой функции inner инкрементируем переменную x и выводим ее значение на консоль. В конце функция outer возвращает функцию inner.

Далее вызываем функцию outer:

const fn = outer();

Поскольку функция outer возвращает функцию inner, то константа fn будет хранить ссылку на функцию inner. При этом эта функция запомнила свое окружение - то есть внешнюю переменную x.

Далее мы фактически три раза вызываем функцию inner, и мы видим, что переменная x, которая определена вне функции inner, инкрементируется:

fn(); // 6 fn(); // 7 fn(); // 8

То есть несмотря на то, что переменная x определена вне функции inner, эта функция запомнила свое окружение и может его использовать, несомотря на то, что она вызывается вне функции outer, в которой была определена. В этом и суть замыканий.

Причем для каждой копии замыкания определяется своя копия лексического окружения:

// определяем объект-пространство имен function outer(){ let x = 5; function inner(){ x++; console.log(x); }; return inner; } const fn1 = outer(); const fn2 = outer(); fn1(); // 6 fn1(); // 7 fn2(); // 6 fn2(); // 7

Здесь видно, что для fn1 и fn2 есть своя копия переменной х, которой они манипулируют независимо друг от друга.

Рассмотрим еще один пример:

function multiply(n){ let x = n; return function(m){ return x * m;}; } const fn1 = multiply(5); const result1 = fn1(6); // 30 console.log(result1); // 30 const fn2= multiply(4); const result2 = fn2(6); // 24 console.log(result2); // 24

Итак, здесь вызов функции multiply() приводит к вызову другой внутренней функции. Внутренняя же функция:

function(m){ return x * m;};

запоминает окружение, в котором она была создана, в частности, значение переменной x.

В итоге при вызове функции multiply определяется константа fn1, которая и представляет собой замыкание, то есть объединяет две вещи: функцию и окружение, в котором функция была создана. Окружение состоит из любой локальной переменной, которая была в области действия функции multiply во время создания замыкания.

То есть fn1 — это замыкание, которое содержит и внутреннюю функцию function(m){ return x * m;}, и переменную x, которая существовала во время создания замыкания.

При создании двух замыканий: fn1 и fn2, для каждого из этих замыканий создается свое окружение.

При этом важно не запутаться в параметрах. При определении замыкания:

const fn1 = multiply(5);

Число 5 передается для параметра n функции multiply.

При вызове внутренней функции:

const result1 = fn1(6);

Число 6 передается для параметра m во внутреннюю функцию function(m){ return x * m;};.

Также мы можем использовать другой вариант для вызова замыкания:

function multiply(n){ let x = n; return function(m){ return x * m;}; } const result = multiply(5)(6); // 30 console.log(result);

Еще пример:

// Функция возвращает функцию function makeCounter() { let count = 0; // Приватная переменная return function() { count++; return count; }; } const counter1 = makeCounter(); console.log(counter1()); // 1 console.log(counter1()); // 2 console.log(counter1()); // 3 const counter2 = makeCounter(); console.log(counter2()); // 1 (новый счётчик)
💡 Замыкание

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

Замыкания и объектно-ориентированное программирование

Хотя объектно-ориентированное программирование будет рассматриваться далее, тем не менее нельзя не заметить, что замыкания по сути явились предтечей объектно-ориентированного программирования. И использование замыканий в некоторой степени позволяет имитировать создание объектов и работу с ними. Например, рассмотрим следующий код:

function person(name, age){ console.log("Person", name, "created"); function print(){ console.log("Person ", name, " (" +age +")"); } function work(){ console.log("Person ", name, " works"); } function incrementAge(value){ age = age + value; } return [print, work, incrementAge]; } const tom = person("Tom", 39); tom[0](); // print tom[1](); // work tom[2](1); // incrementAge tom[0](); // print

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

Здесь функция person выступает своего рода конструктором объекта person - условного человека, а ее параметры name и age принимают извне соответственно имя и возраст человека. Три вложенных функции - print, work, incrementAge могут обращаться к своему лексическому окружению - параметрам функции person.

Чтобы к этим функциям можно было обратиться извне, функция person возвращает вложенные функции в виде массива:

return [print, work, incrementAge];

Далее мы можем вызвать функцию person, передав в нее некоторые параметры, и получить ее результат - условного человека:

const tom = person("Tom", 39);

Что такое в данном случае константа tom? По сути это возвращенный массив из трех функций, котоорые позволяют манипулировать человеком tom. Соответственно, обратившись к определенному элементу в массиве, мы можем вызвать соответствующую функцию:

tom[0](); // print

То есть выражение tom[0] возвращает функцию print, а выражение tom[0]() вызывает ее. В итоге в данном случае вывод консоли браузера будет следующим:

Person Tom created
Person  Tom  (39)
Person  Tom  works
Person  Tom  (40)

IIFE (Immediately Invoked Function Expression)

Самовызывающиеся функции

Обычно определение функции отделяется от ее вызова: сначала мы определяем функцию, а потом вызываем. Но это необязательно. Мы также можем создать такие функции, которые будут вызываться сразу при определении. Такие функции еще называют Immediately Invoked Function Expression или IIFE. Подобные функции заключаются в скобки, и после определения функции идет в скобках передача параметров:

// самозывающаяся функция (function(){ console.log("Привет мир"); }());

Здесь всю это небольшую программу можно разбить на несколько частей. В скобках идет определение анонимной функции:

function(){ console.log("Привет мир"); }

Это обычная функция, которая выводит некоторую строку. Но также в скобках после определения функции идут пустые скобки:

function(){ console.log("Привет мир"); }()

Эти скобки представляют те же скобки, которые идут при вызове функции и в которые помещаются значения для параметров функции. Однако наша анонимная функция не имеет параметров, поэтому и скобки пустые. То есть здесь фактически идет вызов функции сразу после ее определения. И вся эта конструкция обертывается в скобки.

В итоге мы определяем анонимную функцию и сразу же ее вызываем.

Подобным образом можно создать и вызывать функцию, которые принимают параметры:

(function (a, b){ const result = a + b; console.log(`${a} + ${b} = ${result}`); }(4, 5));

Здесь функция принимает два параметра, значения которых складываются, а результат выводится на консоль. И сразу после определения функции мы передаем ее параметрам два числа - 4 и 5. То есть, при выполнении программы будет определяться и сразу же выполняться функция, которая подсчитает сумму чисел 4 и 5.

// Функция, которая вызывается сразу после создания (function() { console.log('Я выполнилась сразу!'); })(); // С параметрами (function(name) { console.log('Привет, ' + name + '!'); })('Alice'); // Arrow IIFE (() => { console.log('Arrow IIFE!'); })();

Функциональный стиль

Замыкания и IIFE-функции упрощают написание программ в функциональном стиле. Так, рассмотрим следующий пример:

console.log( ((x,y) => ( ((proc2) =>( ((proc1)=>proc1(5,30))((x,y) => [x, proc2(), y]) ))(()=>x + y) ))(10, 15) ); // [5, 25, 30]

Рассмотрим, что здесь делается. Прежде всего в функции console.log() выводится результат самовызывающейся функции:

((x,y) => ( ((proc2) =>( ((proc1)=>proc1(5,30))((x,y) => [x, proc2(), y]) ))(()=>x + y) ))(10, 15)

Эта первая IIFE-функция принимает параметры x и y, которым передаются числа 10 и 15.

Эта функция, в свою очередь использует вложенную функцию и возвращает ее результат:

((proc2) =>( ((proc1) => proc1(5,30))((x,y) => [x, proc2(), y]) ))(()=>x + y)

Эта вторая IIFE-функция. Она в качестве параметра proc2 принимет другую функцию. В данном случае это функция ()=>x + y, которая возвращает сумму значений. Поскольку для вложенной функции лексическое окружение представлено параметрами x и y внешней функции, то функция-параметр proc2 получает эти значения и возвращает их сумму - 10 + 15=25.

Однако вложенная функция возвращает результат своей вложенной функции:

((proc1) => proc1(5,30))((x,y) => [x, proc2(), y])

Эта третья функция также является IIFE-функцией. В качестве параметр proc1 она принимает также некоторую функцию-коллбек. И в эту функцию proc1 передаются числа 5, 30. И тут идет наиболее сложный момент - далее в скобках передается сама функция proc1:

(x,y) => [x, proc2(), y]

Она определяет параметры x и y и возвращает в качестве результата массив из трех чисел. Прежде всего первое и третье числа - х и y. Эти параметры не стоит путать с параметрами х и y первой IIFE-функции самого верхнего уровня:

((x,y) => (.....))(10, 15)

Итак, самая третья функция возвращает массив [x, proc2(), y]. x и y представляют параметры этой функции. Второе значение в массиве представляет результат вызова функции proc2. Вспомним, что это за функция, и для этого поднимемся на уровень выше ко второй функции:

((proc2) =>( ................. ))(()=>x + y)

proc2 возвращает сумму значений x и y. Для этой второй функции лексическое окружение определяется первой IIFE-функцией, которая принимает параметры x и y, поэтому в proc2 передаются числа 10 и 15, соответственно функция возвращает 25. И, таким образом, результат будет представлять массив [5, 25, 30].

💡 Зачем нужен IIFE?

IIFE используется для создания изолированной области видимости, чтобы не загрязнять глобальное пространство имён.

📜 Рекурсия

Среди функций отдельно можно выделить рекурсивные функции. Их суть состоит в том, что функция вызывает саму себя.

Например, рассмотрим функцию, определяющую факториал числа:

// Факториал function factorial(n) { if (n === 0 || n === 1) { return 1; // Базовый случай } return n * factorial(n - 1); // Рекурсивный вызов } console.log(factorial(5)); // 120 (5 * 4 * 3 * 2 * 1) // Числа Фибоначчи function fibonacci(n) { if (n <= 1) return n; return fibonacci(n - 1) + fibonacci(n - 2); } console.log(fibonacci(7)); // 13

Функция factorial() возвращает значение 1, если параметр n равен 1, либо возвращает результат опять же функции factorial, то в нее передается значение n - 1. Например, при передаче числа 4, у нас образуется следующая цепочка вызовов:

result = 4 * factorial(3); result = 4 * 3 * factorial(2); result = 4 * 3 * 2 * factorial(1); result = 4 * 3 * 2 * 1; // 24
⚠️ Осторожно с рекурсией!

Рекурсия может привести к переполнению стека (stack overflow), если не задан правильный базовый случай или глубина рекурсии слишком велика.

📜 Переопределение функций

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

function print(){ console.log("Доброе утро"); print = function(){ console.log("Добрый день"); } } print(); // Доброе утро print(); // Добрый день

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

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

function print(){ console.log("Доброе утро"); print = function(){ console.log("Добрый день"); } } // присвоение ссылки на функцию до переопределения const printMessage = print; print(); // Доброе утро print(); // Добрый день printMessage(); // Доброе утро printMessage(); // Доброе утро

Здесь переменная printMessage получает ссылку на функцию print до ее переопределения. Поэтому при вызове printMessage() будет вызываться непереопределенная версия функции print.

Но допустим, мы определили переменную printMessage уже после вызова функции print:

print(); // Доброе утро print(); // Добрый день const printMessage = print; printMessage(); // Добрый день printMessage(); // Добрый день

В этом случае переменная printMessage будет указывать на переопределенную версию функции print.

📜 Callback функции

// Функция принимает другую функцию как аргумент function processArray(arr, callback) { for (let i = 0; i < arr.length; i++) { callback(arr[i]); } } // Передаём callback processArray([1, 2, 3], function(item) { console.log(item * 2); }); // Вывод: 2, 4, 6 // С arrow function processArray([1, 2, 3], item => console.log(item * 2));

📜 Методы объектов

const user = { name: 'Alice', age: 25, // Метод объекта greet: function() { console.log('Привет, я ' + this.name); }, // Сокращённая запись (ES6) sayAge() { console.log('Мне ' + this.age + ' лет'); } }; user.greet(); // "Привет, я Alice" user.sayAge(); // "Мне 25 лет"

📜 Чистые функции (Pure Functions)

// Чистая функция - не изменяет внешнее состояние function pureAdd(a, b) { return a + b; // Только вычисление } // Нечистая функция - имеет побочные эффекты let total = 0; function impureAdd(value) { total += value; // Изменяет внешнюю переменную return total; }
✅ Преимущества чистых функций
  • Предсказуемый результат (одинаковый вход → одинаковый выход)
  • Легко тестировать
  • Легко отлаживать
  • Можно безопасно переиспользовать

📜 Hoisting

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

То есть как будто происходит поднятие кода с определением переменных и функций вверх до их непосредственного использования. Поднятие на английский переводится как hoisting, сообственно поэтому данный процесс так и называется.

Переменные var

var-переменные, которые попадают под hoisting, по умолчанию получают значение undefined. Например, возьмем следующий простейший код:

console.log(foo);

Его выполнение вызовет ошибку ReferenceError: foo is not defined

Добавим определение переменной:

console.log(foo); // undefined var foo = "Tom";

В этом случае консоль выведет значение"undefined". При первом проходе компилятор узнает про существование переменной foo. Она получает значение undefined. При втором проходе вызывается метод console.log(foo).

Возьмем другой пример:

var c = a * b; var a = 7; var b = 3; console.log(c); // NaN

Здесь та же ситуация. Переменные a и b используются до опеределения. По умолчанию им присваиваются значения undefined. А если умножить undefined на undefined, то получим Not a Number (NaN).

Все то же самое относится и к использованию функций. Мы можем сначала вызвать функцию, а потом уже ее определить:

display(); function display(){ console.log("Hello Hoisting"); }

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

Но от этой ситуации надо отличать тот случай, когда функция определяется в виде переменной:

display(); var display = function (){ console.log("Hello Hoisting"); }

В данном случае мы получим ошибку TypeError: display is not a function. При первом проходе компилятор также получит переменную display и присвоет ей значение undefined. При втором проходе, когда надо будет вызывать функцию, на которую будет ссылаться эта переменная, компилятор увидит, что вызывать то нечего: переменная display пока еще равна undefined. И будет выброшена ошибка.

let-переменные и константы

Процесс hoisting для let-переменных и констант будет отличаться: в отличие от var-переменных константам и let-переменным при хостинге не присваивается начальное значение. Итак, выше мы видели, что если мы используем var-переменные до объявления, то они получают по умолчанию значение undefined. Теперь посмотрим, что будет с let-переменной:

console.log(foo); // Uncaught ReferenceError: Cannot access 'foo' before initialization let foo = "Tom"; console.log(foo); // не будет выполняться

В данном случае в первой строке мы получим ошибку

Uncaught ReferenceError: Cannot access 'foo' before initialization

От этой ситуации следует отличать момент, когда let-переменная объявлена, но не инициализирована:

let foo; // по умолчанию foo = undefined console.log(foo); // undefined foo = "Tom"; console.log(foo); // Tom

Неинициализированная let-переменная по умолчанию будет иметь значение undefined, и в данном случае ошибки не будет.

То же самое касается и констант:

console.log(foo); // Uncaught ReferenceError: Cannot access 'foo' before initialization const foo = "Tom"; console.log(foo); // не будет выполняться

📜 Передача параметров по значению и по ссылке

Передача параметров по значению

Строки, числа, логические значения передаются в функцию по значению. Иными словами при передаче значения в функцию, эта функция получает копию данного значения. Рассмотрим, что это значит в практическом плане:

function change(x){ x = 2 * x; console.log("x in change:", x); } let n = 10; console.log("n before change:", n); // n before change: 10 change(n); // x in change: 20 console.log("n after change:", n); // n after change: 10

Функция change получает некоторое число и увеличивает его в два раза. При вызове функции change ей передается число n. Однако после вызова функции мы видим, что число n не изменилось, хотя в самой функции произошло увеличение значения параметра. Потому что при вызове функция change получает копию значения переменной n. И любые изменения с этой копией никак не затрагивают саму переменную n. В итоге мы получим следующий вывод в консоли браузера

n before change: 10
x in change: 20
n after change: 10

Передача по ссылке

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

let bob ={ name: "Bob" };

Переменная bob формально хранит объект, в котором определено одно поле name. Фактически же переменная bob хранит ссылку на объект, который расположен где-то в памяти.

И ссылочные типы - объекты и массивы передаются в функцию по ссылке. То есть функция получает копию ссылки на объект, а не копию самого объекта.

function change(user){ user.name = "Tom"; } let bob ={ name: "Bob" }; console.log("before change:", bob.name); // Bob change(bob); console.log("after change:", bob.name); // Tom

В данном случае функция change получает некоторый объект и меняет его свойство name. При вызове этой функции в нее передается значение переменной bob:

change(bob);

Но, поскольку переменная bob представляет объект и хранит ссылку на некоторый объект в памяти, то функция change получае копию этой ссылки, которая указывает на тот же объект в памяти, что и переменная bob.

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

before change: Bob
after change: Tom

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

function change(user){ // полная переустановка объекта user = { name: "Tom" }; } let bob ={ name: "Bob" }; console.log("before change:", bob.name); // Bob change(bob); console.log("after change:", bob.name); // Bob

Почему здесь данные не изменяются? Потому что, как писалось выше, функция получает копию ссылки. То есть при передачи в функцию параметру user значения переменной bob:

change(bob);

Переменная bob и параметр user представляют две разные ссылки, но которые указывают на один и тот же объект.

При присвоении параметру в функции другого объекта:

user = { name: "Tom" };

ссылка user начиначет указывать на другой объект в памяти. То есть после этого bob и user - две разные ссылки, которые указывают на два разных объекта в памяти.

То же самое касается массивов:

function change(array){ array[0] = 8; } function changeFull(array){ array = [9, 8, 7]; } let numbers = [1, 2, 3]; console.log("before change:", numbers); // [1, 2, 3] change(numbers); console.log("after change:", numbers); // [8, 2, 3] changeFull(numbers); console.log("after changeFull:", numbers); // [8, 2, 3]

Глава 4. Объектно-ориентированное программирование

Объектно-ориентированное программирование на сегодняшний день является одной из господствующих парадигм в разработке приложений, и в JavaScript мы также можем использовать все преимущества ООП. В то же время применительно к JavaScript объектно-ориентированное программирование имеет некоторые особенности.

📜 Объекты

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

Для работы с подобными структурами в JavaScript используются объекты. Каждый объект может хранить свойства, которые описывают его состояние, и методы, которые описывают его поведение

Создание объектов

Первый способ заключается в использовании конструктора Object:

const user = new Object();

В данном случае объект называется user.

Выражение new Object() представляет вызов конструктора - функции, создающей новый объект. Для вызова конструктора применяется оператор new. Вызов конструктора фактически напоминает вызов обычной функции.

Второй способ создания объекта представляет использование фигурных скобок {} - (литеральная нотация):

На сегодняшний день более распространенным является второй способ.

// Литерал объекта const user = { name: 'Alice', age: 25, isAdmin: false }; // Через конструктор Object const person = new Object(); person.name = 'Bob'; person.age = 30; // Object.create() const proto = { greet() { console.log('Hello!'); } }; const obj = Object.create(proto);

Доступ к свойствам

После создания объекта мы можем определить в нем свойства. Чтобы определить свойство, надо после названия объекта через точку указать имя свойства и присвоить ему значение:

const user = {}; user.name = "Tom"; user.age = 26;

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

console.log(user.name); console.log(user.age);

Также можно определить свойства при определении объекта:

const user = { name: "Tom", age: 26 };

В этом случае для присвоения значения свойству используется символ двоеточия, а после определения свойства ставится запятая (а не точка с запятой).

Кроме того, доступен сокращенный способ определения свойств:

const name = "Tom"; const age = 34; const user = {name, age}; console.log(user.name); // Tom console.log(user.age); // 34

В данном случае названия переменных также являются и названиями свойств объекта. И таким образом можно создавать более сложные конструкции:

const name = "Tom"; const age = 34; const user = {name, age}; const teacher = {user, course: "JavaScript"}; console.log(teacher.user); // {name: "Tom", age: 34} console.log(teacher.course); // JavaScript

Еще пример:

const user = { name: 'Alice', age: 25 }; // Точечная нотация console.log(user.name); // "Alice" // Квадратные скобки console.log(user['age']); // 25 // Динамический ключ const key = 'name'; console.log(user[key]); // "Alice"

Добавление и удаление свойств

const user = { name: 'Alice' }; // Добавление user.age = 25; user['isAdmin'] = false; // Удаление delete user.age; // Проверка наличия свойства console.log('name' in user); // true console.log('age' in user); // false // hasOwnProperty (не проверяет прототип) console.log(user.hasOwnProperty('name')); // true

📜 Методы объектов

Методы объекта определяют его поведение или действия, которые он производит. Методы представляют собой функции. Например, определим метод, который бы выводил имя и возраст человека:

const user = {}; user.name = "Tom"; user.age = 26; user.display = function(){ console.log(user.name); console.log(user.age); }; // вызов метода user.display();

Как и в случае с функциями методы сначала определяются, а потом уже вызываются.

Также методы могут определяться непосредственно при определении объекта:

const user = { name: "Tom", age: 26, display: function(){ console.log(this.name); console.log(this.age); } };

Как и в случае со свойствами, методу присваивается ссылка на функцию с помощью знака двоеточия.

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

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

let user = { name: "Tom", age: 26, display(){ console.log(this.name, this.age); }, move(place){ console.log(this.name, "goes to", place); } }; user.display(); // Tom 26 user.move("the shop"); // Tom goes to the shop

Другой пример:

const user = { name: 'Alice', age: 25, // Метод greet() { console.log(`Привет, я ${this.name}`); }, // Метод с параметром celebrate(years) { this.age += years; console.log(`Мне теперь ${this.age}`); } }; user.greet(); // "Привет, я Alice" user.celebrate(1); // "Мне теперь 26"

📜 Ключевое слово "this" в JavaScript

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

const user = { name: 'Alice', regularFunction: function() { console.log(this.name); // this = user }, arrowFunction: () => { console.log(this.name); // this = window/undefined } }; user.regularFunction(); // "Alice" user.arrowFunction(); // undefined (this не связан с объектом)
⚠️ Arrow functions и this

Стрелочные функции не имеют собственного this. Они берут this из внешнего контекста. Не используйте их как методы объектов!

Поведение ключевого слова this зависит от контекста, в котором оно используется, и от того, в каком режиме оно используется - строгом или нестрогом.

Глобальный контекст и объект globalThis

В глобальном контексте this ссылается на глобальный объект. Что такое "глобальный объект" в JavaScript? Это зависит от среды, в которой выполняется код. Так, в веб-браузере this представляет объект window - объект, который представляет окно браузера. В среде Node.js this представляет объект global. А для веб-воркеров this представляет объект self

Например, в веб-браузере при выполнении следующего кода:

console.log(this);

мы получим консольный вывод вроде следующего

Window {window: Window, self: Window, document: document, name: "", location: Location, …}

В стандарт ES2020 было добавлено определение объекта globalThis, который позволяет ссылаться на глобальный конекст вне зависимости, в какой среде и в какой ситуации выполняется код:

console.log(globalThis);

Контекст функции

В пределах функции this ссылается на внешний контекст. Для функций, определенных в глобальном контексте, - это объект globalThis. Например:

function foo(){ var bar = "local"; console.log(this.bar); } var bar = "global"; foo(); // global

Если бы мы не использовали бы this, то обращение шло бы к локальной переменной, определенной внутри функции.

function foo(){ var bar = "local"; console.log(bar); } var bar = "global"; foo(); // local

Но если бы мы использовали строгий режим (strict mode), то this в этом случае имело бы значение undefined:

"use strict"; function foo(){ var bar = "local"; console.log(this.bar); } var bar = "global"; foo(); // ошибка - this - undefined

Контекст объекта

В контексте объекта, в том числе в его методах, ключевое слово this ссылается на этот же объект:

const obj = { bar: "object", foo: function(){ console.log(this.bar); } } var bar = "global"; obj.foo(); // object

Динамическое определение контекста

Код функции всегда использует в качестве this внешний контекст, в которым этот код вызывается (именно вызывается, а не определяется). Рассмотрим более сложный пример:

function foo(){ var bar = "foo_bar"; console.log(this.bar); } const obj1 = {bar:"obj1_bar", foo: foo}; const obj2 = {bar:"obj2_bar", foo: foo}; var bar = "global_bar"; foo(); // global_bar obj1.foo(); // obj1_bar obj2.foo(); // obj2_bar

Здесь определена глобальная переменная bar ("global_bar"). И также в функции foo определена локальная переменная bar ("foo_bar"). Значение какой переменной будет выводиться в функции foo? Функция foo использует определение переменной, которое определено во внешнем контексте. Для функции foo по умолчанию это глобальный контекст, поэтому она выводит значение глобальной переменной (так как данный скрипт запускается в нестрогом режиме, а значит ключевое слово this в функции foo ссылается на внешний контекст).

Иначе дело обстоит с объектами. Они определяют свой собственный контекст, в котором существует свое свойство bar. И при вызове метода foo внешним контекстом по отношению к функции будет контекст объектов obj1 и obj2.

Подобное поведение может привести к некоторому непонимаю в отдельных случаях. Так, рассмотрим другую ситуацию:

var bar = "global_bar"; const obj1 = { bar: "obj1_bar", foo: function(){ console.log(this.bar); // bar = "obj1_bar" } } const obj2 = {bar: "obj2_bar", foo: obj1.foo}; // bar = "obj2_bar" const foo = obj1.foo; // bar = "global_bar" obj1.foo(); // obj1_bar obj2.foo(); // obj2_bar foo(); // global_bar

Здесь в объекте obj1 определена функция foo:

const obj1 = { bar: "obj1_bar", foo: function(){ console.log(this.bar); // bar = "obj1_bar" } }

Эта функция foo будет брать значение для this.bar из внешнего контекста - из объекта obj1, соответственно this.bar = "obj1_bar".

Объект obj2 использует функцию foo из объекта obj1:

const obj2 = {bar: "obj2_bar", foo: obj1.foo};

Однако функция obj1.foo также будет искать значение для this.bar опять же во внешнем котексте, а здесь это объект obj2. А в объекте obj2 это значение равно "obj2_bar".

То же самое с глобальной переменной foo, которая ссылается на ту же функцию, что и метод obj1.foo:

const foo = obj1.foo;

В этом случае также будет происходить поиск значения для this.bar, но теперь в контексте функции foo. А это глобальный контекст, где определена переменная var bar = "global_bar".

Контекст во вложенных функциях

Если мы вызываем функцию из другой функции, вызываемая функция также будет использовать внешний контекст:

var bar = "global bar"; function foo(){ var bar = "foo bar"; function moo(){ console.log(this.bar); } moo(); } foo(); // global bar

Здесь функция foo() в качестве this.bar использует значение переменной bar из внешнего контекста, то есть значение глобальной переменной bar. Функция moo() также в качестве this.bar использует значение переменной bar из внешнего контекста, то есть this.bar для внешней функции foo, которое в свою очередь представляет значение глобальной переменной bar. Поэтому в итоге консоль выведет "global bar", а не "foo bar".

Явная привязка

С помощью методов call() и apply() можно задать явную привязку функции к определенному контексту:

function foo(){ console.log(this.bar); } var obj = {bar: "obj_bar"} var bar = "global_bar"; foo(); // global_bar foo.apply(obj); // obj_bar // или // foo.call(obj);

Во втором случае функция foo привязывается к объекту obj, который и определяет ее контекст. Поэтому во втором случае консоль выведет "obj_bar".

Метод bind

Метод f.bind(o) позволяет создать новую функцию с тем же телом и областью видимости, что и функция f, но с привязкой к объекту o:

function foo(){ console.log(this.bar); } const obj = {bar: "object"} var bar = "global"; foo(); // global const func = foo.bind(obj); func(); // object

this и стрелочные функции

В стрелочных функциях в качестве this используется контекст окружения, а не само окружение, в котором определена стрелочная функция. Рассмотрим следующий пример:

const person = { name: "Tom", say:()=> console.log(`Меня зовут ${this.name}`) } person.say(); // Меня зовут

Здесь стрелочная функция say() обращается к некому свойству this.name, но что здесь представляет this? Для внешнего контекста, в котором определена стрелочная функция - то есть для контекста объекта person this представляет глобальный объект (объект окна браузера). Однако глобальной переменной name не определено, поэтому на консоль будет выведено:

Меня зовут

Теперь немного изменим пример:

const person = { name: "Tom", hello(){ console.log("Привет"); let say = ()=> console.log(`Меня зовут ${this.name}`); say(); } } person.hello();

Теперь стрелочная функция определена в методе hello(). this для этого метода представляет текущий объект person, где определен данный метод. Поэтому и в стрелочной функции this будет представлять объект person, а this.name - свойство name этого объекта. Поэтому при выполнении программы мы получим:

Привет
Меня зовут Tom

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

const school ={ title: "Oxford", courses: ["JavaScript", "TypeScript", "Java", "Go"], printCourses(){ this.courses.forEach(function(course){ console.log(this.title, course); }) } } school.printCourses();

Функция printCourses проходит по всем курсам из массива и при их выводе предваряет их значением свойства title. Однако на консоли при запуске программы мы увидим следующее:

undefined "JavaScript"
undefined "TypeScript"
undefined "Java"
undefined "Go"

Мы видим, что значение this.title не определено, так как this как контекст объекта замещается глобальным контекстом. В этом случае нам надо передать подобное значение this.title или весь контекст объекта.

const school ={ title: "Oxford", courses: ["JavaScript", "TypeScript", "Java", "Go"], printCourses(){ const that = this; this.courses.forEach(function(course){ console.log(that.title, course); }) } } school.printCourses();

Стрелочные функции также позволяют решить данную проблему:

const school ={ title: "Oxford", courses: ["JavaScript", "TypeScript", "Java", "Go"], printCourses(){ this.courses.forEach((course)=>console.log(this.title, course)) } } school.printCourses();

Контекстом для стрелочной функции в данном случае будет выступать контекст объекта school. Соответственно, нам не надо определять дополнительные переменые для передачи данных в функцию.

📜 Синтаксис массивов

Существует также альтернативный способ определения свойств и методов с помощью синтаксиса массивов:

const user = {}; user["name"] = "Tom"; user["age"] = 26; user["display"] = function(){ console.log(user.name); console.log(user.age); }; // вызов метода user["display"]();

Название каждого свойства или метода заключается в кавычки и в квадратные скобки, затем им также присваивается значение. Например, user["age"] = 26.

При обращении к этим свойствам и методам можно использовать либо нотацию точки (user.name), либо обращаться так: user["name"]

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

const user = { ["name"]: "Tom", ["age"]: 26, ["display"]: function(){ console.log(user.name); console.log(user.age); } }; user["display"]();

Строки в качестве свойств и методов

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

const user = { "name": "Tom", "age": 26, "display": function(){ console.log(user.name); console.log(user.age); } }; // вызов метода user.display();

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

const user = { name: "Tom", age: 26, "full name": "Tom Johns", "display info": function(){ console.log(user.name); console.log(user.age); } }; console.log(user["full name"]); user["display info"]();

Только в этом случае для обращении к подобным свойствам и методам мы должны использовать синтаксис массивов.

Динамическое определение имен свойств и методов

Синтаксис массивов открывает нам другую возможность - определение имени свойства вне объекта:

const prop1 = "name"; const prop2 = "age"; const tom = { [prop1]: "Tom", [prop2]: 37 }; console.log(tom); // {name: "Tom", age: 37} console.log(tom.name); // Tom console.log(tom["age"]); // 37

Благодая этому, например, можно динамически создавать объекты с произвольными названиями свойств:

function createObject(propName, propValue){ return { [propName]: propValue, print(){ console.log(`${propName}: ${propValue}`); } }; } const person = createObject("name", "Tom"); person.print(); // name: Tom const book = createObject("title", "JavaScript Reference"); book.print(); // title: JavaScript Reference

Удаление свойств

Выше мы посмотрели, как можно динамически добавлять новые свойства к объекту. Однако также мы можем удалять свойства и методы с помощью оператора delete. И как и в случае с добавлением мы можем удалять свойства двумя способами.

Первый способ - использование нотации точки:

delete объект.свойство

Либо использовать синтаксис массивов:

delete объект["свойство"]

Например, удалим свойство:

let user = {}; user.name = "Tom"; user.age = 26; user.display = function(){ console.log(user.name); console.log(user.age); }; console.log(user.name); // Tom delete user.name; // удаляем свойство // альтернативный вариант // delete user["name"]; console.log(user.name); // undefined

После удаления свойство будет не определено, поэтому при попытке обращения к нему, программа вернет значение undefined.

Создание объекта из переменных и констант

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

function getSalary(status){ if(status==="senior") return 1500; else return 500; } const name = "Tom"; const age = 37; const person = { name: name, age: age, salary: getSalary()}; console.log(person); // {name: "Tom", age: 37, salary: 500}

Но если названия констант/переменных совпадает с названиями свойств, то можно сократить передачу значений:

const name = "Tom"; const age = 37; const salary = 500; const person = { name, age, salary}; console.log(person); // {name: "Tom", age: 37, salary: 500}

В данном случае объект person автоматически получит свойства, названия которых будут соответствовать названиям констант, а в качестве значений иметь значения этих констант.

То же самое относится к передаче функций методам объекта:

function display(){ console.log(this.name, this.age); } const move = function(place){ console.log(this.name, "goes to", place)}; const name = "Tom"; const age = 37; const salary = 500; const person = { name, age, salary, display, move}; person.display(); // Tom 37 person.move("cinema"); // Tom goes to cinema

В данном случае объект person имеет два метода, которые соответствуют переданным в объект функциям - display() и move(). Стоит отметить, что при такой передаче функций методам объекта, мы по прежнему можем использовать в этих функциях ключевое слово this для обращения к функциональности объекта. Однако стоит быть осторожным при передаче лямбд-выражений, поскольку для глобальных лямбд-выражений this будет представлять объект окна браузера:

const move = (place)=>{ console.log(this.name, "goes to", place); console.log(this);}; const name = "Tom"; const person = { name, move}; person.move("cinema"); // goes to cinema

Фукция Object.fromEntries()

С помощью функции Object.fromEntries() можно создать объект из набора пар ключ-значение, где ключ потом будет представляет название свойства. Например, создадим объект из массивов:

const personData = [ ["name", "Tom"], ["age", 37]]; const person = Object.fromEntries(personData); console.log(person); // {name: "Tom", age: 37} console.log(person.name); // Tom

Здесь объект создается из массива personData, который содержит два подмассива. Каждый подмассив содержит два элемента и фактически представляет пару ключ-значение. Первый элемент представляет ключ, а второй - значение.

📜 Вложенные объекты и массивы в объектах

Вложенные объекты

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

const country = { name: "Германия", language: "немецкий", capital:{ name: "Берлин", population: 3375000, year: 1237 } }; console.log("Столица:", country.capital.name); // Берлин console.log("Население:", country["capital"]["population"]); // 3375000 console.log("Год основания:", country.capital["year"]); // 1237

Консольный вывод:

Столица: Берлин
Население: 3375000
Год основания: 1237

Для доступа к свойствам таких вложенных объектов мы можем использовать:

  • стандартную нотацию точки
    country.capital.name
  • Либо обращаться к ним как к элементам массивов:
    country["capital"]["population"]
  • Также допустим смешанный вид обращения:

    country.capital["year"]

Массивы в качестве свойств

В качестве свойств также могут использоваться массивы, в том числе массивы других объектов:

const country = { name: "Швейцария", languages: ["немецкий", "французский", "итальянский"], capital:{ name: "Берн", population: 126598 }, cities: [ { name: "Цюрих", population: 378884}, { name: "Женева", population: 188634}, { name: "Базель", population: 164937} ] }; // вывод всех элементов из country.languages console.log("Официальные языки Швейцарии:"); for(const lang of country.languages){ console.log(lang); } console.log("\n"); // для разделения языков от городов // вывод всех элементов из country.cities console.log("Города Швейцарии:"); for(const city of country.cities){ console.log(city.name); }

В объекте country имеется свойство languages, содержащее массив строк, а также свойство cities, хранящее массив однотипных объектов.

С этими массивами мы можем работать также, как и с любыми другими, например, перебрать с помощью цикла for. При переборе массива объектов каждый текущий элемент будет представлять отдельный объект, поэтому мы можем обратиться к его свойствам и методам:

for(const city of country.cities){ console.log(city.name); }

Либо для перебора мы могли бы использовать другой тип цикла for и также пройтись по всем элементам массива:

for(let i=0; i < country.cities.length; i++){ console.log(country.cities[i].name); }

В итоге браузер выведет содержимое этих массивов:

Официальные языки Швейцарии:
немецкий
французский
итальянский

Города Швейцарии:
Цюрих
Женева
Базель

📜 Копирование и сравнение объектов

Копирование объектов

В отличие от данных примитивных типов данные объектов копируются по ссылке. Что это значит?
Рассмотрим следующий пример:

const tom = { name: "Tom"}; const bob = tom; // проверяем свойство name у обоих констант console.log(tom.name); // Tom console.log(bob.name); // Tom // меняем свойство name у константы bob bob.name = "Bob"; // повторно проверяем свойство name у обоих констант console.log("После изменения") console.log(tom.name); // Bob console.log(bob.name); // Bob

Вначале определяется обычный объект tom с одним свойством name. Затем присваиваем значение этого объекта константе bob

const bob = tom;

В данном случае константа bob получае ссылку или условно говоря адрес константы tom, поэтому после этого присвоения обе константы по сути указывают на один и тот же объект в памяти. Соответственно изменения, произведенные через одну константу:

bob.name = "Bob";

Затронут и другую константу - tom:

console.log(tom.name); // Bob

Более того, добавим к объекту новое свойство через одну из констант:

const tom = { name: "Tom"}; const bob = tom; // добавляем константе bob новое свойство - age bob.age = 37; // и видим, что для tom тоже добавлено новое свойство console.log(tom.age); // 37

После добавления свойства age константе bob можно увидеть, что у константы tom то же появилось это свойство, потому что опять же обе константы представляют один и тот же объект.

Метод Object.assign

Что же если мы хотим скопировать из свойства объекта, но при этом обе константы или переменных указывали бы на совершенно разные объекты, изменения одного из которых никак бы не затрагивали другой? В этом случае мы можем воспользоваться встроенным методом Object.assign().

Метод Object.assign() принимает два параметра:

Object.assign(target, ...sources)

Первый параметр - target представляет объект, в который надо скопировать свойства. Второй параметр - ...sources - набор объектов, из которых надо скопировать свойства (то есть мы можем скопировать свойства сразу из нескольких объектов)

Возвращает метод объект target, в который скопированы свойства из объектов sources.

Например:

const tom = { name: "Tom", age: 37}; const bob = Object.assign({}, tom); bob.name = "Bob"; bob.age = 41; console.log(`Объект tom. Name: ${tom.name} Age: ${tom.age}`); console.log(`Объект bob. Name: ${bob.name} Age: ${bob.age}`);

В данном случае вызов Object.assign({}, tom) означает, что мы копируем данные из объекта tom в пустой объект {}. Результатом этого копирования стал объект bob. Причем это совсем другой объект, нежели tom. И любые изменения с константой bob здесь никак не затронут константу tom.

Консольный вывод программы:

Объект tom. Name: Tom   Age: 37
Объект bob. Name: Bob   Age: 41

Копирование из нескольких объектов

Подобным образом можно копировать данные из нескольких объектов:

const tom = { name: "Tom"}; const sam = { age: 37}; const person = { height: 170}; Object.assign(person, tom, sam); // копируем из tom и sam в person console.log(person); // {height: 170, name: "Tom", age: 37}

Здесь копируются все свойства из объектов tom и sam в объект person. В итоге после копирования объект person будет иметь три свойства.

Копирование одноименных свойств

Если объекты, из которых выполняется копирование, содержат одинаковые свойства, то свойства из последних объектов замещают свойства предыдущих:

const tom = { name: "Tom", age: 37}; const sam = { age: 45}; const person = { height: 170}; Object.assign(person, tom, sam); console.log(person); // {height: 170, name: "Tom", age: 45}

Здесь оба объекта - tom и sam содержат свойство age, но в объекте person свойство age равно 45 - значение из объекта sam, потому что копирование из объекта sam произодится в последнюю очередь.

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

Несмотря на то, что Object.assign() прекрасно работает для простых объектов, но что будет, если свойство копируемого объекта также представляет объект:

const tom = { name: "Tom", company: {title: "Microsoft"}}; const bob = Object.assign({}, tom); bob.name = "Bob"; bob.company.title = "Google"; console.log(tom.name); // Tom console.log(tom.company.title); // Google

Здесь свойство company объекта tom представляет объект с одним свойством. И при копировании объект bob получит не копию значения tom.company, а ссылку на этот объект. Поэтому изменения bob.company затронут и tom.company.

Копирование объекта с помощью spread-оператора

spread-оператор ... позволяет разложить объект на различные пары свойство-значение, которые можно передать другому объекту.

const tom = { name: "Tom", age: 37, company: "Google"}; const bob = {...tom} bob.name = "Bob"; console.log(tom); // {name: "Tom", age: 37, company: "Google"} console.log(bob); // {name: "Bob", age: 37, company: "Google"}

В данном случае объекту bob передаются копии свойств объекта tom.

Если какие-то свойства нового объекта должны иметь другие значения (как в примере выше свойство name), то их можно указать в конце:

const tom = { name: "Tom", age: 37, company: "Google"}; const bob = {...tom, name: "Bob"}; console.log(bob); // {name: "Bob", age: 37, company: "Google"}

Как видно из предыдущего примера, обе константы после копирования представляют ссылки на разные объекты, и изменения одного из них никак не затронет другой объект.

Тем не менее если объекты содержат вложенные объекты, то эти вложенные объекты при копировании опять же по сути будут представлять ссылки на один и тот же объект:

const tom = { name: "Tom", age: 37, company: {title: "Microsoft"}} const bob = {...tom} bob.name = "Bob"; bob.company.title = "Google"; console.log(`${tom.name} - ${tom.company.title}`); // Tom - Google console.log(`${bob.name} - ${bob.company.title}`); // Bob - Google

Сравнение объектов

Сравним два объекта с помощью стандартных операций сравнения и эквивалентности:

const tom = { name: "Tom"}; const bob = { name: "Bob"}; console.log(tom == bob); // false console.log(tom === bob); // false

Оба оператора в данном случае возвратят значение false, то есть объекты не равны. Причем даже если значения свойств объектов будет одинаковым, то мы все равно в обоих случаях получим false

const tom = { name: "Tom"}; const bob = { name: "Tom"}; console.log(tom == bob); // false console.log(tom === bob); // false

Однако, что будет, если обе константы (переменных) хранят ссылку на один и тот же объект:

const tom = { name: "Tom"}; const bob = tom; console.log(tom == bob); // true console.log(tom === bob); // true

В этом случае в обоих случаях мы получим true, поскольку значения обоих констант равны, так как по сути это одно и то же значение.

При динамическом определении в объекте новых свойств и методов перед их использованием бывает важно проверить, а есть ли уже такие методы и свойства. Для этого в javascript может использоваться оператор in. Он имеет следующий синтаксис:

  • Для этого в javascript может использоваться оператор in. Он имеет следующий синтаксис:
    "свойство|метод" in объект

    в кавычках идет название свойства или метода, а после in - название объекта. Если свойство или метод с подобным именем имеется, то оператор возвращает true. Если нет - то возвращается false.

    Например, узнаем, есть ли в объекте ряд свойств:

    const user = {}; user.name = "Tom"; user.age = 26; user.print = function(){ console.log(this.name); console.log(this.age); }; const hasNameProp = "name" in user; console.log(hasNameProp); // true - свойство name есть в user const hasWeightProp = "weight" in user; console.log(hasWeightProp); // false - в user нет свойства или метода под названием weight

    С помощью выражения "name" in user проверяем, есть ли в объекте user свойство "name" и результат проверки передаем в константу hasNameProp. Далее анологичным образом проверяем наличие свойства wheight.

    Подобным образом можно проверить и наличие методов:

    const hasPrintMethod = "print" in user; console.log(hasPrintMethod); // true - в user есть метод print
  • Альтернативный способ заключается в проверке на значение undefined. Если свойство или метод равен undefined, то эти свойство или метод не определены:

    const hasNameProp = user.name!==undefined; console.log(hasNameProp); // true const hasWeightProp = user.weight!==undefined; console.log(hasWeightProp); // false
  • И так как объекты представляют тип Object, а значит, имеет все его методы и свойства, то объекты также могут использовать метод hasOwnProperty(), который определен в типе Object:

    const hasNameProp = user.hasOwnProperty("name"); console.log(hasNameProp); // true const hasPrintMethod = user.hasOwnProperty("print"); console.log(hasPrintMethod); // true const hasWeightProp = user.hasOwnProperty("weight"); console.log(hasWeightProp); // false

Перебор свойств и методов

С помощью цикла for мы можем перебрать объект как обычный массив и получить все его свойства и методы и их значения:

const tom = { name: "Tom", age: 26, print(){ console.log(`Name: ${this.name} Age: ${this.age}`); } }; for(const prop in tom) { console.log(prop, " : ", tom[prop]); }

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

name : Tom
age : 26
print : print(){
	console.log(`Name: ${this.name}  Age: ${this.age}`);
}

📜 Функции Object.entries, Object.keys, Object.values

С помощью дополнительных функций Object.entries, Object.keys и Object,values можно получить все свойства (в том числе методы) объекта и их значения.

Object.entries()

Функция Object.entries() в качестве параметра принимает объект и возвращает массив пар "название_свойства - значение", где каждая пара свойство-значение представляет массив. Например:

const tom = { name: "Tom", age: 26, print(){ console.log(`Name: ${this.name} Age: ${this.age}`); } }; for(const prop of Object.entries(tom)) { console.log(prop); }

Консольный вывод:

["name", "Tom"]
["age", 26]
["print", ƒ]

Object.keys()

Функция Object.keys() позволяет получить массив названий всех свойств объекта. Например, возьмем выше определенный объект tom:

const tom = { name: "Tom", age: 26, print(){ console.log(`Name: ${this.name} Age: ${this.age}`); } }; console.log(Object.keys(tom)); // ["name", "age", "print"]

Соответственно можно перебрать этот набор и получить значения свойств:

for(const prop of Object.keys(tom)) { const value = tom[prop]; // получаем по названию значение свойства console.log(prop,value); }

Консольный вывод:

name Tom
age 26
print ƒ print(){
        console.log(`Name: ${this.name}  Age: ${this.age}`);
}

Object.values()

Функция Object.values() возвращает массив, который содержит все значения свойств объекта:

const tom = { name: "Tom", age: 26, print(){ console.log(`Name: ${this.name} Age: ${this.age}`); } }; console.log(Object.values(tom)); // ["Tom", 26, print()]

📜 Полезные методы Object

const user = { name: 'Alice', age: 25, city: 'Moscow' }; // Object.keys() - массив ключей console.log(Object.keys(user)); // ['name', 'age', 'city'] // Object.values() - массив значений console.log(Object.values(user)); // ['Alice', 25, 'Moscow'] // Object.entries() - массив пар [ключ, значение] console.log(Object.entries(user)); // [['name', 'Alice'], ['age', 25], ['city', 'Moscow']] // Object.assign() - копирование свойств const copy = Object.assign({}, user); // Object.freeze() - заморозка объекта Object.freeze(user); user.age = 30; // Не сработает // Object.seal() - запрет добавления/удаления свойств const obj = {x: 1}; Object.seal(obj); obj.x = 2; // OK obj.y = 3; // Не сработает delete obj.x; // Не сработает

📜 Объекты в функциях

Объект как результат функции

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

function createUser(pName, pAge) { return { name: pName, age: pAge, print: function() { console.log(`Name: ${this.name} Age: ${this.age}`); } }; }; const tom = createUser("Tom", 26); tom.print(); const alice = createUser("Alice", 24); alice.print();

Здесь функция createUser() получает значения pName и pAge и по ним создает новый объект, который является возвращаемым результатом. Результат работы программы:

Name: Tom  Age: 26
Name: Alice  Age: 24

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

function createUser(pName, pAge) { if(pAge < 1 || pAge > 110){ // если возраст меньше 1 или больще 110 console.log("Age is invalid") pAge=1; } return { name: pName, age: pAge, print: function() { console.log(`Name: ${this.name} Age: ${this.age}`); } }; }; const tom = createUser("Tom", 26); tom.print(); const alice = createUser("Alice", 12345); alice.print();

Здесь проверяется параметр pAge, который представляет возвраст пользователя. Понятно, что теоретически это может быть любое число, которое может выходить за разумные пределы, например, быть отрицательным. И в данном случае мы проверяем pAge на соответствие этому пределу. Если значение pAge не соответствует пределу, то присваиваем ему значение по умолчанию - в данном случае 1, и выводим диагностическое сообщение. Консольный вывод программы:

Name: Tom  Age: 26
Age is invalid
Name: Alice  Age: 1

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

function getMinMax(numbers){ // если массив пуст, минимальное и максимальное значения неопределены if(numbers.length === 0) return {min: undefined, max: undefined}; let minNumber = numbers[0]; let maxNumber = numbers[0]; for(let i=1; i < numbers.length; i++){ if(minNumber > numbers[i]) minNumber = numbers[i]; if(maxNumber < numbers[i]) maxNumber = numbers[i]; } return {min: minNumber, max: maxNumber}; } const nums = [1, 2, 3, 4, 5]; const result = getMinMax(nums); console.log("Min:", result.min); // Min: 1 console.log("Max:", result.max); // Max: 5

Здесь в функции getMinMax получаем массив. Если массив не содержит чисел, то возвращаем объект, где поля min и max имеют значения undefined. Иначе проходим по всему массиву и вычисляем максимальное и минимальное значения и возвращаем их в виде одного объекта.

Объект как параметр

Как и все другие значения, объект может передаваться в качестве параметра в функцию:

function printPerson(person){ console.log("Name:", person.name); console.log("Age:", person.age); } const tom = {name: "Tom", age: 39}; const alice = {name: "Alice", age: 35}; printPerson(tom); printPerson(alice);

Здесь в функцию printPerson передается объект, который, как предполагается, будет иметь два свойства: name и age.

При этом стоит учитывать, что объект - ссылочный тип, а переменная/константа/параметр, которые представляют объект, фактически хранят ссылку на объект в памяти, а не сам объект. Соответственно при передаче в функцию объекта параметру передается копия ссылки на этот объект. И через эту ссылку функция может изменять различные свойства объекта:

function setAge(person, pAge){ person.age = pAge; } const sam = {name: "Sam", age: 29}; console.log("Before setAge:", sam.age); setAge(sam, 30); console.log("After setAge:", sam.age);

Здесь сначала определяем константу sam, которая представляет объект со свойствами name и age:

const sam = {name: "Sam", age: 29};

Фактически константа sam хранит ссылку на область памяти, где расположен объект.

Затем вызывается функция setAge, которая получает объект person и изменяет у него свойство age.

setAge(sam, 30);

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

Before setAge: 29
After setAge: 30

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

function setDefault(person){ person = {name: "Undefined", age: 0}; } let sam = {name: "Sam", age: 29}; console.log("Before setDefault:", sam.name); setDefault(sam); console.log("After setDefault:", sam.name);

При передаче переменной sam в функцию setDefault параметр этой функции и переменная sam будут представлять две разные ссылки, но указывать на один и тот же обеъект в памяти:

setDefault(sam);

Но потом внутри функции мы изменяем значение параметра:

person = {name: "Undefined", age: 0};

В итоге ссылке, которая хранится в параметре person, присвается новый объект. Но поскольку переменная sam и параметр person представляют две разные ссылки, то это присваивание никак не затронет объект sam.

📜 Функции-конструкторы объектов

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

Для начала определим конструктор:

function Person(pName, pAge) { this.name = pName; this.age = pAge; this.print = function(){ console.log("Name: ", this.name); console.log("Age: ", this.age); }; }

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

this.name = pName;

В данном случае устанавливаются два свойства name и age и один метод print.

Как правило, названия конструкторов в отличие от названий обычных функций начинаются с большой буквы.

После этого в программе мы можем определить объект типа Person и использовать его свойства и методы:

// определение конструктора объектов типа Person function Person(pName, pAge) { this.name = pName; this.age = pAge; this.print = function(){ console.log(`Name: ${this.name} Age: ${this.age}`); }; } // определение объекта типа Person const tom = new Person("Tom", 39); // обращение к свойству объекта console.log(tom.name); // Том // обращение к методу объекта tom.print(); // Name: Tom Age: 39

Чтобы вызвать конструктор, то есть создать объект типа Person, надо использовать ключевое слово new:

const tom = new Person("Tom", 39);

Далее через имя объекта можно обращаться к его свойствам и методам, которые определены внутри функции конструктора:

// обращение к свойству объекта console.log(tom.name); // Том // обращение к методу объекта tom.print();

Стоит отметить, что, конечно, мы могли бы определить объект стандартным образом:

const tom = { name: "Tom", age: 39, print: function(){ console.log(`Name: ${this.name} Age: ${this.age}`); } }

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

function Person(pName, pAge) { this.name = pName; this.age = pAge; this.print = function(){ console.log(`Name: ${this.name} Age: ${this.age}`); }; } const tom = new Person("Tom", 39); const bob = new Person("Bob", 43); const sam = new Person("Sam", 28); tom.print(); // Name: Tom Age: 39 bob.print(); // Name: Bob Age: 43 sam.print(); // Name: Sam Age: 28

Другой пример:

// Конструктор (с большой буквы) function User(name, age) { this.name = name; this.age = age; this.greet = function() { console.log(`Привет, я ${this.name}`); }; } // Создание экземпляра через new const user1 = new User('Alice', 25); const user2 = new User('Bob', 30); user1.greet(); // "Привет, я Alice" user2.greet(); // "Привет, я Bob"

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

function Person(pName, pAge) { this.name = pName; this.age = pAge; this.print = function(){ console.log(`Name: ${this.name} Age: ${this.age}`); }; } function printPersonName(person){ console.log(person.name); } // массив из трех объектов Person const people = [new Person("Tom", 39), new Person("Bob", 43), new Person("Sam", 28)]; for(person of people){ printPersonName(person); }

Оператор instanceof

Оператор instanceof позволяет проверить, с помощью какого конструктора создан объект. Если объект создан с помощью определенного конструктора, то оператор возвращает true:

// определение конструктора объектов типа Person function Person(pName, pAge) { this.name = pName; this.age = pAge; this.print = function(){ console.log(`Name: ${this.name} Age: ${this.age}`); }; } // определение конструктора объектов типа Employee function Employee(eName, eCompany) { this.name = eName; this.company = eCompany; this.print = function(){ console.log(`Name: ${this.name} Company: ${this.company}`); }; } const tom = new Person("Tom", 39); const bob = new Employee("Bob", "Google"); console.log(tom instanceof Person); // true - tom является объектом типа Person console.log(bob instanceof Employee); // true - bob является объектом типа Employee console.log(tom instanceof Employee); // false - tom НЕ является объектом типа Employee

📜 Расширение объектов. Прототипы

JavaScript — это язык, основанный на прототипах, поэтому он не знает никаких классов — по крайней мере, реальных. Вместо этого все в JavaScript основано на объектах. Почти каждый объект в JavaScript основан на прототипе. Исключения - тип Object (основа всех объектов) или объекты, прототип которых явно установлен в null — не имеют прототипа. Каждый объект также может служить шаблоном, то есть прототипом другого объекта. В этом случае новый объект наследует свойства и методы прототипа.

Прототип объекта хранится в свойстве __proto__, которое реализованно как псевдоним внутреннего свойства [[Prototype]]. Кроме того получить прототип объекта можно с помощью метода getPrototypeOf(). Например:

const tom = {name: "Tom", age: 39}; // получаем прототип console.log(tom.__proto__); // Object console.log(Object.getPrototypeOf(tom)); // Object

В обоих случаях мы получим один и тот же результат в виде определения типа Object:

Object
    constructor: ƒ Object()
    hasOwnProperty: ƒ hasOwnProperty()
    isPrototypeOf: f isPrototypeOf()
    propertyIsEnumerable: f propertyIsEnumerable()
    toLocaleString: f toLocaleString()
    toString: f toString()
    valueOf: f valueOf()
    __defineGetter__: f __defineGetter__()
    __defineSetter__: f __defineSetter__()
    __lookupGetter__: f __lookupGetter__()
    __lookupSetter__: f __lookupSetter__()
    __proto__: null
    get __proto__: f __proto__()
    set __proto__: f __proto__()

Прототип функций-конструкторов

В прошлой теме были рассмотрены функции-конструкторы, который позволяют определить тип объекта и создать объект этого типа. Каждая такая функция-конструктор определяет свой прототип, который служит основой для создаваемых объектов. Этот прототип также можно получить с помощью свойства prototype. Например:

function Person(name, age) { this.name = name; this.age = age; this.print = function(){ console.log(`Name: ${this.name} Age: ${this.age}`); }; } const tom = new Person("Tom", 39); // получаем прототип console.log(Person.prototype); console.log(tom.__proto__); console.log(Object.getPrototypeOf(tom));

Здесь получаем прототип функции-конструктора Person. Все три использованных способа получения прототипа аналогичны, и при выводе на консоль во всех трех случаях мы увидим что-то наподобие:

{constructor: ƒ}
constructor : ƒ Person(name, age)
[[Prototype]] : Object

Конструктор и прототип

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

function Person(name, age) { this.name = name; this.age = age; this.print = function(){ console.log(`Name: ${this.name} Age: ${this.age}`); }; } console.log(Person.prototype);

Консольный вывод:

{constructor: ƒ}
	constructor: ƒ Person(name, age)
	[[Prototype]]: Object

Схематично мы можем представить прототип следующим образом:

Фактически прототип функции-конструктора Person состоит только из конструктора (в который неявно также входят унаследованные от типа Object методы типа toString()). мы можем получить этот конструктор, использовав свойство constructor:

console.log(Person.prototype.constructor);

Консоль должна вывести что-то наподобие:

ƒ Person(name, age) {
	this.name = name;
	this.age = age;
	this.print = function(){
		console.log(`Name: ${this.name}  Age: ${this.age}`);
	};

Поскольку свойство constructor - это часть прототипа, то к нему обратиться можно и через имя объекта:

const tom = new Person("Tom", 39); console.log(tom.constructor);

Теперь уберем метод print() из конструктора и определим его как часть прототипа:

function Person (name, age) { this.name = name; this.age = age; } // функция print определена как часть прототипа Person.prototype.print = function(){ console.log(`Name: ${this.name} Age: ${this.age}`); }; console.log(Person.prototype);

Консольный вывод браузера:

{print: ƒ, constructor: ƒ}
	print: ƒ ()
	constructor: ƒ Person(name, age)
	[[Prototype]]: Object

Теперь прототип состоит из функции print и конструктора:

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

function Person(name, age) { this.name = name; this.age = age; this.print = function(){ console.log(`Name: ${this.name} Age: ${this.age}`); }; } const tom = new Person("Tom", 39); const bob = new Person("Bob", 43); // измененияем прототип Person.prototype.sayHello = function(){ console.log(this.name, "says: Hello"); }; tom.print(); // Name: Tom Age: 39 tom.sayHello(); // Tom says: Hello bob.print(); // Name: Bob Age: 43 bob.sayHello(); // Bob says: Hello

Причем мы можем определить одни и те же свойства и методы как внутри конструктора, так и как часть прототипа:

// конструктор пользователя function Person (name, age) { this.name = name; this.age = age; this.print = function(){ console.log(`[Конструктор] Name: ${this.name} Age: ${this.age}`); }; } Person.prototype.print = function(){ console.log(`[Прототип] Name: ${this.name} Age: ${this.age}`); }; const tom = new Person("Tom", 39); const bob = new Person("Bob", 43); tom.print(); // [Конструктор] Name: Tom Age: 39 bob.print(); // [Конструктор] Name: Bob Age: 43

В этом случае методы, определенные внутри конструктора, будут скрывать одноименные методы прототипа.

Определение свойств прототипа

Подобным образом можно добавлять и свойства. Например, добавим свойство company, которое представляет компанию:

const tom = new Person("Tom", 39); const bob = new Person("Bob", 43); // добавляем в прототип свойство company Person.prototype.company = "SuperCorp"; console.log(tom.company); // SuperCorp console.log(bob.company); // SuperCorp

Но важно заметить, что значение свойства company будет одно и то же для всех объектов, это разделяемое статическое свойство. В отличие, скажем, от свойства this.name, которое хранит значение для определенного объекта.

В то же время мы можем определить в объекте свойство, которое будет назваться также, как и свойство прототипа. В этом случае собственное свойство объекта будет иметь приоритет перед свойством прототипа:

const tom = new Person("Tom", 39); const bob = new Person("Bob", 43); Person.prototype.company = "SuperCorp"; bob.company = "MegaCorp"; // определяем свойство с тем же именем на уровне одного объекта console.log(bob.company); // MegaCorp - берет свойство из объекта bob console.log(tom.company); // SuperCorp - берет свойство из прототипа Person

И при обращении к свойству company javascript сначала ищет это свойство среди свойств объекта, и если оно не было найдено, тогда обращается к свойствам прототипа. То же самое касается и методов.

// Добавление метода в прототип function User(name, age) { this.name = name; this.age = age; } // Метод в прототипе (экономия памяти) User.prototype.greet = function() { console.log(`Привет, я ${this.name}`); }; User.prototype.celebrate = function() { this.age++; console.log(`Мне ${this.age}`); }; const user1 = new User('Alice', 25); user1.greet(); // "Привет, я Alice" user1.celebrate(); // "Мне 26"
💡 Зачем прототипы?

Методы в прототипе создаются один раз и используются всеми экземплярами. Это экономит память по сравнению с созданием метода внутри конструктора.

📜 Функция как объект. Методы call и apply

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

Например, мы можем создать функцию с помощью конструктора Function:

const square = new Function("n", "return n * n;"); console.log(square(5)); // 25

В конструктор Function может передаваться ряд параметров. Последний параметр представляет собой само тело функции в виде строки. Фактически строка содержит код javascript. Предыдущие аргументы содержат названия параметров. В данном случае определяется функция возведения числа в квадрат, которая имеет один параметр n.

Среди свойств объекта Function можно выделить следующие:

  • arguments: массив аргументов, передаваемых в функцию
  • length: определяет количество аргументов, которые ожидает функция
  • caller: определяет функцию, вызвавшую текущую выполняющуюся функцию
  • name: имя функции
  • prototype: прототип функции

С помощью прототипа мы можем определить дополнительные свойства:

function sayHello(){ console.log("Hello"); } // изменяем прототип для всех функций Function.prototype.program ="Hello World"; console.log(sayHello.program); // Hello World

Методы call() и apply()

Метод call() вызывает функцию с указанным значением this и аргументами:

function sum(x, y){ return x + y; } const result = sum.call(this, 3, 8); console.log(result); // 11

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

При передаче объекта через первый параметр, мы можем ссылаться на него через ключевое слово this:

function User (name, age) { this.name = name; this.age = age; } const tom = new User("Tom", 39); function print(){ console.log("Name:", this.name); } print.call(tom); // Name: Tom

В данном случае передается только одно значение, поскольку функция print не принимает параметров. То есть функция будет вызываться для объекта tom.

Если нам не важен объект, для которого вызывается функция, то можно передать значение null:

function sum(x, y){ return x + y; } const result = sum.call(null, 3, 8); console.log(result); // 11

На метод call() похож метод apply(), который также вызывает функцию и в качестве первого параметра также получает объект, для которого функция вызывается. Только теперь в качестве второго параметра передается массив аргументов:

function sum(x, y){ return x + y; } const result = sum.apply(null, [3, 8]); console.log(result); // 11

📜 Функция Object.create. Конфигурация свойств объектов

Функция Object.create

Еще один способ создания объекта предоставляет функция Object.create, которая принимает два параметра:

  • Первый параметр - прототип, на основе которого будет создаваться объект
  • Второй параметр - определение свойств и методов объекта
const tom = Object.create(прототип, { свойства и методы });

Например:

const tom = Object.create(Object.prototype, { name: { value: "Tom" }, age: { value: 39 }, print: { value: function() { console.log(`Name: ${this.name} Age: ${this.age}`);} } }); console.log(tom.name); // Tom console.log(tom.age); // 39 tom.print(); // Name: Tom Age: 39

Здесь в качестве прототипа в функцию Object.create() передается прототип Object - Object.prototype. Второй параметр функции - определение свойств вида:

имя_свойства/метода: { value: значение_свойства/метода }

Имени свойства/метода сопоставляется объект, в котором есть свойство value - это свойство собственно и хранит значение свойства/метода. Например, свойство age равно 39:

age: { value: 39 }

Для метода значением выступает определение функции.

После создания объекта мы можем обращаться к его свойствам и методам, как и в общем случае:

console.log(tom.age); // 39

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

  • writeable: хранит логическое значение, которое указывает, доступно ли это свойство для записи, то есть можно ли ему присвоить новое значение. По умолчанию этот атрибут имеет значение false.
  • enumerable: хранит логическое значение, которое указывает, является ли соответствующее свойство перечислимым, то есть включается ли это свойство при переборе свойств соответствующего объекта (например, с использованием цикла for...in). По умолчанию имеет значение false.
  • configurable: хранит логическое значение, которое указывает, можно ли изменить сам атрибут для соответствующего свойства, то есть можно ли впоследствии настроить свойство с помощью атрибутов. Значение по умолчанию для этого атрибута также равно false
  • set: определяет, какая функция вызывается при изменении значения свойства
  • get: определяет, какая функция вызывается при чтении значения свойства

Применим некоторые из этих атрибутов:

const tom = Object.create(Object.prototype, { name: { value: "Tom", enumerable: true, // доступно для перебора writable: false // НЕ доступно для записи }, age: { value: 39, enumerable: true, // доступно для перебора writable: true // доступно для записи }, print: { value: function() { console.log(`Name: ${this.name} Age: ${this.age}`);}, enumerable: false, // не доступно для перебора writable: false, // НЕ доступно для записи } }); console.log(tom.name); // Tom tom.name = "Tomas"; console.log(tom.name); // Tom - свойство name не доступно для изменения console.log(tom.age); // 39 tom.age = 22; console.log(tom.age); // 22 - свойство age доступно для изменения tom.print(); // Name: Tom Age: 22 // перебор объекта (не доступно для перебора) for(prop in tom){ console.log(prop); } // Консольный вывод: // name // age

Функция: Object.defineProperty:

В примере выше функция Object.create использует много кода для создания объекта. Но что, если у нас есть куча свойств и методов, но некоторая конфигурация (например, сделать свойство доступно только для чтения) нужна только для одного свойства? В этом случае мы можем создать объект стандартным образом, а все дополнительные свойства, которые требуют конфигурации, определить с помощью функции Object.defineProperty:

const tom = { age:39, print: function() { console.log(`Name: ${this.name} Age: ${this.age}`);} }; Object.defineProperty(tom, "name", { value: "Tom", writable: false // НЕ доступно для записи }); console.log(tom.name); // Tom tom.name = "Tomas"; console.log(tom.name); // Tom - свойство name не доступно для изменения tom.print(); // Name: Tom Age: 22

Функция Object.defineProperty() принимает три параметра:

  • Первый параметр - объект, для которого определяется свойство.
  • Второй параметр - название свойства.
  • Третий параметр - конфигурационный объект. То есть в данном случае доопределяем для объекта tom свойство name, которое будет достпуно только для чтения.

Если надо подобным образом доопределить несколько свойств, то применяется функция Object.defineProperties, которая принимает объект и набор конфигурационных настроек для добавляемых свойств:

const tom = { age:39 }; // доопределяем свойства для объекта tom Object.defineProperties(tom, { name: { // определяем свойство name value: "Tom", writable: false // НЕ доступно для записи }, print: { // определяем метод print value: function() { console.log(`Name: ${this.name} Age: ${this.age}`);}, writable: false, // НЕ доступно для записи } }); tom.name = "Tomas"; // свойство name не доступно для изменения tom.print = function(){console.log("Hello Word");} // метод print не доступен для изменения tom.print(); // Name: Tom Age: 39

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

const tom = {name: "Tom"}; // для свойства name запрещаем изменение Object.defineProperty(tom, "name", { writable: false}); tom.name = "Tomas"; console.log(tom.name); // Tom - значение свойства не изменилось

📜 Наследование прототипов

JavaScript поддерживает наследование, что позволяет нам при создании новых типов объектов при необходимости унаследовать их функционал от уже существующих. Однако нужно понимать, что наследование в JavaScript отличается от наследования в других распространенных и популярных языках типа Java, C++, C# и ряде других. В JavaScript наследование - это наследование объектов (а не наследование классов или типов), которое еще называют наследование прототипов или прототипное наследование.

Для создания объекта на основе некоторого прототипа применяется функция Object.create(), в которую передается наследуемый прототип:

const person = { name: "", age: 0, print: function(){ console.log(`Name: ${this.name} Age: ${this.age}`); } }; const employee = Object.create(person); // employee использует прототип объекта person // получаем прототип console.log(employee.__proto__); // {name: "", age: 0, print: ƒ} employee.name = "Tom"; employee.age = 39; employee.print(); // Name: Tom Age: 39

В данном случае объект employee создан на основе прототипа объекта person, по сути объект employee наследует прототип объекта person. Благодаря такому наследованию объект employee обладает всеми теми же свойствами и методами, которые определены в объекте person.

В дополнение объекты могут определять свои свойства и методы. Например:

const person = { name: "", age: 0, print: function(){ console.log(`Name: ${this.name} Age: ${this.age}`); } }; const employee = Object.create(person); // employee использует прототип объекта person employee.name = "Tom"; employee.age = 39; employee.company = "Google"; // новое свойство // новый метод employee.work = function(){ console.log(`${this.name} works in ${this.company}`); } employee.print(); // Name: Tom Age: 39 employee.work(); // Tom works in Google

В данном случае объект employee дополнительно определяет свойство company и метод work.

При необходимости можно переопределить унаследованные методы:

const person = { name: "", age: 0, print: function(){ console.log(`Name: ${this.name} Age: ${this.age}`); } }; const employee = Object.create(person); employee.name = "Tom"; employee.age = 39; employee.company = "Google"; // переопределяем метод print employee.print = function(){ console.log(`Name: ${this.name} Age: ${this.age} Company: ${this.company}`); } employee.print(); // Name: Tom Age: 39 Company: Google

Здесь переопределяем функцию print, чтобы она также выводила компанию работника. Можно пойти дальше и увеличить цепочку наследования:

const person = { name: "", age: 0, print: function(){ console.log(`Name: ${this.name} Age: ${this.age}`); } }; // объект employee наследует прототип объекта person const employee = Object.create(person); employee.company = ""; // объект manager наследует прототип объекта employee const manager = Object.create(employee); // переопределяем метод print manager.print = function(){ console.log(`Name: ${this.name} Age: ${this.age}\nManager in ${this.company}`); } manager.name = "Bob"; manager.age = 43; manager.company = "Microsoft"; manager.print(); // Name: Bob Age: 43 // Manager in Microsoft

Таким образом, получаем цепочку прототипов - person-employee-manager: employee наследует прототип от person, manager наследует прототип от employee

Вызов методов базового прототипа

Иногда может быть необходимо вызвать методы, которые определены в прототипе. Это может быть полезно для сокращения кода, уменьшения дублирования, особенно когда код переопределенного метода повторяет логику метода из прототипа. Получив прототип объекта, мы можем вызвать у него методы с помощью функции call():

const person = { name: "", age: 0, print: function(){ console.log(`Name: ${this.name} Age: ${this.age}`); } }; // объект employee наследует прототип объекта person const employee = Object.create(person); employee.name = "Tom"; employee.age = 39; employee.company = "Google"; // переопределяем метод print employee.print = function(){ this.__proto__.print.call(this); // вызываем версию метода из person // Object.getPrototypeOf(this).print.call(this); // альтернативный вариант console.log(`Company: ${this.company}`); } employee.print(); // Name: Tom Age: 39 // Company: Google

В данном случае в переопределенном методе print у типа employee вызываем через прототип версию метода print из person.

Проверка наследования прототипов и Object.isPrototypeOf()

С помощью метода Object.isPrototypeOf() можно проверить, является ли объект прототипом другого объекта:

const person = { name: "", print: ()=>console.log("Name:", this.name) }; const user = { name: "", print: ()=>console.log("Name:", this.name) }; // объект employee наследует прототип объекта person const employee = Object.create(person); console.log(person.isPrototypeOf(employee)); // true console.log(user.isPrototypeOf(employee)); // false

Здесь объект employee наследует прототип от person. Соответственно вызов person.isPrototypeOf(employee) возвратит true. А объект user не является прототипом для employee даже несмотря на то, что у него тот же набор методов и свойств.

📜 Наследование прототипов конструкторов

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

Например, у нас может быть объект Person, который представляет отдельного пользователя. И также может быть объект Employee, который представляет работника. Но работник также может являться пользователем и поэтому должен иметь все его свойства и методы. Например:

// конструктор пользователя function Person (name, age) { this.name = name; this.age = age; this.sayHello = function(){ console.log(`Person ${this.name} says "Hello"`); }; } // добавляем функцию в прототип Person.prototype.print = function(){ console.log(`Name: ${this.name} Age: ${this.age}`); }; // конструктор работника function Employee(name, age, comp){ Person.call(this, name, age); // применяем конструктор Person this.company = comp; this.work = function(){ console.log(`${this.name} works in ${this.company}`); }; } // наследуем прототип от Person Employee.prototype = Object.create(Person.prototype); // устанавливаем конструктор Employee.prototype.constructor = Employee;

Здесь в начале определяет функция-конструктор Person, который представляет пользователя. В Person определены два свойства и два метода. Для примера один метод - sayHello определен внутри конструктора, а второй метод - print определен непосредственно в прототипе.

Затем определяется функция-конструктор Employee, которая представляет работника.

В конструкторе Employee происходит обращение к конструктору Person с помощью вызова:

Person.call(this, name, age);

Передача первого параметра позволяет вызвать функцию конструктора Person для объекта, создаваемого конструктором Employee. Благодаря этому все свойства и методы, определенные в конструкторе Person, также переходят на объект Employee. Дополнительно определяется свойство company, которое представляет компанию работника, и метод work.

Кроме того, необходимо унаследовать также и прототип Person и соответственно все определенные через прототип функции (например, в примере выше это функция Person.prototype.print). Для этого служит вызов:

Employee.prototype = Object.create(Person.prototype);

Метод Object.create() позволяет создать объект прототипа Person, который затем присваивается прототипу Employee.

Нередко вместо вызова метода Object.create() для установки прототипа используется вызов наследуемого конструктора, например:

Employee.prototype = new Person();

В результате будет создан объект, у которого прототип (Employee.prototype.__proto__) будет указывать на прототип Person

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

Employee.prototype.constructor = Employee;

Конструктор редко используется сам по себе, и, возможно, осутствие установки конструктора никак не скажется на работе программы. Но тем не менее рассмотрим следующую ситуацию

const obj = new Employee.prototype.constructor("Bob", 23, "Google"); console.log(obj); // Employee или Person в зависимости от типа конструктора obj.work(); // Если obj - Person, то будет ошибка

Здесь напрямую вызываем конструктор для создания объекта obj. И тип объекта obj здесь будет зависеть от того, какой конструктор установлен для Employee.prototype.constructor

Протестируем выше определенные функции-конструкторы:

// конструктор пользователя function Person (name, age) { this.name = name; this.age = age; this.sayHello = function(){ console.log(`Person ${this.name} says "Hello"`); }; } Person.prototype.print = function(){ console.log(`Name: ${this.name} Age: ${this.age}`); }; // конструктор работника function Employee(name, age, comp){ Person.call(this, name, age); // применяем конструктор Person this.company = comp; this.work = function(){ console.log(`${this.name} works in ${this.company}`); }; } // наследуем прототип от Person Employee.prototype = Object.create(Person.prototype); // устанавливаем конструктор Employee.prototype.constructor = Employee; // создаем объект Employee const tom = new Employee("Tom", 39, "Google"); // обращение к унаследованному свойству console.log("Age:", tom.age); // обращение к унаследованному методу tom.sayHello(); // Person Tom says "Hello" // обращение к унаследованному методу прототипа tom.print(); // Name: Tom Age: 39 // обращение к собственному методу tom.work(); // Tom works in Google

Переопределение функций

При наследовании мы можем переопределять наследуемый функционал. Например, в примере выше для Person определено два метода: sayHello (в конструкторе) и print() (в прототипе). Но, допустим, для Employee мы хотим изменить их логику, например, в методе print также выводить компанию работника. В этом случае мы можем определить для Employee методы с теми же именами:

function Person (name, age) { this.name = name; this.age = age; this.sayHello = function(){ console.log(`Person ${this.name} says "Hello"`); }; } Person.prototype.print = function(){ console.log(`Name: ${this.name} Age: ${this.age}`); }; function Employee(name, age, comp){ Person.call(this, name, age); this.company = comp; // переопределяем метод sayHello this.sayHello = function(){ console.log(`Employee ${this.name} says "Hello"`); }; } Employee.prototype = Object.create(Person.prototype); Employee.prototype.constructor = Employee; // переопределяем метод print Employee.prototype.print = function(){ console.log(`Name: ${this.name} Age: ${this.age} Company: ${this.company}`); }; const tom = new Employee("Tom", 39, "Google"); tom.sayHello(); // Employee Tom says "Hello" tom.print(); // Name: Tom Age: 39 Company: Google

Метод sayHello() определен внутри конструктора Person, поэтому данный метод переопределяется внутри конструктора Employee. Метод print() определен как метод прототипа Person, поэтому его можно переопределить в прототипе Employee.

Вызов метода родительского прототипа

В прототипе-наследнике может потребоваться вызвать метод из родительского прототипа. Например, это может быть необходимо для сокращении логики кода, если логика метода наследника повторяет логику метода родителя. В этом случае для обращения к методам родительского прототипа применяется функция call()():

function Person (name, age) { this.name = name; this.age = age; } Person.prototype.print = function(){ console.log(`Name: ${this.name} Age: ${this.age}`); }; function Employee(name, age, comp){ Person.call(this, name, age); this.company = comp; } Employee.prototype = Object.create(Person.prototype); Employee.prototype.constructor = Employee; // переопределяем метод print Employee.prototype.print = function(){ Person.prototype.print.call(this); // вызываем метод print из Person console.log(`Company: ${this.company}`); }; const tom = new Employee("Tom", 39, "Google"); tom.print(); // Name: Tom Age: 39 // Company: Google

В данном случае при переопределении метода print в прототипе Employee вызывается метод print из прототипа Person:

Employee.prototype.print = function(){ Person.prototype.print.call(this); // вызываем метод print из Person console.log(`Company: ${this.company}`); };

Проблемы прототипного наследования

Стоит отметить, что тип Employee перенимает не только все текущие свойства и методы из прототипа Person, но и также те, которые будут впоследствии добавляться динамически. Например:

const tom = new Employee("Tom", 39, "Google"); Person.prototype.sleep = function() {console.log(`${this.name} sleeps`);} tom.sleep();

Здесь в прототип Person добавляется метод sleep. Причем он добавляется уже после создания объекта tom, который представляет тип Employee. Тем не менее даже у этого объекта мы можем вызвать метод sleep.

Другой момент, который стоит учитывать, через прототип конструктора-наследника можно изменить прототип конструктора-родителя. Например:

function Person (name, age) { this.name = name; this.age = age; this.sayHello = function(){ console.log(`Person ${this.name} says "Hello"`); }; } Person.prototype.print = function(){ console.log(`Name: ${this.name} Age: ${this.age}`); }; function Employee(name, age, comp){ Person.call(this, name, age); this.company = comp; } // наследуем прототип от Person Employee.prototype = Object.create(Person.prototype); Employee.prototype.constructor = Employee; // меняем метод print в базовом прототипе Person Employee.prototype.__proto__.print = function(){ console.log("Person prototype hacked");}; // создаем объект Person const bob = new Person("Bob", 43); bob.print(); // Person prototype hacked

📜 Инкапсуляция свойств. Геттеры и сеттеры

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

function User(uName, uAge) { this.name = uName; this.age = uAge; this.print = function(){ console.log(`Name: ${this.name} Age: ${this.age}`); }; } const tom = new User("Tom", 39); tom.age = 11500; tom.print(); // Name: Tom Age: 11500

Однако подобный способ доступа может быть нежелателен. Так, в примере выше свойству age, которое представляет возраст, мы можем присвоить самые разные, в том числе и недопустимые значения.

Но мы можем их скрыть от доступа извне. Для этого свойство определяется как локальная переменная/константа:

function User(uName, uAge) { this.name = uName; let _age = uAge; this.print = function(){ console.log(`Name: ${this.name} Age: ${_age}`); }; } const tom = new User("Tom", 39); tom._age = 11500; tom.print(); // Name: Tom Age: 39

В конструкторе User объявляется локальная переменная _age вместо свойства age:

let _age = uAge;

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

tom._age = 11500;

Здесь для объекта tom определяется новое свойство, которое называется, как и переменная _age. Но это свойство _age не окажет никакого влияния на локальную переменную _age, что мы можем увидеть по консольному выводу метода print.

Геттеры и сеттеры

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

  • геттер - для получения значения
  • сеттер - для изменения значения
function User(uName, uAge) { this.name = uName; let _age = uAge; // геттер - возвращаем значение переменной this.getAge = function() { return _age; } // устанавливаем значение переменной this.setAge = function(age) { if(age > 0 && age < 110){ // если возраст больше 0 и меньше 110 _age = age; } else { console.log("Недопустимое значение"); } } this.print = function(){ console.log(`Name: ${this.name} Age: ${_age}`); }; } const tom = new User("Tom", 39); // получаем значение console.log(tom.getAge()) // 39 // устанавливаем новое значение tom.setAge(22); console.log(tom.getAge()) // 22 tom.setAge(11500); // Недопустимое значение console.log(tom.getAge()) // 22

Для того, чтобы работать с возрастом пользователя извне, определяются два метода. Метод getAge() предназначен для получения значения переменной _age. Этот метод еще называется геттер (getter). Второй метод - setAge, который еще называется сеттер (setter), предназначен для установки значения переменной _age.

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

Стоит отметить, что JavaScript также предоставляет специальные конструкции для создания геттеров и сеттеров - get и set соответственно. Правда, в контексте функций-конструкторов они не имеют большого смысла, поэтому будут рассмотрены дальше.

📜 Деструктуризация

Деструктуризация (destructuring) позволяет извлечь из объекта отдельные значения в переменные или константы:

const user = { name: "Tom", age: 24, phone: "+367438787", email: "tom@gmail.com" }; const {name, email} = user; console.log(name); // Tom console.log(email); // tom@gmail.com

При деструктуризации объекта переменные помещаются в фигурные скобки и им присваивается объект. Сопоставление между свойствами объекта и переменными/константами идет по имени.

Мы можем указать, что мы хотим получить значения свойств объекта в переменные/константы с другим именем:

const user = { name: "Tom", age: 24, phone: "+367438787", email: "tom@gmail.com" }; const {name: userName, email: mailAddress} = user; console.log(userName); // Tom console.log(mailAddress); // tom@gmail.com

В данном случае свойство name сопоставляется с переменной userName, а поле email - с переменной mailAddress.

Также можно задать для переменных/констант значения по умолчанию, если в объекте вдруг не окажется соответствующих свойств:

const user = { name: "Tom", age: 24, }; const {name = "Sam", email: mailAddress = "sam@gmail.com"} = user; console.log(name); // Tom console.log(mailAddress); // sam@gmail.com

Если переменная/константа при деструктуризации сопоставляется со свойством, который представляет сложный объект, то после деструктуризации эта переменная/константа также будет представлять сложный объект:

const user = { name: "Tom", age: 24, account: { login: "tom555", password: "qwerty" } }; const {account} = user; console.log(account.login); // tom555 console.log(account.password); // qwerty

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

const user = { name: "Tom", age: 24, account: { login: "tom555", password: "qwerty" } }; const {name, account: {login}} = user; console.log(name); // Tom console.log(login); // tom555

Здесь мы получаем в переменную login значение свойства user.account.login.

Получение оставшихся свойств объекта с помощью rest-оператора

rest-оператор или оператор . . . позволяет получить оставшиеся свойства объекта, которые не сопоставлены с переменными/константами, в отдельную переменную/константу:

const tom = { name: "Tom", age: 24, phone: "+367438787", email: "tom@gmail.com" }; const {name, age, ...contacts} = tom; console.log(name); // Tom console.log(age); // 24 console.log(contacts); // {phone: "+367438787", email: "tom@gmail.com"}

В данном случае мы раскладываем объект tom на три константы: name, age и contacts. Константы name и age сопоставляются со свойствами объекта tom по имени. А константа contacts получает все оставшиеся несопоставленные свойства объекта. Однако чтобы их получить, перед названием константы указыватся оператор . . . : ...contacts. То есть в данном случае константа contacts будет предлагать объект, который будет содержать свойства email и phone объекта tom.

Стоит отметить, что переменная/константа, которая получает все оставшиеся свойства объекта, всегда будет представлять объект, даже если она получит только одно свойства из объекта.

Деструктуризация массивов

Также можно разложить массивы:

const users = ["Tom", "Sam", "Bob"]; const [a, b, c] = users; console.log(a); // Tom console.log(b); // Sam console.log(c); // Bob

При деструктуризации массива переменные помещаются в квадратные скобки и последовательно получают значения элементов массива.

Если переменных/констант меньше, чем элементов массива, то оставшиеся элементы массива просто опускаются.

const users = ["Tom", "Sam", "Bob"]; const [a, b] = users; console.log(a); // Tom console.log(b); // Sam

Если переменных/констант больше, чем элементов массива, то несопоставленные переменные/константы получают значение undefined:

const users = ["Tom", "Sam", "Bob"]; const [a, b, c, d] = users; console.log(a); // Tom console.log(b); // Sam console.log(c); // Bob console.log(d); // undefined

Получение оставшихся элементов массива в другой массив

С помощью rest-оператора . . . также можно получить все оставшиеся элементы массива в виде другого массива:

const users = ["Tom", "Sam", "Bob", "Mike"]; const [tom, ...others] = users; console.log(tom); // Tom console.log(others); // ["Sam", "Bob", "Mike"]

Здесь массив others будет содержать три последних элемента массива.

Пропуск элементов

При этом мы можем пропустить ряд элементов массива, оставив вместо имен переменных пропуски:

const users = ["Tom", "Sam", "Bob", "Ann", "Alice", "Kate"]; const [first,,,,fifth] = users; console.log(first); // Tom console.log(fifth); // Alice

Выражение first,,,,fifth указывает, что мы хотим получить первый элемент массива в переменную first, затем пропустить три элемента и получить пятый элемент в переменную fifth.

Подобным образом можно получить, например, второй и четвертый элементы:

const users = ["Tom", "Sam", "Bob", "Ann", "Alice", "Kate"]; const [,second,,forth] = users; console.log(second); // Sam console.log(forth); // Ann

Деструктуризация многомерных массивов

const coordinates = [[1,2,3], [4,5,6], [7,8,9]]; const [ [x1,y1,z1], [x2,y2,z2], [x3,y3,z3] ] = coordinates;

Деструктуризация объектов из массивов

Можно совместить получение данных из массива и объекта:

const people = [ {name: "Tom", age: 34}, {name: "Bob", age: 23}, {name: "Sam", age: 32} ]; const [,{name}] = people; console.log(name); // Bob

В данном случае получаем значение свойства name второго объекта в массиве.

Другой пример - деструктуризация объектов при переборе массива объектов:

const people = [ {name: "Tom", age: 34}, {name: "Bob", age: 23}, {name: "Sam", age: 32} ]; const [,{name}] = people; for(let {name: username, age: userage} of people){ console.log(`Name: ${username} Age: ${userage}`); } // консольный вывод // Name: Tom Age: 34 // Name: Bob Age: 23 // Name: Sam Age: 32

Деструктуризация параметров

Если в функцию в качестве параметра передается массив или объект, то его также можно подобным образом разложить на отдельные значения:

function display({name:userName, age:userAge}){ console.log(userName, userAge); } function sum([a, b, c]){ const result = a + b + c; console.log(result); } const user = {name:"Alice", age:33, email: "alice@gmail.com"}; const numbers = [3, 5, 7, 8]; display(user); // Alice 33 sum(numbers); // 15

Обмен значениями

Благодаря деструктуризации очень просто стало проводить обмен значениями между двумя переменными:

let first = "Tom"; let second = "Bob"; [first, second] = [second, first]; console.log(first); // Bob console.log(second); // Tom

Что упрощает решение ряда задач. Например, используем деструктуризацию для простейшей сортировки массива:

let nums = [9, 3, 5, 2, 1, 4, 8, 6]; for(let i = 0; i < nums.length; i++) for(let j = 0; j < nums.length; j++) if (nums[i] < nums[j]) [nums[j], nums[i]] = [nums[i], nums[j]]; console.log(nums); // [1, 2, 3, 4, 5, 6, 8, 9]

📜 Spread и Rest с объектами

const user = { name: 'Alice', age: 25 }; // Spread - копирование объекта const copy = {...user}; // Добавление свойств const userWithEmail = { ...user, email: 'alice@example.com' }; // Слияние объектов const address = {city: 'Moscow', country: 'Russia'}; const fullUser = {...user, ...address}; // Rest - сбор оставшихся свойств const {name, ...rest} = fullUser; console.log(name); // "Alice" console.log(rest); // {age: 25, city: 'Moscow', country: 'Russia'}

📜 Оператор ?. (optional chaning)

Оператор ?. или optional chaning-оператор позволяет проверить объект и его свойства и методы на null и undefined, и если объект или его свойства/методы определены, то обратиться к его свойствам или методам:

const tom = null; const bob = {name: "Bob"}; function printName(person){ console.log(person.name); } printName(tom); // Uncaught TypeError: Cannot read properties of null (reading "name") printName(bob);

В данном случае переменная tom равна null, соответственно у нее нет свойства name. И при передаче этого объекта в функцию printName мы получим ошибку. В этом случае мы можем перед обращением к объекту проверять его на null и undefined:

const tom = null; const bob = {name: "Bob"}; function printName(person){ if(person !== null && person !== undefined) console.log(person.name); } printName(tom); printName(bob); // Bob

Также мы можем сократить проверку:

function printName(person){ if(person) console.log(person.name); }

Если person равен null или undefined, то if(person) возвратит false.

Однако оператор ?. предлагает более элегантный способ решения:

const tom = null; const bob = {name: "Bob"}; function printName(person){ console.log(person?.name); } printName(tom); // undefined printName(bob); // Bob

После названия объекта указывается оператор ?. - если значение не равно null и undefined, то идет обращение к свойству/методу, которые указаны после точки. Если же значени равно null/undefined, то обращения к свойству/методу не происходит. И на консоли мы увидим undefined.

Данный оператор можно использовать перед обращением как к свойствам, так и к методам объекта:

const tom = undefined; const bob = { name: "Bob", sayHi(){ console.log(`Hi! I am ${this.name}`); } }; console.log(tom?.name); // undefined console.log(bob?.name); // Bob tom?.sayHi(); // не выполняется bob?.sayHi(); // Hi! I am Bob

В данном случае обращение к свойству name и методу sayHi() происходит только в том случае, если объекты tom и bob не равны null/undefined.

Более того далее по цепочке вызывов можно проверять наличие свойства или метода в объекте.

obj.val?.prop // проверка свойства obj.arr?.[index] // провера массива obj.func?.(args) // проверка функции

Проверка свойства

Объект может быть определен, однако не иметь какого-то свойства:

const tom = { name: "Tom"}; const bob = { name: "Bob", company: { title: "Microsoft" } }; console.log(tom.company?.title); // undefined console.log(bob.company?.title); // Microsoft

Подобным образом мы можем обращаться к свойствам объекта с помощью синтаксиса массивов:

const tom = { name: "Tom"}; const bob = { name: "Bob", company: { title: "Microsoft" } }; console.log(tom.company?.["title"]); // undefined console.log(bob.company?.["title"]); // Microsoft

Проверка свойства-массива

Аналогично мы можем проверять наличие свойства-массива перед обращением к его элементам:

const tom = { name: "Tom"}; const bob = { name: "Bob", languages: ["javascript", "typescript"] }; console.log(tom.languages?.[0]); // undefined console.log(bob.languages?.[0]); // javascript

Проверка метода

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

const tom = { name: "Tom"}; const bob = { name: "Bob", say(words){ console.log(words); } }; console.log(tom.say?.("my name is Tom")); // undefined console.log(bob.say?.("my name is Bob")); // my name is Bob

Цепочка проверок

С помощью оператора ?. можно создавать цепочки проверок, последовательно проверяя, представляет ли значение null/undefined:

const sam = {name: "Sam"}; const tom = { name: "Tom", company: { title: "Google"} }; const bob = { name: "Bob", company: { title: "Microsoft", print(){ console.log(`Компания ${this.title}`) } } }; sam?.company?.print?.(); // не вызывается - нет свойства company tom?.company?.print?.(); // не вызывается - нет метода print bob?.company?.print?.(); // Компания Microsoft

📜 Константные объекты. Запрет изменения объекта

Язык JavaScript позволяет нам динамически менять свойства объектов, добавлять в объекты новые свойства и методы или удалять уже имеющиеся. Однако, подобные изменения объекта могут быть нежелательны. И JavaScript предоставляет для этого три механизма:

  • Запрет расширения объектов
  • Закрытие (sealing) объектов
  • Заморозка (freezing) объектов

Запрет расширения объектов

Метод Object.preventExtensions() позволяет запретить расширение объекта, то есть в этот объект нельзя добавлять новые свойства и методы. Метод Object.preventExtensions() в качестве параметра принимает целевой объект, для которого надо установить запрет на расширение.

Сначала возьмем пример, где мы успешно добавляем новое свойство:

const tom = {name: "Tom"}; // добавляем в объект tom новое свойство - company tom.company = "Localhost"; console.log(`Name: ${tom.name} Company: ${tom.company}`); // Name: Tom Company: Localhost

Здесь в объект tom добавляется новое свойство company. После добавления мы можем использовать это свойство.

Теперь запретим расширение, применив метод Object.preventExtensions():

const tom = {name: "Tom"}; Object.preventExtensions(tom); // запрещаем расширение объекта tom tom.company = "Localhost"; // пытаемся добавить в объект tom новое свойство console.log(`Name: ${tom.name} Company: ${tom.company}`); // Name: Tom Company: undefined

В итоге даже если мы попытаемся определить для объекта новое свойство, оно не будет добавлено. А при попытке обратиться к подобному свойству мы получим undefined

Иногда может возникнуть необходимость определить, является ли объект расширяемым. Например, если объект расширяем, мы можем добавить в его свойства и затем использовать эти свойства. Для проверки расширяемости можно использовать метод Object.isExtensible(). В этот метод передается тестируемый объект. И если объект поддерживает расширение, то метод возвращает true, иначе возвращается false:

const tom = {name: "Tom"}; console.log(Object.isExtensible(tom)); // true Object.preventExtensions(tom); // запрещаем расширение объекта tom console.log(Object.isExtensible(tom)); // false

Закрытие объектов

Закрытие или "запечатывание" объектов (sealing) также позволяет запретить расширение объектов. Но кроме того, также запрещает настройку уже существующих свойств. Для закрытия объектов применяется метод Object.seal().

Сначала посмотрим, что мы можем сделать с объектом без применения Object.seal():

const tom = {name: "Tom"}; // для свойства name запрещаем изменение Object.defineProperty(tom, "name", { writable: false}); tom.name = "Tomas"; // добавляем новое свойство - age tom.age = 39; console.log(`Name: ${tom.name} Age: ${tom.age}`); // Name: Tom Age: 39 // для свойства name разрешаем изменение Object.defineProperty(tom, "name", { writable: true}); tom.name = "Tomas"; console.log(`Name: ${tom.name} Age: ${tom.age}`); // Name: Tomas Age: 39

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

Теперь применим метод Object.seal():

const tom = {name: "Tom"}; Object.seal(tom); // закрываем объект tom от расширения и изменения конфигурации // для свойства name запрещаем изменение Object.defineProperty(tom, "name", { writable: false}); tom.name = "Tomas"; // добавляем новое свойство - age tom.age = 39; console.log(`Name: ${tom.name} Age: ${tom.age}`); // Name: Tom Age: undefined // для свойства name разрешаем изменение Object.defineProperty(tom, "name", { writable: true}); // Uncaught TypeError: // Cannot redefine property: name

После закрытия объекта методом Object.seal(tom) мы не сможем добавить в объект новое свойство. Соответственно в примере выше свойство tom.age будет равно undefined. И также мы не сможем повторно изменить конфигурацию свойства. Так, здесь при втором вызове метода Object.defineProperty() для свойства name мы столкнемся с ошибкой "Uncaught TypeError: Cannot redefine property: name".

Для проверки, является ли объект закрытым, мы можем использовать метод Object.isSealed() - если объект закрыт, метод возвращает true. Стоит отметить, что поскольку закрытый объект нерасширяем, то метод Object.isExtensible() возвращает для него false:

const tom = {name: "Tom"}; console.log(Object.isExtensible(tom)); // true console.log(Object.isSealed(tom)); // false Object.seal(tom); // закрываем объект tom console.log(Object.isExtensible(tom)); // false console.log(Object.isSealed(tom)); // true

Запрет на изменение значений свойств

Заморозка или freezing позволяет запретить изменение значений свойств, то есть позволяет сделать объект в полной мере константным. Так, просто определить объект как обычную константу с помощью оператора const недостаточно. Например:

const tom = {name: "Tom"}; tom.name= "Tomas"; console.log(tom.name); // Tomas

Здесь мы видим, что свойство объекта изменило свое значение, хотя объект определен как константа.

Оператор const лишь влияет на то, что мы не можем присвоить константе новое значение, например, как в следующем случае:

const tom = {name: "Tom"}; tom = {name: "Sam"}; // Ошибка - нельзя константе присвоить значение второй раз

Тем не менее значения свойств объекта мы можем изменять.

Чтобы сделать объект действительно константным, необходимо применить специальный метод Object.freeze(). В этот метод в качестве параметра передается объект, который надо сделать константным:

const tom = {name: "Tom"}; Object.freeze(tom); tom.name= "Tomas"; // значение свойства нельзя изменить console.log(tom.name); // Tom

Для проверки, можно ли изменить значения свойств объекта, применяется метод Object.isFrozen() - если значения свойств изменить нельзя, он возвращает true

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

const tom = {name: "Tom"}; console.log(Object.isExtensible(tom)); // true console.log(Object.isSealed(tom)); // false console.log(Object.isFrozen(tom)); // false Object.freeze(tom); console.log(Object.isExtensible(tom)); // false console.log(Object.isSealed(tom)); // true console.log(Object.isFrozen(tom)); // true

ООП. Классы

Классы (ES6)

С внедрением стандарта ES2015 (ES6) в JavaScript появился новый способ определения объектов - с помощью классов. Класс представляет описание объекта, его состояния и поведения, а объект является конкретным воплощением или экземпляром класса. По сути синтаксис классов является альтернативной конструкцией, которая, как и функции-конструкторы, позволяет определить новый тип объектов.

Но стоит отметить, что несмотря на поддержку классов, JavaScript все же не является классическим объектно-ориентированным языком программирования как Java или C#. Классы JavaScript по сути представляют то, что называют "синтаксический сахар" над функциями-конструкторами - более удобные конструкции для создания объектов. И в реальности в JavaScript объекты по прежнему создаются не на основе классов, а на основе объектов или прототипов.

Определение класса

Для определения класса используется ключевое слово class:

class Person{ }

После слова class идет название класса (в данном случае класс называется Person), и затем в фигурных скобках определяется тело класса.

Это наиболее расспространенный способ определения класса. Но есть и другие способы. Так, также можно определить анонимный класс и присвоить его переменной или константе:

const Person = class{}

В принципе мы можем создать и неанонимный класс и присвоить его переменной или константе:

const User = class Person{}

Создание объектов

Класс - это общее представление некоторых сущностей или объектов. Конкретным воплощением этого представления, класса является объект. И после определения класса мы можем создать объекты класса с помощью конструктора:

class Person{} const tom = new Person(); const bob = new Person();

Для создания объекта с помощью конструктора сначала ставится ключевое слово new. Затем собственно идет вызов конструктора - по сути вызов функции по имени класса. По умолчанию классы имеют один конструктор без параметров. Поэтому в данном случае при вызове конструктора в него не передается никаких аргументов.

Стоит отметить, что в отличие от функций, чтобы использовать класс, его надо сначала определить. Например, в следующем коде мы получим ошибку, так как пытаемся использовать класс до его определения:

const tom = new Person(); // ! Ошибка - Uncaught ReferenceError: // Cannot access 'Person' before initialization class Person{}

Если определение класса присвоено переменной или константе, то мы можем использовать имя этой переменной/константы для создания объектов класса:

const User = class Person{} const tom = new User(); console.log(tom);

Выше в коде несмотря на то, что мы используем вызов new User(), в реальности создаваемый объект будет представлять класс Person.

Пример создания объекта анонимного класса:

const Person = class{} const tom = new Person(); console.log(tom);

Поля и свойства класса

Для хранения данных или состояния объекта в классе используются поля и свойства.

Итак, выше был определен класс Person, который представлял человека. У человека есть отличительные признаки, например, имя и возраст. Определим в классе Person поля для хранения этих данных:

class Person{ name; age; } const tom = new Person(); tom.name = "Tom"; tom.age = 37; console.log(tom.name); // Tom console.log(tom.age); // 37

Определение поля фактически просто представляет его название:

name; age;

Так, здесь определено поле name для хранения имени человека, и поле age для хранения возраста человека.

После создания объекта класса мы можем обратиться к этим полям. Для этого после имени объекта через точку указывается имя поля:

tom.name = "Tom"; // установим значение поля console.log(tom.name); // получим значение свойства

В примере выше поля класса также можно назвать свойствами. По сути свойства представляют доступные извне или публичные поля класса. Дальше мы подробно разберем, когда поля бывают непубличные, то есть недоступными извне. Но пока стоит понимать, что свойства и публичные поля - это одно и то же. И в примере выше поля name и age также можно назвать свойствами.

При необходимости мы можем присвоить полям некоторые начальные значения:

class Person{ name = "Unknown"; age= 18; } const tom = new Person(); console.log(tom.name); // Unknown tom.name = "Tom"; console.log(tom.name); // Tom

Поведение класса и его методы

Кроме хранения данных, которые определяют состояние объекта, класс может иметь методы, которые определяют поведение объекта - действия, которые выполняет объект. Например, определим в классе Person пару методов:

class Person{ name; age; move(place){ console.log(`Go to ${place}`); } eat(){ console.log("Eat apples"); } } const tom = new Person(); tom.move("Hospital"); // Go to Hospital tom.move("Cinema"); // Go to Cinema tom.eat(); // Eat apples

Здесь определен метод move(), который представляет условное передвижение человека. В качестве параметра метод принимает место, к которому идет человек. Второй метод - eat() - представляет условный процесс питания.

Обращение к полям и методам внутри класса. Слово this

Что если мы хотим в методах класса обратиться к полям класса или к другим его методам? В этом случае перед именем поля/свойства или метода указывается ключевое слово this, которое в данном случае указывает на текущий объект.

Например, определим метод, который выводит информацию об объекте:

class Person{ name; age; print(){ console.log(`Name: ${this.name} Age: ${this.age}`); } } const tom = new Person(); tom.name = "Tom"; tom.age = 37; tom.print(); // Name: Tom Age: 37 const bob = new Person(); bob.name = "Bob"; bob.age = 41; bob.print(); // Name: Bob Age: 41

Определение конструктора

Для создания объекта класса используется конструктор:

const bob = new Person();

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

Но также мы можем определить в классах свои конструкторы:

class Person{ name; age; constructor(){ console.log("Вызов конструктора"); } print(){ console.log(`Name: ${this.name} Age: ${this.age}`); } } const tom = new Person(); // Вызов конструктора const bob = new Person(); // Вызов конструктора

Конструктор определяется с помощью метода с именем constructor. По сути это обычный метод, который может принимать параметры. В данном случае конструктор просто выводит на консоль некоторое сообщение. Соответственно при выполнении строки

const tom = new Person();

Мы увидим в консоли браузера соответствующее сообщение.

Как правило, цель конструктора - инициализация объекта некоторыми начальными данными:

class Person{ name; age; constructor(pName, pAge){ this.name = pName; this.age = pAge; } print(){ console.log(`Name: ${this.name} Age: ${this.age}`); } } const tom = new Person("Tom", 37); tom.print(); // Name: Tom Age: 37 const bob = new Person("Bob", 41); bob.print() // Name: Bob Age: 41

Здесь конструктор принимает два параметра и передает их значения полям класса. Соответственно при создании объекта мы можем передать в конструктор соответствующие значения для этих параметров:

const tom = new Person("Tom", 37);

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

class Person{ constructor(pName, pAge){ this.name = pName; this.age = pAge; } print(){ console.log(`Name: ${this.name} Age: ${this.age}`); } } const tom = new Person("Tom", 37); tom.print(); // Name: Tom Age: 37 const bob = new Person("Bob", 41); bob.print() // Name: Bob Age: 41

Выражения классов

JavaScript также позволяет определять классы через выражения классов (class expression). Класс присваивается переменной/константе, через которую далее можно ссылаться на этот класс:

const Person = class { constructor(pName, pAge){ this.name = pName; this.age = pAge; } print(){ console.log(`Name: ${this.name} Age: ${this.age}`); } } const tom = new Person("Tom", 38); tom.print();

Получение прототипа

Как и функция-конструктор, класс имеет прототип, который можно получить стандартными способами:

class Person{ constructor(pName, pAge){ this.name = pName; this.age = pAge; } print(){ console.log(`Name: ${this.name} Age: ${this.age}`); } } const tom = new Person("Tom", 37); // получаем прототип console.log(Person.prototype); // через свойство prototype класса console.log(tom.__proto__); // через свойство __proto__ объекта console.log(Object.getPrototypeOf(tom)); // через функцию Object.getPrototypeOf и объект console.log(tom.constructor); // получение функции-конструктора (определения типа) объекта

Объявление класса

class User { // Конструктор constructor(name, age) { this.name = name; this.age = age; } // Методы greet() { console.log(`Привет, я ${this.name}`); } celebrate() { this.age++; console.log(`Мне ${this.age}`); } } const user = new User('Alice', 25); user.greet(); // "Привет, я Alice" user.celebrate(); // "Мне 26"

📜 Приватные поля и методы (ES2022)

В прошлых темах мы использовали класс, свойства и методы которого были досупны извне, и соответственно мы могли к им обратиться в любом месте программы. Например:

class Person{ constructor(name, age){ this.name = name; this.age = age; } print(){ console.log(`Name: ${this.name} Age: ${this.age}`); } } const tom = new Person("Tom", 37); tom.name = "Sam"; tom.age = -45; tom.print(); // Name: Sam Age: -45

С одной стороны, это замечательно, что мы можем использовать функциональность класса в своей программе, обращаться к его свойствам, методам. Но это может быть источником потенциальных проблем. Как видно в примере выше, мы можем изменить имя человека. Но что, если мы не хотим, чтобы в программе можно было менять начальное имя? Также мы можем изменить возраст человека, но изменить его на любое число, которое может предствлять некорректный возраст (например, отрицательный).

Иногда необходимо, чтобы к данным или действиям извне класса нельзя было обратиться, и чтобы к ним можно было обращаться только внутри этого же класса. Или иными словами, сделать свойства и методы класса приватными - доступными только для этого класса. И язык JavaScript предоставляет для этого необходимый инструментарий. Для этого название полей и методов должно начинаться с символа решетки #.

Приватные поля

Названия приватных полей предваряется символом #:

class Person{ #name; #age; constructor(name, age){ this.#name = name; this.#age = age; } print(){ console.log(`Name: ${this.#name} Age: ${this.#age}`); } } const tom = new Person("Tom", 37); // tom.#name = "Sam"; // ! Ошибка - нельзя обратиться к приватному полю // tom.#age = -45; //! Ошибка - нельзя обратиться к приватному полю tom.print(); // Name: Tom Age: 37

В примере выше определены приватные поля #name и #age. Установить и получить их значение можно только внури класса Person. Вне его они не доступны. Поэтому при попытке обратиться к ним через имя объекта, мы получим ошибку:

tom.#name = "Sam"; // ! Ошибка - нельзя обратиться к приватному полю tom.#age = -45; // ! Ошибка - нельзя обратиться к приватному полю

Если потребуется как-то к ним все-таки обратиться, то мы можем определить для этого методы. Например, выше метод print() получает их значения и выводит на консоль. Подобным образом можно определить и методы для установки значения:

class Person{ #name; #age= 1; constructor(name, age){ this.#name = name; this.setAge(age); } setAge(age){ if (age > 0 && age < 110) this.#age = age; } print(){ console.log(`Name: ${this.#name} Age: ${this.#age}`); } } const tom = new Person("Tom", 37); tom.print(); // Name: Tom Age: 37 tom.setAge(22); tom.print(); // Name: Tom Age: 22 tom.setAge(-1234); tom.print(); // Name: Tom Age: 22

В данном случае метод setAge проверяет корректность переданного значения, и если оно корректно, переустанавливает возраст.

Еще пример:

class BankAccount { #balance = 0; // Приватное поле (начинается с #) constructor(owner) { this.owner = owner; } deposit(amount) { this.#balance += amount; console.log(`Баланс: ${this.#balance}`); } #validateAmount(amount) { // Приватный метод return amount > 0; } } const account = new BankAccount('Alice'); account.deposit(100); // "Баланс: 100" // console.log(account.#balance); // Ошибка! Приватное поле

Приватные методы

Названия приватных методов также предваряются символом #:

class Person{ #name = "undefined"; #age = 1; constructor(name, age){ this.#name = this.#checkName(name); this.setAge(age); } #checkName(name){ if(name!=="admin") return name; } setAge(age){ if (age > 0 && age < 110) this.#age = age; } print(){ console.log(`Name: ${this.#name} Age: ${this.#age}`); } } const tom = new Person("Tom", 37); tom.print(); // Name: Tom Age: 37 const bob = new Person("admin", 41); bob.print(); // Name: Undefined Age 41 //let personName = bob.#checkName("admin"); // ! Ошибка - нельзя обратится к приватному методу

В примере выше определен приватный метод #checkName(), который выполняет условную проверку имени - ели оно не равно "admin", то возвращает переданное значение. (К примеру, мы не хотим, чтобы имя пользователя было "admin"). И также вне класса мы не можем обратиться к этому методу:

let personName = bob.#checkName("admin"); // ! Ошибка

Как правило, подобные приватные методы используются для выполнения каких-то вспомогательных действий, как, например, валидация в примере выше, и которые нет смысла делать доступнми из вне.

📜 Статические поля и методы

Кроме обычных полей и методов класс может определять статические поля и методы. В отличие от обычных полей/свойств и методов они относятся ко всему классу, а не к отдельному объекту.

Статические поля

Статические поля хранят состояния класса в целом, а не отдельного объекта. Перед названием статического поля ставится ключевое слово static. Например:

class Person{ static retirementAge = 65; constructor(name, age){ this.name = name; this.age = age; } print(){ console.log(`Имя: ${this.name} Возраст: ${this.age}`); } } console.log(Person.retirementAge); // 65 Person.retirementAge = 62; console.log(Person.retirementAge); // 62

Здесь в классе Person определено статическое поле retirementAge, которое хранит условный возраст выхода на пенсию:

static retirementAge = 65;

Это поле относится ко всему классу Person в целом и описывает состояние всего класса в целом. Ведь, как правило, есть некий общий возраст выхода на пенсию, принтый для всех (не берем в расчет отдельные случаи для отдельных профессий). И поэтому для обращения к статическому полю применяется имя класса, а не имя какого-либо объекта. Используя имя класса, мы можем получить или установить его значение:

Person.retirementAge = 62; console.log(Person.retirementAge); // 62

При этом мы НЕ можем в нестатических методах и конструкторе класса обращаться к этим полям через this, наподобие следующего:

print(){ console.log(`Имя: ${this.name} Возраст: ${this.age}`); // к статическому полю нельзя обратиться через this console.log(`Пенсионный возраст: ${this.retirementAge}`); }

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

print(){ console.log(`Имя: ${this.name} Возраст: ${this.age}`); console.log(`Пенсионный возраст: ${Person.retirementAge}`); }

Статические методы

Статические методы, как и статические поля, определяются для всего класса в целом, а не для отдельного объекта. Для их определения перед названием метода ставится оператор static. Например:

class Person{ constructor(name, age){ this.name = name; this.age = age; } print(){ console.log(`Имя: ${this.name} Возраст: ${this.age}`); } static printClassInfo(){ console.log("Класс Person представляет человека"); } } Person.printClassInfo(); // Класс Person представляет человека

Здесь определен статический метод printClassInfo(), который для простоты просто выводит некоторое сообщение. В отличие от обычных нестатических методов, которые определяют поведение объекта, статические методы определяют поведение для всего класса. Поэтому для их вызова применяется имя класса, а не имя объекта:

Person.printClassInfo();

Поскольку статический метод относится классу в целом, а не к объекту, то мы НЕ можем обращаться в нем к нестатическим полям/свойствам и методам объекта, наподобие следующего:

class Person{ constructor(name, age){ this.name = name; this.age = age; } print(){ console.log(`Имя: ${this.name} Возраст: ${this.age}`); } static printAge(){ console.log(this.age); } // для статического метода this.age не существует } Person.printAge(); // undefined

Если необходимо в статическом методе обратиться к свойствам объекта, то мы можем опеределить в методе параметр, через который в метод будет передаваться объект:

class Person{ constructor(name, age){ this.name = name; this.age = age; } static print(person){ console.log(`Имя: ${person.name} Возраст: ${person.age}`); } } const tom = new Person("Tom", 37); const bob = new Person("Bob", 41); Person.print(tom); // Tom 37 Person.print(bob); // Bob 41

Однако мы можем использовать в статических методах слово this для обращения к статическим полям и другим статическим методам:

class Person{ static retirementAge = 65; constructor(name, age){ this.name = name; this.age = age; } print(){ console.log(`Имя: ${this.name} Возраст: ${this.age}`); } static calculateRestAges(person){ if(this.retirementAge > person.age){ const restAges = this.retirementAge - person.age; console.log(`До пенсии осталось ${restAges} лет`); } else console.log("Вы уже на пенсии"); } } const tom = new Person("Tom", 37); Person.calculateRestAges(tom); // До пенсии осталось 28 лет const bob = new Person("Bob", 71); Person.calculateRestAges(bob); // Вы уже на пенсии

Здесь определен статический метод calculateRestAges(), который расчитывает, сколько определенному человеку осталось до пенсии. И для вычисления он обращается к статическому полю retirementAge:

Еще пример:

class MathHelper { // Статический метод (вызывается через класс) static sum(a, b) { return a + b; } static multiply(a, b) { return a * b; } } // Вызов без создания экземпляра console.log(MathHelper.sum(5, 3)); // 8 console.log(MathHelper.multiply(4, 2)); // 8 // const helper = new MathHelper(); // helper.sum(5, 3); // Ошибка! Статический метод недоступен

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

Как и обычные поля и методы статические поля и методы могут быть приватными. Такие поля и методы доступны только из других статических методов класса:

class Person{ static #retirementAge = 65; constructor(name, age){ this.name = name; this.age = age; } print(){ console.log(`Имя: ${this.name} Возраст: ${this.age}`); } static calculateRestAges(person){ if(this.#retirementAge > person.age){ const restAges = this.#retirementAge - person.age; console.log(`До пенсии осталось ${restAges} лет`); } else console.log("Вы уже на пенсии"); } } // console.log(Person.#retirementAge); // ! Ошибка: поле retirementAge -приватное const tom = new Person("Tom", 37); Person.calculateRestAges(tom); // До пенсии осталось 28 лет const bob = new Person("Bob", 71); Person.calculateRestAges(bob); // Вы уже на пенсии

В отличие от предыдущего примера теперь статическое поле retirementAge - приватное. И теперь к нему можно обратиться только внутри статических методов класса.

📜 Свойства и методы доступа

Для опосредования доступа к свойствам класса в последних стандартах JavaScript была добавлена поддержка методов доступа - get и set. Сначала рассмотрим проблему, с которой мы можем столкнуться:

class Person{ constructor(name, age){ this.name = name; this.age = age; } } const tom = new Person("Tom", 37); console.log(tom.age); // 37 tom.age = -15; console.log(tom.age); // -15

Класс Person определяет два свойства - name (имя) и age (возраст человека), значения которых мы можем получить или установить. Но что если мы передадим некорректные значения? Так, в примере выше свойству age передается отрицательное число, но возраст не может быть отрицательным.

Чтобы выйти из этой ситуации, мы можем определить приватное поле age, к которому можно было бы обратиться только из текущего класса. А для получения или установки его значения создать специальные методы:

class Person{ #ageValue = 1; constructor(name, age){ this.name = name; this.setAge(age); } getAge(){ return this.#ageValue; } setAge(value){ if(value > 0 && value < 110) this.#ageValue = value; } } const tom = new Person("Tom", 37); console.log(tom.getAge()); // 37 tom.setAge(-15); console.log(tom.getAge()); // 37

Теперь возраст хранится в приватном поле ageValue. При его установке в методе setAge() проверяется переданное значение. И установка происходит, если только передано корректное значение. А метод getAge() возвращает значение этой переменной.

Но есть и другое решение - применение методов доступа get и set.

// определение приватного поля #field; set field(value){ this.#field= value; } get field(){ return this.#field; }

Оба метода - get и set имеют одинаковые названия. Как правило, они опосредуют доступ к некоторому приватному полю. Метод set предназначен для установки. Он принимает в качестве параметра новое значение. Далее в методе set мы можем выполнить ряд действий при установке.

Метод get предназначен для получения значения. Здесь мы можем определить какую-нибудь логику при возвращении значения.

class Person{ #ageValue = 1; constructor(name, age){ this.name = name; this.age = age; } set age(value){ console.log(`Передано ${value}`); if(value > 0 && value < 110) this.#ageValue = value; } get age(){ return this.#ageValue; } } const tom = new Person("Tom", 37); console.log(tom.age); tom.age = -15; console.log(tom.age);

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

console.log(tom.age);

а не

console.log(tom.age());

То есть при обращении tom.age фактически будет срабатывать метод get, который возвратит значение поля ageValue.

А при вызове

tom.age = -15;

будет срабатывать метод set, который получит передаваемое ему значение (здесь число -15) через единственный параметр. И далее в самом методе set мы можем решить, надо ли устанавливать это значение.

Свойства, доступные только для чтения

Выше применялись оба метода get и set, соответственно значение поля можно было и получить, и установить. Однако в реальност мы можем использовать только один из них. Например, мы можем оставить только метод get и тем самым сделать свойство доступным только для чтения.

Например, изменим пример выше и сделаем свойство name доступным только для чтения:

class Person{ #age = 1; #name; constructor(name, age){ this.#name = name; this.age = age; } //set name(value){ this.#name = value; } get name(){ return this.#name; } set age(value){ if(value > 0 && value < 110) this.#age = value; } get age(){ return this.#age; } } const tom = new Person("Tom", 37); console.log(tom.name); // Tom tom.name = "Bob"; // Это ничего не даст console.log(tom.name); // Tom - значение не изменилось

В данном случае вместо общедоступного свойства name определена приватное поле #name. Его можно установить только изнутри класса, что мы и делаем в конструкторе класса. Однако из вне его можно только прочитать с помощью метода get. Поэтому попытка установки свойства

tom.name = "Bob";

ни к чему не приведет

Свойства только для установки

Также мы можем сделать свойство доступным только для записи, оставив только метод set. Например, добавим новое свойство id, которое будет доступно только для записи:

class Person{ #id; constructor(name, age, id){ this.name = name; this.age = age; this.id = id; } set id(value){ this.#id = value;} print(){ console.log(`id: ${this.#id} name: ${this.name} age: ${this.age}`); } } const tom = new Person("Tom", 37, 1); tom.print(); // id: 1 name: Tom age: 37 tom.id = 55; // устанавливаем значение свойства id tom.print(); // id: 55 name: Tom age: 37 console.log(tom.id); // undefined - значение свойства id нельзя получит

Здесь определено свойство id, которое устанавливает значение приватного поля #id. Но поскольку метода get для этого свойства не определено, то при попытке получить значение свойства id, мы получим undefined:

console.log(tom.id); // undefined - значение свойства id нельзя получить

Свойства без обращения к полям

Стоит отметить, что методы get и set необязательно должны обращаться к приватным или неприватным полям. Это могут быть и вычисляемые свойства. Например:

class Person{ constructor(firstName, lastName){ this.firstName = firstName; this.lastName = lastName; } get fullName(){ return `${this.firstName} ${this.lastName}` } } const tom = new Person("Tom", "Smith"); console.log(tom.fullName); // Tom Smith

В данном случае свойство для чтения fullName возращает фактически объединение двух свойств - firstName и lastName.

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

class Person{ constructor(firstName, lastName){ this.firstName = firstName; this.lastName = lastName; } get fullName(){ return `${this.firstName} ${this.lastName}` } set fullName(value){ [this.firstName, this.lastName] = value.split(" "); } } const tom = new Person("Tom", "Smith"); console.log(tom.fullName); // Tom Smith tom.fullName = "Tomas Jefferson"; console.log(tom.lastName); // Jefferson

В данном случае метод set свойства fullName в качестве параметра получает некоторую строку и с помощью ее метода split разбивает по пробелу и получает массив подстрок, которые были разделены пробелом. То есть, теоретически мы рассчитываем, что будет передано что-то наподобие "Tom Smith", а после разделения по пробелу свойство firstName получит значение "Tom", а свойтсво lastName - значение "Smith". Стоит отметить, что для простоты и целй демонстрации здесь мы не рассматриваем исключительные ситуации, когда передается пустая строка или строка, которая не делится по пробелу на две части и т.д.

В итоге при получении нового значения

tom.fullName = "Tomas Jefferson";

Метод set разобьет его по пробелу, и первый элемент массива будет передан свойству firstName, а второй - свойству lastName.

Геттеры и сеттеры

class User { constructor(name, birthYear) { this.name = name; this.birthYear = birthYear; } // Геттер (обращение как к свойству) get age() { return new Date().getFullYear() - this.birthYear; } // Сеттер set age(value) { this.birthYear = new Date().getFullYear() - value; } } const user = new User('Alice', 1998); console.log(user.age); // 27 (в 2025) user.age = 30; // Устанавливаем возраст console.log(user.birthYear); // 1995

📜 Наследование

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

class Person{ name; age; print(){ console.log(`Name: ${this.name} Age: ${this.age}`); } } class Employee{ name; age; company; print(){ console.log(`Name: ${this.name} Age: ${this.age}`); } work(){ console.log(`${this.name} works in ${this.company}`); } } const tom = new Person(); tom.name = "Tom"; tom.age= 34; const bob = new Employee(); bob.name = "Bob"; bob.age = 36; bob.company = "Google"; tom.print(); // Name: Tom Age: 34 bob.print(); // Name: Bob Age: 36 bob.work(); // Bob works in Google

Здесь определены два класса - Person, который представляет человека, и Employee, который представляет работника предприятия. Оба класса прекрасно работают, мы можем создавать их объекты, но мы также видим, что класс Employee повторяет функционал класса Person, так как работник также является человеком, для которого также можно определить свойства name и age и метод print.

Наследование позволяет одним классам автоматически получить функцонал других классов и тем самым сократить объем кода. Для наследования одного класса от другого применяется ключевое слово extends:

class Base{} class Derived extends Base{}

После названия класса-наследника ставится ключевое слово extends, после которого идет имя класса, от которого мы хотим унаследовать функционал.

Так, изменим классы Person и Employee, применив наследование:

class Person{ name; age; print(){ console.log(`Name: ${this.name} Age: ${this.age}`); } } class Employee extends Person{ company; work(){ console.log(`${this.name} works in ${this.company}`); } } const tom = new Person(); tom.name = "Tom"; tom.age= 34; const bob = new Employee(); bob.name = "Bob"; bob.age = 36; bob.company = "Google"; tom.print(); // Name: Tom Age: 34 bob.print(); // Name: Bob Age: 36 bob.work(); // Bob works in Google

Теперь класс Employee наследуется от класса Person. В этом отношении класс Person еще называется базовым или родительским классом, а Employee - производным классом или классом-наследником. Поскольку класс Employee наследует функционал от Person, то нам нет необходимости заново определять в нем свойства name, age и метод print. В итоге код класса Employee получился короче, а результат программы тот же.

Наследование класса с конструктором

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

class Person{ constructor(name, age){ this.name = name; this.age = age; } print(){ console.log(`Name: ${this.name} Age: ${this.age}`); } } class Employee extends Person{ company; work(){ console.log(`${this.name} works in ${this.company}`); } } const tom = new Person("Tom", 34); tom.print(); // Name: Tom Age: 34 const sam = new Employee("Sam", 25); // унаследованный конструктор sam.print(); // Name: Sam Age: 25

В данном случае класс Person определяет конструктор с двумя параметрами. В этом случае класс Employee наследует его и использует для создания объекта Employee.

Еще пример:

// Родительский класс class Animal { constructor(name) { this.name = name; } speak() { console.log(`${this.name} издаёт звук`); } } // Дочерний класс class Dog extends Animal { constructor(name, breed) { super(name); // Вызов конструктора родителя this.breed = breed; } // Переопределение метода speak() { console.log(`${this.name} лает`); } // Новый метод wagTail() { console.log(`${this.name} виляет хвостом`); } } const dog = new Dog('Бобик', 'Овчарка'); dog.speak(); // "Бобик лает" dog.wagTail(); // "Бобик виляет хвостом"

Определение конструктора в классе-наследнике и ключевое слово super.

Производный класс также может определить свой конструктор. Если производный класс определяет конструктор, то в нем должен быть вызван конструктор базового класса. Для обращения в производном классе к функциональности базового класса, в том числе для обращения к конструктору базового класса, применяется ключевое слово super

class Person{ constructor(name, age){ this.name = name; this.age = age; } print(){ console.log(`Name: ${this.name} Age: ${this.age}`); } } class Employee extends Person{ constructor(name, age, company){ super(name, age); this.company = company; } work(){ console.log(`${this.name} works in ${this.company}`); } } const tom = new Person("Tom", 34); tom.print(); // Name: Tom Age: 34 const sam = new Employee("Sam", 25, "Google"); sam.print(); // Name: Sam Age: 25 sam.work(); // Sam works in Google

Класс Employee определяет свой конструктор с тремя параметрами, первой строкой в котором идет обращение к конструктору базового класса Person:

super(name, age);

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

Другой пример:

class Animal { constructor(name) { this.name = name; } eat() { console.log(`${this.name} ест`); } } class Cat extends Animal { constructor(name, color) { super(name); // Обязательно перед использованием this! this.color = color; } eat() { super.eat(); // Вызов метода родителя console.log(`${this.name} мурлычет`); } } const cat = new Cat('Мурка', 'рыжий'); cat.eat(); // "Мурка ест" // "Мурка мурлычет"

Переопределение методов базового класса.

Производный класс, как и в случае с конструктором, может переопределять методы базового класса. Так, в примере выше метод print() класса Person выводит имя и возраст человека. Но что, если мы хотим, чтобы для работника метод print() выводил также и компанию? В этом случае мы можем определить в классе Employee свой метод print():

class Person{ constructor(name, age){ this.name = name; this.age = age; } print(){ console.log(`Name: ${this.name} Age: ${this.age}`); } } class Employee extends Person{ constructor(name, age, company){ super(name, age); this.company = company; } print(){ console.log(`Name: ${this.name} Age: ${this.age}`); console.log(`Company: ${this.company}`); } } const sam = new Employee("Sam", 25, "Google"); sam.print(); // Name: Sam Age: 25 // Company: Google

Однако в коде выше мы видим, что первая строка метода print() в классе Employee по сути повторяет код метода print() из класса Person. В данном случае это всего одна строка, но в другой ситуации повторяемый код мог бы больше. И чтобы не повторяться, мы опять же можем просто обратиться к реализации метода print() родительского класса Person через super:

class Person{ constructor(name, age){ this.name = name; this.age = age; } print(){ console.log(`Name: ${this.name} Age: ${this.age}`); } } class Employee extends Person{ constructor(name, age, company){ super(name, age); this.company = company; } print(){ super.print(); console.log(`Company: ${this.company}`); } } const sam = new Employee("Sam", 25, "Google"); sam.print(); // Name: Sam Age: 25 // Company: Google

То есть в данном случае вызов

super.print();

представляет вызов реализации метода из базового класса. Таким образом, с помощью this и super мы можем разграничить обращение к функциональности текущего класса или его базового класса.

Наследование и приватные поля и методы

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

class Person{ #name; constructor(name, age){ this.#name = name; this.age = age; } print(){ console.log(`Name: ${this.#name} Age: ${this.age}`); } } class Employee extends Person{ constructor(name, age, company){ super(name, age); this.company = company; } print(){ super.print(); console.log(`Company: ${this.company}`); } work(){ console.log(`${this.#name} works in ${this.company}`); // ! Ошибка - поле #name недоступно из Employee } }

В данном случае поле #name в классе Person определено как приватное, поэтому достуно только внутри этого класса. Поытка обратиться к этому полю в классе-наследнике Employee приведет к ошибке вне зависимости будет идти обращение через this.#name или super.#name. При необходимости в базовом классе можно определить геттеры и сеттеры, которые обращаются к приватным полям. А в классе-наследники через эти геттеры и сеттеры обращаться к приватным полям базового класса.

Проверка принадлежности объекта классу

Тот факт, что класс-наследник унаследован от некоторого базового класса говорит о том, что объект класса-наследника также является объектом базового класса. Объектом какого класса является объект, можно проверить с помощью оператора instanceof:

class Person{ constructor(name, age){ this.name = name; this.age = age; } print(){ console.log(`Name: ${this.name} Age: ${this.age}`); } } class Employee extends Person{ constructor(name, age, company){ super(name, age); this.company = company; } print(){ super.print(); console.log(`Works in ${this.company}`); } } class Manager extends Person{ constructor(name, age, company){ super(name, age); this.company = company; } print(){ super.print(); console.log(`Manager in ${this.company}`); } } const sam = new Employee("Sam", 25, "Google"); console.log(sam instanceof Person); // true console.log(sam instanceof Employee); // true console.log(sam instanceof Manager); // false

Здесь константа sam представляет объект класса Employee, который унаследован от Person, соответственно выражения sam instanceof Person и sam instanceof Employee возвратят true. А вот объектом класса Manager константа sam не является, поэтому выражение sam instanceof Manager возвратит false.

Глава 6. Массивы

📜 Создание массивов и объект Array

Для хранения набора данных в языке JavaScript предназначены массивы. Массивы в JavaScript представлены объектом Array. Объект Array предоставляет ряд свойств и методов, с помощью которых мы можем управлять массивом и его элементами.

Создание массива

  • Можно создать пустой массив, используя квадратные скобки или конструктор Array:
    const users = new Array(); const people = []; console.log(users); // Array[0] console.log(people); // Array[0]
  • Можно сразу же инициализировать массив некоторым количеством элементов:

    const users = new Array("Tom", "Bill", "Alice"); const people = ["Sam", "John", "Kate"]; console.log(users); // ["Tom", "Bill", "Alice"] console.log(people); // ["Sam", "John", "Kate"]
  • Можно определить массив и по ходу определять в него новые элементы:

    const users = []; users[1] = "Tom"; users[2] = "Kate"; console.log(users[1]); // "Tom" console.log(users[0]); // undefined

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

  • Еще один способ инициализации массивов представляет метод Array.of() - он принимает элементы и инициизирует ими массив:

    const people = Array.of("Tom", "Bob", "Sam"); console.log(people); // ["Tom", "Bob", "Sam"]

Array.from

И еще один способ представляет функция Array.from(). Она имеет много вариантов, рассмотрим самые распространенные:

Array.from(arrayLike) Array.from(arrayLike, function mapFn(element) { ... }) Array.from(arrayLike, function mapFn(element, index) { ... })
  • В качестве первого параметра - arrayLike функция принимает некий объект, который, условно говоря, "похож на массив", то есть может быть представлен в виде набора элементов. Это может быть и другой массив, это может быть и строка, которая по сути предоставляет набор символов. Вообщем какой-то набор элементов, который можно преобразовать в массив. Кроме того, это может и некий объект, в котором определено свойство length. Например:

    const array = Array.from("Hello"); console.log(array); // ["H", "e", "l", "l", "o"]

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

  • В качестве второго параметра передается функция преобразования, которая через первый параметр получает текущий элемент набора и возвращает некоторый результат его трансформации. Например:

    const numbers = [1, 2, 3, 4]; const array = Array.from(numbers, n => n * n); console.log(array); // [1, 4, 9, 16]

    В данном случае в функцию Array.from() передается массив чисел. Второй параметр - функция (в данном случае в ее роли выступает лямбда-выражение) запускается для каждого числа из этого массива и получает это число через параметр n. В самом лямбда-выражении возвращаем квадрат этого числа. В итоге Array.from() возвратит новый массив, в котором будут квадраты чисел из массива numbers.

  • И еще одна версия функции Array.from() в качестве второго параметра принимает функцию преобразования, в которую кроме элемента из перебираемого набора передается и индекс этого элемента:

    Array.from(arrayLike, function mapFn(element, index) { ... })

    Используем эту версию и передадим в функцию объект со свойством length:

    const array = Array.from({length:3}, (element, index) => { console.log(element); // undefined return index; }); console.log(array); // [0, 1, 2]

    Здесь в функцию передается объект, у которого свойство length равно 3. Для функции Array.from это будет сигналом, в возвращаемом массиве должно быть три элемента. При этом неважно, что функция преобразования из второго параметра принимает элемент набора (параметр element) - в данном случае он будет всегда undefined, тем не менее значение length:3 будет указателем, что возвращаемый массив будет иметь три элемента с соответственно индексами от 0 до 2. И через второй параметр функции преобразования - параметр index мы можем и получить текущий индекс элемента.

Тем не менее мы можем передать объект, где в качестве названий свойств применяются индексы. В этом случае объект превратится в массивоподобный объект, который можно перебрать:

const array = Array.from({length:3, "0": "Tom", "1": "Sam", "2": "Bob"}, (element) => { console.log(element); return element; }); console.log(array); // ["Tom", "Sam", "Bob"]

Или так:

// Способ 1: литерал массива const arr1 = [1, 2, 3, 4, 5]; // Способ 2: конструктор Array const arr2 = new Array(1, 2, 3); const arr3 = new Array(5); // [empty × 5] - массив из 5 пустых элементов // Способ 3: Array.of() - создаёт массив из аргументов // В отличие от new Array(), всегда создаёт массив из переданных значений const arr4 = Array.of(1, 2, 3); // [1, 2, 3] const arr5 = Array.of(5); // [5] - массив с одним элементом 5 const arr6 = Array.of(1, 'two', 3); // [1, "two", 3] // Способ 4: Array.from() - создаёт массив из итерируемого объекта const arr7 = Array.from('hello'); // ['h', 'e', 'l', 'l', 'o'] const arr8 = Array.from([1, 2, 3], x => x * 2); // [2, 4, 6] - с функцией const arr9 = Array.from({length: 5}, (_, i) => i); // [0, 1, 2, 3, 4]
💡 Array.of() vs new Array()
  • new Array(5) создаёт массив с 5 пустыми слотами
  • Array.of(5) создаёт массив с одним элементом: [5]
  • Array.from() может принимать функцию-преобразователь как второй аргумент

length

Чтобы узнать длину массива, используется свойство length:

const fruit = []; fruit[0] = "яблоки"; fruit[1] = "груши"; fruit[2] = "сливы"; console.log("В массиве fruit ", fruit.length, " элемента"); for(let i=0; i < fruit.length; i++) console.log(fruit[i]);

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

const users = []; // в массиве 0 элементов users[0] = "Tom"; users[1] = "Kate"; users[4] = "Sam"; for(let i=0; i < users.length;i++) console.log(users[i]);

Вывод браузера:

Tom
Kate
undefined
undefined
Sam

Несмотря на то, что для индексов 2 и 3 мы не добавляли элементов, но длиной массива в данном случае будет число 5. Просто элементы с индексами 2 и 3 будут иметь значение undefined.

📜 Массивы и spread-оператор

spread-оператор ( оператор . . . ) позволяет разложить массив на отдельные значения. Для этого перед массивом ставится многоточие:

...массив

Простейший пример:

const users = ["Tom", "Sam", "Bob"]; console.log(...users); // Tom Sam Bob

И, применяя этот оператор, мы можем наполнить один массив значениями из другого массива:

const users = ["Tom", "Sam", "Bob"]; console.log(users); // ["Tom", "Sam", "Bob"] const people1 = [...users]; const people2 = new Array(...users); const people3 = Array.of(...users); console.log(people1); // ["Tom", "Sam", "Bob"] console.log(people2); // ["Tom", "Sam", "Bob"] console.log(people3); // ["Tom", "Sam", "Bob"]

Объединение массивов

С помощью spread-оператора можно при создания нового массива передать ему значения сразу нескольких массивов. Например:

const men = ["Tom", "Sam", "Bob"]; const women = ["Kate", "Alice", "Mary"]; const people = [...men, "Alex", ...women]; console.log(people); // ["Tom", "Sam", "Bob", "Alex", "Kate", "Alice", "Mary"]

В данном случае в массив people передаются значения из массивов men, women, и, кроме того, мы можем дополнительно передать отдельные значения, которые не входят в эти массивы.

Передача аргументов функции

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

const people = ["Tom", "Sam", "Bob"]; function print(first, second, third){ console.log(first); console.log(second); console.log(third); } print(...people); // Tom // Sam // Bob

В данном случае функция print() принимает три параметра. Операция ...people при вызове функции позволяет разложить массив people на отдельные значения, которые передаются параметрам этой функции.

Стоит отметить, что это не то же самое, что передать при вызове функции массив:

print(people);

В этом случае весь массив будет передан первому параметру функции - параметру first, а остальные будут иметь значение undefined.

Еще один пример передачи параметрам значений из массива:

function sum(a, b, c){ const d = a + b + c; console.log(d); } sum(1, 2, 3); const nums = [4, 5, 6]; sum(...nums);

Во втором случае в функцию передается числа из массива nums. Но чтобы передавался не просто массив, как одно значение, а именно числа из этого массива, применяется spread-оператор (многоточие ...).

Если количество параметров функции меньше количества элементов массива, то оставшие элементы массива просто будут отброшены. Если количество параметров больше количества элементов массива, то параметры, которым не досталось значений, получат значение undefined:

const people1 = ["Tom", "Sam", "Bob", "Mike"]; const people2 = ["Alex", "Bill"]; function print(first, second, third){ console.log(`${first}, ${second}, ${third}`); } print(...people1); // Tom, Sam, Bob print(...people2); // Alex, Bill, undefined

Копирование массивов

spread-оператор предоставляет самый простой способ скопировать элементы одного массива в другой. Однако тут надо соблюдать осторожность. Рассмотрим пример:

const people = ["Sam", "Tom", "Bob"]; const employees = [...people]; employees[0] = "Dan"; console.log(employees); // ["Dan", "Tom", "Bob"] console.log(people); // ["Sam", "Tom", "Bob"]

Здесь создаются два массива. При этом массиву employees передаются элементы массива people. Далее мы меняем первый элемент массива employees. И по консольному выводу мы можем увидеть, что изменение одного массива не повлияло на другой массив.

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

const people = [{name:"Sam"}, {name:"Tom"}, {name:"Bob"}]; const employees = [...people]; //employees[0] = {name: "Dan"}; employees[0].name = "Dan"; console.log(employees); // [{name:"Dan"}, {name:"Tom"}, {name:"Bob"}] console.log(people); // [{name:"Dan"}, {name:"Tom"}, {name:"Bob"}]

Теперь массив people предоставляет массив объектов, где каждый объект имеет одно свойство - name. Далее мы изменяем значение свойства name у первого элемента.

И консольный вывод нам покажет, что изменение одного массива привело к изменению второго массива. Поскольку объекты - представляют ссылочный тип, и при копировании в новый массив передается не копия объекта (как в случае со строками), а сам объект. Поэтому первый элемент одного массива и первый элемент второго массива фактически будет представлять один и тот же объект.

Однако мы можем полностью заменить элемент одного массива на другой объект, и тогда элемент второго массива по прежнему будет хранить ссылку на старый объект и будет не зависеть от первого массива:

const people = [{name:"Sam"}, {name:"Tom"}, {name:"Bob"}]; const employees = [...people]; employees[0] = {name: "Dan"}; console.log(employees); // [{name:"Dan"}, {name:"Tom"}, {name:"Bob"}] console.log(people); // [{name:"Sam"}, {name:"Tom"}, {name:"Bob"}]

В данном случае первый элемент массива employees заменяется на ссылку на новый объект, а первый элемент массива people по прежнему хранит ссылку на старый объект.

Еще примеры:

const arr1 = [1, 2, 3]; const arr2 = [4, 5, 6]; // Копирование массива const copy = [...arr1]; // Объединение массивов const combined = [...arr1, ...arr2]; // [1,2,3,4,5,6] // Добавление элементов const extended = [0, ...arr1, 4]; // [0,1,2,3,4] // Максимум/минимум const numbers = [3, 1, 4, 1, 5]; console.log(Math.max(...numbers)); // 5 console.log(Math.min(...numbers)); // 1

📜 Методы массивов - Полная таблица

Метод Описание Пример
push() Добавляет элемент в конец arr.push(4) → [1,2,3,4]
pop() Удаляет последний элемент arr.pop() → возвращает 4
unshift() Добавляет элемент в начало arr.unshift(0) → [0,1,2,3]
shift() Удаляет первый элемент arr.shift() → возвращает 0
slice(start, end) Копирует часть массива arr.slice(1, 3) → [2,3]
splice(start, deleteCount, ...items) Удаляет/добавляет элементы arr.splice(1, 2, 'a', 'b')
concat() Объединяет массивы [1,2].concat([3,4]) → [1,2,3,4]
join(separator) Объединяет в строку [1,2,3].join('-') → "1-2-3"
reverse() Разворачивает массив [1,2,3].reverse() → [3,2,1]
sort() Сортирует массив arr.sort((a,b) => a - b)
indexOf(item) Ищет индекс элемента [1,2,3].indexOf(2) → 1
includes(item) Проверяет наличие элемента [1,2,3].includes(2) → true
map(callback) Создаёт новый массив, применяя функцию [1,2,3].map(x => x*2) → [2,4,6]
filter(callback) Фильтрует элементы [1,2,3,4].filter(x => x > 2) → [3,4]
find(callback) Находит первый элемент [1,2,3].find(x => x > 1) → 2
findLast(callback) Возвращает последний элемент, который соответствует условию [1,2,3].findLast(x => x > 1) → 3
findIndex(callback) Находит индекс первого элемента [1,2,3].findIndex(x => x > 1) → 1
findLastIndex(callback) Возвращает индекс последнего элемента, который соответствует условию [1,2,3].findLastIndex(x => x > 1) → 2
reduce(callback, initial) Сводит массив к одному значению [1,2,3].reduce((sum,x) => sum+x, 0) → 6
forEach(callback) Перебирает элементы arr.forEach(x => console.log(x))
some(callback) Проверяет, есть ли хотя бы один [1,2,3].some(x => x > 2) → true
every(callback) Проверяет, все ли удовлетворяют [1,2,3].every(x => x > 0) → true
flat(depth) Разворачивает вложенные массивы [1,[2,[3]]].flat(2) → [1,2,3]
flatMap(callback) map() + flat() [1,2].flatMap(x => [x,x*2]) → [1,2,2,4]

📜 Операции с массивами (применение)

Добавление данных

push()

Метод push() добавляет элемент в конец массива:

const people = []; people.push("Tom"); people.push("Sam"); people.push("Bob","Mike"); console.log("В массиве people элементов: ", people.length); console.log(people); // ["Tom", "Sam", "Bob", "Mike"]

unshift()

Метод unshift() добавляет новый элемент в начало массива:

const people = ["Bob"]; people.unshift("Alice"); console.log(people); // ["Alice", "Bob"] people.unshift("Tom", "Sam"); console.log(people); // ["Tom", "Sam", "Alice", "Bob"]

Добавление данных по определенному индексу ( splice() )

Метод splice позволяет вставить элементы на определенную позицию.

  • Первый аргумент представляет индекс в массиве, по которому надо добавить новые элементы.
  • Второй аргумент представляет количество элементов, которые необходимо удалить (! этот метод также применяется для удаления). Для добавления для этого аргумента устанавливается значение 0.
  • Все остальные аргументы представляют элементы, которые необходимо добавить в массив.
const people = ["Tom", "Sam", "Bob"]; people.splice(1, 0, "Alice"); // добавляем элемент "Alice" по индексу 1 console.log(people); // ["Tom", "Alice", "Sam", "Bob"]

В данном случае добавляем элемент "Alice" по индексу 1.

Также можно добавляеть набор элементов, начиная с определенного индекса:

const people = ["Tom", "Sam", "Bob"]; people.splice(1, 0, "Alice", "Alex", "Kate"); // добавляем элемент "Alice" по индексу 1 console.log(people); // ["Tom", "Alice", "Alex", "Kate", "Sam", "Bob"]

Удаление данных

pop()

Метод pop() удаляет последний элемент из массива:

const people = ["Tom", "Sam", "Bob", "Mike"]; const lastPerson = people.pop(); // извлекаем из массива последний элемент console.log(lastPerson ); // Mike console.log(people); // ["Tom", "Sam", "Bob"]

В качестве результата метод pop возвращает удаленный элемент.

shift()

Метод shift() извлекает и удаляет первый элемент из массива:

const people = ["Tom", "Sam", "Bob", "Mike"]; const first = people.shift(); // извлекаем из массива первый элемент console.log(first); // Tom console.log(people); // ["Sam", "Bob", "Mike"]

Удаление элемента по индексу. splice()

Метод splice() также удаляет элементы с определенного индекса. Например, удаление элементов с третьего индекса:

const people = ["Tom", "Sam", "Bill", "Alice", "Kate"]; const deleted = people.splice(3); console.log(deleted); // [ "Alice", "Kate" ] console.log(people); // [ "Tom", "Sam", "Bill" ]

Метод splice возвращает удаленные элементы в виде нового массива.

В данном случае удаление идет с начала массива. Если передать отрицательный индекс, то удаление будет производиться с конца массива. Например, удалим последний элемент:

const people = ["Tom", "Sam", "Bill", "Alice", "Kate"]; const deleted = people.splice(-1); console.log(deleted); // [ "Kate" ] console.log(people); // ["Tom", "Sam", "Bill", "Alice"]

Дополнительная версия метода позволяет задать количество элементов для удаления. Например, удалим с первого индекса три элемента:

const people = ["Tom", "Sam", "Bill", "Alice", "Kate"]; const deleted = people.splice(1, 3); console.log(deleted); // ["Sam", "Bill", "Alice"] console.log(people); // ["Tom", "Kate"]

Замена элементов

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

const people = ["Tom", "Sam", "Bob", "Alice", "Kate"]; const deleted = people.splice(1, 3, "Alex", "Mike"); console.log(deleted); // ["Sam", "Bob", "Alice"] console.log(people); // ["Tom", "Alex", "Mike", "Kate"]

Здесь удаляем с индекса 1 (первый параметр splice) 3 элемента (второй параметр splice) и вместо них вставляет два элемента - "Alex" и "Mike"

Копирование массива

slice()

Копирование массива может быть поверхностным или неглубоким (shallow copy) и глубоким (deep copy).

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

const users = ["Tom", "Sam", "Bill"]; console.log(users); // ["Tom", "Sam", "Bill"] const people = users; // неглубокое копирование people[1] = "Mike"; // изменяем второй элемент console.log(users); // ["Tom", "Mike", "Bill"]

В данном случае переменная people после копирования будет указывать на тот же массив, что и переменная users. Поэтому при изменении элементов в people, изменятся элементы и в users, так как фактически это один и тот же массив.

Такое поведение не всегда является желательным. Например, мы хотим, чтобы после копирования переменные указывали на отдельные массивы. И в этом случае можно использовать глубокое копирование с помощью метода slice():

const users = ["Tom", "Sam", "Bill"]; console.log(users); // ["Tom", "Sam", "Bill"] const people = users.slice(); // глубокое копирование people[1] = "Mike"; // изменяем второй элемент console.log(users); // ["Tom", "Sam", "Bill"] console.log(people); // ["Tom", "Mike", "Bill"]

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

Но тут стоит отметить, что то же самое копирование по сути можно выполнить и с помощью spread-оператора . . . :

const users = ["Tom", "Sam", "Bill"]; console.log(users); // ["Tom", "Sam", "Bill"] const people = [...users]; people[1] = "Mike"; // изменяем второй элемент console.log(users); // ["Tom", "Sam", "Bill"] console.log(people); // ["Tom", "Mike", "Bill"]

Также метод slice() позволяет скопировать часть массива. Для этого он принимает два параметра:

slice(начальный_индекс, конечный_индекс)
  • Первый параметр указывает на начальный индекс элемента, с которого производится выборка значений из массива.
  • Второй параметр - конечный индекс, по который надо выполнить копирование.

Если указан только начальный индекс, и он положительный, то копирование выполняется с этого индекса до конца массива:

const users = ["Tom", "Sam", "Bill", "Alice", "Kate"]; const people = users.slice(2); // со второго индекса до конца console.log(people); // ["Bill", "Alice", "Kate"]

Если также указан только начальный индекс, но он отрицательный, индекс отсчитывается с конца массива, а копирование выполняется с конца массива до этого индекса:

const users = ["Tom", "Sam", "Bill", "Alice", "Kate"]; const people = users.slice(-2); // до второго индекса с конца console.log(people); // ["Alice", "Kate"]

Второй параметр позволяет ограничить выборку копируемых элементов. Например, выберем в новый массив элементы, начиная с 1 индекса по индекс 4 не включая:

const users = ["Tom", "Sam", "Bill", "Alice", "Kate"]; const people = users.slice(1, 4); console.log(people); // ["Sam", "Bill", "Alice"]

И поскольку индексация массивов начинается с нуля, то в новом массиве окажутся второй, третий и четвертый элемент.

Второй параметр также может быть отрицательным. В этом случае второй индекс отсчитывается с конца:

const users = ["Tom", "Sam", "Bill", "Alice", "Kate"]; const people = users.slice(2, -1); // с индекса 2 по индекс 1 с конца console.log(people); // ["Bill", "Alice"]

copyWithin

Метод copyWithin() позволяет копировать элементы внутри массива. Он принимает три параметра:

copyWithin(index1, // позиция, в которую вставляются копируемые элементы index2, // начальная позиция, с которой будут копироваться элементы index3 // конечная позиция, до которой будут копироваться элементы )

Пример применения:

const users = ["Tom", "Sam", "Bob", "Alice", "Kate"]; const people = users.copyWithin(1, 3, 5); // элементы с индекса 3 по индекс 4 (два элемента) // копируются по индексу 1 console.log(people); // ["Tom", "Alice", "Kate", "Alice", "Kate"]

Получение элементов вне диапазона. toSpliced

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

const people = ["Tom", "Sam", "Bill", "Alice", "Kate"]; const range = people.toSpliced(1, 3); console.log(range); // ["Tom", "Kate"] console.log(people); // ["Tom", "Sam", "Bill", "Alice", "Kate"]

concat()

Метод concat() служит для объединения массивов. В качестве результата он возвращает объединенный массив:

const men = ["Tom", "Sam", "Bob"]; const women = ["Alice", "Kate"]; const people = men.concat(women); console.log(people); // ["Tom", "Sam", "Bob", "Alice", "Kate"]

join()

Метод join() объединяет все элементы массива в одну строку, используя определенный разделитель, который передается через параметр:

const people = ["Tom", "Sam", "Bob"]; const peopleToString = people.join("; "); console.log(peopleToString); // Tom; Sam; Bob

В метод join() передается разделитель между элементами массива. В данном случае в качестве разделителя будет использоваться точка с запятой и пробел ("; ").

sort() и toSorted

Метод sort() сортирует массив по возрастанию:

const people = ["Tom", "Sam", "Bob"]; people.sort(); console.log(people); // ["Bob", "Sam", "Tom"]

Стоит отметить, что по умолчанию метод sort() рассматривает элементы массива как строки и сортирует их в алфавитном порядке. Что может привести к неожиданным результатам, например:

const numbers = [200, 15, 5, 35]; numbers.sort(); console.log(numbers); // [15, 200, 35, 5]

Здесь мы хотим отсортировать массив чисел, но результат может нас обескуражить: [15, 200, 35, 5]. В этом случае мы можем настроить метод, передав в него функцию сортировки. Логику функции сортировки мы определяем сами:

const numbers = [200, 15, 5, 35]; numbers.sort( (a, b) => a - b); console.log(numbers); // [5, 15, 35, 200]

Функция сортировки получает два рядом расположенных элемента массива. Она возвращает отрицательное число, если первый элемент должен находится перед вторым элементом. Если первый элемент должен располагаться после второго, то возвращается положительное число. Если элементы равны, возвращается 0.

JavaScript также предоставляет метод toSorted(), который возвращает отсортированные элементы в виде нового массива, не изменяя текущий массив:

const numbers = [200, 15, 5, 35]; const sorted = numbers.toSorted(); console.log(sorted); // [15, 200, 35, 5]

По умолчанию метод toSorted() также сортирует в алфавитном порядке. Но также можно передать в метод функцию сравнения элементов:

const numbers = [200, 15, 5, 35]; const sorted = numbers.toSorted( (a, b) => a - b); console.log(sorted); // [5, 15, 35, 200]

reverse() и toReversed()

Метод reverse() меняет порядок элементов в массиве на обратный:

const people = ["Tom", "Sam", "Bob"]; people.reverse(); console.log(people); // ["Bob", "Sam", "Tom"]

Метод toReversed() также меняет порядок элементов на обратный, но возвращает их в виде нового массива без изменения текущего:

const people = ["Tom", "Sam", "Bob"]; const reversed = people.toReversed(); console.log(people); // ["Tom", "Sam", "Bob"] console.log(reversed); // ["Bob", "Sam", "Tom"]

Методы indexOf() и lastIndexOf() возвращают индекс первого и последнего включения элемента в массиве. Например:

const people = ["Tom", "Sam", "Bob", "Tom", "Alice", "Sam"]; const firstIndex = people.indexOf("Tom"); const lastIndex = people.lastIndexOf("Tom"); const otherIndex = people.indexOf("Mike"); console.log(firstIndex); // 0 console.log(lastIndex); // 3 console.log(otherIndex); // -1

firstIndex имеет значение 0, так как первое включение строки "Tom" в массиве приходится на индекс 0, а последнее на индекс 3.

Если же элемент отсутствует в массиве, то в этом случае методы indexOf() и lastIndexOf() возвращают значение -1.

Поиск в массиве

Метод find() возвращает первый элемент массива, который соответствует некоторому условию. В качестве параметр метод find принимает функцию условия:

const numbers = [1, 2, 3, 5, 8, 13, 21, 34]; // получаем первый элемент, который больше 10 let found = numbers.find(n => n > 10 ); console.log(found); // 13

В данном случае получаем первый элемент, который больше 10. Если элемент, соответствующий условию, не найден, то возвращается undefined.

Аналогично метод findLast() возвращает последний элемент, который соответствует условию:

const numbers = [1, 2, 3, 5, 8, 13, 21, 34]; // получаем последний элемент, который меньше 10 let found = numbers.find(n => n < 10 ); console.log(found); // 8

Метод findIndex также принимает функцию условия, только возвращает индекс первого элемента массива, который соответствует этому условию:

const numbers = [1, 2, 3, 5, 8, 13, 21, 34]; // получаем индекс первого элемента, который больше 10 let foundIndex = numbers.findIndex(n => n > 10 ); console.log(foundIndex); // 5

Если элемент не найден, то возвращается число -1.

Аналогично метод findLastIndex() возвращает индекс последнего элемента, который соответствует условию:

const numbers = [1, 2, 3, 5, 8, 13, 21, 34]; // получаем индекс последнего элемента, который меньше 10 let foundIndex = numbers.findIndex(n => n < 10 ); console.log(foundIndex); // 4

Проверка наличия элемента

Метод includes() проверяет, есть ли в массиве значение, переданное в метод через параметр. Если такое значение есть, то метод возвращает true, если значения в массиве нет, то возвращается false. Например:

const people = ["Tom", "Sam", "Bob", "Tom", "Alice", "Sam"]; console.log(people.includes("Tom")); // true - Tom есть в массиве console.log(people.includes("Kate")) // false - Kate нет в массиве

В качестве второго параметра метод includes() принимает индекс, с которого надо начинать поиск:

const people = ["Tom", "Sam", "Bob", "Tom", "Alice", "Sam"]; console.log(people.includes("Bob", 2)); // true console.log(people.includes("Bob", 4)) // false

В данном случае мы видим, что при поиске со 2-го индекса в массиве есть строка "Bob", тогда как начиная с 4-го индекса данная строка отсутствует.

Если если этот параметр не передается, то по умолчанию поиск идет с 0-го индекса.

При передаче отрицательного значения поиск идет с конца

const people = ["Tom", "Sam", "Bob", "Tom", "Alice", "Sam"]; console.log(people.includes("Tom", -2)); // false - 2-й индекс с конца console.log(people.includes("Tom", -3)) // true - 3-й индекс с конца

every()

Метод every() проверяет, все ли элементы соответствуют определенному условию:

const numbers = [ 1, -12, 8, -4, 25, 42 ]; const passed = numbers.every(n => n > 0); console.log(passed); // false

В метод every() в качестве параметра передается функция, которая представляет условие. Эта функция в качестве параметра принимает элемент и возвращает true (если элемент соответствует условию) или false (если не соответствует).

Если хотя бы один элемент не соответствует условию, то метод every() возвращает значение false.

В данном случае условие задается с помощью лямбда-выражения n => n > 0, которое проверяет, больше ли элемент нуля.

some()

Метод some() похож на метод every(), только он проверяет, соответствует ли хотя бы один элемент условию. И в этом случае метод some() возвращает true. Если элементов, соответствующих условию, в массиве нет, то возвращается значение false:

const numbers = [ 1, -12, 8, -4, 25, 42 ]; const passed = numbers.some(n => n > 0); console.log(passed); // true

filter()

Метод filter(), как some() и every(), принимает функцию условия:

filter(callbackFn) filter(callbackFn, thisArg)

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

callback(element, index, array)

В функцию-коллбек последовательно передаются:

  • текущий элемент ( element )
  • индекс элемента ( index )
  • ссылка на сам перебираемый массив ( array )

Коллбек возвращает true (или значение, которое соответствует true), если элемент удовлетворяет условию. В ином случае возвращается false.

Опционально в качестве второго параметра в метод filter() можно передать объект, который внутри функции-коллбека можно получить через this

В качестве результата метод filter() возвращает массив тех элементов, которые соответствуют этому условию:

const numbers = [ 1, -12, 8, -4, 25, 42 ]; const filteredNumbers = numbers.filter(n => n > 0); console.log(filteredNumbers); // [1, 8, 25, 42]

Перебор элементов с помощью forEach()

Метод forEach() осуществляют перебор элементов и применяют к каждому из них определенное действие.

forEach(callbackFn) forEach(callbackFn, thisArg)

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

callback(element, index, array)

В функцию-коллбек последовательно передются:

  • текущий элемент ( element )
  • индекс элемента ( index )
  • ссылка на сам перебираемый массив ( array )

Опционально в качестве второго параметра в метод forEach() можно передать объект, который внутри функции-коллбека можно получить через this

Например, используем метод forEach() для вычисления квадратов чисел в массиве:

const numbers = [ 1, 2, 3, 4, 5, 6]; numbers.forEach(n => console.log("Квадрат числа", n, "равен", n * n ) );

Метод forEach() в качестве параметра принимает функцию, которая имеет один параметр - текущий перебираемый элемент массива. А в теле функции над этим элементом можно выполнить различные операции.

Консольный вывод программы:

Квадрат числа 1 равен 1
Квадрат числа 2 равен 4
Квадрат числа 3 равен 9
Квадрат числа 4 равен 16
Квадрат числа 5 равен 25
Квадрат числа 6 равен 36

Трансформация массива и map()

Метод map() похож на метод forEach, он принимает те же параметры:

map(callbackFn) map(callbackFn, thisArg)

Первый параметр - также функция-коллбек, в которую передаются текущий перебираемый элемент, его индекс и ссылка на массив. А второй параметр - значение для this внутри функции-коллбека. Но при этом метод map() возвращает новый массив с результатами операций над элементами массива.

Например, применим метод map к вычислению квадратов чисел массива:

const numbers = [ 1, 2, 3, 4, 5, 6]; const squares = numbers.map(n => n * n); console.log(squares); // [1, 4, 9, 16, 25, 36]

Функция, которая передается в метод map() получает текущий перебираемый элемент, выполняет над ним операции и возвращает некоторое значение. Это значение затем попадает в результирующий массив squares

Метод flat и преобразование массива

Метод flat() упрощает массив с учетом указанной вложенности элементов:

const people = ["Tom", "Bob", ["Alice", "Kate", ["Sam", "Ann"]]]; const flattenPeople = people.flat(); console.log(flattenPeople); // ["Tom", "Bob", "Alice", "Kate", ["Sam", "Ann"]]

То есть метод flat() фактически из вложенных массивов переводит элементы во внешний массив самого верхнего уровня. Однако мы видим, что элементы массива второго уровня вложенности перешли в массив первого уровня вложенности, но тем не менее по-прежнему находятся во вложенном массиве. Дело в том, что метод flat() по умолчанию применяется только к вложенным массивам первого уровня вложенности. Но мы можем передать в метод уровень вложености:

const people = ["Tom", "Bob", ["Alice", "Kate", ["Sam", "Ann"]]]; const flattenPeople = people.flat(2); console.log(flattenPeople); // ["Tom", "Bob", "Alice", "Kate", "Sam", "Ann"]

Если массив содержит вложенные массивы гораздо более глубоких уровней вложенности, или мы даже не знаем, какие уровни вложенности есть в массиве, но мы хотим, чтобы все элементы были преобразованы в один массив, то можно использовать значение Infinity:

const people = ["Tom", "Bob", ["Alice", "Kate", ["Sam", "Ann"]]]; const flattenPeople = people.flat(Infinity); console.log(flattenPeople); // ["Tom", "Bob", "Alice", "Kate", "Sam", "Ann"]

with

Иногда необходимо изменить элемент массива, но при этом также сохранить старое состояние массива. Метод with() автоматически создает копию старого массива, изменяет в ней элемент и возвращает новый массив с измененным элементом:

const people = ["Tom", "Bob", "Sam"]; const modified = people.with(0, "Tomas"); // изменяем "Tom" на "Tomas" console.log(people); // ["Tom", "Bob", "Sam"] - начальный массив не изменился console.log(modified); // ["Tomas", "Bob", "Sam"] - изменилась копия

reduce

Метод reduce позволяет свести все значения массива в одно значение, которое возвращается из метода. В качестве параметра метод принимает функцию с 4 параметрами:

function (prev,current, curIndex, array){ .... }

Параметры:

  1. prev: предыдущий элемент (вначале - самый первый элемент)
  2. current: текущий элемент (вначале - второй элемент)
  3. curIndex: индекс текущего элемента
  4. array: массив, для которого вызывается функция

Применим метод reduce() для нахождения суммы массива чисел:

const numbers = [1, 2, 3, 4, 5]; const sum = numbers.reduce((prev,current) => prev +=current); console.log(sum); // 15

В данном случае в метод reduce() передается функция (prev,current) => prev +=current, которая складывает предыдущий элемент с текущим и возвращает их сумму.

Другая форма метода reduce принимает два параметра. Второй параметр - начальное значение, с которого начинается отсчет:

const numbers = [1, 2, 3, 4, 5]; const sum = numbers.reduce((prev,current) => prev +current, 10); console.log(sum); // 25

В данном случае начальное значение prev будет равно 10.

Если метод reduce просматривает элементы массива с начала (слева направо), то метод reduceRight() делает это в обратном порядке - справо налево:

const numbers = [1, 2, 3, 4, 5]; const reduced1 = numbers.reduce((prev,current) => prev +=current.toString()); console.log(reduced1); // 12345 const reduced2 = numbers.reduceRight((prev,current) => prev +=current.toString()); console.log(reduced2); // 54321

Комбинация методов

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

function Person(name, age){ this.name = name; this.age = age; } const people = [ new Person("Tom", 38), new Person("Kate", 31), new Person("Bob", 42), new Person("Alice", 34), new Person("Sam", 25) ];

Выведем из массива people имена всех пользователей, у которых возраст больше 33:

const isAgeMoreThan33 = (p)=>p.age > 33; const getPersonName = (p)=>p.name; const printPersonName = (p)=>console.log(p); // получаем из Person строку с именем const view = people .filter(isAgeMoreThan33) .map(getPersonName) .forEach(printPersonName);

📜 Примеры работы с массивами

map() - преобразование

const numbers = [1, 2, 3, 4, 5]; // Удвоить каждое число const doubled = numbers.map(num => num * 2); console.log(doubled); // [2, 4, 6, 8, 10] // Преобразование объектов const users = [ {name: 'Alice', age: 25}, {name: 'Bob', age: 30} ]; const names = users.map(user => user.name); console.log(names); // ['Alice', 'Bob']

filter() - фильтрация

const numbers = [1, 2, 3, 4, 5, 6]; // Только чётные const even = numbers.filter(num => num % 2 === 0); console.log(even); // [2, 4, 6] // Фильтрация объектов const users = [ {name: 'Alice', age: 25}, {name: 'Bob', age: 17}, {name: 'Charlie', age: 30} ]; const adults = users.filter(user => user.age >= 18); console.log(adults); // Alice и Charlie

reduce() - сведение к значению

const numbers = [1, 2, 3, 4, 5]; // Сумма const sum = numbers.reduce((acc, num) => acc + num, 0); console.log(sum); // 15 // Произведение const product = numbers.reduce((acc, num) => acc * num, 1); console.log(product); // 120 // Максимум const max = numbers.reduce((max, num) => num > max ? num : max); console.log(max); // 5

find() и findIndex()

const users = [ {id: 1, name: 'Alice'}, {id: 2, name: 'Bob'}, {id: 3, name: 'Charlie'} ]; // Найти пользователя const user = users.find(u => u.id === 2); console.log(user); // {id: 2, name: 'Bob'} // Найти индекс const index = users.findIndex(u => u.id === 2); console.log(index); // 1

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

✅ Удаление дубликатов
const arr = [1, 2, 2, 3, 3, 4]; const unique = [...new Set(arr)]; // [1, 2, 3, 4] // Разбор по шагам: // 1. new Set(arr) - создаёт Set из массива // Set - коллекция уникальных значений (без дубликатов) // Set {1, 2, 3, 4} // 2. ...new Set(arr) - spread оператор разворачивает Set // 1, 2, 3, 4 // 3. [...new Set(arr)] - собираем обратно в массив // [1, 2, 3, 4] // Альтернативные способы: // Способ 1: через filter const unique2 = arr.filter((item, index) => arr.indexOf(item) === index); // Способ 2: через reduce const unique3 = arr.reduce((acc, item) => { if (!acc.includes(item)) { acc.push(item); } return acc; }, []);

Что такое Set?

// Set - коллекция уникальных значений const set = new Set(); // Добавление элементов set.add(1); set.add(2); set.add(2); // Игнорируется (дубликат) set.add(3); console.log(set); // Set {1, 2, 3} console.log(set.size); // 3 // Проверка наличия console.log(set.has(2)); // true // Удаление set.delete(2); console.log(set); // Set {1, 3} // Очистка set.clear(); console.log(set); // Set {} // Создание Set из массива const numbers = [1, 2, 2, 3, 3, 4]; const uniqueSet = new Set(numbers); // Set {1, 2, 3, 4} // Обратно в массив const uniqueArray = [...uniqueSet]; // [1, 2, 3, 4] // или const uniqueArray2 = Array.from(uniqueSet); // [1, 2, 3, 4]
✅ Сумма элементов
const numbers = [1, 2, 3, 4, 5]; const sum = numbers.reduce((acc, num) => acc + num, 0); // 15
✅ Группировка объектов
const users = [ {name: 'Alice', age: 25}, {name: 'Bob', age: 30}, {name: 'Charlie', age: 25} ]; const grouped = users.reduce((acc, user) => { acc[user.age] = acc[user.age] || []; acc[user.age].push(user); return acc; }, {}); // {25: [{...}, {...}], 30: [{...}]}

split() и join() - работа со строками

// split() - строка в массив const str = "JavaScript,Python,Ruby"; const langs = str.split(','); // ['JavaScript', 'Python', 'Ruby'] // join() - массив в строку const arr = ['Hello', 'World']; const result = arr.join(' '); // "Hello World" // Практический пример: разворот слов const text = "Hello World"; const reversed = text.split(' ').reverse().join(' '); // "World Hello"

📜 Деструктуризация массивов

const colors = ['red', 'green', 'blue']; // Деструктуризация const [first, second, third] = colors; console.log(first); // "red" console.log(second); // "green" // Пропуск элементов const [, , third2] = colors; console.log(third2); // "blue" // Rest оператор const [head, ...tail] = colors; console.log(head); // "red" console.log(tail); // ["green", "blue"] // Значения по умолчанию const [a, b, c, d = 'yellow'] = colors; console.log(d); // "yellow"

📜 Многомерные массивы

// Двумерный массив (матрица) const matrix = [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ]; // Доступ к элементам console.log(matrix[0][0]); // 1 console.log(matrix[1][2]); // 6 // Перебор for (let i = 0; i < matrix.length; i++) { for (let j = 0; j < matrix[i].length; j++) { console.log(matrix[i][j]); } } // Через forEach matrix.forEach(row => { row.forEach(item => { console.log(item); }); });

📜 Array-like объекты

// arguments - array-like объект function sum() { // arguments не массив, но похож на него console.log(arguments); // [1, 2, 3] // Преобразование в массив const arr = Array.from(arguments); return arr.reduce((sum, n) => sum + n, 0); } console.log(sum(1, 2, 3)); // 6 // NodeList тоже array-like const divs = document.querySelectorAll('div'); const divsArray = Array.from(divs);

📜 Наследование массивов

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

const team = ["Tom", "Sam", "Bob"]; for(const person of team) { console.log(person); }

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

const team = { name: "Barcelona", // название members: ["Tom", "Sam", "Bob"] // игроки }; for(const person of team.members) { console.log(person); }

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

class Team extends Array{ constructor(name, ...members){ super(...members); this.name = name; } }

Здесь мы предполагаем, что в качестве первого параметра конструктора класса выступает имя команды, а в качестве второго - набор игроков команды, число которых не фиксировано.

Благодаря наследованию от Array мы можем рассматривать объекты класса Team как наборы данных и применять к ним все те операции, которые применяются к массивам:

class Team extends Array{ constructor(name, ...members){ super(...members); this.name = name; } } // создаем объект команды const barcelona = new Team("Barcelona", "Tom", "Sam", "Bob"); // перебор набора for(const person of barcelona) { console.log(person); } console.log(barcelona); // Team(3) ["Tom", "Sam", "Bob"] barcelona.push("Tim"); // добавим один элемент console.log(barcelona); // Team(4) ["Tom", "Sam", "Bob", "Tim"] barcelona.splice(1, 1); // удалим второй элемент console.log(barcelona); // Team(3) ["Tom", "Bob", "Tim"]

Переопределение методов

Как и в общем при наследоваании, мы можем переопределять унаследованные методы. Например, переопределим поведение метода добавления push(), который отвечает за добавление в конец массива:

class Team extends Array{ constructor(name, ...members){ super(...members); this.name = name; } // Переопределяем метод push() push(person){ if(person !== "admin") super.push(person); } } const snowbars = new Team("SnowBars", "Tom", "Sam", "Bob"); snowbars.push("admin"); // добавим один элемент - admin console.log(snowbars); // Team(3) ["Tom", "Sam", "Bob"] snowbars.push("Tim"); // добавим один элемент - Tim console.log(snowbars); // Team(4) ["Tom", "Sam", "Bob", "Tim"]

В данном случае если в метод передано любое имя, кроме "admin", то оно добавляется в команду.

Глава 7. Строки и регулярные выражения (RegExp)

📜 Строки, объект String и его методы

Строки — наиболее часто встречающийся тип данных.

Для создания строк мы можем как напрямую присваивать переменной или константе строку:

const message = "Hello";

Для работы со строками предназначен объект String, поэтому также можно использовать конструктор String:

const message = new String("Hello");

Но, как правило, используется первый более краткий способ. В первом случае JavaScript при необходимости автоматически преобразует переменную примитивного типа в объект String.

С помощью индексов можно обращаться к отдельным символам строки, как к элементам массива (как и в массивах индексация начинается с нуля):

const message = "Hello"; console.log(message[0]); // H console.log(message[4]); // o

Объект String имеет большой набор свойств и методов, с помощью которых мы можем манипулировать строками.

📜 Работа со строками

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

📃 Определение длины строки

Свойство length указывает на длину строки:

const name = "JavaScript"; console.log(name.length); // 10

Повторение строки

Метод repeat() позволяет создать строку путем многократного повторения другой строки. Количество повторов передается в качестве аргумента:

const str = "Ha"; console.log(str.repeat(3)); // "HaHaHa" // Практический пример const line = "=".repeat(50); // 50 знаков равенства

📃 Поиск в строке

indexOf / lastIndexOf

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

  • indexOf() - индекс первого вхождения подстроки
  • lastIndexOf() - индекс последнего вхождения подстроки

Эти методы принимают два параметра:

indexOf(str, index) lastIndexOf(str, index)
  • Подстроку, которую надо найти
  • Индекс, с которого идет поиск (Необязательный параметр)

Оба этих метода возвращают индекс символа, с которого в строке начинается подстрока. Если подстрока не найдена, то возвращается число -1.

const hello = "привет мир. пока мир"; const key = "мир"; const firstPos = hello.indexOf(key); const lastPos = hello.lastIndexOf(key); console.log("Первое вхождение: ", firstPos); // 7 console.log("Последнее вхождение: ", lastPos); // 17

Применим поиск относительно индекса, например, начиная с индекса 10:

const hello = "привет мир. пока мир"; const key = "мир"; const firstPos = hello.indexOf(key, 10); // поиск с 10-го индекса console.log("Первое вхождение: ", firstPos); // 17

Следует учитывать, что поиск регистрозависимый:

const hello = "привет мир. пока мир"; const key = "Мир"; const firstPos = hello.indexOf(key); console.log(firstPos); // -1

Еще пример:

const text = "JavaScript is awesome"; console.log(text.indexOf("is")); // 11 console.log(text.lastIndexOf("a")); // 15 console.log(text.includes("awesome")); // true console.log(text.startsWith("Java")); // true console.log(text.endsWith("some")); // true

📃 includes

Еще один метод - includes() возвращает true, если строка содержит определенную подстроку.

const hello = "привет мир. пока мир"; console.log(hello.includes("мир")); // true console.log(hello.includes("миг")); // false

С помощью второго дополнительного параметра можно определить индекс, с которого будет начинаться поиск подстроки:

const hello = "привет мир. пока мир"; console.log(hello.includes("мир", 5)); // true console.log(hello.includes("привет", 6)); // false

📃 Извлечение части строки / Выбор подстроки

Для того, чтобы вырезать из строки подстроку, применяются методы substring() и slice().

Substring

Метод substring() принимает два параметра:

substring(startIndex, endIndex)
  • индекс символа в строке, начиная с которого надо проводить обрезку строки. Обязательный параметр
  • индекс, до которого надо обрезать строку. Необязательный параметра - если он не указан, то обрезается вся остальная часть строки
const hello = "привет мир. пока мир"; const world = hello.substring(7, 10); // с 7-го по 10-й индекс console.log(world); // мир const bye = hello.substring(12); // c 12 индекса до конца строки console.log(bye); // пока мир

slice

Метод slice() также позволяет получить из строки какую-то ее часть. Он принимает два параметра:

slice(startIndex, endIndex)
  • Начальный индекс подстроки в строке. Обязательный параметр
  • Конечный индекс подстроки в строке. Необязательный параметра - если он не указан, то получаем всю оставшуюся часть строки
const hello = "привет мир. пока мир"; const world = hello.slice(7, 10); // с 7-го по 10-й индекс console.log(world); // мир const bye = hello.slice(12); // c 12 индекса до конца строки console.log(bye); // пока мир

Различия substring() и slice()

Можно заметить, что метод slice()похож на substring(), тем не менее между ними есть различия:

  • В slice() начальный индекс должен быть меньше чем конечный.
    В substring(), если начальный индекс больше конечного, то они меняются местами (то есть substring(5, 1) будет равноценно substring(1, 5)):
    const hello = "привет мир. пока мир"; const world1 = hello.slice(6, 0); // не работает console.log(world1); // пустая строка const world2 = hello.substring(6, 0); // аналогично hello.substring(0, 6) console.log(world2); // привет
  • Другое отличие, что slice позволяет использовать отрицательные индексы. Отрицательный индекс указывает на индекс символа относительно конца строки.
    substring() же отрицательные индексы не поддерживает:

    const hello = "привет мир. пока мир"; const bye1 = hello.slice(-8, -4); // с 8-го индекса с конца до 4 индекса с конца console.log(bye1); // пока const bye2 = hello.substring(-8, -4); // не работает console.log(bye2); //

substr

Следует отметить, что еще есть метод substr(). Этот метод не является частью стандарта и в целом не рекомендуется к использованию, однако он все еще может поддерживаться браузерами, и его до сих пор можно встретить в различных программах. Он принимает два параметра:

substr(startIndex, count)
  • Начальный индекс подстроки в строке. Обязательный параметр
  • Количество выбираемых символов. Необязательный параметра - если он не указан, то выбирается вся остальная часть строки

Применение:

const hello = "привет мир. пока мир"; const world = hello.substr(7, 3); // с 7-го индекса 3 символа console.log(world); // мир const bye = hello.substr(12); // с 12-го индекса до конца console.log(bye); // пока мир

Еще пример:

const url = "http://example.com"; // slice(start, end) - извлекает подстроку const domain = url.slice(7); // "example.com" const protocol = url.slice(0, 4); // "http" // substring(start, end) - аналог slice, но не принимает отрицательные индексы const sub = url.substring(7, 14); // "example" // substr(start, length) - устарел const old = url.substr(7, 7); // "example"

📃 Изменение регистра

Для изменения регистра символов имеются методы:

  • toLowerCase() - для перевода в нижний регистр
  • toUpperCase() - для перевода в верхний регистр
const str = "JavaScript"; console.log(str.toLowerCase()); // "javascript" console.log(str.toUpperCase()); // "JAVASCRIPT"

📃 Получение символа по индексу. charAt(), charCodeAt()

Чтобы получить определенный символ в строке по индексу, можно применять синтаксис массивов. Но также JavaScript предоставляет методы charAt() и charCodeAt(). Оба этих метода в качестве параметра принимают индекс символа:

const hello = "Привет Том"; console.log(hello.charAt(2)); // и console.log(hello.charCodeAt(2)); // 1080

Но если в качестве результата метод charAt() возвращает сам символ, то метод charCodeAt() возвращает числовой код этого символа.

📃 Удаление пробелов

Для удаления начальных и концевых пробелов в стоке используется метод trim():

let hello = " Привет Том "; const beforeLength = hello.length; hello = hello.trim(); const afterLength = hello.length; console.log("Длина строки до: ", beforeLength); // 15 console.log("Длина строки после: ", afterLength); // 10

Дополнительно есть ряд методов, которые удаляют пробелы с определенной стороны строки:

  • trimStart() - удаляет пробел с начала строки (в зависимости от того, является ли письмо правостронним или левостронним, это может быть правый или левый край строки)
  • trimEnd() - удаляет пробел с конца строки (в зависимости от того, является ли письмо правостронним или левостронним, это может быть правый или левый край строки)
  • trimLeft() - удаляет пробел с левой части строки
  • trimRight() - удаляет пробел с правой части строки
const str = " Hello World "; console.log(str.trim()); // "Hello World" console.log(str.trimStart()); // "Hello World " console.log(str.trimEnd()); // " Hello World"

📃 Разделение и объединение строк

Метод concat() объединяет две строки:

let hello = "Привет "; const world = "мир"; hello = hello.concat(world); console.log(hello); // Привет мир

Разделение строки

Метод split() разбивает строку на массив подстрок по определенному разделителю. В качестве разделителя используется строка, которая передается в метод:

const message = "Сегодня была прекрасная погода"; const messageParts = message.split(" "); console.log(messageParts); // ["Сегодня", "была", "прекрасная", "погода"]

В данном случае строка разделяется по пробелу, то есть в итоге в массиве messageParts окажется четыре элемента.

// split() - строка в массив const csv = "apple,banana,orange"; const fruits = csv.split(","); // ["apple", "banana", "orange"] // join() - массив в строку const arr = ["Hello", "World"]; const text = arr.join(" "); // "Hello World"

📃 Замена подстроки в строке

Метод replace() заменяет первое вхождение одной подстроки на другую:

let hello = "Добрый день"; hello = hello.replace("день", "вечер"); console.log(hello); // Добрый вечер
  • первый параметр метода указывает, какую подстроку надо заменить
  • второй параметр - на какую подстроку надо заменить

В то же время у этого метода есть одна особенность - он заменяет только первое вхождение подстроки:

let menu = "Завтрак: каша, чай. Обед: суп, чай. Ужин: салат, чай."; menu = menu.replace("чай", "кофе"); console.log(menu); // Завтрак: каша, кофе. Обед: суп, чай. Ужин: салат, чай.

Однако еще один метод - replaceAll() позволяет заменить все вхождения подстроки:

let menu = "Завтрак: каша, чай. Обед: суп, чай. Ужин: салат, чай."; menu = menu.replaceAll("чай", "кофе"); console.log(menu); // Завтрак: каша, кофе. Обед: суп, кофе. Ужин: салат, кофе.
const text = "Hello World"; // replace() - заменяет первое вхождение console.log(text.replace("World", "JavaScript")); // "Hello JavaScript" // replaceAll() - заменяет все вхождения (ES2021) const str = "cat cat cat"; console.log(str.replaceAll("cat", "dog")); // "dog dog dog"

📃 Проверка начала и окончания строки

  • Метод startsWith() возвращает true, если строка начинается с определенной подстроки
  • метод endsWith() возвращает true, если строка оканчивается на определенную подстроку
const hello = "let me speak from my heart"; console.log(hello.startsWith("let")); // true console.log(hello.startsWith("Let")); // false console.log(hello.startsWith("lets")); // false console.log(hello.endsWith("heart")); // true console.log(hello.startsWith("bart")); // false

При этом играет роль регистр символов, и из примера выше мы видим, что "let" не эквивалентно "Let" .

Дополнительный второй параметр позволяет указать индекс (для startsWith - индекс с начала, а для endsWith - индекс с конца строки), относительно которого будет производиться сравнение:

const hello = "let me speak from my heart"; console.log(hello.startsWith("me", 4)); // true, "me" - 4 индекс с начала строки console.log(hello.endsWith("my", hello.length-8)); // true, "my" - 8 индекс с конца

📃 Заполнение строки

Методы padStart() и padEnd() позволяют растянуть строку на определенное количество символов и заполнить строку слева и справа соответственно.

let hello = "hello".padStart(8); // " hello" console.log(hello); hello = "hello".padEnd(8); // "hello " console.log(hello);

Вызов "hello".padStart(8) будет рястягивать строку "hello" на 8 символов. То есть изначально в строке "hello" 5 символов, значит, к ней будет добавлено 3 символа. При чем они будут добавлено в начале строки. По умолчанию добавляемые символы представляют пробелы. Аналогично вызов "hello".padEnd(8) растянет строку на 8 символов, но оставшие символы в виде пробелов будут добавлены в конец строки.

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

let hello = "hello".padStart(17, "JavaScript, "); // "JavaScript, hello" hello = "hello".padEnd(12, " Eugene"); // "hello Eugene"

Если добавляемое количество символов больше добавляемой строки, то добавляемая строка повторяется:

let hello = "123".padStart(6, "0"); // "000123" hello = "123".padEnd(6, "0"); // "123000"

📜 Шаблоны строк

Шаблоны строк (template strings / template literals) позволяют вставлять в строку различные значения. Подобный прием еще называют интерполяцией. Для этого строки заключаются в косые кавычки, а вставляемое значение предваряется символом $ и заключается в фигурные скобки:

const name = "Tom"; const hello = `Hello ${name}`; console.log(hello); // Hello Tom

Здесь на место ${name} будет вставляться значение константы name. Таким образом, из шаблона `Hello ${name}` мы получим строку Hello Tom.

Подобным образом в строку можно вставлять сразу несколько значений:

const name = "Tom"; const age = 37; const userInfo = `${name} is ${age} years old`; console.log(userInfo); // Tom is 37 years old

Также вместо скалярных значений могут добавляться свойства сложных объектов:

const tom = { name: "Tom", age: 22 } const tomInfo = `${tom.name} is ${tom.age} years old`; console.log(tomInfo); // Tom is 22 years old

Любо можно вставлять более сложные вычисляемые выражения:

function sum(x, y){ return x + y; } const a = 5; const b = 4; const result = `${a} + ${b} = ${sum(a, b)}`; console.log(result); // 5 + 4 = 9 const expression = `${a} * ${b} = ${ a * b}`; console.log(expression); // 5 * 4 = 20

Во втором случае в шаблоне выполняется операция умножения констант: ${ a * b}.

📃 html-код в шаблонах

Шаблоны также могут хранить html-код, который будет динамически формироваться.

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <script> const tom = {name: "Tom", age: 37}; const markup = `<div> <p><b>Name</b>: ${tom.name}</p> <p><b>Age</b>: ${tom.age}</p> </div>`; document.body.innerHTML = markup; </script> </body> </html>

📃 Вложенные / динамические шаблоны

Рассмотрим другой пример - создадим из элементов массива список html:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <script> const people = [{name: "Tom", age: 37}, {name:"Sam", age: 41}, {name: "Bob", age: 21}]; const markup = `<ul> ${people.map(person => `<li>${person.name}</li>`)} </ul>`; document.body.innerHTML = markup; </script> </body> </html>

В данном случае мы имеем дело с вложенным шаблоном. То есть вначале определяется общий внешний шаблон:

const markup = `<ul> ${.............} </ul>`;

А в динамически формируемом выражении применяется еще один шаблон:

${people.map(person => `<li>${person.name}</li>`)}

В данном случае у массива people вызывается функция map(), которое определяет некоторое действие для каждого элемента массива. Это действие передается в map() в виде функции. Здесь для упрощения в качестве такой функции применяется лямбда-выражение. Оно получает каждый элемент массива через параметр person и для него формирует шаблон строки `<li>${person.name}</li>`.

📃 Тег-функции и передача шаблона строки в функцию

JavaScript позволяет передать в функцию шаблон строки, причем не просто как строку, но и все ее динамчески вычисляемые фрагменты в виде отдельных параметров. Для этого применяются тег-функции (tag function). Подобная возможность может применяться, например, для предобработки шаблонов и их значений. Рассмотрим следующий пример:

const person = "Tom"; function check (parts, name){ console.log(parts); return parts[0] + name + parts[1]; } const checkedTemplate = check`Person: ${person}.`; console.log(checkedTemplate);

Здесь определена tag-функция check(), которая имеет два параметра: parts и name

function check (parts, name){ console.log(parts); return parts[0] + name + parts[1]; }

Параметр parts - это массив частей шаблона, разделенных вставляемыми динамическими фрагментами. Второй параметр - name - это динамически вычисляемый фрагмент шаблона. То есть в данном случае мы предполагаем, что шаблон строки, который передается в функцию check(), будет иметь только один динамчески вычисляемый фрагмент. Соответственно в массиве parts будет два элемента: статическая часть шаблона, которая идет до вычисляемого фрагмента, и часть шаблона, которая идет после.

Чтобы было более ясно, о чем идет речь, в функции выводим на консоль эти элементы массива parts.

Функция возвращает return parts[0] + name + parts[1], то есть по сути мы просто возвращаем ранее сформированный шаблон, ничего не меняя.

Обратите внимание, как мы передаем этой функции шаблон:

const checkedTemplate = check`Person: ${person}.`;

шаблон просто указывается после названия функции. Или иначе говоря, tag-функция указывается как префикс перед шаблоном.

Результат работы программы:

Array(2)
	0: "Person: "
	1: "."
	length: 2
	raw: (2) ['Person: ', '.']
	[[Prototype]]: Array(0)
Person: Tom.

Из консольного вывода мы видим, что элементами массива parts являются подстроки "Person: " и ".". А в качестве значения параметра name передается строка "Tom". Стоит отметить, что даже если после динамически вычисляемого фрагмента больше не было бы никаких символов (например, `Person: ${person}`), то массив parts все равно имел бы два элемента, только вторым элементом тогда была бы пустая строка.

Но в примере выше мы просто возвращали то же содержимое, которое было сформировано на основе шаблона. Однако мы можем выполнить некоторую обработку:

const tom = "Tom"; const admin = "Admin"; function check (parts, name){ if(name === "Admin") return "Пользователь не определен"; else return parts[0] + name + parts[1]; } let checkedTemplate1 = check`Пользователь: ${tom}`; let checkedTemplate2 = check`Пользователь: ${admin}`; console.log(checkedTemplate1); console.log(checkedTemplate2);

В данном случае, если в шаблон передается значение "Admin", то возвращаем один результат, иначе возвращаем, то, что было бы сформированно на основе шаблона.

Пользователь: Tom
Пользователь не определен

Подобным образом можно обрабатывать шаблоны с большим количеством вычисляемых фрагментов:

const tom = {name: "Tom", age: 37}; const bob = {name: "Bob", age: 11}; function check (parts, name, age){ if(age > 18) return `${parts[0]}${name}. Доступ открыт`; else return `Для пользователя ${name} доступ закрыт. Возраст ${age} недействителен`; } let checkedTemplate1 = check`Пользователь: ${tom.name} ${tom.age}`; let checkedTemplate2 = check`Пользователь: ${bob.name} ${bob.age}`; console.log(checkedTemplate1); console.log(checkedTemplate2);

В данном случае шаблон содержит два динамческих фрагмента. Соответственно в массива part будет три элемента.

В функции check() в зависимости от значения второго динамического фрамегмента (условного возраста пользователя) возвращаем то или иное значение.

Консольный вывод:

Пользователь:  Tom. Доступ открыт
Для пользователя Bob доступ закрыт. Возраст 11 недействителен

📜 Объект RegExp. Регулярные выражения

Регулярные выражения представляют шаблон, который используется для поиска или модификации строки. Для работы со строками мы, конечно, можем использовать стандартный набор методов типа String. Однако регулярные выражения предоставляют более гибкий инструмент.

Для работы с регулярными выражениями в JavaScript определен объект RegExp.

Определить регулярное выражение можно двумя способами:

const myExp1 = /hello/; const myExp2 = new RegExp("hello");

Используемое здесь регулярное выражение довольно простое: оно состоит из одного слова "hello".

  • литеральная нотация - выражение помещается между двумя косыми чертами
  • во втором случае используется конструктор RegExp, в который выражение передается в виде строки

Обычно, если регулярное выражение определяется динамически (например, с помощью ввода пользователя), то применяется конструктор. Если выражение статическое, неизменяемое, то используется литеральная нотация, как в первом случае.

После определения регулярного выражения для обработки строк мы можем использовать один из методов RegExp:

  • test(): проверяет, присутствует ли определенный шаблон в строке. Если строка соответствует шаблону, то этот метод возвращает true, иначе возвращается false.
  • exec(): ищет вхождения определенного шаблона в строку и возвращает эти вхождения в виде массива.

Кроме того, дополнительно для работы с регулярными выражениями мы можем использовать ряд методов типа String:

  • match(): также ищет вхождения определенного шаблона в строку и возвращает эти вхождения в виде массива.
  • replace(): заменяет вхождения определенного шаблона в строке.
  • search(): ищет вхождения определенного шаблона в строку и возвращает индекс первого вхождения.
  • split(): разделяет строку в соответствии с определенным шаблоном и возвращает индекс полученные части строки в виде массива.

📃 test. Проверка соответствия строки шаблону

Для проверки встречается ли в строке текст, который соответствует регулярному выражению, применяется метод test() объекта RegExp. Этот метод возвращает true, если строка содержит текст, который соответствует регулярному выражению, и false, если не содержит.

const initialText = "hello world!"; // строка для поиска const exp = /hello/; // регулярное выражение const result = exp.test(initialText); // проверяем наличие в строке выражения exp console.log(result); // true const initialText2 = "Hi all"; const result2 = exp.test(initialText2); console.log(result2); // false - в строке "Hi all" нет "hello"

В данном случае мы проверяем, есть ли в строках initialText ("hello world!") и initialText2 ("Hi all") выражение exp (/hello/). Поскольку в первой строке такой текст присутствует, то exp.test(initialText) возвращает true. В случае со второй строкой таой текст отсутствует, поэтому возвращается false.

📃 Метод exec

Аналогично работает метод exec - он также проверяет, есть ли в строке текст, который удовлетворяет регулярному выражению. При наличии такого текста метод возвращает ту часть строки, которая соответствует выражению. Если соответствий нет, то возвращается значение null.

const initialText = "hello world!"; // строка для поиска const exp = /hello/; // регулярное выражение const result = exp.exec(initialText); // проверяем наличие в строке выражения exp console.log(result); // ['hello', index: 0, input: 'hello world!', groups: undefined] const initialText2 = "Hi all"; const result2 = exp.exec(initialText2); console.log(result2); // null - в строке "Hi all" нет "hello"

Так, в строке "hello world!" есть текст, который соответствует шаблону /hello/, поэтому вызов метода exp.exec(initialText) возвратит вхождения текста, который соответствует выражению, в строку. И по консольному выводу мы видим, что это не просто текст, а массив значений:

['hello', index: 0, input: 'hello world!', groups: undefined]
  • Первый элемент массива - собственно текст, который соответствует выражению (в нашем случае "hello")
  • Второй элемент - индекс этого текста в строке (в нашем случае индекс 0 - начало строки)
  • Третий элемент - представляет входную строку
  • Четвертый элемент - представляет группу, которая в примере выше неопределена (группы мы разберем далее)

📃 Группы символов

Выше в качестве регулярного выражения применялась строка"hello". И в реальности для проверки наличия этой подстроки в строке мы могли бы использовать и стандартные методы типа String. Однако это не просто строка, а шаблон, который также может включать специальные элементы синтаксиса регулярных выражений. Рассмотрим самые базовые шаблоны:

  • . (точка) соответствует любому символу
  • a (одиночный символ) соответствует символу "a"
  • ab (объединение символов) соответствует последовательности символов "ab"
  • a|b соответствует либо символу "a", либо символу "b" (символ | можно рассматривать как аналогию операции OR)

Например, нам надо проверить длину пароля - что она не менее 8 символов:

const exp = /......../; // регулярное выражение - 8 символов const password1 = "1234567890"; const password2 = "qwery5"; // проверяем первый пароль console.log(exp.test(password1)); // true - password1 соответствует выражению exp // проверяем второй пароль console.log(exp.test(password2)); // false - password2 не соответствует выражению exp

Здесь регулярное выражение "/......../" имеет 8 точек . То есть для соответствия этому выражению строка должна иметь как минимум 8 символов.

Другой пример:

const exp = /word|work/; // соответствует либо "word", либо "work" const str1 = "hello world"; const str2 = "hello work"; const str3 = "hello word"; console.log(exp.test(str1)); // false console.log(exp.test(str2)); // true console.log(exp.test(str3)); // true

Здесь выражение "/word|work/" соответствует тексту, который содержит либо "word", либо "work". Однако это выражение не оптимально - в обоих вариантах повторется группа символов "wor". Общую группу символов мы можем взять в скобки: "/(wor)d|k/". Результат будет тот же:

const exp = /wor(d|k)/; // соответствует либо "word", либо "work" const str1 = "hello world"; const str2 = "hello work"; const str3 = "hello word"; console.log(exp.test(str1)); // false console.log(exp.test(str2)); // true console.log(exp.test(str3)); // true

Другой пример - проверим, принадлежит ли почтовый адрес "yandex.ru" или "mail.ru":

const exp = /@(yandex|mail).ru/; // соответствует либо "@yandex.ru" либо "@mail.ru" const email1 = "tom@mail.ru"; const email2 = "tom@gmail.ru"; const email3 = "tom@yandex.ru"; console.log(exp.test(email1)); // true console.log(exp.test(email2)); // false console.log(exp.test(email3)); // true

📜 Синтаксис регулярных выражений

📃 Определение классов символов

Для определения регулярных выражений мы можем использовать классы символов. Для определения класса символов применяются квадратные скобки:

  • [xyz] (альтернативное соответствие): соответствует одному из символов: x, y или z (аналог x|y|z)
  • [^xyz] (отрицание): соответствует тексту, который содержит любые символы КРОМЕ x, y или z
  • [a-zA-Z] (диапазон): соответствует любому символу из диапазона a-z или A-Z

Например, нам надо проверить, есть ли в тексте символы "a", "b" или "c":

const exp = /[abc]/; // соответствует либо "a", либо "b", либо "c" const str1 = "JavaScript"; const str2 = "Pascal"; const str3 = "Python"; console.log(exp.test(str1)); // true console.log(exp.test(str2)); // true console.log(exp.test(str3)); // false

Выражение [abc] указывает на то, что строка должна иметь одну из трех букв. Выражение "[abc]" также эквивалентно выражению "a|b|c".

Возьмем более практический пример. Допустим, у нас есть 4-х символьные pin-коды, и нам надо проверить, что pin-код содержит только цифры:

const exp = /[0-9][0-9][0-9][0-9]/; // соответствует четырем цифрам подряд const code1 = "1234"; const code2 = "jav5"; const code3 = "3452"; console.log(exp.test(code1)); // true console.log(exp.test(code2)); // false console.log(exp.test(code3)); // true

Выражение [0-9][0-9][0-9][0-9] соответствует любой последовательности из 4 цифр подряд. Например, такому шаблону соответствует строка "3452", но НЕ соответствует строка "jav5" (здесь только одна цифра). Строка "jav5" соответстветствовала бы шаблону "[a-z][a-z][a-z][0-9]" (первые три алфафитных символа в нижнем регистре, за которыми идет цифра).

Сразу стоит отметить, что выражение [0-9][0-9][0-9][0-9] не оптимально, и далее мы посмотрим, как его можно упростить.

Еще один пример - применим отрицание:

const exp = /[^a-z]/; // соответствует любым символам, кроме символов из диапазона a-z const code1 = "zorro"; const code2 = "zorro5"; const code3 = "34521"; console.log(exp.test(code1)); // false console.log(exp.test(code2)); // true console.log(exp.test(code3)); // true

Здесь строки проверяются на соответствие выражению "[^a-z]", которое соответствует любым символам, кроме символов из диапазона a-z. Например, строка "zorro" НЕ соответствует этому выражению. Однако ему соответствует строка "zorro5", потому что в ней есть символ, не входящий в диапазон "a-z".

При необходимости мы можем собирать комбинации выражений:

const exp = /[дт]о[нм]/; // соответствует строкам "дом", "том", "дон", "тон" const str1 = "дома"; const str2 = "сома"; const str3 = "тона"; console.log(exp.test(str1)); // true console.log(exp.test(str2)); // false console.log(exp.test(str3)); // true

Выражение [дт]о[нм] указывает на те строки, которые могут содержать подстроки "дом", "том", "дон", "тон".

📃 Метасимволы

Вместо определения своих классов символов мы можем использовать встроенные, которые еще называют метасимволы - символы, которые имеют определенный смысл:

  • \d : соответствует любой цифре от 0 до 9. Аналогичен выражению [0-9]
  • \D : соответствует любому символу, который не является цифрой. Аналогичен выражению [^0-9]
  • \w : соответствует любой букве, цифре или символу подчеркивания (диапазоны A–Z, a–z, 0–9). Аналогичен выражению [a-zA-Z_0-9]
  • \W : соответствует любому символу, который не является буквой, цифрой или символом подчеркивания (то есть не находится в следующих диапазонах A–Z, a–z, 0–9). Аналогичен выражению [^\w]
  • \s : соответствует пробелу. Аналогичен выражению [\t\n\x0B\f\r]
  • \S : соответствует любому символу, который не является пробелом. Аналогичен выражению [^\s]
  • . : соответствует любому символу

Здесь надо заметить, что метасимвол \w применяется только для букв латинского алфавита, кириллические символы для него не подходят.

Так, выше для проверки, что код имеет только 4 цифры, использовалось выражение /[0-9][0-9][0-9][0-9]/. Мы его можем сократить, используя метасимвол "\d":

const exp = /\d\d\d\d/; // соответствует четырем цифрам подряд const code1 = "1234"; const code2 = "jav5"; const code3 = "3452"; console.log(exp.test(code1)); // true console.log(exp.test(code2)); // false console.log(exp.test(code3)); // true

Другой пример. Допустим, нам надо найти строки, где определен номер телефона. Причем, номер телефона в формате +х-ххх-ххх-хххх:

const exp = /\+\d-\d\d\d-\d\d\d-\d\d\d\d/; const contact1 = "Email: mycomp@gmail.com"; const contact2 = "Phone: +1-234-567-8901"; console.log(exp.test(contact1)); // false console.log(exp.test(contact2)); // true

Так, номеру телефона +1-234-567-8901 соответствует /\+\d-\d\d\d-\d\d\d-\d\d\d\d/:

Обратите внимание на слеш перед плюсом (\+). Поскольку плюс + имеет специальное значение, то, чтобы указать, что мы имеет ввиду именно плюс как символ строки, перед ним ставится слеш.

В результате в строке "Phone: +1-234-567-8901" метод exp.test(contact2) сопоставит с регулярным выражением подстроку "+1-234-567-8901"

📃 Ограничение применения регулярных выражений

Ряд специальных символов позволяют ограничить диапазон применения регулярных выражений:

  • ^ : соответствует началу строки. Например, ^h соответствует строке "home", но не "ohma", так как h должен представлять начало строки
  • $ : соответствует концу строки. Например, м$ соответствует строке "дом", так как строка должна оканчиваться на букву м
  • \b : соответствует началу или концу слова.
  • \B : не учитывает границы слова

Например, нам нужно найти строки с номером телефона:

const exp = /\d\d\d\d\d\d\d\d\d\d/; // соответствует 10 цифрам подряд const phone1 = "+12345678901"; const phone2 = "42345678901"; console.log(exp.test(phone1)); // true console.log(exp.test(phone2)); // true

Шаблону /\d\d\d\d\d\d\d\d\d\d/ соответствуют как строка "+12345678901", так и строка "42345678901". Но, допустим, нам надо найти номера телефонов, которые не предваряются плюсом +. В этом случае мы можем использовать регулярное выражение /^\d\d\d\d\d\d\d\d\d\d/. Таким образом, строка будет соответствовать шаблону, если она начинается с цифровых символов:

const exp = /^\d\d\d\d\d\d\d\d\d\d/; // соответствует 10 цифрам подряд const phone1 = "+12345678901"; const phone2 = "42345678901"; console.log(exp.test(phone1)); // false console.log(exp.test(phone2)); // true

Другой пример. Пусть нам надо проверить, есть ли в строке упонимание языка "Java". Наивный подход состоял бы в использовании регулярного выражения /Java/:

const exp = /Java/; const str1 = "Java is a high-level, object-oriented programming language"; const str2 = "JavaScript is a programming language of the World Wide Web"; console.log(exp.test(str1)); // true console.log(exp.test(str2)); // true

Однако в реальности шаблон "/Java/" соответствует любой строке, которая содержит подстроку "Java", в том числе строке "JavaScript". Однако нам надо найти только те строки, где речь идет именно о Java, а не о JavaScript. И в этом случае мы можем ограничить поиск границами слова с помощью "\b":

const exp = /Java\b/; const str1 = "Java is a high-level, object-oriented programming language"; const str2 = "JavaScript is a programming language of the World Wide Web"; console.log(exp.test(str1)); // true console.log(exp.test(str2)); // false

Флаг "\B", наоборот, указывает сопоставлять шаблон с подстроками, которые не являются словами:

const exp = /Java\B/; const str1 = "Java is a high-level, object-oriented programming language"; const str2 = "JavaScript is a programming language of the World Wide Web"; console.log(exp.test(str1)); // false console.log(exp.test(str2)); // true

📃 Флаги выражений

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

  • Флаг global позволяет найти все подстроки, которые соответствуют регулярному выражению. По умолчанию при поиске подстрок регулярное выражение выбирает первую попавшуюся подстроку из строки, которая соответствует выражению. Хотя в строке может быть множество подстрок, которые также соответствуют выражению. Для этого применяется данный флаг в виде символа g в выражениях

  • Флаг ignoreCase позволяет найти подстоки, которые соответствуют регулярному выражению, вне зависимости от регистра символов в строке. Для этого в регулярных выражениях применяется символ i

  • Флаг multiline позволяет найти подстроки, которые соответствуют регулярному выражению, в многострочном тексте. Для этого в регулярных выражениях применяется символ m

  • Флаг dotAll позволяет сопоставить точку в регулярном выражении с любым символом текста, в том числе с разделителем строки. Для этого в регулярных выражениях применяется символ s

Флаг i. Регистр символов

Рассмотрим следующий пример:

const str = "Hello World"; const exp = /world/; console.log(exp.test(str)); // false

Здесь совпадения строки с выражением нет, так как "World" отличается от "world" по регистру. В этом случае надо изменить регулярное выражение, добавив в него флаг i:

const str = "Hello World"; const exp = /world/i; console.log(exp.test(str)); // true

Обратите внимание, где в регулярном выражении указывается флаг: /world/i - в самом конце регулярного выражения.

Флаг s

Флаг s позволяет сопоставить символ . (точка) с любым символом, в том числе и с разделителем строки. Например, возьмем следующий пример:

const str = "hello\nworld"; const exp = /hello world/; console.log(exp.test(str)); // false

Здесь в строке "hello\nworld" слова "hello" и "world" разделены переносом строки (например, мы имеем дело с многострочным текстом). Однако, например, мы хотим, чтобы JavaScript не учитывал перенос строки и чтобы данный текст соответствовал регулярному выражению /hello world/. В этом случае мы можем применить флаг s:

const str = "hello\nworld"; const exp = /hello.world/s; console.log(exp.test(str)); // true

В выражении /hello.world/s точка означает произвольный символ. Однако без флага s данное выражение не будет соответствовать многострочному тексту.

Комбинация флагов

Также можно использовать сразу несколько флагов:

const str = "hello\nWorld"; const exp = /hello.world/si; console.log(exp.test(str)); // true

📃 Таблица 16.1: Символы в регулярных выражениях

Символ Описание Пример
. Любой символ (кроме новой строки) /h.t/ → "hat", "hot", "h9t"
\w Буква, цифра или подчёркивание [A-Za-z0-9_] /\w+/ → "word", "test123"
\W НЕ буква, НЕ цифра, НЕ подчёркивание /\W/ → пробел, знаки препинания
\d Любая цифра [0-9] /\d+/ → "123", "42"
\D НЕ цифра /\D+/ → "abc", "test"
\s Пробельный символ (пробел, табуляция, перенос) /\s+/ → пробелы
\S НЕ пробельный символ /\S+/ → "word"
^ Начало строки /^Hello/ → строка начинается с "Hello"
$ Конец строки /world$/ → строка заканчивается на "world"
\b Граница слова /\bword\b/ → "word" (но не "sword")
[abc] Любой из указанных символов /[aeiou]/ → любая гласная
[^abc] Любой символ КРОМЕ указанных /[^0-9]/ → не цифра
| ИЛИ (альтернатива) /cat|dog/ → "cat" или "dog"
\ Экранирование спецсимвола /\./ → точка (буквально)

Таблица 16.2: Квантификаторы

Символ Описание Пример
? 0 или 1 раз (необязательно) /colou?r/ → "color" или "colour"
+ 1 или более раз /\d+/ → "1", "123", "99999"
* 0 или более раз /a*/ → "", "a", "aaa"
{n} Ровно n раз /\d{3}/ → "123" (ровно 3 цифры)
{n,} n или более раз /\d{2,}/ → "12", "123", "1234"
{n,m} От n до m раз /\d{2,4}/ → "12", "123", "1234"

📜 Квантификаторы в регулярных выражениях

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

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

  • ? : соответствует одному вхождению последовательности символов или ее отсутствию в строке. Например, /h?ome/ соответствует подстрокам "home" и "ome".

  • + : соответствует одному и более повторений последовательности символов

  • {n} : соответствует n-ому количеству повторений предыдущего символа. Например, h{3} соответствует подстроке "hhh"

  • {n,} : соответствует n и более количеству повторений предыдущего символа. Например, h{3,} соответствует подстрокам "hhh", "hhhh", "hhhhh" и т.д.

  • {n,m} : соответствует от n до m повторений предыдущего символа. Например, h{2, 4} соответствует подстрокам "hh", "hhh", "hhhh".

Необязательные символы

Номер телефона может иметь дефисы для разделения отдельных блоков цифр, например, "+1-234-567-8901", а может не иметь разделителей, например, "12345678901". То есть в данном случае дефисы-разделители необязательны. И мы можем отразить это с помощью квантификатора ?

const exp = /\d-?\d\d\d-?\d\d\d-?\d\d\d\d/; const phone1 = "+1-234-567-8901"; const phone2 = "12345678901"; const phone3 = "1-2345678901"; console.log(exp.test(phone1)); // true console.log(exp.test(phone2)); // true console.log(exp.test(phone3)); // true

Здесь "-?" отражает, что символ "-" может быть необязательным, однако если он присутствует, то только один раз.

Произвольное количество символов

Символ * указывает, что предыдущий символ может встречаться произвольное число раз (в том числе 0 раз). Например:

const exp = /;*/; // соответствует любому количеству символов ; const str1 = "number1 = 3"; const str2 = "number2 = 4;"; const str3 = "number3 = 5;;;"; console.log(exp.test(str1)); // true console.log(exp.test(str2)); // true console.log(exp.test(str3)); // true

Регулярное выражение /;*/ указывает, что точка с запятой (;) может встречаться 1 и более раз, либо может вообще не встречаться.

Символ встречается как минимум один раз

Квантификатор + указывает, что предыдущий символ может встречаться один или большее количество раз. Например:

const exp = /;+/; // соответствует 1 и более символов ; const str1 = "number1 = 3"; const str2 = "number2 = 4;"; const str3 = "number3 = 5;;;"; console.log(exp.test(str1)); // false console.log(exp.test(str2)); // true console.log(exp.test(str3)); // true

Регулярное выражение /;+/ указывает, что точка с запятой (;) может встречаться как минимум 1 раз (или большее количество раз).

Точное количество вхождений

Квантификатор {n} позволяет определить точное количество повторений предыдущего символа через значение n. Например, для определения телефонного номера может использоваться выражение /\d\d\d\d\d\d\d\d\d\d\d/ - 11 цифр подряд. Однако оно неоптимально. И для его сокращения мы можем использовать другое выражение: /\d{11}/ - символ "\d" (цифровой символ) встречается 11 раз подряд. Например:

const exp = /\d{11}/; const phone1 = "+12345678901"; const phone2 = "1-2345678901"; const phone3 = "12345678901"; console.log(exp.test(phone1)); // true console.log(exp.test(phone2)); // false console.log(exp.test(phone3)); // true

Но что, если блоки цифр у нас разделены разделителем-дефисом типа "+1-234-567-8901". В этот случае мы могли бы задать длину для каждого блока: /\d-\d{3}-\d{3}-\d{4}/. Например:

const exp = /\d-\d{3}-\d{3}-\d{4}/; const phone1 = "+12345678901"; const phone2 = "1-234-567-8901"; const phone3 = "12345678901"; console.log(exp.test(phone1)); // false console.log(exp.test(phone2)); // true console.log(exp.test(phone3)); // false

Комбинируя с другими квантификаторами, можно сделать разделители-дефисы необязательными:

const exp = /\d-?\d{3}-?\d{3}-?\d{4}/; const phone1 = "+12345678901"; const phone2 = "1-234-567-8901"; const phone3 = "1-2345678901"; console.log(exp.test(phone1)); // true console.log(exp.test(phone2)); // true console.log(exp.test(phone3)); // true

Определение минимального количества вхождений

Квантификатор {n,} позволяет задать через n минимальное количество вхождений. Допустим, у нас пароли должны иметь как минимум 8 символов:

const exp = /\w{8,}/; const code1 = "1234567890"; const code2 = "qwery5"; const code3 = "password123"; console.log(exp.test(code1)); // true console.log(exp.test(code2)); // false console.log(exp.test(code3)); // true

Метасимвол "\w" соответствует любому цифровому и алфавитному символу или символу подчеркивания. Соответственно выражение /\w{8,}/ соответствует строкам, где есть подстрока из как минимум 8 таких символов.

Определение минимального и максимального количества вхождений

Квантификатор {n,m} позволяет определить одновременно минимальное (n) и максимальное (m) количество повторений. Например, мы хотим, чтобы имена у нас были не слишком короткими, скажем, не меньше 3 символов, и не слишком длинными (например, не больше 10 символов):

const exp = /^[a-zA-Z]{3,10}$/; const code1 = "Tom"; const code2 = "Li"; const code3 = "Maximilianus"; console.log(exp.test(code1)); // true console.log(exp.test(code2)); // false console.log(exp.test(code3)); // false

Выражение /^[a-zA-Z]{3,10}$/ говорит, что любой символ из диапазонов "a-z" и "A-Z" должен повторяться не меньше 3 раз и не больше 10 раз. Причем, здесь также указано, что это должно быть отдельное слово. Для этого вначале указывается символ "^", а в конце символ "$".

Для поиска в строке подстроки, которая соответствовует регулярному выражению, применяется метод exec() объекта RegExp. Этот метод принимает строку для поиска и возвращает результат в виде массива. Например:

const contacts = "Email: mycomp@gmail.com Phones: +1-234-567-8901 and +1-234-567-8902"; const phonePattern = /\+\d-\d{3}-\d{3}-\d{4}/; const result = phonePattern.exec(contacts); console.log(result); // Консольный вывод // ['+1-234-567-8901', index: 32, input: 'Email: mycomp@gmail.com// Phones: +1-234-567-8901 and +1-234-567-8902', groups: undefined]

Здесь проверяем, есть ли в строке contacts текст, который соответствовует регулярному выражению phonePattern (то есть представляет номер телефона). В качестве результата возвращается массив из следующих элементов:

  • Первый элемент массива - непосредственно тот текст, который соответствует регулярному выражению. Так, в примере выше это текст "+1-234-567-8901"
  • Второй параметр - index - индекс найденного текста в строке
  • Третий параметр - input - входная строка
  • Последний элемент представляет отдельные группы

Если в строке не найден текст, который соответствует регулярному выражению, то возвращается null

Получим отдельные элементы этого массива:

const contacts = "Email: mycomp@gmail.com Phones: +1-234-567-8901 and +1-234-567-8902"; const phonePattern = /\+\d-\d{3}-\d{3}-\d{4}/; const result = phonePattern.exec(contacts); if(result){ console.log("Phone number:", result[0]); // +1-234-567-8901 console.log("Index:", result.index); // 32 }

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

Так, изменим предыдущий пример, применив флаг g:

const contacts = "Email: mycomp@gmail.com Phones: +1-234-567-8901 and +1-234-567-8902"; const phonePattern = /\+\d-\d{3}-\d{3}-\d{4}/g; let result; while ((result = phonePattern.exec(contacts)) !== null){ console.log("Phone number:", result[0]); console.log("Index: ", result.index);

В цикле while извлекаем все сопоставления шаблона с текстом в переменную result, пока не останется сопоставлений. Обратите внимание, где в регулярном выражении указывается флаг g: /\+\d-\d{3}-\d{3}-\d{4}/g. Консольный вывод:

Phone number: +1-234-567-8901
Index:  32
Phone number: +1-234-567-8902
Index:  52

📜 Группы в регулярных выражениях

📃 Определение групп в регулярных выражениях

Для поиска в строке более сложных соответствий применяются группы. В регулярных выражениях группы заключаются в скобки. Например, нам надо получить дату в определенном формате. С помощью метода exec() объекта RegExp мы можем получить все совпадение полностью. Допустим, дата представлена в формате "yyyy-mm-dd" (2021-09-06) :

const exp = /\d{4}-\d{2}-\d{2}/; const text = "Publication Date: 2021-09-06" const result = exp.exec(text); console.log(result[0]); // 2021-09-06

Из результата метода exec мы можем извлечь результат - нужную нам дату. Однако что, если мы хотим получить отдельные компоненты дат - год, месяц, день? В этом случае мы можем воспользоваться группами.

Каждая группа представляет некоторый сегмент регулярного выражения, который заключен в круглые скобки. Например:

const exp = /(\d{4})-(\d{2})-(\d{2})/; const text = "Publication Date: 2021-09-06" const result = exp.exec(text); console.log(result); // Консольный вывод // (4) ['2021-09-06', '2021', '09', '06', index: 18,// input: 'Publication Date: 2021-09-06', groups: undefined]

Здесь регулярное выражение /(\d{4})-(\d{2})-(\d{2})/ содержит три группы: Первая группа - (\d{4}) состоит из 4 цифр и представляет год. Вторая группа - (\d{2}) состоит из 2 цифр и представляет месяц. Третья группа также состоит из 2 цифр и представляет день.

  • Первая группа - (\d{4}) состоит из 4 цифр и представляет год.
  • Вторая группа - (\d{2}) состоит из 2 цифр и представляет месяц.
  • Третья группа - также состоит из 2 цифр и представляет день.

Здесь применяется регулярное выражение "/(\d{4})-(\d{2})-(\d{2})/", где определены три группы:

  1. Первая группа (\d{4}) соответствует числу из четырех цифр
  2. Вторая группа (\d{2}) соответствует числу из двух цифр
  3. Третья группа аналогична второй

И если мы посмотрим на результат метода exec(), то мы увидим, что кроме собственно соответствия дате он содержит соответствия для каждой группы:

(4) ['2021-09-06', '2021', '09', '06', index: 18, ↩ input: 'Publication Date: 2021-09-06', groups: undefined]

Полученный результат представляет массив, где первый элемент (с индексом 0) всегда представляет подстроку, совпавшую с регулярным выражением. Все последующие элементы этого массива представляют группы. То есть первая группа имеет индекс 1, вторая - индекс 2 и так далее. Соответственно, применяя индексы, мы можем получить все соответствия группам из регулярного выражения:

const exp = /(\d{4})-(\d{2})-(\d{2})/; const text = "Publication Date: 2021-09-06" const result = exp.exec(text); console.log(result[0]); // 2021-09-06 - все соответствие console.log(result[1]); // 2021 - первая группа console.log(result[2]); // 09 - вторая группа console.log(result[3]); // 06 - третья группа

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

console.log(`${result[3]}.${result[2]}.${result[1]}`); // 06.09.2021

Группировка упрощает создание более сложных регулярных выражений. К группам, как и к отдельным символам, можно применять квантификаторы. Например, выражение (la)+ представляет одно и более повторений строки "la"&

📃 Именнованные группы

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

(?<название_группы> ... )

Рассмотрим следующий пример:

const exp = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u; const text = "Publication Date: 2021-09-06" const result = exp.exec(text); console.log(result.groups); // {year: "2021", month: "09", day: "06"} console.log(result.groups.year); // 2021 console.log(result.groups.month); // 09 console.log(result.groups.day); // 06

Здесь регулярное выражение определяет три группы. Первая группа называется "year", вторая - "month" и третья "day". При получении результата мы можем обаться к каждой группе через свойство groups. Это свойство представляет объект, в котором свойства называются так же, как и группы, и содержат значения для каждой группы:

console.log(result.groups); // {year: "2021", month: "09", day: "06"}

Соответственно, с помощью названия группы мы можем получить значение для определенной группы.

📃 Утверждения

Утверждения или assertions позвляют получить подстроку, которая соответствует регулярному выражению и которая предваряется или, наоборот, не предваряется определенным выражением.

Положительное утверждение (когда подстрока должна предваряться другой подстрокой) определяется с помощью выражения:

(?<=...)

После знака равно = идет выражение, которым должна предваряться подстрока.

Отрицательное утверждение (когда подстрока НЕ должна предваряться другой подстрокой) определяется с помощью выражения:

(?<!...)

После восклицательного знака ! идет выражение, которым НЕ должна предваряться подстрока.

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

const text1 = "All costs: $10.53"; const text2 = "All costs: €10.53"; const exp = /\d+(\.\d*)?/; const result1 = exp.exec(text1); console.log(result1[0]); // 10.53 const result2 = exp.exec(text2); console.log(result2[0]); // 10.53

Здесь мы видим, что и сумма в долларах () и сумма в евро соответствует нашему регулярному выражению. Но что, если мы хотим получить только сумму в долларах. Для этого применим положительное утверждение:

const text1 = "All costs: $10.53"; const text2 = "All costs: €10.53"; const exp = /(?<=\$)\d+(\.\d*)?/; const result1 = exp.exec(text1); console.log(result1); // ["10.53", ".53", index: 12, input: "All costs: $10.53", groups: undefined] const result2 = exp.exec(text2); console.log(result2); // null

Группа (?<=\$) укзывает, что перед строкой должен идти знак доллара $. Если его нет, то метод exec() не найдет соответствий и возвратит null.

📜 Регулярные выражения в методах String

Ряд методов объекта String могут использовать регулярные выражения в качестве параметра.

Метод match

Для поиска всех соответствий в строке применяется метод match():

const initialText = "Он пришел домой и сделал домашнюю работу"; const exp = /дом[а-я]*/gi; const result = initialText.match(exp); result.forEach(value => console.log(value)); // или так // console.log(result[0]); // console.log(result[1]);

Символ звездочки указывает на возможность наличия после строки "дом" неопределенного количества символов от а до я. В итоге в массиве result окажутся следующие слова:

домой
домашнюю

С одной стороны, этот метод похож на метод exec() объекта RegExp за тем исключением, что exec() возвращает только первое вхождение:

const initialText = "Он пришел домой и сделал домашнюю работу"; const exp = /дом[а-я]*/gi; const result2 = exp.exec(initialText); result2.forEach(value => console.log(value));

Консольный вывод браузера:

домой

Разделение строки. Метод split

Метод split может использовать регулярные выражения для разделения строк. Например, разделим приложение по словам (а точнее по пробелам) с помощью метасимвола "\s":

const initialText = "Сегодня была прекрасная погода"; const exp = /\s/; const result = initialText.split(exp); result.forEach(value => console.log(value));

Вывод браузера:

Сегодня
была
прекрасная
погода

Поиск в строке. Метод search

Метод search находит индекс первого включения соответствия в строке:

const initialText = "hello world"; const exp = /wor/; const result = initialText.search(exp); console.log(result); // 6

Замена. Методы replace

Метод replace позволяет заменить все соответствия регулярному выражению определенной строкой.

  • Первый параметр метода - регулярное выражение
  • второй - на что заменяем совпадения

Пример замены:

let menu = "Завтрак: каша, чай. Обед: суп, чай. Ужин: салат, чай."; const exp = /чай/gi; menu = menu.replace(exp, "кофе"); console.log(menu);

Вывод браузера:

Завтрак: каша, кофе. Обед: суп, кофе. Ужин: салат, кофе.

Другая форма метода в качестве второго параметра принимает функцию замены. Эта функция принимает соответствие регулярному выражению и возвращает строку, на которое заменяется соответствие. Например, заменим чай на кофе только для завтрака:

let menu = "Завтрак: каша, чай. Обед: суп, чай. Ужин: салат, чай."; const exp = /чай/gi; let i = 0; menu = menu.replace(exp, function(tee){ if(i++ == 0) return "кофе"; else return tee; }); console.log(menu); // Завтрак: каша, кофе. Обед: суп, чай. Ужин: салат, чай.

Для индикатора замены используем счетчик - переменную i. Если она равна 0, то производим замену. В остальных случаях возвращаем соответствие - строку "чай".

Более сложный случай. Пусть у нас есть следующий текст:

Publication Date: 2021-09-06
Updated on: 2021-09-14

Здесь у нас применяются даты в формате yyyy-MM-dd. Допустим, нам надо изменить формат дат на "dd.MM.yyyy". Для этого определим следующую программу:

const exp = /\d{4}-\d{2}-\d{2}/g; let text = "Publication Date: 2021-09-06\nUpdated on: 2021-09-14" text = text.replace(exp, function(date){ const arr = date.split("-"); return `${arr[2]}.${arr[1]}.${arr[0]}`; }); console.log(text);

Здесь мы извлекаем все соответствия регулярному выражению /\d{4}-\d{2}-\d{2}/g. В методе replace в функции обратного вызова получаем соответствие через параметр date, с помощью функции split() разбиваем его на три части по разделителю-дефису:

const arr = date.split("-");

То есть, мы получаем массив из трех компонентов даты. И затем возвращаем дату в другом формате:

return `${arr[2]}.${arr[1]}.${arr[0]}`;

В итоге мы получим текст:

Publication Date: 06.09.2021
Updated on: 14.09.2021

📃 Создание регулярных выражений

// Способ 1: литерал const regex1 = /hello/i; // i - игнорировать регистр // Способ 2: конструктор const regex2 = new RegExp('hello', 'i'); // Флаги: // i - игнорировать регистр // g - глобальный поиск (все вхождения) // m - многострочный режим // s - . включает \n // u - Unicode // y - поиск с позиции lastIndex

Методы работы с RegExp

const text = "JavaScript is awesome"; const regex = /is/g; // test() - проверка совпадения (возвращает true/false) console.log(/awesome/.test(text)); // true // exec() - поиск совпадения (возвращает массив или null) const result = /is/.exec(text); console.log(result); // ["is", index: 11, ...] // Строковые методы // match() - поиск в строке console.log(text.match(/a/g)); // ["a", "a", "a"] // matchAll() - все совпадения с группами (ES2020) const matches = [...text.matchAll(/a(\w)/g)]; // search() - индекс первого совпадения console.log(text.search(/is/)); // 11 // replace() с RegExp console.log(text.replace(/is/g, "was")); // "JavaScript was awesome"

📃 Готовые регулярные выражения

✅ Email
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // Пример: user@example.com
✅ Телефон (RU)
const phoneRegex = /^(\+7|8)?[\s-]?\(?\d{3}\)?[\s-]?\d{3}[\s-]?\d{2}[\s-]?\d{2}$/; // Примеры: +7 (999) 123-45-67, 8-999-123-45-67
✅ URL
const urlRegex = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/; // Примеры: https://example.com, http://sub.example.com/path
✅ Дата (DD.MM.YYYY)
const dateRegex = /^(0[1-9]|[12][0-9]|3[01])\.(0[1-9]|1[012])\.\d{4}$/; // Пример: 31.12.2024
✅ Почтовый индекс (RU)
const zipRegex = /^\d{6}$/; // Пример: 123456

📃 Группы захвата

const text = "2024-01-15"; const regex = /(\d{4})-(\d{2})-(\d{2})/; const match = text.match(regex); console.log(match[0]); // "2024-01-15" - полное совпадение console.log(match[1]); // "2024" - первая группа console.log(match[2]); // "01" - вторая группа console.log(match[3]); // "15" - третья группа // Именованные группы (ES2018) const regex2 = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/; const match2 = text.match(regex2); console.log(match2.groups.year); // "2024" console.log(match2.groups.month); // "01" console.log(match2.groups.day); // "15"

📃 Lookahead и Lookbehind

// Positive lookahead (?=...) // Найти число, за которым следует "px" const regex1 = /\d+(?=px)/; console.log("10px 20em".match(regex1)); // "10" // Negative lookahead (?!...) // Найти число, за которым НЕ следует "px" const regex2 = /\d+(?!px)/; console.log("10px 20em".match(regex2)); // "20" // Positive lookbehind (?<=...) // Найти число после "$" const regex3 = /(?<=\$)\d+/; console.log("$100 €200".match(regex3)); // "100" // Negative lookbehind (?<!...) // Найти число НЕ после "$" const regex4 = /(?<!\$)\d+/; console.log("$100 €200".match(regex4)); // "200"

Глава 8. Обработка ошибок

📜 Конструкция try...catch...finally

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

Простейшая ситуация - вызов функции, которой не существует:

callSomeFunc(); console.log("Остальные инструкции");

Здесь вызывается функция callSomeFunc(), которая нигде не определена. Соответственно при вызове этой функции мы столкнемся с ошибкой:

Uncaught ReferenceError: callSomeFunc is not defined

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

Ситуация может показаться искуственной, поскольку мы знаем, что функция callSomeFunc нигде не определена. Однако когда мы имеем дело с большой программой, различные куски которой определяли разные разработчики, становится сложнее контоллировать код. И таких ситуаций может быть много. Какие-то мы можем сами отследить и предупредить, а какие-то нет.

Для обработки подобных ситуаций JavaScript предоставляет конструкцию try...catch...finally, которая имеет следующее формальное определение:

try { инструкции блока try } catch (error) { инструкции блока catch } finally { инструкции блока finally }

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

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

После блока catch идет оператор finally со своим блоком инструкций. Этот блок выполняется в конце после блока try и catch вне зависимости, возникла ошибка или нет.

Стоит отметить, что только блок try является обязательным. А один из остальных блоков - catch или finally мы можем опустить. Однако один из этих блоков (не важно catch или finally) обязательно должен присуствовать. То есть мы можем использовать следующие варианты этой конструкции:

  • try...catch
  • try...finally
  • try...catch...finally

Например, обработаем с помощью этой конструкцию предыдущую ситуацию с несуществующей функцией:

try{ callSomeFunc(); console.log("Конец блока try"); } catch{ console.log("Возникла ошибка!"); } console.log("Остальные инструкции");

Итак, сначала выполняется блок try. Однако при выполнении первой же инструкции - вызова функции callSomeFunc() возникает ошибка. Это приведет к тому, что все последующие инструкции в блоке try НЕ будут выполняться. А управление перейдет к блоку catch. В этом блоке выводится сообщение, что возникла ошибка. После выполнения блока catch выполняются остальные инструкции программы. Таким образом, программа не прерывает свою работу при возникновении ошибки и продолжает свою работу. И в данном случае консольный вывод будет следующим:

Возникла ошибка!
Остальные инструкции

Рассмотим другой пример:

function callSomeFunc(){console.log("Функция callSomeFunc");} try{ callSomeFunc(); console.log("Конец блока try"); } catch(error){ console.log("Возникла ошибка!"); } console.log("Остальные инструкции");

Теперь функция callSomeFunc() определена в прогамме, поэтому при вызове функции ошибки не произойдет, и блок try доработает до конца. А блок catch при отсутствии ошибки не будет выолняться. И консольный вывод будет следующим:

Функция callSomeFunc
Конец блока try
Остальные инструкции

Получение ошибки в блоке catch

В качестве параметра в блок catch передается объект с информацией об ошибке:

try{ callSomeFunc(); console.log("Конец блока try"); } catch(error){ console.log("Возникла ошибка!"); console.log(error); }

В этом случае мы получим консольный вывод на подобие следующего:

Возникла ошибка!
ReferenceError: callSomeFunc is not defined
    at index.html:35

Блок finally

Конструкция try также может содержать блок finally. Мы можем использовать этот блок вместе с блоком catch или вместо него. Блок finally выполняется вне зависимости, произошла ошибка или нет. Например:

try{ callSomeFunc(); console.log("Конец блока try"); } catch{ console.log("Произошла ошибка"); } finally{ console.log("Блок finally") } console.log("Остальные инструкции");

Консольный вывод программы:

Произошла ошибка
Блок finally
Остальные инструкции

Если мы уберем блок catch и оставим только блок finally, то ошибка не будет обработана, и программа завершится ошибкой. Однако блок finally все равно выполнится:

try{ callSomeFunc(); console.log("Конец блока try"); } finally{ console.log("Блок finally") } console.log("Остальные инструкции");

Консольный вывод программы:

Блок finally
Uncaught ReferenceError: callSomeFunc is not defined

Базовый синтаксис

try { // Код, который может вызвать ошибку const result = riskyOperation(); console.log(result); } catch (error) { // Обработка ошибки console.log('Произошла ошибка:', error.message); }

Полный синтаксис с finally

try { // Попытка выполнить код const data = JSON.parse(jsonString); console.log(data); } catch (error) { // Обработка ошибки console.error('Ошибка парсинга JSON:', error.message); } finally { // Выполняется ВСЕГДА (даже если была ошибка) console.log('Очистка ресурсов...'); }
💡 Блок finally

Блок finally выполняется всегда: и при успехе, и при ошибке. Используется для освобождения ресурсов (закрытие файлов, соединений и т.д.).

📜 Генерация ошибок и оператор throw

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

Например, рассмотрим следующую ситуацию:

class Person{ constructor(name, age){ this.name = name; this.age = age; } print(){ console.log(`Name: ${this.name} Age: ${this.age}`);} } const tom = new Person("Tom", -123); tom.print(); // Name: Tom Age: -123

Класс Person описывает человека. В конструкторе класс получает значения для свойств name (имя) и age (возраст). Исходя из здравого смысла мы понимаем, что возраст не может быть отрицательным. Тем не менее пока, исходя из логики класса, ничего не мешает при создании объекта Person передать ему для возраста отрицательное значение. С точки зрения интерпретатора JavaScript ошибки нет, однако с точки логики и здравого смысла - это ошибка. Как исправить эту ситуацию? Есть различные способы, и один из них заключается в генерации исключения.

Для генерации исключения применяется оператор throw, после которого указывается информация об ошибке:

throw информация_об_ошибке;

Информация об ошибке может представлять любой объект.

Так, сгенерируем исключение при передаче в конструктор Person отрицательного значения для свойства age:

class Person{ constructor(name, age){ if(age < 0) throw "Возраст должен быть положительным"; this.name = name; this.age = age; } print(){ console.log(`Name: ${this.name} Age: ${this.age}`);} } const tom = new Person("Tom", -123); // Uncaught Возраст должен быть положительным tom.print();

В итоге при вызове конструктора Person будет сгенерировано исключение и программа завершится ошибкой. А на консоли браузера мы увидим информацию об ошибке, которая указана после оператора throw:

Uncaught Возраст должен быть положительным

Как и в общем случае мы можем обработать эту ошибку с помощью блока try...catch:

class Person{ constructor(name, age){ if(age < 0) throw "Возраст должен быть положительным"; this.name = name; this.age = age; } print(){ console.log(`Name: ${this.name} Age: ${this.age}`);} } try{ const tom = new Person("Tom", -123); // Uncaught Возраст должен быть положительным tom.print(); } catch(error){ console.log("Произошла ошибка"); console.log(error); // Возраст должен быть положительным }

throw в try..catch..finally

Оператор throw может вызываться в различных контекстах, например, в том же блоке try:

try{ throw "Непредвиденная ошибка!"; } catch(error){ console.log(error); // Непредвиденная ошибка! }

Это может быть полезно по ряду причин. Во-первых, мы можем тут же обработать ошибку. Во-вторых, мы можем определить с помощью блока finally некоторые завершающие действия, которые будут выполняться, даже если сгенерирована ошибка. Например:

// класс условной базы данных class Database{ constructor(){ this.data = ["Tom", "Sam", "Bob"]; } // получение данных getItem(index){ this.open(); if(index > 0 && index < this.data.length) return this.data[index]; else throw "Некорректный индекс"; this.close(); // при генерации исключения эта строка не будет выполняться } // открытие БД open(){ console.log("Подключение к базе данных установлено"); } // закрытие БД close(){ console.log("Подключение к базе данных закрыто"); } } const db = new Database(); try { db.getItem(5); // возвращаем полученный элемент } catch(err) { console.error(err); // если произошла ошибка обрабатываем ее }

Здесь определен класс Database - класс условной базы данных. Все данные хранятся в массиве data. Для взаимодействия с базой данных определены три метода. Методы open и close условно открывают и закрывают подключение к базе данных. Метод getItem получает по индексу элемент из массива data. Если же индекс некорректный, то генерируется ошибка. При этом до получения элемента по индексу метод getItem должен открыть подключение методом open, а после получения - закрыть методом close. Однако в примере выше при генерации ошибки закрытия подключения не произойдет:

else throw "Некорректный индекс"; this.close(); // при генерации исключения эта строка не будет выполняться

В итоге при передаче в метод getItem некорректного индекса консольный вывод программы будет следующим:

Подключение к базе данных установлено
Некорректный индекс

Однако что делать, если нам все таки надо вызвать метод close? Мы можем поместить его вызов в блок finally:

class Database{ constructor(){ this.data = ["Tom", "Sam", "Bob"]; } // получение данных getItem(index){ this.open(); try{ if(index > 0 && index < this.data.length) return this.data[index]; else throw "Некорректный индекс"; } finally{ // даже если сгенерирована ошибка, то этот блок выполняется this.close(); // при генерации исключения эта строка также будет выполняться } } // открытие бд open(){ console.log("Подключение к базе данных установлено"); } // закрытие бд close(){ console.log("Подключение к базе данных закрыто"); } } const db = new Database(); try { db.getItem(5); // возвращаем полученный элемент } catch(err) { console.error(err); // если произошла ошибка обрабатываем ее }

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

Подключение к базе данных установлено
Подключение к базе данных закрыто
Некорректный индекс

throw с Error

function divide(a, b) { if (b === 0) { throw new Error('Деление на ноль!'); } return a / b; } try { const result = divide(10, 0); console.log(result); } catch (error) { console.log('Ошибка:', error.message); // "Деление на ноль!" }

Пользовательские типы ошибок

class ValidationError extends Error { constructor(message) { super(message); this.name = 'ValidationError'; } } function validateAge(age) { if (age < 0) { throw new ValidationError('Возраст не может быть отрицательным'); } if (age > 150) { throw new ValidationError('Возраст слишком большой'); } return true; } try { validateAge(-5); } catch (error) { if (error instanceof ValidationError) { console.log('Ошибка валидации:', error.message); } else { console.log('Неизвестная ошибка:', error); } }

📜 Типы ошибок

В блоке catch мы можем получить информацию об ошибке, которая представляет объект. Все ошибки, которые генерируются интерпретатором JavaScript, предоставляют объект типа Error, который имеет ряд свойств:

  • message: сообщение об ошибке
  • name: тип ошибки

Стоит отметить, что отдельные браузеры поддерживают еще ряд свойств, но их поведение в зависимости от браузера может отличаться:

  • fileName: название файла с кодом JavaScript, где произошла ошибка
  • lineNumber: строка в файле, где произошла ошибка
  • columnNumber: столбец в строке, где произошла ошибка
  • stack: стек ошибки

Получим данные ошибки, например, при вызове несуществующей функции:

try{ callSomeFunc(); } catch(error){ console.log("Тип ошибки:", error.name); console.log("Ошибка:", error.message); }

Консольный вывод:

Тип ошибки: ReferenceError
Ошибка: callSomeFunc is not defined

Типы ошибок

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

  • EvalError: представляет ошибку, которая генерируется при выполнении глобальной функции eval()
  • RangeError: ошибка генерируется, если параметр или переменная, представляют число, которое находится вне некотоого допустимого диапазона
  • ReferenceError: ошибка генерируется при обращении к несуществующей ссылке
  • SyntaxError: представляет ошибку синтаксиса
  • TypeError: ошибка генерируется, если значение переменной или параметра представляют некорректный тип или пр попытке изменить значение, которое нельзя изменять
  • URIError: ошибка генерируется при передаче функциям encodeURI() и decodeURI() некорректных значений
  • AggregateError: предоставляет ошибку, которая объединяет несколько возникших ошибок

Например, при попытке присвоить константе второй раз значение, генерируется ошибка TypeError:

try{ const num = 9; num = 7; } catch(error){ console.log(error.name); // TypeError console.log(error.message); // Assignment to constant variable. }

SyntaxError - синтаксическая ошибка

// Ошибка: пропущена закрывающая скобка // if (x > 0 { // console.log('positive'); // }

ReferenceError - переменная не определена

// console.log(unknownVariable); // ReferenceError

TypeError - неправильный тип

// const num = 5; // num.toUpperCase(); // TypeError: num.toUpperCase is not a function

RangeError - значение вне допустимого диапазона

// const arr = new Array(-1); // RangeError: Invalid array length

Объект Error

try { throw new Error('Что-то пошло не так!'); } catch (error) { console.log(error.name); // "Error" console.log(error.message); // "Что-то пошло не так!" console.log(error.stack); // Стек вызовов (для отладки) }

📃 Применение типов ошибок

При генерации ошибок мы можем использовать встроенные типы ошибок. Например:

class Person{ constructor(name, age){ if(age < 0) throw new Error("Возраст должен быть положительным"); this.name = name; this.age = age; } print(){ console.log(`Name: ${this.name} Age: ${this.age}`);} } try{ const tom = new Person("Tom", -45); tom.print(); } catch(error){ console.log(error.message); // Возраст должен быть положительным }

Здесь конструктор класса Person принимает значения для имени и возаста человека. Если передан отрицательный возраст, то генерируем ошибку в виде объекта Error. В качестве параметра в конструктор Error передается сообщение об ошибке:

if(age < 0) throw new Error("Возраст должен быть положительным");

В итоге при обработке исключения в блоке catch мы сможем получить переданное сообщение об ошибке.

Все остальные типы ошибок в качестве первого параметра в конструкторе также принимают сообщение об ошибке. Так, сгенерируем несколько типов ошибок:

class Person{ constructor(pName, pAge){ const age = parseInt(pAge); if(isNaN(age)) throw new TypeError("Возраст должен представлять число"); if(age < 0 || age > 120) throw new RangeError("Возраст должен быть больше 0 и меньше 120"); this.name = pName; this.age = age; } print(){ console.log(`Name: ${this.name} Age: ${this.age}`);} }

Поскольку для возраста можно передать не только число, но и вообще какое угодно значение, то вначале мы пытаемся преобразовать это значение в число с помощью функции parseInt():

const age = parseInt(pAge); if(isNaN(age)) throw new TypeError("Возраст должен представлять число");

Далее с помощью функции isNaN(age) проверяем, является полученное число числом. Если age - НЕ число, то данная функция возвращает true. Поэтому генерируется ошибка типа TypeError.

Затем проверяем, что полученное число входит в допустимый диапазон. Если же не входит, генерируем ошибку типа RangeError:

if(age < 0 || age > 120) throw new RangeError("Возраст должен быть больше 0 и меньше 120");

Проверим генерацию исключений:

try{ const tom = new Person("Tom", -45); } catch(error){ console.log(error.message); // Возраст должен быть больше 0 и меньше 120 } try{ const bob = new Person("Bob", "bla bla"); } catch(error){ console.log(error.message); // Возраст должен представлять число } try{ const sam = new Person("Sam", 23); sam.print(); // Name: Sam Age: 23 } catch(error){ console.log(error.message); }

Консольный вывод:

Возраст должен быть больше 0 и меньше 120
Возраст должен представлять число
Name: Sam  Age: 23

📜 Обработка нескольких типов ошибок. Проверка типа ошибки

При выполнении одного и то же кода могут генерироваться ошибки разных типов. И иногда бывает необходимо разграничить обработку разных типов. В этом случае мы можем проверять тип возникшей ошибки. Например, пример выше с классом Person:

class Person{ constructor(pName, pAge){ const age = parseInt(pAge); if(isNaN(age)) throw new TypeError("Возраст должен представлять число"); if(age < 0 || age > 120) throw new RangeError("Возраст должен быть больше 0 и меньше 120"); this.name = pName; this.age = age; } print(){ console.log(`Name: ${this.name} Age: ${this.age}`);} } try{ const tom = new Person("Tom", -45); const bob = new Person("Bob", "bla bla"); } catch(error){ if (error instanceof TypeError) { console.log("Некорректный тип данных."); } else if (error instanceof RangeError) { console.log("Недопустимое значение"); } console.log(error.message); }
try { // некий код } catch (error) { if (error instanceof TypeError) { console.log('Ошибка типа'); } else if (error instanceof ReferenceError) { console.log('Переменная не найдена'); } else if (error instanceof SyntaxError) { console.log('Синтаксическая ошибка'); } else { console.log('Другая ошибка:', error.message); } }

📜 Встроенные типы ошибок

// Error - базовый тип throw new Error('Общая ошибка'); // SyntaxError - синтаксическая ошибка throw new SyntaxError('Неправильный синтаксис'); // ReferenceError - переменная не найдена throw new ReferenceError('Переменная не определена'); // TypeError - неправильный тип throw new TypeError('Неправильный тип данных'); // RangeError - выход за границы диапазона throw new RangeError('Значение вне диапазона'); // URIError - ошибка URI throw new URIError('Неправильный URI'); // EvalError - ошибка eval() (редко используется) throw new EvalError('Ошибка eval');

📜 Создание своих типов ошибок

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

class PersonError extends Error { constructor(value, ...params) { // остальные параметры передаем в конструктор базового класса super(...params) this.name = "PersonError" this.argument = value; } } class Person{ constructor(pName, pAge){ const age = parseInt(pAge); if(isNaN(age)) throw new PersonError(pAge, "Возраст должен представлять число"); if(age < 0 || age > 120) throw new PersonError(pAge, "Возраст должен быть больше 0 и меньше 120"); this.name = pName; this.age = age; } print(){ console.log(`Name: ${this.name} Age: ${this.age}`);} } try{ //const tom = new Person("Tom", -45); const bob = new Person("Bob", "bla bla"); } catch(error){ if (error instanceof PersonError) { console.log("Ошибка типа Person. Некорректное значение:", error.argument); } console.log(error.message); }

Консольный вывод

Ошибка типа Person. Некорректное значение: bla bla
Возраст должен представлять число

Для представления ошибки класса Person здесь определен тип PersonError, который наследуется от класса Error:

class PersonError extends Error { constructor(value, ...params) { // остальные параметры передаем в конструктор базового класса super(...params) this.name = "PersonError" this.argument = value; } }

В конструкторе мы определяем дополнительное свойство - argument. Оно будет хранить значение, которое вызвало ошибку. С помощью параметра value конструктора получаем это значение. Кроме того, переопреляем имя типа с помощью свойства this.name.

В классе Person используем этот тип, передавая в конструктор PersonError соответствующие значения:

if(isNaN(age)) throw new PersonError(pAge, "Возраст должен представлять число"); if(age < 0 || age > 120) throw new PersonError(pAge, "Возраст должен быть больше 0 и меньше 120");

Затем при обработки исключения мы можем проверить тип, и если он представляет класс PersonError, то обратиться к его свойству argument:

catch(error){ if (error instanceof PersonError) { console.log("Ошибка типа Person. Некорректное значение:", error.argument); } console.log(error.message); }

📜 Обработка ошибок и стек вызова функций

Если внутри функции возникает ошибка, которая не обрабатывается, то интерпретатор JavaScript выходит из этой функции во внешний код в поиске обработчика ошибки. Например:

function A(){ console.log("func A starts"); callSomeFunc(); console.log("func A ends"); } console.log("program ends");

Здесь функция A вызывает неопределенную функцию callSomeFunc(). Поэтому, выполнении программы при вызове функции A прерывается. Интерпретатор выходит во внешний код в поиске обработчика ошибки. Но во внешнем коде вокруг вызова функции A также не определена конструкция try..catch, поэтому выполнение всей программы аварийно завершится. Консольный вывод:

func A starts
Uncaught ReferenceError: callSomeFunc is not defined
    at A (index.html:11:2)
    at index.html:30:6

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

function A(){ console.log("func A starts"); callSomeFunc(); console.log("func A ends"); } function B(){ console.log("func B starts"); A(); console.log("func B ends"); } function C(){ console.log("func C starts"); B(); console.log("func C ends"); } C(); console.log("program ends");

Здесь вызывается функция C, которая вызывает функцию В, которая вызывает функцию А, а та - несуществующую функцию callSomeFunc. В итоге в функции А возникнет ошибка. Поскольку функция А не обработала ошибку, интерпретатор последовательно ищет обработчик ошибки в функции В, затем в функции С и в конце в глобальном контексте. Но так как нигде ошибка не обрабатывается, то после возникновения ошибки, выполнение программы завершится:

func C starts
func B starts
func A starts
Uncaught ReferenceError: callSomeFunc is not defined
    at A (index.html:11:2)
    at B (index.html:16:2)
    at C (index.html:27:2)
    at index.html:31:1

Теперь определим в одной из функций обработчик ошибки, например, в функции С:

function A(){ console.log("func A starts"); callSomeFunc(); console.log("func A ends"); } function B(){ console.log("func B starts"); A(); console.log("func B ends"); } function C(){ console.log("func C starts"); try{ B(); } catch{ console.log("Error occured"); } console.log("func C ends"); } C(); console.log("program ends");

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

func C starts
func B starts
func A starts
Error occured
func C ends
program ends

При этом поскольку функции А и В не обработали ошибку, то они дальше не выполняются.

📃 Проброс ошибки вверх по стеку вызова функций

Иногда ошибки обрабатываются где-то во вложенных вызовах других функций. Например, возьмем следующую ситуацию:

// класс условной базы данных class Database{ constructor(){ this.data = ["Tom", "Sam", "Bob"]; } // получение данных getItem(index){ if(index > 0 && index < this.data.length) return this.data[index]; else // если некорректный индекс - генерируем ошибку throw new RangeError("Invalid index"); } // открытие бд open(){ console.log("Database has opened"); } // закрытие бд close(){ console.log("Database has closed"); } } // функция-обертка для получения объекта из базы данных по индексу function get(index) { const db = new Database(); db.open(); // условно открываем бд try { return db.getItem(index); // возвращаем полученный элемент } catch(err) { console.error(err); // если произошла ошибка обрабатываем ее } db.close(); // условно закрываем бд } // вывод результата function printResult(){ const item = get(5); // пытаемся получить элемент с индексом 5 console.log("Got from database:",item); // выводим полученный элемент на консоль } printResult();

Здесь определен условный класс базы данных Database. Для взаимодействия с данными в нем определены три функции. Функции условного открытия и закрытия базы данных - функции open и close соответственно и функция getItem, которая возвращает элемент по определенному индексу из массива data. Однако если переданный индекс некорректен - меньше 0 или больше допустимого индекса, то генерируем ошибку RangeError.

Стоит отметить отметить, что это не единственный подход к реализации подобного функционала. Часто применяется другой подход, когда, если некорректный индекс/id, то функция возвращает null, что в определенных ситуациях может быть более предпочтительным. Но в данном случае для демонстрации остановимся на генерации исключения, если передан некорректный индекс.

Условимся, что, чтобы взаимодействовать с базой данных, нам надо ее вначале условно "открыть" методом open, а после завершения работы с ней "закрыть" с помощью метода close - довольно распространенный подход при работе с базами данных. Но чтобы абстрагироваться от всех этих деталей определяем дополнительную функцию get, которая принимает id и обращается к базе данных для получения элемента по id. И поскольку при обращении к методу getItem может произойти ошибка, то обрабатываем ее в конструкции try..catch

try { return db.getItem(index); // возвращаем полученный элемент } catch(err) { console.error(err); // если произошла ошибка обрабатываем ее }

Далее эту функцию вызываем из другой функции printResult, которая отображает полученный результат:

function printResult(){ const item = get(5); // пытаемся получить элемент с индексом 5 console.log("Got from database:",item); // выводим полученный элемент на консоль }

Если мы посмотрим на вывод программы:

Database has opened

RangeError: Invalid index at Database.getItem (index.html:19:19) at get (index.html:36:19) at printResult (index.html:44:18) at index.html:47:1 get @ index.html:38 printResult @ index.html:44 (anonymous) @ index.html:47

Database has closed Got from database: undefined

то увидим, что при передаче некорректного индекса, с одной стороны, ошибка обрабатывается, но с другой стороны, в функции printResult мы получаем неопределенный результат (значение undefined) и вообще не в курсе, что произошло - это можно узнать только по консольному выводу во время выполнения. Но функция get необязательно должна выводить ошибку на консоль, ошибка может обрабатываться каким-то другим образом. Соответственно возникает необходимость донести информацию об ошибке до более верхних уровней стека вызова функций (до функции printResult).

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

class Database{ constructor(){ this.data = ["Tom", "Sam", "Bob"]; } getItem(index){ if(index > 0 && index < this.data.length) return this.data[index]; else throw new RangeError("Invalid index"); } open(){ console.log("Database has opened"); } close(){ console.log("Database has closed"); } } function get(index) { const db = new Database(); db.open(); try { return db.getItem(index); } catch(err) { console.error(err); throw new Error(err.message); // снова генерируем ту же ошибку } finally{ db.close(); } } function printResult(){ try{ const item = get(5); console.log("Got from database:",item); } catch(err){ console.log(err); // обрабатываем ошибку из функции get } } printResult();

Теперь в функции get после обработки ошибки повторно генерируем ошибку с помощью оператора throw:

try { return db.getItem(index); } catch(err) { console.error(err); throw new Error(err.message); } finally{ db.close(); }

Также стоит отметить, что вызов db.close(), который условно закрывает базу данных, помещается в блок finally. Это гарантирует, что даже при генерации ошибки эта операция все равно будет выполнена.

Таким образом, если при вызове db.getItem произошла ошибка, то при вызове функции get тоже произойдет ошибка, соотвественно в функции printResult мы можем обработать эту ошибку:

function printResult(){ try{ const item = get(5); console.log("Got from database:",item); } catch(err){ console.log(err); } }

Этот код демонстрирует обёртку над функцией для безопасного выполнения с возвратом массива [результат, ошибка] вместо ручного использования try/catch.

Вначале определяется функция tryCatch:

function tryCatch (fn){ try{ return [fn(), null]; } catch(err){ return [null, err]; } }

tryCatch через параметр fn принимает другую функцию и вызывает ее внутри блока try/catch. В качестве результата возвращается массив с двумя элементами.

Если функция fn выполняется без ошибок, то возвращается массив [результат, null].

Если происходит ошибка — возвращается [null, ошибка].

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

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

const sqrt = (x) => { if (x < 0) throw new Error(`Number ${x} is invalid`); return Math.sqrt(x); };

Далее идут примеры вызова. В первом случае вызывается sqrt(-4), что вызывает исключение:

const [res1, err1] = tryCatch(()=>sqrt(-4));

Здесь tryCatch ловит исключение и возвращает массив [null, Error("Number -4 is invalid")]

Во втором случае вызов sqrt(4) возвращает 2, соответственно ошибки нет

const [res2, err2] = tryCatch(()=>sqrt(4));;

Фактически возвращается массив [2, null]

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

Error: Number -4 is invalid
sqrt(4): 2

Возьмем другой пример - создание объекта класса:

class Person{ constructor(name, age){ if(age < 0) throw `Недопустимый возраст ${age}. Минимальное значение: 1`; if(name.length < 2) throw `Недопустимое имя ${name}: минимальная длина имени - 2 символа`; this.name = name; this.age = age; } print(){ console.log(`Name: ${this.name} Age: ${this.age}`);} }

В данном случае класс Person генерирует исключение, если в конструктор передан недопустимый возраст - меньше 0 или если длина переданного имени меньше 2 символов. Используем вышеопределенную функцию tryCatch для обработки исключений:

function tryCatch (fn){ try{ return [fn(), null]; } catch(err){ return [null, err]; } } class Person{ constructor(name, age){ if(age < 0) throw `Недопустимый возраст ${age}. Минимальное значение: 1`; if(name.length < 2) throw `Недопустимое имя ${name}: минимальная длина имени - 2 символа`; this.name = name; this.age = age; } print(){ console.log(`Name: ${this.name} Age: ${this.age}`);} } // для примера пытаемся создать пару объектов const [tom, err1] = tryCatch(() => new Person("Tom", -123)); if(err1) console.error(err1); else tom.print(); const [bob, err2] = tryCatch(() => new Person("Bob", 46)); if(err2) console.error(err2); else bob.print();

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

Недопустимый возраст -123. Минимальное значение: 1
Name: Bob  Age: 46

Стоит отметить, что выше представлена только одна из возможных реализаций. Некоторые особенности могут отличаться. Например, функция-обертка может дополнительно обрабатывать ошибку. Так, стандартный тип для ошибок в JavaScript - это тип Error. Однако в коде разработчики не всегда генерируют ошибки этого типа. Например, в коде выше в конструкторе класса Person ошибка по сути представляет простую строку:

if(age < 0) throw `Недопустимый возраст ${age}. Минимальное значение: 1`; // ошибка представляет строку

И функция-обертка может проверять тип и при необходимости обертывать ошибку в тип Error:

function tryCatch (fn){ try{ return [fn(), null]; } catch(err){ // проверяем, представляет ли ошибка err объект Error const error = err instanceof Error ? err : new Error(String(err)) return [null, error]; } }

📜 Изоморфная обработка ошибок

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

Ее цельснизить дублирование кода, улучшить сопровождение и обеспечить предсказуемое поведение при возникновении сбоев.

Дело в том, что обработка ошибок с помощью конструкции try...catch может показаться громоздкой, особенно когда приходится писать вложенные блоки try..catch. С блоками try...catch связан ряд проблем. Например, соответствующие блоки захватывают создаваемые в этих блоках значения, соответственно приходится выносить значения во внешние переменные. А глубокая вложенность функций, которые также могут генерировать исключения, усложняют и код, и понимание того, что он делает.

Рассмотрим простейший пример:

// определение обертки для обработки ошибок function tryCatch (fn){ try{ return [fn(), null]; } catch(err){ return [null, err]; } } // использование const sqrt = (x) => { if(x < 0) throw new Error(`Number ${x} is invalid`); return Math.sqrt(x); } // пример получения ошибки const [res1, err1] = tryCatch(()=>sqrt(-4)); if(err1) console.error(err1); else console.log("sqrt(-4):", res1); // пример получения результата const [res2, err2] = tryCatch(()=>sqrt(4)); if(err2) console.error(err2); else console.log("sqrt(4):", res2);

📃 Обработка функции с произвольным числом аргументов

Также сама функция-обертка может отличаться. Так, мы могли бы определить следующую функцию:

const tryWrap = (fn) => (...args) => { try{ return [fn(...args), null]; } catch(err){ return [null, err]; } }

tryWrap определена как стрелочная функция, которая через параметр fn принимает другую функцию и возвращает новую функцию. Возвращаемая функция вызывает функцию fn с переданными аргументами (...args), но делает это внутри блока try/catch. Обратите внимание, что через параметр args можно передать произвольное количество аргументов. В остальном логика та же самая, что и у выше определнной функции tryCatch: если функция fn выполняется без ошибок, то возвращается массив [результат, null]. Если происходит ошибка, то возвращается массив [null, ошибка].

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

// на примере функции sqrt const [res1, err1] = tryWrap(sqrt)(-4); const [res2, err2] = tryWrap(sqrt)(4); // на примере конструктора класса Person const [tom, tomErr] = tryWrap((name, age)=>new Person(name, age))("Tom", -123); const [bob, bobErr] = tryWrap((name, age)=>new Person(name, age))("Bob", 46);

Можно в принципе не определять функцию-обертку, если используемые функции изначально поддерживают требуемые условия - возвращают информацию о результате функции и ошибке в требуемом формате. Например:

// функция возвращает данные в формате "[результат, ошибка]" const sqrt = (x) => { if(x < 0) return [null, new Error(`Number ${x} is invalid`)]; return [Math.sqrt(x), null]; } const [res1, err1] = sqrt(-4); if(err1) console.error(err1); else console.log("sqrt(-4):", res1); const [res2, err2] = sqrt(4); if(err2) console.error(err2); else console.log("sqrt(4):", res2);

Таким образом мы получили унифицированный способ для обработки ошибок, и нам не надо писать try/catch каждый раз при вызове отдельной функции, которая может сгенерировать ошибку. Особенно данный подход упрощает написание кода в функциональном стиле. Для асинхронных операций этот подход можно адаптировать в виде async/await-совместимого варианта.

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

📜 Обработка асинхронных ошибок

Promise с catch

fetch('https://api.example.com/data') .then(response => response.json()) .then(data => console.log(data)) .catch(error => { console.error('Ошибка запроса:', error); });

async/await с try...catch

async function fetchData() { try { const response = await fetch('https://api.example.com/data'); const data = await response.json(); console.log(data); } catch (error) { console.error('Ошибка:', error.message); } finally { console.log('Запрос завершён'); } } fetchData();

📜 Лучшие практики

✅ Используйте try...catch для критичного кода
// Хорошо function parseJSON(jsonString) { try { return JSON.parse(jsonString); } catch (error) { console.error('Ошибка парсинга:', error.message); return null; } } // Плохо - ошибка остановит всю программу function parseJSONBad(jsonString) { return JSON.parse(jsonString); // Может упасть }
⚠️ Не игнорируйте ошибки
// Плохо - ошибка проглатывается try { riskyOperation(); } catch (error) { // Пустой блок - ошибка игнорируется } // Хорошо - хотя бы логируйте try { riskyOperation(); } catch (error) { console.error('Ошибка:', error); // или отправьте в систему мониторинга }
💡 Создавайте информативные сообщения об ошибках
// Плохо throw new Error('Ошибка'); // Хорошо throw new Error(`Пользователь с ID ${userId} не найден`); // Ещё лучше throw new Error(` Не удалось загрузить данные пользователя. ID: ${userId} Endpoint: /api/users/${userId} Статус: ${response.status} `);

📜 Глобальная обработка ошибок

В браузере

// Перехват всех необработанных ошибок window.addEventListener('error', (event) => { console.error('Глобальная ошибка:', event.error); // Отправить в систему логирования }); // Перехват необработанных Promise rejection window.addEventListener('unhandledrejection', (event) => { console.error('Необработанный Promise rejection:', event.reason); });

В Node.js

// Перехват необработанных ошибок process.on('uncaughtException', (error) => { console.error('Необработанная ошибка:', error); process.exit(1); // Завершить процесс }); // Перехват необработанных Promise rejection process.on('unhandledRejection', (reason, promise) => { console.error('Необработанный Promise rejection:', reason); });

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

class FormValidationError extends Error { constructor(field, message) { super(message); this.name = 'FormValidationError'; this.field = field; } } function validateForm(data) { if (!data.email || !data.email.includes('@')) { throw new FormValidationError('email', 'Неправильный email'); } if (!data.password || data.password.length < 6) { throw new FormValidationError('password', 'Пароль должен быть минимум 6 символов'); } if (data.age && (data.age < 0 || data.age > 150)) { throw new FormValidationError('age', 'Неправильный возраст'); } return true; } // Использование try { const formData = { email: 'test@example.com', password: '12345', age: 25 }; validateForm(formData); console.log('Форма валидна!'); } catch (error) { if (error instanceof FormValidationError) { console.log(`Ошибка в поле "${error.field}": ${error.message}`); // Подсветить поле с ошибкой в UI } else { console.error('Неизвестная ошибка:', error); } }

📜 Отладка ошибок

// Вывод стека вызовов try { throw new Error('Тестовая ошибка'); } catch (error) { console.log('Название:', error.name); console.log('Сообщение:', error.message); console.log('Стек вызовов:'); console.log(error.stack); } // Использование debugger function problematicFunction() { debugger; // Остановка выполнения в DevTools // код для отладки } // console.trace() - вывод стека вызовов function a() { b(); } function b() { c(); } function c() { console.trace(); } a(); // Выведет весь путь вызовов: a -> b -> c

Глава 9. Встроенные объекты: Date, Math, Number, Symbol, Proxy

📜 Объект Date. Работа с датами

Объект Date позволяет работать с датами и временем в JavaScript.

📃 Создание даты

Существуют различные способы создания объекта Date:

  • С помошью пустого конструктора Date без параметров. В этом случае созданный объект хранит текущие дату и время
    const currentDate = new Date(); console.log(currentDate); // Thu Oct 26 2023 13:17:53 GMT+0100
  • В конструктор Date передается количества миллисекунд, которые прошли с начала эпохи Unix, то есть с 1 января 1970 года 00:00:00 GMT:
    const myDate = new Date(1359270000000); console.log(myDate); // Sun Jan 27 2013 11:00:00 GMT+0400 (Москва, стандартное время)
  • В конструктор Date передаются день, месяц и год:
    const date1 = new Date("27 March 2008"); console.log(date1); // Thu Mar 27 2008 00:00:00 GMT+0300 (Москва, стандартное время) // или так const date2 = new Date("3/27/2008"); console.log(date2); // Thu Mar 27 2008 00:00:00 GMT+0300 (Москва, стандартное время) // или так const date3 = new Date("3 27 2008"); console.log(date3); // Thu Mar 27 2008 00:00:00 GMT+0300 (Москва, стандартное время)
    Если мы используем полное название месяца, то оно пишется в по-английски, если используем сокращенный вариант, тогда используется формат "месяц/день/год" или "месяц день год".
  • Четвертый способ состоит в передаче в конструктор Date всех параметров даты и времени:
    const myDate = new Date(2012,11,25,18,30,20,10); console.log(myDate); // Tue Dec 25 2012 18:30:20 GMT+0400 (Москва, стандартное время)
    В данном случае используются по порядку следующие параметры:
    new Date(год, месяц, число, час, минуты, секунды, миллисекунды).
    При этом надо учитывать, что отсчет месяцев начинается с нуля, то есть январь - 0, а декабрь - 11.
const now = new Date(); // Текущая дата и время const specificDate = new Date(2024, 0, 1); // 1 января 2024 const fromString = new Date('2024-01-01'); const fromTimestamp = new Date(1704067200000);
⚠️ Месяцы начинаются с 0

В конструкторе Date месяцы нумеруются с 0 (январь = 0, декабрь = 11). Будьте внимательны!

📃 Получение даты и времени

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

  • getDate(): возвращает день месяца
  • getDay(): возвращает день недели (отсчет начинается с 0 - воскресенье, и последний день - 6 - суббота)
  • getMonth(): возвращает номер месяца (отсчет начинается с нуля, то есть месяц с номер 0 - январь)
  • getFullYear(): возвращает год
  • toDateString(): возвращает полную дату в виде строки
  • getHours(): возвращает час (от 0 до 23)
  • getMinutes(): возвращает минуты (от 0 до 59)
  • getSeconds(): возвращает секунды (от 0 до 59)
  • getMilliseconds(): возвращает миллисекунды (от 0 до 999)
  • toTimeString(): возвращает полное время в виде строки

Получим текущую дату:

const today = new Date(); console.log(today.getDate()); // 26 console.log(today.getDay()); // 4 console.log(today.getMonth()); // 9 console.log(today.getFullYear()); // 2023

Преобразуем данные в более читабельную форму:

const days = ["Воскресенье", "Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота"]; const months = ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"]; const today = new Date(); console.log(`Сегодня: ${today.getDate()} ${months[today.getMonth()]} ↩ ${today.getFullYear()}, ${days[today.getDay()]}`); // Сегодня: 26 Октябрь 2023, Четверг

Перевести из числовых значений в более привычные названия для дней недели и месяцев используются массивы. Получив индекс дня недели (today.getDay()) и индекс месяца (today.getMonth()) можно получить нужный элемент из массива.

Теперь получим текущее время:

var welcome; const myDate = new Date(); const hour = myDate.getHours(); const minute = myDate.getMinutes(); const second = myDate.getSeconds(); console.log(`Текущее время: ${hour}:${minute}:${second}`); // Текущее время: 13:38:26

📃 Установка даты и времени

Кроме задания параметров даты в конструкторе для установки мы также можем использовать дополнительные методы объекта Date:

  • setDate(): установка дня в дате
  • setMonth(): уставовка месяца (отсчет начинается с нуля, то есть месяц с номер 0 - январь)
  • setFullYear(): устанавливает год
  • setHours(): установка часа
  • setMinutes(): установка минут
  • setSeconds(): установка секунд
  • setMilliseconds(): установка миллисекунд

Установим дату:

const myDate = new Date(); myDate.setDate(14); myDate.setMonth(10); // ноябрь myDate.setFullYear(2023); console.log(myDate); // Tue Nov 14 2023 13:41:20 GMT+0300 (Москва, стандартное время)

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

myDate.setHours(54);

В этом случае значение часа будет равно 54 - 24 * 2 = 6, а оставшиеся часы будут составлять два дня (24 * 2), что прибавит к дате два дня. То же самое действует и в отношении дней, минут, секунд, миллисекунд и месяцев.

Установка значений (set методы)

const date = new Date(); date.setFullYear(2025); date.setMonth(11); // Декабрь (помним про 0-индекс) date.setDate(31); date.setHours(23); date.setMinutes(59); date.setSeconds(59); console.log(date); // 31 декабря 2025, 23:59:59

📃 Таблица методов Date

Метод Описание Пример
getFullYear() Год (4 цифры) 2024
getMonth() Месяц (0-11): 0 = январь 0 для января
getDate() День месяца (1-31) 15
getDay() День недели (0-6): 0 = воскресенье 1 для понедельника
getHours() Часы (0-23) 14 для 14:00
getMinutes() Минуты (0-59) 30
getSeconds() Секунды (0-59) 45
getMilliseconds() Миллисекунды (0-999) 123
getTime() Timestamp (миллисекунды с 1970-01-01) 1704067200000
getTimezoneOffset() Разница с UTC в минутах -180 для UTC+3
💡 Важно помнить
  • getMonth() возвращает 0-11, а не 1-12
  • getDay() возвращает 0 для воскресенья
  • Для установки значений используйте set* методы

📃 Форматирование даты

const date = new Date(); // Форматирование в строку const formatted = `${date.getDate()}.${date.getMonth() + 1}.${date.getFullYear()}`; console.log(formatted); // "07.11.2025" // С добавлением нулей function pad(num) { return String(num).padStart(2, '0'); } const formatted2 = `${pad(date.getDate())}.${pad(date.getMonth() + 1)}.${date.getFullYear()}`; console.log(formatted2); // "07.11.2025" // Получение названия месяца const months = ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь']; const monthName = months[date.getMonth()]; // Получение названия дня недели const days = ['Воскресенье', 'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота']; const dayName = days[date.getDay()];

Форматирование времени (12/24 часа)

function getTime12h(date) { let hours = date.getHours(); const minutes = String(date.getMinutes()).padStart(2, '0'); const ampm = hours >= 12 ? 'PM' : 'AM'; hours = hours % 12 || 12; // 0 преобразуем в 12 return `${hours}:${minutes} ${ampm}`; } const now = new Date(); console.log(getTime12h(now)); // "2:30 PM" // 24-часовой формат function getTime24h(date) { const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); return `${hours}:${minutes}`; } console.log(getTime24h(now)); // "14:30"

Разница между датами

const date1 = new Date('2024-01-01'); const date2 = new Date('2024-12-31'); // Разница в миллисекундах const diff = date2 - date1; // Конвертация в дни (по шагам из "Макфарланда") const second = 1000; // 1000 миллисекунд в секунде const minute = 60 * second; // 60 секунд в минуте const hour = 60 * minute; // 60 минут в часе const day = 24 * hour; // 24 часа в сутках const totalDays = diff / day; // общее количество дней console.log(`Разница: ${totalDays} дней`); // 365 дней // Короткий вариант (всё в одну формулу) const days = Math.floor(diff / (1000 * 60 * 60 * 24)); console.log(`Разница: ${days} дней`); // 365 дней // Функция для разницы с часами и минутами function dateDiff(date1, date2) { const diff = Math.abs(date2 - date1); const days = Math.floor(diff / (1000 * 60 * 60 * 24)); const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); return {days, hours, minutes}; } console.log(dateDiff(date1, date2)); // {days: 365, hours: 0, minutes: 0}

Встроенные методы форматирования

const date = new Date(); console.log(date.toLocaleDateString('ru-RU')); // "07.11.2025" console.log(date.toLocaleTimeString('ru-RU')); // "14:30:45" console.log(date.toLocaleString('ru-RU')); // "07.11.2025, 14:30:45" // С опциями const options = { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' }; console.log(date.toLocaleDateString('ru-RU', options)); // "пятница, 7 ноября 2025 г."

📜 Объект Math. Математические операции

Объект Math предоставляет ряд математических функций, которые можно использовать при вычислениях. Рассмотрим основные математические функции.

abs()

Функция abs() возвращает абсолютное значение числа:

const x = -25; console.log(Math.abs(x)); // 25 const y = 34; console.log(Math.abs(y)); // 34

min() и max()

Функции min() и max() возвращают соответственно минимальное и максимальное значение из набора чисел:

const max = Math.max(19, 45); // 45 const min = Math.min(33, 24); // 24

Эти функции необязательно должны принимать два числа, в них можно передавать и большее количество чисел:

const max = Math.max(1, 2, 3, -9, 46, -23); // 46

ceil()

Функция ceil() округляет число до следующего наибольшего целого числа:

const x = Math.ceil(9.2); // 10 const y = Math.ceil(-5.9); // -5

Выражение Math.ceil(9.2) возвращает число 10, так как число 10 следующее наибольшее целое число после 9.2. И также выражение Math.ceil(-5.9) возвращает -5, потому что число -5 следующее наибольшее целое после -5.9

floor()

Функция floor() округляет число до следующего наименьшего целого числа:

const x = Math.floor(9.2); // 9 const y = Math.floor(-5.9); // -6

round()

Функция round() округляет число до следующего наименьшего целого числа, если его десятичная часть меньше 0.5. Если же десятичная часть равна или больше 0.5, то округление идет до ближайшего наибольшего целого числа:

const x = Math.round(5.5); // 6 const y = Math.round(5.4); // 5 const z = Math.round(-5.4); // -5 const n = Math.round(-5.5); // -5 const m = Math.round(-5.6); // -6 console.log(x); console.log(y); console.log(z); console.log(n);

random()

Функция random() возвращает случайное число с плавающей точкой из диапазона от 0 до 1:

const x = Math.random();

pow()

Функция pow() возвращает число в определенной степени. Например, возведем число 2 в степень 3:

const x = Math.pow(2, 3); // 8

sqrt()

Функция sqrt() возвращает квадратный корень числа:

const x = Math.sqrt(121); // 11 const y = Math.sqrt(9); // 3 const z = Math.sqrt(20); // 4.47213595499958

log()

Функция log() возвращает натуральный логарифм числа:

const x = Math.log(1); // 0 const z = Math.log(10); // 2.302585092994046

Тригонометрические функции

Целый ряд функций представляют тригонометрические функции:

  • sin() - вычисляет синус угла
  • cos() - вычисляет косинус угла
  • tan() - вычисляет тангенс угла
  • asin() - вычисляет арксинус числа
  • atan() - вычисляет арктангенс числа
const x = Math.sin(90); // 0.8939966636005579 const y = Math.cos(0); // 1 const z = Math.tan(45); // 1.6197751905438615 const x = Math.asin(0.9); // 1.1197695149986342 const y = Math.acos(1); // 1 const z = Math.atan(1); // 0.7853981633974483

Константы

Кроме методов объект Math также определяет набор встроенных констант, которые можно использовать в различных вычислениях:

  • Math.PI - (число PI): 3.141592653589793
  • Math.SQRT2 - (квадратный корень из двух): 1.4142135623730951
  • Math.SQRT1_2 - (половина от квадратного корня из двух): 0.7071067811865476
  • Math.E - (число e или число Эйлера): 2.718281828459045
  • Math.LN2 - (натуральный логарифм числа 2): 0.6931471805599453
  • Math.LN10 - (натуральный логарифм числа 10): 2.302585092994046
  • Math.LOG2E - (двоичный логарифм числа e): 1.4426950408889634
  • Math.LOG10E - (десятичный логарифм числа e): 0.4342944819032518

Используем константы в вычислениях:

const x = Math.log(Math.E); // 1 const z = Math.tan(Math.PI/4); // 0.9999999999999999

📜 Объект Number

Объект Number представляет числа. Чтобы создать число, надо передать в конструктор Number число или строку, представляющую число:

const x = new Number(34); const y = new Number('34'); console.log(x+y); // 68

Определения x и y в данном случае будут практически аналогичны.

Однако создавать объект Number можно и просто присвоив переменной определенное число:

const z = 34;

Объект Number предоставляет ряд свойств и методов.

Некоторые его свойства:

  • Number.MAX_VALUE: наибольшее возможное целое число. Приблизительно равно 1.79E+308. Числа, которые больше этого значения, рассматриваются как Infinity
  • Number.MIN_VALUE: наименьшее возможное положительное целое число. Приблизительно равно 5e-324 (где-то около нуля)
  • Number.NaN: специальное значение, которое указывает, что объект не является числом
  • Number.NEGATIVE_INFINITY: значение, которое обозначает отрицательную бесконечность и которое возникает при перелнении. Например, если мы складываем два отрицательных числа, которые по модулю равны Number.MAX_VALUE. Например:
    const x = -1 * Number.MAX_VALUE const y = -1 * Number.MAX_VALUE const z = x + y; if(z === Number.NEGATIVE_INFINITY) console.log("отрицательная бесконечность"); else console.log(z);
  • Number.POSITIVE_INFINITY: положительная бесконечность. Также, как и отрицательная бесконечность, возникает при переполнении, только теперь в положительную сторону:
    const x = Number.MAX_VALUE const y = Number.MAX_VALUE const z = x * y; if(z === Number.POSITIVE_INFINITY) console.log("положительная бесконечность"); else console.log(z);

Некоторые основные методы:

  • isNaN(): определяет, является ли объект числом. Если объект не является числом, то возвращается значение true:
    const a = Number.isNaN(Number.NaN); // true const b = Number.isNaN(true); // false - new Number(true) = 1 const c = Number.isNaN(null); // false - new Number(null) = 0 const d = Number.isNaN(25); // false const e = Number.isNaN("54"); // false

    Но следующее выражение вернет false, хотя значение не является числом:

    const f = Number.isNaN("hello"); // false

    Чтобы избежать подобных ситуаций, лучше применять глобальную функцию isNaN:

    const f = isNaN("hello"); // true
  • parseFloat(): преобразует строку в число с плавающей точкой. Например:
    const a = Number.parseFloat("34.90"); // 34.9 console.log(a); const b = Number.parseFloat("hello"); // NaN console.log(b); const c = Number.parseFloat("34hello"); // 34 console.log(c);
  • parseInt(): преобразует строку в целое число. Например:
    const a = Number.parseInt("34.90"); // 34 console.log(a); const b = Number.parseInt("hello"); // NaN console.log(b); const c = Number.parseInt("25hello"); // 25 console.log(c);
  • toFixed(): оставляет в числе с плавающей точкой определенное количество знаков в дробной части. Например:
    const a = 10 / 1.44; console.log("До метода toFixed(): ", a, "<br/>"); a = a.toFixed(2); // оставляем два знака после запятой console.log("После метода toFixed(): ", a, "<br/>");

    Вывод браузера:

    До метода toFixed(): 6.944444444444445
    После метода toFixed(): 6.94

Преобразование в другую систему счисления

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

const num1 = "110"; const num2 = parseInt(num1, 2); console.log(num2); // 6

Здесь в функцию parseInt в качестве второго параметра передается число 2, что указаывает, что первый параметр будет рассматриваться как число в двоичной системе. Результатом будет 6, так как 110 в двоичной системе - это число 6 в десятичной.

Из числа в строку, метод toString

Для преобразования числа в строку у него можно вызвать специальный метод toString():

const num = 10; console.log(num.toString()); // "10"

Однако в данном случае нет смысла вызывать у числа num метод toString(), так как число можно вывести на консоль без всяких преобразований. Тем не менее метод toString() может быть полезен - в качестве параметра он принимает основание системы счисления числа и может быть использован для вывода числа в определенной системе счисления:

const num1 = 0b0110; // выводим число в двоичной системе console.log(num1.toString(2)); // 110 const num2 = 0xFF; // выводим число в шестнадцатеричной системе console.log(num2.toString(16)); // ff

Преобразование строки в число

// Способ 1: Number() const num1 = Number('123'); // 123 const num2 = Number('12.5'); // 12.5 const num3 = Number('abc'); // NaN // Способ 2: parseInt() - только целые const int1 = parseInt('123'); // 123 const int2 = parseInt('12.99'); // 12 const int3 = parseInt('20 лет'); // 20 const int4 = parseInt('FF', 16); // 255 (шестнадцатеричная) // Способ 3: parseFloat() - с дробной частью const float1 = parseFloat('12.99'); // 12.99 const float2 = parseFloat('3.14px'); // 3.14 // Способ 4: унарный плюс const num4 = +'123'; // 123

Проверка на число

// isNaN() - проверка на NaN console.log(isNaN('abc')); // true console.log(isNaN(123)); // false // Number.isNaN() - строгая проверка console.log(Number.isNaN('abc')); // false (строка, не NaN) console.log(Number.isNaN(NaN)); // true // Number.isFinite() - проверка на конечное число console.log(Number.isFinite(123)); // true console.log(Number.isFinite(Infinity)); // false console.log(Number.isFinite('123')); // false (строка) // Number.isInteger() - проверка на целое число console.log(Number.isInteger(123)); // true console.log(Number.isInteger(12.5)); // false

Округление чисел

const num = 4.567; // Math.round() - до ближайшего целого console.log(Math.round(num)); // 5 console.log(Math.round(4.4)); // 4 // Math.ceil() - вверх console.log(Math.ceil(4.1)); // 5 // Math.floor() - вниз console.log(Math.floor(4.9)); // 4 // Math.trunc() - отбросить дробную часть console.log(Math.trunc(4.9)); // 4 // toFixed() - до N знаков после запятой console.log(num.toFixed(2)); // "4.57" (строка!) console.log(Number(num.toFixed(2))); // 4.57 (число)

Форматирование валюты

const price = 1234.5; // toFixed() для валюты const formatted = price.toFixed(2); // "1234.50" // Locale formatting const ruPrice = price.toLocaleString('ru-RU', { style: 'currency', currency: 'RUB' }); // "1 234,50 ₽" const usPrice = price.toLocaleString('en-US', { style: 'currency', currency: 'USD' }); // "$1,234.50"

Случайные числа

// Math.random() возвращает 0...1 (не включая 1) const random = Math.random(); // 0.123456... // Случайное число от 0 до 10 const random10 = Math.floor(Math.random() * 10); // Случайное число от 1 до 6 (кубик) const dice = Math.ceil(Math.random() * 6); // Функция для диапазона [min, max] function randomRange(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } console.log(randomRange(10, 20)); // например, 15

Математические операции

// Абсолютное значение console.log(Math.abs(-5)); // 5 // Степень console.log(Math.pow(2, 3)); // 8 console.log(2 ** 3); // 8 (ES2016) // Квадратный корень console.log(Math.sqrt(16)); // 4 // Максимум и минимум console.log(Math.max(1, 5, 3)); // 5 console.log(Math.min(1, 5, 3)); // 1 // С массивом const numbers = [1, 5, 3, 9, 2]; console.log(Math.max(...numbers)); // 9 console.log(Math.min(...numbers)); // 1

Константы Math

console.log(Math.PI); // 3.141592653589793 console.log(Math.E); // 2.718281828459045 (число Эйлера) console.log(Math.SQRT2); // 1.4142135623730951 (√2) console.log(Math.LN2); // 0.6931471805599453 (ln(2)) console.log(Math.LN10); // 2.302585092994046 (ln(10))

📜 Работа с BigInt (ES2020)

// BigInt для работы с очень большими числами const big1 = 1234567890123456789012345678901234567890n; const big2 = BigInt("9999999999999999999999999999"); console.log(big1 + big2); // Работает! // Нельзя смешивать с обычными числами // console.log(big1 + 100); // Ошибка! console.log(big1 + 100n); // OK // Преобразование console.log(Number(123n)); // 123 console.log(BigInt(123)); // 123n

📜 Символы (Symbol)

Символ или тип Symbol представляет некоторое уникальное значение.

Для определения символа применяется конструктор типа Symbol. Например, создадим простейший символ:

const tom = Symbol("Tom"); console.log(tom); // Symbol(Tom)

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

const tom = Symbol("Tom"); console.log(tom); // Symbol(Tom) const tomas = Symbol("Tom"); console.log(tomas); // Symbol(Tom) console.log(tom == tomas); // false console.log(tom === tomas); // false

Несмотря на то, что оба создаваемых выше в примее символа инициализированы одним и тем же значением, но оба оператора - равенства и эквивалентности при сравнению этих символа возвращают false. То есть символы всегда уникальны.

Символы как идентификаторы свойств объектов

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

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

const company = { "Tom": "senior", "Sam": "junior", "Tom": "junior" } for(developer in company) { console.log(`${developer} - ${company[developer]}`); }

Однако консольный вывод покажет, что в нашей компании только два разработчика, поскольку названия двух из них совпадают:

Tom - junior
Sam - junior

Теперь применим символы:

const company = { [Symbol("Tom")]: "senior", [Symbol("Sam")]: "junior", [Symbol("Tom")]: "junior" } const developers = Object.getOwnPropertySymbols(company); for(developer of developers) { console.log(`${developer.toString()} - ${company[developer]}`); }

Для получения всех символов из объекта применяется функция Object.getOwnPropertySymbols(), в которую передается объект. Возвращает эта функция набор символов, которые мы можем перебрать в цикле. Для получения текстового представления символов можно применять метод toString() символа. А для получения значения, как и в общем случае, применяется синтаксис массивов: company[developer]. В итоге мы получим следующий консольный вывод:

Symbol(Tom) - senior
Symbol(Sam) - junior
Symbol(Tom) - junior

Также можно динамически добавлять свойства с символьными идентификаторами в объект:

const company = { }; company[Symbol("Tom")]= "senior"; company[Symbol("Sam")]= "junior"; company[Symbol("Tom")]= "junior";

📜 Proxy

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

Для создания объекта Proxy применяется конструктор Proxy():

const proxy = new Proxy(target, handler);

Конструктор Proxy принимает два параметра:

  • target - цель создания прокси, это может быть любой объект, к которому применяется Proxy
  • handler - другой объект, который определяет, какие именно операции объекта target будут перехватываться и переопределяться и как именно.

Рассмотрим простейший пример:

// объект, к которому применяется прокси const target = {name: "Tom"}; // объект, который определяет, как будет переопределяться target const handler = {}; // объект прокси const proxy = new Proxy(target, handler); console.log(proxy.name); // Tom

Итак, в примере выше target - это объект, к которому будет применяться проксирование. В данном случае этот объект имеет свойство name.

const target = {name: "Tom"};

Далее создается пустой обработчик handler:

const handler = {};

В принципе этот объект должен определять, как будет переопределяться объект target. Но пока оставим его пустым.

Затем создаем объект Proxy, передавая в его конструктор объекты target и handler.

const proxy = new Proxy(target, handler);

Проксирование объекта (в данном случае объекта target) означает, что через прокси мы можем обращаться к функциональности этого объекта. И в данном случае через объект proxy мы можем обратиться к свойству name проксированного объекта target:

console.log(proxy.name); // Tom

И поскольку мы использовали пустой handler, который ничего не переопределяет, то по сути прокси ведет себя как оригинальный объект target.

📃 Переопределение функциональности объекта

Выше мы выполнили проксирование объекта, но пока никак не переопределяли его поведение. Ключевым в данном случае является определение обработчика handler, который может перехватывать обращения к свойствам проксированного объекта. Этот обработчик может определять два метода: get и set.

Метод get и получение свойств объекта

Метод get перехватывает обращения к свойству при получении его значения и возвращает для этого свойства некоторое значение:

const handler = { get: function(target, prop, receiver) { return некоторое_значение; } };

Метод get имеет три параметра:

  • target: сам проксированный объект. Благодаря этому параметру мы можем обратиться к функциональности оригинального объекта
  • prop: название свойства, к которому идет обращение
  • receiver: объект Proxy, через который выполняется проксирование

Возьмем следующий пример:

const target = {name: "Tom"}; const handler = { get: function(target, prop, receiver) { return "Tomas Smith"; } }; const proxy = new Proxy(target, handler); console.log(proxy.name); // Tomas Smith

Здесь в обработчике handler в методе get возвращается строка "Tomas Smith":

get: function(target, prop, receiver) { return "Tomas Smith"; }

Это приведет к тому, что при обращение к любому свойству прокси-объекта будет возвращаться данная строка:

console.log(proxy.name); // Tomas Smith

Так, мы выполнили перехват обращения к свойству и простейшее переопределение. При этом мы можем обащаться и к оригинальному объекту, который проксируется:

const target = {name: "Tom"}; const handler = { get: function(target, prop) { return "Name: " + target.name; } }; const proxy = new Proxy(target, handler); console.log(proxy.name); // Name: Tom

Здесь обработчик возвращает строку "Name: " + target.name, где target.name представляет обращение к свойству name оригинального объекта. Естественно логика возвращение значения свойства может более сложной.

Но возьмем более сложный объект - с двумя свойствами:

const target = {name: "Tom", age: 37}; const handler = { get: function(target, prop) { return target[prop]; } }; const proxy = new Proxy(target, handler); console.log(proxy.name); // Tom console.log(proxy.age); // 37

Здесь целевой объект имеет два свойства: name и age. В обработчике мы перехватываем обращение к ним, но никак его не переопределяем, а просто возвращаем значения свойств оригинального объекта:

return target[prop];

Для обращения к свойствам целевого объекта применяется синтаксис массивов.

Но также мы можем проверить, к какому именно свойству идет обращение, и выполнить некоторое переопределение:

const target = {name: "Tom", age: 37}; const handler = { get: function(target, prop) { if(prop==="name") return target.name.toUpperCase(); else return target[prop]; } }; const proxy = new Proxy(target, handler); console.log(proxy.name); // TOM console.log(proxy.age); // 37

В данном случае, если обращение идет к свойству name, то есть к свойству, которое хранит строку, то вызываем у этой строки метод toUpperCase() и переводим ее в верхний регистр.

Установка свойства и метод set

Метод set перехватывает обращения к свойству при установке его значения:

const handler = { set: function(target, property, value, receiver) { } };

Метод set имеет четыре параметра:

  • target: оригинальный объект, к которому идет проксирование
  • property: название свойства, к которому идет обращение
  • value: устанавливаемое значение
  • receiver: объект Proxy, через который выполняется проксирование

Рассмотрим на примере:

const target = {name: "Tom", age: 37}; const handler = { set: function(target, prop, value) { console.log(value); target[prop] = value; } }; const proxy = new Proxy(target, handler); proxy.name = "Tomas"; console.log(proxy.name); // Tomas proxy.age = 22; console.log(proxy.age); // 22

В данном примере в методе set сначала логирруем передаваеемое свойству значение, затем устанавливаем свойство:

target[prop] = value;

Немного изменим пример:

const target = {name: "Tom", age: 37}; const handler = { set: function(target, prop, value) { if(prop==="age" && value < 1) console.log("Некорректный возраст"); else return target[prop] = value; } }; const proxy = new Proxy(target, handler); proxy.name = "Tomas"; console.log(proxy.name); // Tomas proxy.age = -199; // Некорректный возраст console.log(proxy.age); // 37 proxy.age = 22; console.log(proxy.age); // 22

Здесь в методе set обработчика проверяем, если идет установка свойства age и значение меньше 1, то просто выводим сообщение о некорректности данных

if(prop==="age" && value < 1) console.log("Некорректный возраст");

Иначе передаем значение свойству оригинального объекта:

else return target[prop] = value;

Глава 10. Коллекции и итераторы

📜 Итераторы

Итераторы представляют абстракцию для перебора наборов данных и применяются для организации последовательного доступа к элементам наборов данных - массивам, объектам Set, Map, строкам и т.д..Так, благодаря итераторам мы можем перебрать набор данных (например, массив) с помощью цикла for-of:

const people = ["Tom", "Bob", "Sam"]; for(const person of people){ console.log(person); }

В цикле for-of справа от оператора of указывается набор данных или перебираемый объект (то, что назвается Iterable), из которого в цикле мы можем получить отдельные элементы. Но эта возможность перебора некоторого объекта, как, например, массива в примере выше, реализуются благодаря тому, что эти объекты применяют итераторы. Рассмотрим подробнее, что представляют итераторы и как можно создать свой итератор.

Получение итератора

Любой итерируемый объект (например, массив, Map, Set и т.д.) хранит в свойстве Symbol.iterator функцию, которая возвращает связанный с объектом итератор:

const people = ["Tom", "Bob", "Sam"]; // получаем итератор массива const iterator = people[Symbol.iterator](); console.log(iterator); // Array Iterator {}

Здесь получаем итератор массива, поэтому на консоль будет выведено что-то наподобие Array Iterator {}

Другой пример - строка тоже представляет перебираемый объект, которую можно перебрать посимвольно:

const username = "Tom"; for(char of username){ console.log(char); }

Соответственно для строки мы тоже можем получить итератор:

const username = "Tom"; // получаем итератор строки const iterator = username[Symbol.iterator](); console.log(iterator); // StringIterator {}

Итератор строки представляет тип StringIterator. Аналогичным образом можно получать итераторы и для других типов перебираемых объектов.

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

const people = ["Tom", "Bob", "Sam"]; console.log(people.entries()); // Array Iterator {}

📃 Метод next итераторов

Итераторы предоставляют метод next(), который возвращает объект с двумя свойствами: value и done

{value, done}

Свойство value хранит собственно значение текущего перебираемого элемента. А свойство done указывает, есть ли еще в коллекции объекты, доступные для перебора. Если в наборе еще есть элементы, то свойство done равно false Если же доступных элементов для перебора больше нет, то это свойство равно true, а метод next() возвращает объект

{done: true}

Например:

const people = ["Tom", "Bob", "Sam"]; const iter = people[Symbol.iterator](); const result = iter.next(); console.log(result); // {value: "Tom", done: false}

В данном случае вызываем метод next() и получаем из итератора первыый результат:

{value: "Tom", done: false}

Здесь мы видим, что текущий объект представляет строку "Tom", а значение done: false указывает, что в массиве еще есть элементы для перебора.

Мы можем последовательно несколько раз вызвать метод next() для получения других элементов массива:

const people = ["Tom", "Bob", "Sam"]; const iter = people[Symbol.iterator](); console.log(iter.next()); // {value: "Tom", done: false} console.log(iter.next()); // {value: "Bob", done: false} console.log(iter.next()); // {value: "Sam", done: false} console.log(iter.next()); // {value: undefined, done: true}

Консольный вывод программы:

{value: "Tom", done: false}
{value: "Bob", done: false}
{value: "Sam", done: false}
{value: undefined, done: true}

Здесь мы видим, что при каждом новом вызове метода next() мы получаем из массива следующий объект. А когда объектов для перебора больше не останется, то свойство done будет равно true.

Используя метод next(), мы сами можем перебрать все объекты массива:

const people = ["Tom", "Bob", "Sam"]; const iter = people[Symbol.iterator](); while(!(item = iter.next()).done){ console.log(item.value); }

Здесь в цикле while из метода next() итератора получаем текущий объект в переменную item: item = items.next()

И смотрим на ее свойство done - если оно равно false (то есть в наборе еще есть элементы), то продолжаем цикл

while(!(item = iter.next()).done){

В цикле обращаемся к свойству value полученного объекта

console.log(item.value);

Консольный вывод:

Tom
Bob
Sam

Но в этом нет смысла, поскольку все коллекции, которые возвращают итераторы, поддерживают перебор с помощью цикла for...of, который как раз и использует итератор для получения элементов.

📃 Создание своего итератора

Для примера реализуем итератор, который перебирает массив с конца:

const people = ["Tom", "Bob", "Sam"]; function reverseArrayIterator(array) { let count = array.length; return { next: function(){ if (count > 0) { return { value: array[--count], done: false }; } else { return { value: undefined, done: true }; } } } }; const iter = reverseArrayIterator(people); while(!(item = iter.next()).done){ console.log(item.value); }

Здесь сначала инициализируется переменная count, которая определяет количество перебранных элементов массива. Первоначально переменная имеет значение, равное длине массива.

Далее функция возвращает объект итератора. Его метод next() реализует поведение итерации: если счетчик count больше 0 (то есть имеются еще элементы для перебора), то next() возвращает объект, свойство done которого имеет значение false (поскольку итератор еще не достиг конца или точнее начала массива), а свойство value содержит соответствующий элемент из массива, на который указывает переменная count после декремента.

Когда переменная count станет равна 0 (т. е. итератор достиг конца), next() возвращает объект, у которого свойство done имеет значение true, а свойство value имеет значение undefined.

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

Sam
Bob
Tom

Однако при выполнении цикла for..of элементы массива по прежнему перебираются с начала. Применим наш итератор глобально, чтобы он также использовался в цикле for..of:

const people = ["Tom", "Bob", "Sam"]; function reverseArrayIterator() { const array = this; let count = array.length; return { next: function(){ if (count > 0) { return { value: array[--count], done: false }; } else { return { value: undefined, done: true }; } } } }; // меняем итератор для массива people people[Symbol.iterator]=reverseArrayIterator; for(person of people){ console.log(person); }

Здесь сделано два ключевых изменения:

  • Во-первых, нам надо внутри итератора получить текущий объект через this:
    const array = this;
  • Созданную функцию итератора надо присвоить свойству Symbol.iterator:
    people[Symbol.iterator]=reverseArrayIterator;

📃 Создание итерируемых объектов

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

Для создания перебираемого объекта нам надо определить в объекта метод [Symbol.iterator](). Этот метод собственно и будет представлять итератор:

const iterable = { [Symbol.iterator]() { return { next() { // если еще есть элементы return { value: ..., done: false }; // если больше нет элементов return { value: undefined, done: true }; } }; } };

Метод [Symbol.iterator]() возвращает объект, который имеет метод next(). Этот метод возвращает объект с двумя свойствами value и done.

Если в нашем объекте есть элементы, то свойство value содержит собственно значение элемента, а свойство done равно false.

Если доступных элементов больше нет, то свойство done равно true.

Например, реализуем простейший объект с итератором, который возвращает некоторый набор чисел:

const iterable = { [Symbol.iterator]() { return { current: 1, end: 3, next() { if (this.current <= this.end) { return { value: this.current++, done: false }; } return { done: true }; } }; } };

Здесь итератор фактически возвращает числе от 1 до 3. Для отслеживания текущего элемента в объекте, который возвращается методом , определены два свойства:

current: 1, end: 3,

Свойство current собственно хранит значение текущего элемента. А свойство end задает предел. То есть в данном случае итератор возвращает числа от 1 до 3.

В методе next(), если текущее значение меньше или равно предельному значению, возвращаем объект

return { value: this.current++, done: false };

Инкремент this.current++ приведет к тому, что при следующем вызове метода next значение current будет на единицу больше.

Если достигнут предел, то возвращаем объект

return { done: true };

Это будет указывать, что объектов больше нет.

Получим из итератора возвращаемые им элементы:

const myIterator = iterable[Symbol.iterator](); // получаем итератор console.log(myIterator.next()); // {value: 1, done: false} console.log(myIterator.next()); // {value: 2, done: false} console.log(myIterator.next()); // {value: 3, done: false} console.log(myIterator.next()); // {done: true}

Здесь сначала получаем итератор в константу myIterator. Затем при обращении к ее методу next() последовательно получаем все элементы. При четвертом вызове метода next условный перебор элементов в итераторе закончен, и метод возвращает объект {done: true}.

Однако, если мы хотим перебрать наш объект и получить из него его элементы, то нам не надо обращаться к методу next(). Поскольку объект iterable реализует итератор, то его можно перебрать с помощью цикла for-of:

const iterable = { [Symbol.iterator]() { return { current: 1, end: 3, next() { if (this.current <= this.end) { return { value: this.current++, done: false }; } return { done: true }; } }; } }; for (const value of iterable) { console.log(value); }

Консольный вывод:

1
2
3

Цикл for-of автоматически обращается к методу next() и извлекает значение.

Рассмотрим еще один пример:

// объект-компания const company = { // массив работников employees: [ {name: "Tom", age: 39, position: "Senior Developer"}, {name: "Bob", age: 43, position: "Middle Developer"}, {name: "Sam", age: 28, position: "Junior Developer"}, ] }; // устанавливаем итератор company[Symbol.iterator] = function() { const array = this.employees; // получаем массив работников let current = 0; return { next() { if (current < array.length) { return { value: array[current++].name, done: false }; } return { value:undefined, done: true }; } }; }; for (const employee of company) { console.log(employee); }

Здесь объект company представляет условную компанию, в которой есть массив работников - массив employee. Допустим, с помощью итератора мы хотим получать имя каждого работника. Для этого для объекта company устанавливаем функцию итератора, которая перебирает все элементы из массива employees. Консольный вывод программы:

Tom
Bob
Sam

📜 Генераторы

Генераторы представляют особый тип функции, которые используются для генерации значений. Для определения генераторов применяется символ звездочки *, который ставится после ключевого слова function. Например, определим простейший генератор:

function* getNumber(){ yield 5; } const numberGenerator = getNumber(); const result = numberGenerator.next(); console.log(result); // {value: 5, done: false}

Здесь функция getNumber() представляет генератор. Основные моменты создания и применения генератора:

  • Генератор определяется как функция с помощью оператора function* (символ звездочки после слова function)
    function* getNumber(){ .... }
    Функция генератора возвращает итератор.
  • Для возвращения значения из генератора, как и вообще в итераторах, применяется оператор yield, после которого указывается возвращаемое значение
    yield 5;
    То есть фактически в данном случае генератор getNumber() генерирует число 5.
  • Для получения значения из генератора применяется метод next()
    const result = numberGenerator.next();
    Так, в примере с помощью вызова функции getNumber() создается объект итератора в виде константы numberGenerator. Используя этот объект, мы можем получать из генератора значения.

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

{value: 5, done: false}

То есть по сути возвращается объект, свойство value которого содержит собственно сгенерированное значение. А свойство done указывает, достигли ли мы конца генератора.

Можно заметить, что генераторы похожи на итераторы, но по сути генераторы - это особая форма итераторов.

Теперь изменим код:

function* getNumber(){ yield 5; } const numberGenerator = getNumber(); let next = numberGenerator.next(); console.log(next); next = numberGenerator.next(); console.log(next);

Здесь обращение к методу next() происходит два раза:

{value: 5, done: false}
{value: undefined, done: true}

Но функция генератора getNumber генерирует только одно значение - число 5. Поэтому при повторном вызове свойство value будет иметь значение undefined, а свойство done - true, то есть работа генератора завершена.

Генератор может создавать/генерировать множество значений:

function* getNumber(){ yield 5; yield 25; yield 125; } const numberGenerator = getNumber(); console.log(numberGenerator.next()); console.log(numberGenerator.next()); console.log(numberGenerator.next()); console.log(numberGenerator.next());

Консольный вывод:

{value: 5, done: false}
{value: 25, done: false}
{value: 125, done: false}
{value: undefined, done: true}

То есть при первом вызове метода next() из итератора извлекается значение, которое идет после первого оператора yield, при втором вызове метода next() - значение после второго оператора yield и так далее.

Для упрощения мы можем возвращать в генераторе элементы из массива:

const numbers = [5, 25, 125, 625]; function* getNumber(){ for(const n of numbers){ yield n; } } const numberGenerator = getNumber(); console.log(numberGenerator.next().value); // 5 console.log(numberGenerator.next().value); // 25

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

const numberGenerator = getNumber(); console.log(numberGenerator.next().value); // 5 // ряд других действий console.log(numberGenerator.next().value); // 25

Генератор необязательно содержит только определение операторов yield. Он также может содержать более сложную логику.

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

function* points(){ let x = 0; let y = 0; while(true){ yield {x:x, y:y}; x += 2; y += 1; } } let pointGenerator = points(); console.log(pointGenerator.next().value); console.log(pointGenerator.next().value); console.log(pointGenerator.next().value);

Консольный вывод:

    {x: 0, y: 0}
    {x: 2, y: 1}
    {x: 4, y: 2}

📃 Возвращение из генератора и функция return

Как выше мы увидели, каждый последующий вызов next() возвращает следующее значение генератора, однако мы можем завершить работу генератора с помощью метода return():

function* getNumber(){ yield 5; yield 25; yield 125; } const numberGenerator = getNumber(); console.log(numberGenerator.next()); // {value: 5, done: false} numberGenerator.return(); // завершаем работу генератора console.log(numberGenerator.next()); // {value: undefined, done: true}

📃 Получение значений генератора в цикле

Поскольку для получения значений применяется итератор, то мы можем использовать цикл for...of:

function* getNumber(){ yield 5; yield 25; yield 125; } const numberGenerator = getNumber(); for(const num of numberGenerator){ console.log(num); }

Консольный вывод:

5
25
125

Также мы можем применять и другие типы циклов, например, цикл while:

function* getNumber(){ yield 5; yield 25; yield 125; } const numberGenerator = getNumber(); while(!(item = numberGenerator.next()).done){ console.log(item.value); }

📃 Передача данных в генератор

Инициализация генератора

Функция генератора, как и любая другая функция, может принимать параметры. Соответственно через параметры мы можем передать в генератор некоторые данные. Например:

function* getNumber(start, end, step){ for(let n = start; n <= end; n +=step){ yield n; } } const numberGenerator = getNumber(0, 8, 2); for(num of numberGenerator){ console.log(num); }

Здесь в функцию генератора передается начальное конечное значения и шаг приращенния чисел. Консольный вывод:

0
2
4
6
8

Другой пример - определим генератор, который возвращет данные из массива:

function* generateFromArray(items){ for(item of items) yield item; } const people = ["Tom", "Bob", "Sam"]; const personGenerator = generateFromArray(people); for(person of personGenerator) console.log(person);

В данном случае в генератор передается массив, который используется для генерации значений. Консольный вывод:

Tom
Bob
Sam

Передача данных в метод next

С помощью next() можно передать в генератор данные. Переданные в этот метод данные можно получить в функции генератора через предыдущий вызов оператора yield:

function* getNumber(){ const n = yield 5; // получаем значение numberGenerator.next(2).value console.log("n:", n); const m = yield 5 * n; // получаем значение numberGenerator.next(3).value console.log("m:", m); yield 5 * m; } const numberGenerator = getNumber(); console.log(numberGenerator.next().value); // 5 console.log(numberGenerator.next(2).value); // 10 console.log(numberGenerator.next(3).value); // 15

Консольный вывод:

5
n: 2
10
m: 3
15

При втором вызове метода next():

numberGenerator.next(2).value

Мы можем получить переданные через него данные, присвоив результат первого вызова оператора yield:

const n = yield 5;

То есть здесь константа n будет равна 2, так как в метод next() передается число 2.

Далее мы можем использовать это значение, например, для генерации нового значения:

const m = yield 5 * n;

Соответственно, константа m получит значение, переданное через третий вызов метода next(), то есть число 3.

📃 Обработка ошибок генератора

С помощью функции throw() мы можем сгенерировать в генераторе исключение. В качестве параметра в эту функцию передается произвольное значение, которое представляет информацию об ошибке:

function* generateData(){ try { yield "Tom"; yield "Bob"; yield "Hello Work"; } catch(error) { console.log("Error:", error); } } const personGenerator = generateData(); console.log(personGenerator.next()); // {value: "Tom", done: false} personGenerator.throw("Something wrong"); // Error: Something wrong console.log(personGenerator.next()); // {value: undefined, done: true}

Прежде всего в функции генератора для обработки возможного исключения используем конструкцию try..catch. В блоке catch с помощью параметра error мы можем получить информацию об ошибке, которая передается в функцию throw().

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

personGenerator.throw("Something wrong");

В итоге этот вызов приведет к генерации исключения в функции генератора, и управление перейдет к блоку catch, который выводит информацию об ошибке на консоль:

catch(error) { console.log("Error:", error); }

Консольный вывод программы:

{value: "Tom", done: false}
Error: Something wrong
{value: undefined, done: true}

Стоит отметить, что после вызова функции throw() генератор завершает работу, а далее при вызове метода next() мы получим результат {value: undefined, done: true}

📜 Set - коллекция уникальных значений

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

Создание Set

Для создания множества применяется конструктор этого объекта:

const mySet = new Set();

Также можно передать в конструктор массив значений, которыми будет инициализировано множество:

const arr = [1, 1, 2, 3, 4, 5, 2, 4]; const numbers = new Set(arr); console.log(numbers); // Set(5) {1, 2, 3, 4, 5}

В данном случае в множество передаются данные из массива. Однако поскольку множество может хранить только уникальные значения, то при его создании повторяющиеся значения, которые есть в массиве, удаляются.

Для упрощения создания набора мы можем сразу передать массив в конструктор Set:

const numbers = new Set([1, 2, 3, 4, 5]); console.log(numbers); // Set(5) {1, 2, 3, 4, 5}

Размер набора

Для проверки количества элементов можно использовать свойство size.

const numbers = new Set([1, 1, 2, 3, 4, 5, 2, 4]); console.log(numbers.size); // 5

Добавление

Для добавления применяется метод add(). Его результатом является измененное множество:

const numbers = new Set(); numbers.add(1); numbers.add(3); numbers.add(5); numbers.add(3); // не добавляется numbers.add(1); // не добавляется console.log(numbers); // Set(3) {1, 3, 5}

При этом, поскольку множество хранит только уникальные значения, то добавление элементов, которые уже в нем есть, не имеет смысла.

Так как метод add возвращает ссылку на это же множество, то мы можем вызывать методы по цепочке:

const numbers = new Set(); numbers.add(1).add(3).add(5); console.log(numbers); // Set(3) {1, 3, 5}

Удаление

Для удаления элементов применяется метод delete():

const numbers = new Set([1, 3, 5]); numbers.delete(3); console.log(numbers); // Set(2) {1, 5}

Причем данный метод возвращает булевое значение: true - если элемент удален и false - если удаление не произошло (например, когда удаляемого элемента нет в множестве):

const numbers = new Set([1, 3, 5]); let isDeleted = numbers.delete(3); console.log(isDeleted); // true isDeleted = numbers.delete(54); console.log(isDeleted); // false

Если необходимо удалить вообще все элементы из множества, то применяется метод clear():

let numbers = new Set(); const numbers = new Set([1, 3, 5]); numbers.clear(); console.log(numbers); // Set(0) {}

Проверка наличия элемента

Если нужно проверить, если ли элемент в множестве, то используется метод has(). Если элемент есть, то метод возвращает true, иначе возвращает false:

const numbers = new Set([1, 3, 5]); console.log(numbers.has(3)); // true console.log(numbers.has(32)); // false

Перебор множества

Для перебора элементов множества применяется метод forEach():

const numbers = new Set([1, 2, 3, 5]); numbers.forEach(function(value1, value2, set){ console.log(value1); })

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

Также для перебора множества можно использовать цикл for...of:

const numbers = new Set([1, 2, 3, 5]); for(n of numbers){ console.log(n); }

Получение итератора

Также у объекта Set есть ряд методов, которые возвращают итератор, а точнее объект SetIterator. Это методы values(), keys(), entries():

const numbers = new Set([1, 2, 3, 5]); console.log(numbers.values()); // SetIterator {1, 2, 3, 5} console.log(numbers.keys()); // SetIterator {1, 2, 3, 5} console.log(numbers.entries()); // SetIterator {1 => 1, 2 => 2, 3 => 3, 5 => 5}

Соответственно возвращаемый итератор мы можем использовать для получения объектов множества:

const people = new Set(["Tom", "Bob", "Sam"]); const iterator = people.values(); console.log(iterator.next()); // {value: "Tom", done: false} console.log(iterator.next()); // {value: "Bob", done: false} console.log(iterator.next()); // {value: "Sam", done: false} console.log(iterator.next()); // {value: undefined, done: true}

Удаление из массива повторяющихся элементов

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

const peopleArray = ["Tom", "Bob", "Sam", "Alice", "Sam", "Kate", "Tom"]; const peopleSet = new Set(peopleArray); const newPeopleArray = Array.from(peopleSet); console.log(newPeopleArray); // ["Tom", "Bob", "Sam", "Alice", "Kate"]

Здесь для создания нового массива с неповторяющимися элементами применяется функция Array.from(), которая в качестве аргумента получает объект Set.

// Пустой Set const set = new Set(); // Set из массива const set2 = new Set([1, 2, 3, 4, 5]); // Set автоматически удаляет дубликаты const set3 = new Set([1, 2, 2, 3, 3, 4]); console.log(set3); // Set(4) {1, 2, 3, 4} // Set из строки const set4 = new Set('hello'); console.log(set4); // Set(4) {'h', 'e', 'l', 'o'}

Основные методы Set

const set = new Set(); // add(value) - добавить элемент set.add(1); set.add(2); set.add(2); // Игнорируется (дубликат) set.add(3); // has(value) - проверка наличия console.log(set.has(2)); // true console.log(set.has(5)); // false // delete(value) - удалить элемент set.delete(2); console.log(set.has(2)); // false // size - количество элементов console.log(set.size); // 2 // clear() - очистить Set set.clear(); console.log(set.size); // 0

Разбор примера: удаление дубликатов массива

const arr = [1, 2, 2, 3, 3, 4]; // Пошаговый разбор: // Шаг 1: new Set(arr) // Создаёт Set из массива, автоматически удаляя дубликаты const uniqueSet = new Set(arr); console.log(uniqueSet); // Set(4) {1, 2, 3, 4} // Шаг 2: ...uniqueSet // Spread оператор разворачивает Set в отдельные элементы // Результат: 1, 2, 3, 4 (просто значения, не массив) // Шаг 3: [...uniqueSet] // Квадратные скобки собирают развёрнутые элементы обратно в массив const unique = [...uniqueSet]; console.log(unique); // [1, 2, 3, 4] // Всё вместе в одну строку: const unique2 = [...new Set(arr)]; // [1, 2, 3, 4] // Альтернатива с Array.from(): const unique3 = Array.from(new Set(arr)); // [1, 2, 3, 4]

Перебор Set

const set = new Set(['apple', 'banana', 'orange']); // forEach set.forEach(value => { console.log(value); }); // for...of for (let value of set) { console.log(value); } // keys() и values() возвращают одно и то же (для совместимости с Map) for (let value of set.keys()) { console.log(value); } for (let value of set.values()) { console.log(value); } // entries() возвращает [value, value] for (let entry of set.entries()) { console.log(entry); // ['apple', 'apple'] }

Цепочка вызовов

const set = new Set(); // add() возвращает сам Set set.add(1) .add(2) .add(3); console.log(set); // Set(3) {1, 2, 3}

📜 WeakSet - слабый Set

WeakSet похож на Set, но может содержать только объекты, и они хранятся "слабо".

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

Для создания объекта WeakSet используется его конструктор, в который можно передать начальные значения:

// пустой WeakSet const weakSet1 = new WeakSet(); // инициализация начальными значениями const weakSet2 = new WeakSet([{name:"Tom", age: 37}, {name:"Alice", age: 34}]);

Для инициализации как в случае с объектом Set в конструктор передается массив, но данный массив содержит именно объекты, а не скалярные значения, типа чисел или строк.

Для добавления данных в WeakSet применяется метод add():

const weakSet = new WeakSet(); weakSet.add({lang: "JavaScript"}); weakSet.add({lang: "TypeScript"}); // weakSet.add(34); // так нельзя - 34 - число, а не объект console.log(weakSet); // {{lang: "JavaScript"}, {lang: "TypeScript"}}

Причем опять же добавить мы можем только объект, а не скалярные значения типа чисел или строк.

Для удаления применяется метод delete(), в который передается ссылка на удаляемый объект:

const weakSet = new WeakSet(); const js = {lang: "JavaScript"}; const ts = {lang: "TypeScript"}; weakSet.add(js); weakSet.add(ts); weakSet.delete(js); console.log(weakSet); // {{lang: "TypeScript"}}

Если надо проверить, имеется ли объект в WeakSet, то можно использовать метод has(), который возвращает true при наличии объекта:

const js = {lang: "JavaScript"}; const ts = {lang: "TypeScript"}; const java = {lang: "Java"}; const weakSet = new WeakSet([js, ts]); console.log(weakSet.has(ts)); // true console.log(weakSet.has(java)); // false

Перебор WeakSet

Стоит отметить, что WeakSet не поддерживает перебор ни с помощью метода ForEach, которого у WeakSet нет, ни с помощью цикла for. Например. если мы попробуем перебрать WeakSet через цикл for..of:

const weakSet = new WeakSet([ {lang: "JavaScript"}, {lang: "TypeScript"}, {lang: "Java"} ]); for(item of weakSet){ console.log(item); }

То мы получим ошибку:

Uncaught TypeError: weakSet is not iterable

Слабые ссылки

Объекты передаются в WeakSet по ссылке. И отличительной особенностью WeakSet является то, что когда объект перестает существовать в силу различных причин, он удаляется из WeakSet. Так, рассмотрим следующий пример:

let js = {lang: "JavaScript"}; let ts = {lang: "TypeScript"}; const weakSet = new WeakSet([js, ts]); js = null; console.log(weakSet); // {{lang: "JavaScript"}, {lang: "TypeScript"}} console.log("Некоторая работа"); const timerId = setTimeout(function(){ console.log(weakSet); // {{lang: "TypeScript"}} clearTimeout(timerId); }, 20000);

В данном случае сначала объект WeakSet хранит ссылки на два объекта: js и ts. Далее мы устанавливаем значение для переменной js в null.

Это приведет к тому, что спустя некоторое время начальное значение этой переменной будет удалено сборщиком мусора JavaScript.

js = null;

Причем, если сразу после этого мы посмотрим на содержимое weakSet, то увидим, что объект js в нем еще присутствует. Однако спустя некоторое время ссылка будет удалена из weakSet. Для эмуляции прошествия времени здесь используется функция setTimeout, которая выводит на консоль содержимое weakSet через 9000 секунд (конкретный период времени, через который сборщик мусора удалит значение, может отличаться)

Теперь сравним с тем, что произойдет, если вместо WeakSet использовать Set:

let js = {lang: "JavaScript"}; let ts = {lang: "TypeScript"}; const set = new Set([js, ts]); js = null; console.log(set); // Set(2) {{lang: "JavaScript"}, {lang: "TypeScript"}} console.log("Некоторая работа"); const timerId = setTimeout(function(){ console.log(set); // Set(2){{lang: "JavaScript"}, {lang: "TypeScript"}} clearTimeout(timerId); }, 20000);

В случае с Set даже спустя некоторое время мы увидим, что в объекте Set до сих пор присутствует объект, для которого было установлено значение null

const weakSet = new WeakSet(); let user1 = {name: 'Alice'}; let user2 = {name: 'Bob'}; // Добавление объектов weakSet.add(user1); weakSet.add(user2); console.log(weakSet.has(user1)); // true // Когда объект больше не нужен user1 = null; // Будет удалён из WeakSet автоматически
⚠️ Ограничения WeakSet
  • Только объекты (не примитивы)
  • Нельзя перебирать
  • Нет свойства size
  • Нет метода clear()

Map - коллекция пар ключ-значение

Map — это коллекция для хранения данных в формате ключ-значение. В отличие от обычных объектов, ключами в Map могут быть любые значения (включая объекты и функции).

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

Создание Map

Для создания словаря применяется конструктор объекта Map:

const myMap = new Map();

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

const myMap = new Map([[1, "a"], [2, "b"], [3, "c"]]); console.log(myMap); // Map(3){1 => "a", 2 => "b", 3 => "c"}

В данном случае числа 1, 2, 3 являются ключами, а строки "a", "b", "c" - значениями.

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

const dict = new Map([["red", "красный"], ["green", "зеленый"], ["blue", "синий"]]); console.log(dict); // Map(3) {"red" =>"красный", "green"=> "зеленый", "blue"=>"синий"}
// Пустой Map const map = new Map(); // Map из массива пар const map2 = new Map([ ['name', 'Alice'], ['age', 25], ['city', 'Moscow'] ]); // Из объекта const obj = {name: 'Bob', age: 30}; const map3 = new Map(Object.entries(obj));

Размер словаря

С помощью свойства size можно проверить количество элементов в Map:

const dict = new Map([["red", "красный"], ["green", "зеленый"], ["blue", "синий"]]); console.log(dict.size); // 3

Добавление и изменение элементов

Для установки значения применяется метод set():

const dict = new Map([["red", "красный"], ["green", "зеленый"], ["blue", "синий"]]); dict.set("yellow", "желтый"); // добавление элемента dict.set("red", "червонный"); // изменение элемента console.log(dict); // Map(4) {"red" => "червонный", "green" => "зеленый", ↩ // "blue" => "синий", "yellow" => "желтый"}

Первый параметр метода set() представляет ключ, а второй параметр - значение элемента. Если по такому ключу нет элементов, то добавляется новый элемент. Если ключ уже есть, то уже имеющийся элемент изменяет свое значение.

Получение элементов

Для получения элемента по ключу применяется метод get(), в который передается ключ элемента:

const dict = new Map([["red", "красный"], ["green", "зеленый"], ["blue", "синий"]]); console.log(dict.get("red")); // красный console.log(dict.get("violet")); // undefined

Если map не содержит элемента по заданному ключу, то метод возвращает undefined.

Чтобы избежать возвращения undefined мы можем проверить наличие элемента по ключу с помощью метода has(). Если элемент по ключу имеется, то метод возвращает true, иначе возвращается false:

const dict = new Map([["red", "красный"], ["green", "зеленый"], ["blue", "синий"]]); console.log(dict.has("red")); // true console.log(dict.has("violet")); // false if(dict.has("red")) console.log(dict.get("red")); // красный

Удаление элементов

Для удаления одного элемента по ключу применяется метод delete():

const dict = new Map([["red", "красный"], ["green", "зеленый"], ["blue", "синий"]]); dict.delete("red"); console.log(dict); // Map(2){"green" => "зеленый", "blue" => "синий"}

Для удаления всех элементов используется метод clear():

const dict = new Map([["red", "красный"], ["green", "зеленый"], ["blue", "синий"]]); dict.clear(); console.log(dict); // Map(0){}

Перебор элементов

Для перебора элементов используется метод forEach:

const dict = new Map([["red", "красный"], ["green", "зеленый"], ["blue", "синий"]]); dict.forEach(function(value, key, map){ console.log(key, ":", value); })

Метод forEach в качестве параметра получает функцию обратного вызова, которая имеет три параметра:

  • Первый параметр - значение
  • Второй параметр - ключ
  • Третий параметр - перебираемый объект Map

Консольный вывод данного примера:

red : красный
green : зеленый
blue : синий

Также для перебора объекта Map можно использовать циклы, например, цикл for...of:

const dict = new Map([["red", "красный"], ["green", "зеленый"], ["blue", "синий"]]); for(item of dict){ console.log(item[0], ":", item[1]); }

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

Также объект Map имеет два дополнительных метода: keys() позволяет перебрать только ключи и values() позволяет перебирать значения элементов. Оба метода возвращают итераторы, поэтому для перебора ключей и значений по отдельности также можно использовать цикл for...of:

const dict = new Map([["red", "красный"], ["green", "зеленый"], ["blue", "синий"]]); for(item of dict.keys()){ console.log(item); } // Консольный вывод: // red // green // blue for(item of dict.values()){ console.log(item); } // Консольный вывод: // красный // зеленый // синий

📃 Основные методы Map

const map = new Map(); // set(key, value) - добавить/обновить элемент map.set('name', 'Alice'); map.set('age', 25); map.set(1, 'число как ключ'); map.set(true, 'boolean как ключ'); // get(key) - получить значение console.log(map.get('name')); // "Alice" console.log(map.get('age')); // 25 // has(key) - проверка наличия ключа console.log(map.has('name')); // true console.log(map.has('city')); // false // delete(key) - удалить элемент map.delete('age'); console.log(map.has('age')); // false // size - количество элементов console.log(map.size); // 3 // clear() - очистить Map map.clear(); console.log(map.size); // 0

📃 Map с объектами в качестве ключей

const map = new Map(); // Объект как ключ const user1 = {name: 'Alice'}; const user2 = {name: 'Bob'}; map.set(user1, 'администратор'); map.set(user2, 'пользователь'); console.log(map.get(user1)); // "администратор" console.log(map.get(user2)); // "пользователь" // Это работает, потому что ключи — это ссылки на объекты console.log(map.get({name: 'Alice'})); // undefined (новый объект!)
💡 Map vs Object
  • Ключи: в Map — любые типы, в Object — только строки/Symbol
  • Размер: Map имеет свойство size, для Object нужно вычислять
  • Порядок: Map сохраняет порядок добавления элементов
  • Производительность: Map быстрее при частых добавлениях/удалениях

📃 Перебор Map

const map = new Map([ ['name', 'Alice'], ['age', 25], ['city', 'Moscow'] ]); // forEach map.forEach((value, key) => { console.log(`${key}: ${value}`); }); // for...of for (let [key, value] of map) { console.log(`${key}: ${value}`); } // Только ключи for (let key of map.keys()) { console.log(key); } // Только значения for (let value of map.values()) { console.log(value); } // Пары ключ-значение for (let entry of map.entries()) { console.log(entry); // ['name', 'Alice'] }

📃 Цепочка вызовов (Chaining)

const map = new Map(); // set() возвращает сам Map, поэтому можно делать цепочку map.set('name', 'Alice') .set('age', 25) .set('city', 'Moscow'); console.log(map.size); // 3

Преобразование Map в Object и обратно

// Map → Object const map = new Map([ ['name', 'Alice'], ['age', 25] ]); const obj = Object.fromEntries(map); console.log(obj); // {name: 'Alice', age: 25} // Object → Map const user = {name: 'Bob', age: 30}; const map2 = new Map(Object.entries(user)); console.log(map2); // Map(2) {'name' => 'Bob', 'age' => 30}

📜 WeakMap - слабая Map

WeakMap похож на Map, но ключами могут быть только объекты, и они хранятся "слабо" (могут быть удалены сборщиком мусора).

Создание WeakMap:

// пустой WeakMap const weakMap1 = new WeakMap(); // WeakMap с инициализацией данными let key1 = {key:1}; let key2 = {key:2}; let value1 = {name: "Tom"}; let value2 = {name: "Sam"}; const weakMap2 = new WeakMap([[key1, value1], [key2, value2]]); // или так // const weakMap2 = new WeakMap([[{key:1}, {name: "Tom"}], [{key:2}, {name: "Sam"}]]);

Стоит отметить, что объект WeakMap не поддерживает перебор.

Для добавления новых объектов или изменения старых применяется метод set():

let key1 = {key:1}; let key2 = {key:2}; let value1 = {name: "Tom"}; let value2 = {name: "Sam"}; const weakMap2 = new WeakMap([[key1, value1]]); weakMap2.set(key2, value2); weakMap2.set(key1, {name: "Kate"}); console.log(weakMap2.get(key1)); // {name: "Kate"} console.log(weakMap2.get(key2)); // {name: "Sam"}

Для получения объектов по ключу из WeakMap применяется метод get():

let key1 = {key:1}; let key2 = {key:2}; let value1 = {name: "Tom"}; let value2 = {name: "Sam"}; const weakMap2 = new WeakMap([[key1, value1], [key2, value2]]); console.log(weakMap2.get(key1)); // {name: "Tom"}

Чтобы проверить наличие элемента по определенному ключу, применяется метод has(), который возвращает true при наличии элемента:

let key1 = {key:1}, key2 = {key:2}; let value1 = {name: "Tom"}, value2 = {name: "Sam"}; const weakMap2 = new WeakMap([[key1, value1]]); console.log(weakMap2.has(key1)); // true console.log(weakMap2.has(key2)); // false

Для удаления элемента по ключу применяется метод delete():

let key1 = {key:1}, key2 = {key:2}; let value1 = {name: "Tom"}, value2 = {name: "Sam"}; const weakMap2 = new WeakMap([[key1, value1], [key2, value2]]); console.log(weakMap2.has(key1)); // true weakMap2.delete(key1); console.log(weakMap2.has(key1)); // false

Слабые ссылки

Объекты передаются в WeakMap по ссылке. И отличительной особенностью WeakMap является то, что когда объект перестает существовать в силу различных причин, он удаляется из WeakMap. Рассмотрим следующий пример:

let jsCode = {code: "js"}, tsCode = {code: "ts"}; let js = {lang: "JavaScript"}, ts = {lang: "TypeScript"}; const weakMap = new WeakMap([[jsCode, js], [tsCode, ts]]); jsCode = null; console.log(weakMap); // WeakMap {{code: "js"} => {lang: "JavaScript"},// {code: "ts"} => {lang: "TypeScript"}} console.log("Некоторая работа"); const timerId = setTimeout(function(){ console.log(weakMap); // WeakMap {{code: "ts"} => {lang: "TypeScript"}} clearTimeout(timerId); }, 30000);

В данном случае сначала объект WeakMap хранит ссылки на два элемента с ключами jsCode и tsCode. Далее для переменной jsCode устанавливается значение null.

jsCode = null;

Это приведет к тому, что спустя некоторое время начальное значение этой переменной будет удалено сборщиком мусора JavaScript.

Причем если сразу после этого мы посмотрим на содержимое weakMap, то увидим, что объект с ключом jsCode в нем еще присутствует. Однако спустя некоторое время ссылка будет удалена из weakSet. Для эмуляции прошествия времени здесь используется функция setTimeout, которая выводит на консоль содержимое weakSet через 10000 секунд (конкретный период времени, через который сборщик мусора удалит значение, может отличаться)

Теперь сравним с тем, что произойдет, если вместо WeakMap использовать Map:

let jsCode = {code: "js"}, tsCode = {code: "ts"}; let js = {lang: "JavaScript"}, ts = {lang: "TypeScript"}; const map = new Map([[jsCode, js], [tsCode, ts]]); jsCode = null; console.log(map); // Map(2) {{code: "js"} => {lang: "JavaScript"},// {code: "ts"} => {lang: "TypeScript"}} console.log("Некоторая работа"); const timerId = setTimeout(function(){ console.log(map); // Map(2) {{code: "js"} => {lang: "JavaScript"},// {code: "ts"} => {lang: "TypeScript"}} clearTimeout(timerId); }, 30000);

В случае с Map даже спустя некоторое время мы увидим, что в объекте Map до сих пор присутствует объект, для которого было установлено значение null

const weakMap = new WeakMap(); let user = {name: 'Alice'}; // Добавление weakMap.set(user, 'данные о пользователе'); console.log(weakMap.get(user)); // "данные о пользователе" // Когда user больше не нужен user = null; // Объект будет удалён из WeakMap автоматически
⚠️ Ограничения WeakMap
  • Ключи только объекты (не примитивы)
  • Нельзя перебирать (нет keys(), values(), entries())
  • Нет свойства size
  • Нет метода clear()

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

📃 Подсчёт частоты элементов (Map)

const fruits = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple']; const frequency = new Map(); fruits.forEach(fruit => { frequency.set(fruit, (frequency.get(fruit) || 0) + 1); }); console.log(frequency); // Map(3) {'apple' => 3, 'banana' => 2, 'orange' => 1}

📃 Уникальные значения в массиве объектов (Set)

const users = [ {id: 1, name: 'Alice'}, {id: 2, name: 'Bob'}, {id: 1, name: 'Alice'}, // Дубликат {id: 3, name: 'Charlie'} ]; // Получить уникальные ID const uniqueIds = [...new Set(users.map(u => u.id))]; console.log(uniqueIds); // [1, 2, 3]

📃 Кэширование результатов функций (WeakMap)

const cache = new WeakMap(); function process(obj) { // Проверяем кэш if (cache.has(obj)) { console.log('Из кэша'); return cache.get(obj); } // Вычисляем результат const result = obj.value * 2; // Сохраняем в кэш cache.set(obj, result); return result; } const data = {value: 10}; console.log(process(data)); // Вычисляется: 20 console.log(process(data)); // Из кэша: 20

📃 Множественные операции (Set)

const setA = new Set([1, 2, 3, 4]); const setB = new Set([3, 4, 5, 6]); // Объединение (union) const union = new Set([...setA, ...setB]); console.log(union); // Set(6) {1, 2, 3, 4, 5, 6} // Пересечение (intersection) const intersection = new Set([...setA].filter(x => setB.has(x))); console.log(intersection); // Set(2) {3, 4} // Разность (difference) const difference = new Set([...setA].filter(x => !setB.has(x))); console.log(difference); // Set(2) {1, 2}

Глава 11. DOM (Document Object Model)

📜 Что такое DOM?

DOM (Document Object Model) — это программный интерфейс для HTML и XML документов. Он представляет структуру документа в виде дерева объектов, которыми можно манипулировать с помощью JavaScript.

💡 Структура DOM

Каждый HTML-элемент в DOM представлен объектом (узлом). Документ имеет иерархическую структуру: родители, потомки, соседние элементы.

  • getElementById(value): выбирает элемент, у которого атрибут id равен value

    . Если элемента с таким идентификатором нет, то возвращается null
  • getElementsByTagName(value): выбирает все элементы, у которых тег равен value. Возвращает список элементов (список типа NodeList), который аналогичен массиву.

  • getElementsByClassName(value): выбирает все элементы, которые имеют класс value. Возвращает список NodeList

  • getElementsByName(value): выбирает все элементы, которые называются value. Возвращает список NodeList

  • querySelector(value): выбирает первый элемент, который соответствует css-селектору value

  • querySelectorAll(value): выбирает все элементы, которые соответствуют css-селектору value. Возвращает список NodeList

getElementById

// Находит элемент по ID (возвращает один элемент или null) const header = document.getElementById('header'); console.log(header); // <div id="header">...</div>

getElementsByClassName

// Находит все элементы с классом (возвращает HTMLCollection) const items = document.getElementsByClassName('item'); console.log(items.length); // Количество элементов console.log(items[0]); // Первый элемент // HTMLCollection - живая коллекция (обновляется автоматически) // Нельзя использовать методы массивов напрямую

getElementsByTagName

// Находит все элементы по имени тега const divs = document.getElementsByTagName('div'); const allElements = document.getElementsByTagName('*'); // Все элементы

querySelector (рекомендуется)

// Находит ПЕРВЫЙ элемент по CSS-селектору const header = document.querySelector('#header'); const firstItem = document.querySelector('.item'); const firstButton = document.querySelector('button'); const complexSelector = document.querySelector('div.container > p.text');

querySelectorAll (рекомендуется)

// Находит ВСЕ элементы по CSS-селектору (возвращает NodeList) const items = document.querySelectorAll('.item'); // NodeList можно перебирать forEach items.forEach(item => { console.log(item); }); // Преобразование в массив const itemsArray = [...items]; // или const itemsArray2 = Array.from(items);

Поиск во вложенных элементах

Подобным образом мы можем искать элементы не только во всем документе, но и в отдельных элементах на веб-странице. Например:

<body> <div id="article"> <h1 id="header">Home Page</h1> <p class="text">Page Text 1</p> <p class="text">Page Text 2</p> </div> <div id="footer"> <p class="text">Footer Text</p> </div> <script> // получаем элемент с id="article" const article = document.getElementById("article"); // в этом элементе получаем все элементы с class="text" const articleContent = article.getElementsByClassName("text"); for(p of articleContent){ console.log(p); } </script> </body>

В данном случае мы сначала получаем элемент с id="article", затем внутри этого элемента ищем все элементы с class="text". В результате консоль выведет два элемента:

<p class="text">Page Text 1</p> <p class="text">Page Text 2</p>

📃 Селекторы CSS

Список базовых CSS-селекторов, которые мы можем применять для поиска элементов:

  • *: выбирает все элементы

  • E: выбирает все элементы типа E

  • [a]: выбирает все элементы с атрибутом a

  • [a="b"]: выбирает все элементы, в которых атрибут a имеет значение b

  • [a~="b"]: выбирает все элементы, в которых атрибут a имеет список значений, и одно из этих значений равно b

  • [a^="b"]: выбирает все элементы, в которых значение атрибута a начинается на b

  • [a$="b"]: выбирает все элементы, в которых значение атрибута a завершается на b

  • [a*="b"]: выбирает все элементы, в которых значение атрибута a содержит подстроку b

  • [a|="b"]: выбирает все элементы, в которых значение атрибута a представляет ряд значений, разделенных дефисами, и первое из этих значений равно b

  • :root: выбирает корневой элемент документа

  • :nth-child(n): выбирает n-ый вложенный элемент (отсчет идет с начала)

  • :nth-last-child(n): выбирает n-ый вложенный элемент (отсчет идет с конца)

  • :nth-of-type(n): выбирает n-ый сестринский элемент типа type (отсчет идет с начала)

  • :nth-last-of-type(n): выбирает n-ый сестринский элемент типа type (отсчет идет с конца)

  • :first-child: выбирает первый вложенный элемент

  • :last-child: выбирает последний вложенный элемент

  • :first-of-type: выбирает первый сестринский элемент типа type

  • :last-of-type: выбирает последний сестринский элемент типа type

  • :only-child: выбирает все элементы, которые имеют только один вложенный элемент

  • :only-of-type: выбирает все сестринские элементы типа type

  • :empty: выбирает все элементы, которые не имеют вложенных элементов

  • :link: выбирает все ссылки, которые еще не были нажаты

  • :visited: выбирает все ссылки, которые уже были нажаты

  • :active: выбирает все ссылки, которые в текущий момент активны (нажимаются)

  • :hover: выбирает все ссылки, над которыми в текущий момент находится курсор

  • :focus: выбирает все элементы, которые в текущий момент получили фокус

  • :target: выбирает все элементы, к которым можно обратиться с помощью адресов url внутри страницы

  • :lang(en): выбирает все элементы, в которых атрибут lang имеет значение "en"

  • :enabled: выбирает все элементы форм, которые доступны для взаимодействия

  • :disabled: выбирает все элементы форм, которые НЕ доступны для взаимодействия

  • :checked: выбирает все флажки (чекбоксы) и радиокнопки, которые отмечены

  • .class: выбирает все элементы с классом class

  • #id: выбирает все элементы с идентификтором id

  • :not(s): выбирает все элементы, которые не соответствуют селектору s

  • E F: выбирает все элементы типа F, которые встречаются в элементах типа E

  • E > F: выбирает все элементы типа F, которые являются вложенными в элементы типа E

  • E + F: выбирает все элементы типа F, которые располагаются сразу после элементов типа E

  • E ~ F: ввыбирает все элементы типа F, которые являются сестринскими по отношению к элементам типа E

✅ querySelector vs getElementById
  • querySelector — универсальный, поддерживает любые CSS-селекторы
  • getElementById — быстрее, но только для ID
  • Для современной разработки рекомендуется querySelector/querySelectorAll
✅ Дополнительные замечания

Стоит отметить, что из всех этих способов выбор по id обычно самый быстрый. При всех прочих условиях лучше выбирать метод getElementById()

Также для оптимизации работы с DOM для того, чтобы избежать повторной выборки одних и тех же элементов, при первой выборке их лучше сохранять в константы/переменные.

Ряд методов - getElementsByTagName(), getElementsByClassName(), getElementsByName(), querySelectorAll() возвращает список элементов в виде объекта NodeList, который аналогичен массиву и который мы можем перебрать и получить каждый отдельный элемент из этого набора. Однако метод querySelectorAll() возвращает статический список NodeList, тогда как остальные методы возвращают нестатический список. В чем разница? При изменении элементов нестатического списка все изменения сразу же применяются к веб-странице. При изменении элементов из статического списка изменения могут не сразу изменяться.

📜 Изменение содержимого

textContent

const div = document.querySelector('#content'); // Получить текст console.log(div.textContent); // Весь текст внутри элемента // Установить текст (HTML-теги экранируются) div.textContent = 'Новый текст'; div.textContent = '<b>Жирный</b>'; // Отобразится как текст, не как HTML

innerHTML

const div = document.querySelector('#content'); // Получить HTML console.log(div.innerHTML); // HTML-код внутри элемента // Установить HTML div.innerHTML = '<p>Новый <b>HTML</b></p>'; // Добавить к существующему div.innerHTML += '<p>Ещё один параграф</p>';
⚠️ Осторожно с innerHTML

Использование innerHTML с пользовательскими данными может привести к XSS-атакам. Используйте textContent для обычного текста.

outerHTML

const div = document.querySelector('#content'); // Получить HTML вместе с самим элементом console.log(div.outerHTML); // <div id="content">...</div> // Заменить элемент полностью div.outerHTML = '<section>Новый элемент</section>';

📜 Работа с атрибутами

getAttribute / setAttribute

const link = document.querySelector('a'); // Получить атрибут const href = link.getAttribute('href'); const target = link.getAttribute('target'); // Установить атрибут link.setAttribute('href', 'https://example.com'); link.setAttribute('target', '_blank'); // Проверка наличия атрибута if (link.hasAttribute('target')) { console.log('Атрибут target существует'); } // Удалить атрибут link.removeAttribute('target');

Прямой доступ к атрибутам

const input = document.querySelector('input'); // Стандартные атрибуты доступны как свойства input.value = 'Новое значение'; input.type = 'password'; input.placeholder = 'Введите пароль'; const img = document.querySelector('img'); img.src = 'new-image.jpg'; img.alt = 'Описание изображения';

data-атрибуты

// HTML: <div data-user-id="123" data-role="admin"></div> const div = document.querySelector('div'); // Доступ через dataset console.log(div.dataset.userId); // "123" console.log(div.dataset.role); // "admin" // Установка div.dataset.status = 'active'; // HTML станет: data-status="active"

📜 Работа с классами

className

const div = document.querySelector('div'); // Получить классы (строка) console.log(div.className); // "box active large" // Установить классы (перезаписывает все) div.className = 'container'; // Добавить класс (нужно проверять существующие) div.className += ' active';

classList (рекомендуется)

const div = document.querySelector('div'); // Добавить класс div.classList.add('active'); div.classList.add('highlight', 'important'); // Несколько классов // Удалить класс div.classList.remove('active'); // Переключить класс (toggle) div.classList.toggle('hidden'); // Добавит, если нет; удалит, если есть // Проверить наличие класса if (div.classList.contains('active')) { console.log('Элемент активен'); } // Заменить класс div.classList.replace('old-class', 'new-class');

📜 Работа со стилями

Инлайн-стили (style)

const div = document.querySelector('div'); // Установить стиль div.style.color = 'red'; div.style.backgroundColor = 'blue'; // camelCase! div.style.fontSize = '20px'; // Множественные стили div.style.cssText = 'color: red; background: blue; font-size: 20px;'; // Удалить стиль div.style.color = ''; // Получить стиль (только инлайновые!) console.log(div.style.color); // "red"

Вычисленные стили (getComputedStyle)

const div = document.querySelector('div'); // Получить все вычисленные стили (включая CSS) const styles = getComputedStyle(div); console.log(styles.color); // Вычисленный цвет console.log(styles.backgroundColor); // Вычисленный фон console.log(styles.width); // Вычисленная ширина // Псевдоэлементы const beforeStyles = getComputedStyle(div, '::before'); console.log(beforeStyles.content);

📜 Объект Node. Навигация по DOM

Каждый отдельный узел, будь то html-элемент, его атрибут или текст, в структуре DOM представлен объектом Node. Может возникнуть вопрос: как связаны элементы веб-страницы и узлы веб-страницы? И тут надо отметить, что любой элемент веб-страницы является узлом, но не любой узел является элементом (например, атрибуты и текст элементов также являются отдельными узлами).

Объект Node предоставляет ряд свойств, с помощью которых мы можем получить информацию о данном узле:

  • childNodes: содержит коллекцию дочерних узлов

  • children: содержит коллекцию дочерних узлов, которые являются элементами

  • firstChild: возвращает первый дочерний узел текущего узла

  • firstElementChild: возвращает первый дочерний узел, который является элементом

  • lastChild: возвращает последний дочерний узел текущего узла

  • lastElementChild: возвращает последний дочерний узел, который является элементом

  • previousSibling: возвращает предыдущий узел, который находится на одном уровне с текущим

  • nextSibling: возвращает следующий узел, который находится на одном уровне с текущим

  • previousElementSibling: возвращает предыдущий узел, который является элементом и который находится на одном уровне с текущим

  • nextElementSibling: возвращает следующий узел, который является элементом и который находится на одном уровне с текущим

  • ownerDocument: возвращает корневой узел документа

  • parentNode: возвращает родительский узел для текущего узла

  • parentElement: возвращает родительский узел, который является элементом

  • nodeName: возвращает имя узла

  • nodeType: возвращает тип узла в виде числа

  • nodeValue: возвращает текст текстового узла

Прежде всего мы можем использовать свойства nodeName и nodeType, чтобы узнать тип узла:

<body> <div id="article"> <h1 id="header">Home Page</h1> <p>Page Text</p> </div> <script> const article = document.getElementById("article"); console.log(article.nodeName); // DIV console.log(article.nodeType); // 1 </script> </body>

Здесь получаем информацию по элементу с id="header". В частности, свойство nodeName возвратит имя тега элемента - div, а свойство nodeType число 1. Каждому типу узлов соответствует определенное число:

nodeType Тип узла

1

элемент

2

атрибут

3

текст

Родители и потомки

Для получения родительского элемента применяются свойства parentNode и parentElement. Например:

<body> <div id="article"> <h1 id="header">Home Page</h1> <p>Page Text</p> </div> <script> // выбираем все элемент c id="header" const header = document.getElementById("header"); // получаем родительский элемента const headerParent = header?.parentElement; // можно так // const headerParent = header?.parentNode; console.log(headerParent); // выводим родительский элемент на консоль </script> </body>

Здесь выводим на консоль элемент, в который помещен элемент с id="header".

Стоит отметить, что хотя оба метода в принципе возвращают один и тот же элемент, однако есть исключение - элемент <html>. Для него родительским узлом будет объект document, а вот родительского элемента у него не будет (будет значение null):

const htmlEl = document.getElementsByTagName("html")[0]; const parentElem = htmlEl.parentElement; const parentNode = htmlEl.parentNode; console.log(parentElem); // null console.log(parentNode); // объект document
const element = document.querySelector('.child'); // Родитель const parent = element.parentElement; const parentNode = element.parentNode; // То же самое (почти) // Дети const children = element.children; // HTMLCollection (только элементы) const firstChild = element.firstElementChild; const lastChild = element.lastElementChild; // Количество детей console.log(element.childElementCount); // Все узлы (включая текстовые) const childNodes = element.childNodes; // NodeList (всё)

Получение дочерних элементов

Метод hasChildNodes() возвращает true, если элемент содержит вложенные узлы:

const article = document.querySelector("div"); if(article.hasChildNodes()){ console.log("There are child nodes"); } else{ console.log("No child nodes"); }

Для получения дочерних элементов можно использовать свойство children:

<body> <div id="article"> <h1 id="header">Home Page</h1> <p>Page Text</p> </div> <script> // выбираем элемент c id="article" const article = document.getElementById("article"); for(elem of article.children){ console.log(elem); } </script> </body>

Здесь получаем элемент с id="article" и в цикле проходим по всем его дочерним элементам. А это два элемента:

<h1 id="header">Home Page</h1> <p>Page Text</p>

Если же нам надо выбрать вообще все дочерние узлы (не только элементы, но и атрибуты и текст), то применяется метод childNodes:

<body> <div id="article"> <h1 id="header">Home Page</h1> <p>Page Text</p> </div> <script> // выбираем элемент c id="article" const article = document.getElementById("article"); for(node of article.childNodes){ let type = ""; if(node.nodeType===1) type="элемент"; else if(node.nodeType===2) type="атрибут"; else if(node.nodeType===3) type="текст"; console.log(node.nodeName, ": ", type); } </script> </body>

Здесь мы выбираем тот же элемент, но теперь перебираем его узлы. выбираем элемент div с классом article и пробегаемся по его дочерним узлам. И в цикле выводим имя узла и его тип с помощью свойств nodeName и nodeType.

И несмотря на то, что в блоке div#article только два элемента: заголовок h1 и параграф, консоль отобразит нам пять узлов.

#text : текст H1 : элемент #text : текст P : элемент #text : текст

Дело в том, что пробелы между узлами также считаются за отдельные текстовые узлы. Если бы пробелов не было:

<div id="article"><h1 id="header">Home Page</h1><p>Page Text</p></div>

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

Соседи

const element = document.querySelector('.item'); // Следующий элемент const next = element.nextElementSibling; // Предыдущий элемент const prev = element.previousElementSibling;

Поиск ближайшего родителя

const button = document.querySelector('button'); // Найти ближайший родительский элемент по селектору const form = button.closest('form'); const container = button.closest('.container');

📜 Элементы

Для работы с элементами на веб-странице мы можем использовать как функциональность типа Node, который представляет любой узел веб-страницы, так и функциональность типа HTMLElement, который собственно представляет элемент. То есть объекты HTMLElement - это фактически те же самые узлы - объекты Node, у которых тип узла (свойство nodeType) равно 1.

Каждый элемент веб-страницы соответствует определенному типу в JavaScript. Но все эти типы являются пол типами типа HTMLElement, который определяет базовую функциональность элементов.
Актуальные типы элементов:

  • <a>: тип HTMLAnchorElement
  • <abbr>: тип HTMLElement
  • <address>: тип HTMLElement
  • <area>: тип HTMLAreaElement
  • <audio>: тип HTMLAudioElement
  • <b>: тип HTMLElement
  • <base>: тип HTMLBaseElement
  • <bdo>: тип HTMLElement
  • <blockquote>: тип HTMLQuoteElement
  • <body>: тип HTMLBodyElement
  • <br>: тип HTMLBRElement
  • <button>: тип HTMLButtonElement
  • <caption>: тип HTMLTableCaptionElement
  • <canvas>: тип HTMLCanvasElement
  • <cite>: тип HTMLElement
  • <code>: тип HTMLElement
  • <col>, <colgroup>: тип HTMLTableColElement
  • <data>: тип HTMLDataElement
  • <datalist>: тип HTMLDataListElement
  • <dd>: тип HTMLElement
  • <del>: тип HTMLModElement
  • <dfn>: тип HTMLElement
  • <div>: тип HTMLDivElement
  • <dl>: тип HTMLDListElement
  • <dt>: тип HTMLElement
  • <em>: тип HTMLElement
  • <embed>: тип HTMLEmbedElement
  • <fieldset>: тип HTMLFieldSetElement
  • <form>: тип HTMLFormElement
  • <h1>, <h2>, <h3>, <h4>, <h5>, <h6>: тип HTMLHeadingElement
  • <head>: тип HTMLHeadElement
  • <hr>: тип HTMLHRElement
  • <html>: тип HTMLHtmlElement
  • <i>: тип HTMLElement
  • <iframe>: тип HTMLIFrameElement
  • <img>: тип HTMLImageElement
  • <input>: тип HTMLInputElement
  • <ins>: тип HTMLModElement
  • <kbd>: тип HTMLElement
  • <keygen>: тип HTMLKeygenElement
  • <label>: тип HTMLLabelElement
  • <legend>: тип HTMLLegendElement
  • <li>: тип HTMLLIElement
  • <link>: тип HTMLLinkElement
  • <map>: тип HTMLMapElement
  • <media>: тип HTMLMediaElement
  • <meta>: тип HTMLMetaElement
  • <meter>: тип HTMLMeterElement
  • <noscript>: тип HTMLElement
  • <object>: тип HTMLObjectElement
  • <ol>: тип HTMLOListElement
  • <optgroup>: тип HTMLOptGroupElement
  • <option>: тип HTMLOptionElement
  • <output>: тип HTMLOutputElement
  • <p>: тип HTMLParagraphElement
  • <param>: тип HTMLParamElement
  • <pre>: тип HTMLPreElement
  • <progress>: тип HTMLProgressElement
  • <q>: тип HTMLQuoteElement
  • <s>: тип HTMLElement
  • <samp>: тип HTMLElement
  • <script>: тип HTMLScriptElement
  • <select>: тип HTMLSelectElement
  • <small>: тип HTMLElement
  • <source>: тип HTMLSourceElement
  • <span>: тип HTMLSpanElement
  • <strong>: тип HTMLElement
  • <style>: тип HTMLStyleElement
  • <sub>: тип HTMLElement
  • <sup>: тип HTMLElement
  • <table>: тип HTMLTableElement
  • <tbody>: тип HTMLTableSectionElement
  • <td>: тип HTMLTableCellElement
  • <textarea>: тип HTMLTextAreaElement
  • <tfoot>: тип HTMLTableSectionElement
  • <th>: тип HTMLTableHeaderCellElement
  • <thead>: тип HTMLTableSectionElement
  • <time>: тип HTMLTimeElement
  • <title>: тип HTMLTitleElement
  • <tr>: тип HTMLTableRowElement
  • <track>: тип HTMLTrackElement
  • <ul>: тип HTMLUListElement
  • <var>: тип HTMLElement / HTMLUnknownElement
  • <video>: тип HTMLVideoElement

Мы можем получить конкретный тип элемента с помощью метода Object.getPrototypeOf():

<body> <h1 id="header">Home Page</h1> <script> const header = document.getElementById("header"); console.log(Object.getPrototypeOf(header)); // HTMLHeadingElement </script> </body>

Свойства элементов

Тип Element предоставляет ряд свойств, которые хранят информацию об элементе:

  • tagName: возвращает тег элемента
  • textContent: представляет текстовое содержимое элемента
  • innerText: представляет текстовое содержимое элемента (аналогично textContent)
  • innerHTML: представляет html-код элемента

Одним из ключевых свойств объекта Element является свойство tagName, которое возвращает тег элемента:

<body> <h1 id="header">Home Page</h1> <script> const header = document.getElementById("header"); console.log(header.tagName); // H1 </script> </body>

Управление текстом элемента

Свойство textContent позволяет получить или изменить текстовое содержимое элемента:

<body> <h1 id="header">Home Page</h1> <script> const header = document.getElementById("header"); // получаем текст элемента console.log(header.textContent); // Home Page // изменяем текст элемента header.textContent = "Hello World"; </script> </body>

Аналогично можно использовать другое свойство для управление текстом - innerText:

<body> <h1 id="header">Home Page</h1> <script> const header = document.getElementById("header"); // получаем текст элемента console.log(header.innerText); // Home Page // изменяем текст элемента header.innerText = "Hello World2"; </script> </body>

Тем не менее между textContent и innerText есть некоторые различия:

  • textContent получает содержимое всех элементов, включая <script> и <style, тогда как innerText этого не делает
  • innerText умеет считывать стили и не возвращает содержимое скрытых элементов, тогда как textContent этого не делает.
  • innerText позволяет получить CSS, а textContent — нет.

Управление кодом HTML

Ни textContent, ни innerText не позволяют ни получить, ни изменить код html элемента. Например:

header.innerText = "<span style='color:navy;'>Hello World</span>";

Это изменит только текст, но не html код. Для управления html применяется свойство innerHTML:

<body> <h1 id="header">Home Page</h1> <script> const header = document.getElementById("header"); // получаем html-код элемента console.log(header.innerHTML); // Home Page // изменяем html-код элемента header.innerHTML = "<span style='color:navy;'>Hello World</span>"; </script> </body>

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

JavaScript предоставляет ряд методов для управления элементами на веб-страницы. В частности, мы можем создавать и добавлять новые элементы или заменять и удалять уже имеющиеся. Рассмотрим эти методы.

Создание элементов (createElement)

Для создания элементов объект document имеет следующие методы:

  • createElement(elementName): создает элемент html, тег которого передается в качестве параметра. Возвращает созданный элемент
  • createTextNode(text): создает и возвращает текстовый узел. В качестве параметра передается текст узла.

Создадим элемент с помощью createElement:

const header = document.createElement("h1"); // создаем заголовок <h1> console.log(header); // <h1></h1>

Таким образом, переменная header будет хранить ссылку на элемент h1.

Создадим текстовый узел с помощью createTextNode:

const headerText = document.createTextNode("Hello World"); // создаем текстовый узел console.log( headerText); // "Hello World"
// Создать новый элемент const div = document.createElement('div'); div.textContent = 'Новый блок'; div.className = 'box'; const p = document.createElement('p'); p.innerHTML = 'Новый <b>параграф</b>';

Добавление элементов

Однако одного создания элементов недостаточно, их еще надо добавить на веб-страницу.

Для добавления элементов мы можем использовать один из методов объекта Node:

  • appendChild(newNode): добавляет новый узел newNode в конец коллекции дочерних узлов
  • insertBefore(newNode, referenceNode): добавляет новый узел newNode перед узлом referenceNode

appendChild

Используем метод appendChild():

const header = document.createElement("h1"); // создаем заголовок <h1> const headerText = document.createTextNode("Hello World"); // создаем текстовый узел header.appendChild( headerText); // добавляем в элемент h1 текстовый узел console.log(header); // <h1>Hello World</h1>

И чтобы добавить созданный элемент на страницу, его надо добавить в уже имеющийся на странице элемент:

<body> <script> constheader = document.createElement("h1"); // создаем заголовок <h1> const headerText = document.createTextNode("Hello World");// создаем текстовый узел header.appendChild(headerText); // добавляем в элемент h1 текстовый узел document.body.appendChild(header);// добавляем элемент h1 на страницу в элемент body </script> </body>

Сначала создаем обычный элемент заголовка h2 и текстовый узел. Затем текстовый узел добавляем в элемент заголовка. Затем заголовок добавляем в элемент body:

Стоит отметить, что нам необязательно для определения текста внутри элемента создавать дополнительный текстовый узел, так как мы можем воспользоваться свойством textContent и напрямую ему присвоить текст:

const header = document.createElement("h1");// создаем заголовок <h1> header.textContent = "Hello World";// определяем текст элемента

В этом случае текстовый узел будет создан неявно при установке текста.

insertBefore

Метод appendChild() добавляет элемент в конец контейнера. Чтобы более конкретизировать место для добавления, можно использовать другой метод - insertBefore(), который добавляет один элемент перед другим элементом. Например, у нас есть следующая страница:

<body> <p>Text 1</p> <p>Text 2</p> </body>

Допустим, нам надо добавить в элемент body перед первым параграфом заголовок. Мы можем сделать это так:

<body> <p>Text 1</p> <p>Text 2</p> <script> const header = document.createElement("h1"); // создаем заголовок <h1> header.textContent = "Page Header"; // определяем текст элемента // получаем первый параграф const firstP = document.body.firstElementChild; // добавляем элемент h1 перед параграфом firstP document.body.insertBefore(header, firstP); </script> </body>

Если нам надо вставить новый узел на второе, третье или любое другое место, то нам надо найти узел, перед которым надо вставлять, с помощью комбинаций свойств firstElementChild/lastElementChild и nextSibling/previousSibling.

insertAdjacentHTML

const div = document.querySelector('div'); // beforebegin - перед элементом div.insertAdjacentHTML('beforebegin', '<p>До элемента</p>'); // afterbegin - в начало элемента div.insertAdjacentHTML('afterbegin', '<p>В начало</p>'); // beforeend - в конец элемента div.insertAdjacentHTML('beforeend', '<p>В конец</p>'); // afterend - после элемента div.insertAdjacentHTML('afterend', '<p>После элемента</p>');

📜 Копирование элемента

Иногда элементы бывают довольно сложными по составу, и гораздо проще их скопировать, чем с помощью отдельных вызовов создавать их содержимое. Для копирования уже имеющихся узлов у объекта Node можно использовать метод cloneNode():

<body> <div id="article"> <h1>Home Page</h1> <p>Text 1</p> <p>Text 2</p> </div> <script> const article = document.getElementById("article"); // получаем последний параграф const lastP = article.lastElementChild; // клонируем элемент lastP const newLastP = lastP.cloneNode(true); // изменяем текст newLastP.textContent = "Publication Date: 28/10/2023"; // добавляем в конец элемента article article.appendChild(newLastP); </script> </body>

В метод cloneNode() в качестве параметра передается логическое значение: если передается true, то элемент будет копироваться со всеми дочерними узлами; если передается false - то копируется без дочерних узлов. То есть в данном случае мы копируем узел со всем его содержимым и потом добавляем в конец элемента c id="article".

📜 Замена элемента

Для замены элемента применяется метод replaceChild(newNode, oldNode) объекта Node. Этот метод в качестве первого параметра принимает новый элемент, который заменяет старый элемент oldNode, передаваемый в качестве второго параметра.

<body> <div id="article"> <p>Home Page</p> <p>Text 1</p> <p>Text 2</p> </div> <script> const article = document.getElementById("article"); // находим узел, который будем заменять // пусть это будет первый элемент const oldNode = article.firstElementChild; // создаем новый элемент const newNode = document.createElement("h2"); // определяем для него текст newNode.textContent = "Hello World"; // заменяем старый узел новым article.replaceChild(newNode, oldNode); </script> </body>

В данном случае заменяем первый элемент - первый параграф заголовком h2:

Удаление элемента

Для удаления элемента вызывается метод removeChild() объекта Node. Этот метод удаляет один из дочерних узлов:

<body> <div id="article"> <h1>Home Page</h1> <p>Text 1</p> <p>Text 2</p> </div> <script> const article = document.getElementById("article"); // находим узел, который будем удалять - последний параграф const lastP = article.lastElementChild; // удаляем узел article.removeChild(lastP); </script> </body>

В данном случае удаляется первый параграф из блока div

Удаление всех элементов

Иногда возникает необходимость удалить все элементы. Для этого перебираем все элементы контейнера и удаляем их:

<div id="article"> <h1>Home Page</h1> <p>Text 1</p> <p>Text 2</p> </div> <script> const article = document.getElementById("article"); while(article.firstChild){ article.removeChild(article.firstChild); } </script>
const container = document.querySelector('.container'); const newDiv = document.createElement('div'); // appendChild - добавить в конец container.appendChild(newDiv); // append - добавить в конец (может принимать несколько элементов и строки) container.append(newDiv, 'Текст', anotherDiv); // prepend - добавить в начало container.prepend(newDiv); // before - вставить перед элементом container.before(newDiv); // after - вставить после элемента container.after(newDiv); // insertBefore - вставить перед определённым потомком container.insertBefore(newDiv, container.firstChild);

📜 Удаление элементов

const element = document.querySelector('.item'); // remove() - удалить элемент element.remove(); // removeChild() - удалить потомка const parent = document.querySelector('.container'); const child = parent.querySelector('.item'); parent.removeChild(child); // Очистить содержимое parent.innerHTML = ''; // или while (parent.firstChild) { parent.removeChild(parent.firstChild); }

📜 Клонирование элементов

const original = document.querySelector('.item'); // Поверхностное клонирование (без потомков) const clone1 = original.cloneNode(false); // Глубокое клонирование (с потомками) const clone2 = original.cloneNode(true); // Вставить клон document.body.appendChild(clone2);

📜 Размеры и позиция элемента

const div = document.querySelector('div'); // Размеры (включая padding, но без border) console.log(div.clientWidth); console.log(div.clientHeight); // Размеры (включая padding и border) console.log(div.offsetWidth); console.log(div.offsetHeight); // Позиция относительно offsetParent console.log(div.offsetLeft); console.log(div.offsetTop); // Прокрутка console.log(div.scrollTop); // Сколько прокручено сверху console.log(div.scrollLeft); // Сколько прокручено слева console.log(div.scrollHeight); // Полная высота с прокруткой console.log(div.scrollWidth); // Полная ширина с прокруткой // Координаты относительно окна const rect = div.getBoundingClientRect(); console.log(rect.top); // Расстояние от верха окна console.log(rect.left); // Расстояние от левого края окна console.log(rect.width); // Ширина console.log(rect.height); // Высота

📜 Управление атрибутами элементов

Для управления атрибутами элементов JavaScript предоставляет ряд методов:

  • getAttribute(attr): возвращает значение атрибута attr
  • createAttribute(attr): создает атрибут attr
  • setAttribute(attr, value): устанавливает для атрибута attr значение value. Если атрибута нет, то он добавляется
  • removeAttribute(attr): удаляет атрибут attr и его значение

Получение атрибута

Для получения атрибута у элемента вызывается метод getAttribute(), в который передается имя атрибута. Например, пусть у нас на странице есть следующий элемент, который представляет ссылку:

<a id="home" class="link" href="index.html">Home</a>

Получим атрибуты этого элемента:

// получаем элемент const element = document.getElementById("home"); // получаем атрибуты элемента console.log(element.getAttribute("id")); // home console.log(element.getAttribute("class")); // link console.log(element.getAttribute("href")); // index.html

Стоит отметить, что атрибуты элементы также доступны через его свойства, которые называются аналогично атрибутам (за редким исключением):

// получаем элемент const element = document.getElementById("home"); // получаем атрибуты элемента console.log(element.id); // home console.log(element.className); // link console.log(element.href); // file:///Users/eugene/Documents/app/index.html

Исключение касается в частности атрибута "class", который доступен через свойство className.

Также свойства могут возвращать немного отличающиеся значения. Например, свойство href возвращает полную ссылку, а метод getAttribute("href") - непосредственное значение атрибута.

То же самое касается и атрибута style:

<a id="home" style="color:red;" href="index.html">Home</a> <script> // получаем элемент const element = document.getElementById("home"); // получаем атрибуты элемента console.log(element.style); // CSSStyleDeclaration console.log(element.getAttribute("style")); // color:red; </script>

Метод getAttribute("style") возвращает стиль в виде текста, а свойство style - объект CSSStyleDeclaration, с помощью свойств которого можно получить отдельные аспекты стиля.

Установка атрибутов

Для установки значения атрибутов применяется метод setAttribute(attr, value), первый параметр которого - устанавливаемый атрибут, а второй - его значение:

<a id="home" href="index.html">Home</a> <script> // получаем элемент const element = document.getElementById("home"); // устанавливаем атрибут href element.setAttribute("href", "https://metanit.com"); // устанавливаем атрибут style element.setAttribute("style", "color:navy;"); </script>

Здесь изменяем атрибут "href" и устанавливаем атрибут "style". Поскольку атрибут "style" изначально отсутствует, то он будет добавлен. Но стоит отметить, что в реальности это приведет к тому, что будет создан узел Node, который представляет атрибут. У этого узла будет установлено соответствующее значение, и затем узел атрибута добавляется в коллекцию дочерних узлов элемента. То есть фактически это будет выглядеть следующим образом:

<a id="home" href="https://metanit.com">Home</a> <script> // получаем элемент const element = document.getElementById("home"); // создаем узел-атрибут style const attribute = document.createAttribute("style"); // устанавливаем значение узла-атрибута attribute.value = "color:navy;"; // устанавливаем узел атрибута element.setAttributeNode(attribute); </script>

Удаление атрибута

Для удаления атрибута применяется метод removeAttribute(), в который передается удаляемый атрибут:

<a id="home" href="https://metanit.com" style="color:navy;">Home</a> <script> // получаем элемент const element = document.getElementById("home"); // удаляем атрибут style element.removeAttribute("style"); </script>

📜 Изменение стиля элементов

Для работы со стилевыми свойствами элементов в JavaScript применяются, главным образом, два подхода:

  • Изменение свойства style
  • Изменение значения атрибута class

Свойство style

Свойство style представляет сложный объект CSSStyleDeclaration и напрямую сопоставляется с атрибутом style html-элемента. Этот объект содержит набор свойств CSS, к которым можно обратиться следующим образом:

element.style.свойствоCSS

Например, установим цвет шрифта заголовка:

<body> <h1 id="header">Home Page</h1> <script> const header = document.getElementById("header"); // получаем значение свойства color console.log(header.style.color); // пустая строка // изменяем значение свойства color header.style.color = "navy"; // повторно получаем значение свойства color console.log(header.style.color); // navy </script> </body>

Здесь для заголовка в качестве цвета устанавливаем синий цвет navy. В данном случае название свойства color совпадает со свойством css. Аналогично мы могли бы установить цвет с помощью css:

#header{ color:navy; }

Однако ряд свойств css в названиях имеют дефис, например, font-family. В JavaScript для этих свойств дефис не употребляется. Только первая буква, которая идет после дефиса, переводится в верхний регистр:

const header = document.getElementById("header"); header.style.fontFamily = "Verdana";

Свойство className

С помощью свойства className можно получить или установить значение атрибута class элемента html. Например:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>JS Guide</title> <style> .header-color {color:navy;} .header-font {font-family: Verdana;} </style> </head> <body> <h1 id="header" class="header-font">Home Page</h1> <script> const header = document.getElementById("header"); // получаем текущий класс console.log(header.className); // header-font // устанавливаем класс элемента header.className = "header-color"; // получаем текущий класс console.log(header.className); // header-color </script> </body> </html>

Здесь получаем текущий класс заголовка и затем изменяем его на новый класс - "header-color". Благодаря использованию классов не придется настраивать каждое отдельное свойство css с помощью свойства style.

Но при этом надо учитывать, что прежнее значение атрибута class удаляется. Поэтому, если нам надо добавить класс, надо объединить его название со старым классом:

header.className = header.className + " header-color";

И если надо вовсе удалить все классы, то можно присвоить свойству пустую строку:

header.className = "";

Свойство classList

Выше было рассмотрено, как добавлять классы к элементу, однако для управления множеством классов гораздо удобнее использовать свойство classList. Это свойство представляет объект, реализующий следующие методы:

  • add(className): добавляет класс className
  • remove(className): удаляет класс className
  • toggle(className): переключает у элемента класс на className. Если класса нет, то он добавляется, если есть, то удаляется

Например:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>JS Guide</title> <style> .header-color {color:navy;} .header-font {font-family: Verdana;} .header-size {font-size: 22px;} </style> </head> <body> <h1 id="header" class="header-size">Home Page</h1> <script> const header = document.getElementById("header"); header.classList.remove("header-size"); // удаляем класс header-size header.classList.add("header-font"); // добавляем класс header-font header.classList.toggle("header-color"); // переключаем класс header-color </script> </body> </html>

Стоит отметить, что метод toggle() дополнительно может принимать условие в качестве второго параметра - если это условие верно (возвращает true), то класс переключается:

const i = 5; const condition = i > 0; // условие const header = document.getElementById("header"); header.classList.toggle("header-color", condition); // переключаем класс header-color по условию

При необходимости мы можем перебрать все классы из списка classList или получить отдельные классы по индексу:

// перебор списка классов for(headerClass of header.classList){ console.log(headerClass); } console.log(header.classList[0]); // первый установленный класс

📜 Создание своего элемента HTML

По умолчанию HTML предоставляет ряд встроенных элементов, из которых мы можем составить структуру веб-страницы. Однако мы не ограничены встроенными html-элементами и можем сами создать и использовать свои элементы html.

В JavaScript HTML-элемент представлен интерфейсом HTMLElement. Соответственно, реализуя данный интерфейс в JavaScript, мы можем создать свои классы, которые будут представлять элементы html, и потом их использовать. Что-то наподобие следующего:

<body> <hello-metanit></hello-metanit> <script> </script> </body>

В данном случае в коде странице определен элемент <hello-metanit>, и в реальности такого элемента конечно же не существует. Но сейчас мы его создадим.

Итак, чтобы определить класс, который будет представлять html-элемент, нам достаточно создать класс, который реализует интерфейс HTMLElement:

class HelloMetanit extends HTMLElement { }

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

customElements.define(name, constructor, options);

Она принимает три параметра:

  • name: имя кастомного элемента html, который будет представлять класс JavaScript. Важно: имя должно содержать дефис.
  • constructor: конструктор (по сути класс JavaScript), который представляет кастомный элемент html.
  • options: необязательный параметр - объект, который настраивает кастомный html-элемент. В настоящий момент он поддерживает один параметр - extends. Он определяет название встроенного html-элемента, который применяется для создания кастомного элемента html.

Например, в нашем случае мы могли бы вызвать эту функцию так:

customElements.define("hello-metanit", HelloMetanit);

То есть в общем это будет выглядеть следующим образом:

<body> <hello-metanit></hello-metanit> <script> class HelloMetanit extends HTMLElement { } customElements.define("hello-metanit", HelloMetanit); </script> </body>

Но пока кастомный элемент "hello-metanit" ничего не делает. Добавим ему какую-нибудь примитивную задачу. Пусть он выводит некоторое приветствие.

Как правило, классы кастомных элементов применяют конструктор. Причем в самом начале конструктора должен идти вызов функции super(), который гарантирует, что наш класс унаследовал все методы, атрибуты и свойства интерфейса HTMlElement.

class HelloMetanit extends HTMLElement { constructor() { super(); } }

Но кроме того, в конструкторе мы можем определить некоторую базовую логику нашего элемента. Например:

<body> <hello-metanit></hello-metanit> <script> class HelloMetanit extends HTMLElement { constructor() { super(); let welcome = "Доброе утро"; const hour = new Date().getHours(); if (hour > 17) { welcome = "Добрый вечер"; } else if (hour > 12) { welcome = "Добрый день"; } this.innerText= welcome; // либо так // this.textContent = welcome; } } customElements.define("hello-metanit", HelloMetanit); </script> </body>

В конструкторе мы получаем текущее время и в зависимости от текущего часа определяем текст приветствия. Поскольку наш класс применяет интерфейс HTMLElement, то соответственно мы можем в нем использовать стандартные для html-элементов свойства. В частности, в данном случае для установки текста элемента применяется свойство innerText (также можно было бы использовать свойство textContent).

Добавление методов

Как и в обычных классах, мы можем определять в классах элементов методы и затем вызывать их. Например, определим простейший метод, который возвращает текущее время:

<body> <hello-metanit id="hello"></hello-metanit> <script> class HelloMetanit extends HTMLElement { constructor() { super(); let welcome = "Доброе утро"; const hour = new Date().getHours(); if (hour > 17) { welcome = "Добрый вечер"; } else if (hour > 12) { welcome = "Добрый день"; } this.style="cursor:pointer;" this.innerText= welcome; } showTime(){ console.log(new Date().toTimeString()); } } customElements.define("hello-metanit", HelloMetanit); // получаем элемент const hello = document.getElementById("hello"); // по нажатию вызываем его метод showTime hello.addEventListener("click", ()=> hello.showTime()); </script> </body>

Для примера в классе элемента определен метод showTime, который просто выводит на консоль текущее время. В коде javascript мы получаем по id данный элемент, прикрепляем к нему обработчик нажатия, в котором вызываем вышеопределенный метод showTime(). В итоге по нажатию в консоли мы увидим текущее время:

События жизненного цикла

Кастомный элемент html имеет свой жизненный цикл, который описывается следующими методами:

  • connectedCallback: вызывается каждый раз, когда кастомный элемент html добавляется в DOM.
  • disconnectedCallback: вызывается каждый раз, когда кастомный элемент html удаляется из DOM.
  • adoptedCallback: вызывается каждый раз, когда кастомный элемент html перемещается в новый элемент.
  • attributeChangedCallback: вызывается при каждом изменении (добавлении, изменении значения или удаления) атрибута кастомного элемента html.

Например, применим метод connectedCallback():

<body> <hello-metanit id="hello"></hello-metanit> <script> class HelloMetanit extends HTMLElement { constructor() { super(); let welcome = "Доброе утро"; const hour = new Date().getHours(); if (hour > 17) { welcome = "Добрый вечер"; } else if (hour > 12) { welcome = "Добрый день"; } this.style.cursor="pointer" this.innerText= welcome; } connectedCallback() { this.style.color = "red"; } showTime(){ console.log(new Date().toTimeString()); } } customElements.define("hello-metanit", HelloMetanit); </script> </body>

В данном случае в методе connectedCallback() просто устанавливаем цвет шрифта - в данном случае красный цвет:

this.style.color = "red";

Добавление атрибутов

Также мы можем определить у элемента свои атрибуты и затем использовать их. Например, выше при добавлении элемента на страницу у него устанавливается красный цвет текста. Зададим установку цвета с помощью атрибута:

<body> <hello-metanit hellocolor="#2980b9"></hello-metanit> <br/> <hello-metanit></hello-metanit> <script> class HelloMetanit extends HTMLElement { constructor() { super(); let welcome = "Доброе утро"; const hour = new Date().getHours(); if (hour > 17) { welcome = "Добрый вечер"; } else if (hour > 12) { welcome = "Добрый день"; } this.style.cursor="pointer" this.innerText= welcome; } connectedCallback() { this.style.color = "red"; if (this.hasAttribute("hellocolor")) { this.style.color = this.getAttribute("hellocolor"); } } showTime(){ console.log(new Date().toTimeString()); } } customElements.define("hello-metanit", HelloMetanit); </script> </body>

В данном случае элемент принимает атрибут hellocolor, который задает цвет текста элемента. Если этот атрибут определен, то по нему устанавливаем цвет текста. Если не определен, то применяется цвет по умолчанию - красный:

this.style.color = "red"; if (this.hasAttribute("hellocolor")) { this.style.color = this.getAttribute("hellocolor"); }

Стилизация CSS

Стилизация элемента через CSS производится также, как и стилизация любого другого элемента:

<!DOCTYPE html> <html> <head> <title>JS Guide</title> <meta charset="utf-8"> <style> hello-metanit{ font-family: Verdana; font-size:22px; } </style> </head> <body> <hello-metanit hellocolor="#2980b9"></hello-metanit> <script> class HelloMetanit extends HTMLElement { constructor() { super(); let welcome = "Доброе утро"; const hour = new Date().getHours(); if (hour > 17) { welcome = "Добрый вечер"; } else if (hour > 12) { welcome = "Добрый день"; } this.style.cursor="pointer" this.innerText= welcome; } connectedCallback() { this.style.color = "red"; if (this.hasAttribute("hellocolor")) { this.style.color = this.getAttribute("hellocolor"); } } showTime(){ console.log(new Date().toTimeString()); } } customElements.define("hello-metanit", HelloMetanit); </script> </body> </html>

Глава 12. События в JavaScript

📜 Что такое события?

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

💡 Основные типы событий
  • События мыши: click, dblclick, mouseover, mouseout, mousemove
  • События клавиатуры: keydown, keyup, keypress
  • События форм: submit, change, focus, blur, input
  • События документа: DOMContentLoaded, load, scroll, resize
  • События жизненного цикла элементов: (например, событие загрузки веб-станицы)
  • События, возникающие при касании на сенсорных экранах
  • События, возникающие при возникновении ошибок

📜 Способы назначения обработчиков событий

1. HTML-атрибуты (не рекомендуется)

<button onclick="alert('Clicked!')">Нажми меня</button> <button onclick="handleClick()">Кнопка</button> <script> function handleClick() { alert('Обработчик вызван!'); } </script>
⚠️ Почему не рекомендуется?

Смешивание HTML и JavaScript делает код трудным для поддержки. Современный подход — разделять структуру (HTML) и поведение (JavaScript).

2. DOM-свойства

const button = document.querySelector('button'); // Назначить обработчик button.onclick = function() { alert('Кнопка нажата!'); }; // Можно использовать именованную функцию function handleClick() { alert('Клик!'); } button.onclick = handleClick; // Удалить обработчик button.onclick = null;
⚠️ Ограничение

К одному событию можно привязать только ОДНУ функцию. При повторном присваивании предыдущая функция перезаписывается.

3. addEventListener (рекомендуется)

const button = document.querySelector('button'); // Добавить обработчик button.addEventListener('click', function() { alert('Первый обработчик'); }); // Можно добавить несколько обработчиков button.addEventListener('click', function() { alert('Второй обработчик'); }); // С именованной функцией function handleClick() { console.log('Клик обработан'); } button.addEventListener('click', handleClick); // Удалить обработчик (нужна ссылка на функцию) button.removeEventListener('click', handleClick);
✅ Преимущества addEventListener
  • Можно назначить несколько обработчиков на одно событие
  • Можно удалять обработчики
  • Поддержка фазы захвата (capture)
  • Работает с любыми событиями

📜 Объект события (Event)

button.addEventListener('click', function(event) { // event (или e) - объект события console.log(event.type); // "click" console.log(event.target); // Элемент, который вызвал событие console.log(event.currentTarget); // Элемент, к которому привязан обработчик console.log(event.clientX); // X-координата мыши console.log(event.clientY); // Y-координата мыши console.log(event.timeStamp); // Время события });

Свойства объекта события

Свойство Описание
type Тип события ("click", "keydown" и т.д.)
target Элемент, на котором произошло событие
currentTarget Элемент, к которому привязан обработчик
clientX, clientY Координаты мыши относительно окна
pageX, pageY Координаты мыши относительно документа
timeStamp Время возникновения события

📜 События мыши

Событие Описание Объект события
clickвозникает при нажатии указателем мыши на элементMouseEvent
dblclickвозникает при двойном нажатии указателем мыши на элементMouseEvent
contextmenuвозникает при открытии контекстного меню (правой кнопкой мыши)MouseEvent
mousedownвозникает при нахождении указателя мыши на элементе, когда кнопка мыши находится в нажатом состоянииMouseEvent
mouseupвозникает при нахождении указателя мыши на элементе во время отпускания кнопки мышиMouseEvent
mousemoveвозникает при прохождении указателя мыши над элементомMouseEvent
mouseoverвозникает при вхождении указателя мыши в границы элементаMouseEvent
mouseoutвозникает, когда указатель мыши выходит за пределы элементаMouseEvent
mouseenterвозникает при вхождении указателя мыши в границы элементаMouseEvent
mouseleaveвозникает, когда указатель мыши выходит за пределы элементаMouseEvent

Отдельно стоит сказать про разницу между последними четырьмя событиями. mouseenter и mouseleave срабатывают только тогда, когда пересекается внешний край соответствующего элемента. А события mouseover и mouseout также срабатывают, когда другой элемент находится внутри соответствующего элемента и курсор мыши перемещается во внутренний элемент (т.е. уходит от внешнего элемента) или покидает внутренний элемент (то есть перемещается на внешний элемент).

Например, обработаем события mouseover и mouseout:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>MOUSE EVENT EXAMP</title> <style> #blueRect{ width:100px; height:100px; background-color:blue; } </style> </head> <body> <div id="blueRect"></div> <script> function setColor(e){ if(e.type==="mouseover") e.target.style.backgroundColor = "red"; else if(e.type==="mouseout") e.target.style.backgroundColor = "blue"; } const blueRect = document.getElementById("blueRect"); blueRect.addEventListener("mouseover", setColor); blueRect.addEventListener("mouseout", setColor); </script> </body> </html>

Теперь при наведении указателя мыши на блок blueRect он будет окрашиваться в красный цвет, а при уходе указателя мыши - блок будет обратно окрашиваться в синий цвет.

📃 Объект MouseEvent

Объект Event является общим для всех событий. Однако для разных типов событий существуют также свои объекты событий, которые добавляют ряд своих свойств. Так, для работы с событиями указателя мыши определен объект MouseEvent, который добавляет следующие свойства:

  • altKey: возвращает true, если была нажата клавиша Alt во время генерации события
  • button: содержит номер нажатой кнопки мыши
  • buttons: содержит номер, который представляет нажатую кнопку мыши:
    • 1 обозначает левую кнопку мыши,
    • 2 — правую кнопыши,
    • 4 — колесо мыши или среднюю кнопку мыши,
    • 8 — четвертую кнопку мыши,
    • 16 — пятую кнопку мыши.
    Если при срабатывании события было нажато несколько кнопок, это свойство содержат сумму соответствующих чисел.

  • clientX: определяет координату Х окна браузера, на которой находился указатель мыши во время генерации события
  • clientY: определяет координату Y окна браузера, на которой находился указатель мыши во время генерации события
  • ctrlKey: возвращает true, если была нажата клавиша Ctrl во время генерации события
  • movementX: содержит координату Х относительно предыдущей координаты X при последнем событии перемещения мыши
  • movementY: содержит координату Y относительно предыдущей координаты Y при последнем событии перемещения мыши
  • metaKey: возвращает true, если была нажата во время генерации события метаклавиша клавиатуры
  • region: содержит идентификатор области или элемента, которая относится к событию
  • relatedTarget: определяет вторичный источник возникновения события
  • screenX: определяет координату Х относительно верхнего левого угла экрана монитора, на которой находился указатель мыши во время генерации события
  • screenY: определяет координату Y относительно верхнего левого угла экрана монитора, на которой находился указатель мыши во время генерации события
  • shiftKey: возвращает true, если была нажата клавиша Shift во время генерации события

Определим координаты клика:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <style> #blueRect{ width:100px; height:100px; background-color:blue; } </style> </head> <body> <div id="blueRect"></div> <script> function handleClick(e){ console.log("screenX: " + e.screenX); console.log("screenY: " + e.screenY); console.log("clientX: " + e.clientX); console.log("clientY: " + e.clientY); } const blueRect = document.getElementById("blueRect"); blueRect.addEventListener("click", handleClick); </script> </body> </html>

Еще примеры:

const box = document.querySelector('.box'); // click - одиночный клик box.addEventListener('click', (e) => { console.log('Клик!'); }); // dblclick - двойной клик box.addEventListener('dblclick', (e) => { console.log('Двойной клик!'); }); // mouseover - курсор заходит на элемент box.addEventListener('mouseover', (e) => { box.style.backgroundColor = 'lightblue'; }); // mouseout - курсор уходит с элемента box.addEventListener('mouseout', (e) => { box.style.backgroundColor = ''; }); // mousemove - движение мыши над элементом box.addEventListener('mousemove', (e) => { console.log(`X: ${e.clientX}, Y: ${e.clientY}`); }); // mousedown - кнопка мыши нажата box.addEventListener('mousedown', (e) => { console.log('Кнопка нажата'); }); // mouseup - кнопка мыши отпущена box.addEventListener('mouseup', (e) => { console.log('Кнопка отпущена'); }); // contextmenu - правый клик (контекстное меню) box.addEventListener('contextmenu', (e) => { e.preventDefault(); // Отменить стандартное меню console.log('Правый клик'); });

Определение кнопки мыши

element.addEventListener('mousedown', (e) => { if (e.button === 0) { console.log('Левая кнопка'); } else if (e.button === 1) { console.log('Средняя кнопка (колесо)'); } else if (e.button === 2) { console.log('Правая кнопка'); } });

📜 События клавиатуры

Событие Описание Объект события
keydownвозникает при нажатии клавиши клавиатуры и длится, пока нажата клавишаKeyboardEvent
keyupвозникает при отпускании клавиши клавиатурыKeyboardEvent
keypressвозникает при нажатии клавиши клавиатуры, но после события keydown и до события keyup. Надо учитывать, что данное событие генерируется только для тех клавиш, которые формируют вывод в виде символов, например, при печати символов. Нажатия на остальные клавиши, например, на Alt, не учитываются.KeyboardEvent

Для работы с событиями клавиатуры определен объект KeyboardEvent, который добавляет к свойствам объекта Event ряд специфичных для клавиатуры свойств:

  • altKey: возвращает true, если была нажата клавиша Alt во время генерации события
  • key: возвращает символ нажатой клавиши, например, при нажатии на клавишу "T" это свойство будет содержать "T". А если нажата клавиша "Я", то это свойство будет содержать "Я"

  • code: возвращает строковое представление нажатой клавиши физической клавиатуры QWERTY, например, при нажатии на клавишу "T" это свойство будет содержать "KeyT", а при нажатии на клавишу ";" (точка запятой), то свойство возвратит "Semicolon".

    При использовании этого свойства следует учитывать ряд момент. Прежде всего используется клавиатура QWERTY. То есть мы переключим раскладку, к примеру, на русскоязычную и нажмем на клавишу "Я", то значением будет "KeyZ" - на клавиатуре QWERTY клавиша Z представляет ту же клавишу, что и на русскоязычной раскладке "Я"

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

  • ctrlKey: возвращает true, если была нажата клавиша Ctrl во время генерации события
  • metaKey: возвращает true, если была нажата во время генерации события метаклавиша клавиатуры
  • shiftKey: возвращает true, если была нажата клавиша Shift во время генерации события
const input = document.querySelector('input'); // keydown - клавиша нажата input.addEventListener('keydown', (e) => { console.log('Нажата:', e.key); // Символ клавиши console.log('Код:', e.code); // Физическая клавиша console.log('KeyCode:', e.keyCode); // Устаревший код }); // keyup - клавиша отпущена input.addEventListener('keyup', (e) => { console.log('Отпущена:', e.key); }); // keypress - клавиша нажата (устарел, не использовать) // Проверка модификаторов document.addEventListener('keydown', (e) => { if (e.ctrlKey) console.log('Ctrl зажат'); if (e.shiftKey) console.log('Shift зажат'); if (e.altKey) console.log('Alt зажат'); if (e.metaKey) console.log('Meta/Cmd зажат'); // Комбинация Ctrl+S if (e.ctrlKey && e.key === 's') { e.preventDefault(); console.log('Сохранение...'); } });

Полезные клавиши

document.addEventListener('keydown', (e) => { switch(e.key) { case 'Enter': console.log('Enter нажат'); break; case 'Escape': console.log('Escape нажат'); break; case 'ArrowUp': console.log('Стрелка вверх'); break; case 'ArrowDown': console.log('Стрелка вниз'); break; case ' ': console.log('Пробел'); break; } });

Например, мы можем с помощью клавиш клавиатуры перемещать элемент на веб-странице:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>KB EVENT EXAMP</title> <style> #blueRect{ width:100px; height:100px; background-color:blue; } </style> </head> <body> <div id="blueRect"></div> <script> const blueRect = document.getElementById("blueRect"); // получаем стиль для blueRect const blueRectStyle = window.getComputedStyle(blueRect); // устанавливаем обработчик нажатия клавиши window.addEventListener("keydown", moveRect); function moveRect(e){ const left = parseInt(blueRectStyle.marginLeft); // смещение от левого края const top = parseInt(blueRectStyle.marginTop); // смещения от верхней границы switch(e.key){ case "ArrowLeft": // если нажата клавиша влево if(left>0) blueRect.style.marginLeft = left - 10 + "px"; break; case "ArrowUp": // если нажата клавиша вверх if(top>0) blueRect.style.marginTop = top - 10 + "px"; break; case "ArrowRight": // если нажата клавиша вправо if(left < document.documentElement.clientWidth - 100) blueRect.style.marginLeft = left + 10 + "px"; break; case "ArrowDown": // если нажата клавиша вниз if(top < document.documentElement.clientHeight - 100) blueRect.style.marginTop = top + 10 + "px"; break; } } </script> </body> </html>

В данном случае обрабатывается событие keydown, в обработчке которого управляем стилевыми свойствами элемента blueRect. Так как, при прикреплении обработчика стиль элемента может быть не установлен, то явным образом вычисляем его с помощью метода window.getComputedStyle():

const blueRectStyle = window.getComputedStyle(blueRect);

В обработчике события из этого стиля выбираем значения свойств marginLeft и marginTop.

const left = parseInt(blueRectStyle.marginLeft); // смещение от левого края const top = parseInt(blueRectStyle.marginTop); // смещения от верхней границы

Затем м помощью свойства e.key получаем нажатую клавишу. Список кодов клавиш клавиатуры можно посмотреть на странице https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values.

Здесь нам интересуют четыре клавиши: вверх, вниз, влево, вправо. Им соответственно будут соответствовать названия "ArrowUp", "ArrowDown", "ArrowLeft" и "ArrowRight". Если одна из них нажата, производим действия: увеличение или уменьшение отступа элемента от верхней или левой границы. Ну и чтобы элемент не выходил за границы окна, проверяем предельные значения с помощью document.documentElement.clientWidth (ширина корневого элемента) и document.documentElement.clientHeight (высота корневого элемента).

Стоит отметить, что этот код не очень оптимален, поскольку для проверки значений нам приходится вычислять положение blueRect по горизонтали и вертикали. Плюс необходимо вычислять при каждом вызове обработчика проверяем правый (document.documentElement.clientWidth - 100) и нижний край (document.documentElement.clientHeight - 100; области документа, чтобы blueRect не вышел за предел видимого пространства. В этом случае мы можем добавить дополнительные абстракции в виде текущих координат blueRect и положения правой и нижней границ видимой области.

Так, изменим код JavaScript следующим образом:

const blueRect = document.getElementById("blueRect"); const position = [20, 20]; // позиция blueRect // перемещаем blueRect на позицию в position function setPosition() { blueRect.style.marginLeft = position[0] + "px"; blueRect.style.marginTop = position[1] + "px"; } function init(){ const rightLimit = document.documentElement.clientWidth - 100; // правый край const bottomLimit = document.documentElement.clientHeight - 100; // нижний край setPosition(); // устанавливаем начальную позицию для blueRect function moveRect(e){ switch(e.key){ case "ArrowLeft": // если нажата клавиша влево if(position[0] > 0) position[0] = position[0] - 10; break; case "ArrowUp": // если нажата клавиша вверх if(position[1] > 0) position[1] = position[1] - 10; break; case "ArrowRight": // если нажата клавиша вправо if(position[0] < rightLimit) position[0] = position[0] + 10; break; case "ArrowDown": // если нажата клавиша вниз if(position[1] < bottomLimit) position[1] = position[1] + 10; break; } setPosition(); } window.addEventListener("keydown", moveRect); } // при загрузке страницы выполняем функцию init window.addEventListener("load", init);

Теперь координаты blueRect хранятся в массиве position, где первое значение - это отступ слева, а второе значение - отступ сверху. Чтобы по этим координатам установить реальную позицию blueRect на странице определена функция setPosition.

const position = [20, 20]; // позиция blueRect // перемещаем blueRect на позицию в position function setPosition() { blueRect.style.marginLeft = position[0] + "px"; blueRect.style.marginTop = position[1] + "px"; }

Прикрепляем к событию загрузки окна - load обработчик - функцию init:

window.addEventListener("load", init);

В функции init определяем правый и нижний край для перемещения blueRect, а также устанавливаем его начальную позицию:

const rightLimit = document.documentElement.clientWidth - 100; // правый край const bottomLimit = document.documentElement.clientHeight - 100; // нижний край setPosition(); // устанавливаем начальную позицию для blueRect

Далее определяем обработчик moveRect, в котором изменяем значения в массиве position:

function moveRect(e){ switch(e.key){ case "ArrowLeft": // если нажата клавиша влево if(position[0] > 0) position[0] = position[0] - 10; break; //............ } setPosition(); }

И после всех изменений переустанавливаем позицию с помощью функции setPosition.

В конце прикрепляем обработчик к нажатию клавиши клавиатуры:

📜 События форм

События элементов форм

Событие Описание Объект события
inputвозникает при изменении текста/значения в элементах (в реальном времени)Event
changeвозникает при изменении значения в списках, флажках (checkbox) или радиокнопках (после потери фокуса)Event
submitвозникает при отправке формы.Event
resetвозникает при сбросе формы (через кнопку reset).Event

События фокусировки

Событие Описание Объект события
focusвозникает, когда элемент получил фокусFocusEvent
blurвозникает, когда элемент потерял фокусFocusEvent
focusinвозникает при получении фокуса (в отличие от события focus, это событие поднимающееся. Про поднимающиеся и опускающиеся события далее)FocusEvent
focusoutвозникает при потере фокуса(это событие поднимающееся в отличие от события blur)FocusEvent
const form = document.querySelector('form'); const input = document.querySelector('input'); // submit - отправка формы form.addEventListener('submit', (e) => { e.preventDefault(); // Отменить стандартную отправку console.log('Форма отправлена'); // Собрать данные формы const formData = new FormData(form); console.log(Object.fromEntries(formData)); }); // focus - элемент получил фокус input.addEventListener('focus', (e) => { e.target.style.borderColor = 'blue'; }); // blur - элемент потерял фокус input.addEventListener('blur', (e) => { e.target.style.borderColor = ''; }); // input - значение изменилось (в реальном времени) input.addEventListener('input', (e) => { console.log('Текущее значение:', e.target.value); }); // change - значение изменилось (после потери фокуса) input.addEventListener('change', (e) => { console.log('Финальное значение:', e.target.value); }); // select для <select> const select = document.querySelector('select'); select.addEventListener('change', (e) => { console.log('Выбрано:', e.target.value); });

📜 События документа и окна

Общие события интерфейса:

Событие Описание Объект события
loadвозникает при загрузке веб-страницыUIEvent
unloadвозникает при выгрузке веб-страницы (например, когда запрошена страница по новому адресу)UIEvent
abortвозникает при отмене загрузки ресурсаUIEvent
Errorвозникает при генерации ошибки при загрузке страницы (например, ошибка в коде JavaScript)UIEvent
selectвозникает при выделении текста на страницеUIEvent
resizeвозникает при изменении размеров окна браузераUIEvent
scrollвозникает при прокруткеUIEvent
beforeunloadвозникает непосредственно перед выгрузкой страницыBeforeUnloadEvent
DOMContentLoadedвозникает при полной загрузке дерева DOMEvent
cutвозникает при вырезании текста из поля ввода (например, с помощью Ctrl+X)ClipboardEvent
copyвозникает при копировании текста из поля ввода (например, с помощью Ctrl+C)ClipboardEvent
pasteвозникает при вставке текста в поле ввода (например, с помощью Ctrl+V)ClipboardEvent
selectвозникает при выделении текста в поле вводаClipboardEvent

События мобильных устройств и других устройств с сенсорным экраном:

Событие Описание Объект события
orientationchangeвозникает при изменении ориентации устройстваEvent
deviceorientationвозникает, когда появляются новые данные об ориентации устройстваDeviceOrientationEvent
devicemotionвозникает с регулярными интервалами и указывает на силу ускорения, действующую на конечное устройствоDeviceMotionEvent
touchstartвозникает при касании дисплеяTouchEvent
touchendвозникает, когда палец убран с дисплея (касание завершилось)TouchEvent
touchmoveвозникает при движении пальцем по сенсорному дисплеюTouchEvent
touchcancelвозникает при прерывании отслеживания касанийTouchEvent
// DOMContentLoaded - DOM загружен (без стилей/картинок) document.addEventListener('DOMContentLoaded', () => { console.log('DOM готов!'); }); // load - всё загружено (включая стили/картинки) window.addEventListener('load', () => { console.log('Страница полностью загружена'); }); // beforeunload - перед закрытием/перезагрузкой window.addEventListener('beforeunload', (e) => { e.preventDefault(); e.returnValue = ''; // Показать диалог "Покинуть страницу?" }); // resize - изменение размера окна window.addEventListener('resize', () => { console.log(`Ширина: ${window.innerWidth}px`); }); // scroll - прокрутка страницы window.addEventListener('scroll', () => { console.log(`Прокручено: ${window.scrollY}px`); });

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

Если в коде JavaScript возникает событие, то его обрабатывает связанный с этим событием обработчик. Рассмотрим, как определять обработчики событий.

Встроенные обработчики

Самый простой способ определения обработчиков событий - их установка в коде html. Это так называемые встроенные обработчики или inline-обработчики, которые определяются в коде элемента с помощью атрибутов. Подобные атрибуты начинаются с префикса on. Например, у многих html-элементов есть атрибут onclick, который определяет обработчик нажатия элемента.
Посмотрим на примере кнопки:

<body> <button onclick="console.log('Clicked!')">Click Me</div> </body>

С помощью атрибута onclick="console.log('Clicked!')" к кнопке прикрепляется обработчик ее нажатия. Этот обработчик состоит из одной инструкции JavaScript - console.log("Clicked!"), которая выводит сообщение на консоль. Таким образом, при нажатии на кнопку сработает событие нажатия, и будет выполняться обработчик из атрибута onclick:

Можно даже определить несколько инструкций подобным образом:

<button onclick="console.log('Hello');console.log('Clicked!')">Click Me</div>

Но, очевидно, что это не самый удобный способ. Но также можно вынести все инструкции в отдельную функцию JavaScript. А атрибуту onclick передать вызов этой функции:

<body> <button onclick="btn_click()">Click Me</div> <script> let clicks = 0; // счетчик нажатий function btn_click(){ console.log("Clicked", ++clicks); } </script> </body>

Теперь по нажатию кнопки будет вызываться функция btn_click, которая определена в коде JavaScript.

Хотя этот подход прекрасно работает, но он имеет кучу недостатков:

  • Код html смешивается с кодом JavaScript, в связи с чем становится труднее разрабатывать, отлаживать и поддерживать приложение
  • Обработчики событий можно задать только для уже созданных на веб-странице элементов. Динамически создаваемые элементы в этом случае лишаются возможности обработки событий
  • К элементу для одного события может быть прикреплен только один обработчик
  • Нельзя удалить обработчик без изменения кода

Свойства обработчиков событий

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

<body> <button id="btn">Click Me</div> <script> let clicks = 0; // счетчик нажатий function btn_click(){ console.log("Clicked", ++clicks); } // устанавливаем обработчик нажатия для элемента с id="btn" document.getElementById("btn").onclick = btn_click; </script> </body>

В итоге нам достаточно взять свойство onclick и присвоить ему функцию, используемую в качестве обработчика. За счет этого код html отделяется от кода javascript.

Слушатели событий

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

Для работы со слушателями событий в JavaScript есть объект EventTarget, который определяет методы addEventListener() (для добавления слушателя) и removeEventListener() для удаления слушателя. И поскольку html-элементы DOM тоже являются объектами EventTarget, то они также имеют эти методы. Фактически слушатели представляют те же функции обработчиков.

Метод addEventListener() принимает два параметра: название события без префикса on и функцию обработчика этого события. Например:

<body> <button id="btn">Click Me</div> <script> let clicks = 0; // счетчик нажатий function btn_click(){ console.log("Clicked", ++clicks); } const btn = document.getElementById("btn"); // прикрепляем обработчик события "click" btn.addEventListener("click", btn_click); </script> </body>

То есть в данном случае опять же обрабатывается событие click. Удаление слушателя аналогично добавлению:

rect.removeEventListener("click", btn_click);

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

<body> <button id="btn">Click Me</div> <script> let clicks = 0; // счетчик нажатий function btn_click(){ console.log("Clicked", ++clicks); } const btn = document.getElementById("btn"); // прикрепляем первый обработчик события "click" в виде функции btn_click btn.addEventListener("click", btn_click); // прикрепляем второй обработчик события "click" в виде анонимной функции btn.addEventListener("click", function(){ console.log("Button clicked!") }); // прикрепляем третий обработчик события "click" в виде стрелочной функции btn.addEventListener("click", ()=>console.log("Element clicked!")); </script> </body>

📜 Передача данных в обработчик события. Объект Event

Иногда возникает необходимость передать в обработчик некоторые данные. Если обработчики событий устанавливаются с помощью атрибутов элементов, то это сделать довольно просто. Например, передадим в обработчик нажатия кнопки текст, которые будет использоваться в обработчике:

<body> <button id="btn" onclick="btn_click('Button Clicked')">Click Me</button> <script> // в обработчик передается текст function btn_click(text){ console.log(text); // выводим этот текст } </script> </body>

Итак, здесь в обработчик кнопки передается некоторый текст:

<button id="btn" onclick="btn_click('Button Clicked')">

В функции обработчика получаем этот текст и некоторым образом его используем, например, выводим на консоль:

function btn_click(text){ console.log(text); // выводим этот текст }

В данном случае в функцию обработчика передавалась строка, но в реальности, это может быть любой объект. Например, через значение this можно передать текущий объект, на котором возникает событие:

<body> <button id="btn" onclick="btn_click(this)">Click Me</button> <script> let clicks = 0; // счетчик нажатий // в обработчик передается ссылка на элемент кнопки function btn_click(btn){ // изменяем текст кнопки btn.textContent = `Clicked ${++clicks}`; } </script> </body>

Ключевое слово this указывает на текущий объект ссылки, на которую производится нажатие. И в коде обработчика мы можем получить этот объект и обратиться к его свойствам, например, к свойству textContent и таким образом изменить текст кнопки.

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

<body> <a id="link" href="https://metanit.com" onclick="return a_click(this)">Metanit.com</a> <script> // в обработчик передается ссылка function a_click(anchor){ // получаем адрес ссылки console.log(anchor.href); return false; // запрещаем переадресацию } </script> </body>

Здесь в атрибуте onclick ссылки - элемента <a> не просто вызывается обработчик события, а возвращается его результат:

<a id="link" href="https://metanit.com" onclick="return a_click(this)">

Причем функция обработчика возвращает false:

function a_click(anchor){ console.log(anchor.href); return false; // запрещаем переадресацию }

Дело в том, что для некоторых обработчиков можно подтвердить или остановить обработку события. Например, нажатие на ссылку должно привести к переадресации. Но возвращая из обработчика false, мы можем остановить стандартный путь обработки события, и переадресации не будет. Если же возвращать значение true, то событие обрабатывается в стандартном порядке.

Если же мы вовсе уберем возвращении результата, то событие будет обрабатываться, как будто возвращается значение true:

<a id="link" href="https://metanit.com" onclick="a_click(this)">Metanit.com</a> <script> function a_click(anchor){ console.log(anchor.href); } </script>

Объект Event

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

  • bubbles: возвращает true, если событие является восходящим. Например, если событие возникло на вложенном элементе, то оно может быть обработано на родительском элементе.
  • cancelable: возвращает true, если можно отменить стандартную обработку события
  • currentTarget: определяет элемент, к которому прикреплен обработчик события
  • defaultPrevented: возвращает true, если был вызван у объекта Event метод preventDefault()
  • eventPhase: хранит число, которое представляет стадию обработки события. Возможные значения:
    • 0 (Event.NONE)
    • 1 (Event.CAPTURING_PHASE)
    • 2 (Event.AT_TARGET)
    • 3 (Event.BUBBLING_PHASE)
  • target: указывает на элемент, на котором было вызвано событие
  • timeStamp: хранит время возникновения события
  • type: указывает на имя события
  • isTrusted: указывает, событие было сгенерировано элементами веб-страницы или кодом JavaScript

Например:

<body> <button onclick="btn_click(event)">Click Me</button> <script> function btn_click(e){ console.log(e); } </script> </body>

При вызове функции-обработчика информация о событии доступна через объект event. Этот объект не определяется разработчиком, это просто аргумент функции обработчика, который хранит всю информацию о событии:

<button onclick="btn_click(event)"></button>

В коде JavaScript этот объект можно получить через параметр функции:

function btn_click(e){ console.log(e); }

В данном случае просто выводим объект на консоль. Но естественно также можно было бы получить отдельную конкретную информацию о событии:

function btn_click(e){ console.log("Type:", e.type); // Type: click console.log("Target:", e.target); // Target: <button onclick=​"btn_click(event)​">​Click Me​</button>​ console.log("Timestamp:", e.timeStamp); }

Подобным образом мы можем получить объект события, если обработчик события прикрепляется через свойства элементов или через метод addEventListener(). Например, прикрепеление обработчика через свойство элемента:

<button id="btn">Click Me</button> <script> function btn_click(e){ console.log("Type:", e.type); console.log("Target:", e.target); console.log("Timestamp:", e.timeStamp); } // устанавливаем обработчик нажатия для элемента с id="btn" document.getElementById("btn").onclick = btn_click; </script>

Или прикрепеление обработчика с помощью метода addEventListener:

<button id="btn">Click Me</button> <script> function btn_click(e){ console.log("Type:", e.type); console.log("Target:", e.target); console.log("Timestamp:", e.timeStamp); } const btn = document.getElementById("btn"); // прикрепляем обработчик события "click" btn.addEventListener("click", btn_click); </script>

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

С помощью метода preventDefault() объекта Event мы можем остановить дальнейшее выполнение события. В ряде случаев этот метод не играет большой роли. Однако в некоторых ситуаций он может быть полезен. Например, при нажатии на ссылку мы можем с помощью дополнительной обработки определить, надо ли переходить по ссылке или надо запретить переход. Или другой пример: пользователь отправляет данные формы, но в ходе обработки в обработчике события мы определили, что поля формы заполнены неправильно, и в этом случае мы также можем запретить отправку.

Например, остановим переход по ссылке:

<a id="link" href="https://metanit.com">Metanit.com</a> <script> function linkHandler(e){ console.log("Link has been clicked"); e.preventDefault(); // останавливаем переход по ссылке } const link = document.getElementById("link"); link.addEventListener("click", linkHandler); </script>

Здесь по нажатию на ссылку будет срабатывать метод linkHandler. И, поскольку в этом методе с помощью вызова e.preventDefault() предупреждаем переход по ссылке, то перехода не будет. Данный подход, к примеру, часто используется при ajax-запросах, когда надо обработать нажатие на ссылку, но при этом не выполнять перехода на другой ресурс, а сделать к нему запрос из кода javascript без перезагрузки страницы.

Получение текущего объекта

Для получения текущего объекта, для которого обрабатыватся событие, внутри обработчика события мы можем использовать ключевое слово this:

<button id="btn">Click Me</button> <script> const btn = document.getElementById("btn"); function btn_click(){ console.log(this); // <button id="btn">Click Me</button> } btn.addEventListener("click", btn_click); </script>

Здесь при обработке события click на кнопке объект this в функции btn_click будет представлять эту кнопку. Фактически в данном случае значения this и event.target были бы эквивалентны

function btn_click(e){ console.log(this===e.target); // true }

📜 Распространение (всплытие и погружение) событий

Когда мы нажимаем на какой-либо элемент на станице и генерируется событие нажатия, то это событие может распространяться от элемента к элементу. Например, если мы нажимаем на блок div, то также мы нажимаем и на элемент body, в котором блок div находится. То есть происходит распространение события.

События в DOM проходят три фазы:

  1. Фаза захвата (нисходящее/capturing) — от window к целевому элементу: событие распространяется вверх по дереву DOM от дочерних узлов к родительским
  2. Фаза цели (target) — на самом элементе
  3. Фаза всплытия (восходящее/bubbling) — от целевого элемента к window: событие распространяется вниз по дереву DOM от родительских узлов к дочерним, пока не достигнет того элемента, на котором это событие и возникло

Восходящие события

Восходящие (bubbling) события: распространяются в верх по дереву DOM. Допустим, у нас есть следующая веб-страница:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>BUBBLING EVENT EXAMP</title> <style> #blueRect{ width:100px; height:100px; background-color:blue; } #redRect{ width:50px; height:50px; background-color:red; } </style> </head> <body> <div id="blueRect"> <div id="redRect"></div> </div> <script> const redRect = document.getElementById("redRect"); redRect.addEventListener("click", () => console.log("Событие на redRect")); const blueRect = document.getElementById("blueRect"); blueRect.addEventListener("click", ()=>console.log("Событие на blueRect")); document.body.addEventListener("click", () => console.log("Событие на body")); </script> </body> </html> // Событие на redRect // Событие на blueRect // Событие на body

Если мы нажмем на вложенный (красный) div, то событие пойдет к родительскому элементу div и далее к элементу body:

Подобное поведение не всегда является желательным. И в этом случае мы можем остановить распространение событие с помощью метода stopPropagation() объекта Event:

const redRect = document.getElementById("redRect"); redRect.addEventListener("click", function(e){ console.log("Событие на redRect"); e.stopPropagation(); });

И в результате нажатия событие будет обработано только обработчиком для redRect.

Правда, у stopPropagation() есть одна проблема - он приостанавливает дальнейшее выполнение текущего обработчика. Однако если для одного и того же события элемента прикреплены несколько обработчиков событий, то остальные обработчики продолжат выполняться. И чтобы оставить также выполнение всех остальных обработчиков подобных образом можно вызывать метод stopImmediatePropagation

const redRect = document.getElementById("redRect"); function handler1(e){ console.log("handler1: Событие на redRect"); e.stopImmediatePropagation(); // останавливаем также выполнение handler2 } function handler2(e){ console.log("handler2: Событие на redRect"); } redRect.addEventListener("click", handler1); redRect.addEventListener("click", handler2);

Нисходящие события

События также могут быть нисходящими (capturing). Для их использования в метод addEventListener() в качестве третьего необязательного параметра передается логическое значение true или false. Значение true указывает, что событие нисходящим. По умолчанию все события восходящие.

Возьмем ту же веб-станицу, только изменим ее код javascript:

const redRect = document.getElementById("redRect"); redRect.addEventListener("click", function(){ console.log("Событие на redRect"); }, true); const blueRect = document.getElementById("blueRect"); blueRect.addEventListener("click", function(){ console.log("Событие на blueRect"); }, true); document.body.addEventListener("click", function(){ console.log("Событие на body"); }, true); // Событие на body // Событие на blueRect // Событие на redRect

Теперь события будут распространяться в обратном порядке:

<div class="outer"> <div class="inner"> <button>Кликни</button> </div> </div> <script> const outer = document.querySelector('.outer'); const inner = document.querySelector('.inner'); const button = document.querySelector('button'); // Всплытие (по умолчанию) outer.addEventListener('click', () => { console.log('1. Outer (всплытие)'); }); inner.addEventListener('click', () => { console.log('2. Inner (всплытие)'); }); button.addEventListener('click', () => { console.log('3. Button (цель)'); }); // При клике на button: // 3. Button (цель) // 2. Inner (всплытие) // 1. Outer (всплытие) // Захват (capture: true) outer.addEventListener('click', () => { console.log('Outer (захват)'); }, true); // При клике на button: // Outer (захват) // 3. Button (цель) // 2. Inner (всплытие) // 1. Outer (всплытие) </script>

event.target vs event.currentTarget

outer.addEventListener('click', (e) => { console.log('target:', e.target); // Элемент, на котором произошел клик console.log('currentTarget:', e.currentTarget); // Элемент с обработчиком }); // Если кликнуть на button: // target: button (на чём кликнули) // currentTarget: outer (где висит обработчик)

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

button.addEventListener('click', (e) => { e.stopPropagation(); // Остановить всплытие console.log('Button clicked'); // Обработчики на inner и outer НЕ сработают }); // stopImmediatePropagation - остановить все обработчики button.addEventListener('click', (e) => { e.stopImmediatePropagation(); console.log('Первый обработчик'); }); button.addEventListener('click', (e) => { console.log('Второй обработчик'); // НЕ выполнится });
⚠️ Когда НЕ нужно останавливать всплытие

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

📜 Отмена действия по умолчанию

// Отменить переход по ссылке const link = document.querySelector('a'); link.addEventListener('click', (e) => { e.preventDefault(); console.log('Переход отменён'); }); // Отменить отправку формы form.addEventListener('submit', (e) => { e.preventDefault(); console.log('Отправка отменена'); }); // Отменить контекстное меню document.addEventListener('contextmenu', (e) => { e.preventDefault(); }); // Проверка, можно ли отменить событие element.addEventListener('click', (e) => { if (e.cancelable) { e.preventDefault(); } });

📜 Делегирование событий

Вместо назначения обработчика каждому элементу, назначаем один обработчик родителю и используем event.target.

<ul id="menu"> <li>Пункт 1</li> <li>Пункт 2</li> <li>Пункт 3</li> </ul> <script> // Плохо: обработчик на каждый элемент const items = document.querySelectorAll('li'); items.forEach(item => { item.addEventListener('click', () => { console.log('Клик на:', item.textContent); }); }); // Хорошо: один обработчик на родителе const menu = document.querySelector('#menu'); menu.addEventListener('click', (e) => { if (e.target.tagName === 'LI') { console.log('Клик на:', e.target.textContent); } }); </script>
✅ Преимущества делегирования
  • Меньше обработчиков = меньше памяти
  • Работает с динамически добавленными элементами
  • Упрощает код

Делегирование с классами

<div id="container"> <button class="delete-btn">Удалить 1</button> <button class="edit-btn">Редактировать</button> <button class="delete-btn">Удалить 2</button> </div> <script> const container = document.querySelector('#container'); container.addEventListener('click', (e) => { // Проверка класса if (e.target.classList.contains('delete-btn')) { console.log('Удаление'); } if (e.target.classList.contains('edit-btn')) { console.log('Редактирование'); } // Или через matches (поддерживает CSS-селекторы) if (e.target.matches('.delete-btn')) { console.log('Удаление через matches'); } }); </script>

📜 Программный вызов событий

События могут возникать не только в следствие действий пользователя на веб-странице. События также можно генерировать программно.

Чтобы программно вызвать событие, у элемента на веб-странице можно вызвать метод dispatchEvent(), в который передается экземпляр объекта Event (либо его производные типа MouseEvent или KeybordEvent).

const event = new Event(имя_события, config); // определяем объект события element.dispatchEvent(event); // вызываем событие для элемента element

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

  • cancelable: можно ли событие отменить (если true, то отменяемое событие, false - неотменяемое)
  • bubbles: должно ли событие быть восходящим (если true, то восходящее)

Например, программно нажмем на ссылку:

<body> <a id="link" href="https://metanit.com">Metanit.com</a> <script> const link = document.getElementById("link"); // получаем ссылку const event = new MouseEvent("click"); link.dispatchEvent(event); </script> </body>

Нажатие на ссылку представляет событие мыши "click", поэтому определяем объект события типа MouseEvent:

const event = new MouseEvent("click");

Затем вызываем событие для элемента link:

link.dispatchEvent(event);

В итоге произойдет переход по ссылке уже при загрузке страницы.

И как в общем случае, это событие также можно обработать:

<body> <a id="link" href="https://metanit.com">Metanit.com</a> <script> const link = document.getElementById("link"); link.addEventListener("click", (e)=>{ console.log("Link has been clicked"); e.preventDefault(); // предотвращаем переход }); const event = new MouseEvent("click", {cancelable:true}); link.dispatchEvent(event); </script> </body>

Чтобы выполнение события можно было остановить, в конструктор MouseEvent в качестве второго параметра передаем конфигурационных объект с одним свойством: cancelable:true указывает, что можно остановить обработку события. Благодаря этому в обработчике события "click" можно вызвать метод e.preventDefault()

📜 Пользовательские события

Возможность программной генерации событий открывает нам путь к созданию кастомных событий - мы можем определять и вызывать произвольные события.

Например, у нас есть функция-конструктор Account, которая принимает коичество денег и создает условный денежный счет:

function Account(money) { _money = money; this.pay=function(sum){ if(_money >= sum){ _money -= sum; console.log(_money); } } }

В переменной _money хранится текущее количество денег на счете. С помощью функции pay условно тратим определенную сумму, если баланс позволяет. Но, допустим, нам надо как-то извещать систему, что произошло списание со счета. С одно стороны, мы могли бы это делать непосредственно в методе pay - вызывать в методе console.log() и выводить на консоль какой-то текст. Но на момент написания этого кода мы можем быть не уверены, какой именно текст надо выводить на консоль. А может быть потребуется и не на консоль, а в окне браузере. Или посылать извещение на определенный сетевой ресурс. А может наша функция-конструктор будет использоваться в Node.js, где может потребоваться какая-то другая обработка. Да и использовать нашу функцию-конструктор могут совсем другие разработчики, у которых может быть собственно понимание того, что надо делать при списании средств. В любом случае мы сталкиваемся с многовариантностью, но во всех этих ситуация главное, что нам надо сделать - уведомить систему, что произошло списание средств. И охватить все эти ситуации нам поможет определение собственных событий.

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

<body> <button id="btn">Pay</button> <script> const button = document.getElementById("btn"); const myAcc = new Account(100); // условный денежный счет // устанавливаем обработчик события payment для всего документа document.addEventListener("payment", ()=>console.log("Payment succeeded!")); // по нажатию на кнопку выполняем метод pay button.addEventListener("click", ()=>myAcc.pay(50)); // конструктор объекта счета function Account(money) { _money = money; this.pay=function(sum){ if(_money >= sum){ _money -= sum; console.log(_money); const event = new Event("payment"); // определяем объект события document.dispatchEvent(event); // генерируем событие для всего документа } } } </script> </body>

Основные моменты. В методе pay создаем объект Event, которое будет представлять событие "payment" (не важно, что такого события изначально не существует, мы сами его создаем). Затем генерируем это событие:

const event = new Event("payment"); // определяем объект события document.dispatchEvent(event); // генерируем событие для всего документа

Стоит отметить, что событие генерируется для всего документа: document.dispatchEvent(event), но это может быть любой конкретный элемент веб-страницы.

Чтобы обработать это событие, подписываемся на него:

document.addEventListener("payment", ()=>console.log("Payment succeeded!"));

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

По нажатию на кнопку вызываем метод pay объекта myAcc и тем самым генерируем событие "payment" (если на счете достаточно средств).

Для тестирования понажимаем на кнопку:

// 50 // Payment succeeded! // 0 // Payment succeeded!

Также, как и в общем случае, мы можем получить объект-подобное события в обработчике:

// получаем через параметр e объект события document.addEventListener("payment", (e)=>{ console.log(e.type); // payment console.log("Payment succeeded!"); });

CustomEvent

Однако тип Event хотя и может использоваться, но не очень подходит для определения кастомных событий. Например, что, если мы хотим передать в обработчик события какую-то дополнительную информацию - сумму списания, текущий баланс или что-то еще? И для подобных случаев лучше использовать тип CustomEvent. Так, изменим код JavaScript следующим образом:

const button = document.getElementById("btn"); document.addEventListener("payment", (e)=>{ console.log("Payment succeeded!"); console.log("Payment Sum:", e.detail.paymentSum); // получаем данные события console.log("Current balance:", e.detail.balance); }); const myAcc = new Account(100); // по нажатию на кнопку выполняем метод pay button.addEventListener("click", ()=>myAcc.pay(50)); function Account(money) { _money = money; this.pay=function(sum){ if(_money >= sum){ _money -= sum; // определяем объект события const event = new CustomEvent("payment", { detail:{ // передаем в CustomEvent данные о событии paymentSum: sum, balance: _money } }); document.dispatchEvent(event); // генерируем событие для всего документа } } }

В CustomEvent в качестве второго параметра передается конфигурационный объект, который имеет свойство detail. Это свойство в свою очередь представляет объект с произвольным набором свойств. В данном случае мы определяем в нем свойства paymentSum и balance и передаем этим свойствам интересующие нас значения:

const event = new CustomEvent("payment", { detail:{ paymentSum: sum, balance: _money }

Далее передаем объект CustomEvent (как и Event) в dispatchEvent и тем самым генерируем событие:

document.dispatchEvent(event);

При обработке события мы можем получить переданные данные через свойство detail:

document.addEventListener("payment", (e)=>{ console.log("Payment succeeded!"); console.log("Payment Sum:", e.detail.paymentSum); // получаем данные события console.log("Current balance:", e.detail.balance); });

Пример консольного вывода при первом нажатии кнопки:

Payment succeeded! Payment Sum: 50 Current balance: 50

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

const button = document.getElementById("btn"); document.addEventListener("payment_success", (e)=>{ console.log("Payment succeeded!"); console.log("Payment Sum:", e.detail.paymentSum); console.log("Current balance:", e.detail.balance); }); document.addEventListener("payment_fail", (e)=>{ console.error("Payment failed"); console.error("Current balance:", e.detail.balance, "Requested Sum: ", e.detail.paymentSum); }); const myAcc = new Account(100); button.addEventListener("click", ()=>myAcc.pay(50)); function Account(money) { _money = money; this.pay=function(sum){ const data = { paymentSum: sum, balance: _money }; if(_money >= sum){ _money -= sum; const event = new CustomEvent("payment_success", { detail: data }); document.dispatchEvent(event); } else{ const event = new CustomEvent("payment_fail", { detail: data }); document.dispatchEvent(event); } } }

Теперь, если средст достаточно на счете генерируется событие "payment_success", а если недостаточно - то "payment_fail". И для каждого из этих событий определяем свой обработчик.

консольный вывод программы (при трех нажатиях на кнопку):

Payment succeeded!
Payment Sum: 50
Current balance: 100
Payment succeeded!
Payment Sum: 50
Current balance: 50
Payment failed
Current balance: 0 Requested Sum:  50
// Создать пользовательское событие const myEvent = new CustomEvent('myevent', { detail: { message: 'Привет!', data: 123 }, bubbles: true, cancelable: true }); // Подписаться на событие element.addEventListener('myevent', (e) => { console.log('Событие получено:', e.detail); }); // Вызвать событие element.dispatchEvent(myEvent);

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

// Создаём событие "покупка товара" class ShoppingCart { constructor() { this.items = []; } addItem(item) { this.items.push(item); // Генерируем событие const event = new CustomEvent('itemAdded', { detail: { item, total: this.items.length } }); document.dispatchEvent(event); } } // Слушаем событие document.addEventListener('itemAdded', (e) => { console.log(`Добавлен: ${e.detail.item}`); console.log(`Всего товаров: ${e.detail.total}`); }); // Использование const cart = new ShoppingCart(); cart.addItem('Ноутбук'); // Событие сработает cart.addItem('Мышь'); // Событие сработает

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

Модальное окно

const modal = document.querySelector('.modal'); const openBtn = document.querySelector('.open-modal'); const closeBtn = document.querySelector('.close-modal'); // Открыть модалку openBtn.addEventListener('click', () => { modal.classList.add('active'); }); // Закрыть по кнопке closeBtn.addEventListener('click', () => { modal.classList.remove('active'); }); // Закрыть по клику вне модалки modal.addEventListener('click', (e) => { if (e.target === modal) { modal.classList.remove('active'); } }); // Закрыть по Escape document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && modal.classList.contains('active')) { modal.classList.remove('active'); } });

Табы

const tabs = document.querySelectorAll('.tab'); const contents = document.querySelectorAll('.tab-content'); tabs.forEach((tab, index) => { tab.addEventListener('click', () => { // Убрать активные классы tabs.forEach(t => t.classList.remove('active')); contents.forEach(c => c.classList.remove('active')); // Добавить активные классы tab.classList.add('active'); contents[index].classList.add('active'); }); });

Бесконечная прокрутка

let page = 1; let loading = false; window.addEventListener('scroll', () => { // Если почти долистали до конца if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 500) { if (!loading) { loading = true; loadMoreContent(); } } }); async function loadMoreContent() { console.log('Загрузка страницы', page); // Загрузить данные const data = await fetch(`/api/posts?page=${page}`); const posts = await data.json(); // Добавить на страницу posts.forEach(post => { const div = document.createElement('div'); div.textContent = post.title; document.body.appendChild(div); }); page++; loading = false; }

📜 Оптимизация обработчиков

Debounce (задержка)

// Функция будет вызвана только через 300мс после последнего события function debounce(func, delay) { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), delay); }; } // Использование const input = document.querySelector('input'); input.addEventListener('input', debounce((e) => { console.log('Поиск:', e.target.value); }, 300));

Throttle (ограничение частоты)

// Функция будет вызываться не чаще, чем раз в delay мс function throttle(func, delay) { let lastCall = 0; return function(...args) { const now = Date.now(); if (now - lastCall >= delay) { lastCall = now; func.apply(this, args); } }; } // Использование window.addEventListener('scroll', throttle(() => { console.log('Прокрутка'); }, 100));
💡 Когда использовать?
  • Debounce: поиск при вводе, изменение размера окна
  • Throttle: прокрутка, движение мыши, анимации

Глава 13. Работа с формами

Один из способов взаимодействия с пользователями представляют html-формы. Например, если нам надо получить от пользователя некоторую информацию, мы можем определить на веб-странице формы, которая будет содержать текстовые поля для ввода информации и кнопку для отправки. И после ввода данных мы можем обработать введенную информацию.

Для создания формы используется элемент <form>:

<form id="search" name="search"> </form>

В JavaScript форма представлена объектом HtmlFormElement. И после создания формы мы можем к ней обратиться различными способами.

📜 Доступ к формам и элементам

Доступ к форме

Первый способ заключается в прямом обращении по имени формы:

const searchForm = document.search;

Второй способ состоит в обращении к коллекции форм документа - коллекция forms и поиске в ней нужной формы:

// Через document.forms (коллекция всех форм) const form1 = document.forms[0]; // Первая форма const form2 = document.forms['myForm']; // По атрибуту name const form3 = document.forms.myForm; // Короткая запись

Третий способ представляет получение форм стандартными методами для поиска элемента по id, по тегу или по селектору. Например:

// Через querySelector const form = document.querySelector('#myForm'); const form = document.querySelector('form');

Свойства и методы форм

  • name: имя формы
  • elements: коллекция элементов формы
  • length: количество элементов формы
  • action: значение атрибута action - адрес отправки формы
  • method: значение атрибута method - метод HTTP, применяемый для отправки

Например, получим свойства формы:

<body> <form id="search" name="search" action="https://google.com/search" method="get"> <input type="text" id="key" name="q" /> <input type="submit" id="send" name="send" /> </form> <script> const form = document.getElementById("search"); console.log(form.elements); // HTMLFormControlsCollection(2)// [input, input, q: input, send: input] console.log(form.length); // 2 console.log(form.name); // search console.log(form.action); // https://google.com/search console.log(form.method); // get </script> </body>

Доступ к элементам формы

const form = document.forms.myForm; // Через form.elements (рекомендуется) const input = form.elements.username; // По name const input = form.elements['username']; const input = form.elements[0]; // По индексу // Прямой доступ через name (короткая запись) const input = form.username; // Через querySelector const input = form.querySelector('input[name="username"]'); const input = form.querySelector('#username');

Среди методов формы надо отметить метод submit(), который отправляет данные формы на сервер, и метод reset(), который очищает поля формы:

const form = document.forms["search"]; form.submit(); form.reset();

Элементы форм

Форма может содержать различные элементы ввода html: input, textarea, button, select и т.д. Для каждого из элементов существует свой тип JavaScript:

html-элемент Тип JavaScript
<input>HTMLInputElement
<textarea>HTMLTextAreaElement
<select>HTMLSelectElement
<option> (в списках <select>)HTMLOptionElement

Для получения элементов форм можно использовать несколько способов:

  • Применение стандартных методов getElementById(), getElementsByClassName(), getElementsByTagName(), getElementsByName(), querySelector() и querySelectorAll() для поиска элементов соответственно по id, классу, тегу, имени или селектору. Например, возьмем ранее определенную форму и получим ее поле ввода:
    // получаем элемент по id="key" const keyField = document.getElementById("key"); console.log(keyField);
  • Использование свойства elements соответствующей формы. Например:
    const form = document.getElementById("search"); // получение поля по индексу const keyField = form.elements[0]; console.log(keyField); // получение этого же поля, но через имя const keyField2 = form.elements["q"]; console.log(keyField2);
  • Использование имени формы и элемента. Например:

    // поле q на форме search const keyField = document.search.q; console.log(keyField);

Свойства элементов форм

Все они имеют ряд общих свойств и методов. Также, как и форма, элементы форм имеют свойство name, с помощью которого можно получить значение атрибута name. Другим важным свойством является свойство value, которое позволяет получить или изменить значение поля:

<form name="search"> <input type="text" name="key" value="hello world"></input> <input type="submit" name="send"></input> </form> <script> const form = document.getElementById("search"); // получение поля формы по имени const keyField = form.elements["key"]; // получение значения поля console.log(keyField.value); // установка значения поля keyField.value = "Enter a string"; </script>

Свойство type позволяет получить тип поля ввода. Это либо название тега элемента (например, textarea), либо значение атрибута type у элементов input.

const form = document.getElementById("search"); // получение поля формы по имени const keyField = form.elements["key"]; // получение значения поля console.log(keyField.type); // text

Из методов можно выделить методы focus() (устанавливает фокус на элемент) и blur() (убирает фокус с элемента):

const searchForm = document.forms["search"]; const keyField = searchForm.elements["key"]; keyField.focus();

📜 Кнопки

Для отправки введенных данных на форме используются кнопки. Для создания кнопки используется либо элемент button:

<button name="send">Отправить</button>

Либо элемент input:

<input type="submit" name="send" value="Отправить" />

С точки зрения функциональности в html эти элементы не совсем равноценны, но в данном случае они нас интересуют с точки зрения взаимодействия с кодом javascript.

При нажатии на любой из этих двух вариантов кнопки происходит отправка формы по адресу, который указан у формы в атрибуте action, либо по адресу веб-страницы, если атрибут action не указан. Однако в коде javascript мы можем перехватить отправку, обрабатывая событие click

<body> <form name="search"> <input type="text" name="key"></input> <input type="submit" name="send" value="Отправить" /> </form> <script> function sendForm(e){ // получаем значение поля key const keyBox = document.search.key; const val = keyBox.value; if(val.length<3){ alert("Недопустимая длина строки"); e.preventDefault(); } else alert("Отправка разрешена"); } const sendButton = document.search.send; sendButton.addEventListener("click", sendForm); </script> </body>

При нажатии на кнопку происходит событие click, и для его обработки к кнопке прикрепляем обработчик sendForm. В этом обработчике проверяем введенный в текстовое поле текст. Если его длина меньше 3 символов, то выводим сообщение о недостимой длине и прерываем обычный ход события с помощью вызова e.preventDefault(). В итоге форма не отправляется.

Если же длина текста три и больше символов, то также выводится сообщение, и затем форма отправляется.

Также мы можем при необходимости при отправке изменить адрес, на который отправляются данные:

function sendForm(e){ // получаем значение поля key const keyBox = document.search.key; const val = keyBox.value; if(val.length > 3){ alert("Недопустимая длина строки"); document.search.action="PostForm"; } else alert("Отправка разрешена"); }

В данном случае, если длина текста меньше 3 символов, то текст отправляется, только теперь он отправляется по адресу PostForm, поскольку задано свойство action:

document.search.action="PostForm";

Очистка формы

Для очистки формы предназначены следующие равноценные по функциональности кнопки:

<button type="reset">Очистить</button> <input type="reset" value="Очистить" />

При нажатию на кнопки произойдет очистка форм. Но также функциональность по очистке полей формы можно реализовать с помощью метода reset():

function sendForm(e){ // получаем значение поля key const keyBox = document.search.key; const val = keyBox.value; if(val.length < 3){ alert("Недопустимая длина строки"); document.search.reset(); e.preventDefault(); } else alert("Отправка разрешена"); }

Кроме специальных кнопок отправки и очистки на форме также может использоваться обычная кнопка:

<input type="button" name="send" value="Отправить" />

При нажатии на подобную кнопку отправки данных не происходит, хотя также генерируется событие click:

<body> <form name="search"> <input type="text" name="key" placeholder="Введите ключ"></input> <input type="button" name="print" value="Печать" /> </form> <div id="printBlock"></div> <script> function printForm(e){ // получаем значение поля key const keyBox = document.search.key; const val = keyBox.value; // получаем элемент printBlock const printBlock = document.getElementById("printBlock"); // создаем новый параграф const pElement = document.createElement("p"); // устанавливаем у него текст pElement.textContent = val; // добавляем параграф в printBlock printBlock.appendChild(pElement); } const printButton = document.search.print; printButton.addEventListener("click", printForm); </script> </body>

При нажатии на кнопку получаем введенный в текстовое поле текст, создаем новый элемент параграфа для этого текста и добавляем параграф в элемент printBlock.

📜 Текстовые поля

Для ввода простейшей текстовой информации предназначены элементы <input type="text":

<input type="text" name="kye" size="10" maxlength="15" value="hello world" />

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

  • focus: происходит при получении фокуса
  • blur: происходит при потере фокуса
  • change: происходит при изменении значения поля
  • input: происходит при изменении значения поля
  • select: происходит при выделении текста в текстовом поле
  • keydown: происходит при нажатии клавиши клавиатуры
  • keypress: происходит при нажатии клавиши клавиатуры для печатаемых символов
  • keyup: происходит при отпускании ранее нажатой клавиши клавиатуры

Применим ряд событий:

<body> <form name="search"> <input type="text" name="key" placeholder="Введите ключ" /> <input type="button" name="print" value="Печать" /> </form> <div id="printBlock"></div> <script> const keyBox = document.search.key; // обработчик изменения текста function onchange(e){ // получаем элемент printBlock const printBlock = document.getElementById("printBlock"); // получаем новое значение const val = e.target.value; // установка значения printBlock.textContent = val; } // обработка потери фокуса function onblur(e){ // получаем его значение и обрезаем все пробелы const text = keyBox.value.trim(); if(text==="") keyBox.style.borderColor = "red"; else keyBox.style.borderColor = "green"; } // получение фокуса function onfocus(e){ // установка цвета границ поля keyBox.style.borderColor = "blue"; } keyBox.addEventListener("change", onchange); keyBox.addEventListener("blur", onblur); keyBox.addEventListener("focus", onfocus); </script> </body>

Здесь к текстовому полю прикрепляется три обработчика для событий blur, focus и change. Обработка события change позволяет сформировать что-то вроде привязки: при изменении текста весь текст отображается в блоке printBlock. Но надо учитывать, что событие change возникает не сразу после изменения текста, а после потери им фокуса.

Обработка события потери фокуса blur позволяет провести валидацию введенного значения. Например, в данном случае если текст состоит из пробелов или не был введен, то окрашиваем границу поля в красный цвет.

Кроме события change мы можем обрабатывать изменение введенного текста, обрабатывая событие input. Но если событие change возникает, когда пользователь закончит ввод и переведт фокус с текстового поля на другой элемент, то событие input возникает сразу при вводе нового символа или удаления имеющегося:

const keyBox = document.search.key; // обработчик изменения текста function oninput(e){ // получаем элемент printBlock const printBlock = document.getElementById("printBlock"); // получаем новое значение const val = e.target.value; // установка значения printBlock.textContent = val; } keyBox.addEventListener("input", oninput);

Поле ввода пароля

Кроме данного текстового поля есть еще специальные поля ввода. Так, поле <input type="password" предназначено для ввода пароля. По функциональности оно во многом аналогично обычному текстовому полю за тем исключением, что для вводимых символов используется маска:

<input type="password" name="password" />

Однако само вводимое значение никак не шифруется, и мы его можем спокойно получить:

<body> <form name="loginForm"> <input type="password" name="password" /> </form> <div id="printBlock"></div> <script> const passwordBox = document.loginForm.password; // обработчик изменения текста function oninput(e){ // получаем элемент printBlock const printBlock = document.getElementById("printBlock"); // получаем новое значение printBlock.textContent = e.target.value; } passwordBox.addEventListener("input", oninput); </script> </body>

Скрытое поле

Если нам надо, чтобы на форме было некоторое значение, но чтобы оно было скрыто от пользователя, то для этого могут использоваться скрытые поля:

<input type="hidden" name="id" value="345" />

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

Элемент textarea

Для создания многострочных текстовых полей используется элемент textarea:

<textarea rows="15" cols="40" name="textArea"></textarea>

Данные элемент генерирует все те же самые события, что и обычное текстовое поле:

<body> <form name="search"> <textarea rows="7" cols="40" name="message"></textarea> </form> <div id="printBlock"></div> <script> const messageBox = document.search.message; // обработчик ввода символа function onkeypress(e){ // получаем элемент printBlock const printBlock = document.getElementById("printBlock"); // получаем введенный символ const val = String.fromCharCode(e.keyCode); // добавление символа printBlock.textContent += val; } function onkeydown(e){ if(e.keyCode===8){ // если нажат Backspace // получаем элемент printBlock const printBlock = document.getElementById("printBlock"), length = printBlock.textContent.length; // обрезаем строку по последнему символу printBlock.textContent = printBlock.textContent.substring(0, length-1); } } messageBox.addEventListener("keypress", onkeypress); messageBox.addEventListener("keydown", onkeydown); </script> </body>

Здесь к текстовому полю прикрепляются обработчики для событий keypress и keydown. В обработчике keypress получаем введенный символ с помощью конвертации числового кода клавиши в строку:

const val = String.fromCharCode(e.keyCode);

Затем символ добавляется к содержимому блока printBlock.

Событие keypress возникает при нажатии на клавиши для печатаемых символов, то такие символы отображаются в текстовом поле. Однако есть и другие клавиши, которые оказывают влияние на текстовое поле, но они не дают отображаемого символа, поэтому не отслеживаются событием keypress. К таким клавишам относится клавиша Backspace, которая удаляет последний символ. И для ее отслеживания также обрабатываем событие keydown. В обработчике keydown удаляем из строки в блоке printBlock последний символ.

📜 Флажки (чекбоксы) и радиокнопки

Особую группу элементов ввода составляют флажки (чекбоксы) и радиокнопки.

Флажки (чекбоксы)

Флажки представляют поле, в которое можно поставить отметки и которое создается с помощью элемента <input type="checkbox". Отличительную особенность флажка составляет свойство checked, которое в отмеченном состоянии принимает значение true:

<form name="myForm"> <input type="checkbox" name="enabled" checked><span>Включить</span> </form> <div id="printBlock"></div> <script> const enabledBox = document.myForm.enabled; const printBlock = document.getElementById("printBlock"); // в текст printBlock передаем установленное значение enabledBox.addEventListener("click", (e)=> printBlock.textContent = e.target.checked); </script>

Нажатие на флажок генерирует событие click. В данном случае при обработке данного события мы просто выводим информацию, отмечен ли данный флажок, в блок div.

Радиокнопки

Радиокнопки представляют группы кнопок, из которых мы можем выбрать только одну. Радиокнопки создаются элементом <input type="radio".

Выбор или нажатие на одну из них также представляет событие click:

<form name="myForm"> <input type="radio" name="languages" value="Java" /><span>Java</span> <input type="radio" name="languages" value="C#" /><span>C#</span> <input type="radio" name="languages" value="C++" /><span>C++</span> </form> <div id="printBlock"></div> <script> const printBlock = document.getElementById("printBlock"); const myForm = document.myForm; function onclick(e){ printBlock.textContent = `Вы выбрали: ${language}`; } for (let i = 0; i < myForm.languages.length; i++) { myForm.languages[i].addEventListener("click", onclick); } </script>

При создании группы радиокнопок их атрибут name должен иметь одно и то же значение. В данном случае это - languages. То есть радиокнопки образуют группу languages.

Поскольку радиокнопок может быть много, то при прикреплении к ним обработчика события нам надо пробежаться по всему массиву радиокнопок, который можно получить по имени группы:

for (let i = 0; i < myForm.languages.length; i++) { myForm.languages[i].addEventListener("click", onclick); }

Значение выбранного радиокнопки также можно получить через объект Event: e.target.value

Каждая радиокнопка также, как и флажок, имеет свойство checked, которое возвращает значение true, если радиокнопка отмечена. Например, отметим последнюю радиокнопку:

myForm.languages[myForm.languages.length-1].checked = true;

📜 Список select

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

<select name="language" size="4"> <option value="JS" selected="selected">JavaScript</option> <option value="Java">Java</option> <option value="C#">C#</option> <option value="C++">C++</option> </select>

Атрибут size позволяет установить, сколько элементов будут отображаться одномоментно в списке. Значение size="1" отображает только один элемент списка, а сам список становится выпадающим. Если установить у элемента select атрибут multiple, то в списке можно выбрать сразу несколько значений.

Каждый элемент списка представлен html-элементом option, у которого есть отображаемая метка и есть значения в виде атрибута value.

В JavaScript элементу select соответствует объект HTMLSelectElement, а элементу option - объект HtmlOptionElement или просто Option.

Все элементы списка в javascript доступны через коллекцию options. А каждый объект HtmlOptionElement имеет свойства:

  • index (индекс в коллекции options),
  • text (отображаемый текст)
  • value (значение элемента)
Например, получим первый элемент списка и выведем всю информацию о нем через его свойства:

<form name="myForm"> <select name="language" size="4"> <option value="JS" selected="selected">JavaScript</option> <option value="Java">Java</option> <option value="CS">C#</option> <option value="CPP">C++</option> </select> </form> <script> const firstLanguage = document.myForm.language.options[0]; console.log("Index:", firstLanguage.index); // Index: 0 console.log("Text:", firstLanguage.text); // Text: JavaScript console.log("Value:", firstLanguage.value); // Value: JS </script>

Другой способ получить нужный элемент списка по индексу представляет метод item(), в который передается индекс элемента:

const firstLanguage = myForm.language.item(0); console.log("Index:", firstLanguage.index); // Index: 0 console.log("Text:", firstLanguage.text); // Text: JavaScript console.log("Value:", firstLanguage.value); // Value: JS

Динамическое управление списком

В javascript мы можем не только получать элементы, но и динамически управлять списком. Например, применим добавление и удаление объектов списка:

<body> <form name="myForm"> <select name="language" size="5"> <option value="JS" selected="selected">JavaScript</option> <option value="Java">Java</option> <option value="CS">C#</option> <option value="CPP">C++</option> </select> <p><input type="text" name="textInput" placeholder="Введите текст" /></p> <p><input type="text" name="valueInput" placeholder="Введите значение" /></p> <p> <input type="button" name="addButton" value="Добавить" /> <input type="button" name="removeButton" value="Удалить" /> </p> </form> <script> const myForm = document.myForm; const addButton = myForm.addButton, removeButton = myForm.removeButton, languagesSelect = myForm.language; // обработчик добавления элемента function addOption(){ // получаем текст для элемента const text = myForm.textInput.value; // получаем значение для элемента const value = myForm.valueInput.value; // создаем новый элемента const newOption = new Option(text, value); languagesSelect.options[languagesSelect.options.length]=newOption; } // обработчик удаления элемент function removeOption(){ const selectedIndex = languagesSelect.options.selectedIndex; // удаляем элемент languagesSelect.options[selectedIndex] = null; } addButton.addEventListener("click", addOption); removeButton.addEventListener("click", removeOption); </script> </body>

Для добавления на форме предназначены два текстовых поля (для текстовой метки и значения элемента option) и кнопка. Для удаления выделенного элемента предназначена еще одна кнопка.

За добавление в коде javascript отвечает функция addOption, в которой получаем введенные в текстовые поля значения, создаем новый объект Option и добавляем его в массив options объекта списка.

За удаление отвечает функция removeOption, в которой просто получаем индекс выделенного элемента с помощью свойства selectedIndex и в коллекции options приравниваем по этому индексу значение null.

Для добавления/удаления также в качестве альтернативы можно использовать методы элемента select:

// вместо вызова // languagesSelect.options[languagesSelect.options.length]=newOption; // использовать для добавления вызов метода add languagesSelect.add(newOption); // вместо вызова // languagesSelect.options[selectedIndex] = null; // использовать для удаления метод remove languagesSelect.remove(selectedIndex);

События элемента select. Обработка выбора в списке

Элемент select поддерживает три события:

  • blur (потеря фокуса),
  • focus (получение фокуса)
  • change (изменение выделенного элемента в списке)
Рассмотрим применение события select:

<form name="myForm"> <select name="language" size="5"> <option value="JS" selected="selected">JavaScript</option> <option value="Java">Java</option> <option value="CS">C#</option> <option value="CPP">C++</option> </select> </form> <div id="selection"></div> <script> const languagesSelect = document.myForm.language; const selection = document.getElementById("selection"); function changeOption(){ const selectedOption = languagesSelect.options[languagesSelect.selectedIndex]; selection.textContent = "Вы выбрали: " + selectedOption.text; } languagesSelect.addEventListener("change", changeOption); </script>

Список со множественным выбором

Если у элемента <select> установлен атрибут multiple, то список позволяет выбрать несколько элементов. В этом случае для получения всех выделенных элементов необходимо использовать свойство selectedOptions, которое представляет объект типа HTMLCollection и содержит список выбранных элементов. А каждый объект в этом списке имеет тип HTMLOptionElement. Соответственно для получения каждого из выбранных элементов нам надо перебрать эту коллекцию:

<form name="myForm"> <select name="languages" multiple> <option value="JS">JavaScript</option> <option value="Java">Java</option> <option value="CS">C#</option> <option value="CPP">C++</option> </select> </form> <div id="selection"></div> <script> const languages = document.myForm.languages; const selection = document.getElementById("selection"); function changeOption(){ // удаляем ранее выбранные элементы while (selection.firstChild) { selection.removeChild(selection.firstChild); } // получаем выбранные элементы const options = languages.selectedOptions; for (let i = 0; i < options.length; i++) { // for each option ... const option = options[i].text // получаем выбранный элемент // для каждого выбранного элемента создаем div const div = document.createElement("div"); // создаем текстовый узел для выбранного элемента const optionText = document.createTextNode(option); div.appendChild(optionText); // добавляем optionText в div selection.appendChild(div) // добавляем div в контейнер } } languages.addEventListener("change", changeOption); </script>

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

📜 Получение и установка значений

Текстовые поля (input, textarea)

const input = document.querySelector('input[name="username"]'); // Получить значение console.log(input.value); // "John" // Установить значение input.value = 'Alice'; // Очистить поле input.value = ''; // Проверка на пустоту if (input.value.trim() === '') { console.log('Поле пустое'); }

Checkbox (флажки)

const checkbox = document.querySelector('input[type="checkbox"]'); // Проверить, отмечен ли console.log(checkbox.checked); // true/false // Установить/снять галочку checkbox.checked = true; checkbox.checked = false; // Обработчик изменения checkbox.addEventListener('change', (e) => { if (e.target.checked) { console.log('Отмечено'); } else { console.log('Снято'); } });

Radio buttons (переключатели)

// HTML: // <input type="radio" name="gender" value="male"> Мужской // <input type="radio" name="gender" value="female"> Женский const radios = document.querySelectorAll('input[name="gender"]'); // Получить выбранное значение function getSelectedRadio(name) { const radio = document.querySelector(`input[name="${name}"]:checked`); return radio ? radio.value : null; } console.log(getSelectedRadio('gender')); // "male" или "female" // Установить значение document.querySelector('input[value="male"]').checked = true; // Обработчик для всех radio radios.forEach(radio => { radio.addEventListener('change', (e) => { console.log('Выбрано:', e.target.value); }); });

Select (выпадающий список)

const select = document.querySelector('select'); // Получить значение выбранного option console.log(select.value); // "2" // Получить текст выбранного option console.log(select.options[select.selectedIndex].text); // "Два" // Установить значение select.value = '3'; // Обработчик изменения select.addEventListener('change', (e) => { console.log('Выбрано:', e.target.value); console.log('Текст:', e.target.options[e.target.selectedIndex].text); }); // Добавить новый option const option = document.createElement('option'); option.value = '4'; option.text = 'Четыре'; select.add(option); // Удалить option select.remove(0); // Удалить первый option

Множественный выбор (multiple select)

// <select name="colors" multiple> const select = document.querySelector('select[multiple]'); // Получить все выбранные значения function getSelectedValues(select) { return Array.from(select.selectedOptions).map(option => option.value); } console.log(getSelectedValues(select)); // ["red", "blue"] // Установить множественный выбор Array.from(select.options).forEach(option => { if (option.value === 'red' || option.value === 'blue') { option.selected = true; } });

📜 Отправка формы

Событие submit

const form = document.querySelector('form'); form.addEventListener('submit', (e) => { e.preventDefault(); // Отменить стандартную отправку console.log('Форма отправлена'); // Получить данные формы const formData = new FormData(form); // Вывести все данные for (let [key, value] of formData.entries()) { console.log(`${key}: ${value}`); } }); // Программная отправка формы form.submit(); // Событие submit НЕ сработает! form.requestSubmit(); // Событие submit сработает (современный способ)

Сбор данных формы

const form = document.querySelector('form'); // Способ 1: FormData (рекомендуется) const formData = new FormData(form); // Преобразовать в объект const data = Object.fromEntries(formData); console.log(data); // {username: "John", email: "john@example.com"} // Способ 2: Ручной сбор function getFormData(form) { const data = {}; const inputs = form.querySelectorAll('input, select, textarea'); inputs.forEach(input => { if (input.type === 'checkbox') { data[input.name] = input.checked; } else if (input.type === 'radio') { if (input.checked) { data[input.name] = input.value; } } else { data[input.name] = input.value; } }); return data; } console.log(getFormData(form));

📜 Validation API. Валидация элементов формы

HTML5 валидация

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

  • required требует, чтобы поле ввода обязательно содержало какое-нибудь значение
  • max задает максимальное числовое значение (для ввода числовых данных)
  • min задает минимальное числовое значение (для ввода числовых данных)
  • maxlength задает максимальную длину строки
  • minlength задает минимальную длину строки

Например, возьмем следующую страницу:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>VALIDATION TEST</title> <style> input {width: 150px;} input:invalid {border-color: red; } input:valid { border-color: green;} </style> </head> <body> <form id="registerForm" name="registerForm" method="post" action="register"> <p> <label for="username">Username:</label><br> <input id="username" name="username" maxlength="20" minlength="3" required> </p> <p> <label for="email">Email:</label><br> <input type="email" id="email" name="email" required> </p> <p> <label for="age">Age:</label><br> <input type="number" id="age" name="age" min="1" max="110" value="18"> </p> <button type="submit" id="submit" name="submit">Register</button> </form> </body> </html>

Здесь на форме определено поле username для ввода условного имени пользователя. Причем это имя должно иметь не меньше 3 и не больше 20 символов. Поля для ввода имени и email обязательны для заполнения (имеют атрибут required). Также для поля age, которое предназначено для ввода условного возраста, установлено минимальное и максимальное допустимые значения - 1 и 110 соответственно.

Также стоит отметить, что с помощью селектора input:invalid можно определить стиль для невалидных полей, тогда как селектор input:valid задает стиль для полей, которые прошли валидацию.

И если мы введем в поле значение, которое не соответствует атрибутам валидации, или не введем никакого значения в поля, которые требуют наличия ввода, то при попытке отправить форму браузер нам отобразит для соответствующего поля ошибку валидации:

Конкретное сообщение валидации зависит от веб-браузера. Выше приведен пример для Google Chrome. Однако в данном случае нас будет интересовать, как мы можем взаимодействовать с нативной валидацией HTML5 в коде JavaScript. в других браузерах вид может отличаться

Получение информации о валидации в JavaScript

Современные веб-браузеры позволяют взаимодействовать в коде JavaScript с механизмом нативной валидации HTML5. Для этого предназначен специальный API - Constraint Validation API. Этот API определяет ряд свойств, которые можно применять к формам или элементам форм и которые позволяют получить состояние валидации элементов:

  • willValidate: возвращает булевое значение, которое указывает, доступна ли валидация для элемента формы. Если валидация доступна, то возвращается true, при недоступности возвращается false. Например, если для элемента формы установлен атрибут disabled, что делает этот элемент недоступным для взаимодействия, то валидация для него также недоступна. Для других элементов (не элементов формы) возвращается значение undefined
  • validity: возвращает объект типа ValidityState, который, в свою очередь, содержит информацию о валидации данного элемента формы. Свойства ValidityState:
    • valid: возвращает булевое значение, которое указывает, проходит ли элемент формы валидацию (true) или нет (false
    • valueMissing: возвращает true, если в элементе формы, который требует обязательного ввода, отсутствует значение
    • typeMismatch: возвращает true, если введенное значение не соответствует типу элемента формы (например, вмент <input type="email"> введен текст, которые не является адресом элементронной почты)
    • patternMismatch: возвращает true, если введенное значение не соответствует указанному шаблону
    • tooLong: возвращает true, если введенное значение превышает максимально допустимый лимит
    • tooShort: возвращает true, если введенное значение меньше минимально допустимого значения
    • rangeUnderflow: возвращает true, если введенное значение меньше диапазона допустимых значений
    • rangeOverflow: возвращает true, если введенное значение превышает диапазон допустимых значений
    • stepMismatch: возвращает true, если введенное значение не соответствует значению атрибута step
    • badInput: возвращает true, если введенное значение некорректно
    • customError: возвращает true, если при вводе была сгенерирована кастомная ошибка
  • validationMessage: содержит сообщение об ошибке валидации для текущего элемента формы. Конкретное сообщение зависит от используемого веб-браузера

Применим некоторые из этих свойств для проверки ввода в элемент формы:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>FORM VALIDATION</title> <style> input {width: 150px;} input:invalid {border-color: red; } input:valid { border-color: green;} </style> </head> <body> <form id="registerForm" name="registerForm" method="post" action="register"> <p> <label for="email">E-mail:</label><br> <input type="email" id="email" name="email" required> </p> <button type="submit" id="submit" name="submit">Register</button> </form> <script> const emailField = document.getElementById("email"); emailField.addEventListener("change", validateEmail); function validateEmail() { console.log("Может валидироваться:", emailField.willValidate); console.log("Значение отсутствует:", emailField.validity.valueMissing); console.log("Значение валидно:", emailField.validity.valid); console.log("Значение соответствует типу:", emailField.validity.typeMismatch); console.log(emailField.validationMessage); } </script> </body> </html> // Может валидироваться: true // Значение отсутствует: false // Значение валидно: false // Значение соответствует типу: true // Адрес электронной почты должен содержать символ "@" // В адресе "t" отсутствует символ "@"

Благодаря этому мы можем производить дополнительную обработку информации о валидации, например, выводить ошибки валидации на страницу:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> <style> input {width: 150px;} input:invalid {border-color: red; } input:valid { border-color: green;} #emailErrors {padding:5px;background-color: #ffcccc; color:#b33939; display:none;} </style> </head> <body> <form id="registerForm" name="registerForm" method="post" action="register"> <p> <label for="email">E-mail:</label><br> <input type="email" id="email" name="email" required> <div id="emailErrors"></div> </p> <button type="submit" id="submit" name="submit">Register</button> </form> <script> const emailField = document.getElementById("email"); const emailErrors = document.getElementById("emailErrors"); emailField.addEventListener("change", validateEmail); function validateEmail() { if(!emailField.validity.valid){ emailErrors.textContent = emailField.validationMessage; emailErrors.style.display = "block"; } else{ emailErrors.textContent = ""; emailErrors.style.display = "none"; } } </script> </body> </html>

Здесь при наличии ошибки валидации в блок emailErrors помещаем данное сообщение:

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

function validateEmail() { if(!emailField.validity.valueMissing){ emailErrors.textContent = "Отстуствет email!"; emailErrors.style.display = "block"; } else{ emailErrors.textContent = ""; emailErrors.style.display = "none"; } }

Еще пример валидации:

// HTML атрибуты валидации: // - required - обязательное поле // - minlength, maxlength - длина // - min, max - числовые границы // - pattern - регулярное выражение // - type="email" - проверка email // - type="url" - проверка URL <input type="text" name="username" required minlength="3" maxlength="20"> <input type="email" name="email" required> <input type="number" name="age" min="18" max="100"> <input type="text" name="phone" pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}"> // Проверка валидности const input = document.querySelector('input[name="username"]'); console.log(input.validity.valid); // true/false console.log(input.validity.valueMissing); // Поле пустое console.log(input.validity.tooShort); // Слишком короткое console.log(input.validity.tooLong); // Слишком длинное console.log(input.validity.typeMismatch); // Неверный тип (email, url) console.log(input.validity.patternMismatch); // Не соответствует pattern // Сообщение об ошибке console.log(input.validationMessage);

JavaScript валидация

const form = document.querySelector('form'); form.addEventListener('submit', (e) => { e.preventDefault(); const username = form.elements.username.value.trim(); const email = form.elements.email.value.trim(); const password = form.elements.password.value; // Очистить предыдущие ошибки clearErrors(); let isValid = true; // Проверка username if (username === '') { showError('username', 'Имя пользователя обязательно'); isValid = false; } else if (username.length < 3) { showError('username', 'Минимум 3 символа'); isValid = false; } // Проверка email if (email === '') { showError('email', 'Email обязателен'); isValid = false; } else if (!isValidEmail(email)) { showError('email', 'Некорректный email'); isValid = false; } // Проверка пароля if (password === '') { showError('password', 'Пароль обязателен'); isValid = false; } else if (password.length < 6) { showError('password', 'Минимум 6 символов'); isValid = false; } if (isValid) { console.log('Форма валидна!'); // Отправить данные на сервер } }); function showError(fieldName, message) { const field = document.querySelector(`[name="${fieldName}"]`); const error = document.createElement('div'); error.className = 'error-message'; error.textContent = message; field.parentElement.appendChild(error); field.classList.add('error'); } function clearErrors() { document.querySelectorAll('.error-message').forEach(el => el.remove()); document.querySelectorAll('.error').forEach(el => el.classList.remove('error')); } function isValidEmail(email) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); }

Пользовательские сообщения валидации

const input = document.querySelector('input[name="username"]'); input.addEventListener('invalid', (e) => { e.preventDefault(); // Отменить стандартное сообщение if (input.validity.valueMissing) { input.setCustomValidity('Пожалуйста, введите имя пользователя'); } else if (input.validity.tooShort) { input.setCustomValidity(`Минимум ${input.minLength} символов`); } }); input.addEventListener('input', () => { input.setCustomValidity(''); // Сбросить сообщение при вводе });

📜 Управление валидацией форм

Методы валидации

В прошлой теме было рассмотрено, как получить состояние с помощью свойств Constraint Validation API. Но в дополнение к свойствам Constraint Validation API предоставляет ряд методов, которые позволяют управлять валидацией:

  • checkValidity(): проверяет, проходит ли элемент формы или вся форма валидацию. Этот метод можно вызвать как для формы, так и для отдельных ее элементов. Элемент формы является валидным, если он удовлетворяет всем атрибутам валидации. Форма является валидной, если все ее элементы проходят валидацию. Если форма или ее элементы проходят валидацию, то возвращается true, иначе возвращается false
  • reportValidity(): также проверяет, проходит ли элемент формы или вся форма валидацию. Однако в отличие от checkValidity() этот метод также отображает ошибки валидации. Этот метод также можно вызвать как для формы, так и для отдельных ее элементов.
  • setCustomValidity(): этот метод позволяет настроить сообщения валидации

Например, проверка валидности формы и ее элементов с помощью checkValidity():

<form id="registerForm" name="registerForm" method="post" action="register"> <p> <label for="username">Username:</label><br> <input id="username" name="username" maxlength="20" minlength="3" required> </p> <p> <label for="age">Age:</label><br> <input type="number" id="age" name="age" min="1" max="110" required> </p> <button type="submit" id="submit" name="submit">Register</button> </form> <script> const registerForm = document.registerForm; const submit = registerForm.submit; submit.addEventListener("click", validate); function validate(){ if(!registerForm.username.checkValidity()){ console.log("Username is not valid"); } if(!registerForm.age.checkValidity()){ console.log("Age is not valid"); } if(!registerForm.checkValidity()){ console.log("Form data is not valid"); } } </script>

Для элементов вызов этого метода аналогичен проверке валидности с помощью свойства validity.valid:

function validate(){ if(!registerForm.username.validity.valid){ console.log("Username is not valid"); } if(!registerForm.age.validity.valid){ console.log("Age is not valid"); } if(!registerForm.checkValidity()){ console.log("Form data is not valid"); } }

Настройка собственных сообщений валидации

Для настройки своих сообщений валидации в метод setCustomValidity() передается необходимое сообщение:

<form id="registerForm" name="registerForm"> <p> <label for="username">Username:</label><br> <input id="username" name="username" maxlength="20" minlength="3" required> </p> <button type="submit" id="submit" name="submit">Register</button> </form> <script> const registerForm = document.registerForm; const submit = registerForm.submit; submit.addEventListener("click", validate); function validate(){ if(registerForm.username.validity.valueMissing){ registerForm.username.setCustomValidity("Необходимо ввести имя пользователя"); } if(registerForm.username.validity.tooLong){ registerForm.username.setCustomValidity("Имя пользователя не должно превышать 20 символов"); } if(registerForm.username.validity.tooShort){ registerForm.username.setCustomValidity("Имя пользователя не должно быть меньше 3 символов"); } } </script>

Здесь при отправке формы проверяем значение поля username. В зависимости от того, какое правило валидации не соблюдается, устанавливаем соответствующее сообщение об ошибке. И браузер также будет использовать эти сообщения:

Определение своих правил валидации

При валидации мы не ограничены встроенными правилами валидации, которые применяются к элементу формы с помощью атрибутов required, minlength, maxlength, min, max, либо в зависимости от типа поля ввода. При необходимости мы можем задавать свою логику валидации для кастомных сценарией. Например, возьмем простейший пример: имя пользователя у нас не должно быть равно "admin". Для этого определим следующую программу:

<form id="registerForm" name="registerForm"> <p> <label for="username">Username:</label><br> <input id="username" name="username" maxlength="20" minlength="3" required> </p> <button type="submit" id="submit" name="submit">Register</button> </form> <script> const usernameField = document.registerForm.username; const submit = registerForm.submit; submit.addEventListener("click", validate); function validate(){ if(usernameField.value === "admin"){ usernameField.setCustomValidity("Недопустимое имя пользователя"); } } </script>

В функции validate() проверяем значение поля usernameField. Если оно равно "admin", то устанавливаем сообщение об ошибке.

Поскольку мы установили сообщение об ошибке, то поле username уже не проходит валидацию, даже если оно соответствует атрибутам required. maxlength и minlength. Соотвественно далее мы можем получить это сообщение через свойство validationMessage:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>CASTOM MESSAGE VALIDATION</title> <style> input {width: 150px;} input:invalid {border-color: red; } input:valid { border-color: green;} #usernameErrors {padding:5px;background-color: #ffcccc; color:#b33939; display:none;} </style> </head> <body> <form id="registerForm" name="registerForm"> <p> <label for="username">Username:</label><br> <input id="username" name="username" maxlength="20" minlength="3" required> <div id="usernameErrors"></div> </p> <button type="submit" id="submit" name="submit">Register</button> </form> <script> const usernameErrors = document.getElementById("usernameErrors"); const usernameField = document.registerForm.username; const submit = registerForm.submit; submit.addEventListener("click", validate); function validate(e){ if(usernameField.value === "admin"){ usernameField.setCustomValidity("Недопустимое имя пользователя"); } // проверяем на валидацию if(!usernameField.validity.valid){ usernameErrors.textContent = usernameField.validationMessage; usernameErrors.style.display = "block"; } else{ usernameErrors.textContent = ""; usernameErrors.style.display = "none"; e.preventDefault(); // предупреждаем отправку форму и перезагрузку страницы } } </script> </body> </html>

📜 Работа с файлами

Загрузка файла

const fileInput = document.querySelector('input[type="file"]'); fileInput.addEventListener('change', (e) => { const file = e.target.files[0]; // Первый файл if (file) { console.log('Имя:', file.name); console.log('Размер:', file.size, 'байт'); console.log('Тип:', file.type); console.log('Дата изменения:', file.lastModified); // Проверка размера (макс 5MB) if (file.size > 5 * 1024 * 1024) { alert('Файл слишком большой (макс 5MB)'); e.target.value = ''; // Очистить return; } // Проверка типа const allowedTypes = ['image/jpeg', 'image/png', 'image/gif']; if (!allowedTypes.includes(file.type)) { alert('Разрешены только изображения (JPEG, PNG, GIF)'); e.target.value = ''; return; } } });

Множественная загрузка

// <input type="file" multiple> const fileInput = document.querySelector('input[type="file"][multiple]'); fileInput.addEventListener('change', (e) => { const files = Array.from(e.target.files); console.log(`Выбрано файлов: ${files.length}`); files.forEach(file => { console.log(`${file.name} - ${file.size} байт`); }); });

Чтение файла

const fileInput = document.querySelector('input[type="file"]'); fileInput.addEventListener('change', (e) => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); // Прочитать как текст reader.readAsText(file); // Когда файл прочитан reader.onload = (e) => { console.log('Содержимое:', e.target.result); }; // Обработка ошибки reader.onerror = () => { console.error('Ошибка чтения файла'); }; } }); // Чтение изображения fileInput.addEventListener('change', (e) => { const file = e.target.files[0]; if (file && file.type.startsWith('image/')) { const reader = new FileReader(); reader.readAsDataURL(file); // Прочитать как Data URL reader.onload = (e) => { const img = document.createElement('img'); img.src = e.target.result; document.body.appendChild(img); }; } });

📜 Отправка формы через AJAX

С использованием Fetch API

const form = document.querySelector('form'); form.addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(form); try { const response = await fetch('/api/submit', { method: 'POST', body: formData }); if (response.ok) { const data = await response.json(); console.log('Успех:', data); form.reset(); // Очистить форму } else { console.error('Ошибка:', response.status); } } catch (error) { console.error('Ошибка сети:', error); } }); // Отправка JSON form.addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(form); const data = Object.fromEntries(formData); try { const response = await fetch('/api/submit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); if (response.ok) { const result = await response.json(); console.log('Успех:', result); } } catch (error) { console.error('Ошибка:', error); } });

📜 Автосохранение формы

const form = document.querySelector('form'); // Загрузить сохранённые данные function loadFormData() { const savedData = localStorage.getItem('formData'); if (savedData) { const data = JSON.parse(savedData); Object.keys(data).forEach(key => { const input = form.elements[key]; if (input) { if (input.type === 'checkbox') { input.checked = data[key]; } else { input.value = data[key]; } } }); } } // Сохранить данные формы function saveFormData() { const formData = new FormData(form); const data = Object.fromEntries(formData); localStorage.setItem('formData', JSON.stringify(data)); } // Автосохранение при вводе form.addEventListener('input', () => { saveFormData(); }); // Загрузить при загрузке страницы loadFormData(); // Очистить при успешной отправке form.addEventListener('submit', (e) => { e.preventDefault(); // ... отправка формы ... localStorage.removeItem('formData'); });
✅ Best Practices
  • Всегда используйте e.preventDefault() при обработке submit
  • Валидируйте данные и на клиенте, и на сервере
  • Используйте FormData для сбора данных
  • Отключайте кнопку отправки во время обработки
  • Показывайте понятные сообщения об ошибках
  • Сохраняйте данные формы при длительном заполнении

Глава 14. Работа с браузером и BOM (Browser Object Model)

📜 Что такое BOM?

BOM (Browser Object Model) — это набор объектов, предоставляемых браузером для работы с окном браузера и его компонентами. В отличие от DOM (работа с документом), BOM позволяет управлять самим браузером.

💡 Основные объекты BOM
  • window — глобальный объект браузера
  • navigator — информация о браузере
  • location — URL текущей страницы
  • history — история переходов
  • screen — информация об экране

В вершине находится главный объект - объект window, который представляет глобальный объект (фактически представляет собой браузер). Этот объект в свою очередь включает ряд других объектов, в частности, объект document, который представляет отдельную веб-страницу, отображаемую в браузере.

📜 Объект window

Объект window представляет собой окно веб-браузера, в котором размещаются веб-страницы. window является глобальным объектом, поэтому при доступе к его свойствам и методам необязательно использовать его имя. Например, объект window имеет метод alert(), который отображает окно сообщения. Но нам необязательно писать:

window.alert("Привет мир!");

window можно не использовать:

alert("Привет мир!");

Но так как данный объект глобальный, то это накладывает некоторые ограничения. Например:

var alert = function(message){ console.log("Сообщение: ", message); }; window.alert("Привет мир!");

С помощью var здесь определяется глобальная переменная alert. Все объявляемые в программе глобальные переменные или функции автоматически добавляются к объекту window. И поскольку название новой функции будет совпадать с названием метода alert(), то произойдет переопределение этого метода в объекте window новой функцией.

И если мы объявим в программе какую-нибудь глобальную переменную, то она нам доступна как свойство в объекте window:

var message = "hello"; console.log(window.message);

window — это глобальный объект в браузере. Все глобальные переменные и функции являются его свойствами.

Свойства window

С помощью свойств объекта window можно получить различную информацию об окне браузера. В частности, для определения положения окна применяются следующие свойства:

  • innerHeight: содержит высоту окна, в том числе горизонтальные полосы прокрутки
  • innerWidth: содержит ширину окна, в том числе вертикальные полосы прокрутки
  • outerHeight: содержит высоту окна браузера, в том числе все полосы прокрутки браузера
  • outerWidth: содержит ширину окна браузера, в том числе все полосы прокрутки браузера
  • pageXOffset: псевдоним для window.scrollX
  • pageYOffset: псевдоним для window.scrollY
  • screenX: содержит позицию окна браузера по оси X, то есть расстояние от окна браузера до левого края экрана
  • screenY: содержит позицию окна браузера по оси X, то есть расстояние от окна браузера до верхнего края экрана
  • scrollX: содержит количество пикселей веб-страницы, прокрученных по горизонтали
  • scrollY: содержит количество пикселей веб-страницы, прокрученных по вертикали

Размеры окна

// Размер окна браузера (без панелей) console.log(window.innerWidth); // Ширина в пикселях console.log(window.innerHeight); // Высота в пикселях // Положение окна console.log(window.screenX); console.log(window.screenY); // Размер окна браузера (включая панели) console.log(window.outerWidth); console.log(window.outerHeight); // Размер области просмотра документа console.log(document.documentElement.clientWidth); console.log(document.documentElement.clientHeight);

Позиция прокрутки

// Текущая прокрутка console.log(window.scrollX); // Горизонтальная console.log(window.scrollY); // Вертикальная // Альтернатива (старый способ) console.log(window.pageXOffset); console.log(window.pageYOffset); // Прокрутить к координатам window.scrollTo(0, 500); // x, y // Прокрутить на заданное расстояние window.scrollBy(0, 100); // На 100px вниз // Плавная прокрутка window.scrollTo({ top: 500, left: 0, behavior: 'smooth' });

Компоненты браузера

Окно браузера обычно состоит из различных компонентов. В зависимости от конкретного браузера и персональных настроек набор конкретных компонентов может отличаться. Но в общем случае типовое окно браузера выглядит следующим образом:

Помимо собственно области содержимого, в которой отображается веб-страница, веб-браузер имеет имеет еще ряд компонентов:

  • адресная строка для ввода URL-адреса или поискового запроса (Address Bar)
  • строка состояния (Status Bar), которая указывает, загружена ли веб-страница или находится в процессе загрузки
  • строка меню (Menu Bar)
  • панель инструментов (ToolBar)
  • некоторая "личная" / "персональная" панель, которая, например, содержит закладк (Personal Bar)
  • полосы прокрутки, которые отображают горизонтальное и вертикальное положение (Horizontal/Vertical ScrollBar)

Объект window имеет ограниченные возможности для взаимодействия с этими компонентами, в частности, для проверки наличия этих компонентов объект window имеет ряд свойств:

  • locationbar: содержит объект, который указывает, отображается адресная строка или нет
  • menubar: указывает, отображается ли панель меню или нет
  • personalbar: указывает, отображается ли персональная панель (например, панель закладок) или нет
  • scrollbars: указывает, отображаются ли полосы прокрутки или нет
  • statusbar: указывает, отображается строка состояния или нет
  • toolbar: указывает, отображается ли панель инструментов или нет

Например, узнаем отображается ли персональная панель:

console.log(window.personalbar); // BarProp {visible: true} или BarProp {visible: false}

Свойство возвратит объект BarProp, в котором свойство visible собственно и указывает, отображается панель или нет.

📜 Объект screen

screen содержит информацию об экране пользователя.

Свойство screen

Для получения информации об экране также применяется свойство screen объекта window. Это свойство представляет объект типа Screen

  • availTop: указывает на высоту фиксированных компонентов, которые примыкают к верхней стороне браузера, например, различных верхних панелей
  • availLeft: указывает на ширину фиксированных компонентов, которые примыкают к левой стороне браузера,, например, различных левых панелей
  • availHeight: содержит максимально доступную высоту в пикселях минус высоту верхних и нижних панелей
  • availWidth: содержит максимально доступную ширину в пикселях минус ширину левых и правых панелей
  • colorDepth: содержит глубину цвета экрана
  • height: содержит высоту экрана в пикселях
  • orientation: содержит объект типа ScreenOrientation, который предоставляет информацию об ориентации устройства
  • pixelDepth: содержит глубину пикселя экрана
  • width: содержит ширину экрана в пикселях

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

console.log(screen.availTop); // 25 console.log(screen.availLeft); // 0 console.log(screen.availHeight); // 695 console.log(screen.availWidth); // 1280 console.log(screen.width); // 1280 console.log(screen.height); // 800 console.log(screen.pixelDepth); // 24 console.log(screen.colorDepth); // 24
// Разрешение экрана console.log(screen.width); // Ширина экрана в пикселях console.log(screen.height); // Высота экрана в пикселях // Доступная область (без панелей ОС) console.log(screen.availWidth); console.log(screen.availHeight); // Глубина цвета console.log(screen.colorDepth); // Бит на пиксель (обычно 24) // Ориентация экрана console.log(screen.orientation.type); // "landscape-primary" или "portrait-primary" // Отслеживание изменения ориентации screen.orientation.addEventListener('change', () => { console.log('Новая ориентация:', screen.orientation.type); });

Для взаимодействия с пользователем в объекте window определен ряд методов, которые позволяют создавать диалоговые окна или взаимодействуют с содержимым окна:

  • alert(): выводит окно с сообщением
  • confirm(): отображает окно с сообщением, в котором пользователь должен подтвердить действие двух кнопок OK и Отмена
  • prompt(): позволяет с помощью диалогового окна запрашивать у пользователя какие-либо данные
  • print(): отображает диалоговое окно для вывода страницы на печать
  • find(): позволяет найти на странице определенный текст

alert

Например, с помощью метода alert() по нажатию на кнопку выведем окно с сообщением:

<body> <button id="btn">Click</button> <script> const btn = document.getElementById("btn"); btn.addEventListener("click", ()=>{ alert("Hello METANIT.COM"); // отображаем всплывающее окно при нажати на кнопку }); </script> </body>

confirm

Метод confirm() отображает окно с сообщением, в котором пользователь должен подтвердить действие двух кнопок OK и Отмена. В зависимости от выбора пользователя метод возвращает true (если пользователь нажал OK) или false (если пользователь нажал кнопку Отмены):

<body> <button id="btn">Click</button> <script> const btn = document.getElementById("btn"); btn.addEventListener("click", ()=>{ const result = confirm("Завершить выполнение программы?"); if(result===true) console.log("Работа программы завершена"); else console.log("Программа продолжает работать"); }); </script> </body>

prompt

Метод prompt() позволяет с помощью диалогового окна запрашивать у пользователя какие-либо данные. Данный метод возвращает введенное пользователем значение. Например, запросим на странице имя пользователя:

<body> <button id="btn">Click</button> <script> const btn = document.getElementById("btn"); btn.addEventListener("click", ()=>{ const name = prompt("Введите свое имя:"); console.log("Ваше имя: ", name) }); </script> </body>

Если пользователь откажется вводить значение и нажмет на кнопку отмены, то метод возвратит значение null.

find

Метод find() позволяет найти на странице текст, который передает в метод через параметр. Метод возвращает true, если текст найден, и false, если текст не найден. Например:

<body> <input id="key" name="key" /> <button id="btn">Find</button> <div> <p>— Ах, виноват-с, Петр Николаич! Я буду тихо, — сказал секретарь и продолжал полушёпотом: — Ну-с, а закусить, душа моя Григорий Саввич, тоже нужно умеючи. Надо знать, чем закусывать. Самая лучшая закуска, ежели желаете знать, селедка. Съели вы ее кусочек с лучком и с горчичным соусом, сейчас же, благодетель мой, пока еще чувствуете в животе искры, кушайте икру саму по себе или, ежели желаете, с лимончиком, потом простой редьки с солью, потом опять селедки, но всего лучше, благодетель, рыжики соленые, ежели их изрезать мелко, как икру, и, понимаете ли, с луком, с прованским маслом... объедение! Но налимья печенка — это трагедия!</p> <p>— М-да... — согласился почетный мировой, жмуря глаза. — Для закуски хороши также, того... душоные белые грибы...</p> </div> <script> const btn = document.getElementById("btn"); const keyField = document.getElementById("key"); btn.addEventListener("click", ()=>{ const result = find(keyField.value); // ищем введенное в поле слово console.log(result); }); </script> </body>

В данном случае по нажатию на кнопку ищем на странице введенный в текстовое поле текст. Если текст найден, то он выделяется.

print

Метод print отображает диалоговое сообщение для вывода страницы на печать:

<body> <button id="btn">Print</button> <p>Hello World</p> <script> const btn = document.getElementById("btn"); const keyField = document.getElementById("key"); btn.addEventListener("click", ()=>{ print(); // выводим текущую страницу на печать }); </script> </body>

В зависимости от браузера окно печати может выглядеть различным образом. Например, вид в Google Chrome:

Диалоговые окна

// alert - информационное сообщение alert('Это сообщение'); // confirm - запрос подтверждения (возвращает true/false) const result = confirm('Вы уверены?'); if (result) { console.log('Пользователь нажал OK'); } else { console.log('Пользователь нажал Отмена'); } // prompt - запрос ввода (возвращает строку или null) const name = prompt('Как вас зовут?', 'Анонимус'); // Второй параметр - значение по умолчанию if (name !== null) { console.log('Привет, ' + name); }
⚠️ Недостатки диалоговых окон
  • Блокируют выполнение скрипта
  • Нельзя кастомизировать
  • Плохой UX (пользовательский опыт)
  • Рекомендуется использовать модальные окна на CSS/JS

📜 Открытие, закрытие и позиционирование окон

Язык JavaScript позволят программно управлять окнами веб-браузера. Для этого объект window предоставляет ряд методов. Так, метод open() открывает определенный ресурс в новом окне или вкладке браузера. Стоит учитывать, что подобное действие лучше выполнять по действию пользователя, например, по нажатию на кнопку, потому что в ином случае браузеры могут заблокировать подобные окна. Например, определим следующую страницу:

<body> <button onclick="openWindow()">Click</button> <script> function openWindow(){ window.open("https://microsoft.com"); } </script> </body>

Здесь на веб-странице определена кнопка - элемент button. У кнопки установлен атрибут onclick, который указывает на функцию javascript, которая будет выполняться по нажатию этой кнопки.

В коде javascript определена функция openWindow(), которая выполняется по нажатию на кнопку. В этой функции выполняется функция window.open(), в которую в качестве первого параметра передается адрес - в данном случае "https://microsoft.com". И по нажатию на кнопку будет открываться в новой вкладке страницы "https://microsoft.com".

Метод open() принимает ряд параметров:

open(); open(url); open(url, target); open(url, target, windowFeatures);

В качестве первого параметра - url передается путь к ресурсу.

Второй параметр - target Открытие в новой или текущей вкладке. Распространенные значения:

  • _self: страница открывается в текущей вкладке
  • _blank: страница открывается в новой вкладке или в отдельном окне

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

window.open("https://metanit.com", "_self");

Третий параметр позволяет установить набор стилевых характеристик окна. Каждая стилевая характеристика определяется в виде наборов name=value, где name - название стилевой характеристики, а value - ее значение. Друг от друга стилевые характеристики отделены запятой.

В частности, можно использовать следующие характеристики:

  • popup: указывает, будет ли открываться страница в отдельном всплывающем окне. Для этого может принимать такие значения, как yes, 1 или true.
  • width / innerWidth: ширина окна в пикселях. Например, width=640
  • height / innerHeight: высота окна в пикселях. Например, height=480
  • left / screenX: координата X относительно начала экрана в пикселях. Например, left=0
  • top / screenY: координата Y относительно начала экрана в пикселях. Например, top=0
  • location: указывает, будет ли отображаться адресная строка. Например, location=yes
  • menubar: указывает, будет ли отображаться панель меню. Например, menubar=yes
  • scrollbars: указывает, будет ли окно иметь полосы прокрутки. Например, scrollbars=yes
  • status: указывает, будет ли отображаться строка состояния. Например, status=yes
  • toolbar: указывает, будет ли отображаться панель инструментов. Например, toolbar=yes

Последние пять параметров в качестве значений могут принимать yes и no, вместо которых также можно использовать 1 и 0 соответственно

Пример применения нескольких параметров:

window.open("https://metanit.com", "_blank", "width=600,height=400,left=500,top=200");

Стоит отметить, что функция возвращает ссылку на окно, и с помощью этой ссылки мы можем управлять окном.

Также стоит отметить, что через свойство opener окна можно получить ссылку на главное окно, из которого было открыто текущее.

// Открыть новое окно const newWindow = window.open('https://example.com', '_blank', 'width=600,height=400'); // Параметры окна const popup = window.open( 'popup.html', 'popupWindow', 'width=400,height=300,left=100,top=100,resizable=yes,scrollbars=yes' ); // Закрыть окно (работает только для окон, открытых через window.open) newWindow.close(); // Проверить, закрыто ли окно if (newWindow.closed) { console.log('Окно закрыто'); } // Установить фокус на окно newWindow.focus(); // Убрать фокус newWindow.blur();

Закрытие окна

С помощью метода close() можно закрыть окно. Например:

<body> <button onclick="openWindow()">Open</button> <button onclick="closeWindow()">Close</button> <script> let metanitWindow = null; function openWindow(){ if(!metanitWindow || metanitWindow.closed){ // если окно не открыто metanitWindow = window.open("https://metanit.com", "_blank", "width=600,height=400,left=500,top=200,popup=yes"); } } function closeWindow(){ metanitWindow?.close(); // если окно открыто, то закрываем его metanitWindow = null; } </script> </body>

Здесь определены две кнопки для открытия и закрытия окна. Ссылка на само окно помещается в переменную metanitWindow, которая изначально равна null. По нажатию на первую кнопку вызывается функция openWindow(). В этой функции проверяем, что metanitWindow не равен null и что окно не закрыто (metanitWindow.closed не равно false). Вторая проверка необходима на случай, если окно будет закрыто нажатием на крестик в самом окне (в этом случае closed=true). И если окно не открыто, открываем его.

По нажатию на вторую кнопку у объекта metanitWindow вызываем метод close и устанавливаем переменную в null.

Управление позицией и размером окна

Для управления/изменения позиции и размера окна объект window предоставляет ряд методов:

  • moveBy(): перемещает текущее окно браузера по горизонтали и вертикали на указанное количество пикселей. Первый параметр определяет перемещение по горизонтали, второй параметр - перемещение по вертикали в пикселях.
  • moveTo(): перемещает текущее окно браузера по горизонтали и вертикали в указанное положение. Первый параметр определяет положение по горизонтали, второй параметр — положение по вертикали в пикселях.
  • resizeBy(): масштабирует текущее окно браузера по горизонтали и вертикали на указанное количество пикселей. Первый параметр определяет значение масштабирования по горизонтали, второй параметр — значение масштабирования по вертикали.
  • resizeTo(): масштабирует текущее окно браузера по горизонтали и вертикали до заданного размера. Первый параметр определяет ширину, второй параметр — высоту.
  • scroll(): прокручивает содержимое окна до указанной позиции. Первый параметр указывает положение по горизонтали, второй параметр — положение по вертикали.
  • scrollBy(): прокручивает содержимое окна на указанный коэффициент. Первый параметр определяет коэффициент прокрутки по горизонтали, второй параметр определяет коэффициент прокрутки по вертикали.
  • scrollTo(): прокручивает содержимое окна до указанной позиции. Первый параметр указывает положение по горизонтали, второй параметр — положение по вертикали.

Примеры управления позицией и размерами окна:

// сдвигаем окно браузера на 200 пикселей по горизонтали и на 100 пикселей по вертикали window.moveBy(200, 100); // Помещаем окно браузера на позицию с координатами (200, 150) window.moveTo(200, 150); // Увеличиваем окно браузера на 200 пиксей в ширину и 100 пикселей в высоту window.resizeBy(200, 100); // Сжимаем окно браузера на 200 пиксей в ширину и 100 пикселей в высоту window.resizeBy(-200, -100); // Прокручиваем контент окна на 100 пикселей по горизонтали и 200 пикселей по вертикали window.scrollBy(100, 200); // Прокручиваем содержимое браузера до позиции (100, 200) window.scrollTo(100, 200);

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

Например, с помощью метода moveTo() переместитм окно на новую позицию:

<body> <button onclick="openWindow()">Open</button> <button onclick="moveWindow()">Move</button> <script> let testWindow = null; // открываем окно function openWindow(){ testWindow = window.open("test.html", "_blank", "width=600,height=400,left=200,top=200"); } // сдвигаем окно function moveWindow(){ testWindow.moveTo(500,400); } </script> </body>

В данном случае по нажатию на кнопку Open открываем окно, а по нажатию на кнопку Move перемещаем его на позицию с координатами x=500, y=400 относительно левого верхнего угла экрана.

📜 История браузера. History API

При навигации между страницами браузер сохраняет всю историю о переходах в специальном стеке, который называется history stack. И каждый раз, когда браузер загружает новую веб-страницу или переходит по ссылке на веб-странице, браузер по умолчанию создает новую запись в истории просмотров. В коде JavaScript историю можно получить через свойство history объекта window. Данное свойство представляет тип History.

Объект History для взаимодействия с историей просмотров предоставляет ряд методов и свойств:

  • Свойство length возвращает количество записей в истории просмотров
    console.log("В истории ", history.length, " записей");
  • Свойство state возвращает текущую запись из истории просмотров. По умолчанию при загрузке первой страницы в браузере это свойство равно null
    console.log(history.state);
  • Метод back() переходит к прошлой записи в истории просмотров, аналогично нажатию на кнопку Назад/Back в браузере
    history.back(); // перемещение назад к прошлой странице
  • Метод forward() переходит к следующей просмотренной странице, аналогично нажатию на кнопку Вперед/Next в браузере
    history.forward(); // перемещение вперед к следующей странице
  • Метод go() позволяет перемещаться вперед и назад по истории на определенное число страниц. Методу передается приращение, начиная с текущей веб-страницы. Например, значение -1 приводит к открытию предыдущей веб-страницы, а значение 1 вызывает открытие следующей веб-страницы. Если передается значение, для которого в истории нет соответствующей веб-страницы, этот метод ничего не делает. Если же метод вызывается без значения или со значением 0, текущая веб-страница перезагружается
    history.go(-2); // переход на 2 страницы назад history.go(2); // переход на 2 страницы вперед history.go(0); // перезагружаем текущую страницу
  • Метод pushState() программно добавляет новую запись в историю просмотров. Он принимает три параметра:
    history.pushState(state, title[, url])
    • Параметр state представляет добавляемый объект в историю просмотров. В качестве такого объекта состояния может быть чем угодно
    • Параметр title устанавливает заголовок. Стоит отметить, что браузеры могут игнорировать этот параметр
    • Параметр url представляет URL-адрес новой записи в истории. Является необязательным. Однако если используется, этот адрес url в этом параметре должен относиться к тому же домену, что и текущая страница. Браузер может устанавливать этот адрес в качестве текущего.

    Простейший пример

    const state = { url: "/", title: "Home", decription: "Home Page" }; // history.pushState(state, state.title); // без url history.pushState(state, state.title, state.url); // с url console.log(state); // {url: "/", title: "Home", decription: "Home Page"}
  • Метод replaceState() программно заменяет текущую запись в истории просмотров на новую. Он принимает те же три параметра:
    history.replaceState(state, title, [url])

    Простейший пример

    const state = { url: "home", title: "Home", decription: "Home Page" }; history.replaceState(state, state.title, state.url);

Событие popstate

Каждый раз, когда текущая запись в истории посещений меняется (например, при нажатии на кнопку "Назад" в браузере), срабатывает событие popstate. Соответственно если мы хотим обрабатывать перемещение по истории просмотров с помощью кнопок браузера Назад/Вперед, то нам надо обрабатывать данное событие.

Для обработки события popstate в обработчик события передается объект события типа PopStateEvent. В этом объекте свойство state указывает на запись, удаленную из истории просмотров:

window.addEventListener("popstate", (event) => { console.log(event.state); // получаем старое состояние });

Перемещение по одностраничному сайту

В качестве примера применения History API определим простейший одностраничный сайт в виде следующей веб-страницы index.html:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>HISTORY EXAMP</title> </head> <body> <nav><a href="#home">Home</a> | <a href="#about">About</a> | <a href="#contacts">Contacts</a></nav> <h1 id="content"></h1> <script> // Контейнер, в который загружаем контент const contentElement = document.getElementById("content"); // Объект, который содержит содержимое для различных страниц const pages = { home: { content: "Home Page", url: "#home"}, about: { content: "About Page", url: "#about" }, contacts: { content: "Contact Page", url: "#contacts"} }; // Обработчик нажатия на ссылки function handleClick(event){ // получаем адрес перехода const url = event.target.getAttribute("href"); // получаем имя страницы, которая совпадает с адресом перехода const pageName = url.split("#").pop(); // получаем страницу из объекта pages const page = pages[pageName]; // если текущий адрес совпадает с запрошенным, то игнорируем переход if(history.state.url != url) { contentElement.textContent = page.content; // добавляем в историю history.pushState(page, // объект state event.target.textContent, // Title event.target.href // URL ); document.title = event.target.textContent; // если браузер не устанавливает заголовок } return event.preventDefault(); } // устанавливаем обработчик для извлечения состояния в History API window.addEventListener("popstate", (event) => { if(event.state) // если есть состояние contentElement.textContent = event.state.content; // получаем старое состояние }); // устанавливаем обработчик нажатия для кнопок const links = document.getElementsByTagName("a"); for (let i = 0; i < links.length; i++) { links[i].addEventListener("click", handleClick, true); } // по умолчанию загружаем Home Page contentElement.textContent = pages.home.content; history.pushState(pages.home, "Home", pages.home.url); </script> </body> </html>

Итак, изначально на странице у нас три ссылки, нажимая на которые, мы будем переходить на условные страницы:

<nav><a href="#home">Home</a> | <a href="#about">About</a> | <a href="#contacts">Contacts</a></nav>

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

<h1 id="content"></h1>

В коде JavaScript мы будем ссылаться на этот элемент через константу contentElement

В коде JavaScript определяем код условных страниц в виде объекта pages:

const pages = { home: { content: "Home Page", url: "#home"}, about: { content: "About Page", url: "#about" }, contacts: { content: "Contact Page", url: "#contacts"} };

Каждый объект однотипен: содержит свойство content, которое представляет содержимое условной страницы, и свойство url - адрес страницы. Собственно состояние history.state будет представлять один из этих объектов. Но тут важная условность - для простоты названия этих страниц - home/about/constact совпадают с адресами ссылок. Можно было бы отвязать названия, но это привело бы к увеличению логики в сугубо демонстрационном примере.

Для обработки нажатия ссылок определяется функция handleClick() , в которую передается объект события. И из этого объекта события через event.target мы можем получить нажатую ссылку и ее данные. Так, в начале получаем адрес ссылки и название страницы (которое равно адресу без начального слеша):

// получаем адрес перехода const url = event.target.getAttribute("href"); // получаем имя страницы, которая совпадает с адресом перехода const pageName = url.split("#").pop(); // получаем страницу из объекта pages const page = pages[pageName];

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

if(history.state.url != url) {

Если запрошен адрес, отличный от текущего, то устанавливаем в качестве заголовка содержимое (свойство content) текущей страницы и добавляем запись в историю просмотров:

contentElement.textContent = page.content; // добавляем в историю history.pushState(page, // объект state event.target.textContent, // Title event.target.href // URL ); document.title = event.target.textContent; // если браузер не устанавливает заголовок

Поскольку браузеры могут не устанавливать автоматически заголовок, то устанавливаем его вручную с помощью свойства document.title. Таким образом, в истории просмотров появится запись о переходе по ссылке.

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

Для обработки переходов с помощью кнопок браузера Назад/Вперед устанавливаем обработчик для события popstate:

window.addEventListener("popstate", (event) => { if(event.state) // если есть состояние contentElement.textContent = event.state.content; // получаем старое состояние });

Здесь получаем извлеченное состояние из истории просмотров (event.state) и с помощью его свойства content устанавливаем содержимое заголовка.

В конце устанавливаем обработчик нажатия для кнопок:

const links = document.getElementsByTagName("a"); for (let i = 0; i < links.length; i++) { links[i].addEventListener("click", handleClick, true); }

И по умолчанию устанавливаем в качестве текущей условной страницы объект home из объекта pages, добавляя при этом соответствующую запись в историю просмотров:

contentElement.textContent = pages.home.content; history.pushState(pages.home, "Home", pages.home.url);

Кинем веб-страницу в браузер и мы сможем переходить по ссылкам как по отдельным страницам:

Также вместо символов хеша # для опредения ссылки (то есть индентификаторов фрагмента) также можно использовать слеши /, что, к примеру, будет лучше для индексации страницы поисковиками. Так, пример выше мы можем переписать следующим образом:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <nav><a href="/home">Home</a> | <a href="/about">About</a> | <a href="/contacts">Contacts</a></nav> <h1 id="content"></h1> <script> // Контейнер, в который загружаем контент const contentElement = document.getElementById("content"); // Объект, который содержит содержимое для различных страниц const pages = { home: { content: "Home Page", url: "/home"}, about: { content: "About Page", url: "/about" }, contacts: { content: "Contact Page", url: "/contacts"} }; // Обработчик нажатия на ссылки function handleClick(event){ // получаем адрес перехода const url = event.target.getAttribute("href"); // получаем имя страницы, которая совпадает с адресом перехода const pageName = url.split("/").pop(); // получаем страницу из объекта pages const page = pages[pageName]; // если текущий адрес совпадает с запрошенным, то игнорируем переход if(history.state.url != url) { contentElement.textContent = page.content; // добавляем в историю history.pushState(page, // объект state event.target.textContent, // Title event.target.href // URL ); document.title = event.target.textContent; // если браузер не устанавливает заголовок } return event.preventDefault(); } // устанавливаем обработчик для извлечения состояния в History API window.addEventListener("popstate", (event) => { if(event.state) // если есть состояние contentElement.textContent = event.state.content; // получаем старое состояние }); // устанавливаем обработчик нажатия для кнопок const links = document.getElementsByTagName("a"); for (let i = 0; i < links.length; i++) { links[i].addEventListener("click", handleClick, true); } // по умолчанию загружаем Home Page contentElement.textContent = pages.home.content; history.pushState(pages.home, "Home", pages.home.url); </script> </body> </html>

Но в этом случае страница должна располагаться на веб-сервере:

Объект history

history управляет историей переходов браузера.

// Количество страниц в истории console.log(history.length); // Навигация по истории history.back(); // Назад (как кнопка "Назад") history.forward(); // Вперёд (как кнопка "Вперёд") history.go(-2); // На 2 страницы назад history.go(1); // На 1 страницу вперёд // Добавить запись в историю (без перезагрузки) history.pushState({page: 1}, 'Title', '/page1'); // Заменить текущую запись в истории history.replaceState({page: 2}, 'Title', '/page2'); // Отслеживание навигации window.addEventListener('popstate', (event) => { console.log('История изменилась:', event.state); });

Пример: SPA навигация без перезагрузки

// Простое SPA (Single Page Application) навигация function navigateTo(url) { // Обновляем URL без перезагрузки history.pushState(null, '', url); // Загружаем контент loadContent(url); } function loadContent(url) { // Здесь загрузка контента для страницы console.log('Загружаем контент для:', url); } // Обработка кнопок "Назад/Вперёд" window.addEventListener('popstate', () => { loadContent(location.pathname); }); // Использование document.querySelectorAll('a').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); navigateTo(link.href); }); });

📜 Объект location

Объект location содержит информацию о расположении текущей веб-страницы: URL, информацию о сервере, номер порта, протокол. С помощью свойств объекта мы можем получить эту информацию:

  • href: полный адрес URL веб-страницы
  • origin: общая схема запроса
  • protocol: протокол (включая двоеточие), например, http: или https:
  • host: хост, например, localhost.com. Если адрес URL содержит номер порта, то порт также входит в хост, например, localhost.com:8080
  • hostname: домен, аналогичен хосту, только не включает порт, например, localhost.com
  • port: порт, используемый ресурсом
  • pathname: путь к ресурсу - та часть адреса, которая идет после хоста после слеша /
  • hash: идентификатор фрагмента - та часть адреса, которая идет после символа решетки # (при его наличии)
  • search: строка запроса - та часть адреса, которая идет после знака вопроса ? (при его налии)
  • username: имя пользователя, которое указано в адресе. Например, в адресе «https://tom:qwerty5@localhost.com это подстрока "tom"
  • password: пароль, который указан в адресе. Например, в адресе «https://tom:qwerty5@localhost.com это подстрока "qwerty5"

В общем случае формат адреса URL выглядит следующим образом:

protocol//username:password@hostname:port/path?search#hash

Например, пусть есть следующая веб-страница index.html:

<body> <script> console.log("href:", location.href); console.log("path:", location.pathname); console.log("origin:", location.origin); console.log("protocol:", location.protocol); console.log("port:", location.port); console.log("host:", location.host); console.log("hostname", location.hostname); console.log("hash:", location.hash); console.log("search:", location.search); </script> </body> // href: http://localhost:8080/index.html?name=tom&age=39#userinfo // path: /index.html // origin: http://localhost:8080 // protocol: http // port: 8080 // host: localhost:8080 // hostname: localhost // hash: #userinfo // serch: ?name=tom&age=39

Пусть она лежит на локальном веб-сервере, и к ней мы обращаемся с помощью адреса http://localhost:8080/index.html?name=tom&age=39#userinfo:

Управление адресом

Также объект location предоставляет ряд методов, которые можно использовать для управления адресом веб-страницы:

  • assign(url): загружает ресурс, который находится по пути url
  • reload(forcedReload): перезагружает текущую веб-страницу. Параметр forcedReload указывает, надо ли использовать кэш браузера. Если параметр равен true, то кэш не используется
  • replace(url): заменяет текущую веб-станицу другим ресурсом, который находится по пути url. В отличие от метода assign, который также загружает веб-станицу с другого ресурса, метод replace не сохраняет предыдущую веб-страницу в стеке истории переходов history, поэтому мы не сможем вызвать метод history.back() для перехода к ней.

Для перенаправления на другой ресурс мы можем использовать как свойства, так и методы location:

location = "http://google.com"; // аналогично // location.href = "http://google.com"; // location.assign("http://google.com");

Переход на другой локальный ресурс:

location.replace("index.html");

Например, выполним переход на странице по нажатию на кнопку:

<body> <input type="url" id="url" /> <button id="btn">Click</button> <script> const btn = document.getElementById("btn"); btn.addEventListener("click", () => { const url = document.getElementById("url").value; location.assign(url); }); </script> </body>

Здесь по нажатию на кнопку выполняется переход по адресу, который введен в текстовое поле url.

Переход с помощью метода replace() производится аналогично:

const btn = document.getElementById("btn"); btn.addEventListener("click", () => { const url = document.getElementById("url").value; location.replace(url); });

Перезагрузка страницы:

const btn = document.getElementById("btn"); btn.addEventListener("click", () => { const url = document.getElementById("url").value; location.reload(true); });

location позволяет получать и изменять URL текущей страницы.

// Текущий URL console.log(location.href); // Полный URL: https://example.com:8080/path?query=value#hash // Части URL console.log(location.protocol); // "https:" console.log(location.host); // "example.com:8080" console.log(location.hostname); // "example.com" console.log(location.port); // "8080" console.log(location.pathname); // "/path" console.log(location.search); // "?query=value" console.log(location.hash); // "#hash" // Перейти на другую страницу location.href = 'https://google.com'; // С записью в историю location.assign('https://google.com'); // То же самое // Перезагрузить страницу location.reload(); // Перезагрузить location.reload(true); // Принудительная перезагрузка (игнорировать кэш) // Заменить текущую страницу (без записи в историю) location.replace('https://google.com');

Работа с query параметрами

// URL: https://example.com?name=Alice&age=25&city=Moscow // Способ 1: URLSearchParams const params = new URLSearchParams(location.search); console.log(params.get('name')); // "Alice" console.log(params.get('age')); // "25" console.log(params.has('city')); // true // Перебор всех параметров params.forEach((value, key) => { console.log(`${key}: ${value}`); }); // Получить все параметры в объект const paramsObj = Object.fromEntries(params); console.log(paramsObj); // {name: "Alice", age: "25", city: "Moscow"} // Добавить/изменить параметр params.set('country', 'Russia'); params.delete('age'); // Обновить URL const newUrl = location.pathname + '?' + params.toString(); history.pushState(null, '', newUrl);

📜 Объект navigator

С помощью свойства navigator объекта window можно получить информацию о браузере и операционной системе, в которой браузер запущен. Это свойство представляет объект типа Navigator, которое определяет ряд свойств и методов. Основные свойства:

  • appCodeName: содержит внутреннее кодовое имя текущего браузера (не стоит полагаться на это свойство, так как обычно оно возвращает Mozilla).
  • appName: содержит официальное имя текущего браузера (ненадежно, поскольку каждый браузер выводит значение Netscape).
  • appVersion: содержит номер версии текущего браузера (ненадежно)
  • battery: представляет объект типа BatteryManager, который позволяет применять Battery Status API для взаимодействия со статусом батареии.
  • cookieEnabled: содержит информацию о том, включены файлы cookie или нет.
  • geolocation: представляет объект типа Geolocation, который позволяет применять Geolocation API для работы с геолокацией.
  • language: содержит строку, указывающую предпочтительный язык пользователя. Обычно этот язык также используется в интерфейсе соответствующего браузера. Если предпочтительный язык не может быть определен, это свойство содержит значение null.
  • languages: содержит список строк, указывающих предпочтительные языки пользователя, причем наиболее предпочтительный язык находится в первой позиции (что соответствует языку из свойства language).
  • mimeTypes: содержит список типов MIME, поддерживаемых браузером.
  • onLine: логическое значение, указывающее, подключен ли браузер к интернету или нет.
  • platform: содержит информацию об используемой операционной системе (ненадежно)
  • plugins: содержит список плагинов, поддерживаемых браузером.
  • product: содержит название продукта текущего браузера. Однако в целях обратной совместимости в каждом браузере возвращается значение Gecko
  • productSub: содержит вложенную метку текущего браузера (20030107 или 20100101)..
  • serviceWorker: представляет объект ServiceWorkerContainer, который позволяет работать с API Service Worker.
  • userAgent: содержит строку, идентифицирующую используемый браузер (тоже ненадежно).
  • vendor: содержит информацию о производителе браузера (одно из значений "Apple Computer, Inc.", "Google Inc." или пустая строка).
  • vendorSub: предназначен для получения дополнительной информации о производителе браузера, но всегда содержит пустую строку

Стоит отметить, что объект Navigator имеет кучу свойств для определения типа браузера, однако ни одно из них нельзя считать надежным. Если раньше нередко применялось свойство userAgent для идентификации браузера, то теперь это свойство для двух разных браузеров может возвращать одинаковые значения. Поэтому также не может считаться надежным.

Применение некоторых свойств на примере браузера Google Chrome на платформе MacOS Intel х86-64:

console.log(navigator.appCodeName); // Mozilla console.log(navigator.appName); // Netscape console.log(navigator.appVersion); // 5.0 (Macintosh; Intel Mac OS X 10_15_7) // AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 console.log(navigator.product); // Gecko console.log(navigator.productSub); // 20030107 console.log(navigator.vendor); // Google Inc. console.log(navigator.vendorSub); // [ пустая строка ] console.log(navigator.userAgent); // Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) // AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 console.log(navigator.platform); // MacIntel console.log(navigator.languages); // список поддерживаемых языков console.log(navigator.plugins); // список поддерживаемых плагинов

Еще примеры:

// Информация о браузере console.log(navigator.userAgent); // Строка User-Agent console.log(navigator.platform); // Платформа (Win32, MacIntel, Linux) console.log(navigator.language); // Язык браузера (ru, en-US) console.log(navigator.languages); // Массив предпочитаемых языков // Онлайн/Оффлайн статус console.log(navigator.onLine); // true/false // Слежение за изменением статуса window.addEventListener('online', () => { console.log('Соединение восстановлено'); }); window.addEventListener('offline', () => { console.log('Соединение потеряно'); }); // Количество логических процессоров console.log(navigator.hardwareConcurrency); // Например, 8 // Геолокация navigator.geolocation.getCurrentPosition( (position) => { console.log('Широта:', position.coords.latitude); console.log('Долгота:', position.coords.longitude); }, (error) => { console.error('Ошибка геолокации:', error); } ); // Буфер обмена (требует разрешения) navigator.clipboard.writeText('Текст для копирования') .then(() => console.log('Скопировано!')) .catch(err => console.error('Ошибка:', err));

📜 Таймеры

Для выполнения действий через определенные промежутки времени в объекте window предусмотрены функции таймеров. Есть два типа таймеров: одни выполняются только один раз, а другие постоянно через промежуток времени.

Функция setTimeout

Для одноразового выполнения действий через промежуток времени предназначена функция setTimeout(). Она может принимать два параметра:

const timerId = setTimeout(someFunction, period)

Параметр period указывает на период в миллисекундах, через который будет выполняться функция из параметра someFunction. А в качестве результата функция возвращает id таймера.

function printMessage() { console.log("Hello METANIT.COM");} setTimeout(printMessage, 5000);

В данном случае через 5 секунд после загрузки страницы произойдет срабатывание функции printMessage.

Для остановки таймера применяется функция clearTimeout(), в которую передается id таймера:

function printMessage() { console.log("Hello METANIT.COM");} const timerId = setTimeout(printMessage, 5000); clearTimeout(timerId);

Функция setInterval

Функции setInterval() и clearInterval() работают аналогично функциям setTimeout() и clearTimeout() с той лишь разницей, что setInterval() постоянно выполняет определенную функцию через промежуток времени.

Например, напишем небольшую программу для вывода текущего времени:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>INTERVAL EXAMP</title> </head> <body> <div id="timer"></div> <script> const timer = document.getElementById("timer"); function updateTime() { const now = new Date(); timer.textContent = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`; } setInterval(updateTime, 1000); </script> </body> </html>

Здесь через каждую секунду (1000 миллисекунд) вызывается функция updateTime(), которая обновляет содержимое поля <div id="timer" >, устанавливая в качестве его текста текущее вемя.

requestAnimationFrame()

Метод requestAnimationFrame() действует аналогично setInterval() за тем исключением, что он больше заточен под анимации, работу с графикой и имеет ряд оптимизаций, которые улучшают его производительность.

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <style> #rect { margin: 100px; width: 100px; height: 100px; background: #50c878; } </style> </head> <body> <div id="rect"></div> <script> const square = document.getElementById("rect"); let offset = 0; let step = 1; function moveRect() { if(offset >=600) step = -1; if(offset <=0) step = 1; offset +=step; square.style.marginLeft = offset + "px"; window.requestAnimationFrame(moveRect); } window.requestAnimationFrame(moveRect); </script> </body> </html>

В метод window.requestAnimationFrame() передается функция, которая будет вызываться определенное количество раз (обычно 60) в секунду. В данном случае в этот метод передается функция moveRect, которая изменяет угол поворота блока на странице и затем обращается опять же к методу window.requestAnimationFrame(moveRect).

В качестве возвращаемого результата метод window.requestAnimationFrame() возвращает уникальный id, который может потом использоваться для остановки анимации:

// получаем id const id = window.requestAnimationFrame(moveRect); // останавливаем анимацию window.cancelAnimationFrame(id);

requestAnimationFrame

Метод для создания плавных анимаций (оптимизирован браузером).

// Анимация движения элемента const box = document.querySelector('.box'); let position = 0; function animate() { position += 2; box.style.left = position + 'px'; if (position < 500) { requestAnimationFrame(animate); // Следующий кадр } } requestAnimationFrame(animate); // Отмена анимации const animationId = requestAnimationFrame(animate); cancelAnimationFrame(animationId);
✅ requestAnimationFrame vs setInterval
  • Оптимизирован для анимаций (~60 FPS)
  • Автоматически приостанавливается на неактивных вкладках
  • Синхронизирован с частотой обновления экрана
  • Более плавная анимация

Еще примеры:

// setTimeout - выполнить через заданное время (один раз) const timerId = setTimeout(() => { console.log('Выполнилось через 2 секунды'); }, 2000); // Отменить выполнение clearTimeout(timerId); // setTimeout с параметрами setTimeout((name, age) => { console.log(`${name}, ${age} лет`); }, 1000, 'Alice', 25); // setInterval - выполнять периодически let count = 0; const intervalId = setInterval(() => { count++; console.log('Счётчик:', count); if (count >= 5) { clearInterval(intervalId); // Остановить после 5 раз } }, 1000); // Альтернатива setInterval через рекурсивный setTimeout function repeat() { console.log('Повтор'); setTimeout(repeat, 1000); // Вызывает сама себя } setTimeout(repeat, 1000);

📜 Полноэкранный режим (Fullscreen API)

const element = document.documentElement; // или любой элемент // Войти в полноэкранный режим element.requestFullscreen() .then(() => console.log('Вошли в полноэкранный режим')) .catch(err => console.error('Ошибка:', err)); // Выйти из полноэкранного режима document.exitFullscreen() .then(() => console.log('Вышли из полноэкранного режима')); // Проверить, в полноэкранном ли режиме if (document.fullscreenElement) { console.log('Сейчас в полноэкранном режиме'); } // Отслеживание изменений document.addEventListener('fullscreenchange', () => { if (document.fullscreenElement) { console.log('Вошли в полноэкранный режим'); } else { console.log('Вышли из полноэкранного режима'); } });

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

Определение мобильного устройства

function isMobile() { return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i .test(navigator.userAgent); } if (isMobile()) { console.log('Мобильное устройство'); // Загрузить мобильную версию } else { console.log('Десктоп'); }

Копирование в буфер обмена

async function copyToClipboard(text) { try { await navigator.clipboard.writeText(text); console.log('Скопировано!'); } catch (err) { // Fallback для старых браузеров const textarea = document.createElement('textarea'); textarea.value = text; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); document.body.removeChild(textarea); console.log('Скопировано (fallback)'); } } // Использование copyToClipboard('Текст для копирования');

Сохранение позиции прокрутки

// Сохранить позицию перед уходом со страницы window.addEventListener('beforeunload', () => { sessionStorage.setItem('scrollPos', window.scrollY); }); // Восстановить позицию при загрузке window.addEventListener('load', () => { const scrollPos = sessionStorage.getItem('scrollPos'); if (scrollPos) { window.scrollTo(0, parseInt(scrollPos)); } });

Глава 15. Форматы JSON и XML

📜 Что такое JSON?

JSON (JavaScript Object Notation) представляет легковесный формат хранения данных. JSON описывает структуру и организацию данных JavaScript. Простота JSON привела к тому, что в настоящий момент он является наиболее популярным форматом передачи данных в среде web, вытеснив другой некогда популярный формат XML.

💡 JSON vs JavaScript

Объекты JSON очень похожи на объекты JavaScript, тем более что JSON является подмножеством JavaScript. В то же время важно их различать: JavaScript является языком программирования, а JSON является форматом данных.

📜 Типы данных в JSON

JSON поддерживает три типа данных:

  • Примитивные значения — стандартные строки, числа, значение null, логические значения true и false
  • Объекты — набор простейших данных, других объектов и массивов
  • Массивы — упорядоченные коллекции значений

Объекты JSON

Типичный объект JSON:

{ "name": "Tom", "married": true, "age": 30 }

В JavaScript этому объекту соответствовал бы следующий:

const user = { name: "Tom", married: true, age: 30 }
⚠️ Отличия JSON от объектов JavaScript
  • В JSON названия свойств заключаются в двойные кавычки, как обычные строки
  • Объекты JSON не могут хранить функции и переменные, как объекты JavaScript
  • JSON использует только двойные кавычки ("), одинарные (') недопустимы

Сложные объекты JSON

Объекты могут быть вложенными:

{ "name": "Tom", "married": true, "age": 30, "company": { "name": "Microsoft", "address": "USA, Redmond" } }

Массивы в JSON

Массивы в JSON похожи на массивы JavaScript и также могут хранить простейшие данные или объекты:

// Простой массив ["Tom", true, 30] // Массив объектов [ { "name": "Tom", "married": true, "age": 30 }, { "name": "Alice", "married": false, "age": 23 } ]

📜 Работа с JSON в JavaScript

Для работы с форматом JSON в языке JavaScript предназначен объект JSON. Он позволяет преобразовать объект JavaScript в формат JSON и наоборот.

JSON.stringify() - Сериализация

Для сериализации объекта JavaScript в JSON применяется функция JSON.stringify():

// Объект JavaScript const user = { name: "Tom", married: false, age: 39 }; // Преобразование в JSON (сериализация) const serializedUser = JSON.stringify(user); console.log(serializedUser); // Результат: {"name":"Tom","married":false,"age":39}

JSON.parse() - Десериализация

Для обратной операции — десериализации или парсинга JSON-объекта в JavaScript — применяется метод JSON.parse():

const user = { name: "Tom", married: false, age: 39 }; // Сериализация const serializedUser = JSON.stringify(user); // Десериализация (парсинг) const tomUser = JSON.parse(serializedUser); console.log(tomUser.name); // Tom console.log(tomUser.age); // 39 console.log(tomUser.married); // false

Полный пример работы с JSON

// 1. Создаём сложный объект const company = { name: "TechCorp", employees: [ { name: "Alice", position: "Developer", salary: 80000 }, { name: "Bob", position: "Designer", salary: 65000 }, { name: "Charlie", position: "Manager", salary: 90000 } ], founded: 2010, active: true }; // 2. Преобразуем в JSON-строку const jsonString = JSON.stringify(company); console.log(jsonString); // 3. Можно сохранить в localStorage или отправить на сервер localStorage.setItem('company', jsonString); // 4. Позже получаем из localStorage const storedJson = localStorage.getItem('company'); // 5. Преобразуем обратно в объект JavaScript const restoredCompany = JSON.parse(storedJson); console.log(restoredCompany.name); // "TechCorp" console.log(restoredCompany.employees[0].name); // "Alice"
✅ Применение JSON
  • AJAX-запросы — передача данных между клиентом и сервером
  • localStorage/sessionStorage — сохранение данных в браузере
  • API — большинство современных API используют JSON
  • Конфигурационные файлы — package.json, tsconfig.json и т.д.

Форматированный вывод JSON

Метод JSON.stringify() может принимать дополнительные параметры для красивого форматирования:

const user = { name: "Tom", age: 39, skills: ["JavaScript", "Python", "SQL"] }; // Третий параметр — количество пробелов для отступа const formatted = JSON.stringify(user, null, 2); console.log(formatted); /* Результат: { "name": "Tom", "age": 39, "skills": [ "JavaScript", "Python", "SQL" ] } */

📜 Работа с XML

XML (eXtensible Markup Language) — один из популярных форматов описания данных. Язык JavaScript предоставляет инструментарий для работы с XML.

DOMParser - Парсинг XML

Для создания XML-объектов на основе строки, которая содержит данные в формате XML, применяется объект DOMParser. Его методу parseFormString() можно передать соответствующую строку в качестве первого аргумента и тип MIME (обычно text/xml) в качестве второго аргумента. Если переданная строка содержит корректный код XML, то метод возвратит объект типа Document, который будет содержать разобранный XML. А чтобы выбрать конкретные данные из полученного документа XML, можно применять стандартные методы выбора элементов DOM, например, querySelector().

// XML-строка const xmlString = ‘<?xml version="1.0" encoding="UTF-8" ?> <users> <user name="Tom" age="39"> <company> <title>Microsoft</title> </company> </user> <user name="Bob" age="43"> <company> <title>Google</title> </company> </user> </users>‘ // Создаём парсер const domParser = new DOMParser(); // Парсим XML-строку const xmlDOM = domParser.parseFromString(xmlString, "text/xml"); // Обращаемся к первому элементу user const firstUser = xmlDOM.querySelector("user"); console.log(firstUser.getAttribute("name")); // Tom console.log(firstUser.getAttribute("age")); // 39 console.log(firstUser.querySelector("title").textContent); // Microsoft

Здесь xml-документ задан строкой xmlString. Но пока это именно строка, а не xml-документ. И для парсинга строки в xml-документ создаем объект DOMParser и выполняем его метод parseFormString(), в который передается наша строка:

const domParser = new DOMParser(); const xmlDOM = domParser.parseFromString(xmlString, "text/xml");

Получив xml-документ, выбираем первый элемент user с помощью метода querySelector

const firstUser = xmlDOM.querySelector("user");

Далее мы можем обращаться к содержимому элемента user - к его вложенным элементам и атриубтам

console.log(firstUser.getAttribute("name")); // Tom console.log(firstUser.getAttribute("age")); // 39 console.log(firstUser.querySelector("title").textContent); // Microsoft
💡 Как работает DOMParser

Метод parseFromString() принимает два параметра:

  • Первый — строка с XML-данными
  • Второй — тип MIME (обычно "text/xml")

Если строка содержит корректный XML, метод возвратит объект типа Document, из которого можно выбирать данные стандартными методами DOM (querySelector, getElementById и т.д.).

Получение всех элементов

const xmlString = ‘<?xml version="1.0" encoding="UTF-8" ?> <users> <user name="Tom" age="39"> <company> <title>Microsoft</title> </company> </user> <user name="Bob" age="43"> <company> <title>Google</title> </company> </user> </users>‘ const domParser = new DOMParser(); const xmlDOM = domParser.parseFromString(xmlString, "text/xml"); // Получаем все элементы user const users = xmlDOM.querySelectorAll("user"); // Перебираем всех пользователей users.forEach(user => { const name = user.getAttribute("name"); const age = user.getAttribute("age"); const company = user.querySelector("title").textContent; console.log(`${name}, ${age} лет, работает в ${company}`); }); // Вывод: // Tom, 39 лет, работает в Microsoft // Bob, 43 лет, работает в Google

XMLSerializer - Сериализация XML

Для обратного преобразования — из XML-документа в строку — применяется объект XMLSerializer. Этот объект предоставляет метод serializeToString(), который получает объект XML и возвращает его в форме строки.

const xmlString = ‘<?xml version="1.0" encoding="UTF-8" ?> <users> <user name="Tom" age="39"> <company> <title>Microsoft</title> </company> </user> <user name="Bob" age="43"> <company> <title>Google</title> </company> </user> </users>‘ // Преобразуем строку в XML const domParser = new DOMParser(); const xmlDOM = domParser.parseFromString(xmlString, "text/xml"); // Преобразуем обратно из XML в строку const xmlSerializer = new XMLSerializer(); const xmlString2 = xmlSerializer.serializeToString(xmlDOM); console.log(xmlString2); // В итоге получим обратно изначальную строку xmlString

Сериализация HTML

Поскольку документ HTML по сути также является документом XML, мы можем сериализовать в строку и HTML-страницу или её часть:

// Преобразуем в строку текущую веб-страницу const xmlSerializer = new XMLSerializer(); const htmlString = xmlSerializer.serializeToString(document); console.log(htmlString); // Или сериализуем конкретный элемент const header = document.querySelector('header'); const headerString = xmlSerializer.serializeToString(header); console.log(headerString);

📜 JSON vs XML: Сравнение

Характеристика JSON XML
Читаемость Более компактный и простой Более многословный
Размер данных Меньше (легковесный) Больше (много тегов)
Парсинг Быстрее (встроенный JSON.parse()) Медленнее (требует DOMParser)
Поддержка типов Ограниченная (числа, строки, boolean, null) Всё хранится как текст
Массивы Нативная поддержка Требует дополнительной разметки
Комментарии Не поддерживаются Поддерживаются (<!-- -->)
Метаданные Нет Есть (атрибуты, пространства имён)
Популярность в Web Стандарт де-факто для API Используется реже, в legacy-системах

Пример: те же данные в JSON и XML

// JSON { "users": [ { "name": "Tom", "age": 39, "company": "Microsoft" }, { "name": "Bob", "age": 43, "company": "Google" } ] } // XML <?xml version="1.0" encoding="UTF-8" ?> <users> <user name="Tom" age="39"> <company> <title>Microsoft</title> </company> </user> <user name="Bob" age="43"> <company> <title>Google</title> </company> </user> </users>
✅ Когда использовать JSON?
  • REST API и веб-сервисы
  • Обмен данными между клиентом и сервером
  • Конфигурационные файлы
  • Хранение данных в localStorage
  • Когда нужна скорость и компактность
💡 Когда использовать XML?
  • Legacy-системы, требующие XML
  • Сложные документы с метаданными
  • SOAP веб-сервисы
  • RSS/Atom фиды
  • SVG (графика) и другие XML-based форматы

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

Пример 1: Загрузка данных с сервера (JSON)

// Получаем данные с API fetch('https://api.example.com/users') .then(response => response.json()) // Парсим JSON .then(users => { users.forEach(user => { console.log(`${user.name}: ${user.email}`); }); }) .catch(error => console.error('Ошибка:', error));

Пример 2: Сохранение настроек пользователя (JSON)

// Объект с настройками const userSettings = { theme: 'dark', language: 'ru', notifications: true, volume: 75 }; // Сохраняем в localStorage localStorage.setItem('settings', JSON.stringify(userSettings)); // Позже загружаем настройки const savedSettings = JSON.parse(localStorage.getItem('settings')); console.log(savedSettings.theme); // 'dark'

Пример 3: Работа с RSS (XML)

// RSS-лента (XML) const rssXml = ‘<?xml version="1.0" encoding="UTF-8" ?> <rss version="2.0"> <channel> <title>Новости технологий</title> <item> <title>JavaScript ES2024</title> <link>https://example.com/news1</link> <description>Новые возможности</description> </item> <item> <title>React 19 выпущен</title> <link>https://example.com/news2</link> <description>Обзор изменений</description> </item> </channel> </rss>‘; const parser = new DOMParser(); const rssDoc = parser.parseFromString(rssXml, "text/xml"); // Получаем все новости const items = rssDoc.querySelectorAll("item"); items.forEach(item => { const title = item.querySelector("title").textContent; const link = item.querySelector("link").textContent; const desc = item.querySelector("description").textContent; console.log(`${title}: ${desc} - ${link}`); });
⚠️ Обработка ошибок при парсинге

Всегда оборачивайте парсинг JSON в try-catch для обработки невалидных данных:

try { const data = JSON.parse(jsonString); console.log(data); } catch (error) { console.error('Ошибка парсинга JSON:', error.message); }

Глава 16. Хранение данных

📜 Способы хранения данных в браузере

JavaScript предоставляет несколько способов хранения данных на стороне клиента:

  • Cookies (Куки) — классический способ, данные отправляются с каждым HTTP-запросом
  • localStorage — постоянное хранилище (данные не удаляются автоматически)
  • sessionStorage — временное хранилище (данные удаляются при закрытии вкладки)
  • IndexedDB — полноценная база данных в браузере (см. главу 24)

📜 Cookies (Куки)

Cookies — это небольшие фрагменты данных, которые веб-сервер отправляет браузеру, а браузер сохраняет их и отправляет обратно серверу с каждым последующим запросом.

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

Для работы с куками в языке JavaScript в объекте document предназначено свойство cookie.

Для установки куки достаточно свойству document.cookie присвоить строку с куками:

<body> <script> document.cookie = "login=tom32;"; console.log(document.cookie); </script> </body>

В данном случае устанавливается куки, которая называется "login" и которая имеет значение "tom32". Затем получаем куки и выводим их на консоль.

💡 Особенности cookies
  • Максимальный размер: 4 КБ на одну куку
  • Отправляются с каждым HTTP-запросом к домену
  • Могут иметь срок действия (expires/max-age)
  • Могут быть доступны только серверу (HttpOnly) или только по HTTPS (Secure)

📃 Установка параметров куки

Строка куки принимает до шести различных параметров:

  • Имя и значение куки

    Имя не чувствительно к регистру, что означает, что, например, login и Login относятся к одному и тому же файлу cookie. В качестве значений разрешены только строки (а не, скажем, числа). Имя и значение — единственные обязательные компоненты. Указывать остальную информацию необязательно (если она не указана, используются значения по умолчанию).

  • Срок окончания действия (параметр expires)

    Дата истечения срока действия, до которой файл cookie действителен. По истечении указанной здесь даты срок действия файла cookie истекает, файл cookie удаляется и больше не отправляется на сервер. Если при создании файла cookie не указана дата истечения срока действия, он удаляется по умолчанию при завершении сеанса браузера.

  • Путь (параметр path ) и домен (параметр domain)

    Используются для разграничения куки. Например, файл cookie с доменом www.localhost.com отправляется только с запросами к этому домену. Файл cookie с доменом www.localhost.com и путем /home отправляется только с запросами на www.localhost.com/home, но не на www.localhost.com/about.

  • Параметр secure

    Флаг безопасности, который можно использовать, чтобы дополнительно указать, следует ли отправлять файлы cookie только при соединениях, использующих протокол Secure Sockets Layer (SSL), например, чтобы разрешить отправку по https. Если этот параметр установлен, куки могут быть посланы по адресу https://www.localhost.com, а при запросах по адресу посланы по адресу http://www.localhost.com такие куки НЕ посылаются.

Выше использовались только два параметра: имя куки и значение:

document.cookie = "login=tom32;";

То есть в данном случае куки имеет имя login и значение tom32.

Параметр expires

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

И в этом случае нам надо установить параметр expires, то есть срок действия куков:

document.cookie = "login=tom32;expires=Sun, 31 Dec 2023 23:59:00 GMT;";

То есть срок действия куки login истекает в понедельник 31 декабря 2023 года в 23:59. Формат параметра expires очень важен. Однако его можно сгенерировать программно. Для этого мы можем использовать метод toUTCString() объекта Date:

const expire = new Date(); expire.setHours(expire.getHours() + 4); document.cookie = "login=tom32;expires=" + expire.toUTCString() + ";";

В данном случае срок действия куки будет составлять 4 часа.

Путь и домен

Если в друг нам надо установить куки для какого-то определенного пути на сайте, то мы можем использовать параметр path. Например, мы хотим установить куки только для пути http://localhost:3000/home:

document.cookie = "login=tom32;expires=Sun, 31 Dec 2023 23:59:00 GMT;path=/home;";

В этом случае для других путей на сайте, например, http://localhost:3000/about, эти куки будут недоступны. Однако стоит отметить, что эта кука будет установлена, если мы обращаемся по пути http://localhost:3000/home.\

Если на нашем сайте есть несколько доменов, и мы хотим установить куки непосредственно для определенного домена, тогда можно использовать параметр domain. Например, у нас на сайте есть поддомен blog.mysite.com:

document.cookie="login=tom32;expires=Sun, 31 Dec 2023 23:59:00 GMT;path=/;domain=blog.mysite.com;";

Параметр path=/ указывает, что куки будут доступны для всех директорий и путей поддомена blog.mysite.com.

Параметр secure

Последний параметр - secure задает использование SSL (SecureSockets Layer) и подходит для сайтов, использующих протокол https. Если значение этого параметра равно true, то куки будут использоваться только при установке защищенного соединения ssl. По умолчанию данный параметр равен false.

document.cookie = "login=tom32;expires=Sun, 31 Dec 2023 23:59:00 GMT;path=/;secure=true;";
⚠️ Важные параметры cookies
  • path=/ — cookie доступна для всего сайта
  • domain=example.com — cookie доступна для домена и поддоменов
  • secure — передача только по HTTPS
  • samesite=strict — защита от CSRF-атак

📃 Чтение/получение cookies

Для простейшего извлечения куки из браузера достаточно обратиться к свойству document.cookie:

// Получаем все cookies в виде строки console.log(document.cookie); // Пример вывода: "username=Alice; theme=dark; lang=ru"
const expire = new Date(); expire.setHours(expire.getHours() + 4); document.cookie = "language=JavaScript;expires="+expire.toUTCString()+";"; document.cookie = "company=Localhost;expires="+expire.toUTCString()+";"; document.cookie = "login=tom32;"; console.log(document.cookie);

Здесь были установлены три куки, и консоль браузера выведет нам все эти куки:

language=JavaScript; company=Localhost; login=tom32

Извлеченные куки не включают параметры expires, path, domain и secure. Кроме того, сами куки разделяются точкой с запятой, поэтому нужно еще провести некоторые преобразования, чтобы получить их имя и значение:

const cookies = document.cookie.split(";"); for(cookie of cookies){ const parts = cookie.split("="); console.log("Имя куки:", parts[0]); console.log("Значение:", parts[1],"\n\n"); }

📃 Создание/изменение cookie

// Простая установка cookie document.cookie = "username=Alice"; // Cookie с истечением через 7 дней const date = new Date(); date.setTime(date.getTime() + (7 * 24 * 60 * 60 * 1000)); // +7 дней document.cookie = `username=Alice; expires=${date.toUTCString()}; path=/`; // Cookie с max-age (в секундах) document.cookie = "theme=dark; max-age=604800; path=/"; // 7 дней // Secure cookie (только HTTPS) document.cookie = "token=abc123; secure; path=/"; // HttpOnly cookie (только для сервера, JS не может читать) // Устанавливается только сервером!

📃 Удаление cookie

// Устанавливаем expires в прошлое document.cookie = "username=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/"; // Или используем max-age=0 document.cookie = "theme=; max-age=0; path=/";

📃 Вспомогательные функции для работы с cookies

// Функция для установки cookie function setCookie(name, value, days) { const date = new Date(); date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); const expires = `expires=${date.toUTCString()}`; document.cookie = `${name}=${value}; ${expires}; path=/`; } // Функция для получения cookie function getCookie(name) { const nameEQ = name + "="; const cookies = document.cookie.split(';'); for (let i = 0; i < cookies.length; i++) { let cookie = cookies[i].trim(); if (cookie.indexOf(nameEQ) === 0) { return cookie.substring(nameEQ.length); } } return null; } // Функция для удаления cookie function deleteCookie(name) { document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`; } // Использование setCookie('username', 'Alice', 7); // Установить на 7 дней console.log(getCookie('username')); // "Alice" deleteCookie('username'); // Удалить

📃 Практический пример: Запоминание темы

// Сохраняем выбор темы function saveTheme(theme) { setCookie('theme', theme, 365); // На год applyTheme(theme); } // Применяем тему function applyTheme(theme) { document.body.className = theme === 'dark' ? 'dark-theme' : ''; } // При загрузке страницы проверяем сохранённую тему window.addEventListener('DOMContentLoaded', () => { const savedTheme = getCookie('theme') || 'light'; applyTheme(savedTheme); }); // Переключатель темы document.getElementById('themeToggle').addEventListener('click', () => { const currentTheme = getCookie('theme') || 'light'; const newTheme = currentTheme === 'light' ? 'dark' : 'light'; saveTheme(newTheme); });

Ограничения куки

Все файлы cookie для соответствующего домена и соответствующего пути отправляются с каждым запросом, что влияет на объем пересылаемых данных. Кроме того, файлы cookie, отправляемые по протоколу HTTP (а не по безопасному протоколу HTTPS), передаются в незашифрованном виде, что представляет угрозу безопасности в зависимости от типа передаваемой информации. Еще одним ограничением файлов cookie является разрешенный размер памяти в 4 КБ.

📜 Простой web-сервер для использования cookies

Работа с куками может различаться от того, какой браузер используется, и как запускается веб-страница: как локальный файл или как файл на веб-сервере. Например, если мы запустим веб-страницу как локальный файл, то есть просто бросим выше определенную веб-страницу в браузер Mozilla FireFox или Safari, то браузер установит куки и выведет их на консоль. Браузеры поддерживают просмотр установленных кук (как и других сохраненных данных). Например, если брать Mozilla FireFox, то в инструментах разработчика есть вкладки "Хранилище" (в русскоязычной локализации), и и там можно посмотреть куки:

Однако в других браузерах, например, в Google Chrome или Opera на установку куки в веб-страницах, которые представляют локальные файлы, действуют ограничения. Соответственно, если мы бросим выше определенную страницу в Google Chrome, то консоль нам ничего не отобразит. Потому что Google Chrome по умолчанию поддерживает установку кук только в веб-страницах, которые загружаются с веб-сервера и принадлежат некоторому домену в сети. Что в принципе неудивительно. Ведь куки предназначены прежде всего для пересылки данных по протоколу http от клиента серверу и обратно.

Поэтому в дальнейшем для работы с куками мы будем располагать html-страницы на веб-сервере. В данном случае воспользуемся самым простым вариантом - Node.js, так как эта технология двольно проста, доступна для всех основных операционных систем и также также позволяет использовать javascript для создания приложений. Но естественно перед созданием приложения необходимо установить Node.js. В данном случае не потребуется никаких знаний node.js, весь используемый код подробно описывается. Но опять же вместо node.js это может быть любая другая технология сервера - php, asp.net, python и т.д. либо какой-то определенный веб-сервер типа Apache или IIS.

Итак, создадим в файловой системе папку для файлов веб-сервера. Например, в моем случае это папка C:\app. Далее в этой папке определим файл сервера. Пусть он будет называться server.js и будет иметь следующий код:

const http = require("http"); const fs = require("fs"); http.createServer(function(_, response){ fs.readFile("index.html", (_, data) => response.end(data)); }).listen(3000, ()=>console.log("Сервер запущен по адресу http://localhost:3000"));

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

const http = require("http"); // для обработки входящих запросов const fs = require("fs"); // для чтения с жесткого диска файла index.html

Для создания сервера применяется функция http.createServer(). В эту функцию передается функция-обработчик, которая вызывается каждый раз, когда к серверу приходит запрос. Эта функция имеет два параметра: request (содержит данные запроса) и response (управляет отправкой ответа). Первый параметр не используется, поэтмоу вместо него указан прочерк _.

В функции-обработчике отправляем файл index.html, который мы дальше определим:

fs.readFile("index.html", (error, data) => response.end(data));

Для считывания файлов применяется встроенная функция fs.readFile(). Первый параметр функции - адрес файла (в данном случае предполагается, что файл index.html находится в одной папке с файлом сервера server.js). Второй параметр - функция, которая вызывается после считывания файла и получет его содержимое через свой второй параметр data. Затем считанное содежимое также может быть отпавлено с помощью функции response.end(data).

В конце с помощью функции listen() запускаем веб-сервер на 3000 порту. То есть сервер будет запускаться по адресу http://localhost:3000/

И также в той же папке определим файл index.html со следующим кодом:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <script> document.cookie = "login=tom32;"; console.log(document.cookie); </script> </body> </html>

Здесь, как уже было рассмотрено выше, устанавливается куки login. Затем куки выводятся на консоль.

Теперь в консоли перейдем к папке сервера с помощью команды cd и запустим сервер с помощью команды node server.js

C:\app>node server.js
Сервер запущен по адресу http://localhost:3000

После запуска сервера мы можем перейти в браузере по адресу http://localhost:3000, нам отобразится страница index.html. С помощью инструментов браузера мы сможем посмотреть установленные куки. Так, в Google Chrome это вкладка Application, а в левой части пункт Storage -> Cookies:

📜 Web Storage API

Для хранения данных в HTML5 применяется специальный API - Web Storage API, который обеспечивает доступ к внутреннему хранилищу браузера (web storage). Данное хранилище состоит из двух компонентов: session storage и local storage.

Web Storage API предоставляет два объекта для хранения данных в браузере: localStorage и sessionStorage.

Session storage представляет временное хранилище информации, которая удаляется после закрытия вкладки браузера.

Local storage представляет хранилище для данных на постоянной основе. Данные из local storage автоматически не удаляются и не имеют срока действия. Эти данные не передаются на сервер в запросе HTTP. Кроме того, объем local storage составляет в Chrome и Firefox 5 Mб для домена.

Все данные в web storage представляют набор пар ключ-значение. То есть каждый объект имеет уникальное имя-ключ и определенное значение.

Для работы с local storage в javascript используется объект localStorage, а для работы с session storage - объект sessionStorage. Оба этих объектов с точки зрения API похожи и предоставляют аналогичные свойства и методы:

  • length: содержит количество элементов в хранилище
  • clear(): удаляет все элементы из хранилища
  • getItem(key): возвращает определенный элемент, который имеет ключ key
  • key(index): возвращает ключ элемента, который имеет индекс index
  • removeItem(key): удаляет элемент с ключом key
  • setItem(key, value): устанавливает для элемента с ключом key значение value. Если элемент с ключом key уже есть в хранилище, то его значение перезаписывается. Если элемента нет, то он добавляется.
Характеристика localStorage sessionStorage Cookies
Объём 5-10 МБ 5-10 МБ 4 КБ
Срок хранения Постоянно (пока не удалят) До закрытия вкладки Настраивается (expires)
Отправка на сервер Нет Нет Да (с каждым запросом)
API Простой (key-value) Простой (key-value) Строка (парсинг вручную)
Область видимости Весь домен Только текущая вкладка Весь домен

📜 localStorage

localStorage — постоянное хранилище данных в браузере. Данные сохраняются даже после закрытия браузера.

Основные методы localStorage

// 1. setItem() - сохранение данных localStorage.setItem('username', 'Alice'); localStorage.setItem('age', '25'); // 2. getItem() - получение данных const username = localStorage.getItem('username'); console.log(username); // "Alice" // 3. removeItem() - удаление одного элемента localStorage.removeItem('age'); // 4. clear() - удаление всех данных localStorage.clear(); // 5. key(index) - получение ключа по индексу const firstKey = localStorage.key(0); // 6. length - количество элементов console.log(localStorage.length);

Альтернативный синтаксис (как объект)

// Запись localStorage.username = 'Alice'; localStorage['theme'] = 'dark'; // Чтение console.log(localStorage.username); // "Alice" console.log(localStorage['theme']); // "dark" // Удаление delete localStorage.username;
⚠️ localStorage хранит только строки!

Все данные в localStorage автоматически преобразуются в строки. Для хранения объектов используйте JSON.stringify() и JSON.parse().

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

// Сохранение объекта const user = { name: 'Alice', age: 25, skills: ['JavaScript', 'React', 'Node.js'] }; // Преобразуем в JSON-строку localStorage.setItem('user', JSON.stringify(user)); // Получение объекта const savedUser = JSON.parse(localStorage.getItem('user')); console.log(savedUser.name); // "Alice" console.log(savedUser.skills[0]); // "JavaScript"

Перебор всех элементов localStorage

// Способ 1: через цикл for for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); const value = localStorage.getItem(key); console.log(`${key}: ${value}`); } // Способ 2: через Object.keys() Object.keys(localStorage).forEach(key => { console.log(`${key}: ${localStorage[key]}`); });

📃 Практический пример: Корзина товаров

// Класс для работы с корзиной class ShoppingCart { constructor() { this.storageKey = 'cart'; this.items = this.loadCart(); } // Загрузка корзины из localStorage loadCart() { const data = localStorage.getItem(this.storageKey); return data ? JSON.parse(data) : []; } // Сохранение корзины в localStorage saveCart() { localStorage.setItem(this.storageKey, JSON.stringify(this.items)); } // Добавление товара addItem(product) { const existing = this.items.find(item => item.id === product.id); if (existing) { existing.quantity++; } else { this.items.push({ ...product, quantity: 1 }); } this.saveCart(); } // Удаление товара removeItem(productId) { this.items = this.items.filter(item => item.id !== productId); this.saveCart(); } // Получение общей суммы getTotal() { return this.items.reduce((sum, item) => { return sum + (item.price * item.quantity); }, 0); } // Очистка корзины clear() { this.items = []; this.saveCart(); } } // Использование const cart = new ShoppingCart(); // Добавляем товары cart.addItem({ id: 1, name: 'Книга по JS', price: 1500 }); cart.addItem({ id: 2, name: 'Мышка', price: 800 }); console.log('Товаров в корзине:', cart.items.length); console.log('Итого:', cart.getTotal(), 'руб.'); // Корзина сохранится и после перезагрузки страницы!

📜 sessionStorage

sessionStorage работает аналогично localStorage, но данные удаляются при закрытии вкладки/окна браузера.

Основные методы (такие же, как у localStorage)

// Сохранение sessionStorage.setItem('tempData', 'Это временные данные'); // Получение const data = sessionStorage.getItem('tempData'); // Удаление sessionStorage.removeItem('tempData'); // Очистка всех данных sessionStorage.clear();

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

  • Данные формы — сохранение введённых данных на время сеанса
  • Временные настройки — фильтры, сортировка таблиц
  • Состояние приложения — текущая вкладка, открытые модальные окна
  • Одноразовые токены — данные, которые не должны сохраняться надолго

📃 Пример: Сохранение состояния формы

// Автосохранение данных формы const form = document.getElementById('myForm'); const inputs = form.querySelectorAll('input, textarea, select'); // Загружаем сохранённые данные при загрузке страницы inputs.forEach(input => { const savedValue = sessionStorage.getItem(input.name); if (savedValue) { input.value = savedValue; } // Сохраняем при изменении input.addEventListener('input', () => { sessionStorage.setItem(input.name, input.value); }); }); // Очищаем при отправке формы form.addEventListener('submit', () => { inputs.forEach(input => { sessionStorage.removeItem(input.name); }); });

📜 События Storage

Событие storage срабатывает, когда localStorage/sessionStorage изменяется в другой вкладке/окне того же домена.

// Отслеживаем изменения в других вкладках window.addEventListener('storage', (e) => { console.log('Ключ:', e.key); console.log('Старое значение:', e.oldValue); console.log('Новое значение:', e.newValue); console.log('URL:', e.url); console.log('Хранилище:', e.storageArea); // Например, синхронизируем тему между вкладками if (e.key === 'theme') { applyTheme(e.newValue); } });

📃 Практический пример: Синхронизация между вкладками

// В первой вкладке function updateStatus(status) { localStorage.setItem('userStatus', status); } updateStatus('online'); // Во второй вкладке автоматически обновится window.addEventListener('storage', (e) => { if (e.key === 'userStatus') { console.log('Статус изменён:', e.newValue); updateUI(e.newValue); } });

📜 Сравнение способов хранения

✅ Когда использовать localStorage
  • Настройки пользователя (тема, язык)
  • Сохранение прогресса игры
  • Кэширование данных
  • Корзина покупок
  • История просмотренного
💡 Когда использовать sessionStorage
  • Временное состояние формы
  • Фильтры и сортировка
  • Одноразовые токены
  • Пошаговые мастера (wizards)
⚠️ Когда использовать Cookies
  • Аутентификация (токены сессии)
  • Данные нужны на сервере
  • Поддержка старых браузеров
  • Отслеживание пользователей (analytics)

📜 Ограничения и безопасность

🔒 Важные моменты безопасности
  • Не храните пароли! localStorage/sessionStorage доступны через JavaScript
  • XSS-атаки: злоумышленники могут получить доступ к localStorage
  • Чувствительные данные: храните на сервере, а не в браузере
  • HTTPS: всегда используйте HTTPS для передачи важных данных
  • Валидация: всегда проверяйте данные из localStorage перед использованием

Пример безопасной работы с токенами

// ❌ ПЛОХО: храним токен в localStorage localStorage.setItem('authToken', 'secret123'); // ✅ ХОРОШО: используем HttpOnly cookie (устанавливается сервером) // Клиентский JS не может прочитать такую cookie // Set-Cookie: authToken=secret123; HttpOnly; Secure; SameSite=Strict // Для refresh-токенов можно использовать localStorage с осторожностью // Но access-токен всегда в HttpOnly cookie!

📜 Проверка доступности и лимитов

// Проверка поддержки localStorage function storageAvailable(type) { try { const storage = window[type]; const test = '__storage_test__'; storage.setItem(test, test); storage.removeItem(test); return true; } catch (e) { return false; } } if (storageAvailable('localStorage')) { console.log('localStorage доступен'); } else { console.log('localStorage недоступен (приватный режим или отключен)'); } // Проверка лимита хранилища function getStorageSize() { let total = 0; for (let key in localStorage) { if (localStorage.hasOwnProperty(key)) { total += localStorage[key].length + key.length; } } return (total / 1024).toFixed(2) + ' КБ'; } console.log('Использовано:', getStorageSize());

📜 Полезные утилиты

// Обёртка для работы с localStorage с автоматическим JSON const storage = { set(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); return true; } catch (e) { console.error('Ошибка сохранения:', e); return false; } }, get(key, defaultValue = null) { try { const item = localStorage.getItem(key); return item ? JSON.parse(item) : defaultValue; } catch (e) { console.error('Ошибка чтения:', e); return defaultValue; } }, remove(key) { localStorage.removeItem(key); }, clear() { localStorage.clear(); }, has(key) { return localStorage.getItem(key) !== null; } }; // Использование storage.set('user', { name: 'Alice', age: 25 }); const user = storage.get('user'); console.log(user.name); // "Alice" if (storage.has('user')) { console.log('Пользователь найден'); }

📖 Глава 17. Асинхронность, Promise, async и await

📜 Что такое асинхронность?

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

💡 Асинхронность в JavaScript

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

Пример синхронного кода

console.log("Начало"); // Эта функция блокирует выполнение на 3 секунды function heavyOperation() { const start = Date.now(); while (Date.now() - start < 3000) { // Ничего не делаем, просто ждём } return "Результат"; } const result = heavyOperation(); console.log(result); console.log("Конец"); // Вывод (с задержкой 3 секунды): // Начало // Результат // Конец

Пример асинхронного кода

console.log("Начало"); // Асинхронная функция (setTimeout) setTimeout(() => { console.log("Асинхронная операция завершена"); }, 2000); console.log("Конец"); // Вывод (немедленно): // Начало // Конец // (через 2 секунды): // Асинхронная операция завершена

Определим простую асинхронную функцию, которая эмулирует долгую работу с помощью вызова setTimeout() и задержки в 1 секунду, а затем выводит на консоль случайное число:

function asyncFunction() { setTimeout(()=>{ let result = 22; console.log("result:", result); }, 1000); } asyncFunction(); console.log("Конец программы");

Вместо setTimeout() здесь мог бы быть запрос к базе данных или запрос к сетевому ресурсу, которые могли бы занять продолжительное время и результат которых был бы получен через некоторое время. И в результате значение числа было бы ведено на консоль в самом конце выполнения программы:

Конец программы
result: 22

Здесь мы видим, что асинхронная функция не блокирует выполнение остальных инструкций программы. Однако при работе с подобными функциями мы можем столкнуться с рядом проблем. Так, асинхронные функции не возвращают результат асинхронного вычисления через ключевое слово return, а передают его в качестве параметра функции обратного вызова.

function asyncFunction() { let result; setTimeout(()=>{result = 22;}, 1000); return result; } const asyncResult = asyncFunction(); console.log("result:", asyncResult) // result: undefined

Здесь асинхронная функция asyncFunction вызывается в синхронной манере, в итоге мы получаем неопределенный результат. Потому что переменная asyncResult устанавливается до того, как функция asyncFunction сгенерирует результат.

Другая проблема связана с генерацией ошибок через оператор throw:

function asyncFunction() { let result; setTimeout(()=>{ result = 22; if(result < 50) { throw new Error("Некорректное значение"); } }, 1000); return result; } try { const asyncResult = asyncFunction(); console.log("result:", asyncResult) } catch(error) { console.error("Error:", error); // Эта строка НЕ выполняется } console.log("Конец программы");

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

📜 Callback функции (устаревший подход)

Изначально, до появления Promise, асинхронные операции (обработка результата и ошибок в асинхронных функциях) обрабатывались с помощью callback-функций (функций обратного вызова), которые передавались в другую функцию и вызывались позже в некоторый момент времени. Простейший шаблон использования коллбеков:

// Функция принимает callback function asyncFunction(callback) { setTimeout(() => { const result = 22; callback(result); // Вызываем callback с результатом }, 1000); } // Использование asyncFunction(function(result) { console.log("Результат:", result); // Результат: 22 });
function asyncFunction(callback) { console.log("Перед вызовом коллбека"); callback(); console.log("После вызова коллбека"); } function callbackFunc() { console.log("Вызов коллбека"); } asyncFunction(callbackFunc);

Здесь функция asyncFunction (условно асинхронная функция) принимает функцию обратного вызова - callback и вызывает ее в коде.

Например, используем коллбек для получения и обработки результата и ошибки асинхронной функции:

function handleResult(error, result){ if(error) { // если передана ошибка console.error(error); } else { // если асинхронная функция завершилась успешно console.log("Result:", result); } } function asyncFunction(callback) { setTimeout(()=>{ let result = Math.floor(Math.random() * 100) + 1; if(result < 50) { // если меньше 50, устанавливаем ошибку callback(new Error("Некорректное значение: " + result), null); } else{ // в остальных случаях устанавливаем результат callback(null, result); } }, 1000); } asyncFunction(handleResult);

В качестве коллбека в асинхронную функцию asyncFunction передается функция handleResult

asyncFunction(handleResult);

Для примера, чтобы число представляло случайное значение, здесь применяется метод Math.random().

let result = Math.floor(Math.random() * 100) + 1;

Если сгенерированное число меньше 50, то устанавливаем первый параметр функции handleResult, который представляет ошибку:

if(result < 50) { // если меньше 50, устанавливаем ошибку callback(new Error("Некорректное значение: " + result), null); }

В остальных случаях устанавливаем результат, а для ошибки передаем null:

else{ // в остальных случаях устанавливаем результат callback(null, result); }

консольный вывод при успешной обработке (когда сгенерированное число равно или больше 50):

Result: 70

Если сгенерированное число меньше 50, то будет выводиться ошибка:

Error: Некорректное значение: 35

Проблема: Callback Hell (Ад колбэков)

Это классическая схема использования коллбеков для обработки результата асинхронной функции. Однако она имеет как минимум один большой недостаток: чрезмерное использование функций обратного вызова может привести к созданию структуры кода, известной среди разработчиков JavaScript как callback hell (ад коллбеков). Такая структура кода возникает, когда коллбек в одной асинхронной функции вызывает другую асинхронную функцию, коллбек которой, в свою очередь, может вызывать третью асинхронную функцию и так далее. Пример подобной структуры:

asyncFunction( (error, result) => { asyncFunction2( (error2, result2) => { asyncFunction3( (error3, result3) => { asyncFunction4( (error4, result4) => { // некоторый код }); }); }); });
// Вложенные callback'и становятся нечитаемыми getData(function(a) { getMoreData(a, function(b) { getMoreData(b, function(c) { getMoreData(c, function(d) { getMoreData(d, function(e) { console.log(e); }); }); }); }); });
⚠️ Проблемы callback-функций
  • Сложная вложенность (pyramid of doom)
  • Трудно обрабатывать ошибки
  • Плохая читаемость кода
  • Сложно координировать несколько асинхронных операций

И для решения этой проблемы начиная со стандарта ES2015 в JavaScript была добавлена поддержка промисов, которые далее будут рассмотрены.

📜 Введение в промисы (Promise)

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

Состояния Promise

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

  • pending (ожидание) — начальное состояние, операция ещё не завершена
  • fulfilled (выполнено) — операция завершена успешно
  • rejected (отклонено, завершено с ошибкой) — операция завершилась с ошибкой

Создание Promise

Для создания промиса применяется конструктор типа Promise:

new Promise(executor)

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

const myPromise = new Promise(function(){ console.log("Выполнение асинхронной операции"); });

Здесь функция просто выводит на консоль сообщение. Соответственно при выполнении этого кода мы увидим на консоли сообщение "Выполнение асинхронной операции".

При создании промиса, когда его функция еще не начала выполняться, промис переходит в состояние "pending", то есть ожидает выполнения.

Для эмуляции асинхронности определим несколько промисов:

const myPromise3000 = new Promise(function(){ console.log("[myPromise3000] Выполнение асинхронной операции"); setTimeout(()=>console.log("[myPromise3000] Завершение асинхронной операции"), 3000); }); const myPromise1000 = new Promise(function(){ console.log("[myPromise1000] Выполнение асинхронной операции"); setTimeout(()=>console.log("[myPromise1000] Завершение асинхронной операции"), 1000); }); const myPromise2000 = new Promise(function(){ console.log("[myPromise2000] Выполнение асинхронной операции"); setTimeout(()=>console.log("[myPromise2000] Завершение асинхронной операции"), 2000); });

Здесь определены три однотипных промиса. Чтобы каждый из них не выполнялся сразу, они используют функцию setTimeout с задержкой в несколько секунд. Для разных промисов длительность задержки различается. И в данном случае мы получим следующий консольный вывод:\

[myPromise3000] Выполнение асинхронной операции
[myPromise1000] Выполнение асинхронной операции
[myPromise2000] Выполнение асинхронной операции
[myPromise1000] Завершение асинхронной операции
[myPromise2000] Завершение асинхронной операции
[myPromise3000] Завершение асинхронной операции

Здесь мы видим, что первым начал выполняться промис myPromise3000, однако он же завершился последним, так как для него установлено наибольшее время задержки - 3 секунды. Однако его задержка не помешала выполнению остальных промисов.

// Синтаксис создания Promise const myPromise = new Promise(function(resolve, reject) { // Асинхронная операция console.log("Выполнение асинхронной операции"); // Если успешно - вызываем resolve() resolve("Успех!"); // Если ошибка - вызываем reject() // reject("Ошибка!"); });

Простой пример Promise

// Создаём Promise const myPromise = new Promise(function(resolve) { console.log("Выполнение асинхронной операции"); // Имитация асинхронной работы setTimeout(() => { resolve("Привет, мир!"); }, 1000); }); console.log("Promise создан");

resolve и reject

Как правило, функция, которая передается в конструктор Promise, принимает два параметра:

const myPromise = new Promise(function(resolve, reject){ console.log("Выполнение асинхронной операции"); });

Оба этих параметра - resolve и reject также представляют функции. И каждая из этих функций принимает параметр любого типа.

  • Первый параметр - функция resolve вызывается в случае успешного выполнения. Мы можем в нее передать значение, которое мы можем получить в результате успешного выполнения.
  • Второй параметр - функция reject вызывается, если выполнение операции завершилось с ошибкой. Мы можем в нее передать значение, которое представит некоторую информацию об ошибке.

Успешное выполнение промиса

Итак, первый параметр функции в конструкторе Promise - функция resolve выполняется при успешном выполнении. В эту функцию обычно передается значение, которое представляет результат операции при успешном выполнении. Это значение может представлять любой объект. Например, передадим в эту функцию строку:

const myPromise = new Promise(function(resolve){ console.log("Выполнение асинхронной операции"); resolve("Привет мир!"); });

Функция resolve() вызывается в конце выполняемой операции после всех действий. При вызове этой функции промис переходит в состояние fulfilled (успешно выполнено).

При этом стоит отметить, что теоретически мы можем возвратить из функции результат, но практического смысла в этом не будет:

const myPromise = new Promise(function(resolve, reject){ console.log("Выполнение асинхронной операции"); return "Привет мир!"; });

Данное возвращаемое значение мы не сможем передать во вне. И если действительно надо возвратить какой-то результат, то он передается в функцию resolve().

Передача информации об ошибке

Второй параметр функции в конструкторе Promise - функция reject вызывается при возникновении ошибки. В эту функцию обычно передается некоторая информация об ошибке, которое может представлять любой объект. Например:

const myPromise = new Promise(function(resolve, reject){ console.log("Выполнение асинхронной операции"); reject("Переданы некорректные данные"); });

При вызове функции reject() промис переходит в состояние rejected (завершилось с ошибкой).

Объединение resolve и reject

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

const x = 4; const y = 0; const myPromise = new Promise(function(resolve, reject){ if(y === 0) { reject("Переданы некорректные данные"); } else{ const z = x / y; resolve(z); } });

В данном случае, если значени константы y равно 0, то сообщаем об ошибке, вызывая функцию reject(). Если не равно 0, то выполняем операцию деления и передаем результат в функцию resolve().

📜 Получение результата: .then()

Ранее мы рассмотрели, как из функции промиса мы можем передать во вне результат асинхронной операции:

const myPromise = new Promise(function(resolve){ console.log("Выполнение асинхронной операции"); resolve("Привет мир!"); });

Теперь получим это значение. Для получения результата операции промиса применяется функция then() объекта Promise:

then(onFulfilled, onRejected);

Первый параметр функции - onFulfilled представляет функцию, которая выполняется при успешном завершении промиса и в качестве параметра получает переданные в resolve() данные.

Второй параметр функции - onRejected представляет функцию, которая выполняется при возникновении ошибки и в качестве параметра получает переданные в reject() данные.

Функция then() возвращает также объект Promise.

Так, получим переданные данные:

const myPromise = new Promise(function(resolve){ console.log("Выполнение асинхронной операции"); resolve("Привет мир!"); }); myPromise.then(function(value){ console.log(`Из промиса получены данные: ${value}`); })

То есть параметр value здесь будет представлять строку "Привет мир!", которая передается c помощью resolve("Привет мир!"). В итоге консольный вывод будет выглядеть следующим образом:

Выполнение асинхронной операции
Из промиса получены данные: Привет мир!
const myPromise = new Promise(function(resolve) { setTimeout(() => { resolve("Привет, мир!"); }, 1000); }); // Получаем результат myPromise.then(function(value) { console.log("Из промиса получены данные:", value); }); // Вывод: // Из промиса получены данные: Привет, мир!

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

const myPromise3000 = new Promise(function(resolve){ console.log("[myPromise3000] Выполнение асинхронной операции"); setTimeout(()=>{resolve("[myPromise3000] Готово!")}, 3000); }); const myPromise1000 = new Promise(function(resolve){ console.log("[myPromise1000] Выполнение асинхронной операции"); setTimeout(()=>{resolve("[myPromise1000] Готово!")}, 1000); }); const myPromise2000 = new Promise(function(resolve){ console.log("[myPromise2000] Выполнение асинхронной операции"); setTimeout(()=>{resolve("[myPromise2000] Готово!")}, 2000); }); // Все запускаются одновременно! myPromise3000.then((value)=>console.log(value)); myPromise1000.then((value)=>console.log(value)); myPromise2000.then((value)=>console.log(value));

Здесь определены три однотипных промиса. Чтобы каждый из них не выполнялся сразу, они используют функцию setTimeout и устанавливают возвращаемое значение только через несколько секунд. Для разных промисов длительность задержки различается. И в данном случае мы получим следующий консольный вывод:

[myPromise3000] Выполнение асинхронной операции
[myPromise1000] Выполнение асинхронной операции
[myPromise2000] Выполнение асинхронной операции
[myPromise1000] Готово (через 1 сек)!
[myPromise2000] Готово (через 2 сек)!
[myPromise3000] Готово (через 3 сек)!

Здесь мы видим, что первым начал выполняться промис myPromise3000, однако он же завершился последним, так как для него установлено наибольшее время задержки - 3 секунды. Однако его задержка не помешала выполнению остальных промисов.

При этом нам необязательно вообще передавать в resolve() какое-либо значение. Возможно, асинхронная операция просто выполняется и не передает во вне никакого результата.

const x = 4; const y = 8; const myPromise = new Promise(function(){ console.log("Выполнение асинхронной операции"); const z = x + y; console.log(`Результат операции: ${z}`) }); myPromise.then();

В данном случае функция в промисе вычисляет сумму чисел x и y и выводит результат на консоль.

Метод Promise.resolve

Иногда требуется просто вернуть из промиса некоторое значение. Для этого можно использовать метод Promise.resolve(). В этот метод передается возвращаемое из промиса значение. Метод Promise.resolve() возвращает объект Promise:

const myPromise = Promise.resolve("Привет мир!"); myPromise.then(value => console.log(value)); // Привет мир!

Определение промиса через функцию

Нередко промис определяется через функцию, которая возвращет объект Promise. Например:

// Функция возвращает Promise function sum(x, y){ return new Promise(function(resolve){ const result = x + y; resolve(result); }) } // Использование sum(3, 5).then(function(value){ console.log("Результат операции:", value);}); // Результат: 8 sum(25, 4).then(function(value){ console.log("Сумма чисел:", value);}); // Результат: 29 // Или с arrow function sum(10, 20).then(value => console.log("Сумма:", value)); // Сумма: 30

Здесь функция sum() принимает два числа и возвращает промис, который инкапсулирует операцию суммы этих чисел. После вычисления сумма чисел передается в resolve(), соответственно мы ее затем можем получить через метод then(). Определение промиса через функцию позволяет нам, с одной стороны, при вызове функции передавать разные значения. А с другой стороны, работать с результатом этой функции как с промисом и настроить при каждом конкретном вызове обработку полученного значения.

Однако, что если у нас совпадает принцип обработки полученного из асинхронной функции значения?

sum(3, 5).then(function(value){ console.log("Результат операции:", value);}); sum(25, 4).then(function(value){ console.log("Результат операции:", value);});

В этом случае логика обработки будет повторяться. Но поскольку метод then() также возвращает объект Promise, то мы можем сделать следующим образом:

function sum(x, y){ return new Promise(function(resolve){ const result = x + y; resolve(result); }).then(function(value){ console.log("Результат операции:", value);}); } sum(3, 5); sum(25, 4);

Гибкая настройка функции

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

function sum(x, y, func){ // если обработчик не установлен, то устанавливаем обработчик по умолчанию if(func===undefined) func = function(value){ console.log("Результат операции:", value);}; return new Promise(function(resolve){ const result = x + y; resolve(result); }).then(func); } sum(3, 5); sum(25, 4, function(value){ console.log("Сумма:", value);});

Здесь при первом вызове функции sum() (sum(3, 5)) будет срабатывать обработчик по умолчанию. Во втором случае обработчик явным образом передается через третий параметр, соответственно он будет задействован sum(25, 4, function(value){ console.log("Сумма:", value);})

📜 Обработка ошибок в Promise: .catch()

Одним из преимуществ промисов является более простая обработка ошибок. Для получения и обработки ошибки мы можем использовать функцию catch() объекта Promise, которая в качестве параметра принимает функцию обработчика ошибки:

const myPromise = new Promise(function(resolve, reject){ console.log("Выполнение асинхронной операции"); reject("Переданы некорректные данные"); }); myPromise.catch( function(error){ console.log(error); });

Функция catch() в качестве параметра принимает обработчик ошибки. Параметром этой функции-обработчика является то значение, которое передается в reject().

Консольный вывод:

Выполнение асинхронной операции
Переданы некорректные данные
// Promise с ошибкой const myPromise = new Promise(function(resolve, reject) { const y = 0; if (y === 0) { reject("Деление на ноль!"); } else { resolve(10 / y); } }); // Обрабатываем ошибку myPromise .then(value => console.log("Результат:", value)) .catch(error => console.log("Ошибка:", error)); // Вывод: // Ошибка: Деление на ноль!

Генерация ошибок

Выше для извещения о возникшей ошибке вызывалась функция reject(). Но стоит отметить, что ошибка может возникнуть и без вызова функции reject(). И если в выполняемой в промисе операции генерируется ошибка в силу тех или иных причин, то вся операция также завершается ошибкой. Например, в следующем коде вызывается нигде не определенная функция getSomeWork():

const myPromise = new Promise(function(resolve){ console.log("Выполнение асинхронной операции"); getSomeWork(); // вызов не существующей функции resolve("Hello world!"); }); myPromise.catch( function(error){ console.log(error); });

Поскольку функция getSomeWork() нигде не объявлена, то выполнение асинхронной задачи завершится ошибкой и не дойдет до вызова resolve("Hello world!"). Поэтому сработает функция обработки ошибок из catch(), которая через параметр error получит информацию о возникшей ошибке, и в консоли браузера мы увидим сообщение об ошибке:

Выполнение асинхронной операции
ReferenceError: getSomeWork is not defined
    at index.html:39
    at new Promise (<anonymous>)
    at index.html:37

throw

Также ошибка может быть результатом вызова оператора throw, который генерирует ошибку:

cconst myPromise = new Promise(function(resolve, reject){ console.log("Выполнение асинхронной операции"); const parsed = parseInt("Hello"); if (isNaN(parsed)) { throw "Not a number"; // Генерируем ошибку } resolve(parsed); }); myPromise.catch( function(error){ console.log(error); });

Здесь парсится в число случайная строка. И если результат парсинга не представляет число, то с помощью оператора throw генерируем ошибку. Это придет к завершению всей функции с ошибкой. И в итоге результат будет обработан функцией catch:

Выполнение асинхронной операции
Not a number

В этом случае функция обработчика получает сообщение об оошибке, который указывается после оператора throw.

Данная ситуация может показаться искуственной, так как нам нет смысла генерировать в коде выше ошибку с помощью throw, поскольку в этом случае мы также можем передать сообщение об ошибке в функцию reject:

if (isNaN(parsed)) { reject("Not a number"); }

Однако данный оператор может применяться во внешней функции, которую мы вызываем в коде:

function getNumber(str){ const parsed = parseInt(str); if (isNaN(parsed)) throw "Not a number"; // Генерируем ошибку else return parsed; } const myPromise = new Promise(function(resolve){ console.log("Выполнение асинхронной операции"); const result = getNumber("hello"); resolve(result); }); myPromise.catch( function(error){ console.log(error); });

Здесь парсинг строки в число вынесен во внешнюю функцию - getNumber, однако при вызове этой функции в промисе, также из оператора throw возникнет ошибка. И соответственно будет выполняться функция catch(), где роизойдет обработка ошибки.

Практический пример с обработкой ошибок

function generateNumber(str) { return new Promise((resolve, reject) => { const parsed = parseInt(str); if (isNaN(parsed)) { reject("Значение не является числом"); } else { resolve(parsed); } }); } // Успешный случай generateNumber("23") .then(value => console.log("Результат:", value)) .catch(error => console.log("Ошибка:", error)); // Вывод: Результат: 23 // Случай с ошибкой generateNumber("hello") .then(value => console.log("Результат:", value)) .catch(error => console.log("Ошибка:", error)); // Вывод: Ошибка: Значение не является числом

try..catch

Как и в общем случае, операции, которые могут генерировать ошибку, можно помещать в конструкцию try..catch, а при возникновении исключения в блоке catch вызывать функцию reject():

const myPromise = new Promise(function(resolve, reject){ try{ console.log("Выполнение асинхронной операции"); getSomeWork(); // вызов не существующей функции resolve("Hello world!"); } catch(err){ reject(`Произошла ошибка: ${err.message}`); } }); myPromise.catch( function(error){ console.log(error); });

Консольный вывод:

Выполнение асинхронной операции
Произошла ошибка: getSomeWork is not defined

Обработка ошибки с помощью функции then

Кроме функции catch для получения информации об ошибке и ее обработки также можно использовать функцию then() - ее второй параметр представляет обработчик ошибки, который в качестве параметра получает переданное из функции reject значение:

promise .then(function(value){ // получение значения }, function(error){ // обработка ошибки });

Второй параметр функции then() представляет функцию обработчика ошибок. С помощью параметра error в функции-обработчика мы можем получить переданное в reject() значение, либо информацию о возникшей ошибке.

Рассмотрим следуюший пример:

function generateNumber(str){ return new Promise(function(resolve, reject){ const parsed = parseInt(str); if (isNaN(parsed)) reject("значение не является числом") else resolve(parsed); }) .then(function(value){ console.log("Результат операции:", value);}, function(error){ console.log("Возникла ошибка:", error);}); } generateNumber("23"); generateNumber("hello");

В данном случае для того, чтобы в промис можно было передать разные данные, он определен как возващаемый результат функции generateNumber(). То есть в данном случае консольный вывод будет следующим:

Результат операции: 23
Возникла ошибка: значение не является числом

📜 Цепочки Promise

Одним из примуществом промисов является то, что они позволяют создавать цепочки промисов. Так, ранее мы рассмотрели применение методов then() и catch() для получения и обработки результатов и ошибок асинхронной операции. При выполнении эти методы генерируют новый объект Promise, для которого мы также можем вызвать методы then() и catch(), и, таким образом, построить цепочку промисов. Благодаря этому мы можем обрабатывать подряд несколько асинхронных операций - одна за другой.

promise.then(..).then(..).then(..)
function generateNumber(str) { return new Promise((resolve, reject) => { const parsed = parseInt(str); if (isNaN(parsed)) reject("Not a number"); else resolve(parsed); }); } // Цепочка вызовов generateNumber("3") .then(value => value * value) // 9 .then(value => value + 10) // 19 .then(value => console.log("Результат:", value)) // Результат: 19 .catch(error => console.error("Ошибка:", error));

Возвращаемое значение из функции-обработчика в методе then() передается в последующий вызов метода then() в цепочке:

const helloPromise = new Promise(function(resolve){ resolve("Hello"); }) const worldPromise = helloPromise.then(function(value){ // возвращаем новое значение return value + " World"; }); const metanitPromise = worldPromise.then(function(value){ // возвращаем новое значение return value + " from METANIT.COM"; }); metanitPromise.then(function(finalValue){ // получаем финальное значение console.log(finalValue); // Hello World from METANIT.COM });

Здесь для большей ясности весь процесс раздел на раздельные промисы: helloPromise, worldPromise и metanitPromise.

Рассмотрим поэтапно.

  1. Сначала создается промис helloPromise:
    const helloPromise = new Promise(function(resolve){ resolve("Hello"); });

    В асинхронной операции с помощью вызова resolve("Hello") промис переводится в состояние fulfilled, то есть выполнение операции успешно завершено. А во вне передается значение "Hello".

  2. Далее у промиса helloPromise вызывается метод then():
    const worldPromise = helloPromise.then(function(value){ // возвращаем новое значение return value + " World"; });

    В качестве значения параметра value функция обработчика получает строку "Hello" и затем возвращает строку "Hello World". Эта строка затем можно будет получить через метод then() нового промиса, который генерируется вызовом helloPromise.then() и который называется здесь worldPromise.

  3. Затем аналогичным образом у промиса worldPromise вызывается метод then():
    const metanitPromise = worldPromise.then(function(value){ // возвращаем новое значение return value + " from METANIT.COM"; });

    В качестве значения параметра value функция обработчика получает строку "Hello World" и затем возвращает строку "Hello World from METANIT.COM". Вызов worldPromise.then() возвращает новый промис metanitPromise.

  4. На последним этапе у промиса metanitPromise вызывается метод then():
    metanitPromise.then(function(finalValue){ console.log(finalValue); // Hello World from METANIT.COM });

    Здесь через параметр finalValue получаем финальное значение - строку "Hello World from METANIT.COM" и выводим на консоль. После этого цепочка завершена.

Для большей краткости и наглядности мы можем упростить цепочку:

new Promise(resolve => resolve("Hello")) .then(value => value + " World") .then(value => value + " from METANIT.COM") .then(finalValue => console.log(finalValue));

Обработка ошибок

Для обработки ошибок к цепочке в конце добавляется метод catch(), который также возвращет объект Promise. Рассмотрим на простом примере:

function generateNumber(str){ return new Promise((resolve, reject) => { const parsed = parseInt(str); if (isNaN(parsed)) reject("Not a number"); else resolve(parsed); }); }; function printNumber(str){ generateNumber(str) .then(value => console.log(value)) .catch(error => console.log(error)); } printNumber("rty"); // Not a number printNumber("3"); // 3

В данном случае функция generateNumber() возвращает промис, в котором получаем извне некоторое значение, пытаемся конвертировать его в число. В функции printNumber() вызываем эту функцию и у полученного промиса создаем небольшую цепочку из методов then() и catch().

Если конвертация строки в число в промисе прошла успешно, то распарсенное число передачется в функцию resolve():

else resolve(parsed)

В этом случае при получении этого результата срабатывает метод then(), который в данном случае выводит полученное значение на консоль:

.then(value => console.log(value))

Метод catch() при отстутствии ошибок не выполняется.

Однако если передаеваемое значение невозможно конвертировать в число, соответственно в промисе выполняется вызов

if (isNaN(parsed)) reject("Not a number");

В этом случае метод then() игнорируется, и выполнение переходит к вызову

.catch(error => console.log(error));

Обработка ошибок в цепочке промисов

Теперь усложним цепочку. Пусть у нас в цепочке выполняется сразу несколько промисов:

function generateNumber(str){ return new Promise((resolve, reject) => { const parsed = parseInt(str); if (isNaN(parsed)) reject("Not a number"); else resolve(parsed); }); }; function printNumber(str){ generateNumber(str) .then(value => { if (value===4) throw "Несчастливое число"; return value * value; }) .then(finalValue => console.log(`Result: ${finalValue}`)) .catch(error => console.error(error)); } printNumber("rty"); // Not a number printNumber("3"); // Result: 9 printNumber("4"); // Несчастливое число printNumber("5"); // Result: 25

Здесь для простоты весь код вынесен в функцию generateNumber(), которая создает цепочку промисов. В этой цепочке промисов также получаем извне некоторое значение, пытаемся конвертировать его в число, и затем вычисляем его квадрат и выводит на консоль. В конце которой находится метод catch(). В этот метод передается обработчик ошибки, который получает ошибку и выводит ее на консоль. В итоге если в цепочке промисов на одном из этапов генерируется ошибка (в силу внутренней работы кода, например, при генерации ошибки с помощью оператора throw, либо при вызове функции reject()), то все последующие вызовы метода then(), которые содержат только обработку значения, игнорируются, и выполнение цепочки переходит к методу catch().

Для примера вызываем функцию printNumber(), передавая в нее различные исходные данные. Например, при выполнении вызова

printNumber("rty"); // Not a number

Возвращение Promise из catch

При этом стоит отметить, что, поскольку catch() возвращает объект Promise, то далее также можно продолжить цепочку:

function generateNumber(str){ return new Promise((resolve, reject) => { const parsed = parseInt(str); if (isNaN(parsed)) reject("Not a number"); else resolve(parsed); }); }; function printNumber(str){ generateNumber(str) .then(value => value * value) .then(value => console.log(`Result: ${value}`)) .catch(error => console.error(error)) .then(() => console.log("Work has been done")); } printNumber("3"); // Result: 9 // Work has been done

Причем метод then() после catch() будет вызываться, даже если не произошло ошибок и сам метод catch() не выполнялся.

И мы даже можем из функции-обработчика ошибки в catch() также передавать некоторое значение и получать через последующий метод then():

function generateNumber(str){ return new Promise((resolve, reject) => { const parsed = parseInt(str); if (isNaN(parsed)) reject("Not a number"); else resolve(parsed); }); }; function printNumber(str){ generateNumber(str) .then(value => value * value) .then(value => console.log(`Result: ${value}`)) .catch(error => { console.log(error); return 0; }) .then(value => console.log("Status code:", value)); } printNumber("ert3"); // Not a number // Status code: 0

Метод .finally()

Метод .finally() выполняется всегда, независимо от успеха или ошибки:

function printNumber(str) { generateNumber(str) .then(value => { console.log("Значение:", value); return "hello from then"; }) .catch(error => { console.log("Ошибка:", error); return "hello from catch"; }) .finally(() => { console.log("Завершено"); // Выполнится в любом случае }) .then(message => console.log(message)); } printNumber("3"); // Значение: 3 // Завершено // hello from then

В качестве параметра метод принимает функцию, которая выполняет некоторые финальные действия по обработке работы промиса:

function generateNumber(str){ return new Promise((resolve, reject) => { const parsed = parseInt(str); if (isNaN(parsed)) reject("Not a number"); else resolve(parsed); }); }; function printNumber(str){ generateNumber(str) .then(value => console.log(value)) .catch(error => console.log(error)) .finally(() => console.log("End")); } printNumber("3"); printNumber("triuy");

Здесь мы два раза обращаемся к промису, возвращаемому функцией generateNumber() . В одном случае строка удачно сконвертируется в число, в другом же - произойдет ошибка. Однако вне зависимости от отсутствия или наличия ошибки в обоих случаях будет выполняться метод finally(), который выведет на консоль строку "End".

Метод finally() возвращает объект Promise, поэтому после него можно продолжить продолжить цепочку:

function generateNumber(str){ return new Promise((resolve, reject) => { const parsed = parseInt(str); if (isNaN(parsed)) reject("Not a number"); else resolve(parsed); }); }; function printNumber(str){ generateNumber(str) .then(value => console.log(value)) .catch(error => console.log(error)) .finally(() => console.log("Выполнение промиса завершено")) .then(() => console.log("Промис все еще работает")); } printNumber("3");

Консольный вывод:

3
Выполнение промиса завершено
Промис все еще работает

Стоит отметить что в метод then() также можно передать данные. Но эти данные передаются не из метода finally(), а из предыдущего метода then() или catch():

function generateNumber(str){ return new Promise((resolve, reject) => { const parsed = parseInt(str); if (isNaN(parsed)) reject("Not a number"); else resolve(parsed); }); }; function printNumber(str){ generateNumber(str) .then(value => { console.log(value); return "hello from then"; }) .catch(error => { console.log(error); return "hello from catch"; }) .finally(() => { console.log("End"); return "hello from finally"; }) .then(message => console.log(message)); } printNumber("3");
3
End
hello from then

📜 Комбинирование Promise.
Функции Promise.all, Promise.allSettled, Promise.any и Promise.race

Функции Promise.all(), Promise.allSettled() и Promise.race() позволяют сгруппировать выполнение нескольких промисов.

Promise.all() — ждём все промисы

Promise.all() принимает массив промисов и возвращает новый промис, который выполняется, когда все промисы завершены успешно:

Функция Promise.all() возвращает единый объект Promise, который объединяет набор промисов.

Рассмотрим следуюший код:

const promise1 = new Promise((resolve,reject) => { setTimeout(resolve, 1000, "Hello"); }); const promise2 = new Promise((resolve,reject) => { setTimeout(resolve, 500, "World"); }); promise1.then(value => console.log(value)); // Hello promise2.then(value => console.log(value)); // World

Здесь определено два промиса. Асинхронная операция первого промиса выполняется через 1000 миллисекунд, действие второго промиса выполняется через 500 миллисекунд. Оба этих промиса выполняются независимо друг от друга. Консольный вывод:

World
Hello

Функция Promise.all() позволяет объединить несколько промисов и выполнять их параллельно, но как единое целое. В качестве параметра функция принимает набор промисов:

Promise.all([промис1, промис2, ... прромисN]);

Возвращаемым результатом функции является новый объект Promise.

Теперь изменим предыдущий пример, использовав функцию Promise.all():

const promise1 = new Promise((resolve,reject) => { setTimeout(resolve, 1000, "Hello"); }); const promise2 = new Promise((resolve,reject) => { setTimeout(resolve, 500, "World"); }); Promise.all([promise1, promise2]) .then(values => { const [promise1data, promise2data] = values; console.log(promise1data, promise2data); // Hello World });

Теперь данные обоих промисов возвращаются вместе и доступны в методе then() в виде массива values. Консольный вывод:

Hello World

Значения всех промисов возвращаются только если все они завершились успешно. Но если в асинхронной операции хотя бы одного промиса возникнет ошибка в силу внутренней логики или из-за вызова функции reject(), то все промисы перейдут в состояние rejected, соответственно:

const promise1 = new Promise((resolve,reject) => { reject("Непредвиденная ошибка"); setTimeout(resolve, 500, "Hello"); }); const promise2 = new Promise((resolve,reject) => { setTimeout(resolve, 1000, "World"); }); Promise.all([promise1, promise2]) .then(values => { const [promise1data, promise2data] = values; console.log(promise1data, promise2data); }) .catch(error => console.log(error)); // Непредвиденная ошибка

В данном случае мы явным образом переводим переводим первый промис в состояние rejected, соответственно в это состояние переводятся все промисы, переданные в функцию Promise.all(). И далее через метод catch(), как и в обзем случае, мы можем обработать возникшую ошибку.

const promise1 = new Promise((resolve) => { setTimeout(resolve, 1000, "Hello"); }); const promise2 = new Promise((resolve) => { setTimeout(resolve, 500, "World"); }); // Ждём оба промиса Promise.all([promise1, promise2]) .then(values => { const [result1, result2] = values; console.log(result1, result2); // Hello World (через 1 секунду) }); // Или деструктуризация сразу Promise.all([promise1, promise2]) .then(([result1, result2]) => { console.log(result1, result2); });
⚠️ Promise.all() и ошибки

Если хотя бы один промис завершится с ошибкой, Promise.all() сразу отклонится с этой ошибкой, не дожидаясь остальных промисов.

Promise.allSettled() — ждём все результаты

Promise.allSettled() ждёт завершения всех промисов, независимо от успеха или ошибки:

const promise1 = Promise.resolve("Успех"); const promise2 = Promise.reject("Ошибка"); const promise3 = Promise.resolve("Ещё успех"); Promise.allSettled([promise1, promise2, promise3]) .then(results => { results.forEach(result => { if (result.status === "fulfilled") { console.log("✅", result.value); } else { console.log("❌", result.reason); } }); }); // Вывод: // ✅ Успех // ❌ Ошибка // ✅ Ещё успех

Promise.allSettled() также как и Promise.all() принимает набор промисов и выполняет их как единое целое, но возвращает объект со статусом и результатом промиса:

const promise1 = new Promise((resolve,reject) => { reject("Непредвиденная ошибка"); setTimeout(resolve, 500, "Hello"); }); const promise2 = new Promise((resolve,reject) => { setTimeout(resolve, 1000, "World"); }); Promise.allSettled([promise1, promise2]) .then(values => { const [promise1data, promise2data] = values; console.log(promise1data); // {status: "rejected", reason: "Непредвиденная ошибка"} console.log(promise2data); // {status: "fulfilled", value: "World"} });

При этом при возникновении ошибки в одном из промисов (как в случае выше с первым промисом) функция также передается результаты в метод then(), который следует дальше в цепочке. Каждый результат представляет объект. Первое свойство этого объекта - status описывает статус или состояние промиса. Если произошла ошибка, статус rejected, а второе свойство представляет объект ошибки. Если промис успешно завершил выполнение, то статус fulfilled, а второе свойство - value хранит результат промиса.

Promise.race() — первый завершившийся

Функция Promise.race() также принимает несколько промисов, только возвращает первый завершенный промис (вне зависимости завершился от успешно или с ошибкой):

const promise1 = new Promise(resolve => setTimeout(resolve, 1000, "Медленный")); const promise2 = new Promise(resolve => setTimeout(resolve, 500, "Быстрый")); Promise.race([promise1, promise2]) .then(value => console.log(value)); // Быстрый (через 500мс)

Другой пример:

const promise1 = new Promise((resolve) => { setTimeout(resolve, 500, "Hello"); }); const promise2 = new Promise((resolve) => { setTimeout(resolve, 1000, "World"); }); Promise.race([promise1, promise2]) .then(value => console.log(value)) // Hello .catch(error => console.log(error));

В данном случае первым выполненным будет промис promise1. Поэтому в метод then(value => console.log(value)) в качестве value будет передана строка "Hello".

Promise.any() — первый успешный

Функция Promise.any() принимает несколько промисов и возвращает первый успешно завершившийся промис:

const promise1 = Promise.reject("Ошибка 1"); const promise2 = new Promise(resolve => setTimeout(resolve, 1000, "Успех 2")); const promise3 = new Promise(resolve => setTimeout(resolve, 500, "Успех 3")); Promise.any([promise1, promise2, promise3]) .then(value => console.log(value)) // Успех 3 (первый успешный) .catch(errors => console.log(errors));

Другой пример:

const promise1 = new Promise((resolve, reject) => { reject("error in promise1"); setTimeout(resolve, 500, "Hello"); }); const promise2 = new Promise((resolve) => { setTimeout(resolve, 1000, "World"); }); Promise.any([promise1, promise2]) .then(value => console.log(value)) // World .catch(error => console.log(error));

В данном случае первым выполненным будет промис promise1, однако он завершается с ошибкой. Поэтому в метод then(value => console.log(value)) в качестве value будет передана строка "World" - результат промиса promise2, который успешно завершается.

Может показаться, что Promise.any() делает то же самое, что и Promise.race(), однако эти функции отличаются в отношении того, как они работают с промисами, которые завершились с ошибкой. Promise.race() возвращает первый завершенный промис, вне зависимости завершился он с ошибкой или нет. А Promise.any() возвращает первый успешно завершенный промис (если такой имеется). Если же все промисы завершились с ошибкой, то генерируется исключение типа AggregateError:

const promise1 = new Promise((resolve, reject) => { reject("error in promise1"); setTimeout(resolve, 500, "Hello"); }); const promise2 = new Promise((resolve, reject) => { reject("error in promise2"); setTimeout(resolve, 1000, "World"); }); Promise.any([promise1, promise2]) .then(value => console.log(value)) .catch(error => console.log(error)); // AggregateError: All promises were rejected

С помощью свойства errors типа AggregateError можно получить в виде массива все ошибки, которые возникли в промисах:

const promise1 = new Promise((resolve, reject) => { reject("error in promise1"); setTimeout(resolve, 500, "Hello"); }); const promise2 = new Promise((resolve, reject) => { reject("error in promise2"); setTimeout(resolve, 1000, "World"); }); Promise.any([promise1, promise2]) .then(value => console.log(value)) .catch(e => console.log(e.errors)); // ["error in promise1", "error in promise2"]
Метод Когда завершается Результат
Promise.all() Когда все успешны или первая ошибка Массив результатов или ошибка
Promise.allSettled() Когда все завершены Массив объектов {status, value/reason}
Promise.race() Первый завершившийся (успех/ошибка) Результат или ошибка первого
Promise.any() Первый успешный Результат первого успешного

📜 Async/Await — современный синтаксис

Внедение стандарта ES2017 в JavaScript привнесло два новых оператора: async и await, который призваны упростить работу с промисами.

async/await — это синтаксический сахар над промисами, который делает асинхронный код похожим на синхронный.

Ключевое слово async

async превращает обычную функцию в асинхронную. Такая функция всегда возвращает Promise:

// Обычная функция function regularFunction() { return "Hello"; } // Асинхронная функция async function asyncFunction() { return "Hello"; } // asyncFunction() автоматически возвращает Promise asyncFunction().then(value => console.log(value)); // Hello

Оператор async определяет асинхронную функцию, в которой, как предполагается, будет выполняться одна или несколько асинхронных задач:

async function название_функции(){ // асинхронные операции }

Ключевое слово await

Внутри асинхронной функции мы можем применить оператор await. Он ставится перед вызовом асинхронной операции, которая представляет объект Promise:

async function название_функции(){ await асинхронная_операция(); }

await приостанавливает выполнение async-функции до получения результата промиса. Работает только внутри async-функций:

function sum(x, y) { return new Promise(function(resolve) { const result = x + y; resolve(result); }); } // Асинхронная функция с await async function calculate() { const value = await sum(5, 3); // Ждём результата console.log("Результат:", value); // Результат: 8 } calculate();

Рассмотрим простейший пример с использованием Promise:

function sum(x, y){ return new Promise(function(resolve){ const result = x + y; resolve(result); }); } sum(5, 3).then(function(value){ console.log("Результат асинхронной операции:", value); }); // Результат асинхронной операции: 8

В данной случае функция sum() представляет асинхронную задачу. Она принимает два числа и возвращает объект Promise, в котором выполняется сложение этих чисел. Результат сложения передается в функцию resolve(). И далее в методе then() мы можем получить этот результат и выполнить с ним различные действия.

Теперь перепишем этот пример с использованием async/await:

function sum(x, y){ return new Promise(function(resolve){ const result = x + y; resolve(result); }); } // сокращенный вариант // function sum(x, y){ return Promise.resolve(x + y);} async function calculate(){ const value = await sum(5, 3); console.log("Результат асинхронной операции:", value); } calculate(); // Результат асинхронной операции: 8

Здесь мы определили асинхронную функцию calculate(), к которой применяется async:

async function calculate(){

Внутри функции вызывается асинхронная операция sum(), которой передаются некоторые значения. Причем к этой функции применяется оператор await. Благодаря оператору await больше нет надобности вызывать у промиса метод then(). А результат, который возвращает Promise, мы можем получить напрямую из вызова функции sum и, например, присвоить константе или переменной:

const value = await sum(5, 3);

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

calculate();

Стоит отметить, что для функции, которая определена со словом async неявно создается объект Promise. Поэтому ее также можно использовать как промис:

function sum(x, y){ return new Promise(function(resolve){ const result = x + y; resolve(result); }); } async function calculate(){ const result = await sum(5, 3); return result; } calculate().then(value=> console.log("Результат асинхронной операции:", value));

Сравнение: Promise vs Async/Await

// С Promise (цепочка .then) sum(5, 3) .then(result => { console.log("Результат 1:", result); return sum(result, 10); }) .then(result => { console.log("Результат 2:", result); }); // С Async/Await (выглядит синхронно) async function calculate() { const result1 = await sum(5, 3); console.log("Результат 1:", result1); const result2 = await sum(result1, 10); console.log("Результат 2:", result2); } calculate();
✅ Преимущества async/await
  • Код выглядит как синхронный — легче читать
  • Проще обрабатывать ошибки (try/catch)
  • Удобнее работать с условиями и циклами
  • Легче отлаживать

Выполнение последовательности асинхронных операций (несколько await подряд)

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

function sum(x, y) { return new Promise(resolve => { const result = x + y; resolve(result); }); } async function calculate() { const value1 = await sum(5, 3); console.log("Результат 1 асинхронной операции:", value1); // 8 const value2 = await sum(6, 4); console.log("Результат 2 асинхронной операции:", value2); // 10 const value3 = await sum(7, 5); console.log("Результат 3 асинхронной операции:", value3); // 12 } calculate(); // Результат 1 асинхронной операции: 8 // Результат 2 асинхронной операции: 10 // Результат 3 асинхронной операции: 12

📜 Обработка ошибок в async/await

Для обработки ошибок используется конструкция try...catch...finally:

Например, возьмем следующий код с использованием Promise:

function square(str){ return new Promise((resolve, reject) => { const n = parseInt(str); if (isNaN(n)) reject("Not a number"); else resolve(n * n); }); }; function calculate(str){ square(str) .then(value => console.log("Result: ", value)) .catch(error => console.log(error)); } calculate("g8"); // Not a number calculate("4"); // Result: 16

Функция square() получает некоторое значение, в промисе это значение преобразуется в число. И при удачном преобразовании из промиса возвращается квадра числа. Если переданное значение не является числом, то возвращаем ошибку.

При вызове функции square() с помощью метода catch() можно обработать возникшую ошибку.

Теперь перепишем пример с использованием async/await:

function square(str) { return new Promise((resolve, reject) => { const n = parseInt(str); if (isNaN(n)) reject("Not a number"); else resolve(n * n); }); } async function calculate(str) { try { const value = await square(str); console.log("Результат:", value); } catch (error) { console.log("Ошибка:", error); } } calculate("4"); // Результат: 16 calculate("abc"); // Ошибка: Not a number

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

try...catch...finally

async function fetchUserData(userId) { try { console.log("Загрузка данных..."); const response = await fetch(`/api/users/${userId}`); const data = await response.json(); return data; } catch (error) { console.error("Ошибка загрузки:", error); return null; } finally { console.log("Запрос завершён"); } }

📜 Параллельное выполнение async/await

Последовательное выполнение (медленно)

async function loadData() { // Каждый запрос ждёт предыдущего (МЕДЛЕННО) const user = await fetch('/api/user'); const posts = await fetch('/api/posts'); const comments = await fetch('/api/comments'); // Общее время: 3 секунды (1+1+1) }

Параллельное выполнение (быстро)

async function loadData() { // Запускаем все запросы одновременно const [user, posts, comments] = await Promise.all([ fetch('/api/user'), fetch('/api/posts'), fetch('/api/comments') ]); // Общее время: 1 секунда (максимум из трёх) }

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

Пример 1: Загрузка данных с API

async function loadUser(userId) { try { // Загружаем данные пользователя const response = await fetch(`https://api.example.com/users/${userId}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const user = await response.json(); console.log("Пользователь:", user.name); return user; } catch (error) { console.error("Ошибка:", error.message); return null; } } loadUser(1);

Пример 2: Цепочка зависимых запросов

async function loadUserWithPosts(userId) { try { // 1. Загружаем пользователя const userResponse = await fetch(`/api/users/${userId}`); const user = await userResponse.json(); // 2. Загружаем его посты (используем user.id) const postsResponse = await fetch(`/api/users/${user.id}/posts`); const posts = await postsResponse.json(); // 3. Возвращаем объединённые данные return { user: user, posts: posts }; } catch (error) { console.error("Ошибка:", error); return null; } } loadUserWithPosts(1).then(data => { console.log("Данные:", data); });

Пример 3: Загрузка с таймаутом

// Функция для добавления таймаута function timeout(ms) { return new Promise((_, reject) => { setTimeout(() => reject(new Error('Timeout')), ms); }); } async function fetchWithTimeout(url, ms = 5000) { try { const response = await Promise.race([ fetch(url), timeout(ms) ]); return await response.json(); } catch (error) { if (error.message === 'Timeout') { console.log('Запрос превысил время ожидания'); } else { console.error('Ошибка:', error); } return null; } } fetchWithTimeout('https://api.example.com/slow-endpoint', 3000);

Пример 4: Повторные попытки (retry)

async function fetchWithRetry(url, retries = 3) { for (let i = 0; i < retries; i++) { try { const response = await fetch(url); if (response.ok) { return await response.json(); } } catch (error) { console.log(`Попытка ${i + 1} не удалась`); if (i === retries - 1) { throw new Error(`Не удалось загрузить после ${retries} попыток`); } // Ждём перед следующей попыткой await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1))); } } } fetchWithRetry('https://api.example.com/unstable');

📜 Асинхронные итераторы

Асинхронные итераторы объединяют возможности итераторов и операторов async и await. Асинхронные итераторы прежде всего предназначены для обращения к источникам данных данных, которые используют асинхронный API. Это могут быть какие-нибудь данные, которые загружаются по части, например, по сети, из файловой системы или из базы данных.

Из статьи про итераторы мы должны помнить, что интератор предоставляет метод next(), который возвращает объект с двумя свойствами: { value, done }. Свойство value хранит некоторое значение, которое, например, можно получить в цикле for..of при переборе объекта. А свойство done указывает, завершен ли перебор объектов. Если это свойство равно false, значит, итератор еще не завершил перебор объектов, и есть еще доступные объекты. Если свойство равно true, то перебор закончен, и в наборе больше нет доступных для перебора объектов.

Асинхронный итератор похож на обычный синхронный за тем исключением, что его метод next() возвращает объект Promise. А из промиса, в свою очередь, возвращается объект { value, done }.

Цикл for-await-of

Для получения данных с помощью асинхронных итераторов применяется цикл for-await-of:

for await (variable of iterable) { // действия }

В цикле for-await-of после оператора of идет некоторый набор данных, который можно перебрать по элементам. Это может асинхронный источник данных, но также может быть и синхронный источник данных, как массивы или, например, встроенные объекты String, Map, Set и т.д.

Стоит отметить, что данная форма цикла может использоваться только в функциях, определенных с оператором async.

Рассмотрим простейший пример, где в качестве источника данных выступает обычный массив:

const dataSource = ["Tom", "Sam", "Bob"]; async function readData(){ for await (const item of dataSource) { console.log(item); } } readData(); // Tom // Sam // Bob

Здесь в цикле происходит перебор массива dataSource. При выполнении цикла для источника данных (в данном случае для массива) с помощью метода [Symbol.asyncIterator]() неявно создается асинхронный итератор. И при каждом обращении к очередному элементу в этом источнике данных неявно из итератора возвращается объект Promise, из которого и получаем текущий элемент массива.

Создание асинхронного итератора

В примере выше асинхронный итератор создавался неявно. Но мы также можем его определить явно. Например, определим асинхронный итератор, который возвращает элементы массива:

const generatePerson = { [Symbol.asyncIterator]() { return { index: 0, people: ["Tom", "Sam", "Bob"], next() { if (this.index < this.people.length) { return Promise.resolve({ value: this.people[this.index++], done: false }); } return Promise.resolve({ done: true }); } }; } };

Итак, здесь определен объект generatePerson, в котором реализован только один метод - [Symbol.asyncIterator](), который по сути и представляет асинхронный итератор. Реализация асинхронного итератора (как и в случае с синхронным итератором) позволяет сделать объект generatePerson перебираемым.

Основные моменты асинхронного итератора:

  • Асинхронный итератор реализуется методом [Symbol.asyncIterator](), который возвращает объект.
  • Возвращаемый объект итератора имеет метод next(), который возвращает объект Promise.
  • Объект Promise, в свою очередь, возвращает объект с двумя свойстами { value, done }. Свойство value собственно хранит некоторое значение. А свойство done указывает, завершен ли перебор и соответственно, есть ли в наборе доступные для перебора объекты. Если свойство done равно true (перебор закончен, и доступных для перебора объектов больше нет), то нет смысла указывать свойство value

В данном случае итератор реализует простую задачу - возвращает очереднего пользователя. Для хранения пользователей в объекте итератора определен массив people, а для хранения индекса текущего элемента массива определена переменная index.

index: 0, people: ["Tom", "Sam", "Bob"],

В методе next() возвращаем объект Promise. Если текущий индекс меньше длины массивы (то есть в массиве еще имеются для перебора элементы), то возвращаем Promise, в котором возвращаем элемент массива по текущему индексу:

return Promise.resolve({ value: this.people[this.index++], done: false });

Если все элементы массива уже получены, то возвращаем Promise с объектом { done: true }:

return Promise.resolve({ done: true });

Где значение done: true будет указывать внешнему коду, что все значения итератора уже получены.

Теперь посмотрим, как мы можем получить из итератора данные:

Как и с обычным итератором, мы можем обратиться к самому асинхронному итератору:

generatePerson[Symbol.asyncIterator](); // получаем асинхронный итератор

И вызвать явным образом его метод next():

generatePerson[Symbol.asyncIterator]().next(); // Promise

Этот метод возвращает Promise, у котоого можно вызвать метод then() и обработать его значение:

generatePerson[Symbol.asyncIterator]() .next() .then((data)=>console.log(data.value)); // Tom

Поскольку метод next() возвращает Promise, то мы можем использовать оператор await для получения значений:

async function printPeople(){ const peopleIterator = generatePerson[Symbol.asyncIterator](); while(!(personData = await peopleIterator.next()).done){ console.log(personData.value); } } printPeople();

Здесь в асинхронной функции цикле while с помощью оператора await последовательно получаем из итератора один за другим объекты Promise, из которых извлекаем данные, пока не достигнем конца данных итератора.

Однако для перебора объекта асинхронного итератора гораздо проще использовать выше рассмотренный цикл for-await-of:

const generatePerson = { [Symbol.asyncIterator]() { return { index: 0, people: ["Tom", "Sam", "Bob"], next() { if (this.index < this.people.length) { return Promise.resolve({ value: this.people[this.index++], done: false }); } return Promise.resolve({ done: true }); } }; } }; async function printPeople(){ for await (const person of generatePerson) { console.log(person); } } printPeople();

Поскольку объект generatePerson реализует метод [Symbol.asyncIterator](), то мы его можем перебрать с омощью цикла for-await-of. Соответственно при каждом обращении в цикле метод next() будет возращать промис с очередным элементом из массива people. И в итоге мы получим следующий консольный вывод:

Tom
Sam
Bob

Стоит отметить, что мы НЕ можем использовать для перебора объекта с асинхронным итератором обычный цикл for-of.

Еще один простейший пример - получение чисел:

const generateNumber = { [Symbol.asyncIterator]() { return { current: 0, end: 10, next() { if (this.current <= this.end) { return Promise.resolve({ value: this.current++, done: false }); } return Promise.resolve({ done: true }); } }; } }; async function printNumbers(){ for await (const n of generateNumber) { console.log(n); } } printNumbers();

Здесь асинхронный итератор объекта generateNumber возвращает числа от 0 до 10.

Еще пример:

const asyncIterable = { [Symbol.asyncIterator]() { return { current: 0, end: 5, async next() { await new Promise(resolve => setTimeout(resolve, 500)); if (this.current <= this.end) { return { value: this.current++, done: false }; } return { done: true }; } }; } }; // Использование с for await...of async function printNumbers() { for await (const num of asyncIterable) { console.log(num); // 0, 1, 2, 3, 4, 5 (с задержкой) } } printNumbers();

📜 Асинхронный генератор

Асинхронные итераторы открывают нам путь к созданию асинхронных генераторов. Асинхронные генераторы позволяют нам использовать оператор await и получать и возвращать данные асинхронным образом.

Для определения асинхронного генератора перед функцией генератора ставится оператор async

async function* название_функции_генератора(){ yield возвращаемое_значение; }

Рассмотрим простейший генератор:

async function* generatePersonAsync(){ yield "Tom"; }

Здесь определен асинхронный генератор generatePersonAsync, в котором возвращается одно значение - строка "Tom".

Особенностью асинхронного генератора является то, что при обращении к его методу next() возвращается объект Promise. А полученный объект Promise, в свою очередь, возвращает объект с двумя свойствами { value, done }, где value собственно хранит возвращаемое значение, а done указывает, доступны ли в генераторе еще данные.

Например, возьмем выше определенный асинхронный генератор:

async function* generatePersonAsync(){ yield "Tom"; } const personGenerator = generatePersonAsync(); personGenerator.next(); // Promise

Здесь с помощью метода next() получаем промис. Далее через метод then() мы можем получить из промиса объект:

const personGenerator = generatePersonAsync(); personGenerator.next() .then(data => console.log(data)); // {value: "Tom", done: false}

И при обращении к свойству value полученного из промиса получить сами данные:

const personGenerator = generatePersonAsync(); personGenerator.next() .then(data => console.log(data.value)); // Tom

С помощью оператора await из метода next() генератора мы можем получить данные:

async function* generatePersonAsync(){ yield "Tom"; yield "Sam"; yield "Bob"; } async function printPeopleAsync(){ const personGenerator = generatePersonAsync(); while(!(person = await personGenerator.next()).done){ console.log(person.value); } } printPeopleAsync();

Консольный вывод:

Tom
Sam
Bob

Поскольку асинхронный генератор представляет асинхронный итератор, то данные генератора также можно получить через цикл for-await-of:

async function* generatePersonAsync(){ yield "Tom"; yield "Sam"; yield "Bob"; } async function printPeopleAsync(){ const personGenerator = generatePersonAsync(); for await(person of personGenerator){ console.log(person); } } printPeopleAsync(); // Tom // Sam // Bob

await в асинхронных генераторах

Главным преимуществом асинхронным генераторов является то, что мы можем использовать в них оператор await и соответственно получать данные из источников данных, которые используют асинхронный API.

async function* generatePersonAsync(people){ for(const person of people) yield await new Promise(resolve => setTimeout(() => resolve(person), 2000)); } async function printPeopleAsync(people){ for await (const item of generatePersonAsync(people)) { console.log(item); } } printPeopleAsync(["Tom", "Sam", "Bob"]);

Здесь для имитации получения данных из асинхронного источника данных применяется промис, который через 2000 секуд возвращает один из элементов массива, который передается в функцию генератора:

yield await new Promise(resolve => setTimeout(() => resolve(person), 2000));
// Определение асинхронного генератора async function* generateNumbers() { for (let i = 0; i <= 5; i++) { await new Promise(resolve => setTimeout(resolve, 500)); yield i; } } // Использование async function printNumbers() { for await (const num of generateNumbers()) { console.log(num); // 0, 1, 2, 3, 4, 5 (с задержкой) } } printNumbers();

Практический пример: пагинация API

async function* fetchPages(url) { let page = 1; let hasMore = true; while (hasMore) { const response = await fetch(`${url}?page=${page}`); const data = await response.json(); yield data; hasMore = data.hasNextPage; page++; } } // Использование async function loadAllPages() { for await (const pageData of fetchPages('/api/items')) { console.log(`Страница получена:`, pageData.items.length, 'элементов'); } } loadAllPages();

📜 Частые ошибки и лучшие практики

❌ Частые ошибки
  • Забыть await: const data = getUserData(); вернёт Promise, а не данные
  • Использовать await в не-async функции: приведёт к синтаксической ошибке
  • Не обрабатывать ошибки: всегда используйте try/catch или .catch()
  • Последовательное выполнение вместо параллельного: используйте Promise.all() для независимых операций
  • Забыть return в async функции: если нужно вернуть значение
✅ Лучшие практики
  • Всегда обрабатывайте ошибки: try/catch для async/await, .catch() для промисов
  • Используйте Promise.all() для параллельных операций: когда операции независимы
  • Избегайте смешивания .then() и await: выберите один стиль
  • Не забывайте await: иначе получите Promise вместо значения
  • Используйте async/await для читаемости: особенно в сложной логике
  • Добавляйте таймауты для fetch: чтобы избежать бесконечного ожидания

Примеры правильного кода

// ❌ ПЛОХО: забыли await async function getUserName(id) { const user = fetch(`/api/users/${id}`); // Promise, не данные! return user.name; // undefined или ошибка } // ✅ ХОРОШО: используем await async function getUserName(id) { const response = await fetch(`/api/users/${id}`); const user = await response.json(); return user.name; } // ❌ ПЛОХО: последовательное выполнение async function loadData() { const users = await fetch('/api/users'); const posts = await fetch('/api/posts'); // Ждёт users return { users, posts }; } // ✅ ХОРОШО: параллельное выполнение async function loadData() { const [users, posts] = await Promise.all([ fetch('/api/users'), fetch('/api/posts') ]); return { users, posts }; } // ❌ ПЛОХО: нет обработки ошибок async function loadUser(id) { const response = await fetch(`/api/users/${id}`); return await response.json(); } // ✅ ХОРОШО: с обработкой ошибок async function loadUser(id) { try { const response = await fetch(`/api/users/${id}`); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return await response.json(); } catch (error) { console.error('Ошибка загрузки:', error); return null; } }

📜 Сравнительная таблица подходов

Подход Преимущества Недостатки Когда использовать
Callbacks Простота, поддержка старых браузеров Callback hell, сложная обработка ошибок Простые случаи, legacy-код
Promises Цепочки, лучше обработка ошибок, избежание callback hell Всё ещё может быть сложно читать Когда нужна совместимость с ES6+
Async/Await Читаемый код, легко отлаживать, try/catch Требует ES2017+ Современная разработка (рекомендуется)

📜 Реальные примеры использования

Пример 5: Загрузка и обработка изображений

async function loadAndProcessImages(urls) { try { console.log('Загрузка изображений...'); // Загружаем все изображения параллельно const imagePromises = urls.map(url => fetch(url).then(r => r.blob()) ); const images = await Promise.all(imagePromises); console.log(`Загружено ${images.length} изображений`); // Обрабатываем изображения images.forEach((blob, index) => { const img = document.createElement('img'); img.src = URL.createObjectURL(blob); document.body.appendChild(img); }); } catch (error) { console.error('Ошибка загрузки:', error); } } const imageUrls = [ 'https://example.com/image1.jpg', 'https://example.com/image2.jpg', 'https://example.com/image3.jpg' ]; loadAndProcessImages(imageUrls);

Пример 6: Очередь асинхронных задач

class AsyncQueue { constructor() { this.queue = []; this.processing = false; } async add(asyncFunction) { this.queue.push(asyncFunction); if (!this.processing) { await this.process(); } } async process() { this.processing = true; while (this.queue.length > 0) { const task = this.queue.shift(); try { await task(); } catch (error) { console.error('Ошибка выполнения задачи:', error); } } this.processing = false; } } // Использование const queue = new AsyncQueue(); queue.add(async () => { console.log('Задача 1 начата'); await new Promise(r => setTimeout(r, 1000)); console.log('Задача 1 завершена'); }); queue.add(async () => { console.log('Задача 2 начата'); await new Promise(r => setTimeout(r, 500)); console.log('Задача 2 завершена'); });

Пример 7: Кэширование асинхронных результатов

class AsyncCache { constructor() { this.cache = new Map(); } async get(key, fetchFunction) { // Проверяем кэш if (this.cache.has(key)) { console.log(`Взято из кэша: ${key}`); return this.cache.get(key); } // Загружаем данные console.log(`Загрузка: ${key}`); const value = await fetchFunction(); // Сохраняем в кэш this.cache.set(key, value); return value; } clear() { this.cache.clear(); } } // Использование const cache = new AsyncCache(); async function getUserData(userId) { return cache.get(`user_${userId}`, async () => { const response = await fetch(`/api/users/${userId}`); return await response.json(); }); } // Первый вызов - загрузит с сервера await getUserData(1); // "Загрузка: user_1" // Второй вызов - возьмёт из кэша await getUserData(1); // "Взято из кэша: user_1"

Пример 8: Прогресс загрузки

async function downloadWithProgress(url) { try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } // Получаем размер файла const contentLength = response.headers.get('content-length'); const total = parseInt(contentLength, 10); // Читаем поток данных const reader = response.body.getReader(); let loaded = 0; const chunks = []; while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); loaded += value.length; // Вычисляем прогресс const progress = Math.round((loaded / total) * 100); console.log(`Загружено: ${progress}%`); } // Собираем все части в один массив const allChunks = new Uint8Array(loaded); let position = 0; for (const chunk of chunks) { allChunks.set(chunk, position); position += chunk.length; } return allChunks; } catch (error) { console.error('Ошибка загрузки:', error); throw error; } } downloadWithProgress('https://example.com/large-file.zip');

Пример 9: Отмена асинхронной операции (AbortController)

// Создаём контроллер отмены const controller = new AbortController(); const signal = controller.signal; // Функция с возможностью отмены async function fetchWithCancel(url, signal) { try { const response = await fetch(url, { signal }); return await response.json(); } catch (error) { if (error.name === 'AbortError') { console.log('Запрос отменён'); } else { console.error('Ошибка:', error); } return null; } } // Запускаем запрос const dataPromise = fetchWithCancel('https://api.example.com/slow', signal); // Отменяем через 2 секунды setTimeout(() => { controller.abort(); console.log('Отправлен сигнал отмены'); }, 2000); await dataPromise;

Пример 10: Пакетная обработка с лимитом одновременных операций

async function processInBatches(items, batchSize, processFn) { const results = []; for (let i = 0; i < items.length; i += batchSize) { const batch = items.slice(i, i + batchSize); console.log(`Обработка партии ${i / batchSize + 1}`); // Обрабатываем партию параллельно const batchResults = await Promise.all( batch.map(item => processFn(item)) ); results.push(...batchResults); } return results; } // Использование const userIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; async function loadUser(id) { const response = await fetch(`/api/users/${id}`); return await response.json(); } // Загружаем по 3 пользователя одновременно const users = await processInBatches(userIds, 3, loadUser); console.log(`Загружено ${users.length} пользователей`);

📜 Отладка асинхронного кода

💡 Советы по отладке
  • Используйте async/await: код легче отлаживать в DevTools
  • Добавляйте логирование: console.log перед и после await
  • Breakpoints: ставьте точки останова на строки с await
  • Promise rejection: всегда ловите ошибки через .catch() или try/catch
  • Unhandled rejection: отслеживайте событие unhandledrejection

Отслеживание необработанных ошибок

// Глобальный обработчик необработанных промисов window.addEventListener('unhandledrejection', (event) => { console.error('Необработанная ошибка промиса:', event.reason); console.error('Промис:', event.promise); // Можно отправить на сервер для логирования // sendErrorToServer(event.reason); // Предотвращаем стандартное поведение event.preventDefault(); });

Логирование с временными метками

async function loggedFetch(url) { const startTime = Date.now(); console.log(`[${new Date().toISOString()}] Начало запроса: ${url}`); try { const response = await fetch(url); const data = await response.json(); const duration = Date.now() - startTime; console.log(`[${new Date().toISOString()}] Запрос выполнен за ${duration}мс`); return data; } catch (error) { const duration = Date.now() - startTime; console.error(`[${new Date().toISOString()}] Ошибка через ${duration}мс:`, error); throw error; } } loggedFetch('https://api.example.com/data');

📜 Производительность и оптимизация

✅ Оптимизация асинхронного кода
  • Параллелизм: используйте Promise.all() для независимых операций
  • Кэширование: сохраняйте результаты дорогих операций
  • Debounce/Throttle: для частых асинхронных вызовов (поиск, скролл)
  • Ленивая загрузка: загружайте данные по требованию
  • Пагинация: загружайте данные порциями, а не всё сразу
  • Отмена запросов: используйте AbortController для ненужных запросов

Пример: Debounce для асинхронного поиска

function debounce(func, delay) { let timeoutId; return function(...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => func.apply(this, args), delay); }; } // Асинхронная функция поиска async function search(query) { const response = await fetch(`/api/search?q=${query}`); return await response.json(); } // Обёртка с debounce const debouncedSearch = debounce(async (query) => { console.log('Поиск:', query); const results = await search(query); displayResults(results); }, 300); // При вводе в поле поиска searchInput.addEventListener('input', (e) => { debouncedSearch(e.target.value); });

📜 Заключение

📚 Основные выводы
  • Promise — основа асинхронности в JS (3 состояния: pending, fulfilled, rejected)
  • async/await — синтаксический сахар над промисами для читаемого кода
  • Promise.all() — для параллельного выполнения независимых операций
  • try/catch — стандартный способ обработки ошибок в async/await
  • Обработка ошибок обязательна — всегда используйте .catch() или try/catch
  • Выбирайте правильный подход — async/await для сложной логики, Promise для простых цепочек

Шпаргалка по выбору метода

// Одна операция const result = await fetch('/api/data'); // Несколько независимых операций → Promise.all() const [users, posts] = await Promise.all([ fetch('/api/users'), fetch('/api/posts') ]); // Нужен первый успешный → Promise.any() const fastest = await Promise.any([ fetch('/api/server1'), fetch('/api/server2'), fetch('/api/server3') ]); // Нужен первый завершившийся → Promise.race() const result = await Promise.race([ fetch('/api/data'), timeout(5000) ]); // Нужны все результаты (успех и ошибки) → Promise.allSettled() const results = await Promise.allSettled([ fetch('/api/endpoint1'), fetch('/api/endpoint2'), fetch('/api/endpoint3') ]);

Глава 18. AJAX-запросы и XMLHttpRequest

📜 Что такое AJAX?

AJAX (Asynchronous JavaScript And XML) — это технология для отправки запросов к серверу из клиентского кода JavaScript без перезагрузки страницы.

💡 История термина

Изначально AJAX предполагал асинхронное взаимодействие клиента и сервера посредством данных в формате XML. Однако сейчас XML во многом вытеснил формат JSON, но название AJAX закрепилось.

Зачем нужен AJAX?

  • Динамическая загрузка данных — обновление части страницы без полной перезагрузки
  • Улучшение UX — пользователь не видит "мигание" страницы
  • Снижение нагрузки — загружается только нужная информация, а не вся страница
  • Интерактивность — возможность создавать SPA (Single Page Applications)

📜 Объект XMLHttpRequest

Одним из основных способов для отправки AJAX-запросов является использование объекта XMLHttpRequest.

Создание объекта

// Создание объекта XMLHttpRequest const xhr = new XMLHttpRequest();
⚠️ Устаревающая технология

XMLHttpRequest — это старый API для AJAX-запросов. В современной разработке рекомендуется использовать Fetch API (см. главу 19), который проще и мощнее. Однако XMLHttpRequest всё ещё поддерживается во всех браузерах и используется в legacy-коде.

📜 Основные методы XMLHttpRequest

Метод Описание
open(method, url[, async[, user[, password]]]) Инициализирует запрос
send(data) Отправляет запрос на сервер
setRequestHeader(header, value) Устанавливает значение value для заголовка header, который будет отправляться в запросе
abort() Прерывает запрос
getAllResponseHeaders() Возвращает все заголовки ответа
getResponseHeader(header) Возвращает конкретный заголовок
overrideMimeType(mime) переопределяет MIME-тип, возвращаемый сервером
getResponseHeader(header) Возвращает конкретный заголовок

Метод open()

Эта функция инициализирует запрос. Принимает пять параметров, из которых первые два являются обязательными:

  • method — тип запроса ("GET", "POST", "PUT", "DELETE" и т.д.)
  • url — адрес ресурса к которому отправляется запрос
  • async — асинхронный режим (true по умолчанию)

    async: логическое значение, которое указывает, будет ли запрос асинхронным. Если значение true (значение по умолчанию), то запрос асинхронный

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

  • user — имя пользователя для аутентификации (опционально), которое применяется при его аутентификации на сервере (то есть для определения, какой именно пользователь осуществил запрос), по умолчанию равно null
  • password — пароль для аутентификации (опционально, по умолчанию равно null)
// GET-запрос xhr.open("GET", "/api/users"); // POST-запрос xhr.open("POST", "/api/users"); // Синхронный запрос (НЕ РЕКОМЕНДУЕТСЯ!) xhr.open("GET", "/api/users", false);

📜 Основные свойства XMLHttpRequest

В дополнение к методам объект XMLHttpRequest предоставляет ряд свойств, которые позволяют настроить отправку запроса или извлечт полученные от сервера данные:

Свойство Описание
readyState Состояние запроса (0-4)
status HTTP код ответа (200, 404, 500 и т.д.)

содержит статусный код ответа HTTP, который пришел от сервера. С помощью статусного кода можно судить об успешности запроса или об ошибках, которые могли бы возникнуть при его выполнении. Например, статусный код 200 указывает на то, что запрос прошел успешно. Код 403 говорит о необходимости авторизации для выполнения запроса, а код 404 сообщает, что ресурс не найден и так далее.

statusText Текст статуса ("200 OK", "Not Found")
response Тело ответа (тип зависит от responseType)

Возвращает ответ сервера. Ответ может представлять объекты ArrayBuffer, Blob, Document, объект JSON, строку или null (если запрос еще не завершен или завершился неудачно)

responseText Возвращает ответа сервера в виде строки или значения null (если запрос еще не завершен или завершился неудачно)
responseXML Возвращает объект Document (документ HTML/XML), если ответ от сервера в формате XML/HTML
responseType Тип ответа ("", "text", "json", "blob", "document")
timeout Устанавливает тайм-аут - время в миллисекундах, во время которого может выполняться запрос. Если это время истекло, а запрос еще не завершен, то запрос прерывается

Состояния readyState

Значение Состояние Описание
0 UNSENT Объект создан, open() не вызван
1 OPENED open() вызван
2 HEADERS_RECEIVED Получены заголовки ответа
3 LOADING Загружается тело ответа
4 DONE Запрос завершён

Типы ответа responseType

Тип ответа Описание
"" Пустая строка
"arraybuffer" ответ представляет объект ArrayBuffer, который содержит бинарные данные
"blob" ответ представляет объект Blob, который содержит бинарные данные
"document" ответ представляет объект Document (документ HTML/XML)
"json" ответ представляет данные в формате json
"text" ответ представляет текст

📜 События и обработчики событий XMLHttpRequest

Для отслеживания состояния запроса можно применять события XMLHttpRequest:

Событие Когда происходит Свойство для установки
loadstart После запуска запроса. onloadstart
progress При выполнении запроса. onprogress
load Успешное завершение запроса onload
loadend Завершение запроса (успех или ошибка) onloadend
error Ошибка сети onerror
abort Запрос прерван onabort
timeout Превышен таймаут ontimeout
readystatechange Изменение readyState onreadystatechange

📜 Процесс выполнения ajax-запроса

В общем случае процесс выполнения ajax-запроса с помощью XMLHttpRequest выглядит следующим образом:

  1. Создается объект XMLHttpRequest
    const request = new XMLHttpRequest();
  2. Устанавливается обработчик событий загрузки (например, через свойство onload), который будет вызываться после завершения HTTP-запроса
    request.onload = (event) => { console.log("request finished");}
  3. Запускается HTTP-запрос с помощью метода open(). Методу передается метод HTTP, который будет использоваться для запроса (например, GET или POST), URL-адрес, к которому должен быть отправлен запрос, и при необходимости другие необязательные аргументы

    request.open("GET", "http://localhost/hello");
  4. При необходимости производиться дополнительная конфигурация HTTP-запроса. Например, с помощью метода setRequestHeader() можно определить заголовки, которые будут отправляться вместе с запросом. Однако важно выполнить эту настройку после предыдущего шага, то есть после вызова метода open(), но перед следующим шагом, то есть перед вызовом метода send()

    request.setRequestHeader("Accept", "text/plain"); // установка заголовка на прием данных
  5. Непосредственно отправляется HTTP-запрос с помощью вызова метода send(). При желании в этот метод можно передать данные для отправки на сервер

    request.send()

📜 Создание простого web-сервера

Поскольку Ajax предполагает взаимодействие клиента и сервера, то для работы с Ajax нам потребуется некоторый сетевой ресурс, к которому мы будем обращаться. Для эмуляции сетевого ресурса используем локальный веб-сервер. В данном случае воспользуемся самым простым вариантом - Node.js.

Итак, создадим на жестком диске папку для файлов веб-сервера. Например, в моем случае это папка C:\app. Далее в этой папке определим файл сервера. Пусть он будет называться server.js и будет иметь следующий код:

const http = require("http"); const fs = require("fs"); http.createServer(function(request, response){ if(request.url == "/hello"){ response.end("Hello METANIT.COM"); } else{ fs.readFile("index.html", (error, data) => response.end(data)); } }).listen(3000, ()=>console.log("Сервер запущен по адресу http://localhost:3000"));

Вкратце пробежимся по коду. Сначала подключаются пакеты с функциональностью, которую мы собираемся использовать:

const http = require("http"); // для обработки входящих запросов const fs = require("fs"); // для чтения с жесткого диска файла index.html

Для создания сервера применяется функция http.createServer(). В эту функцию передается функция-обработчик, которая вызывается каждый раз, когда к серверу приходит запрос. Эта функция имеет два параметра: request (содержит данные запроса) и response (управляет отправкой ответа).

В функции-обработчике с помощью свойства request.url мы можем узнать, к какому ресурсу на сервере пришел запрос. Так, в данном случае, если пришел запрос по пути "/hello" (условно к ресурсу "/hello"), то оправляем в ответ с помощью метода response.end() текст "XMLHttpRequest на METANIT.COM":

if(request.url == "/hello"){ response.end("Hello METANIT.COM"); }

Если запрос пришел к какому-то другому ресурсу, то отправляем файл index.html, который мы дальше определим:

else{ fs.readFile("index.html", (error, data) => response.end(data)); }

Для считывания файлов применяется встроенная функция fs.readFile(). Первый параметр функции - адрес файла (в данном случае предполагается, что файл index.html находится в одной папке с файлом сервера server.js). Второй параметр - функция, которая вызывается после считывания файла и получет его содержимое через свой второй параметр data. Затем считанное содежимое также может быть отпавлено с помощью функции response.end(data).

В конце с помощью функции listen() запускаем веб-сервер на 3000 порту. То есть сервер будет запускаться по адресу http://localhost:3000/

📜 Выполнение ajax-запроса

Теперь в папке сервера определим простенький файл index.html:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <script> const xhr = new XMLHttpRequest(); // GET-запрос к ресурсу /hello xhr.open("GET", "/hello"); // обработчик получения ответа сервера xhr.onload = () => { if (xhr.status == 200) { // если код ответа 200 console.log(xhr.responseText); // выводим полученный ответ на консоль браузера } else { // иначе выводим текст статуса console.log("Server response: ", xhr.statusText); } }; xhr.send(); // выполняем запрос </script> </body> </html>

Здесь в метод xhr.open() в качестве типа запроса передается тип "GET", а в качестве адреса ресурса - "/hello".

xhr.open("GET", "/hello");

Для отслеживания завершения запроса устанавливаем обработчик для события load с помощью свойства xhr.onload:

xhr.onload = () => { if (xhr.status == 200) { // если код ответа 200 console.log(xhr.responseText); // выводим полученный ответ на консоль браузера } else { // иначе выводим текст статуса console.log("Server response: ", xhr.statusText); } };

В данном случае в качестве обработчика события выступает лямбда-выражение. И когда запрос завершится, сработает данный обработчик. Если запрос был успешно обрабатан, то по умолчанию сервер посылает статусный код 200. Как мы помним из кода сервера, при обращении по адресу "/hello" сервер посылает клиенту строку. И чтобы получить данную строку, обращаемся к свойству xhr.responseText. Если же в процессе обращения к серверу возникла какая-то ошибка или статусный код не 200, то с помощью свойства xhr.statusText выводит текст статуса ответа.

И в конце собственно выполняем запрос:

xhr.send(); // выполняем запрос

Таким образом, при загрузке данной веб-страницы будет выполняться ajax-запрос к серверу.

Теперь в консоли перейдем к папке сервера с помощью команды cd и запустим сервер с помощью команды node server.js

C:\app>node server.js
Сервер запущен по адресу http://localhost:3000

После запуска сервера мы можем перейти в браузере по адресу http://localhost:3000, нам отобразится страница, в javascript-коде которой произойдет обращение к ресурсу "/hello":

В итоге при обращении к ресурсу "/hello" сервер отправит отправит строку "XMLHttpRequest на METANIT.COM", которую мы сможем получить на веб-странице.

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

xhr.open("GET", "http://localhost:3000/hello");

Вместо события load мы также могли бы обрабатывать событие readystatechange объекта XMLHttpRequest, которое возникает каждый раз, когда изменяется значение свойства readyState:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <script> const xhr = new XMLHttpRequest(); // GET-запрос к ресурсу /hello xhr.open("GET", "/hello"); // обработчик получения ответа сервера xhr.onreadystatechange = () => { if (xhr.readyState == 4) { // если запрос завершен if (xhr.status == 200) { // если код ответа 200 console.log(xhr.responseText); // выводим полученный ответ на консоль браузера } else { // иначе выводим текст статуса console.log("Server response: ", xhr.statusText); } } }; xhr.send(); // выполняем запрос </script> </body> </html>

Здесь в обработчике события сначала проверяет состояние запроса - если код состояния равен 4, то обрабатывает ответ от сервера. Остальная логика та же, что и предыдущем случае.

📜 Простой GET-запрос

Вариант 1: С событием load

const xhr = new XMLHttpRequest(); // Настраиваем запрос xhr.open("GET", "/api/users"); // Обработчик успешного завершения xhr.onload = () => { if (xhr.status === 200) { console.log("Ответ:", xhr.responseText); } else { console.log("Ошибка:", xhr.status, xhr.statusText); } }; // Обработчик ошибки сети xhr.onerror = () => { console.log("Ошибка сети"); }; // Отправляем запрос xhr.send();

Вариант 2: С событием readystatechange

const xhr = new XMLHttpRequest(); xhr.open("GET", "/api/users"); // Отслеживаем изменение состояния xhr.onreadystatechange = () => { if (xhr.readyState === 4) { // DONE if (xhr.status === 200) { console.log("Успех:", xhr.responseText); } else { console.log("Ошибка:", xhr.status); } } }; xhr.send();
✅ Рекомендация

Используйте событие load вместо readystatechange — это современный подход, код получается чище и проще.

📜 POST-запрос с отправкой данных

Отправка текста

const xhr = new XMLHttpRequest(); xhr.open("POST", "/api/users"); xhr.onload = () => { if (xhr.status === 200) { console.log("Ответ:", xhr.responseText); } }; // Отправляем простой текст xhr.send("Tom");

Отправка JSON

const user = { name: "Tom", age: 37, email: "tom@example.com" }; const xhr = new XMLHttpRequest(); xhr.open("POST", "/api/users"); // Указываем, что отправляем JSON xhr.setRequestHeader("Content-Type", "application/json"); xhr.onload = () => { if (xhr.status === 200) { const response = JSON.parse(xhr.responseText); console.log("Создан пользователь:", response); } }; // Преобразуем объект в JSON и отправляем xhr.send(JSON.stringify(user));

Отправка FormData

// Создаём объект FormData const formData = new FormData(); formData.append("name", "Tom"); formData.append("age", 39); formData.append("photo", fileInput.files[0]); // Файл const xhr = new XMLHttpRequest(); xhr.open("POST", "/api/users"); xhr.onload = () => { if (xhr.status === 200) { console.log("Данные отправлены"); } }; // FormData автоматически устанавливает правильный Content-Type xhr.send(formData);

📜 Загрузка HTML с помощью XMLHttpRequest

Нередко в коде страницы требуется получить с сервера некоторый код HTML. Например, страница может представлять одностраничный сайт, который через ajax запрос загружает необходимый html-код и вставляет на страницу. Поэтому рассмотрим, как через AJAX загрузить код html.

В качестве сервера, как и в прошлой статье, будем использовать Node.js как наиболее простой вариант.

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

  • index.html: главная страница приложения
  • home.html: страница с кодом html, который мы будем загружать через AJAX
  • server.js: файл приложения сервера, который будет использовать Node.js

Определение сервера

Файл server.js будет представлять код сервера Node.js. Определим в нем следующий код:

const http = require("http"); const fs = require("fs"); http.createServer((request, response)=>{ // получаем путь после слеша, слеш - первый символ в пути let filePath = request.url.substring(1); // если пустой путь, отправляем главную страницу index.html if(!filePath) filePath = "index.html"; // в качестве типа ответа устанавливаем html response.setHeader("Content-Type", "text/html; charset=utf-8;"); fs.readFile(filePath, (error, data)=>{ if(error){ // если ошибка response.statusCode = 404; response.end("<h1>Resourse not found!</h1>"); } else{ response.end(data); } }); }).listen(3000, ()=>console.log("Сервер запущен по адресу http://localhost:3000"));

Вкратце пробежимся по коду. Сначала подключаются пакеты с функциональностью, которую мы собираемся использовать:

const http = require("http"); // для обработки входящих запросов const fs = require("fs"); // для чтения файлов с жесткого диска

Для создания сервера применяется функция http.createServer(). В эту функцию передается функция-обработчик, которая вызывается каждый раз, когда к серверу приходит запрос. Эта функция имеет два параметра: request (содержит данные запроса) и response (управляет отправкой ответа).

В функции-обработчике с помощью свойства request.url мы можем получить путь к ресурсу, к которому пришел запрос. Нам надо обрабатывать запросы к страницам "index.html" и "home.html" (а в перспективе к любым другим страницам html). Путь всегда начинается со слеша "/". Например, запрос к странице "home.html" будет представлять путь "/home.html". Соответственно, чтобы получить из запрошенного пути путь к файлам на жестком диске, нам надо убрать начальный слеш:

let filePath = request.url.substring(1);

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

if(!filePath) filePath = "index.html";

И, поскольку, в нашем случае ответ сервера будет представлять код html, то с помощью метода setHeader() устанавливаем для заголовка "Content-Type" значение "text/html":

response.setHeader("Content-Type", "text/html; charset=utf-8;");

То есть ответ сервера будет представлять html.

Далее с помощью функции fs.readFile считываем файл, к которому идет запрос. Первый параметр функции - адрес файла (в данном случае предполагается, что файл находится в одной папке с файлом сервера server.js). Второй параметр - функция, которая вызывается после считывания файла и получет его содержимое через свой второй параметр data. Вполне возможно, что запрошенного файла не окажется, и в этом случае отправляем ошибку 404:

fs.readFile(filePath, (error, data)=>{ if(error){ // если ошибка response.statusCode = 404; response.end("<h1>Resourse not found!</h1>"); }

Если ошибки нет, файл найден и успещно считан, то отправляем параметр data, который содержит данные файла:

else{ response.end(data); }

В конце с помощью функции listen() запускаем веб-сервер на 3000 порту. То есть сервер будет запускаться по адресу http://localhost:3000/

Определение кода html для загрузки

Файл home.html будет содержать простенький код, который будет загружаться веб-страницей. Пусть это будет следующий код:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Home Page</title> </head> <body> <h1>Home Page</h1> <p>Home Page Text</p> </body> </html>

Определение главной страницы и загрузка данных

Теперь определим код главной страницы index.html, которая будет загружать страницу home.html:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <script> const xhr = new XMLHttpRequest(); xhr.onload = () => { // обработчик получения ответа сервера if (xhr.status == 200) { // если код ответа 200 const html = xhr.responseText; // получаем ответ console.log(html); // выводим полученный ответ на консоль браузера } else { // иначе выводим текст статуса console.log("Server response: ", xhr.statusText); } }; xhr.open("GET", "/home.html"); // GET-запрос к ресурсу /home.html xhr.setRequestHeader("Accept", "text/html"); // принимаем только html xhr.send(); // выполняем запрос </script> </body> </html>

В обработчике загрузке xhr.onload получаем текст ответа через xhr.responseText и выводим ответ на консоль.

Теперь в консоли перейдем к папке сервера с помощью команды cd и запустим сервер с помощью команды node server.js

C:\app>node server.js
Сервер запущен по адресу http://localhost:3000

После запуска сервера мы можем перейти в браузере по адресу http://localhost:3000, нам отобразится страница, в javascript-коде которой произойдет обращение к странице "home.html". Код javascript получит эту страницу и выведет ее содержимое на консоль:

Управление html-содержимым

В примере выше мы получали содержимое страницы как обычный текст. Однако так как этот текст фактически содержит разметку HTML, то мы можем загрузить его на веб-страницу. Так, изменим код страницы index.html следующим образом:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <div id="content"></div> <script> const contentDiv = document.getElementById("content"); // элемент для загрузки html const xhr = new XMLHttpRequest(); xhr.onload = () => { if (xhr.status == 200) { contentDiv.innerHTML = xhr.responseText; // выводим полученный ответ в contentDiv } else { // иначе выводим текст статуса console.log("Server response: ", xhr.statusText); } }; xhr.open("GET", "/home.html"); // GET-запрос к ресурсу /home.html xhr.setRequestHeader("Accept", "text/html"); // принимаем только html xhr.send(); // выполняем запрос </script> </body> </html>

В данном случае загружаем полученный код страницы "home.html" в элемент c id=content

Однако проблема в данном случае состоит в том, что код страницы "home.html" кроме собственно некоторого содержимого также содержит элементы head, title, метаописания страницы с помощью тегов <meta>. Эти элементы нет смысла загружать на другую веб-страницу. Либо мы хотим загрузить какой-то определенный элемент со страницы "home.html", а не весь ее код. В этом случае мы можем получить ответ через свойство responseXML и затем манипулировать ответом как стандартным документом html. Например, изменим код javascript следующим образом:

const contentDiv = document.getElementById("content"); const xhr = new XMLHttpRequest(); xhr.onload = () => { // обработчик получения ответа сервера if (xhr.status == 200) { // загружаем только содержимое элемента body contentDiv.innerHTML = xhr.responseXML.body.innerHTML; } else { console.log("Server response: ", xhr.statusText); } }; xhr.open("GET", "/home.html"); // GET-запрос к ресурсу /home.html xhr.responseType = "document"; // устанавливаем тип ответа xhr.setRequestHeader("Accept", "text/html"); // принимаем только html xhr.send(); // выполняем запрос

Здесь следует отметить два момента. Прежде всего устанавливаем для ответа тип "document":

xhr.responseType = "document";

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

Чтобы получить ответ в виде html/xml используем свойство responseXML. И далее, поскольку это свойство представляет объект Document, используем свойство body для обращения к непосредственному содержимому страницы:

contentDiv.innerHTML = xhr.responseXML.body.innerHTML;

В результате в contentDiv будет загружено содержимое элемента body страницы "home.html".

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

document.title = xhr.responseXML.title;

Или загрузим на страницу только текст из заголовка <ht1>:

contentDiv.innerHTML = xhr.responseXML.querySelector("h1").textContent;

Динамическая загрузка компонентов

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

  • index.html: главная страница приложения
  • home.html: файл компонента home
  • server.js: файл приложения сервера на Node.js
  • about.html: файл компонента about
  • contact.html: файл компонента contact

Файл приложения сервера на Node.js - server.js остается тем же, что был определен выше в данной статье.

Пусть файл home.html содержит какой-нибудь простейший код типа следующего:

<h1>Home Page</h1> <p>Home Page Text</p>

Файл about.html пусть выглядит аналогичным образом:

<h1>About Page</h1> <p>About Page Text</p>

И код файла contact.html:

<h1>Contact Page</h1> <p>Contact Page Text</p>

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

На главной странице index.html определим следующий код:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <nav><a href="home">Home</a> | <a href="about">About</a> | <a href="contact">Contact</a></nav> <div id="content"></div> <script> const contentDiv = document.getElementById("content"); function loadContent(fileName){ const xhr = new XMLHttpRequest(); xhr.onload = () => { if (xhr.status == 200) { contentDiv.innerHTML = xhr.responseText; // xhr.responseXML.body.innerHTML; document.title = fileName; } }; xhr.open("GET", fileName + ".html"); // GET-запрос по адресу ссылки xhr.setRequestHeader("Accept", "text/html"); // принимаем только html xhr.send(); // выполняем запрос } // устанавливаем обработчик нажатия для кнопок const links = document.getElementsByTagName("a"); for (let i = 0; i < links.length; i++) { links[i].addEventListener("click", (e)=>{ loadContent(links[i].getAttribute("href")); e.preventDefault(); }); } // по умолчанию загружаем компонент home loadContent("home"); </script> </body> </html>

Здесь для навигации по компонентам на страницу помещаем ряд ссылок:

<nav><a href="home">Home</a> | <a href="about">About</a> | <a href="contact">Contact</a></nav>

Адрес каждой такой ссылки совпадает с названием страницы соответствующего компонента без расширения ".html".

Каждый из компонентов будет загружаться на странице в элемент с id="content", который получаем в коде JavaScript в константу contentDiv:

const contentDiv = document.getElementById("content");

Также в коде JavaScript для каждой ссылки устанавливаем обработчки, в котором вызываем функцию loadContent и в которую передаем значение атрибута href ссылки - то есть адрес компонента

const links = document.getElementsByTagName("a"); for (let i = 0; i < links.length; i++) { links[i].addEventListener("click", (e)=>{ loadContent(links[i].getAttribute("href")); e.preventDefault(); }); }

В функции loadContent используем адрес ссылки для отправки ajax-запроса, а ответ (полученный html) загружаем в элемент contentDiv

contentDiv.innerHTML = xhr.responseText; // xhr.responseXML.body.innerHTML;

При загрузке страницы сразу загружаем код компонента home, как компонента по умолчанию:

loadContent("home");

Таким образом, на главной странице мы сможем обращаться к конкретным компонентам, переходя по ссылкам:

Еще пример:

const xhr = new XMLHttpRequest(); xhr.open("GET", "/pages/about.html"); // Указываем, что ожидаем HTML xhr.setRequestHeader("Accept", "text/html"); xhr.onload = () => { if (xhr.status === 200) { // Вставляем полученный HTML на страницу document.getElementById("content").innerHTML = xhr.responseText; } }; xhr.send();

📜 Загрузка XML с помощью XMLHttpRequest

Формат XML представляет популярный формат хранения и передачи данных. Рассмотрим, как загружать xml-документ на веб-странице с помощью ajax-запроса.

В качестве сервера, как и в прошлой статье, будем использовать Node.js как наиболее простой вариант.

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

  • index.html: главная страница приложения
  • users.xml: xml-файл с данными
  • app.js: файл приложения сервера, который будет использовать Node.js

Определение данных

Файл users.xml будет представлять загружаемые данные и пусть будет иметь следующее содержимое:

<?xml version="1.0" encoding="UTF-8" ?> <users> <user name="Tom" age="39"> <contacts> <email>tom@smail.com</email> <phone>+1234567890</phone> </contacts> </user> <user name="Bob" age="43"> <contacts> <email>bob@tmail.com</email> <phone>+1334567181</phone> </contacts> </user> <user name="Sam" age="28"> <contacts> <email>sam@xmail.com</email> <phone>+1434567782</phone> </contacts> </user> </users>

Здесь элемент users представляет набор пользователей, каждый из которых представлен элементом user. Для каждого такого элемента определены два атрибута: name (имя пользователя) и age (возраст пользователя). И также элемент user имеет вложенный элемент contacts, который представляет контактные данные пользователя в виде вложенных элементов phone и email.

Определение сервера

Файл app.js будет представлять код сервера Node.js. Определим в нем следующий код:

nst http = require("http"); const fs = require("fs"); http.createServer((request, response)=>{ // если запрошены данные xml if(request.url == "/data"){ fs.readFile("users.xml", (_, data) => response.end(data)); } else{ fs.readFile("index.html", (_, data) => response.end(data)); } }).listen(3000, ()=>console.log("Сервер запущен по адресу http://localhost:3000"));

Загрузка XML на веб-странице

Для получения файла "users.xml" с сервера определим в файле index.html следующий код:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <script> const xhr = new XMLHttpRequest(); xhr.onload = () => { if (xhr.status == 200) { const xml = xhr.responseXML; console.log(xml); } }; xhr.open("GET", "/data"); // GET-запрос к /data xhr.responseType = "document"; // устанавливаем тип ответа xhr.setRequestHeader("Accept", "text/xml"); // принимаем только xml xhr.send(); // выполняем запрос </script> </body> </html>

Для получения данных отправляем запрос по адресу "/data". Чтобы полученные данные автоматически были распарсены в документ XML, свойству responseType присваиваем значение "document".

xhr.responseType = "document";

Кроме того, следует установить для заголовка Accept значение "text/xml" или "application/xml", чтобы принимать данные только в формате XML:

xhr.setRequestHeader("Accept", "text/xml");

В обработчике события onload документ XML доступен через свойство responseXML в виде объекта типа Document, который в данном случае просто выводится на консоль:

xhr.onload = () => { if (xhr.status == 200) { const xml = xhr.responseXML; console.log(xml); } };

После определения всех файлов в консоли перейдем к папке сервера с помощью команды cd и запустим сервер с помощью команды node server.js

C:\app>node server.js
Сервер запущен по адресу http://localhost:3000

После запуска сервера мы можем перейти в браузере по адресу http://localhost:3000, нам отобразится страница, в javascript-коде произойдет обращение по адресу "/data". Сервер в ответ отправит содержимое файла users.xml, и консоль барузера отобразит это содержимое:

Вывод данных их xml-документа на страницу

Теперь усложим задачу - допустим, нам надо вывести данные о пользователях из xml в таблицу на веб-страницу. Для этого изменим код index.html следующим образом:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <div id="content"></div> <script> const contentDiv = document.getElementById("content"); const xhr = new XMLHttpRequest(); xhr.onload = () => { if (xhr.status == 200) { const xmlDoc = xhr.responseXML; const table = createTable(); // выбираем все элементы user const users = xmlDoc.getElementsByTagName("user"); for (let i = 0; i < users.length; i++) { const user = users[i]; const userName = user.getAttribute("name"); const userAge = user.getAttribute("age"); const contact = user.querySelector("contacts email").textContent; const row = createRow(userName, userAge, contact); table.appendChild(row); } contentDiv.appendChild(table); } }; xhr.open("GET", "/data"); xhr.responseType = "document"; xhr.setRequestHeader("Accept", "text/xml"); xhr.send(); // создаем таблицу function createTable() { const table = document.createElement("table"); const headerRow = document.createElement("tr"); const nameColumnHeader = document.createElement("th"); const ageColumnHeader = document.createElement("th"); const contactColumnHeader = document.createElement("th"); nameColumnHeader.appendChild(document.createTextNode("Name")); ageColumnHeader.appendChild(document.createTextNode("Age")); contactColumnHeader.appendChild(document.createTextNode("Contacts")); headerRow.appendChild(nameColumnHeader); headerRow.appendChild(ageColumnHeader); headerRow.appendChild(contactColumnHeader); table.appendChild(headerRow); return table; } // создаем одну строку для таблицы function createRow(userName, userAge, userContact) { const row = document.createElement("tr"); const nameColumn = document.createElement("td"); const ageColumn = document.createElement("td"); const contactColumn = document.createElement("td"); nameColumn.appendChild(document.createTextNode(userName)); ageColumn.appendChild(document.createTextNode(userAge)); contactColumn.appendChild(document.createTextNode(userContact)); row.appendChild(nameColumn); row.appendChild(ageColumn); row.appendChild(contactColumn); return row; } </script> </body> </html>

В данном случае загружаем таблицу на страницу в элемент с идентификатором "content", который получаем в коде JavaScript в элемент contentDiv

const contentDiv = document.getElementById("content");

Для создания таблицы определены две вспомогательные функции. Функция createTable создает элемент table с одной строкой - заголовками столбцов. Функция createRow принимает через параметры имя, возраст и контакты пользователя и для них создает строку.

В основной части кода выполняем запрос на сервер. Поскольку ответ сервера будет представлять документ xml в виде типа Document, то с помощью стандартных методов типа getElementsByTagName или querySelector мы можем найти нужные в документе элементы. И в начале выбираем все элементы user:

const xmlDoc = xhr.responseXML; const table = createTable(); // выбираем все элементы user const users = xmlDoc.getElementsByTagName("user");

Далее перебираем все элементы user и выбираем у каждого атрибуты name и age, а также вложенный элемент email:

for (let i = 0; i < users.length; i++) { const user = users[i]; const userName = user.getAttribute("name"); const userAge = user.getAttribute("age"); const contact = user.querySelector("contacts email").textContent; const row = createRow(userName, userAge, contact); table.appendChild(row); }

Для каждого элемента user создается строка, которая затем добавляется в таблицу.

Таким образом, при обращении к странице index.html будет загружен xml-документ, по которому будет создана таблица:

Загрузка XML

const xhr = new XMLHttpRequest(); xhr.open("GET", "/data/users.xml"); // Указываем, что ожидаем XML xhr.setRequestHeader("Accept", "text/xml"); xhr.onload = () => { if (xhr.status === 200) { // Получаем XML-документ const xmlDoc = xhr.responseXML; // Работаем с XML как с DOM const users = xmlDoc.querySelectorAll("user"); users.forEach(user => { const name = user.getAttribute("name"); const age = user.getAttribute("age"); console.log(name, age); }); } }; xhr.send();

📜 Загрузка JSON с помощью XMLHttpRequest

JSON представляет один из наиболее популярных форматов хранения и передачи данных. Особенно часто JSON используется при передачи данных через ajax-запросы. Для получения json с помощью XMLHttpRequest следует выполнить две настройки:

  • Чтобы ответ сервера рассматривался как объект json, свойству responseType передается значение "json". Браузеры, которые поддерживают это значение, гарантируют, что ответ может быть прочитан непосредственно как объект JavaScript.
    xhr.responseType = "json";
  • Также при отправке запроса для заголовка Accept следует задать значение "application/json":
    xhr.setRequestHeader("Accept", "application/json");

В качестве сервера, как и в прошлой статье, будем использовать Node.js как наиболее простой вариант.

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

  • index.html: главная страница приложения
  • data.json: файл с данными в формате json
  • app.js: файл приложения сервера, который будет использовать Node.js

Определение данных

Файл data.json будет представлять загружаемые данные и пусть будет иметь следующее содержимое:

{ "users": [ { "name": "Tom", "age": 39, "contacts": { "email": "tom@smail.com", "phone": "+1234567890" } }, { "name": "Bob", "age": 43, "contacts": { "email": "bob@tmail.com", "phone": "+1334567181" } }, { "name": "Sam", "age": 28, "contacts": { "email": "sam@xmail.com", "phone": "+1434567782" } } ] }

Здесь элемент users представляет набор пользователей, каждый из которых представлен элементом user. Для каждого такого элемента определены два атрибута: name (имя пользователя) и age (возраст пользователя). И также элемент user имеет вложенный элемент contacts, который представляет контактные данные пользователя в виде вложенных элементов phone и email.

Определение сервера

Файл app.js будет представлять код сервера Node.js. Определим в нем следующий код:

const http = require("http"); const fs = require("fs"); http.createServer((request, response)=>{ // если запрошены данные xml if(request.url == "/data"){ fs.readFile("data.json", (_, data) => response.end(data)); } else{ fs.readFile("index.html", (_, data) => response.end(data)); } }).listen(3000, ()=>console.log("Сервер запущен по адресу http://localhost:3000"));

Вкратце пробежимся по коду. Сначала подключаются пакеты с функциональностью, которую мы собираемся использовать:

const http = require("http"); // для обработки входящих запросов const fs = require("fs"); // для чтения файлов с жесткого диска

Для создания сервера применяется функция http.createServer(). В эту функцию передается функция-обработчик, которая вызывается каждый раз, когда к серверу приходит запрос. Эта функция имеет два параметра: request (содержит данные запроса) и response (управляет отправкой ответа).

В функции-обработчике с помощью свойства request.url мы можем получить путь к ресурсу, к которому пришел запрос. Так, в данном случае, если пришел запрос по пути "/data", то оправляем data.json:

if(request.url == "/data"){ fs.readFile("data.json", (_, data) => response.end(data)); }

Для считывания файла применяется функция fs.readFile. Первый параметр функции - адрес файла (в данном случае предполагается, что файл находится в одной папке с файлом сервера server.js). Второй параметр - функция, которая вызывается после считывания файла и получет его содержимое через свой второй параметр data.

Для всех остальных запросов отправляем в ответ файл index.html:

else{ fs.readFile("index.html", (_, data) => response.end(data)); }

В конце с помощью функции listen() запускаем веб-сервер на 3000 порту. То есть сервер будет запускаться по адресу http://localhost:3000/

Загрузка JSON на веб-странице

Для получения файла "data.json" с сервера определим в файле index.html следующий код:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <script> const xhr = new XMLHttpRequest(); xhr.onload = () => { if (xhr.status == 200) { const json = xhr.response; console.log(json); } }; xhr.open("GET", "/data"); // GET-запрос к /data xhr.responseType = "json"; // устанавливаем тип ответа xhr.setRequestHeader("Accept", "application/json"); // принимаем только json xhr.send(); // выполняем запрос </script> </body> </html>

Для получения данных отправляем запрос по адресу "/data". Чтобы полученные данные автоматически были распарсены в документ JSON, свойству responseType присваиваем значение "json".

xhr.responseType = "json";

Кроме того, следует установить для заголовка Accept значение "application/json":

xhr.setRequestHeader("Accept", "application/json");

В обработчике события onload объект JSON доступен через свойство response, который в данном случае просто выводится на консоль:

xhr.onload = () => { if (xhr.status == 200) { const json = xhr.response; console.log(json); } };

После определения всех файлов в консоли перейдем к папке сервера с помощью команды cd и запустим сервер с помощью команды node server.js

C:\app>node server.js
Сервер запущен по адресу http://localhost:3000

После запуска сервера мы можем перейти в браузере по адресу http://localhost:3000, нам отобразится страница, в javascript-коде произойдет обращение по адресу "/data". Сервер в ответ отправит содержимое файла data.json, и консоль барузера отобразит это содержимое.

Вывод данных JSON на страницу

В полученном в примере выше объекте JSON мы можем обращаться к свойствам, извлекая из объекта нужные нам данные. Например, пусть нам надо вывести данные о пользователях из xml в таблицу на веб-страницу. Для этого изменим код index.html следующим образом:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <div id="content"></div> <script> const contentDiv = document.getElementById("content"); const xhr = new XMLHttpRequest(); xhr.onload = () => { if (xhr.status == 200) { const json = xhr.response; const table = createTable(); // выбираем все элементы user const users = json.users; for (let i = 0; i < users.length; i++) { const user = users[i]; const row = createRow(user.name, user.age, user.contacts.email); table.appendChild(row); } contentDiv.appendChild(table); } }; xhr.open("GET", "/data"); xhr.responseType = "document"; xhr.setRequestHeader("Accept", "text/xml"); xhr.send(); // создаем таблицу function createTable() { const table = document.createElement("table"); const headerRow = document.createElement("tr"); const nameColumnHeader = document.createElement("th"); const ageColumnHeader = document.createElement("th"); const contactColumnHeader = document.createElement("th"); nameColumnHeader.appendChild(document.createTextNode("Name")); ageColumnHeader.appendChild(document.createTextNode("Age")); contactColumnHeader.appendChild(document.createTextNode("Contacts")); headerRow.appendChild(nameColumnHeader); headerRow.appendChild(ageColumnHeader); headerRow.appendChild(contactColumnHeader); table.appendChild(headerRow); return table; } // создаем одну строку для таблицы function createRow(userName, userAge, userContact) { const row = document.createElement("tr"); const nameColumn = document.createElement("td"); const ageColumn = document.createElement("td"); const contactColumn = document.createElement("td"); nameColumn.appendChild(document.createTextNode(userName)); ageColumn.appendChild(document.createTextNode(userAge)); contactColumn.appendChild(document.createTextNode(userContact)); row.appendChild(nameColumn); row.appendChild(ageColumn); row.appendChild(contactColumn); return row; } </script> </body> </html>

В данном случае загружаем таблицу на страницу в элемент с идентификатором "content", который получаем в коде JavaScript в элемент contentDiv

const contentDiv = document.getElementById("content");

Для создания таблицы определены две вспомогательные функции. Функция createTable создает элемент table с одной строкой - заголовками столбцов. Функция createRow принимает через параметры имя, возраст и контакты пользователя и для них создает строку.

В основной части кода выполняем запрос на сервер. Получив данные JSON, выбираем массив объектов user:

const json = xhr.response; const table = createTable(); // выбираем все объекты user const users = json.users;

Далее перебираем все объекты user, выбираем у каждого объекта свойства name, age и contacts.email и создаем по ним строку в таблице:

for (let i = 0; i < users.length; i++) { const user = users[i]; const row = createRow(user.name, user.age, user.contacts.email); table.appendChild(row); }

Таким образом, при обращении к странице index.html будут загружены данные в формате JSON, и по ним будет создана таблица:

Другой пример:

const xhr = new XMLHttpRequest(); xhr.open("GET", "/api/users"); // Автоматически парсим JSON xhr.responseType = "json"; xhr.onload = () => { if (xhr.status === 200) { // xhr.response уже объект, не нужно JSON.parse() const users = xhr.response; users.forEach(user => { console.log(user.name, user.age); }); } }; xhr.send();

Альтернатива с ручным парсингом

const xhr = new XMLHttpRequest(); xhr.open("GET", "/api/users"); xhr.onload = () => { if (xhr.status === 200) { try { const users = JSON.parse(xhr.responseText); console.log(users); } catch (error) { console.error("Ошибка парсинга JSON:", error); } } }; xhr.send();

📜 Отправка данных в ajax-запросе

Чтобы отправить данные на сервер из кода JavaScript в ajax-запросе, в метод send() объекта XMLHttpRequest передаются отправляемые данные.

Для тестирования отправки, как и в прошлых статьях, в качестве сервера будем использовать Node.js, как самой простой вариант. Итак, создадим файл app.js и определим в нем следующий код сервера, который принимает данные:

const http = require("http"); const fs = require("fs"); http.createServer(async (request, response) => { if(request.url == "/user"){ let userName= ""; // получаем данные в строку // получаем данные из запроса и добавляем их в строку for await (const chunk of request) { userName += chunk; } userName = userName + " Smith"; response.end(userName); } else{ fs.readFile("index.html", (_, data) => response.end(data)); } }).listen(3000, ()=>console.log("Сервер запущен по адресу http://localhost:3000"));

В данном случае при обращении по адресу "/user" сервер получает все отправленные данные:

if(request.url == "/user"){

Мы можем перебрать объект запроса и таким образом извлечеть из него полученные данные:

let userName= ""; // получаем данные в строку // получаем данные из запроса и добавляем их в строку for await (const chunk of request) { userName += chunk; }

В данном случае отправленные данные в виде объектов chunk добавляются в строку userName. В данном случае мы предполагаем, что на сервер отправляется простая строка с текстом, соответственно каждый кусочек данных chunk будет представлять строку.

Также здесь предположим, что клиент отправляет на сервер некоторое имя пользователя. И для примера к этому имени добавляется фамилия и измененное имя отправляется обратно клиенту:

userName = userName + " Smith"; response.end(userName);

Теперь определим на странице index.html код для отправки данных на этот сервер:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <script> // данные для отправки const user = "Tom"; const xhr = new XMLHttpRequest(); // обработчик получения ответа сервера xhr.onload = () => { if (xhr.status == 200) { console.log(xhr.responseText); } else { console.log("Server response: ", xhr.statusText); } }; // POST-запрос к ресурсу /user xhr.open("POST", "/user"); xhr.send(user); // отправляем значение user в методе send </script> </body> </html>

Для отправки применяется метод POST. А в качестве отправляемых данных выступает простая строка "Tom". То есть на сервер отправляется простой текст. И поскольку сервер в ответ также отправляет текст, то для получения ответа здесь применяется свойство xhr.responseText. И при запуске данной веб-страницы будет выполняться отправка данных на сервер, и в консоли браузера можно будет увидеть полученный от сервера ответ:

Отправка json

Подобным образом можно отправлять более сложные по структуре данные. Например, рассмотрим отправку json. Для этого на строне node.js определим следующий сервер:

const http = require("http"); const fs = require("fs"); http.createServer(async (request, response) => { if(request.url == "/user"){ // получаем строковое представление ответа let data=""; for await (const chunk of request) { data += chunk; } // мы ожидаем данные типа {"name":"value","age":1234} const user = JSON.parse(data); // парсим строку в json // для теста изменяем данные полученного объекта user.name = user.name + " Smith"; user.age += 1; // отправляем измененый объект обратно клиенту response.end(JSON.stringify(user)); } else{ fs.readFile("index.html", (_, data) => response.end(data)); } }).listen(3000, ()=>console.log("Сервер запущен по адресу http://localhost:3000"));

В данном случае на сервере ожидаем, что мы получим объект в формате json, который имеет два свойства - name и age. Для примера сервер меняет значения этих свойств и отправляет измененный объект обратно клиенту.

На веб-странице установим объект json для отправки и получим обратно данные:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <script> // данные для отправки const tom = { name: "Tom", age: 37 }; // кодируем объект в формат json const data = JSON.stringify(tom); const xhr = new XMLHttpRequest(); xhr.onload = () => { if (xhr.status == 200) { const user = JSON.parse(xhr.responseText) console.log(user.name, "-", user.age); } else { console.log("Server response: ", xhr.statusText); } }; xhr.open("POST", "/user"); xhr.setRequestHeader("Content-Type", "application/json"); xhr.send(data); </script> </body> </html>

Здесь на сервер отправляется объект tom, который имеет два свойства: name и age. Перед отправкой он кодируется в формат json с помощью функции JSON.stringify().

const data = JSON.stringify(tom);

При отправке с помощью метода setRequestHeader() для заголовка "Content-Type" устанавливаем значение "application/json", тем самым указывая, что мы отправляем данные в формате json:

xhr.setRequestHeader("Content-Type", "application/json");

В обработчике события load сначала парсим текст ответа из формата json в стандартный объект JavaScript:

const user = JSON.parse(xhr.responseText)

Затем выводим данные полученного объекта на консоль браузера:

📜 Отправка формы через AJAX

С помощью объекта FormData можно отправить данные формы из кода JavaScript на сервер через Ajax. Рассмотрим прстейший пример. В качестве сервера, как и в прошлых статьях, будем использовать Node.js.

Сначала создадим файл app.js, который будет представлять сервер. Определим в нем самую простейшую логику:

const http = require("http"); const fs = require("fs"); http.createServer(async (request, response) => { if(request.url == "/user"){ let body = ""; // буфер для получаемых данных // получаем данные из запроса в буфер for await (const chunk of request) { body += chunk; } // для параметра name let userName = ""; // для параметра age let userAge = 0; // регулярное выражения для поиска названия и значения поля формы const exp = /Content-Disposition: form-data; name="([^\"]+)\"\r\n\r\n(\w*)/g; let formElement; while ((formElement = exp.exec(body))){ paramName = formElement[1]; // получаем имя элемента формы paramValue = formElement[2]; // получаем значение элемента формы console.log(paramName, ":", paramValue); // выводим на консоль if(paramName === "name") userName = paramValue; if(paramName === "age") userAge = paramValue; } response.end(`Your name: ${userName} Your Age: ${userAge}`); } else{ fs.readFile("index.html", (_, data) => response.end(data)); } }).listen(3000, ()=>console.log("Сервер запущен по адресу http://localhost:3000"));

Вкратце пробежимся по коду. Сначала подключаются пакеты с функциональностью, которую мы собираемся использовать:

const http = require("http"); // для обработки входящих запросов const fs = require("fs"); // для чтения файлов с жесткого диска

Для создания сервера применяется функция http.createServer(). В эту функцию передается функция-обработчик, которая вызывается каждый раз, когда к серверу приходит запрос. Эта функция имеет два параметра: request (содержит данные запроса) и response (управляет отправкой ответа).

В функции-обработчике с помощью свойства request.url мы можем получить путь к ресурсу, к которому пришел запрос. Здесь мы предполагаем, что клиент будет отправлять форму по адресу "/user". И в начале обрабатывает запрос по этому адресу:

if(request.url == "/user"){

Для получения значений отправленной формы считываем тело запроса в переменную body:

let body = ""; // буфер для получаемых данных // получаем данные из запроса в буфер for await (const chunk of request) { body += chunk; }

Чтобы было представление, что будет содержать body после считывания тела запроса. Допустим, на форме два поля, которые называются name и age. В этом случае тело запроса будет выглядеть примерно следующим образом:

------WebKitFormBoundarya9nLzvDVEN5gPA5Q
Content-Disposition: form-data; name="name"

Tom
------WebKitFormBoundarya9nLzvDVEN5gPA5Q
Content-Disposition: form-data; name="age"

39
------WebKitFormBoundarya9nLzvDVEN5gPA5Q--

Здесь мы видим маркер границы ------WebKitFormBoundarya9nLzvDVEN5gPA5Q, который определяет начало и конец тела запроса, а также отделяет значения отдельных полей формы. (Значение маркера границы может меняться). Для каждого поля формы определяется выражение Content-Disposition: form-data;. Затем с помощью атрибута name указано имя поля формы. Затем через одну строку указано значение соответствующего поля. То есть в примере выше у нас два поля формы: поле "name" со значением "Tom" и поле "age" со значением 39.

Теперь наша задача извлечь названия и значения полей формы. Для этого используем регулярное выражение

const exp = /Content-Disposition: form-data; name="([^\"]+)\"\r\n\r\n(\w*)/g;

Далее проходим по телу запроса регулярным выражением и извлекаем все элементы формы и их значения:

while ((formElement = exp.exec(body))){ paramName = formElement[1]; // получаем имя элемента формы paramValue = formElement[2]; // получаем значение элемента формы if(paramName === "name") userName = paramValue; if(paramName === "age") userAge = paramValue; }

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

После получения данных запроса отправляем в ответ клиенту некоторое сообщение:

response.end(`Your name: ${userName} Your Age: ${userAge}`);

В конце с помощью функции listen() запускаем веб-сервер на 3000 порту. То есть сервер будет запускаться по адресу http://localhost:3000/

Определение клиента

Теперь определим файл index.html, который находится в одной папке с файлом сервера app.js и который будет представлять код клиента:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <script> // данные для отправки const formData = new FormData(); formData.append("name", "Tom"); formData.append("age", 39); const xhr = new XMLHttpRequest(); xhr.onload = () => { if (xhr.status == 200) { console.log(xhr.responseText); } else { console.log("Server response: ", xhr.statusText); } }; xhr.open("POST", "user", true); xhr.send(formData); </script> </body> </html>

Здесь данные формы определяются вручную в виде объекта FormData. После создания объекта FormData с помощью метода add() в него можно добавлять отдельные свойства и их значения. Затем для отправки на сервер объект FormData в качестве аргумента методу send(). В качестве метода HTTP устанавливается метод POST.

В обработчике onload выводим полученное от сервера сообщение на консоль.

В конце перейдем в консоли к папке сервера с помощью команды cd и запустим сервер с помощью команды node server.js

C:\app>node server.js
Сервер запущен по адресу http://localhost:3000

После запуска сервера мы можем перейти в браузере по адресу http://localhost:3000, нам отобразится страница, в javascript-коде которой будет выполняться POST-запрос по адресу "/user". Код javascript получит ответ от сервера и выведет его на консоль.

В примере выше данные формы определяются вручную. Но аналогичным образом можно отправлять и данные форм, которые определены в коде html. Например, изменим код страницы index.html следующим образом:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <form id="myForm" method="post" action="/user"> <p> <label>User Name:</label><br> <input name="name" /> </p> <p> <label>User Age:</label><br> <input name="age" /> </p> <input type="submit" value="Send" /> </form> <script> // данные для отправки const myForm = document.getElementById("myForm"); myForm.addEventListener("submit", (e)=>{ e.preventDefault(); const formData = new FormData(myForm); const xhr = new XMLHttpRequest(); xhr.onload = () => { if (xhr.status == 200) { console.log(xhr.responseText); } }; xhr.open("POST", "user", true); xhr.send(formData); }); </script> </body> </html>

Здесь в коде формы определена форма с двумя полями ввода для отправки на сервер. Причем эти поля также имеют названия name и age. В коде JavaScript перехватываем отправку формы, из формы получаем объект FormData и отправляем его на сервер.

Еще пример:

<form id="myForm" method="post" action="/api/users"> <input name="name" placeholder="Имя" /> <input name="age" type="number" placeholder="Возраст" /> <button type="submit">Отправить</button> </form> <script> const form = document.getElementById("myForm"); form.addEventListener("submit", (e) => { e.preventDefault(); // Отменяем стандартную отправку // Создаём FormData из формы const formData = new FormData(form); const xhr = new XMLHttpRequest(); xhr.open("POST", form.action); xhr.onload = () => { if (xhr.status === 200) { console.log("Форма отправлена:", xhr.responseText); } }; xhr.send(formData); }); </script>

📜 Установка заголовков

const xhr = new XMLHttpRequest(); xhr.open("GET", "/api/users"); // Устанавливаем заголовки (ПОСЛЕ open, ДО send!) xhr.setRequestHeader("Accept", "application/json"); xhr.setRequestHeader("Authorization", "Bearer token123"); xhr.setRequestHeader("X-Custom-Header", "value"); xhr.onload = () => { if (xhr.status === 200) { console.log(xhr.response); } }; xhr.send();

Получение заголовков ответа

xhr.onload = () => { // Все заголовки console.log(xhr.getAllResponseHeaders()); // Конкретный заголовок const contentType = xhr.getResponseHeader("Content-Type"); console.log("Content-Type:", contentType); };

📜 Отслеживание прогресса загрузки

const xhr = new XMLHttpRequest(); xhr.open("GET", "/large-file.zip"); // Прогресс загрузки xhr.onprogress = (event) => { if (event.lengthComputable) { const percentComplete = (event.loaded / event.total) * 100; console.log(`Загружено: ${percentComplete.toFixed(2)}%`); } }; xhr.onload = () => { console.log("Загрузка завершена"); }; xhr.send();

Прогресс отправки данных

const xhr = new XMLHttpRequest(); xhr.open("POST", "/api/upload"); // Прогресс ОТПРАВКИ xhr.upload.onprogress = (event) => { if (event.lengthComputable) { const percentComplete = (event.loaded / event.total) * 100; console.log(`Отправлено: ${percentComplete.toFixed(2)}%`); } }; xhr.upload.onload = () => { console.log("Отправка завершена"); }; const formData = new FormData(); formData.append("file", fileInput.files[0]); xhr.send(formData);

📜 Таймаут и отмена запроса

Установка таймаута

const xhr = new XMLHttpRequest(); xhr.open("GET", "/api/slow-endpoint"); // Таймаут 5 секунд xhr.timeout = 5000; xhr.ontimeout = () => { console.log("Запрос превысил время ожидания"); }; xhr.onload = () => { console.log("Успех:", xhr.responseText); }; xhr.send();

Отмена запроса

const xhr = new XMLHttpRequest(); xhr.open("GET", "/api/users"); xhr.onload = () => { console.log("Запрос завершён"); }; xhr.onabort = () => { console.log("Запрос отменён"); }; xhr.send(); // Отменяем запрос через 1 секунду setTimeout(() => { xhr.abort(); }, 1000);

📜 Promise-обёртка для XMLHttpRequest

Как видно из примеров прошлых тем для создания ajax-запросов используются фактически повторяющиеся вызовы, которые отличаются лишь деталями - строкой запроса, функциями обработки ответа. И вполне было бы не плохо создать для всех действий, связанных с асинхронным ajax-запросом, создать какую-то общую абстракцию и затем использовать ее при следующих обращениях к серверу.

Для создания дополнительного уровня абстракции в данном случае удобно применять объект Promise, который обертывает асинхронную операцию в один объект, который позволяет определить действия, выполняющиеся при успешном или неудачном выполнении этой операции.

Инкапсулируем асинхронный запрос в объект Promise:

function get(url) { return new Promise((succeed, fail) => { const xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.addEventListener("load", () => { if (xhr.status >=200 && xhr.status < 400) succeed(xhr.response); else fail(new Error(`Request failed: ${xhr.statusText}`)); }); xhr.addEventListener("error", () => fail(new Error("Network error"))); xhr.send(); }); }

Метод get получает в качестве параметра адрес ресурса сервера и возвращает объект Promise. Конструктор Promise в качестве параметра принимает функцию обратного вызова, которая в свою очередь принимает два параметра - две функции: одна выполняется при успешной обработке запроса, а вторая - при неудачной.

Допустим, в качестве сервера выступает следующее простенькое приложение на Node.js:

const http = require("http"); const fs = require("fs"); http.createServer(function(request, response){ if(request.url == "/hello"){ response.end("XMLHttpRequest на METANIT.COM"); } else{ fs.readFile("index.html", (_error, data) => response.end(data)); } }).listen(3000, ()=>console.log("Сервер запущен по адресу http://localhost:3000"));

Теперь вызовем метод get для осуществления запроса к серверу:

get("http://localhost:3000/hello") .then(response => console.log(response)) .catch(error => console.error(error));

Для обработки результата объекта Promise вызывается метод then(), который принимает два параметра: функцию, вызываемую при успешном выполнении запроса, и функцию, которая вызывается при неудачном выполнении запроса.

В данном случае функция в первом вызове метода then получает ответ сервера и выводит его на консоль.

Для обработки ошибок мы мы можем использовать метод catch(), в который передается функция обработки ошибок.

Подобным образом через Promise можно было бы отправлять данные на сервер:

function post(url, data) { return new Promise((succeed, fail) => { const xhr = new XMLHttpRequest(); xhr.open("POST", url); xhr.addEventListener("load", () => { if (xhr.status >=200 && xhr.status < 400) succeed(xhr.response); else fail(new Error(`Request failed: ${xhr.statusText}`)); }); xhr.addEventListener("error", () => fail(new Error("Network error"))); xhr.send(data); }); } post("http://localhost:3000/user", "Tom") .then(response => console.log(response)) .catch(error => console.error(error));

В даннном случае по адресу "http://localhost:3000/user" будет отправляться строка "Tom".

Для удобства работы можно обернуть XMLHttpRequest в Promise:

// GET-запрос с Promise function get(url) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 400) { resolve(xhr.response); } else { reject(new Error(`Request failed: ${xhr.statusText}`)); } }; xhr.onerror = () => { reject(new Error("Network error")); }; xhr.send(); }); } // Использование get("/api/users") .then(response => console.log(response)) .catch(error => console.error(error));

POST-запрос с Promise

function post(url, data) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open("POST", url); xhr.setRequestHeader("Content-Type", "application/json"); xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 400) { resolve(xhr.response); } else { reject(new Error(`Request failed: ${xhr.statusText}`)); } }; xhr.onerror = () => reject(new Error("Network error")); xhr.send(JSON.stringify(data)); }); } // Использование post("/api/users", { name: "Tom", age: 37 }) .then(response => console.log("Создан:", response)) .catch(error => console.error(error));

📜 Универсальная функция для AJAX

function ajax(options) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); // Параметры по умолчанию const method = options.method || "GET"; const url = options.url; const data = options.data || null; const headers = options.headers || {}; const timeout = options.timeout || 0; xhr.open(method, url); // Устанавливаем заголовки Object.keys(headers).forEach(key => { xhr.setRequestHeader(key, headers[key]); }); // Таймаут if (timeout) { xhr.timeout = timeout; xhr.ontimeout = () => reject(new Error("Timeout")); } xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 400) { resolve(xhr.response); } else { reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`)); } }; xhr.onerror = () => reject(new Error("Network error")); xhr.send(data); }); } // Использование ajax({ method: "POST", url: "/api/users", data: JSON.stringify({ name: "Tom", age: 37 }), headers: { "Content-Type": "application/json", "Authorization": "Bearer token123" }, timeout: 5000 }) .then(response => console.log(response)) .catch(error => console.error(error));

📜 Обработка различных типов ответов

const xhr = new XMLHttpRequest(); xhr.open("GET", "/api/file"); // Устанавливаем тип ответа xhr.responseType = "blob"; // 'blob', 'arraybuffer', 'document', 'json', 'text' xhr.onload = () => { if (xhr.status === 200) { // Для blob - создаём ссылку на скачивание const blob = xhr.response; const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'file.pdf'; a.click(); URL.revokeObjectURL(url); } }; xhr.send();

Сравнение подходов

Характеристика XMLHttpRequest Fetch API
Синтаксис Сложнее, многословный Проще, на основе Promise
Promise Нужно оборачивать вручную Встроенная поддержка
Прогресс загрузки Есть (onprogress) Сложнее (через ReadableStream)
Отмена запроса xhr.abort() AbortController
Поддержка браузеров Все браузеры (включая старые) Современные браузеры (IE не поддерживает)
Рекомендация Legacy-код, поддержка старых браузеров Современная разработка (предпочтительно)
✅ Когда использовать XMLHttpRequest
  • Поддержка старых браузеров (IE9, IE10)
  • Необходимость отслеживания прогресса загрузки/отправки
  • Работа с legacy-кодом
  • Специфические требования проекта
💡 Переход на Fetch API

Для новых проектов рекомендуется использовать Fetch API (глава 19), который предоставляет более чистый и современный интерфейс для работы с AJAX-запросами.

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

Пример 1: Динамическая подгрузка контента

<nav> <a href="/pages/home.html" class="nav-link">Главная</a> <a href="/pages/about.html" class="nav-link">О нас</a> <a href="/pages/contact.html" class="nav-link">Контакты</a> </nav> <div id="content"></div> <script> const links = document.querySelectorAll('.nav-link'); const contentDiv = document.getElementById('content'); links.forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const url = link.getAttribute('href'); const xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.onload = () => { if (xhr.status === 200) { contentDiv.innerHTML = xhr.responseText; } }; xhr.send(); }); }); </script>

Пример 2: Автодополнение поиска

<input type="text" id="searchInput" placeholder="Поиск..."> <div id="suggestions"></div> <script> const searchInput = document.getElementById('searchInput'); const suggestionsDiv = document.getElementById('suggestions'); let currentXHR = null; searchInput.addEventListener('input', () => { const query = searchInput.value.trim(); if (!query) { suggestionsDiv.innerHTML = ''; return; } // Отменяем предыдущий запрос if (currentXHR) { currentXHR.abort(); } currentXHR = new XMLHttpRequest(); currentXHR.open("GET", `/api/search?q=${encodeURIComponent(query)}`); currentXHR.responseType = "json"; currentXHR.onload = () => { if (currentXHR.status === 200) { const results = currentXHR.response; displaySuggestions(results); } }; currentXHR.send(); }); function displaySuggestions(results) { if (results.length === 0) { suggestionsDiv.innerHTML = '<p>Ничего не найдено</p>'; return; } const html = results.map(item => `<div class="suggestion">${item.name}</div>` ).join(''); suggestionsDiv.innerHTML = html; } </script>

Пример 3: Загрузка файла с прогресс-баром

<input type="file" id="fileInput"> <button id="uploadBtn">Загрузить</button> <div id="progressBar" style="width: 0%; height: 20px; background: blue;"></div> <div id="status"></div> <script> const fileInput = document.getElementById('fileInput'); const uploadBtn = document.getElementById('uploadBtn'); const progressBar = document.getElementById('progressBar'); const status = document.getElementById('status'); uploadBtn.addEventListener('click', () => { const file = fileInput.files[0]; if (!file) { alert('Выберите файл'); return; } const formData = new FormData(); formData.append('file', file); const xhr = new XMLHttpRequest(); xhr.open("POST", "/api/upload"); // Прогресс отправки xhr.upload.onprogress = (e) => { if (e.lengthComputable) { const percent = (e.loaded / e.total) * 100; progressBar.style.width = percent + '%'; status.textContent = `Загружено: ${percent.toFixed(0)}%`; } }; xhr.upload.onload = () => { status.textContent = 'Файл отправлен, ожидание ответа...'; }; xhr.onload = () => { if (xhr.status === 200) { status.textContent = 'Загрузка успешно завершена!'; progressBar.style.background = 'green'; } else { status.textContent = 'Ошибка загрузки'; progressBar.style.background = 'red'; } }; xhr.onerror = () => { status.textContent = 'Ошибка сети'; progressBar.style.background = 'red'; }; xhr.send(formData); }); </script>

Пример 4: Система комментариев

<div id="comments"></div> <form id="commentForm"> <textarea name="text" placeholder="Ваш комментарий"></textarea> <button type="submit">Отправить</button> </form> <script> // Загрузка комментариев function loadComments() { const xhr = new XMLHttpRequest(); xhr.open("GET", "/api/comments"); xhr.responseType = "json"; xhr.onload = () => { if (xhr.status === 200) { displayComments(xhr.response); } }; xhr.send(); } // Отображение комментариев function displayComments(comments) { const commentsDiv = document.getElementById('comments'); const html = comments.map(comment => ` <div class="comment"> <strong>${comment.author}</strong> <p>${comment.text}</p> <small>${new Date(comment.date).toLocaleString()}</small> </div> `).join(''); commentsDiv.innerHTML = html; } // Отправка комментария const form = document.getElementById('commentForm'); form.addEventListener('submit', (e) => { e.preventDefault(); const formData = new FormData(form); const xhr = new XMLHttpRequest(); xhr.open("POST", "/api/comments"); xhr.responseType = "json"; xhr.onload = () => { if (xhr.status === 201) { form.reset(); loadComments(); // Перезагружаем список } }; xhr.send(formData); }); // Загружаем комментарии при загрузке страницы loadComments(); </script>

Пример 5: Бесконечная прокрутка (Infinite Scroll)

<div id="posts"></div> <div id="loading" style="display:none;">Загрузка...</div> <script> let currentPage = 1; let isLoading = false; // Загрузка постов function loadPosts(page) { if (isLoading) return; isLoading = true; document.getElementById('loading').style.display = 'block'; const xhr = new XMLHttpRequest(); xhr.open("GET", `/api/posts?page=${page}`); xhr.responseType = "json"; xhr.onload = () => { if (xhr.status === 200) { const posts = xhr.response; displayPosts(posts); isLoading = false; document.getElementById('loading').style.display = 'none'; } }; xhr.send(); } // Отображение постов function displayPosts(posts) { const postsDiv = document.getElementById('posts'); posts.forEach(post => { const postElement = document.createElement('div'); postElement.className = 'post'; postElement.innerHTML = ` <h3>${post.title}</h3> <p>${post.content}</p> `; postsDiv.appendChild(postElement); }); } // Отслеживаем прокрутку window.addEventListener('scroll', () => { const scrollHeight = document.documentElement.scrollHeight; const scrollTop = document.documentElement.scrollTop; const clientHeight = document.documentElement.clientHeight; // Если прокрутили почти до конца if (scrollTop + clientHeight >= scrollHeight - 100) { currentPage++; loadPosts(currentPage); } }); // Загружаем первую страницу loadPosts(1); </script>

📜 Безопасность AJAX-запросов

🔒 CORS (Cross-Origin Resource Sharing)

По умолчанию браузеры блокируют AJAX-запросы к другим доменам из-за политики Same-Origin Policy. Сервер должен явно разрешить кросс-доменные запросы через заголовки CORS:

// На сервере (например, Node.js) response.setHeader('Access-Control-Allow-Origin', '*'); response.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE'); response.setHeader('Access-Control-Allow-Headers', 'Content-Type');

CSRF (Cross-Site Request Forgery) защита

// Добавляем CSRF-токен в запросы const csrfToken = document.querySelector('meta[name="csrf-token"]').content; const xhr = new XMLHttpRequest(); xhr.open("POST", "/api/users"); xhr.setRequestHeader("X-CSRF-Token", csrfToken); xhr.send(data);

XSS (Cross-Site Scripting) защита

// ❌ ОПАСНО: вставка HTML напрямую element.innerHTML = xhr.responseText; // ✅ БЕЗОПАСНО: экранирование HTML function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } element.innerHTML = escapeHtml(xhr.responseText); // ✅ ЕЩЁ БЕЗОПАСНЕЕ: использование textContent element.textContent = xhr.responseText;

📜 Отладка AJAX-запросов

Логирование в консоль

const xhr = new XMLHttpRequest(); // Логируем все события xhr.addEventListener('loadstart', () => console.log('loadstart')); xhr.addEventListener('progress', (e) => console.log('progress', e.loaded, e.total)); xhr.addEventListener('load', () => console.log('load', xhr.status, xhr.responseText)); xhr.addEventListener('loadend', () => console.log('loadend')); xhr.addEventListener('error', () => console.log('error')); xhr.addEventListener('abort', () => console.log('abort')); xhr.addEventListener('timeout', () => console.log('timeout')); xhr.open("GET", "/api/users"); xhr.send(); console.log('Запрос отправлен');

Использование DevTools

💡 Chrome DevTools для AJAX
  • Network tab: просмотр всех запросов, заголовков, тела запроса/ответа
  • Console: вывод логов и ошибок
  • Preserve log: сохранение логов при перезагрузке страницы
  • XHR filter: фильтр для показа только AJAX-запросов
  • Timing: информация о времени выполнения запроса

📜 Оптимизация AJAX-запросов

Debounce для поисковых запросов

function debounce(func, delay) { let timeoutId; return function(...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => func.apply(this, args), delay); }; } const searchInput = document.getElementById('search'); const debouncedSearch = debounce((query) => { const xhr = new XMLHttpRequest(); xhr.open("GET", `/api/search?q=${query}`); xhr.onload = () => { if (xhr.status === 200) { displayResults(JSON.parse(xhr.responseText)); } }; xhr.send(); }, 300); searchInput.addEventListener('input', (e) => { debouncedSearch(e.target.value); });

Кэширование ответов

const cache = new Map(); function cachedRequest(url) { // Проверяем кэш if (cache.has(url)) { console.log('Из кэша:', url); return Promise.resolve(cache.get(url)); } // Выполняем запрос return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.responseType = "json"; xhr.onload = () => { if (xhr.status === 200) { // Сохраняем в кэш cache.set(url, xhr.response); resolve(xhr.response); } else { reject(new Error(`HTTP ${xhr.status}`)); } }; xhr.onerror = () => reject(new Error("Network error")); xhr.send(); }); } // Использование cachedRequest('/api/users') .then(data => console.log(data)); // Повторный запрос возьмёт данные из кэша cachedRequest('/api/users') .then(data => console.log(data));

📜 Заключение

📚 Основные выводы
  • XMLHttpRequest — классический API для AJAX-запросов
  • Основные методы: open(), send(), setRequestHeader()
  • Основные события: load, error, progress, timeout
  • Состояния readyState: 0-4 (DONE = 4)
  • HTTP статусы: 200 (OK), 404 (Not Found), 500 (Server Error)
  • Для современной разработки рекомендуется Fetch API
  • XMLHttpRequest актуален для поддержки старых браузеров и отслеживания прогресса

📜 Переход на Fetch API

Сравнение XMLHttpRequest и Fetch для одной и той же задачи:

// XMLHttpRequest const xhr = new XMLHttpRequest(); xhr.open("GET", "/api/users"); xhr.responseType = "json"; xhr.onload = () => { if (xhr.status === 200) { console.log(xhr.response); } }; xhr.send(); // Fetch API (проще и современнее) fetch("/api/users") .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error(error));
✅ Рекомендации
  • Для новых проектов используйте Fetch API (глава 19)
  • Используйте XMLHttpRequest только если:
    • Нужна поддержка IE9-IE11
    • Требуется отслеживание прогресса загрузки/отправки
    • Работаете с legacy-кодом
  • Оборачивайте XMLHttpRequest в Promise для удобства
  • Всегда обрабатывайте ошибки (onerror, ontimeout)
  • Используйте debounce для частых запросов (поиск)
  • Кэшируйте повторяющиеся запросы

Глава 19. Fetch API

📜 Введение в Fetch API

Fetch API — это современный интерфейс для выполнения HTTP-запросов в JavaScript. Он предоставляет упрощённый и более мощный инструмент для работы с сетевыми ресурсами по сравнению со стандартным XMLHttpRequest.

✅ Преимущества Fetch API
  • Простой синтаксис — основан на Promise, легко читается
  • Удобная работа с данными — встроенные методы для JSON, текста, blob
  • Поддержка async/await — чистый асинхронный код
  • Мощная настройка — заголовки, методы, CORS, credentials
  • Современный стандарт — активно развивается, поддерживается всеми браузерами

Ключевым элементом этого Fetch API является функция fetch(). Она реализована в различных контекстах. В частности, в браузере она реализована интерфейсом Windows.

📜 Базовый синтаксис

Функция fetch имеет следующий синтаксис:

const fetchPromise = fetch(resource [, init])

В качестве обязательного параметра - resource функция принимает параметры ресурса, к которому функция будет обращаться. В качестве необязательного параметра - init функция может принимать объект с дополнительными настройками запроса.

Функция fetch() возвращает объект Promise, который получает ответ после завершения запроса к сетевому ресурсу.

Определение ресурса на сервере

Рассмотрим простейший пример. Итак, прежде всего нам потребуется некоторый сетевой ресурс, к которому мы будем обращаться. Для эмуляции сетевого ресурса используем локальный веб-сервер. Веб-сервер может быть любым. В данном случае воспользуемся самым простым вариантом - Node.js, поэтому перед созданием приложения необходимо установить Node.js. Но опять же вместо node.js это может быть любая другая технология сервера - php, asp.net, python и т.д. либо какой-то определенный веб-сервер типа Apache или IIS.

Итак, создадим на жестком диске папку для файлов веб-сервера. Например, в моем случае это папка C:\app. Далее в этой папке определим файл сервера. Пусть он будет называться server.js и будет иметь следующий код:

const http = require("http"); const fs = require("fs"); http.createServer(function(request, response){ if(request.url == "/hello"){ response.end("Fetch на METANIT.COM"); } else{ fs.readFile("index.html", (error, data) => response.end(data)); } }).listen(3000, ()=>console.log("Сервер запущен по адресу http://localhost:3000"));

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

const http = require("http"); // для обработки входящих запросов const fs = require("fs"); // для чтения с жесткого диска файла index.html

Для создания сервера применяется функция http.createServer(). В эту функцию передается функция-обработчик, которая вызывается каждый раз, когда к серверу приходит запрос. Эта функция имеет два параметра: request (содержит данные запроса) и response (управляет отправкой ответа).

В функции-обработчике с помощью свойства request.url мы можем узнать, к какому ресурсу на сервере пришел запрос. Так, в данном случае, если пришел запрос по пути "/hello" (условно к ресурсу "/hello"), то оправляем в ответ с помощью метода response.end() текст "Fetch на METANIT.COM":

if(request.url == "/hello"){ response.end("Fetch на METANIT.COM"); }

Если запрос пришел к какому-то другому ресурсу, то отправляем файл index.html, который мы дальше определим:

else{ fs.readFile("index.html", (error, data) => response.end(data)); }

Для считывания файлов применяется встроенная функция fs.readFile(). Первый параметр функции - адрес файла (в данном случае предполагается, что файл index.html находится в одной папке с файлом сервера server.js). Второй параметр - функция, которая вызывается после считывания файла и получет его содержимое через свой второй параметр data. Затем считанное содежимое также может быть отпавлено с помощью функции response.end(data).

В конце с помощью функции listen() запускаем веб-сервер на 3000 порту. То есть сервер будет запускаться по адресу http://localhost:3000/

Вызов функции fetch()

Теперь в папке сервера определим простенький файл index.html

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <script> fetch("/hello") .then(response => response.text()) .then(responseText => console.log(responseText)); </script> </body> </html>

В функцию fetch() передается адрес ресурса - в данном случае "/hello".

fetch("/hello")

Поскольку fetch() возвращает объект Promise, то для получения результата запроса мы можем вызвать метод then()

fetch("/hello").then(response => response.text())

В метод then() передается функция-колбек. которая в качестве параметра response получает ответ от сервера. Однако ответ сервера представляет комплексный объект, который инкапсулирует много различной информации. Пока нас интересует только текст, который посылает сервер. И для получения этого текста у объекта response вызывается метод response.text().

Метод response.text() также возвращает Promise. И чтобы получить собственно текст ответа, подсоединяем второй метод then(), в котором в колбек-функции получаем текст ответа:

.then(responseText => console.log(responseText));

Теперь в консоли перейдем к папке сервера с помощью команды cd и запустим сервер с помощью команды

node server.js

После запуска сервера мы можем перейти в браузере по адресу http://localhost:3000, нам отобразится страница, в javascript-коде которой с помощью функции fetch() произойдет обращение к ресурсу "/hello":

В итоге при обращении к ресурсу "/hello" сервер отправит отправит строку "Fetch на METANIT.COM", которую мы сможем получить на веб-странице.

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

fetch("http://localhost:3000/hello") .then(response => response.text()) .then(responseText => console.log(responseText));

fetch с async/await

Поскольку функция fetch() возвращает Promise, то вместо нанизывания методов then() мы можем использовать операторы async/await для извлечения ответа. Например, перепишем предыдущий пример:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <script> getText(); async function getText() { // получаем объект ответа const response = await fetch("/hello"); // из объекта ответа извлекаем текст ответа const responseText = await response.text(); console.log(responseText); } </script> </body> </html>

Еще пример:

// Простейший GET-запрос fetch(url) .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error(error)); // С дополнительными опциями fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) .then(response => response.json()) .then(data => console.log(data));

📜 Простой GET-запрос

С Promise

// Запрос к API fetch('/api/users') .then(response => { // Проверяем статус ответа if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then(users => { console.log('Пользователи:', users); users.forEach(user => { console.log(user.name); }); }) .catch(error => { console.error('Ошибка:', error); });

С async/await (рекомендуется)

async function getUsers() { try { const response = await fetch('/api/users'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const users = await response.json(); console.log('Пользователи:', users); return users; } catch (error) { console.error('Ошибка:', error); return null; } } getUsers();
⚠️ Важно про обработку ошибок

Fetch не отклоняет Promise при HTTP-ошибках (404, 500 и т.д.). Promise отклоняется только при сетевых ошибках. Всегда проверяйте response.ok или response.status!

📜 Получение ответа. Объект Response

Функция fetch() возвращает Promise, который разрешается в объект Response.

Для представления ответа от сервера в Fetch API применяется интерфейс Response. Функция fetch() возвращает объект Promise, функция-коллбек в котором в качестве параметра получает объект Response с полученным от сервера ответом:

fetch("/hello").then(response => /* действия с response */ )

Либо можно использовать async/await для получения объекта Response

async function getText() { // получаем объект ответа const response = await fetch("http://localhost:3000/hello"); // действия с объектом response ....... }

Основные свойства Response

С помощью свойств объекта Response можно получить из полученного ответа различную информацию. Объект Response имеет следующие свойства:

Свойство Описание
ok true если статус 200-299

хранит булевое значение, которое указывает, завершился ли запрос успешно (то есть если статусной код ответа находится в диапазоне 200-299)

status HTTP код ответа (200, 404, 500...)

хранит статусный код ответа

statusText Текст статуса ("OK", "Not Found"...) хранит сообщение статуса, которое соответствует статусному коду
headers Объект Headers с заголовками
url URL запроса. Хранит адрес URL. Если в процессе запроса происходит ряд переадресаций, то хранит конечный адрес URL после всех переадресаций
redirected Был ли редирект. Хранит булевое значение, которое указывает, является ли ответ результатом переадресации
type Тип ответа ("basic", "cors"...)
body содержимое ответа в виде объекта ReadableStream
bodyUsed Было ли прочитано тело ответа

Стоит отметить, что все эти свойства доступны только для чтения. Например, используем ряд свойств для получения информации об ответа сервера. Для этого определим следующий сервер на Node.js, который обрабатывает запросы:

const http = require("http"); const fs = require("fs"); http.createServer(function(request, response){ if(request.url == "/hello"){ response.end("Fetch на METANIT.COM"); } else{ fs.readFile("index.html", (error, data) => response.end(data)); } }).listen(3000, ()=>console.log("Сервер запущен по адресу http://localhost:3000"));

На странице index.html вызовем функцию fetch и получим информацию об ответе:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <script> fetch("/hello") .then(response => { console.log(response.status); // 200 console.log(response.statusText); // OK console.log(response.url); // http://localhost:3000/hello }); </script> </body> </html>

Аналогичный пример с async/await:

getResponse(); async function getResponse() { const response = await fetch("/hello"); console.log(response.status); // 200 console.log(response.statusText); // OK console.log(response.url); // http://localhost:3000/hello }

Свойство ok возвращает true, если статусный код ответа в диапазоне от 200 до 299, что обычно говорит о том, что запрос успешно выполнен. И мы можем проверять это свойство перед ббработкой ответа:

fetch("/hello").then(response => { if(response.ok){ // обработка ответа } });
async function checkResponse() { const response = await fetch('/api/users'); console.log('OK:', response.ok); // true/false console.log('Статус:', response.status); // 200, 404... console.log('Текст:', response.statusText); // "OK" console.log('URL:', response.url); console.log('Редирект:', response.redirected); // Получаем данные if (response.ok) { const data = await response.json(); console.log(data); } }

Получение заголовков

С помощью свойства headers можно получить заголовки ответа, которые представляют интерфейс Headers.

Для получения данных из заголовков мы можем воспользоваться один из следующих методов интерфейса Headers:

  • entries(): возвращает итератор, который позволяет пройтись по всем заголовкам
  • forEach(): осуществляет перебор заголовков
  • get(): возвращает значение определенного заголовка
  • has(): возвращает true, если установлен определенный заголовок
  • keys(): получает все названия установленных заголовков
  • values(): получает все значения установленных заголовков

Например, получим все заголовки ответа:

fetch("/hello").then(response => { for(header of response.headers){ console.log(header[0],":",header[1]); } });

Каждый заголовок представляет массив из двух элементов, где первый элемент - название заголовка, а второй - его значение.

Вывод консоли браузера в нашем случае:

connection : keep-alive
content-length : 22
date : Fri, 03 Dec 2021 17:09:34 GMT
keep-alive : timeout=5

Другой пример - проверка заголовка и при его наличии вывод его значения:

fetch("/hello").then(response => { const headerTitle = "date"; // название заголовка if(response.headers.has(headerTitle)){ // если заголовок есть console.log(response.headers.get(headerTitle)); // получаем его значение } });

Таким образом, мы можем получать и кастомные заголовки, которые устанавливаются на стороне сервера. Например, пусть сервер node.js устанавливает заголовок "Secret-Code":

const http = require("http"); const fs = require("fs"); http.createServer(function(request, response){ if(request.url == "/hello"){ response.setHeader("Secret-Code", 124); response.end("Fetch на METANIT.COM"); } else{ fs.readFile("index.html", (error, data) => response.end(data)); } }).listen(3000, ()=>console.log("Сервер запущен по адресу http://localhost:3000"));

Для установки заголовка в node.js применяется метод response.setHeader(), первый параметр которого - название заголовка, а второй его значение.

Получим этот заголовок на веб-странице:

fetch("/hello").then(response => { console.log(response.headers.get("Content-Type")); // null - заголовок не установлен console.log(response.headers.get("Secret-Code")); // 124 });

Если заголовок не установлен, то метод response.headers.get() возвращает null.

Переадресация

Если в процессе запроса произошла переадресация, то свойство redirected равно true, а свойство url хранит адрес, на который произошла переадресация. Например, пусть сервер на node.js выполняет переадресацию с адреса "/hello" на адрес "/newpage":

const http = require("http"); const fs = require("fs"); http.createServer(function(request, response){ if(request.url == "/hello"){ response.statusCode = 302; // 302 - код временной переадресации response.setHeader("Location", "/newpage"); // переадресация на адрес localhost:3000/newpage response.end(); } else if(request.url == "/newpage"){ response.setHeader("Secret-Code", "New Page Code: 567"); //для теста устанавливаем заголовок response.end("This is a new page"); } else{ fs.readFile("index.html", (error, data) => response.end(data)); } }).listen(3000, ()=>console.log("Сервер запущен по адресу http://localhost:3000"));

Выполним запрос по адресу "/hello" на веб-странице:

fetch("/hello").then(response =>{ if (response.redirected) { console.log("Произошел редирект на адрес", response.url); console.log(response.headers.get("Secret-Code")); } });

Вывод консоли браузера:

Произошел редирект на адрес http://localhost:3000/newpage
New Page Code: 567	

По консольному выводу, а именно по заголовку Secret-Code мы видим, что функция fetch получила ответ от нового адреса - "/newpage".

📜 Методы получения данных из Response

Все данные, которые отправил сервер, доступны в объекте Response через свойство body, которое представляет объект ReadableStream, но гораздо проще воспользоваться одним из методов объекта Response. Интерфейс Response предоставляет следующие методы:

Метод Возвращает Когда использовать
response.json() Promise<Object> JSON-данные
response.text() Promise<string> Текст, HTML
response.blob() Promise<Blob> Изображения, файлы
response.arrayBuffer() Promise<ArrayBuffer> Бинарные данные
response.formData() Promise<FormData> Данные формы
response.clone() Promise<Response Object> Копия текущего объекта
response.error() Promise<Response Object> новый объект Response, ассоциированный с возникшей ошибкой сети
response.redirect() новый объект Response новый объект Response с другим адресом URL
⚠️ Тело можно прочитать только один раз!

После вызова любого метода чтения (json(), text() и т.д.), свойство bodyUsed становится true, и повторно прочитать тело уже нельзя.

Получение ответа в виде текста/HTML

async function getHTML() { const response = await fetch('/pages/about.html'); const html = await response.text(); document.getElementById('content').innerHTML = html; }

Для получения ответа в виде текста применяется метод text(). Например, сервер на Node.js отправляет в ответ клиенту некоторый текст:

const http = require("http"); const fs = require("fs"); http.createServer(function(request, response){ if(request.url == "/hello"){ response.end("Fetch на METANIT.COM"); } else{ fs.readFile("index.html", (error, data) => response.end(data)); } }).listen(3000, ()=>console.log("Сервер запущен по адресу http://localhost:3000"));

В данном случае при обращении по адресу "/hello" сервер будет отправлять в ответ клиенту строку текста "Fetch на METANIT.COM".

На странице index.html с помощью метода text() получим эту строку

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <script> fetch("/hello") .then(response => response.text()) .then(responseText => console.log(responseText)); </script> </body> </html>

Для получения отправленного текста у объекта response вызывается метод response.text(), который возвращает Promise. И чтобы получить собственно текст ответа, подсоединяем второй метод then(), в котором в функции-колбеке получаем текст ответа:

then(responseText => console.log(responseText));

Либо можно использовать async/await

getText(); async function getText() { // получаем объект ответа const response = await fetch("/hello"); // из объекта ответа извлекаем текст ответа const responseText = await response.text(); console.log(responseText); }

Получение ответа в виде JSON

async function getUsers() { const response = await fetch('/api/users'); const users = await response.json(); return users; }

Пусть сервер отправляет некоторый json-объект:

const http = require("http"); const fs = require("fs"); http.createServer(function(request, response){ if(request.url == "/user"){ const user = {name: "Tom", age: 37}; response.end(JSON.stringify(user)); } else{ fs.readFile("index.html", (error, data) => response.end(data)); } }).listen(3000, ()=>console.log("Сервер запущен по адресу http://localhost:3000"));

В данном случае сервер при обращении по адресу "/user" отправляет объект user в виде кода json.

Получим этот объект:

fetch("/user") .then(response => response.json()) .then(user => console.log(user.name, " - ", user.age));

Метод json() возвращает объект Promise, поэтому во втором методе then() можно получить собственно отправленный объект json и обратиться к его свойствам:

.then(user => console.log(user.name, "-", user.age));

Тот же пример с применением async/await:

getUser(); async function getUser() { // получаем объект ответа const response = await fetch("/user"); // из объекта ответа извлекаем json const user = await response.json(); console.log(user.name, "-", user.age); }

Отправка набора данных

Аналогичым образом можно получать набор объектов в формате json. Допустим, сервер на node.js отправляет массив объектов:

const http = require("http"); const fs = require("fs"); http.createServer(function(request, response){ if(request.url == "/users.json"){ const users = [ {name: "Tom", age: 37}, {name: "Sam", age: 25}, {name: "Bob", age: 41} ]; response.end(JSON.stringify(users)); } else{ fs.readFile("index.html", (error, data) => response.end(data)); } }).listen(3000, ()=>console.log("Сервер запущен по адресу http://localhost:3000"));

Получим эти данные на веб-странице:

fetch("/users.json") .then(response => response.json()) .then(users => users.forEach(user=> console.log(user.name, " - ", user.age))); // аналог с async/await getUsers(); async function getUsers() { const response = await fetch("/users.json"); const users = await response.json(); users.forEach(user=> console.log(user.name, " - ", user.age)) }

Отправка файла json

Пусть в папке сервера определен файл users.json со следующим содержимым:

[ {"name": "Tom", "age": 37}, {"name": "Sam", "age": 25}, {"name": "Bob", "age": 41} ]

В случае с сервером node.js мы могли бы в качестве варианта отправить данный файл следующим образом:

const http = require("http"); const fs = require("fs"); http.createServer(function(request, response){ if(request.url == "/users.json"){ fs.readFile("users.json", (error, data) => response.end(data)); } else{ fs.readFile("index.html", (error, data) => response.end(data)); } }).listen(3000, ()=>console.log("Сервер запущен по адресу http://localhost:3000"));

В зависимости от конкретной технологии сервера отправка файлов может отличаться. Здесь же, как и в случае с отправкой файла index.html, считываем данные из файла users.json с помощью функции fs.readFile() и отправляем в ответ.

На стороне клиента был бы тот же код, что и в предыдущем случае:

fetch("/users.json") .then(response => response.json()) .then(users => users.forEach(user=> console.log(user.name, " - ", user.age)));

Получение изображения (Blob)

async function loadImage() { const response = await fetch('/images/photo.jpg'); const blob = await response.blob(); // Создаём URL для blob const imageUrl = URL.createObjectURL(blob); // Отображаем изображение const img = document.createElement('img'); img.src = imageUrl; document.body.appendChild(img); // Освобождаем память (опционально) img.onload = () => URL.revokeObjectURL(imageUrl); }

С помощью метода blob() можно загрузить бинарные данные. Рассмотрим на примере изображений. Допустим, на сервере есть файл forest.png

Пусть сервер node.js отправляет данный файл при обращении по адресу "forest.png":

const http = require("http"); const fs = require("fs"); http.createServer(function(request, response){ if(request.url == "/forest.png"){ fs.readFile("forest.png", (error, data) => response.end(data)); } else{ fs.readFile("index.html", (error, data) => response.end(data)); } }).listen(3000, ()=>console.log("Сервер запущен по адресу http://localhost:3000"));

На веб-странице index.html для обращения к серверу определим следующий код:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <img /> <script> const img = document.querySelector("img"); fetch("/forest.png") .then(response => response.blob()) .then(data => img.src = URL.createObjectURL(data)); </script> </body> </html>

Для показа изображения на веб-странице определен элемент <img >

Метод blob() возвращает объект Promise, который получает данные ответа в виде объекта Blob. И во втором методе then() получаем этот объект:

then(data => img.src = URL.createObjectURL(data));

Здесь нам надо для html-элемента <img > в качестве источника изображения установить полученный файл. Для этого вызывается функция URL.createObjectURL(), в которую передается объект Blob. Эта функция создает адрес URL, на который проецируется загруженный файл. Соответственно после выполнения запроса html-элемент <img > отобразит загруженное изображение:

Аналогичый пример с применением async/await:

const img = document.querySelector("img"); getImage(); async function getImage() { const response = await fetch("/forest.png"); const imgBlob = await response.blob(); img.src = URL.createObjectURL(imgBlob); }

Подобным образом можно получать и использовать другие типы файлов, например, аудио и видеофайлы.

Получение бинарных данных

async function loadBinary() { const response = await fetch('/data/file.bin'); const buffer = await response.arrayBuffer(); // Работаем с бинарными данными const view = new Uint8Array(buffer); console.log('Первый байт:', view[0]); }

📜 POST-запросы и отправка данных

Настройка запроса (параметр init)

Функция fetch() может дополнительно принимать опции запроса в виде второго необязательного параметра:

fetch(resource [, init])

Параметр init представляет сложный объект, который может имеет большой набор опций:

  • method: метод запроса, например, GET, POST, PUT и т.д.

  • headers: устанавливаемые в запросе заголовки
  • body: тело запроса - данные, которые добавляются в запрос.
  • mode: режим запроса, например, cors, no-cors и same-origin
  • credentials: определяет действия с учетными данными (куки, данные HTTP-аутентификации и сертификаты клиента TLS). Принимает одно из следующих значений:
    • omit: учетные данные исключаются из запроса, а любые учетные данные, присланные в ответе от сервера, игнорируются

    • same-origin: учетные данные включаются только в те запросы и принимаются в ответах только на те запросы, адрес которых принадлежит к тому же домену, что и адрес клиента.
    • include: учетные данные включаются в любые запросы и принимаются в ответах на любые запросы
  • cache: устанавливает принцип взаимодействия с кэшем браузера. Возможные значения: default, no-store, reload, no-cache, force-cache и only-if-cached
  • redirect: устанавливает, как реагировать на редиректы. Может принимать следующие значения:

    • follow: автоматически применять редирект
    • error: при редиректе генерировать ошибку
    • manual: обрабатывать ответ в другом контексте
  • referrer: определяет реферера запроса
  • referrerPolicy: определяет политику реферера - как информация о реферере будет передаваться в запросе. Может принимать следующие значения: no-referrer, no-referrer-when-downgrade, same-origin, origin, strict-origin, origin-when-cross-origin, strict-origin-when-cross-origin и unsafe-url
  • integrity: содержит контрольное значение ресурса
  • keepalive: позволяет запросу существовать дольше, чем время жизни веб-страницы.
  • signal: предоставляет объект AbortSignal и позволяет отменить выполнение запроса.

Второй параметр fetch(url, init) принимает объект с настройками:

Опция Описание Значения
method HTTP-метод "GET", "POST", "PUT", "DELETE", "PATCH"
headers Заголовки запроса Объект или Headers
body Тело запроса String, FormData, Blob, JSON
mode Режим CORS "cors", "no-cors", "same-origin"
credentials Отправка cookies "omit", "same-origin", "include"
cache Режим кэширования "default", "no-cache", "reload", "force-cache"
redirect Обработка редиректов "follow", "error", "manual"
signal AbortSignal для отмены controller.signal

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

async function customRequest() { const response = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer token123', 'X-Custom-Header': 'value' }, body: JSON.stringify({ name: 'Tom' }), mode: 'cors', credentials: 'include', cache: 'no-cache' }); const data = await response.json(); return data; }

Пример настройки опций:

fetch("/user", { method: "GET", headers: { "Accept": "application/json" } }) .then(response => response.json()) .then(user => console.log(user));

В данном случае устанавливаем метод запроса - "GET" и заголовок "Accept" - его значение "application/json" говорит, что клиент принимает данные в формате json.

📜 Работа с заголовками

Стоит отметить, что свойство headers представляет объект Headers. Мы можем применять методы данного объекта для установки заголовков:

const myHeaders = new Headers(); myHeaders.append("Accept", "application/json"); fetch("/user", { method: "GET", headers: myHeaders }) .then(response => response.json()) .then(user => console.log(user));

Метод append() добавляет определенный заголовок, название которого передается через первый параметр, а значение заголовка - через второй параметр.

Также можно использовать метод set() для установки заголовка, а если заголовок уже ранее добавлен, то метод set() заменяет его значение. Если же надо удалить ранее добавленный заголовок, то можно использовать метод delete(), который получает имя удаляемого заголовка:

const myHeaders = new Headers(); myHeaders.append("Accept", "application/json"); // добавляем заголовок Accept myHeaders.set("Accept", "text/xml"); // Изменяем значение заголовка myHeaders.delete("Accept"); // Удаляем заголовок

Установка заголовков

// Вариант 1: объект fetch('/api/users', { headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer token' } }); // Вариант 2: объект Headers const headers = new Headers(); headers.append('Content-Type', 'application/json'); headers.append('Authorization', 'Bearer token'); fetch('/api/users', { headers });

Чтение заголовков ответа

async function getHeaders() { const response = await fetch('/api/users'); // Получаем конкретный заголовок const contentType = response.headers.get('Content-Type'); console.log('Content-Type:', contentType); // Проверяем наличие заголовка if (response.headers.has('X-Custom-Header')) { console.log('Кастомный заголовок присутствует'); } // Перебираем все заголовки for (const [key, value] of response.headers) { console.log(`${key}: ${value}`); } }

Отправка данных в запросе

Для отправки данных в запросе в функции fetch() в рамках второго параметра применяется опция body. Эти данные могут представлять типы Blob, BufferSource, FormData, URLSearchParams, USVString и ReadableStream. Стоит учитывать, что в запросах с методом GET и HEAD для запроса нельзя установить эту опцию.

Для тестирования отправки определим простейший сервер на node.js, который принимает данные:

const http = require("http"); const fs = require("fs"); http.createServer(async (request, response) => { if(request.url == "/user"){ const buffers = []; // буфер для получаемых данных // получаем данные из запроса в буфер for await (const chunk of request) { buffers.push(chunk); } // получаем строковое представление ответа let userName = Buffer.concat(buffers).toString(); userName = userName + " Smith"; response.end(userName); } else{ fs.readFile("index.html", (error, data) => response.end(data)); } }).listen(3000, ()=>console.log("Сервер запущен по адресу http://localhost:3000"));

В данном случае при обращении по адресу "/user" сервер получает все отправленные данные. Объект запроса предоставляет итератор для извлечения данные. И в коде сервера эти данные передаются в специальный массив-буфер:

for await (const chunk of request){ buffers.push(chunk); }

Затем с помощью метода Buffer.concat() объединяем все полученные данные и формируем из них строку:

let userName = Buffer.concat(buffers).toString();

В данном случае мы предполагаем, что на сервер отправляется простая строка с текстом. Пусть она представляет некоторое имя пользователя. И для примера к этому имени добавляется фамилия и измененное имя отправляется обратно клиенту:

userName = userName + " Smith"; response.end(userName);

Теперь определим на странице index.html код для отправки данных на этот сервер:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <script> fetch("/user", { method: "POST", body: "Tom" }) .then(response => response.text()) .then(userName => console.log(userName)); </script> </body> </html>

Для отправки применяется метод POST. А в качестве отправляемых данных выступает простая строка "Tom". Таким образом, мы отправляем простой текст. И поскольку сервер в ответ также отправляет текст, то для получения ответа здесь применяется метод response.text(). И при запуске данной веб-страницы будет выполняться отправка данных на сервер, и в консоли браузера мы сможем лицезреть полученный от сервера ответ:

Отправка JSON

Подобным образом можно отправлять более сложные по структуре данные. Например, рассмотрим отправку json. Для этого на строне node.js определим следующий сервер:

const http = require("http"); const fs = require("fs"); http.createServer(async (request, response) => { if(request.url == "/user"){ const buffers = []; for await (const chunk of request) { buffers.push(chunk); } const data = Buffer.concat(buffers).toString(); const user = JSON.parse(data); // парсим строку в json // изменяем данные полученного объекта user.name = user.name + " Smith"; user.age += 1; // отправляем измененый объект обратно клиенту response.end(JSON.stringify(user)); } else{ fs.readFile("index.html", (error, data) => response.end(data)); } }).listen(3000, ()=>console.log("Сервер запущен по адресу http://localhost:3000"));

В данном случае на сервера ожидаем, что мы получим объект в формате json, который имеет два свойства - name и age. Для примера сервер меняет значения этих свойств и отправляет измененный объект обратно клиенту.

На веб-странице установим объект json для отправки и получим обратно данные с помощью метода respose.json():

fetch("/user", { method: "POST", headers: { "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ name: "Tom", age: 37 }) }) .then(response => response.json()) .then(user => console.log(user.name, "-", user.age));

Другой пример:

async function createUser(userData) { try { const response = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(userData) }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const newUser = await response.json(); console.log('Создан пользователь:', newUser); return newUser; } catch (error) { console.error('Ошибка:', error); return null; } } // Использование createUser({ name: 'Tom', age: 37, email: 'tom@example.com' });

Отправка FormData

async function uploadForm() { const formData = new FormData(); formData.append('name', 'Tom'); formData.append('age', 37); formData.append('photo', fileInput.files[0]); const response = await fetch('/api/upload', { method: 'POST', body: formData // Content-Type устанавливается автоматически! }); const result = await response.json(); console.log(result); }

Отправка формы HTML

<form id="userForm"> <input name="name" placeholder="Имя" /> <input name="age" type="number" placeholder="Возраст" /> <button type="submit">Отправить</button> </form> <script> const form = document.getElementById('userForm'); form.addEventListener('submit', async (e) => { e.preventDefault(); // Создаём FormData из формы const formData = new FormData(form); try { const response = await fetch('/api/users', { method: 'POST', body: formData }); if (response.ok) { const result = await response.json(); console.log('Успех:', result); form.reset(); } } catch (error) { console.error('Ошибка:', error); } }); </script>

📜 Различные типы HTTP-запросов

GET — получение данных

// Простой GET const users = await fetch('/api/users') .then(r => r.json()); // GET с параметрами const query = new URLSearchParams({ page: 1, limit: 10 }); const data = await fetch(`/api/users?${query}`) .then(r => r.json());

POST — создание ресурса

const response = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Tom', age: 37 }) }); const newUser = await response.json();

PUT — полное обновление

const response = await fetch('/api/users/1', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Tom', age: 38, email: 'tom@example.com' }) }); const updatedUser = await response.json();

PATCH — частичное обновление

const response = await fetch('/api/users/1', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ age: 38 }) // Обновляем только возраст }); const updatedUser = await response.json();

DELETE — удаление

const response = await fetch('/api/users/1', { method: 'DELETE' }); if (response.ok) { console.log('Пользователь удалён'); }

📜 Отмена запроса (AbortController)

// Создаём контроллер const controller = new AbortController(); const signal = controller.signal; // Запускаем запрос с сигналом fetch('/api/slow-endpoint', { signal }) .then(response => response.json()) .then(data => console.log(data)) .catch(error => { if (error.name === 'AbortError') { console.log('Запрос отменён'); } else { console.error('Ошибка:', error); } }); // Отменяем запрос через 3 секунды setTimeout(() => controller.abort(), 3000);

Отмена при размонтировании компонента (React)

function UsersList() { const [users, setUsers] = useState([]); useEffect(() => { const controller = new AbortController(); fetch('/api/users', { signal: controller.signal }) .then(r => r.json()) .then(setUsers) .catch(error => { if (error.name !== 'AbortError') { console.error(error); } }); // Отменяем при размонтировании return () => controller.abort(); }, []); return <div>{/* ... */}</div>; }

📜 Обработка ошибок

async function fetchWithErrorHandling(url) { try { const response = await fetch(url); // Проверяем HTTP статус if (!response.ok) { // Пытаемся получить детали ошибки let errorMessage = `HTTP ${response.status}`; try { const errorData = await response.json(); errorMessage = errorData.message || errorMessage; } catch { // JSON парсинг не удался } throw new Error(errorMessage); } const data = await response.json(); return data; } catch (error) { // Сетевая ошибка или наше исключение console.error('Ошибка запроса:', error.message); throw error; } }

Обработка разных статусов

async function handleResponse(response) { if (response.ok) { return await response.json(); } // Обрабатываем разные статусы switch (response.status) { case 400: throw new Error('Неверный запрос'); case 401: throw new Error('Требуется авторизация'); case 403: throw new Error('Доступ запрещён'); case 404: throw new Error('Ресурс не найден'); case 500: throw new Error('Ошибка сервера'); default: throw new Error(`HTTP ${response.status}`); } }

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

Пример 1: CRUD для REST API

const API_URL = '/api/users'; // Create (POST) async function createUser(userData) { const response = await fetch(API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(userData) }); return await response.json(); } // Read (GET) async function getUsers() { const response = await fetch(API_URL); return await response.json(); } async function getUser(id) { const response = await fetch(`${API_URL}/${id}`); return await response.json(); } // Update (PUT) async function updateUser(id, userData) { const response = await fetch(`${API_URL}/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(userData) }); return await response.json(); } // Delete (DELETE) async function deleteUser(id) { const response = await fetch(`${API_URL}/${id}`, { method: 'DELETE' }); return response.ok; }

Пример 2: Загрузка файла с прогрессом

async function uploadFile(file, onProgress) { const response = await fetch('/api/upload', { method: 'POST', body: file }); // Получаем reader для отслеживания прогресса const reader = response.body.getReader(); const contentLength = +response.headers.get('Content-Length'); let receivedLength = 0; const chunks = []; while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); receivedLength += value.length; // Вызываем callback прогресса if (onProgress) { onProgress(receivedLength, contentLength); } } // Собираем chunks const result = new Uint8Array(receivedLength); let position = 0; for (const chunk of chunks) { result.set(chunk, position); position += chunk.length; } return result; } // Использование uploadFile(file, (loaded, total) => { const percent = Math.round((loaded / total) * 100); console.log(`Загружено: ${percent}%`); });

Пример 3: Повторные попытки (Retry)

async function fetchWithRetry(url, options = {}, retries = 3, delay = 1000) { for (let i = 0; i < retries; i++) { try { const response = await fetch(url, options); if (response.ok) { return response; } // Не повторяем при клиентских ошибках (4xx) if (response.status >= 400 && response.status < 500) { throw new Error(`HTTP ${response.status}`); } console.log(`Попытка ${i + 1} не удалась, повторяем...`); } catch (error) { if (i === retries - 1) { throw error; } // Ждём перед следующей попыткой await new Promise(resolve => setTimeout(resolve, delay * (i + 1))); } } } // Использование const response = await fetchWithRetry('/api/unstable', {}, 3, 1000);

Пример 4: Параллельные запросы

async function loadAllData() { try { // Запускаем все запросы параллельно const [users, posts, comments] = await Promise.all([ fetch('/api/users').then(r => r.json()), fetch('/api/posts').then(r => r.json()), fetch('/api/comments').then(r => r.json()) ]); return { users, posts, comments }; } catch (error) { console.error('Ошибка загрузки:', error); return null; } }

Пример 5: Кэширование запросов

class FetchCache { constructor(ttl = 60000) { // TTL = 60 секунд this.cache = new Map(); this.ttl = ttl; } async fetch(url, options) { const key = url + JSON.stringify(options || {}); const cached = this.cache.get(key); // Проверяем кэш if (cached && Date.now() - cached.timestamp < this.ttl) { console.log('Из кэша:', url); return cached.response.clone(); } // Выполняем запрос const response = await fetch(url, options); // Сохраняем в кэш this.cache.set(key, { response: response.clone(), timestamp: Date.now() }); return response; } clear() { this.cache.clear(); } } // Использование const cache = new FetchCache(); const response1 = await cache.fetch('/api/users'); const users1 = await response1.json(); // Второй запрос возьмётся из кэша const response2 = await cache.fetch('/api/users'); const users2 = await response2.json();

📜 Создание клиента для REST API

Используя Fetch API в JavaScript, можно реализовать полноценный клиент для Web API в стиле REST для взаимодействия с пользователем. Архитектура REST предполагает применение следующих методов или типов запросов HTTP для взаимодействия с сервером:

  • GET
  • POST
  • PUT
  • DELETE

Рассмотрим, как создать свой клиент на javascript для API.

Создание сервера на node.js

Для начала определим сервер, который будет и будет собственно представлять Web API. В качестве примера возьмем Node.js. Для обработки запросов определим следующий файл server.js:

const http = require("http"); const fs = require("fs"); // данные, с которыми работает клиент const users = [ { id:1, name:"Tom", age:24}, { id:2, name:"Bob", age:27}, { id:3, name:"Alice", age:23} ] // обрабатываем полученные от клиента данные function getReqData(req) { return new Promise(async (resolve, reject) => { try { const buffers = []; for await (const chunk of req) { buffers.push(chunk); } const data = JSON.parse(Buffer.concat(buffers).toString()); resolve(data); } catch (error) { reject(error); } }); } http.createServer(async (request, response) => { // получение всех пользователей if (request.url === "/api/users" && request.method === "GET") { response.end(JSON.stringify(users)); } // получение одного пользователя по id else if (request.url.match(/\/api\/users\/([0-9]+)/) && request.method === "GET") { // получаем id из адреса url const id = request.url.split("/")[3]; // получаем пользователя по id const user = users.find((u) => u.id === parseInt(id)); // если пользователь найден, отправляем его if(user) response.end(JSON.stringify(user)); // если не найден, отправляем статусный код и сообщение об ошибке else{ response.writeHead(404, { "Content-Type": "application/json" }); response.end(JSON.stringify({ message: "Пользователь не найден" })); } } // удаление пользователя по id else if (request.url.match(/\/api\/users\/([0-9]+)/) && request.method === "DELETE") { // получаем id из адреса url const id = request.url.split("/")[3]; // получаем индекс пользователя по id const userIndex = users.findIndex((u) => u.id === parseInt(id)); // если пользователь найден, удаляем его из массива и отправляем клиенту if(userIndex > -1) { const user = users.splice(userIndex, 1)[0]; response.end(JSON.stringify(user)); } // если не найден, отправляем статусный код и сообщение об ошибке else{ response.writeHead(404, { "Content-Type": "application/json" }); response.end(JSON.stringify({ message: "Пользователь не найден" })); } } // добавление пользователя else if (request.url === "/api/users" && request.method === "POST") { try{ // получаем данные пользователя const userData = await getReqData(request); // создаем нового пользователя const user = {name: userData.name, age: userData.age}; // находим максимальный id const id = Math.max.apply(Math,users.map(function(u){return u.id;})) // увеличиваем его на единицу user.id = id + 1; // добавляем пользователя в массив users.push(user); response.end(JSON.stringify(user)); } catch(error){ response.writeHead(400, { "Content-Type": "application/json" }); response.end(JSON.stringify({ message: "Некорректный запрос" })); } } // изменение пользователя else if (request.url === "/api/users" && request.method === "PUT") { try{ const userData = await getReqData(request); // получаем пользователя по id const user = users.find((u) => u.id === parseInt(userData.id)); // если пользователь найден, изменяем его данные и отправляем обратно клиенту if(user) { user.age = userData.age; user.name = userData.name; response.end(JSON.stringify(user)); } // если не найден, отправляем статусный код и сообщение об ошибке else{ response.writeHead(404, { "Content-Type": "application/json" }); response.end(JSON.stringify({ message: "Пользователь не найден" })); } } catch(error){ response.writeHead(400, { "Content-Type": "application/json" }); response.end(JSON.stringify({ message: "Некорректный запрос" })); } } else if (request.url === "/" || request.url === "/index.html") { fs.readFile("index.html", (error, data) => response.end(data)); } else{ response.writeHead(404, { "Content-Type": "application/json" }); response.end(JSON.stringify({ message: "Ресурс не найден" })); } }).listen(3000, ()=>console.log("Сервер запущен по адресу http://localhost:3000"));

Разберем в общих чертах этот код. Вначале идет определение данных, с которыми будет работать клиент:

const users = [ { id:1, name:"Tom", age:24}, { id:2, name:"Bob", age:27}, { id:3, name:"Alice", age:23} ]

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

Далее определена функция getReqData(), которая извлекает из запроса присланные от клиента данные и конвертирует их в формат json (предполагается, что клиент будет присылать данные в формате json):

function getReqData(req) { return new Promise(async (resolve, reject) => { try { const buffers = []; for await (const chunk of req) { buffers.push(chunk); } const data = JSON.parse(Buffer.concat(buffers).toString()); resolve(data); } catch (error) { reject(error); } }); }

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

Далее для каждого типа запросов определен определенный сценарий.

Когда приложение получает запрос типа GET по адресу "api/users", то срабатывает следующий код:

if (request.url === "/api/users" && request.method === "GET") { response.end(JSON.stringify(users)); }

Здесь просто отправляем выше определенный массив users.

Когда клиент обращается к приложению для получения одного объекта по id в запрос типа GET по адресу "api/users/", то срабатывает следующий код:

else if (request.url.match(/\/api\/users\/([0-9]+)/) && request.method === "GET") { // получаем id из адреса url const id = request.url.split("/")[3]; // получаем пользователя по id const user = users.find((u) => u.id === parseInt(id)); // если пользователь найден, отправляем его if(user) response.end(JSON.stringify(user)); // если не найден, отправляем статусный код и сообщение об ошибке else{ response.writeHead(404, { "Content-Type": "application/json" }); response.end(JSON.stringify({ message: "Пользователь не найден" })); } }

В этом случае нам надо найти нужного пользователя по id в массиве, а если он не был найден, возвратить статусный код 404 с некоторым сообщением в формате JSON.

При получении DELETE-запроса по адресу "/api/users/:id" находим индекс объекта в массива. И если объект найден, то удаляем его из массива и отправляем клиенту:

// удаление пользователя по id else if (request.url.match(/\/api\/users\/([0-9]+)/) && request.method === "DELETE") { // получаем id из адреса url const id = request.url.split("/")[3]; // получаем индекс пользователя по id const userIndex = users.findIndex((u) => u.id === parseInt(id)); // если пользователь найден, удаляем его из массива и отправляем клиенту if(userIndex > -1) { const user = users.splice(userIndex, 1)[0]; response.end(JSON.stringify(user)); } // если не найден, отправляем статусный код и сообщение об ошибке else{ response.writeHead(404, { "Content-Type": "application/json" }); response.end(JSON.stringify({ message: "Пользователь не найден" })); } }

Если объект не найден, возвращаем статусный код 404.

При получении запроса с методом POST по адресу "/api/users" используем функцию getReqData() для извлечения данных из запроса:

else if (request.url === "/api/users" && request.method === "POST") { try{ // получаем данные пользователя const userData = await getReqData(request); // создаем нового пользователя const user = {name: userData.name, age: userData.age}; // находим максимальный id const id = Math.max.apply(Math,users.map(function(u){return u.id;})) // увеличиваем его на единицу user.id = id + 1; // добавляем пользователя в массив users.push(user); response.end(JSON.stringify(user)); } catch(error){ response.writeHead(400, { "Content-Type": "application/json" }); response.end(JSON.stringify({ message: "Некорректный запрос" })); } }

Поскольку при выполнении функции промис может передавать ошибку (например, в результате парсинга в JSON), оборачиваем весь код в try..catch. После получения данных нам надо создать новый объект и добавить его в массив объектов.

Если приложению приходит PUT-запрос, то также с помощью функции getReqData() получаем отправленные клиентом данные. Если объект найден в массиве, то изменяем его, иначе отправляем статусный код 404:

// изменение пользователя else if (request.url === "/api/users" && request.method === "PUT") { try{ const userData = await getReqData(request); // получаем пользователя по id const user = users.find((u) => u.id === parseInt(userData.id)); // если пользователь найден, изменяем его данные и отправляем обратно клиенту if(user) { user.age = userData.age; user.name = userData.name; response.end(JSON.stringify(user)); } // если не найден, отправляем статусный код и сообщение об ошибке else{ response.writeHead(404, { "Content-Type": "application/json" }); response.end(JSON.stringify({ message: "Пользователь не найден" })); } } catch(error){ response.writeHead(400, { "Content-Type": "application/json" }); response.end(JSON.stringify({ message: "Некорректный запрос" })); } }

Таким образом, мы определили простейший API. Теперь добавим код клиента.

Определение клиента

При обращении к корню веб-приложению или по адресу "/index.html", сервер будет отдавать файл index.html. Поэтому в одной папке с файлом сервера определим файл index.html со следующим кодом:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width" /> <title>METANIT.COM</title> <style> tr{height:30px;} td, th {min-width: 40px;text-align: left;} a {cursor:pointer; padding:5px;text-decoration: underline;color:navy;} input{width:180px;} </style> </head> <body> <h2>Список пользователей</h2> <form name="userForm"> <p> <label for="name">Имя:</label><br> <input name="name" /> </p> <p> <label for="age">Возраст:</label><br> <input name="age" type="number" min="1" max="110" /> </p> <p> <button type="submit">Сохранить</button> <button type="reset">Сбросить</button> </p> </form> <table> <thead><tr><th>Id</th><th>Имя</th><th>Возраст</th><th></th></tr></thead> <tbody> </tbody> </table> <script> let userId = 0; // идентификатор пользователя, который редактируется const userForm = document.forms["userForm"]; // форма ввода // Получение всех пользователей async function getUsers() { // отправляет запрос и получаем ответ const response = await fetch("/api/users", { method: "GET", headers: { "Accept": "application/json" } }); // если запрос прошел нормально if (response.ok === true) { // получаем данные const users = await response.json(); const rows = document.querySelector("tbody"); // добавляем полученные элементы в таблицу users.forEach(user => rows.append(row(user))); } } // Получение одного пользователя async function getUser(id) { const response = await fetch("/api/users/" + id, { method: "GET", headers: { "Accept": "application/json" } }); if (response.ok === true) { const user = await response.json(); userId = user.id; userForm.elements["name"].value = user.name; userForm.elements["age"].value = user.age; } } // Добавление пользователя async function createUser(userName, userAge) { const response = await fetch("api/users", { method: "POST", headers: { "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ name: userName, age: parseInt(userAge, 10) }) }); if (response.ok === true) { const user = await response.json(); reset(); document.querySelector("tbody").append(row(user)); } } // Изменение пользователя async function editUser(userId, userName, userAge) { const response = await fetch("api/users", { method: "PUT", headers: { "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ id: userId, name: userName, age: parseInt(userAge, 10) }) }); if (response.ok === true) { const user = await response.json(); reset(); document.querySelector("tr[data-rowid='" + user.id + "']").replaceWith(row(user)); } } // Удаление пользователя async function deleteUser(id) { const response = await fetch("/api/users/" + id, { method: "DELETE", headers: { "Accept": "application/json" } }); if (response.ok === true) { const user = await response.json(); document.querySelector("tr[data-rowid='" + user.id + "']").remove(); } } // сброс формы и текущего идентификатора пользователя function reset() { userForm.reset(); userId = 0; } // создание строки для таблицы function row(user) { const tr = document.createElement("tr"); tr.setAttribute("data-rowid", user.id); const idTd = document.createElement("td"); idTd.append(user.id); tr.append(idTd); const nameTd = document.createElement("td"); nameTd.append(user.name); tr.append(nameTd); const ageTd = document.createElement("td"); ageTd.append(user.age); tr.append(ageTd); const linksTd = document.createElement("td"); const editLink = document.createElement("a"); editLink.setAttribute("data-id", user.id); editLink.append("Изменить"); editLink.addEventListener("click", async e => { e.preventDefault(); await getUser(user.id); }); linksTd.append(editLink); const removeLink = document.createElement("a"); removeLink.setAttribute("data-id", user.id); removeLink.append("Удалить"); removeLink.addEventListener("click", async e => { e.preventDefault(); await deleteUser(user.id); }); linksTd.append(removeLink); tr.appendChild(linksTd); return tr; } // сброс значений формы userForm.addEventListener("reset", e => reset()); // отправка формы userForm.addEventListener("submit", e => { e.preventDefault(); const name = userForm.elements["name"].value; const age = userForm.elements["age"].value; if (userId === 0) createUser(name, age); else editUser(userId, name, age); }); // загрузка пользователей getUsers(); </script> </body> </html>

Основная логика здесь заключена в коде javascript. В самом начале определяются глобальные данные:

let userId = 0; // идентификатор пользователя, который редактируется const userForm = document.forms["userForm"]; // форма ввода

Константа userForm представляет форму для добавления или редактирования объекта. А с помощью переменной userId отслеживаем идентификатор загруженного пользователя. Если он равен 0, то пользователь создается. По умолчанию при загрузке страницы эта переменная равна нулю, так как никакой пользователь не загружен на форму. Если же userId НЕ равен 0, то пользователь ранее был загружен с помощью функции getUser, и мы собираемся отредактировать этого пользователя.

При загрузке страницы в браузере получаем все объекты из БД с помощью функции getUsers :

async function getUsers() { // отправляет запрос и получаем ответ const response = await fetch("/api/users", { method: "GET", headers: { "Accept": "application/json" } }); // если запрос прошел нормально if (response.ok === true) { // получаем данные const users = await response.json(); const rows = document.querySelector("tbody"); // добавляем полученные элементы в таблицу users.forEach(user => rows.append(row(user))); } }

Для добавления строк в таблицу используется функция row(), которая возвращает строку. В этой строке будут определены ссылки для изменения и удаления пользователя.

Ссылка для изменения пользователя с помощью функции getUser() получает с сервера выделенного пользователя:

async function getUser(id) { const response = await fetch("/api/users/" + id, { method: "GET", headers: { "Accept": "application/json" } }); if (response.ok === true) { const user = await response.json(); userId = user.id; userForm.elements["name"].value = user.name; userForm.elements["age"].value = user.age; } }

И выделенный пользователь добавляется в форму над таблицей.

Если userId равен 0, то выполняется функция createUser, которая отправляет данные в POST-запросе:

async function createUser(userName, userAge) { const response = await fetch("api/users", { method: "POST", headers: { "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ name: userName, age: parseInt(userAge, 10) }) }); if (response.ok === true) { const user = await response.json(); reset(); document.querySelector("tbody").append(row(user)); } }

Если же ранее пользователь был загружен на форму, и в переменной userId сохранился его id, то выполняется функция editUser, которая отправляет PUT-запрос:

async function editUser(userId, userName, userAge) { const response = await fetch("api/users", { method: "PUT", headers: { "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ id: userId, name: userName, age: parseInt(userAge, 10) }) }); if (response.ok === true) { const user = await response.json(); reset(); document.querySelector("tr[data-rowid='" + user.id + "']").replaceWith(row(user)); } }

В конце запустим файл сервера server.js командой:

node server.js

И обратимся в браузере по адресу "http://localhost:3000" и мы сможем управлять пользователями, которые хранятся в файле json:

Еще пример:

class ApiClient { constructor(baseURL) { this.baseURL = baseURL; this.defaultHeaders = { 'Content-Type': 'application/json' }; } setAuthToken(token) { this.defaultHeaders['Authorization'] = `Bearer ${token}`; } async request(endpoint, options = {}) { const url = `${this.baseURL}${endpoint}`; const config = { ...options, headers: { ...this.defaultHeaders, ...options.headers } }; try { const response = await fetch(url, config); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return await response.json(); } catch (error) { console.error(`Ошибка запроса к ${endpoint}:`, error); throw error; } } async get(endpoint) { return this.request(endpoint, { method: 'GET' }); } async post(endpoint, data) { return this.request(endpoint, { method: 'POST', body: JSON.stringify(data) }); } async put(endpoint, data) { return this.request(endpoint, { method: 'PUT', body: JSON.stringify(data) }); } async delete(endpoint) { return this.request(endpoint, { method: 'DELETE' }); } } // Использование const api = new ApiClient('https://api.example.com'); // Устанавливаем токен api.setAuthToken('your-token-here'); // Выполняем запросы const users = await api.get('/users'); const newUser = await api.post('/users', { name: 'Tom', age: 37 }); const updated = await api.put('/users/1', { age: 38 }); await api.delete('/users/1');

📜 Сравнение: XMLHttpRequest vs Fetch

Одна и та же задача

// XMLHttpRequest (старый способ) const xhr = new XMLHttpRequest(); xhr.open("GET", "/api/users"); xhr.responseType = "json"; xhr.onload = () => { if (xhr.status === 200) { console.log(xhr.response); } else { console.error('Ошибка:', xhr.status); } }; xhr.onerror = () => console.error('Сетевая ошибка'); xhr.send(); // Fetch API (современный способ) fetch("/api/users") .then(response => { if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); }) .then(data => console.log(data)) .catch(error => console.error('Ошибка:', error)); // Fetch + async/await (ещё удобнее) async function getUsers() { try { const response = await fetch("/api/users"); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); console.log(data); } catch (error) { console.error('Ошибка:', error); } }
Характеристика XMLHttpRequest Fetch API
Синтаксис Многословный, callback-based Простой, Promise-based
Обработка ответа Разные свойства для разных типов Унифицированные методы (.json(), .text())
Обработка ошибок События onerror, ontimeout .catch() или try/catch
HTTP ошибки Обрабатываются через status Нужно проверять response.ok
Прогресс Встроенный (onprogress) Сложнее (через ReadableStream)
Отмена xhr.abort() AbortController
Поддержка браузеров Все (включая IE) Современные (IE не поддерживает)

📜 Обработка CORS

💡 CORS (Cross-Origin Resource Sharing)

По умолчанию браузеры блокируют запросы к другим доменам. Fetch автоматически отправляет CORS-запросы, но сервер должен разрешить их через заголовки.

// Простой CORS-запрос fetch('https://api.example.com/users', { mode: 'cors' // По умолчанию }) .then(response => response.json()) .then(data => console.log(data)); // С credentials (cookies) fetch('https://api.example.com/users', { mode: 'cors', credentials: 'include' // Отправлять cookies }) .then(response => response.json()) .then(data => console.log(data));

Режимы CORS

Режим Описание
cors Стандартный CORS-запрос (по умолчанию)
no-cors Запрос без CORS, ограниченный доступ к ответу
same-origin Только запросы к своему домену

📜 Credentials (cookies и авторизация)

// omit - не отправлять cookies (по умолчанию для кросс-доменных) fetch('/api/users', { credentials: 'omit' }); // same-origin - отправлять cookies только на свой домен fetch('/api/users', { credentials: 'same-origin' }); // include - всегда отправлять cookies (даже кросс-доменные) fetch('https://api.example.com/users', { credentials: 'include' });

📜 Timeout для Fetch

Fetch не имеет встроенного timeout, но его можно реализовать через AbortController:

function fetchWithTimeout(url, options = {}, timeout = 5000) { return new Promise((resolve, reject) => { const controller = new AbortController(); const signal = controller.signal; // Таймер для отмены const timeoutId = setTimeout(() => { controller.abort(); reject(new Error('Timeout')); }, timeout); // Выполняем запрос fetch(url, { ...options, signal }) .then(response => { clearTimeout(timeoutId); resolve(response); }) .catch(error => { clearTimeout(timeoutId); if (error.name === 'AbortError') { reject(new Error('Timeout')); } else { reject(error); } }); }); } // Использование try { const response = await fetchWithTimeout('/api/slow', {}, 3000); const data = await response.json(); console.log(data); } catch (error) { console.error(error.message); // "Timeout" }

📜 Интерцепторы запросов

Создание обёртки с автоматической обработкой токенов и ошибок:

class FetchInterceptor { constructor() { this.requestInterceptors = []; this.responseInterceptors = []; } // Добавить перехватчик запроса addRequestInterceptor(interceptor) { this.requestInterceptors.push(interceptor); } // Добавить перехватчик ответа addResponseInterceptor(interceptor) { this.responseInterceptors.push(interceptor); } async fetch(url, options = {}) { // Применяем перехватчики запроса let modifiedOptions = { ...options }; for (const interceptor of this.requestInterceptors) { modifiedOptions = await interceptor(url, modifiedOptions); } // Выполняем запрос let response = await fetch(url, modifiedOptions); // Применяем перехватчики ответа for (const interceptor of this.responseInterceptors) { response = await interceptor(response); } return response; } } // Использование const api = new FetchInterceptor(); // Добавляем токен к каждому запросу api.addRequestInterceptor((url, options) => { const token = localStorage.getItem('token'); if (token) { options.headers = { ...options.headers, 'Authorization': `Bearer ${token}` }; } return options; }); // Автоматически обрабатываем 401 (не авторизован) api.addResponseInterceptor(async (response) => { if (response.status === 401) { // Перенаправляем на логин window.location.href = '/login'; } return response; }); // Теперь все запросы идут через перехватчики const response = await api.fetch('/api/users'); const data = await response.json();

📜 Мониторинг состояния сети

// Проверка соединения if (!navigator.onLine) { console.log('Нет соединения с интернетом'); } // Отслеживание изменения состояния window.addEventListener('online', () => { console.log('Соединение восстановлено'); // Повторить неудавшиеся запросы }); window.addEventListener('offline', () => { console.log('Соединение потеряно'); });

📜 Service Worker и кэширование

// В Service Worker (sw.js) self.addEventListener('fetch', (event) => { event.respondWith( caches.match(event.request) .then(response => { // Возвращаем из кэша или делаем запрос return response || fetch(event.request); }) ); });

📜 Лучшие практики

✅ Рекомендации по использованию Fetch
  • Всегда проверяйте response.ok — Fetch не отклоняет Promise при HTTP-ошибках
  • Используйте async/await — код получается чище и понятнее
  • Обрабатывайте ошибки — используйте try/catch или .catch()
  • Добавляйте timeout — через AbortController для защиты от зависших запросов
  • Отменяйте запросы — при размонтировании компонентов
  • Создавайте обёртки — для переиспользования логики (токены, обработка ошибок)
  • Кэшируйте данные — для уменьшения количества запросов
  • Используйте credentials осторожно — only when needed для безопасности

Универсальная функция для API-запросов

async function apiRequest(endpoint, options = {}) { const baseURL = 'https://api.example.com'; const timeout = options.timeout || 10000; // Создаём AbortController для timeout const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(`${baseURL}${endpoint}`, { ...options, signal: controller.signal, headers: { 'Content-Type': 'application/json', ...options.headers } }); clearTimeout(timeoutId); // Проверяем статус if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.message || `HTTP ${response.status}`); } // Возвращаем данные return await response.json(); } catch (error) { clearTimeout(timeoutId); if (error.name === 'AbortError') { throw new Error('Request timeout'); } throw error; } } // Использование try { const users = await apiRequest('/users'); console.log(users); } catch (error) { console.error('Ошибка:', error.message); }

📜 Типичные ошибки и их решения

❌ Частые ошибки

1. Забыли проверить response.ok

// ❌ НЕПРАВИЛЬНО const data = await fetch('/api/users').then(r => r.json()); // ✅ ПРАВИЛЬНО const response = await fetch('/api/users'); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json();

2. Забыли await у response.json()

// ❌ НЕПРАВИЛЬНО const data = response.json(); // Promise, а не данные! // ✅ ПРАВИЛЬНО const data = await response.json();

3. Попытка прочитать body дважды

// ❌ НЕПРАВИЛЬНО const text = await response.text(); const json = await response.json(); // Ошибка! // ✅ ПРАВИЛЬНО - клонируем response const text = await response.clone().text(); const json = await response.json();

4. Неправильная отправка JSON

// ❌ НЕПРАВИЛЬНО fetch('/api/users', { method: 'POST', body: { name: 'Tom' } // Объект, а не строка! }); // ✅ ПРАВИЛЬНО fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Tom' }) });

📜 Заключение

📚 Основные выводы
  • Fetch API — современный стандарт для HTTP-запросов
  • Promise-based — отлично работает с async/await
  • Проверяйте response.ok — Fetch не отклоняет Promise при HTTP-ошибках
  • Методы чтения: .json(), .text(), .blob(), .arrayBuffer(), .formData()
  • Отмена запросов через AbortController
  • Настройка запроса: method, headers, body, mode, credentials
  • CORS обрабатывается автоматически при mode: 'cors'
  • Создавайте обёртки для удобства и переиспользования кода

Шпаргалка по Fetch

// GET-запрос const data = await fetch('/api/users') .then(r => r.ok ? r.json() : Promise.reject(`HTTP ${r.status}`)); // POST с JSON await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Tom' }) }); // С async/await и обработкой ошибок async function getUsers() { try { const response = await fetch('/api/users'); if (!response.ok) throw new Error(`HTTP ${response.status}`); return await response.json(); } catch (error) { console.error('Ошибка:', error); return null; } } // С timeout и отменой const controller = new AbortController(); setTimeout(() => controller.abort(), 5000); try { const response = await fetch('/api/users', { signal: controller.signal }); const data = await response.json(); } catch (error) { if (error.name === 'AbortError') { console.log('Timeout'); } }
✅ Когда использовать Fetch API
  • Все современные проекты (рекомендуется)
  • SPA приложения (React, Vue, Angular)
  • Работа с REST API
  • Когда важна простота и читаемость кода
  • Когда нужна поддержка современных браузеров

Для поддержки старых браузеров (IE) используйте XMLHttpRequest (глава 18) или polyfill для Fetch.

Глава 20. WebSocket API и Server-Sent Events

📜 Введение в коммуникацию реального времени

Стандартные HTTP-запросы (AJAX, Fetch) работают по модели "запрос-ответ": клиент отправляет запрос, сервер отвечает, и соединение закрывается. Для приложений реального времени (чаты, онлайн-игры, уведомления) нужны другие технологии.

💡 Технологии реального времени
  • WebSocket — двунаправленная коммуникация (клиент ⇄ сервер)
  • Server-Sent Events (SSE) — однонаправленная коммуникация (сервер → клиент)
  • Long Polling — устаревший подход (не рекомендуется)

📜 WebSocket API

WebSocket — это протокол полнодуплексной связи через единое TCP-соединение. Позволяет серверу и клиенту обмениваться данными в режиме реального времени.

Web Socket API позволяет организовать соединение между клиентом и сервером, благодаря которому клиент и сервер могут отправлять данные друг другу в любое время.

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

  • Двунаправленная связь — сервер может отправлять данные без запроса клиента
  • Низкая задержка — постоянное соединение без overhead HTTP
  • Эффективность — меньше трафика, чем при polling
  • Real-time — идеально для чатов, игр, уведомлений

📜 Создание WebSocket подключения

Для создания подключения через Web Soket API применяется конструктор WebSocket(), в который передается адрес подключения.

const connection = new WebSocket("ws://example.com/test");
// Создаём WebSocket соединение const socket = new WebSocket('ws://localhost:9000'); // Для защищённого соединения используйте wss:// const secureSocket = new WebSocket('wss://example.com/socket');
⚠️ Протоколы WebSocket
  • ws:// — незащищённое соединение (аналог HTTP)
  • wss:// — защищённое соединение через SSL/TLS (аналог HTTPS)
  • В production всегда используйте wss://

📜 События WebSocket

Управление жизненным циклом соединения

Для управления подключением для WebSocket определено следующие события:

Событие Когда происходит Свойство/Метод
open Соединение установлено onopen / addEventListener('open')
message Получено сообщение onmessage / addEventListener('message')
error Произошла ошибка onerror / addEventListener('error')
close Соединение закрыто onclose / addEventListener('close')

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

// Создаём соединение const socket = new WebSocket('ws://localhost:9000'); // Соединение открыто socket.onopen = (event) => { console.log('Соединение установлено'); // Отправляем сообщение серверу socket.send('Hello Server!'); }; // Получено сообщение от сервера socket.onmessage = (event) => { console.log('Сообщение от сервера:', event.data); }; // Ошибка socket.onerror = (error) => { console.error('WebSocket ошибка:', error); }; // Соединение закрыто socket.onclose = (event) => { console.log('Соединение закрыто'); console.log('Код:', event.code); console.log('Причина:', event.reason); console.log('Чистое закрытие:', event.wasClean); };

Или так:

const connection = new WebSocket("ws://example.com/test"); // если соединение успешно установлено connection.onopen = (event) => { console.log("Connection opened"); }; // если возникла ошибка connection.onerror = (error) => { console.log(`WebSocket Error: ${error}`); }; // если соединение закрыто connection.onclose = (event) => { console.log("Connection closed"); };

📜 Состояния WebSocket (readyState)

Константа Значение Описание
CONNECTING 0 Соединение устанавливается
OPEN 1 Соединение открыто
CLOSING 2 Соединение закрывается
CLOSED 3 Соединение закрыто
const socket = new WebSocket('ws://localhost:9000'); // Проверяем состояние console.log(socket.readyState); // 0 (CONNECTING) socket.onopen = () => { console.log(socket.readyState); // 1 (OPEN) // Проверка перед отправкой if (socket.readyState === WebSocket.OPEN) { socket.send('Сообщение'); } };

📜 Отправка данных на сервер

Для отправки данных на сервер применяется метод send() объекта WebSocket. В качестве параметра в метод можно передать строки, бинарные данные (BLOB), объекты ArrayBuffer, типизированные массивы. Естественно отправка данных возможна только после успешной установки соединения. Например, отправка строки:

Отправка текста

// если установлено соединение, отправляем строку connection.onopen = (event) => { connection.send("Hello METANIT.COM"); };
socket.send('Hello Server!');

Отправка JSON

Или к примеру, если мы хотим отправить объект в формате JSON, то в начале его надо преобразовать в данный формат:

const data = { type: 'message', text: 'Hello', user: 'Alice' }; // отправляем объект в формате json socket.send(JSON.stringify(data));

Отправка бинарных данных

// ArrayBuffer const buffer = new ArrayBuffer(8); socket.send(buffer); // Blob const blob = new Blob(['Hello'], { type: 'text/plain' }); socket.send(blob);

📜 Получение данных

Когда приходят данные с сервера, у объекта WebSocket срабатывает событие message, для установки обработчика которого можно использовать свойство onmessage.

В обработчик события message передается объект типа MessageEvent. Этот объект предоставляет ряд свойств, которые позволяют извлечь данные ответа сервера:

  • data: возвращает полученные данные
  • origin: хранит адрес отправителя
  • lastEventId: хранит уникальный идентификатор события в виде строки.
  • source: возвращает объект MessageEventSource, который может быть объектом WindowProxy, MessagePort или ServiceWorker и который представляет отправителя полученных данных.
  • ports: возвращает массив объектов MessagePort, которые хранят использованные для отправки порты

Ключевое свойство тут, конечно, data, которое представляет полученные данные. Оно может представлять строку или бинарные данные. Если присланы бинарные данные, то свойство data может представлять тип ArrayBuffer, либо Blob. Пример получения данных:

connection.onmessage = (event) =>{ console.log(event.data); };

Если клиент получает бинарные данные, то с помощью свойства binaryType объекта WebSocket можно указать, какого именно типа мы хотим данные с помощью значений "blob" (для получения данных в виде объекта Blob) и "arraybuffer" (для получения данных в виде ArrayBuffer). Например:

const connection = new WebSocket("ws://example.com/test"); connection.binaryType = "arraybuffer";

Пример:

socket.onmessage = (event) => { const data = event.data; // Если данные - строка if (typeof data === 'string') { console.log('Текст:', data); // Попытка распарсить JSON try { const json = JSON.parse(data); console.log('JSON:', json); } catch (e) { console.log('Обычный текст'); } } // Если данные - Blob else if (data instanceof Blob) { console.log('Blob размером', data.size, 'байт'); // Читаем Blob const reader = new FileReader(); reader.onload = () => { console.log('Содержимое:', reader.result); }; reader.readAsText(data); } // Если данные - ArrayBuffer else if (data instanceof ArrayBuffer) { console.log('ArrayBuffer размером', data.byteLength, 'байт'); } };

📜 Закрытие соединения

// Закрыть соединение socket.close(); // С кодом и причиной socket.close(1000, 'Работа завершена'); // Обработка закрытия socket.onclose = (event) => { if (event.wasClean) { console.log('Соединение закрыто чисто'); } else { console.log('Обрыв соединения'); } console.log('Код:', event.code); console.log('Причина:', event.reason); };

Коды закрытия WebSocket

Код Название Описание
1000 Normal Closure Нормальное закрытие
1001 Going Away Сервер/клиент уходит (закрытие страницы)
1002 Protocol Error Ошибка протокола
1003 Unsupported Data Неподдерживаемый тип данных
1006 Abnormal Closure Аварийное закрытие (обрыв)
1009 Message Too Big Сообщение слишком большое

📜 Переподключение при обрыве

class ReconnectingWebSocket { constructor(url, options = {}) { this.url = url; this.reconnectInterval = options.reconnectInterval || 5000; this.maxReconnectAttempts = options.maxReconnectAttempts || 10; this.reconnectAttempts = 0; this.socket = null; this.connect(); } connect() { console.log('Подключение к', this.url); this.socket = new WebSocket(this.url); this.socket.onopen = (event) => { console.log('Соединение установлено'); this.reconnectAttempts = 0; if (this.onopen) this.onopen(event); }; this.socket.onmessage = (event) => { if (this.onmessage) this.onmessage(event); }; this.socket.onerror = (error) => { console.error('WebSocket ошибка:', error); if (this.onerror) this.onerror(error); }; this.socket.onclose = (event) => { console.log('Соединение закрыто'); if (this.onclose) this.onclose(event); // Переподключаемся if (this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++; console.log(`Переподключение (попытка ${this.reconnectAttempts})...`); setTimeout(() => this.connect(), this.reconnectInterval); } else { console.error('Превышено максимальное количество попыток'); } }; } send(data) { if (this.socket.readyState === WebSocket.OPEN) { this.socket.send(data); } else { console.error('WebSocket не подключен'); } } close() { this.maxReconnectAttempts = 0; // Отключаем переподключение this.socket.close(); } } // Использование const ws = new ReconnectingWebSocket('ws://localhost:9000', { reconnectInterval: 3000, maxReconnectAttempts: 5 }); ws.onopen = () => console.log('Подключено!'); ws.onmessage = (event) => console.log('Сообщение:', event.data); ws.send('Hello!');

📜 Практический пример: Чат

<div id="chat"> <div id="messages"></div> <input id="messageInput" placeholder="Введите сообщение" /> <button id="sendBtn">Отправить</button> </div> <script> const socket = new WebSocket('ws://localhost:9000'); const messagesDiv = document.getElementById('messages'); const messageInput = document.getElementById('messageInput'); const sendBtn = document.getElementById('sendBtn'); // Соединение установлено socket.onopen = () => { console.log('Подключено к чату'); addMessage('Система', 'Вы подключились к чату', 'system'); }; // Получено сообщение socket.onmessage = (event) => { const data = JSON.parse(event.data); addMessage(data.user, data.text, 'received'); }; // Отправка сообщения function sendMessage() { const text = messageInput.value.trim(); if (!text) return; const message = { user: 'Вы', text: text, timestamp: Date.now() }; // Отправляем на сервер socket.send(JSON.stringify(message)); // Отображаем у себя addMessage('Вы', text, 'sent'); messageInput.value = ''; } // Добавление сообщения в чат function addMessage(user, text, type) { const messageEl = document.createElement('div'); messageEl.className = `message ${type}`; messageEl.innerHTML = ` <strong>${user}:</strong> ${text} <small>${new Date().toLocaleTimeString()}</small> `; messagesDiv.appendChild(messageEl); messagesDiv.scrollTop = messagesDiv.scrollHeight; } // События кнопки и Enter sendBtn.addEventListener('click', sendMessage); messageInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') sendMessage(); }); // Закрытие соединения socket.onclose = () => { addMessage('Система', 'Соединение потеряно', 'system'); }; </script>

📜 Взаимодействие с сервером

Напишем небольшой пример взаимодействия клиента и сервера с помощью WebSocket. Прежде всего создадим код сервера. В качестве технологии сервера будем использовать Node.js, как ниболее простой вариант, тем более он использует тот же JavaScript. Соответственно прежде всего надо установить Node.js. Однако можно выбрать любую другую серверную технологию.

Мы могли бы написать весь код работы с WebSocket с нуля, но это не самая простая задача, и для демонстрации мы будем использовать имеющуюся специальную библиотеку - ws. Так, создадим файл server.js и определим в нем следующий код:

const WebSocket = require("ws"); const server = new WebSocket.Server({port: 9000}); server.on("connection", onConnect); // обработчик подключения клиента // параметр - подключенный клиент function onConnect(client) { console.log("Connection opened"); // обрабатываем входящие сообщения от клиента client.on("message", function(message) { // для диагностики сообщения клиента на консоль console.log("Client message:", message.toString()); client.send("Hello Client"); // отправка сообщения клиенту }); // закрытие подключения client.on("close", function() { console.log("Connection closed"); }); } console.log("Сервер запущен на 9000 порту");

Для создания сервера WebSocket вызываем функцию Server из библиотеки ws:

const WebSocket = require("ws"); const server = new WebSocket.Server({port: 9000});

Далее с помощью метода on() добавляем серверу для события "connection", которое срабатывает при подключении нового клиента, обработчик - функцию onConnect.

server.on("connection", onConnect);

В обработчик события в качестве параметра передается объект, через который можно взамодействовать с клиентом:

function onConnect(client) {

В этом обработчике устанавливаем для подключенного клиента событие "message", которое срабатывает, когда клинет присылает на сервер данные:

client.on("message", function(message) { // для диагностики сообщения клиента на консоль console.log("Client message:", message.toString()); client.send("Hello Client"); // отправка сообщения клиенту });

В функцию-обработчик события "message" передаются присланные от клиента данные (здесь параметр message). Внутри функции-обработчика выводим эти данные на консоль и с помощью метода send() посылаем клиенту некоторый ответ. Таким образом сервер получит от клиента данные и пошлет ему обратно ответ.

Также устанавливается обработчик события "close" для обработчик закрытия подключения.

Проблема с библиотекой "ws" заключается в том, что это не встроенная в Node.js библиотека, а сторонняя библиотека. Поэтому нам ее надо установить. Для этого перейдем в консоли к папке, где находится файл server.js, и далее в консоли выполним следующую команду

npm install ws

Эта команда установит библиотеку ws.

Теперь определим код клиента. Пусть это будет следующая страница index.html:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <script> const connection = new WebSocket("ws://localhost:9000"); // если соединение успешно установлено connection.onopen = (event) => { console.log("Connection opened"); connection.send("Hello Server"); }; // если возникла ошибка connection.onerror = (error) => { console.log(`WebSocket Error: ${error}`); }; // если соединение закрыто connection.onclose = (event) => { console.log("Connection closed"); }; // получаем ответ сервера connection.onmessage = (event) =>{ console.log("Server response:", event.data); }; </script> </body> </html>

В целом код работы с WebSocket был рассмотрен ранее. Отмечу основные моменты. При открытии подключения отправляем на сервер строку "Hello Server"

connection.send("Hello Server");

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

connection.onmessage = (event) =>{ console.log(event.data); };

Перейдем в консоли к папке, где располагается файл сервера - server.js и выполним в консоли команду

node server.js

Эта команда запустит сервер. И после этого кинем нашу веб-страницу index.html в браузер и посмотрим на его консольный вывод. В итоге консольный вывод сервера будет следующим:

C:\app> node server.js
Сервер запущен на 9000 порту
Connection opened
Client message: Hello Server

А консольный вывод клиента - веб-страницы index.html следующим:

Connection opened
Server response: Hello Client

📜 Server-Sent Events (SSE)

Server-Sent Events или сокращенно SSE представляет еще одну технологией взаимодействия клиента и сервера, которая позволяет серверу отправлять сообщению клиенту. Стоит отметить, что в отличие от WebSockets, коммуникация через Server-Sent Events является однонаправленной: сообщения доставляются в одном направлении - от сервера к клиенту (например, веб-браузеру пользователя). Это делает их отличным выбором, когда нет необходимости отправлять данные от клиента на сервер. Например, Server-Sent Events можно применять для обработки таких вещей, как обновление статуса в социальных сетях, ленты новостей или отправка данных для хранения на стороне клиента.

Server-Sent Events (SSE) — это технология, позволяющая серверу отправлять обновления клиенту через HTTP. В отличие от WebSocket, коммуникация однонаправленная: только сервер → клиент.

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

  • Уведомления — push-уведомления от сервера
  • Ленты новостей — обновления в реальном времени
  • Мониторинг — статусы, метрики, логи
  • Обновления данных — курсы валют, цены, счёт в игре
💡 SSE vs WebSocket
Характеристика SSE WebSocket
Направление Сервер → Клиент Двунаправленное
Протокол HTTP WebSocket (ws://)
Переподключение Автоматическое Вручную
Формат данных Только текст Текст и бинарные данные
Поддержка браузеров Все (кроме IE) Все современные
Сложность Проще Сложнее

📜 Использование EventSource

На веб-странице в коде JavaScript для взаимодействия с сервером применяется интерфейс EventSource. Объкт EventSource по сути представляет собой сервер, который генерирует события или отправляет сообщения. Для создания объекта EventSource применяется конструктор:

new EventSource(url, options)

В качестве первого обязательного параметра в конструктор EventSource передается URL-адрес ресурса на сервере:

const evtSource = new EventSource("/events");

Также опционально можно передать необязательный параметр, который настраивает объект EventSource. Этот параметр представляет объект с одним свойством withCredentials. Это свойство указывает, следует ли включать заголовки CORS для кроссдоменного взаимодействия. По умолчвнию оно равно false

События EventSource

Для управления состояния подключением в EventSource определено ряд событий:

  • open: генерируется при установке соединения. Для установки обработчика события можно применять свойство onopen
  • error: генерируется при возникновении ошибки при установке соединения. Для установки обработчика события можно применять свойство onerror
  • message: генерируется при получении данных с сервера. Для установки обработчика события можно применять свойство onmessage
Событие Когда генерируется Свойство/Метод
open При установке соединения onopen / addEventListener('open')
message При получении данных с сервера onmessage / addEventListener('onmessage')
error Ошибка при установке соединения onerror / addEventListener('onerror')

В качестве параметра обработчики этих событий принимают стандартный объект Event. Пример установки обработчиков событий:

const evtSource = new EventSource("/events"); // с помощью addEventListener evtSource.addEventListener("open", () => { console.log("connection opened"); }); evtSource.addEventListener("error", () => { console.log("Error"); }); // с помощью свойств evtSource.onopen = () => { console.log("connection opened"); }; evtSource.onerror = () => { console.log("Error"); };

Или так:

// Подключаемся к серверу const eventSource = new EventSource('/events'); // Получение сообщений eventSource.onmessage = (event) => { console.log('Новое сообщение:', event.data); }; // Соединение открыто eventSource.onopen = () => { console.log('SSE соединение установлено'); }; // Ошибка eventSource.onerror = (error) => { console.error('SSE ошибка:', error); if (eventSource.readyState === EventSource.CLOSED) { console.log('Соединение закрыто'); } };

📜 Прием данных (Объект MessageEvent)

Когда приходят данные с сервера, у объекта WebSocket срабатывает событие message, для установки обработчика которого можно использовать свойство onmessage, либо метод addEventListener().

В обработчик события message передается объект типа MessageEvent. Этот объект предоставляет ряд свойств, которые позволяют извлечь данные ответа сервера:

  • data: возвращает полученные данные
  • origin: хранит адрес отправителя
  • lastEventId: хранит уникальный идентификатор последнего события в виде строки.
  • source: возвращает объект MessageEventSource, который может быть объектом WindowProxy, MessagePort или ServiceWorker и который представляет отправителя полученных данных.
  • ports: возвращает массив объектов MessagePort, которые хранят использованные для отправки порты

При получении сообщения передаётся объект MessageEvent со следующими свойствами:

Свойство Описание
data Данные сообщения (строка)
origin Адрес отправителя
lastEventId ID последнего события
type Тип события
eventSource.onmessage = (event) => { console.log('Данные:', event.data); console.log('Источник:', event.origin); console.log('ID события:', event.lastEventId); console.log('Тип:', event.type); };

Пример получения данных:

const evtSource = new EventSource("/events"); evtSource.onmessage = (event) => { console.log(event.data); // выводим отправленные данные на консоль };

📜 Закрытие SSE соединения

Для закрытия соединения применяется метод close():

// Закрыть соединение eventSource.close(); // Проверка состояния if (eventSource.readyState === EventSource.CLOSED) { console.log('Соединение закрыто'); }

📜 Состояния EventSource (readyState)

Константа Значение Описание
CONNECTING 0 Соединение устанавливается или переподключение
OPEN 1 Соединение открыто
CLOSED 2 Соединение закрыто

📜 Пользовательские события SSE

const eventSource = new EventSource('/events'); // Стандартное событие (без типа) eventSource.onmessage = (event) => { console.log('Сообщение:', event.data); }; // Пользовательское событие eventSource.addEventListener('notification', (event) => { const data = JSON.parse(event.data); console.log('Уведомление:', data.title); showNotification(data.title, data.message); }); // Ещё одно пользовательское событие eventSource.addEventListener('update', (event) => { const data = JSON.parse(event.data); console.log('Обновление:', data); updateUI(data); });

📜 Практический пример: Лента уведомлений

<div id="notifications"></div> <script> const eventSource = new EventSource('/notifications'); const notificationsDiv = document.getElementById('notifications'); // Получение уведомлений eventSource.addEventListener('notification', (event) => { const data = JSON.parse(event.data); showNotification(data); }); // Отображение уведомления function showNotification(data) { const notif = document.createElement('div'); notif.className = 'notification'; notif.innerHTML = ` <h4>${data.title}</h4> <p>${data.message}</p> <small>${new Date(data.timestamp).toLocaleString()}</small> `; notificationsDiv.insertBefore(notif, notificationsDiv.firstChild); // Автоудаление через 5 секунд setTimeout(() => notif.remove(), 5000); } // Обработка ошибок eventSource.onerror = () => { console.log('Ошибка SSE, переподключение...'); }; // Закрытие при выгрузке страницы window.addEventListener('beforeunload', () => { eventSource.close(); }); </script>

📜 Пример взаимодействия между клиентом и сервером с Server-Sent Events

Рассмотрим небольшой пример взаимодействия между клиентом и сервером с помощью Server-Sent Events. В качестве клиента будет выступать код JavaScript на веб-странице. А в качестве сервера будем использовать Node.js.

Сначала определим код сервера. Для этого создадим файл server.js со следующим кодом:

const http = require("http"); const fs = require("fs"); // данные для отправки клиенту const messages = ["Привет", "Как дела?", "Что делаешь?", "Ты че спишь?", "Ну пока"]; http.createServer(function(request, response){ if(request.url == "/events"){ // если запрос SSE if (request.headers.accept && request.headers.accept === "text/event-stream") { sendEvent(response); } else{ response.writeHead(400); response.end("Bad Request"); } } else{ // в остальных случаях отправляем страницу index.html fs.readFile("index.html", (_, data) => response.end(data)); } }).listen(3000, ()=>console.log("Сервер запущен по адресу http://localhost:3000")); // отправляем сообщение клиенту function sendEvent(response) { // формируем заголовки response.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive" }); const id = (new Date()).toLocaleTimeString(); // определяем идентификатор последнего события // раз в 5 секунд отправляем одно сообщение setInterval(() => { createServerSendEvent(response, id); }, 5000); } // отправляем данные клиенту function createServerSendEvent(response, id) { // генерируем случайное число - индекс для массива messages const index = Math.floor(Math.random() * messages.length); const message = messages[index]; response.write("id: " + id + "\n"); response.write("data: " + message + "\n\n"); }

Вкратце пройдемся по коду. Сначала подключаются пакеты с функциональностью, которую мы собираемся использовать:

const http = require("http"); // для обработки входящих запросов const fs = require("fs"); // для чтения с жесткого диска файла index.html

Далее идет определение набора данных, которые будут отправляться клиенту - набор строк с важными сообщениями для клиента:

const messages = ["Привет", "Как дела?", "Что делаешь?", "Ты че спишь?", "Ну пока"];

Для создания сервера применяется функция http.createServer(). В эту функцию передается функция-обработчик, которая вызывается каждый раз, когда к серверу приходит запрос. Эта функция имеет два параметра: request (содержит данные запроса) и response (управляет отправкой ответа).

В функции-обработчике с помощью свойства request.url мы можем узнать, к какому ресурсу на сервере пришел запрос. Так, в данном случае, если пришел запрос по пути "/events", то мы будем взаимодействовать с клиентом с помощью Server-Sent Events:

iif(request.url == "/events"){ // если запрос SSE if (request.headers.accept && request.headers.accept === "text/event-stream") { sendEvent(response); } else{ response.writeHead(400); response.end("Bad Request"); } }

И тут важно, чтобы в запросе были установлен заголовок "Accept": он должен иметь значение "text/event-stream". Если это так, то для отправки данных клиенту выполняем функцию sendEvent(), в которую передаем объект ответа response. Если же заголовки не установлены, то отправляем в ответ ошибку 400.

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

function sendEvent(response) { // формируем заголовки response.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive" }); const id = (new Date()).toLocaleTimeString(); // определяем идентификатор последнего события // раз в 5 секунд отправляем одно сообщение setInterval(() => { createServerSendEvent(response, id); }, 5000); }

Собственно отправка данных производится в функции createServerSendEvent:

function createServerSendEvent(response, id) { // генерируем случайное число - индекс для массива messages const index = Math.floor(Math.random() * messages.length); const message = messages[index]; response.write("id: " + id + "\n"); response.write("data: " + message + "\n\n"); }

Здесь получаем случайное число, которое находится в диапазоне от 0 до messages.length и которое будет служить в качестве индекса, и по этому индексу выбирает некоторое сообщение. Далее формируем ответ. Устанавливаем идентификатор последнего события

response.write("id: " + id + "\n");

И устанавливаем собственно данные:

response.write("data: " + message + "\n\n");

Если запрос пришел на сервер по какому-то другому пути, то отправляем файл index.html, который мы дальше определим:

else{ fs.readFile("index.html", (_, data) => response.end(data)); }

Для считывания файлов применяется встроенная функция fs.readFile(). Первый параметр функции - адрес файла (в данном случае предполагается, что файл index.html находится в одной папке с файлом сервера server.js). Второй параметр - функция, которая вызывается после считывания файла и получет его содержимое через свой второй параметр data. Затем считанное содежимое также может быть отпавлено с помощью функции response.end(data).

В конце с помощью функции listen() запускаем веб-сервер на 3000 порту. То есть сервер будет запускаться по адресу http://localhost:3000/

Теперь в папке сервера определим простенький файл index.html со следующим кодом:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <ul id="list"></ul> <script> const source = new EventSource("/events"); const list = document.getElementById("list") source.addEventListener("message", (e) => { const listItem = document.createElement("li"); listItem.textContent += e.data; list.appendChild(listItem); }); </script> </body> </html>

Здесь при получении данных от сервера добавляем их в список на веб-странице.

Теперь в консоли перейдем к папке сервера с помощью команды cd и запустим сервер с помощью команды node server.js

C:\app>node server.js
Сервер запущен по адресу http://localhost:3000

После запуска сервера мы можем перейти в браузере по адресу http://localhost:3000, нам отобразится страница, в javascript-коде которой будет происходить получение данных от сервера и их вывод на веб-страницу:

📜 Формат данных SSE (для сервера)

Сервер отправляет данные в текстовом формате:

// Простое сообщение data: Hello World\n\n // Многострочное сообщение data: Первая строка\n data: Вторая строка\n\n // С ID события id: 123\n data: Сообщение с ID\n\n // Пользовательский тип события event: notification\n data: {"title": "Привет", "message": "Новое уведомление"}\n\n // Установка retry (миллисекунды) retry: 10000\n\n

📜 Сравнение технологий

✅ Выбор технологии

Используйте WebSocket когда:

  • Нужна двунаправленная коммуникация
  • Высокая частота обмена сообщениями
  • Низкая задержка критична (игры, трейдинг)
  • Нужно отправлять бинарные данные
  • Пример: чаты, многопользовательские игры, коллаборативное редактирование

Используйте SSE когда:

  • Нужна только передача сервер → клиент
  • Достаточно текстовых данных
  • Важна простота реализации
  • Нужно автоматическое переподключение
  • Пример: уведомления, ленты новостей, мониторинг

Используйте Long Polling когда:

  • WebSocket и SSE недоступны
  • Поддержка старых браузеров (IE9)
  • Редкие обновления (не критична задержка)

📜 Заключение

📚 Основные выводы
  • WebSocket — двунаправленная связь через ws:// или wss://
  • События WebSocket: open, message, error, close
  • Состояния: CONNECTING (0), OPEN (1), CLOSING (2), CLOSED (3)
  • SSE (EventSource) — однонаправленная связь через HTTP
  • События SSE: message, open, error + пользовательские
  • SSE автоматически переподключается, WebSocket — нет
  • WebSocket поддерживает бинарные данные, SSE — только текст
  • Выбирайте технологию в зависимости от задачи

Глава 21. Локализация (Internationalization API)

📜 Введение в Internationalization API

Внедрение последних стандартов добавило в язык JavaScript встроенные возможности локализации или то что, представляет Internationalization API. Хотя распостраненные браузеры уже давно внедрили данный API, но если мы работаем со старыми версиями браузеров, то мы можем столкнуться, что данный API не поддерживается. На этот случай мы можем проверить доступность объекта window.Intl:

if (window.Intl && typeof window.Intl === "object"){ console.log("Internationalization API поддерживается"); } else { console.log("Internationalization API не поддерживается"); }

Internationalization API (Intl) — это встроенный в JavaScript набор объектов для локализации чисел, дат, строк и других данных в соответствии с правилами разных языков и регионов.

💡 Зачем нужна локализация?
  • Форматы дат — в США: 12/31/2024, в России: 31.12.2024
  • Форматы чисел — разделители: 1,000.50 (US) vs 1 000,50 (RU)
  • Валюты — $100 (US) vs 100 $ (FR) vs 100 руб. (RU)
  • Названия — Germany (EN) vs Германия (RU) vs Deutschland (DE)
  • Сортировка — ё после е (RU), но не в алфавитном порядке (EN)

Основные объекты Intl

Объект Назначение
Intl.DateTimeFormat Форматирование дат и времени
Intl.NumberFormat Форматирование чисел и валют
Intl.ListFormat Форматирование списков
Intl.DisplayNames Локализация названий (стран, языков, валют)
Intl.Collator Сравнение и сортировка строк
Intl.RelativeTimeFormat Относительное время ("2 дня назад")
Intl.PluralRules Правила множественного числа

Для создания объектов этих типов применяются соответствующие функции-конструкторы, которые принимают до 3 параметров (на примере конструктора Intl.Collator()):

new Intl.Collator() new Intl.Collator(locales) new Intl.Collator(locales, options)

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

📜 Локали (Locales)

Локаль — это код языка и региона в формате BCP 47 или набор языковых кодов, например: "en-US", "ru-RU", "de-DE".

Можно использовать основной код локали. Например, английский язык представлен кодом "en". Однако могут быть разные вариации языка, и чтобы их отразить, к основному коду можно добавить код региона, например, "en-GB" (британский английский) или "en-US" (американский английский). Аналогично русский язык определяется кодом "ru" (либо региональными вариантами типа "ru-RU"), немецкий - кодом "de" и т.д.

Пример создания объектов для русскоязычной локали:

const locale = "ru"; const ruDateTimeFormat = new Intl.DateTimeFormat(locale); const ruNumberFormat = new Intl.NumberFormat(locale); const ruCollactor = new Intl.Collator(locale);

Также можно передать набор локалей

const locales = [ "ru-RU", "en-US", "de-DE"]; const dateTimeFormat = new Intl.DateTimeFormat(locales); const numberFormat = new Intl.NumberFormat(locales); const collactor = new Intl.Collator(locales);

В данном случае локализации будет применяться первая поддерживаемая локаль. Так, в списке [ "ru-RU", "en-US", "de-DE"] первой идет локаль "ru-RU", которая представляет российский вариант русского языка. Если эта локаль не поддерживается, тогда браузер смотрит поддерживается ли язык из основного кода локали, то есть в данном случае "ru" (русский язык без привязки к региону). Если и он не поддерживается, тогда браузер проверяет поддержку второй локали в массиве - в данном случае "en-US". Если и эта локаль не поддерживается, тогда проверяется локаль основного кода - "en" и так далее. В итоге браузер будет применять первую поддерживаемую локаль.

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

Чтобы проверить, поддерживается ли определенная локаль текущим браузером можно использовать статический метод supportedLocalesOf()

Intl.Collator.supportedLocalesOf() Intl.DateTimeFormat.supportedLocalesOf() Intl.NumberFormat.supportedLocalesOf()

В данный метод передается локаль или массив локалей, поддержку которых надо проверить. Например:

console.log(Intl.NumberFormat.supportedLocalesOf("ru")); // русский console.log(Intl.NumberFormat.supportedLocalesOf("ar")); // арабский console.log(Intl.NumberFormat.supportedLocalesOf(["de", "bo"])); // немецкий и тибетский

Данный метод возвращает массив поддерживаемых локалей, которые относятся к проверямым локалям. Например, в моем случае вывод будет следующим:

["ru"]
["ar"]
["de"]

Как видно из вывода тибетский у меня не поддерживается.

Формат локали

// Формат: язык-РЕГИОН "en" // Английский (без региона) "en-US" // Английский (США) "en-GB" // Английский (Великобритания) "ru" // Русский (без региона) "ru-RU" // Русский (Россия) "de-DE" // Немецкий (Германия) "fr-FR" // Французский (Франция) "zh-CN" // Китайский (Китай) "ja-JP" // Японский (Япония)

Определение локали браузера

// Локаль браузера console.log(navigator.language); // "ru-RU" console.log(navigator.languages); // ["ru-RU", "ru", "en-US", "en"] // Проверка поддержки Intl API if (window.Intl && typeof window.Intl === "object") { console.log("Internationalization API поддерживается"); } else { console.log("Internationalization API не поддерживается"); }

Массив локалей (fallback)

// Если первая локаль не поддерживается, используется следующая const locales = ["ru-RU", "en-US", "de-DE"]; const formatter = new Intl.DateTimeFormat(locales); // Будет использована первая поддерживаемая локаль

📜 Intl.DateTimeFormat — локализация дат и времени

Для локализации дат и времени в JavaScript применяется объект Intl.DateTimeFormat. Его конструктор может принимать два параметра:

Intl.DateTimeFormat([locales[, options]])

Параметр locales представляет код языка в формате BCP 47 или набор языковых кодов.

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

  • dateStyle: определяет стиль форматирования даты. Возможные значения:
    • "full"
    • "long"
    • "medium"
    • "short"
  • timeStyle: определяет стиль форматирования времени. Возможные значения:
    • "full"
    • "long"
    • "medium"
    • "short"
  • calendar: задает календарь. Возможные значения: "buddhist", "chinese", " coptic", "ethiopia", "ethiopic", "gregory", " hebrew", "indian", "islamic", "iso8601", "japanese", "persian", "roc"
  • numberingSystem: задает применяемую систему чисел. Возможные значения: "arab", "arabext", " bali", "beng", "deva", "fullwide", " gujr", "guru", "hanidec", "khmr", " knda", "laoo", "latn", "limb", "mlym", " mong", "mymr", "orya", "tamldec", " telu", "thai", "tibt"
  • dayPeriod: формат периода суток. Возможные значения: "narrow", "short", " long".
  • timeZone: временная зона.
  • hour12: указывает, будет ли использоваться 12-часовой формат (значение true) или 24-часовой формат (значение false) .
  • hourCycle: часовой цикл. Возможные значения: "h11", "h12", "h23", "h24".
  • formatMatcher: устанавливает алгоритм сопоставления формата даты/времени. Возможные значения: "basic" и "best fit" (значение по умолчанию).
  • weekday: определяет формат дня недели. Возможные значения:
    • "long" (например, Thursday)
    • "short" (например, Thu)
    • "narrow" (например, T - сокращение от Thursday)
  • era: определяет формат вывода эры. Возможные значения:
    • "long" (например, Anno Domini)
    • "short" (например, AD)
    • "narrow" (например, A)
  • year: определяет формат года. Возможные значения:
    • "numeric" (число полностью, например, 2021)
    • "2-digit" (выводит только две последних цифры)
  • month: определяет формат месяца. Возможные значения:
    • "numeric" (например, 2)
    • "2-digit" (в виде двухцифрового кода, например, 02)
    • "long" (например, March)
    • "short" (например, Mar)
    • "narrow" (например, M)
  • day: определяет, как выводится номер дня. Возможные значения:
    • "numeric" (например, 2)
    • "2-digit" (в виде двухцифрового кода, например, 02)
  • hour: задает формат вывода часа. Возможные значения: "numeric" и "2-digit"
  • minute: задает формат вывода минуты. Возможные значения: "numeric" и "2-digit"
  • second: задает формат вывода секунды. Возможные значения: "numeric" и "2-digit"
  • fractionalSecondDigits: определяет формат вывода долей секунды. Возможные значения:
    • 0 (доли секунды не выводятся)
    • 1 (выводится только первая цифра долей секунды, например, при значении 736 выводится 7)
    • 2 (выводятся только две первых цифра долей секунды, например, при значении 736 выводится 73)
    • 3 (выводятся три цифры долей секунды, например, при значении 736 выводится 736)
  • timeZoneName: определяет представление наименования часового пояса. Возможные значения:
    • "long" (полное название, например, "Pacific Standard Time, Nordamerikanische Westküsten-Normalzeit")
    • "short" (короткое название, например, PST, GMT-8)
    • "longOffset" (полное название в формате GMT, например, "GMT-8")
    • "shortOffset" (короткое название в формате GMT , например, "GMT-0800")
    • "longGeneric" (полный обобщенный формат, например, "Pacific Time, Nordamerikanische Westküstenzeit")
    • "shortGeneric" (короткий обобщенный формат, например, "PT, Los Angeles Zeit")

Для форматирования даты объект Intl.DateTimeFormat предоставляет метод format(), в который передается форматируемая дата - объект Date.

Рассмотрим несколько примеров:

const now = new Date(); const ruDate = new Intl.DateTimeFormat("ru").format(now); console.log(ruDate); // 16.11.2023 const enDate = new Intl.DateTimeFormat("en").format(now); console.log(enDate); // 11/16/2023

В данном случае в конструктор Intl.DateTimeFormat передается значение только для первого параметра locales. В первом случае это код "ru", который представляет русскоязычную культуру, а во втором случае код "en" - англоязычная культура. И в зависимости от переданного кода культуры мы получим разные результаты при форматировании даты.

Базовое использование

const now = new Date(); // Русский формат const ruDate = new Intl.DateTimeFormat("ru").format(now); console.log(ruDate); // 15.11.2025 // Американский формат const usDate = new Intl.DateTimeFormat("en-US").format(now); console.log(usDate); // 11/15/2025 // Немецкий формат const deDate = new Intl.DateTimeFormat("de-DE").format(now); console.log(deDate); // 15.11.2025

Настройка формата даты

По умолчанию метод format() возвращает дату в сокращенном формате, то есть фактически применяя настройку {dateStyle: "short"}. С помощью параметра dateStyle мы можем настроить вывод даты. Посмотрим, какие варианты вывода дат нам предоставляет объект Intl.DateTimeFormat на примере русскоязычной культуры:

const now = new Date(); const shortDate = new Intl.DateTimeFormat("ru", {dateStyle: "short"}).format(now); console.log(shortDate); // 12.09.2021 const mediumDate = new Intl.DateTimeFormat("ru", {dateStyle: "medium"}).format(now); console.log(mediumDate); // 12 сент. 2021 г. const longDate = new Intl.DateTimeFormat("ru", {dateStyle: "long"}).format(now); console.log(longDate); // 12 сентября 2021 г. const fullDate = new Intl.DateTimeFormat("ru", {dateStyle: "full"}).format(now); console.log(fullDate); // воскресенье, 12 сентября 2021 г.

Или так:

const now = new Date(); // Полная дата const fullDate = new Intl.DateTimeFormat("ru", { dateStyle: "full" }).format(now); console.log(fullDate); // суббота, 15 ноября 2025 г. // Длинная дата const longDate = new Intl.DateTimeFormat("ru", { dateStyle: "long" }).format(now); console.log(longDate); // 15 ноября 2025 г. // Средняя дата const mediumDate = new Intl.DateTimeFormat("ru", { dateStyle: "medium" }).format(now); console.log(mediumDate); // 15 нояб. 2025 г. // Короткая дата const shortDate = new Intl.DateTimeFormat("ru", { dateStyle: "short" }).format(now); console.log(shortDate); // 15.11.2025

Форматирование времени

По умолчанию метод format() не выводит время. С помощью параметра timeStyle настроим вывод времени на примере русскоязычной культуры:

const now = new Date(); const shortTime = new Intl.DateTimeFormat("ru", {timeStyle: "short"}).format(now); console.log(shortTime); // 20:42 const mediumTime = new Intl.DateTimeFormat("ru", {timeStyle: "medium"}).format(now); console.log(mediumTime); // 20:42:08 const longTime = new Intl.DateTimeFormat("ru", {timeStyle: "long"}).format(now); console.log(longTime); // 20:42:08 GMT+11 const fullTime = new Intl.DateTimeFormat("ru", {timeStyle: "full"}).format(now); console.log(fullTime); // 20:42:08 GMT+11:00

Другой пример:

const now = new Date(); // Полное время const fullTime = new Intl.DateTimeFormat("ru", { timeStyle: "full" }).format(now); console.log(fullTime); // 14:30:45 Москва, стандартное время // Длинное время const longTime = new Intl.DateTimeFormat("ru", { timeStyle: "long" }).format(now); console.log(longTime); // 14:30:45 GMT+3 // Среднее время const mediumTime = new Intl.DateTimeFormat("ru", { timeStyle: "medium" }).format(now); console.log(mediumTime); // 14:30:45 // Короткое время const shortTime = new Intl.DateTimeFormat("ru", { timeStyle: "short" }).format(now); console.log(shortTime); // 14:30

Комбинирование даты и времени

Если мы используем только настройку dateStyle, то выводится возвращается только дата. Если применяется настройка timeStyle, то возвращается только время. Чтобы возвратить и дату, и время, необходимо установить обе настройки:

const now = new Date(); const shortDateTime = new Intl.DateTimeFormat("ru", { dateStyle: "short", timeStyle: "short" }).format(now); console.log(shortDateTime); // 12.09.2021, 20:43

Другой пример:

const now = new Date(); const dateTime = new Intl.DateTimeFormat("ru", { dateStyle: "long", timeStyle: "short" }).format(now); console.log(dateTime); // 15 ноября 2025 г., 14:30

Детальная настройка

const now = new Date(); const customFormat = new Intl.DateTimeFormat("ru", { weekday: "long", // Полное название дня недели year: "numeric", // Год (2025) month: "long", // Полное название месяца day: "numeric", // День (15) hour: "2-digit", // Час с ведущим нулём (14) minute: "2-digit", // Минута с ведущим нулём (30) second: "2-digit" // Секунда с ведущим нулём (45) }).format(now); console.log(customFormat); // суббота, 15 ноября 2025 г., 14:30:45

Остальные настройки

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

const now = new Date(); const arDateTime1 = new Intl.DateTimeFormat("ar", { dateStyle: "short", timeStyle: "short" }).format(now); console.log(arDateTime1); // 12‏/9‏/2021 8:51 م const arDateTime2 = new Intl.DateTimeFormat("ar", { dateStyle: "short", timeStyle: "short", numberingSystem: "arab" }).format(now); console.log(arDateTime2); // ١٢‏/٩‏/٢٠٢١ ٨:٥٠ م

Если приложение рассчитано на среду, где действует другой календарь, то можно задать параметр calendar:

const now = new Date(); const persianDateTime = new Intl.DateTimeFormat("fa", { dateStyle: "long", numberingSystem: "arab", calendar: "persian" }).format(now); console.log(persianDateTime); // ٢١ شهریور ١٤٠٠ const zhDateTime = new Intl.DateTimeFormat("zh", { dateStyle: "long", calendar: "chinese" }).format(now); console.log(zhDateTime); // 2021辛丑年八月初六

Опции DateTimeFormat

Опция Значения Описание
weekday "narrow", "short", "long" День недели
year "numeric", "2-digit" Год
month "numeric", "2-digit", "narrow", "short", "long" Месяц
day "numeric", "2-digit" День
hour "numeric", "2-digit" Час
minute "numeric", "2-digit" Минута
second "numeric", "2-digit" Секунда
timeZone "UTC", "Europe/Moscow", ... Часовой пояс
hour12 true, false 12/24-часовой формат

Альтернатива: методы Date

Следует отметить, что тип Date также предоставляет ряд методов для локализации даты и времени:

  • toLocaleString()
  • toLocaleDateString()
  • toLocaleTimeString()

В качестве параметра эти методы принимают локаль, в которую надо локализовать дату и время:

const now = new Date(); console.log(now.toLocaleString("en")); // 11/16/2023, 9:17:25 PM console.log(now.toLocaleTimeString("en")); // 9:17:25 PM console.log(now.toLocaleDateString("en")); // 11/16/2023 console.log(now.toLocaleString("ru")); // 16.11.2023, 21:17:25 console.log(now.toLocaleTimeString("ru")); // 21:17:25 console.log(now.toLocaleDateString("ru")); // 16.11.2023

Еще пример:

const now = new Date(); // Методы Date тоже поддерживают локализацию console.log(now.toLocaleString("ru")); // 15.11.2025, 14:30:45 console.log(now.toLocaleDateString("ru")); // 15.11.2025 console.log(now.toLocaleTimeString("ru")); // 14:30:45 console.log(now.toLocaleString("en-US")); // 11/15/2025, 2:30:45 PM console.log(now.toLocaleDateString("en-US")); // 11/15/2025 console.log(now.toLocaleTimeString("en-US")); // 2:30:45 PM

📜 Intl.NumberFormat — Форматирование чисел

В разных культурах используются различные подходы к отображению чисел. Например, в одних культурах (в частности, в США, Великобритании) в качестве разделителя целой и дробной части применяется точка, а в других культурах - запятая. Аналогично разделителем между разрядами может служить как точка, так и запятая. И объект Intl.NumberFormat позволяет нам локализовать числительные под нужную культуру.

Конструктор Intl.NumberFormat может принимать два параметра:

Intl.NumberFormat([locales[, options]])

Параметр locales представляет код языка в формате BCP 47 или набор языковых кодов.

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

  • localeMatcher: алгоритм поиска соответствий. Может принимать два значения: "lookup" и "best fit". Значение по умолчанию - "best fit".
  • compactDisplay: применяется, если параметр notation равен "compact". Возможные значения: "long" и "short" (значение по умолчанию)
  • currency: задает валюту, которая применяется для форматирования. В качестве значения принимает код валюты в формате ISO 4217, например, "USD" (доллар США), "EUR" (евро) и т.д. Этот параметр необходимо обязательно указать, если параметр style имеет значение "currency".
  • currencyDisplay: указывает, как отображать валюту. Возможные значения:
    • "symbol": применяется символ валюты (например, € для евро). Значение по умолчанию
    • "narrowSymbol": применяется сокращенное обозначение валюты (например, "$100" вместо "US$100")
    • "code": применяется код валюты
    • "name": применяется локализованное название валюты (например, "dollar")
  • currencySign: знак перед числительным, которое представляет валюту. Может принимать значения "standard" (значение по умолчанию) и "accounting"
  • notation: задает тип форматирования. Возможные значения:
    • "standard": применяется для форматирования обычных чисел. Значение по умолчанию
    • "scientific": возвращает порядок величины для форматируемого числа
    • "engineering": возвращает значение в экспоненциальной нотации
    • "compact": для представления экспоненциальной записи применяется строка
  • numberingSystem: числовая система. Возможные значения: "arab", "arabext", " bali", "beng", "deva", "fullwide", " gujr", "guru", "hanidec", "khmr", " knda", "laoo", "latn", "limb", "mlym", " mong", "mymr", "orya", "tamldec", " telu", "thai", "tibt"
  • signDisplay: указывает, нужно ли отображать знак перед числом. Возможные значения:
    • "auto": знак отображается только для отрицательных чисел. Значение по умолчанию
    • "never": знак никогда не отображается
    • "always": знак отображается всегда
    • "exceptZero": знак отображается для всех чисел, кроме нуля
  • style: тип форматирования. Возможные значения:
    • "decimal": для форматирования обычных чисел. Значение по умолчанию
    • "currency": для форматирования валюты
    • "percent": для форматирования процентов
    • "unit": для форматирования единиц измерения
  • unit: устанавливает единицу измерения. Применяемые единицы измерения можно найти в следующей таблице.
  • unitDisplay: тип отображения единиц измерения. Возможные значения:
    • "long": полная форма (например, 16 litres)
    • "short": сокращенная форма (например, 16 l). Значение по умолчанию
    • "narrow": сжатая форма (например, 16l)
  • useGrouping: указывает, надо ли использовать разделитель для разрядов числа. Может принимать значения true (использовать разделители - значение по умолчанию) и false (не использовать разделители)
  • minimumIntegerDigits: минимальное количество цифр в числе. Возможные значения: от 1 (значение по умолчанию) до 21
  • minimumFractionDigits: минимальное количество цифр в дробной части числа. Возможные значения: от 0 (значение по умолчанию) до 20
  • maximumFractionDigits: максимальное количество цифр в дробной части числа. Возможные значения: от 0 до 20
  • minimumSignificantDigits: минимальное количество цифр в целой части числа. Возможные значения: от 1 (значение по умолчанию) до 21
  • maximumSignificantDigits: максимальное количество цифр в целой части числа. Возможные значения: от 1 (значение по умолчанию) до 21

Опции NumberFormat

Опция Значения Описание
style "decimal", "currency", "percent", "unit" Стиль форматирования
currency "USD", "EUR", "RUB", ... Код валюты (для style: "currency")
unit "kilometer", "celsius", "byte", ... Единица измерения
minimumFractionDigits число Минимум знаков после запятой
maximumFractionDigits число Максимум знаков после запятой
useGrouping true, false Разделители групп разрядов

Для форматирования числа объект Intl.NumberFormat предоставляет метод format(), в который передается форматируемое число и который возвращает отформатированное число в виде строки.

Локализуем число 5500,67 на разные языки:

const amount = 5500.67; const en = new Intl.NumberFormat("en").format(amount); const ru = new Intl.NumberFormat("ru").format(amount); const de = new Intl.NumberFormat("de").format(amount); console.log(en); // 5,500.67 console.log(ru); // 5 500,67 console.log(de); // 5.500,67

По умолчанию для форматирования чисел применяется параметр {style: "decimal"}. Также мы могли бы его явно применить:

const amount = 5500.67; const ru = new Intl.NumberFormat("ru", {style: "decimal"}).format(amount); // то же самое, что и // const ru = new Intl.NumberFormat("ru").format(amount); console.log(ru); // 5 500,67

Базовое использование

const number = 1234567.89; // Русский формат (пробелы как разделители) const ruNumber = new Intl.NumberFormat("ru").format(number); console.log(ruNumber); // 1 234 567,89 // Американский формат (запятые как разделители) const usNumber = new Intl.NumberFormat("en-US").format(number); console.log(usNumber); // 1,234,567.89 // Немецкий формат (точки как разделители) const deNumber = new Intl.NumberFormat("de-DE").format(number); console.log(deNumber); // 1.234.567,89

Форматирование валют

Для форматирования валюты применяется параметр style: "currency", при этом также надо указать параметр currency, которому передается код валюты:

const value = 85.1; const en = new Intl.NumberFormat("en", {style: "currency", currency: "USD"}).format(value); const ru = new Intl.NumberFormat("ru", {style: "currency", currency: "USD"}).format(value); const tr = new Intl.NumberFormat("tr", {style: "currency", currency: "USD"}).format(value); console.log(en); // $85.10 console.log(ru); // 85,10 $ console.log(tr); // $85,10

Вывод нескольких валют:

const value = 85.1; const usd = new Intl.NumberFormat("ru", {style: "currency", currency: "USD"}).format(value); const euro = new Intl.NumberFormat("ru", {style: "currency", currency: "EUR"}).format(value); const rub = new Intl.NumberFormat("ru", {style: "currency", currency: "RUB"}).format(value); console.log(usd); // 85,10 $ console.log(euro); // 85,10 € console.log(rub); // 85,10 ₽

По умолчанию выводится символ валюты, однако значение currencyDisplay: "name" позволяет вывести локализованное название валюты:

const value = 85; const usd = new Intl.NumberFormat("ru", { style: "currency", currency: "USD", currencyDisplay: "name", minimumFractionDigits: 0 }).format(value); const euro = new Intl.NumberFormat("ru", { style: "currency", currency: "EUR", currencyDisplay: "name" }).format(value); const rub = new Intl.NumberFormat("ru", { style: "currency", currency: "RUB", currencyDisplay: "name" }).format(value); console.log(usd); // 85 долларов США console.log(euro); // 85,00 евро console.log(rub); // 85,00 российского рубля

Еще пример:

const amount = 1234.56; // Рубли const rub = new Intl.NumberFormat("ru", { style: "currency", currency: "RUB" }).format(amount); console.log(rub); // 1 234,56 ₽ // Доллары const usd = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(amount); console.log(usd); // $1,234.56 // Евро const eur = new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" }).format(amount); console.log(eur); // 1.234,56 €

Форматирование процентов

В разных культурах может отличаться написание процентов. Для локализации процентов применяется значение "percent"

const value = 0.851; const en = new Intl.NumberFormat("en", {style: "percent"}).format(value); const ru = new Intl.NumberFormat("ru", {style: "percent"}).format(value); const tr = new Intl.NumberFormat("tr", {style: "percent"}).format(value); console.log(en); // 85% console.log(ru); // 85 % console.log(tr); // %85

Вывод дробной части

Однако в примере выше мы видим, что теряется дробная часть. Чтобы исправить положение, мы можем использовать параметр minimumFractionDigits, который задает количество знаков в дробной части:

const value = 0.851; const en = new Intl.NumberFormat("en", {style: "percent", minimumFractionDigits: 2}).format(value); const ru = new Intl.NumberFormat("ru", {style: "percent", minimumFractionDigits: 2}).format(value); console.log(en); // 85.10% console.log(ru); // 85,10 %

Еще пример:

// Проценты const percent = new Intl.NumberFormat("ru", { style: "percent" }).format(0.75); console.log(percent); // 75 % // Минимум/максимум знаков после запятой const precise = new Intl.NumberFormat("ru", { minimumFractionDigits: 2, maximumFractionDigits: 4 }).format(123.4); console.log(precise); // 123,40 // Единицы измерения (ES2020+) const speed = new Intl.NumberFormat("ru", { style: "unit", unit: "kilometer-per-hour" }).format(120); console.log(speed); // 120 км/ч const storage = new Intl.NumberFormat("en", { style: "unit", unit: "gigabyte" }).format(256); console.log(storage); // 256 GB

Форматирование единиц измерения

Для форматирования единиц измерения применяется значение style: "unit". При этом также необходимо указать название единицы измерения с помощью параметра unit:

const value = 85; const en = new Intl.NumberFormat("en", {style: "unit", unit: "liter"}).format(value); const ru = new Intl.NumberFormat("ru", {style: "unit", unit: "liter"}).format(value); const zh = new Intl.NumberFormat("zh", {style: "unit", unit: "liter"}).format(value); console.log(en); // 85 L console.log(ru); // 85 л console.log(zh); // 85升

По умолчанию применяет сокращенная форма наименования валюты. С помощью значения unitDisplay: "long" можно задать вывод полного наименования:

const value = 85; const longLiter = new Intl.NumberFormat("ru", { style: "unit", unit: "liter", unitDisplay: "long" }).format(value); const shortLiter = new Intl.NumberFormat("ru", { style: "unit", unit: "liter", unitDisplay: "short" }).format(value); console.log(longLiter); // 85 литров console.log(shortLiter); // 85 л

Еще несколько примеров с форматированием разных единиц измерения:

const value = 85; const kilobyte = new Intl.NumberFormat("ru", { style: "unit", unit: "kilobyte", unitDisplay: "long" }).format(value); const meter = new Intl.NumberFormat("ru", { style: "unit", unit: "meter", unitDisplay: "long" }).format(value); const gram = new Intl.NumberFormat("ru", { style: "unit", unit: "gram", unitDisplay: "long" }).format(value); console.log(kilobyte); // 85 килобайт console.log(meter); // 85 метров console.log(gram); // 85 грамм

Метод toLocaleString типа Number

Стоит отметить, что у типа Number есть метод toLocaleString(), который принимает локаль и который локализует в эту локаль число:

const num = 1007.56; console.log(num.toLocaleString("en")); // 1,007.56 console.log(num.toLocaleString("de")); // 1.007,56 console.log(num.toLocaleString("ru")); // 1 007,56

📜 Intl.ListFormat — Форматирование списков

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

Intl.ListFormat([locales[, options]])

Параметр locales представляет код языка в формате BCP 47 или набор языковых кодов.

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

  • localeMatcher: алгоритм поиска соответствий. Может принимать два значения: "lookup" и "best fit". Значение по умолчанию - "best fit".
  • style: длина форматируемой строки. Возможные значения: "long" (например, A, B, and C), "short" или "narrow" (например, A, B,C). Значение по умолчанию - "long"
  • type: формат выходной строки. Возможные значения: "conjunction" (предпоследний и последний элементы в списке соединяются союзом "и" ("and") - A, B и C), "disjunction" (предпоследний и последний элементы в списке соединяются союзом "или" ("or") - A, B или C), "unit" (применяется для списков с числовыми значениями и добавляет к ним единицы измерения). Значение по умолчанию - "conjunction"

Для форматирования списка данный объект предоставляет метод format(), в который передается форматируемый список. Метод возвращает отформатированный локализованный список в виде строки.

Рассмотрим несколько примеров. Добавим союз "И":

const people = ["Tom", "Bob", "Sam"]; const andList = new Intl.ListFormat("ru").format(people); // { style:"long", type: "conjunction" }, console.log(andList); // Tom, Bob и Sam

В данном случае используется локализация с учетом русскоязычной культуры и для этого в качестве параметра locales в конструктор Intl.ListFormat передаем код языка ru. В итоге из массива ["Tom", "Bob", "Sam"] получаем строку Tom, Bob и Sam. В данном случае для второго параметра - options использовались настройки по умолчанию. Однако мы можем их задать и явным образом:

const people = ["Tom", "Bob", "Sam"]; const andList = new Intl.ListFormat("ru" , { style:"long", type: "conjunction" }).format(people); console.log(andList); // Tom, Bob и Sam

В этом случае мы получим тот же самый результат, так как опция type: "conjunction" предполагает использование союза "и".

Теперь используем союз "или":

const people = ["Tom", "Bob", "Sam"]; const orList = new Intl.ListFormat("ru", { style:"short", type: "disjunction" }).format(people); console.log(orList);// Tom, Bob или Sam

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

const people = ["Tom", "Bob", "Sam"]; // английский язык const enList = new Intl.ListFormat("en" , { style:"long", type: "conjunction" }).format(people); console.log(enList); // Tom, Bob, and Sam // немецкий язык const deList = new Intl.ListFormat("de" , { style:"long", type: "conjunction" }).format(people); console.log(deList); // Tom, Bob und Sam // французский язык const frList = new Intl.ListFormat("fr" , { style:"long", type: "conjunction" }).format(people); console.log(frList); // Tom, Bob et Sam // китайский язык const zhList = new Intl.ListFormat("zh" , { style:"long", type: "conjunction" }).format(people); console.log(zhList); // Tom、Bob和Sam

Еще пример:

const items = ["яблоки", "бананы", "апельсины"]; // Русский (и) const ruList = new Intl.ListFormat("ru").format(items); console.log(ruList); // яблоки, бананы и апельсины // Английский (and) const enList = new Intl.ListFormat("en").format(items); console.log(enList); // яблоки, бананы, and апельсины // Разные стили const longList = new Intl.ListFormat("ru", { style: "long", type: "conjunction" // "и" }).format(items); console.log(longList); // яблоки, бананы и апельсины const orList = new Intl.ListFormat("ru", { type: "disjunction" // "или" }).format(items); console.log(orList); // яблоки, бананы или апельсины

📜 Intl.DisplayNames — Локализация названий

Объект Intl.DisplayNames позволяет локализовать названия, в частности, названия стран и регионов, языков, некоторых других наименований. Его конструктор может принимать два параметра:

Intl.DisplayNames([locales[, options]])

Параметр locales представляет код языка в формате BCP 47 или набор языковых кодов.

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

  • localeMatcher: алгоритм поиска соответствий. Может принимать два значения: "lookup" и "best fit". Значение по умолчанию - "best fit".
  • style: длина форматируемой строки. Возможные значения: "long", "short" и "narrow" (например, A, B,C). Значение по умолчанию - "long"
  • type: тип названий, которые будут локализованы. Возможные значения:
    • "language": возвращает название языка
    • "region": возвращает название страны/региона
    • "script": возвращает название письменного скрипта
    • "currency": возвращает название валюты
  • fallback: задает альтернативнй вариант. Возможные значения: "code" и "none". "code" задает код, который определяет локализуемое название.

Какое именно название будет локализоваться, задается с помощью метода of(). В этот метод передается код названия. Метод возвращает локализованное наименование.

Рассмотрим несколько примеров:

const USAInEnglish = new Intl.DisplayNames("en", {type: "region"}).of("US"); const USAInRussian = new Intl.DisplayNames("ru", {type: "region"}).of("US"); console.log(USAInEnglish); // United States console.log(USAInRussian); // Соединенные Штаты

Здесь получаем два названия для одной и той же страны - США. Для константы USAInEnglish применяет код культуры "en", то есть мы будем получать название на английском. Параметр type: "region" указывает, что мы хотим получить название страны.региона. Далее у соданного объекта вызываем метод of(), в который передается значение "US" - это код, который означает, что мы хотим получить название для USA. В итоге мы получим название USA на английском языке - "United States".

А константа USAInRussian также получает название страны по коду "US", только на русском языке.

Названия региона

Для получения названия региона (это может быть название страны или название географического региона) применяется параметр type: "region". Для получения названия региона в метод of() передается код региона. В качестве кода региона может выступать код ISO-3166 из двух букв (например, US, RU, DE и т.д.) или трехчисловой код UN M49.

Например, используем код "DE" для получения названия Германии на разных языках:

const GermanyInEnglish = new Intl.DisplayNames("en", {type: "region"}).of("DE"); const GermanyInRussian = new Intl.DisplayNames("ru", {type: "region"}).of("DE"); const GermanyInGerman = new Intl.DisplayNames("de", {type: "region"}).of("DE"); console.log(GermanyInEnglish); // Germany console.log(GermanyInRussian); // Германия console.log(GermanyInGerman); // Deutschland

Другой пример:

const regions = new Intl.DisplayNames(["ru"], { type: "region" }); console.log(regions.of("US")); // США console.log(regions.of("DE")); // Германия console.log(regions.of("FR")); // Франция console.log(regions.of("CN")); // Китай // На английском const regionsEN = new Intl.DisplayNames(["en"], { type: "region" }); console.log(regionsEN.of("RU")); // Russia

Получение название письменности

Для получения названия письменного скрипта применяется значение type: "script". Для получения названия письменности в метод of() передается четырехбуквенный код ISO-15924. Например:

const CyrlInEnglish = new Intl.DisplayNames("en", {type: "script"}).of("Cyrl"); const CyrlInRussian = new Intl.DisplayNames("ru", {type: "script"}).of("Cyrl"); console.log(CyrlInEnglish); // Cyrillic console.log(CyrlInRussian); // кириллица

Названия языков

Для получения названия языка применяется значение type: "language", а в метод of() передается код языка в формате languageCode[-scriptCode][-regionCode](-variant), где компонент languageCode представляет двухбуквенный код языка в формате ISO 639-1 или трехбуквенный код в формате ISO 639-2. Необязательные компоненты scriptCode и regionCode - выше рассмотренные коды письменности и региона соответственно.

Например:

const enRussian = new Intl.DisplayNames("en", {type: "language"}).of("ru"); const ruRussian = new Intl.DisplayNames("ru", {type: "language"}).of("ru"); const deRussian = new Intl.DisplayNames("de", {type: "language"}).of("ru"); console.log(enRussian); // Russian console.log(ruRussian); // русский console.log(deRussian); // Russisch

Используем код языка вместе с кодом региона:

const ruLang = new Intl.DisplayNames("ru", {type: "language"}); const enUS = ruLang.of("en-US"); const enGB = ruLang.of("en-GB"); console.log(enUS); // американский английский console.log(enGB); // британский английский

Еще прмер:

const languages = new Intl.DisplayNames(["ru"], { type: "language" }); console.log(languages.of("en")); // английский console.log(languages.of("de")); // немецкий console.log(languages.of("fr")); // французский console.log(languages.of("ja")); // японский

Названия валют

Для получения названия валюты применяется значение type: "currency", а в метод of() передается трехбуквенный код ISO 4217. Например:

const ruLang = new Intl.DisplayNames("ru", {type: "currency"}); const usd = ruLang.of("USD"); const euro = ruLang.of("EUR"); const ruble = ruLang.of("RUB"); console.log(usd); // доллар США console.log(euro); // евро console.log(ruble); // российский рубль

Или так:

const currencies = new Intl.DisplayNames(["ru"], { type: "currency" }); console.log(currencies.of("USD")); // доллар США console.log(currencies.of("EUR")); // евро console.log(currencies.of("RUB")); // российский рубль console.log(currencies.of("GBP")); // фунт стерлингов

📜 Intl.Collator — Сравнение и сортировка строк

Для сравнения строк с учетом языка применяется тип Collator. Для этого тип Collator предоставляет метод compare(), который в качестве параметров принимает две строки:

collator.compare(string1, string2)

Метод compare возвращает одно из трех числовых значений:

  • 1, если первая строка "больше", чем вторая (первая строка расположена после второй строки в лексикографическом порядке).
  • -1, если вторая строка "больше" первой (первая строка располагается до второй строки)
  • 0, если обе строки равны

Пример сравнения:

const collator = new Intl.Collator("ru-RU"); console.log(collator.compare("б", "А")); // 1 console.log(collator.compare("б", "Б")); // -1 console.log(collator.compare("б", "В")); // -1 console.log(collator.compare("б", "б")); // 0 console.log(collator.compare("мир", "миг")); // 1 console.log(collator.compare("мир", "миф")); // -1 console.log(collator.compare("мир", "мир")); // 0

Настройка сравнения

Принцип сравнения строк определяется объектом конфигурации, который передается в конструктор Int.Colator в качестве второго параметра и который определяет следующие свойства:

  • localeMatcher: представляет используемый алгоритм выбора локали. Может принимать два значения: "lookup" и "best fit" (значение по умолчанию). Значение lookup предполагает использование алгоритма, определенного в стандарте. Значение best fit использует локаль, которую предоставляет среда выполнения
  • usage: указывает, будет ли сравнение строк использоваться для сортировки или для поиска строк. Возможные значения — сортировка и поиск соответственно. Возможные значения: "sort" (для поиска) и "search" (для сортировки - значение по умолчанию)
  • sensitivity: указывает, какие символы следует считать неравными. Возможные значения:
    • "base": строки считаются неравными, если они различаются по базовым буквам. Например: a ≠ b, a = á, a = A.
    • "accent": строки считаются неравными, если они различаются по базовым буквам или диакритическим знакам. Например: a ≠ b, a ≠ á, a = A.
    • "case": строки считаются неравными, если они различаются по базовым буквам или регистру. Например: a ≠ b, a = á, a ≠ A.
    • "variant": строки считаются неравными, если они различаются по базовым буквам, регистру или диакритическим знакам. Например: a ≠ b, a ≠ á, a ≠ A. Значение по умолчанию
  • ignorePunctuation: надо ли игнорировать пунктуацию. Принимает значения true (пунктуация игнорируется) и false (не игнорируется, значение по умолчанию)
  • numeric: указывает, надо ли сравнивать строки как числа (например, "1" < "5" < "10"). Принимает значения true и false (значение по умолчанию)
  • caseFirst: указывает, надо ли учитывать регистр первой буквы. Принимает значения true и false (значение по умолчанию)

Например, не будем учитывать регистр при сравнении:

const standardCollator = new Intl.Collator("ru-RU"); console.log(standardCollator.compare("Дом", "дом")); // 1 - строки не равны const baseCollator = new Intl.Collator("ru-RU", {sensitivity: "base"}); console.log(baseCollator.compare("Дом", "дом")); // 0 - строки равны

Сортировка строк

Метод compare() удобно использовать для сортировки массива строк. В частности, метод sort() у объекта массива принимает функцию сортировки или сравнения, которая затем сравнивает элементы соответствующего массива попарно и, таким образом, выполняет сортировку:

const collator = new Intl.Collator("en", {usage: "sort"}); const people = ["Tom", "Bob", "Sam", "Alice", "Kate"]; people.sort(collator.compare); // сортируем массив console.log(people); // ["Alice", "Bob", "Kate", "Sam", "Tom"]

Проблема обычной сортировки

const names = ["Яна", "Алексей", "Ёлка", "Борис", "Виктор"]; // Обычная сортировка (неправильно для русского) console.log(names.sort()); // ["Алексей", "Борис", "Виктор", "Яна", "Ёлка"] - Ё в конце! // С учётом локали (правильно) console.log(names.sort(new Intl.Collator("ru").compare)); // ["Алексей", "Борис", "Виктор", "Ёлка", "Яна"] - Ё после Е!

Сравнение строк

const collator = new Intl.Collator("ru"); // Сравнение (возвращает -1, 0 или 1) console.log(collator.compare("яблоко", "апельсин")); // 1 (я > а) console.log(collator.compare("ёлка", "дом")); // 1 (ё > д) console.log(collator.compare("кот", "кот")); // 0 (равны) // Сортировка массива const words = ["яблоко", "ёлка", "дом", "апельсин"]; console.log(words.sort(collator.compare)); // ["апельсин", "дом", "ёлка", "яблоко"]

📜 Intl.RelativeTimeFormat — Относительное время

const rtf = new Intl.RelativeTimeFormat("ru", { numeric: "auto" }); console.log(rtf.format(-1, "day")); // вчера console.log(rtf.format(0, "day")); // сегодня console.log(rtf.format(1, "day")); // завтра console.log(rtf.format(2, "day")); // через 2 дня console.log(rtf.format(-2, "hour")); // 2 часа назад console.log(rtf.format(3, "week")); // через 3 недели console.log(rtf.format(-1, "month")); // в прошлом месяце // Английский const rtfEN = new Intl.RelativeTimeFormat("en", { numeric: "auto" }); console.log(rtfEN.format(-1, "day")); // yesterday console.log(rtfEN.format(1, "day")); // tomorrow

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

Пример 1: Мультиязычный интерфейс

class i18n { constructor(locale) { this.locale = locale; this.dateFormatter = new Intl.DateTimeFormat(locale, { dateStyle: "long" }); this.numberFormatter = new Intl.NumberFormat(locale); this.currencyFormatter = new Intl.NumberFormat(locale, { style: "currency", currency: locale === "ru" ? "RUB" : "USD" }); } formatDate(date) { return this.dateFormatter.format(date); } formatNumber(number) { return this.numberFormatter.format(number); } formatCurrency(amount) { return this.currencyFormatter.format(amount); } } // Использование const ru = new i18n("ru"); const en = new i18n("en-US"); const now = new Date(); console.log(ru.formatDate(now)); // 15 ноября 2025 г. console.log(en.formatDate(now)); // November 15, 2025 console.log(ru.formatNumber(1234567)); // 1 234 567 console.log(en.formatNumber(1234567)); // 1,234,567 console.log(ru.formatCurrency(100)); // 100,00 ₽ console.log(en.formatCurrency(100)); // $100.00

Пример 2: Относительное время для постов

function getRelativeTime(date, locale = "ru") { const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" }); const now = new Date(); const diff = date - now; const seconds = Math.floor(diff / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); if (Math.abs(days) > 0) { return rtf.format(days, "day"); } else if (Math.abs(hours) > 0) { return rtf.format(hours, "hour"); } else if (Math.abs(minutes) > 0) { return rtf.format(minutes, "minute"); } else { return rtf.format(seconds, "second"); } } // Использование const postDate = new Date(Date.now() - 2 * 60 * 60 * 1000); // 2 часа назад console.log(getRelativeTime(postDate)); // 2 часа назад

📜 Заключение

📚 Основные выводы
  • Intl API — встроенный инструмент для локализации
  • Локали в формате BCP 47: "ru-RU", "en-US", "de-DE"
  • DateTimeFormat — даты и время с учётом региона
  • NumberFormat — числа, валюты, проценты, единицы
  • ListFormat — списки ("и", "или", "and", "or")
  • DisplayNames — названия стран, языков, валют
  • Collator — правильная сортировка с учётом языка
  • RelativeTimeFormat — "вчера", "2 часа назад"

Глава 22. Модули (ES6 Modules)

📜 Введение в модули

Что такое модули?

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

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

💡 Зачем нужны модули?
  • Изоляция — каждый модуль имеет свою область видимости
  • Переиспользование — один модуль можно использовать в разных проектах
  • Организация — структурирование большого кода
  • Зависимости — явное указание зависимостей между файлами
  • Инкапсуляция — скрытие внутренней реализации

Отличие модулей от обычных скриптов:

  • Для загрузки модулей применяется политика CORS. Это значит, что мы не можем просто кинуть в браузер html-страницу, которая подключает модуль. Модуль загружается с использованием протокола http/https. То есть страница html, которая загружает модуль, должна располагаться на каком-нибудь веб-сервере.
  • Модули всегда выполняются в режиме strict mode.
  • Модули по умолчанию загружаются асинхронно.
  • Модули загружаются и выполняются только один раз.
  • Модули позволяют использовать выражения await верхнего уровня без определения и вызова асинхронной функции.
  • Модули могут имортировать функционал из других модулей и, в свою очередь, экспортировать свою функциональность в другие модули.
  • Модули выполняются не в глобальном контексте, а в своей собственной области видимости. То есть переменные, константы, функции, классы и т.д., определенные внутри модуля, не доступны извне, пока они не будут явным образом экспортированы. А чтобы другой модуль мог их использовать, он должен их импортировать.

Особенности модулей

  • Строгий режим (strict mode) — модули автоматически выполняются в строгом режиме
  • Собственная область видимости — переменные модуля не попадают в global scope
  • this = undefined — в модуле верхнего уровня this равен undefined
  • Отложенное выполнение — модули выполняются с атрибутом defer
  • Единственное выполнение — модуль выполняется только один раз при первом импорте

Если файл содержит выражения import или export, он рассматривается как модуль. Так, Чтобы сделать из простого скрипта модуль, достаточно добавить в файл:

export {};

📜 Подключение модулей в HTML

<!-- Обычный скрипт --> <script src="script.js"></script> <!-- Модуль --> <script type="module" src="main.js"></script> <!-- Inline модуль --> <script type="module"> import { hello } from './message.js'; console.log(hello); </script>
⚠️ Важно про type="module"
  • Атрибут type="module" обязателен для модулей
  • Модули загружаются асинхронно (как с defer)
  • Модули требуют HTTP-сервер (не работают через file://)
  • CORS применяется для внешних модулей

Определение модуля. Экспорт.

Определим простейший модуль. Для этого создадим файл message.js, в котором определим следующий код:

export function sayHello() { console.log("Hello METANIT.COM"); }

Здесь определена обычная функция sayHello(), которая выводит некоторое сообщение на консоль. Но она определена с ключевым словом export, а это значит, что данный файл представляет модуль, а функцию sayHello() можно импортировать в другие модули.

Подключение модуля. Импорт

Теперь подключим эту функцию в другой файл. Для этого возьмем файл main.js:

import {sayHello} from "./message.js"; sayHello();

Для подключения функционала из другого модуля применяется ключевое слово import, после которого идут названия подключаемых компонентов. Все подключаемые из модуля компоненты помещаются в фигурные скобки: import {sayHello} - в данном случае подключается функция sayHello.

Затем после оператора from указывается модуль, из которого идет импорт. В данном случае указываем "./message.js". В данном случае предполагается что оба модуля - main.js и message.js будут находиться в одной папке.

Загрузка модулей

Для загрузки модулей определим в папке со скомпилированными файлами веб-страницу index.html:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <script type="module" src="main.js"></script> </body> </html>

Для загрузки главного модуля приложения - main.js определяется элемент <script>, у которого устанавливается атрибут type="module".

📜 Создание и запуск сервера

Загрузка модулей производится через AJAX, поэтому скомпилированные модули должны быть размещены на веб-сервере. То есть у нас не получится просто кинуть страницу в веб-браузер и загрузить на нее модули. Такая веб-страница должна быть размещена на веб-сервере. Поэтому прежде всего надо определиться с веб-сервером. Веб-сервер может быть любым. В данном случае воспользуемся самым простым вариантом - Node.js. Но опять же вместо node.js это может быть любая другая технология сервера - php, asp.net, python и т.д. либо какой-то определенный веб-сервер типа Apache или IIS.

Итак, создадим в папке с файлами модулей файл сервера. Пусть он будет называться server.js и будет иметь следующий код:

const http = require("http"); const fs = require("fs"); http.createServer(function(request, response){ // получаем путь после слеша let filePath = request.url.substring(1); if(filePath == "") filePath = "index.html"; fs.readFile(filePath, function(error, data){ if(error){ response.statusCode = 404; response.end("Resourse not found!"); } else{ if(filePath.endsWith(".js")) response.setHeader("Content-Type", "text/javascript"); response.end(data); } }); }).listen(3000, function(){ console.log("Server started at 3000"); });

Это самый примитивный сервер, который отдает пользователю статические файлы. Для создания сервера применяется функция http.createServer(), а для считывания и отправки файлов - функция fs.readFile(). Если имя файла не указано, то отправляется файл index.html. Сервер будет запускаться по адресу http://localhost:3000/

Стоит отметить, что при отправке модулей js нам надо устанавливать mime-тип отправляемого контента в "text/javascript":

if(filePath.endsWith(".js")) response.setHeader("Content-Type", "text/javascript");

Теперь запустим сервер с помощью команды

node server.js

После запуска сервера мы можем перейти в браузере по адресу http://localhost:3000, нам отобразится страница, а в консоли браузера мы сможем увидеть результат работы модуля main.js.

📜 Импорт модуля

Для импорта одного модуля в другом модуле достаточно прописать оператор import и передать ему путь к импортируемому модулю:

import "путь_к_модулю";

Например, пусть у нас будет следующий модуль message.js

const messageText = "Hello METANIT.COM"; console.log(messageText);

Фактически этот модуль выглядит как обычный скрипт, который определяет переменную и выводит ее значение на консоль с помощью функции console.log().

И определим в той же папке файл main.js, в котором подключим выше определенный модуль message.js:

import "./message.js";

Здесь мы просто испортируем модуль message.js. В данном случае предполагается что оба модуля - main.js и message.js будут находиться в одной папке, поэтому при импорте указан путь "./message.js", где "./" указывает на ту же папку, где расположен файл main.js.

Пусть у нас есть html-страница index.html, на которой подключается файл main.js:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <script type="module" src="main.js"></script> </body> </html>

В итоге при выполнении страницы в браузере в его консоли мы увидим результат работы компонентов подключенного модуля:

Hello METANIT.COM

При таком подключении будут выполняться все вызовы функций модуля message.js, однако все определенные и не экспортируемые компоненты в этом модуле из вне будут недоступны. Так, мы не можем в модуле main.js написать так:

import "./message.js"; console.log(messageText); // Ошибка ReferenceError: messageText is not defined

Хотя мы и подключили модуль message.js, но его переменная messageText нам недоступна. Так как она не экспортируется, и соотвестветвенно здесь мы ее не можем импортировать и использовать.

Такой способ импорта может показаться бессмысленным. Тем не менее мы можем воспользоваться преимуществами модуля, например, возможностью использовать выражения await без определения асинхронных функций. Например, изменим модуль message.js следующим образом:

const sum = (x, y)=>Promise.resolve(x + y); const value = await sum(5, 3); console.log("Результат асинхронной операции:", value);

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

📜 Export — Экспорт

1. Именованный экспорт (Named Export)

Для того, чтобы какие-то компоненты модуля (переменные/константы/функции/классы) модуля можно было подключить и использовать в другом модулей, их надо экспортировать. Мы можем экспортировать какждый компонент по отдельности. Для этого перед определением компонента указывается ключевое слово export. Например, пусть у нас будет следующий модуль message.js

Экспортируем отдельные компоненты с помощью export:

// message.js // Экспорт переменной export let welcome = "Welcome"; // Экспорт константы export const hello = "Hello"; // Экспорт функции export function sayHello() { console.log("Hello METANIT.COM"); } // Экспорт класса export class Messenger { send(text) { console.log("Sending message:", text); } }

Здесь экспортируются переменная welcome, константа hello, функция sayHello() и класс Messenger. Стоит отметить, что нам необзательно экспортировать все компоненты модуля, какие-то компоненты мы можем не экспортировать и использовать только внутри этого модуля.

2. Экспорт списком

Альтернативный способ — экспорт в конце файла:

// message.js let welcome = "Welcome"; const hello = "Hello"; function sayHello() { console.log("Hello METANIT.COM"); } class Messenger { send(text) { console.log("Sending message:", text); } } // Экспортируем всё сразу export { welcome, hello, sayHello, Messenger };

3. Экспорт с переименованием (as)

// message.js function sayHello() { console.log("Hello!"); } function sayGoodbye() { console.log("Goodbye!"); } // Экспортируем с другими именами export { sayHello as greet, sayGoodbye as bye };

📜 Import — Импорт

1. Именованный импорт

Все имортируемые компоненты мы можем подключить по отдельности. Для этого после оператора import в фигурных скобках указываются названия подключаемых компонентов- переменных/констант/функций/классов. Затем после оператора from указывается модуль, из которого идет импорт.

import {компонент1, компонент2, ... компонентN} from "путь_к_модулю";

Например, импортируем в модуле main.js экспортируемые компоненты модуля message.js:

import {sayHello, welcome, Messenger} from "./message.js"; sayHello(); const telegram = new Messenger(); telegram.send(welcome);

Итак, здесь подключаются из модуля message.js переменная welcome, функция sayHello() и класс Messenger. При этом нам необязательно подключать все компоненты модуля. Мы можем подключить только те компоненты, которые нам непосредственно нужны и которые мы собираемся использовать.

В итоге при выполнении страницы в браузере в его консоли мы увидим результат работы компонентов подключенного модуля main.js:

Hello METANIT.COM
Sending message: Welcome

Или так:

// main.js // Импортируем конкретные компоненты import { hello, sayHello, Messenger } from './message.js'; console.log(hello); // "Hello" sayHello(); // "Hello METANIT.COM" const telegram = new Messenger(); telegram.send("Hi!"); // "Sending message: Hi!"

2. Импорт всего модуля (* as)

Если в подключаемом модуля очень много компонентов, и мы собираемся использовать все возможности модуля, то перечислять все подключаемые компоненты может быть утомительно. В этом случае мы можем подключить весь функционал модуля в виде:

import * as псевдоним_модуля from "модуль";

После оператора import идет знак звездочки, который указывает, что надо подключить все экспортируемые компоненты. А после оператора as идет псевдоним модуля, с которым будет сопоставляться подключаемый модуль.

Например, подключим в файле main.js весь модуль message.js

// main.js // Импортируем всё под одним именем import * as MessageModule from './message.js'; console.log(MessageModule.hello); MessageModule.sayHello(); const messenger = new MessageModule.Messenger(); messenger.send("Hello");

В даном случае подключаемый модуль message.js сопоставляется с идентификатором MessageModule. В качестве псевдонима модуля может выстуать произвольное название. И далее мы можем обращаться ко всем экспортируемым компонентам модуля через псевдним модуля, например, обращении к функции sayHello: MessageModule.sayHello()

3. Импорт с переименованием (as)

// main.js // Импортируем с другим именем import { sayHello as greet, hello as greeting } from './message.js'; console.log(greeting); // "Hello" greet(); // "Hello METANIT.COM"

4. Простой импорт (без привязки)

// message.js console.log("Module loaded!"); // main.js import './message.js'; // Просто выполнит код модуля // Выведет: "Module loaded!"
💡 Когда использовать простой импорт

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

📜 Export Default — Экспорт по умолчанию

При экспорте мы можем указать компонент, который будет экспортироваться по умолчанию с помощью оператора default. Например, определим следующий модуль message.js:

export default function sayHello() { console.log("Hello from sayHello function"); }

Чтобы сделать экспорт по умолчанию, после оператора export указывается оператор default.

Теперь импортируем эту функцию в модуле main.js:

import sayHello from "./message.js"; sayHello();

Для импорта компонента по умолчанию достаточно после оператора import прописать имя этого компонента.

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

В модуле может быть один экспорт по умолчанию. Импортируется без фигурных скобок.

1. Функция по умолчанию

// message.js export default function sayHello() { console.log("Hello from sayHello function"); } // main.js // Импортируем без фигурных скобок import sayHello from './message.js'; // Можем назвать как угодно import greet from './message.js'; sayHello(); // "Hello from sayHello function" greet(); // "Hello from sayHello function"

2. Класс по умолчанию

// User.js export default class User { constructor(name) { this.name = name; } greet() { console.log(`Hello, ${this.name}!`); } } // main.js import User from './User.js'; const user = new User('Alice'); user.greet(); // "Hello, Alice!"

3. Объект по умолчанию

// config.js export default { apiUrl: 'https://api.example.com', timeout: 5000, debug: true }; // main.js import config from './config.js'; console.log(config.apiUrl); // "https://api.example.com" console.log(config.timeout); // 5000

4. Комбинирование default и named exports

Модуль может одновременно экспортировать отдельные компоненты и компонент по умолчанию:

// message.js export const hello = "Hello"; export const welcome = "Welcome"; export default function sayHello() { console.log("Hello METANIT.COM"); } export class Messenger { send(text){ console.log("Sending message:", text); } } // main.js // Импорт default и named одновременно import sayHello, { hello, welcome, Messenger } from './message.js'; sayHello(); // "Hello METANIT.COM" console.log(hello); // "Hello"

Здесь экспортируются все компоненты, однако только функция sayHello экспортируется по умолчанию. Это определение модуля также эквивалентно следующему определению, где компоненты экспортируются через список:

let welcome = "Welcome"; const hello = "Hello"; function sayHello() { console.log("Hello METANIT.COM"); } class Messenger { send(text){ console.log("Sending message:", text); } } export {welcome, hello, sayHello as default, Messenger}

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

import sayHello, {welcome, Messenger} from "./message.js"; sayHello(); const telegram = new Messenger(); telegram.send(welcome);

Экспорт и импорт по умолчанию всего модуля

Стоит учитывать, что при экспорте по умолчанию мы можем только один раз использовать оператор default. Мы не можем по отдельности экспортировать по умолчанию сразу два компонента. Однако мы можем экспортировать по умолчанию сразу набор компонентов как единое целое. Например, определим следующий модуль message.js:

let welcome = "Welcome"; const hello = "Hello"; function sayHello() { console.log("Hello METANIT.COM"); } class Messenger { send(text){ console.log("Sending message:", text); } } export default {welcome, hello, sayHello, Messenger}

Теперь импортируем функционал модуля message.js в модуле main.js:

import MessageModule from "./message.js"; MessageModule.sayHello(); const telegram = new MessageModule.Messenger(); telegram.send(MessageModule.welcome);

В данном случае весь экспортированный список компонентов будет сопоставляться с названием MessageModule, которое фактически будет представлять модуль. И далее через этот идентификатор можно обратиться к конкреному компоненту, используя имя компонента: MessageModule.sayHello();

📜 Реэкспорт (Re-export)

Можно экспортировать импортированные компоненты из другого модуля:

// user.js export class User { constructor(name) { this.name = name; } } // admin.js export class Admin extends User { constructor(name, role) { super(name); this.role = role; } } // index.js (barrel export) // Реэкспорт из других модулей export { User } from './user.js'; export { Admin } from './admin.js'; // Или всё сразу export * from './user.js'; export * from './admin.js'; // main.js // Импортируем всё из одного места import { User, Admin } from './index.js'; const user = new User('Alice'); const admin = new Admin('Bob', 'Moderator');

📜 Использование псевдонимов при экспорте и импорте

С помощью оператора as экспортируемым/импортируемым компонентам модуля можно назначить псевдоним. Затем для использования подобных компонентов применяется не их непосредственное имя, а их псевдоним.

Псевдонимы в экспорте

Определим следующий модуль message.js:

let welcome = "Welcome"; const hello = "Hello"; function sayHello() { console.log("Hello METANIT.COM"); } class Messenger { send(text){ console.log("Sending message:", text); } } export {welcome as simpleMessage, hello, sayHello as printMessage, Messenger}

Здесь все компоненты модуля экспортируются в виде списка, в котором можно определить для компонента псевдним в виде:

компонент as псевдним

Так, для константы welcome определен псевдоним simpleMessage, а для функции sayHello определен псевдоним printMessage().

В этом случае при импорте модуля message.js данные компоненты будут доступны через свои псевднимы:

import {simpleMessage, printMessage, Messenger} from "./message.js"; printMessage(); const telegram = new Messenger(); telegram.send(simpleMessage);

Псевдонимы при импорте

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

Рассмотрим небольшой пример:

import {simpleMessage as messageText, printMessage as printHello, Messenger} from "./message.js"; const printMessage = ()=>console.log("Hello from main module"); printHello(); printMessage(); const telegram = new Messenger(); telegram.send(messageText);

Здесь в модуле импортируемой константе simpleMessage назначается псевдним messageText: simpleMessage as messageText

Кроме того, здесь определена функция printMessage(). Однако из модуля также импортируется компонент с таким же именем. И чтобы избежать двойственности, импортируемому компоненту назначается псевдним printHello: printMessage as printHello

Далее для обращения к импортированным компонентам с псевднимами используются их псевдонимы.

📜 Динамический импорт (Dynamic Import)

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

import("путь_к_модулю").then((module) =&ggt; { // действия с модулем });

Также можно использовать оператор await для получения объекта модуля:

let module = await import("путь_к_модулю");

Например, пусть у нас определен следующий модуль message.js:

export const hello = "Hello Work!"; export default function sayHello() { console.log("Hello METANIT.COM"); }

Здесь экспортируются константа hello и функция sayHello(), причем функция экспортируется по умолчанию.

Динамически подключим этот модуль в другой модуль main.js:

console.log("Main module starts"); import("./message.js").then((module) => { module.default(); console.log(module.hello); }); console.log("Main module ends");

Здесь функция в методе then() в качестве параметра получает загруженный модуль. Далее мы можем обращаться к компонентам модуля по имени. Например, обращение к константе hello

module.hello

Однако если какой-то компонент экспортируется по умолчанию, то для обращения к нему применяется ключевое слово default. Так, поскольку функция sayHello() экспортируется по умолчанию, то выражение:

module.default();

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

Консольный вывод программы:

Main module starts
Main module ends
Hello METANIT.COM
Hello Work!

Также в данном случае можно было бы применить оператор await для получения загруженного модуля:

console.log("Main module starts"); const module = await import("./message.js"); module.default(); console.log(module.hello); console.log("Main module ends");

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

const hour = new Date().getHours(); if(hour < 17){ const module = await import("./message.js"); console.log(module.hello); } else{ console.log("Go home"); }

В данном случае получаем текущий час. И если он меньше 17, то загружаем модуль и выводим значение константы hello из загруженного модуля. Иначе выводим некоторую строку.

Импорт модулей в любом месте кода с помощью import() как функции:

Базовое использование

// Динамическая загрузка модуля import('./message.js') .then(module => { module.sayHello(); console.log(module.hello); }) .catch(error => { console.error('Ошибка загрузки модуля:', error); });

С async/await

async function loadModule() { try { const module = await import('./message.js'); module.sayHello(); console.log(module.hello); } catch (error) { console.error('Ошибка:', error); } } loadModule();

Условная загрузка

const userAge = 16; if (userAge < 18) { // Загружаем модуль только для несовершеннолетних const module = await import('./teen.js'); console.log(module.hello); } else { // Загружаем модуль для взрослых const module = await import('./adult.js'); console.log(module.hello); }

Загрузка по клику

button.addEventListener('click', async () => { // Загружаем модуль только когда нужно const module = await import('./heavy-feature.js'); module.initialize(); });
✅ Преимущества динамического импорта
  • Ленивая загрузка — загружаем модуль только когда нужно
  • Условная загрузка — выбираем модуль по условию
  • Разделение кода — уменьшаем начальный размер bundle
  • Асинхронность — не блокирует выполнение кода

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

Пример 1: Структура проекта с модулями

project/ ├── index.html ├── js/ │ ├── main.js │ ├── utils/ │ │ ├── math.js │ │ └── string.js │ ├── components/ │ │ ├── Button.js │ │ └── Modal.js │ └── api/ │ └── client.js // utils/math.js export function sum(a, b) { return a + b; } export function multiply(a, b) { return a * b; } // utils/string.js export function capitalize(str) { return str.charAt(0).toUpperCase() + str.slice(1); } // components/Button.js export default class Button { constructor(text) { this.text = text; } render() { const button = document.createElement('button'); button.textContent = this.text; return button; } } // main.js import { sum, multiply } from './utils/math.js'; import { capitalize } from './utils/string.js'; import Button from './components/Button.js'; console.log(sum(5, 3)); // 8 console.log(capitalize('hello')); // "Hello" const btn = new Button('Click me'); document.body.appendChild(btn.render());

Пример 2: API клиент

// api/client.js const API_URL = 'https://api.example.com'; export async function getUsers() { const response = await fetch(`${API_URL}/users`); return await response.json(); } export async function getUser(id) { const response = await fetch(`${API_URL}/users/${id}`); return await response.json(); } export async function createUser(userData) { const response = await fetch(`${API_URL}/users`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(userData) }); return await response.json(); } // main.js import { getUsers, createUser } from './api/client.js'; // Получаем пользователей const users = await getUsers(); console.log(users); // Создаём пользователя const newUser = await createUser({ name: 'Tom', age: 37 }); console.log(newUser);

Пример 3: Конфигурация приложения

// config/development.js export default { apiUrl: 'http://localhost:3000', debug: true, logLevel: 'verbose' }; // config/production.js export default { apiUrl: 'https://api.production.com', debug: false, logLevel: 'error' }; // config/index.js const env = process.env.NODE_ENV || 'development'; let config; if (env === 'production') { config = await import('./production.js'); } else { config = await import('./development.js'); } export default config.default; // main.js import config from './config/index.js'; console.log('API URL:', config.apiUrl); console.log('Debug:', config.debug);

📜 Модули vs Скрипты

Характеристика Обычные скрипты Модули (ES6)
Область видимости Глобальная Своя для каждого модуля
Strict mode Нужно указывать явно Всегда включён
this верхнего уровня window undefined
Загрузка Синхронная Асинхронная (defer)
Повторное выполнение Каждый раз Только один раз
import/export Нет Есть
HTTP-сервер Не обязателен Обязателен

📜 Лучшие практики

✅ Рекомендации
  • Один модуль — одна ответственность (Single Responsibility)
  • Явные импорты — импортируйте только то, что нужно
  • Barrel exports — используйте index.js для переэкспорта
  • Именование файлов — совпадает с export default (User.js → class User)
  • Динамические импорты — для больших/редко используемых модулей
  • Избегайте циклических зависимостей
  • Константы и конфигурацию выносите в отдельные модули

Структура модуля

// ✅ ХОРОШО: четкая структура // 1. Импорты import { helper } from './utils.js'; import config from './config.js'; // 2. Константы const API_URL = 'https://api.example.com'; // 3. Приватные функции (не экспортируются) function privateHelper() { // ... } // 4. Публичные функции (экспортируются) export function publicFunction() { // ... } // 5. Экспорт по умолчанию (если есть) export default class MainClass { // ... }

📜 Заключение

📚 Основные выводы
  • Модули — стандартный способ организации кода в современном JS
  • export/import — ключевые слова для работы с модулями
  • Named exports — экспорт нескольких компонентов
  • Default export — один главный экспорт модуля
  • Динамический импорт — import() для ленивой загрузки
  • type="module" — обязателен в HTML для модулей
  • Изоляция — каждый модуль имеет свою область видимости
  • Переиспользование — модули легко использовать в разных проектах

Глава 23. Canvas API

📜 Введение в Canvas

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

💡 Применение Canvas
  • Игры — 2D и простые 3D игры
  • Графики и диаграммы — визуализация данных
  • Анимации — интерактивные элементы
  • Редактирование изображений — фильтры, эффекты
  • Генеративное искусство — creative coding

📜 Создание Canvas

Пример простейшего определения элемента <canvas> на веб-странице:

<canvas id="myCanvas" width="500" height="300"> Ваш браузер не поддерживает Canvas </canvas>

Обычно для элемента canvas указывается идентификатор для упрощения его выборки в коде JavaScript. И также часто устанавливаюься атрибуты ширины и высоты (если опустить эти атрибуты, то по умолчанию canvas будет иметь ширину 300 пикселей и высоту 150 пикселей).

⚠️ Важно про размеры Canvas
  • Атрибуты width и height задают размер в пикселях
  • По умолчанию: 300×150 пикселей
  • Не используйте CSS для размеров — будет искажение!
  • Правильно: <canvas width="500" height="300">
  • Неправильно: <canvas style="width:500px; height:300px">

📜 Получение контекста рисования

Для управления областью рисования canvas и ее содержимым надо получить контекст рендеринга с помощью метода getContext() элемента canvas:

canvas.getContext(contextId, [config])

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

const canvas = document.getElementById('myCanvas'); const ctx = canvas.getContext('2d'); // Теперь можно рисовать ctx.fillStyle = 'blue'; ctx.fillRect(10, 10, 100, 50);

Типы контекстов

Мы можем использовать следующие идентификаторы контекста:

  • 2d: контекст для рендеринга 2D-графики. При передаче этого идентификатора в метод getContext() данный метод возвращает объект типа CanvasRenderingContext2D.
  • webgl: контекст для рендеринга 3D-графики с помощью технологии WebGL версия 1. При передаче этого идентификатора в метод getContext() данный метод возвращает объект типа WebGLRenderingContext.
  • webgl2: контекст для рендеринга 3D-графики с помощью технологии WebGL версия 2. При передаче этого идентификатора в метод getContext() данный метод возвращает объект типа WebGL2RenderingContext.

Стоит отметить, что хотя все современные браузеры более менее поддерживают все три контекста (особенно первые два контекста), но, например, поддержка последнего контекста webgl2 начала внедряться с 2017 года, а в Safari была внедрена самой последней - в 2021 году.

Например, получение контекста 2d для рисования 2D-графики:

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d");

📜 Система координат Canvas

Canvas использует 2D систему координат, где:

  • (0, 0) — верхний левый угол
  • X увеличивается вправо →
  • Y увеличивается вниз ↓
// Точка (0, 0) - верхний левый угол // Точка (500, 0) - верхний правый угол // Точка (0, 300) - нижний левый угол // Точка (500, 300) - нижний правый угол

📜 Рисование прямоугольников

Для рисования прямоугольников объект CanvasRenderingContext2D предоставляет ряд методов:

  • clearRect(x, y, w, h): очищает определенную прямоугольную область, верхний левый угол которой имеет координаты x и y, ширина равна w, а высота равна h
  • fillRect(x, y, w, h): заливает цветом прямоугольник, верхний леый угол которого имеет координаты x и y, ширина равна w, а высота равна h
  • strokeRect(x, y, w, h): рисует контур прямоугольника без заливки его каким-то определенным цветом

Заполненный прямоугольник

Например, нарисуем на веб-странице простейший прямоугольник с помощью fillRect():

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <canvas id="canvas" width="400" height="250"></canvas> <script> const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.fillRect (10, 10, 100, 100); </script> </body> </html>

Здесь заполняем прямоугольную область шириной в 100 пикселей и высотой также в 100 пикселей, левый верхний угол которой расположен в точке (x=10, y=10).

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

const canvas = document.getElementById('myCanvas'); const ctx = canvas.getContext('2d'); // Установка цвета заливки ctx.fillStyle = 'red'; // Рисуем прямоугольник // fillRect(x, y, width, height) ctx.fillRect(50, 50, 200, 100);

Контур прямоугольника

Метод fillRect() заполняет область без рисования границы, метод strokeRect, наоборот, рисует только границу. Например, изменим код javascript следующим образом:

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.strokeRect (10, 10, 100, 100);

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

Другой пример:

// Цвет линии ctx.strokeStyle = 'blue'; // Толщина линии ctx.lineWidth = 3; // Рисуем контур // strokeRect(x, y, width, height) ctx.strokeRect(300, 50, 150, 80);

Очистка области

В отличие от strokeRect и fillRect метод clearRect очищает определенную область, Фактически эта область приобретатет тот цвет, который у нее был бы, если бы к ней не применялись функции strokeRect и fillRect. Например:

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.fillRect (10, 10, 100, 100); context.clearRect(15, 15, 90, 90);

В данном случае сначала заливаем черным цветом прямоугольную область, затем внутри нее с помощью clearRect очищаем меньшую прямоугольную область:

// Очистка прямоугольной области // clearRect(x, y, width, height) ctx.clearRect(0, 0, canvas.width, canvas.height); // Очистка всего canvas

Более сложные композиции:

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.fillRect (10, 10, 80, 80); // Рисуем голову context.clearRect (20, 20, 60, 20); // Оцищаем место для глаз context.fillRect (30, 25, 10, 10); // Рисуем левый глаз context.fillRect (60, 25, 10, 10); // Рисуем правый глаз context.clearRect (25, 60, 50, 10); // Рисуем рот

📜 Настройка рисования

Контекст элемента canvas - объект CanvasRenderingContext2D предоставляет ряд свойств, с помощью которых можно настроить отрисовку на canvas. К подобным свойствам относятся следующие:

  • strokeStyle: устанавливает цвет линий или цвет контура. По умолчанию установлен черный цвет
  • fillStyle: устанавливает цвет заполнения фигур. По умолчанию установлен черный цвет
  • lineWidth: устанавливает толщину линий. По умолчанию равно 1.0
  • lineJoin: устанавливает стиль соединения линий
  • globalAlpha: устанавливает прозрачность отрисовки на canvas
  • setLineDash: создает линию из коротких черточек

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

  • В виде названия цвета, например, "red" или "green"
  • В виде шестнадцатеричного значения цвета, например, "#00FFFF"
  • В виде значения в формате rgb, например, "rgb(0, 0, 255)"
  • В виде значения в формате rgba, например, "rgba(0, 0, 255, 0.5)"

Например, установим цвет контура или границы фигур с помощью свойства strokeStyle:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <canvas id="canvas" width="400" height="250"></canvas> <script> const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.strokeStyle = "#ff0000"; // устанавливаем цвет контура фигуры context.strokeRect (10, 10, 100, 100); </script> </body> </html>

В данном случае в качестве цвета контура устанавливается красный цвет или "#ff0000":

Установим с помощью свойства fillStyle цвет заливки:

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.fillStyle = "#ee5253"; // устанавливаем цвет заполнения фигуры context.fillRect (10, 10, 100, 100);

Естественно мы можем комбинировать несколько методов:

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.fillStyle = "#c7ecee"; // устанавливаем цвет заполнения фигуры context.fillRect (10, 10, 100, 100); context.strokeStyle = "#22a6b3"; // устанавливаем цвет контура фигуры context.strokeRect (10, 10, 100, 100); context.fillRect (120, 10, 100, 100); // прямоугольник без границы context.strokeRect (230, 10, 100, 100); // прямоугольник без заполнения

Толщина линий

Свойство lineWidth позволяет установить толщину линии:

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.fillStyle = "#c7ecee"; // устанавливаем цвет заполнения фигуры context.fillRect (10, 10, 100, 100); context.strokeStyle = "#22a6b3"; // устанавливаем цвет контура фигуры context.lineWidth = 4.5; // устанавливаем толщину линии context.strokeRect (10, 10, 100, 100);

setLineDash

Метод setLineDash() в качестве параметра принимает массив чисел, которые устанавливают расстояния между линиями. Например:

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.strokeStyle = "red"; context.setLineDash([15,5]); context.strokeRect(10, 10, 100, 100); context.strokeStyle = "blue"; context.setLineDash([2,5,6]); context.strokeRect(130, 10, 100, 100); context.strokeStyle = "green"; context.setLineDash([2]); context.strokeRect(250, 10, 100, 100);

Тип соединения линий

Свойство lineJoin отвечает за тип соединения линий в фигуре. Оно может принимать следующие значения:

  • miter: прямые соединения, которые образуют прямые углы. Это значение по умолчанию
  • round: закругленные соединения
  • bevel: конические соединения
const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.strokeStyle = "red"; context.lineWidth = 10; context.lineJoin = "miter"; context.strokeRect(10, 10, 100, 100); context.lineJoin = "bevel"; context.strokeRect(130, 10, 100, 100); context.lineJoin = "round"; context.strokeRect(250, 10, 100, 100);

Прозрачность

Свойство globalAlpha задает прозрачность отрисовки. Оно может принимать в качестве значения число от 0 (полностью прозрачный) до 1.0 (не прозрачный):

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.fillStyle = "blue"; context.fillRect(50, 50, 100, 100); context.globalAlpha = 0.5; context.fillStyle = "red"; context.fillRect(100, 100, 100, 100);

Здесь на canvas выводятся два прямоугольника: синий и красный. Но до вывода красного прямоугольника установлена полупроразность отрисовки, поэтому сквозь красный прямоугольник мы сможем увидеть и синий:

📜 Фоновые изображения

Вместо конкретного цвета для заливки фигур, например, прямоугольников, мы можем использовать изображения. Для этого у контекста элемент canvas имеется функция createPattern(), которая принимает два параметра: изображение, которое будет использоваться в качестве фона, и принцип повторения изображения. Последний параметр играет роль в том случае, если размер изображения у нас меньше, чем размер фигуры на canvas. Этот параметр может принимать следующие значения:

  • repeat: изображение повторяется для заполнения всего пространства фигуры
  • repeat-x: изображение повторяется только по горизонтали
  • repeat-y: изображение повторяется только по вертикали
  • no-repeat: изображение не повторяется

Нарисуем прямоугольник и выведем в нем изображение:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <canvas id="canvas" width="400" height="250"></canvas> <script> const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); const img = new Image(); img.src = "forest.png"; img.onload = function() { const pattern = context.createPattern(img, "repeat"); context.fillStyle = pattern; context.fillRect(10, 10, 200, 200); context.strokeRect(10, 10, 200, 200); }; </script> </body> </html>

Чтобы использовать изображение, нам надо создать элемент Image и установить источник изображения - локальный файл или ресурс в сети:

const img = new Image(); img.src = "forest.png";

В данном случае предполагается, что в одной папке с файлом html у меня находится файл изображения forest.png. Однако загрузка изображения может занять некоторое время, особенно если файл изображения берется из сети интернет. Поэтому, чтобы быть уверенными, что изображение уже загрузилось, все действия по его использованию производятся в методе img.onload, который вызывается при завершении загрузки изображения:

img.onload = function() { const pattern = context.createPattern(img, "repeat"); context.fillStyle = pattern; context.fillRect(10, 10, 200, 200); context.strokeRect(10, 10, 200, 200); };

Метод createPattern() возвращает объект, который устанавливается в качестве стиля заполнения фигуры: context.fillStyle = pattern;. Отрисовка прямоугольника остается той же.

📜 Стили и цвета

Основные свойства

// Цвет заливки ctx.fillStyle = 'red'; ctx.fillStyle = '#FF0000'; ctx.fillStyle = 'rgb(255, 0, 0)'; ctx.fillStyle = 'rgba(255, 0, 0, 0.5)'; // С прозрачностью // Цвет линии ctx.strokeStyle = 'blue'; // Толщина линии ctx.lineWidth = 5; // Прозрачность всего рисунка ctx.globalAlpha = 0.5; // 0.0 (прозрачный) - 1.0 (непрозрачный)

📜 Создание градиента

Элемент Canvas позволяет использовать градиент в качестве фона. Для этого применяется объект CanvasGradient, который можно создать либо с помощью метода createLinearGradient() (линейный градиент), либо с помощью метода createRadialGradient() (радиальный градиент).

Линейный градиент

Линейный градиент создается помощью метода createLinearGradient(x0, y0, x1, y1), где x0 и y0 - это начальные координаты градиента относительно верхнего левого угла canvas, а x1 и y1 - координаты конечной точки градиента. Например:

const gradient = context.createLinearGradient(50, 30, 150, 150);

Также для создания градиента необходимо задать опорчные точки, которые определяют цвет. Для этого у объекта CanvasGradient применяется метод addColorStop(offset, color), где offset - это смещение точки градиента, а color - ее цвет. Например:

gradient.addColorStop(0, "blue");

Смещение представляет значение в диапазоне от 0 до 1. Смещение 0 представляет начало градиента, а 1 - его конец. Цвет задается либо в виде строки, либо в виде шестнадцатеричного значения, либо в виде значения rgb/rgba.

Применим градиент:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <canvas id="canvas" width="400" height="250"></canvas> <script> const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); const gradient = context.createLinearGradient(50, 30, 150, 150); gradient.addColorStop(0, "blue"); // от синего цвета gradient.addColorStop(1, "white"); // к белому цвету context.fillStyle = gradient; // в качестве цвета заполнения устанавливаем градиент context.fillRect(50, 30, 150, 150); context.strokeRect(50, 30, 150, 150); </script> </body> </html>

Здесь для создания градиента добавлены две опорные точки - для синего и белого цвета. В итоге мы получим переход от синего цвета в белый:

Стоит отметить, что опорных точек для создания градиента может быть несколько

gradient.addColorStop(0, "blue"); // от белого цвета gradient.addColorStop(0.5, "green"); // к зеленому цвету gradient.addColorStop(1, "white"); // к синему цвету

В примере выше мы получаем диагональный линейный градиент. Управляя координатами, мы можем получить горизонтальный или вертикальный градиент.

Совпадение x-координат начальной и конечной точек создает вертикальный градиент:

const gradient = context.createLinearGradient(50, 30, 50, 150);

А совпадение y-координат начальной и конечной точек создает горизонтальный градиент:

const gradient = context.createLinearGradient(50, 30, 150, 30);

Радиальный градиент

Радиальный градиент создается с помощью метода createRadialGradient(x0, y0, r0, x1, y1, r1), который принимает следующие параметры:

  • x0 и y0: координаты центра первой окружности
  • r0: радиус первой окружности
  • x1 и y1: координаты центра второй окружности
  • r1: радиус второй окружности

Например:

const gradient = context.createRadialGradient(120,100,100,120,100,30);

И также для радиального градиента нам надо задать опорные цветовые точки с помощью метода addColorStop()

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); const gradient = context.createRadialGradient(120,100,100,120,100,30); gradient.addColorStop(0, "blue"); gradient.addColorStop(1, "white"); context.fillStyle = gradient; context.fillRect(50, 30, 150, 150); context.strokeRect(50, 30, 150, 150);

📜 Рисование текста

Наряду с геометрическими фигурами и изображениями canvas позволяет выводить текст. Доля этого вначале надо установить у контекста canvas свойство font:

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.font = "22px Verdana";

Свойство font в качестве значения принимает определение шрифта. В данном случае это шрифт Verdana высотой 22 пикселя. В качестве шрифтов используются стандартные шрифты.

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

  • fillText(text, x, y): принимает три параметра: выводимый текст (параметр text) и координаты точки, с которой выводится текст (параметры x и y).
  • strokeText(text, x, y): принимает аналогичные параметры.

Разница между двумя метода состоит в том, что fillText() использует цвет заполнения фигуры (из свойства fillStyle) и заполняет им символы текста. Метод strokeText() использует цвет контура фигуры (задается через свойство strokeStyle) и отрисосывает контур символов.

Например, выведем некоторый текст с помощью метода fillText():

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <canvas id="canvas" width="400" height="250"></canvas> <script> const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.font = "30px Verdana"; context.fillStyle = "navy"; // устанавливаем цвет текста context.fillText("Hello METANIT.COM", 20, 50); </script> </body> </html>

Метод fillText(text, x, y) принимает три параметра: выводимый текст и x и y координаты точки, с которой выводится текст.

Вывод аналогичного текста с помощью метода strokeText():

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.font = "30px Verdana"; context.strokeStyle = "navy"; // устанавливаем цвет текста context.strokeText("Hello METANIT.COM", 20, 50);

Свойство textAlign

Свойство textAlign позволяет выровнить текст относительно одной из сторон. Это свойство может принимать следующие значения:

  • left: текст начинается с указанной позиции
  • right: текст завершается до указанной позиции
  • center: текст располагается по центру относительно указанной позиции
  • start: значение по умолчанию, текст начинается с указанной позиции
  • end: текст завершается до указанной позиции
var canvas = document.getElementById("myCanvas"), context = canvas.getContext("2d"); context.font = "22px Verdana"; context.textAlign = "right"; context.fillText("Right Text", 120, 30); context.textAlign = "left"; context.fillText("Left Text", 120, 60); context.textAlign = "center"; context.fillText("Center Text", 120, 90); context.textAlign = "start"; context.fillText("Start Text", 120, 120); context.textAlign = "end"; context.fillText("End Text", 120, 150);

Свойство textBaseline

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

  • top
  • middle
  • bottom
  • alphabetic
  • hanging
  • ideographic
const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.font = "22px Verdana"; context.textBaseline="top"; context.fillText("Top",10,100); context.textBaseline="bottom"; context.fillText("Bottom",45,100); context.textBaseline="middle"; context.fillText("Middle",130,100); context.textBaseline="alphabetic"; context.fillText("Alphabetic",205,100); context.textBaseline="hanging"; context.fillText("Hanging",320,100);

Определение ширины текста

С помощью метода measureText() можно определить ширину текста на canvase:

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.font = "18px Verdana"; const text = context.measureText("Hello JavaScript"); console.log(text.width);

Еще пример:

// Установка шрифта ctx.font = '30px Arial'; // Заполненный текст ctx.fillStyle = 'black'; ctx.fillText('Hello Canvas!', 50, 100); // Контурный текст ctx.strokeStyle = 'blue'; ctx.strokeText('Outline Text', 50, 150); // Выравнивание текста ctx.textAlign = 'center'; // 'left', 'right', 'center', 'start', 'end' ctx.textBaseline = 'middle'; // 'top', 'middle', 'bottom', 'alphabetic' ctx.fillText('Centered', canvas.width / 2, canvas.height / 2);

📜 Рисование линий и фигур

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

Для создания нового пути надо вызвать метод beginPath():

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.beginPath(); // начинаем рисование фигуры

После метода beginPath() вызываются методы, которые непосредственно создают различные участки пути.

Методы moveTo() и lineTo()

Для начала рисования пути нам надо зафиксировать начальную точку этого пути. Это можно сделать с помощью метода moveTo(), который имеет следующее определение:

moveTo(x, y)

Метод перемещает нас на точку с координатами x и y.

Метод lineTo() рисует линию. Он имеет похожее определение:

lineTo(x, y)

Метод рисует линию от текущей позиции до точки с координатами x и y.

Теперь нарисуем ряд линий:

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.beginPath(); context.moveTo(20, 100); context.lineTo(140, 10); context.lineTo(260, 100);

Здесь мы устанавливаем начало пути в точку (20, 100), затем от нее рисуем линию до точки (140, 10) (линия вверх) и далее рисуем еще одну линию до точки (260, 100).

Отображение пути

Хотя мы нарисовали несколько линий, пока мы их не увидим, потому что их надо отобразить на экране. Для отображения пути надо использовать метод stroke():

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.beginPath(); context.moveTo(20, 100); context.lineTo(140, 10); context.lineTo(260, 100); context.stroke(); // отображаем путь

По умолчанию для отрисовки используется черный цвет, но с помощью свойства strokeStyle можно изменить цвет:

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.beginPath(); context.moveTo(20, 100); context.lineTo(140, 10); context.lineTo(260, 100); context.strokeStyle = "red"; // красный цвет context.stroke(); // отображаем путь

Замыкание пути

Мы нарисовали две линии, и, допустим, мы их хотим соединить, чтобы замкнуть фигуру - в данном случае прямоугольник. В принципе в этом случае мы могли бы нарисовать еще одну линию, и у нас бы получился треугольник. Однако для упрощения Canvas API для этого предоставляет специальный метод - context.closePath(), который позволяет автоматически замкнуть путь, соединив первую и последнюю точки пути, и образовать фигуру:

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.beginPath(); context.moveTo(20, 100); context.lineTo(140, 10); context.lineTo(260, 100); context.closePath(); // закрываем путь context.stroke();

Базовые пути (paths)

ctx.beginPath(); // Начало нового пути ctx.moveTo(50, 50); // Переместиться в точку (50, 50) ctx.lineTo(200, 50); // Линия до точки (200, 50) ctx.lineTo(200, 150); // Линия до точки (200, 150) ctx.lineTo(50, 150); // Линия до точки (50, 150) ctx.closePath(); // Закрыть путь (вернуться к началу) ctx.stroke(); // Обвести контур

Объекты Path2D

При работе со многими путями может возникнуть путаница. В этом случае для разграниченися отдельных путей можно использовать объект Path2D. Этот объект предоставляет методы, аналогичные методам объекта контекста в отношении создания пути. Например:

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); const path1 = new Path2D(); // первый путь path1.moveTo(20, 100); path1.lineTo(140, 10); path1.lineTo(260, 100); path1.closePath(); // закрываем путь context.strokeStyle = "blue"; context.stroke(path1); const path2 = new Path2D(); // первый путь path2.moveTo(20, 110); path2.lineTo(140, 200); path2.lineTo(260, 110); path2.closePath(); // закрываем путь context.strokeStyle = "red"; context.stroke(path2);

Здесь создаются два пути, каждый из которых представляет треугольник. Для отрисовки каждого пути вызывается метод context.stroke(), в который передается путь.

Метод rect

Метод rect() создает прямоугольник. Он имеет следующее определение:

rect(x, y, width, height)

Где x и y - это координаты верхнего левого угла прямоугольника относительно canvas, а width и height - соответственно ширина и высота прямоугольника. Нарисуем, к примеру, следующий прямоугольник:

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.beginPath(); context.rect(30, 20, 100, 90); context.closePath(); context.stroke();

Стоит отметить, что такой же прямоугольник мы могли бы создать из линий:

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.beginPath(); context.moveTo(30, 20); context.lineTo(130, 20); context.lineTo(130, 110); context.lineTo(30, 110); context.closePath(); context.stroke();

Заливка фигуры. Метод fill()

Метод fill() заполняет цветом все внутреннее пространство нарисованного пути:

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.beginPath(); context.moveTo(20, 100); context.lineTo(140, 10); context.lineTo(260, 100); context.closePath(); context.strokeStyle = "#2e86de"; context.fillStyle = "#4bcffa"; context.fill(); // Залить фигуру context.stroke();

С помощью свойства fillStyle опять же можно задать цвет заполнения фигуры. В данном случае это цвет "#4bcffa".

Метод clip()

Метод clip() позволяет вырезать из canvas определенную область, а все, что вне этой области, будет игнорироваться при последующей отрисовке.

Для понимания этого метода сначала нарисуем два прямоугольника:

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); // рисуем первый красный прямоугольник context.beginPath(); context.moveTo(10, 20); context.lineTo(130, 20); context.lineTo(130, 110); context.lineTo(10, 110); context.closePath(); context.strokeStyle = "red"; context.stroke(); // рисуем второй зеленый прямоугольник context.beginPath(); context.rect(30, 50, 180, 70); context.closePath(); context.strokeStyle = "green"; context.stroke();

Теперь применим метод clip() для ограничения области рисования только первым прямоугольником:

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); // рисуем первый красный прямоугольник context.beginPath(); context.moveTo(10, 20); context.lineTo(130, 20); context.lineTo(130, 110); context.lineTo(10, 110); context.closePath(); context.strokeStyle = "red"; context.stroke(); context.clip(); // обрезаем область рисования по первому пути // рисуем второй зеленый прямоугольник context.beginPath(); context.rect(30, 50, 180, 70); context.closePath(); context.strokeStyle = "green"; context.stroke();

Поскольку вызов метода clip() идет после первого прямоугольника, то из второго прямоугольника будет нарисована только та часть, которая попадает в первый прямоугольник.

📜 Дуги и окружности

Метод arc()

Метод arc() добавляет к пути участок окружности или дугу/арку. Он имеет следующее определение:

arc(x, y, radius, startAngle, endAngle, anticlockwise)

Здесь используются следующие параметры:

  • x и y: x- и y-координаты, в которых начинается дуга
  • radius: радиус окружности, по которой создается дуга
  • startAngle и endAngle: начальный и конечный угол, которые усекают окружность до дуги. В качестве единици измерения для углов применяются радианы. Например, полная окружность - это 2π радиан. Если, к примеру, нам надо нарисовать полный круг, то для параметра endAngle можно указать значение 2π. В JavaScript эту веричину можно получить с помощью выражения Math.PI * 2.
  • anticlockwise: направление движения по окружности при отсечении ее части, ограниченной начальным и конечным углом. При значении true направление против часовой стрелки, а при значении false - по часовой стрелке.

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

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.strokeStyle = "red"; context.beginPath(); context.moveTo(20, 90); context.arc(20, 90, 50, 0, Math.PI/2, false); context.closePath(); context.stroke(); context.beginPath(); context.moveTo(130, 90); context.arc(130, 90, 50, 0, Math.PI, false); context.closePath(); context.stroke(); context.beginPath(); context.moveTo(240, 90); context.arc(240, 90, 50, 0, Math.PI * 3 / 2, false); context.closePath(); context.stroke(); context.beginPath(); context.arc(350, 90, 50, 0, Math.PI*2, false); context.closePath(); context.stroke();

Последний параметр anticlockwise играет важную роль, так как определяет движение по окружности, и в случае изменения true на false и наоборот, мы можем получить совершенно разные фигуры:

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.strokeStyle = "red"; context.beginPath(); context.moveTo(80, 90); context.arc(80, 90, 50, 0, Math.PI/2, false); context.closePath(); context.stroke(); context.beginPath(); context.moveTo(240, 90); context.arc(240, 90, 50, 0, Math.PI/2, true); context.closePath(); context.stroke();
// arc(x, y, radius, startAngle, endAngle, counterclockwise) // Углы в радианах! // Полная окружность ctx.beginPath(); ctx.arc(100, 100, 50, 0, Math.PI * 2); ctx.fillStyle = 'orange'; ctx.fill(); // Полукруг ctx.beginPath(); ctx.arc(250, 100, 50, 0, Math.PI); ctx.strokeStyle = 'purple'; ctx.stroke(); // Четверть круга ctx.beginPath(); ctx.arc(400, 100, 50, 0, Math.PI / 2); ctx.stroke();

Метод arcTo()

Метод arcTo() также рисует дугу. Он имеет следующее определение:

arcTo(x1, y1, x2, y2, radius)

Где x1 и y1 - координаты первой контрольной точки, x2 и y2 - координаты второй контрольной точки, а radius - радиус дуги.

Пример рисования дуг с помощью arcTo():

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.strokeStyle = "red"; context.beginPath(); context.moveTo(0, 150); context.arcTo(0, 0, 150, 0, 140) context.closePath(); context.stroke();

Здесь мы перемещаемся вначале на точку (0, 150), и от этой точки до первой контрольной точки (0, 0) будет проходить первая касательная. Далее от первой контрольной точки (0, 0) до второй (150, 0) будет проходить вторая касательная. Эти две касательные оформляют дугу, а 140 служит радиусом окружности, на которой усекается дуга.

Метод quadraticCurveTo()

МетодquadraticCurveTo() создает квадратичную кривую. Он имеет следующее определение:

quadraticCurveTo(x1, y1, x2, y2)

Где x1 и y1 - координаты первой опорной точки, а x2 и y2 - координаты второй опорной точки.

Пример квадратичной Безье:

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.strokeStyle = "red"; context.beginPath(); context.moveTo(20, 90); context.quadraticCurveTo(130, 0, 280, 90) context.closePath(); context.stroke();

Метод bezierCurveTo(). Кривая Безье

Метод bezierCurveTo() рисует кривую Безье. Он имеет следующее определение:

bezierCurveTo(x1, y1, x2, y2, x3, y3)

Где x1 и y1 - координаты первой опорной точки, x2 и y2 - координаты второй опорной точки, а x3 и y3 - координаты третьей опорной точки.

Пример кривой Безье:

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.strokeStyle = "red"; context.beginPath(); context.moveTo(30, 100); context.bezierCurveTo(110, 0, 190, 200, 270, 100); context.closePath(); context.stroke();

Комплексные фигуры

Объединим несколько фигур вместе и нарисуем более сложную двухмерную сцену:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <canvas id="canvas" width="400" height="250"></canvas> <script> const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.beginPath(); context.fill(); context.fillStyle = "yellow"; context.beginPath(); context.arc(160, 130, 100, 0, 2 * Math.PI); context.fill(); // рот context.beginPath(); context.moveTo(100, 160); context.quadraticCurveTo(160, 250, 220, 160); context.closePath(); context.fillStyle = "red"; context.fill(); context.lineWidth = 2; context.strokeStyle = "black"; context.stroke(); // зубы context.fillStyle = "#FFFFFF"; context.fillRect(140, 160, 15, 15); context.fillRect(170, 160, 15, 15); //глаза context.beginPath(); context.arc(130, 90, 20, 0, 2 * Math.PI); context.fillStyle = "#333333"; context.fill(); context.closePath(); context.beginPath(); context.arc(190, 90, 20, 0, 2 * Math.PI); context.fillStyle = "#333333"; context.fill(); context.closePath(); </script> </body> </html>

📜 Рисование изображений

Ранее уже рассматривалась установка изображений в качестве фона фигур, но мы также может отдельно выводить изображения на canvas. Для этого у контекста canvas применяется метод drawImage(). Этот метод имеет три версии:

  • context.drawImage(image, x, y)

    Здесь параметр image передает выводимое изображение, а параметры x и y - координаты верхнего левого угла изображения.

  • context.drawImage(image, x, y, width, height)

    С помощью дополнительных параметров width и height позволяет соответственно задать ширину и высоту выводимого изображения.

  • drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)

    Где параметры sx и sy представляют координаты на изображении, с которого начиется обрезка изображения, а параметры sWidth и sHeight представляют соответственно ширину и высоту выреза относительно координат sx и sy.

    Параметры dx и dy указывают координаты отрисовки обрезанного изображения на canvas, а dWidth и dHeight указывают соответственно на ширину и высоту изображения на canvas.

Например, применим первую версию метода для вывода изображения:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <canvas id="canvas" width="450" height="300"></canvas> <script> const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); const img = new Image(); img.src = "forest2.png"; img.onload = ()=> context.drawImage(img, 0, 0); </script> </body> </html>

Опять же при выводе изображения нам надо быть уверенными, что изображение уже загружено браузером и готово к использованию, поэтому метод отрисовки изображения помещается в обработчик загрузки изображения img.onload.

Вторая версия метода drawImage()

...позволяет дополнительно задать ширину и высоту выводимого изображения, что может использоваться для масштабирования изображения:

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); const img = new Image(); img.src = "forest2.png"; img.onload = ()=> { context.drawImage(img, 10, 10, 180, 150); context.drawImage(img, 200, 10, 180, 150); }

Также применим третью форму метода drawImage():

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); const img = new Image(); img.src = "forest2.png"; img.onload = ()=> context.drawImage(img, 0, 100, 300, 200, 20, 30, 300, 200);
const img = new Image(); img.src = 'photo.jpg'; img.onload = () => { // Простое размещение ctx.drawImage(img, 10, 10); // С указанием размера ctx.drawImage(img, 10, 10, 200, 150); // Обрезка и размещение // drawImage(img, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) ctx.drawImage(img, 50, 50, 100, 100, 10, 10, 200, 200); };

Захват изображений с других элементов

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

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <video id="myVideo" src="cats.mp4" width="300" height="200" controls ></video> <canvas id="canvas" width="300" height="200" style="background-color:#eee; border:1px solid #ccc;"> </canvas> <div><button id="snap">Сделать снимок</button></div> <script> const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); const video = document.getElementById("myVideo"); document.getElementById("snap").onclick = () => context.drawImage(video, 0, 0, 300, 200); </script> </body> </html>

По нажати на кнопку canvas будет получать текущий кадр воспроизводимого видео и фиксировать его в качестве изображения. При этом в метод drawImage в качестве первого параметра передается сам элемент, используемый как источник изображения.

📜 Трансформации

Элемент canvas поддерживает трансформации фигур - перемещение, вращение, масштабирование.

Перемещение (translate)

Перемещение осуществляется с помощью метода translate():

translate(x, y)

Первый параметр указывает на смещение по оси X, а второй параметр - по оси Y.

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <canvas id="canvas" width="400" height="250"></canvas> <script> const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.fillStyle = "blue"; context.fillRect(50, 50, 100, 100); context.globalAlpha = 0.5; // прозрачность для наложения context.fillStyle = "red"; context.translate(50, 25); // смещение на 50 пикселей вправо и 25 пикселей вниз context.fillRect(50, 50, 100, 100); </script> </body> </html>

Здесь на одной позиции отрисовываются два равных прямоугольника: синий и красный. Однако к красному прямоугольнику применяется трансформация перемещения:

Другой пример:

ctx.save(); // Сохраняем состояние ctx.translate(100, 100); // Смещаем начало координат ctx.fillRect(0, 0, 50, 50); // Рисуем относительно нового начала ctx.restore(); // Восстанавливаем состояние

Поворот / Вращение (rotate)

Для поворота фигур на canvase применяется метод rotate():

rotate(angle)

В этот метод в качестве параметра передается угол поворота в радианах относительно точки с координатами (0, 0).

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.fillStyle = "blue"; context.fillRect(50, 20, 100, 100); context.globalAlpha = 0.5; context.fillStyle = "red"; context.rotate(.52); // поворот на 0.52 радиан или 30 градусов context.fillRect(50, 20, 100, 100);

Другой пример:

ctx.save(); ctx.translate(canvas.width / 2, canvas.height / 2); // В центр ctx.rotate(Math.PI / 4); // Поворот на 45 градусов (π/4 радиан) ctx.fillRect(-25, -25, 50, 50); // Рисуем квадрат ctx.restore();

Масштабирование (scale)

Для масштабирования фигур применяется метод scale():

scale(xScale, yScale)

Параметр xScale указывает на масштабирование по оси X, а yScale - по оси Y.

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.fillStyle = "blue"; context.fillRect(0, 0, 100, 100); context.globalAlpha = 0.5; context.fillStyle = "red"; context.scale(1.5, 1.5); // растяжение по ширине в 1.5 раза и сжатие по высоте в 1.5 раза context.fillRect(0, 0, 100, 100);

Другой пример:

ctx.save(); ctx.scale(2, 1); // Растянуть по X в 2 раза ctx.fillRect(10, 10, 50, 50); ctx.restore();

Матрица преобразований

При необходимости мы можем применять последовательно несколько преобразований:

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.fillStyle = "blue"; context.fillRect(0, 0, 100, 100); context.globalAlpha = 0.5; context.fillStyle = "red"; context.scale(1.5, 1.5); context.translate(100, 10); context.rotate(0.34); context.fillRect(0, 0, 100, 100);

Но контекст элемента canvas также предоставляет метод transform(), который позволяет задать матрицу преобразования:

transform(a, b, c, d, e, f)

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

  • a: масштабирование по оси X
  • b: поворот вокруг оси X
  • c: поворот вокруг оси Y
  • d: масштабирование по оси Y
  • e: горизонтальное смещение
  • f: вертикальное смещение

Например:

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.fillStyle = "blue"; context.fillRect(100, 30, 100, 100); context.globalAlpha = 0.5; context.fillStyle = "red"; // применяем трансформации context.transform( Math.cos(Math.PI/6), Math.sin(Math.PI/6), -1 * Math.sin(Math.PI/6), Math.cos(Math.PI/6), 0, 0); context.fillRect(100, 30, 100, 100);

Замена трансформации

При последовательном применении разных трансформаций они просто последовательно применяются к фигурам. Однако может возникнуть ситуация, когда надо применить трансформацию не вместе со другими, а вместо других, то есть заменить трансформацию. Для этого применяется метод setTransform():

setTransform(a, b, c, d, e, f)

Его параметры представляют матрицу преобразования, и в целом его применение аналогино применению метода transform(). Например:

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); let k = 0; for (let x = 0; x < 30; x++) { k = Math.floor(255 / 34 * x); context.fillStyle = "rgb(" + k + "," + k + "," + k + ")"; context.fillRect(50, 50, 200, 100); context.setTransform(1, 0, 0, 1, x, x); }

Сброс трансформаций

При применении трансформаций вся последующая отрисовка фигур подвергается данным трансформациям. Но возможна ситуация, когда после одиночного применения трансформации нам больше не нужно ее применение. И для всей последующей отрисовки мы можем сбросить трансформации с помощью метода resetTransform():

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.fillStyle = "blue"; context.fillRect(100, 10, 100, 100); context.globalAlpha = 0.5; context.fillStyle = "red"; context.translate(50, 25); // дальше применяется трансформация context.fillRect(100, 10, 100, 100); context.fillStyle = "green"; context.resetTransform(); // трансформация больше не применяется context.fillRect(0, 10, 100, 100);

📜 Добавление теней

Элемент canvas поддерживает добавление теней к нарисованным объектам. Для создания теней у контекста canvas применяются следующие свойства:

  • shadowOffsetX: горизонтальное смещение в пикселях справа (или слева при отрицательном значении)
  • shadowOffsetY: вертикальное смещение в пикселях снизу (или сверху при отрицательном значении)
  • shadowBlur: число пикселей ля установки размыти тени
  • shadowColor: цвет тени

Например, установим тень для прямоугольника:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <canvas id="canvas" width="400" height="250"></canvas> <script> const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.fillStyle = "#3498db"; context.shadowOffsetX = 10; context.shadowOffsetY = 10; context.shadowBlur = 10; context.shadowColor = "#999"; context.fillRect(10, 10, 200, 200); </script> </body> </html>

В данном случае устанавливаем тень для прямоугольника:

Подобным образом можно применять тени и к другим фигурам, тексту и изображениям:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <canvas id="canvas" width="400" height="250"></canvas> <script> const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); context.font="16px Verdana"; context.fillStyle = "#222"; context.shadowOffsetX = 3; context.shadowOffsetY = 3; context.shadowBlur = 3; context.shadowColor = "#AAA"; context.fillText("Тени на Canvas", 80, 30); const img = new Image(); img.src = "forest.png"; img.onload = function() { context.shadowOffsetX = 8; context.shadowOffsetY = 8; context.shadowBlur = 5; context.shadowColor = "#333"; context.drawImage(img, 50, 50, 192, 128); }; </script> </body> </html>

📜 Редактирование пикселей

JavaScript предоставляет встроенную функциональность для редактирования изображения и установки значения конкретных пикселей на canvas. В частности, мы можем изменить цветовые значения пикселя, его прозрачность. Для этого предназначены такие методы, как getImageData(), putImageData() и createImageData().

Метод getImageData()

Метод getImageData() позволяет извлечь из canvas какую-либо часть изображеня. Он имеет следующее определение:

Данные из определенной этими параметрами области извлекаются в виде объекта ImageData, который потом используется для манипуляции пикселями.

getImageData(sx, sy, sw, sh);

Здесь sx и sy - координаты верхнего левого угла области, из которой извлекаются данные на canvas, а sw и sh - соотвественно ширина и высота этой области.

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

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); const img = new Image(); img.src = "forest.png"; img.onload = function() { context.drawImage(img, 0, 0); const imageData = context.getImageData(0,0, 100, 100); };

Все данные об изображении в объекте ImageData хранятся в массиве data. Каждый пиксель на canvas характеризуется четырьмя компонентами в формате RGBA: красной, зеленой, синей компонентой, которые устанавливают цвет, и альфа-компонентой, которая устанавливает прозрачность. Каждая компонента принимает значени от 0 до 255. И чтобы получить значения цвета для самого первого пикселя в ImageData, нам надо последовательно получить четыре значения из массива data:

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); const img = new Image(); img.src = "dubi.png"; img.onload = function() { context.drawImage(img, 0, 0); const imageData = context.getImageData(0,0, 100, 100); const red = imageData.data[0]; // компонента красного цвета const green = imageData.data[1]; // компонента зеленого цвета const blue = imageData.data[2]; // компонента синего цвета const alpha = imageData.data[3]; // компонента прозрачности };

В данном случае мы получаем информацию о самом первом пикселе, который находится в самом верхнем левом углу, то есть имеет координаты x = 0 и y = 0.

Чтобы получить информацию о втором пикселе, который имеет координаты x = 1 и y = 0, нам надо получить следующие четыре значения из массива data:

imageData.data[4]; // компонента красного цвета imageData.data[5]; // компонента зеленого цвета imageData.data[6]; // компонента синего цвета imageData.data[7]; // компонента прозрачности

И так далее мы можем получить информацию обо всех пикселях.

Метод putImageData()

Метод putImageData() устанавливает на canvas новые данные. Этот метод имеет следующие виды:

putImageData(imageData, dx, dy) putImageData(imageData, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight)

Параметры dx и dy указывают координаты верхнего левого угла условного прямоугольника imageData, в который размещается на canvas.

Дополнительные параметры dirtyX и dirtyY указывают соотвественно на координаты X и Y левого угла прямоугольной области, которая будет извлекаться из полученного изображения. А параметры dirtyWidth и dirtyHeight задают соотвественно ширину и высоту этой области.

Используем методы getImageData() и putImageData() для преобразования изображения:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <canvas id="canvas" width="700" height="300"></canvas> <script> const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); const img = new Image(); img.src = "forest3.png"; img.onload = function() { context.drawImage(img, 0, 0); const imageData = context.getImageData(0,0, img.width, img.height); let red, green, blue, grayscale; for (let i = 0; i < imageData.data.length; i += 4) { red = imageData.data[i]; // получаем компоненту красного цвета green = imageData.data[i + 1]; // получаем компоненту зеленого цвета blue = imageData.data[i + 2]; // получаем компоненту синего цвета grayscale = red * 0.3 + green * 0.59 + blue * 0.11; // получаем серый фон imageData.data[i] = grayscale; // установка серого цвета imageData.data[i + 1] = grayscale; imageData.data[i + 2] = grayscale; } context.putImageData(imageData, img.width + 10, 0); }; </script> </body> </html>

Ключевым участком кода здесь является цикл, в котором и происходит преобразование изображения в серое:

let red, green, blue, grayscale; for (let i = 0; i < imageData.data.length; i += 4) { red = imageData.data[i]; // получаем компоненту красного цвета green = imageData.data[i + 1]; // получаем компоненту зеленого цвета blue = imageData.data[i + 2]; // получаем компоненту синего цвета grayscale = red * 0.3 + green * 0.59 + blue * 0.11; // получаем серый фон imageData.data[i] = grayscale; // установка серого цвета imageData.data[i + 1] = grayscale; imageData.data[i + 2] = grayscale; }

Здесь мы перемещаемся по всему массиву imageData.data, обрабатывая за раз четыре элемента, которые и представляют отдельный пиксель. При этом учитываются только три элемента, поскольку компонента прозрачности в данном случае нас не интересует.

Сначала мы получаем компоненты RGB. Затем, применяя математическую формулу, преобразуем значения RGB в серый цвет. И в конце серый цвет устанавливается для элементов пикселя.

Вопросы безопасности

Если мы попробуем просто кинуть файл веб-страницы с выше описанным кодом в браузер Google Chrome или попытаемся открыть файл по двойному клику, то Google Chrome не отобразит нам преобразованное серое изображение в связи с политикой браузера. Хотя в других браузерах, как Firefox или Microsoft Edge все может быть нормально. Дело в том, что в Google Chrome не позволяет манипулировать изображением сайта из одного домена пользователю из другого домена. По сути, когда мы загружаем файл по протоколу file:// (просто кинув файл в браузер или по двойному клику), браузер рассматривает пользователя и открытую веб-страницу как разные домены.

Если веб-страница будет расположена на веб-сервере и загружена по протоколу http, как это обычно бывает, то, конечно, проблемы не возникнет, так как пользователь и сайт будут работать в рамках одного домена. Но для тестирования опять же либо придется использовать веб-сервер, либо изменить соответствующим образом настройки браузера Google Chrome.

Метод createImageData()

Метод createImageData() создает новый объект ImageData, который затем может использоваться на canvas. Метод createImageData() имеет две формы:

createImageData(width, height); createImageData(imagedata);

Первая форма принимает параметры width и height, которые устанавливают соотвественно ширину и высоту создаваемого объекта ImageData.

Вторая форма принимает в качестве параметра другой объект ImageData, по которому будет создан новый объект ImageData.

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

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); const img = new Image(); img.src = "river2.png"; img.onload = function() { context.drawImage(img, 0, 0); const imageData = context.getImageData(0,0, img.width, img.height); const newImageData = context.createImageData(imageData); for (let i = 0; i < newImageData.data.length; i++) { newImageData.data[i] = imageData.data[i]; // если это альфа-компонента if( (i+1)%4===0){ newImageData.data[i] = 120; } } context.putImageData(newImageData, img.width + 10, 0); };

В данном случае создаем новый объект newImageData, в этот объект копируем все данные из текущего imageData, который представляет изображение на canvas. При этом при копировании значения альфа-компоненты, которая отвечает за прозрачность, устанавливаем ей значение 120, то есть делаем пиксель полупрозрачным.

📜 Рисование мышью

Ранее мы рассматривали в основном статическую графику на canvas. Но мы также можем создавать фигуры динамически, просто рисуя указателем мыши.

Для этого определим следующую страницу:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <canvas id="canvas" width="400" height="250" style="background-color:#eee; border: 1px solid #ccc; margin:10px;"></canvas> <script> const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); const w = canvas.width; const h=canvas.height; const mouse = { x:0, y:0}; // координаты мыши let draw = false; // нажатие мыши canvas.addEventListener("mousedown", function(e){ mouse.x = e.pageX - this.offsetLeft; mouse.y = e.pageY - this.offsetTop; draw = true; context.beginPath(); context.moveTo(mouse.x, mouse.y); }); // перемещение мыши canvas.addEventListener("mousemove", function(e){ if(draw==true){ mouse.x = e.pageX - this.offsetLeft; mouse.y = e.pageY - this.offsetTop; context.lineTo(mouse.x, mouse.y); context.stroke(); } }); // отпускаем мышь canvas.addEventListener("mouseup", function(e){ mouse.x = e.pageX - this.offsetLeft; mouse.y = e.pageY - this.offsetTop; context.lineTo(mouse.x, mouse.y); context.stroke(); context.closePath(); draw = false; }); </script> </body> </html>

Для обработки движения мыши для элемента canvas определены три обработчика - нажатия мыши, перемещения и отпускания мыши. При нажатии мыши мы устанавливаем переменную draw равным true. То есть идет рисование. Также при нажатии мы фиксируем точку, с которой будет идти рисование.

При перемещении мыши получаем точку, на которую переместился указатель, и рисуем линию. При отпускании указателя закрываем графический путь методом context.closePath() и сбрасываем переменную draw в false.

📜 Сохранение и восстановление состояния canvas

Когда применяются такие методы, как fillRect() или fillText(), может потребоваться заранее настроить цвет заливки, шрифт и ряд других свойств глобально для всего объекта. Чтобы сохранить эти настройки, объект контекста предоставляет метод save(). Каждый раз, когда вызывается этот метод, текущие настройки canvas помещаются в стек и сохраняются. Чтобы в последующем обратно получить сохраненные настройки, применяется метод restore(). Подобное сохранение-восстановление настроек может быть полезно, когда нам необходимо применять к части фигур глобальные общие настройки, а к другой части фигур - локальные.

Например, определим следующую страницу:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <canvas id="canvas" width="400" height="250"></canvas> <script> const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); // Рисуем прямоугольник с настройками по умолчанию // Цвет заливки по умолчанию - черный context.fillRect(0, 0, 150, 150); // Сохраняем текущее состояние (назовем его состояние 1) context.save(); // Изменяем настройки - устанавливаем в качестве цвета заливки - зеленый context.fillStyle = "#7ed6df"; context.fillRect(15, 15, 120, 120); // рисуем прямоугольник с новыми настройками // Сохраняем текущее состояние (назовем его состояние 2) context.save(); // Изменяем настройки - устанавливаем в качестве цвета заливки - красный context.fillStyle = "#ff7979"; context.fillRect(30, 30, 90, 90); // Рисуем прямоугольник с новыми настройками context.restore(); // Загружаем предыдущее состояние (состояние 2) context.fillRect(45, 45, 60, 60); // Рисуем прямоугольник с предыдущими настройками (зеленый цвет) context.restore(); // Загружаем предыдущее состояние (состояние 1) context.fillRect(60, 60, 30, 30); // Рисуем прямоугольник с предыдущими настройками (черный цвет) </script> </body> </html>

Здесь сначала создается квадрат высотой 150 пикселей и шириной 150 пикселей с использованием настроек по умолчанию (цвет заливки по умолчанию черный). Вызвав save(), мы сохранием эти значения по умолчанию в стек. Затем цвет заливки устанавливается на значение "#7ed6df" (оттенок зеленого цвета), и создается квадрат немного меньшего размера (смещением по горизонтали и вертикали). Повторный вызов save() также сохранит эти настройки в стек. Далее цвет фона устанавливается в "#ff7979" (разновидность красного), и снова рисуется квадрат меньшего размера.

Затем вызов restore() загружает настройки, которые были последними сохранены в стеке, то есть те, в которых цвет заливки был установлен на зеленый (поэтому созданный впоследствии квадрат имеет этот цвет). Таким же образом второй вызов функции restore() затем загрузит начальные настройки, где применяется черный цвет.

📜 Простая анимация на canvas

Сочетание отрисовки различных фигур на canvas с приминением функции requestAnimationFrame() позволяет создать анимацию содержимого элемента canvas.

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

  1. Очистка области рисования
  2. Опциональное сохранение состояния
  3. Рисование отдельного кадра
  4. Опциональная загрузка состояния

Эти шаги обычно инкапсулируются в функцию, которая затем вызывается через равные промежутки времени (для каждого кадра анимации). Для выполнения функции применяется метод requestAnimationFrame(), который специально предназначен для работы с анимацией внутри веб-страницы.

Например, определим следующую страницу:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <canvas id="canvas" width="400" height="250" style="background-color: #eee; border-color: #ccc;"></canvas> <script> const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); let x = 0; let step = 1; function draw() { context.clearRect(0,0,400,250); // очистка области рисования console.log(x); context.fillStyle = "red"; context.fillRect(x,10,40,40); // отрисовка прямоугольника if(x >= 360) step = -1; if(x <= 0) step = 1; x += step; window.requestAnimationFrame(draw); } draw(); </script> </body> </html>

Здесь функция draw() содержит код, который отрисовывает прямоугольник. Первый вызов функции вызывает отрисовку первого кадра анимации. В конце вызова функции вызывается метод requestAnimationFrame, которому передается эта же функциия draw, что фактически запускает анимацию. Таким образом, функция draw() фактически постоянно вызывает саму себя для рисования нового кадра анимации. Суть же анимации заключается в том, что меняем х-координату прямоугольника. При каждой отрисовке прямоугольник отображается на новой позиции, за счет чего возникает иллюзия его движения.

Аналогичным образом можно анимировать целые композиции фигур:

const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); function draw() { context.clearRect(0,0,500,300); // очистка области рисования const time = new Date(); // угол для вращения прямоугольников const angle = ((2*Math.PI)/6)*time.getSeconds() + ((2*Math.PI)/6000)*time.getMilliseconds(); context.fillStyle = "red"; context.save(); context.translate(150,150); context.rotate(angle); context.translate(0,25); context.fillRect(5,5,20,20); // красный прямоугольник context.restore(); context.fillStyle = "yellow"; context.save(); context.translate(150,150); context.rotate(angle); context.translate(0,50); context.fillRect(5,5,20,20); // желтый прямоугольник context.restore(); context.fillStyle = "green"; context.save(); context.translate(150,150); context.rotate(angle); context.translate(0,75); context.fillRect(5,5,20,20); // зеленый прямоугольник context.restore(); window.requestAnimationFrame(draw); } draw();

Простой пример:

let x = 0; function animate() { // Очистка canvas ctx.clearRect(0, 0, canvas.width, canvas.height); // Рисуем объект ctx.fillStyle = 'red'; ctx.fillRect(x, 100, 50, 50); // Изменяем позицию x += 2; if (x > canvas.width) x = -50; // Следующий кадр requestAnimationFrame(animate); } animate(); // Запуск анимации

📜 Практический пример: Простая игра

<canvas id="game" width="400" height="400"></canvas> <script> const canvas = document.getElementById('game'); const ctx = canvas.getContext('2d'); // Игрок let player = { x: 175, y: 350, width: 50, height: 50, speed: 5 }; // Враг let enemy = { x: 175, y: 0, width: 50, height: 50, speed: 2 }; // Управление let keys = {}; window.addEventListener('keydown', (e) => keys[e.key] = true); window.addEventListener('keyup', (e) => keys[e.key] = false); function update() { // Движение игрока if (keys['ArrowLeft'] && player.x > 0) player.x -= player.speed; if (keys['ArrowRight'] && player.x < 350) player.x += player.speed; // Движение врага enemy.y += enemy.speed; if (enemy.y > 400) { enemy.y = -50; enemy.x = Math.random() * 350; } } function draw() { // Очистка ctx.fillStyle = '#EEE'; ctx.fillRect(0, 0, 400, 400); // Игрок (синий) ctx.fillStyle = 'blue'; ctx.fillRect(player.x, player.y, player.width, player.height); // Враг (красный) ctx.fillStyle = 'red'; ctx.fillRect(enemy.x, enemy.y, enemy.width, enemy.height); } function gameLoop() { update(); draw(); requestAnimationFrame(gameLoop); } gameLoop(); </script>

📜 Заключение

📚 Основные выводы
  • Canvas — элемент HTML5 для рисования графики
  • getContext('2d') — получение контекста для 2D-рисования
  • Прямоугольники: fillRect(), strokeRect(), clearRect()
  • Пути: beginPath(), moveTo(), lineTo(), closePath()
  • Окружности: arc() с углами в радианах
  • Текст: fillText(), strokeText() с настройкой font
  • Трансформации: translate(), rotate(), scale()
  • Анимация: requestAnimationFrame() для плавности

Глава 24. IndexedDB API

📜 Введение в IndexedDB

IndexedDB — это встроенная в браузер NoSQL база данных для хранения больших объёмов структурированных данных. В отличие от localStorage, IndexedDB позволяет хранить сложные объекты, индексировать данные и выполнять транзакционные операции. Основная идея IndexedDB заключается в хранении объектов под определенными ключами.

💡 Преимущества IndexedDB
  • Большой объём — сотни МБ (зависит от браузера)
  • Асинхронность — не блокирует UI
  • Транзакции — гарантия целостности данных
  • Индексы — быстрый поиск по полям
  • Ключи — уникальная идентификация записей
  • Типы данных — объекты, массивы, blob, файлы

Основные концепции

Термин Описание
База данных (Database) Контейнер для хранилищ данных
Хранилище (Object Store) Аналог таблицы в SQL (хранит объекты)
Ключ (Key) Уникальный идентификатор записи
Индекс (Index) Дополнительное поле для быстрого поиска
Транзакция (Transaction) Группа операций (все выполняются или нет)
Версия (Version) Номер версии схемы БД

📜 Создание и открытие базы данных IndexDB

Для обращения к функциональности IndexedDB применяется свойство indexedDB объекта window:

const dbFactory = window.indexedDB; console.log(dbFactory); // IDBFactory{}

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

open(name) open(name, version)

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

const request = indexedDB.open("test"); // подключаемся к бд test

В этом коде мы пытаемся подключить к базе данных "test". Причем не важно, что изначально такой базы данных нет - если она отсутствует, она автоматически создается.

Возвращаемое значение метода open() — это объект типа IDBOpenDBRequest. Можно сказать, что это объект запроса на открытие соответствующей базы данных.

Для обращения к самой базе данных у объекта IDBOpenDBRequest можно обработать событие success. Оно возникает при успешном открытии базы данных. Для установки обработчика этого события можно использовать свойство onsuccess:

const request = indexedDB.open("test"); // подключаемся к бд test // при удачном открытии срабатывает событие success // обрабатываем это событие request.onsuccess = (event) => { const database = event.target.result; // обращаемся к базе данных console.log(database.name); // "Test" };

Внутри обработчика события через свойство event.target.result мы можем получить базу данных, которая представляет объект IDBDatabase

// Открываем или создаём БД const request = indexedDB.open('myDatabase', 1); // При создании/обновлении версии request.onupgradeneeded = (event) => { const db = event.target.result; console.log('База данных создана/обновлена'); // Создаём хранилище (если не существует) if (!db.objectStoreNames.contains('users')) { const store = db.createObjectStore('users', { keyPath: 'id', // Ключ autoIncrement: true // Автоинкремент }); console.log('Хранилище "users" создано'); } }; // При успешном открытии request.onsuccess = (event) => { const db = event.target.result; console.log('База данных открыта'); console.log('Версия:', db.version); }; // При ошибке request.onerror = (event) => { console.error('Ошибка:', event.target.error); };

Обработка ошибок при открыти базы данных

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

const request = indexedDB.open("test"); // подключаемся к бд test // обрабатываем ошибку request.onerror = (event) => { const error = event.target.error; // Получаем информацию об ошибке console.error(error.message); };

Отслеживание создания базы данных

При открытии базы данных, если она отсутствует, то она автоматически создается. Поэтому в этом плане неважно, есть у нас база данных или нет. Однако иногда бывает необходимо отследить создание базы данных. В этом случае мы можем использовать событие upgradeneeded, которое вызывается всякий раз, когда либо запрошенная база данных еще не существует и поэтому создается, либо когда запрошенная база данных еще не существует в этой конкретной версии. В обоих случаях вызываемый обработчик событий получает в качестве параметра объект типа IDBVersionChangeEvent. Этот объект позволяет получить старую и новую версию бд. Для установки обработчика события можно использовать свойство onupgradeneeded:

request.onupgradeneeded = (event) => { console.log(event.oldVersion); // старая версия базы данных console.log(event.newVersion); // новая версия базы данных const database = event.target.result; // обращаемся к самой бд };

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

Получение баз данных

Метод databases() объекта IDBFactory позволяет получить список всех имеющихся баз данных. Этот метод возвращает promise, из которого можно получить данные в виде массива, где каждый элемент представляет объект с двумя свойствами - name (имя базы данных) и version (версия базы данных):

const promise = indexedDB.databases(); promise.then((databases) => console.log(databases));

Например, вывод в моем случае:

(2) [{…}, {…}]
0: {name: 'test', version: 1}
1: {name: 'test2', version: 1}
length: 2
[[Prototype]]: Array(0)

В данном случае видно, что у меня две базы данных - test и test2, у каждой из которых версия 1.

Удаление базы данных

Для удаления базы данных у объекта IDBFactory применяется метод deleteDatabase(), в который передается имя удаляемой базы данных:

indexedDB.deleteDatabase("test2");

Стоит отметить, что этот метод в качестве результата возвращает объект IDBOpenDBRequest (который упоминался выше). Соответственно мы можем обработать события success и error, чтобы определить, успешно ли произошло удаление или произошла ошибка:

const DBDeleteRequest = indexedDB.deleteDatabase("test2"); DBDeleteRequest.onerror = (event) => { console.error("Error deleting database."); }; DBDeleteRequest.onsuccess = (event) => { console.log("Database deleted successfully"); console.log(event.result); // должно быть undefined };

Если база данных была успешно удалена, то свойство result аргумента в обработчике события success будет не установлено.

📜 Управление базой данных в IndexedDB

База данных в IndexedDB API представлена интерфейсом IDBDatabase, который обеспечивает соединение с базой данных, управление ее хранилищами и создание запросов к данным. Параметры базы данных можно получить с помощью следующих свойств IDBDatabase:

  • name: возвращает имя подключенной базы данных
  • version: возвращает номер версии базы данных. При создании базы данных этот атрибут представляет собой пустую строку.
  • objectStoreNames: возвращает список имен хранилищ (объект DOMStringList), которые в данный момент имеются в подключенной базе данных

Например, получим информацию о базе данных при подключении:

const request = indexedDB.open("test"); // подключаемся к бд test // при удачном открытии срабатывает событие success // обрабатываем это событие request.onsuccess = (event) => { const database = event.target.result; // обращаемся к базе данных console.log(database.name); // имя базы данных - "Test" console.log(database.version); // версия базы данных console.log(database.objectStoreNames); // список хранилищ в базе данных };

Кроме свойств объект IDBDatabase предоставляет ряд методов для управления подключением и хранилищами:

  • close(): закрывает подключение к базе данных
  • createObjectStore(): создает хранилище
  • deleteObjectStore(): удаляет хранилище
  • transaction(): возвращает объект транзакции - объект IDBTransaction, который применяется для получения хранилища и последующего выполнения запроса к данным

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

Создание хранилища объектов

Итак, для создания хранилища объектов применяется метод createObjectStore():

createObjectStore(name) createObjectStore(name, options)

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

  • keyPath: задает название свойства-ключа объекта
  • autoIncrement: при значении true значения ключей генерируются автоматически. По умолчанию значение false (ключи не генерируются)

Комбинации этих параметров очень важны. В IndexedDB каждый объект хранится под определенным ключом, который впоследствии можно использовать для доступа к объекту. Что именно используется в качестве ключа, определяется различными комбинациями параметров конфигурации keyPath и autoIncrement:

keyPath autoIncrement Описание
Не указано false Хранилище объектов может хранить значения произвольного типа (в том числе значения примитивных типов как числа и строки). Однако ключ необходимо явно указывать каждый раз при добавлении нового объекта в базу данных
Указано false Хранилище объектов может хранить только объекты (т.е. никаких примитивных типов данных). Однако эти объекты должны иметь свойство с тем же именем, которое указанно в параметре keyPath
Не указано true Хранилище объектов может хранить значения произвольного типа (в том числе значения примитивных типов). Ключи объектов генерируются автоматически при добавлении объектов. Однако также можно явным образом указать ключ для объекта
Указано true Хранилище объектов может хранить только объекты (т.е. никаких примитивных типов данных). Ключи же называются также, как указано в параметре keyPath. Если у объекта это свойство отсутствует, то значение ключа генерируется автоматически и добавляется к новому объекту. Если же свойство уже существует в объекте, то ключ не генерируется, а в качестве ключа используется уже сохраненное значение

Например, создадим в базе данных test хранилище с именем "users":

const request = indexedDB.open("test", 2); // подключаемся к бд test // обрабатываем создание базы данных или изменение версии request.onupgradeneeded = (event) => { const db = event.target.result; // получаем бд // создаем хранилище объектов users const objectStore = db.createObjectStore("users", { keyPath: "id", autoIncrement: true }); // IDBObjectStore {name: "users", keyPath: "id", indexNames: DOMStringList, // transaction: IDBTransaction, autoIncrement: true} console.log(objectStore); console.log(db.objectStoreNames); // список хранилищ в базе данных };

В данном случае ключом объектов будет служить свойство "id", причем его значение будет генерироваться автоматически. Результатом метода db.createObjectStore() является объект созданного хранилища, который представляет тип IDBObjectStore.

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

Удаление хранилища

Для удаления хранилища применяется метод deleteObjectStore(), в который передается имя удаляемого хранилища:

const request = indexedDB.open("test", 3); // подключаемся к бд test // обрабатываем изменение версии базы данных request.onupgradeneeded = (event) => { const db = event.target.result; // получаем бд // пересоздаем хранилище users - сначала удаляем db.deleteObjectStore("users"); // потом заново создаем db.createObjectStore("users", { keyPath: "id", autoIncrement: true }); console.log(db.objectStoreNames); // список хранилищ в базе данных };

📜 Выполнение запросов к базе данных

Все операции с базой данных IndexDB инкапсулируются в транзакции, которые представляют тип IDBTransaction. Для создания объектов этого типа у объекта базы данных IDBDatabase вызывается метод transaction():

transaction(storeNames) transaction(storeNames, mode)

Этот метод принимает следующие параметры:

  • storeNames: массив имен хранилищ объектов, к которым осуществляется доступ в транзакции.
    const request = indexedDB.open("test", 3); // подключаемся к БД "test" request.onsuccess = (event) => { const db = event.target.result; // получаем БД // создаем транзакцию const transaction = db.transaction("users"); // обращаемся к хранилищу "users" console.log(transaction); };

    Если надо задействовать несколько хранилищ, то они определяются в виде массива:

    // обращаемся к хранилищам "users" и "companies" const transaction = db.transaction(["users", "companies"]);

    Если надо указать все хранилища объектов базы данных, то можно использовать свойство IDBDatabase.objectStoreNames:

    const transaction = db.transaction(db.objectStoreNames);
  • mode: необязательный параметр, устанавливает тип доступа к базе данных. Может принимать следующие значения:
    • readonly: данные доступны только для чтения. Данное значение используется по умолчанию, если параметр mode не указан явным образом.
    • readwrite: данные доступны как для чтения, так и для записи
    • versionchange: разрешены чтение и запись, а также операции по удалению или созданию хранилищ и индексов объектов.

    Пример применения:

    const transaction = db.transaction("users", "readwrite");

С помощью свойств IDBTransaction можно получить некоторую информацию о транзакции:

  • db: объект базы данных (IDBDatabase)
  • error: информация об ошибке в виде объекта DOMException
  • mode: режим доступа к базе данных, если не установлен, то по умолчанию readonly
  • objectStoreNames: список задействованных в транзакции хранилищ (объект DOMStringList), где каждое хранилище представляет объект IDBObjectStore

Например, получение данных:

const request = indexedDB.open("test", 3); // подключаемся к БД "test" request.onsuccess = (event) => { const db = event.target.result; // получаем бд // создаем транзакцию const transaction = db.transaction(["users"], "readwrite"); console.log(transaction.db.name); // test console.log(transaction.mode); // readwrite console.log(transaction.objectStoreNames); // DOMStringList {0: "users", length: 1} };

Получение хранилища

Для выполнения собственно операций с хранилищем в рамках транзакции применяются различные методы объекта IDBObjectStore, который и представляет собой хранилище. Для получения объекта IDBObjectStore у объекта транзакции IDBTransaction вызывается метод objectStore():

const userStore = transaction.objectStore("users"); // получаем хранилище users

Следует отметить, что хранилище должны быть ранее создано с помощью метода createObjectStore() объекта IDBDatabase. Создать хранилище, например, можно при создании или изменении версии базы данных:

const request = indexedDB.open("test", 5); // подключаемся к бд test // при создании или изменении версии базы данных создаем в ней хранилище request.onupgradeneeded = (event) => { const db = event.target.result; // получаем БД // пересоздаем хранилище users - сначала удаляем, если оно существует db.deleteObjectStore("users"); // потом заново создаем const userStore = db.createObjectStore("users", { keyPath: "id", autoIncrement: true }); console.log(userStore); // получаем хранилище }; // при открытии базы данных создаем транзакцию и получаем хранилище request.onsuccess = (event) => { const db = event.target.result; // получаем бд // создаем транзакцию const transaction = db.transaction(["users"], "readwrite"); const userStore = transaction.objectStore("users"); // получаем хранилище users console.log(userStore); };

Тип IDBObjectStore предоставляет следующий ряд методов для операций над данными в хранилище:

  • add(): добавляет новые объекты в хранилище
  • clear(): очищает хранилище (удаленяет все объекты)
  • count(): возвращает общее количество объектов
  • createIndex(): создает новый индекс
  • delete(): удаляет из хранилища объект с определенным ключом
  • deleteIndex(): удаляет указанный индекс
  • get(): возвращает объект c указанным ключом
  • getKey(): возвращает ключ объекта
  • getAll(): возвращает все объекты из хранилища
  • getAllKeys(): возвращает ключи объектов
  • index(): возвращает индекс хранилища
  • openCursor(): используется для перебора хранилища объектов по первичному ключу с помощью курсора
  • openKeyCursor(): возвращает курсор для перебора хранилища объектов
  • put(): обновляет существующие объекты в хранилище

📜 Добавление данных (объектов в хранилище)

Для добавления объектов в хранилище базы данных IndexDB применяется метод add() объекта IDBObjectStore

add(value) add(value, key)

Первый параметр представляет добавляемое значение. Второй, необязательный параметр представляет свойства ключа - если не указан, то по умолчанию равен null.

Повторюсь, что надо ли указывать ключ, если указывать, то какой именно, зависит от параметров конфигурации, которые используются при создании хранилища:

  • keyPath: задает название свойства-ключа объекта
  • autoIncrement: при значении true значения ключей генерируются автоматически. По умолчанию значение false (ключи не генерируются)

Что именно и как используется в качестве ключа, определяется различными комбинациями параметров keyPath и autoIncrement:

keyPath autoIncrement Описание
Не указано false Хранилище объектов может хранить значения произвольного типа (в том числе значения примитивных типов как числа и строки). Однако ключ необходимо явно указывать каждый раз при добавлении нового объекта в базу данных
Указано false Хранилище объектов может хранить только объекты (т.е. никаких примитивных типов данных). Однако эти объекты должны иметь свойство с тем же именем, которое указанно в параметре keyPath
Не указано true Хранилище объектов может хранить значения произвольного типа (в том числе значения примитивных типов). Ключи объектов генерируются автоматически при добавлении объектов. Однако также можно явным образом указать ключ для объекта
Указано true Хранилище объектов может хранить только объекты (т.е. никаких примитивных типов данных). Ключи же называются также, как указано в параметре keyPath. Если у объекта это свойство отсутствует, то значение ключа генерируется автоматически и добавляется к новому объекту. Если же свойство уже существует в объекте, то ключ не генерируется, а в качестве ключа используется уже сохраненное значение

Результат метода add() представляет объект IDBRequest. В случае успешного добавления у этого объекта срабатывает событие success, а его свойство result содержит ключ добавленного объекта. В случае ошибки генерируется событие error вместе с исключением типа DOMException. Для обработки этих событий можно использовать соответственно свойства onsuccess и onerror

Например, добавим один объект:

const request = indexedDB.open("test", 5); // подключаемся к бд test // при создании или изменении версии базы данных создаем в ней хранилище users request.onupgradeneeded = (event) => { const db = event.target.result; // получаем БД // ключом является свойство id, и оно автоматически инкрементируется db.createObjectStore("users", { keyPath: "id", autoIncrement: true }); }; // при открытии базы данных добавляем в хранилище users 1 объект request.onsuccess = (event) => { const db = event.target.result; // получаем БД const transaction = db.transaction(["users"], "readwrite"); // создаем транзакцию const userStore = transaction.objectStore("users"); // получаем хранилище users const tom = {name: "Tom", age: 39}; const addRequest = userStore.add(tom); // добавляем объект tom в хранилище userStore addRequest.onsuccess = (event) => { console.log("Данные успешно добавлены"); console.log("id добавленной записи:", addRequest.result); // id добавленной записи: 1 }; };

Здесь в базе данных test создано хранилище "users", в котором в качестве ключа выступает свойство id, и это свойство автоматически инкрементируется при добавлении каждого нового объекта. То есть добавляемый объект в принципе может не содержать подобное свойство.

В данном случае добавляем один объект с двумя свойствами name и age:

const tom = {name: "Tom", age: 39}; const addRequest = userStore.add(tom);

При успешном добавлении addRequest.result содержит ключ - значение свойства id добавленного объекта

addRequest.onsuccess = (event) => { console.log("Данные успешно добавлены"); console.log("id добавленной записи:", addRequest.result); // id добавленной записи: 1 };

То же самое значение можно получить через параметр обработчика и его свойство event.target.result

console.log("id добавленной записи:", event.target.result);

Стоит отметить, что добавление данных не происходит немедленно после вызова метода add(), а завершается лишь через некоторое время. Поэтому для отслеживания операции применяется обработчик onsuccess.

добавим еще несколько объектов:

const bob = {name: "Bob", age: 43}; const sam = {name: "Sam", age: 28}; userStore.add(bob); userStore.add(sam);

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

Стоит отметить, что современные браузеры позволяют нам просмотреть содержимое IndexDB через инструменты разработчика. Так, в Google Chrome подобная функциональность доступна на вкладке Application при выборе в левом меню пункта Storage/IndexDB:

Еще пример:

const request = indexedDB.open('myDatabase', 1); request.onsuccess = (event) => { const db = event.target.result; // Создаём транзакцию const transaction = db.transaction(['users'], 'readwrite'); // Получаем хранилище const store = transaction.objectStore('users'); // Добавляем данные const addRequest = store.add({ name: 'Tom', age: 37, email: 'tom@example.com' }); addRequest.onsuccess = () => { console.log('Данные добавлены'); }; addRequest.onerror = () => { console.error('Ошибка добавления'); }; };

📜 Получение данных из IndexDB

Получение всех объектов из БД

Для получения всех объектов из хранилища объект IDBObjectStore предоставляет метод getAll()

getAll()

Этот метод возвращает объект IDBRequest. Если метод выполняется успешно, то для объекта IDBRequest генерируется событие success, а его свойство result содержит массив полученных данных из хранилища. В случае возникновения ошибки у объекта IDBRequest срабатывает событие error, а его свойство error содержит информацию об ошибке. Для обработки этих событий можно использовать соответственно свойства onsuccess и onerror

Например, получим все объекты из хранилища "users":

const request = indexedDB.open("test", 5); // подключаемся к БД "test" // при создании или изменении версии базы данных создаем в ней хранилище "users" request.onupgradeneeded = (event) => { const db = event.target.result; // получаем БД // ключом является свойство id, и оно автоматически инкрементируется const userStore = db.createObjectStore("users", { keyPath: "id", autoIncrement: true }); userStore.add({name: "Tom", age: 39}); userStore.add({name: "Bob", age: 43}); userStore.add({name: "Sam", age: 28}); }; // при открытии базы данных получаем все данные request.onsuccess = (event) => { const db = event.target.result; // получаем бд const transaction = db.transaction(["users"], "readwrite"); // создаем транзакцию const userStore = transaction.objectStore("users"); // получаем хранилище users const getRequest = userStore.getAll(); // получаем все объекты getRequest.onsuccess = (e) => { const users = getRequest.result; console.log(users); } getRequest.onerror = (e) => console.log(e.target.error.message); // выводим сообщение об ошибке };

Здесь в обработчике getRequest.onsuccess получаем извлеченные данные в константу users и выводим их на консоль:

getRequest.onsuccess = (e) => { const users = getRequest.result; console.log(users); }

Так, в моем случае в хранилище содержатся три объекта со свойствами id (ключ), name и age:

[
    { name: "Tom", age: 39, id: 1},
    { name: "Bob", age: 43, id: 2},
    { name: "Sam", age: 28, id: 3}
]

Поскольку результат представляет массив, то мы можем использовать индексы и получить какие-то определенные элементы массива или перебрать массив:

getRequest.onsuccess = (e) => { const users = getRequest.result; for(user of users){ console.log(`Name: ${user.name} Age: ${user.age}`); } }

Фильтрация по ключам

Дополнительная версия метода getAll() позволяет отфильтровать объекты или выбрать только те объекты, которые соответствуют определенному диапазону ключей:

getAll(query) getAll(query, count)

В качестве параметра query в метод передается ключ или объект IDBKeyRange, который задает диапазон ключей. Дополнительно параметр count позволяет задать максимальное количество элементов в выборке.

Например, мы можем передать значение ключа:

const getRequest = userStore.getAll(2); // получаем объекты, у которых свойство ключа равно 2 getRequest.onsuccess = (e) => { console.log(e.target.result); }

В данном случае получаем все элементы, у которых значение свойства-ключа равно 2.

Применение объекта IDBKeyRange предоставляет дополнительные возможности с помощью ряда статических методов:

  • IDBKeyRange.bound(): создает диапазон ключей, для которого задано минимальное и максимальное значения
  • IDBKeyRange.only(): создает диапазон ключей, который содержит только одно значение
  • IDBKeyRange.lowerBound(): создает диапазон ключей, для которого задано минимальное значение
  • IDBKeyRange.upperBound(): создает диапазон ключей, для которого задано максимальное значение

Диапазоны ключей, которые создаются с помощью этих методов, можно описать следующей таблицей:

Диапазон Код
Значение ключа >= x IDBKeyRange.lowerBound(x)
Значение ключа > x IDBKeyRange.lowerBound(x, true)
Значение ключа <= y IDBKeyRange.upperBound(y)
Значение ключа < y IDBKeyRange.upperBound(y, true)
Значение ключа >= x && <= y IDBKeyRange.bound(x, y)
Значение ключа > x < y IDBKeyRange.bound(x, y, true, true)
Значение ключа > x && <= y IDBKeyRange.bound(x, y, true, false)
Значение ключа >= x &&< y IDBKeyRange.bound(x, y, false, true)
Значение ключа = z IDBKeyRange.only(z)

Например, получаем все объекты, у которых значение ключа не больше 2:

const getRequest = userStore.getAll(IDBKeyRange.upperBound(2)); getRequest.onsuccess = () => { const users = getRequest.result; console.log(users); }

Еще пример:

const transaction = db.transaction(['users'], 'readonly'); const store = transaction.objectStore('users'); const getAllRequest = store.getAll(); getAllRequest.onsuccess = () => { const users = getAllRequest.result; console.log('Всего пользователей:', users.length); users.forEach(user => { console.log(user.name, user.age); }); };

Получение одного объекта по ключу

Для получения одного объекта по ключу применяется метод get(), в который передается ключ объекта:

get(key)

Результатом метода является объект IDBRequest. В случае успешного добавления у этого объекта срабатывает событие success, а его свойство result будет содержать объект с указанным ключом. В случае ошибки генерируется событие error вместе с исключением типа DOMException.

Например, получим объект с ключом 1:

const request = indexedDB.open("test", 5); // подключаемся к БД "test" // при создании или изменении версии базы данных создаем в ней хранилище "users" request.onupgradeneeded = (event) => { const db = event.target.result; // получаем БД // ключом является свойство id, и оно автоматически инкрементируется db.createObjectStore("users", { keyPath: "id", autoIncrement: true }); }; // при открытии базы данных получаем один объект request.onsuccess = (event) => { const db = event.target.result; // получаем БД const transaction = db.transaction(["users"], "readwrite"); // создаем транзакцию const userStore = transaction.objectStore("users"); // получаем хранилище users const getRequest = userStore.get(1); // получаем объект по ключу 1 getRequest.onsuccess = () => console.log(getRequest.result); // выводим полученный объект getRequest.onerror = (e) => console.log(e.target.error.message); // выводим сообщение об ошибке };

Результат в моем случае:

Object
    age: 39
    id: 1
    name: "Tom"
    [[Prototype]]: Object

Также извлеченный объект мы можем получить через параметр обработчика события - через его свойство event.target.result:

const getRequest = userStore.get(1); // получаем объект по ключу 1 getRequest.onsuccess = (e) => { const user = e.target.result; // полученный объект console.log(user.name); console.log(user.age); }

Если мы попробуем найти объект с несуществующим ключом, то свойство result будет равно undefined

Еще пример:

const transaction = db.transaction(['users'], 'readonly'); const store = transaction.objectStore('users'); // Получаем по ключу const getRequest = store.get(1); getRequest.onsuccess = () => { const user = getRequest.result; if (user) { console.log('Имя:', user.name); console.log('Возраст:', user.age); } else { console.log('Пользователь не найден'); } };

📜 Обновление данных (объектов хранилища)

Метод put() интерфейса IDBObjectStore обновляет запись в базе данных или вставляет новую запись, если данный элемент еще не существует.

put(item) put(item, key)

Первый параметр - item представляет значение, которое обновляется или добавляется в базу данных. Второй и необязательный параметр key представляет ключ, по которому надо обновить/добавить объект.

Метод put() возвращает объект IDBRequest. В случае успешного обновления/добавления у объекта IDBRequest вызывается событие success, а его свойство result будет содержать объект с указанным ключом. В случае ошибки генерируется событие error вместе с исключением типа DOMException. Для обработки этих событий можно использовать соответственно свойства onsuccess и onerror

Например, пусть хранилище users в базе данных test хранит следующие объекты:

[
    { name: "Tom", age: 39, id: 1},
    { name: "Bob", age: 43, id: 2},
    { name: "Sam", age: 28, id: 3}
]

Обновим объект в хранилище "users", у которого ключ равен 1:

const request = indexedDB.open("test", 5); // подключаемся к БД "test" // при создании или изменении версии базы данных создаем в ней хранилище "users" request.onupgradeneeded = (event) => { const db = event.target.result; // получаем БД // ключом является свойство id, и оно автоматически инкрементируется db.createObjectStore("users", { keyPath: "id", autoIncrement: true }); }; // при открытии базы данных изменяем в хранилище "users" 1 объект request.onsuccess = (event) => { const db = event.target.result; // получаем БД const transaction = db.transaction(["users"], "readwrite"); // создаем транзакцию const userStore = transaction.objectStore("users"); // получаем хранилище users // получаем объект c id=1 const getRequest = userStore.get(1); getRequest.onsuccess = () => { const user = getRequest.result; console.log(user); // меняем значения свойств user.name = "Tomas"; user.age = 22; // обновляем значения const updateRequest = userStore.put(user); updateRequest.onsuccess = () => console.log("Data successfully updated"); }; getRequest.onerror = (e) => console.log(e.target.error.message); // выводим сообщение об ошибке };

Другой пример:

const transaction = db.transaction(['users'], 'readwrite'); const store = transaction.objectStore('users'); // Обновляем объект (по ключу) const updateRequest = store.put({ id: 1, name: 'Tom', age: 38, // Изменили возраст email: 'tom.updated@example.com' }); updateRequest.onsuccess = () => { console.log('Данные обновлены'); };

📜 Получение количества объектов

Для получения количества данных, которые хранятся в хранилище базы данных, в IndexDB применяется метод count() интерфейса IDBObjectStore

count() count(query)

Если параметр не передается, то метод возвращает общее количество записей хранилища.

Этот метод возвращает объект IDBRequest. Если метод выполняется успешно, то для объекта IDBRequest генерируется событие success, а его свойство result содержит количество объектов хранилища. В случае возникновения ошибки у объекта IDBRequest срабатывает событие error, а его свойство error содержит информацию об ошибке. Для обработки этих событий можно использовать соответственно свойства onsuccess и onerror

Например, получим общее количество объектов из хранилища "users" базы данных test:

onst request = indexedDB.open("test", 5); // подключаемся к бд test // при создании или изменении версии базы данных создаем в ней хранилище "users" request.onupgradeneeded = (event) => { const db = event.target.result; // получаем БД // ключом является свойство id, и оно автоматически инкрементируется const userStore = db.createObjectStore("users", { keyPath: "id", autoIncrement: true }); // добавление начальных данных userStore.add({name: "Tom", age: 39}); userStore.add({name: "Bob", age: 43}); userStore.add({name: "Sam", age: 28}); }; // при открытии базы данных получаем количество объектов request.onsuccess = (event) => { const db = event.target.result; // получаем БД const transaction = db.transaction(["users"], "readwrite"); // создаем транзакцию const userStore = transaction.objectStore("users"); // получаем хранилище "users" const countRequest = userStore.count(); // получаем количество объектов в БД // при успешном получении выводим количество на консоль countRequest.onsuccess = () => console.log("users count:", countRequest.result); countRequest.onerror = () => console.log(countRequest.error.message); };

И консоль выведет:

users count: 3

В качестве необязательного параметра query в метод count() передается значение ключа или объект IDBKeyRange, который задает диапазон ключей. Дополнительно параметр count позволяет задать максимальное количество элементов в выборке.

Для создания диапазона ключей применяются следуюшие статические методы интерфейса IDBKeyRange:

  • IDBKeyRange.bound(): создает диапазон ключей, для которого задано минимальное и максимальное значения
  • IDBKeyRange.only(): создает диапазон ключей, который содержит только одно значение
  • IDBKeyRange.lowerBound(): создает диапазон ключей, для которого задано минимальное значение
  • IDBKeyRange.upperBound(): создает диапазон ключей, для которого задано максимальное значение

Диапазоны ключей, которые создаются с помощью этих методов, можно описать следующей таблицей:

Диапазон Код
Значение ключа >= x IDBKeyRange.lowerBound(x)
Значение ключа > x IDBKeyRange.lowerBound(x, true)
Значение ключа <= y IDBKeyRange.lowerBound(y)
Значение ключа < y IDBKeyRange.upperBound(y, true)
Значение ключа >= x && <= y IDBKeyRange.bound(x, y)
Значение ключа > x < y IDBKeyRange.bound(x, y, true, true)
Значение ключа > x && <= y IDBKeyRange.bound(x, y, true, false)
Значение ключа >= x &&< y IDBKeyRange.bound(x, y, false, true)
Значение ключа = z IDBKeyRange.only(z)

Например, получаем все объекты, у которых значение ключа не больше 2:

const countRequest = userStore.count(IDBKeyRange.upperBound(2)); countRequest.onsuccess = () => console.log("users count:", countRequest.result);

📜 Удаление данных из хранилища

Для удаления данных из хранилища в IndexDB применяются методы delete() и clear() объекта IDBObjectStore

Метод delete() удаляет все объекты по определенным ключам, которые передаются в качестве параметра:

delete(key)

Причем в качестве значения параметру можно передать конкретное значение либо диапазон ключей в виде объекта IDBKeyRange.

Этот метод возвращает объект IDBRequest. Если метод выполняется успешно, то для объекта IDBRequest генерируется событие success, а его свойство result содержит значение undefined. В случае возникновения ошибки у объекта IDBRequest срабатывает событие error, а его свойство error содержит информацию об ошибке. Для обработки этих событий можно использовать соответственно свойства onsuccess и onerror

Например, удалим объект, у которого ключ равен 1:

const request = indexedDB.open("test", 5); // подключаемся к БД "test" // при создании или изменении версии базы данных создаем в ней хранилище "users" request.onupgradeneeded = (event) => { const db = event.target.result; // получаем БД // ключом является свойство id, и оно автоматически инкрементируется const userStore = db.createObjectStore("users", { keyPath: "id", autoIncrement: true }); userStore.add({name: "Tom", age: 39}); userStore.add({name: "Bob", age: 43}); userStore.add({name: "Sam", age: 28}); }; // при открытии базы данных удаляем из хранилища users 1 объект request.onsuccess = (event) => { const db = event.target.result; // получаем БД const transaction = db.transaction(["users"], "readwrite"); // создаем транзакцию const userStore = transaction.objectStore("users"); // получаем хранилище "users" // удаляем все объекты, у которых ключ равен 1 const deleteRequest = userStore.delete(1); // при успешном получении выводим уведомление количество на консоль deleteRequest.onsuccess = () => console.log("Successfully deleted", deleteRequest.result); deleteRequest.onerror = () => console.log(deleteRequest.error); };

Для создания диапазона ключей применяются следуюшие статические методы интерфейса IDBKeyRange:

  • IDBKeyRange.bound(): создает диапазон ключей, для которого задано минимальное и максимальное значения
  • IDBKeyRange.only(): создает диапазон ключей, который содержит только одно значение
  • IDBKeyRange.lowerBound(): создает диапазон ключей, для которого задано минимальное значение
  • IDBKeyRange.upperBound(): создает диапазон ключей, для которого задано максимальное значение

Диапазоны ключей, которые создаются с помощью этих методов, можно описать следующей таблицей:

Диапазон Код
Значение ключа >= x IDBKeyRange.lowerBound(x)
Значение ключа > x IDBKeyRange.lowerBound(x, true)
Значение ключа <= y IDBKeyRange.lowerBound(y)
Значение ключа < y IDBKeyRange.upperBound(y, true)
Значение ключа >= x && <= y IDBKeyRange.bound(x, y)
Значение ключа > x < y IDBKeyRange.bound(x, y, true, true)
Значение ключа > x && <= y IDBKeyRange.bound(x, y, true, false)
Значение ключа >= x &&< y IDBKeyRange.bound(x, y, false, true)
Значение ключа = z IDBKeyRange.only(z)

Например, удалим все объекты, у которых значение ключа не больше 2:

const deleteRequest = userStore.delete(IDBKeyRange.upperBound(2)); deleteRequest.onsuccess = () => console.log("Successfully deleted");

Удаление одной записи

const transaction = db.transaction(['users'], 'readwrite'); const store = transaction.objectStore('users'); // Удаляем по ключу const deleteRequest = store.delete(1); deleteRequest.onsuccess = () => { console.log('Запись удалена'); };

Удаление всех записей

Для удаления абсолютно всех данных из хранилища базы данных применяется метод clear(). Он не принимает никаких параметров и возвращает объект IDBRequest. При успешном выполнении свойство result у IDBRequest также равно undefined

// удаляем все объекты const deleteRequest = userStore.clear(); deleteRequest.onsuccess = () => console.log("Successfully deleted");

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

const transaction = db.transaction(['users'], 'readwrite'); const store = transaction.objectStore('users'); // Очистка всего хранилища const clearRequest = store.clear(); clearRequest.onsuccess = () => { console.log('Все записи удалены'); };

📜 Курсоры

Для получения объектов из хранилища базы данных также можно применять курсоры. В контексте базы данных курсор представляет механизм прохода по различным объектам в хранилище базы данных.

Для создания курсора у объекта IDBObjectStore используется метод openCursor():

openCursor() openCursor(query) openCursor(query, direction)

В качестве необязательного параметра query в метод передается ключ или объект IDBKeyRange, который задает диапазон ключей. В этом случае курсор будет проходить только по объектам с этими ключами. Если данный параметр не указан, то курсор проходит по всем объектам.

Третий параметр - direction задает направление курсора и может принимать следующие значения:

  • next: курсор начинает проход по объектам в начале хранилища в порядке возрастания ключей. Курсор возвращает все записи из хранилища, в том числе дубликаты. Это значение по умолчанию
  • nextunique: курсор начинает проход по объектам в начале хранилища в порядке возрастания ключей. Курсор возвращает все записи из хранилища, кроме дубликатов
  • prev: курсор начинает проход по объектам в начале хранилища в порядке убывания ключей. Курсор возвращает все записи из хранилища, в том числе дубликаты. Это значение по умолчанию
  • prevunique: курсор начинает проход по объектам в начале хранилища в порядке убывания ключей. Курсор возвращает все записи из хранилища, кроме дубликатов

Метод openCursor() возвращает объект IDBRequest. При успешном получении курсора у IDBRequest срабатывает событие success, а его свойство result представляет либо значение IDBCursorWithValue (если курсор нашел объекты в хранилище), либо null (если для курсора нет объектов). Если курсор не удалось получить, то генерируется событие error, а свойство error объекта IDBRequest хранит информацию об ошибке. Для обработки этих событий можно использовать соответственно свойства onsuccess и onerror

При успешном открытии курсора и наличии в хранилище объектов для перебора свойство result объекта IDBRequest хранит значение IDBCursorWithValue - это и есть непосредственно сам курсор:

const request = indexedDB.open("test", 6); // подключаемся к БД "test" // при создании или изменении версии базы данных создаем в ней хранилище "users" request.onupgradeneeded = (event) => { const db = event.target.result; // получаем БД // пересоздаем хранилище users - сначала удаляем, если оно существует db.deleteObjectStore("users"); // ключом является свойство id, и оно автоматически инкрементируется const userStore = db.createObjectStore("users", { keyPath: "id", autoIncrement: true }); userStore.add({name: "Tom", age: 39}); userStore.add({name: "Bob", age: 43}); userStore.add({name: "Sam", age: 28}); }; // при открытии базы данных получаем курсор request.onsuccess = (event) => { const db = event.target.result; // получаем БД const transaction = db.transaction(["users"]); // создаем транзакцию const userStore = transaction.objectStore("users"); // получаем хранилище "users" const cursorRequest = userStore.openCursor(); // получаем запрос на открытие курсора // получаем курсор cursorRequest.onsuccess = () => { const cursor = cursorRequest.result; // также можно получить через event.target.result console.log(cursor); } cursorRequest.onerror = () => console.log(cursorRequest.error); };

При успешном получении курсора свойство key объекта IDBCursorWithValue будет содержать ключ первого объекта из хранилища, а свойство value содержит сам объект:

request.onsuccess = (event) => { const db = event.target.result; // получаем БД const transaction = db.transaction(["users"]); // создаем транзакцию const userStore = transaction.objectStore("users"); // получаем хранилище users const cursorRequest = userStore.openCursor(); // получаем запрос на открытие курсора // получаем курсор cursorRequest.onsuccess = () => { const cursor = cursorRequest.result; // также можно получить через event.target.result const user = cursor.value; // получаем значение, на которое указывает курсор console.log(user.id); // он же cursor.key console.log(user.name); console.log(user.age); } };

Метод continue() заставляет курсор перемещаться к следующей записи (если она есть), что, в свою очередь, приводит к повторному выполнению обработчика onsuccess и так далее. Например, получим все объекты из хранилища users:

const request = indexedDB.open("test", 6); // подключаемся к БД "test" // при создании или изменении версии базы данных создаем в ней хранилище "users" request.onupgradeneeded = (event) => { const db = event.target.result; // получаем БД // пересоздаем хранилище users - сначала удаляем, если оно существует db.deleteObjectStore("users"); // ключом является свойство id, и оно автоматически инкрементируется const userStore = db.createObjectStore("users", { keyPath: "id", autoIncrement: true }); userStore.add({name: "Tom", age: 39}); userStore.add({name: "Bob", age: 43}); userStore.add({name: "Sam", age: 28}); }; // при открытии базы данных request.onsuccess = (event) => { const db = event.target.result; // получаем БД const transaction = db.transaction(["users"]); // создаем транзакцию const userStore = transaction.objectStore("users"); // получаем хранилище "users" const cursorRequest = userStore.openCursor(); // получаем запрос на открытие курсора const users = []; // массив, в который считываем данные // получаем курсор cursorRequest.onsuccess = () => { const cursor = cursorRequest.result; // также можно получить через event.target.result if(cursor){ // если еще есть данные для чтения const user = cursor.value; users.push(user); // добавляем полученный объект в массив cursor.continue(); // перемещаем курсор к новой записи } else{ console.log(users); // если записей для чтения больше нет, выводит массив } } cursorRequest.onerror = () => console.log(cursorRequest.error); };

В данном случае, пока для считывания имеются данные, считываем их и добавляем в массив users. Когда все данные будут прочитаны выводим массив на консоль.

📜 Полный пример: Менеджер задач

class TaskManager { constructor() { this.db = null; this.init(); } async init() { return new Promise((resolve, reject) => { const request = indexedDB.open('TaskManager', 1); request.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains('tasks')) { const store = db.createObjectStore('tasks', { keyPath: 'id', autoIncrement: true }); // Создаём индекс для поиска по статусу store.createIndex('status', 'status', { unique: false }); } }; request.onsuccess = (event) => { this.db = event.target.result; resolve(this.db); }; request.onerror = () => reject(request.error); }); } async addTask(title, description) { return new Promise((resolve, reject) => { const transaction = this.db.transaction(['tasks'], 'readwrite'); const store = transaction.objectStore('tasks'); const task = { title, description, status: 'pending', created: new Date() }; const request = store.add(task); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async getAllTasks() { return new Promise((resolve, reject) => { const transaction = this.db.transaction(['tasks'], 'readonly'); const store = transaction.objectStore('tasks'); const request = store.getAll(); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async updateTask(id, updates) { return new Promise((resolve, reject) => { const transaction = this.db.transaction(['tasks'], 'readwrite'); const store = transaction.objectStore('tasks'); const getRequest = store.get(id); getRequest.onsuccess = () => { const task = getRequest.result; Object.assign(task, updates); const updateRequest = store.put(task); updateRequest.onsuccess = () => resolve(); updateRequest.onerror = () => reject(updateRequest.error); }; }); } async deleteTask(id) { return new Promise((resolve, reject) => { const transaction = this.db.transaction(['tasks'], 'readwrite'); const store = transaction.objectStore('tasks'); const request = store.delete(id); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }); } } // Использование (async () => { const manager = new TaskManager(); await manager.init(); // Добавить задачу await manager.addTask('Купить молоко', 'В магазине на углу'); await manager.addTask('Написать код', 'Глава 24 про IndexedDB'); // Получить все задачи const tasks = await manager.getAllTasks(); console.log('Задачи:', tasks); // Обновить задачу await manager.updateTask(1, { status: 'completed' }); // Удалить задачу await manager.deleteTask(2); })();

📜 Сравнение: localStorage vs IndexedDB

Характеристика localStorage IndexedDB
Объём 5-10 МБ Сотни МБ/ГБ
Тип данных Только строки Любые (объекты, массивы, blob)
API Синхронный Асинхронный
Поиск Только по ключу По ключу и индексам
Транзакции Нет Да
Сложность Простой Сложнее
Применение Настройки, токены Offline приложения, кэш

Лучшие практики

✅ Рекомендации
  • Версионирование — всегда указывайте версию БД
  • Транзакции — группируйте связанные операции
  • Обработка ошибок — всегда обрабатывайте onerror
  • Promise обёртки — создавайте для удобства работы
  • Индексы — создавайте для полей, по которым ищете
  • Закрытие соединений — db.close() когда закончили

Заключение

📚 Основные выводы
  • IndexedDB — мощная NoSQL база данных в браузере
  • Асинхронный API — не блокирует интерфейс
  • Транзакции — гарантия целостности данных
  • Основные операции: add(), get(), getAll(), put(), delete(), clear()
  • Хранилища — аналог таблиц в SQL
  • Применение — offline приложения, большие объёмы данных

Глава 25. Drag-and-Drop API

📜 Введение. Перетаскивание элементов с помощью Drag-and-Drop API

Drag-and-Drop API позволяет переносить различные элементы мышью на определенную позицию на веб-странице. При перемещении элементов у нас есть источник перемещения - элемент, который перемещаем мышью, и цель перемещения - целевая область на веб-странице (другой элемент), на которую надо переместить источник перемещения.

💡 Основные понятия
  • Источник перетаскивания (Draggable element) — элемент, который можно перемещать
  • Целевая область (Drop zone) — область, на которую можно поместить перетаскиваемый элемент
  • DragEvent — объект события, содержащий информацию о процессе перетаскивания

📜 Атрибут draggable

Чтобы определить элемент на веб-странице, который можно перемещать (источник перетаскивания), нужно для этого элемента определить атрибут draggable со значением true. Теоретически в качестве перетаскиваемого элемента может выступать любой элемент веб-страницы.

<div style="width:50px;height:50px; background-color: red;" draggable="true"></div>
⚠️ Важно

По умолчанию элементы не являются перетаскиваемыми. Только изображения, ссылки и выделенный текст можно перетаскивать без явного указания атрибута draggable="true".

📜 События Drag-and-Drop

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

События на перетаскиваемом элементе

Событие Описание Когда срабатывает
dragstart Начало перетаскивания Пользователь начинает перетаскивать элемент
drag Процесс перетаскивания Постоянно во время перетаскивания
dragend Завершение перетаскивания Пользователь отпускает элемент

События на целевой области

Событие Описание Когда срабатывает
dragenter Вход в целевую область Элемент входит в границы целевой области
dragover Перемещение над целью Постоянно (несколько раз в секунду) над целевой областью
dragleave Выход из целевой области Элемент покидает целевую область
drop Сброс элемента Элемент отпускается на целевой области
💡 Объект DragEvent

Обработчики всех выше перечисленных событий перемещения в качестве параметра получают объект типа DragEvent. Этот тип наследует свойства от MouseEvent и соответственно типа Event.

📜 Базовый пример перетаскивания

Обработчик события dragstart определяется для перетаскиваемого элемента, а обработчики остальных событий определяются для области, на которую надо переместить элемент.

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Drag and Drop Example</title> <style> #source { width: 50px; height: 50px; background-color: red; display: inline-block; } #target { width: 200px; height: 150px; overflow: hidden; border: #ccc 1px dashed; } div { margin: 5px; } </style> </head> <body> <div id="source" draggable="true"></div> <div id="target"></div> <script> const source = document.getElementById("source"); source.addEventListener("dragstart", () => { console.log("Drag operation started"); }); const target = document.getElementById("target"); target.addEventListener("dragover", (event) => { event.preventDefault(); // Разрешаем сброс console.log("Dragover operation"); }); target.addEventListener("drop", () => { console.log("Drag operation finished"); }); </script> </body> </html>

В данном случае перемещаемый элемент имеет идентификатор source, и для него регистрируется обработчик события "dragstart". Оно будет возникать, когда мы захватим элемент указателем мыши и начнем перемещать.

Область, на которую перемещаем элемент, представляет другой элемент с идентификатором target. Для демонстрации для него регистрируем обработчики событий "dragover" и "drop". Событие "dragover" будет возникать, когда элемент item будет перемещаться поверх элемента target.

⚠️ Важно: event.preventDefault()

Чтобы предупредить генерацию события drop во время перемещения, в обработчике события dragover обязательно нужно вызывать метод event.preventDefault(). Без этого событие drop не сработает!

Когда мы отпустим элемент item на элемент target, будет сгенерировано событие "drop".

📜 Копирование элемента

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

<script> let dragged = null; // Перемещенные данные // Источник перемещения const source = document.getElementById("source"); // В обработчике устанавливаем ссылку на перетаскиваемый элемент source.addEventListener("dragstart", (e) => { dragged = e.target; }); // Целевая область перемещения const target = document.getElementById("target"); // Предупреждаем событие drop target.addEventListener("dragover", (e) => { e.preventDefault(); }); // Копируем перетаскиваемый элемент и помещаем его копию на целевую область target.addEventListener("drop", (e) => { e.target.appendChild(dragged.cloneNode()); }); </script>

Здесь при начале перетаскивания мы сохраняем перемещаемый объект в переменную dragged.

source.addEventListener("dragstart", (e) => dragged = e.target);

При окончании перетаскивания помещаем копию элемента source на элемент target с помощью метода cloneNode().

target.addEventListener("drop", (e) => e.target.appendChild(dragged.cloneNode()));

Таким образом, при перетаскивании на область target будут добавляться копии элемента source:

📜 Полное перемещение элемента

Если нужно не копировать, а полностью переместить элемент, нужно сначала удалить его из текущего контейнера, а затем добавить на целевую область:

<script> let dragged = null; const source = document.getElementById("source"); source.addEventListener("dragstart", (e) => { dragged = e.target; }); const target = document.getElementById("target"); target.addEventListener("dragover", (e) => { e.preventDefault(); }); // Полностью перемещаем элемент target.addEventListener("drop", (e) => { // Удаляем элемент из родительского контейнера dragged.parentNode.removeChild(dragged); // Добавляем на целевую область e.target.appendChild(dragged); }); </script>

Здесь в обработчике "drop" сначала удаляем перетаскиваемый элемент из родительского контейнера (в данном случае элемента body), а затем добавляем его на целевую область:

target.addEventListener("drop", (e) => { dragged.parentNode.removeChild(dragged); e.target.appendChild(dragged); });
✅ Копирование vs Перемещение
  • cloneNode() — создает копию элемента, оригинал остается на месте
  • parentNode.removeChild() + appendChild() — полностью перемещает элемент

📜 Установка и получение перетаскиваемых данных с помощью объекта DataTransfer

При перетаскивании элементов в обработчик событий перетаскивания передается объект типа DragEvent. Этот тип наследует свойства от MouseEvent и соответственно типа Event, но в дополнение к ним также определяет свойство dataTransfer. Это свойство представляет перетаскиваемые данные в виде объекта DataTransfer.

Тип DataTransfer определяет ряд свойств, которые позволяют получить информацию о получаемых данных или настроить их перетаскивание:

  • dropEffect: получает или устанавливает тип операции перетаскивания. Может принимать значения:
    • copy: создается копия перетаскиваемых данных, и эта копия помещается на новую позицию
    • move: данные полностью перемещаются на новую позицию
    • link: создается ссылка на источник данных
    • none: данные не перетскиваются
  • effectAllowed: устанавливает возможные типы операций. Может принимать следующие значения
    • none: элемент не перетаскивается
    • copy: элемент может копироваться на новую позицию
    • copyLink: допустимо копирование элемента или создание ссылки на него
    • copyMove: допустимо копирование или перемещение элемента
    • link: допустимо создание ссылки на перетаскиваемый элемент
    • linkMove: допустимо перемещение элемента или создание ссылки на него
    • move: допустимо перемещение элемента на новую позицию
    • all: все операции допустимы
    • uninitialized: значение по умолчанию, если это свойство не установлено. Эквивалентно all
  • files: содержит список всех локальных файлов, доступных для передачи данных. Если операция перетаскивания не предполагает перетаскивание файлов, это свойство представляет собой пустой список.
  • items: предоставляет объект DataTransferItemList, который представляет собой список всех данных перетаскивания.
  • types: массив строк, задающих форматы, заданные в событии перетаскивания.

Свойства DataTransfer

Свойство Описание
dropEffect Получает или устанавливает тип операции перетаскивания: copy, move, link, none
effectAllowed Устанавливает возможные типы операций: none, copy, move, link, all и комбинации
files Содержит список всех локальных файлов, доступных для передачи данных
items Предоставляет объект DataTransferItemList — список всех данных перетаскивания
types Массив строк, задающих форматы данных в событии перетаскивания

Методы DataTransfer

  • clearData(): удаляет данные, связанные с объектом DataTransfer

  • getData(format): извлекает данные объекта DataTransfer. В качестве параметра передается формат данных. Возвращаются данные указанного формата. Если данные указанного формата не установлены, возвращает пустую строку

  • setData(format, data): устанавливает для объекта DataTransfer данные data, которые относятся к формату format. Если в DataTransfer уже есть данные указанного формата, то новые данные заменяют те, которые имелись ранее.

  • setDragImage(imgElement, xOffset, yOffset): устанавливает изображение, применяемое при перетаскивании. Первый параметр - imgElement представляет элемент <img>, используемый в качестве источника изображения. А параметры xOffset, yOffset задают соответственно смещения внутри изображения по оси x и y

Метод Описание
setData(format, data) Устанавливает данные для перетаскивания с указанным форматом (например, "text/html", "text/plain")
getData(format) Извлекает данные указанного формата. Возвращает пустую строку, если данных нет
clearData() Удаляет все данные, связанные с объектом DataTransfer
setDragImage(img, x, y) Устанавливает изображение для перетаскивания с указанными смещениями

📜 Использование setData() и getData()

Методы setData() и getData() позволяют нам легко установить и получить нужные данные при перетаскивании элементов.

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>DataTransfer Example</title> <style> #target { width: 200px; height: 150px; border: #ccc 1px dashed; } #target.dragover { border-color: #000; } .item { width: 50px; height: 50px; display: inline-block; margin: 5px; } </style> </head> <body> <div class="item" style="background-color: red;" draggable="true"></div> <div class="item" style="background-color: blue;" draggable="true"></div> <div id="target"></div> <script> const items = document.getElementsByClassName("item"); // Устанавливаем обработчик перетаскивания элемента for (item of items) { item.addEventListener("dragstart", (e) => { // В качестве перетаскиваемых данных устанавливаем HTML-код элемента e.dataTransfer.setData("text/html", e.target.outerHTML); }); } const target = document.getElementById("target"); target.addEventListener("dragover", (e) => e.preventDefault()); // При заходе и выходе из целевой области меняем класс target.addEventListener("dragenter", (e) => { e.target.classList.add("dragover"); }); target.addEventListener("dragleave", (e) => { e.target.classList.remove("dragover"); }); // При отпускании элемента добавляем его на целевую область target.addEventListener("drop", (e) => { e.srcElement.innerHTML += e.dataTransfer.getData("text/html"); e.target.classList.remove("dragover"); }); </script> </body> </html>

Разбор примера

Перетаскиваемые элементы здесь определены с классом item - это синий и красный квадраты. Перетаскивание осуществляется на элемент <div id="target">

1. Установка данных при dragstart:

Сначала регистрируется обработчик события dragstart для всех перемещаемых элементов item. В этом обработчике через параметр и его свойство dataTransfer можно получить объект DataTransfer:

const items = document.getElementsByClassName("item"); // устанавливаем обработчик перетаскивания элемента for (item of items) { item.addEventListener("dragstart", (e) => { // в качестве перетаскиваемых данных устанавливаем html-код элемента e.dataTransfer.setData("text/html", e.target.outerHTML); }); }

Объект DataTransfer представляет данные, которые перетаскиваются. Эти данные можно определить с помощью метода setData():

e.dataTransfer.setData("text/html", event.target.outerHTML);

Здесь e.target представляет перемещаемый элемент (у которого установлен атрибут draggable). А e.target.outerHTML представляет html-код этого элемента. То есть таким образом мы будем перемещать html-код, а перемещаемое содержимое будет иметь тип "text/html"

2. Получение данных при drop:

На целевой области перетаскивания (в элементе target) эти данные затем можно получить с помощью метода getData(). В примере выше это делается в обработчике события drop, когда пользователь отпустил перетаскиваемый элемент на целевую область:

target.addEventListener("drop", (e) => { // Получаем HTML-код и добавляем в целевую область e.srcElement.innerHTML += e.dataTransfer.getData("text/html"); e.target.classList.remove("dragover"); });

В данном случае мы берем перетаскиваемые данные (html-код элемента) и добавляем их в элемент target.

3. Визуальная обратная связь:

Кроме того, для визуального эффекта, когда перетаскиваемый элемент пересекает границу целевой области, переключаем класс loading:

// При входе в целевую область добавляем класс target.addEventListener("dragenter", (e) => { e.target.classList.add("dragover"); }); // При выходе из целевой области удаляем класс target.addEventListener("dragleave", (e) => { e.target.classList.remove("dragover"); });

Таким образом, мы сможем перемещать элементы item на элемент target:

💡 Форматы данных

В качестве формата данных можно использовать:

  • "text/html" — HTML-код
  • "text/plain" — обычный текст
  • "text/uri-list" — список URL
  • "application/json" — JSON-данные

📜 Типы операций dropEffect

Свойство dropEffect определяет тип операции перетаскивания:

Значение Описание Визуальная подсказка
copy Создается копия элемента Курсор со знаком "+"
move Элемент перемещается Обычный курсор перетаскивания
link Создается ссылка на источник Курсор со стрелкой ссылки
none Операция запрещена Курсор "запрещено"

📜 Типы операций effectAllowed

Свойство effectAllowed устанавливает возможные типы операций:

Значение Описание
none Элемент не перетаскивается
copy Элемент может копироваться на новую позицию
copyLink Допустимо копирование элемента или создание ссылки на него
copyMove Допустимо копирование или перемещение элемента
link Допустимо создание ссылки на перетаскиваемый элемент
linkMove Допустимо перемещение элемента или создание ссылки на него
move Допустимо перемещение элемента на новую позицию
all Все операции допустимы
uninitialized Значение по умолчанию (эквивалентно all)

📜 Практический пример: сортировка списка

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Sortable List</title> <style> .list { width: 300px; border: 2px solid #ccc; padding: 10px; } .list-item { padding: 10px; margin: 5px 0; background: #f0f0f0; border: 1px solid #ddd; cursor: move; } .list-item.dragging { opacity: 0.5; } .list-item.drag-over { border-top: 3px solid #0066cc; } </style> </head> <body> <div class="list" id="sortableList"> <div class="list-item" draggable="true">Элемент 1</div> <div class="list-item" draggable="true">Элемент 2</div> <div class="list-item" draggable="true">Элемент 3</div> <div class="list-item" draggable="true">Элемент 4</div> </div> <script> let draggedItem = null; const items = document.querySelectorAll('.list-item'); items.forEach(item => { item.addEventListener('dragstart', (e) => { draggedItem = e.target; e.target.classList.add('dragging'); }); item.addEventListener('dragend', (e) => { e.target.classList.remove('dragging'); }); item.addEventListener('dragover', (e) => { e.preventDefault(); }); item.addEventListener('dragenter', (e) => { if (e.target.classList.contains('list-item')) { e.target.classList.add('drag-over'); } }); item.addEventListener('dragleave', (e) => { if (e.target.classList.contains('list-item')) { e.target.classList.remove('drag-over'); } }); item.addEventListener('drop', (e) => { e.preventDefault(); if (e.target.classList.contains('list-item')) { e.target.classList.remove('drag-over'); // Вставляем перетаскиваемый элемент перед целевым const list = document.getElementById('sortableList'); list.insertBefore(draggedItem, e.target); } }); }); </script> </body> </html>
✅ Объяснение примера сортировки
  • dragstart — сохраняем ссылку на перетаскиваемый элемент, добавляем класс для визуального эффекта
  • dragend — убираем класс после завершения перетаскивания
  • dragenter/dragleave — добавляем/убираем визуальную подсказку (синяя линия сверху)
  • drop — вставляем элемент с помощью insertBefore()

📜 Перетаскивание файлов

Drag-and-Drop API также позволяет перетаскивать файлы из файловой системы на веб-страницу. Для этого используется свойство files объекта DataTransfer.

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>File Drop Example</title> <style> #dropZone { width: 400px; height: 200px; border: 3px dashed #ccc; display: flex; align-items: center; justify-content: center; font-size: 18px; color: #999; } #dropZone.dragover { border-color: #0066cc; background: #e7f3ff; color: #0066cc; } #fileInfo { margin-top: 20px; font-family: monospace; } </style> </head> <body> <div id="dropZone">Перетащите файлы сюда</div> <div id="fileInfo"></div> <script> const dropZone = document.getElementById('dropZone'); const fileInfo = document.getElementById('fileInfo'); dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('dragover'); }); dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('dragover'); }); dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.classList.remove('dragover'); // Получаем список файлов const files = e.dataTransfer.files; // Выводим информацию о файлах let info = '<h3>Загружено файлов: ' + files.length + '</h3>'; for (let i = 0; i < files.length; i++) { const file = files[i]; info += `<p> <strong>Файл ${i + 1}:</strong><br> Имя: ${file.name}<br> Размер: ${(file.size / 1024).toFixed(2)} KB<br> Тип: ${file.type || 'неизвестен'} </p>`; } fileInfo.innerHTML = info; }); </script> </body> </html>
💡 Работа с файлами

Свойство e.dataTransfer.files возвращает объект FileList, содержащий информацию о перетаскиваемых файлах. Каждый файл имеет свойства:

  • name — имя файла
  • size — размер файла в байтах
  • type — MIME-тип файла
  • lastModified — время последнего изменения

📜 Чтение содержимого файла

Для чтения содержимого перетаскиваемых файлов используется FileReader API:

<script> dropZone.addEventListener('drop', (e) => { e.preventDefault(); const files = e.dataTransfer.files; // Обрабатываем первый файл if (files.length > 0) { const file = files[0]; const reader = new FileReader(); // Обработчик загрузки файла reader.onload = (event) => { console.log('Содержимое файла:', event.target.result); fileInfo.innerHTML = '<pre>' + event.target.result + '</pre>'; }; // Читаем файл как текст reader.readAsText(file); // Альтернативы: // reader.readAsDataURL(file); // для изображений // reader.readAsArrayBuffer(file); // для бинарных данных } }); </script>

📜 Установка пользовательского изображения при перетаскивании

Метод setDragImage() позволяет установить пользовательское изображение, отображаемое при перетаскивании:

<script> const source = document.getElementById('source'); source.addEventListener('dragstart', (e) => { // Создаем изображение для перетаскивания const img = new Image(); img.src = 'icon.png'; // Устанавливаем изображение со смещением (50px, 50px) e.dataTransfer.setDragImage(img, 50, 50); // Устанавливаем данные e.dataTransfer.setData('text/plain', 'Перетаскиваемый элемент'); }); </script>
💡 Параметры setDragImage()
  • imgElement — элемент изображения (можно использовать существующий элемент <img> или Canvas)
  • xOffset — смещение по горизонтали (в пикселях)
  • yOffset — смещение по вертикали (в пикселях)

📜 Полный пример: канбан-доска

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Канбан доска</title> <style> .board { display: flex; gap: 20px; padding: 20px; } .column { flex: 1; background: #f0f0f0; border-radius: 8px; padding: 10px; min-height: 400px; } .column h3 { text-align: center; margin-bottom: 10px; } .card { background: white; padding: 15px; margin: 10px 0; border-radius: 4px; cursor: move; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .card.dragging { opacity: 0.5; } .column.drag-over { background: #e7f3ff; border: 2px dashed #0066cc; } </style> </head> <body> <div class="board"> <div class="column" data-status="todo"> <h3>К выполнению</h3> <div class="card" draggable="true">Задача 1</div> <div class="card" draggable="true">Задача 2</div> <div class="card" draggable="true">Задача 3</div> </div> <div class="column" data-status="in-progress"> <h3>В процессе</h3> </div> <div class="column" data-status="done"> <h3>Готово</h3> </div> </div> <script> let draggedCard = null; // Обработчики для карточек document.addEventListener('dragstart', (e) => { if (e.target.classList.contains('card')) { draggedCard = e.target; e.target.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; } }); document.addEventListener('dragend', (e) => { if (e.target.classList.contains('card')) { e.target.classList.remove('dragging'); } }); // Обработчики для колонок document.addEventListener('dragover', (e) => { if (e.target.classList.contains('column')) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; } }); document.addEventListener('dragenter', (e) => { if (e.target.classList.contains('column')) { e.target.classList.add('drag-over'); } }); document.addEventListener('dragleave', (e) => { if (e.target.classList.contains('column')) { e.target.classList.remove('drag-over'); } }); document.addEventListener('drop', (e) => { if (e.target.classList.contains('column')) { e.preventDefault(); e.target.classList.remove('drag-over'); // Перемещаем карточку в новую колонку e.target.appendChild(draggedCard); console.log(`Карточка перемещена в: ${e.target.dataset.status}`); } }); </script> </body> </html>

📜 Советы и лучшие практики

✅ Рекомендации
  • Всегда вызывайте e.preventDefault() в обработчике dragover, иначе событие drop не сработает
  • Используйте визуальную обратную связь — изменяйте стили при dragenter/dragleave
  • Устанавливайте effectAllowed и dropEffect для правильного отображения курсора
  • Очищайте данные с помощью clearData() при необходимости
  • Используйте dataTransfer для передачи сложных данных между элементами
⚠️ Частые ошибки
  • Забывать e.preventDefault() в dragover
  • Не устанавливать атрибут draggable="true"
  • Пытаться получить dataTransfer.files в событии dragover (доступно только в drop)
  • Не учитывать всплытие событий при работе с вложенными элементами

Поддержка браузерами

Drag-and-Drop API поддерживается всеми современными браузерами:

  • Chrome/Edge: полная поддержка
  • Firefox: полная поддержка
  • Safari: полная поддержка
  • Opera: полная поддержка
  • IE11: базовая поддержка (устаревший браузер)
💡 Мобильные устройства

Нативный Drag-and-Drop API не поддерживается на мобильных устройствах (сенсорные экраны). Для мобильной поддержки используйте:

  • Touch Events API (события touchstart, touchmove, touchend)
  • Библиотеки типа SortableJS, Draggable
  • Полифиллы для drag-and-drop на touch-устройствах

Резюме

В этой главе мы рассмотрели:

  • ✅ Атрибут draggable для создания перетаскиваемых элементов
  • ✅ События drag-and-drop: dragstart, drag, dragend, dragenter, dragover, dragleave, drop
  • ✅ Объект DataTransfer и его методы setData()/getData()
  • ✅ Свойства dropEffect и effectAllowed
  • ✅ Перетаскивание файлов и работу со свойством files
  • ✅ Практические примеры: сортировка списков, канбан-доска, загрузка файлов

Глава 26. File API

📜 Введение в File API

File API позволяет с помощью JavaScript считывать локальные файлы. Для этого применяются следующие интерфейсы:

Интерфейс Описание
File Представляет один файл и содержит информацию о файле (имя, дата изменения и т.д.)
FileList Представляет список объектов File
Blob Представляет бинарные данные
FileReader Предоставляет методы для считывания объектов типа File и Blob
⚠️ Безопасность

Чтобы получить доступ к содержимому файла, пользователь должен сначала выбрать соответствующие файлы. Это гарантирует, что произвольные файлы не могут быть прочитаны незаметно с помощью JavaScript.

Способы выбора файлов

Есть два основных способа выбора файлов для обработки:

  • Через элемент <input type="file"> — диалоговое окно выбора файлов
  • Drag-and-Drop — перетаскивание файлов на определенную область веб-страницы

📜 Выбор файлов через <input>

Чтобы получить доступ к локальным файлам через File API, пользователь должен сначала выбрать соответствующие файлы. Это гарантирует, что произвольные файлы не могут быть прочитаны незаметно с помощью JavaScript. Один из вариантов предоставления пользователям выбора файла — через элемент <input type="file">. Например:

<input type="file" id="files" name="files[]" multiple />

Если пользователь выбирал один или несколько файлов, то у элемента <input> генерируется событие change. Используя обработчик этого события, мы можем получить выбранные файлы.

Пример: вывод информации о файлах

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>File API Example</title> </head> <body> <input type="file" id="files" name="files[]" multiple /> <div id="list"></div> <script> function printFiles(e) { const files = e.target.files; // получаем все выбранные файлы let output = ""; for (let i = 0; i < files.length; i++) { // Перебираем все выбранные файлы const file = files[i]; // Получаем файл console.log(file); output += "<li><p><strong>" + file.name + "</strong></p>"; output += "<p>Type: " + file.type || "n/a</p>"; output += "<p>Size: " + file.size + " bytes</p>"; output += "<p>Changed on: " + file.lastModifiedDate.toLocaleDateString() + "</p></li>"; } document.getElementById("list").innerHTML = "<ul>" + output + "</ul>"; } document.getElementById("files").addEventListener("change", printFiles); </script> </body> </html>

Здесь на странице расположен элемент для выбора файлов, а также элемент <div> для вывода информации о выбранных файлах.

В коде JavaScript вешаем на событие change элемента выбора файлов функцию-обработчик printFiles. В этой функции через параметр получаем выбранные файлы:

const files = e.target.files;

Значение e.target.files представляет объект FileList - своего рода массив файлов, где каждый файл представлен объектом File.

Свойства объекта File

Свойство Описание
name Имя файла
type MIME-тип файла (например, "image/png", "text/plain")
size Размер файла в байтах
lastModifiedDate Дата и время последнего изменения

И далее в цикле мы перебираем все файлы и значения всех свойств файла добавляем в переменную output, которая затем выводится на веб-страницу:

for (let i = 0; i < files.length; i++) { // Перебираем все выбранные файлы const file = files[i]; // Получаем файл console.log(file); output += "<li><p><strong>" + file.name + "</strong></p>"; output += "<p>Type: " + file.type || "n/a</p>"; output += "<p>Size: " + file.size + " bytes</p>"; output += "<p>Changed on: " + file.lastModifiedDate.toLocaleDateString() + "</p></li>"; }

Например, с помощью элемента <input type="file" /> я выбираю в файловой системе 2 файла

И после выбора код JavaScript получит выбранные файлы и выведет полученную информацию на веб-страницу:

📜 Выбор файлов через Drag-and-Drop

Применение Drag-and-Drop API представляет второй способ получения файлов. Для применения этого API необходимо:

  1. Определить элемент на веб-странице, на который пользователь будет перетаскивать файл(ы). В качестве такого элемента может служить элемент <div>
  2. Зарегистрировать обработчики двух событий: для события перетаскивания dragover и для события завершения перетаскивания drop. Событие перетаскивания dragover выполняется, когда файл перетаскивается на элемент (но еще не опускается). Событие завершения перетаскивания drop выполняется, когда пользователь отпустил файл на элемент, и перетаскивание завершено.
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Drag and Drop Files</title> </head> <body> <div id="target" style="width:300px;padding: 15px; background-color: gray;"> Перетащите файлы сюда </div> <div id="fileList"></div> <script> function printFiles(e) { e.preventDefault(); const files = e.dataTransfer.files; // получаем все выбранные файлы let output = ""; for (let i = 0; i < files.length; i++) { const file = files[i]; console.log(file); output += "<li><p><strong>" + file.name + "</strong></p>"; output += "<p>Type: " + file.type || "n/a</p>"; output += "<p>Size: " + file.size + " bytes</p>"; output += "<p>Changed on: " + file.lastModifiedDate.toLocaleDateString() + "</p></li>"; } document.getElementById("fileList").innerHTML = "<ul>" + output + "</ul>"; } function handleDragOver(e) { e.preventDefault(); e.dataTransfer.dropEffect = "copy"; } const target = document.getElementById("target"); target.addEventListener("dragover", handleDragOver); target.addEventListener("drop", printFiles); </script> </body> </html>

Здесь на странице у нас также элемент <div> для отображения информации по выбранным файлам. И также определен отдельный элемент <div>, на который пользователь будет перетаскивать файлы. Чтобы область для перетаскивания была заметной, она окрашена в серый цвет.

Для области перетаскивания (<div id="target">) устанавливаются два обработчика.

Обработчик события dragover устанавливает значение "copy" в качестве эффекта перетаскивания:

function handleDragOver(e) { e.preventDefault(); e.dataTransfer.dropEffect = "copy"; }

Обработчик события drop выводит данные файлов на веб-страницу. Для получения выбранных файлов применяется свойство e.dataTransfer.files.

const files = e.dataTransfer.files

Это свойство также представляет объект-список FileList, где каждый элемент представляет объект File.

Таким образом, мы можем перетащить на серую область нужные нам файлы, и ниже будет выведена информация об этих файлах:

📜 Чтение файлов с FileReader

Для чтения выбранных файлов в File API применяется объект FileReader. Этот тип предоставляет различные методы чтения файлов:

Методы FileReader

Метод Описание
readAsBinaryString() Считывает данные в строку байтов
readAsText() Считывает данные как текст
readAsDataURL() Считывает данные как URL-адреса данных (data:image/png;base64,...)
readAsArrayBuffer() Считывает данные в объект ArrayBuffer
abort() Прерывает процесс считывания данных
💡 Data URL

URL-адреса данных — это особая схема URL-адресов для встраивания данных в HTML-код. URL-адреса данных начинаются со строки data:, за которой следует MIME-тип и информация о кодировке, а также соответствующие закодированные данные.

Например: data:image/png;base64,iVBORw0KGgo...

События FileReader

Событие Описание
loadstart Генерируется при начале процесса чтения данных
progress Генерируется в процессе чтения, уведомляя о прогрессе чтения
load Генерируется при успешном завершении чтения данных
loadend Генерируется после завершения чтения данных
abort Генерируется при прерывании чтения методом abort()
error Генерируется при возникновении ошибки

При успешном завершении считывания срабатывает событие load, в обработчике которого можно получить считанные данные через свойство result объекта FileReader или через свойство target.result параметра обработчика события.

Чтение текстовых файлов

Рассмотрим пример считывания текстовых файлов:

Например, считаем файлы, которые выбираются через элемент <input type="file">. Допустим, у меня есть следующий файл "hello.txt":

Hello METANIT.COM Hello World

Для его считывания (и для считывания любых других текстовых файлов) определим следующую веб-страницу:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Reading Text Files</title> </head> <body> <input type="file" id="files" accept="text/*" multiple /> <script> function printFiles(e) { const files = e.target.files; // получаем все выбранные файлы for (file of files) { // создаем объект FileReader для считывания файла const reader = new FileReader(); // при успешном чтении файла выводим его содержимое reader.onload = () => { console.log(reader.result); // выводим содержимое console.log("=============================="); }; // считываем файл reader.readAsText(file); } } document.getElementById("files").addEventListener("change", printFiles); </script> </body> </html>

Здесь на странице определен элемент ввода input, для которого с помощью атрибута accept="text/*" задан фильтр на прием только текстовых файлов.

<input type="file" id="files" accept="text/*" multiple />

В коде JavaScript для элемента input в качестве обработчика события "change" устанавливаем функцию printFiles:

document.getElementById("files").addEventListener("change", printFiles);

В функции printFiles проходим по всем выбранным файлам и создаем объект FileReader для считывания каждого файла. Определяем обработчик для события load, в котором с помощью свойства reader.result получаем считанные данные и выводим их в консоль.

📜 Работа с замыканиями при чтении файлов. Вывод метаданных.

При переборе файлов через свойства объекта File нам доступны различные метаданные файла - имя, размер, тип, дата изменения.

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

// ❌ Проблемный подход for (file of files) { console.log("File Name:", file.name); // выводим имя файла const reader = new FileReader(); reader.onload = () => { console.log("File Name:", file.name); // может быть неправильное имя! console.log(reader.result); }; reader.readAsText(file); }

Проблема в том, что обработчик reader.onload имеет асинхронную природу - считывание предыдущего файла может завершиться, когда начнется (или уже закончено) считывание следующего файла.

✅ Решение через замыкание

Одно из возможных решений — захватить нужные данные через замыкание:

function printFiles(e) { const files = e.target.files; for (file of files) { const reader = new FileReader(); reader.onload = (function(fileData) { return function(e) { console.log("File Name:", fileData.name); console.log(e.target.result); // то же самое, что и reader.result console.log("=============================="); }; })(file); reader.readAsText(file); } }

В данном случае обработчику onload присваивается результат самовыполняющейся функции, которая образует замыкание. Через ее параметр fileData в функцию передается текущий объект File.

📜 Чтение изображений

Аналогичным образом можно считывать и другие типы файлов. Например, считаем и выведем на веб-страницу изображения:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Reading Images</title> <style> div.item {width: 250px; margin:0 10px;} .image {width: 250px;} </style> </head> <body> <input type="file" id="files" accept="image/*" multiple /> <div id="fileList"></div> <script> function printFiles(e) { const files = e.target.files; for (file of files) { const reader = new FileReader(); reader.onload = (function(fileData) { return function() { // создаем элемент div const fileItem = document.createElement("div"); fileItem.className = "fileItem"; // создаем заголовок для добавляемого файла const fileHeader = document.createElement("h3"); fileHeader.textContent = fileData.name; fileItem.appendChild(fileHeader); // создаем элемент img для отображения файла const img = document.createElement("img"); img.src = reader.result; img.className = "image"; fileItem.appendChild(img); document.getElementById("fileList").appendChild(fileItem); }; })(file); reader.readAsDataURL(file); } } document.getElementById("files").addEventListener("change", printFiles); </script> </body> </html>

Теперь для элемента ввода задан фильтр accept="image/*", а все загружаемые изображения отображаются в элементе <div id="fileList">. Для этого в обработчике onload создаем элемент div, в него добавляем заголовок h3 с именем файла и элемент img с содержимым файла. Содержимое файла считывается с помощью метода reader.readAsDataURL().

📜 Работа с файлами разных типов

Можно сочетать открытие файлов различных типов:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Multiple File Types</title> <style> div.item {width: 250px; margin:0 10px;} .text {min-height: 80px; width: 250px; border:1px solid #ccc; padding:10px;} .image {width: 250px;} </style> </head> <body> <input type="file" id="files" multiple /> <div id="fileList"></div> <script> const fileList = document.getElementById("fileList"); // создаем элемент, который представляет отдельный файл на странице function createFileItem(file) { const fileItem = document.createElement("div"); fileItem.className = "fileItem"; // создаем заголовок для добавляемого файла const fileHeader = document.createElement("h3"); fileHeader.textContent = file.name; fileItem.appendChild(fileHeader); return fileItem; } function readTextFile(file) { return function(e) { const fileItem = createFileItem(file); // создаем элемент div для вывода текста файла const div = document.createElement("div"); div.textContent = e.target.result.replace("\\n", "\\n"); div.className = "text"; fileItem.appendChild(div); fileList.appendChild(fileItem); }; } function readImageFile(file) { return function(e) { const fileItem = createFileItem(file); // создаем элемент img для отображения файла const img = document.createElement("img"); img.src = e.target.result; img.className = "image"; fileItem.appendChild(img); fileList.appendChild(fileItem); }; } function printFiles(e) { const files = e.target.files; for (file of files) { const reader = new FileReader(); if (file.type.match("text.*")) { reader.onload = readTextFile(file); reader.readAsText(file); } else if (file.type.match("image.*")) { reader.onload = readImageFile(file); reader.readAsDataURL(file); } } } document.getElementById("files").addEventListener("change", printFiles); </script> </body> </html>

Здесь в зависимости от того, какой тип представляет файл, создаем определенный элемент (<div> для вывода текстовых файлов и <img> для вывода изображений).

📜 Отслеживание прогресса загрузки

При загрузке больших файлов через File API может быть полезно информировать пользователей о ходе операции чтения. Для этой цели тип FileReader позволяет обрабатывать событие progress. В обработчик этого события передается объект, который имеет тип ProgressEvent и который предоставляет следующие свойства:

Свойства объекта ProgressEvent

Свойство Описание
lengthComputable Булевое свойство, которое указывает, можно ли вычислить прогресс
loaded 64-битное целое число, указывающее на объем уже загруженных данных
total 64-битное целое число, хранящее общее количество загружаемых данных

Пример с индикатором загрузки

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>File Upload Progress</title> <style> #progress { width: 0; height: 100%; background-color: #ccc; } #progress-bar { width: 100px; height: 20px; border: 1px solid #888; } </style> </head> <body> <input type="file" id="files" multiple /><br><br> <div id="progress-bar"> <div id="progress"></div> </div> <script> const progressbar = document.getElementById("progress-bar"); const progress = document.getElementById("progress"); // отслеживаем прогресс загрузки function updateProgress(e) { if (e.lengthComputable) { const percentLoaded = Math.round((e.loaded / e.total) * 100); if (percentLoaded < 100) { progress.style.width = percentLoaded + "%"; progress.textContent = percentLoaded + "%"; } } } // обрабатываем выбор файлов function handleFileSelected(event) { progress.style.width = "0%"; progress.textContent = "0%"; const reader = new FileReader(); reader.onprogress = updateProgress; reader.onerror = (e) => console.error(e.target.error); reader.onload = () => { progress.style.width = "100%"; progress.textContent = "100%"; }; if (event.target.files.length > 0) { reader.readAsBinaryString(event.target.files[0]); } } document.getElementById("files").addEventListener("change", handleFileSelected); </script> </body> </html>

На странице определен элемент <input> для выбора файла. Для индикации загрузки файла на странице определен элемент <div id="progress-bar"> с вложенным элементом <div id="percent">.

В качестве обработчика события change для элемента <input> используется функция handleFileSelected. В ней устанавливаем начальные значения для индикатора загрузки, затем создаем объект FileReader и для его события progress в качестве обработчика применяем функцию updateProgress.

function updateProgress(e) { if (e.lengthComputable) { const percentLoaded = Math.round((e.loaded / e.total) * 100); if (percentLoaded < 100) { progress.style.width = percentLoaded + "%"; progress.textContent = percentLoaded + "%"; } } }

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

Когда загрузка завершена, у объекта FileReader срабатывает событие load, в котором устанавливаем финальные значения для элемента progress.

reader.onload = () => { progress.style.width = "100%"; progress.textContent = "100%"; };

В конце обработчика выбора файла начинаем его загрузку как набора байтов:

if(event.target.files.length>0) reader.readAsBinaryString(event.target.files[0]);

📜 Практические советы

✅ Лучшие практики
  • Используйте атрибут accept для фильтрации типов файлов: accept="image/*", accept=".pdf,.doc"
  • Всегда обрабатывайте ошибки с помощью события error
  • Используйте замыкания при работе с несколькими файлами для избежания проблем с асинхронностью
  • Показывайте прогресс при загрузке больших файлов
  • Проверяйте размер файлов перед загрузкой через свойство file.size
⚠️ Ограничения безопасности
  • JavaScript не может получить доступ к файлам без явного выбора пользователем
  • Путь к файлу на локальном диске недоступен (только имя файла)
  • Нельзя записывать файлы напрямую в файловую систему пользователя
  • Доступ к файлам возможен только через <input type="file"> или drag-and-drop

📜 Дополнительные примеры использования

Проверка типа и размера файла

function validateFile(file) { // Проверка типа файла const allowedTypes = ['image/jpeg', 'image/png', 'image/gif']; if (!allowedTypes.includes(file.type)) { alert('Недопустимый тип файла. Разрешены только JPEG, PNG, GIF'); return false; } // Проверка размера (максимум 5 МБ) const maxSize = 5 * 1024 * 1024; // 5 МБ в байтах if (file.size > maxSize) { alert('Файл слишком большой. Максимальный размер: 5 МБ'); return false; } return true; } document.getElementById("files").addEventListener("change", (e) => { const files = e.target.files; for (let file of files) { if (validateFile(file)) { // Обрабатываем файл console.log('Файл прошел проверку:', file.name); } } });

Предпросмотр изображения перед загрузкой

<input type="file" id="imageInput" accept="image/*"> <img id="preview" style="max-width: 300px; display: none;"> <script> document.getElementById("imageInput").addEventListener("change", (e) => { const file = e.target.files[0]; if (file && file.type.startsWith('image/')) { const reader = new FileReader(); reader.onload = (e) => { const preview = document.getElementById("preview"); preview.src = e.target.result; preview.style.display = "block"; }; reader.readAsDataURL(file); } }); </script>

Чтение CSV файла

document.getElementById("csvInput").addEventListener("change", (e) => { const file = e.target.files[0]; if (file && file.type === 'text/csv') { const reader = new FileReader(); reader.onload = (e) => { const csv = e.target.result; const lines = csv.split('\\n'); // Обрабатываем каждую строку lines.forEach((line, index) => { const values = line.split(','); console.log(`Строка ${index}:`, values); }); }; reader.readAsText(file); } });

📜 Поддержка браузерами

File API поддерживается всеми современными браузерами:

  • ✅ Chrome/Edge: полная поддержка
  • ✅ Firefox: полная поддержка
  • ✅ Safari: полная поддержка
  • ✅ Opera: полная поддержка
  • ⚠️ IE10+: частичная поддержка (устаревший браузер)
💡 Проверка поддержки

Перед использованием File API можно проверить его поддержку:

if (window.File && window.FileReader && window.FileList && window.Blob) { // File API поддерживается console.log('File API доступен'); } else { alert('Ваш браузер не поддерживает File API'); }

Резюме

В этой главе мы рассмотрели:

  • ✅ Интерфейсы File API: File, FileList, Blob, FileReader
  • ✅ Два способа выбора файлов: через <input type="file"> и Drag-and-Drop
  • ✅ Свойства объекта File: name, type, size, lastModifiedDate
  • ✅ Методы FileReader: readAsText(), readAsDataURL(), readAsBinaryString(), readAsArrayBuffer()
  • ✅ События FileReader: load, progress, error, loadstart, loadend
  • ✅ Работу с замыканиями для корректной обработки асинхронных операций
  • ✅ Чтение текстовых файлов и изображений
  • ✅ Отслеживание прогресса загрузки с помощью события progress
  • ✅ Практические примеры: валидация файлов, предпросмотр изображений, чтение CSV

Глава 27. Web Worker API

📜 Введение в Web Workers

JavaScript представляет язык, который выполняется как однопоточный, а это означает, что несколько скриптов не могут выполняться одновременно. Скрипты интерпретируются и выполняются один за другим, строка за строкой.

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

То же самое относится и к коллбекам - функциям обратного вызова. Например, при отправке на сервер Ajax-запроса скрипт, который отправляет запрос, продолжает выполняться до тех пор, пока сервер не подготовит ответ и не отправит его клиенту. Когда коллбек получит ответ сервера, окружающий код приостанавливается и возобновляет свою работу, лишь когда коллбек завершит обработку ответа сервера.

💡 Что такое Web Workers?

Web Worker API снимает это ограничение, позволяя обрабатывать задачи параллельно в фоновом режиме. Веб-воркеры выполняются в отдельных потоках. Благодаря веб-воркерам становится возможным выполнять в фоновом режиме параллельно с основным потоком различные ресурсоемкие сценарии, которые в противном случае отрицательно повлияли бы на производительность веб-приложения.

📜 Создание Web Worker

Для создания веб-воркера применяется функция-конструктор Worker:

const worker = new Worker("worker.js");

Задачи, которые выполняет веб-воркер, оформляются в виде отдельных файлов, и в функцию-конструктор в качестве параметра передается путь к скрипту, который будет выполняться веб-воркером.

Создаваемый функцией Worker() веб-воркер еще называют выделенным веб-воркером (dedicated web worker).

⚠️ Важно: требуется веб-сервер

Для загрузки файлов веб-воркеров веб-страница и сами файлы веб-воркеров должны располагаться на веб-сервере. Нельзя просто открыть HTML-файл локально через file:// протокол.

Простой пример Web Worker

Рассмотрим простейший пример. Создадим три файла:

  • index.html — главная страница приложения
  • worker.js — файл задачи веб-воркера
  • server.js — файл приложения сервера (Node.js)

Файл index.html

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Web Worker Example</title> </head> <body> <script> const worker = new Worker("worker.js"); </script> </body> </html>

Фактически здесь только создается объект веб-воркера, который будет выполнять код из файла "worker.js".

Файл worker.js

let result = 1; const intervalID = setInterval(work, 1000); function work() { result = result * 2; console.log("result=", result); if (result >= 32) { clearInterval(intervalID); } }

Здесь с помощью функции setInterval() каждую секунду будет выполняться функция work. В функции work мы просто умножаем значение переменной result на 2, сохраняем результат обратно в переменную result и текущий результат выводим на консоль. Когда result достигнет предела — числа 32, то останавливаем таймер, что приведет к завершению скрипта и соответственно задачи веб-воркера.

Файл server.js (Node.js)

const http = require("http"); const fs = require("fs"); http.createServer((request, response) => { // получаем путь после слеша let filePath = request.url.substring(1); // если пустой путь, отправляем главную страницу index.html if (!filePath) filePath = "index.html"; // в качестве типа ответа устанавливаем html response.setHeader("Content-Type", "text/html; charset=utf-8;"); fs.readFile(filePath, (error, data) => { if (error) { // если ошибка response.statusCode = 404; response.end("<h1>Resource not found!</h1>"); } else { response.end(data); } }); }).listen(3000, () => console.log("Сервер запущен по адресу http://localhost:3000"));

Вкратце пробежимся по коду. Сначала подключаются пакеты с функциональностью, которую мы собираемся использовать:

const http = require("http"); // для обработки входящих запросов const fs = require("fs"); // для чтения файлов с жесткого диска

Для создания сервера применяется функция http.createServer(). В эту функцию передается функция-обработчик, которая вызывается каждый раз, когда к серверу приходит запрос. Эта функция имеет два параметра: request (содержит данные запроса) и response (управляет отправкой ответа).

В функции-обработчике с помощью свойства request.url мы можем получить путь к ресурсу, к которому пришел запрос. Нам надо обрабатывать запросы к страницам "index.html" и "home.html" (а в перспективе к любым другим страницам html). Путь всегда начинается со слеша "/". Например, запрос к странице "home.html" будет представлять путь "/home.html". Соответственно, чтобы получить из запрошенного пути путь к файлам на жестком диске, нам надо убрать начальный слеш:

let filePath = request.url.substring(1);

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

if(!filePath) filePath = "index.html";

И поскольку в нашем случае ответ сервера будет представлять код html, то с помощью метода setHeader() устанавливаем для заголовка "Content-Type" значение "text/html":

response.setHeader("Content-Type", "text/html; charset=utf-8;");

То есть ответ сервера будет представлять html.

Далее с помощью функции fs.readFile считываем файл, к которому идет запрос. Первый параметр функции - адрес файла (в данном случае предполагается, что файл находится в одной папке с файлом сервера server.js). Второй параметр - функция, которая вызывается после считывания файла и получет его содержимое через свой второй параметр data. Вполне возможно, что запрошенного файла не окажется, и в этом случае отправляем ошибку 404:

fs.readFile(filePath, (error, data)=>{ if(error){ // если ошибка response.statusCode = 404; response.end("<h1>Resourse not found!</h1>"); }

Если ошибки нет, файл найден и успешно считан, то отправляем параметр data, который содержит данные файла:

else{ response.end(data); }

В конце с помощью функции listen() запускаем веб-сервер на 3000 порту. То есть сервер будет запускаться по адресу http://localhost:3000/

Запуск и тестирование приложения

Теперь в консоли перейдем к папке сервера с помощью команды cd и запустим сервер с помощью команды node server.js:

C:\app>node server.js
Сервер запущен по адресу http://localhost:3000

После запуска сервера мы можем перейти в браузере по адресу http://localhost:3000, нам отобразится страница, в javascript-коде которой будет создан веб-воркер. Этот веб-воркер выполнит задачу, определенную в файле worker.js, а на консоли мы увидим результат этой работы.

📜 Доступные возможности в Web Workers

В примере выше в коде веб-воркера использовался таймер, создаваемый функцией setInterval(). Однако не всю функциональность стандартного браузерного JavaScript мы можем использовать в задачах веб-воркера.

⚠️ Ограничения Web Workers

Веб-воркеры не имеют доступа к:

  • DOM (нельзя манипулировать элементами страницы)
  • Объекту window (в полном объеме)
  • Объекту document
  • Объекту parent

Доступные функции

Функция Описание
atob() Декодирование строки base64
btoa() Кодирование строки в base64
clearInterval() Остановка повторяющегося таймера
clearTimeout() Остановка отложенного таймера
setInterval() Создание повторяющегося таймера
setTimeout() Создание отложенного таймера
queueMicrotask() Добавление микрозадачи в очередь
structuredClone() Глубокое клонирование объекта
requestAnimationFrame() Анимационный цикл (только для выделенных воркеров)
cancelAnimationFrame() Отмена анимационного цикла (только для выделенных воркеров)

Доступные свойства объекта window

Свойство Описание
console Консоль для отладки
location Информация о текущем URL
navigator Информация о браузере
indexedDB Доступ к IndexedDB

Доступные API

  • Barcode Detection API
  • Broadcast Channel API
  • Cache API
  • Channel Messaging API
  • Console API
  • Web Crypto API (например, Crypto)
  • CSS Font Loading API
  • Encoding API (например, TextEncoder, TextDecoder)
  • Fetch API
  • FileReader
  • FormData
  • ImageBitmap, ImageData
  • IndexedDB
  • Media Source Extensions API
  • Network Information API
  • Notifications API
  • OffscreenCanvas
  • Performance API
  • Server-sent events
  • ServiceWorkerRegistration
  • URL API
  • WebCodecs API
  • WebSocket
  • XMLHttpRequest

📜 Получение/Обращение к объекту веб-воркера и self

С помощью слова self в скрипте веб-воркера (worker.js) мы можем обращаться к объекту веб-воркера:

console.log(self); // получим данные о веб-воркере let result = 1; const intervalID = setInterval(work, 1000); function work() { result = result * 2; console.log("result=", result); if (result >= 32) { clearInterval(intervalID); } }

📜 Остановка Web Worker

Веб-воркер может работать долго вплоть до бесконечности, пока пользователь находится на странице. Для завершения выполнения веб-воркера интерфейс Worker определяет метод terminate().

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

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Web Worker Stop Example</title> </head> <body> <button id="btn">Stop</button> <script> const worker = new Worker("worker.js"); // по нажатию на кнопку останавливаем работу веб-воркера document.getElementById("btn").addEventListener("click", () => { worker.terminate(); console.log("web worker stopped"); }); </script> </body> </html>

Здесь на веб-странице определена кнопка, по нажатию на которую происходит остановка веб-воркера.

📜 Обмен сообщениями между основным потоком и веб-воркером

Основной поток и запускаемые в нем веб-воркеры могут взаимодействовать посредством сообщений. Для отправки сообщения веб-воркеру у объекта Worker вызывается метод postMessage():

postMessage(message) postMessage(message, options) postMessage(message, transfer)

Параметры postMessage()

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

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

Параметр Описание
message Отправляемое сообщение (может быть любым значением: строка, число, объект). Веб-воркер получает копию данных
options Конфигурационный объект со свойством transfer для передачи дополнительных данных
transfer Массив передаваемых объектов. При передаче воркеру переходит владение этими данными

📜 Получение сообщений

Чтобы получать сообщение на стороне веб-воркера, необходимо зарегистрировать функцию-обработчик для события получения сообщения - события message. Например, это можно сделать через свойство onmessage веб-воркера.

// установка обработчика события через addEventListener worker.addEventListener("message", (event) => { console.log(event.data); }); // установка обработчика события через свойство onmessage worker.onmessage = (event) => { console.log(event.data); };

В обработчик события передается объект MessageEvent, у которого через свойство data можно получить переданные данные.

Полный пример обмена сообщениями

Файл worker.js

// прослушиваем событие message self.addEventListener("message", (event) => { // выводим полученные из основного потока данные console.log(`[Worker] Message received: ${event.data}`); // отправляем в ответ основному потоку некоторое сообщение self.postMessage("Hello main thread"); });

С помощью слова self получаем доступ к объекту веб-воркера и устанавливаем для него обработчик события "message". В этом обработчике мы получаем переданные из основного потока данные и с помощью вызова self.postMessage отправляем основному потоку некоторый ответ.

Файл index.html

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Web Worker Messaging</title> </head> <body> <script> const worker = new Worker("worker.js"); const message = "Hello web worker"; console.log(`[Main thread] Send message: ${message}`); // отправляем веб-воркеру сообщение worker.postMessage(message); // получаем от веб-воркера сообщения worker.addEventListener("message", (e) => { console.log(`[Main thread] Response from worker: ${e.data}`); }); // если произошла ошибка worker.addEventListener("error", (e) => { console.log("Error occurred"); }); </script> </body> </html>

Здесь аналогичным образом с помощью вызова worker.postMessage() посылаем веб-воркеру сообщение и также с помощью обработчика события "message" получаем ответ.

💡 Обработка ошибок

В процессе работы веб-воркера может произойти ошибка. В этом случае мы можем обработать событие error объекта Worker для корректной обработки исключительных ситуаций.

📜 Практический пример: вычисление чисел Фибоначчи

Файл fibonacci-worker.js

self.addEventListener("message", (e) => { const num = e.data; const result = fibonacci(num); self.postMessage(result); }); function fibonacci(n) { if (n <= 1) return n; return fibonacci(n - 1) + fibonacci(n - 2); }

Файл index.html

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Fibonacci Calculator</title> </head> <body> <h1>Калькулятор чисел Фибоначчи</h1> <input type="number" id="num" value="40" /> <button id="calculate">Вычислить</button> <div id="result"></div> <script> const worker = new Worker("fibonacci-worker.js"); const calculateBtn = document.getElementById("calculate"); const numInput = document.getElementById("num"); const resultDiv = document.getElementById("result"); calculateBtn.addEventListener("click", () => { const num = parseInt(numInput.value); resultDiv.textContent = "Вычисление..."; // отправляем число воркеру worker.postMessage(num); }); // получаем результат от воркера worker.addEventListener("message", (e) => { resultDiv.textContent = `Результат: ${e.data}`; }); worker.addEventListener("error", (e) => { resultDiv.textContent = `Ошибка: ${e.message}`; }); </script> </body> </html>

В этом примере тяжелые вычисления чисел Фибоначчи выполняются в отдельном потоке, не блокируя основной поток и пользовательский интерфейс.

📜 Типы Web Workers

Тип Описание
Dedicated Workers Выделенные воркеры. Доступны только из скрипта, который их создал
Shared Workers Разделяемые воркеры. Могут быть доступны из нескольких скриптов, даже из разных окон/вкладок
Service Workers Сервис-воркеры. Используются для управления сетевыми запросами, кэширования, push-уведомлений

Когда использовать Web Workers?

✅ Идеальные сценарии использования
  • Математические вычисления — сложные вычисления, которые занимают много времени
  • Обработка больших данных — парсинг больших JSON/XML файлов
  • Обработка изображений — фильтры, манипуляции с пикселями
  • Шифрование/дешифрование — криптографические операции
  • Анализ текста — поиск, сортировка больших объемов текста
  • Работа с IndexedDB — операции с базой данных
⚠️ Когда НЕ использовать Web Workers
  • Для простых операций (накладные расходы на создание воркера больше выгоды)
  • Когда нужен доступ к DOM
  • Для частых коммуникаций с основным потоком (передача сообщений имеет свои накладные расходы)

Поддержка браузерами

Web Workers поддерживаются всеми современными браузерами:

  • ✅ Chrome/Edge: полная поддержка
  • ✅ Firefox: полная поддержка
  • ✅ Safari: полная поддержка
  • ✅ Opera: полная поддержка
  • ✅ IE10+: базовая поддержка (устаревший браузер)
💡 Проверка поддержки
if (typeof Worker !== "undefined") { // Web Workers поддерживаются console.log("Web Workers доступны"); } else { // Web Workers не поддерживаются console.log("Web Workers недоступны"); }

Резюме

В этой главе мы рассмотрели:

  • ✅ Концепцию Web Workers и зачем они нужны
  • ✅ Создание веб-воркера с помощью конструктора Worker()
  • ✅ Ограничения веб-воркеров (нет доступа к DOM)
  • ✅ Доступные функции, свойства и API в воркерах
  • ✅ Остановку воркера методом terminate()
  • ✅ Обмен сообщениями между основным потоком и воркером через postMessage()
  • ✅ Обработку событий message и error
  • ✅ Практические примеры использования (вычисление чисел Фибоначчи)
  • ✅ Типы воркеров: Dedicated, Shared, Service Workers
  • ✅ Сценарии использования и best practices

Глава 28. Дополнительные Web API

📜 Geolocation API

С помощью Geolocation API мы можем получить в коде JavaScript данные о географическом положении пользователя. Для работы с Geolocation API объект navigator определяет свойство geolocation, которое представляет тип Geolocation. Для получения положения у объекта Geolocation применяется метод getCurrentPosition():

getCurrentPosition(success) getCurrentPosition(success, error) getCurrentPosition(success, error, options)

Метод может принимать до трех параметров:

  • success: функция, которая вызывается при успешном определении координат пользователя
  • error: функция, которая вызывается при возникновении ошибки
  • options: дополнительные параметры конфигурации

Поскольку позиция определяется асинхронно, то в качестве перевого параметра success в метод передается функция обратного вызова, которая вызывается, как только позиция будет успешно определена. В качестве параметра в эту функцию обратного вызова передается объект GeolocationPosition. Его свойство coords представляет тип GeolocationCoordinates, свойства которого собственно и хранят координаты пользователя:

  • latitude: географическая широта
  • longitude: географическая долгота
  • altitude: высота над уровнем моря в метрах
  • speed: скорость, с которой перемещается пользователь (например, если он идет или перемещается на транспорте)
  • accuracy: точность определения широты и долготы в метрах
  • altitudeAccuracy: точность определения высоты над уровнем моря в метрах
  • heading: куда направлено устройство пользователя. (градусов) соответствует северу, а направление определяется по часовой стрелке (это означает, что восток равен 90°, (градусам) а запад — 270° градусам). Если скорость (свойство speed) равна 0, это свойство равно NaN. Если устройство не может предоставить информацию о направлении, это свойство равно нулю.

Например, получим геоданные пользователя:

function success(position) { console.log("Широта: ", position.coords.latitude); console.log("Долгота: ", position.coords.longitude); console.log("Высота: ", position.coords.altitude); console.log("Скорость перемещения: ", position.coords.speed); console.log("Точность: ", position.coords.aaccuracy); console.log("Направление: ", position.coords.heading); }; navigator.geolocation.getCurrentPosition(success);

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

Получение информации об ошибке

Но если пользователь откажется предоставить доступ к геоданным в браузере, то будет срабатывать функция error, которая передается в качестве второго параметра. Эта функция в качестве параметра принимает информацию об ошибке в виде объекта GeolocationPositionError. Через свойство message можно получить сообщение об ошибке. Кроме того через свойство code можно получить код ошибки, который может принимать следующие значения:

  • 1 (PERMISSION_DENIED): отсутствуют разрешения на получение геоданных
  • 2 (POSITION_UNAVAILABLE): не удалось установить позицию
  • 3 (TIMEOUT): допустимый таймаут истек до получения позиции пользователя

Применим обработчик ошибки:

function successHandler(position) { console.log("Широта: ", position.coords.latitude); console.log("Долгота: ", position.coords.longitude); }; function errorHandler(error) { console.log(error.message); // выводим сообщение об ошибке console.log(error.code); // выводим код ошибки } navigator.geolocation.getCurrentPosition(successHandler, errorHandler);

И в случае, если разрешение на получение данных отсутствует, мы получим следующий консольный вывод:

User denied Geolocation
1

Отслеживание изменения позиции

Метод watchPosition() объекта Geolocation позволяет зарегистрировать функцию-обработчик, которая будет вызываться автоматически каждый раз при изменении положения устройства. Этот метод принимает те же параметры, что и getCurrentPosition():

watchPosition(success) watchPosition(success, error) watchPosition(success, error, options)

В качестве результата метод возвращает идентификатор, который может быть передан методу clearWatch() объекта Geolocation для остановки отслеживания. Например:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <button id="btn">Stop watching</button> <script> function successHandler(position) { console.log("Широта: ", position.coords.latitude); console.log("Долгота: ", position.coords.longitude); }; function errorHandler(error) { console.log(error.message); } const geolocation = navigator.geolocation; const watchID = geolocation.watchPosition(successHandler, errorHandler); // по нажатию на кнопку прекращаем отслеживание позиции document.getElementById("btn").addEventListener("click", ()=>geolocation.clearWatch(watchID)); </script> </body> </html>

📜 Battery Status API

Через Battery Status API можно получить доступ к информации о батарее устройства. Эта информация может, к примеру, использоваться для адаптации веб-страницы в зависимости от состояния батареи. Для получения информации о батарее данный API определяет интерфейс BatteryManager, который можно использовать. Чтобы получить объект типа BatteryManager, у свойства window.navigator вызывается метод getBattery():

navigator.getBattery() .then((batteryManager)=>console.log(batteryManager)); // BatteryManager { ......}

navigator.getBattery() возвращает промис. Функция, которая передается в then(), в качестве параметра получает объект BatteryManager.

Интерфейс BatteryManager предоставляет ряд свойств с информацией о батарее:

  • charging: логическое значение, которое указывает, заряжается ли аккумулятор в данный момент.
  • chargingTime: число, которое обозначает оставшееся время в секундах до полной зарядки аккумулятора, или 0, если аккумулятор уже полностью заряжен. Если батарея в текущий момент не заряжается, то имеет значение Infinity
  • dischargingTime: число, которое обозначает оставшееся время в секундах до полной разрядки аккумулятора и приостановки работы системы. Если батарея в текущий момент заряжается, то имеет значение Infinity
  • level: число, которое обозначает уровень заряда аккумулятора системы, масштабированное до значения от 0,0 до 1,0

Например, получим информацию о состоянии батареи:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <script> async function dislpayBatteryStatus(){ const batteryManager = await navigator.getBattery(); console.log("is charging: ", batteryManager.charging); console.log("charging time (sec): ", batteryManager.chargingTime); console.log("discharging time (sec): ", batteryManager.dischargingTime); console.log("charge level: ", batteryManager.level); } dislpayBatteryStatus(); </script> </body> </html>

События батареи

BatteryManager предоставляет ряд событий, с помощью которых можно отследить изменение состояния батареи:

  • chargingchange: срабатывает, когда изменяется статус зарядки батареи (свойство charging)
  • chargingtimechange: срабатывает, когда изменяется время до полной зарядки батареи (свойство chargingTime)
  • dischargingtimechange: срабатывает, когда изменяется время до полной разрядки батареи (свойство dischargingTime)
  • levelchange: срабатывает, когда изменяется уровень заряда батареи (свойство level)

Например, обработаем события для отслеживания статуса батареи:

function dislpayBatteryStatus(battery){ console.log("is charging: ", battery.charging); console.log("charging time (sec): ", battery.chargingTime); console.log("discharging time (sec): ", battery.dischargingTime); console.log("charge level: ", battery.level); } navigator.getBattery().then((battery)=>{ dislpayBatteryStatus(battery); battery.addEventListener("chargingchange", (e)=>console.log("is charging: ", e.target.charging)); battery.addEventListener("chargingtimechange", (e)=>console.log("charging time (sec): ", e.target.chargingTime)); battery.addEventListener("dischargingtimechange", (e)=>console.log("discharging time (sec): ", e.target.dischargingTime)); battery.addEventListener("levelchange", (e)=>console.log("charge level: ", e.target.level)); });

📜 Глава 28. Web Speech API. Синтез речи.

Введение в Web Speech API

Web Speech API позволяет веб-разработчикам программно генерировать и распознавать речь на веб-странице. Для этого Web Speech API определяет два интерфейса:

  • SpeechSynthesis — для синтеза речи (преобразование текста в речь, Text-to-Speech)
  • SpeechRecognition — для распознавания речи (Speech-to-Text)
⚠️ Статус стандарта

Данный API (на момент написания этой главы) еще не является официальным стандартом W3C, и его поддержка в различных браузерах может отличаться.

Часть 1: Синтез речи (Speech Synthesis)

Синтез речи (Text-to-Speech, TTS) — это процесс преобразования текста в речь, которая воспроизводится через динамик устройства.

Проверка поддержки

На уровне браузера синтез речи доступен через свойство speechSynthesis объекта window, которое представляет SpeechSynthesis и через которое можно проверить поддержку браузером синтеза речи:

if (window.speechSynthesis) { console.log("Синтез речи поддерживается"); } else { console.log("Синтез речи НЕ поддерживается"); } // Альтернативный способ проверки if ("speechSynthesis" in window) { console.log("Синтез речи поддерживается"); } else { console.log("Синтез речи НЕ поддерживается"); }

Объект SpeechSynthesisUtterance

Для синтеза речи применяется объект типа SpeechSynthesisUtterance, который представляет собой отдельное высказывание и который позволяет определить конфигурацию синтеза с помощью ряда свойств:

Свойство Описание
text Получает и задает текст, который будет синтезироваться при произнесении
lang Получает и устанавливает язык высказывания (например, "ru", "en-US")
voice Получает и задает голос для произнесения высказывания
volume Получает и задает громкость (от 0 до 1)
rate Получает и задает скорость произнесения (от 0.1 до 10)
pitch Получает и задает высоту звука (от 0 до 2)

В общем случае нам достаточно установить свойство text:

const utterance = new SpeechSynthesisUtterance(); utterance.text = "Hello World";

Методы SpeechSynthesis

Непосредственно для синтеза речи и ее управлением вызывается один из методов типа SpeechSynthesis:

Метод Описание
speak(utterance) Добавляет высказывание в очередь для последующего произнесения
pause() Приостанавливает синтез речи
resume() Возобновляет синтез речи (если он ранее был приостановлен)
cancel() Удаляет все высказывания из очереди
getVoices() Возвращает список доступных голосов на текущем устройстве

Простой пример синтеза речи

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Speech Synthesis Example</title> </head> <body> <input id="text" value="Hello World" /> <button id="btn">Speak</button> <script> document.getElementById("btn").addEventListener("click", speak); function speak() { if (window.speechSynthesis) { const utterance = new SpeechSynthesisUtterance(); utterance.text = document.getElementById("text").value; window.speechSynthesis.speak(utterance); } else { console.log("Feature not supported"); } } </script> </body> </html>

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

⚠️ Важно: активация пользователем

В браузерах могут действовать ограничения на запуск синтеза. Например, в Google Chrome требуется, чтобы пользователь активировал действие. Именно поэтому в примере синтез выполняется по нажатию на кнопку, а не сразу при загрузке страницы.

Настройка параметров речи

document.getElementById("btn").addEventListener("click", speak); function speak() { if (window.speechSynthesis) { const utterance = new SpeechSynthesisUtterance(); utterance.text = "Привет"; utterance.lang = "ru"; // аббревиатура языка utterance.volume = 0.5; // громкость (0-1) utterance.rate = 0.5; // скорость (0.1-10) utterance.pitch = 0.5; // высота (0-2) window.speechSynthesis.speak(utterance); } }

Выбор голоса

Если браузер поддерживает несколько голосов, то их можно выбрать с помощью метода getVoices() объекта SpeechSynthesis. Каждый голос имеет разные свойства, включая имя и связанное с ним сокращение страны или языка.

// Получаем список доступных голосов const voices = window.speechSynthesis.getVoices(); voices.forEach(function(voice) { console.log(voice.name); // имя голоса console.log(voice.lang); // язык console.log(voice.default); // является ли голос по умолчанию });

Выбрав нужный голос, его можно установить с помощью свойства voice объекта SpeechSynthesisUtterance:

document.getElementById("btn").addEventListener("click", speak); function speak() { if (window.speechSynthesis) { const utterance = new SpeechSynthesisUtterance(); utterance.text = "Привет"; const voices = window.speechSynthesis.getVoices(); const selectedVoice = voices[0]; // выбираем первый голос utterance.voice = selectedVoice; window.speechSynthesis.speak(utterance); } }

События синтеза речи

В процессе синтеза речи могут возникать различные события (на объекте SpeechSynthesisUtterance):

Событие Описание Обработчик
start Возникает при начале речи onstart
end Возникает при завершении речи onend
pause Возникает, когда речь приостановлена onpause
resume Возникает, когда речь возобновлена onresume
error Возникает при возникновении ошибки onerror
boundary Возникает при достижении границы слова или фразы onboundary
mark Возникает при достижении именованного тега "метки" SSML onmark

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

document.getElementById("btn").addEventListener("click", speak); function speak() { if (window.speechSynthesis) { const utterance = new SpeechSynthesisUtterance(); utterance.onstart = () => console.log("Начало речи"); utterance.onend = () => console.log("Конец речи"); utterance.onerror = (e) => console.log("Ошибка:", e.error); utterance.onpause = () => console.log("Речь приостановлена"); utterance.onresume = () => console.log("Речь возобновлена"); utterance.text = "Привет"; window.speechSynthesis.speak(utterance); } }

📜 Часть 2: Распознавание речи (Speech Recognition)

Распознавание речи (Speech-to-Text) — это процесс преобразования речи пользователя в текст.

Проверка поддержки

Распознавание речи управляется объектом SpeechRecognition. Для его получения применяется свойство webkitSpeechRecognition глобального объекта window:

if (window.webkitSpeechRecognition) { console.log("Распознавание речи поддерживается"); } else { console.log("Распознавание речи НЕ поддерживается"); } // Альтернативный способ проверки if ("webkitSpeechRecognition" in window) { console.log("Распознавание речи поддерживается"); } else { console.log("Распознавание речи НЕ поддерживается"); }
💡 Префикс webkit

Префикс webkit в названии свойства намекает, что это свойство поддерживается только в браузерах на движке WebKit. Соответственно, в реальности в браузерах на движке WebKit распознавание будет осуществляться с помощью объекта webkitSpeechRecognition, а не SpeechRecognition.

Свойства SpeechRecognition

Интерфейс SpeechRecognition предоставляет ряд свойств для настройки распознавания:

Свойство Описание
lang Возвращает и устанавливает язык распознавания (например, "ru", "en-US")
continuous Определяет, возвращаются ли непрерывные результаты (true) или только один результат (false)
interimResults Определяет, следует ли возвращать промежуточные результаты (true) или нет (false)
maxAlternatives Устанавливает максимальное количество вариантов распознавания. По умолчанию — 1
grammars Возвращает и устанавливает коллекцию объектов SpeechGrammar (используемые грамматики)

Методы SpeechRecognition

Метод Описание
start() Запускает распознавание речи
stop() Останавливает распознавание и пытается вернуть результат, используя записанный звук
abort() Прерывает распознавание, не пытаясь вернуть результат

События SpeechRecognition

После запуска распознавания речи методом start() в процессе распознавания могут возникать различные события:

Событие Описание Обработчик
start Служба начала прослушивать звук onstart
audiostart Начался захват звука onaudiostart
soundstart Обнаружен звук (речь или шум) onsoundstart
speechstart Обнаружена речь onspeechstart
result Служба возвращает результат — слово или фразу onresult
speechend Завершено обнаружение речи onspeechend
soundend Завершено обнаружение звука onsoundend
audioend Завершен захват звука onaudioend
end Служба распознавания отключилась onend
error Возникла ошибка onerror
nomatch Распознавание не удалось onnomatch

Получение результатов распознавания

Чтобы получить доступ к результату распознавания речи, регистрируется обработчик события result:

const recognition = new webkitSpeechRecognition(); recognition.onresult = function(event) { const results = event.results; // получаем результат распознавания console.log(results); // список SpeechRecognitionResultList };

Параметр функции-обработчика представляет тип SpeechRecognitionEvent, у которого через свойство results можно получить результаты распознавания в виде списка SpeechRecognitionResultList.

Каждая запись в этом списке представляет объект SpeechRecognitionResult и содержит один или несколько вариантов распознавания речи (объектов SpeechRecognitionAlternative).

const recognition = new webkitSpeechRecognition(); recognition.onresult = function(event) { const results = event.results; // получаем список результатов const firstResult = results[0]; // получаем первый результат const firstAlternative = firstResult[0]; // получаем первый вариант const transcript = firstAlternative.transcript; // распознанный текст const confidence = firstAlternative.confidence; // уровень уверенности (0-1) console.log(transcript); console.log(confidence); };

Полный пример распознавания речи

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Speech Recognition Example</title> </head> <body> <button id="startBtn">Start</button> <button id="stopBtn">Stop</button> <div id="output"></div> <script> const recognition = new webkitSpeechRecognition(); let index = 0; recognition.onresult = function(event) { const results = event.results; const firstResult = results[index++]; const firstAlternative = firstResult[0]; const transcript = firstAlternative.transcript; const confidence = firstAlternative.confidence; console.log(transcript); console.log(confidence); document.getElementById("output").innerHTML += `<p>${transcript} (уверенность: ${(confidence * 100).toFixed(2)}%)</p>`; }; // по нажатию на кнопку Start запускаем распознавание document.getElementById("startBtn").addEventListener("click", () => { if (window.webkitSpeechRecognition) { recognition.continuous = true; recognition.lang = "ru"; // распознавание на русском recognition.start(); } else { console.log("Распознавание речи НЕ поддерживается"); } }); // по нажатию на кнопку Stop останавливаем распознавание document.getElementById("stopBtn").addEventListener("click", () => { recognition.stop(); index = 0; }); </script> </body> </html>

В данном случае по нажатию на кнопку Start запускаем распознавание речи на русском языке. Результаты распознавания выводятся на консоль и на страницу. При нажатии на кнопку Stop останавливаем распознавание.

⚠️ Важно: разрешение на микрофон

При запуске распознавания браузер предложит пользователю дать разрешение на использование микрофона. Если пользователь хочет использовать распознавание речи, то он должен разрешить браузеру доступ к микрофону.

Переменная index для непрерывного распознавания

Для получения результата определяем переменную index. При каждом срабатывании события result в список распознаваний будет добавляться новый результат. Инкрементируя переменную index, мы сможем при последующем срабатывании события result получить распознанный результат по этому индексу.

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

Голосовой помощник

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Voice Assistant</title> </head> <body> <h1>Голосовой помощник</h1> <button id="voiceBtn">🎤 Нажмите и говорите</button> <p id="result"></p> <script> const recognition = new webkitSpeechRecognition(); recognition.lang = "ru"; recognition.continuous = false; recognition.interimResults = false; const synth = window.speechSynthesis; document.getElementById("voiceBtn").addEventListener("click", () => { recognition.start(); }); recognition.onresult = (event) => { const transcript = event.results[0][0].transcript.toLowerCase(); document.getElementById("result").textContent = `Вы сказали: ${transcript}`; // Простой обработчик команд let response = ""; if (transcript.includes("привет")) { response = "Привет! Как дела?"; } else if (transcript.includes("как дела")) { response = "У меня всё отлично!"; } else if (transcript.includes("который час")) { response = `Сейчас ${new Date().toLocaleTimeString()}`; } else { response = "Я вас не понял"; } // Синтезируем ответ const utterance = new SpeechSynthesisUtterance(response); utterance.lang = "ru"; synth.speak(utterance); }; recognition.onerror = (event) => { console.error("Ошибка:", event.error); }; </script> </body> </html>

Поддержка браузерами

Синтез речи (Speech Synthesis)

  • ✅ Chrome/Edge: полная поддержка
  • ✅ Firefox: полная поддержка
  • ✅ Safari: полная поддержка
  • ✅ Opera: полная поддержка

Распознавание речи (Speech Recognition)

  • ✅ Chrome/Edge: полная поддержка (с префиксом webkit)
  • ✅ Opera: полная поддержка (с префиксом webkit)
  • ❌ Firefox: не поддерживается
  • ❌ Safari: не поддерживается на iOS
⚠️ Ограничения
  • Распознавание речи имеет гораздо более ограниченную поддержку браузеров
  • В Chrome распознавание речи работает через серверы Google (требуется интернет)
  • На мобильных устройствах поддержка может отличаться

📜 Web Animation API

Web Animation API позволяет определять и управлять анимациями на веб-странице. Для создания анимации у элементов веб-страницы вызывается метод animate()

animate(keyframes, options)

Первый параметр - keyframes представляет определения ключевых кадров. Второй параметр представляет конфигурационные настройки анимации в виде объекта со следующими свойствами:

  • delay: задержка (в миллисекундах), после которой запускается анимация
  • endDelay: задержка (в миллисекундах), после которой завершается анимация
  • fill: поведение заполнения анимации (возможные значения: none, forwards, backwards, both, auto)
  • iterationStart: определяет итерацию, в которой активируется определенный эффект анимации
  • iterations: количество повторений (для бесконечного повторения анимации передается значение infinity)
  • duration: длительность анимации в миллисекундах
  • direction: направление анимации (возможные значения: alternate, normal, reverse, alternate-reverse)
  • easing: поведение анимации (возможные значения: ease, ease-in, ease-out, ease-in-out, cubic-bezier)

Результатом метода animate() является анимация в виде объекта Animation

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

Анимация на CSS

Например, возьмем примитивную анимацию CSS:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> <style> @keyframes frames { 0% { transform: scale(2); opacity: 0.2; } 30% { transform: scale(3); opacity: 0.4; } 60% { transform: scale(4); opacity: 0.6; } 100% { transform: scale(5); opacity: 0.8; } } #circle { width:50px; height: 50px; opacity: 0.2; background-color: red; margin: 100px; border-radius: 25px; animation: frames 500ms ease-in-out 10ms infinite alternate both; } </style> </head> <body> <div id="circle"></div> </body> </html>

здесь пока нет никакого кода JavaScript, вся анимация задана целиком в CSS. Анимация состоит из отдельных фреймов или состояний, а вся суть анимации заключается в переходе от одного из таких состояний к другому. Для установки фреймов применяется слово @keyframes. В данном случае набор фреймов называется frames и содержит 4 фрейма, каждый из которых описывает значения свойств translation и opacity. Например, возьмем следующий фрейм:

30% { transform: scale(3); opacity: 0.4; }

Процентные доли - 30% указывают, что данный фрейм будет выполняться, после прохождения анимацией 30% времени. В данном фрейме к анимируемому элементу применяется настройка scale(3) - элемент увеличивается в 3 раза. Кроме того с помощью свойства opacity: 0.4 для элемента устанавливается прозрачность в 0.4

Для применения анимации у элемента применяется свойство animation

#circle { ............. animation: frames 500ms ease-in-out 10ms infinite alternate both; }

Через стилевое свойство animation устанавливаем фреймы анимации - frames и дополнительные параметры:

  • 500ms: время анимации - 500 миллисекунд (параметр duration)
  • ease-in-out: поведение анимации (параметр easing)
  • 10ms: задержка при старте анимации - 10 миллисекунд (параметр delay)
  • Infinity: количество повторений - бесконечно (параметр iterations)
  • alternate: направление анимации (параметр direction)
  • both: "заполнение" анимации - 500 миллисекунд (параметр fill)

Таким образом, мы получим пульсирующий круг, который меняет размеры и прозрачность:

Использование Web Animation API

Теперь используем Web Animation API и определим ту же самую анимацию в коде JavaScript:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> <style> #circle { width:50px; height: 50px; opacity: 0.2; background-color: red; margin: 100px; border-radius: 25px; } </style> </head> <body> <div id="circle"></div> <script> const circle = document.getElementById("circle"); // определяем кадры анимации const frames = [{ transform: "scale(2)", opacity: 0.2, offset: 0 },{ transform: "scale(3)", opacity: 0.4, offset: 0.3 },{ transform: "scale(4)", opacity: 0.6, offset: 0.6 },{ transform: "scale(5)", opacity: 1.0, offset: 1 }]; // параметры анимации const config = { duration: 500, // время анимации в миллисекундах easing: "ease-in-out", // поведение анимации delay: 10, // задержка в миллисекундах iterations: Infinity, // кол-во повторений direction: "alternate", // направление анимации fill: "both" // заполнение поведения анимации }; // выполняем анимацию circle.animate(frames, config); </script> </body> </html>

Здесь кадры/фреймы анимации заданы массивом frames, каждый элемент которого имеет три свойства. Например:

{ transform: "scale(3)", opacity: 0.4, offset: 0.3 }

Первые два свойства (transform и opacity) - это те же стилевые свойства элемента, которые устанавливались в CSS. Третье свойство - offset задает момент времени, когда данный кадр должен отображаться в анимации. Так, offset: 0.3 соответствует 30% в CSS. Если это свойство опущено, отдельные ключевые кадры распределяются равномерно в течение определенной продолжительности.

Второй параметр функции animate() аналогичен дополнительным параметрам анимации, которые устанавливаются в CSS.

Управление анимацией

Метод animate() возвращает объект Animation, который позволяет управлять анимацией с помощью ряда методов:

  • pause(): приостанавливает анимацию
  • play(): возобновляет анимацию
  • cancel(): отменяет анимацию
  • finish(): завершает анимацию

Кроме того, с помощью свойства playbackRate можно управлять скоростью анимации. Например:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> <style> #rect { width:50px; height: 50px; background-color: green; margin-top: 20px; } </style> </head> <body> <div> <button id="pause">Pause</button> <button id="play">Play</button> <button id="cancel">Cancel</button> <button id="faster">Faster</button> <button id="slower">Slower</button> </div> <div id="rect"></div> <script> // анимируемый элемент const rect = document.getElementById("rect"); // фреймы анимации const frames = [{ marginLeft: "50px", offset: 0 },{ marginLeft: "100px", offset: 0.3 },{ marginLeft: "150px", offset: 0.6 },{ marginLeft: "200px", offset: 1 }]; // параметры анимации const config = { duration: 600, easing: "ease-in-out", iterations: Infinity, direction: "alternate" }; const animation = rect.animate(frames, config); document.getElementById("pause").addEventListener("click", () => animation.pause()); document.getElementById("play").addEventListener("click", () => animation.play()); document.getElementById("cancel").addEventListener("click", () => animation.cancel()); // увеличиваем скорость в 2 раза document.getElementById("faster").addEventListener("click", () => animation.playbackRate *= 2); // уменьшаем скорость в 2 раза document.getElementById("slower").addEventListener("click", () => animation.playbackRate /= 2); </script> </body> </html>

В данном случае с помощью набора кадров изменяем свойство "margin-left" у элемента div, который стилизован под зеленный квадрат. А с помощью кнопок управляем его анимацией:

📜 Резюме

В этой главе мы рассмотрели:

  • ✅ Введение в Web Speech API и его два интерфейса
  • Синтез речи: объект SpeechSynthesisUtterance, методы speak/pause/resume/cancel
  • ✅ Настройку параметров речи: lang, volume, rate, pitch, voice
  • ✅ Выбор голоса с помощью getVoices()
  • ✅ События синтеза: start, end, pause, resume, error, boundary
  • Распознавание речи: объект webkitSpeechRecognition
  • ✅ Свойства распознавания: lang, continuous, interimResults, maxAlternatives
  • ✅ Методы: start(), stop(), abort()
  • ✅ События распознавания: result, start, end, error, nomatch
  • ✅ Получение результатов: SpeechRecognitionResultList и SpeechRecognitionAlternative
  • ✅ Практический пример: голосовой помощник
  • ✅ Поддержку браузерами и ограничения

Глава 29. Дополнительные паттерны и техники

Введение

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

📜 1. Паттерн "Пространство имен" (Namespace Pattern)

Проблема конфликта имен

Если на веб-страницу подключается множество скриптов, то может возникнуть проблема конфликта имен, если разные скрипты определяют переменные/константы и функции с одним и тем же именем.

В ряде языков программирования можно сгруппировать функционал в отдельные блоки — "пространства имен" (как в C#) или пакеты (как в Java). Разные пакеты/пространства имен могут определять переменные и функции с одинаковыми именами, но конфликта имен не будет.

⚠️ Ограничение JavaScript

В JavaScript не существует ни пакетов, ни пространств имен, но есть возможность эмулировать пространства имен с помощью специальной техники, которую еще называют паттерном "пространство имен".

Реализация паттерна

Паттерн "пространство имен" использует объекты в качестве контейнеров для группировки связанного функционала:

// Определяем объект-пространство имен var MathLib = MathLib || {}; // Определяем переменную внутри пространства имен MathLib.MAX = 1234; // Определяем функцию внутри пространства имен MathLib.sum = function(a, b) { return a + b; }; console.log(MathLib.sum(4, 5)); // 9 console.log(MathLib.MAX); // 1234 MathLib.MAX = 5678; console.log(MathLib.MAX); // 5678

Здесь определяется пространство имен в виде объекта MathLib (условно говоря пространство имен MathLib). Обратите внимание на форму определения:

💡 Объяснение кода

Выражение var MathLib = MathLib || {} предотвращает перезапись объекта MathLib, если он уже существует. Если MathLib не определен, создается новый пустой объект. Но при таком определении мы не можем использовать ключевые слова let или const для определения объекта. Поэтому в данном случае объект определяется с помощью var.

Затем в MathLib для демонстрации определяются переменная MAX и функция sum, которые мы можем использовать, применяя имя объекта MathLib.

Определение содержимого сразу

var MathLib = MathLib || { MAX: 1234, sum: function(a, b) { return a + b; } };

Избежание конфликтов имен

Таким образом, мы можем определять переменные и функции с одинаковыми именами внутри разных объектов-пространств имен, и у нас не возникнет конфликта имен:

var MathLib = MathLib || { sum: function(a, b) { return a + b; } }; var OtherMathLib = OtherMathLib || { sum: function(nums) { let result = 0; for (n of nums) result += n; return result; } }; console.log(MathLib.sum(4, 5)); // 9 console.log(OtherMathLib.sum([4, 5, 6])); // 15

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

Вложенные пространства имен

Подобным образом можно определять вложенные пространства имен:

var Messages = Messages || { ru: { hello: "Привет", bye: "Пока" }, en: { hello: "Hello", bye: "Good bye" }, }; console.log(Messages.ru.hello); // Привет console.log(Messages.en.hello); // Hello
✅ Преимущества
  • Разгрузка глобальной области видимости
  • Избежание конфликтов имен
  • Логическая группировка функционала
  • Простота реализации

📜 2. Паттерн "Модуль" (Module Pattern)

Основная идея

Паттерн "Модуль" базируется на замыканиях и состоит из двух компонентов:

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

Простейший модуль

const printer = (function() { const messages = {greeting: "hello"}; return { print: function() { console.log(messages.greeting); } }; })(); printer.print(); // "hello"

Здесь определена константа printer, которая представляет результат анонимной функции (IIFE). Внутри подобной функции определен объект messages с некоторыми данными. Этот объект является приватным — к нему нельзя обратиться извне. Доступ к данным предоставляется только через публичный метод print.

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

return { print: function(){ console.log(messages.greeting); } }

Такая конструкция позволяет закрыть некоторый набор данных в рамках функции-модуля и опосредовать доступ к ним через определенный API - возвращаемые внутренние функции.

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

const printer = (function(){ const messages = {greeting: "Hello METANIT.COM"}; const printMessage = function(){ console.log(messages.greeting); }; return { print: printMessage // функция printMessage определена вне объекта } })(); printer.print(); // Hello METANIT.COM

Если существует вероятность, что модуль уже определен где-то ранее в коде или во внешних подключаемых файлах, то мы можем использовать следующую конструкцию:

var printer = printer || (function(){ const messages = {greeting: "Hello World"}; return { print: function(){ console.log(messages.greeting); } } })(); printer.print(); // Hello World

Определение var printer = printer || (function(){ ... присваивает переменной значение некоторого объекта printer, если он существует, либо присваивает результат вызова анонимной IIFE-функции. Но при таком определении мы не можем использовать ключевые слова let или const для определения объекта. Поэтому в данном случае объект определяется с помощью var.

Практический пример: калькулятор

const calculator = (function() { // Приватные данные const data = { result: 0 }; // Публичные методы return { sum: function(a, b) { data.result = a + b; return this; }, subtract: function(a, b) { data.result = a - b; return this; }, print: function() { console.log(data.result); return this; }, getResult: function() { return data.result; } }; })(); // Использование calculator.sum(10, 5).print(); // 15 calculator.subtract(20, 8).print(); // 12 console.log(calculator.getResult()); // 12 // Попытка прямого доступа к приватным данным console.log(calculator.data); // undefined

Данный модуль представляет примитивный калькулятор, который выполняет три операции: сложение, вычитание и вывод результата. Все данные инкапсулированы в объекте data, который хранит результат операции. Извне к этому объекту нет доступа — он приватный.

Все данные инкапсулированы в объекте data, который хранит результат операции. Все операции представлены тремя возвращаемыми функциями: sum, subtract и print. Через эти функции мы можем управлять результатом калькулятора извне.

💡 Инкапсуляция

Паттерн "Модуль" позволяет эмулировать приватные переменные и методы в JavaScript, которые недоступны извне, но доступны публичным методам через замыкание.

Передача внешних модулей

Через параметры IIFE-функций в модули можно передать какие-нибудь данные, например, другие модули:

var moduleA = moduleA || (function () { const message = "Hello World"; return { printMessage: function() { console.log(message); } } })(); var moduleB = moduleB || (function (moduleA) { return { print: function() { moduleA.printMessage(); } } })(moduleA); moduleB.print();

В данном случае модуль moduleB ожидает получение модуля moduleA. Внутри модуля moduleB идет обращение к функции moduleA.printMessage. Аналогично можно передавать и набор модулей.

Расширение модуля

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

// первая техника var localeModule = localeModule || (function(locale){ const enMessage = "Hello World"; locale.printEn = function(){console.log(enMessage);}; return locale; })(localeModule || {}); // вторая техника var localeModule = (function(locale){ const ruMessage = "Привет мир"; locale.printRu = function(){console.log(ruMessage);}; return locale; })(localeModule); localeModule.printEn(); // Hello World localeModule.printRu(); // Привет мир

Для расширения модуля можно применять две техники.

Первая техника заключается в том, что если модуль еще не создан, то в качестве параметра передается пустой объект:

var localeModule = localeModule || (function(locale){ const enMessage = "Hello World"; locale.printEn = function(){console.log(enMessage);}; return locale; })(localeModule || {});

Так, в данном случае, если модуля localModule еще не существует, то будет создан объект, в который будет добавлена функция printEn для вывода некоторого сообщения.

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

Вторая техника предполагает, что модуль уже существует:

var localeModule = (function(locale){ const ruMessage = "Привет мир"; locale.printRu = function(){console.log(ruMessage);}; return locale; })(localeModule);

Здесь мы уверены, что уже есть объект localModule, и также добавляем к нему новую функцию - printRu. В обоих случаях модуль возвращает в качестве результата расширенный новой функциональность объект из параметра.

Но независимо от того, какой тип расширения модуля применяется, у них есть один общий недостаток: функции, определенные в одном файле исходного кода для модуля, не имеют доступа к частным переменным и константам, определенным в другом файле исходного кода для того же модуля. Например, метод printRu не имеет доступа к константе enMessage.

Модули можно расширять, добавляя новые методы:

const calculator = (function(calc) { // Добавляем новый метод calc.multiply = function(a, b) { this.result = a * b; return this; }; return calc; })(calculator || {});
✅ Преимущества паттерна "Модуль"
  • Инкапсуляция данных
  • Приватные переменные и методы
  • Защита от несанкционированного вмешательства
  • Чистая глобальная область видимости
  • Возможность создания публичного API

📜 3. JavaScript в CSS

Использование CSS переменных для хранения JS-кода

CSS-переменные (Custom Properties) могут использоваться для хранения JavaScript-кода, который затем выполняется с помощью функции eval() или Function().

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>JavaScript в CSS</title> <style> :root { --script: console.log('Hello from CSS!'); } </style> </head> <body> <script> // Получаем стили документа const style = getComputedStyle(document.documentElement); // Получаем значение CSS-переменной const script = style.getPropertyValue("--script"); // Выполняем код eval(script); // "Hello from CSS!" // Альтернативный способ const fn = new Function(script); fn(); </script> </body> </html>

Здесь свойство или переменная CSS --script хранит js-код, который выводит на консоль браузера сообщение.

Чтобы выполнить этот код, сначала получаем стили документа:

const style = getComputedStyle(document.documentElement);

Затем получаем среди этих стилей определение свойства --script:

const script = style.getPropertyValue("--script");

Далее выполняем код:

new Function(script)();

В качестве альтернативы для выполнения кода можно вызвать функцию eval():

eval(script);

В качестве хранимого кода могут применяться более сложные выражения JavaScript. Например, используем конструкцию if:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> <style> :root { --script: if (x > 5) document.body.style.background = "blue"; } </style> </head> <body> <script> let x = 10; const style = getComputedStyle(document.documentElement); const script = style.getPropertyValue("--script"); eval(script); </script> </body> </html>

Здесь если значение переменной x больше 5, то окрашиваем пространство элемента body в синий цвет.

Практический пример: логирование ширины экрана

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Media Query Logging</title> <style> :root { --log: localStorage["minwidth"] = 0; } @media (min-width: 500px) { :root { --log: localStorage["minwidth"] = 500; } } @media (min-width: 800px) { :root { --log: localStorage["minwidth"] = 800; } } @media (min-width: 1200px) { :root { --log: localStorage["minwidth"] = 1200; } } </style> </head> <body> <script> window.onload = window.onresize = () => { const log = getComputedStyle(document.documentElement) .getPropertyValue("--log"); eval(log); console.log("Минимальная ширина:", localStorage["minwidth"]); }; </script> </body> </html>

Здесь в localStorage записываем элемент с ключом "minwidth", значение которого зависит от значений media-query. А в коде javascript определяем обработчик событий window.onresize и window.onload, чтобы при загрузке страницы, а также при изменении ширины окна браузера значение в localStorage перезаписывалось.

⚠️ Безопасность

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

📜 4. Определение мобильного устройства в JavaScript

Метод 1: Проверка User Agent

Анализ строки User Agent браузера, который можно получить с помощью свойства navigator.userAgent, может дать приблизительную информацию об устройстве. Например, возьмем следующий код:

function isMobileDevice() { return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i .test(navigator.userAgent); } if (isMobileDevice()) { console.log("Мобильное устройство"); } else { console.log("Десктоп"); }

В данном случае тестируем строку navigator.userAgent на наличие в ней распространенных маркеров мобильных устройств. Тем не менее этот способ имеет недостатки. В частности, строку с User Agent можно подделать. Также некоторые планшеты могут определяться как мобильные или наоборот.

Метод 2: Проверка ширины экрана и сенсорных (touch) событий

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

"ontouchstart" in window

Она проверяет, существует ли в объекте window свойство ontouchstart. Если да, значит, браузер поддерживает touch-события (например, на смартфонах и планшетах). Но важно помнить, что некоторые десктопные браузеры (например, Chrome с эмуляцией) тоже могут возвращать true.

Более современный способ предполагает проверку свойства maxTouchPoints интерфейса Navigator (свойство navigator.maxTouchPoints), которое возвращает максимальное количество одновременных точек касания, поддерживаемых текущим устройством. Если это свойство возвращает значение больше 0, значит, устройство сенсорное.

В конечном счете, мы можем совместить оба выше описанных способа проверки. На десктопах (без тачскрина) оба способа обычно дают false. На мобильных устройствах — true. Но есть нюансы: некоторые ноутбуки с тачскрином (например, Windows Surface) вернут true. Браузеры в режиме эмуляции мобильных устройств тоже могут вернуть true.

function isMobile() { return isTouchScreen = "ontouchstart" in window || navigator.maxTouchPoints > 0; } if (isMobile()) { console.log("Мобильное устройство (сенсорный экран + малый размер)"); } else { console.log("Десктоп или планшет"); }

Можно комбинировать проверку ширины экрана и поддержку сенсорных событий. Например:

function isMobile() { const isTouchScreen = "ontouchstart" in window || navigator.maxTouchPoints > 0; // Обычно мобильные устройства имеют ширину меньше 768px, но это не точно const isSmallScreen = window.innerWidth < 768; return isTouchScreen && isSmallScreen; } if (isMobile()) { console.log("Мобильное устройство (сенсорный экран + малый размер)"); } else { console.log("Десктоп или планшет"); }

Из плюсов - это более точный метод, чем просто проверка User Agent. Однако на десктопах с сенсорными экранами может давать ложные срабатывания.

// Проверка поддержки touch-событий function isTouchDevice() { return ('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0); } if (isTouchDevice()) { console.log("Устройство поддерживает сенсорный ввод"); }

Метод 3: Проверка maxTouchPoints (современный способ)

function isMobile() { return navigator.maxTouchPoints > 0; } console.log("Максимальное количество точек касания:", navigator.maxTouchPoints);

Свойство maxTouchPoints возвращает максимальное количество одновременных точек касания, поддерживаемых текущим устройством.

Метод 4: window.matchMedia

Можно использовать медиа-запросы для проверки мобильного разрешения с помощью метода window.matchMedia(). Этот метод возвращает объект MediaQueryList, который затем можно использовать для определения соответствия веб-страницы строке медиа-запроса, а также для мониторинга соответствия страницы (или прекращения соответствия) этому медиа-запросу. Например:

function isMobileView() { return window.matchMedia("(max-width: 767px)").matches; } if (isMobileView()) { console.log("Мобильное разрешение экрана"); } else { console.log("Широкий экран (десктоп/планшет)"); }

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

Метод 5: Анализ плотности пикселей (window.devicePixelRatio)

Плотность пикселей, которая представлена свойством window.devicePixelRatio, показывает соотношение между физическими и логическими пикселями на экране или DPR.

DPR = Физические пиксели/Логические (CSS) пиксели

Несколько примеров:

  • 1.0 — обычный экран (старые ноутбуки, десктопы).
  • 1.5-2.0 — HD-экраны (многие смартфоны, планшеты).
  • 2.0-3.0+ — Retina (iPhone), Super AMOLED (Samsung).

Это значение полезно для:

  • Различения Retina-экранов (Apple) и высокоплотных дисплеев (Android).
  • Определения, является ли устройство смартфоном или планшетом (у смоартфонов обычно значение devicePixelRatio >= 2, а у десктопов — 1 или 1.25).
  • Оптимизации отображения графики (например, загрузка изображений в высоком разрешении для Retina-экранов).

Например, используем devicePixelRatio для определения устройства:

function isHighDensityDisplay() { return window.devicePixelRatio >= 2; } if (isHighDensityDisplay()) { console.log("Устройство с высоким DPI (скорее всего, мобильное)"); } else { console.log("Обычный экран (возможно, десктоп)"); }

Отличие iPhone от Android

У iPhone (особенно Retina) значение DPR обычно 2 или 3, а у Android-устройств может быть 2.5, 3.5 и выше:

// Определение типа устройства по DPR const dpr = window.devicePixelRatio; if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) { console.log(`Apple устройство с DPR = ${dpr}`); } else if (/Android/i.test(navigator.userAgent)) { console.log(`Android устройство с DPR = ${dpr}`); } else { console.log("Другое устройство (десктоп?)"); }
💡 Device Pixel Ratio (DPR)

DPR — это отношение физических пикселей к логическим (CSS) пикселям:

DPR = Физические пиксели / Логические (CSS) пиксели

  • iPhone Retina: DPR = 2 или 3
  • Android: DPR = 2.5, 3.5 и выше
  • Десктоп: DPR = 1 или 1.25

Благодаря этому мы можем оптимизировать различные аспекты, например, изображения под дисплеи Retina:

const dpr = window.devicePixelRatio; const img = document.querySelector("img"); if (dpr >= 2) { img.src = "image@2x.png"; // Загрузка HD-версии } else { img.src = "image.png"; // Обычная версия }

Комбинированный подход

Для оптимальной точности лучше всего использовать несколько проверок:

function isMobile() { const userAgent = navigator.userAgent; const isMobileUA = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent); const isTouchDevice = "ontouchstart" in window || navigator.maxTouchPoints > 0; const isSmallScreen = window.innerWidth < 768; // здесь возможны варианты типа (isMobileUA && isTouchDevice && isSmallScreen) return isMobileUA || (isTouchDevice && isSmallScreen); } if (isMobile()) { console.log("Мобильное устройство"); } else { console.log("Десктоп или планшет"); }

Также можно объединить с DRP:

function isMobileDevice() { const isMobileUA = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent); const isHighDPI = window.devicePixelRatio >= 2; const isSmallScreen = window.innerWidth < 768; return isMobileUA || (isHighDPI && isSmallScreen); } if (isMobileDevice()) { console.log("Скорее всего, это мобильное устройство"); } else { console.log("Десктоп или устройство без высокого DPI"); }

Или просто комбинировать отдельные аспекты, например, плотность пикселей и строку User Agent:

function isRetinaMobile() { const dpr = window.devicePixelRatio; const isApple = /iPhone|iPad|iPod/i.test(navigator.userAgent); const isAndroid = /Android/i.test(navigator.userAgent); return (dpr >= 2) && (isApple || isAndroid); }

Комплексная проверка

function detectDevice() { const userAgent = navigator.userAgent.toLowerCase(); const isMobileUA = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i .test(userAgent); const isTouch = navigator.maxTouchPoints > 0; const isSmallScreen = window.matchMedia("(max-width: 768px)").matches; const isHighDPI = window.devicePixelRatio >= 2; return { isMobile: isMobileUA || (isTouch && isSmallScreen), isTouch: isTouch, isSmallScreen: isSmallScreen, isHighDPI: isHighDPI, deviceType: isMobileUA ? 'mobile' : 'desktop', touchPoints: navigator.maxTouchPoints, dpr: window.devicePixelRatio }; } const device = detectDevice(); console.log(device); // Пример использования if (device.isMobile) { document.body.classList.add('mobile'); } else { document.body.classList.add('desktop'); }
⚠️ Ограничения
  • User Agent можно подделать
  • Touch события поддерживаются некоторыми десктопными браузерами
  • Размер экрана не всегда определяет тип устройства (планшеты)
  • Рекомендуется использовать комбинацию методов

📜 Сравнение паттернов

Паттерн Преимущества Недостатки Когда использовать
Пространство имен Простота, избежание конфликтов имен Все данные публичные Группировка функционала, разделение кода
Модуль Инкапсуляция, приватные данные Сложнее в реализации Когда нужна защита данных
ES6 Modules Нативная поддержка, import/export Требует сборщик для старых браузеров Современные приложения

Резюме

В этой главе мы рассмотрели:

  • Паттерн "Пространство имен" — избежание конфликтов имен через объекты-контейнеры
  • Паттерн "Модуль" — инкапсуляция данных с помощью замыканий
  • JavaScript в CSS — хранение и выполнение JS-кода через CSS-переменные
  • Определение мобильных устройств — 5 методов:
    • User Agent
    • Сенсорные события (ontouchstart)
    • navigator.maxTouchPoints
    • window.matchMedia
    • window.devicePixelRatio
  • ✅ Комплексный подход к определению устройств
  • ✅ Сравнение различных паттернов организации кода
✅ Рекомендации
  • Для новых проектов используйте ES6 модули (import/export)
  • Для определения устройств комбинируйте несколько методов
  • Избегайте eval() в production-коде
  • Используйте паттерны для организации кода и избежания конфликтов

Глава 30. jQuery

📜 Введение в jQuery

jQuery — это быстрая, легкая и многофункциональная JavaScript-библиотека, которая упрощает работу с HTML-документами, обработку событий, анимацию и AJAX-взаимодействие для быстрой веб-разработки.

💡 Основные преимущества jQuery
  • Простой и понятный синтаксис
  • Кроссбраузерная совместимость
  • Мощные селекторы (как в CSS)
  • Удобная работа с DOM
  • Встроенная анимация
  • Поддержка AJAX
  • Огромное сообщество и множество плагинов
⚠️ Современное состояние

В современной веб-разработке jQuery используется реже из-за появления нативных API (querySelector, fetch) и фреймворков (React, Vue, Angular). Однако jQuery все еще активно используется в legacy-проектах и для быстрого прототипирования.

📜 Подключение библиотеки

Работа с jQuery всегда начинается с подключения библиотеки. Есть два основных способа:

1. Локальное подключение

<body> <!-- Ваш контент --> <script src="js/jquery.js"></script> <script src="js/script.js"></script> </body>

2. Подключение через CDN (рекомендуется)

<body> <!-- Ваш контент --> <!-- jQuery 3.7.1 (последняя версия) --> <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script> <!-- Альтернативно: Google CDN --> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script> <script src="js/script.js"></script> </body>
✅ Преимущества CDN
  • Быстрая загрузка (серверы по всему миру)
  • Кэширование браузером (библиотека может быть уже загружена с другого сайта)
  • Экономия трафика вашего сервера

📜 Запуск кода

Для выбора элемента на странице в jQuery используется функция $() или jQuery():

<script> jQuery("h2").css("color", "red"); // или $("h2").css("color", "red"); </script> <body> <h2>Заголовок</h2> <p>Много текста в одну строку.</p> </body>

Вопрос: Будет ли изменен цвет заголовка?

Ответ: Нет! Скрипт выполнится раньше, чем браузер создаст элемент <h2> в DOM.

Ожидание загрузки документа

Для того, чтобы код сработал верно, мы должны:

  • Поместить скрипт после искомого элемента
  • Поместить скрипт в самый низ страницы (перед закрывающим </body>)
  • Использовать метод .ready()

Полная запись

<script> jQuery(document).ready(function() { jQuery("h2").css("color", "red"); }); </script>

Сокращенная запись (рекомендуется)

<script> $(function() { $("h2").css("color", "red"); }); </script>
💡 Best Practice

Сокращенную запись стоит причислить к «best practices», ведь в jQuery 3.0 метод .ready() уже помечен как deprecated.

Решение конфликта с другими библиотеками

Чтобы у вас не возникало конфликтов с другими библиотеками за использование символа $, оборачивайте код в анонимную функцию:

<script> ;(function($, undefined) { $("h2").css("color", "red"); })(jQuery); </script>

📜 Выбор элементов на странице

Все простые выборки на странице можно сделать с помощью CSS-селекторов:

Селектор Описание
$("#content") Выбираем элемент с id="content"
$("section#content") Выбираем <section> с id="content"
$(".intro") Выбираем элементы с class="intro"
$("p.intro") Выбираем все <p> с class="intro"
$(".intro.pinned") Выбираем элементы с классами "intro" И "pinned"
$("h3") Выбираем все теги <h3>
$("h1, h2") Выбираем все теги <h1> и <h2>
$("a[class]") Выбираем все теги <a> с атрибутом class
$("a[href^='http']") Ссылки, начинающиеся с "http"
$("input[type='text']") Текстовые input-поля
$(":checked") Все отмеченные checkbox и radio
$("li:first") Первый элемент <li>
$("li:last") Последний элемент <li>
$("li:even") Четные элементы <li> (0, 2, 4...)
$("li:odd") Нечетные элементы <li> (1, 3, 5...)

📜 Функции выборки элементов

jQuery предоставляет дополнительные методы для навигации по DOM:

Метод Описание
.find(selector) Поиск потомков элемента
.children() Выбор прямых дочерних элементов
.parent() Выбор прямого родителя
.parents(selector) Выбор всех предков
.closest(selector) Выбор первого подходящего предка
.next() Следующий соседний элемент
.prev() Предыдущий соседний элемент
.siblings() Все соседние элементы
.first() Первый элемент в выборке
.last() Последний элемент в выборке
.eq(index) Элемент по индексу

Примеры

$("section").find("article").find("h3"); // поиск h3 внутри article внутри section $("#post").next(); // следующий элемент после #post $("article").children(); // дочерние элементы article $("p").parent(); // прямые родители всех <p> $("p").parents("section"); // все предки <section> для <p> $("p").closest("section"); // первый предок <section> для <p>

📜 Работа с CSS-стилями

Для работы со стилями предназначен метод .css():

Синтаксис Описание
css(property) Получение значения CSS-свойства
css(property, value) Установка значения CSS-свойства
css({prop: val, prop: val}) Установка нескольких значений
css(prop, function(i, val){}) Установка через callback-функцию

Примеры

// Установка одного свойства $("#my").css("color", "red"); $("#my").css("background-color", "yellow"); // Установка нескольких свойств (строковые ключи) $("#my").css({ "color": "red", "font-size": "18px", "background-color": "white" }); // Установка нескольких свойств (camelCase) $("#my").css({ color: "black", fontSize: "12px", backgroundColor: "transparent" }); // Использование callback-функции $("#my").css("font-size", function(i, value) { return parseFloat(value) * 1.5; // увеличиваем размер шрифта в 1.5 раза });

📜 Работа с CSS-классами

Метод Описание
addClass(className) Добавление класса элементу
removeClass(className) Удаление класса
toggleClass(className) Переключение класса (добавить/удалить)
hasClass(className) Проверка на причастность к классу

Примеры

$("p").addClass("highlight"); // добавить класс $("p").removeClass("highlight"); // удалить класс $("p").toggleClass("highlight"); // переключить класс if ($("p").hasClass("highlight")) { // проверка класса console.log("Параграф выделен"); }

📜 Работа с атрибутами

Метод Описание
attr(attrName) Получение значения атрибута
attr(attrName, attrValue) Установка значения атрибута
attr({name: val, name: val}) Установка нескольких атрибутов
removeAttr(attrName) Удаление атрибута

Примеры

// Получение адреса ссылки $("a").attr("href"); // Изменение адреса и заголовка ссылки $("a").attr({ "href": "https://example.com", "title": "Example Website" }); // Получение альтернативного текста картинки $("img").attr("alt"); // Изменение адреса картинки $("img").attr("src", "/images/default.png");

📜 Работа со свойствами

Для работы со свойствами используем методы из семейства .prop():

Метод Описание
prop(propName) Получение значения свойства
prop(propName, propValue) Установка значения свойства
removeProp(propName) Удаление свойства
💡 attr() vs prop()

attr() — работает с HTML-атрибутами (то, что в разметке)

prop() — работает со свойствами DOM-объекта (реальное состояние)

Для checkbox, radio, selected используйте prop()!

Примеры

// Получение состояния checkbox $("#checkbox").prop("checked"); // true или false // Снятие галочки $("#checkbox").prop("checked", false); // Установка галочки $("#checkbox").prop("checked", true); // Для select $("option").prop("selected", true);

📜 События

jQuery работает практически со всеми событиями в JavaScript. Список самых востребованных:

События мыши

Событие Описание
click Клик по элементу (mousedown → mouseup → click)
dblclick Двойной щелчок мышкой
mousedown Нажатие клавиши мыши
mouseup Отжатие клавиши мыши
mousemove Движение курсора
mouseenter Наведение на элемент (не всплывает)
mouseleave Вывод курсора из элемента (не всплывает)
mouseover Наведение на элемент (всплывает)
mouseout Вывод курсора из элемента (всплывает)

События формы

Событие Описание
focus Получение фокуса на элементе
blur Фокус ушел с элемента
focusin Фокус на элементе (всплывает)
focusout Фокус ушел (всплывает)
change Изменение значения элемента
keydown Нажатие клавиши на клавиатуре
keypress Удержание клавиши (только буквы и цифры)
keyup Отжатие клавиши
select Выбор текста в input или textarea
submit Отправка формы

📜 Работа с событиями

Для работы с событиями существует три основных метода:

Метод Описание
on(event, handler) Добавление обработчика события
trigger(event) Инициация события из скрипта
off(event) Отключение обработчика событий

Примеры

// Вешаем обработчик $("p").on("click", function() { alert("Click!"); }); // Инициируем событие программно $("p").trigger("click"); // Отключаем обработчик $("p").off("click");

Использование this

Внутри обработчика вы можете получить доступ к DOM-элементу используя ключевое слово this. Если нужно использовать jQuery-инструменты, используйте конструкцию $(this):

$("p").on("click", function() { $(this).css("color", "red"); // изменяем цвет кликнутого параграфа });

Делегирование событий

// Обработчик для элементов, добавленных динамически $("ul").on("click", "li", function() { $(this).toggleClass("selected"); });

📜 Анимация

jQuery позволяет очень легко анимировать элементы на странице. При такой анимации происходит изменение атрибутов width, height, opacity и т.д.

Показать/Скрыть

$('img').hide(); // скрыть все картинки $('img').show(); // показать все картинки $('img').toggle(); // переключить (hide ↔ show) $('img').hide('slow'); // slow == 600ms, fast == 200ms $('img').show(400); // показать за 400ms $('img').hide('slow', function() { alert("Images was hidden"); // callback после завершения });

Slide (вертикальная анимация)

$('img').slideUp(); // скрыть с анимацией вверх $('img').slideDown(); // показать с анимацией вниз $('img').slideToggle(); // переключить

Fade (изменение прозрачности)

$('img').fadeIn(); // проявление (opacity: 0 → 1) $('img').fadeOut(); // затухание (opacity: 1 → 0) $('img').fadeToggle(); // переключение $('img').fadeTo("slow", 0.5); //изменение до указанного значения

Произвольная анимация

Метод animate() позволяет анимировать любые CSS-свойства:

$('img').animate({ 'opacity': 0.5, 'height': '100px', 'width': '100px' }, 4000); // за 4 секунды // С callback $('div').animate({ left: '250px', opacity: '0.5', height: '150px', width: '150px' }, { duration: 1000, complete: function() { console.log("Анимация завершена!"); } });

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

Рассмотрим создание элемента с атрибутами id и class:

// Самый быстрый способ var myDiv = $('<div>').attr({'id':'my', 'class':'some'}); // Немного медленнее var myDiv = $('<div>', {'id':'my', 'class':'some'}); // Самый медленный var myDiv = $('<div id="my" class="some"></div>');

Манипуляция с элементами

Вставка после/перед (after/before)

// after(content) — вставляет контент ПОСЛЕ каждого элемента $("p").after("<hr/>"); // после каждого <p> будет добавлена линия // insertAfter(element) — вставляет элемент ПОСЛЕ указанного $("<hr/>").insertAfter("p"); // before(content) — вставляет контент ПЕРЕД каждым элементом $("p").before("<hr/>"); // insertBefore(element) — вставляет элемент ПЕРЕД указанным $("<hr/>").insertBefore("p");

Вставка внутрь (append/prepend)

// append(content) — вставляет контент В КОНЕЦ элемента $("p").append("<hr/>"); // appendTo(element) — вставляет выбранный контент в конец $("<hr/>").appendTo("p"); // prepend(content) — вставляет контент В НАЧАЛО элемента $("p").prepend("<hr/>"); // prependTo(element) — вставляет выбранный контент в начало $("<hr/>").prependTo("p");

Замена элементов

// replaceWith(content) — заменяет найденные элементы новым $("p").replaceWith("<hr/>"); // replaceAll(target) — вставляет контент взамен найденному $("<hr/>").replaceAll("h3");

Клонирование и удаление

// clone(withDataAndEvents) — клонирует элементы var copy = $("p").clone(); // без событий var copy = $("p").clone(true); // с событиями // empty() — удаляет текст и дочерние элементы $("div").empty(); // remove() — удаляет элемент из DOM $("div").remove();

📜 Работа с содержимым

Метод Описание
html() Возвращает HTML заданного элемента
html(newHtml) Заменяет HTML в заданном элементе
text() Возвращает текст заданного элемента
text(newText) Заменяет текст (HTML экранируется)
val() Получает значение input, select, textarea
val(newValue) Устанавливает значение

Примеры

// Получение/установка HTML $("div").html(); // получить HTML $("div").html("<p>Новый контент</p>"); // установить HTML // Получение/установка текста $("div").text(); // получить текст $("section").text("Some <strong>text</strong>"); // Результат: Some &lt;strong&gt;text&lt;/strong&gt; // Работа с полями форм $("#name").val(); // получить значение $("#name").val("John Doe"); // установить значение

📜 Размеры элементов

Метод Описание
height() Высота элемента (без padding, border)
height(value) Установка высоты
width() Ширина элемента (без padding, border)
width(value) Установка ширины
innerHeight() Высота + padding
innerWidth() Ширина + padding
outerHeight() Высота + padding + border
outerWidth() Ширина + padding + border
outerHeight(true) Высота + padding + border + margin
outerWidth(true) Ширина + padding + border + margin

Примеры

// Получение размеров var h = $("div").height(); // высота контента var w = $("div").width(); // ширина контента var ih = $("div").innerHeight(); // с padding var oh = $("div").outerHeight(); // с padding и border var ohm = $("div").outerHeight(true); // с padding, border и margin // Установка размеров $("div").height(200); // установить 200px $("div").width("50%"); // установить 50%

📜 AJAX запросы

jQuery значительно упрощает работу с AJAX-запросами:

Метод $.ajax()

$.ajax({ url: "/api/data", method: "GET", dataType: "json", success: function(data) { console.log("Успех:", data); }, error: function(xhr, status, error) { console.log("Ошибка:", error); } });

Сокращенные методы

// GET-запрос $.get("/api/users", function(data) { console.log(data); }); // POST-запрос $.post("/api/users", {name: "John", age: 30}, function(data) { console.log("Создан:", data); }); // Загрузка JSON $.getJSON("/api/users.json", function(data) { console.log("JSON:", data); }); // Загрузка HTML $("#content").load("/page.html");

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

$.ajax({ url: "/api/data", method: "GET" }) .done(function(data) { console.log("Успех:", data); }) .fail(function(xhr, status, error) { console.log("Ошибка:", error); }) .always(function() { console.log("Завершено"); });

📜 Цепочки методов (Method Chaining)

Одна из мощных возможностей jQuery — цепочки методов. Большинство методов возвращают объект jQuery, что позволяет вызывать методы последовательно:

$("p") .addClass("highlight") .css("font-size", "16px") .fadeIn(500) .html("Новый текст"); // То же самое, но без цепочки: $("p").addClass("highlight"); $("p").css("font-size", "16px"); $("p").fadeIn(500); $("p").html("Новый текст");

📜 Утилиты jQuery

$.each() — перебор массивов и объектов

// Перебор массива var arr = [1, 2, 3, 4, 5]; $.each(arr, function(index, value) { console.log(index + ": " + value); }); // Перебор объекта var obj = {name: "John", age: 30}; $.each(obj, function(key, value) { console.log(key + ": " + value); });

$.map() — преобразование массивов

var arr = [1, 2, 3, 4, 5]; var doubled = $.map(arr, function(value, index) { return value * 2; }); console.log(doubled); // [2, 4, 6, 8, 10]

$.grep() — фильтрация массивов

var arr = [1, 2, 3, 4, 5]; var filtered = $.grep(arr, function(value) { return value > 2; }); console.log(filtered); // [3, 4, 5]

$.extend() — объединение объектов

var obj1 = {a: 1, b: 2}; var obj2 = {b: 3, c: 4}; var result = $.extend({}, obj1, obj2); console.log(result); // {a: 1, b: 3, c: 4}

$.trim() — удаление пробелов

var str = " hello world "; var trimmed = $.trim(str); console.log(trimmed); // "hello world"

📜 Практический пример: Todo-список

<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>jQuery Todo List</title> <style> .completed { text-decoration: line-through; color: #999; } li { cursor: pointer; padding: 5px; } li:hover { background: #f0f0f0; } </style> </head> <body> <h1>Todo List</h1> <input type="text" id="newTask" placeholder="Новая задача"> <button id="addBtn">Добавить</button> <ul id="taskList"></ul> <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script> <script> $(function() { // Добавление новой задачи $("#addBtn").on("click", function() { var task = $("#newTask").val().trim(); if (task) { var li = $("<li>") .text(task) .append(' <button class="delete">✕</button>'); $("#taskList").append(li); $("#newTask").val("").focus(); } }); // Добавление по Enter $("#newTask").on("keypress", function(e) { if (e.which === 13) { $("#addBtn").click(); } }); // Переключение выполнения задачи $("#taskList").on("click", "li", function(e) { if (!$(e.target).hasClass("delete")) { $(this).toggleClass("completed"); } }); // Удаление задачи $("#taskList").on("click", ".delete", function(e) { e.stopPropagation(); $(this).parent().fadeOut(300, function() { $(this).remove(); }); }); }); </script> </body> </html>

📜 Сравнение jQuery и нативного JavaScript

Задача jQuery Нативный JS
Выбор элемента $("#id") document.getElementById("id")
Выбор по классу $(".class") document.querySelectorAll(".class")
Добавить класс $("p").addClass("active") el.classList.add("active")
Изменить CSS $("p").css("color", "red") el.style.color = "red"
Событие клик $("p").on("click", fn) el.addEventListener("click", fn)
AJAX GET $.get(url, fn) fetch(url).then(r => r.json())

📜 Плагины jQuery

jQuery имеет огромную экосистему плагинов. Вот некоторые популярные:

  • jQuery UI — компоненты интерфейса (datepicker, dialog, tabs)
  • Slick — карусели и слайдеры
  • Select2 — расширенные select-боксы
  • DataTables — таблицы с сортировкой, поиском, пагинацией
  • Magnific Popup — модальные окна и галереи
  • jQuery Validation — валидация форм

Создание собственного плагина

(function($) { $.fn.highlight = function(options) { var settings = $.extend({ color: "yellow", duration: 500 }, options); return this.each(function() { $(this).css("background-color", settings.color) .delay(settings.duration) .queue(function(next) { $(this).css("background-color", "transparent"); next(); }); }); }; })(jQuery); // Использование $("p").highlight({color: "lightblue", duration: 1000});

📜 Когда использовать jQuery?

✅ Плюсы jQuery
  • Простой и понятный синтаксис
  • Кроссбраузерная совместимость (актуально для старых браузеров)
  • Быстрое прототипирование
  • Огромное количество плагинов
  • Отличная документация
⚠️ Минусы jQuery
  • Дополнительная зависимость (~30 КБ минимизированного кода)
  • Медленнее нативного JavaScript
  • Современные браузеры имеют аналогичные нативные API
  • Фреймворки (React, Vue) предоставляют лучшие паттерны для сложных приложений

Миграция с jQuery на нативный JS

Если вы решили отказаться от jQuery, вот полезные ресурсы:

  • You Might Not Need jQuery — сайт с примерами замены jQuery на нативный JS
  • querySelector/querySelectorAll — заменяют большинство селекторов jQuery
  • fetch API — современная замена $.ajax()
  • classList API — работа с классами
  • addEventListener — обработка событий

Резюме

В этой главе мы рассмотрели:

  • ✅ Подключение jQuery (локально и через CDN)
  • ✅ Ожидание загрузки документа с $(function(){})
  • ✅ Выбор элементов (CSS-селекторы и методы навигации)
  • ✅ Работу с CSS-стилями и классами
  • ✅ Работу с атрибутами (attr) и свойствами (prop)
  • ✅ Обработку событий (on, trigger, off)
  • ✅ Анимацию (hide/show, slide, fade, animate)
  • ✅ Создание и манипуляцию элементами
  • ✅ Работу с содержимым (html, text, val)
  • ✅ Работу с размерами элементов
  • ✅ AJAX-запросы
  • ✅ Цепочки методов (method chaining)
  • ✅ Утилиты jQuery (each, map, grep, extend)
  • ✅ Практический пример: Todo-список
  • ✅ Сравнение с нативным JavaScript
  • ✅ Создание плагинов
💡 Заключение

jQuery остается отличной библиотекой для быстрого прототипирования и поддержки старых проектов. Однако для новых проектов рекомендуется рассмотреть нативные возможности JavaScript или современные фреймворки.

Глава 31. Best Practices и оптимизация

Введение

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

📜 1. Оптимизация кода

1.1. Кэширование DOM-элементов

// ❌ Плохо: повторяющиеся запросы к DOM document.querySelector('.box').style.color = '#FF0000'; document.querySelector('.box').style.fontSize = '16px'; document.querySelector('.box').style.padding = '10px'; // ✅ Хорошо: сохранили в переменную const box = document.querySelector('.box'); box.style.color = '#FF0000'; box.style.fontSize = '16px'; box.style.padding = '10px';
💡 Почему это важно?

Каждый вызов querySelector() — это поиск по всему DOM-дереву. Кэширование элемента в переменной значительно ускоряет работу.

1.2. Группировка настроек в объектах

// ✅ Группируем настройки в конфигурационном объекте const config = { colors: { primary: '#0066cc', secondary: '#6c757d', success: '#28a745', danger: '#dc3545', warning: '#ffc107' }, delays: { animation: 300, tooltip: 200, modal: 500 }, apiUrl: 'https://api.example.com', timeout: 5000 }; // Использование element.style.color = config.colors.primary; setTimeout(showTooltip, config.delays.tooltip); fetch(config.apiUrl, {timeout: config.timeout});

1.3. Тернарный оператор для простых условий

// ❌ Громоздкий if-else для простого условия let status; if (isLoggedIn) { status = 'Авторизован'; } else { status = 'Гость'; } // ✅ Тернарный оператор const status = isLoggedIn ? 'Авторизован' : 'Гость'; // Можно вкладывать (но не злоупотребляйте!) const level = score > 90 ? 'Высокий' : score > 60 ? 'Средний' : 'Низкий';

1.4. Switch вместо множественных if-else

// ❌ Множественные if-else if (day === 'Monday') { console.log('Начало недели'); } else if (day === 'Friday') { console.log('Конец недели'); } else if (day === 'Saturday' || day === 'Sunday') { console.log('Выходные'); } else { console.log('Рабочий день'); } // ✅ Switch (более читаемо) switch(day) { case 'Monday': console.log('Начало недели'); break; case 'Friday': console.log('Конец недели'); break; case 'Saturday': case 'Sunday': console.log('Выходные'); break; default: console.log('Рабочий день'); }

1.5. Деструктуризация

// ❌ Старый способ const firstName = user.firstName; const lastName = user.lastName; const age = user.age; // ✅ Деструктуризация объекта const {firstName, lastName, age} = user; // С переименованием const {firstName: name, age: userAge} = user; // Со значениями по умолчанию const {city = 'Не указан'} = user; // ✅ Деструктуризация массива const [first, second, ...rest] = [1, 2, 3, 4, 5]; console.log(first); // 1 console.log(second); // 2 console.log(rest); // [3, 4, 5] // Пропуск элементов const [, , third] = [1, 2, 3]; console.log(third); // 3

1.6. Spread оператор (...)

// Копирование массива const arr1 = [1, 2, 3]; const arr2 = [...arr1]; // [1, 2, 3] // Объединение массивов const combined = [...arr1, 4, 5, ...arr2]; // Копирование объекта const user = {name: 'Alice', age: 25}; const userCopy = {...user}; // Добавление/изменение свойств const userWithEmail = { ...user, email: 'alice@example.com', age: 26 // перезапишет age }; // Передача аргументов в функцию const numbers = [5, 10, 15]; Math.max(...numbers); // 15

1.7. Template literals (шаблонные строки)

// ❌ Конкатенация строк const greeting = 'Привет, ' + name + '! Тебе ' + age + ' лет.'; // ✅ Template literals const greeting = `Привет, ${name}! Тебе ${age} лет.`; // Многострочные строки const html = ` <div class="card"> <h1>${title}</h1> <p>${text}</p> </div> `; // Вычисления внутри const total = `Итого: ${price * quantity} руб.`; // Вызов функций const message = `Результат: ${calculate(a, b)}`;

1.8. Стрелочные функции

// ❌ Обычная функция const numbers = [1, 2, 3]; const doubled = numbers.map(function(n) { return n * 2; }); // ✅ Стрелочная функция (короткая запись) const doubled = numbers.map(n => n * 2); // С несколькими параметрами const sum = (a, b) => a + b; // С телом функции const process = (data) => { const result = data * 2; return result + 10; }; // Без параметров const getRandomNumber = () => Math.random();
⚠️ Когда НЕ использовать стрелочные функции
  • Методы объектов (проблемы с this)
  • Конструкторы (нельзя использовать new)
  • Когда нужен arguments

📜 2. Именование переменных и функций

2.1. Используйте осмысленные имена

// ❌ Плохо const d = new Date(); const x = users.filter(u => u.a > 18); // ✅ Хорошо const currentDate = new Date(); const activeUsers = users.filter(user => user.age > 18);

2.2. Соглашения об именовании

// camelCase для переменных и функций const userName = 'Alice'; function calculateTotal() {} // PascalCase для классов и конструкторов class UserAccount {} function User() {} // UPPER_CASE для констант const MAX_USERS = 100; const API_KEY = 'abc123'; // Булевы переменные с префиксами is, has, should const isActive = true; const hasPermission = false; const shouldUpdate = true;

2.3. Избегайте сокращений

// ❌ Плохо const btn = document.querySelector('button'); const msg = 'Hello'; const usr = getCurrentUser(); // ✅ Хорошо const button = document.querySelector('button'); const message = 'Hello'; const user = getCurrentUser();

📜 3. Работа с переменными

3.1. Используйте const по умолчанию

// ✅ const для значений, которые не переназначаются const MAX_SIZE = 100; const users = []; // можно изменять содержимое массива users.push('Alice'); // OK // let только если нужно переназначение let counter = 0; counter++; // ❌ Избегайте var (устаревшее) var oldStyle = 'bad';

3.2. Объявляйте переменные в начале блока

// ✅ Хорошо function processData(data) { const result = []; const total = 0; // ... код ... return result; }

3.3. Одна переменная — одна строка

// ❌ Плохо const a = 1, b = 2, c = 3; // ✅ Хорошо const a = 1; const b = 2; const c = 3;

📜 4. Работа с функциями

4.1. Функции должны делать одну вещь

// ❌ Плохо: функция делает слишком много function processUserData(user) { validateUser(user); saveToDatabase(user); sendEmail(user); updateUI(user); } // ✅ Хорошо: разделили на маленькие функции function processUserData(user) { if (!isValidUser(user)) return false; saveUser(user); notifyUser(user); refreshUserList(); return true; }

4.2. Используйте значения по умолчанию

// ❌ Старый способ function greet(name) { name = name || 'Гость'; console.log('Привет, ' + name); } // ✅ ES6 параметры по умолчанию function greet(name = 'Гость') { console.log(`Привет, ${name}`); } // Для объектов function createUser({name = 'Аноним', age = 0} = {}) { return {name, age}; }

4.3. Раннее возвращение (Early Return)

// ❌ Глубокая вложенность function processOrder(order) { if (order) { if (order.items.length > 0) { if (order.isPaid) { // обработка заказа } } } } // ✅ Ранний выход function processOrder(order) { if (!order) return; if (order.items.length === 0) return; if (!order.isPaid) return; // обработка заказа }

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

5.1. Используйте методы массивов

const numbers = [1, 2, 3, 4, 5]; // map - преобразование const doubled = numbers.map(n => n * 2); // filter - фильтрация const evens = numbers.filter(n => n % 2 === 0); // reduce - свёртка const sum = numbers.reduce((acc, n) => acc + n, 0); // find - поиск первого элемента const firstEven = numbers.find(n => n % 2 === 0); // some - хотя бы один const hasEven = numbers.some(n => n % 2 === 0); // every - все элементы const allPositive = numbers.every(n => n > 0);

5.2. Избегайте мутации данных

// ❌ Мутация оригинального массива const arr = [1, 2, 3]; arr.push(4); // изменяет arr // ✅ Создание нового массива const arr = [1, 2, 3]; const newArr = [...arr, 4]; // arr не изменён // Для объектов const user = {name: 'Alice', age: 25}; // ❌ user.age = 26; // ✅ const updatedUser = {...user, age: 26};

📜 6. Обработка ошибок

6.1. Всегда используйте try-catch для async/await

// ✅ Обработка ошибок async function fetchData() { try { const response = await fetch('/api/data'); const data = await response.json(); return data; } catch (error) { console.error('Ошибка загрузки:', error); return null; } }

6.2. Проверяйте входные данные

function divide(a, b) { if (typeof a !== 'number' || typeof b !== 'number') { throw new TypeError('Аргументы должны быть числами'); } if (b === 0) { throw new Error('Деление на ноль'); } return a / b; }

📜 7. Асинхронный код

7.1. Предпочитайте async/await промисам

// ❌ Цепочки промисов fetch('/api/user') .then(response => response.json()) .then(user => fetch(`/api/posts/${user.id}`)) .then(response => response.json()) .then(posts => console.log(posts)) .catch(error => console.error(error)); // ✅ async/await (более читаемо) async function getUserPosts() { try { const userResponse = await fetch('/api/user'); const user = await userResponse.json(); const postsResponse = await fetch(`/api/posts/${user.id}`); const posts = await postsResponse.json(); console.log(posts); } catch (error) { console.error(error); } }

📜 8. Производительность

8.1. Debounce и Throttle

// Debounce - выполнение после паузы function debounce(func, delay) { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), delay); }; } // Использование для поиска const searchInput = document.querySelector('#search'); const debouncedSearch = debounce(performSearch, 300); searchInput.addEventListener('input', debouncedSearch); // Throttle - ограничение частоты вызовов function throttle(func, limit) { let inThrottle; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; } // Использование для скролла window.addEventListener('scroll', throttle(handleScroll, 100));

8.2. Минимизируйте работу с DOM

// ❌ Много операций с DOM for (let i = 0; i < 1000; i++) { document.body.innerHTML += `<div>${i}</div>`; } // ✅ Используйте DocumentFragment const fragment = document.createDocumentFragment(); for (let i = 0; i < 1000; i++) { const div = document.createElement('div'); div.textContent = i; fragment.appendChild(div); } document.body.appendChild(fragment);

📜 9. Размещение скриптов

9.1. Defer и Async

<!-- defer: загрузка параллельно, выполнение после HTML --> <script src="script.js" defer></script> <!-- async: загрузка и выполнение параллельно (порядок не гарантирован) --> <script src="analytics.js" async></script>
💡 Когда что использовать?
  • defer — для скриптов, которые зависят от DOM и друг от друга
  • async — для независимых скриптов (аналитика, реклама)
  • Без атрибутов — размещайте перед </body>

9.2. Оптимальное размещение

<!-- ❌ Плохо: блокирует рендеринг --> <head> <script src="large-script.js"></script> </head> <!-- ✅ Хорошо: не блокирует --> <body> <!-- контент страницы --> <script src="script.js"></script> </body> <!-- ✅ Ещё лучше: с defer --> <head> <script src="script.js" defer></script> </head>

📜 10. Минификация и сжатие

💡 Минификация кода

Минификация удаляет пробелы, комментарии и сокращает имена переменных, уменьшая размер файла на 30-50%.

Популярные инструменты:

  • JavaScript Minifier (онлайн)
  • Terser (NPM, самый популярный)
  • UglifyJS (классика)
  • Webpack/Vite — автоматическая минификация в production

Пример минификации

// До минификации (150 байт) function calculateTotal(price, quantity) { const tax = 0.2; const subtotal = price * quantity; return subtotal * (1 + tax); } // После минификации (70 байт) function calculateTotal(p,q){const t=.2,s=p*q;return s*(1+t)}

📜 11. Комментарии и документация

11.1. Пишите понятный код вместо комментариев

// ❌ Плохо: код нуждается в комментариях // Проверяем возраст пользователя if (u.a > 18) { // Разрешаем доступ a = true; } // ✅ Хорошо: код самодокументируемый const isAdult = user.age > 18; if (isAdult) { allowAccess = true; }

11.2. JSDoc для функций

/** * Вычисляет общую стоимость заказа с налогом * @param {number} price - Цена за единицу товара * @param {number} quantity - Количество товара * @param {number} [taxRate=0.2] - Ставка налога (по умолчанию 20%) * @returns {number} Общая стоимость с налогом */ function calculateTotal(price, quantity, taxRate = 0.2) { const subtotal = price * quantity; return subtotal * (1 + taxRate); }

📜 12. Чек-лист Best Practices

✅ Проверьте свой код
  • ☐ Используется const по умолчанию, let при необходимости
  • ☐ Нет var
  • ☐ Осмысленные имена переменных и функций
  • ☐ Функции делают одну вещь
  • ☐ Используются стрелочные функции где уместно
  • ☐ Деструктуризация вместо множественных присваиваний
  • ☐ Template literals вместо конкатенации
  • ☐ Spread оператор для копирования
  • ☐ Обработка ошибок с try-catch
  • ☐ async/await вместо цепочек промисов
  • ☐ Кэширование DOM-элементов
  • ☐ Минимизация работы с DOM
  • ☐ Скрипты с defer или перед </body>
  • ☐ Код минифицирован для production

📜 Резюме

В этой главе мы рассмотрели:

  • ✅ Оптимизацию кода (кэширование, группировка настроек)
  • ✅ Современный синтаксис ES6+ (деструктуризация, spread, template literals)
  • ✅ Именование переменных и функций
  • ✅ Работу с переменными (const/let, объявления)
  • ✅ Принципы написания функций
  • ✅ Методы массивов и иммутабельность
  • ✅ Обработку ошибок
  • ✅ Асинхронный код (async/await)
  • ✅ Оптимизацию производительности (debounce, throttle)
  • ✅ Размещение скриптов (defer, async)
  • ✅ Минификацию и сжатие
  • ✅ Комментирование и документацию
  • ✅ Чек-лист проверки кода

Глава 32. Отладка и диагностика

Введение

Отладка — это процесс поиска и исправления ошибок в коде. В этой главе рассмотрим типы ошибок, инструменты отладки и распространённые проблемы в JavaScript.

📜 1. Типы ошибок

1.1. Синтаксические ошибки (Syntax Errors)

🔴 Синтаксические ошибки

Грамматические ошибки в коде. Скрипт не запустится, браузер выдаст ошибку при парсинге.

// ❌ Ошибка: пропущена закрывающая скобка if (x > 0 { console.log('positive'); } // SyntaxError: Unexpected token '{' // ❌ Ошибка: пропущена запятая const obj = { name: 'Alice' age: 25 }; // SyntaxError: Unexpected identifier // ✅ Правильно if (x > 0) { console.log('positive'); } const obj = { name: 'Alice', age: 25 };

1.2. Ошибки выполнения (Runtime Errors)

🟡 Ошибки выполнения

Возникают во время работы программы. Код синтаксически верный, но при выполнении происходит ошибка.

// ReferenceError - переменная не определена console.log(unknownVariable); // ReferenceError: unknownVariable is not defined // TypeError - неправильный тип операции const num = 5; num.toUpperCase(); // TypeError: num.toUpperCase is not a function // RangeError - значение вне допустимого диапазона const arr = new Array(-1); // RangeError: Invalid array length // URIError - неправильное использование URI функций decodeURIComponent('%'); // URIError: URI malformed

1.3. Логические ошибки (Logic Errors)

🔵 Логические ошибки

Код работает без ошибок, но результат не тот, что ожидался. Самые сложные для поиска!

// ❌ Ошибка: присваивание вместо сравнения if (x = 5) { // должно быть x === 5 console.log('x равно 5'); } // ❌ Ошибка: неправильный порядок операций const total = price + tax * quantity; // (price + tax) * quantity ? // ❌ Ошибка: off-by-one for (let i = 0; i <= arr.length; i++) { // должно быть i < arr.length console.log(arr[i]); } // ✅ Правильно if (x === 5) { console.log('x равно 5'); }

📜 2. Консоль разработчика

Консоль — главный инструмент отладки JavaScript. Доступна в Chrome DevTools (F12), Firefox Developer Tools, Safari Web Inspector.

2.1. console.log() и вариации

// Простой вывод console.log('Привет, мир!'); // Несколько значений const x = 5, y = 10; console.log('x =', x, 'y =', y); // Объекты (развёрнутый вид) const user = {name: 'Alice', age: 25}; console.log(user); // Форматированный вывод console.log('Пользователь: %s, возраст: %d', 'Alice', 25); // С CSS-стилями console.log('%cВажное сообщение', 'color: red; font-size: 20px; font-weight: bold;');

2.2. Другие методы console

// console.error() - ошибка (красным цветом) console.error('Произошла ошибка!'); // console.warn() - предупреждение (жёлтым) console.warn('Это предупреждение'); // console.info() - информационное сообщение console.info('Информация'); // console.table() - таблица (удобно для массивов объектов) const users = [ {name: 'Alice', age: 25}, {name: 'Bob', age: 30} ]; console.table(users); // console.group() / console.groupEnd() - группировка console.group('Пользовательские данные'); console.log('Имя: Alice'); console.log('Возраст: 25'); console.groupEnd(); // console.groupCollapsed() - свёрнутая группа console.groupCollapsed('Детали'); console.log('Детальная информация'); console.groupEnd();

2.3. Замер производительности

// console.time() / console.timeEnd() - замер времени console.time('myTimer'); for (let i = 0; i < 1000000; i++) { // какие-то вычисления } console.timeEnd('myTimer'); // myTimer: 123.456ms // console.count() - счётчик вызовов function processItem(item) { console.count('processItem вызван'); // обработка } processItem('a'); // processItem вызван: 1 processItem('b'); // processItem вызван: 2 processItem('c'); // processItem вызван: 3 // console.trace() - стек вызовов function foo() { function bar() { console.trace('Trace'); } bar(); } foo();

2.4. console.assert() - проверка условий

// Выводит ошибку только если условие false const x = 5; console.assert(x === 10, 'x должен быть равен 10'); // Assertion failed: x должен быть равен 10 // Полезно для валидации function divide(a, b) { console.assert(b !== 0, 'Деление на ноль!'); return a / b; }

📜 3. Отладчик (Debugger)

Отладчик позволяет пошагово выполнять код, проверять значения переменных и анализировать стек вызовов.

3.1. Ключевое слово debugger

function calculate(a, b) { debugger; // программа остановится здесь (если открыты DevTools) const sum = a + b; const product = a * b; return {sum, product}; } calculate(5, 10);

3.2. Точки останова (Breakpoints)

💡 Как использовать отладчик
  1. Откройте DevTools (F12 или Ctrl+Shift+I)
  2. Перейдите на вкладку Sources (Chrome) или Debugger (Firefox)
  3. Найдите нужный файл в дереве файлов слева
  4. Кликните на номер строки, чтобы поставить breakpoint (синяя метка)
  5. Обновите страницу — выполнение остановится на breakpoint

3.3. Управление выполнением

Кнопка Горячая клавиша Действие
Resume (▶️) F8 Продолжить выполнение до следующего breakpoint
Step Over (⤵️) F10 Выполнить текущую строку (не заходя в функции)
Step Into (⬇️) F11 Войти в функцию
Step Out (⬆️) Shift+F11 Выйти из текущей функции
Deactivate breakpoints Ctrl+F8 Временно отключить все breakpoints

3.4. Типы breakpoints

// 1. Обычный breakpoint - остановка на строке // 2. Условный breakpoint - остановка при условии // Правый клик на номер строки → Add conditional breakpoint // Условие: i === 50 // 3. Logpoint - вывод в консоль без остановки // Условие: console.log('i =', i) // 4. XHR/Fetch breakpoints - остановка при AJAX-запросах // 5. Event listener breakpoints - остановка при событиях // (click, keydown, и т.д.)

3.5. Watch и Scope

💡 Панели отладчика
  • Watch — отслеживание значений выражений (можно добавить любое выражение)
  • Scope — текущие переменные в области видимости (Local, Closure, Global)
  • Call Stack — стек вызовов функций
  • Breakpoints — список всех установленных точек останова

📜 4. Распространённые ошибки

4.1. Незакрытые скобки

// ❌ Ошибка function test() { console.log('Hello'; } // SyntaxError: missing ) after argument list // ❌ Ошибка const arr = [1, 2, 3; // SyntaxError: Unexpected token ';' // ✅ Правильно function test() { console.log('Hello'); } const arr = [1, 2, 3];

4.2. Проблемы с кавычками

// ❌ Ошибка: смешаны ' и " const name = "It's a string'; // ❌ Ошибка: не экранирована кавычка const text = 'He said: 'Hello''; // ✅ Правильно const name = "It's a string"; // или const name = 'It\'s a string'; // экранирование // или const name = `It's a string`; // template literal

4.3. Чувствительность к регистру

// ❌ Ошибка const myVariable = 10; console.log(myvariable); // ReferenceError // ❌ Ошибка const User = {name: 'Alice'}; console.log(user.name); // ReferenceError // ✅ Правильно console.log(myVariable); console.log(User.name);

4.4. Область видимости (Scope)

// ❌ Ошибка: переменная недоступна function test() { const x = 10; } console.log(x); // ReferenceError: x is not defined // ❌ Ошибка: let в блоке if (true) { let y = 20; } console.log(y); // ReferenceError // ✅ Правильно const x = 10; // глобальная переменная function test() { console.log(x); // доступна внутри функции } // или function test() { const x = 10; return x; } const result = test(); console.log(result);

4.5. Асинхронные операции

// ❌ Ошибка: данные ещё не загрузились let data; fetch('https://api.example.com/data') .then(response => response.json()) .then(json => data = json); console.log(data); // undefined // ✅ Правильно: await async function getData() { const response = await fetch('https://api.example.com/data'); const data = await response.json(); console.log(data); // данные загружены return data; } // ✅ Правильно: then fetch('https://api.example.com/data') .then(response => response.json()) .then(data => { console.log(data); // данные загружены });

4.6. Проблемы с this

// ❌ Ошибка: потеря контекста const user = { name: 'Alice', greet: function() { console.log('Hello, ' + this.name); } }; const greetFunc = user.greet; greetFunc(); // Hello, undefined (this = window) // ✅ Правильно: bind const greetFunc = user.greet.bind(user); greetFunc(); // Hello, Alice // ✅ Правильно: стрелочная функция const user = { name: 'Alice', greet: function() { setTimeout(() => { console.log('Hello, ' + this.name); // this = user }, 1000); } };

4.7. Изменение массива во время итерации

// ❌ Ошибка: изменяем массив в цикле const arr = [1, 2, 3, 4, 5]; for (let i = 0; i < arr.length; i++) { if (arr[i] % 2 === 0) { arr.splice(i, 1); // удаляем элемент — сбивается индекс! } } // ✅ Правильно: filter создаёт новый массив const arr = [1, 2, 3, 4, 5]; const filtered = arr.filter(item => item % 2 !== 0); // ✅ Правильно: идём с конца const arr = [1, 2, 3, 4, 5]; for (let i = arr.length - 1; i >= 0; i--) { if (arr[i] % 2 === 0) { arr.splice(i, 1); } }

4.8. Сравнение объектов

// ❌ Ошибка: сравнение по ссылке const obj1 = {x: 1}; const obj2 = {x: 1}; console.log(obj1 === obj2); // false (разные объекты) // ✅ Правильно: сравнение значений function isEqual(obj1, obj2) { return JSON.stringify(obj1) === JSON.stringify(obj2); } console.log(isEqual(obj1, obj2)); // true // или библиотека lodash // _.isEqual(obj1, obj2);

📜 5. Стратегии отладки

5.1. Метод "разделяй и властвуй"

// Добавляйте console.log в разных местах function complexFunction(data) { console.log('1. Вход:', data); const processed = processData(data); console.log('2. После обработки:', processed); const filtered = filterData(processed); console.log('3. После фильтрации:', filtered); const result = formatData(filtered); console.log('4. Результат:', result); return result; }

5.2. Упрощение кода

// Если код не работает, упростите его // ❌ Сложная цепочка const result = data.map(x => x * 2) .filter(x => x > 10) .reduce((a, b) => a + b, 0); // ✅ Разбейте на шаги для отладки const doubled = data.map(x => x * 2); console.log('Doubled:', doubled); const filtered = doubled.filter(x => x > 10); console.log('Filtered:', filtered); const result = filtered.reduce((a, b) => a + b, 0); console.log('Result:', result);

5.3. Резиновая уточка (Rubber Duck Debugging)

💡 Метод резиновой уточки

Объясните свой код вслух (резиновой уточке, коллеге, или просто себе). Часто в процессе объяснения вы сами находите ошибку!

📜 6. Инструменты для отладки

6.1. Chrome DevTools

  • Elements — просмотр и редактирование HTML/CSS
  • Console — выполнение JS-кода и просмотр логов
  • Sources — отладчик, breakpoints
  • Network — анализ сетевых запросов
  • Performance — профилирование производительности
  • Memory — анализ утечек памяти
  • Application — localStorage, cookies, Service Workers

6.2. Расширения браузера

  • React Developer Tools — для React-приложений
  • Vue.js devtools — для Vue-приложений
  • Redux DevTools — для Redux state management

6.3. ESLint — статический анализ

// Устанавливаем ESLint npm install eslint --save-dev // Инициализация npx eslint --init // Проверка файлов npx eslint yourfile.js // Автоматическое исправление npx eslint yourfile.js --fix

📜 7. Типичные сообщения об ошибках

Ошибка Причина Решение
Uncaught ReferenceError: x is not defined Переменная не объявлена Проверьте написание, объявите переменную
Uncaught TypeError: Cannot read property 'x' of undefined Попытка обратиться к свойству undefined Проверьте, существует ли объект
Uncaught SyntaxError: Unexpected token Синтаксическая ошибка Проверьте скобки, запятые, кавычки
Uncaught RangeError: Maximum call stack size exceeded Бесконечная рекурсия Проверьте условие выхода из рекурсии
Failed to fetch Проблема с сетевым запросом Проверьте URL, CORS, интернет

📜 8. Чек-лист отладки

✅ Шаги при поиске ошибки
  1. ☐ Прочитайте сообщение об ошибке в консоли
  2. ☐ Найдите строку, где произошла ошибка
  3. ☐ Добавьте console.log() до и после проблемного места
  4. ☐ Проверьте типы данных (typeof, console.log)
  5. ☐ Используйте debugger или breakpoint
  6. ☐ Проверьте область видимости переменных
  7. ☐ Упростите код — удалите лишнее
  8. ☐ Погуглите текст ошибки
  9. ☐ Объясните код вслух (rubber duck debugging)
  10. ☐ Сделайте перерыв — свежий взгляд помогает!

📜 Резюме

В этой главе мы рассмотрели:

  • ✅ Три типа ошибок: синтаксические, runtime, логические
  • ✅ Методы console: log, error, warn, table, time, assert
  • ✅ Отладчик: breakpoints, step over/into/out
  • ✅ Типы breakpoints: обычные, условные, logpoints
  • ✅ 8 распространённых ошибок и их решения
  • ✅ Стратегии отладки: разделяй и властвуй, упрощение, rubber duck
  • ✅ Инструменты: Chrome DevTools, ESLint, расширения
  • ✅ Типичные сообщения об ошибках
  • ✅ Чек-лист из 10 шагов для поиска ошибок
💡 Главное правило отладки

Не паникуйте! Ошибки — это нормальная часть разработки. Каждая исправленная ошибка делает вас лучшим программистом. 🚀

Глава 33. Ресурсы для изучения JavaScript

Введение

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

📜 1. Официальная документация

1.1. MDN Web Docs (Mozilla Developer Network)

✅ Главный источник информации

URL: developer.mozilla.org

Описание: Самая полная и достоверная документация по JavaScript, HTML, CSS и Web API. Содержит примеры, совместимость с браузерами и лучшие практики.

Разделы:

  • JavaScript Guide (Руководство)
  • JavaScript Reference (Справочник)
  • Web APIs
  • Примеры и туториалы

1.2. ECMAScript спецификация

💡 Для продвинутых

URL: tc39.es/ecma262

Описание: Официальная спецификация языка JavaScript (ECMAScript). Технически сложна, но содержит точные определения всех возможностей языка.

1.3. Can I Use

💡 Проверка совместимости

URL: caniuse.com

Описание: База данных совместимости браузеров с различными JavaScript/CSS/HTML функциями. Показывает, какие браузеры поддерживают ту или иную возможность.

📜 2. Обучающие платформы

2.1. JavaScript.ru (Learn JavaScript)

✅ Лучший русскоязычный учебник

URL: learn.javascript.ru

Автор: Илья Кантор

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

Темы:

  • Основы JavaScript
  • Качество кода
  • Объекты, прототипы, классы
  • Промисы, async/await
  • DOM, события, формы
  • Анимация, Web компоненты

2.2. Metanit.com

✅ Русскоязычные учебники

URL: metanit.com/web/javascript

Описание: Подробные уроки по JavaScript и многим другим языкам программирования. Систематизированное изложение материала.

2.3. freeCodeCamp

💡 Интерактивное обучение (EN)

URL: freecodecamp.org

Описание: Бесплатная платформа с интерактивными уроками. Практика прямо в браузере, сертификаты после прохождения курсов.

Курсы:

  • JavaScript Algorithms and Data Structures
  • Front End Development Libraries
  • APIs and Microservices

2.4. Codecademy

💡 Интерактивные курсы (EN)

URL: codecademy.com

Описание: Интерактивное обучение программированию. Базовые курсы бесплатны, продвинутые — по подписке.

2.5. JavaScript30

💡 30 проектов за 30 дней (EN)

URL: javascript30.com

Автор: Wes Bos

Описание: Бесплатный курс из 30 практических проектов. Каждый день — новый проект на чистом JavaScript (без фреймворков).

📜 3. Книги

3.1. "JavaScript. Подробное руководство" (Дэвид Фланаган)

📚 JavaScript: The Definitive Guide

Автор: David Flanagan

Описание: Энциклопедический справочник по JavaScript. Охватывает все аспекты языка от основ до продвинутых тем.

Рекомендуется для: Опытных разработчиков, справочник

3.2. "Вы не знаете JS" (You Don't Know JS)

📚 Серия книг YDKJS

Автор: Kyle Simpson

URL: GitHub (русский перевод)

Описание: Глубокое погружение в механизмы работы JavaScript. 6 книг, покрывающих все аспекты языка.

Темы:

  • Область видимости и замыкания
  • this и прототипы объектов
  • Типы и грамматика
  • Асинхронность и производительность
  • ES6 и beyond

3.3. "JavaScript и jQuery" (Дэвид Макфарланд)

📚 Для начинающих

Автор: David Sawyer McFarland

Описание: Отличная книга для начинающих. Много примеров, иллюстраций и практических задач.

3.4. "Eloquent JavaScript" (Выразительный JavaScript)

📚 Бесплатная онлайн-книга

Автор: Marijn Haverbeke

URL: eloquentjavascript.net

Описание: Современная книга о JavaScript с акцентом на программирование. Доступна бесплатно онлайн.

📜 4. Видеокурсы и YouTube каналы

4.1. Русскоязычные каналы

Канал Описание
Vladilen Minin Курсы по JavaScript, React, Vue, Node.js. Четкое объяснение, много практики.
Фрилансер по жизни Уроки по веб-разработке, JavaScript, фреймворкам.
ulbi TV Подробные курсы по JavaScript, React, TypeScript.
WebDev с нуля Курсы для начинающих веб-разработчиков.

4.2. Англоязычные каналы

Канал Описание
Traversy Media Один из лучших каналов. Проекты, туториалы, курсы по всем технологиям.
The Net Ninja Плейлисты по JavaScript, фреймворкам, Node.js. Понятные объяснения.
Fireship Короткие, емкие видео. Отлично для быстрого изучения новых технологий.
Web Dev Simplified Упрощенные объяснения сложных концепций JavaScript.
JavaScript Mastery Полные проекты на JavaScript, React, Next.js.

📜 5. Практика и задачи

5.1. Codewars

✅ Задачи по программированию

URL: codewars.com

Описание: Тысячи задач (kata) разной сложности. Система рейтингов, обсуждение решений.

Уровни: 8 kyu (новичок) → 1 kyu (мастер)

5.2. LeetCode

💡 Подготовка к собеседованиям

URL: leetcode.com

Описание: Задачи по алгоритмам и структурам данных. Часто используется для подготовки к техническим интервью.

5.3. HackerRank

💡 Соревнования и сертификаты

URL: hackerrank.com

Описание: Задачи, соревнования, тесты на знание JavaScript. Можно получить сертификаты.

5.4. Exercism

💡 Менторство и обратная связь

URL: exercism.org

Описание: Бесплатная платформа с задачами и менторской поддержкой. Получайте отзывы от опытных разработчиков.

📜 6. Инструменты разработчика

6.1. Онлайн-редакторы кода

Инструмент URL Описание
CodePen codepen.io Лучший онлайн-редактор для фронтенда. HTML, CSS, JS в одном месте.
JSFiddle jsfiddle.net Быстрое создание и тестирование JS-кода.
StackBlitz stackblitz.com Онлайн IDE с поддержкой npm, фреймворков (React, Angular, Vue).
CodeSandbox codesandbox.io Полноценная среда разработки онлайн. Отлично для React-проектов.
Replit replit.com Онлайн IDE для множества языков, включая JavaScript/Node.js.

6.2. Редакторы кода

Редактор Описание
Visual Studio Code Самый популярный редактор для веб-разработки. Бесплатный, множество расширений.
WebStorm Мощная IDE от JetBrains. Платная, но с множеством встроенных функций.
Sublime Text Быстрый и легковесный редактор. Платный, но с бесплатным trial.
Atom Бесплатный редактор от GitHub. Настраиваемый, но медленнее VSCode.

6.3. Расширения VS Code

  • Prettier - форматирование кода
  • ESLint - проверка качества кода
  • JavaScript (ES6) code snippets - сниппеты
  • Live Server - локальный сервер
  • Path Intellisense - автодополнение путей

📜 7. Онлайн-инструменты

7.1. Валидаторы и проверка

7.2. Генераторы

Тестеры RegExp

✅ Онлайн-тестеры регулярных выражений

📜 🎨 7.3. Дизайн-ресурсы

Изображения (бесплатные)

Иконки

Удаление фона с изображений

Оптимизация изображений

📜 8. Библиотеки и фреймворки

8.1. JavaScript библиотеки

  • jQuery - манипуляции с DOM
  • Lodash - утилиты
  • Moment.js - работа с датами
  • date-fns - современная альтернатива Moment
  • Axios - HTTP запросы

8.2. Фреймворки

8.3. CDN для библиотек

📜 9. Блоги и новости

9.1. Dev.to

💡 Сообщество разработчиков

URL: dev.to/t/javascript

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

9.2. CSS-Tricks

💡 Не только CSS

URL: css-tricks.com

Описание: Много статей по JavaScript, особенно в контексте веб-разработки.

9.3. JavaScript Weekly

💡 Еженедельная рассылка

URL: javascriptweekly.com

Описание: Еженедельный дайджест новостей, статей и инструментов JavaScript.

📜 10. Сообщества

10.1. Stack Overflow

✅ Главный Q&A ресурс

URL: stackoverflow.com

Описание: Вопросы и ответы по программированию. Миллионы решений проблем.

10.2. Reddit

  • r/javascript — новости, обсуждения
  • r/learnjavascript — для начинающих, вопросы и помощь
  • r/webdev — общее сообщество веб-разработчиков

10.3. Discord серверы

  • The Programmer's Hangout
  • Reactiflux (React, но много общего JS)
  • freeCodeCamp

📜 11. Подкасты

11.1. Русскоязычные

  • Веб-стандарты — новости фронтенда
  • RadioJS — подкаст о JavaScript

11.2. Англоязычные

  • JavaScript Jabber — обсуждения с экспертами
  • Syntax.fm — Wes Bos и Scott Tolinski о веб-разработке
  • JS Party — новости и обсуждения JavaScript-сообщества

📜 12. Справочники и шпаргалки

12.1. Cheat Sheets

12.2. Roadmaps

✅ Дорожная карта разработчика

URL: roadmap.sh/javascript

Описание: Визуальная карта того, что нужно изучить JavaScript/Frontend/Backend разработчику.

📜 13. GitHub репозитории

13.1. Awesome JavaScript

💡 Коллекция ресурсов

URL: github.com/sorrycc/awesome-javascript

Описание: Огромная коллекция JavaScript-библиотек, фреймворков, инструментов.

13.2. 30 Seconds of Code

💡 Короткие сниппеты

URL: 30secondsofcode.org

Описание: Коллекция полезных JavaScript-сниппетов, которые можно понять за 30 секунд.

13.3. JavaScript Algorithms

💡 Алгоритмы и структуры данных

URL: github.com/trekhleb/javascript-algorithms

Описание: Реализации популярных алгоритмов и структур данных на JavaScript.

📜 14. Конференции

14.1. Международные

  • JSConf — серия конференций по JavaScript по всему миру
  • React Conf — официальная конференция от Facebook
  • Node.js Interactive — для Node.js разработчиков

14.2. Русскоязычные

  • HolyJS — крупнейшая JS-конференция в России
  • Я ❤ Фронтенд — конференция по фронтенд-разработке
  • FrontendConf — конференция Mail.ru Group

15. Полезные инструменты

Инструмент Назначение
Babel Транспайлер ES6+ → ES5
Webpack Сборщик модулей
Vite Современный быстрый сборщик
ESLint Линтер для поиска ошибок
Prettier Форматирование кода
npm/yarn/pnpm Менеджеры пакетов

📜 Рекомендации по обучению

✅ План изучения JavaScript
  1. Изучите основы — переменные, типы данных, операторы, функции
  2. Практикуйтесь ежедневно — решайте задачи на Codewars/LeetCode
  3. Создавайте проекты — делайте реальные приложения (to-do, калькулятор, игры)
  4. Читайте чужой код — изучайте open source проекты на GitHub
  5. Углубляйтесь в сложные темы — замыкания, прототипы, async/await
  6. Изучите фреймворк — React, Vue или Angular
  7. Изучите бэкенд — Node.js, Express, базы данных
  8. Следите за новостями — подпишитесь на JavaScript Weekly
  9. Участвуйте в сообществах — Stack Overflow, Reddit, Discord
  10. Никогда не останавливайтесь — JavaScript постоянно развивается!

Резюме

В этой главе мы собрали:

  • ✅ Официальную документацию (MDN, ECMAScript)
  • ✅ Обучающие платформы (JavaScript.ru, freeCodeCamp, Codecademy)
  • ✅ Книги (Фланаган, YDKJS, Eloquent JavaScript)
  • ✅ Видеокурсы и YouTube каналы (русские и английские)
  • ✅ Платформы для практики (Codewars, LeetCode, HackerRank)
  • ✅ Инструменты разработчика (онлайн-редакторы, IDE)
  • ✅ Блоги, новости и подкасты
  • ✅ Сообщества (Stack Overflow, Reddit, Discord)

Глава 34. CSS решения и практические приёмы

Введение

В этой главе собраны готовые CSS-решения для типовых задач, которые встречаются в повседневной разработке. Каждый пример сопровождается подробным объяснением.

📜 1. Центрирование элементов

1.1. Flexbox (универсальный метод)

.container { display: flex; justify-content: center; /* по горизонтали */ align-items: center; /* по вертикали */ min-height: 100vh; }

1.2. Grid (современный метод)

.container { display: grid; place-items: center; /* центрирование в обе стороны */ min-height: 100vh; }

1.3. Абсолютное позиционирование

.centered { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }

📜 2. Работа с текстом

2.1. Обрезка текста многоточием (одна строка)

.text-ellipsis { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

2.2. Многострочное многоточие

.text-clamp { display: -webkit-box; -webkit-line-clamp: 3; /* количество строк */ -webkit-box-orient: vertical; overflow: hidden; }

2.3. Запрет выделения текста

.no-select { user-select: none; -webkit-user-select: none; -moz-user-select: none; }

2.4. Перенос длинных слов

.break-word { word-break: break-word; /* или */ overflow-wrap: break-word; }

📜 3. Сохранение пропорций изображений

3.1. Aspect Ratio (современный метод)

.image-wrapper { aspect-ratio: 16 / 9; /* соотношение сторон */ overflow: hidden; } .image-wrapper img { width: 100%; height: 100%; object-fit: cover; }

3.2. Padding Hack (старый метод)

.image-wrapper { position: relative; padding-bottom: 56.25%; /* 9/16 = 0.5625 (для 16:9) */ height: 0; overflow: hidden; } .image-wrapper img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; }

3.3. Object-fit варианты

img { width: 100%; height: 300px; /* Варианты заполнения: */ object-fit: cover; /* заполняет, обрезая лишнее */ object-fit: contain; /* вписывает целиком */ object-fit: fill; /* растягивает без пропорций */ object-fit: none; /* оригинальный размер */ }

📜 4. Flexbox практические примеры

4.1. Равномерное распределение

.flex-container { display: flex; justify-content: space-between; /* по краям */ /* или */ justify-content: space-around; /* с отступами */ /* или */ justify-content: space-evenly; /* равные отступы */ }

4.2. Адаптивная сетка без медиа-запросов

.flex-grid { display: flex; flex-wrap: wrap; gap: 1rem; } .flex-grid > * { flex: 1 1 300px; /* растет, сжимается, базовая ширина 300px */ }

4.3. Sticky Footer (подвал внизу)

body { display: flex; flex-direction: column; min-height: 100vh; } main { flex: 1; /* занимает всё свободное пространство */ } footer { /* остаётся внизу */ }

📜 5. Grid практические примеры

5.1. Автозаполнение сетки

.grid-auto { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; } /* auto-fit - заполняет доступное пространство auto-fill - создаёт пустые ячейки */

5.2. Простой layout (шапка-контент-подвал)

.page-layout { display: grid; grid-template-rows: auto 1fr auto; min-height: 100vh; } /* header - auto main - 1fr (растягивается) footer - auto */

5.3. Sidebar + Content

.layout { display: grid; grid-template-columns: 250px 1fr; gap: 2rem; } @media (max-width: 768px) { .layout { grid-template-columns: 1fr; } }

📜 6. Анимации и переходы

6.1. Плавные переходы

.button { transition: all 0.3s ease; } .button:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); } /* Отдельные свойства для оптимизации */ .optimized { transition: transform 0.3s ease, opacity 0.3s ease; }

6.2. Анимация появления

@keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .fade-in { animation: fadeIn 0.5s ease; }

6.3. Бесконечная вращающаяся анимация

@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .spinner { animation: spin 1s linear infinite; }

Пример разметки спиннера

<!-- Простой круглый спиннер --> <div class="spinner-simple"></div> <style> .spinner-simple { width: 50px; height: 50px; border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite; } </style> <!-- Спиннер с текстом "Загрузка..." --> <div class="spinner-wrapper"> <div class="spinner-circle"></div> <p>Загрузка...</p> </div> <style> .spinner-wrapper { display: flex; flex-direction: column; align-items: center; gap: 1rem; } .spinner-circle { width: 60px; height: 60px; border: 5px solid rgba(0, 0, 0, 0.1); border-left-color: #0066cc; border-radius: 50%; animation: spin 1s linear infinite; } </style> <!-- Спиннер для кнопки --> <button class="btn-loading"> <span class="spinner-btn"></span> Загрузка... </button> <style> .btn-loading { display: flex; align-items: center; gap: 0.5rem; padding: 0.75rem 1.5rem; background: #0066cc; color: white; border: none; border-radius: 6px; } .spinner-btn { width: 16px; height: 16px; border: 2px solid rgba(255, 255, 255, 0.3); border-top-color: white; border-radius: 50%; animation: spin 0.8s linear infinite; } </style>
💡 Готовые спиннеры

Больше готовых CSS-лоадеров можно найти на сайте:

6.4. Пульсация (для уведомлений)

@keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.1); } } .notification { animation: pulse 2s ease infinite; }

6.5. Кастомные чекбоксы (NextJS, React)

import styles from "./styles.module.css"; const SidebarCategories = () => { // Список категорий const categoriesItems = [ { name: "Футболки", href: "#!" }, { name: "Худи", href: "#!" }, { name: "Кофты", href: "#!" }, ]; return ( <div className={styles["sidebar-categories"]}> <h3 className={styles["sidebar-categories__title"]}>Categories</h3> <ul className={styles["sidebar-categories__list"]}> {categoriesItems.map((item) => ( <li key={item.name} className={styles["sidebar-categories__item"]} > <label className={styles["sidebar-categories__label"]}> <input type="checkbox" className={`${styles["sidebar-categories__input"]} ${styles["bounce-animation"]}`} /> <span className={styles["sidebar-categories__span"]} > {item.name} </span> </label> </li> ))} </ul> </div> ); } export default SidebarCategories;

Стили для кастомных чекбоксов

<style> .sidebar-categories__label { display: flex; justify-content: flex-start; align-items: center; gap: 10px; cursor: pointer; user-select: none; /* Предотвращаем выделение текста при клике */ } .sidebar-categories__input { appearance: none; cursor: pointer; width: 18px; height: 18px; border: 2px solid var(--neutral-black-b100); border-radius: 3px; position: relative; transition: all 0.2s ease; /* Плавная анимация */ flex-shrink: 0; /* Предотвращаем сжатие чекбокса */ } .sidebar-categories__input:hover { border-color: var(--neutral-black-b200); } .sidebar-categories__input:focus { outline: 2px solid var(--neutral-black-b100); outline-offset: 2px; } .sidebar-categories__input:checked { background-color: var(--neutral-black-b300); border-color: var(--neutral-black-b300); } /* ========== ВАРИАНТ 1: Анимация рисования галочки ========== */ .sidebar-categories__input:checked::after { content: ""; position: absolute; left: 4px; top: 1px; width: 6px; height: 10px; border: solid white; border-width: 0 2px 2px 0; transform: rotate(45deg); animation: drawCheck 0.3s ease forwards; } @keyframes drawCheck { 0% { width: 0; height: 0; } 50% { width: 6px; height: 0; } 100% { width: 6px; height: 10px; } } /* ========== ВАРИАНТ 2: Масштабирование (простой и эффектный) ========== */ .sidebar-categories__input.scale-animation:checked::after { content: ""; position: absolute; left: 4px; top: 1px; width: 6px; height: 10px; border: solid white; border-width: 0 2px 2px 0; transform: rotate(45deg) scale(0); animation: scaleCheck 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55) forwards; } @keyframes scaleCheck { 0% { transform: rotate(45deg) scale(0); } 100% { transform: rotate(45deg) scale(1); } } /* ========== ВАРИАНТ 3: Поворот + масштабирование ========== */ .sidebar-categories__input.rotate-animation:checked::after { content: ""; position: absolute; left: 4px; top: 1px; width: 6px; height: 10px; border: solid white; border-width: 0 2px 2px 0; transform: rotate(45deg) scale(0) rotate(-180deg); animation: rotateCheck 0.4s ease forwards; } @keyframes rotateCheck { 0% { transform: rotate(45deg) scale(0) rotate(-180deg); opacity: 0; } 50% { opacity: 1; } 100% { transform: rotate(45deg) scale(1) rotate(0deg); opacity: 1; } } /* ========== ВАРИАНТ 4: Плавное появление сверху ========== */ .sidebar-categories__input.slide-animation:checked::after { content: ""; position: absolute; left: 4px; top: 1px; width: 6px; height: 10px; border: solid white; border-width: 0 2px 2px 0; transform: rotate(45deg) translateY(-10px); opacity: 0; animation: slideCheck 0.3s ease forwards; } @keyframes slideCheck { 0% { transform: rotate(45deg) translateY(-10px); opacity: 0; } 100% { transform: rotate(45deg) translateY(0); opacity: 1; } } /* ========== ВАРИАНТ 5: Эластичный отскок ========== */ .sidebar-categories__input.bounce-animation:checked::after { content: ""; position: absolute; left: 4px; top: 1px; width: 6px; height: 10px; border: solid white; border-width: 0 2px 2px 0; transform: rotate(45deg) scale(0); animation: bounceCheck 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55) forwards; } @keyframes bounceCheck { 0% { transform: rotate(45deg) scale(0); } 60% { transform: rotate(45deg) scale(1.2); } 80% { transform: rotate(45deg) scale(0.9); } 100% { transform: rotate(45deg) scale(1); } } .sidebar-categories__input:checked::before { content: "✓"; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); color: white; font-size: 12px; font-weight: bold; line-height: 1; display: none; /* Используйте либо ::after, либо ::before */ } .sidebar-categories__span { font-family: var(--font-family); font-weight: 400; font-size: 14px; line-height: 175%; color: var(--neutral-black-b600); transition: color 0.2s ease; } /* Изменение цвета текста при наведении на label */ .sidebar-categories__label:hover .sidebar-categories__span { color: var(--neutral-black-b700); } </style>

📜 7. Эффекты наведения

7.1. Плавное увеличение картинки

.image-zoom { overflow: hidden; } .image-zoom img { transition: transform 0.3s ease; /* Для сохранения качества при медленной анимации */ image-rendering: optimizeQuality; } .image-zoom:hover img { transform: scale(1.1); }

7.2. Подчеркивание при наведении

.link { position: relative; text-decoration: none; } .link::after { content: ''; position: absolute; bottom: 0; left: 0; width: 0; height: 2px; background: currentColor; transition: width 0.3s ease; } .link:hover::after { width: 100%; }

7.3. Эффект вогнутой кнопки

.button-inset { appearance: none; background: #f0f0f0; border: none; padding: 1rem 2rem; box-shadow: inset 2px 2px 5px rgba(0, 0, 0, 0.2); transition: box-shadow 0.2s; } .button-inset:active { box-shadow: inset 4px 4px 8px rgba(0, 0, 0, 0.3); }

📜 8. Тени и размытие

8.1. Красивые тени для карточек

.card { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); transition: box-shadow 0.3s ease; } .card:hover { box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); } /* Многослойная тень для глубины */ .card-deep { box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), 0 8px 16px rgba(0, 0, 0, 0.1); }

8.2. Тень для изображений (drop-shadow)

img { /* filter: drop-shadow работает с формой изображения */ filter: drop-shadow(10px 10px 20px rgba(0, 0, 0, 0.3)); } /* box-shadow работает только с прямоугольником */ img.box-shadow { box-shadow: 10px 10px 20px rgba(0, 0, 0, 0.3); }

8.3. Backdrop blur (эффект матового стекла)

.glass-effect { background: rgba(255, 255, 255, 0.7); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.3); }

📜 9. Градиенты

9.1. Линейный градиент

.gradient-linear { background: linear-gradient( 135deg, #667eea 0%, #764ba2 100% ); } /* С несколькими цветами */ .gradient-multi { background: linear-gradient( to right, #ff0000, #00ff00, #0000ff ); }

9.2. Радиальный градиент

.gradient-radial { background: radial-gradient( circle, #667eea 0%, #764ba2 100% ); }

9.3. Градиент как оверлей на изображение

.image-overlay { position: relative; } .image-overlay::after { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: linear-gradient( to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.7) 100% ); }

📜 10. Кастомизация скроллбара

10.1. Webkit (Chrome, Safari, Edge)

/* Ширина скроллбара */ ::-webkit-scrollbar { width: 10px; } /* Трек (фон) */ ::-webkit-scrollbar-track { background: #f1f1f1; } /* Ползунок */ ::-webkit-scrollbar-thumb { background: #888; border-radius: 5px; } /* Ползунок при наведении */ ::-webkit-scrollbar-thumb:hover { background: #555; }

10.2. Firefox

.custom-scroll { scrollbar-width: thin; scrollbar-color: #888 #f1f1f1; }

10.3. Скрытие скроллбара

.hide-scrollbar { /* Firefox */ scrollbar-width: none; /* IE and Edge */ -ms-overflow-style: none; } /* Webkit */ .hide-scrollbar::-webkit-scrollbar { display: none; }

📜 11. Адаптивность

11.1. Медиа-запросы (breakpoints)

Mobile First подход (от меньшего к большему)

/* Mobile First - начинаем с мобильных устройств */ .container { padding: 1rem; font-size: 14px; } /* Tablet (768px и выше) */ @media (min-width: 768px) { .container { padding: 2rem; font-size: 16px; } } /* Desktop (1024px и выше) */ @media (min-width: 1024px) { .container { padding: 3rem; max-width: 1200px; margin: 0 auto; font-size: 18px; } } /* Large Desktop (1440px и выше) */ @media (min-width: 1440px) { .container { max-width: 1400px; } }

Desktop First подход (от большего к меньшему)

/* Desktop First - начинаем с десктопа */ .container { padding: 3rem; max-width: 1200px; margin: 0 auto; font-size: 18px; } /* Tablet (1023px и меньше) */ @media (max-width: 1023px) { .container { padding: 2rem; max-width: 100%; font-size: 16px; } } /* Mobile (767px и меньше) */ @media (max-width: 767px) { .container { padding: 1rem; font-size: 14px; } } /* Small Mobile (479px и меньше) */ @media (max-width: 479px) { .container { padding: 0.5rem; font-size: 13px; } }
💡 Какой подход выбрать?
  • Mobile First — рекомендуется для новых проектов. Начинаем с базовых стилей для мобильных, затем добавляем улучшения для больших экранов.
  • Desktop First — подходит для адаптации существующих десктопных сайтов под мобильные устройства.

11.2. Адаптивная типографика (clamp)

h1 { /* min, preferred, max */ font-size: clamp(1.5rem, 5vw, 3rem); } p { font-size: clamp(1rem, 2vw, 1.25rem); line-height: 1.6; }

11.3. Минимальная высота (viewport)

.hero { /* min() выбирает меньшее значение */ min-height: min(600px, 100vh); } .section { /* max() выбирает большее значение */ padding: max(2rem, 5vh); }

11.4. Media Queries Preferences (настройки пользователя)

Современные браузеры поддерживают медиа-запросы, которые учитывают системные настройки пользователя. Это позволяет создавать более доступные и удобные интерфейсы.

Основные Media Features для preferences

Media Feature Описание Возможные значения
prefers-color-scheme Предпочитаемая цветовая схема пользователя light, dark, no-preference
prefers-reduced-motion Запрос на уменьшение анимаций (accessibility) reduce, no-preference
prefers-contrast Предпочтение по контрастности more, less, no-preference, custom
prefers-reduced-transparency Запрос на уменьшение прозрачности reduce, no-preference
prefers-reduced-data Запрос на экономию трафика reduce, no-preference
forced-colors Принудительная цветовая схема (Windows High Contrast) active, none
inverted-colors Инверсия цветов inverted, none

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

1. prefers-color-scheme (тёмная тема)

/* Светлая тема по умолчанию */ :root { --bg: #ffffff; --text: #333333; } /* Автоматическая тёмная тема */ @media (prefers-color-scheme: dark) { :root { --bg: #1a1a1a; --text: #e0e0e0; } } body { background: var(--bg); color: var(--text); } /* Комбинирование с классом для ручного переключения */ body.dark-mode { --bg: #1a1a1a; --text: #e0e0e0; } @media (prefers-color-scheme: dark) { body:not(.light-mode) { --bg: #1a1a1a; --text: #e0e0e0; } }

2. prefers-reduced-motion (уменьшение анимаций)

/* Анимации по умолчанию */ .element { transition: transform 0.3s ease; } .element:hover { transform: scale(1.1); } /* Отключение анимаций для пользователей с настройкой */ @media (prefers-reduced-motion: reduce) { * { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } /* Или более избирательно */ .element { transition: none; } .element:hover { transform: none; } } /* Плавная прокрутка с учётом настроек */ html { scroll-behavior: smooth; } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

3. prefers-contrast (повышенная контрастность)

/* Обычная контрастность */ .button { background: #0066cc; color: white; } /* Повышенная контрастность */ @media (prefers-contrast: more) { .button { background: #0052a3; color: white; border: 2px solid black; font-weight: bold; } } /* Пониженная контрастность */ @media (prefers-contrast: less) { .button { background: #6699cc; color: white; } }

4. prefers-reduced-transparency (без прозрачности)

/* С прозрачностью */ .modal-overlay { background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(10px); } /* Без прозрачности для accessibility */ @media (prefers-reduced-transparency: reduce) { .modal-overlay { background: rgb(0, 0, 0); backdrop-filter: none; } }

5. prefers-reduced-data (экономия трафика)

/* Обычная загрузка изображений */ .hero { background-image: url('large-image.jpg'); } /* Лёгкая версия для экономии трафика */ @media (prefers-reduced-data: reduce) { .hero { background-image: url('small-image.jpg'); /* или вообще убрать фон */ background-image: none; background-color: #f0f0f0; } /* Отключить автозагрузку видео */ video { preload: none; } }

6. Комбинирование нескольких preferences

/* Тёмная тема + уменьшенная анимация */ @media (prefers-color-scheme: dark) and (prefers-reduced-motion: reduce) { body { background: #1a1a1a; color: #e0e0e0; } * { transition: none !important; animation: none !important; } } /* Высокая контрастность + тёмная тема */ @media (prefers-color-scheme: dark) and (prefers-contrast: more) { body { background: #000000; color: #ffffff; } a { color: #66b3ff; text-decoration: underline; font-weight: bold; } }

Проверка в JavaScript

// Проверка тёмной темы if (window.matchMedia('(prefers-color-scheme: dark)').matches) { console.log('Пользователь предпочитает тёмную тему'); } // Проверка уменьшенной анимации if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { console.log('Пользователь предпочитает меньше анимаций'); } // Отслеживание изменений const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)'); darkModeQuery.addEventListener('change', (e) => { if (e.matches) { console.log('Переключено на тёмную тему'); document.body.classList.add('dark-theme'); } else { console.log('Переключено на светлую тему'); document.body.classList.remove('dark-theme'); } });
✅ Лучшие практики
  • Всегда учитывайте prefers-reduced-motion — это важно для accessibility
  • Используйте prefers-color-scheme для автоматической тёмной темы
  • Комбинируйте с ручным управлением — дайте пользователю возможность переключить тему вручную
  • Тестируйте — проверяйте, как работает сайт с разными настройками
💡 Как протестировать

В Chrome DevTools:

  1. Откройте DevTools (F12)
  2. Нажмите Cmd+Shift+P (Mac) или Ctrl+Shift+P (Windows)
  3. Введите "Rendering"
  4. В панели Rendering найдите "Emulate CSS media feature"
  5. Выберите нужную настройку (prefers-color-scheme, prefers-reduced-motion и др.)

В Firefox:

  1. about:config
  2. Найдите ui.prefersReducedMotion
  3. Установите значение 1 (reduce) или 0 (no-preference)

Поддержка браузерами

Feature Chrome Firefox Safari Edge
prefers-color-scheme 76+ 67+ 12.1+ 79+
prefers-reduced-motion 74+ 63+ 10.1+ 79+
prefers-contrast 96+ 101+ 14.1+ 96+
prefers-reduced-transparency 118+ 113+ - 118+

* Данные актуальны на 2024 год. Проверяйте актуальную поддержку на caniuse.com

📜 12. Форматирование форм

12.1. Красивые инпуты

input[type="text"], input[type="email"] { appearance: none; width: 100%; padding: 0.75rem 1rem; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 1rem; transition: border-color 0.3s ease; } input:focus { outline: none; border-color: #667eea; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); }

12.2. Placeholder стили

input::placeholder { color: #999; opacity: 1; } input:focus::placeholder { opacity: 0.5; }

12.3. Удаление иконки очистки в search

/* Удалить 'x' в input type='search' */ input[type="search"]::-webkit-search-cancel-button { display: none; }

12.4. Расширение input при фокусе

input { width: 200px; transition: width 0.3s ease; } input:focus, input:not(:placeholder-shown) { width: 300px; }

📜 13. Полезные CSS-переменные

13.1. Цветовая схема

:root { /* Основные цвета */ --primary: #667eea; --secondary: #764ba2; --success: #28a745; --danger: #dc3545; --warning: #ffc107; /* Текст */ --text-primary: #333; --text-secondary: #666; /* Фон */ --bg-primary: #ffffff; --bg-secondary: #f8f9fa; /* Отступы */ --spacing-xs: 0.25rem; --spacing-sm: 0.5rem; --spacing-md: 1rem; --spacing-lg: 2rem; --spacing-xl: 4rem; /* Тени */ --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1); --shadow-md: 0 4px 8px rgba(0, 0, 0, 0.1); --shadow-lg: 0 8px 16px rgba(0, 0, 0, 0.1); } /* Использование */ .card { background: var(--bg-primary); color: var(--text-primary); padding: var(--spacing-lg); box-shadow: var(--shadow-md); }

13.2. Тёмная тема

[data-theme="dark"] { --bg-primary: #1a1a1a; --bg-secondary: #2d2d2d; --text-primary: #e0e0e0; --text-secondary: #b0b0b0; } /* Автоматическое переключение */ @media (prefers-color-scheme: dark) { :root { --bg-primary: #1a1a1a; --text-primary: #e0e0e0; } }

📜 14. Плавная прокрутка

html { scroll-behavior: smooth; } /* Отключить для пользователей с настройками accessibility */ @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

📜 15. Генераторы и инструменты

✅ Полезные онлайн-инструменты

📜 Резюме

В этой главе мы рассмотрели:

  • ✅ Способы центрирования элементов
  • ✅ Работу с текстом (обрезка, перенос)
  • ✅ Сохранение пропорций изображений
  • ✅ Flexbox и Grid практические примеры
  • ✅ Анимации и переходы
  • ✅ Эффекты наведения
  • ✅ Тени и размытие
  • ✅ Градиенты
  • ✅ Кастомизацию скроллбара
  • ✅ Адаптивность и медиа-запросы
  • ✅ Стилизацию форм
  • ✅ CSS-переменные и тёмную тему
  • ✅ Полезные генераторы

Глава 35. JavaScript решения и практические приёмы

Введение

В этой главе собраны готовые JavaScript-решения для типовых задач, которые встречаются в повседневной разработке. Все примеры проверены и готовы к использованию.

📜 1. Ожидание загрузки DOM

1.1. DOMContentLoaded

// Современный способ document.addEventListener('DOMContentLoaded', function() { // Ваш код здесь console.log('DOM загружен'); }); // Сокращённая стрелочная функция document.addEventListener('DOMContentLoaded', () => { // Код выполнится после загрузки DOM });

1.2. Полная загрузка страницы (включая изображения)

window.addEventListener('load', function() { // Выполнится после загрузки всех ресурсов console.log('Страница полностью загружена'); });

📜 2. Работа с датами

2.1. Форматирование даты

function formatDate(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0'); return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; } const now = new Date(); console.log(formatDate(now)); // "2025-01-15 14:30:45"

2.2. Разница между датами

function getDaysDifference(date1, date2) { const diffTime = Math.abs(date2 - date1); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); return diffDays; } const start = new Date('2025-01-01'); const end = new Date('2025-01-15'); console.log(getDaysDifference(start, end)); // 14

2.3. Получение названия месяца и дня недели

const months = [ 'Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь' ]; const days = [ 'Воскресенье', 'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота' ]; const date = new Date(); const monthName = months[date.getMonth()]; const dayName = days[date.getDay()]; console.log(`${dayName}, ${date.getDate()} ${monthName}`); // "Понедельник, 15 Января"

📜 3. Валидация данных

3.1. Проверка email

function isValidEmail(email) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } console.log(isValidEmail('user@example.com')); // true console.log(isValidEmail('invalid-email')); // false

3.2. Проверка телефона (RU)

function isValidPhone(phone) { const phoneRegex = /^(\+7|8)?[\s-]?\(?\d{3}\)?[\s-]?\d{3}[\s-]?\d{2}[\s-]?\d{2}$/; return phoneRegex.test(phone); } console.log(isValidPhone('+7 (999) 123-45-67')); // true console.log(isValidPhone('8-999-123-45-67')); // true

3.3. Проверка пустого значения

function isEmpty(value) { return value === null || value === undefined || value === '' || (Array.isArray(value) && value.length === 0) || (typeof value === 'object' && Object.keys(value).length === 0); } console.log(isEmpty('')); // true console.log(isEmpty([])); // true console.log(isEmpty({})); // true console.log(isEmpty('text')); // false

📜 4. Работа с массивами

4.1. Удаление дубликатов

// Способ 1: Set const arr = [1, 2, 2, 3, 3, 4]; const unique = [...new Set(arr)]; console.log(unique); // [1, 2, 3, 4] // Способ 2: filter const unique2 = arr.filter((item, index) => arr.indexOf(item) === index);

4.2. Группировка объектов

function groupBy(array, key) { return array.reduce((result, item) => { const group = item[key]; result[group] = result[group] || []; result[group].push(item); return result; }, {}); } const users = [ {name: 'Alice', age: 25}, {name: 'Bob', age: 30}, {name: 'Charlie', age: 25} ]; const grouped = groupBy(users, 'age'); // {25: [{name: 'Alice'...}, {name: 'Charlie'...}], 30: [{name: 'Bob'...}]}

4.3. Сортировка объектов

const users = [ {name: 'Charlie', age: 25}, {name: 'Alice', age: 30}, {name: 'Bob', age: 20} ]; // По возрасту (по возрастанию) users.sort((a, b) => a.age - b.age); // По имени (по алфавиту) users.sort((a, b) => a.name.localeCompare(b.name)); // По убыванию users.sort((a, b) => b.age - a.age);

4.4. Поиск максимального/минимального значения

const numbers = [5, 2, 8, 1, 9]; // Максимум const max = Math.max(...numbers); // 9 // Минимум const min = Math.min(...numbers); // 1 // Максимальный объект по свойству const users = [{age: 25}, {age: 30}, {age: 20}]; const oldest = users.reduce((prev, current) => (prev.age > current.age) ? prev : current );

📜 5. Работа со строками

5.1. Капитализация (первая буква заглавная)

function capitalize(str) { return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); } console.log(capitalize('hello')); // "Hello" console.log(capitalize('WORLD')); // "World" // Капитализация каждого слова function capitalizeWords(str) { return str.split(' ') .map(word => capitalize(word)) .join(' '); } console.log(capitalizeWords('hello world')); // "Hello World"

5.2. Удаление пробелов

const str = ' Hello World '; // Удаление с краёв console.log(str.trim()); // "Hello World" // Удаление всех пробелов console.log(str.replace(/\s+/g, '')); // "HelloWorld" // Замена множественных пробелов на один console.log(str.replace(/\s+/g, ' ').trim()); // "Hello World"

5.3. Генерация случайной строки

function generateRandomString(length) { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let result = ''; for (let i = 0; i < length; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } return result; } console.log(generateRandomString(10)); // "aB3xK9mP2q"

📜 6. Debounce и Throttle

6.1. Debounce (задержка выполнения)

function debounce(func, delay) { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), delay); }; } // Использование для поиска const searchInput = document.querySelector('#search'); searchInput.addEventListener('input', debounce(function(e) { console.log('Поиск:', e.target.value); }, 500));

6.2. Throttle (ограничение частоты вызовов)

function throttle(func, delay) { let lastCall = 0; return function(...args) { const now = Date.now(); if (now - lastCall >= delay) { lastCall = now; func.apply(this, args); } }; } // Использование при скролле window.addEventListener('scroll', throttle(function() { console.log('Скролл:', window.scrollY); }, 200));

📜 7. Работа с DOM

7.1. Проверка видимости элемента

function isInViewport(element) { const rect = element.getBoundingClientRect(); return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= window.innerHeight && rect.right <= window.innerWidth ); } const box = document.querySelector('.box'); if (isInViewport(box)) { console.log('Элемент виден'); }

7.2. Плавная прокрутка к элементу

function scrollToElement(selector) { const element = document.querySelector(selector); if (element) { element.scrollIntoView({ behavior: 'smooth', block: 'start' }); } } scrollToElement('#section-2');

7.3. Копирование в буфер обмена

async function copyToClipboard(text) { try { await navigator.clipboard.writeText(text); console.log('Скопировано!'); } catch (err) { console.error('Ошибка копирования:', err); } } copyToClipboard('Текст для копирования');

7.4. Получение параметров URL

// Способ 1: URLSearchParams const params = new URLSearchParams(window.location.search); const id = params.get('id'); const name = params.get('name'); // Способ 2: Все параметры в объект function getUrlParams() { const params = {}; const searchParams = new URLSearchParams(window.location.search); for (const [key, value] of searchParams) { params[key] = value; } return params; } console.log(getUrlParams()); // {id: "123", name: "test"}

📜 8. Работа с событиями

8.1. Делегирование событий

// Вместо множества обработчиков на кнопки document.querySelector('.buttons-container').addEventListener('click', (e) => { if (e.target.matches('.btn')) { console.log('Кликнули на кнопку:', e.target.textContent); } });

8.2. Отмена стандартного поведения

// Отмена отправки формы const form = document.querySelector('form'); form.addEventListener('submit', (e) => { e.preventDefault(); console.log('Форма не отправлена'); }); // Остановка всплытия события const modal = document.querySelector('.modal'); modal.querySelector('.modal-window').addEventListener('click', (e) => { e.stopPropagation(); });

8.3. Проверка нажатия клавиши Enter

document.addEventListener('keydown', (e) => { if (e.key === 'Enter') { console.log('Нажат Enter'); } // Или по коду клавиши if (e.keyCode === 13) { console.log('Enter (по коду)'); } });

📜 9. Определение устройства и браузера

9.1. Проверка мобильного устройства

function isMobile() { return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i .test(navigator.userAgent); } if (isMobile()) { console.log('Мобильное устройство'); } else { console.log('Десктоп'); }

9.2. Определение браузера

function getBrowser() { const ua = navigator.userAgent; if (ua.indexOf('Firefox') > -1) return 'Firefox'; if (ua.indexOf('Chrome') > -1) return 'Chrome'; if (ua.indexOf('Safari') > -1) return 'Safari'; if (ua.indexOf('MSIE') > -1 || ua.indexOf('Trident') > -1) return 'IE'; return 'Unknown'; } console.log('Браузер:', getBrowser());

📜 10. LocalStorage помощники

10.1. Сохранение и получение объектов

// Сохранение объекта function saveToStorage(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); } catch (e) { console.error('Ошибка сохранения:', e); } } // Получение объекта function getFromStorage(key, defaultValue = null) { try { const item = localStorage.getItem(key); return item ? JSON.parse(item) : defaultValue; } catch (e) { console.error('Ошибка чтения:', e); return defaultValue; } } // Использование const user = {name: 'Alice', age: 25}; saveToStorage('user', user); const savedUser = getFromStorage('user'); console.log(savedUser); // {name: 'Alice', age: 25}

10.2. Очистка localStorage

// Удалить конкретный ключ localStorage.removeItem('user'); // Очистить всё localStorage.clear();

📜 11. Асинхронные операции

11.1. Задержка (sleep)

function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // Использование async function example() { console.log('Начало'); await sleep(2000); console.log('Прошло 2 секунды'); } example();

11.2. Fetch с таймаутом

async function fetchWithTimeout(url, timeout = 5000) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { signal: controller.signal }); clearTimeout(timeoutId); return await response.json(); } catch (error) { if (error.name === 'AbortError') { throw new Error('Превышено время ожидания'); } throw error; } } // Использование try { const data = await fetchWithTimeout('https://api.example.com/data'); console.log(data); } catch (error) { console.error('Ошибка:', error.message); }

11.3. Retry (повторные попытки)

async function retry(fn, maxAttempts = 3, delay = 1000) { for (let i = 0; i < maxAttempts; i++) { try { return await fn(); } catch (error) { if (i === maxAttempts - 1) throw error; await sleep(delay); } } } // Использование const data = await retry( () => fetch('https://api.example.com/data').then(r => r.json()), 3, 1000 );

📜 12. Полезные утилиты

12.1. Глубокое клонирование объекта

function deepClone(obj) { return JSON.parse(JSON.stringify(obj)); } const original = {name: 'Alice', info: {age: 25}}; const copy = deepClone(original); copy.info.age = 30; console.log(original.info.age); // 25 (не изменился)

12.2. Сравнение объектов

function isEqual(obj1, obj2) { return JSON.stringify(obj1) === JSON.stringify(obj2); } const a = {name: 'Alice', age: 25}; const b = {name: 'Alice', age: 25}; console.log(isEqual(a, b)); // true

12.3. Генерация уникального ID

function generateId() { return Date.now().toString(36) + Math.random().toString(36).substr(2); } console.log(generateId()); // "l8r9x2k5p"

12.4. Форматирование числа с разделителями

function formatNumber(num) { return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' '); } console.log(formatNumber(1234567)); // "1 234 567" // Или встроенный метод const price = 1234567.89; console.log(price.toLocaleString('ru-RU')); // "1 234 567,89"

📜 13. Паттерны программирования

13.1. Singleton (одиночка)

class Config { constructor() { if (Config.instance) { return Config.instance; } this.settings = {}; Config.instance = this; } set(key, value) { this.settings[key] = value; } get(key) { return this.settings[key]; } } const config1 = new Config(); const config2 = new Config(); console.log(config1 === config2); // true

13.2. Observer (наблюдатель)

class EventEmitter { constructor() { this.events = {}; } on(event, callback) { if (!this.events[event]) { this.events[event] = []; } this.events[event].push(callback); } emit(event, data) { if (this.events[event]) { this.events[event].forEach(callback => callback(data)); } } off(event, callback) { if (this.events[event]) { this.events[event] = this.events[event].filter(cb => cb !== callback); } } } // Использование const emitter = new EventEmitter(); emitter.on('userLoggedIn', (user) => console.log('Вход:', user)); emitter.emit('userLoggedIn', {name: 'Alice'});

📜 14. Производительность

14.1. Замер времени выполнения

// Способ 1: console.time console.time('myTimer'); // ... код ... console.timeEnd('myTimer'); // "myTimer: 123ms" // Способ 2: performance.now() const start = performance.now(); // ... код ... const end = performance.now(); console.log(`Время: ${end - start}ms`);

14.2. Мемоизация (кеширование результатов)

function memoize(fn) { const cache = {}; return function(...args) { const key = JSON.stringify(args); if (cache[key]) { return cache[key]; } const result = fn.apply(this, args); cache[key] = result; return result; }; } // Использование const fibonacci = memoize(function(n) { if (n < 2) return n; return fibonacci(n - 1) + fibonacci(n - 2); }); console.log(fibonacci(40)); // Быстро благодаря кешу

📜 Резюме

В этой главе мы рассмотрели:

  • ✅ Ожидание загрузки DOM
  • ✅ Работу с датами (форматирование, разница)
  • ✅ Валидацию (email, телефон, пустые значения)
  • ✅ Работу с массивами (дубликаты, группировка, сортировка)
  • ✅ Работу со строками (капитализация, генерация)
  • ✅ Debounce и Throttle
  • ✅ Работу с DOM (видимость, прокрутка, копирование)
  • ✅ Работу с событиями (делегирование, клавиши)
  • ✅ Определение устройства и браузера
  • ✅ LocalStorage помощники
  • ✅ Асинхронные операции (sleep, timeout, retry)
  • ✅ Полезные утилиты (клонирование, ID, форматирование)
  • ✅ Паттерны (Singleton, Observer)
  • ✅ Производительность (замер времени, мемоизация)

Глава 36. Сниппеты и готовые решения

Введение

В этой главе собраны готовые сниппеты кода для быстрого использования в проектах. Все примеры протестированы и готовы к копированию.

📜 1. JavaScript сниппеты

1.1. Ожидание загрузки DOM

// Vanilla JS document.addEventListener('DOMContentLoaded', function() { // Ваш код }); // jQuery $(document).ready(function() { // Ваш код }); // Короткая форма jQuery $(function() { // Ваш код });

1.2. Форматирование даты

function formatDate(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0'); return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; } const now = new Date(); console.log(formatDate(now)); // "2024-01-25 14:30:45"

1.3. Debounce функция

function debounce(func, delay) { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), delay); }; } // Использование const searchInput = document.querySelector('#search'); searchInput.addEventListener('input', debounce(function(e) { console.log('Поиск:', e.target.value); }, 500));

1.4. Throttle функция

function throttle(func, delay) { let lastCall = 0; return function(...args) { const now = Date.now(); if (now - lastCall >= delay) { lastCall = now; func.apply(this, args); } }; } // Использование при скролле window.addEventListener('scroll', throttle(function() { console.log('Скролл:', window.scrollY); }, 200));

1.5. Копирование в буфер обмена

async function copyToClipboard(text) { try { await navigator.clipboard.writeText(text); console.log('Скопировано!'); } catch (err) { console.error('Ошибка копирования:', err); } } // Использование copyToClipboard('Текст для копирования');

1.6. Проверка видимости элемента

function isInViewport(element) { const rect = element.getBoundingClientRect(); return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= window.innerHeight && rect.right <= window.innerWidth ); } // Использование const box = document.querySelector('.box'); if (isInViewport(box)) { console.log('Элемент виден'); }

1.7. Получение параметров URL

// Способ 1: URLSearchParams const params = new URLSearchParams(window.location.search); const id = params.get('id'); const name = params.get('name'); // Способ 2: все параметры в объект function getUrlParams() { const params = {}; const searchParams = new URLSearchParams(window.location.search); for (const [key, value] of searchParams) { params[key] = value; } return params; } console.log(getUrlParams()); // {id: "123", name: "test"}

1.8. Плавная прокрутка к элементу

function scrollToElement(selector) { const element = document.querySelector(selector); if (element) { element.scrollIntoView({ behavior: 'smooth', block: 'start' }); } } // Использование scrollToElement('#section-2');

1.9. Определение мобильного устройства

function isMobile() { return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i .test(navigator.userAgent); } if (isMobile()) { console.log('Мобильное устройство'); }

1.10. LocalStorage помощники

// Сохранение объекта function saveToStorage(key, value) { localStorage.setItem(key, JSON.stringify(value)); } // Получение объекта function getFromStorage(key, defaultValue = null) { const item = localStorage.getItem(key); return item ? JSON.parse(item) : defaultValue; } // Использование const user = {name: 'Alice', age: 25}; saveToStorage('user', user); const savedUser = getFromStorage('user');

📜 2. CSS сниппеты

2.1. Плавная прокрутка

html { scroll-behavior: smooth; }

2.2. Центрирование элемента (Flexbox)

.container { display: flex; justify-content: center; align-items: center; min-height: 100vh; }

2.3. Обрезка текста многоточием

.text-truncate { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* Для нескольких строк */ .text-truncate-multiline { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }

2.4. Запрет выделения текста

.no-select { user-select: none; -webkit-user-select: none; -moz-user-select: none; }

2.5. Сохранение пропорций (aspect-ratio)

/* Современный способ */ .box { aspect-ratio: 16 / 9; } /* Старый способ (padding-hack) */ .box-old { position: relative; padding-bottom: 56.25%; /* 9/16 = 0.5625 */ } .box-old > * { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }

2.6. Скрытие полосы прокрутки

/* Для Webkit (Chrome, Safari) */ .no-scrollbar::-webkit-scrollbar { display: none; } /* Для Firefox */ .no-scrollbar { scrollbar-width: none; } /* Общее */ .no-scrollbar { -ms-overflow-style: none; }

2.7. Эффект матового стекла (backdrop blur)

.glass-effect { background: rgba(255, 255, 255, 0.7); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.3); }

2.8. Тень для изображений

/* Тень повторяет форму изображения */ img { filter: drop-shadow(10px 15px 30px rgba(41, 41, 42, 0.5)); } /* Прямоугольная тень */ img.box-shadow { box-shadow: 10px 15px 30px rgba(41, 41, 42, 0.5); }

2.9. Адаптивная типографика (clamp)

h1 { /* min, preferred, max */ font-size: clamp(1.5rem, 5vw, 3rem); } p { font-size: clamp(1rem, 2vw, 1.25rem); }

2.10. Градиенты

/* Линейный градиент */ .gradient { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } /* Радиальный градиент */ .gradient-radial { background: radial-gradient(circle, #667eea 0%, #764ba2 100%); } /* Градиент как оверлей */ .image-overlay::after { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: linear-gradient( to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.7) 100% ); }

📜 3. HTML сниппеты

3.1. Базовая структура HTML5

<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> </body> </html>

3.2. Подключение JavaScript

<!-- В конце body --> <script src="script.js"></script> <!-- С defer (рекомендуется) --> <script src="script.js" defer></script> <!-- С async --> <script src="script.js" async></script>

3.3. Адаптивные изображения

<picture> <source media="(min-width: 1024px)" srcset="large.jpg"> <source media="(min-width: 768px)" srcset="medium.jpg"> <img src="small.jpg" alt="Описание"> </picture> <!-- Lazy loading --> <img src="image.jpg" alt="Описание" loading="lazy">

3.4. Figure и figcaption

<figure> <img src="photo.jpg" alt="Описание фото"> <figcaption>Подпись к изображению</figcaption> </figure>

📜 4. Готовые функции

4.1. Удаление дубликатов из массива

const arr = [1, 2, 2, 3, 3, 4]; const unique = [...new Set(arr)]; console.log(unique); // [1, 2, 3, 4]

4.2. Группировка объектов по ключу

function groupBy(array, key) { return array.reduce((result, item) => { const group = item[key]; result[group] = result[group] || []; result[group].push(item); return result; }, {}); } const users = [ {name: 'Alice', age: 25}, {name: 'Bob', age: 30}, {name: 'Charlie', age: 25} ]; const grouped = groupBy(users, 'age');

4.3. Глубокое клонирование объекта

function deepClone(obj) { return JSON.parse(JSON.stringify(obj)); } const original = {name: 'Alice', info: {age: 25}}; const copy = deepClone(original);

4.4. Генерация случайного числа в диапазоне

function randomRange(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } console.log(randomRange(10, 20)); // например, 15

4.5. Капитализация строки

function capitalize(str) { return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); } console.log(capitalize('hello')); // "Hello" // Капитализация каждого слова function capitalizeWords(str) { return str.split(' ') .map(word => capitalize(word)) .join(' '); }

4.6. Форматирование числа с разделителями

function formatNumber(num) { return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' '); } console.log(formatNumber(1234567)); // "1 234 567" // Или встроенный метод const price = 1234567.89; console.log(price.toLocaleString('ru-RU')); // "1 234 567,89"

4.7. Задержка (sleep)

function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // Использование async function example() { console.log('Начало'); await sleep(2000); console.log('Прошло 2 секунды'); }

4.8. Генерация уникального ID

function generateId() { return Date.now().toString(36) + Math.random().toString(36).substr(2); } console.log(generateId()); // "l8r9x2k5p"

📜 5. Модальное окно (готовое решение)

5.1. HTML

<button id="openModal">Открыть модалку</button> <div class="modal" id="modal"> <div class="modal-content"> <span class="close">&times;</span> <h2>Заголовок</h2> <p>Содержимое модального окна</p> </div> </div>

5.2. CSS

.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); } .modal.active { display: flex; justify-content: center; align-items: center; } .modal-content { background-color: #fff; padding: 2rem; border-radius: 8px; max-width: 500px; width: 90%; position: relative; } .close { position: absolute; right: 1rem; top: 1rem; font-size: 2rem; cursor: pointer; }

5.3. JavaScript

const modal = document.getElementById('modal'); const openBtn = document.getElementById('openModal'); const closeBtn = document.querySelector('.close'); // Открытие openBtn.addEventListener('click', () => { modal.classList.add('active'); }); // Закрытие по крестику closeBtn.addEventListener('click', () => { modal.classList.remove('active'); }); // Закрытие по клику вне модалки modal.addEventListener('click', (e) => { if (e.target === modal) { modal.classList.remove('active'); } }); // Закрытие по Escape document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && modal.classList.contains('active')) { modal.classList.remove('active'); } });

📜 6. Валидация формы (готовое решение)

const form = document.querySelector('form'); form.addEventListener('submit', function(e) { e.preventDefault(); const email = document.querySelector('#email').value; const password = document.querySelector('#password').value; // Валидация email const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { alert('Некорректный email'); return; } // Валидация пароля (минимум 6 символов) if (password.length < 6) { alert('Пароль должен содержать минимум 6 символов'); return; } // Если всё ок console.log('Форма валидна:', {email, password}); // Отправка данных на сервер });

📜 7. Слайдер (простая реализация)

7.1. HTML

<div class="slider"> <div class="slides"> <div class="slide active">Слайд 1</div> <div class="slide">Слайд 2</div> <div class="slide">Слайд 3</div> </div> <button class="prev">&laquo;</button> <button class="next">&raquo;</button> </div>

7.2. CSS

.slider { position: relative; max-width: 600px; margin: 0 auto; } .slides { position: relative; height: 300px; } .slide { position: absolute; top: 0; left: 0; width: 100%; height: 100%; opacity: 0; transition: opacity 0.5s; display: flex; align-items: center; justify-content: center; background: #f0f0f0; } .slide.active { opacity: 1; } .prev, .next { position: absolute; top: 50%; transform: translateY(-50%); background: rgba(0, 0, 0, 0.5); color: white; border: none; padding: 1rem; cursor: pointer; font-size: 1.5rem; } .prev { left: 0; } .next { right: 0; }

7.3. JavaScript

const slides = document.querySelectorAll('.slide'); const prevBtn = document.querySelector('.prev'); const nextBtn = document.querySelector('.next'); let currentSlide = 0; function showSlide(n) { slides[currentSlide].classList.remove('active'); currentSlide = (n + slides.length) % slides.length; slides[currentSlide].classList.add('active'); } prevBtn.addEventListener('click', () => showSlide(currentSlide - 1)); nextBtn.addEventListener('click', () => showSlide(currentSlide + 1)); // Автопрокрутка (опционально) setInterval(() => showSlide(currentSlide + 1), 3000);

📜 8. Полезные ссылки на генераторы

✅ Генераторы кода

📜 Резюме

В этой главе мы собрали:

  • ✅ JavaScript сниппеты (debounce, throttle, копирование, DOM)
  • ✅ CSS сниппеты (центрирование, текст, градиенты, эффекты)
  • ✅ HTML сниппеты (базовая структура, изображения)
  • ✅ Готовые функции (массивы, объекты, форматирование)
  • ✅ Модальное окно (полная реализация)
  • ✅ Валидация формы
  • ✅ Простой слайдер
  • ✅ Полезные генераторы
💡 Как использовать

Все сниппеты можно копировать и использовать в своих проектах. Для удобства используйте генератор сниппетов для VS Code.