КулЛиб - Классная библиотека! Скачать книги бесплатно 

Изучаем JavaScript. Руководство по созданию современных веб-сайтов [Этан Браун] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]
Этан Браун

1
1

o·REILLY®

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

"Всем разрабо'l'чикам

спецификации JavaScript - ECMAScript 6.0 (ЕSб)

дейс'l'ВИ'l'ельно при­

-

научиться соз­

давать высококачественные приложения на этом языке стало

шло время ИЗУЧИ'l'Ь
Под изучением я

проще, чем когда-либо ранее. Эта книга знакомит программистов

JS.

(любителей и профессионалов) со спецификацией ЕSб наряду с

не имею в виду при­

некоторыми связанными с ней инструментальными средствами

МИ'l'ивное «Я получил

и методиками на сугубо практической основе.
Этан Браун, автор книги Web Development with Node and Express,
излагает не только простые и понятные темы (переменные, вет­
вление потока, массивы), но и более сложные концепции, такие
как функциональное и асинхронное программирование. Вы узна­
ете, как создавать мощные и эффективные веб-приложения для
работы на клиенте или сервере Node.js.

• Используйте ЕSб для транскомпиляции в переносимый
код ES5
• Преобразуйте данные в формат, который может
использовать JavaScript
• Усвойте основы и механику применения функций JavaScript
• Изучите объекты и объектно-ориентированное
программирование

некий рабО'l'ОСПОСОб­

иый код». Э'l'а книга куда глубже и обе­
спечивае'l' именно 'l'O
изучение,

в ко'l'ором

все мы нуждаемся!"
Кайл Симлсон (Ку/е Simpson),
автор серии Уои Don't Know JS

"Хорошо написанное
сжа'l'ое введение
в JavaScript,

вклю­

чая ECМAScript

б".

Аксель Роуwмайер
(Ахе/ Rauschmayer),

автор Speaking JavaScript

• Ознакомьтесь с новыми концепциями, такими как
итераторы, генераторы и прокси-объекты
• Преодолейте сложности асинхронного программирования
• Используйте объектную модель документа
для приложений, выполняемых в браузере
• Изучите основы применения платформы Node.js
для разработки серверных приложений

Этан Браун - директор интерактивного маркетингового агентства
Eпgiпeeriпg at Рор Art, в котором он отвечает за архитектуру и реали­
зацию веб-сайтов и веб-служб для любых клиентов, от малых пред­
приятий до транснациональных компаний. Этан имеет более чем
20-летний стаж программирования.
ПРОГРАММИРОВАНИЕ

�DJ

ДЛЯ

ВЕБ /

JAVASCRIPT

ISBN 978-5-9908463-9-5

JU•д.1Eкmuк.i

www.dialektika.com
Twitter: @oreillymedia
facebook.com/oreilly

9 785990 846395

Изучаем
JavaScript

Learning
JavaScript

THIRD EDIТION

Ethan Brown

Beijing Cambridge Farnham Kбln Sebastopol Tokyo










O"REILLY®

Изучаем
JavaScript
РУКОВОДСТВО ПО СОЗДАНИЮ
СОВРЕМЕННЫХ ВЕБ-САЙТОВ
3-ЕИЗДАНИЕ

Этан Браун

Москва ·Санкт-Петербург· Киев
2017

ББК 32.973.26-018.2.75
Б87
УДК 681 .3.07
Компьютерное издательство "Диалектика"
Зав. редакцией С.Н. Тригуб
Перевод с английского и редакция В.А. Коваленко
По общим вопросам обращайтесь в издательство "Диалектика" по адресу:
info@dialektika.com, http://www.dialektika.com

Браун, Этан.

Б87

Изучаем JavaScript: руководство по созданию современных веб-сайтов, 3-е изд. :
Пер. с англ. - СпБ. : ООО "Альфа-книга'; 2017. - 368 с. : ил. - Парал. тит.
ISBN 978-5-9908463-9-5 (рус.)
ББК 32.973.26-018.2.75
Все названия программных продуктов являются зарегистрированными торговыми марками соответ­
ствующих фирм.
Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни
было форме и какими бы то ни было средствами, будь то электронные или механические, включая фо­
токопирование и запись на магнитный носитель, если на это нет письменного разрешения издательства
O'Reilly

& Associates.

Authorized Rнssiaп translation of the English edition of
2016 Ethan Brown.

Learning favaScript (ISBN 978-1-491-91491-5) ©

This translatioп is puЬlished апd sold Ьу permissioп of O'Reilly Media, !пс., which owпs or coпtrols all rights
to publish апd sell the same.
All rights reserved. No part of this work тау Ье reprodнced or traпsmitted iп any form or Ьу апу meaпs,
electroпic or mechaпical, iпcludiпg photocopyiпg, recordiпg, or Ьу any information storage or retrieval system,
without the prior written permission of the copyright owner and the PuЬlisher.

Научно -по пулярное издание
Этан Браун

Изучаем JavaScript
Руководство по созданию современных веб-сайтов
3-е издание

Литературный редактор Л.Н. Красножон
Верстка О.В. Мишут ина
Художественный редактор Е.П. Дынник
Корректор Л.А. Гордиенко
Подписано в печать 23.03.2017. Формат 70х100/16.
Гарнитура Times.
Усл. печ. л. 23,0. У ч.-изд. л. 17,8.
Тираж 500 экз. Заказ

№ 2261

Отпечатано в АО «Первая Образцовая типография»
Филиал «Чеховский Печатный Двор»
142300, Московская об11асть, г. Чехов, ул. Полиграфистов, д. 1
ООО "Альфа-книга� 195027, Санкт-Петербург, Магнитогорская ул" д. 30
ISBN 978-5-9908463-9-5 (рус.)

© 2017, Компьютерное издательство "Диа11ектика':

ISBN 978-1-491-91491-5 (англ.)

© 2016, Ethan Brown

перевод, оформ11ение, макетирование

Оrла вление
Введение

17

Глава 1. Ваше первое приложение

25

Глава 2. Инструменты разработки JavaScript

39

Глава 3. Литералы, переменные, константы и типы данных

57

Глава 4. Управление потоком

81

Глава 5. Выражения и операторы

105

Глава 6. Функции

129

Глава 7. Область видимости

145

Глава 8. Массивы и их обработка

159

Глава 9. Объекты и объектно-ориентированное программирование

175

Глава 1О. Отображения и наборы

191

Глава 11. Исключения и обработка ошибок

197

Глава 12. Итераторы и генераторы

205

Глава 13. Функции и мощь абстрактного мышления

215

Глава 14. Асинхронное программирование

231

Глава 15. Дата и время

253

Глава 16. Объект Math

263

Глава 17. Регулярные выражения

271

Глава 18. JavaScript в браузере

293

Глава 19. Библиотека jQuery

313

Глава 20. Платформа Node

319

Глава 21. Свойства объекта и прокси-объекты

339

Глава 22. Дополнительные ресурсы

351

Приложение А. Зарезервированные ключевые слова

357

Приложение Б. Приоритет операторов

361

Предметный указатель

363

Сод ерж ание

Об авторе
Изображение на обложке

16
16

Введение

17

Краткая история JavaScript
ES6
Для кого предназначена эта книга
Для кого не предназначена эта книга
Соглашения, принятые в этой книге
Благодарности
От издательства

18
19
20
20
20
21
24

Глава 1. Ваше первое приложение

25

С чего начать
Инструменты
Комментарий о комментариях
Первые шаги
Консоль JavaScript
Библиотека jQuery
Рисование графических примитивов
Автоматизация повторяющихся задач
Обработка пользовательского ввода
Программа Hello, World

26
26
28
29
31
32
33
35
36
37

Глава 2. Инструменты разработки JavaScript

39

Написание кода ES6 сегодня
Возможности ES6
Установка Git
Терминал
Корневой каталог проекта
Git: контроль версий
Управление пакетами: npm

39
41
41
41
42
43
46

Инструменты сборки: Gulp и Grunt
Структура проекта
Транскомпиляторы
Запуск Babel с Gulp
Анализ
Заключение

48
49
50
50
52
55

Глава 3. Литералы, переменные , константы и типы данных

57

Переменные и константы
Переменные или константы: что использовать?
Именование идентификаторов
Литералы
Базовые типы и объекты
Числа
Строки
Экранирование специальных символов
Специальные символы
Строковые шаблоны
Поддержка многострочных строк
Числа как строки
Логические значения
Символы
Типы nul l и undefined
Объекты
Объекты Number, String и Boolean
Массивы
Завершающие зап'Ятые в объектах и массивах
Даты
Регулярные выражения
Отображения и наборы
Преобразование типов данных
Преобразование в числовой формат
Преобразование в строку
Преобразование в логическое значение
Заключение

57
58
59
60
61
62
64
64
65
66
67
68
69
69
69
70
72
73
75
75
76
76
77
77
78
78
79

Глава 4. Управление потоком

81

Учебник для новичков в управлении потоком
Циклы whi le
Блоки операторов
Отступ

81
84
85
86
Содерж а ние

7

Вспомогательные функции
87
Оператор if."else
87
Цикл do. . . while
89
90
Цикл for
91
Оператор if
Объединим все вместе
92
93
Операторы управления потоком в JavaScript
94
Исключения в управлении потоком
94
Сцепление операторов if. . .else
95
Метасинтаксис
96
Дополнительные шаблоны цикла for
97
Операторы switch
101
Цикл for.. . in
101
Цикл for...of
1 02
Популярные схемы управления потоком
Использование continue для сокращения содержимого условных выражений 102
102
Использование break или return во избежание ненужного вычисления
Использование значения индекса после завершения цикла
103
103
Использование убывающих индексов при изменении списков
104
Заключение
Глава 5. Выражения и операторы

105

Операторы
Арифметические операторы
Приоритет операторов
Операторы сравнения
Сравнение чисел
Конкатенация строк
Логические операторы
Истинные и ложные значения
Операторы AND, OR и NOT
Вычисление по сокращенной схеме
Логические операторы с не логическими операндами
Условный оператор
Оператор "запятая"
Оператор группировки
Побитовые операторы
Оператор typeof
Оператор void
Операторы присваивания
Деструктурирующее присваивание
Операторы объектов и массивов

107
107
1 10
111
1 13
1 14
1 15
115
1 16
1 17
1 18
118
1 19
1 19
1 19
121
1 22
1 22
1 24
125

8

Сод ержани е

Выражения в строковых шаблонах
Выражения и шаблоны управления потоком
Преобразование операторов if ...else в условные выражения
Преобразование операторов if в сокращенные выражения логического ИЛИ
Заключение

126
126
126
127
127

Глава 6. Функции

129

Возвращаемые значения
Вызов или обращение
Аргументы функции
Определяют ли аргументы функцию�
Деструктуризация аргументов
Стандартные аргументы
Функции как свойства объектов
Ключевое слово this
Функциональные выражения и анонимные функции
Стрелочная нотация
Методы cal l, apply и Ьind
Заключение

130
130
131
133
134
135
135
136
138
140
141
143

Глава 7. Область видимости

145

Область видимости и существование переменных
Лексическая или динамическая область видимости
Глобальная область видимости
Область видимости блока
Маскировка переменной
Функции, замкнутые выражения и лексическая область видимости
Немедленно вызываемые функциональные выражения
Область видимости функции и механизм подъема объявлений
Подъем функций
Временная мертвая зона
Строгий режим
Заключение

146
146
147
149
150
151
153
154
1 56
157
157
158

Глава 8. Массивы и их обработка

159

Обзор массивов
Манипулирование содержимым массива
Добавление отдельных элементов в начало или конец и их удаление
Добавление нескольких элементов в конец
Получение подмассива
Добавление и удаление элементов в любой позиции

159
160
161
161
162
162

Содержание

9

Копирование и вставка в пределах массива
Заполнение массива заданным значением
Обращение и сортировка массивов
Поиск в массиве
Фундаментальные операции над массивом: map и fil ter
Магия массивов: метод reduce
Методы массива и удаленные или еще не определенные элементы
Соединение строк
Заключение

162
163
163
164
166
168
171
172
173

Глава 9. Объекты и объектно-ориентированное программирование

175

Перебор свойств
Цикл for...in
Метод Obj ect.keys
Объектно-ориентированное программирование
Создание класса и экземпляра
Динамические свойства
Классы как функции
Прототип
Статические методы
Наследование
Полиморфизм
Перебор свойств объектов (снова)
Строковое представление
Множественное наследование, примеси и интерфейсы
Заключение

175
176
176
177
178
179
181
181
183
184
185
186
187
188
190

Глава 1О. Отображения и наборы

191

Отображения
Слабые Отображения
Наборы
Слабые наборы
Расставаясь с объектной привычкой

191
193
194
195
196

Глава 11. Исключения и обработка ошибок

197

Объект Error
Обработка исключений с использованием блоков try и catch
Генерирование ошибки
Обработка исключений и стек вызовов
Конструкция try...catch . ..finally
Позвольте исключениям быть исключениями

197
198
199
200
202
203

10

Сод ержание

Глава 12. Итераторы и генераторы

205

Протокол итератора
Генераторы
Выражения yield и двухсторонняя связь
Генераторы и оператор return
Заключение

207
209
210
212
213

Глава 13. Функции и мощь абстрактного мышления

215

Функции как подпрограммы
Функции как подпрограммы, возвращающие значение
Функции как . . . функции
И что?
Функции являются объектами
Немедленно вызываемое функциональное выражение и асинхронный код
Переменные функций
Функции в массиве
Передача функции в функцию
Возвращение функции из функции
Рекурсия
Заключение

215
216
217
220
221
22 1
223
225
227
228
229
230

Глава 14. Асинхронное программирование

231

Аналогия
Обратные вызовы
Функции setinterva l и clearinterval
Область видимости и асинхронное выполнение
Передача ошибок функциям обратного вызова
Проклятье обратных вызовов
Обязательства
Создание обязательств
Использование обязательств
События
Сцепление обязательств
Предотвращение незавершенных обязательств
Генераторы
Шаг вперед и два назад?
Не пишите собственных пускателей генераторов
Обработка исключений в пускателях генераторов
Заключение

232
232
234
234
236
237
238
239
240
241
244
245
246
249
250
250
251

Глава 15. Дата и время

253

Даты, часовые пояса, временные метки и эпохи Unix
Создание объектов Da te

253
254
Содержание

11

Библиотека Moment.j s
Практический подход к датам в JavaScript
Создание дат
Создание дат на сервере
Создание дат в браузере
Передача дат
Отображение дат
Компоненты даты
Сравнение дат
Арифметические операции с датами
Удобные относительные даты
Заключение

255
256
256
256
257
257
258
260
260
261
261
262

Глава 16. Объект Math

263

Форматирование чисел
Числа с фиксированным количеством десятичных цифр
Экспоненциальная форма записи
Фиксированная точность
Другие основания
Дополнительное-форматирование чисел
Константы
Алгебраические функции
Возведение в степень
Логарифмические функции
Другое
Генерация псевдослучайных чисел
Тригонометрические функции
Гиперболические функции

263
264
264
264
265
265
266
266
266
267
267
268
269
269

Глава 17. Регулярные выражения

271

Распознавание и замена подстрок
Создание регулярных выражений
Поиск с использованием регулярных выражений
Замена с использованием регулярных выражений
Переработка входных данных
Чередование
Анализ HTML-кода
Наборы символов
Именованные наборы символов
Повторение
Метасимвол "точка" и экранирование

271
272
273
273
274
276
277
278
279
280
281

12

Содержание

Шаблон, соответствующий всему
Группировка
Ленивое и жадное распознавания
Обратные ссылки
Группы замены
Функции замены
Привязка
Распознавание границ слов
Упреждения
Динамическое создание регулярных выражений
Заключение

282
282
283
284
285
286
289
289
290
292
292

Глава 18. JavaScript в браузере

293

ESS или ЕSб?
Объектная модель документа
Немного терминологии
Методы-получатели модели DOM
Выборка элементов DOM
Манипулирование элементами DOM
Создание новых элементов DOM
Применение стилей к элементам
Атрибуты данных
События
Перехват и всплытие событий
Категории событий
Ajax
Заключение

293
294
297
297
298
299
299
300
301
302
303
307
308
312

Глава 1 9. Библиотека jQuery

313

Всемогущий доллар (знак)
Подключение jQuery
Ожидание загрузки и построения дерева DOM
Элементы DOM в оболочке jQuery
Манипулирование элементами
Извлечение объектов jQuery из оболочки
Ajax
Заключение

313
314
314
314
315
317
318
318

Глава 20. Платформа Node

319

Основные принципы Node
Модули

319
320
Содержание

13

Базовые, файловые и nрm-модули
Изменение параметров модулей с помощью модулей-функций
Доступ к файловой системе
Переменная proces s
Информация об операционной системе
Дочерние процессы
Потоки
Веб-серверы
Заключение

322
325
327
330
333
333
335
336
338

Глава 21. Свойства объекта и прокси-обьекты

339

Свойства доступа: получатели и установщики
Атрибуты свойств объекта
Защита объектов: замораживание, запечатывание и запрет расширения
Прокси-объекты
Заключение

339
341
343
346
349

Глава 22. Дополнительные рес�рсы

35 1

Сетевая документация
Периодические издания
Блоги и учебные курсы
Система Stack Overflow
Вклад в проекты Open Source
Заключение

35 1
352
352
353
356
356

Приложение А. Зарезервированные ключевые слова

357

Приложение Б. Приоритет операторов

361

Предметный указатель

363

14

Содержание

д�я Марка - истинного друга и собрата.

Об авторе
- директор интерактивного маркетингового агентства Engineering at
Рор Art, в котором он отвечает за архитектуру, реализацию веб-сайтов и веб-служб
для любых клиентов, от малых предприятий до транснациональных компаний. Этан
обладает более чем 20-летним стажем программирования, начиная со встраивания в
веб и заканчивая семейством JavaScript как веб-платформой будущего.
Этан Браун

Изображе ние на обложке
Животное на обложке книги - это детеныш черного носорога (Diceros blcornis).
Черный носорог - это один из двух видов африканских носорогов. Весящий до
половины тонны, он меньше своего собрата - белого носорога. Черные носороги
живут. в саванне, на открытой лесистой местности и в горных лесах нескольких
небольших областей Южной, Юго-Западной, Центральной и Восточной Африки.
Они предпочитают жить поодиночке и агрессивно защищают свою территорию.
Внешним отличием черного носорога от белого является форма верхней губы: у
черного носорога она заострена и свисает хоботком над нижней. С помощью этой
губы животное захватывает листву с веток кустарника. Это позволяет ему есть более
грубую растительность, чем другие травоядные животные.
Черные носороги - непарнокопытные животные, т.е. у них по три пальца на
каждой ноге. У них толстая серая кожа без шерсти. Одной из главных отличительных
особенностей носорога являются два рога, фактически состоящих из слипшихся
волос, а не из кости. Носорог использует их для защиты от львов, тигров и гиен,
а также для привлечения особей противоположного пола. Ритуал ухаживания
зачастую довольно груб и рога могут нанести серьезные раны.
Впоследствии самцы и самки носорогов никаких контактов не поддерживают.
Период беременности составляет 14-18 месяцев, а молоком детеныши кормятся в
течение года, хотя они в состоянии есть растительную пищу почти сразу же после
рождения. Связь между матерью и ее теленком может продолжаться четыре года,
пока он оставит ее.
Охота на носорогов поставила их на грань исчезновения. По оценкам ученых
сто лет назад популяция черных носорогов в Африке составляла порядка миллиона
особей, а сейчас это число сократилось до 2400. Сегодня в опасности находятся все
пять оставшихся видов, включая индийского, яванского и суматранского носорогов.
Людей не зря считают самыми свирепыми хищниками.
Большинство животных с обложек O'Reilly находятся в опасности; все они
важны для мира. Чтобы узнать больше, как можно им помочь, обратитесь по адресу
anima l s . ore i l l y . сот. Изображение на этой обложке взято из книги Natural History
Джона Кассела (John Cassell).

Хотя это моя вторая книга по технологиям JavaScript, роль эксперта по JavaScript
меня все еще несколько смущает. Подобно большинству программистов, я имел не­
кое предубеждение относительно JavaScript вплоть до примерно 2012 года. Хотя моя
позиция резко изменилась, я все еще чувствую легкое смущение.
Причина моего предубеждения была обычной: я считал JavaScript "игрушечным"
языком (не изучив его толком, а потому и не зная, о чем говорю), опасным, сырым,
используемым безграмотными программистами-любителями. В обеих этих причинах
была некая доля истины. Спецификация ES6 была разработана очень быстро, и даже
ее изобретатель, Брендан Айк (Brendan Eich), признает, что есть вещи, которых он
в первое время не понимал, а когда понял, уже слишком много людей полагались
на проблематичное для него поведение, чтобы эффективно его изменить (но пока­
жите мне язык, который не страдал бы от подобных проблем). Что касается вто­
рой причины, JavaScript внезапно сделал программирование доступным. Мало того
что браузер есть у всех, так еще и усилий для создания веб-сайтов с использованием
JavaScript, которые быстро множились бы в Интернете, необходимо совсем немного.
Люди учатся методом проб и ошибок, читая коды друг друга и (в очень многих слу­
чаях) подражая плохо написанному коду безо всякого понимания.
Я рад, что узнал о JavaScript достаточно, чтобы понять, что этот (далеко не игру­
шечный) язык разработан на чрезвычайно прочном фундаменте и отличается мо­
щью, гибкостью и выразительностью. Я также рад, что уловил доступность, о�еспе­
чиваемую языком JavaScript. Я, конечно, не испытываю никакой враждебности к лю­
бителям: все должны с чего-то начинать, программирование - выгодный навык, и у
карьеры программиста есть много преимуществ.
Начинающему программисту, любителю, я могу сказать, что нет ничего позорного
в том, чтобы быть любителем. Есть некий позор в том, чтобы оставаться любителем
(если, конечно, вы сделали программирование своей профессией). Если нужен опыт
в программировании, то приобретайте его. Изучите все, что сможете, все доступные
первоисточники, какие найдете. Не будьте предвзятыми и (возможно, это важнее
всего) подвергайте сомнению все. Расспрашивайте каждого эксперта. Расспрашивай­
те каждого опытного программиста. Постоянно спрашивайте "Почему?"
В этой книге по большей части я пытался придерживаться "фактов" JavaScript,
но полностью избежать собственного мнения невозможно. Когда я выражаю

собственное мнение, я так и говорю. Вы вполне можете не соглашаться с ним и при­
держиваться мнения других опытных разработчиков.
Вы изучаете JavaScript в самый подходящий момент. Веб вышел из младенческого
возраста (с технической точки зрения), а веб-разработка, без сомнения, - больше не
Дикий Запад, которым она была лет 5-1 О назад. Такие стандарты, как HTML5 и ES6,
облегчают изучение веб-разработки и упрощают разработку высококачественных
приложений. Платформа Node.js делает JavaScript доступным и вне браузера; теперь
это вполне подходящий выбор для системных сценариев, разработки приложений
раб,очего стола, приложений для веб-серверов и даже для встраиваемых приложе­
ний. Конечно, я не имел такого удовольствия в программировании, поскольку начал
лишь с середины 1980-х годов.

К раткая история JavaScript
Язык JavaScript был разработан Бренданом Айком из корпорации Netscape
Communications Corporation в 1 995 году. Его первая разработка была весьма скоро­
спелой, и критики JavaScript по большей части порицали недостатки предварительно­
го планирования во время его разработки. Однако Брендан Айк не был дилетантом:
у него был серьезный опыт в информатике, и он заложил в JavaScript на удивление
сложные и передовые идеи. Так или иначе, он опередил время, и потребовалось 1 5 лет,
чтобы этот замечательный язык завоевал популярность у ведущих разработчиков.
До официального переименования в "JavaScript", в выпуске Netscape Navigator
1 995 года язык назывался сначала "Mocha", а затем "LiveScript". Слово "Java" в на­
звании "JavaScript" не было случайным, хотя и не было очевидным: кроме общей
синтаксической родословной, JavaScript имеет больше общего с Self (основанный
на прототипах язык, разработанный в Xerox PARC в середине 1 980-х годов) и Scheme
(язык, разработанный в 1 970-х Гаем Стилом (Guy Steele) и Джеральдом Сассманом
(Gerald Sussman) под сильным влиянием Lisp и ALGOL), чем с Java. Айк был знаком
и с Self, и с Scheme и использовал некоторые из их передовых парадигм в разработке
"JavaScript': Название "JavaScript" частично было маркетинговой попыткой прима­
заться к успеху языка Java, которого он достиг в то время1•
В ноябре 1996 года компания Netscape объявила о передаче JavaScript ассоциации
Ecma - приватной международной некоммерческой организации по стандартиза­
ции, оказывающей существенное влияние на технологии и отрасли связи. Ассоциа­
ция Ecma International опубликовала первое издание спецификации ЕСМА-26, став­
шее основой JavaScript.
Отношения между спецификациями Ecma (определяющими язык ECMAScript)
и JavaScript являются главным образом академическими. Технически JavaScript - это
1 Айк рассказал об этом в интервью в 20 1 4 году.
18

Введение

реализация ECMAScript, но практически "JavaScript" и "ECMAScript" можно считать

равнозначными терминами.
Последняя главная версия ECMAScript, 5.1 (обычно называемая "ES5"), была опу­
бликована в июне 201 1 года. Устаревшие браузеры, не поддерживающие ECMAScript 5.1,
утратили популярность, и можно смело сказать, что ECMAScript 5. 1 является теку­
щим общепринятым языком веба.
Язык ECMAScript 6 (ЕSб), являющийся предметом рассмотрения этой книги,
опубликован Ecma International в июне 2015 года. Рабочим названием этой специфи­
кации до публикации было "Harmony" (Гармония), и вы полнее можете услышать та­
кое название ЕSб, как "Harmony'; "ЕSб Harmony'; "ЕSб'; "ES201 5" и "ECMAScript 201 5".
В этой книге мы называем его просто "ЕSб".

ЕSб
Внимательный читатель мог бы задаться вопросом "Если текущим общеприня­
тым языком веба является ES5, то почему эта книга о ЕSб?"
Спецификация ЕSб представляет существенное усовершенствование языка
JavaScript, и некоторые из главных недостатков спецификации ES5 были устране­
ны в ЕSб. Я полагаю, что вы найдете язык ЕSб намного более приятным и мощным
в применении (и ES5 был бы весьма хорошим началом). Кроме того (благодаря
транскомпиляторам), сегодня вы можете написать код ЕSб и транскомпилировать
его в код, "совместимый с вебом" ES5.
И наконец после публикации ЕSб поддержка этой спецификации браузерами бу­
дет устойчиво расти, и в некий момент транскомпиляция больше не будет необхо­
димой для доступа широкой аудитории (я не настолько глуп, чтобы делать прогноз
(даже грубый) о том, когда именно это случится).
Но что абсолютно ясно, так это то, что за ЕSб - будущее разработки JavaScript,
и, инвестировав свое время в его изучение, вы будете готовы к будущему, хотя и с
транскомпиляцией, препятствующей ныне переносимости кода.
Однако сегодня не каждый разработчик имеет роскошь писать код ЕSб. Вполне воз­
можно, что вы работаете с очень большим объемом существующего базового кода ES5,
который весьма дорого преобразовать в код ЕSб. Некоторые разработчики просто не
пожелают приложить дополнительные усилия, необходимые для транскомпиляции.
За исключением главы 1 , в этой книге рассматривается ЕSб, а не ES5. По воз­
можности я буду указывать, где ЕSб отличается от ES5, но не ожидайте построчного
сравнения примеров кода или обширного обсуждения, где "путь ES5" будет лучше,
чем "путь ЕSб': Если вы относитесь к той категории программистов, которые по лю­
бой причине вынуждены придерживаться спецификации ES5, то эта книга не для вас
( хотя я и надеюсь, что вы вернетесь к ней когда-либо в будущем!).

В ведение

19

Выбор спецификации ЕSб был сделан редакцией обдуманно. Усовершенствования
ЕSб достаточно существенны, чтобы затруднить четкое и ясное изложение материа­
ла. Короче говоря, книга, которая попыталась бы рассмотреть и ESS, и ЕSб, навреди­
ла бы обеим темам.

Для кого предназна ч ена эта книга
Эта книга предназначена, прежде всего, для читателей, уже обладающих некото­
рым опытом программирования (освоивших хотя бы вводный курс программирова­
ния или сетевые классы) . Новичкам в программировании эта книга тоже будет по­
лезна, однако будет не лишним дополнить ее вводным курсом или классом.
Те, кто уже обладают некоторым опытом программирования в JavaScript (особен­
но если это только ESS), найдут практически полное описание важнейших концеп­
ций этого языка. Программистам, переходящим на JavaScript с другого языка, содер­
жимое этой книги также должно понравиться.
В этой книге предпринята попытка всесторонне рассмотреть возможности язы ка, связанные с ним инструментальные средства, методики и парадигмы, которые
управляют современной разработкой на JavaScript. Поэтому в данную книгу включе­
ны как простой и понятный материал (переменные, контроль потока, функции), так
и довольно сложный (асинхронное программирование, регулярные выражения). В
зависимости от своего уровня подготовки, вы можете найти одни главы более слож­
ными, чем другие: начинающий программист, без сомнения, должен будет повторно
пройти часть материала.

Для кого не предназна ч ена эта книга
Эта книга - не полный справочник по JavaScript или связанным с ним библиоте­
кам. Сеть Mozilla Developer Network (MDN) представляет собой превосходный, пол­
ный, актуальный и бесплатный сетевой с правочник по ]avaScript, на который я ссы­
лаюсь повсюду в этой книге. Если вы предпочитаете физическую книгу, то книга Дэ­
вида Флэнагана (David Flanagan) ]avaScript. Подробное руководство является весьма
подходящей (хотя на момент написания этой книги в ней ЕSб не рассматривалась).

Согл а ш ения, принятые в это й книге
Здесь используются соглашения, общепринятые в компьютерной литературе.

20



Новые термины в тексте выделяются курсивом. Чтобы обратить внимание чи­
тателя на отдельные фрагменты текста, также применяется курсив.



Текст программ, функций, переменных, URL веб-страниц и другой код выделе­
ны моноширинным шрифтом.
Введение



Все, что придется вводить с клавиатуры, выделено полужирным
шрифтом.



Знакоместо в описаниях синтаксиса выделено курсивом. Это указывает на не­
обходимость заменить знакоместо фактическим именем переменной, пара­
метром или другим элементом, который должен находиться на этом месте:
BINDSIZE= (максима льная ширина колонки)* (номер колонки).



моноширинным

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

Menu Option

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

Этот элемент содержит примечание.

&

Этот элемент содержит предупреждение или предостережение.

Благодарности
Возможность писать книги для O'Reilly является огромной честью, и я должен
поблагодарить Саймона С. Лорент (Simon St. Laurent) за то, что он увидел во мне
потенциал и взял на борт. Мег Фоли (Meg Foley), мой редактор, была благосклонна,
профессиональна и весьма полезна. Книга O'Reilly это результат командных уси лий, и мои литературный редактор Рейчел Монахан (Rachel Monaghan) , выпускаю­
щий редактор Кристен Браун (Kristen Brown) и корректор Жасмин Куитин (Jasmine
Kwityn) были быстры, обстоятельны и проницательны. Благодарю вас за все усилия!
Благодарю моих технических рецензентов Мэтт Инман (Matt Inman), Шелли Па­
уэрс (Shelley Powers), Ник Пинкхам (Nick Pinkham) и Коди Линдли (Cody Lindley) за
содержательные отзывы, блестящие идеи и помощь в улучшении этой книги. Можно
сказать, что без вас, возможно, неточностей было бы намного больше. Хотя все от­
зывы были невероятно полезны, я хочу выразить отдельную признательность Мэтту:
его опыт педагога обеспечил ценную способность проникновения в суть, а отзывы
Стивена Кольбера (Stephen Colbert) помогли мне сохранить свое здравомыслие!
-

Введение

21

Шелли Пауэрс (автор предыдущих изданий этой книги) заслуживает особенной
благодарности не только за то, что передала эту тему мне, но и за то, что написала
компетентный отзыв и помогла сделать эту книгу лучше (а также за некоторые бур­
ные обсуждения!).
Я хотел бы выразить признательность всем читателям моей предыдущей книги
(Web Development with Node and Express). Если бы вы не покупали ту книгу (и не от­
зывались о ней положительно!), у меня, вероятно, не было бы возможности написать
эту книгу. Особая благодарность читателям, которые уделили время, чтобы прислать
мне отзывы и исправления: я узнал многое из ваших писем!
Спасибо всем сотрудникам агентства Engineering at Рор Art, где я имею честь ра­
ботать. Вы - моя судьба. Ваша поддержка важна для меня, ваш энтузиазм мотиви­
рует меня, а ваши профессионализм и преданность - это именно то, что по утрам
поднимает меня с постели. Том Пол (Tom Paul) заслуживает моей особой благодар­
ности: его незыблемые принципы, инновационные деловые идеи и исключительные
лидерские качества вдохновили меня приложить все усилия. Благодарю Стива Розен­
баума (Steve Rosenbaum) за то, что он основал Рор Art, выдержал все бури и успешно
передал факел Тому. Поскольку я был занят написанием этой книги, Колвин Фриц­
Мур (Colwyn Fritze-Moor) и Эрик Бучман (Eric Buchmann) работали дополнительное
время, чтобы выполнить те работы, которые обычно делал я. Спасибо обоим! Благо­
дарю Дилана Халлстрома (Dylan Hallstrom) за то, что он был образцом надежности.
Благодарю Лиз Том (Liz Тот) и Сэма Виски (Sam Wilskey) за то, что они присоедини­
лись к коллективу Рор Art! Благодарю Кэрол Харди (Carole Hardy), Никки Броволда
(Nikki Brovold), Дженнифер Эртц (Jennifeг Erts), Рэнди Кинер (Randy Keener), Патри­
ка Ву (Patгick Wu) и Лайзу Мелог (Lisa Melogue) за всю поддержку. Наконец спасибо
всем моим предшественникам, у которых я учился, а именно: Тони Алферезу (Топу
Alferez), Полу Инману (Paul Inman) и Делу Олдсу(Dеl Olds).
Мой энтузиазм по поводу этой книги (и темы языков программирования в осо­
бенности) был заложен доктором Дэном Реслером (Dan Resler), адъюнкт-профессо­
ром Университета содружества Вирджинии. Я записался на его курс по теории ком­
пиляторов с абсолютным отсутствием интереса, а закончил его со страстью к теории
формальных языков. Спасибо, что поделились своим энтузиазмом (и некой частью
своей глубины понимания).
Благодарю всех моих друзей по PSU в когорте МВА. Это такое удовольствие узнать вас всех! Особенная благодарность - Кэти, Аманде, Миске, Сахар, Полу С.,
Кэти, Джону Р., Лауре, Джоэл, Тайлер П., Тайлер С. и Джес: вы все обогатили мою
жизнь!
Меня мотивируют не только сотрудники по Рор Art, но и мои друзья. Марк Бут
(Mark Booth): никто из друзей не знает меня лучше; я доверил бы ему самые пота­
енные секреты. Твои творческий потенциал и талант вдохновляют меня. Надеюсь не
разочаровать вас этой глупой книгой. Кэти Робертс (Katy Roberts) так же надежна,
22

Введение

как прилив, и так же красива. Кэти, благодарю вас за сердечную доброту и дружбу.
Сара Льюис (Sarah Lewis): мне нравится ваше лицо. Байрон (Byron) и Эмбер Клэй­
тон (Amber C1ayton) - истинные и верные друзья, которые всегда вызывают у меня
улыбку. Лоррэйн (Lorraine), прошли годы, но ты находишь все самое лучшее во мне2.
Кэйт Нахас (Kate Nahas): я был очень рад встретиться через столько лет; я надеюсь
поднять тост в память Дьюка. Десембер: спасибо за ваше доверие, теплоту и дру­
жеское отношение. Наконец благодарю моих новых друзей Криса Онстада ( Chris
Onstad) и Джессику Роу (Jessica Rowe): за прошлые два года вы двое принесли в мою
жизнь так много радости и смеха, что я не знаю, как обходился бы без вас.
Моей матери, Энн: спасибо за вашу поддержку, любовь и терпение. Спасибо мое­
му отцу, Тому, который остается для меня образцом любознательности, новаторства
и самоотдачи. Без него я был бы плохим инженером (или, возможно, не был бы им
вообще). Моя сестра, Мериес, всегда будет точкой опоры в моей жизни, демонстри­
руя преданность и уверенность.

2 Слова из песни.

-

Примеч. ред.

Введени е

23

От издательства
Вы, читатель этой книги, и есть главный ее критик. Мы ценим ваше мнение и хо­
тим знать, что было сделано нами правильно, что можно было сделать лучше и что
еще вы хотели бы увидеть изданным нами. Нам интересно услышать и любые другие
замечания, которые вам хотелось бы высказать авторам.
Мы ждем ваших комментариев. Вы можете прислать письмо по электронной по­
чте или просто посетить наш веб-сайт, оставив на нем свои замечания. Одним сло­
вом, любым удобным для вас способом дайте нам знать, нравится ли вам эта книга,
а также выскажите свое мнение о том, как сделать наши книги более подходящими
для вас.
Отсылая письмо или сообщение, не забудьте указать название книги и ее авторов,
а также ваш e-mail. Мы внимательно ознакомимся с вашим мнением и обязательно
учтем его при отборе и подготовке к изданию следующих книг. Наши координаты:
E-mail:
WWW:

info@dialekt ika . com
http : / /www . dialekt i ka . com

Наши почтовые адреса:
195027, Санкт-Петербург, Магнитогорская ул., д. 30, ящик 1 16
в России:
в Украине: 03 1 50, Киев, а/я 1 52

24

Введение

ГЛАВА 1

Ваше п ер в ое приложение

Лучше всего учиться на практике. Именно поэтому мы начнем с создания прос­
того приложения. Задача этой главы не в объяснении всего, что с этим связано: здесь
слишком много незнакомого и неясного. Мой вам совет - расслабьтесь и не пы­
тайтесь понять абсолютно все прямо сейчас. Эта глава должна заинтересовать вас.
Просто наслаждайтесь поездкой, и к тому времени, когда вы закончите эту книгу, все
в этой главе будет иметь для вас абсолютный смысл.
Если у вас нет серьезной практики в программировании, то одной
из вероятных сложностей для вас будет поначалу то, как компьюте­
ры воспринимают литералы. Человеческий разум способен спра­
виться с неправильным текстом довольно легко, но не компьютеры.
Если я сделаю грамматическую ошибку, то это может изменить ваше
мнение о моей литературной грамотности, но вы, вероятно, все же
поймете меня. У JavaScript, как и у всех языков программирования,
нет возможности справляться с неправильным вводом. Регистр букв,
орфография, порядок слов и пунктуация крайне важны. Если возни­
кают проблемы, удостоверьтесь, что скопировали все правильно: воз­
можно, вы не заметили точки с запятой, двоеточия, запятой или про­
бела, а возможно, вы смешали одиночные и парные кавычки или
употребили прописные буквы вместо строчных. Приобретя немного
опыта, вы узнаете, где можно "делать по-своему': а где следует быть
совершенно дотошным. Пока же вам имеет смысл вводить код при меров точно так, как он написан.
По традиции книги по программированию начинаются с примера "Hello, World"
(Привет, мир), который просто выводит на терминал фразу ''hello world". Если ин­
тересно, то эту традицию заложил в 1 972 году Брайан Керниган, специалист по ин­
форматике, работавший в Bell Labs. В печати это впервые появилось в книге The
С Programming Language1, опубликованной Брайаном Керниганом и Деннисом Ритчи
Брайан У Керни ган, Денни с М. Ритчи . Язык программирования С, 2-е издание, ISBN 978-5-84591 975-5, пер. с англ" Ид "Вильяме': 201 7.

1

в 1 978 году. Эта книга и по сей день является одной из наилучших и влиятельных
книг по языкам программирования, и я почерпнул из нее немало вдохновения, рабо­
тая над этой книгой.
Хотя "Hello, World" может показаться устаревшей традицией для современных по­
колений обучающихся программированию, скрытый смысл этой простой фразы оста­
ется сегодня таким же действенным, как и в 1978 году: это первые слова, произноси­
мые кем-то, в кого вы вдохнули жизнь. Это свидетельство того, что вы - как Проме­
тей, похитивший огонь у богов; как раввин, написавший истинное имя Бога на шине
Голема; как доктор Франкенштейн, вдохнувший жизнь в свое создание2. Такое подобие
творения, генезиса, и подвигло меня изначально к программированию. Возможно, од­
нажды некий программист (может быть, и вы) даст жизнь первому искусственно раз­
умному существу, и, возможно, его первыми словами будут "привет, мир':
В этой главе мы сбалансируем традицию, заложенную Брайаном Керниганом
44 года назад, искушенностью, доступной нынешним программистам. Мы увидим
'Ъello world" на экране, но это будет далеко от тех примитивных слов, высветивших­
ся пылающим фосфором на экране, которыми вы наслаждались бы в 1972 году.

С че rо на ч ать
В этой книге мы будем рассматривать использование JavaScript во всех его текущих
воплощениях (сервер, сценарий, рабочий стол, для браузера и т.д.), но по историчес­
ким и практическим причинам мы собираемся начать с программы для браузера.
Одна из причин, по которым мы начинаем с примера, выполняемого в браузе­
ре, в том, что это дает нам свободный доступ к графическим библиотекам. Люди
воспринимают информацию визуально, поэтому возможность соотнести концепции
программирования с визуальными элементами - это весьма мощный инструмент
обучения. В этой книге мы и так проведем много времени, уставившись на строки
текста, но давайте начнем с чего-то немного более наглядного. Этот пример я также
выбрал потому, что он органически знакомит с некоторыми очень важными концеп­
циями, такими как управляемое событиями программирование, которое вам весьма
пригодится в последующих главах.

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

2 Надеюсь, что у вас будет больше сострадания к своим созданиям, чем у доктора Франкенштейна,
и дела пойдут лучше.
26

Глава

1.

Ваше первое приложение

Я счастлив сообщить, что на момент написания книги на рынке нет ни одного бра­
узера, который не подходил бы для наших задач. Даже Internet Explorer, который долго
был камнем в ботинке программистов, взялся за ум и стал теперь на равнее с Chrome,
Firefox, Safari и Opera. Как уже говорилось, мой выбор - браузер Firefox, и здесь я буду
описывать его особенности, которые помогут вам в работе. У других браузеров также
есть эти возможности, но я опишу их так, как они реализуются в Firefox. Таким обра­
зом, при чтении этой книги вам имеет смысл использовать Firefox.
Вам понадобится текстовый редактор, чтобы писать код. Выбор текстовых редак­
торов может быть очень спорным (почти религиозные дебаты). В общем, текстовые
редакторы можно подразделить на редакторы текстового режима и оконные редак­
торы. Два самых популярных редактора текстового режима - это vi/vim и Emacs.
Одним из наибольших преимуществ редакторов текстового режима является то, что,
кроме собственного компьютера, вы можете использовать их по SSH, т.е. вы можете
соединиться с дистанционным компьютером и редактировать свои файлы в знако­
мом редакторе. Оконные редакторы выглядят современнее и обладают некоторыми
полезными (и более знакомыми)элементами пользовательского интерфейса. В боль­
шинстве случаев, однако, вы будете редактировать только текст, поэтому оконный
редактор не будет демонстрировать существенных преимуществ перед редактором
текстового режима. Популярные оконные редакторы - это Atom, SuЬlime Text, Coda,
Visual Studio, Notepad++, TextPad и Xcode. Если вы уже знакомы с одним из этих
редакторов, то, вероятно, нет никакого резона его менять. Но если вы используете
Блокнот из Windows, то я настоятельно рекомендую сменить его на более серьезный
редактор (Notepad++ - простой и бесплатный выбор для пользователей Windows).
Описание всех возможностей вашего редактора выходит за рамки этой книги, но
есть несколько средств, научиться использовать которые имеет смысл.
Выдепение синтаксиса

Для выделения синтаксиса используются разные цвета, позволяющие различать
синтаксические элементы в программе. Например, литералы могли бы быть одного
цвета, а переменные - другого (что означают эти термины, вы узнаете вскоре!). Это
может облегчить поиск проблем в коде. У большинства современных текстовых ре­
дакторов выделение синтаксиса есть и оно разрешено стандартно; если ваш код не
разноцветный, обратитесь к документации своего редактора и узнайте, как включить
возможность выделения.
Соответствие скобок

В большинстве языков программирования интенсивно используются круглые,
фигурные и квадратные скобки. Иногда содержимое этих скобок охватывает много
строк или даже несколько экранов; у вас будут скобки в пределах скобок, зачастую
разных типов. Критически важно, чтобы количество открывающих скобок совпадало
с количеством закрывающих, т.е. был их "баланс"; если этого нет, то ваша программа
Инструменты

27

не будет работать правильно. Редактор предоставляет визуальные маркеры в местах,
где скобки открываются и закрываются, помогая вам выявить проблемы незакры­
тых скобок. Средства соответствия скобок в различных редакторах реализованы
по-разному, от почти незаметного маркера до вполне очевидного. Несогласованные
скобки - весьма распространенная причина ошибок у новичков, поэтому я настоя­
тельно рекомендую узнать, как задействовать это средство в вашем редакторе.
Свертывание кода

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

Автозавершение (или завершение слов (word completion), или IntelliSense3)
весь­
ма удобное средство, пытающееся предположить то, что вы вводите, прежде, чем
закончите ввод. У этого средства две задачи. Первая - сэкономить время ввода.
Вместо, например, слова encodeURIComponent, вы можете просто ввести enc, а затем
выбрать encodeURIComponent из списка. Вторая задача - исследование. Например,
если вы введете enc, потому что хотите ввести encodeURIComponent, то обнаружите,
что есть еще функция encodeURI. В зависимости от редактора вы можете даже уви­
деть некоторую документацию, чтобы сделать выбор. Реализовать автозавершение
для JavaScript сложнее, чем для многих других языков, поскольку это язык со слабой
типизацией, а также из-за его правил областей видимости (о которых вы узнаете
позже). Если автозавершение важно для вас, то, вероятно, придется присмотреться
к ценам на редактор, удовлетворяющий вашим запросам. Здесь одни редакторы вхо­
дят в состав пакета, а другие (vim, например) обеспечивают очень мощное автоза­
вершение, но не без некоторой дополнительной настройки.
-

Комментари й о комментариях
В JavaScript, как и в большинстве языков программирования, есть синтак­
сис для комментариев (comment) в коде. Комментарии полностью игнорируются
JavaScript; они предназначены только для вас и других программистов. Они позволя­
ют добавлять в код объяснения, когда происходящее не ясно. В этой книге мы будем
щедро использовать комментарии в примерах кода, чтобы объяснить происходящее.
В JavaScript есть два вида комментариев: встраиваемые и блоковые. Встраивае­
мые начинаются с двух косых черт ( / / ) и простираются до конца строки. Блоковые
3

Терминология Microsoft.

28

Гла ва

1.

Ваше первое при л ожение

комментарии начинаются с косой черты и звездочки ( / * ) , а завершаются звездочкой
и косой чертой ( * /). Они могут охватить несколько строк. Следующий пример де­
монстрирует оба типа комментариев.
console . log ( 11echo11 )
/*

; 11 Выводит "ech o " на консоль

Все в предыдущей строке, д о пары косых черт, - это к:од Java Script ,
подчиняющийся синтаксическим правилам. Две косые черты на чинают
комментарий, игнорируемый Ja va Script . Этот текст находится в блоке
комментариев и также будет проигнорирован Ja va Script . Мы репмли
сдела ть отступ в этом блоке комментариев только для удобочитаемости,
необходимости в нем нет .

*/
/*

Смотри, мама , никакого отступа !

*/

Каскадные таблицы стилей (Cascading Style Sheet - CSS), которые м ы рассмот­

рим вскоре, также используют синтаксис JavaScript для блоковых комментариев
(встраиваемые комментарии в CSS не поддерживаются). В HTML (как и в CSS) нет
встраиваемых комментариев, а его блоковые комментарии отличаются от JavaScript.
Они окружены символами < ! и >.
-

-


HTML and CSS Example



body : { color : red; }
/ * Это комментарий CSS . . .
способный охва тывать несколько строк: . * /


console . log ( 11echo11 ) ; 11 Назад в JavaScript . . .
/* .
где поддерживаются и встраива емые,
и блоковые комментарии . * /
< / s cript>
< /head>
.

.

Первые ш аги
Мы собираемся начать с создания трех файлов: файла HTML, файла CSS и файла
исходного кода JavaScript. Мы могли бы все сделать в файле HTML (код JavaScript
и CSS может быть встроен в HTML), но в их раздельном хранении есть определен­
ные преимущества. Если вы новичок в программировании, я настоятельно рекомен­
дую следовать этим инструкциям шаг за шагом: в этой главе мы собираемся приме­
нить весьма любопытный подход, который облегчит процесс обучения.

Первые шаги

29

Может показаться, что мы делаем слишком много работы для осуществления
чего-то весьма простого, и некоторая правда в этом есть. Конечно, я мог бы предо­
ставить пример, получающий тот же результат за значительно меньше этапов, но
этим я привил бы вам плохие привычки. Представленные здесь дополнительные эта­
пы вы будете видеть еще много раз, и хотя сейчас это может показаться сверхслож­
ным, вы можете по крайней мере утешить себя тем, что учитесь делать все правильно.
Последнее важное примечание об этой главе. Это единственная глава в книге,
в которой примеры кода будут написаны в синтаксисе ESS, а не ES6 (Harmony). Это
сделано для гарантии того, что примеры кода будут выполняться, даже если вы не
используете браузер, поддерживающий ЕSб. В следующих главах будет изложено, как
написать код в ЕSб и "транскомпилировать" его так, чтобы он выполнялся на уста­
ревших браузерах. После рассмотрения этих основ в остальной части книги будет
использован синтаксис ЕSб. Примеры кода в этой главе достаточно просты, и ис­
пользование ESS не представляет существенного препятствия.
Файлы, создаваемые для этого упражнения, должны находиться в той
же папке. Я рекомендую создать для этого примера новую папку, что­
бы файлы не потерялись среди других.
Начнем с файла JavaScript. Используя текстовый редактор, создайте файл main . j s.
Добавьте в него одну следующую строку.
console . log ( ' main . j s loaded ' ) ;

Затем создайте файл CSS, main . css. Пока у нас нет ничего, что стоило бы вста­
вить здесь, поэтому включим в него только комментарий, чтобы не было пустого
файла.
/ * Здесь будут стили. * /

Затем создайте файл с именем index . html.
< ! doctype html>



< /head>

My first app l i cat ion !
Welcome t o < i >Learning Java S cript , Зrd Edit ion< / i > . < /p>
< s cript s rc="main . j s " >< / s cript>
< /body>
< /html>

Хотя эта книга и не об HTML или разработке веб-приложений, большинство
из вас изучает JavaScript именно с этой целью, поэтому мы затронем некоторое
30

Глава

1.

Ваше первое п риложе н и е

количество аспектов языка HTML, поскольку они касаются разработки на JavaScript.
Документ HTML состоит из двух основных частей: заголовка (head) и тела (body).
В заголовке содержится информация, которая непосредственно не отображает­
ся в браузере (хотя она может повлиять на то, что отображается в браузере). В теле
находится содержимое страницы, которая будет отображена в браузере. Важно по­
нимать, что элементы в заголовке никогда не будут представлены в браузере, тогда
как элементы в теле обычно отображаются (некоторые типы элементов, такие как
, не будут видимы, и стили CSS также способны скрыть элементы тела).
В заголовке содержится строка < l ink rel=" styleshee t " href= "ma i n . css " >;
вот так пустой в настоящее время файл CSS связывается с вашим документом. За­
тем, в конце тела, имеется строка < / script>, связывающая
файл JavaScript с вашим документом. Может показаться странным, что один под­
ключается в заголовке, а другой в конце тела. Дескриптор можно, конечно,
поместить и в заголовок, однако по некоторым причинам, включая производитель­
ность, имеет смысл помещать его в конец тела.
В теле имеется дескриптор My first application ! , представляющий
собой текст заголовка первого уровня (означающий самый больший и важный текст
на странице), сопровождаемый дескриптором (параграф), содержащим некий
текст, часть которого выделена курсивом (дескриптор < i > ).
Найдите и загрузите файл index . html в свой браузер. В большинстве систем проще
всего сделать это, дважды щелкнув на файле в средстве просмотра файлов (можно также
перетащить файл в окно браузера). Вы увидите содержимое тела своего файла HTML.
В этой книге много примеров кода. Поскольку файлы HTML
и JavaScript могут стать очень большими, я не буду представлять их
содержимое целиком каждый раз: вместо этого я объясню в тексте,
где фрагмент кода располагается в файле. У начинающих программис­
тов это может вызвать некоторое неудобство, однако понять способ
сборки кода необходимо и очень важно.

Консоль JavaScript
Мы уже написали некий код JavaScript: cons o l e . log ( ' main . j s l oaded ' ) . Что
он делает? Консоль (console) - это текстовый инструмент для программистов, по­
могающий им диагностировать свою работу. Вы будете часто использовать консоль
по мере изучения этой книги.
Разные браузеры предоставляют разные способы обращения к консоли. По­
скольку вы будете делать это весьма часто, я рекомендую узнать соответствующую
комбинацию клавиш. В Firefox - это (для Windows и Linux) или
(для Мае).

В окне, в котором загружен файл index . html, откройте консоль JavaScript; вы
должны увидеть текст "main.js loaded" (main.js загружен) (если вы не видите его, по­
пробуйте перезагрузить страницу). cons ol e . log - это метод4 вывода на консоль,
весьма полезный при отладке и подобном изучении.
Одним из многих преимуществ консоли является возможность, кроме наблю­
дения вывода своей программы, непосредственно вводить код ]avaScript, проверяя
что-то таким образом, изучая возможности JavaScript или даже внося временные из­
менения в свою программу.

Б иблиотека jQuery
Мы собираемся добавить к нашей странице чрезвычайно популярную клиентскую
библиотеку сценариев - jQиery. Хотя это и не обязательно, а для данной простой за­
дачи даже избыточно, именно такая вездесущая библиотека зачастую является первой
включаемой в код веба. Даже при том, что в этом примере мы могли бы легко обойтись
без библиотеки jQuery, вскоре вы привыкнете встречать ее в своем коде.
Библиотеку jQuery мы подключаем в конце тела перед собственно файлом
mai n . j s:
< script src="http s : / /code . j query . com/j query- 2 . 1 . 1 . min . j s " >< / s cript>
< s cript src="main . j s " >< / s cript>

Обратите внимание, что мы используем адрес URL из Интернета, а это значит,
что без доступа к Интернету ваша страница не будет работать правильно. Мы под­
ключаем библиотеку jQuery из открытой сети доставки контента (Content Delivery
Network - CDN), обладающей определенными преимуществами по производитель­
ности. Если вы будете работать над своим проектом без подключения к сети, придет­
ся загрузить файл и подключать его со своего компьютера. Теперь мы изменим свой
файл ma in . j s так, чтобы использовать в своих интересах одно из средств jQuery:
$ ( document ) . ready ( funct ion ( ) {
' us e s t ri c t ' ;
console . log ( ' main . j s loaded ' ) ;
});

Если у вас еще нет опыта использования библиотеки jQuery, то это, вероятно, выглядит непонятно. Здесь будет много такого, что станет понятным намного позже.
В данном случае jQuery позволяет удостовериться, что браузер загрузил весь код
HTML, прежде чем выполнить наш код JavaScript (который в настоящее время со­
стоит только из одной команды consol e . log). Всякий раз, работая с кодом JavaScript
в браузере, мы будем делать это только для практики: любой код JavaScript, который
4 Более подробная информация о различии между функцией и методом приведена в главе 9.
32

Глава

1.

Ваше первое п риложен ие

вы пишете, располагается между строками $ ( docwnent ) . ready ( function ( ) { и ) ) ; .
Обратите также внимание на то, что строка ' us e s trict '
это нечто, о чем вы
узнаете больше попозже, но в основном она указывает интерпретатору JavaScript об­
рабатывать ваш код более жестко. Хотя сначала это может показаться не очень хоро­
шей идеей, фактически это помогает писать лучший код JavaScript и предотвращает
наиболее распространенные ошибки. В этой книге мы, конечно, будем учиться пи­
сать очень строгий код JavaScript!
-

Р исование rрафи ческих примитивов
Одним из множества преимуществ HTMLS является стандартизированный гра­
фический интерфейс. Холст (canvas) HTMLS позволяет рисовать такие графические
примитивы, как квадраты, круги и многоугольники. Непосредственное использова­
ние холста может быть затруднительно, поэтому мы будем применять графическую
библиотеку Pape r . j s, чтобы использовать в своих интересах холст HTMLS.
Pape r . j s
не единственная доступная графическая библиотека.
Весьма популярны и надежны такие альтернативы, как KineticJS,
Fabric . j s и EaselJS. Я использовал все эти библиотеки, и все они
очень высокого качества.
-

Прежде чем мы начнем использовать библиотеку Pape r . j s, нам понадобится эле­
мент холста HTML для рисования. Добавьте в тело следующее (можно поместить
куда угодно, например после вводного параграфа):


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

г-1



У каждого элемента HTML может быть идентификатор. Чтобы быть
допустимым (правильным), каждый идентификатор должен быть
уникален. Создав холст с идентификатором "mainCanvas", мы не мо­
жем повторно использовать этот идентификатор. Поэтому рекомен­
дуется экономно использовать идентификаторы. Мы используем этот
идентификатор здесь потому, что новичкам зачастую проще знако­
миться с одной вещью за раз и по определению идентификатор может
относиться только к одной вещи на странице.

Р исование графических примитивов

33

Давайте изменим файл main . c s s так, чтобы наш холст выделялся на странице.
Если вы не знакомы с CSS, то это нормально. CSS просто устанавливает ширину
и высоту для нашего элемента HTML, а также добавляет черную границу.5
#mainCanvas {
width : 4 0 0рх ;
height : 4 0 0рх ;
border : solid lpx Ы а с k ;

Если вы перезагрузите свою страницу, то увидите холст.
Получив холст для рисования, давайте подключим библиотеку Pape r . j s, что­
бы она помогла нам с рисунком. Сразу после подключения библиотеки jQuery, но
до подключения собственного файла main . j s, добавьте следующую строку.
< s cript src="https : / / cdnj s . cloudflare . com/ a j a x / l ib s /paper . j s / 0 . 9 . 2 4 / l
paper-full . min . j s " >< / s cript>

Обратите внимание: для подключения библиотеки Paper . j s в наш проект мы ис­
пользуем CDN, как и в случае с библиотекой jQuery.
Вы уже начали понимать, что порядок подключения очень важен. По­
скольку мы собираемся использовать библиотеки jQuery и Paper . j s
в нашем файле mai n . j s, обе они подключаются первыми. Ни одна из
них не зависит от другой, поэтому не имеет значения, какая из библиотек подключается первой, но я всегда подключаю первой библиотеку
jQuery (в силу привычки), поскольку очень многое в веб-разработке за­
висит от нее.
Теперь, подключив библиотеку Pape r . j s, проделаем небольшую работу по ее
настройке. Подобный часто встречаемый код (повторяющийся перед вашим соб­
ственным) зачастую называют шаблоном (boilerplate). Добавьте следующее в файл
main . j s, сразу после ' use strict ' (если хотите, можете удалить console . log):
paper . install ( window) ;
paper . se tup ( document . getElementByid ( ' mainCanvas ' ) ) ;
/ / TODO
paper . view . draw ( ) ;

В первой строке библиотека Pape r . j s устанавливается в глобальную область
видимости (что будет иметь больше смысла в главе 7). Во второй строке библио­
тека Paper . j s подключается к холсту и готовится к рисованию. В середине, где мы
5 Всем, кто желает узнать больше о CSS и HTML, я рекомендую бесплатный курс по HTML и CSS
на Codecaderny.

34

Гла ва

1.

Ваше первое прил ожение

поместили комментарий TODO, будет расположен фактически интересный материал.
В последней строке Pape r . j s получает инструкцию нарисовать нечто на экране.
Теперь, когда с шаблоном покончено, давайте что-нибудь нарисуем! Начнем с зе­
леного круга в середине холста. Замените комментарий "TODO" следующими стро­
ками.
var с = Shape . Circle ( 2 0 0 , 2 0 0 , 5 0 ) ;
c . fil lColor = ' green ' ;

Обновите свой браузер и полюбуйтесь зеленым кругом. Вы написали свой первый
реальный код JavaScript. Фактически в этих двух строках происходит довольно мно­
го, но пока важно знать лишь несколько вещей. В первой строке создается объект
(object) круга с использованием трех аргументов (argument): координат х и у центра
круга, а также его радиуса. Помните, мы создали свой холст размером 400 пикселей
шириной и 400 пикселей высотой, поэтому центр холста находится в точке с коорди натами (200, 200). И радиус 50 делает круг размером в одну восьмую ширины и вы­
соты холста. Во второй строке устанавливается цвет заполнения, отличный от цвета
контура (штрих (stroke), на языке Paper . j s). Не бойтесь экспериментировать с из­
менением этих аргументов.

Автоматизация повторя ю щ ихся зада ч
Предположим, необходимо не просто добавить один круг, а заполнить холст ими,
расположив в табличном порядке. Если сделать круги немного меньше, то на холсте
можно разместить 64 круга. Конечно, вы могли бы скопировать только что напи­
санный код 63 раза и вручную модифицировать все координаты так, чтобы круги
располагались в виде таблицы. Похоже на большое количество работы, не так ли?
К счастью, компьютер отлично подходит для повторяющихся задач такого вида. Да­
вайте рассмотрим, как мы можем нарисовать 64 равномерно расположенных круга.
Заменим свой код, рисующий одиночный круг, следующим.
var с ;
for ( var х=2 5 ; х 5;
3 >= 5 ;
3 < 5;
3 5;
5 >= 5 ;
5 < 5;
5 = O ; i - - )
nameBackwards += sel f . name [ i ] ;

{

return nameBackwards ;
return ' $ { getRever s eName ( ) } s i eman ym , olleH ' ;
},
};
o . greetBackwards ( ) ;

Это общепринятая методика, и вы часто будете встречать присваивание значения
переменной this константам s e l f, или that. Стрелочные функции, которые мы бу­
дем рассматривать далее в этой главе, являются еще одним средством решения этой
проблемы.

Функциональные выражения и анонимные функции
До сих пор мы имели дело исключительно с объявлениями функций (function
declaration), которые присваивают функции и тело (т.е. то, что функция дела­
ет), и идентификатор (он позволяет впоследствии вызывать функцию по имени).
JavaScript поддерживает также анонимные функции (anonymous function), у которых
не обязательно есть идентификатор.
У вас может возникнуть резонный вопрос "Как использовать функцию, у кото­
рой нет идентификатора? Как мы должны вызывать ее без идентификатора?" От­
вет кроется в понятии функциональных выражений (function expression). Известно,
что выражение - это нечто, что вычисляет значение, и мы также знаем, что функ­
ция - это также значение, как и все остальное в JavaScript. Функциональное вы­
ражение - это просто средство для того, чтобы объявить (возможно, безымянную)
функцию. Функциональное выражение может быть присвоено чему-нибудь (в ре­
зультате ему будет назначен идентификатор) или сразу же вызвано2•
Функциональные выражения синтаксически идентичны объявлениям функций,
за исключением того, что вы можете опустить имя функции. Давайте рассмотрим
пример, в котором мы используем функциональное выражение и присвоим резуль­
тат переменной (который фактически эквивалентен объявлению функции).

2 Это немедленно вызываемое функц иональное выражение (Immediately Invoked Function Expression IIFE), которое мы рассмотрим в главе 7.

1 38

Гла ва 6 . Ф ункции

const f = function ( )
11 . . .
};

Результат такой, как будто мы объявили функцию обычным способом: здесь
имеется идентификатор f, который позволяет обращаться к функции. Как и при
обычном объявлении функции, мы можем вызвать ее, используя синтаксис f ( ) .
Единственное отличие в том, что здесь мы создаем анонимную функцию (используя
функциональное выражение) и присваиваем ее переменной.
Анонимные функции используются все время: как аргументы других функций
или методов либо при создании функциональных свойств в объекте. Мы будем
встречаться с ними повсюду в книге.
Я сказал, что имя функции является необязательным в функциональном выраже­
нии . . . Так что же происходит, когда мы присваиваем функции имя и присваиваем ее
переменной (и для чего это может нам понадобиться) ? Рассмотрим пример.
const g = function f ( )
11 . . .

{

Когда функция создается таким способом, имя g имеет приоритет, и для обра­
щения к функции (извне функции) мы используем имя g; попытка доступа к f при­
водит к ошибке неопределенности переменной. С учетом вышесказанного зачем бы
нам это могло понадобиться? Это может быть необходимо, если нужно обратиться
к функции из самой функции (так называемая рекурсия (recursion)).
const g = funct ion f ( stop) {
i f ( stop) console . log ( ' f остановлена ' ) ;
f ( true ) ;
};
g ( fa l se ) ;

В самой функции мы используем f, чтобы сослаться на функцию, а вне мы ис­
пользуем g. Нет никакого особенно серьезного основания, чтобы присваивать функ­
ции два разных имени, но здесь мы делаем так для пояснения работы именованных
функциональных выражений.
Поскольку объявление функции и функционального выражения выглядят идентич­
но, вы могли бы задаться вопросом «Как JavaScript их различает (или есть ли там ка­
кое-нибудь различие)?» Оrвет - контекст: если объявление функции используется как
выражение, то это функциональное выражение, а если нет, то это объявление функции.
Различие является главным образом академическим, и вы не должны обычно ду­
мать о нем. Определяя именованную функцию, которую вы намереваетесь вызвать
позже, вы, вероятно, будете использовать объявление функции, не думая об этом,
а если необходимо создать функцию для присваивания чему-то или для передачи
в другую функцию, вы будете использовать функциональное выражение.
Ф ункциона л ьные вы ражения и анонимные функ ции

1 39

Стрелоч ная нотация
В спецификацию ЕSб введен новый долгожданный синтаксис стрелочной нота­
ции (arrow notation). Это чрезвычайно полезный синтаксис (он имеет одно серьез­
ное функциональное отличие, до которого мы вскоре дойдем), который существенно
экономит время на вводе слова funct ion, а также сокращает количество фигурных
скобок, которые нужно ввести.
Стрелочные функции позволяют упростить синтаксис тремя способами.


Опустить слово function.



Если функции передается один аргумент, опустить круглые скобки.



Если тело функции - одно выражение, опустить фигурные скобки и оператор
return.

Стрелочные функции всегда являются анонимными. Вы вполне можете присво­
ить их переменной, но не можете создать именованную функцию, как в случае ис­
пользования ключевого слова function.
Рассмотрим следующие эквивалентные функциональные выражения.
const f 1

function ( )

1 1 или
const f l

( ) => "hello ! " ;

cons t f2

funct ion ( name )

1 1 или
const f2

name => ' He l l o , $ { name } ! ' ;

cons t fЗ

function ( а , Ь )

1 1 или
const fЗ

( а , Ь ) => а

{ r eturn "hello ! " ; }

+

{ r eturn ' He l l o , $ { name } ! ' ; }

{ return а + Ь ; }

Ь;

Эти примеры немного надуманы; обычно, если необходима именованная функ­
ция, вы просто используете обычное объявление функции. Стрелочные функции
особенно полезны для создания и передачи в виде параметров анонимных функций,
которые мы будем видеть весьма часто начиная с главы 8.
У стрелочных функций действительно есть одно серьезное отличие от обычных
функций: переменная this привязывается лексически, точно так же, как и любая дру­
гая переменная. Вспомните наш пример greetBackwards ранее в главе. Со стрелочной
функцией мы можем использовать переменную this во внутренней функции.
const о = {
name : ' Ju l i e ' ,
greetBackwards : funct ion ( ) {
const getRever seName = ( ) =>
l e t nameBackwards = ' ' ,.

1 40

Глава 6. Ф ункции

for ( let i=this . name . length - 1 ; i>= O ; i - - )
nameBackwards += this . name [ i ] ;

{

return nameBackwards ;
};
return ' $ { getReverseName ( ) } s i eman ym , ol l eH ' ;
},
};
o . greetBackwards ( ) ;

У стрелочных функций есть еще два дополнительных отличия от обычных функ­
ций: они не могут использоваться как конструкторы объекта (см. главу 9) и в них не­
доступна специальная переменная arguments (в которой больше нет необходимости
благодаря оператору расширения).

М етоды call, apply и Ьind
Мы уже видели, что значение переменной this зависит от вызываемого контекста
(как и в других объектно-ориентированных языках). Но JavaScript позволяет опреде­
лять, к чему привязана переменная this, независимо от того, как или где вызывается
рассматриваемая функция. Давайте начнем с метода call, который доступен во всех
функциях. Он позволяет вызывать функцию с определенным значением this.
const bruce
{ name : " Bruce" } ;
const madel ine = { name : "Made l in e " } ;
=

1 1 эта функция не связана ни с каким объектом,
1 1 но в се же в ней используется ' this ' !
funct ion greet ( ) {
return ' Привет ! Меня зовут $ { th i s . name } ! ' ;

greet ( ) ;
greet . ca l l ( bruce ) ;
зовут Bruce ! " - ' this
greet . call (made l ine ) ;

/ / "Приве т ! Меня зовут ! " - ' this ' не привязана
// "Приве т ! Меня
' привязана к 'bruce '
/ / "Приве т ! Меня зовут Madeline ! " - ' this ' привязана
/ / к 'made l in e '

Как можно заметить, метод c a l l позволяет вызывать функцию, как будто это
метод, предоставляемый объектом, к которому привязана переменная thi s. Первый
аргумент метода c a l l
это значение, к которому вы хотите привязать this, а все
остальные аргументы становятся аргументами вызываемой функции.
-

function update ( birthYear, occupat ion )
t hi s . birthYear
b irthYe a r ;
t hi s . occupat ion
occupat i o n ;
=

=

М етод ы call, apply и Ьind

1 41

update . ca l l ( bruce, 1 9 4 9 , ' s inger ' ) ;
/ / bruce теперь { пате : "Bruce " , ЬirthYea r : 1 94 9 ,
//
occupa t ioп : "siпger " }
update . ca l l (made l i n e , 1 9 4 2 , ' actress ' ) ;
/ / тadeliпe теперь { пате : "Madeliп e " , Ьirth Year : 1 942,
//
occupa t ioп : "actress " }

Метод apply идентичен методу call, за исключением способа, которым он обраба­
тывает аргументы функции. Методу call аргументы передаются непосредственно, точно
так же, как и обычной функции. Метод apply аргументы передаются в виде массива.
update . app l y ( bruce , ( 1 9 5 5 , " actor" ] ) ;
/ / bruce теперь { пате : "Bruce " , ЬirthYea r : 1 955,
//
оссира tioп : "a ctor" J
update . appl y ( made l ine , ( 1 9 1 8 , "writer " ] ) ;
/ / тadeliпe теперь { пате : "Ma deliп e " , Ьirth Year : 1 9 1 8 ,
11
occupa tioп : "wri ter " }

Метод apply полезен, если у вас есть массив и вы хотите использовать его зна­
чения как аргументы функции. Классический пример - поиск минимального или
максимального числа в массиве. Вспомогательным функциям Ма th . rnin и Ма th . шах
можно передать любое количество аргументов, а они возвращают минимальное или
максимальное значение, соответственно. Мы можем использовать метод apply, что­
бы вызвать эти функции с существующим массивом.
const arr = ( 2 , 3 , - 5 , 1 5 , 7 ] ;
Math . mi n . appl y ( nul l , arr ) ;
Math . max . appl y ( nul l , arr ) ;

11 -5
11 1 5

Обратите внимание, что вместо значения this м ы просто передаем nul l. Так про­
исходит потому, что в функциях Math . rnin и Math . rnax не используется переменная
this вообще; поэтому не имеет никакого значения, что мы передаем в качестве зна­
чения для this.
Оператор расширения ( . . . ) ЕSб позволяет достичь того же результата, что и ме­
тод apply. В экземпляре нашего метода update, в котором значение this действи­
тельно важно, мы все еще должны использовать метод call, но для функций Math .
rnin и Math . rnax, для которых оно не имеет значения, мы можем использовать опера­
тор расширения, чтобы вызвать эти функции непосредственно.
const newBruce = [ 1 9 4 0 , "mart i a l
update . c a l l ( bruce, . . . newBruc e ) ;
Math . min ( . . . arr ) ;
Math . max ( . . . arr ) ;

artist " ] ;
/ / эквивалент apply (bruce, п ewBruce)
// -5
// 1 5

Есть еще одна функция, Ьind, которая позволяет определить значение для пе­
ременной thi s . Функция Ь i nd позволяет перманентно ассоциировать значение
для t h i s с функцией. Предположим, что мы распространяем свой метод upda te

1 42

Глав а 6 . Функции

и хотим удостовериться, что в нем переменной this всегда будет присвоено значение
bruce, независимо от того, как он будет вызван (даже с функцией call, app ly или
другой функцией bind). Функция bind позволяет сделать так.
const updateBruce = update . bind ( bruce ) ;
updateBruce ( l 9 0 4 , " actor " ) ;
11 bruce теперь ( пате : "Bruce " , birthYear : 1 90 4 , occupa tioп : "a ctor" }
updateBruce . ca l l (madeline, 1 2 7 4 , " king" ) ;
1 1 bruce теперь ( пате : "Bruce " , ЬirthYear : 1 2 7 4 , occupa tioп : "king" } ;
11 тadeline не бwro присвоено !

Тот факт, что действие функции bind является постоянным, делает ее потенци­
альным источником ошибок, которые трудно обнаружить: в результате вы получаете
функцию, которую фактически нельзя использовать с функциями call, app l y или
bind (во второй раз). Представьте себе, что функция передается в виде параметра,
и она, будучи вызванной с помощью функций call или apply в некоем отдаленном
месте, полностью уверена в правильной привязке this. Я не говорю вам, что Ьind не
нужно использовать, она весьма полезна, но помните о налагаемых ею ограничениях.
Вы можете также предоставить функции Ьind параметры, которые позволяют
создать новую функцию, всегда вызываемую с определенными параметрами. Напри­
мер, если необходима функция update, которая всегда устанавливает год рождения
bruce равным 1 9 4 9, но все еще позволяет изменять род занятия, вполне можете по­
ступить следующим образом.
const updateBruce l 9 4 9 = update . bind ( bruc e , 1 9 4 9 ) ;
updateBruce l 9 4 9 ( " s inger, songwriter " ) ;
1 1 bruce теперь ( пате : "Bruce " , Ьirth Year : 1 94 9 ,
11
occupa t ion : "singer, songwriter" }

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

ГЛАВА 7

Област ь в идимости

Область видимости (scope) устанавливает, где и когда определяются переменные,
константы и аргументы. Мы уже имеем некоторое представление об области види­
мости: нам известно, что аргументы функции существуют только в теле функции.
Рассмотрим следующее.
function f (х) {
return х + 3 ;
f (5) ;
х;

11 8
1 1 Referen ceError : х не определена

Мы знаем, что переменная х существует очень недолго (только при вычислении
х + 3 ), а за пределами тела функции х как будто не существует. Таким образом, мы
говорим, что область видимости переменной х - это функция f.
Когда мы говорим, что область видимости переменной - это данная функция, мы
должны помнить, что формальных аргументов в теле функции не существует, пока
функция не будет вызвана (и они не станут, таким образом, фактическими аргумен­
тами). Функция может быть вызвана многократно, и при каждом вызове ее аргументы
появляются и затем выходят из области видимости, когда функция завершается.
Мы также принимаем как очевидное, что переменные и константы не существу­
ют, пока мы их не создадим. Таким образом, они не находятся в области видимости,
пока мы не объявляем их с ключевым словом let или cons t (var - это частный
случай, который мы рассмотрим далее в этой главе).

В некоторых языках есть явное различие между объявлением
(declaration) и определением (definition). Как правило, объявление
переменной означает, что вы объявляете о ее существовании, указав
компилятору ее идентификатор. Определение, напротив, обычно оз­
начает объявление и присвоение значения переменной. В JavaScript
эти два термина являются синонимами, поскольку всем переменным
присваиваются значения при их объявлении (в противном случае им
неявно присваиваются значения undefined).

Область видимости и существование переменных
Интуитивно понятно, что если переменной не существует, ее нет и в области ви­
димости. Таким образом, переменные, которые еще не были объявлены, или пере­
менные, которые прекратили существование после выхода из функции, очевидно не
находятся в области видимости.
А наоборот? Если переменная не находится в области видимости, то означает ли это,
что ее не существует? Вовсе не обязательно, и именно здесь необходимо сделать разли­
чие между областью видимости (scope) и существованием (existence) переменной.
Область видимости (или видимость (visiЬility)) относится к идентификаторам, ко­
торые в настоящее время видимы и доступны выполняющейся в данный момент час­
ти программы (называемой контекстом выполнения (execution context)). Существо­
вание, напротив, относится к идентификаторам, которые содержат нечто, для чего
была распределена (т.е. зарезервирована) область памяти. Скоро мы увидим примеры
переменных, которые существуют, но не находятся в области видимости.
Когда нечто прекращает существовать, JavaScript не обязательно освобождает па­
мять сразу же: она просто помечается как не используемая и освобождается только
при периодически запускаемом процессе сборки мусора (garbage collection). Сборка
мусора в JavaScript осуществляется автоматически и будет вас интересовать только
в определенных чрезвычайно требовательных приложениях.

Лекси ч еская или динами ч еская область видимости
Глядя на исходный код программы, вы видите ее лексическую структуру (lexical
structure). Когда программа выполняется фактически, поток выполнения может быть
не последовательным. Рассмотрим программу с двумя функциями.
funct ion fl ( ) {
console . log ( ' one ' ) ;
funct ion f2 ( ) {
console . log ( ' two ' ) ;

f2 ( ) ;
f1 ( ) ;
f2 ( ) ;

Лексически эта программа - просто набор операторов, которые мы обычно чи­
таем сверху вниз. Но когда мы запускаем эту программу, поток выполнения пере­
ходит сначала к телу функции f2, затем - к телу функции fl (даже при том, что она
определена до f2) и наконец - снова к телу функции f2.

1 46

Глава 7. Область вид имости

Области видимости в JavaScript являются лексическими, а значит, можно опреде­
лить, какие переменные находятся в области видимости, просто посмотрев на ис­
ходный код. Я не хочу сказать, что области видимости всегда и сразу очевидны из
исходного кода: в этой главе мы увидим несколько примеров, которые требуют прис­
тального внимания для определения областей видимости.
Лексическая область видимости означает, что в области видимости функции на­
ходятся только те переменные, которые были определены до момента определения
самой функции (не путать с моментом ее вызова). Рассмотрим пример.
const х
3;
function f ( ) {
console . log ( x ) ; / / это сработает
console . log ( y ) ; / / а это - нет
=

const у
f() ;

3;

Когда мы определили функцию f переменная х уже существовала, а переменная
у
еще нет. Затем мы объявили у и вызвали f. Переменная х находится в области
видимости тела функции f при ее вызове, а переменная у
нет. Это пример лекси­
ческой области видимости: у функции f есть доступ к идентификаторам, которые
существовали на момент ее определения, но не на момент вЬLзова.
Лексическими в JavaScript являются глобальная область видимости (global scope),
область видимости блока (Ыосk scope) и области видимости функции (function scope).
-

-

Глобальная область видимости
Область видимости имеет иерархический, древовидный характер. Та область
видимости, в которой вы находитесь в момент запуска программы, называется гло­
бальной областью видимости (global scope). Вновь запущенная программа JavaScript
(прежде, чем будут вызваны любые функции) выполняется в глобальной области
видимости. Таким образом, все, что вы объявите в глобальной области видимости,
будет доступно для всех областей видимости в вашей программе.
Все, что объявлено в глобальной области видимости, называется глобальными
переменными, а у глобальных переменных, как известно, очень плохая репутация.
Открыв любую книгу по программированию вы узнаете, что при использовании гло­
бальных переменных "земля уйдет из под ваших ног и поглотит вас целиком". Так
почему же глобальные переменные столь плохи?
Глобальные переменные вовсе не плохи, это нужная вещь. Плохо, когда глобаль­
ную область видимости используют неправильно. Мы уже упоминали, что все до­
ступное в глобальной области видимости доступно во всех областях видимости. От­
сюда мораль: глобальные переменные следует использовать рассудительно.
Глобал ьна я область вид имости

1 47

Догадливый читатель мог бы подумать: "Хорошо, я создам в глобальной области
видимости одну функцию и сведу мои глобальные переменные к одной функции!"
Прекрасно, но только теперь вы просто перенесли проблему на один уровень вниз.
Все, что будет объявлено в пределах этой функции, будет доступно для всего, что
вызывается в этой функции ... что едва ли лучше глобальной области видимости!
Подведем черту: у вас, вероятно, будет нечто в глобальной области видимо­
сти, и это не обязательно плохо, а вот пытаться избегать следует того, что зависит
от глобальной области видимости. Давайте рассмотрим простой пример: отслежива­
ние информации о пользователе. Ваша программа отслеживает имя и возраст поль­
зователя, а также имеет несколько функций, которые работают с этой информацией.
Это можно сделать, используя глобальные переменные.
let n ame = " I rena " ;
let age = 2 5 ;

/ / глобальная
/ / глобальная

funct ion greet ( )
console . log ( ' He l l o , $ ( name } ! ' ) ;
funct ion getBirthYear ( )
return new Date ( ) . ge t Ful lYear ( ) - age ;

Проблема этого подхода в том, что наши функции жестко зависят от контекста
(или области видимости), из которого они вызываются. Любая функция (в любой час­
ти вашей программы) может изменить значение name (случайно или преднамеренно).
Идентификаторы "name " и " age" (имя и возраст) весьма распространены и вполне мо­
гут использоваться в другом месте по другим причинам. Поскольку функции greet
и getBi rthYear зависят от глобальных переменных, они, возможно, безосновательно
полагают, что остальная часть программы использует name и age правильно.
Лучше поместить всю информацию о пользователе в один объект.
let user = (
name
" I rena " ,
a ge = 2 5 ,
};
=

funct i on greet ( ) (
console . log ( ' He l l o , $ ( user . name } ! ' ) ;
funct ion getBirthYea r ( )
return new Date ( ) . getFullYear ( ) - user . age ;

В этом простом примере мы сократили количество идентификаторов в глобаль­
ной области видимости только на один (мы избавились от name и age, но добавили
user), но что если у нас будет 10 пользователей ... или 1 00?
148

Гла ва 7. Область ви д имости

Но мы могли бы добиться большего: наши функции greet и getB i r thYear все
еще зависят от глобального объекта user, который может быть изменен как-то еще.
Давайте улучшим эти функции таким образом, чтобы они не зависели от глобальной
области видимости.
function greet ( us e r )
console . log ( ' Hello, $ { user . name } ! ' ) ;
funct ion getBirthYear ( u s e r ) {
return new Date ( ) . getFul lYea r ( ) - user . age ;

Теперь наши функции могут быть вызваны из любой области видимости, и поль­
зователь им передается явно (узнав о модулях и объектно-ориентированном про­
граммировании, мы увидим лучшие способы решения этой проблемы).
Если бы все программы были так просты, то едва ли имело бы значение, исполь­
зуем мы глобальные переменные или нет. Но когда ваша программа насчитывает ты­
сячи (или сотни тысяч) строк, вы не можете помнить все области видимости сразу
(или даже только те, что находятся на экране). Поэтому становится критически важ­
но не зависеть от глобальной области видимости.

Область видимости блока
Ключевые слова l e t и const объявляют идентификаторы в области видимости
блока (Ьlock scope). В главе 5 упоминалось, что блок - это список операторов, за­
ключенный в фигурные скобки. Поэтому, под областью видимости блока подразуме­
ваются только те идентификаторы, которые доступны в пределах блока.
console . log ( ' пepeд блоком ' ) ;
{
console . log ( ' внyтpи блока ' ) ;
const х = 3 ;
console . log ( x ) : / / выводит З
console . log ( ' зa пределами блока ; х=$ { х } ' ) ; // ReferenceError : х не определена

Это автономный блок (standalone Ыосk): обычно блок является частью оператора
управления потоком, такого как i f или for, но это вполне допустимый синтаксис,
блок может быть и автономным. Переменная х определяется в блоке, поэтому по за­
вершении блока она выходит из области видимости и считается неопределенной.

В главе 4 упоминалось, что автономные блоки не очень часто приме­
няются на практике; они используются для контроля областей в иди мости (как мы увидим в этой главе), но это редко необходимо. Одна­
ко они очень удобны для объяснения действия областей видимости,
вот почему мы используем их в этой главе.
Область видимости блока

1 49

М аскировка переменной
Популярным источником недопонимания являются одноименные переменные
или константы в разных областях видимости. Когда области видимости следуют
одна за другой, все относительно просто.
1 1 блок 1
const х = ' Ыuе ' ;
conso l e . log ( х ) ;

/ / выводит "Ы ие "

console . lo g ( typeof х ) ; 1 1 выводит "undefined"; х вне области видимости
{
1 1 блок 2
con s t х = 3 ;
console . log ( x ) ;

1 1 ВЫВОДИТ ,,3,,

console . lo g ( typeof х ) ; / / выводит "undefined " ; х вне обла сти видимости

Здесь вполне понятно, что есть две разные переменные, обе по имени х, но в раз­
ных областях видимости. Теперь рассмотрим, что происходит во вложенных областях
видимости.
// внешний блок
let х = ' Ыuе ' ;
console . log ( x ) ;

1 1 выводит "Ы ие "

{
1 1 внутренний блок
let х = 3 ;
console . log ( x ) ; / / выводит "3 "

console . log ( х ) ;
console . log ( typeof х ) ;

/ / выводит "Ыи е "
1 1 выводит "undefined"; х внеобла сти видимости

Этот пример демонстрирует маскировку переменной (variaЬle masking). Перемен­
ная х во внутреннем блоке отличается от таковой во внешнем блоке (хотя и имеет
такое же имя), что в действительности маскирует (или скрывает), переменную х,
определенную во внешней области видимости.
Здесь важно понимать, что, когда процесс выполнения входит во внутренний
блок и определяется новая переменная х, в области видимости находятся обе пере­
менные; у нас просто нет никакого способа обратиться к переменной из внешней
области видимости (поскольку у нее то же имя). Сравните это с предыдущим при­
мером, где один х входит в область видимости, а затем выходит из нее прежде, чем
вторая переменная х делает то же самое.

1 50

Глава 7. Область вид имости

С учетом этого рассмотрим следующий пример.
1 1 внешний блок
l e t х = { color : "Ыuе" } ;
let у
х;
1 1 у и х ссылаются на тот же объект
let z = 3 ;
{
1 1 внутренний блок
let х = 5 ;
1 1 внешний х теперь зама скирован
console . log ( x ) ;
1 1 выводит 5
console . log ( y . color ) ; 1 1 выводит "Ы u е " ; объект, на который
1 1 указывает у (и х во внешней обла сти
1 1 видимости) , в се еще находится в области
1 1 видимости
y . color = " red" ;
console . log ( z ) ;
console . log ( x . color ) ;
console . log ( y . color ) ;
console . log ( z ) ;

1 1 выводит 3;

z

не зама скирована

выводит "red" ; объект изменяется в о
внутренней обла сти видимости
выводит "red"; х и у ссылаются на тот
же объект
1 1 выводит 3
11
11
11
11

Маскировку переменных иногда называют затенением (shadowing)
( т.е. переменная из внешней области видимости попадает в тень пе­
ременной с тем же именем во внутренней области). Мне никогда не
нравился этот термин, поскольку тени никогда не скрывают сущности полностью, только делают их более темными. Когда переменная
маскируется, она полностью недоступна.
Иерархический характер областей видимости на настоящий момент должен быть
уже понятен: вы можете ввести новую область видимости, не покидая прежней. Это
создает цепь областей видимости, определяющих, какие переменные находятся в об­
ласти видимости: все переменные в текущей цепи областей видимости находятся
в области видимости и (пока они не маскируются) вполне доступны.

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

1 51

а использованы - в другом, поэтому вам, возможно, придется приложить некото­
рые усилия, чтобы разобраться в их областях видимости.
В "традиционной" программе все ваши функции могли бы быть определены
в глобальной области видимости, и если вы не будете обращаться к глобальной об­
ласти видимости из своих функций (как я рекомендую), вы даже не должны будете
заботиться о том, к какой области видимости у ваших функций есть доступ.
В современных программах на JavaScript, однако, функции зачастую определяют­
ся везде, где они необходимы. Их присваивают переменным или свойствам объектов,
добавляют в массивы, передают в другие функции, возвращают из функций, а иногда
не присваивают им имен вообще.
Довольно часто функцию преднамеренно определяют в некоторой области види­
мости, чтобы гарантированно получить доступ к переменным из этой области. В ре­
зультате получается замкнутое выражение (closure) (вы можете считать его областью
видимости, замкнутой вокруг функции). Давайте рассмотрим пример замкнутого
выражения.
/ / неопределенная глобальная функция

l e t globa l Fun c ;
let ЬlockVar = ' а ' ;
global Func = funct ion ( )
console . log ( Ьl ockVar ) ;

1 1 переменная обла сти видимости блока

/ / выводит "а "

globalFunc ( ) ;

В функции glob a l Func присваивается значение переменной в пределах блока:
этот блок (и его родительская, глобальная область видимости) формирует замкнутое
выражение. Независимо от того, где вы вызываете функцию globalFunc, у нее будет
доступ к идентификаторам в этом замкнутом выражении.
Давайте рассмотрим происходящее: при вызове у функции global Func есть до­
ступ к переменной ЫockVar, несмотря на то что мы вь1шли из этой области ви­
димости. Обычно после выхода из области видимости объявленные в этой области
видимости переменные могут безопасно прекратить существование. Здесь движок
JavaScript замечает, что функция определена в этой области видимости (обратиться
к функции можно и за пределами области видимости), поэтому переменная должна
быть всегда доступна.
Таким образом, определение функции в пределах замкнутого выражения может
повлиять на продолжительность существования замкнутого выражения; это также
позволяет нам получить доступ к сущностям, к которым у нас обычно не было до­
ступа. Рассмотрим пример.
let f;

/ / неопределенная функция

let о = { not e :

1 52

' Безопасно ' } ;

Глава 7. Область видимости

f

fuпct i oп ( )
returп о ;

l e t oRef = f ( ) ;
oRe f . пo t e = "Все же не совсем безопасно ! " ;

Обычно нечто вне области видимости строго недоступно. Функции являются ис­
ключением, они предоставляют нам окна в области видимости, которые в противном
случае были бы недоступны. Мы убедимся в важности этого в следующих главах.

Немедленно вызываемые
функциональные выражения
В главе 6 м ы рассматривали функциональные выражения. Функциональные вы­
ражения позволяют создавать немедленно вызываемые функциональные выражения
(Immediately Invoked Function Expression - IIFE). IIFE объявляет функцию, а затем
немедленно ее запускает. Теперь, имея понятие об областях видимости и замкнутых
выражениях, можно обсудить, зачем они могли бы нам понадобиться. IIFE выглядит
следующим образом.
( fuпct ioп ( ) {
/ / это тело IIFE
} ) () ;

Мы создаем анонимную функцию, используя функциональное выражение, а за­
тем немедленно ее вызываем. Преимущество IIFE в том, что все в ней имеет соб­
ственную область видимости, а также, поскольку это функция, она может передать
нечто из области видимости.
coпst message = ( func t i o n ( )
con s t secret = " Здесь указан пароль ! " ;
return ' Пароль имее т длину $ { s e cret . lengt h } символов . ' ;
}) ();
consol e . l og ( me s s age ) ;

Переменная secret защищена в области видимости IIFE, к ней нельзя обратить­
ся извне. Вы можете возвратить из IIFE все, что хотите, и весьма часто возвращают
массивы, объекты и функции. Давайте рассмотрим функцию, которая способна со­
общить о количестве вызовов и в которую нельзя вмешаться.
const f = ( function ( )
let count = О ;
return function ( )
return ' Меня вызывали $ { ++count } раз ( а ) . ' ;

Немедленно вы з ы ваемые функциональные выражения

1 53

}) () ;
f ( ) ; / / " раз (а) . "
f ( ) ; / / "Меня вызывали 2 раз (а ) . "
11" .

Поскольку переменная count надежно защищена в IIFE, нет никакого способа из­
менить ее из вне: у функции f всегда будет точный подсчет количества раз, когда она
была вызвана.
Хотя использование переменных из области видимости блока в ЕSб несколько
снизило потребность в IIFE, последнее все еще весьма популярно и полезно, когда
нужно создать замкнутое выражение и возвратить нечто из него.

Область видимости функции и механизм
подъема объявлени й
До введения в ЕSб ключевого слова let переменные объявлялись с ключевым
словом var и имели всю область видимости функции (function scope) (глобальные
переменные, объявленные с ключевым словом var вне функции, имеют то же по­
ведение).
Когда вы объявляете переменную с ключевым словом let, она не будет существо­
вать в коде до момента ее объявления. Когда вы объявляете переменную с ключевым
словом var, она будет доступна повсюду в текущей области видимости". даже перед
ее оператором объявления. Прежде чем мы рассмотрим пример, запомните, что есть
различие между переменной, которая не объявлена, и переменной, которой присво­
ено значение undefined. Необъявленные переменные приводят к ошибке, тогда как
переменные, которые существуют, но имеют значение unde fined, - нет.
l e t var l ;
let var2
unde f ined;
/ / undefined
varl ;
/ / undefined
var 2 ;
undefinedVar ; / / Referen ceError : undefin edVar

не определена

При использовании ключевого слова let вы получите ошибку, если попытаетесь
обратиться к переменной до ее объявления.
/ / Referen ceError : х не определена
l e t х = 3 ; / / мы никогда не дойдем сюда - ошибка остановит выполнение программы

х;

К переменным, объявленным с ключевым словом var, напротив, можно обра­
щаться прежде, чем они будут объявлены.
/ / undefined

х;

var х = 3 ;
х;

1 54

// 3

Гл ава 7 . Область ви д имости

Так что же здесь происходит? На первый взгляд, не имеет никакого смысла обра­
щаться к переменной до ее объявления. Однако к переменным, объявленным с клю­
чевым словом var, применяется механизм подъема (hoisting). JavaScript просматри­
вает всю область видимости (функции или глобальную) и поднимает к ее вершине
все переменные, объявленные с ключевым словом var. Важно понимать, что подни­
маются только объявления, а не присвоения. Таким образом, JavaScript интерпрети­
ровал бы предыдущий пример так.
var х ;
х;
х = 3;
х;

/ / поднято объявление (но н е присвоение)
/ / undefined

11 3

Давайте рассмотрим более сложный пример наряду со способом, которым
JavaScript его интерпретирует.
/ / что вы пишете

i f ( x ! == 3 ) {
console . log ( y ) ;
var у = 5 ;
i f ( y === 5 ) {
var х = 3 ;

/ / как JavaScript интерпретирует это
var х ;
var у ;
i f ( x ! == 3 ) {
console . log ( y ) ;
у = 5;
if (y
5)
х = 3;
===

console . log ( y ) ;

console . log ( у ) ;

i f ( х === 3 ) {
console . lo g ( y ) ;

i f ( х === 3 ) {
console . lo g ( y) ;

Я не утверждаю, что это хорошо написанный код на JavaScript. Не нужно исполь­
зовать переменные прежде, чем вы их объявите. Это ведет к ошибкам и не имеет
никакого практического смысла. Но в данном примере действительно поясняется,
как работает механизм подъема.
Еще один аспект переменных, объявленных с ключевым словом var, - движок
JavaScript не обращает внимание на их повторное объявление.
/ / что вы пишете
var х = 3 ;
i f ( х === 3 )
var х = 2 ;
console . log ( x ) ;
conso l e . log ( x ) ;

/ / как JavaScript интерпретирует это
var х ;
х = 3;
i f ( х === 3 ) {
х = 2;
console . log ( x ) ;
console . log ( x ) ;

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

155

переменных. Поэтому в данном случае маскировки переменных не происходит так, как
это делается с помощью ключевого слова let. В данном примере есть только одна пе­
ременная х, даже при том что в блоке есть второе определение с ключевым словом var.
Это снова то, чего я не рекомендую делать во избежание возможных ошибок.
Случайный читатель (особенно знакомый с другими языками программирования)
может посмотреть на этот пример и резонно предположить, что автор намеревался
создать новую переменную х в области видимости блока, созданного оператором i f,
чего на самом деле не было.
Если вы задались вопросом "Почему ключевое слово var позволяет делать такие
запутанные и бесполезные вещи?': то вы теперь понимаете, почему появилось клю­
чевое слово let. Конечно, вы можете и дальше использовать ключевое слово var от­
ветственно и однозначно, но при этом случайно можно очень легко написать код,
который окажется двусмысленным и неясным. В спецификации ЕSб не могли просто
"исправить" ключевое слово var, поскольку это нарушило бы работоспособность су­
ществующего кода; поэтому было введено ключевое слово let.
Я не могу придумать пример испопьзования ключевого слова var, который нельзя
было бы переписать лучше или яснее с использованием ключевого слова let . Други­
ми словами, ключевое слово var не имеет преимуществ перед ключевым словом let,
и многие в сообществе JavaScript (включая меня самого) полагают, что ключевое сло­
во let в конечном счете полностью заменит ключевое слово var (и даже возможно,
что определения с ключевым словом var в конечном счете устареют).
Итак, зачем же нужно изучать ключевое слово var и механизм подъема? По двум
причинам. Во-первых, спецификация ЕSб не будет общепринята еще на протяжении
некоторого времени, а значит, код придется транскомпилировать в ESS, и, конечно,
существует много кода, написанного в ESS. Поэтому в течение некоторого времени
еще будет важно понимать, как работает ключевое слово var. Во-вторых, объявле­
ния функции также поднимаются, что подводит нас к следующей теме.

Подъем функци й
Подобно объявлениям переменных с использованием ключевого слова var, объ­
явления функций поднимаются к началу их области видимости. Это позволяет вы­
зывать функции прежде, чем они будут объявлены.
f() ;
functi on f ( ) {
consol e . log ( ' f ' ) ;

1 1 выводит "["

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

Глава 7 . Область видимости

f() ;
let f = function ( ) {
console . log ( ' f ' ) ;

1 1 TypeError : f

-

не функция

В ременная мертвая зона
Временная мертвая зона (Temporal Dead Zone - TDZ) - это образное название
для интуитивно понятной концепции, согласно которой переменные, объявляемые
с ключевым словом let, не существуют в коде до момента их объявления. Для пере­
менной к временной мертвой зоне в пределах области видимости относится тот код,
который предшествует ее объявлению.
По большей части это не должно вызывать недопонимания или проблем, но есть
один аспект TDZ, который собьет с толку людей, знакомых с JavaScript до ЕSб.
Оператор t ypeof общепринят для определения, была ли переменная объявлена,
и считается "безопасным" способом проверки ее существования. Таким образом,
до появления ключевого слова let в пределах TDZ, это всегда безопасно срабатыва­
ло для любого идентификатора х и не заканчивалось ошибкой.
if ( typeof х "'== " undefined " ) {
console . log ( " x не суще ствуе т или равен unde fined " ) ;
else {
1 1 безопа сное обращение к х . . . .

При объявлении переменных с ключевым словом l e t этот код больше нельзя
считать безопасным. Например, следующий код закончится ошибкой.
if ( t ypeof х "'== "unde fined " ) {
console . log ( " x не существует или равен unde fined " ) ;
else {
/ ! безопасное обращение к х . . . .
let

х

=

5;

Проверка определенности переменных с использованием typeof в ЕSб будет ме­
нее необходима, поэтому на практике поведение оператора typeof во временной
мертвой зоне не должно вызывать проблем.

Строги й режим
Синтаксис ESS допускал неявные глобальные переменные (implicit global), которые
были источником многих ошибок в программе. Короче говоря, если вы забывали
объявить переменную с ключевым словом var, то JavaScript беззаботно подразуме­
вал, что вы обращаетесь к глобальной переменной. Если до этого никакой такой
В ременная мертвая зона

1 57

глобальной переменной не существовало, то она тут же создавалась! Можете пред­
ставить себе проблемы, к которым это приводило.
По этой причине (и ряду других) в JavaScript была введена концепция строгого
режима (strict mode), предотвращающего неявные глобальные переменные. Строгий
режим включается с помощью строкового литерала "use strict" (здесь вы можете
использовать одиночные или двойные кавычки), расположенного в отдельной стро­
ке, перед любым другим кодом. Если сделать это в глобальной области видимости,
весь сценарий будет выполняться в строгом режиме, а если сделать это в функции,
то в строгом режиме будет выполняться только функция.
Поскольку строгий режим относится ко всему сценарию, если перейти к нему
в глобальной области видимости, то могут возникнуть проблемы. На многих совре­
менных веб-сайтах используются вместе различные сценарии, написанные разными
людьми. Поэтому переход в строгий режим в глобальной области видимости в од­
ном из таких сценариев переводит в строгий режим их все. Хотя было бы, конечно,
хорошо, чтобы все сценарии работали правильно в строгом режиме, но это далеко
не так. Значит, обычно нецелесообразно использовать строгий режим в глобальной
области видимости. Если вы не хотите включать строгий режим в каждой функ­
ции по отдельности (и кто бы это захотел делать?), можете заключить весь свой код
в одну немедленно выполняемую функцию (больше об этом мы узнаем в главе 1 3).
( function ( ) {
' use s t r i ct ' ;
//
//
//
//
11
}) ();

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

Строгий режим считается хорошей вещью, и я рекомендую его использовать. Если
вы будете использовать анализатор (что обязательно нужно делать!), то это предот­
вратит большинство распространенных проблем, но страховка никогда не помешает!
Чтобы узнать больше о строгом режиме, читайте соответствующую статью в биб­
лиотеке MDN.

Закл юч ение
Знать области видимости важно для изучения любого языка программирования.
Введение ключевого слова let приводит JavaScript в соответствие с большинством
других современных языков. Хотя JavaScript - не первый язык, который поддер­
живает замкнутые выражения, это один из первых популярных (не академических)
языков, сделавших это. В сообществе JavaScript замкнутые выражения используются
для пущего эффекта, но это важная часть современной разработки JavaScript.
1 58

Глава 7 . Область вид имости

ГЛАВА 8

М асси в ы и их о б р а б от ка

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

Обзор массивов
Прежде чем продолжить, давайте вспомним об основах массивов. Массивы (в
отличие от объектов) имеют упорядоченный характер, а числовые индексы их эле­
ментов отсчитываются от нуля. Массивы в JavaScript могут быть негомогенными, т.е.
их элементы не обязаны иметь одинаковый тип (из этого следует, что элементами
массивов могут быть друrие массивы или объекты). Литеральные массивы создаются
с помощью квадратных скобок, и те же квадратные скобки используются для досту­
па к элементам массива по индексу. Каждый массив имеет свойство length, указы­
вающее количество элементов в массиве. Присвоение значения по индексу, превос­
ходящему размер массива, автоматически приводит к увеличению массива, а неис­
пользуемые индексы получают значение unde f ined. Для создания массива можно
также использовать конструктор Array, хотя это редко необходимо. Удостоверьтесь,
что все нижеследующее вам понятно, прежде чем переходить далее.
/ / литеральные ма ссивы
// ма ссив чисел
const arrl
[ 1 , 2, 3 ] ;
const arr2
/ / негомогенный ма ссив
[ " one " , 2, " three " ] ;
const arr3
[ [ 1, 2, 3] , [ "one " , 2 , " thre e " ] ] ; / / ма ссив , содержащий
1 1 массивы
/ / негомогенный ма ссив
const arr4
{ name : " Fred " , type : " ob j ect " , luckyNumЬers = [ 5 , 7 , 1 3 ] } ,
[
name : " Susan " , t ype : " ob j ect " } ,
name : "Anthony" , t ype : " ob j e ct " } ,
]'
1,

funct ion ( )

{ return " в элементе массива может также находиться и функция " ;

},
" three " ,
];
/ / доступ к элементам
arrl [ O ] ;
arr1 [ 2 ] ;
arrЗ [ l ] ;
arr4 [ 1 ] [ О ] ;

11
11
11
//

/ / длина ма ссива
arr l . l ength ;
arr4 . lengt h ;
arr4 [ 1 ] . length ;

11 3
11 5
11 2

/ / увеличение размера массива
arr 1 [ 4 ] = 5 ;
arr l ;
arrl . lengt h ;

/ / [ 1 , 2 , 3 , undefined, 5 ]
11 5

1
3
[ "опе " , 2 , "three "]
{ пате : "Susan " , type : "obj ect " }

/ / при доступе (не присвоении) по индексу, большему, чем есть
/ / в ма ссив е , размер ма ссива *не* изменяется
arr2 [ 1 0 ] ;
/ / undefined
arr2 . lengt h ;
11 3
/ / Конструктор Array (используется редко)
new Array ( ) ;
const arrS
1 1 пустой массив
new Array ( l , 2 , 3 ) ; 1 1 [ 1 , 2 , 3 ]
const arrб
new Array ( 2 ) ;
const arr7
1 1 массив длиной 2 (все
1 1 элементы undefined)
new Array ( " 2 " ) ;
con s t arr8
1 1 [ "2 "]

Манипули рование содержимым массива
Прежде чем продолжить, рассмотрим весьма полезные методы манипулирования
массивами. Один из аспектов обработки массивов, к сожалению, невразумительный,
относится к различию между методами, изменяющими сам массив "по месту'', и ме­
тодами, возвращающими новый массив. Никакого соглашения по этому поводу нет,
и это только один из нюансов, которые вам придется запомнить (например, метод
push изменяет сам массив, а concat - возвращает новый массив).

В некоторых языках, таких как Ruby, есть соглашения, которые облег­
чают определение, модифицирует ли метод нечто по месту или воз­
вращает копию. Например, в Ruby, если у вас есть строка str и вы
вызываете метод s t r . downcase, он возвратит литерал в нижнем
1 60

Глава 8. Массивы и их обработка

регистре, но сама s t r останется неизменной. С другой стороны, если
вы вызываете s t r . downcase ! , то это изменит саму строку s t r . Тот
факт, что стандартные библиотеки JavaScript не предоставляют ин­
формации о том, какие методы возвращают копию, а какие модифи­
цируют источник, по моему мнению, является одним из недостатков
языка, требующего ненужного запоминания.

Д о б авление отдельны х элементов
в начало или конец и и х удаление
Обращаясь к началу массива, м ы обращаемся к его первому элементу (элемен­
ту О). Аналогично конец массива является элементом с наибольшим индексом (точ­
нее элементом arr . length-1 массива arr) . Методы push и рор добавляют элементы
в конец массива (по месту) и удаляют их соответственно. Методы shi ft и unshift
добавляют элементы в начало массива (по месту) и удаляют их соответственно.
Названия этих методов происходят от терминов из информатики. По­
мещение (push) и извлечение (рор) - это действия со стеком (stack),
в котором первым извлекается элемент, добавленный последним. Ме­
тоды shift и unshift обрабатывают массив как очередь (queue), в которой первым извлекается элемент, добавленный в очередь первым.
Методы push и unshift возвращают новую длину массива после добавления но­
вого элемента, а рор и shift возвращают удаленный элемент. Вот примеры этих ме­
тодов в действии.
const arr = [ "Ь " , " с " , " d" ] ;
arr . push ( " e " ) ;
1 1 возвраща ет
11 в озвращает
arr . рор ( ) ;
arr . unshi ft ( " a " ) ;
1 1 возвраща ет
arr . shift ( ) ;
11 в озвращает

4 ; теперь a rr [ "Ь " ,

" с " , "d " , "е "]
"е"; теперь arr [ "Ь " , "с", "d"J
4 ; теперь arr [ "а ", "Ь " , " с " , "d "J
"а "; теперь arr [ "Ь " , "с", "d "]

До б а вление нескольких элементов в конец
Метод concat добавляет в массив несколько элементов и возвращает его копию.
Если передать методу concat массивы, он разделит их и добавит их элементы в ис­
ходный массив. Рассмотрим примеры.
const arr = ( 1 , 2 , 3 ]
arr . concat ( 4 , 5 , 6 ) ;
arr . concat ( [ 4 , 5 , 6 ] )
arr . concat ( [ 4 , 5 ]
6)
arr . concat ( [ 4 , [ 5 , 6 ]
1

;
1 1 возвраща ет
1 1 возвраща ет
;
1 1 в озвращает
;
] ) ; 1 1 возвраща ет

[1 ,
[1 ,
[1 ,
[1 ,

2, 3 , 4 , 5 , 6] ; a rr неизменен
2 , 3 , 4 , 5 , 6) ; arr неизменен
2 , 3 , 4 , 5 , 6) ; arr неизменен
2 , 3, 4 , [ 5 , 6] ] ; a rr неизменен

Ман и п улирование содержимым массива

161

Обратите внимание, что concat разделяет массивы, предоставленные только не­
посредственно; он не разделяет массивы в этих массивах.

Получение подмассива
Если вы хотите получить подмассив из массива, используйте метод s l ice, кото­
рому можно передать два аргумента. Первый аргумент - индекс начала подмассива,
а второй - индекс его конца (не включая указанный элемент). Если пропустить ко­
нечный аргумент, возвратятся все элементы до конца массива. Этот метод позволяет
использовать отрицательные индексы для ссылки на элементы относительно конца
массива, что весьма удобно. Рассмотрим примеры.
const arr
[1, 2, 3, 4, 5]
arr . sl i c e ( 3 ) ;
arr . s l i ce ( 2 , 4 ) ;
arr . s l i ce ( -2 ) ;
arr . s l i ce ( l , -2 ) ;
arr . s l i ce ( - 2 , - 1 ) ;
=

;
11
11
11
11
11

возвраща ет
возвраща ет
возвраща ет
возвраща ет
возвраща ет

[ 4 , 5] ; arr неизменен
[3, 4 1 ; arr неизменен
[ 4 , 5] ; arr неизменен
[2, 3] ; a rr неизменен
[ 4 1 ; arr неизменен

Д об а вление и удаление элементов в лю бой позиции
Метод s p l i c e позволяет изменять массив по месту, добавляя и/или удаляя эле­
менты из любого индекса. Первый аргумент - индекс, с которого должно начинать­
ся изменение; второй аргумент - количество удаляемых элементов (если вы не хо­
тите удалять элементы, используйте О), а остальные аргументы - это добавляемые
элементы. Рассмотрим примеры.
const arr = [ 1 5 , 7 ] ;
arr . splice ( l , О , 2 , 3 , 4 ) ;
arr . splice ( 5 , о , 6 ) ;
arr . splice ( l , 2 ) ;
arr . splice ( 2 , 1 , , а
'Ь' ) ;
/

1 ,

11
11
11
11

возвраща ет
возвраща ет
возвраща ет
возвраща ет

[ ] ; теперь arr [ 1 , 2, 3, 4 , 5, 7)
[ } ; теперь a rr [ 1 , 2, 3, 4, 5, 6, 7]
[2, 3] ; теперь a rr [ 1 , 4 , 5, 6, 7]
[ 5 ] ; теперь arr [ 1 , 4,

'а ' ,

'Ь ' , 6, 7]

К опирование и вставка в предела х массива
Спецификация ЕSб представляет новый метод, copyWi thin, получающий после­
довательность элементов из массива, и копирующий по месту, в другую часть масси­
ва, переписывая любые находящиеся там элементы. Первый аргумент - откуда ко­
пировать, второй аргумент - куда копировать, а заключительный (необязательный)
аргумент - где прекратить копирование. Как и в методе s l ice, вы можете исполь­
зовать отрицательные числа для индексов начала и завершения; они рассчитываются
от конца массива. Рассмотрим примеры.
const arr = [ 1 , 2 , 3 , 4 ] ;
arr . copyWithin ( l , 2 ) ;

1 62

/ / теперь arr [1 , 3 , 4 , 4 1

Глава 8. Массивы и их обработка

1 1 теперь arr [ 1 , 3, 1 , 3 }
arr . copyWithin ( 2 , О , 2 ) ;
arr . copyWithin ( O , - 3 , - 1 ) ; 1 1 теперь a r r [3, 1 , 1 , 3 }

Заполнение массива заданн ы м значением
Спецификация ЕSб вводит новый метод, f i l l, который позволяет задать любое
количество элементов с фиксированным значением (по месту). Он особенно полезен,
когда используется вместе с конструктором Array (который позволяет определить
начальный размер массива). Вы можете произвольно задать начальный и конечный
индексы, если хотите заполнить только часть массива (отрицательные индексы рабо­
тают как обычно). Рассмотрим примеры.
const arr = new Array ( 5 ) . fill ( l ) ; 1 1 arr инициализируется [ 1 , 1 , 1 , 1 , 1 }
arr . f ill ( " a " ) ;
1 1 теперь arr [ "а " ' "а " , "а " ' "а " ' "а " }
arr . fill ( "b " , 1 ) ;
1 1 теперь arr [ "а ", "Ь " , "Ь " ' "Ь " , "Ь " }
arr . fill ( " c " , 2 , 4 ) ;
1 1 теперь arr [ "а "' "Ь " , ''с " , " с " ' "Ь " }
arr . fill ( 5 . 5 , - 4 ) ;
1 1 теперь arr [ "а "' 5 . 5 , 5 . 5 , 5 . 5, 5 . 5}
arr . fill ( O , - 3 , - 1 ) ;
1 1 теперь arr [ "а "' 5 . 5 , О , о , 5 . 5]

Об ращение и сортировка массивов
Метод reve rse прост, он изменяет порядок элементов массива на обратный (по
месту).
const arr = [ 1 , 2 , 3 , 4 , 5 ] ;
arr . reverse ( ) ;
1 1 теперь arr [5, 4 , 3 , 2 , 1 }

Метод sort сортирует массив (по месту).
const arr

=

arr . sort ( ) ;

[5, 3 , 2 , 4 , 1 ] ;
1 1 теперь arr [ 1 , 2, 3 , 4 , 5 }

Метод sort позволяет также определить функцию сортировки (sort function),
которая может оказаться весьма удобной. Например, для сортировки объектов нет
однозначного способа.
name : " Jim" } ,
const arr = [ { name : " Suzanne" } ,
name : "Amanda" } ] ;
{ name : " Trevor" } ,
arr . sort ( ) ;
1 1 arr неизменен
arr . s ort ( ( a , Ь ) => a . name > b . name ) ;
1 1 arr отсортирован в
1 1 алфавитном порядке ПО
1 1 свойству пате

arr . sort ( ( a , Ь) => a . name [ l ] < b . name [ l ] ) ; 1 1 arr отсортирован в обратном
1 1 алфавитноМУ порядке по
1 1 ВТОРОМУ СИМВОЛУ В
1 1 свойстве пате

Мани п улирование содержимым массива

1 63

В данном примере мы возвращаем логическое значение. Но метод
sort понимает также и число в виде возвращаемого значения. Если
вы возвратите О, то метод sort будет полагать, что эти два элемен­
та "равны'', и оставит порядок неизменным. Это позволило бы нам,
например, сортировать в алфавитном порядке, за исключением слов,
начинающихся с символа k. При этом все было бы отсортировано
в алфавитном порядке, со всеми словами k, расположенными после
всех слов j и перед всеми словами l, но слова k будут в их исходном
порядке (т.е. несортированными).

Поиск в массиве
Если вы хотите найти что-то в массиве, у вас есть несколько возможностей. Нач­
нем со скромного метода indexOf, который был доступен в JavaScript довольно дав­
но. Метод indexOf просто возвращает индекс первого найденного элемента, строго
равного искомому (есть соответствующий метод lastindexOf, осуществляющий по­
иск в обратном направлении и возвращающий последний индекс, который соответ­
ствует искомому). Чтобы выполнять поиск только в части массива, можно опреде­
лить необязательный индекс начала. Если indexOf (или last indexOf) возвращает -1,
это означает, что соответствие не найдено.
coпst о = { паше : 11 Jerry 11 } ;
coпst arr = [ 1 , 5 , 11 а 11 , о , true ,
arr . indexOf ( 5 ) ;
arr . l a s t indexOf ( 5 ) ;
arr . iпdexO f ( 11 a 11 ) ;
arr . l a s t indexOf ( 11 a 11 ) ;
arr . indexOf ( { паше : 11 Jerry 11 } ) ;
arr . indexOf ( o ) ;
arr . indexOf ( [ 1 , 2 ] ) ;
arr . iпdexO f ( 11 9 11 ) ;
arr . iпdexOf ( 9 ) ;
arr . iпdexOf ( 11 a 11 , 5 ) ;
arr . iпdex0f ( 5 , 5 ) ;
arr . l a s t iпdexOf ( 5 , 4 ) ;
arr . l a s t iпdexOf ( true , 3 ) ;

5,

[ 1 , 2 ] , 11 9 11 ] ;

1 1 возвращает 1
1 1 возвращает 5
/ / возвраща ет 2

//
11
//
11
11
11
//
11
//
11

в озвраща ет
в озвраща ет
возвраща ет
возвраща ет
возвраща ет
возвращает
возвращает
возвращает
возвращает
возвращает

2
-1
3
-1
7

-1
-1
5
1
-1

Далее, метод f indindex подобен методу indexOf в том, что возвращает индекс
(или - 1 при отсутствии соответствия), но более гибко. Он позволяет задать функ­
цию, которая определяет, является ли элемент соответствующим ( f indindex не мо­
жет начать работу с произвольного индекса и не имеет аналога lastindexOf).
coпst arr = [ { id : 5 , паше : 11 Judith 11 } , { i d : 7 , паше : 11 Francis 11 ) ] ;
arr . findindex ( o => o . id === 5 ) ;
/ / возвращает О
a rr . findindex ( o = > о . паше === 11 Fraпcis 11 ) ; / / возвращает 1

1 64

Глава 8. Массивы и их обработка

arr . findindex ( o
arr . findindex ( o

=
=

> о === 3 ) ;
> o . id === 1 7 ) ;

1 1 возвраща ет -1
// возвраща ет -1

Методы find и findi ndex применяются при поиске индекса элемента. Но что
если индекс элемента не интересен, а нужен только сам элемент? Метод find похож
на findindex тем, что позволяет определять функцию для поиска, но возвращает сам
элемент, а не индекс (или nul l, если элемент не был найден).
const arr = [ { i d : 5 , name : " Judit h " } , { i d : 7 , name : " Franc i s " } ] ;
5 ) ; / / возвращает объект ( id: 5 , пате : "Judi th "
arr . find ( o = > o . id
arr . find ( o > o . id === 2 ) ; / / возвраща ет null
=

Функции, которые вы передаете методам find и findindex, получают, кроме каж­
дого элемента в их первом аргументе, также индекс текущего элемента и весь сам
массив в качестве аргументов. Это позволяет осуществлять, например, поиск квадра­
тов чисел, соответствующих определенным индексам.
const arr = [ 1 , 1 7 , 1 6 , 5 , 4 , 1 6 , 1 0 , 3 , 4 9 ] ;
arr . find ( ( х , i ) => i > 2 && NurnЬe r . i s i nteger ( Ma t h . sqrt ( x ) ) ) ; / / возвраща ет 4

Методы find и findindex позволяют также использовать переменную this во
время вызова функции. Это может быть очень удобно, если вам нужно вызвать
функцию, как будто она является методом объекта. Рассмотрим следующие эквива­
лентные методики поиска объекта Person по идентификатору.
class Person {
constructor ( name )
t hi s . name = name ;
this . id = Person . next id++ ;

Person . next i d
О;
new Person ( " Jamie " ) ,
const j amie
j ul i e t
new Person ( " Ju l ie t " ) ,
new Person ( " Peter" ) ,
peter
new Person ( " Jay " ) ;
j ay
const arr = [ j amie , j ul i e t , peter, j ay ] ;
=

/ / возможность 1 : прямое сравнение идентифика тора :
/ / возвраща ет объект j ul i e t
arr . find ( p = > p . id
j ul i e t . id ) ;
===

/ / в озможность 2 : использование аргумента "this " :
arr . find ( p = > p . id
this . id , j ul i e t ) ; / / возвраща ет объект j ul i e t
===

Вы, вероятно, сейчас найдете не много поводов для определения значения thi s
в функциях find и findindex, но впоследствии вы увидите, где эта методика весьма
полезна.

Поиск в массиве

1 65

Подобно тому, как нас не всегда заботит индекс элемента в пределах массива, сам
элемент нас тоже не всегда интересует: иногда мы просто хотим знать, есть он или
нет. Очевидно, мы можем использовать одну из приведенных выше функций и выяс­
нить, возвращает ли она - 1 или null, но в JavaScript есть для этого два метода: some
и every.
Метод some возвращает t rue, если находит элемент, который соответствует кри­
терию (это все, что нужно, дальнейший поиск сразу прекращается) , и fa l s e в про­
тивном случае. Вот пример.
const arr = [ 5 , 7 , 1 2 , 1 5 , 1 7 ] ;
arr . some ( x => х% 2===0 ) ;
/ / true; 1 2 четно
arr . some ( x => NumЬer . i s integer ( Math . sqrt ( x ) ) ) ; // fal s e ; нет квадратов

Метод eve ry возвращает t rue, если каждый элемент в массиве удовлетворяет
критерию, и false в противном случае. Он прекращает поиск и возвращает false,
как только найдет элемент, не соответствующий критерию; в противном случае он
должен будет просмотреть весь массив.
const arr = [ 4 , 6, 1 6 , 3 6 ] ;
// true; нет нечетных чисел
arr . every ( x => х % 2 === 0 ) ;
arr . every ( x => NumЬer . i s i nteger ( Math . sqrt ( x ) ) ) ; / / fa lse; 6 - не квадрат

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

Фундаментаn ьные операции
над массивом : map и f i l ter
Из всех операций над массивом map и f i l t e r вы найдете самыми полезными.
Просто удивительно, чего можно достичь с помощью этих двух методов.
Метод map преобразует элементы в массиве. Во что? Это вам решать. У вас есть
объекты, которые содержат числа, а вам нужны именно сами числа? Легко! Ваш мас­
сив содержит функции, а нужны возвращаемые ими значения? Легко! Всякий раз,
когда массив находится в одном формате, а необходим другой, используйте метод
map. Методы map и f i l ter возвращают копии и не изменяют исходный массив. Да­
вайте рассмотрим несколько примеров.
const
}];
const
const
const
const

1 66

cart = [ { name : "Widget " , price : 9 . 95 } ,

{ name : " Gadge t " , price : 2 2 . 9 5

names = cart . map ( x => x . name ) ;
//
prices = cart . map ( x => x . price ) ;
//
discountPrices = price s . map ( x => х * О . 8 ) ; //
l cNames = name s . map ( String . toLowerCa se ) ; //

Глава 8. Массивы и их обработка

[ "Widget ", "Gadget "J
[ 9 . 95 , 22 . 95 ]
[ 7 . 96, 1 8 . 3 6}
[ "widge t " , "gadget "]

Вы можете задаваться вопросом "Как работает l cNames? Этот случай выглядит
не так, как другие': Все обсуждаемые здесь методы, которым передаются функции,
включая map, не заботятся о том, в каком виде им передается эта функция. В случаях
names, prices и discountPrices мы создаем собственную функцию (используя стре­
лочную нотацию). Для l cNames мы используем функцию, которая уже существует,
String . toLowerCase. Этой функции передается один строковый аргумент, а она воз­
вращает строку в нижнем регистре. Мы легко могли бы написать names . map (х = >
х . toLowe rCase ( ) ) , но важно понимать, что функция - это функция, независимо
от того, какую форму она принимает.
Передаваемая в качестве параметра функция вызывается для каждого элемента.
Ей передаются три аргумента: сам элемент, его индекс в массиве и сам массив (ко­
торый редко полезен). Рассмотрим пример, в котором имеются наши товары и соот­
ветствующие цены в двух отдельных массивах, а мы хотим объединить их.
const i tems = [ "Widge t " , " Gadget " J ;
const prices = [ 9 . 95 , 22 . 9 5 ) ;
const cart = i t ems . map ( ( x , i ) => ( { name : х, price : prices [ i ] } ) ) ;
1 1 cart : [ ( пате : "Widge t " , price : 9 . 95 } , ( пате : "Gadge t " , price : 22 . 95 } ]

Этот пример немного сложнее, но в нем демонстрируется вся мощь функции map.
Здесь, мы используем не только сам элемент (х), но и его индекс ( i ) . Индекс необхо­
дим потому, что мы хотим соотнести элементы в массиве i tems с элементами в мас­
сиве prices согласно их индексу. Здесь метод map преобразует массив строк в массив
объектов, извлекая информацию из отдельных массивов. (Обратите внимание, что
мы должны заключить объект в круглые скобки; без круглых скобок стрелочная но­
тация примет фигурные скобки за обозначение блока.)
Метод filte r, как и подразумевает его имя, предназначен для удаления всего не­
желательного из массива. Как и map, после удаления элементов он возвращает новый
массив. Какие элементы удаляются? Это снова полностью ваше дело. Если вы догада­
лись, что для определения удаляемых элементов мы предоставляем функцию, то вы
уловили суть. Давайте рассмотрим несколько примеров.
1 1 создать колоду игральных карт
const cards = ( ] ;
for ( le t suit of [ ' Н ' , ' С ' , ' D ' , ' S ' ] ) 1 1 червы, трефы, бубны, пики
for ( le t value=l ; value c . value === 2 ) ; 1 1 [
1 1 ( sui t :
1 1 ( sui t :
1 1 ( s ui t :
1 1 ( sui t :

' Н ' ' val u e : 2 } '
'С '
' val u e : 2 } '
'D , ' val u e : 2 } '

, s , , val u e : 2 }

Фун дамен тальные о п ера ц ии н ад массивом : map и filter

1 67

11 ]

/ / (далее для краткости

мы

/ / получить все бубны:
cards . fi l t e r ( c => c . suit

выводим только длину)

'D' ) ;

1 1 длина : 1 3

/ / получить все фигуры
cards . fi l t e r ( c => c . va lue > 1 0 ) ;
/ / получить все червовые фигуры
cards . fi l t e r ( c = > c . value > 1 0 & & c . suit

1 1 длина : 1 2

===

' Н ' ) ; / / длина : 3

Полагаю, вы начали понимать, как методы map и filter могут быть объединены
для получения интересного эффекта. Скажем, например, мы хотим создать краткое
строковое представление карт в нашей колоде. Мы будем использовать символы
Юникода для мастей и буквы "Р\.', "J", "Q" и "К" для обозначения туза и фигур. По­
скольку создающая функция довольно длинна, мы создадим ее отдельно и не будем
пытаться использовать анонимную функцию.
function cardToString ( c )
const suits = { ' Н ' : ' \u 2 6 6 5 ' , ' С ' : ' \u2 6 63 ' , ' D ' : ' \u2 6 66 ' , ' S ' : ' \u2 6 6 0 '
};
const values = { 1 : ' А ' , 1 1 : ' J ' , 1 2 : ' Q ' , 1 3 : ' К ' } ;
/ / создание ма ссива значений при каждом вызове функции cardToString
/ / не очень эффективно; попробуйте найти лучшее решение
for ( le t i=2 ; i c . va lue === 2 )
. map ( cardToString ) ;
1 1 [ «2'1 » , «2• », «2+ » , «2• » ]
/ / получить все червовые фигуры
cards . fi lter ( c => c . value > 1 0 & & c . suit === ' Н ' )
. map ( cardToString ) ;
/ / [ «J'I » , «Q'I », «K'I »

М а rия массивов : метод reduce
Из всех методов массивов мой любимый reduce. В то время как map преобразу­
ет каждый элемент в массиве, метод reduce преобразует весь массив. Он называется
reduce потому, что зачастую используется для сведения (reduce) массива к единому
значению. Например, суммирование чисел, хранящихся в массиве, или вычисление их
среднего являются способами свести массив к единому значению. Однако фактически
-

1 68

Глава 8. Массивы и их обработка

результатом сведения к единому значению может быть объект или другой массив метод reduce способен воспроизвести возможности функций map и f i l t e r (и если
на то пошло, любой другой рассмотренной здесь функции массива).
Метод reduce, подобно map и fi lter, позволяет предоставить функцию, которая
контролирует результат. Прежде мы уже имели дело с функциями обратного вызова
(callback), первый переданный им элемент всегда является текущим элементом мас­
сива. Однако первое значение функции reduce - аккумулятор (accumulator), в ко­
торый сводится массив. Остальная часть аргументов вполне ожидаема: текущий эле­
мент массива, текущий индекс и сам массив.
Помимо функции обратного вызова, методу reduce передается (необязательно)
начальное значение для аккумулятора. Давайте рассмотрим простой пример - сум­
мирование чисел в массиве.
const arr = [ 5 , 7 , 2 , 4 ] ;
const sum = arr . reduce ( ( a , х ) => а += х , 0 ) ;

Передаваемая в reduce функция получает два параметра: аккумулятор (а) и те­
кущий элемент массива (х). В этом примере аккумулятор изначально содержит зна­
чение О. Поскольку это наш первый опыт с reduce, рассмотрим все этапы, которые
проходит JavaScript, чтобы лучше понять, как это работает.
1 . Для первого элемента массива (5) вызывается (анонимная) функция. Первона­
чальна, а имеет значение О, а х - значение 5. Функция возвращает сумму а и х
(5), что становится значением а на следующем этапе.
2. Функция вызывается для второго элемента массива (7). Теперь а имеет значение

5 (переданное с предыдущего этапа), а х имеет значение 7 . Функция возвращает
сумму а и х (12), которая становится значением а на следующем этапе.
3. Функция вызывается для третьего элемента массива (2). Теперь а имеет значе­
ние 12, а х - значение 2. Функция возвращает сумму а и х ( 1 4 ) .
4. Функция вызывается для четвертого, и последнего, элемента массива ( 4 ). Те­
перь а имеет. значение 1 4 , а х - значение 4 . Функция возвращает сумму а и х
( 1 8 ) , которая и будет возвращаемым значением функции reduce (которое за­

тем присваивается константе sum) .
Проницательный читатель уже мог бы понять, что в этом очень простом примере
мы даже не должны присваивать значение а; важнее всего то, что возвращается из
функции (помните, что стрелочная нотация не требует явного оператора r e turn ) ,
таким образом, мы можем просто возвращать а + х. Однако в более сложных при­
мерах нам может понадобиться сделать с аккумулятором нечто большее. Таким об­
разом, модификация аккумулятора в функции - это хорошая привычка.

Магия массивов : метод reduce

1 69

Прежде чем мы перейдем к более интересным случаям применения метода
reduce, давайте рассмотрим, что будет, если аккумулятору не присваивается на­
чальное значение, т.е. он равен unde f i ned. Тогда метод reduce считает первый

элемент массива в качестве начального значения и начинает вызывать функцию
со вторым элементом. Давайте вернемся к нашему примеру, но опустим начальное
значение.
const arr
const sum

[ 5 , 7, 2 , 4 ) ;
arr . reduce ( ( а , х ) => а += х ) ;

1 . Для второго элемента массива ( 7 ) вызывается (анонимная) функция. У а те­
перь начальное значение 5 (первый элемент массива), а х содержит значение 7 .
Функция возвращает сумму а и х ( 1 2 ), что становится значением а на следу­
ющем этапе.
2. Функция вызывается для третьего элемента массива (2). Теперь а имеет на­
чальное значение 12, а х - значение 2. Функция возвращает сумму а и х ( 1 4 ).

3. Функция вызывается для четвертого, и последнего, элемента массива ( 4 ). Те­
перь а имеет значение 1 4, а х - значение 4 . Функция возвращает сумму а и х
( 1 8 ) , которая и становится возвращаемым значением reduce (оно затем при­
сваивается константе sum) .
Как можно заметить, здесь на один этап меньше, но результат то же. В этом при­
мере (и в любом другом случае, когда первый элемент может служить начальным
значением аккумулятора) мы можем извлечь пользу, исключив начальное значение.
Обычно для метода reduce в качестве аккумулятора используется значение ба­
зового типа (число или строка), но использование для аккумулятора объекта - это
очень мощный подход (о котором часто забывают). Например, если у вас есть мас­
сив строк и вы хотите сгруппировать строки в упорядоченные по алфавиту массивы
(слова на А, слова на Б и т.д.), можете использовать объект.
const words = [ "Beachba l l " , "Rodeo" , "Ange l " ,
"Aardvar k " , "Xylophone " , "NovemЬer " , "Chocolate " ,
" Рарауа " , "Uni form" , "Joker" , "Clover" , "Bal i " ] ;
const alphabetical = words . reduce ( ( a , х ) => {
if ( ! a [x [O ] J ) a [x [ OJ J = [ ] ;
а [х [ 0 ] ] . push ( х ) ;
return а ; } , { } ) ;

Этот пример немного сложнее, но принцип тот же. Для каждого элемента
в массивефункция проверяет аккумулятор на наличие у него свойства для пер­
вой буквы в слове; если его нет, она добавляет пустой массив (когда она встре­
чает " Be a chba l l " и никакого свойства а . В нет, она создает для него пустой мас­
сив). Затем она добавляет слово в соответствующий массив (который, возможно,
был только что создан) , и наконец аккумулятор (а ) возвращается (помните, что
1 70

Глава 8. Массивы и их обработка

значение, которое вы возвращаете, используется как аккумулятор для следующего
элемента в массиве).
Другой пример - вычислительная статистика. Давайте, например, вычислим
среднее и дисперсию для набора данных.
con s t data = [ 3 . 3 , 5 , 7 . 2 , 1 2 , 4 , 6 , 1 0 . 3 ] ;
11 Алгоритм Дональда Кнута для вычисления дисперсии : Искусство
11 программирования, том 2 . Получисленные алгоритмы, 3 -е изд . 2000 год
cons t stats
dat a . reduce ( ( a , х ) => {
a . N++;
let de l t a = х
a . me a n ;
a . mean + = delta / a . N ;
а . М2 + = del t a * ( x - a . me an ) ;
return а ;
} , { N : О , mean : О , М 2 : О } ) ;
i f ( st a t s . N > 2 ) {
stats . variance = s t at s . M2 / ( st a t s . N
l) ;
stat s . stdev = Math . s qrt ( st a t s . variance ) ;
=

-

-

И снова мы исцользуем объект как аккумулятор, поскольку необходимо несколь­
ко переменных (в частности - mean и М2: при желании вместо N мы могли бы ис­
пользовать индексный аргумент минус один).
Давайте рассмотрим еще один пример, демонстрирующий гибкость метода reduce
при использовании аккумулятора, тип которого мы еще не применяли, - строки.
cons t words = [ "Beachbal l " , " Rodeo " , "Ange l " ,
"Aardvark" , "Xylophone" , "NovemЬer " , "Chocol a t e " ,
" Рара уа " , " Uni form" , " Jo ke r " , " Clove r " , "Bali " ] ;
const longWords = words . reduce ( ( а , w } => w . length>б ? а + " " +w : а , " " } . trim ( } ;
11 longWords : "Bea chba l l Aardvark Xyl ophone November Chocol a t e Uniform"

Здесь мы используем строковый аккумулятор для получения единой строки, со­
держащей все слова, которые состоят более чем из шести символов. В качестве са­
мостоятельного упражнения попробуйте переписать его, используя вместо reduce
методы filter и j oin (строковый метод). (Начните с ответа на вопрос "Почему не­
обходимо вызвать trim после reduce?")
Надеюсь, вам понравилась мощь метода reduce. Из всех методов обработки мас­
сива этот является наиболее универсальным и мощным.

М етоды массива и удаленные
или е ще не определенные элементы
Зачастую недопонимание поведения методов массива ведет к неправильным
предположениям относительно обработки ими элементов, которые были удалены
М етоды массива и уд аленные и л и е ще не о п редел енные эл ементы

171

или еще не были определены. Методы map, filter и reduce не вызывают функцию
для элементов, которые никогда не присваивались или были удалены. Например,
до ЕSб, если бы вы попытались хитро инициализировать массив таким способом, то
были бы разочарованы.
const arr = Array ( l O ) . map ( functi on ( x )

{

return 5 } ) ;

Массив arr был бы массивом с 1 0 элементами, но все они содержали бы значение
undefined. Точно так же, если вы удалите элемент из середины массива, а затем вы­
зовете метод map, то получите массив с "дыркой':
cons t arr = [ 1 , 2 , 3 , 4 , 5 ] ;
del e t e arr [ 2 ] ;
// [ О , О, , О , О]
arr . map ( x => О ) ;

На практике эта проблема возникает редко, поскольку обычно вы работаете
с массивами, элементы которых заданы явно (а если вам специально понадобил­
ся промежуток в массиве, что бывает редко, вы не будете вызывать метод delete
для всего массива), но знать об этом нужно.

Соединение строк
Довольно часто приходится объединять (строковые) значения элементов масси­
ва, используя некий разделитель. Функция Arra у . protot уре . j oin получает один ар­
гумент, разделитель (стандартно - запятая, если вы его не укажете), и возвращает
строку с объединенными элементами (включая еще не определенные и удаленные
элементы в виде пустых строк; значения null и unde fined также становятся пусты­
ми строками).
con s t arr = [ 1 , null , "hel l o " , "world" , true , undefine d ] ;
del e t e arr [ З ] ;
11 "1 , , hello, , true, "
arr . j oi n ( ) ;
/ / "lhellotrue "
arr . j oin ( ' ' ) ;
"
arr . j oin ( ' -- ' ) ; / / "1 -- -- hello -- -- true

При грамотном использовании (совместно с объединением и конкатенацией
строк) функция Array . prototype . j oin позволяет создать такие элементы, как спис­
ки HTML .
con s t attribute s = [ "NimЫe " , " Perceptive " , " Generous " J ;
cons t html = ' ' + a t t r ibut es . j oin ( ' < / l i> ' ) + ' < / l i> ' ;
/ / h tml : "NimЫePercep t i veGenerous " ;

Будьте внимательны, не поступайте так с пустым массивом: вы получите один
пустой элемент !

1 72

Глава 8. Массивы и их обработка

З акл юч ение
Встроенный класс JavaScript Array обладает большой мощью и гибкостью, но
иногда может быть не до конца понятно, когда какой метод использовать. Возмож­
ности методов класса Array приведены в табл. 8. 1 -8.4.
Для методов Arra y . prot otype, которым передается функция ( find, findindex,
some, every, map, f i l t e r и reduce ) , предоставляемая функция получает аргументы,
представленные в табл. 8 . 1 , для каждого элемента в массиве.
Таблица 8.1 . Аргументы функции массива (по порядку)
Метод

Описание

Только reduce

Аккумулятор (исходное значение или значение, возвра щенное последним
вызовом)

Все

Элемент (значение текущего элемента)

Все

Индекс текущего элемента
Сам массив (редко полезен)

Все

Всем методам Array . prototype, которым передается функция, можно также пе­
редать необязательное значение переменной this, позволяющее вызывать функции,
как будто это метод.
Таблица 8.2. М анипулирование содержимым массива
Когда необходимо".

Испол ьзуйте".

По месту
или копия

Создать стек ("последним при ш ел,
первым вышел" [LIFOJ)

push (возвра щ ает новую длину), рор

По месту

Создать очередь ("первым пришел,
первым вышел" [FIFO])

unshift (возвра щает новую длину),
shift
concat

По месту

Добавить несколько элементов
в конец

Копия

s lice
spli ce

Копия

Вырезка и замена в пределах
массива

copyWithin

По месту

Заполнение массива

fill
reverse

П о месту

sort (передается функция для специальной сортировки)

По месту

Получить подмассив
Добавить или удалить элементы
в любой позиции

Обращение массива
Сортировка массива

По месту

По месту

Таблица 8.3. Поиск в массиве
Когда необходимо знать/найти".
Индекс элемента

Используйте".

indexOf (простые значения), findlndex (сложные
значения)

Последний и ндекс элемента
lastindexOf (простые значения)
f ind
Сам элемент
s
ome
Есть ли в массиве элемент, удовлетворяющ ий некому критерию
Все л и элементы в массиве удовлет- every
воряют некому критерию

Таблица 8.4. Преобразование массива
Когда необходимо".

Используйте."

По месту или копия

Преобразовать каждый элемент в массив
Удалить элементы из массива на основании неких
критериев
Преобразовать весь массив в другой тип данных
П реобразовать элементы в строки и объединить

map
f i lter

Копия
Копия

reduce
j oin

Копия
Копия

1 74

Глава 8. Массивы и их обработка

ГЛАВА 9

Объ е к ты и о б ъ е к тно ·
ориен т иро в анное
про r раммиро в ание
Основы объектов JavaScript мы рассмотрели в главе 3, а теперь пришло время из­
учить их глубже.
Как и массивы, объекты в JavaScript - это контейнеры, которые называют агре­
гатными или комплексными типами данных. У объектов есть два основных отли­
чия от массивов.


Массивы содержат значения, индексированные в числовой форме; объекты со­
держат свойства, индексированные строкой или символом.



Массивы упорядочены (элемент arr [ О ] всегда следует перед arr [ 1 ] ); объекты
не упорядочены (вы не можете гарантировать, что свойство obj а расположе­
но перед obj . Ь).
.

Эти различия носят довольно эзотерический (но важный) характер, поэтому да­
вайте считать свойства тем, что делает объекты по настоящему особенными. Свой­
ство (property) состоит из ключа (key) (строки или символа) и значения (value). Осо­
бенными объекты делает то, что вы можете обращаться к свойствам по их ключам.

Перебор свойств
Обычно, если нужно вывести содержимое некоторого контейнера (операция пе­
ребора), чаще всего используется массив, а не объект. Тем не менее объекты также
являются контейнерами, и они обеспечивают перебор свойств; вам только нужно
знать об особенностях и возможных сложностях.
Первое, что необходимо помнить о переборе свойств, - это то, что порядок
не гарантируется. Проведя небольшой эксперимент, вы можете обнаружить, что
свойства выводятся в том порядке, в котором они добавляются, и это справедли­
во для многих реализаций движка почти всегда. Тем не менее JavaScript не дает
никаких гарантий этому, и изменение реализации движка в любой момент может

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

Цикп

for .

. . in

Традиционным способом перебора свойств объекта является цикл for . . . in. Рас­
смотрим объект, у которого есть несколько строковых свойств и одно символьное
свойство.
const SYM

SymЬol ( ) ;

const о =

а : 1 , Ь : 2, с: 3,

[ SYM] : 4 } ;

for ( le t prop i n о ) {
i f ( ! o . hasOwnProperty (prop) ) cont inue ;
conso l e . log ( ' $ { prop } : $ { o [ prop] } ' ) ;

Все кажется довольно простым . . . кроме, вероятно, вполне резонного вопро­
са "Что делает hasOwnPrope rty?" Он ликвидирует опасность, связанную с циклом
for . . . in, которая станет ясна далее в этой главе. Речь идет об унаследованных свой­
ствах. В данном примере мы это могли бы опустить и не придать значения. Но, пере­
бирая свойства объектов других типов (особенно объектов, производных от других),
вы можете обнаружить свойства, которых не ожидали. Поэтому я рекомендую вы­
работать привычку использовать для проверки метод hasOwnProperty. Вы скоро уз­
наете, почему это так важно, а также научитесь определять, когда его можно безбо­
лезненно (или желательно) опустить.
Обратите внимание, что цикл for . . . in не выводит значения свойств с символь­
ными ключами.
Несмотря на то что с помощью цикла for . . . i n можно выполнить
перебор элементов массива, обычно это считается плохой идеей. Для
массивов я рекомендую использовать обычный цикл for или forEach.
�---·--_J

М етод Obj ect . keys
Метод Obj ect . keys позволяет получить все перечислимые строковые свойства
объекта в виде массива.
const SYM

SymЬol ( ) ;

cons t о =

а: 1, Ь: 2, с: 3,

[ SYM] : 4 } ;

Obj e c t . keys ( o ) . forEach ( prop => console . log ( ' $ { prop } : $ { o [ prop] } ' ) ) ;

1 76

Глава 9. Объекты и объектно-ориентированное п рограммирование

Этот пример приводит к тому же результату, что и цикл for . . . in (здесь даже не
нужно выполнять проверку с помощью метода hasOwnProperty). Это весьма удобно,
когда нужно собрать ключи свойств объекта в виде массива. Например, это облегча­
ет вывод всех свойств объекта, которые начинаются с символа х.
const о = { appl e : 1 , xochit l : 2 , balloon : 3 , guitar : 4 , xylophone : 5 ,

};

Obj ect . keys ( o )
. fi lter ( prop => prop . match ( / лx/ ) )
. forEach ( prop => console . log ( ' $ { prop } : $ { o [ prop ] } ' ) ) ;

Объектно-ориентированное программирование
Объектно -ориентированное программирование (ООП, Obj ect-Oriented
Programming) - старая добрая парадигма в информатике. Некоторые из концепций,
которые мы теперь знаем как ООП, появились еще в 1 950-х годах, но только после
появления языков программирования Simula 67 и Smalltalk они обрели форму ООП.
Фундаментальная идея проста и интуитивно понятна: объект - это логически
связанная коллекция данных и функций. Она призвана отразить наше понимание
естественного мира. Автомобиль
это объект, у которого есть данные (марка, мо­
дель, количество дверей, идентификатор транспортного средства (VIN) и т.д.), а так­
же функции (ускорение, переключение передач, открывание дверей, включение фар
и т.д.). Кроме того, ООП позволяет думать о вещах абстрактно (автомобиль) и кон­
кретно (определенный автомобиль).
Прежде чем продолжать, давайте рассмотрим базовую лексику ООП. Термин класс
(class) описывает обобщенную сущность (автомобиль), а экземпляр (instance) (или
экземпляр объекта (object instance)) - определенную сущность (конкретный автомо­
биль, такой как "мой автомобиль"). Одна часть функций (ускорение) является мето­
дами (method). Другая часть функций, связанных с классом, но не относящихся к кон­
кретному экземпляру, является методами класса (например, "создание нового VIN"
могло бы быть методом класса: это не имеет отношения к конкретному новому авто­
мобилю и, конечно, мы не ожидаем, что у конкретного автомобиля будет возможность
или способность создать новый, законный VIN). Когда экземпляр создается, выполня­
ется его конструктор (constructor). Конструктор инициализирует экземпляр объекта.
ООП предоставляет нам также среду для иерархической категоризации клас­
сов. Например, мог бы существовать более общий класс транспортного средства.
У транспортного средства может быть характеристика дальности (дистанция, кото­
рую он может пойти без дозаправки или перезарядки), но в отличие от автомобиля
у него может не быть колес (например, у такого транспортного средства, как лод­
ка, очевидно нет колес). Мы говорим, что транспортное средство - это суперкласс
(superclass) автомобиля, а автомобиль - это производный класс (subclass) транспорт­
ного средства. У класса транспортного средства может быть несколько производных
классов: автомобили, лодки, планеры, мотоциклы, велосипеды и т.д. У производных
-

Объектно-ориентированное п рограммирование

1 77

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

Создание класса и э кземпляра
До ЕSб создание классов в JavaScript было суетным и не интуитивно понятным
делом. Теперь появился новый удобный синтаксис создания классов.
class Car {
const ructor ( )
}

Это создает новый класс по имени Car. Никаких его экземпляров (конкретных
автомобилей) еще не создано, но теперь есть возможность сделать это. Чтобы соз­
дать конкретный автомобиль, мы используем ключевое слово new.
cons t carl
const car2

=

new Car ( ) ;
new Car ( ) ;

Теперь у нас есть два экземпляра класса Car. Прежде чем сделать класс Car более
сложным, давайте рассмотрим оператор instanceof, который может сказать вам, яв­
ляется ли данный объект экземпляром данного класса.
carl instanceof Car
// true
carl instanceof Array // fa lse

Из этого видно, что carl
экземпляр класса Car, а Array нет.
Давайте сделаем класс Car немного интереснее. Придадим ему некие данные (мар­
ка, модель) и некие функции (переключение передач).
-

-

class C a r {
constructor ( ma k e , mode l )
this . make = ma ke ;
this . model = mode l ;
this . u s e rGears = [ ' Р ' , ' N ' , ' R ' , ' D ' J ;
this . us erGear = thi s . us erGears [ O ] ;
shift ( ge a r ) {
i f ( this . userGears . indexOf ( ge a r ) < 0 )
throw new Еrrоr ( ' Ошибочная передача : $ { gear } ' ) ;
thi s . u s e rGear = gear;

Здесь ключевое слово this используется по прямому назначению: для обраще­
ния к экземпляру, метод которого был вызван. Вы можете считать его знакоместом:
1 78

Глава 9 . Объекты и объектно- ориентированное п рограммирование

когда вы пишете свой класс, вероятно, абстрактный, ключевое слово this является
знакоместом для конкретного экземпляра, который будет известен на момент вы­
зова метода. Этот конструктор позволяет задать марку и модель автомобиля при его
создании, а также установить некоторые стандартные значения: допустимые переда­
чи (userGears) и текущую передачу (gear), которую мы инициализируем значением
первой допустимой передачи. (Я решил назвать это пользовательскими передачами
(user gears) потому, что если этот автомобиль оснащен автоматической коробкой пе­
редач, то, когда автомобиль будет находиться в движении, фактически использующа­
яся передача может отличаться от включенной пользователем.) Кроме конструктора
(который вызывается неявно при создании нового объекта), мы также создали метод
shi ft, позволяющий переключать передачу. Давайте рассмотрим это в действии.
const carl = new Car ( " T e s la " , "Model S " ) ;
const car2 = new Car ( "Mazda " , " З i " ) ;
carl . shift ( ' D ' ) ;
car2 . shift ( ' R ' ) ;

В этом примере, когда мы вызываем carl . shi ft ( ' D ' ) , переменная this связана
с carl. Точно так же при вызове car2 . shift ( ' R ' ) она связана с car2. Мы можем
убедиться, что carl находится в движении на передаче D (drive), а car2 сдает назад
на передаче R (reverse).
> carl . userGear // "D "
> car2 . userGear / / "R "

Д инамические свойства
То, что метод shi ft нашего класса Car предотвращает выбор недопустимой пере­
дачи по небрежности, может казаться очень умным ходом. Но эта защита ограни­
чена, поскольку нет ничего, что помешало бы установить значение непосредствен­
но: carl . userGear = ' Х ' . Большинство объектно-ориентированных языков идет
на большие затраты, чтобы предоставить механизмы защиты от этого вида непра­
вильного обращения, разрешая вам определять уровень доступа к методам и свой­
ствам. В JavaScript такого механизма нет, за что его нередко и критикуют.
Динамические свойства1 способны несколько сгладить этот недостаток. Они об­
ладают семантикой свойств с функциональными возможностями методов. Давайте
изменим наш класс Car так, чтобы использовать это в своих интересах.
c l a s s Car {
constructor (make , model )
thi s . ma ke = make ;
thi s . model
model ;
[ ' Р ' , 'N' , 'R' , ' D' ] ;
thi s . _userGears
thi s . _userGear = thi s . _us erGears [ O ] ;
=

=

1 Динамические свойства было бы правильнее называть
properties), о которых мы узнаем больше в главе 2 1 .

методами доступа

к

свойствам (accessor

Объектно-ориентированное п рограммирование

1 79

get userGea r ( ) { return thi s . userGea r ;
s e t userGea r ( value ) {
i f ( this . _use rGears . indexOf ( value ) < 0 )
throw new Еrrоr ( ' Ошибочная передача : $ { value ) ' ) ;
t hi s . use rGear
value ;
=

shift ( ge a r )

{ thi s . us erGear

=

gea r ; )

Проницательный читатель заметил, что мы не устранили проблему, поскольку
значение _userGear все еще можно установить непосредственно: carl . _userGear =
' Х ' . В этом примере мы используем "ограничение доступа для бедных" - свойства,
имена которых начинаются с символа подчеркивания, мы считаем закрытыми. Эта
защита сугубо в соглашении, позволяющем быстро просмотреть код и выявить свой­
ства, к которыми вы не должны обращаться непосредственно.
Если вы действительно должны обеспечить конфиденциальность, то можете ис­
пользовать экземпляр WeakMap (см. главу 1 0), который защищен областью видимо­
сти (если мы не будем использовать WeakMap, то наши закрытые свойства никогда
не будут выходить из области видимости, даже если экземпляры, к которым они от­
носятся, выйдут). Чтобы сделать основное текущее свойство передачи действительно
закрытым, мы можем изменить свой класс Car так.
const Car

=

( function ( )

const carProps

=

{

new W e a kMap ( ) ;

c l a s s Car {
constructor ( ma ke , mode l )
thi s . ma ke = ma ke ;
t hi s . model
model ;
thi s . _userGears = [ ' Р ' , ' N ' , ' R ' , ' D ' ] ;
carProps . set ( th i s , { userGea r : t hi s . userGears [ О ] ) )
=

_

get userGear ( ) { return carProps . ge t ( th i s ) . userGear ;
s e t userGear ( value ) {
i f ( this . _userGears . indexOf ( value ) < 0 )
throw new Еrrоr ( ' Ошибочная передача : $ { value ) ' ) ;
carProps . get ( th i s ) . us erGear = value ;

shift ( ge a r )

{ thi s . userGear

gea r ; )

return C a r ;
}) ();

1 80

Гла ва 9. Объекты и объектно-ориентированное п рограммирование

;

Чтобы поместить наш WeakMap в замкнутое выражение, к которому нельзя об­
ратиться извне, мы используем немедленно вызываемое функциональное выражение
(см. главу 13). Теперь WeakМap может безопасно хранить любые свойства, к которым
мы не хотим обращаться за пределами класса.
Существует и другой способ, который подразумевает использование символов
для имен свойств; они также предоставляют некоторую защиту от случайного ис­
пользования, но к символьным свойствам класса также можно обратиться, а значит,
даже эту защиту можно обойти.

Классы как функции
До введения в ЕSб ключевого слова class для создания класса приходилось созда­
вать функцию, которая служила бы конструктором класса. Хотя синтаксис class на­
много более интуитивно понятен и прост, внутренний характер классов в JavaScript не
изменился (ключевое слово class лишь обеспечивает немного более удобный синтак­
сис), поэтому важно понимать, что именно представляет собой класс в JavaScript.
В действительности класс - это только функция. В ESS мы начали бы свой класс
Car так.
funct ion Car (ma ke , mode l )
thi s . make
ma ke ;
t h i s . model
mode l ;
t hi s . _userGears = [ ' Р ' , ' N ' , ' R ' , ' D ' ] ;
thi s . _userGear = thi s . userGears ( O J ;
=

=

Мы все еще можем сделать это и в ЕSб - результат будет тот же (до методов мы
дойдем ниже). Мы можем проверить это, опробовав оба пути.
class E s бCar { }
function E s5Car { }
> typeof E s бCar·
> t ypeof E s5Car

1 1 опустим конструктор для кра ткости
1 1 "funct ion "
1 1 "fun c tion "

Таким образом, ничего действительно нового в ЕSб нет; у нас есть только некий
новый удобный синтаксис.

Прототи п
Когда говорят о методах, доступных в экземплярах класса, имеют в виду про­
тотип (prototype) методов. Например, упоминая метод shift, доступный в экзем­
плярах класса Car, вы имеете в виду прототип метода и зачастую можете встретить
синтаксис Car . prototype . shift. (Точно так же функция forEach класса Array мо­
жет выглядеть как Arra y . prototype . forEach. ) Теперь пришло время фактически уз­
нать, что такое прототип и как JavaScript осуществляет динамический вызов ( dynamic
dispatch), используя цепь прототипов (prototype chain).
Объ е ктно - ориентированное п рограммирование

181

Использование знака диеза ( # ) стало популярным соглашением
для описания прототипов методов. Например, вы будете часто встре­
чать Car . prototype . shift, записанный просто как Car # shift.
Каждая функция имеет специальное свойство prototype. (Вы можете изменить
его для любой функции f, введя на консоли f . prototype.) Для обычных функций
прототип не используется, но он критически важен для функций, которые действуют
как конструкторы объектов.
В соответствии с соглашением имена конструкторов объектов (ина­
че - классов) всегда начинаются с заглавной буквы, например Car.
Это соглашение - не догма, но многие анализаторы предупредят вас,
если вы попытаетесь называть функцию с заглавной буквы или кон­
структор объекта - со строчной.
Свойство функции prototype становится важным, когда вы создаете новый эк­
земпляр с использованием ключевого слова new: вновь созданный объект имеет до­
ступ к свойству protot уре его конструктора. Экземпляр объекта хранит его в своем
свойстве _proto_

·

Свойство
proto
считается внутренней частью JavaScгipt, как
и любое свойство, заключенное между двойными символами подчер­
кивания. Используя эти свойства, можно сделать очень, очень много
вреда. Иногда их можно использовать очень хитро и правильно, но
пока у вас нет полного понимания JavaScript, я настоятельно рекомен­
дую только просматривать (но не изменять) эти свойства.

В прототипе важнее всего механизм динамического вызова (термин "dispatch"
это синоним вызова метода). Когда вы пытаетесь получить доступ к свойству или
методу объекта, если его не существует, JavaScript проверяет прототип объекта,
чтобы убедиться, есть ли он там. Поскольку все экземпляры данного класса совмест­
но используют один и тот же прототип, к свойству или методу, имеющемуся в про­
тотипе, есть доступ для всех экземпляров этого класса.
-

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

Глава 9. Объекты и объектно-ориентирован ное п рограммирование

1 1 определенный ранее кла сс Ca r с мет одом shift
const carl
new Car ( ) ;
const car2
new Car ( ) ;
carl . shift
Car . prototype . shift ; / / true
carl . s hift ( ' D ' ) ;
carl . s hift ( ' d ' ) ;
1 1 ошибка
carl . userGear ;
/ / 'D '
carl . shift
car2 . shift
1 1 true
carl . shift
function ( ge a r )
thi s . use rGear
gear . toUpperCas e ( ) ; }
carl . s hift
Car . prototype . shift ; / / fa lse
/ / fa lse
carl . shift
car2 . shift ;
carl . s hift ( ' d ' ) ;
carl . userGear ;
/ / 'D '
=

=

===

===

=

===

===

В этом примере ясно показано, как JavaScript осуществляет динамический вызов.
Первоначально у объекта carl нет метода shi ft, но при вызове car l . shift ( ' D ' )
JavaScript просматривает прототип для carl и находит метод с таким именем. Когда
мы заменяем метод shift собственной версией, то у объекта carl и у его прототипа
появляется метод с этим именем. Однако при вызове carl . shift ( ' d ' ) , будет вызван
метод объекта carl, а не его прототипа.
Обычно в знании механики цепи прототипов и динамического вызова у вас не
будет особой нужды, но все же может встретиться проблема, которая потребует их
глубокого понимания. Поэтому, прежде чем продолжать, имеет смысл узнать детали.

Статические методы
Методы, которые мы рассматривали до сих пор, являлись методами экземпляра
(instance method). Они предназначены для работы с конкретным экземпляром. Есть
также статические методы (static method) (или методы класса (class method)), кото­
рые не относятся ни к какому конкретному экземпляру. В статическом методе пере­
менная this привязана к самому классу, но в этом случае вместо нее рекомендуется
использовать имя класса.
Статические методы используются для выполнения обобщенных задач, которые
связаны с классом, а не с любым конкретным экземпляром. Давайте рассмотрим
пример использования VIN автомобиля (идентификатор транспортного средства).
Нет смысла позволять индивидуальному автомобилю создавать собственный VIN:
что помешало бы автомобилю использовать такой же VIN, как и у другого автомо­
биля? Однако присвоение VIN является абстрактной концепцией, которая связа­
на с идеей автомобиля вообще; следовательно, это кандидат в статические методы.
Кроме того, статические методы зачастую используются для работы с несколькими
транспортными средствами (объектами). Например, нам может понадобиться метод
areS irnilar, который возвращает t rue, если у двух автомобилей те же марка и мо­
дель, а метод areSarne, возвращающий t rue, если у двух автомобилей один и тот же
VIN. Давайте рассмотрим эти статические методы, реализованные для класса Car.
Объектно-ориентирован ное п рограммирование

1 83

class Car {
static getNextVin ( )
return Car . nextVin + + ; / / мы могли бы также использовать
/ / this . nextVin+ + , но обращение к Car
/ / подчеркивает, что это статический метод
const ructor (make , model )
thi s . ma ke = make ;
thi s . model = mode l ;
thi s . vin = Car . getNextVin ( ) ;
s t a t i c areSimilar ( car l , car 2 ) {
return carl . ma ke===car2 . ma ke && car l . model===car2 . mode l ;
s t a t i c areSame ( carl , car2 ) {
return carl . vin===car2 . vi n ;

Car . nextVin
const carl
const car2
const car3
car l . vin;
car2 . vi n ;
car3 . vin

=

О;

new Car ( " Te s la " , " S " ) ;
new Car ( "Mazda " , " 3 " ) ;
new Car ( "Ma zda " , " 3 " ) ;
// О
// 1
// 2

Car . areSimi lar ( ca r l , car2 ) ;
Car . areSimil a r ( car2 , car3 ) ;
Car . areSame ( car2 , car3 ) ;
Car . areSame ( car2 , car2 ) ;

11
11
11
11

fa lse
true
fa lse
true

Н аследование
Рассматривая прототипы, мы уже встречали некий вид наследования: при созда­
нии экземпляра класса он наследовал все функции, находящиеся в прототипе класса.
Но на этом дело не заканчивается: если метод не найден в прототипе объекта, прове­
ряется прототип прототипа. Так получается цепь прототипов. JavaScript будет идти
по цепи прототипов, пока не найдет тот прототип, который удовлетворяет запросу.
Если такой прототип не будет найден, то все закончится ошибкой.
Это пригодится при создании иерархии классов. Мы уже упоминали, что авто­
мобиль - это общий тип транспортного средства. Цепь прототипов позволяет рас­
полагать функции там, где им самое место. Например, у автомобиля мог бы быть
метод deployAi rbags. Мы могли бы сделать его методом обобщенного транспорт­
ного средства, но вы когда-либо видели лодку с подушками безопасности? С другой
стороны, почти все транспортные средства могут перевозить пассажиров; таким об­
разом, у транспортного средства мог бы быть метод addPas s enger (который мог бы
184

Глава 9 . Объекты и объектно-ориентирован ное п рограммирование

сообщать об ошибке, если количество пассажиров превышено). Давайте посмотрим,
как этот сценарий реализуется в коде JavaScript.
class Vehicle {
constructor ( ) {
t his . pas sengers = [ ] ;
consol e . log ( " Tpaнcпopтнoe средство создано " ) ;
addPa s s enge r ( p ) {
this . p a s sengers . push ( p ) ;

class Car extends Vehicle
constructor ( )
super ( ) ;
соnsоlе . lоg ( "Автомобиль создан " ) ;
deployAirbags { ) {
console . log ( "БАБАХ ! ! ! " ) ;

Первое нововведение, которое мы замечаем, - это ключевое слово extends; этот
синтаксис указывает, что класс Car происходит от класса Vehicle. Второй новостью
является вызов super ( ) . Это специальная функция JavaScript, которая вызывает кон­
структор суперкласса. Для производных классов это обязательно; если вы опустите
его, то получите ошибку.
Давайте рассмотрим этот пример в действии.
const v = new Vehicle ( } ;
v . addPassenge r ( " Frank" ) ;
v . addPas s enge r ( " Judy" ) ;
v . passengers ;
const с = new Car { ) ;
c . addPas s enger ( "Alice " ) ;
c . addPas s enge r ( " Cameron " } ;
c . passenge r s ;
v . deployAirbags ( } ;
c . deployAirbags ( } ;

1 1 [ "Frank " , "Judy "J

11 [ "A l i ce " , "Cameron "]
11 о=бка
1 1 "БАБАХ ! ! ! "

Обратите внимание, что мы можем вызвать метод deployAirbags с с, но не с v.
Другими словами, наследование работает только в одном направлении. Экземпляры
класса Car могут обращаться ко всем методам класса Vehicle, но не наоборот.

Полиморфизм
Термин полиморфизм (polymorphism) из лексикона объектно-ориентированных
языков описывает ситуацию, когда экземпляр рассматривается как член не только
Объектно-ориентированное п рограммирование

1 85

его собственного класса, но и любых суперклассов. На многих объектно-ориентиро­
ванных языках полиморфизм - это нечто особенное, приносящее большую пользу
ООП. Язык JavaScript не является типизированным, т.е. любой объект может быть
использован в любом месте (хотя правильный результат не гарантирован). Таким об­
разом, в некотором смысле у JavaScript есть абсолютный полиморфизм.
Код JavaScript, который вы пишете, довольно часто использует некую форму
утиной типизации (duck typing). Эта методика исходит из выражения "Если это
выглядит, как утка, плавает, как утка и крякает, как утка, то это, возможно, и есть
утка': В нашем примере с классом Car, если у вас есть объект, обладающий методом
deployAi rbags, то вы могли бы резонно заключить, что это экземпляр класса Car.
Это может быть правда, а может и нет, но попытка довольно хорошая.
В JavaScript предусмотрен оператор instanceof, который укажет вам, является ли
объект экземпляром данного класса. Как ни удивительно, но до тех пор, пока вы не
оперируете напрямую свойствами prototype и _proto_, этот оператор будет воз­
вращать правильный результат.
class Motorcycle extends Vehicle { }
const с = new Car ( ) ;
const m = new Motorcyle ( ) ;
с instanceof Car ;
1 1 true
с instanceof Vehicl e ;
1 1 true
m instanceof Car;
1 1 false
m instanceof Motorcycle ;
1 1 true
m instanceof Vehicle ;
1 1 true

Все объекты в JavaScript являются экземплярами корневого клас­
са Obj ect. Таким образом, для любого объекта о выражение о
instanceof Obj ect будет истинным (если только вы явно не устано­
вите значение его свойства _proto_, чего следует избегать). С прак­
тической точки зрения в этом есть небольшой смысл, поскольку такая
возможность позволяет создать ряд важных методов для всех объек­
тов иерархии. В качестве примера можно привести метод toSt ring,
который будет рассмотрен ниже в этой главе.

П ере б ор свойств объектов ( снова }
Мы уже видели, как можно перебрать свойства объекта в цикле for . . . in. Теперь,
когда мы понимаем механизм наследования прототипов, мы можем полностью оце­
нить использование метода hasOwnProperty при переборе свойств объекта. Для объ­
екта obj и свойства х вызов obj . hasOwnProperty ( х ) возвратит t rue, если у obj будет
свойство х, и false, если свойство х не определено или определено в цепи прототипов.
Если вы будете использовать классы ES6 так, как они задуманы, то свойства
данных всегда будут определяться в экземплярах, а не в цепи прототипов. Однако,
1 86

Глава 9 . Объекты и объектно- ориентированное п ро г раммирование

поскольку нет ничего, что предотвратило бы добавление свойств непосредствен­
но в прототип, всегда лучше использовать hasOwnProperty, чтобы удостовериться
в этом. Рассмотрим пример.
cla ss Super {
const ructor ( ) {
this . narne = ' Super ' ;
this . i s Super = true ;

1 1 это допустимо, но не жела тельно
Super . prototyp e . sneaky = ' Не рекомендуется ! ' ;
.

.

.

class Sub extends Super
constructor ( ) {
super ( ) ;
this . narne
' Sub ' ;
t hi s . i sSub = t ru e ;

const obj = new Sub ( ) ;
for ( le t р in obj ) {
console . log ( ' $ { p ) : $ { obj [ р ] } ' +
( obj . hasOwnPropert y ( p ) ?

'

( унаследовано ) ' ) ) ;

Если вы запустите эту программу, то увидите
narne : Sub
i sSuper : true
i sSub : true
sneaky : Не рекомендуется !

( унаследовано )

Свойства name, i sSuper и i sSub определяются в экземпляре, а не в цепи прото­
типов (обратите внимание, что свойства, объявленные в конструкторе суперкласса,
присутствуют также в экземпляре производного класса). Свойство sneaky, напро­
тив, было вручную добавлено в прототип суперкласса.
Вы можете избежать этой проблемы в целом, используя метод Obj ect . keys, кото­
рый включает только свойства, определенные в прототипе.

Строковое представление
Каждый объект в конечном счете происходит от класса Obj ect. Таким образом,
все методы, доступные в классе Obj ect, стандартно доступны для всех объектов.
Одним из этих методов является toSt ring, возвращающий стандартное строковое
Объектно · ориентирован ное п рограммирование

1 87

представление объекта. Стандартное поведение метода toString подразумевает воз­
вращение строки 11 [ obj ect Obj ect ] 11, что не особенно полезно.
Наличие метода toString, выводящего нечто описательное об объекте, может
очень пригодиться при отладке, позволяя сразу получить важную информацию
об объекте. Например, мы могли бы изменить свой класс Car так, чтобы его метод
toString возвращал марку, модель и VIN.
c l a s s Car {
toString ( )
return ' $ { th i s . ma ke } $ { t hi s . mode l } : $ { this . vi n } ' ;
}
// . . .

Теперь вызов метода toString для экземпляра Car дает немного больше инфор­
мации об объекте.

М ножественное наследование, примеси и интерфейсы
Некоторые объектно-ориентированные языки поддерживают множественное на­
следование (multiple inheritance), когда у одного класса может быть два прямых супер­
класса (в отличие от одного суперкласса, у которого, в свою очередь, есть один супер­
класс). Множественное наследование создает риск коллизий (collision) или конфликтов.
Таким образом, если нечто унаследовано от двух родителей и у обоих родителей есть
метод g reet, то от кого именно он будет унаследован производным классом? Во мно­
гих языках предпочитается одиночное наследование, при котором этой проблемы нет.
Но когда мы решаем реальные задачи, множественное наследование зачастую
имеет смысл. Например, автомобили могли бы происходить как от транспортных
средств, так и от "подлежащих страхованию" (вы можете застраховать и автомобиль,
и дом, но дом, безусловно, - не транспортное средство). В языках, где не поддер­
живается множественное наследование, зачастую вводится концепция интерфейса
(interface), чтобы обойти эту проблему. Класс (Car) может происходить только от од­
ного родителя (Vehicle), но у него может быть несколько интерфейсов (InsuraЬle,
Container и т.д.).
JavaScript - интересный гибрид. Технически это язык одиночного наследования,
поскольку поиск по цепи прототипов не распространяется на несколько родителей,
но он предоставляет пути, которые иногда превосходят и множественное наследова­
ние, и интерфейсы (а иногда - нет).
Основной механизм решения проблемы множественного наследования - это
концепция примеси (mixin). Примесь позволяет "подмешивать" функциональные
возможности по мере необходимости. Поскольку JavaScript позволяет чрезвычайно
много и без контроля типов, вы можете подмешать почти любые функции к любому
объекту в любое время.

1 88

Глава 9 . Объекты и объектно-ориентированное п рограммирова ние

Давайте создадим примесь "страхуемый", которую мы могли бы применить к ав­
томобилям. Мы не будем усложнять пример, но в дополнение к примеси страхова­
ния необходимо создать класс InsurancePol i cy. Примесь страхования нуждается
в методах addinsurancePolicy, get insurancePolicy и (для удобства) i s insured.
Давайте рассмотрим, как это могло бы работать.
class I n s urancePolicy ( ) { }
function makeinsuraЬl e ( o ) {
o . addinsurancePolicy
function ( p ) { thi s . insurancePolicy = р ; }
o . get insurancePolicy
function ( ) { return thi s . insurance Policy;
о. i s i nsured = function ( ) { return ! ! thi s . insurancePo l i c y ; }
=

=

Теперь мы можем сделать любой объект подлежащим страхованию. Так как мы
собираемся сделать подлежащим страхованию класс Car? Ваша первая мысль могла
бы быть такой.
ma keinsuraЬl e ( Ca r ) ;

Но вы были бы неприятно удивлены.
const carl
new Car ( ) ;
carl . addinsurancePol icy ( new InsurancePolicy ( ) ) ; / / ошибка
=

Если вы подумали "Конечно, ведь addinsurancePolicy не находится в цепи про­
тотипов': то будете совершенно правы. Делать класс Car подлежащим страхованию
вообще плохая идея. Кроме того, это вообще не имеет смысла: абстрактная концеп­
ция автомобиля не подлежит страхованию, но конкретный автомобиль - подлежит.
Таким образом, наше следующее решение могло бы быть таким.
const carl
new Car ( ) ;
make insuraЬle ( car l ) ;
carl . addinsurancePo l i cy ( new I nsurancePol icy ( ) ) ; / / работает
=

Это работает, но теперь нужно не забыть вызывать функцию ma ke i n s uraЫe
для каждого создаваемого нами автомобиля. Мы могли бы добавить этот вызов
в конструктор Car, но тогда мы продублируем эту функцию для каждого созданного
автомобиля. К счастью, решение простое.
makeinsuraЬl e ( Car . prototype ) ;
const carl = new Car ( ) ;
carl . addi nsurancePo li cy ( new I n s urancePolicy ( ) ) ; / / работает

Теперь это выглядит так, как будто наши методы всегда были частью класса Car. И
с точки зрения JavaScript так и есть. С точки зрения разработчика мы облегчили под­
держку этих двух важных классов. Группа разработчиков автомобилей создает и обслу­
живает класс Car, а группа страхования занимается классом InsurancePolicy и приме­
сью makeinsuraЫe. В результате место для пересечения двух групп все таки имеется, но
это куда лучше, чем когда все работают над одним гигантским классом Car.
М ножественное н аследование, п римеси и интерфейсы

1 89

Примеси не устраняют проблему коллизий: если бы по каким-то причинам стра­
ховая группа должна была бы создать в своей примеси метод shi ft, то это наруши­
ло бы класс Car. Кроме того, мы не можем использовать instanceof для выявления
объектов, которые допускают страхование: в лучшем случае мы можем рассчитывать
на утиную типизацию (если у объекта есть метод addinsurancePolicy, он, вероятно,
подлежит страхованию).
Мы можем сгладить некоторые из этих проблем, используя символы. Скажем,
страховая группа постоянно добавляет очень обобщенные методы, которые кон­
фликтуют с методами класса Car. Вы могли бы попросить их использовать для всех
своих ключей символы. Теперь их примесь будет выглядеть следующим образом.
class InsurancePol i cy ( ) { }
const ADD_POLICY
SymЬol ( ) ;
const GET_POLICY
SymЬol ( ) ;
const I S_ INSURED
SymЬol ( ) ;
const POLICY
SymЬol ( ) ;
function makeinsuraЬl e ( o ) {
function ( p ) { thi s [ POLICY] = р ; }
o [ADD_POLICY]
function ( ) { return this [ POLICY] ;
o [GET_POLICY]
funct ion ( ) { return ! ! this [ POLI CY ] ;
o [ I S INSURED]
=
=

=

=

Поскольку символы уникальны, это гарантирует, что примесь никогда не будет
конфликтовать с существующими функциями класса Car. В использовании это мо­
жет быть немного неуклюже, но намного безопаснее. Компромиссный подход, воз­
можно, подразумевал бы использование обычных строк для методов, а символов (та­
ких, как POLI CY) для свойств данных.

Закл юч ение
Объектно-ориентированное программирование - это чрезвычайно популярная
парадигма и на то есть серьезные причины. Оно обеспечивает организацию и инкап­
суляцию кода для решения многих реальных задач, что облегчает поддержку, отлад­
ку и исправление ошибок. Реализацию ООП в JavaScript серьезно критикуют - не­
которые доходят до утверждения, что JavaScript даже не соответствует определению
объектно-ориентированного языка (обычно из-за нехватки контроля доступа к дан­
ным). Этот аргумент заслуживает внимания, но как только вы привыкнете к тому,
как в JavaScript реализовано ООП, вы найдете этот язык весьма гибким и мощным.
Он позволяет делать такие вещи, на которые другие объектно-ориентированные
языки не способны.

1 90

Глава 9 . Объекты и объектно-ориентированное п рограмми рование

ГЛАВА 1 0

Ото б ра жени я и на б ор ы

В ЕSб введены две популярные структуры данных: отображения (map) и наборы
(set). Отображения подобны объектам, они способны сопоставлять ключи со зна­
чениями, а наборы подобны массивам за исключением того, что дубликаты не до­
пускаются.

Отображения
До появления ЕSб, когда требовалось сопоставить ключам значения, использова­
лись объекты, поскольку объекты позволяют сопоставить строковые ключи со зна­
чениями объектов любых типов. Однако при использовании объектов для этой цели
возникает много проблем.


Прототипы, лежащие в основе объектов, способны создать сопоставления,
о которых вы и не предполагали.



Нет никакого простого способа узнать количество сопоставлений, находящих­
ся в объекте.



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



Объекты не гарантируют порядка своих свойств.

Объект Мар ликвидирует эти недочеты и является превосходным выбором для сопо­
ставления ключей со значениями (даже если ключи - строки). Предположим, например,
что у вас есть объекты пользователей, которые необходимо сопоставить с ролями.
const
const
const
const

ul
u2

u4

name :
name :
name :
name :

' Cynthia '
' Jackson '
' Ol ive ' }
' Jame s ' }

};
};
;
;

Сначала создадим отображение.
const u s e rRole s

=

new Мар ( ) ;

Затем используем отображение для назначения пользователям ролей с использо­
ванием ее метода set ( ) .
userRole s . set ( u l , ' Us er ' )
userRole s . set ( u2 , ' Us er ' )
userRole s . s e t ( u З , ' Admin '
/ / бедный Джеймс . . . мы не

;
;
);
назначили ему роль

Метод set ( ) допускает также цепочки, что позволяет сэкономить на вводе.
userRole s
. se t ( u l ,
. se t (u2 ,
. se t ( u З ,

' Us er ' )
' Us er ' )
' Admin ' ) ;

Вы можете также передать в конструктор массив массивов.
const userRol e s
new Мар ( [
[ u l , ' Us er ' ] ,
[u2 , ' Us er ' ] ,
[ u З , ' Admin ' ] ,
]);
=

Теперь, если необходимо выяснить роль пользователя u2, можноиспользовать
метод get ( ) .
userRole s . ge t ( u2 ) ;

1 1 "User "

Вызов метода get для ключа, отсутствующего в отображении, возвратит значение
undefined. Кроме того, вы можете использовать метод has ( ) для определения нали­
чия в отображении заданного ключа.
userRo l e s . ha s ( ul )
userRole s . ge t ( u l )
userRoles . ha s ( u4 )
userRole s . ge t ( u4 )

;
;
;
;

1 1 t rue
1 1 "User"
1 1 fa lse
1 1 undefined

Вызов метода s e t ( ) для ключа, уже присутствующего в отображении, приведет
к замене его значения.
// ' User '
userRole s . ge t ( ul ) ;
userRole s . set ( ul , ' Admin ' ) ;
userRole s . ge t ( ul ) ;
/ / 'Admin '

Свойство s i ze возвращает количество элементов в отображении.
userRole s . s i z e ;

11

3

Метод key s ( ) позволяет получить ключи в отображении, метод values ( ) - воз­
вратить значения, а метод entries ( ) - получить элементы в виде массивов, в ко­
торых первый элемент - ключ, а второй - значение. Все эти методы возвращают
итерируемый объект, который может быть перебран в цикле for . . . of.
1 92

Глава

1 0.

Отображения и наборы

for ( le t и of userRole s . keys ( ) )
console . log ( u . name ) ;
for ( le t r of userRoles . value s ( } )
console . log ( r ) ;
for ( le t ur of userRol e s . entrie s ( ) )
console . log ( ' $ { ur [ О ] . name } : $ { ur [ 1 ] } ' ) ;
1 1 обратите внимание : чтобы сделать этот перебор еще более
1 1 естественным, мы можем использова ть деструктуризацию :
for ( le t [ u , r ] of userRoles . entr i e s ( ) )

console . log ( ' $ { u . name } : $ { r } ' ) ;
/ / метод entries ()
это стандартный итератор для отображений, так
/ / вы можете сократить предыдущий пример :
for ( le t [ u , r] of userRoles )
console . log ( ' $ { u . name ) : $ { r } ' ) ;
-

Если вместо итерируемого объекта необходим массив, вы можете использовать
оператор расширения.
[.

. . userRole s . va lues ( ) ] ;

/ / [ "User " , "User " , "Admin " ]

Чтобы удалить одиночный элемент из отображения, используйте метод delete ( )
userRoles . de l e t e ( u2 ) ;
userRoles . s i z e ;

.

11 2

Наконец, если вы хотите удалить все элементы из отображения, то можете сде­
лать это, используя метод clear ( ) .
userRoles . clear ( ) ;
userRole s . s i z e ;

11 о

Слабые отображения
Объект WeakМap идентичен объекту Мар, кроме следующего.


Его ключи должны быть объектами.



Ключи в WeakМap допускают сборку мусора.



Объект WeakМap не может быть перебран или очищен.

Обычно JavaScript хранит объект в памяти, пока где-нибудь есть ссылка на него.
Например, если у вас будет объект, который является ключом в Мар, то JavaScript
будет хранить этот объект в памяти, пока объект Мар существует. С WeakМap все не
так. Из-за этого объект WeakMap не может быть перебран (есть слишком большая

Слабые отображения

1 93

опасность, что при переборе произойдет доступ к объекту, который уже был унич­
тожен в процессе сборки мусора).
Благодаря этим свойствам объект WeakMap применяется для хранения закрытых
ключей в экземплярах объекта.
const SecretHolder = ( functi on ( )
const secrets = new WeakМap ( ) ;
return class {
s e t Secret ( secre t ) {
s ecret s . set ( th i s , secret ) ;
getSecret ( ) {
return secrets . ge t ( th is ) ;

}) () ;

Здесь мы поместили свой объект WeakMap в немедленно вызываемое функцио­
нальное выражение (IIFE) наравне с классом, который его использует. Вне IIFE мы
получаем класс SecretHolde r, экземпляры которого способны хранить секреты. Мы
можем установить секрет, только используя метод setSecret, а получить к нему до­
ступ - только через метод getSecret.
const а
const Ь

=

new SecretHolder ( ) ;
new S e cretHolder ( ) ;

a . setSecret ( ' s e cret А ' ) ;
b . s e t S e cret ( ' s ecret В ' ) ;
1 1 "secret А "
1 1 "secret В "

a . getSecret ( ) ;
b . getSecret ( ) ;

Мы могли бы использовать обычный объект Мар, но сообщенные его экземпля­
рам SecretHolder секреты никогда не будут уничтожены в процессе сборки мусора!

Наборы
Набор (set) - это коллекция данных, в которой дубликаты недопустимы. Ис­
пользуя наш предыдущий пример, мы можем назначить пользователя на несколько
ролей. Например, у всех пользователей могла бы быть роль "User", а у администра­
торов - и " U s e r " , и "Adrnin". Однако для пользователя нет никакого логического
смысла иметь одну и ту же роль многократно. Набор - идеальная структура данных
для этого случая.
Сначала создайте экземпляр объекта Set.
const role s

1 94

=

Гла ва

new Set ( ) ;

10.

Отображения и наборы

Если мы теперь хотим добавить роль пользователя, можем воспользоваться ме­
тодом add ( ) .
role s . add ( " U s e r " ) ;

/ / Набор [ 11User11 ]

Чтобы сделать этого пользователя администратором, вызовите метод add ( ) снова.
rol e s . add ( "Admin " ) ;

/ / На бор [ "User " , "Admin "

Как и у Мар, у объекта Set есть свойство s ize.
roles . si z e ;

11 2

Достоинство наборов в том, что мы не должны выяснять, находится ли уже нечто
в наборе, прежде чем его добавим. При попытке добавить в набор нечто, что уже там
находится, ничего не происходит.
role s . add ( " U s e r " ) ;
roles . si z e ;

1 1 На бор
11 2

"Us er " , "Admin " ]

Чтобы удалить роль, мы просто вызываем метод delete ( ) , который возвращает
true, если роль была в наборе, и false - в противном случае.
roles . delete ( "Admin " ) ;
role s ;
roles . de l et e ( "Admin " ) ;

1 1 true
/ / На бор [ "User " J
/ / fa lse

Слабые наборы
Слабые наборы могут содержать только объекты, и эти объекты удаляются в про­
цессе сборки мусора. Как и в WeakМap, значения в WeakSet не могут быть перебраны,
что делает слабые наборы очень редко применяемыми. Фактически единственный
подходящий случай использования для слабых наборов - это когда необходимо
определять, есть ли данный объект в наборе.
Например, у Санта Клауса мог бы быть WeakSet по имени naught y (непослуш­
ные), чтобы он мог решить, кому достанется уголь.
const naughty = new WeakSet ( ) ;
const children
[
name : " Suzy" } ,
name : " Derek" } ,
];
=

naught y . add ( children [ l ] ) ;
for ( let child of childre n )
if ( naughty . ha s ( child) )

Слабые наборы

1 95

console . log ( ' Yгoль для $ { child . name } ! ' ) ;
else
console . log ( ' Пoдapки для $ { ch i l d . name } ! ' ) ;

Расставаясь с объектно й привы ч ко й 1
Если вы - опытный программист JavaScript, который является новичком в ЕSб,
возможно, вы уже привыкли использовать объекты для сопоставления значений.
И без сомнения, вы изучили все нюансы применения объектов в виде отображений,
позволяющие обойти подводные камни. Но теперь у вас есть реальные отображения,
и вы должны использовать их! Аналогично вы, вероятно, привыкли использовать
объекты с логическими значениями в качестве наборов; вам также больше не нужно
делать это. Когда вы создаете объект, остановитесь и спросите себя: "Я использую
этот объект, только чтобы получить отображение?" Если ответ - "Да': то рассмотри­
те возможность использования вместо него объекта Мар.

1 Перефразировка "Breaking the Hablt" рок-группы Linkin Park.
1 96

Глава

10.

Отображения и н аборы

-

Примеч. ред.

ГЛА ВА 1 1

И с к nюч ени я и о б ра б от к а оши б о к

Все мы хотели бы жить в мире без ошибок, но у нас такой роскоши нет. Большин­
ство даже тривиальных приложений подвержено ошибкам, являющимся результатом
обстоятельств, которые вы не предвидели. Первый шаг к созданию надежного, высо­
кокачественного программного обеспечения - это признать, что в нем будут ошибки.
Второй шаг - предвидение этих ошибок и их обработка разумным способом.
Обработка исключений (exception handling) - это механизм, который позволяет
справляться с ошибками контролируемым способом. Обработка исключений, в от­
личие от обработки ошибок (error handling), предназначена, чтобы справляться с ис­
ключительными обстоятельствами, т.е. не с теми ошибками, которые вы ожидаете,
а с непредвиденными.
Грань между ожидаемыми и непредвиденными ошибками (исключениями) весь­
ма расплывчата и очень ситуативна. От приложения, которое предназначено для ис­
пользования широкой неподготовленной публикой, можно ожидать намного более
непредсказуемого поведения, чем от приложения, предназначенного для использова­
ния квалифицированными пользователями.
Примером ожидаемой ошибки является ввод в форме неправильного адреса элек­
тронной почты: люди все время делают опечатки. Непредвиденной ошибкой могло
бы быть исчерпание дискового пространства или невозможность доступа к обычно
всегда работающей службе.

Объект Error
В JavaScript есть встроенный объект E rror, который удобен для обработки оши­
бок любого вида (исключений и ожидаемых). Создавая экземпляр объекта E r ror, вы
можете присвоить ему сообщение об ошибке.
const err

=

new Е rrоr ( ' Ошибочный ema il ' ) ;

Создание экземпляра E rror само по себе ничего не делает. Оно лишь предостав­
ляет вам средство для сообщения об ошибке. Вообразите функцию, которая прове­
ряет адреса электронной почты. Если функция сработала успешно, она возвращает

адрес электронной почты как строку. Если это не так, она возвращает экземпляр
объекта E rror. Для простоты будем считать нечто, содержащее символ @ , допусти­
мым адресом электронной почты (см. главу 1 7) .
functi on validateEmai l ( emai l )
return ema i l . match ( /@ / ) ?
emai l :
new Еrrоr ( ' Ошибочный ema i l : $ { emai l } ' ) ;

Чтобы определить, был ли возвращен экземпляр объекта E rror, мы можем ис­
пользовать оператор typeof. Предоставленное нами сообщение об ошибке будет
присвоено свойству mes sage.
const emai l = " j ane@doe . com" ;
cons t validatedEmai l = validateEma i l ( emai l ) ;
i f ( validat edEmai l ins tanceof Error ) {
console . error ( ' Oшибкa : $ { va l idatedEma i l . me s sage } ) ;
else {
соnsоlе . lоg ( ' Корректный ema i l : $ { validatedEmail } ' ) ;

Хотя это вполне допустимый и полезный способ использования экземпляра
объекта E rror, он чаще используется в процессе обработки исключений, который
мы рассмотрим далее.

Обработка искпюч ений с испоп ьзованием
бп оков try и с а tch
Для обработки исключений используется конструкция операторов try . . . catch.
Идея в том, что осуществляется "попытка" (try) что-то сделать, и если при этом произой­
дет какое-либо исключение, оно будет "перехвачено" (catch). Функция validateEma i l
в нашем предыдущем примере обрабатывает ожидаемую ошибку, когда некто пропу­
скает символ @ в адресе электронной почты, но есть также возможность возникнове­
ния и непредвиденной ошибки: непутевый программист может присвоить переменной
email нечто отличное от строки. Как следует из предыдущего примера, присвоение пе­
ременной emai l значения null, числа или объекта (чего угодно, кроме строки) приводит
к ошибке. В результате программа сразу же прекращает свое выполнение, что весьма
недружественно по отношению к пользователю. Чтобы обезопасить себя от непредви­
денной ошибки мы можем поместить свой код в блок оператора try . . . catch.
const ema i l

=

nul l ;

1 1 упс

try {

1 98

Глава

11.

И сключения и обработка ошибок

const validatedEma i l = val idateEma i l ( emai l ) ;
i f ( va l idatedEma i l instanceof Error ) {
console . error ( ' Oшибкa : $ { validatedEma i l . me s sage } ) ;
else {
соnsоlе . lоg ( ' Корректный ema i l : $ { va l i datedEmai l } ' ) ;
catch ( err) {
console . error ( ' Oшибкa : $ { err . me s sage } ' ) ;

Поскольку мы перехватываем ошибку, наша программа не будет аварийно завер­
шать работу. В данном случае в обработчике ошибок мы просто выводим соответ­
ствующее сообщение и продолжаем работу. Однако что делать, если для продолже­
ния работы программы требуется правильный адрес электронной почты? Очевидно,
что в таком случае нужно обработать ошибку более изящно и красиво завершить
работу программы.
Обратите внимание, что поток выполнения покидает блок catch, как только про­
исходит ошибка; т.е. оператор i f, который следует за вызовом va l idateEmai l ( ) , не
будет выполнен. В блоке t ry у вас может быть столько операторов, сколько нужно;
первый из них, который закончится ошибкой, передаст управление блоку catch. Если
никаких ошибок нет, блок catch не выполняется, и программа продолжает работу.

Генерирование о ш ибки
В нашем предыдущем примере м ы использовали оператор t ry . . . catch для об­
работки ошибок, которые возникали в самом движке JavaScript (когда мы пытались
вызвать метод match для чего-то, что не является строкой). Вы можете также сге­
нерировать ошибку самостоятельно, чтобы задействовать механизм обработки ис­
ключений.
В отличие от других языков с обработкой исключений, в JavaScript при генерации
ошибки вы можете использовать любое значение: число, строку или любой другой
тип. Однако обычно оператору throw передают экземпляр объекта Error. Большин­
ство блоков catch ожидает экземпляра объекта E rror. Имейте в виду, что вы не
всегда можете контролировать, где будет обработана сгенерированная вами ошибка
(функции, которые вы пишете, могут быть использованы другими программистами,
вполне резонно ожидающими, что в процессе генерации ошибки оператору throw
передается экземпляр объекта Error) .
Например, создавая приложение по оплате счетов для банка, вы могли бы генери­
ровать исключения, если остаток на счете не покрывает платеж (это действительно
исключительный случай, поскольку проверка на такую ситуацию должна осущест­
вляться прежде, чем начнется оплата по счету).

Генерирование о шибки

1 99

function b i llPay ( amount, рауе е , account )
i f ( amount > a ccount . balance )
throw new Error ( "Maлo денег . " ) ;
accoun t . t rans fer ( payee , amount ) ;

При выполнении оператора t hrow текущая функция немедленно прекращает
свою работу. Поэтому в нашем примере вызова метода account . t rans fer не будет,
что нам и требовалось.

Обработка искл юч ени й и стек вызовов
Типичная программа вызывает функции, а эти функции, в свою очередь, вызы­
вают другие функции, а эти функции - следующие функции и т.д. Интерпретатор
JavaScript должен отслеживать их все. Если функция а вызывает функцию Ь, а функ­
ция Ь вызывает функцию с, то, когда функция с завершает работу, управление воз­
вращается функции Ь, а когда завершается функция Ь, управление возвращается
функции а. Поэтому, когда выполняется функция с, функции а и Ь "ожидают': Эти
вложенные функции, которые еще не завершили работу, формируют стек вызовов
(call stack).
Если в функции с происходит ошибка, то что будет с функциями а и Ы Оче­
видно, что в функции Ь также возникнет ошибка, поскольку в ней может исполь­
зоваться значение, возвращаемое функцией с . Это в свою очередь вызовет ошибку
в функции а, поскольку в ней также может использоваться значение, возвращаемое
функцией Ь. По существу, ошибка будет распространяться по стеку вызовов вверх,
пока не будет перехвачена и обработана.
Ошибки могут быть перехвачены и обработаны на любом уровне в стеке вызо­
вов. Если они так и не будут перехвачены, интерпретатор JavaScript просто остано­
вит программу. Это явление называется необработанным исключением ( unhandled
exception) или не перехваченным исключением (uncaught exception), оно всегда при­
водит к аварийному завершению программы. С учетом количества мест, где может
произойти ошибка, перехват всех возможных ошибок, способных привести к ава­
рийному завершению программы, становится трудоемким и громоздким.
Когда ошибка перехватывается, стек вызовов предоставляет полезную инфор­
мацию для диагностики проблемы. Например, если функция а вызывает функцию
Ь, которая вызывает функцию с и ошибка происходит в функции с, то стек вызовов
говорит нам не только о том, что ошибка произошла в функции с, но и что она про­
изошла, когда эта функция была вызвана функцией Ь, когда она была вызвана функ­
цией а. Это полезная информация, если функция с вызывается из многих разных
мест в вашей программе.

200

Глава

11.

И скл ючения и обработка о ш ибок

В большинстве реализаций JavaScript экземпляры объекта E rror содержат свойство
s tack, которое является строковым представлением стека (это нестандартное средство
JavaScript, но оно доступно в большинстве систем). Вооружившись этими знаниями,
мы можем написать пример, который демонстрирует обработку исключений.
function а ( ) {
console . log ( ' а : вызываем Ь ' ) ;
Ь() ;
console . log ( ' a : готово ' ) ;
func t i on Ь ( ) {
console . log ( ' Ь : вызываем с ' ) ;
с() ;
console . log ( ' b : готово ' ) ;
func t i on с ( ) {
console . log ( ' c : генерируем ошибку ' ) ;
throw new Error ( ' c ошибка ' ) ;
console . log ( ' c : готово ' ) ;
func t i on d ( ) {
console . log ( ' d : вызываем с ' ) ;
с() ;
console . log ( ' d : готово ' ) ;

try
а();
catch ( err)
console . log ( err . s t a ck ) ;

try
d() ;
catch ( err)
console . log ( err . s t a c k ) ;

Запуск этого примера в Firefox приводит к следующему выводу на консоль.
а : вызываем Ь
Ь : вызываем с
с : генерируем ошибку
c@debugger eva l code : l З : l
b@debugger eva l code : 8 : 4
a@debugger eva l code : 3 : 4
@ debugger eva l code : 2 3 : 4

Обработка исключений и стек вызовов

201

d : вызываем с
с : генерируем ошибку
c@debugger eval code : l З : l
d@debugger eval code : 1 8 : 4
@debugger eval code : 2 9 : 4

Строки со знаком @ означают трассировку стека, которая начинается с "самой
глубокой" функции (с) и завершается без функции вообще (сам браузер). Как можно
заметить, имеется две разных трассировки стека. В первой мы видим, что функция
с была вызвана из Ь, а та, в свою очередь, была вызвана из а. Во второй видно, что
функция с была вызвана непосредственно из d.

Конструкция try . . . catch . . . finally
Иногда в коде блока t ry задействуется некий ресурс, такой как подключение
к серверу НТТР или открытие файла. Вне зависимости от ошибки мы должны ос­
вободить этот ресурс, чтобы он не был постоянно связан с нашей программой. По­
скольку блок try может содержать любое количество операторов, в каждом из кото­
рых может возникнуть ошибка, поэтому блок t ry не самое безопасное место для ос­
вобождения ресурса (поскольку ошибка может произойти прежде, чем представится
шанс сделать это). Также небезопасно освобождать ресурс в блоке catch, поскольку
он не выполняется, если не будет ошибки. Это именно та ситуация, которая требует
блока finally, выполняемого вне зависимости от наличия ошибки.
Поскольку мы еще не рассматривали работу с файлами или подключениями
к серверу НТТР, для демонстрации блока fina l l y мы будем просто использовать
пример с операторами console . log.
try
console . log ( " Этa строка вьmолнена . . . " ) ;
throw new Error ( "Yпc ! " ) ;
consol e . log ( " Эта строка не вьmолняется . . . " ) ;
catch ( err) {
console . l o g ( "Была ошибка . . . " ) ;
finally {
console . log ( " . . . вceгдa выполняется " ) ;
console . log ( " Здесь вьmолняется очистка " ) ;

Опробуйте этот пример с оператором throw и без него; вы увидите, что блок
finally выполняется в любом случае.

202

Гла ва

11.

И сключения и обработка ошибок

П озвол ьте искл юч ениям быть искл ючениями
Теперь, когда вы знаете, что такое обработка исключений и как ее осуществлять,
наверняка вы захотите использовать ее для обработки всех ошибок - как ожидае­
мых, так и нет. В конце концов, генерирование ошибки, чрезвычайно простой и удоб­
ный способ "выхода", когда вы попадаете в ситуацию, с которой не можете справить­
ся. Но обработка исключений имеет свою цену. Кроме риска, что исключение так
и не будет перехвачено (это приведет к аварийному завершению программы), при­
менение исключений создает дополнительную вычислительную нагрузку. Поскольку
исключения должны "прокрутить" стек, пока не встретится блок catch, интерпрета­
тор JavaScript вынужден выполнять некоторые дополнительные служебные действия.
При постоянном росте скоростей компьютеров это вызывает все меньше и меньше
беспокойства, но генерирование исключений в часто используемых ветках програм­
мы может снизить ее производительность.
Помните, что каждый раз, генерируя исключение, вы должны обработать его
(если не хотите столкнуться с аварийным завершением программы). Вы не можете
получить нечто из ничего. Исключения лучше использовать лишь как последнюю
линию обороны, для обработки исключительных ситуаций, которые вы не можете
предвидеть, а для исправления ожидаемых ошибок используйте операторы управле­
ния потоком.

Позвол ьте исключениям быть исключениями

203

ГЛАВА 1 2

И тераторы и г енер атор ы
В спецификацию ЕSб введено две очень важные новые концепции: итераторы
(iterator) и генераторы (generator). Генераторы связаны с итераторами, поэтому да­
вайте начнем с итераторов.
Итератор напоминает закладку: он помогает следить, где вы находитесь. Массив пример итерируемого (iteraЫe) объекта: он содержит множество элементов (по аналогии
со страницами в книге) и может предоставить итератор (который похож на закладку).
Давайте сделаем эту аналогию конкретней: предположим, что у вас есть массив book
(книга), каждый элемент которого - строка, представляющая страницу. Формату такой
книги лучше всего соответствует стишок "Twinkle, Twinkle, Little Bat" (Крокодильчики
мои, цветики речные!1) из Алисы в стране чудес Льюиса Кэрролла (представьте, что у вас
детская книжка, на каждой странице которой расположена всего одна строчка).
const book = [
" Twinkl e , twinkle , l it t l e bat ! " ,
" How I wonder what you ' re a t ! " ,
" Up above the world you f l y , " ,
" L i ke а tea tray in the s ky . " ,
" Twinkl e , twink l e , l it t l e bat ! " ,
" How I wonder what you ' re at ! " ,
];

Теперь, когда у нас есть массив book, мы можем получить итератор, используя его
метод values.
const it = boo k . values ( ) ;

Продолжая нашу аналогию, итератор (обычно сокращаемый как it ) является за­
кладкой, но это закладка только для конкретной книги. Кроме того, мы ее еще ни­
куда не поместили и мы еще не начали читать книгу. Чтобы "начать читать", необхо­
дим вызов метода next итератора, который возвращает объект с двумя свойствами:
value, который содержит текущую "страницу", и done, которому присваивается зна­
чение t rue после того, как вы прочитаете последнюю страницу. Наша книга - дли­
ной лишь шесть страниц, поэтому довольно просто продемонстрировать, как мы
можем прочитать ее полностью.
1 Пересказ с английского Бориса Заходера. - Примеч. ред.

i t . next
i t . next
i t . next
i t . next
i t . next
i t . next
i t . next
i t . next
i t . next

()
()
()
()
()
()
()
()
()

;
;
;
;
;
;
;
;
;

va l u e :
va l u e :
va l u e :
va l u e :
va l u e :
va l u e :
va l u e :
va l u e :
va l u e :

11
11
11
11
11
11
11
11
11

"Twinkle, t winkle, l i t tle ba t ! " , don e : fa lse }
"How I wonder wha t you ' re a t ! " , done : fa lse }
"Ир above the wor ld you f1 у, ", done : fa lse }
"Like а t ea tray in the sky. " done : fa lse }
"Twinkl e , twinkle , l i t tl e ba t ! " , done : fa lse
"How I wonder wha t you ' re a t ! " , done : fa lse }
undefined, don e : true
undefined, don e : true
undefined , don e : true
'

Здесь есть несколько важных моментов, на которые стоит обратить внимание.
Прежде всего, когда метод next возвращает последнюю страницу книги, он не ука­
зывает, что это конец. Здесь аналогия с книгой немного нарушается: читая послед­
нюю страницу книги, вы знаете, что она последняя, правильно? Итераторы применя­
ются не для книг, и не всегда настолько просто узнать, когда вы закончили. Обратите
внимание, что по завершении свойство value имеет значение unde fined и вы можете
продолжать вызывать метод next и он будет продолжать возвращать то же самое.
Как только итератор достигает конца, он в буквальном смысле достигает конца на­
бора данных, и больше не должен возвращать никаких данных.2
Хотя в этом примере и не показано все непосредственно, вам уже должно быть
понятно, что между вызовами метода i t . next ( ) можно выполнять некие действия.
Другими словами, итератор i t всегда хранит указатель на текущее место.
Если нужно перебрать массив, можно использовать цикл for или цикл for . . . of.
Механика цикла for проста: известно, что элементы массива пронумерованы и по­
следовательны, поэтому мы можем использовать индексную переменную для доступа
к каждому элементу массива по очереди. А как насчет цикла for . . . оН Как он делает
свою работу без индекса? Оказывается он использует итератор: цикл for . . . of будет
работать с любЬtм объектом, который предоставляет итератор. Мы скоро увидим, как
использовать это в своих интересах. Сначала давайте рассмотрим, как мы можем сэму­
лировать цикл for . . . of, используя цикл while и наше обретенное знание итераторов.
const it = book . values ( ) ;
l e t current = i t . next ( ) ;
while ( ! current . done ) {
console . log ( current . value ) ;
current = i t . next ( ) ;

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

2 Поскольку объекты сами отвечают за предоставление собственного итеративного механизма, как
мы увидим вскоре, фактически вполне возможно создать "плохой итератор': в котором значение
свойства done изменено на обратное; такой итератор считался бы дефектным. Вообще, вы должны
полагаться на правильное поведение итератора.

206

Глава

12.

Итераторы

и

генераторы

const i t l = book . values ( ) ;
const i t 2
boo k . values ( ) ;
1 1 оба итератора в начальном положении
=

1 1 чтение двух страниц с i tl :
i t l . next ( ) ; / / { va l u e : " Twinkle, twinkle, l i t t l e ba t ! ", don e : fa lse }
i t l . next ( ) ; / / { va l u e : "How I wonder wha t you ' re a t ! " , don e : fa lse }
1 1 чтение одной страницы с i t 2 :
i t 2 . next ( ) ; / / { va l u e : " Twinkl e , t wink l e , l i t t l e ba t ! ", don e : false }
1 1 чтение другой страницы с i tl :
i t l . next ( ) ; / / { va l u e : "Ир above the world you fly, ", don e : false }

В данном примере эти два итератора независимы и перебирают массив по соб­
ственному индивидуальному расписанию.

П ротокол итератора
Итераторы сами по себе мало интересны: они - лишь инструмент, обеспечива­
ющий более интересные действия. Протокол итератора (iterator protocol) позволя­
ет стать итерируемым любому объекту. Предположим, что вы хотите создать класс
системноrо журнала, в котором сообщениям будут добавляться временные метки.
Внутренне для хранения сообщений с временными метками мы используем массив.
class Log {
con s t ructor ( ) {
this . me s s ages

[] ;

add ( me s sage ) {
this . messages . push ( { me s sage : mes sage , t ime stamp : Dat e . now ( )

});

Пока неплохо". Но что если мы захотим впоследствии перебрать все элементы
в журнале (т.е. выполнить их итерацию)? Конечно, мы могли бы обращаться напря­
мую к массиву log . me s s ages, но было бы куда лучше, если бы мы могли обработать
log так, как будто он непосредственно итерируем, подобно массиву? Протокол итера­
тора позволяет нам сделать это. Он гласит, что если ваш класс предоставляет символь­
ный метод SymЬol . iterator, который возвращает объект с поведением итератора (т.е.
у него есть метод next, возвращающий объект со свойствами value и done), то он ите­
рируем! Давайте изменим наш класс Log так, чтобы он имел метод SymЬol . i terator.
class Log {
con s t ructor ( ) {
this . me s sages
add (message )

[] ;

{

П ротокол итератора

207

t hi s . me s sages . push ( { mes sage : mes s a g e , timestamp : Date . now ( )

));

[ S ymЬol . iterator ] ( ) {
return this . me s s a ge s . values ( ) ;

Теперь мы можем перебрать содержимое экземпляра класса Log точно так же, как
если бы это был массив.
const log
new
l оg . аdd ( " Первый
lоg . аdd ( " Видели
log . add ( "Видели
11. . .
=

L og ( ) ;
день на море " ) ;
большую рыбу " ) ;
корабль " ) ;

/ / перебор log, как будто это ма ссив !
for ( le t entry of l o g ) {
console . log ( ' $ { entry . me s sage ) @ $ { entry . t imes t amp ) ' ) ;

В этом примере мы соблюдаем протокол итератора, получив итератор массива
mes s ages, но мы вполне могли бы написать и собственный итератор.
class Log
// . . .
[ S ymЬol . iterat o r ] ( )
let i
О;
const messages
this . me s sage s ;
return {
next ( ) {
i f ( i >= mes s a ge s . lengt h )
return { value : unde fined, done : true } ;
return { value : message s [ i + + ] , done : false ) ;
=

В рассмотренных здесь примерах задействован перебор предопределенного набора
элементов: страниц в книге или сообщений в журнале. Но итераторы можно использо­
вать и для представления объекта, значения которого никогда не исчерпаются.
Для демонстрации рассмотрим очень простой пример: создание чисел Фибонач­
чи. Числа Фибоначчи не особенно трудно создавать, но они зависят от предыдущих
чисел. Для непосвященного: последовательность Фибоначчи - это сумма предыду­
щих двух чисел в последовательности. Последовательность начинается с 1 и 1 : сле­
дующее число 1 + 1 равно 2. Следующее число 1 + 2 равно 3. Четвертое число 2 + 3
равно 5 и т.д. Последовательность выглядит следующим образом.
1, 1 , 2, 3, 5 , 8 , 13, 21, 34, 55, 89, 144, . . .

208

Гn ава

1 2.

Итераторы

и

ге нераторы

Последовательность Фибоначчи продолжается бесконечно, и наше приложение
не знает, сколько элементов будет необходимо, что делает ее идеальным применени­
ем для итераторов. Единственное различие между этим и предыдущими примерами
в том, что этот итератор никогда не будет возвращать t rue для свойства done:
c l a s s FibonacciSequence {
[ S ymЬol . iterato r ] ( ) {
let а = О , Ь = 1 ;
return {
next ( )
let rval = { value : Ь , done : false } ;
Ь += а ;
а = rva l . value ;
return rval ;
};

Если бы мы использовали экземпляр FibonacciSequence с циклом for . . . of, то
получили бы бесконечный цикл . . . ведь числа Фибоначчи никогда не закончатся, ни­
когда! Чтобы предотвратить это, мы добавим оператор break после 10 элементов.
const fib = new FibonacciSequence ( ) ;
l et i
О;
for ( let n o f f i b ) {
console . log ( n ) ;
i f ( ++ i > 9 ) brea k ;
=

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


Функция может возвратить (yield) управление вызывающей стороне в любой
момент.



Когда вы вызываете генератор, он не запускается сразу. Вместо этого вы полу­
чаете итератор. Функция запускается при вызове метода next итератора.
Генераторы

209

Генераторы в JavaScript отмечаются звездочками после ключевого слова function;
в остальном их синтаксис идентичен обычным функциям. Если функция является
генератором, вы можете использовать ключевое слово yield в дополнение к return.
Давайте рассмотрим простой пример генератора, который возвращает все цвета
радуги.
rainbow ( ) { // звездочка указывает, что это генера т ор
' красный ' ;
' оранжевый ' ;
' желтый ' ;
' зеленый ' ;
' голубой ' ;
' синий ' ;
' фиолетовый ' ;

funct ion*
yield
yield
yield
yield
yield
yield
yield

Теперь давайте рассмотрим, как происходит вызов этого генератора. Помните,
что при вызове генератора возвращается итератор. Мы вызовем функцию, а затем
пролистаем итератор.
=

const it
rainbow ( ) ;
va l u e : "красный ", don e : false }
i t . next ( ) ; 1 1
va lue : "оранжевый ", don e : false
i t . next ( ) ; 1 1
va lue : "желтый " , don e : fa lse }
i t . next ( ) ; 1 1
val ue : "зеленый " , don e : fa lse
i t . next ( ) ; 1 1
va lue : "голубой " , don e : fa lse }
i t . next ( ) ; 1 1
va lue : "синий " , don e : fa lse }
i t . next ( ) ; 1 1
va l ue : "фиолетовый ", don e : fa lse
i t . next ( ) ; 1 1
va l ue : undefined, done : true }
i t . next ( ) ; 1 1

Поскольку генератор rainbow возвращает итератор, мы можем также использо­
вать его в цикле for . . . of.
for ( let color o f rainbow ( ) )
console . log ( color) ;

Это выведет все цвета радуги!

В ыражения yield и двухсторонн яя свя зь
Как уже упоминалось, генераторы обеспечивают двухстороннюю связь между ге­
нератором и его вызывающей стороной. Для этого используется выражение yield.
Помните, что выражения возвращают значение, и выражение yield тоже должно
что-то возвращать. Оно возвращает аргументы (если они есть), предоставленные вы­
зывающей стороной при каждом вызове метода next итератора генератора. Давайте
рассмотрим генератор, который способен поддерживать диалог.
function* interrogate ( ) {
const name = yield " Как вас зовут ? " ;

210

Глава

12.

Итераторы и г енераторы

const color = yield " Какой ваш любимый цвет ? " ;
return ' У $ { name } любимый цвет $ { co l o r } . ' ;

Вызывая этот генератор, мы получаем итератор, но никакая часть генератора еще
не была выполнена. Когда происходит вызов метода next, он пытается выполнить
первую строку. Но поскольку в этой строке содержится выражение yield, генератор
должен возвратить управление вызывающей стороне. Вызывающая сторона должна
снова вызвать метод next, прежде чем первая строка будет выполнена и переменной
name будет присвоено значение, которое было передано в next. Вот как это будет вы­
глядеть, когда мы запустим этот генератор до конца.
const it = interrogate ( ) ;
/ / { va l u e : "Как в а с зовут ? " , don e : fa lse }
i t . next ( ) ;
i t . next ( ' Коля ' ) ; / / { va l u e : "Какой ваш любимый цвет ? " don e : fa l s e }
i t . nехt ( ' оранжевый ' ) ; / / { val u e : "У Коля любимый цвет оранжевый . " , done : true }
1

Последовательность событий при выполнении этого генератора представлена
на рис. 12. 1 .
1.

Генератор запущен и ожидает; возвращается итератор .

func t i on* interrogate ( ) {
c on s t narne = yield ' Как вас зовут ? ' ;
const color = yield ' Какой ваш любимый цвет ? ' ;
return ' у $ { narne } любимый цвет $ ( color} . ' ;

int errogate ( ) ;

const i t

2 . narne=unde f ined; возвращается строка ' Как вас зовут ? ' ; генератор ожидает .
func t i on* interrogate ( ) {
const narne = y i e l d ' Как вас зовут? ' ;
const color = yield ' Какой ваш любимый цвет? ' ;
return ' у $ ( narne } любимый цвет $ { color } . ' ;

i t . next ( ) ;

З . nаmе= ' Коля " ; воз вращается строка " Какой ваш любимый цвет ? ' ; генератор ожидает .
func t i on* interrogate ( ) {
const ·narne =· yield ' Как вас зовут ? ' ;
const color = y i e l d ' Какой ваш любимый цвет? ' ;
return ' у $ { narne } любимый цвет $ ( color } . ' ;

� i t . next ( ' Коля ' ) ;

4 . соlоr= ' оранжевый ' ; воз вращается строка ' У Коля любимый цвет оранжевый . ' ;
генератор завершает работу .
func t i on* interrogate ( )
const narne = y i e l d ' Как вас зовут ? ' ;
const color = y i e l d/' Kaкoй ваш лЮбииый цвет ? ' ;
return 'у $ ( narne } любимый цвет $ { co l or } . ' ;

1111(
1
---i•- i t . пехt ( ' оранжевый· • ) ;

Рис. 12. 1 : Пример работы генератора
Ге нераторы

211

В этом примере показано, что генераторы очень мощны; они позволяют вызыва­
ющей стороне контролировать выполнение функций. Кроме того, поскольку вызы­
вающая сторона может передать информацию в генератор, он может даже изменить
свое поведение на основании переданной информации.
Вы не можете создать генератор, используя стрелочную нотацию;
для этого следует использовать ключевое слово function * .

Генераторы и оператор return
Оператор yield сам по себе не завершает генератор, даже если это его последний
оператор. Вызов оператора return в любом месте генератора приводит к присвое­
нию значения t rue свойству done. При этом не имеет значения, что вы возвращаете,
как показано ниже.
funct ion* аЬс ( )
yield ' а ' ;
yield ' Ь ' ;
return ' с ' ;

=

count ( ) ;
const it
i t . next ( ) ;
1 1 { va l ue :
i t . next ( ) ;
1 1 { va l ue :
va l u e :
i t . next ( ) ;
11

'а , ' don e : fa lse }
,
'Ь ' done : fa lse }
' с , , done : true }

Хотя это и вполне корректное поведение генератора, имейте в виду, что при ис­
пользовании генераторов не всегда обращают внимание на свойство value, когда
done - t rue. Например, если мы будем использовать это в цикле for . . . of, "с" не
будет выведено вообще.
11 выведет "а " и "Ь " , но не "с "
for ( le t 1 of аЬс ( ) )
console . log ( l ) ;

Я не рекомендую использовать оператор return для возвращения
важного значения из генератора. Для этого лучше использовать опе­
ратор yield; а оператор return лучше использовать только для экс­
тренной остановки генератора. Поэтому я рекомендую вообще не
указывать значений в операторе return генератора.

212

Глава

12.

И тераторы и генераторы

Закл ю ч ение
Итераторы обеспечивают стандартный механизм для коллекций или объектов,
способных содержать несколько значений. Хотя итераторы не предоставляют ниче­
го такого, что нельзя было бы реализовать до появления ЕSб, они стандартизируют
действительно важное и общее действие.
Генераторы позволяют создавать намного более управляемые и изменяющие свое
поведение функции: вызывающая сторона больше не должна заранее предостав­
лять данные функции, ожидать завершения ее выполнения и получения результата.
По существу, генераторы позволяют отсрочить вычисления и выполнять их только
по мере необходимости. В главе 14 мы увидим, как они позволяют реализовать мощ­
ные схемы для управления асинхронным кодом.

За ключение

213

ГЛАВА 1 3

Ф у н к ц и и и мо щь
а б стра ктно го мышлени я

Если бы JavaScript был Бродвейской постановкой, то функции были бы блестя­
щей звездой: они постоянно купались бы в свете софитов и выходили бы на поклон
под гром аплодисментов (а иногда, без сомненья, и под свист: всем не понравишься).
Мы рассмотрели механизм функций в главе 6, а здесь рассмотрим способы примене­
ния функций и то, как они могут преобразить ваш подход к решению задач.
Сама концепция функций подобна хамелеону: в различных контекстах они про­
являют свои разные стороны. Первый (и самый простой) аспект функций - много­
кратное использование кода.

Функции как подпрограммы
Идея подпрограмм (subroutiвe)
очень старый практический подход для сниже­
ния сложности. Без подпрограмм программирование было бы весьма монотонным
делом. Подпрограммы просто упаковывают некий объем повторяемых функцио­
нальных возможностей, присваивают им имя и позволяют их выполнять в любое
время, обратившись по этому имени.
-

Подпрограммы известны также под названиями процедура (procedure),
функция (routiвe), подпрограмма (subprogram), макрос (macro) и очень
расплывчатым и обобщенным вызываемый блок (callaЫe uвit). Обрати­
те внимание, что в JavaScript мы фактически не используем слово под­
программа (subroutiвe). Мы просто называем функцию функцией (или
методом). Здесь мы употребляем термин подпрограмма, только чтобы
подчеркнуть столь простой способ использования функций.
Довольно часто подпрограмма используется для упаковки алгоритма, что являет­
ся простым и понятым способом решения данной задачи. Давайте рассмотрим алго­
ритм определения, принадлежит ли текущая дата високосном году.

const year = new Date ( ) . ge tFul lYear ( ) ;
i f ( year % 4 ! == 0 ) console . log ( ' $ { ye a r } не високосный . ' )
e l s e i f ( year % 1 0 0 ! = 0 ) console . log ( ' $ { year } високосный . ' )
e l s e i f ( year % 4 О О ! = О ) consol e . l o g ( ' $ { year } не високосный . ' )
e l s e conso l e . log ( ' { $ year } високосный ' ) ;

Вообразите, что вы должны выполнить этот код в программе 10 или даже 1 00 раз.
Теперь предположим, что понадобилось изменить формулировку сообщения, кото­
рое выводится на консоль; вам придется найти все случаи использования этого кода
и в каждом изменить по четыре строки! Это именно та проблема, которую решают
подпрограммы. В JavaScript эту потребность может удовлетворить функция.
function printLeapYearStatus ( ) {
const year = new Date ( ) . getFullYear ( ) ;
i f ( year % 4 ! == О ) console . log ( ' $ { year } не високосный . ' )
e l s e i f ( year % 1 0 0 ! = 0 ) console . log ( ' $ { ye a r } високосный . ' )
e l s e i f ( year % 4 0 0 ! = 0 ) console . log ( ' $ { year } не високосный . ' )
e l s e console . log ( ' { $ year } високосный . ' ) ;

Мы создали м1-югократно используемую подпрограмму (функцию) printLeap
YearStatus. Теперь это должно быть вам вполне знакомо.
Обратите в н имание на имя, которое мы выбр али для функции:
printLeapYearStatus. Почему не getLeapYearS tatus или leapYearStatus, или про­
сто leapYea r? Хотя эти имена были бы короче, они упускают важную деталь: эта
функция просто выводит текущее состояние високосного года. Осмысленное имя
функции - это отчасти наука, отчасти искусство. Имя - не для JavaScript, его не
заботит, какое имя вы используете. Имя - для людей (или для вас в будущем). Ког­
да вы называете функции, думайте о том, что представит себе человек, если будет
судить о функции только по ее имени. В идеале имя должно точно сообщать, что
делает функция. С другой стороны, имя функции может быть слишком подробным.
Например, мы могли бы назвать эту функцию calculateCurrentLeapYearStatusAn
dPrintToConsole, но дополнительная информация в таком длинном имени явно за­
шкаливает. Вот где начинается искусство.

Функции как подпрограммы, возвра ща ю щ ие з на ч ение
Функция printLeapYea rStatus в нашем предыдущем примере - это подпро­
грамма в обычном смысле слова: она лишь объединяет несколько функциональных
возможностей для удобства многократного использования, не более. Это простейшее
использование функций, к которому вы будете прибегать не очень часто, и даже еще
реже, когда ваш подход к программированию станет более сложным и абстрактным.
Давайте сделаем следующий шаг в направлении абстрактного мышления и рассмот­
рим функции как подпрограммы, которые возвращают значение.
21 б

Глава

13.

Функции и мощ ь абстрактного м ышления

Функция printLeapYearStatus хороша, но, когда мы начнем создавать свои про­
граммы, простого вывода на консоль часто становится недостаточно. Теперь мы хо­
тим использовать для вывода НТМL-код или выполнять запись в файл, или исполь­
зовать текущее состояние високосного года в других вычислениях. Но мы все еще не
хотим возвращаться к обстоятельному объяснению нашего алгоритма каждый раз,
когда хотим знать, является ли текущий год високосным.
К счастью, нашу функцию достаточно просто переписать (и переименовать!) так,
чтобы она стала подпрограммой, которая возвращает значение.
funct ion i sCurrentYearLeapYear ( ) {
const year = new Date ( ) . ge t FullYear ( ) ;
i f ( year % 4 ! == 0 ) return false ;
e l s e i f ( year % 1 0 0 ! = 0 ) return true ;
e l s e i f ( year % 4 0 0 ! = 0 ) return false ;
e l s e return true ;

Теперь давайте рассмотрим некоторые примеры того, как мы могли бы использо­
вать возвращаемое значение нашей новой функции.
const days inMonth =
( 3 1 , isCurrentYearLeapYear ( ) ? 2 9 : 2 8 , 3 1 , 3 0 , 3 1 , 3 0 ,
31, 31, 30, 31, 30, 31] ;
i f ( i sCurrentYearLeapYea r ( ) ) console . log ( ' Ceйчac високосный год . ' ) ;

Прежде чем двигаться дальше, давайте рассмотрим почему мы выбрали для этой
функции именно такое название. Весьма популярно начинать имена функций, кото­
рые возвращают логическое значение (или предназначены для использования в ло­
гическом контексте), с сочетания букв is. Мы также включали в имя функции слово
cиrrent (текущее). Почему? Потому что в этой функции текущая дата используется
явно. Другими словами, эта функция возвратит разные значения, если вы запустите
ее 3 1 декабря 2016 года, а затем на следующий день
1 января 2017 года.
-

Ф ункции как . . . функции
Теперь, когда мы рассмотрели некоторые из наиболее очевидных способов ис­
пользования функций, пришло время подумать о функциях как . . . о функциях. Если
бы вы были математиком, то вы думали бы о функции как о зависимости (relation)
выходных данных от входных. Любой вывод зависит от ввода. Программисты счи­
тают функции, которые придерживаются математического определения, чистыми
функциями (pure function). В некоторых языках (таких, как Haskell) допускаются
только чистые функции.
Чем такая функция отличается от функций, которые мы уже рассматривали? Во­
первых, чистая функция должна всегда возвращать одно и то же значение для одно­
го и того же набора входных данных. Функция i sCurrentYearLeapYea r не является
Функции как под п рограммы, возвращающие значение

21 7

чистой, поскольку возвращает то одно значение, то другое в зависимости от того,
когда вы ее вызовете (в один год она может возвратить t rue, а на следующий false). Во-вторых, у функции не должно быть побочных эффектов (side effect). Та­
ким образом, вызов функции не должен изменять состояние программы. В нашем
обсуждении мы еще не встречали функций с побочными эффектами (мы не считаем
вывод наконсоль побочным эффектом). Давайте рассмотрим простой пример.
const colors = [ ' красный ' , ' оранжевый ' , ' желтый ' , ' зеленый ' ,
' голубой ' , ' синий ' , ' фиолетовый ' ] ;
let colorindex = - 1 ;
function getNextRa inbowColor ( ) {
О;
i f ( ++colorindex >= colors . length) colorindex
return colors [ colorindex ] ;

Функция getNextRainbowColor возвращает каждый раз другой цвет, цикличе­
ски проходя все цвета радуги. Эта функция нарушает оба правила чистой функции:
у нее разные возвращаемые значение для одного и того же входного значения (у
нее нет аргументов, поэтому ее входного значение - ничего), и ее вызов приводит
к побочному эффекту (изменение значения переменной colorindex ) . Переменная
colorindex не является частью функции; вызов getNextRainbowColor модифициру­
ет ее, что является побочным эффектом.
Вернемся на мгновение к нашей задаче определения високосного года. Как мы
можем преобразовать свою функцию високосного года в чистую функцию? Просто!
function i s LeapYear ( year ) {
i f ( year % 4 ! == 0 ) return fal s e ;
e l s e i f ( year % 1 0 0 ! = 0 ) return true ;
e l s e i f ( year % 4 0 0 ! = 0 ) return false ;
e l s e return true ;

Эта новая функция всегда будет возвращать одно и то же значение для одного и того
же входного значения, и она не вызывает побочных эффектов, что делает ее чистой.
Наша функция getNextRainbowColor немного сложнее. Мы можем устранить по­
бочный эффект, поместив внешние переменные в замкнутое выражение.
const getNextRainbowColor = ( function ( ) {
const colors = [ ' красный ' , ' оранжевый ' , ' желтый ' , ' зеленый ' ,
' голубой ' , ' синий ' , ' фиолетовый ' ] ;
let colorindex = - 1 ;
return func t i on ( ) {
i f ( ++colorindex >= colors . lengt h ) colorindex
О;
return colors [ co lo r i nde x ] ;
};
}) () ;

218

Гла ва

13.

Функции и мощ ь абстрактного м ышления

Теперь у нас есть функция без побочных эффектов, но это все еще не чистая
функция, поскольку она не всегда возвращает один и тот же результат для одного
и того же ввода. Для устранения этой проблемы следует тщательно рассмотреть, как
мы используем данную функцию. Есть шанс, что мы будем вызывать ее цикличе­
ски, например в браузере, чтобы изменять цвет элемента два раза в секунду (о коде
для браузера мы узнаем больше в главе 1 8).
set Interval ( function ( ) {
document . querySelector ( ' . rainbow ' )
. s tyle [ ' background-color ' ] = getNextRainbowColor ( ) ;
} 500) ;
1

Выглядит не так уж и плохо, и, конечно, намерение вполне однозначно: некий
элемент HTML использует класс rainbow для циклической смены цвета. Проблема
в том, что если что-то еще вызовет метод getNextRainbowColor ( ) , то оно вмешается
в работу этого кода! Здесь стоит остановиться и задаться вопросом "Настолько ли
хорошей идеей является функция с побочными эффектами': В данном случае, веро­
ятно, лучшим выбором был бы итератор.
funct ion getRainbowi terator ( ) {
const colors = [ ' красный ' , ' оранжевый ' , ' желтый ' , ' зеленый ' ,
' голубой ' , ' синий ' , ' фиолетовый ' ] ;
let colorindex
-1;
return {
next ( ) !
i f ( ++colorindex > = colors . l engt h ) colorindex = О ;
return { value : colors [ colorindex ] , done : false } ;
=

};

Наша функция getRainbow i t e ra tor является теперь чистой: она возвращает
одно и то же каждый раз (итератор), и у нее нет никаких побочных эффектов. Мы
могли бы использовать это иначе, но так намного безопаснее.
const rainbowi terator = getRainbowiterator ( ) ;
s e t i n terva l ( funct ion ( ) {
document . querySelector ( ' . ra inbow ' )
. style [ ' background-co lor ' ]
rainbowit erator . next ( ) . va lue ;
} 500 ) ;
=

/

Вы могли бы подумать, что это только поверхностное решение проблемы: разве ме­
тод next ( ) не возвращает разные значения каждый разr Так и есть, однако вспомните,
что next ( ) является методом, а не функцией. Он работает в контексте объекта, которо­
му принадлежит, поэтому его поведение контролируется этим объектом. Если мы будем
использовать getRainbowlterator в других частях нашей программы, то они создадут
разные итераторы, которые не будут конфликтовать ни с какими другими итераторами.
Функции как под программы, возвращающие значение

219

И ч то ?
Теперь, увидев три разные шляпы, которые может носить функция (подпрограм­
ма, подпрограмма с возвращаемым знаqением и чистая функция), мы сделаем паузу
и спросим себя "И что?1 Почему эти различия имеют значение?"
Моя задача в этой главе не столько объяснить синтаксис JavaScript, как заставить
вас думать, почему именно это так. Зачем нужны функции? Рассматривая функции
как подпрограммы, можно найти один из ответов на этот вопрос: чтобы избежать
повторов. Подпрограммы позволяют упаковывать общепринятые функциональные
возможности - довольно очевидное преимущество.
Избежание повторения кода за счет упаковки является настолько ос­
новополагающей концепцией, что для нее есть собственная аббреви­
атура: DRY (don't repeat yourself - не повторяйся). Хотя она, возмож­
но, лингвистически и сомнительна (дословно - "сухой"), вы найдете,
что, описывая код, люди используют эту аббревиатуру как прилагательное. "Этот код мог бы быть более СУХИМ': Если кто-то говорит
вам это, то имеет в виду, что вы излишне повторяете функциональ­
ные возможности.
С чистыми функциями дела обстоят несколько хуже - они отвечают на вопрос
"Почему" немного более абстрактным способом. Один из ответов мог бы быть та­
ким: "Потому что они делают программирование более похожим на математику!"
Это ответ, который мог бы вызвать следующий вопрос: "И почему это хорошо?"
Наилучший ответ мог бы быть таким: "Потому что чистые функции делают код про­
ще и понятнее, облегчают его проверку и делают более переносимым':
Функции, которые возвращают разные значения при разных обстоятельствах или
имеют побочные эффекты, привязаны к своему контексту. Если у вас есть действи тельно полезная функция с побочными эффектами, например, и вы извлекаете ее
из одной программы, чтобы поместить в другую, это может не сработать. Или, что
хуже того, она может сработать в 99% случаев, а в 1 % привести к серьезной ошибке.
Любой программист знает, что неустойчивые ошибки - самый плохой вид ошибок:
они могут долго оставаться незамеченными, а когда обнаруживаются, поиск причин
их возникновения напоминает поиск иголки в стоге сена.
Если чистые функции лучше всех, то напрашивается вполне резонный вывод,
что вы всегда должны предпочитать чистые функции. Я говорю "предпочитать" по­
скольку иногда проще создать функцию с побочными эффектами. Начинающие про­
граммисты испытывают желание делать это весьма часто. Я не собираюсь отговари­
вать вас от этого, я просто рекомендую вам остановиться и подумать, можете ли вы
1

Композиция "So What" была исполнена Pink в 2008 году. - Примеч. ред.

220

Глава

13.

Функции и мощ ь абстрактного м ышления

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

Функци и являются о бъектами
В JavaScript функции являются экземплярами объекта Function. С практической
точки зрения это никак не влияет на то, как вы их используете; это просто информа­
ция к размышлению. Заслуживает внимания то, что, если вы попытаетесь идентифи­
цировать тип переменной v, оператор typeof v возвратит для функций " function".
Это весьма разумно, в отличие от случая, когда v является массивом: он возвра­
тит " obj ect " . В результате вы можете использовать оператор typeo f v для иден­
тификации функции. Обратите, однако, внимание, что если v
это функция, то
v instanceof Obj ect даст истину. Поэтому, если вам нужно отличать функции
от объектов других типов, то для проверки сначала используйте оператор typeof.
-

Немедленно вызываемое функциональное
выражение и асинхронны й код
Мы познакомились с немедленно вызываемыми функциональными выражения­
ми (IIFE) в главе 6 и увидели, что они позволяют создавать замкнутые выражения.
Давайте рассмотрим важный пример (к которому мы вернемся в главе 14) того, как
IIFE может помочь нам с асинхронным кодом.
Один из первых случаев использования IIFE подразумевает создание новых пе­
ременных в новых областях видимости, чтобы асинхронный код выполнялся пра­
вильно. Рассмотрим классический пример таймера, который осуществляет обратный
отсчет от 5 секунд до О (команда "Старт ! " ). В этом коде использована встроенная
функция setTimeout, которая задерживает выполнение ее первого аргумента (функ­
ции) на второй аргумент (количество миллисекунд). Например, следующий код вы­
водит строку "Приве т ! " через 1 ,5 секунды.
s e tTimeout ( funct ion ( )

{ console . log ( "Привет ! " ) ; }

,

1500) ;

Теперь, обладая этим знанием, создадим нашу функцию обратного отсчета.
var i ;
for ( i=5 ; i >= O ; i - - ) {
setTimeout ( funct ion ( )
console . log ( i===O ? "Старт ! "
( 5-i ) * 1 0 0 0 ) ;
}

i) ;

,

Немедленно вызы ваемое фун кциональное вы ражение и аси нхронный код

221

Обратите внимание, что здесь мы используем var вместо l et . Давайте рассмот­
рим, почему IIFE так важны. Если вы ожидаете увидеть вывод 5, 4 , 3, 2, 1 , "Старт ! ",
то будете разочарованы. Вместо этого вы найдете шесть раз выведенное число 1
Дело в том, что функция, передаваемая setTimeout, вызывается не сразу в цикле,
а через некоторое время. Таким образом, цикл выполнится начиная с i, равного 5,
и в конечном счете достигнет 1 . еще до того, как любая из функций будет вызвана.
Таким образом, на момент вызова функции i будет иметь значение - 1 .
Даже при том, что область видимости уровня блока ( с переменными let), п о су­
ществу, решает эту проблему, данный пример все еще очень важен, если вы новичок
в асинхронном программировании. Трудно объять необъятное, но понимание асин­
хронного выполнения критически важно (глава 14).
Без применения переменных области видимости уровня блока для решения этой
задачи нужно было использовать другую функцию. При использовании дополни­
тельной функции создается новая область видимости, и значение i может быть "за­
фиксировано" (в замкнутом выражении) на каждом этапе. Рассмотрим сначала ис­
пользование именованной функции.
-

-

.

..

function loopBody ( i) {
setTimeout ( function ( )
console . lo g ( i===O ? " Старт ! "
} ' ( 5- i ) * 1 0 0 0 ) ;

i) ;

var i ;
for ( i= S ; i > O ; i - - )
loopBody ( i ) ;

На каждом этапе цикла вызывается функция loopBody. Напомню, что в JavaScript
аргументы передаются функции при вызове по значению. Таким образом, на каж­
дом этапе функции передается не переменная i, а ее значение. Вначале передается
значение 5, во второй раз - значение 4 и т.д. Не имеет значения, что в обоих местах
мы используем имя переменной (i): по существу, мы создаем шесть разных областей
видимости и шесть независимых переменных (одну для внешней области видимости
и пять для каждого из вызовов loopBody).
Но все же создание именованной функции для цикла, который вы собираетесь
использовать только однажды, довольно утомительное занятие. Задействуйте IIFE:
они, по существу, создают эквивалентные анонимные функции, которые вызываются
немедленно. Вот как предыдущий пример выглядит с IIFE.
var i ;
for ( i=S ; i > O ; i-- ) {
( function ( i ) {
setTimeout ( funct ion ( )
console . log ( i===O ? " Старт ! "

222

Глава

1 3.

i) ;

Функции и мощь абстрактного мышления

}
} ) (i) ;
,

( 5- i ) * 1 0 0 0 ) ;

Как много скобок! Если проанализировать их, то можно заметить, что здесь про­
исходит то же самое: мы создаем функцию, которой передается один аргумент, и она
вызывается на каждом шаге цикла (рис. 1 3. 1 ) .

var i ;
for ( i=S ; i>O ; i-- ) {
loopBody ( i ) ;
}

var i ;
for ( i=S ; i>O ; i-- ) {
( function ( i ) {
setTimeout ( function ( ) {
console . log ( i===O ? "Старт ! "
} , ( 5-i ) * l O O O ) ;
}) (i) ;
}

i) ;

Вызов именованной функции заменен анонимной

Рис. 13. 1 . Немедленно вызываемое функциональное выражение

Переменные области видимости блока решают эту задачу без введения дополни­
тельной функции, чтобы создать новую область видимости. Использование переменных области видимости блока существенно упрощает этот пример.
for ( let i=5 ; i > O ; i - - ) (
setTimeout ( function ( )
console . log ( i===O ? " Старт ! "
} , (5-i) * 1 00 0 ) ;

i) ;

Обратите внимание, что мы используем ключевое слово let в аргументах цикла
for. Если бы мы поместили его вне цикла for, у нас была бы та же проблема, что
и прежде. Таким образом, использование ключевого слова let сообщает JavaScript,
что на каждом этапе цикла должна быть новая, независимая копия переменной i .
В результате, когда функции, переданные s e t T ime out, выполняются в будущем,
они каждый раз получают свое значение переменной в ее собственной области ви­
димости.

Пе ременные функций
Если вы новичок в программировании, можете налить себе еще кофе2 и усесть­
ся поудобнее: в этом разделе рассматривается концепция, которая очень важна, но
у новичков зачастую вызывает затруднения.
2 Имеется в виду композиция "Another Cup of Coffee" группы Mike And The Mechanics.

-

Примеч. ред.

Переменные функций

223

Проще всего считать числа, строки и даже массивы обычными переменными. Эта
концепция приводит нас к очень удобной идее, что переменная - это фрагмент дан­
ных (набор, или коллекция данных, в случае массива или объекта). Если размыш­
лять именно так о переменных, то будет куда сложнее понять и реализовать весь
потенциал функций, поскольку вы можете передавать функцию в виде параметра
другой функции точно так же, как если бы это была любая другая переменная. По­
скольку функции активны, мы не думаем о них как о фрагментах данных (которые
мы считаем пассивными). И это правда, функция активна, когда ее вызывают. Одна­
ко до вызова . . . она пассивна, точно так же, как любая другая переменная.
Вот аналогия, которая могла бы вам помочь. Если вы идете в супермаркет, то
можете считать фрукты фрагментами данных: 2 банана, 1 яблоко и т.д. Вы хотите
сделать коктейль из фруктов, поэтому покупаете еще и блендер. Блендер больше по­
хож на функцию: он делает нечто (а именно - измельчает фрукты в коктейль). Но
пока блендер находится в вашей корзине (выключенный и ничего не смешивающий),
это только еще один товар в вашей корзине, и вы можете сделать с ним то же, что
и с фруктами: переложить из корзины на кассовую ленту, заплатить за него, поло­
жить в хозяйственную сумку, отнести домой и т.д. И только когда вы подключите
его к сети, загрузите фруктами и включите, он станет чем-то отличным от фруктов.
Таким образом, функция применима везде, где применима переменная, но что это
значит? В частности, это значит, что вы можете делать следующие вещи.


Присвоить функции псевдоним, что создаст переменную, которая указывает
на нее.



Поместить функцию в массив (возможно, смешанный, из данных других типов).



Использовать функцию как свойство объекта (см. главу 9).



Передать функцию в функцию.



Возвратить функцию из функции.



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

Голова еще не кружится? Этот список действительно кажется очень абстрактным,
и вы, естественно, могли бы задаться вопросом "С какой стати я должен это исполь­
зовать?" На самом деле эта гибкость невероятно мощна, и все эти возможности при­
меняются весьма часто.
Давайте начнем с самого простого элемента этого списка: присвоения функции
псевдонима. Предположим, что у вас есть функция с очень длинным именем и вы
хотите использовать ее многократно в пределах нескольких строк. Вы быстро устане­
те вводить такое длинное название функции, да и читать код будет очень трудно. По­
скольку функция - это только тип данных, как и любой другой, вы можете создать
новую переменную с более коротким именем.
224

Глава

1 3.

Функции и мощь абстрактного мышления

function addThreeSquareAddFiveTakeSquareRoot ( x ) {
1 1 это совсем бестолковая функция, не так ли?
return Math . sqrt ( Math . pow ( x+З , 2 ) + 5 ) ;

/ / ДО
const answer = ( addThreeSquareAddFiveTakeSquareRoot ( S ) +
addThreeSquareAddFiveTakeSquareRoot ( 2 ) ) /
addThreeSquareAddFiveTakeSqureRoot ( 7 ) ;
1 1 после
const f = addThreeSquareAddFiveT a ke SquareRoot ;
const answer = ( f ( S ) + f ( 2 ) ) / f ( 7 ) ;

Обратите внимание, что в примере "после" мы не используем круглые скобки после
имени функции addThree Squa reAddFi veTakeSquareRoot. Сделав это, мы вызвали бы
функцию, и переменная f вместо того, чтобы стать псевдонимом addThreeSquareAd
dFiveTakeSquareRoot, содержала бы результат этого вызова. Затем, когда мы попы­
тались бы использовать ее как функцию (например, f ( 5 ) ), это привело бы к ошибке,
поскольку f не была бы функцией, а вызывать вы можете только функции.
Конечно, это совершенно надуманный пример, и в действительности встречает­
ся не часто. Но это на самом деле происходит при применении пространств имен
(namespacing), что весьма распространено при разработке приложений для Node
(см. главу 20), например так.
=

require ( ' math-money ' ) ; / / require - функция Node для
/ / импорта библиотек
const oneDollar
Mone y . Dollar ( l ) ;
/ / или, если мы не хотим писать повсюду "Money . Dollar " :
const Dollar
Money . Do l l a r ;
c o n s t twoDollars = Dollar ( 2 ) ;
/ / обратите внима ние : oneDollar и twoDollars - экземпляры того же типа
const Money

=

=

В данном случае эффект от применения псевдонимов (aliasing) не так уж и велик,
Mone y . Dollar сокращается до просто Dol lar, что кажется достаточно разумным.
Теперь, завершив умственную разминку, давайте перейдем к более энергичному
абстрактному размышлению.

Функции в массиве
Исторически это программное решение использовалось не очень широко, но его
популярность растет, а при определенных обстоятельствах оно чрезвычайно полез­
но. Одно из его применений - реализация идеи конвейера (pipeline), когда есть на­
бор индивидуальных этапов, которые мы хотим выполнять часто. Преимущество ис­
пользования массивов в том, что вы можете изменять их в любое время. Необходимо

П еременные функ ций

225

удалить этапы? Достаточно удалить их из массива. Необходимо добавить этап? До­
статочно добавить его в массив.
Один из примеров - графические преобразования. Если вы создаете некое про­
граммное обеспечение для визуализации, "конвейер" преобразований будет часто
использоваться во многих местах. Вот пример обычных двумерных преобразований.
const
const
const
const
const

s in = Math . si n ;
c o s = Math . co s ;
theta = Math . PI / 4 ;
zoom = 2 ;
offset = [ 1 , - 3 ] ;

const pipeline = [
function rotate ( p )
return {
х : р . х * cos ( t heta )
р . у * s i n ( theta ) ,
у : р . х * sin ( t he t a ) + р . у * cos ( theta ) ,
};
},
functi on scale ( p ) {
return { х : р . х * zoom, у : р . у * zoom } ;
},
funct ion translat e ( p )
return { х : р . х + offset [ O ] , у : р . у + offset [ l ] ; } ;
},
];
-

/ / pipeline - это ма ссив функций для определенного двумерного
/ / преобразования, теперь мы можем преобразовать точку:
cons t р = { х : 1 , у : 1 } ;
let р2 = р ;
for ( l e t i=O ; i х ;
return arr . reduce ( ( a , х ) = > а += f ( x ) , О ) ;
sum ( [ 1 , 2 , 3 ] ) ;
1 1 возвращает б
/ / возвращает 1 4
sum ( [ l , 2 , 3 ] , х => х * х ) ;
s um ( [ l , 2 , 3 ] , х = > Math . pow ( x , 3 ) ) ; 1 1 возвращает 3 6

При передаче произвольной функции в sum мы можем заставить ее сделать . . . все,
что хотим. Нужна сумма квадратных корней'? Никаких проблем. Нужна сумма чисел,
возведенных в степень 4,233'? Проще простого. Обратите внимание, что мы хотим
предусмотреть возможность простого вызова функции sum, т.е. не делая ничего спе­
циально и не передавая ей никакой функции. В функции параметр f имеет значение
unde fined, и если мы попытаемся его вызвать, то получим ошибку. Чтобы предот­
вратить это, мы превращаем нечто, не являющееся функцией, в "пустую функцию':

Переменные функций

227

которая в основном ничего не делает. Таким образом, если вы передаете ей 5, она
возвращает 5, и т.д. Есть более эффективные способы справиться с этой ситуаци­
ей (такой, как вызов другой функции без дополнительных затрат на вызов пустой
функции для каждого элемента), но это хорошая практика, чтобы научиться созда­
вать "безопасные" функции таким способом.

В озвращение функции из функции
Возвращение функции из функции является, вероятно, не только самым загадоч­
ным способом использования функций, но и чрезвычайно полезным. Это можно
сравнить с трехмерным принтером: это вещь, которая делает нечто (как функция),
что может, в свою очередь, делать нечто, что также делает что-то. Но самая захваты­
вающая часть здесь в том, что функция, которую вы возвращаете, может быть видо­
изменена - очень похоже на то, как вы можете изменить то, что печатаете на трех­
мерном принтере.
Давайте предположим, что нашей функции sum, как и ранее, может передаваться
(а может и нет!) функция для обработки каждого элемента массива, до того, как они
будут просуммированы. Помните, как мы упоминали, что могли бы создать отдельную
функцию sumOfSqua res, если бы захотели? Давайте рассмотрим ситуацию в которой
такая функция необходима, т.е. когда передавать в функцию и массив и другую функ­
цию нежелательно. Нам явно нужна функция, которой передается только один массив
значений, а она возвращает сумму его квадратов. (Если вы задаетесь вопросом, когда
такое обстоятельство могло бы возникнуть, рассмотрите проект некого API, в котором
вам разрешено создавать функции типа sum, только с одним аргументом.)
Один из подходов подразумевает создание новой функции, которая просто вы­
зывает нашу старую функцию.
function sumOfSquares ( arr) {
return sum ( arr, х => х * х ) ;

Данный подход, конечно, прекрасен и вполне может сработать, если все, что не­
обходимо, - это одна функция, но что если необходимо повторять этот шаблон раз
за разом? Решением нашей проблемы могло бы быть создание функции, которая воз­
вращает специализированную функцию.
funct i on newSumme r ( f ) {
return arr => sum ( ar r , f ) ;

Эта новая функция, newSumme r, создает совершенно новый вариант функции sum,
которая имеет только один аргумент, но в ней используется специальная функция.
Давайте рассмотрим, как мы могли бы использовать это для получения различных
видов сумм.
228

Глава

1 3.

Функции и мощ ь абстрактного м ышления

const sumOfSquares = newSumme r ( x => х * х ) ;
const sumOfCube s = newSummer ( x = > Math . pow ( x , 3 ) ) ;
sumOfSquares ( [ 1 , 2 , 3 ] ) ;
/ / возвращает 1 4
sumOfCube s ( [ l , 2 , 3 ] ) ;
/ / в озвращает 3 6

Эта методика, когда м ы берем функцию с несколькими аргументами
и преобразуем ее в функцию с одним аргументом, называется каррин­
гом (currying) в честь ее разработчика, американского математика Ха­
скелла Брукса Карри ( Haskell Curry).
Случаи возвращения функции из функции зачастую глубоки и сложны. Если вы
хотите увидеть больше примеров этого, взгляните на пакеты приложения среднего
уровня для Express или Коа (популярные среды веб-разработки JavaScript).

Рекурсия
Другой весьма распространенный и важный способ использования функций это рекурсия (recursion), когда функция вызывает саму себя. Это особенно мощная
методика, когда функция делает то же самое с постепенно уменьшающимися набо­
рами данных.
Давайте начнем с вымышленного примера: поиска иголки в стоге сена. Если бы
у вас были реальный стог сена и игла, которую нужно найти в нем, то реальный под­
ход мог бы быть таким.
1. Если вы можете увидеть иглу в стоге сена, перейти к п. 3.
2. Удалите часть сена из стога. Перейти к п. 1 .
3 . Готово!

Вы просто каждый раз уменьшаете размер стога сена, пока не находите иглу; это
и есть рекурсия. Давайте посмотрим, как преобразовать этот пример в код.
function findNeedle ( haystack) {
i f ( haystac k . length === 0 ) return " Здесь иголки нет ! " ;
i f ( ha ystac k . shift ( ) === ' иголка ' ) return " Нашли ! "
r e turn findNeedle ( ha ystack ) ; / / стог сена уменьшился на один элемент

findNeedle ( [ ' сено ' ,

' сено ' ,

' сено ' ,

' сено ' ,

' иголка ' ,

' сено ' ,

' сено ' ] ) ;

В этой рекурсивной функции важно обратить внимание на то, что она учитывает все
возможности: если массив haystack пуст (когда негде и ничего искать), когда иголка первый элемент в массиве (готово!) или не первый (она находится где-то в остальной
части массива, поэтому мы удаляем первый элемент и повторяем функцию; помните, что
Array . prototype . shi ft удаляет первый элемент из массива по месту).

Рекурсия

229

Важно, что у рекурсивной функции обязательно должно быть условие остановки
(stopping condition) ; без него она продолжит вызывать саму себя до тех пор, пока
интерпретатор JavaScript не решит, что стек вызовов стал слишком большим (что
приведет к аварийному завершению программы). В нашей функции findNeedle есть
два условия выхода: когда иголка найдена и когда стог закончился. Поскольку мы
уменьшаем размер стога каждый раз, в конечном счете мы неизбежно достигнем од­
ного из этих условий остановки.
Давайте рассмотрим более полезный, проверенный временем пример: поиск
факториала числа. Факториал числа - это число, умноженное на все числа до него
и обозначаемое восклицательным знаком после числа. Таким образом 4! вычисляется
как 4 х 3 х 2 х 1 24. Вот как мы реализовали бы это в виде рекурсивной функции.
=

function fact ( n ) {
i f ( n === 1 ) return 1 ;
return n * fact ( n - 1 ) ;

Здесь есть условие остановки (n === 1), и каждый раз, делая рекурсивный вызов,
мы уменьшаем значение n на единицу. Так мы в конечном счете доберемся до 1 (эта
функция не будет правильно работать, если вы передадите ей О или отрицательное
число, хотя, конечно, мы могли бы добавить ряд проверок, чтобы этого не случилось).

З акл ючение
Если у вас есть опыт работы с другими функциональными языками програм­
мирования, такими как ML, Haskell, Clojure или F#, эта глава вряд ли была для вас
сложной. В противном случае она, вероятно, расширила ваш круrозор, и вы узнали
немного больше об абстрактных возможностях функционального программирова­
ния (поверьте, впервые столкнувшись с этими идеями, я был, конечно, удивлен). Вы
могли бы быть поражены количеством способов, которыми можно достичь той же
цели, и задаться вопросом "Какой путь лучше?" Боюсь, что простого ответа на этот
вопрос нет. Зачастую он зависит от решаемой задачи: некоторые из них имеют толь­
ко определенную методику. Многое зависит и от вас: какие методики вам нравятся?
Если представленные в этой главе методики ставят вас в тупик, то я рекомендую
перечитать ее несколько раз. Изложенные здесь концепции чрезвычайно мощны,
и единственный способ уяснить, какие из них будут полезны именно для вас, - это
внимательно изучить их и понять.

230

Гла ва 1 З. Функции и мощь абстрактного мышления

ГЛАВА 1 4

А синхронное про г раммиро в ание

Мы упоминали асинхронное программирование в главе 1 , когда обсуждали взаи­
модействие с пользователем. Помните, что взаимодействие с пользователем, вполне
естественно, является асинхронным: вы не можете контролировать, когда пользо­
ватель щелкнет, коснется, скажет или введет. Однако пользовательский ввод - не
единственная причина для асинхронного выполнения программ: сама природа
JavaScript вынуждает к этому во многих случаях.
Приложение JavaScript выполняется в одном потоке (single-threaded). Таким об­
разом, JavaScript делает только что-то одно за один раз. Большинство современных
компьютеров способно выполнять одновременно несколько операций (подразумева­
ется, что у них есть несколько ядер), и даже компьютеры с одним ядром настолько
быстры, что могут имитировать одновременное выполнение нескольких задач, вы­
полняя сначала часть задачи А, затем - небольшую часть задачи В, затем - задачи
С и так до тех пор, пока все задачи не будут выполнены (это так называемый муль­
типрограммный режим работы с приоритетами (preemptive multitasking)). С точ­
ки зрения пользователя, задачи А, В и С выполняются одновременно, как будто они
фактически выполняются одновременно на нескольких ядрах.
Таким образом, однопоточный характер движка JavaScript мог бы показаться
ограничивающим фактором, но фактически это освобождает нас от заботы о некото­
рых очень сложных проблемах, присущих многопоточному программированию. Эта
свобода имеет цену: она означает, что для написания корректно выполняющегося
программного обеспечения нужно мыслить асинхронно, и не только о пользователь­
ском вводе. Поначалу так мыслить может быть трудно, особенно если вы переходите
с языка программирования, в котором выполнение обычно происходит синхронно.
В JavaScript с самого начала был заложен механизм для асинхронного выполнения
программ. Однако по мере роста популярности JavaScript (и изощренности создавае­
мого на нем программного обеспечения) в него были добавлены новые конструкции
для асинхронного программирования. Фактически мы можем считать, что JavaScript
имеет три разные фазы асинхронной поддержки: фазу обратного вызова, фазу обя­
зательства и фазу генератора. Если бы это был вопрос только генераторов, ставших
лучше, чем прежде, мы объяснили бы, как они работают, и двинулись бы дальше.

Но не тут-то было. Генераторы сами по себе не обеспечивают никакой асинхронной
поддержки: для обеспечения асинхронного поведения они полагаются либо на обя­
зательства (в русскоязычной документации MDN они названы обещаниями), либо
на специальный тип функций обратного вызова. Аналогично обязательства сами
по себе столь же полезны, но они полагаются на обратные вызовы (а обратные вы­
зовы сами по себе очень пригодятся для таких вещей, как обработка событий).
Кроме пользовательского ввода, вы будете использовать асинхронные методики
в следующих трех областях.


Сетевые запросы (например, вызов Ajax).



Операции с файловой системой (чтение, запись в файлы и т.д.).



Преднамеренно отсроченные функциональные возможности (например, опо­
вещение).

Анало r ия
Аналогия, которую мне нравится использовать для функций обратных вызовов
и обязательств, - это попытка заполучить столик в занятом ресторане, когда вы за­
ранее не зарезервировали себе место. Таким образом, вы не должны ждать своей
очереди (в некоторых ресторанах могут взять номер вашего мобильного телефона
и перезвонить, когда столик освободится). Это похоже на функцию обратного вызо­
ва: вы предоставили менеджеру ресторана нечто, что позволит ему сообщить, когда
ваш столик освободится. Ресторан занят своими делами, и вы можете заниматься
своими; никто никого не ждет. В другом ресторане вам могут предоставить пейджер,
который сработает, когда столик будет свободен. Это больше похоже на обязатель­
ство: нечто, что менеджер ресторана дает вам, и что сообщит, когда столик будет
свободен.
Не забывайте эти аналогии, когда мы будем рассматривать обратные вызовы
и обязательства, особенно если вы новичок в асинхронном программировании.

Обратные вызовы
Обратный вызов (callback) - самый старый асинхронный механизм в JavaScript,

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

Глава

14.

А синхронное п рограммирование

Давайте начнем с простого примера использования встроенной функции
setTimeout, которая задерживает запуск программы на указанное количество мил­
лисекунд.
cons o l e . log ( "До таймаут а :
+ new Date ( ) ) ;
function f ( ) {
console . log ( "Пocлe таймаута : " + new Date ( ) ) ;
"

/ / одна минута
set Timeout ( f , 6 0 * 1 0 0 0 ) ;
console . log ( " Эт o произошло после вызова setTimeout ! " ) ;
console . log ( "И это тоже ! " ) ;

Если запустить данный пример на консоли (если только вы не очень медленно его
вводите), можно увидеть нечто такое.
До таймаута : Sun Aug 02 2 0 1 5 1 7 : 1 1 : 32 GMT- 0 7 0 0 ( Pa c i f i c Daylight Time )
Это произошло после вызова setTimeout !
И это тоже !
После т аймаута : Sun Aug 02 2 0 1 5 1 7 : 1 2 : 32 GMT - 0 7 0 0 ( Pa c i f i c Daylight T ime )

При'lина затруднений у новичков кроется в разрыве между линейной природой
кода, который мы пишем, и фактическим выполнением этого кода. Некоторые из нас
хотят (или ожидают), чтобы компьютер выполнял код точно в том порядке, в кото­
ром он был написан. Другими словами, мы хотели бы увидеть следующее.
До таймаута : Sun Aug 0 2 2 0 1 5 1 7 : 1 1 : 32 GMT- 0 7 0 0 ( Pa c i f i c Daylight Time )
После т аймаута : Sun Aug 0 2 2 0 1 5 1 7 : 1 2 : 3 2 GMT- 0 7 0 0 ( Pa c i f i c Daylight Time )
Это произошло после вызова setTimeout !
И это тоже !

Хотеть не вредно . . . но от этого код не стал бы асинхронным! Основной момент
асинхронного выполнения состоит в том, что оно не должно ничего блокировать.
Поскольку JavaScript имеет однопоточный характер, то если бы мы указали движку
ждать в течение 60 секунд, а затем запустить некоторый код, и сделали бы это син­
хронно, то в этот момент ничего бы не работало. Ваша программа просто "зависла"
бы на это время: она перестала бы реагировать на пользовательский ввод, не обнов­
ляла бы экран и т.д. У всех нас были подобные случаи, к сожалению. Асинхронная
техника помогает предотвратить данный вид блокировки.
В нашем примере для ясности мы использовали именованную функцию при пе­
редаче в setTimeout. Если нет серьезной причины использовать именованную функ­
цию, обычно используют анонимную функцию.
setTimeout ( funct ion ( ) {
console . log ( "Пocлe т аймаута : " + new Date ( ) ) ;
} , 60*1000 ) ;

Функция setTimeout немного проблематична, поскольку числовой параметр ин­
тервала времени является последним аргументом. При использовании анонимных
Обратные вызовы

233

функций, особенно если они длинны, его можно легко потерять, или он будет вы глядеть, как часть функции. Это достаточно распространенное явление, поэтому
вы должны привыкнуть к использованию функции setT imeout (и ее компаньон­
ки set interval ) с анонимными функциями. Только не забывайте, что в последней
строке содержится параметр задержки!

Ф ун кции setinterval и clearinterval
Кроме функции setTimeout, которая запускает свою функцию один раз и оста­
навливается, есть функция set interval, которая запускает функцию обратного вы­
зова через определенный интервал времени бесконечно или пока вы не вызовете
функцию clea r i nte rva l. Вот пример, в котором код запускается каждые 5 секунд
в течение одной минуты, или 1 0 раз, если это произойдет раньше.
const start = new Date ( ) ;
let i=O ;
const interva l i d = s e t i nt erval ( function ( )
l e t now = new Date ( ) ;
i f ( now . getMinutes ( ) ! == start . getMinut e s ( )
return clearint e rva l ( interval i d ) ;
console . log ( ' $ { i } : $ { now } ' ) ;
} 5*1000 ) ;

1 1

++i> l O )

'

Здесь мы видим, что setinterval возвращает идентификатор, который впослед­
ствии можно использовать для отмены режима интервального запуска кода. Есть со­
ответствующая функция c l ea rTimeout, которая работает так же и позволяет сбро­
сить интервал времени и предотвратить запуск кода.
Функции s e t T ime out, s et in t e rval и c l e a r inte rva l определены
в глобальном объекте (window в браузере и global в Node).

Область видимости и асинх ронное выполнение
Распространенным источником беспорядка (и ошибок) в асинхронном выполне­
нии является то, как области видимости и замкнутые выражения влияют на асин хронное выполнение. Каждый раз, вызывая функцию, вы создаете замкнутое выра­
жение: все переменные, которые создаются в функции (включая аргументы), сущес­
твуют, пока что-то может к ним обращаться.
Мы видели этот пример прежде, но его имеет смысл повторить для важного уро­
ка, который мы можем из него извлечь. Рассмотрим пример функции countdown.
Наша цель - создать 5-секундный обратный отсчет.
funct ion countdown ( } {
/ / заметьте, что мы объявляем l e t за пределами цикла for
let i ;

234

Глава

1 4.

Асинхронное программирование

соnsоlе . lоg ( " Обратный отсчет : " ) ;
for ( i=5 ; i>=O ; i - - ) {
setTimeout ( function ( )
console . log ( i===O ? " Старт ! "
} , (5-i) *lOOO) ;

i) ;

countdown ( ) ;

Давайте сначала пройдем этот пример мысленно. Вы, вероятно, помните, что здесь
что-то не так. Все выглядит так, как будто мы выполняем обратный отсчет от 5 до О.
Вместо этого получаем шесть раз по - 1 и без вывода строки " Старт ! " . Мы уже видели
это, когда использовали var; на сей раз мы используем let, но в объявлении за предела­
ми цикла for, поэтому возникает та же проблема: цикл for быстро выполняется полно­
стью, оставляя i со значением 1 , и только затем запускает на выполнение функцию
обратного вызова. Проблема в том, что, когда она выполняется, i уже имеет значение -1.
Важный урок здесь заключается в способе, которым область видимости и асин­
хронное выполнение влияют друг на друга. Вызывая countdown, мы создаем замкну­
тое выражение, которое содержит переменную i. Все (анонимные) обратные вызовы,
которые мы создаем в цикле for, имеют доступ к той же переменной i.
Суть этого примера в том, что в цикле for мы видим i, используемую двумя разны­
ми способами. Когда мы используем ее для вычисления периода ( ( 5- i ) * 1 О О О), все рабо­
тает как ожидалось: первый период
О, второй период 1 000, третий период 2 0 0 0
и т.д. Это потому, что вычисление происходит синхронно. Фактически вызов функции
s etTimeout также синхронен. В ней выполняются некие вычисления, позволяющие точ­
но определить момент запуска функции обратного вызова. Асинхронная часть - это
функция, которая передается функции setTimeout, и именно здесь кроется проблема.
Напомню, что мы можем решить эту проблему, используя немедленно вызывае­
мое функциональное выражением (IIFE), или еще проще, переместив объявление i
в объявление цикла for.
-

-

-

-

function countdown ( ) {
соnsоlе . lоg ( " Обратный отсчет : " ) ;
/ / теперь i имеет обла сть видимости блока
for ( le t i=5 ; i >= O ; i - - ) {
setTimeout ( funct ion ( )
console . log ( i===O ? " Старт ! " : i ) ;
} ' (5-i) *1000) ;

countdown ( ) ;

Урок здесь состоит в том, что нужно помнить области видимости, в которых объ­
являются ваши функции обратного вызова: у них будет доступ ко всему в этих об­
ластях видимости (замкнутое выражение). Именно из-за этого значение переменной
может быть отличным от ожидаемого, когда функция обратного вызова выполняется
Обратные вызовы

235

фактически. Этот принцип применим ко всем асинхронным методикам, а не только
к обратным вызовам.

П ередача оши бок функци я м обратного вызова
В некий момент роста популярности среды Node было принято соглашение об ис­
пользовании первого аргумента в функции обратного вызова для передачи ей ошибок
(error-first callback). Поскольку, как мы вскоре увидим, механизм обратных вызовов за­
трудняет обработку исключений, нужен стандартный способ сообщения о проблеме,
возникшей в момент запуска функции обратного вызова. Соглашение подразумевает
использование первого аргумента функции обратного вызова для доступа к объекту
ошибки. Если значение этого аргумента nul l или unde fined, никакой ошибки не было.
Всякий раз, когда вы имеете дело с функцией обратного вызова с первым аргу­
ментом для передачи ошибки, первое, что нужно сделать, - проверить его на на­
личие ошибки и выполнить соответствующее действие. Рассмотрим попытку чтения
содержимого файла в Node, приводящую к ошибке, для обработки которой исполь­
зуется соглашение о передаче ошибок функциям обратного вызова.
const fs

=

require ( ' fs ' ) ;

const fname = ' may_or_may_not_exi s t . txt ' ;
f s . readFile ( fname , function ( er r , d a t a ) {
i f { err) return console . error ( ' Oшибкa при чтении файла $ { fname } : $ { err . me s ­
sage } ' ) ;
console . log ( ' $ { fname } содержит : $ { da t a } ' ) ;
});

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

и мы выводим на консоль сообщение об этом, а затем немедленно выходим (метод
cons o l e . error не возвращает никакого смыслового значения, и мы не используем
его ни коим образом, поэтому мы можем все объединить в одном операторе). При
использовании описанного выше механизма наиболее часто допускаемой ошибкой,
вероятно, является случай, когда программист, после проверки, а возможно, и выво­
да сообщения об ошибке, забывает о том, что нужно немедленно выйти из функции.
Если этого не сделать и позволить функции продолжить выполняться, она будет счи­
тать, что в момент ее вызова не возникло никаких проблем, соответственно резуль­
тат работы функции обратного вызова будет непредсказуем. Разумеется, возможен
случай, когда в функции обратного вызова предусмотрена специальная ветка, кото­
рая должна выполняться в случае ошибки. Тогда после анализа переданного аргу­
мента на предмет ошибки и ее фиксации можно продолжить выполнение функции.
Соглашение о передаче ошибок в функцию обратного вызова стало де-факто стан дартом при разработке программ для Node (когда обязательства не используются),
236

Глава

14.

Асинхронное программирование

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

П рокл ять е обратны х вызовов
Хотя обратные вызовы позволяют управлять асинхронным выполнением, у них
есть практический недостаток: они с трудом справляются с ситуацией, когда необ­
ходимо ждать завершения нескольких процессов перед продолжением. Вообразите
случай, когда вы пишете приложение для Node, которое должно получить содержи­
мое трех разных файлов, а затем выждать 60 секунд, прежде чем объединить содер­
жимое этих файлов и записать в четвертый файл.
const fs

=

require ( ' fs ' ) ;

fs . readFile ( ' a . txt ' , function ( er r , dataA) {
i f ( err) console . error ( err ) ;
fs . readFile ( ' b . txt ' , funct ion ( err , dataB)
i f ( er r ) cons o l e . error ( err) ;
f s . readFile ( ' c . txt ' , funct ion ( er r , dataC )
i f ( err) console . error ( err) ;
setTimeout ( function ( ) {
fs . writeFile ( ' d . txt ' , dataA+dataB+dataC, function ( er r )
i f ( err) console . error ( err ) ;
}

,

{

});
60*1000 ) ;

});
});
});

Программисты назьmают это проклятьем обратных вызовов (callback hell). Его признаком
является треугольная форма блоков кода, образованных вложенными фигурными скобками.
Проблема обработки ошибок здесь стоит еще острее. Все, что мы делаем в этом примере, это
регистрируем ошибки, но если бы мы попьттались сгенерировать исключение, то были бы не­
приятно удивлены. Рассмотрим следующий упрощенный пример.
-

const fs
require ( ' fs ' ) ;
funct i on readSket chyFil e ( ) {
try {
fs . readFil e ( ' does_not_exist . txt ' , function ( er r , data ) {
i f ( err) throw e r r ;
});
catch ( er r ) {
соnsоlе . lоg ( ' Внимание : возникли небольшие проблемы, продолжаем
вьmолнение программы ' ) ;
=

readS ketchyFi le ( ) ;

На первый взгляд, все кажется достаточно резонным и не вызывает неодобрения
у программистов, использующих обработку исключений. Но только это не будет ра­
ботать. Давайте опробуем эту программу. Она завершится аварийно, даже при том
что мы проявили столько заботы, чтобы гарантировать отсутствие проблем из-за
этой почти ожидаемой ошибки. Дело в том, что блоки try . . . catch работают только
в пределах одной и той же функции. Блок t ry . . . catch находится в readSketchyFi le,
а ошибка возникает в анонимной функции, которую fs . readFi le вызывает как функ­
цию обратного вызова.
Кроме того, нет никакой гарантии, чтофункция обратного вызова не будет вы­
звана несколько раз (или вообще ни разу!). Если при написании программы вы пред­
полагаете, что она должна вызываться только один раз, в самом языке не предусмо­
трено никаких средств контроля за этим процессом.
Эта проблема вполне преодолима, но с распространением асинхронного кода она
делает написание удобного в сопровождении и безошибочного кода весьма затруд­
нительным. Вот здесь и пригодятся обязательства.

Обязательства
Обязательства1 (promise) пытаются устранить некоторые из недостатков функ­

ций обратного вызова. Используя обязательства (хотя это и не всегда просто), мож­
но получить более безопасный и "простой в сопровождении" код.
Обязательства не заменяют функций обратного вызова; фактически с обязатель­
ствами вы все еще должны использовать обратные вызовы. Что на самом деле делают
обязательства, так это гарантируют единообразный и предсказуемый способ обработ­
ки обратных вызовов, устраняя некоторые из нежелательных неожиданностей и труд­
но обнаруживаемых ошибок, которые можно получить, используя только функции об­
ратного вызова.
Основная идея обязательств проста: когда вы вызываете асинхронную функцию
на базе обязательства, она возвращает экземпляр объекта P rorni s e. С этим обяза­
тельством могут случиться только две вещи: оно может быть выполнено (fulfilled)
(в случае успеха) или отклонено (rejected) (в случае неудачи). Вам гарантирует­
ся, что произойдет только одно из этих событий (обязательство не может сначала
быть выполнено, а затем отклонено) и будет получен только один результат. Если
1 В русскоязычной документации MDN термин promise переведен как обещание. Однако по смыс­
лу, который вложили в этот термин разработчики языка, - это именно обязательство! Имеется в
виду, что создавая обязательство и возвращая его в исходный код, интерпретатор JavaScript обя­
зуется в дальнейшем при наступлении нужного события либо выполнить его, либо отклонить.
Причем только однократно! Русскоязычный же термин обещание несет на себе некий оттенок не­
обязательности, который плохо сочетается со строгими рамками работы языка программирования.
Поэтому в дальнейшем в книге мы будем использовать термин обязательство. К тому же это одно
из значений английского слова promise. Переводчики документации MDN почему то решили взять
его самое первое значение. - Примеч. ред.

238

Глава

1 4.

Асинхронное программирование

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

Создание обя зательств
Создание обязательств является простым делом: вам нужно создать новый экзем­
пляр объекта Promi s e с функцией, которой передаются две функции обратного вы­
зова resolve (выполнено) и rej ec t (отклонено) (я предупреждал вас, что обязатель­
ства не спасают от обратных вызовов!). Давайте возьмем нашу функцию countdown,
параметризируем ее (чтобы не зацикливаться только на 5-секундном обратным от­
счете) и сделаем так, чтобы она возвратила обязательство, когда обратный отсчет
начнется.
function countdown ( seconds )
return new Promise ( function ( resol ve , rej e c t ) {
for ( let i=seconds ; i>= O ; i - - )
setTimeout ( function ( ) {
i f ( i> O ) console . log ( i + ' . .
);
e l s e re solve ( consol e . log ( " Cтapт ! " ) ) ;
}
( seconds- i ) * 1 0 0 0 ) ;
.

'

,

});

Прямо сейчас эта функция не очень гибка. Дело в том, что нам не всегда требуется
вербальный вывод, более того, нам не всегда требуется выводить что-то на консоль.
Такой подход совершенно не годится, если мы планируем выводить информацию
на веб-странице, модифицируя соответствующий элемент DOM с помощью нашей
функции обратного отсчета. Но это только начало... и демонстрация создания обя­
зательств. Обратите внимание, что resol ve (как и rej ect) является функцией. Вы
могли бы подумать "Ха-ха! Я могу вызвать resolve несколько раз и нарушить ... обя­
зательство обязательств': Вы действительно можете вызывать функции resol ve или
re j ect многократно или даже попеременно ... но будет учитываться только их пер­
вый вызов. Обязательство гарантирует, что кто бы его ни использовал, он получит

Обяза теnьства

239

только одно событие - выполнение или отклонение (в настоящее время в нашей
функции нет ветки для реализации отклонения).

И спользование обязательств
Давайте посмотрим, как мы можем использовать свою функцию countdown. Мы
могли бы только вызвать ее и проигнорировать обязательство вообще: countdown ( 5) .
В результате мы также получим свой обратный отсчет и можем не возиться с обяза­
тельствами вообще. Но что если необходимо воспользоваться преимуществами обя­
зательств? Вот как мы используем возвращаемое обязательство.
countdown ( 5 ) . then (
function ( ) {
соnsоlе . lоg ( " Обратный отчет завершен " ) ;
},
function ( err) {
consol e . log ( " Oшибкa при обратном отсчете : " + err . me s s a ge ) ;
);

В этом примере мы не потрудились присвоить возвращенное обязательство пере­
менной; мы просто вызвали его (метод then) обработчик непосредственно. Этому
обработчику передается две функции обратного вызова: первая вызывается при вы­
полнении обязательства (т.е. при нормальном завершении), а вторая - при его от­
клонении (т.е. при возникновении ошибки). Причем вызвана будет только одна из
этих функций. Обязательства поддерживают также обработчик catch. Таким обра­
зом, вы можете разделить эти два обработчика (мы также сохранили обязательство
в переменной, чтобы продемонстрировать это).
cons t р = countdown ( 5 ) ;
p . then ( function ( ) {
соnsоlе . lоg ( " Обратный отчет завершен " ) ;
});
p . catch ( functi on ( e rr ) {
consol e . log ( " Oшибкa при обратном отсчете : " + err . me s sage ) ;
});

Давайте изменим нашу функцию countdown так, чтобы создать условие для воз­
никновения ошибки. Предположим, что мы суеверны и считаем ошибкой случай,
если при счете встретится число 1 3 .
funct ion countdown ( se conds ) {
return new Promi s e ( funct ion ( re s olve , rej ect ) {
for ( le t i=seconds ; i>=O ; i - - ) {
s e t T imeout ( function ( ) {
i f ( i=== l З ) return r e j ect ( new Еrrоr ( " Принципиально это не
считаем ! " ) ) ;

240

Глава

1 4.

Асинхронное программирование

}) ;

Давайте поэкспериментируем с этим примером. Обратите внимание на его инте­
ресное поведение. Вполне очевидно, что вы можете считать в обратном порядке от лю­
бого числа, меньшего чем 1 3, и поведение будет обычным. Обратный отсчет от 13 или
больше должен завершиться неудачей, когда дойдет до 1 3. Однако ... вывод на консоль
будет продолжаться! Вызов rej ect (или resol ve) не останавливает нашу функцию; он
только управляет состоянием обязательства.
Конечно, наша функция countdown нуждается в некоторых усовершенствованиях.
Обычно нам не нужно, чтобы функция продолжала работать после завершения обя­
зательства (успешного или нет), а наша продолжает. Мы также уже упомянули, что
вывод на консоль не очень гибок. Он действительно не дает нам желаемого контроля.
Обязательства дают нам чрезвычайно четкий и безопасный механизм для асинхрон­
ных задач, которые либо выполняются, либо отклоняются, но они (пока что!) не предо­
ставляют никакого способа сообщения о ходе выполнения самого процесса. Таким обра­
зом, обязательство либо выполняется, либо нет. Мы никогда не узнаем, что оно выполне­
но "только на 50%': В некоторых библиотеках обязательств2 реализована очень полезная
возможность сообщать о ходе выполнения процесса, и вполне возможно, что в будущем
эти функциональные возможности появятся в JavaScript, но пока что мы должны уметь
обходиться без этого, что плавно подводит нас к следующему разделу.

События
События (event) - это еще одна старая идея, которая получила продолжение

в JavaScript. Концепция событий проста: эмиттер (источник) события передает сооб­
щение о событии, а любой, кто желает услышать (или "подписаться") об этом событии,
может сделать это. Как подписаться на событие? Используя функцию обратного вызо­
ва, конечно! Создать собственную систему событий очень просто, но Node обеспечи­
вает их встроенную поддержку. Если вы работаете в браузере, jQuery также предостав­
ляет механизм событий. Чтобы улучшить функцию countdown, мы будем использовать
класс EventEmitter от Node. Хотя вполне можно использовать EventEmit t e r с такой
функцией, как countdown, он предназначен для использоваться с классом. Таким об­
разом, мы превратим свою функцию countdown в класс Countdown.
const EventEmit t e r

=

require ( ' events ' ) . EventEmi t t e r ;

class Countdown extends EventEmit ter {
con s t ructor ( s econd s , superst i t i ous )

2 Например, Q.
Обязательства

241

super ( ) ;
t hi s . se conds = s econds ;
t hi s . super s t i t ious = ! ! super s t i t i ou s ;
go ( )

{
const countdown = t hi s ;
return new Promi s e ( funct ion ( res olve , r e j ect ) {
for ( le t i=countdown . seconds ; i>= O ; i-- ) {
setTimeout ( function ( ) {
i f ( countdown . super s t i t ious & & i=== 1 3 )
return rej ect ( new Еrrоr ( "Принципиально это не считаем ! " ) ) ;
countdown . emit ( ' ti ck ' , i ) ;
i f ( i=== O ) resolve ( ) ;
} , ( countdown . se conds - i ) * 1 0 0 0 ) ;
});

Класс Count down наследует класс EventEmitter, который позволяет ему гене­
рировать события. Метод go - это то, что фактически запускает обратный отсчет
и возвращает обязательство. Обратите внимание, что первое, что мы делаем в методе
go, - это присваиваем значение this константе countdown. Дело в том, что для по­
лучения длины обратного отсчета необходимо использовать значение this вне за­
висимости от того, суеверен ли обратный отсчет в обратных вызовах. Помните, что
this - это специальная переменная, и у нее не будет того же значения в функции
обратного вызова. Таким образом, следует сохранить текущее значение thi s, чтобы
можно было использовать его в обязательствах.
Магия происходит при вызове countdown . emi t ( ' t i ck ' , i ) . Любой, кто хочет
услышать о событии t i c k (мы могли бы называть его как угодно по своему усмотре­
нию; на мой взгляд, "tick" не хуже других), может сделать это. Давайте посмотрим,
как можно использовать этот новый, улучшенный обратный отсчет.
const с = new Countdown ( S ) ;
c . on ( ' tick ' , function ( i ) {
i f ( i > O ) console . log ( i +
});

' .

.

. );
'

c . go ( )
. then ( funct ion ( )
console . log ( ' Cтapт ! ' ) ;
})
. catch ( funct ion ( err) {
consol e . error ( er r . mes sage ) ;
})

242

Глава

1 4.

Ас инхронное программирование

Метод on класса EventEmi tter как раз и позволяет прослушивать сообщения о со­
бытии. В этом примере мы предоставляем обратный вызов для каждого события ti ck.
Если t i c k не О, мы выводим его. Затем происходит вызов метода go, который запу­
скает обратный отсчет. Когда обратный отсчет заканчивается, мы выводим строку
" Старт ! " . Конечно, мы могли бы поместить вывод " Старт ! " в обработчик события
t ick, но так мы подчеркнули различие между событиями и обязательствами.
Результат, определенно, более подробен, чем наша первоначальная функция
countdown, и мы получили намного больше функциональных возможностей. Теперь
у нас есть полный контроль над регистрацией событий при обратном отсчете и обя­
зательство, выполняемое при завершении обратного отсчета.
Мы все еще имеем в запасе одну задачу - мы не решили проблему суеверного
экземпляра Countdown, продолжающего обратный отсчет после 1 3, даже при том что
обязательство было отклонено.
с = new Countdown ( l 5 , t rue )
. on ( ' tick ' , function ( i ) { / / заметьте, мы можем "сцепить " вызов ' оп '
i f ( i > O ) console . log ( i + ' . . . ) ;

const

'

});
с.

go ( )
. then ( funct ion ( )
console . log ( ' Старт ! ' ) ;
})
. catch ( function ( er r ) {
console . error ( err . me s sage ) ;

})

Мы получим сообщение о событии t i c k даже если будет достигнуто значение О
(хотя и не выводим информации о нем). Решение этой проблемы немного затруд­
нительно, поскольку мы уже создали все нужные нам интервалы времени. Конечно,
здесь мы могли бы просто "смошенничать" и немедленно прервать работу, если су­
еверный таймер создается для отсчета 1 3 или более секунд, но это противоречило
бы задаче упражнения! Для решения данной проблемы, поскольку мы обнаружили,
что не можем без этого продолжать, мы должны будем очистить все ожидающие об­
работки интервалы времени.
const EventEmit t e r = require ( ' events ' ) . EventEmitte r ;
class Countdown extends EventEmit t e r {
constructor ( seconds , supers t i t ious )
supe r ( ) ;
thi s . s econds
seconds ;
thi s . supe r s t i t ious
1 ! supe r s t i t ious ;
=

=

go ( )

{

Обязательства

243

const countdown = t hi s ;
const timeout ids = [ ] ;
return new Promi s e ( funct ion ( re solve , rej ect ) {
for ( le t i=countdown . seconds ; i >= O ; i - - ) {
t imeout ids . push ( setTimeout ( function ( ) {
i f ( countdown . supe r s t i t ious && i=== l З )
/ / очистить все ожидающие обра ботки периоды
t imeout ids . forEach ( clearTimeout ) ;
return rej ect ( new Еrrоr ( " Принципиально это не
считаем ! " ) ) ;

},

countdown . emit ( ' t ick ' , i ) ;
i f ( i=== O ) resolve ( ) ;
( countdown . seconds - i ) * 1 0 0 0 ) ) ;

});

Сцепление обязательств
Одно из преимуществ обязательств в том, что они могут быть сцетzены (chained),
т.е. когда одно обязательство выполняется, вы можете немедленно вызвать другую
функцию, которая возвращает обязательство". и т.д. Давайте создадим функцию
launch, которую мы можем сцепить с обратным отсчетом.
funct ion launch ( ) {
return new Promi s e ( funct i on ( re s o lve , rej ect ) {
console . log ( " Пoexaли ! " ) ;
setTimeout ( function ( ) {
resolve ( "Ha орбите ! " ) ;
/ / действительно очень быстрая ракета
} , 2*1000) ;
});

Сцепить эту функцию с обратным отсчетом довольно просто.
const с = new Countdown ( 5 )
. on ( ' t ick ' , i => console . log ( i + ' . . . ) ) ;
'

c . go ( )
. then ( launch )
. then ( functi on ( ms g )
console . log (msg ) ;
})
. catch ( funct ion ( err) {
console . error ( " Xьюcтoн, у нас проблемы . . . . " ) ;
})

244

Глава

1 4.

Асинхронное программирован ие

Одно из преимуществ сцепления обязательств в том, что вы не обязаны обраба­
тывать ошибки на каждом этапе; если где-нибудь в цепочке произойдет ошибка, то
цепочка остановится и управление перейдет к обработчику catch. Давайте заменим
обратный отсчет 1 5-секундным суеверным обратным отсчетом; вы обнаружите, что
функция launch никогда не будет вызвана.

П редотвращен ие незавершенны х обязательств
Обязательства могут упростить ваш асинхронный код и защитить вас от проблем
функций обратных вызовов, вызываемых несколько раз, но они не защищают вас
от проблемы обязательств, которые никогда не завершаются ( т.е. когда вы забываете
вызвать resol ve или rej ect). Ошибки этого вида может быть очень трудно обнару­
жить, поскольку никакой ошибки нет . . . а в сложной системе незавершенное обяза­
тельство может быть просто потеряно.
Один из способов предотвращения этого заключается в определении периода
для обязательств; если обязательство не завершено за некий разумный период вре­
мени, оно автоматически отклоняется. Вполне очевидно, что это вам решать, каков
"разумный период времени''. Если у вас достаточно сложный алгоритм, выполнение
которого предположительно займет 10 минут, не устанавливайте 1 -секундный период.
Давайте вставим в нашу функцию launch искусственный отказ. Скажем, наша ра­
кета очень экспериментальная, и она отказывает приблизительно в половине случаев.
function launch { ) {
return new Promi s e { funct ion ( resolve, rej e c t ) {
i f { Math . random { ) < 0 . 5 ) return ; / / отказ ракеты
console . l og { " Пoexaли ! " ) ;
setTimeout { funct ion { ) {
resolve { "Ha орбите ! " ) ;
} , 2* 1 0 00 ) ;
/ / действительно очень быстрая ракета
});

Способ отказа в данном примере не очень достойный: мы не вызываем функцию
rej ect и даже ничего не выводим на консоль. Мы просто тихо выходим в половине
случаев. Если запустить этот пример несколько раз, то можно увидеть, что иногда он
срабатывает, а иногда нет". безо всякого сообщения об ошибке, что явно нежелательно.
Мы можем написать функцию, которая задает обязательству период.
function addTimeout { fn , t imeout ) {
i f { t imeout
unde fined) t imeout = 1 0 0 0 ; / / стандартный период
return functi on { . . . args ) {
return new Promi s e { funct ion ( re solve , r e j e c t ) {
const t i d
setTimeout { re j e c t , t imeout ,
new Еrrоr { "Истек период обязательства " ) ) ;
===

=

fn { . . . arg s )

Обяза тельства

245

Если вы уже говорите "Ого." функция, которая возвращает функцию, которая
возвращает обязательство, которое вызывает функцию, которая возвращает обя­
зательство ... У меня уже голова кружится!': я могу вас понять: добавление периода
в возвращающую обязательство функцию не является тривиальной задачей и требу­
ет напряжения всех извилин. Полное понимание этой функции является упражнени ем для "продвинутого" читателя. Однако использовать эту функцию весьма просто:
мы можем добавить период к любой функции, которая возвращает обязательство.
Скажем, наша самая медленная ракета достигает орбиты через 1 О секунд (разве не
прекрасны ракетные технологии будущего?). Таким образом, мы устанавливаем пе­
риод на 1 1 секунд.
с . go ( )
. then ( addTimeout ( launch, 4 * 1 0 0 0 ) )
. then ( function (msg) {
cons o l e . log (msg) ;
})
. ca t ch ( funct ion ( er r )

(
console . error ( "Xьюcтoн, у нас проблемы: " + err . me s sage ) ;

});

Теперь наша цепь обязательств всегда будет завершаться, даже когда функция
launch ведет себя плохо.

Генераторы
Как уже обсуждалось в главе 1 2, генераторы обеспечивают двухстороннюю связь
между функцией и ее вызывающей стороной. Генераторы синхронны по своей при­
роде, но, будучи объединены с обязательствами, обеспечивают мощную технологию
для управления асинхронным кодом в JavaScript.
Давайте припомним главную сложность асинхронного кода: его труднее писать,
чем синхронный код. Когда мы решаем задачу, наш ум стремится свести ее к син­
хронному виду: этап 1, этап 2, этап 3 и т.д. Однако при этом подходе могут быть про­
блемы производительности, которых нет при асинхронном подходе. Разве не было
246

Глава

1 4.

Асинхронное программирование

бы хорошо иметь преимущества производительности асинхронных технологий без
дополнительных концептуальных трудностей? Вот где могут пригодиться генерато­
ры.
Рассмотрим использованный ранее пример "проклятья обратных вызовов": чте­
ние трех файлов, задержка на одну минуту и последующая запись содержимого пер­
вых трех файлов одного за другим в четвертый файл. Наш человеческий разум хо­
тел бы написать это в виде примерно такого псевдокода.
dataA
читаем содержимое файла ' a . txt '
dataB = читаем содержимое файла ' b . txt '
dataC = читаем содержимое файла ' c . txt '
Ждем 6 0 секунд
Записываем dataA + dataB + dataC в файл ' d . txt '

Генераторы позволяют нам писать код, который выглядит очень похоже на этот...
однако необходимые функциональные возможности не появятся как из коробки:
сначала придется проделать небольшую работу.
Первое, что необходимо, - это способ превратить функцию обратного вызова
с первым аргументом для передачи ошибки Node в обязательство. Мы инкапсулиру­
ем это в функцию nfcall (Node function call - вызов функции Node).
function nfcall ( f , . . . args ) {
return new Promise ( funct ion ( re s o lve , rej ect ) {
f . cal l ( nu l l , . . . args , function ( err, . . . args )
i f ( er r ) return rej ect ( err) ;
resolve ( args . length i t . throw ( err ) ) ;
else {
setTimeout ( iterate , О , x . value ) ;

} ) () ;

Данная функция g run основана на функции runGenerator, представ­
ленной Кайлом Симпсоном (Kyle Simpson) в его превосходной серии
статей о генераторах. Я настоятельно рекомендую прочитать эти
статьи как дополнение к данному тексту.
Это очень скромный рекурсивный пускатель генератора. Вы передаете ему функ­
цию генератора, и он запускает его. Как уже было сказано в главе 6, генераторы, кото­
рые вызывают оператор yie ld, будут делать паузу, пока не будет вызван метод next
его итератора. Данная функция делает это рекурсивно. Если итератор возвращает
обязательство, она ожидает выполнения обязательства перед возобновлением итера­
тора. С другой стороны, если итератор возвращает простое значение, она немедлен­
но возобновляет итерацию. Вы можете задаться вопросом "Почему происходит вызов
setT irneout вместо обычного непосредственного вызова i te rate?" Причина в том,
что мы получим более эффективный код при отказе от синхронной рекурсии (асин­
хронная рекурсия позволяет движку JavaScript освобождать ресурсы куда быстрее).
Вы можете подумать "Как много суеты!" и "Это называется упрощает жизнь?" Од­
нако сложная часть завершена. Функция nfcall позволяет увязать прошлое (функции

248

Глава

1 4.

Асинхронное программирование

обратного вызова с первым аргументом для передачи ошибки в стиле Node) с насто­
ящим (обязательства), а функция grun обеспечивает доступ к будущему уже сегодня
(в спецификации ES7 ожидается ключевое слово awai t, которое будет, по существу,
функцией grun с еще более естественным синтаксисом). Теперь, когда самая трудная
часть закончена, давайте посмотрим, как все это упрощает нашу жизнь.
Помните наш псевдокод "разве не было бы хорошо" ранее в этой главе? Теперь
мы можем его реализовать.
funct i on *
const
const
const
yield
yield

theFuturei sNow ( )
dataA
yield nfcall
dataB = yield nfcall
dataC = y i e ld nfcall
pt imeout ( 6 0 * 1 0 0 0 ) ;
nfcall ( fs . writeFile ,

( f s . readFi l e ,
( fs . readFile ,
( fs . r eadFile ,

' а . txt ' ) ;
' b . txt ' ) ;
' c . txt ' ) ;

' d . t xt ' , dataA+dataB+dataC) ;

Выглядит намного лучше, чем проклятье обратных вызовов, не так ли? Это куда
аккуратнее, чем одни только обязательства, и напоминает способ, которым мы дума­
ем. Используется это так же просто.
grun ( theFuture i sNow ) ;

Ш аг в пер ед и два назад ?
Вы могли бы (и весьма резонно) полагать, что мы зашли в такие глубокие деб­
ри, только чтобы понять природу асинхронного выполнения и сделать все проще."
а теперь мы вернулись к тому, с чего начали, кроме дополнительных сложностей
с генераторами и преобразованием всего в обязательства, а также функции g run.
И в этом есть некоторая доля правды: в нашей функции theFuturei sNow ребенка
выплеснули с грязной водой несколько раз. Мы добились достаточно простого в на­
писании и чтении кода. Но мы получили лишь часть преимуществ от асинхронного
выполнения, но не их все. Здесь вполне резонен вопрос ''А не было бы эффективнее
читать эти три файла параллельно?" Ответ на этот вопрос зависит от конкретной
задачи, реализации вашего движка JavaScript, вашей операционной системы и вашей
файловой системы. Но давайте отложим эти сложности на мгновение и уясним, что
не имеет значения, в каком порядке мы будем читать эти три файла, и что выигрыш
в эффективности зависит от способности выполнения операций чтения файлов па­
раллельно операционной системой. Именно здесь пускатели генераторов могут соз­
дать иллюзию ложной простоты: ведь мы написали функцию именно в таком стиле
только потому, что этот путь казался нам простым и очевидным.
Проблема (предположим, что это проблема) решается просто. В классе P romi s e
есть метод a l l , который будет завершен (resolves), когда будут завершены (resolve)
все обязательства в массиве". и выполняет асинхронный код параллельно, если это

Генераторы

249

возможно. Остается только модифицировать нашу функцию так, чтобы использо­
вать метод Promise . a l l .
function* theFuture i sNow ( )
const data
yield Promis e . al l ( [
nfca l l ( fs . readFi l e , ' а . t xt ' ) ,
nfcal l ( f s . readF i l e , ' b . txt ' ) ,
nfcal l ( fs . readFil e , ' с . txt ' ) ,
]);
yield p t imeout ( 60 * 1 0 0 0 ) ;
yield n f c a l l ( fs . wr i t e F i l e , ' d . txt ' , data [ O ] +data [ l ] +data [ 2 ] ) ;
=

Обязательство, возвращенное методом Promi s e . a l l , представляет собой массив,
содержащий значение состояния выполнения каждого обязательства в порядке их
расположения в массиве. Даже при том, что файл с . txt вполне может быть прочитан
прежде, чем файл а . txt, элемент data [ О ] будет все еще хранить содержимое а . txt,
а da ta [ 2 ] - содержимое с . txt.
Из этого раздела вам уже должно было стать понятно, что в основе всего лежит
не метод Promi s e . a l l (хотя это и весьма удобный инструмент) , а то, что следует
учитывать, какие именно части вашей программы могут быть выполнены парал­
лельно, а какие не могут. В этом примере вполне можно даже запустить интерваль­
ный таймер параллельно с чтением файла: все это зависит от задачи, которую вы
пытаетесь решить. Если важно, чтобы эти три файла были прочитаны, затем про­
шло 60 секунд, а затем результат их объединения записан в другой файл, то мы уже
имеем то, что хотим. С другой стороны, нам может понадобиться, чтобы эти три
файла были прочитаны не ранее, чем через 60 секунд, а результат был записан в чет­
вертый файл - в этом случае нам нужно переместить установку интервала в метод
Promi s e . a l l .

Н е п и шите соб ствен н ы х п ускателей генераторов
Хотя написание собственного пускателя генератора, как это было сделано с grun,
является хорошим упражнением, в него следовало бы внести много нюансов и усо­
вершенствований. Лучше не изобретать колесо. Пускатель генератора со полноце­
нен и надежен. Если вы создаете веб-сайты, вам может иметь смысл изучить Коа, ко­
торый предназначен для работы с со и позволяет писать веб-обработчики, используя
yield, как в функции theFuturei sNow.

Об ра б отка исключений в п ускател ях генераторов
Другое важное преимущество пускателей генератора состоит в том, что они до­
пускают возможность обработки исключений с помощью блоков try/ catch. Пом­
ните, что при использовании функций обратного вызова и обязательств обработка
250

Гла ва

14.

Асинхронное программирова ние

исключений довольно проблематична. Генерирование исключения в функции обрат­
ного вызова не может быть обработано за пределами этой функции. У пускателей
генератора, поскольку они допускают синхронную семантику при все еще асинхрон­
ном выполнении, есть дополнительное преимущество при работе с t ry/ cat ch. Да­
вайте добавим в нашу функцию theFuturei sNow несколько обработчиков исключе­
ний.
function * theFuture i sNow ( )
let dat a ;
try
data = yield Promise . al l ( [
nfcall ( fs . readFile , ' а . t xt ' ) ,
nfcall ( fs . readFi l e , ' Ь . t xt ' ) ,
nfcall ( fs . readFi l e , ' с . txt ' ) ,
]);
catch ( er r ) {
consol e . error ( " Oшибкa при чтении файлов : " + err . message ) ;
throw e r r ;
y i e l d p t imeout ( 60 * 1 0 0 0 ) ;
try {
yield nfcall ( fs . writ e F i l e , ' d . txt ' , data [ 0 ] +data [ 1 ] +data [ 2 ] ) ;
catch ( er r ) {
console . error ( "Oшибкa при записи файла : " + err . me s s age ) ;
throw e r r ;

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

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

З акл ю ч ение
Полное понимание сложностей асинхронного программирования (и различных
механизмов, выработанных для управления им) критически важно для понимания
современной разработки на JavaScript. В этой главе мы изучили следующие вопросы.


Управление асинхронным выполнением в JavaScript осуществляется функция­
ми обратного вызова.



Обязательства не заменяют обратные вызовы; на самом деле они требуют
функций обратного вызова then и catch.



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



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



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



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



При написании функций генератора с синхронной семантикой следует быть
внимательными и четко понимать, какие части вашего алгоритма могут вы­
полняться параллельно с использованием метода Promise . a l l .



Вам не стоит писать собственные пускатели генераторов; используйте со или Коа.



Вам не стоит писать собственный код для преобразования обратных вызовов
в стиле Node в обязательства; используйте Q.



Обработка исключений работает с синхронной семантикой, как позволяют пус­
катели генераторов.

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

252

Глава

1 4.

Асинхронное программирование

ГЛАВА 1 5

Д ата и в рем я

В большинстве реальных приложений используются данные о дате и времени.
К сожалению, объект JavaScript Date (который хранит данные о дате и времени) не
является одним из шедевров языка. Из-за ограниченного удобства этого встроен­
ного объекта я буду использовать Moment . j s, который наследует функциональные
возможности объекта Date, для реализации наиболее популярных функциональных
возможностей.
Интересный исторический факт: объект JavaScript Date был первоначально ре­
ализован программистом Netscape Кеном Смитом (Ken Smith); он, по существу,
перенес в JavaScript реализацию j ava . util . Date из языка Java. Таким образом, ут­
верждение, что язык JavaScript не имеет никакого отношения к Java, не полностью
соответствует действительности: если вас когда-либо спросят, имеют ли эти языки
что-то общее, можете сказать "Очень немногое, кроме объекта Date и общего син­
таксического предка':
Поскольку регулярно повторять слова "дата и время" утомительно, далее я буду
использовать термин "дата': Дата без явного указания времени будет подразумевать
1 2:00 утра текущего дня.

Даты, ч асовые пояса, временные метки и эпохи Unix
Давайте посмотрим правде в глаза: наш современный Григорианский кален­
дарь - капризный, сверхсложный, нумеруемый начиная с 1, с нечетной кратностью
времени и високосными годами. Часовые пояса добавляют еще больше сложности.
Однако эта система по большей части универсальна и нам с ней жить.
Давайте начнем с чего-то простого: с секунд. В отличие от сложного деления вре­
мени в Григорианском календаре, секунды просты. Дата и время (представленные
в секундах) являются одним числом, аккуратно упорядоченным на числовой оси.
Поэтому представление даты и времени в секундах идеально подходит для вычисле­
ний. Но для коммуникаций между людьми подходит не очень хорошо: "Эй, Байрон,
пообедаем в 1437595200?" ( 1437595200
это среда, 22 июля 20 1 5 года, 1 час по по­
лудни тихоокеанского времени.) Но если даты представляются секундами, то чему
-

соответствует дата Ы Это не дата рождения Христа, а просто произвольная дата:
1 января 1 970 года, 00:00:00 UTC.
Поскольку вы, вероятно, знаете, что мир делится на часовые пояса (TZ), незави­
симо от того, где вы находитесь, в 7 утра будет утро, а в 7 после полудня - вечер.
Часовые пояса могут быть сложны, тем более если учитывать летнее время. Я не буду
пытаться объяснить в этой книге все нюансы Григорианского календаря или часовых
поясов - Википедия превосходно решает эту задачу. Но чтобы помочь вам понять
объект JavaScript Date (и пользу Moment . j s ) , некоторые из основ рассмотреть стоит.
Все часовые пояса определяются как смещения от Всемирного координированного
времени (сокращенно - UTC), а все подробности ищите в Википедии. Иногда UTC
(не совсем корректно) называют Средним временем по Гринвичу (Greenwich Mean
Time - GMT). Например, я в настоящее время нахожусь в Орегоне, который на­
ходится в Тихоокеанском часовом поясе. Тихоокеанское время на семь или восемь
часов отстает от UTC. Что значит "на семь или восемь"? Как это возможно? Все за­
висит от времени года. Летом это летнее время, и смещение - семь часов. Осталь­
ную часть года это стандартное время, и смещение - восемь часов. Здесь важно не
запомнить часовые пояса, а понять, как представляются смещения. Если я открою
терминал JavaScript и введу new Date ( ) , я увижу следующее.
Sat Jul 1 8 2 0 1 5 1 1 : 0 7 : 0 6 GMT - 0 7 0 0 ( Pacific Daylight Tirne )

Обратите внимание, что в этом очень подробном формате часовой пояс определя ется как смещение и от UTC (GMT-0 700), и по имени ( Pac i fi c Dayl i ght T ime).
В JavaScript во всех экземплярах объекта Date дата и время хранится в виде оди­
ночного числа - количества миллисекунд (не секунд!), прошедших с Эпохи Unix.
Обычно JavaScript преобразует это число в удобочитаемую Григорианскую дату, ког­
да вы запросите это (как только что было показано) . Если вы хотите увидеть число­
вое представление, просто используйте метод valueOf ( ) .
const d = new Date ( ) ;
/ / форма тированная Григорианская дата с TZ
console . log ( d ) ;
console . log ( d . valueOf ( ) ) ; / / миллисекунды начиная с Эпохи Un ix

Создание объектов Da te
Объект Date может быть создан четырьмя способами. Без аргументов (как мы
уже видели), возвращается просто объект Da te, представляющий текущую дату. Мы
можем также предоставить строку, которую JavaScript попытается проанализировать,
или мы можем задать конкретную (локальную) дату в миллисекундах. Вот примеры.
1 1 в се дальнейшее интерпретируется с учетом местного времени
new Date ( ) ;
/ / текущая дата

254

Глава

1 5.

Дата

и

время

1 1 заметь те, что в Ja vaScript месяцы отсчитываются от
11 нуля : O=Jan , l=Feb и т . д .
new Date ( 2 0 1 5 , 0 ) ;
1 1 1 2 : 00 А . М. , Jan 1 , 2015
new Date ( 2 0 1 5 , 1 ) ;
1 1 1 2 : 00 А . М. , Feb 1 , 2 0 1 5
new Date ( 2 0 1 5 , 1 , 1 4 ) ;
1 1 12 : 00 А . М. , Feb 1 4 , 2015
new Date ( 2 0 1 5 , 1 , 1 4 , 1 3 ) ;
1 1 3 : 00 Р . М. , Feb 1 4 , 2015
new Date ( 2 0 1 5 , 1 , 1 4 , 1 3 , 3 0 ) ;
1 1 3 : 30 Р . М. , Feb 1 4 , 2 0 1 5

new Date ( 2 0 1 5 , 1 , 1 4 , 1 3 , 3 0 , 5 ) ;
1 1 3 : 30 : 05 Р . М. , Feb 1 4 , 2015
new Date ( 2 0 1 5 , 1 , 1 4 , 1 3 , 3 0 , 5 , 5 0 0 ) ; 1 1 3 : 30 : 05 . 5 Р . М. , Feb 1 4 , 2015
1 1 создание да т из временных меток Эпохи Un ix
new Date ( O ) ;
/ / 1 2 : 00 А . М. , Jan 1 , 1 9 70 ИТС
new Date ( 1 0 0 0 ) ;
1 1 1 2 : 00 : 01 А . М. , Jan 1 , 1 9 70 ИТС
/ / 5 : 00 Р . М. , Мау 1 6, 201 6 ИТС
new Date ( 1 4 6 3 4 4 3 2 0 0 0 0 0 ) ;
1 1 для получения дат до Эпохи Unix используйте отрицательные зна чения
new Date ( - 3 65 * 2 4 * 60 * 6 0 * 1 0 0 0 ) ;
/ / 1 2 : 00 А . М. , Jan 1 , 1 969 ИТС

/ / анализ строк да ты (стандартное время - местное)
/ / 1 2 : 00 А . М. , Jun 1 4 , 1 903 local time
new Date ( ' June 1 4 , 1 9 03 ' ) ;
new Date ( ' June 1 4 , 1 9 0 3 GMT- 0 0 0 0 ' ) ; / / 1 2 : 00 А . М. , Jun 1 4 , 1 903 ИТС

Выполняя эти примеры, обратите внимание на то, что результаты, которые вы
получите, всегда будут давать местное время. Если вы используете UTC (привет,
Тимбукту, Мадриду и Гринвичу!), то результаты, представленные в UTC, будут отли­
чаться от представленных в этом примере. Это демонстрирует нам один из основных
недостатков объекта JavaScript Date: нет никакого способа указать, в каком часовом
поясе он должен быть. Внутренне он всегда будет хранить объекты в формате UTC
и представлять их согласно местному времени (которое определяется настройками
вашей операционной системы). С учетом назначения JavaScript как языка сценариев
для браузеров это традиционно было "правильно': Если вы работаете с датами, то,
вероятно, хотите отображать их в часовом поясе пользователя. Однако в связи с гло­
бальным характером Интернета (и переносом JavaScript на сервер в виде проекта
Node) необходима более надежная обработка часовых поясов.

Б иблиотека Momen t . j s
Хотя эта книга о самом языке JavaScript, а не о библиотеках, манипуляции да­
той - настолько важная и общая задача, что я решил познакомить вас с известной
и весьма надежной библиотекой дат Moment . j s.
Библиотека Moment . j s бывает двух разновидностей: с поддержкой часового по­
яса и без нее. Поскольку версия с поддержкой часового пояса значительно больше
(у нее есть информация обо всех часовых поясах в мире), вы можете использовать
ее и без такой поддержки. Для простоты все изложенное ниже относится к версии
Б иблиотека Moment.js

255

с поддержкой часовых поясов. Если нужна меньшая версия, ознакомьтесь с инфор­
мацией о ее возможностях по адресу http : / /momentj s . сот.
Разрабатывая веб-ориентированный проект, вы можете подключить библиотеку
Moment . j s от CDN, как показано ниже.
< / s cript>

Если вы работаете с Node, то можете установить библиотеку Moment . j s, исполь­
зуя команду npm insta l l --save moment-timezone, а затем подключить ее в свой
сценарий с помощью функции require.
const moment = require ( ' moment - t ime z one ' ) ;

Библиотека Moment . j s велика и надежна, она обладает всеми функциональными
возможностями, необходимыми для манипулирования датой. Более подробная ин­
формация об этой библиотеке содержится в ее документации.

П ракти ч еский подход к датам в JavaScript
Теперь, завершив рассмотрение основ и обладая библиотекой Moment . j s, давайте
применим немного иной подход к изложению этой информации. Исчерпывающий
охват методов, доступных в объекте Date, был бы сух и не очень полезен для боль­
шинства людей. Кроме того, если эта информация необходима, есть исчерпывающая
и хорошо написанная библиотека MDN, содержащая полное описание объекта Date.
Вместо этого в данной книге будет использован подход, напоминающий поварен­
ную книгу, - мы рассмотрим обработку дат в общем, как необходимо большинству
людей, а что именно при этом применять, Date или Moment . j s, будет зависеть от об­
стоятельств.

Создание дат
Мы уже рассматривали доступные для вас возможности создания объектов Date
в JavaScript, и они по большей части адекватны. Всякий раз, когда вы создаете дату
без явного указания часового пояса, полученная дата будет использовать часовой
пояс, зависящий от того, где создается дата. В прошлом это сбивало с толку мно­
гих новичков: они использовали тот же код даты на сервере в Арлингтоне (штат
Виргиния), просматривали его в браузере пользователя, подключившегося в Лос­
Анджелесе (штат Калифорния), и с удивлением обнаруживали разницу в три часа.

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

Глава

1 5.

Дата

и

время

(облаке) подходе к разработке приложений один и тот же базовый код может вы­
полняться на серверах во всем мире. Создавая локальные даты, вы напрашиваетесь
на неприятности. Если вы в состоянии использовать даты UTC, можете создавать их,
используя метод UTC объекта Date.
const d = new Date ( Da t e . UTC ( 2 0 1 6 , 4 , 2 7 ) ) ; / / Мау 2 7 , 201 6 ИТС

Метод Date . UTC получает все те же варианты аргументов, что и кон­
структор Date, но вместо нового экземпляра объекта Date он возвра­
щает числовое значение даты. Затем это число может быть передано
в конструктор Date для создания экземпляра даты.
Если необходимо создавать даты на сервере находящемся в определенном часо­
вом поясе (и нет желания осуществлять преобразования часового пояса вручную),
вы можете использовать moment . tz для создания экземпляров Date с определенным
часовым поясом.
/ / Передача массива в Momen t . js использует те же параметры, что и
/ / конструктор Da te Ja vaScrip t , включая отсчитываемый от нуля месяц
/ / (O=Jan , l=Feb и т . д . ) . Метод toDa te () преобразует назад
/ / в объект Da t e JavaScript .
const d
moment . t z ( [ 2 0 1 6 , 3 , 2 7 , 9 , 1 9 ] , ' America /Los_Angele s ' ) . t oDate ( ) ;
=

Создание дат в браузере
Вообще, стандартное поведение JavaScript соответствует браузеру. Браузеру
от операционной системы известен часовой пояс, в котором он находится, и поль­
зователи обычно предпочитают для работы местное время. Если вы создаете при­
ложение, которое должно обрабатывать даты в других часовых поясах, то лучше ис­
пользовать Moment . j s для обработки, преобразования и представления дат в других
часовых поясах.

Переда ч а дат
Все становится куда интересней при передаче даты, когда сервер посылает дату
в браузер или наоборот. Сервер и браузер могут находиться в разных часовых по­
ясах, а пользователи хотят видеть даты в их локальном часовом поясе. К счастью,
поскольку экземпляры Date JavaScript хранят дату как числовое смещение времени
в UTC от Эпохи Unix, передавать объекты Date обычно безопасно.
Мы говорили о "передаче" весьма неопределенно, но что же именно мы под ней
подразумеваем? Самый верный способ гарантировать правильность передачи дат
в JavaScript - это использовать спецификацию JSON (JavaScript Object Notation).
Фактически эта спецификация не определяет тип данных для дат, что весьма при­
скорбно, поскольку это предотвращает симметричный анализ JSON.
Передач а д ат

257

const before = { d : new Date ( ) } ;
before . d instanceof date
const j son = JSON . s tringify ( be fore ) ;
const after = JSON . parse ( j son) ;
after . d instdanceof date
typeo f after . d

1 1 true

11 false
/ / "s tring"

Таким образом, плохая новость в том, что JSON не может полностью и симмет­
рично обрабатывать даты в JavaScript. Хорошая новость в том, что строковая сери­
ализация, которую использует JavaScript, всегда единообразна, поэтому вы можете
"восстановить" дату.
after . d = new Date ( after . d ) ;
a fter . d instanceof date

/! true

Независимо от того, какой именно часовой пояс первоначально использовался
при создании даты, после ее перекодировки в JSON она будет в формате UTC, а ког­
да строка кода JSON будет передана конструктору Date, дата будет отображена в ло­
кальном часовом поясе.
Другой безопасный способ передачи даты между клиентом и сервером подраз­
умевает просто использование числового значения даты.
const before = { d : new Date ( ) . va lueOf ( ) } ;
1 1 "numЬer "
t ypeof be fore . d
const j son = JSON . stringi f y ( be fore ) ;
const after = JSON . parse ( j son) ;
/ / "п итЬеr "
typeof a fter . d
const d = new Date ( a fter . d ) ;

Хотя JavaScript прекрасно работает с перекодировкой JSON дат
в строки, библиотеки JSON для других языков и платформ нет. Сери­
ализатор JSON для .NET, в частности, заключает кодированные JSON
объекты даты в оболочку их собственного формата. Так, если вы взаимодействуете с JSON из другой системы, потрудитесь разобраться,
как она сериализирует даты. Если вы контролируете исходный код,
то, возможно, безопаснее будет передавать числовые даты как смеще­
ния от Эпохи Unix. Но даже в этом случае следует быть вниматель­
ным: библиотеки дат зачастую предоставляют числовое значение в се­
кундах, а не в миллисекундах.

Отображение дат
Форматирование дат при выводе зачастую является одной из самых раздражаю­
щих задач для новичков. Встроенный объект JavaScript Date включает лишь несколь­
ко встроенных форматов даты, и если они не удовлетворяют вашим потребностям,
258

Глава 1 5 . Дата

и

время

то осуществить форматирование самостоятельно будет довольно сложно. К счастью,
библиотека Moment . j s хороша в этой области, и если вы требовательны к отображе­
нию даты, то я рекомендую использовать именно ее.
Для форматирования даты используйте метод format библиотеки Moment . j s. Он
получает строку из метасимволов, которые заменяются соответствующим компонен­
том даты. Например, строка " УУУУ " будет заменена четырехразрядным годом. Вот
несколько примеров форматирования даты встроенными методами объекта Da te
и более надежными методами Moment . j s .
const d = new Date { Dat e . UTC { 1 93 0 , 4 , 1 0 ) ) ;
1 1 здесь представлен вывод для Лос-Анджелеса

d . toLocaleDateString { )
d .toLocaleFormat { )
d . toLocaleTimeString { )
d . toTimeString { )
d . toUTCString { )
moment
moment
moment
moment

1 1 "5/9/1 930 "
1 1 "5/9/1 930 4 : 00 : 00 РМ"
11 "4 : 00 : 00 РМ"
1 1 "1 7 : 00 : 00 GМТ-0 700 (Pacific Daylight Time) "
1 1 "Sa t , 1 0 Мау 1 93 0 , 00 : 00 : 00 GМТ"

11
{ d) . format { 11УУУУ -ММ-DD ) ;

1 1 "1 930 - 0 5 - 0 9 "
1 1 " 1 930-05-09 1 7: 00
{ d ) . format { 11 YYYY-ММ-DD HH : mm" ) ;
1 1 "1 930-05-09 1 7 : 00 -0 7 : 00
{ d ) . format { "YYYY-ММ-DD HH : mm Z 11 ) ;
{ d ) . format { "YYYY-ММ-DD HH : mm [ UTC ] Z 11 ) ; 1 1 " 1 930-05-09 1 7 : 00 ИТС- 0 7 : 00

moment { d ) . format { " dddd, ММММ [ the ] Do , УУУУ 11 ) ; 1 1 "Friday, Мау the 9th, 1 930 "
moment { d ) . format { "h : mm а " ) ;

1 1 "5 : 00 рт "

В этом примере продемонстрировано, насколько противоречивы и мало гибки
встроенные возможности форматирования даты. К чести JavaScript следует заметить,
что эти встроенные параметры форматирования действительно пытаются обеспе­
чить формат, подходящий для региона пользователя. Если необходимо обеспечить
форматирование даты в нескольких регионах, то это недорогой, хотя и не гибкий,
способ сделать это.
Не будем приводить здесь полный справочник по опциям форматирования
Moment . j s; он доступен в сетевой документации. Достаточно будет сообщить, что,
если у вас есть потребность в форматировании дат, то Moment . j s почти наверняка
поможет в этом. У нее есть некие общие соглашения по форматированию дат, подобные многим метаязыкам. Чем больше символов, тем подробнее, т.е. "М" даст 1, 2, 3 ... ;
"ММS " - 01, 02, 0 3 . .. ; "МММ " - Jan, Feb, Mar . . . ; а "ММММ " - January, February, March... .
Символ " о " в нижнем регистре обеспечит числительные: так "Do" даст lst, 2nd, З rd
и т.д. Если вы хотите включить символы, которые не нужно интерпретировать как
метасимволы, заключите их в квадратные скобки: " [М] М" даст Ml, М2 и т.д.

Отображение д ат

259

Одно из затруднений, которые библиотека Moment . j s не решает пол­
ностью, - это использование сокращений часового пояса, таких как
EST или PST. Она исключила символ форматирования z в связи с от­
сутствием единых международных стандартов. Подробное обсужде­
ние проблем с сокращениями часового пояса приведено в документации библиотеки Moment . j s.

Компоненты даты
Если необходимо получить доступ к индивидуальным компонентам экземпляра
Date, используйте соответствующие методы.
const d = new Date ( Date . UTC ( 1 8 1 5 , 9 , 1 0 ) ) ;
/ / здесь представлен вывод для Лос-Анджелеса
/ / 1 81 5
d . getFullYear ( )
/ / 9 - October
d . getMonth ( )
// 9
d . getDate ( )
/ / 1 - Monday
d . getDay ( )
d . getHours ( )
// 17
// О
d . getMinute s ( )
// О
d . getSeconds ( )
d . getMi l l i seconds ( ) / / О
// есть также эквиваленты ИТС для вьппеупомянутого :
d . getUTCFullYear ( )
/ / 1815
d . getUTCMonth ( )
// 9
October
/ / 10
d . getUTCDate ( )
11 . . и т.д.
-

.

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

С равнение дат
Для простых сравнений даты (действительно ли дата А следует после даты В или
наоборот?) вы можете использовать встроенные операторы сравнения JavaScript.
Помните, что экземпляры Date хранят дату как число, поэтому операторы сравнения
просто работают с числами.
const dl
const d2

new Date ( 1 9 9 6 , 2 , 1 ) ;
new Date ( 2 0 0 9 , 4 , 2 7 ) ;

dl > d2
dl < d2

/ / false
// true

260

Глава 1 5 . Д ата

и

время

А р ифмети ч еские операции с датами
Поскольку даты - это только числа, в ы можете вычитать и х для получения коли­
чества миллисекунд между ними.
/ / 4 1 7 740400000 миллисекунд
const msDiff
d2 - dl ;
const daysDiff = msDi f f / 1 0 0 0 / 60 / 6 0/ 2 4 ; / / 4834 . 9 6 дней
=

Это свойство облегчает также сортировку дат с использованием Arra y .
prototype . sort.
const dates = [ ) ;
/ / создать несколько случайных да т
const min = new Date ( 2 0 1 7 , О , 1 ) . va lueOf ( ) ;
const delta
new Date ( 2 0 2 0 , О , 1 ) . va l ueOf ( ) - min;
for ( le t i = O ; i < l O ; i++ )
date s . push ( new Date ( min + delta *Math . random ( ) ) ) ;
/ / даты случайны и (вероятно) перемешаны
/ / мы можем отсортирова ть их (по убыванию) :
dates . sort ( ( а , Ь ) => Ь - а ) ;
1 1 или возра станию :
dates . sort ( ( а , Ь ) => а - Ь ) ;
=

Библиотека Moment . j s предоставляет множество мощных методов для общих опе­
раций с датами, позволяя добавлять или вычитать произвольные единицы времени.
const m
moment ( ) ;
m . add ( З , ' da ys ' ) ;
m . suЬt ract ( 2 , ' years ' ) ;

1 1 сейча с
1 1 теперь т на три дня в будущем
1 1 теперь т на два года минус три дня в прошлом

m
moment ( ) ;
m . startOf ( ' year ' ) ;
m . endOf ( ' month ' ) ;

1 1 сброс
1 1 теперь т 1 января этого года
1 1 теперь т 31 января этого года

=

=

Библиотека Moment . j s позволяет также сцеплять методы.
const m = moment ( )
. add ( l O , ' hours ' )
. suЬtract ( З , ' days ' )
. endOf ( ' month ' ) ;
/ / т - конец месяца , в котором вы оказались бы, если бы путешествовали
1 1 1 0 ча сов в будущее , а затем 3 дня назад

Удобные относительные даты
Довольно часто возникает необходимость представить информацию даты в от­
носительном виде: не как конкретную дату, а "три дня назад': Библиотека Moment . j s
позволяет сделать это.
А рифметические опера ци и с д а тами

261

moment
moment
moment
moment
moment
moment
moment
moment
moment
moment
moment

()
()
()
()
()
()
()
()
()
()
()

. subtract ( l O , ' seconds ' ) . fromNow ( )
. subtract ( 4 4 , ' seconds ' ) . fromNow ( )
. subtract ( 4 5 , ' seconds ' ) . fromNow ( )
. subtract ( S , ' minute s ' ) . fromNOw ( ) ;
. subtract ( 4 4 , ' minute s ' ) . fromNOw ( )
. subtract ( 45 , ' minutes ' ) . fromNOw ( )
. subtract ( S , ' hours ' ) . fromNOw ( ) ;
. subtract ( 2 1 , ' hours ' ) . fromNOw ( ) ;
. subtract ( 2 2 , ' hours ' ) . fromNOw ( ) ;
. subtract ( 3 4 4 , ' da ys ' ) . fromNOw ( ) ;
. subtract ( 3 4 5 , ' days ' ) . fromNOw ( ) ;

;
;
;

1 1 несколько секунд назад
1 1 несколько секунд назад
1 1 минуту назад
1 1 5 минут назад

;
;

1 1 4 4 минуты назад
1 1 час назад
1 1 4 часа назад
1 1 21 ча с назад
1 1 день назад
1 1 3 4 4 дня назад
1 1 год назад

Как можно заметить, в Mornent . j s выбраны некие произвольные (но разумные)
контрольные точки для перехода к отображению других единиц. Это удобный спо­
соб получения относительных дат.

Закл юч ение
И з этой главы в ы должны были извлечь следующие уроки.


Внутренне даты представляются как количество миллисекунд от Эпохи Unix
( 1 января 1 970 года UTC).



Создавая даты, помните о часовом поясе.



Если необходимо сложное форматирование даты, используйте Mornent . j s.

В большинстве реальных приложений трудно избежать работы с датами. Мы наде­
емся, что эта глава дала вам понимание важнейших концепций. Полная и подробная до­
кументация по Mornent . j s содержится в сети разработчика Mozilla Developer Network.

262

Глава

1 5.

Дата

и

время

ГЛА ВА 1 6

Объ е кт Math

В этой главе описан встроенный объект JavaScript Math, который содержит ма­
тематические функции, обычно встречающиеся при разработке приложений (если
вы осуществляете сложный математический анализ, вам, вероятно, имеет смысл вос­
пользоваться библиотеками стороннего производителя).
Прежде чем углубляться в библиотеки, давайте вспомним, как JavaScript обраба­
тывает числа. В частности, вспомним что нет никакого специального целочислен ного класса; все числа представляются как 64-битовые числа с плавающей запятой
стандарта IEEE 754. Это упрощает задачу большинству функций в математической
библиотеке: число есть число. Хотя никакой компьютер никогда не сможет точно
представить произвольное вещественное число, с практической точки зрения вы
можете считать числа JavaScript вещественными. Обратите внимание, что никакой
встроенной поддержки для комплексных чисел в JavaScript нет. Если необходимы
комплексные числа, сверхбольшие числа, более сложные структуры или алгоритмы,
я рекомендую использовать библиотеку Ма th . j s .
Кроме некоторых основ, эта глава не о математике. Этой теме посвящено множес­
тво других книг.
Для указания на то, что данное значение приблизительно, в комментариях к коду
этой главы я буду использовать тильду ( �) как префикс. Я также буду именовать
свойства объекта Ма th функциями, а не методами. Хотя технически они являются
статическими методами, различие здесь является чисто академическим, поскольку
объект Ма th предоставляет пространство имен, а не контекст.

Ф орматирование ч исел
Обычно числа необходимо форматировать, т.е. вместо того чтобы отобра­
жать 2 . 0 0 9 3, вы хотите отобразить 2 . 1 или вместо 1 9 4 9 0 3 2 вы хотите отобразить
1 , 9 4 9 , 032.1

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

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

Ч исла с фиксированным количеством десятичных цифр
Если необходимо отобразить фиксированное количество цифр после десятичной
точки, вы можете использовать NщnЬer . prototype . t oFixed.
const х = 1 9 . 5 1 ;
x . toFixed ( З ) ;
x . toFixed ( 2 ) ;
х . toFixed ( 1 ) ;
x . toFixed ( O ) ;

1 1 "1 9 . 51 0 "
1 1 " 1 9 . 51 "
1 1 "19. 5"
1 1 "20 "

Обратите внимание, что это не усечение: вывод округляется до указанного коли­
чества десятичных цифр.

Э кспоненциальна я форма зап иси
Если необходимо отображать числа в экспоненциальной форме, используйте
NщnЬe r . prototype . toExponential.
const х = 3 8 0 0 . 5 ;
x . toExponentia l ( 4 )
x . toExponentia l ( З )
x . toExponentia l ( 2 )
x . toExponent ial ( l )
x . toExponent ial ( O )

;
;
;
;
;

1 1 "3 . 8005е+ 4 " ;
1 1 "3 . 80 1 е+ 4 " ;
1 1 "3 . 80е+4 ";
1 1 "3 . 8е+4 ":
1 1 "4е+ 4 ";

Подобно NщnЬe r . prototype . toFixed, вывод округляется, а не усекается. Задается
точность - количество цифр после десятичной точки.

Ф иксированная точность
Если необходимо фиксированное количество цифр независимо от положения де­
сятичной точки, вы можете использовать NшnЬer . prototype . toPrecis ion.

264

Глава 1 6 . Объект Math

let х = 1 0 0 0 ;
x . toPrecis ion ( 5 )
x . t o Precis ion ( 4 )
x . t o Precis ion ( 3 )
x . t o Precis ion ( 2 )
x . toPre c i s ion ( l )
х = 1 5 . 335;
x . toPre c i s ion ( б )
x . toPreci s ion ( 5 )
x . t o Pre c i s ion ( 4 )
x . toPrec i s ion ( 3 )
x . toPrec i s ion ( 2 )
x . t o Precision ( l )

;
;
;
;
;
;
;
;
;
;
;

11 "1000 . О "

/! "1000 "
1 1 "1 . 00е+3 "
1 1 "1 . Ое+3 "
1 1 "l e + 3 "
1 1 "1 5 . 335 0 "
1 1 "1 5 . 335 "
1 1 "1 5 . 34 "
1 1 "1 5 . 3 "
1 1 "1 5 "
1 1 "2e+l "

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

Д ругие основа ния
Если вы хотите отображать числа с другим основанием (двоичным, восьмеричным или шестнадцатеричным), используйте NumЬer . prototype . toString, которому
передается аргумент, определяющий основание (в диапазоне 2-36).
const х = 1 2 ;
x . toString ( ) ;
x . toString ( l O ) ;
x . toStr ing ( l б ) ;
x . toString ( 8 ) ;
x . toString ( 2 ) ;

1 1 "1 2 " (по основанию 1 0)
11 "1 2 " (по основанию 1 0)
11 "с " (шестнадца теричный)

/ / "1 4 " (восьмеричный)
1 1 "1 1 00 " (двоичный)

Д ополнительное форматирование чисел
Если вы отображаете в своем приложении много чисел, ваши потребности могут
быстро превзойти возможности встроенных методов JavaScript. Обычно необходимо
следующее.


Разделители тысяч.



Иной способ отображения отрицательных чисел (например, с круглыми скоб­
ками).



Инженерная форма записи (подобная экспоненциальной форме).



Префиксы системы Си (мульти-, микро-, кило-, мега- и т.д.).

Обеспечение этих функциональных возможностей может быть хорошим самосто­
ятельным упражнением. Но если особого желания нет, я рекомендую пользоваться
библиотекой Nurneral . j s, которая предоставляет все эти и многие другие функцио­
нальные возможности.

Константы
Наиболее важные константы доступны как свойства объекта Math.
// фундаментальные константы
Mat h . E
/ / основание на турального логарифма : -2 . 71 8
Math . PI / / отношение длины окружности к диаметру: -3 . 1 42
/ / логарифмические константы доступны в библиотеках ;
/ / подобные вызовы доста точно общеприняты и гарантируют удобство
Mat h . LN2
/ / на туральный логарифм 2 : -0 . 693
Math . LN l O
/ / натуральный логарифм 1 0 : - 2 . 303
Math . LOG2E
/ / логарифм по основанию 2 от Ma th . E : -1 . 433
Math . LOG l OE / / логарифм по основанию 1 0 от Ma th . E: 0 . 434
// алгебраические константы
Mat h . SQRT1 _2 / / корень квадратный из 1 /2 : � О . 70 7
Mat h . SQRT2
/ / корень квадра тный из 2 : -1 . 4 1 4

Алгебраи ч еские функции
В озведение в степень
Базовая функция возведения в степень - это Ма th . pow, но есть и дополнитель­
ные функции для квадратного корня, кубического корня и степеней числа е, как по­
казано в табл. 1 6. 1 .
Табл ица 1 6.1 . Функции возведения в степень
Функция
Math . pow ( х ,
Math . s qrt ( x )

у)
х

Math . exp ( x )

Гла ва

R. эквивалент

Math . pow ( х , О . 5 )
Кубический корень х.
Эквивалент Math . pow
(х, 1 / 3 )

дескриптором . Вот наша первая попытка.
const input = "Regex pros know the difference between\n" +
" < i>greedy< / i > and < i > l a zy< / i > matching . " ;
input . replace ( / ( . * ) < \ / i> / i g , ' < strong> $ 1 < / strong> ' ) ;

Часть $ 1 в строке замены будет заменена содержимым группы ( . * ) в регулярном
выражении (подробности - далее).
Давайте опробуем это. Вы получите следующий неутешительный результат.
"Regex pros know the d i f ference be tween
< s t rong>greedy< / i > and < i > l a z y < / s t rong> matching . "

Чтобы понять происходящее здесь, давайте вспомним, как функционирует обра­
ботчик регулярного выражения: он перерабатывает входные данные, пока не най­
дет соответствие, а затем снова продолжает переработку. Стандартно он делает это
жадным способом: находит первое , а затем говорит "Я не остановлюсь, пока не
увижу самый последний < / i >. Поскольку есть два экземпляра < / i >, он закончит свою
работу на втором, а не на первом.
Есть несколько способов исправить этот пример, но поскольку мы обсуждаем
различие между жадным и ленивым распознаваниями, давайте создадим ленивый
метасимвол повторения ( * ). Для этого нужно просто поместить после него вопро­
сительный знак.
input . replace ( / ( . * ? ) < \ / i > / i g ,

' $ 1 ' ) ;

Регулярное выражение - точно то же, за исключением вопросительного знака пос­
ле метасимвола *. Теперь процессор регулярного выражения рассматривает его так: "я
остановлюсь, как только увижу первый < / i »: Таким образом, он лениво прекращает
анализ, когда встречает первый < / i >, несмотря на то что подобное соответствие может
встретиться далее еще не один раз. Хотя со словом ленивый (lazy) обычно ассоциируется
нечто отрицательное, это поведение - именно то, что нам нужно в данном случае.
Все метасимволы повторения ( * , +, ?, { n } , { n , } и { n , m} ) можно сопроводить во­
просительным знаком, чтобы сделать их ленивыми (хотя на практике я использовал
его только для * и + ) .

Обратные ссып ки
Группировка обеспечивает еще одну возможность - обратные ссылки
(backreference). В моей практике это одно из наименее используемых средств регу­
лярного выражения, но есть один случай, когда без него не обойтись. Прежде чем
рассмотреть действительно полезный пример, давайте сначала рассмотрим глупый.
Предположим, что вы хотите распознавать названия поп-групп согласно шаблону
ХУУХ (держу пари, вы можете вспомнить названия реальных поп -групп, которые

284

Глава

17.

Рег улярные в ы ражения

удовлетворяют этому шаблону). Таким образом, мы хотим распознать PJJP, GOOG
и АВВА. Вот где в игру вступают обратные ссылки. Каждой группе (включающей
подгруппы) в регулярном выражении присваивается номер, слева направо, начиная
с 1 . Вы можете обратиться к этой группе в регулярном выражении, используя об­
ратную косую черту, после которой указан ее номер. Другими словами, \ 1 означает
"соответствие первой группы". Непонятно? Давайте рассмотрим пример.
const promo
" Opening for УААХ is the dynamic GOOG ! At the Ьох o f fice now ! " ;
const bands = promo . ma t ch ( / ( ? : [ A-Z ] ) ( ? : [ A-Z ) ) \2 \ 1 /g ) ;

Читая слева направо, мы видим, что есть две группы, а затем \ 2 \ 1 . Так, если пер­
вая группа соответствует х и вторая группа соответствует А, то \2 должно соответ­
ствовать А и \ 1 должно соответствовать Х.
Если это кажется вам замечательным, но не очень полезным, вы не одиноки.
Единственный случай, когда я полагаю, что обратные ссылки полезны (кроме случа­
ев решения головоломок), - это распознавание кавычек.
В HTML вы можете использовать одиночные или двойные кавычки для указания
значений атрибутов. Это можно сделать так.
/ ! здесь мы используем обра тные апострофы, поскольку одиночные
/ / и дв ойные кавычки применяются в коде :
const html = ' ' +
' ' ;
const matches = html . match ( /< img alt= ( ? : [ ' " ] ) . * ? \ l / g ) ;

Обратите внимание, что в этом примере есть некоторое упрощение: если атрибут
alt не будет указан первым, наше регулярное выражение не сработает. Точно также
оно не сработает, если перед al t будет указан дополнительный пробел. Позже мы
вернемся к этому примеру и решим проблему.
Как и раньше, первая группа будет распознавать одиночную или двойную кавыч­
ку, сопровождаемую любым количеством символов (обратите внимание на вопро­
сительный знак, который делает распознавание ленивым), за которыми следует \ 1
т.е. любое первое соответствие одинарной или двойной кавычки.
Давайте закрепим понятие ленивых и жадных соответствий. Удалим вопроси­
тельный знак после *, сделав распознавание жадным. Запустите выражение снова.
Что вы видите? Понимаете, почему так получилось? Это очень важная концепция
для понимания регулярных выражений, поэтому, если остались неясности, я реко­
мендую вам прочитать еще раз раздел по ленивому и жадному распознаваниям.
-

Группы замены
Одним из преимуществ, предоставляемых группировкой, является способность
выполнять более сложные замены. Продолжая наш пример с НТМL-кодом, скажем,

Группы замены

285

что мы хотим отбросить все лишнее из дескриптора и оставить только его URL,
указанный в атрибуте href.
l e t html = ' Yep< / a> ' ;
html = html . replace ( / / , ' ' ) ;

Подобно обратным ссылкам, всем группам присваиваются номера начиная с 1 .
В самом регулярном выражении м ы обращаемся к первой группе как к \ 1 ; в стро­
ке для замены мы должны использовать $ 1 . Обратите внимание на использование
в этом регулярном выражении ленивых квалификаторов, чтобы оно не распростра­
нялось на несколько дескрипторов . Это регулярное выражение не сработает,
если в атрибуте hre f будут использоваться одинарные кавычки вместо двойных.
Давайте дополним пример. Сохраним только атрибуты class и href и ничего более.
let html = ' Yep ' ;
html = html . replace ( / / ,

' ' ) ;

Обратите внимание: в этом регулярном выражении мы изменяем порядок следо­
вания атрибутов class и href на обратный, чтобы первым шел атрибут href. Проб­
лема этого регулярного выражения в том, что если атрибуты class и href не рас­
полагаются друг за другом (как уже упоминалось выше), а также, если в них будут
использоваться одинарные кавычки вместо парных, оно не сработает. В следующем
разделе мы увидим еще более сложное решение.
Кроме выражений $ 1, $ 2 , существуют также $ ' (все перед соответствием), $ &
(само соответствие) и $ ' (все после соответствия). Если вы хотите использовать ли­
теральный знак доллара, используйте $ $ .
const input = " One two three " ;
input . replace ( /two / , ' ( $ ' ) ' ) ;
input . replace ( / \w+/g , ' ( $ & ) ' ) ;
input . replace ( /two / , " ( $ ' ) " ) ;
input . replace ( /t wo / , " ( $ $ ) " ) ;

//
//
11
11

"Оп е (Опе ) three "
" (One) (two) (three) "
"Опе ( three) thre e "
"Оп е ( $ ) three "

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

Функции замены
Это мое любимое средство регулярных выражений, которое зачастую позволяет
разделять очень сложные регулярные выражения на несколько более простых.
Давайте снова рассмотрим практический пример изменения элементов НТМL­
кода. Предположим, что вы пишете программу, которая преобразовывает все ссыл­
ки в очень специфический формат: вы хотите сохранить атрибуты c l a s s , i d
и href, н о удалить все остальные. Проблема в том, что код на входе может быть
беспорядочным. Атрибуты присутствуют не всегда, а когда они есть, вы не можете
286

Глава 1 7. Регулярные выражения

гарантировать, что они будут следовать в том же порядке. Таким образом, нужно
учитывать следующие варианты исходного кода (среди многих других).
const html =
' Foo< /a>\n ' +
' Foo \ n ' +
' Foo< /a>\n ' +
' Foo< / a> ' ;

К настоящему времени вам необходимо понимать, что это трудная задача для ре­
гулярного выражения: слишком много возможных вариантов! Однако мы можем
значительно сократить количество вариантов, разделив это регулярное выражение
на два: одно - для распознавания дескрипторов и второе - для замены содер­
жимого дескрипторов только тем, что вы хотите.
Давайте сначала рассмотрим вторую задачу. Если все, чего вы хотели, - это толь­
ко дескриптор , а все атрибуты, кроме c l a s s , id и href, можно отбросить, то
задача куда проще. Но даже в этом случае, как мы видели ранее, может возникнуть
проблема, если мы не сможем гарантировать, что атрибуты следуют в определен­
ном порядке. Есть несколько способов решить эту задачу, но мы будем использовать
String . prototype . spli t, чтобы просматривать атрибуты по одному.
function sani t i z eATag ( aTag) {
1 1 получить части дескриптора . . .
const parts = aTag . match ( /< a \ s + ( . * ? ) > ( . * ? ) < \ /a > / i ) ;
/ / parts [ 1 ] - а трибуты открывающего дескриптора
1 1 parts [2] - то, что между дескрипторами и
const attribut e s = parts [ l ]
1 1 теперь разделяем на отдельные а трибуты
. sp l i t ( / \s+ / ) ;
return ' '
/ / добавить содержимое
+ parts [ 2 ]
1 1 и завершающий дескриптор
+ ' < / а> ' ;

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

Функции замен ы

287

пробельных символов) и одно - для фильтрации только желательных атрибутов.
Было бы куда труднее сделать все это в одном регулярном выражении.
Теперь интересная часть: использование функции saniti zeATag в блоке HTML,
который, кроме прочего НТМL-кода, может содержать много дескрипторов .
Достаточно просто написать регулярное выражение для распознавания только де­
скрипторов .
html . ma t ch ( /< a . * ? > ( . * ? ) < \ / a > / ig ) ;

Но что нам с этим делать? Как можно передать функцию в S tr ing . prototype .
replace в качестве параметра замены. До сих пор в качестве параметра замены мы
использовали только строки. Функция позволяет предпринять специальное дей­
ствие для каждой замены. Прежде чем закончить свой пример, давайте используем
console . log, чтобы увидеть, как это работает.
html . replace ( / ( . * ? ) < \ / a > / i g , funct ion (m, g l , o f f s e t )
соnsоlе . lоg ( ' Дескриптор < а > найден в позиции $ { of f s et } . Содержимое :
$ { gl } 1 ) ;
});

Функция, которую вы передаете в S tring . prototype . replace, получает следую­
щие аргументы по порядку.


Вся соответствующая строка (эквивалент $&).



Соответствующая группа (если есть). Таких аргументов будет столько, сколько
есть групп.



Смещение в пределах исходной строки (число), где произошло распознавание.



Исходная строка (используется редко).

Здесь возвращаемое значение функции используется для замены текста в воз­
вращенной строке. В данном примере мы только регистрируем факт на консоли,
но ничего не возвращаем из функции. Таким образом, из функции будет возвраще­
но значение undefined, которое затем преобразовывается в строку и используется
для замены. Задачей этого примера была механика, а не фактическая замена; здесь
мы просто отбрасываем получающуюся строку.
Теперь вернемся к нашему примеру. У нас уже есть своя функция для санации
дескриптора и способ для поиска дескрипторов в блоке HTML, поэтому мы
можем просто их совместить.
html . replace ( / / i g , functi on ( m )
return s aniti z eATag (m) ;
});

{

Мы можем упростить это еще больше - полагая, что параметры в функции
sani t i z eATag точно соответствуют тому, что передает String . prototype . replace,
288

Гла ва

1 7.

Регулярные выражения

можно избавиться от анонимной функции и использовать saniti zeATag непосред­
ственно.
html . replace ( / / i g , saniti z eATag ) ;

Надеемся, что мощь этих функциональных возможностей очевидна. Всякий раз,
сталкиваясь с задачей, подразумевающей распознавание малых строк в пределах
большей строки и обработку меньших строк, помните, что можно передать функцию
в String . prototype . replace!

П р и вязка
Очень часто приходится учитывать вещи, которые должны происходить в начале
или конце строки, а также во всей строке сразу, а не в ее части. Здесь нам пригодятся
якоря (anchor). Есть два якоря: который соответствует началу строки, и $, который
соответствует концу строки.
л,

const
const
const
const
const
const

input
" I t was the best of t ime s , i t was the worst o f t ime s " ;
beginning
inpu t . match ( / л \wt / g ) ;
1 1 "It "
end
input . match ( / \w+ $ /g ) ;
1 1 " times "
everything
input . match ( / л . * $ /g ) ; 1 1 то же , что и на входе
input . match ( / лbe s t / ig ) ;
nomatchl
nomatch2
inpu t . match ( /wor s t $ / ig ) ;
=

=

=

=

=

У якорей есть еще один нюанс, о котором следует знать. Обычно они соответ­
ствуют началу и концу всей строки, даже если в ней есть символы новой строки.
Если вы хотите обработать строку как многострочную (разделенную символами но­
вой строки), используйте параметр m (multiline).
const input
"One l i n e \ nTwo l i ne s \nThree l ines \nFour " ;
const beginnings
input . match ( / л \w+/mg ) ; / / [ "One ", " Two ", " Thre e " , "Four "J
const endings = inpu t . match ( / \w+ $ /mg ) ;
// [ "lin e " , "lines " , "lines " , "Four"J
=

=

Распознавание границ сn ов
Одним из малоиспользуемых, но полезных элементов регулярных выражений, яв­
ляются границы слова. Подобно якорям начала и конца строки, метасимвол грани­
цы слова \Ь и его инверсия \В не перерабатывает входные данньtе. Это может быть
очень удобным свойством, как мы вскоре убедимся.
Граница слова определяется местом, где метасимволу \w предшествует или следу­
ет после него метасимвол \W (не слово) либо символ начала или конца строки. Пред­
положим, что вы пытаетесь заменить адреса электронной почты в английском тексте
гиперссылками (в этом примере мы подразумеваем, что адреса электронной почты
начинаются с символа и заканчиваются символом). Обсудим ситуации, которые сто­
ит рассмотреть.
П ривязка

289

const inputs = [
" j ohn@doe . coт" ,
" j ohn@doe . coт is ту eтa i l " ,
"ту eтa i l i s j ohn@doe . coт" ,
" u s e j ohn@doe . coт, ту eтa il " ,
"ту eтai l : j ohn@doe . coт . " ,
];

//

ТОЛЬКО

адрес

11 адрес вна чале
11 адрес в конце
11 адрес в середине с запятой после
! / адрес окружен пунктуацией

Учесть следует много, но все эти адреса электронной почты существуют в грани­
цах слова. Еще одно преимущество маркеров границ слов в том, что, поскольку они
не перерабатывают входные данные, мы не должны заботиться об их "откладыва­
нии" в строке замены.
const eтa i lMatcher =
/ \Ь [ а- z ] [ a - z 0 - 9 . _- ] * @ [ a - z ] [ a - z 0 - 9_- ] + \ . [ a - z ] + ( ? : \ . [ a - z ] + ) ? \Ь / i g ;
input s . тар ( s => s . replace ( eтa i lMatcher , ' $ &< / а> ' ) ) ;
/ / в озвраща ет [
"j ohn @doe. сот " ,
//
"j ohn @doe. сот is ту ema i l " ,
//
"ту eтa i l i s john@doe. coт " ,
//
"use j ohn@doe . coт , ту eтa i l " ,
//
"ту eтa i l : ), можно вы­
брать узлы, которые являются прямыми потомками указанного элемента. Например,
#content > р выберет элементы , которые находятся в элементе с идентифика­
тором content (сравните это с " # c ontent р " ) .

298

Гла ва 1 8. JavaScript в браузере

Обратите внимание, что вы можете объединить предков с прямыми потомками. На­
пример, body . content > р выберет дескрипторы , которые являются прямыми по­
томками элементов класса content, которые являются потомками дескриптора .
Существуют куда более сложные селекторы, но здесь рассматриваются лишь наи­
более распространенные. Более подробная информация по этой теме приведена
в части по селекторам в документации MDN ( ht tps : / / developer . moz i l l a . org/ru /
doc s /Learn/CS S / Introduction_to_CSS/ Selectors ) .

М анипулирование элементами DOM
Теперь, когда известно, как обходить, находить и выбирать элементы, возникает
вопрос "Что с ними делать?" Начнем с модификации содержимого. У каждого эле­
мента есть два свойства, textContent и innerHTML, которые позволяют получить до­
ступ к содержимому элемента и изменить его. Свойство textContent содержит только
"голые" текстовые данные НТМL-дескриптора, а inne rHTML позволяет использовать
НТМL-код (что приводит к образованию новых узлов DOM). Давайте рассмотрим, как
можно обратиться к первому абзацу в нашем примере и изменить его.
const paral
document . getElementsByTagName ( ' p ' ) [ О ] ;
para l . t extContent ; / / "Это простой НТМL-файл . "
para l . innerHTML;
/ / "Это простой НТМL-файл . "
para l . t extContent
"Измененный НТМL-файл " ; / / Посмотрите в браузере ,
/ / что изменилось !
para l . innerHTML
" < i>Измененный< / i > НТМL-файл " ;
=

=

=

-

Присвоение значений свойствам t extContent и inne rHTML являет­
ся деструктивной операцией: она заменят то, что находится в элементе, независимо от его размера или сложности. Например, мож­
но заменить сразу все содержимое веб-страницы, изменив свойство
innerHTML элемента !

Создание новых элементов DOM
Мы уже видели, как можно неявно создать новый узел DOM, изменив значение
свойства innerHTML элемента. Но можно и явно создать новый узел, используя метод
document . createE l ement. Он создает новый элемент, но не добавляет его в дерево
DOM; вам нужно будет сделать это самостоятельно позже. Давайте создадим два
новых элемента абзаца; один станет первым абзацем в блоке ,
а второй - последним.
const pl = document . createElement ( ' p ' ) ;
const р2 = document . createElement ( ' p ' ) ;
p l . t extContent
"Это было создано динамически ! " ;
p2 . textContent = "И это тоже было создано динамически ! " ;

Манипуп ирован ие элементами DOM

299

Чтобы добавить эти вновь созданные элементы в DOM используются методы
insertBe fore и appendChi ld. Но сначала мы должны будем получить ссылки на ро­
дительский элемент DOM ( ) и его первый дочерний узел.
cons t parent = document . getElementByid ( ' content ' ) ;
const firstChild = paren t . childNode s [ O ] ;

Теперь можно вставить вновь созданные элементы.
parent . insertBefore ( p l , firstChild ) ;
parent . appendChild ( p2 ) ;

Методу inse rtBefore передается сам вставляемый элемент и ссылка на узел, пе­
ред которым должна быть выполнена вставка. Метод appendChild очень прост, он
просто добавляет определенный элемент в конец списка узлов данного элемента (т.е.
создает последний дочерний узел).

П р именение стиле й к элементам
API DOM обеспечивают полный контроль над стилями элементов. Однако вместо
изменения свойств индивидуальных элементов хорошей практикой обычно считается
использование классов CSS. Таким образом, если вы хотите изменить стиль элемента,
создайте новый класс CSS, а затем примените его к элементу (или элементам), стиль
которого собираетесь изменить. Используя JavaScript, довольно просто применить су­
ществующий класс CSS к элементу. Например, если нам нужно выделить все абзацы,
в которых содержится слово уникальный, то сначала создадим новый класс CSS.
. highlight {
background : # ffO ;
font-style : i t a l i c ;

Теперь можно найти все дескрипторы и, если они содержат слово уникаль­
ный, добавить к ним класс highl ight. У каждого элемента есть свойство clas sList,
которое содержит все имеющиеся у элемента классы (если они есть). У свойства
c l a s s L i s t есть метод add, который позволяет добавлять новые классы. Мы будем
использовать этот пример далее в главе, поэтому поместим его в функцию по имени
highlightParas.
funct ion highlightPara s ( containing )
i f ( typeo f containing
' string ' )
containing
new RegExp ( ' \ \Ь$ { containing } \ \Ь ' ,
const paras = document . getE lementsByTagName ( ' p ' ) ;
console . l og ( para s ) ;
for ( le t р of para s ) {
i f ( ! containing . te s t ( p . textContent ) ) continue ;
p . classLi s t . add ( ' highl ight ' ) ;
===

=

300

Глава 1 8. JavaScript в браузере

'i');

highl ight Paras ( ' уникальный ' ) ;

А впоследствии, если мы захотим удалить выделение, мы сможем использовать
метод classLi s t . remove.
function rernove ParaHighlight s ( )
const paras = docurnent . querySelectorAl l ( ' p . hi gh light ' ) ;
for ( let р of para s ) {
p . classList . rernove ( ' highl ight ' ) ;

При удалении класса h i gh l i g h t можно многократно исполь­
зовать одну и ту же переменную paras и просто вызвать метод
remove ( ' highl i ght ' ) для каждого элемента абзаца. Однако это не
сработает, если элементу еще не был назначен класс. Но вероятнее
всего, удаление нужно будет выполнить в некий более поздний мо­
мент времени, когда, возможно, будут выделены абзацы, добавленные
другим кодом. Поэтому если наше намерение заключается в том, что­
бы снять все, что выделено, использование метода queryS e lectorAll
является самым надежным способом.

Атрибуты данных
В HTMLS введены атрибуты данных (data attribute), которые позволяют добав­
лять произвольные данные к НТМL-элементам; браузером эти данные не отобра­
жаются, но они позволяют добавлять к элементам информацию, легко читаемую
и изменяемую с помощью JavaScript. Давайте изменим наш НТМL-код, добавив
кнопки, которые в конечном счете соединим с нашими функциями highl ightparas
и remove ParaHighlights.

Выделяет абзацы, содержащие слово " уникальный"


Удаляет вьщеление
< /button>

Назовем наши атрибуты данных act ion и conta ins (имена мы выбираем сами),
и используем document . que rySelectorAll для поиска всех элементов, имеющих дей­
ствие "highlight " .

Атрибуты да н н ых

301

const highl ightAc t i ons
action= "highlight " ] ' ) ;

document . querySelectorAl l ( ' [ data-

Тем самым мы ввели новый тип селектора CSS. До сих пор мы сталкивались с се­
лекторами, которые соответствовали определенным именам дескрипторов, классов
и идентификаторов. Синтаксис с использованием квадратных скобок позволяет искать
элементы с любым атрибутом". в данном случае - с определенным атрибутом данных.
Поскольку у нас есть только одна кнопка, мы можем использовать метод
querySelector вместо querySe l e ctorAl l . Однако последний позволяет нам обраба­
тывать сразу несколько элементов, предназначенных для выполнения одного и того
же действия (что весьма распространено: вспомните о действиях, которые можно
выполнить через меню, ссылку или панель инструментов, и все на той же странице).
Если обратить внимание на один из элементов в highlightAc t i ons, то можно заме­
тить, что у него есть свойство dataset.
highlightAct ions [ O ] . datas e t ;
/ / DOМStringMap { con taining: "уникальный " , a c tion : "highligh t "

Согласно API DOM значения атрибутов данных хранятся в виде
строк (как и предполагалось при реализации класса DOMSt r ingMap) .
Таким образом, вы не можете хранить в атрибутах данных объектные
данные. jQuery расширяет функциональные возможности атрибутов
данных, предоставляя интерфейс, который позволяет хранить объек­
ты как атрибуты данных, о чем мы узнаем в главе 1 9.
Используя JavaScript, можно также изменять или добавлять атрибуты данных. На­
пример, если бы мы хотели выделить абзацы со словом жираф и указать, что регистр
символов имеет значение, то мы могли бы поступить так.
highlightActions [ O ] . dataset . containing = "жираф " ;
highli ghtAc t i ons [ O ] . dataset . caseSen s i t ive = " true " ;

События
В API DOM описано почти 200 событий, и в каждом браузере дополнительно ре­
ализованы свои нестандартные события, поэтому мы, конечно, не будем обсуждать
здесь все события, но рассмотрим то, что о них необходимо знать. Начнем с очень
простого для понимания события c l i ck. Мы будем использовать событие c l i c k
для соединения нашей кнопки "выделения" с нашей функцией highl i ghtParas.
const highlightAct ions = document . querySelectorAll ( ' [ dataaction= " highlight " ] ' ) ;
for ( l e t а of highl ightActions ) {
a . addEventLi stene r ( ' cl i ck ' , evt => {

302

Глава

1 8.

JavaScript в браузере

evt . preventDefault ( ) ;
highlightParas ( a . dataset . containing ) ;
})

i

const removeHigh l i ghtActions =
document . querySe l e ctorAl l ( ' [ da ta-act ion=" removeHighlights " ] ' ) ;
for ( le t а o f removeHighli ghtActions ) {
a . addEventListener ( ' cl i ck ' , evt = > {
evt . preventDe fault ( ) ;
removeParaHighlight s ( ) ;
}) i

У каждого элемента есть метод addEventListener, который позволяет определять
функцию, вызываемуiо, когда происходит указанное событие. Этой функции пере­
дается один аргумент - объект типа Event. Объект события содержит всю необхо­
димую информацию о событии, которая специфична для данного типа события. На­
пример, событие click будет иметь свойства clientX и clientY, которые указывают
координаты, где произошел щелчок кнопкой мыши, а также свойство target - эле­
мент, для которого событие click было сгенерировано.
Модель событий спроектирована так, чтобы несколько обработчиков могли об­
рабатывать одно и то же событие. У многих событий есть стандартные обработчики;
например, если пользователь щелкает на ссылке , то браузер обработает это со­
бытие, загрузив нужную страницу. Если вы хотите предотвратить такое поведение,
вызовите метод preventDefault ( ) для объекта события. В большинстве обработчи­
ков событий, которые вы пишете, нужно вызывать метод preventDe faul t ( ) (если
только вы не хотите добавить нечто к стандартному обработчику).
Для выделения текстового абзаца в нашем примере вызывается функция
highl i ghtParas, которой передается значение элемента данных containing кноп­
ки: это позволяет оперативно изменять выделяемое слово, просто отредактировав
НТМL-код!

П ерехват и всплытие событий
Поскольку НТМL-документ имеет иерархический характер, события могут быть
обработаны в нескольких местах. Например, если вы щелкаете на кнопке, то событие
могла бы обработать сама кнопка, родитель кнопки, родитель родителя и т.д. По­
скольку обработать событие могут несколько элементов, возникает вопрос "В каком
порядке элементы получают возможность отреагировать на событие?"
По существу, есть две возможности. Первая - в предке начиная с самого даль­
него. Это так называемый перехват (capturing) события. В нашем примере кнопки
являются дочерними для элемента , который, в свою очередь,

События

303

является дочерним для . Поэтому у элемента есть возможность пере­
хватыват ь события, исходящие от кнопок.
Вторая возможн ость начинает ся с элемента, где событие произош ло, а затем
вверх по иерархии , чтобы у всех предков был шанс отреагир овать. Это так называе­
мое всплытие (bubЬling) событий .
Для поддержки обоих возможно стей распрост ранение событий в HTMLS начи­
нается с разрешен ия обработчи кам перехватывать события (начиная с самого даль­
него предка и вниз к исходному (target) элементу), а затем события всплывают вверх
от исходного элемента к самому дальнему предку.
Для обеспечения возможности вызова дополнительных обработчиков любой об­
работчик может опционально предпринять одно из трех действий. Первое и наибо­
лее распространенное, которое мы уже видели, - это вызов метода preventDe faul t,
который отменяет событие. Отмененные события продолжают распростр аняться,
но их свойство default Prevented устанавлив ается равным t rue. Встроенные в бра­
узер обработчики событий проверяют значение свойства default Prevented и, если
оно истинно, ничего не предпринимают. Обработчик и событий, которые вы пишете,
могут проигнорир овать значение этого свойства (и обычно так и делают). Второй
подход подразумевает вызов метода s t opPropa g a t i on, что предотвращает даль­
нейшее распространение события за текущий элемент. При этом все обработчики ,
связанные с текущим элементом, будут вызваны, но никакие из обработчиков, свя занные с другими элементами, не вызываются. И наконец ( крупный калибр ) вызов
метода stopimmediatePropagat i on запретит вызов дальнейших о бработчиков (даже
если они связаны с текущим элементом.
Для демонстрации всех этих действий рассмотрим следующий НТМL-код.
< ! doctype html>


< t i t lе >Ра спространение событий< / t i t l е >

< /head>


Щелкни здecь !

< script>
11 это созда ст обра ботчик событий и в озвратит его
funct i on logEvent ( handlerName , t ype , cance l ,

s t op , stopimmediat e )

{

1 1 это фактический обработчик событий

return funct ion ( evt ) {
i f ( cance l ) evt . preventDefault ( ) ;
i f ( st o p ) evt . st opPropagation ( ) ;

304

Глава 1 8. JavaScript в браузере

i f ( stopirnmediat e ) evt . s t op irnmediat e Propaga t i on ( ) ;
console . log ( ' $ { type } : $ { handlerName } ' +
( evt . default Prevented ? ' ( отменено ) ' : ' ' ) ) ;

1 1 это добавляет регистра тор события к элементу
function addEventLogger ( el t , t ype , act i o n ) {
const capture = t yp e === ' capture ' ;
e l t . addEventList ener ( ' cl i ck ' ,
l ogEvent ( elt . tagName , t yp e , a c t ion=== ' cancel ' ,
a c t i on=== ' st op ' , action=== ' st op ! ' ) , captur e ) ;

cons t body = document . querySelector ( ' body ' ) ;
con s t div = document . querySelector ( ' div ' ) ;
const butt o n
document . querySel e ctor ( ' bu t t on ' ) ;
=

addEventLogger ( body, ' capture ' ) ;
addEventLogger ( body, ' ЬuЬЫе ' ) ;
addEventLogger ( div, ' capture ' ) ;
addEventLogger ( div, ' ЬuЬЫе ' ) ;
addEventLogger ( button, ' capture ' ) ;
addEventLogger ( button , ' ЬuЬЫе ' ) ;
< / s cript>
< /body>


Щелкнув на кнопке, вы увидите на консоли следующее.
capture : BODY
capture : DIV
capture : BUTTON
ЬuЬЫ е : BUTTON
ЬuЬЫ е : DIV
ЬuЬЫ е : BODY

Здесь мы ясно видим процесс перехвата событий, сопровождаемый их всплыти­
ем. Обратите внимание, что обработчики элемента, на котором фактически произо­
шло событие, будут вызваны в порядке их добавления, будь то перехват или распро­
странение события (если мы изменим на обратный порядок, в котором мы добавля­
ли обработчики перехвата и всплытия событий, мы увидим, что всплытие следует
перед перехватом).
Рассмотрим, что произойдет при отмене распространения события. Изменим
пример так, чтобы отменить распространение при перехвате события в .
addEventLogger ( body,
addEventLogger ( body ,

' capture ' ) ;
' ЬuЬЫе ' ) ;

addEventLogge r ( div, ' capture ' , ' cancel ' ) ;
addEventLogger ( di v , ' ЬuЬЫе ' ) ;
addEventLogger ( button , ' capture ' ) ;
addEventLogger ( button, ' ЬuЬЫе ' ) ;

Здесь распространение продолжается, но событие отмечено как отмененное.
capture : BODY
capture : DIV ( о тменено )
capture : BUTTON ( отменено )
ЬuЬЫ е : BUTTON ( о тменено )
ЬuЬЫ е : D IV ( о тменено )
ЬuЬЫ е : BODY ( о тменено)

Остановим распространение при перехвате в элементе .
addEventLogger ( body , ' capture ' ) ;
addEventLogger ( body, ' ЬuЬЫе ' ) ;
addEventLogger ( div, ' capture ' , ' cancel ' ) ;
addEventLogger ( div, ' ЬuЬЫе ' ) ;
addEventLogger ( button , ' capture ' , ' st op ' ) ;
addEventLogger ( button, ' ЬuЬЫе ' ) ;

Мы видим, что распространение события остановилось после элемента .
Для элемента события все еще генерируются, даже несмотря на то, что
произошел перехват события и остановлено его распространение. Элементы
и , однако, не получают всплывающие события.
capture : BODY
capture : DIV ( cancel e d )
capture : BUTTON ( canceled)
ЬuЬЫ е : BUTTON ( canceled)

Наконец выполним немедленную остановку распространения при перехвате со­
бытия в элементе .
addEventLogger ( body, ' capture ' ) ;
addEventLogger ( body, ' ЬuЬЫе ' ) ;
addEventLogger ( div, ' capture ' , ' cancel ' ) ;
addEventLogger ( div, ' ЬuЬЫе ' ) ;
addEventLogger ( button, ' capture ' , ' stop ! ' ) ;
addEventLogger ( button, ' ЬuЬЫе ' ) ;

Распространение события полностью останавливается приперехвате в элементе
, никакого д·альнейшего распространения не происходит.
capture : BODY
capture : DIV ( canceled)
capture : BUTTON ( canceled)

306

Глава 1 8 . JavaScript в браузере

Метод addEventL i s tener заменяет уже устаревший способ добавле­
ния событий с использованием свойств "on . . . ': Например, обработ­
чик щелчка мог бы быть добавлен к элементу e l t с помощью кода
e l t . onclick
function ( evt ) { / * обработчик * / } . Основной
недостаток этого метода в том, что за раз может быть зарегистриро­
ван только один обработчик.
=

Хотя маловероятно, что вам придется создавать сложную систему управления
распространением событий, очень часто, эта тема вызывает большие затруднения
среди новичков. Хорошо понимая детали распространения события, вы сможете
возвыситься над толпой.
В библиотеке jQuery обработчики событий могут явно возвращать
значение fal se. Это эквивалентно вызову метода s topPropagation
в обработчике. Данное соглашение jQuery, и оно не распространяется
на АРI DOM.

Категории событий
В MDN есть превосходный справочник по всем событиям DOM, сгруппированным
по категориям. Вот некоторые из наиболее распространенных категорий событий.
События п еретаскивания

Позволяют реализовать интерфейс перетаскивания с использованием таких со­
бытий, как dragstart, drag, dragend, drop и др.
События ф ок уса

Позволяют принимать меры, когда пользователь взаимодействует с редактиру­
емыми элементами (такими, как поля формы). Событие focus происходит, когда
пользователь "переходит" к полю (щелкнув на нем или использовав клавишу
либо касание), а Ыur - когда пользователь "покидает" поле (щелкнув где-то еще,
нажав или выполнив касание в другом месте). Событие change происходит,
когда пользователь вносит изменение в поле.
События формы

Когда пользователь передает данные формы на сервер (щелкнув на кнопке Отпра­
или нажав клавишу в соответствующем контексте), в ней происходит
событие submi t.
вит ь

События устройства ввода данных

Мы уже встречали событие c l ic k, но есть и дополнительные события для мыши
(mou s e down, move, mou s eup, mou s eenter, mou s e l e ave, mous eover, mou s ewh e e l )

События

307

и клавиатуры ( ke ydown, keypre s s , keyup ) . Обратите внимание, что события "ка­
сания" (для устройств с сенсорным экраном) имеют приоритет перед событиями
мыши, но если сенсорные события не обрабатываются, они приводят к событиям
мыши. Например, если пользователь касается кнопки и это событие не будет обра­
ботано явно, то будет передано событие c l i ck.
События м ул ьтим е дийной сре ды

Позволяет отслеживать взаимодействие пользователя с видео и аудиоустройства­
ми в HTMLS (pause, play и т.д.).
События хода выпол не ния

Сообщают о ходе выполнения работы браузером при загрузке содержимого. Наи­
более распространено событие l oad, происходящее, как только браузер загружает
элемент и все его зависимые ресурсы. Событие error также полезно; оно позволяет
принять меры, когда элемент недоступен (например, некорректная ссылка на изоб­
ражение).
События касания

События касания обеспечивают всестороннюю поддержку обработчиков для сен­
сорных устройств. Разрешается несколько одновременных касаний (посмотрите опи­
сание свойства touches элемента события), обеспечивающих сложную сенсорную
обработку, такую как поддержка жестов (сжатие, сдвиг и т.д.).

Ajax
Ajax (Asynchronous Javascript And Xml) технология обращения к серверу без пе­
резагрузки страницы. Обеспечивает асинхронное взаимодействие с сервером, позво­
ляя элементам на странице обновлять данные с сервера, не перезагружая всю стра­
ницу. Это новшество стало возможным благодаря введению объекта XMLHttpReques t
в начале 2000-х годов и возвестило начало эры "Web 2.0".
Базовая концепция Ajax проста: код JavaScript на стороне браузера программ­
но осуществляет НТТР-запросы к серверу, который возвращает данные, обычно
в формате JSON (с которым намного проще работать в JavaScript, чем с XML). Эти
данные используются для обеспечения функциональных возможностей в браузере.
Хотя в Ajax используется протокол НТТР (точно так же, как и для пересылки
веб-страниц без использования Ajax), накладные затраты на передачу и визуализа­
цию страницы снижаются. Это позволяет веб-приложениям выполняться намного
быстрее, или по крайней мере выглядеть так с точки зрения пользователя.
Чтобы использовать Ajax, необходим сервер. Давайте напишем чрезвычайно про­
стой сервер в Node.js (это тема главы 20), который предоставляет доступ к конечной
-

308

Глава 1 8. JavaScript в браузере

точке (endpoint) Ajax (особой службе, которая может использоваться в других служ­
бах или приложениях). Создайте файл aj axServe r . j s .
const http = require ( ' ht tp ' ) ;
const server = http . createServe r ( funct ion ( req, re s ) {
res . s etHeader ( ' Content-Type ' , ' application / j son ' ) ;
res . s etHeader ( ' Acce s s-Contro l-Allow-Origin ' , ' * ' ) ;
res . end ( JSON . stringify ( {
plat form : proce s s . p l a t form,
nodeVe r s i on : proce s s . version,
upt ime : Mat h . round ( proce s s . upt ime ( ) ) ,
}));
}) ;

const port = 7 0 7 0 ;
server . l i s ten ( p o r t , function ( ) {
console . log ( ' Aj ax server s tarted on port $ { port } ' ) ;
});

Это код очень простого сервера, который сообщает свою платформу ("linux",
"darwin'; "win32" и т.д.), версию Node . j s и продолжительность непрерывной работы
сервера.
Благодаря Ajax создается брешь в системе безопасности сайта, назы­
ваемая кросс-доменными запросами (Cross-Origin Resource Sharing CORS). В этом примере мы добавляем заголовок Acc e s s - Control­
Allow-Origin со значением *, который уведомляет клиента (браузер)
о том, что не нужно предотвращать вызов из соображений безопасности. На реальном сервере обычно используется тот же самый про­
токол, домен и порт (который разрешен по умолчанию), либо явно
указывается, какой протокол, домен и порт может обращаться к ко­
нечной точке. Тем не менее в демонстрационных целях, лучше всего
отключить CORS.
Для запуска этого сервера достаточно выполнить команду
$ baЬel-node aj axServer . j s

Загрузив страницу http : / / localho s t : 7 07 0 в браузер, вы увидите вывод сервера.
Теперь, когда имеется сервер, добавим код Ajax в наш пример НТМL-страницы (вы
можете использовать ту же страницу, что и ранее в этой главе). Для начала добавим
где-нибудь в теле документа элемент, в котором будет отображаться информация.

Сервер н а платформе ? ? ? < / span> ,
версия Node < span data-replace="nodeVers ion " > ? ? ? < / span> . Время

Ajax

309

его непрерывной работы ? ? ? < /span> секунд .


Теперь, когда есть элемент для отображения данных, поступающих с сервера, мы
можем использовать объект XMLHttpRequest, чтобы выполнить Аjах-запрос. Внизу
вашего НТМL-файла (прямо перед закрывающим дескриптором < /body> ), добавьте
следующий код.
< script t ype= " application/j avascrip t ; version=l . 8 " >
function refreshSe rverinfo ( ) {
const req = new XMLHttpReque st ( ) ;
req . addEventListener ( ' l oad ' , funct ion ( )
/ / TODO: внести эти данные в НТМL-код
console . log ( th i s . re sponseText ) ;
}) ;
req . open ( ' GET ' , ' ht tp : / / localhost : 7 0 7 0 ' , true ) ;
req . send ( ) ;
refreshServerinfo ( ) ;
< / script>

Этот сценарий осуществляет простой Ajax-запрос. Сначала мы создаем новый
объект XMLHttpRe que s t , а затем добавляем обработчик, который перехватыва­
ет событие load (он будет запущен, если Аjах-запрос успешно завершится). В нем
мы пока что только выводим ответ сервера (который находится в свойстве thi s .
re spons eText) на консоль. Затем происходит вызов метода open, который факти­
чески устанавливает соединение с сервером. Мы определяем, что это НТТР-запрос
GET. Это такой же тип запроса, который используется в браузере при посещении веб­
страницы (есть и другие типы запросов: POST, DELETE и др.). Далее мы сообщаем
методу URL сервера. И наконец происходит вызов метода s end, который фактически
выполняет запрос. В этом примере мы явно не посылаем данные на сервер, хотя мог­
ли бы это сделать.
Запустив этот пример, вы увидите данные, возвращаемые с сервера и отображае­
мые на консоли. Наш следующий шаг - поместить эти данные в наш НТМL-код. Мы
структурировали свой HTML так, чтобы проще было найти любой элемент, у кото­
рого есть атрибут данных replace, и заменить содержимое этого элемента данными
из возвращенного объекта. Для этого мы перебираем свойства, которые были воз­
вращены сервером (с использованием метода Ob j ec t . keys), и если есть какие-ни­
будь элементы с соответствующими атрибутами данных replace, мы заменяем их
содержимое.
req . addEventListener ( ' lo ad ' , function ( ) {
1 1 thi s . responseText - это строка , содержащая JSON; мы используем
/ / JSON.parse , чтобы преобразовать ее в объект
const data = JSON . parse ( t hi s . responseText ) ;

310

Глава

1 8.

JavaScript в браузере

1 1 В этом примере мы только хотим заменить текст в пределах ,
1 1 имеющего кла сс "serverinfo "

const serverinfo = document . querySe l ector ( ' . se rverinfo ' ) ;
1 1 Перебор по ключам в объекте, возвращенном с сервера
1 1 ( "pla tform " , "nodeVers i on " и "up t ime ") :

Obj e ct . keys ( d a t a ) . forEach ( p > {
1 1 Найти элементы для замены для этого свойства (если есть)
cons t replacements =
serverinfo . queryS e le ctorAl l ( ' [ da ta-replace= " $ { р } " ] ' ) ;
1 1 заменить все элементы зна чением, возвращенным с сервера
for ( l e t r o f replacements ) {
r . t extContent
data [ p ] ;
=

=

});
});

Поскольку refreshServe rinfo - это функция, мы можем вызвать ее в любое
время. В частности мы можем обновлять информацию, полученную с сервера пери­
одически (вот почему мы добавили поле upt ime ) . Например, если мы хотим обнов­
лять информацию с сервера пять раз в секунду (каждые 200 мс), то можем добавить
следующий код.
s e t interval ( refre shServerinfo, 2 0 0 ) ;

Сделав это, мы будем наблюдать в браузере последовательное увеличение време­
ни непрерывной работы сервера!
В этом примере, когда страница загружается впервые, в элементе находится текстовый заполнитель в виде во­
просительных знаков. При медленном соединении с Интернетом
пользователь может увидеть эти вопросительные знаки на мгновение,
прежде чем они будут заменены информацией, полученной от сервера. Это известная проблема появления нестилизованного содержимо­
го (Flash Of Unstyled Content - FOUC). Одно из ее решений подра­
зумевает получение от сервера начальной страницы с правильными
значениями. Другое решение - полностью скрыть элемент, пока его
содержимое не будет изменено. Это может вызывать раздражение
у пользователей, но зачастую это куда лучше, чем созерцание бес­
смысленных вопросительных знаков.
В этом разделе были рассмотрены только основные концепции, используемые
при создании Аjах-запросов. Более подробная информация по этой теме приведена
в статье " Using XMLHttpReqиest" библиотеки MDN.

Ajax

311

Закn ючение
Как в ы уже наверное заметили, в этой главе, при создании веб-приложения, кро­
ме самого языка JavaScript, мы использовали несколько сложных технологий. Здесь
мы затронули их только поверхностно, и если вы - веб-разработчик, я рекомен­
дую книгу Сэмми Пьюривала (Semmy Purewal) Основы разработки веб-приложений
(Learning Web Арр Development), а если вы хотите узнать больше о CSS, то любую из
книг Эрика А. Мейера (Eric А. Meyer).

31 2

Гла ва 1 8 . JavaScript в браузере

ГЛАВА 1 9

Б и б лиоте к а jQuery

jQuery - это популярная библиотека для манипулирования элементами DOM
и выполнения Аjах-запросов. Библиотека jQuery не может сделать ничего, что вы не
смогли бы сделать с API DOM (в конце концов, jQuery сама основана на API DOM),
но она предоставляет три основных преимущества.


jQuery избавляет от необходимости заботься об индивидуальных особенно­
стях различных браузеров, реализующих API DOM (особенно устаревших).



jQuery предоставляет упрощенный API Ajax (что очень кстати, поскольку
на нынешних веб-сайтах использовать Ajax сложно).



j Query предоставляет множество мощных и компактных р асширений
для встроенного API DOM.

Сегодня наблюдается рост сообщества веб-разработчиков, полагающих, что
в jQuery больше нет необходимости, поскольку API DOM и современные браузе­
ры достигли совершенства. Это сообщество рекламирует эффективность и чистоту
"традиционного JavaScript". Это правда, что первый пункт (особенности браузера)
со временем становится менее актуальным, но полностью он не снимается. Я пола­
гаю, что библиотека jQuery до сих пор остается актуальной и предоставляет много
средств, повторная реализация которых с помощью API DOM отняла бы чрезвычай­
но много времени. Решите вы использовать jQuery или нет, ее высокая популярность
требует от квалифицированного веб-разработчика знания хотя бы ее основ.

В семо rущ ий доллар (знак)
jQuery была одной из первых библиотек, в которых использовалось включение
в JavaScript знака доллара как идентификатора. Возможно, первоначально это реше­
ние принималось из-за оригинальности разработчиков, но сейчас, благодаря везде­
сущности jQuery, оно оказалось поистине пророческим. Включив jQuery в свой про­
ект, вы можете использовать либо переменную j Query, либо намного более краткий
псевдоним $.1 Здесь мы будем использовать псевдоним $ .
1 Можно запретить jQuery использовать псевдоним $ , если это вступает в противоречие с другой
библиотекой (см. jQuery. noConflict).

Подкл ю ч ение jQuery
Самый простой способ подключения библиотеки jQuery - это использовать сеть
CDN.
< / s cript>

В jQuery 2.х уже не поддерживаются устаревшие браузеры Internet
Explorer 6, 7 и 8. Если необходима поддержка для этих браузеров, ис­
пользуйте jQuery 1 .х. Библиотека jQuery 2.х значительно меньше и про­
ще, поскольку не должна поддерживать эти устаревшие браузеры.

Ожидание за rрузки и построения дерева DOM
Способ, которым браузер читает, интерпретирует и визуализирует НТМL-файл,
довольно сложен, и многие веб-разработчики по неосторожности сталкиваются с не­
приятностями при попытке программного доступа к элементам DOM прежде, чем
у браузера будет шанс их загрузить.
jQuery позволяет вам поместить свой код в функцию обратного вызова, которая бу­
дет вызвана, как только браузер полностью загрузит страницу и построит дерево DOM.
$ ( document ) . ready ( funct i on ( ) {
1 1 расположенный здесь код запускается после загрузки
/ / всего HTML и построения дерева DOM
});

Эту методику можно вполне безопасно использовать многократно, что позволяет по­
мещать код jQuery в различные места и все еще иметь безопасное ожидание построения
дерева DOM. Есть также сокращенная версия, которая эквивалентна предыдущей.
$ ( function ( ) {
1 1 ра сположенный здесь код запускается после загрузки
1 1 всего НТМL-документа и построения дерева DOM
});

Помещение всего кода в такой блок является общепринятой практикой при ис­
пользовании библиотеки jQuery.

Элементы DOM в оболоч ке jQuery
Основная методика манипулирования DOM с использованием jQuery - это эле­
менты DOM в оболочке jQuery (jQuery-wrapped DOM elements). Любая манипуля­
ция DOM, осуществляемая с использованием jQuery, начинается с создания объекта
jQuery, являющегося "оболочкой" для набора элементов DOM (имейте в виду, что
набор может быть пустым или содержать только один элемент).
314

Глава 1 9 . Б иблиот ека jQuery

Функция jQuery ($ или j Query) создает набор элементов DOM в оболочке jQuery
(который начиная с этого момента мы будем называть "объект jQuery" (jQuery
object); просто помните, что объект jQuery содержит набор элементов DOM!). Функ­
цию jQuery главным образом вызывают одним из двух способов: с селектором CSS
или с НТМL-кодом.
Вызов jQuery с селектором CSS возвращает объект jQuery, соответствующий это­
му селектору (подобно возвращению из document . que rySelectorAl l ) . Например,
чтобы получить объект jQuery, соответствующий всем дескрипторам , просто
сделайте так.
const $paras

$ ( 'р' ) ;
/ / количество соответствующих дескрипторов а бзаца
$paras . lengt h ;
/ / "obj ec t "
t ypeof $para s ;
$paras instanceof $ ;
/ / true
$paras instanceof j Query; // true
=

Вызов jQuery с НТМL-кодом, напротив, создает новые элементы DOM на осно­
вании HTML, который вы предоставляете (подобно тому, как это происходит при
установке значения свойства innerHTML элемента).
const $newPara

=

$ ( ' Только что созданньм абзац . . . ' ) ;

Обратите внимание, что имя переменной, которой мы присваиваем объект
jQuery, в обоих примерах начинается со знака доллара. Необходимости в этом нет,
но следовать этому соглашению имеет смысл. Так можно быстро распознавать пере­
менные, которые являются объектами jQuery.

М анипуn ирование эn ементами
Теперь, когда м ы имеем кой-какую информацию о б объектах jQuery, что м ы мо­
жем сделать с ними? jQuery существенно упрощает добавление и удаление содержи­
мого. Наилучший способ рассмотрения этих примеров - загрузить НТМL-пример
в браузер и запустить эти примеры на консоли. Будьте готовы перезагрузить файл,
поскольку мы будем произвольно удалять, добавлять и изменять его содержимое.
jQuery предоставляет методы text и html, которые грубо говоря являются экви­
валентами присвоения свойствам textContent и inne rHTML элемента DOM. Напри­
мер, чтобы заменить каждый абзац в документе одним и тем же текстом, используем
$ ( 1 р 1 ) text ( ВСЕ АБЗАЦЫ ЗАМЕНЕНЫ ) ;


1

1

Аналогично мы можем применить метод html, чтобы использовать НТМL-код.
$ ( ' р ' ) . html ( ' BCE < / i > АБЗАЦЫ ЗАМЕНЕНЫ ' ) ;

Из этого следует важный момент: jQuery существенно упрощает работу со
многими элементами сразу. При использовании API DOM метод d o c ume n t .
Манипулирование элементами

315

queryS e l e ctorAll ( ) возвратит несколько элементов, но их перебор и выполнение
любых необходимых операций - это уже забота программиста. Библиотека jQuery
сама осуществляет перебор и стандартно подразумевает, что вы хотите выполнить
действия с каждым элементом в объекте jQuery. Что если вы хотите изменить толь­
ко третий абзац? jQuery предоставляет метод eq, который возвращает новый объект
jQuery, содержащий одиночный элемент.
/ / соответствует всем абзацам
$ ('р' )
! / третий а бзац (отсчитыва ется от нуля)
. eq ( 2 )
. html ( ' TPETИЙ < / i > АБЗАЦ ЗАМЕНЕН ' ) ;

Чтобы удалить элементы, достаточно вызвать метод remove объекта jQuery. Сле­
дующее удалит все абзацы.
$ ( ' р ' ) . remove ( ) ;

Это демонстрирует еще одну важную парадигму в разработке j Query: сцепление
(chaining). Все методы jQuery возвращают объект jQuery, что позволяет сцеплять их
вызовы, как мы сделали только что. Сцепление обеспечивает очень мощный и ком­
пактный синтаксис для манипулирования несколькими элементами.
Библиотека jQuery предоставляет много методов для добавления нового содержи­
мого. Один из этих методов, append, просто добавляет предоставленное содержимое
в каждый элемент в объекте jQuery. Например, можно очень легко добавить сноску
к каждому абзацу.
$ ('р' )
. append ( ' < sup> * < / sup> ' ) ;

Метод append добавляет дочерний элемент к соответствующим элементам; мы
также можем вставить элементы одного уровня, используя методы before или a fter.
Например, добавим горизонтальные линии (элементы ) перед каждым абзацем
и после него.
$('р')
. a fter ( ' ' )
. be fore ( ' ' ) ;

Эти методы вставки также имеют соответствующие дубликаты appendTo,
insertBefore и inse rtAfter, меняющие порядок вставки на обратный, что может
быть полезно в определенных ситуациях, например таких.
$ ( ' < sup > * < / sup> ' ) . appendTo ( ' р ' ) ; 1 1 эквивалент $ ( 'р ' ) . append ( ' * ' )
/ / эквивалент $ ( 'р ' ) . before ( ' ' )
$ ( ' ' ) . ins ertBe fore ( ' р ' ) ;
/ / эквивалент $ ( 'р ' ) . after ( ' ' ) ;
$ ( ' ' ) . insertAfter ( ' р ' ) ;

Библиотека jQuery также весьма упрощает изменение стиля элемента. Вы можете
добавлять класс, используя метод addC l a s s, удалять класс, используя removeClass,
и переключать классы, используя toggleC l a s s (что добавит класс, если у элемента
316

Глава 1 9 . Б иблиотека jQuery

его нет, и удалит класс, если он уже есть). Вы можете также манипулировать сти­
лем непосредственно, используя метод c s s . Мы также познакомимся с селекторами
: even и : odd, которые позволяют выбрать любой элемент. Например, если бы нам
нужно было выделить каждый нечетный абзац красным цветом, то могли бы сделать
следующее.
$ ( ' p : odd ' ) . cs s ( ' color ' ,

' red ' ) ;

Сцепление в jQuery не является чем-то обязательным для выбора подмножества
соответствующих элементов. Мы уже видели метод e q, который позволяет свести
объект jQuery к одному элементу; для модификации выбранных элементов мы мо­
жем также использовать методы f i l t e r, not и f ind. Метод f i l t e r сводит набор
к элементам, которые соответствуют определенному селектору. Например, мы можем
использовать f i l t e r в цепочке, чтобы выделить каждый нечетный абзац красным
цветом после того, как он будет изменен.
$ ( 'р' )
. after ( ' ' )
. append ( ' * < / sup> ' )
. filter ( ' : odd ' )
. c s s ( ' color ' , ' red ' ) ;

Метод not это, по существу, метод, обратный методу filter. Например, можно
добавить после каждого абзаца, а затем сдвинуть все абзацы, которым не на­
значен класс highlight.
-

$ ( 'р' )
. a fter ( ' ' )
. not ( ' . highligh t ' )
. cs s ( ' margin-left ' ,

' 2 0рх ' ) ;

И наконец метод find возвращает набор дочерних элементов, которым соответ­
ствует данный запрос (в противоположность методу filter, который фильтрует су­
ществующий набор). Например, мы можем добавить перед каждым абзацем,
а затем увеличить размер шрифта элементов, которым назначен класс code, который
в нашем примере является потомком абзацев.
$ ( 'р' )
. be fore ( ' ' )
. find ( ' . code ' )
. c s s ( ' font - s i z e ' ,

' З Орх ' ) ;

Извле ч ение объектов jQuery из оболоч ки
"Распаковать" объект jQuery (получить доступ к внутренним элементам DOM)
можно, используя метод get. Давайте получим элемент DOM для второго абзаца.
И звлечение объектов jQ uery из оболочки

317

const para2 = $ ( ' p ' ) . ge t ( l ) ; / / второй (начиная с нуля)

Получим массив, содержащий все элементы DOM абзацев.
const paras = $ ( ' р ' ) . ge t ( ) ;

/ / ма ссив всех элементов

Ajax
jQuery предоставляет удобные методы, упрощающие Аjах-запросы. В jQuery ре­
ализован метод aj ах, обеспечивающий полный контроль над Аjах-запросами. Он
предоставляет также методы get и post, которые выполняют Аjах-запросы самых
распространенных типов. Хотя эти методы поддерживают обратные вызовы, они
также возвращают обязательства, которые и являются рекомендуемым средством
обработки ответа сервера. Например, мы можем использовать метод get, чтобы пе­
реписать наш пример refreshServerinfo так.
funct ion refreshServerinfo ( ) {
const $ s e rverinfo = $ ( ' . server i nfo ' ) ;
$ . ge t ( ' http : / / l ocalhost : 7 0 7 0 ' ) . then (
1 1 успешное возвращение
function ( da t a ) {
Obj ect . keys ( data ) . forEach ( p => {
$ ( ' [ data-replace=" $ { p } " ] ) text ( data [ р ] ) ;
});
'

.

},
function ( j qXHR, t ex tStatus , err) {
conso l e . error ( e rr ) ;
$ serve rinfo . addClas s ( ' error ' )
. html ( ' Ошибка при подключении к серверу . ' ) ;
);

Как можно заметить, использование jQuery значительно упростило наш код Ajax.

Закл юч ение
Будущее jQuery неясно. Не приведут ли усовершенствования API JavaScript и бра­
узера к устареванию jQuery? Не победят ли борцы за "чистоту JavaScript"? Только
время покажет. Я чувствую, что библиотека jQuery будет полезной и в обозримом
будущем. Конечно, использование jQuery остается весьма популярным, и любой
стремящийся к успеху разработчик должен знать по крайней мере ее основы.
Если вы хотите узнать больше о jQuery, я рекомендую книгу Адама Фримена
jQuery 2.0 для профессионалов (пер. с англ, ИД "Вильяме'; 2016, ISBN 978-5-8459- 1919-9).
Сетевая документация по jQuery также очень хороша.
318

Глава 1 9. Библиотека jQuery

ГЛАВА 20

П латф орма Node

Вплоть до 2009 года JavaScript был почти исключительно языком сценариев
для браузеров.1 В 2009 году разработчик компании Joyent по имени Райан Дал (Ryan
Dahl), расстроенный из-за состояния серверных опций, создал Node. Платформа
Node была принята молниеносно и стала популярной даже на достаточно консерва­
тивном корпоративном рынке.
Тем, кому JavaScript понравился как язык, платформа Node позволила использовать
его для задач, традиционно связанных с другими языками. Для веб-разработчиков
привлекательность оказалась куда сильнее, чем просто выбор языка. Возможность
писать серверный код на JavaScript означает единообразную среду программирования.
Больше не нужно в уме переключать контексты выполнения программ, вам не нужны
специалисты по другим серверным технологиям и (что, возможно, важнее всего) один
и тот же код можно запускать как на сервере, так и на клиенте.
Хотя платформа Node была предназначена для того, чтобы сделать возможной
разработку веб-приложений, ее перенос на сервер неожиданно обеспечил другое не­
традиционное использование, такое как разработка приложений для рабочего стола
и системных сценариев. В некотором смысле платформа Node позволила JavaScript
повзрослеть и укрепиться.

Основные принципы Node
Написание приложений для Node ничем не отличается от написания любых
других приложений на JavaScript. Я не хочу сказать, что вы можете просто взять
любую JavaScript-пpoгpaммy для браузера и запустить ее в среде Node, поскольку
в коде JavaScript для браузера используется API, специфичное для браузера. В част­
ности, в Node нет никакого DOM (зачем он нужен, ведь никакого НТМL-документа
нет и в помине!). Аналогично в Node есть свой интерфейс API, который специфи­
чен для Node и не поддерживается в браузере. Некоторые вещи, такие как прямые
1 Попытки создания серверного JavaScript предпринимались и до Node; в частности, Netscape
Enterprise Server поддерживал серверный JavaScript уже в 1 995 году. Однако серверный JavaScript не
получил распространения до 2009 года, когда появилась Node.

вызовы функций операционной и файловой системы, недоступны в браузере из со­
ображений безопасности (можете себе представить, что сделали бы хакеры, если бы
они смогли получить доступ к вашим файлам прямо из браузера?). Другие возмож­
ности, такие как создание веб-сервера, для браузера просто бесполезны.
Важно понять, что составляет основу JavaScript и что является частью API. Про­
граммист, который всегда писал код для браузера, мог бы вполне резонно полагать,
что объекты window и docurnent - это просто часть JavaScript. Однако это части API,
предоставляемых средой браузера (как было описано в главе 1 8) . В этой главе мы
будем рассматривать API, предоставляемые платформой Node.
Если это еще не сделано, установите среду Node и npm (см. главу 2).

М одуn и
Модули - это механизм для упаковки кода и применения в нем пространств
имен. Пространства имен (namespacing) - это средство для предотвращения кон­
фликтов имен. Например, если Аманда и Тайлер написали два варианта функции
calculate, а вы просто берете и копируете их код в свою программу, то вторая
функция заменит первую. Пространства имен позволяют тем или иным образом об­
ращаться к функции calculate "от Аманды" и к функции calculate "от Тайлера".
Давайте рассмотрим, как модули Node решают эту проблему. Создайте файл amanda .
j s.
function calculate ( а , х , n ) {
i f ( x === 1 ) return a * n ;
return a * ( l - Mat h . pow ( x , n ) ) / ( 1 - х ) ;

module . exports = calculate ;

И создайте файл tyler . j s .
function calculat e ( r ) {
return 4 / З *Math . PI *Math . pow ( r , 3 ) ;

modu le . exports

=

calcula t e ;

Вполне понятно, что Аманда и Тайлер н е проявили фантазии при выборе имен
своих функций, но для примера мы позволим этому непотребству продолжиться.
Важная строка в обоих этих файлах - module . exports = cal culate ; . Конструкция
modul e - это специальный объект, который введен в Node ради реализации моду­
лей. Все то, что будет присвоено его свойству export s, будет экспортироваться из
этого модуля. Теперь, написав несколько модулей, давайте посмотрим, как их можно

320

Глава

20.

Платформа Node

использовать в совершенно другой программе. Давайте создадим файл
тором импортируем эти модули.

арр .

j s, в ко­

const amanda_calculate = require ( ' . /amanda . j s ' ) ;
const t yler_calculate
require ( ' . /t yler . j s ' ) ;
console . log ( amanda_calculate ( l , 2 , 5 ) ) ; // выводит 3 1
consol e . log ( t yler_calculate ( 2 ) ) ;
// выводит 33 . 51 0321 638291 1 24
=

Обратите внимание, что выбранные нами имена (amanda_ c a l cu l a t e и t y l e r_
calculate) совершенно произвольны; это только переменные. Получаемые значения
являются результатом вызова функции Node require.
Математически подготовленный читатель уже, вероятно, узнал эти два вычисле­
ния: Аманда предоставляет сумму геометрической прогрессии а + ах + ах2 + ... +
ах•-1, а Тайлер вычисляет объем сферы радиусом r. Теперь, когда известно, что это
такое, мы можем упрекнуть Аманду и Тайлера за плохой выбор имен и выбрать бо­
лее подходящие имена в арр . j s .
const geometricSum
const sphereVolume

=
=

require ( ' . /amanda . j s ' ) ;
require ( ' . / t yl er . j s ' ) ;

consol e . log ( geometricSum ( l , 2 , 5 ) ) ; / / выводит 31
/ / выводит 33 . 51 0321 638291 1 24
console . log ( sphereVolume ( 2 ) ) ;

Модули могут экспортировать значение любого типа (даже базового, хотя на это
есть немного причин). Обычно ваш модуль будет содержать не одну функцию, а не­
сколько. В этом случае вы должны экспортировать объект со свойствами функции.
Предположим, что Аманда - математик, которая снабжает нас многими другими
полезными алгебраическими функциями, а не только функцией расчета суммы гео­
метрической прогрессии.
module . exports
{
geometricSum ( a , х , n ) {
i f ( x === 1 ) return a * n ;
return a * ( l - Math . pow ( x , n ) ) / ( 1 - х ) ;
},
a r ithme t i cSum ( n ) {
return ( n + l ) * n / 2 ;
},
quadrat i c Formul a ( a , Ь , с )
const D = Math . sqrt ( b * b - 4 * а * с ) ;
return [ ( -Ь + D ) / ( 2 * a ) , ( -Ь - D ) / ( 2 * a ) ] ;
},
};
=

Это приводит к более традиционному подходу к пространствам имен - мы при­
сваиваем имя тому, что возвращается из модуля, но возвращаемое значение (объект)
содержит собственные имена.
Мод ули

321

const amanda = require ( ' . / amanda . j s ' ) ;
console . log ( amanda . geometricSum ( l , 2 , 5 ) ) ;
/ / выводит 31
console . log ( amanda . quadraticFormul a ( l , 2, - 1 5 ) ) ; / / выводит [ 3 ,

-

5 }

Здесь нет никакого чуда: модуль просто экспортирует обычный объект с функ­
циональными свойствами (не позволяйте сокращенному синтаксису ЕSб себя запу­
тать; это обычные функции, являющиеся свойствами объекта). Данная парадигма
настолько распространена, что для нее был предусмотрен сокращенный синтаксис,
использующий специальную переменную exports. Мы можем переписать экспорти­
руемый код Аманды более компактно.
export s . geometricSum
funct ion ( a , х, n) {
i f ( x === 1 ) return a * n ;
return a* ( l - Math . pow ( x , n ) ) / ( 1 - х ) ;
=

};
exports . ari thme t i cSum
function ( n )
return ( n + l ) * n/ 2 ;

{

};
expor t s . quadr a t icFormu l a = function ( a , Ь , с )
const D = Math . sqrt ( b * b - 4 * а * с ) ;
return [ ( -Ь + D ) / ( 2 * а ) , ( - Ь - D ) / ( 2 * а ) ] ;
};

Сокращение "expo rts" работает только при экспорте объектов; если
вы хотите экспортировать функцию или некое другое значение, ис­
пользуйте module . export s . Кроме того, вы не можете их смешивать:
используйте либо одно, либо другое.

Б азовые, файловые и n р m -модуn и
Модули относятся к трем категориям: базовые (core module), файловь1е (file module)
и прт-модули (npm module). Имена базовых модулей зарезервированы; эти модули,
например, fs и o s, предоставляет сама среда Node (мы их обсудим далее в этой гла­
ве). С файловыми модулями мы уже встречались, когда создавали файл с экспорти­
руемой функцией, в котором присваивалось нечто свойству module . export s , а затем
использовали этот файл в других программах. Модули npm - это обычные файловые
модули, которые находятся в специальной папке, называемой node _modules. Когда вы
используете функцию require, Node определяет тип модуля (их описание приведены
в табл. 20. 1 ) из передаваемой строки.

322

Глава 20. Платформа Node

Табnица 20.1 . Типы модуnей
Тип

Строка, передаваемая require

Примеры

Базовый Не начинается с /, . / или . . /

Файловый

npm

require ( ' fs ' )
require ( ' os ' )
require ( ' http ' )
require ( ' chi ld_process ' )
require ( ' . /debug . j s ' )
Начинается с /, . / или . /
require ( ' / full /path/to/module . j s ' )
require ( ' . . / a . j s ' )
requi re ( ' . . / . . / а . j s ' )
Не базовый модуль и не начинается requi re ( ' debug ' )
require ( ' express ' )
с /, . / ил и
/
require ( ' chal k ' )
require ( ' koa ' )
require ( ' q ' )
.

.

.

Некоторые базовые модули, такие как process и buffer, являются глобальными.
Они доступны всегда и не требуют явного оператора require. Базовые модули при­
ведены в табл. 20.2.
Табnица 20.2. Базовые модуnи
М одуnь

Гnобаnьный

Описание

as sert
buffer

Нет
Да

child_process
cluster

Нет
Нет

crypto
dns

Нет
Нет

domain

Нет

events
fs
http
https
net
os
path
punycode

Нет
Нет
Нет
Нет
Нет
Нет
Нет
Нет

querystring

Нет

Используется в проверочных целях
Используется для операций ввода-вывода (1/0) (прежде
всего, в файл и сеть)
Функции для запуска внешних программ (Node и др.)
Позволяет использовать несколько процессов для повы­
шения производительности
Встроенные криптографические библиотеки
Функции системы доменных имен (DNS) для преобразо­
вания сетевых имен
Позволяет группировать ввод-вывод и другие асинхрон­
ные операции для изоляции ошибок
Утилиты для поддержки асинхронных событий
Операции файловой системы
Сервер НТТР и связанные с ним утилиты
Сервер НТТР S и связанные с ним утилиты
Асинхронное сетевое API на базе сокетов
Утилиты операционной системы
Утилиты имен и путей файловой системы
Кодировка символов U nicode с помощ ь ю ограниченного
подмножества символов ASCll
Утилиты для анализа и создания строк запросов URL

Б азовые, файловые и n р m -модули

323

Окончание табл. 20.2

readline

Гл обальный
Нет

smal loc
s tream
s tring_decoder
tls

Нет
Да
Нет
Нет

tty
dgram

Нет
Нет

url
util

Да
Нет
Нет

Модуль

vm

z lib

Нет

Описание
Интерактивные утилиты ввода-вывода; в первую оче­
редь, для программ командной строки
Обеспечивает явное распределение памяти для буферов
Передача потоковых данных
Преобразован ие буфера в строки
Утилиты TLS (Transport Layer Security - безопасный
транспортный уровень)
Низкоуровневые функции ПY(ТeleTYpewriter)
Утилиты UDP (User Datagram Protocol - протокол поль­
зовательских дейтаграмм) для работы в сети
Утилиты анализа URL
Внутренние утилиты Node
Виртуальная ма ш и на (JavaScript): обеспечивает функции
метапрограммирования и создания контекста
Утилита сжатия

Рассмотрение всех этих модулей выходит за рамки данной книги. Мы обсудим
лишь самые важные из них, но это даст вам отправную точку для получения допол­
нительной информации. Подробная документация для этих модулей доступна в до­
кументации API Node (https : / /nodej s . o rg/api/).
И наконец, есть прт-модули - файловые модули со специфическим соглашени­
ем об именовании. Если вам необходим некоторый модуль х (где х - не базовый
модуль), то Node будет искать в текущем каталоге подкаталог node_modules. Если
он его найдет, то будет искать модуль х в этом каталоге. Если он его не найдет, то
перейдет к родительскому каталогу, и снова начнет искать каталог node _modules
и продолжит поиск в нем. Процесс будет повторяться, пока не будет найден модуль
или достигнут корневой каталог. Например, если ваш проект находится в каталоге
/ home / j doe / t e s t_pro j ect и в своем файле приложения вы вызываете функцию
requi re ( ' х ' ) , Node будет искать модуль х в перечисленных ниже каталогах в таком
порядке.


/home/ j doe/test_proj ect /node_modules/x



/home / j doe /node_modules/x



/home/node modules /x



/node modules/x

Для большинства проектов создается один каталог node modules в корневом ка­
талоге приложений. Кроме того, вы не должны ничего добавлять или удалять из того
каталога вручную; позвольте утилите npm сделать все самостоятельно. Однако весьма
_

324

Глава

20.

Платформа Node

полезно знать, как Node ищет импортируемые модули, особенно когда наступает
время поиска проблемы в модулях стороннего производителя.
Не помещайте те модули, которые вы пишете сами, в каталог node _modules. Ра­
ботать это будет, но особенность каталога node _modules в том, что он может быть
удален утилитой npm в любой момент и воссоздан из зависимостей, перечисленных
в файле package . j son (см. главу 2).
Вы можете, конечно, опубликовать собственный модуль с помощью утилиты npm
и управлять им, используя npm, но тогда вы должны избегать внесения в него изме­
нений непосредственно в каталоге node_modules!

Изменение параметров модуn е й
с помо щь ю модуn ей-функци й
Обычно модули экспортируют объекты, а иногда и одну функцию. Однако есть
и другой популярный сценарий их использования - модуль, который экспортирует
функцию, предназначенную для немедленного вызова. Речь идет о том, что возвра­
щаемое модулем значение само по себе является функцией, которая сразу же вы­
зывается. Другими словами, вы не используете далее в своей программе то, что воз­
вращает модуль, а вызываете эту функцию и используете то, что она возвращает.
Этот сценарий используется, когда нужно изменить параметры модуля либо полу­
чить информацию об окружающем контексте. Давайте рассмотрим реальный пакет
npm debug. При импортировании функции debug ей передается текстовая строка,
которая будет использоваться как префикс при выводе отладочных сообщений. Бла­
годаря этому можно будет различать сообщения, выведенные из различных частей
программы.
const debug

=

require ( ' debug ' ) ( ' ma in ' ) ; 11 обратите внимание, что

мы

1 1 сразу вызыва ем функцию, которую

/ / возвраща ет модуль
dеЬug ( " начало " ) ;

/ / выводит
1 1 "ma in начало +Oms " , если
1 1 отладка будет разрешена

Чтобы разрешить отладку с использованием библиотеки debug, уста­
новите переменную окружения DEBUG. Для нашего примера мы устано­
вили бы DEBUG=main. Вы можете также установить DEBUG=*, что раз­
решает вывод всех отладочных сообщений.
Из этого примера ясно, что модуль debug возвращает функцию (поскольку мы
непосредственно вызываем это как функцию)". и эта функция сама возвраща­
ет функцию, которая "помнит" строку, переданную первой функции. По сути мы
И зменение п а раметров мод ул ей с п омощью мод ул ей-фун кц ий

325

"интегрировали" значение в этот модуль. Давайте посмотрим, как мы могли бы реа­
лизовать собственный модуль debug.
l e t l a s tMe ssage ;
module . exports = function ( prefix )
return function (mes sage ) {
const now = Date . now ( ) ;
const s inceLastMe s sage
now - ( la s tMessage 1 1 now) ;
console . log ( ' $ { prefix } $ {mes sage } + $ { s inceLa s tMe s sage } ms ' ) ;
l as tMessage = now ;

Этот модуль экспортирует функцию, которая написана так, чтобы значение пере­
менной prefix можно было использовать в модуле. Обратите внимание, что у нас
есть и другое значение, lastMessage, которое является временной меткой последне­
го сообщения, которое было выведено; мы используем его, чтобы вычислить время
между сообщениями.
Этот фрагмент кода демонстрирует важный момент: что произойдет, если вы им­
портируете модуль многократно? Рассмотрим, например, что происходит при импорте
самодельного модуля debug дважды.
const debugl
const debug2

require ( ' . /debug ' ) ( ' Первый ' ) ;
require ( ' . / debug ' ) ( ' Второй ' ) ;

debugl ( ' запущен первый отладчик ! ' )
dеЬug2 ( ' запущен второй отладчик ! ' )
setTimeout ( function ( ) {
debugl ( ' пpoшлo немного времени . . . ' ) ;
debug2 ( ' чтo случилось ? ' ) ;
} 200 ) ;
,

Если вы ожидаете увидеть нечто такое:
Первый
Второй
Первый
Второй

запущен первый
запущен второй
прошло немного
что случилос ь ?

отладчик ! + Oms
отладчик ! + Oms
времени . . . + 2 0 0ms
+2 0 0ms

то я вас разочарую. На самом деле вы увидите это (плюс или минус несколько мил­
лисекунд).
Первый
Второй
Первый
Второй

326

запущен перв ый
запущен второй
прошло немного
что случилось ?

Глава

20.

отладчик ! + Oms
отладчик ! + Oms
времени . . . + 2 0 0ms
+ Oms

Платформа Node

Оказывается, Node импортирует каждый конкретный модуль только один раз при
запуске приложения Node. Таким образом, даже при том, что мы импортируем свой
модуль debug дважды, Node "помнит': что мы уже это импортировали раньше, и ис­
пользует тот же экземпляр. Таким образом, даже при том, что debugl и debug2 это
отдельные функции, в их обеих используется ссылка на одну и ту же переменную
lastMes sage.
Такое поведение программы вполне безопасно и желательно. По соображениям
производительности, экономии памяти и удобства сопровождения, модули должны
загружаться только один раз!
-

Сценарий импортирования, который мы использовали при создании
нашего самодельного модуля debug, подобен тому, что используется
и для его nрm-тезки. Но если нам действительно нужно получить не­
сколько журналов отладки с независимым хронометражем, то пере­
менную l a s tMes s age, хранящую временную метку, нужно переместить в тело функции, которую возвращает модуль. Тогда каждый раз
при создании регистратора ей будет присваиваться новое, независи­
мое значение.

Доступ к фа й n ово й системе
Во многих книгах по программированию доступ к файловой системе рассматри­
вается с самого начала, поскольку это критически важная часть "обычного" програм­
мирования. Бедный JavaScript: вплоть до появления Node он не был членом клуба
файловой системы.
В примерах из этой главы подразумевается, что корневой каталог вашего проекта
/home/ / fs. Это типичный путь к каталогу в системах Unix, только нужно вме­
сто подставить имя вашей учетной записи. Те же принципы применимы и к
системе Windows (в которой корневой каталог вашего проекта мог бы располагаться
в папке С : \Users\ \Documents \ fs).
Чтобы создать файл, используйте метод fs . writeFile. Создайте в корневом ката­
логе своего проекта файл write . j s.
const fs = require ( ' fs ' ) ;
f s . wri teFile ( ' he l l o . txt ' , ' Привет из Node ! ' function ( e r r ) {
i f ( err) return console . log ( ' Oшибкa при записи в файл . ' ) ;
});
,

Этот код создаст файл hello . txt в том каталоге, в котором вы находились при
запуске приложения wri te . j s. Здесь подразумевается, что вы имеете права доступа
по записи к этому каталогу и что в к аталоге нет заранее созданного файла только

Доступ к файловой систе м е

327

для чтения hello . txt. Каждый раз при запуске приложения Node, оно будет исполь­
зовать текущий рабочий каталог, который может отличаться от того, где располо­
жен сам файл, как показано ниже.
$ cd /home / j do e / f s
# текущий рабочий каталог /home/jdoe/fs
$ node write . j s
# созда ется /home/jdoe/fs/hel l o . txt
# теперь текущий рабочий ка талог /home/j doe
$ cd . .
$ node fs /write . j s # создается /home/jdoe/hello . tx t

В Node существует специальная переменная, _d i rname, которая всегда содержит
путь к каталогу, в котором располагается файл исходного кода. Например, мы можем
изменить свой пример так.
const fs

=

require ( ' f s ' ) ;

fs . writeFile ( _dirname + ' /hel l o . txt ' ,
' Привет из Node ! ' , funct ion ( err)
i f ( er r ) return console . error ( ' Oшибкa при записи в файл . ' ) ;
});

Теперь приложение wri te . j s всегда будет создавать файл hel l o . txt в катало­
ге /home / < j doe>/ fs (где находится write . j s) . Использование конкатенации строк
для объединения переменной _di rname и нашего имени файла - не очень хоро­
шая идея. Это может вызвать проблемы на компьютере под управлением Windows,
поскольку разделитель имен каталогов там другой. Поэтому в Node в модуле path
предусмотрены независимые от платформы утилиты для работы с именами файлов
и путями. Таким образом, мы можем переписать этот модуль так, чтобы он без про­
блем работал на любой платформе.
const fs = require ( ' fs ' ) ;
const path = requ i re ( ' path ' ) ;
fs . writeFi l e ( path . j oi n (_dirname , ' he l l o . t xt ' ) ,
' Привет из Node ! ' , function ( err )
i f ( err ) return consol e . error ( ' Oшибкa при записи в файл . ' ) ;

});

Метод path . j oin объединяет элементы пути, используя тот разделитель катало­
гов, который принят в текущей операционной системе, что является хорошей прак­
тикой.
Что если мы хотим теперь прочитать содержимое этого файла? Для этого мы ис­
пользуем метод fs . readFile. Создайте файл read . j s.
const fs = require ( ' fs ' ) ;
const path = requ ire ( ' path ' ) ;

328

Глава

20.

Платформа Node

fs . readFi l e (path . j oin(_dirname , ' he l l o . txt ' ) , function ( err, dat a )
i f ( er r ) return console . error ( ' Oшибкa при чтении файла . ' ) ;
console . log ( ' Coдepжимoe файла : ' ) ;
console . log ( data ) ;
));

Если вы запустите этот пример, то можете быть неприятно удивлены результатом.
Содержимое файла :
< Buffer dO 9f dl 8 0 dO Ь8 dO Ь2 dO bS dl 8 2 2 0 dO Ь8 dO Ь7 2 0 4е 6f 64 65 2 1 >

Если преобразовать эти шестнадцатеричные коды в их эквивалент ASCII/Unicode,
то вы обнаружите, что получится текстовая строка Привет из Node ! но наша про­
грамма в ее текущем состоянии не очень дружественна. Если при вызове метода
fs . readFile вы не укажете, какую именно кодировку нужно использовать, то он
возвратит буфер, содержащий "сырые" двоичные данные. Хотя мы не определяли ко­
дировку символов в wri te . j s явно, стандартной кодировкой для символьных строк
в JavaScript является UTF-8 (кодировка Unicode). Мы можем изменить файл read . j s,
определив UTF-8, и получить ожидаемый результат.
,

const fs
require ( ' fs ' ) ;
const path
require ( ' ра th ' ) ;
=

=

fs . readFile (path . j oin (_dirname , ' he l l o . txt ' ) ,
{ encodin g : ' ut f 8 ' ) , function ( err, dat a )
i f ( err) return consol e . error ( ' Oшибкa при чтении файла . ' ) ;
console . log ( ' Coдepжимoe файла : ' ) ;
console . log ( data ) ;
));

У всех функций модуля f s есть синхронные эквиваленты (их имена завершаются
суффиксом "Sync"). В wri te . j s мы можем использовать синхронный эквивалент.
fs . writeFileSync (path . j o in (_dirname ,

' he l l o . txt ' ) ,

' Привет из Node ! ' ) ;

И в read . j s мы можем использовать синхронный эквивалент.
const data = f s . readFileSync ( path . j oin (_dirname ,
encoding : ' ut f 8 ' ) ) ;

' he l l o . txt ' ) ,

Поскольку в синхронных версиях функций обработка ошибок выполняется с ис­
пользованием исключений, чтобы сделать наши примеры надежнее, заключим их
в блоки t ry/catch, например так.
t ry {
f s . writeFi l e S ync ( path . J oin (_dirname , ' he l l o . txt ' ) ,
catch ( err) {
console . error ( ' Oшибкa при записи файла . ' ) ;

' Привет из Node 1 ' ) ;

Д осту п к файловой системе

329

Синхронные функции файловой системы заманчиво удобны. Но если
вы пишете веб-сервер или сетевое приложение, помните, что скорость
его работы будет максимальной при асинхронном выполнении. В этих
случаях всегда следует использовать асинхронные версии функций ввода-вывода. Если вы пишете утилиту командной строки, использование
синхронных версий функций обычно не представляет особой проблемы.
Используя метод fs . readdir, вы можете вывести список файлов в каталоге. Соз­
дайте файл l s . j s.
const f s = require ( ' fs ' ) ;
f s . readdir (�dirname , function ( er r , file s ) {
i f ( er r ) return console . error ( ' Heвoзмoжнo прочитать содержимое каталога ' ) ;
console . log ( ' Coдepжимoe каталога $ { dirname ) : ' ) ;
console . log ( fi l e s . map ( f => ' \ t ' + f ) . j oin ( ' \n ' ) ) ;
)) ;

В модуле f s содержится довольно много функций файловой системы; вы можете
удалять файлы (fs . unl ink), перемещать или переименовывать их (fs . rename), полу­
чать информацию о файлах и каталогах (fs . stat) и многое другое. Более подробная
информация по этой теме приведена в документации по Node API (https : / /nodej s .
org/ap i / fs . html) .

Переменная proces s
Каждая выполняющаяся программа Node имеет доступ к переменной process, ко­
торая позволяет получать информацию о выполнении текущего процесса и управлять
им. Например, если ваше приложение встречается с ошибкой, настолько серьезной, что
продолжение выполнения становится нецелесообразным или бессмысленным (неустра­
нимая ошибка (fatal error)), вы можете немедленно остановить ее выполнение, вызвав
метод process . exit. Можно также передать числовой код завершения (exit code), ко­
торый используется сценариями для определения, успешно ли завершилась программа.
Традиционно код завершения О означает "отсутствие ошибки': а отличный от нуля код
означает ту или иную ошибку. Рассмотрим сценарий, который обрабатывает файлы . txt
в подкаталоге data: если никаких файлов для обработки нет и делать нечего, то програм­
ма завершает работу немедленно, но это не ошибка. С другой стороны, если подкаталог
da ta не будет существовать, то эта проблема будет считаться более серьезной, и про­
грамма должна вернуть код ошибки. Вот как могла бы выглядеть эта программа.
const fs = require ( ' fs ' ) ;
fs . readdir ( ' data ' , funct ion ( er r , f i le s )
i f ( er r ) {

330

Глава

20.

Платформа Node

{

consol e . error ( " Oшибкa : не могу прочитать каталог data . " ) ;
proce s s . exit ( l ) ;
const txtFi les = f i le s . f i l t e r ( f => / \ . txt $ / i . t e s t ( f ) ) ;
i f ( txtFile s . length === 0 ) {
console . log ( " Файлы . txt не найдены . " ) ;
proces s . ex i t ( O ) ;
}
/ / обра ботка файлов txt . . .
.

});

Объект process также позволяет обращаться к массиву, содержащему аргументы
командной строки, переданные программе. Запуская приложение Node, вы можете
передать ему необязательные аргументы командной строки. Например, мы могли
написать программу, которой передаются в виде аргументов командной строки не­
сколько имен файлов, а она выводит количество строк текста в каждом файле. Мы
могли бы вызвать программу так.
$ node linecount . j s filel . txt file2 . txt fileЗ . txt

Аргументы командной строки содержатся в массиве proces s . a rgv.2 Прежде чем
подсчитывать строки в наших файлах, давайте выведем значение свойства proces s .
argv, чтобы понять, что нам передается.
console . log ( proce s s . argv ) ;

Наряду с filel . txt, file2 . txt и fileЗ . txt вы увидите несколько дополнитель­
ных элементов в начале массива.
' node ' ,
' /home / j do e / l inecount . j s ' ,
' f i l e l . txt ' ,
' file2 . txt ' ,
' fi l e З . txt ' ]

Первый элемент - это интерпретатор, или программа, которая интерпретирует
файл исходного кода (в данном случае - node). Второй элемент - это полный путь
к выполняемому сценарию, а остальная часть элементов является всеми переданны­
ми программе аргументами. Поскольку нам не нужны эти дополнительные элемен ты, воспользуемся методом Array . s l i ce, чтобы избавиться от них прежде, чем на­
чать подсчет строк в наших файлах.
const fs

=

require ( ' fs ' ) ;

const filename s

2 Имя argv
массиву.

-

=

proce s s . argv . s l i ce ( 2 ) ;

это кивок языку С.

v

-

это первая буква слова vector (вектор), который подобен

Переменная

process

331

l e t counts = fil ename s . map ( f => {
try {
const data = fs . readFileSync ( f , { encoding :
return ' $ { f } : $ { da t a . sp l i t ( ' \n ' ) . length} ' ;
catch ( er r ) {
return ' $ { f } : ошибка при чтении файла ' ;

' ut f 8 '

}) ;

}) ;
console . log ( count s . j oin ( ' \n ' ) ) ;

Объект process позволяет также обращаться к переменным среды окружения че­
рез объект proces s . env. Переменные среды окружения называют системными пере­
менными, которые главным образом используются для программ командной строки.
В большинстве систем U11ix вы можете установить переменную среды окружения,
просто введя команду export ИМЯ=Зна чение (традиционно имена переменных окру­
жения пишутся прописными буквами). В Wi11dows для этого используется команда
set ИМЯ=Зна чение. Переменные окружения зачастую используются для изменения
поведения некого аспекта вашей программы, когда нежелательно передавать значе­
ние в командной строке каждый раз при запуске программы.
Например, мы могли бы использовать среду окружения для отключения вьmода отла­
дочной информации. Для управления поведением программы используется переменная
среды окружения DEBUG, которую мы устанавливаем равной 1, если хотим, чтобы про­
грамма выводила отладочную информацию (любое другое значение отключит отладку).
cons t debug = proce s s . env . DEBUG === " 1 " ?
consol e . log :
funct ion ( ) { } ;
dеЬug ( " Выводится в случае , если переменная окружения DEBUG=l ! " ) ;

В этом примере мы создаем функцию, debug, которая просто является псевдо­
нимом для console . log, если переменная окружения DEBUG установлена, и пустой
функцией (которая не делает ничего) в противном случае (если бы мы оставили пе­
ременную debug неопределенной, то вызвали бы ошибку, когда попытались бы ис­
пользовать ее!).
В предыдущем разделе мы говорили о текущем рабочем каталоге, которым
по умолчанию является каталог запуска программы (а не каталог, в . котором про­
грамма расположена). Метод proce s s . cwd указывает текущий рабочий каталог,
а process . chdir позволяет его изменить. Например, если нужно вывести каталог,
из которого программа была запущена, и изменить текущий каталог на тот каталог,
в котором находится сама программа, то можно сделать следующее.
console . log ( ' Teкyщий каталог : $ { process . cwd ( ) } ' ) ;
proces s . chdir (�dirname ) ;
console . log ( ' Hoвый текущий каталог : $ { proce s s . cwd ( ) } ' ) ;

332

Глава

20.

Платформа Node

Информация об операционно й системе
Модуль os предоставляет некую специфическую для платформы информацию
о компьютере, на котором выполняется приложение. Вот пример, демонстрирующий
самую полезную информацию, предоставляемую модулем os и их значения, полу­
ченные на моем компьютере.
cons t os = require ( ' os ' } ;
console . log ( "Имя хост а : " + os . hostname ( } } ;
1 1 prometheus
1 1 Lin ux
console . l og ( " Тип ОС : " + os . type ( } } ;
1 1 l i n ux
соnsоl е . l оg ( "Платформа : " + os . platform ( } } ;
console . log ( " Bepcия : " + os . release ( } } ;
1 1 3 . 1 3 . 0-52-generic
console . log ( " Bpeмя работы : " +
( o s . upt ime ( ) / 6 0 / 6 0 / 2 4 ) . toFixed ( l } + " days " } ;
1 1 8 0 . 3 days
console . log ( "Apxитe ктypa процессора : " + os . arch ( } } ; / / х 64
соnsоlе . l оg ( " Количе ство проце ссоров : " + os . cpus ( } . length } ; // 1
console . l og ( " Oбъeм памяти : " +
1 1 1 042 . 3 мв
( o s . totalmem ( } / l e 6 } . toFixed ( l } + " МВ " } ;
consol e . log ( " Cвoбoднo : " +
1 1 1 95 . 8 мв
( o s . freemem ( } / l e 6 } . t oFixed ( l } + " МВ " } ;

До ч ерние процессы
Модуль chi ld_process позволяет вашему приложению запускать другие про­
граммы, будь то другие программы Node, а также исполняемые файлы или сценарии
на другом языке. Описание всех подробностей управления дочерними процессами
выходит за рамки этой книги, но простой пример мы рассмотрим.
Модуль child_process предоставляет три основные функции: ехес, exec F i l e
и fork. Как и у модуля fs, здесь есть синхронные версии этих функций ( execSync,
execFi leSync и forkSync ) . Функции ехес и execFi le могут запустить любой выпол­
няемый файл, поддерживаемый вашей операционной системой. Функция ехес вы­
зывает оболочку (это то, что лежит в основе командной строки вашей операционной
системы; если вы можете запустить нечто из командной строки, вы можете запустить
это с помощью функции ехес). Функция execFile позволяет запустить исполняемый
файл непосредственно; она обеспечивает немного улучшенное использование памяти
и ресурсов, но требует большего внимания. Наконец функция fork позволяет запус­
кать другие сценарии Node (что также может быть сделано функцией ехес).
Функция fork запускает отдельный процессор Node, поэтому расход
ресурсов будет таким же, как и при использовании функции е хес;
но функция fork позволяет обращаться к некоторым возможностям
взаимодействия между процессами (interprocess communication). Более

И н форма ция об опера ционной системе

333

подробная информация по этой теме приведена в официалыюй доку­
ментации (https : / /nodej s . org/ap i / child_proce s s . html #child_
proces s_child_process fork_modulepath_args _options).
Поскольку функция ехес является наиболее общей и наименее требовательной,
в этой главе мы будем использовать ее.
В демонстрационных целях мы выполним команду dir, которая отображает
список содержимого каталога (хотя пользователям Unix более знакома команда l s ,
в большинстве систем Unix она равнозначна команде dir).
const ехес = require ( ' child_proces s ' ) . ех е с ;
exec ( ' di r ' , function ( err, s tdou t , stder r ) {
i f ( er r ) return console . error ( ' Oшибкa при запуске "dir" ' ) ;
stdout
stdout . toString ( ) ; / / преобразует Buffer в строку
console . log ( s tdout ) ;
stderr = s tderr . toString ( ) ;
i f ( s tderr ! == ' ' ) {
console . error ( ' Oшибкa : ' ) ;
consol e . error ( stderr ) ;
=

});

Поскольку функция ехес запускает системную оболочку, мы не должны указы­
вать путь к каталогу, где хранится выполняемый файл. Чтобы вызвать определенную
программу, которая обычно недоступна из оболочки вашей системы, необходимо
указать полный путь к ее исполняемому файлу.
Функции обратного вызова метода ехес помимо признака ошибки передается два
объекта типа Buffer - один для s tdout (стандартный поток вывода программы)
и другой для s tderr (стандартный поток вывода ошибок, если они есть). В этом при­
мере, поскольку мы не предусматриваем вывод на устройство stde rr, мы сначала
проверяем значение первого аргумента на предмет возникновения ошибки в процес­
се запуска программы, а затем выводим полученные результаты на консоль.
Функции ехес можно передать опциональный объект options, который позво­
ляет задать рабочий каталог, переменные окружения и т.д. Более подробная инфор­
мация по этой теме приведена в официальной документации (https : / /node j s . org/
api / child_process . html).
Обратите внимание на способ, которым мы импортируем функцию
ехес. Вместо того чтобы импортировать модуль child_proces s , ис­
пользуя const child_process = require ( ' child_proces s ' ) , а затем
вызывать метод ехес как child_proces s . ехес, мы непосредственно
используем псевдоним ехес. Вы можете пользоваться любым вариан­
том, но тот способ, которым мы это сделали, весьма распространен.
334

Глава 2 0 . Платформа Node

Потоки
Концепция потока (stream) важна для Node. Поток - это объект, который имеет
дело с данными (как и подразумевает его название) в потоке (слово поток должно
заставить вас думать о течении, а поскольку течение - это нечто, происходящее во
времени, ему имеет смысл быть асинхронным).
Потоки могут быть потоками чтения, записи или того и другого (дуплексные по­
токи). Потоки имеют смысл тогда, когда передача данных осуществляется на про­
тяжении некоторого времени. Примерами могут служить ввод пользователем дан­
ных с клавиатуры или веб-службы с двусторонней связью с клиентом. При доступе
к файлам также зачастую используются потоки (даже при том, что мы вполне можем
читать и писать в файлы без потоков) . Мы будем использовать файловые потоки
для демонстрации создания потоков, чтения из них и записи, а также создания кана­
ла (pipe) между ними.
Начнем с создания потока записи и запишем в него данные.
const fs

require ( ' fs ' ) ;

const ws
fs . createWriteStream ( ' stream . tx t ' , { encodin g :
ws . write ( ' Строка 1 \ n ' ) ;
ws . write ( ' Строка 2 \ n ' ) ;
ws . end ( ) ;

' ut f 8 ' } ) ;

Методу end можно дополнительно передать аргумент данных, тогда
он будет эквивалентен вызову метода wri te. Таким образом, если
нужно вывести данные только один раз, можете просто вызвать ме­
тод end с теми данными, которые вы хотите записать.
В наш поток записи (write stream - ws) можно выводить данные с помощью ме­
тода write до вызова метода end, после чего поток будет закрыт, и дальнейшие вы­
зовы метода wri te приведут к ошибке. Поскольку вы можете вызывать метод wri te
столько раз, сколько необходимо, а затем вызвать метод end, поток записи идеален
для записи данных в течение некоторого времени.
Точно так же мы можем создать поток чтения, чтобы читать данные по мере их
поступления.
const fs

require ( ' fs ' ) ;

const rs = fs . creat eReadStream ( ' s tream . txt ' , { encodin g : ' ut f 8 ' } ) ;
rs . on ( ' da ta ' , funct i on ( da t a ) {
console . log ( ' >> Данные : ' + dat a . replace ( ' \n ' , ' \ \n ' ) ) ;
});
rs . on ( ' end ' , function ( da t a )
console . l o g ( ' >> Конец ' ) ;
});

Потоки

335

В этом примере мы просто выводим содержимое файла на консоль (заменяя сим­
волы перехода на новую строку для наглядности). Вы можете поместить оба эти при­
мера в один и тот же файл: у вас может быть поток записи, пишущий в файл, и по­
ток чтения, читающий из него.
Дуплексные потоки не столь распространены и не рассматриваются в этой кни­
ге. Как и следовало ожидать, вы можете вызвать метод wri te, чтобы писать данные
в дуплексный поток, а также прослушивать события data и end.
Поскольку данные в потоках "текут': вполне резонно взять данные, выходящие из
потока чтения, и перенаправить их в поток записи. Этот процесс называется конвей­
ером (piping). Например, мы могли бы перенаправить поток чтения в поток записи,
чтобы скопировать содержимое одного файла в другой.
const rs
f s . createReadStrearn ( ' strearn . txt ' ) ;
const ws
f s . createWriteStrearn ( ' strearn_copy . txt ' ) ;
rs . pipe ( ws ) ;
=

=

Обратите внимание, что в этом примере мы не должны определять кодировку
символов: rs просто пересылает байты из файла s t ream . txt в поток ws (что при­
водит к их записи в файл s tream_сор у . txt); кодировка символов имеет значение,
только если мы пытаемся интерпретировать данные.
Конвейерная обработка - это общая методика для перемещения данных. Напри­
мер, можно переслать содержимое файла в виде ответа веб-сервера. Либо вы могли
бы переслать сжатые данные процессору распаковки, который, в свою очередь, пере­
шлет данные программе записи в файл.

В еб-серверы
Хотя Node теперь используется во многих приложениях, его первоначальная цель
состояла в предоставлении услуг веб-сервера. Таким образом, нельзя не рассмотреть
и этот способ его применения.
Те из вас, кто настраивал сервер Apache (или IIS, или любой другой веб-сервер ),
могут быть поражены простой создания и функционирования этого веб-сервера.
Модуль http (и его защищенный дубликат, модуль https) предоставляет метод
createServer, который создает простой веб-сервер. Все, что вы должны сделать, это указать функцию обратного вызова, которая будет обрабатывать входящие за­
просы. Чтобы запустить сервер, нужно просто вызывать его метод l is ten и указать
номер прослушиваемого порта.
const h t tp

=

require ( ' http ' ) ;

const s erver = http . createServer ( funct i on ( req, res )
console . log ( ' $ { re q . rnethod } $ { req . url } ' ) ;
rеs . еnd ( ' Прив е т , мир ! ' ) ;

336

Глава

20.

Платфо рма Node

{

});
const port
8080;
server . li s t e n ( port , funct ion ( ) {
1 1 методу l i s ten переда ется функция обра тного вызова ,
1 1 которая вызывается после запуска сервера
console . log ( ' Cepвep запущен на порту $ { port } ' ) ;
});
=

Из соображений безопасности в большинстве операционных систем
запрещено прослушивать стандартный порт НТТР (80) без запро­
са на повышение прав. Фактически повышенные права необходимы
для прослушивания любого порта ниже 1 024. Разумеется, это сделать
не сложно: если у вас есть доступ к команде sudo, можете запустить
свой сервер через sudo и, получив права администратора, начать про­
слушивать порт 80. Для целей разработки и отладки обычно исполь­
зуются порты выше 1 024. Обычно выбирают такие номера, как 3000,
8000, 3030 и 8080, поскольку их легче запомнить.
Если вы запустите эту программу и перейдете в браузере по адресу http : / /
localhost : 8 0 8 0, то увидите строку Привет , мир ! . На консоли мы регистрируем все
запросы, которые состоят из метода и пути URL. Вас может удивить тот факт, что
каждый раз при переходите в браузере по этому URL, на сервер отправляется два
запроса.
GET /
GET / favicon . ico

Большинство браузеров неявно запрашивают пиктограмму, которую они затем
отображают на панели URL или заголовке вкладки. Поэтому мы видим этот запрос
на нашей консоли.
В основе веб-сервера Node лежит функция обратного вызова, которую вы долж­
ны указать при создании сервера. Именно она обрабатывает все входящие запросы.
Ей передается два аргумента, объект I ncomingMes sage (зачастую для него выбира­
ется переменная req) и объект ServerRequest (зачастую для него выбирается пере­
менная res ) . Объект I ncomingMes sage содержит всю информацию о НТТР-запросе:
какой URL затребован, все посланные заголовки, все посланные в теле данные и т.д.
Объект ServerResponse содержит свойства и методы дпя управления ответом, кото­
рый отсыпается назад клиенту (обычно браузеру). Если вы увидели, что мы вызвали
метод req . end, и задались вопросом "Явпяется ли req потоком записи?': то просмо­
трите заголовок класса. Объект ServerResponse реализует интерфейс потока запи­
си, который определяет то, как именно данные пересылаются клиенту. Поскольку
объект ServerResponse - это поток записи, он облегчает передачу файла... но нам
ничто не мешает создать поток для чтения файла и переслать его в качестве ответа
Веб-серверы

·

337

НТТР-сервера. Например, если у вас есть файл favi con . ico, улучшающий внешний
вид вашего веб-сайта, вы можете выделить этот запрос и отправить содержимое дан ного файла непосредственно клиенту.
const s e rver
h t tp . createServer ( function ( req, re s ) {
i f ( re q . me thod
' GET ' && r e q . url
' / favicon . ico ' )
const fs = require ( ' fs ' ) ;
fs . createReadStream ( ' favicon . ico ' ) ;
fs . pipe ( re s ) ;
/ / это вместо вызова метода ' en d '
else {
console . log ( ' $ { re q . method } $ { re q . url } ' ) ;
res . end ( ' He l l o world ! ' ) ;
=

===

===

});

Выше приведен минимально возможный, хотя и не очень интересный, веб-сервер.
Анализируя информацию, содержащуюся в объекте IncomingRequest, вы можете рас­
ширить приведенную выше модель и создать любой вид веб-сайта по своему желанию.
Если вы планируете использовать Node для обслуживания веб-сайта, то вам, ве­
роятно, понадобится изучить использование таких каркасов, как Express или Коа, ко­
торые возьмут на себя часть работы по построению веб-сервера с нуля.
Коа - это преемник весьма популярного каркаса Express, и это не
случайно: оба написаны Ти Джей Головайчуком. Если вы уже знако­
мы с Express, то и с Коа вы почувствуете себя как дома, за исключени­
ем только того, что в нем применяется подход к веб-разработке, более
ориентированный на ЕSб.

З акл юч ение
Здесь м ы поверхностно затронули самые важные моменты интерфейса API Node.
Мы сосредоточились на тех пакетах, которые вы, вероятно, увидите почти в каждом
приложении (таких, как f s , Buffer, proc e s s и stream). Однако существует и множе­
ство других пакетов, которые вы должны будете изучить самостоятельно. Официаль­
ная документация (ht tps : / / nodej s . o rg / en/ docs / ) - очень подробна, но для но­
вичка может быть сложной. Если вас интересует разработка приложений для Node,
рекомендую начать с книги Шелли Пауэрса (Shelley Powers) Learning Node.

338

Глава 20. Платформа Node

ГЛАВА 21

Свойства о бъ е кта
и про кси - о бъ е кты

С войства доступа : получатели и установ щ и ки
Существует два типа свойств объектов: свойства данных (data property) и свойства
доступа (accessor property). Мы уже сталкивались с обоими типами, но свойства до­
ступа остались за кадром благодаря некоторым синтаксическим нововведениям ЕSб (в
главе 9 мы называли их "динамическими свойствами").
Мы знакомы с функциональными свойствами (или методами); свойства доступа
подобны им, но у них есть две функции (получения значения (getter) и установки зна­
чения (setter)), которые при доступе к ним действуют скорее как свойство данных,
чем как функция.
Давайте рассмотрим динамические свойства. Предположим в классе User есть
методы setEmai l и getEmail. Мы решили использовать методы "get" и "set" вместо
обычного свойства ema i l потому, что хотим предотвратить ввод пользователем не­
допустимого адреса электронной почты. Наш класс очень прост (для простоты мы
считаем любую строку, содержащую символ " @ " допустимым адресом электронной
почты).
const USER EМAIL = SymЬol ( ) ;
c l a s s User {
setEmai l ( va lue ) {
i f ( ! / @ / . te s t ( value ) ) throw new Еrrоr ( ' Неправильный адре с : $ { value } ' ) ;
this [ USER_EМAI L ] = value ;
getEma i l ( )
return t h i s [ USER_EМAI L ) ;

Единственное, что в этом примере заставляет нас использовать два метода (вме­
сто обычного свойства данных), - это предотвращение присваивания свойству
USER_EMAI L недопустимого адреса электронной почты. Здесь мы используем сим­
вольное свойство, чтобы блокировать случайный прямой доступ к свойству данных,
содержащему адрес электронной почты. Если бы мы назвали строковое свойство
ema i l или даже _ema i l, то было бы довольно просто по небрежности обратиться
к нему непосредственно.
Это типичный сценарий использования, и он прекрасно работает, но несколько
громоздким, чем нам хотелось бы. Вот пример использования этого класса.
const u = new U s e r ( ) ;
u . setEmai l ( " j ohn@doe . com" ) ;
console . log ( ' Aдpe c поль зователя : $ { u . getEma i l ( ) } ' ) ;

Хотя это вполне сработает, было бы естественнее написать
const u = new User ( ) ;
u . emai l = " j ohn@doe . com" ;
console . log ( ' Aдpec пользователя : $ { u . email } ' ) ;

Введем свойства доступа: они позволят сохранить преимущества прежнего под­
хода с естественным синтаксисом последнего. Давайте перепишем наш класс, ис­
пользуя свойства доступа.
SymЬol ( ) ;
const USER_EМAIL
class User {
s et ema i l ( value ) {
i f ( ! / @ / . te s t ( value ) ) throw new Еrrоr ( ' Неправильный адрес : $ { value } ' ) ;
this [ USER_EМAIL ] = value ;
=

get ema i l ( ) {
return this [ USER_EМAI L ] ;

Мы создали две разные функции, но они связаны с единым свойством ema i l .
Если значение свойству присваивается, то вызывается функция установки значения
(с передачей присваиваемого значения в качестве первого аргумента), а если значе­
ние свойства запрашивается, то вызывается функция получения значения.
Можно создать функцию-получатель без функции-установщика значения; рас­
смотрим, например, функцию-получатель, которая возвращает периметр прямоу­
гольника.
class Rectangle
constructor ( wi dt h , hei gh t )
thi s . width = width ;
thi s . height = height ;

340

Глава 2 1 . Свойства объекта и п рокси -объекты

get perimeter ( ) {
return thi s . width* 2 + this . height * 2 ;

Здесь нет функции-установщика значения для периметра потому, что нет ника­
кого очевидного способа определить ширину и высоту прямоугольника исходя из
длины периметра; это свойство имеет смысл сделать свойством только для чтения.
Аналогично вы можете создать функцию-установщик без функции-получателя,
хотя это намного менее распространенный вариант.

Атрибуты сво йств объекта
К настоящему моменту вы уже достаточно поработали со свойствами объектов.
Известно, что у них есть ключ (который может быть строкой или символом) и зна­
чение (которое может иметь любой тип). Мы также знаем, что порядок следования
свойств в объекте не гарантируется (как, например, у массива или объекта Мар).
Известны два способа обращения к свойствам объекта (доступ к члену с использо­
ванием точечной формы записи и вычисляемый доступ к члену с использованием
квадратных скобок). Наконец известны три способа создания свойства с помощью
литеральной формы записи объекта (обычные свойства с ключами, которые явля­
ются идентификаторами, вычисляемые имена свойств, позволяющие обойтись без
идентификаторов и использовать символы, а также сокращения методов).
Как бы то ни было, о свойствах необходимо знать больше. В частности, у свойств
есть атрибуты, контролирующие поведение свойства в контексте объекта, которому
они принадлежат. Начнем с создания свойства, используя одну из известных мето­
дик, а затем используем метод Obj ect . getOwnPropertyDes criptor для исследования
его атрибутов.
const obj = { foo : "bar" } ;
Obj ect . getOwnPropertyDes criptor ( obj ,

' foo ' J ;

Это код возвратит следующее.
value : " bar " , writaЫ e : true , e numeraЫ e : t rue , configuraЫe : true }

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


Атрибут writaЫe (перезаписываемый) определяет, может ли значение свой­
ства быть изменено.
Атрибуты свойств объекта

341



Атрибут enumeraЫe (перечислимый) определяет, будет ли свойство участво­
вать в перечислении свойств объекта (с использованием for . . . in, Obj ect .
keys или оператора расширения).



Атрибут configuraЫe (перестраиваемый) определяет, может ли свойство
быть удалено из объекта или могут ли быть изменены его атрибуты.

Мы можем управлять атрибутами свойства, используя метод Obj ect . de f i ne
P roper t y. Он позволяет создавать новые свойства или изменять существующие
(если это свойство перестраиваемое).
Например, чтобы сделать свойство foo объекта obj доступным только для чте­
ния, можно использовать Obj ect . defineProperty так.
Obj ect . definePropert y { ob j ,

' foo ' , { writaЫ e : false } ) ;

Теперь, если мы попытаемся присвоить значение свойству foo, то получим ошибку.
obj . foo = З ;
11
TypeError : Нельзя присваивать значение свойству ' foo ' , доступному только чтение

Попытка изменить значение свойства только для чтения закончится
ошибкой только в строгом режиме. В нестрогом режиме ничего при­
своено не будет, но и ошибки при этом тоже не будет.
Мы можем также использовать метод Obj ect . def ineProperty для добавления
к объекту нового свойства. Это особенно полезно для свойств с атрибутами, поскольку
в отличие от свойств данных нет никакого другого способа добавить свойство доступа
после того, как объект был создан. Давайте добавим к объекту obj свойство color (на
сей раз мы не будем заботиться о символах или проверке правильности).
Obj ect . de f inePropert y { ob j , ' co l or ' , {
get : function { ) { return thi s . color ; } ,
set : function { value ) { thi s . color = value ; } ,
});

Чтобы создать свойство данных, нужно указать его значение в параметрах при
вызове Obj ect . defineProperty. Добавим к объекту obj свойства name и greet.
Obj ect . defineProperty { ob j , ' name ' , {
value : ' Cynthia ' ,
});
Obj ect . defineProperty { ob j , ' greet ' , {
value : funct ion ( ) { return ' Привет , меня зовут $ { thi s . name } ! ' ; }
});

Один из популярных случаев применения Obj ect . de f ine Property
это сде­
лать свойства неперечислимыми в массиве. Мы упоминали прежде, что не стоит
-

342

Глава 2 1 . Свойства объекта и n рокси-объекты

использовать строковое или символьное свойство в массиве, поскольку это противо­
речит самой идее применения массива, но это может быть полезно, если сделано осто­
рожно и осмысленно. Хотя применение цикла for . . . in или Obj ect . keys для массива
также не очень хорошо (вместо них рекомендуется использовать for, for . . . of или
Arra y . prototype . forEach), вы не можете запретить людям ими пользоваться. Поэто­
му, добавляя нечисловые свойства к массиву, необходимо делать их неперечислимыми
на случай, если кто-то (по неосторожности) воспользуется массивом for . . . in или
Obj ect . keys. Вот пример добавления к массиву методов sum и avg.
const arr = [ 3 , 1 . 5 , 9 , 2 , 5 . 2 ] ;
arr . sum = funct ion ( ) { return t hi s . reduce ( ( a , х ) => а+х ) ;
arr . avg = functi on ( ) { return this . sum ( ) /thi s . length;
Obj ect . defineProperty ( arr, ' sum ' , { enumeraЫe : false } ) ;
Obj ect . defineProperty ( arr, ' avg ' , { enumeraЬle : false } ) ;

Мы моrли бы также сделать это за один этап для каждого свойства.
const arr = [ 3 , 1 . 5 , 9, 2 , 5 . 2 ] ;
Obj ect . defineProperty ( ar r , ' sum ' , {
value : funct ion ( ) { return thi s . reduce ( ( а , х ) => а+х ) ; ) ,
enumeraЫe : f a l s e
});
Obj ect . define Property ( arr, ' avg ' , {
value : function ( ) { return this ; sum ( ) /thi s . l ength ; } ,
enumeraЫ e : f a l s e
}) ;

Наконец есть также метод Obj ect . defineProperties (обратите внимание на на­
звание во множественном числе!), который позволяет определить сразу несколько
свойств для объекта. Таким образом, мы можем переписать предыдущий пример как
const arr = [ 3 , 1 . 5 , 9 , 2 , 5 . 2 ] ;
Obj ect . definePropert i e s ( arr,
sum : {
value : function ( ) { return t hi s . reduce ( ( а , х ) => а+х ) ; } ,
enumeraЫe : false
}) ,
avg : {
value : funct ion ( ) { return this . sum ( ) /thi s . length ; } ,
enumeraЫ e : fal s e
})
);

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

343

месте может изменить объект любым способом, довольно просто написать код, кото­
рый непреднамеренно или, что еще хуже, преднамеренно, будет делать опасные вещи.
В JavaScript предусмотрено три механизма для предотвращения неумышленных
изменений (и затруднения злонамеренных): замораживание (freezing), запечатыва­
ние (sealing) и запрет расширения (preventing extension).
Замораживание предотвращает любые изменения объекта. Как только вы замора­
живаете объект, вы не можете


установить значение его свойств;



вызывать методы, которые изменяют значение свойств объекта;



вызывать функции установки значения объекта (которые изменяют значение
свойств объекта);



добавлять новые свойства;



добавлять новые методы;



изменять конфигурацию существующих свойств или методов.

По сути, замораживание объекта делает его неизменным. Это полезно для объек­
тов-данных, поскольку замораживание объекта, содержащего методы делает беспо­
лезным любые методы, которые изменяют состояние объекта.
Чтобы заморозить объект, используйте метод Obj ect . freeze (чтобы выяснить,
заморожен ли объект, вызовите метод Obj ect . i sFrozen). Предположим, например,
что у вас есть объект, который вы используете для хранения неизменяемой инфор­
мации о своей программе (такой, как название компании, версия, идентификатор
сборки и метод получения информации об авторских правах).
con s t appi nfo
{
compan y : ' Wh i t e Knight Softwa r e , Inc . ' ,
version : ' 1 . 3 . 5 ' ,
bui ldi d : ' Oa 9 9 5 4 4 8 -ead4- 4 a 8b-b05 0 - 9 c9 0 8 3 2 7 9ea2 ' ,
/ / эта функция только читает значения свойств , поэтому заморажива ние
/ ! на нее не повлияет
copyright ( ) {
return ' с $ { new Date ( ) . ge t FullYear ( ) } , $ { th i s . compan y } ' ;
},
};
Obj ect . free z e ( appinfo ) ;
Obj ect . i s Frozen ( appinfo ) ; / / true
=

appi n fo . newProp = ' te st ' ;
/ / TypeError : Нельзя добавить свойство newProp, объект не ра сширяем
del e t e appinfo . compan y ;
/ / TypeError : Нельзя удалить свойство ' сатрапу '

344

Глава 2 1 . Свойства объекта и прокси-объекты

app i nfo . company = ' te s t ' ;
/ / TypeError : Нельзя присваивать значение свойству только для чтения ' сатрапу '
Obj ect . defineProperty ( appinfo, ' company ' , { enumeraЫe : false } ) ;
/ / TypeError: Нельзя переопределять свойство : сатрапу

Запечатывание объекта предотвращает добавление новых свойств, реконфигура­
цию или удаление существующих свойств. Запечатывание применяется, когда нуж­
но, чтобы работали все методы экземпляра класса, изменяющие свойства объекта (по
крайней мере до тех пор, пока они не попытаются перенастроить эти свойства). Вы
можете запечатать объект методом Obj ect . seal, а узнать, запечатан ли объект, - вы­
звав метод Obj ec;t . i sSealed:
c la s s Logger {
cons t ructor ( name )
this . name = name ;
this . log = [ ] ;
add ( entry) {
this . log . push ( {
log : entry,
t ime stamp : Date . now ( ) ,
}) ;

const log = new Logger ( " Бортовой журнал" ) ;
Obj ect . seal ( log) ;
/ / true
Obj e ct . i s Sealed ( log ) ;
log . name
" Бортовой журнал капитан а " ;
/ / ОК
log . add ( " Eщe один скучный день на море . . . . " ) ; / / ОК
=

log . newProp = ' te st ' ;
/ / TypeError : Нельзя добавить свойство пewProp , объект не ра сширяем
log . name

=

' te s t ' ;

/ / ОК

delete l o g . name ;
/ / TypeError : Нельзя удалить свойство 'пате '
Obj ect . defineProper t y ( log, ' log ' , { enumeraЫe : false } ) ;
/ / TypeError: Нельзя переопределить свойство : log

И наконец самая слабая защита - сделать объект нерасширяемым, что запретит
только добавление новых свойств. Свойствам могут быть присвоены значения, они
могут быть удалены и реконфигурированы. Мы можем продемонстрировать методы

Защита объе ктов: замораживание, за печатывание и за п рет расширения

345

Obj ect . preventExtens ions и Obj ect . i sExtensiЫe на примере нашего прежнего
класса Logger.
const log2 = new Logger ( "Жypнaл первого помощника " } ;
Obj e ct . preventExtensions ( log2 } ;
Obj ect . i sExtensiЬle ( log2 } ; / / true
log2 . name = " Бортовой журнал первого помощника" ;
// ОК
log2 . add ( "Eщe один скучный день на море . . . . " } ; / / ОК
log2 . newProp = ' te s t ' ;
/ / TypeError : Нельзя добавить свойство newProp, объект не ра сширяем
log2 . name = ' te st ' ;
delete log2 . name ;
Obj ect . definePropert y ( log2 ,
{ enumeraЫe : false } } ;

1 1 ок
1 1 ок

' log ' ,
1 1 ок

Я использую метод Obj ect . preventExtensions не очень часто. Для предотвраще­
ния модификации объекта обычно я также хочу предотвратить удаление и реконфи­
гурацию его свойств, поэтому я предпочитаю запечатывать объект.
Возможности защиты объектов приведены в табл. 2 1 . 1 .
Табл ица 2 1 .1 . Возможности защиты объектов
Действие
Доба вление с войства
Чтение с войства
Установ ка значения с войства
Перенастройка с войства
Удаление с войств а

Обычный
объект
Разреш ено
Разрешено
Разреш ено
Разреш ено
Разреш ено

Замороженный объект
Запрещено
Разрешено
Запрещено
Запрещено
Запрещено

Запечатанный объект
Запре щено
Разрешено
Разрешено
Запре щено
Запре щено

Нерасwиряемый объект
Запрещено
Разре ш ено
Разрешено
Разрешено
Разрешено

Прокси-объекты
Прокси-объекты (proxy) - это нововведение ЕSб, которое обеспечивает дополни­
тельные функциональные возможности метапрограммuрованuя (metaprogramming),
т.е. способности программы изменять саму себя.
Прокси-объект, по существу, способен перехватывать и (опционально) изменять
действия объекта. Для начала рассмотрим простой пример: изменение свойства до­
ступа. Начнем с обычного объекта, у которого есть несколько свойств.
const coefficients
а: 1,
Ь: 2,
с: 5,
};

346

=

{

Гла ва 2 1 . Свойства объекта и прокси-объекты

Предположим, что свойства этого объекта представляют коэффициенты в мате­
матическом уравнении. Мы могли бы использовать его так.
funct ion evaluat e ( x , с) {
return с . а + с . Ь * х + с . с * Math . pow ( x , 2 ) ;

Пока неплохо". Теперь мы можем хранить коэффициенты квадратного уравнения
в объекте и вычислять уравнение для любого значения х. Но что если мы передадим
объект с недостающими коэффициентами?
const coeff icients = {
а : 1,
с: 3,
};
evalua t e ( S , coe fficients ) ; / / NaN

Мы могли бы решить проблему, установив coe fficient s . Ь равным О, но прок­
си-объекты предоставляют нам лучшую возможность. Поскольку они способны
перехватывать действия с объектом, мы можем гарантировать, что неопределенные
свойства всегда будут иметь значение О. Давайте создадим прокси-объект для наше­
го объекта coe fficients.
const betterCoefficients
new Proxy ( coefficients ,
get ( targe t , key ) {
return t arget [ ke y ] 1 1 О ;
},
});
=

На момент написания этой книги прокси-объекты не поддержива­
лись в Babel. Однако они поддерживаются в текущем выпуске Firefox,
и эти примеры кода можно проверить там.

целевой объект (target), или объект,
Первый аргумент конструктора Proxy
к которому применяется прокси-объект. Второй аргумент
обработчик (handler),
который определяет перехватываемые действия. В данном случае мы перехватываем
только доступ к свойствам, обозначаемый функцией get. Этот процесс несколько от­
личается от методов доступа к свойствам get . . , поскольку он работает и для обыч­
ных свойств, и для методов доступа. Функции get передается три аргумента (мы ис­
пользуем только первые два): целевой объект, ключ свойства (строка или символ)
и получатель (сам прокси-объект или нечто происходящее от него).
В этом примере мы просто выясняем, установлен ли ключ на целевом объекте,
и если не установлен, то возвращаем значение О. Давайте опробуем это.
-

-

.

betterCo e f f i c i ent s . a ;
betterCoefficients . b ;
betterCoe fficient s . c ;

11 1
11 о
11 3

Прокси -объекты

347

bet terCoeffi cient s . d ;
// О
betterCoeffi cient s . anything ; // О ;

По существу, мы создали прокси-объект для нашего объекта coe fficients, кото­
рый способен иметь бесконечное количество свойств (все устанавливаются равными
О, кроме тех, которые были определены явно)!
Мы могли бы еще несколько улучшить свой прокси-объект, чтобы он обрабаты­
вал только свойства, имена которых состоят из одиночных строчных букв.
const betterCoe fficient s

=

new Proxy ( coefficient s ,

{

get ( t arge t , key ) {
i f ( ! / л [ a- z ] $ / . te s t ( ke y ) ) return target [ ke y ] ;
return target [ ke y ] 1 1 О ;
},
}) ;

Вместо простой проверки н а существование свойства target [ key] м ы могли бы
возвращать О, если его значение не является числом . .. Я оставляю это упражнение
для читателя.
Точно так же мы можем перехватить свойства (или методы доступа), устанавливае­
мые обработчиком set. Давайте рассмотрим пример, в котором у объекта имеются опас­
ные свойства. Мы хотим воспрепятствовать установке значений этих свойств и вызову
методов-установщиков без дополнительного этапа контроля. Дополнительным этапом
контроля, который мы будем использовать перед обращением к опасным функциям, яв­
ляется установка свойства allowDangerousOperations равным значению t rue.
const cook = {
name : "Wal t " ,
redPhosphorus : 1 0 0 , / / опа сно
/ / безопасно
water : 5 0 0 ,
};
con s t protect edCook
new Proxy ( cook,
set ( targe t , key, value ) {
i f ( ke y === ' redPhosphorus ' ) {
i f ( target . a l lowDangerousOperations )
return t arget . redPhosphorus = value ;
else
return console . log ( " Oчeнь опасно ! " ) ;
}

/ / в се остальные свойства безопа сны
targe t [ ke y ] = value ;
},
}) ;
protectedCoo k . water = 5 5 0 ;
protectedCook . redPhosphorus

348

Глава

21 .

11 550
1 5 0 ; // Очень опа сно !

Сво йства объекта и п рокси-объекты

protectedCook . al lowDangerousOperat ions = true ;
protectedCook . redPhosphorus = 1 5 0 ; / / 1 50

В этом разделе мы весьма поверхностно рассмотрели основные функции прокси­
объектов. Чтобы узнать больше, я рекомендую начать со статьи Акселя Роушмайера
(Axel Rauschmayer) Meta Prograrnrning with ECMAScript 6 Proxies (http : / /www . 2ality .
com/ 2 0 1 4 / 1 2 / e s б -proxie s . h tml ) , а затем читать документацию MDN (https://
developer. mozilla.org/ru/docs/WebIJavaScript/Reference/Global_Objects/Proxy).

Закл ю ч е н ие
В этой главе м ы приподняли занавес, который скрывает механизм объектов
JavaScript, и получили подробную картину работы свойств объекта, а также способов
изменения их поведения. Мы также узнали, как защитить объекты от изменения.
Наконец мы узнали о чрезвычайно полезной новой концепции ЕSб
прокси­
объектах. Прокси-объекты предоставляют мощные методики метапроrраммирова­
ния, и я подозреваю, что мы еще увидим некоторые весьма интересные способы их
использования в связи с ростом популярности преимуществ ЕSб.
-

За ключени е

349

ГЛАВА 22

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

Мое мнение о том, что JavaScript - это выразительный и мощный язык, сформи­
ровалось довольно давно. Это не "игрушечный" язык, который легко изучить или
отбросить как "для начинающих". Вы наверняка это уже очень хорошо понимаете,
потому что изучили предыдущие главы данной книги!
Моя задача в этой книге не в том, чтобы исчерпывающе описать каждое сред­
ство языка JavaScript, а в том, чтобы рассмотреть каждую наиболее важную методику
программирования. Если JavaScript - это ваш первый язык, то вы только вначале
своего пути. Я надеюсь, что дал вам стройную структуру информации, основываясь
на которой, вы сможете стать экспертом.
Большая часть материала этой главы взята из моей первой книги
Web
Developтent with Node and Express (издательство O'Reilly).
-

Сетевая документация
Для JavaScript, CSS и HTML документация сети разработчиков Mozilla (Mozilla
Developer Network - MDN, https : / / developer . mo z i l la . org/ru/) не имеет равных.
Если мне нужна документация по JavaScript, я либо ищу ее непосредственно в MDN,
либо добавляю "mdn" к своему поисковому запросу. В противном случае в резуль­
татах поиска будет неизбежно присутствовать w3schools. Кто бы ни предоставлял
услуги SEO для w3schools, он гений, но я рекомендую избегать этого сайта; я нахожу,
что документации на нем зачастую недостает.
Хотя MDN - это великолепный справочник по HTML, если вы новичок в HTMLS
(или даже не знакомы с ним), вам стоит прочитать книгу Эда Титтеля и Криса Мин­
ника HTMLS и CSSЗ для чайников. Сообщество WНATWG поддерживает постоянно
обновляемую спецификацию HTML5 (https : / /developers . whatwg . org/); именно
к нему я обычно обращаюсь в первую очередь в поисках ответов на действитель­
но сложные вопросы по HTML. И наконец есть официальные спецификации HTML
и CSS, расположенные на веб-сайте WЗС; это сухие, трудные для чтения документы,
но иногда это единственная надежда при решении самых серьезных проблем.

ЕSб соответствует языковой спецификации ЕСМА-262 ECMAScript 2015 Laпgиage
Specificatioп (http : / / www . ecma-internat ional . o rg / ecma- 2 62 / 6 . О /) . Для провер­
ки доступности средств ЕSб в Node (и в различных браузерах) обращайтесь к пре­
восходному руководству, поддерживаемому @kaпgax (h t t р : / / kang ах . g i thub . i о /

compat-taЬle / e s б / ).
Для jQиery (ht tp : / / api . j que ry . com/ ) и Bootstrap (http : / / getbootstrap . com/)
есть чрезвычайно хорошая сетевая документация.
Документация по Node (https : / /node j s . org/api /) является весьмавысококаче­
ственной и исчерпывающей, и это должен быть ваш первый выбор при поиске авто­
ритетной документации о модулях Node (таких, как http, http и fs). Документация
по прт (https : / /docs . npmj s . com/) является исчерпывающей и полезной, особен­
но страница о файле package . j son (https://docs.npmjs.com/getting-started/using-a­
package.json).

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


JavaScript Weekly (http : / / j avascriptweekly . com/)



Node Weekly (http : / /nodeweekly . com/)



HTMLS Weekly (http : / / frontendfocus . co/)

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

6nor и и у ч ебные курсы
Блоги - это отличное средство быть в курсе событий по JavaScript. Но при чте­
нии некоторых из этих блогов я не совсем был абсолютно согласен.


В благе Акселя Роушмайера (Axel Rauschmayer, http : / /www . 2ality. com/) есть
великолепные статьи о ЕSб и связанных с ним технологиях. Д-р Роушмайер
подходит к JavaScript с академической точки зрения информатики, но его ста­
тьи весьма доступны и просты для чтения, а специалисты в информатике оце­
нят по достоинству дополнительные подробности, которые он предоставляет.



В благе Нолана Лоусона (Nolan Lawson, h t tps : / /nol anl awson . com/) есть
много прекрасных подробных постов о реальной разработке приложений
на JavaScript. Его статья We Have а РrоЫет with Promises (https : / /pouchdb .
com/ 2 0 1 5 / 0 5 / 1 8 /we-have - a-proЫ em-wi th-p romi s e s . html ) обязательна
для изучения.

352

Глава 2 2 . Дополнительные ресурсы



В благе Дэвида Уолша (David Walsh, https : / /davidwal s h . name / ) есть фан­
тастические статьи о разработке приложений на JavaScript и о связанных
с ним технологиях. Если вы испытывали затруднения при изучении главы 1 4,
обязательно прочитайте его статью The Basics of ЕSб Generators (http s : / /

davidwalsh . name/es6-generators).


Блог @ kangax, Perfection Kills (http : / /perfect ionki l l s . com/), полон фантас­
тических учебных пособий, упражнений и вопросов. Он настоятельно реко­
мендуется и для новичков, и для экспертов.

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


Курсы Lynda.com JavaScript (https : / /www . lynda . com/ JavaScript-training­

tutorials / 2 4 4 - 0 . html) .


Курсы Treehouse JavaScript (htt ps : / / t e amtreehouse . com/ l e a rn-to-code/

j avascript).


Курсы Codecademy's JavaScript ( h t t p s : / / www . c od e c a demy . c om/ l e a rn /

j avascript).


Вводный курс Microsoft Virtual Academy's intro course оп JavaScript (https : / /
mva . mi c rosoft . com/en-U S / t ra ining-course s / j avascript-fundamenta l s ­
for-absolute-beginners- 1 4 1 9 4 ). Если в ы пишете код JavaScript для систем
Windows, я рекомендую это как материал по использованию Visual Studio
для разработки приложений на JavaScript.

Система Stack Overflow
Весьма велики шансы, что вы уже используете популярную систему вопросов
и ответов Stack Overflow (SO). Будучи запущенной в 2008 году, она стала доминиру­
ющим сетевым сайтом вопросов и ответов для программистов, и это ваш наилучший
ресурс, чтобы задавать вопросы и получать ответы по JavaScript (и любой другой
технологии, описанной в этой книге). Stack Overflow - это поддерживаемый сооб­
ществом сайт вопросов и ответов на основании репутации. Модель репутации - это
то, что отвечает за качество сайта и его успех. Пользователи могут зарабатывать ре­
путацию в ходе "голосования" по их вопросам или ответам или давая одобренные
ответы. Чтобы задать вопрос, наличие репутации необязательно, а регистрация бес­
платна. Однако есть вещи, которые вы можете сделать, чтобы увеличить вероятность
получения ответа на свой вопрос, что мы и обсудим в этом разделе.

Система Stack Overflow

353

Репутация - это валюта Stack Overflow. И хотя там есть люди, которые ис крен не хотят вам помочь, это также дает им шанс заработать себе репутацию - доста­
точно большой соблазн, мотивирующий давать хорошие ответы. В сообществе SO
есть много действительно умных людей, и все они конкурируют между собой, чтобы
предоставить первый и/или лучший правильный ответ на ваш вопрос (к счастью,
крайне не выгодно давать быстрые, но плохие ответы). Вот что вы можете сделать,
чтобы увеличить возможности получения хорошего ответа на свой вопрос.
Будьте осведомлены

Пройдите обзорный тур SO tour (http : / / stackoverflow . corn/tour), а затем про­
читайте раздел How do I ask а good question? (Как задать хороший вопрос?, http : / /
stackoverflow . corn/help/how-to-ask). Если хотите, можете прочитать всю вспо­
могательную документацию (help docuтentation, http : 1 1 stackoverflow . corn/help)
и заработать значок!
Н е задавай те вопросы, на которые уж е о твечали

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

Вы быстро обнаружите, что ваш вопрос проголосован "против" и закрыт, если
просто спросите "Как мне сделать":' Сообщество SO ожидает, что вы прикладывали
усилия для решения своей проблемы, прежде чем обратиться к SO. Опишите в своем
вопросе, что вы пытались сделать и почему это не сработало.
З адавай те по одному вопросу за раз

Отвечать сразу на несколько вопросов ("Как я могу сделать это". а затем это". а за­
тем что-то еще и как поступить лучше?") трудно; они отбивают желание отвечать.
П редоставьте краткий пример свое й проблемы

Я ответил на множество вопросов SO, но я почти автоматически пропускаю во­
просы, когда вижу в них три (или больше! ) страницы кода. Просто взять свой файл
из 5000 строк и вставить его в вопрос SO
это не наилучший способ добраться
до ответа (но именно так люди все время и делают). Это "ленивый" подход, который
не часто вознаграждается. Мало того что вы скорее всего не получите полезный от­
вет, сам процесс устранения элементов кода, которые явно не являются причиной
проблемы, может привести вас к решению (и вам даже не понадобится задавать во­
прос на SO). Создание краткого примера позволит вам приобрести навыки в отладке
и научиться мыслить критически, что сделает вас хорошим гражданином SO.
-

354

Глава 22 . До п олнительные рес у рсы

И зучите язык разметки Markdown

Для оформления вопросов и ответов в Stack Overflow используется язык размет­
ки Markdown. У хорошо оформленного вопроса больше шансов на ответ, поэтому
имеет смысл потратить время на изучение этого полезного и все более и более вез­
десущего языка разметки.
О тветы и гол осование

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

SO - это общественный ресурс; если у вас возникла проблема, возможно, она
есть и у кого-то еще. Если вы самостоятельно решили ее, ответьте на собственный
·
вопрос для общей пользы.
Если вам нравится помогать сообществу, попробуйте сами отвечать на вопросы:
это интересно и полезно, а также может дать преимущества, куда более материальные,
чем вероятный бал репутации. Если у вас есть вопрос, на который вы не получали
полезных ответов в течение двух дней, можете назначить премию (bounty) за ответ,
используя собственную репутацию. С вашей учетной записи будут немедленно сняты
пункты репутации, и это невозмещаемо. Если кто-то отвечает на вопрос удовлетво­
рительно и вы принимаете ответ, он получает премию. Конечно, для назначения пре­
мии у вас должна быть достаточная репутация: минимальная премия - 50 пунктов
репутации. Хотя вы можете заработать репутацию, задавая качественные вопросы, как
правило, можно быстрее заработать ее, предоставляя качественные ответы.
У ответов на вопросы есть также то преимущество, что это отличный способ са­
мообучения. Отвечая на вопросы других, я обычно чувствую, что узнаю больше, чем
когда ищу ответы на собственные вопросы. Если вы действительно хотите полностью
изучить технологию, узнайте ее основы, а затем попытайтесь заняться вопросами дру­
гих на SO. Поначалу вы, может быть, будете стесняться людей, которые уже являются
экспертами, но со временем вы заметите, что сами стали одним из экспертов.
Наконец вы не должны смущаться, используя свою репутацию для продвижения
своей карьеры. Хорошую репутацию, безусловно, нужно упоминать в резюме. Это сра­
ботало у меня, и теперь, когда я сам интервьюирую разработчиков, меня всегда впечат­
ляет их хорошая репутация SO (хорошей я считаю репутацию SO более 3000; репута­
ция с пятью знаками
великолепна). Хорошая репутация SO говорит мне, что некто
не только компетентен в своей области, но и коммуникабелен и вообще полезен.
-

Система Stack Overflow

355

В клад в проекты О реп Source
Отличный способ обучения - продвижение проектов реализации с открытым
исходным кодом: мало того что вы столкнетесь с трудностями, которые активизи­
руют ваши способности, ваш код будут просматривать партнеры по сообществу, что
сделает вас лучшим программистом. Это также будет прекрасно выглядеть в резюме.
Если вы новичок, то наилучшее для вас - это помощь кому-либо в составлении
документации. Многие проекты с открытым исходным кодом страдают в части доку­
ментации, а вы как новичок находитесь в превосходной позиции: вы можете что-то
изучить, а затем объяснить это способом, который будет полезен для других новичков.
Иногда сообщество реализации с открытым исходным кодом может быть агрес­
сивным, но если вы будете последовательны и открыты для конструктивной кри­
тики, то заметите, что ваш вклад приветствуется. Начните с чтения превосход­
ной статьи Bringing Кindness Back to Ореп Source (http : / /www . hanse lman . com/
Ыog/BringKindnes sBackToOpenSource . a spx) в блоге Скотта Хансельмана (Scot
Hanselman). В ней автор рекомендует веб-сайт Ир for Grabs (http : / /up-for-grabs .
net), который помогает объединить программистов для проектов реашрации с от­
крытым исходным кодом. Выполните поиск по ключевому слову "JavaScript'', и вы
найдете множество проектов реализации с открытым исходным кодом, которым
нужна помощь.

З а кл юч ение
Поздравляю вас с началом пути к профессии разработчика JavaScript! Часть мате­
риала в этой книге очень сложна, но надеюсь, вы уделили достаточно времени, чтобы
в нем разобраться, и теперь ваши знания этого важного языка должны быть четко
упорядочены. Если остались сложные моменты, не пугайтесь! JavaScript - сложный
и мощный язык, который нельзя изучить быстро (даже в течение года). Если вы но­
вичок в программировании, то в будущем вы, вероятно, захотите повторить часть
материала этой книги; вы найдете новую суть в материале, который вначале казался
вам сложным.
Спецификация ЕSб породила новое поколение программистов, а вместе с ними
и многие замечательные идеи. Я рекомендую вам читать все, что сможете, говорить
с каждым программистом JavaScript, с которым сможете, и изучать каждый первоис­
точник, который сможете найти. В сообществе разработчиков JavaScript ожидается
взрыв творческого потенциала, и я искренне надеюсь, что вы будете его частью.

356

Глава

22.

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

П Р ИЛОЖЕ Н И Е А

З аре з ерв и р ованные
к л юч е в ые слова

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


await (зарезервировано для использования в будущем)



break



case



class



catch



const



continue



debugger



default



delete



do



else



enum (зарезервировано для использования в будущем)



export



extends



false (литеральное значение)



finally



for



function



if



implements (зарезервировано для использования в будущем)



import



in



instanceof



interface (зарезервировано для использования в будущем)



let



new



null (литеральное значение)



package (зарезервировано для использования в будущем)



private (зарезервировано для использования в будущем)



protectd (зарезервировано для использования в будущем)



puЫi c (зарезервировано для использования в будущем)



return



super



s tatic (зарезервировано для использования в будущем)



switch



this



throw



t rue (литеральное значение)



t ry



typeof



var



void



whi l e



with



yield

Перечисленные ниже ключевые слова были зарезервированы в спецификациях
ECMAScript 1 -3. Сейчас эти слова больше не зарезервированы, но я не рекомендую
их использовать, поскольку в некоторых реализациях JavaScript они могут непра­
вильно считаться зарезервированными.

358

П риложение А . Зарезервированные ключевые слова



abstract



boolean



byte



char



douЫe



final



float



goto



int



long



native



short



s ynchroni zed



t rans ient



volat i l e

Приложение А . Зарезервирова нные кл ю чевые слова

359

П РИЛОЖЕ Н И Е Б

Приоритет о п ераторо в

Содержимое табл. Б. 1 взято из документации Mozilla Developer Network и приведе­
но для справки. Операторы спецификации ES7 опущены.
Таблица Б.1 . Приоритет операторов от самого высокого ( 1 9 ) до самого низкого (О)

new (со списком аргументов)

Ассоциативно сть
Не определена
Справа налево
Справа налево
Не определена

17

Вызов функции
new (без списка аргументов)

Справа налево
Справа налево

16

Постфиксный инкремент
Постфиксный декремент

Не определена
Не определена

15

Логическое NOT
Побитовое NOT

Справа налево
Справа налево

!

Унарная сумма

Справа налево

+" .

Унарное вычитание

Справа налево

П рефиксный и нкремент

Справа налево

Префиксный декремент

Справа налево

typeof
void
delete

Справа налево

Умножение
Деление

Справа налево
Справа налево

Остаток

Справа налево



Сложение
Вычитание

Справа налево
Справа налево

. "+ " .

Бинарный оператор сдвига
влево

Справа налево

Приоритет Тип оператора
19
18

14

13
12

Группировка
Доступ к члену
Вычисляемый доступ к члену

Справа налево
Справа налево

Операторы
(. .)
.

". [".]
new . . . ( . . )
" ( .)
new . . .
. . . ++
.

.

.

.

"



++. . .

typeof . . .
void . . .
delete . . .
. .*
. "/" .
.





g.
о

-

.

.

.







. ..
. . . > . . .

Справа налево

. . . >>> . . .

Справа налево
Справа налево

. . .= . . .
. . . in . . .

instanceof

Справа налево

. . . inst anceof . . .

Равенство
Неравенство

Справа налево
Справа налево

. . . ! =. . .

Строгое равенство

Справа налево

Строгое неравенство

Справа налево

. . . ! ==

Побитовое AND
Побитовое XOR
Побитовое OR
Логическое AND
Логическое OR
Условное выражение
Присваивание

Справа
Справа
Справа
Справа
Справа
Справа
Справа

" .&.

налево
налево
налево
налево
налево
налево
налево

.

.

... ..

.

л

... 1...
. . .. & & . . .
.

11...
......
=
.. ...
. ·.

. . . ?
.

. . . += . . .

*=
. . . /= . . .
. %=





. . . = . . .
. . . >>>= . . .
. . . &= . . .

л=
. . . 1=. . .
2
1
о

362

yield
Расш ирение
Запятая

П риложение Б . П риоритет операторов

Справа налево
Не определена
Справа налево

yi eld . . .
. ...
...'. .
.

.

.

.

Предметный у ка з ател ь
D

А
Accessor property, 339
Accumulator, 1 69
Ajax, 308
Alternation, 276
Anchor, 289
Anonymous function, 1 38
Argument, 35; 107; 1 3 1
Array, 73
Arrow notation, 140
Asynchronous
event, 36
Javascript And Xml, 308

в
Backreference, 284
Block
scope, 1 49
statement, 85
Boilerplate, 34
BubЬling, 304
Build tool, 39; 48

Data
attribute, 301
property, 339
type, 57
Declaration, 1 45
Definition, 1 45
Dependencies, 47
Destructuring assignment, 1 24
Document, 294
Object Model, 294
DOM, 294
DRY, 220
Duck typing, 1 86
Dynamic dispatch, 1 8 1

Е

с
Callback, 1 69; 227; 232
hell, 237
Call stack, 200
Camel case, 59
Canvas, 33
Capturing, 303
Cascading Style Sheet, 29
CDN, 32
Chaining, 316
Character set, 278
Class, 1 77
method, 183
Closure, 1 52
Collision, 1 88
Comment, 28
Commit, 44
Condition, 84
Console, 3 1
Constructor, 1 77
Content Delivery Network, 32
CORS, 308
CSS, 29
selector, 298
Currying, 229

EBNF, 9 5
Element, 295
Escaping, 64
Event, 241
handler, 37
Evergreen, 40
Exception, 94
handling, 1 97
Execнtion context, 146
Existence, 1 46
Exit code, 330
Expression, 1 05; 107

F
Flowchart, 8 1
FOUC, 3 1 1
Freezing, 344
Fнnction, 1 29
expression, 1 38
scope, 1 54

G
Garbage collection, 1 46
Generator, 209
Getter, 339
Global scope, 147
Grouping, 281

н
Haskell Curry, 229
Hoisting, 1 55

IIFE, 153
Immediately Invoked Function Expression, 153
Instance, 177
method, 183
Interface, 1 88
lterator, 205
protocol, 207

J
JQuery object, 3 1 5
JQuery-wrapped DOM elements, 3 1 4
JSON, 75

к
Кеу, 1 75

L
Lexical structure, 1 46
Linter, 39; 52
Litera\, 60
Lookahead, 290
Lvalue, 122
L-значение, 122

м
Мар, 1 9 1
Metaprogramming, 346
Metasyntax, 95
Method, 1 35; 1 77
Mixin, 1 88
Modifier, 280
Multiple inheritance, 1 88
Multitasking, 231

N
NaN, 1 1 3
Node, 294; 3 1 9
Nul\, 69

о
Object, 35; 62
Object-Oriented Programming, 1 77
Operand, 1 07
Operator precedence, 1 06

р
Parameter, 1 3 1
Parsing, 277
Pipe, 335
Pipeline, 5 1 ; 225
Piping, 336
Polymorphism, 1 85
Preventing extension, 344

364

Предметный указатель

Primitive, 6 1
Promise, 238
Property, 1 75
attribute, 341
Prototype, 1 8 1
chain, 1 8 1
Proxy, 346

R
Recursion, 1 39; 229
Reference type, 1 33
Regex, 76
Regexp, 76
Regular expression, 76; 271
Repetition, 280
Return value, 1 30

s
Scope, 145
Sealing, 344
Set, 1 94
Setter, 339
Short-circuit evaluation, 1 1 7
Side effect, 1 17
Signature, 1 33
Snake case, 60
Sort function, 1 63
Spread operator, 1 25
Standalone Ыосk, 149
Statement, 1 07
Static method, 183
Stopping condition, 230
Stream, 335
Strict mode, 1 58
String, 64
concatenation, 66
interpolation, 67
Subc\ass, 1 77
Subexpression, 282
Subroutine, 2 1 5
Superc\ass, 1 77
Symbo\, 69

т
TDZ, 157
Template string, 67; 126
Temporal Dead Zone, 1 57
Terminal, 4 1
Ternary, 1 18
Transcompiler, 39
Transpilation, 40
Traverse, 296

u
Uncaught exception, 200
Undefined, 69
Unhandled exception, 200
Unicode, 64
UTC, 254

v
Value, 1 75
type, 1 33
VariaЬle, 57
masking, 1 50

А
Автономный блок, 1 49
Аккумулятор, 1 69
Анализатор, 277
Анонимная функция, 1 38
Аргумент, 35; 107
командной строки, 3 3 1
функции, 1 3 1
Асинхронное событие, 36
Атрибут
данных, 301
свойства, 341

Б
Базовый тип, 6 1
Библиотека
jQuery, 32
Math.js, 263
Moment.js, 255
Numeral.js, 266
Paper.js, 33
seedrandom.js, 268
Блок
finally, 202
операторов, 85
Блок-схема, 8 1

в
Верблюжья нотация, 59
Вечнозеленый, 40
Возвращаемое значение, 1 30
Временная мертвая зона, 157
Всплытие
события, 304
Выражение, 105; 1 07
Вычисление по сокращенной схеме, 1 1 7

г
Генератор, 208
псевдослучайных чисел, 268

Граница слова, 289
Группировка, 282

д

Дата, 75
Деструктурирующее присваивание, 1 24
Диапазоны, 278
Динамические свойства, 339
Динамический вызов, 1 8 1
Документ, 294

3
Зависимость, 4 7
времени разработки, 47
Замкнутое выражение, 152
Замораживание, 344
Запечатывание, 344
Запрет расширения, 344
Змеиная нотация, 60
Значение, 1 75

и
Идентификатор, 59
Инструмент сборки, 39; 48
И нтерфейс, 1 88
Исключение, 94
Итератор, 205

к
Канал, 335
Карринг, 229
Каскадная таблица стилей, 29
Класс, 1 77
Node, 295
Object, 1 86
RegExp, 272
Ключ, 1 75
Ключевое слово
const, 58
let, 57; 1 54
return, 1 30
this, 1 36
var, 1 54
yield, 2 1 0
Код завершения, 330
Комментарий, 28
Конвейер, 5 1 ; 225; 336
Конкатенация строк, 66; 1 1 4
Консоль, 3 1
Константа, 58
Конструктор, 1 77
Конструкция
try... catch ... fina\ly, 202

П редметный у казатель

365

л

Лексическая структура, 146
Литерал, 60
Логическое значение, 69
м

Маскировка переменной, 1 50
Массив, 73; 1 59
Метапрограммирование, 346
Метасинтаксис, 95
Метод, 1 35; 1 37; 1 77
apply, 1 42
Ьind, 1 42
call, 1 4 1
every, 1 66
getElementByid, 298
getElementsByClassName, 298
getElementsByTagName, 298
Object.keys, 1 76
querySelector, 298
querySelectorAll, 298
reduce, 1 68
some, 1 66
класса, 183
статический, 183
экземпляра, 183
Многозадачность, 231
Множественное наследование, 1 88
Модификатор, 280
Модуль, 320
npm, 322
базовый, 322
файловый, 322
н

Набор, 76; 1 94
символов, 278
Неизменность, 6 1
Немедленно вызываемое
функциональное выражение, 1 53
Необработанное исключение, 200
о

Область видимости, 1 45
блока, 1 49
глобальная, 1 47
функции, 1 54
Обработка исключений, 1 97; 1 98
Обработчик событий, 37
Обратная ссылка, 284
Обратный вызов, 227; 232
Обход, 296
Объект, 35; 62; 70
Boolean, 72
Date, 75; 253
366

П редметный указател ь

Error, 1 97
Function, 22 1
jQuery, 3 1 5

Мар, 1 9 1
Math, 263
Number, 72
Set, 1 94
String, 72
атомарный. См. Атомарный объект
Объектная модель документа, 294
Объектно-ориентированное
программирование, 1 77
Объявление, 145
Обязательство, 238
Операнд, 1 07
Оператор, 107
break, 94
continue, 94
if, 9 1
if. . . else, 88
instanceof, 1 86
return, 94; 2 1 2
switch, 97
throw, 94
try".catch, 1 98
typeof, 1 2 1 ; 1 57; 1 98
void, 1 22
yield, 2 1 2
арифметический, 107
запятая, 1 1 9
логический, 1 1 6
побитовый, 1 1 9
присваивания, 1 22
расширения, 125
сравнения, 1 1 1
тройственный, 1 18
Определение, 145
Отображение, 76; 1 9 1
п

Параметр, 1 3 1
Переменная, 57
arguments, 1 35
this, 1 36
Перехват события, 303
Побочный эффект, l 1 7
Повторение, 280
Подвыражение, 282
Подпрограмма, 2 1 5
Подъем, 1 55
Поле
битовое. См. Битовое поле
Полиморфизм, 185

Пользовательский ввод, 36
Поток, 335
Примесь, 1 88
Приоритет операторов, 1 06
Производный класс, 1 77
Прокси-объекты, 346
Пространство имен, 320
Протокол итератора, 207
Прототип, 1 8 1

р
Равенство, 1 1 1
Расширенная форма Бэкуса-Наура, 95
Регулярное выражение, 76; 271
Рекурсия, 1 39; 229

с
Сборка мусора, 1 46
Свойство, 175
_proto_, 1 82
данных, 339
доступа, 339
Селектор CSS, 298
Сервер, 308
Сеть доставки контента, 32
Сигнатура, 1 33
Символ, 69
Событие, 24 1
click, 302; 303
Составной оператор присваивания, 1 23
Состояние гонки. См. Гонка данных
Специальный символ, 65
Стек вызовов, 200
Стрелочная нотация, 140
Строгий режим, 1 58
Строка, 64
Строковая интерполяция, 67
Строковый шаблон, 67; 1 26
Суперкласс, 1 77
Сушествование, 1 46
Сцепление, 244; 3 1 6

т
Таблица ИСТИННОСТИ, 1 16
Терминал, 4 1
Тип
данных, 57

значения, 1 33
ссылочный, 1 33
Транскомпилятор, 39; 50
Транскомпиляция, 40

у
Узел, 294
Упреждение, 290
Условие, 84
остановки, 230
Утиная типизация, 1 86

ф
Фиксация изменений, 44
Функциональное выражение, 1 38
Функция, 1 29; 2 1 5
обратного вызова, 1 69
сортировки, 1 63

х
Хаскелл Бруке Карри, 229
Холст, 33

ц

Цепь прототипов, 1 8 1
Цикл
do . . . while, 89
for, 35; 90
for...in, 1 0 1 ; 1 76
for."of, 1 0 1 ; 206
while, 84

ч
Чередование, 276
Чис11а Фибоначчи, 208
Ч исло, 62

ш
Шабпон, 34

э
Экземпляр, 1 77
Э11емент
HTML, 295
Элементы DOM в обо11очке jQuery, 3 1 4
Якорь, 289

СЕКРЕТЫ
JAVASCRIPT НИНДЗЯ

ДЖОН РЕЗИГ
БЕЭР БИБО

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

В книге уделяется

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

www.williamspuЫishing.com

в программ ировании на
JavaScript в частности
и разработке веб-приложен и й
вообще .

ISBN 978-5-8459- 1 9 59-5

в продаже