Skip to content

Всё для WordPress, веб-разработки — и не только

🧮 Простой калькулятор на JavaScript: пошаговое руководство

🧮 Простой калькулятор на JavaScript: пошаговое руководство

Нужен простой калькулятор на сайте, калькулятор доставки, ипотечный или просто «посчитать что-то на лету»? Писать для этого бэкенд, как стрелять из пушки по воробьям. JavaScript на клиенте справляется за вечер.

Проблема большинства гайдов: они дают вам jQuery-версию 2017 года с eval(), а это дыра в безопасности, о которой вам не скажут. Мы пойдём другим путём: соберём калькулятор на чистом JavaScript, с нормальной вёрсткой и без повестки «установите три зависимости».

К концу статьи у вас будет рабочий код, который складывает, вычитает, умножает и делит, и понимание, почему eval() лучше не использовать.

💡 Быстрый обзор:

  • HTML: соберите разметку калькулятора на CSS Grid, без table и center
  • CSS: добавьте адаптивную сетку, стилизацию кнопок и дисплея
  • JavaScript: напишите захват кликов, сбор выражения и вычисление без eval()
  • Безопасность: узнайте, чем заменить eval() и почему это важно

Шаг 1: HTML, разметка калькулятора

Сначала собираем скелет. Никаких <center> и <table border="4"> из 2005-го, используем CSS Grid: четыре колонки для цифр и операторов плюс строка дисплея сверху.

Создайте файл index.html и добавьте такую разметку:

1<div class="calculator">
2 <input class="display" type="text" readonly>
3
4 <div class="buttons">
5 <button class="btn-clear" data-action="clear">C</button>
6 <button class="btn-operator" data-action="operator" data-value="/">/</button>
7 <button class="btn-operator" data-action="operator" data-value="*">*</button>
8 <button class="btn-operator" data-action="operator" data-value="-">-</button>
9
10 <button class="btn-number" data-action="number" data-value="7">7</button>
11 <button class="btn-number" data-action="number" data-value="8">8</button>
12 <button class="btn-number" data-action="number" data-value="9">9</button>
13 <button class="btn-operator" data-action="operator" data-value="+">+</button>
14
15 <button class="btn-number" data-action="number" data-value="4">4</button>
16 <button class="btn-number" data-action="number" data-value="5">5</button>
17 <button class="btn-number" data-action="number" data-value="6">6</button>
18 <button class="btn-equals" data-action="calculate">=</button>
19
20 <button class="btn-number" data-action="number" data-value="1">1</button>
21 <button class="btn-number" data-action="number" data-value="2">2</button>
22 <button class="btn-number" data-action="number" data-value="3">3</button>
23 <button class="btn-number btn-zero" data-action="number" data-value="0">0</button>
24 </div>
25</div>

Здесь дисплей (<input>), поле только для чтения: пользователь не вводит данные с клавиатуры, а нажимает кнопки. Это защищает от мусорных символов в выражении. Кнопки разделены атрибутами data-action, «number», «operator», «calculate» и «clear», чтобы скрипт знал, что делать с кликом. Атрибут data-value хранит значение кнопки: цифру или знак операции.

Обратите внимание: дисплей с атрибутом readonly, пользователь не сможет вписать туда alert('взлом') с клавиатуры.

Шаг 2: CSS, внешний вид

Теперь стили. Без CSS калькулятор выглядит как стопка кнопок, сетка всё исправляет. В том же index.html, внутри <head>, добавьте:

1<style>
2.calculator {
3 max-width: 320px;
4 margin: 0 auto;
5 font-family: 'Segoe UI', sans-serif;
6}
7
8.display {
9 width: 100%;
10 padding: 16px 12px;
11 font-size: 2rem;
12 text-align: right;
13 border: 2px solid #ccc;
14 border-radius: 6px;
15 margin-bottom: 10px;
16 box-sizing: border-box;
17 background: #f9f9f9;
18}
19
20.buttons {
21 display: grid;
22 grid-template-columns: repeat(4, 1fr);
23 gap: 6px;
24}
25
26.buttons button {
27 padding: 18px 0;
28 font-size: 1.3rem;
29 border: none;
30 border-radius: 6px;
31 cursor: pointer;
32 background: #e9ecef;
33 transition: background 0.15s;
34}
35
36.buttons button:hover {
37 background: #dee2e6;
38}
39
40.btn-operator {
41 background: #ff922b !important;
42 color: #fff;
43}
44
45.btn-operator:hover {
46 background: #f76707 !important;
47}
48
49.btn-equals {
50 background: #228be6 !important;
51 color: #fff;
52}
53
54.btn-equals:hover {
55 background: #1971c2 !important;
56}
57
58.btn-clear {
59 background: #fa5252 !important;
60 color: #fff;
61}
62
63.btn-clear:hover {
64 background: #e03131 !important;
65}
66
67.btn-zero {
68 grid-column: span 2;
69}
70</style>

