Головна / Статті

Перевірка utils.js: Шість вбудованих функцій, які замінюють ручно створені допоміжні засоби

Дізнайтеся, які вбудовані API JavaScript можуть замінити методи глибокого клонування, захисні копії, ручне встановлення таймерів, відкладені обіцянки та lodash groupBy, а також які обмеження існують у їх використанні.

Видавець: REACTAPP.TOP5 хв читання1052 слів

Джерела: 6 Modern JavaScript Features You’re Probably Still Working Around

Багато кодових баз досі містять функції-допоміжники та імпорти бібліотек для проблем, які платформа вже вирішила багато років тому. Ці додатки потрапили туди непомітно, тому звички, сформовані через старі способи розв’язання проблем, так і не були переглянуті. Нижче наведено шість вбудованих функцій, які варто перевірити у вашому utils.js, інформація про проблеми, які вони усувають, та обмеження, про які варто знати перед тим, як щось видаляти.

structuredClone() замість обробки через JSON

Найпоширеніший спосіб створення глибокої копії полягає у серіалізації об’єкта та його подальшому парсингу:

const copy = JSON.parse(JSON.stringify(original));

Він не працює з тим, що JSON не може представити: екземпляри Date повертаються у вигляді рядків, властивості, встановлені на undefined, втрачаються, Map та Set перетворюються на порожні об’єкти без жодних попереджень, а циклічні посилання змушують JSON.stringify кидати помилку.

structuredClone() — це глобальна функція, доступна у сучасних браузерах та Node.js, яку не потрібно імпортувати. Розглянемо об’єкт, який містить дату, множину та карту:

const original = {
  date: new Date(),
  items: new Set([1, 2, 3]),
  meta: new Map([['key', 'value']]),
};

Його клонування зберігає кожен із цих типів у недоторканному вигляді, а циклічні структури також копіюються правильно:

const copy = structuredClone(original);
// Dates, Sets, Maps, and circular refs all survive intact

Вона використовує алгоритм структурованого клонування, який лежить в основі postMessage та IndexedDB, тому враховує крайні випадки, які проходять повз при ручному клонуванні. Однак у неї є обмеження: функції та елементи DOM не можна клонувати, що призводить до появи помилки, а екземпляри ваших власних класів повертаються як звичайні об’єкти без їхнього прототипу.

toSorted() та toReversed() замість захисних копій

.sort() та .reverse() змінюють об’єкт на місці, тому сортування масиву, який поділяється за посиланням, змінює його всюди. Починаючи з цього масиву:

const scores = [30, 10, 20];

toSorted() повертає новий, відсортований масив та не змінює вихідний:

const sorted = scores.toSorted((a, b) => a - b);console.log(scores); // [30, 10, 20] — untouched
console.log(sorted);  // [10, 20, 30]

toReversed() та toSpliced() дотримуються такої ж схеми повернення копії. Якщо ваш код містить безліч викликів [...arr].sort() для збереження незмінності стану, як це часто роблять редюсери в React, тоді використання спрейду більше не є необхідним. Більш детальний огляд цих методів, які повертають копії, наведено у сучасних методах масивів JavaScript, які замінюють стандартні шаблони коду.

Object.hasOwn() замість hasOwnProperty

Виклик методу через об’єкт приховує підступ:

obj.hasOwnProperty('key'); // breaks if obj = Object.create(null)

Об’єкт, створений за допомогою Object.create(null), не має прототипу, тому він ніколи не успадковує метод hasOwnProperty, і це призводить до виникнення помилки TypeError. Те саме відбувається, якщо об’єкт визначає власну властивість з такою ж назвою. Функція Object.hasOwn() є статичною, тому вона не залежить від того, що успадковує об’єкт:

Object.hasOwn(obj, 'key'); // works regardless of the object's prototype chain

Ця заміна є незначною, але вона запобігає проблемам, які проявляються лише у продакшені, коли користувач передає об’єкт у форматі, який ніхто не планував.

AbortSignal.timeout() замість ручного таймера

Для встановлення терміну для fetch раніше було необхідно використовувати AbortController разом із setTimeout, який його активував:

const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 5000);

Потім доводилося пам’ятати про необхідність скасування таймера після завершення запиту:

fetch(url, { signal: controller.signal }).finally(() => clearTimeout(id));

Тепер сигнал може мати власний термін дії:

fetch(url, { signal: AbortSignal.timeout(5000) });

Один рядок, без очищення, без залишкового таймера. Коли він спрацьовує, запит відхиляється з помилкою TimeoutError DOMException, що дозволяє відрізнити тайм-аут від скасування, спричиненого користувачем. Щоб поєднати тайм-аут з іншим джерелом скасування, ознайомтесь з AbortSignal.any() у поточній документації.

Promise.withResolvers() замість використання resolve та reject

Іноді обіцянку потрібно вирішити ззовні її виконувача. Класичний спосіб — оголошувати змінні та присвоювати їм значення всередині копі-функції конструктора:

let resolve, reject;
const promise = new Promise((res, rej) => {
  resolve = res;
  reject = rej;
});

Це працює, але передача функцій з закритого простору легко призводить до неправильного розуміння коду. Promise.withResolvers() повертає обіцянку та обидві функції вирішення разом:

const { promise, resolve, reject } = Promise.withResolvers();

Це природно підходить, коли ви створюєте чергу асинхронних завдань чи обгортаєте API, засноване на подіях, за допомогою обіцянок. Для більш детального прикладу дивіться координацію викликів інструментів LLM за допомогою Promise.withResolvers.

Object.groupBy() замість lodash groupBy

Групування записів за ключем — одна з найпоширеніших причин імпортування бібліотеки з утилітами. Якщо у нас є список людей:

const people = [
  { name: 'Sam', age: 21 },
  { name: 'Lee', age: 34 },
  { name: 'Kai', age: 21 },
];

Object.groupBy() приймає ітерований об’єкт та функцію-колбек, і повертає об’єкт, у якому кожен ключ має відповідний масив:

const byAge = Object.groupBy(people, (p) => p.age);
// { 21: [Sam, Kai], 34: [Lee] }

Зверніть увагу на назву. Раніша пропозиція передбачала використання Array.prototype.group(), але перед стандартизацією її перейменували та перемістили у статичну функцію Object.groupBy() (а також Map.groupBy(), коли ключі — не рядки), і до постачання потрапляють лише статичні версії. Результат має прототип null, і це найновіший API у цьому списку, тож перевірте свої мінімальні підтримувані версії браузера та Node.js перед тим, як видаляти залежність.

Перш ніж видаляти допоміжні функції

Жодна з цих змін не впливає на архітектуру, але кожна усуває схеми, схильні до помилок, або зайві залежності. Розумний порядок дій:

  • Перевірте підтримку на caniuse.com або MDN щодо середовищ, які ви насправді обробляєте, оскільки деякі з цих функцій є новішими за інші, а старіші браузери можуть їх не підтримувати.
  • Зберігайте поліфіли, поки ваша матриця підтримки дійсно не дозволить їх видалити.
  • Поступово замінюйте місця виклику функцій, проводячи тести щодо клонування та групування у випадках, коли форма результату має значення.
  • Видаліть старий помічник чи імпорт бібліотеки лише тоді, коли ніщо не буде на нього посилатися.