├── .github └── PULL_REQUEST_TEMPLATE ├── .gitignore ├── README.md ├── SUMMARY.md ├── book.json ├── chapters ├── chapter0.md ├── chapter1.md ├── chapter10.md ├── chapter11.md ├── chapter12.md ├── chapter13.md ├── chapter14.md ├── chapter15.md ├── chapter16.md ├── chapter17.md ├── chapter18.md ├── chapter19.md ├── chapter2.md ├── chapter20.md ├── chapter21.md ├── chapter3.md ├── chapter4.md ├── chapter5.md ├── chapter6.md ├── chapter7.md ├── chapter8.md └── chapter9.md ├── cover.jpg ├── custom └── css │ └── app.css ├── img ├── 0-1.png ├── 1-1.png ├── 13-1.png ├── 13-2.png ├── 13-3.png ├── 13-4.png ├── 15-1.png ├── 15-2.png ├── 16-1.png ├── 16-2.png ├── 16-3.png ├── 16-4.png ├── 16-5.png ├── 19-1.png ├── 19-2.png ├── 2-1.jpg ├── 2-2.png ├── 2-3.png ├── 2-4.png ├── 2-5.png ├── 2-6.png ├── 2-7.png ├── 2-8.png ├── 20-1.png ├── 20-1.svg ├── 21-1.png ├── 21-2.png ├── 3-1.png ├── 3-2.jpg ├── 3-3.jpg ├── 3-4.png ├── 3-5.png ├── 3-6.png ├── 6-0-1.png ├── 6-0-2.jpg ├── 9-1.png ├── 9-2.png └── 9-3.png └── styles └── style.css /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | Make sure these boxes are checked before submitting your issue - thank you! 2 | 3 | - [] no typos 4 | - [] checked with original 5 | - [] have fun 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node rules: 2 | ## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 3 | .grunt 4 | 5 | ## Dependency directory 6 | ## Commenting this out is preferred by some people, see 7 | ## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git 8 | node_modules 9 | 10 | # Book build output 11 | _book 12 | 13 | # eBook build output 14 | *.epub 15 | *.mobi 16 | *.pdf 17 | 18 | gulpfile.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Выразительный Javascript 2 | 2-е издание 3 | 4 | **Автор: Марейн Хавербек 5 | Переводчик: Вячеслав Голованов 6 | Редактор: Антон Кармазин** 7 | 8 | *Распространяется под лицензией [Creative Commons Attribution-Noncommercial](http://creativecommons.org/licenses/by-nc/3.0/). 9 | Исходный код в книге распростроняется под лицензией [MIT](http://opensource.org/licenses/MIT).* 10 | 11 | 12 |










13 |










