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

ES6 и не только [Кайл Симпсон] (pdf) читать онлайн

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


 [Настройки текста]  [Cбросить фильтры]
ББК 32.988.02-018
УДК 004.738.5
С37

Симпсон К.
С37

ES6 и не только. — СПб.: Питер, 2017. — 336 с.: ил. — (Серия
«Бестселлеры O’Reilly»).
ISBN 978-5-496-02445-7
Даже если у вас уже есть опыт работы с JavaScript, скорее всего, язык вы в полной
мере не знаете. Особое внимание в этой книге уделяется новым функциям, появившимся в Ecmascript 6 (ES6) — последней версии стандарта JavaScript.
ES6 повествует о тонкостях языка, малознакомых большинству работающих на
JavaScript программистов. Вооружившись этими знаниями, вы достигнете подлинного
мастерства; выучите новый синтаксис; научитесь корректно использовать итераторы,
генераторы, модули и классы; сможете более эффективно работать с данными; познакомитесь с новыми API, например Array, Object, Math, Number и String; расширите
функционал программ с помощью мета-программирования.

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

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

ISBN 978-5-496-02445-7

© 2016 Piter Press Ltd.
Authorized Russian translation of the English edition of You
Don’t Know JS: ES6 & Beyond, ISBN 9781491904244
© 2016 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.
© Перевод на русский язык ООО Издательство «Питер», 2017
© Издание на русском языке, оформление ООО Издательство
«Питер», 2017
© Серия «Бестселлеры O’Reilly», 2017

Оглавление

Введение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Предисловие. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
Цели и задачи. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
Обзор. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
Условные обозначения. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
Использование примеров кода . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
Safari® Books в Интернете . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
От издательства. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16

Глава 1. ES: современность и будущее . . . . . . . . . . . . . . . . 17
Поддержка версий . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
Транскомпиляция. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
Подводим итоги . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23

Глава 2. Синтаксис. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
Объявления на уровне блоков кода. . . . . . . . . . . . . . . . . . . . . . . . 25
Операторы Spread и Rest. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
Значения параметров по умолчанию. . . . . . . . . . . . . . . . . . . . . . . 38
Деструктурирующее присваивание. . . . . . . . . . . . . . . . . . . . . . . . 44
Расширения объектных литералов . . . . . . . . . . . . . . . . . . . . . . . . 65
Шаблонные строки. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
Стрелочные функции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
Цикл for..of. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
Регулярные выражения. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97

6

Оглавление

Расширения числовых литералов . . . . . . . . . . . . . . . . . . . . . . . . 107
Unicode. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
Тип данных Symbol. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
Подводим итоги . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124

Глава 3. Структура . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
Итераторы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
Генераторы. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
Модули. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
Классы. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
Подводим итоги . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200

Глава 4. Управление асинхронными операциями. . . . . . 202
Обещания. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202
Генераторы и обещания. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
Подводим итоги . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215

Глава 5. Коллекции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217
TypedArrays. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 218
Карты. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224
Объекты WeakMap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 230
Объекты Set. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231
WeakSets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 234
Подводим итоги . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 234

Глава 6. Дополнения к API . . . . . . . . . . . . . . . . . . . . . . . . . 236
Массив. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 236
Объект. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249
Объект Math. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254
Объект Number. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 256
Объект String . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 261
Подводим итоги . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 263

Оглавление

7

Глава 7. Метапрограммирование. . . . . . . . . . . . . . . . . . . . 265
Имена функций. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266
Метасвойства. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 269
Известные символы. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .270
Прокси. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279
Reflect API. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 296
Тестирование функциональных особенностей. . . . . . . . . . . . . . . 301
Оптимизация хвостовой рекурсии. . . . . . . . . . . . . . . . . . . . . . . . 305
Подводим итоги . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 315

Глава 8. За пределами ES6 . . . . . . . . . . . . . . . . . . . . . . . . . 317
Асинхронные функции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 318
Метод Object.observe(..) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 323
Оператор возведения в степень . . . . . . . . . . . . . . . . . . . . . . . . . 327
Свойства объектов и оператор . . . . . . . . . . . . . . . . . . . . . . . . . . 328
Метод Array#includes(..) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 329
Принцип SIMD . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330
Язык WebAssembly (WASM). . . . . . . . . . . . . . . . . . . . . . . . . . . . . 331
Подводим итоги . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 334

Введение

Кайл Симпсон — законченный прагматик.
На мой взгляд, нет высшей похвалы. Я считаю, что это самое важное качество, которым должен обладать разработчик программного обеспечения. Именно так: должен. Никто не умеет разделять
слои языка JavaScript, превращая их в понятные и осмысленные
фрагменты, так, как это делает Кайл.
Книга «ES6 и за его пределами» относится к уже известной читателям серии You Don’t Know JS, а следовательно, вас ждет глубокое
погружение в тему. Автор рассмотрит как очевидные, так и нетипичные случаи, раскрывающие семантику, которая или принимается как должно, или даже не рассматривается. То, о чем
рассказывали предыдущие книги серии You Don’t Know JS, в той
или иной степени было знакомо читателям. Они или видели описываемые вещи, или слышали о них, или даже сталкивались на
собственном опыте. Эта же книга содержит материал, практически
не известный сообществу разработчиков, поскольку спецификация ECMAScript 2015 привела к революционным изменениям
в языке JavaScript.
На протяжении последних лет я наблюдал, как Кайл стремится
постичь новый материал, поднимаясь к вершинам, открытым лишь

Введение

9

небольшому числу его коллег. Задача, которую он поставил перед
собой, была крайне непростой, ведь тогда еще не вышла в свет
официальная спецификация языка. Я говорю чистую правду,
и я прочитал от и до все, что Кайл написал для своей книги. Я отслеживал все изменения и наблюдал, как его код становился лучше,
что свидетельствовало о растущем уровне понимания.
Эта книга коренным образом изменит ваше восприятие и откроет
вам множество новых, доселе не известных вещей. Автор писал ее
с намерением углубить ваши знания и одновременно расширить
инструментарий. С ней вы уверенно вступите в новую эпоху программирования на языке JavaScript.
Рик Уолдрон (@rwaldron),
инженер открытых интернет-проектов в фирме Bocoup,
представитель Ecma/TC39,
для сайта jQuery

Предисловие

Уверен, что вы обратили внимание на «JS» в названии серии. Это
вовсе не первые буквы тех слов, которые мы обычно произносим,
когда хотим нелицеприятно высказаться по поводу JavaScript, —
ведь все мы постоянно ругаемся на странности этого языка.
С момента появления Всемирной паутины JavaScript был основной
технологией, обеспечивающей интерактивное взаимодействие
с информацией. И если поначалу JavaScript ассоциировался с такими вещами, как тянущийся за указателем мыши мерцающий
шлейф и надоедливые всплывающие окна, то за два десятилетия
возможности языка возросли на много порядков, и сегодня вряд
ли кто-то усомнится в его важности для функционирования Интернета в целом.
Тем не менее JavaScript всегда был мишенью для неумеренной
критики, отчасти из-за своих странностей, но в основном из-за
присущих языку особенностей проектирования. Даже само его имя,
как однажды выразился Брендан Эйх, заставляет думать, что
JavaScript — это непутевый младший брат более совершенного
языка Java. Другое дело, что назвали его так практически случайно,
по политическим и маркетинговым соображениям. Эти два языка

Предисловие

11

сильно отличаются во многих важных аспектах. «JavaScript» имеет такое же отношение к «Java», как карнавал к автомобилю1.
Так как концепцию и синтаксические особенности JavaScript унаследовал от нескольких языков — например, бросающиеся в глаза
программные конструкции в стиле C или менее очевидные принципы функционального программирования в стиле Scheme/Lisp, —
он оказался понятен множеству людей, в том числе не имеющим
серьезного опыта в разработке приложений. Демонстрирующая
возможности языка программа «Hello World» на JavaScript пишется чрезвычайно просто, что делает его крайне привлекательным
и легким для освоения.
Язык JavaScript прост, он позволяет быстро освоить теоретические
основы и приступить к программированию, но из-за его странностей
достичь в нем мастерства намного сложнее, чем в других языках.
В ситуациях, когда для написания полноценной программы на C
или C++ требуется глубокое понимание этих языков, JavaScript
зачастую позволяет обойтись поверхностными знаниями.
Сложные концепции этого языка представлены обманчиво упрощенными способами, такими как передача функций в виде обратных вызовов, что провоцирует разработчиков применять инструменты JavaScript, не задумываясь о том, как именно они работают.
Этот язык популярен, легок в освоении и прост в использовании,
но одновременно и сложен, так что без тщательного изучения его
глубинных механизмов смысл происходящего будет ускользать
даже от самых опытных JavaScript-разработчиков.
Вот в чем заключается парадокс JavaScript, вот где его ахиллесова
пята. Изучением всех сложностей такого рода мы с вами и займемся — ведь если пользоваться JavaScript, не вникая в то, как он работает, можно так и не понять принцип его функционирования.
1

Игра слов: carnival и car (англ.). — Примеч. пер.

12

Предисловие

Цели и задачи
Если вы склонны заносить в черный список все, что в JavaScript
кажется странным или непонятным (а некоторые привыкли поступать именно так), в какой-то момент от богатого возможностями языка у вас останется лишь пустая оболочка.
Такое доступное всем подмножество механизмов JavaScript принято считать сильными сторонами этого языка, но правильнее
назвать это легкими в освоении, безопасными или даже минимальными возможностями.
Я предлагаю вам поступить наоборот: досконально изучить JavaScript,
чтобы понять даже самые сложные его особенности. Именно о них
пойдет речь в этой книге.
Мне известна склонность JS-разработчиков изучать лишь минимум,
необходимый для решения конкретной задачи, но в моей книге вы
не встретите распространенной рекомендации избегать сложностей.
Даже если что-то работает нужным мне образом, я не готов удовлетвориться самим этим фактом — мне важно понять, почему и как
оно работает. Хотелось бы, чтобы вы разделили мой подход. Я ненавязчиво зову вас пройти по тернистой дороге, которой мало кто
ходил, и полностью осмыслить, что представляет собой язык
JavaScript и какие возможности он дает. И когда вы будете обладать
этими знаниями, ни одна техника, ни одна платформа, ни один
новый подход не окажутся за пределами вашего понимания.
Каждая из книг серии You Don’t Know JS глубоко и исчерпывающе
раскрывает конкретные ключевые элементы языка, которые зачастую толкуются неверно или поверхностно. После прочтения этой
литературы вы получите твердую уверенность в том, что понимаете не только теоретические, но и практические нюансы.
Те познания в JavaScript, что у вас есть, скорее всего, вы получили
от людей, которые сами попали в ловушку недостаточного понимания. Это всего лишь тень того, чем JavaScript является на самом

Условные обозначения

13

деле. Вы еще толком не знаете его, но книги серии You Don’t Know JS
помогут вам наверстать упущенное. Поэтому вперед, дорогие друзья, язык JavaScript ждет вас!

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

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

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

14

Предисловие

Моноширинный жирный шрифт

Указывает на команды или другой текст, который должен
быть набран пользователем.
Моноширинный курсив

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

Этот элемент обозначает общее примечание.

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

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

Safari® Books в Интернете

15

код из книги, вполне допустимо, а вот вставлять те же материалы
в значительных объемах в документацию к вашей продукции можно только с разрешения издательства.
Мы не требуем, чтобы вы всегда ссылались на источник материалов,
но будем крайне признательны, если вы это сделаете. Ссылка обычно включает в себя название книги, указание автора и издателя,
а также ISBN. В данном случае она выглядит так: «You Don’t Know
JavaScript: ES6 & Beyond by Kyle Simpson (O’Reilly). Copyright 2016
Getify Solutions, Inc., 978-1-491-90424-4».
Если вас интересует использование примеров, выходящее за рамки
данных выше разрешений, обращайтесь по адресу permissions@
oreilly.com.

