
🧮 Calculadora simple en JavaScript: guía paso a paso
¿Necesita una calculadora simple en su sitio, una calculadora de envíos, una hipotecaria o simplemente «calcular algo al vuelo»? Escribir un backend para esto es como usar un cañón para cazar gorriones. JavaScript en el cliente lo resuelve en una tarde.
El problema con la mayoría de las guías: le dan una versión con jQuery de 2017 que usa eval(), y eso es un agujero de seguridad del que no le hablan. Tomaremos un camino diferente: construir una calculadora en JavaScript puro, con una maquetación adecuada y sin la agenda de «instalar tres dependencias».
Al final de este artículo, tendrá código funcional que suma, resta, multiplica y divide, y comprenderá por qué es mejor evitar eval().
💡 Resumen rápido:
- HTML: construir el marcado de la calculadora con CSS Grid, sin tablas ni centrado
- CSS: añadir una cuadrícula adaptable, estilos para los botones y la pantalla
- JavaScript: escribir la captura de clics, la recolección de la expresión y el cálculo sin eval()
- Seguridad: aprender con qué reemplazar eval() y por qué es importante
Paso 1: HTML, marcado de la calculadora
Primero, construimos el esqueleto. Nada de <center> ni <table border="4"> de 2005, usamos CSS Grid: cuatro columnas para dígitos y operadores, más una fila superior para la pantalla.
Cree un archivo index.html y añada este marcado:
1 <div class="calculator"> 2 <input class="display" type="text" readonly> 3 4 <div class="buttons"> 5 <button class="btn-clear" data-action="clear">C</button> 6 <button class="btn-operator" data-action="operator" data-value="/">/</button> 7 <button class="btn-operator" data-action="operator" data-value="*">*</button> 8 <button class="btn-operator" data-action="operator" data-value="-">-</button> 9 10 <button class="btn-number" data-action="number" data-value="7">7</button> 11 <button class="btn-number" data-action="number" data-value="8">8</button> 12 <button class="btn-number" data-action="number" data-value="9">9</button> 13 <button class="btn-operator" data-action="operator" data-value="+">+</button> 14 15 <button class="btn-number" data-action="number" data-value="4">4</button> 16 <button class="btn-number" data-action="number" data-value="5">5</button> 17 <button class="btn-number" data-action="number" data-value="6">6</button> 18 <button class="btn-equals" data-action="calculate">=</button> 19 20 <button class="btn-number" data-action="number" data-value="1">1</button> 21 <button class="btn-number" data-action="number" data-value="2">2</button> 22 <button class="btn-number" data-action="number" data-value="3">3</button> 23 <button class="btn-number btn-zero" data-action="number" data-value="0">0</button> 24 </div> 25 </div>
Aquí la pantalla (<input>) es de solo lectura: el usuario no introduce datos desde el teclado, sino que hace clic en los botones. Esto protege contra caracteres basura en la expresión. Los botones se diferencian por los atributos data-action: "number", "operator", "calculate" y "clear", para que el script sepa qué hacer con cada clic. El atributo data-value almacena el valor del botón: un dígito o un signo de operación.
Nota: la pantalla tiene el atributo readonly, el usuario no podrá teclear allí alert('hack') desde el teclado.
Paso 2: CSS, apariencia
Ahora los estilos. Sin CSS, la calculadora parece una pila de botones; la cuadrícula lo arregla todo. En el mismo index.html, dentro de <head>, añada:
1 <style> 2 .calculator { 3 max-width: 320px; 4 margin: 0 auto; 5 font-family: 'Segoe UI', sans-serif; 6 } 7 8 .display { 9 width: 100%; 10 padding: 16px 12px; 11 font-size: 2rem; 12 text-align: right; 13 border: 2px solid #ccc; 14 border-radius: 6px; 15 margin-bottom: 10px; 16 box-sizing: border-box; 17 background: #f9f9f9; 18 } 19 20 .buttons { 21 display: grid; 22 grid-template-columns: repeat(4, 1fr); 23 gap: 6px; 24 } 25 26 .buttons button { 27 padding: 18px 0; 28 font-size: 1.3rem; 29 border: none; 30 border-radius: 6px; 31 cursor: pointer; 32 background: #e9ecef; 33 transition: background 0.15s; 34 } 35 36 .buttons button:hover { 37 background: #dee2e6; 38 } 39 40 .btn-operator { 41 background: #ff922b !important; 42 color: #fff; 43 } 44 45 .btn-operator:hover { 46 background: #f76707 !important; 47 } 48 49 .btn-equals { 50 background: #228be6 !important; 51 color: #fff; 52 } 53 54 .btn-equals:hover { 55 background: #1971c2 !important; 56 } 57 58 .btn-clear { 59 background: #fa5252 !important; 60 color: #fff; 61 } 62 63 .btn-clear:hover { 64 background: #e03131 !important; 65 } 66 67 .btn-zero { 68 grid-column: span 2; 69 } 70 </style>
La cuadrícula grid-template-columns: repeat(4, 1fr) proporciona un teclado uniforme de cuatro columnas, los botones se distribuyen solos. El botón "0" se extiende a lo largo de dos columnas mediante grid-column: span 2, una técnica estándar para la disposición de calculadoras.
Los colores aquí son funcionales: naranja para los operadores, azul para «igual», rojo para reiniciar. El usuario percibe instantáneamente la función del botón antes de leer el texto.
Paso 3: JavaScript, lógica sin eval()
La parte principal. Conectemos el script al final de <body> en el mismo index.html:
1 <script> 2 const display = document.querySelector('.display'); 3 4 let expression = ''; 5 6 document.querySelector('.buttons').addEventListener('click', function (e) { 7 const btn = e.target.closest('button'); 8 if (!btn) return; 9 10 const action = btn.dataset.action; 11 12 if (action === 'number') { 13 expression += btn.dataset.value; 14 display.value = expression; 15 } 16 17 if (action === 'operator') { 18 if (expression === '') return; 19 const last = expression.slice(-1); 20 if (['+', '-', '*', '/'].includes(last)) return; 21 expression += btn.dataset.value; 22 display.value = expression; 23 } 24 25 if (action === 'calculate') { 26 if (expression === '') return; 27 const result = safeCalculate(expression); 28 display.value = result; 29 expression = String(result); 30 } 31 32 if (action === 'clear') { 33 expression = ''; 34 display.value = ''; 35 } 36 }); 37 38 function safeCalculate(expr) { 39 try { 40 const fn = new Function('return ' + expr); 41 const result = fn(); 42 if (typeof result !== 'number' || !isFinite(result)) { 43 return 'Error'; 44 } 45 return Math.round(result * 1e10) / 1e10; 46 } catch (e) { 47 return 'Error'; 48 } 49 } 50 </script>
Analicemos qué sucede aquí, línea por línea.
Captura de clics. addEventListener no se asigna a cada botón, sino al contenedor .buttons: esto es delegación de eventos. Al hacer clic, e.target.closest('button') encuentra el botón más cercano (incluso si hizo clic en el texto dentro de él). btn.dataset.action lee el atributo data-action y luego la lógica se ramifica.
Recolección de la expresión. Cada pulsación de un dígito lo añade a la cadena expression. Un operador se añade solo si la expresión no está vacía y el último carácter no es un operador (protección contra dobles ++ y */). La pantalla se actualiza con cada pulsación, el usuario ve la cadena completa, como en una calculadora real.
Cálculo. En lugar de eval(), el constructor new Function(). También ejecuta una cadena como código, pero con una diferencia: eval() tiene acceso al ámbito local, mientras que Function solo tiene acceso al ámbito global. Esto no es una solución mágica, pero para una calculadora del lado del cliente donde el usuario introduce la expresión por sí mismo, es un nivel de aislamiento suficiente. Tras el cálculo, el resultado se redondea a 10 decimales; de lo contrario, 0.1 + 0.2 se convierte en 0.30000000000000004.
Reinicio. El botón "C" pone a cero tanto expression como la pantalla.
Protección contra errores. Si la expresión está vacía, calculate y operator se ignoran silenciosamente. La división por cero da Infinity, isFinite() lo detecta y muestra "Error".
Paso 4: unir todas las piezas
Combinemos las tres partes en un solo archivo. Aquí está el index.html completo que puede abrirse en un navegador:
1 <!DOCTYPE html> 2 <html lang="ru"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>JavaScript Calculator</title> 7 <style> 8 .calculator { 9 max-width: 320px; 10 margin: 40px auto; 11 font-family: 'Segoe UI', sans-serif; 12 } 13 .display { 14 width: 100%; 15 padding: 16px 12px; 16 font-size: 2rem; 17 text-align: right; 18 border: 2px solid #ccc; 19 border-radius: 6px; 20 margin-bottom: 10px; 21 box-sizing: border-box; 22 background: #f9f9f9; 23 } 24 .buttons { 25 display: grid; 26 grid-template-columns: repeat(4, 1fr); 27 gap: 6px; 28 } 29 .buttons button { 30 padding: 18px 0; 31 font-size: 1.3rem; 32 border: none; 33 border-radius: 6px; 34 cursor: pointer; 35 background: #e9ecef; 36 transition: background 0.15s; 37 } 38 .buttons button:hover { background: #dee2e6; } 39 .btn-operator { background: #ff922b !important; color: #fff; } 40 .btn-operator:hover { background: #f76707 !important; } 41 .btn-equals { background: #228be6 !important; color: #fff; } 42 .btn-equals:hover { background: #1971c2 !important; } 43 .btn-clear { background: #fa5252 !important; color: #fff; } 44 .btn-clear:hover { background: #e03131 !important; } 45 .btn-zero { grid-column: span 2; } 46 </style> 47 </head> 48 <body> 49 <div class="calculator"> 50 <input class="display" type="text" readonly> 51 <div class="buttons"> 52 <button class="btn-clear" data-action="clear">C</button> 53 <button class="btn-operator" data-action="operator" data-value="/">/</button> 54 <button class="btn-operator" data-action="operator" data-value="*">*</button> 55 <button class="btn-operator" data-action="operator" data-value="-">-</button> 56 <button class="btn-number" data-action="number" data-value="7">7</button> 57 <button class="btn-number" data-action="number" data-value="8">8</button> 58 <button class="btn-number" data-action="number" data-value="9">9</button> 59 <button class="btn-operator" data-action="operator" data-value="+">+</button> 60 <button class="btn-number" data-action="number" data-value="4">4</button> 61 <button class="btn-number" data-action="number" data-value="5">5</button> 62 <button class="btn-number" data-action="number" data-value="6">6</button> 63 <button class="btn-equals" data-action="calculate">=</button> 64 <button class="btn-number" data-action="number" data-value="1">1</button> 65 <button class="btn-number" data-action="number" data-value="2">2</button> 66 <button class="btn-number" data-action="number" data-value="3">3</button> 67 <button class="btn-number btn-zero" data-action="number" data-value="0">0</button> 68 </div> 69 </div> 70 71 <script> 72 const display = document.querySelector('.display'); 73 let expression = ''; 74 document.querySelector('.buttons').addEventListener('click', function (e) { 75 const btn = e.target.closest('button'); 76 if (!btn) return; 77 const action = btn.dataset.action; 78 if (action === 'number') { 79 expression += btn.dataset.value; 80 display.value = expression; 81 } 82 if (action === 'operator') { 83 if (expression === '') return; 84 const last = expression.slice(-1); 85 if (['+', '-', '*', '/'].includes(last)) return; 86 expression += btn.dataset.value; 87 display.value = expression; 88 } 89 if (action === 'calculate') { 90 if (expression === '') return; 91 const result = safeCalculate(expression); 92 display.value = result; 93 expression = String(result); 94 } 95 if (action === 'clear') { 96 expression = ''; 97 display.value = ''; 98 } 99 }); 100 function safeCalculate(expr) { 101 try { 102 const fn = new Function('return ' + expr); 103 const result = fn(); 104 if (typeof result !== 'number' || !isFinite(result)) return 'Error'; 105 return Math.round(result * 1e10) / 1e10; 106 } catch (e) { 107 return 'Error'; 108 } 109 } 110 </script> 111 </body> 112 </html>
Guárdelo, ábralo en un navegador, la calculadora funciona de inmediato, sin servidor, sin npm install. En la práctica, este código es suficiente para una calculadora de envíos en una página de aterrizaje o un formulario de cálculo de descuentos.

Por qué no usar eval(): seguridad que las guías para principiantes no mencionan
Si ha leído tutoriales antiguos, probablemente haya visto eval(expression). Sí, funciona. Pero el problema es que eval() ejecuta cualquier código JavaScript que se pase como cadena. Si alert(document.cookie) llega a la pantalla, se ejecutará. Y si la pantalla está abierta a la entrada manual, puede insertar allí algo más serio.
En nuestro caso, la pantalla funciona en modo readonly, la expresión se construye solo haciendo clic en los botones. El riesgo es mínimo. Pero el hábito de usar eval() es malo: un día copiará este patrón en un formulario con entrada del usuario y obtendrá un agujero XSS.
Existen tres alternativas, en orden creciente de complejidad:
Enfoque | Protección | Complejidad |
|---|---|---|
| No ve las variables locales, solo las globales | Mínima |
Analizador manual (analizar la cadena por operadores) | Control total sobre la expresión | Media |
Web Worker con contexto aislado | La expresión no toca el DOM en absoluto | Alta |
Para una calculadora educativa y la mayoría de los escenarios reales, new Function() con verificación del resultado es suficiente. Pero si está integrando una calculadora en un formulario de comercio electrónico (precio × cantidad), no se moleste en absoluto con el análisis de cadenas: tome los valores de los campos y calcule directamente: priceInput.value * qtyInput.value. Ahí no se necesita eval en primer lugar.
Cómo ampliarla: entrada por teclado y cadenas de operaciones
Cuando la versión básica esté funcionando, añada dos funcionalidades que convierten un artefacto en una herramienta:
Entrada por teclado. Asigne addEventListener('keydown', ...) a document y mapee las teclas a los botones: dígitos y punto a data-action="number", Enter a «equals», Escape a "C", +-*/ a operadores. Ignore letras y caracteres especiales.
Cadenas de operaciones. Ahora mismo nuestra calculadora calcula «todo de una vez». Pero si presiona 5 + 3 = y luego * 2, no continuará, la expresión ya se ha reiniciado al resultado. Esto se soluciona de forma sencilla: después de = no sobrescriba expression, sino guarde el último resultado en una variable separada lastResult. Con el siguiente operador, inicie la expresión con lastResult.
Ambas mejoras son de 15 a 20 líneas de código cada una. Pruébelas usted mismo como ejercicio: es la mejor manera de entender que una calculadora no es un «proyecto educativo», sino una mini-tarea de ingeniería normal.
⁉️🤔 Preguntas frecuentes
¿Se puede prescindir totalmente de JavaScript?
Para cuatro operaciones aritméticas en el cliente, no. HTML y CSS son marcado y estilos, no realizan cálculos. El cálculo del lado del servidor mediante PHP implica recargar la página con cada pulsación de "=". JavaScript es aquí la única opción práctica. Existen trucos CSS con
calc()y casillas de verificación que imitan lógica, pero eso es una atracción, no una calculadora: como máximo una operación, sin reinicio, con un código terrible.
¿En qué se diferencia este enfoque de «tomar un paquete npm ya hecho»?
En que no tiene ni una sola dependencia.
npm install calculator-libarrastra 40 paquetes más, un empaquetador y medio megabyte de bundle, para sumar dos números. Nuestra variante: un archivo HTML, 90 líneas de JavaScript, se abre en cualquier navegador sin servidor. Para la mayoría de las tareas típicas, una calculadora de envíos, un cálculo hipotecario en una landing page, un formulario de «calcular coste», esto es suficiente.
¿Por qué no jQuery?
jQuery para una calculadora es como un minibús para ir a comprar pan. En 2017, aún tenía sentido:
$('.btn')es más corto quedocument.querySelectorAll('.btn'), y los navegadores se comportaban de forma diferente. HoyaddEventListeneryclosest()funcionan igual en todos los navegadores, y arrastrar 87 KB de jQuery comprimido para cinco manejadores de clics es excesivo. Si jQuery ya está en el proyecto, úselo, pero para código nuevo escriba en JS puro.
¿Qué hacer con la coma flotante? 0.1 + 0.2 sigue dando 0.30000000000000004.
En el código pusimos
Math.round(result * 1e10) / 1e10, esto elimina la cola en diez dígitos. Para cálculos financieros (precios, porcentajes), use enteros, calcule en céntimos, no en euros:(100 + 200) / 100 = 3.00en lugar de0.1 + 0.2. Aquí no se necesitanevalniFunction, simplemente tome los números de los campos y súmelos.
¿Debería escribir su propia calculadora o tomar una ya hecha?
Si necesita una calculadora para una sola página, escríbala usted mismo en una hora usando esta guía. El código tiene menos de 100 líneas, cero dependencias, usted controla el comportamiento.
Si está integrando el cálculo en WooCommerce o en un formulario de pedido, no escriba un analizador de expresiones. Tome los valores de los campos de entrada y calcule directamente: total = basePrice + optionsSum. Esto es más fiable, más simple y más seguro que cualquier cálculo con cadenas.
El código del artículo es una base funcional. Cambie la disposición, añada porcentajes, memoria (botones M+/M-/MR), historial de operaciones. Escriba una calculadora una vez con sus propias manos y la pregunta «¿cómo funciona esto?» quedará resuelta para siempre.



