JavaScript. Шпаргалка

Теория

Основы

Вставка на страницу

<script>const foo = 'bar';</script>
<script src="/a/js/main.js"></script>

Атрибут async. Браузер скачивает скрипт в фоновом режиме и запускает по завершению загрузки.

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

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

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

☝️🧐 При одновременном указании async и defer будет использован только async.

Вставка модулей на страницу

Модуль — это часть программы, которая содержит класс или библиотеку. Их можно собирать вместе с помощью специального инструмента — например, Webpack’а — либо вставлять на странице с атрибутом type="module".

<script src="/js/my-element.js" type="module"></script>

DOM

document.getElementById('my-el').innerHTML =
  'Hello World!';

Структура кода

  • Команда. Код JavaScript состоит из команд. Каждая команда описывает небольшую часть выполняемой операции, а весь набор команд определяет поведение страницы. Командой может быть присваивание переменной, условные конструкции. Команды состоят из ключевых слов, знаков присвоения и выражений, заканчиваются точкой с запятой.
  • Выражение (expression; конструкция). Это часть команды, производящее вычисление, сравнивание, операцию с текстом или объектом. Таким образом различают цифровые, строковые и логические выражения.

    В примерах выражения — это части команд после знаков присвоения и условие в команде if.

  • Операции. Выражения состоят из операций - записей действий, аналогичных математическим операциям. Например, 2 + 2.

Комментарии

// Однострочный

/* Многострочный. Lorem ipsum dolor, sit amet
consectetur adipisicing elit. Sed repellendus. */

/**
 * Многострочный JSDoc-комментарий функции
 *
 * @param {string} title
 * @param {number} width
 * @param {number} height
 */

Следует комментировать.

  • Общую архитектуру, вид «с высоты птичьего полёта».
  • Использование функций.
  • Неочевидные решения, важные детали.

При этом стараться писать код так, чтоб свести к минимуму объяснение работы кода. Стараться разбивать его на функции со значащими названиями и JSDoc-комментариями.

☝️🧐 Использовать FIXME:, чтобы описать проблему. TODO: - чтобы описать решение.

Строгий режим

Устанавливает жесткие правила кода. Если б в примере ниже не было директивы строгого режима, код сработал бы, но представлял угрозу сбоя для программы в целом. С директивой выполнение программы на третьей строке будет приостановлено, потому что переменная x не была объявлена до присвоения значения.

'use strict';
// Ошибка будет выведена в консоль
x = 1;

Не рекомендуется
// https://bit.ly/3h5O8KN
let result = eval(text);

// https://bit.ly/3HmuhC1
document.write('Hello');

// https://bit.ly/3IcrtZ8
var foo = 'bar';

/* Различные способы обратной связи.
Профессионалы используют только на этапах
разработки и в прототипах */
console.log(a);
alert(a);
confirm('Really?');
prompt('Your age?', '0');
Типы данных
// Число
const age = 18;

// Строка
const name = 'Jane';

// Логический (булевый) тип
let truth = false;

let a;

// undefined
truth = typeof a;

// null
a = null;

/* Символ — примитивный тип данных, который
может быть использован, как ключ для свойства
объекта */
let id = Symbol();

// Массив
const sheets = ['HTML', 'CSS', 'JS'];

// Объект
const fullName = { first: 'Jane', last: 'Doe' };

Преобразование типов

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

В первом примере строка ‘11’ будет преобразована в число и условие вернет true. Поэтому для сравнения рекомендуется использовать «тройное равно» — оно не допускает преобразования, типы сравниваются.

if (11 == '11')
if (77 == 'vanilla') // false
if (1 == true) // true
if ('1' == true) // true
if (undefined == null) // true
if (1 == '') // false

При попытке сложения числа со строкой JavaScript выполнит строковое преобразование — преобразует число в строку — и выполняет конкатенацию. Поэтому лучше использовать строковый литерал `2 + ${a}`.

Чтобы преобразовать строку в число и сложить ее с другим числом, используют встроенные функции Number или parseInt.

Функция Number получает аргумент, и если возможно, преобразует его в число. Если преобразовать аргумент в число не удается, Number возвращает NaN. Результаты примеров: 7 и 4.

const num = 3 + Number('4');

const inputValue = '4';
const val = Number(inputValue);

При конкатенации булевого значения со строкой создается строка. Результат примера: true love.

const stringPlusBoolean = true + 'love';

⛔️ Следующие типы в условных операторах всегда возвращают ложь. Впрочем, использовать прямое сравнение по типу не рекомендуется. Исключение составляет NaN (это, строго говоря и не тип — Non a Number).

if (undefined) {
  // code
} else if (null) {
  // code
} else if ('') {
  // code
} else if (NaN) {
  // code
} else if (0) {
  // code
}

Тип данных undefined и объект null

JS возвращает

  • null когда объект ещё не создан или не найден;
  • undefined — переменной не присвоено значение либо отсутствуют свойство объекта или элемент массива.

Значения undefined и null равны, а типы данных разные.

☝️🧐 JavaScript-программисты привыкли к тому, что null является объектом (null === 'object'; // true), хотя фактически null — примитивный тип данных, со значением, равным значению undefined. Лично Брэндан Айк, создатель языка, признает что это баг. И он, скорее всего, никогда не будет исправлен из-за необходимости сохранения обратной совместимости существующего кода с новыми версиями языка.

if (typeof undefined) // undefined
if (typeof null) // object

if (null === undefined) // false
if (null == undefined) // true
Переменные

В современном JS переменные объявляются с помощью слов const и let.

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

Значение let может быть переопределено в рамках блока. Значение const нет.

// Переменные
let a;

// Сохраняем в переменную строку
a = 'init';

// Меняем значение на цифру
a = 4;

/* Преобразование типов. В `с` будет сохранена
не цифра, а строка '33' */
const b = `${1 + 2}3`;

// Булево значение
const c = false;

// Массив
const d = [2, 3, 5, 8];

// Объект
const e = { title: 'Hello', subtitle: 'world' };

/* Функциональное выражение: в переменную
сохраняется функция */
const f = () => {};

// Объект регулярного выражения
const g = /()/;

// Константа
const PI = 3.14;

Ключевое слово const может ввести в заблуждение. Оно значит не константу значения (как число Пи), а константу ссылки на значение.

Это значит следующее.

  • Мы не можем переопределить значение const, в том числе сохранив в нее новый массив или объект.
  • Но можем изменить элементы внутри массива или свойства объекта, сохраненных в const.

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

Глобальные. Если переменная объявлена вне блоков кода, то считается, что она определена в глобальной области видимости (scope).

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

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

☝️🧐 Глобальные переменные живут до закрытия браузера или вкладки, но доступны для новых страниц открытых в той же вкладке.

☝️🧐 Область видимости глобальной переменной, созданная с помощью var распространяется на встроенный объект window. А область видимости глобальной переменной, созданная с помощью let нет.

👍 Хороший стиль программирования основан на использовании локальных переменных, если только нет крайне необходимости в глобальных. Загромождение глобальной области видимости повышает риск ошибочного использования переменных.

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

⚠️ Переменная, определенная в функции без слова const, let или var будет считается глобальной.

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

Термины

Объявление (declaring). Переменная объявлена, значение не присвоено. Полезно для создания глобальной переменной результат которой присваивается в функции или условном выражении.

🚫 const-переменную нельзя просто объявить, ее нужно сразу инициализировать.

let name;

// ⛔️ Так делать нельзя - JS вернет ошибку.
const name;

Инициализация. Переменная объявлена, ей присвоено значение. В примере - пустая строка и value.

const firstName = '';
const secondName = 'value';

☝️🧐 Переменные в JS — динамические. Это значит, что они могут содержать любой тип данных. И данные в переменной могут заменяться данными любого другого типа: строка — числом, число — булевым значением и т.д.

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

☝️🧐 JS «поднимает» выше момента использования (hoist) только var-переменные. Let- и const-переменные не поднимает.

☝️🧐 JS поднимает только объявление переменных. А инициализацию нет. Например, из записанной где-то внизу команды var x = 5; JS поднимет вверх только var x, а = 5 исполнит в порядке очереди.

☝️🧐 Директива use strict запрещает hoisting.

Операторы

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

let a;
let b;

// Присвоение (знак равенства), сложение и вычитание
a = 2 + 3 - 4;

// Умножение, деление и группировка (в скобках)
a = 2 * (a / 4);

// Возведение в степень
a = 3 ** 2;

// Взятие остатка (modulo): 100 / 48; остаток = 4
a = 100 % 48;

/* Увеличение / уменьшение / умножение и деление
на то же значение, что и после оператора */
a += 6; //То же самое, что a = a + 6
a += b; // То же самое, что и a = a + b
a -= 6; // Ну и так далее…
a *= 6;
a /= 6;
a %= 6;

/* Тот же оператор рекомендуется использовать
вместо инкремента и декремента `a++, a--` */
a += 1;
a -= 1;

Плюс также используется для перевода неопределенного типа данных в цифры. А в ранних версиях — еще и для конкатенации.

guess = prompt(clue);
guess = +guess;

Сравнение

Эти операторы используются в условиях. ⚠️ Работают только с примитивами; массивы и объекты не могут быть сравнены.

  • === — равно (значение и тип 👍)
  • !== — не равно (значение и тип 👍)
  • == — равно (только значение 👎)
  • != — не равно (только значение 👎)
  • > — больше, чем
  • < — меньше, чем
  • >= — больше или равно
  • <= — меньше или равно
  • ? — тернарный оператор — см. «Условные выражения»

Сравнивать можно не только цифры, но и строки. Строки сравниваются побуквенно: по порядковым номерам в алфавите. ‘А’ меньше чем ‘Я’. Код у строчной буквы обычно больше, чем у прописной.

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

Поэтому для сравнения рекомендуется использовать «тройное равно» (или «строгое равенство», или «идентично», или «оператор тождественности»). Оператор защитит от ошибок «двойного равно». Два значения считаются строго равными только в том случае, если они относятся к одному типу.

Логические операторы

  • && — И
  • || — ИЛИ
  • ! — НЕ

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

  • typeof — тип переменной
  • instanceof возвращает true, если объект является экземпляром объекта, указанного в параметре

Прежде чем проводить над какой-то переменной операции, надо узнать тип данных, которые в ней содержатся: число, строка или объект. Делается это с помощью оператора typeof, который возвращает тип в виде строки: typeof (someVariable).

☝️🧐 Переменная в операторе может быть записана в скобки — typeof (x) — но предпочтительным считается удобочитаемый вариант без скобок.

if (typeof x !== 'number') { /* … */ }

typeof 'foo'; // 'string'
typeof 44;    // 'number'
typeof 0;     // 'number'
typeof NaN;   // 'number'
typeof true;  // 'boolean'
typeof {};    // 'object'
typeof [];    // 'object'
typeof Date;  // 'object'

Псевдоложные значения

В JavaScript существуют пять псевдоложных значений:

undefined,\ null,\ 0,\ "" (пустая строка)\ и false.

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

const yourName = ' ';

if (yourName) { // true
  console.log('Guess you lied about your name');
}

Побитовые операторы

Интерпретируют операнды как последовательность из 32 битов (нулей и единиц). Подробнее.

  Пример == Итог Дес.  
& И 5 & 1 0101 & 0001 0001 1
| ИЛИ 5 | 1 0101 | 0001 0101 5
~ НЕ ~ 5 ~0101 1010 10
^ исключающее ИЛИ 5 ^ 1 0101 ^ 0001 0100 4
<< Левый сдвиг 5 << 1 0101 << 1 1010 10
>> Правый сдвиг 5 >> 1 0101 >> 1 0010 2
>>> Правый сдвиг с заполнением нулями 5 >>> 1 0101 >>> 1 0010 2
Условные выражения

if … else

// Переменные в примерах
let age;
let eligible;

if (age >= 14 && age < 19) {
  eligible = true;

// Необязательный блок
} else if (age > 50) {
  console.log('стар, суперстар');

// Необязательный блок
} else {
  eligible = false;
}

См. также дополнительные материалы по «if...else».

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

// Переменная в примере
let text;

// В качестве условия получаем текущую дату
switch (new Date().getDay()) {

  // if (day == 6)
  case 6:
    text = 'Суб';
    break;

  // if (day == 0)
  case 0:
    text = 'Вск';
    break;

  // else
  default:
    text = 'Тоска';
}

См. также дополнительные материалы по «switch».

Тернарный оператор / тернарная условная операция

Сокращенная версия if…else:

условие ? выражениеЕслиУсловиеВерно : иВыражениеЕслиНет;

Как правило, тернарная операция не используется одна — только после объявления переменной, которой присваивается значение, в зависимости от результатов проверки условия.

const voteable = (age < 18) ? 'Too young' : 'Old enough';

☝️🧐 Рекомендация Airbnb: не использовать вложенные тернарные операции.

// ⛔️ плохо
const foo = maybe1 > maybe2
  ? 'bar'
  : value1 > value2 ? 'baz' : null;

// 👍Лучше использовать `if … else if… else`
let foo;

if (maybe1 > maybe2 ) {
  foo = 'bar';
} else if (value1 > value2) {
  foo = 'baz';
} else {
  foo = null;
}

☝️🧐 Рекомендация Airbnb: избегать ненужных тернарных операторов.

// плохо
const foo4 = a ? a : b;
const bar = c ? true : false;
const baz = c ? false : true;

// хорошо
const foo5 = a || b;
const bar2 = !!c;
const baz2 = !c;

Еще одна альтернатива if…elseусловие && выражениеЕслиВерно

// Если переменные `foo` и `bar` равны, радуемся
foo === bar && console.log('Huzzah!');

Или так.

/* Если переменные `foo` и `bar` равны, радуемся.
В противном случае — вздыхаем */
(foo === bar &&
  console.log('Huzzah!')) ||
  console.log('Doh!');
События

Обработчики события

Обработчик события может быть назначен прямо в разметке, в атрибуте, который называется on-событие.

<button type="button" onclick="myFunc()">
  Нажми меня
</button>

Можно назначать обработчик из скрипта, через свойство DOM-объекта.

<button id="elem" type="button">Нажми меня</button>
<script>
  elem.onclick = () => {
    alert('Спасибо');
  };
</script>

Фундаментальный недостаток описанных выше способов — невозможность повесить несколько обработчиков на одно событие.

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

function handler() {
  alert( 'Спасибо!' );
}

elem.addEventListener('click', handler);
elem.removeEventListener('click', handler);

Аргумент для обработчика — объект события

Каждый обработчик события на вход принимает объект события event. Он содержит информацию о том, какое именно событие наступило, и подробности. В частности, использовав свойство этого объекта event.target, можно обратиться к элементу, на котором сработало событие. А свойство event.currentTarget возвращает элемент на котором установлен слушатель.

То есть, если пользователь кликнет на потомке кнопки, описанной в примере, — span или иконку — event.currentTarget всё равно будет содержать объект btn.

`btn.addEventListener('click', (event) => {
  const el = event.currentTarget;
  console.log(`You've clicked on ${el}`);
});

Распространенные события

  • click — клик или прикосновение на элементе страницы.
  • focus — поле ввода попало в фокус.
  • blur — поле ввода пропало из фокуса.
  • input — ввод в поле: нажатие клавиши, вставка из буфера, перетаскивание.
  • submit — отправка формы.
  • load — завершении загрузки страницы браузером.
  • unload — закрытие окна браузера или уход со страницы.
  • resize — изменение размера окна браузера.
  • contextmenu – пользователь кликнул на элемент правой кнопкой мыши.
  • touchstart — прикосновение к элементам на устройствах с сенсорными экранами.
  • touchend — завершении прикосновения.
  • mouseover — наведение курсора на элемент.
  • mousemove — перемещение курсора над элементом.
  • mouseout — выход курсора за границы элемента.
  • dragstart — Начало перетаскивания элемента на странице.
  • drop — Перетаскиваемый элемент отпущен.
  • keypress — нажатие клавиши.
  • transitionend — завершение CSS-перехода.
  • play — нажатие кнопки воспроизведения элемента <video>.
  • pause — нажатии кнопки паузы.

События мыши и клавиатуры

  • dblclick,
  • keydown,
  • keyup,
  • mousedown,
  • mouseenter,
  • mouseleave,
  • mouseup

События объекта window

  • scroll,
  • abort,
  • beforeunload,
  • error,
  • hashchange,
  • pageshow,
  • pagehide

События формы

  • change,
  • focusin,
  • focusout,
  • invalid,
  • reset,
  • search,
  • select

События анимации

  • animationend,
  • animationiteration,
  • animationstart

