Модуль числа, остаток и циклический индекс в JavaScript
Автор: Казачкин Даниил Михайлович · Обновлено
Модуль числа и остаток от деления решают разные задачи: Math.abs измеряет величину без знака, а оператор % вычисляет остаток. Различие особенно важно при отрицательных значениях и циклическом переключении элементов.
Три значения похожего слова
В этом уроке «модуль» относится к арифметике. Файлы с import и export рассматриваются отдельно в уроке о модулях JavaScript. Запускайте примеры в Node.js 22 или новее, сохранив первый блок как numbers.mjs и выполнив node numbers.mjs. Они также работают в консоли современного браузера. Нужны условия, функции и представление о типе number.
Абсолютное значение, или модуль числа, — расстояние от нуля на числовой прямой. Для -7 и 7 оно равно семи. Остаток отвечает другому вопросу: что остаётся после удаления целого числа делителей. У JavaScript знак ненулевого результата % совпадает со знаком делимого. Математическая операция modulo часто нужна для помещения значения в повторяющийся диапазон; для положительной длины такой диапазон обычно начинается с нуля.
Полный пример: кольцо из пяти вкладок
function wrapIndex(index, length) {
if (!Number.isSafeInteger(index) ||
!Number.isSafeInteger(length) || length <= 0) {
throw new RangeError('Expected a safe index and positive length');
}
const remainder = index % length;
return remainder < 0 ? remainder + length : remainder + 0;
}
console.log(Math.abs(-7));
console.log(-7 % 5);
console.log(wrapIndex(-7, 5));
console.log(wrapIndex(5, 5));
console.log(wrapIndex(-1, 5));Вывод по строкам: 7, -2, 3, 0, 4. Первая строка убирает знак и не зависит от размера кольца. Вторая вычисляет остаток: -7 можно представить как (-1) * 5 + (-2). Третья переносит этот остаток в диапазон допустимых индексов. Последняя строка показывает переход назад с нулевой позиции на последнюю вкладку.
Проверка длины исключает пустое кольцо. Индекс и длина принимаются только как безопасные целые числа, поэтому функция не превращает строку из поля ввода в число незаметно для вызывающего кода. Выражение remainder + 0 нормализует отрицательный ноль. Для обычного доступа к массиву это редко существенно, но делает контракт результата аккуратным: индекс всегда представлен неотрицательным числом.
Почему Math.abs не исправляет отрицательный индекс
Попытка Math.abs(index % length) выдаст 1 для индекса -1 и длины 5. Нужна позиция 4: переход назад должен вести к последнему элементу. Абсолютное значение отражает число относительно нуля, а циклическая нормализация обходит окружность. На положительных входах ошибка скрыта, поэтому тест только с индексом 7 недостаточен.
Часто встречается формула ((index % length) + length) % length. Для обычных небольших целых индексов она также работает. В нашей функции прибавление выполняется только для отрицательного остатка: так мы избегаем лишнего сложения двух больших положительных чисел около границы безопасной точности. Это не делает любую арифметику точной: если значение потеряло точность до вызова функции, восстановить исходное число уже нельзя.
Делитель может быть отрицательным, но для индекса длина коллекции отрицательной быть не должна. Поэтому контракт ограничивает область задачи, вместо того чтобы прятать неоднозначность внутри универсального помощника. Если вы пишете математическую библиотеку, отдельно определите соглашение о знаке modulo и поведение для нуля. Одно название функции не заменяет эту договорённость.
Нули, дроби и большие целые
Для number деление по остатку на ноль даёт NaN. Остаток бесконечного делимого также не является обычным числовым результатом. Проверять его через result === NaN бесполезно: NaN не равен самому себе. Проверка входного контракта обычно понятнее, чем поиск странного значения в конце длинной цепочки вычислений.
Оператор % работает и с дробями, но тип number хранит двоичные числа с ограниченной точностью. Поэтому остаток от десятичной дроби может содержать неожиданную длинную дробную часть. Денежные суммы и точные десятичные правила требуют заранее выбранного представления: например, целых минимальных единиц в допустимом диапазоне. Округление результата «до красивого вида» и исправление математической модели — разные действия.
У BigInt есть собственный остаток, например 17n % 5n равен 2n. Смешивать 17n % 5 нельзя: получится TypeError. Деление по остатку на 0n вызывает RangeError, поскольку значения NaN в этом типе нет. Для идентификаторов и очень больших счётчиков можно написать отдельную функцию с BigInt; преобразование уже неточного number в BigInt не возвращает потерянные разряды.
Практика: повернуть часы
Реализуйте функцию, которая сдвигает час в диапазоне от нуля до двадцати трёх. Положительный сдвиг идёт вперёд, отрицательный — назад. Даты, часовые пояса и переходы на летнее время здесь не моделируются: это упражнение на циклическое целое число, а не календарный расчёт. Для времени реального события такая функция недостаточна.
function shiftHour(hour, delta) {
if (!Number.isInteger(hour) || hour < 0 || hour > 23 ||
!Number.isSafeInteger(delta)) {
throw new RangeError('Invalid hour or shift');
}
const reduced = delta % 24;
return ((hour + reduced) % 24 + 24) % 24;
}
console.log(shiftHour(2, -5));
console.log(shiftHour(23, 49));
console.log(shiftHour(0, -24));Ожидаются 21, 0, 0. Сначала уменьшаем большой сдвиг до остатка, затем складываем его с маленьким часом. Это удерживает промежуточную арифметику в небольшом диапазоне. Проверьте нулевой сдвиг, полный оборот, несколько отрицательных оборотов и недопустимый час 24. Последний случай должен завершиться ошибкой, а не молча превратиться в полночь: вход уже нарушил заявленный контракт.
Частые вопросы
Почему из поля ввода приходит неожиданный результат?
Значение элемента формы обычно строка. Отдельно преобразуйте его, обработайте пустую строку и проверьте число. Не полагайтесь на то, что разные арифметические операторы одинаково интерпретируют текст.
Нужно ли использовать битовые операции для ускорения?
Битовые операции над number работают со своей ограниченной целочисленной моделью. Замена арифметики на них ради предполагаемой скорости может изменить результат больших значений. Сначала обеспечьте правильность и измерьте реальную нагрузку.
Как выбрать проверочные случаи?
Возьмите значения непосредственно до и после границы, точную границу, ноль и отрицательный шаг. Для кольца длиной пять это -6, -5, -1, 0, 4, 5, 6. Такие входы показывают направление перехода и ошибки сравнения гораздо лучше длинного списка случайных положительных чисел.