Articolo originale: How to build an HTML calculator app from scratch using JavaScript di Zell Liew

Tradotto e adattato da: Angelo Mirabelli

Questo è un articolo epico in cui impari come costruire una calcolatrice da zero. Ci concentreremo su cosa è necessario scrivere in JavaScript: come pensare alla creazione della calcolatrice, come scrivere il codice e, infine, come ripulire il tuo codice.

Alla fine dell'articolo, dovresti ottenere una calcolatrice che funziona esattamente come una calcolatrice per iPhone (senza +/- e la funzione percentuale).

Cw7jNVIhWFV4NSNY8-Lv8uX4583Hr5LvzYFq

I prerequisiti

Prima di provare a seguire la lezione, assicurati di avere una discreta padronanza di JavaScript. Come minimo, devi sapere queste cose:

  1. Istruzioni if/else
  2. Cicli for
  3. Funzioni in JavaScript
  4. Funzioni arrow
  5. Opertori && e ||
  6. Come modificare il testo con la proprietà textContent
  7. Come aggiungere il listener di eventi con il modello di delega di eventi

Prima di iniziare

Ti esorto a provare a costruire tu stesso la calcolatrice prima di seguire la lezione. È una buona pratica, perché ti allenerai a pensare come uno sviluppatore.

Torna a questa lezione dopo aver provato per un'ora (non importa se ci riesci o fallisci. Quando ci provi, pensi, e questo ti aiuterà ad assorbire la lezione il doppio più velocemente).

Detto questo, iniziamo col capire come funziona una calcolatrice.

Costruire la calcolatrice

Per prima cosa, vogliamo costruire la calcolatrice.

La calcolatrice è composta da due parti: il display e i tasti.

rfV0r9RtFghhau8sZU5CzOFMuJAT1H48tFeL
<div class="calculator">
  <div class="calculator__display">0</div>
  <div class="calculator__keys"> … </div>
</div>

Possiamo usare CSS Grid per creare i tasti, poiché sono disposti in un formato simile a una griglia. Questo è già stato fatto per te nel file di partenza. Puoi trovare il file di partenza qui .

.calculator__keys { 
  display: grid; 
  /* other necessary CSS */ 
}

Per aiutarci a identificare i tasti per le operazioni, per la virgola, per  cancella e per uguale, forniremo un attributo chiamato data-action che descrive ciò che fanno.

<div class="calculator__keys">
  <button class="key--operator" data-action="add">+</button>
  <button class="key--operator" data-action="subtract">-</button
  <button class="key--operator" data-action="multiply">&times;</button>
  <button class="key--operator" data-action="divide">÷</button
  <button>7</button>
  <button>8</button>
  <button>9</button>
  <button>4</button>
  <button>5</button>
  <button>6</button>
  <button>1</button>
  <button>2</button>
  <button>3</button>
  <button>0</button>
  <button data-action="decimal">.</button>
  <button data-action="clear">AC</button>
  <button class="key--equal" data-action="calculate">=</button>
</div>

Ascolto della pressione dei tasti

Cinque cose possono accadere quando una persona prende una calcolatrice. Può schiacciare:

  1. un tasto numerico (0–9)
  2. un tasto operatore (+, -, ×, ÷)
  3. il tasto virgola
  4. il tasto di uguale
  5. il tasto cancella

I primi passi per costruire questa calcolatrice sono: essere in grado di (1) ascoltare tutte le pressioni dei tasti e (2) determinare il tipo di tasto che viene premuto. In questo caso, possiamo utilizzare un modello di delega di eventi per l'ascolto, poiché i tasti sono tutti figli di .calculator__keys.

const calculator = document.querySelector('.calculator')
const keys = calculator.querySelector('.calculator__keys')

keys.addEventListener('click', e => {
 if (e.target.matches('button')) {
   // Do something
 }
})

Successivamente, possiamo utilizzare l'attributo data-action per determinare il tipo di tasto su cui si fa clic.

const key = e.target
const action = key.dataset.action

Se il tasto non ha un attributo data-action , allora deve essere un tasto numerico.

if (!action) {
  console.log('number key!')
}

Se il tasto ha un attributo data-action che è  add, subtract, multiply o divide, allora capiamo che è un tasto operatore.

if (
  action === 'add' ||
  action === 'subtract' ||
  action === 'multiply' ||
  action === 'divide'
) {
  console.log('operator key!')
}

Se l'attributo data-action del tasto è decimal, sappiamo che l'utente ha fatto clic sulla tasto decimale.

Seguendo lo stesso schema, se l'attributo data-action del tasto è clear, sappiamo che l'utente ha fatto clic sul tasto Cancella (quello con scritto AC). Se l'attributo data-action del tasto è calculate, sappiamo che l'utente ha cliccato sul tasto uguale.

if (action === 'decimal') {
  console.log('decimal key!')
}

if (action === 'clear') {
  console.log('clear key!')
}

if (action === 'calculate') {
  console.log('equal key!')
}

A questo punto, dovresti ricevere una risposta console.log da ogni tasto della calcolatrice.

lbXTncsu2Ni5V-Ejx6RYCO-kW8XJm7f5woGC

Costruzione in uno scenario standard

Consideriamo cosa farebbe la persona media quando prende in mano una calcolatrice. Questo “ciò che farebbe la persona media” è chiamato lo scenario standard .

Chiamiamo la nostra persona media Mary.

Quando Mary prende in mano una calcolatrice, potrebbe premere uno qualsiasi di questi tasti:

  1. un tasto numerico (0–9)
  2. un tasto operatore (+, -, ×, ÷)
  3. il tasto virgola
  4. il tasto uguale
  5. il tasto cancella

Può essere pesante considerare cinque tipi di tasti contemporaneamente, quindi procediamo passo dopo passo.

Quando un utente preme un tasto numerico

A questo punto, se la calcolatrice mostra 0 (il numero di default), il numero selezionato dovrebbe sostituire lo zero.

mpr4JFLSU-MHaq8LPMedsaDxnU5Y-MTx56SU

Se la calcolatrice mostra un numero diverso da zero, il numero selezionato deve essere aggiunto al numero visualizzato.

PNfa-nAlgIBtFt1MaVEDvuzisaIps6Kdb482

Qui, è necessario sapere due cose:

  1. Il numero del tasto su cui è stato fatto clic
  2. Il numero attualmente visualizzato

Possiamo ottenere questi due valori rispettivamente attraverso la proprietà textContent  del tasto cliccato e di .calculator__display .

const display = document.querySelector('.calculator__display')

keys.addEventListener('click', e => {
  if (e.target.matches('button')) {
    const key = e.target
    const action = key.dataset.action
    const keyContent = key.textContent
    const displayedNum = display.textContent
    // ...
  }
})

Se la calcolatrice mostra 0, vogliamo sostituire il valore mostrato sul display della calcolatrice con quello del tasto cliccato. Possiamo farlo sostituendo la proprietà textContent del display.

if (!action) {
  if (displayedNum === '0') {
    display.textContent = keyContent
  }
}

Se la calcolatrice mostra un numero diverso da zero, vogliamo aggiungere il tasto cliccato al numero visualizzato. Per aggiungere un numero, concateniamo una stringa.

if (!action) {
  if (displayedNum === '0') {
    display.textContent = keyContent
  } else {
    display.textContent = displayedNum + keyContent
  }
}

A questo punto, Mary può fare clic su uno di questi tasti:

  1. Il tasto virgpla
  2. Un tasto operatore

Diciamo che Mary preme il tasto decimale.

Quando un utente preme il tasto virgola

Quando Mary preme il tasto virgola, sul display dovrebbe apparire una virgola. Se Mary preme un numero qualsiasi dopo aver premuto il tasto virgola, anche il numero dovrebbe essere aggiunto sul display.

5Pc6RLFHdPNzPi3BrlXJSs3xrFf2L90A2WXx

