JavaScript получает новый стандарт языка каждый год. Некоторые редакции представляют синтаксис, который меняет то, как программы пишутся или как они выполняются. Версия ECMAScript 2026 (ES17) представила набор полезных возможностей, которые мы рассмотрим в этой статье. Эти возможности добавляют API для задач, которые сейчас решаются с помощью небольших утилит, повторяющихся проверок или обходных путей, подверженных ошибкам.
В этой статье я сфокусируюсь на дополнениях, позволяющих удалить часть кода, сделать намерения более явными или предотвратить трудноуловимые ошибки.
❯ Построение карты по мере поступления значений
В статье про группировку массивов я показал простой способ группировки связанных вещей с помощью методов Object.groupBy() и Map.groupBy(). Оба метода работают с коллекцией, которая уже существует, но что если нам требуется обрабатывать потоковые данные?
Например, этот код группирует работников из асинхронного потока:
const employeesByDepartment = new Map(); for await (const employee of employeeStream) { if (!employeesByDepartment.has(employee.department)) { employeesByDepartment.set(employee.department, []); } employeesByDepartment .get(employee.department) .push(employee); }
Здесь ничего сложного, но последовательность из has(), set() и get() - это шаблонный код вокруг операции, которая нам действительно нужна - добавление нового работника в подразделение.
ECMAScript 2026 добавляет методы Map.prototype.getOrInsert() и Map.prototype.getOrInsertComputed().
Эти методы возвращают значение, соответствующее указанному ключу. При отсутствии ключа, добавляется новая сущность с ключом и указанным значением по умолчанию. Добавленное значение возвращается.
getOrInsertComputed() следует использовать, когда дефолтное значение создается лениво (lazily), имеет побочные эффекты или выделяет (allocate) мутабельный объект, такой как массив. С вычисляемой (computed) версией ранее приведенный код будет выглядеть так:
const employeesByDepartment = new Map(); for await (const employee of employeeStream) { employeesByDepartment .getOrInsertComputed(employee.department, () => []) .push(employee); }
Результат простой и компактный.
❯ Преобразование асинхронного итератора в массив
Асинхронный генератор полезен для скрытия пагинации. Его вызывающий (caller) может потреблять одну последовательность без знания того, где заканчивается одна страница и начинается другая:
async function* fetchAllIssues(url) { while (url) { const response = await fetch(url); if (!response.ok) { throw new Error(`Request failed: ${response.status}`); } const page = await response.json(); yield* page.items; url = page.next; } }
Для сбора результатов в массив обычно используется цикл:
const issues = []; for await (const issue of fetchAllIssues("/api/issues")) { issues.push(issue); }
Метод Array.fromAsync() может быть использован для выполнения этой операции напрямую:
const issues = await Array.fromAsync( fetchAllIssues("/api/issues"), );
Это короче и проще, не правда ли?
Несмотря на название, этот метод также принимает синхронные итераторы и массивоподобные (array-like) объекты.
Array.fromAsync() также принимает функцию связывания (mapping), и среда выполнения ждет связанного результата перед чтением следующего значения. Например, тот же код может быть использован для возврата только заголовка задачи с помощью функции связывания:
const issueTitles = await Array.fromAsync( fetchAllIssues("/api/issues"), (issue) => issue.title, );
Глядя на этот пример, можно подумать, что Array.fromAsync() запускает независимые операции одновременно, но это не так:
// Запросы выполняются один за другим const sequential = await Array.fromAsync(urls, fetchJson); // Все запросы инициируются сразу, не ожидая друг друга const concurrent = await Promise.all(urls.map(fetchJson));
Promise.all() следует использовать, когда независимые операции могут запускаться одновременно. А Array.fromAsync() — когда источник или шаг связывания могут быть асинхронными, и мы ждем ленивого упорядоченного потребления. Также помните о том, что возвращает этот метод — массив, содержащий каждый результат. Используйте цикл for await...of, когда хотите обработать большой поток инкрементально и избежать сбора потока, который может оказаться бесконечным.
❯ Сбор итераторов в одну ленивую последовательность
Иногда несколько итерируемых источников должны вести себя как одна продолжающаяся последовательность. Предположим, что приложение сначала проверяет встроенные роуты, затем регистрирует роуты с помощью плагинов и, наконец, перехватывает все роуты. Можно оформить все отдельным массивом, но это будет сразу потреблять каждый итератор и выделять новую коллекцию.
Генератор делает последовательность ленивой:
function* allRoutes() { yield* builtInRoutes; yield* pluginRoutes; yield fallbackRoute; }
Iterator.concat(), с другой стороны, делает тоже самое без кастомного генератора:
const allRoutes = Iterator.concat( builtInRoutes, pluginRoutes, [fallbackRoute], );
Сначала возвращаются (yields) встроенные роуты, затем — роуты плагинов и, наконец, резерв. Значения извлекаются только по мере продвижения потребителя по итератору; Iterator.concat() не собирает их сразу в новый массив. Каждый аргумент должен быть итерируемым объектом, поэтому в примере fallbackRoute обернут в массив.
Метод Iterator.concat() работает только с синхронными итераторами. Он не собирает асинхронные итераторы.
❯ Сохранение больших целых чисел в JSON
Чат-платформы часто выдают Snowflake ID в составе сообщений или других типов данных. Обычно это 64-битные целые числа, и их значение может превышать Number.MAX_SAFE_INTEGER, поэтому при парсинге JSON их точность может незаметно снижаться:
const payload = `{ "messageId": 1183028002140618753, "channel": "general" }`; const event = JSON.parse(payload); console.log(event.messageId); // 1183028002140618800
Видим, что значения не совпадают.
В ECMAScript 2026 функция восстановления (reviver function, второй параметр) JSON.parse() получает третий аргумент, называемый context. Если значением является неизмененный (unmodified) примитивный тип данных из парсера, context.source содержит исходный JSON-текст. Мы можем использовать его для анализа и преобразования текста в нужный тип, в данном случае — BigInt.
Вот тот же пример с использованием функции восстановления:
const event = JSON.parse( payload, (key, value, context) => { if (key === "messageId") { return BigInt(context.source); } return value; }, ); console.log(event.messageId); // 1183028002140618753n
К тому моменту, когда восстановитель (reviver) получает значение, Number уже теряет точность. Однако context.source позволяет коду игнорировать это поврежденное значение и создать BigInt из исходных цифр.
Сериализация имеет противоположную проблему: JSON.stringify() выбрасывает исключение при встрече с BigInt. Новый метод JSON.rawJSON() позволяет предоставить функции-заменителю (replacer function, второй параметр) JSON.stringify() валидный текст JSON для примитивного значения. Метод JSON.rawJSON() создает объект “сырого JSON”, содержащий текст JSON.
Пример использования JSON.stringify() и JSON.rawJSON():
const json = JSON.stringify( event, (key, value) => typeof value === "bigint" ? JSON.rawJSON(value.toString()) : value, ); console.log(json); // { "messageId":1183028002140618753, "channel":"general" }
Эти два API позволяют программе восстановить оригинальные цифры как BigInt и сериализовать их обратно в JSON без округления.
Использование этих API не означает, что вы должны преобразовывать каждое целое число в BigInt. Количество, цена и идентификатор могут отображаться в виде чисел JSON, но они не обязательно относятся к одному и тому же базовому типу JavaScript. Тем не менее, важно помнить, что парсер предоставляет context.source только для неизмененных примитивных значений, а JSON.rawJSON() принимает только допустимый текст JSON, представляющий примитивное значение.
❯ Преобразование байтов в формат Base64 или Hex и обратно
Двоичные данные часто подвергаются ненужным преобразованиям. Преобразование в байты и обратно не было естественным интерфейсом в языке. Думаю, Bun был первой средой выполнения JavaScript, которую я использовал, имевшей встроенный API для преобразования байтов в различные типы данных. К счастью, в стандарте ECMAScript 2026 года появились следующие функции:
Uint8Array.prototype.toBase64Uint8Array.prototype.toHexUint8Array.prototype.setFromHexи статичный вариантUint8Array.fromHexUint8Array.prototype.setFromBase64и статичный вариантUint8Array.fromBase64
Чем они могут быть полезны?
Представьте, что хотите создать URL-безопасный токен. Код может выглядеть так:
const bytes = crypto.getRandomValues(new Uint8Array(32)); const token = btoa(String.fromCharCode(...bytes)) .replaceAll("+", "-") .replaceAll("/", "_") .replace(/=+$/, "");
Программа начинает работу с байтов, преобразует их во временную строку, кодирует эту строку, а затем корректирует алфавит и заполнение (padding). Она работает, но ни один из этих промежуточных шагов не решает реальную задачу: закодировать эти байты как base64url.
Вместо этого, можно использовать метод Uint8Array.prototype.toBase64():
const bytes = crypto.getRandomValues(new Uint8Array(32)); const token = bytes.toBase64({ alphabet: "base64url", omitPadding: true, });
Обратное преобразование:
const decoded = Uint8Array.fromBase64(token, { alphabet: "base64url", });
Методы setFromBase64() и setFromHex() пишут в существующий массив и возвращают объект со счетчиками read и written. В отличие от fromBase64() и fromHex(), они полезны для управления выделением памяти, декодирования в заранее выделенный буфер или отслеживания объема входных данных. Для получения информации о доступных параметрах ввода и поддержке среды выполнения ознакомьтесь с документацией по этому методу.
Эти методы не заменяют TextEncoder или TextDecoder. Используйте эти API для преобразований между текстом и байтами; используйте новые методы Uint8Array для конвертаций между байтами и base64 или шестнадцатиричными (hexadecimal) представлениями.
❯ Распознавание объектов ошибок из разных областей
Конструкция instanceof Error кажется очевидным способом проверки того, является ли объект объектом Error. Однако она перестает быть надежной, когда значение поступает из другой области (realm) JavaScript, например, из iframe или контекста виртуальной машины Node.js. Каждая область имеет свой собственный конструктор Error, поэтому ошибка из другой области может не пройти проверку instanceof.
Попробуйте выполнить этот код в консоли браузера:
const iframe = document.createElement("iframe"); document.body.append(iframe); const otherError = new iframe.contentWindow.Error("Failure"); console.log(otherError instanceof Error); // false
Это вряд ли удивит многих опытных JavaScript-программистов, которых уже обманывали некоторые особенности JavaScript.
Решение — новый метод Error.isError(). Он выполняет встроенную проверку наличия внутреннего слота [[ErrorData]], вместо цепочки прототипов текущей области. Это делает его аналогом Array.isArray() для надежной проверки.
Если вы добавите console.log(Error.isError(otherError)) к предыдущему фрагменту кода, то увидите правильный результат.
Этот метод также может быть полезен в блоке catch, поскольку JS позволяет выбрасывать (throw) любое значение:
try { await runPlugin(); } catch (value) { const error = Error.isError(value) ? value : new Error(String(value), { cause: value }); reportError(error); }
Error.isError() следует использовать, когда необходимо определить, является ли значение настоящим объектом Error. Он не реагирует на обычные объекты со свойствами name и message.
❯ Точное суммирование чисел с плавающей запятой
Простой вызов функции reduce() может привести к потере информации при добавлении значений с плавающей запятой, и этот сбой оказывается более коварным, чем можно ожидать, поскольку результат не всегда выглядит очевидно неправильным. Рассмотрим библиотеку для работы с датчиками движения, которая применяет большое смещение калибровки для каждого устройства, добавляет небольшое показание, а затем удаляет смещение:
const readings = [1e16, 3.5, -1e16]; const total = readings.reduce( (sum, value) => sum + value, 0, ); console.log(total); // 4
Правильный ответ - 3,5 — это фактическое значение после вычитания смещения. Вблизи 1e16 соседние представимые числа находятся на расстоянии двух единиц друг от друга. Следовательно, точное значение 1e16 + 3,5 округляется до 1e16 + 4, и вычитание смещения дает 4, вместо 3,5. В результате получается не ошибка, а правдоподобно выглядящее неверное число, что и делает подобные ошибки трудноуловимыми при проверке кода.
Вот где пригодится метод Math.sumPrecise(). Он использует более точный алгоритм суммирования, поэтому переключение с .reduce() на Math.sumPrecise() даст верный ответ:
const total = Math.sumPrecise(readings); console.log(total); // 3.5
Рассматриваемый метод принимает итератор чисел. Он не приводит строки или BigInt к числам. При передаче пустого итератора или итератора, содержащего только -0, возвращается -0. Название может ввести в заблуждение: Precise не означает точность десятичных арифметических вычислений. Знакомый всем результат не меняется:
console.log(Math.sumPrecise([0.1, 0.2])); // 0.30000000000000004
Оба входных значения уже представляют собой двоичные аппроксимации с плавающей запятой. Метод Math.sumPrecise() уменьшает дополнительную погрешность, возникающую при их суммировании; он не меняет способ представления чисел в JavaScript. Это делает его полезным для агрегирования числовых данных, но не является полным решением для работы с деньгами, например. Для финансовых операций следует использовать соответствующий десятичный тип или целочисленное представление.
❯ Заключение
Новшества ECMAScript 2026 не меняют язык существенно. Однако несколько распространенных задач теперь имеют более простые и легкие решения: получение или создание значения Map, сохранение исходных цифр из JSON, кодирование байтов без их текстового представления, сбор асинхронной последовательности, распознавание реальной ошибки, суммирование чисел с минимальными потерями и ленивое объединение итерируемых объектов. Ни один из этих API сам по себе не может кардинально изменить приложение. Однако они могут заменить шаблонный или чрезмерно сложный код, а также код, в котором легко допустить небольшие ошибки.
В любом случае, JavaScript стал еще удобнее в использовании.
Перед использованием этих новшеств в продакшене обязательно проверьте поддержку среды выполнения. Стандарт определяет язык, но каждая среда выполнения следует своему собственному графику релизов.
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩
