Принято считать, что каждый фрагмент данных на JavaScript, предоставляемый или используемый нами, содержит значение. Из уже рассмотренного примера мы узнали, что вместо слов hello, world! могут быть любые слова, с помощью которых мы задаем функцию alert:
alert("hello, world!");
В JavaScript эти слова несут в себе определенную информацию и считаются значениями. Мы могли об этом особенно не задумываться, набирая их на клавиатуре, но во вселенной JavaScript каждый элемент данных, с которым мы имеем дело, считается значением.
Итак, почему это так важно понимать? А все потому, что нам предстоит много работать со значениями. И важно, чтобы эта работа не свела вас с ума. Чтобы облегчить себе жизнь, вам пригодятся умения:
• легко идентифицировать эти значения;
• повторно их использовать в ходе разработки приложения, не прибегая к лишнему дублированию самого значения.
Две вещи, на которые предстоит потратить все наше оставшееся время, — это переменные. Давайте рассмотрим их поподробнее.
Переменная — это идентификатор значения. Чтобы не набирать hello, world! каждый раз, когда вы хотите использовать этот фрагмент для написания кода приложения, можно присвоить эту фразу переменной и использовать ее тогда, когда нужно. Еще чуть-чуть, и вам станет гораздо понятнее — я обещаю!
Есть несколько способов, как использовать переменные. В большинстве случаев лучше всего полагаться на ключевое слово let, после которого вы можете задать имя своей переменной:
let myText
В этой строке кода мы объявляем переменную myText. На данный момент наша переменная просто объявлена и не содержит никакого значения, то есть является пустой оболочкой.
Давайте исправим это через инициализацию переменной значением. К примеру, hello, world!.
let myText = "hello, world!";
С этого момента при выполнении кода значение hello, world! будет ассоциироваться с нашей переменной myText. Теперь соберем все части в единое выражение. Если у вас все еще открыт файл hello_world.htm, замените содержимое тега script следующим (или создайте новый файл, добавив в него следующий код):
let myText = "hello, world!";
alert(myText);
Обратите внимание, что мы больше не передаем текст hello, world! в функцию alert напрямую. Вместо этого мы передаем в нее имя переменной myText. Конечный результат такой же, то есть при выполнении этой строчки отобразится функция alert с надписью hello, world!. Благодаря этому изменению нам достаточно определить hello, world! в одном месте кода. И если мы в дальнейшем захотим изменить hello, world! к примеру, на The dog ate my homework! (Собака съела мою домашку!), то все, что нам понадобится, — это просто изменить фразу, определенную в переменной myText:
let myText = "The dog ate my homework!";
alert(myText);
Теперь во всех секциях кода при обращении к переменной myText мы будем видеть новый текст. Невозможно придумать ничего более удобного и значительно экономящего время, особенно при работе со сложными приложениями, что позволяет вносить изменение в одном месте, и при этом это действие отразится в остальных частях кода. Вы увидите другие, менее тривиальные примеры со значимыми переменными далее.
Материал из предыдущей главы позволяет нам сильно продвинуться вперед. По меньшей мере, в той части, которая касается знакомства с JavaScript. Сейчас мы не станем слишком углубляться в переменные, так как займемся ими в следующих главах, когда рассмотрим код с важными переменными посложнее. И прежде чем закругляться, затронем еще несколько моментов.
Мы вольны именовать переменные так, как нам подходит. Игнорируя то, какие имена нам следует выбрать с точки зрения философских, культурных или стилистических предпочтений, с технической точки зрения язык JavaScript очень гибок и позволяет включать в них символы.
Однако эта гибкость не безгранична, поэтому при именовании следует помнить о следующем:
• Имя переменной может содержать один символ или столько, сколько вы хотите, — только представьте, тысячи тысяч символов!
• Переменные могут начинаться с буквы, нижнего подчеркивания или символа $, но не могут начинаться с числа.
• Следом за первым символом переменные могут состоять из любого сочетания букв, подчеркиваний, чисел и $. Мы также можем смешивать и комбинировать нижний и верхний регистры, пока не надоест.
• Пробелы не допускаются.
Ниже приведены некоторые примеры имен переменных:
let myText;
let $;
let r8;
let _counter;
let $field;
let thisIsALongVariableName_butItCouldBeLonger;
let $abc;
let OldSchoolNamingScheme;
Чтобы определить, является ли имя переменной допустимым, воспользуйтесь прекрасным сервисом по проверке имен переменных JavaScript https://mothereff.in/js-variables.
Помимо допустимых имен есть еще другие важные моменты, такие как соглашения о присвоении имен и то, сколько людей обычно именуют переменные и другие компоненты, которые вы идентифицируете с именем. Мы затронем эти темы в следующих главах.
Одна из особенностей JavaScript, о которой вы скоро узнаете, состоит в том, что это всепрощающий язык, который может дать вам множество поблажек.
Например, нам не обязательно использовать ключевое слово let, чтобы объявить переменную. Можно просто написать так:
myText = "hello, world!";
alert(myText);
Обратите внимание, что переменная myText использована, не будучи формально объявленной с помощью ключевого слова let. И хотя так делать и не рекомендуется, это считается вполне допустимым. В конечном счете мы получаем переменную с именем myText. Единственный нюанс состоит в том, что при объявлении переменной подобным образом мы делаем это глобально. Не беспокойтесь, если последнее предложение вам не понятно. Мы поговорим о значении слова «глобально» позже, когда рассмотрим область видимость переменных.
Стоит упомянуть, что объявление и инициализация переменной не обязательно должны быть частью одной инструкции. Можно разделить эти действия на разные строки:
let myText;
myText = "hello, world!";
alert(myText);
На практике мы будем разделять их постоянно.
И наконец, отмечу, что мы можем менять значение переменной, объявленной с let, на что угодно и когда угодно:
let myText;
myText = "hello, world!";
myText = 99;
myText = 4 * 10;
myText = true;
myText = undefined;
alert(myText);
Если вы работали с более требовательными языками, которые не позволяют переменным хранить разные типы данных, то знайте, что эту гибкость JavaScript одни восхваляют, а другие ненавидят. Тем не менее в JavaScript есть способ запретить изменять значения переменной после инициализации. Это можно сделать с помощью ключевого слова const, с которым мы можем объявлять и инициализировать переменные:
const siteURL = "https://www.google.com";
alert(siteURL);
Используя const, мы не можем изменить значение siteURL на что-то иное, чем https://www.google.com. При такой попытке JavaScript начнет ругаться. Несмотря на то что в использовании этого ключевого слова есть свои подводные камни, в целом оно может оказаться очень полезным для предотвращения случайного изменения переменной. Когда придет время, мы рассмотрим подобные подводные камни более подробно.
Почитайте «Область видимости переменных»
Теперь, когда вы знаете, как объявлять и инициализировать переменные, очень важно разобраться с их видимостью. Нужно понимать, при каких условиях можно использовать объявленную переменную. Иначе это зовется областью видимости переменной. Интересно? Тогда прочитайте главу 8 «Область видимости переменных».
КОРОТКО О ГЛАВНОМ
Значения — хранилища для данных, а переменные — легкий способ обратиться к этим данным. В значениях сокрыто много интересных деталей, но на данный момент они вам ни к чему. Просто знайте, что JavaScript позволяет представлять различные значения вроде текста и чисел без шума и пыли.
Вы объявляете переменные, чтобы значения стали более запоминающимися и годными для многократного использования. Это делается с помощью ключевого слова let и имени переменной. Если вы хотите инициализировать переменную со значением по умолчанию, поставьте после имени переменной знак равенства (=) и укажите значение, с которым хотите инициализировать эту переменную.
До сих пор весь написанный нами код не имел никакой структуры и был простым до безобразия:
alert("hello, world!");
С таким кодом все в порядке, особенно учитывая, что он состоит из единственной инструкции. Но в реальном мире нам не отделаться так легко и код, написанный на JavaScript под настоящие задачи в жизни, редко будет таким простым.
Предположим, что нужно отобразить расстояние, пройденное объектом (рис. 3.1).
Рис. 3.1. Пройденное расстояние
Как вы помните еще со школы, чтобы вычислить расстояние, нужно скорость объекта умножить на время его движения (рис. 3.2).
расстояние = скорость × время
Рис. 3.2. Вычисление расстояния
Версия этого выражения на JavaScript будет выглядеть примерно так:
let speed = 10;
let time = 5;
alert(speed * time);
У нас есть две переменные, обозначенные как скорость (speed) и время (time), каждая из которых содержит число. Функция alert отображает результат умножения значений, содержащихся в переменных speed и time. Это весьма буквальная интерпретация уравнения для вычисления расстояния, которое мы только что рассмотрели.
Представим, к примеру, что нам нужно рассчитать расстояние при наличии большего числа значений. Опираясь только на те знания, которые мы освоили к этому моменту, мы запишем код вот так:
let speed = 10;
let time = 5;
alert(speed * time);
let speed1 = 85;
let time1 = 1.5;
alert(speed1 * time1);
let speed2 = 12;
let time2 = 9;
alert(speed2 * time2);
let speed3 = 42;
let time3 = 21;
alert(speed3 * time3);
Не знаю, как вам, но по мне — это полная печалька. Наш код слишком многословный и повторяющийся. Мы уже видели ранее при изучении темы переменных, что повторение не позволяет создавать легкий в сопровождении код и отнимает время.
Подобная проблема полностью решаема с помощью того, что мы еще не раз встретим в этой книге, а именно функций:
function showDistance(speed, time) {
alert(speed * time);
}
showDistance(10, 5);
showDistance(85, 1.5);
showDistance(12, 9);
showDistance(42, 21);
Не пугайтесь того, что этот код только что провернул на ваших глазах. Все просто: укороченный фрагмент кода выполнил ту же работу, что и множество строчек из рассмотренного ранее примера, но только без побочных действий. Мы узнаем о функциях все, в том числе как им удается так превосходно справляться со своей работой, и начнем мы прямо сейчас!
Поехали!
На самом базовом уровне функция — не более чем оболочка для некоего кода.
По сути функция:
• группирует инструкции;
• позволяет использовать код многократно.
Вы редко напишете или воспользуетесь кодом, в котором не будет функций, поэтому важно не только ознакомиться с ними, но и разобраться в тонкостях их правильной работы.
Лучший способ понять функции — это погрузиться в них и начать использовать. Поэтому создадим для начала самую простую функцию. Это не особо увлекательный процесс. Требуется лишь понимание некоторых особенностей синтаксиса вроде использования причудливых фигурных скобок и их собратьев.
Ниже приведен пример того, как выглядит простая функция:
function sayHello() {
alert("hello!");
}
Однако простого определения функции еще недостаточно, ее нужно также вызвать путем добавления следующих строк в конце:
function sayHello() {
alert("hello!");
}
sayHello();
Чтобы попрактиковаться, создайте новый HTML-документ (назовите его functions_sayhello.htm):
function sayHello() {
alert("hello!");
}
sayHello();
Если вы наберете этот текст полностью и просмотрите страницу в браузере, то увидите, как отобразится hello!. Это нужно сделать, чтобы убедиться, что наш код работает. Далее разберем, почему этот код сработал, а для этого разобьем функцию sayHello на несколько самостоятельных фрагментов и рассмотрим каждый из них подробнее.
Во-первых, мы видим ключевое слово function (рис. 3.3).
function sayHello() {
alert("hello!");
}
Рис. 3.3. Ключевое слово function
Это ключевое слово сообщает движку JavaScript, который живет в глубинах вашего браузера, что весь этот блок кода нужно рассматривать как связанный с функциями.
После ключевого слова function мы указываем актуальное имя функции и ставим после него открывающую и закрывающую скобки (), как показано на рис. 3.4.
function sayHello() {
alert("hello!");
}
Рис. 3.4. Имя функции и скобки
Завершая процесс объявления функции, нужно поставить открывающую и закрывающую фигурные скобки, внутри которых указываются нужные инструкции (рис. 3.5).
function sayHello() {
alert("hello!");
}
Рис. 3.5. Открывающая и закрывающая фигурные скобки
В заключение рассмотрим содержимое функции, а именно те инструкции, которые задают функциональность (рис. 3.6).
function sayHello() {
alert("hello!");
}
Рис. 3.6. Содержимое функции
В нашем примере содержимым является функция alert, отображающая диалоговое окно со словом hello!.
Последнее, что осталось рассмотреть, — это вызов функции (рис. 3.7).
function sayHello() {
alert("hello!");
}
sayHello();
Рис. 3.7. Вызов функции
Как правило, вызов функции — это имя той функции, которую мы хотим вызвать (как всегда, со скобками в конце). Без вызова функции она ничего не будет делать. Именно с вызовом наша функция просыпается и начинает что-то делать.
Вот мы и рассмотрели простейшую функцию. Далее, с опорой на только что пройденный материал, мы ознакомимся с более жизненными примерами функций.
Предыдущий пример с sayHello слишком прост:
function sayHello() {
alert("hello!");
}
sayHello();
Мы вызываем функцию, и она производит определенные действия. Такое упрощение вполне нормально, потому что все функции работают одинаково. Отличия состоят лишь в особенностях того, как производится вызов функции, откуда в нее поступают данные и т. д. Первая особенность, которую мы рассмотрим, относится к функциям, принимающим аргументы.
Начнем с простого и знакомого примера:
alert("my argument");
Перед нами функция alert. Вероятно, мы уже видели ее пару-тройку (или несколько десятков) раз. Суть в том, что в эту функцию передается так называемый аргумент, который описывает то, что требуется отобразить при вызове. На рис. 3.8 показано, что отобразится на экране, если вызвать функцию alert с аргументом myargument.
Аргумент — это то, что находится между открывающими и закрывающими скобками. Функция alert — лишь одна из множества возможных функций, принимающих аргументы. Многие из функций, которые вы создадите в будущем, будут также принимать аргументы.
В этой главе мы рассматривали еще одну функцию, принимающую аргументы, а именно showDistance:
function showDistance(speed, time) {
alert(speed * time);
}
Рис. 3.8. Отображение аргумента
Понять, что функция принимает аргументы, можно, просто взглянув на ее описание (объявление):
function showDistance(speed, time) {
alert(speed * time);
}
То, что ранее было пустыми скобками после имени функции, теперь содержит информацию о количестве аргументов, необходимых функции, а также подсказывает, какие значения заданы этим аргументам.
В случае с showDistance можно сделать вывод о том, что эта функция принимает два аргумента. Первый из них соответствует speed (скорости), а второй — time (времени).
Мы задаем значения аргументов в рамках вызова функции:
function showDistance (speed, time) {
alert(speed * time);
}
showDistance(10, 5);
В нашем случае мы вызываем функцию showDistance и задаем значения, которые хотим в нее передать, внутри скобок (рис. 3.9).
Поскольку мы передаем больше одного аргумента, то перечисляем значение каждого через запятую. И пока я не забыл, отмечаю еще один важный момент: важен порядок, в котором вы определяете аргументы.
showDistance(10, 5);
Рис. 3.9. Значения, которые мы хотим передать в функцию
Давайте рассмотрим этот процесс подробнее и начнем с диаграммы на рис. 3.10.
Рис. 3.10. Диаграмма вызова функции
Для вызова в функцию showDistance передается 10 как аргумент для speed и 5 — для time. Изображенный на диаграмме перенос полностью основан на последовательности.
Как только передаваемые значения достигают функции, определенные для аргументов имена начинают обрабатываться как имена переменных (рис. 3.11).
Рис. 3.11. Имена аргументов работают как переменные
Мы можем использовать эти имена переменных, чтобы легко и без забот ссылаться на значения, содержащиеся в аргументах внутри функции.
Несоответствие числа аргументов
Если вы не задаете аргументы при вызове или задаете меньше или больше аргументов, чем требуется функции, все по-прежнему может работать. Чтобы избежать подобных ситуаций, вы можете применять защитное программирование, и в дальнейшем мы рассмотрим этот вопрос подробнее.
В целом, чтобы создать код с более четкой структурой, необходимо передавать в функцию соответствующее число аргументов.
Последний вид функции, который мы рассмотрим, — это та функция, которая возвращает результат в ответ на вызов. Вот, что нам нужно сделать. У нас есть функция showDistance, которая, как нам прекрасно известно, выглядит так:
function showDistance(speed, time) {
alert(speed * time);
}
Нам нужно, чтобы наша функция не вычисляла расстояние и отображала его в виде уведомления, а сохраняла полученное значение для дальнейшего использования. Мы хотим получить примерно следующее:
let myDistance = showDistance(10, 5);
Переменная myDistance будет содержать результат вычисления, которое выполнит функция showDistance.
Возврат данных из функции производится посредством ключевого слова return. Давайте создадим новую функцию с именем getDistance, которая будет выглядеть как showDistance, но отличаться в том, что происходит при выполнении функции до завершения:
function getDistance(speed, time) {
let distance = speed * time;
return distance;
}
Обратите внимание, что мы вычисляем расстояние, умножая speed на time. Вместо отображения уведомления (alert) мы возвращаем значение расстояния (которое содержится в переменной distance).
Мы можем выполнить вызов функции getDistance в рамках инициализации переменной:
let myDistance = getDistance(10, 5);
Когда функция getDistance будет вызвана, она выполнит вычисление и вернет численное значение, которое затем будет присвоено переменной myDistance. И всего делов-то!
Как только функция доходит до ключевого слова return, она прекращает выполнять обработку, которую делала до этого момента, возвращает значение, заданное в вызывающей функции (caller), и производит выход:
function getDistance(speed, time) {
let distance = speed * time;
return distance;
if (speed < 0) {
distance *= -1;
}
}
Любой код, прописанный после инструкции return, не будет обработан. Эта часть будет проигнорирована, как если бы ее и не было вовсе.
На практике инструкция return используется для завершения функции после того, как она выполнит нужные нам действия. Эта функция могла бы вернуть значение вызывающей функции, как вы видели в предыдущем примере, или просто произвести выход:
function doSomething() {
let foo = "Nothing interesting";
return;
}
Использовать ключевое слово return для возвращения результата не обязательно. Оно может использоваться отдельно, как мы увидели в примере выше, просто для выхода из функции. Если значение return не задано функцией, возвращается значение по умолчанию, undefined.
КОРОТКО О ГЛАВНОМ
Функции относятся к тому небольшому числу компонентов, которые вы будете использовать практически в каждом приложении JavaScript. Они предоставляют востребованную возможность — создавать переиспользуемый код. Не важно, используете ли вы собственные функции или те, что встроены в JavaScript, — вы просто не сможете обойтись без них.
Все, что вы прочитали на данный момент, является примерами распространенного использования функций. Существуют некоторые продвинутые особенности функций, которые я не затрагивал в этой главе. Эти особенности мы рассмотрим в далеком будущем… очень далеком. Пока что изученного вами материала хватит для углубленного понимания, как использовать функций в реальной жизни.
Если у вас есть вопросы по пройденной теме — добро пожаловать на форум https://forum.kirupa.com, где я или другие смышленые веб-разработчики поможем вам.
Как только вы просыпаетесь, начинается осознанный или неосознанный процесс принятия решений. Выключить будильник. Включить свет. Выглянуть из окна, чтобы проверить погоду. Почистить зубы. Надеть мантию и шляпу волшебника. Посмотреть в календарь. В общем… вы меня поняли. К тому моменту, когда вы шагнете за порог, можно будет насчитать уже сотни принятых вами осознанных или неосознанных решений. Каждое из этих решений определенным образом повлияло на то, что вы в итоге станете делать.
Например, если бы на улице было холодно, вам пришлось бы решать, что надеть: худи или куртку. На рис. 4.1 показано, как формируется это решение.
Рис. 4.1. Моделирование решений
На каждой стадии принятия решения вы задаете себе вопрос, на который можно дать ответ true (верно) или false (неверно). Ответ на этот вопрос определяет ваш следующий шаг и то, что в итоге вы наденете: футболку, худи или куртку. В более широком смысле любое наше решение можно смоделировать в виде череды инструкций true и false. От этого может стать слегка не по себе (еще бы!), но в общем и целом именно так мы, наше окружение и большинство живых существ совершаем свой выбор.
Такое обобщение особенно применимо ко всему, что делает наш компьютер. На примере всех кодов, которые мы успели написать, это не сразу бросается в глаза, но мы скоро исправим эту ситуацию. В этом уроке мы рассмотрим условные выражения. Они являются цифровым эквивалентом решений в ситуациях, где код производит некоторое действие в зависимости от того, оказывается что-либо true или false.
Поехали!
Самая распространенная условная инструкция, используемая в коде, — это инструкция if…else, или просто инструкция if. Принцип ее работы показан на рис. 4.2.
Рис. 4.2. Как работает инструкция if
Чтобы в этом разобраться, рассмотрим инструкцию if…else в действии. Создайте новый HTML-документ и добавьте в него следующие разметку и код:
let safeToProceed = true;
if (safeToProceed) {
alert("You shall pass!");
} else {
alert("You shall not pass!");
}
Сохраните этот документ под именем if_else.htm и выполните его предпросмотр в браузере. Если все сработает как надо, вы увидите уведомление с текстом You shall pass! (рис. 4.3).
Рис. 4.3. Вы увидите это уведомление
За полученный результат отвечают следующие строки кода:
let safeToProceed = true;
if (safeToProceed) {
alert("You shall pass!");
} else {
alert("You shall not pass!");
}
Наше выражение (то, что следует за ключевым словом if и в итоге оценивается как true или false) — это переменная safeToProceed. Эта переменная инициализирована как true, следовательно, был задействован вариант true инструкции if.
Теперь замените значение переменной safeToProceed с true на false:
let safeToProceed = true;
if (safeToProceed) {
alert("You shall pass!");
} else {
alert("You shall not pass!");
}
На этот раз при запуске кода вы увидите уведомление с текстом You shall not pass! так как теперь выражение вычисляется как false (рис. 4.4).
Рис. 4.4. Уведомление, получаемое, когда выражение вычисляется как false
Пока что все это может казаться скучным, но в основном потому, что мы еще не добавляли элемент сложности, чтобы рассмотреть более жизненные сценарии. Это ждет нас на следующем этапе, когда мы глубже погрузимся в эту тему.
В большинстве случаев наше выражение не будет простой переменной со значением true или false, подобно предыдущему примеру. В выражениях будут задействованы так называемые условные операторы, помогающие сравнивать два или больше двух выражений для утверждения результата true или false.
В общих чертах подобные выражения показаны на рис. 4.5.
if (expression operator expression) {
do_something;
} else {
do_something_different;
}
Рис. 4.5. Общий формат выражений с условными операторами
Оператор (то есть условный оператор) определяет связь между выражениями. Конечная цель — вернуть результат true или false, чтобы наша инструкция if понимала, какой блок кода выполнять. Ключом к выполнению всех этих действий являются сами условные операторы, которые приведены в табл. 4.1.
Табл. 4.1. Операторы
Оператор
Если true
= =
Если первое выражение дает результат, равный второму выражению
>=
Если первое выражение дает результат, который больше или равен второму выражению
>
Если первое выражение дает результат больше, чем второе выражение
<=
Если первое выражение дает результат, меньший или равный второму выражению
<
Если первое выражение дает результат меньше, чем второе выражение
!=
Если первое выражение дает результат, не равный второму выражению
&&
Если и первое, и второе выражения дают результат true
| |
Если либо первое, либо второе выражение дает результат true
Теперь перейдем от обобщенного понимания условных операторов к более конкретному, рассмотрев еще один пример, в котором подсвечен интересующий нас if-сегмент кода:
let speedLimit = 55;
function amISpeeding(speed) {
if (speed >= speedLimit) {
alert("Yes. You are speeding.");
} else {
alert("No. You are not speeding. What's wrong with you?");
}
}
amISpeeding(53);
amISpeeding(72);
Разберемся, что же именно здесь происходит. У нас есть переменная speedLimit, инициализированная как 55. Затем есть функция amISpeeding, получающая аргумент speed. Внутри нее инструкция if, чье выражение проверяет, является ли полученное значение speed большим или равным (привет, условный оператор >=) значению, содержащемуся в переменной speedLimit:
function amISpeeding(speed) {
if (speed >= speedLimit) {
alert("Yes. You are speeding.");
} else {
alert("No. You are not speeding. What's wrong with you?");
}
}
Последнее, что делает код, — это вызывает функцию amISpeeding, передавая ей два значения speed:
amISpeeding(53);
amISpeeding(72);
Когда мы называем эту функцию со скоростью 53, выражение speed >= speedLimit вычисляется как false. Так происходит, потому что 53 не больше и не равно значению, сохраненному в speedLimit, а именно 55. В итоге будет выводиться уведомление о том, что вы не превышаете скорость (No. You are not speeding…).
Противоположное происходит, когда мы вызываем amISpeeding со скоростью 72. В этом случае мы превышаем скорость и выражение вычисляется как true с последующим появлением соответствующего уведомления.
Про выражения следует знать, что они могут быть такими сложными или простыми, какими вы их сами сделаете. Они могут состоять из переменных, вызовов функций или одних значений. Они даже могут быть сделаны из сочетаний переменных, вызовов функций или голых значений, разделенных с помощью любых из описанных выше операторов. Важно сделать так, чтобы в итоге выражение вычислялось как true или false.
Привожу чуть более сложный пример:
let xPos = 300;
let yPos = 150;
function sendWarning(x, y) {
if ((x < xPos) && (y < yPos)) {
alert("Adjust the position");
} else {
alert("Things are fine!");
}
}
sendWarning(500, 160);
sendWarning(100, 100);
sendWarning(201, 149);
Обратите внимание на то, как выглядит условие внутри инструкции if, принадлежащей функции sendWarning:
function sendWarning(x, y) {
if ((x < xPos) && (y < yPos)) {
alert("Adjust the position");
} else {
alert("Things are fine!");
}
}
В данном случае было выполнено три сравнения. Во-первых, меньше ли x, чем xPos. Во-вторых, меньше ли y, чем yPos. И в-третьих — проверка, не расценивается ли первая и вторая инструкции как true, чтобы оператор && мог также вернуть это значение. Можно соединять в цепочки множество условных инструкций в зависимости от того, что мы делаем. Помимо запоминания действий самих операторов может быть непросто проследить, чтобы все условия и подусловия были правильно изолированы скобками.
Весь рассматриваемый материал из текущего и предыдущего разделов подпадает под общее определение бинарной логики. Если вы ничего не знаете по этой теме, я рекомендую прочитать прекрасную статью о режимах совместимости.
Мы почти закончили с инструкцией if. Осталось лишь разобраться с ее «родственниками».
Первый вариант — это одиночная инструкция if без родственника else:
if (weight > 5000) {
alert("No free shipping for you!");
}
В этом случае если выражение вычисляется как true, то все отлично. Если как false, тогда код просто пропускает уведомление и переходит к выполнению следующих действий. При работе с инструкциями if блок else является опциональным. Чтобы составить контраст одиночной инструкции if, на помощь спешит ее родня.
Не все можно четко уложить в одиночную инструкцию if или if…else. В таких ситуациях можно использовать ключевое слово else if, чтобы создавать цепочки инструкций if. Не будем вдаваться в подробности, а просто посмотрим следующий пример:
if (position < 100) {
alert("Do something!");
} else if ((position >= 200) && (position < 300)) {
alert("Do something else!");
} else {
alert("Do something even more different!");
}
Если первая инструкция if будет вычислена как true, тогда обработка кода пойдет по ветке первого уведомления. Если первая инструкция окажется false, тогда обработка переходит к вычислению инструкции else if, проверяя, true она или false. Это будет продолжаться, пока код не будет обработан до конца. Другими словами, обработка нашего кода — это движение вниз через все инструкции if и else if, пока одно из выражений не будет вычислено как true:
if (condition) {
…
} else if (condition) {
…
} else if (condition) {
…
} else if (condition) {
…
} else if (condition) {
…
} else if (condition) {
…
} else {
…
}
Если ни одна из инструкций не имеет выражений, вычисляемых как true, то выполняется обработка кода внутри блока else (если таковой существует). Если блока else нет, тогда выполнение просто перейдет к следующей части кода, находящейся за пределами инструкций if. С помощью более сложных выражений и инструкций if…else if вы можете выразить практически любое решение, которое потребуется вычислить в коде.
Теперь вы знаете все, что нужно знать об инструкции if. Пришло время познакомиться с совершенно иным видом условных инструкций…
В мире программирования, наполненном прекрасными инструкциями if, else и else if, потребность в ином виде взаимодействий с условными инструкциями может отсутствовать. Но суровые люди, писавшие код на машинах размером с комнату и не боявшиеся волков в заснеженных горах, не согласились бы с этим. Поэтому теперь у нас есть так называемые инструкции switch. И прямо сейчас мы узнаем, зачем они нужны.
Не будем тянуть кота за хвост и сразу посмотрим на пример. Основа структуры инструкции switch выглядит так:
switch (expression) {
case value1:
statement;
break;
case value2:
statement;
break;
case value3:
statement;
break;
default:
statement;
break;
}
Всегда нужно помнить, что инструкция switch — это условная инструкция, проверяющая, является что-либоtrue или false, и не более того. Это что-либо, в свою очередь, является вариацией того, является ли результат вычисления выражения равным значению case. Чтобы прояснить этот момент, рассмотрим более подходящий пример:
let color = "green";
switch (color) {
case "yellow":
alert("yellow color");
break;
case "red":
alert("red color");
break;
case "blue":
alert("blue color");
break;
case "green":
alert("green color");
break;
case "black":
alert("black color");
break;
default:
alert("no known color specified");
break;
}
Здесь у нас есть переменная color, которой задано значение green:
let color = "green";
Мы также определяем переменную color в качестве выражения в инструкции switch:
switch (color) {
case "yellow":
alert("yellow color");
break;
case "red":
alert("red color");
break;
case "blue":
alert("blue color");
break;
case "green":
alert("green color");
break;
case "black":
alert("black color");
break;
default:
alert("no known color specified");
break;
}
Инструкция switch содержит коллекцию блоков case (случаев). При выполнении кода лишь один из этих блоков станет избранным. Выбор конкретного блока происходит путем сопоставления значения блока case с результатом вычисления выражения. В нашем случае, так как выражение вычисляется со значением green, будет выполнен код внутри блока case с тем же значением green:
switch (color) {
case "yellow":
alert("yellow color");
break;
case "red":
alert("red color");
break;
case "blue":
alert("blue color");
break;
case "green":
alert("green color");
break;
case "black":
alert("black color");
break;
default:
alert("no known color specified");
break;
}
Обратите внимание, что выполняется код, содержащийся только внутри блока case green. Так происходит благодаря ключевому слову break в конце этого блока. Когда выполнение кода достигает break, происходит выход из всего блока switch и код продолжает свое выполнение с участка, расположенного ниже. Если вы не указали ключевое слово break, то код продолжит выполняться внутри блока case green. Разница в том, что затем произойдет переход к следующему блоку case (в нашем примере black) и выполнению его кода. Таким же образом будут выполнены все последующие блоки case, если на пути не попадется другое ключевое слово break.
Таким образом, если вы запустите приведенный выше код, то увидите окно уведомления, как на рис. 4.6.
Рис. 4.6. Окно уведомления
Вы можете менять значение переменной color на другие допустимые значения, чтобы посмотреть, как выполняются другие блоки case. В некоторых случаях ни одно их значений блоков case не будет совпадать с результатом вычисления выражения. В таких ситуациях инструкция switch просто ничего не будет делать. Если вы захотите определить для нее поведение по умолчанию, добавьте блок default:
switch (color) {
case "yellow":
alert("yellow color");
break;
case "red":
alert("red color");
break;
case "blue":
alert("blue color");
break;
case "green":
alert("green color");
break;
case "black":
alert("black color");
break;
default:
alert("no known color specified");
break;
}
Обратите внимание, что блок default выглядит немного иначе, чем другие инструкции case. Фактически в нем просто отсутствует слово case.
Мы видели, что инструкция switch используется для вычисления условий — так же, как и инструкция if…else, на которую мы потратили уйму времени. Учитывая такой финт, давайте изучим этот момент подробнее и рассмотрим, как будет выглядеть инструкция if, если мы буквально переведем ее в инструкцию switch.
Допустим, есть такая инструкция if:
let number = 20;
if (number > 10) {
alert("yes");
} else {
alert("nope");
}
Так как переменная number имеет значение 20, инструкция if будет вычисляться как true. Выглядит достаточно просто. А теперь преобразуем ее в инструкцию switch:
switch (number > 10) {
case true:
alert("yes");
break;
case false:
alert("nope");
break;
}
Обратите внимание, что наше выражение — это number > 10. Значение case для блоков case установлено как true или false. Поскольку number>10 вычисляется как true, выполняется код внутри блока true. Несмотря на то что выражение в этом случае не было таким же простым, как считывание значения цвета из переменной в предыдущем разделе, на наш взгляд, принцип работы инструкции switch не изменился. Выражения могут быть настолько сложными, насколько вы пожелаете. Если они вычисляются во что-то, что может быть сопоставлено со значением блока case, тогда все в шоколаде.
Далее предлагаю рассмотреть чуть более сложный пример. Преобразуем уже рассмотренную инструкцию switch с переменной color в эквивалентные ей инструкции if…else. Первоначальная версия этой инструкции выглядит так:
let color = "green";
switch (color) {
case "yellow":
alert("yellow color");
break;
case "red":
alert("red color");
break;
case "blue":
alert("blue color");
break;
case "green":
alert("green color");
break;
case "black":
alert("black color");
break;
default:
alert("no color specified");
break;
}
Если преобразовать ее в череду инструкций if…else, она станет выглядеть так:
let color = "green";
if (color == "yellow") {
alert("yellow color");
} else if (color == "red") {
alert("red color");
} else if (color == "blue") {
alert("blue color");
} else if (color == "green") {
alert("green color");
} else if (color == "black") {
alert("black color");
} else {
alert("no color specified";
}
Как мы видим, инструкции if…else очень схожи с инструкциями switch, и наоборот. Блок default в этом случае становится блоком else, а связь между выражением и значением case инструкции switch объединена в условии if…else инструкции if…else.
В предыдущем разделе мы увидели, насколько взаимозаменяемы инструкции switch и if…else. При наличии двух схожих способов выполнения каких-либо действий возникает естественное желание понять, когда лучше использовать один, а когда другой. Если кратко, то используйте тот, который вам больше нравится. В интернете много спорят о том, когда и какую из инструкций лучше использовать, но такие споры никогда не приводят к чему-то вразумительному.
Лично я предпочитаю использовать ту инструкцию, которая будет легче читаться. Если посмотреть на предыдущее сравнение инструкций if…else и switch, можно заметить, что при наличии большого количества условий инструкция switch выглядит немного чище. Она однозначно лаконичнее и читабельнее. Но только вам решать, какое количество условий определит ваш выбор использовать ту или иную инструкцию. Для меня обычно это четыре или пять условий.
Инструкция switch лучше работает, когда вы вычисляете выражение и сопоставляете результат со значением. Если вы выполняете более сложные действия с использованием странных условий, проверкой значения и т. д., вероятно, вы предпочтете использовать что-то другое. Кроме того, в ход могут пойти даже не инструкции if…else. Об альтернативах поговорим позже.
Подводя итог, останемся верны прежней рекомендации — использовать то, что нам больше нравится. Если вы являетесь членом команды, имеющей свои предпочтения по написанию кода, то следуйте им. В любом случае, что бы вы ни делали, будьте последовательны. Это облегчит не только вашу жизнь, но и жизнь тех, кто будет работать с вашим кодом. Кстати говоря, лично я никогда не имел дело с ситуациями, в которых мне пришлось бы использовать инструкцию switch. Ваш опыт все же может отличаться от моего.
КОРОТКО О ГЛАВНОМ
Несмотря на то что создание настоящего искусственного интеллекта выходит за рамки этой книги, вы можете писать код, помогающий приложениям принимать решения. Этот код почти всегда будет принимать форму инструкции if…else, в которой вы предоставите браузеру набор доступных для него выборов:
let loginStatus = false;
if (name == "Admin") {
loginStatus = true;
}
Эти выборы основываются на условиях, которые должны быть вычислены как true или false.
В этой главе мы изучили механику работы инструкций if…else и их кузена — инструкции switch. В будущих главах вы увидите, что мы будем часто взаимодействовать с этими инструкциями, как если бы они были нашими старыми друзьями. А к концу книги вы сдружитесь еще больше.
Если у вас есть какие-либо вопросы, касающиеся пройденного материала, не беспокойтесь. Обращайтесь с ними на форуме по адресу: https://forum.kirupa.com. Там вы получите быструю помощь от меня или других добрейших разработчиков.
Иногда при написании программы требуется использовать повторяющиеся действия или выполнять код несколько раз. Например, нам нужно вызвать функцию saySomething десять раз подряд.
Один из способов это сделать — просто перекопировать ее 10 раз и вызвать:
saySomething();
saySomething();
saySomething();
saySomething();
saySomething();
saySomething();
saySomething();
saySomething();
saySomething();
saySomething();
Такой способ сработает, и мы добьемся того, чего хотели… но так делать не стоит. Все же дублирование кода — плохая идея. Если бы нам давали пятак каждый раз, когда вы это прочтете, мы стали бы на четыре или пять монет богаче. #killing_it
Итак, даже если мы решим продублировать код несколько раз вручную, такой подход не сработает на практике. Количество повторений, которые потребуется сделать, будет варьировать в зависимости от таких внешних факторов, как число элементов в коллекции данных, результат, полученный от веб-сервиса, число букв в слове и многих других вещей, которые постоянно будут меняться. Такое количество повторов не будет всегда одинаковым, к примеру 10. Часто нам может потребоваться выполнение ОГРОМНОГО количества повторов. И нам уж точно не захочется перекопировать фрагменты кода сотни или тысячи раз, чтобы получить нужное число повторений. Это было бы ужасно.
Альтернатива alert
В предыдущих главах мы использовали функцию alert для отображения текста на экране. В этой главе мы познакомимся с другим, не столь навязчивым способом отображения. И это будет функция document.write:
document.write("Show this on screen!");
С помощью этой функции можно вывести заданный текст на страницу браузера без использования диалогового окна, которое к тому же нужно постоянно закрывать. Вы поймете, почему мы предпочитаем более упрощенные структуры, когда изучите циклы и то, как выводить на экран большое количество информации.
Нужно универсальное решение для повтора кода с сохранением контроля над тем, сколько раз этот повтор будет произведен. В JavaScript такое решение представлено в виде цикла, который может производиться в трех вариантах:
• циклы for;
• циклы while;
• циклы do…while.
Каждый из вариантов позволяет определить код, который требуется повторить (то есть цикл), а также способ остановить это повторение при соблюдении заданного условия. В следующих разделах мы все это изучим.
Поехали!
Один из наиболее популярных способов создания цикла — это использование инструкции for. Цикл for позволяет повторять выполнение кода до тех пор, пока заданное нами выражение не вернет false. Разберем наглядный пример.
Если преобразовать прошлый пример с saySomething с помощью for, он будет выглядеть так:
for (let i = 0; i < 10; i++) {
saySomething();
}
function saySomething() {
document.writeln("hello!");
}
Если вы хотите сразу испробовать свои силы, попробуйте вписать следующий код в тег script внутри HTML-документа:
for (let i = 0; i < 10; i++) {
saySomething();
}
function saySomething() {
document.writeln("hello!");
}
Когда документ будет готов, сохраните его и выполните предпросмотр в браузере. На рис. 5.1 показано то, что вы увидите после загрузки страницы.
Рис. 5.1. Слово hello! повторяется слева направо
Слово hello! повторится десять раз по всей странице. Это стало возможным благодаря циклу for. Поэтому в виде благодарности за полученный нами опыт изучим все аспекты работы цикла. А вот и наша звезда:
for (let i = 0; i < 10; i++) {
saySomething();
}
Это цикл for, и он существенно отличается от тех инструкций, с которыми мы познакомились к этому моменту. Для понимания отличий представим цикл for в обобщенном виде, как показано на рис. 5.2.
for (start_point; condition; step) {
// код для выполнения
}
Рис. 5.2. Общий вид верхнего уровня цикла
Далее верхний уровень сопоставим с действительными значениями из нашего примера (рис. 5.3).
for (let i = 0; i < 10; i++) {
// код для выполнения
}
Рис. 5.3. Действительные значения
Каждая из этих трех разноцветных секций (стадий) играет свою важную роль в процессе выполнения цикла. Чтобы использовать цикл for грамотно, необходимо понимать, за что отвечает каждая из секций. Рассмотрим каждую подробнее.
В этой секции мы определяем стартовое значение переменной-счетчика нашего цикла. Обычно сюда помещается некий код для объявления и инициализации переменной, подобно тому как мы сделали на рис. 5.4.
for (let i = 0; i < 10; i++) {
// код для выполнения
}
Рис. 5.4. Объявление и инициализация переменной i
Так мы сообщаем JavaScript, что наш цикл начинается с переменной i, инициализированной как 0.
Мы пока пропустим вторую секцию и перейдем к секции шаг (рис. 5.5).
for (let i = 0; i < 10; i++) {
// код для выполнения
}
Рис. 5.5. Шаг
На этой стадии мы определяем, как будет изменяться наше стартовое значение. Например, здесь мы сообщаем, что при каждом выполнении цикла значение i будет увеличиваться на 1. Это обозначается таинственной записью i++. Мы разберемся со значением ++ позже, когда рассмотрим принципы работы чисел и математики в JavaScript, однако иначе это можно было бы выразить как i = i + 1.
Возвращаясь к пропущенной нами секции, мы видим условие, которое определяет, когда закончится повторение цикла (рис. 5.6).
for (let i = 0; i < 10; i++) {
// код для выполнения
}
Рис. 5.6. Часть цикла, представляющая условие
В нашем примере условие гласит, что переменная i должна иметь значение меньше 10:
• если переменная i меньше 10, выражение вычисляется как true и цикл продолжает выполнение;
• если переменная становится равна или больше 10, то условие вычисляется как false и цикл прекращается.
Теперь, когда мы изучили каждую часть цикла for более подробно, воспользуемся свежими знаниями, чтобы еще раз пробежаться по всему процессу от начала до конца и понять, что при этом происходит. Наш пример целиком выглядит так:
for (let i = 0; i < 10; i++) {
saySomething();
}
function saySomething() {
document.writeln("hello!");
}
Когда цикл for впервые достигает стартового значения, переменная i создается и инициализируется как 0. Далее мы переходим к условию, определяющему, следует ли циклу продолжаться или нет. Условие проверяет, является ли значение i меньше 10. 0 меньше 10? Да, следовательно, условие вычисляется как true и код, содержащийся внутри цикла, выполняется. Как только это происходит, наступает черед шага. На этой стадии переменная i увеличивается на 1 и получает значение 1. К этому моменту наш цикл сработал один раз, обычно это называют итерацией. Теперь настало время следующей итерации.
При следующей итерации весь цикл начинается с начала, но переменная i уже не инициализируется, а просто представляет значение 1, перешедшее из предыдущей итерации. Далее в условии вновь проверяется, меньше ли это значение, чем 10, что оказывается верным. После этого выполняются код внутри цикла (в нашем случае функция saySomething) и шаг, увеличивающий значение i на 1. В итоге значение i становится равно уже 2, на чем текущая итерация завершается, уступая место следующей.
В этом процессе итерации сменяют друг друга, пока условие i < 10 не будет вычислено как false. Поскольку мы начали цикл при i, равной 0, определили, что он завершится при i, равном или большем 10, а i увеличивается на 1 при каждой итерации, то этот цикл (и любой содержащийся в нем код) будет выполнен 10 раз до своего завершения.
В предыдущем разделе мы разобрали простой цикл for и описали все его внутренние процессы. Но в отношении таких циклов и вообще всего остального в JavaScript есть один нюанс, а именно простые примеры, как правило, не охватывают все интересующие нас случаи. Лучшим решением будет рассмотреть еще несколько примеров с циклами for, чем мы и займемся в следующих разделах.
Иногда возникает необходимость прервать цикл прежде, чем он завершится. Для этого мы используем ключевое слово break. Ниже приведен пример:
for (let i = 0; i < 100; i++) {
document.writeln(i);
if (i == 45) {
break;
}
}
Если задать i значение 45, ключевое слово break прервет цикл. И хотя я просто взял этот пример из своей головы, отныне, если у вас возникнет необходимость прервать цикл, вы будете вооружены этим знанием.
Кроме того, иногда могут возникать ситуации, когда нужно пропустить текущую итерацию, чтобы перейти к следующей. Ловчее всего это сделать с помощью ключевого слова continue:
let floors = 28;
for (let i = 1; i <= floors; i++) {
if (i == 13) {
// нет такого этажа (floor)
continue;
}
document.writeln("At floor: " + i + "
");
}
В отличие от break, который просто прерывает цикл, continue как бы сообщает ему: «Остановись и переходи к следующей итерации». Чаще всего мы будем использовать ключевое слово continue при обработке ошибок, чтобы цикл переходил к следующему элементу.
Нет никаких причин, по которым стартовое значение должно иметь переменную, инициализированную как 0, и увеличивать ее:
for (let i = 25; i > 0; i-) {
document.writeln("hello");
}
Можно легко начать с большего значения и затем производить его уменьшение, пока условие цикла не вернет false.
Вы могли слышать, что такой подход повышает производительность цикла. Дискуссия на тему того, действительно ли уменьшение быстрее увеличения, ведется до сих пор, но вы вольны экспериментировать и на своем опыте понаблюдать, так ли это.
Необязательно использовать числа при заполнении цикла for:
for (let i = "a"; i!= "aaaaaaaa"; i += "a") {
document.writeln("hmm. ");
}
Вы можете написать все, что захотите, пока это не помешает циклу завершиться. Обратите внимание, что в этом примере в качестве единицы исчисления цикла мы используем букву a. При каждой итерации значение i увеличивается на одну a, а цикл останавливается, когда i становится равна aaaaaaaa.
О, да! Я сделал это! Побывал там, сфотографировал, запостил фотку на фейсбуке и вернулся:
let i = 0;
let yay = true;
for (; yay;) {
if (i == 10) {
yay = false;
} else {
i++;
document.writeln("weird");
}
}
Не обязательно заполнять все три секции цикла for, чтобы он заработал. До тех пор пока вы обеспечиваете выполнение условия завершения цикла, вы можете делать все, что захотите. Прямо как в примере выше.
В тени его превосходительства цикла for живут и другие варианты циклов, а именно while и do…while. Для полного завершения темы давайте рассмотрим и их.
Цикл while повторяет заданный код, пока его условие (другое выражение) не вернет false. Взгляните на следующий пример:
let count = 0;
while (count < 10) {
document.writeln("looping away!");
count++;
}
В этом примере условие представлено выражением count < 10. При каждой итерации цикл увеличивает count на 1:
let count = 0;
while (count < 10) {
document.writeln("looping away!");
count++;
}
Как только count станет равен 10, цикл прекратится, так как выражение count < 10 вернет false. Если вы посмотрите на все, что делает этот цикл, то увидите, что он во многом имитирует работу цикла for. В то время как цикл for требует определения стадий начала, условия и шага, цикл while предполагает, что вы определите все эти стадии по-своему.
А теперь пора познакомиться с Мег Гриффин[1] в семействе циклов. Цель цикла do…while определена еще меньше, чем в случае с while. Если в цикле while условное выражение расположено перед выполнением самого цикла, то в do…while оно находится в конце.
Вот вам пример:
let count = 0;
do {
document.writeln("I don't know what I am doing here!
");
count++;
} while (count < 10);
Главное отличие между циклами while и do…while в том, что содержимое первого не может быть выполнено, если его условное выражение изначально вернет false:
while (false) {
document.writeln("Can't touch this!");
}
В случае же с циклом do…while, из-за того что условное выражение вычисляется только после одной итерации, содержимое цикла будет выполнено минимум один раз:
do {
document.writeln("This code will run once!");
} while (false);
В некоторых ситуациях это может сыграть на руку. Прежде чем подвести итоги, хочу сказать еще кое-что. Инструкции break и continue, которые мы встречали ранее как часть прекрасного цикла for, схожим образом работают внутри циклов while и do…while.
КОРОТКО О ГЛАВНОМ
Итак, вы познакомились с циклами for и способами их использования и параллельно затронули их аналоги while и do…while. Пока что мы не будем часто использовать циклы. По мере погружения в более сложные ситуации вроде сбора данных, элементов DOM, управления текстом и другие процессы мы будем прибегать к их использованию все чаще. Главное — не забывать изученную в этой главе информацию.
Если у вас есть вопросы по пройденному материалу, не стесняйтесь задавать их на форуме https://forum.kirupa.com и вы получите оперативный ответ если не от меня, то от других умнейших и готовых помочь разработчиков.
Кажется, что все, что мы пишем в редакторе кода, предназначено исключительно для браузера:
let xPos = -500;
function boringComputerStuff() {
xPos += 5;
if (xPos > 1000) {
xPos = -500;
}
}
boringComputerStuff();
Но скоро мы убедимся, что это совсем не так. У кода есть и другая аудитория — люди.
Код часто используют или читают другие. В особенности если вы работаете в одной команде с другими разработчиками. Чтобы на выходе код выглядел максимально эффективным, нужно убедиться, что он понятен другим. Это касается и тех, кто работает независимо. Любая прекрасная функция, которая кажется логичной сегодня, может выглядеть полной чушью через неделю.
Есть множество способов решения этой проблемы. Один из лучших — это использование комментариев. В этом коротком разделе мы ответим на вопрос, что такое комментарии, узнаем, как они обозначаются в JavaScript, и рассмотрим правильные способы их использования.
Поехали!
Комментарии — это то, что мы пишем в виде части кода для передачи информации читающим его:
// Это вам за то, что не пригласили меня на день рождения!
let blah = true;
function sweetRevenge() { while (blah) {
// Бесконечные диалоговые окна! Ха-ха-ха!!!!
alert("Hahahaha!");
}
}
sweetRevenge();
В этом примере комментарии отмечены символами // и дают относительно точную информацию о коде, который описывают.
О комментариях важно помнить, что они не выполняются вместе с остальным кодом. JavaScript игнорирует комментарии. Вы ему не нравитесь, и его не волнует, что вы хотите сказать, поэтому даже не парьтесь по поводу синтаксиса, пунктуации и всего того, что важно при написании кода. Комментарии нужны только для понимания действий с отдельными фрагментами кода.
Помимо этого, комментарии служат еще одной цели. Их можно оставлять, чтобы отмечать строки кода, выполнять которые сейчас не нужно:
function insecureLogin(input) {
if (input == "password") {
// let key = Math.random() * 100000;
// processLogin(key);
}
return false;
}
В этом примере две нижеприведенные строки отображаются в редакторе, но не выполняются:
// let key = Math.random() * 100000;
// processLogin(key);
Мы будем часто использовать редактор в качестве черновика, и комментарии — это отличный способ отслеживать шаги, которые мы предпринимали, чтобы код заработал, при этом никак не влияя на работу приложения.
Есть несколько вариантов комментариев в коде. Один из них — это однострочные комментарии, которые обозначаются двумя наклонными чертами // и содержат сообщение. Такой вид комментариев мы уже видели.
Мы можем размещать их в отдельно выделенной строке:
// Возвращает больший из двух аргументов.
function max(a, b) {
if (a > b) {
return a;
} else {
return b;
}
}
Или в одну строку с инструкцией:
let zorb = "Alien"; // Раздражать жителей планеты.
Только вам решать, где именно размещать комментарий. Выбирайте место, которое покажется наиболее подходящим.
Поскольку мне нравится быть заезженной пластинкой, еще раз повторю: комментарии не выполняются вместе с остальной частью приложения. Они видны только вам, мне и, возможно, нашим друзьям. Если последняя фраза вам непонятна, значит, вы, скорее всего, не смотрели одну из величайших комедий нашего века (к/ф «Он, я и его друзья»). В таком случае я настоятельно рекомендую отвлечься от учебы и потратить пару часов на исправление этого упущения.
Комментарии с помощью JSDoc
Когда мы пишем код и знаем, что с ним будут работать другие, наверняка захочется передать информацию наиболее простым способом и избавить от необходимости пересматривать весь исходник. Такой способ есть, и все благодаря инструменту JSDoc, который предполагает несколько иной подход к написанию комментариев:
/**
* Перетасовывает содержимое массива (Array).
* @this {Array}
* @returns {Array} Текущий массив с перетасованным содержимым.
*/
Array.prototype.shuffle = function () {
let input = this;
for (let i = input.length — 1; i >= 0; i-) {
let randomIndex = Math.floor(Math.random() * (i + 1));
let itemAtIndex = input[randomIndex];
input[randomIndex] = input[i];
input[i] = itemAtIndex;
}
return input;
}
Как только вы оставили комментарии к файлу, воспользуйтесь JSDoc для экспорта согласующихся частей комментариев в удобно просматриваемый набор HTML-страниц. Это позволяет тратить больше времени на написание самого кода и в то же время помогает пользователям разобраться в действиях вашего кода и понять, как использовать его отдельные части.
Если вы хотите подробнее узнать об использовании JSDoc, посетите сайт этого проекта https://jsdoc.app/.
Сложность с однострочными комментариями состоит в том, что приходится указывать символы // в начале каждой строки, к которой нужно его оставить. Это может быть очень утомительным, особенно если вы пишите большой комментарий или комментируете большой кусок кода.
Для таких случаев существует другой способ оформления комментариев, а именно с помощью символов /* и */, которые определяют начало и конец. И в результате получаются многострочные комментарии:
/*
let mouseX = 0; let mouseY = 0;
canvas.addEventListener("mousemove", setMousePosition, false);
function setMousePosition(e) {
mouseX = e.clientX;
mouseY = e.clientY;
}
*/
Вместо добавления символов // в каждую строку можно использовать символы /* и */, что сэкономит наши время и нервы.
Мы будем комбинировать однострочные и многострочные комментарии в зависимости от того, что хотим задокументировать. Поэтому нам нужно знать, как использовать оба описанных подхода.
Теперь, когда мы уже имеем достаточное представление о том, что такое комментарии, и знаем несколько способов их написания в JavaScript, давайте поговорим о том, как их правильно использовать, чтобы облегчить чтение кода.
Всегда комментируйте код по ходу его написания. Создание комментариев — ужасно скучное занятие, но оно является важной частью написания кода. Вам и другим будет гораздо проще и быстрее понять код, прочитав комментарий, чем просматривать строка за строкой скучную писанину на JavaScript.
Не откладывайте написание комментариев на потом. Такой подход ведет к прокрастинации, которая будет возрастать при выполнении рутинной работы. Если вы не прокомментируете код по ходу написания, то, скорее всего, этого уже не сделаете, что точно никому не пойдет на пользу.
Используйте обычные слова и поменьше JavaScript. Комментарии — это одно из немногих мест, где при написании программы вы можете свободно пользоваться человеческим языком. Не усложняйте комментарии кодом без необходимости. Выражайтесь ясно, сжато, словами.
Используйте пустые пространства. Вам нужно добиться того, чтобы при просматривании больших блоков кода комментарии выделялись и легко прослеживались. Для этого нужно активнее пользоваться пробелом и клавишами Enter-Return. Взгляните на следующий пример:
function selectInitialState(state) {
let selectContent = document.querySelector("#stateList"); let
stateIndex = null;
/*
Для возвращения в прежнее состояние нужно убедиться, что
мы выбираем его в нашем UI. Это означает, что производится
итерация по каждому состоянию выпадающего списка, пока
не будет найдено совпадение. Когда совпадение найдено,
нужно убедиться, чтобы оно было выбрано.
*/
for (let i = 0; i < selectContent.length; i++) {
let stateInSelect = selectContent.options[i].innerText;
if (stateInSelect == state) {
stateIndex = i;
}
}
selectContent.selectedIndex = stateIndex;
}
Обратите внимание, что наш комментарий грамотно выделен пробелами и его отлично видно в остальной части кода. Если ваши комментарии будут разбросаны где попало и их будет сложно опознать, это сильно замедлит чтение кода как для вас, так и для других.
Не комментируйте очевидные вещи. Если строка кода говорит сама за себя, не стоит тратить время на объяснение ее действий. Это допустимо, только если в ней кроется некое едва уловимое поведение, которое вы хотите обозначить в качестве предупреждения. В остальных же случаях лучше потратить время на комментирование менее понятных участков кода.
Эти рекомендации помогут вам комментировать код значительно эффективнее. Если вы работаете в крупном проекте с другими людьми, могу вас заверить, что в вашей команде уже существует устоявшийся регламент, определяющий правильное написание комментариев. Уделите время ознакомлению с этим регламентом и следуйте ему. В итоге останутся довольными все: как вы, так и ваша команда.
КОРОТКО О ГЛАВНОМ
Комментарии зачастую рассматриваются как вынужденное зло. В конце концов, стали бы вы тратить несколько минут на то, чтобы прокомментировать то, что вы и так прекрасно понимаете, или предпочли бы поработать над очередной «вкусной» частью функциональности? Я предпочитаю относиться к комментариям как к «долгосрочному вложению». Значение и польза комментариев зачастую проявляются не сразу, а становятся очевидными, когда с вашим кодом начинают работать другие люди, а также если вам приходится вновь вернуться к своему коду, а вы уже забыли, что он делает и как работает. Не жертвуйте долгосрочной экономией времени в перспективе ради незначительного ускорения в работе сейчас. Делайте ставки на однострочные (//) и многострочные (/* и */) комментарии уже сегодня, пока еще не слишком поздно.
И если у вас появились какие-либо вопросы по этой теме, обращайтесь за оперативной помощью ко мне и другим разработчикам на форуме https://forum.kirupa.com.
По умолчанию код выполняется синхронно. Проще говоря, это значит, что выполнение инструкции происходит сразу, как только до нее доходит очередь. В этом процессе нет никаких и, если или но. Возможность задержки выполнения кода или откладывания его на потом не характерна для работы JavaScript по умолчанию. Мы видели нечто подобное, когда изучали циклы. Цикл выполняется со скоростью света без малейших задержек между итерациями. Это в значительной степени способствует скоростным вычислениям, но в то же время мешает, если мы хотим производить обновление в более размеренном (то есть медленнее) темпе.
Однако это вовсе не значит, что нет способа препятствовать мгновенному выполнению кода. Если немного отклониться от основной темы, можно обнаружить три функции, позволяющие это делать, — setTimeout, setInterval и requestAnimationFrame. В этом разделе мы рассмотрим назначение и применение каждой из них.
Поехали!
Функция setTimeout позволяет откладывать выполнение заданного кода. Вариант ее использования достаточно интересен. С помощью этой функции мы можем обозначить, какой код выполнять и сколько миллисекунд должно пройти, прежде чем это выполнение произойдет. На практике она выглядит примерно так:
let timeoutID = setTimeout(someFunction, delayInMilliseconds);
Немного углубимся в этот пример. Допустим, нужно, чтобы функция showAlert была вызвана через 5 секунд. В этом случае объявление функции setTimeout будет выглядеть так:
function showAlert() {
alert("moo!");
}
let timeoutID = setTimeout(showAlert, 5000);
Круто, не правда ли? Теперь поговорим о чем-то менее интересном, что позволит внести большую ясность в эту тему. Это что-то связано с переменной timeoutID, инициализированной как функция setTimeout. Эта переменная появилась не случайно. Если понадобится вновь обратиться к таймеру setTimeout, то потребуется способ, как сослаться на него. Самый легкий способ — это ассоциирование переменной с объявлением setTimeout.
Вы можете поинтересоваться, а зачем вообще повторно обращаться к таймеру? Что ж, единственная причина, которая приходит на ум, — для его отмены. Можно легко отменить функцию setTimeout с помощью функции clearTimeout, в которую в качестве аргумента нужно передать ID таймаута (timeoutID):
clearTimeout(timeoutID);
Если вы вообще не планируете отменять установленный таймер, можете использовать setTimeout напрямую, не прибегая к инициализации переменной.
Давайте поговорим о том, когда это применяется на практике, а именно при разработке UI (пользовательского интерфейса). При его разработке откладывание некоторых действий на определенное время достаточно распространено.
Например, когда меню разворачивается и сворачивается обратно через несколько секунд, если пользователь с ним не взаимодействует.
Или в случае, когда есть некая операция, которая выполняется слишком долго и не может завершиться, а функция setTimeout прерывает эту операцию и возвращает контроль пользователю.
Мой любимый пример (которому я также посвятил целый урок) — это использование функции setTimeout, чтобы определить, активен ли пользователь или отсутствует.
Если вы поищите информацию о setTimeout на указанном сайте или в Google, то найдете множество реальных примеров, где она может очень пригодиться.
Следующая функция-таймер, которую мы рассмотрим, — это setInterval. Она похожа на setTimeout в том, что позволяет выполнять код спустя определенное время. Отличие же ее в том, что она не просто выполняет код один раз, а продолжает его повторное выполнение бесконечно.
Вот как она используется:
let intervalID = setInterval(someFunction, delayInMilliseconds);
За исключением имени, способ использования функции setInterval идентичен способу применения setTimeout. Первый аргумент определяет встроенный код или функцию, которую нужно выполнить. Второй аргумент определяет время задержки (интервал) между выполнениями. При желании вы также можете инициализировать функцию setInterval как переменную для хранения ID интервала, который позднее сможете использовать, например, для прекращения выполнения.
Отлично! Теперь, когда вы уже знакомы с процессом, рассмотрите пример кода, выполняющего функцию drawText с интервалом между циклами в 2 секунды:
let thingToPrint = "";
function drawText() {
thingToPrint += "#";
document.writeln(thingToPrint);
}
setInterval(drawText, 2000);
Для прекращения выполнения цикла мы можем использовать функцию clearInterval с соответствующим аргументом:
clearInterval(intervalID);
Используется она в точности как clearTimeout — мы передаем ей ID таймера setInterval, который при желании можем задать в момент его создания.
В реальной жизни функция setInterval долгое время оставалась основной функцией для создания анимации в JavaScript. Например, если потребуется получить 30 или 60 кадров в секунду, вы будете «играть» со значением задержки времени примерно так:
// 1000 разделить на 60 — это значение в миллисекундах для 60fps
setInterval(moveCircles, 1000 / 60);
Чтобы увидеть работу setInterval и в других реалистичных примерах на самом сайте, загляните в конец статьи «Создание симпатичного слайдера контента» (https://www.kirupa.com/html5/creating_a_sweet_content_slider.htm) или прочтите материал «Создание аналоговых часов» (https://www.kirupa.com/html5/create_an_analog_clock_using_the_canvas.htm). И там и там вы найдете достаточно яркие примеры использования setInterval.
Настало время перейти к моей любимой функции: requestAnimatonFrame. Эта функция создана для синхронизации кода с событием перерисовки содержимого браузера. Поясняю подробнее: в любой момент браузер занят обработкой миллиона различных элементов. Среди них: манипуляции с макетом, реакция на прокрутку страниц, прослушивание кликов мыши, отображение результатов нажатий клавиатуры, исполнение кода, загрузки ресурсов и т. д. Одновременно со всем этим он также перерисовывает экран со скоростью 60 кадров в секунду, ну или пытается это делать.
Если у вас есть код, запускающий какую-либо анимацию на экране, скорее всего, вам захочется, чтобы эта анимация выполнялась корректно и не затерялась в остальной мешанине действий, выполняемых браузером. В таких случаях использование ранее отмеченной техники setInterval не гарантирует, что скорость обновления не упадет, когда браузер будет занят оптимизацией других элементов. Чтобы браузер не приравнивал код, отвечающий за анимацию, любому другому JavaScript-коду, существует функция requestAnimationFrame. Браузеры воспринимают эту функцию особым образом, что позволяет задавать время ее выполнения наиболее оптимально, избегая провала частоты кадров, а также ненужных действий и прочих побочных эффектов, мешающих применению других решений, связанных с выполнением циклов.
Запуск этой функции почти такой же, как в случае с setTimeout / setInterval:
let requestID = requestAnimationFrame(someFunction);
Единственное заметное отличие состоит в том, что мы не задаем значение продолжительности. Это значение вычисляется автоматически в зависимости от: текущей скорости обновления кадров, активна ли текущая вкладка или нет, работает ли устройство от батареи или сети и многих других факторов, выходящих далеко за пределы нашего контроля и понимания.
Тем не менее подобное использование функции requestAnimationFrame — лишь учебный пример. В реальности вам вряд ли придется вызывать requestAnimationFrame всего лишь раз, как в нашем примере. Ключом ко всем анимациям, создаваемым в JavaScript, является анимационный цикл, и именно к этому циклу нам нужно применить requestAnimationFrame. Результат такого применения выглядит следующим образом:
function animationLoop() {
// код, отвечающий за анимацию
requestAnimationFrame(animationLoop)
}
// Начать выполнение цикла анимации!
animationLoop();
Обратите внимание, что наша функция requestAnimationFrame определяет выполнение вызова функции animationLoop каждый раз, когда браузер производит обновление. Выглядит это так, как будто функция requestAnaimationFrame вызывает animationLoop напрямую, что не совсем так. И это вовсе не ошибка в коде. Несмотря на то что такой вид циклической зависимости на практике гарантированно привел бы к зависанию браузера, реализация requestAnimationFrame этого избегает. Наоборот, она обеспечивает вызов animationLoop именно такое число раз, чтобы гарантированно отобразить плавную и текущую анимацию. При этом не возникает проблем с функционированием остальных приложений.
Чтобы узнать подробнее о функции requestAnimationFrame и ее основном использовании для создания крутых анимаций, ознакомьтесь со статьей «Анимации в JavaScript» (https://www.kirupa.com/html5/animating_in_code_using_javascript.htm). В этом разделе также более глубоко освещена тема использования requestAnimationFrame и рассмотрены аспекты, не затронутые в текущей главе.
КОРОТКО О ГЛАВНОМ
Если вы считаете, что таймеры применяются в более узкой сфере, чем такие важные элементы, как инструкции if…else и циклы, пройденные нами ранее, то вы, скорее всего, правы. Вы можете создать множество прекрасных приложений, не прибегая к использованию setTimeout, setInterval или requestAnimationFrame. Тем не менее это не отменяет того, что знать их обязательно. Однажды наступит тот час, когда вам потребуется отложить выполнения кода, продолжительное выполнение цикла или создать приятную анимацию с помощью JavaScript. И когда этот момент наступит, вы будете готовы! Ну или по крайней мере будете знать, что гуглить.
Чтобы увидеть, как применяются эти функции-таймеры в рабочей среде, ознакомьтесь с приведенными ниже статьями и примерами, которые помогут лучше усвоить материал:
• Создание анимаций с помощью requestAnimationFrame (http://bit.ly/kirupaAnimationsJS).
• Создание симпатичного слайдера контента (http://bit.ly/sliderTutorial).
• Создание аналоговых часов (http://bit.ly/kirupaAnalogClock).
• Генератор припадка (http://bit.ly/kirupaSeizureGenerator).(Спойлер: анимация на этом сайте бешено мерцает.)
Я уже неоднократно отмечал, что JavaScript может вызвать ступор, в особенности когда дело доходит до таймеров. Если у вас возникнут сложности, я и другие опытные разработчики с радостью вам поможем. Обращайтесь на форум https://forum.kirupa.com, и мы выведем вас из ступора.
Давайте вспомним материал о переменных, пройденный несколько глав назад. Каждая объявленная нами переменная имеет конкретный уровень видимости, который определяет то, когда мы можем ее использовать. Выражаясь человеческим языком, это значит, что простое объявление переменной еще не дает нам возможности вызывать ее из любой части кода. Поэтому нужно понять несколько простых аспектов области видимости переменных.
В этом уроке я объясню области видимости переменных на примере распространенных случаев, большинство из которых мы уже встречали. Вообще, это достаточно обширная тема, но мы пробежимся по верхам. Время от времени эта тема будет всплывать в последующих уроках, так что мы сможем пополнить знания, полученные здесь.
Поехали!
Мы начнем с изучения самой обширной области, которая известна как глобальная область видимости. В реальной жизни, когда мы говорим, что что-то может быть услышано глобально, то имеем в виду, что услышим это «что-либо», находясь в любой точке мира.
В JavaScript все работает аналогично. Например, если мы говорим, что переменная доступна глобально, то имеем в виду, что у любого кода на нашей странице есть доступ для ее считывания или изменения. Если мы хотим что-либо определить глобально, то делаем это в коде, находящемся исключительно за пределами какой-либо функции.
Рассмотрим пример:
let counter = 0;
alert(counter);
Здесь мы просто объявляем переменную counter и инициализируем ее как 0. Поскольку эта переменная объявлена напрямую в теге script и не помещена в функцию, она считается глобальной. Это означает, что к этой переменной counter может обратиться любой код, находящийся в документе.
В следующем примере выделен этот аспект:
let counter = 0;
function returnCount() {
return counter;
}
alert(returnCount());
Мы видим, что переменная counter объявлена вне функции returnCount. Но, несмотря на это, у функции есть полный доступ к ней. При выполнении кода функция alert вызывает функцию returnCount, которая возвращает значение переменной counter.
Все это время мы использовали глобальные переменные, не придавая этому значения. В данной ситуации я всего лишь официально представил вам гостя, который тусуется на вашей вечеринке уже какое-то время.
Становится еще интереснее, когда мы смотрим на элементы, не объявленные глобально. С этого момента понимание, что такое области видимости, начинает приносить свои плоды. Как мы видели ранее, к переменной, объявленной глобально, внутри функции также есть доступ:
let counter = 0;
function returnCount() {
return counter;
}
Но не наоборот. Переменная, объявленная внутри функции, не будет доступна извне:
function setState() {
let state = "on";
}
setState();
alert(state) // undefined
В этом примере переменная state объявлена внутри функции setState и обращение к ней извне не работает. Причина в том, что область видимости переменной state является локальной относительно самой функции setState. В более общей форме это можно описать, сказав, что переменная stateлокальна.
Использование переменных без объявления
Если мы инициализируем переменную state, не объявляя ее формально, область ее видимости будет определяться совсем иначе:
function setState() {
state = "on";
}
setState(); alert(state) // "on"
Хотя в этом случае переменная state и появляется внутри функции setState, сам факт того, что мы объявили ее без let, const (или var — устаревший способ объявления переменных), делает ее глобальной. По большому счету, вам не понадобится объявлять переменные подобным образом. Поэтому всегда используйте let или const.
Поскольку сейчас мы разбираем JavaScript, было бы слишком просто останавливаться на достигнутом понимании областей видимости. В последующих разделах я обозначу некоторые важные особенности.
Наш код состоит из блоков. Множества блоков! И вообще, что такое блок? Блок — это набор инструкций JavaScript, почти всегда заключенный в фигурные скобки. Например, посмотрим на следующий код:
let safeToProceed = false;
function isItSafe() {
if (safeToProceed) {
alert("You shall pass!");
} else {
alert("You shall not pass!");
}
}
isItSafe();
Если посчитать количество пар фигурных скобок, станет ясно, что здесь три блока. Один из них — это область, содержащаяся в самой функции isItSafe:
let safeToProceed = false;
function isItSafe()
if (safeToProceed) {
alert("You shall pass!");
} else {
alert("You shall not pass!");
}
}
isItSafe();
Второй блок — это область инструкции if:
let safeToProceed = false;
function isItSafe() {
if (safeToProceed) {
alert("You shall pass!");
} else {
alert("You shall not pass!");
}
}
Третий блок — это область, охваченная инструкцией else:
let safeToProceed = false;
function isItSafe() {
if (safeToProceed) {
alert("You shall pass!");
} else {
alert("You shall not pass!");
}
}
Любая переменная, объявленная внутри блока через let или const, является локальной для этого блока и всех содержащихся в нем дочерних блоков. Чтобы лучше это понять, посмотрите на следующий код — вариацию функции isItSafe:
function isThePriceRight(cost) {
let total = cost + 1;
if (total > 3) {
alert(total);
} else {
alert("Not enough!");
}
}
isThePriceRight(4);
Мы объявляем переменную total как часть блока функции, затем обращаемся к ней внутри блока if. Как вы думаете, к чему это приведет? Переменная total полностью (ха-ха!) доступна отсюда, так как блок if является дочерним блоком блока функции. Выражаясь корректно, скажем, что переменная total находится в области функции alert.
А что насчет следующей ситуации?
function isThePriceRight(cost) {
let total = cost + 1;
if (total > 3) {
let warning = true;
alert(total);
} else {
alert("Not enough!");
}
alert(warning);
}
isThePriceRight(4);
У нас есть переменная warning, объявленная внутри блока if, а еще — функция alert, которая пытается вывести значение warning. В этом случае, так как мы пытаемся обратиться к переменной warning в блоке, находящемся вне блока, в котором она была объявлена, функция alert не будет отображать значение true. Учитывая то, где находится функция alert, переменная warning находится вне области видимости.
объявление переменных с ключевым словом var
Несколько абзацев выше я упомянул вскользь, что переменные когда-то объявлялись с помощью ключевого слова var. Ключевые слова let (и const) были введены позднее для облегчения объявления переменных. Если вы до сих пор используете var, пора переходить на let. Мы еще не обсуждали, почему let — более предпочтительный вариант, и решили, что обсудим это позже, когда начнем рассматривать области переменных подробнее. Что ж, настал этот момент!
Переменные, объявленные с var, распространяются на область функции. Они не распространяются на блоки, подобные инструкциям if и else. Если мы изменим последний пример, объявив в нем переменную warning с var вместо let, то код будет выглядеть так:
function isThePriceRight(cost) {
let total = cost + 1;
if (total > 3) {
var warning = true;
alert(total);
} else {
alert("Not enough!");
}
alert(warning);
}
isThePriceRight(4);
В изначальном варианте этого кода функция alert в отношении переменной warning ничего бы не отобразила, так как эта переменная, будучи объявленной с let, оказывалась вне области видимости. При использовании же var ситуация меняется и вы увидите отображение true. В этом и есть отличие между var и let. Область видимости переменных, объявленных с var, ограничивается уровнем функций, поэтому если переменная объявляется в любом месте внутри функции, то считается, что она находится в области видимости. Область же видимости переменных, объявленных с let, как мы увидели ранее, определяется уровнем блока.
Степень свободы, с которой переменная var определяет область видимости, слишком большая, и поэтому легко допустить ошибку, связанную с переменными. По этой причине я ратую за использование let, когда дело доходит до объявления переменных.
Если вам показалось, что логика областей блока из предыдущего раздела выглядит странно, значит, вы еще не видели следующий пример. Взгляните на этот код:
let foo = "Hello!";
alert(foo);
Глядя на него, мы можем обоснованно утверждать, что отобразится значение Hello! и в целом будем правы. А что, если переместить объявление и инициализацию переменной в конец?
alert(foo);
let foo = "Hello!";
В этом случае код выдаст ошибку. Доступ к переменной foo осуществляется без обращения к ней. А теперь давайте заменим let на var, получив следующее:
alert(foo);
var foo = "Hello!";
При выполнении этого варианта кода его поведение будет отличаться от предыдущего и вы увидите отображение undefined. Что конкретно здесь происходит?
Когда JavaScript при выполнении достигает определенной области (глобальной, функции и т. д.), то первое, что он делает, — полностью сканирует тело кода в поиске объявленных переменных. Если он встречает переменные, объявленные с var, то по умолчанию инициализирует их как undefined. Если же они объявлены с let или const, он оставляет их полностьюнеинициализированными. При завершении он перемещает все встреченные переменные в верхнюю часть соответствующей им области, которой в случае с let и const является ближайший блок, а в случае с var — ближайшая функция.
Давайте подробнее рассмотрим, что это значит. Изначально наш код выглядит так:
alert(foo);
let foo = "Hello!";
Когда JavaScript приступает к его обработке, код принимает следующий вид:
let foo;
alert(foo);
foo = "Hello!";
Несмотря на то что переменная foo была объявлена в нижней части кода, она смещается вверх. Формально это называется поднятием переменной. Особенность let и const в том, что при поднятии переменные остаются неинициализированными. Если вы попробуете обратиться к неинициализированной переменной, то код выдаст ошибку и прекратит выполнение. Если мы изменим предыдущий пример, используя var, то в итоге для JavaScript код будет выглядеть так:
var foo = undefined;
alert(foo);
foo = "Hello!";
Переменная по-прежнему поднимается, но при этом инициализируется как undefined, благодаря чему код продолжает выполнение.
Главная мысль из всего этого: пожалуйста, объявляйте и инициализируйте переменные прежде, чем их использовать. Несмотря на то что JavaScript способна в некоторой степени справиться с работой в тех случаях, когда мы пренебрегаем данными действиями, это лишь добавит еще больше путаницы.
Ни одно обсуждение области переменных не будет полным, если не рассмотреть замыкания. Сейчас я не стану объяснять эту тему, прибережем ее для главы 9.
Прежде чем вы продолжите читать дальше, убедитесь, что весь пройденный материал вам понятен. Если у вас есть какие-то вопросы, обращайтесь на форум https://forum.kirupa.com, где я и другие разработчики с радостью помогут вам.
КОРОТКО О ГЛАВНОМ
Место размещения переменных в коде имеет огромное влияние на то, где они могут использоваться. Переменные, объявленные глобально, доступны по всему приложению. Переменные, объявленные локально, — только внутри области, в которой расположены. Внутри диапазона глобальных и локальных переменных у JavaScript есть огромное количество действий в запасе.
Эта глава представила обзор аспектов влияния области переменных на ваш код. В ближайшем будущем вам предстоит встретиться с некоторыми наиболее актуальными из этих аспектов.
Вероятно, что к этому моменту вы уже знаете все о функциях и обо всех их забавных особенностях. Важная часть работы с функциями, JavaScript и (возможно) жизнью в целом — это понимание замыканий. На рис. 9.1 замыкание обозначено серой областью, в которой пересекаются области функций и переменных.
Рис. 9.1. Замыкания
Больше ничего не буду говорить о замыканиях — лучше всего за них скажет сам код. Как бы я ни пытался сейчас их описать, это только еще больше вас запутает. В последующих разделах мы начнем со знакомого нам материала и будем постепенно продвигаться вглубь вражеской территории, населенной замыканиями.
Поехали!
Первое, что мы сделаем, — проясним, что происходит, когда вы используете функции внутри функций, причем внутренняя функция возвращается. Для начала рассмотрим короткий пример.
Взгляните на этот код:
function calculateRectangleArea(length, width) {
return length * width;
}
let roomArea = calculateRectangleArea(10, 10);
alert(roomArea);
Функция calculateRectangleArea получает два аргумента и возвращает результат их умножения туда, откуда пришел вызов. В данном примере роль стороны, направившей вызов, играет переменная roomArea.
После выполнения этого кода переменная roomArea содержит результат умножения 10 на 10, равный 100 (рис. 9.2).
Рис. 9.2. Результат roomArea
Как вы знаете, функция может вернуть практически что угодно. В данном случае мы вернули число. Вы так же легко можете вернуть текст (то есть строку), значение undefined, пользовательский объект и т. д. До тех пор пока код, вызывающий функцию, знает, как поступить с возвращаемым ей результатом, вы можете делать практически все, что пожелаете. Вы даже можете вернуть другую функцию. На этом моменте остановимся поподробнее.
Ниже представлен простейший пример того, что я имею в виду:
function youSayGoodBye() {
alert("Good Bye!");
function andISayHello() {
alert("Hello!");
}
return andISayHello;
}
У нас могут быть функции, содержащие в себе другие функции. В данном примере есть функция youSayGoodbye, которая содержит alert и функцию andISayHello (рис. 9.3).
Рис. 9.3. Функция внутри функции
В этом примере интересно то, что возвращает функция youSayGoodbye, когда ее вызывают. А возвращает она функцию andISayHello:
function youSayGoodBye() {
alert("Good Bye!");
function andISayHello() {
alert("Hello!");
}
return andISayHello;
}
Попрактикуемся на таком примере. Для вызова функции инициализируем переменную, указывающую на youSayGoodBye:
let something = youSayGoodBye();
В момент выполнения этой строки кода будет также выполнен весь код внутри функции youSayGoodBye. Это значит, что вы увидите диалоговое окно (благодаря alert), говорящее нам Good Bye! (рис. 9.4).
Рис. 9.4. Диалоговое окно Good Bye!
Как часть процесса выполнения до завершения функция andISayHello будет также создана и затем возвращена. В этот момент наша переменная — something, способная обратиться только к одному элементу, а именно к функции andISayHello (рис. 9.5).
Рис. 9.5. Переменная something и функция andISayHello
С точки зрения переменной something внешняя функция youSayGoodBye просто исчезает. Так как теперь переменная something указывает на функцию, вы можете активировать эту функцию, вызвав ее, как обычно, с помощью открывающих и закрывающих скобок:
let something = youSayGoodBye();
something();
Когда вы это сделаете, произойдет выполнение внутренней возвращенной функции (то есть andISayHello). Как и раньше, ждите появления диалогового окна, но теперь оно скажет Hello! (рис. 9.6), что определено в alert внутри этой функции.
Рис. 9.6. Hello!
Все это скорее похоже на обзор. Единственное новое, о чем вы могли узнать, — это то, что как только функция возвращает значение, она уходит из поля действия. Остается только возвращенное значение.
Хорошо. А теперь, как я и обещал, мы подобрались к границе вражеской территории. В следующем разделе дополним только что пройденный материал, рассмотрев схожий пример, но с некоторой особенностью.
В предыдущем примере внутренняя функция andISayHello была самостоятельной и не опиралась ни на какие переменные или состояние внешней функции:
function youSayGoodBye() {
alert("Good Bye!");
function andISayHello() {
alert("Hello!");
}
return andISayHello;
}
На практике мы будем сталкиваться с подобной ситуацией очень редко. Зачастую у нас будут переменные и данные, используемые совместно как внешней, так и внутренней функцией. Для наглядности посмотрим на такой пример:
function stopWatch() {
let startTime = Date.now();
function getDelay() {
let elapsedTime = Date.now() — startTime;
alert(elapsedTime);
}
return getDelay;
}
Здесь показан очень простой способ измерения времени, необходимого для какого-либо действия. Внутри функции stopWatch мы видим переменную, для которой установлено значение Date.now():
function stopWatch() {
let startTime = Date.now();
function getDelay() {
let elapsedTime = Date.now() — startTime;
alert(elapsedTime);
}
return getDelay;
}
У нас также есть внутренняя функция getDelay:
function stopWatch() {
let startTime = Date.now();
function getDelay() {
let elapsedTime = Date.now() — startTime;
alert(elapsedTime);
}
return getDelay;
}
Функция getDelay отображает диалоговое окно, содержащее разницу во времени между новым вызовом Date.now() и ранее объявленной переменной startTime.
Что касается функции stopWatch, то последнее, что она делает перед завершением, — это возврат функции getDelay. Как мы можем увидеть, этот код очень похож на код из предыдущего примера. У нас есть внешняя и внутренняя функции, а также внешняя функция, возвращающая внутреннюю.
Теперь, чтобы увидеть stopWatch в действии, добавьте следующие строки кода:
let timer = stopWatch();
// Сделать что-нибудь за некоторое время.
for (let i = 0; i < 1000000; i++) {
let foo = Math.random() * 10000;
}
// Вызвать возвращаемую функцию.
timer();
Полностью разметка и код выглядят так:
function stopWatch() {
var startTime = Date.now();
function getDelay() {
var elapsedTime = Date.now() — startTime;
alert(elapsedTime);
}
return getDelay;
}
let timer = stopWatch();
// Сделать что-нибудь за некоторое время.
for (let i = 0; i < 1000000; i++) {
let foo = Math.random() * 10000;
}
// Вызвать возвращаемую функцию.
timer();
Если вы запустите этот код, то увидите диалоговое окно, отображающее, сколько миллисекунд прошло между инициализацией переменной timer, выполнением цикла for до завершения и вызовом переменной timer в качестве функции (рис. 9.7).
Рис. 9.7. Переменная timer, вызванная в качестве функции
Если объяснить по-другому, то мы вызываем функцию stopWatch, затем выполняем длительную операцию и вызываем эту функцию повторно, чтобы узнать продолжительность этой длительной операции.
Теперь, когда мы видим, что наш пример работает, вернемся к функции stopWatch и посмотрим, что именно происходит. Как я уже отмечал чуть выше, многое из того, что мы видим, схоже с примером youSayGoodBye / andISayHello. Но есть одна особенность, которая привносит отличие в текущий пример, и важно обратить внимание на то, что происходит, когда функция getDelay возвращается в переменную timer.
На рис. 9.8 мы видим незавершенную визуализацию этого процесса:
Рис. 9.8. Внешняя функция stopWatch больше не действует, и переменная timer становится привязанной к функции getDelay
Внешняя функция stopWatch вышла из игры, и переменная timer стала привязанной к функции getDelay. А теперь укажем на эту особенность. Функция getDelay опирается на переменную startTime, существующую в контексте внешней функции stopWatch:
function stopWatch() {
let startTime = Date.now();
function getDelay() {
let elapsedTime = Date.now() — startTime;
alert(elapsedTime);
}
return getDelay;
}
Когда внешняя функция stopWatch перестает действовать и getDelay возвращается в переменную timer, что происходит на следующей строке?
function getDelay() {
let elapsedTime = Date.now() — startTime;
alert(elapsedTime);
}
В текущем контексте показалось бы логичным, если бы переменная startTime не была определена, верно? Но пример сработал, а значит, дело в чем-то еще, а именно в скромном и загадочном замыкании. Теперь остается пояснить, что должно произойти, чтобы переменная startTime сохранила значение, а не оставалась неопределенной.
Рабочая среда JavaScript, отслеживающая все переменные, использование памяти, ссылок и т. д., действительно умна. В нашем примере ею обнаружено, что внутренняя функция (getDelay) опирается на переменные из внешней функции (stopWatch). Когда это происходит, рабочая среда обеспечивает, чтобы любые нужные переменные из внешней функции были доступны внутренней функции, даже если внешняя функция перестает действовать.
Для наглядности посмотрим, как выглядит переменная timer, на рис. 9.9.
Она по-прежнему ссылается на функцию getDelay, но getDelay при этом также имеет доступ к переменной startTime, которая существовала во внешней функции stopWatch. Так как внутренняя функция замкнула связанные с ней переменные внешней функции в своей области, мы называем ее замыканием (рис. 9.10).
Рис. 9.9. Переменная timer
Рис. 9.10. Схематичное изображение замыкания
Формально замыкание можно определить как вновь созданную функцию, которая также содержит свой переменный контекст (рис. 9.11).
В нашем примере это описано так: переменная startTime получает значение Date.now в момент инициализации переменной timer и начинает выполнение функции stopWatch. Затем функция stopWatch возвращает внутреннюю функцию getDelay и прекращает действие, оставляя при этом те из своих переменных, на которые опирается внутренняя функция. Внутренняя же функция, в свою очередь, замыкает эти переменные.
Рис. 9.11. Более формальное определение замыкания
КОРОТКО О ГЛАВНОМ
Разбор замыканий на примерах позволил обойтись без множества скучных определений, теорий и жестикуляций. На самом деле замыкания — обычная для JavaScript тема. Вы будете иметь с ними дело в любых мудреных и менее сложных ситуациях.
Если из всего этого нужно было бы запомнить что-то одно, то вот оно: самое важное, что делают замыкания, — это позволяют функциям работать, даже когда их среда существенно изменяется или исчезает. Любые переменные, находившиеся в области при создании функции, замыкаются и защищаются, чтобы обеспечить продолжение работы функции. Подобное поведение очень важно для таких динамических языков, как JavaScript, где вам часто приходится создавать, изменять и уничтожать что-либо на ходу. Удачи!
В этой главе мы затронули много тем. Если у вас есть какие-либо вопросы касательно пройденного, пожалуйста, пишите мне на форуме https://forum.kirupa.com, и вы получите ответ в кратчайшие сроки.
Ну что ж, немного отвлечемся от программирования по расписанию (ха!). Пока что весь написанный нами код в полном объеме «жил» внутри HTML-документа:
body {
background-color: #EEE;
}
h1 {
font-family: sans-serif;
font-size: 36px;
}
p {
font-family: sans-serif;
}
Are you ready for seeing (or already having seen!) the most
amazing dialog ever?
alert("hello, world!");
Попробуем отмотать немного назад и поразмыслить вот над чем: целесообразно ли использовать элементы HTML, CSS и JS в одном документе? Для простоты нашего толкования структуры документа попробуем прибегнуть к творческому подходу и изменить представление кода, например отобразив его в виде таких вот чудных рамок (рис. 10.1):
Рис. 10.1. Наше представление веб-страницы
В примере ниже HTML-документ отделен от JavaScript лишь парой тегов script. Но на самом деле JavaScript-код не обязательно селить внутри HTML-документа. Его можно написать в отдельном файле (рис. 10.2).
При этом подходе мы больше не пишем JavaScript-код в HTML-документе. У нас по-прежнему есть тег script, но он лишь указывает на файл JavaScript, а не содержит все строки его кода.
Рис. 10.2. Теперь код JS живет в своем собственном файле!
Стоит заметить, что оба подхода не исключают друг друга, то есть мы можем совмещать их в документе HTML, применяя гибридный подход, при котором у нас будут как внешний файл JavaScript, так и строки JS-кода, содержащиеся внутри документа (рис. 10.3).
Рис. 10.3. Смешанный подход, при котором код JS существует в нескольких местах
Что еще интереснее, так это то, что у каждого из этих подходов могут быть свои дополнительные вариации. Например, создание нескольких секций script в HTML-документе или нескольких JS-файлов и т. д. В последующих разделах мы рассмотрим подробнее оба подхода и обсудим, когда и какой из них лучше выбирать.
В конце у вас сложится представление обо всех плюсах и минусах, что позволит вам грамотно взаимодействовать с JavaScript при разработке веб-страниц и приложений.
Поехали!
Первым мы рассмотрим подход, который использовали все это время. Он предполагает, что весь код JS находится внутри тега script рядом с остальной частью HTML-документа:
function showDistance(speed, time) {
alert(speed * time);
}
showDistance(10, 5);
showDistance(85, 1.5);
showDistance(12, 9);
showDistance(42, 21);
Когда ваш браузер загружает страницу, он поочередно считывает каждую строку кода в HTML-документе сверху вниз. Дойдя до тега script, он также обрабатывает все строки JavaScript-кода. Закончив выполнение, он продолжает считывать оставшуюся часть документа. Это значит, что местоположение тега script очень важно. Мы обсудим этот момент чуть позже, при рассмотрении темы выполнения кода в подходящее время.
В этом случае наш основной HTML-документ не содержит никакого JS-кода. Вместо этого весь код располагается в отдельном документе. Такой подход разделен на две части. Первая относится к самому файлу JavaScript, а вторая отвечает за связь этого файла с HTML. Давайте рассмотрим обе эти части подробнее.
Основой описываемого подхода является отдельный файл, содержащий код JavaScript. Не имеет значения, как вы назовете этот файл, но его расширение, как правило, будет. js. Например, мой файл JS назван example.js.
Внутри этого файла будет размещен только JavaScript:
function showDistance(speed, time) {
alert(speed * time);
}
showDistance(10, 5);
showDistance(85, 1.5);
showDistance(12, 9);
showDistance(42, 21);
Все, что мы обычно поместили бы в тег script, попадет сюда. Кроме этого, мы не будем ничего добавлять, так как появление в этом файле других элементов вроде произвольно выбранных частей HTML и CSS не допускается, а в ответ на подобные действия браузер заругается.
Как только мы создали файл JS, второй (и заключительный) шаг — это связать его со страницей HTML. За это отвечает тег script, а конкретнее — его атрибут src, который указывает на расположение JavaScript-файла:
В этом примере, если файл JavaScript расположен в одном каталоге с HTML, мы можем использовать относительный путь и сослаться напрямую на имя файла. А если этот файл находится в другом каталоге, нам нужно будет соответственно изменить путь:
В этом случае наш файл сценария вложен в три уровня каталогов с именами some, other и folder. Кроме того, мы также можем прибегнуть к использованию абсолютного пути вместо относительного:
Не важно, если пути относительные или абсолютные, — они будут прекрасно работать. В ситуациях же, когда путь между HTML-страницей и сценарием, на который мы ссылаемся, отличается (например, внутри шаблона, серверного включения, сторонней библиотеки и т. д.), безопаснее использовать абсолютный путь.
У нас есть два основных подхода, определяющих, как и где следует размещать код (рис. 10.4).
Рис. 10.4. Два основных подхода, используемых для работы с кодом JavaScript
Конечный выбор подхода зависит от ответа на следующий вопрос: «Планируется ли использование одного и того же кода в нескольких HTML-документах?»
Считывание и расположение сценариев в документе
Несколькими разделами ранее я описывал выполнения сценариев. Браузер считывает HTML-страницу, начиная сверху и построчно перемещаясь вниз. Когда он достигает тега script, то начинает выполнять содержащийся в нем код. Это выполнение также происходит построчно, начинаясь с верхней части. Все остальные возможные действия страницы на время этого выполнения приостанавливаются. Если тег script ссылается на внешний JS-файл, то прежде, чем начать выполнение этого файла, браузер его загрузит.
При таком линейном считывании документа браузером наблюдается интересные нюансы, влияющие на то, где лучше размещать теги script. Технически их можно разместить в любой части HTML-документа. Тем не менее для этого есть предпочтительное место. Учитывая то, как браузер считывает страницу, и то, что он все блокирует во время выполнения сценариев, следует размещать теги script в нижней части HTML-документа, после всех его элементов.
Если тег script окажется в верхней части документа, то на время его выполнения браузер заблокирует все остальное. В результате если вы начнете загружать большой файл сценария или будете выполнять сценарий, то пользователь сможет увидеть частично загруженную и не отвечающую HTML страницу. До тех пор пока у вас не возникнет реальной потребности в выполнении JavaScript до полного считывания документа, размещайте теги script в конце, как показано в большинстве предыдущих примеров. Есть и еще одно преимущество в расположении скриптов внизу страницы, но о нем я расскажу значительно позднее, когда мы будем говорить о DOM (объектная модель документа) и о том, что происходит в процессе загрузки страницы.
Если ответ да, тогда наверняка лучше будет поместить код в отдельный файл и сделать на него ссылку во всех HTML-страницах, которые должны его выполнять. Первая причина, по которой это стоит сделать, — вы сможете избежать повторения кода на всех этих страницах (рис. 10.5).
Рис. 10.5. Повторение кода — это проблема!
Сопровождение повторяющегося кода — это кошмар, при котором изменение сценария потребует внесения изменений в каждый HTML-документ. Если вы используете некий шаблон или логику SSI, где ваш сценарий содержится только в одном HTML-фрагменте, тогда проблема сопровождения окажется не столь велика.
Вторая причина заключается в размере файла. Если вы дублируете сценарий во множестве HTML-страниц, то каждый раз, когда пользователь загружает одну из этих страниц, он заново загружает весь сценарий. Это не такая уж и проблема, когда речь идет о небольших сценариях, но, как только в коде появляется нескольких сотен строк, размер начинает существенно расти.
Если вы размещаете весь код в одном файле, то все только что упомянутые проблемы у вас уже не возникнут (рис. 10.6).
Рис. 10.6. Весь код в одном месте
Сопровождение кода облегчается, так как его обновление производится только в одном файле. Любой HTML-документ, ссылающийся на этот JS-файл, при загрузке автоматически получает его последнюю версию. Это также позволит браузеру загружать код всего один раз и предоставлять его сохраненную в кэше версию для последующих обращений.
Если ваш ответ нет, то можете поступать по своему усмотрению. Вы по-прежнему можете разместить код в отдельном файле и сослаться на него из HTML-документа, но это принесет меньше выгоды, чем в случае с несколькими документами, который мы рассмотрели ранее.
В такой ситуации вполне подойдет размещение всего кода полностью в HTML-файле. В этом аспекте код из большинства примеров написан внутри HTML-документа. Примеры рассматриваемых кодов не предполагают их использования для множественных страниц и также большие размеры, при которых для повышения читаемости может потребоваться размещение кода в отдельном файле.
КОРОТКО О ГЛАВНОМ
Теперь вы увидели, что решение даже такого простого вопроса, как размещение кода, требует многостраничных объяснений. Добро пожаловать в мир HTML и JavaScript, где нет четкого деления на то, что такое хорошо и что такое плохо. Возвращаясь к сути этого урока, стоит отметить, что типичный HTML-документ будет содержать много файлов сценариев, загружаемых из внешних источников. Некоторые из этих файлов будут вашими, некоторые же будут взяты из сторонних ресурсов.
Напомню и о смешанном подходе, когда в вашем HTML-документе содержится не только ссылка на отдельный JS-файл, но и сам JS-код. Такой подход тоже достаточно распространен. В конце концов, только вам решать, какой именно подход использовать. Надеюсь, что эта глава предоставила вам достаточно информации, чтобы вы могли выбирать подходы осознанно. В главе 35 «События загрузки страницы и прочее» мы еще больше углубимся в пройденную в этой главе тему, рассмотрев на примерах связанные с загрузкой страниц и некоторые особые атрибуты, которые привносят в этот процесс свои сложности. Но пока не будем о них думать.
Если у вас появились вопросы, уверенно задавайте их на форуме https://forum.kirupa.com. Я и другие разработчики будем рады вам помочь!