Per creare questo effetto, possiamo concatenare . al numero visualizzato.

if (action === 'decimal') {
  display.textContent = displayedNum + '.'
}

Quindi, supponiamo che Mary continui il suo calcolo premendo un tasto operatore.

Quando un utente preme un tasto operatore

Se Mary preme un tasto operatore, l'operazione dovrebbe essere evidenziata in modo che Mary sappia che l'operazione è attiva.

VarwRgJGrN0mwcgYGpX1Zw54QRfbXdMmQNEG

Per fare ciò, possiamo aggiungere la classe is-depressed  al tasto dell'operatore.

if (
  action === 'add' ||
  action === 'subtract' ||
  action === 'multiply' ||
  action === 'divide'
) {
  key.classList.add('is-depressed')
}

Una volta che Mary ha premuto un tasto operatore, premerà un altro tasto numerico.

Quando un utente preme un tasto numerico dopo un tasto operatore

Quando Mary preme di nuovo un tasto numerico, il display precedente dovrebbe essere sostituito con il nuovo numero. Il tasto operatore dovrebbe anche rilasciare il suo stato premuto.

GDuLfupPob7rW0UWTH6RqI5CuQX36vcILKwo

Per rilasciare lo stato premuto, rimuoviamo la classe is-depressed da tutti i tasti attraverso un ciclo forEach:

keys.addEventListener('click', e => {
  if (e.target.matches('button')) {
    const key = e.target
    // ...
    
    // Remove .is-depressed class from all keys
    Array.from(key.parentNode.children)
      .forEach(k => k.classList.remove('is-depressed'))
  }
})

Successivamente, vogliamo aggiornare il display col valore del tasto cliccato. Prima di farlo, abbiamo bisogno di un modo per sapere se il tasto precedente è un tasto operatore.

Un modo per farlo è attraverso un attributo personalizzato. Chiamiamo questo attributo personalizzato data-previous-key-type.

const calculator = document.querySelector('.calculator')
// ...

keys.addEventListener('click', e => {
  if (e.target.matches('button')) {
    // ...
    
    if (
      action === 'add' ||
      action === 'subtract' ||
      action === 'multiply' ||
      action === 'divide'
    ) {
      key.classList.add('is-depressed')
      // Add custom attribute
      calculator.dataset.previousKeyType = 'operator'
    }
  }
})

Se previousKeyType è un operatore, vogliamo sostituire il numero visualizzato con il numero cliccato.

const previousKeyType = calculator.dataset.previousKeyType

if (!action) {
  if (displayedNum === '0' || previousKeyType === 'operator') {
    display.textContent = keyContent
  } else {
    display.textContent = displayedNum + keyContent
  }
}

Quindi, supponiamo che Mary decida di completare il suo calcolo premendo il tasto uguale.

Quando un utente preme il tasto uguale

Quando Mary preme il tasto uguale, la calcolatrice dovrebbe calcolare un risultato che dipende da tre valori:

  1. Il primo numero inserito nella calcolatrice
  2. L' operatore
  3. Il secondo numero inserito nella calcolatrice

Dopo il calcolo, il risultato dovrebbe sostituire il valore visualizzato.

TMFTHXrjCGzKQBIzBFApP7usoJCjcQ-oz2Jc

A questo punto, conosciamo solo il secondo numero , ovvero il numero attualmente visualizzato.

if (action === 'calculate') {
  const secondValue = displayedNum
  // ...
}

Per ottenere il primo numero , dobbiamo memorizzare il valore visualizzato della calcolatrice prima di ripulire il display. Un modo per salvare questo primo numero consiste nell'aggiungerlo a un attributo personalizzato quando si fa clic sul pulsante dell'operatore.

Per ottenere l'operatore, possiamo usare anche la stessa tecnica.

if (
  action === 'add' ||
  action === 'subtract' ||
  action === 'multiply' ||
  action === 'divide'
) {
  // ...
  calculator.dataset.firstValue = displayedNum
  calculator.dataset.operator = action
}

Una volta che abbiamo i tre valori di cui abbiamo bisogno, possiamo eseguire un calcolo. Alla fine, vogliamo che il codice assomigli a questo:

if (action === 'calculate') {
  const firstValue = calculator.dataset.firstValue
  const operator = calculator.dataset.operator
  const secondValue = displayedNum
  
  display.textContent = calculate(firstValue, operator, secondValue)
}

Ciò significa che dobbiamo creare una funzione calculate. Dovrebbe avere tre parametri: il primo numero, l'operatore e il secondo numero.

const calculate = (n1, operator, n2) => {
  // Perform calculation and return calculated value
}

Se l'operatore è add, vogliamo sommare i valori. Se l'operatore è subtract, vogliamo sottrarre i valori e così via.

const calculate = (n1, operator, n2) => {
  let result = ''
  
  if (operator === 'add') {
    result = n1 + n2
  } else if (operator === 'subtract') {
    result = n1 - n2
  } else if (operator === 'multiply') {
    result = n1 * n2
  } else if (operator === 'divide') {
    result = n1 / n2
  }
  
  return result
}

Ricorda che firstValue e secondValue sono attualmente delle stringhe. Se addizioni due stringhe, le concatenerai ( 1 + 1 = 11).

Quindi, prima di calcolare il risultato, vogliamo convertire le stringhe in numeri. Possiamo farlo con le due funzioni parseInt e parseFloat.

  • parseInt converte una stringa in un intero .
  • parseFloat converte una stringa in un float (questo significa un numero con cifre decimali).

Per una calcolatrice, abbiamo bisogno di un float.

const calculate = (n1, operator, n2) => {
  let result = ''
  
  if (operator === 'add') {
    result = parseFloat(n1) + parseFloat(n2)
  } else if (operator === 'subtract') {
    result = parseFloat(n1) - parseFloat(n2)
  } else if (operator === 'multiply') {
    result = parseFloat(n1) * parseFloat(n2)
  } else if (operator === 'divide') {
    result = parseFloat(n1) / parseFloat(n2)
  }
  
  return result
}

Questo è tutto per lo scenario standard!

Puoi ottenere il codice sorgente per lo scenario standard attraverso questo link (scorri verso il basso e inserisci il tuo indirizzo e-mail nella casella e invierò il codici sorgente direttamente alla tua casella di posta).

I casi limite

Lo scenario standard non è abbastanza. Per costruire una calcolatrice robusta, è necessario rendere la calcolatrice resiliente a schemi di input strani. Per farlo, devi immaginare un piantagrane che cerca di rompere la calcolatrice premendo i tasti nell'ordine sbagliato. Chiamiamo questo individuo Tim.

Tim può premere questi tasti in qualsiasi ordine:

  1. Un tasto numerico (0–9)
  2. Un tasto operatore (+, -, ×, ÷)
  3. Il tasto virgola
  4. Il tasto uguale
  5. Il tasto cancella

Cosa succede se Tim preme il tasto virgola

Se Tim preme il tasto virgola quando il display mostra già un punto decimale, non dovrebbe succedere nulla.

Lbvc-ZcYHO2iWjXIjdYiOVJcmPTmtwkknBw5
Orj4wS6vgnPAMYFq1xI3DEYXBMS4PWLlSw8a

Qui possiamo verificare che il numero visualizzato contenga un . con il metodo includes.

includes controlla le stringhe per una determinata corrispondenza. Se viene trovata una stringa, restituisce true; in caso contrario, ritorna false.

Nota : includes fa distinzione tra maiuscole e minuscole.

// Example of how includes work.
const string = 'The hamburgers taste pretty good!'
const hasExclaimation = string.includes('!')
console.log(hasExclaimation) // true

Per verificare se la stringa ha già un punto, facciamo questo:

// Do nothing if string has a dot
if (!displayedNum.includes('.')) {
  display.textContent = displayedNum + '.'
}