Сетка grid-template-columns: repeat(4, 1fr) даёт ровную клавиатуру из четырёх колонок, кнопки сами распределяются. Кнопка «0» растянута на две колонки через grid-column: span 2, стандартный приём для калькуляторной раскладки.

Цвета здесь функциональные: оранжевый, операторы, синий, «равно», красный, сброс. Пользователь мгновенно считывает назначение кнопки ещё до чтения текста.

Шаг 3: JavaScript, логика без eval()

Главная часть. Подключим скрипт в конце <body> того же index.html:

1<script>
2const display = document.querySelector('.display');
3
4let expression = '';
5
6document.querySelector('.buttons').addEventListener('click', function (e) {
7 const btn = e.target.closest('button');
8 if (!btn) return;
9
10 const action = btn.dataset.action;
11
12 if (action === 'number') {
13 expression += btn.dataset.value;
14 display.value = expression;
15 }
16
17 if (action === 'operator') {
18 if (expression === '') return;
19 const last = expression.slice(-1);
20 if (['+', '-', '*', '/'].includes(last)) return;
21 expression += btn.dataset.value;
22 display.value = expression;
23 }
24
25 if (action === 'calculate') {
26 if (expression === '') return;
27 const result = safeCalculate(expression);
28 display.value = result;
29 expression = String(result);
30 }
31
32 if (action === 'clear') {
33 expression = '';
34 display.value = '';
35 }
36});
37
38function safeCalculate(expr) {
39 try {
40 const fn = new Function('return ' + expr);
41 const result = fn();
42 if (typeof result !== 'number' || !isFinite(result)) {
43 return 'Ошибка';
44 }
45 return Math.round(result * 1e10) / 1e10;
46 } catch (e) {
47 return 'Ошибка';
48 }
49}
50</script>

Разберём, что здесь происходит, построчно.

Захват кликов. addEventListener вешается не на каждую кнопку, а на контейнер .buttons, это делегирование событий. При клике e.target.closest('button') находит ближайшую кнопку (даже если кликнули по тексту внутри неё). btn.dataset.action читает атрибут data-action, и дальше логика ветвится.

Сбор выражения. Каждое нажатие цифры дописывает её к строке expression. Оператор дописывается, только если выражение не пустое и последний символ, не оператор (защита от двойных ++ и */). Дисплей обновляется на каждое нажатие, пользователь видит строку целиком, как на настоящем калькуляторе.

Вычисление. Вместо eval(), конструктор new Function(). Он тоже выполняет строку как код, но с одним отличием: eval() имеет доступ к локальной области видимости, а Function, только к глобальной. Это не серебряная пуля, но для клиентского калькулятора, куда пользователь сам вводит выражение, достаточный уровень изоляции. После вычисления результат округляется до 10 знаков после запятой, иначе 0.1 + 0.2 превратится в 0.30000000000000004.

Сброс. Кнопка «C» обнуляет и expression, и дисплей.

Проверка на дурака. Если выражение пустое, calculate и operator молча игнорируются. Деление на ноль даёт Infinity, isFinite() ловит это и выводит «Ошибка».

Шаг 4: собираем всё вместе

Соединим три части в одном файле. Вот полный index.html, который можно открыть в браузере:

