Замыкания и объекты [Кайл Симпсон] (pdf) читать онлайн

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


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

this & Object Prototypes
Scope & Closures

Kyle Simpson

ВЫ НЕ ЗНАЕТЕ

ЗАМЫКАНИЯ
ОБЪЕКТЫ
КАЙЛ СИМПСОН

ББК 32.988-02-018
УДК 004.738.5
С37

Симпсон К.
С37 {Вы не знаете JS} Замыкания и объекты. — СПб.: Питер, 2019. —
336 с.: ил. — (Серия «Бестселлеры O’Reilly»).
ISBN 978-5-4461-1255-5
Каким бы опытом программирования на JavaScript вы ни обладали, скорее всего, вы
не понимаете язык в полной мере. Это лаконичное, но при этом глубоко продуманное
руководство познакомит вас с областями видимости, замыканиями, ключевым словом this
и объектами — концепциями, которые необходимо знать для более эффективного и производительного программирования на JS. Вы узнаете, почему они работают и как замыкания
могут стать эффективной частью вашего инструментария разработки.
Как и в других книгах серии «Вы не знаете JS», здесь показаны нетривиальные аспекты
языка, от которых программисты JavaScript предпочитают держаться подальше. Вооружившись этими знаниями, вы достигнете истинного мастерства JavaScript.

16+ (В соответствии с Федеральным законом от 29 декабря 2010 г. № 436-ФЗ.)

ББК 32.988-02-018
УДК 004.738.5
Права на издание получены по соглашению с O’Reilly. Все права защищены. Никакая часть
данной книги не может быть воспроизведена в какой бы то ни было форме без письменного
разрешения владельцев авторских прав.
Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как надежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не может гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за возможные ошибки, связанные с использованием книги. Издательство не несет ответственности за доступность материалов, ссылки на
которые вы можете найти в этой книге. На момент подготовки книги к изданию все ссылки на
интернет-ресурсы были действующими.

ISBN 978-1491904152 англ.

ISBN 978-5-4461-1255-5

Authorized Russian translation of the English edition of You
Don’t Know JS: this & Object Prototypes (ISBN 9781491904152)
© 2014 Getify Solutions, Inc.
Authorized Russian translation of the English edition of You
Don’t Know JS: Scope & Closures (ISBN 9781449335588)
© 2014 Getify Solutions, Inc.
This translation is published and sold by permission of O’Reilly
Media, Inc., which owns or controls all rights to publish and sell
the same.
© Перевод на русский язык ООО Издательство «Питер»,
2019
© Издание на русском языке, оформление
ООО Издательство «Питер», 2019
© Серия «Бестселлеры O’Reilly», 2019

Оглавление

Введение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
Задача. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
Благодарности. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
О книге. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
Типографские соглашения. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
Использование программного кода примеров. . . . . . . . . . . . . . . . . 24
От издательства. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24

ЧАСТЬ 1. ОБЛАСТЬ ВИДИМОСТИ И ЗАМЫКАНИЯ......25
Предисловие. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Глава 1. Что такое область видимости?. . . . . . . . . . . . . . . 28
Немного теории компиляторов. . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Разбираемся в областях видимости . . . . . . . . . . . . . . . . . . . . . . . . 31
Участники. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
Туда и обратно. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
Немного терминологии. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
Общение Движка с Областью видимости . . . . . . . . . . . . . . . . . . . . 36
Упражнение. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
Вложенная область видимости. . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
Метафоры. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
Ошибки. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41

6

Оглавление

Итоги. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
Ответ на упражнение. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43

Глава 2. Лексическая область видимости. . . . . . . . . . . . . 44
Стадия лексического анализа. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
Поиск. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .47
Искажение лексической области видимости. . . . . . . . . . . . . . . . . . 48
eval. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
with . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
Быстродействие. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
Итоги. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56

Глава 3. Функциональные и блочные
области видимости. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
Области видимости из функций. . . . . . . . . . . . . . . . . . . . . . . . . . . 57
Как скрыться у всех на виду. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
Предотвращение конфликтов. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
Функции как области видимости. . . . . . . . . . . . . . . . . . . . . . . . . . . 64
Анонимные и именованные функциональные выражения. . . . . . . . 66
Немедленный вызов функциональных выражений . . . . . . . . . . . . . 67
Блоки как области видимости. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
with . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
try/catch. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
let. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
const. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
Итоги. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80

Глава 4. Поднятие. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
Курица или яйцо? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
Компилятор наносит ответный удар. . . . . . . . . . . . . . . . . . . . . . . . 84
Сначала функции. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
Итоги. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89

Оглавление

7

Глава 5. Замыкание области видимости. . . . . . . . . . . . . . 90
Просветление . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
Технические подробности . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
Теперь я вижу. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
Циклы и замыкания. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
Снова о блочной области видимости . . . . . . . . . . . . . . . . . . . . . . 102
Модули . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
Современные модули . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
Будущие модули. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
Итоги. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113

Приложение А. Динамическая область видимости . . . . 115
Приложение Б. Полифилы для блочной
области видимости. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
Traceur. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
Неявные и явные блоки. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
Быстродействие. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123

Приложение В. Лексическое this . . . . . . . . . . . . . . . . . . . 124

ЧАСТЬ 2. THIS И ПРОТОТИПЫ ОБЪЕКТОВ.............. 129
Предисловие. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
Глава 6. Что такое this?. . . . . . . . . . . . . . . . . . . . . . . . . . . 133
Для чего нужно this? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
Путаница. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135
Сама функция. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
Область видимости. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
Что такое this?. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
Итоги. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144

8

Оглавление

Глава 7. this обретает смысл!. . . . . . . . . . . . . . . . . . . . . . 145
Место вызова. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
Ничего кроме правил. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
Связывание по умолчанию . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
Неявное связывание. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
Явное связывание. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
Связывание new. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
Все по порядку . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
Определение this . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
Исключения связывания. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .167
Игнорирование this. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
Косвенные ссылки. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .170
Мягкое связывание. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
Лексическое поведение this. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
Итоги. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176

Глава 8. Объекты . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
Синтаксис . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
Тип. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
Встроенные объекты. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179
Содержимое. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182
Вычисление имен свойств. . . . . . . . . . . . . . . . . . . . . . . . . . . . 184
Свойства и методы. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
Массивы. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
Дублирование объектов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189
Дескрипторы свойств . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192
Неизменяемость. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197
[[Get]] . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200
[[Put]]. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202
Геттеры и сеттеры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203
Существование. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206

Оглавление

9

Перебор. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209
Итоги. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215

Глава 9. Классы. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217
Теория классов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 218
Паттерн проектирования «класс». . . . . . . . . . . . . . . . . . . . . . 220
«Классы» JavaScript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221
Механика классов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222
Строительство . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222
Конструктор. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224
Наследование . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225
Полиморфизм. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 228
Множественное наследование. . . . . . . . . . . . . . . . . . . . . . . . . 231
Примеси . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 232
Явные примеси. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233
Неявные примеси. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241
Итоги. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 242

Глава 10. Прототипы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 244
[[Prototype]]. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 244
Object.prototype . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247
Назначение и замещение свойств. . . . . . . . . . . . . . . . . . . . . . 247
«Класс». . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251
Функции «классов». . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251
«Конструкторы». . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 256
Механика. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259
Наследование (на основе прототипов). . . . . . . . . . . . . . . . . . . . . 263
Анализ связей «классов» . . . . . . . . . . . . . . . . . . . . . . . . . . . . 268
Связи между объектами. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 273
Создание связей вызовом Create(). . . . . . . . . . . . . . . . . . . . . . 273
Связи как резерв?. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 277
Итоги. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279

10

Оглавление

Глава 11. Делегирование поведения. . . . . . . . . . . . . . . . 281
Проектирование, ориентированное на делегирование . . . . . . . . . 282
Теория классов. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283
Теория делегирования . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 285
Сравнение моделей мышления. . . . . . . . . . . . . . . . . . . . . . . . 292
Классы и объекты . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 298
«Классы» виджетов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 298
Делегирование для объектов Widget. . . . . . . . . . . . . . . . . . . . 302
Упрощение архитектуры. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .305
Расставание с классами. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .309
Более приятный синтаксис. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 312
Нелексичность . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 314
Интроспекция . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 316
Итоги. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 321

Приложение Г. Классы ES6. . . . . . . . . . . . . . . . . . . . . . . . 322
class. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 323
Проблемы class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 325
Статический > динамический?. . . . . . . . . . . . . . . . . . . . . . . . . . . 331
Итоги. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 332

Об авторе. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 333

Введение

С самых первых дней существования Всемирной паутины язык
JavaScript стал фундаментальной технологией для управления
интерактивностью контента, потребляемого пользователями. Хотя
история JavaScript начиналась с мерцающих следов от указателя
мыши и раздражающих всплывающих подсказок, через два десятилетия технология и возможности JavaScript выросли на несколько порядков, и лишь немногие сомневаются в его важности
как ядра самой распространенной программной платформы
в мире: веб-технологий.
Но как язык JavaScript постоянно подвергался серьезной критике — отчасти из-за своего происхождения, еще больше из-за
своей философии проектирования. Даже само его название наводит на мысли, как однажды выразился Брендан Эйх (Brendan
Eich), о «недоразвитом младшем брате», который стоит рядом
со своим старшим и умным братом Java. Тем не менее такое название возникло исключительно по соображениям политики
и маркетинга. Между этими двумя языками существуют колоссальные различия. У JavaScript с Java общего не больше, чем
у луна-парка с Луной.
Так как JavaScript заимствует концепции и синтаксические идио­
мы из нескольких языков, включая процедурные корни в стиле C
и менее очевидные функциональные корни в стиле Scheme/Lisp,
он в высшей степени доступен для широкого спектра разработчиков — даже обладающих минимальным опытом программирования. Программа «Hello World» на JavaScript настолько проста, что

12

Введение

язык располагает к себе и кажется удобным с самых первых минут
знакомства.
Пожалуй, JavaScript — один из самых простых языков для изучения и начала работы, но из-за его странностей хорошее знание
этого языка встречается намного реже, чем во многих других
языках. Если для написания полноценной программы на C или
C++ требуется достаточно глубокое знание языка, полномасштабное коммерческое программирование на JavaScript порой (и достаточно часто) едва затрагивает то, на что способен этот язык.
Хитроумные концепции, глубоко интегрированные в язык, проявляются в простых на первый взгляд аспектах, например, передаче функций в форме обратных вызовов. У разработчика
JavaScript появляется соблазн просто использовать язык «как
есть» и не беспокоиться о том, что происходит «внутри».
Это одновременно простой и удобный язык, находящий повсеместное применение, и сложный, многогранный набор языковых
механик, истинный смысл которых без тщательного изучения
останется непонятным даже для самого опытного разработчика
JavaScript.
В этом заключается парадокс JavaScript; ахиллесова пята языка;
проблема, которой мы сейчас займемся. Так как JavaScript можно
использовать без полноценного понимания, очень часто понимание языка так и не приходит к разработчику.

Задача
Если каждый раз, сталкиваясь с каким-то сюрпризом или неприятностью в JavaScript, вы заносите их в «черный список» (как
многие привыкли делать), вскоре от всей мощи JavaScript у вас
останется пустая скорлупа. Хотя это подмножество принято на-

Задача

13

зывать «Хорошими Частями», я призываю вас, дорогой читатель,
рассматривать его как «Простые Части», «Безопасные Части»
и даже «Неполные Части».
Серия «Вы не знаете JS» идет в прямо противоположном направлении: изучить и глубоко понять весь язык JavaScript, и особенно
«Сложные Части».
Здесь мы прямо говорим о существующей среди разработчиков JS
тенденции изучать «ровно столько, сколько нужно» для работы,
не заставляя себя разбираться в том, что именно происходит и почему язык работает именно так. Более того, мы воздерживаемся
от распространенной тактики отступить, когда двигаться дальше
становится слишком трудно.
Я не привык останавливаться в тот момент, когда что-то просто
работает, а я толком сам не знаю почему, — и вам не советую. Приглашаю вас на путешествие по этим неровным, непростым дорогам;
здесь вы узнаете, что собой представляет язык JavaScript и что он
может сделать. С этими знаниями ни один метод, ни один фреймворк, ни одно модное сокращение или термин недели не будут за
пределами вашего понимания.
В каждой из книг серии мы возьмем одну из конкретных базовых
частей языка, которые часто понимаются неправильно или недостаточно глубоко, и рассмотрим ее очень глубоко и подробно.
После чтения у вас должна сформироваться твердая уверенность
в том, что вы понимаете не только теорию, но и практические
аспекты «того, что нужно знать для работы».
Скорее всего, то, что вы сейчас знаете о JavaScript, вы узнавали
по частям от других людей, которые тоже недостаточно хорошо
разбирались в теме. Такой JavaScript — не более чем тень настоящего языка. На самом деле вы пока JavaScript не знаете, но будете знать, если как следует ознакомитесь с этой серией книг. Читайте дальше, друзья мои. JavaScript ждет вас.

14

Введение

Благодарности
Я должен поблагодарить многих людей, без которых эта книга
и вся серия не появились бы на свет.
Прежде всего, я должен поблагодарить свою жену Кристен Симпсон (Christen Simpson) и своих детей Итана и Эмили — они мирились с тем, что папа подолгу сидит за компьютером. Даже
когда я не пишу книги, мое увлечение JavaScript приковывает
меня к экрану намного чаще, чем следовало бы. Впрочем, именно
благодаря времени, позаимствованному у моей семьи, эти книги
так глубоко и полно объясняют JavaScript вам, читателю. Я обязан
моей семье всем.
Хочу поблагодарить своих редакторов из O’Reilly, а именно Симона Сен-Лорана (Simon St.Laurent) и Брайана Макдональда
(Brian MacDonald), а также весь остальной коллектив издательства и специалистов по маркетингу. Работать с ними было невероятно интересно, и они особенно доброжелательно отнеслись
к эксперименту с написанием, редактированием и печатью этой
книги как проекта «с открытым кодом».
Спасибо всем, кто внес свой вклад в работу над этой серией книг,
кто поделился своими предложениями и улучшениями: Шелли
Пауэрс (Shelley Powers), Тим Ферро (Tim Ferro), Эван Борден
(Evan Borden), Форрест Л. Норвелл (Forrest L Norvell), Дженнифер Дэвис (Jennifer Davis), Джесс Харлин (Jesse Harlin) и многие
другие. Спасибо Шейну Хадсону (Shane Hudson) за отличное
введение.
Спасибо бесчисленным участникам сообщества, включая представителей комитета TC39. Они делились с нами своими знаниями, терпеливо и подробно отвечали на мои бесчисленные вопросы: Джон-Дэвид Далтон (John-David Dalton), Юрий «kangax»
Зайцев (Juriy Zaytsev), Матиас Байненс (Mathias Bynens), Рик

Благодарности

15

Уолдрон (Rick Waldron), Аксель Раушмайер (Axel Rauschmayer),
Николас Закас (Nicholas Zakas), Энгус Кролл (Angus Croll), Джордан Харбанд (Jordan Harband), Дэйв Херман (Dave Herman),
Брендан Эйх (Brendan Eich), Аллен Вирфс-Брок (Allen WirfsBrock), Брэдли Мек (Bradley Meck), Доменик Деникола (Domenic
Denicola), Дэвид Уолш (David Walsh), Тим Дисней (Tim Disney),
Крис Ковал (Kris Kowal), Петер ван дер Зее (Peter van der Zee),
Андреа Джиаммарчи (Andrea Giammarchi), Кит Кэмбридж (Kit
Cambridge)… и так много других, что я не затронул даже вершину
айсберга.
Так как серия книг «Вы не знаете JS» родилась на Kickstarter,
я также хочу поблагодарить всех моих (почти) 500 щедрых бэкеров, без которых эта серия не могла состояться. Вот они: Ян Шпила (Jan Szpila), nokiko, Мурали Кришнамурти (Murali
Krishnamoorthy), Райан Джой (Ryan Joy), Крейг Пэтчетт (Craig
Patchett), pdqtrader, Дэйл Фуками (Dale Fukami), Рэй Хэтфилд
(ray hatfield), Родриго Перес (R0drigo Perez) [Mx], Дэн Петитт
(Dan Petitt), Джек Фрэнклин (Jack Franklin), Эндрю Берри
(Andrew Berry), Брайан Гринстед (Brian Grinstead), Роб Сазерленд
(Rob Sutherland), Сержи Месегер (Sergi Meseguer), Филип Гурли
(Phillip Gourley), Марк Уотсон (Mark Watson), Джефф Карут (Jeff
Carouth), Альфредо Сумаран (Alfredo Sumaran), Мартин Сакс
(Martin Sachse), Марсио Барриос (Marcio Barrios), Дэн (Dan),
AimelyneM, Мэтт Салливан (Matt Sullivan), Делнатт Пьер-Антуан
(Delnatte Pierre-Antoine), Джейк Смит (Jake Smith), Юджин Тудорансеа (Eugen Tudorancea), Айрис (Iris), Дэвид Трин (David
Trinh), simonstl, Рэй Дэли (Ray Daly), Урос Грубер (Uros Gruber),
Джастин Майерс (Justin Myers), Шай Зонис (Shai Zonis), Mom &
Dad, Дэвин Кларк (Devin Clark), Дэннис Палмер (Dennis Palmer),
Брайан Панахи Джонсон (Brian Panahi Johnson), Джош Маршалл
(Josh Marshall), Маршалл (Marshall), Дэннис Керр (Dennis Kerr),
Мэтт Стил (Matt Steele), Эрик Слагтер (Erik Slagter), Sacah, Джастин Рэйнбоу (Justin Rainbow), Кристиан Нилссон (Christian

16

Введение

Nilsson), Делапуи (Delapouite), Д. Перейра (D. Pereira), Николас
Хойзи (Nicolas Hoizey), Джордж В. Рейли (George V. Reilly), Дэн
Ривс (Dan Reeves), Бруно Латернер (Bruno Laturner), Чед Дженнингс (Chad Jennings), Шейн Кинг (Shane King), Джереми Ли
Кохик (Jeremiah Lee Cohick), od3n, Стэн Ямейн (Stan Yamane),
Марко Вучинич (Marko Vucinic), Jim B, Стивен Коллинз (Stephen
Collins), Эгир Торстейнссон (Ægir Þorsteinsson), Эрик Педерсон
(Eric Pederson), Овейн (Owain), Нейтан Смит (Nathan Smith),
Jeanetteurphy, Александр (Alexandre) ELISÉ, Крис Петерсон (Chris
Peterson), Рик Уотсон (Rik Watson), Люк Мэтьюз (Luke Matthews),
Джастин Лоуэри (Justin Lowery), Мортен Нильсен (Morten
Nielsen), Вернон Кеснер (Vernon Kesner), Четан Шеной (Chetan
Shenoy), Пол Трегоинг (Paul Tregoing), Марк Грабански (Marc
Grabanski), Дион Альмейр (Dion Almaer), Эндрю Салливан
(Andrew Sullivan), Кейт Элзасс (Keith Elsass), Том Берк (Tom
Burke), Брайан Эшенфелтер (Brian Ashenfelter), Дэвид Стюарт
(David Stuart), Карл Сведберг (Karl Swedberg), Грэм (Graeme),
Брэндон Хейс (Brandon Hays), Джон Кристофер (John Christopher),
Gior, manoj reddy, Чед Смит (Chad Smith), Джаред Харбор (Jared
Harbour), Минору Тода (Minoru TODA), Крис Уигли (Chris
Wigley), Дэниел Ми (Daniel Mee), Майк (Mike), Handyface, Алекс
Яраус (Alex Jahraus), Карл Фурроу (Carl Furrow), Роб Фулкрод
(Rob Foulkrod), Макс Шишкин (Max Shishkin), Ли Пенни мл.
(Leigh Penny Jr.), Роберт Фергусон (Robert Ferguson), Майк ван
Хенселаар (Mike van Hoenselaar), Хасс Шугаард (Hasse
Schougaard), Раджан Венкатагуру (rajan venkataguru), Джефф
Адамс (Jeff Adams), Трей Роббинс (Trae Robbins), Рольф Лангенхузен (Rolf Langenhuijzen), Хорхе Антунес (Jorge Antunes), Алекс
Колосков (Alex Koloskov), Хью Гриниш (Hugh Greenish), Тим
Джонс (Tim Jones), Хосе Очоа (Jose Ochoa), Майкл Бреннан-Уайт
(Michael Brennan-White), Нага Хариш Мувва (Naga Harish
Muvva), Баркоци Давид (Barkóczi Dávid), Китт Ходсден (Kitt
Hodsden), Пол Макгроу (Paul McGraw), Саша Голдхофер (Sascha
Goldhofer), Эндрю Меткаф (Andrew Metcalf), Маркус Крог