14 | 15 | #Введение 16 | Эта книга рассказывает, как заставить компьютеры делать то, что вам от них нужно. Компьютеры сегодня так же распространены, как отвёртки – но содержат гораздо больше скрытых сложностей, и поэтому их сложнее понять и с ними сложнее работать. Для многих они остаются чуждыми, слегка угрожающими штуками. 17 | 18 | 19 | 20 | Мы обнаружили два эффективных способа уменьшить коммуникационный разрыв между нами – водянистыми биологическими организмами, у которых есть талант к социальным связям и пространным рассуждениям, и компьютерами – бесчувственными манипуляторами, работающими с бессмысленными данными. Первый – обратиться к нашему ощущению физического мира, и строить интерфейсы, имитирующие его, чтобы мы могли при помощи пальцев манипулировать формами на экране. Для простого взаимодействия с компьютером это неплохо подходит. 21 | 22 | Но мы не нашли хороший способ передавать компьютеру при помощи перемещений и нажатий мышью те вещи, которые дизайнер интерфейса не предусмотрел. Для того, чтобы взаимодействовать с компьютером на более сложных уровнях, например задавать ему произвольные задачи на выполнение, лучше подходит наш талант к общению: мы обучаем компьютер языку. 23 | 24 | Человеческие языки позволяют комбинировать слова великим множеством способов, так, что мы можем сказать очень много разных вещей. Компьютерные языки устроены примерно так же, хотя и менее гибки грамматически. 25 | 26 | За последние 20 лет работа с компьютером стала очень распространённым явлением, и интерфейсы, построенные на языке (а когда-то это был единственный способ общения с компьютером) почти вытеснены графическими. Но они всё ещё есть – если вы знаете, где их искать. Один из таких языков, JavaScript, встроен почти в любой веб-браузер, и потому доступен почти на каждом вычислительном устройстве. 27 | 28 | Эта книга ставит целью познакомить вас с этим языком достаточно для того, чтобы вы могли заставить компьютер делать то, что вам нужно. 29 | 30 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [О программировании](chapters/chapter0.md) 4 | * [Величины, типы и операторы](chapters/chapter1.md) 5 | * [Структура программ](chapters/chapter2.md) 6 | * [Функции](chapters/chapter3.md) 7 | * [Структуры данных: объекты и массивы](chapters/chapter4.md) 8 | * [Функции высшего порядка](chapters/chapter5.md) 9 | * [Тайная жизнь объектов](chapters/chapter6.md) 10 | * [Проект: электронная жизнь](chapters/chapter7.md) 11 | * [Поиск и обработка ошибок](chapters/chapter8.md) 12 | * [Регулярные выражения](chapters/chapter9.md) 13 | * [Модули](chapters/chapter10.md) 14 | * [Проект: язык программирования](chapters/chapter11.md) 15 | * [JavaScript и браузер](chapters/chapter12.md) 16 | * [Document Object Model](chapters/chapter13.md) 17 | * [Обработка событий](chapters/chapter14.md) 18 | * [Проект: игра-платформер](chapters/chapter15.md) 19 | * [Рисование на холсте](chapters/chapter16.md) 20 | * [HTTP](chapters/chapter17.md) 21 | * [Формы и поля форм](chapters/chapter18.md) 22 | * [Проект: Paint](chapters/chapter19.md) 23 | * [Node.js](chapters/chapter20.md) 24 | * [Проект: веб-сайт по обмену опытом](chapters/chapter21.md) -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "anchors", 4 | "ga", 5 | "-disqus", 6 | "typekit@0.0.2" 7 | ], 8 | "pluginsConfig": { 9 | "ga": { 10 | "token": "UA-44401879-3" 11 | }, 12 | "fontSettings": { 13 | "size": 2 14 | }, 15 | "typekit" : { 16 | "kitID": "qvh7ipq" 17 | } 18 | }, 19 | "pdf": { 20 | "fontSize": 18 21 | }, 22 | "disqus": { 23 | "shortName": "eloquentjavascript" 24 | }, 25 | "styles": { 26 | "website": "styles/style.css", 27 | "pdf": "styles/style.css", 28 | "epub": "styles/style.css", 29 | "mobi": "styles/style.css" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /chapters/chapter0.md: -------------------------------------------------------------------------------- 1 | #О программировании 2 | Я не просвещаю тех, кто не жаждет учиться, и не побуждаю тех, кто не хочет искать ответы самостоятельно. Если я покажу один угол квадрата, и они не приходят ко мне с остальными тремя – мне не нужно давать повторных объяснений. 3 | 4 | Конфуций 5 | 6 | Кроме объяснения JavaScript я также хочу объяснить основные принципы программирования. Как выясняется, программировать тяжело. Обычно базовые принципы просты и понятны. Но программы, построенные на этих принципах, становятся сложными настолько, что вводят свои собственные правила и уровни сложности. Вы строите свой собственный лабиринт, и можете в нём потеряться. 7 | 8 | Возможно, временами чтение будет разочаровывать вас. Если вы новичок в программировании, вам нужно будет много чего переварить. Много материала будет скомбинировано таким образом, что вам нужно будет установить новые связи между его частями. 9 | 10 | Вы сами должны обосновать необходимость этих усилий. Если вам тяжело продираться через книгу, не нужно думать о себе плохо. С вами всё в порядке – вам нужно просто продолжать движение. Сделайте перерыв, вернитесь назад – и всегда удостоверяйтесь, что вы прочли и поняли примеры программ. Обучение – это сложная работа, но раз вы что-то выучили, оно уже принадлежит вам, и облегчает дальнейшие шаги. 11 | 12 | Программист создаёт вселенные, за которые он один в ответе. В компьютерных программах могут быть созданы вселенные практически неограниченной сложности. 13 | 14 | Джозеф Вайзенбаум, «Сила компьютеров и Разум людей» 15 | 16 | Программа – сложное понятие. Это кусок текста, набранный программистом, это направляющая сила, заставляющая компьютер что-то делать, это данные в памяти компьютера, и при этом она контролирует работу с этой же самой памятью. Аналогии, которые пытаются сравнивать программы со знакомыми нам объектами обычно не справляются с этим. Одна более-менее подходящая – аналогия с машиной. Множество отдельных частей составляют одно целое, и чтобы заставить её работать, нам нужно представлять себе способы, которыми эти части взаимодействуют и что они привносят в работу целой машины. 17 | 18 | Компьютер – это машина, которая устроена так, чтобы содержать в себе эти нематериальные машинки. Компьютеры сами по себе могут выполнять только простые действия. Польза их в том, что они могут делать это очень быстро. Программа может очень хитрым образом комбинировать эти действия так, чтобы в результате выполнялись очень сложные действия. 19 | 20 | Для некоторых из нас программирование – это увлекательная игра. Программа – это мысленная конструкция. Ничего не стоит её построить, она ничего не весит, и она легко вырастает под нашими пальцами. 21 | 22 | Если не быть осторожным, размер и сложность выходят из-под контроля, запутывая даже того, кто её пишет. Это основная проблема программирования: сохранять контроль над программами. Когда программа работает – это прекрасно. Искусство программирования – это умение контролировать сложность. Большая программа находится под контролем, и выполнена просто в своей сложности. 23 | 24 | Многие программисты верят, что этой сложностью лучше всего управлять, используя в программах небольшой набор хорошо известных техник. Они описали строгие правила («наилучшие практики») того, какую форму программы должны иметь. И самые ревностные среди них считают тех, кто отклоняется от этих практик, плохими программистами. 25 | 26 | Что за враждебность по отношению к богатству программирования – попытки принизить его до чего-то прямолинейного и предсказуемого, наложить табу на всякие странные и прекрасные программы! Ландшафт техник программирования огромен, увлекателен своим разнообразием, и до сих пор изучен мало. Это опасное путешествие, заманивающее и запутывающее неопытного программиста, но это всего лишь означает, что вы должны следовать этим путём осторожно и думать головой. По мере обучения вам всегда будут встречаться новые задачи и новые неизведанные территории. Программисты, не изучающие новое, стагнируют, забывают свою радость, их работа наскучивает им. 27 | 28 | ##Почему язык имеет значение 29 | В начале, при зарождении компьютерных дисциплин, не было языков программирования. Программы выглядели так: 30 | 31 | ``` 32 | 00110001 00000000 00000000 33 | 00110001 00000001 00000001 34 | 00110011 00000001 00000010 35 | 01010001 00001011 00000010 36 | 00100010 00000010 00001000 37 | 01000011 00000001 00000000 38 | 01000001 00000001 00000001 39 | 00010000 00000010 00000000 40 | 01100010 00000000 00000000 41 | ``` 42 | 43 | Это программа, складывающая числа от 1 до 10, и выводящая результат (1 + 2 +… + 10 = 55). Она может выполняться на очень простой гипотетической машине. Для программирования первых компьютеров было необходимо устанавливать большие массивы переключателей в нужные позиции, или пробивать дырки в перфокартах и скармливать их компьютеру. Можете представить, какая это была утомительная, подверженная ошибкам процедура. Написание даже простых программ требовало большого ума и дисциплины. Сложные программы были практически немыслимы. 44 | 45 | Конечно, ручной ввод этих мистических диаграмм бит (нулей и единиц) давал программисту возможность ощутить себя волшебником. И это чего-то стоило в смысле удовлетворения работой. 46 | 47 | Каждая строка указанной программы содержит одну инструкцию. На обычном языке их можно описать так: 48 | 49 | ``` 50 | 1. записать 0 в ячейку памяти 0 51 | 2. записать 1 в ячейку памяти 1 52 | 3. записать значение ячейки 1 в ячейку 2 53 | 4. вычесть 11 из значения ячейки 2 54 | 5. если у ячейки 2 значение 0, тогда продолжить с пункта 9. 55 | 6. добавить значение ячейки 1 к ячейке 0 56 | 7. добавить 1 к ячейке 1 57 | 8. продолжить с пункта 3. 58 | 9. вывести значение ячейки 0 59 | ``` 60 | 61 | Этот вариант легче прочесть, чем кучу бит, но он всё равно не очень удобен. Использование имён вместо номеров инструкций и ячеек памяти может улучшить понимание. 62 | 63 | ``` 64 | установить ‘total’ в 0 65 | установить ‘count’ в 1 66 | [loop] 67 | установить ‘compare’ в ‘count’ 68 | вычесть 11 из ‘compare’ 69 | если ‘compare’ равно нулю, перейти на [end] 70 | добавить ‘count’ к ‘total’ 71 | добавить 1 к ‘count’ 72 | перейти на [loop] 73 | [end] 74 | вывести ‘total’ 75 | ``` 76 | 77 | Вот теперь уже не так сложно понять, как работает программа. Справитесь? Первые две строки назначают двум областям памяти начальные значения. total будет использоваться для подсчёта результата вычисления, а count будет следить за числом, с которым мы работаем в данный момент. Строчки, использующие ‘compare’, наверно, самые странные. Программе нужно понять, не равно ли count 11, чтобы прекратить подсчёт. Так как наша воображаемая машина довольно примитивна, она может только выполнить проверку на равенство переменной нулю, и принять решение о том, надо ли перепрыгнуть на другую строку. Поэтому она использует область памяти под названием ‘compare’, чтобы подсчитать значение count – 11 и принять решение на основании этого значения. Следующие две строки добавляют значение count в счетчик результата и увеличивают count на 1 каждый раз, когда программа решает, что ещё не достигла значения 11. 78 | 79 | Вот та же программа на JavaScript: 80 | 81 | ```js 82 | var total = 0, count = 1; 83 | while (count <= 10) { 84 | total += count; 85 | count += 1; 86 | } 87 | console.log(total); 88 | // → 55 89 | ``` 90 | 91 | Еще несколько улучшений. Главное – нет необходимости вручную обозначать переходы между строками. Конструкция языка while делает это сама. Она продолжает вычислять блок, заключённый в фигурные скобки, пока условие выполняется (count <=10), то есть значение count меньше или равно 10. Уже не нужно создавать временное значение и сравнивать его с нулём. Это было скучно, и сила языков программирования в том, что они помогают избавиться от скучных деталей. 92 | 93 | В конце программы по завершению while к результату применяется операция console.log с целью вывода. 94 | 95 | И наконец, вот так могла бы выглядеть программа, если б у нас были удобные операции range и sum, которые, соответственно, создавали бы набор номеров в заданном промежутке и подсчитывали сумму набора: 96 | 97 | ```js 98 | console.log(sum(range(1, 10))); 99 | // → 55 100 | ``` 101 | 102 | Мораль сей басни – одна и та же программа может быть написана и долго, и коротко, читаемо и нечитаемо. Первая версия программы была совершенно смутной, а последняя – почти настоящий язык – записать сумму диапазона номеров от 1 до 10. В следующих главах мы рассмотрим, как делать такие вещи. 103 | 104 | Хороший язык программирования помогает программисту сообщать компьютеру о необходимых операциях на высоком уровне. Позволяет опускать скучные детали, даёт удобные строительные блоки (while и console.log), позволяет создавать свои собственные блоки (sum и range), и делает простым комбинирование блоков. 105 | 106 | ##Что такое JavaScript? 107 | JavaScript был представлен в 1995 году как способ добавлять программы на веб-страницы в браузере Netscape Navigator. С тех пор язык прижился во всех основных графических браузерах. Он дал возможность появиться современным веб-приложениям – браузерные е-мейл-клиенты, карты, социальные сети. А ещё он используется на более традиционных сайтах для обеспечения интерактивности и всяких наворотов. 108 | 109 | Важно отметить, что JavaScript практически не имеет отношения к другому языку под названием Java. Похожее имя было выбрано из маркетинговых соображений. Когда появился JavaScript, язык Java широко рекламировался и набирал популярность. Кое-кто решил, что неплохо бы прицепиться к этому паровозу. А теперь мы уже никуда не денемся от этого имени. 110 | 111 | После того, как язык вышел за пределы Netscape, был составлен документ, описывающий работу языка, чтобы разные программы, заявляющие о его поддержке, работали одинаково. Он называется стандарт ECMAScript по имени организации ECMA. На практике можно говорить о ECMAScript и JavaScript как об одном и том же. 112 | 113 | Многие ругают JavaScript и говорят о нём много плохого. И многое из этого – правда. Когда мне первый раз пришлось писать программу на JavaScript, я быстро почувствовал отвращение – язык принимал практически всё, что я писал, при этом интерпретировал это вовсе не так, как я подразумевал. В основном это было из-за того, что я не имел понятия о том, что делаю, но тут есть и проблема: JavaScript слишком либерален. Задумывалось это как облегчение программирования для начинающих. В реальности, это затрудняет розыск проблем в программе, потому что система о них не сообщает. 114 | 115 | Гибкость имеет свои преимущества. Она оставляет место для разных техник, невозможных в более строгих языках. Иногда, как мы увидим в главе «модули», её можно использовать для преодоления некоторых недостатков языка. После того, как я по-настоящему изучил и поработал с ним, я научился любить JavaScript. 116 | 117 | Вышло уже несколько версий языка JavaScript. ECMAScript 3 была доминирующей, распространённой версией во время подъёма языка, примерно с 2000 до 2010. В это время готовилась амбициозная 4-я версия, в которой было запланировано несколько радикальных улучшений и расширений языка. Однако политические причины сделали изменение живого популярного языка очень сложным, и работа над 4-й версией была прекращена в 2008. Вместо неё вышла менее амбициозная 5-я версия в 2009. Сейчас большинство браузеров поддерживает 5-ю версию, которую мы и будем использовать в книге. 118 | 119 | JavaScript поддерживают не только браузеры. Базы данных типа MongoDB and CouchDB используют его в качестве скриптового языка и языка запросов. Есть несколько платформ для декстопов и серверов, наиболее известная из которых Node.js, предоставляющие мощное окружение для программирования вне браузера. 120 | 121 | ##Код, и что с ним делать 122 | Код – это текст, из которого состоят программы. В большинстве глав книги есть код. Чтение и написание кода – это неотъемлемая часть обучения программированию. Старайтесь не просто пробегать глазами примеры – читайте их внимательно и разбирайтесь. Сначала это будет происходить медленно и непонятно, но вы быстро овладеете навыками. То же – насчёт упражнений. Не подразумевайте, что вы в них разобрались, пока не напишете работающий вариант. 123 | 124 | Рекомендую пробовать ваши решения в настоящем интерпретаторе языка, чтобы сразу получать отзыв, и, надеюсь, подвергаться искушению поэкспериментировать далее. 125 | 126 | Вы можете установить Node.js и выполнять программы с его помощью. Также вы можете делать это в консоли браузера. В 12 главе будет объяснено, как встраивать программы в HTML-страницы. Также есть сайты типа [jsbin.com](http://jsbin.com), позволяющие просто запускать программы в браузере. На сайте книги есть [песочница для кода](http://eloquentjavascript.net/code). 127 | -------------------------------------------------------------------------------- /chapters/chapter1.md: -------------------------------------------------------------------------------- 1 | #Величины, типы и операторы 2 | 3 | Под поверхностью машины движется программа. Без усилий, она расширяется и сжимается. Находясь в великой гармонии, электроны рассеиваются и собираются. Формы на мониторе – лишь рябь на воде. Суть остаётся скрытой внутри… 4 | 5 | Мастер Юан-Ма, Книга программирования 6 | 7 | В компьютерном мире есть только данные. Можно читать данные, изменять данные, создавать новые – но кроме данных ничего нет. Все данные хранятся как длинные последовательности бит, этим они сходны между собой. 8 | 9 | Биты – это сущности с двумя состояниями, обычно описываемые как нули и единицы. В компьютере они живут в виде высоких и низких электрических зарядов, сильного или слабого сигнала, или блестящего и матового участка на поверхности CD. Каждая часть информации может быть представлена в виде последовательности нулей и единиц, то есть бит. 10 | 11 | К примеру, номер 13. Вместо десятичной системы, состоящей из 10 цифр, у вас есть двоичная система с двумя цифрами. Значение каждой позиции числа удваивается при движении справа налево. Биты, составляющие число 13, вместе с их весами: 12 | 13 | ```js 14 | 0 0 0 0 1 1 0 1 15 | 128 64 32 16 8 4 2 1 16 | ``` 17 | 18 | Получается двоичное число 00001101, или 8 + 4 + 1, что равно 13. 19 | 20 | ##Величины 21 | Представьте океан бит. Типичный современный компьютер хранит более 30 миллиардов бит в оперативной памяти. Постоянная память (жёсткий диск) обычно ещё на пару порядков объёмнее. 22 | 23 | 24 | 25 | Чтобы работать с ними и не заблудиться, вы можете делить их на куски, представляющие единицы информации. В JavaScript эти куски называются величинами. Все они состоят из бит, но играют разные роли. У каждой величины есть тип, определяющий её роль. Всего есть шесть основных типов: числа, строки, булевые величины, объекты, функции и неопределённые величины. 26 | 27 | Для создания величины вам нужно указать её имя. Это удобно. Вам не надо собирать стройматериалы или платить за них. Нужно просто позвать – и оп-па, готово. Они не создаются из воздуха – каждая величина где-то хранится, и если вы хотите использовать огромное их количество, у вас могут закончиться биты. К счастью, это только если они все нужны вам одновременно. Когда величина вам станет не нужна, она растворяется, и использованные ею биты поступают в переработку как стройматериал для новых величин. 28 | 29 | В этой главе мы знакомимся с атомами программ JavaScript – простыми типами величин и операторами, которые к ним применимы. 30 | 31 | ##Числа 32 | Величины числовых типов, это – сюрприз – числа. В программе JavaScript они записываются как 33 | 34 | ```js 35 | 13 36 | ``` 37 | 38 | Используйте эту запись в программе, и она вызовет к жизни в компьютерной памяти цепочку бит, представляющую число 13. 39 | 40 | JavaScript использует фиксированное число бит (64) для хранения численных величин. Число величин, которые можно выразить при помощи 64 бит, ограничено – то есть и сами числа тоже ограничены. Для N десятичных цифр количество чисел, которые ими можно записать, равно 10 в степени N. Аналогично, 64 битами можно выразить 2 в 64 степени чисел. Это довольно много. 41 | 42 | Раньше у компьютеров памяти было меньше, и тогда для хранения чисел использовали группы из 8 или 16 бит. Было легко случайно превысить максимальное число для таких небольших чисел – то есть, использовать число, которое не помещалось в этот набор бит. Сегодня у компьютеров памяти много, можно использовать куски по 64 бит, и значит, вам надо беспокоиться об этом только, если вы работаете с астрономическими числами. 43 | 44 | Правда, не все числа меньше 2^64 помещаются в число JavaScript. В этих битах также хранятся отрицательные числа – поэтому, один бит хранит знак числа. Кроме того, нам нужно иметь возможность хранить дроби. Для этого часть бит используется для хранения позиции десятичной точки. Реальный максимум для чисел – примерно 10^15, что в общем всё равно довольно много. 45 | 46 | Дроби записываются с помощью точки. 47 | 48 | ```js 49 | 9.81 50 | ``` 51 | 52 | Очень большие или маленькие числа записываются научной записью с буквой «e» (exponent), за которой следует степень: 53 | 54 | ```js 55 | 2.998e8 56 | ``` 57 | 58 | Это 2.998 × 10^8 = 299800000. 59 | 60 | Вычисления с целыми числами (которые также называются integer), меньшими, чем 10^15, гарантированно будут точными. Вычисления с дробями обычно нет. Так же, как число π (пи) нельзя представить точно при помощи конечного числа цифр, так и многие дроби нельзя представить в случае, когда у нас есть только 64 бита. Плохо, но это мешает в очень специфических случаях. Важно помнить об этом и относиться к дробям как к приближённым значениям. 61 | 62 | ##Арифметика 63 | Главное, что можно делать с числами – это арифметические вычисления. Сложения и умножения используют два числа и выдают третье. Как это записывается в JavaScript: 64 | 65 | ```js 66 | 100 + 4 * 11 67 | ``` 68 | 69 | Символы + и * называются операторами. Первый – сложение, второй – умножение. Помещаем оператор между двумя величинами и получаем значение выражения. 70 | 71 | А в примере получается «сложить 4 и 100 и затем умножить результат на 11», или умножение выполняется сначала? Как вы могли догадаться, умножение выполняется первым. Но как и в математике, это можно изменить при помощи скобок: 72 | 73 | ```js 74 | (100 + 4) * 11 75 | ``` 76 | 77 | Для вычитания используется оператор -, а для деления - / 78 | 79 | Когда операторы используются без скобок, порядок их выполнения определяется их приоритетом. У операторов * и / приоритет одинаковый, выше, чем у + и -, которые между собой равны по приоритету. При вычислении операторов с равным приоритетом они вычисляются слева направо: 80 | 81 | ```js 82 | 1 - 2 + 1 83 | ``` 84 | 85 | вычисляется как (1 - 2) + 1 86 | 87 | Пока беспокоиться о приоритетах не надо. Если сомневаетесь – используйте скобки. 88 | 89 | Есть ещё один оператор, который вы не сразу узнаете. Символ % используется для получения остатка. X % Y – остаток от деления X на Y. 314 % 100 даёт 14, и 144 % 12 даёт 0. Приоритет у оператора такой же, как у умножения и деления. Математики для операции *нахождения остатка от деления* % могут использовать термин *сравнение по модулю*. 90 | 91 | ##Специальные числа 92 | В JavaScript есть три специальных значения, которые считаются числами, но ведут себя не как обычные числа. 93 | 94 | Это `Infinity` и `-Infinity`, которые представляют положительную и отрицательную бесконечности. Infinity - 1 = Infinity, и так далее. Не надейтесь сильно на вычисления с бесконечностями, они не слишком строгие. 95 | 96 | Третье число: `NaN`. Обозначает «not a number» (не число), хотя это величина числового типа. Вы можете получить её после вычислений типа 0 / 0, Infinity – Infinity, или других операций, которые не ведут к точным осмысленным результатам. 97 | 98 | ##Строки 99 | Следующий базовый тип данных – строки. Они используются для хранения текста. Записываются они в кавычках: 100 | 101 | ```js 102 | "Что посеешь, то из пруда" 103 | 'Баба с возу, потехе час' 104 | ``` 105 | 106 | Можно использовать как двойные, так и одинарные кавычки – главное использовать их вместе. Почти всё можно заключить в кавычки и сделать из этого строку. Но некоторые символы вызывают проблемы. Например, сложно заключить кавычки в кавычки. Перевод строки тоже нельзя просто так заключить в них – строка должна идти одной строкой. 107 | 108 | Для заключения специальных символов используется обратный слеш \\. Он обозначает, что символ, идущий за ним, имеет специальное значение – это называется «экранирование символов» (escape character). \” можно заключать в двойные кавычки. \n обозначает перевод строки, \t – табуляцию. 109 | 110 | Строка “Между первой и второй\nсимвол будет небольшой” на самом деле будет выглядеть так: 111 | 112 | ```js 113 | Между первой и второй 114 | символ будет небольшой 115 | ``` 116 | 117 | Если вам нужно включить в строку обратный слеш, его тоже нужно экранировать: \\\\. Инструкцию “Символ новой строки — это “\n”” нужно будет написать так: 118 | 119 | ```js 120 | "Символ новой строки – это \"\\n\"" 121 | ``` 122 | 123 | Строки нельзя делить, умножать и складывать. Однако с ними можно использовать оператор +, который будет соединять их друг с другом. Следующее выражение выдаст слово «соединение»: 124 | 125 | ```js 126 | "сое" + "ди" + "н" + "ение" 127 | ``` 128 | 129 | Есть много способов манипуляций со строками, которые мы обсудим в главе 4. 130 | 131 | ##Унарные операторы 132 | Не все операторы записываются символами – некоторые словами. Один из таких операторов – `typeof`, который выдаёт название типа величины, к которой он применяется. 133 | 134 | ```js 135 | console.log(typeof 4.5) 136 | // → number 137 | 138 | console.log(typeof "x") 139 | // → string 140 | ``` 141 | 142 | Будем использовать вызов `console.log` в примерах, когда захотим увидеть результат на экране. Как именно будет выдан результат – зависит от окружения, в котором вы запускаете скрипт. 143 | 144 | Предыдущие операторы работали с двумя величинами, однако typeof использует только одну. Операторы, работающие с двумя величинами, называются бинарными, а с одной – унарными. Минус (вычитание) можно использовать и как унарный, и как бинарный. 145 | 146 | ```js 147 | console.log(- (10 - 2)) 148 | // → -8 149 | ``` 150 | 151 | ##Булевские величины 152 | Часто вам нужна величина, которая просто показывает одну из двух возможностей – типа «да» и «нет», или «вкл» и «выкл». Для этого в JavaScript есть тип Boolean, у которого есть всего два значения – `true` и `false` (правда и ложь). 153 | 154 | ##Сравнения 155 | Один из способов получить булевские величины: 156 | 157 | ```js 158 | console.log(3 > 2) 159 | // → true 160 | console.log(3 < 2) 161 | // → false 162 | ``` 163 | 164 | Знаки < и > традиционно обозначают «меньше» и «больше». Это бинарные операторы. В результате их использования мы получаем булевскую величину, которая показывает, является ли неравенство верным. 165 | 166 | Строки можно сравнивать так же: 167 | 168 | ```js 169 | console.log("Арбуз" < "Яблоко") 170 | // → true 171 | ``` 172 | 173 | Строки сравниваются по алфавиту: буквы в верхнем регистре всегда «меньше» букв в нижнем регистре. Сравнение основано на стандарте Unicode. Этот стандарт присваивает номер практически любому символу из любого языка. Во время сравнения строк JavaScript проходит по их символам слева направо, сравнивая номерные коды этих символов. 174 | 175 | Другие сходные операторы – это >= (больше или равно), <= (меньше или равно), == (равно), != (не равно). 176 | 177 | ```js 178 | console.log("Хочется" != "Колется") 179 | // → true 180 | ``` 181 | 182 | В JavaScript есть только одна величина, которая не равна самой себе – `NaN` («не число»). 183 | 184 | ```js 185 | console.log(NaN == NaN) 186 | // → false 187 | ``` 188 | 189 | NaN – это результат любого бессмысленного вычисления, поэтому он не равен результату какого-то другого бессмысленного вычисления. 190 | 191 | Есть операции, которые можно совершать и с самими булевскими значениями. JavaScript поддерживает три логических оператора: и, или, нет. 192 | 193 | Оператор && — логическое «и». Он бинарный, и его результат – правда, только если обе величины, к которым он применяется, тоже правда. 194 | 195 | ```js 196 | console.log(true && false) 197 | // → false 198 | console.log(true && true) 199 | // → true 200 | ``` 201 | 202 | Оператор || — логическое «или». Выдаёт true, если одна из величин true. 203 | 204 | ```js 205 | console.log(false || true) 206 | // → true 207 | console.log(false || false) 208 | // → false 209 | ``` 210 | 211 | «Нет» записывается при помощи восклицательного знака: !. Это унарный оператор, который обращает данную величину на обратную. !true получается false, !false получается true. 212 | 213 | При использовании логических и арифметических операторов не всегда ясно, когда нужны скобки. На практике вы можете справиться с этим, зная, что у || приоритет ниже всех, потом идёт &&, потом операторы сравнения, потом все остальные. Такой порядок был выбран для того, чтобы в выражениях типа следующего можно было использовать минимальное количество скобок: 214 | 215 | ```js 216 | 1 + 1 == 2 && 10 * 10 > 50 217 | ``` 218 | 219 | Последний логический оператор не унарный и не бинарный – он тройной. Записывается при помощи вопросительного знака и двоеточия: 220 | 221 | ```js 222 | console.log(true ? 1 : 2); 223 | // → 1 224 | console.log(false ? 1 : 2); 225 | // → 2 226 | ``` 227 | 228 | Это условный оператор, у которого величина слева от вопросительного знака выбирает одну из двух величин, разделённых двоеточием. Когда величина слева true, выбираем первое значение. Когда false, второе. 229 | 230 | ##Неопределённые значения 231 | Существуют два специальных значения, null и undefined, которые используются для обозначения отсутствия осмысленного значения. Сами по себе они никакой информации не несут. 232 | 233 | Много операторов, которые не выдают значения, возвращают undefined просто для того, чтобы что-то вернуть. Разница между undefined и null появилась в языке случайно, и обычно не имеет значения. 234 | 235 | ##Автоматическое преобразование типов 236 | Ранее я упоминал, что JavaScript позволяет выполнять любые, подчас очень странные программы. К примеру: 237 | 238 | ```js 239 | console.log(8 * null) 240 | // → 0 241 | console.log("5" - 1) 242 | // → 4 243 | console.log("5" + 1) 244 | // → 51 245 | console.log("пять" * 2) 246 | // → NaN 247 | console.log(false == 0) 248 | // → true 249 | ``` 250 | 251 | Когда оператор применяется «не к тому» типу величин, JavaScript втихую преобразовывает величину к нужному типу, используя набор правил, которые не всегда соответствуют вашим ожиданиям. Это называется приведением типов (coercion). В первом выражении null превращается в 0, а “5” становится 5 (из строки – в число). Однако в третьем выражении + выполняет конкатенацию (объединение) строк, из-за чего 1 преобразовывается в “1” (из числа в строку). 252 | 253 | Когда что-то неочевидное превращается в число (к примеру, “пять” или undefined), возвращается значение NaN. Последующие арифметические операции с NaN опять получают NaN. Если вы получили такое значение, поищите, где произошло случайное преобразование типов. 254 | 255 | При сравнении величин одного типа через == легко предсказать, что вы должны получить true, если они одинаковые (исключая случай с NaN). Но когда типы различаются, JavaScript использует сложный и запутанный набор правил для сравнений. Обычно он пытается преобразовать тип одной из величин в тип другой. Когда с одной из сторон оператора возникает null или undefined, он выдаёт true только если обе стороны имеют значение null или undefined. 256 | 257 | ```js 258 | console.log(null == undefined); 259 | // → true 260 | console.log(null == 0); 261 | // → false 262 | ``` 263 | 264 | Последний пример демонстрирует полезный приём. Когда вам надо проверить, имеет ли величина реальное значение вместо null или undefined, вы просто сравниваете её с null при помощи == или !=. 265 | 266 | Но что, если вам надо сравнить нечто с точной величиной? Правила преобразования типов в булевские значения говорят, что 0, NaN и пустая строка “” считаются false, а все остальные – true. Поэтому 0 == false и “” == false. В случаях, когда вам не нужно автоматическое преобразование типов, можно использовать ещё два оператора: === и !==. Первый проверяет, что две величины абсолютно идентичны, второй – наоборот. И тогда сравнение “” === false возвращает false. 267 | 268 | Рекомендую использовать трёхсимвольные операторы сравнения для защиты от неожиданных преобразований типов, которые могут привести к непредсказуемым последствиям. Если вы уверены, что типы сравниваемых величин будут совпадать, можно спокойно использовать короткие операторы. 269 | 270 | ##Короткое вычисление логических операторов 271 | Логические операторы && и || работают с величинами разных типов очень странным образом. Они преобразуют величину с левой стороны оператора в булевскую, чтобы понять, что делать дальше, но в зависимости от оператора и от результата этого преобразования, возвращают оригинальное значение либо левой, либо правой части. 272 | 273 | К примеру, || вернёт значение с левой части, когда его можно преобразовать в true – а иначе вернёт правую часть. 274 | 275 | ```js 276 | console.log(null || "user") 277 | // → user 278 | console.log("Karl" || "user") 279 | // → Karl 280 | ``` 281 | 282 | Такая работа оператора || позволяет использовать его как откат к значению по умолчанию. Если вы дадите ему выражение, которое может вернуть пустое значение слева, то значение справа будет служить заменой на этот случай. 283 | 284 | Оператор && работает сходным образом, но наоборот. Если величина слева преобразовывается в false, он возвращает эту величину, а иначе – величину справа. 285 | 286 | Ещё одно важное их свойство – выражение в правой части вычисляется только при необходимости. В случае true || X неважно, чему равно X. Даже если это какое-то ужасное выражение. Результат всегда true и X не вычисляется. Так же работает false && X – X просто игнорируется. Это называется коротким вычислением. 287 | 288 | Оператор условия работает так же. Первое выражение всегда вычисляется, а из второго и третьего значения – только то, которое оказывается выбранным в результате. 289 | 290 | ##Итог 291 | Мы рассмотрели четыре типа величин JavaScript: числа, строки, булевские и неопределённые. 292 | 293 | Эти величины получаются, когда мы пишем их имена (true, null) или значения (13, “ёпрст”). Их можно комбинировать и изменять при помощи операторов. Для арифметики есть бинарные операторы (+, -, *, / и %), объединение строк (+), сравнение (==, !=, ===, !==, <, >, <=, >=) и логические операторы (&&, ||), а также несколько унарных операторов (- для отрицательного значения, ! для логического отрицания и typeof для определения типа величины). 294 | 295 | Эти знания позволяют использовать JavaScript в качестве калькулятора, но и только. В следующей главе мы будем связывать эти простые значения вместе, чтобы составлять простые программы. 296 | -------------------------------------------------------------------------------- /chapters/chapter10.md: -------------------------------------------------------------------------------- 1 | #Модули 2 | 3 | Начинающий программист пишет программы так, как муравьи строят муравейник – по кусочку, без размышления над общей структурой. Его программы как песок. Они могут недолго простоять, но вырастая, они разваливаются. 4 | Поняв проблему, программист тратит много времени на размышления о структуре. Его программы получаются жёстко структурированными, как каменные изваяния. Они тверды, но когда их нужно менять, над ними приходится совершать насилие. 5 | Мастер-программист знает, когда нужна структура, а когда нужно оставить вещи в простом виде. Его программы словно глина – твёрдые, но податливые. 6 | 7 | Мастер Юан-Ма, Книга программирования 8 | 9 | У каждой программы есть структура. В частностях она определяется тем, как программист делит код на функции и блоки внутри этих функций. Программисты вольны в создании структуры своей программы. Структура определяется больше вкусом программиста, нежели функциональностью программы. 10 | 11 | В случае больших программ отдельные функции уже теряются в коде, и нам необходима единица организации кода больших масштабов. Модули группируют программный код по каким-то определённым признакам. В этой главе мы рассмотрим преимущества такого деления и техники создания модулей в JavaScript. 12 | 13 | ##Зачем нужны модули 14 | Есть несколько причин, по которым авторы делят свои книги на главы и секции. Это помогает читателю понять, как построена книга, и найти нужные им части. Автору это помогает концентрироваться на каждой конкретной части. Преимущества организации программ в нескольких файлах, или модулях, примерно те же. Структуризация помогает незнакомым с кодом людям найти то, что им нужно, и помогает программистам хранить связанные друг с другом вещи в одном месте. 15 | 16 | Некоторые программы организованы по модели обычного текста, где порядок следования чётко определён, и где читателю предлагается последовательное изучение программы и множество прозы (комментариев) для описания кода. Это делает чтение кода менее пугающим (а чтение чужого кода обычно пугает), но требует больших усилий при создании кода. Также такую программу сложнее менять, потому что части прозы связаны между собой сильнее, чем части кода. Этот стиль называется литературным программированием. Те главы книги, в которых обсуждаются проекты, можно считать литературным кодом. 17 | 18 | Обычно структурирование чего-либо требует затрат энергии. На ранних стадиях проекта, когда вы ещё не уверены, что где будет, и какие модули вообще нужны, я пропагандирую бесструктурную минималистическую организацию кода. Просто размещайте все части там, где удобно, пока код не стабилизируется. Таким образом не придётся тратить время на перестановку кусков программы, и вы не поймаете себя в такую структуру, которая не подходит для вашей программы. 19 | 20 | ##Пространство имён 21 | У большинства современных ЯП есть промежуточные области видимости (ОВ) между глобальной (видно всем) и локальной (видно только этой функции). У JavaScript такого нет. По умолчанию, всё, что необходимо видеть снаружи функции верхнего уровня, находится в глобальной ОВ. 22 | 23 | Загрязнение пространства имён (ПИ), когда не связанные друг с другом части кода делят один набор переменных, упоминалась в главе 4. Там объект Math был приведён в качестве примера объекта, который группирует функциональность, связанную с математикой, в виде модуля. 24 | 25 | Хотя JavaScript не предлагает непосредственно конструкции для создания модуля, мы можем использовать объекты для создания подпространств имён, доступных отовсюду. А функции можно использовать для создания изолированных частных пространств имён внутри модуля. Чуть дальше мы обсудим способ построения достаточно удобных модулей, изолирующих ПИ при помощи базовых концепций языка. 26 | 27 | ##Повторное использование 28 | В проекте, не разбитом на модули, непонятно, какие части кода необходимы для конкретной функции. В моей программе, шпионящей за врагами (глава 9), я написал функцию чтения файлов с настройками. Если мне понадобится использовать её в другом проекте, я должен буду скопировать части старой программы, которые вроде бы связаны с этой функцией, в мой новый проект. А если я найду там ошибку, я её исправлю только в том проекте, над которым работаю в данный момент, и забуду исправить её во всех остальных. 29 | 30 | Когда у вас будет множество таких сдублированных кусков кода, вы обнаружите, что тратите кучу времени и сил на их копирование и обновление. Если разместить связанные между собой части программ в отдельные модули, их будет проще отслеживать, исправлять и обновлять, потому что везде, где этот функционал потребуется, вы сможете просто загрузить этот модуль из файла. 31 | 32 | Эту идею можно использовать ещё лучше, если чётко прописать взаимоотношения разных модулей – кто от кого зависит. Тогда можно автоматизировать процесс установки и обновления внешних модулей (библиотек). 33 | 34 | Если ещё развить идею – представьте себе онлайн-сервис, который отслеживает и распространяет сотни тысяч таких библиотек, и вы можете искать нужную вам функциональность среди них, а когда найдёте – ваш проект автоматически скачает её. 35 | 36 | И такой сервис есть! Он называется NPM (npmjs.org). NPM – онлайн-база модулей и инструмент для скачивания и апгрейда модулей, от которых зависит ваша программа. Он вырос из Node.js, окружения JavaScript, не требующего браузера, которое мы обсудим в главе 20, но также может использоваться и в браузерных программах. 37 | 38 | ##Устранение связей (Decoupling) 39 | Ещё одна задача модулей – изолировать несвязанные между собой части кода так, как это делают интерфейсы объектов. Хорошо продуманный модуль предоставляет интерфейс для его использования вовне. Когда модуль обновляют или исправляют, существующий интерфейс остаётся неизменным, чтобы другие модули могли использовать новую, обновлённую версию без изменений в них самих. 40 | 41 | Стабильный интерфейс не означает, что в него не добавляют новые функции, методы или переменные. Главное, что существующая функциональность не удаляется и её смысл не меняется. Хороший интерфейс позволяет модулю расти, не ломая старый интерфейс. А это значит – выставлять наружу как можно меньше внутренней кухни модуля, при этом язык интерфейса должен быть достаточно гибким и мощным для применения в различных ситуациях. 42 | 43 | Интерфейсы, выполняющие простую задачу, вроде чтения настроек из файла, выходят такими естественным образом. Для других – к примеру, для редактора текстов, у которого есть множество разных аспектов, требующих доступа извне (содержимое, стили, действия пользователя и т.п.) интерфейс необходимо скрупулёзно продумывать. 44 | 45 | ##Использование функций в качестве пространств имён 46 | Функции – единственная вещь в JavaScript, создающая новую область видимости. Если нам нужно, чтобы у модулей была своя область видимости, придётся основывать их на функциях. 47 | 48 | Обратите внимание на простейший модуль, связывающий имена с номерами дней недели – как делает метод getDay объекта Date. 49 | 50 | ```js 51 | var names = ["Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье"]; 52 | function dayName(number) { 53 | return names[number]; 54 | } 55 | 56 | console.log(dayName(1)); 57 | // → Вторник 58 | ``` 59 | 60 | Функция dayName – часть интерфейса модуля, а переменная names – нет. Но хотелось бы не загрязнять глобальное пространство имён. 61 | 62 | Можно сделать так: 63 | 64 | ```js 65 | var dayName = function() { 66 | var names = ["Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье"]; 67 | return function(number) { 68 | return names[number]; 69 | }; 70 | }(); 71 | 72 | console.log(dayName(3)); 73 | // → Четверг 74 | ``` 75 | 76 | Теперь names – локальная переменная безымянной функции. Функция создаётся и сразу вызывается, а её возвращаемое значение (уже нужная нам функция dayName) хранится в переменной. Мы можем написать много страниц кода в функции, объявить там сотню переменных, и все они будут внутренними для нашего модуля, а не для внешнего кода. 77 | 78 | Подобный шаблон можно использовать для изолирования кода. Следующий модуль пишет в консоль значение, но не предоставляет никаких значений для использования другими модулями. 79 | 80 | ```js 81 | (function() { 82 | function square(x) { return x * x; } 83 | var hundred = 100; 84 | 85 | console.log(square(hundred)); 86 | })(); 87 | // → 10000 88 | ``` 89 | 90 | Этот код выводит квадрат сотни, но в реальности это мог бы быть модуль, добавляющий метод к какому-то прототипу, или настраивающий виджет на веб-странице. Он обёрнут в функцию для предотвращения загрязнения глобальной ОВ используемыми им переменными. 91 | 92 | А зачем мы заключили функцию в круглые скобки? Это связано с глюком синтаксиса JavaScript. Если выражение начинается с ключевого слова function, это функциональное выражение. А если инструкция начинается с function, это объявление функции, которое требует названия, и, так как это не выражение, не может быть вызвано при помощи скобок () после неё. Можно представлять себе заключение в скобки как трюк, чтобы функция принудительно интерпретировалась как выражение. 93 | 94 | ##Объекты в качестве интерфейсов 95 | 96 | Представьте, что нам надо добавить ещё одну функцию в наш модуль «день недели». Мы уже не можем возвращать функцию, а должны завернуть две функции в объект. 97 | 98 | ```js 99 | var weekDay = function() { 100 | var names = ["Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье"]; 101 | return { 102 | name: function(number) { return names[number]; }, 103 | number: function(name) { return names.indexOf(name); } 104 | }; 105 | }(); 106 | 107 | console.log(weekDay.name(weekDay.number("Воскресенье"))); 108 | // → Воскресенье 109 | ``` 110 | 111 | Когда модуль большой, собирать все возвращаемые значения в объект в конце функции неудобно, потому что многие возвращаемые функции будут большими, и вам было бы удобнее их записывать где-то в другом месте, рядом со связанным с ними кодом. Удобно объявить объект (обычно называемый exports) и добавлять к нему свойства каждый раз, когда нам надо что-то экспортировать. В следующем примере функция модуля принимает объект интерфейса как аргумент, позволяя коду снаружи функции создать его и сохранить в переменной. Снаружи функции this ссылается на объект глобальной области видимости. 112 | 113 | ```js 114 | (function(exports) { 115 | var names = ["Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье"]; 116 | 117 | exports.name = function(number) { 118 | return names[number]; 119 | }; 120 | exports.number = function(name) { 121 | return names.indexOf(name); 122 | }; 123 | })(this.weekDay = {}); 124 | 125 | console.log(weekDay.name(weekDay.number("Четверг"))); 126 | // → Четверг 127 | ``` 128 | 129 | ##Отсоединяемся от глобальной области видимости 130 | Такой шаблон часто используется в модулях JavaScript, предназначающихся для браузера. Модуль возьмёт одну глобальную переменную и обернёт свой код в функцию, чтобы у него было своё личное пространство имён. Но с этим шаблоном бывают проблемы, когда много модулей требуют одно и то же имя, или когда вам надо загрузить две версии модуля одновременно. 131 | 132 | Подкрутив кое-что, мы можем сделать систему, разрешающую одному модулю обращаться к интерфейсному объекту другого, без выхода в глобальную ОВ. Наша цель – функция require, которая, получая имя модуля, загрузит его файл (с диска или из сети, в зависимости от платформы) и вернёт соответствующее значение с интерфейсом. 133 | 134 | Этот подход решает проблемы, упомянутые ранее, и у него есть ещё одно преимущество – зависимости вашей программы становятся явными, и поэтому сложнее случайно вызвать ненужный вам модуль без чёткого его объявления. 135 | 136 | Нам понадобятся две вещи. Во-первых, функция readFile, возвращающая содержимое файла в виде строки. В стандартном JavaScript такой функции нет, но разные окружения, такие как браузер или Node.js, предоставляют свои способы доступа к файлам. Пока притворимся, что у нас есть такая функция. Во-вторых, нам нужна возможность выполнить содержимое этой строки как код. 137 | 138 | ##Выполняем данные как код 139 | Есть несколько способов получить данные (строку кода) и выполнить их как часть текущей программы. 140 | 141 | Самый очевидный – оператор eval, который выполняет строку кода в текущем окружении. Это плохая идея – он нарушает некоторые свойства окружения, которые обычно у него есть, например изоляция от внешнего мира. 142 | 143 | ```js 144 | function evalAndReturnX(code) { 145 | eval(code); 146 | return x; 147 | } 148 | 149 | console.log(evalAndReturnX("var x = 2")); 150 | // → 2 151 | ``` 152 | 153 | Способ лучше – использовать конструктор Function. Он принимает два аргумента – строку, содержащую список имён аргументов через запятую, и строку, содержащую тело функции. 154 | 155 | ```js 156 | var plusOne = new Function("n", "return n + 1;"); 157 | console.log(plusOne(4)); 158 | // → 5 159 | ``` 160 | 161 | Это то, что нам надо. Мы обернём код модуля в функцию, и её область видимости станет областью видимости нашего модуля. 162 | 163 | ##Require 164 | Вот минимальная версия функции require: 165 | 166 | ```js 167 | function require(name) { 168 | var code = new Function("exports", readFile(name)); 169 | var exports = {}; 170 | code(exports); 171 | return exports; 172 | } 173 | 174 | console.log(require("weekDay").name(1)); 175 | // → Вторник 176 | ``` 177 | 178 | Так как конструктор new Function оборачивает код модуля в функцию, нам не надо писать функцию, оборачивающую пространство имён, внутри самого модуля. А так как exports является аргументом функции модуля, модулю не нужно его объявлять. Это убирает много мусора из нашего модуля-примера. 179 | 180 | ```js 181 | var names = ["Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье"]; 182 | 183 | exports.name = function(number) { 184 | return names[number]; 185 | }; 186 | exports.number = function(name) { 187 | return names.indexOf(name); 188 | }; 189 | ``` 190 | 191 | При использовании такого шаблона модуль обычно начинается с объявления нескольких переменных, которые загружают модули, от которых он зависит. 192 | 193 | ```js 194 | var weekDay = require("weekDay"); 195 | var today = require("today"); 196 | 197 | console.log(weekDay.name(today.dayNumber())); 198 | ``` 199 | 200 | У такого простого варианта require есть недостатки. Во-первых, он загрузит и выполнит модуль каждый раз, когда его грузят через require – если у нескольких модулей есть одинаковые зависимости, или вызов require находится внутри функции, которая вызывается многократно, будет потеряно время и энергия. 201 | 202 | Это можно решить, храня уже загруженные модули в объекте, и возвращая существующее значение, когда он грузится несколько раз. 203 | 204 | Вторая проблема – модуль не может экспортировать переменную напрямую, только через объект export. К примеру, модулю может потребоваться экспортировать только конструктор объекта, объявленного в нём. Сейчас это невозможно, поскольку require всегда использует объект exports в качестве возвращаемого значения. 205 | 206 | Традиционное решение – предоставить модули с другой переменной, module, которая является объектом со свойством exports. Оно изначально указывает на пустой объект, созданный require, но может быть перезаписано другим значением, чтобы экспортировать что-либо ещё. 207 | 208 | ```js 209 | function require(name) { 210 | if (name in require.cache) 211 | return require.cache[name]; 212 | 213 | var code = new Function("exports, module", readFile(name)); 214 | var exports = {}, module = {exports: exports}; 215 | code(exports, module); 216 | 217 | require.cache[name] = module.exports; 218 | return module.exports; 219 | } 220 | require.cache = Object.create(null); 221 | ``` 222 | 223 | Сейчас у нас есть система модулей, использующих одну глобальную переменную require, чтобы позволять модулям искать и использовать друг друга без выхода в глобальную область видимости. 224 | 225 | Такой стиль системы модулей называется CommonJS, по имени псевдостандарта, который первым его описал. Он встроен в систему Node.js. Настоящие реализации делают гораздо больше описанного мною. Главное, что у них есть более умный способ перехода от имени модуля к его коду, который разрешает загружать модули по относительному пути к файлу, или же по имени модуля, указывающему на локально установленные модули. 226 | 227 | ##Медленная загрузка модулей 228 | Хотя и возможно использовать стиль CommonJS для браузера, но он не очень подходит для этого. Загрузка файла из Сети происходит медленнее, чем с жёсткого диска. Пока скрипт в браузере работает, на сайте ничего другого не происходит (по причинам, которые станут ясны к 14 главе). Значит, если бы каждый вызов require скачивал что-то с дальнего веб-сервера, страница бы зависла на очень долгое время при загрузке. 229 | 230 | Можно обойти это, запуская программу типа Browserify с вашим кодом перед выкладыванием её в веб. Она просмотрит все вызовы require, обработает все зависимости и соберёт нужный код в один большой файл. Веб-сайт просто грузит этот файл и получает все необходимые модули. 231 | 232 | Второй вариант – оборачивать код модуля в функцию, чтобы загрузчик модулей сначала грузил зависимости в фоне, а потом вызывал функцию, инициализирующую модуль, после загрузки зависимостей. Этим занимается система AMD (асинхронное определение модулей). 233 | 234 | Наша простая программа с зависимостями выглядела бы в AMD так: 235 | 236 | ```js 237 | define(["weekDay", "today"], function(weekDay, today) { 238 | console.log(weekDay.name(today.dayNumber())); 239 | }); 240 | ``` 241 | 242 | Функция define здесь самая важная. Она принимает массив имён модулей, а затем функцию, принимающую один аргумент для каждой из зависимостей. Она загрузит зависимости (если они не были загружены ранее) в фоне, позволяя странице работать, пока файлы скачиваются. Когда всё загружено, define вызывает данную ему функцию, с интерфейсами этих зависимостей в качестве аргументов. 243 | 244 | Загруженные таким образом модули должны содержать вызовы define. В качестве их интерфейса используется то, что было возвращено функцией, переданной в define. Вот модуль weekDay: 245 | 246 | ```js 247 | define([], function() { 248 | var names = ["Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье"]; 249 | return { 250 | name: function(number) { return names[number]; }, 251 | number: function(name) { return names.indexOf(name); } 252 | }; 253 | }); 254 | ``` 255 | 256 | Чтобы показать минимальную реализацию define, притворимся, что у нас есть функция backgroundReadFile, которая принимает имя файла и функцию, и вызывает эту функцию с содержимым этого файла, как только он будет загружен. (В главе 17 будет объяснено, как написать такую функцию). 257 | 258 | Чтоб отслеживать модули, пока они загружаются, define использует объекты, описывающие состояние модулей, сообщает нам, доступны ли они уже, и предоставляет их интерфейс по доступности. 259 | 260 | Функция getModule принимает имя и возвращает такой объект, и убеждается в том, что модуль поставлен в очередь загрузки. Она использует кеширующий объект, чтобы не грузить один модуль дважды. 261 | 262 | ```js 263 | var defineCache = Object.create(null); 264 | var currentMod = null; 265 | 266 | function getModule(name) { 267 | if (name in defineCache) 268 | return defineCache[name]; 269 | 270 | var module = {exports: null, 271 | loaded: false, 272 | onLoad: []}; 273 | defineCache[name] = module; 274 | backgroundReadFile(name, function(code) { 275 | currentMod = module; 276 | new Function("", code)(); 277 | }); 278 | return module; 279 | } 280 | ``` 281 | 282 | Мы предполагаем, что загружаемый файл тоже содержит вызов define. Переменная currentMod используется, чтобы сообщить этому вызову о загружаемом объекте модуля, чтобы тот смог обновить этот объект после загрузки. Мы ещё вернёмся к этому механизму. 283 | 284 | Функция define сама использует getModule для загрузки или создания объектов модулей для зависимостей текущего модуля. Её задача – запланировать запуск функции moduleFunction (содержащей сам код модуля) после загрузки зависимостей. Для этого она определяет функцию whenDepsLoaded, добавляемую в массив onLoad, содержащий все пока ещё не загруженные зависимости. Эта функция сразу прекращает работу, если есть ещё незагруженные зависимости, так что она выполнит свою работу только раз, когда последняя зависимость загрузится. Она также вызывается сразу из самого define, в случае когда никакие зависимости не нужно грузить. 285 | 286 | ```js 287 | function define(depNames, moduleFunction) { 288 | var myMod = currentMod; 289 | var deps = depNames.map(getModule); 290 | 291 | deps.forEach(function(mod) { 292 | if (!mod.loaded) 293 | mod.onLoad.push(whenDepsLoaded); 294 | }); 295 | 296 | function whenDepsLoaded() { 297 | if (!deps.every(function(m) { return m.loaded; })) 298 | return; 299 | 300 | var args = deps.map(function(m) { return m.exports; }); 301 | var exports = moduleFunction.apply(null, args); 302 | if (myMod) { 303 | myMod.exports = exports; 304 | myMod.loaded = true; 305 | myMod.onLoad.every(function(f) { f(); }); 306 | } 307 | } 308 | whenDepsLoaded(); 309 | } 310 | ``` 311 | 312 | Когда все зависимости доступны, whenDepsLoaded вызывает функцию, содержащую модуль, передавая в виде аргументов интерфейсы зависимостей. 313 | 314 | Первое, что делает define, это сохраняет значение currentMod, которое было у него при вызове, в переменной myMod. Вспомните, что getModule прямо перед исполнением кода модуля сохранил соответствующий объект модуля в currentMod. Это позволяет whenDepsLoaded хранить возвращаемое значение функции модуля в свойстве exports этого модуля, установить свойство loaded модуля в true, и вызвать все функции, ждавшие загрузки модуля. 315 | 316 | Этот код изучать тяжелее, чем функцию require. Его выполнение идёт не по простому и предсказуемому пути. Вместо этого, несколько операций должны быть выполнены в неопределённые моменты в будущем, что затрудняет изучения того, как выполняется этот код. 317 | 318 | Настоящая реализация AMD гораздо умнее подходит к превращению имён модулей в URL и более надёжна, чем показано в примере. Проект RequireJS предоставляет популярную реализацию такого стиля загрузчика модулей. 319 | 320 | ##Разработка интерфейса 321 | Разработка интерфейсов – один из самых тонких моментов в программировании. Любую нетривиальную функциональность можно реализовать множеством способов. Поиск работающего способа требует проницательности и предусмотрительности. 322 | 323 | Лучший способ познать значимость хорошего интерфейса – использовать много интерфейсов. Некоторые будут плохие, некоторые хорошие. Опыт покажет вам, что работает, а что – нет. Никогда не принимайте как должное плохой интерфейс. Исправьте его, или заключите в другой интерфейс, который лучше вам подходит. 324 | 325 | ##Предсказуемость 326 | Если программист может предсказать, как работает ваш интерфейс, ему не придётся часто отвлекаться и смотреть подсказку по его использованию. Постарайтесь следовать общепринятым соглашениям. Если есть модуль или часть языка JavaScript, которая делает что-то похожее на то, что вы пытаетесь реализовать – будет неплохо, если ваш интерфейс будет напоминать существующий. Таким образом, он будет привычен для людей, знакомых с существующим интерфейсом. 327 | 328 | В поведении вашего кода предсказуемость также важна. Вас может постичь искушение сделать интерфейс слишком заумным якобы потому, что его удобнее использовать. К примеру, вы можете принимать любые виды типов и комбинаций аргументов и проделывать с ними «то, что надо». Или предоставлять десятки специализированных функций, которые предлагают незначительно отличающуюся функциональность. Это может сделать код, опирающийся на ваш интерфейс, немного короче, зато затруднить людям, работающим с ним, строить чёткую мысленную модель работы вашего модуля. 329 | 330 | ##Компонуемость 331 | Старайтесь использовать в интерфейсах настолько простые структуры данных, насколько это возможно. Делайте так, чтобы функции выполняли простые и понятные вещи. Если это применимо, делайте функции чистыми (см. Главу 3). 332 | 333 | К примеру, частенько модули предлагают свою версию массивоподобных коллекций объектов со своим интерфейсом для подсчёта и извлечения элементов. У таких объектов нет методов map или forEach, и никакая функция, ожидающая настоящий массив, не сможет с ними работать. Это пример плохой компонуемости – модуль нельзя легко скомпоновать с другим кодом. 334 | 335 | Примером может служить модуль для орфографической проверки текста, который может пригодиться в текстовом редакторе. Проверочный модуль можно сделать таким, чтобы он работал с любыми сложными структурами, используемыми самим редактором, и вызывал внутренние функции редактора для предоставления пользователю выбора вариантов написания. Если вы поступите таким образом, модуль нельзя будет использовать с другими программами. С другой стороны, если мы определим интерфейс модуля проверки, который принимает простую строку и возвращает позицию, на которой в строке есть возможная ошибка, а впридачу – массив предлагаемых поправок, тогда у нас будет интерфейс, который можно скомпоновать с другими системами, потому что строки и массивы всегда доступны в JavaScript. 336 | 337 | ##Многослойные интерфейсы 338 | Разрабатывая интерфейс для сложной системы (к примеру, отправка емейл), часто приходишь к дилемме. С одной стороны, не нужно перегружать пользователя интерфейса деталями. Не надо заставлять их изучать его 20 минут перед тем, как они смогут отправить емейл. С другой стороны, не хочется и прятать все детали – когда людям надо сделать что-то сложное при помощи вашего модуля, у них должна быть такая возможность. 339 | 340 | Часто приходится предлагать два интерфейса: детализированный низкоуровневый для сложных ситуаций, и простой высокоуровневый для обычного использования. Второй можно построить на основе первого. В модуле для отправки емейлов высокоуровневый интерфейс может быть просто функцией, которая принимает сообщение, адрес получателя и отправителя, и отправляет письмо. Низкоуровневый должен давать доступ к заголовкам, приложенным файлам, HTML письмам и т.д. 341 | 342 | ##Итог 343 | Модули позволяют структурировать большие программы, разделяя код по разным файлам и пространствам имён. Если обеспечить их хорошо разработанными интерфейсами, их будет просто использовать, применять в других проектах и продолжать использовать при развитии и эволюции самого проекта. 344 | 345 | Хотя JavaScript совершенно не помогает делать модули, его гибкие функции и объекты позволяют сделать достаточно неплохую систему модулей. Область видимости функций используется как внутреннее пространство имён модуля, а объекты используются для хранения наборов переменных. 346 | 347 | Есть два популярных подхода к использованию модулей. Один – CommonJS, построенный на функции require, которая вызывает модули по имени и возвращает их интерфейс. Другой – AMD, использующий функцию define, принимающую массив имён модулей и, после их загрузки, исполняющую функцию, аргументами которой являются их интерфейсы. 348 | 349 | ##Упражнения 350 | ###Названия месяцев 351 | Напишите простой модуль типа weekday, преобразующий номера месяцев (начиная с нуля) в названия и обратно. Выделите ему собственное пространство имён, т.к. ему потребуется внутренний массив с названиями месяцев, и используйте чистый JavaScript, без системы загрузки модулей. 352 | 353 | ```js 354 | // Ваш код 355 | 356 | console.log(month.name(2)); 357 | // → March 358 | console.log(month.number("November")); 359 | // → 10 360 | ``` 361 | 362 | ###Вернёмся к электронной жизни 363 | Надеюсь, что глава 7 ещё не стёрлась из вашей памяти. Вернитесь к разработанной там системе и предложите способ разделения кода на модули. Чтобы освежить вам память – вот список функций и типов, по порядку появления: 364 | 365 | ``` 366 | Vector 367 | Grid 368 | directions 369 | directionNames 370 | randomElement 371 | BouncingCritter 372 | elementFromChar 373 | World 374 | charFromElement 375 | Wall 376 | View 377 | WallFollower 378 | dirPlus 379 | LifelikeWorld 380 | Plant 381 | PlantEater 382 | SmartPlantEater 383 | Tiger 384 | ``` 385 | 386 | Не надо создавать слишком много модулей. Книга, в которой на каждой странице была бы новая глава, действовала бы вам на нервы (хотя бы потому, что всё место съедали бы заголовки). Не нужно делать десять файлов для одного мелкого проекта. Рассчитывайте на 3-5 модулей. 387 | 388 | Некоторые функции можно сделать внутренними, недоступными из других модулей. Правильного варианта здесь не существует. Организация модулей – вопрос вкуса. 389 | 390 | ###Круговые зависимости 391 | Запутанная тема в управлении зависимостями – круговые зависимости, когда модуль А зависит от Б, а Б зависит от А. Многие системы модулей это просто запрещают. Модули CommonJS допускают ограниченный вариант: это работает, пока модули не заменяют объект exports, существующий по-умолчанию, другим значением, и начинают использовать интерфейсы друг друга только после окончания загрузки. 392 | 393 | Можете ли вы придумать способ, который позволил бы воплотить систему поддержки таких зависимостей? Посмотрите на определение require и подумайте, что нужно сделать этой функции для этого. 394 | -------------------------------------------------------------------------------- /chapters/chapter11.md: -------------------------------------------------------------------------------- 1 | #Проект: язык программирования 2 | 3 | То, что проверяет и определяет смысл выражений в языке программирования, является в свою очередь просто программой. 4 | 5 | Хэл Абельсон и Жеральд Сасман, «Структура и интерпретация компьютерных программ». 6 | 7 | Когда ученик спросил учителя о природе цикла Данных и Контроля, Юань-Ма ответил: «Подумай о компиляторе, компилирующем самого себя». 8 | 9 | Мастер Юань-Ма, «Книга программирования» 10 | 11 | Создать свой язык программирования удивительно легко (пока вы не ставите запредельных целей) и довольно поучительно. 12 | 13 | Главное, что я хочу продемонстрировать в этой главе – в построении языка нет никакой магии. Мне часто казалось, что некоторые человеческие изобретения настолько сложны и заумны, что мне их никогда не понять. Однако после небольшого самообразования и ковыряния такие штуки часто оказываются довольно обыденными. 14 | 15 | Мы построим язык программирования Egg (Яйцо). Он будет небольшим, простым, но достаточно мощным для выражения любых расчётов. Он также будет осуществлять простые абстракции, основанные на функциях. 16 | 17 | ##Разбор (parsing) 18 | То, что лежит на поверхности языка – синтаксис, запись. Грамматический анализатор, или парсер – программа, читающая кусок текста и выдающая структуру данных, описывающую структуры программы, содержавшейся в тексте. Если текст не описывает корректную программу, парсер должен пожаловаться и указать на ошибку. 19 | 20 | У нашего языка будет простой и однородный синтаксис. В Egg всё будет являться выражением. Выражение может быть переменной, число, строка или приложение. Приложения используются для вызова функций и конструкций типа if или while. 21 | 22 | Для упрощения парсинга строки в Egg не будут поддерживать обратных слешей и подобных вещей. Строка – просто последовательность символов, не являющихся двойными кавычками, заключённая в двойные кавычки. Число – последовательность цифр. Имена переменных могут состоять из любых символов, не являющихся пробелами и не имеющих специального значения в синтаксисе. 23 | 24 | Приложения записываются так же, как в JS — при помощи скобок после выражения и с любым количеством аргументов в скобках, разделённых запятыми. 25 | 26 | ```js 27 | do(define(x, 10), 28 | if(>(x, 5), 29 | print("много"), 30 | print("мало"))) 31 | ``` 32 | 33 | Однородность языка означает, что то, что в JS является операторами, применяется так же, как и остальные функции. Так как в синтаксисе нет концепции блоков, нам нужна конструкция do для обозначения нескольких вещей, выполняемых последовательно. 34 | 35 | Структура данных, описывающая программу, будет состоять из объектов выражений, у каждого из которых будет свойство type, отражающее тип этого выражения и другие свойства, описывающие содержимое. 36 | 37 | Выражения типа “value” представляют строки или числа. Их свойство value содержит строку или число, которое они представляют. Выражения типа “word” используются для идентификаторов (имён). У таких объектов есть свойство name, содержащее имя идентификатора в виде строки. И наконец, выражения “apply” представляют приложения. У них есть свойство “operator”, ссылающееся на применяемое выражение, и свойство “args” с массивом аргументов. 38 | 39 | Часть >(x, 5) будет представлена так: 40 | 41 | ```js 42 | { 43 | type: "apply", 44 | operator: {type: "word", name: ">"}, 45 | args: [ 46 | {type: "word", name: "x"}, 47 | {type: "value", value: 5} 48 | ] 49 | } 50 | ``` 51 | 52 | Такая структура данных называется синтаксическим деревом. Если вы представите объекты в виде точек, а связи между ними в виде линий, то получите древовидную структуру. То, что выражения содержат другие выражения, которые в свою очередь могут содержать свои выражения, сходно с тем, как разветвляются ветки. 53 | 54 | ##Структура синтаксического дерева 55 | Сравните это с парсером, написанным нами для файла настроек в главе 9, у которого была простая структура: он делил ввод на строки и обрабатывал их одну за другой. Там было всего несколько форм, которые разрешено принимать строке. 56 | 57 | Здесь нам нужен другой подход. Выражения не разделяются на строчки, и их структура рекурсивна. Выражения-приложения содержат другие выражения. К счастью, эта задача элегантно решается применением рекурсивной функции, отражающей рекурсивность языка. 58 | 59 | Мы определяем функцию parseExpression, принимающую строку на вход и возвращающую объект, содержащий структуру данных для выражения с начала строки, вместе с частью строки, оставшейся после парсинга. При разборе подвыражений (таких, как аргумент приложения), эта функция снова вызывается, возвращая выражение аргумента вместе с оставшимся текстом. Тот текст может, в свою очередь, содержать ещё аргументы, или же быть закрывающей скобкой, завершающей список аргументов. 60 | 61 | Первая часть парсера: 62 | 63 | ```js 64 | function parseExpression(program) { 65 | program = skipSpace(program); 66 | var match, expr; 67 | if (match = /^"([^"]*)"/.exec(program)) 68 | expr = {type: "value", value: match[1]}; 69 | else if (match = /^\d+\b/.exec(program)) 70 | expr = {type: "value", value: Number(match[0])}; 71 | else if (match = /^[^\s(),"]+/.exec(program)) 72 | expr = {type: "word", name: match[0]}; 73 | else 74 | throw new SyntaxError("Неожиданный синтаксис: " + program); 75 | 76 | return parseApply(expr, program.slice(match[0].length)); 77 | } 78 | 79 | function skipSpace(string) { 80 | var first = string.search(/\S/); 81 | if (first == -1) return ""; 82 | return string.slice(first); 83 | } 84 | ``` 85 | 86 | Поскольку Egg разрешает любое количество пробелов в элементах, нам надо постоянно вырезать пробелы с начала строки. С этим справляется skipSpace. 87 | 88 | Пропустив начальные пробелы, parseExpression использует три регулярки для распознавания трёх простых (атомарных) элементов, поддерживаемых языком: строк, чисел и слов. Парсер создаёт разные структуры для разных типов. Если ввод не подходит ни под одну из форм, это не является допустимым выражением, и он выбрасывает ошибку. SyntaxError – стандартный объект для ошибок, который создаётся при попытке запуска некорректной программы JavaScript. 89 | 90 | Мы можем отрезать обработанную часть программы, и передать его, вместе с объектом выражения, в parseApply, определяющая, не является ли выражение приложением. Если так и есть, он парсит список аргументов в скобках. 91 | 92 | ```js 93 | function parseApply(expr, program) { 94 | program = skipSpace(program); 95 | if (program[0] != "(") 96 | return {expr: expr, rest: program}; 97 | 98 | program = skipSpace(program.slice(1)); 99 | expr = {type: "apply", operator: expr, args: []}; 100 | while (program[0] != ")") { 101 | var arg = parseExpression(program); 102 | expr.args.push(arg.expr); 103 | program = skipSpace(arg.rest); 104 | if (program[0] == ",") 105 | program = skipSpace(program.slice(1)); 106 | else if (program[0] != ")") 107 | throw new SyntaxError("Ожидается ',' or ')'"); 108 | } 109 | return parseApply(expr, program.slice(1)); 110 | } 111 | ``` 112 | 113 | Если следующий символ программы – не открывающая скобка, то это не приложение, и parseApply просто возвращает данное ей выражение. 114 | 115 | В ином случае, она пропускает открывающую скобку и создаёт объект синтаксического дерева для этого выражения. Затем она рекурсивно вызывает parseExpression для разбора каждого аргумента, пока не встретит закрывающую скобку. Рекурсия непрямая, parseApply и parseExpression вызывают друг друга. 116 | 117 | Поскольку приложение само по себе может быть выражением (multiplier(2)(1)), parseApply должна, после разбора приложения, вызвать себя снова, проверив, не идёт ли далее другая пара скобок. 118 | 119 | Вот и всё, что нам нужно для разбора Egg. Мы обернём это в удобную функцию parse, проверяющую, что она дошла до конца строки после разбора выражения (программа Egg – это одно выражение), и это даст нам структуру данных программы. 120 | 121 | ```js 122 | function parse(program) { 123 | var result = parseExpression(program); 124 | if (skipSpace(result.rest).length > 0) 125 | throw new SyntaxError("Неожиданный текст после программы"); 126 | return result.expr; 127 | } 128 | 129 | console.log(parse("+(a, 10)")); 130 | // → {type: "apply", 131 | // operator: {type: "word", name: "+"}, 132 | // args: [{type: "word", name: "a"}, 133 | // {type: "value", value: 10}]} 134 | ``` 135 | 136 | Работает! Она не выдаёт полезной информации при ошибке, и не хранит номера строки и столбца, с которых начинается каждое выражение, что могло бы пригодиться при разборе ошибок – но для нас и этого хватит. 137 | 138 | ##Интерпретатор 139 | А что нам делать с синтаксическим деревом программы? Запускать её! Этим занимается интерпретатор. Вы даёте ему синтаксическое дерево и объект окружения, который связывает имена со значениями, а он интерпретирует выражение, представляемое деревом, и возвращает результат. 140 | 141 | ```js 142 | function evaluate(expr, env) { 143 | switch(expr.type) { 144 | case "value": 145 | return expr.value; 146 | 147 | case "word": 148 | if (expr.name in env) 149 | return env[expr.name]; 150 | else 151 | throw new ReferenceError("Неопределённая переменная: " + 152 | expr.name); 153 | case "apply": 154 | if (expr.operator.type == "word" && 155 | expr.operator.name in specialForms) 156 | return specialForms[expr.operator.name](expr.args, 157 | env); 158 | var op = evaluate(expr.operator, env); 159 | if (typeof op != "function") 160 | throw new TypeError("Приложение не является функцией."); 161 | return op.apply(null, expr.args.map(function(arg) { 162 | return evaluate(arg, env); 163 | })); 164 | } 165 | } 166 | 167 | var specialForms = Object.create(null); 168 | ``` 169 | 170 | 171 | У интерпретатора есть код для каждого из типов выражений. Для литералов он возвращает их значение. Например, выражение 100 интерпретируется в число 100. У переменной мы должны проверить, определена ли она в окружении, и если да – запросить её значение. 172 | 173 | С приложениями сложнее. Если это особая форма типа if, мы ничего не интерпретируем, а просто передаём аргументы вместе с окружением в функцию, обрабатывающую форму. Если это простой вызов, мы интерпретируем оператор, проверяем, что это функция и вызываем его с результатом интерпретации аргументов. 174 | 175 | Для представления значений функций Egg мы будем использовать простые значения функций JavaScript. Мы вернёмся к этому позже, когда определим специальную форму fun. 176 | 177 | Рекурсивная структура интерпретатора напоминает парсер. Оба отражают структуру языка. Можно было бы интегрировать парсер в интерпретатор и интерпретировать во время разбора, но их разделение делает программу более читаемой. 178 | 179 | Вот и всё, что нужно для интерпретации Egg. Вот так просто. Но без определения нескольких специальных форм и добавления полезных значений в окружение, вы с этим языком ничего не сможете сделать. 180 | 181 | ##Специальные формы 182 | Объект specialForms используется для определения особого синтаксиса Egg. Он сопоставляет слова с функциями, интерпретирующими эти специальные формы. Пока он пуст. Давайте добавим несколько форм. 183 | 184 | ```js 185 | specialForms["if"] = function(args, env) { 186 | if (args.length != 3) 187 | throw new SyntaxError("Неправильное количество аргументов для if"); 188 | 189 | if (evaluate(args[0], env) !== false) 190 | return evaluate(args[1], env); 191 | else 192 | return evaluate(args[2], env); 193 | }; 194 | ``` 195 | 196 | Конструкция if языка Egg ждёт три аргумента. Она вычисляет первый, и если результат не false, вычисляет второй. В ином случае вычисляет третий. Этот if больше похож на тернарный оператор ?:. Это выражение, а не инструкция, и она выдаёт значение, а именно, результат второго или третьего выражения. 197 | 198 | Egg отличается от JavaScript тем, как он обрабатывает условие if. Он не будет считать ноль или пустую строку за false. 199 | 200 | if представлено в виде особой формы а не обычной функции, потому что аргументы функций вычисляются перед вызовом, а if должен интерпретировать один из двух аргументов – второй или третий, в зависимости от значения первого. 201 | 202 | Форма для while схожая. 203 | 204 | ```js 205 | specialForms["while"] = function(args, env) { 206 | if (args.length != 2) 207 | throw new SyntaxError("Неправильное количество аргументов для while"); 208 | 209 | while (evaluate(args[0], env) !== false) 210 | evaluate(args[1], env); 211 | 212 | // Поскольку undefined не задано в Egg, 213 | // за отсутствием осмысленного результата возвращаем false 214 | return false; 215 | }; 216 | ``` 217 | 218 | Ещё одна основная часть языка – do, выполняющий все аргументы сверху вниз. Его значение – это значение, выдаваемое последним аргументом. 219 | 220 | ```js 221 | specialForms["do"] = function(args, env) { 222 | var value = false; 223 | args.forEach(function(arg) { 224 | value = evaluate(arg, env); 225 | }); 226 | return value; 227 | }; 228 | ``` 229 | 230 | Чтобы создавать переменные и давать им значения, мы создаём форму define. Она ожидает word в качестве первого аргумента, и выражение, производящее значение, которое надо присвоить этому слову в качестве второго. define, как и всё, является выражением, поэтому оно должно возвращать значение. Пусть оно возвращает присвоенное значение (прям как оператор = в JavaScript). 231 | 232 | ```js 233 | specialForms["define"] = function(args, env) { 234 | if (args.length != 2 || args[0].type != "word") 235 | throw new SyntaxError("Bad use of define"); 236 | var value = evaluate(args[1], env); 237 | env[args[0].name] = value; 238 | return value; 239 | }; 240 | ``` 241 | 242 | ##Окружение 243 | Окружение, принимаемое интерпретатором — это объект со свойствами, чьи имена соответствуют именам переменных, а значения – значениям этих переменных. Давайте определим объект окружения, представляющий глобальную область видимости. 244 | 245 | Для использования конструкции if мы должны создать булевские значения. Так как их всего два, особый синтаксис для них не нужен. Мы просто делаем две переменные со значениями true и false. 246 | 247 | ```js 248 | var topEnv = Object.create(null); 249 | 250 | topEnv["true"] = true; 251 | topEnv["false"] = false; 252 | ``` 253 | 254 | Теперь мы можем вычислить простое выражение, меняющее булевское значение на обратное. 255 | 256 | ```js 257 | var prog = parse("if(true, false, true)"); 258 | console.log(evaluate(prog, topEnv)); // → false 259 | ``` 260 | 261 | Для поддержки простых арифметических операторов и сравнения мы добавим несколько функций в окружение. Для упрощения кода мы будем использовать new Function для создания набора функций-операторов в цикле, а не определять их все по отдельности. 262 | 263 | ```js 264 | ["+", "-", "*", "/", "==", "<", ">"].forEach(function(op) { 265 | topEnv[op] = new Function("a, b", "return a " + op + " b;"); 266 | }); 267 | ``` 268 | 269 | Также пригодится способ вывода значений, так что мы обернём console.log в функцию и назовём её print. 270 | 271 | ```js 272 | topEnv["print"] = function(value) { 273 | console.log(value); 274 | return value; 275 | }; 276 | ``` 277 | 278 | Это даёт нам достаточно элементарных инструментов для написания простых программ. Следующая функция run даёт удобный способ записи и запуска. Она создаёт свежее окружение, парсит и разбирает строчки, которые мы ей передаём, так, как будто они являются одной программой. 279 | 280 | ```js 281 | function run() { 282 | var env = Object.create(topEnv); 283 | var program = Array.prototype.slice 284 | .call(arguments, 0).join("\n"); 285 | return evaluate(parse(program), env); 286 | } 287 | ``` 288 | 289 | Использование Array.prototype.slice.call – уловка для превращения объекта, похожего на массив, такого как аргументы, в настоящий массив, чтобы мы могли применить к нему join. Она принимает все аргументы, переданные в run, и считает, что все они – строчки программы. 290 | 291 | ```js 292 | run("do(define(total, 0),", 293 | " define(count, 1),", 294 | " while(<(count, 11),", 295 | " do(define(total, +(total, count)),", 296 | " define(count, +(count, 1)))),", 297 | " print(total))"); 298 | // → 55 299 | ``` 300 | 301 | Эту программу мы видели уже несколько раз – она подсчитывает сумму чисел от 1 до 10 на языке Egg. Она уродливее эквивалентной программы на JavaScript, но не так уж и плоха для языка, заданного менее чем 150 строчками кода. 302 | 303 | ##Функции 304 | Язык программирования без функций – плохой язык. 305 | 306 | К счастью, несложно добавить конструкцию fun, которая расценивает последний аргумент как тело функции, а все предыдущие – имена аргументов функции. 307 | 308 | ```js 309 | specialForms["fun"] = function(args, env) { 310 | if (!args.length) 311 | throw new SyntaxError("Функции нужно тело"); 312 | function name(expr) { 313 | if (expr.type != "word") 314 | throw new SyntaxError("Имена аргументов должны быть типа word"); 315 | return expr.name; 316 | } 317 | var argNames = args.slice(0, args.length - 1).map(name); 318 | var body = args[args.length - 1]; 319 | 320 | return function() { 321 | if (arguments.length != argNames.length) 322 | throw new TypeError("Неверное количество аргументов"); 323 | var localEnv = Object.create(env); 324 | for (var i = 0; i < arguments.length; i++) 325 | localEnv[argNames[i]] = arguments[i]; 326 | return evaluate(body, localEnv); 327 | }; 328 | }; 329 | ``` 330 | 331 | У функций в Egg своё локальное окружение, как и в JavaScript. Мы используем Object.create для создания нового объекта, имеющего доступ к переменным во внешнем окружении (своего прототипа), но он также может содержать новые переменные, не меняя внешней области видимости. 332 | 333 | Функция, созданная формой fun, создаёт своё локальное окружение и добавляет к нему переменные-аргументы. Затем она интерпретирует тело в этом окружении и возвращает результат. 334 | 335 | ```js 336 | run("do(define(plusOne, fun(a, +(a, 1))),", 337 | " print(plusOne(10)))"); 338 | // → 11 339 | 340 | run("do(define(pow, fun(base, exp,", 341 | " if(==(exp, 0),", 342 | " 1,", 343 | " *(base, pow(base, -(exp, 1)))))),", 344 | " print(pow(2, 10)))"); 345 | // → 1024 346 | ``` 347 | 348 | ##Компиляция 349 | Мы с вами построили интерпретатор. Во время интерпретации он работает с представлением программы, созданным парсером. 350 | 351 | Компиляция – добавление ещё одного шага между парсером и запуском программы, которая превращает в программу в нечто, что можно выполнять более эффективно, путём проделывания большинства работы заранее. К примеру, в хорошо организованных языках при каждом использовании переменной очевидно, к какой переменной обращаются, даже без запуска программы. Это можно использовать, чтобы не искать переменную по имени каждый раз, когда к ней обращаются, а напрямую вызывать её из какой-то заранее определённой области памяти. 352 | 353 | По традиции компиляция также превращает программу в машинный код – сырой формат, пригодный для исполнения процессором. Но каждый процесс превращения программы в другой вид, по сути, является компиляцией. 354 | 355 | Можно было бы создать другой интерпретатор Egg, который сначала превращает программу в программу на языке JavaScript, использует new Function для вызова компилятора JavaScript и возвращает результат. При правильной реализации Egg выполнялся бы очень быстро при относительно простой реализации. 356 | 357 | Если вам это интересно, и вы хотите потратить на это время, я поощряю вас попробовать сделать такой компилятор в качестве упражнения. 358 | 359 | ##Мошенничество 360 | Когда мы определяли if и while, вы могли заметить, что они представляли собой простые обёртки вокруг if и while в JavaScript. Значения в Egg – также обычные значения JavaScript. 361 | 362 | Сравнивая реализацию Egg, построенную на JavaScript, с объёмом работы, необходимой для создания языка программирования непосредственно на машинном языке, то разница становится огромной. Тем не менее, этот пример, надеюсь, даёт вам представление о работе языков программирования. 363 | 364 | И когда вам надо что-то сделать, смошенничать будет более эффективно, нежели делать всё с нуля самому. И хотя игрушечный язык ничем не лучше JavaScript, в некоторых ситуациях написание своего языка помогает быстрее сделать работу. 365 | 366 | Такой язык не обязан напоминать обычный ЯП. Если бы JavaScript не содержал регулярных выражений, вы могли бы написать свои парсер и интерпретатор для такого суб-языка. 367 | 368 | Или представьте, что вы строите гигантского робота-динозавра и вам нужно запрограммировать его поведение. JavaScript – не самый эффективный способ сделать это. Можно вместо этого выбрать язык примерно такого свойства: 369 | 370 | ``` 371 | behavior walk 372 | perform when 373 | destination ahead 374 | actions 375 | move left-foot 376 | move right-foot 377 | 378 | behavior attack 379 | perform when 380 | Godzilla in-view 381 | actions 382 | fire laser-eyes 383 | launch arm-rockets 384 | ``` 385 | 386 | Обычно это называют языком для выбранной области (domain-specific language) – язык, специально предназначенный для работы в узком направлении. Такой язык может быть более выразительным, чем язык общего назначения, потому что он разработан для выражения именно тех вещей, которые надо выразить в этой области – и больше ничего. 387 | 388 | ##Упражнения 389 | ###Массивы 390 | Добавьте поддержку массивов в Egg. Для этого добавьте три функции в основную область видимости: array(...) для создания массива, содержащего значения аргументов, length(array) для возврата длины массива и element(array, n) для возврата n-ного элемента. 391 | 392 | ```js 393 | // Добавьте кода 394 | topEnv["array"] = "..."; 395 | topEnv["length"] = "..."; 396 | topEnv["element"] = "..."; 397 | 398 | run("do(define(sum, fun(array,", 399 | " do(define(i, 0),", 400 | " define(sum, 0),", 401 | " while(<(i, length(array)),", 402 | " do(define(sum, +(sum, element(array, i))),", 403 | " define(i, +(i, 1)))),", 404 | " sum))),", 405 | " print(sum(array(1, 2, 3))))"); 406 | // → 6 407 | ``` 408 | 409 | 410 | ###Замыкания 411 | Способ определения fun позволяет функциям в Egg замыкаться вокруг окружения, и использовать локальные переменные в теле функции, которые видны во время определения, точно как в функциях JavaScript. 412 | 413 | Следующая программа иллюстрирует это: функция f возвращает функцию, добавляющую её аргумент к аргументу f, то есть, ей нужен доступ к локальной области видимости внутри f для использования переменной a. 414 | 415 | ```js 416 | run("do(define(f, fun(a, fun(b, +(a, b)))),", 417 | " print(f(4)(5)))"); 418 | // → 9 419 | ``` 420 | 421 | Объясните, используя определение формы fun, какой механизм позволяет этой конструкции работать. 422 | 423 | ###Комментарии 424 | Хорошо было бы иметь комментарии в Egg. К примеру, мы могли бы игнорировать оставшуюся часть строки, встречая символ “#” – так, как это происходит с “//” в JS. 425 | 426 | Большие изменения в парсере делать не придётся. Мы просто поменяем skipSpace, чтобы она пропускала комментарии, будто они являются пробелами – и во всех местах, где вызывается skipSpace, комментарии тоже будут пропущены. Внесите это изменение. 427 | 428 | ```js 429 | // Поменяйте старую функцию 430 | function skipSpace(string) { 431 | var first = string.search(/\S/); 432 | if (first == -1) return ""; 433 | return string.slice(first); 434 | } 435 | 436 | console.log(parse("# hello\nx")); 437 | // → {type: "word", name: "x"} 438 | 439 | console.log(parse("a # one\n # two\n()")); 440 | // → {type: "apply", 441 | // operator: {type: "word", name: "a"}, 442 | // args: []} 443 | ``` 444 | 445 | ###Чиним область видимости 446 | Сейчас мы можем присвоить переменной значение только через define. Эта конструкция работает как при присвоении старым переменным, так и при создании новых. 447 | 448 | Эта неоднозначность приводит к проблемам. Если вы пытаетесь присвоить новое значение нелокальной переменной, вместо этого вы определяете локальную с таким же именем. (Некоторые языки так и делают, но мне это всегда казалось дурацким способом работы с областью видимости). 449 | 450 | Добавьте форму set, схожую с define, которая присваивает переменной новое значение, обновляя переменную во внешней области видимости, если она не задана в локальной. Если переменная вообще не задана, швыряйте ReferenceError (ещё один стандартный тип ошибки). 451 | 452 | Техника представления областей видимости в виде простых объектов, до сего момента бывшая удобной, теперь будет вам мешать. Вам может понадобиться функция Object.getPrototypeOf, возвращающая прототип объекта. Также помните, что область видимости не наследуется от Object.prototype, поэтому если вам надо вызвать на них hasOwnProperty, придётся использовать такую неуклюжую конструкцию: 453 | 454 | ```js 455 | Object.prototype.hasOwnProperty.call(scope, name); 456 | ``` 457 | 458 | Это вызывает метод hasOwnProperty прототипа Object и затем вызывает его на объекте scope. 459 | 460 | ```js 461 | specialForms["set"] = function(args, env) { 462 | // Ваш код 463 | }; 464 | 465 | run("do(define(x, 4),", 466 | " define(setx, fun(val, set(x, val))),", 467 | " setx(50),", 468 | " print(x))"); 469 | // → 50 470 | run("set(quux, true)"); 471 | // → Ошибка вида ReferenceError 472 | ``` 473 | -------------------------------------------------------------------------------- /chapters/chapter12.md: -------------------------------------------------------------------------------- 1 | #JavaScript и браузер 2 | 3 | Браузер – крайне враждебная программная среда 4 | 5 | Дуглас Крокфорд, «Язык программирования JavaScript» (видеолекция) 6 | 7 | Следующая часть книги расскажет о веб-браузерах. Без них не было бы JavaScript. А если бы и был, никто бы не обратил на него внимания. 8 | 9 | Технологии веба с самого начала были децентрализованными – не только технически, но и с точки зрения их эволюции. Различные разработчики браузеров добавляли новую функциональность «по случаю», непродуманно, и часто эта функциональность обретала поддержку в других браузерах и становилась стандартом. 10 | 11 | Это и благословление и проклятие. С одной стороны, здорово не иметь контролирующего центра, чтобы технология развивалась различными сторонами, иногда сотрудничающими, иногда конкурирующими. С другой – бессистемное развитие языка привело к тому, что результат не является ярким примером внутренней согласованности. Некоторые части привносят путаницу и беспорядок. 12 | 13 | ##Сети и интернет 14 | Компьютерные сети появились в 1950-х. Если вы проложите кабель между двумя или несколькими компьютерами и разрешите им передавать данные, вы может делать много удивительных вещей. А если связь двух машин в одном здании позволяет делать много разного, то связь компьютеров по всей планете должна позволять ещё больше. Технология, позволяющая это сделать, была создана в 1980-х, и получившаяся сеть зовётся интернетом. И она оправдала ожидания. 15 | 16 | Компьютер может использовать эту сеть, чтобы кидаться битами в другой компьютер. Чтобы общение вышло эффективным, оба компьютера должны знать, что эти биты означают. Значение любой заданной последовательности битов зависит от того, что пытаются ими выразить, и какой механизм кодирования используется. 17 | 18 | Стиль общения по сети описывает сетевой протокол. Есть протоколы для отправки е-мейлов, для получения е-мейлов, для распространения файлов и даже для контроля над компьютерами, заражёнными вредоносным софтом. 19 | 20 | К примеру, простой протокол чата может состоять из одного компьютера, отправляющего биты, представляющие текст «ЧАТ?» на другой, а второго отвечающего текстом «ОК!», для подтверждения того, что он понял протокол. Дальше они могут перейти к отправке друг другу текстов, чтения полученных текстов и вывода их на экран. 21 | 22 | Большинство протоколов построено на основе других протоколов. Наш протокол чата из примера рассматривает сеть как потоковое устройство, в которое можно вводить биты и заказывать их приход на конкретный адрес в правильном порядке. А обеспечение этого процесса – само по себе является сложной задачей. Transmission Control Protocol (TCP) – протокол, решающий эту задачу. Все устройства, подключённые к интернету, говорят на нём, и большинство общения в интернете построено на его основе. 23 | 24 | Соединение по TCP работает так: один компьютер ждёт, или «слушает», пока другие не начнут с ним говорить. Чтобы можно было слушать разные виды общения в одно и то же время, для каждого из них назначается номер (называемый портом). Большинство протоколов устанавливают порт, используемый по умолчанию. К примеру, если мы отправляем е-мейл по протоколу SMTP, компьютер, через который мы его шлём, должен слушать порт 25. 25 | 26 | Тогда другой компьютер может установить соединение, связавшись с компьютером назначения по правильному порту. Если машина назначения доступна, и она слушает этот порт, соединение устанавливается. Слушающий компьютер зовётся сервером, а соединяющийся – клиентом. 27 | 28 | Такое соединение работает как двусторонняя труба, по которой текут биты – обе машины могут помещать в неё данные. Когда биты переданы, другая машина может их прочесть. Это удобная модель. Можно сказать, что TCP обеспечивает абстракцию сети. 29 | 30 | ##Веб 31 | World Wide Web, всемирная паутина (это не то же самое, что весь интернет в целом) – набор протоколов и форматов, позволяющий нам посещать странички через браузер. Web (рус. «паутина») в названии обозначает, что страницы можно легко связать друг с другом, в результате чего образуется гигантская сеть-паутина, по которой движутся пользователи. 32 | 33 | Чтобы добавить в Веб содержимое, вам нужно соединить машину с интернетом и заставить её слушать 80 порт, используя протокол передачи гипертекста, Hypertext Transfer Protocol (HTTP). Он позволяет другим компьютерам запрашивать документы по сети. 34 | 35 | Каждый документ имеет имя в виде универсального локатора ресурсов, Universal Resource Locator (URL), который выглядит примерно так: 36 | 37 | ``` 38 | http://eloquentjavascript.net/12_browser.html 39 | | | | | 40 | протокол сервер путь 41 | ``` 42 | 43 | Первая часть говорит нам, что URL использует протокол HTTP (в отличие от, скажем, зашифрованного HTTP, который записывается как https://). Затем идёт часть, определяющая, с какого сервера мы запрашиваем документ. Последняя – строка пути, определяющая конкретный документ или ресурс. 44 | 45 | У каждой машины, присоединённой к интернету, есть свой адрес IP, который выглядит как 37.187.37.82. Его иногда можно использовать вместо имени сервера в URL. Но цифры сложнее запоминать и печатать, чем имена – поэтому обычно вы регистрируете доменное имя, которое указывает на конкретную машину (или набор машин). Я зарегистрировал eloquentjavascript.net, указывающий на IP-адрес машины, которую я контролирую, поэтому можно использовать этот адрес для предоставления веб-страниц. 46 | 47 | Если вы введёте указанный URL в адресную строку браузера, он попробует запросить и показать документ, находящийся по этому URL. Во-первых, браузеру надо выяснить, куда ссылается домен eloquentjavascript.net. Затем, используя протокол HTTP, он соединяется с сервером по этому адресу, и спрашивает его ресурс по имени /12_browser.html 48 | 49 | В главе 17 мы подробнее рассмотрим протокол HTTP. 50 | 51 | ##HTML 52 | HTML, или язык разметки гипертекста, Hypertext Markup Language – формат документа, использующийся для веб-страниц. HTML содержит текст и теги, придающие тексту структуру, описывающие такие вещи, как ссылки, параграфы и заголовки. 53 | 54 | Простой HTML документ может выглядеть так: 55 | 56 | ```html 57 | 58 | 59 | 60 | Моя домашняя страничка 61 | 62 | 63 |

