ES2018 не выглядит таким громким релизом, как ES6, но именно в таких версиях язык становится удобнее в повседневной работе. Здесь появились улучшения, которые быстро превращаются в привычку: object rest/spread, Promise.prototype.finally(), асинхронные итераторы и более сильные регулярные выражения.

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

Ниже разберем изменения ES2018 на примерах, которые реально встречаются в приложениях и инструментах.

Object rest и spread

В ES6 мы уже получили rest и spread для массивов. ES2018 перенес ту же идею на объекты.

Rest для объектов

rest помогает забрать часть полей отдельно, а остальные собрать в новый объект:

const user = {
  id: 1,
  name: "Alex",
  role: "author",
  city: "Madrid",
};

const { id, ...publicProfile } = user;

console.log(id); // 1
console.log(publicProfile);
// { name: "Alex", role: "author", city: "Madrid" }

Это удобно, когда нужно убрать служебные поля перед отправкой данных в UI или API.

Spread для объектов

spread разворачивает свойства объекта в новый объект. Это особенно полезно для иммутабельных обновлений:

const user = {
  name: "Alex",
  city: "Madrid",
};

const updatedUser = {
  ...user,
  city: "Barcelona",
  role: "author",
};

console.log(updatedUser);
// { name: "Alex", city: "Barcelona", role: "author" }

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

Асинхронная итерация

for await...of позволяет последовательно читать асинхронный источник данных. Чаще всего его используют с async iterable, но он также умеет ждать значения из массива промисов.

const requests = [
  Promise.resolve("first"),
  Promise.resolve("second"),
  Promise.resolve("third"),
];

async function printInOrder() {
  for await (const value of requests) {
    console.log(value);
  }
}

printInOrder();
// first
// second
// third

Важно понимать ограничение: for await...of ждет значение перед переходом к следующей итерации. Если нужна именно параллельная загрузка, сначала запускай все операции, а потом собирай результат через Promise.all.

Promise.prototype.finally()

До ES2018 одинаковый код очистки приходилось дублировать и в then, и в catch. finally() убирает это повторение:

fetch("https://api.github.com/users/oleksiimyzgin")
  .then((response) => response.json())
  .then((data) => {
    console.log(data.login);
  })
  .catch((error) => {
    console.error(error);
  })
  .finally(() => {
    console.log("request finished");
  });

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

Новые возможности регулярных выражений

ES2018 заметно усилил RegExp. Самые полезные изменения: lookbehind, именованные группы, Unicode property escapes и флаг s.

Lookbehind

lookahead проверяет, что нужная строка идет после текущего совпадения. lookbehind делает то же самое, но в обратную сторону.

const lookAhead = /Java(?=Script)/;
const lookBehind = /(?<=Java)Script/;

console.log(lookAhead.test("JavaScript")); // true
console.log(lookAhead.test("Java Script")); // false

console.log(lookBehind.test("JavaScript")); // true
console.log(lookBehind.test("TypeScript")); // false

Отрицательный lookbehind тоже работает:

const notAfterJava = /(?<!Java)Script/;

console.log(notAfterJava.test("JavaScript")); // false
console.log(notAfterJava.test("TypeScript")); // true

Именованные группы

С именованными группами результат регулярного выражения становится заметно понятнее:

const datePattern = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = datePattern.exec("2019-04-10");

console.log(match.groups.year); // "2019"
console.log(match.groups.month); // "04"
console.log(match.groups.day); // "10"

Это особенно полезно в сложных шаблонах, где доступ к match[1] и match[2] быстро превращается в источник ошибок.

Unicode property escapes

\\p{...} и \\P{...} позволяют описывать классы символов через свойства Unicode:

const asciiOnly = /^\p{ASCII}+$/u;
const greekOnly = /^\p{Script=Greek}+$/u;
const emojiOnly = /^\p{Emoji}+$/u;

console.log(asciiOnly.test("ABC123")); // true
console.log(asciiOnly.test("ABC🙃")); // false

console.log(greekOnly.test("ελληνικά")); // true
console.log(greekOnly.test("hello")); // false

console.log(emojiOnly.test("🙃🙃")); // true
console.log(emojiOnly.test("A")); // false

Флаг s

Флаг s включает режим dotAll: точка начинает совпадать и с символом новой строки.

const withoutDotAll = /hi.welcome/;
const withDotAll = /hi.welcome/s;

console.log(withoutDotAll.test("hi\nwelcome")); // false
console.log(withDotAll.test("hi\nwelcome")); // true

Итог

ES2018 не меняет язык радикально, но сильно улучшает ежедневную работу. object rest/spread делают обновление данных чище, finally() убирает дублирование, for await...of упрощает работу с асинхронными потоками, а RegExp впервые становятся заметно удобнее для серьезных задач.

Если выбираешь, что внедрять в старом коде в первую очередь, начинай с object rest/spread и finally() — изменения минимальные, а читаемость растет сразу.