Руководство по javascript с примерами

Время на прочтение
8 мин

Количество просмотров 262K

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

Это руководство, по замыслу автора, рассчитано на тех, кто уже немного знаком JavaScript и хочет привести свои знания в порядок а также узнать о языке что-то новое. Мы решили немного расширить аудиторию этого материала, включить в неё тех, кто совершенно ничего не знает о JS, и начать его с написания нескольких вариантов «Hello, world!».

→ Часть 1: первая программа, особенности языка, стандарты
→ Часть 2: стиль кода и структура программ
→ Часть 3: переменные, типы данных, выражения, объекты
→ Часть 4: функции
→ Часть 5: массивы и циклы
→ Часть 6: исключения, точка с запятой, шаблонные литералы
→ Часть 7: строгий режим, ключевое слово this, события, модули, математические вычисления
→ Часть 8: обзор возможностей стандарта ES6
→ Часть 9: обзор возможностей стандартов ES7, ES8 и ES9

Hello, world!

Программа, которую по традиции называют «Hello, world!», очень проста. Она выводит куда-либо фразу «Hello, world!», или другую подобную, средствами некоего языка.

JavaScript — это язык, программы на котором можно выполнять в разных средах. В нашем случае речь идёт о браузерах и о серверной платформе Node.js. Если до сих пор вы не написали ни строчки кода на JS и читаете этот текст в браузере, на настольном компьютере, это значит, что вы буквально в считанных секундах от своей первой JavaScript-программы.

Для того чтобы её написать, если вы пользуетесь Google Chrome, откройте меню браузера и выберите в нём команду Дополнительные инструменты > Инструменты разработчика. Окно браузера окажется разделённым на две части. В одной из них будет видна страница, в другой откроется панель с инструментами разработчика, содержащая несколько закладок. Нас интересует закладка Console (Консоль). Щёлкните по ней. Не обращайте внимания на то, что уже может в консоли присутствовать (для её очистки можете воспользоваться комбинацией клавиш Ctrl + L). Нас сейчас интересует приглашение консоли. Именно сюда можно вводить JavaScript-код, который выполняется по нажатию клавиши Enter. Введём в консоль следующее:

console.log("Hello, world!")

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

После того, как текст программы оказался в консоли, нажмём клавишу Enter.

Если всё сделано правильно — под этой строчкой появится текст Hello, world!. На всё остальное пока не обращайте внимания.

Первая программа в консоли браузера — вывод сообщения в консоль

Ещё один вариант браузерного «Hello, world!» заключается в выводе окна с сообщением. Делается это так:

alert("Hello, world!")

Вот результат выполнения этой программы.

Вывод сообщения в окне

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

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

Существуют и другие способы запуска JS-кода в браузере. Так, если говорить об обычном использовании программ на JavaScript, они загружаются в браузер для обеспечения работы веб-страниц. Как правило, код оформляют в виде отдельных файлов с расширением .js, которые подключают к веб-страницам, но программный код можно включать и непосредственно в код страницы. Всё это делается с помощью тега <script>. Когда браузер обнаруживает такой код, он выполняет его. Подробности о теге script можно посмотреть на сайте w3school.com. В частности, рассмотрим пример, демонстрирующий работу с веб-страницей средствами JavaScript, приведённый на этом ресурсе. Этот пример можно запустить и средствами данного ресурса (ищите кнопку Try it Yourself), но мы поступим немного иначе. А именно, создадим в каком-нибудь текстовом редакторе (например — в VS Code или в Notepad++) новый файл, который назовём hello.html, и добавим в него следующий код:

<!DOCTYPE html>
<html>
  <body>
    <p id="hello"></p>

    <script>
      document.getElementById("hello").innerHTML = "Hello, world!";
    </script>
  </body>
</html>

Здесь нас больше всего интересует строчка document.getElementById("hello").innerHTML = "Hello, world!";, представляющая собой JS-код. Этот код заключён в открывающий и закрывающий теги <script>. Он находит в документе HTML-элемент с идентификатором hello и меняет его свойство innerHTML (то есть — тот HTML код, который содержится внутри этого элемента) на Hello, world!. Если открыть файл hello.html в браузере, на ней будет выведен заданный текст.

Сообщение, выведенное средствами JavaScript в HTML-элемент

Как уже говорилось, примеры, приводимые на сайте w3school.com, можно тут же и попробовать. Существуют и специализированные онлайн-среды для веб-разработки, и, в частности, для выполнения JS-кода. Среди них, например codepen.io, jsfiddle.net, jsbin.com.

Вот, например, как выглядит наш пример, воссозданный средствами CodePen.

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

Выше мы говорили о том, что JavaScript-программы можно выполнять на различных платформах, одной из которых является серверная среда Node.js. Если вы собираетесь изучать JavaScript, ориентируясь именно на серверную разработку, вероятно, вам стоит запускать примеры именно средствами Node.js. Учтите, что, говоря упрощённо, и не учитывая особенности поддержки конкретных возможностей языка используемыми версиями Node.js и браузера, в Node.js и в браузере будет работать один и тот же код, в котором используются базовые механизмы языка. То есть, например, команда console.log("Hello, world!") будет работать и там и там. Программы, использующие механизмы, специфичные для браузеров, в Node.js работать не будут (то же самое касается и попыток запуска программ, рассчитанных на Node.js, в браузере).

Для того чтобы запустить наш «Hello, world!» в среде Node.js, установим Node.js, скачав отсюда подходящий дистрибутив. Теперь создадим файл hello.js и поместим в него следующий код:

console.log('Hello, World!');

Средствами командной строки перейдём в папку, в которой хранится этот файл, и выполним такую команду:

node hello.js

Вот каким будет результат её выполнения:

Сообщение, выведенное средствами Node.js

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

Теперь, после того, как состоялось ваше первое знакомство с JavaScript, предлагаем подробнее поговорить об этом языке.

Общие сведения о JavaScript

JavaScript — это один из самых популярных языков программирования в мире. Он, созданный более 20 лет назад, прошёл в своём развитии огромный путь. JavaScript задумывался как скриптовый язык для браузеров. В самом начале он обладал куда более скромными возможностями, чем сейчас. Его, в основном, использовали для создания несложных анимаций, вроде выпадающих меню, о нём знали как о части технологии Dynamic HTML (DHTML, динамический HTML).

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

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

Основные характеристики JavaScript

JavaScript — это язык, который отличается следующими особенностями:

  • Высокоуровневый. Он даёт программисту абстракции, которые позволяют не обращать внимание на особенности аппаратного обеспечения, на котором выполняются JavaScript-программы. Язык автоматически управляет памятью, используя сборщик мусора. Разработчик, в результате, может сосредоточиться на решении стоящих перед ним задач, не отвлекаясь на низкоуровневые механизмы (хотя, надо отметить, это не отменяет необходимости в рациональном использовании памяти). Язык предлагает мощные и удобные средства для работы с данными различных типов.
  • Динамический. В отличие от статических языков программирования, динамические языки способны, во время выполнения программы, выполнять действия, которые статические языки выполняют во время компиляции программ. У такого подхода есть свои плюсы и минусы, но он даёт в распоряжение разработчика такие мощные возможности, как динамическая типизация, позднее связывание, рефлексия, функциональное программирование, изменение объектов во время выполнения программы, замыкания и многое другое.
  • Динамически типизированный. Типы переменных при JS-разработке задавать необязательно. В одну и ту же переменную можно, например, сначала записать строку, а потом — целое число.
  • Слабо типизированный. В отличие от языков с сильной типизацией, языки со слабой типизацией не принуждают программиста, например, использовать в неких ситуациях объекты определённых типов, выполняя, при необходимости, неявные преобразования типов. Это даёт больше гибкости, но JS-программы не являются типобезопасными, из-за этого усложняются задачи проверки типов (на решение этой проблемы направлены TypeScript и Flow).
  • Интерпретируемый. Широко распространено мнение, в соответствии с которым JavaScript является интерпретируемым языком программирования, что означает, что программы, написанные на нём, не нуждаются в компиляции перед выполнением. JS в этом плане противопоставляют таким языкам, как C, Java, Go. На практике же браузеры, для повышения производительности программ, выполняют компиляцию JS-кода перед его выполнением. Этот шаг, однако, прозрачен для программиста, он не требует от него дополнительных усилий.
  • Мультипарадигменный. JavaScript не навязывает разработчику использование какой-то конкретной парадигмы программирования, в отличие, например, от Java (объектно-ориентированное программирование) или C (императивное программирование). Писать JS-программы можно, используя объектно-ориентированную парадигму, в частности — применяя прототипы и появившиеся в стандарте ES6 классы. Программы на JS можно писать и в функциональном стиле, благодаря тому, что функции здесь являются объектами первого класса. JavaScript допускает и работу в императивном стиле, используемом в C.

Да, кстати, надо отметить, что у JavaScript и Java нет ничего общего. Это — совершенно разные языки.

JavaScript и стандарты

ECMAScript, или ES, это название стандарта, которым руководствуются разработчики JavaScript-движков, то есть — тех сред, где выполняются JS-программы. Различные стандарты вводят в язык новые возможности, говоря о которых нередко упоминают наименование стандартов в сокращённой форме, например — ES6. ES6 — это то же самое, что и ES2015, только в первом случае число означает номер версии стандарта (6), а во втором — год принятия стандарта (2015).

Сложилось так, что в мире веб-программирования очень долго был актуален стандарт ES3, принятый в 1999 году. Четвёртой версии стандарта не существует (в неё попытались добавить слишком много новых возможностей и так и не приняли). В 2009 году был принят стандарт ES5, который представлял собой прямо-таки огромное обновление языка, первое за 10 лет. После него, в 2011 году, был принят стандарт ES5.1, в нём тоже было немало нового. Весьма значительным, в плане новшеств, стал и стандарт ES6, принятый в 2015 году. Начиная с 2015 года, новые версии стандарта принимают каждый год.

Самой свежей версией стандарта на момент публикации этого материала является ES9, принятая в июне 2018 года.

Итоги

Сегодня мы написали «Hello, World!» на JavaScript, рассмотрели основные особенности языка и поговорили о его стандартизации. В следующий раз поговорим о стиле JavaScript-текстов и о лексической структуре программ.

Уважаемые читатели! Если вы, до чтения этого материала, не были знакомы с JavaScript, просим рассказать о том, получилось ли у вас запустить «Hello, world!».

Учебник по JavaScript, с понятными и подробными материалами, содержащими множество примеров и решения различных реальных практических задач.

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

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

Гайд по принципу Парето: 20% языка, которые нужны вам в 80% случаев. Только основные концепции JavaScript с примерами кода.

JS-гайд: основные концепции JavaScript с примерами кода

С момента появления JavaScript 20 лет назад он прошел долгий путь от скромного инструмента для простеньких анимаций до первой десятки рейтинга Tiobe.

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

Версии и стандарты

Язык JavaScript реализует стандарт ECMAScript, поэтому название его версий начинается с букв ES: ES6, ES2016, ES2018 и так далее. Версии имеют порядковый номер, а также нумеруются по году релиза. На данный момент последняя утвержденная версия – ES2017, он же ES8.

За развитие языка отвечает комитет TC39. Каждая новая фича должна пройти несколько этапов от предложения до стандарта.

Стайлгайды

Чтобы JavaScript-код был чистым и аккуратным, следует выработать систему соглашений и строго их придерживаться. Удобно использовать готовые стайлгайды, например, от Google или AirBnb.

Переменные

Имена переменных и функций в JavaScript должны начинаться с буквы, $ или символа подчеркивания. Они могут даже содержать эмодзи или иероглифы! Идентификаторы регистрозависимы: something и SomeThing – это разные переменные.

Нельзя использовать в качестве имен зарезервированные слова языка:

break
do
instanceof
typeof
case
else
new
var
catch
finally
return
void
continue
for
switch
while
debugger
function
this
with
default
if
throw
delete
in
try
class
enum
extends
super
const
export
import
implements
let
private
public
interface
package
protected
static
yield

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

// до ES6 были только var-переменные
var a = 'variable' 
var b = 1, c = 2 

// let-переменные можно изменять
let x = 10
x = 20

// const-переменные нельзя изменять
const y = 10
y = 20 // ошибка
  • var-переменные имеют контекстную область видимости и обладают свойством хойстинга (поднятия).
  • У let и const видимость блочная, и они не поднимаются.
  • неизменяемость const-переменных широко используется для обеспечения иммутабельности.

Выражения

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

// арифметические выражения преобразуются к числу
1 / 2
i++
i -= 2
i * 2

// строковые - к строке
"привет, " + "мир"
'hello, ' += 'world'

// логические - к булеву значению
a && b
a || b
!a

// литералы, переменные, константы - это тоже выражения
2
0.02
'something'
true
false
this 
undefined
i

// как и некоторые ключевые слова
function
class
function* 
yield 
/pattern/i
() // группирующие скобки

// выражения создания и инициализации
[]
{a: 1, b: 2}
new Person()
function() {}
a => a

// выражения вызова функций и методов
Math.paw(2, 3)
window.resize()

Примитивные типы данных

Числа

Все числа в JavaScript (даже целые) имеют тип float (число с плавающей точкой). Мы подготовили отдельную подробную статью об особенностях чисел и математических методах в JavaScript.

Строки

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

'Одна строка'
"Другая строка"

// кавычки внутри строк необходимо экранировать
// двойные, если строка в двойных кавычках
"Ресторан "У конца вселенной""
// одинарные, если в одинарных
'I'm Groot'

// строки могут содержать управляющие последовательности
"Первая строкаnВторая строка"

Для конкатенации строк используется оператор +:

"Hello, " + "world"

Строку можно заполнить символами до определенной длины (с начала или с конца):

padStart(targetLength [, padString])
padEnd(targetLength [, padString])

'test'.padStart(7) // ' test'
'test'.padStart(7, 'a') // 'aaatest'
'test'.padStart(7, 'abcd') // 'abctest'

'test'.padEnd(7) // 'test '
'test'.padEnd(7, 'a') // 'testaaa'
'test'.padEnd(7, 'abcd') // 'testabc'

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

// для шаблонных строк используются обратные кавычки
let str = `шаблонная строка`

let answer = 42
let a = `Ответ на главный вопрос жизни, вселенной и всего такого - ${answer}`
let b = `Дважды два равно ${ 2*2 }`
let c = `something ${ foo() ? 'x' : 'y' }`

let multiline = `Первая строка
вторая строка

третья строка`

Логические значения

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

// приводятся к false в логическом контексте
0
-0
NaN
undefined
null
'' // пустая строка

// остальные значения становятся true

А вот подборка зубодробительных особенностей логики JavaScript.

null

null означает отсутствие значения у переменной. У этой концепции JavaScript есть аналоги в других языках программирования, например, nil или None.

undefined

undefined означает, что переменная неинициализирована и не имеет значения.

Функции без директивы return возвращают именно undefined. Неинициализированные параметры функций также являются undefined.

Функции

Функция – это самостоятельный блок JavaScript-кода, который можно повторно использовать в программе. Особенность функций в том, что их можно вызывать, передавать им аргументы и получать некоторое новое значение.

function dosomething(foo) {
  return foo * 2
}

const dosomething = function(foo) {
  return foo * 2
}

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

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

Параметры

С версии ES6 функции поддерживают параметры по умолчанию:

const foo = function(index = 0, testing = true) { /* ... */ }
foo()

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

const doSomething = (var1, var2,) => {
  //...
}
doSomething('test2', 'test2',)

Возвращаемое значение

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

Замыкания

Эффект замыканий основан на том, что в концепции JavaScript области видимости ограничены функциями. Это сложная тема, которую, тем не менее, необходимо понять для успешной работы. Мы посвятили ей большой отдельный материал (часть 1, часть 2).

this

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

const car = {
  brand: 'Ford',
  model: 'Fiesta',
  start: function() {
    console.log(`Started ${this.brand} ${this.model}`)
  }
}
car.start() // Started Ford Fiesta

this можно установить искусственно с помощью методов call, apply и bind:

const car1 = {
  maker: 'Ford',
  model: 'Fiesta',
  drive() {
    console.log(`Driving a ${this.maker} ${this.model} car!`)
  }
}
const anotherCar = {
  maker: 'Audi',
  model: 'A4'
}
car1.drive.bind(anotherCar)()
//Driving a Audi A4 car!

const car2 = {
  maker: 'Ford',
  model: 'Fiesta'
}
const drive = function(kmh) {
  console.log(`Driving a ${this.maker} ${this.model} car at ${kmh} km/h!`)
}
drive.call(car2, 100)
//Driving a Ford Fiesta car at 100 km/h!
drive.apply(car2, [100])
//Driving a Ford Fiesta car at 100 km/h!

Если функция вызывается не в контексте объекта, ее this равен undefined.

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

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

const foo1 = () => {
  //...
}

// можно даже в одну строку
const foo2 = () => doSomething()

// с передачей параметра
const foo3 = param => doSomething(param)


// неявный возврат значения
const foo4 = param => param * 2
foo4(5) // 10

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

const a = {
  method: () => {
    console.log(this);
  }
}

a.method() // undefined

IIFE

Immediately Invoked Function Expressions – функции, которые выполняются сразу же после объявления.

(function () {
  console.log('executed')
})()

// executed

Генераторы

Особые функции, работу которых можно приостановить с помощью ключевых слов yield и возобновить позже. Это позволяет использовать совершенно новые концепции JavaScript-программирования.

// создание функции-генератора
function *calculator(input) {
    var doubleThat = 2 * (yield (input / 2))
    var another = yield (doubleThat)
    return (input * doubleThat * another)
}

// инициализация со значением 10
const calc = calculator(10)

// запуск калькулятора
calc.next() 

// возвращает { done: false, value: 5 },
// где value - это результат выражения input / 2

// продолжаем с новым значением
// оно подставляется вместо первого yield
calc.next(7)

// возвращает { done: false, value: 14 },
// где value - это вычисленное значение doubleThat

// продолжаем с новым значением
// оно подставляется вместо второго yield
calc.next(100)

// функция отрабатывает до конца и возвращает
// { done: true, value: 14000 }
// где value = 10 * 14 * 100

Поймите концепции JavaScript на примере объяснений или задач.

Массивы

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

Объекты

В ES2015 объектные литералы получили новые возможности:

  • Упрощение синтаксиса включения переменных.
    // до ES2015
    const something = 'y'
    const x = {
      something: something
    }
    
    // ES2015
    const something = 'y'
    const x = {
      something
    }
  • Прототипы и ключевое слово super.
    const anObject = { y: 'y', test: () => 'zoo' }
    const x = {
      __proto__: anObject,
      test() {
        return super.test() + 'x'
      }
    }
    x.test() //zoox
  • Динамические имена свойств.
    const x = {
      ['a' + '_' + 'b']: 'z'
    }
    x.a_b //z

Получение ключей и значений объекта

// массив значений собственных свойств объекта
const person = { name: 'Fred', age: 87 }
Object.values(person) // ['Fred', 87]

const people = ['Fred', 'Tony']
Object.values(people) // ['Fred', 'Tony']

// массив собственных свойств объекта в виде пар [ключ, значение]
const person = { name: 'Fred', age: 87 }
Object.entries(person) // [['name', 'Fred'], ['age', 87]]

const people = ['Fred', 'Tony']
Object.entries(people) // [['0', 'Fred'], ['1', 'Tony']]

// набор дескрипторов всех собственных свойств объекта
Object.getOwnPropertyDescriptors(object)

Циклы

for

const list = ['a', 'b', 'c']
for (let i = 0; i < list.length; i++) {
  console.log(list[i]) //value
  console.log(i) //index
}

for-each

const list = ['a', 'b', 'c']
list.forEach((item, index) => {
  console.log(item) //value
  console.log(index) //index
})
//index is optional
list.forEach(item => console.log(item))

do-while

const list = ['a', 'b', 'c']
let i = 0
do {
  console.log(list[i]) //value
  console.log(i) //index
  i = i + 1
} while (i < list.length)

while

const list = ['a', 'b', 'c']
let i = 0
while (i < list.length) {
  console.log(list[i]) //value
  console.log(i) //index
  i = i + 1
}

for-in

for (let property in object) {
  console.log(property) //property name
  console.log(object[property]) //property value
}

for-of

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

for (const v of ['a', 'b', 'c']) {
  console.log(v);
}

for (const [i, v] of ['a', 'b', 'c'].entries()) {
  console.log(i, v);
}

Деструктуризация

Спред-оператор

Дает возможность развернуть массив, объект или строку на элементы:

const a = [1, 2, 3]

// простое объединение массивов
const b = [...a, 4, 5, 6]

// простое копирование массива
const c = [...a]

// работает и с объектами
const newObj = { ...oldObj }

// и со строками
const hey = 'hey'
const arrayized = [...hey] // ['h', 'e', 'y']

