Главная / Статьи

Проверка файла utils.js: шесть встроенных функций, заменяющих ручно написанные вспомогательные компоненты

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

Издатель: 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, то использование метода spread больше не требуется. Более подробное рассмотрение этих методов, возвращающих копии, представлено в современных методах массивов 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 для тех сред, которые вы действительно используете, поскольку некоторые из этих функций появились позже, а старые браузеры могут их не поддерживать.
  • Сохраняйте полифилы, пока ваша матрица поддержки действительно не позволит их удалять.
  • Постепенно заменяйте места вызовов функций, проводя тесты на копирование и группировку в тех случаях, когда форма результата имеет значение.
  • Удалите старый вспомогательный модуль или импорт библиотеки только тогда, когда ничто больше на него не будет ссылаться.