ECMAScript — стандарт для языков написания сценариев на стороне клиента. Первый выпуск спецификации ECMAScript состоялся в 1997 г., а шестой был завершен в 2015 г. Стандарт ECMAScript реализован в нескольких языках, а самой популярной является его реализация в JavaScript. В этом приложении будет рассмотрена JavaScript-реализация ECMAScript 6 (ES6), также известная как ECMAScript 2015.
На момент написания этих строк полная поддержка спецификации ES6 обеспечивалась не всеми браузерами. Чтобы прояснить текущую ситуацию с ее поддержкой, можно зайти на сайт, предоставляющий сведения о совместимости с ECMAScript: /. Но откладывать в долгий ящик разработку в соответствии со спецификацией ES6 вам не придется, поскольку для превращения кода ES6 в код версии ES5, поддерживаемый всеми браузерами, уже сегодня можно воспользоваться таким транспилятором, как Traceur () или Babel ().
ПРИМЕЧАНИЕ
Чтобы протестировать свой код ES6 в грядущих версиях популярных браузеров, загрузите самую последнюю тестовую сборку (nightly build) браузера Firefox с сайта или воспользуйтесь удаленной версией Internet Explorer, находящейся на . Можно также взять с так называемую канареечную сборку (Canary build) браузера Chrome. При этом может понадобиться включить экспериментальные версии функциональных свойств JavaScript, прочитав об этом по адресу chrome://flags (для Chrome) или about://flags (для IE).
Предполагая, что вы уже знакомы с синтаксисом и API ES5, мы избирательно рассмотрим только новые функциональные возможности, появившиеся в ES6. Если вы в программировании на JavaScript немного отстали от новых веяний, то прочитайте интернет-версию приложения книги Enterprise Web Development Якова Файна (Yakov Fain) и др. (O’Reilly, 2014), доступную на GitHub: . В 2016 г. была выпущена спецификация для ES7 (или в ином варианте названия ES 2016). Она невелика по объему. Спецификация языка ECMAScript 2017 опубликована на .
В данном приложении часто будут показываться фрагменты кода на ES5 и их эквиваленты на ES6. Но в ES6 не существует запретов на какой-либо устаревший синтаксис, поэтому в будущих версиях браузеров или в средах автономных JavaScript-интерпретаторов можно будет совершенно свободно запускать устаревший код на ES5 или ES3.
Примеры кода для данного приложения даются в виде простых HTML-файлов, включающих сценарии на ES6, поэтому их можно запускать и отлаживать в инструментарии разработчика вашего браузера при условии, что он полностью поддерживает ES6. При отсутствии такой поддержки у вас есть несколько вариантов.
• Воспользоваться сайтом ES6 Fiddle, позволяющим копировать и вставлять фрагменты кода ES6 в поле, расположенное в левой части окна, нажимать кнопку Play (Запуск) и просматривать информацию, выводимую в консоли в правой части окна (рис. А.1).
Рис. А.1. Применение ES6 Fiddle
• Задействовать транспилятор Traceur или Babel, чтобы преобразовать свой код из ES6 в ES5. Интерактивное средство, позволяющее быстро запускать фрагменты кода, называется Read-Eval-Print-Loop (REPLs). Им можно воспользоваться из Traceur (#) или из Babel (). На рис. A.2 слева в окне можно увидеть код, написанный на ES6, а справа показан его созданный средой ES5-эквивалент. Информация, выводимая в консоли при выполнении кодового примера (если таковая имеется), показана в нижней правой части экрана.
Рис. А.2. Использование Babel REPL
В ES6 введен новый синтаксис для работы со строковыми литералами, которые могут содержать встроенные выражения. Это свойство называется строковой интерполяцией.
В ES5 для создания строки, содержащей строковые литералы, перемешанные со значениями переменных, приходилось использовать объединение:
var customerName = "John Smith";
console.log("Hello" + customerName);
В ES6 литералы шаблонов заключены в символы обратных кавычек. Встраивать выражения в литералы можно путем заключения их в фигурные скобки, предваряемые знаком доллара. В следующем примере кода показана вставка значения переменной customerName в строковый литерал:
var customerName = "John Smith";
console.log(`Hello ${customerName}`);
function getCustomer(){
return "Allan Lou";
}
console.log(`Hello ${getCustomer()}`);
Информация, выводимая эти кодом, будет иметь следующий вид:
Hello John Smith
Hello Allan Lou
В этом примере сначала в литерал шаблона было вставлено значение переменной customerName, а затем в него было включено значение, возвращенное функцией getCustomer(). Внутри фигурных скобок может быть встроено любое допустимое выражение JavaScript.
Строки могут располагаться на нескольких строчках вашего кода. Применяя обратные кавычки, можно записывать строки с переносами, не нуждаясь в их объединении или в использовании символа обратного слеша:
var message = `Please enter a password that
has at least 8 characters and
includes a capital letter`;
console.log(message);
В получающейся строке все пробелы будут рассматриваться в качестве части строки, поэтому на выходе будет показана следующая информация:
Please enter a password that
has at least 8 characters and
includes a capital letter
Если строке шаблона предшествует имя функции, то сначала строка вычисляется, а затем передается функции для дальнейшей обработки. Строковые части шаблона задаются как массив, и все выражения, вычисляемые в шаблоне, передаются в виде отдельных аргументов. Синтаксис имеет несколько необычный вид, поскольку в нем не используются круглые скобки, как это делается при обычных вызовах функций:
mytag`Hello ${name}`;
Посмотрим, как этот синтаксис работает при выводе на стандартное устройство суммы со знаком валюты, который зависит от значения переменной region. Если значение переменной region равно 1, то сумма не изменяется и перед ней ставится знак доллара. Если значением переменной region является 2, то сумму следует преобразовать, применяя в качестве коэффициента обменного курса значение 0.9 и предваряя сумму знаком евро. Строка шаблона выглядит следующим образом:
`You've earned ${region} ${amount}!`
Вызовем тег-функцию currencyAdjustment. Тегированная строка шаблона имеет следующий вид:
currencyAdjustment`You've earned ${region} ${amount}!`
Функция currencyAdjustment получает три аргумента: первый из них представляет все строковые части из строки шаблона, второй представляет регион, а третий предназначается для суммы. После первого аргумента можно добавлять любое количество аргументов. Полноценный пример показан в листинге А.1.
Листинг A.1. Вывод суммы в валюте
Функция currencyAdjustment получает строку со вставленными регионом и суммой и производит синтаксический разбор шаблона, отделяя строковые части от этих значений (пробелы также рассматриваются в качестве строковых частей). Для иллюстрации сначала выведем данные значения. Затем currencyAdjustment проверит регион, применит конвертацию и возвратит новый строковый шаблон. При запуске кода листинга A.1 на выходе будет получена следующая информация:
["You've earned "," ","!"]
2
100
You've earned €90!
Более подробные сведения о тегированных шаблонах можно получить, прочитав главу Template Literals в публикации Exploring ES6, принадлежащей Акселю Раушмайеру (Axel Rauschmayer), доступной на .
В ES6 в качестве параметров (аргументов) функции можно указывать значения по умолчанию, которые станут использоваться, если соответствующее значение не будет предоставлено в ходе вызова функции. Предположим, что создается функция для вычисления налога, получающая два аргумента: годовой доход и штат в составе США, в котором проживает налогоплательщик. Если значение штата state не предоставлено, то нужно использовать значение Florida.
В ES5 тело функции пришлось бы начинать с проверки факта предоставления штата, и, если он не предоставлен, использовать Florida:
function calcTaxES5(income, state){
state = state || "Florida";
console.log("ES5. Calculating tax for the resident of " + state +
" with the income " + income);
}
calcTaxES5(50000);
Этот код выведет следующую информацию:
"ES5. Calculating tax for the resident of Florida with the income 50000"
В ES6 можно указать значение по умолчанию непосредственно в сигнатуре функции:
function calcTaxES6(income, state = "Florida") {
console.log("ES6. Calculating tax for the resident of " + state +
" with the income " + income);
}
calcTaxES6(50000);
На выходе получится такая же информация:
"ES6. Calculating tax for the resident of Florida with the income 50000"
Вместо предоставления для необязательного параметра жестко заданного значения можно даже вызвать функцию, возвращающую нужное значение:
function calcTaxES6(income, state = getDefaultState()) {
console.log("ES6. Calculating tax for the resident of " + state +
" with the income " + income);
}
function getDefaultState(){
return "Florida";
}
Однако нужно иметь в виду: функция getDefaultState() будет вызываться при каждом вызове функции calcTaxES6(), что может негативно отразиться на производительности. Этот новый синтаксис для необязательных параметров позволяет воспользоваться более лаконичным и понятным кодом.
В ES5 используется весьма запутанный механизм области видимости. Независимо от места объявления переменной с помощью ключевого слова var, это объявление перемещается на вершину области видимости. Данный эффект называется поднятием (hoisting). А применение ключевого слова this не всегда толкуется так же однозначно, как в языках Java или C#.
В ES6 за счет введения ключевого слова let эта путаница с поднятием устранена (о чем пойдет речь в следующем подразделе), а от неразберихи, связанной с ключевым словом this, позволяют избавиться функции, обозначаемые в виде стрелок. Рассмотрим проблемы поднятия и использования ключевого this более подробно.
В JavaScript все объявления переменных «всплывают» наверх, при этом блочная область видимости отсутствует. Рассмотрим следующий простой пример: внутри цикла for объявляется переменная i, которая тем не менее используется и за пределами данного цикла.
function foo(){
for(var i=0;i<10;i++){
}
console.log("i=" + i);
}
foo();
При запуске этого кода будет выведено i=10. Переменная i все еще доступна за пределами цикла, несмотря на полное впечатление о том, что она предназначена исключительно для использования внутри него. JavaScript автоматически поднимает объявление переменной наверх.
В данном примере такое поднятие не приносит вреда, поскольку в нем имеется только одна переменная с именем i. Но если две переменные с одним и тем же именем объявлены внутри и за пределами какой-либо функции, то это может привести к запутанному поведению сценария. Рассмотрим код листинга A.2, где переменная customer объявляется в глобальной области видимости. А чуть ниже в локальной области видимости вводится еще одна переменная customer, но пока она закомментирована.
Листинг А.2. Поднятие объявления переменной
<!DOCTYPE html>
<html>
<head>
<title>hoisting.html</title>
</head>
<body>
<script>
"use strict";
var customer = "Joe";
(function (){
console.log("The name of the customer inside the function is " +
customer);
/* if (2 > 1) {
var customer = "Mary";
}*/
})();
console.log("The name of the customer outside the function is " +
customer);
</script>
</body>
</html>
Откройте этот файл в браузере Chrome и посмотрите на информацию, выводимую в консоли на панели Developer Tools (Инструменты разработчика). На рис. А.3 показано, что глобальная переменная customer видна как внутри, так и за пределами функции.
Рис. A.3. Объявление переменной поднято
Уберите знаки комментария, в которые заключена инструкция if, содержащая объявление и инициализацию переменной customer внутри фигурных скобок. Теперь у вас имеются две переменные с одним и тем же именем: одна в глобальной области видимости, а другая — в области видимости функции. Обновите страницу в браузере. Как показано на рис. А.4, информация, выводимая в консоли, изменится: переменная customer, находящаяся в области видимости функции, будет иметь значение undefined.
Рис. A.4. Инициализация переменной не поднята
Причиной такого поведения является то, что в ES5 объявления переменных поднимаются наверх области видимости, а инициализации переменных нет. Поэтому объявление второй инициализированной переменной customer было поднято на верх функции, и инструкция console.log() вывела значение, определенное внутри функции, которое затенило значение глобальной переменной customer.
Объявления функций также поднимаются, поэтому функцию можно вызвать до ее объявления:
doSomething();
function doSomething(){
console.log("I'm doing something");
}
С другой стороны, функциональные выражения считаются инициализациями переменных, поэтому не поднимаются. Следующий фрагмент кода выдаст для переменной doSomething значение undefined:
doSomething();
var doSomething = function(){
console.log("I'm doing something");
}
Теперь посмотрим, что в понятиях области видимости изменилось в ES6.
Объявление переменных с применением имеющегося в ES6 ключевого слова let вместо var позволяет переменным иметь блочную область видимости. Рассмотрим пример (листинг А.3).
Листинг A.3. Переменные с блочной областью видимости
<!DOCTYPE html>
<html>
<head>
<title>let.html</title>
</head>
<body>
<script>
"use strict";
let customer = "Joe";
(function (){
console.log("The name of the customer inside the function is " +
customer);
if (2 > 1) {
let customer = "Mary";
console.log("The name of the customer inside the block is " +
customer);
}
})();
for (let i=0; i<5; i++){
console.log("i=" + i);
}
console.log("i=" + i); //
prints Uncaught ReferenceError: i is not defined
</script>
</body>
</html>
Теперь, как показано на рис. А.5, у двух переменных customer имеются разные области видимости и значения.
Рис. A.5. Блочная область видимости, получаемая с помощью let
Если переменная объявляется с применением let в цикле, то она будет доступна только внутри цикла:
for (let i=0; i<5; i++){
console.log("i=" + i);
}
console.log("i=" + i); // ReferenceError: i is not defined
Тестирование ключевого слова let в Traceur REPL Чтобы получить представление о внешнем виде транспилированного кода, зайдите на веб-страницу Traceur Transcoding Demo (#), позволяющую вводить код в синтаксисе ES6 и преобразовывать (транскодировать) его в код в синтаксисе ES5 в интерактивном режиме. Перенесите код из листинга A.3 в левое текстовое поле, и, как показано на рисунке, в правом текстовом поле появится его ES5-версия.
Транспиляция ES6 в ES5 с использованием Traceur REPL |
Как видите, средство Traceur ввело отдельную переменную customer$_0, чтобы отличить ее от переменной customer. Откройте при работе с Traceur REPL веб-консоль вашего браузера и тут же увидите результаты выполнения вашего кода. |
Проще говоря, при разработке нового приложения вместо ключевого слова var используйте let. Оно позволит вам присваивать и переприсваивать значение переменной столько раз, сколько понадобится.
Если нужно объявить переменную, не способную изменять свое значение, то объявите ее с помощью ключевого слова const. Для констант также поддерживается блочная область видимости.
ПРИМЕЧАНИЕ
Единственное отличие let от const — последнее ключевое слово не позволяет присвоенному значению изменяться. Вам лучше всего использовать в своих программах const; если окажется, что эта переменная нуждается в изменении, то укажите вместо const ключевое слово let.
Если внутри блока (внутри фигурных скобок) объявить функцию, то за пределами блока она будет не видна. При выполнении следующего кода будет выдана ошибка с сообщением doSomething is not defined (doSomething не определена):
{
function doSomething(){
console.log("In doSomething");
}
}
doSomething();
В ES5 объявление doSomething() будет поднято, и при выполнении кода появится сообщение In doSomething. Объявление функции внутри блока при использовании синтаксиса ES5 не рекомендовалось (см. публикацию ES5 Implementation Best Practice на ), поскольку такое действие способно было привести к выдаче разных результатов на различных браузерах, которые могли проводить разбор этого синтаксиса по-разному.
В ES6 введены выражения стрелочных функций, предоставляющих краткую форму записи для безымянных функций и добавляющих лексическую область видимости для переменной this. В некоторых других языках программирования (например, C# и Java) похожий синтаксис называется лямбда-выражениями.
Синтаксис стрелочной функции состоит из аргументов, знака жирной стрелки (=>), и тела функции. Если последнее состоит всего лишь из одного выражения, то вам даже не понадобятся фигурные скобки. При условии, что функция, состоящая из одного выражения, возвращает значение, указывать инструкцию return не нужно — результат возвращается подразумеваемым образом:
let sum = (arg1, arg2) => arg1 + arg2;
Тело выражения стрелочной функции, составленное из нескольких строк, нужно заключать в фигурные скобки и использовать явное указание инструкции return:
(arg1, arg2) => {
// выполнение какой-либо операции
return someResult;
}
При отсутствии у стрелочной функции аргументов используются пустые круглые скобки:
() => {
// выполнение какой-либо операции
return someResult;
}
Если у функции всего лишь один аргумент, то круглые скобки не нужны:
arg1 => {
// выполнение какой-либо операции
}
В следующем фрагменте кода выражение стрелочной функции передается в качестве аргумента методу reduce(), принадлежащему массиву, для вычисления суммы, а также методу filter() для вывода четных чисел:
var myArray = [1,2,3,4,5];
console.log( "The sum of myArray elements is " +
myArray.reduce((a,b) => a+b)); // выводит 15
console.log( "The even numbers in myArray are " +
myArray.filter( value => value % 2 == 0)); // выводит 2 4
Теперь, после ознакомления с синтаксисом стрелочных функций, посмотрим, как они оптимизируют работу с объектом this.
Определить в ES5, на какой из объектов ссылается ключевое слово this, порой становится весьма сложной задачей. Поищите в Интернете информацию по запросу JavaScript this and that и увидите множество статей, в которых люди жалуются на то, что this указывает на «не тот» объект. У ссылки this могут быть различные значения в зависимости от того, как вызвана функция и был ли использован строгий режим — strict mode (см. документацию раздела Strict Mode на ). Сначала мы опишем суть проблемы, а затем пути ее решения, предлагаемые ES6.
Рассмотрим код в файле thisAndThat.html, в котором каждую секунду вызывается функция getQuote()(листинг А.4). Она выводит случайно сгенерированную котировку для символа акции, предоставляемого функцией-конструктором StockQuoteGenerator().
Листинг A.4. Содержимое файла thisAndThat.html
<!DOCTYPE html>
<html>
<head>
<title>thisAndThat.html</title>
</head>
<body>
<script>
function StockQuoteGenerator(symbol){
// this.symbol = symbol;
// внутри getQuote() этот объект не определен
var that = this;
that.symbol = symbol;
setInterval(function getQuote(){
console.log("The price quote for " + that.symbol
+ " is " + Math.random());
}, 1000);
}
var stockQuoteGenerator = new StockQuoteGenerator("IBM");
</script>
</body>
</html>
В закомментированной строке показан неправильный способ использования this, когда значение необходимо в функции, в которой, казалось бы, имеется такая же ссылка this, но это не так. Если значение переменной this не было сохранено в that, то значение this.symbol в функции getQuote(), вызванной в функции setInterval() в качестве функции обратного вызова, будет не определено. В функции getQuote() ключевое слово this указывает на глобальный объект, отличающийся от того объекта, на который указывает то же ключевое слово, определенное функцией-конструктором StockQuoteGenerator().
Другим возможным решением, гарантирующим выполнение функции в конкретном объекте this, является применение функций JavaScript call(), apply() или bind().
ПРИМЕЧАНИЕ
Если проблема, связанная с использованием ключевого слова this в языке программирования JavaScript, вам не знакома, то прочитайте статью Ричарда Бовелла (Richard Bovell) Understand JavaScript’s 'this' with Clarity and Master It (/).
В файле fatArrow.html (листинг А.5) показано решение с использованием стрелочной функции, исключающее необходимость сохранять this в that, как это делалось в коде файла thisAndThat.html.
Листинг A.5. Содержимое файла fatArrow.html
<!DOCTYPE html>
<html>
<head>
<title>fatArrow.html</title>
</head>
<body>
<script>
"use strict";
function StockQuoteGenerator(symbol){
this.symbol = symbol;
setInterval(() => {
console.log("The price quote for " + this.symbol
+ " is " + Math.random());
}, 1000);
}
var stockQuoteGenerator = new StockQuoteGenerator("IBM");
</script>
</body>
</html>
Стрелочная функция, предоставляемая функции setInterval() в качестве аргумента, использует значение this окружающего контекста, поэтому распознает IBM в качестве значения выражения this.symbol.
В ES5 написание функции с переменным числом параметров требует использования специального объекта arguments. Этот объект похож на массив и содержит значения, соответствующие аргументам, передаваемым функции. Подразумеваемая переменная arguments может рассматриваться в любой функции в качестве локальной переменной.
В ES6 имеются операторы предоставления остальных аргументов rest и разложения массива на элементы spread, и оба они представлены многоточием (…). Оператор rest служит для передачи функции переменного числа аргументов и должен быть последним в списке аргументов. Если название аргумента функции начинается с многоточия, то все остальные аргументы будут получены в массиве.
Например, функции можно передать нескольких клиентов, используя только одно имя переменной с помощью оператора rest:
function processCustomers(...customers){
// здесь находится реализация функции
}
В этой функции могут обрабатываться данные customers аналогично обработке любого массива. Предположим, что нужно создать функцию для вычисления налогов, вызываемую с первым аргументом, income, за которым следует любое количество аргументов, представляющих имена клиентов. В листинге A.6 показано, как можно обработать переменное количество аргументов с помощью сначала старого, а затем нового синтаксиса. В функции calcTaxES5() используется объект по имени arguments, а в функции calcTaxES6() — имеющийся в ES6 оператор rest.
Листинг A.6. Содержимое файла rest.html
Обе функции, и calcTaxES5(), и calcTaxES6(), выдают одинаковые результаты:
ES5. Calculating tax for customers with the income 50000
Processing Smith
Processing Johnson
Processing McDonald
ES5. Calculating tax for customers with the income 750000
Processing Olson
Processing Clinton
ES6. Calculating tax for customers with the income 50000
Processing Smith
Processing Johnson
Processing McDonald
ES6. Calculating tax for customers with the income 750000
Processing Olson
Processing Clinton
Но обработка клиентов различается. Поскольку объект arguments не является настоящим массивом, в версии ES5 приходится создавать массив, задействуя методы slice() и call() для извлечения имен клиентов, начинающихся со второго элемента в arguments. В версии ES6 применять такие приемы не нужно, поскольку оператор rest предоставляет обычный массив клиентов. Использование остальных аргументов упрощает код и облегчает его чтение.
Если оператор rest может превратить переменное число параметров в массив, то оператор spread способен сделать обратное: превратить массив в список аргументов. Предположим, что нужно создать функцию, которая будет вычислять налог для трех клиентов с заданным доходом. На этот раз число аргументов фиксировано, но клиенты находятся в массиве. Чтобы превратить его в список отдельных аргументов, можно воспользоваться оператором spread, обозначаемым многоточием (…) (листинг А.7).
Листинг A.7. Содержимое файла spread.html
В данном примере вместо извлечения значений из массива customers с последующим предоставлением этих значений в качестве аргументов функции используется массив с оператором spread, как будто функции говорят: «Тебе нужны три аргумента, но я даю тебе массив. Разбей его на элементы». Обратите внимание: в отличие от оператора rest оператор spread не обязан быть последним в списке аргументов.
Когда браузер выполняет обычную функцию JavaScript, он непрерывно запускает череду команд, принадлежащих функции, пока эти команды не закончатся. А выполнение функции-генератора может быть приостановлено и возобновлено множество раз. Функция-генератор может уступать управление вызывающему сценарию, запускаемому в том же самом потоке. Как только при выполнении кода в функции-генераторе встретится ключевое слово yield, данное выполнение приостанавливается, и вызывающий сценарий может возобновить его, вызвав в отношении генератора метод next().
Чтобы превратить обычную функцию в генератор, нужно поставить между ключевым словом function и именем функции звездочку. Рассмотрим пример:
function* doSomething(){
console.log("Started processing");
yield;
console.log("Resumed processing");
}
При вызове этой функции ее код не выполняется сразу же, а возвращается специальный объект Generator, который служит в качестве итератора. Следующая строка кода не приведет к выводу на экран какой-либо информации:
var iterator = doSomething();
Чтобы запустить выполнение тела функции, нужно вызвать в отношении генератора метод next():
iterator.next();
После этой строки кода функция doSomething() выведет строку Started processing и приостановится, поскольку встретит оператор yield. Повторный вызов метода next() приведет к выводу строки Resumed processing.
Функции-генераторы используются, когда нужно создать функцию, выдающую поток данных. Предположим, нужна функция для извлечения и выдачи цен акций для указанного символа (IBM, MSFT и т.д.). Если цена акции падает ниже указанного значения (предельной цены), то вам нужно купить эту акцию.
Данный сценарий имитирует следующая функция-генератор: getStockPrice() (листинг А.8). Чтобы ничего не усложнять, она не извлекает цены из фондовой биржи, создавая вместо этого случайные числовые значения с помощью метода Math.random().
Листинг A.8. Содержимое функции getStockPrice()
function* getStockPrice(symbol){
while(true){
yield Math.random()*100;
console.log(`resuming for ${symbol}`);
}
}
Если после оператора yield имеется значение, то оно возвращается вызвавшему функцию коду, но на этом выполнение функции не завершается. Даже если в getStockPrice() имеется бесконечный цикл, то цена будет выдана (возвращена), только когда сценарий, вызвавший getStockPrice(), вызывает в отношении данного генератора метод next(), как в следующем коде (листинг А.9).
Листинг A.9. Вызов getStockPrice()
Запуск кода листинга A.9 выведет в консоли браузера некую информацию, похожую на представленную ниже:
The generator returned 61.63144460879266
resuming for IBM
The generator returned 96.60782956052572
resuming for IBM
The generator returned 31.163037824444473
resuming for IBM
The generator returned 18.416578718461096
resuming for IBM
The generator returned 55.80756475683302
resuming for IBM
The generator returned 14.203652134165168
buying at 14.203652134165168 !!!
Обратите внимание на порядок следования сообщений. Когда в отношении priceGenerator вызывается метод next(), выполнение приостановленного метода getStockPrice() возобновляется со строки кода, расположенной ниже оператора yield, которая выводит resuming for IBM. Если даже поток управления покинет функцию, а затем вернется, то getStockPrice() будет помнить, что значением символа была строка IBM. Когда оператор yield возвращает управление за пределы сценария, он создает снимок стека; это позволяет ему запомнить все значения локальных переменных. Когда выполнение функции-генератора возобновится, данные значения не будут утрачены.
С помощью генераторов можно отделить реализацию конкретных операций (например, получение ценового предложения) от потребления данных, произведенных этими операциями. Потребитель данных в ленивом режиме вычисляет результаты и принимает решение, нужно ли запрашивать дополнительные данные.
Создание экземпляров объектов означает выстраивание их в памяти. Деструктурирование есть разделение объектов. В ES5 любой объект или коллекцию можно разобрать, создав занимающуюся этим функцию. В ES6 введен синтаксис деструктурирующего присваивания, позволяющий извлекать данные из свойств объекта или массива в простом выражении с указанием шаблона соответствия.
Деструктурирующее выражение состоит из шаблона соответствия, знака равенства и объекта или массива, подлежащего разделению. Проще всего все объяснить на примере, что мы и сделаем.
Предположим, что функция getStock() возвращает объект Stock, имеющий атрибуты symbol и price. В ES5 при желании присвоить значения этих атрибутов отдельным переменным пришлось бы сначала вводить переменную для хранения объекта Stock, а затем создавать две инструкции присваивания атрибутов объекта соответствующим переменным:
var stock = getStock();
var symbol = stock.symbol;
var price = stock.price;
В ES6 нужно лишь создать шаблон соответствия в левой части выражения и присвоить ему объект Stock:
let {symbol, price} = getStock();
Видеть фигурные скобки слева от знака присваивания несколько необычно, но это часть синтаксиса выражения соответствия. Когда вы видите фигурные скобки слева, думайте о них как о блоке кода, а не как об объектном литерале.
В следующем сценарии (листинг А.10) показывается получение объекта Stock из функции getStock() и его деструктурирование в две переменные.
Листинг A.10. Деструктурирование объекта
function getStock(){
return {
symbol: "IBM",
price: 100.00
};
}
let {symbol, price} = getStock();
console.log(`The price of ${symbol} is ${price}`);
При запуске этого сценария будет выведена следующая информация:
The price of IBM is 100
Иными словами, в одном выражении присваивания получается привязка комплекта данных (в нашем случае атрибутов объекта) к набору переменных (symbol и price). Даже если у объекта Stock имеется больше двух атрибутов, это выражение деструктурирования все равно продолжит работать, поскольку symbol и price будут соответствовать шаблону. В выражении соответствия перечисляются только те переменные для атрибутов объекта, к которым проявляется интерес.
Код в листинге A.10 работает по причине совпадения имен переменных с именами атрибутов объекта Stock. Заменим имя symbol именем sym:
let {sym, price} = getStock();
Теперь выводимая информация изменится, поскольку JavaScript не знает, что значение принадлежащего объекту атрибута symbol должно быть присвоено переменной sym:
The price of undefined is 100
Это пример неверного шаблона соответствия. Если действительно нужно отобразить переменную по имени sym на атрибут symbol, то введите для имени symbol псевдоним:
let {symbol: sym, price} = getStock();
Если предоставить в левой части переменных больше, чем атрибутов у объекта, то лишние переменные получат значение undefined. При добавлении слева переменной stockExchange она будет проинициализирована значением undefined, поскольку в объекте, возвращенном методом getStock(), атрибут с таким именем отсутствует:
let {sym, price, stockExchange} = getStock();
console.log(`The price of ${symbol} is ${price} ${stockExchange}`);
В случае применения предыдущего деструктурирующего присваивания к тому же объекту Stock вывод на консоль будет иметь следующий вид:
The price of IBM is 100 undefined
Если нужно, чтобы у переменной stockExchange было значение по умолчанию, например, NASDAQ, то можно переписать деструктурирующее выражение следующим образом:
let {sym, price, stockExchange="NASDAQ"} = getStock();
Можно также выполнить деструктурирование вложенных объектов. В листинге A.11 создается вложенный объект, представляющий акцию Microsoft и передаваемый функции printStockInfo(), которая извлекает из этого объекта символ акции и имя фондовой биржи.
Листинг A.11. Деструктурирование вложенного объекта
let msft = {symbol: "MSFT",
lastPrice: 50.00,
exchange: {
name: "NASDAQ",
tradingHours: "9:30am-4pm"
}
};
function printStockInfo(stock){
let {symbol, exchange:{name}} = stock;
console.log(`The ${symbol} stock is traded at ${name}`);
}
printStockInfo(msft);
При запуске этого сценария будет выведена следующая информация:
The MSFT stock is traded at NASDAQ
Работает во многом похоже на деструктурирование объектов, но вместо фигурных скобок используются квадратные. При деструктурировании объектов необходимо указывать переменные, соответствующие атрибутам, а при работе с массивами нужно определять переменные, соответствующие индексам. Следующий код извлекает значения двух элементов массива в две переменные:
let [name1, name2] = ["Smith", "Clinton"];
console.log(`name1 = ${name1}, name2 = ${name2}`);
Выводимая информация выглядит следующим образом:
name1 = Smith, name2 = Clinton
При необходимости извлечь только второй элемент этого массива шаблон соответствия имеет следующий вид:
let [, name2] = ["Smith", "Clinton"];
Если функция возвращает массив, то применение синтаксиса деструктурирования превращает ее в функцию с несколькими возвращаемыми значениями, как показано в функции getCustomers():
function getCustomers(){
return ["Smith", , , "Gonzales"];
}
let [firstCustomer,,,lastCustomer] = getCustomers();
console.log(`The first customer is ${firstCustomer} and the last one is
${lastCustomer}`);
А теперь объединим деструктурирование массива с остальными параметрами. Предположим, что имеется массив, состоящий из имен нескольких клиентов, но нужно обработать только первые два. Как это делается, показано в следующем фрагменте кода:
let customers = ["Smith", "Clinton", "Lou", "Gonzales"];
let [firstCust, secondCust, ...otherCust] = customers;
console.log(`The first customer is ${firstCust} and the second one is
${secondCust}`);
console.log(`Other customers are ${otherCust}`);
Вывод в консоли, выполненный данным кодом, имеет следующий вид:
The first customer is Smith and the second one is Clinton
Other customers are Lou,Gonzales
Аналогичным образом, шаблон соответствия с параметром rest можно передать функции:
var customers = ["Smith", "Clinton", "Lou", "Gonzales"];
function processFirstTwoCustomers([firstCust, secondCust, ...otherCust]) {
console.log(`The first customer is ${firstCust} and the second one is
${secondCust}`);
console.log(`Other customers are ${otherCust}`);
}
processFirstTwoCustomers(customers);
Информация, выведенная в консоли, будет такой же:
The first customer is Smith and the second one is Clinton
Other customers are Lou,Gonzales
Подводя черту, можно отметить: преимущества деструктурирования заключаются в возможности писать меньше кода, когда необходимо инициализировать ряд переменных теми данными, которые находятся в свойствах объектов или в массивах.
Циклический обход элементов коллекции объектов может быть выполнен с использованием различных ключевых слов и API JavaScript. В этом разделе будет показано применение нового цикла for-of. Мы сравним его с циклом for-in и методом forEach().
Рассмотрим следующий код, выполняющий обход элементов массива, состоящего из четырех чисел. У этого массива также есть дополнительное свойство description, которое игнорируется методом forEach():
var numbersArray = [1, 2, 3, 4];
numbersArray.description = "four numbers";
numbersArray.forEach((n) => console.log(n));
Информация, выводимая сценарием, имеет следующий вид:
1
2
3
4
Метод forEach() получает в качестве аргумента функцию и исправно выводит четыре числа из массива, игнорируя свойство description. Еще одно ограничение метода forEach() заключается в том, что он не позволяет прервать цикл преждевременно. Вместо forEach() приходится использовать метод every() или придумывать какие-либо другие ухищрения. Посмотрим, как здесь может помочь цикл for-in.
Данный цикл позволяет совершить обход имен свойств объектов и коллекций данных. В JavaScript любой объект является коллекцией пар «ключ — значение», где ключ представлен именем свойства, а значение — значением свойства. У массива имеется пять свойств: четыре для чисел и description. Выполним обход свойств этого массива:
var numbersArray = [1, 2, 3, 4];
numbersArray.description = "four numbers";
for (let n in numbersArray) {
console.log(n);
}
Предыдущий код выведет следующую информацию:
0
1
2
3
description
Запуск данного кода в отладчике показывает: каждое из этих свойств является строкой. Чтобы посмотреть фактические значения свойств, вывод элементов массива нужно выполнить с помощью записи numbersArray[n]:
var numbersArray = [1, 2, 3, 4];
numbersArray.description = "four numbers";
for (let n in numbersArray) {
console.log(numbersArray[n]);
}
Теперь вывод выглядит следующим образом:
1
2
3
4
four numbers
Как видите, цикл for-in позволяет обойти все свойства, а не только данные, в которых может быть не то, что вам нужно. А теперь применим новый синтаксис for-of.
В ES6 введен новый цикл for-of, позволяющий проводить итерацию данных независимо от того, какие еще свойства имеются в коллекции данных. При необходимости этот цикл можно прервать, задействуя ключевое слово break:
var numbersArray = [1, 2, 3, 4];
numbersArray.description = "four numbers";
console.log("Running for of for the entire array");
for (let n of numbersArray) {
console.log(n);
}
console.log("Running for of with a break");
for (let n of numbersArray) {
if (n >2) break;
console.log(n);
}
Этот сценарий выведет следующую информацию:
Running for of for the entire array
1
2
3
4
Running for of with a break
1
2
Цикл for-of работает с любым итерируемым объектом, включая Array, Map, Set и др. Строки также являются итерируемыми объектами. Следующий код посимвольно выводит содержимое строки John:
for (let char of "John") {
console.log(char);
}
Как в ES3, так и в ES5 поддерживается объектно-ориентированное программирование и наследование. Но с помощью классов ES6 писать и читать код существенно проще.
В ES5 объекты могут создаваться либо с самого начала, либо путем наследования из других объектов. Изначально все объекты JavaScript наследуются от объекта Object. Данное наследование реализуется благодаря специальному свойству prototype, которое указывает на предка того или иного объекта. Это называется прототипным наследованием. Например, чтобы создать объект NJTax, являющийся наследником объекта Tax, можно написать следующий код:
В ES6 введены ключевые слова class и extends, чтобы поставить синтаксис в один ряд с синтаксисом других объектно-ориентированных языков, таких как Java и C#. В ES6 аналог предыдущего кода выглядит следующим образом:
class Tax {
// Здесь находится код класса tax}
class NJTax extends Tax {
// Здесь находится код объекта New Jersey tax
}
var njTax = new NJTax();
Класс Tax — класс-предок, или суперкласс, а NJTax — потомок, или подкласс. Можно также сказать, что класс NJTax имеет с классом Tax отношения типа is a («является»). Иными словами, NJTax является классом Tax. В NJTax можно реализовать дополнительные функциональные возможности, но NJTax по-прежнему «является» (is a) классом Tax или относится к его разновидности (is a kind of). Аналогично этому, если создать класс Employee, являющийся наследником класса Person, то можно сказать, что Employee — это Person.
Можно создать один или несколько экземпляров объектов:
ПРИМЕЧАНИЕ
Объявления классов поднятию не подвергаются. Сначала нужно объявить класс и только потом работать с ним.
У каждого из этих объектов будут свойства и методы, имеющиеся в классе Tax, но у них будет другое состояние. Например, первый экземпляр может быть создан для клиента с годовым доходом $50 000, а второй — для клиента, заработавшего за год $75 000. Каждый экземпляр будет совместно использовать одни и те же копии методов, объявленных в классе Tax, исключая тем самым дублирование кода.
В ES5 можно также избежать дублирования кода, объявляя методы не внутри объектов, а в их прототипах:
function Tax() {
// Здесь находится код объекта tax
}
Tax.prototype = {
calcTax: function() {
// Здесь находится код для вычисления налога (tax)
}
}
JavaScript остается языком с прототипным наследованием, но ES6 позволяет создавать более элегантный код:
class Tax(){
calcTax(){
// Здесь находится код для вычисления налога (tax)
}
}
Поддержка переменных элементов класса отсутствует Синтаксис ES6 не позволяет объявлять переменные элементов класса, как в Java, C# или TypeScript. Следующий синтаксис не поддерживается: class Tax { var income; } |
В ходе создания экземпляров в классах выполняется код, помещенный в специальные методы под названием конструкторы. В таких языках, как Java и C#, у конструктора должно быть имя, совпадающее с именем класса, но в ES6 конструктор класса указывается с помощью ключевого слова constructor:
class Tax{
constructor (income){
this.income = income;
}
}
var myTax = new Tax(50000);
Метод constructor имеет специализированный характер и выполняется только один раз при создании объекта. Если вам знаком синтаксис Java или C#, то предыдущий код может показаться несколько странным: в нем нет объявления отдельной переменной income на уровне класса, она создается в динамическом режиме в отношении объекта this, при этом this.income инициализируется значениями аргумента конструктора. Переменная this указывает на экземпляр текущего объекта.
В следующем примере показывается порядок создания экземпляра подкласса NJTax, где его конструктору предоставляется значение дохода, равное 50 000:
class Tax{
constructor(income){
this.income = income;
}
}
class NJTax extends Tax{
// Здесь находится код объекта New Jersey tax
}
var njTax = new NJTax(50000);
console.log(`The income in njTax instance is ${njTax.income}`);
Вывод этого фрагмента кода выглядит следующим образом:
The income in njTax instance is 50000
Поскольку в подклассе NJTax собственный конструктор не определяется, при создании экземпляра NJTax происходит автоматический вызов конструктора из родительского класса Tax. Если в подклассе определен собственный конструктор, то этого не произойдет. Соответствующий пример будет показан в подразделе A.7.4.
Обратите внимание: у вас есть возможность доступа к значению income за пределами класса через ссылочную переменную njTax. А можно ли скрыть income за пределами объекта? Этот вопрос мы рассмотрим в разделе A.9.
Если вам нужно свойство класса, совместно используемое множеством экземпляров объекта, то необходимо создать его за пределами объявления класса. В следующем примере переменная counter совместно применяется обоими экземплярами объекта A:
class A{
}
A.counter = 0;
var a1 = new A();
A.counter++;
console.log(A.counter);
var a2 = new A();
A.counter++;
console.log(A.counter);
Этот код выводит следующий результат:
1
2
Синтаксис для объектов геттеров и сеттеров не является новшеством ES6, но изучим его, прежде чем перейти к новому синтаксису определения методов. Сеттеры и геттеры привязывают функции к свойствам объекта. Рассмотрим объявление и использование литерала объекта Tax:
var Tax = {
taxableIncome:0,
get income() {return this.taxableIncome;},
set income(value){ this.taxableIncome=value}
};
Tax.income=50000;
console.log("Income: " + Tax.income); // выводит Income: 50000
Обратите внимание: присваивание и извлечение значения income выполняется с применением системы записи, использующей точку, точно так же, как при объявлении свойства объекта Tax.
В ES5 приходится применять ключевое слово function, например, calculateTax = function(){…}. ES6 позволяет в любом определении метода опустить ключевое слово function:
var Tax = {
taxableIncome:0,
get income() {return this.taxableIncome;},
set income(value){ this.taxableIncome=value},
calculateTax(){ return this.taxableIncome*0.13}
};
Tax.income=50000;
console.log(`For the income ${Tax.income} your tax is ${Tax.calculateTax()}`);
Ниже показана информация, выводимая этим кодом:
For the income 50000 your tax is 6500
Геттеры и сеттеры предлагают удобный синтаксис для работы со свойствами. Например, если будет решено добавить к геттеру income какой-нибудь проверочный код, то изменять сценарии, использующие форму записи Tax.income, не придется. Плохо только то, что в ES6 в классах не поддерживаются закрытые переменные, поэтому ничто не препятствует программисту получить доступ к переменной, применяемой в геттере или сеттере (например, к taxableIncome) напрямую. О сокрытии (инкапсуляции) переменных разговор пойдет в разделе A.9.
Функция super() позволяет подклассу (потомку) вызывать конструктор из родительского класса (предка). Ключевое слово super служит для вызова метода, определенного в родительском классе. Использование функции super() и ключевого слова super проиллюстрировано в листинге A.12. У класса Tax имеется метод calculateFederalTax(), а в его подклассе NJTax добавляется метод calculateStateTax(). У обоих этих классов имеются свои собственные версии метода calcMinTax().
Листинг A.12. Использование функции super() и ключевого слова super
"use strict";
class Tax{
constructor(income){
this.income = income;
}
calculateFederalTax(){
console.log(`Calculating federal tax for income ${this.income}`);
}
calcMinTax(){
console.log("In Tax. Calculating min tax");
return 123;
}
}
class NJTax extends Tax{
constructor(income, stateTaxPercent){
super(income);
this.stateTaxPercent=stateTaxPercent;
}
calculateStateTax(){
console.log(`Calculating state tax for income ${this.income}`);
}
calcMinTax(){
super.calcMinTax();
console.log("In NJTax. Adjusting min tax");
}
}
var theTax = new NJTax(50000, 6);
theTax.calculateFederalTax();
theTax.calculateStateTax();
theTax.calcMinTax();
Запуск этого кода приведет к выводу следующей информации:
Calculating federal tax for income 50000
Calculating state tax for income 50000
In Tax. Calculating min tax
In NJTax. Adjusting min tax
У класса NJTax имеется свой, определенный явным образом конструктор с двумя аргументами, income и stateTaxPercent, предоставляемыми при создании экземпляра NJTax. Чтобы гарантировать вызов конструктора Tax (который устанавливает в объекте атрибут income), из конструктора подкласса явным образом вызывается super("50000");. Без данной строки кода фрагмент программы, показанный в листинге A.12, выдаст сообщение об ошибке и, даже если он этого не сделает, код в Tax не будет видеть значение income.
Если нужно вызвать конструктор родительского класса, то это нужно сделать в конструкторе подкласса путем вызова функции super(). Еще один способ вызова кода в родительском классе заключается в использовании ключевого слова super. Метод calcMinTax() имеется как в Tax, так и в NJTax. Метод, определенный в родительском классе Tax, вычисляет базовую минимальную сумму в соответствии с федеральными налоговыми законами, а версия данного метода из подкласса применяет базовое значение и проводит его уточнение. У обоих методов есть одинаковая сигнатура, поэтому имеет место переопределение метода.
С помощью вызова super.calcMinTax() гарантируется, что для вычисления налога штата берется в расчет базовый федеральный налог. Если не вызвать super.calcMinTax(), то на первый план выйдет переопределение метода и применена будет версия метода calcMinTax(), определенная в подклассе. Такое переопределение метода часто используется для замены его функциональных возможностей, определяемых в родительском классе, без изменения его кода.
Предостережение, касающееся классов и наследования Классы в ES6 являются всего лишь синтаксической уловкой, улучшающей читаемость кода. «За кулисами» JavaScript по-прежнему использует прототипное наследование, позволяющее в динамическом режиме заменять предка в ходе выполнения сценария, а у класса может быть только один предок. Постарайтесь избегать создания глубоких иерархий наследования, поскольку они снижают гибкость вашего кода и усложняют его реструктуризацию в случае необходимости. Хотя применение ключевого слова super или функции super() позволяет вызывать код предка, нужно избегать их использования, поскольку они приводят к жесткому связыванию между объектом-потомком и объектом-предком. Чем меньше потомок знает о своем предке, тем лучше. Если предок объекта изменится, то у его новой версии может не оказаться того метода, попытка вызова которого предпринимается с помощью функции super(). |
Для организации асинхронной обработки в предыдущих реализациях ECMAScript приходилось использовать функции обратного вызова (callbacks), представляющие собой функции, передаваемые в качестве аргументов другим функциям для вызова. Функции обратного вызова могут вызываться в синхронном или в асинхронном режиме.
В разделе A.6 функция обратного вызова передавалась функции forEach() для синхронного вызова. При создании Ajax-запросов к серверу функция обратного вызова передается для асинхронного вызова, когда результаты поступают от сервера.
Рассмотрим пример получения данных с сервера о заказанных товарах. Все начинается с асинхронного обращения к серверу для получения информации о клиентах, а затем для каждого клиента нужно сделать еще один вызов, чтобы получить заказы. Для каждого из них нужно получить товары. При завершающем вызове будут получены подробные описания товаров.
При асинхронной обработке неизвестно, когда каждая из этих операций завершится, так что нужно создавать функции обратного вызова, которые вызываются после окончания работы их предшественников. Чтобы имитировать задержки, как будто на завершение каждой операции уходит одна секунда, воспользуемся функцией setTimeout() (листинг А.13).
Листинг A.13. Вложенные функции обратного вызова
function getProductDetails() {
setTimeout(function () {
console.log('Getting customers');
setTimeout(function () {
console.log('Getting orders');
setTimeout(function () {
console.log('Getting products');
setTimeout(function () {
console.log('Getting product details')
}, 1000);
}, 1000);
}, 1000);
}, 1000);
};
getProductDetails();
При запуске этого кода с односекундными задержками будут выведены следующие сообщения:
Getting customers
Getting orders
Getting products
Getting product details
Уровень вложенности, показанный в листинге A.13, уже затрудняет чтение кода. А теперь представим, что к этому добавляются логика приложения и обработка ошибок. Написание кода подобным образом зачастую называют адом обратного вызова или гибельным треугольником (пустые пространства в коде принимают форму треугольника).
В ES6 введены промисы, позволяющие избавиться от этой вложенности и улучшить читаемость кода, обеспечивая наличие тех же функциональных возможностей, что и у функций обратного вызова. Объект Promise ожидает и отслеживает результат асинхронной операции и позволяет узнать о ее успешном или аварийном завершении, чтобы у вас появилась возможность предпринять соответствующие последующие шаги. Объект Promise представляет будущий результат операции и может находиться в одном из следующих состояний:
• выполнен — операция успешно завершилась;
• отклонен — операция дала сбой и возвратила ошибку;
• в ожидании — операция находится в работе, не будучи ни выполненной, ни отклоненной.
Экземпляр объекта Promise создается путем предоставления его конструктору двух функций для вызова в двух случаях: при выполнении операции и при ее отклонении. Рассмотрим сценарий, использующий функцию getCustomers() (листинг А.14).
Листинг A.14. Использование промиса
Функция getCustomers() возвращает объект Promise, экземпляр которого создается с функцией, имеющей в качестве аргументов конструктора resolve и reject. В коде resolve() вызывается, если будет получена информация о клиенте. В целях упрощения функция setTimeout() имитирует асинхронный вызов, длящийся одну секунду. Кроме того, жестко задается значение true для флага success. В настоящем сценарии можно выдать запрос с использованием объекта XMLHttpRequest и вызвать resolve() в случае успешного возвращения результата или reject() при возникновении ошибки.
В нижней части листинга A.14 к экземпляру Promise() прикрепляются методы then() и catch(). Из этих двух методов будет вызван только один. Вызов внутри функции resolve("John Smith") приводит к вызову метода then(), получающего в качестве аргумента John Smith. Если заменить значение success на false, то будет вызван метод catch() с аргументом Can't get customers.
Запуск кода листинга A.14 приведет к выводу в консоли следующих сообщений:
Getting customers
Invoked getCustomers. Waiting for results
John Smith
Обратите внимание: сообщение Invoked getCustomers. Waiting for results выводится перед John Smith. Тем самым подтверждается, что функция getCustomers() работает асинхронно.
Каждый промис соответствует одной асинхронной операции, и для обеспечения конкретного порядка выполнения промиса можно выстраивать в цепочку. Добавим функцию getOrders(), которая находит заказы, принадлежащие указанному клиенту, и составляет цепочку с функцией getCustomers() (листинг А.15).
Листинг A.15. Выстраивание промисов в цепочку
В этом коде не только объявляются и выстраиваются в цепочку две функции, но и демонстрируется способ вывода в консоли промежуточных результатов. Далее показан вывод, выполняемый кодом листинга A.15 (обратите внимание: клиент, возвращенный из функции getCustomers(), был должным образом передан функции getOrders()):
Getting customers
Chained getCustomers and getOrders. Waiting for results
John Smith
Found the order 123 for John Smith
Используя функцию then(), можно выстраивать в цепочку сразу несколько функций и при этом иметь только один сценарий обработки ошибок для всех цепочных вызовов. При возникновении ошибки она распространится по всей цепочке then, пока не найдет обработчик ошибки. После того как произойдет ошибка, никакие then уже вызываться не будут.
Если в листинге А.15 изменить значение переменной success на false, то выведется сообщение Can’t get customers и метод getOrders() вызван не будет. При перемещении всех этих выводов в консоли код, извлекающий клиентов и заказы, станет выглядеть более чистым и понятным:
getCustomers()
.then((cust) => getOrders(cust))
.catch((err) => console.error(err));
Добавление then не снижает читаемость этого кода (сравните его с гибельным треугольником из листинга A.13).
Еще один рассматриваемый вопрос касается асинхронных функций, не зависящих друг от друга. Предположим, нужно вызвать две функции, не придерживаясь какого-либо определенного порядка, но при этом следует совершить некое действие только после того, как обе функции завершат работу. У объекта Promise имеется метод all(), получающий коллекцию промисов, которая допускает последовательный обход элементов и выполняет (разрешает) все эти промисы. Поскольку метод all() возвращает объект Promise, к результату можно добавить then() или catch() (или оба метода).
Посмотрим, что получится, если воспользоваться all() с функциями getCustomers() и getOrders():
Promise.all([getCustomers(), getOrders()])
.then((order) => console.log(order));
При выполнении этого кода будет выведена следующая информация:
Getting customers
Getting orders for undefined
["John Smith","Order 123"]
Обратите внимание на сообщение Getting orders for undefined. Оно появилось из-за беспорядочности разрешения промисов, по причине которой функция getOrders() не получила клиента в качестве аргумента. Конечно, для данного сценария использование Promise.all() вряд ли подходит, но бывают и более подходящие для этого ситуации. Представим веб-портал, которому необходимо выполнить несколько асинхронных вызовов, чтобы получить сводку погоды, новости рынка акций и информацию об интенсивности движения транспорта. Если нужно, чтобы страница портала отображалась по завершении всех этих вызовов, то лучшего средства, чем Promise.all(), не найти:
Promise.all([getWeather(), getStockMarketNews(), getTraffic()])
.then(renderGUI);
По сравнению с функциями обратного вызова промисы могут сделать код более линейным и легче читаемым и соответствовать сразу нескольким состояниям приложения. К их недостаткам можно отнести невозможность их прекратить. Представим нетерпеливого пользователя, несколько раз нажимающего кнопку, чтобы получить данные от сервера. При каждом щелчке создается промис и инициируется HTTP-запрос. Способа сохранить только последний запрос и отменить незавершенные запросы не существует. Следующим этапом эволюции объекта Promise является объект Observable, который может появиться в будущих спецификациях ECMAScript; как он может использоваться уже сейчас, описано в главе 5.
ПРИМЕЧАНИЕ
Вскоре для получения ресурсов по сети вместо объекта XMLHttpRequest можно будет воспользоваться API Fetch. Он основан на применении промисов, а более подробные сведения о нем можно получить, изучив документацию Mozilla Developer Network ().
В любом языке программирования разбиение кода на модули помогает выстроить приложение из логически независимых и по возможности многократно используемых модулей. Модульные приложения позволяют более эффективно распределять программируемые задачи между разработчиками программного кода. Разработчики должны решить, какие API должны встречаться в модулях для внешнего применения, а какие — для внутреннего.
В ES5 нет языковых конструкций для создания модулей, поэтому приходится прибегать к одному из следующих вариантов:
• реализовать шаблон конструкции модуля вручную в виде тут же инициализируемой функции (см. статью Тодда Мотто (Todd Motto) Mastering the Module Pattern на /);
• воспользоваться сторонней реализацией стандарта AMD () или стандарта CommonJS ().
Стандарт CommonJS был создан для внедрения модульности в приложения JavaScript, запускаемые вне браузеров (например, тех, которые созданы в среде Node.js и разработаны под управлением Google-движка V8). AMD используется преимущественно для приложений, запускаемых в браузере.
В любом скромном по размеру веб-приложении следует минимизировать объем JavaScript-кода, загружаемого на стороне клиента. Представим обычный интернет-магазин. Нужно ли загружать код для обработки платежей, когда пользователь открывает главную страницу приложения? Что, если пользователи никогда не нажмут кнопку Place Order (Разместить заказ)? Было бы вполне разумно разбить приложение на модули для загрузки кода по мере необходимости. Require JS является, наверное, наиболее популярной библиотекой стороннего производителя, реализующей стандарт AMD. Она позволяет определять зависимости между модулями и загружать их в браузер по требованию.
Начиная с ES6, модули стали частью языка, что означает прекращение использования разработчиками сторонних библиотек для реализации различных стандартов. Даже если браузеры не поддерживают модули ES6 естественным образом, то существуют полифиллы, позволяющие приступить к использованию модулей JavaScript уже сейчас. В этой книге мы используем полифилл под названием SystemJS.
Как правило, модуль представляет собой обыкновенный файл с кодом JavaScript, в котором реализуется определенное функциональное свойство и предоставляется открытый интерфейс, позволяющий задействовать этот модуль другим программам на JavaScript. Специального ключевого слова для объявления кода конкретного файла в качестве модуля не существует. Но в сценарии можно воспользоваться ключевыми словами import и export, превращающими сценарий в модуль ES6.
Ключевое слово import позволяет одному сценарию объявлять свои потребности в применении переменных или функций, находящихся в другом сценарном файле. Аналогично этому ключевое слово export позволяет объявлять переменные, функции или классы, которые модуль может выставить для использования другими сценариями. Иными словами, с помощью ключевого слова export можно сделать выбранные API доступными другим модулям. Имеющиеся в модуле функции, переменные и классы, не объявленные явным образом экспортируемыми, остаются инкапсулированными в модуле.
ПРИМЕЧАНИЕ
Основным отличием модуля от обычного файла JavaScript является то, что при добавлении файла к странице с помощью тега <script> он становится частью глобального контекста, а объявления в модулях являются локальными и никогда не становятся частью глобального пространства имен. Даже экспортируемые элементы доступны только тем модулям, которые их импортируют.
В ES6 две разновидности применения ключевого слова export: именованное и по умолчанию. При именованном экспортировании можно воспользоваться ключевым словом export, поставив его перед несколькими элементами модуля (такими как классы, функции и переменные). Код в следующем файле (tax.js) экспортирует переменную taxCode и функцию calcTaxes(), но функция doSomethingElse() остается скрытой от внешних сценариев:
export var taxCode;
export function calcTaxes() { // сюда помещается код }
function doSomethingElse() { // сюда помещается код }
Когда сценарий импортирует поименованные элементы экспортируемого модуля, их имена должны быть заключены в фигурные скобки. Эта особенность показана в файле main.js:
import {taxCode, calcTaxes} from 'tax';
if (taxCode === 1) { // выполнение каких-либо операций }
calcTaxes();
Здесь tax ссылается на имя файла, содержащего модуль, за исключением расширения.
Один из экспортируемых элементов модуля может быть помечен как default, что означает безымянный экспорт, и другой модуль способен присвоить этому элементу в свой инструкции import любое имя.
Код файла my_module.js, экспортирующий функцию, может иметь следующий вид:
Код файла main.js импортирует как именованные, так и безымянные экспортируемые элементы, присваивая последнему имя coolFunction:
import coolFunction, {taxCode} from 'my_module';
coolFunction();
Обратите внимание: имя coolFunction, в отличие от имени taxCode, в фигурные скобки не заключается. Сценарий, импортирующий класс, переменную или функцию, которые экспортируются с указанием ключевого слова, должен давать им имена без применения каких-либо специальных ключевых слов:
import aVeryCoolFunction, {taxCode} from 'my_module';
aVeryCoolFunction();
Но чтобы предоставить псевдоним именованным экспортируемым элементам, нужно воспользоваться кодом, подобным следующему:
import coolFunction, {taxCode as taxCode2016} from 'my_module';
Инструкции import не приводят к копированию экспортируемого кода. Они служат в качестве ссылок. Сценарий, импортирующий модули или элементы, не может их изменять, и если значения в импортируемых модулях изменяются, то новые значения тут же оказывают влияние на все места, куда они были импортированы.
В ранних проектах спецификации ES6 определен динамический загрузчик модулей под названием System, но в окончательную версию спецификации он не попал. В будущем объект System будет естественным образом реализован браузерами как загрузчик на основе промисов, который может быть использован следующим образом:
System.import('someModule')
.then (function(module){
module.doSomething();
})
.catch (function(error){
// здесь выполняется обработка ошибок
})
;
Поскольку пока объект System еще не реализован ни в одном браузере, мы воспользуемся полифиллом. Одним из полифиллов System является ES6 Module Loader, а другим — SystemJS.
ПРИМЕЧАНИЕ
В то время как ES6-module-loader.js является полифиллом для объекта System, загружающим только модули ES6, универсальный загрузчик SystemJS поддерживает не только модули ES6, но и модули AMD и CommonJS. В данной книге, начиная с главы 3, повсюду используется SystemJS (за исключением главы 10, в которой применяется загрузчик из Webpack). SystemJS позволяет загружать код JavaScript, а также файлы CSS и HTML в динамическом режиме.
Полифилл для ES6 Module Loader доступен в GitHub на . Вы можете скачать и распаковать этот загрузчик, скопировать файл ES6-module-loader.js в каталог вашего проекта и включить его в ваш HTML-файл перед сценариями вашего приложения:
<script src="es6-module-loader.js"></script>
<script src="my_app.js"></script>
Чтобы гарантировать работу сценария ES6 на всех браузерах, нужно транспилировать его в ES5. Это можно сделать либо заранее, в качестве части процесса сборки, либо динамически в браузере. Мы покажем вам последний вариант, используя компилятор Traceur.
В HTML-файл нужно включить транспилятор, загрузчик модулей и ваш сценарий (или сценарии). Можно загрузить сценарий Traceur в ваш локальный каталог или предоставить прямую ссылку на него:
<script src="">
</script>
<script src="es6-module-loader.js"></script>
<script src="my-es6-app.js"></script>
Рассмотрим простое приложение интернет-магазина, имеющее загружаемые по требованию модули доставки товаров и выставления счетов. Приложение состоит из одного HTML-файла и двух модулей. В HTML-файле имеется одна кнопка с надписью Load the Shipping Module (Загрузить модуль доставки). Когда пользователь ее нажимает, приложение должно загрузить модуль доставки и воспользоваться им, а он, в свою очередь, зависит от модуля выставления счетов. Модуль доставки имеет следующий вид (листинг А.16).
Листинг A.16. Содержимое файла shipping.js
import {processPayment} from 'billing';
export function ship() {
processPayment();
console.log("Shipping products...");
}
function calculateShippingCost(){
console.log("Calculating shipping cost");
}
Функция ship() может вызываться внешними сценариями, а функция calculateShippingCost() является закрытой. Модуль доставки начинается с инструкции import, поэтому может вызвать функцию processPayment() из показанного далее модуля выставления счетов (листинг А.17).
Листинг A.17. Содержимое файла billing.js
function validateBillingInfo() {
console.log("Validating billing info...");
}
export function processPayment(){
console.log("processing payment...");
}
В модуле выставления счетов имеется также открытая функция processPayment() и закрытая функция validateBillingInfo().
HTML-файл включает одну кнопку с обработчиком события щелчка, загружающим модуль доставки с помощью System.import() из ES6-module-loader (листинг А.18).
Листинг A.18. Содержимое файла moduleLoader.html
Метод System.import() возвращает ES6-объект Promise, а когда модуль загружен, выполняется функция, указанная в then(). В случае ошибки управление передается функции catch().
В then() в консоли выводится сообщение, и из модуля доставки вызывается функция ship(), запускающая из модуля выставления счетов функцию processPayment(). После этого предпринимается попытка вызвать имеющуюся в модуле функцию calculateShippingCost(), которая заканчивается выдачей ошибки, поскольку эта функция не была экспортирована и осталась закрытой.
СОВЕТ
Если используется Traceur и в HTML-файле имеется встроенный сценарий, то, чтобы гарантировать транспиляцию кода с помощью Traceur в ES5, задействуйте атрибут type="module". Без него в браузерах, не поддерживающих ключевое слово let и стрелочные функции, этот сценарий работать не будет.
Чтобы запустить приведенный пример на своем компьютере, вам необходимо наличие Node.js с установленным средством npm. Затем загрузите и установите в любой каталог загрузчик модулей ES6-module-loader, введя следующую команду npm:
npm install ES6-module-loader
Затем создайте папку приложения и скопируйте в нее файл ES6-module-loader.js (это минимизированная версия загрузчика, загруженная с помощью npm). У приложения, используемого в качестве примера, имеется три дополнительных файла, показанных в листингах A.16–A.18. Чтобы ничего не усложнять, все три файла нужно хранить в одной папке.
ПРИМЕЧАНИЕ
Чтобы увидеть этот код в действии, вам нужно его «подать», используя веб-сервер. Можно установить базовый HTTP-сервер, подобный live-серверу, применив пояснения, которые были даны в подразделе 2.1.4.
Мы запускали moduleLoader.html в Google Chrome и открывали панель Developer Tools (Инструменты разработчика). На рис. A.6 показано, как выглядит окно этого браузера после нажатия кнопки Load the Shipping Module (Загрузить модуль доставки).
Обратите внимание на вкладку XHR в середине окна. HTML-страница загружает shipping.js и billing.js только после того, как пользователь нажмет кнопку. Эти файлы невелики по размеру (440 и 387 байт, включая объекты HTTP-ответа), и выполнение дополнительного сетевого вызова для их получения представляется неким излишеством. Но если приложение состоит из десяти модулей по 500 Кбайт каждый, то в его разбиении на модули и применении ленивой загрузки появляется вполне определенный смысл.
Рис. А.6. Использование загрузчика модулей ES6-module-loader
В нижней части рисунка на вкладке Console (Консоль) можно увидеть сообщение из сценария в moduleLoader, свидетельствующее о загрузке модуля доставки. Затем сценарий вызывает функцию ship() из модуля доставки и выдает, как и ожидалось, ошибку при попытке вызова функции calculateShippingCost().
ПРИМЕЧАНИЕ
Целью этого приложения было познакомить вас с синтаксисом ES6. Для его углубленного изучения прочитайте книгу Exploring ES6, написанную Акселем Раушмайером (Axel Rauschmayer) (/). Эрик Дуглас (Eric Douglas) собирает на GitHub различные обучающие ресурсы, имеющие отношение к ES6, получить доступ к которым можно на .