Моя домашняя страничка

64 |

Привет, я Марийн и это моя домашняя страничка.

65 |

А ещё я книжку написал! Читайте её 66 | здесь.

67 | 68 | 69 | ``` 70 | 71 | Теги, окружённые угловыми скобками < и >, описывают информацию о структуре документа. Всё остальное – просто текст. 72 | 73 | Документ начинается с , и это говорит браузеру, что его надо интерпретировать как современный HTML, в отличие от разных диалектов прошлого. 74 | 75 | У HTML документов есть заголовок и тело. Заголовок содержит информацию о документе, а тело – сам документ. В нашем случае мы объявили, что название страницы будет «Моя домашняя страничка», затем описали документ, содержащий заголовок 76 | (`

`, то есть heading 1, заголовок 1. Есть ещё `

` – `

`, заголовки разных размеров) и два параграфа. 77 | 78 | У тегов может быть несколько форм. Элемент вроде тела, параграфа и ссылки начинается открывающим тегом `

` и заканчивается закрывающим `

`. Некоторые открывающие теги, типа ссылки ``, содержат дополнительную информацию в виде имя=”значение”. Она называется «атрибутами». В нашем случае адрес ссылки задан как `href="http://eloquentjavascript.net"`, где href означает «гипертекстовая ссылка», “hypertext reference”. 79 | 80 | Некоторые теги ничего не окружают, и их не надо закрывать. Пример – тег картинки 81 | 82 | ```html 83 | 84 | ``` 85 | 86 | Чтобы включать в текст документа угловые скобки, нужно пользоваться специальной записью, так как в HTML они имеют особое значение. Открывающая скобка (она же знак «меньше») записывается как `<` («less than», «меньше, чем»), закрывающая — `>` («greater that», «больше, чем»). В HTML амперсанд &, за которым идёт слово и точка с запятой, зовётся сущностью и заменяется символом, который кодируется этой последовательностью. 87 | 88 | Это похоже на обратные слэши, используемые в строках JavaScript. Из-за специального значения амперсанда его самого в текст можно включать в виде `&`. В атрибуте, заключаемом в двойные кавычки, символ кавычек записывается как `"`. 89 | 90 | HTML разбирается парсером довольно либерально по отношению к возможным ошибкам. Если какие-то теги пропущены, браузер их воссоздаёт. Как именно это происходит, записано в стандартах, поэтому можно ожидать, что все современные браузеры будут делать это одинаково. 91 | 92 | Следующий документ будет обработан так же, как и предыдущий. 93 | 94 | ```html 95 | 96 | 97 | Моя домашняя страничка 98 | 99 |

