12k
All articles

Что нового в ECMAScript 2026

ES2026 добавляет семь функций JavaScript: Array.fromAsync, Math.sumPrecise, base64 и hex для Uint8Array, доступ к исходному тексту в JSON.parse и getOrInsert.

OpenReplay Team
OpenReplay Team
Что нового в ECMAScript 2026

Ecma International утвердила спецификацию языка ECMAScript 2026 30 июня 2026 года, и она содержит ровно семь новых возможностей: Array.fromAsync, Error.isError, Math.sumPrecise, методы для работы с base64 и hex у Uint8Array, Iterator.concat, доступ к исходному тексту в JSON.parse и getOrInsert для Map и WeakMap.

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

Если вы недавно искали информацию про ES2026, вы наверняка нашли списки, которые противоречат друг другу, причём в некоторые из них включены Temporal или ключевое слово using. Эти списки неверны, и данная статья объясняет почему, а затем разбирает каждую из семи возможностей, которые действительно вошли в стандарт: какую проблему она решает, какой код она заменяет и тот однострочник, который приходит на замену.

Авторитетным источником является объявление Ecma об утверждении, которое подтверждает, что ES2026 — это 17-я редакция ECMA-262, а также таблица завершённых предложений TC39, где ровно семь строк имеют ожидаемый год публикации 2026.

Ключевые выводы

  • ES2026 был утверждён 30 июня 2026 года и добавляет ровно семь возможностей; Temporal и Explicit Resource Management в их число не входят.
  • Math.sumPrecise([1e17, 1, -1e17]) возвращает 1, тогда как сумма тех же значений через reduce даёт 0.
  • Uint8Array получает встроенные toBase64(), toHex(), fromBase64() и fromHex(), что позволяет избавиться от распространённой зависимости.
  • Reviver-функция в JSON.parse теперь получает третий аргумент, открывающий доступ к исходному тексту, а JSON.rawJSON() решает проблему на стороне сериализации, поэтому BigInt теперь может пройти полный цикл через JSON без потерь.
  • Map.prototype.getOrInsert(key, value) возвращает существующее значение, если ключ уже присутствует; ваше значение по умолчанию вставляется только при отсутствии ключа.

Что не попало в ES2026?

Temporal и Explicit Resource Management (using / await using) не входят в ES2026. В списке завершённых предложений TC39 у обоих указан ожидаемый год публикации 2027, наряду с Atomics.pause и Joint Iteration. Все четыре достигли stage 4 — финальной ступени процесса TC39 — уже после срока отсечения для ES2026. Публикации конца 2025 года, вышедшие до утверждения, широко предсказывали их появление в 2026-м, откуда и берутся противоречивые списки возможностей. Проверить это может кто угодно: отфильтруйте столбец Expected Publication Year в таблице завершённых предложений. Семь предложений содержат 2026, и ни одно из них не является Temporal.

Array.fromAsync

Array.fromAsync принимает асинхронный итератор и возвращает массив, обёрнутый в промис. Он также принимает синхронный итератор, значениями которого являются промисы, объект, похожий на массив, и необязательную функцию отображения — точно так же, как Array.from. До сих пор сбор значений асинхронного итератора означал цикл for await с добавлением элементов в изменяемый массив:

const arr = [];
for await (const n of asyncGen(1, 3)) {
  arr.push(`page ${n}`);
}

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

const arr = await Array.fromAsync(asyncGen(1, 3), (n) => `page ${n}`);
// [ 'page 1', 'page 2', 'page 3' ]

Error.isError

Error.isError(value) возвращает true только тогда, когда перехваченное значение является настоящим объектом Error. JavaScript позволяет выбросить что угодно, а instanceof Error начинает вводить в заблуждение, как только значения пересекают границы iframe или realm, поскольку у каждого realm есть свой собственный конструктор Error. Блок catch, условие которого опирается на instanceof Error, может незаметно пропустить ветку, отображающую полезное сообщение, и записи сессий с такими багами имеют знакомую картину: пользователь застрял на экране, который ничего не сообщает, тогда как под ним произошло реальное исключение. Error.isError устраняет этот пробел.

try {
  throw new Error("session upload failed");
} catch (error) {
  Error.isError(error); // true
}

try {
  throw "session upload failed";
} catch (error) {
  Error.isError(error); // false
}

Math.sumPrecise

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

const values = [1e17, 1, -1e17];

values.reduce((a, b) => a + b, 0); // 0
Math.sumPrecise(values);           // 1

Вариант с reduce полностью теряет 1. Помимо точности, это также избавляет от шаблонного редьюсера для суммирования.

Uint8Array toBase64, toHex, fromBase64 и fromHex

ES2026 добавляет в Uint8Array встроенные методы toBase64() и toHex(), а также статические Uint8Array.fromBase64() и Uint8Array.fromHex() для декодирования. Раньше преобразование двоичных данных в текст означало подключение библиотеки или написание кодирования вручную; теперь это зависимость, которую большинство кодовых баз может удалить.

const value = new Uint8Array([79, 112, 101, 110, 82, 101, 112, 108, 97, 121]);

const valueBase64 = value.toBase64(); // 'T3BlblJlcGxheQ=='
const decoded = Uint8Array.fromBase64(valueBase64);

new TextDecoder().decode(decoded); // 'OpenReplay'

Iterator.concat