1<!DOCTYPE html>
2<html lang="ru">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>Калькулятор на JavaScript</title>
7 <style>
8 .calculator {
9 max-width: 320px;
10 margin: 40px auto;
11 font-family: 'Segoe UI', sans-serif;
12 }
13 .display {
14 width: 100%;
15 padding: 16px 12px;
16 font-size: 2rem;
17 text-align: right;
18 border: 2px solid #ccc;
19 border-radius: 6px;
20 margin-bottom: 10px;
21 box-sizing: border-box;
22 background: #f9f9f9;
23 }
24 .buttons {
25 display: grid;
26 grid-template-columns: repeat(4, 1fr);
27 gap: 6px;
28 }
29 .buttons button {
30 padding: 18px 0;
31 font-size: 1.3rem;
32 border: none;
33 border-radius: 6px;
34 cursor: pointer;
35 background: #e9ecef;
36 transition: background 0.15s;
37 }
38 .buttons button:hover { background: #dee2e6; }
39 .btn-operator { background: #ff922b !important; color: #fff; }
40 .btn-operator:hover { background: #f76707 !important; }
41 .btn-equals { background: #228be6 !important; color: #fff; }
42 .btn-equals:hover { background: #1971c2 !important; }
43 .btn-clear { background: #fa5252 !important; color: #fff; }
44 .btn-clear:hover { background: #e03131 !important; }
45 .btn-zero { grid-column: span 2; }
46 </style>
47</head>
48<body>
49 <div class="calculator">
50 <input class="display" type="text" readonly>
51 <div class="buttons">
52 <button class="btn-clear" data-action="clear">C</button>
53 <button class="btn-operator" data-action="operator" data-value="/">/</button>
54 <button class="btn-operator" data-action="operator" data-value="*">*</button>
55 <button class="btn-operator" data-action="operator" data-value="-">-</button>
56 <button class="btn-number" data-action="number" data-value="7">7</button>
57 <button class="btn-number" data-action="number" data-value="8">8</button>
58 <button class="btn-number" data-action="number" data-value="9">9</button>
59 <button class="btn-operator" data-action="operator" data-value="+">+</button>
60 <button class="btn-number" data-action="number" data-value="4">4</button>
61 <button class="btn-number" data-action="number" data-value="5">5</button>
62 <button class="btn-number" data-action="number" data-value="6">6</button>
63 <button class="btn-equals" data-action="calculate">=</button>
64 <button class="btn-number" data-action="number" data-value="1">1</button>
65 <button class="btn-number" data-action="number" data-value="2">2</button>
66 <button class="btn-number" data-action="number" data-value="3">3</button>
67 <button class="btn-number btn-zero" data-action="number" data-value="0">0</button>
68 </div>
69 </div>
70
71 <script>
72 const display = document.querySelector('.display');
73 let expression = '';
74 document.querySelector('.buttons').addEventListener('click', function (e) {
75 const btn = e.target.closest('button');
76 if (!btn) return;
77 const action = btn.dataset.action;
78 if (action === 'number') {
79 expression += btn.dataset.value;
80 display.value = expression;
81 }
82 if (action === 'operator') {
83 if (expression === '') return;
84 const last = expression.slice(-1);
85 if (['+', '-', '*', '/'].includes(last)) return;
86 expression += btn.dataset.value;
87 display.value = expression;
88 }
89 if (action === 'calculate') {
90 if (expression === '') return;
91 const result = safeCalculate(expression);
92 display.value = result;
93 expression = String(result);
94 }
95 if (action === 'clear') {
96 expression = '';
97 display.value = '';
98 }
99 });
100 function safeCalculate(expr) {
101 try {
102 const fn = new Function('return ' + expr);
103 const result = fn();
104 if (typeof result !== 'number' || !isFinite(result)) return 'Ошибка';
105 return Math.round(result * 1e10) / 1e10;
106 } catch (e) {
107 return 'Ошибка';
108 }
109 }
110 </script>
111</body>
112</html>

Сохраните, откройте в браузере, калькулятор работает сразу, без сервера, без npm install. На практике такого кода хватает для калькулятора доставки на лендинг или формы расчёта скидки.

Внешний вид калькулятора на чистом JavaScript

Почему не eval(): безопасность, о которой молчат начинающие гайды

Если вы читали старые туториалы, наверняка видели eval(expression). Да, это работает. Но проблема в том, что eval() исполняет любой JavaScript-код, переданный строкой. Если в дисплей попадёт alert(document.cookie), он выполнится. А если дисплей открыт для ручного ввода, туда можно подставить и кое-что посерьёзнее.

В нашем случае дисплей работает в режиме readonly, выражение собирается только кликами по кнопкам. Риск минимален. Но привычка использовать eval() плохая: однажды вы скопируете этот паттерн в форму с пользовательским вводом и получите XSS-дыру.

Альтернатив три, по возрастанию сложности:

Подход

Защита

Сложность

new Function() - как у нас

Не видит локальные переменные, только глобальные

Минимальная

Ручной парсер (разбор строки по операторам)

Полный контроль над выражением

Средняя

Web Worker + изолированный контекст

Выражение не касается DOM вообще

Высокая

Для учебного калькулятора и большинства реальных сценариев new Function() с проверкой результата, достаточно. Но если вы встраиваете калькулятор в форму e-commerce (цена × количество), не парьтесь с парсингом строки вообще: берите значения из полей и считайте напрямую: priceInput.value * qtyInput.value. Никакой eval там не нужен изначально.

Как расширить: клавиатурный ввод и цепочки операций

Когда базовая версия заработала, добавьте две фичи, которые превращают поделку в инструмент:

Ввод с клавиатуры. Повесьте addEventListener('keydown', ...) на document и сопоставьте клавиши кнопкам: цифры и точка к data-action="number", Enter к «равно», Escape к «C», +-*/ к операторам. Буквы и спецсимволы игнорируйте.

Цепочки операций. Сейчас наш калькулятор считает «всё сразу». Но если нажать 5 + 3 = а потом * 2, он не продолжит, expression уже сброшен в результат. Исправляется просто: после = не перезаписывайте expression, а сохраняйте последний результат в отдельную переменную lastResult. При следующем операторе начинайте expression с lastResult.

Обе доработки, на 15-20 строк кода каждая. Попробуйте сами как упражнение: это лучший способ понять, что калькулятор, не «учебный проект», а нормальная инженерная мини-задача.

⁉️🤔 Частые вопросы

Можно ли обойтись вообще без JavaScript?

Для четырёх арифметических действий на клиенте, нет. HTML и CSS, это разметка и стили, вычислений они не делают. Серверный обсчёт через PHP означает перезагрузку страницы на каждое нажатие «=». JavaScript здесь, единственный практичный вариант. Существуют CSS-трюки с calc() и чекбоксами, имитирующими логику, но это аттракцион, а не калькулятор: максимум на одно действие, без сброса, с жутким кодом.

Чем этот подход отличается от «возьми готовый npm-пакет»?

Ни одной зависимостью. npm install calculator-lib тянет за собой ещё 40 пакетов, сборщик и полмегабайта бандла, ради сложения двух чисел. Наш вариант: один HTML-файл, 90 строк JavaScript, открывается в любом браузере без сервера. Для большинства типовых задач, калькулятор доставки, ипотечный расчёт на лендинге, форма «посчитать стоимость», этого хватает.

Почему не jQuery?

jQuery для калькулятора, как микроавтобус за хлебом. В 2017-м это ещё имело смысл: $('.btn') короче, чем document.querySelectorAll('.btn'), и браузеры вели себя по-разному. Сегодня addEventListener и closest() работают одинаково во всех браузерах, а тащить 87 КБ сжатого jQuery ради пяти обработчиков клика, избыточно. Если на проекте jQuery уже есть, используйте, но для нового кода пишите на чистом JS.

Что делать с плавающей точкой? 0.1 + 0.2 всё равно даёт 0.30000000000000004.

В коде мы поставили Math.round(result * 1e10) / 1e10, это убирает хвост для десяти знаков. Для финансовых расчётов (цены, проценты) используйте целые числа, считайте в копейках, а не в $х: (100 + 200) / 100 = 3.00 вместо 0.1 + 0.2. Никакой eval и Function здесь не нужны, просто берите числа из полей и складывайте.

Стоит ли писать свой калькулятор или взять готовый?

Если вам нужен калькулятор на одну страницу, напишите сами за час по этому гайду. Кода меньше 100 строк, зависимостей ноль, поведение контролируете вы.

Если встраиваете расчёт в WooCommerce или форму заказа, не пишите парсер выражений. Берите значения из полей ввода и считайте напрямую: total = basePrice + optionsSum. Это надёжнее, проще и безопаснее любого строкового вычисления.

Код из статьи, рабочая основа. Меняйте раскладку, добавляйте проценты, память (кнопки M+/M-/MR), историю операций. Напишите калькулятор один раз своими руками, и вопрос «как это работает» закроется навсегда.