Skip to content

Kaikki WordPressistä, web-kehityksestä — ja paljon muuta

🧮 Yksinkertainen JavaScript-laskin: vaiheittainen opas

🧮 Yksinkertainen JavaScript-laskin: vaiheittainen opas

Tarvitsetko sivustollesi yksinkertaisen laskimen, toimituskululaskurin, asuntolainalaskurin tai vain jotain "laske lennossa" -toimintoa? Taustajärjestelmän kirjoittaminen tätä varten on kuin tykillä ampumista varpusiin. Selainpään JavaScript hoitaa homman illassa.

Useimpien oppaiden ongelma: ne tarjoavat jQuery-versiota vuodelta 2017, jossa käytetään eval()-funktiota, ja se on tietoturva-aukko, josta ne eivät kerro. Me otamme toisenlaisen reitin: rakennamme laskimen puhtaalla JavaScriptillä, asianmukaisella asettelulla ja ilman "asenna kolme riippuvuutta" -agendaa.

Tämän artikkelin lopussa sinulla on toimiva koodi, joka laskee yhteen, vähentää, kertoo ja jakaa, sekä ymmärrys siitä, miksi eval()-funktiota on parempi välttää.

💡 Pikakatsaus:

  • HTML: rakenna laskimen merkintä CSS Gridillä, ei taulukkoa eikä keskitystä
  • CSS: lisää responsiivinen grid, painikkeiden tyylit ja näyttö
  • JavaScript: kirjoita klikkausten sieppaus, lausekkeen keräys ja laskenta ilman eval()-funktiota
  • Tietoturva: opi, millä eval() korvataan ja miksi sillä on merkitystä

Vaihe 1: HTML, laskimen merkintä

Ensin rakennamme rungon. Ei <center>- ja <table border="4"> -elementtejä vuodelta 2005, vaan käytämme CSS Gridiä: neljä saraketta numeroille ja operaattoreille sekä yksi näyttörivi ylhäällä.

Luo index.html-tiedosto ja lisää tämä merkintä:

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>

Tässä näyttö (<input>) on vain luku -tilassa: käyttäjä ei syötä tietoja näppäimistöltä vaan klikkaa painikkeita. Tämä suojaa roskamerkeiltä lausekkeessa. Painikkeet on jaoteltu data-action-attribuuteilla: "number", "operator", "calculate" ja "clear", jotta skripti tietää, mitä klikkauksella tehdään. data-value-attribuutti tallentaa painikkeen arvon: numeron tai laskutoimitusmerkin.

Huomio: näytössä on readonly-attribuutti, joten käyttäjä ei pysty kirjoittamaan sinne näppäimistöltä esimerkiksi alert('hack')-koodia.

Vaihe 2: CSS, ulkoasu

Sitten tyylit. Ilman CSS:ää laskin näyttää pinolta painikkeita, grid korjaa kaiken. Lisää saman index.html-tiedoston <head>-osion sisään:

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) -grid antaa tasaisen neljän sarakkeen näppäimistön, painikkeet jakautuvat itsekseen. "0"-painike on venytetty kahden sarakkeen levyiseksi grid-column: span 2 -määrityksellä, mikä on vakiomenetelmä laskimen asettelussa.

Värit ovat tässä toiminnallisia: oranssi operaattoreille, sininen "yhtä kuin" -painikkeelle, punainen nollaukselle. Käyttäjä hahmottaa painikkeen tarkoituksen välittömästi, jo ennen tekstin lukemista.

Vaihe 3: JavaScript, logiikka ilman eval()-funktiota

Pääosa. Liitetään skripti <body>-osion loppuun samassa index.html-tiedostossa:

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 'Error';
44 }
45 return Math.round(result * 1e10) / 1e10;
46 } catch (e) {
47 return 'Error';
48 }
49}
50</script>

Puretaan, mitä tässä tapahtuu, rivi riviltä.

Klikkauksen sieppaus. addEventListener ei ole kiinnitetty jokaiseen painikkeeseen vaan .buttons-säiliöön, tämä on tapahtumien delegointia. Klikkauksessa e.target.closest('button') etsii lähimmän painikkeen (vaikka klikkaisit sen sisällä olevaa tekstiä). btn.dataset.action lukee data-action-attribuutin, ja sen jälkeen logiikka haarautuu.