Благодарности

17

(Markus Krogh), Майкл Мэтьюз (Michael Mathews), Мэтт Джаред
(Matt Jared), Juanfran, Джорджи Киршнер (Georgie Kirschner),
Кенни Ли (Kenny Lee), Тед Чжан (Ted Zhang), Амит Пахва (Amit
Pahwa), Инбал Синаи (Inbal Sinai), Дэн Рейн (Dan Raine), Шабсе
Лакс (Schabse Laks), Майкл Терворт (Michael Tervoort), Александр
Абро (Alexandre Abreu), Алан Джозеф Уильямс (Alan Joseph
Williams), NicolasD, Синди Вонг (Cindy Wong), Рег Брайтуэйт
(Reg Braithwaite), LocalPCGuy, Джон Фрискис (Jon Friskics), Крис
Мерриман (Chris Merriman), Джон Пена (John Pena), Джейкоб
Кац (Jacob Katz), Сью Локвуд (Sue Lockwood), Магнус Йоханссон
(Magnus Johansson), Джереми Крэпси (Jeremy Crapsey), Гжегож
Павловски (Grzegorz Pawłowski), Нико Нуззачи (nico nuzzaci),
Кристин Уилкс (Christine Wilks), Ханс Бергрен (Hans Bergren),
Чарльз Монтгомери (charles montgomery), Ариэль Фогел (Ariel
‫בבל‬-‫ ךב‬Fogel), Иван Колев (Ivan Kolev), Дэниел Кампос (Daniel
Campos), Хью Вуд (Hugh Wood), Кристиан Брэдфорд (Christian
Bradford), Фредерик Харпер (Frédéric Harper), Ионут Дан Попа
(Ionuţ Dan Popa), Джефф Тримбл (Jeff Trimble), Руперт Вуд
(Rupert Wood), Трей Каррико (Trey Carrico), Панчо Лопес (Pancho
Lopez), Джоэл Куйтен (Joël kuijten), Том А. Марра (Tom A Marra),
Джефф Джуисс (Jeff Jewiss), Джейкоб Риос (Jacob Rios), Паоло
Ди Стефано (Paolo Di Stefano), Соледад Пенадес (Soledad Penades),
Крис Гербер (Chris Gerber), Андрей Долганов (Andrey Dolganov),
Уилл Мур III (Wil Moore III), Томас Мартино (Thomas Martineau),
Карим (Kareem), Бен Туре (Ben Thouret), Уди Нир (Udi Nir),
Морган Лаупис (Morgan Laupies), Джори Карсон-Берсон (jory
carson-burson), Натан Л. Смит (Nathan L. Smith), Эрик Дэймон
Уолтерс (Eric Damon Walters), Дерри Лозано-Хойленд (Derry
Lozano-Hoyland), Джеоффри Уайзман (Geoffrey Wiseman),
mkeehner, KatieK, Скотт Макфарлейн (Scott MacFarlane), Брайан
Лашомб (Brian LaShomb), Адриен Мас (Adrien Mas), Кристофер
Росс (christopher ross), Иэн Литтман (Ian Littman), Дэн Аткинсон
(Dan Atkinson), Эллиот Джоуб (Elliot Jobe), Ник Дозье (Nick
Dozier), Питер Вули (Peter Wooley), Джон Гувер (John Hoover),

18

Введение

dan, Мартин Э. Джексон (Martin A. Jackson), Гектор Фернандо
Хуртадо (Héctor Fernando Hurtado), Энди Эннаморато (andy
ennamorato), Пол Селтман (Paul Seltmann), Мелисса Гор (Melissa
Gore), Дэйв Поллард (Dave Pollard), Джек Смит (Jack Smith),
Филип Да Силва (Philip Da Silva), Гай Израэли (Guy Israeli), @
megalithic, Дэмиан Кроуфорд (Damian Crawford), Феликс Глише
(Felix Gliesche), Эйприл Картер Грант (April Carter Grant), Хайди
(Heidi), Джим Тирни (jim tierney), Андреа Джиаммарчи (Andrea
Giammarchi), Нико Виньола (Nico Vignola), Дон Джонс (Don
Jones), Крис Хартьес (Chris Hartjes), Алекс Хоуз (Alex Howes),
Джон Гиббон (john gibbon), Дэвид Дж. Грум (David J. Groom),
BBox, Ю Дилис Сун (Yu Dilys Sun), Нэйт Стейнер (Nate Steiner),
Брэндон Сатром (Brandon Satrom), Брайан Уайант (Brian Wyant),
Уэсли Хейлз (Wesley Hales), Иэн Паунси (Ian Pouncey), Тимоти
Кевин Оксли (Timothy Kevin Oxley), Джордж Терезакис (George
Terezakis), Санджай Радж (sanjay raj), Джордан Харбанд (Jordan
Harband), Марко Маклайон (Marko McLion), Вольфганг Кауфман
(Wolfgang Kaufmann), Паскаль Пеккерт (Pascal Peuckert), Дэйв
Нагент (Dave Nugent), Маркус Либелт (Markus Liebelt), Уэллинг
Гусман (Welling Guzman), Ник Кули (Nick Cooley), Дэниел Мескита (Daniel Mesquita), Роберт Сайварт (Robert Syvarth), Крис
Койе (Chris Coyier), Реми Бах (Rémy Bach), Адам Дугал (Adam
Dougal), Алистер Даггин (Alistair Duggin), Дэвид Лойдолт (David
Loidolt), Эд Ричер (Ed Richer), Брайан Шено (Brian Chenault),
GoldFire Studios, Карлес Андре (Carles Andrés), Карлос Кабо
(Carlos Cabo), Юя Сайто (Yuya Saito), Роберто Рикардо (roberto
ricardo), Барнетт Клейн (Barnett Klane), Майк Мур (Mike Moore),
Кевин Маркс (Kevin Marx), Джастин Лав (Justin Love), Джо Тейлор (Joe Taylor), Пол Дижу (Paul Dijou), Майкл Колер (Michael
Kohler), Роб Кэсси (Rob Cassie), Майк Тирни (Mike Tierney), Коди
Лерой Линдли (Cody Leroy Lindley), tofuji, Шимон Шварц (Shimon
Schwartz), Рэймонд (Raymond), Люк Де Бруве (Luc De Brouwer),
Дэвид Хейс (David Hayes), Рис Бретт-Боуэн (Rhys Brett-Bowen),
Дмитрий (Dmitry), Азиз Хури (Aziz Khoury), Дин (Dean), Скотт

Благодарности

19

Толински (Scott Tolinski Level Up), Клеман Буари (Clement
Boirie), Джордже Лукич (Djordje Lukic), Антон Котенко (Anton
Kotenko), Рафаэль Коррал (Rafael Corral), Филипп Гурвиц (Philip
Hurwitz), Джонатан Пиджин (Jonathan Pidgeon), Джейсон Кэмпбелл (Jason Campbell), Джозеф С. (Joseph C.), SwiftOne, Иэн
Хонер (Jan Hohner), Дерик Бэйли (Derick Bailey), getify, Дэниел
Кузино (Daniel Cousineau), Крис Чарлтон (Chris Charlton), Эрик
Тернер (Eric Turner), Дэвид Тернер (David Turner), Джоэл Галеран
(Joël Galeran), Dharma Vagabond, Адам (adam), Дирк ван Берген
(Dirk van Bergen), dave ♥♫★ furf, Ведран Закани (Vedran Zakanj),
Райан Макаллен (Ryan McAllen), Натали Пэтрис Такер (Natalie
Patrice Tucker), Эрик Дж. Бивона (Eric J. Bivona), Адам Спунер
(Adam Spooner), Аарон Кавано (Aaron Cavano), Келли Пэкер
(Kelly Packer), Эрик Джней (Eric J), Мартин Дреновак (Martin
Drenovac), Эмилис (Emilis), Майкл Пеликан (Michael Pelikan),
Скотт Ф. Уолтер (Scott F. Walter), Джош Фримен (Josh Freeman),
Брэндон Хадженс (Brandon Hudgeons), Виджай Ченнупати (vijay
chennupati), Билл Гленнон (Bill Glennon), Робин Р. (Robin R.),
Трой Форстер (Troy Forster), otaku_coder, Брэд (Brad), Скотт
(Scott), Фредерик Острандер (Frederick Ostrander), Адам Брилл
(Adam Brill), Себ Флиппенс (Seb Flippence), Майкл Андерсон
(Michael Anderson), Джейкоб (Jacob), Адам Рэндлетт (Adam
Randlett), Standard, Джошуа Клэнтон (Joshua Clanton), Себастиан Куба (Sebastian Kouba), Крис Дек (Chris Deck), SwordFire,
Ханнес Папенберг (Hannes Papenberg), Ричард Вобер (Richard
Woeber), hnzz, Роб Кроутер (Rob Crowther), Джедидайя Бродбент
(Jedidiah Broadbent), Сергей Чернышев (Sergey Chernyshev),
Джей-Эр Хамон (Jay-Ar Jamon), Бен Комби (Ben Combee), Лучиано Боначела (luciano bonachela), Марк Томлинсон (Mark
Tomlinson), Кит Кэмбридж (Kit Cambridge), Майкл Мелгарес
(Michael Melgares), Джейкоб Адамс (Jacob Adams), Адриан Бруинхаут (Adrian Bruinhout), Бев Вибер (Bev Wieber), Скотт Пулео
(Scott Puleo), Томас Херцог (Thomas Herzog), Эйприл Леоне (April
Leone), Дэниел Мизилински (Daniel Mizieliński), Кеес ван Гинкел

20

Введение

(Kees van Ginkel), Джон Абрамс (Jon Abrams), Эрвин Хайзер
(Erwin Heiser), Ави Лавиад (Avi Laviad), Дэвид Ньюэлл (David
Newell), Жан-Франсуа Тюрко (Jean-Francois Turcot), Нико Робертс (Niko Roberts), Эрик Дана (Erik Dana), Чарльз Нилл (Charles
Neill), Аарон Холмс (Aaron Holmes), Гжегож Циолковски (Grzegorz
Ziółkowski), Нейтан Янгман (Nathan Youngman), Тимоти (Timothy),
Джейкоб Мазер (Jacob Mather), Майкл Аллен (Michael Allan),
Мохит Сет (Mohit Seth), Райан Эвинг (Ryan Ewing), Бенджамин
Ван Тризе (Benjamin Van Treese), Марсело Сантос (Marcelo
Santos), Денис Вольф (Denis Wolf), Фил Киз (Phil Keys), Крис
Юнг (Chris Yung), Тимо Тийхоф (Timo Tijhof), Мартин Леквалл
(Martin Lekvall), Agendine, Грег Уитворт (Greg Whitworth), Хелен
Хамфри (Helen Humphrey), Дугал Кэмпбелл (Dougal Campbell),
Йоханнес Харт (Johannes Harth), Бруно Гирин (Bruno Girin),
Брайан Хоу (Brian Hough), Даррен Ньютон (Darren Newton),
Крейг Макфит (Craig McPheat), Оливье Тилл (Olivier Tille),
Деннис Ротиг (Dennis Roethig), Матиас Байненс (Mathias Bynens),
Брендан Стромбергер (Brendan Stromberger), sundeep, Джон
Мейер (John Meyer), Рон Мэйл (Ron Male), Джон Ф. Кростон III
(John F Croston III), gigante, Карл Бергенхэм (Carl Bergenhem),
Б. Дж. Мэй (B.J. May), Ребека Тайлер (Rebekah Tyler), Тед Фоксберри (Ted Foxberry), Джордан Риз (Jordan Reese), Терри Сьютор
(Terry Suitor), afeliz, Том Кифер (Tom Kiefer), Даррах Даффи
(Darragh Duffy), Кевин Вандербекен (Kevin Vanderbeken), Энди
Пирсон (Andy Pearson), Саймон Макдональд (Simon Mac Donald),
Абид Дин (Abid Din), Крис Джоэл (Chris Joel), Томас Тониссен
(Tomas Theunissen), Дэвид Дик (David Dick), Пол Грок (Paul
Grock), Брэндон Вуд (Brandon Wood), Джон Вайс (John Weis),
dgrebb, Ник Дженкинс (Nick Jenkins), Чак Лейн (Chuck Lane),
Джонни Мегахан (Johnny Megahan), marzsman, Тату Тамминен
(Tatu Tamminen), Джеоффри Кнаут (Geoffrey Knauth), Александр
Тармолов (Alexander Tarmolov), Джереми Таймс (Jeremy Tymes),
Чед Олд (Chad Auld), Шон Пармели (Sean Parmelee), Роб Стенке
(Rob Staenke), Дэн Бендер (Dan Bender), Янник Дерва (Yannick

Благодарности

21

Derwa), Джошуа Джонс (Joshua Jones), Герт Плейзир (Geert
Plaisier), Том Лезотт (Tom LeZotte), Кристен Симпсон (Christen
Simpson), Стефан Брувик (Stefan Bruvik), Джастин Фальконе
(Justin Falcone), Карлос Сантана (Carlos Santana), Майкл Вайс
(Michael Weiss), Пабло Виллослада (Pablo Villoslada), Петер деХаан (Peter deHaan), Димитрис Илиопулос (Dimitris Iliopoulos),
seyDoggy, Адам Джорденс (Adam Jordens), Ной Кантровиц (Noah
Kantrowitz), Эмол М. (Amol M), Мэтью Уиннард (Matthew
Winnard), Дирк Гинейдер (Dirk Ginader), Финэм Буи (Phinam
Bui), Дэвид Рэпсон (David Rapson), Эндрю Бакстер (Andrew
Baxter), Флориан Бугел (Florian Bougel), Майкл Джордж (Michael
George), Альбан Эскалье (Alban Escalier), Дэниел Селлерс (Daniel
Sellers), Саша Рудан (Sasha Rudan), Джон Грин (John Green), Роберт
Ковальски (Robert Kowalski), Дэвид И. Тезейра (David I. Teixeira),
@ditma, Чарльз Карпентер (Charles Carpenter), Джастин Йост
(Justin Yost), Сэм С. (Sam S), Денис Чиккале (Denis Ciccale), Кевин
Шорс (Kevin Sheurs), Янник Круассан (Yannick Croissant), По
Фрейсес (Pau Fracés), Стивен Макгоуэн (Stephen McGowan), Шон
Сирси (Shawn Searcy), Крис Раппел (Chris Ruppel), Кевин Лэмпинг
(Kevin Lamping), Джессика Кэмпбелл (Jessica Campbell), Кристофер
Шмитт (Christopher Schmitt), Sablons, Джонатан Рейсдорф
(Jonathan Reisdorf), Банни Гек (Bunni Gek), Тедди Хафф (Teddy
Huff), Майкл Маллани (Michael Mullany), Майкл Фюрстенберг
(Michael Fürstenberg), Карл Хендерсон (Carl Henderson), Рик Йостинг (Rick Yoesting), Скотт Николс (Scott Nichols), Эрнан Сьюдад
(Hernán Ciudad), Эндрю Майер (Andrew Maier), Майк Стапп (Mike
Stapp), Джесси Шоул (Jesse Shawl), Серхио Лопес (Sérgio Lopes),
jsulak, Шон Прайс (Shawn Price), Джоэл Клермон (Joel Clermont),
Крис Ридманн (Chris Ridmann), Шон Тимм (Sean Timm), Джейсон
Финч (Jason Finch), Эйден Монтгомери (Aiden Montgomery),
Элайджа Мэнор (Elijah Manor), Дерек Гатрайт (Derek Gathright),
Джесси Харлин (Jesse Harlin), Диллон Карри (Dillon Curry), Кортни Майерс (Courtney Myers), Диего Каденас (Diego Cadenas), Арн
де Бри (Arne de Bree), Жоао Пауло Дуба (João Paulo Dubas), Джеймс

22

Введение

Тейлор (James Taylor), Филипп Креутли (Philipp Kraeutli), Михай
Паун (Mihai Păun), Сэм Гарегозлу (Sam Gharegozlou), joshjs, Мэтт
Мерчисон (Matt Murchison), Эрик Уиндэм (Eric Windham), Тимо
Берманн (Timo Behrmann), Эндрю Холл (Andrew Hall), Джошуа
Прайс (joshua price) и Теофил Виллар (Théophile Villard).
Эта книга писалась на условиях открытого доступа к информации,
включая редактирование и печать. Мы многим обязаны репозиторию Github, благодаря которому перед сообществом открылись
такие возможности!
Спасибо всем бесчисленным людям, которых я не назвал, но которые заслуживают благодарности. Пусть эта книжная серия
«принадлежит» всем нам, и пусть она внесет свой вклад в популя­
ризацию и понимание языка JavaScript к пользе всех нынешних
и будущих участников сообщества.

О книге
JavaScript — замечательный язык. Его легко изучать частично
и намного сложнее изучать полностью (или хотя бы в достаточной
мере). Когда разработчики сталкиваются с трудностями, они
обычно винят в этом язык вместо своего ограниченного понимания. В этих книгах я постараюсь исправить такое положение дел
и помогу оценить по достоинству язык, который вы можете
(и должны) знать достаточно глубоко.
Многие примеры, приведенные в книге, рассчитаны на современные (и обращенные в будущее) среды JavaScript, такие как
ES6. При запуске в более старых версиях движка (предшествующих ES6) поведение некоторых примеров может отличаться
от описанного в тексте.

Типографские соглашения

23

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

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

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

Так выделяются советы и предложения.

Так обозначаются советы, предложения и примечания общего
характера.

Так обозначаются предупреждения и предостережения.

24

Введение

Использование программного
кода примеров
Вспомогательные материалы (примеры кода, упражнения и т. д.)
доступны для загрузки по адресу http://bit.ly/1c8HEWF и http://bit.ly/
ydkjs-this-code.
Эта книга призвана оказать вам помощь в решении ваших задач.
В общем случае все примеры кода из этой книги вы можете использовать в своих программах и в документации. Вам не нужно
обращаться в издательство за разрешением, если вы не собираетесь
воспроизводить существенные части программного кода. Например, если вы разрабатываете программу и используете в ней несколько отрывков программного кода из книги, вам не нужно
обращаться за разрешением. Однако в случае продажи или распространения компакт-дисков с примерами из этой книги вам
необходимо получить разрешение от издательства O’Reilly. Если
вы отвечаете на вопросы, цитируя данную книгу или примеры из
нее, получение разрешения не требуется. Но при включении существенных объемов программного кода примеров из этой книги
в вашу документацию вам необходимо будет получить разрешение
издательства.

От издательства
Ваши замечания, предложения, вопросы отправляйте по адресу
comp@piter.com (издательство «Питер», компьютерная редакция).
Мы будем рады узнать ваше мнение!
На веб-сайте издательства www.piter.com вы найдете подробную
информацию о наших книгах.

ЧА С Т Ь 1

Область видимости
и замыкания

Предисловие

В детстве мне нравилось разбирать и собирать всякие устройства:
старые мобильные телефоны, стереосистемы и вообще все, до чего
я мог добраться. Я был еще слишком мал, чтобы нормально пользоваться гаджетами, но каждый раз, когда что-то ломалось, я сразу же спрашивал у родителей, можно ли мне разобраться, как оно
работает.
Помню, однажды я рассматривал печатную плату старого радиоприемника. На ней была какая-то странная длинная трубка, обмотанная медным проводом. Я не мог понять ее назначение и тут
же перешел в режим исследования. Что она делает? Зачем она
в приемнике? Она не похожа на другие части платы, почему? А для
чего ее обмотали медным проводом? Что будет, если снять провод?
Теперь я знаю, что это была рамочная антенна, которая строится
наматыванием медного провода на ферритовый сердечник и которая часто используется в транзисторных приемниках.
А вам интересно пытаться искать ответы на все «почему»? Большинству детей это интересно. Пожалуй, мне это больше всего
нравится в детях — желание узнавать новое.
К сожалению, теперь я считаюсь профессионалом и провожу
будни за созданием новых вещей. Когда я был молод, мне нравилась представлять, что когда-нибудь я буду создавать те вещи,
которые разбираю сегодня. Конечно, большинство того, что я создаю сегодня, представляет собой код JavaScript, а не ферритовые
сердечники… но не так уж далеко код от них ушел! Но хотя мне
когда-то нравилась идея создания, сейчас мне часто не хватает

