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