Safari® Books в Интернете
Safari Books Online (https://www.safaribooksonline.
com/) — это цифровая библиотека, предоставляющая в формате книг и видеозаписей материалы (https://www.safaribooksonline.com/explore/) ведущих мировых
экспертов в области технологий и бизнеса.
Технические специалисты, разработчики программного обеспечения, веб-дизайнеры, предприниматели и люди творческих профессий используют сайт Safari Books Online как основной ресурс для
исследований, решения задач, обучения и проведения сертификационных тренингов.
Сайт предлагает различные варианты подписки для компаний
(https://www.safaribooksonline.com/enterprise/), государственных учреждений (https://www.safaribooksonline.com/government/), учебных заведений
(https://www.safaribooksonline.com/academic-public-library/) и физических
лиц.
Покупка членства дает доступ к тысячам книг, обучающих видеороликов и еще не опубликованных материалов, собранных в единую

16

Предисловие

базу данных с полнофункциональным поиском. Там есть книги
таких издательств, как O’Reilly Media, Prentice Hall Professional,
AddisonWesley Professional, Microsoft Press, Sams, Que, Peachpit
Press, Focal Press, Cisco Press, John Wiley & Sons, Syngress, Morgan
Kaufmann, IBM Redbooks, Packt, Adobe Press, FT Press, Apress,
Manning, New Riders, McGraw-Hill, Jones & Bartlett, Course Tech­
nology и сотен других (https://www.safaribooksonline.com/our-library/).
Дополнительную информацию вы найдете на сайте Safari Books
Online.

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

1

ES: современность
и будущее

Для чтения этой книги вы должны хорошо владеть языком JavaScript
вплоть до последнего (на момент написания книги) стандарта,
который называется ES5 (точнее, ES5.1), поскольку мы с вами
будем рассматривать новый стандарт ES6, попутно пытаясь понять,
какие перспективы ждут JS.
Если вы не очень уверены в своих знаниях JavaScript, рекомендую
предварительно ознакомиться с предшествующими книгами серии
You Don’t Know JS.
 Up & Going: Вы только начинаете изучать программирование

и JS? Перед вами карта, которая поможет вам в путешествии
по новой области знаний.
 Scope & Closures: Известно ли вам, что в основе лексическо-

го контекста JS лежит семантика компилятора (а не интерпретатора)? Можете ли вы объяснить, каким образом замыкания являются прямым результатом лексической области
видимости и функций как значений?
 this & Object Prototypes: Можете ли вы назвать четыре варианта значения ключевого слова this в зависимости от кон-

текста вызова? Приходилось ли вам путаться в псевдоклас-

18

Глава 1. ES: современность и будущее

сах JS, вместо того чтобы воспользоваться более простым
шаблоном проектирования behavior delegation? А слышали
ли вы когда-нибудь про объекты, связанные с другими объектами (OLOO)?
 Types & Grammar: Знакомы ли вы со встроенными типами

в JS и, что более важно, знаете ли способы корректного и безопасного приведения типов? Насколько уверенно вы разбираетесь в нюансах грамматики и синтаксиса этого языка?
 Async & Performance: Вы все еще используете обратные вы-

зовы для управления асинхронными действиями? А можете
ли вы объяснить, что такое объект promise и как он позволяет избежать ситуации, когда каждая фоновая операция возвращает свой результат (или ошибку) в обратном вызове?
Знаете ли вы, как с помощью генераторов улучшить читабельность асинхронного кода? Наконец, известно ли вам, что
представляет собой полноценная оптимизация JS-программ
и отдельных операций?
Если вы уже прочитали все эти книги и освоили рассматриваемые
там темы, значит, пришло время погрузиться в эволюцию языка JS
и исследовать перемены, которые ждут нас как в ближайшее время,
так и в отдаленной перспективе.
В отличие от предыдущего стандарта, ES6 нельзя назвать еще одним
скромным набором добавленных к языку API. Он принес с собой
множество новых синтаксических форм, и к некоторым из них,
вполне возможно, будет не так-то просто привыкнуть. Появились
также новые структуры и новые вспомогательные модули API для
различных типов данных.
ES6 — это шаг далеко вперед. Даже если вы считаете, что хорошо
знаете JS стандарта ES5, вы столкнетесь с множеством незнакомых
вещей, так что будьте готовы! В книге рассмотрены все основные
нововведения ES6, без которых невозможно войти в курс дела,
а также дан краткий обзор планируемых функций — о них имеет
смысл знать уже сейчас.

Поддержка версий

19

Весь приведенный в книге код рассчитан на среду исполнения ES6+.
На момент написания этих строк уровень поддержки ES6 в браузерах и в JS-средах (таких, как Node.js) несколько разнился, так что
вы можете обнаружить, что полученный вами результат отличается от описанного.

Поддержка версий
Стандарт JavaScript официально называется ECMAScript (или
сокращенно ES), и до недавнего времени все его версии обозначались только целыми числами. ES1 и ES2 не получили известности
и массовой реализации. Первой широко распространившейся основой для JavaScript стал ES3 — стандарт этого языка для браузеров Internet Explorer с 6-й по 8-ю версию и для мобильных браузеров Android 2.x. По политическим причинам, о которых я умолчу,
злополучная версия ES4 так и не увидела света.
В 2009 году был официально завершен ES5 (ES5.1 появился
в 2011-м), получивший распространение в качестве стандарта для
множества современных браузеров, таких как Firefox, Chrome,
Opera, Safari и др.
Следующая версия JS (появление которой было перенесено
с 2013-го сначала на 2014-й, а затем на 2015 год) в обсуждениях
фигурировала под очевидным именем ES6. Но позднее стали поступать предложения перейти к схеме именования, основанной на
годе выхода очередной версии, например ES2016 (она же ES7),
которая будет закончена до конца 2016 года. Согласны с таким
подходом далеко не все, но есть вероятность, что стандарт ES6
станет известен пользователям под названием ES2015. А появление
версии ES2016 станет свидетельством окончательного перехода на
новую схему именования.
Кроме того, было отмечено, что скорость эволюции JS превышает
одну версию в год. Как только в обсуждениях стандарта возникает
новая идея, разработчики браузеров предлагают прототипы нового

20

Глава 1. ES: современность и будущее

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

Транскомпиляция
Быстрая эволюция функционала ставит серьезную проблему перед
разработчиками, желающими использовать новые возможности
в ситуации, когда их сайты или приложения работают в более старых браузерах, не поддерживающих нововведения.
По всей видимости, ES5 не прижился во многих местах, потому что
в основном базу кода не приводили в соответствие с новым стандартом до тех пор, пока не прекратилась поддержка большинства,
если не всех, предшествующих платформ. В результате многие
разработчики только недавно начали пользоваться такими вещами,
как, к примеру, строгий режим, появившийся в ES5 более пяти лет
назад.
Подобные многолетние промедления повсеместно считаются вредными для будущего экосистемы JS. Люди, занимающиеся развитием языка, мечтают, чтобы разработчики начинали создавать код
с учетом новых функциональных особенностей и шаблонов, сразу

Транскомпиляция

21

же после того, как будет утверждена спецификация, и браузеры
смогут все это реализовывать.
Как же разрешить противоречие? Здесь на помощь приходят специальные инструменты, в частности техника транскомпиляции1.
Грубо говоря, вы посредством специального инструмента преобразуете код ES6 в эквивалент (или нечто близкое к таковому),
работающий в окружениях ES5.
В качестве примера возьмем сокращенные определения свойства
(см. раздел «Расширения объектных литералов» в главе 2). Вот как
это делается в ES6:
var
var
};

foo = [1,2,3];
obj = {
foo
// означает 'foo: foo'

obj.foo;

// [1,2,3]

А вот каким образом (примерно) он транскомпилируется:
var
var
};

foo = [1,2,3];
obj = {
foo: foo

obj.foo;

// [1,2,3]

Такое небольшое, но удобное преобразование позволяет в случае
одинаковых имен сократить объявление объектного литерала foo:
foo до foo. Действия транскомпилятора в этом случае представляют собой встроенный рабочий процесс, аналогичный линтингу,
минификации и другим подобным операциям.

1

Transpiling — термин образован от transformation (преобразование) и com­
piling (компиляция) (англ.). — Примеч. пер.

22

Глава 1. ES: современность и будущее

Библиотеки Shim (полизаполнения)
Далеко не всем новым функциональным особенностям ES6 требуется транскомпилятор. Полизаполнения (polyfills), которые также
называют библиотеками Shim, представляют собой шаблоны для
определения поведений из новой среды для более старых сред.
В синтаксисе полизаполнения недопустимы, но для различных API
их вполне можно использовать.
Давайте рассмотрим новый метод Object.is(..), предназначенный
для проверки строгой эквивалентности двух значений, но без подробных исключений, которые есть у оператора === для значений
NaN и -0. Полизаполнение для метода Object.is(..) создается очень
просто:
if (!Object.is) {
Object.is = function(v1, v2) {
// проверка для значения '-0'
if (v1 === 0 && v2 === 0) {
return 1 / v1 === 1 / v2;
}
// проверка для значения 'NaN'
if (v1 !== v1) {
return v2 !== v2;
}
// все остальное
return v1 === v2;
};
}
Обратите внимание на внешнее граничное условие оператора if,
охватывающее полизаполнение. Это важная деталь, означающая,
что резервное поведение данный фрагмент кода включает только
в более старых контекстах, где рассматриваемый API еще не определен; необходимости переписывать существующий API практически никогда не возникает.

Есть замечательная коллекция ES6 Shim (https://github.com/paulmillr/
es6-shim/), которую стоит включать во все новые JS-проекты.

Подводим итоги

23

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

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

2

Синтаксис

Если у вас есть хоть какой-то опыт написания программ на JS,
скорее всего, вы достаточно хорошо знакомы с его синтаксисом.
У него немало своих особенностей, но несмотря на это он достаточно рационален и несложен, а кроме того, имеет множество аналогий
в других языках.
Стандарт ES6 добавляет ряд новых синтаксических форм, к которым вам нужно будет привыкнуть. О них я расскажу в этой главе,
чтобы дать представление о том, с чем вам предстоит работать.
На момент написания этой книги некоторые составляющие нового
функционала уже были полноценно реализованы в браузерах
(Firefox, Chrome и др.), другие — реализованы лишь частично,
а какие-то — вообще пока недоступны нигде. Соответственно, если
вы будете использовать приведенные в книге примеры как есть,
результаты могут оказаться непредсказуемыми, так что лучше вам
прибегнуть к транскомпиляторам, умеющим работать с большей
частью новых функциональных особенностей.
Например, существует замечательная, легкая в использовании
«песочница» ES6Fiddle (http://www.es6fiddle.net/) для запуска ES6кода прямо в браузере, а также онлайновая REPL-среда для транскомпилятора Babel (http://babeljs.io/repl/).

Объявления на уровне блоков кода

25

Объявления на уровне блоков кода
Вы, наверное, знаете, что областью видимости переменной
в JavaScript в основном всегда была функция. Предпочтительным
способом задания видимости переменной в определенном блоке
кода, кроме обычного объявления функции, является немедленно
вызываемая функция (IIFE). Например:
var a = 2;
(function IIFE(){
var a = 3;
console.log( a );
})();
console.log( a );

// 3
// 2

Оператор let
Впрочем, теперь у нас есть возможность создать объявление, связанное с произвольным блоком, которое вполне логично называется блочной областью видимости (block scoping). Для этого достаточно будет пары фигурных скобок { .. }. Вместо оператора var,
объявляющего область видимости переменных внутри функции,
в которую он вложен (или глобальную область видимости, если он
находится на верхнем уровне), мы воспользуемся оператором let:
var a = 2;
{
}

let a = 3;
console.log( a );

console.log( a );

// 3
// 2

Использование отдельных блоков { .. } — не очень распространенная практика в JS, но это всегда работает. Если вы имели дело

26

Глава 2. Синтаксис

с языками, в которых возможна область видимости на уровне блоков, вы легко распознаете данный шаблон.
На мой взгляд, это самый лучший способ создания переменных
с блочной областью видимости. Оператор let всегда должен находиться в верхней части блока, причем желательно, чтобы
он был один, вне зависимости от количества объявляемых переменных.
Если говорить о стилистике, то я считаю, что правильнее помещать
оператор let на одну строчку с открывающейся фигурной скобкой {,
как бы подчеркивая, что единственное назначение блока — это
определение области видимости переменных.
{ let a = 2, b, c;
// ..
}

Я понимаю, что написанное мной выглядит странно и, скорее всего, противоречит рекомендациям, которые даются в других книгах
по ES6. Но у меня есть на это веские причины.
Существует еще одна экспериментальная (не входящая в стандарт)
форма объявления при помощи оператора let , называемая letблоком. Она выглядит следующим образом:
let (a = 2, b, c) {
// ..
}

Я называю эту форму явным объявлением области видимости внутри блока. Имитирующая оператор var форма объявления переменной let .. более неявна — она как бы захватывает все содержимое фигурных скобок { .. }. Как правило, разработчики считают
явные механизмы более предпочтительными, и мне кажется, что
в данном случае имеет смысл придерживаться этого подхода.
Если сравнивать два предыдущих фрагмента кода, в глаза бросается их сходство, и, с моей точки зрения, оба они стилистически

27

Объявления на уровне блоков кода

могут рассматриваться как явное объявление блочной области
видимости. К сожалению, наиболее явная форма let (..) { .. }
в стандарт ES6 не вошла. Существует вероятность, что она появится в последующих версиях ES6, пока же предыдущий пример — это
лучшее, чем вы можете воспользоваться.
Подчеркнуть неявную природу объявления переменных с помощью
оператора let .. можно так, как показано ниже:
let a = 2;
if (a > 1) {
let b = a * 3;
console.log( b );

// 6

for (let i = a; i ,
на первый взгляд кажутся попытками обеспечить лаконичный
и элегантный синтаксис, но на самом деле обладают крайне специ­
фическим поведением и должны использоваться только в определенных ситуациях.
Завершает синтаксическую эволюцию ES6 расширенная поддержка стандарта Unicode, дополнительные возможности работы с регулярными выражениями и новый примитивный тип symbol.

3

Структура

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

Итераторы
Итератором (iterator) называется структурированный шаблон,
предназначенный для последовательного извлечения информации
из источника. В программировании подобные шаблоны используются давно. Разумеется, и на JS с незапамятных времен проектируются и реализуются итераторы, так что это далеко не новая
тема.
В ES6 для итераторов появился неявный стандартизованный интерфейс. Многие встроенные структуры JavaScript предоставляют

Итераторы

127

итератор, реализующий этот стандарт. Вы можете сконструировать
и собственные версии итераторов, если будете придерживаться
стандарта для обеспечения максимальной совместимости.
Итераторы позволяют упорядоченно, последовательно извлекать
данные.
Например, можно реализовать сервис, который при каждом запросе будет генерировать новый уникальный идентификатор; или
формировать бесконечный ряд значений, прокручиваемых в фиксированном списке циклическим способом; или присоединить
итератор к результатам запроса к базе данных, чтобы по одной
извлекать новые строки.
Есть еще одна особенность, которая, как правило, не используется
в JS. На итераторы можно посмотреть как на контролирующее поведение, действующее пошагово. Иллюстрацию этого вы найдете
в разделе «Генераторы» данной главы, хотя подобное реализуемо
и без генераторов.

Интерфейсы
На момент написания книги раздел 25.1.1.2 спецификации ES6
описывал интерфейс Iterator как отвечающий следующим требованиям:
Iterator [обязательные параметры]
next() {метод}: загружает следующий IteratorResult

Два дополнительных параметра для расширения некоторых итераторов:
Iterator [необязательные параметры]
return() {метод}: останавливает итератор и возвращает
IteratorResult
throw() {метод}: сообщает об ошибке и возвращает IteratorResult

Интерфейс IteratorResult определен следующим образом:

128

Глава 3. Структура

IteratorResult
value {свойство}: значение на текущей итерации или
окончательное возвращаемое значение
(не обязательно, если это 'undefined')
done {свойство}: тип boolean, показывает состояние выполнения

Я называю эти интерфейсы неявными не потому, что они в явном
виде не указаны в спецификации — они указаны! — просто они не
появляются в качестве доступных коду непосредственных объектов.
В ES6 язык JavaScript не поддерживает понятие «интерфейсов»,
поэтому их появление в вашем собственном коде является откровенно условным. Тем не менее в том месте, где ожидается итератор — например, в цикле for..of, — ваш код должен выглядеть
в соответствии с этими интерфейсами, или он не будет работать.

Существует также интерфейс Iterable, описывающий объект, который должен уметь генерировать итераторы:
Iterable
@@iterator() {метод}: генерирует Iterator

В разделе «Встроенные символы» главы 2 упоминался @@iterator —
специальный встроенный символ, представляющий метод, который
умеет генерировать итераторы для объекта.

IteratorResult
Интерфейс IteratorResult определяет, что возвращаемое значение
любой из операций итератора будет представлять собой объект
следующего вида:
{ value: .. , done: true / false }

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

129

Итераторы

взялись данные, сколько времени заняло их извлечение, каково
время жизни кэша и частота для следующего подходящего запроса
и т. п.).
С технической точки зрения значение считается необязательным,
если в иной форме оно рассматривается как отсутствующее или
незаданное, например как в случае значения undefined. Так как
доступ к res.value даст нам undefined вне зависимости от того,
есть там значение или нет, наличие/отсутствие данного свойства
относится, скорее, к деталям реализации или оптимизации, а не
к функциональным вопросам.

Метод next()
Рассмотрим итерируемый массив и итератор, который он создает
для работы со своими значениями:
var arr = [1,2,3];
var it = arr[Symbol.iterator]();
it.next();
it.next();
it.next();

// { value: 1, done: false }
// { value: 2, done: false }
// { value: 3, done: false }

it.next();

// { value: undefined, done: true }

Каждый раз, когда связанный со свойством Symbol.iterator (оно
рассматривается в главах 2 и 7) метод вызывается со значением arr,
он создает свежий итератор. Большинство структур будут давать
аналогичный результат, в том числе встроенные в JS структуры
данных.
Но структура, которая, к примеру, является получателем очереди
событий, может создать лишь единственный итератор (шаблонодиночка). Еще бывают структуры, допускающие в каждый момент
времени только один итератор. В этом случае перед созданием
нового итератора следует завершить уже существующий.

130

Глава 3. Структура

Итератор it из последнего фрагмента кода не сообщает о завершении своей работы: когда вы получаете значение 3, false меняется
на true. Но чтобы узнать об этом, вам приходится еще раз вызывать
метод next(), фактически выходя за границу значений массива.
Чуть позже вы поймете, почему такое проектное решение, как правило, считается наилучшим вариантом.
Примитивные строковые значения также по умолчанию итерируемы:
var greeting = "hello world";
var it = greeting[Symbol.iterator]();
it.next();
it.next();
..

// { value: "h", done: false }
// { value: "e", done: false }

С технической точки зрения само по себе значение примитива
итерируемым не является, но благодаря «упаковке» строка "hello
world" преобразуется в итерируемый объект-оболочку String.
Подробно данная тема рассматривается в книге Types & Grammar
этой серии.

В ES6 также появилось несколько новых структур данных, называемых коллекциями (см. главу 5). Коллекции не только итерируемы, но и предоставляют методы API для генерации итератора.
Например:
var m = new Map();
m.set( "foo", 42 );
m.set( { cool: true }, "hello world" );
var it1 = m[Symbol.iterator]();
var it2 = m.entries();
it1.next();
it2.next();
..

// { value: [ "foo", 42 ], done: false }
// { value: [ "foo", 42 ], done: false }

Итераторы

131

Метод итератора next(..)в зависимости от вашего желания принимает один или несколько аргументов. Встроенные итераторы эту
возможность в основном не используют, чего нельзя сказать об
итераторе генератора (см. ниже раздел «Генераторы»).
По общему соглашению, вызов метода next(..) после завершения
работы итератора (в том числе для встроенных итераторов) не приводит к ошибке, а дает результат { value: undefined, done: true }.

Необязательные методы: return(..) и throw(..)
Необязательные методы интерфейса итератора — return(..)
и throw(..) — у большинства встроенных итераторов не реализуются. Тем не менее они, безусловно, значимы в контексте генераторов, поэтому более подробную информацию вы найдете в разделе «Генераторы».
Метод return(..) определен как отправляющий итератору сигнал
о том, что код извлечения информации завершил работу и больше
не будет извлекать значения. Этот сигнал уведомляет итератор,
отвечающий на вызовы метода next(..), что пришло время приступить к очистке, например к освобождению/закрытию сетевого
соединения, базы данных или дескриптора файла.
Если у итератора есть метод return(..) и при этом возникает любое
условие, которое можно интерпретировать как аномальное или
раннее прекращение использования итератора, этот метод вызывается автоматически. Впрочем, это легко сделать и вручную.
Метод return(..), как и next(..), возвращает объект IteratorResult.
В общем случае необязательное значение, отправленное в метод
return(..), будет возвращено как значение в объекте IteratorResult,
хотя возможны и другие варианты развития событий.
Метод throw(..) сообщает итератору об исключении или ошибке,
причем эту информацию тот может использовать иначе, нежели
подаваемый методом return(..)сигнал завершения. Ведь, в отличие

132

Глава 3. Структура

от последнего, исключение или ошибка вовсе неподразумевает
обязательного прекращения работы итератора.
Например, в случае с итераторами генератора метод throw(..), по
сути, вставляет в приостановленный контекст выполнения генератора порожденное им исключение, которое может быть перехвачено оператором try..catch. Необработанное исключение в конечном
счете прерывает работу итератора.
По общему соглашению, после вызова методов return(..) или
throw(..) итератор не должен больше генерировать никаких результатов.

Цикл итератора
Как было сказано в разделе «Цикл for..of» главы 2, появившийся
в ES6 цикл for..of работает непосредственно с итерируемыми
объектами.
Если итератор сам является итерируемым, его можно напрямую
использовать с циклом for..of. Чтобы сделать итератор таковым,
его следует передать методу Symbol.iterator, который вернет нужный результат:
var

};

it = {
// делаем итератор 'it' итерируемым
[Symbol.iterator]() { return this; },
next() { .. },
..

it[Symbol.iterator]() === it; // true

Теперь можно вставить итератор it в цикл for..of:

133

Итераторы

for
}

(var v of it) {
console.log( v );

Чтобы полностью понять, что тут происходит, вспомните, как работает эквивалент цикла for..of — цикл for (мы говорили о нем
в главе 2):
for
}

(var v, res; (res = it.next()) && !res.done; ) {
v = res.value;
console.log( v );

При ближайшем рассмотрении мы видим, что перед каждой итерацией вызывается метод it.next(), после чего происходит сравнение с переменной res.done. Если она имеет значение true, выражение получает результат false , и следующей итерации не
происходит.
Напомню, что ранее мы уже говорили о принятой для итераторов
тенденции не возвращать вместе с предполагаемым окончательным
значением done: true. Теперь вы видите, почему так происходит.
Если итератор вернет { done: true, value: 42 }, цикл for..of отбросит
последнее значение 42, и оно будет потеряно. Именно из-за того,
что итераторы могут использоваться такими шаблонами, как цикл
for..of или его эквиваленты, следует подождать с возвращением
сигнализирующего о завершении работы значения done: true, пока
не будут возвращены все связанные с итерациями значения.
Разумеется, ничто не мешает целенаправленно спроектировать
итератор, который будет одновременно возвращать последнее
значение и done: true. Но такие вещи обязательно следует документировать, потому что в противном случае вы неявно заставите
потребителей итератора использовать шаблон итераций, отличный
от задаваемого циклом for..of или его эквивалентами.

134

Глава 3. Структура

Пользовательские итераторы
В дополнение к стандартным встроенным итераторам теперь вы
можете создать собственный. Для обеспечения взаимодействия
с элементами ES6, использующими итераторы (например, с циклом
for..of или оператором ...), достаточно корректного интерфейса
(или интерфейсов).
Давайте создадим итератор, генерирующий бесконечную последовательность чисел Фибоначчи:
var

Fib = {
[Symbol.iterator]() {
var
n1 = 1, n2 = 1;
return {
// делаем итератор итерируемым
[Symbol.iterator]() { return this; },
next() {
var current = n2;
n2 = n1;
n1 = n1 + current;
return { value: current, done: false };
},

};
for

}

};

return(v) {
console.log(
"Последовательность Фибоначчи завершена."
);
return { value: v, done: true };
}

(var v of Fib) {
console.log( v );

if
(v > 50) break;
}
// 1 1 2 3 5 8 13 21 34 55
// Последовательность Фибоначчи завершена

135

Итераторы

Без оператора break цикл for..of работал бы бесконечно, а это
не имеет смысла.

Метод Fib[Symbol.iterator]() возвращает объект-итератор, обладающий методами next() и return(..). Состояние поддерживается
посредством переменных n1 и n2, сохраненных замыканием.
А теперь рассмотрим итератор, выполняющий по очереди некоторые действия:
var

tasks = {
[Symbol.iterator]() {
var steps = this.actions.slice();
return {
// делаем итератор итерируемым
[Symbol.iterator]() { return this; },
next(...args) {
if
(steps.length > 0) {
let res = steps.shift()( ...args );
return { value: res, done: false };
}
else {
return { done: true }
}
},
return(v) {
steps.length = 0;
return { value: v, done: true };
}

};

};
},
actions: []

Итератор объекта tasks перебирает функции, обнаруженные
в свойстве-массиве actions, и выполняет по очереди, передавая
в них аргументы, которые ранее были переданы методу next(..),

136

Глава 3. Структура

а затем возвращая полученное значение в виде стандартного объекта Itera­torResult.
Вот как мы можем воспользоваться этой очередью задач:
tasks.actions.push(
function step1(x){
console.log( "step 1:", x );
return x * 2;
},
function step2(x,y){
console.log( "step 2:", x, y );
return x + (y * 2);
},
function step3(x,y,z){
console.log( "step 3:", x, y, z );
return (x * y) + z;
}
);
var it = tasks[Symbol.iterator]();
it.next( 10 );

// step 1: 10
// { value: 20, done: false }

it.next( 20, 50 );

// step 2: 20 50
// { value: 120, done: false }

it.next( 20, 50, 120 );

// step 3: 20 50 120
// { value: 1120, done: false }

it.next();

// { done: true }

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

137

Итераторы

от 0 до какого-то заданного положительного или отрицательного
значения:
if

(!Number.prototype[Symbol.iterator]) {
Object.defineProperty(
Number.prototype,
Symbol.iterator,
{
writable: true,
configurable: true,
enumerable: false,
value: function iterator(){
var i, inc, done = false, top = +this;
// итерации в положительную или отрицательную
// сторону?
inc = 1 * (top < 0 ? -1 : 1);
return {
// делаем итерируемым сам итератор
[Symbol.iterator](){ return this; },
next() {
if

(!done) {
// начальная итерация всегда 0
if
(i == null) {
i = 0;
}
// итерации в положительном
// направлении
else if (top >= 0) {
i = Math.min(top,i + inc);
}
// итерации в отрицательном
// направлении
else {
i = Math.max(top,i + inc);
}
// закончить после этой итерации?
if
(i == top) done = true;
return { value: i, done: false };

138

}

Глава 3. Структура

);

}

}

};

}

}
else {
return { done: true };
}

Что же мы получили в результате такого творческого подхода?
for

(var i of 3) {
console.log( i );

}
// 0 1 2 3
[...-3];

// [0,-1,-2,-3]

Эти приемы кажутся забавными, хотя их практическая польза
остается до некоторой степени спорной. В то же время может возникнуть вопрос: почему в ES6 отсутствует эта незначительная, но
приятная функциональная особенность?
Было бы упущением не напомнить вам, что к расширению встроенных прототипов, пример одного из которых вы видели в последнем фрагменте кода, нужно подходить аккуратно и все время
помнить о потенциальных опасностях.
В рассмотренном случае шансы конфликта с другим кодом или
с какой-нибудь будущей функциональной особенностью JS, скорее
всего, исчезающее малы. Но даже к такой малой вероятности следует подходить серьезно, поэтому подробно документируйте такие
вещи, чтобы избежать проблем в будущем.
Я расширил эту технику, см. http://blog.getify.com/iterating-es6numbers/. В одном из комментариев к записи http://blog.getify.com/
iterating-es6-numbers/comment-page-1/#comment-535294 мне предложили использовать аналогичный прием для диапазонов строковых
символов.

139

Итераторы

Применение итераторов
Вы уже видели, как итератор шаг за шагом работает в цикле for..of.
Но им могут пользоваться и другие структуры.
Рассмотрим итератор, присоединенный к массиву (хотя аналогичное поведение будет демонстрировать любой выбранный нами
итератор):
var a = [1,2,3,4,5];

Оператор разделения ... исчерпывает итератор полностью. Например:
function foo(x,y,z,w,p) {
console.log( x + y + z + w + p );
}
foo( ...a );

// 15

Кроме того, можно распределить итератор внутри массива:
var b = [ 0, ...a, 6 ];
b;

// [0,1,2,3,4,5,6]

При процедуре деструктуризации массива (см. раздел «Деструктурирующее присваивание» главы 2) итератор может использоваться как частично, так и полностью (в сочетании с rest/gatherоператором ...).
var it = a[Symbol.iterator]();
var [x,y] = it;
// берем из 'it' только первые два элемента
var [z, ...w] = it;
// берем третий элемент, а затем сразу все остальные
// истощен ли 'it' полностью? Да
it.next();
// { value: undefined, done: true }
x;

// 1

140

Глава 3. Структура

y;
z;
w;

// 2
// 3
// [4,5]

Генераторы
Все функции работают до своего завершения, верно? Другими
словами, запущенная функция заканчивает работу до того, как
начнет выполняться другая операция.
По крайней мере, именно так обстояли дела на протяжении всей
истории существования языка JavaScript. Но в ES6 появилась новая, несколько необычная форма функции, названная генератором.
Такая функция может остановиться во время выполнения, а затем
продолжить работу с прерванного места.
Более того, каждый цикл остановки и возобновления работы позволяет передавать сообщения в обе стороны. Как генератор может
вернуть значение, так и восстанавливающий его работу управляющий код может переслать туда что-нибудь.
Так же, как в случае с итераторами, рассмотренными в предыдущем
разделе, на генераторы, вернее на то, для чего они в основном предназначены, можно смотреть с разных сторон. Единственно верной
точки зрения не существует.
Более подробно генераторы рассматриваются в книге Async &
Performance этой серии, а также в главе 4 данной книги.

Синтаксис
Функция-генератор объявляется следующим образом:
function *foo() {
// ..
}

Генераторы

141

Положение звездочки * функционально несущественно. То
же ­самое объявление можно написать любым из следующих способов:
function *foo() { .. }
function* foo() { .. }
function * foo() { .. }
function*foo() { .. }
..

Единственное, что имеет значение в данном случае, — это стилистические предпочтения. Авторы большинства руководств пишут
function* foo(..) { .. }. Мне же больше нравится вариант function
*foo(..) { .. } — его я и буду придерживаться в этой и следующих
книгах.
Причина тут чисто дидактического характера. В тексте для обозначения функции-генератора я буду писать *foo(..), в то время
как обычная функция выглядит как foo(..). Также символ * расположен и в объявлении function *foo(..) { .. }.
Кроме того, существует краткая форма генератора в объектных
литералах (с краткими методами вы познакомились в главе 2):
var
};

a = {
*foo() { .. }

С моей точки зрения, в случае кратких генераторов запись *foo()
{ .. } выглядит естественнее, чем * foo() { .. }, — еще один довод,
почему лучше писать так, как это делаю я. Единообразие в подобных
вещах облегчает понимание материала.

Выполнение генератора
Хотя генератор объявляется с символом *, вызывается он как обычная функция:
foo();

142

Глава 3. Структура

В него можно передавать аргументы:
function *foo(x,y) {
// ..
}
foo( 5, 10 );

Основное отличие состоит в том, что запуск генератора, например
foo(5,10), не приводит к исполнению его кода. Вместо этого создается итератор, контролирующий то, как генератор исполняет
свой код.
Мы вернемся к этому в разделе «Контроль со стороны итератора»,
а пока ограничимся примером:
function *foo() {
// ..
}
var it = foo();
// чтобы начать/продолжить выполнение '*foo()',
// вызываем 'it.next(..)'

Ключевое слово yield
Внутри генераторов используется ключевое слово, сигнализирующее о прерывании работы: yield. Рассмотрим пример:
function *foo() {
var x = 10;
var y = 20;
yield;
}

var z = x + y;

В этом генераторе *foo() сначала запускаются операции из первых
двух строк, а затем ключевое слово yield останавливает работу.

Генераторы

143

После ее возобновления запускается последняя строчка генератора *foo(). Ключевое слово yield может появляться в генераторе
произвольное количество раз (или вообще отсутствовать).
Ничто не мешает поместить ключевое слово yield в тело цикла,
создав повторяющуюся точку останова. При этом в случае с бесконечным циклом вы получите генератор, работа которого никогда не завершается, что иногда бывает вполне допустимо и даже
необходимо.
Ключевое слово yield не просто прерывает работу генератора.
В момент остановки оно посылает наружу значение. Вот пример
цикла while..true внутри генератора, который на каждой итерации
получает новое случайное число:
function *foo() {
while (true) {
yield Math.random();
}
}

Выражение yield .. позволяет не только передать значение (ключевое слово yield, не сопровождающееся ничем, означает yield
undefined), но и получить — то есть заместить — конечное значение
при возобновлении работы генератора. Рассмотрим пример:
function *foo() {
var x = yield 10;
console.log( x );
}

Этот генератор в момент остановки сначала получит значение 10.
После возобновления работы — методом it.next(..) — любое восстановленное значение (если таковое вообще существует) полностью заместит выражение yield 10, то есть именно оно будет присвоено переменной x.
Выражение yield .. может появляться во всех тех местах, куда
вставляют обычные выражения. Например:

144

Глава 3. Структура

function *foo() {
var arr = [ yield 1, yield 2, yield 3 ];
console.log( arr, yield 4 );
}

В данном случае генератор *foo() содержит четыре записи yield ...
Каждая из них останавливает работу генератора и ожидает некого
нового значения, которое затем вставляется в различные контексты
выражения.
Строго говоря, слово yield — не оператор, хотя выражения вида
yield 1 выглядят именно как операторы. Тем не менее оно используется и само по себе, например var x = yield;, поэтому его представление в качестве оператора может привести к путанице.
С технической точки зрения выражение yield .. имеет такой же
приоритет (концептуально в данном случае это соответствует приоритету операторов), как и выражение присваивания, например
a = 3. Таким образом, yield .. может появляться везде, где допустимо выражение a = 3.
Проиллюстрируем эту аналогию:
var a, b;
a = 3;
b = 2 + a = 3;
b = 2 + (a = 3);

// valid
// invalid
// valid

yield 3;
a = 2 + yield 3;
a = 2 + (yield 3);

// valid
// invalid
// valid

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

Если нужно вставить yield .. в место, где недопустимы такие выражения, как a = 3, его помещают в скобки ( ).

145

Генераторы

Из-за низкого приоритета ключевого слова yield практически все
выражения, следующие за yield .., будут вычисляться раньше.
Более низким приоритетом обладают только оператор распределения ... и запятая ,, до них дело доходит после вычисления выражения с yield.
Как и в случае с набором обычных операторов, есть еще один вариант применения скобок ( ) . Они позволяют переопределить
(поднять) низкий приоритет ключевого слова yield, как в случае
с этими двумя выражениями:
yield 2 + 3;

// то же самое, что и 'yield (2 + 3)'

(yield 2) + 3;

// сначала 'yield 2', затем '+ 3'

Подобно оператору присваивания =, ключевое слово yield имеет
правую ассоциативность. Это означает, что набор последовательных
выражений yield рассматривается как сгруппированный с помощью скобок ( .. ) справа налево. То есть yield yield yield 3 трактуется как yield (yield (yield 3)). Трактовка с левой ассоциативностью, то есть ((yield) yield) yield 3 не имеет смысла.
При комбинации ключевого слова yield с какими-либо операторами или с другими ключевыми словами yield разумно прибегнуть
к группировке с помощью скобок ( .. ), чтобы четко обозначить
свои намерения (как вы помните, с другими операторами дело
обстоит так же).
Подробно тема приоритета операторов и ассоциативность рассматриваются в книге Types & Grammar этой серии.

Выражение yield *
Аналогично тому, как символ * превращает объявление функции
в объявление генератора, в случае использования * с ключевым
словом yield образуется совершенно другой механизм, называемый

146

Глава 3. Структура

yield-делегированием. Грамматически выражение yield *.. будет
вести себя так же, как и рассмотренное в предыдущем разделе
yield ...
Выражению yield *.. требуется итерируемый объект; оно вызывает его итератор и передает ему управление собственным генератором. Рассмотрим пример:
function *foo() {
yield *[1,2,3];
}
Как и в случае с объявлением генератора, положение символа *
в выражениях yield * зависит только от ваших стилистических
предпочтений. В других литературных источниках предпочитают
запись yield* .., я же выбрал вариант yield *.. по причинам,
рассматривавшимся в предыдущем разделе.

Значение [1,2,3] даст нам итератор, который будет пошагово передавать свои значения генератору *foo(). Другой способ продемонстрировать такое поведение — сделать yield-делегирование другому генератору:
function *foo() {
yield 1;
yield 2;
yield 3;
}
function *bar() {
yield *foo();
}

Итератор, появившийся при вызове генератором *bar() генератора
*foo(), делегируется при помощи выражения yield *. Это означает,
что все значения, порождаемые генератором *foo(), будут выводиться генератором *bar().

Генераторы

147

Если завершающее значение выражения yield .. появляется при
возобновлении работы генератора методом it.next(..), то завершающее значение выражения yield *.. представляет собой значение, возвращаемое итератором, которому были делегированы
полномочия (если таковое существует).
У встроенных итераторов возвращаемые значения, как правило,
отсутствуют, как было показано в конце раздела «Цикл итератора».
Но собственноручно созданный вами итератор (или генератор)
можно заставить возвращать значение, которым в конечном итоге
и воспользуется выражение yield *..:
function *foo() {
yield 1;
yield 2;
yield 3;
return 4;
}
function *bar() {
var x = yield *foo();
console.log( "x:", x );
}
for

(var v of bar()) {
console.log( v );

}
// 1 2 3
// x: 4

Если значения 1, 2 и 3 отданы генератором *foo(), а затем генератором *bar(), то возвращенное генератором *foo() значение 4 является завершающим для выражения yield *foo() и присваивается
переменной x.
Выражение yield * может не только вызвать еще один генератор
(путем делегирования его итератору), но и породить рекурсию
генератора, вызывая само себя:

148

Глава 3. Структура

function *foo(x) {
if
(x < 3) {
x = yield *foo( x + 1 );
}
return x * 2;
}
foo( 1 );

В результате foo(1) и последующего вызова метода next() итератора для прохода через этапы рекурсии мы получим 24. При
первом запуске генератора *foo(..) переменная x имеет значение 1, то есть соблюдается условие x < 3. Выражение x + 1 рекурсивно передается генератору *foo(..) , соответственно, x при­
обретает значение 2. Следующий рекурсивный вызов дает
переменной x значение 3.
После этого условие x < 3 перестает выполняться, и рекурсия прекращается, а оператор return 3 * 2 возвращает выражению yield *..
из предыдущего вызова значение 6, которое, в свою очередь, присваивается переменной x. Следующий оператор return 6 * 2 возвращает предыдущему вызову x значение 12. Наконец, после завершающего этапа работы генератора *foo(..) возвращается
значение 12 * 2, то есть 24.

Контроль со стороны итератора
Выше уже упоминалось, что генераторы управляются итераторами.
Давайте подробно разберем этот процесс.
Для примера рассмотрим рекурсивную форму генератора *foo(..)
из предыдущего раздела. Вот как она действует:
function *foo(x) {
if
(x < 3) {
x = yield *foo( x + 1 );
}
return x * 2;

149

Генераторы

}
var it = foo( 1 );
it.next();

// { value: 24, done: true }

В этом случае генератор вообще не останавливает свою работу, так
как выражение yield .. отсутствует. Вместо него в коде имеется
выражение yield *, которое обеспечивает прохождение каждой
итерации путем рекурсивного вызова. Так что работа генератора
осуществляется исключительно вызовами функции next() итератора.
Теперь рассмотрим генератор, имеющий несколько этапов и соответственно дающий несколько значений:
function *foo() {
yield 1;
yield 2;
yield 3;
}

Мы уже знаем, что воспользоваться итератором, даже если тот присоединен к генератору *foo(), можно с помощью цикла for..of:
for (var v of foo()) {
console.log( v );
}
// 1 2 3
Для цикла for..of необходим итерируемый объект. Сама по себе
ссылка на функцию генератора (например, foo) таковым считаться
не может; для получения итератора ее следует выполнить как foo()
(при этом, как говорилось выше, такой итератор сам является итерируемым). Теоретически можно расширить GeneratorPrototype
(прототип всех функций генератора) функцией Symbol.iterator,
которая, по сути, всего лишь возвращает this(). Это сделает
ссылку foo итерируемой, что обеспечит работу цикла for (var v
of foo) { .. } (обратите внимание на отсутствие скобок () у функции foo).

150

Глава 3. Структура

Попробуем выполнить итерации генератора вручную:
function *foo() {
yield 1;
yield 2;
yield 3;
}
var it = foo();
it.next();
it.next();
it.next();

// { value: 1, done: false }
// { value: 2, done: false }
// { value: 3, done: false }

it.next();

// { value: undefined, done: true }

Мы видим, что на три оператора yield приходится четыре вызова
метода next(). Такое несовпадение может показаться странным.
Однако количество вызовов метода next() всегда на единицу превышает количество выражений yield, тем самым давая возможность сделать все вычисления и позволить генератору отработать
до конца.
Впрочем, если смотреть с другой стороны (изнутри, а не снаружи),
более осмысленным кажется равное число yield и next().
Напомню, что выражение yield .. завершается значением, которое
используется в момент возобновления работы генератора. Таким
образом, передаваемый в метод next(..) аргумент завершает выражение, приостановленное в текущий момент.
Проиллюстрируем это следующим фрагментом кода:
function *foo() {
var x = yield 1;
var y = yield 2;
var z = yield 3;
console.log( x, y, z );
}

151

Генераторы

Здесь каждое выражение yield .. посылает значение (1, 2, 3), точнее, останавливает генератор, чтобы он дождался значения. Это
напоминает обращение: «Дай знать, что мне тут следует использовать, я пока подожду».
Вот каким способом мы заставляем генератор *foo() начать работу:
var it = foo();
it.next();

// { value: 1, done: false }

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

Попробуем ответить на пока еще открытый вопрос: «Какое значение следует присвоить переменной x?» Для этого отправим значение следующему вызываемому методу next(..):
it.next( "foo" );

// { value: 2, done: false }

Теперь переменная x имеет значение "foo", но возникает новый
вопрос: «Что присвоить переменной y?» Вот что мы ответим:
it.next( "bar" );

// { value: 3, done: false }

Ответ дан, следующий вопрос сформулирован. Окончательный
ответ будет таким:
it.next( "baz" );

// "foo" "bar" "baz"
// { value: undefined, done: true }

152

Глава 3. Структура

Теперь, когда стало видно, что на каждый вопрос выражения yield ..
отвечает следующий вызов метода next(..), легко понять, что наблюдаемый нами «лишний» вызов метода next() — это самая первая операция, которая запускает работу генератора.
Объединим все шаги:
var it = foo();
// запускает генератор
it.next();

// { value: 1, done: false }

// отвечает на первый вопрос
it.next( "foo" );
// { value: 2, done: false }
// отвечает на второй вопрос
it.next( "bar" );
// { value: 3, done: false }
// отвечает на третий вопрос
it.next( "baz" );
// "foo" "bar" "baz"
// { value: undefined, done: true }

Генератор можно представить как поставщик значений, в этом
случае каждая итерация просто создает значение для дальнейшего
использования.
Но в более общем смысле, возможно, корректнее будет рассматривать генераторы как управляемое поэтапное выполнение кода, во
многом напоминающее очередь задач, рассмотренную в разделе
«Пользовательские итераторы».
Изложенная выше точка зрения — это повод снова вернуться к рассмотрению генераторов в главе 4. В частности, нет поводов вызывать очередной метод next(..) сразу же после завершения
работы предыдущего. В то время, когда внутренний контекст генератора приостановлен, прочие части программы продолжают выполняться, предоставляя в числе прочего возможность для управления асинхронными действиями при возобновлении работы
генератора.

153

Генераторы

Раннее завершение
Как уже упоминалось, присоединенный к генератору итератор
поддерживает необязательные методы return(..) и throw(..) — оба
немедленно прерывают работу приостановленного генератора.
Рассмотрим пример:
function *foo() {
yield 1;
yield 2;
yield 3;
}
var it = foo();
it.next();

// { value: 1, done: false }

it.return( 42 );

// { value: 42, done: true }

it.next();

// { value: undefined, done: true }

Метод return(x) как бы вынуждает немедленно выполнить оператор return x, и вы сразу получаете указанное значение. Генератор,
чья работа была завершена обычным образом или преждевременно,
как показано в примере, больше ничего не делает ни с каким кодом
и не возвращает никаких значений.
Метод return(..) может вызываться не только вручную, но и автоматически в конце итераций. В последнем случае его вызывает
компонент, использующий итератор, например цикл for..of или
оператор разделения ....
Эта функциональная особенность позволяет уведомить генератор,
что контролирующий код прекратил выполнять перебор и можно
приступить к задачам, связанным с очисткой (освобождением ресурсов, сбросом состояния и т. п.). Данная задача, аналогично
обычному шаблону очистки функции, в основном решается с помощью оператора finally:

154

Глава 3. Структура

function *foo() {
try
{
yield 1;
yield 2;
yield 3;
}
finally {
console.log( "cleanup!" );
}
}
for

(var v of foo()) {
console.log( v );

}
// 1 2 3
// очистка!

var it = foo();
it.next();
it.return( 42 );

// { value: 1, done: false }
// очистка!
// { value: 42, done: true }

Помещать оператор yield внутрь оператора finally нельзя! Формально такое допустимо, но делать этого не стоит. В определенном
смысле finally действует как отложенное завершение вызванного вами метода return(..), потому что любое выражение yield ..
в операторе finally будет прерывать его работу и отправлять
сообщения; вы не получите немедленно завершающийся генератор,
как ожидаете. Нет разумных причин использовать настолько сумасшедший вариант, поэтому избегайте его!

Представленный выше фрагмент не только показывает, как метод
return(..) прерывает работу генератора, запуская оператор finally,
но и демонстрирует, что при каждом вызове генератор формирует
новый итератор. Более того, вы можете использовать несколько
итераторов, одновременно присоединенных к одному и тому же
генератору:

155

Генераторы

function *foo() {
yield 1;
yield 2;
yield 3;
}
var it1 = foo();
it1.next();
it1.next();

// { value: 1, done: false }
// { value: 2, done: false }

var it2 = foo();
it2.next();

// { value: 1, done: false }

it1.next();

// { value: 3, done: false }

it2.next();
it2.next();

// { value: 2, done: false }
// { value: 3, done: false }

it2.next();
it1.next();

// { value: undefined, done: true }
// { value: undefined, done: true }

Раннее прерывание
Вместо метода return(..) можно вызвать метод throw(..). Аналогично тому, как метод return(x), по сути, представляет собой оператор return x, вставленный в текущую точку останова генератора,
вызов метода throw(x) вставляет туда оператор throw x.
Помимо генерации исключений (операторы try будут рассмотрены
в следующем разделе), метод throw(..) выполняет раннее завершение, прерывая работу генератора в текущей точке останова. Например:
function *foo() {
yield 1;
yield 2;
yield 3;
}
var it = foo();
it.next();

// { value: 1, done: false }

156
try {
it.throw( "Oops!" );
}
catch (err) {
console.log( err );
}
it.next();

Глава 3. Структура

// Исключение: Ой!
// { value: undefined, done: true }

Так как метод throw(..), по сути, вставляет вместо строчки yield 1
оператор throw .., и при этом обработчик исключения отсутствует,
исключение немедленно возвращается к вызывающему коду, который использует блок try..catch.
В отличие от метода return(..), метод итератора throw(..) автоматически никогда не вызывается.
В представленном фрагменте кода этого не показано, но если блок
try..finally в момент вызова метода throw(..) ожидал внутри
генератора, оператор finally получает шанс завершить работу до
момента, когда исключение вернется в вызывающий код.

Обработка ошибок
Я уже упоминал, что в случае с генераторами обработка ошибок
реализуется при помощи блока try..catch, который работает как
во входящем, так и в исходящем направлении.
function *foo() {
try
{
yield 1;
}
catch (err) {
console.log( err );
}
yield 2;
throw "Hello!";

157

Генераторы

}
var it = foo();
it.next();
try

{
it.throw( "Hi!" );
it.next();

// { value: 1, done: false }
// Hi!
// { value: 2, done: false }

console.log( "never gets here" );
}
catch (err) {
console.log( err );
// Hello!
}

Ошибки также могут распространяться в обоих направлениях через
делегат yield *:
function *foo() {
try
{
yield 1;
}
catch (err) {
console.log( err );
}
yield 2;
}

throw "foo: e2";

function *bar() {
try
{
yield *foo();
console.log( "never gets here" );
}
catch (err) {
console.log( err );
}

158

Глава 3. Структура

}
var it = bar();
try

{
it.next();
it.throw( "e1" );
it.next();

// { value: 1, done: false }
// e1
// { value: 2, done: false }
// foo: e2
// { value: undefined, done: true }

}
catch (err) {
console.log( "never gets here" );
}
it.next();

// { value: undefined, done: true }

Вы уже видели, что когда генератор *foo() вызывает оператор
yield 1, значение 1 проходит через генератор *bar() без изменений.
Впрочем, самое интересное в этом фрагменте то, что при вызове
генератором *foo() строчки throw "foo: e2" ошибка переходит в генератор *bar() и немедленно перехватывается вставленным туда
блоком try..catch. Ей не удается беспрепятственно пройти через
*bar(), как это делает значение 1.
Оператор catch внутри генератора *bar() выполняет обычный вывод err ("foo: e2"), после чего генератор обычным образом завершает свою работу. Вот почему от метода it.next() приходит результат итератора { value: undefined, done: true }.
Если бы в *bar() возле выражения yield *.. отсутствовал блок try..
catch, ошибка, разумеется, проходила бы генератор насквозь, и при
этом все равно требовалось бы завершить его работу.

Транскомпиляция генератора
Можно ли было представить что-нибудь подобное генератору в JS
до появления стандарта ES6? Оказывается, генераторы существо-

Генераторы

159

вали и в более ранних версиях, мало того, есть несколько хороших
инструментов для их реализации, в том числе такие серьезные, как
Regenerator от Facebook (https://facebook.github.io/regenerator/).
Чтобы лучше понять, что представляют собой генераторы, да­
вайте попробуем выполнить преобразование вручную. По сути,
мы собираемся создать простой конечный автомат на базе замыкания.
Исходный генератор будет очень простым:
function *foo() {
var x = yield 42;
console.log( x );
}

Для начала нам потребуется функция foo(), которую мы можем
выполнять и которая должна возвращать итератор.
function foo() {
// ..
return {
next: function(v) {
// ..
}

}

};

// пропустим методы 'return(..)' и 'throw(..)'

Теперь нужна внутренняя переменная для отслеживания нашего
положения в логической схеме этого «генератора». Назовем ее
state. У нас будет три состояния: 0 — изначальное, 1 — в ожидании
завершения выражения yield и 2 — после завершения работы генератора.
Нам нужно, чтобы при каждом вызове метода next(..)обрабатывался следующий шаг и происходило инкрементирование состояния. Для удобства мы поместим каждый шаг в оператор case блока

160

Глава 3. Структура

switch , и все это будет расположено в теле внешней функции
nextState(..), доступной для вызова методу next(..). Так как переменная x находится в области видимости нашего «генератора», ее
следует поместить за пределами функции nextState(..).

Вот как все это будет выглядеть (разумеется, в упрощенном виде,
позволяющем наглядно продемонстрировать принцип преобразования):
function foo() {
function nextState(v) {
switch (state) {
case 0:
state++;
// выражение 'yield'
return 42;
case 1:
state++;
// выражение 'yield' выполнено
x = v;
console.log( x );
// неявный оператор 'return'
return undefined;
}

// не нужно обрабатывать состояние '2'

}
var state = 0, x;
return {
next: function(v) {
var ret = nextState( v );
}

}

};

return { value: ret, done: (state == 2) };

// пропустим методы 'return(..)' и 'throw(..)'

161

Генераторы

Теперь давайте протестируем наш «генератор», работающий в стандартах, предшествующих ES6:
var it = foo();
it.next();

// { value: 42, done: false }

it.next( 10 );

// 10
// { value: undefined, done: true }

Неплохо, не так ли? Я надеюсь, упражнение позволило вам осо­
знать, что генераторы — это всего лишь простой синтаксис для
логической схемы конечного автомата, что и делает их широко
применимыми.

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

162

Глава 3. Структура

Очередь задач для последовательного выполнения
Этот вариант применения часто представляет собой управление шагами какого-нибудь алгоритма, причем на каждом
из них требуется извлекать данные из некого внешнего источника — немедленно или с асинхронной задержкой.
С точки зрения кода внутри генератора детали реализации
синхронного или асинхронного извлечения данных в точке
оператора yield совершенно непрозрачны. Более того, они
намеренно сделаны абстрактными, чтобы не прятать естественное последовательное выражение шагов за сложностями реализации. Кроме того, абстрагирование дает возможность часто менять или перерабатывать реализацию, никак
не затрагивая код внутри генератора.
Если смотреть на генераторы с точки зрения их применения, они
перестают быть всего лишь красивым синтаксисом для конечного
автомата с ручным управлением. Генераторы — это мощный инструмент абстрагирования для систематизации и контроля за
упорядоченным созданием и использованием данных.

Модули
Не будет преувеличением предположить, что наиболее важным
способом структуризации кода в JavaScript всегда считался модуль.
Для меня и едва ли не для всего сообщества разработчиков шаблон
«модуль» обусловливает большую часть кода.

Старый способ
Традиционно шаблон «модуль» базировался на внешней функции
с внутренними переменными и функциями, возвращающей «открытый API» с методами, которые замыкались на внутренних
данных и функциональных особенностях. Зачастую это выражалось
следующим образом:

163

Модули

function Hello(name) {
function greeting() {
console.log( "Hello " + name + "!" );
}

}

// открытый API
return {
greeting: greeting
};

var me = Hello( "Kyle" );
me.greeting();

// Hello Kyle!

Модуль Hello(..) при вызове несколько раз подряд позволяет генерировать набор экземпляров. Иногда модуль необходим как
синглтон (то есть нужен всего один экземпляр). В этом случае повсеместно применяется вариация предыдущего фрагмента с использованием IIFE:
var

me = (function Hello(name){
function greeting() {
console.log( "Hello " + name + "!" );
}

// открытый API
return {
greeting: greeting
};
})( "Kyle" );
me.greeting();

// Hello Kyle!

Это проверенный временем шаблон. Он достаточно гибок и допускает широкий диапазон вариаций, подходящих к различным
сценариям.
Наиболее распространены асинхронное (AMD — asynchronous
module definition) и универсальное определения модуля (UMD —
universal module definition). Детали реализации этих шаблонов

164

Глава 3. Структура

и техник мы рассматривать не будем, так как они подробно описаны в многочисленных источниках, доступных в Сети.

Продвигаясь вперед
В ES6 модули получили первоклассную синтаксическую и функциональную поддержку, для их реализации уже не требуется опираться на внешнюю функцию и замыкание.
Но перед тем как мы углубимся в детали синтаксиса, нужно понять
несколько крайне важных различий между модулями в ES6 и в предыдущих версиях языка.
 Стандарт ES6 использует модули на основе файлов: один

модуль содержится в одном файле. На текущий момент стандартизованный способ объединения в один файл нескольких
модулей отсутствует.
Все это означает, что в веб-приложение в браузере их потребуется загружать по отдельности, а не в виде большой совокупности внутри единого файла, как было принято ранее для
оптимизации производительности.
Ожидается, что HTTP/2 значительно смягчит проблемы
с производительностью, так как он поддерживает постоянное
соединение через сокеты и позволяет оперативно загружать
много небольших файлов как параллельно, так и один за
другим.
 Модули ES6 обладают статическим API: вы статически опре-

деляете все виды экспорта верхнего уровня на открытом API
вашего модуля, и изменить это потом уже будет нельзя.
Есть варианты применения, адаптированные к динамическому API, методы которого можно добавлять/удалять/заменять, подстраиваясь под ситуацию в исполняемом коде. Вам
придется или менять эти варианты, подгоняя их под стати-

Модули

165
ческие API в ES6, или ограничивать динамические изменения
свойствами/методами объекта второго уровня.

 Модули ES6 являются синглтонами: существует только один

экземпляр, поддерживающий состояние модуля. Каждый раз,
импортируя его в другой модуль, вы получаете ссылку на
единственный экземпляр. Для получения набора экземпляров модуль должен предоставить своего рода фабрику.
 Свойства и методы, доступные через открытый API модуля,

нельзя рассматривать как обычные присваивания значений
и ссылки. На самом деле это привязки (почти как указатели)
к идентификаторам во внутреннем определении модуля.
В случае с модулями, которые существовали до ES6, если
свойство, содержащее примитивное значение (например,
число или строку), помещалось в открытый API, присваивание этого свойства выполнялось копированием значения,
а все внутренние обновления соответствующей переменной
производились отдельно и не влияли на открытую копию
в объекте API.
В ES6 при экспорте локальной закрытой переменной, даже
если в ней в этот момент хранится примитивная строка, число или что-нибудь подобное, экспортируется привязка к переменной. Если модуль меняет значение переменной, внешняя привязка импорта получает это новое значение.
 Импорт модуля аналогичен статическому запросу на его за-

грузку (если она еще не сделана). В браузере это предполагает блокирующую загрузку. Если вы находитесь на сервере
(например, Node.js), аналогичная загрузка будет осуществ­
ляться из файловой системы.
Не стоит паниковать по поводу проблем с производитель­
ностью. Благодаря тому что у модулей ES6 есть статические
определения, требования к импорту могут быть статически

166

Глава 3. Структура

просканированы, и загрузка произойдет заранее, до того как
вы воспользуетесь модулем.
Однако ES6 не определяет и не обрабатывает механизм работы этих запросов на загрузку. Существует отдельное понятие загрузчика модулей, и в каждой среде выполнения
(браузер, Node.js и т. п.) есть свой загрузчик по умолчанию.
Для импорта модуля используется строковое значение, указывающее, где его можно получить (адрес URL, путь к файлу
и т. п.), но в программе оно непрозрачно и имеет смысл только для загрузчика.
Вы можете определить собственный загрузчик, если вам
нужен более полный контроль над процессом, чем тот, который предоставляет загрузчик по умолчанию, — в последнем
случае контроль, по сути, отсутствует, так как все настройки
полностью скрыты от кода программы.
Как видите, модули ES6 служат общей цели структуризации кода
посредством инкапсуляции, контроля открытых API и ссылок на
импорт зависимостей. Однако все это реализуется особым способом, скорее всего, непривычным для вас.

Модули CommonJS
Существует похожий, но не полностью совместимый со стандартом
синтаксис модулей, который называется CommonJS. Он знаком
тем, кто работает в экосистеме Node.js.
Предполагается, что в долгосрочной перспективе модули ES6 заместят собой все предшествующие форматы и стандарты, даже
CommonJS, так как базируются на синтаксической поддержке
в языке. Со временем этот подход неизбежно станет превалирующим, хотя бы по причине своего широкого распространения.
Однако то положение дел, которое мы имеем сейчас, складывалось
долгое время. Существует добрая сотня тысяч модулей CommonJS

167

Модули

на серверной стороне и в десять раз больше модулей в различных
форматах (UMD, AMD и пр.) на стороне браузера. Поэтому значительных изменений стоит ждать лишь по прошествии многих лет.
А пока, на нынешней переходной стадии, нам не обойтись без
транскомпиляторов/преобразователей для модулей. Я допускаю,
что вы уже привыкли к этой новой реальности и, будучи автором
обычных модулей, AMD, UMD, CommonJS или ES6, учитываете
тот факт, что они должны уметь анализировать среду, в которой
им предстоит запускаться, и преобразовываться в подходящий
формат.
Для среды Node.js это, скорее всего, означает CommonJS, для браузеров — UMD или AMD. В ближайшие несколько лет следует
ожидать появления множества новых модулей, так как будет происходить их совершенствование и поиск наилучших подходов.
Поэтому вот лучшее, что я могу вам сейчас посоветовать: к какому
бы формату вы ни были привязаны, начинайте изучать модули ES6
и воспринимать их как есть. Пусть все прочие тенденции постепенно сойдут на нет. Эти модули — будущее JS, даже если реальность
пойдет немного по другому пути.

Новый способ
Два основных ключевых слова, активирующие модули ES6, —
import и export. Этот синтаксис имеет множество нюансов, потому
рассмотрим его более подробно.
Эту важную деталь легко упустить из виду: ключевые слова import
и export всегда должны появляться на верхнем уровне области
видимости, в которой будут применяться. Например, их нельзя
вставлять в условный оператор if; они должны располагаться вне
блоков и функций.

168

Глава 3. Структура

Экспорт членов API
Ключевое слово export или помещается перед объявлением, или
используется в качестве своего рода оператора со специальным
списком привязок, предназначенных для экспорта.
Например:
export function foo() {
// ..
}
export var awesome = 42;
var bar = [1,2,3];
export { bar };

Другой способ реализации этого же экспорта:
function foo() {
// ..
}
var awesome = 42;
var bar = [1,2,3];
export { foo, awesome, bar };

Все это называется именованным экспортом (named exports), так
как вы, по сути, экспортируете привязки имени переменных, функций и пр.
Все, что вы не помечаете ключевым словом export, остается закрытым внутри области видимости модуля. То есть, несмотря на
то что запись var bar = .. выглядит как объявление на верхнем
уровне глобальной области видимости, на самом деле верхний
уровень здесь — сам модуль; в модулях глобальной области видимости просто нет.

169

Модули

Вдействительности у модулей есть доступ к объекту window и всем
связанным с ним «глобальным» вещам, но не как к лексической
области видимости верхнего уровня. Тем не менее следует прилагать все усилия, чтобы в модулях не было никаких глобальных
переменных.

В процессе именованного экспорта можно переименовать член
модуля (это явление известно как псевдоним):
function foo() { .. }
export { foo as bar };

Здесь при импорте доступным будет только член bar, а член foo
останется скрытым внутри модуля.
Экспорт модулей отличается от обычного присваивания значений
и ссылок, которое вы привыкли выполнять при помощи оператора =.
На самом деле при экспорте какого-либо элемента вы передаете
привязку (своего рода указатель).
Если вы поменяете внутри модуля значение переменной, привязку
к которой уже экспортировали, то, даже если ее импорт уже завершился (см. следующий раздел), импортированная привязка даст
текущее (обновленное) значение.
Рассмотрим пример:
var awesome = 42;
export { awesome };
// позднее
awesome = 100;

Когда модуль уже импортирован, не имеет значения, до или после
этого было выполнено присваивание awesome = 100. По завершении
импортированная привязка начнет давать нам значение 100, а не 42.

170

Глава 3. Структура

Она ведь, по сути, представляет собой ссылку или указатель на
саму переменную awesome, а не на копию ее значения. Эта концепция,
появившаяся с введением привязок модулей в ES6, — одна из самых
примечательных в JS.
Хотя внутри определения модуля ничто не запрещает несколько
раз использовать ключевое слово export, в ES6 более предпочтителен подход, при котором оно существует в единственном экземпляре. Это так называемый экспорт по умолчанию (default export). По
мнению некоторых членов комитета TC39, если следовать этому
шаблону, можно получить более простой синтаксис.
Экспорт по умолчанию задает конкретную экспортированную
привязку как вариант по умолчанию, выбираемый при импорте
модуля. Вот почему этой привязке дали имя default. Позднее вы
увидите, что во время импорта привязок модуля их можно переименовывать, и будете часто делать это в случае экспорта по
умолчанию.
Возможна только одна привязка default на одно определение модуля. Импорт мы будем рассматривать в следующем разделе, и вы
убедитесь, насколько сокращается синтаксис в случае экспорта
модуля по умолчанию.
Здесь есть нюанс, на который следует обратить внимание. Давайте
сравним два фрагмента. Первый:
function foo(..) {
// ..
}
export default foo;

Второй:
function foo(..) {
// ..
}
export { foo as default };

171

Модули

В первом случае вы экспортируете привязку в значение функционального выражения, а не в идентификатор foo. Другими словами,
export default .. преобразовывает выражение. Если позднее присвоить функцию foo другому значению внутри модуля, процедура
импорта все равно будет показывать изначально экспортированную
функцию.
Кстати, первый фрагмент можно переписать еще и таким образом:
export default function foo(..) {
// ..
}
Хотя с технической точки зрения function foo.. в данном случае —
функциональное выражение, в контексте внутренней области видимости модуля эта запись рассматривается как объявление функции, в котором имя foo связано с верхним уровнем области
видимости модуля (это часто называют «поднятием»). То же самое
верно для записи export default class Foo... Но если написать
export var foo = .. вы можете, то export default var foo = ..
(или let, или const) — нет, что являет собой крайне огорчительный
пример несогласованности. На момент написания книги это уже
обсуждается, и, возможно, ситуация будет исправлена в следующей
версии стандарта JS.

Вернемся ко второму фрагменту:
function foo(..) {
// ..
}
export { foo as default };

Здесь привязка экспорта по умолчанию на самом деле выполняется к идентификатору foo, а не к его значению, то есть вы получаете
ранее описанное поведение (в частности, если позднее вы поменяете значение foo, обновится и значение на стороне импорта).

172

Глава 3. Структура

Будьте крайне осторожны с этим небольшим подводным камнем
в синтаксисе экспорта по умолчанию, особенно если логическая
схема требует обновлять экспортируемые значения. Если вы не
планируете этого делать, прекрасно подойдет вариант export
default ... В противном случае следует пользоваться вариантом
export { .. as default }. И не забудьте снабдить ваш код комментариями, поясняющими ваши намерения!
Так как внутри модуля должно быть только одно ключевое слово
default, возникает соблазн создать модуль с одним экспортом по
умолчанию и экспортировать объект со всеми вашими методами API, например:
export default {
foo() { .. },
bar() { .. },
..
};

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

173

Модули

export default function foo() { .. }
foo.bar = function() { .. };
foo.baz = function() { .. };

Следует написать вот что:
export default function foo() { .. }
export function bar() { .. }
export function baz() { .. }
В предыдущем фрагменте имя foo использовалось для функции,
помеченной словом default. Однако в контексте экспорта имя foo
игнорировалось — экспортировано было имя default. При импорте этой привязки по умолчанию ей можно дать любое имя по вашему выбору, в чем вы убедитесь, читая следующий раздел.

В качестве альтернативы некоторые предпочитают такой вариант:
function foo() { .. }
function bar() { .. }
function baz() { .. }
export { foo as default, bar, baz, .. };

Эффект комбинации экспорта по умолчанию с именованным экспортом станет более понятным, когда мы начнем рассматривать
импорт. Но, по сути, это означает, что самая краткая форма импорта по умолчанию будет извлекать только функцию foo(). Пользователь может дополнительно вручную указать в качестве элементов
именованного импорта функции bar и baz, если они ему нужны.
Представьте себе, сколь утомительным будет использование модуля при наличии многочисленных именованных привязок экспорта!
Существуют шаблон импорта с символом обобщения, в котором
все, что было экспортировано из модуля, импортируется внутри
единого объекта пространства имен, но подобный импорт невозможен в привязки верхнего уровня.
Мы снова возвращаемся к тому, что данный механизм в ES6 специально разрабатывался таким образом, чтобы затруднить использо-

174

Глава 3. Структура

вание модулей с большим количеством экспортируемых элементов
и поощрить проектирование простых модулей.
Я рекомендую избегать комбинаций экспорта по умолчанию и именованного экспорта, особенно при наличии большого API и ситуации, когда реструктуризация кода с целью разделения модулей
непрактична или нежелательна. В этом случае достаточно воспользоваться именованным экспортом и документировать тот факт, что
пользователи вашего модуля должны придерживаться подхода
import * as .. (о нем пойдет речь в следующем разделе), чтобы
импортировать API целиком в едином пространстве имен.
Выше это уже упоминалось, но давайте рассмотрим этот момент
еще раз, более детально. Помимо формы export default ..., экспортирующей выражение со значением привязки, все остальные
экспортируют привязки к локальным идентификаторам. В этом
случае изменение значения переменной внутри модуля после экспорта приводит к тому, что внешняя импортированная привязка
обращается к обновленному значению:
var foo = 42;
export { foo as default };
export var bar = "hello world";
foo = 10;
bar = "cool";

При импорте этого модуля результаты экспорта функций default
и bar будут привязаны к локальным переменным foo и bar, то есть
покажут обновленные значения 10 и "cool" независимо от того,
какими они были на момент экспорта и на момент импорта. Привязки — это живые ссылки, потому важно только их значение на
момент обращения к ним.
Двусторонняя привязка недопустима. Если после импорта из модуля переменной foo вы попытаетесь поменять значение импортированной переменной, появится сообщение об ошибке. Мы еще поговорим об этом в следующем разделе.

175

Модули

Можно повторно экспортировать элементы, экспортированные из
другого модуля, например:
export { foo, bar } from "baz";
export { foo as FOO, bar as BAR } from "baz";
export * from "baz";

Эти формы — аналог импорта из модуля "baz" и составления списка членов для экспорта из вашего модуля. Однако в приведенном
примере члены модуля "baz" никогда не будут импортированы
в локальную область видимости вашего модуля; они как бы пройдут насквозь без каких-либо изменений.

Импорт членов API
Для импорта модулей очевидным образом используется ключевое
слово import. Этот процесс, как и экспорт, имеет несколько вариаций, так что не поленитесь как следует изучить следующий материал и поэкспериментировать с собственным кодом.
Для импорта конкретных именованных членов API модуля в область
видимости верхнего уровня применяется следующий синтаксис:
import { foo, bar, baz } from "foo";

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

Строка "foo" называется спецификатором модуля (module specifier).
Так как нам нужен статически анализируемый синтаксис, роль
спецификатора играет строковый литерал; здесь нельзя использовать переменную, содержащую строковое значение.
С точки зрения кода ES6 и движка JS содержимое строкового литерала непрозрачно и бессмысленно. Загрузчик модуля интерпре-

176

Глава 3. Структура

тирует эту строку как инструкцию по поиску модуля, содержащую
или адрес URL, или маршрут к файлу в локальной файловой системе.
Перечисленные здесь идентификаторы foo, bar и baz должны совпадать с элементами именованного экспорта API модуля (применяется статический анализ и утверждение ошибки). В текущей
области видимости они связаны как идентификаторы верхнего
уровня:
import { foo } from "foo";
foo();

Вы можете переименовать импортированные связанные идентификаторы:
import { foo as theFooFunc } from "foo";
theFooFunc();

Если модуль обладает только результатами экспорта по умолчанию,
которые вы хотите импортировать и привязать к идентификатору,
можно по вашему выбору опустить { .. } для этого связывания.
Тогда синтаксис примет самую приятную и краткую форму:
import foo from "foo";
// или:
import { default as foo } from "foo";

Как объяснялось в предыдущем разделе, ключевое слово default
в процедуре экспорта модуля задает именованный экспорт, в котором фигурирует имя default, как это показано во втором, более
многословном варианте. Изменение имени с default на другое
(в данном случае на foo) в явном виде выполняется в последнем
фрагменте и неявно в первом.

Модули

177

Кроме того, при наличии у модуля соответствующего определения
можно импортировать результаты экспорта по умолчанию вместе
с результатами именованного экспорта. Вспомним это уже встречавшееся нам определение модуля:
export default function foo() { .. }
export function bar() { .. }
export function baz() { .. }

Давайте импортируем результаты экспорта по умолчанию этого
модуля и два результата именованного экспорта.
import FOOFN, { bar, baz as BAZ } from "foo";
FOOFN();
bar();
BAZ();

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

178

Глава 3. Структура

пространство имен вместо импорта отдельных членов в область
видимости. К счастью, у оператора import есть синтаксическая
вариация, поддерживающая такой стиль работы с модулем. Она
называется импортом пространства имен (namespace import).
Допустим, модуль "foo" экспортируется следующим образом:
export function bar() { .. }
export var x = 42;
export function baz() { .. }

Вы можете импортировать API целиком в единую привязку к пространству имен модуля:
import * as foo from "foo";
foo.bar();
foo.x;
foo.baz();

// 42

Оператор * as .. требует группового символа *. Другими словами,
нельзя, к примеру, написать import { bar, x } as foo from "foo"
для извлечения некоторой части API, оставаясь привязанным к пространству имен foo. Я хотел бы, чтобы подобная возможность была,
но в ES6 импорт в пространство имен происходит по принципу «всё
или ничего».

Если модуль, который импортируется с помощью выражения
* as .., обладает результатом экспорта по умолчанию, этот результат в указанном пространстве имен называется default. Можно
дополнительно именовать импорт по умолчанию извне привязки
пространства имен как идентификатор верхнего уровня. Рассмотрим модуль "world", экспортированный следующим образом:
export default function foo() { .. }
export function bar() { .. }
export function baz() { .. }

179

Модули

А это процедура импорта:
import foofn, * as hello from "world";
foofn();
hello.default();
hello.bar();
hello.baz();

Это корректный синтаксис, но ситуацию запутывает тот факт, что
один из методов модуля (экспорт по умолчанию) привязан к верхнему уровню области видимости, в то время как остальные результаты именованного экспорта (и один с именем default) привязаны
как свойства к идентификатору пространства имен с другим названием (hello).
Я уже говорил, что лучше избегать такого способа проектирования
экспорта модулей, чтобы пользователям не приходилось иметь дело
с подобными странными вещами.
Все импортированные привязки неизменяемы и/или предназначены только для чтения. Рассмотрим результат предыдущего импорта (все последующие попытки присваивания будут приводить
к появлению исключения TypeErrors):
import foofn, * as hello from "world";
foofn = 42;
hello.default = 42;
hello.bar = 42;
hello.baz = 42;

//
//
//
//

(runtime)
(runtime)
(runtime)
(runtime)

TypeError!
TypeError!
TypeError!
TypeError!

Выше, в разделе «Экспорт членов API», мы обсуждали, каким образом привязки bar и baz связаны с реальными идентификаторами
внутри модуля "world". Вы должны помнить, что если модуль поменяет эти значения, hello.bar и hello.baz начнут ссылаться на
обновленные версии.
Локальные импортированные привязки неизменяемы и/или предназначены только для чтения, и при попытке что-нибудь с ними

180

Глава 3. Структура

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

Объявления, возникающие в результате импорта, считаются «приподнятыми» (см. книгу Scope & Closures этой серии). Рассмотрим
пример:
foo();
import { foo } from "foo";

Функцию foo() можно запускать, потому что статическое разрешение оператора import .. не только определило ее во время
компиляции, но и «приподняло» объявление в верхнюю часть
области видимости модуля, сделав эту функцию доступной во
всем модуле.

Модули

181

Наконец, основная форма импорта выглядит следующим образом:
import "foo";

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

Циклическая зависимость модулей
A импортирует B. B импортирует A. Как такое возможно?
Сразу скажу, что я стараюсь избегать проектирования систем с преднамеренной циклической зависимостью. Но я признаю, что это бывает оправданно и порой позволяет выйти из неприятных ситуаций.
Посмотрим, как с такими вещами справляется ES6. Начнем с модуля "A":
import bar from "B";
export default function foo(x) {
if (x > 10) return bar( x - 1 );
return x * 2;
}

А вот модуль "B":
import foo from "A";
export default function bar(y) {
if (y > 5) return foo( y / 2 );
return y * 3;
}

182

Глава 3. Структура

Если бы функции foo(..) и bar(..) находились в одной области
видимости, мы имели бы дело со стандартной процедурой объявления функций. В данном случае объявления «приподняты» в общую область видимости и, соответственно, доступны друг для
друга независимо от порядка разработки.
В случае модулей объявления находятся в разных областях видимости, и для обеспечения работоспособности циклических ссылок
приходится прилагать дополнительные усилия.
Коротко говоря, циклические зависимости импорта проверяются
и разрешаются следующим образом.
 Если первым загружается модуль "А", сначала файл сканиру-

ется и анализируется на предмет результатов экспорта, чтобы
получить возможность зарегистрировать все доступные для
импорта привязки. Затем обрабатывается выражение import
.. from "B", сообщающее, что пришло время извлечь модуль "B".
 После загрузки модуля "B" движок производит такой же
анализ его привязок экспорта, как и в случае "А". Когда дело
доходит до выражения import .. from "A", API модуля "А" уже

известен, поэтому остается удостовериться в корректности
импорта. Когда же становится известен API модуля "B", появляется возможность проверить также и выражение import ..
from "B" в ожидающем модуле "А".
По сути, взаимный импорт вкупе со статической проверкой обоих
операторов импорта виртуально объединяет области видимости
двух модулей (через привязки), так что функция foo(..) может
вызывать функцию bar(..) и наоборот. Это сравнимо с ситуацией,
когда оба модуля изначально объявляются в одной области видимости.
Теперь попробуем использовать два модуля вместе. Начнем
с foo(..):
import foo from "foo";
foo( 25 ); // 11

Модули

183

Теперь попробуем bar(..):
import bar from "bar";
bar( 25 ); // 11.5

К моменту выполнения foo(25) или bar(25) анализ и компиляция
всех модулей завершатся. Это означает, что функция foo(..) знает
о функции bar(..), а той известно о функции foo(..).
Если нам требуется только взаимодействие с функцией foo(..),
достаточно будет импортировать модуль "foo". Аналогичным образом обстоят дела с функцией bar(..) и модулем "bar".
Разумеется, при желании мы можем импортировать обе функции:
import foo from "foo";
import bar from "bar";
foo( 25 ); // 11
bar( 25 ); // 11.5

Семантика статической загрузки оператора import означает, что
зависящие друг от друга модули "foo" и "bar" в результате импорта будут гарантированно загружены, проанализированы и скомпилированы до запуска любого из них. То есть циклическая зависимость разрешается статически, и все работает ожидаемым образом.

Загрузка модуля
В начале раздела «Модули» я уже писал, что оператор import использует предоставляемый средой размещения (браузером, Node.js
и т. п.) механизм для превращения строки спецификатора в инструкцию по поиску и загрузке нужного модуля. Этот механизм
называется системным загрузчиком модуля (module loader).
Загрузчик, по умолчанию предоставляемый средой в браузере,
будет интерпретировать спецификатор модуля как URL-адрес,
а тот, что на сервере, например Node.js (в общем случае), — как

184

Глава 3. Структура

локальный путь в файловой системе. Поведение по умолчанию
предполагает, что загружаемый файл разработан в формате стандартных модулей ES6.
Кроме того, загружать модули в браузер можно при помощи
тега HTML, аналогичного тому, который в настоящее время загружает программы сценариев. На момент написания книги не
совсем понятно, будет ли это тег или тег
. Решение не имеет отношения к ES6, его обсуждение ведется соответствующим комитетом по стандартам.
Независимо от того, какой тег в итоге выберут, он будет использовать загрузчик по умолчанию (или индивидуально настроенный,
который вы предварительно укажете).
Как и теги, используемые в разметке, загрузчик модуля не описан
в ES6. Для этого есть отдельный стандарт (см. http://whatwg.github.io/
loader/), в настоящее время контролируемый группой WHATWG,
устанавливающей стандарты браузеров.
То, о чем вы прочитаете ниже, на момент написания книги было
первым вариантом проектирования API и вполне может поменяться в дальнейшем.

Загрузка модулей извне
Один из вариантов непосредственного взаимодействия с загрузчиком модулей — это ситуация, когда нечто, не являющееся модулем,
должно загрузить модуль. Рассмотрим пример:
// обычный сценарий в браузере загружается через '',
// оператор 'import' здесь недопустим
Reflect.Loader.import( "foo" ) // возвращает обещание для '"foo"'
.then( function(foo){
foo.bar();
} );

185

Модули

Служебная программа Reflect.Loader.import(..) целиком импортирует модуль в именованный параметр (как пространство имен),
аналогично тому, как рассмотренное ранее выражение import * as
foo .. осуществляло импорт в пространство имен.
Служебная программа Reflect.Loader.import(..) возвращает
обещание, которое считается выполненным, когда модуль готов
к работе. Для импорта набора модулей можно объединить обещания
от нескольких вызовов Reflect.Loader.import(..) с помощью
метода Promise.all([ .. ]). Подробно обещания будут рассматриваться в главе 4.

Служебную программу Reflect.Loader.import(..) допускается использовать в реальном модуле, чтобы выполнить динамическую/
условную загрузку там, где оператор import не работает. Например,
можно выбрать для загрузки модуль, содержащий полизаполнение
для функциональной особенности из ES7+, если ее тестирование
покажет, что она не определяется текущим движком.
По соображениям производительности желательно избегать динамической загрузки, так как она затрудняет способность движка JS
запускать раннюю выборку из своего статического анализа.

Специализированная загрузка
Другой случай прямого взаимодействия с загрузчиком модуля —
ситуация, когда вам нужно настроить его поведение посредством
изменения конфигурации или даже переопределения.
На момент написания книги был разработан полизаполнитель для
API загрузчика модуля (см. https://github.com/ModuleLoader/es6-moduleloader). Его характеристики пока немногочисленны и, скорее всего,
со временем поменяются, но мы посмотрим, на что можно в итоге
рассчитывать.

186

Глава 3. Структура

Вызов служебной программы Reflect.Loader.import(..) допускает
второй аргумент, задающий различные варианты настройки задач
импорта/загрузки. Например:
Reflect.Loader.import( "foo", { address: "/path/to/foo.js" } )
.then( function(foo){
// ..
} )

Также ожидается, что возможность настройки будет предоставлена
(с помощью каких-то средств) для подключения к процессу загрузки модуля, в котором после загрузки, но до момента компиляции есть вероятность трансляции/транскомпиляции.
Например, можно загрузить модуль, формат которого пока не совместим с ES6 (например, CoffeeScript, TypeScript, CommonJS,
AMD). На этапе трансляции он будет преобразован в ES6совместимый модуль для дальнейшей обработки движком.

Классы
Практически с момента возникновения языка JavaScript его синтаксис и шаблоны разработки навевали мысли о возможной поддержке классов. Благодаря таким вещам, как операторы new
и instanceof, а также свойству .constructor, возникает соблазн
считать, что в JS существуют классы, скрытые где-то внутри системы прототипов.
Разумеется, «классы» JS не имеют ничего общего с обычными
классами. Все различия хорошо документированы, поэтому я не
буду тратить времени на их описание.
Чтобы лучше изучить шаблоны, с помощью которых в JS имитируются классы, и получить альтернативное представление о прототипах и о том, что называется делегированием, читайте вторую
половину книги this & Object Prototypes этой серии.

187

Классы

Ключевое слово class
Хотя механизм прототипов в JS не умеет работать как традиционные классы, это не уменьшает количество просьб расширить
синтаксическое удобство до такой степени, чтобы представление
о «классах» больше совпадало с реальностью. Давайте рассмотрим
появившееся в ES6 ключевое слово class и связанный с ним механизм.
Эта функциональная особенность появилась после бурных и продолжительных дебатов и стала компромиссом между несколькими
противоположными точками зрения на подход к реализации классов в JS. Большинство разработчиков, мечтавших о полноценных
классах, сочтут новый синтаксис весьма привлекательным, хотя
и обнаружат отсутствие важных составляющих. Но поводов для
волнения тут нет. Комитет TC39 уже работает над дополнительными функциональными особенностями, которые улучшат классы
в следующих версиях стандарта.
В основе механизма классов, появившегося в ES6, лежит ключевое
слово class, идентифицирующее блок, содержимое которого определяет члены прототипа функции. Рассмотрим пример:
class Foo {
constructor(a,b) {
this.x = a;
this.y = b;
}

}

gimmeXY() {
return this.x * this.y;
}

Здесь нужно обратить внимание на следующие вещи.
 Запись class Foo влечет за собой создание (специальной)
функции с именем Foo, во многом напоминающей те, что вы

создавали до появления ES6.

188

Глава 3. Структура

 constructor(..) определяет сигнатуру функции Foo(..) и со-

держимое ее тела.
 Для методов класса применяется тот же самый синтаксис

«кратких методов», что и для объектных литералов. Он обсуждался в главе 2. Сюда же входит краткая форма генератора, которую мы рассматривали выше, а также синтаксис
методов чтения/записи из ES5. Но методы класса не являются перечисляемыми, в то время как методы объекта перечисляемы по умолчанию.
 В отличие от объектных литералов, в теле класса отсутству-

ют запятые, отделяющие члены друг от друга. Более того,
запятые там вообще недопустимы.
Определение синтаксиса для ключевого слова class в предыдущем
фрагменте можно грубо представить как существовавший до ES6
эквивалент, который, скорее всего, вполне знаком тем, кто пользовался в своем коде прототипами.
function Foo(a,b) {
this.x = a;
this.y = b;
}
Foo.prototype.gimmeXY = function() {
return this.x * this.y;
}

Как в предшествующей ES6 форме, так и в новой «класс» допускает создание экземпляров, которые используются точно так, как
следовало бы ожидать:
var f = new Foo( 5, 15 );
f.x;
f.y;
f.gimmeXY();

// 5
// 15
// 75

Обратите внимание! Хотя класс Foo по виду сильно напоминает
функцию Foo(), между ними есть ряд важных отличий.

189

Классы

 Обращение функции Foo(..) к классу Foo должно осуществ­
ляться через оператор new, так как существовавший до ES6
вариант Foo.call( obj ) уже не работает.
 Если функция Foo допускает «поднятие» (см. книгу Scope &
Closures этой серии), для класса Foo такая возможность отсутствует; оператор extends .. задает выражение, которое

нельзя «поднять». Поэтому следует объявить класс, прежде
чем создавать его экземпляры.
 class Foo наверху глобальной области видимости создает
в ней лексический идентификатор Foo, но, в отличие от функции Foo, не создает свойства глобального объекта с таким

именем.
Общепринятый оператор instanceof все еще работает с классами ES6, так как ключевое слово class всего лишь создает функциюконструктор с таким же именем. Однако в ES6 появился способ
настраивать работу оператора instanceof с помощью метода Symbol.
hasInstance (см. раздел «Известные символы» главы 7).
Ключевое слово class можно рассматривать и другим способом, на
мой взгляд, более удобным. Это своего рода макрос, который автоматически заполняет прототип объекта. По вашему желанию он
также связывает соотношение [[Prototype]], когда используется
с ключевым словом extends (см. следующий раздел).
Класс ES6 не является реальной программной единицей, это метаконцепция, которая охватывает существующие программные
единицы, такие как функции и свойства, и связывает их друг
с другом.
Ключевое слово class может не только быть объявлением, но
и находиться в составе выражения, например var x = class Y { .. }.
В основном подобные вещи используются для передачи определения класса (с технической точки зрения самого конструктора)
как аргумента функции или для присваивания его свойству объекта.

190

Глава 3. Структура

Ключевые слова extends и super
Классы ES6 имеют удобный синтаксис для установления делегирующей связи [[Prototype]] между двумя прототипами функций,
часто неправильно называемой «наследованием» или (по непонятной причине) «наследованием прототипов». Для этого используется ориентированная на классы терминология, с которой вы уже
знакомы, — ключевое слово extends:
class Bar extends Foo {
constructor(a,b,c) {
super( a, b );
this.z = c;
}

}

gimmeXYZ() {
return super.gimmeXY() * this.z;
}

var b = new Bar( 5, 15, 25 );
b.x;
b.y;
b.z;
b.gimmeXYZ();

//
//
//
//

5
15
25
1875

А вот слово super — совершенно новое, оно позволяет делать вещи,
которые до появления ES6 было невозможно реализовать напрямую
(без кое-каких не слишком хороших и сложно достижимых компромиссов). В конструкторе это ключевое слово автоматически
ссылается на «родительский конструктор». В предыдущем примере он назывался Foo(..) . Внутри метода super ссылается на
«родительский объект», обеспечивая вам доступ к свойству/методу, например super.gimmeXY().
Запись Bar extends Foo, разумеется, означает связывание [[Proto­
type]] свойства Bar.prototype со свойством Foo.prototype. Соответственно, ключевое слово super в таком методе, как gimmeXYZ(),

191

Классы

означает Foo.prototype, в то время как в конструкторе Bar оно
указывает на Foo.
Применение ключевого слова super не ограничивается объявлениями классов. Работает оно и в объектных литералах, причем во
многом тем же способом, который мы тут обсуждаем. Дополнительную информацию по этой теме вы найдете в разделе «Ключевое
слово super» главы 2.

Super-драконы существуют
Нужно отметить, что поведение ключевого слова super зависит от
того, в каком месте оно находится. К счастью, в большинстве случаев проблем с этим не возникает. Но в нетипичных ситуациях вы
можете столкнуться в сюрпризами.
Бывает, что в конструкторе вам требуется ссылка на Foo.prototype,
например, для прямого доступа к одному из его свойств/методов.
Но в конструкторе ключевое слово super таким способом использовать нельзя; запись super.prototype не будет работать. Запись
super(..) означает вызов новой функции Foo(..), но ссылкой на
саму функцию Foo это не является.
В качестве симметричного случая рассмотрим необходимость сослаться на функцию Foo(..) изнутри не принадлежащего конструктору метода. Написав super.constructor, мы укажем на функцию
Foo(..) , но следует помнить, что вызываться она будет только
оператором new. Запись new super.constructor(..) вполне корректна,
но в большинстве случаев пользоваться ею нельзя, так как вы не
можете заставить вызов ссылаться на являющийся его текущим
контекстом объект или использовать его. А ведь именно это нам
зачастую и требуется.
Кроме того, ключевое слово super выглядит как доступное для
управления контекстом функции, подобно this, — то есть создается впечатление динамической связи между этими двумя случаями.

192

Глава 3. Структура

Но, в отличие от ключевого слова this, super динамическим не
является. Когда конструктор или метод с его помощью создает
внутри себя ссылку во время объявления (в теле класса), слово
super статически связывается с иерархией конкретного класса и не
допускает переопределения (по крайней мере, в ES6).
Что все это означает? Если у вас есть привычка брать метод одного «класса» и заимствовать для другого класса, переопределяя
ключевое слово this, например, с помощью методов call(..) или
apply(..), то при наличии в заимствуемом методе ключевого слова
super вы можете столкнуться с сюрпризами. Рассмотрим иерархию
классов:
class ParentA {
constructor() { this.id = "a"; }
foo() { console.log( "ParentA:", this.id ); }
}
class ParentB {
constructor() { this.id = "b"; }
foo() { console.log( "ParentB:", this.id ); }
}
class ChildA extends ParentA {
foo() {
super.foo();
console.log( "ChildA:", this.id );
}
}
class ChildB extends ParentB {
foo() {
super.foo();
console.log( "ChildB:", this.id );
}
}
var a = new ChildA();
a.foo();
var b = new ChildB();
b.foo();

//
//
//
//

ParentA: a
ChildA: a
ParentB: b
ChildB: b

Классы

193

В этом фрагменте кода все кажется совершенно естественным. Но,
если вы попытаетесь позаимствовать метод b.foo() и воспользоваться им в контексте a (в силу динамического связывания с помощью this подобное заимствование — достаточно распространенное явление, оно используется множеством способов, в том числе
в такой вещи, как примеси), результат может вас удивить:
// заимствуем 'b.foo()', чтобы использовать в контексте 'a'
b.foo.call( a );
// ParentB: a
// ChildB: a

Как видите, ссылка this.id оказалась динамически изменена, в результате чего в обоих случаях было выведено : a вместо : b. Но
ссылка super.foo() функции b.foo() динамически не поменялась,
поэтому мы увидели ParentB вместо ожидаемого результата ParentA.
Так как b.foo() ссылается через ключевое слово super, оно статически привязывается к иерархии ChildB/ParentB и не может быть
использовано на иерархическом уровне ChildA/ParentA. Обойти
это ограничение средствами ES6 нельзя.
Действие ключевого слова super наглядно проявляется в случае
статической иерархии классов без взаимного влияния. Но, по большому счету, такая гибкость — и есть основная причина использовать
в коде ключевое слово this. Просто в случае комбинации class
и super таких техник следует избегать.
Здесь нужно сузить процесс проектирования объектов до статических иерархий — ключевые слова class, extends и super в этом
случае будут прекрасно работать. Еще один вариант — отказаться
от имитации классов и использовать динамические гибкие бесклассовые объекты и делегирование [[Prototype]] (см. книгу this
& Object Prototypes данной серии).

Конструктор подкласса
Для классов и подклассов конструкторы не требуются; если конструктор по умолчанию опустить, в обоих случаях он будет заме-

194

Глава 3. Структура

щен. Однако вид замещенного конструктора по умолчанию зависит
от того, с каким классом — непосредственным или расширенным —
мы имеем дело.
В частности, используемый по умолчанию конструктор подкласса
автоматически вызывает родительский конструктор и передает
любые аргументы туда. Другими словами, этот конструктор можно
представить примерно вот так:
constructor(...args) {
super(...args);
}

Отметим важную деталь. Не во всех языках программирования,
поддерживающих классы, конструктор подклассов автоматически
вызывает родительский конструктор. В C++ подобное имеет место,
а вот в Java — нет. Еще важнее тот факт, что в версиях классов,
существовавших до ES6, подробного тоже не было. Так что будьте
осторожны с преобразованиями классов в стандарт ES6, если в вашем коде такого автоматического вызова не предполагается.
В ES6 на конструкторы подклассов накладывается еще одно странное ограничение. Доступ к ключевому слову this в таком конструкторе появляется только после вызова метода super(..). Это поведение имеет крайне сложные причины, но все главным образом
упирается в тот факт, что именно родительский конструктор создает и инициализирует значение this для вашего экземпляра. До
ES6 это работало в обратную сторону: объект this создавался
«конструктором подклассов», а затем вы вызвали родительский
конструктор в контексте this этого подкласса.
Для иллюстрации рассмотрим пример. Так работали до ES6:
function Foo() {
this.a = 1;
}
function Bar() {
this.b = 2;
Foo.call( this );

195

Классы

}
// 'Bar' "расширяет" 'Foo'
Bar.prototype = Object.create( Foo.prototype );

А вот такое в ES6 недопустимо:
class Foo {
constructor() { this.a = 1; }
}
class Bar extends Foo {
constructor() {
this.b = 2; // недопустимо до вызова 'super()'
super(); // для исправления нужно поменять местами эти
два оператора
}
}

В данном случае исправить ситуацию несложно. Достаточно поменять местами два оператора в конструкторе подкласса Bar. Но
если в варианте кода, который имел место до появления ES6, вы
полагались на возможность пропустить вызов «родительского
конструктора», будьте осторожны, так как теперь ее нет.

Расширение встроенных объектов
Одним из самых многообещающих преимуществ в новом варианте
использования ключевых слов class и extend стала долгожданная
возможность превращать в подклассы встроенные объекты, например Array. Рассмотрим пример:
class MyCoolArray extends Array {
first() { return this[0]; }
last() { return this[this.length - 1]; }
}
var a = new MyCoolArray( 1, 2, 3 );
a.length;
a;

// 3
// [1,2,3]

a.first();
a.last();

// 1
// 3

196

Глава 3. Структура

До ES6 имитация «подкласса» Array путем создания объекта вручную и связывания со свойством Array.prototype работала лишь
частично. Не удавалось воспроизвести особые поведения настоящих массивов, например автоматическое обновление свойства
length. Имейте в виду, подклассы ES6 должны полноценно работать
с унаследованными и новыми поведениями.
Другое распространенное ограничение «подклассов» до ES6 было
связано с объектом Error. Оно состояло в создании пользовательских «подклассов» ошибок. В момент появления настоящие объекты Error автоматически захватывают из стека специальную информацию, в том числе о номере строки и о файле, где появилась
ошибка. У возникавших до ES6 пользовательских «подклассов»
ошибок такого поведения не наблюдалось, что несколько ограничивало их применимость.
Тут нам поможет ES6:
class Oops extends Error {
constructor(reason) {
this.oops = reason;
}
}
// позднее:
var ouch = new Oops( "I messed up!" );
throw ouch;

Пользовательский объект ouch в приведенном фрагменте будет
вести себя как настоящий объект ошибки, в том числе и захватывать
информацию из стека. Это существенное улучшение!

Свойство new.target
В ES6 появилась новая концепция, называемая метасвойством
(meta property). Она имеет форму new.target и подробно будет
рассматриваться в главе 7.

Классы

197

Добавление к ключевому слову точки ., да и само имя свойства
выглядят странно для JS.
Свойство new.target представляет собой новое «магическое» значение, доступное во всех функциях, хотя в обычных функциях оно
всегда равняется undefined. В любом конструкторе new.target всегда
будет указывать на конструктор, непосредственно вызвавший оператор new, даже если тот располагается в параллельном классе и был
делегирован через вызов super(..) из дочернего конструктора.
Рассмотрим пример:
class Foo {
constructor() {
console.log( "Foo: ", new.target.name );
}
}
class Bar extends Foo {
constructor() {
super();
console.log( "Bar: ", new.target.name );
}
baz() {
console.log( "baz: ", new.target );
}
}
var a = new Foo();
// Foo: Foo
var b = new Bar();
// Foo: Bar