Найти в Дзене
Dev Articles

ECMAScript 2025: лучшие нововведения в JavaScript

Последняя спецификация JavaScript стандартизирует хорошо сбалансированный и продуманный набор функций, включая встроенный глобальный объект Iterator, новые методы Set, улучшения регулярных выражений и многое другое. Обновление спецификации JavaScript за этот год охватывает широкий спектр изменений. Наиболее значимым дополнением является новый встроенный объект Iterator и его функциональные методы. Другие обновления включают новые методы Set, прямой импорт модуля JSON, улучшения регулярных выражений, новый метод Promise.try для упрощения цепочек промисов и новый массив типа Float16Array. Давайте ознакомимся с новыми функциями JavaScript и рассмотрим, чем они могут нам помочь. Начнем с наиболее масштабного дополнения — нового объекта Iterator и его статическими и прототипными методами для работы с итераторами. Начнём со встроенного глобального объекта Iterator. Этот объект позволяет обернуть существующие итерируемые объекты новым интерфейсом, предоставляющим функциональные методы, такие
Оглавление

Последняя спецификация JavaScript стандартизирует хорошо сбалансированный и продуманный набор функций, включая встроенный глобальный объект Iterator, новые методы Set, улучшения регулярных выражений и многое другое.

Обновление спецификации JavaScript за этот год охватывает широкий спектр изменений. Наиболее значимым дополнением является новый встроенный объект Iterator и его функциональные методы. Другие обновления включают новые методы Set, прямой импорт модуля JSON, улучшения регулярных выражений, новый метод Promise.try для упрощения цепочек промисов и новый массив типа Float16Array.

Давайте ознакомимся с новыми функциями JavaScript и рассмотрим, чем они могут нам помочь.

Объект Iterator

Начнем с наиболее масштабного дополнения — нового объекта Iterator и его статическими и прототипными методами для работы с итераторами.

Начнём со встроенного глобального объекта Iterator. Этот объект позволяет обернуть существующие итерируемые объекты новым интерфейсом, предоставляющим функциональные методы, такие как map и filter.

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

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

Следует подчеркнуть, что массивы JavaScript, хотя и имеют встроенные функциональные методы, работают путем полного перебора всего массива и создания промежуточных "временных массивов" для каждой операции. Таким образом, каждый раз, когда вы вызываете map или filter, это подразумевает создание нового массива на каждом этапе. Объект Iterator работает аналогично другим функциональным API (например, Java Streams), где каждый метод обрабатывается поэлементно, а новый массив создается только тогда, когда мы доходим до последней операции.

Несколько коротких примеров продемонстрируют, как это работает. Допустим, у нас есть массив умных людей:

let smarties = ["Plato", "Lao Tzu", "St. Augustine", "Ibn Arabi", "Max Planck", "David Bohm"];

Если мы хотим преобразовать этот массив, используя улучшенную функцию map объекта Iterator, мы можем сделать следующее:

Iterator.from(smarties).map(x => x.length).toArray(); // [5, 7, 13, 9, 10, 10]

Мы используем from(), чтобы создать объект Iterator, а toArray — завершающая операция, возвращающая результат. Напоминаем, что вы могли бы сделать это и с помощью Array.map, однако реализация метода Iterator.map() ленива. Чтобы увидеть разницу, давайте дополнительно отфильтруем результат по длине:

Iterator.from(smarties).map(x => x.length).filter(x => x < 10).toArray(); // [5, 7, 9]

Добавление оператора take() в качестве заверщающей операции указыает, что нас интересуют только первые два элемента, соответствующие нашим критериям. Это значит, что будет обработана только та часть исходного массива, которая необходима для удовлетворения критериев:

Iterator.from(smarties).map(x => x.length).filter(x => x < 10).take(2).toArray() // [5, 7]

Далее посмотрим, как объект Iterator оборачивает итератор не являющийся массивом. Начнём с простого генератора, производящего тот же вывод:

function* getSmartiesNames() {
yield "Plato";
yield "Lao Tzu";
yield "St. Augustine";
yield "Ibn Arabi";
yield "Max Planck";
yield "David Bohm";
}

Теперь обернём его объектом Iterator:

Iterator.from(getSmartiesNames())
.map(name => name.length)
.filter(length => length < 10)
.take(2)
.toArray();

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

Новые методы класса Set

Класс Set используется в JavaScript реже, чем в других языках, потому что он появился позже, а объект Array очень распространён и гибок. Однако класс Set предлагает гарантированную уникальную несортированную коллекцию, сохраняющую порядок вставки, и линейное время операций has и delete. Добавление новых методов, основанных на теории множеств, делает его ещё полезнее.

Эти методы легко понять, особенно если вы уже использовали их в языке вроде SQL. Они весьма удобны.

Метод intersection находит элементы, общие для двух множеств:

let set = new Set(["A","B","C"]);
let set2 = new Set(["C","D","E"]);
set.intersection(set2); // возвращает {‘C’}

Заметим, что элемент 'C' присутствует в результирующем Set только один раз, поскольку данные множества не содержат дубликатов.

set.difference "вычитает" правое множество из левого:

set.difference(set2); // возвращает {"A","B"}

Следующий метод возвращает нам всё, что было в первом множестве, но отсутствует во втором:

set.symmetricDifference(set2); // возвращает {'A', 'B', 'D', 'E'}

Заметьте, что элемент 'C' пропал в данном примере, так как он был единственным общим элементом обоих множеств.

Спецификация также включает три новых метода для проверки отношений между множествами Set. Их названия достаточно понятны сами по себе:

  • isSubsetOf проверяет, содержится ли первое множество целиком во втором.
  • isSupersetOf проверяет, содержит ли первое множество второе целиком.
  • isDisjointFrom проверяет, различны ли два множества полностью.