События перетаскивания

  • drag,
  • dragend,
  • dragenter,
  • dragleave,
  • dragover

События буфера обмена

  • copy,
  • cut,
  • paste

События медиафайлов

  • abort,
  • canplay,
  • canplaythrough,
  • durationchange,
  • ended,
  • error,
  • loadeddata,
  • loadedmetadata,
  • loadstart,
  • pause,
  • playing,
  • progress,
  • ratechange,
  • seeked,
  • seeking,
  • stalled,
  • suspend,
  • timeupdate,
  • volumechange,
  • waiting

Прочие

  • message,
  • mousewheel,
  • online,
  • offline,
  • popstate,
  • show,
  • storage,
  • toggle,
  • wheel,
  • touchcancel,
  • touchmove

Хронометражные события (события таймеров)

Создаются вызовами setTimeout или setInterval.

setTimeout(() => {
  alert('Hello');
}, 3000);

События конкретных API

Например JavaScript API, относящиеся к Geolocation, LocalStorage, Web Workers и т. д. Пример — определение координат пользователя по событию JavaScript API.

Циклы

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

метод forEach()

Выполняет указанную функцию один раз для каждого элемента в массиве или DOM-коллекции.

const emotions = ['happy', 'sad', 'angry'];
emotions.forEach((emotion) => console.log(emotion));

for

let sum = 0;
const a = [10, 20, 30, 40];

// Используем свойство массива length…
for (let i = 0; i < a.length; i++) {
  sum += a[i];
  console.log(sum);
}

// Можно явно указать число итераций (5)
for (let i = 0; i < 5; i++) {
  // ...
}

См. также дополнительные материалы по «for».

for...of

Перебирает фактически любые итерируемые объекты (включая строки, массивы, наборы узлов, карты, объект аргументов и подобные).

const language = 'JavaScript';
let text = '';

for (const x of language) {
  text += x;
  console.log(text);
}

while

Выполняется если и пока условие верно.

☝️🧐 Обычно используется только в тех случаях, когда неизвестно количество итераций. При известном количестве применяется цикл for.

let i = 1;

while (i < 100) {
  /* ☝️🧐 Счетчик нужно обновлять в теле кода. Если
  этого не сделать цикл станет бесконечным. */
  i += 1
  console.log(`${i}, `);
}

do...while

Выполняется минимум один раз и затем пока условие верно.

let j = 1;

do {
  j *= 2;
  console.log(`${j}, `);
} while (j < 100);

for...in

⛔️ Также перебирает итерируемые объекты, но проходит по свойствам в произвольном порядке, поэтому не рекомендуется — безопаснее использовать какой-нибудь другой цикл.

const numbers = [45, 4, 9, 16, 25];
let txt = '';
for (const x in numbers) {
  txt += numbers[x];
  console.log(txt);
}

break

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

for (let x = 0; x < 10; x++) {
  if (x === 5) {
    // Остановиться и выйти из цикла
    break;
  }
  console.log(`${x}, `);
}

continue

⛔️ Не рекомендуется. Лучше использовать условие для подходящих элементов, а не для исключений. В примере это было бы if (y !== 5) { console.log(${y}, ); }.

for (let y = 0; y < 10; y++) {
  if (y === 5) {
    /* Пропустить и продолжить
    со следующим элементом */
    continue;
  }
  console.log(`${y}, `);
}
Функции

Функция — это блок кода (логических и математических операций) для многократного использования с уникальным именем и параметрами — собственными переменными для входных данных.

Из программы в функцию передаются аргументы (значения параметров). Можно сказать, что аргумент передается не просто в функцию, но в параметр. Внутри тела функции параметры играют роль локальных переменных, а аргументы — их значений. В Си таких различий не делают: там всё, что используется для передачи входных данных в функцию, называется аргументами.

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

Существуют функции и без команды return — меняющие DOM, выводящие что-то в консоль. Если попробовать сохранить их вызов в переменную, то значением переменной станет undefined.

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

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

☝️🧐 Функции, созданные таким образом, поднимаются в коде (hoist), и к ним можно обратиться до объявления.

function addNumbers(a, b) {
  return a + b;
}

Функциональное выражение и стрелочные функции

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

const multiplyNumbers = (a, b) => {
  return a * b;
};

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

Вызов функций

const x = addNumbers(1, 2);
const y = multiplyNumbers(3, 4);
saySmthInspiring('peace');

☝️🧐  Обращение к функции без круглых скобок приведет к получению ее кода, а не к вызову. В примере в переменную y будет сохранен код функции myFunc.

const foo = myFunc;

Подробнее о стрелочных функциях см. здесь

Контекст и this

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

const user = {
  firstName: 'Вася',
};

function func() {
  alert(this.firstName);
}

const funcUser = func.bind(user);
funcUser(); // Вася
Строки
let abc = 'abcdefghijklmnopqrstuvwxyz';

// \n = с новой строки
const esc = 'I don’t \n know';

// длина строки
const len = abc.length;

// В верхний регистр
abc.toUpperCase();

// В нижний
abc.toLowerCase();

/* Разделить строку по фрагментам,
заканчивающимся запятой; сохранить в массив */
abc.split(',');

// Разделить строку по символам, сохранить в массив
abc.split('');

// abc + ' ' + esc
abc.concat(' ', esc);

/* Получить последовательность символов.
В примере — с 4-го (отчет начинается с 0)
до 6-го (единица на второй границе
не прибавляется). В примере получим `def` */
abc.slice(3, 6);

// Найти и заменить. Можно использовать «регулярки»
abc.replace('abc', '123');

/* `indexOf()` возвращает индекс первого символа
первого совпадения поискового запроса: в примере —
индекс `l` в подстроке `lmno` в строке
`abcdefghijklmnopqrstuvwxyz`. Если ничего не найдено,
метод возвращает `-1` */
abc.indexOf('lmno');

/* возвращает индекс первого символа
последнего совпадения поискового запроса */
abc.lastIndexOf('lmno');

// Символ по указанном индексу. В примере — `c`
abc.charAt(2);

// Код символа. В примере — 99 (код `c`)
abc.charCodeAt(2);

abc = 24;
/* Перевести число в др. систему счисления.
В примере — шестнадцатеричную */
abc.toString(16);

// Удалить пробелы в начале и конце строки
const text = ' Hello World!';
const result = text.trim();

Другие методы.

Числа и методы объекта `Math`
let num;
const pi = 3.141;

/* Метод округляет десятичные дроби до указанного
в параметре количества. В примере вернет 3 */
pi.toFixed(0);

/* 3.14. С параметром 2 метод используют
для расчета денежных сумм с мелочью */
pi.toFixed(2);

/* Сокращает число до указанного количества
знаков. Если в исходном числе знаков меньше,
то добавляет нули в десятичных дробях.
В примере вернет 3.1 */
pi.toPrecision(2); // returns 3.1

/* Общий для Object и всех потомков метод
возвращает примитивное значение указанного
объекта. В случае с числом Пи до третьего
знака, сохраненном в переменную, — 3.141.
Но именно, как число, а не объект со свойствами
и методами. Используется редко, так как JS
автоматически вызывает метод при обнаружении
объекта, когда ожидается примитивное значение. */
pi.valueOf();

/* Явное преобразование к числу. В примере
булево значение будет преобразовано в 1. */
Number(true);

// Время в миллисекундах с 1 января 1970-го
Number(new Date());

/* Возвращает первое целое число. В примере — это 3.
Вторым параметром указывается основание системы
счисления (radix) */
parseInt('3 months', 10);

// Возвращает число с плавающей запятой — 3.5
parseFloat('3.5 days');

/* Самое большое число, доступное в JS-коде:
1.7976931348623157e+308 */
num = Number.MAX_VALUE;

// Самое маленькое число: 5e-324
num = Number.MIN_VALUE;

// Вернёт -Infinity
num = Number.NEGATIVE_INFINITY;

// Infinity
num = Number.POSITIVE_INFINITY;

Объект Math

// 3.141592653589793
const pi = Math.PI;

// Округление — 4
Math.round(4.4);

// Округление — 5
Math.round(4.5);

// Округление в большую сторону — 4
Math.ceil(3.14);

// Округление в меньшую сторону — 3
Math.floor(3.99);

// Случайное число от 0 до 1
Math.random();

// Случайное целое число от 1 до 5
num = Math.floor(Math.random() * 5) + 1;

// Выбор минимального числа — минус 2
Math.min(0, 3, -2, 2);

// Выбор максимального числа — 3
Math.max(0, 3, -2, 2);

// Квадратный корень — 7
Math.sqrt(49);

// Абсолютная величина — 3.14
Math.abs(-3.14);

// Синус — 0
Math.sin(0);

/* Косинус. Другие методы тригонометрических
функций: tan, atan, asin, acos */
Math.cos(Math.PI);

// Натуральный логарифм — 0
Math.log(1);

/* Метод возвращает значение выражения ex,
где x — аргумент метода, а e — число Эйлера,
основание натурального логарифма.
Результат примера — 2.718281828459045 */
Math.exp(1);

/* Возведение в степень. Не рекомендуется. Вместо
этого метода следует использовать оператор `**` */
Math.pow(2, 8); // = 256 - 2 to the power of 8

Константы объекта Math

Вызов — через точечную нотацию — например, Math.PI.

PI, SQRT2, SQRT1_2, E, LN2, LN10, LOG2E, Log10E.

Массивы

Создание

let dogs = ['German Shepherd', 'Pug', 'Labrador'];
dogs = ['Poodle', 'Beagle', 'Labrador'];

Доступ к элементам, изменения

// Доступ ко второму объекту, первый - [0]
console.log(dogs[1]);

// Замена первого элемента
dogs[0] = 'Bulldog';

Перебор

for (let i = 0; i < dogs.length; i++) {
  console.log(dogs[i]);
}

Методы

  • dogs.splice(2, 0, 'Pug', 'Boxer') — «перочинный нож» массива, добавление и удаление/замена элементов; параметры: «куда вставить», «сколько элементов удалить/заменить», список добавляемых элементов.
  • dogs.forEach(() => {/**/}) выполняет указанную функцию один раз для каждого элемента в массив.
  • dogs.join(' * ') объединяет все элементы массива (или массивоподобного объекта) в строку, с указанным разделителем: ‘Bulldog * Beagle * Labrador’.
  • dogs.toString() преобразовывают в строку: ‘Bulldog,Beagle,Labrador’.
  • dogs.push('Chihuahua') добавляет элемент на последнее место в массиве.
  • dogs[dogs.length] = 'Chihuahua' — альтернатива push‘у.
  • dogs.unshift("Chihuahua") добавляет элемент на первое место в массиве.
  • dogs.shift() удаляет первый элемент.
  • dogs.pop() удаляет последний элемент.
  • const animals = dogs.concat(cats, birds) объединяет массивы, добавляя в конец массива, на котором вызван метод, те, что переданы в параметры.
  • dogs.slice(1,3) возвращает новый массив — копию участка исходного; параметры — индексы начала и конца (исключительно) копии; в примере — со второго по третий.
  • dogs.sort() сортирует по алфавиту.
  • dogs.reverse() сортирует по алфавиту в обратном порядке.
  • x.sort(function(a, b) { return a - b; }) — сортировка чисел.
  • x.sort(function(a, b) { return b - a; }) — сортировка чисел в обратном порядке.
  • const highest = x[0] — максимальное значение из сортированного в предыдущем примере массива x.
  • x.sort(function(a, b) { return 0.5 - Math.random(); }) — сортировка в обратном порядке.
  • map(callback) создаёт новый массив, который будет состоять из результатов вызова функции обратного вызова для каждого элемента.
  • dogs.at(-2) вовзвращает элемент массива по указанному индексу; бесполезная альтернатива синтаксису квадратных скобок, если использовать положительный индекс, но удобная вещь, если нужно использовать отрицательный индекс — получить какой-то элемент с конца.

Пример использования map(callback)

const names = ['HTML', 'CSS', 'JavaScript'];

const nameLengths = names.map((name) => {
  return name.length;
});

// получили массив с длинами: 4,3,10
alert(nameLengths);

Прочие

copyWithin, delete (не рекомендуется), every, fill, filter, find, findIndex, indexOf, isArray, lastIndexOf, map, reduce, reduceRight, some, valueOf

Объекты
const student = {
  // Свойства
  firstName: 'Jane',
  lastName: 'Doe',
  age: 18,
  height: 170,

  // Метод
  fullName() {
    return `${this.firstName} ${this.lastName}`;
  },
};

/* Переопределение значения свойства и точечная
нотация — обращение к свойству через точку */
student.age = 19;
student[age] += 1;

// Вызов метода объекта
fullName = student.fullName();

DOM-коллекции

В JS всё является объектом. В том числе — так называемые DOM-коллекции. Это объекты (псевдомассивы), представляющие список узлов (DOM-элементов).

  • HTMLCollection — коллекция HTML-элементов;
  • NodeList — коллекция DOM-узлов (кроме HTML-элементов, это текстовые узлы, комментарии и др.), возвращаемая такими это коллекция узлов, возвращаемая такими методами, как Node.childNodes и document.querySelectorAll.

DOM-коллекции бывают:

  • динамическими — реагируют на изменение DOM;
  • статическими — моментальный снимок данных, не реагируют на изменение DOM.

Вид коллекции зависит от способа, с помощью которого она получена: querySelector() и querySelectorAll().

Несмотря на то, что NodeList не является массивом, его вполне возможно перебрать при помощи метода forEach(). NodeList также можно конвертировать в массив при помощи Array.from().

Классы
Регулярные выражения
const a = str.search(/CheatSheet/i);

Регулярные выражения — это шаблоны расширенного поиска текста. В JavaScript регулярные выражения являются объектами — со свойствами и методами, наследуемыми в основном из Function и String: match, replace, apply, call, toString и остальные.

☝️🧐 В частности, регулярные выражения в JS обычно используются вместе с методами объекта String: match (поиск) и replace.

☝️🧐 В регулярках имеют значения все символы — включая пробелы. Так что нельзя использовать пробелы для удобочитаемости.

«Флаги»

  • g — найти все совпадения.
  • m — поиск в многострочном тексте — с переносами строк и абзацами.
  • i — регистр букв не имеет значения; — найти все совпадения.

Cпециальные символы, символьные классы, квантификаторы и т. д.

  • \ — Экранирующий символ.
  • . — Любой символ.
  • \d — Число.
  • \s — Пробел.
  • \b — На границе слова: в начале или конце.
  • n+ — Как минимум один символ; в примере — n.
  • n* — Указанный символ может быть пропущен или быть набранным сколько угодно раз подряд.
  • n? — Пропуск или одно совпадение.
  • $ — В конце строки.
  • ^ — В начале строки. Символ каретки, циркумфлекс вместе со знаком доллара называют «якорями».
  • (abc) — Группа (скобочная группа). Также, как в арифметике, группа обрабатывается как единое целое, имеет приоритет операций, а также может быть квантифицирована.
  • (x|y|z) — Логическое ИЛИ. В примере — a или b.
  • [abc] — Только один из перечисленных символов. Набор.
  • [0-9] — Указанный диапазон цифр.
  • [^abc] — Исключение или исключающий диапазон.
  • a{2} — Количество повторений предыдущего символьного класса, набора или группы. Самый простой квантификатор. В примере — две строчных буквы a.
  • a{2,} — Две или больше a.
  • a{,5} — До пяти a.
  • a{2,5} — От двух до пяти a.
  • a{2,5}? — От двух до пяти a в ленивом режиме — одиночное совпадение.
  • [:punct:] — Знак препинания.
  • [:space:] — Пробельный символ. То же, что \s.
  • \u0057 — Символ Юникода. В примере — знак евро, €.

Два способа записи

Регулярки можно записать с помощью слешей…

let regexp = /go+gle/gmi

…или с помощью конструктора.

// параметры заключаются в кавычки
regexp = new RegExp('ab+c', 'i');

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

Жадная и ленивая квантификация

У квантификаторов есть два режима работы.

  • Жадный. По умолчанию движок регулярного выражения пытается повторить квантификатор максимальное количество раз, сколько это возможно. Например, \d+ получит все возможные цифры. И только потом ищет по оставшейся части шаблона, проверяя строку в обратном направлении, с конца.

    Таким образом, жадным (англ. greedy) квантификаторам в регулярных выражениях соответствует максимально длинная строка. И обычно, это не то, что нужно. Например, можно ожидать, что выражение (<.*>) найдёт в тексте теги HTML. Однако если в тексте есть более одного HTML-тега, то этому выражению соответствует строка, содержащая все теги. Например.

      <p><b>Википедия</b> — свободная энциклопедия, в которой <i>каждый</i> может изменить или дополнить любую статью.</p>
    
  • Ленивый. Проблему корректного поиска по части шаблона, следующей после квантификатора, можно решить с помощью знака вопроса после квантификатора. Включается ленивый режим: движок пытается найти совпадение по шаблону после ? посимвольно. Если вернуться к примеру с тегами: нашёл любой символ, проверил, нет ли за ним закрывающей угловой скобки, повторил.