Successivamente, se Tim preme il tasto virgola dopo aver premuto un tasto operatore, il display dovrebbe mostrare 0..

fLLhOqkyFZqsOZIxgMPAkpezrUisGpDKFEsw

Qui dobbiamo sapere se il tasto precedente è un operatore. Possiamo dirlo controllando l'attributo personalizzato, data-previous-key-type, che abbiamo impostato nel paragrafo precedente.

data-previous-key-type non è ancora completo. Per identificare correttamente se previousKeyType è un operatore, dobbiamo aggiornare previousKeyType per ogni tasto cliccato.

if (!action) {
  // ...
  calculator.dataset.previousKey = 'number'
}

if (action === 'decimal') {
  // ...
  calculator.dataset.previousKey = 'decimal'
}

if (action === 'clear') {
  // ...
  calculator.dataset.previousKeyType = 'clear'
}

if (action === 'calculate') {
 // ...
  calculator.dataset.previousKeyType = 'calculate'
}

Una volta che abbiamo il corretto previousKeyType, possiamo usarlo per verificare se il tasto precedente è un operatore.

if (action === 'decimal') {
  if (!displayedNum.includes('.')) {
    display.textContent = displayedNum + '.'
  } else if (previousKeyType === 'operator') {
    display.textContent = '0.'
  }
  
calculator.dataset.previousKeyType = 'decimal'
}

Cosa succede se Tim preme un tasto operatore

Se Tim preme prima un tasto operatore, il tasto operatore dovrebbe accendersi. (Abbiamo già trattato questo caso limite, ma come? Vedi se riesci a identificare cosa abbiamo fatto).

q3D72rgBjtPOPUltYm1MMIN06dvxGOKyJyUs

Secondo, non dovrebbe succedere nulla se Tim preme più volte lo stesso tasto operatore (Abbiamo già coperto anche questo caso limite).

Nota: se vuoi fornire una migliore UX, puoi mostrare il tasto operatore che viene cliccato ripetutamente con alcune modifiche CSS. Non l'abbiamo fatto qui, ma vedi se puoi farlo tu stesso come una sfida di codifica aggiuntiva.

IXW7zY77RWE7tNQ6HZMYma73hsxW44EjWg0n

Terzo, se Tim preme un altro tasto operatore dopo aver premuto il primo tasto operatore, il primo tasto operatore dovrebbe essere rilasciato. Quindi, il secondo tasto operatore dovrebbe essere premuto. (Abbiamo coperto anche questo caso limite, ma come?).

Rez20RY9AcS6ORFWIIumk69YWzwTyv8qseM7

Quarto, se Tim clicca in sequenza un numero, un operatore, un numero e un altro operatore,  il display dovrebbe essere aggiornato al valore calcolato.

MAMWFTkNu6Ho8tlMGyJlTfjCbeYq8rO0bQyR

Ciò significa che dobbiamo usare la funzione calculate quando esistono firstValue, operator e secondValue.

if (
  action === 'add' ||
  action === 'subtract' ||
  action === 'multiply' ||
  action === 'divide'
) {
  const firstValue = calculator.dataset.firstValue
  const operator = calculator.dataset.operator
  const secondValue = displayedNum
  
// Nota: È sufficiente controllare per firstValue e operator perch{ secondValue esiste sempre
  if (firstValue && operator) {
    display.textContent = calculate(firstValue, operator, secondValue)
  }
  
key.classList.add('is-depressed')
  calculator.dataset.previousKeyType = 'operator'
  calculator.dataset.firstValue = displayedNum
  calculator.dataset.operator = action
}

Sebbene possiamo calcolare un valore quando si fa clic sul tasto operatore per la seconda volta, a questo punto abbiamo anche introdotto un bug: clic aggiuntivi sul tasto operatore calcolano un valore quando non dovrebbero.

8ktjtHeYaRTEn-lPbOM3fhEg3qrvDl5WfOVY

Per evitare che la calcolatrice esegua un calcolo ai clic successivi sul tasto operatore, è necessario verificare se il previousKeyType è un operatore. Se lo è, non eseguiamo il calcolo.

if (
  firstValue &&
  operator &&
  previousKeyType !== 'operator'
) {
  display.textContent = calculate(firstValue, operator, secondValue)
}

Quinto, dopo che il tasto operatore ha calcolato un numero, se Tim schiaccia un numero, seguito da un altro operatore, l'operatore dovrebbe continuare con il calcolo, in questo modo: 8 - 1 = 7, 7 - 2 = 5, 5 - 3 = 2.

RSsXyuKJe0biqkH-WPDdrGLhFBWmyZ2R1J2Y

Al momento, la nostra calcolatrice non può eseguire calcoli consecutivi. Il secondo valore calcolato sarà errato. Ecco cosa abbiamo: 99 - 1 = 98, 98 - 1 = 0.

0r9I8Gu7J9pMbfzUG4hL6tU7RCP-cDhsaGp1

Il secondo valore è calcolato in modo errato, perché abbiamo inserito i valori sbagliati nella funzione  calculate. Esaminiamo alcune immagini per capire cosa fa il nostro codice.

Comprendere la nostra funzione di calcolo

Per prima cosa, supponiamo che un utente clicchi su un numero, 99. A questo punto, nella calcolatrice non è ancora registrato nulla.

0hH4Cz5kOEaDOcTQ2PMPmkDl26a8JHSXNrJ7

In secondo luogo, supponiamo che l'utente faccia clic sull'operatore di sottrazione. Dopo aver fatto clic sull'operatore di sottrazione, impostiamo a 99  firstValue. Impostiamo anche operator a sottrazione.

0K-KPTzdCBgfVvVaDNcVDYSjXfUO8p5LRs2v

Terzo, supponiamo che l'utente faccia clic su un secondo valore: questa volta è 1. A questo punto, il numero visualizzato viene aggiornato a 1, ma firstValue, operator e secondValue rimangono invariati.

0MacG-A5Tl7rZeB6NLeNvghVyBpmSqaZQkn9

Quarto, l'utente fa nuovamente clic su sottrai. Subito dopo aver fatto clic su sottrai, prima di calcolare il risultato, impostiamo secondValue come numero visualizzato.

RgDMKK92og4djxxmaYO1HUYiVoetKDK9x0j7

Quinto, eseguiamo il calcolo con firstValue a 99, operator a sottrazione e secondValue a 1. Il risultato è 98.

Una volta calcolato il risultato, impostiamo la visualizzazione sul risultato. Quindi, impostiamo operator su sottrazione e firstValue al numero visualizzato precedente.

X3VFJ5ar--k84pP3pM5VDVODvYlX4fCwHcnS

Beh, è ​​terribilmente sbagliato! Se vogliamo continuare con il calcolo, dobbiamo aggiornare firstValue con il valore calcolato.

gp-lkqhUOjoo46fIwx-7oLtbV7CP7jZwzc9y
const firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
const secondValue = displayedNum

if (
  firstValue &&
  operator &&
  previousKeyType !== 'operator'
) {
  const calcValue = calculate(firstValue, operator, secondValue)
  display.textContent = calcValue
  
// Aggiorna il valore calcolato come firstValue
  calculator.dataset.firstValue = calcValue
} else {
  // Se non ci sono calcoli, imposta displayedNum come firstValue
  calculator.dataset.firstValue = displayedNum
}

key.classList.add('is-depressed')
calculator.dataset.previousKeyType = 'operator'
calculator.dataset.operator = action

Con questa correzione, i calcoli consecutivi eseguiti dai tasti operatori dovrebbero ora essere corretti.

tKZ-VlIHo7dRNHDR2BBxZChE1cgqIuMU0Uh-

Cosa succede se Tim preme il tasto uguale?

Innanzitutto, non dovrebbe succedere nulla se Tim preme il tasto uguale prima di qualsiasi tasto operatore.