Импорт JSON как модуля

ES2025 стандартизует возможность импорта JSON непосредственно как модуля. Это устраняет необходимость вручную загружать файлы JSON или использовать этап сборки для этой цели. Импортирование JSON становится настолько простым, насколько только можно пожелать:

import appConfig from './data.json' with { type: 'json' };

Содержимое appConfig теперь представляет собой фактический распаршенный объект JSON (не строку). Эта возможность доступна только для локальных файлов, а не удалённых ресурсов.

Ключевое слово with ссылается на атрибуты модулей, указанные в спецификации. Сейчас это ключевое слово используется исключительно для определения типа модуля JSON как JSON (оно было введено, чтобы избежать возможности интерпретации содержимого как JavaScript-кода). В будущем with может использоваться для обозначения других атрибутов модулей.

Улучшения регулярных выражений

Новое статическое свойство RegExp.escape позволяет предотвратить атаки при помощи инъекций в строки регулярных выражений. Оно экранирует любые символы, имеющие специальное значение в контексте регулярного выражения, аналогично подобным функциям предотвращения SQL-инъекций в произвольных SQL-строках. Это позволяет безопасно обрабатывать регулярные выражения, полученные из ненадёжных источников.

Представьте ситуацию, когда ваша система принимает вводимые пользователем данные и объединяет их с регулярными выражениями. Пользователь со злым умыслом мог добавить выражение, которое вызвало бы ошибку синтаксиса. Просто запустив сначала RegExp.escape(userInput), вы предотвращаете такую проблему.

Другое улучшение описано в спецификации следующим образом: "добавлена синтаксическая конструкция для включения и отключения флагов-модификаторов прямо в теле регулярных выражений."

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

Теперь вы можете определить секции, которые применяют собственные флаги. Представим, что у вас есть фрагменты некоторых известных песен группы The Beatles:

const beatlesLyrics = [
"All You Need Is Love",
"Can't Buy Me Love",
"A Hard Day's Night",
"Eight Days a Week",
"Yesterday, all my troubles seemed so far away.",
"Love Me Do"
];

Предположим, мы хотим найти слово "love" без учёта регистра, а слово "Day" с учётом регистра. До сих пор хорошего способа сделать это не существовало, но теперь это довольно просто:

const beatlesSearchRegex = /(?i:Love)|(?-i:Day)/;

Мы ищем совпадение по двум частям выражения в скобках (вопросительный знак указывает на то что мы исключаем из ответа) с независимыми друг от друга флагами. В данном случае мы применяем нечувствительность к регистру (i) к слову "Love", но не к слову "Day". Поскольку слово "day" написано строчными буквами, мы исключаем из ответа "Yesterday" в этом наборе строк.

Новый метод Promise.try

Согласно спецификации, новый метод Promise.try предназначен для "вызова функций, которые могут возвращать или не возвращать объект Promise, гарантируя, что результатом всегда будет объект Promise".

Это удобное дополнение. Оно позволяет обернуть цепочку промисов методом Promise.try(), так что любые синхронные ошибки, всплывающие по стеку, будут перехвачены блоком .catch(). Это значит, что вы можете пропустить дополнительный блок try{} и использовать ваш блок catch() для обработки как асинхронных, так и синхронных ошибок:

Promise.try(() => thisMightThrowSyncError("foobar"))
.then(val => console.log("Всё в порядке", val))
.catch(err => console.error("Ошибка:", err.message));

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

Promise.try(() => possibleSyncError("foo"))
.then(processedStep1 => possibleAsyncError("bar"))
.then(processedStep2 => anotherPossibleAsyncError("baz"))
.then(finalResult => console.log("Успех", finalResult))
.catch(error => console.error("Все ошибки приходят сюда",
error.message, "Я ошибка"));

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

function fetchData(key) {
if (cache[key]) {
return cache[key];
} else {
return new Promise(resolve => {
setTimeout(() => { data = { id: key, name: New Item ${key}, source: "DB" };
cache[key] = data;
}, 500);
});
}
}

Независимо от того, сработает это или нет, Promise.try отработает корректно и направит все ошибки в блок catch:

Promise.try(() => fetchData("user:1"))
.then(data => console.log("Результат 1 (из кеша):", data))
.catch(error => console.error("Ошибка 1:", error.message));

Типизированный массив Float16Array

Этот апдейт описан в спецификации как "добавлен новый типизированный массив Float16Array вместе с соответствующими методами DataView.prototype.getFloat16, DataView.prototype.setFloat16 и Math.f16round".

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

JavaScript не добавил новый скалярный числовой тип. По-прежнему существует только тип Number, но теперь доступен Float16Array для хранения коллекций такого типа. Тип JavaScript Number использует двойную точность формата 64-битных чисел с плавающей точкой; однако в случаях, когда используются большие объёмы чисел, допустима меньшая точность (например, веса и смещения нейронных сетей), использование 16 бит может значительно повысить оптимизацию.

Добавленные методы предназначены для работы с сырыми бинарными буферами данных и Float16Array:

  • DataView.prototype.getFloat16 позволяет считывать число типа Float16 из представления данных (результатом будет представление числа в виде 64-разрядного значения):
    newView.getFloat16(0, true);
  • DataView.prototype.setFloat16 позволяет записывать число типа Float16 в представление данных:
    view.setFloat16(0, 3.14159, true);
  • Math.f16round проверяет, потеряется ли точность при конвертации числа в тип Float16.

Заключение

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

Оригинал статьи читайте по ссылке