Скобочные группы

  • Скобочной группе можно дать имя с помощью конструкции ?<имя>, добавляемой сразу после открытия скобки. Именованные группы сохраняются в свойство groups результата String.match.

      let dateRegexp
        = /(?<yy>[0-9]{4})-(?<mm>[0-9]{2})-(?<dd>[0-9]{2})/;
      let str = "2019-04-30";
      let groups = str.match(dateRegexp).groups;
      alert(groups.yy); // 2019
      alert(groups.mm); // 04
      alert(groups.dd); // 30
    
  • В массив результатов записывается сначала полое совпадение (индекс [0]). Затем — совпадения, соответствующие скобочным группам.
  • Метод String.match возвращает скобочные группы только без флага g. А метод String.matchAll — всегда.
  • Для ссылки на скобочную группу в шаблоне поиска используют обратные ссылки: \1, \2 и т.д. — по порядку групп в шаблоне. Или именные — \k<имя>.
  • Для ссылки в методе замены String.replace() синтаксис меняется — вместо косой черты и буквы k используется знак вопроса: $1, $2, $<имя>.
  • Можно исключить скобочную группу из результатов, добавив после открывающей скобки знак вопроса и двоеточие — ?:. Это используется, если группа создана только для квантификации.

Катастрофический возврат

Есть выражения, которые могут «подвесить» интерпретатор JavaScript с потреблением 100% процессора. В браузере движок перестанет реагировать на другие события и понадобится перезагрузить страницу. Происходит это тогда, когда шаблон недостаточно конкретен и интерпретатору приходится перебирать миллионы вариантов.

Решить проблему помогает опережающая проверка.

JSON

JSON — это текстовой формат для хранения и передачи данных, записываемых по правилам JS-объекта. Пары ключ/значение, массивы в квадратных скобках, объекты в фигурных скобках.

Однако, в отличие от JS, в котором «ключи» могут быть строками, идентификаторами без кавычек и числами, в JSON «ключи» должны быть только строками и записываться в двойных кавычках.

Пример записи объекта в JSON.

const str = `{ "names":
  [
    {"first": "Hakuna", "last": "Matata" }
    {"first": "Jane", "last": "Doe" }
    {"first": "Air", "last": "Jordan"}
  ]
}`;

Парсинг данных, полученных с сервера

Сервер передает данные всегда в виде строки. Для превращения их в JS-объект используется метод JSON.parse.

const obj = JSON.parse(str);
// Доступ к данным объекта
console.log(obj.names[1].first);

Отправка данных на сервер

// Создать объект
const myObj = { name: 'Jane', age: 18, city: 'Chicago' };

// Перевести его в JSON
const myJSON = JSON.stringify(myObj);

// Передать JSON PHP-скрипту
window.location = `demo.php?x=${myJSON}`;

Хранение данных в браузере. Доступ к этим данным

// Создать объект
const myObj = { name: 'Jane', age: 18, city: 'Chicago' };

// Перевести его в JSON
const myJSON = JSON.stringify(myObj);

/* Сохранить myJSON в объект веб-хранилища localStorage
под именем `testJSON` */
localStorage.setItem('testJSON', myJSON);

// Получить `testJSON` из объекта localStorage
const text = localStorage.getItem('testJSON');

// Парсинг данных
const obj = JSON.parse(text);

// Использование
console.log(obj.name);
Дата
/* Вызов конструктора без параметров возвращает
объект даты— миллисекунды c 1 января 1970г. */
let d = new Date();

/* Преобразование в число. Результат на момент
написания — 1647003979580 */
d= Number(d);

// Создание объекта с определенной датой
d = Date('2022-06-23');

// Без месяца и дня, устанавливается 1 января
d = Date('2022');

// Объект с датой и временем
d = Date('2022-06-23T12:00:00-09:45');

// Можно записывать дату так
d = Date('June 23 2022');

// Можно указывать часовой пояс
d = Date('Jun 23 2022 07:45:00 GMT+0100 (Tokyo Time)');

Получить дату и время

d = new Date();

// Получить день недели
const a = d.getDay();
  • getFullYear(); — год, четыре цифры.
  • getMonth(); — месяц: 0-11.
  • getDate(); — день месяца: 1-31.
  • getDay(); — день недели: 0-6.
  • getHours(); — час: 0-23.
  • getMinutes(); — минуты: 0-59.
  • getSeconds(); — секунды: 0-59.
  • getMilliseconds(); — миллисекунды: 0-999.
  • getTime(); — миллисекунды с 1 января 1970.

Установить дату и время

d = new Date();

// Добавить неделю к дате
d.setDate(d.getDate() + 7);
  • setFullYear(); — год, опционально — месяц и день.
  • setMonth(); — месяц: 0-11.
  • setDate(); — день месяца: 1-31.
  • setHours(); — час: 0-23.
  • setMinutes(); — минуты: 0-59.
  • setSeconds(); — секунды: 0-59.
  • setMilliseconds(); — миллисекунды: 0-999.
  • setTime(); — миллисекунды с 1 января 1970.
Глобальные функции и объекты
// Явное преобразование к строке
String(23);
(23).toString();

// Явное преобразование к числу
Number('23');

/* Кодирует строку по правилам универсального
идентификатора ресурса (URI), замещая всё, что не
является цифрами, латинскими буквами и символами,
разделяющие URL на компоненты (слеши, знаки вопросов,
«решетки») управляющими последовательностями,
представляющими UTF-8 кодировку символа. Например,
пробел кодируется в `%20`, или `Б` — в `%D0%91` */
encodeURI('http://site.com:8080/abc?efg#hij');

/* Кодирует строку по правилам URI, заменяя, помимо
прочего слеши, знаки вопросов, «решетки», двоеточия.
Используется, когда надо закодировать компоненты URL
в которых есть перечисленные символы-разделители */
encodeURIComponent('hello world');

// Декодирует URI
decodeURI('hello%20world');

// Декодирует компонент URI
decodeURIComponent(enc);

/* Является ли число конечным, т.е. не
`Infinity`, `-Infinity`, или `NaN` */
isFinite();

/* Если математическая операция не может быть
совершена, то возвращается специальное значение
`NaN` (Not-A-Number). `NaN` используется для
обозначения математической ошибки. Например,
деление `0/0` в математическом смысле не определено,
поэтому его результат `NaN`. Тоже самое получится,
если попробовать умножить строку на число.
Функция isNaN проверяет полученное значение на `NaN` */
isNaN();

/* Возвращает первое целое число. В примере — это 3.
Вторым параметром указывается основание системы
счисления (radix) */
parseInt('3 months', 10);

// Возвращает число с плавающей запятой — 3.5
parseFloat('3.5 days');

/* ⛔️ Выполняет строку, как код. Создает возможности
для злоумышленников и в прикладных программах
не используется */
eval();
Промисы

Промис (promise; термин не переводится) – это специальный объект, который связывает «создающий» и «потребляющий» код.

  • «Создающий» код делает что-то, что занимает время. Например, загружает данные по сети или поставлен на таймер.
  • «Потребляющий» код хочет получить результат «создающего» кода, когда тот будет готов.

Такая связь возможна потому, что промис содержит своё состояние:

  • pending («ожидание»),
  • fulfilled («выполнено успешно») или rejected («выполнено с ошибкой»).

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

Синтаксис

Создаем промис — вызываем конструктор и передаём в параметры функцию — «создающий» код (другое название — «исполнитель», executor). Она вызывается автоматически и получает два аргумента: resolve и reject. Это встроенные функции, писать их не нужно. Исполнитель вызовет одну из двух по готовности.

/* Стрелочная функция, переданная в экземпляр —
это «создающий» код (может занять время) */
const myPromise = new Promise((myResolve, myReject) => {
  /* Первый аргумент – функция, которая выполняется,
  если промис переходит в состояние  `fulfilled` —
  «выполнен успешно» — и получает  результат. */
  myResolve();

  /* Второй аргумент – функция, которая
  выполняется, если промис переходит в состояние
  «выполнен с ошибкой», и получает объект ошибки */
  myReject();
});

// «Потребляющий» код —ждет смены выполнения промиса
myPromise.then(
  (value) => {
    // код в случае успеха
  },
  (error) => {
    // код в случае ошибки
  }
);

Функции-потребители регистрируются (подписываются) с помощью методов then, catch и finally.

then

Наиболее важный и фундаментальный метод.

promise.then(
  (result) => {
    // обработает успешное выполнение
  },
  (error) => { /* обработает ошибку */ }
);

Например, вот реакция на успешно выполненный промис.

const promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve("done!"), 1000);
});

// resolve запустит первую функцию, переданную в .then
promise.then(
  result => alert(result),
  error => alert(error)
);

В then можно передать только первую, «успешную» функцию.

let promise = new Promise(resolve => {
  setTimeout(() => resolve('done!'), 1000);
});

// выведет "done!" спустя одну секунду
promise.then(alert);

catch

Если нужно обработать только ошибку, то используют метод catch(errorHandlingFunction).

let promise = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error("Ошибка!")), 1000);
});

// выведет "Error: Ошибка!" спустя одну секунду
promise.catch(alert);

Того же результата можно добиться, передав в первый, «успешный» параметр then значение null: then(null, errorHandlingFunction).

finally

Выполнится, когда промис завершится, независимо от того, успешно или нет. Аналогичен блоку finally конструкции try {...} catch {...} — см. раздел «Ошибки».

new Promise((resolve, reject) => {
/* сделать что-то, что займёт время, и после
вызвать resolve/reject */
}).finally(() => остановить индикатор загрузки)
  .then(
    result => показать результат,
    err => показать ошибку
  )

Свойства

Promise.length, Promise.prototype

Методы

Promise.all, Promise.race, Promise.reject, Promise.resolve

Модули

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

Долгое время в JavaScript отсутствовал синтаксис модулей на уровне языка. Появились библиотеки модулей: AMD, UMD и CommonJS, созданная для Node.js. В модулях «ноды» еще можно встретить их конструкции, типа const lib = require('package/lib');.

Но к настоящему моменту сторонние решения де-факто вытеснены собственными модулями JS, появившиеся в 2016-м году.

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

Экспорт

// 📁 mymath.js
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

class Graph {
  addNode() {
    this.bar = 'node added';
  }
}

const PI = 3.1415;

/* Здесь файл становится модулем. В фигурных
скобках export'а перечисляем всё, что
экспортируем — например, только функции,
оставив класс и константу недоступными извне. */
export { add, subtract };

Экспортируемые функции, константы, классы можно переименовывать.

export { add as a, subtract as s };

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

export function add(a, b) {
  return a + b;
}

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

Экспорт по умолчанию

На практике модули встречаются двух типов:

  • Модуль, содержащий библиотеку или набор функций.
  • Модуль, который объявляет что-то одно. Например, модуль User.js экспортирует только class User.

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

Для второго подхода Модули предоставляют специальный синтаксис export default («экспорт по умолчанию»).

// Экспортируем только класс Graph
export default Graph;

Часто экспорт по умолчанию совмещают с инициализацией класса.

// 📁  User.js
export default class User {
  constructor(name) {
    this.name = name;
  }
}

☝️🧐 Рекомендации Airbnb

Название файла должно совпадать с именем, использованном в export default. То есть файлы модулей должны называться в верблюжьим или паскальном регистре, когда экспортируешь класс.

// myFunction.js
function myFunction() {
  // ...
}
export default myFunction;
// MyClass.js
class MyClass {
  // ...
}
export default MyClass;
// в других файлах
// ⛔️ плохо
import myFunction from './my-function';
import MyClass from './my-class';

// 👍  хорошо
import myFunction from './myFunction';
import MyClass from './MyClass';

Импорт

Чтобы добавить функциональность модуля в новый файл перечисляем нужную функциональность в конструкции import { /*что*/ } from './откуда';

import { add, subtract } from './mymath';

console.log(add(2, 2));

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

import { add as a, subtract as s, PI } from './mymath';

console.log(a(2, 2));

Можно импонировать всё, как объект.

import * as calc from './mymath';

/* Чтобы посмотреть список свойств импортированного
объекта, можно вывести его временно в консоль. */
console.log(calc);

calc.add(1, 2);

Чтобы импортировать то, что было экспортировано по умолчанию, не нужны фигурные скобки…

import Graph from './mymath';

…а для того чтобы импорт по умолчанию переименовать, не нужен союз as.

import newNameForGraph from './mymath';

Если экспорт был и именованный и по умолчанию, можно импортировать одновременно и то, и другое…

import Graph, { add, subtract } from './mymath';

…Или.

import Graph, * as calc from './mymath';

Пути к импортируемым файлам

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

  • точка со слешом означает верхний уровень — CWD, current working directory;
  • двоеточие со слешом — уровень вложенности текущего файла.
import { listen, enter, leave } from '../../util';

Модули-зависимости — библиотеки, установленные с помощью npm или другого менеджера — указываются, просто как названия.

import React from 'react';
import * as lib from 'smth-from-node_modules';

Импорт без присваивания переменной

Можно только подключить модуль (его код запустится), но не присваивать его переменной.

import './mymath';

Таким способом, в некоторых средах и сборщиках, в частности, в webpack можно импортировать стили и графику.

import './style.css';
import './logo.svg';

Однако попытка таким образом импортировать картинки в Node.js приведёт к ошибке.

Ошибки

Перехват ошибок — try...catch

Пример валидации пользовательского ввода с помощью конструкции try...catch.

// Получаем введенное значение
let x = document.getElementById('myNum').value;

try {
  // Тестируемый блок кода.
  if(x == '') throw 'is empty';
  if(isNaN(x)) throw 'not a number';
  if(x > 10) throw 'too high';
  if(x < 5) throw 'too low';
} catch(err) {
  // Обработчик ошибки
  message.innerHTML = 'Input ' + err;
} finally {
  // Необязательный раздел завершения проверки.
  console.log('');
}

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

Необязательный раздел finally выполняется в любом случае:

  • после try, если не было ошибок,
  • после catch, если ошибки были.

Стандартные ошибки

В JavaScript встроен ряд конструкторов для обработки стандартных ошибок.

  • SyntaxError — синтаксически неправильный код.
  • TypeError — ошибка, связанная с типом данных.
  • RangeError — значение не входит в множество или выходит за диапазон допустимых значений.
  • ReferenceError — ошибка, возникающая при обращении к несуществующей переменной.
  • URIError — ошибка декодирования или кодирования URI.
// В объекте нет имени пользователя
const data = '{ "age": 30 }';

try {
  // выполнится без ошибок
  const user = JSON.parse(data);

  if (!user.name) {
    /* Для вывода ошибки используется стандартный
    конструктор */
    throw new SyntaxError('Данные некорректны');
  }

  alert(user.name);
} catch (err) {
  alert('Извините, в данных ошибка');
}
Правила хорошего кода
  1. Избегать глобальных переменных.
  2. Объявлять/инициализировать переменные и классы вверху программы.
  3. Никогда не использовать new и экземпляры встроенных классов для присвоения значений примитивного типа.
  4. Также избегать new для создания экземпляров Object, Array, Function.
  5. Остерегаться случайного преобразования типов.
  6. Использовать тройное равно для сравнения.
  7. Функциональные выражения предпочтительнее объявлений функций
  8. Функции: JSDoc-комментарии и параметры по умолчанию.
  9. Заканчивать switch default’ом.
  10. Не использовать eval().
  11. Не использовать параллельные массивы.
  12. Не хранить длину массива в переменной.
  13. eventListener вместо on-функций
  14. Соглашение об именовании.
  15. Использовать директиву ‘use strict mode’;
  16. Всегда придерживаться одного стиля кодирования.

1) Избегать глобальных переменных

Они могут быть переопределены любыми скриптами. Использовать const, let и замыкания.

// ⛔️ плохо
const globalVar = 1;

// 👍 хорошо — замыкание
(() => {
  const localVar = 1;
  // код функции
})();

2) Объявлять/инициализировать переменные, классы и функции вверху программы

const myNum = 0;
const myStr = '';
const myArr = [];
const myObj = {};

3) Никогда не использовать new и экземпляры встроенных классов для присвоения значений примитивного типа

Строки, числа, логические типы данных можно преобразовывать, если надо, и сравнивать. Объекты сравнивать между собой или с примитивами нельзя. eslint: no-new-wrappers

const y = new String('John'); // ⛔️

Новые символы создаются с помощью функции Symbol(). Следовательно, оператор new в данном случае не просто не рекомендован, а вызовет ошибку.

const sym = new Symbol(); // ⛔️ TypeError
const id = Symbol('id'); // 👍

4) Также не использовать оператор new для создания экземпляров встроенных классов Object, Array, Function

Это усложняет код и замедляет его выполнение.

const myObject = new Object(); // ⛔️
const myObject = {}; // 👍

Ограничения не означает полного запрета на оператор new. Ег нужно использовать для создания экземпляров пользовательских объектов, и некоторых встроенных объектов. Например, new RegExp() используется, когда надо создать регулярное выражение «на лету» из динамически сгенерированной строки. А new Event() для создания пользовательского события.

// новый объект
const x1 = {};

// новая строка
const x2 = '';

// новое число
const x3 = 0;

// новый логический тип
const x4 = false;

// новый массив
const x5 = [];

// новый объект регулярного выражения
const x6 = /()/;

// новое функциональное выражение
const x7 = () => {};

5) Остерегаться случайного преобразования типов

Выполняя математические операции JS может преобразовывать числа в строки. При вычитании строки из строки возвращает NaN.

// строка…
let x = '';

// NaN (Not-A-Number) — математическая ошибка
x = '5' - 'x';

// число
x = '5' - 7;

6) Использовать тройное равно для сравнения

Двойное равно сначала преобразовывает типы, а потом сравнивает. Тройное сравнивает тип и значение.

// условие вернет true
if (0 == '') {

// условие вернет true
} else if (1 == '1') {

// условие вернет true
} else if (1 == true) {

// условие вернет false
} else if (0 === '') {

// условие вернет false
} else if (1 === '1') {

// условие вернет false
} else if (1 === true) {
  // code
}

7) Функциональные выражения предпочтительнее объявлений функций

const multiplyNumbers = (a, b) => {
  return a * b;
};

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

Объявления функции используются только тогда, когда функция нужна из любой точки кода (объявленные функции «всплывают» — hoist).

8) Функции всегда надо комментировать по стандарту JSDoc и по возможности использовать параметры по умолчанию

Функцию можно вызвать с любым количеством аргументов. Если параметр не передан при вызове – он считается равным undefined.

/**
 * Функция с JSDoc-комментарием
 * @param {string} title
 * @param {number} width
 * @param {number} height
 */
function showMenu(
  title = 'Без заголовка',
  width = 100,
  height = 200
) {
  alert(`${title} ${width} ${height}`);
}

9) Заканчивать switch default’ом

switch (new Date().getDay()) {
  case 0:
    day = 'Sunday';
    break;
  case 1:
    day = 'Monday';
    break;
  case 2:
    day = 'Tuesday';
    break;
  case 3:
    day = 'Wednesday';
    break;
  case 4:
    day = 'Thursday';
    break;
  case 5:
    day = 'Friday';
    break;
  case 6:
    day = 'Saturday';
    break;
  default:
    day = 'Unknown';
}

10) Не использовать eval()

Функция eval() получает строку и выполняет ее как код JavaScript. Это плохо сказывается на производительности и безопасности. Можно, например, нечаянно выполнить вредоносный код полученный из сети. Если код известен (а не определяется в ходе выполнения процесса), вообще нет причин использовать eval(). Если код динамически генерируется во время выполнения, то часто возможно достичь цели лучшим методом, чем использование eval(). Например, использование записи с квадратными скобками для динамического доступа к свойствам.

const property = 'name';

// ⛔️ плохо
alert(eval(`obj.${property}`));

// 👍 хорошо
alert(obj[property]);

11) Не использовать параллельные массивы

При работе с вложенными данными следует соблюдать правила иерархии. Cвязанные данные должны иметь общий контейнер. Cвойства должны храниться внутри объекта, а НЕ объект — внутри свойства.

// ⛔️ плохо
firstNames = ['Иван', 'Пётр', 'Сидор'];
lastNames = ['Иванов', 'Петров', 'Сидоров'];

// 👍 хорошо
const people = [
  {
    lastName: 'Иванов',
    firstName: 'Иван',
    patronymic: 'Иваныч'
  },
  {
    lastName: 'Петров',
    firstName: 'Пётр',
    patronymic: 'Петрович'
  },
  {
    lastName: 'Сидоров',
    firstName: 'Сидор',
    patronymic: 'Сидорович'
  },
];

12) Не хранить длину массива в специальной переменной

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

// ⛔️ плохо
staff = firstNames.length;
for (let i = 0; i < staff; i++) {
  // Do something
}

// 👍 хорошо
for (let i = 0; i < firstNames.length; i++) {
  // Do something
}

13) eventListener вместо on-функций

Обработчик события может быть назначен прямо в разметке, в атрибуте, который называется on-событие.

<button type="button" onclick="myFunc()">
  Нажми меня
</button>

Можно назначать обработчик, из внешнего скрипта.

<button id="elem" type="button">Нажми меня</button>
<script>
  elem.onclick = function() {
    alert('Спасибо');
  };
</script>

Фундаментальный недостаток описанных выше способов назначения обработчика — невозможность повесить несколько обработчиков на одно событие.

Проблему решают методы addEventListener и removeEventListener.

function handler() {
  alert( 'Спасибо!' );
}

elem.addEventListener('click', handler);
elem.removeEventListener('click', handler);

14) Соглашение об именовании

  • Избегать названий из одной буквы.
  • Имя должно быть значащим.
  • PascalCase — для классов. camelCase — для именования объектов, функций и экземпляров.
  • Не использовать _ в начале или в конце названий.
  • Сокращения или буквенные аббревиатуры писать согласно логике верблюжьего регистра.
// ⛔️ плохо
function q() {
  // ...
}

const _firstName_ = 'Panda'; // eslint-disable-line

// 👍 хорошо
function myQuery() {
  // ...
}

// 👍 хорошо
class User {
  // ...
}

// ⛔️ плохо
const optimizedJPG = [
  // ...
];

// 👍 хорошо
const optimizedJpg = [
  // ...
];

15) Использовать директиву ‘use strict mode’

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

16) Всегда придерживаться общего стиля кодирования

  • Для командной работы можно прописать краткий свод правил, ориентируясь на признанный сообществом стиль. Наиболее популярным сейчас является стиль AirBnb. А упрощенную форму изложения можно позаимствовать у W3Schools.
  • Установить линтер и соответствующие правила на все машины.

Некоторые правила AirBnb, которые не приведены ранее.

  • Для строк использовать одиночные кавычки.
  • Максимальная длина строки — 80 символов.

    Чтобы длина строки не превышала этот предел, части команды переносятся на новые строки. Оператор присваивания остается в конце строки, остальные операторы переносятся на новые строки.

    document.getElementById('demo').innerHTML =
      'Hello Dolly';
    
    if (
      (foo === 123 || bar === 'abc')
      && doesItLookGoodWhenItBecomesThatLong()
      && isThisReallyHappening()
    ) {
      thing1();
    }
    
  • Объявление функции (не присваивание) недопустимо в условных блоках.
  • Доступ к свойствам и методам объекта всегда по возможности через точку. Запрос через квадратные скобки — вроде user['get'] — не рекомендуется (если это не требуется для чего-то особого в коде).
  • Фигурные скобки только для многострочных блоков кода.
  • Табуляция — два пробела.
  • Использовать логические отступы в цепочках вызовов, каждый вызов на новой строке. Например.
document.getElementById('child')
  .closest('.mommy')
  .classList
  .add('.is-angel-now');
  • Точки с запятой — всегда, где это необходимо.
  • Приведение типов по возможности в начале операции, для чисел всегда использовать parseInt или Number. Побитовые операции для приведения допустимы ради быстродействия, каждую такую операцию нужно комментировать.

  • Оригинал руководства
  • Перевод (устаревшей версии){:target=”_blank”}

См. также советы по оптимизации кода

Поиск и выбор элементов

Выбор элемента

По ID

Старый метод getElementById наиболее быстрый. Поэтому, желательно в разметке добавлять важным интерактивным элементам ID и использовать этот метод.

let el = document.getElementById('my-id');

⚠️ При попытке получить элемент из DOM по отсутствующему идентификатору, getElementById возвращает null. Поэтому стоит выполнить проверку на null перед тем, как обращаться к свойствам элемента.

if (el) {
  el.innerHTML = 'Boom!';
}

По CSS-селектору

Первый элемент на странице.

el = document.querySelector('.my > div .class');

Поиск одного элемента внутри другого.

el = document.querySelector('.my > div .class');

const child = el.querySelector('.my-pesdyuk');

По CSS-селектору: все элементы

Возвращает массив всех элементов страницы, соответствующих селектору.

el = document.querySelectorAll('.my > div .class');

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

☝️🧐 К элементам NodeList можно обратиться, указав конкретный индекс. В примере первому параграфу на странице будет присвоен красный фон.

cont text = document.querySelectorAll('p');
text[0].style.backgroundColor = 'red';

Чтобы применять к набору элементов методы массива — map или filter — надо создать из набора элементов массив.

const cells =
  [].slice.call(table.querySelectorAll('th, td'));

// Или с помощью spread-оператора
const cells = table.querySelectorAll('th, td');
const texts = [...cells].map(n => n.textContent)

Устаревшие методы: по HTML-тегу или по CSS-классу

Возвращают псевдомассивы всех указанных узлов или элементов страницы определенного класса.

document.getElementsByTagName('button');
document.getElementsByClassName('my-class');
Перебор массива элементов

1. Рекомендовано: циклом forEach

const elements =
  document.querySelectorAll('.my-element');

elements.forEach((el) => {
  console.log(el);
  // Do smth useful...
});

2. Тоже, но еще и с оператором расширения (spread-оператором)

[...elements].forEach((el) => {
  // Do smth
});

3. Тот же forEach, только, как метод массива

Array.from(elements).forEach((el) => {
  // Do smth...
});

// Или
[].forEach.call(elements, (el) => {
  // Do smth...
});

// Или
[].slice.call(elements, 0).forEach((el) => {
  // Do smth...
});

В данном случае метод массива \[\].forEach передается псевдомассиву NodeList (у которого раньше метода forEach не было) с помощью метода call.

4. Более быстрый цикл for

for (let i = 0; i < elements.length; i++) {
  //Do smth w/ elements[i]
}
Получить родительский элемент
const parent = el.parentElement;

// Или так
const parent = el.parentNode;

Разница между parentNode и parentElement в том, что попытка найти родителя document‘а с помощью parentElement вернет null, так как корневой узел не является элементом.

Выбор предка по селектору
const parent = el.closest('.some-top-level-container');

Метод Element.closest() возвращает ближайший родительский элемент (или сам элемент) по CSS-селектору или null, если таковых элементов вообще нет.

<article>
  <div class="granny">Большая матрешка
    <div class="mommy">Средняя матрешка
      <div id="child">Маленькая матрешка</div>
    </div>
  </div>
</article>
// Находим элемент, чьи предки нас интересуют
const el = document.getElementById('child');

// Находим ближайшего предка с классом .mommy
const r1 = el.closest('.mommy');

/* В эту переменную будет сохранен сам элемент
.child, так как closest возвращает и сам элемент,
если он соответствует селектору.
В данном случае — div'у вложенному в div */
const r2 = el.closest('div div');

/* Здесь мы выбрали .granny — первый div,
являющийся прямым потомком article */
const r3 = el.closest('article > div');

/* Здесь мы выбрали article — первого предка,
не являющегося div'ом */
const r4 = el.closest(':not(div)');
Получить потомков

Потомки можно получать из двух свойств:

  • children возвращает коллекцию HTML-элементов, то есть узлов, соответствующих тегам;
  • childNodes — все дочерние элементы, включая текст и комментарии.
const children = el.children;
const childNodes = el.childNodes;

Потомков можно перебрать циклом, выбрать по индексу или с помощью методов firstChild и lastChild.

const first = el.firstChild;
const last = el.lastChild;

// Те же потомки — первый и последний
const first = childNodes[0];
const last = childNodes[childNodes.length - 1];
Получить соседей

Найти соседа выше.

const prev = el.previousElementSibling;

Найти соседа ниже.

const next = el.nextElementSibling;

Если нужно найти не только элементы, но и текстовые узлы, тогда так:

const prev = el.previousSibling;
const next = el.nextSibling;

Найти всех соседей.

// Сначала найти родителя
const parent = el.parentNode;

// Найти всех потомков за исключением исходного `
const siblings = [].slice
  .call(parent.children)
  .filter((child) => {
    return child !== el;
  });
Проверка на соответствие селектору
if (event.target.matches('[my-attr], .my-class')) {
  // Do smth
}
Является ли элемент потомком/предком другого?

Является ли элемент потомком определенного предка? Содержит ли элемент определенного потомка?

const isDescendant = parent.contains(child);
const isParent = parent.querySelector('.child');
Получение индекса элемента в коллекции
function index(item, collection) {
  return [].slice
    .call(document.querySelectorAll(collection))
    .indexOf(document.querySelector(item));
}

console.log(index('#item', '.collection'));
Поиск предка с прокруткой

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

const isScrollable = (el) => {
const hasScrollableContent =
  el.scrollHeight > el.clientHeight;

const overflowYStyle =
  window.getComputedStyle(el).overflowY;
const isOverflowHidden =
  overflowYStyle.indexOf('hidden') !== -1;

  return (
    hasScrollableContent && !isOverflowHidden;
  );
};

const getScrollableParent = (el) => {
  return !el || el === document.body
  ? document.body
  : isScrollable(el)
  ? ele
  : getScrollableParent(el.parentNode);
};

Манипуляции с DOM

Создать элемент или текстовой узел

Элемент

const el = document.createElement('div');

/* Необязательно, но обычно добавляются атрибуты
и содержание. А затем элемент вставляется в документ */
el.width = '100px';
el.ariaLabel = 'Huzzah!';
/* Альтернативный способ установки и изменения
значений атрибутов — см. раздел ниже */
el.setAttribute('data-role', 'the-dummy');
el.innerHTML = 'Huzzah!';
document.body.appendChild(el);

Текстовой узел

const el = document.createTextNode('Hello World!');
Клонирование элемента

Со всеми атрибутами и потомками

const cloned = el.cloneNode(true);

Без потомков

const cloned = el.cloneNode(false);
Атрибуты: проверить наличие; получить, задать или переопределить значение; удалить

Проверить: есть ли определенный атрибут у элемента

if (!myImg.hasAttribute('alt')) {
  alert('Добавь `alt`, дятел');
}

Получить значение

const title = link.getAttribute('title');

Удалить атрибут title

el.removeAttribute('title');

Задать или поменять значения атрибутов

Это можно сделать двумя способами. С помощью метода setAttribute или напрямую поменяв свойство DOM-узла. Разница между свойствами и атрибутами незначительная, но есть.

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

Вместе с тем, нужно учитывать, что не браузер создает DOM-свойства не для всех атрибутов. Например, свойства role нет и его нужно задавать через setAttribute.

// Установить свойство
image.width = '100px';
image.height = '120px';

// Установить атрибут
image.setAttribute('width', '100px');
image.setAttribute('height', '120px');

button.setAttribute('aria-expanded', false);

Переключение логических значений атрибута

button.getAttribute('aria-expanded') === 'true'
  ? button.setAttribute('aria-expanded', 'false')
  : button.setAttribute('aria-expanded', 'true');

Переключение логических атрибутов, типа disabled

input.toggleAttribute('readonly');
Получение, установка, удаление data-атрибутов

Получим значение атрибута data-message элемента.

const message = el.getAttribute('data-message');

// …или так
const message = el.dataset.message;

Установим значение data-атрибута.

el.setAttribute('data-message', 'Hello World');

// или
el.dataset.message = 'Hello World';

Удалим data-атрибут.

el.removeAttribute('data-message');

// или
delete el.dataset.message;

Кстати delete el.dataset не удалит все атрибуты. Надо удалять по одному.

Вставить элемент после открывающего тега будущего родителя
target.insertBefore(el, target.firstChild);
Вставить элемент перед закрывающим тегом будущего родителя
target.appendChild(el);
Вставить элемент до или после другого элемента

Вставить до элемента target

target.parentNode.insertBefore(el, target);

// Или
target.insertAdjacentElement('beforebegin', el);

Вставить после элемента target

target.parentNode.insertBefore(el, target.nextSibling);

// Или
target.insertAdjacentElement('afterend', el);
Вставить HTML-разметку до или после элемента

Вставить до элемента `

const html = '<h1>Huzzah!</h1>'
el.insertAdjacentHTML('beforebegin', html);

Вставить после элемента `

el.insertAdjacentHTML('afterend', html);
Заменить элемент

Элемент ` будет удален из DOM, а на его место в коде добавлен newEl.

el.parentNode.replaceChild(newEl, el);
«Завернуть» один элемент в другой

Завернуть элемент ` в эемент wrapper

// Сначала добавляем `wrapper` перед ` — в общего предка
el.parentNode.insertBefore(wrapper, el);

// Затем делаем ` потомком `wrapper`'а
wrapper.appendChild(el);
Удалить элемент

1. Метод remove

el.remove();

2. Метод removeChild

if (el.parentNode) {
  el.parentNode.removeChild(el);
}
Удалить все дочерние узлы элемента

Следует удалить все дочерние узлы, пока не останется ни одного.

while (node.firstChild) {
  node.removeChild(node.firstChild);
}

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

el.innerHTML = '';
Удалить элемент, но сохранить его потомков
// Получаем «дедушку»
const parent = el.parentNode;

// Выносим всех потомков на уровень «дедушки»
while (el.firstChild) {
  parent.insertBefore(el.firstChild, el);
}

// Когда родитель опустел, удаляем его
parent.removeChild(el);
Поменять два узла в DOM'е местами

Функция меняет местами узлы, передаваемые в параметры.

const swap = (nodeA, nodeB) => {
  const parentA = nodeA.parentNode;
  const siblingA =
    nodeA.nextSibling === nodeB
      ? nodeA
      : nodeA.nextSibling;
  
  // Перемещаем `nodeA` выше узла `nodeB`
  nodeB.parentNode.insertBefore(nodeA, nodeB);
  
  // Перемещаем `nodeB` выше соседа снизу `nodeA`
  parentA.insertBefore(nodeB, siblingA);
};
Шаблонные литералы

Шаблонные литералы заключены в обратные кавычки (` `).

  • Могут быть многострочными;
  • содержать подстановки, обозначаемые знаком доллара и фигурными скобками — ${переменная или выражение};
  • содержать условные конструкции: тернарный оператор или конструкцию if … else, заключенную в анонимную самовызывающуюся функцию (IIFE);
const wizards = [
    'Hermione', 'Neville', 'Gandalf', 'Radagast'
  ];
  const showHeading = true;

  const str = `
    ${showHeading ? '<h1>Awesome Wizards</h1>' : ''}
    <p>Abracadabra! Expecto Patronum!</p>
    ${(() => {
      if (wizards.length > 3) {
        return '<p>There are at least 3 wizards.</p>';
      }
      return '<p>There are fewer than 3 wizards.</p>';
    })()}
    <ul>
      ${wizards
        .map((wizard) => {
          return `<li>${wizard}</li>`;
        })
        .join('')}
    </ul>
  `;

  console.log(str);`

Вложенные шаблонные литералы

const classes = `header ${ isLargeScreen() ? '' :
  `icon-${item.isCollapsed ? 'expander' : 'collapser'}` }`;

Теговый шаблон и функции тега

Расширенной формой шаблонных литералов являются теговые шаблоны. Они позволяют разбирать шаблонные литералы с помощью специальной пользовательской функции.

Вызывается такая функция, не как обычно — myFunc(arg1, arg2). А так: myTag`строковый литерал с ${подстановками}!`.

При вызове в первый параметр такой функции передается массив подстрок из шаблона. В первом примере это будет ['строковый литерал с ', '!'].

В остальные параметры передаются выражения или переменные из подстановок.

В итоге, функция должна вернуть собранную строку или что-то еще, как будет показано в примере №3.

const person = 'Mike';
const age = 28;

function myTag(strings, personExp, ageExp) {
  /* При вызове функции ниже, в параметр
  `strings` будет передан массив
  ['That ', 'is a '] первый, нулевой
  элемент — 'That ' */
  const str0 = strings[0];
  // 'is a '
  const str1 = strings[1];

  let ageStr;
  /* `ageExp` — это третий параметр функции тега,
  а, значит, второе выражение в шаблоне-аргументе */
  if (ageExp > 99) {
    ageStr = 'centenarian';
  } else {
    ageStr = 'youngster';
  }

  /* Возвращаем строку, также используя шаблонный
  литерал. `personExp` — это второй параметр
  функции тега, а, значит, первое выражение
  в шаблоне-аргументе */
  return `${str0}${personExp}${str1}${ageStr}`;
}

/* Вызываем функцию myTag и передаем в параметры
строку с подстановками из заранее определенных
переменных */
const output = myTag`That ${person} is a ${age}`;

console.log(output); // That Mike is a youngster

Пример №3. Функция тега не обязана возвращать строку.

function template(strings, ...keys) {
  return (...values) => {
    const dict = values[values.length - 1] || {};
    const result = [strings[0]];
    keys.forEach((key, i) => {
      const value =
        Number.isInteger(key) ? values[key] : dict[key];
      result.push(value, strings[i + 1]);
    });
    return result.join('');
  };
}

const t1Closure = template`${0}${1}${0}!`;
t1Closure('Y', 'A'); // "YAY!"
const t2Closure = template`${0} ${'foo'}!`;
t2Closure('Hello', { foo: 'World' }); // "Hello World!"`

Сырые строки

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

function tag(strings) {
  return strings.raw[0];
}

/* В rawSample будет сохранена строка 'line 1 \\n line 2'
со символом экранирования (первый слеш) и символом новой
строки `\n`, а без rоw сохранилось бы две строки
без спецсимовлов */
const rawSample = tag`line 1 \\n line 2`;
Тег `template`

HTML-тег <template> позволяет записывать на странице блоки разметки, невидные по умолчанию, но готовые к наполнению и выводу на экран с помощью JS.

Пример

<table id="product-table">
  <thead>
    <tr>
      <td>UPC_Code</td>
      <td>Product_Name</td>
    </tr>
  </thead>
  <tbody>
    <!-- данные будут вставлены сюда -->
  </tbody>
</table>

<template id="product-row">
  <tr>
    <td></td>
    <td></td>
    <td>
      <!-- внутри можно добавить стили и скрипты -->
      <style>
        td { font-weight: bold; }
      </style>
      <script>
        alert('Привет');
      </script>
    </td>
  </tr>
</template>

У нас есть таблица и разметка строки таблицы в templat‘е. Используем JavaScript, чтобы вставить строки в таблицу. Содержимое шаблона доступно по его свойству content в качестве DocumentFragment – особого типа DOM-узла.

// Убедимся, что браузер поддерживает <template>
if ('content' in document.createElement('template')) {
  // Находим элемент tbody таблицы и шаблон строки
  const tbody = document.querySelector('tbody');
  const template =
    document.querySelector('#product-row');

  // Клонируем новую строку…
  const row01 = template.content.cloneNode(true);
  const td = row01.querySelectorAll('td');

  // заполняем содержимым…
  td[0].textContent = '1235646565';
  td[1].textContent = 'Stuff';

  // и вставляем клон в таблицу
  tbody.appendChild(clone);

  // Также клонируем новые строки: row02, row03 etc
} else {
  alert(`
    Извините, ваш браузер не поддерживает
    технологии нашего сайта.
  `);
}

☝️🧐 Обработка событий

Когда мы клонируем и вставляем в шаблон — template.content.cloneNode(true) и т.д. — то, так как это DocumentFragment, вместо тега <template> с потомками вставляются только потомки (<tr>, <td>, <style>, <script>).

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

<div id="container"></div>

<template id="template">
  <div>Click me</div>
</template>
const container = document.getElementById('container');
const template = document.getElementById('template');

function clickHandler(event) {
  alert('Huzzah!');
}

const firstClone = template.content.cloneNode(true);
/* Ставим слушатель на корневой элемент шаблона.
Не сработает */
firstClone.addEventListener('click', clickHandler);
container.appendChild(firstClone);

// Теперь клонируем потомка…
const secondClone =
  template.content.firstElementChild.cloneNode(true);
// И слушатель, установленный на нём, сработает
secondClone.addEventListener('click', clickHandler);
container.appendChild(secondClone);

В переменную firstClone мы сохранили экземпляр шаблона, как DocumentFragment. И хотя нас получилось отрисовать его внутри контейнера, клик по нему не срабатывает. В переменной secondClone у нас экземпляр потомка templat‘а — div. Клик на нём обрабатывается.

☝️🧐 Сложности применения

У <template> есть очевидное преимущество — возможность вставки «живого» содержимого, вместе со скриптами. Но конструкции с использованием <template> слишком многословны. Решение не предлагает операторов итерации, связывания данных и подстановки переменных. И хотя эти возможности можно реализовать дополнительными средствами, разработчики предпочитают шаблонизаторы и шаблонные литералы.

<div id="app"></div>

<template id="list-item">
  <div class="wizard">
    <strong id="wizard-name"></strong>
  </div>
</template>
// Получаем элементы
const app = document.querySelector('#app');
const listItem = document.querySelector('#list-item');
const wizards = ['Merlin', 'Gandalf', 'Neville'];

// Создаем временный контейнер
const elems = [];

// Loop through each wizard
wizards.forEach((wizard) => {
  // Клонируем шаблон ип находим в нём <div>
  const div =
    listItem.content.cloneNode(true).querySelector('div');

  // А теперь — <strong> внутри <div>
  const strong = div.querySelector('strong');

  // Вставляем текст
  strong.textContent = wizard;

  // Добавляем во временный массив
  elems.push(div);
});

// Переносим из elems в интерфейс
app.append(...elems);
Динамическая загрузка JS
// Создать script-ссылку
const script = document.createElement('script');
script.src = '/path/to/js/file.js';

// Вставить перед закрывающим тегом `body`
document.body.appendChild(script);

Исполнить JS по завершению загрузки

// Создаем script-ссылку
// ...

script.addEventListener('load', function() {
  // Сейчас скрипт полностью загружен
  // Do smth
});

// Вставляем перед закрывающим тегом `body`
//...

Загрузка нескольких скриптов по порядку

Для это задачи мы подготовили массив скриптов arrayOfJs, собрав ссылки в нужном порядке. На странице мы подгрузим сначала первый скрипт. Когда он будет полностью загружен — второй. И так далее — пока не загрузим всё, что есть.

/* Загружаем скрипт по ссылки переданной в `url`.
Можно добавить второй параметр — `reject` и вызывать
reject(true), если скрипт не загрузится — например,
по таймеру `setInterval` */
const loadScript = (url) => {
  return new Promise((resolve) => {
    const script = document.createElement('script');
    script.src = url;

    script.addEventListener('load', () => {
      // Скрипт загружен, выходим из слушателя
      resolve(true);
    });

    // Вставляем в документ
    document.body.appendChild(script);
  });
};

// Выполняем все промисы по порядку
const waterfall = (promises) => {
  return promises.reduce(
    (p, c) => {
      // Ждем пока не будет выполнено `p`
      return p.then(() => {
        // и затем `c`
        return c().then((result) => {
          return true;
        });
      });
    },
    /* Метод resolve(value) возвращает промис
    выполненный с переданным значением */
    Promise.resolve([])
  );
};

// Загружаем массив скриптов по порядку
const loadScriptsInOrder = (arrayOfJs) => {
  const promises = arrayOfJs.map((url) => {
    return loadScript(url);
  });
  return waterfall(promises);
};

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

loadScriptsInOrder([
  '/path/to/file.js',
  '/path/to/another-file.js',
  '/yet/another/file.js',
]).then(() => {
  // Все скрипты загружены
  // Do smth
});

Разметка и текстовое содержание

Получить или задать/переопределить HTML-разметку элемента
<h2 class="subhead">Lorem <b>Ip.</b></h2>
const el = document.querySelector('h2');

// Получить. Результат примера: `Lorem <b>Ip.</b>`
const html = el.innerHTML;

// Задать или переопределить
el.innerHTML = 'Hello World!';

/* Задать или переопределить, включая открывающий
и закрывающий теги элемента. Результатом примера
будет замена <h2 class="subhead">Hello World!</h2>
заголовком `h3` с новым содержанием. */
el.outerHTML = '<h3>Изменили!</h3>';
Создать / изменить содержимое элемента

Для большинства случаев подойдет свойство innerHTML.

el.innerHTML = '<b>Hello,</b> world!';

Хотя чистый текст, без разметки можно вставить и с помощью свойств textContent или innerText.

el.textContent = 'Hello, world!';
Получить текст элемента и его потомков
const content = el.textContent;

В content сохранится текст ` и всех его потомков. Все HTML-теги будут удалены.

Выделить текстовое содержимое элемента

Функция выделяет текст элемента `. Не получает, и не сохраняет — только выделяет — также, как пользователь текст курсором)

const selectText = (el) => {
  const selection = window.getSelection();
  const range = document.createRange();
  range.selectNodeContents(el);
  selection.removeAllRanges();
  selection.addRange(range);
};
Получить выделенный пользователем текст
const selectedText = window.getSelection().toString();
Очистить содержание элемента от HTML-разметки

1. С помощью DOMParser

const stripHtml = function (html) {
  const doc = new DOMParser().parseFromString(html, 'text/html');
  return doc.body.textContent || '';
};

2. С помощью тега <template>

<template> предназначен для заготовок разметки, которая изначально скрыта от пользователя, но может быть использована для вывода данных с помощью JS.

const stripHtml = (html) => {
  const el = document.createElement('template');
  el.innerHTML = html;
  return el.content.textContent || '';
};

3. Извлечь текст из поддельного элемента (не рекомендуется)

const stripHtml = (html) => {
  // Создаем новый элемент
  const el = document.createElement('div');
// const el = document.createElement('textarea');
  
  // Задаем HTML-разметку
  el.innerHTML = html;
  
  // Возвращаем только текст
  return el.textContent || '';
};

Подход не рекомендуется, потому что оставляет возможность злоумышленникам вставить в разметку теги, например <script>. И хотя этого можно избежать, используя при создании ` не div, а textarea, какого-то преимущества перед первым способом — DOMParser — всё равно не появится.

Вставить из буфера обмена чистый текст, без форматирования

Предположим, у нас есть текстовой редактор #editor и нам нужно вставить в него содержимое буфера обмена, очистив его от форматирования — чистый текст.

const editorEl = document.getElementById('editor');

// Обработчик события `paste`
editorEl.addEventListener('paste', (e) => {
  // Предотвращаем действия браузера по умолчанию
  e.preventDefault();

  // Получаем текст из буфера
  const text = e.clipboardData
    ? (
        e.originalEvent || e
      ).clipboardData.getData('text/plain')
    : '';

  /* метод execCommand устарел - https://mzl.la/3oWCFle
  но используем, если поддерживается браузером */
  if (document.queryCommandSupported('insertText')) {
    document.execCommand('insertText', false, text);
  } else {
    // Вставляем текст в место положения курсора
    const range = document.getSelection().getRangeAt(0);
    range.deleteContents();

    const textNode = document.createTextNode(text);
    range.insertNode(textNode);
    range.selectNodeContents(textNode);
    range.collapse(false);

    const selection = window.getSelection();
    selection.removeAllRanges();
    selection.addRange(range);
  }
});
Копировать текст в буфер обмена

Напрямую текст можно скопировать в буфер обмена только из поля ввода. Если нужно копировать сдержание, полученное каким-то другим способом, — например, выделенное пользователем — решение немножко усложнится. Сначала придется сохранить нужный фрагмент в переменную. Затем создать временное поле ввода. Передать в его атрибут value сохраненное значение. А после копирования в бувер, удалить временный элемент (в примере — textarea.myTextarea).

// Если нужно скопировать из поля, всё просто:
  const myTextarea =
    document.getElementById('my-tеxtarea');

/* В иных случаях создаем «перевалочную» `textarea`.
Удаляем оформление. Прячем, чтобы пользователь
не увидел. Переносим в атрибут `value` нужный
фрагмент, сохраненный в переменную (в примере —
`text`). Вставляем «времянку» в документ и
переводим на неё фокус. */
// const myTextarea =
//   document.createElement('textarea');
// myTextarea.style.border = '0';
// myTextarea.style.padding = '0';
// myTextarea.style.margin = '0';
// myTextarea.style.position = 'absolute';
// myTextarea.style.left = '-9999px';
// myTextarea.style.top =
//   `${document.documentElement.scrollTop}px`;
// myTextarea.value = text;
// document.body.appendChild(myTextarea);
// myTextarea.focus();

myTextarea.select();
// Протестировать: хак для мобильных устройств
// myTextarea.setSelectionRange(0, 99999);

// Копируем текст в буфер
navigator.clipboard.writeText(myTextarea.value);

// «Времянку» удаляем
// document.body.removeChild(myTextarea);

См. «Clipboard_API.

Сохранить и восстановить выделение
/**
* Сохранить выделение
* @returns экземпляр Range, если на странице что-то выделено
*/
const save = () => {
  const selection = window.getSelection();
  return selection.rangeCount === 0 ? null : selection.getRangeAt(0);
};

// Восстановить выделение
// `range` is a `Range` object
/**
* Восстановление выделения
* @param {object} range объект Range, фрагмент документа
*/
const restore = (range) => {
  const selection = window.getSelection();
  selection.removeAllRanges();
  selection.addRange(range);
};

CSS

Добавить, удалить, переключить, заменить класс

Добавить

el.classList.add('class-name');

// Сразу несколько
el.classList.add('another', 'class', 'name');

Удалить

el.classList.remove('class-name');

// Сразу несколько
el.classList.remove('another', 'class', 'name');

Переключить: включить, если выключен и наоборот

el.classList.toggle('class-name');

Заменить

el.classList.replace('is-focused', 'is-blurred');

className

У свойства classList есть примитивная альтернатива - свойство className, которое содержит значение атрибута class элемента.

const el = document.getElementById('my-div');

/* Получение значения. Если классов несколько, будет
возвращена строка со списком */
let value = el.className;

// Установка нового значения
if (el.className == 'my-style') {
  el.className = 'new-style';
} else {
  el.className = 'my-style';
}

// Добавление классов
el.className += ' new-class-1 new-class-2';

// Изменение свойства в классе
if (el.className == 'my-style') {
  el.style.fontSize = '30px';
}

Однако classList всё-таки предпочтительней, так как с его помощью можно добавить или удалить только один класс из нескольких, не затрагивая другие. А className заменяет всё сразу.

Проверка CSS-класса

Назначен ли элементу, например, класс my-class

el.classList.contains('my-class');
Получить стили элемента

Мы можем получить все стили элемента из блоков разных селекторов.

const styles = window.getComputedStyle(el, null);

Из полученных стилей можем извлечь значение нужного свойства.

const bgColor = styles.backgroundColor;
/* Или так. Свойства можно писать, как в шашлычном
так и в верблюжьем регистре */
const bgColor =
    styles.getPropertyValue('background-color');
const bgColor =
  styles.getPropertyValue('backgroundColor');

Для свойств с браузерным префиксом это делается иначе.

const textSizeAdjust =
  styles['-webkit-text-size-adjust'];
CSS-переменные
// Получение глобальной переменной (в :root)
const myVar =
  getComputedStyle(document.documentElement)
  .getPropertyValue('--my-var');

// Изменение глобальной переменной
document.documentElement.style
.setProperty('--my-var', 'pink');

const el = document.querySelector('.my-element');

if (el) {
  // Получение локальной переменной
  getComputedStyle(el)
    .getPropertyValue('--my-var');

// Изменение локальной переменной
  el.style.setProperty('--my-var', '10px');
}
Добавление, обновление и удаление строковых стилей

Добавить стили

el.style.backgroundColor = 'red';
el.style['backgroundColor'] = 'red';
el.style['background-color'] = 'red';

Обновить стили

Можно обновить или заменить несколько уже созданных стилей через свойство cssText.

// Добавить к существующим
el.style.cssText += 'color: white; margin: 8px';

// Заменить существующие
el.style.cssText = 'color: white; margin: 8px';

Удалить стили

el.style.removeProperty('background-color');

/* ⚠️ «Верблюжий регистр» в методе `removeProperty`
не сработает */
el.style.removeProperty('backgroundColor');
Показать / скрыть элемент
const box = document.querySelector('.box');

box.style.display = 'none';
box.style.display = 'block';

// Функция-переключатель
const toggle = (el) => {
  const { display } = el.style;
  el.style.display = display === 'none' ? 'block' : 'none';
};
Выделение целевой области для перетаскиваемых файлов

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

Создадим блок зоны и класс для выделения.

<div id="upload-photos">...</div>
.is-over-me {
  border: 4px dashed #ccc;
}

Добавляем класс is-over-me, когда пользователь перетягивает что-то над upload-photos. Заодно предотвращаем действия браузера по умолчанию — открытие картинки.

const el = document.getElementById('upload-photos');

el.addEventListener('dragenter', (e) => {
  e.preventDefault();
  e.target.classList.add('is-over-me');
});

el.addEventListener('dragover', (e) => {
  e.preventDefault();
});

Удаляем класс, когда пользователь «опускает» в зону отправки или тащит его куда-то дальше, за границы upload-photos.

el.addEventListener('dragleave', (e) => {
  e.preventDefault();
  e.target.classList.remove('is-over-me');
});

el.addEventListener('drop', (e) => {
  e.preventDefault();
  e.target.classList.remove('is-over-me');
});
Остановка прокрутки `body` при открытом модальном окне
// Остановка прокрутки `body`
document.body.style.overflow = 'hidden';

// Возобновление прокрутки `body`
document.body.style.removeProperty('overflow');

Тоже можно сделать и добавляя body CSS-класс с overflow: hidden.

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

Показать

el.style.display = '';

Спрятать

el.style.display = 'none';
Динамическая загрузка стилей
// Создаем ссылку
const link = document.createElement('link');
link.setAttribute('rel', 'stylesheet');
link.setAttribute('href', '/path/to/js/file.css');

// Вставляем в `head`
document.head.appendChild(link);
Узнать значение по умолчанию для определенного CSS-свойства

Функция getDefaultProperty возвращает значение по умолчанию для CSS-свойства property указанного тега. В процессе не задействуется текущая разметка — создается временный элемент по аргументу переданному в параметр tagName.

const getDefaultProperty = (tagName, property) => {
  // Создать элемент по параметру `tagName`
  const el = document.createElement(tagName);
  document.body.appendChild(el);

  // Получить стили `
  const styles = window.getComputedStyle(el);

  /* Получить значение по умолчанию для свойства,
  переданного в параметр `property` */
  const value = styles.getPropertyValue(property);

  // Удалить элемент
  document.body.removeChild(el);

  // Вернуть полученное значение
  return value;
};

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

getDefaultProperty('h2', 'font-size');

// Или
getDefaultProperty('h2', 'fontSize');

События

Добавить / удалить обработчик события
const handler = () => {
  // ...
};

// Добавить обработчик событий `click`, `mouseenter`, `keyup`
el.addEventListener('click', handler);
el.addEventListener('mouseenter', (e) => { /* ... */ });
document.addEventListener('keyup', (e) => { /* ... */ });

// Удалить обработчик события `click`
el.removeEventListener('click', handler);

Установка слушателя на все элементы коллекции узлов

const elems = document.querySelectorAll('.my-class');

elems.forEach((elem) => {
  elem.addEventListener('click', (event) => {
    // do smth...
  });
});

Установка слушателя на динамически добавленные элементы

Если в jQuery для этой задачи использовался специальный метод on, то в ванильном JS — тот же addEventListener, что и для исходных элементов DOM.

const searchElement = document.createElement('div');
document
  .querySelector('.search-container')
  .appendChild(searchElement);
// Устанавливаем слушатель
searchElement.addEventListener('click', handleClick);

Однократный вызов обработчика

Чтобы обработчик вызывался только раз, можно использовать опцию once метода addEventListener

const handler = function (e) {
  // The event handler
};

el.addEventListener('event-name', handler, { once: true });

…Либо использовать «самоуничтожение» — записать удаление в самом теле обработчика.

const handler = function (e) {
  // The event handler. Do something ...

  // Remove the handler
  e.target.removeEventListener(e.type, handler);
};

el.addEventListener('event-name', handler);
Предотвращение действия по умолчанию

Сценарии использования

  • Показывать пользовательское, а не стандартное контекстное меню.
  • Открыть внешнюю страницу в модальном окне, предовратив переход по ссылке.
  • Отложить отправку формы, предварительно проверив введённые данные.
  • Не открывать и не загружать файл при перетаскивании.

Метод preventDefault()

Работает, как в обработчиках событий, так и в HTML-атрибутах.

el.addEventListener('click', (e) => {  
  e.preventDefault();
  // Do smth
});

el.onclick = (e) => {
  e.preventDefault();
  
  // Do smth
};
<button
  type="submit"
  onclick="event.preventDefault()"
>
  Click
</button>

Есть еще один способ, но непрофессиональный — возврат false на событии

el.onclick = (e) => {
  // Do smth
  return false;
};
<button
  type="submit"
  onclick="return false"
>
  Click
</button>
Всплытие и делегирование: обработка событий на динамически созданных элементах и на массе однотипных

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

  1. Назначаем обработчик на контейнер.
  2. В обработчике проверяем event.target.
  3. Обрабатываем событие, если оно произошло внутри искомого элемента.
window.addEventListener('click', (event) => {
  if (event.target.matches('[data-dropdown-toggle]')) {
    toggleDropdownOnLabelClick(event);
  } else {
    closeDropdowns();
  }
});
Вложенные обработчики событий

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

Типичный пример. Пользователь кликает на кнопку и открывает модальное окно. Оно может быть закрыто нажатием клавиши Esc. За логику отвечают обработчики двух событий: click и keydown. Слушатель первого установлен на кнопку. Слушатель второго — на весь document.

Обычно мы записываем обработчики последовательно.

const handleClick = () => {
  // Открыть модалку
};

const handleKeydown = (e) => {
  // Закрыть модалку
};

// `buttonEl` — это наша кнопка
buttonEl.addEventListener('click', handleClick);
document.addEventListener(
  'keydown', handleKeydown
);

Обработчик handleKeydown зависит от handleClick, поскольку имеет смысл только тогда, когда модалка открыта. В принципе, можно использовать флаг, чтобы знать, когда окно открыто.

let isModalOpened = false;

const handleClick = () => {
  // Переключаем флаг
  isModalOpened = true;
  // Открываем окно ...
};

const handleKeydown = (e) => {
  // Проверяем открыто ли окно
  if (isModalOpened) {
    // Закрываем окно ...
  }
};

Не самое хорошее решение — слишком много кода, и это усложняет поддержку.

Более элегантное решение — вложенный обработчик

const handleClick = () => {
  document.addEventListener(
    'keydown', handleKeydown
  );
};

Никаких флагов. Код легче читается и поддерживается.

События клавиатуры
// Другое событие клавиатуры - `keyup`
window.addEventListener('keydown', (event) => {
  if (event.key === 'Esc' || event.key === 'Escape') {
    closeDropdowns();
  }
});
Событие прокрутки

Можно отслеживать прокрутку, как всего документа…

window.addEventListener('scroll',(event) => {
  console.log('Scrolling...');
});

// или

window.onscroll = (event) => {
  console.log('Прокрутка...');
};

…Так и элемента с прокруткой (overflow: visible | auto).

myEl.addEventListener('scroll', (event) => {
  console.log('Прокрутка...');
});

Для снижения потребления вычислительных мощностей при обработке события прокрутки желательно использовать технику throttling: «тормозящий декоратор» — функцию-обёртку ограничивающую вызовы в «обёрнутую» функцию.

// По умолчанию, снимаем флаг «прокрутка»
let scrolling = false;

// Во время прокрутки флаг устанавливаем
window.scroll = () => {
  scrolling = true;
};

/* Исполняем на прокрутке обработчик не постоянно,
а только каждые 300мс */
setInterval(() => {
  if (scrolling) {
    scrolling = false;
    /* Здесь описываем логику обработки
    прокрутки - это будет основная,
    «обёрнутая» функция */
  }
}, 300);

В обработчиках прокрутки часто используются:

Кликнул ли пользователь за границами элемента?

isClickedOutside вернёт true, если пользователь кликнет не по элементу `.

document.addEventListener('click', (event) => {
  const isClickedOutside = !el.contains(event.target);
});
Проверка поддержки событий касания
const touchSupported =
  'ontouchstart' in window ||
  (
    window.DocumentTouch &&
    document instanceof DocumentTouch
  );
Генерация пользовательских событий, конструктор `Event` и его потомки

Можно не только назначать обработчики, но и генерировать события из JavaScript. Это могут быть пользовательские события — например, menuOpen, menuClose и т.п. — или встроенные: как clickmousedown. Генерация встроенных событий бывает полезна для автоматического тестирования.

Пользовтели jQuery в своё время генерировали события с помощью метода trigger()

Базовый конструктор событий

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

Чтобы сгенерировать событие из кода, вначале надо создать объект события. Базовый конструктор Event принимает обязательное имя события и объект options:

  • bubbles: true чтобы событие всплывало.
  • cancelable: true если нужно, чтобы работал event.preventDefault().
// Всплытие: ловим на document...
document.addEventListener('hello', (event) => {
  alert(`Привет от ${event.target.tagName}`);
});

/* Создаем пользовательское событие
и привязываем к элементу методом `dispatchEvent` */
const myEvent = new Event('hello', { bubbles: true });
elem.dispatchEvent(myEvent);

// Обработчик на document сработает и выведет сообщение.

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

MouseEvent, KeyboardEvent и другие специальные конструкторы

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

  • UIEvent
  • FocusEvent
  • MouseEvent
  • WheelEvent
  • KeyboardEvent
// Создаем событие
const event = new MouseEvent('click', {
  bubbles: true,
  cancelable: true,
  clientX: 100,
  clientY: 100,
});

alert(event.clientX); // '100'

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

Предотвращение прокрутки к элементу, попавшему в фокус

Иногда требуется избежать прокрутки к элементу, которому передан фокус — методом element.focus() или autofocus="true". Например, фокус на поле в модальном окне может привести к прокрутке страницы к началу документа.

Опция preventScroll метода focus()

⚠️  Значительный процент браузеров не поддерживает эту опцию. Решение надо принимать по ситуации.

element.focus({preventScroll: true});

Прокрутка к области документа, видимой перед передачей фокуса

Метод работает во всех браузерах. Перед передачей фокуса надо сохранить координаты мыши. А после передачи прокрутить документ к соответствующей области.

const x = window.scrollX;
const y = window.scrollY;
elem.focus();

// Scroll to the previous location
window.scrollTo(x, y);

Формы и ввод данных

Показать/скрыть введенный пароль

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

<input id="form-password" type="password">
<button
  class="btn"
  id="form-password-toggle"
  aria-label="Показать/скрыть пароль"
>
  <!-- Иконки "глаз" и "закрытый глаз" -->
</button>
// Получаем необходимые элементы
const password =
  document.getElementById('form-password');
const toggle =
  document.getElementById('form-password-toggle');

toggle.addEventListener('click', () => {
  const type = password.getAttribute('type');
  
  password.setAttribute(
    'type',
    /* Переключить значение на `text`, если
    установлено `password`, и наоборот. */
    type === 'password' ? 'text' : 'password'
  );
  /* Можно еще менять класс кнопки или ее иконок,
  чтобы «глаз» открывался и закрывался. А можно
  использовать CSS-селекторы типа
  `[type='password'] + .btn` */
});
Перевод строки в `textarea` только Shift+Enter'ом

По умолчанию, нажатие клавиши Enter или одновременное нажатие клавиш Enter и Shift переводит каретку в textarea на новую строку. Но часто, например, в строковых редактируемых элементах, нажатием Enter надо подтверждать правки и выходить из режима редактирования. Для того, чтобы создать новую строку в textarea остается только комбинация Shift+Enter.

Для предотвращения перевода на новую строку Enter’ом мы можем обработать событие keydown.

<textarea id="message"></textarea>
const el = document.getElementById('message');

el.addEventListener('keydown', (e) => {
  // Получаем кодовый номер кнопки
  const keyCode = e.which || e.keyCode;
  
  // 13 — это Enter
  if (keyCode === 13 && !e.shiftKey) {
    // Запрещаем реакцию по умолчанию - перевод строки
    e.preventDefault();
    
    // И делаем что-то другое, например, отправляем форму
  }
});
Ограничение ввода данных определенным набором символов

Специальные типы полей

<input type="email">
<input type="number">
<input type="tel">
<input type="url">
<input type="date">
<input type="time">
<input type="color">`

Другие специальные типы можно найти здесь.

Атрибут pattern элемента input

<!-- Только 3 буквы. Никаких цифр и др. символов -->
<input name="code" type="text" pattern="[A-Za-z]{3}">

<!-- Не менее 8 символов. Мин. одна цифра и одна
буква в верхнем регистре -->
<input
  name="pwd"
  type="password"
  pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}"
>

<!-- Веб-адрес должен начинаться с протокола -->
<input name="url" type="url" pattern="https?://.+">

Для особых случаев — JS

JS используется в тех случаях, когда нужны ограничения, отличные от свойственных специальным типам полей. В примере ввод ограничивается числами и пробелами. input[type='number'] для такой задачи не годится.

<input type="text" id="input">

Обработка событий

«Повесив» обработчик на событие keypress, мы можем предотвратить ввод символов отличных от цифр и пробелов.

const el = document.getElementById('input');

el.addEventListener('keypress', (event) => {
  // Получаем код нажатой клавиши
  const key = event.which || event.keyCode;
  
  /* Клавишам цифр 0–9 назначены коды48–57.
  Пробел — 32 */
  if (key != 32 && (key < 48 || key > 57)) {
    // Если клавиша «неправильная», игнорируем
    event.preventDefault();
  }
});

Теперь надо разобраться со вставкой из буфера обмена и перетаскиванием текста в поле. Назначаем обработчик событию input.

// Получаем текущее значение поля
let currentValue = el.value || '';

el.addEventListener('input', (event) => {
  const { target } = event;

  // Если пользователь ввел цифры или пробелы
  /^[0-9\s]*$/.test(target.value)
    ? // Сохраняем введенное значение
      (currentValue = target.value)
    : /* В противном случае возвращаем исходное
      значение. `e.preventDefault()` здесь
      не поможет */
      (target.value = currentValue);
});

Теперь остается маленькая проблема: target.value = currentValue переводит курсор в конец поля ввода. Нам следует сохранить позицию курсора.

// Засекаем текущую позицию курсора в поле
const selection = {};

el.addEventListener('keydown', (e) => {
  const { target } = e;
  selection = {
    selectionStart: target.selectionStart,
    selectionEnd: target.selectionEnd,
  };
});

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

el.addEventListener('input', (e) => {
  const target = e.target;
  
  if (/^[0-9\s]*$/.test(target.value)) {
    currentValue = target.value;
  } else {
  /* Введены неразрешенные символы. Возвращаем
  исходное значение и позицию курсора */
  target.value = currentValue;
  target.setSelectionRange(
      selection.selectionStart,
      selection.selectionEnd
    );
  }
});

Мы можем объединить отслеживаемые свойства selectionStart и selectionEnd в одной переменной.

const target = e.target;
state.selectionStart = target.selectionStart;
state.selectionEnd = target.selectionEnd;
Подсчёт количества знаков, введенных в <textarea>
<textarea id="message" maxlength="200"></textarea>
<div id="counter"></div>

Обработка события <input>, происходящего при любом изменении введенных данных.

const messageEl = document.getElementById('message');
const counterEle = document.getElementById('counter');

messageEl.addEventListener('input', (e) => {
  const target = e.target;
  
  // Получаем значение `maxlength`
  const maxLength = target.getAttribute('maxlength');
  
  // Получаем набранное количество символов
  const currentLength = target.value.length;
  
  counterEle.innerHTML = `${currentLength}/${maxLength}`;
});

☝️🧐 Распространенной ошибкой является отслеживание события keyup. Такой подход не сработает, если:

  • Пользователь перетащит текст в <textarea>
  • Пользователь вставит в <textarea> текст из клипборда.
Выделить на фокусе текст в `textarea`
el.addEventListener('focus', (e) => {
  // Выделить текст
  e.target.select();
});
Перенести курсор в конец введенной строки

Сценарий использования

Нужно перенести курсор в конец введенного текста по нажатию кнопки Edit.

<input type="text" id="fullName" value="V. Nikishin">
<button id="edit">Edit</button>
const fullNameEle = document.getElementById('fullName');
const editEl = document.getElementById('edit');

editEl.addEventListener('click', (e) => {
  // Фокус на поле Ф.И.О.
  fullNameEle.focus();
  
  // Переносим курсор
  const length = fullNameEle.value.length;
  fullNameEle.setSelectionRange(length, length);
});
Отправить форму AJAX'ом, метод `POST`

Следующая функция отправит данные из формы formEle на сервер в AJAX-запросе.

const submit = (formEle) => {
  return new Promise(function (resolve, reject) {
    /* Вызов функции сериализации данных —
    см. раздел «Сериализация данных формы» */
    const params = serialize(formEle);    
    
    /* Создаем Ajax-запрос. Лучше использовать
    `fetch` — см. ниже */
    const req = new XMLHttpRequest();
    req.open('POST', formEle.action, true);
    req.setRequestHeader(
      'Content-Type',
      'application/x-www-form-urlencoded;charset=UTF-8'
    );    
    
    // Обработчики на загрузке и ошибке
    req.onload = () => {
      if (req.status >= 200 && req.status < 400) {
        resolve(req.responseText);
      }
    };
    req.onerror = function () {
      reject();
    };

    // Отправляем
    req.send(params);
  });
};

Применение

const formEle = document.getElementById('my-form');

submit(formEle).then((response) => {
  /* `response` — это то, что мы получим с сервера.
  Если пришел JSON, парсим */
  const data = JSON.parse(response);
  // И т.д.
});
Сериализация данных формы

Функция сериализует данные формы: названия и значения полей.

const serialize = (formEle) => {
  /* Получаем все элементы формы `formEle`
  и конвертируем их в массив, чтобы использовать
  методы: `map` и `join`. */
  const fields = [].slice.call(formEle.elements, 0);

  return fields
    .map((el) => {
      const { name } = el;
      const { type } = el;
      
      /* Игнорируем:
      - поля без атрибута `name`
      - поля с атрибутом `disabled`
      - поля типа `file`
      - неотмеченные `checkbox`/`radio` */
      if (
        !name ||
        el.disabled ||
        type === 'file' ||
        (/(checkbox|radio)/.test(type) && !el.checked)
      ) {
        return '';
      }

      // Множественный `select`
      if (type === 'select-multiple') {
        return el.options
          .map((opt) => {
            return opt.selected
              ? `${encodeURIComponent(name)}=
                 ${encodeURIComponent(opt.value)}`
              : '';
          })
          .filter((item) => {
            return item;
          })
          .join('&');
      }

      return `
        ${encodeURIComponent(name)}=
        ${encodeURIComponent(el.value)}`;
    })
    .filter((item) => {
      return item;
    })
    .join('&');
};
Динамическое изменение ширины поля ввода

Исходное поле ввода.

<input id="textbox" type="text" >

Чтобы изменять его по содержанию, создадим вспомогательный элемент, чьё содержание будет идентичным значению атрибута value поля. Затем будем менять ширину поля, измеря ширину вспомогательного элемента.

// Создаем div
const fakeEl = document.createElement('div');

// Прячем его
fakeEl.style.position = 'absolute';
fakeEl.style.top = '0';
fakeEl.style.left = '-9999px';
fakeEl.style.overflow = 'hidden';
fakeEl.style.visibility = 'hidden';
fakeEl.style.whiteSpace = 'nowrap';
fakeEl.style.height = '0';

/* Назначаем вспомогательному элементу те стили
поля ввода, которые влияют на ширину */
const textboxEl = document.getElementById('textbox');
const styles = window.getComputedStyle(textboxEl);

fakeEl.style.fontFamily = styles.fontFamily;
fakeEl.style.fontSize = styles.fontSize;
fakeEl.style.fontStyle = styles.fontStyle;
fakeEl.style.fontWeight = styles.fontWeight;
fakeEl.style.letterSpacing = styles.letterSpacing;
fakeEl.style.textTransform = styles.textTransform;
fakeEl.style.borderLeftWidth = styles.borderLeftWidth;
fakeEl.style.borderRightWidth = styles.borderRightWidth;
fakeEl.style.paddingLeft = styles.paddingLeft;
fakeEl.style.paddingRight = styles.paddingRight;

// Вставляем вспомогательный элемент в документ
document.body.appendChild(fakeEl);

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

const setWidth = () => {
  const string =
    textboxEl.value ||
    textboxEl.getAttribute('placeholder') ||
    '';
  fakeEl.innerHTML = string.replace(/\s/g, '&nbsp;');

  const fakeElStyles = window.getComputedStyle(fakeEl);
  textboxEl.style.width = fakeElStyles.width;
};

Вызываем функцию на загрузке страницы и на пользовательском вводе.

setWidth();

textboxEl.addEventListener('input', () => {
  setWidth();
});

AJAX

Получить данные с сервера — `fetch`

JavaScript может отправлять сетевые запросы на сервер и асинхронно подгружать новую информацию по мере необходимости. Сетевые запросы из JS широко известны под аббревиатурой AJAX (Asynchronous JavaScript And XML).

Мы можем использовать сетевой запрос, чтобы:

  • отправить заказ;
  • загрузить информацию о пользователе;
  • запросить последние обновления с сервера;
  • и т.п.

Практическая реализация сетевых запросов (AJAX) осуществляется с помощью двух API: XMLHttpRequest и более современного и удобного Fetch — аналога методов jQuery ajax() и get().

Первым аргументом методу fetch() передается URL. Вторым, необязательным — опции запроса. fetch() возвращает промис для обработки запроса.

fetch('/data.json')
  .then((data) => {
    // Обработчик данных
  })
  .catch((error) => {
    // Обработчик ошибки
  });

Пример. Открытие внешней ссылки в модальном окне Bootstrap

(() => {
  const modal = document.getElementById('modal');

  if (modal) {
    const container = modal.querySelector('.modal__box');

    modal.addEventListener('show.bs.modal', (event) => {
      // Индикатор загрузки
      const loader = document.createElement('div');
      loader.className = 'progress';
      loader.setAttribute('role', 'status');
      loader.innerHTML = 'Товар загружается...';
      document.body.appendChild(loader);

      // Получаем URL документа из атрибута `href`
      const href =
        event.relatedTarget.getAttribute('href');

      fetch(href)
        .then((response) => {
          // Получаем HTML как строку
          return response.text();
        })
        // Цепочка `then`
        .then((html) => {
          // Конвертируем строку в DOM-дерево
          const parser = new DOMParser();
          const doc =
            parser.parseFromString(html, 'text/html');

          /* Забераем из DOM нужную часть
          и вставляем в окно */
          const content =
            doc.querySelector('.is-modal-src');
          container.appendChild(content);

          // Удаляем индикатор
          loader.remove();
        });
    });

    // Очищаем модальное окно в момент закрытия
    modal.addEventListener('hidden.bs.modal', () => {
      container.innerHTML = '';
    });
  }
})();
Послать файлы на сервер — `fetch`

Для отправки файлов (иными словами POST-запроса) или запроса с методом, отличным от GET, необходимо использовать параметры fetch:

  • method – HTTP-метод: POST, PUT, DELETE, etc.
  • body – тело запроса. Чаще всего — JSON. Но может быть объект FormData, Blob для картинок и пр. бинарных данных, URLSearchParams.
const url = 'https://example.com/profile';
const data = { username: 'example' };

// Асинхронная функция. Всегда возвращает промис
(async () => {
  try {
    const response = await fetch(url, {
      method: 'POST',
      // данные могут быть 'строкой' или {объектом}
      body: JSON.stringify(data),
      headers: {
        'Content-Type': 'application/json',
      },
    });
    const json = await response.json();
    console.log('Успех:', JSON.stringify(json));
  } catch (error) {
    console.error('Ошибка:', error);
  }
})();
Послать файлы на сервер — `XMLHttpRequest`

☝️🧐 Лучше использовать fetch. Встроенный объект XMLHttpRequest приводится здесь только для полноты картины.

Функция upload посылает выбранные в fileEl файлы на сервер.

const upload = (fileEl, backendUrl) => {
  return new Promise((resolve, reject) => {
    // Получаем выбранные файлы
    const { files } = fileEl;

    /* Создаем объект FormData. Он позволяет
    конструировать наборы пар ключ-значение,
    представляющие поля формы и их значения,
    которые в дальнейшем можно отправить
    с помощью метода `send()`.
    https://mzl.la/3JpvKsr */
    const formData = new FormData();

    // Добавляем выбранные файлы в `formData`
    files.forEach((file) => {
      formData.append(fileEl.name, file, file.name);
    });

    // Создаем AJAX-запрос.
    const request = new XMLHttpRequest();
    request.open('POST', backendUrl, true);

    // Обработчики событий «успешно» и «ошибка»
    request.onload = () => {
      if (
      request.status >= 200 &&
      request.status < 400
    ) {
        resolve(request.responseText);
      }
    };
    request.onerror = () => {
      reject();
    };

    // Отправляем
    request.send(formData);
  });
};

См. также «Отправить форму AJAX’ом»

Общие проверки

Медиазапросы в JS

Для исполнения скриптов по результатам медиазапросов используется методы window.matchMedia и matches.

const title = document.getElementById('title');

if (window.matchMedia('(max-width: 400px)').matches) {
  title.classList.remove('is-huge');
  title.classList.add('is-small');
} else {
  title.classList.remove('is-small');
  title.classList.add('is-huge');
}

Определение ориентации устройства

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

if (window.matchMedia('(orientation: landscape)')
.matches) {
  // Do smth
} else {
  // Do smth else
}
Виден ли элемент в области просмотра браузера

IntersectionObserver

Для работы с Intersection Observer API (IOA) необходимо с помощью конструктора создать объект-наблюдатель с двумя параметрами — функцией обратного вызова и настройками.

if (
  window.matchMedia('(min-width: 768px)').matches
) {
  const { body } = document;
  const footer = document.getElementById('footer');

  const config = {
    threshold: 0.1,
    /* Если нужно отслеживать область видимости
    не всего браузера, а блока с прокруткой, тогда
    в опциях следует еще указать свойство `root`
    и передать в него нужный элемент, полученный,
    в общем случае, с помощью `getElementById` */
  };

  const observer =
    new IntersectionObserver((entries) => {
      /* В параметр `entries` передается массив
      записей, о целевых элементах, которые попали
      в поле видимости. В примере нас интересует
      только первый, с индексом `[0]` — «подвал».
      В условии проверяется, есть ли он
      в документе и видим ли сейчас в области
      просмотра. */
      if (entries[0] && entries[0].isIntersecting) {
        body.classList.add('has-visible-footer');
      } else {
        body.classList.remove('has-visible-footer');
      }
  }, config);

  // Вызов функции
  observer.observe(footer);
}

Каждая запись в массиве entries содержит свойства с изменениями, произошедшими с целевым элементом:

entries.forEach((entry) => {
  /*  entry (запись) - изменение
        entry.boundingClientRect
        entry.intersectionRatio
        entry.intersectionRect
        entry.isIntersecting
        entry.rootBounds
        entry.target
        entry.time */
});

В частности, свойство target (IntersectionObserverEntry.target) позволяет определить один из множества целевых элементов в момент появления в области видимости. То есть также помогает вызвать методы нужного объекта, как и event.target.

const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      if (entry.intersectionRatio >= 0.15) {
        // It's magic
        entry.target.classList.add('is-my-hero');
      }
    });
  },
  { threshold: 0.15 }
);

Если объект-наблюдатель должен отслеживать один UI-компонент, в параметр callback‘a можно передавать первый и единственный элемент массива — ([entry]) => {/*...*/}. Цикл не нужен.

const toggleTitleStyle = (el) => {
  const observer = new IntersectionObserver(
    ([entry]) =>
      entry.target.classList.toggle(
        'is-pinned',
        e.intersectionRatio < 1
      ),
    { threshold: [1] }
  );

  observer.observe(el);
};

Материалы по теме:

Альтернативный способ

const isInViewport = (el) => {
  const rect = el.getBoundingClientRect();
  return (
    rect.top >= 0 &&
    rect.left >= 0 &&
    rect.bottom <=
        (window.innerHeight ||
      document.documentElement.clientHeight
    ) && rect.right <= (
      window.innerWidth ||
      document.documentElement.clientWidth
    )
  );
};
Проверка: элемент в фокусе?
const hasFocus = el === document.activeElement;
Клик левой кнопкой или ПКМ?

Определять можно по событию mousedown или mouseup.

el.addEventListener('mousedown', (e) => {
  // e.button === 0: the left button is clicked
  // e.button === 1: the middle button is clicked
  // e.button === 2: the right button is clicked
  // e.button === 3: the `Browser Back` button is clicked
  // e.button === 4: it's the `Browser Forward` button
});
Проверка: набор в верхнем режиме

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

В примерах ниже проверяется ввод в input#textbox, а предупреждение выводится текстом в #message.

<input type="text" id="textbox">
<div id="message"></div>

1. Функция getModifierState()

getModifierState() возвращает состояние клавишей-модификаторов. В том числе — фиксатора верхнего режима Caps Lock.

const textboxEl = document.getElementById('textbox');
const messageEl = document.getElementById('message');

textboxEl.addEventListener('keydown', (e) => {
  const capsLockOn = e.getModifierState('CapsLock');

  // Обновляем предупреждение
  messageEl.innerHTML =
    capsLockOn ? 'Набираете прописными' : '';

  // Показываем или скрываем предупреждение
  messageEl.style.display =
    capsLockOn ? 'block' : 'none';
});

Добавляем проверку смены регистра с помощью клавиши Shift

getModifierState() не позволяет одновременно определить нажатый Shift. Поэтому добавляем еще один слушатель на keypress.

⚠️ MDN и VS Code считают метод navigator.platform устаревшим. И ходят слухи, что Chrome планирует отказаться от поддержки. Хотя в официальных документах таких планов найти не удалось. В качестве альтернативы MDN рекомендует использовать navigator.userAgentData. Однако поддержка браузерами нового метода пока сильно ограничена. Поэтому проверку на macOS делаем двойной. См. здесь и здесь.

textboxEl.addEventListener('keypress', (e) => {
  const isMac =
    /Mac/.test(navigator.userAgentData.platform) ||
    /Mac/.test(navigator.platform);

  const keyCode = e.keyCode || e.which;

  // Shift нажат?
  const shiftKey = e.shiftKey || keyCode === 16;

  // Получаем введенный символ
  const s = String.fromCharCode(keyCode);
  const capsLockOn =
    (s.toUpperCase() === s &&
      s.toLowerCase() !== s &&
      !(shiftKey && isMac)) ||
    (s.toUpperCase() !== s &&
      s.toLowerCase() === s &&
      shiftKey);

  messageEl.innerHTML =
    capsLockOn ? 'Набираете прописными' : '';
  messageEl.style.display =
    capsLockOn ? 'block' : 'none';
});
Выполнить код, когда DOM готов и все файлы загружены

Если по каким-то причинам JS нужно записывать в разделе head, а не в body после разметки. Можно установить на страницу слушатель события DOMContentLoaded. Получится аналог $(document).ready()($()) в jQuery.

document.addEventListener(
  'DOMContentLoaded',
  () => {
    // Do smth
  },
  false
);
Поддерживает ли браузер `input[type="date"]`?
const isDateInputSupported = () => {
  // Создаем новый input
  const el = document.createElement('input');
  
  // Задаем атрибут type со значениемdate
  el.setAttribute('type', 'date');
  
  const invalidValue = 'not-a-valid-date';
  
  // Передаем в value переменную invalidValue
  el.setAttribute('value', invalidValue);

  /* Если браузер поддерживает type='date', value
  останется пустым. В противном случае `el.value`
  вернет 'not-a-valid-date' */
  return el.value !== invalidValue;
};
Определение мобильного браузера
const isMobile = function () {
  const match = window.matchMedia('(pointer:coarse)');
  return match && match.matches;
};

Можно использовать и медиазапрос размера, но надо учитывать, что max-width мобильных устройств может достигать 1024px и выше.

if (window.matchMedia('(max-width: 400px)').matches) {
  // Do smth
}
Определение темной темы ОС

Темную тему операционной системы macOS или Windows 10 можно определить с помощью медиазапроса prefers-color-scheme и его значений: light, dark и no-preference

const isDarkMode =
  window.matchMedia &&
  window.matchMedia('(prefers-color-scheme: dark)').matches;
Определение операционной системы

Определение операционной системы.

if (navigator.userAgent.indexOf('Win') !== -1) { /* Windows */ };
if (navigator.userAgent.indexOf('Mac') !== -1) { /* MacOS */ };
if (navigator.userAgent.indexOf('X11') !== -1) { /* UNIX */ };
if (navigator.userAgent.indexOf('Linux') !== -1) { /* Linux */ };
Определение браузера в iOS / macOS

На MDN метод navigator.platform объявлен устаревшим. Вместо этого можно использовать navigator.userAgent — см. выше.

const isMacBrowser =
  /Mac|iPod|iPhone|iPad/.test(navigator.platform);
Определение прокрутки у блока

Функция isScrollable возвращает true, если блок ` scrollable — то есть содержание превышает границы, и браузер добавляет полосу прокрутки.

const isScrollable = (el) => {
  /* Первая проверка. Больше ли высота содержания
  высоты блока? */
  const hasScrollableContent =
    el.scrollHeight > el.clientHeight;

  /* `overflow-y` элемента может быть `hidden`. Полоса
  прокрутки в таком случае не показывается. Поэтому: */
  const overflowYStyle =
    window.getComputedStyle(el).overflowY;
  const isOverflowHidden =
    overflowYStyle.indexOf('hidden') !== -1;

  // Возвращаем булево значение
  return hasScrollableContent && !isOverflowHidden;
};
Определение направления выделения текста: слева направо или наоборот

Функция возвращает forward, если пользователь выделил текст слева направо, и backward, если наоборот.

const getDirection = () => {
  const selection = window.getSelection();
  const range = document.createRange();
  range.setStart(
    selection.anchorNode, selection.anchorOffset
  );
  range.setEnd(
    selection.focusNode, selection.focusOffset
  );

  return range.collapsed ? 'backward' : 'forward';
};
Выполняет ли код в браузере или где-то ещё?
const isBrowser =
  typeof window === 'object' &&
  typeof document === 'object';

Измерения

Получить размеры элемента
// Стили документа
const styles = window.getComputedStyle(el);

// Размер без рамки и padding
const height =
  el.clientHeight -
  parseFloat(styles.paddingTop) -
  parseFloat(styles.paddingBottom);
const width =
  el.clientWidth -
  parseFloat(styles.paddingLeft) -
  parseFloat(styles.paddingRight);

// Размер c padding
const { clientHeight } = el;
const { clientWidth } = el;

// Размер c рамкой и padding
const { offsetHeight } = el;
const { offsetWidth } = el;

// Размер c рамкой, padding и margin
const heightWithMargin =
  el.offsetHeight +
  parseFloat(styles.marginTop) +
  parseFloat(styles.marginBottom);
const widthWithMargin =
  el.offsetWidth +
  parseFloat(styles.marginLeft) +
  parseFloat(styles.marginRight);
Получить координаты элемента
/* Получить координаты 'top' и 'left'
относительно области просмотра */
const rect = el.getBoundingClientRect();

/* Приплюсовать дистанцию прокрутки
по вертикали и горизонтали */
const top = rect.top + document.body.scrollTop;
const left = rect.left + document.body.scrollLeft;
Позиция элемента относительно другого

Получим отступ элемента ` сверху и слева от элемента target.

// Координаты `top` и `left` обоих элементов
const eleRect = el.getBoundingClientRect();
const targetRect = target.getBoundingClientRect();

// Вычисляем отступы
const top = eleRect.top - targetRect.top;
const left = eleRect.left - targetRect.left;
Получить габариты изображения

Изображение уже загружено

const image = document.querySelector(...);

// Исходные размеры
const naturalWidth = image.naturalWidth;
const naturalHeight = image.naturalHeight;

// Размеры масштабированного изображения
const width = image.width;
const height = image.height;

Изображение еще не загружено

Первым делом создаем тег изображения и слушаем событие load, чтобы вычислить размер изображения, загружаемое с указанного URL…

const image = document.createElement('img');

image.addEventListener('load', (e) => {
  const width = e.target.width;
  const height = e.target.height;
});

/* …Устанавливаем атрибут `src` и колбэк
в слушателе вернет нам размеры */
image.src = '/path/to/image.png';

Мы можем использовать объект Promise, чтобы превратить предыдущий сниппет в функцию.

const calculateSize = (url) => {
  return new Promise((resolve, reject) => {
    const image = document.createElement('img');
    image.addEventListener('load', (e) => {
      resolve({
        width: e.target.width,
        height: e.target.height,
      });
    });
    
    image.addEventListener('error', () => {
      reject();
    });
    
    image.src = url;
  });
};

calculateSize('/path/to/image.png').then((data) => {
  const width = data.width;
  const height = data.height;
});

Сценарий использования

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

Поставим слушателя на поле #avatar[type="file"]. Загрузим выбранный файл через объект FileReader. Получим размеры с помощью созданной ранее функции calculateSize.

<input type="file" id="avatar">
<div id="size"></div>
const avatarEle = document.getElementById('avatar');
const sizeEle = document.getElementById('size');

avatarEle.addEventListener('change', (e) => {
  // Получаем выбранный файл
  const file = e.target.files[0];
  
  const reader = new FileReader();
  reader.readAsDataURL(file);
  reader.addEventListener('loadend', (e) => {
    const src = e.target.result;
    
    calculateSize(src).then((data) => {
      const width = data.width;
      const height = data.height;
      
      sizeEle.innerHTML = `${width} x ${height}`;
    });
  });
});
Получить ширину и высоту документа

В большинстве случаев достаточно offsetWidth и offsetHeight — свойств любого HTMLElement‘а. Однако, если задача требует предельной точности, можно выбрать из нескольких значений максимальное.

Получить ширину документа, включая полосу вертикальной прокрутки.

const fullWidth = Math.max(
  document.body.scrollWidth,
  document.documentElement.scrollWidth,
  document.body.offsetWidth,
  document.documentElement.offsetWidth,
  document.body.clientWidth,
  document.documentElement.clientWidth
);

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

const fullHeight = Math.max(
  document.body.scrollHeight,
  document.documentElement.scrollHeight,
  document.body.offsetHeight,
  document.documentElement.offsetHeight,
  document.body.clientHeight,
  document.documentElement.clientHeight
);
Получить файловый размер

Сценарий использования: проверка ограничений по размеру перед отправкой файла на сервер.

<input type="file" id="upload" />
<div id="size"></div>

Мы поставим слушателя на событие change поля [type="file"]. Размер в байтах можно получить из свойства size одного выбранного для загрузки файла.

Надпись с размером появляется, когда пользователь выбирает файл.

const fileEl = document.getElementById('upload');
const sizeEle = document.getElementById('size');

fileEl.addEventListener('change', function (e) {
  const files = e.target.files;
  if (files.length === 0) {
    // Спрятать надпись с размером, если файл не выбран
    sizeEle.innerHTML = '';
    sizeEle.style.display = 'none';
  } else {
    // Файловый размер в байтах
    sizeEle.innerHTML = `${files[0].size} Б`;
    
    // Показать надпись
    sizeEle.style.display = 'block';
  }
});

Размер в КБ, МБ, ГБ, ТБ

// Convert the file size to a readable format
const formatFileSize = function (bytes) {
  const sufixes = ['Б', 'КБ', 'MB', 'ГБ', 'ТБ'];
  const i = Math.floor(Math.log(bytes) / Math.log(1024));
  return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${sufixes[i]}`;
};

// Показать файловый размер
sizeEle.innerHTML = formatFileSize(files[0].size);
Абсолютное позиционирование элемента относительно другого

Выпадающие и контекстные меню, тултипы нужно как-то привязывать к элементам-триггерам. Для примера возьмём два элемента #target и #popover.

1. Проще всего: сделать всплывающую панель дочерним элементом и использовать CSS

<style>
.target {
  position: relative;
}

.popover {
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translate(-50%, .75rem);
}
</style>
  <!-- ... -->
<div class="target" id="target">
  <!-- Основное содержание триггера -->
  <div class="popover" id="popover">...</div>
</div>

Способ подходит в 80 процентах случаев, но не срабатывает, когда элемент-триггер имеет свойство overflow: hidden.

2. Рассчитывать координаты всплывающей панели относительно body

Можно сделать всплывающую панель прямым потомком body — либо сразу под триггером, а если это невозможно из-за общих предков — перед закрывающим тегом </body>

<body>
  <div id="target">...</div>
  <div id="popover">...</div>
</body>

В этом случае мы можем в JS рассчитать координаты панели, получив границы элементов методом getBoundingClientRect.

const target = document.getElementById('target');
const popover = document.getElementById('popover');

const targetRect = target.getBoundingClientRect();
const popoverRect = popover.getBoundingClientRect();

const top = targetRect.top + targetRect.height;
const left =
  targetRect.left +
  targetRect.width / 2 - popoverRect.width / 2;

/* Если нужно добавить «стрелочку-хвостик» панели,
к ее верхней координате приплюсуем нужное значение */
popover.style.top = `${top + 8}px`;
popover.style.left = `${left}px`;

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

Координаты клика
el.addEventListener('mousedown', (e) => {
  const target = e.target;

  // Границы цели
  const rect = target.getBoundingClientRect();

  // Координаты клика
  const x = e.clientX - rect.left;
  const y = e.clientY - rect.top;
});
Ширина полосы прокрутки

clientWidth — это ширина окна браузера без полосы прокрутки. offsetWidth — с полосой. Соответственно:

const scrollbarWidth =
  document.body.offsetWidth -
  document.body.clientWidth;
Расчет ширины надписи, набранной определенным шрифтом

1. C помощью метода measureText() элемента canvas

const measureWidth = (text, font) => {
  // Создаем холст
  const canvas = document.createElement('canvas');

  // Создаем контекст рендеринга
  const context = canvas.getContext('2d');

  // Устанавливаем шрифт
  context.font = font;

  // Передаем строку и измеряем текст
  const metrics = context.measureText(text);

  // Возвращаем ширину в пикселях
  return metrics.width;
};

2. С помощью временного элемента

const measureWidth = (text, font) => {
  // Создаем элемент
  const el = document.createElement('div');

  // Скрываем и запрещаем переносы
  el.style.position = 'absolute';
  el.style.visibility = 'hidden';
  el.style.whiteSpace = 'nowrap';
  el.style.left = '-9999px';

  // Устанавливаем шрифт и передаем строку
  el.style.font = font;
  el.innerText = text;

  // Вставляем в документ
  document.body.appendChild(el);

  // Получаем ширину
  const { width } = window.getComputedStyle(el);

  // Удаляем элемент
  document.body.removeChild(el);

  return width;
};

Документ, область просмотра, переходы

Получить или задать `title`
// Получить
const title = document.title;

// Задать
document.title = 'Hello World';
Переадресация на другую страницу
window.location.href = '/the/new/url';
Вернуться на предыдущую страницу
window.history.back();

// Или
window.history.go(-1);
Перегрузить страницу

Перегрузить и сохранить уже полученные данные POST-запроса

window.location.reload();

Перегрузить и не сохранять данные POST-запроса

window.location = window.location.href;
Прокрутка к элементу
el.scrollIntoView({ behavior: 'smooth' });

// ...Или неплавно, без замедления
el.scrollIntoView();

Опцию плавной прокрутки можно прописать и в CSS: scroll-behavior: smooth;

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

Мы можем прокрутить документ к любым указанным координатам…

window.scrollTo(pageX, pageY);

…В том числе — к нулевым, началу документа.

window.scrollTo(0, 0);

Для плавности можно использовать в стилях scroll-behavior: smooth или передать опции в параметры.

window.scrollTo({
  top: 1000,
  left: 0,
  behavior: 'smooth',
})

Сценарии использования

  • Кнопка «Наверх».
  • В SPA (одностраничных веб-приложениях) при «переходе» на другую страницу. Браузер сохраняет позцию документа в окне просмотра, поэтому, чтобы пользователь понял, что страница сменилось нужно прокрутить её вверх. Ниже показан пример React-приложения, использующего React Router.
import { useLocation } from 'react-router-dom';

export default ({ children }) => {
  const { pathname } = useLocation();

  React.useEffect(() => {
    window.scrollTo(0, 0);
  }, [pathname]);

  // Do smth...
};
Смена favicon сайта

Функция setFavicon меняет URL иконки.

const setFavicon = function (url) {
  // Получаем текущую иконку
  const favicon =
    document.querySelector('link[rel="icon"]');

  if (favicon) {
    // Если нашли — обновляем
    favicon.href = url;
  } else {
    // Не нашли — создаем
    const link = document.createElement('link');
    link.rel = 'icon';
    link.href = url;
    document.head.appendChild(link);
  }
};

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

setFavicon('/path/to/user/profile/icon.ico');

Использование эмодзи в качестве фавиконки

В метод setFavicon можно передавать помимо обычных веб-адресов и Data URL. Таким образом можно создать элемент canvas, добавить на него эмодзи, конвертировать в Data Url и передать в setFavicon.

const emojiFavicon = (emoji) => {
  // Создаем `canvas`
  const canvas = document.createElement('canvas');
  canvas.height = 64;
  canvas.width = 64;

  // Задаем параметры холста и переносим эмодзи
  const context = canvas.getContext('2d');
  context.font = '64px serif';
  context.fillText(emoji, 0, 64);
  
  // Конвертируем холст в Data URL
  const url = canvas.toDataURL();
  
  // Обновляем фавиконку
  setFavicon(url);
};

// Вызов функции
emojiFavicon('🎉');

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