Lausekkeen keräys. Jokainen numeron painallus lisää sen expression-merkkijonoon. Operaattori lisätään vain, jos lauseke ei ole tyhjä ja viimeinen merkki ei ole operaattori (suoja tuplia ++- ja */-merkkejä vastaan). Näyttö päivittyy jokaisella painalluksella, käyttäjä näkee koko merkkijonon, kuten oikeassa laskimessa.

Laskenta. eval()-funktion sijaan käytetään new Function() -konstruktoria. Sekin suorittaa merkkijonoa koodina, mutta yhdellä erolla: eval():lla on pääsy paikalliseen näkyvyysalueeseen, kun taas Function:lla on pääsy vain globaaliin näkyvyysalueeseen. Tämä ei ole hopealuoti, mutta selainpään laskimessa, jossa käyttäjä syöttää lausekkeen itse, se on riittävä eristystaso. Laskennan jälkeen tulos pyöristetään 10 desimaalin tarkkuuteen, muuten 0.1 + 0.2 muuttuu muotoon 0.30000000000000004.

Nollaus. "C"-painike nollaa sekä expression-muuttujan että näytön.

Tyhmyyssuojaus. Jos lauseke on tyhjä, calculate ja operator ohitetaan äänettömästi. Nollalla jakaminen antaa tulokseksi Infinity, isFinite() sieppaa tämän ja tulostaa "Error".

Vaihe 4: kaiken kokoaminen

Yhdistetään kolme osaa yhdeksi tiedostoksi. Tässä on valmis index.html, jonka voi avata selaimessa:

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 Calculator</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 'Error';
105 return Math.round(result * 1e10) / 1e10;
106 } catch (e) {
107 return 'Error';
108 }
109 }
110 </script>
111</body>
112</html>

Tallenna se, avaa selaimessa, laskin toimii välittömästi, ei palvelinta, ei npm install -komentoa. Käytännössä tämä koodi riittää toimituskululaskimeen laskeutumissivulla tai alennuslaskentalomakkeeseen.

JavaScript-laskimen ulkoasu

Miksi ei eval(): tietoturva, josta aloittelijaoppaat eivät mainitse

Jos olet lukenut vanhoja tutoriaaleja, olet todennäköisesti nähnyt eval(expression)-kutsun. Kyllä, se toimii. Mutta ongelma on, että eval() suorittaa mitä tahansa JavaScript-koodia, joka sille annetaan merkkijonona. Jos alert(document.cookie) päätyy näyttöön, se suoritetaan. Ja jos näyttö on avoinna manuaaliselle syötölle, sinne voi syöttää jotain vakavampaakin.

Meidän tapauksessamme näyttö toimii readonly-tilassa, lauseke kootaan vain painikkeita klikkaamalla. Riski on minimaalinen. Mutta tapa käyttää eval()-funktiota on huono: jonain päivänä kopioit tämän mallin lomakkeeseen, jossa on käyttäjän syöte, ja saat XSS-aukon.

Vaihtoehtoja on kolme, vaikeusjärjestyksessä:

Lähestymistapa

Suojaus

Monimutkaisuus

new Function() (kuten meillä)

Ei näe paikallisia muuttujia, vain globaalit

Minimaalinen

Manuaalinen jäsennin (merkkijonon pilkkominen operaattoreittain)

Täysi hallinta lausekkeesta

Keskitaso

Web Worker eristetyllä kontekstilla

Lauseke ei koske DOMiin lainkaan

Korkea

Opetuskäyttöön tarkoitetulle laskimelle ja useimpiin todellisiin skenaarioihin new Function() ja tuloksen tarkistus riittää. Mutta jos upotat laskimen verkkokaupan lomakkeeseen (hinta × määrä), älä vaivaudu merkkijonojen jäsentämisen kanssa lainkaan: ota arvot kentistä ja laske suoraan: priceInput.value * qtyInput.value. Siihen ei tarvita eval-funktiota alun alkaenkaan.

Miten laajennat: näppäimistösyöte ja operaatioketjut

Kun perusversio toimii, lisää kaksi ominaisuutta, jotka muuttavat käsityön työkaluksi:

Näppäimistösyöte. Kiinnitä addEventListener('keydown', ...) document-objektiin ja yhdistä näppäimet painikkeisiin: numerot ja piste data-action="number"-toimintoon, Enter "yhtä kuin" -toimintoon, Escape "C"-toimintoon, +-*/ operaattoreihin. Ohita kirjaimet ja erikoismerkit.

Operaatioketjut. Tällä hetkellä laskimemme laskee "kaiken kerralla". Mutta jos painat 5 + 3 = ja sitten * 2, se ei jatka, koska lauseke on jo nollattu tulokseen. Tämä korjataan yksinkertaisesti: =-painalluksen jälkeen älä ylikirjoita expression-muuttujaa, vaan tallenna viimeisin tulos erilliseen lastResult-muuttujaan. Seuraavalla operaattorilla aloita lauseke lastResult-arvolla.

Molemmat parannukset ovat 15-20 riviä koodia kumpikin. Kokeile niitä itse harjoituksena: se on paras tapa ymmärtää, että laskin ei ole "opetusprojekti" vaan normaali insinöörityön minitehtävä.

⁉️🤔 Usein kysytyt kysymykset

Pärjääkö ilman JavaScriptiä lainkaan?

Neljälle peruslaskutoimitukselle selaimessa ei. HTML ja CSS ovat merkintää ja tyylejä, ne eivät tee laskentaa. Palvelinpuolen laskenta PHP:llä tarkoittaa sivun uudelleenlatausta jokaisella "="-painalluksella. JavaScript on tässä ainoa käytännöllinen vaihtoehto. On olemassa CSS-temppuja, joissa käytetään calc()-funktiota ja valintaruutuja logiikan jäljittelyyn, mutta se on sirkustemppu, ei laskin: maksimissaan yksi operaatio, ei nollausta, ja koodi on kauheaa.

Miten tämä lähestymistapa eroaa "ota valmis npm-paketti" -ajattelusta?

Ei ainuttakaan riippuvuutta. npm install calculator-lib vetää mukanaan 40 muuta pakettia, bundlerin ja puoli megatavua nippua, kahden numeron yhteenlaskua varten. Meidän versiomme: yksi HTML-tiedosto, 90 riviä JavaScriptiä, avautuu missä tahansa selaimessa ilman palvelinta. Useimpiin tyypillisiin tehtäviin, toimituskululaskuri, asuntolainalaskenta laskeutumissivulla, "laske hinta" -lomake, tämä riittää.

Miksei jQueryä?

jQuery laskimeen on kuin pikkubussi leivän hakemiseen. Vuonna 2017 siinä oli vielä järkeä: $('.btn') on lyhyempi kuin document.querySelectorAll('.btn'), ja selaimet käyttäytyivät eri tavoin. Nykyään addEventListener ja closest() toimivat samoin kaikissa selaimissa, ja 87 kilotavun pakatun jQueryn raahaaminen viittä klikkauskäsittelijää varten on liioittelua. Jos jQuery on jo projektissa, käytä sitä, mutta uuteen koodiin kirjoita puhdasta JS:ää.

Mitä tehdä liukuluvuille? 0.1 + 0.2 antaa edelleen 0.30000000000000004.

Koodissa laitoimme Math.round(result * 1e10) / 1e10, tämä poistaa häntäosan kymmenen numeron tarkkuudella. Talouslaskentaan (hinnat, prosentit) käytä kokonaislukuja, laske senteissä, älä euroissa: (100 + 200) / 100 = 3.00 sen sijaan että 0.1 + 0.2. Tässä ei tarvita eval- eikä Function-funktiota, ota vain numerot kentistä ja laske ne yhteen.

Kannattaako kirjoittaa oma laskin vai ottaa valmis?

Jos tarvitset laskimen yhdelle sivulle, kirjoita se itse tunnissa tämän oppaan avulla. Koodia on alle 100 riviä, nolla riippuvuutta, hallitset toiminnan itse.

Jos upotat laskentaa WooCommerceen tai tilauslomakkeeseen, älä kirjoita lausekkeen jäsennintä. Ota arvot syötekentistä ja laske suoraan: total = basePrice + optionsSum. Tämä on luotettavampaa, yksinkertaisempaa ja turvallisempaa kuin mikään merkkijonolaskenta.

Artikkelin koodi on toimiva perusta. Muuta asettelua, lisää prosentit, muisti (M+/M-/MR-painikkeet), operaatiohistoria. Kirjoita laskin kerran omin käsin, ja kysymys "miten tämä toimii" on ratkaistu lopullisesti.