Iterator.concat() последовательно объединяет итераторы в цепочку. До сих пор единственным способом сделать это был написанный вручную генератор, который проходил по каждому источнику и передавал управление через yield*:

function* combine(...iterators) {
  for (const source of iterators) {
    yield* source;
  }
}

Теперь это один вызов, а между итераторами можно вставлять обычные массивы значений. [300] в середине примера ниже — это обычный массив, и его не нужно оборачивать в Iterator.from.

const iOne = Iterator.from([100, 200]);
const iTwo = Iterator.from([400, 500]);

const combined = Iterator.concat(iOne, [300], iTwo);
Array.from(combined); // [ 100, 200, 300, 400, 500 ]

Доступ к исходному тексту в JSON.parse

До ES2026 полный цикл преобразования JSON в JavaScript был лишён точности в обоих направлениях. Разбор литерала большого целого числа незаметно его округляет, а сериализация BigInt выбрасывает исключение:

JSON.parse("9007199254740993");   // 9007199254740992
JSON.stringify(9007199254740993n); // TypeError: Do not know how to serialize a BigInt

Предложение о доступе к исходному тексту в JSON.parse решает проблему с обеих сторон. JSON.parse теперь передаёт своей reviver-функции третий аргумент, содержащий нетронутый исходный текст source, который вы можете преобразовать самостоятельно без потери цифр. На стороне сериализации JSON.rawJSON() позволяет replacer-функции вывести значение буквально:

JSON.parse("9007199254740993", (key, value, { source }) => BigInt(source));
// 9007199254740993n

JSON.stringify(9007199254740993n, (key, value) => JSON.rawJSON(value));
// 9007199254740993

Теперь BigInt может пройти полный цикл через JSON, не теряя цифр и не вызывая исключения.

Map.prototype.getOrInsert и WeakMap.prototype.getOrInsert

Предложение Upsert добавляет getOrInsert(key, value) как в Map, так и в WeakMap, поэтому вам больше не нужно вызывать has(), а затем set(). Семантика здесь важна: getOrInsert возвращает существующее значение, если ключ уже присутствует. Вставка происходит — и ваше значение по умолчанию возвращается — только тогда, когда ключ отсутствует.

const limits = new Map();
limits.set("uploads", 25);

limits.getOrInsert("exports", 10);   // 10  (inserted)
limits.getOrInsert("uploads", 100);  // 25  (existing value wins)

Обратите внимание, что 100 отбрасывается, поскольку "uploads" уже был установлен. Рассматривайте второй аргумент как значение по умолчанию, а не как перезапись.

Что можно удалить прямо сейчас?

ES2026 — это релиз-уборка: каждая из семи возможностей избавляет от паттерна, который у вас, скорее всего, есть в продакшене сегодня. Зависимость-хелпер для base64, редьюсер для суммирования, защитные блоки has()-затем-set(), генератор объединения через yield* и цикл сбора for await — у всего этого теперь есть прямые встроенные замены. Доступность в рантаймах различается от движка к движку, поэтому перед отказом от полифилов проверьте справочник стандартных встроенных объектов на MDN, чтобы узнать актуальную картину поддержки каждого метода. Затем начните с самой простой победы в вашей кодовой базе — для большинства команд это замена проверок существования ключа в Map на getOrInsert.

Часто задаваемые вопросы

В чём разница между Array.fromAsync и Promise.all?

Array.fromAsync ожидает значения последовательно, одно за другим, и принимает асинхронные итерируемые объекты, синхронные итерируемые объекты, отдающие промисы, и объекты, похожие на массив. Promise.all выполняет итерируемый набор промисов параллельно и отклоняется сразу же, как только отклоняется любой из промисов. Используйте Promise.all для параллельной работы, а Array.fromAsync — когда важен порядок или когда источником является асинхронный итератор. Оба возвращают промис, который разрешается массивом.

Возвращает ли Error.isError значение true для TypeError и других подклассов Error?

Да. Error.isError возвращает true для любого настоящего подкласса Error, включая TypeError, RangeError, SyntaxError и пользовательские классы, наследующие Error, поскольку движки проверяют внутреннюю метку (brand), которую несут реальные ошибки, а не цепочку прототипов. В браузерах DOMException тоже считается реальной ошибкой. Метод возвращает false для объектов, которые лишь имитируют ошибки, например для обычных объектов со свойством message или объектов, созданных через Object.create(Error.prototype).

Может ли Uint8Array.prototype.toBase64 генерировать URL-безопасный base64?

Да. toBase64 принимает объект опций со свойством alphabet, установленным в 'base64' (значение по умолчанию, использующее + и /) или 'base64url' (использующее - и _), а также логический флаг omitPadding, который убирает завершающие символы =. Uint8Array.fromBase64 принимает ту же опцию alphabet и опцию lastChunkHandling ('loose', 'strict' или 'stop-before-partial'), которая управляет тем, как декодируется финальный фрагмент короче четырёх символов.

Как избежать вычисления «дорогого» значения по умолчанию при использовании getOrInsert?

Используйте getOrInsertComputed(key, callback), который то же предложение Upsert добавляет и в Map, и в WeakMap. Он вызывает колбэк для получения значения только в том случае, если ключ отсутствует, а затем вставляет и возвращает результат; если ключ уже существует, колбэк никогда не выполняется и возвращается существующее значение. Обычный getOrInsert всегда вычисляет свой второй аргумент до вызова, даже когда ключ уже присутствует.

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.