Chegamos ao fim do primeiro módulo. Em nove artigos você percorreu um caminho sólido — da sintaxe básica até conceitos que muitos desenvolvedores levam meses para entender de verdade.
Este artigo tem dois objetivos: primeiro, revisar os conceitos centrais do módulo com exemplos rápidos. Segundo, construir um projeto real que une tudo em um único programa funcional. Projetos são onde o aprendizado se consolida — teoria sem prática esquece rápido.
Vamos revisar e construir.
Revisão rápida — Módulo 1
Variáveis e tipos
const nome = "Ricardo"; // string imutável
let pontos = 0; // número mutável
const ativo = true; // boolean
const endereco = null; // ausência intencional
let profissao; // undefined
// Template literal
console.log(`${nome} tem ${pontos} pontos.`);
Condicionais
const nota = 7.5;
if (nota >= 9) {
console.log("Excelente");
} else if (nota >= 6) {
console.log("Aprovado");
} else {
console.log("Reprovado");
}
// Ternário
const status = nota >= 6 ? "Aprovado" : "Reprovado";
Laços
// for clássico
for (let i = 0; i < 5; i++) {
console.log(i);
}
// for...of
const frutas = ["maçã", "banana", "laranja"];
for (const fruta of frutas) {
console.log(fruta);
}
// while
let tentativas = 3;
while (tentativas > 0) {
console.log(`Tentativas restantes: ${tentativas}`);
tentativas--;
}
Funções
// Declaração
function somar(a, b) { return a + b; }
// Expressão
const subtrair = function(a, b) { return a - b; };
// Arrow
const multiplicar = (a, b) => a * b;
// Parâmetros padrão
const saudar = (nome = "visitante") => `Olá, ${nome}!`;
Arrays
const numeros = [1, 2, 3, 4, 5];
const dobrados = numeros.map(n => n * 2);
const pares = numeros.filter(n => n % 2 === 0);
const soma = numeros.reduce((acc, n) => acc + n, 0);
Objetos
const usuario = {
nome: "Ana",
idade: 28,
saudar() {
return `Olá, sou ${this.nome}!`;
},
};
const { nome, idade } = usuario; // desestruturação
const copia = { ...usuario }; // spread
Escopo e Closures
function criarContador() {
let count = 0;
return {
incrementar: () => ++count,
valor: () => count,
};
}
const contador = criarContador();
contador.incrementar();
contador.incrementar();
console.log(contador.valor()); // 2
Tratamento de erros
function dividir(a, b) {
if (b === 0) throw new Error("Divisão por zero.");
return a / b;
}
try {
console.log(dividir(10, 0));
} catch (erro) {
console.log(`Erro: ${erro.message}`);
} finally {
console.log("Operação finalizada.");
}
Mini Projeto — Calculadora no Console
Agora vamos construir uma calculadora completa que aplica tudo do Módulo 1. Ela terá:
- Operações básicas: soma, subtração, multiplicação, divisão
- Operações avançadas: potência, resto, raiz quadrada
- Histórico de operações
- Validação de entrada com tratamento de erros
- Memória (guardar e recuperar um valor)
- Relatório final com estatísticas
Construiremos em partes, do mais simples ao mais complexo.
Parte 1 — Operações básicas
// Erros personalizados
class ErroCalculadora extends Error {
constructor(mensagem) {
super(mensagem);
this.name = "ErroCalculadora";
}
}
// Validação de entradas
function validarNumeros(...valores) {
for (const valor of valores) {
if (typeof valor !== "number" || isNaN(valor)) {
throw new ErroCalculadora(`"${valor}" não é um número válido.`);
}
}
}
// Operações — cada uma faz apenas uma coisa
const operacoes = {
somar: (a, b) => {
validarNumeros(a, b);
return a + b;
},
subtrair: (a, b) => {
validarNumeros(a, b);
return a - b;
},
multiplicar: (a, b) => {
validarNumeros(a, b);
return a * b;
},
dividir: (a, b) => {
validarNumeros(a, b);
if (b === 0) throw new ErroCalculadora("Divisão por zero não é permitida.");
return a / b;
},
potencia: (base, expoente) => {
validarNumeros(base, expoente);
return base ** expoente;
},
resto: (a, b) => {
validarNumeros(a, b);
if (b === 0) throw new ErroCalculadora("Divisão por zero não é permitida.");
return a % b;
},
raizQuadrada: (a) => {
validarNumeros(a);
if (a < 0) throw new ErroCalculadora("Não é possível calcular raiz de número negativo.");
return Math.sqrt(a);
},
};
Parte 2 — A fábrica da calculadora (closures + objetos)
function criarCalculadora() {
// Estado privado — acessível apenas pelos métodos
const historico = [];
let memoria = null;
let totalOperacoes = 0;
let totalErros = 0;
// Registra operação no histórico
function registrar(expressao, resultado) {
totalOperacoes++;
const entrada = {
numero: totalOperacoes,
expressao,
resultado,
horario: new Date().toLocaleTimeString("pt-BR"),
};
historico.push(entrada);
console.log(`[${entrada.numero}] ${expressao} = ${resultado}`);
return resultado;
}
// Executa uma operação com tratamento de erros
function executar(operacao, expressao, ...args) {
try {
const resultado = operacao(...args);
return registrar(expressao, resultado);
} catch (erro) {
totalErros++;
console.log(`❌ Erro: ${erro.message}`);
return null;
}
}
// Interface pública da calculadora
return {
somar(a, b) {
return executar(operacoes.somar, `${a} + ${b}`, a, b);
},
subtrair(a, b) {
return executar(operacoes.subtrair, `${a} - ${b}`, a, b);
},
multiplicar(a, b) {
return executar(operacoes.multiplicar, `${a} × ${b}`, a, b);
},
dividir(a, b) {
return executar(operacoes.dividir, `${a} ÷ ${b}`, a, b);
},
potencia(base, exp) {
return executar(operacoes.potencia, `${base} ^ ${exp}`, base, exp);
},
resto(a, b) {
return executar(operacoes.resto, `${a} % ${b}`, a, b);
},
raizQuadrada(a) {
return executar(operacoes.raizQuadrada, `√${a}`, a);
},
// Memória
salvarMemoria(valor) {
if (typeof valor !== "number" || isNaN(valor)) {
console.log("❌ Valor inválido para memória.");
return;
}
memoria = valor;
console.log(`💾 Memória salva: ${memoria}`);
},
recuperarMemoria() {
if (memoria === null) {
console.log("⚠️ Memória vazia.");
return null;
}
console.log(`💾 Memória: ${memoria}`);
return memoria;
},
limparMemoria() {
memoria = null;
console.log("💾 Memória limpa.");
},
// Histórico
verHistorico() {
if (historico.length === 0) {
console.log("Nenhuma operação realizada ainda.");
return;
}
console.log("\n📋 HISTÓRICO DE OPERAÇÕES:");
console.log("─".repeat(40));
historico.forEach(({ numero, expressao, resultado, horario }) => {
console.log(` [${numero}] ${horario} | ${expressao} = ${resultado}`);
});
console.log("─".repeat(40));
},
// Relatório final com estatísticas
relatorio() {
const resultados = historico.map(h => h.resultado).filter(r => r !== null);
const maior = resultados.length > 0 ? Math.max(...resultados) : null;
const menor = resultados.length > 0 ? Math.min(...resultados) : null;
const media = resultados.length > 0
? (resultados.reduce((acc, r) => acc + r, 0) / resultados.length).toFixed(2)
: null;
console.log("\n📊 RELATÓRIO DA SESSÃO:");
console.log("═".repeat(40));
console.log(` Total de operações : ${totalOperacoes + totalErros}`);
console.log(` Operações com erro : ${totalErros}`);
console.log(` Operações válidas : ${totalOperacoes}`);
console.log(` Maior resultado : ${maior}`);
console.log(` Menor resultado : ${menor}`);
console.log(` Média dos resultados: ${media}`);
console.log(` Memória atual : ${memoria ?? "vazia"}`);
console.log("═".repeat(40));
},
};
}
Parte 3 — Usando a calculadora
const calc = criarCalculadora();
console.log("🔢 CALCULADORA JAVASCRIPT\n");
// Operações básicas
calc.somar(10, 5); // [1] 10 + 5 = 15
calc.subtrair(20, 8); // [2] 20 - 8 = 12
calc.multiplicar(6, 7); // [3] 6 × 7 = 42
calc.dividir(100, 4); // [4] 100 ÷ 4 = 25
// Operações avançadas
calc.potencia(2, 10); // [5] 2 ^ 10 = 1024
calc.resto(17, 5); // [6] 17 % 5 = 2
calc.raizQuadrada(144); // [7] √144 = 12
// Testando erros
calc.dividir(10, 0); // ❌ Divisão por zero
calc.raizQuadrada(-9); // ❌ Raiz de negativo
calc.somar("abc", 5); // ❌ Valor inválido
// Memória
calc.salvarMemoria(calc.somar(3, 7)); // salva 10
calc.recuperarMemoria(); // 💾 Memória: 10
calc.multiplicar(calc.recuperarMemoria(), 5); // 10 × 5 = 50
calc.limparMemoria();
// Histórico e relatório
calc.verHistorico();
calc.relatorio();
Parte 4 — Saída esperada no console
🔢 CALCULADORA JAVASCRIPT
[1] 10 + 5 = 15
[2] 20 - 8 = 12
[3] 6 × 7 = 42
[4] 100 ÷ 4 = 25
[5] 2 ^ 10 = 1024
[6] 17 % 5 = 2
[7] √144 = 12
❌ Erro: Divisão por zero não é permitida.
❌ Erro: Não é possível calcular raiz de número negativo.
❌ Erro: "abc" não é um número válido.
[8] 3 + 7 = 10
💾 Memória salva: 10
💾 Memória: 10
[9] 10 × 5 = 50
💾 Memória limpa.
📋 HISTÓRICO DE OPERAÇÕES:
────────────────────────────────────────
[1] 10:32:15 | 10 + 5 = 15
[2] 10:32:15 | 20 - 8 = 12
[3] 10:32:15 | 6 × 7 = 42
[4] 10:32:15 | 100 ÷ 4 = 25
[5] 10:32:15 | 2 ^ 10 = 1024
[6] 10:32:15 | 17 % 5 = 2
[7] 10:32:15 | √144 = 12
[8] 10:32:15 | 3 + 7 = 10
[9] 10:32:15 | 10 × 5 = 50
────────────────────────────────────────
📊 RELATÓRIO DA SESSÃO:
════════════════════════════════════════
Total de operações : 12
Operações com erro : 3
Operações válidas : 9
Maior resultado : 1024
Menor resultado : 2
Média dos resultados: 132.44
Memória atual : vazia
════════════════════════════════════════
O que este projeto exercitou
| Conceito | Onde apareceu |
|---|---|
const e let |
Estado interno da calculadora |
| Condicionais | Validações e tratamento de casos especiais |
| Funções e arrow functions | Todas as operações e helpers |
| Objetos e métodos | Interface pública e objeto operacoes |
| Arrays e métodos | Histórico, map, filter, reduce |
| Desestruturação | forEach no histórico |
| Spread operator | Math.max(...resultados) |
| Escopo | Estado privado da calculadora |
| Closures | historico, memoria, totalOperacoes privados |
| Classes de erro | ErroCalculadora |
| try/catch/finally | executar() |
| Template literals | Todas as mensagens formatadas |
Desafio final do Módulo 1
Expanda a calculadora com pelo menos duas das funcionalidades abaixo:
- Método
desfazer()que remove a última operação do histórico - Método
filtrarHistorico(operador)que retorna apenas operações de um tipo (ex: só somas) - Método
exportarHistorico()que retorna o histórico como string formatada - Suporte a cálculos encadeados:
calc.somar(5, 3).multiplicar(2)retornando16 - Método
limpar()que zera todo o estado da calculadora
Ver solução — as cinco funcionalidades, incluindo o encadeamento
// O enunciado pede pelo menos duas. Aqui estão as cinco — e a quarta
// (encadeamento) é a que muda o desenho da calculadora, então vem por
// último, depois que as outras estiverem no lugar.
// As operações da Parte 1, resumidas para o bloco rodar sozinho.
const operacoes = {
somar: (a, b) => a + b,
subtrair: (a, b) => a - b,
multiplicar: (a, b) => a * b,
dividir: (a, b) => {
if (b === 0) throw new Error("divisão por zero");
return a / b;
},
};
function criarCalculadora() {
const historico = [];
let totalOperacoes = 0;
let totalErros = 0;
function registrar(expressao, resultado, operador) {
totalOperacoes++;
// `operador` é novo: sem ele, filtrarHistorico teria de adivinhar o
// tipo da operação lendo a string da expressão com regex.
historico.push({
numero: totalOperacoes,
expressao,
resultado,
operador,
horario: new Date().toLocaleTimeString("pt-BR"),
});
return resultado;
}
function executar(operacao, expressao, operador, ...args) {
try {
return registrar(expressao, operacao(...args), operador);
} catch (erro) {
totalErros++;
console.log(`❌ Erro: ${erro.message}`);
return NaN; // NaN propaga pela cadeia; null quebraria a conta seguinte
}
}
const calc = {
// ------------------------------------------------------------
// 1 — desfazer()
// ------------------------------------------------------------
desfazer() {
if (historico.length === 0) {
console.log("⚠️ Nada para desfazer.");
return null;
}
const removida = historico.pop();
// O contador volta junto: sem isso o próximo item viraria [4]
// logo depois de [2] ter sido desfeito, e o histórico mentiria.
totalOperacoes--;
console.log(`↩️ Desfeito: ${removida.expressao} = ${removida.resultado}`);
return removida;
},
// ------------------------------------------------------------
// 2 — filtrarHistorico(operador)
// ------------------------------------------------------------
filtrarHistorico(operador) {
// Devolve cópia: se devolvesse o array interno, quem chamou
// poderia dar push nele e furar o encapsulamento da closure.
return historico.filter((item) => item.operador === operador);
},
// ------------------------------------------------------------
// 3 — exportarHistorico()
// ------------------------------------------------------------
exportarHistorico() {
if (historico.length === 0) return "Histórico vazio.";
const linhas = historico.map(
(item) =>
`[${String(item.numero).padStart(2, "0")}] ${item.horario} ` +
`${item.expressao} = ${item.resultado}`
);
return [
"═".repeat(46),
" HISTÓRICO DA CALCULADORA",
"═".repeat(46),
...linhas,
"─".repeat(46),
` ${historico.length} operação(ões) · ${totalErros} erro(s)`,
].join("\n");
},
// ------------------------------------------------------------
// 5 — limpar()
// ------------------------------------------------------------
limpar() {
// historico.length = 0 esvazia o MESMO array. Trocar por
// `historico = []` seria erro: a variável é const, e mesmo que
// não fosse, quem já tivesse a referência antiga veria o array
// velho.
historico.length = 0;
totalOperacoes = 0;
totalErros = 0;
console.log("🧹 Calculadora zerada.");
return calc;
},
verHistorico() {
return [...historico];
},
};
// ------------------------------------------------------------
// 4 — cálculos encadeados
// ------------------------------------------------------------
// O truque: os métodos param de devolver número e passam a devolver
// um objeto que CARREGA o número e sabe continuar a conta. Como ele
// implementa valueOf, continua funcionando em contexto numérico.
const NOMES = ["somar", "subtrair", "multiplicar", "dividir"];
const SIMBOLO = { somar: "+", subtrair: "-", multiplicar: "×", dividir: "÷" };
function encadeavel(valor) {
const alvo = {
valor: () => valor,
valueOf: () => valor,
toString: () => String(valor),
};
for (const nome of NOMES) {
// Na cadeia, o resultado anterior é o primeiro operando.
alvo[nome] = (b) => calc[nome](valor, b);
}
return alvo;
}
for (const nome of NOMES) {
calc[nome] = (a, b) =>
encadeavel(
executar(operacoes[nome], `${a} ${SIMBOLO[nome]} ${b}`, nome, a, b)
);
}
return calc;
}
// ---------------------------------------------------------------
// Usando
// ---------------------------------------------------------------
const calc = criarCalculadora();
console.log(calc.somar(5, 3).multiplicar(2).valor()); // 16
console.log(calc.somar(10, 5).subtrair(3).dividir(2).valor()); // 6
calc.dividir(1, 0); // ❌ Erro: divisão por zero
console.log(calc.somar(2, 2).valor()); // 4 — a calculadora sobrevive ao erro
console.log(calc.filtrarHistorico("somar").map((h) => h.expressao));
// [ '5 + 3', '10 + 5', '2 + 2' ]
calc.desfazer(); // ↩️ Desfeito: 2 + 2 = 4
console.log(calc.exportarHistorico());
calc.limpar();
console.log(calc.exportarHistorico()); // Histórico vazio.
// ---------------------------------------------------------------
// O detalhe que morde: console.log não chama valueOf
// ---------------------------------------------------------------
const cadeia = criarCalculadora().somar(5, 3);
console.log(cadeia); // { valor: [Function], valueOf: ..., ... }
console.log(cadeia + 0); // 8 — contexto numérico converte
console.log(`${cadeia}`); // 8 — template literal chama toString
console.log(Number(cadeia)); // 8
console.log(cadeia.valor()); // 8 — explícito, e é o que se deve usar
Encadear é devolver um objeto onde antes havia um número — e objeto impresso é objeto, não valor. valueOf resolve conta e comparação, toString resolve template literal, mas console.log não chama nenhum dos dois. Por isso a cadeia termina em .valor(): explícito é melhor que mágico.
A calculadora é pequena de propósito: cabe num arquivo e, ainda assim, obriga a decidir o que fazer quando o usuário digita algo que não é número, onde termina o cálculo e começa a exibição, e como registrar o que já foi feito. É o primeiro código da série em que o difícil deixa de ser a sintaxe e passa a ser escolher onde cada responsabilidade mora — e é essa escolha, não o conhecimento de map ou de try, que separa um script que funciona de um que se consegue manter.
Fontes e Referências
- MDN Web Docs — JavaScript Guide: https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Guide
- javascript.info — operadores: https://javascript.info/operators
- MDN — objeto
Math: https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Global_Objects/Math - Node.js —
readline, para ler do console: https://nodejs.org/api/readline.html - Clean Code — Robert C. Martin (Alta Books)
- Airbnb JavaScript Style Guide: https://github.com/airbnb/javascript
- freeCodeCamp — JavaScript Algorithms and Data Structures: https://www.freecodecamp.org/learn/javascript-algorithms-and-data-structures/
Exercícios
Exercício 1
O que cada uma das três linhas marcadas imprime — e por quê a segunda calculadora não sabe nada da primeira?
const calc = criarCalculadora();
calc.somar(10, 5);
console.log(calc.historico); // A
console.log(calc.verHistorico()); // B
const outra = criarCalculadora();
outra.relatorio(); // C
Ver resposta
✓ Resposta: A imprime undefined. O array historico vive dentro de criarCalculadora e o objeto devolvido nunca o expõe — não há erro de acesso, simplesmente não existe propriedade com esse nome. B imprime a tabela do histórico e, logo abaixo, undefined: verHistorico() escreve no console mas não tem return, então o console.log de fora recebe o vazio. C mostra um relatório zerado. Cada chamada a criarCalculadora() cria uma closure nova, com seu próprio historico, sua própria memoria e seus próprios contadores — não há estado global sendo compartilhado. É essa combinação, variáveis presas na closure e um objeto que só devolve métodos, que dá encapsulamento sem class e sem convenção de nome com underscore.
Exercício 2
Depois deste trecho, quais são os três primeiros números do relatório? E por que Operações válidas não pode ser calculado como totalOperacoes - totalErros?
const calc = criarCalculadora();
calc.somar(10, 5);
calc.dividir(10, 0); // erro
calc.raizQuadrada(-9); // erro
calc.multiplicar(6, 7);
calc.relatorio();
Ver resposta
✓ Resposta: Total de operações 4, operações com erro 2, operações válidas 2. A chave está em quem incrementa o quê: totalOperacoes++ mora dentro de registrar(), que só é chamado quando a operação deu certo — nas duas falhas o fluxo desviou para o catch e só totalErros subiu. Logo totalOperacoes vale 2, e a subtração daria 0: estaria descontando erros de uma contagem que nunca os incluiu. O total precisa ser totalOperacoes + totalErros, e as válidas são o próprio totalOperacoes. E o contador não pode simplesmente passar a subir também nas falhas, porque é ele que numera as linhas do histórico — furaria a sequência, saltando de [2] para [5] sem que existissem [3] e [4].
Exercício 3
Rode as duas primeiras linhas mentalmente. O que sai? E o que o reduce da terceira linha faria sem o , 0 no fim?
const resultados = []; // nenhuma operação ainda
console.log(Math.max(...resultados));
console.log(Math.min(...resultados));
console.log(resultados.reduce((acc, r) => acc + r, 0));
Ver resposta
✓ Resposta: Sai -Infinity e depois Infinity. Com o spread de um array vazio, Math.max() é chamado sem argumento nenhum e devolve o elemento neutro do máximo, que é -Infinity; Math.min(), pela mesma lógica, devolve Infinity. É exatamente por isso que relatorio() testa resultados.length > 0 antes: sem a guarda, uma calculadora recém-criada anunciaria "Maior resultado: -Infinity / Menor resultado: Infinity" — invertido e absurdo. Já o reduce imprime 0 e não precisa de guarda nenhuma, porque o , 0 é o valor inicial. Retire-o e o reduce sobre array vazio lança TypeError: Reduce of empty array with no initial value.
Exercício 4
A memória guarda o valor zero. As duas versões da linha do relatório imprimem a mesma coisa?
calc.salvarMemoria(0);
// dentro de relatorio(), duas escritas possíveis:
console.log(` Memória atual: ${memoria ?? "vazia"}`);
console.log(` Memória atual: ${memoria || "vazia"}`);
Ver resposta
✓ Resposta: Não. A primeira imprime 0; a segunda imprime vazia — e mente. O ?? só recorre ao valor da direita quando o da esquerda é null ou undefined, isto é, quando de fato não há valor. O || recorre a ele diante de qualquer coisa falsy, e 0 é falsy — assim como "", NaN e false. Zero na memória é um valor legítimo, salvo de propósito: trocar o ?? pelo || transformaria "guardei zero" em "não guardei nada". Os dois só coincidem quando memoria é realmente null — que é o estado inicial e o estado depois de limparMemoria(), e é o caso que costuma passar em qualquer teste rápido, escondendo o bug.
Exercício 5
Quantas mensagens de erro aparecem no console, e o que a segunda reclama? Repare que só houve uma divisão por zero.
const calc = criarCalculadora();
const resultado = calc.dividir(10, 0);
calc.multiplicar(resultado, 5);
Ver resposta
✓ Resposta: Aparecem duas. A primeira é ❌ Erro: Divisão por zero não é permitida. — o executar() capturou, contou o erro e devolveu null. Esse null segue vivo em resultado e entra na operação seguinte: validarNumeros(null, 5) avalia typeof null, que é "object" e não "number", e lança a segunda mensagem, ❌ Erro: "null" não é um número válido. O programa não quebra em momento algum — a falha apenas se degrada em outra falha, um pouco mais confusa que a original. É essa a razão de a solução do desafio final devolver NaN em vez de null: NaN é do tipo number, atravessa a cadeia sem disparar um segundo erro de validação, ainda que contamine o resultado. Nenhuma das duas escolhas é obviamente melhor; o que não pode é ser escolha acidental, porque o valor devolvido por uma operação que falhou continua circulando pelo programa.