Projeto: calculadora em JavaScript
LidoNesta aula, construímos uma calculadora completa em JavaScript, abordando desde a lógica de operações até o tratamento de eventos e validação de entradas. Exploramos boas práticas, edge cases e exercícios práticos para consolidar o aprendizado.
Nesta aula, vamos construir uma calculadora funcional em JavaScript, explorando conceitos essenciais como lógica de programação, manipulação de eventos, tratamento de entradas e casos extremos (edge cases). Este projeto prático consolida habilidades fundamentais para qualquer desenvolvedor front-end, unindo HTML, CSS e JavaScript de forma integrada.
Você aprenderá a estruturar o código de forma clara, lidar com interações do usuário, validar dados e garantir que a calculadora se comporte corretamente mesmo em situações inesperadas. Ao final, terá um projeto completo que pode ser estendido e personalizado.
Lógica
A lógica de uma calculadora envolve armazenar o primeiro operando, o operador e o segundo operando, e então realizar a operação quando o usuário pressiona igual. Vamos implementar uma máquina de estados simples, onde a calculadora pode estar em diferentes modos: aguardando o primeiro número, aguardando o operador, aguardando o segundo número, ou exibindo o resultado.
Uma abordagem comum é manter variáveis de estado: currentValue (o valor exibido), previousValue (o primeiro operando), operator (o operador selecionado) e shouldResetDisplay (um flag que indica se o display deve ser limpo ao digitar o próximo número). A cada tecla pressionada, atualizamos essas variáveis de acordo com a transição de estado.
Por exemplo, quando o usuário digita um número, se shouldResetDisplay for verdadeiro, limpamos o display e o definimos como falso. Quando o usuário pressiona um operador, armazenamos o valor atual como previousValue, definimos o operador e ativamos o reset do display. Ao pressionar igual, calculamos o resultado usando uma função que executa a operação adequada.
Vamos criar uma função calculate que recebe dois números e um operador, e retorna o resultado, tratando divisão por zero e outras operações inválidas.
function calculate(num1, num2, operator) {
const a = parseFloat(num1);
const b = parseFloat(num2);
switch (operator) {
case '+': return a + b;
case '-': return a - b;
case '*': return a * b;
case '/':
if (b === 0) return 'Erro';
return a / b;
default: return null;
}
}Essa função será usada quando o usuário pressionar igual ou quando um operador for pressionado em sequência (para encadear operações). No projeto, também tratamos o caso de múltiplas operações seguidas, como 2 + 3 + 4, que deve ser resolvido da esquerda para a direita.
Eventos
Os eventos são a ponte entre a interface e a lógica. No JavaScript, usamos o método addEventListener para escutar cliques nos botões da calculadora. Podemos selecionar todos os botões numéricos e de operadores e atribuir a eles a função adequada.
Vamos estruturar nosso HTML com botões que possuem classes ou data attributes para identificação. Por exemplo, cada botão de número tem a classe num, cada operador tem a classe op, e botões especiais como igual, limpar e ponto decimal têm ids ou classes próprias.
<button class="btn num" data-num="7">7</button>
<button class="btn op" data-op="+">+</button>
<button class="btn" id="equals">=</button>
<button class="btn" id="clear">C</button>No JavaScript, selecionamos todos os botões com document.querySelectorAll('.num') e iteramos sobre eles, adicionando um listener de clique. Dentro do listener, acessamos o valor do botão via dataset.num e chamamos a função inputNumber.
document.querySelectorAll('.num').forEach(button => {
button.addEventListener('click', () => {
inputNumber(button.dataset.num);
});
});Da mesma forma, para os operadores, chamamos inputOperator com o operador correspondente. Para o igual, chamamos calculateResult, e para limpar, clearCalculator.
Uma boa prática é usar a delegação de eventos: em vez de adicionar um listener para cada botão, adicionamos um único listener ao contêiner da calculadora e verificamos o alvo do evento. Isso é mais eficiente e facilita a manutenção, especialmente se novos botões forem adicionados dinamicamente.
const calculator = document.getElementById('calculator');
calculator.addEventListener('click', (e) => {
const target = e.target;
if (target.classList.contains('num')) {
inputNumber(target.dataset.num);
} else if (target.classList.contains('op')) {
inputOperator(target.dataset.op);
} else if (target.id === 'equals') {
calculateResult();
} else if (target.id === 'clear') {
clearCalculator();
}
});Além disso, podemos adicionar eventos de teclado para que a calculadora funcione com o teclado físico, melhorando a acessibilidade. Escutamos o evento keydown no documento e mapeamos teclas para ações.
document.addEventListener('keydown', (e) => {
if (e.key >= '0' && e.key <= '9') inputNumber(e.key);
if (['+', '-', '*', '/'].includes(e.key)) inputOperator(e.key);
if (e.key === 'Enter' || e.key === '=') calculateResult();
if (e.key === 'Escape' || e.key.toLowerCase() === 'c') clearCalculator();
if (e.key === '.') inputDecimal();
if (e.key === 'Backspace') deleteLast();
});Tratamento de entrada
O tratamento de entrada é crucial para uma boa experiência do usuário. Devemos controlar o que é exibido no display e evitar entradas inválidas, como múltiplos pontos decimais ou números começando com zero desnecessário.
Primeiro, vamos gerenciar o display: ele deve mostrar o valor atual enquanto o usuário digita. Quando o usuário pressiona um operador, o display pode mostrar o primeiro operando e o operador, mas é comum manter apenas o valor atual até que o segundo número seja digitado. Em nossa implementação, usamos a variável currentValue para armazenar a string exibida no display.
Na função inputNumber, verificamos se o display deve ser resetado (após um operador ou igual). Se sim, substituímos o conteúdo por apenas o dígito pressionado; caso contrário, concatenamos o dígito ao valor atual. Também limitamos o comprimento do número para evitar overflow de tela.
function inputNumber(digit) {
if (shouldResetDisplay) {
display.textContent = digit;
shouldResetDisplay = false;
} else {
// Evita vários zeros à esquerda
if (display.textContent === '0' && digit !== '.') {
display.textContent = digit;
} else {
if (display.textContent.length < 12) {
display.textContent += digit;
}
}
}
}Para o ponto decimal, verificamos se já existe um ponto no valor atual. Se não houver, adicionamos; se houver, ignoramos. Também tratamos o caso de o display estar vazio ou em estado de reset.
function inputDecimal() {
if (shouldResetDisplay) {
display.textContent = '0.';
shouldResetDisplay = false;
} else if (!display.textContent.includes('.')) {
display.textContent += '.';
}
}Além disso, implementamos a função de limpar (clearCalculator) que reseta todas as variáveis de estado e exibe '0'. Também podemos adicionar uma função de apagar o último dígito (deleteLast), que remove o último caractere do display.
function deleteLast() {
if (shouldResetDisplay) return;
let current = display.textContent;
if (current.length > 1) {
display.textContent = current.slice(0, -1);
} else {
display.textContent = '0';
}
}Outro aspecto é o armazenamento do operador e do primeiro operando. Quando o usuário pressiona um operador, devemos guardar o valor atual como previousValue e definir o operador. Se já houver um operador pendente e o usuário pressionar outro, calculamos o resultado parcial e atualizamos o display.
function inputOperator(op) {
if (operator && !shouldResetDisplay) {
// Calcula o resultado parcial se já houver um operador
const result = calculate(previousValue, display.textContent, operator);
display.textContent = result;
previousValue = result;
} else {
previousValue = display.textContent;
}
operator = op;
shouldResetDisplay = true;
}Edge cases
Edge cases são situações inesperadas que podem quebrar a calculadora se não forem tratadas. Vamos listar os principais e como lidamos com eles:
- Divisão por zero: Retornamos a string 'Erro' e exibimos no display. O usuário deve limpar para continuar.
- Múltiplos pontos decimais: Impedimos que o usuário digite dois pontos no mesmo número.
- Números com muitos dígitos: Limitamos o display a 12 caracteres para evitar que o layout quebre.
- Pressionar operador logo após igual: Se o usuário pressionar igual e depois um operador, o resultado deve ser usado como primeiro operando para a próxima operação.
- Pressionar operador múltiplas vezes seguidas: Deve atualizar o operador sem alterar o primeiro operando.
- Pressionar igual sem operador: Simplesmente não faz nada ou repete o último resultado.
- Pressionar ponto decimal no início: Deve exibir '0.' para facilitar a entrada.
- Pressionar zero repetidamente: Evita múltiplos zeros à esquerda (ex.: '0007').
- Entrada de teclado com caracteres não numéricos: Ignoramos teclas que não correspondem a ações.
Vamos implementar essas verificações no código. Por exemplo, na função calculateResult, se não houver operador, simplesmente retornamos sem fazer nada. Se o resultado for 'Erro', mantemos o display como está.
function calculateResult() {
if (operator === null || shouldResetDisplay) return;
const result = calculate(previousValue, display.textContent, operator);
display.textContent = result;
previousValue = null;
operator = null;
shouldResetDisplay = true;
}Também tratamos a divisão por zero na função calculate, retornando 'Erro'. No display, esse valor é mostrado, e qualquer nova operação deve ser limpa antes de continuar. Podemos também desabilitar botões de operação quando o display mostra 'Erro', mas é mais simples apenas permitir que o usuário pressione C.
Outro edge case é quando o usuário pressiona um operador após um resultado de erro. Nesse caso, devemos resetar o estado para evitar comportamentos estranhos. Vamos verificar se o display contém 'Erro' e, se sim, tratar como início de nova operação.
function inputOperator(op) {
if (display.textContent === 'Erro') {
clearCalculator();
}
// ... resto da lógica
}Boas práticas e observações finais
Ao construir uma calculadora, é importante manter o código organizado e legível. Use funções claras para cada ação, evite duplicação de código e comente trechos complexos. Além disso, lembre-se de testar todos os edge cases manualmente para garantir a robustez.
Uma boa prática é separar a lógica de negócio da manipulação do DOM. Assim, você pode testar as funções de cálculo isoladamente com um framework de testes, como Jest. Também é recomendável usar constantes para valores fixos e evitar números mágicos.
Outra dica é usar o operador de igualdade estrita (===) para comparar valores e evitar conversões implícitas que podem causar bugs. Por fim, considere adicionar animações sutis nos botões para melhorar a experiência do usuário, mas lembre-se de que a funcionalidade vem primeiro.
Referências
- MDN: Referência de Eventos
- MDN: Guia de Eventos em JavaScript
- MDN: parseFloat
- MDN: switch
- MDN: addEventListener
- MDN: Operadores aritméticos
- W3Schools: Eventos em JavaScript
- javascript.info: Eventos
Exercícios
- Implemente a função
calculateque recebe dois números e um operador e retorna o resultado. Teste com divisão por zero e retorne 'Erro' nesse caso. Resposta:✓ Resposta:function calculate(num1, num2, operator) { const a = parseFloat(num1); const b = parseFloat(num2); switch (operator) { case '+': return a + b; case '-': return a - b; case '*': return a * b; case '/': if (b === 0) return 'Erro'; return a / b; default: return null; } } - Escreva uma função
inputNumberque recebe um dígito e o adiciona ao display, considerando o estado de reset e evitando múltiplos zeros à esquerda. Resposta:✓ Resposta:function inputNumber(digit) { if (shouldResetDisplay) { display.textContent = digit; shouldResetDisplay = false; } else { if (display.textContent === '0' && digit !== '.') { display.textContent = digit; } else { if (display.textContent.length < 12) { display.textContent += digit; } } } } - Implemente a função
inputOperatorque gerencia o armazenamento do primeiro operando e do operador, calculando resultados parciais quando necessário. Resposta:✓ Resposta:function inputOperator(op) { if (display.textContent === 'Erro') { clearCalculator(); } if (operator && !shouldResetDisplay) { const result = calculate(previousValue, display.textContent, operator); display.textContent = result; previousValue = result; } else { previousValue = display.textContent; } operator = op; shouldResetDisplay = true; } - Adicione um evento de teclado que permita usar números, operadores, Enter para igual, Escape para limpar e vírgula para decimal. Resposta: ✓ Resposta:
document.addEventListener('keydown', (e) => { if (e.key >= '0' && e.key <= '9') inputNumber(e.key); if (['+', '-', '*', '/'].includes(e.key)) inputOperator(e.key); if (e.key === 'Enter' || e.key === '=') calculateResult(); if (e.key === 'Escape' || e.key.toLowerCase() === 'c') clearCalculator(); if (e.key === '.') inputDecimal(); if (e.key === 'Backspace') deleteLast(); }); - Explique como você lidaria com o caso de o usuário pressionar o botão de igual sem ter digitado um segundo número ou sem operador. Resposta: ✓ Resposta: Na função
calculateResult, se não houver operador (operator === null) ou se o display deve ser resetado (shouldResetDisplay), a função retorna sem fazer nada, mantendo o display inalterado. Isso evita que o usuário veja resultados inesperados.