Семь вялідных функцыяй JavaScript, які заменяюць залежнасці ад дапаможных бібліятак
Как клас Temporal, выкарыстоўваючы деклараціі, Map upserts, Math.sumPrecise, памочныя элементы ітератара, методы Set і Promise.withResolvers, адзьяўляе звычайныя шаблонаваныя часткі коду ў JavaScript.
Раней, каб запусціць серйозны проект на JavaScript, паводзілася установка бібліятэкі для працы з датамі, інструменту для агрэгацыі дадзеных і памагчыкі для групавання, а пасля кожны адчынены ресурс быў заглушаны ў захоцкія блокі try...finally. За апошнія калькі выданняў некалькі праектаў TC39, якія гады працаваліся над імі, сталі часткай стандарту і ўжо ўключаныя ў сучасныя браузеры, 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 з абыходам праблемы якщо элемент вялікі за размер
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, таму што точная сума гэтых двух значэнняў заокругляецца да гэтага розмеру. Гэта справжнія палегчэнне для статыстыкі і панелей керування; пры роботе з грошамі болей безпечным выборам застаюцца цэлыя дробныя елементы (центы) або бібліятэкі для працы з дробамі.
Падпрамыслы ітератара і 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, базаваных на калбэках аб эмітарах, да async-кода.
Што гэта значыць для вашых залежнасцей
Адыяктны элемент — парітэт: гэты язык тепер падпірае звычайныя задачы, такія як працэю з датамі, чыстка ресурсоў, групаванне па ключах і матэматыка над множынамі, якія раней выконваліся за дапомогою бібліятэкаў у корыстніку. Перш чым дадаць новы пакет у наступным спрінт, пераканайцеся ў своіх цялях:
- Якщо вы выкорыстоўваете сучасныя браузеры або актуальныя версіі 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, а таксама якія є застерэжнення.