FBvnFZadNPXTllID0R7JfAkrsDb5SLcWTUhV
fKJV0ZqgVf-ppPqrx-70FpByKioVL2T9oAsF

Sappiamo che i tasti operatore non sono ancora stati cliccati se firstValue non è impostato su un numero. Possiamo usare questa conoscenza per impedire al tasto uguale di eseguire il  calcolo.

if (action === 'calculate') {
  const firstValue = calculator.dataset.firstValue
  const operator = calculator.dataset.operator
  const secondValue = displayedNum
  
if (firstValue) {
    display.textContent = calculate(firstValue, operator, secondValue)
  }
  
calculator.dataset.previousKeyType = 'calculate'
}

In secondo luogo, se Tim clicca un numero, seguito da un operatore, seguito da un uguale, la calcolatrice dovrebbe calcolare il risultato in modo tale che:

  1. 2 + = —> 2 + 2 = 4
  2. 2 - = —> 2 - 2 = 0
  3. 2 × = —> 2 × 2 = 4
  4. 2 ÷ = —> 2 ÷ 2 = 1
MUgIi0ck8OJRV18hfJ-kdn8k7Ydyy5mDvV6z

Abbiamo già preso in considerazione questo strano input. Riesci a capire perché? :)

Terzo, se Tim preme il tasto uguale dopo che un calcolo è stato completato, un altro calcolo dovrebbe essere eseguito di nuovo. Ecco come dovrebbe essere letto il calcolo:

  1. Tim preme i tasti 5–1
  2. Tim schiaccia uguale. Il valore calcolato è 5 - 1 = 4
  3. Tim schiaccia uguale. Il valore calcolato è 4 - 1 = 3
  4. Tim schiaccia uguale. Il valore calcolato è 3 - 1 = 2
  5. Tim schiaccia uguale. Il valore calcolato è 2 - 1 = 1
  6. Tim schiaccia uguale. Il valore calcolato è 1 - 1 = 0
vB2oVoTXZsMABqV60qqclJhoOxYu2JeVhLx4

Sfortunatamente, la nostra calcolatrice scompiglia questo calcolo. Ecco cosa mostra il nostro calcolatore:

  1. Tim preme il tasto 5–1
  2. Tim schiaccia uguale. Il valore calcolato è 4
  3. Tim schiaccia uguale. Il valore calcolato è 1
8roqRbhSH3hLVvtK7t-T2iRsRegqPWSrn4SF

Correzione del calcolo

Per prima cosa, supponiamo che il nostro utente faccia clic su 5. A questo punto, nel calcolatore non è ancora registrato nulla.

2vf5VGXNZ0vjGkyaY0y22PRTqqHDwgEKvCC3

In secondo luogo, supponiamo che l'utente faccia clic sull'operatore di sottrazione. Dopo aver fatto clic sull'operatore di sottrazione, impostiamo firstValue su 5. Impostiamo anche operator su sottrazione.

Fc-QupYbv3HInXqv1vHFCc1avhDe3iyEErhs

Terzo, l'utente fa clic su un secondo valore. Diciamo che è 1. A questo punto, il numero visualizzato viene aggiornato a 1, ma  firstValue, operatore secondValue rimangono invariati.

lW3CtoXJ1gxpUS5SZM3zh3zmqSB-ksM6E0vr

Quarto, l'utente fa clic sul tasto uguale. Subito dopo aver fatto clic su uguale, ma prima del calcolo, impostiamo secondValue come displayedNum

yeQCYcu0ecbNbJlHa9aqEZopHj-FyTqXuRmw

Quinto, la calcolatrice calcola il risultato di 5 - 1 e restituisce 4. Il risultato viene aggiornato sul display. firstValue e operator vengono riportati al calcolo successivo poiché non li abbiamo aggiornati.

YOsfq7AWCs0YbABkiebax-oaQVGc5tWsNyXJ

Sesto, quando l'utente schiaccia di nuovo uguale, impostiamo  secondValue con displayedNum priema del calcolo.

BF7tBEUHJN4gnIwQqUTq9ctHIUIVcYM026Ro

Puoi dire cosa c'è che non va qui.

Invece di secondValue, vogliamo impostare firstValue con il numero visualizzato.

if (action === 'calculate') {
  let firstValue = calculator.dataset.firstValue
  const operator = calculator.dataset.operator
  const secondValue = displayedNum
  
if (firstValue) {
    if (previousKeyType === 'calculate') {
      firstValue = displayedNum
    }
    
display.textContent = calculate(firstValue, operator, secondValue)
  }
  
calculator.dataset.previousKeyType = 'calculate'
}

Vogliamo anche portare avanti il ​​valore precedente di secondValue nel nuovo calcolo. Per mantenere secondValue fino al calcolo successivo, dobbiamo memorizzarlo in un altro attributo personalizzato. Chiamiamo questo attributo personalizzato modValue (sta per valore modificatore).

if (action === 'calculate') {
  let firstValue = calculator.dataset.firstValue
  const operator = calculator.dataset.operator
  const secondValue = displayedNum
  
if (firstValue) {
    if (previousKeyType === 'calculate') {
      firstValue = displayedNum
    }
    
display.textContent = calculate(firstValue, operator, secondValue)
  }
  
// Assegna l'attributo modValue
  calculator.dataset.modValue = secondValue
  calculator.dataset.previousKeyType = 'calculate'
}

Se  previousKeyType è calculate, sappiamo che possiamo usare calculator.dataset.modValue come secondValue. Una volta che lo sappiamo, possiamo eseguire il calcolo.

if (firstValue) {
  if (previousKeyType === 'calculate') {
    firstValue = displayedNum
    secondValue = calculator.dataset.modValue
  }
  
display.textContent = calculate(firstValue, operator, secondValue)
}

Con ciò, abbiamo il calcolo corretto quando si fa clic consecutivamente sul tasto uguale.

sjYX-ImohfhbFFbw1-FqmKagBvfFQKm0PzAu

Torna al tasto di uguale

Quarto, se Tim preme un tasto decimale o un tasto numerico dopo il tasto della calcolatrice, il display dovrebbe essere sostituito con 0. o con  il nuovo numero.

Qui, invece di controllare solo se previousKeyType è operator, dobbiamo anche verificare se è calculate.

if (!action) {
  if (
    displayedNum === '0' ||
    previousKeyType === 'operator' ||
    previousKeyType === 'calculate'
  ) {
    display.textContent = keyContent
  } else {
    display.textContent = displayedNum + keyContent
  }
  calculator.dataset.previousKeyType = 'number'
}

if (action === 'decimal') {
  if (!displayedNum.includes('.')) {
    display.textContent = displayedNum + '.'
  } else if (
    previousKeyType === 'operator' ||
    previousKeyType === 'calculate'
  ) {
    display.textContent = '0.'
  }
  
calculator.dataset.previousKeyType = 'decimal'
}

Quinto, se Tim preme un tasto operatore subito dopo il tasto uguale, la calcolatrice non dovrebbe calcolare.

uuifuJ41Oo86NXMsPj44RSQf7ExULROc2GaI

Per fare ciò, controlliamo se previousKeyType è calculate prima di eseguire calcoli con i tasti operatore.

if (
  action === 'add' ||
  action === 'subtract' ||
  action === 'multiply' ||
  action === 'divide'
) {
  // ...
  
if (
    firstValue &&
    operator &&
    previousKeyType !== 'operator' &&
    previousKeyType !== 'calculate'
  ) {
    const calcValue = calculate(firstValue, operator, secondValue)
    display.textContent = calcValue
    calculator.dataset.firstValue = calcValue
  } else {
    calculator.dataset.firstValue = displayedNum
  }
  
// ...
}

Il tasto cancella ha due usi:

  1. Cancella tutto (indicato da AC) cancella tutto e riporta la calcolatrice allo stato iniziale.
  2. Cancella numero (indicata da CE) cancella il numero corrente. Mantiene in memoria i numeri precedenti.

Quando la calcolatrice è nel suo stato predefinito, AC dovrebbe essere visualizzato.

22fj2VLJJ1SPexybqdWIqPRkj9JkrlI3AAYl

Innanzitutto, se Tim preme un tasto (qualsiasi tasto tranne cancella), AC dovrebbe essere cambiato in CE.

Hs9tjp3JQIYOaAgh8KDnxj5QShScU0nMkDa7

Lo facciamo controllando se data-action è clear. Se non è  clear, cerchiamo il pulsante cancella e gli cambiamo il  textContent.

if (action !== 'clear') {
  const clearButton = calculator.querySelector('[data-action=clear]')
  clearButton.textContent = 'CE'
}

Secondo, se Tim preme CE, il display dovrebbe visualizzare 0. Allo stesso tempo, CE dovrebbe essere ripristinato in AC in modo che Tim possa riportare la calcolatrice al suo stato iniziale.**

Dv6SFw5LY8wB0WqTFQBe46-QoraBiq8TvpdY
if (action === 'clear') {
  display.textContent = 0
  key.textContent = 'AC'
  calculator.dataset.previousKeyType = 'clear'
}

Terzo, se Tim preme AC, ripristina la calcolatrice allo stato iniziale.

Per riportare la calcolatrice al suo stato iniziale, dobbiamo cancellare tutti gli attributi personalizzati che abbiamo impostato.

if (action === 'clear') {
  if (key.textContent === 'AC') {
    calculator.dataset.firstValue = ''
    calculator.dataset.modValue = ''
    calculator.dataset.operator = ''
    calculator.dataset.previousKeyType = ''
  } else {
    key.textContent = 'AC'
  }
  
display.textContent = 0
  calculator.dataset.previousKeyType = 'clear'
}

Questo è tutto, per la parte dei casi limite, comunque!

Puoi prendere il codice sorgente per la parte dei casi limite tramite questo link (scorri verso il basso e inserisci il tuo indirizzo e-mail nella casella e invierò i codici sorgente direttamente alla tua casella di posta).

A questo punto, il codice che abbiamo creato insieme è piuttosto confuso. Probabilmente ti perderai se provi a leggere il codice da solo. Rivediamolo per renderlo più pulito.

Refactoring del codice

Quando si esegue il refactoring, si inizia spesso con i miglioramenti più evidenti. In questo caso, iniziamo con calculate.

Prima di continuare, assicurati di conoscere queste pratiche/funzionalità di JavaScript. Li useremo nel refactoring.

  1. Ritorni anticipati
  2. Operatori ternari
  3. Funzioni pure
  4. ES6 Destructuring

Con questo, iniziamo!

Refactoring della funzione di calcolo

Ecco cosa abbiamo finora.

const calculate = (n1, operator, n2) => {
  let result = ''
  if (operator === 'add') {
    result = parseFloat(n1) + parseFloat(n2)
  } else if (operator === 'subtract') {
    result = parseFloat(n1) - parseFloat(n2)
  } else if (operator === 'multiply') {
    result = parseFloat(n1) * parseFloat(n2)
  } else if (operator === 'divide') {
    result = parseFloat(n1) / parseFloat(n2)
  }
  
  return result
}

Hai imparato che dovremmo ridurre il più possibile le riassegnazioni. Qui, possiamo rimuovere le assegnazioni se restituiamo il risultato del calcolo all'interno delle istruzioni if e else if:

const calculate = (n1, operator, n2) => {
  if (operator === 'add') {
    return parseFloat(n1) + parseFloat(n2)
  } else if (operator === 'subtract') {
    return parseFloat(n1) - parseFloat(n2)
  } else if (operator === 'multiply') {
    return parseFloat(n1) * parseFloat(n2)
  } else if (operator === 'divide') {
    return parseFloat(n1) / parseFloat(n2)
  }
}

Dal momento che restituiamo tutti i valori, possiamo utilizzare i ritorni anticipati . Se lo facciamo, non c'è bisogno di alcuna condizione else if.

const calculate = (n1, operator, n2) => {
  if (operator === 'add') {
    return parseFloat(n1) + parseFloat(n2)
  }
  
  if (operator === 'subtract') {
    return parseFloat(n1) - parseFloat(n2)
  }
  
  if (operator === 'multiply') {
    return parseFloat(n1) * parseFloat(n2)
  }
  
  if (operator === 'divide') {
    return parseFloat(n1) / parseFloat(n2)
  }
}

E poiché abbiamo una sola dichiarazione per ogni condizione if, possiamo rimuovere le parentesi. (Nota: alcuni sviluppatori preferiscono mantenere le parentesi graffe, però). Ecco come sarebbe il codice:

const calculate = (n1, operator, n2) => {
  if (operator === 'add') return parseFloat(n1) + parseFloat(n2)
  if (operator === 'subtract') return parseFloat(n1) - parseFloat(n2)
  if (operator === 'multiply') return parseFloat(n1) * parseFloat(n2)
  if (operator === 'divide') return parseFloat(n1) / parseFloat(n2)
}

Infine, abbiamo chiamato parseFloat otto volte nella funzione. Possiamo semplificarla creando due variabili per contenere valori float:

const calculate = (n1, operator, n2) => {
  const firstNum = parseFloat(n1)
  const secondNum = parseFloat(n2)
  if (operator === 'add') return firstNum + secondNum
  if (operator === 'subtract') return firstNum - secondNum
  if (operator === 'multiply') return firstNum * secondNum
  if (operator === 'divide') return firstNum / secondNum
}

Abbiamo finito calculateora. Non pensi che sia più facile da leggere rispetto a prima?

Refactoring dell'event listener

Il codice che abbiamo creato per l'event listener è enorme. Ecco cosa abbiamo al momento:

keys.addEventListener('click', e => {
  if (e.target.matches('button')) {
  
    if (!action) { /* ... */ }
    
    if (action === 'add' ||
      action === 'subtract' ||
      action === 'multiply' ||
      action === 'divide') {
      /* ... */
    }
    
    if (action === 'clear') { /* ... */ }
    if (action !== 'clear') { /* ... */ }
    if (action === 'calculate') { /* ... */ }
  }
})

Come si inizia il refactoring di questo pezzo di codice? Se non conosci le migliori pratiche di programmazione, potresti essere tentato di rifattorizzare suddividendo ogni tipo di azione in una funzione più piccola:

// Don't do this!
const handleNumberKeys = (/* ... */) => {/* ... */}
const handleOperatorKeys = (/* ... */) => {/* ... */}
const handleDecimalKey = (/* ... */) => {/* ... */}
const handleClearKey = (/* ... */) => {/* ... */}
const handleCalculateKey = (/* ... */) => {/* ... */}

Non farlo. Non aiuta, perché stai semplicemente dividendo blocchi di codice. Quando lo fai, la funzione diventa più difficile da leggere.

Un modo migliore è dividere il codice in funzioni pure e impure. Se lo fai, otterrai un codice simile a questo:

keys.addEventListener('click', e => {
  // Pure function
  const resultString = createResultString(/* ... */)
  
  // Impure stuff
  display.textContent = resultString
  updateCalculatorState(/* ... */)
})

Qui createResultString è una funzione pura che restituisce ciò che deve essere visualizzato sulla calcolatrice. updateCalculatorState è una funzione impura che modifica l'aspetto visivo e gli attributi personalizzati della calcolatrice.

Creazione di createResultString

Come accennato in precedenza, createResultString dovrebbe restituire il valore che deve essere visualizzato sulla calcolatrice.
Puoi ottenere questi valori attraverso le parti del codice dove dice display.textContent = 'some value'.

display.textContent = 'some value'

Invece di display.textContent = 'some value', vogliamo restituire ogni valore in modo da poterlo utilizzare in seguito.

