Веб-калькулятору не нужны фреймворки и eval. Один HTML-файл будет хранить текущее число, первый операнд и знак, а затем точно выполнит выбранное действие.
В кнопочном калькуляторе пользователь не вводит готовую строку 12 + 3. Каждый клик меняет состояние: сначала растёт текущее число, затем сохраняются операнд и знак. После знака равенства полученное число становится новым текущим.
current, previous и operator описывают всё состояние.
Кнопки вызывают три короткие функции по типу действия.
render переносит current на дисплей после каждого клика.
Какое состояние нужно калькулятору
Вместо парсинга длинной строки мы будем знать, на каком шаге находится человек. Цифра дописывается к current, знак переносит current в previous, а равенство вызывает одну функцию compute. Точка добавляется только один раз.
| Переменная | Пример | Смысл |
|---|---|---|
| current | 12.5 | То, что видно на экране |
| previous | 8 | Первый операнд |
| operator | + | Ожидающее действие |
| justComputed | true | Следующая цифра начнёт новое число |
После этого шага у каждой части есть одна роль. Интерфейс собирает действие, логика меняет состояние, а вывод показывает признак успеха. Это проще отлаживать, чем один большой обработчик.

Состояние меняется по шагам. Один обработчик обслуживает всю сетку
Собираем HTML, CSS и JavaScript
Кнопкам не нужны отдельные id. Атрибут data-action говорит, какой это тип кнопки, а data-value хранит цифру или знак. Один обработчик на контейнере ловит все клики и выбирает нужную ветку.
<div class="calculator" id="calculator">
<output class="display" id="display">0</output>
<div class="keys">
<button data-action="clear">C</button>
<button data-action="operator" data-value="/">÷</button>
<button data-action="operator" data-value="*">×</button>
<button data-action="operator" data-value="-">−</button>
<button data-value="7">7</button><button data-value="8">8</button>
<button data-value="9">9</button><button data-action="operator" data-value="+">+</button>
<button data-value="4">4</button><button data-value="5">5</button>
<button data-value="6">6</button><button data-action="equals">=</button>
<button data-value="1">1</button><button data-value="2">2</button>
<button data-value="3">3</button><button data-value="0">0</button>
<button data-value=".">.</button>
</div>
</div>
<script>
const display = document.querySelector('#display');
let current = '0', previous = null, operator = null, justComputed = false;
function render() { display.value = current; display.textContent = current; }
function append(value) {
if (justComputed) { current = '0'; justComputed = false; }
if (value === '.' && current.includes('.')) return;
current = current === '0' && value !== '.' ? value : current + value;
}
function compute() {
if (previous === null || operator === null) return;
const a = Number(previous), b = Number(current);
const actions = { '+': (x,y)=>x+y, '-': (x,y)=>x-y,
'*': (x,y)=>x*y, '/': (x,y)=>y === 0 ? NaN : x/y };
const result = actions[operator](a, b);
current = Number.isFinite(result) ? String(Number(result.toFixed(10))) : 'Ошибка';
previous = operator = null; justComputed = true;
}
document.querySelector('#calculator').addEventListener('click', event => {
const button = event.target.closest('button'); if (!button) return;
const { action, value } = button.dataset;
if (action === 'clear') { current='0'; previous=operator=null; }
else if (action === 'operator') { previous=current; operator=value; justComputed=true; }
else if (action === 'equals') compute();
else append(value);
render();
});
</script>Цифры и точка собирают текущее число. Операция запоминает первый операнд. Деление на ноль показывает ошибку без Infinity. Кнопка C возвращает начальное состояние.

Выбираем контролируемую модель. Сравнение показывает, какой вариант легче поддерживать и проверять.
Как работает цепочка вычислений
После расчёта на дисплее остаётся ответ. Если нажать знак, ответ можно использовать дальше. Если нажать цифру, начнётся новое число. Это решает флаг justComputed, который не даёт склеить ответ со следующей цифрой.
| Клик | Состояние до | Состояние после |
|---|---|---|
| 7 | current = 0 | current = 7 |
| + | current = 7 | previous = 7, operator = + |
| 3 | justComputed = true | current = 3 |
| = | 7, +, 3 | current = 10 |
| 2 | justComputed = true | current = 2 |
Сохраняйте в одном месте то, что может измениться. Если одно и то же значение скопировано в три функции, после первой правки они начнут расходиться. Один объект состояния и одна функция отрисовки снимают эту проблему.
Практика: клики, дроби и ноль
Проверяйте не только арифметику, но и последовательность кликов. Дважды нажатая точка не должна ломать Number, а новая цифра после равенства не должна дописываться к старому ответу.
- Откройте index.html и наберите 12 + 3 =.
- Нажмите × 4 =, чтобы продолжить цепочку от ответа 15.
- Нажмите C, введите 2.5 × 4.
- Нажмите точку дважды и убедитесь, что на экране одна точка.
- Поделите 8 на 0, затем нажмите C.
- Откройте DevTools и проверьте, что в консоли нет ошибок.
Все кнопки работают мышью и касанием. Цепочка 12 + 3 × 4 даёт 60 в пошаговой модели. Дробь и деление на ноль обрабатываются без сбоя. Кнопка C сбрасывает все три части состояния.

Четыре сценария до готового дизайна. Пройдите все четыре сценария перед публикацией проекта.
Доводим интерфейс до готового вида
Когда логика готова, сделайте сетку кнопок одинаковой и оставьте операциям один акцентный цвет. Дисплей должен помещать длинное число, поэтому не задавайте ему жёсткую высоту без overflow. Для телефона кнопкам нужна высота около 48 пикселей, чтобы по ним было легко попасть.
Выполнять текст не нужно. Храните операнды и знак отдельно.
Перед добавлением проверьте current.includes.
Обработайте ноль до деления или проверьте Number.isFinite.
После операции округлите незначащий шум float.
Что получится в итоге

Готовый калькулятор умещается в одной карточке и сразу готов к проверке в браузере.
Почему не нужен eval?
Все допустимые операции известны. Объект с четырьмя функциями решает задачу без выполнения строки.
Зачем justComputed?
Флаг отличает результат от числа, которое пользователь сейчас набирает.
Как запретить вторую точку?
Перед добавлением проверить current.includes('.').
Как понять, что проект готов?
Он повторяет целевой сценарий несколько раз подряд, понятно реагирует на ошибочный ввод и не теряет состояние при обычных действиях.
Можно ли копировать код целиком?
Можно как стартовую точку, но сначала замените значения из примера, затем запустите проект по шагам. Если вы не можете объяснить одну строку, её лучше разобрать до следующего шага.
События, DOM и состояние разбираем по шагам в курсе JavaScript. Если скрипт не запускается, сверьте подключение со статьё про script.
Для следующего проекта возьмите Fetch API или localStorage. В Кодике код можно запускать прямо на телефоне.