├── .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 |
Привет, я Марийн и это моя домашняя страничка.
65 |А ещё я книжку написал! Читайте её 66 | здесь.
67 | 68 | 69 | ``` 70 | 71 | Теги, окружённые угловыми скобками < и >, описывают информацию о структуре документа. Всё остальное – просто текст. 72 | 73 | Документ начинается с , и это говорит браузеру, что его надо интерпретировать как современный HTML, в отличие от разных диалектов прошлого. 74 | 75 | У HTML документов есть заголовок и тело. Заголовок содержит информацию о документе, а тело – сам документ. В нашем случае мы объявили, что название страницы будет «Моя домашняя страничка», затем описали документ, содержащий заголовок 76 | (`` и заканчивается закрывающим `
`. Некоторые открывающие теги, типа ссылки ``, содержат дополнительную информацию в виде имя=”значение”. Она называется «атрибутами». В нашем случае адрес ссылки задан как `href="http://eloquentjavascript.net"`, где href означает «гипертекстовая ссылка», “hypertext reference”. 79 | 80 | Некоторые теги ничего не окружают, и их не надо закрывать. Пример – тег картинки 81 | 82 | ```html 83 |Привет, я Марийн и это моя домашняя страничка. 101 |
А ещё я книжку написал! Читайте её 102 | here. 103 | ``` 104 | 105 | Отсутствуют теги ``, `
` и ``. Браузер знает, что `