Представь до боли знакомую картину: ты пишешь компонент фильтрации каталога в React или Vue. Пользователь кликает «Сначала дешёвые», ты вызываешь products.sort(), обновляешь состояние... и ничего не происходит. Данные в консоли отсортированы, но интерфейс застыл как вкопанный. Или ещё хуже: внезапно перерисовывается соседний виджет корзины, который вообще не должен был знать о твоих манипуляциях.
Корень этого зла — мутирующая природа классического метода Array.prototype.sort(). Он изменяет исходный массив прямо в памяти (in-place) и возвращает ссылку на тот же самый объект: arr === arr.sort(). Декларативные фреймворки полагаются на поверхностное сравнение ссылок (shallow comparison) в хуках useMemo и useEffect. Ссылка не изменилась — значит, ререндер не нужен.
Годами мы городили обходные пути вроде [...arr].sort() или arr.slice().sort(). Но начиная со стандарта ECMAScript 2023 (вместе с предложением Change Array by Copy) в JavaScript появился штатный метод Array.prototype.toSorted(). Сегодня он поддерживается во всех современных браузерах и рантаймах (Node.js 20+, Bun, Deno).
Ранее мы уже наблюдали за эволюцией языка, когда разбирали методы Iterator Helpers на Stage 4 для ленивых вычислений. Метод toSorted() решает смежную задачу: делает работу с массивами безопасной и предсказуемой.
Подводные камни: алфавитный сюрприз и дыры в массиве
Хотя метод называется спасительным, он полностью наследует исторические странности своего предшественника:
- Лексикографическая сортировка по умолчанию. Если ты вызовешь
[3, 10, 2].toSorted()без аргументов, JavaScript услужливо приведет числа к строкам и расставит их по кодовым точкам Unicode UTF-16:[10, 2, 3]. Числовая функция сравнения(a, b) => a - bостаётся строго обязательной. - Разреженные массивы (Sparse Arrays). Если в массиве есть пустые «дыры» после удаления элементов или создания через
new Array(5), методtoSorted()материализует эти пропуски как явные значенияundefinedи аккуратно переместит их в самый конец результирующего массива, не вызывая для них функцию компаратора.
// Исходные числовые данные
const scores = [45, 100, 12, 8, 99];
// ЛОВУШКА: Сортировка без компаратора работает со строками!
const lexicographical = scores.toSorted();
console.log(lexicographical);
// [100, 12, 45, 8, 99] — 100 встало раньше 12, а 8 уехало в конец!
// ПРАВИЛЬНО: Передаем функцию сравнения чисел
const ascending = scores.toSorted((a, b) => a - b);
console.log(ascending);
// [8, 12, 45, 99, 100]
// Исходный массив scores остался абсолютно чистым:
console.log(scores);
// [45, 100, 12, 8, 99]
// А вот что творит старый метод sort():
const mutated = scores.sort((a, b) => a - b);
console.log(scores === mutated);
// true — исходный массив мутирован по месту!
Метод toSorted() создаёт новый массив верхнего уровня, но выполняет лишь поверхностное копирование (shallow copy). Если элементами являются объекты или вложенные структуры, новый массив хранит ссылки на те же самые участки памяти. Изменение поля sortedList[0].price = 999 моментально изменит этот же объект и в исходном initialList. Если требуется полная изоляция состояния, комбинируй метод с structuredClone() или создавай копии объектов при рендере.
Применение в React-компонентах и производительность
В реальном UI-коде toSorted() избавляет от лишних аллокаций spread-оператора. Движки V8 и SpiderMonkey оптимизируют этот вызов на уровне нативного C++ кода: движок сразу выделяет память под целевой массив нужного размера и раскладывает отсортированные ссылки за один проход:
import React, { useState } from react;
export function ProductCatalog({ initialProducts }) {
const [products] = useState(initialProducts);
const [sortOrder, setSortOrder] = useState(asc);
// Безопасная сортировка без риска поломать состояние родителя:
// toSorted() возвращает свежую ссылку на новый массив
const sortedProducts = products.toSorted((a, b) => {
return sortOrder === asc ? a.price - b.price : b.price - a.price;
});
return (
<section className="catalog">
<div className="toolbar">
<button onClick={() => setSortOrder(asc)}>Сначала дешёвые</button>
<button onClick={() => setSortOrder(desc)}>Сначала дорогие</button>
</div>
<ul>
{sortedProducts.map((item) => (
<li key={item.id}>
<strong>{item.title}</strong> — {item.price} ₽
</li>
))}
</ul>
</section>
);
}
В паре с методами-компаньонами toReversed(), toSpliced() и with() современный JavaScript наконец-то позволяет писать чистый иммутабельный код без подключения сторонних утилит вроде Lodash или Immer в повседневных задачах.
