Найти в Дзене
Chris Roylance

ECMAScript 2025: лучшие новые возможности JavaScript

Оглавление

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

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

Давайте рассмотрим новейшие возможности JavaScript и то, что вы можете с ними сделать.

Объект итератора

Начнем с самого масштабного дополнения, которое в спецификации описывается как «новый глобальный итератор со связанными статическими и прототипными методами для работы с итераторами».

Здесь всё начинается со встроенного глобального итератора. (Если нажать F12 и открыть консоль JavaScript в Devtools, то можно увидеть, что объект там есть.) Этот объект позволяет обернуть существующие итерируемые объекты в новый интерфейс, предоставляющий функциональные операторы, такие как 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.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";
}

Теперь мы помещаем это в итератор:

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

Обратите внимание, что теперь массив и генератор имеют одинаковый функциональный API, и оба они выигрывают от базовой оптимизации. (Конечно, эти примеры намеренно очень просты.)

Новые методы Set

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

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

Set.intersection находит элементы, которые одинаковы в двух множествах:

let set =new Set(["A","B","C"]);
let set2 = new Set(["C","D","E"]);
set.intersection(set2); // yields {‘C’}

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

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

set.difference(set2); // yields {“A”,”B”}

Это оставляет нам то, что было в первом наборе, но не было во втором:

set.symetricDifference(set2); // yields {'A', 'B', 'D', 'E'}

Обратите внимание, что элемент «C» в этом примере был опущен, поскольку он был единственным общим для обоих наборов.

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

  • 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. Это позволяет безопасно использовать ненадёжные строки регулярных выражений.

Представьте, что часть вашей системы обрабатывает пользовательский ввод и объединяет его с регулярными выражениями. Злонамеренный (или хитрый) пользователь может добавить регулярные выражения, которые приведут к синтаксическим ошибкам. Однако просто выполните сначала RexExp.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("All is well", val))
.catch(err => console.error("Error:", err.message));

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

Promise.try(() => possibleSyncError("foo"))
.then(processedStep1 => possibleAsyncError("bar"))
.then(processedStep2 => fanotherPossibleAssyncError("baz"))
.then(finalResult => console.log("Success", finalResult))
.catch(error => console.error("All errors arrive here",
error.message, "I am an error"));

Ещё одно преимущество 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("Result 1 (from cache):", data))
.catch(error => console.error("Error 1:", error.message));

Float16Array TypedArray

Это обновление описано в спецификации как «добавленный новый тип Float16Array TypedArray, а также связанные с ним методы 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 продолжают прилагать усилия по включению в спецификацию полезных и актуальных новых языковых функций. Этот набор новых стандартизированных функций выглядит продуманным и сбалансированным и предполагает широкий спектр применения, продемонстрированный здесь.