// replace the above with this
return 'some value'

Esaminiamolo insieme, passo passo, partendo dai tasti numerici.

Creazione della stringa del risultato per i tasti numerici

Ecco il codice che abbiamo per i tasti numerici:

if (!action) {
  if (
    displayedNum === '0' ||
    previousKeyType === 'operator' ||
    previousKeyType === 'calculate'
  ) {
    display.textContent = keyContent
  } else {
    display.textContent = displayedNum + keyContent
  }
  calculator.dataset.previousKeyType = 'number'
}

Il primo passo è copiare le parti che dicono display.textContent = 'some value' in createResultString. Quando lo fai, assicurati di cambiare display.textContent = in return.

const createResultString = () => {
  if (!action) {
    if (
      displayedNum === '0' ||
      previousKeyType === 'operator' ||
      previousKeyType === 'calculate'
    ) {
      return keyContent
    } else {
      return displayedNum + keyContent
    }
  }
}

Successivamente, possiamo convertire l'istruzione  if/else in un operatore ternario:

const createResultString = () => {
  if (action!) {
    return displayedNum === '0' ||
      previousKeyType === 'operator' ||
      previousKeyType === 'calculate'
      ? keyContent
      : displayedNum + keyContent
  }
}

Quando esegui il refactoring, ricorda di annotare un elenco di variabili di cui hai bisogno. Torneremo più tardi sull'elenco.

const createResultString = () => {
  // Variabili necessarie:
  // 1. keyContent
  // 2. displayedNum
  // 3. previousKeyType
  // 4. action
  
  if (action!) {
    return displayedNum === '0' ||
      previousKeyType === 'operator' ||
      previousKeyType === 'calculate'
      ? keyContent
      : displayedNum + keyContent
  }
}

Creazione della stringa di risultato per il tasto virgola

Ecco il codice che abbiamo per il tasto virgola:

if (action === 'decimal') {
  if (!displayedNum.includes('.')) {
    display.textContent = displayedNum + '.'
  } else if (
    previousKeyType === 'operator' ||
    previousKeyType === 'calculate'
  ) {
    display.textContent = '0.'
  }
  
  calculator.dataset.previousKeyType = 'decimal'
}

Come prima, vogliamo spostare tutto ciò che cambia display.textContent in createResultString.

const createResultString = () => {
  // ...
  
  if (action === 'decimal') {
    if (!displayedNum.includes('.')) {
      return = displayedNum + '.'
    } else if (previousKeyType === 'operator' || previousKeyType === 'calculate') {
      return = '0.'
    }
  }
}

Dal momento che vogliamo restituire tutti i valori, possiamo convertire le istruzioni else if in return anticipati.

const createResultString = () => {
  // ...
  
  if (action === 'decimal') {
    if (!displayedNum.includes('.')) return displayedNum + '.'
    if (previousKeyType === 'operator' || previousKeyType === 'calculate') return '0.'
  }
}

Un errore comune qui è dimenticare di restituire il numero attualmente visualizzato quando nessuna delle due condizioni è soddisfatta. Ne abbiamo bisogno perché sostituiremo il display.textContent con il valore restituito da createResultString. Se ce lo siamo perso, createResultString tornerà undefined, che non è ciò che desideriamo.

const createResultString = () => {
  // ...
  
  if (action === 'decimal') {
    if (!displayedNum.includes('.')) return displayedNum + '.'
    if (previousKeyType === 'operator' || previousKeyType === 'calculate') return '0.'
    return displayedNum
  }
}

Come sempre, prendi nota delle variabili richieste. A questo punto, le variabili necessarie rimangono le stesse di prima:

const createResultString = () => {
  // Variabili necessarie:
  // 1. keyContent
  // 2. displayedNum
  // 3. previousKeyType
  // 4. action
}

Creazione della stringa di risultato per i tasti operatore

Ecco il codice che abbiamo scritto per i tasti operatore.

if (
  action === 'add' ||
  action === 'subtract' ||
  action === 'multiply' ||
  action === 'divide'
) {
  const firstValue = calculator.dataset.firstValue
  const operator = calculator.dataset.operator
  const secondValue = displayedNum
  
  if (
    firstValue &&
    operator &&
    previousKeyType !== 'operator' &&
    previousKeyType !== 'calculate'
  ) {
    const calcValue = calculate(firstValue, operator, secondValue)
    display.textContent = calcValue
    calculator.dataset.firstValue = calcValue
  } else {
    calculator.dataset.firstValue = displayedNum
  }
  
  key.classList.add('is-depressed')
  calculator.dataset.previousKeyType = 'operator'
  calculator.dataset.operator = action
}

Ormai conosci il gioco: vogliamo spostare tutto ciò che cambia display.textContent in createResultString. Ecco cosa deve essere spostato:

const createResultString = () => {
  // ...
  if (
    action === 'add' ||
    action === 'subtract' ||
    action === 'multiply' ||
    action === 'divide'
  ) {
    const firstValue = calculator.dataset.firstValue
    const operator = calculator.dataset.operator
    const secondValue = displayedNum
    
    if (
      firstValue &&
      operator &&
      previousKeyType !== 'operator' &&
      previousKeyType !== 'calculate'
    ) {
      return calculate(firstValue, operator, secondValue)
    }
  }
}

Ricorda, createResultString deve restituire il valore da visualizzare sulla calcolatrice. Se la condizione if non è soddisfatta, vogliamo comunque restituire il numero visualizzato.

const createResultString = () => {
  // ...
  if (
    action === 'add' ||
    action === 'subtract' ||
    action === 'multiply' ||
    action === 'divide'
  ) {
    const firstValue = calculator.dataset.firstValue
    const operator = calculator.dataset.operator
    const secondValue = displayedNum
    
    if (
      firstValue &&
      operator &&
      previousKeyType !== 'operator' &&
      previousKeyType !== 'calculate'
    ) {
      return calculate(firstValue, operator, secondValue)
    } else {
      return displayedNum
    }
  }
}

Possiamo quindi convertire l'istruzione if/else in un operatore ternario:

const createResultString = () => {
  // ...
  if (
    action === 'add' ||
    action === 'subtract' ||
    action === 'multiply' ||
    action === 'divide'
  ) {
    const firstValue = calculator.dataset.firstValue
    const operator = calculator.dataset.operator
    const secondValue = displayedNum
    
    return firstValue &&
      operator &&
      previousKeyType !== 'operator' &&
      previousKeyType !== 'calculate'
      ? calculate(firstValue, operator, secondValue)
      : displayedNum
  }
}

Se guardi da vicino, ti renderai conto che non è necessario memorizzare una variabile secondValue. Possiamo usare displayedNum direttamente nella funzione calculate.

const createResultString = () => {
  // ...
  if (
    action === 'add' ||
    action === 'subtract' ||
    action === 'multiply' ||
    action === 'divide'
  ) {
    const firstValue = calculator.dataset.firstValue
    const operator = calculator.dataset.operator
    
    return firstValue &&
      operator &&
      previousKeyType !== 'operator' &&
      previousKeyType !== 'calculate'
      ? calculate(firstValue, operator, displayedNum)
      : displayedNum
  }
}

Infine, prendiamo nota delle variabili e delle proprietà necessarie. Questa volta, abbiamo bisogno di  calculator.dataset.firstValue e calculator.dataset.operator.

const createResultString = () => {
  // Variabili & proprietà necessarie:
  // 1. keyContent
  // 2. displayedNum
  // 3. previousKeyType
  // 4. action
  // 5. calculator.dataset.firstValue
  // 6. calculator.dataset.operator
}

Creare la stringa del risultato per il tasto cancella

Abbiamo scritto il codice seguente per gestire il tasto clear.

if (action === 'clear') {
  if (key.textContent === 'AC') {
    calculator.dataset.firstValue = ''
    calculator.dataset.modValue = ''
    calculator.dataset.operator = ''
    calculator.dataset.previousKeyType = ''
  } else {
    key.textContent = 'AC'
  }
  
  display.textContent = 0
  calculator.dataset.previousKeyType = 'clear'
}

Come sopra, vuoi spostare tutto ciò che cambia display.textContent in createResultString.

const createResultString = () => {
  // ...
  if (action === 'clear') return 0
}

Creare la stringa del risultato per il tasto uguale

Ecco il codice che abbiamo scritto per il tasto uguale:

if (action === 'calculate') {
  let firstValue = calculator.dataset.firstValue
  const operator = calculator.dataset.operator
  let secondValue = displayedNum
  
  if (firstValue) {
    if (previousKeyType === 'calculate') {
      firstValue = displayedNum
      secondValue = calculator.dataset.modValue
    }
    
    display.textContent = calculate(firstValue, operator, secondValue)
  }
  
  calculator.dataset.modValue = secondValue
  calculator.dataset.previousKeyType = 'calculate'
}

Come sopra, vogliamo copiare tutto ciò che cambia display.textContent in createResultString. Ecco cosa deve essere copiato:

if (action === 'calculate') {
  let firstValue = calculator.dataset.firstValue
  const operator = calculator.dataset.operator
  let secondValue = displayedNum
  
  if (firstValue) {
    if (previousKeyType === 'calculate') {
      firstValue = displayedNum
      secondValue = calculator.dataset.modValue
    }
    display.textContent = calculate(firstValue, operator, secondValue)
  }
}

Quando copi il codice in createResultString, assicurati di restituire i valori per ogni possibile scenario:

const createResultString = () => {
  // ...
  
  if (action === 'calculate') {
    let firstValue = calculator.dataset.firstValue
    const operator = calculator.dataset.operator
    let secondValue = displayedNum
    
    if (firstValue) {
      if (previousKeyType === 'calculate') {
        firstValue = displayedNum
        secondValue = calculator.dataset.modValue
      }
      return calculate(firstValue, operator, secondValue)
    } else {
      return displayedNum
    }
  }
}

Successivamente, vogliamo ridurre le riassegnazioni. Possiamo farlo passando i valori corretti a calculate attraverso un operatore ternario.

const createResultString = () => {
  // ...
  
  if (action === 'calculate') {
    const firstValue = calculator.dataset.firstValue
    const operator = calculator.dataset.operator
    const modValue = calculator.dataset.modValue
    
    if (firstValue) {
      return previousKeyType === 'calculate'
        ? calculate(displayedNum, operator, modValue)
        : calculate(firstValue, operator, displayedNum)
    } else {
      return displayedNum
    }
  }
}

Puoi semplificare ulteriormente il codice sopra con un altro operatore ternario se ti senti a tuo agio con esso:

const createResultString = () => {
  // ...
  
  if (action === 'calculate') {
    const firstValue = calculator.dataset.firstValue
    const operator = calculator.dataset.operator
    const modValue = calculator.dataset.modValue
    
    return firstValue
      ? previousKeyType === 'calculate'
        ? calculate(displayedNum, operator, modValue)
        : calculate(firstValue, operator, displayedNum)
      : displayedNum
  }
}

A questo punto, vogliamo prendere nuovamente nota delle proprietà e delle variabili necessarie:

const createResultString = () => {
  // Variabili e proprietà necessarie:
  // 1. keyContent
  // 2. displayedNum
  // 3. previousKeyType
  // 4. action
  // 5. calculator.dataset.firstValue
  // 6. calculator.dataset.operator
  // 7. calculator.dataset.modValue
}

Passando alle variabili necessarie

Abbiamo bisogno di sette proprietà/variabili in createResultString:

  1. keyContent
  2. displayedNum
  3. previousKeyType
  4. action
  5. firstValue
  6. modValue
  7. operator

Possiamo ottenere keyContent e action da key. Possiamo anche ottenere firstValue, modValue, operator e previousKeyType da calculator.dataset.

Ciò significa che la funzione createResultString necessita di tre variabili: key , displayedNum e calculator.dataset. Poiché calculator.dataset rappresenta lo stato della calcolatrice, utilizziamo invece una variabile chiamata state.

const createResultString = (key, displayedNum, state) => {
  const keyContent = key.textContent
  const action = key.dataset.action
  const firstValue = state.firstValue
  const modValue = state.modValue
  const operator = state.operator
  const previousKeyType = state.previousKeyType
  // ... Refactor as necessary
}

// Using createResultString
keys.addEventListener('click', e => {
  if (e.target.matches('button')) return
  const displayedNum = display.textContent
  const resultString = createResultString(e.target, displayedNum, calculator.dataset)
  
  // ...
})

Sentiti libero di destrutturare le variabili se lo desideri:

const createResultString = (key, displayedNum, state) => {
  const keyContent = key.textContent
  const { action } = key.dataset
  const {
    firstValue,
    modValue,
    operator,
    previousKeyType
  } = state
  
  // ...
}

Compattezza all'interno delle istruzioni if

In createResultString, abbiamo utilizzato le seguenti condizioni per verificare il tipo di tasti su cui è stato fatto clic:

// If key is number
if (!action) { /* ... */ }

// If key is decimal
if (action === 'decimal') { /* ... */ }

// If key is operator
if (
  action === 'add' ||
  action === 'subtract' ||
  action === 'multiply' ||
  action === 'divide'
) { /* ... */}

// If key is clear
if (action === 'clear') { /* ... */ }

// If key is calculate
if (action === 'calculate') { /* ... */ }

Non sono compatte, quindi sono difficili da leggere. Se possibile, vogliamo renderle compatte in modo da poter scrivere qualcosa del genere:

if (keyType === 'number') { /* ... */ }
if (keyType === 'decimal') { /* ... */ }
if (keyType === 'operator') { /* ... */}
if (keyType === 'clear') { /* ... */ }
if (keyType === 'calculate') { /* ... */ }

Per fare ciò, possiamo creare una funzione chiamata getKeyType. Questa funzione dovrebbe restituire il tipo di chiave su cui è stato fatto clic.

const getKeyType = (key) => {
  const { action } = key.dataset
  if (!action) return 'number'
  if (
    action === 'add' ||
    action === 'subtract' ||
    action === 'multiply' ||
    action === 'divide'
  ) return 'operator'
  // For everything else, return the action
  return action
}

Ecco come useresti la funzione:

const createResultString = (key, displayedNum, state) => {
  const keyType = getKeyType(key)
  
  if (keyType === 'number') { /* ... */ }
  if (keyType === 'decimal') { /* ... */ }
  if (keyType === 'operator') { /* ... */}
  if (keyType === 'clear') { /* ... */ }
  if (keyType === 'calculate') { /* ... */ }
}

Abbiamo finito createResultString. Passiamo a updateCalculatorState.

Creare updateCalculatorState

updateCalculatorStateè una funzione che modifica l'aspetto visivo e gli attributi personalizzati della calcolatrice.

Come con createResultString, dobbiamo controllare il tipo di tasto su cui è stato fatto clic. Qui possiamo riutilizzare getKeyType.

const updateCalculatorState = (key) => {
  const keyType = getKeyType(key)
  
  if (keyType === 'number') { /* ... */ }
  if (keyType === 'decimal') { /* ... */ }
  if (keyType === 'operator') { /* ... */}
  if (keyType === 'clear') { /* ... */ }
  if (keyType === 'calculate') { /* ... */ }
}

Se guardi il codice residuo, potresti notare che cambiamo data-previous-key-type per ogni tipo di chiave. Ecco come appare il codice:

