ECMAScript 6 - Nikolas Zakas
ECMAScript 6 - Nikolas Zakas
02-018
УДК 004.738.5
З-18
Закас Н.
З-18 ECMAScript 6 для разработчиков. — СПб.: Питер, 2017. — 352 с.: ил. — (Серия
«Библиотека программиста»).
ISBN 978-5-496-03037-3
Познакомьтесь с радикальными изменениями в языке JavaScript, которые произошли благодаря
новому стандарту ECMAScript 6. Николас Закас — автор бестселлеров и эксперт-разработчик — создал
самое полное руководство по новым типам объектов, синтаксису и интересным функциям. Каждая
глава содержит примеры программ, которые будут работать в любой среде JavaScript и познакомят
вас с новыми возможностями языка. Прочитав эту книгу, вы узнаете о том, чем полезны итераторы и
генераторы, чем ссылочные функции отличаются от обычных, какие дополнительные опции позволяют
работать с данными, о наследовании типов, об асинхронном программировании, о том, как модули
меняют способ организации кода, и многом другом.
Более того, Николас Закас заглядывает в будущее, рассказывая про изменения, которые появятся
в ECMAScript 7. Неважно, являетесь вы веб-разработчиком или работаете с node.js, в этой книге вы
найдете самую необходимую информацию, позволяющую эффективно использовать все возможности
ECMAScript 6.
Права на издание получены по соглашению с No Starch Press. Все права защищены. Никакая часть данной книги
не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских
прав.
Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как на-
дежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не может
гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за возможные
ошибки, связанные с использованием книги.
Предисловие........................................................................................... 16
Благодарности........................................................................................ 18
Введение................................................................................................. 19
История ECMAScript 6.............................................................................................................. 19
О книге............................................................................................................................................. 20
Совместимость с браузерами и Node.js.......................................................................... 20
Кому адресована книга........................................................................................................ 20
Обзор содержания................................................................................................................. 21
Используемые соглашения................................................................................................ 22
От издательства............................................................................................................................ 23
Глава 3. Функции.................................................................................... 59
Функции со значениями параметров по умолчанию...................................................... 59
Имитация значений параметров по умолчанию в ECMAScript 5........................ 59
Значения параметров по умолчанию в ECMAScript 6............................................. 60
Как значения параметров по умолчанию влияют на объект arguments............. 62
Выражения в параметрах по умолчанию....................................................................... 63
Временная мертвая зона для параметров по умолчанию........................................ 65
Неименованные параметры...................................................................................................... 67
Неименованные параметры в ECMAScript 5............................................................... 67
Остаточные параметры........................................................................................................ 68
Дополнительные возможности конструктора Function................................................. 70
Оператор расширения................................................................................................................ 71
Свойство name............................................................................................................................... 72
Выбор соответствующих имен.......................................................................................... 73
Специальные случаи свойства name............................................................................... 73
Оглавление 7
Классы-выражения.................................................................................................................... 199
Простой класс-выражение................................................................................................ 200
Именованные классы-выражения................................................................................. 200
Классы как сущности первого класса................................................................................. 202
Свойства с методами доступа................................................................................................ 203
Вычисляемые имена членов................................................................................................... 205
Методы-генераторы.................................................................................................................. 206
Статические члены.................................................................................................................... 207
Наследование в производных классах................................................................................ 208
Затенение методов класса................................................................................................. 211
Унаследованные статические члены............................................................................. 211
Производные классы из выражений............................................................................. 212
Наследование встроенных объектов............................................................................. 214
Свойство Symbol.species.................................................................................................... 216
Использование new.target в конструкторах классов..................................................... 219
В заключение............................................................................................................................... 221
Юрий Зайцев (Juriy Zaytsev, известен в Сети под псевдонимом kangax) — веб-
разработчик, проживающий в Нью-Йорке. Исследует и пишет о необычной при-
роде JavaScript начиная с 2007 года. Вносит вклад в развитие нескольких открытых
проектов, включая Prototype.js, и других популярных проектов, таких как его
собственный Fabric.js. Сооснователь компании printio.ru, занимающейся печатью
под заказ. В настоящее время работает в Facebook.
Предисловие
Язык ECMAScript 6 вихрем ворвался в мир. Он появился, когда многие уже пере-
стали ждать его, и распространялся быстрее, чем многие успевали знакомиться
с ним. У каждого своя история об этом. А вот моя.
В 2013 году я работал в проекте, целью которого была связать iOS с Веб. Это было
еще до моего участия в создании проекта Redux и до вступления в сообщество раз-
работчиков программного обеспечения с открытым исходным кодом на JavaScript.
В тот момент я упорно пытался освоить принципы разработки веб-приложений
и пребывал в страхе перед новым и неизведанным. Наша команда должна была всего
за несколько месяцев создать с нуля веб-версию нашего продукта на JavaScript.
Сначала мне казалось невозможным написать на JavaScript что-то более или ме-
нее серьезное. Но новый член команды убедил меня, что JavaScript — далеко не
игрушечный язык программирования, и я согласился дать ему шанс. Я отбросил
все свои предубеждения, открыл MDN и StackOverflow и впервые приступил к де-
тальному изучению JavaScript. Простота языка, которую я обнаружил, очаровала
меня. Один из коллег научил меня пользоваться такими инструментами, как linter
и bundler. Однажды, спустя несколько недель, я проснулся и понял, что обожаю
писать на JavaScript.
Но, как известно, совершенных языков не существует. Я не видел частых обновле-
ний, к которым привык, работая с другими языками. Единственное существенное
обновление в JavaScript за десятилетие — ECMAScript 5 — оказалось простой
чисткой, но даже в этом случае потребовалось несколько лет, чтобы все браузеры
реализовали полноценную поддержку обновленной версии. В то время грядущая
спецификация ECMAScript 6 (ES6) под кодовым названием Harmony была далека
от завершения, и казалось, что она выйдет в далеком-далеком будущем. Я тогда
думал, что, возможно, лишь лет через десять смогу написать первый код на ES6.
На тот момент существовало несколько экспериментальных «трансляторов», таких
как Google Traceur, которые преобразовывали код на ES6 в код на ES5. Большинство
из них имело серьезные ограничения или было жестко завязано на существующий
конвейер сборки JavaScript. Но затем появился новый транслятор под названием
6to5, и все изменилось. Он был прост в установке, хорошо интегрировался с су-
ществующими инструментами и производил читаемый программный код. Он
распространился, подобно лесному пожару. Транслятор 6to5, ныне известный под
названием Babel, позволил использовать новые возможности ES6 еще до того, как
Предисловие 17
История ECMAScript 6
В 2007 году язык JavaScript оказался на перепутье. Появление популярной техноло-
гии Ajax возвестило о начале новой эры динамических веб-приложений, но к тому
времени JavaScript не изменялся с момента выхода третьей редакции ECMA-262,
опубликованной в 1999 году.
TC-39 — комитет, ответственный за развитие ECMAScript, — собрал воедино рабо-
чий вариант огромной спецификации ECMAScript 4, предусматривавшей крупные
и мелкие изменения в языке. В числе изменений были: новый синтаксис, поддержка
модулей, классы, классическое наследование, приватные члены объектов, необяза-
тельные аннотации типов и многое другое.
Большой объем изменений, предлагавшихся в ECMAScript 4, вызвал раскол в ко-
митете TC-39 — некоторые его члены полагали, что четвертая редакция включает
слишком много изменений. Группа лидеров из Yahoo!, Google и Microsoft разработала
альтернативный вариант следующей версии ECMAScript, который первоначально
назывался ECMAScript 3.1. Номер версии «3.1» должен был подчеркнуть, что эта
версия вносит в существующий стандарт небольшие поступательные изменения.
В ECMAScript 3.1 предлагались очень ограниченные изменения в синтаксисе
и основной упор делался на введение атрибутов свойств, встроенной поддержки
JSON и дополнительных методов в уже существующие объекты. Первые попытки
согласовать ECMAScript 3.1 и ECMAScript 4 полностью провалились, потому что
два лагеря имели совершенно разные взгляды на то, как должен развиваться язык.
20 Введение
В 2008 году Брендан Эйх, создатель JavaScript, заявил, что комитет TC-39
должен сосредоточить свои усилия на стандартизации ECMAScript 3.1 и от-
ложить существенные изменения синтаксиса и особенностей языка, пред-
ложенные в ECMAScript 4, пока не будет стандартизована следующая версия
ECMAScript, и все члены комитета должны стремиться свести воедино все лучшее
из ECMAScript 3.1 и 4. С этого момента начала свою историю версия ECMAScript
под названием Harmony.
В конечном итоге спецификация ECMAScript 3.1 была стандартизована как пятая
редакция ECMA-262, которую также часто называют ECMAScript 5. Версия стан-
дарта ECMAScript 4 так и не была выпущена, чтобы избежать путаницы с неудав-
шейся попыткой. После этого началась работа над ECMAScript Harmony, и версия
стандарта ECMAScript 6 стала первой, созданной в новом духе «гармонии».
Работы над версией ECMAScript 6 были завершены в 2015 году, и спецификация
получила официальное название «ECMAScript 2015». (Но в этой книге она по-
прежнему называется ECMAScript 6, потому что это название привычнее для раз-
работчиков.) Она определяет весьма широкий диапазон изменений — от совершенно
новых объектов до синтаксических конструкций и новых методов в существующих
объектах. Самое замечательное, что все изменения в ECMAScript 6 направлены на
решение проблем, с которыми разработчики сталкиваются ежедневно.
О книге
Знание и понимание особенностей ECMAScript 6 совершенно необходимы любым
разработчикам на JavaScript. Возможности языка, введенные в ECMAScript 6, об-
разуют фундамент, на котором будут строиться JavaScript-приложения в обозримом
будущем. Я надеюсь, что, читая эту книгу, вы сможете освоить новые возможности
ECMAScript 6 и будете готовы применить их, когда это потребуется.
условием для работы с этой книгой, однако оно поможет вам понять различия между
ECMAScript 5 и 6. В частности, эта книга адресована разработчикам на JavaScript
с опытом программирования сценариев для браузеров или Node.js, желающим
узнать о последних нововведениях, появившихся в языке.
Эта книга точно не для начинающих, никогда не писавших на JavaScript. Для чтения
этой книги необходимо хорошо знать хотя бы основы языка.
Обзор содержания
Все главы и приложения в этой книге охватывают разные аспекты ECMAScript 6.
Многие главы начинаются с обсуждения проблем, на решение которых направлены
изменения в ECMAScript 6, чтобы вы получили более широкое представление об
этих изменениях. Все главы включают примеры программного кода, демонстри-
рующие новые идеи и синтаксические конструкции.
В главе 1 «Блочные привязки» рассказывается об операторах let и const, за-
мещающих оператор var на уровне блоков.
Глава 2 «Строки и регулярные выражения» охватывает дополнительные стро-
ковые функции, а также знакомит с шаблонными строками.
В главе 3 «Функции» обсуждаются различные изменения, коснувшиеся функ-
ций, включая стрелочные функции, параметры со значениями по умолчанию,
остаточные параметры и некоторые другие особенности.
В главе 4 «Расширенные возможности объектов» разъясняются изменения
в подходах к созданию, изменению и использованию объектов. В числе рас-
сматриваемых тем изменения в синтаксисе литералов объектов и новые методы
рефлексии.
Глава 5 «Деструктуризация для упрощения доступа к данным» знакомит с син-
таксисом деструктуризации объектов и массивов, позволяющим расчленять
объекты и массивы, используя компактный синтаксис.
Глава 6 «Символы и символьные свойства» знакомит с идеей символов — новым
способом определения свойств. Символы — это новый простой тип данных,
который можно использовать для сокрытия (хотя и не полного) свойств и ме-
тодов объектов.
В главе 7 «Множества и ассоциативные массивы» описываются новые типы
коллекций: Set, WeakSet, Map и WeakMap. Эти типы добавляют в обычные массивы
гарантию уникальности значений и средства управления памятью, спроектиро-
ванные специально для JavaScript.
В главе 8 «Итераторы и генераторы» обсуждается добавление в язык итераторов
и генераторов. Эти инструменты открывают новые мощные способы работы
с коллекциями данных, недоступные в предыдущих версиях JavaScript.
22 Введение
Используемые соглашения
Ниже перечислены типографские соглашения, принятые в этой книге:
Курсив используется для выделения новых терминов и имен файлов.
Моноширинным шрифтом выделяются фрагменты программного кода.
Кроме того, длинные фрагменты кода оформляются как блоки, набранные моно-
ширинным шрифтом, например:
function doSomething() {
// пустая
}
От издательства 23
console.log("Hi"); // "Hi"
От издательства
Ваши замечания, предложения, вопросы отправляйте по адресу [email protected] (из-
дательство «Питер», компьютерная редакция).
Мы будем рады узнать ваше мнение!
На веб-сайте издательства www.piter.com вы найдете подробную информацию о на-
ших книгах.
1 Блочные привязки
function getValue(condition) {
if (condition) {
var value = "blue";
// прочий код
return value;
} else {
return null;
}
function getValue(condition) {
var value;
if (condition) {
value = "blue";
// прочий код
return value;
} else {
return null;
}
}
Объявления let
Объявление let имеет тот же синтаксис, что и объявление var. В простейшем случае
оператор var в объявлении переменной можно заменить на let, но это ограничит
область видимости переменной текущим блоком кода (существуют также другие
тонкие отличия, которые обсуждаются ниже в разделе «Временная мертвая зона»).
Объявления let не поднимаются к началу блока, поэтому их лучше размещать
в блоке первыми, чтобы сделать доступными во всем блоке. Например:
function getValue(condition) {
if (condition) {
let value = "blue";
// прочий код
return value;
} else {
return null;
}
В данном примере переменная count объявляется дважды: один раз с помощью var
и второй — с помощью let. Так как объявление let не позволяет переопределять
идентификаторы, уже присутствующие в текущей области видимости, попытка
выполнить его вызовет ошибку. С другой стороны, если объявление let создает
новую переменную с именем, уже объявленным во внешней области видимости,
это не приведет к ошибке, как демонстрирует следующий фрагмент:
if (condition) {
// не вызовет ошибку
let count = 40;
// прочий код
}
Это объявление let не вызовет ошибку, потому что создает новую переменную
с именем count внутри инструкции if , а не в окружающем ее блоке. Внутри
блока if эта новая переменная закроет доступ к глобальной переменной count
до конца блока.
Объявления const
Привязки в ECMAScript 6 можно также объявлять с помощью const. Такие при-
вязки считаются константами, то есть их значения невозможно изменить после
инициализации. По этой причине каждая привязка const должна включать значение
для инициализации, как показано в следующем примере:
// допустимая константа
const maxItems = 30;
if (condition) {
const maxItems = 5;
// прочий код
}
const maxItems = 5;
// вызовет ошибку
maxItems = 6;
const person = {
name: "Nicholas"
};
Объявления на уровне блока 29
// работает
person.name = "Greg";
// вызовет ошибку
person = {
name: "Greg"
};
if (condition) {
console.log(typeof value); // вызовет ошибку
let value = "blue";
}
В этом примере переменная i существует только внутри цикла for. Когда цикл
завершится, переменная окажется недоступной.
Функции в циклах
Особенности объявлений var долгое время усложняли определение функций
в циклах, потому что переменные, объявленные таким способом внутри циклов,
доступны за их пределами. Взгляните на следующий фрагмент:
funcs.forEach(function(func) {
func(); // десять раз выведет число "10"
});
На первый взгляд кажется, что этот код должен вывести числа от 0 до 9, но в дей-
ствительности он десять раз выведет число 10. Причина в том, что во всех итерациях
цикла используется одна и та же переменная i, то есть все функции, созданные
в теле цикла, будут хранить ссылку на одну и ту же переменную. После последней
итерации цикла переменная i получит значение 10, которое и выведут все инструк-
ции console.log(i) во вновь созданных функциях.
Для решения этой проблемы разработчики используют внутри циклов выражения
немедленно вызываемых функций (Immediately Invoked Function Expression, IIFE),
чтобы принудительно создать новую копию переменной, участвующей в итерациях,
как показано в следующем примере:
funcs.forEach(function(func) {
func(); // выведет 0, 1, 2, ..., 9
});
funcs.forEach(function(func) {
func(); // выведет 0, 1, 2, ..., 9
})
funcs.forEach(function(func) {
func(); // выведет "a", затем "b", затем "c"
});
Блочные привязки в циклах 33
ПРИМЕЧАНИЕ
Имейте в виду, что поведение объявлений let в циклах отдельно определено в специ
фикации и, возможно, никак не связано с особенностью let, препятствующей «подъ
ему». В действительности, первые реализации let не обладали таким поведением —
оно было добавлено позднее в процессе доработки спецификации.
// не вызовет ошибку
for (const key in object) {
34 Глава 1 • Блочные привязки
funcs.push(function() {
console.log(key);
});
}
funcs.forEach(function(func) {
func(); // выведет "a", затем "b", затем "c"
});
Этот код работает почти так же, как во втором примере в разделе «Объявления
let в циклах» (см. выше). Единственное отличие — значение key нельзя изменить
в теле цикла. Такое поведение const в циклах for-in и for-of объясняется тем,
что в каждой итерации выражение инициализации создает новую привязку, а не
пытается изменить значение существующей привязки (как это происходит в при-
мере с циклом for).
Несмотря на то что объект window уже имеет свойство RegExp, объявление var
может затереть его. В этом примере объявляется новая глобальная переменная
RegExp, затирающая оригинальное свойство. Аналогично объявление глобальной
переменной ncz немедленно создает свойство в объекте window — именно так всегда
работал JavaScript.
Объявления let или const создают новые привязки в глобальной области ви-
димости, но не добавляют свойства в глобальный объект. Это означает, что объ-
явление let или const не затрет глобальную переменную, а просто замаскирует
ее. Например:
// в браузере
let RegExp = "Hello!";
console.log(RegExp); // "Hello!"
console.log(window.RegExp === RegExp); // false
В заключение 35
ПРИМЕЧАНИЕ
Тем не менее иногда объявление var может оказаться востребованным, например,
если имеется какой-то код, который должен быть доступен из глобального объекта.
Такая потребность в основном характерна для браузеров, когда требуется обеспечить
доступность кода между фреймами или окнами.
В заключение
Блочные привязки let и const вводят в JavaScript понятие лексической области
видимости. Эти объявления не поднимаются интерпретатором и существуют только
в блоке, где они объявлены. Блочные привязки демонстрируют поведение, более
36 Глава 1 • Блочные привязки
Строки, вне всяких сомнений, — один из самых важных типов данных в программи-
ровании. Они имеются практически во всех высокоуровневых языках программи-
рования, а возможность эффективной работы с ними является для разработчиков
важнейшим условием создания полезных программ. Немаловажную роль играет
также поддержка регулярных выражений, потому что дает разработчикам до-
полнительные возможности для работы со строками. Учитывая все это, создатели
ECMAScript 6 усовершенствовали поддержку строк и регулярных выражений, до-
бавив новые и долгожданные возможности. Эта глава описывает оба типа изменений.
console.log(text.length); // 2
console.log(/^.$/.test(text)); // false
console.log(text.charAt(0)); // ""
console.log(text.charAt(1)); // ""
console.log(text.charCodeAt(0)); // 55362
console.log(text.charCodeAt(1)); // 57271
Метод codePointAt()
Одним из методов, добавленных в ECMAScript 6 для полноценной поддержки
UTF-16, является метод codePointAt(), который извлекает кодовый пункт Юникода
из указанной позиции в строке. Этот метод принимает позицию кодового пункта,
а не символа, и возвращает целочисленное значение. Сравните эти результаты
с результатами, которые возвращает charCodeAt():
console.log(text.charCodeAt(0)); // 55362
console.log(text.charCodeAt(1)); // 57271
console.log(text.charCodeAt(2)); // 97
console.log(text.codePointAt(0)); // 134071
console.log(text.codePointAt(1)); // 57271
console.log(text.codePointAt(2)); // 97
function is32Bit(c) {
return c.codePointAt(0) > 0xFFFF;
}
console.log(is32Bit("𠮷")); // true
console.log(is32Bit("a")); // false
Метод String.fromCodePoint()
Если JavaScript поддерживает какую-то операцию, значит, он поддерживает и об-
ратную ей операцию. Например, метод codePointAt() позволяет получить кодовый
40 Глава 2 • Строки и регулярные выражения
Метод normalize()
Еще один интересный аспект Юникода — возможность считать два разных символа
эквивалентными при выполнении сортировки и других операций, основанных на
сравнении. Существует два вида подобных отношений между символами. Первый —
каноническая эквивалентность, когда две последовательности кодовых пунктов
являются полностью взаимозаменяемыми. Например, комбинация двух символов
может быть канонически эквивалентна одному символу. Второй вид отношений —
совместимость. Две совместимые последовательности кодовых пунктов выглядят
по-разному, но в некоторых ситуациях могут быть взаимозаменяемыми.
Из-за этих отношений две строки, представляющие по сути один и тот же текст,
могут содержать разные последовательности кодовых пунктов. Например, символ
«æ» и двухсимвольную строку «ae» можно использовать взаимозаменяемо, но они
не являются строго эквивалентными, если не провести некоторую нормализацию.
ECMAScript 6 поддерживает несколько форм нормализации строк Юникода с по-
мощью метода normalize(). В этот метод может передаваться дополнительный
однострочный параметр, определяющий форму нормализации:
Форма нормализации канонической композицией (Normalization Form Canonical
Composition, "NFC"), по умолчанию.
Форма нормализации канонической декомпозицией (Normalization Form
Canonical Decomposition, "NFD").
Форма нормализации совместимой композицией (Normalization Form
Compatibility Composition, "NFKC").
Форма нормализации совместимой декомпозицией (Normalization Form
Compatibility Decomposition, "NFKD").
Разъяснение различий между этими четырьмя формами выходит за рамки данной
книги. Поэтому просто запомните, что перед сравнением двух строк их необходимо
нормализовать к одной и той же форме. Например:
normalized.sort(function(first, second) {
if (first < second) {
return -1;
} else if (first === second) {
return 0;
} else {
return 1;
}
});
values.sort(function(first, second) {
let firstNormalized = first.normalize(),
secondNormalized = second.normalize();
И снова, самое важное в этом примере, на что следует обратить внимание, — обе
строки, first и second, нормализуются к одной форме. В этих примерах по умол-
чанию используется форма NFC, но точно так же можно использовать любую из
поддерживаемых форм:
values.sort(function(first, second) {
let firstNormalized = first.normalize("NFD"),
secondNormalized = second.normalize("NFD");
Действие флага u
Если в регулярном выражении установлен флаг u, оно переключается в режим
работы с символами, а не с кодовыми единицами. Это означает, что регулярное вы-
ражение не будет интерпретировать суррогатные пары как два отдельных символа
и должно действовать, как ожидается. Например:
console.log(text.length); // 2
console.log(/^.$/.test(text)); // false
console.log(/^.$/u.test(text)); // true
function codePointLength(text) {
let result = text.match(/[\s\S]/gu);
return result ? result.length : 0;
}
console.log(codePointLength("abc")); // 3
console.log(codePointLength("𠮷bc")); // 3
Другие изменения в поддержке строк 43
ПРИМЕЧАНИЕ
Несмотря на то что это решение вполне работоспособно, оно не отличается высокой
скоростью, особенно при анализе длинных строк. Более быстрое решение основывается
на применении строкового итератора (обсуждается в главе 8). А вообще, старайтесь
минимизировать количество операций подсчета кодовых пунктов.
function hasRegExpU() {
try {
var pattern = new RegExp(".", "u");
return true;
} catch (ex) {
return false;
}
}
Эта функция вызывает конструктор RegExp и передает ему флаг u в качестве ар-
гумента. Такой синтаксис допустим даже при работе с более ранними версиями
JavaScript, но конструктор вызовет ошибку, если флаг u не поддерживается.
ПРИМЕЧАНИЕ
Если ваш код должен сохранять работоспособность при выполнении старыми движками
JavaScript, всегда используйте конструктор RegExp для проверки поддержки флага u.
Это поможет предотвратить появление синтаксических ошибок и позволит выяснить
возможность применения флага u, не вызывая остановку выполнения программы.
console.log(msg.startsWith("Hello")); // true
console.log(msg.endsWith("!")); // true
console.log(msg.includes("o")); // true
console.log(msg.startsWith("o")); // false
console.log(msg.endsWith("world!")); // true
console.log(msg.includes("x")); // false
Первые три вызова осуществляются без второго параметра, поэтому они выпол-
няют поиск по всей строке, если потребуется. Последние три вызова выполняют
поиск в ограниченном фрагменте строки. Вызов msg.startsWith("o", 4) начинает
поиск с 4-й позиции в строке msg, где находится символ o в слове Hello. Вызов
msg.endsWith("o", 8) также начинает поиск с 4-й позиции в строке, потому что
при вычитании аргумента 8 из длины строки (12) получается число 4. Вызов msg.
includes("o", 8) начинает поиск с 8-й позиции, где находится символ r в слове world.
Эти три метода действительно упрощают поиск подстроки в строке, но все они воз-
вращают логическое значение. Если вам понадобится найти фактическую позицию
одной строки в другой, используйте метод indexOf() или lastIndexOf().
Другие изменения в регулярных выражениях 45
ПРИМЕЧАНИЕ
Методы startsWith(), endsWith() и includes() вызывают ошибку, если вместо
строки передать регулярное выражение. Методы indexOf() и lastIndexOf(), напро
тив, преобразуют аргумент с регулярным выражением в строку и выполняют поиск
в этой строке.
Метод repeat()
Спецификация ECMAScript 6 добавила в поддержку строк еще один новый метод —
метод repeat(), который принимает аргумент с количеством повторений строки.
Он возвращает новую строку, содержащую исходную указанное количество раз.
Например:
console.log("x".repeat(3)); // "xxx"
console.log("hello".repeat(2)); // "hellohello"
console.log("abc".repeat(4)); // "abcabcabcabc"
pattern.lastIndex = 1;
globalPattern.lastIndex = 1;
stickyPattern.lastIndex = 1;
result = pattern.exec(text);
globalResult = globalPattern.exec(text);
stickyResult = stickyPattern.exec(text);
console.log(pattern.lastIndex); // 0
console.log(globalPattern.lastIndex); // 7
console.log(stickyPattern.lastIndex); // 7
result = pattern.exec(text);
globalResult = globalPattern.exec(text);
stickyResult = stickyPattern.exec(text);
console.log(pattern.lastIndex); // 0
console.log(globalPattern.lastIndex); // 14
console.log(stickyPattern.lastIndex); // 14
console.log(pattern.sticky); // true
48 Глава 2 • Строки и регулярные выражения
function hasRegExpY() {
try {
var pattern = new RegExp(".", "y");
return true;
} catch (ex) {
return false;
}
}
Переменная re2 — это всего лишь копия переменной re1. Но если в вызов кон-
структора RegExp передать второй аргумент, определяющий флаги для регулярного
выражения, такой код не будет работать, например:
console.log(re1.toString()); // "/ab/i"
console.log(re2.toString()); // "/ab/g"
console.log(re1.test("ab")); // true
console.log(re2.test("ab")); // true
console.log(re1.test("AB")); // true
console.log(re2.test("AB")); // false
Свойство flags
Помимо нового флага и возможности изменять имеющиеся флаги спецификация
ECMAScript 6 добавила новое свойство, связанное с ними. В ECMAScript 5 есть
возможность получить текст регулярного выражения с помощью свойства source,
но чтобы получить строку с флагами, требуется выполнить парсинг результата
вызова метода toString(), как показано ниже:
function getFlags(re) {
var text = re.toString();
return text.substring(text.lastIndexOf("/") + 1, text.length);
}
console.log(getFlags(re)); // "g"
let re = /ab/g;
console.log(re.source); // "ab"
console.log(re.flags); // "g"
Литералы шаблонов
Чтобы дать разработчикам возможность решать более сложные задачи, синтаксис
литералов шаблонов в ECMAScript 6 позволяет определять предметно-ориентиро-
ванные языки (Domain-Specific Languages, DSL) для более безопасной работы с со-
держимым, чем это позволяют решения, доступные в ECMAScript 5 и более ранних
версиях. Предметно-ориентированный язык — это язык программирования, пред-
назначенный для решения в узкой, специализированной области, в противополож-
ность универсальным языкам, таким как JavaScript. На вики-странице ECMAScript
(http://wiki.ecmascript.org/doku.php?id=harmony:quasis/) предлагается следующее
черновое описание литералов шаблонов:
Основной синтаксис
В простейшем виде литералы шаблонов действуют подобно обычным строкам, от-
личаясь только ограничивающими символами — обратные апострофы (`) вместо
двойных или одиночных кавычек. Например:
Этот пример показывает, что переменная message хранит обычную для JavaScript
строку. Здесь с помощью синтаксиса литералов шаблонов было создано строковое
значение, которое затем было присвоено переменной message.
Если потребуется использовать обратные апострофы в строке, просто экранируйте
их символами обратного слеша (\), как в следующей версии переменной message:
Многострочный текст
Разработчики на JavaScript еще с самых первых версий языка ищут способы опре-
деления многострочного текста. Но строки в двойных или одиночных кавычках
требуют, чтобы весь текст был определен в одной строке кода.
console.log(message); // "Multiline
// string"
Этот код должен вывести содержимое message в двух отдельных строках во всех
основных движках JavaScript; однако такое поведение объявлено ошибочным,
и многие разработчики рекомендуют не использовать данный прием.
Другие приемы создания строк с многострочным текстом, использовавшиеся до
появления ECMAScript 6, обычно основаны на применении массивов или конка-
тенации строк, как в следующем примере:
var message = [
"Multiline ",
"string"
].join("\n");
console.log(message); // "Multiline
// string"
console.log(message.length); // 16
console.log(message); // "Multiline
// string"
console.log(message.length); // 31
В этом примере все пробельные символы в начале второй строки в литерале шаблона
вошли в состав получившегося строкового значения.
Если выравнивание и отступы играют важную роль, оставьте пустой первую строку
в литерале многострочного шаблона и затем оформляйте отступы как требуется,
например:
let html = `
<div>
<h1>Title</h1>
</div>`.trim();
console.log(message); // "Multiline
// string"
console.log(message.length); // 16
Подстановка значений
Сейчас литералы шаблонов кому-то могут показаться всего лишь непривычной
версией обычных строк. Истинное отличие между ними заключается в возможности
подстановки значений, позволяющей встраивать в литералы шаблонов допустимые
выражения на JavaScript и получать на выходе строку с внедренными результатами
вычисления этих выражений.
Встраиваемые выражения в литералах начинаются парой символов ${ и заверша-
ются }. Простейшим примером подстановки может служить внедрение локальных
переменных, например:
ПРИМЕЧАНИЕ
Литералы шаблонов имеют доступ ко всем переменным в области видимости, где они
(литералы) определены. Попытка использовать необъявленную переменную вызывает
ошибку в обоих режимах выполнения, строгом и нестрогом.
В этом примере один литерал шаблона вложен в другой. За первой парой символов
${ следует второй литерал шаблона. Вторая пара ${ отмечает начало встроенного
выражения внутри вложенного литерала шаблона. Этим выражением является имя
переменной name, значение которой вставляется в результат.
Теги шаблонов
Теперь вы знаете, как с помощью литералов шаблонов определять многострочный
текст и вставлять в строки значения, не используя операцию конкатенации. Но
истинная их мощь заключается в возможности определять теги шаблонов. Тег
шаблона (template tag) преобразует литерал шаблона и возвращает получившееся
строковое значение. Имя тега определяется в начале шаблона — непосредственно
перед первым символом `, как показано ниже:
В данном примере tag — это имя тега шаблона, при обращении к которому при-
меняется литерал шаблона `Hello world`.
Определение тегов
Тег — это всего лишь функция, которая вызывается для обработки данных литерала
шаблона. Она принимает информацию о шаблоне в виде отдельных фрагментов,
которые требуется объединить, чтобы получить результат. Первый аргумент —
массив литеральных строк, полученных движком JavaScript из исходного кода. Все
остальные аргументы — интерпретированные значения для подстановки.
Функции тегов обычно определяются с применением остаточных аргументов (rest
arguments), чтобы упростить обработку данных и избавиться от использования
именованных параметров, как показано ниже:
Чтобы лучше понять, что получают функции тегов, рассмотрим следующий фраг-
мент:
return result;
}
ПРИМЕЧАНИЕ
Значения элементов в массиве substitutions не всегда являются строками. Если
выражение возвращает число, как в предыдущем примере, в массив записывается
числовое значение. Определение способа вывода таких значений как раз и является
задачей тега.
return result;
}
console.log(message); // "Multiline\\nstring"
console.log(message.length); // 17
В заключение
Полноценная поддержка Юникода в ECMAScript 6 позволяет сценариям на
JavaScript обрабатывать строки в кодировке UTF-16 логичными способами. По-
явление возможности преобразований между кодовыми пунктами и символами
с помощью codePointAt() и String.fromCodePoint() — важное усовершенствование
для работы со строками. Добавление флага u в регулярные выражения открывает
возможность оперировать не только 16-битными символами, но и кодовыми пун-
ктами, а появление метода normalize() дает возможность сравнивать эквивалентные
строки.
Спецификация ECMAScript 6 добавила новые методы для работы со строками,
упрощающие поиск подстрок независимо от их местоположения в строке. В регу-
лярные выражения также были добавлены дополнительные возможности.
Литералы шаблонов — важное дополнение, появившееся в ECMAScript 6, которое
позволяет создавать предметно-ориентированные языки (Domain-Specific Languages,
DSL) с целью упростить конструирование строк. Возможность внедрять перемен-
ные непосредственно в литералы шаблонов дает разработчикам более безопасный
способ создания строк, чем операция конкатенации.
Встроенная поддержка определения многострочного текста также оказывается
ценным усовершенствованием в сравнении с обычными строками JavaScript, кото-
рые никогда не давали такой возможности. Несмотря на то что литералы шаблонов
допускают непосредственное использование символов перевода строки, вы все еще
можете использовать \n и другие экранированные последовательности.
Теги шаблонов — наиболее важная часть поддержки литералов шаблонов, способ-
ствующая созданию предметно-ориентированных языков. Теги — это функции,
получающие литералы шаблонов, разбитые на части. Эти данные можно исполь-
зовать для конструирования возвращаемой строки. Данные, доступные в функции
тега, включают массив литералов, их необработанные эквиваленты и значения для
подстановки. Эти данные помогают правильно сформировать результат тега.
3 Функции
Хотя это решение более безопасное, оно по-прежнему вынуждает писать допол-
нительный код, чтобы выполнить элементарную операцию. Данный подход пред-
ставляет типичный шаблон программирования, и многие популярные библиотеки
на JavaScript переполнены подобными шаблонами.
Эта функция ожидает получить только первый обязательный параметр, а два других
имеют значения по умолчанию. Новый синтаксис помогает существенно сократить
Функции со значениями параметров по умолчанию 61
mixArgs("a", "b");
true
true
true
true
mixArgs("a", "b");
true
true
false
false
mixArgs("a");
Он выведет следующее:
1
true
false
false
false
function getValue() {
return 5;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 6
let value = 5;
function getValue() {
return value++;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 6
console.log(add(1)); // 7
ВНИМАНИЕ
Будьте внимательны, используя функции для получения значений по умолчанию.
Если вы забудете поставить круглые скобки после имени функции в определении
параметра, например second = getValue, в параметр будет записана ссылка на
функцию, а не результат ее вызова.
console.log(add(1, 1)); // 2
console.log(add(1)); // 2
function getValue(value) {
return value + 5;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 7
console.log(add(1, 1)); // 2
console.log(add(undefined, 1)); // вызовет ошибку
function getValue(value) {
return value + 5;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 7
При первом вызове функции add() во временную мертвую зону для параметров
добавляются привязки first и second (подобно тому, как действует let). Параметр
second может инициализироваться значением first, потому что к этому моменту
first уже инициализирован, но обратный порядок инициализации невозможен.
Теперь рассмотрим переписанную версию функции add():
console.log(add(1, 1)); // 2
console.log(add(undefined, 1)); // вызовет ошибку
ПРИМЕЧАНИЕ
Параметры функции имеют собственную область видимости и собственную времен
ную мертвую зону отдельно от области видимости тела функции. Это означает, что
в значении параметра по умолчанию нельзя обращаться ни к каким переменным,
объявленным в теле функции.
Неименованные параметры
До сих пор в этой главе рассматривались только примеры параметров с именами,
присвоенными в определении функции. Однако в JavaScript количество параметров,
которые можно передавать функциям, не ограничивается количеством объявлен-
ных именованных параметров. Вы всегда можете передать меньше или больше
параметров, чем формально определено. Значения параметров по умолчанию ясно
показывают, что функция способна принимать меньшее количество параметров,
но ECMAScript 6 помогает не менее ясно показать, что функция может также при-
нимать больше параметров, чем определено.
function pick(object) {
let result = Object.create(null);
return result;
68 Глава 3 • Функции
let book = {
title: "Understanding ECMAScript 6",
author: "Nicholas C. Zakas",
year: 2016
};
Остаточные параметры
Остаточный параметр (rest parameter) отмечается троеточием (... ), предше-
ствующим именованному параметру. Такой именованный параметр превращается
в массив Array, содержащий все остальные параметры, переданные в вызов функции,
откуда и взялось название остаточные. Например, с использованием остаточных
параметров функцию pick() можно переписать, как показано ниже:
return result;
}
Неименованные параметры 69
ПРИМЕЧАНИЕ
Остаточные параметры не влияют на свойство length функции, которое определяет
количество именованных параметров. В этом примере свойство length функции
pick() имеет значение 1, потому что учитывает только присутствие параметра object.
return result;
}
Здесь параметр last следует за остаточным параметром keys, что вызывает син-
таксическую ошибку.
Второе ограничение: остаточные параметры не могут использоваться в методах
записи свойств в литералах объектов. Следовательно, следующий код также вы-
зовет синтаксическую ошибку:
let object = {
Это ограничение объясняется тем, что в литералах объектов методы записи свойств
могут принимать только один аргумент. Остаточные параметры по определению
70 Глава 3 • Функции
function checkArgs(...args) {
console.log(args.length);
console.log(arguments.length);
console.log(args[0], arguments[0]);
console.log(args[1], arguments[1]);
}
checkArgs("a", "b");
2
2
a a
b b
Дополнительные возможности
конструктора Function
Конструктор Function — редко используемая часть JavaScript, позволяющая
динамически создавать новые функции. В аргументах конструктору передаются
параметры и тело функции, все в виде строк. Например:
console.log(add(1, 1)); // 2
console.log(add(1, 1)); // 2
console.log(add(1)); // 2
console.log(pickFirst(1, 2)); // 1
Оператор расширения
С остаточными параметрами тесно связан оператор расширения (spread). Но, в от-
личие от остаточных параметров, позволяющих объединить в массив множество
независимых аргументов, оператор расширения дает возможность передать массив,
который должен быть разбит и передан в функцию в виде отдельных аргументов.
Рассмотрим встроенный метод Math.max(), принимающий произвольное количество
аргументов и возвращающий наибольшее значение. Ниже показан простейший
случай применения этого метода:
console.log(Math.max(value1, value2)); // 50
Когда имеется всего два значения, как в данном примере, пользоваться методом
Math.max() очень просто. Достаточно передать два значения и получить наиболь-
шее значение. Но представьте, что имеется целый массив значений и требуется
получить наибольшее из них. Метод Math.max() не принимает массивы, поэтому
в ECMAScript 5 и более ранних версиях приходилось выполнять поиск в массиве
вручную или применять метод apply(), как показано ниже:
72 Глава 3 • Функции
// эквивалентно вызову
// console.log(Math.max(25, 50, 75, 100));
console.log(Math.max(...values)); // 100
Теперь вызов Math.max() выглядит более понятным и не требует в простых опера-
циях прибегать к таким сложностям, как передача привязки this (первый аргумент
Math.max.apply() в предыдущем примере).
Оператор расширения можно также использовать с другими аргументами. Пред-
положим, необходимо, чтобы наименьшим числом, возвращаемым методом Math.
max() , был 0 (чтобы исключить возврат отрицательных чисел). В этом случае
данное значение можно передать в отдельном аргументе и использовать оператор
расширения для остальных, как показано ниже:
console.log(Math.max(...values, 0)); // 0
Свойство name
Идентификация функций в JavaScript может вызывать определенные сложности,
учитывая богатство способов определения функций. Кроме того, широкое приме-
нение анонимных функций-выражений усложняет отладку, потому что приходится
Свойство name 73
console.log(doSomething.name); // "doSomething"
console.log(doAnotherThing.name); // "doAnotherThing"
var person = {
get firstName() {
return "Nicholas"
},
sayName: function() {
console.log(this.name);
}
}
console.log(doSomething.name); // "doSomethingElse"
console.log(person.sayName.name); // "sayName"
console.log(person.firstName.name); // "get firstName"
74 Глава 3 • Функции
function Person(name) {
this.name = name;
}
Для создания notAPerson функция Person() вызывается без ключевого слова new
и возвращает значение undefined (а в нестрогом режиме дополнительно устанавли-
вает значение свойства name глобального объекта). Первая заглавная буква в имени
Person — единственный признак, широко используемый в программах на JavaScript
и указывающий на то, что функция предполагает вызов с ключевым словом new.
Эта путаница, связанная с двойственной природой функций, стала причиной не-
которых изменений в ECMAScript 6.
Функции в JavaScript имеют два разных внутренних метода: [[Call]] и [[Construct]].
Когда функция вызывается без ключевого слова new, выполняется метод [[Call]],
который выполняет тело функции, присутствующее в коде. Когда функция вызыва-
ется с ключевым словом new, выполняется метод [[Construct]]. Метод [[Construct]]
создает новый объект, который еще называют экземпляром, и затем выполняет
тело функции, предварительно присвоив этот экземпляр ссылке this. Функции,
имеющие метод [[Construct]], называют конструкторами.
Имейте в виду, что не все функции имеют метод [[Construct]] и, соответственно,
не все функции могут вызываться с ключевым словом new. Например, стрелочные
функции, которые обсуждаются в разделе «Стрелочные функции» ниже, не имеют
метода [[Construct]].
function Person(name) {
if (this instanceof Person) {
this.name = name; // с ключевым словом new
} else {
throw new Error("You must use new with Person.")
}
}
function Person(name) {
if (this instanceof Person) {
this.name = name;
} else {
throw new Error("You must use new with Person.")
}
}
Метасвойство new.target
Чтобы дать функциям возможность определить факт вызова с ключевым словом
new, ECMAScript 6 определяет новое метасвойство new.target. Метасвойство —
это необъектное свойство с дополнительной информацией о его цели (такой, как
new). Когда вызывается метод [[Construct]] функции, в new.target сохраняется
цель оператора new. Такой целью обычно является конструктор вновь созданного
экземпляра объекта, который будет присвоен ссылке this в теле функции. Когда
вызывается метод [[Call]], new.target получает значение undefined.
Это новое метасвойство позволяет с полной уверенностью отличить вызов функции
с ключевым словом new , как показано ниже:
function Person(name) {
if (typeof new.target !== "undefined") {
this.name = name;
} else {
throw new Error("You must use new with Person.")
}
}
function Person(name) {
if (typeof new.target === Person) {
Функции уровня блоков 77
this.name = name;
} else {
throw new Error("You must use new with Person.")
}
}
function AnotherPerson(name) {
Person.call(this, name);
}
Для нормальной работы этот код требует, чтобы метасвойство new.target имело
тип Person. Вызов Person.call(this, name), который производится в вызове new
AnotherPerson("Nicholas"), возбудит ошибку, потому что в этом случае new.target
внутри конструктора Person получит значение undefined (так как он был вызван
без ключевого слова new).
ВНИМАНИЕ
Использование new.target за пределами функции вызовет синтаксическую ошибку.
"use strict";
if (true) {
function doSomething() {
// пустая
}
}
"use strict";
if (true) {
function doSomething() {
// пустая
}
doSomething();
}
"use strict";
if (true) {
doSomething();
}
console.log(typeof doSomething);
Стрелочные функции 79
Этот код прекратит выполнение, когда встретится оператор typeof doSomething, по-
тому что инструкция let к этому моменту еще не была выполнена и идентификатор
doSomething() находится во временной мертвой зоне. Зная это отличие, вы сможете
выбирать между функциями уровня блока и выражениями let в зависимости от
того, желаете ли получить эффект подъема.
// Поведение в ECMAScript 6
if (true) {
function doSomething() {
// пустая
}
doSomething();
}
Стрелочные функции
Одним из интереснейших новшеств в ECMAScript 6 являются стрелочные функ-
ции (arrow functions). Стрелочные функции, как следует из названия, объявляются
с применением нового синтаксиса, в котором используется оператор стрелка (=>).
Кроме того, стрелочные функции имеют несколько важных отличий от традици-
онных функций JavaScript:
80 Глава 3 • Функции
ПРИМЕЧАНИЕ
Стрелочные функции имеют свойство name, которое подчиняется тем же правилам,
что и свойство name других функций.
Стрелочные функции 81
Внутри фигурных скобок допустимо практически все то же самое, что и в тра-
диционных функциях, за исключением объекта arguments, который отсутствует
в стрелочных функциях.
Если потребуется объявить функцию, которая ничего не делает, достаточно просто
указать пустые фигурные скобки, например:
return {
getName: function() {
return name;
}
};
}("Nicholas");
console.log(person.getName()); // "Nicholas"
return {
getName: function() {
return name;
}
};
})("Nicholas");
console.log(person.getName()); // "Nicholas"
let PageHandler = {
id: "123456",
init: function() {
document.addEventListener("click", function(event) {
84 Глава 3 • Функции
this.doSomething(event.type); // ошибка
}, false);
},
doSomething: function(type) {
console.log("Handling " + type + " for " + this.id);
}
};
let PageHandler = {
id: "123456",
init: function() {
document.addEventListener("click", (function(event) {
this.doSomething(event.type); // ошибки нет
}).bind(this), false);
},
doSomething: function(type) {
console.log("Handling " + type + " for " + this.id);
}
};
Теперь код будет работать как ожидается, хотя выглядит немного странно. Вызы-
вая bind(this), вы фактически создаете новую функцию, в которой ссылка this
связана с текущим значением this, указывающим на PageHandler. Чтобы избежать
создания дополнительной функции, этот код лучше исправить, применив стрелоч-
ную функцию.
Стрелочные функции не имеют привязки this, поэтому значение this внутри
стрелочной функции определяется только цепочкой областей видимости. Если
стрелочная функция находится внутри нестрелочной функции, ссылка this в ней
будет иметь то же значение, что и во вмещающей функции; в противном случае
ссылка this будет не определена. Ниже демонстрируется один из вариантов реа-
лизации желаемого поведения с применением стрелочной функции:
Стрелочные функции 85
let PageHandler = {
id: "123456",
init: function() {
document.addEventListener("click",
event => this.doSomething(event.type), false);
},
doSomething: function(type) {
console.log("Handling " + type + " for " + this.id);
}
};
В этом примере вызов new MyType() приведет к ошибке, потому что MyType — стре-
лочная функция, не имеющая метода [[Construct]]. Знание того, что стрелочные
функции не могут использоваться с ключевым словом new, позволяет движкам
JavaScript оптимизировать их выполнение.
Кроме того, поскольку значение this определяется функцией, вмещающей стрелоч-
ную функцию, внутри стрелочной функции нельзя изменить это значение вызовом
call(), apply() или bind().
Довольно много кода для простой процедуры. Сравните это с более краткой версией
на основе стрелочной функции:
Методы массивов, принимающие функции обратного вызова, такие как sort(), map()
и reduce(), могут получать дополнительные выгоды от лаконичного синтаксиса
стрелочных функций, с помощью которого внешне сложные процедуры можно
выразить в простом коде.
function createArrowFunctionReturningFirstArg() {
return () => arguments[0];
}
console.log(arrowFunction()); // 5
Кроме того, как и любые другие функции, стрелочные функции имеют методы
call(), apply() и bind(), правда, при этом они не затрагивают привязку this. Вот
несколько примеров:
console.log(sum.call(null, 1, 2)); // 3
console.log(sum.apply(null, [1, 2])); // 3
console.log(boundSum()); // 3
function doSomething() {
return doSomethingElse(); // хвостовой вызов
}
"use strict";
function doSomething() {
// оптимизируется
return doSomethingElse();
}
"use strict";
function doSomething() {
// не оптимизируется - отсутствует оператор return
doSomethingElse();
}
"use strict";
function doSomething() {
// не оптимизируется - после вызова выполняется операция сложения
return 1 + doSomethingElse();
}
"use strict";
function doSomething() {
// не оптимизируется - вызов выполняется не последним
Оптимизация хвостовых вызовов 89
"use strict";
function doSomething() {
var num = 1,
func = () => num;
function factorial(n) {
if (n <= 1) {
return 1;
} else {
// не оптимизируется - после вызова выполняется операция умножения
return n * factorial(n - 1);
}
}
Эта версия функции не может быть оптимизирована, потому что после рекурсив-
ного вызова factorial() требуется выполнить умножение. Если n — очень большое
число, количество кадров в стеке вызовов может увеличиться до такой степени, что
вызовет его переполнение.
90 Глава 3 • Функции
function factorial(n, p = 1) {
if (n <= 1) {
return 1 * p;
} else {
let result = n * p;
// оптимизируется
return factorial(n - 1, result);
}
}
ВНИМАНИЕ
На момент написания этих строк оптимизация хвостовых вызовов в ECMAScript 6 под
вергалась переработке. Вполне возможно, что для большей ясности включение этой
оптимизации будет реализовано с применением какого-то особого синтаксиса. Продол
жающиеся обсуждения могут вылиться в изменения в ECMAScript 8 (ECMAScript 2017).
В заключение
Функции не подверглись существенным изменениям в спецификации ECMAScript 6,
но в ней появились дополнительные изменения, упрощающие работу с функциями.
Значения параметров по умолчанию позволяют указать, какое значение должен
получить конкретный аргумент, если он не был передан в вызов. До ECMAScript 6
для этого требовалось писать дополнительный код внутри функции, чтобы прове-
рить наличие аргументов и присвоить им соответствующие значения по умолчанию.
В заключение 91
Категории объектов
В JavaScript используется разная терминология для описания объектов, опреде-
ляемых стандартом и добавляемых средами выполнения, такими как браузеры.
Спецификация ECMAScript 6 четко определяет каждую категорию объектов. По-
этому знание данной терминологии совершенно необходимо, чтобы освоить язык
в целом. Определены следующие категории объектов:
var person = {
name: "Nicholas",
sayName: function() {
console.log(this.name);
}
};
var person = {
name: "Nicholas",
sayName() {
console.log(this.name);
}
};
ПРИМЕЧАНИЕ
Свойство name метода, созданного с помощью сокращенного синтаксиса, получает
строку с именем, указанным перед круглыми скобками. В данном примере свойство
name метода person.sayName() получит значение "sayName".
Так как переменной lastName присвоена строка "last name", оба свойства в этом
примере получают имена, содержащие пробел, что делает невозможным обраще-
ние к ним с использованием точечной нотации. Квадратные скобки, напротив,
позволяют использовать любые символы в именах свойств, поэтому присваивание
свойству "first name" значения "Nicholas" и свойству "last name" значения "Zakas"
выполняется без ошибок.
Кроме того, в литералах объектов допускается использовать строки в качестве
имен свойств:
var person = {
"first name": "Nicholas"
};
Этот прием можно использовать, когда имена свойств известны заранее и могут быть
представлены в виде строковых литералов. Однако если имя свойства "first name"
хранится в переменной (как в предыдущем примере) или должно вычисляться, вы
не сможете определить его в литерале объекта в ECMAScript 5.
В ECMAScript 6 вычисляемые имена свойств стали частью синтаксиса литералов
объектов и используют ту же форму записи с квадратными скобками, которая
96 Глава 4 • Расширенные возможности объектов
let person = {
"first name": "Nicholas",
[lastName]: "Zakas"
};
Квадратные скобки внутри литерала объекта подсказывают, что имя свойства вы-
числяется и интерпретируется как строка. Это означает, что в квадратных скобках
можно использовать даже выражения, как в следующем примере:
var person = {
["first" + suffix]: "Nicholas",
["last" + suffix]: "Zakas"
};
Имена этих свойств вычисляются как "first name" и "last name", и эти строки
можно использовать для обращения к свойствам позднее. Любые выражения, до-
пустимые в квадратных скобках при обращении к свойствам экземпляров объектов,
допустимы также внутри литералов объектов.
Новые методы
Разработчики ECMAScript, начиная с ECMAScript 5, стремились избежать до-
бавления новых глобальных функций и методов в Object.prototype. Вместо этого,
когда требовалось добавить в стандарт новые методы, они добавляли их в соответ-
ствующие существующие объекты. В результате глобальный объект Object получал
новые методы, только когда они были неуместны в других объектах. ECMAScript 6
ввела в глобальный объект Object два новых метода, которые упрощают решение
некоторых задач.
Метод Object.is()
Когда в JavaScript требуется сравнить два значения, обычно используют опера-
тор равенства (==) или идентичности (===). Многие разработчики предпочитают
Новые методы 97
Метод Object.assign()
Примеси (mixins) — один из самых популярных шаблонов компоновки объектов
в JavaScript. В примеси один объект получает свойства и методы другого объ-
екта. Многие библиотеки на JavaScript включают метод mixin, напоминающий
следующий:
return receiver;
}
98 Глава 4 • Расширенные возможности объектов
myObject.emit("somethingChanged");
ПРИМЕЧАНИЕ
Похожие методы с той же функциональностью в разных библиотеках могут иметь
другие имена; популярными альтернативами являются методы extend() и mix().
Вслед за методом Object.assign() вскоре в ECMAScript 6 был добавлен метод Object.
mixin(). Основное отличие между ними заключается в том, что Object.mixin()
копирует также методы доступа к свойствам, но потом этот метод был удален из-за
проблем, возникающих при использовании super (обсуждается ниже в разделе «Про
стой доступ к прототипу с помощью ссылки super»).
var myObject = {}
Object.assign(myObject, EventTarget.prototype);
myObject.emit("somethingChanged");
Object.assign(receiver,
{
type: "js",
name: "file.js"
},
{
type: "css"
}
);
console.log(receiver.type); // "css"
console.log(receiver.name); // "file.js"
}
};
Object.assign(receiver, supplier);
console.log(descriptor.value); // "file.js"
console.log(descriptor.get); // undefined
"use strict";
var person = {
name: "Nicholas",
name: "Greg" // синтаксическая ошибка в строгом режиме ES5
};
"use strict";
var person = {
name: "Nicholas",
name: "Greg" // нет ошибки в строгом режиме ES6
};
console.log(person.name); // "Greg"
В этом примере свойство person.name получит значение "Greg", потому что оно
было присвоено последним.
Порядок перечисления собственных свойств 101
var obj = {
a: 1,
0: 1,
c: 1,
2: 1,
b: 1,
1: 1
};
obj.d = 1;
console.log(Object.getOwnPropertyNames(obj).join("")); // "012acbd"
ПРИМЕЧАНИЕ
Цикл for-in по-прежнему не гарантирует какой-то определенный порядок перечис
ления свойств, потому что он по-разному реализован в разных движках JavaScript.
Методы Object.keys() и JSON.stringify() перечисляют свойства в том же (не
определенном) порядке, что и цикл for-in.
Расширения в прототипах
Прототипы — основа наследования в JavaScript, и спецификация ECMAScript 6
продолжила вносить усовершенствования в прототипы. Ранние версии JavaScript
строго ограничивали круг допустимых операций с прототипами. Однако по мере
развития языка и освоения программистами особенностей работы с прототипами
стало очевидно, что разработчикам требуется более полный контроль над про-
тотипами и простые средства для работы с ними. В результате в ECMAScript 6
появилось несколько усовершенствований прототипов.
let person = {
getGreeting() {
return "Hello";
}
};
let dog = {
getGreeting() {
return "Woof";
}
};
// прототип - person
let friend = Object.create(person);
console.log(friend.getGreeting()); // "Hello"
Расширения в прототипах 103
В этом фрагменте определяются два простых объекта: person и dog. Оба объекта
имеют метод getGreeting(), возвращающий строку. Объект friend первоначально
наследует объект person, соответственно его метод getGreeting() выводит "Hello".
После смены прототипа на объект dog вызов person.getGreeting() вывел "Woof",
потому что первоначальная связь с объектом person была разорвана.
Фактическая ссылка на прототип объекта хранится во внутреннем свойстве
[[Prototype]]. Метод Object.getPrototypeOf() возвращает значение свойства
[[Prototype]] и метод Object.setPrototypeOf() изменяет значение свойства
[[Prototype]]. Однако эти два метода — не единственный способ доступа к зна-
чению [[Prototype]].
let person = {
getGreeting() {
return "Hello";
}
};
let dog = {
getGreeting() {
return "Woof";
}
};
let friend = {
getGreeting() {
return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi!";
}
};
let friend = {
getGreeting() {
// в предыдущем примере ту же операцию выполняла инструкция:
// Object.getPrototypeOf(this).getGreeting.call(this)
return super.getGreeting() + ", hi!";
}
};
let friend = {
getGreeting: function() {
// синтаксическая ошибка
return super.getGreeting() + ", hi!";
}
};
let person = {
getGreeting() {
return "Hello";
}
};
// прототип - person
let friend = {
getGreeting() {
return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi!";
}
};
Object.setPrototypeOf(friend, person);
// прототип - friend
let relative = Object.create(friend);
console.log(person.getGreeting()); // "Hello"
console.log(friend.getGreeting()); // "Hello, hi!"
console.log(relative.getGreeting()); // ошибка!
let person = {
getGreeting() {
return "Hello";
}
};
// прототип - person
let friend = {
getGreeting() {
return super.getGreeting() + ", hi!";
}
};
Object.setPrototypeOf(friend, person);
// прототип - friend
let relative = Object.create(friend);
console.log(person.getGreeting()); // "Hello"
console.log(friend.getGreeting()); // "Hello, hi!"
console.log(relative.getGreeting()); // "Hello, hi!"
106 Глава 4 • Расширенные возможности объектов
let person = {
// метод
getGreeting() {
return "Hello";
}
};
// не метод
function shareGreeting() {
return "Hi!";
}
let person = {
getGreeting() {
return "Hello";
}
};
// прототип - person
let friend = {
getGreeting() {
return super.getGreeting() + ", hi!";
В заключение 107
}
};
Object.setPrototypeOf(friend, person);
В заключение
Объекты — основа программирования на языке JavaScript, и спецификация
ECMAScript 6 привнесла несколько интересных изменений в объекты, которые
упрощают работу с ними и делают их более гибкими.
Несколько изменений в ECMAScript 6 затрагивают литералы объектов. Сокра-
щенный синтаксис определения свойств упрощает их инициализацию значениями
одноименных локальных переменных. Поддержка вычисляемых имен свойств
позволяет использовать небуквенные символы, которые допустимы в других
конструкциях языка. Сокращенный синтаксис определения методов позволяет
сэкономить несколько символов при определении методов в литералах объектов,
отбросив двоеточие и ключевое слово function. ECMAScript 6 ослабила требования
строгого режима, устранив проверку дубликатов свойств в литералах объектов.
Теперь литерал объекта может содержать два определения свойств с одинаковыми
именами, не вызывая ошибки.
Новый метод Object.assign() упрощает изменение свойств в одном объекте
и с успехом может использоваться для реализации шаблона «Примесь». Метод
Object.is() выполняет строгое сравнение любых значений и фактически является
безопасной версией оператора ===, поддерживающей особые значения JavaScript.
Спецификация ECMAScript 6 ясно определила порядок перечисления собственных
свойств объектов. Первыми всегда возвращаются числовые свойства, следующие
в порядке возрастания, за ними идут строковые ключи в порядке добавления в объ-
ект при его определении, а за ними следуют символические ключи также в порядке
добавления в объект.
Теперь благодаря введению в ECMAScript 6 метода Object.setPrototypeOf() по-
явилась возможность менять прототип объекта после его создания.
Кроме того, вызов методов прототипа объекта теперь можно осуществлять с по-
мощью ссылки super. Привязка this внутри метода, вызванного с помощью super,
автоматически настраивается для работы с текущим объектом.
5 Деструктуризация
для упрощения доступа
к данным
let options = {
repeat: true,
save: false
};
Этот код извлекает значения свойств repeat и save из объекта options и сохраняет
данные в одноименных локальных переменных. Хотя этот код выглядит вполне
простым, но представьте, что требуется присвоить значения большому количеству
переменных — вам придется для каждой написать отдельную инструкцию при-
сваивания. А если потребуется обойти вложенную структуру данных в поисках
информации, для этого может понадобиться углубиться в структуру до самого
нижнего уровня, только чтобы найти нужные сведения.
Деструктуризация объектов 109
Деструктуризация объектов
В синтаксисе деструктуризации объектов слева от оператора присваивания ис-
пользуется литерал объекта. Например:
let node = {
type: "Identifier",
name: "foo"
};
console.log(type); // "Identifier"
console.log(name); // "foo"
// синтаксическая ошибка!
let { type, name };
// синтаксическая ошибка!
const { type, name };
Присваивание с деструктуризацией
В примерах деструктуризации объектов, показанных выше, использовались объ-
явления переменных. Однако деструктуризацию можно выполнять в обычных
операциях присваивания. Например, можно изменить значения переменных после
их определения, как показано ниже:
let node = {
type: "Identifier",
name: "foo"
},
type = "Literal",
name = 5;
console.log(type); // "Identifier"
console.log(name); // "foo"
let node = {
type: "Identifier",
name: "foo"
},
type = "Literal",
name = 5;
function outputInfo(value) {
console.log(value === node); // true
}
console.log(type); // "Identifier"
console.log(name); // "foo"
Деструктуризация объектов 111
ПРИМЕЧАНИЕ
Если правая часть выражения присваивания с деструктуризацией (после знака =)
вычисляется как null или undefined, генерируется ошибка. Это объясняется тем,
что любая попытка прочитать свойство из значения null или undefined приводит
к ошибке времени выполнения.
Значения по умолчанию
Когда в выражении присваивания с деструктуризацией указывается локальная
переменная, для которой в объекте отсутствует одноименное свойство, она полу-
чает значение undefined. Например:
let node = {
type: "Identifier",
name: "foo"
};
console.log(type); // "Identifier"
console.log(name); // "foo"
console.log(value); // undefined
let node = {
type: "Identifier",
name: "foo"
};
console.log(type); // "Identifier"
console.log(name); // "foo"
console.log(value); // true
112 Глава 5 • Деструктуризация для упрощения доступа к данным
let node = {
type: "Identifier",
name: "foo"
};
console.log(localType); // "Identifier"
console.log(localName); // "foo"
let node = {
type: "Identifier"
};
Деструктуризация объектов 113
console.log(localType); // "Identifier"
console.log(localName); // "bar"
let node = {
type: "Identifier",
name: "foo",
loc: {
start: {
line: 1,
column: 1
},
end: {
line: 1,
column: 4
}
}
};
console.log(start.line); // 1
console.log(start.column); // 1
let node = {
type: "Identifier",
name: "foo",
loc: {
start: {
line: 1,
column: 1
},
end: {
line: 1,
column: 4
}
}
};
// извлечь node.loc.start
let { loc: { start: localStart }} = node;
console.log(localStart.line); // 1
console.log(localStart.column); // 1
СИНТАКСИЧЕСКАЯ ЛОВУШКА
Будьте осторожны, используя синтаксис деструктуризации вложенных эле-
ментов, потому что можно по неосторожности создать инструкцию, не оказы-
вающую никакого эффекта. Пустые фигурные скобки допустимы в шаблоне
деструктуризации, но они ничего не делают. Например:
// переменная не будет создана!
let { loc: {} } = node;
Деструктуризация массивов
Синтаксис деструктуризации массивов очень напоминает деструктуризацию
объектов, только вместо синтаксиса литералов объектов используется синтаксис
литералов массивов. Деструктуризация применяется к позициям в массиве, а не
к именованным свойствам, доступным в объектах. Например:
console.log(firstColor); // "red"
console.log(secondColor); // "green"
console.log(thirdColor); // "blue"
ПРИМЕЧАНИЕ
По аналогии с деструктуризацией объектов при выполнении деструктуризации масси
ва в операторах var, let или const всегда требуется указывать инициализирующее
значение.
116 Глава 5 • Деструктуризация для упрощения доступа к данным
Присваивание с деструктуризацией
Деструктуризацию массивов можно использовать в контексте операции присваива-
ния, но, в отличие от деструктуризации объектов, в этом случае нет необходимости
заключать выражение в круглые скобки. Взгляните на следующий пример.
console.log(firstColor); // "red"
console.log(secondColor); // "green"
tmp = a;
a = b;
b = tmp;
console.log(a); // 2
console.log(b); // 1
[ a, b ] = [ b, a ];
console.log(a); // 2
console.log(b); // 1
Деструктуризация массивов 117
ПРИМЕЧАНИЕ
По аналогии с операцией присваивания объектов с деструктуризацией, если правая
часть выражения присваивания вернет null или undefined, интерпретатор сгене
рирует ошибку.
Значения по умолчанию
Синтаксис присваивания массивов с деструктуризацией поддерживает возможность
определения значений по умолчанию для любых элементов массива. Значение
по умолчанию используется в случае отсутствия элемента в данной позиции или
элемент имеет значение undefined. Например:
console.log(firstColor); // "red"
console.log(secondColor); // "green"
В этом примере массив colors имеет только один элемент, то есть в нем отсутствует
элемент, соответствующий переменной secondColor в шаблоне деструктуризации.
Поэтому переменная secondColor получила значение по умолчанию "green" вместо
undefined.
// позднее
console.log(firstColor); // "red"
console.log(secondColor); // "green"
118 Глава 5 • Деструктуризация для упрощения доступа к данным
Остаточные элементы
В главе 3 были представлены остаточные параметры функций. Синтаксис деструкту-
ризации массивов имеет схожее понятие, которое называется остаточные элементы
(rest items). Для обозначения остаточных элементов используется синтаксис ...,
который служит для присваивания оставшихся элементов в массиве конкретной
переменной. Взгляните на следующий пример:
console.log(firstColor); // "red"
console.log(restColors.length); // 2
console.log(restColors[0]); // "green"
console.log(restColors[1]); // "blue"
console.log(clonedColors); // "[red,green,blue]"
console.log(clonedColors); // "[red,green,blue]"
Смешанная деструктуризация 119
ПРИМЕЧАНИЕ
Остаточные элементы должны быть последним компонентом в шаблоне деструкту
ризации массива и за ними не может следовать запятая. Добавление запятой после
остаточных элементов вызывает синтаксическую ошибку.
Смешанная деструктуризация
Шаблоны деструктуризации объектов и массивов можно смешивать для созда-
ния более сложных выражений. Используя этот прием, можно извлекать только
требуемые фрагменты информации из смеси объектов и массивов. Взгляните на
следующий пример:
let node = {
type: "Identifier",
name: "foo",
loc: {
start: {
line: 1,
column: 1
},
end: {
line: 1,
column: 4
}
},
range: [0, 3]
};
let {
loc: { start },
range: [ startIndex ]
} = node;
console.log(start.line); // 1
console.log(start.column); // 1
console.log(startIndex); // 0
Деструктурированные параметры
Деструктуризация имеет еще одну область применения, где она оказывается осо-
бенно удобной, — это передача аргументов функциям. Когда функция принимает
большое количество необязательных аргументов, разработчики часто создают объ-
ект options со свойствами, определяющими необязательные параметры. Например:
setCookie("type", "js", {
Деструктурированные параметры 121
secure: true,
expires: 60000
});
ПРИМЕЧАНИЕ
Деструктурированные параметры обладают всеми свойствами деструктуризации,
с которыми вы познакомились в этой главе. В них можно использовать значения по
умолчанию, смешивать шаблоны деструктуризации объектов и массивов и исполь
зовать имена переменных, отличающиеся от имен свойств передаваемых объектов.
Деструктурированные параметры
являются обязательными
Один из недостатков деструктурированных параметров заключается в том, что
в случае их отсутствия в вызове функции по умолчанию возбуждается ошибка.
Например, следующий вызов функции setCookie() из предыдущего примера при-
ведет к ошибке.
// ошибка!
setCookie("type", "js");
Так как операция деструктуризации возбуждает ошибку, если правая часть вы-
ражения возвращает null или undefined, эта же ошибка появится, если вызвать
функцию setCookie() без третьего аргумента.
Если вы хотите сделать деструктурированный параметр обязательным, такое по-
ведение не является проблемой. Но если требуется сделать деструктурированный
122 Глава 5 • Деструктуризация для упрощения доступа к данным
Значения по умолчанию
для деструктурированных параметров
Для деструктурированных параметров допускается определять деструктурирован-
ные значения по умолчанию, как это делается в операции присваивания с деструк-
туризацией. Достаточно просто добавить знак «равно» после параметра и указать
значение по умолчанию. Например:
В заключение
Деструктуризация упрощает работу с объектами и массивами в JavaScript. Исполь-
зуя знакомый синтаксис литералов объектов и массивов, вы можете извлекать из
структур данных любую необходимую информацию. Шаблоны объектов позволяют
извлекать данные из объектов, а шаблоны массивов — из массивов.
В заключение 123
Создание символов
Символы занимают уникальное положение среди других примитивов JavaScript —
они не имеют литеральной формы, как true для логических значений или 42
для чисел. Символ можно создать с помощью глобальной функции Symbol, на-
пример:
person[firstName] = "Nicholas";
console.log(person[firstName]); // "Nicholas"
Создание символов 125
ПРИМЕЧАНИЕ
Поскольку символы являются элементарными значениями, вызов new Symbol() возбу
дит ошибку. Экземпляр Symbol можно также создать вызовом new Object(yourSymbol),
но я не знаю, где такой способ мог бы пригодиться.
person[firstName] = "Nicholas";
ИДЕНТИФИКАЦИЯ СИМВОЛОВ
Поскольку символы — это элементарные значения, можно применить опе-
ратор typeof с целью определить, хранит ли некоторая переменная символ.
В ECMAScript 6 оператор typeof возвращает "symbol", когда применяется
к символу. Например:
let symbol = Symbol("test symbol");
console.log(typeof symbol); // "symbol"
Использование символов
Символы можно использовать везде, где допустимы вычисляемые имена свойств.
В этой главе вы уже видели форму применения символов в квадратных скобках,
но кроме этого символы можно использовать в вычисляемых именах свойств в ли-
тералах объектов, а также передавать в вызовы Object.defineProperty() и Object.
defineProperties():
Object.defineProperties(person, {
[lastName]: {
value: "Zakas",
writable: false
}
});
console.log(person[firstName]); // "Nicholas"
console.log(person[lastName]); // "Zakas"
object[uid] = "12345";
console.log(object[uid]); // "12345"
console.log(uid); // "Symbol(uid)"
console.log(object[uid]); // "12345"
console.log(uid); // "Symbol(uid)"
В этом примере переменные uid и uid2 хранят один и тот же символ и могут ис-
пользоваться взаимозаменяемо. Первый вызов Symbol.for() создает символ, а вто-
рой — вызов извлекает символ из глобального реестра.
Другая уникальная особенность совместно используемых символов заключается
в возможности извлекать ключ, связанный с символом в глобальном реестре, вы-
зовом метода Symbol.keyFor(). Например:
128 Глава 6 • Символы и символьные свойства
ПРИМЕЧАНИЕ
Глобальный реестр символов является совместно используемым окружением, в точ
ности как глобальная область видимости. Это означает, что нельзя делать никаких
предположений о том, что уже имеется или отсутствует в этом окружении. При
меняйте пространства имен для ключей символов, чтобы уменьшить вероятность
конфликтов при использовании сторонних компонентов. Например, в jQuery ко
всем ключам символов может добавляться префикс "jquery.", например "jquery.
element".
console.log(desc); // "Symbol(uid)"
console.log(symbols.length); // 1
console.log(symbols[0]); // "Symbol(uid)"
console.log(object[symbols[0]]); // "12345"
Метод Symbol.hasInstance
Каждая функция имеет метод Symbol.hasInstance, определяющий, является ли дан-
ный объект экземпляром этой функции. Метод определяется в Function.prototype,
поэтому все функции наследуют поведение по умолчанию для свойства instanceof.
Свойство Symbol.hasInstance определено как недоступное для записи, настройки
и перечисления, чтобы гарантировать невозможность его перезаписи по ошибке.
Метод Symbol.hasInstance принимает единственный аргумент: проверяемое значе-
ние. Он возвращает true, если указанное значение является экземпляром функции.
Чтобы проще было понять, как действует метод Symbol.hasInstance, взгляните на
следующую инструкцию:
Array[Symbol.hasInstance](obj);
function MyObject() {
// пустая
}
Object.defineProperty(MyObject, Symbol.hasInstance, {
value: function(v) {
return false;
}
});
function SpecialNumber() {
// пустая
}
Object.defineProperty(SpecialNumber, Symbol.hasInstance, {
value: function(v) {
return (v instanceof Number) && (v >=1 && v <= 100);
}
});
ПРИМЕЧАНИЕ
Можно также перезаписать свойство по умолчанию Symbol.hasInstance во всех
встроенных функциях, таких как Date и Error. Однако это не рекомендуется, потому
что влияние на код может оказаться весьма неожиданным. Лучше переопределять
метод Symbol.hasInstance только для собственных функций, и только когда это
действительно необходимо.
Свойство Symbol.isConcatSpreadable
Метод concat() массивов в JavaScript предназначен для слияния двух массивов.
Ниже показано, как он используется:
Экспортирование внутренних операций в виде стандартных символов 133
console.log(colors2.length); // 4
console.log(colors2); // ["red","green","blue","black"]
Этот код добавляет новый массив в конец colors1 и создает colors2 — единый
массив, включающий все элементы из обоих массивов. Но метод concat() может
также принимать аргументы, не являющиеся массивами; такие аргументы просто
добавляются в конец массива. Например:
console.log(colors2.length); // 5
console.log(colors2); // ["red","green","blue","black","brown"]
let collection = {
0: "Hello",
1: "world",
length: 2,
[Symbol.isConcatSpreadable]: true
};
console.log(messages.length); // 3
console.log(messages); // ["hi","Hello","world"]
Объект collection в этом примере настроен так, что выглядит как массив: он име-
ет свойство length и два числовых ключа. Свойству Symbol.isConcatSpreadable
присвоено значение true с целью показать, что значения свойств объекта должны
добавляться в массив по отдельности. Когда concat() получает объект collection,
134 Глава 6 • Символы и символьные свойства
ПРИМЕЧАНИЕ
Свойству Symbol.isConcatSpreadable можно также присвоить значение false
в классах, производных от массивов, чтобы предотвратить их деление на элементы
в вызовах concat(). Подробности смотрите в разделе «Наследование в производных
классах» главы 9.
console.log(match1); // null
console.log(match2); // ["Hello John"]
console.log(search1); // -1
console.log(search2); // 0
split2 = message2.split(hasLengthOf10);
Метод Symbol.toPrimitive
JavaScript часто пытается неявно преобразовывать объекты в элементарные значе-
ния, когда применяются определенные операции. Например, при сравнении строки
с объектом с использованием оператора равенства (==) перед сравнением объект
преобразуется в элементарное значение. Каким должно быть это элементарное
значение, раньше определялось внутренними операциями, но ECMAScript 6 от-
крыла доступ к этому механизму (сделав его изменяемым) в виде метода Symbol.
toPrimitive.
Метод Symbol.toPrimitive определяется в прототипе каждого стандартного типа
и реализует преобразование объектов в элементарные значения. Когда возникает
необходимость в таком преобразовании, вызывается метод Symbol.toPrimitive
с единственным аргументом, который в спецификации упоминается под именем
hint. Аргумент hint может принимать одно из трех строковых значений. Если
в нем передается строка "number", метод Symbol.toPrimitive должен вернуть число.
При передаче строки "string" должна быть возвращена строка, а передача строки
"default" означает, что операция не имеет каких-то предпочтений в отношении типа.
Для большинства стандартных объектов числовой режим действует, как описы-
вается ниже:
1. Вызывается метод valueOf() и возвращается его результат, если он является
элементарным значением.
2. Иначе вызывается метод toString() и возвращается его результат, если он яв-
ляется элементарным значением.
3. Иначе возбуждается ошибка.
Экспортирование внутренних операций в виде стандартных символов 137
ПРИМЕЧАНИЕ
Режим по умолчанию используется только операторами == и +, а также когда кон
структору Date передается единственный аргумент. Большинство операций используют
строковый или числовой режим.
function Temperature(degrees) {
this.degrees = degrees;
}
Temperature.prototype[Symbol.toPrimitive] = function(hint) {
switch (hint) {
case "string":
return this.degrees + "\u00b0"; // знак градуса
case "number":
return this.degrees;
case "default":
return this.degrees + " degrees";
}
};
Свойство Symbol.toStringTag
Одной из самых интересных проблем в JavaScript было существование несколь-
ких глобальных окружений выполнения. Такое случалось в веб-браузерах, когда
страница имела плавающие фреймы (iframe), потому что страница и плавающие
фреймы имели собственные окружения выполнения. Обычно это не представляет
большой проблемы, потому что передача данных между окружениями выполняется
достаточно просто. Проблема возникает при попытке определить тип объекта после
его передачи между разными объектами.
Каноническим примером этой проблемы может служить передача массива из фрей-
ма в страницу, содержащую фрейм, или наоборот. В терминологии ECMAScript 6
плавающий фрейм и содержащая его страница представляют разные пространства
(realm) — окружения выполнения для JavaScript. Каждое пространство имеет соб-
ственную глобальную область видимости с собственными копиями глобальных
объектов. В каком бы пространстве ни создавался массив, он определенно оста-
ется массивом. Однако после передачи массива в другое пространство оператор
instanceof Array для него будет возвращать false, потому что этот массив соз-
давался конструктором из другого пространства, тогда как идентификатор Array
представляет конструктор из текущего пространства.
function isArray(value) {
return Object.prototype.toString.call(value) === "[object Array]";
}
console.log(isArray([])); // true
Экспортирование внутренних операций в виде стандартных символов 139
function supportsNativeJSON() {
return typeof JSON !== "undefined" &&
Object.prototype.toString.call(JSON) === "[object JSON]";
}
function Person(name) {
this.name = name;
}
Person.prototype[Symbol.toStringTag] = "Person";
function Person(name) {
this.name = name;
}
Person.prototype[Symbol.toStringTag] = "Person";
Person.prototype.toString = function() {
return this.name;
};
console.log(me.toString()); // "Nicholas"
console.log(Object.prototype.toString.call(me)); // "[object Person]"
ПРИМЕЧАНИЕ
Все объекты наследуют свойство Symbol.toStringTag от Object.prototype, если
оно не было переопределено явно. По умолчанию это свойство хранит строку
"Object".
function Person(name) {
this.name = name;
}
Person.prototype[Symbol.toStringTag] = "Array";
Person.prototype.toString = function() {
return this.name;
};
console.log(me.toString()); // "Nicholas"
console.log(Object.prototype.toString.call(me)); // "[object Array]"
Array.prototype[Symbol.toStringTag] = "Magic";
Свойство Symbol.unscopables
Инструкция with является одной из конструкций языка JavaScript, вызывающих
самые жаркие споры. Первоначально задумывавшаяся как средство, помогающее
избежать ввода избыточного программного кода, инструкция with подвергается
жесткой критике за то, что делает код трудночитаемым, отрицательно сказывается
на производительности и способствует появлению ошибок. В результате инструкция
with была сделана недопустимой в строгом режиме; это ограничение затрагивает
также классы и модули, которые по умолчанию включают строгий режим, не давая
никакой возможности обойти это условие.
Несмотря на то что в будущем инструкция with, вне всяких сомнений, будет ис-
ключена из языка, ECMAScript 6 все еще поддерживает ее в нестрогом режиме
для обратной совместимости, то есть с целью не нарушить работу существующего
кода, использующего with.
142 Глава 6 • Символы и символьные свойства
with(colors) {
push(color);
push(...values);
}
В этом примере два вызова push() внутри инструкции with эквивалентны вызовам
colors.push(), потому что with добавляет push как локальную привязку. Ссылка
color указывает на переменную, созданную за пределами инструкции with, то же
относится и к ссылке values.
Но ECMAScript 6 добавила в массивы новый метод values. (Метод values() под-
робно обсуждается в главе 8.) В результате в окружении ECMAScript 6 ссылка
values внутри инструкции with должна ссылаться не на локальную переменную
values, а на метод values массивов, что может нарушить нормальную работу этого
кода. Именно по этой причине был добавлен символ Symbol.unscopables.
Символ Symbol.unscopables используется в Array.prototype с целью показать, для
каких свойств инструкция with не должна создавать локальные привязки. Когда
свойство Symbol.unscopables определено и является объектом, его ключи со значе-
нием true интерпретируются как идентификаторы свойств родительского объекта,
для которых инструкция with не должна создавать локальные привязки. Ниже
приводится содержимое по умолчанию свойства Symbol.unscopables для массивов:
// встроено в ECMAScript 6 по умолчанию
Array.prototype[Symbol.unscopables] = Object.assign(Object.create(null), {
copyWithin: true,
entries: true,
fill: true,
find: true,
findIndex: true,
keys: true,
values: true
});
В заключение
Символы — это новый тип элементарных значений в JavaScript, который исполь-
зуется для создания неперечислимых свойств, недоступных без ссылки на символ.
Такие свойства не являются по-настоящему приватными, тем не менее изменить
или затереть их по ошибке существенно сложнее, а значит, они позволяют повысить
уровень безопасности для функциональных возможностей, требующих дополни-
тельной защиты от разработчиков.
Символы можно снабжать описаниями, упрощающими их идентификацию. Под-
держка глобального реестра символов дает возможность использовать в разных
фрагментах кода общие символы. Таким образом, один и тот же символ можно
использовать в разных местах.
Методы, такие как Object.keys() или Object.getOwnPropertyNames(), не возвраща-
ют символы, поэтому спецификация ECMAScript 6 добавила новый метод Object.
getOwnPropertySymbols(), чтобы дать возможность извлекать символьные свойства.
Для определения символьных свойств по-прежнему можно использовать методы
Object.defineProperty() и Object.defineProperties().
Стандартные символы открывают доступ к внутренней функциональности стан-
дартных объектов и определяются как глобальные константы-символы, такие как
свойство Symbol.hasInstance. В спецификации эти символы начинаются с префикса
Symbol. и позволяют разработчикам изменять поведение стандартных объектов.
7 Множества
и ассоциативные массивы
Долгое время в JavaScript имелся только один тип коллекций — тип Array. (Не-
которые разработчики утверждают, что все объекты помимо массивов являются
коллекциями пар ключ/значение, однако следует заметить, что первоначально они
имели иное предназначение, отличное от массивов.) Массивы в JavaScript исполь-
зуются точно так же, как массивы в других языках, но до появления ECMAScript 6
из-за нехватки коллекций других видов массивы часто применялись как очереди
и стеки. Массивы позволяют использовать только числовые индексы, поэтому, когда
возникает потребность в нечисловых индексах, разработчики используют объекты,
не являющиеся массивами. Это привело к созданию нестандартных реализаций
множеств и ассоциативных массивов на основе простых объектов.
Множество (set) — это список неповторяющихся значений. Обычно при исполь-
зовании множества нет необходимости обращаться к отдельным его элементам,
как в случае с массивами; гораздо чаще требуется просто проверить присутствие
в множестве некоторого значения. Ассоциативный массив (map) — это коллекция
ключей, соответствующих определенным значениям. Каждый элемент в ассоциа-
тивном массиве хранит два фрагмента данных, и значение извлекается из такого
массива по указанному ключу. Ассоциативные массивы часто применяются для
кэширования данных, которыми часто будут пользоваться в программе позднее.
Спецификация ECMAScript 5 не определяет множества и ассоциативные масси-
вы, тем не менее разработчики обходят это ограничение с применением обычных
объектов.
ECMAScript 6 добавила множества и ассоциативные массивы в JavaScript, и эта
глава обсуждает все, что вам следует знать об этих двух типах коллекций. Сначала
я расскажу об обходных решениях, использовавшихся разработчиками для реали-
зации множеств и ассоциативных массивов до появления ECMAScript 6, и связан-
ных с ними проблемах. Затем опишу, как действуют множества и ассоциативные
массивы в ECMAScript 6.
Недостатки обходных решений 145
set.foo = true;
// проверка наличия
if (set.foo) {
// код для выполнения
}
// извлечь значение
var value = map.foo;
console.log(value); // "bar"
свойств объектов должны быть строками, вам придется убедиться, что никакие
два ключа не могут быть представлены в виде одной и той же строки. Взгляните
на следующий пример:
console.log(map["5"]); // "foo"
console.log(map[key2]); // "foo"
map.count = 1;
ПРИМЕЧАНИЕ
В JavaScript имеется оператор in, возвращающий true, если свойство присутствует
в объекте, игнорируя его значение. Однако оператор in выполняет поиск в прототипе
объекта, что делает его безопасным, только когда объект создается с пустым про
тотипом. Но, несмотря на это, многие разработчики все еще ошибочно используют
код, как в предыдущем примере, вместо применения оператора in.
Множества в ECMAScript 6
Спецификация ECMAScript 6 добавляет тип Set — упорядоченный1 список непо-
вторяющихся значений. Множества дают быстрый доступ к содержащимся в них
данным, поддерживая более эффективный способ извлечения дискретных значений.
console.log(set.size); // 2
1
Под упорядоченностью здесь подразумевается «в порядке добавления» и ни в каком другом
порядке. — Примеч. пер.
148 Глава 7 • Множества и ассоциативные массивы
set.add(key1);
set.add(key2);
console.log(set.size); // 2
Так как key1 и key2 не преобразуются в строки, они считаются двумя уникальными
элементами в множестве. Если бы они преобразовывались в строки, оба получили
бы одно и то же значение "[object Object]".
Если вызвать метод add() несколько раз с одним и тем же значением, все вызовы,
кроме первого, будут просто проигнорированы:
console.log(set.size); // 2
Здесь множество set имеет размер 2, потому что второе значение 5 не было добав-
лено в него. Также допускается инициализировать множества массивами, при этом
конструктор Set оставит только уникальные значения. Например:
ПРИМЕЧАНИЕ
В действительности, конструктор Set принимает любые итерируемые объекты в каче
стве аргументов. Массивы поддерживаются по той простой причине, что они являются
итерируемыми по умолчанию, так же как множества и ассоциативные массивы. Для
извлечения значений из аргумента конструктор Set использует итератор. Итерируемые
объекты и итераторы обсуждаются в главе 8.
console.log(set.has(5)); // true
console.log(set.has(6)); // false
Здесь set.has(6) возвращает false, потому что это значение отсутствует в мно-
жестве.
Удаление элементов
Существует также возможность удалять элементы из множеств. Вызов метода
delete() удалит один элемент, а вызов метода clear() удалит все элементы. Сле-
дующий пример демонстрирует применение обоих методов:
console.log(set.has(5)); // true
set.delete(5);
console.log(set.has(5)); // false
console.log(set.size); // 1
set.clear();
console.log(set.has("5")); // false
console.log(set.size); // 0
Вызов delete() в этом примере удалил только элемент 5; вызов clear() очистил
множество, удалив все элементы.
Множества — очень простой механизм поддержки уникальности упорядоченных
значений. Но что если после заполнения множества потребуется выполнить неко-
торые операции с каждым элементом? Для этой цели существует метод forEach().
Версия forEach() для множеств имеет одно странное отличие от версии для мас-
сивов — первый и второй аргументы функции обратного вызова в версии для мно-
жеств получают одно и то же значение. На первый взгляд такое положение вещей
выглядит как ошибка, но оно имеет вполне разумное объяснение.
Другие объекты, обладающие методом forEach() (простые и ассоциативные мас-
сивы), передают в свои функции обратного вызова три аргумента. В первых двух
аргументах для простых и ассоциативных массивов передаются значение и ключ
(числовой индекс для простых массивов).
Однако множества не имеют ключей. Люди, стоящие за стандартом ECMAScript 6,
могли бы определить функцию обратного вызова для метода forEach() множеств
как принимающую два аргумента, но тогда ее сигнатура получилась бы отличной
от двух других версий. Вместо этого они сочли возможным сохранить сигнатуру
функции обратного вызова как функции с тремя аргументами: каждое значение
в множестве считается ключом и значением одновременно. В результате для со-
хранения функционального единообразия с методами forEach() простых и ассоци-
ативных массивов первый и второй аргументы функции обратного вызова в версии
метода forEach() множеств всегда получают одно и то же значение.
Кроме разницы в аргументах, метод forEach() множеств используется практиче-
ски так же, как одноименный метод массивов. Следующий пример демонстрирует
применение метода:
1 1
true
2 2
true
let processor = {
output(value) {
console.log(value);
Множества в ECMAScript 6 151
},
process(dataSet) {
dataSet.forEach(function(value) {
this.output(value);
}, this);
}
};
processor.process(set);
let processor = {
output(value) {
console.log(value);
},
process(dataSet) {
dataSet.forEach(value => this.output(value));
}
};
processor.process(set);
console.log(array); // [1,2,3,4,5]
function eliminateDuplicates(items) {
return [...new Set(items)];
}
console.log(noDuplicates); // [1,2,3,4,5]
set.add(key);
console.log(set.size); // 1
console.log(set.size); // 1
В этом примере присваивание значения null переменной key удаляет одну ссылку
на объект key, но остается еще одна ссылка внутри set. Вы все еще можете из-
влечь key, преобразовав множество в массив с помощью оператора расширения
Множества в ECMAScript 6 153
console.log(set.has(key)); // true
set.delete(key);
console.log(set.has(key)); // false
console.log(set.has(key1)); // true
console.log(set.has(key2)); // true
В этом примере в вызов конструктора WeakSet передается массив. Так как этот
массив содержит два объекта, эти объекты добавляются в множество со слабыми
ссылками. Имейте в виду, что если в массиве окажется хотя бы одно значение, не
являющееся объектом, это приведет к ошибке, потому что WeakSet не принимает
элементарных значений.
154 Глава 7 • Множества и ассоциативные массивы
console.log(set.has(key)); // true
После того как этот код выполнится, ссылка на key в множестве со слабыми
ссылками окажется недоступной. Проверить этот факт невозможно, потому что
для этого потребовалось бы передать ссылку на объект в вызов метода has(). Это
обстоятельство может превратить тестирование множеств со слабыми ссылками
в запутанную задачу, но вы можете верить, что ссылка действительно удаляется
движком JavaScript.
Как показывает предыдущий пример, множества со слабыми ссылками обладают
почти теми же характеристиками, что и обычные множества, но существуют не-
которые важные отличия:
Методы add(), has() и delete() экземпляра WeakSet возбуждают ошибку при
передаче им значения, не являющегося объектом.
Множества со слабыми ссылками не являются итерируемыми объектами и по-
тому не могут использоваться в цикле for-of.
Множества со слабыми ссылками не экспортируют итераторов (таких, как
методы keys() и values()), поэтому нет никакой возможности программно
определить содержимое множества со слабыми ссылками.
Множества со слабыми ссылками не имеют метода forEach().
Множества со слабыми ссылками не имеют свойства size.
Такие ограничения возможностей множеств со слабыми ссылками, по-видимому,
необходимы для корректной работы с памятью. Вообще говоря, если вам требуется
только следить за ссылками на объекты, используйте множества со слабыми ссыл-
ками вместо обычных множеств.
Множества дают новые способы обработки списков значений, но они не особенно
полезны, когда требуется сопроводить эти значения дополнительной информацией.
Именно поэтому в ECMAScript 6 были добавлены ассоциативные массивы.
Ассоциативные массивы в ECMAScript 6 155
map.set(key1, 5);
map.set(key2, 42);
console.log(map.get(key1)); // 5
console.log(map.get(key2)); // 42
В этом примере в качестве ключей используются объекты key1 и key2, для которых
в ассоциативном массиве сохраняются два разных значения. Поскольку ключи не
преобразуются ни в какую другую форму, каждый объект считается уникальным.
Это позволяет ассоциировать с объектами дополнительную информацию без из-
менения самих объектов.
1
Под упорядоченностью здесь подразумевается «в порядке добавления» и ни в каком другом
порядке. — Примеч. пер.
156 Глава 7 • Множества и ассоциативные массивы
console.log(map.size); // 2
console.log(map.has("name")); // true
console.log(map.get("name")); // "Nicholas"
console.log(map.has("age")); // true
console.log(map.get("age")); // 25
map.delete("name");
console.log(map.has("name")); // false
console.log(map.get("name")); // undefined
console.log(map.size); // 1
map.clear();
console.log(map.has("name")); // false
console.log(map.get("name")); // undefined
console.log(map.has("age")); // false
console.log(map.get("age")); // undefined
console.log(map.size); // 0
Так же как во множествах, свойство size всегда возвращает количество пар ключ/
значение в ассоциативном массиве. Экземпляр Map в этом примере сначала полу-
чает ключи "name" и "age", поэтому has() возвращает true, когда ему передается
любой из этих ключей. После удаления ключа "name" вызовом метода delete()
метод has() возвращает false для ключа "name", а свойство size показывает, что
количество элементов уменьшилось на единицу. Затем вызовом метода clear()
удаляется оставшийся ключ, о чем сообщает метод has(), возвращающий false
для обоих ключей, а свойство size получает значение 0.
Ассоциативные массивы в ECMAScript 6 157
console.log(map.has("name")); // true
console.log(map.get("name")); // "Nicholas"
console.log(map.has("age")); // true
console.log(map.get("age")); // 25
console.log(map.size); // 2
name Nicholas
true
age 25
true
ПРИМЕЧАНИЕ
Методу forEach() можно также передать второй аргумент — значение для ссылки
this внутри функции обратного вызова. Такой вызов будет действовать в точности
как версия метода forEach() для множеств.
map.set(element, "Original");
console.log(map.has(key1)); // true
console.log(map.get(key1)); // "Hello"
console.log(map.has(key2)); // true
console.log(map.get(key2)); // 42
map.set(element, "Original");
console.log(map.has(element)); // true
console.log(map.get(element)); // "Original"
map.delete(element);
console.log(map.has(element)); // false
console.log(map.get(element)); // undefined
Здесь снова в качестве ключа используется элемент DOM. Метод has() позволяет
проверить присутствие в данный момент искомого ключа в ассоциативном массиве
со слабыми ссылками. Имейте в виду, что этот прием дает положительный резуль-
тат, только когда ключ хранит непустую ссылку. Метод delete() принудительно
удаляет ключ из ассоциативного массива, после чего вызов has() возвращает false,
а get() возвращает undefined.
function Person(name) {
this._name = name;
}
Person.prototype.getName = function() {
return this._name;
};
function Person(name) {
Object.defineProperty(this, "_id", { value: privateId++ });
privateData[this._id] = {
name: name
};
}
Person.prototype.getName = function() {
return privateData[this._id].name;
};
return Person;
}());
function Person(name) {
privateData.set(this, { name: name });
}
Person.prototype.getName = function() {
return privateData.get(this).name;
};
return Person;
}());
В этой версии Person для хранения приватных данных вместо объекта используется
ассоциативный массив со слабыми ссылками. Так как в качестве ключей можно
использовать сами экземпляры объекта Person, отпала необходимость в отдельном
числовом идентификаторе. Когда вызывается конструктор Person, в ассоциативный
массив со слабыми ссылками добавляется новый элемент с ключом this и объектом
с приватной информацией в качестве значения. В данном случае значение — это
объект с единственным свойством name. Функция getName() извлекает приватную
информацию, передавая this в вызов метода privateData.get(), который извлекает
объект-значение и возвращает его свойство name. Этот прием помогает сохранить
приватную информацию недоступной и уничтожает эту информацию, как только
будет уничтожен соответствующий ей экземпляр объекта.
В заключение
ECMAScript 6 официально ввела множества и ассоциативные массивы в JavaScript.
Прежде для имитации множеств и ассоциативных массивов разработчики ис-
пользовали обычные объекты, часто сталкиваясь с проблемами из-за ограничений,
характерных для свойств объектов.
Множества — это упорядоченные списки уникальных значений. Значения счита-
ются уникальными, если они не эквивалентны с точки зрения метода Object.is().
Множества автоматически удаляют повторяющиеся значения, поэтому множество
можно использовать как фильтр для устранения повторяющихся элементов из мас-
сивов. Множества не являются подклассом массивов, поэтому они не поддерживают
произвольный доступ к своим элементам. Вместо этого вам придется использовать
метод has(), чтобы определить, присутствует ли значение в множестве, и свойство
size, чтобы узнать количество значений в множестве. Тип Set имеет также метод
forEach() для обработки всех значений в множестве.
Множества со слабыми ссылками — это особая разновидность множеств, которая
позволяет хранить только объекты. Объекты сохраняются в виде слабых ссылок
на них, то есть элемент со слабой ссылкой не препятствует утилизации объекта
сборщиком мусора, если этот элемент оказался последней ссылкой на этот объект.
Содержимое множества со слабыми ссылками нельзя исследовать из-за сложностей,
связанных с управлением памятью, поэтому такие множества лучше использовать
только для слежения за объектами.
Ассоциативные массивы — это упорядоченные списки пар ключ/значение, где
ключом могут быть данные любого типа. По аналогии с множествами уникальность
ключей определяется с помощью метода Object.is(), то есть числовой ключ 5
164 Глава 7 • Множества и ассоциативные массивы
Это стандартный способ использования цикла for для перебора индексов в мас-
сиве colors с применением переменной i. Значение i увеличивается на единицу
166 Глава 8 • Итераторы и генераторы
function createIterator(items) {
var i = 0;
return {
next: function() {
return {
done: done,
value: value
};
Что такое генераторы? 167
}
};
}
// генератор
function *createIterator() {
yield 1;
yield 2;
yield 3;
}
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3
168 Глава 8 • Итераторы и генераторы
function *createIterator(items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
}
items.forEach(function(item) {
// синтаксическая ошибка
yield item + 1;
});
}
Выражения функций-генераторов
Для создания генераторов можно использовать функции-выражения. В этом случае
звездочку (*) следует поместить между ключевым словом function и открывающей
круглой скобкой. Например:
ПРИМЕЧАНИЕ
Создание стрелочных функций-генераторов невозможно.
Методы-генераторы объектов
Так как генераторы — это всего лишь функции, их можно добавлять в объекты. На-
пример, ниже показано, как с помощью функции-выражения определить генератор
в литерале объекта в стиле ECMAScript 5:
let o = {
let o = {
*createIterator(items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
}
};
ПРИМЕЧАНИЕ
Все итераторы, созданные с помощью генераторов, сами являются итерируемыми объ
ектами, потому что по умолчанию генераторы присваиваются свойству Symbol.iterator.
1
2
3
Данный цикл for-of сначала вызовет метод Symbol.iterator массива values, чтобы
получить итератор. (Вызов Symbol.iterator выполняется движком JavaScript неяв-
но.) Затем вызовет iterator.next() и прочитает свойство value объекта результата
в переменную num. Переменная num сначала получит значение 1, затем 2 и, наконец, 3.
Когда свойство done объекта результата получит значение true, цикл завершится,
поэтому переменная num никогда не получит значения undefined.
Если требуется просто обойти значения в массиве или коллекции, цикл for-of
выглядит предпочтительнее, чем for. Вообще цикл for-of способствует уменьше-
нию количества ошибок, потому что проверяется меньшее количество условий.
Применяйте традиционный цикл for, только когда требуется реализовать более
сложную логику управления.
ВНИМАНИЕ
Инструкция for-of возбуждает ошибку при попытке применить ее к неитерируемому
объекту, значению null или undefined.
172 Глава 8 • Итераторы и генераторы
function isIterable(object) {
return typeof object[Symbol.iterator] === "function";
}
let collection = {
items: [],
*[Symbol.iterator]() {
Встроенные итераторы 173
collection.items.push(1);
collection.items.push(2);
collection.items.push(3);
1
2
3
ПРИМЕЧАНИЕ
В разделе «Делегирование генераторов» (см. ниже) описываются разные подходы
к применению итераторов других объектов.
Встроенные итераторы
Итераторы являются важным элементом ECMAScript 6, и поэтому нет необходи-
мости создавать собственные итераторы для многих встроенных типов, посколь-
ку они уже включены в язык. Вам придется определять собственные итераторы,
только когда стандартные не будут соответствовать вашим целям — обычно при
определении собственных объектов или классов. В остальных случаях вы сможете
положиться на встроенные итераторы. Чаще других, пожалуй, вам придется ис-
пользовать итераторы коллекций.
174 Глава 8 • Итераторы и генераторы
Итераторы коллекций
В ECMAScript 6 имеется три типа объектов коллекций: массивы, ассоциативные
массивы и множества. Все три имеют следующие встроенные итераторы, помога-
ющие в навигации по их содержимому:
Итератор entries()
Итератор entries() возвращает двухэлементный массив в ответ на каждый вызов
next(). Двухэлементный массив содержит ключ и значение для каждого элемента
в коллекции. В первом элементе для массивов возвращается числовой индекс, для
множеств — копия значения (потому что в множествах значения дублируют ключи),
для ассоциативных массивов — ключ.
Ниже приводится несколько примеров использования итератора entries():
[0, "red"]
[1, "green"]
Встроенные итераторы 175
[2, "blue"]
[1234, 1234]
[5678, 5678]
[9012, 9012]
["title", "Understanding ECMAScript 6"]
["format", "ebook"]
В этом фрагменте кода вызывается метод entries(), чтобы получить итератор для
коллекции каждого вида, и используются циклы for-of для итераций по элементам.
Вывод в консоль позволяет увидеть, как для каждого объекта возвращаются пары
ключей и значений.
Итератор values()
Итератор values() просто возвращает значения в том виде, в каком они хранятся
в коллекции. Например:
"red"
"green"
"blue"
1234
5678
9012
"Understanding ECMAScript 6"
"ebook"
Итератор keys()
Итератор keys() возвращает ключи, хранящиеся в коллекции. Для простых масси-
вов он возвращает только числовые ключи и никогда не возвращает собственные
свойства объекта массива. Для множеств ключи являются копиями значений, по-
этому keys() и values() возвращают один и тот же итератор. Для ассоциативных
массивов итератор keys() возвращает уникальные ключи. Следующий пример
демонстрирует действие итератора для всех трех типов:
0
1
2
1234
5678
9012
"title"
"format"
"red"
"green"
"blue"
1234
5678
9012
["title", "Understanding ECMAScript 6"]
["format", "print"]
Итераторы строк
Строки в JavaScript еще больше стали похожи на массивы после выхода
ECMAScript 5. Например, в ECMAScript 5 была формализована форма записи
с квадратными скобками для обращения к символам в строках (то есть синтаксис
text[0] для получения первого символа и т. д.). Но квадратные скобки работают
с кодовыми единицами, а не с символами, поэтому с их помощью нельзя получить
двухбайтный символ, как показывает следующий пример:
A
(пустая строка)
𠮷 (пустая строка)
B
Итераторы NodeList
В объектной модели документа (DOM) имеется тип NodeList, представляющий
коллекцию элементов в документе. Для тех, кто пишет сценарии на JavaScript для
выполнения в веб-браузерах, всегда было немного трудно осознать различия между
объектами NodeList и массивами. Обе структуры, объекты NodeList и массивы,
имеют свойство length, хранящее количество элементов, и при работе с обеими
используются квадратные скобки для доступа к отдельным элементам. Однако
внутренне NodeList и массив действуют совершенно по-разному, из-за чего раз-
работчики часто допускают ошибки.
С появлением в ECMAScript 6 итераторов по умолчанию определение NodeList
в DOM (дается в спецификации HTML, а не ECMAScript 6) включает итератор
по умолчанию, который действует, подобно итератору по умолчанию массива. Это
означает, что NodeList можно использовать в цикле for-of или в любом другом
контексте, где применяется итератор по умолчанию. Например:
console.log(array); // [1,2,3,4,5]
console.log(allNumbers.length); // 7
console.log(allNumbers); // [0, 1, 2, 3, 100, 101, 102]
Дополнительные возможности итераторов 181
function *createIterator() {
let first = yield 1;
let second = yield first + 2; // 4 + 2
yield second + 3; // 5 + 3
}
Первый вызов next() — особый случай, когда любой переданный аргумент будет
потерян. Поскольку аргументы, переданные в next(), превращаются в значения,
возвращаемые инструкцией yield, аргумент в первом вызове next() мог бы просто
заменить первую инструкцию yield в функции генератора, если бы он был доступен
до этой инструкции yield. Однако это невозможно, поэтому бессмысленно пере-
давать аргумент в первый вызов next().
Во второй вызов next() передается аргумент со значением 4. В результате внутри
функции генератора число 4 присваивается переменной first. В инструкции yield,
включающей присваивание, правая часть выражения вычисляется в первом вызове
next(), а левая часть — во втором вызове next(), прежде чем функция продолжит
выполнение. Поскольку во второй вызов next() передается число 4, оно присваи-
вается переменной first, и выполнение продолжается.
Вторая инструкция yield использует результат, возвращенный первой инструкцией
yield, и прибавляет к нему 2, в результате чего в вызывающую программу возвра-
щается число 6. Когда next() вызывается в третий раз, ему передается аргумент
с числом 5. Это значение присваивается переменной second и затем используется
в третьей инструкции yield, возвращающей 8.
Происходящее проще представить, если выделить код, который выполняется в каж-
дом вызове функции генератора. На рис. 8.1 оттенками серого выделены фрагменты
кода, выполняемого перед возвратом значений.
function*createIterator(){
next() let first = yield 1;
next(4) let second = yield first + 2;
next(5) yield second + 3;
}
function *createIterator() {
let first = yield 1;
let second = yield first + 2; // вернет 4 + 2 и возбудит ошибку
yield second + 3; // никогда не будет выполнена
}
В этом примере первые две инструкции yield выполняются как обычно, но после вы-
зова throw()перед вычислением let second будет возбуждена ошибка. В результате
работа кода прерывается, как если бы ошибка была возбуждена непосредственно.
Единственное отличие — точка появления ошибки. На рис. 8.2 показано, какой код
выполняется на каждом шаге.
function*createIterator(){
next() let first = yield 1;
next(4) let second = yield first + 2;
throw(newError()); yield second + 3;
}
Рис. 8.2. Возбуждение ошибки внутри генератора
Как и на рис. 8.1, светло-серым и серым фоном выделены вызовы next() и yield,
выполняемые без ошибок. Вызов throw() выделен темно-серым фоном, а черной
звездой отмечена примерная точка возбуждения ошибки внутри генератора. Первые
две инструкции yield выполнились, а после вызова throw() перед выполнением
какого-либо другого кода была возбуждена ошибка. Зная это, можно перехватывать
такие ошибки внутри генератора с помощью блока try-catch:
184 Глава 8 • Итераторы и генераторы
function *createIterator() {
let first = yield 1;
let second;
try {
second = yield first + 2; // вернет 4 + 2 и возбудит ошибку
} catch (ex) {
second = 6; // в случае ошибки присваивает другое значение
}
yield second + 3;
}
В этом примере вторая инструкция yeld заключена в блок try-catch. Эта инструк-
ция выполняется без ошибок, но когда приходит черед присвоить значение пере-
менной second, возбуждается ошибка. Блок catch перехватывает ее и присваивает
переменной число 6. Далее поток выполнения достигает следующей инструкции
yield, которая возвращает 9.
Обратите внимание на одну интересную особенность: метод throw() вернул объ-
ект результата, в точности как метод next(). Поскольку ошибка была перехвачена
внутри генератора, код продолжил выполнение до следующей инструкции yield
и вернул следующее значение — 9.
Это помогает интерпретировать методы next() и throw() как команды итератора.
Метод next() командует итератору продолжить выполнение (возможно, с указан-
ным значением), а метод throw() командует продолжить с возбуждением ошибки.
Что произойдет после этого, зависит от реализации генератора.
Методы next() и throw() управляют выполнением внутри итератора, когда исполь-
зуется инструкция yield, но можно также воспользоваться инструкцией return.
Однако return действует здесь немного иначе, чем в обычных функциях, как будет
видно из следующего раздела.
function *createIterator() {
yield 1;
return;
yield 2;
yield 3;
}
function *createIterator() {
yield 1;
return 42;
}
Здесь второй вызов next() (который оказывается первым, вернувшим true в поле
done) возвращает число 42 в поле value. Третий вызов next() возвращает объект,
свойство value которого вновь получает значение undefined. Значение, указанное
в инструкции return, доступно в возвращаемом объекте только один раз, после
чего все остальные вызовы next() будут возвращать объект со значением undefined
в поле value.
ПРИМЕЧАНИЕ
Оператор расширения (...) и цикл for-of игнорируют значение, указанное в ин
струкции return, так как, обнаружив значение true в свойстве done, они прекраща
ют читать свойство value. Однако значения, возвращаемые итератором с помощью
return, могут пригодиться при делегировании генераторов.
186 Глава 8 • Итераторы и генераторы
Делегирование генераторов
Иногда может понадобиться объединить значения, возвращаемые двумя итератора-
ми. Генераторы можно делегировать другим генераторам, используя специальную
форму инструкции yield со звездочкой (*). Так же как в объявлениях генераторов,
точное местоположение звездочки не имеет значения. Главное, чтобы она находилась
между ключевым словом yield и именем функции генератора. Например:
function *createNumberIterator() {
yield 1;
yield 2;
}
function *createColorIterator() {
yield "red";
yield "green";
}
function *createCombinedIterator() {
yield *createNumberIterator();
yield *createColorIterator();
yield true;
}
function *createNumberIterator() {
yield 1;
yield 2;
return 3;
}
Дополнительные возможности итераторов 187
function *createRepeatingIterator(count) {
for (let i=0; i < count; i++) {
yield "repeat";
}
}
function *createCombinedIterator() {
let result = yield *createNumberIterator();
yield *createRepeatingIterator(result);
}
function *createNumberIterator() {
yield 1;
yield 2;
return 3;
}
function *createRepeatingIterator(count) {
for (let i=0; i < count; i++) {
yield "repeat";
}
}
function *createCombinedIterator() {
let result = yield *createNumberIterator();
yield result;
yield *createRepeatingIterator(result);
}
ПРИМЕЧАНИЕ
Конструкцию yield * можно применить непосредственно к строке (например: yield
* "hello"). В результате будет использоваться итератор по умолчанию для строк.
let fs = require("fs");
doSomethingWith(contents);
console.log("Done");
});
function run(taskDef) {
// создать итератор
let task = taskDef();
// запустить задание
let result = task.next();
// запустить обработку
step();
}
run(function*() {
console.log(1);
yield;
console.log(2);
yield;
console.log(3);
});
190 Глава 8 • Итераторы и генераторы
Этот пример просто выводит три числа в консоль с целью наглядно показать, что
были выполнены все вызовы next(). Однако простой вывод нескольких фиксиро-
ванных значений асинхронным способом не особенно полезен. Поэтому далее мы
посмотрим, как передавать значения в итератор и получать их из итератора.
function run(taskDef) {
// создать итератор
let task = taskDef();
// запустить задание
let result = task.next();
// запустить обработку
step();
}
run(function*() {
let value = yield 1;
console.log(value); // 1
value = yield value + 3;
console.log(value); // 4
});
Этот пример выведет в консоль два числа: 1 и 4. Значение 1 получено в результате
выполнения yield 1, потому что число 1 было тут же передано обратно в переменную
value. Число 4 было вычислено сложением переменной value с числом 3 и передано
обратно в переменную value. Теперь, когда данные свободно передаются между
вызовами yield, нам осталось сделать всего один маленький шаг, чтобы обеспечить
возможность асинхронных вызовов.
Асинхронное выполнение заданий 191
function fetchData() {
return function(callback) {
callback(null, "Hi!");
};
}
Для целей этого примера нам потребуется функция, вызываемая инструментом вы-
полнения заданий, которая будет возвращать функцию, выполняющую обратный
вызов. Функция fetchData() возвращает функцию, которая принимает функцию
обратного вызова в аргументе. Когда будет вызвана возвращаемая функция, она
выполнит функцию обратного вызова с единственным фрагментом данных (строкой
"Hi!"). Аргумент callback должен передаваться из инструмента выполнения зада-
ний, чтобы гарантировать корректное взаимодействие функции обратного вызова
с итератором. Несмотря на то что fetchData() является синхронной функцией,
ее легко можно превратить в асинхронную, запустив функцию обратного вызова
с небольшой задержкой, как показано ниже:
function fetchData() {
return function(callback) {
setTimeout(function() {
callback(null, "Hi!");
}, 50);
};
}
Эта версия fetchData() вводит задержку в 50 мс перед запуском функции обратного
вызова и демонстрирует, что данный шаблон хорошо подходит для выполнения не
только синхронных, но и асинхронных операций. От вас требуется только гаранти-
ровать, что каждая функция, которая должна вызываться с помощью yield, следует
одному и тому же шаблону.
Хорошо понимая, как функция может сообщить, что она выполняет обработку
асинхронно, можно изменить инструмент выполнения заданий, чтобы учесть это
обстоятельство. Всякий раз когда result.value представляет функцию, инструмент
192 Глава 8 • Итераторы и генераторы
function run(taskDef) {
// создать итератор
let task = taskDef();
// запустить задание
let result = task.next();
result = task.next(data);
step();
});
} else {
result = task.next(result.value);
step();
}
}
}
// запустить обработку
step();
let fs = require("fs");
function readFile(filename) {
return function(callback) {
fs.readFile(filename, callback);
};
}
run(function*() {
let contents = yield readFile("config.json");
doSomethingWith(contents);
console.log("Done");
});
В заключение
Итераторы — важный элемент ECMAScript 6 и фундамент некоторых других клю-
чевых особенностей языка. С внешней стороны итераторы позволяют организовать
получение последовательности значений с использованием простого API. Однако
в ECMAScript 6 поддерживаются более сложные способы применения итераторов.
Для определения итераторов объектов применяется символ Symbol.iterator. Любые
объекты, встроенные и созданные разработчиком, могут использовать этот символ
для реализации метода, возвращающего итератор. Если объект поддерживает свой-
ство Symbol.iterator, он считается итерируемым.
194 Глава 8 • Итераторы и генераторы
function PersonType(name) {
this.name = name;
}
PersonType.prototype.sayName = function() {
196 Глава 9 • Введение в классы JavaScript
console.log(this.name);
};
Объявление класса
Простейшей формой класса в ECMAScript 6 является объявление класса, которое
выглядит похожим на объявления классов в других языках.
class PersonClass {
ПРИМЕЧАНИЕ
Прототипы классов, такие как PersonClass.prototype в предыдущем примере, до
ступны только для чтения. Это означает, что свойству prototype нельзя присвоить
новое значение, как это допускается в случае с функциями.
"use strict";
this.name = name;
}
Object.defineProperty(PersonType2.prototype, "sayName", {
value: function() {
console.log(this.name);
},
enumerable: false,
writable: true,
configurable: true
});
return PersonType2;
}());
Классы-выражения 199
Классы-выражения
Классы, как и функции, имеют две формы: объявления и выражения. Объявления
функций и классов начинаются с ключевого слова (function и class соответствен-
но), за которым следует идентификатор. Функции имеют форму выражения, не
требующую указывать идентификатор после function , а классы имеют форму
выражения, не требующую указывать идентификатор после class. Такие классы-
выражения предназначены для использования в объявлениях переменных или для
передачи в функции через аргументы.
200 Глава 9 • Введение в классы JavaScript
Простой класс-выражение
Ниже приводится класс-выражение, эквивалентный предыдущим примерам
PersonClass, за которым следует некоторый код, использующий его:
// эквивалент PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
};
Именованные классы-выражения
В предыдущем примере был показан анонимный класс-выражение, но классам-вы-
ражениям, так же как функциям-выражениям, можно присваивать имена. Для этого
нужно лишь включить идентификатор после ключевого слова class, например:
Классы-выражения 201
// эквивалент PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
};
"use strict";
this.name = name;
}
Object.defineProperty(PersonClass2.prototype, "sayName", {
value: function() {
console.log(this.name);
},
enumerable: false,
writable: true,
202 Глава 9 • Введение в классы JavaScript
configurable: true
});
return PersonClass2;
}());
function createObject(classDef) {
return new classDef();
}
sayHi() {
console.log("Hi!");
}
});
obj.sayHi(); // "Hi!"
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}("Nicholas");
person.sayName(); // "Nicholas"
class CustomHTMLElement {
constructor(element) {
this.element = element;
}
get html() {
return this.element.innerHTML;
}
set html(value) {
this.element.innerHTML = value;
204 Глава 9 • Введение в классы JavaScript
}
}
"use strict";
this.element = element;
}
Object.defineProperty(CustomHTMLElement.prototype, "html", {
enumerable: false,
configurable: true,
get: function() {
return this.element.innerHTML;
},
set: function(value) {
this.element.innerHTML = value;
}
});
return CustomHTMLElement;
}());
Как и предыдущие, этот пример демонстрирует, как много кода позволяет опустить
синтаксис классов. Определение свойства html с методами доступа — единственный
фрагмент, размер которого практически не изменился по сравнению с эквивалент-
ным объявлением класса.
Вычисляемые имена членов 205
class PersonClass {
constructor(name) {
this.name = name;
}
[methodName]() {
console.log(this.name);
}
};
class CustomHTMLElement {
constructor(element) {
this.element = element;
}
get [propertyName]() {
return this.element.innerHTML;
}
set [propertyName](value) {
this.element.innerHTML = value;
}
}
Здесь методы чтения и записи для свойства html определяются с помощью переменной
propertyName. Обращение к свойству по имени .html лишь отражает определение.
206 Глава 9 • Введение в классы JavaScript
Методы-генераторы
Как рассказывалось в главе 8, чтобы определить генератор в литерале объекта, нужно
добавить звездочку (*) перед именем метода. Тот же синтаксис можно использовать
в объявлениях классов, чтобы создать метод-генератор. Например:
class MyClass {
*createIterator() {
yield 1;
yield 2;
yield 3;
}
}
class Collection {
constructor() {
this.items = [];
}
*[Symbol.iterator]() {
yield *this.items.values();
}
}
collection.items.push(3);
// Вывод:
// 1
// 2
// 3
Статические члены
Еще один распространенный шаблон в ECMAScript 5 и предыдущих версиях —
добавление методов непосредственно в конструкторы для имитации статических
членов. Например:
function PersonType(name) {
this.name = name;
}
// статический метод
PersonType.create = function(name) {
return new PersonType(name);
};
// метод экземпляра
PersonType.prototype.sayName = function() {
console.log(this.name);
};
class PersonClass {
// эквивалент PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
// эквивалент PersonType.create
static create(name) {
return new PersonClass(name);
}
}
ПРИМЕЧАНИЕ
Статические члены недоступны как методы и свойства экземпляров. Для обращения
к ним всегда необходимо использовать сам класс.
Rectangle.prototype.getArea = function() {
return this.length * this.width;
};
Наследование в производных классах 209
function Square(length) {
Rectangle.call(this, length, length);
}
Square.prototype = Object.create(Rectangle.prototype, {
constructor: {
value: Square,
enumerable: true,
writable: true,
configurable: true
}
});
console.log(square.getArea()); // 9
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
getArea() {
return this.length * this.width;
}
}
console.log(square.getArea()); // 9
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
210 Глава 9 • Введение в классы JavaScript
// эквивалентный класс
class Square extends Rectangle {
constructor(...args) {
super(...args);
}
}
Так как метод getArea() определяется как часть класса Square, метод Rectangle.
prototype.getArea() становится недоступен экземплярам Square. Конечно, вы
всегда можете вызвать версию метода из базового класса, используя метод super.
getArea(), например:
Ссылка super в данном случае действует в точности как обсуждалось в главе 4 (раз-
дел «Простой доступ к прототипу с помощью ссылки super»). Она автоматически
корректирует ссылку this и позволяет просто вызвать метод.
class Rectangle {
constructor(length, width) {
this.length = length;
this.width = width;
}
212 Глава 9 • Введение в классы JavaScript
getArea() {
return this.length * this.width;
}
Rectangle.prototype.getArea = function() {
return this.length * this.width;
};
console.log(x.getArea()); // 9
console.log(x instanceof Rectangle); // true
Rectangle.prototype.getArea = function() {
return this.length * this.width;
};
function getBase() {
return Rectangle;
}
let SerializableMixin = {
serialize() {
return JSON.stringify(this);
}
};
let AreaMixin = {
getArea() {
return this.length * this.width;
}
};
214 Глава 9 • Введение в классы JavaScript
function mixin(...mixins) {
var base = function() {};
Object.assign(base.prototype, ...mixins);
return base;
}
ПРИМЕЧАНИЕ
После ключевого слова extends можно использовать любое выражение, но не все
выражения дают в результате допустимый класс. В частности, если указать значение
null или функцию-генератор (рассматриваются в главе 8) после extends, это станет
причиной ошибок. В этих случаях попытка создать новый экземпляр вызовет ошибку
из-за невозможности вызвать [[Construct]].
colors.length = 0;
console.log(colors[0]); // undefined
MyArray.prototype = Object.create(Array.prototype, {
constructor: {
value: MyArray,
writable: true,
configurable: true,
enumerable: true
}
});
colors.length = 0;
console.log(colors[0]); // "red"
colors.length = 0;
console.log(colors[0]); // undefined
MyArray наследует встроенный тип Array и поэтому действует как Array. Операции
со свойствами, имеющими числовые имена, оказывают влияние на свойство length,
а манипуляции со свойством length отражаются на свойствах с числовыми име-
нами. Это означает, что теперь вы можете унаследовать возможности Array, чтобы
создать собственный производный класс массивов, а также унаследовать возмож-
ности и любых других встроенных объектов. Что касается этой дополнительной
функциональности, то из ECMAScript 6 и производных классов фактически был
убран последний особый случай наследования встроенных объектов, но в нем
осталось еще кое-что, достойное исследования.
Свойство Symbol.species
Наследование встроенных объектов обладает одной интересной особенно-
стью: любой метод, возвращающий экземпляр встроенного объекта, в произ-
водном классе автоматически будет возвращать экземпляр этого производного
класса. Это означает, что если имеется производный класс MyArray, наследующий
Array, его методы, такие как slice(), будут возвращать экземпляры MyArray. На-
пример:
constructor(value) {
this.value = value;
}
clone() {
return new this.constructor[Symbol.species](this.value);
}
}
constructor(value) {
this.value = value;
}
clone() {
return new this.constructor[Symbol.species](this.value);
}
}
Использование new.target
в конструкторах классов
В главе 3 вы познакомились с метасвойством new.target и узнали, как изменяется
его значение в зависимости от способа вызова функции. Метасвойство new.target
можно также использовать в конструкторах классов, чтобы определить, как вы-
зывался класс. В простейшем случае new.target содержит ссылку на функцию-
конструктор класса, как в следующем примере:
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
this.length = length;
this.width = width;
}
}
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
this.length = length;
this.width = width;
}
220 Глава 9 • Введение в классы JavaScript
В этом примере конструктор класса Shape возбуждает ошибку, если new.target ссы-
лается на Shape, то есть вызов new Shape() всегда будет вызывать ошибку. Однако
Shape можно использовать в роли базового класса, что и делает класс Rectangle.
Вызов super() выполняет конструктор Shape, а так как в этом случае new.target
ссылается на Rectangle, этот конструктор продолжает работу, не возбуждая ошибки.
ПРИМЕЧАНИЕ
Классы не могут вызываться без ключевого слова new, поэтому внутри конструкторов
классов метасвойство new.target никогда не будет иметь значения undefined.
В заключение 221
В заключение
Классы, появившиеся в ECMAScript 6, упрощают реализацию наследования
в JavaScript, и вам пригодится опыт применения механизмов наследования в других
языках, если он у вас имеется. Первоначально классы в ECMAScript 6 играли роль
синтаксического сахара для классической модели наследования в ECMAScript 5,
но постепенно в них были добавлены дополнительные особенности, уменьшающие
вероятность ошибок.
Классы в ECMAScript 6 используют механизм наследования прототипов для
определения нестатических методов в прототипах классов, тогда как статические
методы добавляются непосредственно в конструктор. Все методы создаются как
неперечислимые, что более точно соответствует поведению встроенных объектов,
чьи методы обычно являются неперечислимыми по умолчанию. Кроме того, кон-
структоры классов не могут вызываться без ключевого слова new, что гарантирует
невозможность вызова класса как обычной функции по ошибке.
Наследование на основе классов позволяет создавать классы, производные от других
классов, функций или выражений. Таким образом, вызовом функции можно опреде-
лить базу для наследования, что дает возможность создавать примеси и применять
другие шаблоны комбинирования с целью создания нового класса. Механизм на-
следования теперь действует так, что позволяет наследовать встроенные объекты,
такие как Array, и получать в результате ожидаемое поведение.
Вы можете использовать метасвойство new.target в конструкторах классов, чтобы
определять разные варианты поведения в зависимости от того, как класс вызывался.
Чаще всего эта возможность используется для определения абстрактных базовых
классов, которые возбуждают ошибку при попытке создать их экземпляры непо-
средственно, но допускают наследование другими классами.
В целом классы являются важным дополнением языка JavaScript. Они обеспечи-
вают более компактный синтаксис и более широкие возможности для определения
пользовательских типов объектов безопасным и непротиворечивым способом.
10 Расширенные
возможности
массивов
Создание массивов
До ECMAScript 6 существовало два основных способа создания массивов: с по-
мощью конструктора Array и с использованием синтаксиса литералов массивов.
Оба подхода требовали перечислить элементы будущего массива по отдельности
и имели существенные ограничения. Возможности преобразования объектов, по-
добных массивам (то есть объектов с числовыми индексами и свойством length),
в массивы также имели ограничения и часто требовали дополнительного кода.
Чтобы упростить создание массивов JavaScript, в ECMAScript 6 были добавлены
методы Array.of() и Array.from().
Метод Array.of()
Одной из причин добавления новых методов в JavaScript было стремление избавить
разработчиков от ошибок при создании массивов с помощью конструктора Array,
поскольку этот конструктор проявляет разное поведение в зависимости от типов
и количества аргументов. Например:
Создание массивов 223
items = Array.of(2);
console.log(items.length); // 1
console.log(items[0]); // 2
items = Array.of("2");
console.log(items.length); // 1
console.log(items[0]); // "2"
Чтобы создать массив с помощью метода Array.of(), достаточно передать ему зна-
чения, которые требуется включить в массив. Первый из примеров выше создает
массив с двумя числами, второй — массив с одним числом и последний — массив
с одной строкой. Этот подход напоминает использование литерала массива, и в боль-
шинстве случаев для создания простых массивов вместо метода Array.of() можно
224 Глава 10 • Расширенные возможности массивов
ПРИМЕЧАНИЕ
Метод Array.of() не использует свойство Symbol.species (см. раздел «Свойство
Symbol.species» в главе 9) для определения типа возвращаемого значения. Вместо
этого используется текущий конструктор (this внутри метода of()).
Метод Array.from()
Преобразование объектов, не являющихся массивами, в настоящие массивы всегда
было сложной задачей в JavaScript. Например, если вы пожелаете использовать
объект arguments (объект, подобный массиву) как массив, вам придется сначала
преобразовать его в массив. Для преобразования в массивы объектов, подобных
массивам, в ECMAScript 5 требовалось написать функцию, например, как показано
ниже:
function makeArray(arrayLike) {
var result = [];
return result;
}
function doSomething() {
var args = makeArray(arguments);
// использовать args
}
function makeArray(arrayLike) {
return Array.prototype.slice.call(arrayLike);
}
function doSomething() {
var args = makeArray(arguments);
// использовать args
}
function doSomething() {
var args = Array.from(arguments);
// использовать args
}
ПРИМЕЧАНИЕ
Для определения типа возвращаемого массива метод Array.from() также использует
this.
Преобразование с отображением
Если необходимо выполнить более сложное преобразование, методу Array.from()
во втором аргументе можно передать функцию отображения. Эта функция должна
принимать каждое значение из объекта, подобного массиву, и преобразовывать его
226 Глава 10 • Расширенные возможности массивов
function translate() {
return Array.from(arguments, (value) => value + 1);
}
let helper = {
diff: 1,
add(value) {
return value + this.diff;
}
};
function translate() {
return Array.from(arguments, helper.add, helper);
}
console.log(numbers); // 2,3,4
В этом примере роль функции отображения играет метод helper.add(). Так как
helper.add() использует свойство this.diff, в вызов Array.from() требуется пере-
дать третий аргумент, определяющий значение this. Благодаря третьему аргументу
Array.from() легко может преобразовывать данные, не вызывая bind() и не опре-
деляя значение this каким-либо другим способом.
let numbers = {
*[Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
}
Новые методы всех массивов 227
};
console.log(numbers2); // 2,3,4
Так как объект numbers является итерируемым, его можно передать в вызов ме
тода Array.from() и преобразовать в массив. Функция отображения прибавля-
ет 1 к каждому числу, поэтому получившийся массив содержит 2, 3 и 4 вместо
1, 2 и 3.
ПРИМЕЧАНИЕ
Если объект является одновременно итерируемым и подобным массиву, для получения
значений Array.from() использует итератор.
Единственное отличие между этими двумя методами состоит в том, что find()
возвращает значение, а findIndex() — индекс найденного значения. Это отличие
демонстрирует следующий пример:
Этот код вызывает find() и findIndex(), чтобы найти в массиве numbers первое
значение, большее 33. Вызов find() возвращает 35, а вызов findIndex() возвращает
2 — индекс числа 35 в массиве numbers.
Оба метода, find() и findIndex(), удобно использовать для поиска в массивах эле-
ментов, соответствующих условиям, а не определенному значению. Если требуется
просто найти значение, лучшим выбором будут методы indexOf() и lastIndexOf().
Метод fill()
Метод fill() заполняет один или несколько элементов массива определенным
значением. Когда передается одно значение, fill() затирает им все значения
в массиве. Например:
numbers.fill(1);
console.log(numbers.toString()); // 1,1,1,1
numbers.fill(1, 2);
console.log(numbers.toString()); // 1,2,1,1
numbers.fill(0, 1, 3);
console.log(numbers.toString()); // 1,0,0,1
ПРИМЕЧАНИЕ
Если в начальном или конечном индексе передать отрицательное число, это число
будет прибавляться к значению длины массива, чтобы получить искомое местопо
ложение. Например, значение -1 в аргументе, определяющем начальный индекс,
соответствует индексу array.length -1, где array — это массив, для которого был
вызван метод fill().
Метод copyWithin()
Метод copyWithin() напоминает fill() тем, что также изменяет сразу несколько
элементов массива. Однако вместо единственного значения для записи в элементы
массива copyWithin() позволяет копировать элементы в массиве. Для этого методу
copyWithin() нужно передать два аргумента: индекс первого элемента в массиве,
куда должно выполняться копирование, и индекс первого элемента в массиве, от-
куда должны извлекаться значения.
Например, скопировать значения первых двух элементов в массиве в последние
два элемента в этом же массиве можно так:
console.log(numbers.toString()); // 1,2,1,2
Этот код вставит значения в массив numbers, начиная с индекса 2, то есть будут за-
терты элементы с индексами 2 и 3. Значение 0 во втором аргументе сообщает методу
copyWithin(), что значения для копирования должны извлекаться из элементов,
начиная с индекса 0, и копирование должно продолжаться, пока не будет записан
последний элемент массива.
По умолчанию copyWithin() всегда копирует значения до конца массива, но вы
можете передать необязательный третий аргумент и ограничить количество из-
меняемых элементов. Этот третий аргумент представляет индекс элемента, по до-
стижении которого копирование должно быть прекращено, при этом элемент с этим
индексом не будет скопирован. Ниже показано, как такая операция выглядит в коде:
console.log(numbers.toString()); // 1,2,1,4
Этот пример скопирует только элемент с индексом 0, потому что в третьем необя-
зательном аргументе передан индекс 1 элемента, который уже не должен копиро-
ваться. В результате последний элемент массива не изменился.
ПРИМЕЧАНИЕ
Так же как при использовании метода fill(), если в любом аргументе передать ме
тоду copyWithin() отрицательное число, это число будет прибавляться к значению
длины массива, чтобы получить искомый индекс.
Типизированные массивы
Типизированные массивы — это особые массивы, предназначенные для работы
с числовыми типами (не со всеми типами, как можно было бы предположить).
Своими корнями типизированные массивы уходят в WebGL — реализацию OpenGL
ES 2.0 для использования в веб-страницах с элементом <canvas>. Типизированные
массивы были созданы как часть этой реализации для поддержки быстрой пораз-
рядной арифметики в JavaScript.
Арифметические операции со встроенными числами JavaScript выполняются слиш-
ком медленно для WebGL, потому что числа хранятся в 64-разрядном вещественном
формате и при необходимости преобразуются в 32-разрядные целые числа. Типизи-
рованные массивы создавались с целью обойти это ограничение и обеспечить более
высокую производительность арифметических операций. Идея состоит в том, что
любое отдельное число можно интерпретировать как массив битов и таким образом
использовать знакомые методы, доступные в массивах JavaScript.
ECMAScript 6 официально добавила поддержку типизированных массивов в язык,
чтобы обеспечить лучшую совместимость движков JavaScript и функциональное
сходство с массивами JavaScript. Несмотря на то что типизированные массивы
в ECMAScript 6 не полностью соответствуют типизированным массивам в WebGL,
они имеют достаточно много общего, чтобы считать версию ECMAScript 6 даль-
нейшим развитием версии WebGL, а не каким-то другим решением.
Типизированные массивы 231
ПРИМЕЧАНИЕ
Далее в книге для ссылки на эти восемь типов я буду использовать сокращения,
указанные в круглых скобках. Эти сокращения не являются частью языка JavaScript;
это просто более короткий способ отразить то, что потребовало бы более длинного
описания.
Буферы массивов
Основой всех типизированных массивов является буфер массива — фрагмент па-
мяти, способный хранить заданное количество байтов. Создание буфера массива
сродни вызову функции malloc() в языке C, которая выделяет память, не определяя,
что в ней будет храниться. Буфер массива можно создать с помощью конструктора
ArrayBuffer:
Этот фрагмент создаст новый буфер buffer2 и скопирует в него байты с индексами
4 и 5. По аналогии с версией этого метода для массивов второй аргумент метода
slice() представляет индекс, по достижении которого копирование прекращается,
и сам элемент с этим индексом не копируется.
Конечно, было бы бессмысленно выделять память, если бы не было возможности
записывать в нее данные. Однако для этого нужно создать представление.
ПРИМЕЧАНИЕ
Буфер массива всегда представляет блок памяти с размером в байтах, указанным
в момент его создания. Вы можете изменять данные, содержащиеся в буфере, но
размер буфера — никогда.
Объект view в этом примере имеет доступ ко всем 10 байтам в буфере. Поддержива-
ется также возможность создания представления для части буфера. Просто укажите
смещение в байтах от начала буфера и, что необязательно, количество представ-
ляемых байтов. Если количество байтов не указано, DataView будет представлять
часть буфера от указанного смещения до конца буфера. Например:
Здесь view представляет только байты с индексами 5 и 6. Такой подход дает возмож-
ность создать несколько представлений в одном буфере, что может пригодиться,
когда для всего приложения желательно использовать один блок памяти, а не вы-
делять память динамически по мере необходимости.
Этот код создаст view1, представление для всего буфера, и view2, представление
для малой части буфера. Эти представления будут иметь одинаковые значения
свойств buffer, потому что оба связаны с одним и тем же буфером. Однако свойства
byteOffset и byteLength этих представлений будут отличаться. Они отражают части
буфера, на которые распространяется действие каждого представления.
Конечно, информация о представляемой области памяти сама по себе не особенно
полезна. Необходимо еще иметь возможность читать данные из памяти и записы-
вать их в нее.
234 Глава 10 • Расширенные возможности массивов
view.setInt8(0, 5);
view.setInt8(1, -1);
console.log(view.getInt8(0)); // 5
console.log(view.getInt8(1)); // -1
view.setInt8(0, 5);
view.setInt8(1, -1);
console.log(view.getInt16(0)); // 1535
console.log(view.getInt8(0)); // 5
console.log(view.getInt8(1)); // -1
Buffer contents
new Array.Buffer(2)
view.setInt8(0, 5)
view.setInt8(1, -1)
биты как одно 16-разрядное целое число со знаком, которое в десятичном пред-
ставлении имеет вид 1535.
Объект DataView прекрасно подходит для случаев, когда приходится смешивать
типы данных подобным способом. Однако если вы используете только один опре-
деленный тип данных, лучшим выбором будет применение специализированных
представлений.
console.log(ints.byteLength); // 4
console.log(ints.length); // 2
console.log(floats.byteLength); // 20
console.log(floats.length); // 5
Здесь создается массив ints размером в два элемента. Каждое 16-разрядное целое
число занимает два байта, поэтому всего массив занимает четыре байта. Массив
floats имеет размер в пять элементов, поэтому для его размещения требуется
20 байтов (по четыре байта на элемент). В обоих случаях создается новый буфер,
доступный через свойство buffer.
ПРИМЕЧАНИЕ
Если конструктор типизированного массива вызывается без аргументов, он действует
так, как если бы получил число 0. В результате создается типизированный массив,
не способный хранить данные, потому что для буфера выделено ноль байт памяти.
console.log(ints1.byteLength); // 4
console.log(ints1.length); // 2
Сходства типизированных и обычных массивов 239
console.log(ints1[0]); // 25
console.log(ints1[1]); // 50
console.log(ints2.byteLength); // 8
console.log(ints2.length); // 2
console.log(ints2[0]); // 25
console.log(ints2[1]); // 50
РАЗМЕР ЭЛЕМЕНТА
Каждый типизированный массив состоит из множества элементов, и размер
элемента — это количество байтов, составляющих каждый элемент. Данное
значение хранится в свойстве BYTES_PER_ELEMENT каждого конструктора и каж-
дого экземпляра, благодаря чему вы легко сможете узнать размер элемента:
console.log(UInt8Array.BYTES_PER_ELEMENT); // 1
console.log(UInt16Array.BYTES_PER_ELEMENT); // 2
console.log(ints.length); // 2
console.log(ints[0]); // 25
console.log(ints[1]); // 50
ints[0] = 1;
240 Глава 10 • Расширенные возможности массивов
ints[1] = 2;
console.log(ints[0]); // 1
console.log(ints[1]); // 2
ПРИМЕЧАНИЕ
В отличие от обычных массивов, размеры типизированных массивов нельзя изменить
с помощью свойства length. Это свойство недоступно для записи, поэтому любая
попытка изменить его просто игнорируется в нестрогом режиме и вызывает ошибку
в строгом.
Общие методы
Типизированные массивы также включают большое количество методов, функцио-
нально эквивалентных одноименным методам обычных массивов. В типизирован-
ных массивах доступны следующие методы:
console.log(mapped.length); // 2
console.log(mapped[0]); // 50
console.log(mapped[1]); // 100
Здесь для создания нового массива на основе значений в ints используется метод
map(). Функция отображения удваивает каждое значение в массиве и возвращает
новый массив Int16Array.
Те же самые итераторы
Типизированные массивы имеют те же три итератора, что и обычные массивы:
метод entries(), метод keys() и метод values(). Это означает, что типизированные
массивы, как и обычные массивы, можно использовать в циклах for-of и операторах
расширения. Например:
console.log(ints.length); // 2
console.log(ints[0]); // 25
console.log(ints[1]); // 50
console.log(floats.length); // 2
console.log(floats[0]); // 1.5
console.log(floats[1]); // 2.5
Различия в поведении
Обычные массивы могут увеличиваться и сжиматься по мере операций с ними, но
типизированные массивы всегда сохраняют свой размер неизменным. Нельзя при-
своить значение элементу типизированного массива с несуществующим числовым
индексом, как это возможно с обычными массивами, потому что типизированные
массивы проигнорируют такую операцию. Например:
console.log(ints.length); // 2
console.log(ints[0]); // 25
console.log(ints[1]); // 50
ints[2] = 5;
console.log(ints.length); // 2
console.log(ints[2]); // undefined
console.log(ints.length); // 1
console.log(ints[0]); // 0
Различия типизированных и обычных массивов 243
console.log(mapped.length); // 2
console.log(mapped[0]); // 0
console.log(mapped[1]); // 0
Поскольку строка "hi" не является 16-разрядным целым числом, вместо нее в массив
результата будет записан 0. Благодаря такой коррекции ошибок методы типизи-
рованных массивов не возбуждают ошибок в присутствии недопустимых данных,
потому что недопустимые данные никогда не попадают в массивы.
Отсутствующие методы
Несмотря на наличие у типизированных массивов большого количества тех же
методов, что и у обычных массивов, у них отсутствуют некоторые типичные
методы. Например, следующие методы не поддерживаются типизированными
массивами:
concat() shift()
pop() splice()
push() unshift()
Кроме метода concat(), все остальные методы могут изменять размер массива.
Типизированные массивы имеют фиксированный размер, именно поэтому данные
методы недоступны для типизированных массивов. Метод concat() не поддер-
живается, потому что результат конкатенации двух типизированных массивов
(особенно если они хранят данные разных типов) выглядит сомнительным, и такая
операция, прежде всего, противоречила бы целям применения типизированных
массивов.
244 Глава 10 • Расширенные возможности массивов
Дополнительные методы
Наконец, типизированные массивы имеют два метода, отсутствующие в обычных
массивах: set() и subarray(). Эти два метода выполняют противоположные опера-
ции: set() копирует содержимое другого массива в существующий типизирован-
ный массив, а метод subarray() извлекает часть существующего типизированного
массива в новый типизированный массив.
Метод set() принимает массив (типизированный или обычный) и необязательное
смещение, определяющее, откуда начинать вставку данных. Если опустить второй
аргумент, смещение получит значение по умолчанию, равное 0. Данные из мас-
сива-аргумента будут скопированы в данный типизированный массив, при этом
гарантируется, что будут записаны данные только допустимых типов. Например:
ints.set([25, 50]);
ints.set([75, 100], 2);
console.log(ints.toString()); // 25,50,75,100
Этот код создает Int16Array с четырьмя элементами. Первый вызов set() скопирует
два значения в первый и второй элементы массива. Второй вызов set() получает
смещение 2, указывающее, что значения должны записываться в массив, начиная
с третьего элемента.
Метод subarray() принимает необязательные начальный и конечный индексы
(значение из элемента с конечным индексом не копируется, так же как в методе
slice() ) и возвращает новый типизированный массив. Можно опустить оба
аргумента, чтобы создать полную копию исходного типизированного массива.
Например:
console.log(subints1.toString()); // 25,50,75,100
console.log(subints2.toString()); // 75,100
console.log(subints3.toString()); // 50,75
В заключение
Спецификация ECMAScript 6 продолжила курс, начатый в ECMAScript 5, на уве-
личение практической ценности массивов. Появилось два новых способа создания
массивов: методы Array.of() и Array.from(). Метод Array.from() может преоб-
разовывать в массивы итерируемые объекты и объекты, подобные массивам. Оба
метода наследуются производными классами и используют свойство Symbol.species
для определения типа возвращаемого значения (другие наследуемые методы, воз-
вращающие массивы, также используют свойство Symbol.species).
У массивов появилось и несколько новых методов. Методы fill() и copyWithin()
дают возможность заменять элементы массива. Методы find() и findIndex() удобно
использовать для поиска первого элемента в массиве, соответствующего некоторому
критерию. Метод find() возвращает первый элемент, соответствующий критерию,
а findIndex() возвращает индекс.
Типизированные массивы формально не являются массивами, потому что не на-
следуют тип Array, но они выглядят и действуют как массивы. Типизированные
массивы могут содержать данные любого из восьми разных числовых типов и раз-
мещаются в объектах ArrayBuffer, представляющих биты числа или последователь-
ности чисел. Типизированные массивы обеспечивают более эффективный способ
выполнения арифметических операций, потому что значения не преобразуются
взад и вперед между форматами, как это происходит при использовании числового
типа JavaScript.
11 Объект Promise
и асинхронное
программирование
Модель событий
Когда пользователь щелкает на кнопке или нажимает клавишу на клавиатуре,
генерируется событие, такое как onclick. В ответ на действие пользователя это
событие может добавить новое задание в конец очереди. Это самая простая форма
асинхронного программирования на JavaScript. Код обработчика событий не будет
выполнен, пока не возникнет событие, а когда он выполняется, получает соответ-
ствующий контекст. Например:
Обратные вызовы
Фреймворк Node.js продвигает улучшенную модель асинхронного программиро-
вания, основанную на обратных вызовах. Шаблон обратных вызовов напоминает
модель событий, потому что в нем асинхронный код не выполняется до некоторой
248 Глава 11 • Объект Promise и асинхронное программирование
точки времени в будущем. И отличается тем, что функция для вызова передается
в аргументе, как показано ниже:
console.log(contents);
});
console.log("Hi!");
writeFile("example.txt", function(err) {
if (err) {
throw err;
}
method1(function(err, result) {
if (err) {
throw err;
}
method2(function(err, result) {
if (err) {
throw err;
}
method3(function(err, result) {
if (err) {
throw err;
}
method4(function(err, result) {
if (err) {
throw err;
}
method5(result);
});
});
});
});
ПРИМЕЧАНИЕ
Любой объект, реализующий метод then(), описанный в предыдущем абзаце, на
зывается thenable-объектом1. Все объекты Promise являются thenable-объектами,
но не все thenable-объекты представляют асинхронные операции.
promise.then(function(contents) {
// выполнено
console.log(contents);
}, function(err) {
// отклонено
console.error(err.message);
});
promise.then(function(contents) {
// выполнено
console.log(contents);
});
promise.then(null, function(err) {
// отклонено
console.error(err.message);
});
Все три вызова then() оперируют с одним и тем же объектом promise. Первый готов
обработать успешное и неуспешное завершение операции. Второй обрабатывает
только успешное завершение; ошибки будут просто игнорироваться. Третий об-
рабатывает только неуспешное завершение; успешное завершение игнорируется.
Объекты Promise имеют также метод catch(), действующий подобно методу then(),
когда определяется только обработчик отказа. Например, следующие вызовы
catch() и then() функционально эквивалентны:
promise.catch(function(err) {
// отклонено
console.error(err.message);
});
// то же самое:
promise.then(null, function(err) {
// отклонено
console.error(err.message);
});
ПРИМЕЧАНИЕ
Каждый вызов then() или catch() создает новое задание для выполнения после
того, как объект Promise будет установлен. Но эти задания помещаются в отдельную
очередь, предназначенную специально для нужд объектов Promise. Знание всех
деталей работы этой второй очереди заданий не требуется для понимания особен
ностей использования объектов Promise. Достаточно понимать, как вообще действует
очередь заданий.
Основы объектов Promise 253
// Пример из Node.js
let fs = require("fs");
function readFile(filename) {
return new Promise(function(resolve, reject) {
Имейте в виду, что исполнитель запускается сразу же, как только вызывается
readFile(). Когда исполнитель вызывает resolve() или reject(), в очередь добав-
ляется задание, которое выполнит установку объекта Promise. Это называется пла-
нированием задания, и если вам приходилось использовать функцию setTimeout()
или setInterval(), вы должны уже быть знакомы с этим понятием. Планируя
задание, вы добавляете новое задание в очередь, как бы говоря: «Выполнить его
не прямо сейчас, а позднее». Например, функция setTimeout() позволяет указать
задержку перед добавлением задания в очередь:
console.log("Hi!");
Этот код запланирует добавление задания в очередь через 500 мс. Два вызова
console.log() выведут следующее:
Hi!
Timeout
console.log("Hi!");
Promise
Hi!
});
promise.then(function() {
console.log("Resolved.");
});
console.log("Hi!");
Promise
Hi!
Resolved
Использование Promise.resolve()
Метод Promise.resolve() принимает единственный аргумент и возвращает
объект Promise в состоянии «выполнено». Значит, в данном случае не происхо-
дит планирования задания, и вам нужно добавить один или несколько обработ
чиков успешного выполнения, чтобы извлечь значение из объекта Promise. На-
пример:
promise.then(function(value) {
console.log(value); // 42
});
Использование Promise.reject()
Аналогично можно создавать объекты Promise в состоянии «отклонено», используя
метод Promise.reject(). Он действует подобно методу Promise.resolve(), за исклю-
чением того, что создает объект Promise в состоянии «отклонено», как показано ниже:
promise.catch(function(value) {
console.log(value); // 42
});
ПРИМЕЧАНИЕ
Если в вызов Promise.resolve() или Promise.reject() передать объект Promise,
они вернут его без изменений.
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
console.log(value); // 42
});
Основы объектов Promise 257
let thenable = {
then: function(resolve, reject) {
reject(42);
}
};
let p1 = Promise.resolve(thenable);
p1.catch(function(value) {
console.log(value); // 42
});
Ошибки исполнителя
Если ошибка возникнет внутри исполнителя, будет вызван обработчик отказа
в объекте Promise. Например:
promise.catch(function(error) {
console.log(error.message); // "Explosion!"
});
258 Глава 11 • Объект Promise и асинхронное программирование
promise.catch(function(error) {
console.log(error.message); // "Explosion!"
});
// немного позднее...
rejected.catch(function(value) {
// теперь ошибка обработана
console.log(value);
});
В любом месте можно вызвать then() или catch() и правильно обработать оба со-
стояния объекта Promise, но с таким подходом трудно понять, когда точно будет
Глобальная обработка отклоненных объектов Promise 259
let rejected;
let rejected;
process.on("rejectionHandled", function(promise) {
console.log(rejected === promise); // true
});
260 Глава 11 • Объект Promise и асинхронное программирование
process.on("rejectionHandled", function(promise) {
possiblyUnhandledRejections.delete(promise);
});
setInterval(function() {
possiblyUnhandledRejections.forEach(function(reason, promise) {
console.log(reason.message ? reason.message : reason);
// обработать отказы
handleRejection(promise, reason);
});
possiblyUnhandledRejections.clear();
}, 60000);
Этот код использует ассоциативный массив для хранения объектов Promise и со-
ответствующих им ошибок, описывающих причину отказа. Объекты Promise
служат ключами, а объекты ошибок — связанными с ними значениями. Каждый
раз когда возникает событие unhandledRejection, объект Promise с причиной от-
каза добавляется в ассоциативный массив. Каждый раз когда возникает событие
rejectionHandled, объект Promise, отказ которого был обработан, удаляется из
Глобальная обработка отклоненных объектов Promise 261
let rejected;
window.onunhandledrejection = function(event) {
console.log(event.type); // "unhandledrejection"
console.log(event.reason.message); // "Explosion!"
console.log(rejected === event.promise); // true
});
window.onrejectionhandled = function(event) {
262 Глава 11 • Объект Promise и асинхронное программирование
console.log(event.type); // "rejectionhandled"
console.log(event.reason.message); // "Explosion!"
console.log(rejected === event.promise); // true
});
window.onrejectionhandled = function(event) {
possiblyUnhandledRejections.delete(event.promise);
};
setInterval(function() {
possiblyUnhandledRejections.forEach(function(reason, promise) {
console.log(reason.message ? reason.message : reason);
// обработать отказы
handleRejection(promise, reason);
});
possiblyUnhandledRejections.clear();
}, 60000);
p1.then(function(value) {
console.log(value);
}).then(function() {
console.log("Finished");
});
42
Finished
Вызов p1.then() вернет второй объект Promise, для которого также будет вызван
метод then(). Обработчик успешного выполнения во втором вызове then() будет
вызван, только если первый объект Promise перейдет в состояние «выполнено».
Если рассоединить цепочку в этом примере, то же самое можно было бы реали-
зовать так:
let p2 = p1.then(function(value) {
console.log(value);
})
p2.then(function() {
console.log("Finished");
});
В этой новой версии кода результат вызова p1.then() сохраняется в p2, а затем
вызывается p2.then(), чтобы добавить заключительный обработчик успешного
264 Глава 11 • Объект Promise и асинхронное программирование
Перехват ошибок
Составление цепочек из объектов Promise дает возможность перехватывать ошибки,
возникающие в обработчиках успешного или неуспешного выполнения операции,
подключенных к предыдущему объекту Promise. Например:
p1.then(function(value) {
throw new Error("Boom!");
}).catch(function(error) {
console.log(error.message); // "Boom!"
});
p1.catch(function(error) {
console.log(error.message); // "Explosion!"
throw new Error("Boom!");
}).catch(function(error) {
console.log(error.message); // "Boom!"
});
ПРИМЕЧАНИЕ
Всегда добавляйте обработчик отказа в конец цепочки объектов Promise, чтобы
гарантировать правильную обработку любых возникающих ошибок.
Составление цепочек из объектов Promise 265
p1.then(function(value) {
console.log(value); // "42"
return value + 1;
}).then(function(value) {
console.log(value); // "43"
});
p1.catch(function(value) {
// обработчик отказа первого объекта
console.log(value); // "42"
return value + 1;
}).then(function(value) {
// обработчик успешного выполнения второго объекта
console.log(value); // "43"
});
p1.then(function(value) {
// первый обработчик успешного выполнения
console.log(value); // 42
return p2;
}).then(function(value) {
// второй обработчик успешного выполнения
console.log(value); // 43
});
let p3 = p1.then(function(value) {
// первый обработчик успешного выполнения
console.log(value); // 42
return p2;
});
p3.then(function(value) {
// второй обработчик успешного выполнения
console.log(value); // 43
});
p1.then(function(value) {
// первый обработчик успешного выполнения
console.log(value); // 42
return p2;
}).then(function(value) {
// второй обработчик успешного выполнения
console.log(value); // не будет вызван
});
p1.then(function(value) {
// первый обработчик успешного выполнения
console.log(value); // 42
return p2;
}).catch(function(value) {
// обработчик отказа
console.log(value); // 43
});
268 Глава 11 • Объект Promise и асинхронное программирование
Теперь в случае ошибки в p2 вызывается обработчик отказа, и ему передается зна-
чение отказа 43 из p2.
Возврат thenable-объектов из обработчиков успешного или неуспешного выпол-
нения не влияет на момент вызова исполнителя. Первый объект Promise в цепочке
вызовет своего исполнителя первым, затем второй объект вызовет своего испол-
нителя и т. д. Возможность вернуть thenable-объект просто позволяет определить
дополнительные обработчики результатов. Создавая новый объект Promise в об-
работчике успешного выполнения, вы откладываете вызов других обработчиков
успешного выполнения. Например:
p1.then(function(value) {
console.log(value); // 42
return p2
}).then(function(value) {
console.log(value); // 43
});
Метод Promise.all()
Метод Promise.all() принимает единственный аргумент — итерируемый объект
(например, массив) с объектами Promise для мониторинга и возвращает объ-
ект Promise, который выходит из состояния ожидания, только когда все Promise
Обработка сразу нескольких объектов Promise 269
p4.then(function(value) {
console.log(Array.isArray(value)); // true
console.log(value[0]); // 42
console.log(value[1]); // 43
console.log(value[2]); // 44
});
p4.catch(function(value) {
270 Глава 11 • Объект Promise и асинхронное программирование
console.log(Array.isArray(value)) // false
console.log(value); // 43
});
Метод Promise.race()
Метод Promise.race() реализует несколько иной подход к мониторингу сразу
нескольких объектов Promise. Он также принимает итерируемый объект с объ-
ектами Promise для мониторинга и возвращает Promise, но возвращаемый Promise
устанавливается сразу же, как только устанавливается первый из переданных объ-
ектов Promise. В отличие от Promise.all(), метод Promise.race() не ждет, когда
выполнятся все Promise, а возвращает соответствующий объект Promise, как только
любой из переданных объектов завершит выполнение. Например:
let p1 = Promise.resolve(42);
p4.then(function(value) {
console.log(value); // 42
});
В этом примере p1 создается как установившийся объект Promise, тогда как другие
лишь планируют свои задания. Обработчик успешного завершения для p4 вызыва-
ется немедленно со значением 42, при этом другие объекты Promise игнорируются.
Метод Promise.race() проверяет переданные ему объекты Promise, пока не найдет
первый установившийся. Если первый установившийся объект Promise находится
в состоянии «выполнено», возвращаемый объект Promise также получает состояние
«выполнено»; если первый установившийся объект Promise находится в состоянии
«отклонено», возвращаемый объект Promise получает состояние «отклонено». Ниже
приводится пример с отказом:
Наследование Promise 271
let p2 = Promise.reject(43);
p4.catch(function(value) {
console.log(value); // 43
});
Наследование Promise
Promise можно использовать в качестве основы для создания производных классов,
как любые другие встроенные типы. Это позволяет определять свои реализации
асинхронных операций, расширяющие возможности встроенного объекта Promise.
Например, представьте, что требуется создать объект Promise, который может
использовать методы с именами success() и failure() помимо обычных then()
и catch(). Такой тип можно определить, как показано ниже:
success(resolve, reject) {
return this.then(resolve, reject);
}
failure(reject) {
return this.catch(reject);
}
}
promise.success(function(value) {
console.log(value); // 42
}).failure(function(value) {
console.log(value);
});
272 Глава 11 • Объект Promise и асинхронное программирование
В этом примере класс MyPromise наследует тип Promise и добавляет два метода.
Метод success() имитирует resolve(), а failure() имитирует метод reject().
Оба метода используют ссылку this для вызова имитируемого метода. Производ
ный класс действует точно так же, как встроенный тип Promise, но дополнительно
теперь можно при желании вызывать методы success() и failure().
Поскольку статические методы наследуются, производный класс также получает
методы MyPromise.resolve(), MyPromise.reject(), MyPromise.race() и MyPromise.
all(). Последние два метода действуют точно так же, как встроенные методы, но
первые два обретают несколько иное поведение.
Оба метода, MyPromise.resolve() и MyPromise.reject(), будут возвращать экземпля-
ры MyPromise независимо от передаваемых им значений, потому что для определения
типа возвращаемого объекта они используют свойство Symbol.species (см. раздел
«Свойство Symbol.species» в главе 9). Если в любой из методов передать встроенный
объект Promise, он будет выполнен — успешно или неуспешно, — и метод вернет
новый экземпляр MyPromise, поэтому вы сможете использовать новые методы для
обработки успешного или неуспешного выполнения. Например:
let p2 = MyPromise.resolve(p1);
p2.success(function(value) {
console.log(value); // 42
});
let fs = require("fs");
function run(taskDef) {
// запустить задание
let result = task.next();
result = task.next(data);
step();
});
} else {
result = task.next(result.value);
step();
}
}
}
// запустить обработку
step();
}
function readFile(filename) {
return function(callback) {
fs.readFile(filename, callback);
};
}
// запустить задание
run(function*() {
let contents = yield readFile("config.json");
doSomethingWith(contents);
console.log("Done");
});
274 Глава 11 • Объект Promise и асинхронное программирование
let fs = require("fs");
function run(taskDef) {
// создать итератор
let task = taskDef();
// запустить задание
let result = task.next();
function readFile(filename) {
return new Promise(function(resolve, reject) {
fs.readFile(filename, function(err, contents) {
if (err) {
reject(err);
} else {
Выполнение асинхронных заданий с помощью Promise 275
resolve(contents);
}
});
});
}
// запустить задание
run(function*() {
let contents = yield readFile("config.json");
doSomethingWith(contents);
console.log("Done");
});
В этой версии обобщенная функция run() вызывает генератор, чтобы создать ите-
ратор. Затем она вызывает task.next(), чтобы запустить задание, и рекурсивно
продолжает вызывать step(), пока итератор не исчерпается.
Внутри функции step() свойство result.done имеет значение false, если работа
еще не закончена. В этот момент свойство result.value должно ссылаться на объ-
ект Promise. На всякий случай, если вдруг используемая функция вернет что-то
отличное от объекта Promise, вызывается Promise.resolve(). (Напомню, что если
методу Promise.resolve() передать объект Promise, он просто вернет его, ничего не
изменив, но любое другое значение завернет в объект Promise.) Далее добавляется
обработчик успешного завершения, извлекающий значение из объекта Promise
и передающий это значение обратно в итератор. Затем переменной result при-
сваивается следующий результат, возвращаемый итератором, после чего функция
step() вызывает сама себя.
Обработчик отказа сохраняет любое значение отказа в объект ошибки. Метод task.
throw() передает этот объект ошибки обратно в итератор, и если ошибка перехваты-
вается заданием, результат присваивается следующему возвращаемому значению.
В заключение внутри catch() вызывается step(), чтобы продолжить работу.
Данная функция run() может вызывать любые генераторы, использующие yield
для доступа к асинхронному коду без передачи объектов Promise (или обратных
вызовов) разработчику. В действительности, поскольку возвращаемое значение
функции всегда преобразуется в объект Promise, эта функция может возвращать
все, что угодно, а не только объекты Promise. Это означает, что при вызове в ин-
струкции yield все методы, синхронные и асинхронные, будут работать корректно,
и вам никогда не придется проверять, является ли возвращаемое ими значение
объектом Promise.
Единственное, что от вас потребуется, — это гарантировать, что асинхронные функ-
ции, такие как readFile(), будут возвращать объект Promise, который корректно
идентифицирует свое состояние. Для Node.js это означает, что вам придется пре-
образовать встроенные методы так, чтобы они возвращали объекты Promise вместо
использования обратных вызовов.
276 Глава 11 • Объект Promise и асинхронное программирование
В заключение
Объекты Promise помогают упростить асинхронное программирование на JavaScript,
давая более полный контроль над асинхронными операциями и возможностью их
сочетания, чем позволяют события и обратные вызовы. Объекты Promise плани-
руют задания, добавляя их в очередь заданий движка JavaScript для выполнения
в будущем, а вторая очередь заданий следит за состоянием объектов Promise
и гарантирует надлежащее выполнение обработчиков успешного и неуспешного
выполнения операции.
Объекты Promise имеют три возможных состояния: «ожидание», «выполнено»
и «отклонено». Сразу после создания объект Promise находится в состоянии «ожи-
дание» и переходит в состояние «выполнено» или «отклонено» в случае успешного
или неуспешного завершения исполнителя. В любом случае вы можете добавлять
обработчики, чтобы перехватить момент, когда объект Promise установится. Метод
then() позволяет установить два обработчика, успешного и неуспешного выполне-
ния операции, а метод catch() — только обработчик отказа.
В заключение 277
Проблема с массивами
До выхода ECMAScript 6 разработчики не могли имитировать работу объектов
массивов в собственных объектах. Свойство length массива изменяется автома-
тически, когда программа присваивает значения определенным элементам этого
массива, а изменяя свойство length, можно оказывать влияние на элементы мас-
сива. Например:
console.log(colors.length); // 3
colors[3] = "black";
console.log(colors.length); // 4
Введение в прокси-объекты и Reflection API 279
console.log(colors[3]); // "black"
colors.length = 2;
console.log(colors.length); // 2
console.log(colors[3]); // undefined
console.log(colors[2]); // undefined
console.log(colors[1]); // "green"
ПРИМЕЧАНИЕ
Такое нестандартное поведение свойств с числовыми именами и свойства length
объясняет, почему в ECMAScript 6 массивы считаются экзотическими объектами.
ПРИМЕЧАНИЕ
В первоначальной версии спецификации ECMAScript 6 имелась еще одна ловушка —
enumerate, предназначавшаяся для изменения поведения перечисления свойств
объекта в цикле for-in и методе Object.keys(). Однако она была убрана в специ
фикации ECMAScript 7 (ECMAScript 2016), потому что в ходе реализации вскрылись
некоторые сложности. Ловушка enumerate не поддерживается ни в одном окружении
JavaScript и потому не рассматривается в этой главе.
всех операций, кроме тех, для которых определены ловушки. Чтобы создать про-
стейший прокси-объект, который лишь передает вызовы реализации по умолчанию,
можно использовать обработчик без ловушек, например:
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
console.log(target.name); // "proxy"
target.name = "target";
console.log(proxy.name); // "target"
console.log(target.name); // "target"
аргумента, что и ловушка set, что дает возможность использовать этот метод внутри
ловушки. Ловушка должна вернуть true, если значение было присвоено свойству,
и false — в противном случае. (Метод Reflect.set() возвращает правильное зна-
чение в зависимости от успеха операции.)
Для проверки значений, присваиваемых свойствам, можно использовать ловушку
set. Например:
let target = {
name: "target"
};
// добавить свойство
return Reflect.set(trapTarget, key, value, receiver);
}
});
значение, и поэтому возбуждается ошибка. Так как в этом примере свойству count
присваивается 1, ловушка вызывает Reflect.set() с теми же четырьмя аргументами,
которые получила сама, чтобы добавить новое свойство.
Когда свойству proxy.name присваивается строка, операция выполняется успешно.
Так как объект target уже имеет свойство name, оно исключается из проверки вы-
зовом метода trapTarget.hasOwnProperty(). Это гарантирует поддержку свойств,
которые прежде имели нечисловые значения.
Однако попытка присвоить строку свойству proxy.anotherName вызывает ошибку.
Свойство anotherName не существовало в объекте target прежде, поэтому оно под-
вергается проверке. В процессе проверки значения возбуждается ошибка, потому
что "proxy" не является числовым значением.
Ловушка set позволяет перехватывать операции записи в свойства, а ловушка
get — операции чтения свойств.
console.log(target.name); // undefined
Так как проверка свойства должна выполняться только при чтении свойств, ее
можно реализовать в ловушке get. Ловушка get вызывается, когда производится
операция чтения свойства, даже если это свойство отсутствует в объекте. Она при-
нимает три аргумента:
let target = {
value: 42;
}
Объект имеет оба свойства, value и toString, поэтому в обоих случаях оператор in
вернул true. Свойство value — это собственное свойство, тогда как свойство toString
принадлежит прототипу (унаследовано от Object). Прокси-объекты позволяют
перехватить эту операцию с помощью ловушки has и вернуть другое значение.
Ловушка has вызывается при любом использовании оператора in. Она принимает
два аргумента:
let target = {
name: "target",
value: 42
};
Ловушка has в объекте proxy сравнивает аргумент key со строкой "value" и воз-
вращает false, если условие выполняется. В противном случае вызывается метод
Reflect.has(), реализующий поведение по умолчанию. В результате для свойства
value оператор in возвращает false, даже если оно существует в объекте target. Для
других свойств, name и toString, корректно возвращается true, когда их наличие
проверяется оператором in.
let target = {
name: "target",
value: 42
};
let target = {
name: "target",
value: 42
};
setPrototypeOf(trapTarget, proto) {
return false;
}
Ловушки операций с прототипом 289
});
// завершится успехом
Object.setPrototypeOf(target, {});
// вызовет ошибку
Object.setPrototypeOf(proxy, {});
Этот пример подчеркивает разницу между поведением target и proxy. Для target
метод Object.getPrototypeOf() возвращает его прототип, а для proxy возвращается
null, потому что вызывается ловушка getPrototypeOf. Аналогично метод Object.
setPrototypeOf() завершается успехом, когда он вызывается для объекта target,
но возбуждает ошибку, когда вызывается для proxy, из-за вмешательства ловушки
setPrototypeOf.
Если вдруг понадобится поведение по умолчанию для этих двух ловушек, ис-
пользуйте соответствующие методы объекта Reflect. Например, следующий код
реализует поведение по умолчанию для ловушек getPrototypeOf и setPrototypeOf:
setPrototypeOf(trapTarget, proto) {
return Reflect.setPrototypeOf(trapTarget, proto);
}
});
// завершится успехом
Object.setPrototypeOf(target, {});
// вызовет ошибку
Reflect.getPrototypeOf(1);
ПРИМЕЧАНИЕ
При использовании внутри прокси-объектов методы Reflect.getPrototypeOf()/
Object.getPrototypeOf() и Reflect.setPrototypeOf()/Object.setPrototypeOf()
будут вызывать ловушки getPrototypeOf и setPrototypeOf соответственно.
Ловушки, связанные
с расширяемостью объектов
В ECMAScript 5 была добавлена возможность управления способностью объ-
ектов к расширению в виде пары методов Object.preventExtensions() и Object.
isExtensible(), а в ECMAScript 6 появилась возможность перехватывать вызовы
этих методов с помощью ловушек preventExtensions и isExtensible. Обе ловушки
принимают единственный аргумент trapTarget — объект, для которого был вызван
метод. Ловушка isExtensible должна вернуть логическое значение, сообщающее
о способности объекта расширяться. Ловушка preventExtensions также должна
вернуть логическое значение, сообщающее об успехе операции. Методы Reflect.
preventExtensions() и Reflect.isExtensible() реализуют поведение по умолча-
нию. Оба возвращают логическое значение, поэтому их можно непосредственно
использовать в соответствующих ловушках.
preventExtensions(trapTarget) {
return Reflect.preventExtensions(trapTarget);
}
});
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true
Object.preventExtensions(proxy);
console.log(Object.isExtensible(target)); // false
console.log(Object.isExtensible(proxy)); // false
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true
Object.preventExtensions(proxy);
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true
// вызовет ошибку
let result2 = Reflect.isExtensible(2);
// вызовет ошибку
let result3 = Reflect.preventExtensions(2);
getOwnPropertyDescriptor(trapTarget, key) {
return Reflect.getOwnPropertyDescriptor(trapTarget, key);
}
});
Object.defineProperty(proxy, "name", {
value: "proxy"
});
console.log(proxy.name); // "proxy"
console.log(descriptor.value); // "proxy"
Этот код определяет в объекте proxy свойство с именем "name", используя метод
Object.defineProperty(). Затем он извлекает дескриптор для этого свойства вы-
зовом Object.getOwnPropertyDescriptor().
Object.defineProperty(proxy, "name", {
value: "proxy"
});
console.log(proxy.name); // "proxy"
// вызовет ошибку
Object.defineProperty(proxy, nameSymbol, {
value: "proxy"
});
ПРИМЕЧАНИЕ
Можно также скрыть ошибку в вызове Object.defineProperty(), просто возвращая
true и не вызывая Reflect.defineProperty(). В этом случае ошибка возбуждаться
не будет, даже при невозможности определить свойство.
Object.defineProperty(proxy, "name", {
value: "proxy",
name: "custom"
});
// вызовет ошибку
let descriptor = Object.getOwnPropertyDescriptor(proxy, "name");
Методы defineProperty()
Методы Object.defineProperty() и Reflect.defineProperty() отличаются возвра-
щаемыми значениями. Метод Object.defineProperty() возвращает свой первый
аргумент, тогда как Reflect.defineProperty() возвращает true в случае успеха
операции и false — в противном случае. Например:
console.log(result2); // true
Методы getOwnPropertyDescriptor()
Метод Object.getOwnPropertyDescriptor() преобразует свой первый аргумент
в объект, если он является простым значением, и затем продолжает работу. Метод
Reflect.getOwnPropertyDescriptor(), напротив, возбудит ошибку, если в первом ар-
гументе получит простое значение. Следующие пример демонстрирует обе ситуации:
// вызовет ошибку
let descriptor2 = Reflect.getOwnPropertyDescriptor(2, "name");
298 Глава 12 • Прокси-объекты и Reflection API
Ловушка ownKeys
Ловушка ownKeys перехватывает вызов внутреннего метода [[OwnPropertyKeys]]
и позволяет переопределять это поведение, возвращая массив значений. Этот массив
используется четырьмя методами: Object.keys(), Object.getOwnPropertyNames(),
Object.getOwnPropertySymbols() и Object.assign(). (Метод Object.assign() ис-
пользует массив, чтобы определить, какие свойства копировать.)
Поведение по умолчанию ловушки ownKeys реализует метод Reflect.ownKeys(),
возвращающий массив со всеми ключами собственных свойств, включая строки
и символы. Методы Object.getOwnPropertyNames() и Object.keys() исключают
символьные ключи из массива и возвращают получившийся результат, а метод
Object.getOwnPropertySymbols(), наоборот, исключает из массива строковые ключи.
Метод Object.assign() использует все имеющиеся в массиве ключи — и строковые,
и символьные.
Ловушка ownKeys принимает единственный аргумент target и должна вернуть объ-
ект, подобный массиву; в противном случае будет возбуждена ошибка. Ловушку
ownKeys можно использовать, например, для фильтрации свойств с определенными
ключами, которые не должны обнаруживаться методами Object.keys(), Object.
getOwnPropertyNames(), Object.getOwnPropertySymbols() и Object.assign(). Пред-
ставьте, что вам требуется исключить любые свойства, имена которых начинаются
с символа подчеркивания — типичный способ обозначения приватных свойств
в JavaScript. Для этого можно было бы использовать ловушку ownKeys, как по-
казано ниже:
proxy.name = "proxy";
proxy._name = "private";
proxy[nameSymbol] = "symbol";
keys = Object.keys(proxy),
symbols = Object.getOwnPropertySymbols(proxy);
console.log(names.length); // 1
console.log(names[0]); // "proxy"
console.log(keys.length); // 1
console.log(keys[0]); // "proxy"
console.log(symbols.length); // 1
console.log(symbols[0]); // "Symbol(name)"
console.log(proxy()); // 42
Здесь у нас имеется функция, возвращающая число 42. Прокси-объект для этой
функции определяет ловушки apply и construct, которые делегируют поведение
по умолчанию методам Reflect.apply() и Reflect.construct() соответственно.
В результате функция proxy действует в точности как целевая функция target,
и даже оператор typeof идентифицирует ее как функцию. Сначала функция proxy
вызывается без ключевого слова new, чтобы получить 42, а затем с ключевым словом
new, чтобы создать объект с именем instance. Этот объект интерпретируется как
экземпляр обеих функций, proxy и target, потому что для определения принад-
лежности instanceof использует цепочку прототипов. Поиск в цепочке прототипов
не перехватывается в этом прокси-объекте, поэтому proxy и target выглядят как
имеющие один и тот же прототип.
argumentList.forEach((arg) => {
if (typeof arg !== "number") {
throw new TypeError("All arguments must be numbers.");
}
});
console.log(sumProxy(1, 2, 3, 4)); // 10
// вызовет ошибку
console.log(sumProxy(1, "2", 3, 4));
Этот пример демонстрирует, как с помощью ловушки apply гарантировать, что все
аргументы функции являются числами. Функция sum() складывает все полученные
аргументы. Если ей передать нечисловое значение, она все равно попытается вы-
полнить операцию, но результат может получиться неожиданным. Прокси-объект
sumProxy, в который заключена функция sum(), перехватывает вызов функции
и проверяет каждый аргумент. Если все аргументы являются числами, вызывается
оригинальная функция. Для большей надежности в прокси-объекте определена
также ловушка construct, гарантирующая невозможность вызова с ключевым
словом new.
Аналогично можно гарантировать вызов только с ключевым словом new и проверить,
что все аргументы являются числами:
function Numbers(...values) {
this.values = values;
}
// вызовет ошибку
NumbersProxy(1, 2, 3, 4);
function Numbers(...values) {
this.values = values;
}
// вызовет ошибку
Numbers(1, 2, 3, 4);
Этот код возбудит ошибку при попытке вызвать Numbers() без ключевого слова
new. Своим действием этот пример напоминает второй пример из раздела «Про-
верка параметров функции» выше, но не использует прокси-объект. Такой код
писать намного проще, чем код, использующий прокси-объект, и данный подход
предпочтительнее, если единственной целью является предотвращение возможно-
сти вызова функции без ключевого слова new. Но иногда функции, чье поведение
требуется модифицировать, пишут другие. В таких случаях применение прокси-
объекта вполне оправданно.
Обработка вызовов функций с помощью ловушек apply и construct 303
function Numbers(...values) {
this.values = values;
}
class AbstractNumbers {
constructor(...values) {
if (new.target === AbstractNumbers) {
throw new TypeError("This function must be inherited from.");
}
this.values = values;
304 Глава 12 • Прокси-объекты и Reflection API
}
}
// вызовет ошибку
new AbstractNumbers(1, 2, 3, 4);
class AbstractNumbers {
constructor(...values) {
if (new.target === AbstractNumbers) {
throw new TypeError("This function must be inherited from.");
}
this.values = values;
}
}
class Person {
constructor(name) {
this.name = name;
}
}
let me = PersonProxy("Nicholas");
console.log(me.name); // "Nicholas"
console.log(me instanceof Person); // true
console.log(me instanceof PersonProxy); // true
Отключение прокси-объектов
Обычно прокси-объект нельзя отключить от целевого объекта после создания
прокси-объекта. Все примеры, приводившиеся до этого момента в данной главе,
использовали неотключаемые прокси-объекты. Но иногда бывает желательно от-
ключить прокси, чтобы он больше не использовался. Возможность отключения
прокси-объекта может пригодиться, когда из соображений безопасности требуется
предоставить объект через API и иметь возможность выключать некоторые функ-
циональные возможности в любой момент.
Для этого можно создавать отключаемые прокси-объекты с помощью метода
Proxy.revocable(), который принимает те же аргументы, что и конструктор Proxy,
306 Глава 12 • Прокси-объекты и Reflection API
let target = {
name: "target"
};
console.log(proxy.name); // "target"
revoke();
// вызовет ошибку
console.log(proxy.name);
console.log(colors.length); // 3
Решение проблемы с массивами 307
colors[3] = "black";
console.log(colors.length); // 4
console.log(colors[3]); // "black"
colors.length = 2;
console.log(colors.length); // 2
console.log(colors[3]); // undefined
console.log(colors[2]); // undefined
console.log(colors[1]); // "green"
function toUint32(value) {
return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}
function isArrayIndex(key) {
let numericKey = toUint32(key);
return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1);
}
function toUint32(value) {
return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}
function isArrayIndex(key) {
let numericKey = toUint32(key);
return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1);
}
function createMyArray(length=0) {
return new Proxy({ length }, {
set(trapTarget, key, value) {
// специальный случай
if (isArrayIndex(key)) {
let numericKey = Number(key);
colors[0] = "red";
colors[1] = "green";
colors[2] = "blue";
Решение проблемы с массивами 309
console.log(colors.length); // 3
colors[3] = "black";
console.log(colors.length); // 4
console.log(colors[3]); // "black"
function toUint32(value) {
return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}
function isArrayIndex(key) {
let numericKey = toUint32(key);
return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1);
}
function createMyArray(length=0) {
310 Глава 12 • Прокси-объекты и Reflection API
// специальный случай
if (isArrayIndex(key)) {
let numericKey = Number(key);
colors[0] = "red";
colors[1] = "green";
colors[2] = "blue";
colors[3] = "black";
console.log(colors.length); // 4
colors.length = 2;
console.log(colors.length); // 2
console.log(colors[3]); // undefined
console.log(colors[2]); // undefined
console.log(colors[1]); // "green"
console.log(colors[0]); // "red"
Ловушка set в этом примере сравнивает ключ со строкой "length" и, если условие
выполняется, корректирует содержимое объекта. Сначала вызовом Reflect.get()
извлекается текущая длина массива и сравнивается с новым значением. Если
новое значение меньше текущей длины, цикл for удаляет все свойства в целевом
объекте, которые должны быть недоступны. Цикл for выполняет обход в обратном
Решение проблемы с массивами 311
class Thing {
constructor() {
return new Proxy(this, {});
}
}
function toUint32(value) {
return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}
function isArrayIndex(key) {
let numericKey = toUint32(key);
return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1);
}
class MyArray {
constructor(length=0) {
this.length = length;
// специальный случай
if (isArrayIndex(key)) {
let numericKey = Number(key);
console.log(colors.length); // 3
colors[0] = "red";
colors[1] = "green";
colors[2] = "blue";
colors[3] = "black";
Использование прокси-объекта в качестве прототипа 313
console.log(colors.length); // 4
colors.length = 2;
console.log(colors.length); // 2
console.log(colors[3]); // undefined
console.log(colors[2]); // undefined
console.log(colors[1]); // "green"
console.log(colors[0]); // "red"
Использование прокси-объекта
в качестве прототипа
Прокси-объект можно использовать в качестве прототипа, но такая реализация
будет выглядеть сложнее, чем предыдущие примеры в этой главе. Когда прокси-
объект играет роль прототипа, его ловушки вызываются, только когда операции по
умолчанию выполняются прототипом, что ограничивает возможности прокси как
прототипа. Рассмотрим следующий пример.
Object.defineProperty(newTarget, "name", {
value: "newTarget"
});
console.log(newTarget.name); // "newTarget"
console.log(newTarget.hasOwnProperty("name")); // true
314 Глава 12 • Прокси-объекты и Reflection API
thing.name = "thing";
console.log(thing.name); // "thing"
// вызовет ошибку
let unknown = thing.unknown;
console.log(thing.hasOwnProperty("name")); // false
console.log(thing.name); // "thing"
console.log(thing.hasOwnProperty("name")); // true
console.log(thing.name); // "boo"
316 Глава 12 • Прокси-объекты и Reflection API
thing.name = "thing";
function NoSuchProperty() {
// пустая
}
function NoSuchProperty() {
// пустая
}
function NoSuchProperty() {
// пустая
}
NoSuchProperty.prototype = proxy;
super();
this.length = length;
this.width = width;
}
}
function NoSuchProperty() {
// пустая
}
В заключение
До появления ECMAScript 6 некоторые объекты (такие, как массивы) демонстриро-
вали нестандартное поведение, которое невозможно было повторить в собственных
объектах. Прокси-объекты изменили эту ситуацию. Они позволяют определять не-
стандартное поведение для некоторых низкоуровневых операций и с помощью их
ловушек воспроизводить любые аспекты поведения встроенных объектов JavaScript.
Эти ловушки вызываются за кулисами, когда выполняются разные операции, такие
как применение оператора in.
В ECMAScript 6 появился также механизм рефлексии (Reflection API), дающий
разработчикам возможность реализовать поведение по умолчанию для любой
ловушки в прокси-объектах. Для каждой ловушки в объекте Reflect (еще одно
новшество в ECMAScript 6) имеется соответствующий метод с тем же именем. Ис-
пользуя комбинацию из ловушек прокси-объекта и методов Reflection API, можно
заставить некоторые операции вести себя иначе в одних ситуациях и действовать
по умолчанию — в других.
Отключаемые прокси-объекты — это специальные прокси-объекты, которые можно
эффективно отключать вызовом функции revoke(). Функция revoke() завершает
функционирование прокси-объекта, поэтому любые попытки взаимодействий со
свойствами прокси после ее вызова приводят к ошибке. Отключаемые прокси-объ-
екты играют важную роль в обеспечении безопасности приложений, когда сторон-
ним разработчикам может понадобиться доступ к объектам в течение некоторого
интервала времени.
Прямое использование прокси-объектов — наиболее действенный путь, но иногда
бывает желательно использовать их в роли прототипов других объектов. Однако
в этом случае уменьшается число ловушек, сохраняющих свою практическую
ценность. Только ловушки get, set и has могут вызываться в ограниченном числе
случаев, когда прокси-объект используется в качестве прототипа.
13 Инкапсуляция кода
в модули
Основы экспортирования
Доступ к компонентам модуля извне можно открыть с помощью ключевого слова
export. В простейшем случае достаточно поместить export перед объявлением
переменной, функции или класса, чтобы экспортировать их из модуля, например:
// экспортировать данные
export var color = "red";
export let name = "Nicholas";
export const magicNumber = 7;
// экспортировать функцию
export function sum(num1, num2) {
return num1 + num1;
}
// экспортировать класс
export class Rectangle {
constructor(length, width) {
this.length = length;
this.width = width;
}
}
// определить функцию...
function multiply(num1, num2) {
return num1 * num2;
}
// ...и экспортировать ее
export multiply;
что это необходимо для экспорта. Анонимные функции или классы с помощью
этого синтаксиса можно экспортировать только с использованием ключевого слова
default (подробно обсуждается в разделе «Значения по умолчанию в модулях»
ниже).
Обратите также внимание на функцию multiply(), которая не экспортируется во
время определения. Такой способ тоже допустим, потому что не всегда требуется
экспортировать объявление: можно экспортировать ссылки. Кроме того, отметьте,
что функция subtract() в этом примере не экспортируется. Она будет недоступна
за пределами модуля, потому что любые переменные, функции и классы, не экс-
портированные явно, остаются закрытыми компонентами, доступными только
в модуле.
Основы импортирования
При наличии модуля, экспортирующего свои компоненты, доступ к этим ком-
понентам в другом модуле можно открыть с помощью ключевого слова import.
Инструкция import состоит из двух частей: первая определяет импортируемые
идентификаторы, а вторая — модуль, из которого они импортируются.
Ниже приводится базовая форма этой инструкции:
ПРИМЕЧАНИЕ
Список привязок в инструкции import выглядит как операция деструктуризации объ
екта, но это не так.
Когда вы импортируете привязки из модуля, они действуют, как если бы они были
объявлены с ключевым словом const. В результате вы не сможете объявить другую
переменную с тем же именем (а также импортировать другую привязку с тем же
именем), использовать идентификатор до инструкции import или изменить зна-
чение привязки.
324 Глава 13 • Инкапсуляция кода в модули
console.log(sum(1, 2)); // 3
Хотя помимо этой функции example.js экспортирует еще несколько привязок, дан-
ный пример импортирует только функцию sum(). Попытка связать имя sum с новым
значением вызовет ошибку, потому что импортированные привязки нельзя изменять.
ПРИМЕЧАНИЕ
Не забудьте добавить /, ./ или ../ в начало строки с именем импортируемого файла
для лучшей совместимости с разными версиями браузеров и Node.js.
// импортировать все
import * as example from "./example.js";
console.log(example.sum(1,
example.magicNumber)); // 8
console.log(example.multiply(1, 2)); // 2
Основы импортирования 325
Несмотря на то что в данном модуле имеется три инструкции import, файл example.js
выполнится только один раз. Если привязки из example.js попытается импортиро-
вать другой модуль в этом же приложении, он получит тот же экземпляр модуля,
который использует данный код.
function tryImport() {
import flag from "./example.js"; // синтаксическая ошибка
}
console.log(name); // "Nicholas"
setName("Greg");
console.log(name); // "Greg"
Переименование экспортируемых
и импортируемых привязок
Иногда бывает нежелательно использовать оригинальное имя переменной, функ-
ции или класса, импортируемого из модуля. К счастью, есть возможность изменять
имена при экспортировании и импортировании.
Значения по умолчанию в модулях 327
Здесь функция с локальным именем sum() экспортируется под именем add(). Это
означает, что когда другой модуль будет импортировать эту функцию, он должен
использовать имя add:
Этот код импортирует функцию add() и использует импортируемое имя для ее пере-
именования в sum() (локальное имя в данном контексте). Изменение локального име-
ни функции в инструкции import приведет к тому, что идентификатор add() в данном
модуле будет недоступен, несмотря на то что модуль импортирует функцию add().
console.log(sum(1, 2)); // 3
console.log(sum(1, 2)); // 3
console.log(color); // "red"
console.log(sum(1, 2)); // 3
console.log(color); // "red"
Реэкспорт привязки
Рано или поздно вам может потребоваться повторно экспортировать какие-то
привязки, импортированные модулем. Например, если вы создаете библиотеку,
состоящую из нескольких маленьких модулей. Повторно экспортировать импорти-
рованное значение можно с использованием тех же шаблонов, что уже обсуждались
в этой главе:
330 Глава 13 • Инкапсуляция кода в модули
Эта форма инструкции export отыщет в указанном модуле объявление sum и экс-
портирует его. Конечно, также поддерживается возможность экспорта значения
под другим именем:
export { sum as add } from "./example.js";
Эта инструкция экспортирует все, в том числе и значение по умолчанию, что может
повлиять на экспортирование значений, принадлежащих данному модулю. Напри-
мер, если example.js экспортирует значение по умолчанию, вы уже не сможете опре-
делить новое экспортируемое значение по умолчанию, используя этот синтаксис.
Это вполне допустимый модуль, даже если он ничего не экспортирует и не импор-
тирует. Этот код можно использовать и как модуль, и как сценарий. Поскольку
он ничего не экспортирует, его можно выполнить с помощью упрощенной версии
инструкции import, которая не импортирует никаких привязок:
import "./example.js";
items.pushAll(colors);
ПРИМЕЧАНИЕ
Вероятнее всего, импортирование без привязок будет использоваться в основном для
создания полифиллов и расширений.
Загрузка модулей
Спецификация ECMAScript 6 определяет синтаксис модулей, но не определяет,
как они должны загружаться. Отчасти этот недостаток спецификации объясня-
ется стремлением обеспечить независимость от конкретных окружений. Вместо
того чтобы пытаться определить единые инструменты для использования во всех
окружениях JavaScript, ECMAScript 6 определяет только синтаксис и обобща-
ет механизм загрузки как неопределенную внутреннюю операцию с названием
HostResolveImportedModule. Разработчики веб-браузеров и Node.js сами решают,
как реализовать операцию HostResolveImportedModule, чтобы она лучше соответ-
ствовала их окружениям.
загрузка файлов с кодом на JavaScript для его запуска в виде фоновых потоков
выполнения (Web Worker или Service Worker).
Для полноценной поддержки модулей разработчики веб-браузеров должны были
обновить все эти механизмы. Конкретные детали полностью описаны в специфи-
кации HTML, и я кратко опишу их в следующих разделах.
</script>
Первый элемент <script> в этом примере загружает внешний файл модуля с по-
мощью атрибута src. Единственное отличие между загрузкой модуля и сценария
заключается в наличии атрибута type="module". Второй элемент <script> содержит
код модуля, встроенный непосредственно в веб-страницу. Переменная result не
попадет в глобальную область видимости, потому что она объявлена внутри моду-
ля (согласно определению элемента <script>), и поэтому не будет добавлена как
свойство объекта window.
Как видите, подключение модулей к веб-страницам осуществляется очень просто
и напоминает подключение сценариев. Однако сама процедура загрузки модулей
имеет некоторые отличия.
ПРИМЕЧАНИЕ
Возможно, вы обратили внимание, что строка "module" не является определением
типа содержимого, как тип "text/javascript". Файлы с модулями JavaScript имеют
тот же тип содержимого, что и другие файлы с кодом на JavaScript, поэтому их нель
зя отличить по одному только типу содержимого. Кроме того, браузеры игнорируют
элементы <script>, в которых атрибут type имеет недопустимое значение, поэтому
браузеры, не поддерживающие модули, будут автоматически игнорировать строку
<script type="module">, которая обеспечивает хорошую обратную совместимость.
Загрузка модулей 333
ПРИМЕЧАНИЕ
Атрибут defer в элементах <script type="module"> игнорируется, потому что они
и так действуют, как если бы этот атрибут был указан.
Этот код загрузит module.js как модуль, а не как сценарий, потому что во втором
аргументе конструктору передается объект со свойством type, имеющим значение
"module". (Свойство type имитирует атрибут type элемента <script>, позволяющий
отличать модули и сценарии.) Второй аргумент поддерживается всеми механизмами
фоновых потоков выполнения в веб-браузерах.
Модули в фоновых потоках выполняются в основном так же, как сценарии, кроме
пары исключений. Во-первых, сценарии могут загружаться в фоновые потоки только
из того же домена, откуда была загружена веб-страница, создавшая эти потоки, но
к модулям применяется менее строгое ограничение. В отношении модулей, пере-
даваемых непосредственно в фоновые потоки, действует то же самое ограничение,
но они могут загружать файлы, имеющие соответствующие заголовки CORS
(Cross-Origin Resource Sharing — совместное использование ресурсов между раз-
ными источниками). Во-вторых, сценарий, загруженный в фоновый поток, может
использовать метод self.importScripts(), чтобы загрузить дополнительные сце-
нарии, но в модулях вызов self.importScripts() всегда вызывает ошибку, потому
что модули должны для этих целей использовать инструкцию import.
336 Глава 13 • Инкапсуляция кода в модули
В заключение
Спецификация ECMAScript 6 добавила в язык поддержку модулей с целью дать
возможность упаковки и инкапсуляции функциональных возможностей. Модули
действуют иначе, чем сценарии, — их переменные, функции и классы, объявленные
на верхнем уровне, не попадают в глобальную область видимости, а ссылка this
внутри модуля имеет значение undefined. Для достижения такого поведения за-
грузка модулей производится в другом режиме.
Чтобы открыть доступ к любым функциональным возможностям модуля, их не-
обходимо экспортировать. Экспортироваться могут все компоненты — переменные,
функции и классы. Кроме того, каждый модуль может экспортировать что-то одно
как значение по умолчанию. Другие модули могут импортировать все или некоторые
экспортированные имена. Эти имена, однако, интерпретируются, как если бы были
объявлены с помощью инструкции let, и действуют подобно блочным привязкам,
которые не допускают возможности переопределения в том же модуле.
Модули могут ничего не экспортировать, если их цель — выполнить некоторые
манипуляции в глобальной области видимости. Фактически импортирование
функциональных возможностей из таких модулей происходит без создания каких-
либо привязок в области видимости модуля.
Так как модули должны выполняться в специальном режиме, в браузерах была
реализована поддержка версии тега <script type="module">, сообщающей, что ис-
ходный файл или встроенный код должен выполняться как модуль. Тег <script
type="module"> загружает файлы модулей, как если бы имел атрибут defer. Кроме
того, модули выполняются в порядке их перечисления во вмещающем документе
и после того, как документ будет загружен и проанализирован.
Приложение A.
Мелкие изменения
в ECMAScript 6
console.log(Number.isInteger(25)); // true
console.log(Number.isInteger(25.0)); // true
console.log(Number.isInteger(25.1)); // false
Новые приемы работы с целыми числами 339
Здесь нет опечатки, действительно два разных числа в JavaScript имеют одинаковое
целочисленное представление. Эффект становится тем заметнее, чем дальше число
выходит за пределы безопасного диапазона.
В ECMAScript 6 появился метод Number.isSafeInteger(), идентифицирующий
числа, имеющие точное представление. Также были добавлены свойства Number.
MAX_SAFE_INTEGER и Number.MIN_SAFE_INTEGER для представления верхней и нижней
границ безопасного диапазона целых чисел соответственно. Возвращая true, метод
Number.isSafeInteger() гарантирует, что значение является целым числом и по-
падает в безопасный диапазон целочисленных значений, например:
var inside = Number.MAX_SAFE_INTEGER,
outside = inside + 1;
console.log(Number.isInteger(inside)); // true
console.log(Number.isSafeInteger(inside)); // true
console.log(Number.isInteger(outside)); // true
console.log(Number.isSafeInteger(outside)); // false
Число inside — наибольшее целое число в безопасном диапазоне, поэтому для него
методы Number.isInteger() и Number.isSafeInteger() возвращают true. Число
outside — первое сомнительное целочисленное значение; оно не считается безо
пасным, хотя и определяется как целое.
В большинстве случаев в арифметических операциях или операциях сравнения вы
предпочтете использовать только безопасные целые числа, поэтому использование
Number.isSafeInteger() на этапе проверки допустимости ввода может оказаться
отличной идеей.
340 Приложение A. Мелкие изменения в ECMAScript 6
Метод Возвращает
Метод Возвращает
console.log(\u0061); // "abc"
// эквивалентно вызову:
console.log(a); // "abc"
В этом примере после инструкции var для доступа к переменной можно исполь-
зовать идентификатор \u0061 или a. В качестве идентификаторов в ECMAScript 6
допускается также использовать экранированные последовательности кодовых
пунктов Юникода, например:
console.log(\u{61}); // "abc"
// эквивалентно вызову:
console.log(a); // "abc"
let person = {
getGreeting() {
return "Hello";
}
};
let dog = {
getGreeting() {
return "Woof";
}
};
let result = 5 ** 2;
console.log(result); // 25
console.log(result === Math.pow(5, 2)); // true
Этот пример вычисляет выражение 52, результат которого равен 25. При желании
те же вычисления все еще можно выполнять с помощью метода Math.pow().
Порядок операций
Оператор возведения в степень имеет высший приоритет из всех двухместных опе-
раторов в JavaScript (унарные операторы имеют более высокий приоритет, чем **).
Это означает, что он выполняется первым в любом сложном выражении, например:
let result = 2 * 5 ** 2;
console.log(result); // 50
Здесь сначала будет найден результат 52, затем полученное значение умножается
на 2. Конечный результат получится равным 50.
Ограничения операндов
Оператор возведения в степень накладывает некоторые необычные ограничения,
отсутствующие в других операторах. Левый операнд не может быть выражением
с унарным оператором, кроме ++ и --. Например, следующий пример вызовет син-
таксическую ошибку:
// синтаксическая ошибка
let result = -5 ** 2;
// правильно
let result1 = -(5 ** 2); // результат равен -25
// тоже правильно
let result2 = (-5) ** 2; // результат равен 25
let num1 = 2,
num2 = 2;
console.log(++num1 ** 2); // 9
console.log(num1); // 3
console.log(num2-- ** 2); // 4
console.log(num2); // 1
Метод Array.prototype.includes()
Возможно, вы помните, что ECMAScript 6 добавила метод String.prototype.
includes() для проверки вхождения подстроки в строку. Первоначально предпола-
галось, что ECMAScript 6 добавит также метод Array.prototype.includes(), чтобы
обеспечить единообразие функциональных возможностей строк и массивов. Но
определение метода Array.prototype.includes() не было завершено до окончания
работ над ECMAScript 6, поэтому метод Array.prototype.includes() был добавлен
в редакции ECMAScript 2016.
console.log(values.includes(0)); // false
Сравнение значений
Для сравнения значений метод includes() использует оператор ===, но с одним
исключением: NaN считается равным NaN, даже если выражение NaN === NaN воз-
вращает false. Этим метод includes() отличается от метода indexOf(), который
использует оператор === без всяких исключений. Следующий пример демонстри-
рует это отличие:
console.log(values.indexOf(NaN)); // -1
console.log(values.includes(NaN)); // true
Метод values.indexOf() возвращает —1 для NaN, даже если значение NaN присут-
ствует в массиве values. Метод values.includes(), напротив, возвращает true для
NaN, потому что он использует другой способ сравнения значений.
Когда требуется просто проверить присутствие значения в массиве и не нужно
определять его индекс, я советую использовать includes() из-за различий в ин-
терпретации значения NaN между includes() и indexOf(). Если требуется узнать
индекс элемента, в котором хранится искомое значение, используйте метод
indexOf().
Еще одна особенность этой реализации состоит в том, что она считает равными
+0 и —0. В этом случае indexOf() и includes() показывают одинаковое поведение:
console.log(values.indexOf(-0)); // 1
console.log(values.includes(-0)); // true
// синтаксическая ошибка
function notOkay1(first, second=first) {
"use strict";
Изменение области видимости функций в строгом режиме 349
return first;
}
// синтаксическая ошибка
function notOkay2({ first, second }) {
"use strict";
return first;
}