Семь встроенных функций JavaScript, заменяющих сторонние библиотеки
Как функция Temporal с использованием деклараций, операций вставки/обновления в Map, функции Math.sumPrecise, помощников-итераторов, методов множеств и функции Promise.withResolvers устраняет распространённые шаблонные элементы JavaScript.
Раньше для запуска серьезного проекта на JavaScript необходимо было устанавливать библиотеку для работы с датами, инструменты для обработки коллекций и помощники для группировки данных, а затем оборачивать каждый загружаемый ресурс в защитные блоки try...finally. За последние несколько циклов выпусков ряд предложений TC39, которые обсуждались многие годы, стали частью стандарта и появились в браузерах типа Evergreen, Node.js, Bun и Deno. В этом руководстве рассматриваются семь из этих предложений, проблемы, которые они решают, и на что следует обратить внимание перед удалением зависимости.
Примечание относительно поддержки перед началом работы: эти функции появились в движках в разное время, причем некоторые из них совсем недавние. Проверьте текущие данные о совместимости для выбранных вами браузеров и версий среды выполнения, а также используйте полифилы или старые библиотеки там, где поддержка еще не гарантирована.
Temporal: настоящий API для работы с датами и временем
Любой, кто использовал встроенный объект Date хотя бы несколько минут, сталкивался с его проблемами:
- Месяцы идут с нуля (
0— январь,11— декабрь), однако номера дней в месяце начинаются с1. - Экземпляры
Dateявляются изменяемыми, поэтому передача их в вспомогательную функцию может незаметно изменить значение у исходного объекта. - Для преобразования часовых поясов и учета времени летнего/зимнего периода требуется сложные ручные вычисления или использование библиотек вроде date-fns, Day.js или Luxon.
Temporal — это новый неизменяемый API для работы с датами и временем, предназначенный заменить Date. Он не удаляет Date из языка, поскольку существующий код продолжает работать, но новый код уже не нуждается в нем.
Отдельные типы для отдельных концепций
Ключевое решение при проектировании Temporal заключается в том, чтобы представлять разные идеи через отдельные типы:
Temporal.PlainDate: дата по календарю без указания времени и часового пояса, например дата рождения.Temporal.PlainTime: время, указанное по настенным часам без даты, например будильник на 07:00.Temporal.ZonedDateTime: точный момент, связанный с конкретным часовым поясом и календарем.
В приведённом ниже фрагменте считывается текущее время в Нью-Йорке, к простой дате добавляются 14 дней, а разница между двумя датами измеряется в месяцах и днях. Обратите внимание, что разница запрашивается с параметром largestUnit: 'month', благодаря чему результат представлен в виде месяцев плюс дней, а не простого количества дней.
// 1. Getting current time in a specific time zone
const meetingTime = Temporal.Now.zonedDateTimeISO('America/New_York');
console.log(meetingTime.toString());
// e.g. "2026-09-04T12:09:28-04:00[America/New_York]"
// 2. Safe, readable date math
const deadline = Temporal.PlainDate.from('2026-09-01');
const followUp = deadline.add({ days: 14 });
console.log(followUp.toString()); // "2026-09-15"
// 3. Calculating the exact difference between two dates
const start = Temporal.PlainDate.from('2026-01-01');
const end = Temporal.PlainDate.from('2026-09-04');
const diff = start.until(end, { largestUnit: 'month' });
console.log(`${diff.months} months and ${diff.days} days`);
// "8 months and 3 days"
Каждый объект Temporal является неизменяемым. Вызов метода add() никогда не изменяет исходный объект; он возвращает новый объект. Для многих проектов это означает, что библиотека для работы с датами может находиться вне пакета. Перед миграцией ознакомьтесь с распространенными ловушками, связанными с Temporal.
Явное управление ресурсами с использованием using
Частой причиной утечек памяти и застревания блокировок является забывчивость закрытия того, что было открыто:
- файловый хендл, который остается открытым после чтения
- соединение с базой данных, которое не закрывается при возникновении ошибки в API-маршруте
- слушатели событий или рабочие потоки, которые никогда не отсоединяются
Традиционным решением является конструкция try...finally. Однако при использовании трех ресурсов в одной функции это быстро превращается в глубоко вложенные шаблонные фрагменты кода. Для явного управления ресурсами используется декларация using вместе с двумя известными символами — Symbol.dispose и Symbol.asyncDispose.
Как происходит освобождение ресурсов
Когда значение объявляется с помощью using, движок вызывает его метод [Symbol.dispose]() сразу после выхода из окружающего блока, независимо от того, завершился ли блок нормально или была возбуждена ошибка. Если несколько ресурсов объявлены в одном области видимости, они освобождаются в обратном порядке их объявления, что полностью соответствует действию вложенных блоков try...finally.
В примере DatabaseSession реализует механизм утилизации, а runReport объявляет сессию с использованием using. Порядок записей в журнале показывает, что процедура очистки выполняется после выполнения запроса и завершения функции:
// Define a resource that knows how to clean itself up
class DatabaseSession {
constructor(dbName) {
this.connection = `Connected to ${dbName}`;
console.log("Database opened.");
}
query(sql) {
return `Results for: ${sql}`;
}
// Built-in disposal hook
[Symbol.dispose]() {
console.log("Database connection closed automatically!");
}
}
// Using the resource
function runReport() {
using session = new DatabaseSession("AnalyticsDB");
const data = session.query("SELECT * FROM metrics");
console.log(data);
// When runReport finishes (or if it throws),
// session[Symbol.dispose]() runs immediately.
}
runReport();
// Logs:
// 1. "Database opened."
// 2. "Results for: SELECT * FROM metrics"
// 3. "Database connection closed automatically!"
Для асинхронной утилизации, например при закрытии сетевого сокета, необходимо реализовать [Symbol.asyncDispose() и объявить ресурс с использованием await using внутри асинхронной функции. Тогда среда выполнения будет ждать завершения процедуры очистки перед продолжением работы.
Дополнение или замена элементов в Map с помощью getOrInsert и getOrInsertComputed
Map — подходящая структура для коллекций с ключами, однако подход «прочитать элемент или создать его, если он отсутствует» всегда был неудобным. Группировка значений под одним ключом обычно выглядит следующим образом:
// The old way: multiple lookups and manual branching
if (!userCache.has(userId)) {
userCache.set(userId, fetchDefaultProfile(userId));
}
const profile = userCache.get(userId);
Это два поиска (has за которым следует get) плюс ветвление, всё это делается лишь для того, чтобы убедиться в наличии записи. Новые методы объединяют это в один вызов и один поиск:
map.getOrInsert(key, defaultValue): возвращает существующее значение дляkey, или сохраняетdefaultValueпод этим ключом и возвращает его.map.getOrInsertComputed(key, callback): то же самое, но обратная связь выполняется только тогда, когда ключ отсутствует, так что дорогостоящее значение по умолчанию никогда не создаётся зря.
Это различие имеет значение для приведённого выше примера. getOrInsert(userId, fetchDefaultProfile(userId)) будет вызывать fetchDefaultProfile при каждом доступе, поскольку аргументы функции оцениваются до вызова; используемая версия избегает этого. В следующем фрагменте события группируются по идентификаторам пользователей, причём пустой массив создаётся только в первый раз, когда появляется пользователь:
const userActivity = new Map();
// Grouping events under user IDs
function logEvent(userId, eventName) {
// If userId doesn't exist, create an empty array, insert it, and return it.
const events = userActivity.getOrInsertComputed(userId, () => []);
events.push(eventName);
}
logEvent("user_42", "login");
logEvent("user_42", "clicked_button");
console.log(userActivity.get("user_42"));
// ['login', 'clicked_button']
Math.sumPrecise для точного подсчёта сумм
Числа в JavaScript реализованы по стандарту IEEE 754 с двойной точностью, что приводит к известному ниже результату:
0.1 + 0.2; // 0.30000000000000004
В счётах, корзинах или агрегациях данных в графиках такие ошибки накапливаются, пока итоговая сумма не станет явно неверной.
Math.sumPrecise() принимает итерируемый набор чисел и вычисляет их сумму, как будто точность неограничена, округляя результат лишь один раз в конце. Примитивный метод reduce округляет результат после каждого шага, и именно поэтому возникает отклонение. В примере сумма четырех позиций счета составляет 0.9999999999999999 при использовании reduce, тогда как Math.sumPrecise возвращает 1 (в консоли выводится 1, а не 1.0):
const invoiceItems = [0.1, 0.2, 0.3, 0.4];
// Old reduce approach:
const naiveTotal = invoiceItems.reduce((acc, n) => acc + n, 0);
console.log(naiveTotal);
// 0.9999999999999999
// New Math.sumPrecise approach:
const accurateTotal = Math.sumPrecise(invoiceItems);
console.log(accurateTotal);
// 1.0
Важно понимать этот лимит. Функция Math.sumPrecise устраняет накопленную ошибку округления, но она не может изменить тот факт, что такие значения, как 0.1, невозможно точно представить в двоичной системе. Сумма [0.1, 0.2] всё равно дает 0.30000000000000004, поскольку точная сумма этих двух чисел типа double округляется до этого значения. Это действительно улучшение для статистики и панелей управления; однако при работе с деньгами более безопасным вариантом остаются целочисленные дробные единицы (центы) или библиотеки для работы с десятичными числами.
Помощники итераторов и Iterator.concat
Уже более десяти лет массивы предоставляют методы .map(), .filter() и .slice(), в то время как итераторы и генераторы, такие как бесконечные последовательности или курсоры баз данных, не имели ничего сопоставимого. Чтобы использовать .map() с генератором, выдающим тысячи строк, необходимо сначала преобразовать его в массив с помощью [...generator()], что сводит на нет преимущества потоковой обработки с точки зрения использования памяти.
Вспомогательные методы для итераторов добавляют прямо в них такие функции, как map, filter, take, drop и flatMap, а метод Iterator.concat() объединяет несколько итераторов в один. Все они являются ленивыми: элемент вычисляется только тогда, когда за ним обращаются.
В примере помощники для работы с итераторами подключаются к бесконечному генератору. Поскольку оценка значений происходит по мере необходимости, а функция take(3) останавливается после трех значений, цикл завершается, хотя источник данных не имеет конца:
function* infiniteCounter() {
let count = 1;
while (true) {
yield count++;
}
}
// Grab an iterator from our infinite generator
const stream = infiniteCounter()
.filter(num => num % 2 === 0) // Keep even numbers
.map(num => `Count: ${num}`) // Format them
.take(3); // Stop after 3 values
for (const item of stream) {
console.log(item);
}
// Logs:
// "Count: 2"
// "Count: 4"
// "Count: 6"
Чтобы объединить несколько последовательностей без создания промежуточного массива, передайте их в метод Iterator.concat(). Он поочередно обрабатывает каждый итератор:
const firstBatch = [1, 2, 3].values();
const secondBatch = [4, 5, 6].values();
const combined = Iterator.concat(firstBatch, secondBatch);
console.log([...combined]); // [1, 2, 3, 4, 5, 6]
Сами помощники для работы с итераторами доступны в основных движках уже давно; метод Iterator.concat() появился позже, поэтому его следует рассматривать отдельно.
Встроенные операции над множествами
Долгое время класс Set предлагал лишь методы .has(), .add() и .delete(). Для выполнения операций вроде пересечения требовалось преобразование множества в массивы и ручная фильтрация:
// The old manual way:
const intersection = new Set([...setA].filter(x => setB.has(x)));
Set.prototype теперь включает настоящую алгебру множеств: union, intersection, difference, symmetricDifference, а также предикаты isSubsetOf, isSupersetOf и isDisjointFrom. Каждая операция возвращает новое множество Set, не изменяя исходные.
В примере сравниваются два набора ролей, извлекаются только те права, которыми обладают администраторы, и проверяется, являются ли роли редактора подмножеством ролей администраторов:
const adminRoles = new Set(['read', 'write', 'delete', 'audit']);
const editorRoles = new Set(['read', 'write']);
// Find privileges exclusive to admins
const adminOnly = adminRoles.difference(editorRoles);
console.log([...adminOnly]); // ['delete', 'audit']
// Check role containment
console.log(editorRoles.isSubsetOf(adminRoles)); // true
Они также игнорируют временные массивы, которые создаётся с помощью методов распространения и фильтрации.
Promise.withResolvers для отложенных обещаний
Иногда обещание необходимо реализовать вне его конструктора, например с помощью обработчика событий или функции возврата, зарегистрированной в другом месте. Для такого «отложенного» подхода раньше требовалось выносить переменные вне блока выполнения:
// The clunky way
let resolveFn, rejectFn;
const myPromise = new Promise((res, rej) => {
resolveFn = res;
rejectFn = rej;
});
Promise.withResolvers() возвращает обещание вместе с его функциями resolve и reject в одном объекте. В примере обработчик клика решает задачу, связанную с обещанием, а остальной код просто ждет ее выполнения. Обратите внимание, что в этом фрагменте используется оператор await на верхнем уровне, поэтому он должен выполняться в ES-модуле или внутри асинхронной функции:
const { promise, resolve, reject } = Promise.withResolvers();
// Attach your listeners or pass the resolve handle to an event handler
document.getElementById("submit-btn").addEventListener("click", () => {
resolve("User clicked submit!");
}, { once: true });
// Await the promise wherever you need it
const message = await promise;
console.log(message);
Этот подход удобен для кастомных очередей событий, взаимодействия с Web Workers, а также для адаптации API, основанных на функциях возврата или эмиттерах, к асинхронному коду.
Что это означает для ваших зависимостей
Общим элементом является принцип паритета: теперь этот язык покрывает рутинные задачи, такие как работа с датами, очистка ресурсов, группировка по ключам и математические операции над множествами, которые раньше выполнялись с помощью библиотек пользовательского уровня. Прежде чем добавлять ещё один пакет в следующем спринте, проверьте свои цели:
- Если вы выпускаете код для современных браузеров или текущих версий Node.js, Bun или Deno, многие из этих инструментов уже доступны в глобальном пространстве имён.
- Каждая избегаемая зависимость означает меньший размер пакета, более быстрый запуск и код, который любой разработчик JavaScript может понять без необходимости изучения API сторонних библиотек.
Основные выводы
- Temporal предоставляет неизменяемые, чётко разделённые типы для дат; он заменяет
Dateв новом коде, не удаляя его. usingиawait usingавтоматизируют и организуют процесс очистки, заменяя вложенные конструкцииtry...finally.
getOrInsertComputed, когда создание значения по умолчанию сопряжено с высокими затратами.Math.sumPrecise устраняет накопленные ошибки округления, а не проблемы с двоичным представлением; для работы с деньгами следует использовать целые числа.Связанные материалы
- Шесть встроенных функций HTML, заменяющих распространённые библиотеки интерфейса на JavaScript — Узнайте, как popover, exclusive details, dialog, Declarative Shadow DOM, fetchpriority и datalist заменяют пользовательский JavaScript, а также какие ограничения у каждого из них всё ещё существуют.
- Функции Node.js 26, которые тихо заменяют годы использования временных решений — Обзор API Temporal в Node.js 26, возможностей нативной обработки TypeScript, инструментов кэширования и других нововведений, устраняющих давно существующие временные решения.
- Инструменты auditing.utils.js: шесть встроенных функций, заменяющих ручно написанные помощники — Узнайте, какие нативные API JavaScript могут заменить методы глубокой копирования, защитные копии, ручное установление таймеров, отложенные обещания и функцию groupBy из lodash, а также какие ограничения существуют.