Предисловие

27

желания разбирать и разбираться. Конечно, я часто выясняю
лучший способ решения задачи или исправления ошибок, но
редко трачу время на то, чтобы ставить под сомнение свои инструменты.
Именно по этой причине мне так интересна серия книг «Вы не
знаете JS». Это правильно. Я не знаю JS. Я использую JavaScript
в своей повседневной работе, занимаюсь этим уже много лет, но
действительно ли понимаю этот язык? Нет. Конечно, я понимаю
многие его стороны, часто читаю спецификации и рассылки, но
нет — я не понимаю его в той мере, как того желал бы мой внутренний двойник в шестилетнем возрасте.
Книга, которую вы держите в руках, — прекрасное начало серии.
Она ориентирована на таких людей, как я (и будем надеяться,
и вы). Она не учит вас JavaScript с нуля. Она показывает, насколько мало вы знаете о внутреннем устройстве языка. Кроме того,
книга вышла в идеальный момент: стандарт ES6 постепенно приживается, а в разных браузерах успешно идет работа над его реализацией. Если вы еще не взялись за изучение новых возмож­
ностей (таких, как let и const), это издание станет отличным
введением в тему.
Итак, я надеюсь, что книга вам понравится. Но я еще сильнее надеюсь, что подход Кайла, его привычка критически осмысливать
каждый крошечный фрагмент языка, закрепится у вас в мозгу
и в общем рабочем процессе. Не используйте антенну просто так,
разберитесь в том, как и почему она работает.
Шейн Хадсон (Shane Hudson)
www.shanehudson.net

1

Что такое область
видимости?

Одна из самых фундаментальных парадигм почти всех языков
программирования — возможность хранения значений в переменных и последующего чтения или изменения этих значений.
Возможность сохранения и чтения значений из переменных — то,
что образует состояние программы.
Без этой концепции программа сможет выполнять некоторые
операции, но они будут в высшей степени ограниченными и не
особенно интересными.
Однако включение переменных в программу поднимает самый
интересный вопрос, которым мы сейчас займемся: где размещаются эти переменные? Другими словами, где они хранятся? И самое важное, как ваша программа находит их, когда в них возникнет надобность?
Эти вопросы показывают, почему так необходим четко определенный набор правил для хранения переменных в определенном
месте и их нахождения в будущем. Этот набор правил называется
областью видимости (scope).
Но где и как задаются правила области видимости?

Немного теории компиляторов

29

Немного теории компиляторов
Это может быть очевидно, а может быть и удивительно, в зависимости от вашего опыта работы с разными языками, но несмотря
на тот факт, что JavaScript относится к общей категории «динамических» или «интерпретируемых» языков, на самом деле это
компилируемый язык. Код не компилируется заранее, как во
многих традиционных компилируемых языках, а результаты
компиляции не портируются между разными распределенными
системами. Тем не менее движок JavaScript выполняет многие те
же действия, что и любой традиционный компилятор (хотя и на
более сложном уровне).
В традиционном процессе компиляции блок исходного кода —
ваша программа — перед выполнением обычно проходит через три
фазы обработки, которые приближенно объединяются термином
«компиляция»:
 Лексический анализ/Разбиение на токены (Tokenizing/Lexing) —

разбиение последовательности символов на осмысленные
(с точки зрения языка) фрагменты, называемые токенами. Для
примера возьмем программу var a = 2;. Скорее всего, эта программа будет разбита на следующие токены: var, a, =, 2 и ;.
Пропуски могут сохраняться в виде токенов, а могут и не сохраняться в зависимости от того, имеет это смысл или нет.
Разница между tokenizing и lexing — вопрос достаточно тонкий
и теоретический. Важно то, будет ли происходить идентификация токеном с состоянием или без. Проще говоря, если при
вызове токенизатора активизируются правила разбора с состоянием, определяющие, должен ли данный токен считаться
отдельным токеном или частью другого токена, это будет
называться lexing.
 Разбор (parsing) — преобразование потока (массива) токенов

в дерево вложенных элементов, которые в совокупности пред-

30

Глава 1. Что такое область видимости?

ставляют грамматическую структуру программы. Это дерево
называется «абстрактным деревом синтаксиса», или AST
(Abstract Syntax Tree). Скажем, дерево для var a = 2; может
начинаться с узла верхнего уровня VariableDeclaration, который содержит дочерний узел Identifier (со значением a )
и другой дочерний узел AssignmentExpression, у которого есть
свой дочерний узел с именем NumericLiteral (его значение
равно 2).
 Генерирование кода — процесс преобразования AST в исполня-

емый код. Эта часть сильно зависит от языка, целевой платформы и т. д.
Итак, вместо того чтобы вязнуть в подробностях, я просто скажу,
что описанное ранее AST-дерево для var a = 2; может быть преобразовано в набор машинных команд для создания переменной
с именем a (включая резервирование памяти и т. д.) и последующего сохранения значения в a.
Подробности того, как движок управляет системными ресурсами, выходят за рамки нашей темы.Поэтому будем просто
считать, что движок способен создавать и сохранять переменные по мере необходимости.

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

Разбираемся в областях видимости

31

для оптимизации, потому что компиляция JavaScript не выполняется в фазе построения заранее, как в других языках.
Для JavaScript компиляция во многих случаях выполняется за
считаные микросекунды (и менее) до выполнения кода. Для обеспечения максимального быстродействия движка JS применяют
всевозможные хитрости (например, JIT-компиляцию с отложенной
компиляцией и даже оперативной перекомпиляцией и т. д.), которые
выходят далеко за рамки «области видимости» нашего обсуждения.
Простоты ради будем считать, что любой фрагмент JavaScript должен компилироваться перед (обычно непосредственно перед) его
выполнением. Итак, компилятор JS берет программу var a = 2;,
сначала компилирует ее, а потом готовит ее к исполнению (обычно
это происходит немедленно).

Разбираемся в областях видимости
В своем изучении области видимости мы будем рассматривать
процесс как некое подобие разговора. Но кто с кем ведет этот разговор?

Участники
Сейчас мы познакомимся поближе с участниками, совместными
усилиями обрабатывающими программу var a = 2;. Это поможет
вам понять смысл их диалога, к которому мы вскоре прислушаемся:
 Движок — отвечает за всю компиляцию от начала до конца

и выполнение программы JavaScript.
 Компилятор — один из друзей Движка; берет на себя всю чер-

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

32

Глава 1. Что такое область видимости?

 Область видимости — еще один друг Движка; собирает и ведет

список всех объявленных идентификаторов (переменных)
и устанавливает строгий набор правил их доступности для кода,
выполняемого в данный момент.
Чтобы в полной мере понять, как работает JavaScript, вы должны
начать думать как Движок (и его друзья), задавать себе те же вопросы, что и они, и давать на эти вопросы те же ответы.

Туда и обратно
Когда вы видите программу var a = 2;, скорее всего, вы считаете,
что она состоит из одной команды. Но с точки зрения Движка дело
обстоит иначе. Движок видит здесь две разные команды: одну
Компилятор обрабатывает во время компиляции, а другую Движок обрабатывает во время выполнения. Итак, разобьем на этапы
процесс обработки программы var a = 2; Движком и другими
компонентами.
Прежде всего, Компилятор проводит лексический анализ и разбирает программу на токены, которые затем разбираются в дерево. Но когда Компилятор добирается до генерирования кода, он
рассматривает эту программу немного не так, как вы, возможно,
ожидали.
Разумно было предположить, что Компилятор генерирует код,
который можно было бы описать следующим псевдокодом: «Выделить память для переменной, присвоить ей метку a и сохранить
в этой переменной значение 2». К сожалению, такое описание не
совсем точно.
Вместо этого Компилятор действует так:
1. Обнаруживая var a, Компилятор обращается к Области видимости, чтобы узнать, существует ли переменная a в наборе этой
конкретной Области видимости. Если переменная существует,

Немного терминологии

33

то Компилятор игнорирует объявление и двигается дальше.
В противном случае Компилятор обращается к Области видимости для объявления новой переменной с именем a в наборе
этой области видимости.
2. Компилятор генерирует код для последующего выполнения
Движком для обработки присваивания a = 2. Код, выполняемый Движком, сначала спрашивает у Области видимости,
доступна ли переменная с именем a в наборе текущей области
видимости. Если переменная доступна, то Движок использует
эту переменную. Если нет, Движок ищет в другом месте (см.
«Вложенная область видимости», с. 38).
Если Движок в конечном итоге находит переменную, он присваи­
вает ей значение 2. Если нет, Движок поднимает тревогу и сообщает об ошибке.
Подведем итог: для присваивания значения переменной выполняются два разных действия. Сначала Компилятор объявляет
переменную (если она не была объявлена ранее) в текущей Области видимости, а затем при выполнении Движок ищет переменную в Области видимости, и если переменная будет найдена —
присваивает ей значение.

Немного терминологии
Чтобы вы поняли суть происходящего далее, нам понадобятся
некоторые специальные термины.
Когда Движок выполняет код, сгенерированный Компилятором
на шаге 2, он должен провести поиск переменной a и определить,
была ли она объявлена; этот поиск называется проверкой Области
видимости. Однако тип проверки, выполняемой Движком, влияет на результат поиска.

34

Глава 1. Что такое область видимости?

В нашем примере Движок будет выполнять LHS-поиск переменной a. Другая разновидность поиска называется RHS. Сокращения
озна­чают «LeftHand Side» (левосторонний) и «RightHand Side»
(правосторонний).
Левосторонний, правосторонний… по отношению к чему? К операции присваивания.
Иначе говоря, LHS-поиск выполняется при нахождении переменной
в левой части операции присваивания, а RHS-поиск выполняется
при нахождении переменной в правой части операции присваивания.
На самом деле стоит немного уточнить. Для наших целей RHSпоиск неотличим от простого поиска значения некоторой переменной, тогда как LHS-поиск пытается найти саму переменнуюконтейнер для присваивания. В этом отношении термин RHS на
самом деле означает не «правую сторону присваивания» как таковую, а скорее «не левую сторону». В несколько упрощенном виде
можно считать, что RHS означает «получить исходное значение».
А теперь разберемся подробнее.
В следующей команде:
console.log( a );