Моя домашняя страничка

100 |

Привет, я Марийн и это моя домашняя страничка. 101 |

А ещё я книжку написал! Читайте её 102 | here. 103 | ``` 104 | 105 | Отсутствуют теги ``, `` и ``. Браузер знает, что `` должен быть в `<head>`, а `<h1>` — в `<body>`. Кроме того, параграфы не закрыты, поскольку открытие нового параграфа или конец документа означают их принудительное закрытие. Также адрес не заключён в кавычки. 106 | 107 | В этой книге мы опустим теги `<html>`, `<head>` и `<body>` для краткости. Но я буду закрывать теги, и заключать атрибуты в кавычки. 108 | 109 | Также обычно я буду опускать doctype. Я не советую делать это вам – браузеры иногда могут творить странные вещи, когда вы их опускаете. Считайте, что они присутствуют в примерах по умолчанию. 110 | 111 | ##HTML и JavaScript 112 | В контексте нашей книги самый главный тег HTML — `<script>`. Он позволяет включать в документ программу на JavaScript. 113 | 114 | ```html 115 | <h1>Внимание, тест.</h1> 116 | <script>alert("Привет!");</script> 117 | ``` 118 | 119 | Такой скрипт запустится сразу, как только браузер встретит тег `<script>` при разборе HTML. На странице появится диалог-предупреждение. 120 | 121 | Включать большие программы в HTML непрактично. У тега `<script>` есть атрибут src, чтобы запрашивать файл со скриптом (текст, содержащий программу на JavaScript) с адреса URL. 122 | 123 | ```html 124 | <h1>Внимание, тест.</h1> 125 | <script src="code/hello.js"></script> 126 | ``` 127 | 128 | 129 | В файле code/hello.js содержится та же простая программа «alert('Привет!');». Когда страница ссылается на другой URL и включает его в себя, браузер подгружает этот файл и включает их в страницу. 130 | 131 | Тег script всегда надо закрывать при помощи , даже если он не содержит кода и ссылается на файл скрипта. Если вы забудете это сделать, оставшаяся часть страницы будет обработана как скрипт. 132 | 133 | Некоторые атрибуты тоже могут содержать программу JavaScript. У тега (на странице он выглядит как кнопка) есть атрибут onClick, и его содержимое будет запущено, когда по кнопке щёлкают мышкой. 134 | 135 | ```html 136 | <button onclick="alert('Бабах!');">НЕ ЖМИ</button> 137 | ``` 138 | 139 | Заметьте, что я использовал одинарные кавычки для строки в атрибуте onclick, поскольку двойные кавычки уже используются в самом атрибуте. Можно было бы использовать `"`, но это бы затруднило чтение. 140 | 141 | ##Песочница 142 | Запуск скачанных из интернета программ небезопасен. Вы не знаете ничего о тех людях, которые делали посещаемые вами сайты, и они не всегда доброжелательны. Запуская программы злых людей, вы можете заразить компьютер вирусами, потерять свои данные или дать доступ к своим аккаунтам третьим лицам. 143 | 144 | Но привлекательность веба в том, что по нему можно сёрфить без обязательного доверия всем посещаемым страницам. Поэтому браузеры сильно ограничивают то, что может сделать программа JavaScript. Она не может открывать файлы на компьютере, или менять что-либо, не связанное со страницей, в которую она встроена. 145 | 146 | Изолированное таким образом окружение называется песочницей – в том смысле, что программа безобидно играется в песочнице. Представляйте, однако, эту песочницу как клетку из толстых стальных прутьев. 147 | 148 | Сложность в создании песочницы – позволять программам делать достаточно много, чтобы они были полезными, при этом ограничивая их от совершения опасных действий. Много из того, что делает пользователь, например общение с другими серверами или чтение содержимого буфера обмена, можно использовать для нарушения приватности. 149 | 150 | Время от времени кто-то придумывает способ обойти ограничения браузера и сделать что-то вредное, от утечки некоей приватной информации до полного контроля над компьютером, где запущен скрипт. Разработчики исправляют эту дырку в браузере, и снова всё хорошо – до появления следующей проблемы, которая, можно надеяться, будет опубликована, и не тайно использоваться правительством или мафией. 151 | 152 | ##Совместимость и браузерные войны 153 | На ранних стадиях развития Веба браузер по имени Mosaic занимал большую часть рынка. Через несколько лет баланс сместился в сторону Netscape, который затем был сильно потеснён браузером Internet Explorer от Microsoft. В любой момент превосходства одного из браузеров его разработчики позволяли себе в одностороннем порядке изобретать новые свойства веба. Так как большинство людей использовали один и тот же браузер, сайты просто начинали использовать эти свойства, не обращая внимания на остальные браузеры. 154 | 155 | Это были тёмные века совместимости, которые иногда называли «войнами браузеров». Веб-разработчики сталкивались с двумя или тремя несовместимыми платформами. Кроме того, браузеры около 2003 года были полны ошибок, причём у каждого они были свои. Жизнь людей, создававших веб-страницы, была тяжёлой. 156 | 157 | Mozilla Firefox, некоммерческое ответвление Netscape, бросил вызов гегемонии Internet Explorer в конце 2000-х. Так как Microsoft особо не стремилась к конкуренции, Firefox отобрал солидную часть рынка. Примерно в это время Google представил свой браузер Chrome, а Apple – Safari. Это привело к появлению четырёх основных игроков вместо одного. 158 | 159 | У новых игроков были более серьёзные намерения по отношению к стандартам и больше инженерного опыта, что привело к лучшей совместимости и меньшему количеству багов. Microsoft, видя сжатие своей части рынка, приняла эти стандарты. Если вы начинаете изучать веб-разработку сегодня – вам повезло. Последние версии основных браузеров работают одинаково и в них мало ошибок. 160 | 161 | Нельзя сказать, что ситуация уже идеальная. Некоторые люди в вебе по причинам инерционности или корпоративных правил используют очень старые браузеры. Пока они не отомрут совсем, написание веб-страниц для них потребует мистических знаний об их недостатках и причудах. Эта книга не про причуды – она представляет современный, разумный стиль веб-программирования. 162 | -------------------------------------------------------------------------------- /chapters/chapter18.md: -------------------------------------------------------------------------------- 1 | #Формы и поля форм 2 | 3 | <i>Я нынче ж на ученом кутеже 4 | Твое доверье службой завоюю, 5 | Ты ж мне черкни расписку долговую, 6 | Чтоб мне не сомневаться в платеже.</i> 7 | 8 | <i>Мефистофель, в «Фаусте» Гёте</i> 9 | 10 | Формы были кратко представлены в предыдущей главе в качестве способа передачи информации, введённой пользователем, через HTTP. Они были разработаны в вебе до появления JavaScript, с тем расчётом, что взаимодействие с сервером происходит при переходе на другую страницу. 11 | 12 | Но их элементы являются частями DOM, как и остальные части страницы, а элементы DOM, представляющие поля формы, поддерживают несколько свойств и событий, которых нет у других элементов. Это делает возможным просматривать и управлять полями ввода из программ JavaScript и добавлять функциональности к классическим формам или использовать формы и поля как основу для построения приложения. 13 | 14 | ##Поля 15 | Веб-форма состоит из любого числа полей ввода, окружённых тегом `<form>`. HTML предлагает много разных полей, от простых галочек со значениями вкл/выкл до выпадающих списков и полей для ввода текста. В этой книге не будут подробно обсуждаться все виды полей, но мы сделаем небольшой их обзор. 16 | 17 | Много типов полей ввода используют тег `<input>`. Его атрибут type используется для выбора стиля поля. Вот несколько распространённых типов: 18 | 19 | <b>text </b>текстовое поле на одну строку 20 | <b>password </b>то же, что текст, но прячет ввод 21 | <b>checkbox </b>переключатель вкл/выкл 22 | <b>radio </b>часть поля с возможностью выбора из нескольких вариантов 23 | <b>file </b>позволяет пользователю выбрать файл на его компьютере 24 | 25 | Поля форм не обязательно должны появляться внутри тега `<form>`. Их можно разместить в любом месте страницы. Информацию из таких полей нельзя передавать на сервер (это возможно только для всей формы целиком), но когда мы делаем поля, которые обрабатывает JavaScript, нам обычно и не нужно передавать информацию из полей через submit. 26 | 27 | ```html 28 | <p><input type="text" value="abc"> (text)</p> 29 | <p><input type="password" value="abc"> (password)</p> 30 | <p><input type="checkbox" checked> (checkbox)</p> 31 | <p><input type="radio" value="A" name="choice"> 32 | <input type="radio" value="B" name="choice" checked> 33 | <input type="radio" value="C" name="choice"> (radio)</p> 34 | <p><input type="file" checked> (file)</p> 35 | ``` 36 | 37 | Интерфейс JavaScript для таких элементов разнится в зависимости от типа. Мы рассмотрим каждый из них чуть позже. 38 | 39 | У текстовых полей на несколько строк есть свой тег `<textarea>`. У тега должен быть закрывающий тег `</textarea>`, и он использует текст внутри этих тегов вместо использования атрибута value. 40 | 41 | ```html 42 | <textarea> 43 | один 44 | два 45 | три 46 | </textarea> 47 | ``` 48 | 49 | А тег `<select>` используется для создания поля, которое позволяет пользователю выбрать один из заданных вариантов. 50 | 51 | ```html 52 | <select> 53 | <option>Блины</option> 54 | <option>Запеканка</option> 55 | <option>Мороженка </option> 56 | </select> 57 | ``` 58 | 59 | Когда значение поля изменяется, запускается событие “change”. 60 | 61 | ##Фокус 62 | В отличие от большинства элементов документа HTML, поля форм могут получать фокус ввода клавиатуры. При щелчке или выборе их другим способом они становятся активными, т.е. главными приёмниками клавиатурного ввода. 63 | 64 | Если в документе есть текстовое поле, то набираемый текст появится в нём, только если поле имеет фокус ввода. Другие поля по-разному реагируют на клавиатуру. К примеру, `<select>` пытается перейти на вариант, содержащий текст, который вводит пользователь, а также отвечает на нажатия стрелок, передвигая выбор варианта вверх и вниз. 65 | 66 | Управлять фокусом из JavaScript можно методами focus и blur. Первый перемещает фокус на элемент DOM, из которого он вызван, а второй убирает фокус. Значение document.activeElement соответствует текущему элементу, получившему фокус. 67 | 68 | ```html 69 | <input type="text"> 70 | <script> 71 | document.querySelector("input").focus(); 72 | console.log(document.activeElement.tagName); 73 | // → INPUT 74 | document.querySelector("input").blur(); 75 | console.log(document.activeElement.tagName); 76 | // → BODY 77 | </script> 78 | ``` 79 | 80 | На некоторых страницах нужно, чтобы пользователь сразу начинал работу с какого-то из полей формы. При помощи JavaScript можно передать этому полю фокус при загрузке документа, но в HTML также есть атрибут autofocus, который приводит к тому же результату, но сообщает браузеру о наших намерениях. В этом случае браузер может отменить это поведение в подходящих случаях, например когда пользователь перевёл фокус куда-то ещё. 81 | 82 | ```html 83 | <input type="text" autofocus> 84 | ``` 85 | 86 | Браузеры по традиции позволяют пользователю перемещать фокус клавишей Tab. Мы можем влиять на порядок перемещения через атрибут tabindex. В примере документ будет переносить фокус с текстового поля на кнопку OK, вместо того, чтобы сначала пройти через ссылку help. 87 | 88 | ```html 89 | <input type="text" tabindex=1> <a href=".">(help)</a> 90 | <button onclick="console.log('ok')" tabindex=2>OK</button> 91 | ``` 92 | 93 | По умолчанию, большинство типов элементов HTML не получают фокус. Но добавив tabindex к элементу, вы сделаете возможным получение им фокуса. 94 | 95 | ##Отключённые поля 96 | Все поля можно отключить атрибутом disabled, который существует и в виде свойства элемента объекта DOM. 97 | 98 | ```html 99 | <button>У меня всё хорошо</button> 100 | <button disabled>Я в отключке</button> 101 | ``` 102 | 103 | Отключённые поля не принимают фокус и не изменяются, и в отличие от активных, обычно выглядят серыми и выцветшими. 104 | 105 | Когда программа находится в процессе обработки нажатия на кнопку или другой элемент, которое может потребовать общение с сервером и занять длительное время, неплохо отключать элемент до завершения действия. В этом случае, когда пользователь потеряет терпение и нажмёт на элемент ещё раз, действие не будет повторено лишний раз. 106 | 107 | ##Форма в целом 108 | Когда поле, содержится в элементе `<form>`, у его элемента DOM будет свойство form, которое будет ссылаться на форму. Элемент `<form>`, в свою очередь, имеет свойство elements, содержащее массивоподобную коллекцию полей. 109 | 110 | Атрибут name поля задаёт, как будет определено значение этого поля при передаче на сервер. Его также можно использовать как имя свойства при доступе к свойству формы elements, который работает и как объект, похожий на массив (с доступом по номерам), так и map (с доступом по имени). 111 | 112 | ```html 113 | <form action="example/submit.html"> 114 | Имя: <input type="text" name="name"><br> 115 | Пароль: <input type="password" name="password"><br> 116 | <button type="submit">Войти</button> 117 | </form> 118 | <script> 119 | var form = document.querySelector("form"); 120 | console.log(form.elements[1].type); 121 | // → password 122 | console.log(form.elements.password.type); 123 | // → password 124 | console.log(form.elements.name.form == form); 125 | // → true 126 | </script> 127 | ``` 128 | 129 | Кнопка с атрибутом type равным submit при нажатии отправляет форму. Нажатие клавиши Enter внутри поля формы имеет тот же эффект. 130 | 131 | Отправка формы обычно означает, что браузер переходит на страницу, обозначенную в атрибуте формы action, используя либо GET либо POST запрос. Но перед этим запускается свойство “submit”. Его можно обработать в JavaScript, и обработчик может предотвратить поведение по умолчанию, вызвав на объекте event preventDefault. 132 | 133 | ```html 134 | <form action="example/submit.html"> 135 | Значение: <input type="text" name="value"> 136 | <button type="submit">Сохранить </button> 137 | </form> 138 | <script> 139 | var form = document.querySelector("form"); 140 | form.addEventListener("submit", function(event) { 141 | console.log("Saving value", form.elements.value.value); 142 | event.preventDefault(); 143 | }); 144 | </script> 145 | ``` 146 | 147 | Перехват событий “submit” полезен в нескольких случаях. Мы можем написать код, проверяющий допустимость введённых значений и сразу же показать ошибку вместо передачи данных формы. Или мы можем отключить отправку формы по умолчанию и дать программе возможность самой обработать ввод, например используя XMLHttpRequest для отправки данных на сервер без перезагрузки страницы. 148 | 149 | ##Текстовые поля 150 | Поля с тегами `<input>` и типами text и password, а также теги , имеют общий интерфейс. У их элементов DOM есть свойство value, в котором содержится их текущее содержимое в виде строки текста. Присваивание этому свойству значения меняет содержимое поля. 151 | 152 | Свойства текстовых полей selectionStart и selectionEnd содержат данные о положении курсора и выделения текста. Когда ничего не выделено, их значение одинаковое, и равно положению курсора. Например, 0 обозначает начало текста, 10 обозначает, что курсор находится на 10-м символе. Когда выделена часть поля, свойства имеют разные значения, а именно начало и конец выделенного текста. В эти поля также можно записывать значение. 153 | 154 | К примеру, представьте, что вы пишете статью про Khasekhemwy, но затрудняетесь писать его имя правильно. Следующий код назначает тегу `<textarea>` обработчик событий, который при нажатии F2 вставляет строку “ Khasekhemwy”. 155 | 156 | ```html 157 | <textarea></textarea> 158 | <script> 159 | var textarea = document.querySelector("textarea"); 160 | textarea.addEventListener("keydown", function(event) { 161 | // The key code for F2 happens to be 113 162 | if (event.keyCode == 113) { 163 | replaceSelection(textarea, "Khasekhemwy"); 164 | event.preventDefault(); 165 | } 166 | }); 167 | function replaceSelection(field, word) { 168 | var from = field.selectionStart, to = field.selectionEnd; 169 | field.value = field.value.slice(0, from) + word + 170 | field.value.slice(to); 171 | // Put the cursor after the word 172 | field.selectionStart = field.selectionEnd = 173 | from + word.length; 174 | }; 175 | </script> 176 | ``` 177 | 178 | Функция replaceSelection заменяет текущий выделенный текст заданным словом, и перемещает курсор на позицию после этого слова, чтобы можно было продолжать печатать. 179 | 180 | Событие “change” для текстового поля не срабатывает каждый раз при вводе одного символа. Оно срабатывает после потери полем фокуса, когда его значение было изменено. Чтобы мгновенно реагировать на изменение текстового поля нужно зарегистрировать событие “input”, которое срабатывает каждый раз при вводе символа, удалении текста или других манипуляциях с содержимым поля. 181 | 182 | В следующем примере мы видим текстовое поле и счётчик, показывающий текущую длину введённого текста. 183 | 184 | ```html 185 | <input type="text"> length: <span id="length">0</span> 186 | <script> 187 | var text = document.querySelector("input"); 188 | var output = document.querySelector("#length"); 189 | text.addEventListener("input", function() { 190 | output.textContent = text.value.length; 191 | }); 192 | </script> 193 | ``` 194 | 195 | ##Галочки и радиокнопки 196 | Поле галочки – простой бинарный переключатель. Его значение можно извлечь или поменять через свойство checked, содержащее булевскую величину. 197 | 198 | ```html 199 | <input type="checkbox" id="purple"> 200 | <label for="purple">Сделать страницу фиолетовой</label> 201 | <script> 202 | var checkbox = document.querySelector("#purple"); 203 | checkbox.addEventListener("change", function() { 204 | document.body.style.background = 205 | checkbox.checked ? "mediumpurple" : ""; 206 | }); 207 | </script> 208 | ``` 209 | 210 | Тег `<label>` используется для связи куска текста с полем ввода. Атрибут for должен совпадать с id поля. Щелчок по метке label включает поле ввода, оно получает фокус и меняет значение – если это галочка или радиокнопка. 211 | 212 | Радиокнопка схожа с галочкой, но она связана с другими радиокнопками с тем же именем, так что только одна из них может быть выбрана. 213 | 214 | ```html 215 | Цвет: 216 | <input type="radio" name="color" value="mediumpurple"> Фиолетовый 217 | <input type="radio" name="color" value="lightgreen"> Зелёныйы 218 | <input type="radio" name="color" value="lightblue"> Голубой 219 | <script> 220 | var buttons = document.getElementsByName("color"); 221 | function setColor(event) { 222 | document.body.style.background = event.target.value; 223 | } 224 | for (var i = 0; i < buttons.length; i++) 225 | buttons[i].addEventListener("change", setColor); 226 | </script> 227 | ``` 228 | 229 | Метод document.getElementsByName выдаёт все элементы с заданным атрибутом name. Пример перебирает их (посредством обычного цикла for, а не forEach, потому что возвращаемая коллекция – не настоящий массив) и регистрирует обработчик событий для каждого элемента. Помните, что у объектов событий есть свойство target, относящееся к элементу, который запустил событие. Это полезно для создания обработчиков событий – наш обработчик может быть вызван разными элементами, и у него должен быть способ получить доступ к текущему элементу, который его вызвал. 230 | 231 | ##Поля select 232 | Поля select похожи на радиокнопки – они также позволяют выбрать из нескольких вариантов. Но если радиокнопки позволяют нам контролировать раскладку вариантов, то вид поля `<select>` определяет браузер. 233 | 234 | У полей select есть вариант, больше похожий на список галочек, чем на радиокнопки. При наличии атрибута multiple тег `<select>` позволит выбирать любое количество вариантов, а не один. 235 | 236 | ```html 237 | <select multiple> 238 | <option>Блины</option> 239 | <option>Запеканка</option> 240 | <option>Мороженка </option> 241 | </select> 242 | ``` 243 | 244 | В большинстве браузеров внешний вид поля будет отличаться от поля с единственным вариантом выбора, который обычно выглядит как выпадающее меню. 245 | 246 | Атрибут size тега `<select>` используется для задания количества вариантов, которые видны одновременно – так вы можете влиять на внешний вид выпадушки. К примеру, назначив size 3, вы увидите три строки одновременно, безотносительно того, присутствует ли опция multiple. 247 | 248 | У каждого тега `<option>` есть значение. Его можно определить атрибутом value, но если он не задан, то значение тега определяет текст, находящийся внутри тега `<option>..</option>`. Свойство value элемента отражает текущий выбранный вариант. Для поля с возможностью выбора нескольких вариантов это свойство не особо нужно, т.к. в нём будет содержаться только один из нескольких выбранных вариантов. 249 | 250 | К тегу `<option>` поля `<select>` можно получить доступ как к массивоподобному объекту через свойство options. У каждого варианта есть свойство selected, показывающее, выбран ли сейчас этот вариант. Свойство также можно менять, чтобы вариант становился выбранным или не выбранным. 251 | 252 | Следующий пример извлекает выбранные значения из поля select и использует их для создания двоичного числа из битов. Нажмите Ctrl (или Command на Маке), чтобы выбрать несколько значений сразу. 253 | 254 | ```html 255 | <select multiple> 256 | <option value="1">0001</option> 257 | <option value="2">0010</option> 258 | <option value="4">0100</option> 259 | <option value="8">1000</option> 260 | </select> = <span id="output">0</span> 261 | <script> 262 | var select = document.querySelector("select"); 263 | var output = document.querySelector("#output"); 264 | select.addEventListener("change", function() { 265 | var number = 0; 266 | for (var i = 0; i < select.options.length; i++) { 267 | var option = select.options[i]; 268 | if (option.selected) 269 | number += Number(option.value); 270 | } 271 | output.textContent = number; 272 | }); 273 | </script> 274 | ``` 275 | 276 | ##Файловое поле 277 | Файловое поле изначально было предназначено для закачивания файлов с компьютера через форму. В современных браузерах они также позволяют читать файлы из JavaScript. Поле работает как охранник для файлов. Скрипт не может просто взять и открыть файл с компьютера пользователя, но если тот выбрал файл в этом поле, браузер разрешает скрипту начать чтение файла. 278 | 279 | Файловое поле обычно выглядит как кнопка с надписью вроде «Выберите файл», с информацией про выбранный файл рядом с ней. 280 | 281 | ```html 282 | <input type="file"> 283 | <script> 284 | var input = document.querySelector("input"); 285 | input.addEventListener("change", function() { 286 | if (input.files.length > 0) { 287 | var file = input.files[0]; 288 | console.log("You chose", file.name); 289 | if (file.type) 290 | console.log("It has type", file.type); 291 | } 292 | }); 293 | </script> 294 | ``` 295 | 296 | Свойство files элемента – массивоподобный объект (не настоящий массив), содержащий список выбранных файлов. Изначально он пуст. У элемента нет простого свойства file, потому что пользователь может выбрать несколько файлов за раз при включённом атрибуте multiple. 297 | 298 | У объектов в свойстве files есть свойства имя (имя файла), размер (размер файла в байтах), и тип (тип файла в смысле media type — text/plain или image/jpeg). 299 | 300 | Чего у него нет, так это свойства, содержащего содержимое файла. Чтобы получить содержимое, приходиться постараться. Так как чтение файла с диска занимает длительное время, интерфейс должен быть асинхронным, чтобы документ не замирал. Конструктор FileReader можно представлять себе, как конструктор XMLHttpRequest, только для файлов. 301 | 302 | ```html 303 | <input type="file" multiple> 304 | <script> 305 | var input = document.querySelector("input"); 306 | input.addEventListener("change", function() { 307 | Array.prototype.forEach.call(input.files, function(file) { 308 | var reader = new FileReader(); 309 | reader.addEventListener("load", function() { 310 | console.log("File", file.name, "starts with", 311 | reader.result.slice(0, 20)); 312 | }); 313 | reader.readAsText(file); 314 | }); 315 | }); 316 | </script> 317 | ``` 318 | 319 | Чтение файла происходит при помощи создания объекта FileReader, регистрации события “load” для него, и вызова его метода readAsText с передачей тому файла. По окончанию загрузки в свойстве result сохраняется содержимое файла. 320 | 321 | Пример использует Array.prototype.forEach для прохода по массиву, так как в обычном цикле было бы неудобно получать нужные объекты file и reader от обработчика событий. Переменные были бы общими для всех итераций цикла. 322 | 323 | У FileReaders также есть событие “error”, когда чтение файла не получается. Объект error будет сохранён в свойстве error. Если вы не хотите забивать голову ещё одной неудобной асинхронной схемой, вы можете обернуть её в обещание (см. главу 17): 324 | 325 | ```js 326 | function readFile(file) { 327 | return new Promise(function(succeed, fail) { 328 | var reader = new FileReader(); 329 | reader.addEventListener("load", function() { 330 | succeed(reader.result); 331 | }); 332 | reader.addEventListener("error", function() { 333 | fail(reader.error); 334 | }); 335 | reader.readAsText(file); 336 | }); 337 | } 338 | ``` 339 | 340 | Возможно читать только часть файла, вызывая slice и передавая результат (т.н. объект blob) объекту reader. 341 | 342 | ##Хранение данных на стороне клиента 343 | Простые HTML-странички с добавкой JavaScript могут выступать отличной основой для мини-приложений – небольших вспомогательных программ, автоматизирующих ежедневные дела. Присоединив к полям формы обработчики событий вы можете делать всё – от конвертации фаренгейтов в цельсии до генерации паролей из основного пароля и имени веб-сайта. 344 | 345 | Когда такому приложению нужно сохранять информацию между сессиями, переменные JavaScript использовать не получится – их значения выбрасываются каждый раз при закрытии страницы. Можно было бы настроить сервер, подсоединить его к интернету и тогда приложение хранило бы ваши данные там. Это мы разберём в главе 20. Но это добавляет вам работы и сложности. Иногда достаточно хранить данные в своём браузере. Но как? 346 | 347 | Можно хранить строковые данные так, что они переживут перезагрузку страниц — для этого надо положить их в объект localStorage. Он разрешает хранить строковые данные под именами (которые тоже являются строками), как в этом примере: 348 | 349 | ```js 350 | localStorage.setItem("username", "marijn"); 351 | console.log(localStorage.getItem("username")); 352 | // → marijn 353 | localStorage.removeItem("username"); 354 | ``` 355 | 356 | Переменная в localStorage хранится, пока её не перезапишут, удаляется при помощи removeItem или очисткой локального хранилища пользователем. 357 | 358 | У сайтов с разных доменов – разные отделения в этом хранилище. То есть, данные, сохранённые с вебсайта в localStorage, могут быть прочтены или перезаписаны только скриптами с этого же сайта. 359 | 360 | Также браузеры ограничивают объём хранимых данных, обычно в несколько мегабайт. Это ограничение, вкупе с тем фактом, что забивание жёстких дисков у людей не приносит прибыли, предотвращает отъедание места на диске. 361 | 362 | Следующий код реализует простую программу для ведения заметок. Она хранит заметки в виде объекта, ассоциируя заголовки с содержимым. Он кодируется в JSON и хранится в localStorage. Пользователь может выбрать записку через поле `<select>` и поменять её текст в `<textarea>`. Добавляется запись по нажатию на кнопку. 363 | 364 | ```html 365 | Заметки: <select id="list"></select> 366 | <button onclick="addNote()">новая</button><br> 367 | <textarea id="currentnote" style="width: 100%; height: 10em"> 368 | </textarea> 369 | 370 | <script> 371 | var list = document.querySelector("#list"); 372 | function addToList(name) { 373 | var option = document.createElement("option"); 374 | option.textContent = name; 375 | list.appendChild(option); 376 | } 377 | 378 | // Берём список из локального хранилища 379 | var notes = JSON.parse(localStorage.getItem("notes")) || 380 | {"что купить": ""}; 381 | for (var name in notes) 382 | if (notes.hasOwnProperty(name)) 383 | addToList(name); 384 | 385 | function saveToStorage() { 386 | localStorage.setItem("notes", JSON.stringify(notes)); 387 | } 388 | 389 | var current = document.querySelector("#currentnote"); 390 | current.value = notes[list.value]; 391 | 392 | list.addEventListener("change", function() { 393 | current.value = notes[list.value]; 394 | }); 395 | current.addEventListener("change", function() { 396 | notes[list.value] = current.value; 397 | saveToStorage(); 398 | }); 399 | 400 | function addNote() { 401 | var name = prompt("Имя записи", ""); 402 | if (!name) return; 403 | if (!notes.hasOwnProperty(name)) { 404 | notes[name] = ""; 405 | addToList(name); 406 | saveToStorage(); 407 | } 408 | list.value = name; 409 | current.value = notes[name]; 410 | } 411 | </script> 412 | ``` 413 | 414 | Скрипт инициализирует переменную notes значением из localStorage, а если его там нет – простым объектом с одной записью «что купить». Попытка прочесть отсутствующее поле из localStorage вернёт null. Передав null в JSON.parse, мы получим null обратно. Поэтому для значения по умолчанию можно использовать оператор ||. 415 | 416 | Когда данные в note меняются (добавляется новая запись или меняется текущая), для обновления хранимого поля вызывается функция saveToStorage. Если б мы рассчитывали, что у нас будут храниться тысячи записей, это было бы слишком накладно, и нам пришлось бы придумать более сложную процедуру для хранения – например, своё поле для каждой записи. 417 | 418 | Когда пользователь добавляет запись, код должен обновить текстовое поле, хотя у поля и есть обработчик “change”. Это нужно потому, что событие “change” происходит, только когда пользователь меняет значение поля, а не когда это делает скрипт. 419 | 420 | Есть ещё один похожий на localStorage объект под названием sessionStorage. Разница между ними в том, что содержимое sessionStorage забывается по окончанию сессии, что для большинства браузеров означает момент закрытия. 421 | 422 | ##Итог 423 | HTML предоставляет множество различных типов полей формы – текстовые, галочки, множественного выбора, выбора файла. 424 | 425 | Из JavaScript можно получать значение и манипулировать этими полями. По изменению они запускают событие “change”, по вводу с клавиатуры – “input”, и ещё много разных клавиатурных событий. Они помогают нам отловить момент, когда пользователь взаимодействует с полем ввода. Свойства вроде value (для текстовых полей и select) или checked (для галочек и радиокнопок) используются для чтения и записи содержимого полей. 426 | 427 | При передаче формы происходит событие “submit”. Обработчик JavaScript затем может вызвать preventDefault этого события, чтобы остановить передачу данных. Элементы формы не обязаны быть заключены в теги `<form>`. 428 | 429 | Когда пользователь выбрал файл с жёсткого диска через поле выбора файла, интерфейс FileReader позволит нам добраться до содержимого файла из программы JavaScript. 430 | 431 | Объекты localStorage и sessionStorage можно использовать для хранения информации таким способом, который переживёт перезагрузку страницы. Первый сохраняет данные навсегда (ну или пока пользователь специально не сотрёт их), а второй – до закрытия браузера. 432 | 433 | ##Упражнения 434 | ###Верстак JavaScript 435 | Сделайте интерфейс, позволяющий писать и исполнять кусочки кода JavaScript. 436 | 437 | Сделайте кнопку рядом с `<textarea>`, по нажатию которой конструктор Function из главы 10 будет обёртывать введённый текст в функцию и вызывать его. Преобразуйте значение, возвращаемое функцией, или любую её ошибку, в строку, и выведите её после текстового поля. 438 | 439 | ```html 440 | <textarea id="code">return "hi";</textarea> 441 | <button id="button">Поехали</button> 442 | <pre id="output"></pre> 443 | 444 | <script> 445 | // Ваш код. 446 | </script> 447 | ``` 448 | 449 | ###Автодополнение 450 | Дополните текстовое поле так, что при вводе текста под ним появлялся бы список вариантов. У вас есть массив возможных вариантов, и показывать нужно те из них, которые начинаются с вводимого текста. Когда пользователь щёлкает по предложенному варианту, он меняет содержимое поля на него. 451 | 452 | ```html 453 | <input type="text" id="field"> 454 | <div id="suggestions" style="cursor: pointer"></div> 455 | 456 | <script> 457 | // Строит массив из имён глобальных перменных, 458 | // типа 'alert', 'document', и 'scrollTo' 459 | var terms = []; 460 | for (var name in window) 461 | terms.push(name); 462 | 463 | // Ваш код. 464 | </script> 465 | ``` 466 | 467 | ###Игра «Жизнь» Конвея 468 | Это простая симуляция жизни на прямоугольной решётке, каждый элемент которой живой или нет. Каждое поколение (шаг игры) применяются следующие правила: 469 | 470 | — каждая живая клетка, количество соседей которой меньше двух или больше трёх, погибает 471 | — каждая живая клетка, у которой от двух до трёх соседей, живёт до следующего хода 472 | — каждая мёртвая клетка, у которой есть ровно три соседа, оживает 473 | 474 | Соседи клетки – это все соседние с ней клетки по горизонтали, вертикали и диагонали, всего 8 штук. 475 | 476 | Обратите внимание, что правила применяются ко всей решётке одновременно, а не к каждой из клеток по очереди. То есть, подсчёт количества соседей происходит в один момент перед следующим шагом, и изменения, происходящие на соседних клетках, не влияют на новое состояние клетки. 477 | 478 | Реализуйте игру, используя любые подходящие структуры. Используйте Math.random для создания случайных начальных популяций. Выводите поле как решётку из галочек с кнопкой «перейти на следующий шаг». Когда пользователь включает или выключает галочки, эти изменения нужно учитывать при подсчёте следующего поколения. 479 | 480 | ```html 481 | <div id="grid"></div> 482 | <button id="next">Следующее поколение</button> 483 | 484 | <script> 485 | // Ваш код. 486 | </script> 487 | ``` 488 | -------------------------------------------------------------------------------- /chapters/chapter19.md: -------------------------------------------------------------------------------- 1 | #Проект: Paint 2 | 3 | <i>Я смотрю на многообразие цветов. Я смотрю на пустой холст. Затем я пытаюсь нанести цвета как слова, из которых возникают поэмы, как ноты, из которых возникает музыка.</i> 4 | 5 | <i>Жоан Миро</i> 6 | 7 | Материал предыдущих глав даёт вам всё необходимое для создания простого веб-приложения. Именно этим мы и займёмся. 8 | 9 | Наше приложение будет программой для рисования в браузере, схожей с Microsoft Paint. С его помощью можно будет открывать файлы с изображениями, малевать на них мышкой и сохранять обратно. Вот, как это будет выглядеть: 10 | 11 | <img src="../img/19-1.png"> 12 | 13 | ##Простая программа рисования 14 | Рисовать на компьютере клёво. Не надо волноваться насчёт материалов, умения, таланта. Просто берёшь, и начинаешь калякать. 15 | 16 | ##Реализация 17 | Интерфейс программы выводит вверху большой элемент `<canvas>`, под которым есть несколько полей ввода. Пользователь рисует на картинке, выбирая инструмент из поля `<select>`, а затем нажимая на холсте мышь. Есть инструменты для рисования линий, стирания кусочков картинки, добавления текста и т.п. 18 | 19 | Щелчок на холсте передаёт событие «mousedown» текущему инструменту, который обрабатывает его, как считает нужным. Рисование линий, например, будет слушать события «mousemove», пока кнопка мыши не будет отпущена, и нарисует линию по пути мыши текущим цветом и размером кисти. 20 | 21 | Цвет и размер кисти выбираются в дополнительных полях ввода. Они выполняют обновление свойств контекста рисования на холсте fillStyle, strokeStyle, и lineWidth каждый раз при их изменении. 22 | 23 | Загрузить картинку в программу можно двумя способами. Первый использует поле file, где пользователь выбирает файл со своего диска. Вторая запрашивает URL и скачивает картинку из интернета. 24 | 25 | Картинки хранятся нестандартным способом. Ссылка save с правой стороны ведёт на текущую картинку. По ней можно проходить, делиться ей или сохранять файл через неё. Я скоро объясню, как это работает. 26 | 27 | ##Строим DOM 28 | Интерфейс программы состоит из более чем 30 элементов DOM. Нужно их как-то собрать вместе. 29 | 30 | Очевидным форматом для сложных структур DOM является HTML. Но разделять программу на HTML и скрипт неудобно – для элементов DOM понадобится множество обработчиков событий или других необходимых вещей, которые надо будет как-то обрабатывать из скрипта. Для этого придётся делать много вызовов querySelector и им подобных, чтобы найти нужный элемент DOM для работы. 31 | 32 | Было бы удобно определять части DOM рядом с теми частями кода JavaScript, которые ими управляют. Поэтому я решил создавать всю конструкцию DOM прямо в JavaScript. Как мы видели в главе 13, встроенный интерфейс для создания структур DOM ужасно многословен. Поскольку нам придётся создать много конструкций, нам понадобится вспомогательная функция. 33 | 34 | Эта функция – расширенная версия функции elt из главы 13. Она создаёт элемент с заданным именем и атрибутами, и добавляет все остальные аргументы, которые получает, в качестве дочерних узлов, автоматически преобразовывая строки в текстовые узлы. 35 | 36 | ```js 37 | function elt(name, attributes) { 38 | var node = document.createElement(name); 39 | if (attributes) { 40 | for (var attr in attributes) 41 | if (attributes.hasOwnProperty(attr)) 42 | node.setAttribute(attr, attributes[attr]); 43 | } 44 | for (var i = 2; i < arguments.length; i++) { 45 | var child = arguments[i]; 46 | if (typeof child == "string") 47 | child = document.createTextNode(child); 48 | node.appendChild(child); 49 | } 50 | return node; 51 | } 52 | ``` 53 | 54 | Так мы легко и просто создаём элементы, не раздувая код до размеров лицензионного соглашения. 55 | 56 | ##Основание 57 | Ядро нашей программы – функция createPaint, добавляющая интерфейс рисования к элементу DOM, который передаётся в качестве аргумента. Так как мы создаём программу последовательно, мы определяем объект controls, который будет содержать функции для инициализации разных элементов управления под картинкой. 58 | 59 | ```js 60 | var controls = Object.create(null); 61 | 62 | function createPaint(parent) { 63 | var canvas = elt("canvas", {width: 500, height: 300}); 64 | var cx = canvas.getContext("2d"); 65 | var toolbar = elt("div", {class: "toolbar"}); 66 | for (var name in controls) 67 | toolbar.appendChild(controls[name](cx)); 68 | 69 | var panel = elt("div", {class: "picturepanel"}, canvas); 70 | parent.appendChild(elt("div", null, panel, toolbar)); 71 | } 72 | ``` 73 | 74 | У каждого элемента управления есть доступ к контексту рисования на холсте, а через него – к элементу `<canvas>`. Основное состояние программы хранится в этом холсте – он содержит текущую картинку, выбранный цвет (в свойстве fillStyle) и размер кисти (в свойстве lineWidth). 75 | 76 | Мы обернём холст и элементы управления в элементы `<div>` с классами, чтобы можно было добавить им стили, например серую рамку вокруг картинки. 77 | 78 | ##Выбор инструмента 79 | Первый элемент управления, который мы добавим – элемент `<select> `, позволяющий выбирать инструмент рисования. Как и в случае с controls, мы будем использовать объект для сбора необходимых инструментов, чтобы не надо было описывать их работу в коде по отдельности, и чтобы можно было легко добавлять новые. Этот объект связывает названия инструментов с функцией, которая вызывается при их выборе и при клике на холсте. 80 | 81 | ```js 82 | var tools = Object.create(null); 83 | 84 | controls.tool = function(cx) { 85 | var select = elt("select"); 86 | for (var name in tools) 87 | select.appendChild(elt("option", null, name)); 88 | 89 | cx.canvas.addEventListener("mousedown", function(event) { 90 | if (event.which == 1) { 91 | tools[select.value](event, cx); 92 | event.preventDefault(); 93 | } 94 | }); 95 | 96 | return elt("span", null, "Tool: ", select); 97 | }; 98 | ``` 99 | 100 | В поле tool есть элементы `<option>` для всех определённых нами инструментов, а обработчик события «mousedown» на холсте берёт на себя обязанность вызывать функцию текущего инструмента, передавая ей объекты event и context. Также он вызывает preventDefault, чтобы зажатие и перетаскивание мыши не вызывало выделения участков страницы. 101 | 102 | Самый простой инструмент – линия, который рисует линии за мышью. Чтобы рисовать линию, нам надо сопоставить координаты курсора мыши с координатами точек на холсте. Вскользь упомянутый в 13 главе метод getBoundingClientRect может нам в этом помочь. Он говорит, где показывается элемент, относительно левого верхнего угла экрана. Свойства события мыши clientX и clientY также содержат координаты относительно этого угла, поэтому мы можем вычесть верхний левый угол холста из них и получить позицию относительно этого угла. 103 | 104 | ```js 105 | function relativePos(event, element) { 106 | var rect = element.getBoundingClientRect(); 107 | return {x: Math.floor(event.clientX - rect.left), 108 | y: Math.floor(event.clientY - rect.top)}; 109 | } 110 | ``` 111 | 112 | Несколько инструментов рисования должны слушать событие «mousemove», пока кнопка мыши нажата. Функция trackDrag регистрирует и убирает событие для данных ситуаций. 113 | 114 | ```js 115 | function trackDrag(onMove, onEnd) { 116 | function end(event) { 117 | removeEventListener("mousemove", onMove); 118 | removeEventListener("mouseup", end); 119 | if (onEnd) 120 | onEnd(event); 121 | } 122 | addEventListener("mousemove", onMove); 123 | addEventListener("mouseup", end); 124 | } 125 | ``` 126 | 127 | У неё два аргумента. Один – функция, которая вызывается при каждом событии «mousemove», а другая – функция, которая вызывается при отпускании кнопки. Каждый аргумент может быть не задан. 128 | 129 | Инструмент для рисования линий использует две вспомогательные функции для самого рисования. 130 | 131 | ```js 132 | tools.Line = function(event, cx, onEnd) { 133 | cx.lineCap = "round"; 134 | 135 | var pos = relativePos(event, cx.canvas); 136 | trackDrag(function(event) { 137 | cx.beginPath(); 138 | cx.moveTo(pos.x, pos.y); 139 | pos = relativePos(event, cx.canvas); 140 | cx.lineTo(pos.x, pos.y); 141 | cx.stroke(); 142 | }, onEnd); 143 | }; 144 | ``` 145 | 146 | Функция сначала устанавливает свойство контекста lineCap в “round”, из-за чего концы нарисованного пути становятся закруглёнными, а не квадратными, как это происходит по умолчанию. Этот трюк обеспечивает непрерывность линий, когда они нарисованы в несколько приёмов. Если рисовать линии большой ширины, вы увидите разрывы в углах линий, если будете использовать установку lineCap по умолчанию. 147 | 148 | Затем, по каждому событию «mousemove», которое случается, пока кнопка нажата, рисуется простая линия между старой и новой позициями мыши, с использованием тех значений параметров strokeStyle и lineWidth, которые заданы в данный момент. 149 | 150 | Аргумент onEnd просто передаётся дальше, в trackDrag. При обычном вызове третий аргумент передаваться не будет, и при использовании функции он будет содержать undefined, поэтому в конце перетаскивания ничего не произойдёт. Но он поможет нам организовать ещё один инструмент, ластик erase, используя очень небольшое дополнение к коду. 151 | 152 | ```js 153 | tools.Erase = function(event, cx) { 154 | cx.globalCompositeOperation = "destination-out"; 155 | tools.Line(event, cx, function() { 156 | cx.globalCompositeOperation = "source-over"; 157 | }); 158 | }; 159 | ``` 160 | 161 | Свойство globalCompositeOperation влияет на то, как операции рисования на холсте меняют цвет пикселей. По умолчанию, значение свойства «source-over», что означает, что цвет, которым рисуют, накладывается поверх существующего. Если цвет непрозрачный, он просто заменит существующий, но если он частично прозрачный, они будут смешаны. 162 | 163 | Инструмент “erase” устанавливает globalCompositeOperation в «destination-out», что имеет эффект ластика, и делает пиксели снова прозрачными. 164 | 165 | Вот у нас уже есть два инструмента для рисования. Мы можем рисовать чёрные линии в один пиксель шириной (это задано значениями свойств холста strokeStyle и lineWidth по умолчанию), и стирать их. Работающий, хотя и примитивный, прототип программы. 166 | 167 | ##Цвет и размер кисти 168 | Предполагая, что пользователи захотят рисовать не только чёрным цветом и не только одним размером кисти, добавим элементы управления для этих настроек. 169 | 170 | В главе 18 я обсуждал разные варианты полей формы. Среди них не было полей для выбора цвета. По традиции у браузеров нет встроенных полей для выбора цвета, но за последнее время в стандарт включили несколько новых типов полей форм. Один из них — `<input type="color">`. Среди других — «date», «email», «url» и «number». Пока ещё их поддерживают не все. Для тега `<input>` тип по умолчанию – “text”, и при использовании нового тега, который ещё не поддерживается браузером, браузеры будут обрабатывать его как текстовое поле. Значит, пользователям с браузерами, которые не поддерживают инструмент для выбора цвета, необходимо будет вписывать название цвета вместо того, чтобы выбирать его через удобный элемент управления. 171 | 172 | ```js 173 | controls.color = function(cx) { 174 | var input = elt("input", {type: "color"}); 175 | input.addEventListener("change", function() { 176 | cx.fillStyle = input.value; 177 | cx.strokeStyle = input.value; 178 | }); 179 | return elt("span", null, "Color: ", input); 180 | }; 181 | ``` 182 | 183 | При смене значения поля color значения свойств контекста холста fillStyle и strokeStyle заменяются на новое значение. 184 | 185 | Настройка размера кисти работает сходным образом. 186 | 187 | ```js 188 | controls.brushSize = function(cx) { 189 | var select = elt("select"); 190 | var sizes = [1, 2, 3, 5, 8, 12, 25, 35, 50, 75, 100]; 191 | sizes.forEach(function(size) { 192 | select.appendChild(elt("option", {value: size}, 193 | size + " pixels")); 194 | }); 195 | select.addEventListener("change", function() { 196 | cx.lineWidth = select.value; 197 | }); 198 | return elt("span", null, "Brush size: ", select); 199 | }; 200 | ``` 201 | 202 | Код создаёт варианты размеров кистей из массива, и убеждается в том, что свойство холста lineWidth обновлено при выборе кисти. 203 | 204 | ##Сохранение 205 | Чтобы объяснить, как работает ссылка на сохранение, сначала мне нужно рассказать про URL с данными. В отличие от обычных http: и https:, URL с данными не указывают на ресурс, а содержат весь ресурс в себе. Это URL с данными, содержащий простой HTML документ: 206 | 207 | ``` 208 | data:text/html,<h1 style="color:red">Hello!</h1> 209 | ``` 210 | 211 | Такие URL полезны для разных вещей, как, например, включение небольших картинок прямо в файл стилей. Они также позволяют нам ссылаться на создаваемые файлы на стороне клиента, в браузере, не перемещая их сперва на какой-либо сервер. 212 | 213 | У элемента холста есть удобный метод toDataURL, который возвращает URL с данными, содержащий картинку на холсте в виде графического файла. Но нам не следует обновлять ссылку для сохранения при каждом изменении картинки. В случае больших картинок перемещение данных в URL занимает много времени. Вместо этого мы подключаем обновление к ссылке, чтоб она обновляла свой атрибут href каждый раз, когда она получает фокус с клавиатуры или над ней появляется курсор мыши. 214 | 215 | ```js 216 | controls.save = function(cx) { 217 | var link = elt("a", {href: "/"}, "Save"); 218 | function update() { 219 | try { 220 | link.href = cx.canvas.toDataURL(); 221 | } catch (e) { 222 | if (e instanceof SecurityError) 223 | link.href = "javascript:alert(" + 224 | JSON.stringify("Can't save: " + e.toString()) + ")"; 225 | else 226 | throw e; 227 | } 228 | } 229 | link.addEventListener("mouseover", update); 230 | link.addEventListener("focus", update); 231 | return link; 232 | }; 233 | ``` 234 | 235 | Таким образом, линк просто сидит себе тихонечко и указывает на неправильные данные, но как только пользователь приблизится к нему, он волшебным образом обновляет себя так, чтобы указывать на текущую картинку. 236 | 237 | Если вы загрузите большую картинку, некоторые браузеры поперхнутся слишком большим URL с данными, который получится в результате. Для маленьких картинок система работает без проблем. 238 | 239 | Но здесь мы опять сталкиваемся с деталями реализации песочницы в браузере. Когда картинка грузится с URL с другого домена, если ответ сервера не содержит заголовок, разрешающий использование ресурса с других доменов (см. главу 17), холст будет содержать информацию, которая будет видна пользователю, но не видна скрипту. 240 | 241 | Мы могли запросить картинку с приватной информацией (график изменений банковского счёта). Если бы скрипт мог получить к ней доступ, он мог бы шпионить за пользователем. 242 | 243 | Для предотвращения таких утечек информации, когда картинка, невидимая скрипту, будет загружена на холст, браузеры пометят его как «испорчен». Пиксельные данные, включая URL с данными, нельзя будет получить с «испорченного» холста. На него можно писать, но с него нельзя читать. 244 | 245 | Поэтому нам нужна обработка try/catch в функции update для ссылки сохранения. Когда холст «портится», вызов toDataURL выбросит исключение, являющееся экземпляром SecurityError. В этом случае мы перенаправляем ссылку на ещё один вид URL с протоколом javascript:. Такие ссылки просто выполняют скрипт, стоящий после двоеточия, и наша ссылка покажет предупреждение, сообщающее о проблеме. 246 | 247 | ##Загрузка картинок 248 | Последние два элемента управления используются для загрузки картинок с локального диска и с URL. Нам потребуется вспомогательная функция, которая пробует загрузить картинку с URL и заменить ею содержимое холста. 249 | 250 | ```js 251 | function loadImageURL(cx, url) { 252 | var image = document.createElement("img"); 253 | image.addEventListener("load", function() { 254 | var color = cx.fillStyle, size = cx.lineWidth; 255 | cx.canvas.width = image.width; 256 | cx.canvas.height = image.height; 257 | cx.drawImage(image, 0, 0); 258 | cx.fillStyle = color; 259 | cx.strokeStyle = color; 260 | cx.lineWidth = size; 261 | }); 262 | image.src = url; 263 | } 264 | ``` 265 | 266 | Нам надо поменять размер холста, чтобы он соответствовал картинке. Почему-то при смене размера холста его контекст забывает все настройки (fillStyle и lineWidth), в связи с чем функция сохраняет их и загружает обратно после обновления размера холста. 267 | 268 | Элемент управления для загрузки локального файла использует технику FileReader из главы 18. Кроме используемого здесь метода readAsText у таких объектов есть метод под названием readAsDataURL – а это то, что нам нужно. Мы загружаем файл, который пользователь выбирает, как URL с данными, и передаём его в loadImageURL для вывода на холст. 269 | 270 | ```js 271 | controls.openFile = function(cx) { 272 | var input = elt("input", {type: "file"}); 273 | input.addEventListener("change", function() { 274 | if (input.files.length == 0) return; 275 | var reader = new FileReader(); 276 | reader.addEventListener("load", function() { 277 | loadImageURL(cx, reader.result); 278 | }); 279 | reader.readAsDataURL(input.files[0]); 280 | }); 281 | return elt("div", null, "Open file: ", input); 282 | }; 283 | ``` 284 | 285 | Загружать файл с URL ещё проще. Но с текстовым полем мы не знаем, закончил ли пользователь набирать в нём URL, поэтому мы не можем просто слушать события “change”. Вместо этого мы обернём поле в форму и среагируем, когда она будет отправлена – либо по нажатию Enter, либо по нажатию кнопку load. 286 | 287 | ```js 288 | controls.openURL = function(cx) { 289 | var input = elt("input", {type: "text"}); 290 | var form = elt("form", null, 291 | "Open URL: ", input, 292 | elt("button", {type: "submit"}, "load")); 293 | form.addEventListener("submit", function(event) { 294 | event.preventDefault(); 295 | loadImageURL(cx, form.querySelector("input").value); 296 | }); 297 | return form; 298 | }; 299 | ``` 300 | 301 | Теперь мы определили все элементы управления, требующиеся нашей программе, но нужно добавить ещё несколько инструментов. 302 | 303 | ##Закругляемся 304 | Очень просто можно добавить инструмент для вывода текста, который выводит запрос пользователю, куда он должен ввести текст. 305 | 306 | ```js 307 | tools.Text = function(event, cx) { 308 | var text = prompt("Text:", ""); 309 | if (text) { 310 | var pos = relativePos(event, cx.canvas); 311 | cx.font = Math.max(7, cx.lineWidth) + "px sans-serif"; 312 | cx.fillText(text, pos.x, pos.y); 313 | } 314 | }; 315 | ``` 316 | 317 | Можно было бы добавить полей для размера текста и шрифта, но для простоты мы всегда используем шрифт sans-serif и размер шрифта, как у текущей кисти. Минимальный размер – 7 пикселей, потому что меньше текст будет нечитаемый. 318 | 319 | Ещё один необходимый инструмент для каляк-маляк – “аэрозоль”. Она рисует случайные точки под кистью, пока нажата кнопка мыши, создавая более или менее густые точки в зависимости от скорости движения курсора. 320 | 321 | ```js 322 | tools.Spray = function(event, cx) { 323 | var radius = cx.lineWidth / 2; 324 | var area = radius * radius * Math.PI; 325 | var dotsPerTick = Math.ceil(area / 30); 326 | 327 | var currentPos = relativePos(event, cx.canvas); 328 | var spray = setInterval(function() { 329 | for (var i = 0; i < dotsPerTick; i++) { 330 | var offset = randomPointInRadius(radius); 331 | cx.fillRect(currentPos.x + offset.x, 332 | currentPos.y + offset.y, 1, 1); 333 | } 334 | }, 25); 335 | trackDrag(function(event) { 336 | currentPos = relativePos(event, cx.canvas); 337 | }, function() { 338 | clearInterval(spray); 339 | }); 340 | }; 341 | ``` 342 | 343 | Аэрозоль использует setInterval для выплёвывания цветных точек каждые 25 миллисекунд, пока нажата кнопка мыши. Функция trackDrag используется для того, чтобы currentPos указывала на текущее положение курсора, и для выключения интервала при отпускании кнопки. 344 | 345 | Чтобы посчитать, сколько точек нужно нарисовать каждый раз по окончанию интервала, функция подсчитывает размер области текущей кисти и делит его на 30. Для поиска случайного положения под кистью используется функция randomPointInRadius. 346 | 347 | ```js 348 | function randomPointInRadius(radius) { 349 | for (;;) { 350 | var x = Math.random() * 2 - 1; 351 | var y = Math.random() * 2 - 1; 352 | if (x * x + y * y <= 1) 353 | return {x: x * radius, y: y * radius}; 354 | } 355 | } 356 | ``` 357 | 358 | Эта функция создаёт точки в квадрате между (-1,-1) и (1,1). Используя теорему Пифагора, она проверяет, лежит ли созданная точка внутри круга с радиусом 1. Когда такая точка находится, она возвращает её координаты, умноженные на радиус. 359 | 360 | Цикл нужен для равномерного распределения точек. Проще было бы создавать точки в круге, взяв случайный угол и радиус и вызвав Math.sin и Math.cos для создания точки. Но тогда точки с большей вероятностью появлялись бы ближе к центру круга. Это ограничение можно обойти, но результат будет сложнее, чем предыдущий цикл. 361 | 362 | Теперь наша программа для рисования готова. Запустите код и попробуйте. 363 | 364 | ```html 365 | <link rel="stylesheet" href="css/paint.css"> 366 | <body> 367 | <script>createPaint(document.body);</script> 368 | </body> 369 | ``` 370 | 371 | ##Упражнения 372 | В программе ещё очень много чего можно улучшить. Давайте добавим ей возможностей. 373 | 374 | ###Прямоугольники 375 | Определите инструмент Rectangle, заполняющий прямоугольник (см. метод fillRect из главы 16) текущим цветом. Прямоугольник должен появляться из той точки, где пользователь нажал кнопку мыши, и до той точки, где он отпустил кнопку. Заметьте, что последнее действие может произойти левее или выше первого. 376 | 377 | Когда это заработает, вы заметите, что изображение прямоугольника дрожит и его плохо видно. Можете ли вы придумать способ показа прямоугольника во время движения мыши, но чтобы он не выводился на холст, пока кнопка не отпущена? 378 | 379 | Если не придумаете, вспомните о стиле position: absolute, который мы обсуждали в главе 13, который можно использовать, чтобы выводить узел поверх остального документа. Свойства pageX и pageY событий мыши можно использовать для точного расположения элемента под мышью, записывая нужные значения в стили left, top, width и height. 380 | 381 | ```html 382 | <script> 383 | tools.Rectangle = function(event, cx) { 384 | // Ваш код 385 | }; 386 | </script> 387 | 388 | <link rel="stylesheet" href="css/paint.css"> 389 | <body> 390 | <script>createPaint(document.body);</script> 391 | </body> 392 | ``` 393 | 394 | ###Выбор цвета 395 | Ещё один часто встречающийся инструмент – выбор цвета, который позволяет щелчком мыши на картинке выбрать цвет, который находится под курсором. Сделайте такой инструмент. 396 | 397 | Для его изготовления понадобится доступ к содержимому холста. Метод toDataURL примерно это и делал, но получить информацию о пикселе из URL с данными сложно. Вместо этого мы возьмём метод контекста getImageData, возвращающий прямоугольный кусок картинки в виде объекта со свойствами width, height и data. В свойстве data содержится массив чисел от 0 до 255, и для каждого пикселя хранится четыре номера — red, green, blue и alpha (прозрачность). 398 | 399 | Этот пример получает числа из одного пикселя холста, один раз, когда тот пуст (все пиксели – прозрачные чёрные), и один раз, когда пиксель окрашен в красный цвет. 400 | 401 | ```js 402 | function pixelAt(cx, x, y) { 403 | var data = cx.getImageData(x, y, 1, 1); 404 | console.log(data.data); 405 | } 406 | 407 | var canvas = document.createElement("canvas"); 408 | var cx = canvas.getContext("2d"); 409 | pixelAt(cx, 10, 10); 410 | // → [0, 0, 0, 0] 411 | 412 | cx.fillStyle = "red"; 413 | cx.fillRect(10, 10, 1, 1); 414 | pixelAt(cx, 10, 10); 415 | // → [255, 0, 0, 255] 416 | ``` 417 | 418 | Аргументы getImageData показывают начальные координаты прямоугольника x и y, которые нам надо получить, за которыми идут ширина и высота. 419 | 420 | Игнорируйте прозрачность в этом упражнении, работайте только с первыми тремя цифрами для заданного пикселя. Также не волнуйтесь по поводу обновления поля color при выборе цвета. Просто убедитесь, что fillStyle и strokeStyle контекста установлены в цвет, оказавшийся под курсором. 421 | 422 | Помните, что эти свойства принимают любой цвет, который понимает CSS, включая запись вида rgb(R, G, B), которую вы видели в главе 15. 423 | 424 | Метод getImageData имеет те же ограничения, что и toDataURL – он выдаст ошибку, когда на холсте содержатся пиксели картинки, скачанной с другого домена. Используйте запись try/catch для сообщения об этих ошибках через окно alert. 425 | 426 | ```html 427 | <script> 428 | tools["Pick color"] = function(event, cx) { 429 | // Your code here. 430 | }; 431 | </script> 432 | 433 | <link rel="stylesheet" href="css/paint.css"> 434 | <body> 435 | <script>createPaint(document.body);</script> 436 | </body> 437 | ``` 438 | 439 | ###Заливка 440 | Это упражнение более сложное, чем предыдущие, и оно потребует разработки нетривиального решения хитрой задачи. Убедитесь, что у вас есть свободное время и терпение перед началом работы, и не отчаивайтесь, если сразу у вас что-то не будет получаться. 441 | 442 | Инструмент заливки окрашивает пиксель под курсором мыши и под целой группой пикселей вокруг него, имеющих тот же цвет. Для целей нашего упражнения мы будем считать, что эта группа включает все пиксели, до которых можно добраться от начального, двигаясь по одному пикселю по горизонтали и вертикали (но не по диагонали), не прикасаясь к пикселям, чей цвет отличается от исходного. 443 | 444 | Следующая картинка демонстрирует набор пикселей, окрашиваемых, когда инструмент заливки применяется к помеченному пикселю: 445 | 446 | <img src="../img/19-2.png"> 447 | 448 | Заливка не протекает через диагональные разрывы и не касается пикселей, которых нельзя достичь, даже если они того же цвета, что и исходный. 449 | 450 | Вам вновь понадобится getImageData для выяснения цвета пикселя. Скорее всего, удобнее будет получить всю картинку за раз, а потом уже получать данные по пикселям из получившегося массива. Пиксели в массиве организованы схожим образом с решёткой из главы 7, по рядам, только каждый пиксель описывается четырьмя значениями. Первое значение для пикселя с координатами (x,y) находится на позиции (x + y × width) × 4 451 | 452 | Включайте в рассмотрение четвёртое число (альфа), потому что нам нужно будет различать чёрные и пустые (прозрачные) пиксели. 453 | 454 | Поиск соседних пикселей того же цвета требует пройти по поверхности пикселей вверх, вниз, влево и вправо, пока там находятся пиксели того же цвета. За первый проход всю группу пикселей найти не получится. Вместо этого нужно будет сделать что-то похожее на отслеживание в регулярных выражениях, описанное в главе 9. Когда у вас есть больше одного возможного направления, нужно сохранить все те, по которым вы прямо сейчас не идёте, и просмотреть их позже, по окончанию текущего шага. 455 | 456 | У картинки среднего размера много пикселей. Постарайтесь свести работу программы к минимуму, или же она будет работать слишком долго. К примеру, игнорируйте пиксели, которые вы уже обрабатывали. 457 | 458 | Рекомендую для окраски отдельных пикселей вызывать fillRect, и хранить какую-то структуру данных, где записано, какие пиксели вы уже обошли. 459 | 460 | ```html 461 | <script> 462 | tools["Flood fill"] = function(event, cx) { 463 | // Ваш код 464 | }; 465 | </script> 466 | 467 | <link rel="stylesheet" href="css/paint.css"> 468 | <body> 469 | <script>createPaint(document.body);</script> 470 | </body> 471 | ``` 472 | -------------------------------------------------------------------------------- /chapters/chapter2.md: -------------------------------------------------------------------------------- 1 | #Структура программ 2 | 3 | <i>Сердце моё сияет ярко-красным светом под моей тонкой, прозрачной кожей, и им приходится вколоть мне десять кубиков JavaScript, чтобы вернуть меня к жизни (я хорошо реагирую на токсины в крови). От этой фигни у вас враз жабры побледнеют!</i> 4 | 5 | <i>_why, Why's (Poignant) Guide to Ruby</i> 6 | 7 | В этой главе мы начнём заниматься тем, что уже можно назвать программированием. Мы расширим использование языка JavaScript за пределы существительных и фрагментов предложений к более-менее осмысленной прозе. 8 | 9 | ##Выражения и инструкции 10 | В первой главе мы создавали величины и применяли к ним операторы, получая новые величины. Это важная часть каждой программы, но только лишь часть. 11 | 12 | Фрагмент кода, результатом работы которого является некая величина, называется выражением. Каждая величина, записанная буквально (например, 22 или “психоанализ”) тоже является выражением. Выражение, записанное в скобках, также является выражением, как и бинарный оператор, применяемый к двум выражениям или унарный – к одному. 13 | 14 | Это часть красоты языкового интерфейса. Выражения могут включать другие выражения так же, как сложноподчинённое предложение состоит из простых. Это позволяет нам комбинировать выражения для создания вычислений любой сложности. 15 | 16 | Если выражение – это фрагмент предложения, то инструкция – это предложение полностью. Программа – это просто список инструкций. 17 | 18 | Простейшая инструкция – это выражение с точкой с запятой после него. Это — программа: 19 | 20 | ```js 21 | 1; 22 | !false; 23 | ``` 24 | 25 | Правда, это бесполезная программа. Выражение можно использовать только для получения величины, которая может быть использована в другом выражении, охватывающем это. Инструкция стоит сама по себе и её применение изменяет что-то в мире программы. Она может выводить что-то на экран (изменение в мире), или менять внутреннее состояние машины таким образом, что это повлияет на следующие за ним инструкции. Эти изменения называются побочными эффектами. Инструкции в предыдущем примере просто выдают величины 1 и true, и сразу их выбрасывают. Они не оказывают никакого влияния на мир программы. При выполнении программы ничего заметного не происходит. 26 | 27 | В некоторых случаях JavaScript позволяет опускать точку с запятой в конце инструкции. В других случаях она обязательна, или следующая строка будет расцениваться как часть той же инструкции. Правила, согласно которым можно или нельзя опускать точку с запятой, довольно сложны и увеличивают вероятность ошибиться. В этой книге мы не будем опускать точку с запятой, и я рекомендую делать так же в своих программах, пока вы не накопите опыт. 28 | 29 | ##Переменные 30 | Как же программа хранит внутреннее состояние? Как она его запоминает? Мы получали новые величины из старых, но старые величины это не меняло, а новые нужно было использовать сразу, или же они исчезали. Чтобы захватить и хранить их, JavaScript предлагает нечто под названием «переменная». 31 | 32 | ```js 33 | var caught = 5 * 5; 34 | ``` 35 | 36 | И это даёт нам второй вид инструкций. Специальное ключевое слово (keyword) `var` показывает, что в этой инструкции мы объявляем переменную. За ним идёт имя переменной, и, если мы сразу хотим назначить ей значение – оператор = и выражение. 37 | 38 | Пример создаёт переменную под именем caught и использует её для захвата числа, которое получается в результате перемножения 5 и 5. 39 | 40 | После определения переменной её имя можно использовать в выражениях. Величина переменной будет такой, какое значение в ней сейчас содержится. Пример: 41 | 42 | ```js 43 | var ten = 10; 44 | console.log(ten * ten); 45 | // → 100 46 | ``` 47 | 48 | Переменные можно называть любым словом, которое не является ключевым (типа var). Нельзя использовать пробелы. Цифры тоже можно использовать, но не первым символом в названии. Нельзя использовать знаки пунктуации, кроме символов $ и _. 49 | 50 | Переменной присваивают значение не навсегда. Оператор = можно использовать на существующих переменных в любое время, чтобы присвоить им новое значение. 51 | 52 | ```js 53 | var mood = "лёгкое"; 54 | console.log(mood); 55 | // → лёгкое 56 | mood = "тяжёлое"; 57 | console.log(mood); 58 | // → тяжёлое 59 | ``` 60 | 61 | Представляйте себе переменные не в виде коробочек, а в виде щупалец. Они не содержат значения – они хватают их. Две переменные могут ссылаться на одно значение. Программа имеет доступ только к значениям, которые они содержат. Когда вам нужно что-то запомнить, вы отращиваете щупальце и держитесь за это, или вы используете существующее щупальце, чтобы удержать это. 62 | 63 | <img src="../img/2-1.jpg"> 64 | 65 | ##Переменные как щупальца 66 | Пример. Для запоминания количества денег, которые вам должен Василий, вы создаёте переменную. Затем, когда он выплачивает часть долга, вы даёте ей новое значение. 67 | 68 | ```js 69 | var vasyaDebt = 140; 70 | vasyaDebt = vasyaDebt - 35; 71 | console.log(vasyaDebt); 72 | // → 105 73 | ``` 74 | 75 | Когда вы определяете переменную без присваивания ей значения, щупальцу не за что держаться, оно висит в воздухе. Если вы запросите значение пустой переменной, вы получите undefined. 76 | 77 | Одна инструкция var может содержать несколько переменных. Определения нужно разделять запятыми. 78 | 79 | ```js 80 | var one = 1, two = 2; 81 | console.log(one + two); 82 | // → 3 83 | ``` 84 | 85 | ##Ключевые и зарезервированные слова 86 | Слова со специальным смыслом, типа var – ключевые. Их нельзя использовать как имена переменных. Также есть несколько слов, «зарезервированных для использования» в будуших версиях JavaScript. Их тоже нельзя использовать, хотя в некоторых средах исполнения это возможно. Полный их список достаточно большой. 87 | 88 | ``` 89 | break case catch continue debugger default delete do else false finally 90 | for function if implements in instanceof interface let new null package 91 | private protected public return static switch throw true try typeof var 92 | void while with yield this 93 | ``` 94 | 95 | Не нужно их запоминать, но имейте в виду, что ошибка может крыться здесь, если ваши определения переменных не работают, как надо. 96 | 97 | ##Окружение 98 | Коллекция переменных и их значений, которая существует в определённый момент, называется окружением. Когда программа запускается, окружение не пустое. Там всегда есть переменные, являющиеся частью программного стандарта, и большую часть времени там есть переменные, помогающие взаимодействовать с окружающей системой. К примеру, в браузере есть переменные и функции для изучения состояния загруженной веб-страницы и влияния на неё, для чтения ввода с мыши и клавиатуры. 99 | 100 | ##Функции 101 | Многие величины из стандартного окружения имеют тип `function` (функция). Функция – отдельный кусочек программы, который можно использовать вместе с другими величинами. К примеру, в браузере переменная alert содержит функцию, которая показывает небольшое окно с сообщением. Используют его так: 102 | 103 | ```js 104 | alert("С добрым утром!"); 105 | ``` 106 | 107 | <img src="../img/2-2.png"> 108 | 109 | ##Диалог alert 110 | Выполнение функции называют вызовом. Вы можете вызвать функцию, записав скобки после выражения, которое возвращает значение функции. Обычно вы напрямую используете имя функции в качестве выражения. Величины, которые можно написать внутри скобок, передаются программному коду внутри функции. В примере, функция alert использует данную ей строку для показа в диалоговом окне. Величины, передаваемые функциям, называются аргументами функций. Функция alert требует один аргумент, но другие могут требовать разное количество аргументов разных типов. 111 | 112 | ##Функция console.log 113 | Функция alert может использоваться как средство вывода при экспериментах, но закрывать каждый раз это окно вам скоро надоест. В прошлых примерах мы использовали функцию console.log для вывода значений. Большинство систем JavaScript (включая все современные браузеры и Node.js) предоставляют функцию console.log, которая выводит величины на какое-либо устройство вывода. В браузерах это консоль JavaScript. Эта часть браузера обычно скрыта – большинство браузеров показывают её по нажатию F12, или Command-Option-I на Маке. Если это не сработало, поищите в меню “web console” или “developer tools”. 114 | 115 | В примерах этой книги результаты вывода показаны в комментариях: 116 | 117 | ```js 118 | var x = 30; 119 | console.log("the value of x is", x); 120 | // → the value of x is 30 121 | ``` 122 | 123 | Хотя в именах переменных нельзя использовать точку – она, очевидно, содержится в названии console.log. Это оттого, что console.log – не простая переменная. Это выражение, возвращающее свойство log переменной console. Мы поговорим об этом в главе 4. 124 | 125 | ##Возвращаемые значения 126 | Показ диалогового окна или вывод текста на экран – это побочный эффект. Множество функций полезны оттого, что они производят эти эффекты. Функции также могут производить значения, и в этом случае им не нужен побочный эффект для того, чтобы быть полезной. К примеру, функция Math.max принимает любое количество переменных и возвращает значение самой большой: 127 | 128 | ```js 129 | console.log(Math.max(2, 4)); 130 | // → 4 131 | ``` 132 | 133 | Когда функция производит значение, говорят, что она возвращает значение. Всё, что производит значение – это выражение, то есть вызовы функций можно использовать внутри сложных выражений. К примеру, возвращаемое функцией Math.min (противоположность Math.max) значение используется как один из аргументов оператора сложения: 134 | 135 | ```js 136 | console.log(Math.min(2, 4) + 100); 137 | // → 102 138 | ``` 139 | 140 | В следующей главе описано, как писать собственные функции. 141 | 142 | ##prompt и confirm 143 | Окружение браузера содержит другие функции, кроме alert, которые показывают всплывающие окна. Можно вызвать окно с вопросом и кнопками OK/Cancel при помощи функции `confirm`. Она возвращает булевское значение – true, если нажато OK, и false, если нажато Cancel. 144 | 145 | ```js 146 | confirm("Ну что, поехали?"); 147 | ``` 148 | 149 | <img src="../img/2-3.png"> 150 | 151 | Функцию `prompt` можно использовать, чтобы задать открытый вопрос. Первый аргумент – вопрос, второй – текст, с которого пользователь начинает. В диалоговое окно можно вписать строку текста, и функция вернёт его в виде строки. 152 | 153 | ```js 154 | prompt("Расскажи мне всё, что знаешь.", "..."); 155 | ``` 156 | 157 | <img src="../img/2-4.png"> 158 | 159 | Эти функции нечасто используют, потому что нельзя изменять внешний вид этих окон — но они могут пригодиться для экспериментальных программ. 160 | 161 | ##Управление порядком выполнения программы 162 | Когда в программе больше одной инструкции, они выполняются сверху вниз. В этом примере у программы две инструкции. Первая спрашивает число, вторая, выполняемая следом, показывает его квадрат. 163 | 164 | ```js 165 | var theNumber = Number(prompt("Выбери число", "")); 166 | alert("Твоё число – квадратный корень из " + theNumber * theNumber); 167 | ``` 168 | 169 | Функция Number преобразовывает величину в число. Нам это нужно, потому что prompt возвращает строку. Есть сходные функции String и Boolean, преобразующие величины в соответствующие типы. 170 | 171 | Простая схема прямого порядка исполнения программы: 172 | 173 | <img src="../img/2-5.png"> 174 | 175 | Условное выполнение 176 | 177 | Выполнять инструкции по порядку – не единственная возможность. В качестве альтернативы существует условное выполнение, где мы выбираем из двух возможных путей, основываясь на булевской величине: 178 | 179 | <img src="../img/2-6.png"> 180 | 181 | Условное выполнение записывается при помощи ключевого слова if. В простом случае нам нужно, чтобы некий код был выполнен, только если выполняется некое условие. К примеру, в предыдущей программе мы можем считать квадрат, только если было введено именно число. 182 | 183 | ```js 184 | var theNumber = prompt("Выбери число ", ""); 185 | if (!isNaN(theNumber)) 186 | alert("Твоё число – квадратный корень из " + theNumber * theNumber); 187 | ``` 188 | 189 | Теперь, введя «сыр», вы не получите вывод. 190 | 191 | Ключевое слово if выполняет или пропускает инструкцию, в зависимости от значения булевого выражения. Это выражение записывается после if в скобках, и за ним идёт нужная инструкция. 192 | 193 | Функция `isNaN` – стандартная функция JavaScript, которая возвращает true, только если её аргумент – NaN (не число). Функция Number возвращает NaN, если задать ей строку, которая не представляет собой допустимое число. В результате, условие звучит так: «выполнить, если только theNumber не является не-числом». 194 | 195 | Часто нужно написать код не только для случая, когда выражение истинно, но и для случая, когда оно ложно. Путь с вариантами – это вторая стрелочка диаграммы. Ключевое слово else используется вместе с if для создания двух раздельных путей выполнения. 196 | 197 | ```js 198 | var theNumber = Number(prompt("Выбери число", "")); 199 | if (!isNaN(theNumber)) 200 | alert("Твоё число – квадратный корень из " + theNumber * theNumber); 201 | else 202 | alert("Ну ты что число-то не ввёл?"); 203 | ``` 204 | 205 | Если вам нужно больше разных путей, можно использовать несколько пар if/else по цепочке. 206 | 207 | ```js 208 | var num = Number(prompt("Выбери число", "0")); 209 | 210 | if (num < 10) 211 | alert("Маловато"); 212 | else if (num < 100) 213 | alert("Нормально"); 214 | else 215 | alert("Многовато"); 216 | ``` 217 | 218 | Программа проверяет, действительно ли num меньше 10. Если да – выбирает эту ветку, и показывает «Маловато». Если нет, выбирает другую – на которой ещё один if. Если следующее условие выполняется, значит номер будет между 10 и 100, и выводится «Нормально». Если нет – значит, выполняется последняя ветка. 219 | 220 | Последовательность выполнения примерно такая: 221 | 222 | <img src="../img/2-7.png"> 223 | 224 | ##Циклы while и do 225 | Представьте программу, выводящую все чётные числа от 0 до 12. Можно записать её так: 226 | 227 | ```js 228 | console.log(0); 229 | console.log(2); 230 | console.log(4); 231 | console.log(6); 232 | console.log(8); 233 | console.log(10); 234 | console.log(12); 235 | ``` 236 | 237 | Это работает – но смысл программирования в том, чтобы работать меньше, чем компьютер, а не наоборот. Если б нам понадобились все числа до 1000, это решение было бы неприемлемым. Нам нужна возможность повторения. Этот вид контроля над порядком выполнения называется циклом. 238 | 239 | <img src="../img/2-8.png"> 240 | 241 | Зацикливание даёт возможность вернуться назад к какой-то инструкции и повторить всё заново с новым состоянием программы. Если скомбинировать это с переменной для подсчёта, можно сделать следующее: 242 | 243 | ```js 244 | var number = 0; 245 | while (number <= 12) { 246 | console.log(number); 247 | number = number + 2; 248 | } 249 | // → 0 250 | // → 2 251 | // … и т.д. 252 | ``` 253 | 254 | Инструкция, начинающаяся с ключевого слова `while` – это цикл. За while следует выражение в скобках, и затем инструкция (тело цикла) – так же, как у if. Цикл выполняет инструкцию, пока выражение выдаёт истинный результат. 255 | 256 | В цикле нам нужно выводить значение и прибавлять к нему. Если нам нужно выполнять в цикле несколько инструкций, мы заключаем его в фигурные скобки { }. Фигурные скобки для инструкций – как круглые скобки для выражений. Они группируют их и превращают в единое. Последовательность инструкций, заключённая в фигурные скобки, называется блоком. 257 | 258 | Много программистов заключают любое тело цикла в скобки. Они делают это для единообразия, и для того, чтобы не нужно было добавлять и убирать скобки, если приходится изменять количество инструкций в цикле. В книге я не буду писать скобки вокруг единичных инструкций в цикле, так как люблю краткость. Вы можете делать, как угодно. 259 | 260 | Переменная number показывает, как переменная может отслеживать прогресс программы. При каждом повторении цикла number увеличивается на 2. Перед каждым повторением оно сравнивается с 12, чтобы понять, сделала ли программа всё, что требовалось. 261 | 262 | Для примера более полезной работы мы можем написать программу вычисления 2 в 10 степени. Мы используем две переменные: одну для слежения за результатом, а вторую – для подсчёта количества умножений. Цикл проверяет, достигла ли вторая переменная 10, и затем обновляет обе. 263 | 264 | ```js 265 | var result = 1; 266 | var counter = 0; 267 | while (counter < 10) { 268 | result = result * 2; 269 | counter = counter + 1; 270 | } 271 | console.log(result); 272 | // → 1024 273 | ``` 274 | 275 | Можно начинать counter с 1 и проверять его на <=10, но по причинам, которые станут ясны далее, всегда лучше начинать счётчики с 0. 276 | 277 | Цикл do похож на цикл while. Отличается только в одном: цикл do всегда выполняет тело хотя бы один раз, а проверяет условие после первого выполнения. Поэтому и тестируемое выражение записывают после тела цикла: 278 | 279 | ```js 280 | do { 281 | var name = prompt("Who are you?"); 282 | } while (!name); 283 | console.log(name); 284 | ``` 285 | 286 | Эта программа заставляет ввести имя. Она спрашивает его снова и снова, пока не получит что-то кроме пустой строки. Добавление "!" превращает значение в булевское и затем применяет логическое отрицание, а все строки, кроме пустой, преобразуются в булевское true. 287 | 288 | Вы, наверно, заметили пробелы перед некоторыми инструкциями. В JavaScript это не обязательно – программа отработает и без них. Даже переводы строк не обязательно делать. Можно написать программу в одну строку. Роль пробелов в блоках – отделять их от остальной программы. В сложном коде, где в блоках встречаются другие блоки, может быть сложно разглядеть, где кончается один и начинается другой. Правильно отделяя их пробелами вы приводите в соответствие внешний вид кода и его блоки. Я люблю отделять каждый блок двумя пробелами, но вкусы различаются – некоторые используют четыре, некоторые – табуляцию. Чем больше пробелов использовать, тем заметнее отступ, но тем быстрее вложенные блоки убегают за правый край экрана. 289 | 290 | ##Циклы for 291 | Много циклов строятся по такому шаблону, как в примере. Создаётся переменная-счётчик, потом идёт цикл while, где проверочное выражение обычно проверяет, не достигли ли мы какой-нибудь границы. В конце тела цикла счётчик обновляется. 292 | 293 | Поскольку это такой частый случай, в JavaScript есть вариант покороче, цикл `for`. 294 | 295 | ```js 296 | for (var number = 0; number <= 12; number = number + 2) 297 | console.log(number); 298 | // → 0 299 | // → 2 300 | // … и т.д. 301 | ``` 302 | 303 | Эта программа эквивалентна предыдущей. Только теперь все инструкции, относящиеся к отслеживанию состояния цикла, сгруппированы. 304 | 305 | Скобки после for содержат две точки с запятой, разделяя инструкцию на три части. Первая инициализирует цикл, обычно задавая начальное значение переменной. Вторая – выражение проверки необходимости продолжения цикла. Третья – обновляет состояние после каждого прохода. В большинстве случаев такая запись более короткая и понятная, чем while. 306 | 307 | Вычисляем 2^10 при помощи for: 308 | 309 | ```js 310 | var result = 1; 311 | for (var counter = 0; counter < 10; counter = counter + 1) 312 | result = result * 2; 313 | console.log(result); 314 | // → 1024 315 | ``` 316 | 317 | Хотя я не писал фигурных скобок, я отделяю тело цикла пробелами. 318 | 319 | ##Выход из цикла 320 | 321 | Дождаться, пока условие цикла не станет ложным – не единственный способ закончить цикл. Специальная инструкция `break` приводит к немедленному выходу из цикла. 322 | 323 | В следующем примере мы покидаем цикл, когда находим число, большее 20 и делящееся на 7 без остатка. 324 | 325 | ```js 326 | for (var current = 20; ; current++) { 327 | if (current % 7 == 0) 328 | break; 329 | } 330 | console.log(current); 331 | // → 21 332 | ``` 333 | 334 | Конструкция for не имеет проверочной части – поэтому цикл не остановится, пока не сработает инструкция break. 335 | 336 | Если вы не укажете эту инструкцию, или случайно напишете условие, которое всегда выполняется, программа зависнет в бесконечном цикле и никогда не закончит работу – обычно это плохо. 337 | 338 | Если вы сделаете бесконечный цикл, обычно через несколько секунд среда исполнения предложит вам прервать его. Если нет, вам придётся закрыть закладку, или даже весь браузер. 339 | 340 | Ключевое слово `continue` также влияет на исполнение цикла. Когда это слово встречается в цикле, он немедленно переходит на следующую итерацию. 341 | 342 | ##Короткое обновление переменных 343 | Особенно часто в циклах программе нужно обновить переменную, основываясь на её предыдущем состоянии. 344 | 345 | ```js 346 | counter = counter + 1; 347 | ``` 348 | 349 | В JavaScript есть для этого короткая запись: 350 | 351 | ```js 352 | counter += 1; 353 | ``` 354 | 355 | Подобные записи работают для многих других операторов, к примеру result *= 2 для удвоения, или counter -= 1 для обратного отсчёта. 356 | 357 | Это позволяет нам сократить программу вывода чётных чисел: 358 | 359 | ```js 360 | for (var number = 0; number <= 12; number += 2) 361 | console.log(number); 362 | ``` 363 | 364 | Для counter += 1 и counter -= 1 есть ещё более короткие записи: counter++ и counter--. 365 | 366 | ##Работаем с переменными при помощи switch 367 | 368 | Часто код выглядит так: 369 | 370 | ```js 371 | if (variable == "value1") action1(); 372 | else if (variable == "value2") action2(); 373 | else if (variable == "value3") action3(); 374 | else defaultAction(); 375 | ``` 376 | 377 | Существует конструкция под названием `switch`, которая упрощает подобную запись. К сожалению, синтаксис JavaScript в этом случае довольно странный – часто цепочка if/else выглядит лучше. Пример: 378 | 379 | ```js 380 | switch (prompt("Как погодка?")) { 381 | case "дождь": 382 | console.log("Не забудь зонт."); 383 | break; 384 | case "снег": 385 | console.log("Блин, мы в России!"); 386 | break; 387 | case "солнечно": 388 | console.log("Оденься полегче."); 389 | case "облачно": 390 | console.log("Иди гуляй."); 391 | break; 392 | default: 393 | console.log("Непонятная погода!"); 394 | break; 395 | } 396 | ``` 397 | 398 | В блок switch можно поместить любое количество меток `case`. Программа перепрыгивает на метку, соответствующую значению переменной в switch, или на метку `default`, если подходящих меток не найдено. После этого инструкции исполняются до первой инструкции `break` – даже если мы уже прошли другую метку. Иногда это можно использовать для исполнения одного и того же кода в разных случаях (в обоих случаях «солнечно» и «облачно» программа порекомендует пойти погулять). Однако очень легко забыть запись break, что приведёт к выполнению нежелательного участка кода. 399 | 400 | ##Регистр имён 401 | Имена переменных не могут содержать пробелы, однако часто удобно использовать несколько слов для понятного описания переменной. Вы можете выбирать из нескольких вариантов: 402 | 403 | ``` 404 | fuzzylittleturtle 405 | fuzzy_little_turtle 406 | FuzzyLittleTurtle 407 | fuzzyLittleTurtle 408 | ``` 409 | 410 | Первый довольно сложно читать. Мне нравятся подчёркивания, хотя их не очень удобно печатать. Стандартные функции JavaScript и большинство программистов используют последний вариант – каждое слово с большой буквы, кроме первого. 411 | 412 | В некоторых случаях, например в случае функции Number, первую букву тоже пишут большой – когда нужно выделить функцию как конструктор. О конструкторах мы поговорим в главе 6. Сейчас просто не обращайте на это внимания. 413 | 414 | ##Комментарии 415 | Часто код не содержит всю информацию, которую хотелось бы передать читателям-людям, или доносит её в непонятном виде. Иногда вы чувствуете поэтическое вдохновение, или просто хотите поделиться мыслями в своей программе. Для этого служат комментарии. 416 | 417 | Комментарий – это текст, который записан в программе, но игнорируется компьютером. В JavaScript комментарии можно писать двумя способами. Для однострочного комментария можно использовать два слеша: 418 | 419 | ```js 420 | var accountBalance = calculateBalance(account); 421 | // Издалека долго 422 | accountBalance.adjust(); 423 | // Течёт река Волга 424 | var report = new Report(); 425 | // Течёт река Волга 426 | addToReport(accountBalance, report); 427 | // Конца и края нет 428 | ``` 429 | 430 | Комментарий продолжается только до конца строки. Код между символами /\* и \*/ будет игнорироваться вместе с возможными переводами строки. Это подходит для включения целых информационных блоков в программу: 431 | 432 | ```js 433 | /* 434 | Этот город – самый лучший 435 | Город на Земле. 436 | Он как будто нарисован 437 | Мелом на стене. 438 | */ 439 | var myCity = 'Челябинск'; 440 | ``` 441 | 442 | ##Итог 443 | Теперь вы знаете, что программа состоит из инструкций, которые сами могут содержать инструкции. В инструкциях содержатся выражения, которые могут состоять из выражений. 444 | 445 | Записывая инструкции подряд, мы получаем программу, которая выполняется сверху вниз. Вы можете изменять этот поток выполнения, используя условные (if, else и switch) операторы и операторы цикла (while, do и for). 446 | 447 | Переменные можно использовать для хранения кусочков данных под определённым названием и для отслеживания состояния программы. Окружение – набор определённых переменных. Системы, исполняющие JavaScript, всегда добавляют несколько стандартных переменных в ваше окружение. 448 | 449 | Функции – особые переменные, включающие части программы. Их можно вызвать командой functionName(argument1, argument2). Такой вызов – это выражение и может выдавать значение. 450 | 451 | ##Упражнения 452 | Каждое упражнение начинается с описания задачи. Прочтите и постарайтесь выполнить. В сложных ситуациях обращайтесь к подсказкам. Готовые решения задач можно найти на сайте книги <a href="http://eloquentjavascript.net/code/">eloquentjavascript.net/code/</a>. Чтобы обучение было эффективным, не заглядывайте в ответы, пока не решите задачу сами, или хотя бы не попытаетесь её решить достаточно долго для того, чтобы у вас слегка заболела голова. Там же можно писать код прямо в браузере и выполнять его. 453 | 454 | ###Треугольник в цикле 455 | Напишите цикл, который за 7 вызовов console.log выводит такой треугольник: 456 | 457 | ``` 458 | # 459 | ## 460 | ### 461 | #### 462 | ##### 463 | ###### 464 | ####### 465 | ``` 466 | 467 | Будет полезно знать, что длину строки можно узнать, приписав к переменной .length. 468 | 469 | ```js 470 | var abc = "abc"; 471 | console.log(abc.length); 472 | // → 3 473 | ``` 474 | 475 | ###FizzBuzz 476 | Напишите программу, которая выводит через console.log все числа от 1 до 100, с двумя исключениями. Для чисел, нацело делящихся на 3, она должна выводить ‘Fizz’, а для чисел, делящихся на 5 (но не на 3) – ‘Buzz’. 477 | 478 | Когда сумеете – исправьте её так, чтобы она выводила «FizzBuzz» для всех чисел, которые делятся и на 3, и на 5. 479 | 480 | (На самом деле, этот вопрос подходит для собеседований, и, говорят, он позволяет отсеивать довольно большое число кандидатов. Поэтому, когда вы решите эту задачу, можете себя похвалить.) 481 | 482 | ###Шахматная доска 483 | Напишите программу, создающую строку, содержащую решётку 8х8, в которой линии разделяются символами новой строки. На каждой позиции либо пробел, либо #. В результате должна получиться шахматная доска. 484 | 485 | ``` 486 | # # # # 487 | # # # # 488 | # # # # 489 | # # # # 490 | # # # # 491 | # # # # 492 | # # # # 493 | # # # # 494 | ``` 495 | 496 | Когда справитесь, сделайте размер доски переменным, чтобы можно было создавать доски любого размера. 497 | -------------------------------------------------------------------------------- /chapters/chapter8.md: -------------------------------------------------------------------------------- 1 | #Поиск и обработка ошибок 2 | 3 | <i>Отладка изначально вдвое сложнее написания кода. Поэтому, если вы пишете код настолько заумный, насколько можете, то по определению вы не способны отлаживать его.</i> 4 | 5 | <i>Брайан Керниган и П.Ж.Плауэр, «Основы программного стиля»</i> 6 | 7 | <i>Юан-Ма написал небольшую программу, использующую много глобальных переменных и ужасных хаков. Ученик, читая программу, спросил его: «Вы предупреждали нас о подобных техниках, но при этом я нахожу их в вашей же программе. Как это возможно?» Мастер ответил: «Не нужно бежать за поливальным шлангом, если дом не горит».</i> 8 | 9 | <i>Мастер Юан-Ма, «Книга программирования».</i> 10 | 11 | Программа – это кристаллизованная мысль. Иногда мысли путаются. Иногда при превращении мыслей в программу в код вкрадываются ошибки. В обоих случаях получается повреждённая программа. 12 | 13 | Недостатки в программах обычно называют ошибками. Это могут быть ошибки программиста или проблемы в системах, с которыми программа взаимодействует. Некоторые ошибки очевидны, другие – трудноуловимы и могут скрываться в системах годами. 14 | 15 | Часто проблема возникает в тех ситуациях, возникновение которых программист изначально не предвидел. Иногда этих ситуаций нельзя избежать. Когда пользователя просят ввести его возраст, а он вводит «апельсин», это ставит программу в непростую ситуацию. Эти ситуации необходимо предвидеть и как-то обрабатывать. 16 | 17 | ##Ошибки программистов 18 | В случае ошибок программистов наша цель ясна. Нам надо найти их и исправить. Таковые ошибки варьируются от простых опечаток, на которые компьютер пожалуется сразу же, как только увидит программу, до скрытых ошибок в нашем понимании того, как программа работает, которые приводят к неправильным результатам в особых случаях. Ошибки последнего рода можно искать неделями. 19 | 20 | Разные языки по-разному могут помогать вам в поиске ошибок. К сожалению, JavaScript находится на конце этой шкалы, обозначенном как «вообще почти не помогает». Некоторым языкам надо точно знать типы всех переменных и выражений ещё до запуска программы, и они сразу сообщат вам, если типы использованы некорректно. JavaScript рассматривает типы только во время исполнения программ, и даже тогда он разрешает делать не очень осмысленные вещи без всяких жалоб, например 21 | 22 | ```js 23 | x = true * "обезьяна" 24 | ``` 25 | 26 | На некоторые вещи JavaScript всё-таки жалуется. Написание синтаксически неправильной программы сразу вызовет ошибку. Другие ошибки, например вызов чего-либо, не являющегося функцией, или обращение к свойству неопределённой переменной, возникнут при выполнении программы, когда она сталкивается с такой бессмысленной ситуацией. 27 | 28 | Но часто ваши бессмысленные вычисления просто породят NaN (not a number) или undefined. Программа радостно продолжит, будучи уверенной в том, что она делает что-то осмысленное. Ошибка проявит себя позже, когда такое фиктивное значение уже пройдёт через несколько функций. Она может вообще не вызвать сообщение об ошибке, а просто привести к неправильному результату выполнения. Поиск источника таких проблем – сложная задача. 29 | 30 | Процесс поиска ошибок (bugs) в программах называется отладкой (debugging). 31 | 32 | ##Строгий режим (strict mode) 33 | JavaScript можно заставить быть построже, переведя его в строгий режим. Для этого наверху файла или тела функции пишется "use strict". Пример: 34 | 35 | ```js 36 | function canYouSpotTheProblem() { 37 | "use strict"; 38 | for (counter = 0; counter < 10; counter++) 39 | console.log("Всё будет офигенно"); 40 | } 41 | 42 | canYouSpotTheProblem(); 43 | // → ReferenceError: counter is not defined 44 | ``` 45 | 46 | Обычно, когда ты забываешь написать var перед переменной, как в примере перед counter, JavaScript по-тихому создаёт глобальную переменную и использует её. В строгом режиме выдаётся ошибка. Это очень удобно. Однако, ошибка не выдаётся, когда глобальная переменная уже существует – только тогда, когда присваивание создаёт новую переменную. 47 | 48 | Ещё одно изменение – привязка this содержит undefined в тех функциях, которые вызывали не как методы. Когда мы вызываем функцию не в строгом режиме, this ссылается на объект глобальной области видимости. Поэтому если вы случайно неправильно вызовете метод в строгом режиме, JavaScript выдаст ошибку, если попытается прочесть что-то из this, а не будет радостно работать с глобальным объектом. 49 | 50 | К примеру, рассмотрим код, вызывающий конструктор без ключевого слова new, в случае чего this не будет ссылаться на создаваемый объект. 51 | 52 | ```js 53 | function Person(name) { this.name = name; } 54 | var ferdinand = Person("Евлампий"); // ой-вэй 55 | console.log(name); 56 | // → Евлампий 57 | ``` 58 | 59 | Некорректный вызов Person успешно происходит, но возвращается как undefined и создаёт глобальную переменную name. В строгом режиме всё по-другому: 60 | 61 | ```js 62 | "use strict"; 63 | function Person(name) { this.name = name; } 64 | // Опаньки, мы ж забыли 'new' 65 | var ferdinand = Person("Евлампий"); 66 | // → TypeError: Cannot set property 'name' of undefined 67 | ``` 68 | 69 | Нам сразу сообщают об ошибке. Очень удобно. 70 | 71 | Строгий режим умеет ещё кое-что. Он запрещает вызывать функцию с несколькими параметрами с одним и тем же именем, и удаляет некоторые потенциально проблемные свойства языка (например, инструкцию with, которая настолько ужасна, что даже не обсуждается в этой книге). 72 | 73 | Короче говоря, надпись "use strict" перед текстом программы редко причиняет проблемы, зато помогает вам видеть их. 74 | 75 | ##Тестирование 76 | Если язык не собирается помогать нам в поиске ошибок, приходится искать их сложным способом: запуская программу и наблюдая, делает ли она что-то так, как надо. 77 | 78 | Делать это вручную, снова и снова – верный способ сойти с ума. К счастью, часто возможно написать другую программу, которая автоматизирует проверку вашей основной программы. 79 | 80 | Для примера вновь обратимся к типу Vector. 81 | 82 | ```js 83 | function Vector(x, y) { 84 | this.x = x; 85 | this.y = y; 86 | } 87 | Vector.prototype.plus = function(other) { 88 | return new Vector(this.x + other.x, this.y + other.y); 89 | }; 90 | ``` 91 | 92 | Мы напишем программу, которая проверит, что наша реализация Vector работает, как нужно. Затем после каждого изменения реализации мы будем запускать проверочную программу, чтобы убедиться, что мы ничего не сломали. Когда мы добавим функциональности (к примеру, новый метод) к типу Vector, мы добавим проверок этой новой функциональности. 93 | 94 | ```js 95 | function testVector() { 96 | var p1 = new Vector(10, 20); 97 | var p2 = new Vector(-10, 5); 98 | var p3 = p1.plus(p2); 99 | 100 | if (p1.x !== 10) return "облом: значение x не то"; 101 | if (p1.y !== 20) return "облом: значение y не то"; 102 | if (p2.x !== -10) return "облом: отрицательное значение x не то"; 103 | if (p3.x !== 0) return "облом: результат сложения x не тот"; 104 | if (p3.y !== 25) return "облом: результат сложения y не тот"; 105 | return "всё пучком"; 106 | } 107 | console.log(testVector()); 108 | // → всё пучком 109 | ``` 110 | 111 | Написание таких проверок приводит к появлению повторяющегося кода. К счастью, есть программные продукты, помогающие писать наборы проверок при помощи специального языка, приспособленного именно для написания проверок. Их называют testing frameworks. 112 | 113 | ##Отладка (debugging) 114 | Когда вы заметили проблему в программе (она ведёт себя неправильно и выдаёт ошибки), самое время выяснить, в чём проблема. 115 | 116 | Иногда это очевидно. Сообщение об ошибке наводит вас на конкретную строку программы, и если вы прочтёте описание ошибки и эту строку, вы часто сможете найти проблему. 117 | 118 | Но не всегда. Иногда строчка, приводящая к ошибке, просто оказывается первым местом, где некорректное значение, полученное где-то ещё, используется неправильно. Иногда вообще нет сообщения об ошибке – есть просто неверный результат. Если вы делали упражнения из предыдущих глав, вы наверняка попадали в такие ситуации. 119 | 120 | Следующий пример пробует преобразовать число заданной системы счисления в строку, отнимая последнюю цифру и совершая деление, чтобы избавиться от этой цифры. Но дикий результат, выдаваемый программой, как бы намекает на присутствие в ней ошибки. 121 | 122 | ```js 123 | function numberToString(n, base) { 124 | var result = "", sign = ""; 125 | if (n < 0) { 126 | sign = "-"; 127 | n = -n; 128 | } 129 | do { 130 | result = String(n % base) + result; 131 | n /= base; 132 | } while (n > 0); 133 | return sign + result; 134 | } 135 | console.log(numberToString(13, 10)); 136 | // → 1.5e-3231.3e-3221.3e-3211.3e-3201.3e-3191.3e-3181.3… 137 | ``` 138 | 139 | Даже если вы нашли проблему – притворитесь, что ещё не нашли. Мы знаем, что программа сбоит, и нам нужно узнать, почему. 140 | 141 | Здесь вам надо преодолеть желание начать вносить случайные изменения в код. Вместо этого подумайте. Проанализируйте результат и придумайте теорию, по которой это происходит. Проведите дополнительные наблюдения для проверки теории, а если теории нет – проведите наблюдения, которые бы помогли вам изобрести её. 142 | 143 | Размещение нескольких вызовов console.log в стратегических местах – хороший способ получить дополнительную информацию о том, что программа делает. В нашем случае нам нужно, чтобы n принимала значения 13, 1, затем 0. Давайте выведем значения в начале цикла: 144 | 145 | ```js 146 | 13 147 | 1.3 148 | 0.13 149 | 0.013 150 | … 151 | 1.5e-323 152 | ``` 153 | 154 | Н-да. Деление 13 на 10 выдаёт не целое число. Вместо n /= base нам нужно n = Math.floor(n / base), тогда число будет корректно «сдвинуто» вправо. 155 | 156 | Кроме console.log можно воспользоваться отладчиком в браузере. Современные браузеры умеют ставить точку остановки на выбранной строчке кода. Это приведёт к приостановке выполнения программы каждый раз, когда будет достигнута выбранная строчка, и тогда вы сможете просмотреть содержимое переменных. Не буду подробно расписывать процесс, поскольку у разных браузеров он организован по-разному – поищите в вашем браузере “developer tools”, инструменты разработчика. Ещё один способ установить точку остановки – включить в код инструкцию для отладчика, состоящую из ключевого слова debugger. Если инструменты разработчика активны, исполнение программы будет приостановлено на этой инструкции, и вы сможете изучить состояние программы. 157 | 158 | ##Распространение ошибок 159 | К сожалению, программист может предотвратить появление не всех проблем. Если ваша программа общается с внешним миром, она может получить неправильные входные данные, или же системы, с которыми она пытается взаимодействовать, окажутся сломанными или недоступными. 160 | 161 | Простые программы, или программы, работающие под вашим надзором, могут просто «сдаваться» в такой момент. Вы можете изучить проблему и попробовать снова. «Настоящие» приложения не должны просто «падать». Иногда приходится принимать неправильные входные данные и как-то с ними работать. В других случаях нужно сообщить пользователю, что что-то пошло не так, и потом уже сдаваться. В любом случае программа должна что-то сделать в ответ на возникновение проблемы. 162 | 163 | Допустим, у вас есть функция promptInteger, которая запрашивает целое число и возвращает его. Что она должна сделать, если пользователь введёт «апельсин»? 164 | 165 | Один из вариантов – вернуть особое значение. Обычно для этих целей используют null и undefined. 166 | 167 | ```js 168 | function promptNumber(question) { 169 | var result = Number(prompt(question, "")); 170 | if (isNaN(result)) return null; 171 | else return result; 172 | } 173 | 174 | console.log(promptNumber("Сколько пальцев видите?")); 175 | ``` 176 | 177 | Это надёжная стратегия. Теперь любой код, вызывающий promptNumber, должен проверять, было ли возвращено число, и если нет, как-то выйти из ситуации – спросить снова, или задать значение по-умолчанию. Или вернуть специальное значение уже тому, кто его вызвал, сообщая о неудаче. 178 | 179 | Во многих таких случаях, когда ошибки возникают часто и вызывающий функцию код должен принимать их во внимание, совершенно допустимо возвращать специальное значение как индикатор ошибки. Но есть и минусы. Во-первых, что, если функция и так может вернуть любой тип значения? Для неё сложно найти специальное значение, которое будет отличаться от допустимого результата. 180 | 181 | Вторая проблема – работа со специальными значениями может замусорить код. Если функция promptNumber вызывается 10 раз, то надо 10 раз проверить, не вернула ли она null. Если реакция на null заключается в возврате null на уровень выше, тогда там, где вызывался этот код, тоже нужно встраивать проверку на null, и так далее. 182 | 183 | ##Исключения 184 | Когда функция не может работать нормально, мы бы хотели остановить работу и перепрыгнуть туда, где такая ошибка может быть обработана. Этим занимается обработка исключений. 185 | 186 | Код, встретивший проблему в момент выполнения, может поднять (или выкинуть) исключение (raise exception, throw exception), которое представляет из себя некое значение. Возврат исключения напоминает некий «прокачанный» возврат из функции – он выпрыгивает не только из самой функции, но и из всех вызывавших её функций, до того места, с которого началось выполнение. Это называется развёртыванием стека (unwinding the stack). Может быть, вы помните стек функций из главы 3… Исключение быстро проматывает стек вниз, выкидывая все контексты вызовов, которые встречает. 187 | 188 | Если бы исключения сразу доходили до самого низа стека, пользы от них было бы немного. Они бы просто предоставляли интересный способ взорвать программу. Их сила в том, что на их пути в стеке можно поставить «препятствия», которые будут ловить исключения, мчащиеся по стеку. И тогда с этим можно сделать что-то полезное, после чего программа продолжает выполняться с той точки, где было поймано исключение. 189 | 190 | Пример: 191 | 192 | ```js 193 | function promptDirection(question) { 194 | var result = prompt(question, ""); 195 | if (result.toLowerCase() == "left") return "L"; 196 | if (result.toLowerCase() == "right") return "R"; 197 | throw new Error("Недопустимое направление: " + result); 198 | } 199 | 200 | function look() { 201 | if (promptDirection("Куда?") == "L") 202 | return "дом"; 203 | else 204 | return "двух разъярённых медведей"; 205 | } 206 | 207 | try { 208 | console.log("Вы видите", look()); 209 | } catch (error) { 210 | console.log("Что-то не так: " + error); 211 | } 212 | ``` 213 | 214 | Ключевое слово throw используется для выбрасывания исключения. Ловлей занимается кусок кода, обёрнутый в блок try, за которым следует catch. Когда код в блоке try выкидывает исключение, выполняется блок catch. Переменная, указанная в скобках, будет привязана к значению исключения. После завершения выполнения блока catch, или же если блок try выполняется без проблем, выполнение переходит к коду, лежащему после инструкции try/catch. 215 | 216 | В данном случае для создания исключения мы использовали конструктор Error. Это стандартный конструктор, создающий объект со свойством message. В современных окружениях JavaScript экземпляры этого конструктора также собирают информацию о стеке вызовов, который был накоплен в момент выкидывания исключения – так называемое отслеживание стека (stack trace). Эта информация сохраняется в свойстве stack, и может помочь при разборе проблемы – она сообщает, в какой функции случилась проблема и какие другие функции привели к данному вызову. 217 | 218 | Обратите внимание, что функция look полностью игнорирует возможность возникновения проблем в promptDirection. Это преимущество исключений – код, обрабатывающий ошибки, нужен только в том месте, где происходит ошибка, и там, где она обрабатывается. Промежуточные функции просто не обращают на это внимания. 219 | 220 | Ну, почти. 221 | 222 | ##Подчищаем за исключениями 223 | Представьте следующую ситуацию: функция withContext желает удостовериться, что во время её выполнения переменная верхнего уровня context содержит специальное значение контекста. В конце выполнения функция восстанавливает прежнее значение переменной. 224 | 225 | ```js 226 | var context = null; 227 | 228 | function withContext(newContext, body) { 229 | var oldContext = context; 230 | context = newContext; 231 | var result = body(); 232 | context = oldContext; 233 | return result; 234 | } 235 | ``` 236 | 237 | Что, если функция body выбросит исключение? В таком случае вызов withContext будет выброшен исключением из стека, и переменной context никогда не будет возвращено первоначальное значение. 238 | 239 | Но у инструкции try есть ещё одна особенность. За ней может следовать блок finally, либо вместо catch, либо вместе с catch. Блок finally означает «выполнить код в любом случае после выполнения блока try». Если функции надо что-то подчистить, то подчищающий код нужно включать в блок finally. 240 | 241 | ```js 242 | function withContext(newContext, body) { 243 | var oldContext = context; 244 | context = newContext; 245 | try { 246 | return body(); 247 | } finally { 248 | context = oldContext; 249 | } 250 | } 251 | ``` 252 | 253 | Заметьте, что нам больше не нужно сохранять результат вызова body в отдельной переменной, чтобы вернуть его. Даже если мы возвращаемся из блока try, блок finally всё равно будет выполнен. Теперь мы можем безопасно сделать так: 254 | 255 | ```js 256 | try { 257 | withContext(5, function() { 258 | if (context < 10) 259 | throw new Error("Контекст слишком мал!"); 260 | }); 261 | } catch (e) { 262 | console.log("Игнорируем: " + e); 263 | } 264 | // → Игнорируем: Error: Контекст слишком мал! 265 | 266 | console.log(context); 267 | // → null 268 | ``` 269 | 270 | Несмотря на то, что вызываемая из withContext функция «сломалась», сам по себе withContext по-прежнему подчищает значение переменной context. 271 | 272 | ##Выборочный отлов исключений 273 | Когда исключение доходит до низа стека и его никто не поймал, его обрабатывает окружение. Как именно – зависит от конкретного окружения. В браузерах описание ошибки выдаётся в консоль (она обычно доступна в меню «Инструменты» или «Разработка»). 274 | 275 | Если речь идёт об ошибках или проблемах, которые программа не может обработать в принципе, допустимо просто пропустить такую ошибку. Необработанное исключение – разумный способ сообщить о проблеме в программе, и консоль в современных браузерах выдаст вам необходимую информацию о том, какие вызовы функций были в стеке в момент возникновения проблемы. 276 | 277 | Если возникновение проблемы предсказуемо, программа не должна падать с необработанным исключением – это не очень дружественно по отношению к пользователю. 278 | 279 | Недопустимое использование языка – ссылки на несуществующую переменную, запрос свойств у переменной, равной null, или вызов чего-то, что не является функцией – тоже приводит к выбрасыванию исключений. Такие исключения можно отлавливать точно так же, как свои собственные. 280 | 281 | При входе в блок catch мы знаем только, что что-то внутри блока try привело к исключению. Мы не знаем, что именно, и какое исключение произошло. 282 | 283 | JavaScript (что является вопиющим упущением) не предоставляет непосредственной поддержки выборочного отлова исключений: либо ловим все, либо никакие. Из-за этого люди часто предполагают, что случившееся исключение – именно то, ради которого и писался блок catch. 284 | 285 | Но может быть и по-другому. Нарушение произошло где-то ещё, или в программу вкралась ошибка. Вот пример, где мы пробуем вызывать promptDirection до тех пор, пока не получим допустимый ответ: 286 | 287 | ```js 288 | for (;;) { 289 | try { 290 | var dir = promtDirection("Куда?"); // ← опечатка! 291 | console.log("Ваш выбор", dir); 292 | break; 293 | } catch (e) { 294 | console.log("Недопустимое направление. Попробуйте ещё раз."); 295 | } 296 | } 297 | ``` 298 | 299 | Конструкция for (;;) – способ устроить бесконечный цикл. Мы вываливаемся из него, только когда получаем допустимое направление. Но мы неправильно написали название promptDirection, что приводит к ошибке “undefined variable”. А так как блок catch игнорирует значение исключения e, предполагая, что он разбирается с другой проблемой, он считает, что выброшенное исключение является результатом неправильных входных данных. Это приводит к бесконечному циклу и скрывает полезное сообщение об ошибке насчёт неправильного имени переменной. 300 | 301 | Как правило, не стоит так ловить исключения, если только у вас нет цели перенаправить их куда-либо – к примеру, по сети, чтобы сообщить другой системе о падении нашей программы. И даже тогда внимательно смотрите, не будет ли скрыта важная информация. 302 | 303 | Значит, нам надо поймать определённое исключение. Мы можем в блоке catch проверять, является ли случившееся исключение интересующим нас исключением, а в противном случае заново выбрасывать его. Но как нам распознать исключение? 304 | 305 | Конечно, мы могли бы сравнить свойство message с сообщением об ошибке, которую мы ждём. Но это ненадёжный способ писать код – использовать информацию, предназначающуюся для человека (сообщение), чтобы принять программное решение. Как только кто-нибудь поменяет или переведёт это сообщение, код перестанет работать. 306 | 307 | Давайте лучше определим новый тип ошибки и используем instanceof для его распознавания. 308 | 309 | ```js 310 | function InputError(message) { 311 | this.message = message; 312 | this.stack = (new Error()).stack; 313 | } 314 | InputError.prototype = Object.create(Error.prototype); 315 | InputError.prototype.name = "InputError"; 316 | ``` 317 | 318 | Прототип наследуется от Error.prototype, поэтому instanceof Error тоже будет выполняться для объектов типа InputError. И ему назначено свойство name, как и другим стандартным типам ошибок (Error, SyntaxError, ReferenceError, и т.п.) 319 | 320 | Присвоение свойству stack пытается передать этому объекту отслеживание стека, на тех платформах, которые это поддерживают, путём создания объекта Error и использования его стека. 321 | 322 | Теперь promptDirection может сотворить такую ошибку. 323 | 324 | ```js 325 | function promptDirection(question) { 326 | var result = prompt(question, ""); 327 | if (result.toLowerCase() == "left") return "L"; 328 | if (result.toLowerCase() == "right") return "R"; 329 | throw new InputError("Invalid direction: " + result); 330 | } 331 | 332 | А в цикле её будет ловить сподручнее. 333 | 334 | for (;;) { 335 | try { 336 | var dir = promptDirection("Куда?"); 337 | console.log("Ваш выбор", dir); 338 | break; 339 | } catch (e) { 340 | if (e instanceof InputError) 341 | console.log("Недопустимое направление. Попробуйте ещё раз."); 342 | else 343 | throw e; 344 | } 345 | } 346 | ``` 347 | 348 | Код отлавливает только экземпляры InputError и пропускает другие исключения. Если вы снова сделаете такую же опечатку, будет корректно выведено сообщение о неопределённой переменной. 349 | 350 | ##Утверждения (Assertions) 351 | Утверждения – инструмент для простой проверки ошибок. Рассмотрим вспомогательную функцию assert: 352 | 353 | ```js 354 | function AssertionFailed(message) { 355 | this.message = message; 356 | } 357 | AssertionFailed.prototype = Object.create(Error.prototype); 358 | 359 | function assert(test, message) { 360 | if (!test) 361 | throw new AssertionFailed(message); 362 | } 363 | 364 | function lastElement(array) { 365 | assert(array.length > 0, "пустой массив в lastElement"); 366 | return array[array.length - 1]; 367 | } 368 | ``` 369 | 370 | Это – компактный способ ужесточения требований к значениям, который выбрасывает исключение в случае, когда заданное условие не выполняется. К примеру, функция lastElement, добывающая последний элемент массива, вернула бы undefined для пустых массивов, если бы мы не использовали assertion. Извлечение последнего элемента пустого массива не имеет смысла, и это явно было бы ошибкой программиста. 371 | 372 | Утверждения – способ убедиться в том, что ошибки провоцируют прерывание программы в том месте, где они совершены, а не просто выдают странные величины, которые передаются по системе и вызывают проблемы в каких-то других, не связанных с этим, местах. 373 | 374 | ##Итог 375 | Ошибки и недопустимые входные данные случаются в жизни. Ошибки в программах надо искать и исправлять. Их легче найти, используя автоматические системы проверок и добавляя утверждения в ваши программы. 376 | 377 | Проблемы, вызванные чем-то, что неподвластно вашей программе, нужно обрабатывать достойно. Иногда, когда проблему можно решить локально, допустимо возвращать специальные значения для отслеживания таких случаев. В других случаях предпочтительно использовать исключения. 378 | 379 | Выброс исключения приводит к разматыванию стека до тех пор, пока не будет встречен блок try/catch или пока мы не дойдём до дна стека. Значение исключения будет передано в блок catch, который сможет удостовериться в том, что это исключение действительно то, которое он ждёт, и обработать его. Для работы с непредсказуемыми событиями в потоке программы можно использовать блоки finally, чтобы определённые части кода были выполнены в любом случае. 380 | 381 | ##Упражнения 382 | ###Повтор 383 | Допустим, у вас есть функция primitiveMultiply, которая в 50% случаев перемножает 2 числа, а в остальных случаях выбрасывает исключение типа MultiplicatorUnitFailure. Напишите функцию, обёртывающую эту, и просто вызывающую её до тех пор, пока не будет получен успешный результат. 384 | 385 | Убедитесь, что вы обрабатываете только нужные вам исключения. 386 | 387 | ```js 388 | function MultiplicatorUnitFailure() {} 389 | 390 | function primitiveMultiply(a, b) { 391 | if (Math.random() < 0.5) 392 | return a * b; 393 | else 394 | throw new MultiplicatorUnitFailure(); 395 | } 396 | 397 | function reliableMultiply(a, b) { 398 | // Ваш код 399 | } 400 | 401 | console.log(reliableMultiply(8, 8)); 402 | // → 64 403 | ``` 404 | 405 | ###Запертая коробка 406 | Рассмотрим такой, достаточно надуманный, объект: 407 | 408 | ```js 409 | var box = { 410 | locked: true, 411 | unlock: function() { this.locked = false; }, 412 | lock: function() { this.locked = true; }, 413 | _content: [], 414 | get content() { 415 | if (this.locked) throw new Error("Заперто!"); 416 | return this._content; 417 | } 418 | }; 419 | ``` 420 | 421 | Это коробочка с замком. Внутри лежит массив, но до него можно добраться только, когда коробка не заперта. Напрямую обращаться к свойству _content нельзя. 422 | 423 | Напишите функцию withBoxUnlocked, принимающую в качестве аргумента функцию, которая отпирает коробку, выполняет функцию, и затем обязательно запирает коробку снова перед выходом – неважно, выполнилась ли переданная функция правильно, или она выбросила исключение. 424 | 425 | ```js 426 | function withBoxUnlocked(body) { 427 | // Ваш код 428 | } 429 | 430 | withBoxUnlocked(function() { 431 | box.content.push("золотишко"); 432 | }); 433 | 434 | try { 435 | withBoxUnlocked(function() { 436 | throw new Error("Пираты на горизонте! Отмена!"); 437 | }); 438 | } catch (e) { 439 | console.log("Произошла ошибка:", e); 440 | } 441 | console.log(box.locked); 442 | // → true 443 | ``` 444 | 445 | В качестве призовой игры убедитесь, что при вызове withBoxUnlocked, когда коробка не заперта, коробка остаётся незапертой. 446 | -------------------------------------------------------------------------------- /cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/cover.jpg -------------------------------------------------------------------------------- /custom/css/app.css: -------------------------------------------------------------------------------- 1 | .book .book-body .page-wrapper .page-inner section.normal { 2 | #000; 3 | } 4 | -------------------------------------------------------------------------------- /img/0-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/0-1.png -------------------------------------------------------------------------------- /img/1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/1-1.png -------------------------------------------------------------------------------- /img/13-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/13-1.png -------------------------------------------------------------------------------- /img/13-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/13-2.png -------------------------------------------------------------------------------- /img/13-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/13-3.png -------------------------------------------------------------------------------- /img/13-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/13-4.png -------------------------------------------------------------------------------- /img/15-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/15-1.png -------------------------------------------------------------------------------- /img/15-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/15-2.png -------------------------------------------------------------------------------- /img/16-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/16-1.png -------------------------------------------------------------------------------- /img/16-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/16-2.png -------------------------------------------------------------------------------- /img/16-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/16-3.png -------------------------------------------------------------------------------- /img/16-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/16-4.png -------------------------------------------------------------------------------- /img/16-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/16-5.png -------------------------------------------------------------------------------- /img/19-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/19-1.png -------------------------------------------------------------------------------- /img/19-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/19-2.png -------------------------------------------------------------------------------- /img/2-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/2-1.jpg -------------------------------------------------------------------------------- /img/2-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/2-2.png -------------------------------------------------------------------------------- /img/2-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/2-3.png -------------------------------------------------------------------------------- /img/2-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/2-4.png -------------------------------------------------------------------------------- /img/2-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/2-5.png -------------------------------------------------------------------------------- /img/2-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/2-6.png -------------------------------------------------------------------------------- /img/2-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/2-7.png -------------------------------------------------------------------------------- /img/2-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/2-8.png -------------------------------------------------------------------------------- /img/20-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/20-1.png -------------------------------------------------------------------------------- /img/20-1.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="375" height="148" viewBox="-2 -2 375 148"> 3 | <g> 4 | <text x="0" y="14" font-size="14px" font-family="PT Mono">синхронный, один поток контроля</text> 5 | <path d="M 0.5 28.5 L 10.5 28.5" stroke="#44f" stroke-width="3"></path><path d="M 10.5 28.5 L 90.5 28.5" stroke="#c22" stroke-width="1"></path><ellipse cx="10" cy="28.5" rx="2.5" ry="2.5" fill="#c22" width="5" height="5"></ellipse><path d="M 90.5 28.5 L 100.5 28.5" stroke="#44f" stroke-width="3"></path><path d="M 100.5 28.5 L 200.5 28.5" stroke="#c22" stroke-width="1"></path><ellipse cx="100" cy="28.5" rx="2.5" ry="2.5" fill="#c22" width="5" height="5"></ellipse><path d="M 200.5 28.5 L 220.5 28.5" stroke="#44f" stroke-width="3"></path> 6 | <text x="0" y="54.5" font-size="14px" font-family="PT Mono">синхронный, два потока контроля</text> 7 | <path d="M 0.5 68.5 L 10.5 68.5" stroke="#44f" stroke-width="3"></path><path d="M 10.5 68.5 L 90.5 68.5" stroke="#c22" stroke-width="1"></path><ellipse cx="10" cy="68.5" rx="2.5" ry="2.5" fill="#c22" width="5" height="5"></ellipse><path d="M 110.5 68.5 L 130.5 68.5" stroke="#44f" stroke-width="3"></path><path d="M 0.5 83.5 L 10.5 83.5" stroke="#44f" stroke-width="3"></path><path d="M 10.5 83.5 L 110.5 83.5" stroke="#c22" stroke-width="1"></path><ellipse cx="10" cy="83.5" rx="2.5" ry="2.5" fill="#c22" width="5" height="5"></ellipse><path d="M 110.5 83.5 L 120.5 83.5" stroke="#44f" stroke-width="3"></path> 8 | <text x="0" y="109.5" font-size="14px" font-family="PT Mono">асинхронный</text> 9 | <path d="M 0.5 123.5 L 30.5 123.5" stroke="#44f" stroke-width="3"></path><path d="M 10.5 123.5 L 10.5 123.5" stroke="#c22" stroke-width="1"></path><ellipse cx="10" cy="123.5" rx="2.5" ry="2.5" fill="#c22" width="5" height="5"></ellipse><path d="M 20.5 123.5 L 20.5 123.5" stroke="#c22" stroke-width="1"></path><ellipse cx="20" cy="123.5" rx="2.5" ry="2.5" fill="#c22" width="5" height="5"></ellipse><path d="M 10.5 123.5 a 20 20 0 0 0 20 20 l 50 0" stroke="#c22" fill="none"></path><path d="M 20.5 123.5 a 10 10 0 0 0 10 10 l 80 0" stroke="#c22" fill="none"></path><path d="M 110.5 133.5 L 130.5 133.5" stroke="#44f" stroke-width="3"></path><path d="M 80.5 143.5 L 90.5 143.5" stroke="#44f" stroke-width="3"></path></g></svg> -------------------------------------------------------------------------------- /img/21-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/21-1.png -------------------------------------------------------------------------------- /img/21-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/21-2.png -------------------------------------------------------------------------------- /img/3-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/3-1.png -------------------------------------------------------------------------------- /img/3-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/3-2.jpg -------------------------------------------------------------------------------- /img/3-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/3-3.jpg -------------------------------------------------------------------------------- /img/3-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/3-4.png -------------------------------------------------------------------------------- /img/3-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/3-5.png -------------------------------------------------------------------------------- /img/3-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/3-6.png -------------------------------------------------------------------------------- /img/6-0-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/6-0-1.png -------------------------------------------------------------------------------- /img/6-0-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/6-0-2.jpg -------------------------------------------------------------------------------- /img/9-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/9-1.png -------------------------------------------------------------------------------- /img/9-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/9-2.png -------------------------------------------------------------------------------- /img/9-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karmazzin/eloquentjavascript_ru/cd0499264b420d1bb1bc6779234075ecba6d6c46/img/9-3.png -------------------------------------------------------------------------------- /styles/style.css: -------------------------------------------------------------------------------- 1 | p { 2 | color: #000; 3 | } 4 | --------------------------------------------------------------------------------