Новые возможности ES2018 о которых должен знать каждый разработчик JavaScript
Девятое издание стандарта ECMAScript, официально известного как ECMAScript 2018 (или ES2018 для краткости), было выпущено в июне 2018 года. Начиная с ES2016, новые версии спецификаций ECMAScript выпускаются ежегодно, а не раз в несколько лет, и содержат меньше новых возможностей, чем основные редакции. Новейшая версия стандарта продолжает ежегодный цикл выпуска. В нем было добавлено четыре новых функции RegExp, свойства rest/spread, асинхронную итерацию и Promise.prototype.finally. Кроме того, ES2018 отменяет синтаксическое ограничение escape-последовательностей из тегированных шаблонов.
Описание этих новые возможностей объясняются в следующих подразделах.
Свойства rest/spread
Одно из наиболее интересных свойств добавленных в ES2015 был оператор spread. Этот оператор делает операции копирования и объединения массивов намного легче. Вместо вызова методов concat()
или slice()
, можно просто использовать оператор ...
:
const arr1 = [10, 20, 30]; // make a copy of arr1 const copy = [...arr1]; console.log(copy); // → [10, 20, 30] const arr2 = [40, 50]; // merge arr2 with arr1 const merge = [...arr1, ...arr2]; console.log(merge); // → [10, 20, 30, 40, 50]
Оператор spread так же удобен в ситуациях массив должен быть передан в качестве отдельного аргумента в функцию. Для примера:
const arr = [10, 20, 30] // equivalent to // console.log(Math.max(10, 20, 30)); console.log(Math.max(...arr)); // → 30
ES2018 дополнительно расширяет этот синтаксис, добавляя свойства spread к литеральным объектам. С помощью свойств spread вы можете скопировать собственные перечисляемые свойства объекта в новый объект. Рассмотрим следующий пример:
const obj1 = { a: 10, b: 20 }; const obj2 = { ...obj1, c: 30 }; console.log(obj2); // → {a: 10, b: 20, c: 30}
В коде, оператор ...
был использован для получения свойств объекта obj1
и назначению их объекту obj2
. До появления ES2018, подобная конструкция вызвала бы ошибку. Если будут определены несколько свойств с одинаковыми именами то останутся только те которые были последними:
const obj1 = { a: 10, b: 20 }; const obj2 = { ...obj1, a: 30 }; console.log(obj2); // → {a: 30, b: 20}
Свойство spread так же предоставляет новый путь объединения двух и более объектов, вместо использования метода Object.assign()
:
const obj1 = {a: 10}; const obj2 = {b: 20}; const obj3 = {c: 30}; // ES2018 console.log({...obj1, ...obj2, ...obj3}); // → {a: 10, b: 20, c: 30} // ES2015 console.log(Object.assign({}, obj1, obj2, obj3)); // → {a: 10, b: 20, c: 30}
Однако обратите внимание, что оператор spread не всегда дает тот же результат что и Object.assign()
. Рассмотрим следующий код:
Object.defineProperty(Object.prototype, 'a', { set(value) { console.log('set called!'); } }); const obj = {a: 10}; console.log({...obj}); // → {a: 10} console.log(Object.assign({}, obj)); // → set called! // → {}
В этом коде, метод Object.assign()
вызывает унаследованое свойство сеттера. И наоборот, оператор spread игнорирует свойство сеттера.
Важно понимать, что оператор spread копирует только перечисляемые свойства.
В следующем примере, свойство type
не будет скопировано потому что его атрибут enumerable
установлен в false
:
const car = { color: 'blue' }; Object.defineProperty(car, 'type', { value: 'coupe', enumerable: false }); console.log({...car}); // → {color: "blue"}
Унаследованные свойство так же игнорируются даже если они перечисляемые:
const car = { color: 'blue' }; const car2 = Object.create(car, { type: { value: 'coupe', enumerable: true, } }); console.log(car2.color); // → blue console.log(car2.hasOwnProperty('color')); // → false console.log(car2.type); // → coupe console.log(car2.hasOwnProperty('type')); // → true console.log({...car2}); // → {type: "coupe"}
В этом коде, car2
наследует совойство color
от car
. Так как оператор spread копирует только собственные свойство объекта, color
не копируется.
Так же имейте в виду что оператор spread может сделать только поверхностную копию объекта. Если свойство содержит объект, будет скопирована только ссылка на объект:
const obj = {x: {y: 10}}; const copy1 = {...obj}; const copy2 = {...obj}; console.log(copy1.x === copy2.x); // → true
Свойство x
в copy1
ссылается на тоже самый объект в памяти что и x
в copy2
, поэтому оператор сравнения возвращает true
.
Еще одна полезная функция, добавленная в ES2015, — это rest параметры, которые позволяют использовать … для представления значений как массив. Например:
const arr = [10, 20, 30]; const [x, ...rest] = arr; console.log(x); // → 10 console.log(rest); // → [20, 30]
Здесь, первый элемент в arr
назначен переменной x
, а оставшиеся элементы назначены переменной rest
. Этот паттерн, называемый деструктуризацией массива, стал настолько популярным, что технический комитет Ecma решил привнести аналогичную функциональность в объекты:
const obj = { a: 10, b: 20, c: 30 }; const {a, ...rest} = obj; console.log(a); // → 10 console.log(rest); // → {b: 20, c: 30}
Этот код использует свойство rest для копирования оставшиеся перечисляемых свойств в новый объект. Обратите внимание что rest свойства всегда должны быть в конце объекта, иначе будет ошибка:
const obj = { a: 10, b: 20, c: 30 }; const {...rest, a} = obj; // → SyntaxError: Rest element must be last element
Также имейте в виду, что использование нескольких rest в объекте вызывает ошибку, если они не являются вложенными:
const obj = { a: 10, b: { x: 20, y: 30, z: 40 } }; const {b: {x, ...rest1}, ...rest2} = obj; // no error const {...rest, ...rest2} = obj; // → SyntaxError: Rest element must be last element
Поддержка свойств Rest/Spread
Chrome | Firefox | Safari | Edge |
---|---|---|---|
60 | 55 | 11.1 | No |
Chrome Android | Firefox Android | iOS Safari | Edge Mobile | Samsung Internet | Android Webview |
---|---|---|---|---|---|
60 | 55 | 11.3 | No | 8.2 | 60 |
Node.js:
- 8.0.0 (требует флаг
--harmony
) - 8.3.0 (полная поддержка)
Асинхронная итерация
Перебор набора данных является важной частью программирования. До ES2015 JavaScript предоставлял для этой цели операторы for, for … in и while, а также такие методы, как map(), filter() и forEach(). Чтобы программисты могли обрабатывать элементы коллекции по одному, в ES2015 появился интерфейс итератора.
Объект является итеративным, если у него есть свойство Symbol.iterator. В ES2015 объекты строк и коллекций, такие как Set, Map и Array, имеют свойство Symbol.iterator и, следовательно, являются итеративными. Следующий код дает пример того, как получить доступ к итерируемым элементам:
const arr = [10, 20, 30]; const iterator = arr[Symbol.iterator](); console.log(iterator.next()); // → {value: 10, done: false} console.log(iterator.next()); // → {value: 20, done: false} console.log(iterator.next()); // → {value: 30, done: false} console.log(iterator.next()); // → {value: undefined, done: true}
Symbol.iterator — это широко известный символ, определяющий функцию, которая возвращает итератор. Основным способом взаимодействия с итератором является метод next(). Этот метод возвращает объект с двумя свойствами: value и done. Свойство value содержит значение следующего элемента в коллекции. Свойство done содержит true или false, указывающие, достигнут ли конец коллекции.
По умолчанию простой объект не является итеративным, но он может стать итеративным, если вы определите для него свойство Symbol.iterator, как в этом примере:
const collection = { a: 10, b: 20, c: 30, [Symbol.iterator]() { const values = Object.keys(this); let i = 0; return { next: () => { return { value: this[values[i++]], done: i > values.length } } }; } }; const iterator = collection[Symbol.iterator](); console.log(iterator.next()); // → {value: 10, done: false} console.log(iterator.next()); // → {value: 20, done: false} console.log(iterator.next()); // → {value: 30, done: false} console.log(iterator.next()); // → {value: undefined, done: true}
Этот объект является итеративным, поскольку у него определенно свойство Symbol.iterator. Итератор использует метод Object.keys() для получения массива имен свойств объекта, а затем присваивает его константе values. Он также определяет переменную-счетчик и присваивает ему начальное значение 0. При выполнении итератора он возвращает объект, содержащий метод next(). Каждый раз, когда вызывается метод next(), он возвращает пару {value, done} со value, содержащим следующий элемент в коллекции, и done хранящее логическое значение, указывающее, достиг ли итератор конца в коллекции.
Хотя этот код работает отлично, он неоправданно сложен. К счастью, использование функции генератора может значительно упростить процесс:
const collection = { a: 10, b: 20, c: 30, [Symbol.iterator]: function * () { for (let key in this) { yield this[key]; } } }; const iterator = collection[Symbol.iterator](); console.log(iterator.next()); // → {value: 10, done: false} console.log(iterator.next()); // → {value: 20, done: false} console.log(iterator.next()); // → {value: 30, done: false} console.log(iterator.next()); // → {value: undefined, done: true}
Внутри этого генератора цикл for … in используется для перечисления по коллекции и получения значения каждого свойства. Результат точно такой же, как и в предыдущем примере, но код значительно короче.
Недостатком итераторов является то, что они не подходят для работы с асинхронными источниками данных. Поэтому в ES2018 появились асинхронные итераторы и асинхронные итерируемые объекты -итерации (iterables). Асинхронный итератор отличается от обычного итератора тем, что вместо возврата простого объекта в формате {value, done} он возвращает промис (promise), которое соответствует {value, done}. Асинхронная итерируемые объекты (iterable) содержат метод Symbol.asyncIterator (вместо Symbol.iterator), который возвращает асинхронный итератор.
Пример:
const collection = { a: 10, b: 20, c: 30, [Symbol.asyncIterator]() { const values = Object.keys(this); let i = 0; return { next: () => { return Promise.resolve({ value: this[values[i++]], done: i > values.length }); } }; } }; const iterator = collection[Symbol.asyncIterator](); console.log(iterator.next().then(result => { console.log(result); // → {value: 10, done: false} })); console.log(iterator.next().then(result => { console.log(result); // → {value: 20, done: false} })); console.log(iterator.next().then(result => { console.log(result); // → {value: 30, done: false} })); console.log(iterator.next().then(result => { console.log(result); // → {value: undefined, done: true} }));
Обратите внимание, что для достижения того же результата невозможно использовать итератор промисей (promises). Хотя обычный синхронный итератор может асинхронно определять values, он все равно должен определять состояние «done» синхронно.
Опять же, вы можете упростить процесс, используя функцию генератора, как показано ниже:
const collection = { a: 10, b: 20, c: 30, [Symbol.asyncIterator]: async function * () { for (let key in this) { yield this[key]; } } }; const iterator = collection[Symbol.asyncIterator](); console.log(iterator.next().then(result => { console.log(result); // → {value: 10, done: false} })); console.log(iterator.next().then(result => { console.log(result); // → {value: 20, done: false} })); console.log(iterator.next().then(result => { console.log(result); // → {value: 30, done: false} })); console.log(iterator.next().then(result => { console.log(result); // → {value: undefined, done: true} }));
Обычно, функция генератор с помощью метода next()
возвращает объект генератор . То есть когда вызывается next()
он возвращает пару {value, done}
где свойство value
содержит полученное значение. Асинхронный генератор делает то же самое за исключением то что он возвращает промисис (promise) которые соответствует {value, done}
.
Простой способ перебора итерируемого объекта состоит в использовании оператора for … of, но for … of не работает с асинхронными итерируемыми объектами, поскольку value и done не определяются синхронно. По этой причине ES2018 предоставляет for…await…of. Давайте посмотрим на пример:
const collection = { a: 10, b: 20, c: 30, [Symbol.asyncIterator]: async function * () { for (let key in this) { yield this[key]; } } }; (async function () { for await (const x of collection) { console.log(x); } })(); // logs: // → 10 // → 20 // → 30
В этом коде оператор for … await … of неявно вызывает метод Symbol.asyncIterator для объекта коллекции, чтобы получить асинхронный итератор. Каждый раз в цикле вызывается метод итератора next(), который возвращает promise. Как только promise разрешено, свойство value результирующего объекта считывается в переменную x. Цикл продолжается до тех пор, пока свойство done возвращаемого объекта не будет иметь значение true.
Имейте в виду, что оператор for … await … of действует только в асинхронных генераторах и асинхронных функциях. Нарушение этого правила приводит к ошибке SyntaxError.
Метод next() может вернуть promise, которое будет отклонено. Чтобы корректно обработать отклоненное обещание, вы можете заключить оператор for…await…of в оператор try…catch, например:
const collection = { [Symbol.asyncIterator]() { return { next: () => { return Promise.reject(new Error('Something went wrong.')) } }; } }; (async function() { try { for await (const value of collection) {} } catch (error) { console.log('Caught: ' + error.message); } })(); // logs: // → Caught: Something went wrong.По
Поддержка асинхронных итераторов
Chrome | Firefox | Safari | Edge |
---|---|---|---|
63 | 57 | 12 | No |
Chrome Android | Firefox Android | iOS Safari | Edge Mobile | Samsung Internet | Android Webview |
---|---|---|---|---|---|
63 | 57 | 12 | No | 8.2 | 63 |
Node.js:
- 8.10.0 (требуется флаг —harmony_async_iteration)
- 10.0.0 (полная поддержка)
Promise.prototype.finally
Еще одно интересное дополнение в ES2018 — метод finally(). Несколько библиотек ранее реализовывали подобный метод, который оказался полезным во многих ситуациях. Это побудило технический комитет Ecma официально добавить finally() в спецификацию. С помощью этого метода можно выполнять блок кода независимо от состояния promise. Давайте посмотрим на простой пример:
fetch('https://www.google.com') .then((response) => { console.log(response.status); }) .catch((error) => { console.log(error); }) .finally(() => { document.querySelector('#spinner').style.display = 'none'; });
Метод finally()
очень полезен когда нужно например что то очистить после операции которая может завершиться успешно или нет. В этом коде, метод finally()
просто скрывает спиннер загрузки после получения данных. Вместо то что бы дублировать финальную логику в методах then()
и catch()
можно прописать этот код в одном методе которых выполнится независимо от статуса выполнения promises.
Вы можете достичь такого же результата используя promise.then(func, func)
вместо promise.finally(func)
, но нужно будет дублировать одинаковый код в обработчике успешного и ошибочного завершения promises, ну или использовать отдельную переменную для этого:
fetch('https://www.google.com') .then((response) => { console.log(response.status); }) .catch((error) => { console.log(error); }) .then(final, final); function final() { document.querySelector('#spinner').style.display = 'none'; }
Как и в случае then() и catch(), метод finally() всегда возвращает promise, поэтому можно связать несколько методов. Обычно finally() используется последним элементом в цепочке, но в определенных ситуациях, например, при выполнении HTTP-запроса, рекомендуется объединить в цепочку другую функцию catch() для обработки ошибок, которые так же могут возникнуть в finally().
Поддержка Promise.prototype.finally
Chrome | Firefox | Safari | Edge |
---|---|---|---|
63 | 58 | 11.1 | 18 |
Chrome Android | Firefox Android | iOS Safari | Edge Mobile | Samsung Internet | Android Webview |
---|---|---|---|---|---|
63 | 58 | 11.1 | No | 8.2 | 63 |
Node.js:
10.0.0 (полная поддержка)
Новые свойства RegExp
ES2018 добавленно четыре новых свойства объекта RegExp
, которые значительно улучшают возможности обработки строк JavaScript. Новые свойства:
- s (dotAll) флаг
- Именованные группы
- Ретроспективные проверки
- Unicode property escapes
s (dotAll) Флаг
В регулярных выражениях точка (.
) специальный символ который соответствует любому символу за исключением символов разрыва строки такими как перехода на новую строку (\n
) или возврате каретки (\r
). Обходной путь для сопоставления всех символов, включая разрывы строк, заключается в использовании класса символов с двумя противоположными сокращениями, такими как [\d\D]. Этот класс символов сообщает обработчику регулярных выражений, что нужно найти символ, который является либо цифрой (\d
), либо не цифрой (\D
). В результате он соответствует любому символу:
console.log(/one[\d\D]two/.test('one\ntwo')); // → true
ES2018 вводит режим, в котором точка может быть использована для достижения того же результата. Этот режим можно активировать для каждого регулярного выражения с помощью флага s:
console.log(/one.two/.test('one\ntwo')); // → false console.log(/one.two/s.test('one\ntwo')); // → true
Преимущество использования флага для выбора нового поведения заключается в обратной совместимости. Таким образом, не затрагиваются, уже существующие шаблоны регулярных выражений, использующие символ точки.
Именованные группы
В некоторых шаблонах регулярных выражений использование числа для ссылки на группу может привести к путанице. Например, возьмем регулярное выражение /(\d{4})-(\d{2})-(\d{2})/, соответствующее дате. Поскольку нотация даты в американском английском отличается от британского английского, трудно понять, какая группа относится к дню, а какая группа относится к месяцу:
const re = /(\d{4})-(\d{2})-(\d{2})/; const match= re.exec('2019-01-10'); console.log(match[0]); // → 2019-01-10 console.log(match[1]); // → 2019 console.log(match[2]); // → 01 console.log(match[3]); // → 10
ES2018 представляет именованные группы, которые используют синтаксис (?<name>…). Таким образом, шаблон для сопоставления с датой можно записать менее двусмысленным образом:
const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/; const match = re.exec('2019-01-10'); console.log(match.groups); // → {year: "2019", month: "01", day: "10"} console.log(match.groups.year); // → 2019 console.log(match.groups.month); // → 01 console.log(match.groups.day); // → 10
Позже вы можете вызвать именованную группу в шаблоне, используя синтаксис \k<name> . Например, чтобы найти последовательные повторяющиеся слова в предложении, вы можете использовать /\b(?<dup>\w+)\s+\k<dup>\b/:
const re = /\b(?<dup>\w+)\s+\k<dup>\b/; const match = re.exec('Get that that cat off the table!'); console.log(match.index); // → 4 console.log(match[0]); // → that that
Чтобы вставить именованную группу в строку замены метода replace(), нужно будет использовать конструкцию $<name> . Например:
const str = 'red & blue'; console.log(str.replace(/(red) & (blue)/, '$2 & $1')); // → blue & red console.log(str.replace(/(?<red>red) & (?<blue>blue)/, '$<blue> & $<red>')); // → blue & red
Ретроспективные проверки (Lookbehind Assertion)
ES2018 привносит в JavaScript assertion (утверждения), которые были доступны в других реализациях регулярных выражений в течение многих лет. Ранее JavaScript поддерживал только предварительные утверждения. Ретроспективные (Lookbehind) утверждение обозначается (?<=…) и позволяет сопоставить шаблон на основе подстроки, которая предшествует шаблону. Например, если вы хотите сопоставить цену продукта в долларах, фунтах или евро, не используя символ валюты, вы можете использовать /(?<=\$|£|€)\d+(\.\d*)?/:
const re = /(?<=\$|£|€)\d+(\.\d*)?/; console.log(re.exec('199')); // → null console.log(re.exec('$199')); // → ["199", undefined, index: 1, input: "$199", groups: undefined] console.log(re.exec('€50')); // → ["50", undefined, index: 1, input: "€50", groups: undefined]
Существует также отрицательная версия lookbehind, которая обозначается (?<!…). Отрицательный lookbehind позволяет сопоставить шаблон только в том случае, если ему не предшествует шаблон внутри lookbehind. Например, шаблон /(?<!un)available/соответствует доступному слову, если у него нет префикса «un»:
const re = /(?<!un)available/; console.log(re.exec('We regret this service is currently unavailable')); // → null console.log(re.exec('The service is available')); // → ["available", index: 15, input: "The service is available", groups: undefined]
Экранированные свойства Unicode (Unicode Property Escapes)
ES2018 предоставляет новый тип escape-последовательности, известный как Unicode property escape, который обеспечивает полную поддержку Unicode в регулярных выражениях. Предположим, вы хотите сопоставить символ Unicode ㉛ в строке. Хотя символ является числом, вы не можете сопоставить его с классом сокращенных символов \d, поскольку он поддерживает только символы ASCII [0-9]. С другой стороны, экранирование свойств Unicode может использоваться для сопоставления с любым десятичным числом в Unicode:
const str = '㉛'; console.log(/\d/u.test(str)); // → false console.log(/\p{Number}/u.test(str)); // → true
Точно так же, если вы хотите сопоставить любой алфавитный символ Unicode, вы можете использовать \p{Alphabetic}:
const str = 'ض'; console.log(/\p{Alphabetic}/u.test(str)); // → true // the \w shorthand cannot match ض console.log(/\w/u.test(str)); // → false
Существует также негативная версия \p{…}, которая обозначается \P{…}:
console.log(/\P{Number}/u.test('㉛')); // → false console.log(/\P{Number}/u.test('ض')); // → true console.log(/\P{Alphabetic}/u.test('㉛')); // → true console.log(/\P{Alphabetic}/u.test('ض')); // → false
Помимо буквенных и числовых, в экранированных свойствах Unicode можно использовать еще несколько свойств. Вы можете найти список поддерживаемых свойств Unicode в предложении текущей спецификации.
Поддержка новый свойств RegExp
Chrome | Firefox | Safari | Edge | |
---|---|---|---|---|
s (dotAll) Флаг | 62 | No | 11.1 | No |
Named Capture Groups | 64 | No | 11.1 | No |
Lookbehind Assertions | 62 | No | No | No |
Unicode Property Escapes | 64 | No | 11.1 | No |
Chrome (Android) | Firefox (Android) | iOS Safari | Edge Mobile | Samsung Internet | Android Webview | |
---|---|---|---|---|---|---|
s (dotAll) Flag | 62 | No | 11.3 | No | 8.2 | 62 |
Named Capture Groups | 64 | No | 11.3 | No | No | 64 |
Lookbehind Assertions | 62 | No | No | No | 8.2 | 62 |
Unicode Property Escapes | 64 | No | 11.3 | No | No | 64 |
Node.js:
- 8.3.0 (требуется флаг —harmony )
- 8.10.0 (поддержка s (dotAll) флага и lookbehind assertions)
- 10.0.0 (полная поддержка)
Шаблонные строки ( Template Literal Revision )
Когда шаблонному литералу непосредственно предшествует выражение, оно называется теговым литералом шаблона. Шаблон с тегами пригодится, когда вы хотите проанализировать литерал шаблона с помощью функции. Рассмотрим следующий пример:
function fn(string, substitute) { if(substitute === 'ES6') { substitute = 'ES2015' } return substitute + string[1]; } const version = 'ES6'; const result = fn`${version} was a major update`; console.log(result); // → ES2015 was a major update
В этом коде выражение тега — которое является обычной функцией — вызывается и передается шаблонному литералу. Функция fn просто изменяет динамическую часть строки и возвращает ее.
До ES2018 теги шаблонных литералов имели синтаксические ограничения, связанные с escape-последовательностями. Обратная косая черта, сопровождаемая определенной последовательностью символов, обрабатывается как специальные символы: \x интерпретируется как экранирование шестнадцатеричного числа, \u интерпретируется как экранирование Юникода, \ сопровождается цифрой, интерпретируемой как восьмеричное экранирование. В результате такие строки, как «C:\xxx\uuu» или «\ubuntu», были интерпретируются как недопустимые escape-последовательности и приводили к ошибке SyntaxError.
ES2018 удаляет эти ограничения из тегированных шаблонов и вместо того, чтобы выдавать ошибку, представляет недопустимые escape-последовательности как undefined:
function fn(string, substitute) { console.log(substitute); // → escape sequences: console.log(string[1]); // → undefined } const str = 'escape sequences:'; const result = fn`${str} \ubuntu C:\xxx\uuu`;
Имейте в виду, что использование недопустимых escape-последовательностей в обычном шаблонном литерале по-прежнему вызывает ошибку:
const result = `\ubuntu`; // → SyntaxError: Invalid Unicode escape sequence
Поддержка Template Literal Revision
Chrome | Firefox | Safari | Edge |
---|---|---|---|
62 | 56 | 11 | No |
Chrome Android | Firefox Android | iOS Safari | Edge Mobile | Samsung Internet | Android Webview |
---|---|---|---|---|---|
62 | 56 | 11 | No | 8.2 | 62 |
Node.js:
- 8.3.0 (требует флаг —harmony)
- 8.10.0 (полная поддержка)
Завершение
Мы рассмотрели несколько ключевых функций, представленных в ES2018, включая асинхронную итерацию, свойства rest/spread, Promise.prototype.finally() и дополнения к объекту RegExp. Хотя некоторые из этих функций еще не полностью реализованы в некоторых браузерах, ими можно пользоваться благодаря транспортерам JavaScript, таким как Babel.
ECMAScript стремительно развивается, и новые функции появляются все чаще, поэтому ознакомьтесь со списком предложений, чтобы заранее знать все новые возможности. Есть ли какие-то новые функции, которые вас особенно порадуют? Поделитесь ими в комментариях!