ссылка на a является RHS-ссылкой, потому что a здесь ничего не
присваивается. Вместо этого мы собираемся прочитать значение a,
чтобы значение могло быть передано console.log(..).
Сравните:
a = 2;

Эта ссылка является LHS-ссылкой, потому что текущее значение
переменной нас не интересует. Мы просто хотим найти переменную, которая могла бы послужить приемником для операции
присваивания = 2.

Немного терминологии

35

«Левая/правая сторона присваивания» в обозначениях LHS
и RHS не обязательно буквально означает «левая/правая
сторона оператора присваивания =». Присваивание также
может выполняться другими способами, поэтому лучше концептуально рассматривать их как «приемник присваивания»
(LHS) и «источник присваивания» (RHS).

Возьмем следующую программу, в которой задействованы как
LHS-, так и RHS-ссылки:
function foo(a) {
console.log( a ); // 2
}
foo( 2 );

Последняя строка с вызовом функции foo(..) также требует RHSссылки на foo, которая означает «Найти значение foo и предоставить его мне». Более того, (..) означает, что значение foo должно
быть выполнено, а значит, это должна быть функция!
Здесь тоже выполняется неочевидное, но важное присваивание.
Возможно, вы упустили неявное присваивание a = 2 в этом фрагменте кода. Оно происходит при передаче значения 2 в аргументе
функции foo(..), при котором значение 2 присваивается парамет­
ру a. Чтобы (неявно) присвоить значение параметру a, выполняется LHS-поиск.
Также здесь присутствует RHS-ссылка на значение a; полученное
значение передается console.log(..). Для выполнения console.
log(..) тоже необходима ссылка. Сначала выполняется RHSпоиск объекта console, после чего поиск по свойствам определяет,
существует ли среди них метод с именем log.
Наконец, можно на концептуальном уровне представить, что при
передаче значения 2 (посредством RHS-поиска переменной a)
методу log(..) происходит LHS/RHS-взаимодействие. Внутри

36

Глава 1. Что такое область видимости?

