
🧮 Prosty kalkulator w JavaScript: przewodnik krok po kroku
Potrzebuje Pan/Pani prostego kalkulatora na stronie, kalkulatora dostawy, kalkulatora hipotecznego czy po prostu narzędzia, żeby „coś na szybko policzyć"? Pisanie do tego backendu to jak strzelanie z armaty do wróbli. JavaScript po stronie klienta rozwiązuje sprawę w jeden wieczór.
Problem większości poradników: dają Panu/Pani wersję jQuery z 2017 roku z eval(), a to dziura w zabezpieczeniach, o której nie wspomną. Pójdziemy inną drogą: zbudujemy kalkulator na czystym JavaScript, z normalnym układem i bez mantry „zainstaluj trzy zależności".
Na końcu artykułu będzie Pan/Pani mieć działający kod, który dodaje, odejmuje, mnoży i dzieli, a także zrozumienie, dlaczego lepiej nie używać eval().
💡 Szybki przegląd:
- HTML: proszę zbudować znaczniki kalkulatora na CSS Grid, bez table i center
- CSS: proszę dodać responsywną siatkę, stylizację przycisków i wyświetlacza
- JavaScript: proszę napisać przechwytywanie kliknięć, budowanie wyrażenia i obliczanie bez eval()
- Bezpieczeństwo: proszę dowiedzieć się, czym zastąpić eval() i dlaczego to ważne
Krok 1: HTML, znaczniki kalkulatora
Najpierw budujemy szkielet. Żadnych <center> i <table border="4"> rodem z 2005 roku, używamy CSS Grid: cztery kolumny na cyfry i operatory plus wiersz wyświetlacza na górze.
Proszę utworzyć plik index.html i dodać następujące znaczniki:
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>
Tutaj wyświetlacz (<input>) jest polem tylko do odczytu: użytkownik nie wprowadza danych z klawiatury, tylko klika przyciski. Chroni to przed niepożądanymi znakami w wyrażeniu. Przyciski są rozróżnione atrybutami data-action: „number", „operator", „calculate" i „clear", aby skrypt wiedział, co zrobić z kliknięciem. Atrybut data-value przechowuje wartość przycisku: cyfrę lub znak działania.
Proszę zwrócić uwagę: wyświetlacz z atrybutem readonly, użytkownik nie będzie mógł wpisać tam alert('взлом') z klawiatury.
Krok 2: CSS, wygląd
Teraz style. Bez CSS kalkulator wygląda jak stos przycisków, siatka wszystko naprawia. W tym samym pliku index.html, wewnątrz <head>, proszę dodać:
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>
Siatka grid-template-columns: repeat(4, 1fr) daje równą klawiaturę z czterech kolumn, przyciski same się rozmieszczają. Przycisk „0" jest rozciągnięty na dwie kolumny przez grid-column: span 2, to standardowy zabieg w układzie kalkulatora.
Kolory są tu funkcjonalne: pomarańczowy to operatory, niebieski to „równa się", czerwony to reset. Użytkownik natychmiast odczytuje przeznaczenie przycisku, jeszcze zanim przeczyta tekst.
Krok 3: JavaScript, logika bez eval()
Główna część. Podłączmy skrypt na końcu <body> tego samego pliku index.html:
1 <script> 2 const display = document.querySelector('.display'); 3 4 let expression = ''; 5 6 document.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 38 function 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>
Przeanalizujmy, co tu się dzieje, linijka po linijce.
Przechwytywanie kliknięć. addEventListener jest podpinany nie do każdego przycisku, tylko do kontenera .buttons, to delegacja zdarzeń. Przy kliknięciu e.target.closest('button') znajduje najbliższy przycisk (nawet jeśli kliknięto w tekst wewnątrz niego). btn.dataset.action odczytuje atrybut data-action, a następnie logika się rozgałęzia.
Budowanie wyrażenia. Każde naciśnięcie cyfry dopisuje ją do ciągu expression. Operator jest dopisywany tylko wtedy, gdy wyrażenie nie jest puste i ostatni znak nie jest operatorem (zabezpieczenie przed podwójnymi ++ i */). Wyświetlacz jest aktualizowany przy każdym naciśnięciu, użytkownik widzi cały ciąg, jak na prawdziwym kalkulatorze.
Obliczanie. Zamiast eval() używamy konstruktora new Function(). On również wykonuje ciąg jako kod, ale z jedną różnicą: eval() ma dostęp do lokalnego zakresu zmiennych, a Function tylko do globalnego. To nie jest srebrna kula, ale dla kalkulatora po stronie klienta, gdzie użytkownik sam wprowadza wyrażenie, to wystarczający poziom izolacji. Po obliczeniu wynik jest zaokrąglany do 10 miejsc po przecinku, w przeciwnym razie 0.1 + 0.2 zamieni się w 0.30000000000000004.
Resetowanie. Przycisk „C" zeruje zarówno expression, jak i wyświetlacz.
Zabezpieczenie przed błędami. Jeśli wyrażenie jest puste, calculate i operator są po cichu ignorowane. Dzielenie przez zero daje Infinity, isFinite() wyłapuje to i wyświetla „Błąd".
Krok 4: składamy wszystko razem
Połączmy trzy części w jednym pliku. Oto kompletny index.html, który można otworzyć w przeglądarce:
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>
Proszę zapisać, otworzyć w przeglądarce, kalkulator działa od razu, bez serwera, bez npm install. W praktyce taki kod wystarczy do kalkulatora dostawy na landing page lub formularza obliczania rabatu.

