Что нового в ECMAScript 2017 (ES8)

В истории развития JavaScript были периоды застоя и бурного роста. С момента появления языка (1995) и вплоть до 2015 года обновленные спецификации выходили не регулярно.

В июне 2017 года вышла обновленная спецификация: ES8 или, что более правильно, ES2017. Давайте вместе рассмотрим, какие обновления в языке произошли в этой версии стандарта.

Асинхронные функции

Пожалуй, одно из самых ожидаемых нововведений в JavaScript. Теперь все официально.

Синтаксис

Для создания асинхронной функции используется ключевое слово async.

  • Объявление асинхронной функции: async function asyncFunc() {}
  • Выражение с асинхронной функцией: const asyncFunc = async function () {};
  • Метод с асинхронной функцией: let obj = { async asyncFunc() {} }
  • Стрелочная асинхронная функция: const asyncFunc = async () => {};

Оператор async

Давайте разберемся как это работает. Создадим простую асинхронную функцию:

async function mainQuestion() {
return 42;
}

Функция mainQuestion вернет промис, несмотря на то что мы возвращаем число:

const result = mainQuestion();
console.log(result instanceof Promise);
// true

Если вы до этого уже использовали генераторы из ES2015, то сразу вспомните, что функция-генератор автоматически создает и возвращает итератор. С асинхронной функцией происходит точно так же.

А где же число 42 которое мы вернули? Им совершенно очевидным образом разрешится промис, который мы вернули:

console.log('Начало');
mainQuestion()
.then(result => console.log(`Результат: ${result}`));
console.log('Конец');

Промис разрешается асинхронно, поэтому мы получим такой вывод в консоль:

Начало
Конец
Результат: 42

А что будет, если наша асинхронная функция вообще ничего не вернет?

async function dumbFunction() {}
console.log('Начало');
dumbFunction()
.then(result => console.log(`Результат: ${result}`));
console.log('Конец');

Промис разрешится «ничем», что в JavaScript соответствует типу undefined:

Начало
Конец
Результат: undefined

Не смотря на название, сама асинхронная функция вызывается и выполняется синхронно:

async function asyncLog(message) {
console.log(message);
}
console.log('Начало');
asyncLog('Асинхронная функция');
console.log('Конец');

Вывод в консоль будет таким, хотя многие могли ожидать иного:

Начало
Асинхронная функция
Конец

Асинхронная функция возвращает промис. Надеюсь вы уже хорошо разобрались что такое промисы. В ином случае рекомендую предварительно разобраться с ними.

А что будет если мы в теле функции тоже вернем промис?

async function timeout(message, time = 0) {
return new Promise(done => {
setTimeout(() => done(message), time * 1000);
});
}
console.log('Начало');
timeout('Прошло 5 секунд', 5)
.then(message => console.log(message));
console.log('Конец');

Он встанет в цепочку к тому промису, который создаётся автоматически, как если бы мы вернули промис внутри колбэка, переданного в метод then:

Начало
Конец
Прошло 5 секунд (_через 5 секунд_)

Но если бы мы написали не асинхронную функцию, а обычную, то всё работало бы точно так же, как в примере выше:

function timeout(message, time = 0) {
return new Promise(done => {
setTimeout(() => done(message), time * 1000);
});
}

Тогда для чего нужны асинхронные функции? Самая крутая особенность асинхронной функции — возможность в теле такой функции подождать результата (когда разрешится промис) другой асинхронной функци:

function rand(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
async function randomMessage() {
const message = [
'Привет',
'Куда пропал?',
'Давно не виделись'
][rand(0, 2)];
return timeout(message, 5);
}
async function chat() {
const message = await randomMessage();
console.log(message);
}
console.log('Начало');
chat();
console.log('Конец');

Обратите внимание, тело функции chat выглядит как тело синхронной функции, нет даже ни одной функции обратного вызова. Но await randomMessage() вернет нам не промис, а дождется 5 секунд и вернёт нам само сообщение, которым разрешится промис. В этом и заключается его роль: «дождаться результата правого операнда».

Начало
Конец
Куда пропал?

Сообщение Конец после вызова функции chat выведется сразу, не дожидаясь вывода сообщения в теле функции chat. Поэтому логично переписать эту часть так:

console.log('Начало');
chat()
.then(() => console.log('Конец'));
console.log('Это еще не конец');

Оператор await

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

console.log('Начало');
await chat();
console.log('Конец');
// SyntaxError: Unexpected token, expected ;

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

А что если мы «подождем» синхронную функцию, возвращающую промис? Да, так можно:

function mainQuestion() {
return new Promise(done => done(42));
}
async function dumbAwait() {
const number = await mainQuestion();
console.log(number);
}
dumbAwait();
// 42

А что если мы «ожидаем» синхронную функцию, которая вернёт число (строку или что-либо еще)? Да, так тоже можно:

function mainQuestion() {
return 42;
}
async function dumbAwait() {
const number = await mainQuestion();
console.log(number);
}
dumbAwait();
// 42

Мы можем даже «подождать» число, правда особого смысла в этом нет:

async function dumbAwait() {
const number = await 42;
console.log(number);
}
dumbAwait();
// 42

Оператору await нет никакой разницы чего ожидать. Он работает аналогично тому, как работает колбэк метода then:

  1. если вернулся промис: ждем промис, и возвращаем результат;
  2. если вернулся не промис: оборачиваем в Promise.resolve и дальше аналогично.

await отправляет асинхронную функцию в асинхронное плавание:

async function longTask() {
console.log('Синхронно');
await null;
console.log('Асинхронно');
for (const i of Array (10E6)) {}
return 42;
}
console.log('Начало');
longTask()
.then(() => console.log('Конец'));
console.log('Это еще не конец');

Воспринимайте, пожалуйста, этот пример как демонстрацию работы await, а не как «удобный» трюк. Результат работы ниже:

Начало
Синхронно
Это еще не конец
Асинхронно
Конец

Обработка ошибок

А что если промис которого мы «ожидаем» с await не разрешится? Тогда await бросит исключение:

async function failPromise() {
return Promise.reject('Ошибка');
}
async function catchMe() {
try {
const result = await failPromise();
console.log(`Результат: ${result}`);
} catch (error) {
console.error(error);
}
}
catchMe();
// Ошибка

Мы можем поймать это исключение как любое другое, с помощью try-catch и что-то предпринять.

Применение

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

В теле асинхронной функции мы можем записывать последовательные асинхронные вызовы как плоский синхронный код, и это то, чего мы ждали:

async function fetchAsync(url) {
const response = await fetch(url);
const data = await response.json();
return data;
}
async function getUserPublicMessages(login) {
const profile = await fetchAsync(`/user/${login}`);
const messages = await fetchAsync(`/user/${profile.id}/last`);
return messages.filter(message => message.isPublic);
}
getUserPublicMessages('spiderman')
.then(messages => show(messages));

Попробуйте переписать этот код на промисах, и оцените разницу в читаемости.

Николас Бевакуа в своей статье «Understanding JavaScript’s async await» очень подробно разбирает принципы и особенности работы асинхронных функций. Статья обильно приправлена примерами кода и юзкейсами.

Поддержка

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


Object.values и Object.entries

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

Object.entries()

Данная функция возвращает массив собственных перечисляемых свойств объекта в формате [ключ, значение].

Если структура объекта содержит ключи и значения, то запись на выходе будет перекодирована в массив, содержащий в себе массивы с двумя элементами: первым элементом будет ключ, а вторым элементом — значение. Пары [ключ, значение] будут расположены в том же порядке, что и свойства в объекте.

Object.entries({ аты: 1, баты: 2 });

Результатом работы кода будет:

[ [ 'аты', 1 ], [ 'баты', 2 ] ]

Если структура данных, передаваемая в Object.entries() не содержит ключей, то на их место встанет индекс элемента массива.

Object.entries(['n', 'e', 't', 'o', 'l', 'o', 'g', 'y']);

На выходе получим:

[ [ '0', 'n' ],   
[ '1', 'e' ],
[ '2', 't' ],
[ '3', 'o' ],
[ '4', 'l' ],
[ '5', 'o' ],
[ '6', 'g' ],
[ '7', 'y' ] ]

Символы игнорируются

Обратите внимание, что свойство, ключом которого является символ, будет проигнорировано:

Object.entries({ [Symbol()]: 123, foo: 'bar' });

Результат:

[ [ 'foo', 'bar' ] ]

Итерация по свойствам

Появление функции Object.entries() наконец дает нам способ итерации по свойствам объекта с помощью цикла for-of:

let obj = { аты: 1, баты: 2 };
for (let [x,y] of Object.entries(obj)) {
console.log(`${JSON.stringify(x)}: ${JSON.stringify(y)}`);
}

Вывод:

"аты": 1
"баты": 2

Object.values()

Эта функция близка к Object.entries(). На выходе мы получим массив, состоящий только из значений собственных свойств, без ключей. Что, в принципе, можно понять из названия.

Поддержка

На сегодняшний день Object.entries() и Object.values() поддерживаются основными браузерами.

«Висячие» запятые в параметрах функций

Теперь законно оставлять запятые в конце списка аргументов функций. При вызове функции запятая в конце тоже вне криминала.

function randomFunc(
param1,
param2,
) {}
randomFunc(
'foo',
'bar',
);

«Висячие» запятые разрешены так же в массивах и объектах. Они просто игнорируются и никак не влияют на работу.

Такое небольшое, но безусловно полезное нововведение!

let obj = {
имя: 'Иван',
фамилия: 'Петров',
};
let arr = [
'красный',
'зеленый',
'синий',
];

Поддержка

Придется немного подождать прежде чем оставлять запятую в конце списка параметров.

«Заглушки» для строк: достигаем нужной длинны

В ES8 появилось два новых метода для работы со строками: padStart() и padEnd().

Метод padStart() подставляет дополнительные символы перед началом строки, слева. А padEnd(), в свою очередь, справа, после конца строки.

Интерфейс функции

str.padStart(желаемаяДлинна, [строкаЗаглушка]);
str.padEnd(желаемаяДлинна, [строкаЗаглушка]);

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

Второй параметр является не обязательным. Если он не указан, то строка будет дополнена пробелами (значение по умолчанию).

Если исходная строка длиннее чем заданный параметр, то строка останется неизменной.

'я'.padStart(6, '~'); // '~~~~~я'
'прямо в цель'.padStart(15, '-->'); // '-->прямо в цель'
'пусто'.padEnd(10); // 'пусто     '
'Ч'.padEnd(10, '0123456789'); // 'Ч012345678'

Поддержка

Прекрасная картина!

Функция Object.getOwnPropertyDescriptors()

Функция возвращает массив с дескрипторами всех собственных свойств объекта.

const person = {
first: 'Ирвинг',
last: 'Гофман',
get fullName() {
return `Добрый день, мое имя ${first} ${last}`;
},
};
console.log(Object.getOwnPropertyDescriptors(person));

Результат:

{ first: 
{ value: 'Ирвинг',
writable: true,
enumerable: true,
configurable: true },
last:
{ value: 'Гофман',
writable: true,
enumerable: true,
configurable: true },
fullName:
{ get: [Function: get fullName],
set: undefined,
enumerable: true,
configurable: true } }

Область применения

  1. Для копирования свойств объекта, в том числе геттеров, сеттеров, неперезаписываемых свойств.
  2. Копирование объекта. .getOwnPropertyDescriptor можно использовать в качестве второго параметра в Object.create().
  3. Создание кроссплатформенных литералов объектов с определенным прототипом.

Обратите внимание, что методы с super не могут быть скопированы, поскольку тесно связаны с изначальным объектом.

Поддержка

Даже у IE все в порядке.

Разделение памяти и объект Atomics

Это новшество вводит в JavaScript понятие разделяемой памяти. Новая конструкция SharedArrayBuffer и уже существовавшие ранее TypedArray and DataView помогают распределять доступную память. Это обеспечивает необходимый порядок выполнения операций при одновременном использовании общей памяти несколькими потоками.

Объект SharedArrayBuffer является примитивным строительным блоком для высокоуровневых абстракций. Буфер может использоваться для перераспределения байтов между несколькими рабочими потоками. У этого есть два явных преимущества:

  1. Повышается скорость обмена данными между воркерами.
  2. Координация между воркерами становится быстрее и проще (по сравнению с postMessage()).

Безопасный доступ к общим данным

Новый объект Atomics не может использоваться как конструктор, но имеет ряд собственных методов, которые призваны решить проблему безопасности при выполнении различных операций с типизированными массивами SharedArrayBuffer.

Поддержка

У этой «обновки» пока все плохо с поддержкой. Надеемся, верим, ждем.

Алекс Раушмайер подробно описал механизм работы этой возможности языка в статье «ES proposal: Shared memory and atomics».
На русском языке есть качественный перевод статьи Лин Кларк «Быстрый курс по управлению памятью»


По итогам опроса, проведенного StackOverflow в 2017 году JavaScript является самым используемым языком программирования. Лично меня очень радует сложившаяся на сегодня картина:

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

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

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

Мои коллеги в Нетологии и я понимаем, что преимуществом онлайн-образования является его гибкость и способность подстраиваться под постоянно меняющиеся реалии мира разработки. По этой причине мы не прекращаем работу над обновлением материалов наших курсов и своевременно рассказываем о всех новых возможностях. Вот и сейчас мы дорабатываем курс «JavaScript: от нуля до промисов» так, чтобы каждый студент знал и умел работать с инструментами, появившимися в ECMAScript 2017.



Оригинал статьи опубликован в блоге Нетологии