// позволяет передавать в функции параметры-массивы
const f = (foo, bar) => {}
const a = [1, 2]
f(...a)

Деструктурирующее присваивание

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

const person = {
  firstName: 'Tom',
  lastName: 'Cruise',
  actor: true,
  age: 54, 
}
const {firstName: name, age} = person

// работает и с массивами
const a = [1,2,3,4,5]
[first, second, , , fifth] = a

ООП

В ООП-концепции JavaScript главное место занимают прототипы.

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

Каждый объект имеет свойство prototype, в котором хранится ссылка на его прототип – своего рода хранилище методов и свойств. У прототипа в свою очередь есть свой прототип, к которому объект также имеет доступ «по цепочке».

// создание массива
const list = []

// его прототипом является прототип объекта Array
Array.isPrototypeOf(list) //true
list.__proto__ == Array.prototype // true

Классы

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

class Person {
  // метод constructor используется для инициализации экземпляра
  constructor(name) {
    this.name = name
  }
  hello() {
    return 'Hello, I am ' + this.name + '.'
  }
}

// наследование классов
class Actor extends Person {
  hello() {
    // super позволяет ссылаться на методы родительского класса
    return super.hello() + ' I am an actor.'
  }
}
var tomCruise = new Actor('Tom Cruise')
tomCruise.hello() // "Hello, I am Tom Cruise. I am an actor."

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

class Person {
  get fullName() {
    return `${this.firstName} ${this.lastName}`
  }

  set age(years) {
    this.theAge = years
  }
}

Исключения

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

throw value

Для обработки нативных и кастомных исключений используется конструкция try-catch-finally.

try {
  // здесь выполняется код
} catch (e) {
  // здесь обрабатываются исключения, если они появились
} finally {
  // этот код выполняется при любом исходе
}

События

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

Каждое событие представлено объектом со множеством свойств и распространяется на веб-странице в три стадии:

  • Перехват (capturing). Событие спускается от корневого элемента к своей непосредственной цели. На этой стадии его можно перехватить.
  • Срабатывание на целевом элементе.
  • Всплытие — обратный путь от цели наверх.

Установить обработчик можно тремя способами:

// через атрибут html-элемента
<a href="site.com" onclick="dosomething();">Ссылка</a>


// через on-свойство
window.onload = () => {
  //window loaded
}

// с помощью метода addEventListener
window.addEventListener('load', () => {
  //window loaded
})

Основные браузерные события вы можете найти здесь.

Цикл событий

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

// простая программа
const bar = () => console.log('bar')
const baz = () => console.log('baz')
const foo = () => {
  console.log('foo')
  bar()
  baz()
}
foo()

// вывод:
// foo
// bar
// baz

Асинхронность

Для более глубокого понимания темы мы подготовили материал по основным концепциям асинхронного программирования.

Коллбэки

Исторически асинхронность в JavaScript обеспечивалась с помощью обратных вызовов:

document.getElementById('button').addEventListener('click', () => {
  //item clicked
})

window.addEventListener('load', () => {
  //window loaded
})

setTimeout(() => {
  // runs after 2 seconds
}, 2000)

Однако при большом уровне вложенности код превращался в настоящий кошмар – ад коллбэков.

window.addEventListener('load', () => {
  document.getElementById('button').addEventListener('click', () => {
    setTimeout(() => {
      items.forEach(item => {
        //your code here
      })
    }, 2000)
  })
})

Промисы

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

// без промисов
setTimeout(function() {
  console.log('I promised to run after 1s')
  setTimeout(function() {
    console.log('I promised to run after 2s')
  }, 1000)
}, 1000)

// с промисами
const wait = () => new Promise((resolve, reject) => {
  setTimeout(resolve, 1000)
})
wait().then(() => {
  console.log('I promised to run after 1s')
  return wait()
})
.then(() => console.log('I promised to run after 2s'))

Основы работы с промисами:

// создание
let done = true
const isItDoneYet = new Promise(
  (resolve, reject) => {
    if (done) {
      const workDone = 'Here is the thing I built'
      resolve(workDone)
    } else {
      const why = 'Still working on something else'
      reject(why)
    }
  }
)

// цепочка промисов
const status = (response) => {
  if (response.status >= 200 && response.status < 300) {
    return Promise.resolve(response)
  }
  return Promise.reject(new Error(response.statusText))
}
const json = (response) => response.json()
fetch('/todos.json')
  .then(status)
  .then(json)
  .then((data) => { console.log('Request succeeded with JSON response', data) })
  .catch((error) => { console.log('Request failed', error) })

// выполнение нескольких промисов
const f1 = fetch('/something.json')
const f2 = fetch('/something2.json')
Promise.all([f1, f2]).then((res) => {
    console.log('Array of results', res)
})
.catch((err) => {
  console.error(err)
})

// выполнение первого из нескольких промисов
const first = new Promise((resolve, reject) => {
    setTimeout(resolve, 500, 'first')
})
const second = new Promise((resolve, reject) => {
    setTimeout(resolve, 100, 'second')
})
Promise.race([first, second]).then((result) => {
  console.log(result) // second
})

Асинхронные функции

Сочетание промисов и генераторов – асинхронная абстракция более высокого уровня и с более простым синтаксисом.

function doSomethingAsync() {
    return new Promise((resolve) => {
        setTimeout(() => resolve('I did something'), 3000)
    })
}
async function doSomething() {
    console.log(await doSomethingAsync())
}
console.log('Before')
doSomething()
console.log('After')

// Вывод программы:
// Before
// After
// I did something - через 3 секунды

Асинхронные функции легко объединять в цепочки:

function promiseToDoSomething() {
    return new Promise((resolve)=>{
        setTimeout(() => resolve('I did something'), 10000)
    })
}
async function watchOverSomeoneDoingSomething() {
    const something = await promiseToDoSomething()
    return something + ' and I watched'
}
async function watchOverSomeoneWatchingSomeoneDoingSomething() {
    const something = await watchOverSomeoneDoingSomething()
    return something + ' and I watched as well'
}
watchOverSomeoneWatchingSomeoneDoingSomething().then((res) => {
    console.log(res)
})

Мы подготовили небольшой обзор плюсов и минусов async/await.

Таймеры

Таймеры – один из способов асинхронного выполнения кода.

let timerId = setTimeout(() => {
  // запустится через 2 секунды
}, 2000)
clearTimeout(timerId) // очистка таймаута

let intervalId = setInterval(() => {
  // будет запускаться каждые 2 секунды
}, 2000)
clearInterval(intervalId ) // очистка интервала

// рекурсивный setTimeout
const myFunction = () => {
  // do something
  setTimeout(myFunction, 1000)
}
setTimeout(
  myFunction()
}, 1000)

Модули

До ES2015 было по крайней мере три конкурирующих стандарта модулей: AMD, RequireJS и CommonJS, но теперь появился единый формат.

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

<script type="module" src="module.js"></script>
<script nomodule src="fallback.js"></script>

Импорт модуля осуществляется с помощью директивы import:

import * from 'mymodule'
import React from 'react'
import { React, Component } from 'react'
import React as MyLibrary from 'react'

А экспорт с помощью слова export:

export var foo = 2
export function bar() { /* ... */ }

Платформа Node.js продолжает использовать модули CommonJS.

Разделенная память и атомарные операции

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

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

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

ES2018

Стандарт ES2018 вводит несколько новых языковых фич.

Асинхронная итерация

Новый цикл for-await-of позволяет асинхронно перебирать свойства итерируемого объекта:

for await (const line of readLines(filePath)) {
  console.log(line)
}

Эта конструкция может быть использована только внутри асинхронных функций.

Promise.prototype.finally

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

fetch('file.json')
  .then(data => data.json())
  .catch(error => console.error(error))
  .finally(() => console.log('finished'))

Улучшение регулярных выражений

Опережающие (lookahead) и ретроспективные (lookbehind) проверки

// ?= ищет строку, за которой следует конкретная строка
/Roger(?=Waters)/
/Roger(?= Waters)/.test('Roger is my dog') //false
/Roger(?= Waters)/.test('Roger is my dog and Roger Waters is a famous musician') //true

// ?! выполняет обратную операцию
/Roger(?!Waters)/
/Roger(?! Waters)/.test('Roger is my dog') //true
/Roger(?! Waters)/.test('Roger Waters is a famous musician') //false

// ?<= ищет строку, перед которой идет конкретная строка
/(?<=Roger) Waters/
/(?<=Roger) Waters/.test('Pink Waters is my dog') //false
/(?<=Roger) Waters/.test('Roger is my dog and Roger Waters is a famous musician') //true

// ?<! выполняет обратную операцию
/(?<!Roger) Waters/
/(?<!Roger) Waters/.test('Pink Waters is my dog') //true
/(?<!Roger) Waters/.test('Roger is my dog and Roger Waters is a famous musician') //false

Паттерны для символов Юникода (и его отрицание)

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

/^p{ASCII}+$/u.test('abc')   // true
/^p{ASCII}+$/u.test('ABC@')  // true
/^p{ASCII}+$/u.test('ABC?') // false

Подробнее обо всех свойствах вы можете прочитать в самом предложении.

Именованные группы захвата

const re = /(?<year>d{4})-(?<month>d{2})-(?<day>d{2})/
const result = re.exec('2015-01-02')
// result.groups.year === '2015';
// result.groups.month === '01';
// result.groups.day === '02';

Флаг s

s – сокращение от single line. Позволяет символу . (точка) совпадать с символами новой строки.

/hi.welcome/.test('hinwelcome') // false
/hi.welcome/s.test('hinwelcome') // true
А какие фичи и концепции JavaScript вы используете чаще всего?

Оригинал: The Complete JavaScript Handbook

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:

Список зарезервированных слов в 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;
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);

Присвоение значение 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 = {name: "Tom", age:24};
console.log(user.name);

В данном случае объект называется 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);

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

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

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

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

Выше в обоих случая к переменной 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, так как наибольшее целое число, которое меньше или равно 40 и при этом делится на 7 равно 35, а 40 - 35 = 5.

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

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

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

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

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

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

Декремент

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

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

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

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);
  • -=
    Вычитание с последующим присвоением результата.

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

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

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

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

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);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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);

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

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

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

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

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

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

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

Если методу не удастся выполнить преобразование, то он возвращает значение 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);

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

Преобразование в число в двоичной системе

var num1 = "110";
var num2 = parseInt(num1, 2);
console.log(num2);

Результатом будет 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 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);

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

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

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

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

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

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

Также стоит отметить, что в отличие от ряда языков программирования в 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);

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

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

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

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

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

То есть если визуально двумерный массив можно представить в виде таблицы, то элемент 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]);

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

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:

Например:

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);

Если значение переменной 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();

Фактически определяем переменную 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); // вызов функции

Функция 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);

Во втором случае в функцию передается числа из массива 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);

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

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

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

Если параметрам 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);

В данном случае значение параметра 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);

При этом даже неважно, что при определении функции не указаны параметры, все равно можно их передать и получить значения через массив 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;
}

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

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

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);

Функция 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);
    }
}

В данном случае функция 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();

В этом случае в функции будет использоваться та переменная 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);

Несмотря на то, что вне функции 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();

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

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

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

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

То есть несмотря на то, что переменная 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);

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

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

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

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

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

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

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

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

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

Число 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);

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();

Здесь определена переменная 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();

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

Все данные сокрыты в объекте 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);

Функция 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);

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.

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

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

Uncaught ReferenceError: foo is not defined

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

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

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

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

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

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

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

display();

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

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

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

display();

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

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

TypeError: display is not a function

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

Поэтому при определении переменных и функций следует учитывать такой аспект как 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);

В данном случае функция 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);

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

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));

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

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

console.log(square(5));

Для возвращения результата из функции в таком случае применяется стандартный оператор 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);

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

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

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

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

4.1. Объекты

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

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

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

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

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

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

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

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

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 объект["свойство"]
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);

После удаления свойство будет не определено, поэтому при попытке обращения к нему, программа вернет значение 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"]);

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

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

country["capital"]["population"]

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

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

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.

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

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);

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

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

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

И так как объекты представляют тип 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);

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:

После создания переменной 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);

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);

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

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);

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

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

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

console.log(result);

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);

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();

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);

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();

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);

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

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

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

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

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

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

console.log(a);
console.log(b);
console.log(c);

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

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

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

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

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);

4.12. Классы

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

Для определения класса используется ключевое слово 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);

Конструктор определяется с помощью метода с именем 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);

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);

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

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]);

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

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);

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);

В метод 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);

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

Объект 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

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

Для работы со строками предназначен объект 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);

Аналогично работает метод 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);
})

6.3.3. Поиск в строке. Метод search()

Метод search() находит индекс первого включения соответствия в строке:

var initialText = "hello world";
var exp = /wor/;
var result = initialText.search(exp);
console.log(result);

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 можно не использовать:

Но так как данный объект глобальный, то это накладывает некоторые ограничения. Например:

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() выводит окно с сообщением:

Метод 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 страницы назад:

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

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);

Однако ряд свойств 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. В данном случае обработчик выглядит довольно просто:

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

Пример события

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

<!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.

Здесь нас интересуют четыре клавиши: вверх, вниз, влево, вправо. Им соответственно будут соответствовать названия 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 сохраняются в виде строки:

localStorage.setItem("age", 23);
var age = localStorage.getItem("age");
age = parseInt(age) + 10;
console.log(age);

Если в данном случае не преобразовать значение к числу с помощью 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);

И в завершении надо сказать, что в некоторых браузерах с помощью специальных инструментов мы можем увидеть сохраненные объекты в 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();
}

Но в этом нет смысла, поскольку все коллекции, возвращающие итераторы, поддерживают перебор с помощью цикла 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]);
}

12.2. Генераторы

Генераторы представляют особый тип функции, которые используются для генерации значений. Для определения генераторов применяется символ звездочки *, который ставится после ключевого слова function. Например, определим простейший генератор:

function* getNumber() {
    yield 5;
}
let numberGenerator = getNumber();
let next = numberGenerator.next();
console.log(next);

Функция getNumber() представляет генератор. Функция генератора возвращает итератор. Для получения значения из генератора применяется оператор yield. То есть фактически в данном случае генератор генерирует число 5.

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

Для перехода к следующему значению применяется метод next(). Если мы посмотрим на консольный вывод, то мы увидим, что данный метод возвращает следующие данные:

То есть по сути возвращается объект, свойство 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, а свойство donetrue, то есть работа генератора завершена.

Генератор может создавать множество значений:

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);
}

Генератор необязательно содержит только определение операторов 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);

При втором вызове метода next():

numberGenerator.next(2).value

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

То есть здесь переменная n будет равна 2, так как в метод next() передается число 2.

Далее мы можем использовать это значение, например, для генерации нового значения:

Соответственно, переменная 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 arr = [1, 1, 2, 3, 4, 5, 2, 4];
let numbers = new Set(arr);
console.log(numbers);

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

Для проверки количества элементов можно использовать свойство size.

let arr = [1, 1, 2, 3, 4, 5, 2, 4];
let numbers = new Set(arr);
console.log(numbers.size);

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);

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

Так как метод add() возвращает ссылку на это же множество, то мы можем вызывать методы по цепочке:

let numbers = new Set();
numbers.add(1).add(3).add(5);
console.log(numbers);

12.3.2. Удаление

Для удаления элементов применяется метод delete():

let numbers = new Set();
numbers.add(1).add(3).add(5);
numbers.delete(3);
console.log(numbers);

Причем данный метод возвращает булево значение: 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);

Если необходимо удалить вообще все элементы из множества, то применяется метод clear():

let numbers = new Set();
numbers.add(1).add(3).add(5);
numbers.clear();
console.log(numbers);

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));

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([[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));

Если 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));

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);

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.

Также для перебора объекта 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);

Если надо проверить, имеется ли объект в 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

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));

Для добавления новых объектов или изменения старых применяется метод 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));

Для удаления элемента по ключу применяется метод 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));

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():

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);
    });

Памятка по современному JavaScript

Памятка по современному JavaScript
За картинку спасибо Ahmad Awais ⚡️

Введение

Мотивация

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

Цель этого руководства — не обучить вас JavaScript с нуля, а помочь разработчикам с базовыми знаниями, которые при изучении современных кодовых баз (или, скажем, React) сталкиваются со сложностями из-за использованных в них концепций JavaScript.

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

Примечание: Большинство представленных здесь понятий взяты из обновления языка JavaScript (ES2015, которое часто называют ES6). Вы можете найти новые возможности из этого обновления здесь; они хорошо описаны.

Дополнительные ресурсы

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

  • MDN (сеть разработчиков Mozilla).
  • Вы не знаете JS (серия книг).
  • ES6 Features with examples.
  • Блог WesBos (ES6).
  • Javascript Basics for Beginners — бесплатный курс от Udacity.
  • Reddit (JavaScript).
  • Google для поиска специализированных блогов и ресурсов.
  • StackOverflow.

Содержание

  • Памятка по современному JavaScript
    • Введение
      • Мотивация
      • Дополнительные ресурсы
    • Содержание
    • Понятия
      • Объявление переменных: var, const, let
        • Краткое объяснение
        • Пример кода
        • Подробное объяснение
        • Дополнительные материалы
      • Стрелочные функции
        • Пример кода
        • Подробное объяснение
          • Краткость
          • Использование this
        • Полезные ресурсы
      • Значение аргументов функции по умолчанию
        • Дополнительные материалы
      • Деструктуризация объектов и массивов
        • Объяснение с помощью примера кода
        • Полезные ресурсы
      • Методы массивов — map / filter / reduce
        • Пример кода
        • Объяснение
          • Array.prototype.map()
          • Array.prototype.filter()
          • Array.prototype.reduce()
        • Дополнительные материалы
      • Оператор расширения ...
        • Пример кода
        • Объяснение
          • В итерируемых объектах (например, массивах)
          • Оставшиеся аргументы функции
          • Расширение свойств объектов
        • Дополнительные материалы
      • Сокращенная запись свойств объектов
        • Объяснение
        • Дополнительные материалы
      • Промисы
        • Пример кода
        • Пояснение
          • Создание промиса
          • Использование обработчиков промисов
        • Дополнительные материалы
      • Шаблонные строки
        • Пример кода
        • Дополнительные материалы
      • Тегированные шаблонные строки
        • Дополнительные материалы
      • Импорт / экспорт
        • Объяснение с помощью примера кода
          • Именованный экспорт
          • Импорт / экспорт по умолчанию
        • Дополнительные материалы
      • this в JavaScript
        • Дополнительные материалы
      • Класс
        • Примеры
        • Дополнительные материалы
      • Ключевые слова Extends и super
        • Пример кода
        • Дополнительные материалы
      • Async Await
        • Пример кода
        • Объяснение с помощью примера кода
        • Обработка ошибок
        • Дополнительные материалы
      • Истина / Ложь
        • Дополнительные материалы
      • Анаморфизмы и катаморфизмы
        • Анаморфизмы
          • Пример кода
        • Катаморфизмы
          • Пример кода
        • Дополнительные материалы
      • Генераторы
        • Пример кода
        • Дополнительные материалы
      • Статические методы
        • Краткое объяснение
        • Пример кода
        • Подробное объяснение
          • Вызов статических методов из статического метода
          • Вызов статических методов из нестатических методов
        • Дополнительные материалы
    • Глоссарий
      • Область видимости
      • Изменение переменных

Понятия

Объявление переменных: var, const, let

В JavaScript есть три ключевых слова, отвечающих за объявление переменных, и у каждого из них свои особенности. Эти слова − var, let и const.

Краткое объяснение

Переменным, объявленным с помощью ключевого слова const, нельзя позже присвоить новое значение, в то время как переменным, объявленным с помощью let или var, можно.

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

Область видимости Можно переопределять Можно изменять Временная мертвая зона
const Блок Нет Да Да
let Блок Да Да Да
var Функция Да Да Нет

Пример кода

const person = "Коля";
person = "Ваня" // Вызовет ошибку, переменной person нельзя присвоить новое значение.
let person = "Коля";
person = "Ваня";
console.log(person) // -> "Ваня", присвоение нового значения разрешено в случае с let.

Подробное объяснение

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

var

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

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

function myFunction() {
  var myVar = "Коля";
  console.log(myVar); // -> "Коля" — myVar доступна внутри функции.
}
console.log(myVar); // ReferenceError, myVar недоступна снаружи функции.

Вот менее очевидный пример области видимости переменных:

function myFunction() {
  var myVar = "Коля";
  if (true) {
      var myVar = "Ваня";
      console.log(myVar); // -> "Ваня"
      /* На самом деле, область видимости myVar — функция,
      мы всего лишь удалили предыдущее значение переменной myVar "Коля"
      и заменили его на "Ваня". */
    }
    console.log(myVar); // -> "Ваня" — обратите внимание, как код в блоке if повлиял на это значение.
  }
  console.log(myVar); // ->  
  /* ReferenceError, переменная myVar недоступна
  за пределами функции, в которой определена. */

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

Этот фрагмент кода:

console.log(myVar) // -> undefined — ошибок нет.
var myVar = 2;

при выполнении понимается как:

var myVar;
console.log(myVar) // -> undefined — ошибок нет.
myVar = 2;
let

var и let примерно одинаковы, в то время как переменные, объявленные словом let:

  • имеют в качестве области видимости блок;
  • недоступны до объявления;
  • не могут быть повторно объявлены в той же области видимости.

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

function myFunction() {
  let myVar = "Коля";
  if (true) {
    let myVar = "Ваня";
    console.log(myVar); // -> "Ваня"
    /* Поскольку myVar имеет блочную область видимости,
    здесь мы только что создали новую переменную myVar.
    Эта переменная недоступна вне блока и никак не зависит
    от первой переменной myVar, которую мы создали до этого! */
  }
  console.log(myVar); // -> "Коля" — обратите внимание: инструкции в блоке if НЕ повлияли на значение переменной.
}
console.log(myVar); // -> ReferenceError, myVar недоступна за пределами функции.

Теперь разберемся, что значит «переменные, объявленные с помощью let и const, недоступны до их объявления»:

console.log(myVar) // Вызовет ReferenceError!
let myVar = 2;

В отличие от переменных, объявленных через var, попытка обратиться к переменной let или const до её объявления вызовет ошибку. Этот феномен часто называют Временной мёртвой зоной.

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

В дополнение к сказанному: нельзя повторно объявить переменную, объявленную с помощью let:

let myVar = 2;
let myVar = 3; // Вызовет SyntaxError.
const

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

Итак, переменные, объявленные с помощью const:

  • имеют в качестве области видимости блок;
  • недоступны до объявления;
  • не могут быть повторно объявлены в той же области видимости;
  • не могут быть переопределены.
const myVar = "Коля";
myVar = "Ваня" // Вызовет ошибку, переопределять переменную нельзя.
const myVar = "Коля";
const myVar = "Ваня" // Вызовет ошибку, объявить переменную можно только один раз.

Но есть одна тонкость: переменные, объявленные с помощью const, не являются неизменными! А именно, это означает, что объекты и массивы, объявленные с помощью const, могут быть изменены.

В случае объектов:

const person = {
  name: 'Коля',
};
person.name = 'Ваня'; // Сработает! Переменная person не полностью переопределяется, а просто меняется.
console.log(person.name); // -> "Ваня"
person = "Сандра"; // Вызовет ошибку, потому что переменные, объявленные через const, переопределять нельзя.

В случае массивов:

const person = [];
person.push('Ваня'); // Сработает!  Переменная person не полностью переопределяется, а просто меняется.
console.log(person[0]); // -> "Ваня"
person = ["Коля"]; // Вызовет ошибку, потому что переменные, объявленные через const, переопределять нельзя.

Дополнительные материалы

  • How let and const are scoped in JavaScript — WesBos.
  • Temporal dead zone (tdz) demystified.

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

В обновлении JavaScript ES6 добавлены стрелочные функции — новый синтаксис записи функций. Вот некоторые их преимущества:

  • краткость;
  • this берется из окружающего контекста;
  • неявный возврат.

Пример кода

  • Краткость и неявный возврат.
function double(x) { return x * 2; } // Обычный способ.
console.log(double(2)); // -> 4
const double = x => x * 2; /* Та же функция, записанная в виде стрелочной функции с неявным возвратом. */
console.log(double(2)); // -> 4
  • Использование this.

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

function myFunc() {
  this.myVar = 0;
  setTimeout(() => {
    this.myVar++;
    console.log(this.myVar); // -> 1
  }, 0);
}

Подробное объяснение

Краткость

Стрелочные функции во многих отношениях более краткие, чем обычные. Рассмотрим все возможные случаи:

  • Явный и неявный возврат.

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

function double(x) {
  return x * 2; // Эта функция явно возвращает x * 2, использовано ключевое слово *return*.
}

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

const double = (x) => {
  return x * 2; // Явный возврат.
}

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

const double = (x) => x * 2; // Всё верно, вернётся x * 2.

Для этого нам просто нужно убрать фигурные скобки и ключевое слово return. Поэтому это и называется неявным возвратом: ключевого слова return нет, но функция все равно вернет x * 2.

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

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

const getPerson = () => ({ name: "Коля", age: 24 })
console.log(getPerson())
// { name: "Коля", age: 24 } — объект, неявно возвращенный стрелочной функцией.
  • Только один аргумент.

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

const double = (x) => x * 2; // Эта стрелочная функция принимает только один аргумент.

Скобки вокруг этого аргумента можно опустить:

const double = x => x * 2; // Эта стрелочная функция принимает только один аргумент.
  • Без аргументов.

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

() => { // Скобки есть, все хорошо.
  const x = 2;
  return x;
}
=> { // Скобок нет, так работать не будет!
  const x = 2;
  return x;
}
Использование this

Чтобы понять эту тонкость поведения стрелочных функций, нужно понимать, как this ведёт себя в JavaScript.

Внутри стрелочной функции значение this равно значению this внешнего окружения. Это значит, что стрелочная функция не создает новый this, а получает его из окружения.

Без использования стрелочных функций для получения доступа к переменной через this в функции, вложенной в другую функцию, придется использовать хак that = this или self = this.

Вот, к примеру, использование функции setTimeout внутри функции myFunc:

function myFunc() {
  this.myVar = 0;
  var that = this; // Тот самый хак *that = this*
  setTimeout(
    function() { // В этой области видимости функции создается новый *this*.
      that.myVar++;
      console.log(that.myVar); // -> 1
      console.log(this.myVar); // -> undefined — см. объявление функции выше.
    },
    0
  );
}

Но в случае стрелочных функций this берется из окружения:

function myFunc() {
  this.myVar = 0;
  setTimeout(
    () => { // this берется из окружения. В данном случае — из myFunc.
      this.myVar++;
      console.log(this.myVar); // -> 1
    },
    0
  );
}

Полезные ресурсы

  • JavaScript Arrow Functions Introduction — WesBos.
  • Стрелочные функции в JavaScript — MDN.
  • Javascript ES6 — Arrow Functions and Lexical this.

Значение аргументов функции по умолчанию

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

function myFunc(x = 10) {
  return x;
}
console.log(myFunc()); /* -> 10 — никакое значение не передается,
поэтому в myFunc х присваивается значение по умолчанию, т.е. 10 */
console.log(myFunc(5)); /* -> 5 — передается значение,
поэтому в myFunc х присваивается значение 5 */

console.log(myFunc(undefined)); /* -> 10 — передается значение undefined,
поэтому х присваивается значение по умолчанию */
console.log(myFunc(null)); // -> null — передается значение null. Подробнее см. ниже.

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

  • значение не передано;
  • передано значение undefined.

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

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

Дополнительные материалы

  • Extended Parameter Handling.
  • Параметры по умолчанию — MDN.

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

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

На практике деструктуризацию можно использовать, чтобы присваивать переменным разбитые на части параметры функции или this.props в React-проектах.

Объяснение с помощью примера кода

  • Объект.

Давайте использовать во всех примерах следующий объект:

const person = {
  firstName: "Коля",
  lastName: "Андреев",
  age: 35,
  sex: "М",
};

Без деструктуризации:

const first = person.firstName;
const age = person.age;
const city = person.city || "Санкт-Петербург";

С деструктуризацией всё поместится в одну строку:

const { firstName: first, age, city = "Санкт-Петербург" } = person; // И всё!
console.log(age); /* -> 35 — Создана новая переменная age,
и ей присвоено значение, равное person.age. */
console.log(first); /* -> "Коля" — Создана новая переменная first,
и ей присвоено значение, равное person.firstName. */
console.log(firstName); /* -> ReferenceError — person.firstName существует,
НО новая созданная переменная называется first. */
console.log(city); /* -> "Санкт-Петербург" — Создана новая переменная city,
и, поскольку свойство person.city ранее не было определено,
переменной присвоено альтернативное значение "Санкт-Петербург". */

Примечание: В const { age } = person; скобки после ключевого слова const используются не для обозначения объекта или блока. Это синтаксис деструктуризации.

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

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

Без деструктуризации:

function joinFirstLastName(person) {
  const firstName = person.firstName;
  const lastName = person.lastName;
  return `${firstName}${lastName}`;
}
joinFirstLastName(person); // -> "Коля-Андреев"

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

function joinFirstLastName({ firstName, lastName }) { /* Мы создали переменные firstName и lastName
  из частей параметра person. */
  return `${firstName}${lastName}`;
}
joinFirstLastName(person); // -> "Коля-Андреев"

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

const joinFirstLastName = ({ firstName, lastName }) => `${firstName}${lastName}`;
joinFirstLastName(person); // -> "Коля-Андреев"
  • Массив.

Давайте рассмотрим следующий массив:

const myArray = ["a", "b", "c"];

Без деструктуризации:

const x = myArray[0];
const y = myArray[1];

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

const [x, y] = myArray; // Вот и всё!

console.log(x); // -> "a"
console.log(y); // -> "b"

Полезные ресурсы

  • Destructuring Assignment.
  • Destructuring Assignment — WesBos.
  • ExploringJS — Destructuring.

Методы массивов — map / filter / reduce

map, filter и reduce — это методы массивов, пришедшие из парадигмы функционального программирования.

Перечислю их:

  • Array.prototype.map() принимает массив, каким-нибудь образом преобразует его элементы и возвращает новый массив трансформированных элементов.
  • Array.prototype.filter() принимает массив, просматривает каждый элемент и решает, убрать его или оставить. Возвращает массив оставшихся значений.
  • Array.prototype.reduce() принимает массив и вычисляет на основе его элементов какое-то единое значение, которое и возвращает.

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

Вооружившись этими тремя методами, вы можете обойтись без использования for и forEach в большинстве ситуаций. Когда в следующий раз соберётесь запустить цикл for, попробуйте решить задачу с помощью map, filter и reduce. Поначалу это будет трудно, потому что вам придётся научиться мыслить по-другому, но, разобравшись один раз, вы сможете применять эти методы без особых усилий.

Пример кода

const numbers = [0, 1, 2, 3, 4, 5, 6];
const doubledNumbers = numbers.map(n => n * 2); // -> [0, 2, 4, 6, 8, 10, 12]
const evenNumbers = numbers.filter(n => n % 2 === 0); // -> [0, 2, 4, 6]
const sum = numbers.reduce((prev, next) => prev + next, 0); // -> 21

Давайте посчитаем сумму баллов всех студентов, которые набрали больше 10 баллов, используя map, filter и reduce:

const students = [
  { name: "Коля", grade: 10 },
  { name: "Ваня", grade: 15 },
  { name: "Юля", grade: 19 },
  { name: "Наташа", grade: 9 },
];

const aboveTenSum = students
  .map(student => student.grade) // Создаём массив оценок из массива студентов с помощью метода map.
  .filter(grade => grade >= 10) // Выбираем только оценки выше 10 при помощи метода filter.
  .reduce((prev, next) => prev + next, 0); // Суммируем все оценки выше 10 друг с другом.

console.log(aboveTenSum); /* -> 44: 10 (Коля) + 15 (Ваня) + 19 (Юля),
оценка Наташи меньше 10 и была проигнорирована */

Объяснение

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

const numbers = [0, 1, 2, 3, 4, 5, 6];
Array.prototype.map()
const doubledNumbers = numbers.map(function(n) {
  return n * 2;
});
console.log(doubledNumbers); // -> [0, 2, 4, 6, 8, 10, 12]

Что же здесь происходит? Мы применяем к массиву numbers метод map, который взаимодействует с каждым элементом массива, передавая его в нашу функцию. Цель функции — произвести расчёт и вернуть новое значение, чтобы map мог подставить его вместо переданного в функцию.

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

const doubleN = function(n) { return n * 2; };
const doubledNumbers = numbers.map(doubleN);
console.log(doubledNumbers); // -> [0, 2, 4, 6, 8, 10, 12]

numbers.map(doubleN) создаёт [doubleN(0), doubleN(1), doubleN(2), doubleN(3), doubleN(4), doubleN(5), doubleN(6)], что равняется [0, 2, 4, 6, 8, 10, 12].

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

Array.prototype.filter()
const evenNumbers = numbers.filter(function(n) {
  return n % 2 === 0; // Истинно, если n чётное; ложно, если n нечётное.
});
console.log(evenNumbers); // -> [0, 2, 4, 6]

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

Array.prototype.reduce()

Цель метода reduce заключается в том, чтобы вычислить на основе массива какое-то одно значение. Какие именно вычисления метод произведет с элементами, зависит только от вас.

const sum = numbers.reduce(
  function(acc, n) {
    return acc + n;
  },
0 // Значение аккумулирующей переменной на первом шаге цикла.
);
console.log(sum); // -> 21

Так же, как методы .map и .filter, метод .reduce применяется к массиву и в качестве первого параметра принимает функцию.

На этот раз, впрочем, кое-что изменилось:

  • .reduce принимает два параметра.

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

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

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

Функция, которую вы передаёте в качестве первого параметра метода .reduce, принимает два аргумента. Первый аргумент — это аккумулирующая переменная (acc в нашем примере), второй аргумент — текущий элемент.

Аккумулирующая переменная равна значению, возвращённому нашей функцией на предыдущем шаге цикла. В самом начале каждого цикла acc равна значению, которое было передано в качестве второго параметра .reduce.

На первом шаге

acc = 0, потому что мы передали 0 в качестве второго параметра метода reduce.

n = 0 — первый элемент массива number.

Функция возвращает acc + n —> 0 + 0 —> 0.

На втором шаге

acc = 0, потому что это значение функция вернула на предыдущем шаге.

n = 1 — второй элемент массива number.

Функция возвращает acc + n —> 0 + 1 —> 1.

На третьем шаге

acc = 1, потому что это значение функция вернула на предыдущем шаге.

n = 2 — третий элемент массива number.

Функция возвращает acc + n —> 1 + 2 —> 3.

На четвертом шаге

acc = 3, потому что это значение функция вернула на предыдущем шаге.

n = 3 — четвёртый элемент массива number.

Функция возвращает acc + n —> 3 + 3 —> 6.

На последнем шаге

acc = 15, потому что это значение функция вернула на предыдущем шаге.

n = 6 — последний элемент массива number.

Функция возвращает acc + n —> 15 + 6 —> 21.

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

Дополнительные материалы

  • Understanding map, filter and reduce in JavaScript.

Оператор расширения ...

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

Пример кода

const arr1 = ["a", "b", "c"];
const arr2 = [...arr1, "d", "e", "f"]; // -> ["a", "b", "c", "d", "e", "f"]
function myFunc(x, y, ...params) {
  console.log(x); // -> "a"
  console.log(y); // -> "b"
  console.log(params); // -> ["c", "d", "e", "f"]
}

myFunc("a", "b", "c", "d", "e", "f");
// "a"
// "b"
// ["c", "d", "e", "f"]
const { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
console.log(x); // -> 1
console.log(y); // -> 2
console.log(z); // -> { a: 3, b: 4 }
const n = { x, y, ...z };
console.log(n); // -> { x: 1, y: 2, a: 3, b: 4 }

Объяснение

В итерируемых объектах (например, массивах)

Если у нас есть два следующих массива:

const arr1 = ["a", "b", "c"];
const arr2 = [arr1, "d", "e", "f"]; // -> [["a", "b", "c"], "d", "e", "f"]

Первый элемент массива arr2 — это массив, потому что arr1 напрямую вставляется в arr2. Но мы хотим, чтобы arr2 состоял только из букв. Чтобы добиться этого, мы можем развернуть элементы массива arr1 в массиве arr2.

С использованием оператора расширения:

const arr1 = ["a", "b", "c"];
const arr2 = [...arr1, "d", "e", "f"]; // -> ["a", "b", "c", "d", "e", "f"]
Оставшиеся аргументы функции

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

function myFunc() {
  for (var i = 0; i < arguments.length; i++) {
    console.log(arguments[i]);
  }
}
myFunc("Коля", "Андреев", 10, 12, 6);
// "Коля"
// "Андреев"
// 10
// 12
// 6

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

Именно это позволяет нам сделать оператор оставшихся аргументов!

function createStudent(firstName, lastName, ...grades) {
  /* firstName = "Коля"
  lastName = "Андреев"
  [10, 12, 6] — оператор `...` берет все остальные параметры, переданные функции,
  и создает переменную grades с массивом, в котором они хранятся. */

  const avgGrade = grades.reduce((acc, curr) => acc + curr, 0) / grades.length;
  // Вычисляет средний балл из всех оценок.

  return {
    firstName: firstName,
    lastName: lastName,
    grades: grades,
    avgGrade: avgGrade,
  }
}

const student = createStudent("Коля", "Андреев", 10, 12, 6);
console.log(student);
/* {
firstName: "Коля",
lastName: "Андреев",
grades: [10, 12, 6],
avgGrade: 9,33
} */

Примечание: createStudent — плохая функция, потому что мы не проверяем, существует ли grades.length и отличается ли от 0. Но так функцию легче прочитать, поэтому я не учитывал эти случаи.

Расширение свойств объектов

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

const myObj = { x: 1, y: 2, a: 3, b: 4 };
const { x, y, ...z } = myObj; // Деструктуризация объекта.
console.log(x); // -> 1
console.log(y); // -> 2
console.log(z); // -> { a: 3, b: 4 }

// z - это остаток от деструктурированного объекта: объект myObj минус деструктурированные свойства х и у.

const n = { x, y, ...z };
console.log(n); // -> { x: 1, y: 2, a: 3, b: 4 }

// Здесь свойства объекта z расширяются в n

Дополнительные материалы

  • TC39 — Object Rest/Spread Properties for ECMAScript.
  • Spread Operator Introduction — WesBos.
  • JavaScript & The spread operator.
  • 6 Great Uses of the Spread Operator.

Сокращенная запись свойств объектов

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

const x = 10;
const myObj = { x };
console.log(myObj.x) // -> 10

Объяснение

Раньше (до ES2015), если вы хотели при объявлении нового литерала объекта использовать переменные в качестве его свойств, вам пришлось бы писать подобный код:

const x = 10;
const y = 20;

const myObj = {
  x: x, // Запись значения переменной х в myObj.x.
  y: y, // Запись значения переменной у в myObj.y.
};

console.log(myObj.x); // -> 10
console.log(myObj.y); // -> 20

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

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

const x = 10;
const y = 20;

const myObj = {
  x,
  y,
};

console.log(myObj.x); // -> 10
console.log(myObj.y); // -> 20

Дополнительные материалы

  • Enhanced Object Properties — ES6 Features.

Промисы

Промис (promise) — это объект, который может быть синхронно возвращён из асинхронной функции (Ссылка).

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

Пример кода

const fetchingPosts = new Promise((res, rej) => {
  $.get("/posts")
  .done(posts => res(posts))
  .fail(err => rej(err));
});

fetchingPosts
  .then(posts => console.log(posts))
  .catch(err => console.log(err));

Пояснение

Когда вы делаете AJAX-запрос, ответ будет несинхронным, так как вы запрашиваете ресурс, на обработку которого требуется некоторое время. Ответ может быть вообще не получен, если запрашиваемый ресурс недоступен по какой-то причине (404).

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

  • выполняется;
  • выполнено;
  • отклонено.

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

Создание промиса

Сначала создадим промис. Будем использовать GET-метод jQuery для создания AJAX-запроса к ресурсу X.

const xFetcherPromise = new Promise(
// Создаём промис с помощью ключевого слова new и сохраняем его в переменную
  function(resolve, reject) {
    /* Конструктор промиса принимает в виде параметра функцию, которая, в свою очередь,
    принимает 2 параметра: resolve и reject. */
    $.get("X") // Запускаем AJAX-запрос
      .done(function(X) { // Как только запрос выполнен...
        resolve(X); // ... выполняем промис со значением X в качестве параметра.
      })
      .fail(function(error) { // Если запрос не прошёл...
        reject(error); // ... отклоняем промис со значением error.
      });
  }
)

Как видно из рассмотренного примера, объект Promise принимает функцию-исполнитель, в свою очередь принимающую два параметра: resolve и reject. Эти параметры — функции, которые при вызове изменяют состояние промиса со значения выполняется на выполнено или отклонено соответственно.

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

Использование обработчиков промисов

Чтобы получить результат (или ошибку) промиса, нужно назначить ему обработчики следующим образом:

xFetcherPromise
  .then(function(X) {
    console.log(X);
  })
  .catch(function(err) {
    console.log(err);
  })

Если вызов прошёл успешно, вызывается resolve и выполняется функция, переданная в метод .then.

Если вызов не прошёл, вызывается reject и выполняется функция, переданная в .catch.

Примечание: Если промис уже выполнен или отклонён на момент назначения соответствующего обработчика, обработчик всё равно будет вызван. Так что между выполнением асинхронной операции и назначением обработчиков не возникает состояние гонки. (Ссылка: MDN)

Дополнительные материалы

  • JavaScript Promises for Dummies — Jecelyn Yeen.
  • JavaScript Promise API — David Walsh.
  • Using promises — MDN.
  • Master the JavaScript Interview: What is a Promise? — Eric Elliott.
  • JavaScript Promises: an Introduction — Jake Archibald.
  • Документация по промисам — MDN.

Шаблонные строки

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

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

Пример кода

const name = "Коля";
`Привет, ${name}, следующее выражение равно четырем: ${2+2}.`;

// -> Привет, Коля, следующее выражение равно четырем: 4.

Дополнительные материалы

  • String Interpolation — Особенности ES6.
  • Getting Literal With ES6 Template Strings — Addy Osmani.

Тегированные шаблонные строки

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

Примечание: Известная библиотека, которая называется стилизованные компоненты, основана на этой возможности.

Ниже приведен пример работы тегированных шаблонных строк:

function highlight(strings, ...values) {
  const interpolation = strings.reduce((prev, current) => {
    return prev + current + (values.length ? "<mark>" + values.shift() + "</mark>" : "");
  }, "");

  return interpolation;
}

const meal = "круассаны";
const drink = "кофе";

highlight`Я люблю ${meal} с ${drink}.`;
// -> <mark>Я люблю круассаны с кофе.</mark>

Более интересный пример:

function comma(strings, ...values) {
  return strings.reduce((prev, next) => {
    let value = values.shift() || [];
    value = value.join(", ");
    return prev + next + value;
  }, "");
}

const snacks = ["яблоки", "бананы", "апельсины"];
comma`Я люблю ${snacks} на десерт.`;
// -> Я люблю яблоки, бананы, апельсины на десерт.

Дополнительные материалы

  • Tagged Template Literals — Wes Bos.
  • Библиотека common-tags.

Импорт / экспорт

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

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

Объяснение с примером кода

Именованный экспорт

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

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

// mathConstants.js
export const pi = 3.14;
export const exp = 2.7;
export const alpha = 0.35;

// -------------

// myFile.js
import { pi, exp } from './mathConstants.js';
// Именованный импорт — с синтаксисом, похожим на деструктуризацию.
console.log(pi) // -> 3.14
console.log(exp) // -> 2.7

// -------------

// mySecondFile.js
import * as constants from './mathConstants.js';
// Все экспортированные значения записываются в переменную constants.
console.log(constants.pi) // -> 3.14
console.log(constants.exp) // -> 2.7

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

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

import { foo as bar } from 'myFile.js';
// foo импортируется и записывается в новую переменную bar.
Импорт / экспорт по умолчанию

Что касается экспорта по умолчанию, то для каждого модуля (файла) может быть только один экспорт. Экспортом по умолчанию может быть функция, класс, объект или что-то еще. Это значение считается «основным», поскольку его будет проще всего импортировать. Ссылка: MDN.

// coolNumber.js
const ultimateNumber = 42;
export default ultimateNumber;

// ------------

// myFile.js
import number from './coolNumber.js';
/* В переменную number автоматически попадает экспорт по умолчанию —
вне зависимости от его имени в исходном модуле. */
console.log(number) // -> 42

Экспорт функций:

// sum.js
export default function sum(x, y) {
  return x + y;
}

// -------------

// myFile.js
import sum from './sum.js';
const result = sum(1, 2);
console.log(result) // -> 3

Дополнительные материалы

  • ES6 Modules in bulletpoints
  • Экспорт — MDN.
  • Импорт — MDN.
  • Understanding ES6 Modules.
  • Destructuring special case — import statements.
  • Misunderstanding ES6 Modules — Kent C. Dodds.
  • Modules in JavaScript.

this в JavaScript

Оператор this в JavaScript ведет себя не так, как в других языках. В большинстве случаев он определяется тем, как вызвана функция (Ссылка: MDN).

Это сложное понятие с множеством тонкостей, так что я крайне рекомендую вам тщательно изучить приведенные ниже Дополнительные материалы. Я покажу вам, как сам лично определяю, чему равно this. Этому меня научила вот эта статья Yehuda Katz.

function myFunc() {
  ...
}

// После каждого выражения находим значение this в myFunc.

myFunc.call("myString", "привет");
// myString — в this записывается значение первого параметра .call.

// В non-strict-режиме.
myFunc("привет");
// window — myFunc() — это синтаксический сахар для myFunc.call(window, "привет").

// В strict-режиме.
myFunc("привет");
// undefined — myFunc() — это синтаксический сахар для myFunc.call(undefined, "привет").
var person = {
  myFunc: function() { ... }
}

person.myFunc.call(person, "test");
// person Object — в this записывается значение первого параметра call.
person.myFunc("test");
// person Object — person.myFunc() — это синтаксический сахар для person.myFunc.call(person, "test").

var myBoundFunc = person.myFunc.bind("привет");
// Создает новую функцию, в которой мы записываем "привет" в значение this.
person.myFunc("test");
// person Object — Метод bind не влияет на первоначальный метод.
myBoundFunc("test");
// "hello" — myBoundFunc — это person.myFunc, в которой this привязана к "привет".

Дополнительные материалы

  • Understanding JavaScript Function Invocation and «this» — Yehuda Katz.
  • this в JavaScript — MDN.

Класс

JavaScript — это язык, основанный на прототипах (в то время как, например, Java — язык, основанный на классах). В обновлении ES6 представлены классы JavaScript, которые являются синтаксическим сахаром для наследования на основе прототипов, а не новой моделью наследования на основе классов (Ссылка).

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

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

Примеры

До ES6, синтаксис на основе прототипов:

var Person = function(name, age) {
  this.name = name;
  this.age = age;
};
Person.prototype.stringSentence = function() {
  return "Привет, меня зовут " + this.name + " и мне " + this.age;
};

Начиная с ES6, синтаксис на основе классов:

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

  stringSentence() {
    return `Привет, меня зовут ${this.name} и мне ${this.age}`;
  }
}

const myPerson = new Person("Маша", 23);
console.log(myPerson.age); // -> 23
console.log(myPerson.stringSentence()); // -> "Привет, меня зовут Маша и мне 23

Дополнительные материалы

Для понимания прототипов:

  • Understanding Prototypes in JS — Yehuda Katz
  • A plain English guide to JS prototypes — Sebastian Porto
  • Наследование и цепочка прототипов — MDN.

Для понимания классов:

  • ES6 Classes in Depth — Nicolas Bevacqua
  • ES6 Features — Classes
  • Классы JavaScript — MDN.

Ключевые слова Extends и super

Ключевое слово extends используется в объявлении класса или в выражениях класса для создания дочернего класса (Ссылка: MDN). Дочерний класс наследует все свойства родительского класса и дополнительно может добавлять новые свойства или изменять унаследованные.

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

  • В конструкторе ключевое слово super должно использоваться раньше, чем ключевое слово this.
  • Вызов super() вызывает конструктор родительского класса. Если вы хотите передать какие-то аргументы из конструктора класса в конструктор родительского класса, то нужно вызывать функцию следующим образом: super(arguments).
  • Если у родительского класса есть метод X (даже статический), для его вызова в дочернем классе можно использовать super.X().

Пример кода

class Polygon {
  constructor(height, width) {
    this.name = 'Многоугольник';
    this.height = height;
    this.width = width;
  }

  getHelloPhrase() {
    return `Привет, я — ${this.name}`;
  }
}

class Square extends Polygon {
  constructor(length) {
    /* Здесь вызывается конструктор родительского класса со значением length,
    передаваемым для переменных width и height класса Polygon. */

    super(length, length);
    /* Примечание: в производных классах перед тем, как использовать 'this',
    нужно вызвать функцию super(), иначе это приведёт к ошибке. */

    this.name = 'Квадрат';
    this.length = length;
  }

  getCustomHelloPhrase() {
    const polygonPhrase = super.getHelloPhrase();
    // Получение доступа к родительскому методу с помощью синтаксиса super.X().
    return `${polygonPhrase} с длиной стороны ${this.length}`;
  }

  get area() {
    return this.height * this.width;
  }
}

const mySquare = new Square(10);
console.log(mySquare.area) // -> 100
console.log(mySquare.getHelloPhrase())
/* -> 'Привет, я — Квадрат'
Класс Square наследуется от класса Polygon и имеет доступ к его методам.*/
console.log(mySquare.getCustomHelloPhrase())
// -> 'Привет, я — Квадрат с длиной стороны'

Примечание: Если бы мы попытались использовать this перед вызовом super() в классе Square, произошёл бы ReferenceError:

class Square extends Polygon {
  constructor(length) {
    this.height;
    // ReferenceError, сначала нужно вызывать super!

    /* Здесь вызывается конструктор родительского класса со значением length
    в качестве значений width и height класса Polygon. */
    // Here, it calls the parent class' constructor with lengths
    super(length, length);

    /* Примечание: в производных класса super() должен быть вызван до использования 'this'.
    Иначе это приведёт к ошибке. */
    this.name = 'Квадрат';
  }
}

Дополнительные материалы

  • Extends — MDN.
  • Оператор Super — MDN.
  • Inheritance — MDN.

Async Await

Помимо Промисов вам может встретиться еще один синтаксис для обработки асинхронного кода — async/await.

Цель функций async/await — упростить синхронное использование промисов и выполнить какое-либо действие над группой промисов. Точно так же, как промисы похожи на структурированные функции обратного вызова, async/await похожи на комбинацию генераторов и промисов. (Ссылка: MDN)

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

Примечание 2: await должен использоваться в async функции, что означает, что вы не можете использовать await на верхнем уровне вашего кода, так как он не находится внутри async-функции.

Пример кода

async function getGithubUser(username) {
  // Ключевое слово async позволяет использовать await в функции и означает, что функция возвращает промис.
  const response = await fetch(`https://api.github.com/users/${username}`);
  // «Синхронное» ожидание промиса перед переходом на новую строку.
  return response.json();
}

getGithubUser('mbeaudru')
  .then(user => console.log(user))
  /* Логирование пользователя — не может использовать синтаксис await,
  так как этот код не находится внутри async-функции. */
  .catch(err => console.log(err));
  // Если в нашей асинхронной функции возникнет ошибка, то мы перехватим ее здесь.

Объяснение с помощью примера кода

async/await построены на промисах, но позволяют использовать более императивный стиль кода.

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

async function myFunc() {
  // Можно использовать оператор await, так как это async-функция.
  return "hello world";
}

myFunc().then(msg => console.log(msg));
// "Привет, мир!" — возвращаемое значение myFunc превращается в промис из-за оператора async.

Когда будет достигнут оператор return async-функции, промис выполняется с возвращаемым значением. Если внутри async-функции генерируется ошибка, состояние промиса изменится на rejected. Если async-функция не возвращает никакого значения, промис всё равно будет возвращен и выполнится без значения, когда выполнение async-функции будет завершено.

Оператор await используется для ожидания выполнения Промиса и может быть использован только в теле async-функции. При этом выполнение кода приостанавливается, пока не будет выполнен промис.

Примечание: fetch — это функция, возвращающая промис, который позволяет выполнить AJAX-запрос.

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

function getGithubUser(username) {
  return fetch(`https://api.github.com/users/${username}`).then(response => response.json());
}

getGithubUser('mbeaudru')
  .then(user => console.log(user))
  .catch(err => console.log(err));

Вот эквивалент с использованием async/await:

async function getGithubUser(username) {
  // Превращение в промис + разрешено использование ключевого слова await.
  const response = await fetch(`https://api.github.com/users/${username}`);
  // Выполнение останавливается здесь, пока не закончится выполнение промиса.
  return response.json();
}

getGithubUser('mbeaudru')
  .then(user => console.log(user))
  .catch(err => console.log(err));

Синтаксис async/await особенно удобен для построения цепочек взаимозависимых промисов.

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

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

async function fetchPostById(postId) {
  const token = (await fetch('token_url')).json().token;
  const post = (await fetch(`/posts/${postId}?token=${token}`)).json();
  const author = (await fetch(`/users/${post.authorId}`)).json();

  post.author = author;
  return post;
}

fetchPostById('gzIrzeo64')
  .then(post => console.log(post))
  .catch(err => console.log(err));
Обработка ошибок

Если мы не добавим блок try / catch вокруг выражения await, неперехваченные исключения — неважно, были ли они выброшены в теле вашей async-функции или во время ожидания выполнения await — отклонят промис, возвращенный из async-функции. Использование состояния throw в асинхронной функции — то же самое, что возврат промиса, который был отклонен. (Ссылка: PonyFoo).

Примечание: Промисы ведут себя так же!

С помощью промисов вот как бы мы обработали ошибки:

function getUser() { // Этот промис будет отклонен!
  return new Promise((res, rej) => rej("Пользователь не найден!"));
}

function getAvatarByUsername(userId) {
  return getUser(userId).then(user => user.avatar);
}

function getUserAvatar(username) {
  return getAvatarByUsername(username).then(avatar => ({ username, avatar }));
}

getUserAvatar('mbeaudru')
  .then(res => console.log(res))
  .catch(err => console.log(err)); // -> "Пользователь не найден!"

Эквивалент с использованием async/await:

async function getUser() {
  // Возвращенный промис будет отклонен!
  throw "User not found !";
}

async function getAvatarByUsername(userId) => {
  const user = await getUser(userId);
  return user.avatar;
}

async function getUserAvatar(username) {
  var avatar = await getAvatarByUsername(username);
  return { username, avatar };
}

getUserAvatar('mbeaudru')
  .then(res => console.log(res))
  .catch(err => console.log(err)); // -> "Пользователь не найден!"

Дополнительные материалы

  • Async/Await — JavaScript.Info.
  • ES7 Async/Await.
  • 6 Reasons Why JavaScript’s Async/Await Blows Promises Away.
  • JavaScript awaits.
  • Using Async Await in Express with Node 8.
  • Функция Async.
  • Await.
  • Using async / await in express with node 8.

Истина / Ложь

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

Любое значение будет приведено к true (истина), кроме:

  • false (ложь);
  • 0;
  • "" (пустая строка);
  • null;
  • undefined;
  • NaN.

Вот примеры булева контекста:

  • значение условия if.

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

  • После логического оператора NOT !.

Этот оператор возвращает значение «ложь», если его единственный операнд может быть преобразован к значению «истина»; иначе он возвращает значение «истина».

!0 // -> «истина»: 0 — это «ложь», поэтому вернется "истина".
!!0 // -> «ложь»: 0 — это «ложь», следовательно !0 возвращает истину, а !(!0) возвращает «ложь».
!!"" // -> «ложь»: пустая строка — «ложь», поэтому НЕ (НЕ «ложь») равно «ложь».
  • Конструктор объектов типа Boolean.
new Boolean(0); // «ложь»
new Boolean(1); // «истина»
  • Тернарный оператор.
myVar ? "истина" : "ложь"

Значение myVar вычисляется в булевом контексте.

Будьте внимательны при сравнении двух значений. Значения объектов (которые должны быть приведены к истине), не приводятся к булеву типу, а приводятся к примитивному типу в соответствии со спецификацией. Внутри, когда объект сравнивается с булевым значением, например, [] == true, выполняется [].toString() == true, происходит следующее:

let a = [] == true // a ложно, так как [].toString() возвращает пустую строку ("").
let b = [1] == true // b истинно, так как [1].toString() возвращает "1".
let c = [2] == true // c ложно, так как [2].toString() возвращает "2".

Дополнительные материалы

  • Truthy (MDN).
  • Falsy (MDN).
  • Truthy and Falsy values in JS — Josh Clanton.

Анаморфизмы и катаморфизмы

Анаморфизмы

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

Рассмотрим разворачивание целого числа в список целых чисел. Целое число — наш изначальный объект, а список целых чисел — более сложная структура.

Пример кода
function downToOne(n) {
  const list = [];

  for (let i = n; i > 0; --i) {
    list.push(i);
  }

  return list;
}

downToOne(5)
  //-> [ 5, 4, 3, 2, 1 ]

Катаморфизмы

Катаморфизмы противоположны анаморфизмам: они берут объекты более сложной структуры и складывают их в более простые структуры.

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

Пример кода
function product(list) {
  let product = 1;

  for (const n of list) {
    product = product * n;
  }

  return product;
}

product(downToOne(5)) // -> 120

Дополнительные материалы

  • Anamorphisms in JavaScript.
  • Anamorphism.
  • Catamorphism.

Генераторы

Другой способ написания функции downToOne — использование генератора. Чтобы создать объект типа Generator, нужно использовать объявление function *. Генераторы — это функции, выполнение которых может быть прервано, а затем продолжено с тем же контекстом (привязками переменных), сохраняющимся при всех вызовах.

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

function * downToOne(n) {
  for (let i = n; i > 0; --i) {
    yield i;
  }
}

[...downToOne(5)] // -> [ 5, 4, 3, 2, 1 ]

Генераторы возвращают итерируемый объект. Когда вызывается метод next() итератор, она выполняется до первого выражения yield, которое указывает значение, которое должно быть возвращено из итератора или с помощью yield*, которое дегегирует выполнение другому генератору. Когда в генераторе вызывается выражение return, он будет помечать генератор как выполненный и возвращать значение из выражения return. Дальнейшие вызовы next() не будут возвращать никаких новых значений.

Пример кода

// Пример использования
function * idMaker() {
  var index = 0;
  while (index < 2) {
    yield index;
    index = index + 1;
  }
}

var gen = idMaker();

gen.next().value; // -> 0
gen.next().value; // -> 1
gen.next().value; // -> undefined

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

// Пример использования yield *
function * genB(i) {
  yield i + 1;
  yield i + 2;
  yield i + 3;
}

function * genA(i) {
  yield i;
  yield* genB(i);
  yield i + 10;
}

var gen = genA(10);

gen.next().value; // -> 10
gen.next().value; // -> 11
gen.next().value; // -> 12
gen.next().value; // -> 13
gen.next().value; // -> 20
// Пример возврата из генератора
function* yieldAndReturn() {
  yield "Y";
  return "R";
  yield "unreachable";
}

var gen = yieldAndReturn()
gen.next(); // -> { value: "Y", done: false }
gen.next(); // -> { value: "R", done: true }
gen.next(); // -> { value: undefined, done: true }

Дополнительные материалы

  • Итераторы и генераторы — MDN.

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

Краткое объяснение

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

Пример кода

class Repo {
  static getName() {
    return "Repo name is modern-js-cheatsheet";
  }
}

// Обратите внимание, что нам не пришлось создавать экземпляр класса Repo.
console.log(Repo.getName()); // Repo name is modern-js-cheatsheet

let r = new Repo();
console.log(r.getName()); // Не пойманный TypeError: repo.getName не является функцией.

Подробное объяснение

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

Вызов статических методов из статического метода.

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

class Repo {
  static getName() {
    return "Repo name is modern-js-cheatsheet";
  }

  static modifyName(){
    return `${this.getName()}-added-this`;
  }
}

console.log(Repo.modifyName()); // Repo name is modern-js-cheatsheet-added-this
Вызов статических методов из нестатических методов

Нестатические методы могут вызывать статические двумя способами:

  1. Используя имя класса.

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

class Repo {
  static getName() {
    return "Repo name is modern-js-cheatsheet"
  }

  useName(){
    return `${Repo.getName()} and it contains some really important stuff`;
  }
}

// Нужно создать экземпляр класса для использования нестатических методов.
let r = new Repo();
console.log(r.useName()); // Repo name is modern-js-cheatsheet and it contains some really important stuff
  1. Используя конструктор.

Статические методы можно вызвать как свойства объекта-конструктора класса.

class Repo {
  static getName() {
    return "Repo name is modern-js-cheatsheet"
  }

useName(){
// Вызывает статический метод как обычное свойство конструктора.
  return `${this.constructor.getName()} and it contains some really important stuff`;
  }
}

// Нужно создать экземпляр класса для использования нестатических функций.
let r = new Repo();
console.log(r.useName()); // Repo name is modern-js-cheatsheet and it contains some really important stuff

Дополнительные материалы

  • Ключевое слово static — MDN.
  • Static Methods- Javascript.info.
  • Static Members in ES6 — OdeToCode.

Глоссарий

Область видимости

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

Источник: MDN

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

Говорят, что переменная изменилась, когда её значение изменилось относительно начального.

var myArray = [];
myArray.push("firstEl") // Значение myArray изменено.

Переменная называется неизменяемой, если она не может быть изменена.

Более подробно смотрите в статье на MDN.

Понравилась статья? Поделить с друзьями:
  • Дэу матиз мануал по ремонту
  • Антигриппин инструкция по применению порошок взрослым инструкция цена
  • Руководство гениального принца по вызволению страны из долгов мультсериал 2022 2 сезон
  • Скачать руководство по ремонту infiniti qx56
  • Доксорубицин инструкция по применению для животных