Dlaczego nie eval(): bezpieczeństwo, o którym milczą poradniki dla początkujących
Jeśli czytał Pan/Pani stare tutoriale, na pewno widział Pan/Pani eval(expression). Tak, to działa. Problem jednak w tym, że eval() wykonuje dowolny kod JavaScript przekazany jako ciąg. Jeśli na wyświetlacz trafi alert(document.cookie), zostanie wykonany. A jeśli wyświetlacz jest otwarty na ręczne wprowadzanie, można tam podstawić coś poważniejszego.
W naszym przypadku wyświetlacz działa w trybie readonly, wyrażenie jest budowane wyłącznie przez klikanie przycisków. Ryzyko jest minimalne. Ale nawyk używania eval() jest zły: pewnego dnia skopiuje Pan/Pani ten wzorzec do formularza z danymi od użytkownika i otrzyma dziurę XSS.
Są trzy alternatywy, według rosnącej złożoności:
Podejście | Ochrona | Złożoność |
|---|---|---|
| Nie widzi zmiennych lokalnych, tylko globalne | Minimalna |
Ręczny parser (analiza ciągu według operatorów) | Pełna kontrola nad wyrażeniem | Średnia |
Web Worker + izolowany kontekst | Wyrażenie w ogóle nie dotyka DOM | Wysoka |
Dla edukacyjnego kalkulatora i większości rzeczywistych scenariuszy new Function() z weryfikacją wyniku jest wystarczające. Ale jeśli osadza Pan/Pani kalkulator w formularzu e-commerce (cena × ilość), proszę w ogóle nie zawracać sobie głowy parsowaniem ciągu: proszę pobrać wartości z pól i policzyć bezpośrednio: priceInput.value * qtyInput.value. Żaden eval nie jest tam od samego początku potrzebny.
Jak rozszerzyć: wprowadzanie z klawiatury i łańcuchy operacji
Gdy podstawowa wersja zacznie działać, proszę dodać dwie funkcje, które zmieniają samoróbkę w narzędzie:
Wprowadzanie z klawiatury. Proszę podpiąć addEventListener('keydown', ...) do document i dopasować klawisze do przycisków: cyfry i kropka do data-action="number", Enter do „równa się", Escape do „C", +-*/ do operatorów. Litery i znaki specjalne proszę ignorować.
Łańcuchy operacji. Obecnie nasz kalkulator liczy „wszystko naraz". Ale jeśli nacisnąć 5 + 3 =, a potem * 2, nie będzie kontynuował, expression jest już nadpisane wynikiem. Poprawia się to prosto: po = proszę nie nadpisywać expression, tylko zapisać ostatni wynik w osobnej zmiennej lastResult. Przy następnym operatorze proszę zaczynać expression od lastResult.
Obie modyfikacje to po 15-20 linijek kodu każda. Proszę spróbować samodzielnie jako ćwiczenie: to najlepszy sposób, by zrozumieć, że kalkulator to nie „projekt edukacyjny", tylko normalne, małe zadanie inżynierskie.
⁉️🤔 Często zadawane pytania
Czy można obejść się całkowicie bez JavaScript?
Dla czterech działań arytmetycznych po stronie klienta nie. HTML i CSS to znaczniki i style, nie wykonują obliczeń. Obliczenia po stronie serwera przez PHP oznaczają przeładowanie strony przy każdym naciśnięciu „=". JavaScript jest tu jedyną praktyczną opcją. Istnieją sztuczki CSS z
calc()i checkboxami, które imitują logikę, ale to atrakcja, a nie kalkulator: maksymalnie na jedno działanie, bez resetowania, z koszmarnym kodem.
Czym to podejście różni się od „weź gotowy pakiet npm"?
Brakiem jakichkolwiek zależności.
npm install calculator-libciągnie za sobą jeszcze 40 pakietów, bundler i pół megabajta paczki, wszystko po to, by dodać dwie liczby. Nasza wersja: jeden plik HTML, 90 linijek JavaScript, otwiera się w dowolnej przeglądarce bez serwera. Dla większości typowych zadań, kalkulator dostawy, kalkulacja hipoteczna na landing page, formularz „oblicz koszt", to wystarczy.
Dlaczego nie jQuery?
jQuery do kalkulatora to jak mikrobus po bułki. W 2017 roku miało to jeszcze sens:
$('.btn')jest krótsze niżdocument.querySelectorAll('.btn'), a przeglądarki zachowywały się różnie. DziśaddEventListenericlosest()działają tak samo we wszystkich przeglądarkach, a ciągnięcie 87 KB skompresowanego jQuery dla pięciu procedur obsługi kliknięcia jest zbędne. Jeśli jQuery jest już w projekcie, proszę go użyć, ale do nowego kodu proszę pisać na czystym JS.
Co zrobić z liczbami zmiennoprzecinkowymi? 0.1 + 0.2 i tak daje 0.30000000000000004.
W kodzie ustawiliśmy
Math.round(result * 1e10) / 1e10, to usuwa ogon dla dziesięciu miejsc. Do obliczeń finansowych (ceny, procenty) proszę używać liczb całkowitych, liczyć w groszach, a nie w złotówkach:(100 + 200) / 100 = 3.00zamiast0.1 + 0.2. ŻadenevalaniFunctionnie są tu potrzebne, proszę po prostu wziąć liczby z pól i dodać.
Czy warto pisać własny kalkulator, czy wziąć gotowy?
Jeśli potrzebuje Pan/Pani kalkulatora na jedną stronę, proszę napisać go samodzielnie w godzinę według tego poradnika. Kodu jest mniej niż 100 linijek, zero zależności, zachowanie jest pod Pana/Pani kontrolą.
Jeśli osadza Pan/Pani obliczenia w WooCommerce lub formularzu zamówienia, proszę nie pisać parsera wyrażeń. Proszę pobrać wartości z pól wejściowych i policzyć bezpośrednio: total = basePrice + optionsSum. To pewniejsze, prostsze i bezpieczniejsze niż jakiekolwiek obliczenia na ciągach znaków.
Kod z artykułu to działająca podstawa. Proszę zmieniać układ, dodawać procenty, pamięć (przyciski M+/M-/MR), historię operacji. Proszę napisać kalkulator raz własnymi rękami, a pytanie „jak to działa" zostanie zamknięte na zawsze.