const updateCalculatorState = (key, calculator) => {
  const keyType = getKeyType(key)
  
  if (!action) {
    // ...
    calculator.dataset.previousKeyType = 'number'
  }
  
  if (action === 'decimal') {
    // ...
    calculator.dataset.previousKeyType = 'decimal'
  }
  
  if (
    action === 'add' ||
    action === 'subtract' ||
    action === 'multiply' ||
    action === 'divide'
  ) {
    // ...
    calculator.dataset.previousKeyType = 'operator'
  }
  
  if (action === 'clear') {
    // ...
    calculator.dataset.previousKeyType = 'clear'
  }
  
  if (action === 'calculate') {
    calculator.dataset.previousKeyType = 'calculate'
  }
}

Questo è ridondante perché conosciamo già il tipo di chiave con getKeyType. Possiamo rifattorizzare quanto sopra con:

const updateCalculatorState = (key, calculator) => {
  const keyType = getKeyType(key)
  calculator.dataset.previousKeyType = keyType
    
  if (keyType === 'number') { /* ... */ }
  if (keyType === 'decimal') { /* ... */ }
  if (keyType === 'operator') { /* ... */}
  if (keyType === 'clear') { /* ... */ }
  if (keyType === 'calculate') { /* ... */ }
}

Realizzare updateCalculatorState per i taasti operatore

Visivamente, dobbiamo assicurarci che tutti i tasti rilascino il loro stato premuto. Qui possiamo copiare e incollare il codice che avevamo prima:

const updateCalculatorState = (key, calculator) => {
  const keyType = getKeyType(key)
  calculator.dataset.previousKeyType = keyType
  
  Array.from(key.parentNode.children).forEach(k => k.classList.remove('is-depressed'))
}

Ecco cosa resta da ciò che abbiamo scritto per i tasti operatore, dopo aver spostato i pezzi relativi a display.textContent in  createResultString.

if (keyType === 'operator') {
  if (firstValue &&
      operator &&
      previousKeyType !== 'operator' &&
      previousKeyType !== 'calculate'
  ) {
    calculator.dataset.firstValue = calculatedValue
  } else {
    calculator.dataset.firstValue = displayedNum
  }
  
  key.classList.add('is-depressed')
  calculator.dataset.operator = key.dataset.action
}

Potresti notare che possiamo abbreviare il codice con un operatore ternario:

if (keyType === 'operator') {
  key.classList.add('is-depressed')
  calculator.dataset.operator = key.dataset.action
  calculator.dataset.firstValue = firstValue &&
    operator &&
    previousKeyType !== 'operator' &&
    previousKeyType !== 'calculate'
    ? calculatedValue
    : displayedNum
}

Come prima, prendi nota delle variabili e delle proprietà di cui hai bisogno. Qui, abbiamo bisogno di calculatedValue e di displayedNum.

const updateCalculatorState = (key, calculator) => {
  // Variabili e prorpietà necessarie
  // 1. key
  // 2. calculator
  // 3. calculatedValue
  // 4. displayedNum
}

Realizzazione di updateCalculatorState per il tasto cancella

Ecco il codice rimanente per il tasto cancella:

if (action === 'clear') {
  if (key.textContent === 'AC') {
    calculator.dataset.firstValue = ''
    calculator.dataset.modValue = ''
    calculator.dataset.operator = ''
    calculator.dataset.previousKeyType = ''
  } else {
    key.textContent = 'AC'
  }
}

if (action !== 'clear') {
  const clearButton = calculator.querySelector('[data-action=clear]')
  clearButton.textContent = 'CE'
}

Non c'è molto su cui fare refactoring qui. Sentiti libero di copiare/incollare tutto in updateCalculatorState.

Creare updateCalculatorState per il tasto uguale

Ecco il codice che abbiamo scritto per il tasto uguale:

if (action === 'calculate') {
  let firstValue = calculator.dataset.firstValue
  const operator = calculator.dataset.operator
  let secondValue = displayedNum
  
  if (firstValue) {
    if (previousKeyType === 'calculate') {
      firstValue = displayedNum
      secondValue = calculator.dataset.modValue
    }
    
    display.textContent = calculate(firstValue, operator, secondValue)
  }
  
  calculator.dataset.modValue = secondValue
  calculator.dataset.previousKeyType = 'calculate'
}

Ecco cosa ci resta se rimuoviamo tutto ciò che riguarda display.textContent.

if (action === 'calculate') {
  let secondValue = displayedNum
  
  if (firstValue) {
    if (previousKeyType === 'calculate') {
      secondValue = calculator.dataset.modValue
    }
  }
  
  calculator.dataset.modValue = secondValue
}

Possiamo rifattorizzare questo in quanto segue:

if (keyType === 'calculate') {
  calculator.dataset.modValue = firstValue && previousKeyType === 'calculate'
    ? modValue
    : displayedNum
}

Come sempre, prendi nota delle proprietà e delle variabili utilizzate:

const updateCalculatorState = (key, calculator) => {
  // Variabili e proprietà necessarie
  // 1. key
  // 2. calculator
  // 3. calculatedValue
  // 4. displayedNum
  // 5. modValue
}

Passando alle variabili necessarie

Sappiamo che abbiamo bisogno di cinque variabili/proprietà per updateCalculatorState:

  1. key
  2. calculator
  3. calculatedValue
  4. displayedNum
  5. modValue

Poiché modValue può essere recuperata da calculator.dataset, abbiamo solo bisogno di passare quattro valori:

const updateCalculatorState = (key, calculator, calculatedValue, displayedNum) => {
  // ...
}

keys.addEventListener('click', e => {
  if (e.target.matches('button')) return
  
  const key = e.target
  const displayedNum = display.textContent
  const resultString = createResultString(key, displayedNum, calculator.dataset)
  
  display.textContent = resultString
  
  // Pass in necessary values
  updateCalculatorState(key, calculator, resultString, displayedNum)
})

Refactoring nuovamente di updateCalculatorState

Abbiamo cambiato tre tipi di valori in updateCalculatorState:

  1. calculator.dataset
  2. La classe per la pressione/rilascio dei tasti operatore
  3. Il testo AC vs CE

Se vuoi renderlo più pulito, puoi dividere (2) e (3) in un'altra funzione - updateVisualState. Ecco come può essere updateVisualState:

const updateVisualState = (key, calculator) => {
  const keyType = getKeyType(key)
  Array.from(key.parentNode.children).forEach(k => k.classList.remove('is-depressed'))
  
  if (keyType === 'operator') key.classList.add('is-depressed')
  
  if (keyType === 'clear' && key.textContent !== 'AC') {
    key.textContent = 'AC'
  }
  
  if (keyType !== 'clear') {
    const clearButton = calculator.querySelector('[data-action=clear]')
    clearButton.textContent = 'CE'
  }
}

Ricapitolando

Il codice diventa molto più pulito dopo il refactor. Se guardi nell'event listener, saprai cosa fa ciascuna funzione. Ecco come appare l'event listener alla fine:

keys.addEventListener('click', e => {
  if (e.target.matches('button')) return
  const key = e.target
  const displayedNum = display.textContent
  
  // Pure functions
  const resultString = createResultString(key, displayedNum, calculator.dataset)
  
  // Update states
  display.textContent = resultString
  updateCalculatorState(key, calculator, resultString, displayedNum)
  updateVisualState(key, calculator)
})

Puoi prendere il codice sorgente per la parte di refactoring tramite questo link (scorri verso il basso e inserisci il tuo indirizzo email nella casella e invierò i codici sorgente direttamente alla tua casella di posta).

Spero che questo articolo ti sia piaciuto. Se è così, potrebbe interessarti Learn JavaScript , un corso in cui ti mostro come costruire 20 componenti, passo dopo passo, come abbiamo costruito oggi questa calcolatrice.

Nota: possiamo migliorare ulteriormente la calcolatrice aggiungendo il supporto della tastiera e funzionalità di accessibilità come le Live Regions. Vuoi scoprire come? Vai a dare un'occhiata a Learn JavaScript :)