встроенной реализации log(..) можно считать, что у нее есть параметры, с первым из которых (вероятно, с именем arg1) выполняется LHS-поиск, перед тем как ему будет присвоено значение 2.
Возникает соблазн представить объявление функции function
foo(a) {… как обычное объявление переменной с присваиванием, что-то вроде var foo и foo = function(a){…. При
таком представлении возникает столь же соблазнительная
мысль считать, что при таком объявлении функции также задействуется LHS-поиск.
Однако здесь существует неочевидное, но важное различие:
Компилятор обрабатывает объявление и определение значения
во время генерирования кода, чтобы во время выполнения
кода Движком «присваивание» значения функции foo не требовало никакой дополнительной обработки. А следовательно,
на самом деле неправильно рассматривать объявление функции как присваивание с LHS-поиском в том смысле, в котором
он здесь рассматривается.

Общение Движка с Областью
видимости
function foo(a) {
console.log( a ); // 2
}
foo( 2 );

Представим это взаимодействие (с обработкой этого фрагмента)
в виде разговора. Общение между Движком и Областью видимости будет происходить примерно так:
Движок: Эй, Область видимости, у меня есть RHS-ссылка на foo.
Знаешь, что это?
Область видимости: Ну да, знаю. Компилятор объявил foo секунду назад. Это функция. Вот, держи.

Упражнение

37

Движок: Здорово, отлично! Перехожу к выполнению foo.
Движок: Эй, Область видимости, у меня есть LHS-ссылка на а.
Знаешь, что это?
Область видимости: Ну да, знаю. Компилятор только что объявил
a как формальный параметр foo. Вот, держи.
Движок: Полезно, как всегда, Область видимости. Еще раз спасибо. А теперь пора присвоить a значение 2.
Движок: Эй, Область видимости, снова придется тебя побеспокоить. Мне нужно выполнить RHS-поиск для console. Знаешь, что
это?
Область видимости: Без проблем, Движок, целыми днями только
этим и занимаюсь. Да, я знаю, что такое console — это встроенный
объект. Вот, держи.
Движок: Прекрасно. Теперь ищу log(..). Отлично, это функция.
Движок: Область видимости, а сможешь помочь с RHS-ссылкой
на a? Вроде бы я помню что-то такое, но хочу проверить лишний
раз.
Область видимости: Верно, Движок. Та же переменная, ничего не
изменилось. Вот, держи.
Движок: Отлично. Передаю значение a, то есть 2, функции log(..).


Упражнение
Проверьте, хорошо ли вы поняли материал. Возьмите на себя роль
Движка и проведите «разговор» с Областью видимости:

38

Глава 1. Что такое область видимости?

function foo(a) {
var b = a;
return a + b;
}
var c = foo( 2 );

1. Найдите все LHS-поиски (всего 3).
2. Найдите все RHS-поиски (всего 4).

Ответы к упражнению приведены после раздела «Итоги» этой
главы.

Вложенная область видимости
Ранее было сказано, что область видимости — набор правил поиска переменных по имени идентификатора. Впрочем, обычно
приходится принимать во внимание не одну область видимости,
а несколько.
Подобно тому как блок или функция может вкладываться внутрь
другого блока или функции, области видимости могут вкладываться в другие области видимости. Итак, если переменную не
удается найти в текущей области видимости, движок обращается к следующей внешней области видимости. Это продолжается до тех пор, пока не будет найдена искомая переменная или
не будет достигнута внешняя (то есть глобальная) область видимости.
Пример:
function foo(a) {
console.log( a + b );

Метафоры

39

}
var b = 2;
foo( 2 ); // 4

RHS-ссылку для b не удается разрешить внутри функции foo, но
она может быть разрешена во внешней области видимости (в данном случае глобальной).
Итак, возвращаясь к разговору между Движком и Областью видимости, мы услышим следующее:
Движок: Эй, Область видимости, знаешь, что такое b? У меня тут
RHS-ссылка.
Область видимости: Нет, впервые слышу про такое.
Движок: Эй, Область видимости за пределами foo… Э, да ты глобальная Область видимости? Ну и ладно. Знаешь, что такое b?
У меня тут RHS-ссылка.
Область видимости: Ага, знаю. Вот, держи.
Простые правила проверки вложенных областей видимости: Движок начинает с текущей области видимости и ищет переменную
в ней. Если поиск не дает результатов, Движок поднимается на
один уровень вверх и т. д. При достижении внешней глобальной
области видимости поиск прекращается независимо от того, была
найдена переменная или нет.

Метафоры
Чтобы наглядно представить процесс разрешения вложенных
областей видимости, вообразите высокое здание (рис. 1.1).

40

Глава 1. Что такое область видимости?

Лексическая область(-и) видимости

Глобальная область видимости

Текущая область видимости

Рис. 1.1

Это здание изображает набор правил вложенных областей видимости. Первый этаж — текущая область видимости, какой бы она
ни была, а верхний этаж — глобальная область видимости.
Чтобы разрешить LHS- или RHS-ссылку, система начинает поиск
с текущего этажа. Если переменная не будет найдена, поиск поднимается на следующий этаж, ищет там, потом на следующем
и т. д. Добравшись до верхнего этажа (глобальной области видимости), вы либо находите искомое, либо не находите. В любом
случае здесь придется остановиться.

Ошибки

41

Ошибки
Почему важно отличать LHS от RHS?
Потому что эти два типа поиска по-разному ведут себя в ситуации,
когда переменная еще не была объявлена (не найдена ни в одной
из просмотренных областей видимости).
Пример:
function foo(a) {
console.log( a + b );
b = a;
}
foo( 2 );

Когда RHS-поиск для b происходит впервые, он завершается неудачей. Переменная, не найденная в области видимости, считается «необъявленной».
Если RHS-поиск не находит переменную ни в одной из вложенных областей видимости, движок выдает ошибку ReferenceError.
Важно заметить, что ошибка относится именно к типу Reference­
Error.
С другой стороны, если движок выполняет LHS-поиск и прибывает на верхний этаж (глобальная область видимости), так и не
обнаружив искомое, если программа не выполняется в строгом
режиме strict1, в глобальной области видимости создается новая
переменная с указанным именем, которая передается движку.
«Нет, такой переменной не было, но я хочу помочь и создать ее
для тебя».
1

См. описание на сайте MSDN (https://developer.mozilla.org/en-US/docs/Web/
JavaScript/Reference/Strict_mode).

42

Глава 1. Что такое область видимости?

Режим strict, добавленный в ES5, во многих отношениях отличается от обычного/нестрогого режима. Одно из отличий заключается в том, что он запрещает автоматическое/неявное создание
глобальных переменных. В этом случае LHS-поиск не вернет
переменную с глобальной областью видимости, и движок выдаст
ошибку ReferenceError по аналогии со случаем RHS.
Если переменная будет найдена для RHS-поиска, но вы пытаетесь
сделать с ее значением нечто невозможное (например, попытка
выполнить как функцию значение, которое функцией не является, или обращение к свойству для значения null или undefined),
движок выдаст другую разновидность ошибки — TypeError.
Ошибка ReferenceError относится к проблемам при разрешении
области видимости, а ошибка TypeError подразумевает, что разрешение области видимости прошло успешно, но была сделана
попытка выполнить с результатом недопустимую/невозможную
операцию.

Итоги
Область видимости — набор правил, определяющих, где и как
осуществляется поиск переменной (идентификатора). Поиск
может выполняться для цели присваивания переменной (LHS,
или левосторонняя ссылка) или же для цели чтения ее значения
(RHS, или правосторонняя ссылка).
LHS-ссылки появляются в результате операций присваивания.
Присваивания, связанные с областью видимости, могут происходить либо в операторе =, либо при передаче аргументов параметрам функции.
Движок JavaScript сначала компилирует код перед выполнением.
При этом команды вида var a = 2; разбиваются на две части:

Ответ на упражнение

43

1. Сначала var a для объявления переменной в области видимости. Этот шаг выполняется перед выполнением кода.
2. Потом a = 2 для поиска переменной (LHS-ссылка) и присваивания ей, если переменная будет успешно найдена.
Поиск по LHS- и RHS-ссылкам начинается с текущей области
видимости. При необходимости (то есть если искомый идентификатор не будет найден) поиск поднимается вверх от вложенной
области видимости, по одной области видимости (этажу) за раз,
пока не доберется до глобальной области видимости (верхнего
этажа). Здесь поиск останавливается: либо идентификатор найден,
либо нет.
Для неразрешенных RHS-ссылок выдается исключение Referen­
ceError. Для неразрешенных LHS-ссылок автоматически создается глобальная переменная с заданным именем (если не действует режим strict ), или происходит ошибка ReferenceError
(в режиме strict).

Ответ на упражнение
function foo(a) {
var b = a;
return a + b;
}
var c = foo( 2 );

1. Найдите все LHS-поиски (всего 3).
c = ..;, a = 2 (неявное присваивание параметра) и b = ..

2. Найдите все RHS-поиски (всего 4).
foo(2.., = a;, a .. и .. b

2

Лексическая
область видимости

В главе 1 область видимости была определена как набор правил,
управляющих тем, как движок ищет переменную по ее идентификатору и находит ее в текущей области видимости или любой из
внешних областей видимости, в которых она содержится.
Существуют две основные модели работы области видимости.
Первая модель встречается намного чаще и используется в подавляющем большинстве языков программирования. Она называется лексической областью видимости (lexical scope); в этой
главе она будет рассмотрена подробно.
Другая модель, которая до сих пор используется в некоторых
языках (например, в сценариях Bash, некоторых режимах Perl
и т. д.), называется динамической областью видимости (dynamic
scope). Динамическая область видимости рассматривается в приложении А. Я упоминаю ее здесь только для того, чтобы сравнить
ее с лексической областью видимости — моделью, используемой
в JavaScript.

Стадия лексического анализа

45

Стадия лексического анализа
Как обсуждалось в главе 1, первая традиционная фаза работы
стандартного компилятора называется разбиением на токены (или
лексическим анализом). Напомню, что процесс лексического анализа изучает последовательность символов исходного кода и назначает семантический смысл токенам в результате разбора с учетом состояния. Именно эта концепция закладывает фундамент
для понимания того, что такое лексическая область видимости
и откуда берется это название.
Лексической областью видимости называется область видимости,
определяемая на стадии лексического анализа. Другими словами,
лексическая область видимости определяется тем, где вы разместили переменные и блоки области видимости во время написания
программы, а следовательно, (в основном) жестко фиксируется
на момент обработки вашего кода лексическим анализатором.
Вскоре вы увидите, что лексическую область видимости можно «обмануть», то есть изменить ее после обработки кода
лексическим анализатором, но делать это не рекомендуется.
Старайтесь интерпретировать лексическую область видимости
как чисто лексическую, а следовательно, полностью определяемую на стадии написания кода.

Рассмотрим следующий блок кода:
function foo(a) {
var b = a * 2;
function bar(c) {
console.log( a, b, c );
}

46

}

Глава 2. Лексическая область видимости

bar( b * 3 );

foo( 2 ); // 2, 4, 12

В этом примере существуют три области видимости. Для наглядности можно представить их как пузыри, вложенные друг в друга
(рис. 2.1).

Рис. 2.1

Пузырь 1 охватывает глобальную область видимости и содержит
всего один идентификатор: foo.
Пузырь 2 охватывает область видимости foo и содержит три идентификатора: a, bar и b.
Пузырь 3 охватывает область видимости bar и включает один
идентификатор: c.
Пузыри областей видимости определяются тем, где располагаются блоки, какой из них вложен в другой и т. д. В следующей главе
мы обсудим различные единицы области видимости, а пока будем
считать, что каждая функция создает новый пузырь области видимости.

Поиск

47

Пузырь для bar полностью содержится внутри пузыря для foo,
потому что (и только по этой причине) мы решили определить
функцию bar именно здесь.
Обратите внимание на строгую вложенность пузырей. Структура
не похожа на диаграммы Венна, в которых возможно пересечение
пузырей. Другими словами, пузырь некоторой функции не может
находиться одновременно в двух других пузырях внешней области
действия, и никакая функция не может частично находиться внутри двух других родительских функций.

Поиск
Структура и относительное размещение пузырей областей видимости полностью объясняет движку, где он должен искать идентификатор. В предыдущем фрагменте кода движок выполняет
команду console.log(..) и переходит к поиску трех переменных,
a, b и c. Поиск начинается с внутренней области видимости, то
есть области видимости функции bar(..). Здесь найти переменную
a не удается, поэтому поиск поднимается на один уровень к ближайшей области видимости, то есть области видимости foo(..).
Здесь находится переменная a, поэтому движок использует эту
переменную a. То же самое происходит с b. Но переменная c обнаруживается внутри bar(..).
Если бы переменная c присутствовала и внутри bar(..), и внутри
foo(..), команда console.log(..) нашла и использовала бы переменную из bar(..), так и не добравшись до переменной из foo(..).
Поиск по областям видимости останавливается при нахождении
первого совпадения. Одно имя идентификатора может быть задано на нескольких уровнях вложенных областей видимости, что
называется «замещением» (внутренний идентификатор «замещает» внешний идентификатор). Независимо от замещения поиск

48

Глава 2. Лексическая область видимости

области видимости всегда начинается с внутренней области видимости, выполняемой в настоящее время, и перемещается наружу/вверх до первого совпадения, где останавливается.
Глобальные переменные также автоматически являются свойствами глобального объекта (window в браузерах и т. д.). Это
означает, что на глобальную переменную можно ссылаться не
только напрямую по ее лексическому имени, но и косвенно
через свойство глобального объекта:
window.a
Этот синтаксис открывает доступ к глобальной переменной,
которая в противном случае была бы недоступна из-за замещения. Неглобальные замещенные переменные остаются недоступными.

Неважно, где вызывается функция, и даже как она вызывается —
ее лексическая область видимости определяется только тем, где
была объявлена функция.
Лексическая область видимости применяется только к полноценным идентификаторам, таким как a, b и c. Если фрагмент кода
содержит ссылку foo.bar.baz, поиск лексической области видимости будет относиться к идентификатору foo, но после того, как
переменная будет обнаружена, вступят в силу правила обращения
к свойствам объектов для разрешения имен свойств bar и baz соответственно.

Искажение лексической
области видимости
Если лексическая область видимости определяется только местом
объявления функции (которое выбирается исключительно во

Искажение лексической области видимости

49

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

eval
Функция eval(..) в JavaScript получает строковый аргумент
и интерпретирует содержимое строки так, словно это реальный
код в текущей точке программы. Иначе говоря, вы можете на программном уровне генерировать код внутри своей программы
и выполнять сгенерированный код так, словно вы сами его написали.
Если рассматривать eval(..) в этом свете, станет ясно, как eval()
позволяет изменить окружение лексической области видимости.
В строках кода, следующих за выполнением eval(..), движок не
будет «знать», что код был интерпретирован динамически, а следовательно, изменил окружение лексической области видимости.
Движок просто выполнит поиск по лексической области видимости так, как он это делает всегда.
Рассмотрим следующий код:
function foo(str, a) {
eval( str ); // изменение!
console.log( a, b );

50

Глава 2. Лексическая область видимости

}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3

Строка "var b = 3;" в точке вызова eval(..) интерпретируется
как код, который здесь был изначально. Так как в этом коде объявляется новая переменная b, он изменяет существующую лексическую область видимости foo(..). Как упоминалось ранее, этот
код фактически создает внутри foo(..) переменную b, которая
замещает переменную b, объявленную во внешней (глобальной)
области видимости.
Когда в программе происходит вызов console.log(..), он находит
a и b в области видимости foo(..) и не находит внешнюю переменную b. Таким образом, программа выводит «1, 3» вместо «1, 2»,
как должна была бы.
В этом упрощенном примере передаваемая строка «кода»
представляет собой фиксированный литерал. Но она с таким
же успехом могла строиться на программном уровне с объединением символов на основании логики программы. Функция
eval(..) обычно используется для выполнения динамически
создаваемого кода, так как динамическое вычисление статического по своей сути кода из строкового литерала не даст
никаких реальных преимуществ перед простым написанием
кода.

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

Искажение лексической области видимости

51

бы то ни было, eval(..) позволяет изменить лексическую область
видимости на стадии выполнения.
При использовании в режиме strict eval(..) работает
в своей собственной лексической области видимости. Это означает, что объявления, созданные внутри eval(..), не будут
изменять внешнюю область видимости.

function foo(str) {
"use strict";
eval( str );
console.log( a ); // ReferenceError: переменная a
// не определена
}
foo( "var a = 2" );

В JavaScript существуют и другие средства, приводящие практически к тому же результату, что и eval(..) . setTimeout(..)
и setInterval(..) могут получать строку в своем первом аргументе, содержимое которого интерпретируется как код динамически
генерируемой функции. Это старое унаследованное поведение,
которое давным-давно считается устаревшим. Не используйте
его!
Функция-конструктор new Function(..) тоже получает в своем
последнем аргументе строку кода, который преобразуется в динамически сгенерированную функцию (первый аргумент), если
он есть, содержит именованные параметры новой функции).
Синтаксис конструктора чуть безопаснее eval(..), но его следует
избегать в вашем коде.
Ситуации, требующие динамического генерирования кода в программе, встречаются крайне редко, так как снижение быстродействия почти никогда не стоит того.

52

Глава 2. Лексическая область видимости

with
Другая нежелательная (а теперь считающаяся устаревшей) возможность искажения лексической области видимости в JavaScript
основана на использовании ключевого слова with. Объяснить
происходящее можно несколькими способами, но я предпочитаю
объяснять с точки зрения того, как оно взаимодействует с лексической областью видимости и влияет на нее; обычно with объясняется как сокращенная запись для многократных обращений
к свойствам объекта без повторения ссылки на объект.
Пример:
var obj = {
a: 1,
b: 2,
c: 3
};
// "рутинные" повторения "obj"
obj.a = 2;
obj.b = 3;
obj.c = 4;
// "более простая" сокращенная запись
with (obj) {
a = 3;
b = 4;
c = 5;
}

Тем не менее этот синтаксис представляет собой нечто гораздо
большее, чем упрощенную запись для обращения к свойствам
объектов. Пример:
function foo(obj) {
with (obj) {
a = 2;
}

Искажение лексической области видимости

53

}
var o1 = {
a: 3
};
var o2 = {
b: 3
};
foo( o1 );
console.log( o1.a ); // 2
foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2 — ой, утечка глобального значения!

В этом примере создаются два объекта, o1 и o2. У одного объекта
есть свойство a, у другого его нет. Функция foo(..) получает
ссылку на объект obj в аргументе и вызывает with (obj) { .. }
для этой ссылки. В блоке with с переменной a встречается то, что
выглядит как нормальная лексическая ссылка на переменную a,
а на самом деле LHS-ссылка (см. главу 1) для присваивания ей
значения 2.
При передаче o1 присваивание a = 2 находит свойство o1.a и присваивает ему значение 2, как показывает следующая команда
console.log(o1.a). Однако при передаче o2, поскольку у этого
объекта нет свойства a, такое свойство не создается, и o2.a остается неопределенным.
И тут обнаруживается странный побочный эффект: команда
a = 2 создает глобальную переменную a. Как такое могло случиться?
Команда with получает объект (с 0 или более свойствами) и интерпретирует этот объект так, словно он образует совершенно
отдельную лексическую область видимости, а следовательно,

54

Глава 2. Лексическая область видимости

свойства объекта интерпретируются как лексически определенные
идентификаторы в этой области видимости.
Несмотря на то что блок with интерпретирует объект как
лексическую область видимости, для обычного объявления
var внутри этого блока with областью видимости будет не
этот блок, а область видимости внешней функции.

Если функция eval(..) может изменить существующую лексическую область видимости в том случае, если переданная ей
строка кода содержит одно или несколько объявлений, команда
with создает совершенно новую лексическую область видимости
на основе переданного ей объекта.
Если понимать происходящее таким образом, областью видимости, объявленной командой with при передаче o1, будет o1; в этой
области видимости существует идентификатор, соответствующий
свойству o1.a. Но когда в качестве области видимости используется o2, идентификатор a в ней не обнаруживается, поэтому вступают в действие обычные правила LHS-поиска идентификаторов
(см. главу 1).
Ни в области видимости o2, ни в области видимости foo(..), ни
даже в глобальной области видимости идентификатор a найти не
удается, поэтому при выполнении команды a = 2 автоматически
создается глобальная переменная (так как программа не выполняется в режиме strict).
Мало того что eval(..) и with использовать не рекомендуется, на них еще и влияет режим strict. Команда with запрещена полностью, тогда как различные косвенные или небезо­
пасные формы eval(..) запрещены при сохранении базовой
функциональности.

Быстродействие

55

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

Быстродействие
Как eval(..), так и with искажают лексическую область видимости,
определенную во время написания программы, посредством изменения или создания новой лексической области видимости во
время выполнения.
И что здесь такого, спросите вы? Если они предоставляют более
сложную функциональность и гибкость при программировании,
разве эти возможности не полезны? Нет.
Движок JavaScript содержит ряд оптимизаций быстродействия,
которые выполняются в фазе компиляции. Некоторые из них
сводятся к тому, что движок по сути выполняет статический анализ кода в процессе лексического анализа и заранее определяет,
где находятся все переменные и объявления функций, чтобы
ускорить разрешение идентификаторов в процессе выполнения.
Но если движок обнаруживает в коде eval(..) или with, он вынужден предположить, что вся информация о местонахождении
идентификаторов может оказаться недействительной. Ведь во
время лексического анализа движок не может знать, какой код
может быть передан eval(..) для изменения лексической области
видимости или что может содержать объект, переданный with для
создания новой лексической области видимости. Другими словами, в пессимистическом варианте многие эти оптимизации при
наличии eval(..) или with становятся бессмысленными, поэтому
движок вообще не выполняет никаких оптимизаций.

56

Глава 2. Лексическая область видимости

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

Итоги
Лексическая область видимости определяется решениями о том,
где объявляются функции, принимаемыми во время написания
программы. Фаза лексического анализа в процессе компиляции
располагает информацией о том, где и как объявлены все идентификаторы, — а следовательно, может спрогнозировать их поиск
во время выполнения.
В JavaScript существуют два механизма, которые могут «исказить»
лексическую область видимости: eval(..) и with. Первый может
изменить существующую лексическую область видимости (во
время выполнения), обрабатывая строку «кода» с одним или несколькими объявлениями. Второй фактически создает новую
лексическую область видимости (снова во время выполнения),
интерпретируя ссылку на объект как область видимости, а свойства этого объекта — как идентификаторы в области видимости.
Недостаток этих механизмов заключается в том, что они не позволяют движку выполнить оптимизации на стадии компиляции,
связанные с поиском по областям видимости, потому что движок
вынужден пессимистично считать, что такие оптимизации будут
недействительными. При использовании любой из этих возможностей программа будет работать медленнее. Не используйте их.

3

Функциональные
и блочные области
видимости

Как объяснялось в главе 2, область видимости состоит из пузырей.
Каждый пузырь представляет собой нечто вроде контейнера,
в котором объявляются идентификаторы (переменные, функции).
Пузыри вложены друг в друга, причем это вложение определяется во время написания кода.
Но что именно создает новый пузырь? Только функции? А могут
ли другие структуры JavaScript создавать область видимости?

Области видимости из функций
Самый распространенный ответ на эти вопросы — что в JavaScript
используются функциональные области видимости. Иначе говоря, каждая объявленная функция создает область видимости для
себя, но другие структуры не создают своих областей видимости.
Как вы вскоре увидите, это не совсем так.
Но сначала исследуем функциональную область видимости и последствия ее использования. Возьмем следующий пример:

58

Глава 3. Функциональные и блочные области видимости

function foo(a) {
var b = 2;
// Какой-то код
function bar() {
// ...
}
// Еще код
}

var c = 3;

В этом фрагменте пузырь области видимости для foo(..) включает идентификаторы a, b, c и bar. Неважно, где в области видимости находится объявление, в любом случае переменная или
функция принадлежит вмещающей ее области видимости. В следующей главе мы более подробно выясним, как это работает.
bar(..) имеет cобственный пузырь области видимости. Свой пу-

зырь есть и у глобальной области видимости, к которой присоединен всего один идентификатор: foo.
Поскольку a, b, c и bar принадлежат области видимости foo(..),
они недоступны за пределами foo(..). Иначе говоря, в следующем
коде произойдут ошибки ReferenceError, так как идентификаторы
недоступны в глобальной области видимости:
bar(); // ошибка
console.log( a, b, c ); // целых 3 ошибки

Однако все эти идентификаторы (a, b, c, foo и bar) доступны внутри foo(..), а также внутри bar(..) (предполагается, что в bar(..)
нет замещающих объявлений идентификаторов).
Функциональная область видимости подчеркивает ту идею, что
все переменные принадлежат функции и могут использоваться

Как скрыться у всех на виду

59

во всей этой функции (и даже быть доступными для вложенных
областей видимости). Такая архитектура может быть весьма полезной; безусловно, она в полной мере использует «динамическую» природу переменных JavaScript, способных принимать
значения разных типов по мере надобности.
С другой стороны, если не принять защитных мер, существование
переменных на протяжении всей области видимости может создать
непредвиденные проблемы.

Как скрыться у всех на виду
Традиционный подход к функциям подразумевает, что вы объявляете функцию, а потом добавляете в нее код. Однако не менее
полезно взглянуть в другом направлении: вы берете произвольный
фрагмент кода, написанный вами, и заключаете его в объявление
функции, что фактически «скрывает» код от наблюдателя.
На практике вокруг кода создается область видимости, это означает, что любые объявления (переменные или функции) в этом
коде теперь будут связаны с областью видимости новой функцииобертки (вместо прежней внешней области видимости). Иначе
говоря, можно «скрывать» переменные и функции, заключая их
в функциональную область видимости.
Для чего может быть полезно «скрывать» переменные и функции?
Существует множество причин для подобной маскировки на базе
области видимости. Обычно они происходят от «принципа наименьших привилегий»1 при проектировании программных продуктов.
Этот принцип гласит, что при проектировании программного
продукта (например, API модуля/объекта) следует предоставлять
1

https://ru.wikipedia.org/wiki/Принцип_минимальных_привилегий.

60

Глава 3. Функциональные и блочные области видимости

доступ только к тому, что абсолютно необходимо, и «скрывать»
все остальное.
Этот принцип распространяется на выбор области видимости,
содержащей переменные и функции. Если все переменные и функции будут находиться в глобальной области видимости, конечно,
они будут доступны для любой вложенной области видимости.
Однако это нарушит «принцип наименьших привилегий», поскольку вы (с большой вероятностью) будете предоставлять доступ ко многим переменным и функциям, которые следовало бы
оставить недоступными, так как при правильном использовании
кода доступ к этим переменным/функциям будет нежелателен.
Пример:
function doSomething(a) {
b = a + doSomethingElse( a * 2 );
console.log( b * 3 );
}
function doSomethingElse(a) {
return a - 1;
}
var b;
doSomething( 2 ); // 15

Вероятно, в этом фрагменте переменная b и функция doSome­
thingElse(..) относятся к «приватным» подробностям того, как
doSomething(..) делает свою работу. Предоставление «доступа» к b
и doSomethingElse(..) внешней области видимости не только излишне, но и, скорее всего, опасно в том отношении, что они могут
быть использованы непредвиденным образом (намеренно или нет),
и это может нарушить предусловия doSomething(..). В более «правильной» архитектуре эти приватные подробности реализации
будут скрыты в области видимости doSomething(..), примерно так:

Предотвращение конфликтов

61

function doSomething(a) {
function doSomethingElse(a) {
return a - 1;
}
var b;
b = a + doSomethingElse( a * 2 );

}

console.log( b * 3 );

doSomething( 2 ); // 15

Теперь b и doSomethingElse(..) недоступны для внешнего влияния,
ими распоряжается только doSomething(..). Функциональность
и конечный результат не изменились, но архитектура скрывает
приватные детали. Такой код обычно считается более качественным.

Предотвращение конфликтов
Другое преимущество «сокрытия» переменных и функций внутри
области видимости — предотвращение случайных конфликтов
между идентификаторами с одинаковыми именами, но разным
предполагаемым использованием. Конфликты часто приводят
к непреднамеренной перезаписи значений.
Пример:
function foo() {
function bar(a) {
i = 3; // изменение `i` в цикле for внешней области
// видимости
console.log( a + i );
}

62

}

Глава 3. Функциональные и блочные области видимости

for (var i=0; i