JavaScript Учебник начального уровня
Программы, мысли, темы
JavaScript — начало.
Необычный для меня опыт, но что делать, попробую. Недавно я начал осваивать JavaScript. Понимаю, что этим никого не удивишь, но, пока ещё первые шаги изучения остались в памяти, решил попробовать написать небольшое руководство для тех, кто также только начинает изучение этого языка программирования.
Несколько слов об особенностях JavaScript, другими словами — что я знал об этом языке до начала его изучения. Первое и главное: JS код легко интегрируется в HTML разметку и является де факто стандартом для интернет браузеров. Из этого следует, что если вы совершенно не знаете HTML - JS, вероятнее всего, будет для вас бесполезна. В данном опусе я постараюсь разжевать всё максимально детально, но надо иметь в виду, что HTML и JS идут бок о бок, дополняя, а не заменяя друг друга. Для тех, кто сталкивался раньше с Delphi программированием или чем–то подобным можно, в первом приближении, привести такую аналогию: HTML — это форма, а JS — сам код программы.
Другая важная особенность языка — высокая интерактивность. Процитирую фразу, которую мне многократно доводилось слышать: «JavaScript — это событийно ориентированный язык программирования». Что это значит? Это означает, что вы можете написать кусок кода, который выполнится когда пользователь нажмёт на кнопку, можете написать кусок кода, который выполнится когда вы только подведёте к ней курсор мышки. Для Java Script и то и другое — события и она умеет на них реагировать.
Отличительная особенность языка и кода на нём — то, что результат можно увидеть имея лишь один из современных браузеров. То есть вам не нужно ставить и настраивать какие–то компиляторы, среды программирования и так далее. Для запуска любого из приведённых ниже примеров — достаточно набрать текст в любом редакторе (блокноте, например), изменить расширение на html и открыть при помощи браузера. Всё.
Завершая вводную часть отмечу, что пишу я под Android Scripting, и JavaScript там может иметь свои особенности. Надеюсь, что не имеет.
Сделаем, чтобы хоть что–то работало.
Первое, что смущает при изучении новых языков программирования — как тут сделать хоть что–то. Именно поэтому в мировой практике часто в качестве первого примера программы для языка приводят так называемый «Hello world». По сути пример представляет собой код минимальной программы, которая при запуске выводит на экран фразу «Hello world» соответственно. Почему именно эта фраза? Видимо это такой программистский юмор, так как самая простая программа едва–ли сможет сделать что–то полезное, то пусть хотя бы поздоровается.
Мой первый пример чем–то похож на тот, просто употребляются другие слова и не используется программирования как такового. Пример будет на чистом HTML без JavaScript.
<html>
<head>
</head>
<body>
Забавно писать на html + java script не зная ни того, ни другого. <br>
Но попробуем.
</body>
</html>
В результате, при запуске странички в браузере мы получим такую картину:
Понимая, что тем, кто знаком с HTML читать будет не интересно, я всё же сдержу обещание и буду давать максимально подробные объяснения.
Сразу отвечу на возможные обвинения в том, что приводимая информация не полная. Для каждого примера я буду стараться приводить только ту информацию, которая необходима для понимания именно этого примера. Ни больше, чтобы не путать читателя, ни меньше, чтобы у него не оставалось пробелов.
Ключевое понятие языка HTML разметки — так называемый «тэг». Есть понятие открывающего тэга и закрывающего. Для части тэгов предусмотрено обязательное наличие закрывающего тэга, для части — нет. Тэги — это по сути любые слова, заключённые в треугольные скобки. Само это слово — название тэга. Закрывающий тэг должен начинаться с символа "/". Всё, что находится между закрывающим и открывающим тэгом, относится к этому тэгу.
Первое, что мы видим в нашем примере — тэг html. Соответствующий ему закрывающий тэг находится в самом конце. Этот тэг указывает, что внутри него (между открывающим и закрывающим тэгом) используется html разметка. Логично, что весь наш документ внутри этого тэга.
Далее следует тэг head. Он отвечает за всевозможную служебную информацию о документе. Оставим его пустым, поставив сразу за ним соответствующий закрывающий тэг, для нас он сейчас не важен.
Наконец мы добрались до тэга body. Именно там находится то, что мы видим на экране. Например, текст, написанный напрямую в этом тэге (не находящийся во вложенных тэгах и не содержащий служебных символов) сразу выводится на экран в том же виде, в котором введён в исходном коде страницы.
Внутри тэга body мы просто написали текст, который при запуске будет выведен на экран. Всё просто, кроме одного маленького нюанса. Между двумя предложениями мы натыкаемся на новый тэг <br>. Этот тэг отвечает за начало новой строки.
Сделай ещё что–нибудь.
В предыдущем примере мы добились того, что на экран вывелся текст. После этого, чтобы ни делал пользователь, ничего не произойдёт. Независимо от того как, когда и зачем пользователь откроет ваш документ, он увидит тот же самый текст. Исходя из этого делаем вывод, что программой это называть ещё рано.
Как я уже упоминал, HTML вообще говоря к программированию отношения не имеет. Но он, если можно так выразиться, поддерживает скриптовые расширения. Самый распространённый язык программирования, используемый для этих целей - JavaScript. Слегка расширим наш предыдущий пример, чтобы проиллюстрировать его возможности:
<html>
<head>
</head>
<body> Забавно писать на html + java script не зная ни того, ни другого. <br>
Но попробуем.
<br>
<br>
<input type=button value='Нажми меня!' onclick=' this.value = «Спасибо»;'>
</body>
</html>
Запускаем в браузере и видим:
Основная часть кода осталась без изменений, после текста добавились две пустые строки для более гармоничного внешнего вида странички. Больше всего в этом примере нас интересует новая строчка
<input type=button value='Нажми меня!' onclick=' this.value = «Спасибо»;'>
рассмотрим её внимательно.
Первое, что бросается в глаза — непривычно большой тэг input. Да, название тэга именно input, от остальных элементов оно отделено пробелом. Что это за элементы? Мы о них раньше не говорили…
Дело в том, что тэг, помимо названия, может содержать так называемые атрибуты. Думаю нет смысла давать им определения, долго и нудно объяснять понятие, лучше просто попытаться научиться их читать. Давайте попробуем, я буду читать, одновременно переводя на русский:
<Поле ввода, типа кнопка, со значением «нажми меня», в случае клика должен выполниться скрипт «this.value = «Спасибо»;">
Ничего сложного. Поле ввода или элемент управления или просто контрол может быть разных типов, мы в нашем примере используем кнопку. Атрибут value в данном случае задает текст, который будет написан на нашей кнопке. Для того, чтобы «подчеркнуть», что заданное нами значение именно текст (строка), его следует заключить в кавычки или апострофы.
Почему я написал в данном случае задаёт текст, разве это работает не всегда? Дело в том, что для разных тэгов атрибут с одним и тем же именем может означать разные вещи. Для кнопок этот код работает, для чего–то другого — может работать, а может и не работать.
Вот, наконец, мы и подошли непосредственно к программированию, а строчка «this.value = «Спасибо»;" по сути наша первая программа. Разберём и её тоже.
Ключевое слово this в данном случае — это наша кнопка. Этим словом (в Delphi похожую функцию выполняет Self) кодируется объект, которому принадлежит вызываемая функция (в JS есть свои нюансы, но в первом приближении это верно). То есть в нашем случае мы написали, что в случае, если наша конкретная кнопка будет нажата будет выполнен некий код. Так вот, в этом коде this — это и будет наша кнопка.
Точка после this означает, что далее мы будем обращаться к тому, что есть у this. В данном случае мы обращаемся к атрибуту value тэга. Да, да, к тому самому, значение которого чуть раньше мы задавали в тэге input. Теперь мы по сути делаем то же самое, только чуть иначе.
Ну и последнее тут — точка с запятой в конце. Это, как и во многих других языках является разделителем для аналога предложений в языках программирования. Как и в обычных человеческих языках там есть свои нюансы использования, но об этом позже.
Начнём первый проект.
Как пример, иллюстрирующий написание программ на JavaScript, я выбрал известную игру сапёр. Сделал я это не осмысленно, просто я попытался нарисовать таблицу и поменять в ней что–то при нажатие на ячейку. Сделал и подумал, да это же почти сапёр!
Сразу скажу, что никакой особенной графики в программе не будет, зачастую следует различать создание интерфейса приложения и программирования его логики. Несмотря на то, что разделить их бывает не всегда просто — задача эта важная и нужная.
Ладно, перейдём к делу. Давайте пока просто попробуем нарисовать таблицу:
<html>
<head>
</head>
<body>
<table id=«tbl8765875120451524678» border=«2» width=«80%" height=«80%">
<tr>
<td>
cell11
<td>
cell12
<tr>
<td>
cell21
<td>
cell22
</table>
</body>
</html>
cell11 cell12 cell21 cell22Как видно из кода, всё новое для нас заключено внутри тэга table. Собственно этот тэг и добавляет таблицу в документ.
Непосредственно в открывающем тэге выставляются ряд атрибутов, давайте коротко поговорим о каждом:
«id» — это идентификатор таблицы, именно с его помощью мы будем использовать таблицу в JavaScript. В общем случае — это произвольная строка. Для этого примера я его сделал равным «tbl» + идентификатор поста в блоге, чтобы не было наведённых проблем, а именно, если из другого скрипта (а он может быть в соседнем посте) будет обращение к элементу с тем же «id», что и в этом скрипте — наша табличка может тоже модифицироваться, что нежелательно.
«border» — это размер так называемого бордюра вокруг ячеек. Как это лучше сказать по–русски… Окантовка, наверное ближе всего. Мы указали, что бордюр будет шириной в два пикселя (пиксель — это как бы единица длины экрана монитора, причём самая маленькая, то есть не может быть пол пикселя). Можете поэкспериментировать со значением этого атрибута, его, например, можно сделать равным нулю и тогда ячейки совсем не будут разделены.
«width» — это ширина нашей таблицы. Можно было задать её в пикселях, как и толщину бордюра, но, в данном случае, как мне кажется, уместнее использовать относительную величину. А именно, 80% от ширины нашего документа. Следует всегда иметь ввиду, что реальный размер пикселя зависит и от разрешения экрана и от его размера. В одном случае объект шириной в 1000 пикселей может занимать половину ширины экрана, а в другом он может на экран не влезть.
«height» — тэг аналогичный тэгу «width», только он отвечает за высоту нашей таблички. Мы его также сделали равным 80%. Более подходящим вариантом было бы сделать таблицу квадратной, беря 80% от минимума высоты и ширины документа, но я решил излишни не усложнять код.
Далее идёт тэг «tr», он означает начало новой строки таблицы.
Следом за ним идёт тэг «td», означающий начало следующей колонки (ячейки в строке).
После тэга «td» идёт текст «cell11». Это тот текст, что будет написан в нашей первой ячейке. Затем опять переход к следующей ячейке, надпись «cell12», новая строка и там ещё две ячейки.
Пока эта таблица ничего не умеет, да и маловата она для сапёра, не интересно играть будет. Думаю стоит сделать её побольше, только вот не хочется для каждой ячейки писать по две строчки, плюс ещё по одной на каждую строку. Эдак у нас для поля 10х 10 получится 210 строчек текста на одну только таблицу, которая ничего не умеет. Может можно сделать это как–то компактнее? Давайте поговорим об этом в следующей главе.
Как программирование помогает сделать вашу страничку компактнее.
Помните пример из прошлой главы? Там мы рисовали табличку из четырёх ячеек на чистом HTML. Глава закончилась на том, что для создания таблицы 10х 10 ячеек будет нужно 210 строк внутри основных тэгов. Посмотрим, сумеем–ли мы сделать текст более компактны с помощью JavaScript.
Вот, что получилось у меня:
<html>
<head>
head>
<script>
function initTable()
{
var rowCount = 10;
var colCount = 10;
var tbl = document.getElementById(«tbl»);
for (var i = 0; i < rowCount; i++)
{
var row = tbl.insertRow(i);
for(var j = 0; j < colCount; j++)
{
var cell = row.insertCell(j);
cell.innerHTML = 'cell' + i.toString() + j.toString();
}
}
}
script>
<body onload=«initTable();">
<table id=«tbl» border=«2» width=«80%" height=«80%">
table>
body>
html>
В итоге получается такая табличка:
Начнём анализ с содержимого тэга «body». В самом открывающем тэге появился новый атрибут «onload». Как можно предположить из названия, этот атрибут отвечает за то, какое действие выполнится после загрузки страницы. В нашем примере мы указали, что после загрузки страницы должна выполниться некая функция, которая называется «initTable». Более подробно об этом поговорим несколько ниже.
Что ещё поменялось в тэге «body»? Мы убрали всё содержимое тэга «table». Это вполне логично, так как заполнять его мы решили при помощи скрипта.
Раз уж упомянули слово скрипт, придётся отдуваться. «script» — это самый большой тэг в примере к этой главе, но, как вы видете, он всё–таки значительно меньше 210 строк. Внутри него нет ничего, кроме скрипта на JavaScript. А теперь внимание, требуется перестроиться, внутри этого тэга больше не будет привычных для HTML конструкций. И если в HTML разбиение кода на смысловые блоки происходило с помощью тэгов, то теперь это будет происходить с помощью фигурных скобок.
Первое, что мы встречаем в нашем скрипте — строчку «function initTable()». Таким синтаксисом в JavaScript объявляется функция. Что же такое функция в данном контексте — по сути — это фактически самостоятельный скрипт (на самом деле часть скрипта), которая может выполнять какие–то действия. Более подробную информацию я приведу позже, для понимания данного примера достаточно знать того, что я уже написал.
Кстати, обратили внимание, название функции совпадает с тем, что мы задали в атрибуте «onload» тэга «body». Это означает, что именно эта функция выполнится после того, как страница загрузится.
Название функции, разумеется, может быть отличным от того, что привёл я, на поведение функции оно никак не влияет, однако, для улучшения читабельности кода, следует давать функциям названия перекликающиеся с тем, что функция делает.
Далее мы переходим к следующей строчке, в ней всего один символ, открывающая фигурная скобка. Как я уже упоминал, она служит обозначением начала нового блока. Это значит, что с этого момента и до достижения парной ей фигурной скобки, весь код будет относиться к функции «initTable()».
Как узнать какая скобка парная данной? Есть нехитрый алгоритм. Представьте, что до неё у вас ноль скобок, вот вы дошли до открывающей скобки и уже одна скобка, дошли до следующей открывающей — их две. Потом, допустим, закрывающая, после неё снова осталась одна. И так следует прибавлять единицу для открывающих и вычитать для закрывающих до тех пор, пока вы не получите ноль. Та закрывающая скобка, после которой ваш «счётчик» обнулился и будет парной к первой открывающей.
Применив алгоритм, получим, что весь код внутри тэга «script» относится к нашей функции.
Длинная глава получается, надо было отдельно поговорить про функции, отдельно потом про переменные, но, я в этой работе решил идти не от понятий, не от терминов, а от функционала, чего и пытаюсь придерживаться.
Так вот, мы переходим непосредственно к телу функции (её содержимому). Первая её строка выглядит так: «var rowCount = 10;". Давайте по порядку. Ключевое слово «var» говорит о том, что в этой строке мы будем объявлять новую переменную. После этого слова идёт название переменной, а затем (не обязательно), ей присваивается значение. Равнозначно было бы написать:
var rowCount;
rowCount = 10;
но это, как вы понимаете длиннее.
Так что же такое переменная? Попробую объяснить на пальцах. Переменная — это нечто, имеющее имя и соответствующее ему значение. Например, вы родились 29 ноября. Тогда имя соответствующей переменной «число вашего рождения», я её значение — 29 ноября (только не пробуйте именовать переменные кириллицей или используя пробелы, так делать нельзя, я так назвал для удобства, а вот «dateOfYouBirth», если я не ошибся в правописании, которое у меня, как и у Винни Пуха хромает, хотя оно есть, вполне нормальное название для этой переменной).
Другой не плохой пример из реальной жизни — счёт в банке, у него есть номер и на нем есть какое–то количество средств. Номер тут — имя переменной, а количество средств — значение. Мы можем изменить значение этой переменной, например, всё потратив.
Так вот, в первой строке я объявил переменную rowCount и присвоил ей значение 10. Во второй строке я проделал то же самое с переменной colCount. Эти переменные будут нам задавать количество строк и колонок в нашей таблице, можете поменять их значения и увидите, что будет.
В следующей строке опять много нового, но потерпите, зато в этой главе мы сможем разобраться с куда большим количеством понятий, чем в остальных. По–моему, оно того стоит. Итак: " var tbl = document.getElementById(«tbl»);". Тут мы объявляем переменную «tbl», она в последствии позволит нам управлять нашей табличкой и присваиваем ей выражение «document.getElementById(«tbl»)».
Тут мы впервые вскользь коснемся такого понятия как ООП (объектно ориентированное программирование). Собственно мы попробуем упрощённо определить, что такое объект в контексте ООП.
Объект — это по сути переменная, которая содержит в себе другие переменные и функции. Они также называются полями и методами соответственно. Так вот, слово «document» в нашем выражении — это название переменной, уже до выполнения скрипта объявленной самой JavaScript, которая соответствует объекту, который предоставляет доступ к нашему документу. По сути, он позволяет JavaScript коду работать с HTML кодом в рамках одного документа.
Немного запутанно? Надеюсь станет понятнее, когда мы перейдём к примерам, не пытайтесь понять всё сразу, это на самом деле довольно сложные вещи, которым посвящено большое количество весьма увесистых книг. Для понимания этого примера важно понять, что есть некая переменная «document» и что с помощью неё мы можем как–то взаимодействовать с элементами нашей странички, в нашем примере это будет таблица.
Ещё пару слов про объекты, как я уже говорил, у них есть поля и методы. Но как к ним обращаться? В JavaScript, как и во многих других языках программирования, к ним принято обращаться через точку. То есть, например, наше выражение «document.getElementById» можно прочитать как «метод getElementById объекта document». Следует понимать, что у разных документов этот метод вернёт скорее всего разный результат, так как он возвращает элемент именно этого того документа, переменная которого находится перед точкой.
Так что же в результате делает метод «getElementById»? Как и следует из названия, он возвращает контрол, «id» которого (элемент, соответствующий тэгу, в котором атрибут id которого) передается в скобках.
Кстати то, что передаётся в скобках после названия функции (метода), тоже имеет своё название. В прошлый раз, когда я говорил о функциях, я это пропустил, так как скобки у нас были пустые, а теперь, придётся упомянуть. В круглых скобках после названия функции передаются её параметры. У функции может быть один или несколько параметров, может совсем не быть параметров. В случае нескольких параметров, в JavaScript они разделяются запятыми. В случае отсутствия параметров, скобки остаются пустыми.
В данном случае, функция требует передачи одного строкового параметра, соответствующего идентификатору (id) элемента.
В результате выполнения строчки, мы получили в переменной «tbl» объект, с помощью которого мы сможем управлять нашей таблицей (получим доступ к нашей таблице).
Следующая строка — снова не простое, но фундаментальное понятие — цикл. Циклы бывают нескольких видов и предназначены для выполнения части кода несколько раз. Основное отличие циклов в том, как формулируется условие прекращения цикла (выхода из цикла).
В нашем случае мы используем цикл «for» в котором заранее знаем сколько раз нужно будет выполнить его тело (содержимое блока его принадлежащего). В этом цикле есть понятие переменной цикла, значение которой увеличивается на единицу каждую итерацию (после каждого выполнения тела цикла). Переменная цикла доступна на чтение из тела цикла, то есть мы можем посмотреть, что в ней лежит, но, в большинстве языков программирования, изменить её нам не удастся.
Давайте попробуем прочитать нашу строчку по–русски: «for (var i = 0; i < rowCount; i++)». Я бы её перевёл как «выполнять следующий блок кода и каждый раз увеличивать переменную «i» на единицу до тех пор, пока её значение остаётся меньше значения переменной «rowCount» (количества строк в нашей таблице). Блок, принадлежащий этому циклу также заканчивается в самом конце функции.
Далее идёт строка «var row = tbl.insertRow(i);". Там мы переменной «row» присваивает результат метода объекта нашей таблицы «insertRow» с нашей переменной цикла «i» в качестве. параметра. По аналогии с тем, что мы видели ранее, данный метод делает то, что следует из его названия, а именно, добавляет новую строку в таблицу. Причём, когда i = 5, строка добавляется так, что становится пятой (после четвёртой), ну и так далее.
Не забываем, что мы в цикле, поэтому строка будет добавляться rowCount раз и каждый раз, проходя эту строчку, интерпретатор будет изменять значение row на новое.
Далее идёт новый цикл с переменной цикла «j», который выполняется colCount раз. Когда один цикл выполняется внутри другого, последний называется вложенным циклом.
Отличительной его особенностью является то, что его тело выполняется во столько раз чаще, чем если бы он не был вложенным, сколько раз выполняется внешний цикл. То есть в нашем случае тело этого цикла будет выполняться rowCount * colCount раз, или, другими словами, на каждую строку по числу колонок.
Первая строка внутри этого цикла: «var cell = row.insertCell(j);" создаёт новую ячейку в строке row нашей таблицы и присваивает её переменной cell. Вторая строка: «cell.innerHTML = 'cell' + i.toString() + j.toString();" присваивает полю innerHTML нашей ячейки строку «cell» + значение переменной «i», переведённая в строку вызовом метода «toString», плюс значение переменной «j», также в виде строки. Чтобы различать строки и названия функций, переменных и прочие куски кода, строки в JavaScript принято заключать в апострофы или двойные кавычки. Причём, если вы хотите использовать апострофы внутри строки — следует добавить перед ними символ "\". В случае с двойными кавычками можно этого не делать, если сама строка заключена в апострофы.
Что ещё может быть не понятно? Вероятно поле «innerHTML». По сути это фрагмент HTML кода, который вы программным образом присваиваете тэгу контрола. Так как в этой главе и так было не мало информации, думаю будет достаточно указать на то, что с помощью этого тэга мы изменяем надпись, которая будет отображаться в нашей ячейке.
Глава оказалась длиннее, чем я рассчитывал. Сильно длиннее, но в ней было затронуто много основополагающих аспектов программирования и, по правде сказать, затронуты весьма поверхностно. Так или иначе, в будущем постараюсь, чтобы новых элементов в главе встречалось меньше.
Расставим бомбы.
Раз уж так получилось, что мы решили написать аналог игры сапёр, расставим на нашей табличке бомбы. Предположим их будет 10 штук и тот кто будет играть в нашу игру не должен будет знать где они. Но в этой главе мы для простоты всё же раскроем карты. Сделаю я это намеренно, чтобы результат можно было увидеть.
Вот что у меня получилось.
<html>
<head>
head>
<script>
function intRand(maxVal)
{
return Math.round((maxVal + 1) * Math.random() — 0.5);
}
function fillMines(rowCount, colCount, minesCount)
{
var res = new Array(rowCount * colCount);
var mines = minesCount;
while (mines > 0)
{
var n = intRand(rowCount * colCount‑1);
if (res[n] != 1)
{
res[n] = 1;
mines--;
}
}
return res;
}
function initTable()
{
var rowCount = 10;
var colCount = 10;
var minesCount = 10;
var mines = fillMines(rowCount, colCount, minesCount);
var tbl = document.getElementById(«tbl»);
for (var i = 0; i < rowCount; i++)
{
var row = tbl.insertRow(i);
for(var j = 0; j < colCount; j++)
{
var cell = row.insertCell(j);
if (mines[i * rowCount + j] == 1)
cell.innerHTML = '*'
else
cell.innerHTML = '.';
}
}
}
script>
<body onload=«initTable();">
<table id=«tbl» border=«2» width=«80%" height=«80%">
table>
body>
html>
К сожалению в каждой новой главе, кода становится всё больше, при этом нового кода вообще говоря всего ничего. Это вечная дилема — приводить куски кода в каждой новой главе, но тогда могут быть проблемы с пониманием того, что к чему относится, либо постоянно копировать и тогда текст раздувается до каких–то нереальных размеров. Я пока решил придерживаться второго варианта. Дальше посмотрю.
Мы видим уже знакомую страничку со скриптом. Посмотрим скрипт снизу вверх. Внизу у нас уже знакомая по прошлой статье функция initTable(). В чём её отличие от предыдущей реализации. Во–первых, тут мы добавили переменную minesCount, которой сразу же присвоили значение 10. Кроме того, мы объявили переменную mines, которой присвоили результат функции fillMines с числом строк, столбцов и мин в качестве параметров. Подробнее о том, как устроена эта функция я расскажу ниже, скажу лишь что представляет собой её результат. Функция fillMines возвращает массив длины rowCount * colCount, причём для каждому элементу массива, равному единице соответствует мина, а иначе мины нет.
Что же такое массив? Это особый тип переменной, которая сама по себе ничего не хранит, но как бы предоставляет доступ к другим переменным внутри себя. Вариант классического массива — это как бы набор элементов, доступ к которым осуществляется по индексу. Например, Arr — это массив, тогда Arr[0] — это переменная, соответствующая нулевому элементу массива Arr. Ну и так далее.
Каким образом наш массив mines будет соотноситься с нашей таблицей? Да всё очень просто. Для первой строки первого столбца, то есть элемента [0, 0] (будем нумеровать с нуля, в программировании так почти везде) будет соответствовать нулевой элемент массива [0]. Для первой строки второго столбца [0, 1] — первый элемент массива [1]. Для [0, 9] — [9]. Для первой строки закончились столбцы — не беда, теперь возьмём вторую строку первый столбец [1, 0], этой ячейке будет соответствовать десятый элемент массива mines, то есть, в нашей терминологии, [10]. Так будем идти дальше, пока не дойдем до последней (десятой) строки и последнего (десятого) столбца [9, 9], им будет соответствовать элемент [99].
Всё это на самом деле можно записать куда короче: [i, j] соответствует [i * colCount + j]. Можете проверить на листе бумаги.
Хорошо, мы получили наш массив с бомбами. По сути это основное функциональное отличие от того, что мы делали в прошлой статье. Далее мы видим уже знакомый вложенный цикл, мы получаем переменную ячейки, а дальше, вместо того, чтобы положить туда строку «cell» с индексами ячейки в таблице, мы пишем сразу 4 строки.
if (mines[i * rowCount + j] == 1)
cell.innerHTML = '*'
else
cell.innerHTML = '.';
Тут мы снова сталкнёмся с новой конструкцией. Оператор if или условный оператор. Что это такое? Давайте опять попробуем перевести фразу на русский: «если (mines[i * rowCount + j] == 1), то выполнить cell.innerHTML = '*', а иначе выполнить cell.innerHTML = '.'". Другими словами, если выполняется условие после if (в JavaScript обязательно заключено в круглые скобки), выполняется блок (в нашем случае одна строка, так как нет фигурных скобок обрамляющих блок) сразу после оператора if, если же условие не выполняется (не обязательно, блока else может и не быть), выполняется блок сразу после оператора else. Если за блоком, идущем сразу после блока if нет оператора else, последующий код выполняется всегда, независимо от выполнения или невыполнения условия.
Что же значит выполнение условия, давайте внимательней разберём выражение «mines[i * rowCount + j] == 1». «mines[i * rowCount + j]» — это, как вы вероятно уже догадались переменная, которая соответствует текущей ячейке (строка i, столбец j) в нашем массиве бомб. Как вы помните, эта переменная равна 1, если бомба в соответствующей ячейке есть. Именно равенство переменной единице мы и проверяем. Оператор точного сравнения переменных в JavaScript — "==".
Давайте немного потренируемся составлять такие условия. (i > 2) — значение переменной i больше, чем 2. (i != 3) — значение переменной i не равно 3. (i == j) — значение переменной i равно значению переменной j.
Ну и чтобы формально закончить рассмотрение этого кусочка кода. Получается, что если у нас есть бомба в ячейке — мы делаем текст ячейки равным строке '*', если же бомбы нет — строке '.'.
Теперь перейдём к рассмотрению следующей большой функции в нашем скрипте - fillMines.
function fillMines(rowCount, colCount, minesCount)
{
var res = new Array(rowCount * colCount);
var mines = minesCount;
while (mines > 0)
{
var n = intRand(rowCount * colCount‑1);
if (res[n] != 1)
{
res[n] = 1;
mines--;
}
}
return res;
}
Что же происходит тут. Именно эта функция создаёт нам наш массив с бомбами.
Первой же строчкой создаётся пустой массив, длиной rowCount * colCount. В нём ещё ничего нет, далее мы должны будем его заполнить. Кроме того, мы заводим некую вспомогательную переменную mines, которой присваиваем начальное значение minesCount. Вероятно я её не совсем удачно назвал, будем считать, что она называется как–то вроде «количество бомб, которые ещё нужно распределить». Это куда лучше описывает её назначение.
Далее мы сталкиваемся с новым типом цикла. Это цикл while. Как работает этот цикл? Он выполняет свой блок кода (тело) до тех пор, пока выполняется условие в скобках сразу после слова while. То есть в нашем случае цикл будет выполняться пока ещё есть неразмещённые бомбы. Вообще говоря это довольно опасный цикл, так как его легко сделать бесконечным (в том числе и ошибочно). Например в моём случае функция не проверяет (хотя стоило бы), что бомб меньше чем ячеек в таблице, а ведь если это условие не выполняется, мы никогда не сможем разместить все бомбы так, чтобы в каждой ячейке было не больше одной, а значит никогда не выйдем из этого цикла и наша программа зависнет.
Само тело цикла довольно простое, переменной n мы присваиваем результат функции intRand c параметром числа ячеек таблицы — 1 (максимальный индекс в нашем массиве бомб). Эта функция, как она работает будет ниже, возвращает случайное целое число от 0 до значения переданного в качестве параметра включительно.
Далее мы проверяем, есть–ли уже бомба в элементе с индексом n в массиве res. И если её нет, мы её туда добавляем (res[n] = 1) и уменьшаем на единицу количество бомб, которые надо добавить (mines--). Как только все бомбы будут распределены (mines станет равно 0), наш цикл закончится и мы вернём результат (return res). Из незнакомого тут может быть оператор декремента " — ". Он увеличивает значение переменной на единицу, то есть запись mines-- эквивалентна записи mines = mines‑1. И оператор return. Этот оператор возвращает переменную или выражение после себя в качестве результата текущей функции и выходит из неё. То есть если вы вызовете оператор return в середине функции, оставшаяся её часть выполняться уже не будет.
Вообще говоря, предложенный алгоритм имеет ряд изъянов. Как я уже говорил, возможно зацикливание (зависание), кроме того, неизвестно сколько времени потребуется для того, чтобы случайным образом выбрать ячейки для мин. Чисто теоретически, может 1000 раз подряд выпасть число 39, таким образом мы будем выполнять цикл более тысячи раз, а может сразу выпасть 10 разных чисел и мы будем выполнять цикл всего 10 раз. И хотя вероятность описанной мной ситуации ничтожно мала, она всё–таки существует и тем больше, чем большее количество мин нам нужно расставить. Грубо говоря, мой алгоритм, наверняка будет не плохо работать при количестве мин значительно меньшем, чем количество ячеек, но его не стоит применять, когда их почти столько же.
Наконец мы подошли к последней функции, о которой следует рассказать в этой статье. Она совсем небольшая, всего в одну строчку, итак, функция intRand.
function intRand(maxVal)
{
return Math.round((maxVal + 1) * Math.random() — 0.5);
}
Как я уже говорил, она возвращает случайное целое число от 0 до значения параметра maxVal включительно. То есть intRand(2) может вернуть или 0, или 1, или 2, причём каждый вероятность возврата каждого из этих вариантов равна.
Для реализации этой функции мы будем использовать ещё один предопределённый объект JavaScript‑Math, а именно, два метода этого объекта: round() и random(). Метод round() округляет число, переданное в качестве параметра до ближайшего целого. Например, raund(2.93) вернёт число 3, а raund(2.45) — число 2. Метод random() возвращает случайное дробное число от 0 до 1.
Предположим, мы вызвали функцию intRand() от 2, тогда (maxVal + 1) * Math.random() будет случайным дробным числом от 0 до 3. Каждое значение равновероятно, причём, при последующем округлении (если мы сразу применим функцию random к этому числу), нам вернётся 0 в случае, если число от 0 до 0.5; 1 если число от 0.5 до 1.5; 2 если число от 1.5 до 2.5 и 3 для числа от 2.5 до 3. Как видно из рассчётов, вероятность того, что вернётся 0 или 3 в два раза ниже, чем вероятность того, что вернётся 1 или 2. При том, что тройка нам не нужна совсем, а вероятность возврата 0 хотелось бы иметь равной вероятности возврата 1 и 2. Можно, конечно, написать условие, что если результат maxVal + 1, то вернуть 0 и это тоже будет формально правильно, однако я перед округлением вычел из дробного числа 0.5, что привело к по сути такому же результату.
Первый класс
Долго думал, стоит ли в рамках этого опуса использовать классы. На практике я часто сталкивался с тем, что очень не просто бывает объяснить зачем они нужны. На первый взгляд, после переделки какой–то простой процедурно–ориентированной программы в объектно ориентированную, ну или попросту после выделения одного–двух классов, программа, как правило, начинает выглядеть даже сложнее. Я всё же попробую. Причём как обычно буду стараться говорить человеческим языком, а если и буду вводить какие–то термины, постараюсь их объяснять.
Собственно преимущества выделения классов должны всплывать и в следующих главах по ходу повествования. Тут я лишь вскользь об этом упомяну.
Перейдём к делу, как я, кажется, уже говорил ранее, класс — это такая особенная переменная, которая может содержать другие переменные (поля), а также собственные функции (методы). В этой главе наша программа не научится делать ничего нового, при запуске произойдёт всё тоже самое, что и при запуске программы из прошлой главы. Изменился лишь сам код. Такое изменение программы называется рефакторингом.
Что же я поменял? Я выделил новый класс «minesClass» и перенёс туда часть переменных и функций. Для чего мне это понадобилось? Я решил отделить логику программы от её интерфейса. То есть класс «minesClass» ничего не будет знать о нашей таблице, однако он сам расставит бомбы при своём создании, а затем останется лишь их нарисовать.
Предлагаю читателю посмотреть на то, что получилось, а затем я всё–таки попытаюсь объяснить ему, что стало лучше.
<html>
<head>
head>
<script>
function minesClass(aRowCount, aColCount, aMinesCount)
{
this.intRand = function(maxVal)
{
return Math.floor((maxVal‑1) * Math.random() + 0.5) — 1;
}
this.fillMines = function()
{
var res = new Array(this.rowCount * this.colCount);
var mines = this.minesCount;
while (mines > 0)
{
var n = this.intRand(this.rowCount * this.colCount‑1);
if (res[n] != 1)
{
res[n] = 1;
mines--;
}
}
return res;
}
this.colCount = aColCount;
this.rowCount = aRowCount;
this.minesCount = aMinesCount;
this.mines = this.fillMines();
}
function initTable()
{
var mines = new minesClass(10, 10, 10);
var tbl = document.getElementById(«tbl»);
for (var i = 0; i < mines.rowCount; i++)
{
var row = tbl.insertRow(i);
for(var j = 0; j < mines.colCount; j++)
{
var cell = row.insertCell(j);
if (mines.mines[i * mines.rowCount + j] == 1)
cell.innerHTML = '*'
else
cell.innerHTML = '.';
}
}
}
script>
<body onLoad = initTable();>
<table ID = «tbl» BORDER = 2 width = 80% height = 80%>
table>
body>
html>
Короче и понятнее, ничего лишнего. Сразу видно, что к чему относится. Представте, что у вас в программе есть таблица для игры mines и одновременно есть таблица для игры в точки. Довольно трудно будет сказать в этом случае, что значит переменная rowCount. А вот если написано mines.rowCount, сразу всё понятно.
Перейдём к функции minesClass. Это по сути и есть наш новый класс. В JavaScript класс — это просто функция, а чтобы создать новый экземпляр классна нужно перед вызовом функции написать слово new.
В этой функции мы инициализируем методы intRand, fillMines, colCount, rowCount, minesCount и mines. В JavaScript методы также могут быть полями. Например, так мы описываем новый метод intRand. По сути переносим его в класс minesClass.
this.intRand = function(maxVal)
{
return Math.floor((maxVal‑1) * Math.random() + 0.5) — 1;
}
Я бы прочитал это так: поле «intRand» соответствует (равно) функции, которая принимает максимальное значение в качестве единственного параметра и возвращает случайное целое число от нуля до этого числа.
Аналогичным образом мы переносим функцию fillMines, делая её методом класса minesClass. Параметры этой функции уже не нужны, так как у метода класса есть доступ к его полям. Следует только не забывать перед обращением к полям класса писать «this.». Например, this.rowCount, this.mines и так далее.
Надеюсь мне хотя бы примерно удалось рассказать про то, как можно создавать и использовать классы в JavaScript и даже показать, что это может сделать программу понятнее. Если не вышло — значит я плохо старался… Однако у меня ещё остаётся надежда, что дальше всё станет понятнее. Тут затрагивались довольно трудные для понимания вещи и если что–то осталось непонятным — ничего удивительного и ничего страшного.
Считаем бомбы
На самом деле в этой главе тоже не будет ничего нового. Всё основное, чтобы реализовать функциональность этой главы я уже рассказал. Но что поделать, надо довести начатое до конца, да и потом, даже если где–то в чём–то я повторюсь, так ведь повторение — мать учения.
В прошлой главе мы остановились на том, что у нас была табличка, в которой рисовались бомбы в виде символа "*" или клетки без бомб в виде точки. Но это не очень–то похоже на сапёр в который мы все привыкли играть. Там вроде как в каждой клетке пишется количество соседних с ней клеток в которых есть бомба.
Ну что же, давайте сделаем это и в нашей программе. Ниже я приведу код, а потом, как обычно, его прокомментирую.
<html>
<head>
head>
<script>
function minesClass(aRowCount, aColCount, aMinesCount)
{
this.intRand = function(maxVal)
{
return Math.floor((maxVal‑1) * Math.random() + 0.5) — 1;
}
this.fillMines = function()
{
var res = new Array(this.rowCount * this.colCount);
var mines = this.minesCount;
while (mines > 0)
{
var n = this.intRand(this.rowCount * this.colCount‑1);
if (res[n] != 1)
{
res[n] = 1;
mines--;
}
}
return res;
}
this.hasMine = function(i, j)
{
if ((i < 0) || (j < 0) || (i >= this.rowCount) || (j >= this.colCount))
return 0;
if (this.mines[i * this.rowCount + j] == 1)
return 1;
return 0;
}
this.checkCell = function(i, j)
{
if (this.hasMine(i, j))
return '*';
return this.hasMine(i - 1, j - 1) + this.hasMine(i - 1, j) + this.hasMine(i - 1, j + 1) +
this.hasMine(i, j - 1) + this.hasMine(i, j + 1) +
this.hasMine(i + 1, j - 1) + this.hasMine(i + 1, j) + this.hasMine(i + 1, j + 1);
}
this.colCount = aColCount;
this.rowCount = aRowCount;
this.minesCount = aMinesCount;
this.mines = this.fillMines();
}
function initTable()
{
var mines = new minesClass(10, 10, 10);
var tbl = document.getElementById(«tbl»);
for (var i = 0; i < mines.rowCount; i++)
{
var row = tbl.insertRow(i);
for(var j = 0; j < mines.colCount; j++)
{
var cell = row.insertCell(j);
var s = mines.checkCell(i, j);
cell.innerHTML = s;
if (s == "*")
cell.bgColor = 'red';
}
}
}
script>
<body onLoad = initTable();>
<table ID = «tbl» BORDER = 2 width = 80% height = 80%>
table>
body>
html>
Давайте начнём обсуждение с изменений в функции initTable. По сути сейчас, как вы, наверное, помните, состоит из этой функции и класса minesClass, который она использует.
Для решения нашей задачи, я завёл в нашем классе новый метод checkCell, который принимает номер строки и номер столбца в качестве параметров и возвращает символ "*", если в соответствующей ячейке есть бомба или число соседних ячеек с бомбами, если бомбы в ней нет.
Теперь мы заполняем нашу табличку не звёздочками и точками, а результатом этой функции. Другими словами, метод checkCell возвращает то, что мы должны будем нарисовать в ячейке после того, как игрок на ней кликнет (пока для простоты мы это всё показываем сразу).
Так вот, для каждой ячейки мы сперва кладём результат функции checkCell для неё во временную переменную, а потом присваиваем значение этой переменной полю innerHTML этой ячейки.
После этого я решил сделать ещё небольшое улучшение. Посмотрим это сперва в коде:
if (s == "*")
cell.bgColor = 'red';
Ничего сложного, просто в случае если значение нашей временной переменной соответствует тому, что в текущей ячейке бомба, мы выставляем в поле bgColor ячейки значение 'red'. Теперь, при отрисовке, фон ячеек с бомбами будет заливаться красным. В дальнейшем, когда мы не будем сразу показывать где что находится, а будем давать возможность пользователю открывать ячейки, благодаря этому, станет более очевидно, что наш игрок открыл бомбу и проиграл.
В этой главе нам осталось поговорить о новом методе, который нам понадобился. Для его работы я ввёл небольшой вспомогательный метод hasMine:
this.hasMine = function(i, j)
{
if ((i < 0) || (j < 0) || (i >= this.rowCount) || (j >= this.colCount))
return 0;
if (this.mines[i * this.rowCount + j] == 1)
return 1;
return 0;
}
Этот метод принимает индекс строки и столбца для ячейки и возвращает ноль, если хотя бы один из индексов за пределами нашей таблицы (это проверяется в первой строке). Напомню, на всякий случай, что "||" соответствует логической операции «ИЛИ». Третья строка непосредственно проверяет, есть–ли в ячейке бомба и если она есть, мы возвращаем единичку. И, наконец в конце, если мы всё ещё не вышли из функции (ничего не вернули, так как ни одно из условий не выполнилось), мы тоже возвращаем ноль.
Ну и наконец последнее, сам метод checkCell. Он тоже принимает на вход номер строки и столбца. Давайте сперва посмотрим на его реализацию.
this.checkCell = function(i, j)
{
if (this.hasMine(i, j))
return '*';
return this.hasMine(i - 1, j - 1) + this.hasMine(i - 1, j) + this.hasMine(i - 1, j + 1) +
this.hasMine(i, j - 1) + this.hasMine(i, j + 1) +
this.hasMine(i + 1, j - 1) + this.hasMine(i + 1, j) + this.hasMine(i + 1, j + 1);
}
На самом деле он сдеан довольно–таки топорно и куда элегантней, да и правильней, наверное, было бы сделать вложенный цикл, но так мне показалось нагляднее. Можете поэкспериментировать если будет желание. Помните, от row, равной i - 1, до row <= i + 1 выполним от col = j - 1 до col <= j + 1, res += this.hasMine(row, col).
Ладно, вернёмся к тому, как это сделал я. Сначала мы проверяем наличие бомбы непосредственно для ячейки со строкой i и столбцом j, если она там есть, мы возвращаем "*". Если бомбы нет, мы возвращаем сумму результатов функции hasMine для всех соседей. Как вы помните, hasMine возвращает ноль, если бомбы нет и единицу, если она там есть, таким образом, мы вернём именно то, что хотели.
Уже можно играть
Продолжу серию статей про программирование для JavaScript. Сегодняшняя статья рискует закончить что–то вроде учебника по этому языку. В вашего сапёра после этой главы уже можно будет играть. Конечно, останется ещё ряд недоделок, например, стоит добавить сообщения при выигрыше и при проигрыше, нужно также сделать так, чтобы при открытие ячейки, рядом с которой нет бомб автоматически открывались также все её соседи. Ещё было бы не плохо сделать возможность выбирать размер поля и количество бомб на нём, добавить возможность помечать ячейки как ячейки с бомбами.
В общем при большом желании можно ещё писать и писать. Однако, дело в том, что ничего принципиально нового мы при этом не пройдём, с теми знаниями, что мы уже прошли всё это и так можно сделать. Возможно стоит только сделать ряд опций, посмотрим.
Сегодня впервые я решил не приводить весь код программы, так как изменения коснулись только функции initTable. Вот во что она превратилась:
var mines = new minesClass(10, 10, 10);
function checkCell(i, j)
{var tbl = document.getElementById(«tbl»);
var cell = tbl.rows[i].cells[j];
var s = mines.checkCell(i, j);
cell.innerHTML = s;
if (s == "*")
cell.bgColor = 'red';}
function initTable()
{var tbl = document.getElementById(«tbl»);
for (var i = 0; i < mines.rowCount; i++)
{var row = tbl.insertRow(i);
for(var j = 0; j < mines.colCount; j++)
{var cell = row.insertCell(j);
cell.innerHTML = '?';
function SetCellClick(i, j)
{cell.onclick= function() {checkCell(i, j)};};
SetCellClick(i, j);}}}
Давайте по порядку. Строчку «var mines = new minesClass(10, 10, 10);" я перенёс наверх, чтобы переменная mines стала доступна из других функций (напомню, что переменная, объявленная внутри функции доступна только внутри этой функции, а «внешние» переменные доступны также в других функциях). Нам потребуется использовать эту переменную в функции checkCell, которую мы выделили из функции initTable.
По сути, в функции checkCell мы получаем из таблицы ячейку с i-ой строкой и j-ым столбцом и присваиваем ей текст, соответствующий количеству бомб рядом (раньше мы это делали непосредственно при инициализации таблицы). Для получения ссылки на ячейку, мы получаем ссылку на таблицу с помощью метода getElementById заранее предопределённой переменной document. Далее мы получаем ссылку на нужную строку, обращаясь к полю rows таблицы (это массив, поэтому после имени поля в квадратных скобках указываем индекс) и сразу же обращаемся к полю cells строки (это тоже массив) для получения искомой ссылки на ячейку.
Последующие строки функции checkCell мы перенесли из функции initTable без изменений.
Ну и последнее о чём осталось рассказать — функция initTable. Несколько строчек мы оттуда унесли и появилась, несколько добавили:
cell.innerHTML = '?';
function SetCellClick(i, j)
{cell.onclick= function() {checkCell(i, j)};};
SetCellClick(i, j);
С первой строкой, думаю, проблем не будет, а вот дальше придётся применить один интересный приём под названием замыкание. Этот метод нашёл широкое применение в JavaScript, не знаю используется–ли он в других языках, не сталкивался, а тут без него ряд задач сделать крайне сложно.
Так вот, зачем же мы пишем так:
function SetCellClick(i, j)
{cell.onclick= function() {checkCell(i, j)};};
SetCellClick(i, j);
и почему нельзя написать просто так:
cell.onclick= function() {checkCell(i, j)};
Чтобы ответить на этот вопрос, давайте попробуем поставить себя на место интерпретатора. Как ему определить, что вам в качестве параметра функции checkCell нужны именно конкретные значения переменных i и j в данный момент, а не значения этих переменных в момент вызова функции. Не буду сейчас загромождать вам мозг понятием ссылок и указателей, с помощью которых зачастую подобные проблемы решаются в других языках, в JavaScript, насколько мне известно таких понятий нет.
Так вот, чтобы в качестве параметров функции checkCell в будущем брались те значения переменных i и j, которые они имеют в момент присваивания, делается такой хитрый приём: создается местная функция внутри которой и происходит присваивание. Фокус в том, что в этом случае используются не долгоживущие переменные, объявленные в функции initTable, а короткоживущие параметры функции SetCellClick.
Всем пока.
Комментарии к книге «JavaScript. Учебник начального уровня», DarkGoodWIN
Всего 0 комментариев