Глава 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) — интерпретируемый язык программирования, выполняющийся в браузере. Позволяет создавать интерактивные веб-страницы.
- Изменения содержимого и стилей страницы
- Реакции на действия пользователя (клики, ввод)
- Отправки запросов на сервер (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...)
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); // SamJavaScript позволяет НЕ использовать ключевые слова 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); // trueNumber
Тип 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 + 5console.log(a);// 28 - -=
Вычитание с последующим присвоением результата. Например:
let a = 28; a -= 10;// аналогично a = a - 10console.log(a);// 18 - *=
Умножение с последующим присвоением результата:
let x = 20; x *= 2;// аналогично x = x * 2console.log(x);// 40 - **=
Возведение в степень с последующим присвоением результата:
let x = 5; x **= 2; console.log(x);// 25 - /=
Деление с последующим присвоением результата:
let x = 40; x /= 4;// аналогично x = x / 4console.log(x);// 10 - %=
Получение остатка от деления с последующим присвоением результата:
let x = 10; x %= 3;// аналогично x = x % 3console.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. Затем выстраиваем остатки в линию в обратном порядке и таким образом формируем двоичное представление числа. Конкретно в данном случае по шагам:
- Делим число 13 на 2. Результат деления - 6, остаток от деления - 1 (так как 13 - 6 *2 = 1)
- Далее делим результат предыдущей операции деления - число 6 на 2. Результат деления - 3, остаток от деления - 0
- Делим результат предыдущей операции деления - число 3 на 2. Результат деления - 1, остаток от деления - 1
- Делим результат предыдущей операции деления - число 1 на 2. Результат деления - 0, остаток от деления - 1
- Последний результат деления равен 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 - 8console.log(res);// 82в двоичном представлении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 на три разряда вправо = 1111111111111110console.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. Поэтому получаем:
| 1 | 0 | 1 |
| 0 | 1 | 0 |
| 1 | 1 | 1 |
В итоге получаем число 111, что в десятичной записи представляет число 7.
Возьмем другое выражение 6 & 2. Число 6 в двоичной записи равно 110, а число 2 - 10 или 010. Умножим соответствующие разряды
обоих чисел. Произведение обоих разрядов равно 1, если оба этих разряда равны 1. Иначе произведение равно 0. Поэтому получаем:
| 1 | 1 | 0 |
| 0 | 1 | 0 |
| 0 | 1 | 0 |
Получаем число 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);// falselet c = false; let d = true; c &&= d; console.log(c);// falselet a = true; let b = true; a &&= b; console.log(a);// truelet 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);// truelet a = true; let b = true; a ||= b; console.log(a);// truelet c = false; let d = true; c ||= d; console.log(c);// truelet 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
| 0 | 1 | 2 | 3 | 4 | 5 |
Двухмерный массив numbers2
| 0 | 1 | 2 |
| 3 | 4 | 5 |
Поскольку массив 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 можно представить в виде следующей таблицы:
| Tom | 25 | false |
| Bill | 38 | true |
| Alice | 21 | false |
Чтобы получить отдельный элемент массива, также используется индекс:
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){ |
| String |
false - если константа/переменная равна пустой строке, то есть ее длина равна 0, true - в остальных случаях |
|
| Object | Всегда возвращает |
const user = {name:"Tom"}; |
Конструкция 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 выполнение "проваливается" в следующий 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); // 8Function 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;- 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); // 10Scope 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 используется для создания изолированной области видимости, чтобы не загрязнять глобальное пространство имён.
📜 Рекурсия
Среди функций отдельно можно выделить рекурсивные функции. Их суть состоит в том, что функция вызывает саму себя.
Например, рассмотрим функцию, определяющую факториал числа:
// Факториал
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 не связан с объектом)Стрелочные функции не имеют собственного 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(); // objectthis и стрелочные функции
В стрелочных функциях в качестве 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 есть в userconst 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);// trueconst hasWeightProp = user.weight!==undefined; console.log(hasWeightProp);// false -
И так как объекты представляют тип
Object, а значит, имеет все его методы и свойства, то объекты также могут использовать метод hasOwnProperty(), который определен в типеObject:const hasNameProp = user.hasOwnProperty("name"); console.log(hasNameProp);// trueconst hasPrintMethod = user.hasOwnProperty("print"); console.log(hasPrintMethod);// trueconst 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); // 11this указывает на объект, для которого вызывается функция - в данном случае это глобальный объект 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 идет название класса (в данном случае класс называется
Это наиболее расспространенный способ определения класса. Но есть и другие способы. Так, также можно определить анонимный класс и присвоить его переменной или константе:
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);// undefinedreturn 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]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); // -1firstIndex имеет значение 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); // truefilter()
Метод 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){ .... }Параметры:
prev: предыдущий элемент (вначале - самый первый элемент)current: текущий элемент (вначале - второй элемент)curIndex: индекс текущего элемента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 и Charliereduce() - сведение к значению
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); // 5find() и 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); // 15const 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})/", где определены три группы:
- Первая группа
(\d{4})соответствует числу из четырех цифр - Вторая группа
(\d{2})соответствует числу из двух цифр - Третья группа аналогична второй
И если мы посмотрим на результат метода 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"📃 Готовые регулярные выражения
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
// Пример: user@example.comconst phoneRegex = /^(\+7|8)?[\s-]?\(?\d{3}\)?[\s-]?\d{3}[\s-]?\d{2}[\s-]?\d{2}$/;
// Примеры: +7 (999) 123-45-67, 8-999-123-45-67const urlRegex = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/;
// Примеры: https://example.com, http://sub.example.com/pathconst dateRegex = /^(0[1-9]|[12][0-9]|3[01])\.(0[1-9]|1[012])\.\d{4}$/;
// Пример: 31.12.2024const 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 выполняется всегда: и при успехе, и при ошибке. Используется для освобождения ресурсов (закрытие файлов, соединений и т.д.).
📜 Генерация ошибок и оператор 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); // ReferenceErrorTypeError - неправильный тип
// const num = 5;
// num.toUpperCase(); // TypeError: num.toUpperCase is not a functionRangeError - значение вне допустимого диапазона
// 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 openedRangeError: 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();📜 Лучшие практики
// Хорошо
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);В конструкторе 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-12getDay()возвращает 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)); // 34min() и 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); // 46ceil()
Функция 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); // -6round()
Функция 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); // 8sqrt()
Функция sqrt() возвращает квадратный корень числа:
const x = Math.sqrt(121); // 11
const y = Math.sqrt(9); // 3
const z = Math.sqrt(20); // 4.47213595499958log()
Функция 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);// trueconst b = Number.isNaN(true);// false - new Number(true) = 1const c = Number.isNaN(null);// false - new Number(null) = 0const d = Number.isNaN(25);// falseconst 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.9console.log(a); const b = Number.parseFloat("hello");// NaNconsole.log(b); const c = Number.parseFloat("34hello");// 34console.log(c); - parseInt(): преобразует строку в целое число. Например:
const a = Number.parseInt("34.90");// 34console.log(a); const b = Number.parseInt("hello");// NaNconsole.log(b); const c = Number.parseInt("25hello");// 25console.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 автоматически- Только объекты (не примитивы)
- Нельзя перебирать
- Нет свойства 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 — любые типы, в 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 автоматически- Ключи только объекты (не примитивы)
- Нельзя перебирать (нет 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.
Каждый HTML-элемент в DOM представлен объектом (узлом). Документ имеет иерархическую структуру: родители, потомки, соседние элементы.
📜 Поиск элементов
getElementById(value): выбирает элемент, у которого атрибут
. Если элемента с таким идентификатором нет, то возвращается nullidравен valuegetElementsByTagName(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): выбирает все элементы, которые не соответствуют селектору sE F: выбирает все элементы типа F, которые встречаются в элементах типа EE > F: выбирает все элементы типа F, которые являются вложенными в элементы типа EE + F: выбирает все элементы типа F, которые располагаются сразу после элементов типа EE ~ F: ввыбирает все элементы типа F, которые являются сестринскими по отношению к элементам типа E
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>'; // Отобразится как текст, не как HTMLinnerHTML
const div = document.querySelector('#content');
// Получить HTML
console.log(div.innerHTML); // HTML-код внутри элемента
// Установить HTML
div.innerHTML = '<p>Новый <b>HTML</b></p>';
// Добавить к существующему
div.innerHTML += '<p>Ещё один параграф</p>';Использование 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); // объект documentconst 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);- Можно назначить несколько обработчиков на одно событие
- Можно удалять обработчики
- Поддержка фазы захвата (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 | возникает при полной загрузке дерева DOM | Event |
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)
- 0 (
- 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 проходят три фазы:
- Фаза захвата (нисходящее/capturing) — от window к целевому элементу: событие распространяется вверх по дереву DOM от дочерних узлов к родительским
- Фаза цели (target) — на самом элементе
- Фаза всплытия (восходящее/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 на форме searchconst 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){// получаем значение поля keyconst 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){// получаем значение поля keyconst 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){// получаем значение поля keyconst 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){// получаем значение поля keyconst keyBox = document.search.key; const val = keyBox.value;// получаем элемент printBlockconst printBlock = document.getElementById("printBlock");// создаем новый параграфconst pElement = document.createElement("p");// устанавливаем у него текстpElement.textContent = val;// добавляем параграф в printBlockprintBlock.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){// получаем элемент printBlockconst 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){// получаем элемент printBlockconst 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){// получаем элемент printBlockconst 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){// получаем элемент printBlockconst printBlock = document.getElementById("printBlock");// получаем введенный символconst val = String.fromCharCode(e.keyCode);// добавление символаprintBlock.textContent += val; } function onkeydown(e){ if(e.keyCode===8){// если нажат Backspace// получаем элемент printBlockconst 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: 0console.log("Text:", firstLanguage.text);// Text: JavaScriptconsole.log("Value:", firstLanguage.value);// Value: JS</script>Другой способ получить нужный элемент списка по индексу представляет метод item(), в который передается индекс элемента:
const firstLanguage = myForm.language.item(0); console.log("Index:", firstLanguage.index);// Index: 0console.log("Text:", firstLanguage.text);// Text: JavaScriptconsole.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;// использовать для добавления вызов метода addlanguagesSelect.add(newOption);// вместо вызова// languagesSelect.options[selectedIndex] = null;// использовать для удаления метод removelanguagesSelect.remove(selectedIndex);События элемента select. Обработка выбора в списке
Элемент select поддерживает три события:
blur(потеря фокуса),focus(получение фокуса)change(изменение выделенного элемента в списке)
<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// получаем выбранный элемент// для каждого выбранного элемента создаем divconst div = document.createElement("div");// создаем текстовый узел для выбранного элементаconst optionText = document.createTextNode(option); div.appendChild(optionText);// добавляем optionText в divselection.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;// Обработчик для всех radioradios.forEach(radio => { radio.addEventListener('change', (e) => { console.log('Выбрано:', e.target.value); }); });Select (выпадающий список)
const select = document.querySelector('select');// Получить значение выбранного optionconsole.log(select.value);// "2"// Получить текст выбранного optionconsole.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); });// Добавить новый optionconst option = document.createElement('option'); option.value = '4'; option.text = 'Четыре'; select.add(option);// Удалить optionselect.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) или нет (falsevalueMissing: возвращаетtrue, если в элементе формы, который требует обязательного ввода, отсутствует значениеtypeMismatch: возвращаетtrue, если введенное значение не соответствует типу элемента формы (например, вмент<input type="email">введен текст, которые не является адресом элементронной почты)patternMismatch: возвращаетtrue, если введенное значение не соответствует указанному шаблонуtooLong: возвращаетtrue, если введенное значение превышает максимально допустимый лимитtooShort: возвращаетtrue, если введенное значение меньше минимально допустимого значенияrangeUnderflow: возвращаетtrue, если введенное значение меньше диапазона допустимых значенийrangeOverflow: возвращаетtrue, если введенное значение превышает диапазон допустимых значенийstepMismatch: возвращаетtrue, если введенное значение не соответствует значению атрибутаstepbadInput: возвращает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/falseconsole.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;// Проверка usernameif (username === '') { showError('username', 'Имя пользователя обязательно'); isValid = false; } else if (username.length < 3) { showError('username', 'Минимум 3 символа'); isValid = false; }// Проверка emailif (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 URLreader.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);// 25console.log(screen.availLeft);// 0console.log(screen.availHeight);// 695console.log(screen.availWidth);// 1280console.log(screen.width);// 1280console.log(screen.height);// 800console.log(screen.pixelDepth);// 24console.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=640height/innerHeight: высота окна в пикселях. Например,height=480left/screenX: координата X относительно начала экрана в пикселях. Например,left=0top/screenY: координата Y относительно начала экрана в пикселях. Например,top=0location: указывает, будет ли отображаться адресная строка. Например,location=yesmenubar: указывает, будет ли отображаться панель меню. Например,menubar=yesscrollbars: указывает, будет ли окно иметь полосы прокрутки. Например,scrollbars=yesstatus: указывает, будет ли отображаться строка состояния. Например,status=yestoolbar: указывает, будет ли отображаться панель инструментов. Например,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);// без urlhistory.pushState(state, state.title, state.url);// с urlconsole.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();// получаем страницу из объекта pagesconst page = pages[pageName];// если текущий адрес совпадает с запрошенным, то игнорируем переходif(history.state.url != url) { contentElement.textContent = page.content;//добавляем в историю history.pushState(page,// объект stateevent.target.textContent,// Titleevent.target.href// URL); document.title = event.target.textContent;// если браузер не устанавливает заголовок} return event.preventDefault(); }// устанавливаем обработчик для извлечения состояния в History APIwindow.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 PagecontentElement.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();// получаем страницу из объекта pagesconst page = pages[pageName];Получив нужную страницу, смотрим, какая ссылка нажата. Например, мы не хотим, чтобы находясь на определенной странице, пользователь заново загружал данные этой страницы, повторно нажимая на одну и ту же ссылку. И для этой цели берем в истории просмотров текущее состояние и проверяем его свойство url. Если текущее состояние (по сути текущая страница) имеет тот же адрес url, который запрошен, то нет смысла заново перезагружать содержимое страницы:
if(history.state.url != url) {Если запрошен адрес, отличный от текущего, то устанавливаем в качестве заголовка содержимое (свойство content) текущей страницы и добавляем запись в историю просмотров:
contentElement.textContent = page.content;//добавляем в историю history.pushState(page,// объект stateevent.target.textContent,// Titleevent.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();// получаем страницу из объекта pagesconst page = pages[pageName];// если текущий адрес совпадает с запрошенным, то игнорируем переходif(history.state.url != url) { contentElement.textContent = page.content;// добавляем в историюhistory.pushState(page,// объект stateevent.target.textContent,// Titleevent.target.href// URL); document.title = event.target.textContent;// если браузер не устанавливает заголовок} return event.preventDefault(); }// устанавливаем обработчик для извлечения состояния в History APIwindow.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 PagecontentElement.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 текущей страницы.
// Текущий URLconsole.log(location.href);// Полный URL: https://example.com:8080/path?query=value#hash// Части URLconsole.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: URLSearchParamsconst 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');// Обновить URLconst 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);// Mozillaconsole.log(navigator.appName);// Netscapeconsole.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.36console.log(navigator.product);// Geckoconsole.log(navigator.productSub);// 20030107console.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.36console.log(navigator.platform);// MacIntelconsole.log(navigator.languages);// список поддерживаемых языковconsole.log(navigator.plugins);// список поддерживаемых плагиновЕще примеры:
// Информация о браузереconsole.log(navigator.userAgent);// Строка User-Agentconsole.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, который может потом использоваться для остановки анимации:// получаем idconst 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 через рекурсивный setTimeoutfunction 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():// Объект JavaScriptconst 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);// Tomconsole.log(tomUser.age);// 39console.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. Позже получаем из localStorageconst storedJson = localStorage.getItem('company');// 5. Преобразуем обратно в объект JavaScriptconst 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");// Обращаемся к первому элементу userconst firstUser = xmlDOM.querySelector("user"); console.log(firstUser.getAttribute("name"));// Tomconsole.log(firstUser.getAttribute("age"));// 39console.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 с помощью метода
querySelectorconst firstUser = xmlDOM.querySelector("user");Далее мы можем обращаться к содержимому элемента user - к его вложенным элементам и атриубтам
console.log(firstUser.getAttribute("name"));// Tomconsole.log(firstUser.getAttribute("age"));// 39console.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");// Получаем все элементы userconst 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 лет, работает в GoogleXMLSerializer - Сериализация 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>‘// Преобразуем строку в XMLconst 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)
// Получаем данные с APIfetch('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 };// Сохраняем в localStoragelocalStorage.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;";⚠️ Важные параметры cookiespath=/— cookie доступна для всего сайтаdomain=example.com— cookie доступна для домена и поддоменовsecure— передача только по HTTPSsamesite=strict— защита от CSRF-атак
📃 Чтение/получение cookies
Для простейшего извлечения куки из браузера достаточно обратиться к свойству document.cookie:
console.log(document.cookie);// Получаем все cookies в виде строки// Пример вывода: "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
// Простая установка cookiedocument.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=0document.cookie = "theme=; max-age=0; path=/";📃 Вспомогательные функции для работы с cookies
// Функция для установки cookiefunction 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=/`; }// Функция для получения cookiefunction 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; }// Функция для удаления cookiefunction 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: через цикл forfor (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(); }loadCart() { const data = localStorage.getItem(this.storageKey); return data ? JSON.parse(data) : []; }// Загрузка корзины из localStorage// Сохранение корзины в localStoragesaveCart() { 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 перед использованием
Пример безопасной работы с токенами
// ❌ ПЛОХО: храним токен в localStoragelocalStorage.setItem('authToken', 'secret123');// ✅ ХОРОШО: используем HttpOnly cookie (устанавливается сервером)//Клиентский JS не может прочитать такую cookie//Set-Cookie: authToken=secret123; HttpOnly; Secure; SameSite=Strict//Для refresh-токенов можно использовать localStorage с осторожностью//Но access-токен всегда в HttpOnly cookie!📜 Проверка доступности и лимитов
// Проверка поддержки localStoragefunction 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 с автоматическим JSONconst 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-функций (функций обратного вызова), которые передавались в другую функцию и вызывались позже в некоторый момент времени. Простейший шаблон использования коллбеков:
// Функция принимает callbackfunction 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передается функцияhandleResultasyncFunction(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 секунды. Однако его задержка не помешала выполнению остальных промисов.
// Синтаксис создания Promiseconst myPromise = new Promise(function(resolve, reject) {// Асинхронная операцияconsole.log("Выполнение асинхронной операции");// Если успешно - вызываем resolve()resolve("Успех!");// Если ошибка - вызываем reject()// reject("Ошибка!");});Простой пример Promise
// Создаём Promiseconst 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. Например:
// Функция возвращает Promisefunction sum(x, y){ return new Promise(function(resolve){ const result = x + y; resolve(result); }) }// Использованиеsum(3, 5).then(function(value){ console.log("Результат операции:", value);});// Результат: 8sum(25, 4).then(function(value){ console.log("Сумма чисел:", value);});// Результат: 29// Или с arrow functionsum(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:37throw
Также ошибка может быть результатом вызова оператора 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.
Рассмотрим поэтапно.
- Сначала создается промис
helloPromise:const helloPromise = new Promise(function(resolve){ resolve("Hello"); });В асинхронной операции с помощью вызова
resolve("Hello")промис переводится в состояниеfulfilled, то есть выполнение операции успешно завершено. А во вне передается значение "Hello". - Далее у промиса
helloPromiseвызывается метод then():const worldPromise = helloPromise.then(function(value){// возвращаем новое значениеreturn value + " World"; });В качестве значения параметра
valueфункция обработчика получает строку "Hello" и затем возвращает строку "Hello World". Эта строка затем можно будет получить через методthen()нового промиса, который генерируется вызовомhelloPromise.then()и который называется здесьworldPromise. - Затем аналогичным образом у промиса
worldPromiseвызывается метод then():const metanitPromise = worldPromise.then(function(value){// возвращаем новое значениеreturn value + " from METANIT.COM"; });В качестве значения параметра
valueфункция обработчика получает строку "Hello World" и затем возвращает строку "Hello World from METANIT.COM". ВызовworldPromise.then()возвращает новый промисmetanitPromise. - На последним этапе у промиса
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 numberprintNumber("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 numberprintNumber("3");// Result: 9printNumber("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));// Hellopromise2.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() автоматически возвращает PromiseasyncFunction().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); }); }// Асинхронная функция с awaitasync 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);// 8const value2 = await sum(6, 4); console.log("Результат 2 асинхронной операции:", value2);// 10const 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 numbercalculate("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");// Результат: 16calculate("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...ofasync 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// Bobawait в асинхронных генераторах
Главным преимуществом асинхронным генераторов является то, что мы можем использовать в них оператор 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(); }// Обёртка с debounceconst 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.
Создание объекта
// Создание объекта XMLHttpRequestconst 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— имя пользователя для аутентификации (опционально), которое применяется при его аутентификации на сервере (то есть для определения, какой именно пользователь осуществил запрос), по умолчанию равно nullpassword— пароль для аутентификации (опционально, по умолчанию равно null)
// GET-запросxhr.open("GET", "/api/users");// POST-запросxhr.open("POST", "/api/users");// Синхронный запрос (НЕ РЕКОМЕНДУЕТСЯ!)xhr.open("GET", "/api/users", false);📜 Основные свойства XMLHttpRequest
В дополнение к методам объект XMLHttpRequest предоставляет ряд свойств, которые позволяют настроить отправку запроса или извлечт полученные от сервера данные:
Свойство Описание readyStateСостояние запроса (0-4) statusHTTP код ответа (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/HTMLresponseTypeТип ответа ("", "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После запуска запроса. onloadstartprogressПри выполнении запроса. onprogressloadУспешное завершение запроса onloadloadendЗавершение запроса (успех или ошибка) onloadenderrorОшибка сети onerrorabortЗапрос прерван onaborttimeoutПревышен таймаут ontimeoutreadystatechangeИзменение readyState onreadystatechange📜 Процесс выполнения ajax-запроса
В общем случае процесс выполнения ajax-запроса с помощью XMLHttpRequest выглядит следующим образом:
- Создается объект XMLHttpRequest
const request = new XMLHttpRequest(); - Устанавливается обработчик событий загрузки (например, через свойство onload), который будет вызываться после завершения HTTP-запроса
request.onload = (event) => { console.log("request finished");} -
Запускается HTTP-запрос с помощью метода open(). Методу передается метод HTTP, который будет использоваться для запроса (например, GET или POST), URL-адрес, к которому должен быть отправлен запрос, и при необходимости другие необязательные аргументы
request.open("GET", "http://localhost/hello"); -
При необходимости производиться дополнительная конфигурация HTTP-запроса. Например, с помощью метода
setRequestHeader()можно определить заголовки, которые будут отправляться вместе с запросом. Однако важно выполнить эту настройку после предыдущего шага, то есть после вызова методаopen(), но перед следующим шагом, то есть перед вызовом методаsend()request.setRequestHeader("Accept", "text/plain");// установка заголовка на прием данных -
Непосредственно отправляется 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();xhr.open("GET", "/hello");// GET-запрос к ресурсу /helloxhr.onload = () => { if (xhr.status == 200) {// обработчик получения ответа сервера// если код ответа 200console.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) {// если код ответа 200console.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-запрос к ресурсу /helloxhr.open("GET", "/hello");// обработчик получения ответа сервераxhr.onreadystatechange = () => { if (xhr.readyState == 4) {// если запрос завершенif (xhr.status == 200) {// если код ответа 200console.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) {// DONEif (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");// Указываем, что отправляем JSONxhr.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
// Создаём объект FormDataconst 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-Typexhr.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.htmlif(!filePath) filePath = "index.html";// в качестве типа ответа устанавливаем htmlresponse.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) {// если код ответа 200const html = xhr.responseText;// получаем ответconsole.log(html);// выводим полученный ответ на консоль браузера} else {// иначе выводим текст статусаconsole.log("Server response: ", xhr.statusText); } }; xhr.open("GET", "/home.html");// GET-запрос к ресурсу /home.htmlxhr.setRequestHeader("Accept", "text/html");// принимаем только htmlxhr.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");// элемент для загрузки htmlconst 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.htmlxhr.setRequestHeader("Accept", "text/html");// принимаем только htmlxhr.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) {// загружаем только содержимое элемента bodycontentDiv.innerHTML = xhr.responseXML.body.innerHTML; } else { console.log("Server response: ", xhr.statusText); } }; xhr.open("GET", "/home.html");// GET-запрос к ресурсу /home.htmlxhr.responseType = "document";// устанавливаем тип ответаxhr.setRequestHeader("Accept", "text/html");// принимаем только htmlxhr.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");// принимаем только htmlxhr.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(); }); }// по умолчанию загружаем компонент homeloadContent("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) загружаем в элементcontentDivcontentDiv.innerHTML = xhr.responseText;// xhr.responseXML.body.innerHTML;При загрузке страницы сразу загружаем код компонента home, как компонента по умолчанию:
loadContent("home");Таким образом, на главной странице мы сможем обращаться к конкретным компонентам, переходя по ссылкам:
Еще пример:
const xhr = new XMLHttpRequest(); xhr.open("GET", "/pages/about.html");// Указываем, что ожидаем HTMLxhr.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)=>{// если запрошены данные xmlif(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-запрос к /dataxhr.responseType = "document";// устанавливаем тип ответаxhr.setRequestHeader("Accept", "text/xml");// принимаем только xmlxhr.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();// выбираем все элементы userconst 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();// выбираем все элементы userconst 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");// Указываем, что ожидаем XMLxhr.setRequestHeader("Accept", "text/xml"); xhr.onload = () => { if (xhr.status === 200) {// Получаем XML-документconst xmlDoc = xhr.responseXML;// Работаем с XML как с DOMconst 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)=>{// если запрошены данные xmlif(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-запрос к /dataxhr.responseType = "json";// устанавливаем тип ответаxhr.setRequestHeader("Accept", "application/json");// принимаем только jsonxhr.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();// выбираем все элементы userconst 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 в элемент
contentDivconst contentDiv = document.getElementById("content");Для создания таблицы определены две вспомогательные функции. Функция createTable создает элемент table с одной строкой - заголовками столбцов. Функция createRow принимает через параметры имя, возраст и контакты пользователя и для них создает строку.
В основной части кода выполняем запрос на сервер. Получив данные JSON, выбираем массив объектов user:
const json = xhr.response; const table = createTable();// выбираем все объекты userconst 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");xhr.responseType = "json"; xhr.onload = () => { if (xhr.status === 200) {// Автоматически парсим JSON// 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-запрос к ресурсу /userxhr.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 };// кодируем объект в формат jsonconst 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; }// для параметра namelet userName = "";// для параметра agelet 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();// Отменяем стандартную отправку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>// Создаём FormData из формы📜 Установка заголовков
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-запрос с Promisefunction 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;// ✅ БЕЗОПАСНО: экранирование HTMLfunction escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } element.innerHTML = escapeHtml(xhr.responseText);// ✅ ЕЩЁ БЕЗОПАСНЕЕ: использование textContentelement.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 для одной и той же задачи:
// XMLHttpRequestconst 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
// Запрос к APIfetch('/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 имеет следующие свойства:
Свойство Описание oktrue если статус 200-299 хранит булевое значение, которое указывает, завершился ли запрос успешно (то есть если статусной код ответа находится в диапазоне 200-299)
statusHTTP код ответа (200, 404, 500...) хранит статусный код ответа
statusTextТекст статуса ("OK", "Not Found"...) хранит сообщение статуса, которое соответствует статусному коду headersОбъект Headers с заголовками urlURL запроса. Хранит адрес 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);// 200console.log(response.statusText);// OKconsole.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);// 200console.log(response.statusText);// OKconsole.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/falseconsole.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/newpageresponse.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");// из объекта ответа извлекаем jsonconst 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/awaitgetUsers(); 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 для blobconst 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)принимает объект с настройками:Опция Описание Значения methodHTTP-метод "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" signalAbortSignal для отмены 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");// добавляем заголовок AcceptmyHeaders.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 — получение данных
// Простой GETconst 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); } }// Собираем chunksconst 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)); }// получение одного пользователя по idelse if (request.url.match(/\/api\/users\/([0-9]+)/) && request.method === "GET") {// получаем id из адреса urlconst id = request.url.split("/")[3];// получаем пользователя по idconst 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: "Пользователь не найден" })); } }// удаление пользователя по idelse if (request.url.match(/\/api\/users\/([0-9]+)/) && request.method === "DELETE") {// получаем id из адреса urlconst id = request.url.split("/")[3];// получаем индекс пользователя по idconst 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};// находим максимальный idconst 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);// получаем пользователя по idconst 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 из адреса urlconst id = request.url.split("/")[3];// получаем пользователя по idconst 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"находим индекс объекта в массива. И если объект найден, то удаляем его из массива и отправляем клиенту:// удаление пользователя по idelse if (request.url.match(/\/api\/users\/([0-9]+)/) && request.method === "DELETE") {// получаем id из адреса urlconst id = request.url.split("/")[3];// получаем индекс пользователя по idconst 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};// находим максимальный idconst 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);// получаем пользователя по idconst 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 для timeoutconst 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 с JSONawait 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');⚠️ Протоколы WebSocketws://— незащищённое соединение (аналог 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)
Константа Значение Описание CONNECTING0 Соединение устанавливается OPEN1 Соединение открыто CLOSING2 Соединение закрывается CLOSED3 Соединение закрыто 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' };// отправляем объект в формате jsonsocket.send(JSON.stringify(data));Отправка бинарных данных
// ArrayBufferconst buffer = new ArrayBuffer(8); socket.send(buffer);// Blobconst 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);// Попытка распарсить JSONtry { const json = JSON.parse(data); console.log('JSON:', json); } catch (e) { console.log('Обычный текст'); } }// Если данные - Blobelse if (data instanceof Blob) { console.log('Blob размером', data.size, 'байт');// Читаем Blobconst reader = new FileReader(); reader.onload = () => { console.log('Содержимое:', reader.result); }; reader.readAsText(data); }// Если данные - ArrayBufferelse 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; }// События кнопки и EntersendBtn.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");// с помощью addEventListenerevtSource.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Адрес отправителя lastEventIdID последнего события 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)
Константа Значение Описание CONNECTING0 Соединение устанавливается или переподключение OPEN1 Соединение открыто CLOSED2 Соединение закрыто 📜 Пользовательские события 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"){// если запрос SSEif (request.headers.accept && request.headers.accept === "text/event-stream") { sendEvent(response); } else{ response.writeHead(400); response.end("Bad Request"); } } else{// в остальных случаях отправляем страницу index.htmlfs.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) {// генерируем случайное число - индекс для массива messagesconst 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"){// если запрос SSEif (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) {// генерируем случайное число - индекс для массива messagesconst 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 APIif (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.2023const 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.2021const 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:42const mediumTime = new Intl.DateTimeFormat("ru", {timeStyle: "medium"}).format(now); console.log(mediumTime);// 20:42:08const longTime = new Intl.DateTimeFormat("ru", {timeStyle: "long"}).format(now); console.log(longTime);// 20:42:08 GMT+11const 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", ... Часовой пояс hour12true, false 12/24-часовой формат Альтернатива: методы Date
Следует отметить, что тип
Dateтакже предоставляет ряд методов для локализации даты и времени:toLocaleString()toLocaleDateString()toLocaleTimeString()
В качестве параметра эти методы принимают локаль, в которую надо локализовать дату и время:
const now = new Date(); console.log(now.toLocaleString("en"));// 11/16/2023, 9:17:25 PMconsole.log(now.toLocaleTimeString("en"));// 9:17:25 PMconsole.log(now.toLocaleDateString("en"));// 11/16/2023console.log(now.toLocaleString("ru"));// 16.11.2023, 21:17:25console.log(now.toLocaleTimeString("ru"));// 21:17:25console.log(now.toLocaleDateString("ru"));// 16.11.2023Еще пример:
const now = new Date();//Методы Date тоже поддерживают локализацию console.log(now.toLocaleString("ru"));// 15.11.2025, 14:30:45console.log(now.toLocaleDateString("ru"));// 15.11.2025console.log(now.toLocaleTimeString("ru"));// 14:30:45console.log(now.toLocaleString("en-US"));// 11/15/2025, 2:30:45 PMconsole.log(now.toLocaleDateString("en-US"));// 11/15/2025console.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число Максимум знаков после запятой useGroupingtrue, 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.67console.log(ru);// 5 500,67console.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.10console.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 Lconsole.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.56console.log(num.toLocaleString("de"));// 1.007,56console.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 Statesconsole.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);// Germanyconsole.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);// Cyrillicconsole.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);// Russianconsole.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("б", "А"));// 1console.log(collator.compare("б", "Б"));// -1console.log(collator.compare("б", "В"));// -1console.log(collator.compare("б", "б"));// 0console.log(collator.compare("мир", "миг"));// 1console.log(collator.compare("мир", "миф"));// -1console.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"));// yesterdayconsole.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, 2025console.log(ru.formatNumber(1234567));// 1 234 567console.log(en.formatNumber(1234567));// 1,234,567console.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.jslet 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.jsfunction 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.jsconsole.log("Module loaded!");// main.jsimport './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.jsexport 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.jsexport default class User { constructor(name) { this.name = name; } greet() { console.log(`Hello, ${this.name}!`); } }// main.jsimport User from './User.js'; const user = new User('Alice'); user.greet();// "Hello, Alice!"3. Объект по умолчанию
// config.jsexport default { apiUrl: 'https://api.example.com', timeout: 5000, debug: true };// main.jsimport config from './config.js'; console.log(config.apiUrl);// "https://api.example.com"console.log(config.timeout);// 50004. Комбинирование default и named exports
Модуль может одновременно экспортировать отдельные компоненты и компонент по умолчанию:
// message.jsexport 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.jsexport 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()в качестве параметра получает загруженный модуль. Далее мы можем обращаться к компонентам модуля по имени. Например, обращение к константеhellomodule.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.jsexport function sum(a, b) { return a + b; } export function multiply(a, b) { return a * b; }// utils/string.jsexport function capitalize(str) { return str.charAt(0).toUpperCase() + str.slice(1); }// components/Button.jsexport default class Button { constructor(text) { this.text = text; } render() { const button = document.createElement('button'); button.textContent = this.text; return button; } }// main.jsimport { sum, multiply } from './utils/math.js'; import { capitalize } from './utils/string.js'; import Button from './components/Button.js'; console.log(sum(5, 3));// 8console.log(capitalize('hello'));// "Hello"const btn = new Button('Click me'); document.body.appendChild(btn.render());Пример 2: API клиент
// api/client.jsconst 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.jsimport { 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.jsexport default { apiUrl: 'http://localhost:3000', debug: true, logLevel: 'verbose' };// config/production.jsexport default { apiUrl: 'https://api.production.com', debug: false, logLevel: 'error' };// config/index.jsconst 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.jsimport 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 задает выравнивание текста по базовой линии. Оно может принимать следующие значения:
topmiddlebottomalphabetichangingideographic
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: масштабирование по оси Xb: поворот вокруг оси Xc: поворот вокруг оси Yd: масштабирование по оси Ye: горизонтальное смещение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.Анимация сама по себе предполагает последовательную смену кадров, причем каждый анимации кадр нужно нарисовать самостоятельно. В общем случае чтобы создать анимацию, следует выполнить следующие шаги:
- Очистка области рисования
- Опциональное сохранение состояния
- Рисование отдельного кадра
- Опциональная загрузка состояния
Эти шаги обычно инкапсулируются в функцию, которая затем вызывается через равные промежутки времени (для каждого кадра анимации). Для выполнения функции применяется метод 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() {// Очистка canvasctx.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;// получаем бд// создаем хранилище объектов usersconst 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);// testconsole.log(transaction.mode);// readwriteconsole.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");// получаем хранилище usersconsole.log(userStore); };Тип IDBObjectStore предоставляет следующий ряд методов для операций над данными в хранилище:
- add(): добавляет новые объекты в хранилище
- clear(): очищает хранилище (удаленяет все объекты)
- count(): возвращает общее количество объектов
- createIndex(): создает новый индекс
- delete(): удаляет из хранилища объект с определенным ключом
- deleteIndex(): удаляет указанный индекс
- get(): возвращает объект c указанным ключом
- getKey(): возвращает ключ объекта
- getAll(): возвращает все объекты из хранилища
- getAllKeys(): возвращает ключи объектов
- index(): возвращает индекс хранилища
- openCursor(): используется для перебора хранилища объектов по первичному ключу с помощью курсора
- openKeyCursor(): возвращает курсор для перебора хранилища объектов
- put(): обновляет существующие объекты в хранилище
📜 Добавление данных (объектов в хранилище)
Для добавления объектов в хранилище базы данных IndexDB применяется метод add() объекта
IDBObjectStoreadd(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// при создании или изменении версии базы данных создаем в ней хранилище usersrequest.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");// получаем хранилище usersconst tom = {name: "Tom", age: 39}; const addRequest = userStore.add(tom);// добавляем объект tom в хранилище userStoreaddRequest.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.resultconsole.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");// получаем хранилище usersconst 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);// получаем объекты, у которых свойство ключа равно 2getRequest.onsuccess = (e) => { console.log(e.target.result); }В данном случае получаем все элементы, у которых значение свойства-ключа равно 2.
Применение объекта IDBKeyRange предоставляет дополнительные возможности с помощью ряда статических методов:
IDBKeyRange.bound(): создает диапазон ключей, для которого задано минимальное и максимальное значенияIDBKeyRange.only(): создает диапазон ключей, который содержит только одно значениеIDBKeyRange.lowerBound(): создает диапазон ключей, для которого задано минимальное значениеIDBKeyRange.upperBound(): создает диапазон ключей, для которого задано максимальное значение
Диапазоны ключей, которые создаются с помощью этих методов, можно описать следующей таблицей:
Диапазон Код Значение ключа >= xIDBKeyRange.lowerBound(x)Значение ключа > xIDBKeyRange.lowerBound(x, true)Значение ключа <= yIDBKeyRange.upperBound(y)Значение ключа < yIDBKeyRange.upperBound(y, true)Значение ключа >= x&& <=yIDBKeyRange.bound(x, y)Значение ключа > x<yIDBKeyRange.bound(x, y, true, true)Значение ключа > x&& <=yIDBKeyRange.bound(x, y, true, false)Значение ключа >= x&&<yIDBKeyRange.bound(x, y, false, true)Значение ключа = zIDBKeyRange.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");// получаем хранилище usersconst getRequest = userStore.get(1);// получаем объект по ключу 1getRequest.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);// получаем объект по ключу 1getRequest.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=1const 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() интерфейсаIDBObjectStorecount() 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(): создает диапазон ключей, для которого задано максимальное значение
Диапазоны ключей, которые создаются с помощью этих методов, можно описать следующей таблицей:
Диапазон Код Значение ключа >= xIDBKeyRange.lowerBound(x)Значение ключа > xIDBKeyRange.lowerBound(x, true)Значение ключа <= yIDBKeyRange.lowerBound(y)Значение ключа < yIDBKeyRange.upperBound(y, true)Значение ключа >= x&& <=yIDBKeyRange.bound(x, y)Значение ключа > x<yIDBKeyRange.bound(x, y, true, true)Значение ключа > x&& <=yIDBKeyRange.bound(x, y, true, false)Значение ключа >= x&&<yIDBKeyRange.bound(x, y, false, true)Значение ключа = zIDBKeyRange.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"// удаляем все объекты, у которых ключ равен 1const 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(): создает диапазон ключей, для которого задано максимальное значение
Диапазоны ключей, которые создаются с помощью этих методов, можно описать следующей таблицей:
Диапазон Код Значение ключа >= xIDBKeyRange.lowerBound(x)Значение ключа > xIDBKeyRange.lowerBound(x, true)Значение ключа <= yIDBKeyRange.lowerBound(y)Значение ключа < yIDBKeyRange.upperBound(y, true)Значение ключа >= x&& <=yIDBKeyRange.bound(x, y)Значение ключа > x<yIDBKeyRange.bound(x, y, true, true)Значение ключа > x&& <=yIDBKeyRange.bound(x, y, true, false)Значение ключа >= x&&<yIDBKeyRange.bound(x, y, false, true)Значение ключа = zIDBKeyRange.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.resultconsole.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");// получаем хранилище usersconst cursorRequest = userStore.openCursor();// получаем запрос на открытие курсора// получаем курсорcursorRequest.onsuccess = () => { const cursor = cursorRequest.result;// также можно получить через event.target.resultconst user = cursor.value;// получаем значение, на которое указывает курсорconsole.log(user.id);// он же cursor.keyconsole.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.resultif(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");// Предупреждаем событие droptarget.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,noneeffectAllowedУстанавливает возможные типы операций: 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— время последнего изменения
📜 Чтение содержимого файла
Для чтения содержимого перетаскиваемых файлов используется
FileReaderAPI:<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 Представляет список объектов FileBlob Представляет бинарные данные 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Имя файла typeMIME-тип файла (например, "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 необходимо:
- Определить элемент на веб-странице, на который пользователь будет перетаскивать файл(ы).
В качестве такого элемента может служить элемент
<div> - Зарегистрировать обработчики двух событий: для события перетаскивания 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 URLURL-адреса данных — это особая схема 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.resultconsole.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() {// создаем элемент divconst 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.htmlif (!filePath) filePath = "index.html";// в качестве типа ответа устанавливаем htmlresponse.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 веб-воркера.
// установка обработчика события через addEventListenerworker.addEventListener("message", (event) => { console.log(event.data); });// установка обработчика события через свойство onmessageworker.onmessage = (event) => { console.log(event.data); };В обработчик события передается объект
MessageEvent, у которого через свойствоdataможно получить переданные данные.Полный пример обмена сообщениями
Файл worker.js
// прослушиваем событие messageself.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: куда направлено устройство пользователя.0°(градусов) соответствует северу, а направление определяется по часовой стрелке (это означает, что восток равен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Возникает при начале речи onstartendВозникает при завершении речи onendpauseВозникает, когда речь приостановлена onpauseresumeВозникает, когда речь возобновлена onresumeerrorВозникает при возникновении ошибки onerrorboundaryВозникает при достижении границы слова или фразы onboundarymarkВозникает при достижении именованного тега "метки" 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Служба начала прослушивать звук onstartaudiostartНачался захват звука onaudiostartsoundstartОбнаружен звук (речь или шум) onsoundstartspeechstartОбнаружена речь onspeechstartresultСлужба возвращает результат — слово или фразу onresultspeechendЗавершено обнаружение речи onspeechendsoundendЗавершено обнаружение звука onsoundendaudioendЗавершен захват звука onaudioendendСлужба распознавания отключилась onenderrorВозникла ошибка onerrornomatchРаспознавание не удалось 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));// 9console.log(MathLib.MAX);// 1234MathLib.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));// 9console.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();// 15calculator.subtract(20, 8).print();// 12console.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 WorldlocaleModule.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 и выше:// Определение типа устройства по DPRconst 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 <strong>text</strong>// Работа с полями форм$("#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();// с paddingvar oh = $("div").outerHeight();// с padding и bordervar 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-элементов
// ❌ Плохо: повторяющиеся запросы к DOMdocument.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-elseif (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);// 1console.log(second);// 2console.log(rest);// [3, 4, 5]// Пропуск элементовconst [, , third] = [1, 2, 3]; console.log(third);// 31.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);// 151.7. Template literals (шаблонные строки)
// ❌ Конкатенация строкconst greeting = 'Привет, ' + name + '! Тебе ' + age + ' лет.';// ✅ Template literalsconst 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, shouldconst 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
// ❌ Много операций с DOMfor (let i = 0; i < 1000; i++) { document.body.innerHTML += `<div>${i}</div>`; }// ✅ Используйте DocumentFragmentconst 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 malformed1.3. Логические ошибки (Logic Errors)
🔵 Логические ошибкиКод работает без ошибок, но результат не тот, что ожидался. Самые сложные для поиска!
// ❌ Ошибка: присваивание вместо сравненияif (x = 5) {// должно быть x === 5console.log('x равно 5'); }// ❌ Ошибка: неправильный порядок операцийconst total = price + tax * quantity;//(price + tax) * quantity ?// ❌ Ошибка: off-by-onefor (let i = 0; i <= arr.length; i++) {// должно быть i < arr.lengthconsole.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 вызван: 1processItem('b');// processItem вызван: 2processItem('c');// processItem вызван: 3// console.trace() - стек вызововfunction foo() { function bar() { console.trace('Trace'); } bar(); } foo();2.4. console.assert() - проверка условий
// Выводит ошибку только если условие falseconst 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)
💡 Как использовать отладчик- Откройте DevTools (F12 или Ctrl+Shift+I)
- Перейдите на вкладку Sources (Chrome) или Debugger (Firefox)
- Найдите нужный файл в дереве файлов слева
- Кликните на номер строки, чтобы поставить breakpoint (синяя метка)
- Обновите страницу — выполнение остановится на 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 literal4.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// ✅ Правильно: awaitasync function getData() { const response = await fetch('https://api.example.com/data'); const data = await response.json(); console.log(data);// данные загруженыreturn data; }// ✅ Правильно: thenfetch('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)// ✅ Правильно: bindconst 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 — статический анализ
// Устанавливаем ESLintnpm 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. Чек-лист отладки
✅ Шаги при поиске ошибки- ☐ Прочитайте сообщение об ошибке в консоли
- ☐ Найдите строку, где произошла ошибка
- ☐ Добавьте console.log() до и после проблемного места
- ☐ Проверьте типы данных (typeof, console.log)
- ☐ Используйте debugger или breakpoint
- ☐ Проверьте область видимости переменных
- ☐ Упростите код — удалите лишнее
- ☐ Погуглите текст ошибки
- ☐ Объясните код вслух (rubber duck debugging)
- ☐ Сделайте перерыв — свежий взгляд помогает!
📜 Резюме
В этой главе мы рассмотрели:
- ✅ Три типа ошибок: синтаксические, 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)
✅ Главный источник информацииОписание: Самая полная и достоверная документация по 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
Описание: Глубокое погружение в механизмы работы JavaScript. 6 книг, покрывающих все аспекты языка.
Темы:
- Область видимости и замыкания
- this и прототипы объектов
- Типы и грамматика
- Асинхронность и производительность
- ES6 и beyond
3.3. "JavaScript и jQuery" (Дэвид Макфарланд)
📚 Для начинающихАвтор: David Sawyer McFarland
Описание: Отличная книга для начинающих. Много примеров, иллюстраций и практических задач.
3.4. "Eloquent JavaScript" (Выразительный JavaScript)
📚 Бесплатная онлайн-книгаАвтор: Marijn Haverbeke
Описание: Современная книга о 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. Валидаторы и проверка
- W3C HTML Validator - проверка HTML
- CSS Validator - проверка CSS
- JSHint - проверка JavaScript
- Can I Use - поддержка браузерами
7.2. Генераторы
- CSS Gradient Generator
- Box Shadow Generator
- Snippet Generator
- RegExr - тестер регулярных выражений
- Regex101 - еще один тестер RegEx
Тестеры RegExp
✅ Онлайн-тестеры регулярных выражений- RegExr - с объяснениями
- Regex101 - подробная отладка
- RegEx Tester
📜 🎨 7.3. Дизайн-ресурсы
Изображения (бесплатные)
Иконки
- Font Awesome
- Flaticon - иконки
- Icons8
- Feather Icons
- Iconify
Удаление фона с изображений
Оптимизация изображений
- TinyPNG - сжатие изображений
- Squoosh - от Google
- Image Compressor
📜 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
💡 Не только CSSURL: 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
- JavaScript Cheat Sheet (MDN) — developer.mozilla.org
- ES6 Cheat Sheet — devhints.io/es6
- Modern JS Cheat Sheet — GitHub
12.2. Roadmaps
✅ Дорожная карта разработчикаОписание: Визуальная карта того, что нужно изучить 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- Изучите основы — переменные, типы данных, операторы, функции
- Практикуйтесь ежедневно — решайте задачи на Codewars/LeetCode
- Создавайте проекты — делайте реальные приложения (to-do, калькулятор, игры)
- Читайте чужой код — изучайте open source проекты на GitHub
- Углубляйтесь в сложные темы — замыкания, прототипы, async/await
- Изучите фреймворк — React, Vue или Angular
- Изучите бэкенд — Node.js, Express, базы данных
- Следите за новостями — подпишитесь на JavaScript Weekly
- Участвуйте в сообществах — Stack Overflow, Reddit, Discord
- Никогда не останавливайтесь — 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-лоадеров можно найти на сайте:
- loading.io/css - коллекция 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-preferenceprefers-reduced-motionЗапрос на уменьшение анимаций (accessibility) reduce,no-preferenceprefers-contrastПредпочтение по контрастности more,less,no-preference,customprefers-reduced-transparencyЗапрос на уменьшение прозрачности reduce,no-preferenceprefers-reduced-dataЗапрос на экономию трафика reduce,no-preferenceforced-colorsПринудительная цветовая схема (Windows High Contrast) active,noneinverted-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:
- Откройте DevTools (F12)
- Нажмите Cmd+Shift+P (Mac) или Ctrl+Shift+P (Windows)
- Введите "Rendering"
- В панели Rendering найдите "Emulate CSS media feature"
- Выберите нужную настройку (prefers-color-scheme, prefers-reduced-motion и др.)
В Firefox:
- about:config
- Найдите ui.prefersReducedMotion
- Установите значение 1 (reduce) или 0 (no-preference)
Поддержка браузерами
Feature Chrome Firefox Safari Edge prefers-color-scheme76+ 67+ 12.1+ 79+ prefers-reduced-motion74+ 63+ 10.1+ 79+ prefers-contrast96+ 101+ 14.1+ 96+ prefers-reduced-transparency118+ 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. Генераторы и инструменты
✅ Полезные онлайн-инструменты- Box Shadow Generator - генератор теней
- CSS Gradient - генератор градиентов
- CSS Gradient (альтернатива)
- UI Gradients - красивые готовые градиенты
- Ceaser - CSS timing function generator
- Min-Max Calculator - калькулятор clamp
- Can I Use - проверка поддержки браузерами
📜 Резюме
В этой главе мы рассмотрели:
- ✅ Способы центрирования элементов
- ✅ Работу с текстом (обрезка, перенос)
- ✅ Сохранение пропорций изображений
- ✅ 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));// 142.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'));// trueconsole.log(isValidEmail('invalid-email'));// false3.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'));// trueconsole.log(isValidPhone('8-999-123-45-67'));// true3.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(''));// trueconsole.log(isEmpty([]));// trueconsole.log(isEmpty({}));// trueconsole.log(isEmpty('text'));// false📜 4. Работа с массивами
4.1. Удаление дубликатов
// Способ 1: Setconst arr = [1, 2, 2, 3, 3, 4]; const unique = [...new Set(arr)]; console.log(unique);// [1, 2, 3, 4]// Способ 2: filterconst 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: URLSearchParamsconst 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));// true12.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);// true13.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.timeconsole.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 JSdocument.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: URLSearchParamsconst 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));// например, 154.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">×</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'); } });// Закрытие по Escapedocument.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;// Валидация emailconst 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">«</button> <button class="next">»</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. Полезные ссылки на генераторы
✅ Генераторы кода- Snippet Generator - генератор сниппетов для VS Code
- Box Shadow Generator
- CSS Gradient Generator
- Ceaser - CSS timing functions
- Loading.io - CSS лоадеры
- Anime.js - библиотека анимаций
📜 Резюме
В этой главе мы собрали:
- ✅ JavaScript сниппеты (debounce, throttle, копирование, DOM)
- ✅ CSS сниппеты (центрирование, текст, градиенты, эффекты)
- ✅ HTML сниппеты (базовая структура, изображения)
- ✅ Готовые функции (массивы, объекты, форматирование)
- ✅ Модальное окно (полная реализация)
- ✅ Валидация формы
- ✅ Простой слайдер
- ✅ Полезные генераторы
💡 Как использоватьВсе сниппеты можно копировать и использовать в своих проектах. Для удобства используйте генератор сниппетов для VS Code.
- Использование свойства elements соответствующей формы. Например: