1. Введение в JavaScript

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

JavaScript является интерпретируемым языком, это означает, что код на языке JavaScript выполняется с помощью интерпретатора. Он получает инструкции языка JavaScript, которые определены на web-странице, выполняет их (или интерпретирует).

2. Основы синтаксиса

2.1. Инструкции

Код JavaScript состоит из инструкций, каждая из которых завершается точкой запятой:

alert("Вычисление выражения"); var a = 5 + 8; alert(a);

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

alert("Вычисление выражения")
var a = 5 + 8
alert(a)

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

alert("Вычисление выражения");
var a = 5 + 8;
alert(a);

2.2. Комментарии

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

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

// вывод сообщения
alert("Вычисление выражения");
// арифметическая операция
var a = 5 + 8;
alert(a);

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

// вывод сообщения
/* вывод сообщения и
арифметическая операция */
alert("Вычисление выражения");
var a = 5 + 8;
alert(a);

2.3. Переменные

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

Для создания переменных применяются ключевые слова var и let. Например, объявим переменную myIncome:

var myIncome;
// другой вариант
let myIncome2;

Каждая переменная имеет имя, оно представляет собой произвольный набор алфавитно-цифровых символов, знака подчеркивания _ или знака доллара $, причем названия не должны начинаться с цифровых символов. То есть можно использовать в названии буквы, цифры, подчеркивание. Однако все остальные символы запрещены.

Например, правильные названия переменных:

$commision
someVariable
product_Store
income2
myIncome_from_deposit

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

222lol
@someVariable
my%percent

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

var for;

Список зарезервированных слов в JavaScript:

abstract, boolean, break, byte, case, catch, char, class, const, continue, debugger, default, delete, do, double, else, enum, export, extends, false, final, finally, float, for, function, goto, if, implements, import, in, instanceof, int, inteface, long, native, new, null, package, private, protected, public, return, short, static, super, switch, synchronized, this, throw, throws, transient, true, try, typeof, var, volatile, void, while, with

При названии переменных надо учитывать, что JavaScript является регистрозависимым языком, то есть в следующем коде объявлены две разные переменные:

var myIncome;
var MyIncome;

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

var myIncome, procent, sum;
let a, b, c;

С помощью знака равно = можно присвоить переменной какое-либо значение:

var income = 300;
let price = 76;

Процесс присвоения переменной начального значения называется инициализацией. Теперь переменная income будет хранить число 300, а переменная price - число 76. Отличительной чертой переменных является то, что можно изменить их значение:

var income = 300;
income = 400;
console.log(income);

let price = 76;
price = 54;
console.log(price);

2.4. Константы

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

const rate = 10;

Если попробовать изменить ее значение, то возникнет ошибка:

const rate = 10;
rate = 23;  // ошибка, rate - константа, поэтому  нельзя изменить ее значение

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

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

2.5. Типы данных

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

  • String: представляет строку

  • Number: представляет числовое значение

  • Boolean: представляет логическое значение true или false

  • undefined: указывает, что значение не установлено

  • null: указывает на неопределенное значение

Все данные, которые не попадают под вышеперечисленные пять типов, относятся к типу object.

2.5.1. Числовые данные

Числа в JavaScript могут иметь две формы:

  • Целые числа, например, 35. Можно использовать как положительные, так и отрицательные числа. Диапазон используемых чисел: от -2^53 до 2^53.

  • Дробные числа (числа с плавающей точкой), например, 3.5575. Опять же можно использовать как положительные, так и отрицательные числа. Для чисел с плавающей точкой используется тот же диапазон: от -2^53 до 2^53.

var x = 45;
var y = 23.897;

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

2.5.2. Строки

Тип string представляет строки, то есть такие данные, которые заключены в кавычки. Причем можно использовать как двойные, так и одинарные кавычки.

var helloWorld = "Привет мир";
var helloWorld2 = 'Привет мир';

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

var helloWorld = 'Привет мир"; // ошибка

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

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

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

var companyName1 = "Бюро 'Рога и копыта'";
var companyName2 = 'Бюро "Рога и копыта"';

2.5.3. Тип Boolean

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

var isAlive = true;
var isDead = false;

2.5.4. null и undefined

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

var isAlive;
console.log(isAlive);
undefined

Присвоение значение null означает, что переменная имеет некоторое неопределенное значение (не число, не строка, не логическое значение), но все-таки имеет значение. undefined означает, что переменная не имеет значения.

var isAlive;
console.log(isAlive); // undefined
isAlive = null;
console.log(isAlive); // null
isAlive = undefined;  // снова установим тип undefined
console.log(isAlive); // undefined

2.5.5. object

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

var user = {};

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

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

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

2.5.6. Слабая типизация

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

var xNumber; // тип `undefined`
console.log(xNumber);
xNumber = 45; // тип `number`
console.log(xNumber);
xNumber = "45"; // тип `string`
console.log(xNumber);
undefined
45
45

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

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

var xNumber = 45; // тип `number`
var yNumber = xNumber + 5;
console.log(yNumber);

xNumber = "45"; // тип `string`
var zNumber = xNumber + 5
console.log(zNumber);
50
455

Выше в обоих случая к переменной xNumber применяется операция сложения +. Но в первом случае xNumber представляет число, поэтому результатом операции xNumber + 5 будет число 50.

Во втором случае xNumber представляет строку. Но операция сложения между строкой и числом 5 невозможна. Поэтому число 5 будет преобразовываться к строке, и будет происходить операция объединения строк. И результатом выражения xNumber + 5 будет стока "455".

2.6. Операторы

2.6.1. Оператор typeof

С помощью оператора typeof можно получить тип переменной:

var name = "Tom";
console.log(typeof name);
var income = 45.8;
console.log(typeof income);
var isEnabled = true;
console.log(typeof isEnabled);
var undefVariable;
console.log(typeof undefVariable);
string
number
boolean
undefined

2.6.2. Математические операторы

JavaScript поддерживает все базовые математические операции:

  • Сложение:

var x = 10;
var y = x + 50;
  • Вычитание:

var x = 100;
var y = x - 50;
  • Умножение:

var x = 4;
var y = 5;
var z = x * y;
  • Деление:

var x = 40;
var y = 5;
var z = x / y;
  • Деление по модулю (оператор %) возвращает остаток от деления:

var x = 40;
var y = 7;
var z = x % y;
console.log(z);
5

Результатом будет 5, так как наибольшее целое число, которое меньше или равно 40 и при этом делится на 7 равно 35, а 40 - 35 = 5.

Инкремент
var x = 5;
x++;
console.log(x);
6

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

var x = 5;
var z = ++x;
console.log(x);
console.log(z);
6
6

И есть постфиксный инкремент, который сначала возвращает значение переменной, а затем увеличивает его на единицу:

var a = 5;
var b = a++;
console.log(a);
console.log(b);
6
5

Постфиксный инкремент аналогичен операции:

a = a + 1; // a++
Декремент

Декремент уменьшает значение переменной на единицу. Также есть префиксный и постфиксный декремент:

// префиксный декремент
var x = 5;
var z = --x;
console.log(x);
console.log(z);
4
4
// постфиксный декремент
var a = 5;
var b = a--;
console.log(a);
console.log(b);
4
5

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

var x = 10;
var y = 5 + (6 - 2) * --x;
console.log(y); //41

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

  • =
    Приравнивает переменной определенное значение: var x = 5;

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

var a = 23;
a += 5; // аналогично a = a + 5
console.log(a);
28
  • -=
    Вычитание с последующим присвоением результата.

var a = 28;
a -= 10; // аналогично a = a - 10
console.log(a);
18
  • *=
    *Умножение* с последующим присвоением результата:

var x = 20;
x *= 2; // аналогично x = x * 2
console.log(x);
40
  • /=
    Деление с последующим присвоением результата:

var x = 40;
x /= 4; // аналогично x = x / 4
console.log(x);
10
  • %=
    Получение остатка от деления с последующим присвоением результата:

var x = 10;
x %= 3; // аналогично x = x % 3
console.log(x);
1

2.6.4. Операторы сравнения

Как правило, для проверки условия используются операторы сравнения. Операторы сравнения сравнивают два значения и возвращают значение true или false:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Логические операции применяются для объединения результатов двух операций сравнения. В JavaScript есть следующие логические операции:

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

var income = 100;
var percent = 10;
var result = income > 50 && percent < 12;
console.log(result);
true
  • ||
    Возвращает true, если хотя бы одна операция сравнения возвращают true, иначе возвращает false:

var income = 100;
var isDeposit = true;
var result = income > 50 || isDeposit == true;
console.log(result);
true
  • !
    Возвращает true, если операция сравнения возвращает false:

var income = 100;
var result1 = !(income > 50);
console.log(result1);

var isDeposit = false;
var result2 = !isDeposit;
console.log(result2);
false
true

2.6.6. Операции со строками

Строки могут использовать оператор + для объединения.

var name = "Том";
var surname = "Сойер"
var fullname = name + " " + surname;
console.log(fullname);
Том Сойер

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

var name = "Том";
var fullname = name + 256;
console.log(fullname);
Том256

2.6.7. Пример

Программа, которая продемонстрирует работу с операциями над переменными.

<!DOCTYPE html>
<html>
    <head>
    <meta charset="utf-8" />
    <title>JavaScript</title>
</head>
<body>
    <script>
        var sum = 500; // сумма вклада
        var percent = 10;  // процент по вкладу
        var income = sum * percent / 100;  // доход по вкладу
        sum = sum + income; // определяем новую сумму
        console.log("Доход по вкладу: " + income);
        console.log("Сумма вклада после первого года: " + sum);
    </script>
</body>
</html>

В скрипте объявляются три переменных: sum, percent и income. Переменная income вычисляется по остальным двум переменным с помощью операций умножения и деления. И в конце ее значение суммируется со значением переменной sum.

И консоль браузера выведет:

Результат операций с переменными

2.7. Преобразование данных

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

var number1 = "46";
var number2 = "4";
var result = number1 + number2;
console.log(result);
464

Обе переменных представляют строки, а точнее строковые представления чисел. И в итоге получим не число 50, а строку 464. Но было бы неплохо, если бы их тоже можно было бы складывать, вычитать, в общем работать как с обычными числами.

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

var number1 = "46";
var number2 = "4";
var result = parseInt(number1) + parseInt(number2);
console.log(result);
50

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

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

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

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

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

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

var num1 = "javascript";
var num2 = "22";
var result = isNaN(num1);
console.log(result);

result = isNaN(num2);
console.log(result);
true
false

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

Преобразование в число в двоичной системе
var num1 = "110";
var num2 = parseInt(num1, 2);
console.log(num2);
6

Результатом будет 6, так как 110 в двоичной системе - это число 6 в десятичной.

2.7.1. Пример

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

<!DOCTYPE html>
    <html>
        <head>
        <meta charset="utf-8" />
        <title>JavaScript</title>
        </head>
    <body>
        <script>
            var strSum = prompt("Введите сумму вклада", 1000);
            var strPercent = prompt("Введите процентную ставку", 10);
            var sum = parseInt(strSum);
            var percent = parseInt(strPercent);
            sum = sum + sum * percent / 100;
            alert("После начисления процентов сумма вклада составит: " + sum);
        </script>
    </body>
</html>

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

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

После открытия страницы в браузере увидим приглашение к вводу суммы вклада:

приглашение к вводу суммы

Затем подобное сообщение отобразится и для ввода процента. И в конце программа получит данные, преобразует их в числа и выполнит подсчет:

результаты подсчета

2.8. Массивы

Для работы с наборами данных предназначены массивы. Для создания массива применяется выражение new Array():

var myArray = new Array();

Существует также более короткий способ инициализации массива:

var myArray = [];

В данном случае создаётся пустой массив. Но можно также добавить в него начальные данные:

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

В этом случае в массиве myArray будет три элемента. Его можно представить в виде таблицы так:

Индекс Элемент

0

Tom

1

Alice

2

Sam

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

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

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

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

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

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

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

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

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

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

2.8.1. spread-оператор

spread-оператор …​ позволяет взять значения из массива по отдельности:

let numbers = [1, 2, 3, 4];
console.log(...numbers);
console.log(numbers);
1 2 3 4
[1, 2, 3, 4]

spread-оператор указывается перед массивом. В результате выражение …​numbers возвратит набор чисел, но это будет не массив, а именно отдельные значения.

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

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

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

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

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

0

1

2

3

4

5

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

0

1

2

3

4

5

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

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

var people = [
        ["Tom", 25, false],
        ["Bill", 38, true],
        ["Alice", 21, false]
];

console.log(people[0]);
console.log(people[1]);
["Tom", 25, false]
["Bill", 38, true]

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

Tom

25

false

Bill

38

true

Alice

21

false

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

var tomInfo = people[0];

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

var people = [
        ["Tom", 25, false],
        ["Bill", 38, true],
        ["Alice", 21, false]
];

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

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

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

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

people[1] = ["Bob", 29, false]; // присваиваем массив
console.log(people[1][0]);
56
Bob

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

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

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

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

2.9.1. Выражение if

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

if (условие) действия;

Например:

var income = 100;
if (income > 50) alert("доход больше 50");

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

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

var income = 100;
if (income > 50) {
    var message = "доход больше 50";
    alert(message);
}

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

var income = 100;
var age = 19;
if (income < 150 && age > 18) {
    var message = "доход больше 50";
    alert(message);
}

Конструкция if позволяет проверить наличие значения.

var myVar = 89;
if (myVar) {
    // действия
}

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

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

if (typeof myVar != "undefined") {
    // действия
}

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

var age = 17;
if (age >= 18) {
    alert("Вы допущены к программе кредитования");
} else {
    alert("Вы не можете участвовать в программе, так как возраст меньше 18");
}

С помощью конструкции else if можно добавить альтернативное условие к блоку if:

var income = 300;
if (income < 200) {
    alert("Доход ниже среднего");
} else if (income >= 200 && income <= 400) {
    alert("Средний доход");
} else {
    alert("Доход выше среднего");
}

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

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

2.9.2. true или false

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

  • undefined
    Возвращает false

  • null
    Возвращает false

  • Boolean
    Если переменная равна false, то возвращается false. Соответственно, если переменная равна true, то возвращается true

  • Number
    Возвращает false, если число равно 0 или NaN (Not a Number), в остальных случаях возвращается true

var x = NaN;
if (x) {  // false
}
  • String
    Возвращает false, если переменная равна пустой строке, то есть ее длина равна 0, в остальных случаях возвращается true

var y = ""; // false - так как пустая строка
var z = "javascript"; // true - строка не пустая
  • Object
    Всегда возвращает true

var user = {name:"Tom"}; // true
var isEnabled = new Boolean(false); // true
var car = {}; // true

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

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

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

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

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

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

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

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

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

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

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

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

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

2.10. Циклы

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

  • for

  • for..in

  • for..of

  • while

  • do..while

2.10.1. Цикл for

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

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

Например, используем цикл for для перебора элементов массива:

var people = ["Tom", "Alice", "Bob", "Sam"];
for (var i = 0; i < people.length; i++) {
    console.log(people[i]);
}

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

Вторая часть - условие, при котором будет выполняться цикл. В данном случае цикл будет выполняться, пока значение i не достигнет величины, равной длине массива people. Получить длину массива можно с помощью свойства length: people.length.

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

И так как в массиве 4 элемента, то блок цикла сработает 4 раза, пока значение i не станет равным people.length (то есть 4). И каждый раз это значение будет увеличиваться на 1. Каждое отдельное повторение цикла называется итерацией. Таким образом, в данном случае сработают 4 итерации.

А с помощью выражения people[i] можно получить элемент массива для его последующего вывода в браузере.

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

var people = ["Tom", "Alice", "Bob", "Sam"];
for (var i = people.length - 1; i >= 0; i--) {
    console.log(people[i]);
}

В данном случае массив выводится с конца, а перебор массива начинается с i = 3 до i = 0.

2.10.2. Цикл for..in

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

for (индекс in массив) {
    // действия
}

Например, переберем элементы массива:

var people = ["Tom", "Alice", "Bob", "Sam"];
for (var index in people) {
    console.log(people[index]);
}

2.10.3. Цикл for…​of

Цикл for…​of похож на цикл for…​in и предназначен для перебора коллекций, например, массивов:

let users = ["Tom", "Bob", "Sam"];
for (let val of users) {
    console.log(val);
}

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

2.10.4. Цикл while

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

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

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

var people = ["Tom", "Alice", "Bob", "Sam"];
var index = 0;
while (index < people.length) {
    console.log(people[index]);
    index++;
}

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

2.10.5. Цикл do..while

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

var x = 1;
do {
    console.log(x * x);
    x++;
} while (x < 10)

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

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

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

var array = [ 1, 2, 3, 4, 5, 12, 17, 6, 7 ];
for (var i = 0; i < array.length; i++) {
    if (array[i] > 10) {
        break;
    }
    document.write(array[i] + "</br>");
}

Данный цикл перебирает все элементы массива, однако последние четыре элемента не будут выведены в браузере, поскольку поверка if (array[i] > 10) прервет выполнение цикла с помощью оператора break, когда перебор массива дойдет до элемента 12.

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

var array = [ 1, 2, 3, 4, 5, 12, 17, 6, 7 ];
for (var i = 0; i < array.length; i++) {
    if (array[i] > 10) {
        continue;
    }
    document.write(array[i] + "</br>");
}

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

3. Функциональное программирование

3.1. Функции

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

Синтаксис определения функции:

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

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

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

Определим простейшую функцию:

function display() {
    console.log("функция в JavaScript");
}

Данная функция называется display(). Она не принимает никаких параметров и все, что она делает, это пишет на web-страницу строку.

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

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
</head>
<body>
    <script>
        function display() {
            console.log("функция в JavaScript");
        }
        display();
    </script>
</body>
</html>

Необязательно давать функциям определенное имя. Можно использовать анонимные функции:

var display = function() { // определение функции
    console.log("функция в JavaScript");
}
display();
функция в JavaScript

Фактически определяем переменную display и присваиваем ей ссылку на функцию. А затем по имени переменной функция вызывается.

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

function goodMorning() {
    console.log("Доброе утро");
}

function goodEvening() {
    console.log("Добрый вечер");
}

var message = goodMorning;
message();
message = goodEvening;
message();
Доброе утро
Добрый вечер

3.1.1. Параметры функции

Рассмотрим передачу параметров:

function display(x) { // определение функции
    var z = x * x;
    console.log(x + " в квадрате равно " + z);
}
display(5); // вызов функции
5 в квадрате равно 25

Функция display принимает один параметр - x. Поэтому при вызове функции можно передать для него значение, например, число 5, как в данном случае.

Если функция принимает несколько параметров, то с помощью spread-оператора …​ можно передать набор значений для этих параметров из массива:

function sum(a, b, c) {
    let d = a + b + c;
    console.log(d);
}

sum(1, 2, 3);
let nums = [4, 5, 6];
sum(...nums);
6
15

Во втором случае в функцию передается числа из массива nums. Но чтобы передавался не просто массив, как одно значение, а именно числа из этого массива, применяется spread-оператор …​.

3.1.2. Необязательные параметры

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

function display(x, y) {
    if(y === undefined) y = 5;
    if(x === undefined) x = 8;
    let z = x * y;
    console.log(z);
}
display();
display(6);
display(6, 4);
40
30
24

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

Есть и другой способ определения значения для параметров по умолчанию:

function display(x = 5, y = 10) {
    let z = x * y;
    console.log(z);
}
display();
display(6);
display(6, 4);
50
60
24

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

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

function display(x = 5, y = 10 + x) {
    let z = x * y;
    console.log(z);
}
display();
display(6);
display(6, 4);
75
96
24

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

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

function display() {
    var z = 1;
    for (var i = 0; i < arguments.length; i++) {
        z *= arguments[i];
    }
    console.log(z);
}
display(6);
display(6, 4);
display(6, 4, 5);
6
24
120

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

3.1.3. Неопределенное количество параметров

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

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

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

Консольный вывод
Весна
-2
-3
4
2
5
Лето
20
23
31

Но нужно учесть, что каждое значение будет выведено с новой строки.

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

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

var y = 5;
var z = square(y);
console.log(y + " в квадрате равно " + z);

function square(x) {
    return x * x;
}
5 в квадрате равно 25

После оператора return идет значение, которое надо возвратить из метода. В данном случае это квадрат числа х.

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

var z = square(y);

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

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

function sum(x, y) {
    return x + y;
}

function subtract(x, y) {
    return x - y;
}

function operation(x, y, func) {
    var result = func(x, y);
    console.log(result);
}

console.log("Sum");
operation(10, 6, sum);
console.log("Subtract");
operation(10, 6, subtract);
Sum
16
Subtract
4

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

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

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

function menu(n) {
    if (n == 1)
        return function(x, y) { return x + y;}
    else if(n == 2)
        return function(x, y) { return x - y;}
    else if(n == 3)
        return function(x, y) { return x * y;}
    return undefined;
}

for (var i=1; i < 5; i++) {
    var action = menu(i);
    if (action !== undefined) {
        var result = action(5, 4);
        console.log(result);
    }
}
9
1
20

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

3.2. Область видимости переменных

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

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

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

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
</head>
<body>
    <script>
        var x = 5;
        let d = 8;
        function displaySquare() {
            var z = x * x;
            console.log(z);
        }
    </script>
</body>
</html>

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

3.2.2. Локальные переменные

Переменная, определенная внутри функции, является локальной:

function displaySquare() {
    var z = 10;
    console.log(z);

    let b = 8;
    console.log(b);
}

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

function displaySquare() {
    var z = 10;
    console.log(z);
}
console.log(z); // ошибка, так как z не определена

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

3.2.3. Сокрытие переменных

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

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

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

3.2.4. var или let

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

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

displayZ();
console.log("Global: ", z)

Здесь внутри функции displayZ() определен блок кода, в котором определена переменная z. Она скрывает глобальную переменную и переменную z, определенную на уровне функции. В реальной программе блок мог быть предоставлять вложенную функцию, блок цикла for или конструкции if. Но в любом случае такой блок определяет новую область видимости, вне которого переменная не существует.

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

Block: 30
Function: 20
Global: 10

С помощью оператора var можно определить одновременно переменную с одним и тем же именем и в функции, и в блоке кода в этой функции:

var c = 10;
function displaySquare() {
    var c = 20;
    {
        var c = 30;
        console.log("Block:", c);
    }
    console.log("Function:", c);
}
displaySquare()
console.log("Global:", c)
Block: 30
Function: 30
Global: 10

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

3.2.5. Константы

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

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

displayZ();
console.log("Global:", d);
Block: 30
Function: 20
Global: 10

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

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

function bar() {
    foo = "25";
}
bar();
console.log(foo);
25

Несмотря на то, что вне функции bar переменная foo нигде не определяется, тем не менее она доступна вне функции во внешнем контексте.

Иначе, если не только присвоить значение переменной, но и переопределить ее:

function bar() {
    var foo = "25";
}

bar();
console.log(foo);
Uncaught ReferenceError: foo is not defined

3.2.7. strict mode

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

"use strict";
function bar() {
    foo = "25";
}

bar();
console.log(foo);
Uncaught ReferenceError: assignment to undeclared variable foo

Установить режим strict mode можно двумя способами:

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

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

3.3. Замыкания

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

Замыкание технически включает три компонента:

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

  • Переменные (лексическое окружение), которые определены во внешней функции

  • Вложенная функция, которая использует эти переменные

function outer() { // внешняя функция
    var n; // некоторая переменная
    return inner() { // вложенная функция
        // действия с переменной n
    }
}

3.3.1. Пример

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

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

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

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

let fn = outer();

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

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

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

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

3.3.2. Пример

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

function multiply(n) {
    var x = n;
    return function(m) { return x * m;};
}
var fn1 = multiply(5);
var result1 = fn1(6);
console.log(result1);

var fn2= multiply(4);
var result2 = fn2(6);
console.log(result2);
30
24

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

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

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

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

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

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

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

var fn1 = multiply(5);

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

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

var result1 = fn1(6);

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

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

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

3.4. Функции IIFE (Самовызывающиеся функции)

Обычно определение функции отделяется от ее вызова: сначала определяем функцию, а потом вызываем. Но это необязательно. Также можем создать такие функции, которые будут вызываться сразу при определении. Такие функции еще называют Immediately Invoked Function Expression (IIFE).

(function() {
    console.log("Привет мир");
}());

(function (n) {
    var result = 1;
    for (var i = 1; i <= n; i++)
        result *=i;
    console.log("Факториал числа " + n + " равен " + result);
}(4));
Привет мир
Факториал числа 4 равен 24

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

3.5. Паттерн Модуль

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

Определим простейший модуль:

let foo = (function() {
    let obj = {greeting: "hello"};
    return {
        display: function() {
            console.log(obj.greeting);
        }
    }
})();
foo.display();
hello

Здесь определена переменная foo, которая представляет результат анонимной функции. Внутри подобной функции определен объект obj с некоторыми данными.

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

return {
    display: function() {
        console.log(obj.greeting);
    }
}

Такая конструкция позволяет закрыть некоторый набор данных в рамках функции-модуля и опосредовать доступ к ним через определенный API - возвращаемые внутренние функции.

Рассмотрим чуть более сложный пример:

let calculator = (function() {
    let data = { number: 0};

    return {
        sum: function(n) {
            data.number += n;
        },
        subtract: function(n) {
            data.number -= n;
        },
        display: function() {
            console.log("Result: ", data.number);
        }
    }
})();
calculator.sum(10);
calculator.sum(3);
calculator.display();
calculator.subtract(4);
calculator.display();
Result: 13
Result: 9

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

Все данные сокрыты в объекте data, который хранит результат операции. Все операции представлены тремя возвращаемыми функциями: sum(), subtract() и display(). Через эти функции можно управлять результатом калькулятора извне.

3.6. Рекурсивные функции

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

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

function getFactorial(n) {
    if (n === 1) {
        return 1;
    } else {
        return n * getFactorial(n - 1);
    }
}
var result = getFactorial(4);
console.log(result);
24

Функция getFactorial() возвращает значение 1, если параметр n равен 1, либо возвращает результат опять же функции getFactorial, то в нее передается значение n-1. Например, при передаче числа 4, у нас образуется следующая цепочка вызовов:

var result = 4 * getFactorial(3);
var result = 4 * 3 * getFactorial(2);
var result = 4 * 3 * 2 * getFactorial(1);
var result = 4 * 3 * 2 * 1; // 24

Рассмотрим другой пример - определение чисел Фибоначчи:

function getFibonachi(n) {
    if (n === 0) {
        return 0;
    }
    if (n === 1) {
        return 1;
    } else {
        return getFibonachi(n - 1) + getFibonachi(n - 2);
    }
}
var result = getFibonachi(8);
console.log(result);
21

3.7. Переопределение функций

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

function display() {
    console.log("Доброе утро");
    display = function() {
        console.log("Добрый день");
    }
}

display();
display();
Доброе утро
Добрый день

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

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

function display() {
    console.log("Доброе утро");
    display = function() {
        console.log("Добрый день");
    }
}
// присвоение ссылки на функцию до переопределения
var displayMessage = display;
display();
display();
displayMessage();
displayMessage();
Доброе утро
Добрый день
Доброе утро
Доброе утро

Здесь переменная displayMessage получает ссылку на функцию display() до ее переопределения. Поэтому при вызове displayMessage() будет вызываться не переопределенная версия функции display.

Но допустим, определили переменную displayMessage уже после вызова функции display():

display();
display();
var displayMessage = display;
displayMessage();
displayMessage();
Доброе утро
Добрый день
Добрый день
Добрый день

В этом случае переменная displayMessage будет указывать на переопределенную версию функции display().

3.8. Hoisting

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

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

Переменные, которые попадают под hoisting, получают значение undefined.

Например, возьмем следующий простейший код:

console.log(foo);

Его выполнение вызовет ошибку

Uncaught ReferenceError: foo is not defined

Добавим определение переменной:

console.log(foo);
var foo = "Tom";
undefined

В этом случае консоль выведет значение undefined. При первом проходе компилятор узнает про существование переменной foo. Она получает значение undefined. При втором проходе вызывается метод console.log(foo).

Возьмем другой пример:

var c = a * b;
var a = 7;
var b = 3;
console.log(c);
NaN

Здесь та же ситуация. Переменные a и b используются до определения. По умолчанию им присваиваются значения undefined. А если умножить undefined на undefined, то получим Not a Number (NaN).

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

display();

function display() {
    console.log("Hello Hoisting");
}
Hello Hoisting

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

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

display();

var display = function () {
    console.log("Hello Hoisting");
}

В данном случае получим ошибку

TypeError: display is not a function

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

Поэтому при определении переменных и функций следует учитывать такой аспект как hoisting.

3.9. Передача параметров по значению и по ссылке

3.9.1. Передача параметров по значению

Строки, числа, логические значения передаются в функцию по значению. Иными словами при передаче значения в функцию, эта функция получает копию данного значения. Рассмотрим, что это значит в практическом плане:

function change(x) {
    x = 2 * x;
    console.log("x in change:", x);
}

var n = 10;
console.log("n before change:", n);
change(n);
console.log("n after change:", n);
n before change: 10
x in change: 20
n after change: 10

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

3.9.2. Передача по ссылке

Объекты и массивы передаются по ссылке. То есть функция получает сам объект или массив, а не их копию.

function change(user) {
    user.name = "Tom";
}

var bob = {
    name: "Bob"
};
console.log("before change:", bob.name);
change(bob);
console.log("after change:", bob.name);
Bob
Tom

В данном случае функция change получает объект и меняет его свойство name. В итоге увидим, что после вызова функции изменился оригинальный объект bob, который передавался в функцию.

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

function change(user) {
    // полная переустановка объекта
    user= {
        name:"Tom"
    };
}

var bob = {
    name: "Bob"
};
console.log("before change:", bob.name);
change(bob);
console.log("after change:", bob.name);
Bob
Bob

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

function change(array) {
    array[0] = 8;
}

function changeFull(array) {
    array = [9, 8, 7];
}

var numbers = [1, 2, 3];

console.log("before change: ", numbers);
change(numbers);
console.log("after change: ", numbers);
changeFull(numbers);
console.log("after changeFull: ", numbers);
before change: [ 1, 2, 3 ]
after change: [ 8, 2, 3 ]
after changeFull: [ 8, 2, 3 ]

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

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

let sum = (x, y) => x + y;
let a = sum(4, 5); // 9
let b = sum(10, 5); // 15

В данном случае функция (x, y) => x + y осуществляет сложение двух чисел и присваивается переменной sum. Функция принимает два параметра - x и y. Ее тело составляет сложение значений этих параметров. И поскольку после стрелки фактически идет конкретное значение, которое представляет сумму чисел, то функция возвращает это значение. И можно через переменную sum вызвать данную функцию и получить ее результат в переменные a и b.

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

let sum = (x, y) => console.log(x + y);
sum(4, 5); // 9
sum(10, 5); // 15

В данном случае функция console.log() ничего не возвращает, и соответственно функция sum также не возвращает никакого результата.

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

var square = n => n * n;

console.log(square(5));
console.log(square(6));
console.log(square(-7));
25
36
49

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

var square = n => {
    let result = n * n;
    return result;
}

console.log(square(5));
25

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

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

let user = (userName, userAge) => ({name: userName, age: userAge});

let tom = user("Tom", 34);
let bob = user("Bob", 25);

console.log(tom.name, tom.age);
console.log(bob.name, bob.age);
Tom 34
Bob 25

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

Если стрелочная функция не принимает никаких параметров, то ставятся пустые скобки:

var hello = () => console.log("Hello World");
hello();
hello();
Hello World
Hello World

4. Реализация ООП в JavaScript

4.1. Объекты

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

4.1.1. Создание нового объекта

Есть несколько способов создания нового объекта.

Первый способ заключается в использовании конструктора Object:

var user = new Object();

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

Выражение new Object() представляет вызов конструктора - функции, создающей новый объект. Для вызова конструктора применяется оператор new. Вызов конструктора фактически напоминает вызов обычной функции.

Второй способ создания объекта представляет использование фигурных скобок:

var user = {};

Более распространенным является второй способ создания объекта.

4.1.2. Свойства объекта

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

var user = {};
user.name = "Tom";
user.age = 26;

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

console.log(user.name);
console.log(user.age);

Также можно определить свойства при определении объекта:

var user = {
    name: "Tom",
    age: 26
};

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

Кроме того, доступен сокращенный способ определения свойств:

var name = "Tom";
var age = 34;
var user = {name, age};
console.log(user.name);
console.log(user.age);

4.1.3. Методы объекта

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

var user = {};
user.name = "Tom";
user.age = 26;
user.display = function() {
    console.log(user.name);
    console.log(user.age);
};

// вызов метода
user.display();

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

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

var user = {
    name: "Tom",
    age: 26,
    display: function() {
        console.log(this.name);
        console.log(this.age);
    }
};

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

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

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

var user = {
    name: "Tom",
    age: 26,
    display() {
        console.log(this.name, this.age);
    },
    move(place) {
        console.log(this.name, "goes to", place);
    }
};

user.display();
user.move("the shop");
Tom 26
Tom goes to the shop

4.1.4. Синтаксис массивов

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

var user = {};
user["name"] = "Tom";
user["age"] = 26;
user["display"] = function() {
    console.log(user.name);
    console.log(user.age);
};

// вызов метода
user["display"]();

Название каждого свойства или метода заключается в кавычки и в квадратные скобки, затем им также присваивается значение. Например, user["age"] = 26.

При обращении к этим свойствам и методам можно использовать либо нотацию точки user.name, либо обращаться так: user["name"].

4.1.5. Строки в качестве свойств и методов

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

var user = {
    "name": "Tom",
    "age": 26,
    "display": function() {
        console.log(user.name);
        console.log(user.age);
    }
};
// вызов метода
user.display();

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

var user = {
    name: "Tom",
    age: 26,
    "full name": "Tom Johns",
    "display info": function() {
        console.log(user.name);
        console.log(user.age);
    }
};
console.log(user["full name"]);
user["display info"]();

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

4.1.6. Удаление свойств

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

delete объект.свойство

Либо использовать синтаксис массивов:

delete объект["свойство"]
var user = {};
user.name = "Tom";
user.age = 26;
user.display = function() {
    console.log(user.name);
    console.log(user.age);
};

console.log(user.name);
delete user.name; // удаляем свойство
// альтернативный вариант
// delete user["name"];
console.log(user.name);
Tom
undefined

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

4.2. Вложенные объекты и массивы в объектах

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

var country = {
    name: "Германия",
    language: "немецкий",
    capital: {
        name: "Берлин",
        population: 3375000,
        year: 1237
    }
};
console.log("Столица: " + country.capital.name);
console.log("Население: " + country["capital"]["population"]);
console.log("Год основания: " + country.capital["year"]);
Берлин
3375000
1237

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

country.capital.name

Либо обращаться к ним как к элементам массивов:

country["capital"]["population"]

Также допустим смешанный вид обращения:

country.capital["year"]

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

capital:{
        name: "Берн",
        population: 126598
    },
    cities: [
        { name: "Цюрих", population: 378884},
        { name: "Женева", population: 188634},
        { name: "Базель", population: 164937}
    ]
};

// вывод всех элементов из country.languages
document.write("<h3>Официальные языки Швейцарии</h3>");
for (var i = 0; i < country.languages.length; i++) {
    document.write(country.languages[i] + "<br>");
}

// вывод всех элементов из country.cities
document.write("<h3>Города Швейцарии</h3>");
for (var i = 0; i < country.cities.length; i++) {
    document.write(country.cities[i].name + "<br>");
}

В объекте country имеется свойство languages, содержащее массив строк, а также свойство cities, хранящее массив однотипных объектов.

С этими массивами можно работать так же, как и с любыми другими, например, перебрать с помощью цикла for.

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

country.cities[i].name

4.3. Проверка наличия и перебор методов и свойств

При динамическом определении в объекте новых свойств и методов перед их использованием бывает важно проверить, а есть ли уже такие методы и свойства. Для этого в JavaScript может использоваться оператор in:

var user = {};
user.name = "Tom";
user.age = 26;
user.display = function() {
    console.log(user.name);
    console.log(user.age);
};

var hasNameProp = "name" in user;
console.log(hasNameProp);
var hasWeightProp = "weight" in user;
console.log(hasWeightProp);
true
false

Оператор in имеет следующий синтаксис: "свойство|метод" in объект - в кавычках идет название свойства или метода, а после in - название объекта. Если свойство или метод с подобным именем имеется, то оператор возвращает true. Если нет - то возвращается false.

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

var hasNameProp = user.name !== undefined;
console.log(hasNameProp);
var hasWeightProp = user.weight !== undefined;
console.log(hasWeightProp);
true
false

И так как объекты представляют тип Object, а значит, имеет все его методы и свойства, то объекты также могут использовать метод hasOwnProperty(), который определен в типе Object:

var hasNameProp = user.hasOwnProperty('name');
console.log(hasNameProp);
var hasDisplayProp = user.hasOwnProperty('display');
console.log(hasDisplayProp);
var hasWeightProp = user.hasOwnProperty('weight');
console.log(hasWeightProp);
true
true
false

4.3.1. Перебор свойств и методов

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

var user = {};
user.name = "Tom";
user.age = 26;
user.display = function() {
    console.log(user.name);
    console.log(user.age);
};
for (var key in user) {
    console.log(key + " : " + user[key]);
}

4.4. Объекты в функциях

Функции могут возвращать значения. Но эти значения необязательно должны представлять примитивные данные - числа, строки, но также могут быть сложными объектами.

Например, вынесем создание объекта user в отдельную функцию:

function createUser(pName, pAge) {
    return {
        name: pName,
        age: pAge,
        displayInfo: function() {
            document.write("Имя: " + this.name + " возраст: " + this.age + "<br/>");
        }
    };
};
var tom = createUser("Tom", 26);
tom.displayInfo();
var alice = createUser("Alice", 24);
alice.displayInfo();

Здесь функция createUser() получает значения pName и pAge и по ним создает новый объект, который является возвращаемым результатом.

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

Кроме того объект может передаваться в качестве параметра в функцию:

function createUser(pName, pAge) {
    return {
        name: pName,
        age: pAge,
        displayInfo: function() {
            document.write("Имя: " + this.name + " возраст: " + this.age + "<br/>");
        },
        driveCar: function(car) {
            document.write(this.name + " ведет машину " + car.name + "<br/>");
        }
    };
};

function createCar(mName, mYear) {
    return {
        name: mName,
        year: mYear
    };
};
var tom = createUser("Том", 26);
tom.displayInfo();
var bently = createCar("Бентли", 2004);
tom.driveCar(bently);

4.5. Конструкторы объектов

Кроме создания новых объектов JavaScript предоставляет возможность создавать новые типы объектов с помощью конструкторов. Так, одним из способов создания объекта является применение конструктора типа Object:

var tom = new Object();

После создания переменной tom она будет вести себя как объект типа Object.

Конструктор позволяет определить новый тип объекта. Можно еще провести следующую аналогию.

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

function User(pName, pAge) {
    this.name = pName;
    this.age = pAge;
    this.displayInfo = function(){
        document.write("Имя: " + this.name + "; возраст: " + this.age + "<br>");
    };
}

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

Чтобы вызвать конструктор, то есть создать объект типа User, надо использовать ключевое слово new:

var tom = new User("Том", 26);
console.log(tom.name);
tom.displayInfo();
Том

4.5.1. Оператор instanceof

Оператор instanceof позволяет проверить, с помощью какого конструктора создан объект. Если объект создан с помощью определенного конструктора, то оператор возвращает true:

var tom = new User("Том", 26);
var isUser = tom instanceof User;
var isCar = tom instanceof Car;
console.log(isUser);
console.log(isCar);
true
false

4.6. Расширение объектов. Prototype

Кроме непосредственного определения свойств и методов в конструкторе мы также можем использовать свойство prototype. Каждая функция имеет свойство prototype, представляющее прототип функции. То есть свойство User.prototype представляет прототип объектов User. И любые свойства и методы, которые будут определены в User.prototype, будут общими для всех объектов User.

Например, после определения объекта User необходимо добавить к нему метод и свойство:

function User(pName, pAge) {
    this.name = pName;
    this.age = pAge;
    this.displayInfo = function() {
        document.write("Имя: " + this.name + "; возраст: " + this.age + "<br/>");
    };
};

User.prototype.hello = function() {
    document.write(this.name + " говорит: 'Привет!'<br/>");
};
User.prototype.maxAge = 110;

var tom = new User("Том", 26);
tom.hello();
var john = new User("Джон", 28);
john.hello();
console.log(tom.maxAge);
console.log(john.maxAge);
110
110

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

4.7. Инкапсуляция

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

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

function User (name) {
    this.name = name;
    var _age = 1;
    this.displayInfo = function() {
        console.log("Имя: " + this.name + "; возраст: " + _age);
    };
    this.getAge = function() {
        return _age;
    }
    this.setAge = function(age) {
        if (typeof age === "number" && age > 0 && age < 110) {
            _age = age;
        } else {
            console.log("Недопустимое значение");
        }
    }
}

var tom = new User("Том");
console.log(tom._age);
console.log(tom.getAge());
tom.setAge(32);
console.log(tom.getAge());
tom.setAge("54");
tom.setAge(123);
undefined
1
32
Недопустимое значение
Недопустимое значение

В конструкторе User объявляется локальная переменная _age вместо свойства age. Как правило, названия локальных переменных в конструкторах начинаются со знака подчеркивания.

Для того, что бы работать с возрастом пользователя извне, определяются два метода. Метод getAge() предназначен для получения значения переменной _age. Этот метод еще называется геттер (getter). Второй метод - setAge, который еще называется сеттер (setter), предназначен для установки значения переменной _age.

4.8. Функция как объект. Методы call() и apply()

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

Например, мы можем создать функцию с помощью конструктора Function:

var square = new Function('n', 'return n * n;');
console.log(square(5));

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

Среди свойств объекта Function можно выделить следующие:

  • arguments: массив аргументов, передаваемых в функцию

  • length: определяет количество аргументов, которые ожидает функция

  • caller: определяет функцию, вызвавшую текущую выполняющуюся функцию

  • name: имя функции

  • prototype: прототип функции

С помощью прототипа можно определить дополнительные свойства:

function display() {
    console.log("привет мир");
}
Function.prototype.program ="Hello";

console.log(display.program);
Hello

Среди методов надо отметить методы call() и apply().

Метод call() вызывает функцию с указанным значением this и аргументами:

function add(x, y) {
    return x + y;
}
var result = add.call(this, 3, 8);

console.log(result);
11

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

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

function User (name, age) {
    this.name = name;
    this.age = age;
}
var tom = new User("Том", 26);
function display(){
    console.log("Ваше имя: " + this.name);
}
display.call(tom);
Ваше имя: Том

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

На метод call() похож метод apply(), который так же вызывает функцию. В качестве первого параметра также получает объект, для которого функция вызывается. Только теперь в качестве второго параметра передается массив аргументов:

function add(x, y) {
    return x + y;
}
var result = add.apply(null, [3, 8]);

console.log(result);
11

4.9. Наследование

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

// конструктор пользователя
function User (name, age) {
    this.name = name;
    this.age = age;
    this.go = function() {
        document.write(this.name + " идет <br/>");
    }
    this.displayInfo = function() {
        document.write("Имя: " + this.name + "; возраст: " + this.age + "<br/>");
    };
}
User.prototype.maxage = 110;

// конструктор работника
function Employee(name, age, comp) {
    User.call(this, name, age);
    this.company = comp;
    this.displayInfo = function() {
        document.write("Имя: " + this.name + "; возраст: " + this.age + "; компания: " + this.company + "<br/>");
    };
}
Employee.prototype = Object.create(User.prototype);

var tom = new User("Том", 26);
var bill = new Employee("Билл", 32, "Google");
tom.go();
bill.go();
tom.displayInfo();
bill.displayInfo();
console.log(bill.maxage);

В конструкторе Employee происходит обращение к конструктору User с помощью вызова:

User.call(this, name, age);

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

Кроме того, необходимо унаследовать также и прототип User. Для этого служит вызов:

Employee.prototype = Object.create(User.prototype);

Метод Object.create() позволяет создать объект прототипа User, который затем присваивается прототипу Employee. При этом при необходимости в прототипе Employee мы также можем определить дополнительные свойства и методы.

При наследовании мы можем переопределять наследуемый функционал.

4.10. Ключевое слово this

Поведение ключевого слова this зависит от контекста, в котором оно используется, и от того, в каком режиме оно используется - строгом или нестрогом.

4.10.1. Глобальный контекст

В глобальном контексте this ссылается на глобальный объект. В данном случае поведение не зависит от режима (строгий или нестрогий):

this.alert("global alert");
this.console.log("global console");

var currentDocument = this.document;

4.10.2. Контекст функции

В пределах функции this ссылается на внешний контекст. Для функций, определенных в глобальном контексте, - это объект Window, который представляет окно браузера.

function foo() {
    var bar = "bar2";
    console.log(this.bar);
}

var bar = "bar1";

foo();  // bar1

Если не использовать this, то обращение шло бы к локальной переменной, определенной внутри функции.

function foo() {
    var bar = "bar2";
    console.log(bar);
}

var bar = "bar1";

foo(); // bar2

Но если использовать строгий режим (strict mode), то this в этом случае имело бы значение undefined:

"use strict";
var obj = {
    function foo() {
        var bar = "bar2";
        console.log(this.bar);
    }
}

var bar = "bar1";

foo(); // ошибка - this - undefined

4.10.3. Контекст объекта

В контексте объекта, в том числе в его методах, ключевое слово this ссылается на этот же объект:

var o = {
    bar: "bar3",
    foo: function() {
        console.log(this.bar);
    }
}
var bar = "bar1";
o.foo();
bar3

4.10.4. Явная привязка

С помощью методов call() и apply() можно задать явную привязку функции к определенному контексту:

function foo() {
    console.log(this.bar);
}

var o3 = {bar: "bar3"}
var bar = "bar1";
foo();
foo.apply(o3);
// или
// foo.call(o3);
bar1
bar3

4.10.5. Метод bind

Метод f.bind(o) позволяет создать новую функцию с тем же телом и областью видимости, что и функция f(), но с привязкой к объекту o:

function foo() {
    console.log(this.bar);
}

var o3 = {bar: "bar3"}
var bar = "bar1";
foo();
var func = foo.bind(o3);
func();
bar1
bar3

4.10.6. this и стрелочные функции

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

var school = {
    title: "Oxford",
    courses: ["JavaScript", "TypeScript", "Java", "Go"],
    printCourses: function() {
        this.courses.forEach(function(course) {
            console.log(this.title, course);
        })
    }
}
school.printCourses();
undefined "JavaScript"
undefined "TypeScript"
undefined "Java"
undefined "Go"

Видно, что значение this.title не определено, так как this как контекст объекта замещается глобальным контекстом. В этом случае нам надо передать подобное значение this.title или весь контекст объекта.

var school = {
    title: "Oxford",
    courses: ["JavaScript", "TypeScript", "Java", "Go"],
    printCourses: function() {
        var that = this;
        this.courses.forEach(function(course) {
            console.log(that.title, course);
        })
    }
}
school.printCourses();

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

var school = {
    title: "Oxford",
    courses: ["JavaScript", "TypeScript", "Java", "Go"],
    printCourses: function() {
        this.courses.forEach((course) => console.log(this.title, course))
    }
}
school.printCourses();

Контекстом для стрелочной функции в данном случае будет выступать контекст объекта school. Соответственно, не надо определять дополнительные переменные для передачи данных в функцию.

4.11. Декомпозиция

Декомпозиция (destructuring) позволяет извлечь из объекта отдельные значения в переменные:

let user = {
    name: "Tom",
    age: 24,
    phone: "+367438787",
    email: "tom@gmail.com"
};
let {name, email} = user;
console.log(name);
console.log(email);
Tom
tom@gmail.com

Для декомпозиции объекта переменные помещаются в фигурные скобки и им присваивается объект. Сопоставление между свойствами объекта и переменными идет по имени.

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

let user = {
    name: "Tom",
    age: 24,
    phone: "+367438787",
    email: "tom@gmail.com"
};
let {name: userName, email: mailAddress} = user;
console.log(userName);
console.log(mailAddress);
Tom
tom@gmail.com

4.11.1. Декомпозиция массивов

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

let users = ["Tom", "Sam", "Bob"];
let [a, b, c] = users;

console.log(a);
console.log(b);
console.log(c);
Tom
Sam
Bob

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

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

let users = ["Tom", "Sam", "Bob", "Ann", "Alice", "Kate"];
let [first,,,,fifth] = users;

console.log(first);
console.log(fifth);
Tom
Alice

4.11.2. Декомпозиция параметров

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

function display({name:userName, age:userAge}) {
    console.log(userName, userAge);
}
function sum([a, b, c]) {
    var result = a + b + c;
    console.log(result);
}
let user = {name:"Alice", age:33, email: "alice@gmail.com"};

let numbers = [3, 5, 7, 8];

display(user);
sum(numbers);
Alice 33
15

4.12. Классы

С внедрением стандарта ES2015 (ES6) в JavaScript появился новый способ определения объектов - с помощью классов. Класс представляет описание объекта, его состояния и поведения, а объект является конкретным воплощением или экземпляром класса.

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

class Person {
}

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

let Person = class {}

После этого можно создать объекты класса с помощью конструктора:

class Person {}

let tom = new Person();
let bob = new Person();

Для создания объекта с помощью конструктора сначала ставится ключевое слово new. Затем собственно идет вызов конструктора - по сути вызов функции по имени класса. По умолчанию классы имеют один конструктор без параметров. Поэтому в данном случае при вызове конструктора в него не передается никаких аргументов.

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

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    display() {
        console.log(this.name, this.age);
    }
}

let tom = new Person("Tom", 34);
tom.display();
console.log(tom.name);
Tom 34
Tom

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

Для хранения состояния в классе определяются свойства. Для их определения используется ключевое слово this. В данном случае в классе два свойства: name и age.

Поведение класса определяют методы. В данном случае определен метод display(), который выводит значения свойств на консоль.

4.12.1. Наследование

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

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    display() {
        console.log(this.name, this.age);
    }
}

class Employee extends Person {
    constructor(name, age, company) {
        super(name, age);
        this.company = company;
    }

    display() {
        super.display();
        console.log("Employee in", this.company);
    }

    work() {
        console.log(this.name, "is hard working");
    }
}

let tom = new Person("Tom", 34);
let bob = new Employee("Bob", 36, "Google");
tom.display();
bob.display();
bob.work();
Tom 34
Bob 36
Employee in Google
Bob is hard working

Для наследования одного класса от другого в определении класса применяется оператор extends, после которого идет название базового класса. То есть в данном случае класс Employee наследуется от класса Person. Класс Person еще называется базовым классом, классом-родителем, суперклассом, а класс Employee - классом-наследником, подклассом, производным классом.

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

4.12.2. Статические методы

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

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    static nameToUpper(person) {
        return person.name.toUpperCase();
    }

    display() {
        console.log(this.name, this.age);
    }
}
let tom = new Person("Tom Soyer", 34);
let personName = Person.nameToUpper(tom);
console.log(personName);
TOM SOYER

5. Встроенные объекты

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

5.1. Объект Date. Работа с датами

Объект Date позволяет работать с датами и временем в JavaScript.

Существуют различные способы создания объекта Date. Первый способ заключается в использовании пустого конструктора без параметров:

var currentDate = new Date();
console.log(currentDate);
Sun Jul 11 2021 07:52:42 GMT+0300 (Moscow Standard Time)

В этом случае объект будет указывать на текущую дату компьютера.

Второй способ заключается в передаче в (конструктор) Date количества миллисекунд, которые прошли с начала эпохи Unix, то есть с 1 января 1970 года 00:00:00 GMT:

var myDate = new Date(1359270000000);
console.log(myDate);
Sun Jan 27 2013 10:00:00 GMT+0300 (Moscow Standard Time)

Третий способ состоит в передаче в конструктор Date дня, месяца и года:

var myDate = new Date("27 March 2021");
// или так
// var myDate = new Date("3/27/2021");
console.log(myDate);
Thu Mar 27 2021 00:00:00 GMT+0200 (Moscow Standard Time)

Если используется полное название месяца, то оно пишется в по-английски, если используем сокращенный вариант, тогда используется формат месяц/день/год.

Четвертый способ состоит в передаче в конструктор Date всех параметров даты и времени:

var myDate = new Date(2021, 11, 25, 18, 30, 20, 10);
console.log(myDate)
Sat Dec 25 2021 18:30:20 GMT+0300 (Moscow Standard Time)

При этом надо учитывать, что отсчет месяцев начинается с нуля, то есть январь - 0, а декабрь - 11.

5.1.1. Получение даты и времени

Для получения различных компонентов даты применяется ряд методов:

  • getDate(): возвращает день месяца

  • getDay(): возвращает день недели (отсчет начинается с 0 - воскресенье, и последний день - 6 - суббота)

  • getMonth(): возвращает номер месяца (отсчет начинается с нуля, то есть месяц с номер 0 - январь)

  • getFullYear(): возвращает год

  • toDateString(): возвращает полную дату в виде строки

  • getHours(): возвращает час (от 0 до 23)

  • getMinutes(): возвращает минуты (от 0 до 59)

  • getSeconds(): возвращает секунды (от 0 до 59)

  • getMilliseconds(): возвращает миллисекунды (от 0 до 999)

  • toTimeString(): возвращает полное время в виде строки

5.1.2. Установка даты и времени

Коме задания параметров даты в конструкторе для установки мы также можем использовать дополнительные методы объекта Date:

  • setDate(): установка дня в дате

  • setMonth(): уставовка месяца (отсчет начинается с нуля, то есть месяц с номер 0 - январь)

  • setFullYear(): устанавливает год

  • setHours(): установка часа

  • setMinutes(): установка минут

  • setSeconds(): установка секунд

  • setMilliseconds(): установка миллисекунд

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

5.2. Объект Math

5.2.1. Математические операции

Объект Math предоставляет ряд математических функций, которые можно использовать при вычислениях. Рассмотрим основные математические функции.

  • abs(): возвращает абсолютное значение числа.

  • min() и max(): возвращают соответственно минимальное и максимальное значение из набора чисел.

  • ceil(): округляет число до следующего наибольшего целого числа.

  • floor(): округляет число до следующего наименьшего целого числа.

  • round(): округляет число до следующего наименьшего целого числа, если его десятичная часть меньше 0.5. Если же десятичная часть равна или больше 0.5, то округление идет до ближайшего наибольшего целого числа.

  • random(): возвращает случайное число с плавающей точкой из диапазона от 0 до 1.

  • pow(): возвращает число в определенной степени.

  • sqrt(): возвращает квадратный корень числа.

  • log(): возвращает натуральный логарифм числа.

5.2.2. Тригонометрические функции

Целый ряд функций представляют тригонометрические функции:

  • sin() - вычисляет синус угла

  • cos() - вычисляет косинус угла

  • tan() - вычисляет тангенс угла

В качестве значения они принимают значение в радианах.

5.2.3. Константы

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

  • Math.PI - число PI: 3.141592653589793

  • Math.SQRT2 - квадратный корень из двух: 1.4142135623730951

  • Math.SQRT1_2 - половина от квадратного корня из двух: 0.7071067811865476

  • Math.E - число e или число Эйлера: 2.718281828459045

  • Math.LN2 - натуральный логарифм числа 2: 0.6931471805599453

  • Math.LN10 - натуральный логарифм числа 10: 2.302585092994046

  • Math.LOG2E - двоичный логарифм числа e: 1.4426950408889634

  • Math.LOG10E - десятичный логарифм числа e: 0.4342944819032518

5.3. Объект Array. Работа с массивами

Объект Array представляет массив и предоставляет ряд свойств и методов, с помощью которых мы можем управлять массивом.

5.3.1. Инициализация массива

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

var users = new Array();
var people = [];

console.log(users);
console.log(people);
Array[0]
Array[0]

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

var users = new Array("Tom", "Bill", "Alice");
var people = ["Sam", "John", "Kate"];

console.log(users);
console.log(people);
["Tom", "Bill", "Alice"]
["Sam", "John", "Kate"]

Можно определить массив и по ходу определять в него новые элементы:

var users = new Array();
users[1] = "Tom";
users[2] = "Kate";
console.log(users[1]);
console.log(users[0]);
Tom
undefined

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

5.3.2. length

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

var fruit = new Array();
fruit[0] = "яблоки";
fruit[1] = "груши";
fruit[2] = "сливы";

console.log("В массиве fruit " + fruit.length + " элемента: <br>");
for (var i = 0; i < fruit.length; i++) {
    console.log(fruit[i]);
}

5.3.3. Копирование массива. slice()

Копирование массива может быть поверхностным или неглубоким (shallow copy) и глубоким (deep copy).

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

var users = ["Tom", "Sam", "Bill"];
console.log(users);
var people = users; // неглубокое копирование

people[1] = "Mike"; // изменяем второй элемент
console.log(users);
["Tom", "Sam", "Bill"]
["Tom", "Mike", "Bill"]

Такое поведение не всегда является желательным. Например, необходимо, чтобы после копирования переменные указывали на отдельные массивы. И в этом случае можно использовать глубокое копирование с помощью метода slice():

var users = ["Tom", "Sam", "Bill"];
console.log(users);
var people = users.slice(); // глубокое копирование

people[1] = "Mike"; // изменяем второй элемент
console.log(users);
console.log(people);
["Tom", "Sam", "Bill"]
["Tom", "Sam", "Bill"]
["Tom", "Mike", "Bill"]

Также метод slice() позволяет скопировать часть массива:

ar users = ["Tom", "Sam", "Bill", "Alice", "Kate"];
var people = users.slice(1, 4);
console.log(people);
["Sam", "Bill", "Alice"]

5.3.4. push()

Метод push() добавляет элемент в конец массива.

5.3.5. pop()

Метод pop() удаляет последний элемент из массива.

5.3.6. shift()

Метод shift() извлекает и удаляет первый элемент из массива.

5.3.7. unshift()

Метод unshift() добавляет новый элемент в начало массива.

5.3.8. Удаление элемента по индексу. splice()

Метод splice() удаляет элементы с определенного индекса. Например, удаление элементов с третьего индекса:

var users = ["Tom", "Sam", "Bill", "Alice", "Kate"];
var deleted = users.splice(3);
console.log(deleted);
console.log(users);
["Alice", "Kate"]
["Tom", "Sam", "Bill"]

Метод splice() возвращает удаленные элементы.

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

var users = ["Tom", "Sam", "Bill", "Alice", "Kate"];
var deleted = users.splice(-1);
console.log(deleted);
console.log(users);
[ "Kate" ]
[ "Tom", "Sam", "Bill", "Alice" ]

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

var users = ["Tom", "Sam", "Bill", "Alice", "Kate"];
var deleted = users.splice(1, 3);
console.log(deleted);
console.log(users);
[ "Sam", "Bill", "Alice" ]
[ "Tom", "Kate" ]

Еще одна версия метода splice() позволяет вставить вместо удаляемых элементов новые элементы:

var users = ["Tom", "Sam", "Bill", "Alice", "Kate"];
var deleted = users.splice(1, 3, "Ann", "Bob");
console.log(deleted);
console.log(users);
[ "Sam", "Bill", "Alice" ]
[ "Tom", "Ann", "Bob", "Kate" ]

5.3.9. concat()

Метод concat() служит для объединения массивов:

var fruit = ["яблоки", "груши", "сливы"];
var vegetables = ["помидоры", "огурцы", "картофель"];
var products = fruit.concat(vegetables);

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

Также можно объединять разнотипные массивы.

5.3.10. join()

Метод join() объединяет все элементы массива в одну строку:

var fruit = ["яблоки", "груши", "сливы", "абрикосы", "персики"];
var fruitString = fruit.join(", ");
console.log(fruitString);
яблоки, груши, сливы, абрикосы, персики

В метод join() передается разделитель между элементами массива. В данном случае в качестве разделителя будет использоваться запятая , и пробел ` `).

5.3.11. sort()

Метод sort() сортирует массив по возрастанию.

5.3.12. reverse()

Метод reverse() переворачивает массив задом наперед. В сочетании с методом sort() можно отсортировать массив по убыванию.

5.3.13. Поиск индекса элемента

indexOf() и lastIndexOf()

Методы indexOf() и lastIndexOf() возвращают индекс первого и последнего включения элемента в массиве.

every()

Метод every() проверяет, все ли элементы соответствуют определенному условию:

var numbers = [ 1, -12, 8, -4, 25, 42 ];
function condition(value, index, array) {
    var result = false;
    if (value > 0) {
        result = true;
    }
    return result;
};
var passed = numbers.every(condition);
console.log(passed);
false

В метод every() в качестве параметра передается функция, представляющая условие.

function condition(value, index, array) {
}

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

some()

Метод some() похож на метод every(), только он проверяет, соответствует ли хотя бы один элемент условию. И в этом случае метод some() возвращает true. Если элементов, соответствующих условию, в массиве нет, то возвращается значение false.

filter()

Метод filter(), как some() и every(), принимает функцию условия. Но при этом возвращает массив* тех элементов, которые соответствуют этому условию.

forEach() и map()

Методы forEach() и map() осуществляют перебор элементов и выполняют с ними определенный операции. Например, для вычисления квадратов чисел в массиве можно использовать следующий код:

var numbers = [ 1, 2, 3, 4, 5, 6];
for (var i = 0; i<numbers.length; i++) {
    var result = numbers[i] * numbers[i];
    console.log("Квадрат числа " + numbers[i] + " равен " + result );
}

Но с помощью метода forEach() можно упростить эту конструкцию:

var numbers = [ 1, 2, 3, 4, 5, 6];

function square(value) {
    var result = value * value;
    console.log("Квадрат числа " + value + " равен " + result );
};

numbers.forEach(square);

Метод forEach() в качестве параметра принимает все ту же функцию, в которую при переборе элементов передается текущий перебираемый элемент и над ним выполняются операции.

Метод map() похож на метод forEach(), он также в качестве параметра принимает функцию, с помощью которой выполняются операции над перебираемыми элементами массива, но при этом метод map() возвращает новый массив с результатами операций над элементами массива.

5.4. Объект Number

Объект Number представляет числа. Чтобы создать число, надо передать в конструктор Number число или стоку, представляющую число:

var x = new Number(34);
var y = new Number('34');
console.log(x + y);
68

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

var z = 34;

Объект Number предоставляет ряд свойств и методов. Некоторые его свойства:

  • Number.MAX_VALUE: наибольшее возможное число. Приблизительно равно 1.79E+308. Числа, которые больше этого значения, рассматриваются как Infinity

  • Number.MIN_VALUE: наименьшее возможное положительное число. Приблизительно равно 5e-324 (где-то около нуля)

  • Number.NaN: специальное значение, которое указывает, что объект не является числом

  • Number.NEGATIVE_INFINITY: значение, которое обозначает отрицательную неопределенность и которое возникает при переполнении. Например, если складывать два отрицательных числа, которые по модулю равны Number.MAX_VALUE

  • Number.POSITIVE_INFINITY: положительная неопределенность. Также, как и отрицательная неопределенность, возникает при переполнении, только теперь в положительную сторону

  • parseFloat(): преобразует строку в число с плавающей точкой.

  • parseInt(): преобразует строку в целое число.

  • toFixed(): оставляет в числе с плавающей точкой определенное количество знаков в дробной части.

  • isNaN(): определяет, является ли объект числом. Если объект не является числом, то возвращается значение true.

Но следующее выражение вернет false, хотя значение не является числом:

var f = Number.isNaN("hello"); // false

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

var f = isNaN("hello"); // true

6. Строки

6.1. Строки и объект String

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

let name = "Tom";

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

var name = new String("Tom");

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

Объект String имеет большой набор свойств и методов, с помощью которых мы можем манипулировать строками.

Свойство length указывает на длину строки:

var hello = "привет мир";
console.log("В строке '" + hello + "' " + hello.length + " символов");
В строке 'привет мир' 10 символов

6.1.1. Шаблоны строк

Шаблоны строк позволяют вставлять в строку различные значения. Для этого строки заключаются в косые кавычки `:

let name = "Tom";
let hello = `Hello ${name}`;
console.log(hello);

let age = 23;
let info = `${name} is ${age} years old`;
console.log(info);
Hello Tom
Tom is 23 years old

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

Также вместо скалярных значений могут добавляться свойства сложных объектов или результаты выражений.

6.1.2. Поиск в строке

Для поиска в строке некоторой подстроки используются методы indexOf() (индекс первого вхождения подстроки) и lastIndexOf() (индекс последнего вхождения подстроки*. Эти методы принимают два параметра:

  • Подстроку, которую надо найти

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

Оба метода возвращают индекс символа, с которого в строке*начинается подстрока. Если подстрока не найдена, то возвращается число -1.

Еще один метод - includes() возвращает true, если строка содержит определенную подстроку. С помощью второго дополнительного параметра можно определить индекс, с которого будет начинаться поиск подстроки.

6.1.3. Выбор подстроки

Для того, что бы вырезать из строки подстроку, применяются методы substr() и substring().

Метод substring() принимает два параметра:

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

  • индекс, до которого надо обрезать строку

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

6.1.4. Управление регистром

Для изменения регистра имеются методы toLowerCase() (для перевода в нижний регистр) и toUpperCase() (для перевода в верхний регистр).

6.1.5. Получение символа по индексу

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

Но если в качестве результата метод charAt() возвращает сам символ, то метод charCodeAt() возвращает числовой код этого символа.

6.1.6. Удаление пробелов

Для удаления начальных и концевых пробелов в стоке используется метод trim().

6.1.7. Объединение строк

Метод concat() объединяет две строки.

6.1.8. Замена подстроки

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

6.1.9. Разделение строки

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

6.1.10. Проверка начала и окончания строки

Метод startsWith() возвращает true, если строка начинается с определенной подстроки. А метод endsWith() возвращает true, если строка оканчивается на определенную подстроку. При этом играет роль регистр символов.

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

6.2. Объект RegExp

6.2.1. Регулярные выражения

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

Определить регулярное выражение можно двумя способами:

var myExp = /hello/;
var myExp = new RegExp("hello");

6.2.2. Методы RegExp

Чтобы определить, соответствует ли регулярное выражение строке, в объекте RegExp определен метод test(). Этот метод возвращает true, если строка соответствует регулярному выражению, и false, если не соответствует.

var initialText = "hello world!";
var exp = /hello/;
var result = exp.test(initialText);
console.log(result);

initialText = "beautifull wheather";
result = exp.test(initialText);
console.log(result);
true
false

Аналогично работает метод exec() - он также проверяет, соответствует ли строка регулярному выражению, только теперь данный метод возвращает ту часть строки, которая соответствует выражению. Если соответствий нет, то возвращается значение null.

6.2.3. Группы символов

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

var initialText = "обороноспособность";
var exp = /[абв]/;

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

var initialText = "обороноспособность";
var exp = /[а-я]/;

Если, наоборот, не надо, чтобы строка имела только определенные символы, то необходимо в квадратных скобках перед перечислением символов ставить знак ^:

var initialText = "обороноспособность";
var exp = /[^а-я]/;

6.2.4. Свойства выражений

  • Свойство global позволяет найти все подстроки, которые соответствуют регулярному выражению. По умолчанию при поиске подстрок регулярное выражение выбирает первую попавшуюся подстроку из строки, которая соответствует выражению. Хотя в строке может быть множество подстрок, которые также соответствуют выражению. Для этого применяется данное свойство в виде символа g в выражениях

  • Свойство ignoreCase позволяет найти подстроки, которые соответствуют регулярному выражению, вне зависимости от регистра символов в строке. Для этого в регулярных выражениях применяется символ i

  • Свойство multiline позволяет найти подстроки, которые соответствуют регулярному выражению, в многострочном тексте. Для этого в регулярных выражениях применяется символ m

var exp = /мир/i;
var exp = /мир/ig;

6.3. Регулярные выражения в методах String

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

6.3.1. Разделение строки. Метод split()

Метод split() может использовать регулярные выражения для разделения строк:

var initialText = "Сегодня была прекрасная погода";
var exp = /\s/;
var result = initialText.split(exp);
result.forEach(function(value, index, array) {
    console.log(value);
})
Сегодня
была
прекрасная
погода

6.3.2. Метод match()

Для поиска всех соответствий в строке применяется метод match():

var initialText = "Он пришел домой и сделал домашнюю работу";
var exp = /дом[а-я]*/gi;
var result = initialText.match(exp);
result.forEach(function(value, index, array) {
    console.log(value);
})
домой
домашнюю

Метод search() находит индекс первого включения соответствия в строке:

var initialText = "hello world";
var exp = /wor/;
var result = initialText.search(exp);
console.log(result);
6

6.3.4. Замена. Метод replace()

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

var menu = "Завтрак: каша, чай. Обед: суп, чай. Ужин: салат, чай.";
var exp = /чай/gi;
menu = menu.replace(exp, "кофе");
console.log(menu);
Завтрак: каша, кофе. Обед: суп, кофе. Ужин: салат, кофе.

7. Работа с браузером и BOM

7.1. Browser Object Model и объект window

Большое значение в JavaScript имеет работа с web-браузером и теми объектами, которые он предоставляет. Например, использование объектов браузера позволяет манипулировать элементами html, которые имеются на странице, или взаимодействовать с пользователем.

Все объекты, через которые JavaScript взаимодействует с браузером, описываются таким понятием как Browser Object Model (Объектная Модель Браузера).

Browser Object Model можно представить в виде следующей схемы:

Browser Object Model in JavaScript

В вершине находится главный объект — объект window, который представляет собой браузер. Этот объект в свою очередь включает ряд других объектов, в частности, объект document, который представляет отдельную web-страницу, отображаемую в браузере.

7.1.1. Объект window

Объект window представляет собой окно web-браузера, в котором размещаются web-страницы. window является глобальным объектом, поэтому при доступе к его свойствам и методам необязательно использовать его имя. Например, window имеет метод alert(), который отображает окно сообщения. Но нам необязательно писать:

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

window можно не использовать:

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

Но так как данный объект глобальный, то это накладывает некоторые ограничения. Например:

var alert = function(message) {
    document.write("Сообщение: " + message);
};
window.alert("Привет мир!");

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

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

var message = "hello";
document.write(window.message);

7.2. Управление окнами

7.2.1. Диалоговые окна

Для взаимодействия с пользователем в объекте window определен ряд методов, которые позволяют создавать диалоговые окна.

Метод alert()

Метод alert() выводит окно с сообщением:

alert("hello world");
Метод confirm()

Метод confirm() отображает окно с сообщением, в котором пользователь должен подтвердить действие двух кнопок OK и Отмена. В зависимости от выбора пользователя метод возвращает:

  • true если пользователь нажал OK

  • false если пользователь нажал кнопку Отмены

var result = confirm("Завершить выполнение программы?");
if (result === true) {
    document.write("Работа программы завершена");
} else {
    document.write("Программа продолжает работать");
}
Функция confirm в JavaScript
Метод prompt()

Метод prompt() позволяет с помощью диалогового окна запрашивать у пользователя какие-либо данные. Данный метод возвращает введенное пользователем значение:

var age = prompt("Введите свой возраст:");
document.write("Вам " + age + " лет");

Если пользователь откажется вводить значение и нажмет на кнопку отмены, то метод возвратит значение null.

Функция prompt в JavaScript

7.2.2. Открытие, закрытие и позиционирование окон

Объект window также предоставляет ряд методов для управления окнами браузера.

Метод open()

Метод open() открывает определенный ресурс в новом окне браузера:

var popup = window.open('https://microsoft.com', 'Microsoft', 'width=400, height=400, resizable=yes');

Метод open() принимает ряд параметров: путь к ресурсу, описательное название для окна и в качестве третьего параметра набор стилевых значений окна. Метод возвращает ссылку на объект нового окна.

Можно установить следующие стилевые характеристики:

  • width: ширина окна в пикселях. Например, width=640

  • height: высота окна в пикселях. Например, height=480

  • left: координата x относительно начала экрана в пикселях. Например, left=0

  • top: координата y относительно начала экрана в пикселях. Например, top=0

  • titlebar: будет ли окно иметь строку с заголовком. Например, titlebar=no

  • menubar: будет ли окно иметь панель меню. Например, menubar=yes

  • toolbar: будет ли окно иметь панели инструментов. Например, toolbar=yes

  • location: будет ли окно иметь адресную строку. Например, location=no

  • scrollbars: допускается ли наличие полос прокрутки. Например, scrollbars=yes

  • status: наличие статусной строки. Например, status=yes

  • resizable: может ли окно изменять размеры. Например, resizable=no

Метод close()

С помощью метода close() можно закрыть окно. Например, откроем новое окно и через 10 секунд закроем его:

var popup = window.open('https://microsoft.com', 'Microsoft', 'width=400, height=400, resizable=yes');

function closeWindow() {
    popup.close();
}

setTimeout(closeWindow, 10000);
Метод moveTo()

Метод moveTo() позволяет переместить окно на новую позицию:

var popup = window.open('https://microsoft.com', 'Microsoft', 'width=400, height=400, resizable=yes');
popup.moveTo(50, 50);

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

Метод resizeTo()

Метод resizeTo() позволяет изменить размеры окна:

var popup = window.open('https://microsoft.com', 'Microsoft', 'width=400, height=400, resizable=yes');
popup.resizeTo(500, 350); // 500 - ширина и 350 - высота

7.3. История браузера

Объект history предназначен для хранения истории посещений web-страниц в браузере. Этот объект доступен через объект window.

Все сведения о посещении пользователя хранятся в специальном стеке (history stack). С помощью свойства length можно узнать, как много web-станиц хранится в стеке:

document.write("В истории " + history.length + " страниц(ы)");

Для перемещения по страницам в истории в объекте history определены методы:

  • back(): перемещение к прошлой посмотренной странице

  • forward(): перемещение к следующей просмотренной странице

history.back(); // перемещение назад

Также в объекте history определен специальный метод go(), который позволяет перемещаться вперед и назад по истории на определенное число страниц. Например, переместимся на 2 страницы назад:

history.go(-2);

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

history.go(3);

7.4. Расположение web-страницы

Объект location содержит информацию о расположении текущей web-страницы: URL, информацию о сервере, номер порта, протокол. С помощью свойств объекта мы можем получить эту информацию:

  • href: полная строка запроса к ресурсу

  • pathname: путь к ресурсу

  • origin: общая схема запроса

  • protocol: протокол

  • port: порт, используемый ресурсом

  • host: хост

  • hostname: название хоста

  • hash: если строка запроса содержит символ решетки (#), то данное свойство возвращает ту часть строки, которая идет после этого символа

  • search: если строка запроса содержит знак вопроса (?), например, то данное свойство возвращает ту часть строки, которая идет после знака вопроса

Например, пусть есть следующая web-страница test.html, которая лежит на локальном web-сервере:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
</head>
<body>
    <script>
        document.write("Строка запроса: " + location.href + "<br>");
        document.write("Путь к ресурсу: " + location.pathname + "<br>");
        document.write("Схема: " + location.origin + "<br>");
        document.write("Протокол: " + location.protocol + "<br>");
        document.write("Порт: " + location.port + "<br>");
        document.write("Хост: " + location.host + "<br>");
        document.write("Имя хоста: " + location.hostname + "<br>");
        document.write("Хэш: " + location.hash + "<br>");
        document.write("Поиск: " + location.search + "<br>");
    </script>
</body>
</html>
Объект location в JavaScript

Также объект location предоставляет ряд методов, которые можно использовать для управления путем запроса:

  • assign(url): загружает ресурс, который находится по пути url

  • reload(forcedReload): перезагружает текущую web-страницу. Параметр forcedReload указывает, надо ли использовать кэш браузера. Если параметр равен true, то кэш не используется

  • replace(url): заменяет текущую web-станицу другим ресурсом, который находится по пути url. В отличие от метода assign, который также загружает web-станицу с другого ресурса, метод replace не сохраняет предыдущую web-страницу в стеке истории переходов history, поэтому мы не сможем вызвать метод history.back() для перехода к ней.

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

location = "http://google.com";
// аналогично
// location.href = "http://google.com";
// location.assign("http://google.com");

Переход на другой локальный ресурс:

location.replace("index.html");

7.5. Информация о браузере и операционной системе

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

document.write(navigator.userAgent);

Данное свойство хранит полную стоку юзер-агента, например, Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0

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

var browser, uAgent = navigator.userAgent;

if (uAgent.indexOf("Chrome") > -1) {
    browser = "Google Chrome";
} else if (uAgent.indexOf("Safari") > -1) {
    browser = "Apple Safari";
} else if (uAgent.indexOf("Opera") > -1) {
    browser = "Opera";
} else if (uAgent.indexOf("Firefox") > -1) {
    browser = "Mozilla Firefox";
} else if (uAgent.indexOf("MSIE") > -1) {
    browser = "Microsoft Internet Explorer";
}
document.write(browser);

7.5.1. Географическое положение пользователя

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

function success(position) {
    var latitude = position.coords.latitude;
    var longitude = position.coords.longitude;
    var altitude = position.coords.altitude;
    var speed = position.coords.speed;

    document.write("Широта: " + latitude + "<br>");
    document.write("Долгота: " + longitude + "<br>");
    document.write("Высота: " + altitude + "<br>");
    document.write("Скорость перемещения: " + speed + "<br>");
};

function error(obj) {
    document.write("Ошибка при определении положения");
};
navigator.geolocation.getCurrentPosition(success, error);

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

  • latitude: географическая широта

  • longitude: географическая долгота

  • altitude: высота

  • speed: скорость, с которой перемещается пользователь (например, если он идет или перемещается на транспорте)

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

Объект geolocation в JavaScript

7.6. Таймеры

Для выполнения действий через определенные промежутки времени в объекте window предусмотрены функции таймеров. Есть два типа таймеров:

  • срабатывающие только один раз

  • срабатывающие постоянно через промежуток времени

7.6.1. Функция setTimeout()

Для одноразового выполнения действий через промежуток времени предназначена функция setTimeout(). Она может принимать два параметра:

var timerId = setTimeout(someFunction, period);

Параметр period указывает на промежуток, через который будет выполняться функция из параметра someFunction. А в качестве результата функция возвращает id таймера.

function timerFunction() {
    document.write("выполнение функции setTimeout");
}
setTimeout(timerFunction, 3000);

В данном случае через 3 секунды после загрузки страницы произойдет срабатывание функции timerFunction.

Для остановки таймера применяется функция clearTimeout().

function timerFunction() {
    document.write("выполнение функции setTimeout");
}
var timerId = setTimeout(timerFunction, 3000);
clearTimeout(timerId);

7.6.2. Функция setInterval()

Функции setInterval() и clearInterval() работают аналогично функциям setTimeout() и clearTimeout() с той лишь разницей, что setInterval() постоянно выполняет определенную функцию через промежуток времени.

Например, напишем небольшую программу для вывода текущего времени:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
</head>
<body>
    <div id="time"></div>
    <script>
        function updateTime() {
            document.getElementById("time").innerHTML = new Date().toTimeString();
        }
        setInterval(updateTime, 1000);
    </script>
</body>
</html>

Здесь через каждую секунду (1000 миллисекунд) вызывается функция updateTime(), которая обновляет содержимое элемента <div id="time" >, устанавливая в качестве его значения текущее время.

7.6.3. Функция requestAnimationFrame()

Метод requestAnimationFrame() действует аналогично setInterval() за тем исключением, что он больше заточен под анимацию, работу с графикой и имеет ряд оптимизаций, которые улучшают его производительность.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <style>
        #rect {
            margin: 100px;
            width: 100px;
            height: 100px;
            background: #50c878;
        }
    </style>
</head>
<body>
    <div id="rect"></div>
    <script>
        var square = document.getElementById("rect");
        var angle = 0;
        function rotate() {
            angle = (angle + 2) % 360;
            square.style.transform = "rotate(" + angle + "deg)";
            window.requestAnimationFrame(rotate);
        }
        var id = window.requestAnimationFrame(rotate);
    </script>
</body>
</html>

В метод window.requestAnimationFrame() передается функция, которая будет вызываться определенное количество раз в секунду (обычно 60 кадров в секунду или же FPS). В данном случае в этот метод передается функция rotate, которая изменяет угол поворота блока на странице и затем обращается опять же к методу window.requestAnimationFrame(rotate).

В качестве возвращаемого результата метод window.requestAnimationFrame() возвращает уникальный id, который может потом использоваться для остановки анимации:

window.cancelAnimationFrame(id);

8. Работа с DOM

Одой из ключевых задач JavaScript является взаимодействие с пользователем и манипуляция элементами web-страницы. Для JavaScript web-страница доступна в виде объектной модели документа (document object model) или сокращенно DOM. DOM описывает структуру web-страницы в виде древовидного представления и предоставляет разработчикам способ получить доступ к отдельным элементам web-страницы.

Важно не путать понятия BOM (Browser Object Model - объектная модель браузера) и DOM (объектная модель документа). Если BOM предоставляет доступ к браузеру и его свойствам в целом, то DOM предоставляет доступ к отдельной web-странице или HTML-документу и его элементам.

Рассмотрим простейшую web-страницу:

<!DOCTYPE html>
<html>
<head>
    <title>Page Title</title>
</head>
<body>
    <h2>Page Header</h2>
    <div>
        <h3>Block Header</h3>
        <p>Text</p>
    </div>
</body>
</html>

Дерево DOM для этой страницы будет выглядеть следующим образом:

дерево HTML страницы

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

Существует следующие виды узлов:

  • Element: HTML-элемент

  • Attr: атрибут HTML-элемента

  • Document: корневой узел HTML-документа

  • DocumentType: DTD или тип схемы XML-документа

  • DocumentFragment: место для временного хранения частей документа

  • EntityReference: ссылка на сущность XML-документа

  • ProcessingInstruction: инструкция обработки web-страницы

  • Comment: элемент комментария

  • Text: текст элемента

  • CDATASection: секция CDATA в документе XML

  • Entity: необработанная сущность DTD

  • Notation: нотация, объявленная в DTD

8.1. Объект document

Для работы со структурой DOM в JavaScript предназначен объект document, который определен в глобальном объекте window. Объект document предоставляет ряд свойств и методов для управления элементами страницы.

8.1.1. Поиск элементов

Для поиска элементов на странице применяются следующие методы:

  • getElementById(value): выбирает элемент, у которого атрибут id равен value

  • getElementsByTagName(value): выбирает все элементы, у которых тег равен value

  • getElementsByClassName(value): выбирает все элементы, которые имеют класс value

  • querySelector(value): выбирает первый элемент, который соответствует css-селектору value

  • querySelectorAll(value): выбирает все элементы, которые соответствуют css-селектору value

Например, найдем элемент по id:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
</head>
<body>
    <div>
        <h3 id="header">Block Header</h3>
        <p>Text</p>
    </div>
    <script>
        var headerElement = document.getElementById("header");
        document.write("Текст заголовка: " + headerElement.innerText);
    </script>
</body>
</html>

С помощью вызова document.getElementById("header") находим элемент, у которого id="header". А с помощью свойства innerText можно получить текст найденного элемента.

Поиск по определенному тегу:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
</head>
<body>
    <div>
        <h3>Заголовок</h3>
        <p>Первый абзац</p>
        <p>Второй абзац</p>
    </div>
    <script>
        var pElements = document.getElementsByTagName("p");

        for (var i = 0; i < pElements.length; i++) {
            document.write("Текст параграфа: " + pElements[i].innerText + "<br/>");
        }
    </script>
</body>
</html>

С помощью вызова document.getElementsByTagName("p") находим все элементы параграфов. Этот вызов возвращает массив найденных элементов. Поэтому, чтобы получить отдельные элементы массива, необходимо пробежаться по ним в цикле.

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

var pElement = document.getElementsByTagName("p")[0];
document.write("Текст параграфа: " + pElement.innerText);

Получение элемента по классу:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
</head>
<body>
    <div class="article">
        <h3>Заголовок статьи</h3>
        <p class="text">Первый абзац</p>
        <p class="text">Второй абзац</p>
    </div>
    <script>
        var articleDiv = document.getElementsByClassName("article")[0];
        console.log(articleDiv);
        var textElems = document.getElementsByClassName("text");
        for (var i = 0; i < textElems.length; i++) {
            console.log(textElems[i]);
        }
    </script>
</body>
</html>

Выбор по селектору CSS:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
</head>
<body>
    <div class="annotation">
        <p>Аннотация статьи</p>
    </div>
    <div class="text">
        <p>Первый абзац</p>
        <p>Второй абзац</p>
    </div>
    <script>
        var elem = document.querySelector(".annotation p");
        document.write("Текст селектора: " + elem.innerText);
    </script>
</body>
</html>

Выражение document.querySelector(".annotation p") находит элемент, который соответствует селектору .annotation p. Если на странице несколько элементов, соответствующих селектору, то метод выберет первый из них. В итоге браузер выведет:

Аннотация статьи

Первый абзац

Второй абзац

Текст селектора: Аннотация статьи

Чтобы получить все элементы по селектору, можно подобным образом использовать метод document.querySelectorAll(), который возвращает массив найденных элементов:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
</head>
<body>
    <div class="annotation">
        <p>Аннотация статьи</p>
    </div>
    <div class="text">
        <p>Первый абзац</p>
        <p>Второй абзац</p>
    </div>
    <script>
        var elems = document.querySelectorAll(".text p");

        for (var i = 0; i < elems.length; i++) {
            document.write("Текст селектора " + i + ": " + elems[i].innerText + "<br/>");
        }
    </script>
</body>
</html>
Вывод браузера:

Аннотация статьи

Первый абзац

Второй абзац

Текст селектора 0: Первый абзац
Текст селектора 1: Второй абзац

8.2. Свойства объекта document

Кроме ранее рассмотренных методов объект document позволяет обратиться к определенным элементам web-страницы через свойства:

  • documentElement: предоставляет доступ к корневому элементу <html>

  • body: предоставляет доступ к элементу <body> на web-странице

  • images: содержит коллекцию всех объектов изображений (элементов img)

  • links: содержит коллекцию ссылок - элементов <a> и <area>, у которых определен атрибут href

  • anchors: предоставляет доступ к коллекции элементов <a>, у которых определен атрибут name

  • forms: содержит коллекцию всех форм на web-странице

Эти свойства не предоставляют доступ ко всем элементам, однако позволяют получить наиболее часто используемые элементы на web-странице.

Получение всех изображений на странице:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
</head>
<body>
    <img src="picure1.png" alt="Картинка 1" />
    <img src="picure2.png" alt="Картинка 2" />
    <img src="picure3.png" alt="Картинка 3" />
    <script>
        var images = document.images;
        // изменим первое изображение
        images[0].src="pics/picture_4.jpg";
        images[0].alt="Новая картинка";
        // перебирем все изображения
        for (var i = 0; i < images.length; i++) {
            document.write("<br/>" + images[i].src);
            document.write("<br/>" + images[i].alt);
        }
    </script>
</body>
</html>

Подобно тому, как в коде HTML можно установить атрибуты у элемента img, так и в коде JavaScript можно через свойства src и alt получить и установить значения этих атрибутов.

Рассмотрим получение всех ссылок на странице:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
</head>
<body>
    <a href="article1.html">Статья 1</a>
    <a href="article2.html">Статья 2</a>
    <a href="article3.html">Статья 3</a>
    <script>
        var links = document.links;

        for (var i = 0; i < links.length; i++) {
            document.write("<br/>" + links[i].innerText);
            document.write("<br/>" + links[i].href);
        }
    </script>
</body>
</html>

Подобно тому, как в коде HTML можно установить атрибуты у элемента img, так и в коде javascript можно через свойства src и alt получить и установить значения этих атрибутов.

8.3. Объект Node

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

  • childNodes: содержит коллекцию дочерних узлов

  • firstChild: возвращает первый дочерний узел текущего узла

  • lastChild: возвращает последний дочерний узел текущего узла

  • previousSibling: возвращает предыдущий элемент, который находится на одном уровне с текущим

  • nextSibling: возвращает следующий элемент, который находится на одном уровне с текущим

  • ownerDocument: возвращает корневой узел документа

  • parentNode: возвращает элемент, который содержит текущий узел

  • nodeName: возвращает имя узла

  • nodeType: возвращает тип узла в виде числа. 1 - элемент, 2 - атрибут, 3 - текст

  • nodeValue: возвращает или устанавливает значение узла в виде простого текста

8.4. Создание, добавление элементов web-страницы

Для создания элементов объект document имеет следующие методы:

  • createElement(elementName): создает HTML-элемент, тег которого передается в качестве параметра. Возвращает созданный элемент

  • createTextNode(text): создает и возвращает текстовый узел. В качестве параметра передается текст узла.

var elem = document.createElement("div");
var elemText = document.createTextNode("Привет мир");

Переменная elem будет хранить ссылку на элемент div. Однако одного создания элементов недостаточно, их еще надо добавить на web-страницу.

Для добавления элементов мы можем использовать один из методов объекта Node:

  • appendChild(newNode): добавляет новый узел newNode в конец коллекции дочерних узлов

  • insertBefore(newNode, referenceNode): добавляет новый узел newNode перед узлом referenceNode

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
</head>
<body>
    <div class="article">
        <h3>Заголовок статьи</h3>
        <p>Первый абзац</p>
        <p>Второй абзац</p>
    </div>
    <script>
        var articleDiv = document.querySelector("div.article");
        // создаем элемент
        var elem = document.createElement("h2");
        // создаем для него текст
        var elemText = document.createTextNode("Привет мир");
        // добавляем текст в элемент в качестве дочернего элемента
        elem.appendChild(elemText);
        // добавляем элемент в блок div
        articleDiv.appendChild(elem);
    </script>
</body>
</html>

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

var elem = document.createElement("h2");
elem.textContent = "Привет мир";

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

8.4.1. Копирование элементов

Иногда элементы бывают довольно сложными по составу, и гораздо проще их скопировать, чем с помощью отдельных вызовов создавать из содержимое. Для копирования уже имеющихся узлов у объекта Node можно использовать метод cloneNode().

В метод cloneNode() в качестве параметра передается логическое значение: если передается true, то элемент будет копироваться со всеми дочерними узлами; если передается false - то копируется без дочерних узлов.

8.4.2. Удаление элемента

Для удаления элемента вызывается метод removeChild(removalNode) объекта Node. Этот метод удаляет один из дочерних узлов.

8.4.3. Замена элемента

Для замены элемента применяется метод replaceChild(newNode, oldNode) объекта Node.

8.5. Объект Element

Кроме методов и свойств объекта Node в JavaScript мы можем использовать свойства и методы объектов Element. Важно не путать эти два объекта: Node и Element. Node представляет все узлы web-страницы, в то время как объект Element представляет непосредственно только HTML-элементы. То есть объекты Element - это фактически те же самые узлы - объекты Node, у которых тип узла (свойство nodeType) равно 1.

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

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
</head>
<body>
    <div class="article">
        <h3>Заголовок статьи</h3>
        <p>Первый абзац</p>
        <p>Второй абзац</p>
    </div>
    <script>
        function getChildren(elem) {
            for(var i in elem.childNodes) {
                if(elem.childNodes[i].nodeType===1) {
                    console.log(elem.childNodes[i].tagName);
                    getChildren(elem.childNodes[i]);
                }
            }
        }
        var root = document.documentElement;
        console.log(root.tagName);
        getChildren(root);
    </script>
</body>
</html>

8.5.1. Свойства innerText и innerHTML

Для получения или установки текстового содержимого элемента можно использовать свойство innerText, а для получения или установки HTML-кода - свойство innerHTML.

Надо отметить, что свойство innerText во многом аналогично свойству textContent. То есть следующие вызовы будут равноценны:

var pElement = document.querySelectorAll("div.article p")[0];
pElement.innerText = "hello";
pElement.textContent = "hello";

Установка HTML-кода у элемента:

var articleDiv = document.querySelector("div.article");
articleDiv.innerHTML ="<h2>Hello World!!!</h2><p>bla bla bla</p>";

8.5.2. Методы объекта Element

Среди методов объекта Element можно отметить методы управления атрибутами:

  • getAttribute(attr): возвращает значение атрибута attr

  • setAttribute(attr, value): устанавливает для атрибута attr значение value. Если атрибута нет, то он добавляется

  • removeAttribute(attr): удаляет атрибут attr и его значение

Работа с атрибутами:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
</head>
<body>
    <div class="article" style="color:red;">
        <h3>Заголовок статьи</h3>
        <p>Первый абзац</p>
        <p>Второй абзац</p>
    </div>
    <script>
        var articleDiv = document.querySelector("div.article");
        // получаем атрибут style
        var styleValue = articleDiv.getAttribute("style");
        console.log("До изменения атрибута: " + styleValue);
        // удаляем атрибут
        articleDiv.removeAttribute("style");
        // добавляем заново атрибут style
        articleDiv.setAttribute("style", "color:blue;");
        styleValue = articleDiv.getAttribute("style");
        console.log("После изменения атрибута: " + styleValue);
    </script>
</body>
</html>

8.5.3. Размеры и позиция элементов

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

Свойства offsetWidth и offsetHeight определяют соответственно ширину и высоту элемента в пикселях. В ширину и высоту включается граница элемента.

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

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <style>
        #rect {
            width: 100px;
            height: 100px;
            background: #50c878;
            border: 3px solid silver;
        }
    </style>
</head>
<body>
    <div id="rect"></div>
    <script>
        var rect = document.getElementById("rect");
        console.log("offsetHeight: " + rect.offsetHeight);
        console.log("offsetWidth: " + rect.offsetWidth);
        console.log("clientHeight: " + rect.clientHeight);
        console.log("clientWidth: " + rect.clientWidth);
    </script>
</body>
</html>

Для определения позиции элемента наиболее эффективным способом является метод getBoundingClientRect().

Этот метод возвращает объект со свойствами top, bottom, left, right, которые указывают на смещение элемента относительно верхнего левого угла браузера.

8.5.4. Изменение стиля элементов

Для работы со стилевыми свойствами элементов в JavaScript применяются, главным образом, два подхода:

  • Изменение свойства style

  • Изменение значения атрибута class

Свойство style

Свойство style представляет сложный объект для управления стилем и напрямую сопоставляется с атрибутом style HTML-элемента. Этот объект содержит набор свойств CSS: element.style.свойствоCSS.

var root = document.documentElement;
// устанавливаем стиль
root.style.color = "blue";
// получаем значение стиля
document.write(root.style.color);
blue

Однако ряд свойств CSS в названиях имеют дефис, например, font-family. В JavaScript для этих свойств дефис не употребляется. Только первая буква, которая идет после дефиса, переводится в верхний регистр.

var root = document.documentElement;
root.style.fontFamily = "Verdana";
Свойство className

С помощью свойства className можно установить атрибут class HTML-элемента:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <style>
    .blueStyle {
        color:blue;
        font-family:Verdana;
    }
    .article {
        font-size:20px;
    }
    </style>
</head>
<body>
    <div class="article">
        <h3>Заголовок статьи</h3>
        <p>Первый абзац</p>
        <p>Второй абзац</p>
    </div>
    <script>
        var articleDiv = document.querySelector("div.article");
        // установка нового класса
        articleDiv.className = "blueStyle";
        // получаем название класса
        document.write(articleDiv.className);
    </script>
</body>
</html>

Благодаря использованию классов не придется настраивать каждое отдельное свойство css с помощью свойства style.

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

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

Свойство classList

Выше было рассмотрено, как добавлять классы к элементу, однако для управления множеством классов гораздо удобнее использовать свойство classList. Это свойство представляет объект, реализующий следующие методы:

  • add(className): добавляет класс className

  • remove(className): удаляет класс className

  • toggle(className): переключает у элемента класс на className. Если класса нет, то он добавляется, если есть, то удаляется

var articleDiv = document.querySelector("div.article");
// удаляем класс
articleDiv.classList.remove("article");
// добавляем класс
articleDiv.classList.add("blueStyle");
// переключаем класс
articleDiv.classList.toggle("article");

8.6. Создание своего элемента HTML

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

В JavaScript HTML-элемент представлен интерфейсом HTMLElement. Соответственно, реализуя данный интерфейс в JavaScript, мы можем создать свои классы, которые будут представлять HTML-элементы, и потом их использовать.

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

class HelloMetanit extends HTMLElement {
}

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

customElements.define(name, constructor, options);

Она принимает три параметра:

  • name: имя разработанного элемента HTML, который будет представлять класс JavaScript. Важно: имя должно содержать дефис.

  • constructor: конструктор (по сути класс JavaScript), который представляет разработанный элемент HTML.

  • options: необязательный параметр - объект, который настраивает разработанный HTML-элемент.

Как правило, классы кастомных элементов применяют конструктор. Причем в самом начале конструктора должен идти вызов функции super(), который гарантирует, что класс унаследовал все методы, атрибуты и свойства интерфейса HTMlElement.

class HelloMetanit extends HTMLElement {
    constructor() {
        super();
    }
}

Но кроме того, в конструкторе мно определить некоторую базовую логику элемента.

8.6.1. Добавление методов

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

8.6.2. События жизненного цикла

Кастомный элемент HTML имеет свой жизненный цикл, который описывается следующими методами:

  • connectedCallback: вызывается каждый раз, когда разработанный HTML-элемент добавляется в DOM.

  • disconnectedCallback: вызывается каждый раз, когда разработанный HTML-элемент удаляется из DOM.

  • adoptedCallback: вызывается каждый раз, когда разработанный HTML-элемент перемещается в новый элемент.

  • attributeChangedCallback: вызывается при каждом изменении (добавлении, изменении значения или удаления) атрибута разработанного HTML-элемента.

8.6.3. Добавление атрибутов

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

this.style.color = "red";
if (this.hasAttribute("hellocolor")) {
    this.style.color = this.getAttribute("hellocolor");
}

8.6.4. Стилизация CSS

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

9. События

9.1. Введение в обработку событий

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

В JavaScript есть следующие типы событий:

  • События мыши (перемещение курсора, нажатие мыши и т.д.)

  • События клавиатуры (нажатие или отпускание клавиши клавиатуры)

  • События жизненного цикла элементов (например, событие загрузки web-страницы)

  • События элементов форм (нажатие кнопки на форме, выбор элемента в выпадающем списке и т.д.)

  • События, возникающие при изменении элементов DOM

  • События, возникающие при касании на сенсорных экранах

  • События, возникающие при возникновении ошибок

Рассмотрим простейшую обработку событий. Например, на web-странице у нас есть следующий элемент div:

<div id="rect" onclick="alert('Нажато')" style="width:50px; height:50px; background-color:blue;">
</div>

Здесь определен обычный блок div, который имеет атрибут onclick задающий обработчик события _нажатия на блок div. То есть, чтобы обработать какое-либо событие, нам надо определить для него обработчик. Обработчик представляет собой код на языке JavaScript. В данном случае обработчик выглядит довольно просто:

alert('Нажато')

И при нажатии на кнопку будет появляться всплывающее окно:

Пример события

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

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
</head>
<body>
    <div id="rect" onclick="displayMessage()" style="width:50px;height:50px;background-color:blue;">
    </div>
    <script>
        function displayMessage() {
            alert('Нажато');
        }
    </script>
</body>
</html>

Теперь обработчиком события будет выступать функция displayMessage().

9.1.1. Передача параметров обработчику события

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

<a href="page1.html" onclick="return handler(this)">Страница 1</a>
<script>
    function handler(obj) {
        alert(obj.href);
        return false;
    }
</script>

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

Кроме того, надо отметить, что здесь обработчик возвращает результат. Хотя в первом примере с блоком div от обработчика не требовалось возвращения результата. Дело в том, что для некоторых обработчиков можно подтвердить или остановить обработку события. Например, нажатие на ссылку должно привести к переадресации. Но возвращая из обработчика false, можно остановить стандартный путь обработки события, и переадресации не будет. Если же возвращать значение true, то событие обрабатывается в стандартном порядке.

Если же убрать возвращение результата, то событие будет обрабатываться, как будто возвращается значение true:

<a href="page1.html" onclick="handler(this)">Страница 1</a>
<script>
    function handler(obj) {
        alert(obj.href);
    }
</script>

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

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <style>
        #rect {
            width: 50px;
            height: 50px;
            background-color: blue;
        }
    </style>
</head>
<body>
    <div id="rect" onclick="handler(event)"></div>
    <script>
        function handler(e) {
            alert(e.type);
        }
    </script>
</body>
</html>

В данном случае с помощью свойства type объекта event получаем тип события (в данном случае тип click).

9.2. Обработчики событий

9.2.1. Встроенные обработчики

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

<div id="rect" onclick="handler(event)"></div>

Хотя этот подход прекрасно работает, но он имеет кучу недостатков:

  • Код HTML смешивается с кодом JavaScript, в связи с чем становится труднее разрабатывать, отлаживать и поддерживать приложение.

  • Обработчики событий можно задать только для уже созданных на web-странице элементов. Динамически создаваемые элементы в этом случае лишаются возможности обработки событий.

  • К элементу для одного события может быть прикреплен только один обработчик.

  • Нельзя удалить обработчик без изменения кода

9.2.2. Свойства обработчиков событий

Проблемы, которые возникают при использовании встроенных обработчиков, были призваны решить свойства обработчиков. Подобно тому, как у HTML-элементов есть атрибуты для обработчиков, так и в коде JavaScript у элементов DOM можно получить свойства обработчиков, которые соответствуют атрибутам:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <style>
        #rect {
            width: 50px;
            height: 50px;
            background-color: blue;
        }
    </style>
</head>
<body>
    <div id="rect">
    </div>
    <script>
        function handler(e) {
            alert(e.type);
        }
        document.getElementById("rect").onclick = handler;
    </script>
</body>
</html>

В итоге достаточно взять свойство onclick и присвоить ему функцию, используемую в качестве обработчика. За счет этого код HTML отделяется от кода JavaScript.

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

9.2.3. Слушатели событий

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

Для работы со слушателями событий в JavaScript есть объект EventTarget, который определяет методы

  • addEventListener() для добавления слушателя

  • removeEventListener() для удаления слушателя

Поскольку HTML-элементы DOM тоже являются объектами EventTarget, то они также имеют эти методы. Фактически слушатели представляют те же функции обработчиков.

Метод addEventListener() принимает два параметра: название события без префикса on и функцию обработчика этого события.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <style>
        #rect {
            width: 50px;
            height: 50px;
            background-color: blue;
        }
    </style>
</head>
<body>
    <div id="rect">
    </div>
    <script>
        var rect = document.getElementById("rect");
        rect.addEventListener("click", function(e) {
            alert(e.type);
        });
    </script>
</body>
</html>

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

function handler(e) {
    alert(e.type);
}
var rect = document.getElementById("rect");
rect.addEventListener("click", handler);

Удаление слушателя аналогично добавлению:

rect.removeEventListener("click", handler);

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

var clicks = 0;

function handlerOne(e) {
    alert(e.type);
}

function handlerTwo(e) {
    clicks++;
    var newNode = document.createElement("p");
    newNode.textContent = "произошло нажатие " + clicks;
    document.body.appendChild(newNode);
}

var rect = document.getElementById("rect");
// прикрепляем первый обработчик
rect.addEventListener("click", handlerOne);
// прикрепляем второй обработчик
rect.addEventListener("click", handlerTwo);

9.3. Объект Event

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

  • bubbles: возвращает true, если событие является восходящим. Например, если событие возникло на вложенном элементе, то оно может быть обработано на родительском элементе.

  • cancelable: возвращает true, если можно отменить стандартную обработку события.

  • currentTarget: определяет элемент, к которому прикреплен обработчик события.

  • defaultPrevented: возвращает true, если был вызван у объекта Event метод preventDefault().

  • eventPhase: определяет стадию обработки события.

  • target: указывает на элемент, на котором было вызвано событие.

  • timeStamp: хранит время возникновения события.

  • type: указывает на имя события.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <style>
        #rect {
            width: 50px;
            height: 50px;
            background-color: blue;
        }
    </style>
</head>
<body>
    <div id="rect">
    </div>
    <script>
        function handler(event) {
            console.log("Тип события: " + event.type);
            console.log(event.target);
        }
        var rect = document.getElementById("rect");
        rect.addEventListener("click", handler);
    </script>
</body>
</html>

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

function handler(e) {
    e.target.style.backgroundColor = "red";
}

9.3.1. Остановка выполнения события

С помощью метода preventDefault() объекта Event что можно остановить дальнейшее выполнение события. В ряде случаев этот метод не играет большой роли. Однако может быть полезен, например, при нажатии на ссылку можно с помощью дополнительной обработки определить, надо ли переходить по ссылке или надо запретить переход. Или другой пример: пользователь отправляет данные формы, но в ходе обработки в обработчике события определили, что поля формы заполнены неправильно, и в этом случае также можно запретить отправку.

Например, запретим переход по ссылке после 12 часов:

<a href="http://google.com" id="link">Поиск</a>
<script>
    function linkHandler(e) {
        var date = new Date();
        var hour = date.getHours();
        console.log(hour);
        if (hour > 12) {
            e.preventDefault();
            document.write("После 12 переход запрещен");
        }
    }
    var link = document.getElementById("link");
    link.addEventListener("click", linkHandler);
</script>

9.4. Распространение событий

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

Есть две форм распространения событий:

  • Восходящие: событие распространяется вверх по дереву DOM от дочерних узлов к родительским.

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

9.4.1. Восходящие события

Рассмотрим восходящие события, которые распространяются в верх по дереву DOM. Допустим, у нас есть следующая web-страница:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <style>
        #blueRect {
            width: 100px;
            height: 100px;
            background-color: blue;
        }

        #redRect {
            width: 50px;
            height: 50px;
            background-color: red;
        }
    </style>
</head>
<body>
    <div id="blueRect">
        <div id="redRect">
        </div>
    </div>
    <script>
        var redRect = document.getElementById("redRect");
        redRect.addEventListener("click", function() {
            console.log("Событие на redRect");
        });
        var blueRect = document.getElementById("blueRect");
        blueRect.addEventListener("click", function() {
            console.log("Событие на blueRect");
        });
        document.body.addEventListener("click", function() {
            console.log("Событие на body");
        });
    </script>
</body>
</html>

Если нажать на вложенный div, то событие пойдет к родительскому элементу div и далее к элементу body:

Восходящее событие

Надо сказать, что подобное поведение не всегда является желательным. И в этом случае можно остановить распространение событие с помощью метода stopPropagation() объекта Event:

var redRect = document.getElementById("redRect");
redRect.addEventListener("click", function(e){
    console.log("Событие на redRect");
    e.stopPropagation();
});

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

9.4.2. Нисходящие события

События также могут быть нисходящими. Для их использования в метод addEventListener() в качестве третьего необязательного параметра передается логическое значение true или false, которое указывает, будет ли событие нисходящим. По умолчанию все события восходящие.

Возьмем ту же web-станицу, только изменим ее код JavaScript:

var redRect = document.getElementById("redRect");
redRect.addEventListener("click", function() {console.log("Событие на redRect");}, true);
var blueRect = document.getElementById("blueRect");
blueRect.addEventListener("click", function() {console.log("Событие на blueRect");}, true);
document.body.addEventListener("click", function() {console.log("Событие на body");}, true);

Теперь события будут распространяться в обратном порядке:

Восходящее событие в обратном порядке

9.5. События мыши

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

  • click: возникает при нажатии указателем мыши на элемент

  • mousedown: возникает при нахождении указателя мыши на элементе, когда кнопка мыши находится в нажатом состоянии

  • mouseup: возникает при нахождении указателя мыши на элементе во время отпускания кнопки мыши

  • mouseover: возникает при вхождении указателя мыши в границы элемента

  • mousemove: возникает при прохождении указателя мыши над элементом

  • mouseout: возникает, когда указатель мыши выходит за пределы элемента

Например, обработаем события mouseover и mouseout:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <style>
        #blueRect {
            width: 100px;
            height: 100px;
            background-color: blue;
        }
    </style>
</head>
<body>
    <div id="blueRect"></div>
    <script>
        function setColor(e) {
            if (e.type === "mouseover") {
                e.target.style.backgroundColor = "red";
            } else if (e.type === "mouseout") {
                e.target.style.backgroundColor = "blue";
            }
        }
        var blueRect = document.getElementById("blueRect");
        blueRect.addEventListener("mouseover", setColor);
        blueRect.addEventListener("mouseout", setColor);
    </script>
</body>
</html>

Теперь при наведении указателя мыши на блок blueRect он будет окрашиваться в красный цвет, а при уходе указателя мыши - блок будет обратно окрашиваться в синий цвет.

Объект Event является общим для всех событий. Однако для разных типов событий существуют также свои объекты событий, которые добавляют ряд своих свойств. Так, для работы с событиями указателя мыши определен объект MouseEvent, который добавляет следующие свойства:

  • altKey: возвращает true, если была нажата клавиша Alt во время генерации события.

  • button: указывает, какая кнопка мыши была нажата.

  • clientX: определяет координату Х окна браузера, на которой находился указатель мыши во время генерации события.

  • clientY: определяет координату Y окна браузера, на которой находился указатель мыши во время генерации события.

  • ctrlKey: возвращает true, если была нажата клавиша Ctrl во время генерации события.

  • metaKey: возвращает true, если была нажата во время генерации события метаклавиша клавиатуры.

  • relatedTarget: определяет вторичный источник возникновения события.

  • screenX: определяет координату Х относительно верхнего левого угла экрана монитора, на которой находился указатель мыши во время генерации события.

  • screenY: определяет координату Y относительно верхнего левого угла экрана монитора, на которой находился указатель мыши во время генерации события.

  • shiftKey: возвращает true, если была нажата клавиша Shift во время генерации события.

Определим координаты клика:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <style>
        #blueRect {
            width: 100px;
            height: 100px;
            background-color: blue;
        }
    </style>
</head>
<body>
    <div id="blueRect">
    </div>
    <script>
        function handleClick(e) {
            console.log("screenX: " + e.screenX);
            console.log("screenY: " + e.screenY);
            console.log("clientX: " + e.clientX);
            console.log("clientY: " + e.clientY);
        }
        var blueRect = document.getElementById("blueRect");
        blueRect.addEventListener("click", handleClick);
    </script>
</body>
</html>

9.6. События клавиатуры

Другим распространенным типом событий являются события клавиатуры.

  • keydown: возникает при нажатии клавиши клавиатуры и длится, пока нажата клавиша.

  • keyup: возникает при отпускании клавиши клавиатуры.

  • keypress: возникает при нажатии клавиши клавиатуры, но после события keydown и до события keyup. Надо учитывать, что данное событие генерируется только для тех клавиш, которые формируют вывод в виде символов, например, при печати символов. Нажатия на остальные клавиши, например, на Alt, не учитываются.

Для работы с событиями клавиатуры определен объект KeyboardEvent, который добавляет к свойствам объекта Event ряд специфичных для клавиатуры свойств:

  • altKey: возвращает true, если была нажата клавиша Alt во время генерации события.

  • key: возвращает символ нажатой клавиши, например, при нажатии на клавишу T это свойство будет содержать T. А если нажата клавиша Я, то это свойство будет содержать Я.

  • code: возвращает строковое представление нажатой клавиши физической клавиатуры QWERTY, например, при нажатии на клавишу T это свойство будет содержать KeyT, а при нажатии на клавишу ; (точка запятой), то свойство возвратит Semicolon.

При использовании этого свойства следует учитывать ряд момент. Прежде всего используется клавиатура QWERTY. То есть переключая раскладку, к примеру, на русскоязычную и нажмем на клавишу Я, то значением будет KeyZ - на клавиатуре QWERTY клавиша Z представляет ту же клавишу, что и на русскоязычной раскладке Я.

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

  • ctrlKey: возвращает true, если была нажата клавиша Ctrl во время генерации события.

  • metaKey: возвращает true, если была нажата во время генерации события метаклавиша клавиатуры.

  • shiftKey: возвращает true, если была нажата клавиша Shift во время генерации события.

Например, можно с помощью клавиш клавиатуры перемещать элемент на web-странице:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <style>
        html,
        body {
            margin: 0;
            overflow: hidden;
        }

        #blueRect {
            width: 100px;
            height: 100px;
            background-color: blue;
        }
    </style>
</head>
<body>
    <div id="blueRect"></div>
    <script>
        function moveRect(e) {
            var blueRect = document.getElementById("blueRect");
            // получаем стиль для blueRect
            var cs = window.getComputedStyle(blueRect);
            var left = parseInt(cs.marginLeft);
            var top = parseInt(cs.marginTop);
            switch (e.key) {
                case "ArrowLeft": // если нажата клавиша влево
                    if (left > 0)
                        blueRect.style.marginLeft = left - 10 + "px";
                    break;
                case "ArrowTop": // если нажата клавиша вверх
                    if (top > 0)
                        blueRect.style.marginTop = top - 10 + "px";
                    break;
                case "ArrowRight": // если нажата клавиша вправо
                    if (left < document.documentElement.clientWidth - 100)
                        blueRect.style.marginLeft = left + 10 + "px";
                    break;
                case "ArrowDown": // если нажата клавиша вниз
                    if (top < document.documentElement.clientHeight - 100)
                        blueRect.style.marginTop = top + 10 + "px";
                    break;
            }
        }
        addEventListener("keydown", moveRect);
    </script>
</body>
</html>

В данном случае обрабатывается событие keydown. В обработчике moveRect с помощью метода window.getComputedStyle() получаем стиль элемента blueRect. А затем из этого стиля выбираем значения свойств marginLeft и marginTop.

С помощью свойства e.key получаем нажатую клавишу. Список кодов клавиш клавиатуры можно посмотреть на странице https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values.

Здесь нас интересуют четыре клавиши: вверх, вниз, влево, вправо. Им соответственно будут соответствовать названия ArrowTop, ArrowDown, ArrowLeft и ArrowRight. Если одна из них нажата, производим действия: увеличение или уменьшение отступа элемента от верхней или левой границы. Ну и чтобы элемент не выходил за границы окна, проверяем предельные значения с помощью document.documentElement.clientWidth (ширина корневого элемента) и document.documentElement.clientHeight.

10. Работа с формами

10.1. Формы и их элементы

Один из способов взаимодействия с пользователями представляют HTML-формы. Например, если необходимо получить от пользователя некоторую информацию, можно определить на web-странице формы, которая будет содержать текстовые поля для ввода информации и кнопку для отправки. И после ввода данных можно обработать введенную информацию.

Для создания формы используется элемент <form>:

<form name="search">
</form>

В JavaScript форма представлена объектом HtmlFormElement. И после создания формы можно к ней обратиться различными способами.

Первый способ заключается в прямом обращении по имени формы:

var searchForm = document.search;

Второй способ состоит в обращении к коллекции форм документа и поиске в ней нужной формы:

<!DOCTYPE html>
<html>
    <head>
       <meta charset="utf-8" />
    </head>
    <body>
       <form name="search"></form>
       <form name="settings"></form>
       <script>
          var searchForm;
          for (var i = 0; i < document.forms.length; i++) {
              if(document.forms[i].name==="search")
                  searchForm = document.forms[i];
          }
          document.write(searchForm.name);
       </script>
    </body>
</html>

С помощью свойства name объекта формы можно получить значение атрибута name у соответствующего элемента формы в коде HTML.

Еще один способ сочетает оба подхода:

var searchForm = document.forms["search"];

И также можно применять стандартные способы для поиска элемента формы, например, по идентификатору, по тегу или по селектору.

var searchForm = document.getElementsByTagname("form")[0]

Форма имеет ряд свойств, из которых наиболее важными являются, рассмотренное выше, свойство name. Так же имеется свойство elements, которое содержит коллекцию элементов формы.

<form name="search">
    <input type="text" name="key"/>
    <input type="submit" name="send"/>
</form>
<script>
    var searchForm = document.forms["search"];
    for(var i=0; i<searchForm.elements.length;i++)
    document.write(searchForm.elements[i].name + "<br>");
</script>
Пример формы

Среди методов формы надо отметить метод submit(), который отправляет данные формы на сервер. Так же есть метод reset(), который очищает поля формы.

var searchForm = document.forms["search"];
searchForm.submit();
searchForm.reset();

10.1.1. Элементы форм

Форма может содержать различные элементы ввода HTML: input, textarea, button, select и т.д. Но все они имеют ряд общих свойств и методов.

Так же, как и форма, элементы форм имеют свойство name, с помощью которого можно получить значение атрибута name:

<form name="search">
    <input type="text" name="key" value="hello world"/>
    <input type="submit" name="send"/>
</form>
<script>
    var searchForm = document.forms["search"];
    // выведем имя всех элементов
    for(var i=0; i<searchForm.elements.length;i++)
    document.write(searchForm.elements[i].name + "<br>");

    // получим по имени текстовое поле
    var keyBox = searchForm.elements["key"];
    document.write(keyBox.name); // key
</script>

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

var searchForm = document.forms["search"];
var keyBox = searchForm.elements["key"];
document.write(keyBox.value); // hello world
// установка значения
keyBox.value = "Привет мир";

С помощью свойства form можно получить родительский объект формы:

var searchForm = document.forms["search"];
var keyBox = searchForm.elements["key"];
document.write(keyBox.form.name); // search

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

Свойство type позволяет получить тип поля ввода. Это либо название тега элемента, либо значение атрибута type у элементов input.

Из методов можно выделить методы:

  • focus(): устанавливает фокус на элемент

  • blur(): убирает фокус с элемента

var searchForm = document.forms["search"];
var keyBox = searchForm.elements["key"];
keyBox.focus();

10.2. Кнопки

Для отправки введенных данных на форме используются кнопки. Для создания кнопки используется либо элемент button:

<button name="send">Отправить</button>

Либо элемент input:

<input type="submit" name="send" value="Отправить"/>

С точки зрения функциональности в HTML эти элементы не совсем равноценны, но в данном случае они нас интересуют с точки зрения взаимодействия с кодом JavaScript.

При нажатии на любой из этих двух вариантов кнопки происходит отправка формы по адресу, который указан у формы в атрибуте action, либо по адресу web-страницы, если атрибут action не указан. Однако в коде JavaScript можно перехватить отправку, обрабатывая событие click.

<!DOCTYPE html>
<html>
    <head>
       <meta charset="utf-8" />
    </head>
    <body>
       <form name="search">
          <input type="text" name="key"/>
          <input type="submit" name="send" value="Отправить" />
       </form>
       <script>
          function sendForm(e) {
             // получаем значение поля key
             var keyBox = document.search.key;
             var val = keyBox.value;
             if (val.length > 5) {
                alert("Недопустимая длина строки");
                e.preventDefault();
             } else {
                alert("Отправка разрешена");
             }
          }

          var sendButton = document.search.send;
          sendButton.addEventListener("click", sendForm);
       </script>
    </body>
</html>

При нажатии на кнопку происходит событие click, и для его обработки к кнопке прикрепляем обработчик sendForm. В этом обработчике проверяем введенный в текстовое поле текст. Если его длина больше 5 символов, то выводим сообщение о недопустимой длине и прерываем обычный ход события с помощью вызова e.preventDefault(). В итоге форма не отправляется.

Если же длина текста меньше шести символов, то также выводится сообщение, и затем форма отправляется.

Пример формы

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

function sendForm(e) {
    // получаем значение поля key
    var keyBox = document.search.key;
    var val = keyBox.value;
    if (val.length > 5) {
        alert("Недопустимая длина строки");
        document.search.action="PostForm";
    } else {
        alert("Отправка разрешена");
    }
}

В данном случае, если длина текста больше 5 символов, то текст отправляется, только теперь он отправляется по адресу PostForm, поскольку задано свойство action:

document.search.action="PostForm";

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

<button type="reset">Очистить</button>
<input type="reset" value="Очистить"/>

При нажатии на кнопки произойдет очистка форм. Но также функциональность по очистке полей формы можно реализовать с помощью метода reset():

function sendForm(e) {
    // получаем значение поля key
    var keyBox = document.search.key;
    var val = keyBox.value;
    if (val.length > 5) {
        alert("Недопустимая длина строки");
        document.search.reset();
        e.preventDefault();
    } else {
        alert("Отправка разрешена");
    }
}

Кроме специальных кнопок отправки и очистки на форме также может использоваться обычная кнопка:

<input type="button" name="send" value="Отправить"/>

При нажатии на подобную кнопку отправки данных не происходит, хотя также генерируется событие click:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
</head>
<body>
    <form name="search">
        <input type="text" name="key" placeholder="Введите ключ"/>
        <input type="button" name="print" value="Печать"/>
    </form>
    <div id="printBlock"></div>
    <script>
        function printForm(e) {
            // получаем значение поля key
            var keyBox = document.search.key;
            var val = keyBox.value;
            // получаем элемент printBlock
            var printBlock = document.getElementById("printBlock");
            // создаем новый параграф
            var pElement = document.createElement("p");
            // устанавливаем у него текст
            pElement.textContent = val;
            // добавляем параграф в printBlock
            printBlock.appendChild(pElement);
        }

        var printButton = document.search.print;
        printButton.addEventListener("click", printForm);
    </script>
</body>
</html>

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

Пример параграфа

10.3. Текстовые поля

Для ввода простейшей текстовой информации предназначены элементы <input type="text":

<input type="text" name="kye" size="10" maxlength="15" value="hello world"/>

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

  • focus: происходит при получении фокуса.

  • blur: происходит при потере фокуса.

  • change: происходит при изменении значения поля.

  • select: происходит при выделении текста в текстовом поле.

  • keydown: происходит при нажатии клавиши клавиатуры.

  • keypress: происходит при нажатии клавиши клавиатуры для печатаемых символов.

  • keyup: происходит при отпускании ранее нажатой клавиши клавиатуры.

<!DOCTYPE html>
<html>
<head>
   <meta charset="utf-8" />
</head>
<body>
   <form name="search">
      <input type="text" name="key" placeholder="Введите ключ"/>
      <input type="button" name="print" value="Печать"/>
   </form>
   <div id="printBlock"></div>
   <script>
      var keyBox = document.search.key;

      // обработчик изменения текста
      function onchange(e) {
          // получаем элемент printBlock
          var printBlock = document.getElementById("printBlock");
          // получаем новое значение
          var val = e.target.value;
          // установка значения
          printBlock.textContent = val;
      }
      // обработка потери фокуса
      function onblur(e) {
          // получаем его значение и обрезаем все пробелы
          var text = keyBox.value.trim();
          if (text === "")
              keyBox.style.borderColor = "red";
          else
              keyBox.style.borderColor = "green";
      }
      // получение фокуса
      function onfocus(e) {
          // установка цвета границ поля
          keyBox.style.borderColor = "blue";
      }
      keyBox.addEventListener("change", onchange);
      keyBox.addEventListener("blur", onblur);
      keyBox.addEventListener("focus", onfocus);
   </script>
</body>
</html>

Здесь к текстовому полю прикрепляется три обработчика для событий blur, focus и change. Обработка события change позволяет сформировать что-то вроде привязки: при изменении текста весь текст отображается в блоке printBlock. Но надо учитывать, что событие change возникает не сразу после изменения текста, а после потери им фокуса.

Обработка события потери фокуса blur позволяет провести валидацию введенного значения. Например, в данном случае если текст состоит из пробелов или не был введен, то окрашиваем границу поля в красный цвет.

Поле ввода

Кроме данного текстового поля есть еще специальные поля ввода. Так, поле <input type="password" предназначено для ввода пароля. По функциональности оно во многом аналогично обычному текстовому полю за тем исключением, что для вводимых символов используется маска:

<input type="password" name="password" />

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

<input type="hidden" name="id" value="345" />

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

10.3.1. Элемент textarea

Для создания многострочных текстовых полей используется элемент textarea:

<textarea rows="15" cols="40" name="textArea"></textarea>

Данные элемент генерирует все те же самые события, что и обычное текстовое поле:

<!DOCTYPE html>
<html>
<head>
   <meta charset="utf-8" />
</head>
<body>
    <form name="search">
        <textarea rows="7" cols="40" name="message"></textarea>
    </form>
    <div id="printBlock"></div>
    <script>
        var messageBox = document.search.message;

        // обработчик ввода символа
        function onkeypress(e) {
            // получаем элемент printBlock
            var printBlock = document.getElementById("printBlock");
            // получаем введенный символ
            var val = String.fromCharCode(e.keyCode);
            // добавление символа
            printBlock.textContent += val;
        }

        function onkeydown(e) {
            if (e.keyCode === 8) { // если нажат Backspace
                // получаем элемент printBlock
                var printBlock = document.getElementById("printBlock"),
                length = printBlock.textContent.length;
                // обрезаем строку по последнему символу
                printBlock.textContent = printBlock.textContent.substring(0, length-1);
            }
        }

        messageBox.addEventListener("keypress", onkeypress);
        messageBox.addEventListener("keydown", onkeydown);
   </script>
</body>
</html>

Здесь к текстовому полю прикрепляются обработчики для событий keypress и keydown. В обработчике keypress получаем введенный символ с помощью конвертации числового кода клавиши в строку:

var val = String.fromCharCode(e.keyCode);

Затем символ добавляется к содержимому блока printBlock.

Событие keypress возникает при нажатии на клавиши для печатаемых символов, то такие символы отображаются в текстовом поле. Однако есть и другие клавиши, которые оказывают влияние на текстовое поле, но они не дают отображаемого символа, поэтому не отслеживаются событием keypress. К таким клавишам относится клавиша Backspace, которая удаляет последний символ. И для ее отслеживания также обрабатываем событие keydown. В обработчике keydown удаляем из строки в блоке printBlock последний символ.

Блок для ввода

10.4. Флажки и переключатели

Особую группу элементов ввода составляют флажки и переключатели.

10.4.1. Флажки

Флажки представляют поле, создаваемое с помощью элемента <input type="checkbox" и в которое можно поставить отметки. Отличительную особенность флажка составляет свойство checked, которое в отмеченном состоянии принимает значение true:

<form name="myForm" xmlns="http://www.w3.org/1999/html">
    <input type="checkbox" name="enabled" checked><span>Включить</span></input>
</form>
<div id="printBlock"></div>
<script>
    var enabledBox = document.myForm.enabled;

    function onclick(e) {
        var printBlock = document.getElementById("printBlock");
        var enabled = e.target.checked;
        printBlock.textContent = enabled;
    }

    enabledBox.addEventListener("click", onclick);
</script>

Нажатие на флажок генерирует событие click. В данном случае при обработке данного события просто выводится информация, отмечен ли данный флажок, в блок div.

Флажок

10.4.2. Переключатели

Переключатели представляют группы кнопок, из которых можно выбрать только одну. Переключатели создаются элементом <input type="radio".

Выбор или нажатие на одну из них также представляет событие click:

<form name="myForm">
    <input type="radio" name="languages" checked="checked" value="Java" /><span>Java</span>
    <input type="radio" name="languages" value="C#" /><span>C#</span>
    <input type="radio" name="languages" value="C++" /><span>C++</span>
</form>
<div id="printBlock"></div>
<script>
    function onclick(e) {
        var printBlock = document.getElementById("printBlock");
        var language = e.target.value;
        printBlock.textContent = "Вы выбрали: " + language;
    }
    for (var i = 0; i < myForm.languages.length; i++) {
        myForm.languages[i].addEventListener("click", onclick);
    }
</script>

При создании группы переключателей их атрибут name должен иметь одно и то же значение. В данном случае это - languages. То есть переключатели образуют группу languages.

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

for (var i = 0; i < myForm.languages.length; i++) {
    myForm.languages[i].addEventListener("click", onclick);
}

Значение выбранного переключателя также можно получить через объект Event: e.target.value

Флажок

Каждый переключатель также, как и флажок, имеет свойство checked, которое возвращает значение true, если переключатель выбран. Например, отметим последний переключатель:

myForm.languages[myForm.languages.length-1].checked = true;

10.5. Список

Для создания списка используется HTML-элемент select. Причем с его помощью можно создавать как выпадающие списки, так и обычные с одинарным или множественным выбором.

Стандартный список
<select name="language" size="4">
    <option value="JS" selected="selected">JavaScript</option>
    <option value="Java">Java</option>
    <option value="C#">C#</option>
    <option value="C++">C++</option>
</select>

Атрибут size позволяет установить, сколько элементов будут отображаться одномоментно в списке. Значение size="1" отображает только один элемент списка, а сам список становится выпадающим. Если установить у элемента select атрибут multiple, то в списке можно выбрать сразу несколько значений.

Каждый элемент списка представлен HTML-элементом option, у которого есть отображаемая метка и есть значения в виде атрибута value.

В JavaScript элементу select соответствует объект HTMLSelectElement, а элементу option - объект HtmlOptionElement или просто Option.

Все элементы списка в JavaScript доступны через коллекцию options. А каждый объект HtmlOptionElement имеет свойства: index, text (отображаемый текст) и value (значение элемента). Например, получим первый элемент списка и выведем о нем через его свойства всю информацию:

Поля выбора
<form name="myForm">
    <select name="language" size="4">
       <option value="JS" selected="selected">JavaScript</option>
       <option value="Java">Java</option>
       <option value="CS">C#</option>
       <option value="CPP">C++</option>
    </select>
</form>
<script>
    var firstLanguage = myForm.language.options[0];
    document.write("Index: " + firstLanguage.index + "<br>");
    document.write("Text: " + firstLanguage.text + "<br>");
    document.write("Value: " + firstLanguage.value + "<br>");
</script>

В JavaScript можно не только получать элементы, но и динамически управлять списком.

Добавление и удаление объектов списка
<!DOCTYPE html>
<html>
<head>
   <meta charset="utf-8" />
</head>
<body>
    <form name="myForm">
        <select name="language" size="5">
            <option value="JS" selected="selected">JavaScript</option>
            <option value="Java">Java</option>
            <option value="CS">C#</option>
            <option value="CPP">C++</option>
        </select>
        <p><input type="text" name="textInput" placeholder="Введите текст"/></p>
        <p><input type="text" name="valueInput" placeholder="Введите значение"/></p>
        <p>
            <input type="button" name="addButton" value="Добавить" />
            <input type="button" name="removeButton" value="Удалить" />
        </p>
    </form>
    <script>
        var addButton = myForm.addButton,
        removeButton = myForm.removeButton,
        languagesSelect = myForm.language;
        // обработчик добавления элемента
        function addOption() {
            // получаем текст для элемента
            var text = myForm.textInput.value;
            // получаем значение для элемента
            var value = myForm.valueInput.value;
            // создаем новый элемента
            var newOption = new Option(text, value);
            languagesSelect.options[languagesSelect.options.length]=newOption;
        }
        // обработчик удаления элемент
        function removeOption() {
            var selectedIndex = languagesSelect.options.selectedIndex;
            // удаляем элемент
            languagesSelect.options[selectedIndex] = null;
        }

        addButton.addEventListener("click", addOption);
        removeButton.addEventListener("click", removeOption);
    </script>
</body>
</html>

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

За добавление в коде JavaScript отвечает функция addOption(), в которой получаем введенные в текстовые поля значения, создаем новый объект Option и добавляем его в массив options объекта списка.

За удаление отвечает функция removeOption(), в которой просто получаем индекс выделенного элемента с помощью свойства selectedIndex и в коллекции options приравниваем по этому индексу значение null.

Поля выбора

Для добавления/удаления также в качестве альтернативы можно использовать методы элемента select:

// вместо вызова
// languagesSelect.options[languagesSelect.options.length]=newOption;
// использовать для добавления вызов метода add
languagesSelect.add(newOption);
// вместо вызова
// languagesSelect.options[selectedIndex] = null;
// использовать для удаления метод remove
languagesSelect.remove(selectedIndex);

10.5.1. События элемента select

Элемент select поддерживает три события:

  • blur потеря фокуса

  • focus получение фокуса

  • change изменение выделенного элемента в списке.

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

<form name="myForm">
    <select name="language" size="5">
        <option value="JS" selected="selected">JavaScript</option>
        <option value="Java">Java</option>
        <option value="CS">C#</option>
        <option value="CPP">C++</option>
    </select>
</form>
<div id="selection"></div>
<script>
    var languagesSelect = myForm.language;

    function changeOption() {
        var selection = document.getElementById("selection");
        var selectedOption = languagesSelect.options[languagesSelect.selectedIndex];
        selection.textContent = "Вы выбрали: " + selectedOption.text;
    }

    languagesSelect.addEventListener("change", changeOption);
</script>

11. Хранение данных

11.1. Куки

Одну из возможностей сохранения данных в JavaScript представляет использование куки. Для работы с куками в объекте document предназначено свойство cookie.

Для установки куков достаточно свойству document.cookie присвоить строку с куками:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
</head>
<body>
    <script>
        document.cookie = "login=tom32;";
    </script>
</body>
</html>

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

Установка куки в JavaScript

Строка куки принимает до шести различных параметров:

  • имя куки

  • значение

  • срок окончания действия (expires)

  • путь (path)

  • домен (domain)

  • secure

Выше использовались только два параметра: имя куки и значение. То есть в случае со строкой "login=tom32;" куки имеет имя login и значение tom32.

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

И в этом случае нам надо установить параметр expires, то есть срок действия куков:

document.cookie = "login=tom32;expires=Mon, 30 Aug 2021 00:00:00 GMT;";

То есть срок действия куки login истекает в понедельник 30 августа 2021 года в 00:00. Формат параметра expires очень важен. Однако его можно сгенерировать программно. Для этого мы можем использовать метод toUTCString() объекта Date:

var expire = new Date();
expire.setHours(expire.getHours() + 4);
document.cookie = "login=tom32;expires=" + expire.toUTCString() + ";";

В данном случае срок действия куки будет составлять 4 часа.

Если вдруг нам надо установить куки для какого-то определенного пути на сайте, то мы можем использовать параметр path. Например, мы хотим установить куки только для пути www.mysite.com/home:

document.cookie = "login=tom32;expires=Mon, 31 Aug 2015 00:00:00 GMT;path=/home;";

В этом случае для других путей на сайте, например, www.mysite.com/shop, эти куки будут недоступны.

Если на нашем сайте есть несколько доменов, и мы хотим установить куки непосредственно для определенного домена, тогда можно использовать параметр domain. Например, у нас на сайте есть поддомен blog.mysite.com:

document.cookie = "login=tom32;expires=Mon, 31 Aug 2015 00:00:00 GMT;path=/;domain=blog.mysite.com;";

Параметр path=/ указывает, что куки будут доступны для всех директорий и путей поддомена blog.mysite.com.

Последний параметр - secure задает использование SSL (SecureSockets Layer) и подходит для сайтов, использующих протокол HTTPS. Если значение этого параметра равно true, то куки будут использоваться только при установке защищенного соединения ssl. По умолчанию данный параметр равен false.

document.cookie = "login=tom32;expires=Mon, 31 Aug 2015 00:00:00 GMT;path=/;domain=blog.mysite.com;secure=true;";

11.1.1. Получение куки

Для простейшего извлечения куки из браузера достаточно обратиться к свойству document.cookie:

var expire = new Date();
expire.setHours(expire.getHours() + 4);
document.cookie = "city=Berlin;expires="+expire.toUTCString()+";";
document.cookie = "country=Germany;expires="+expire.toUTCString()+";";
document.cookie = "login=tom32;";
document.write(document.cookie);

Здесь были установлены три куки, и браузер выведет нам все эти куки:

Получить куки в JavaScript

Извлеченные куки не включают параметры expires, path, domain и secure. Кроме того, сами куки разделяются точкой с запятой, поэтому нужно еще провести некоторые преобразования, чтобы получить их имя и значение:

var cookies = document.cookie.split(";");
for (var i = 0; i < cookies.length; i++) {
    var parts = cookies[i].split("=");
    name = parts[0];
    value = parts[1];
    document.write("Имя куки: " + name + "<br>");
    document.write("Значение: " + value + "<br><br>");
}

11.2. Web Storage

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

Поэтому в HTML5 была внедрена новая концепция для хранения данных - web storage. Web storage состоит из двух компонентов:

  • session storage

  • local storage

Session storage представляет временное хранилище информации, которая удаляется после закрытия браузера.

Local storage представляет хранилище для данных на постоянной основе. Данные из local storage автоматически не удаляются и не имеют срока действия. Эти данные не передаются на сервер в запросе HTTP. Кроме того, объем local storage составляет в Chrome и Firefox 5 Мб для домена, а в IE - 10 Мб.

Все данные в web storage представляют набор пар ключ-значение. То есть каждый объект имеет уникальное имя-ключ и определенное значение.

Для работы с local storage в JavaScript используется объект localStorage, а для работы с session storage - объект sessionStorage.

Для сохранения данных надо передать в метод setItem() объекта localStorage:

localStorage.setItem("login", "tom32@gmail.com");

В этот метод передаются два значения: ключ и значение сохраняемого объекта.

Если в localStorage уже есть объект с ключом login, то его значение заменяется новым.

Для получения сохраненных данных надо вызвать метод getItem():

var login = localStorage.getItem("login"); // tom32@gmail.com

В этот метод передается ключ объекта.

Чтобы удалить объект, применяется метод removeItem(), который принимает ключ удаляемого объекта:

localStorage.removeItem("login");

И для полного удаления всех объектов из localStorage можно использовать метод clear():

localStorage.clear();

С сохранением простых объектов все просто, однако при этом надо учитывать, что данные в localStorage сохраняются в виде строки:

localStorage.setItem("age", 23);
var age = localStorage.getItem("age");
age = parseInt(age) + 10;
console.log(age);
33

Если в данном случае не преобразовать значение к числу с помощью parseInt(), то age будет действовать как строка.

Трудности могут возникнуть с сохранением сложных объектов:

var user = {
    name: "Tom",
    age: 23,
    married: false
};
localStorage.setItem("user", user);
var savedUser = localStorage.getItem("user");
console.log(savedUser);
console.log(savedUser.name);
[object Object]
undefined

В этом случае нам надо использовать сериализацию в формат JSON:

var user = {
    name: "Tom",
    age: 23,
    married: false
};

localStorage.setItem("user", JSON.stringify(user));
var savedUser = JSON.parse(localStorage.getItem("user"));
console.log(savedUser.name + " " + savedUser.age +" " + savedUser.married);
Tom 23 false

И в завершении надо сказать, что в некоторых браузерах с помощью специальных инструментов мы можем увидеть сохраненные объекты в local storage. Например, в Google Chrome:

Local Storage в JavaScript

12. Коллекции и итераторы

12.1. Итераторы

Итераторы применяются для организации последовательного доступа к элементам коллекции — массивам, объектам Set и Map. Итераторы предоставляют метод next(), который возвращает два значения: value и done. value хранит собственно значение текущего перебираемого элемента. А свойство done указывает, есть ли еще в коллекции объекты, доступные для перебора.

Некоторые методы коллекций возвращают итераторы. Например, метод entries(), который есть у коллекций Array, Set, Map:

let users = ["Tom", "Bob", "Sam"];
let items = users.entries();
console.log(items.next());

Метод next() возвратит следующий объект на консоль:

{value: Array(2), done: false}
done:	false
value:	Array(2)
0:	0
1:	"Tom"
length:	2
__proto__:	Array(0)
__proto__:	Object

Здесь мы видим, что свойство done имеет значение false, так как мы перебрали только один элемент в множестве, и там еще есть два элемента.

Свойство value представляет массив из двух значений. Первое значение представляет ключ или индекс элемента массива, а второй элемент — значение по этому индексу имеет ключ и значение.

Соответственно мы можем организовать и перебор всей коллекции:

let users = ["Tom", "Bob", "Sam"];
let items = users.entries();
var result = items.next();
while (result.done === false) {
    console.log(result.value[0], result.value[1]);
    result = items.next();
}
Консольный вывод
0 Tom
1 Bob
2 Sam

Но в этом нет смысла, поскольку все коллекции, возвращающие итераторы, поддерживают перебор с помощью цикла for …​ of, который как раз и использует итератор для получения элементов:

let users = ["Tom", "Bob", "Sam"];
for(let val of users) {
    console.log(val);
}

Если мы хотим извлечь еще и индекс элемента в массиве, то мы можем использовать для перебора итератор из entries():

let users = ["Tom", "Bob", "Sam"];
for (let user of users.entries()) {
    console.log(user[0], user[1]);
}
Консольный вывод
0 Tom
1 Bob
2 Sam

12.2. Генераторы

Генераторы представляют особый тип функции, которые используются для генерации значений. Для определения генераторов применяется символ звездочки *, который ставится после ключевого слова function. Например, определим простейший генератор:

function* getNumber() {
    yield 5;
}
let numberGenerator = getNumber();
let next = numberGenerator.next();
console.log(next);

Функция getNumber() представляет генератор. Функция генератора возвращает итератор. Для получения значения из генератора применяется оператор yield. То есть фактически в данном случае генератор генерирует число 5.

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

Для перехода к следующему значению применяется метод next(). Если мы посмотрим на консольный вывод, то мы увидим, что данный метод возвращает следующие данные:

{value: 5, done: false}

То есть по сути возвращается объект, свойство value которого содержит собственно сгенерированное значение. А свойство done указывает, достигли ли мы конца генератора.

Теперь изменим код:

function* getNumber() {
    yield 5;
}
let numberGenerator = getNumber();
let next = numberGenerator.next();
console.log(next);
next = numberGenerator.next();
console.log(next);

Здесь обращение к методу next() происходит два раза:

{value: 5, done: false}
{value: undefined, done: true}

Но функция генератора getNumber() генерирует только одно значение — число 5. Поэтому при повторном вызове свойство value будет иметь значение undefined, а свойство done - true, то есть работа генератора завершена.

Генератор может создавать множество значений:

function* getNumber() {
    yield 5;
    yield 25;
    yield 125;
}
let numberGenerator = getNumber();
console.log(numberGenerator.next());
console.log(numberGenerator.next());
console.log(numberGenerator.next());
console.log(numberGenerator.next());
Консольный вывод
{value: 5, done: false}
{value: 25, done: false}
{value: 125, done: false}
{value: undefined, done: true}

То есть при первом вызове метода next() из итератора извлекается значение, которое идет после первого оператора yield, при втором вызове метода next() - значение после второго оператора yield и так далее.

Поскольку для получения значений применяется итератор, то мы можем использовать цикл for …​ of:

function* getNumber() {
    yield 5;
    yield 25;
    yield 125;
}
let numberGenerator = getNumber();
for(let num of numberGenerator) {
    console.log(num);
}
Консольный вывод
5
25
125

Генератор необязательно содержит только определение операторов yield. Он также может содержать более сложную логику.

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

function* points() {
    let x = 0;
    let y = 0;
    while(true) {
        yield {x:x, y:y};
        x += 2;
        y += 1;
    }
}
let pointGenerator = points();
console.log(pointGenerator.next().value);
console.log(pointGenerator.next().value);
console.log(pointGenerator.next().value);
Консольный вывод
{x: 0, y: 0}
{x: 2, y: 1}
{x: 4, y: 2}

12.2.1. Передача данных в генератор

С помощью next() можно передать в генератор данные.

function* getNumber() {
    let n = yield 5;
    console.log("n:", n);
    let m = yield 25 * n;
    console.log("m:", m);
    yield 125 * m;
}
let numberGenerator = getNumber();
console.log(numberGenerator.next().value);
console.log(numberGenerator.next(2).value);
console.log(numberGenerator.next(3).value);
Консольный вывод
5
n: 2
50
m: 3
375

При втором вызове метода next():

numberGenerator.next(2).value

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

let n = yield 5;

То есть здесь переменная n будет равна 2, так как в метод next() передается число 2.

Далее мы можем использовать это значение, например, для генерации нового значения:

let m = yield 25 * n;

Соответственно, переменная m получить значение, переданное через третий вызов метода next(), то есть число 3.

12.2.2. Инициализация генератора

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

function* takeItem(arr) {
    for (var i = 0; i < arr.length; i++) {
        yield arr[i];
    }
}
var users = ["Tom", "Bob", "Sam", "Alice", "Kate", "Ann"];
var userGenerator = takeItem(users);
var timer = setInterval(function() {
    var user = userGenerator.next();
    if (user.done) {
        clearInterval(timer);
        console.log("The End...");
    } else {
        console.log(user.value);
    }
}, 500);

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

12.3. Множества Set

Множества (sets) представляют структуру данных, которая может хранить только уникальные значения. В JavaScript функционал множества определяет объект Set. Для создания множества применяется конструктор этого объекта:

let mySet = new Set();

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

let arr = [1, 1, 2, 3, 4, 5, 2, 4];
let numbers = new Set(arr);
console.log(numbers);
Set(5) {1, 2, 3, 4, 5}

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

Для проверки количества элементов можно использовать свойство size.

let arr = [1, 1, 2, 3, 4, 5, 2, 4];
let numbers = new Set(arr);
console.log(numbers.size);
5

12.3.1. Добавление

Для добавления применяется метод add(). Его результатом является измененное множество:

let numbers = new Set();
numbers.add(1);
numbers.add(3);
numbers.add(5);
numbers.add(3); // не добавляется, т.к. есть
numbers.add(1); // не добавляется, т.к. есть
console.log(numbers);
Set(3) {1, 3, 5}

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

Так как метод add() возвращает ссылку на это же множество, то мы можем вызывать методы по цепочке:

let numbers = new Set();
numbers.add(1).add(3).add(5);
console.log(numbers);
Set(3) {1, 3, 5}

12.3.2. Удаление

Для удаления элементов применяется метод delete():

let numbers = new Set();
numbers.add(1).add(3).add(5);
numbers.delete(3);
console.log(numbers);
Set(2) {1, 5}

Причем данный метод возвращает булево значение: true - если элемент удален и false - если удаление не произошло (например, когда удаляемого элемента нет в множестве):

let numbers = new Set();
numbers.add(1).add(3).add(5);
let isDeleted = numbers.delete(3);
console.log(isDeleted);
isDeleted = numbers.delete(54);
console.log(isDeleted);
true
false

Если необходимо удалить вообще все элементы из множества, то применяется метод clear():

let numbers = new Set();
numbers.add(1).add(3).add(5);
numbers.clear();
console.log(numbers);
Set(0) {}

12.3.3. Проверка наличия элемента

Если нужно проверить, если ли элемент в множестве, то используется метод has(). Если элемент есть, то метод возвращает true, иначе возвращает false:

let numbers = new Set();
numbers.add(1).add(3).add(5);
console.log(numbers.has(3));
console.log(numbers.has(32));
true
false

12.3.4. Перебор множества

Для перебора элементов множества применяется метод forEach():

let arr = [1, 2, 3, 5];
let numbers = new Set(arr);
numbers.forEach(function(value1, value2, set) {
    console.log(value1);
})

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

Также для перебора множества можно использовать цикл for …​ of:

let numbers = new Set([1, 2, 3, 5]);
for (n of numbers) {
    console.log(n);
}

12.4. Map

Map или карта (отображение, словарь) представляет структуру данных, где каждый элемент имеет ключ и значение. Ключи в рамках карты являются уникальными, то есть с одним ключом может быть сопоставлен только один элемент. Для создания карты применяется конструктор объекта Map:

let myMap = new Map();

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

let myMap = new Map([[1, "a"], [2, "b"], [3, "c"]]);
console.log(myMap);
Map(3) {1 => "a", 2 => "b", 3 => "c"}

В данном случае числа 1, 2, 3 являются ключами, а строки a, b, c - значениями.

При этом ключи и значения необязательно должны быть одного типа:

let myMap = new Map([["a", 1], [2, "b"], ["c", true]]);
console.log(myMap);
Map(3) {"a" => 1, 2 => "b", "c" => true}}

12.4.1. Добавление и изменение элементов

Для добавления или изменения значения применяется метод set():

let myMap = new Map([[1, "a"], [2, "b"], [3, "c"]]);
myMap.set(4, "d"); // добавление элемента
myMap.set(2, "v"); // изменение элемента
console.log(myMap);
Map(4) {1 => "a", 2 => "v", 3 => "c", 4 => "d"}

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

12.4.2. Получение элементов

Для получения элемента по ключу применяется метод get(), в который передается ключ элемента:

let myMap = new Map([[1, "Tom"], [2, "Bob"], [3, "Sam"]]);
console.log(myMap.get(2));
console.log(myMap.get(7));
Bob
undefined

Если map не содержит элемента по заданному ключу, то метод возвращает undefined.

Чтобы избежать возвращения undefined, мы можем проверить наличие элемента по ключу с помощью метода has(). Если элемент по ключу имеется, то метод возвращает true, иначе возвращается false:

let myMap = new Map([[1, "Tom"], [2, "Bob"], [3, "Sam"]]);
console.log(myMap.has(2));
console.log(myMap.has(7));
true
false

12.4.3. Удаление элементов

Для удаления одного элемента по ключу применяется метод delete():

let myMap = new Map([[1, "Tom"], [2, "Bob"], [3, "Sam"]]);
myMap.delete(2);
console.log(myMap);
Map(2) {1 => "Tom", 3 => "Sam"}

Для удаления всех элементов используется метод clear():

let myMap = new Map([[1, "Tom"], [2, "Bob"], [3, "Sam"]]);
myMap.clear();
console.log(myMap);
Map(0) {}

12.4.4. Перебор элементов

Для перебора элементов используется метод forEach():

let myMap = new Map([[1, "Tom"], [2, "Bob"], [3, "Sam"]]);
myMap.forEach(function(value1, value2, map) {
    console.log(value2, value1);
})

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

Консольный вывод
1 Tom
2 Bob
3 Sam

Также для перебора объекта Map можно использовать цикл for …​ of:

let myMap = new Map([[1, "Tom"], [2, "Bob"], [3, "Sam"]]);
for (item of myMap) {
    console.log(item[0], item[1]);
}

Каждый элемент из Map помещается в переменную item, которая в свою очередь представляет массив. Первый элемент этого массива — ключ, а второй элемент — значение элемента.

Также объект Map имеет два дополнительных метода: keys() позволяет перебрать только ключи и values() позволяет перебирать значения элементов. Оба метода возвращают итераторы, поэтому для перебора ключей и значений по отдельности также можно использовать цикл for…​of:

let myMap = new Map([[1, "Tom"], [2, "Bob"], [3, "Sam"]]);

for (item of myMap.keys()) {
    console.log(item);
}
for (item of myMap.values()) {
    console.log(item);
}

12.5. WeakSet и WeakMap

12.5.1. WeakSet

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

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

let weakSet1 = new WeakSet(); // пустой WeakSet
let weakSet2 = new WeakSet([{name:"Tom"}, {age: 34}]); // инициализация начальными значениями

Для инициализации как в случае с объектом Set в конструктор передается массив, но данный массив содержит именно объекты, а не скалярные значения, типа чисел или строк.

Для добавления данных в WeakSet применяется метод add():

let weakSet = new WeakSet();
weakSet.add({lang: "JavaScript"});
weakSet.add({lang: "TypeScript"});
// weakSet.add(34); // так нельзя - 34 - число, а не объект
console.log(weakSet);
{{lang: "JavaScript"}, {lang: "TypeScript"}}

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

Для удаления применяется метод delete(), в который передается ссылка на удаляемый объект:

let weakSet = new WeakSet();
var js = {lang: "JavaScript"};
var ts = {lang: "TypeScript"};
weakSet.add(js);
weakSet.add(ts);
weakSet.delete(js);
console.log(weakSet);
{{lang: "TypeScript"}}

Если надо проверить, имеется ли объект в WeakSet, то можно использовать метод has(), который возвращает true при наличии объекта:

var js = {lang: "JavaScript"};
var ts = {lang: "TypeScript"};
var java = {lang: "Java"};
let weakSet = new WeakSet([js, ts]);
console.log(weakSet.has(ts)); // true
console.log(weakSet.has(java)); //  false
true
false

12.5.2. WeakMap

WeakMap представляет развитие коллекции Map. Особенностью WeakMap является то, что все ее элементы должны представлять объекты. При этом объектами должны быть как ключи, так и значения.

Создание WeakMap:

// пустой WeakMap
let weakMap1 = new WeakMap();
// WeakMap с инициализацией данными
var key1 = {key:1};
var key2 = {key:2};
var value1 = {name: "Tom"};
var value2 = {name: "Sam"};
let weakMap2 = new WeakMap([[key1, value1], [key2, value2]]);

Для получения объектов по ключу из WeakMap применяется метод get():

var key1 = {key:1};
var key2 = {key:2};
var value1 = {name: "Tom"};
var value2 = {name: "Sam"};
let weakMap2 = new WeakMap([[key1, value1], [key2, value2]]);
console.log(weakMap2.get(key1));
{name: "Tom"}

Для добавления новых объектов или изменения старых применяется метод set():

var key1 = {key:1};
var key2 = {key:2};
var value1 = {name: "Tom"};
var value2 = {name: "Sam"};

let weakMap2 = new WeakMap([[key1, value1]]);
weakMap2.set(key2, value2);
weakMap2.set(key1, {name: "Kate"});
console.log(weakMap2.get(key1));
console.log(weakMap2.get(key2));
{name: "Kate"}
{name: "Sam"}

Чтобы проверить наличие элемента по определенному ключу, применяется метод has(), который возвращает true при наличии элемента:

var key1 = {key:1},
    key2 = {key:2};
var value1 = {name: "Tom"},
    value2 = {name: "Sam"};
let weakMap2 = new WeakMap([[key1, value1]]);
console.log(weakMap2.has(key1));
console.log(weakMap2.has(key2));
true
false

Для удаления элемента по ключу применяется метод delete():

var key1 = {key:1},
    key2 = {key:2};
var value1 = {name: "Tom"},
    value2 = {name: "Sam"};
let weakMap2 = new WeakMap([[key1, value1], [key2, value2]]);
console.log(weakMap2.has(key1));
weakMap2.delete(key1);
console.log(weakMap2.has(key1));
true
false

13. AJAX

Современные web-приложение, как правило, разделяются на две части: клиент и сервер. Клиент представляет собой web-страницу с кодом JavaScript. К серверным технологиям относятся Java, PHP, Ruby, Node.js, ASP.NET и т.д., которые получают запрос от клиента, обрабатывают и отправляют в ответ результат обработки.

AJAX представляет технологию для отправки запросов к серверу из клиентского кода JavaScript без перезагрузки страницы. Сам термин расшифровывается как Asynchronous JavaScript And XML. То есть изначально AJAX предполагал асинхронное взаимодействие клиента и сервера посредством данных в формате XML. Хотя сейчас XML во многом вытеснил формат JSON. В любом случае AJAX революционизировал web-среду, позволив создавать динамичные отзывчивые web-приложения.

Поскольку AJAX предполагает взаимодействие клиента и сервера, то для работы с AJAX и в частности этой главы необходим локальный web-сервер. Это может быть любой web-сервер: nginx, Apache, IIS и т.д.

13.1. Объект XMLHttpRequest

Для создания приложений, использующих AJAX, применяются различные способы. Но самым распространенным способом является использование объекта XMLHttpRequest:

var request = new XMLHttpRequest();

После создания объекта XMLHttpRequest можно отправлять запросы к серверу. Но для начала надо вызвать метод open() для инициализации:

request.open("GET", "http://localhost/hello.txt", false);

Метод open() принимает три параметра: тип запроса (GET, POST, HEAD, PUT), адрес запроса и третий необязательный параметр — логическое значение true или false, указывающее, будет ли запрос осуществляться в асинхронном режиме. То есть в данном случае запрос будет иметь тип GET, он будет направляться по адресу http://localhost/hello.txt в синхронном режиме, так как стоит значение false (для асинхронного режима указывается значение true).

Синхронный и асинхронный режим отличаются тем, что запрос в синхронном режиме пока запрос не выполнится, остальной код JavaScript не может выполняться. По умолчанию, если третий параметр не используется, то запрос отправляется в асинхронном режиме, что позволяет параллельно с выполнением запроса выполнять также и другой код JavaScript. И в большинстве случаев, как правило, используется именно асинхронный режим.

Кроме того, метод open() может принимать еще два параметра: логин и пароль пользователя, если для выполнения запроса нужна аутентификация.

request.open("GET", "http://localhost/home.php", true, "login", "password");

После инициализации запроса методом open() необходимо отправить запрос с помощью метода send():

request.send();

13.1.1. Свойства XMLHttpRequest

Объект XMLHttpRequest имеет ряд свойств, которые позволяют проконтролировать выполнение запроса:

  • status: содержит статусный код ответа HTTP, который пришел от сервера. С помощью статусного кода можно судить об успешности запроса или об ошибках, которые могли бы возникнуть при его выполнении. Например, статусный код 200 указывает на то, что запрос прошел успешно. Код 403 говорит о необходимости авторизации для выполнения запроса, а код 404 сообщает, что ресурс не найден и так далее.

  • statusText: возвращает текст статуса ответа, например, "200 OK"

  • responseType: возвращает тип ответа. Есть следующие типы:

    • ``

    • arraybuffer

    • blob

    • document

    • json

    • text

  • response: возвращает ответ сервера

  • responseText: возвращает текст ответа сервера

  • responseXML: возвращает xml, если ответ от сервера в формате xml

Например, выполним запрос к текстовому файлу, который находится на локальном web-сервере. Для выполнения AJAX-запросов потребуется запущенный локальный web-сервер, на котором будет лежать файл hello.txt, в котором будет содержаться одна строка: Привет мир.

Код web-страницы (пусть она называется test.html) будет следующим:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
</head>
<body>
    <script>
        var request = new XMLHttpRequest();
        request.open("GET", "http://localhost:8080/hello.txt", false);
        request.send();
        var status = request.status;
        if (status == 200) {
            document.write("Текст ответа: " + request.responseText)
        } else if (status == 404) {
            document.write("Ресурс не найден")
        } else {
            document.write(request.statusText)
        }
    </script>
</body>
</html>

И после загрузки страницы выполнится ajax-запрос к ресурсу http://localhost:8080/hello.txt. Но важно отметить, что получение статуса сразу после вызова метода request.send() будет работать только для синхронного запроса.

XMLHttpRequest в JavaScript

13.1.2. Асинхронные запросы

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

Работа с асинхронными запросами чуть более сложна, чем с синхронными, поскольку нам надо еще обработать событие readystatechange объекта XMLHttpRequest.

При асинхронном запросе объект XMLHttpRequest использует свойство readyState для хранения состояния запроса. Состояние запроса представляет собой число:

  • 0: объект XMLHttpRequest создан, но метод open() еще не был вызван для инициализации объекта

  • 1: метод open() был вызван, но запрос еще не был отправлен методом send()

  • 2: запрос был отправлен, заголовки и статус ответа получены и готовы к использованию

  • 3: ответ получен от сервера

  • 4: выполнение запроса завершено (даже если получен код ошибки, например, 404)

Событие readystatechange возникает каждый раз, когда изменяется значение свойства readyState. Например, выполним асинхронный запрос:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
</head>
<body>
    <script>
        var request = new XMLHttpRequest();

        function reqReadyStateChange() {
            if (request.readyState == 4) {
                var status = request.status;
                if (status == 200) {
                    document.write(request.responseText);
                } else {
                    document.write("Ответ сервера " + request.statusText);
                }
            }
        }
        request.open("GET", "http://localhost:8080/hello.txt");
        request.onreadystatechange = reqReadyStateChange;
        request.send();
    </script>
</body>
</html>

Кроме обработки события readystatechange для получения ответа сервера можно также обрабатывать событие load, которое возникает после выполнения запроса. Его использование аналогично:

var request = new XMLHttpRequest();
function responceLoad() {
    if (request.readyState == 4) {
        var status = request.status;
        if (status == 200) {
            document.write(request.responseText);
        } else {
            document.write("Ответ сервера " + request.statusText);
        }
    }
}
request.open("GET", "http://localhost:8080/hello.txt");
request.onload = responceLoad;
request.send();

13.2. Отправка данных

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

13.2.1. Отправка GET-запроса

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

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
</head>
<body>
    <div id="output"></div>
    <script>
        // объект для отправки
        var user = {
            name: "Tom",
            age: 23
        };

        var request = new XMLHttpRequest();
        function reqReadyStateChange() {
            if (request.readyState == 4) {
                var status = request.status;
                if (status == 200) {
                    document.getElementById("output").innerHTML=request.responseText;
                }
            }
        }
        // строка с параметрами для отправки
        var body = "name=" + user.name + "&age="+user.age;
        request.open("GET", "http://localhost:8080/postdata.php?"+body);
        request.onreadystatechange = reqReadyStateChange;
        request.send();
    </script>
</body>
</html>

Для отправки берем свойства объекта user и формируем из их значений сроку с параметрами:

"name=" + user.name + "&age="+user.age

Затем эта строка добавляется к строке запроса в методе open("GET", "http://localhost:8080/postdata.php?"+body)

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

<?php
    $name = "Не известно";
    $age = "Не известно";
    if(isset($_GET['name'])) $name = $_GET['name'];
    if (isset($_GET['age'])) $age = $_GET['age'];
    echo "Ваше имя: $name  <br> Ваш возраст: $age";
?>

Конкретная технология стороны сервера тут не важна. И в качестве тестирования можно взять любую другую технологию. Например, ASP.NET MVC. Метод контроллера в ASP.NET MVC, который принимает данные, мог бы выглядеть следующим образом:

public string PostData(string name, int age) {
    return "Ваше имя: "+name +"; Ваш возраст: "+ age;
}

13.2.2. Кодирование параметров

Все отправляемые в GET-запросе параметры разделяются знаком амперсанда (&). Но что, если какой-нибудь параметр имеет знак амперсанда. Например,

var user = {
    name: "Tom&Tim",
    age: 23
};
// строка с параметрами для отправки
var body = "name=" + user.name + "&age="+user.age;

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

var body = "name=" + encodeURIComponent(user.name) + "&age="+encodeURIComponent(user.age);

При этом строка Tom&Tim будет кодирована в следующую строку: Tom%26Tim.

При необходимости мы можем выполнить обратное декодирование с помощью функции decodeURIComponent():

var encodeName = encodeURIComponent(user.name); // Tom%26Tim
var decodeName = decodeURIComponent(encodeName); // Tom&Tim

13.2.3. POST-запросы

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

var user = {
    name: "Tom",
    age: 23
};
var request = new XMLHttpRequest();
function reqReadyStateChange() {
    if (request.readyState == 4 && request.status == 200) {
        document.getElementById("output").innerHTML=request.responseText;
    }
}
var body = "name=" + user.name + "&age="+user.age;
request.open("POST", "http://localhost:8080/postdata.php");
request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
request.onreadystatechange = reqReadyStateChange;
request.send(body);

Для отправки данных методом POST надо установить заголовок Content-Type с помощью метода setRequestHeader(). В данном случае заголовок имеет значение application/x-www-form-urlencoded.

13.2.4. Отправка форм. FormData

Начиная со спецификации XMLHttpRequest2 в JavaScript появился новый объект - FormData, который позволяет сериализовать данные формы для ее последующей отправки. При этом нам даже необязательно создавать форму в коде HTML, мы можем создать ее динамически в JavaScript:

var formData = new FormData();
formData.append('name', 'Tom');
formData.append('age', 23);
var request = new XMLHttpRequest();
function reqReadyStateChange() {
    if (request.readyState == 4 && request.status == 200) {
        document.getElementById("output").innerHTML=request.responseText;
    }
}
request.open("POST", "http://localhost:8080/display.php");
request.onreadystatechange = reqReadyStateChange;
request.send(formData);

Для добавления данных у объекта FormData используется метод append('имя_параметра', значение). При этом никакие заголовки указывать не надо.

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

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
</head>
<body>
    <div id="output">
    </div>
    <form name="user" action="http://localhost:8080/postdata.php">
        <input type="text" name="username" placeholder="Введите имя" /><br/>
        <input type="text" name="age" placeholder="Введите возраст" /><br/>
        <input type="submit" name="submit" value="Отправить" />
    </form>
    <script>
        // получаем объект формы
        var form = document.forms.user;
        // прикрепляем обработчик кнопки
        form.submit.addEventListener("click", sendRequest);

        // обработчик нажатия
        function sendRequest(event) {
            event.preventDefault();
            var formData = new FormData(form);
            var request = new XMLHttpRequest();
            request.open("POST", form.action);
            request.onreadystatechange = function () {
                if (request.readyState == 4 && request.status == 200)
                    document.getElementById("output").innerHTML=request.responseText;
            }
            request.send(formData);
        }
    </script>
</body>
</html>

Для сериализации всех полей формы нам достаточно передать объект формы в конструктор FormData: var formData = new FormData(form).

13.2.5. Отправка данных в формате json

Для отправки данных в формате json нам необходимо установить соответствующий заголовок и сериализовать данные с помощью метода JSON.stringify():

// объект для отправки
var user = {
    username: "Tom",
    age: 23
};
var json = JSON.stringify(user);
var request = new XMLHttpRequest();
request.open("POST", "http://localhost:8080/postjson.php");
request.setRequestHeader('Content-type', 'application/json; charset=utf-8');
request.onreadystatechange = function () {
    if (request.readyState == 4 && request.status == 200)
        document.getElementById("output").innerHTML=request.responseText;
}
request.send(json);

13.3. Promise в AJAX-запросах

Как видно из примеров прошлых тем для создания AJAX-запросов используются фактически повторяющиеся вызовы, отличающиеся лишь деталями — строкой запроса, функциями обработки ответа. И вполне было бы неплохо создать для всех действий, связанных с асинхронным AJAX-запросом, создать какую-то общую абстракцию и затем использовать ее при следующих обращениях к серверу.

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

Инкапсулируем асинхронный запрос в объект Promise:

function get(url) {
    return new Promise(function(succeed, fail) {
        var request = new XMLHttpRequest();
        request.open("GET", url, true);
        request.addEventListener("load", function() {
            if (request.status < 400)
                succeed(request.response);
            else
            fail(new Error("Request failed: " + request.statusText));
        });
        request.addEventListener("error", function() {
            fail(new Error("Network error"));
        });
        request.send();
    });
}

Метод get получает в качестве параметра адрес ресурса сервера и возвращает объект Promise. Конструктор Promise в качестве параметра принимает функцию обратного вызова, которая в свою очередь принимает два параметра — две функции: одна выполняется при успешной обработке запроса, а вторая — при неудачной.

Допустим, на сервере будет размещен файл users.json со следующим содержимым:

[
    {
        "name": "Tom",
        "age": 34
    },
    {
        "name": "Sam",
        "age": 32
    },
    {
        "name": "Bob",
        "age": 26
    }
]

Теперь вызовем метод get() для осуществления запроса к серверу:

get("http://localhost:8080/users.json").then(function(text) {
        console.log(text);
    }, function(error) {
        console.log("Error!!!");
        console.log(error);
    });

Для обработки результата объекта Promise вызывается метод then(), который принимает два параметра:

  • функцию, вызываемую при успешном выполнении запроса

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

Метод then() также возвращает объект Promise. Поэтому при необходимости мы можем применить к его результату цепочки вызовов метода then: get().then().then()…​. Например:

get("http://localhost:8080/users.json").then(function(response) {
    console.log(response);
    return JSON.parse(response);
}).then(function(data) {
    console.log(data[0]);
});

В данном случае функция в первом вызове метода then получает ответ сервера и возвращает разобранные данные в виде массива с помощью функции JSON.parse().

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

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

get("http://localhost:8080/users.jsn").then(function(response) {
    console.log(response);
    return JSON.parse(response);
}).then(function(data) {
    console.log(data[0]);
}).catch(function(error) {
    console.log("Error!!!");
    console.log(error);
});

Подобным образом через Promise можно было бы отправлять данные на сервер:

function post(url, requestuestBody) {
    return new Promise(function(succeed, fail) {
        var request = new XMLHttpRequest();
        request.open("POST", url, true);
        request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
        request.addEventListener("load", function() {
            if (request.status < 400) {
                succeed(request.responseText);
            } else {
                fail(new Error("Request failed: " + request.statusText));
            }
        });
        request.addEventListener("error", function() {
            fail(new Error("Network error"));
        });
        request.send(requestuestBody);
    });
}

var user = {
    name: "Tom&Tim",
    age: 23
};
// данные для отправки
var params = "name=" + user.name + "&age="+user.age;

post("http://localhost:8080/postdata.php", params).then(function(text) {
        console.log(text);
    }, function(error) {
        console.log(error);
    });