Làm máy tính bỏ túi bằng JavaScript: xử lý nút bấm và biểu thức an toàn

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Máy tính bỏ túi là một dự án kinh điển để luyện JavaScript: giao diện dạng lưới nút bấm, xử lý nhiều sự kiện và một chút logic trạng thái. Bài này hướng dẫn bạn làm một máy tính có bốn phép tính cơ bản mà không dùng hàm eval nguy hiểm.

Thiết kế trước khi viết code

Máy tính cần ba thành phần dữ liệu: số đang nhập, phép tính đang chọn và số đã nhập trước đó. Khi người dùng bấm một phép tính, ta lưu số hiện tại và phép tính, đợi số thứ hai. Khi bấm dấu bằng, ta tính kết quả. Bạn có thể xem đây là một "máy trạng thái" nhỏ.

Về giao diện, bố cục dạng lưới rất hợp với CSS Grid. Mỗi nút mang thông tin của nó trong thuộc tính data-, nhờ đó ta chỉ cần một bộ lắng nghe sự kiện cho cả bàn phím (kỹ thuật event delegation).

Bước 1: HTML

<div class="calc">
  <div id="display" class="display">0</div>
  <div class="keys" id="keys">
    <button data-action="clear" class="span-2">AC</button>
    <button data-action="delete">⌫</button>
    <button data-op="/">÷</button>

    <button data-num="7">7</button>
    <button data-num="8">8</button>
    <button data-num="9">9</button>
    <button data-op="*">×</button>

    <button data-num="4">4</button>
    <button data-num="5">5</button>
    <button data-num="6">6</button>
    <button data-op="-">−</button>

    <button data-num="1">1</button>
    <button data-num="2">2</button>
    <button data-num="3">3</button>
    <button data-op="+">+</button>

    <button data-num="0" class="span-2">0</button>
    <button data-action="dot">.</button>
    <button data-action="equals">=</button>
  </div>
</div>

Bước 2: CSS Grid

.calc { width: 280px; margin: 40px auto; background: #1f2937; padding: 16px; border-radius: 16px; }
.display { background: #111827; color: #fff; font-size: 2rem; text-align: right; padding: 16px; border-radius: 8px; margin-bottom: 12px; overflow-x: auto; }
.keys { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
.keys button { padding: 16px 0; font-size: 1.2rem; border: 0; border-radius: 8px; cursor: pointer; }
.span-2 { grid-column: span 2; }

Bước 3: Logic JavaScript

Ta giữ trạng thái trong ba biến và định nghĩa hàm tính riêng. Việc tách hàm calculate giúp bạn dễ kiểm thử và tránh eval.

const display = document.getElementById('display');
const keys = document.getElementById('keys');

let current = '0';      // số đang nhập (dạng chuỗi)
let previous = null;    // số trước đó
let operator = null;    // phép tính đang chờ
let justCalculated = false;

function calculate(a, b, op) {
  switch (op) {
    case '+': return a + b;
    case '-': return a - b;
    case '*': return a * b;
    case '/': return b === 0 ? NaN : a / b;
    default: return b;
  }
}

function format(value) {
  if (Number.isNaN(value)) return 'Lỗi';
  // làm tròn để tránh 0.1 + 0.2 = 0.30000000000000004
  return String(Math.round(value * 1e10) / 1e10);
}

function updateDisplay() {
  display.textContent = current;
}

function inputNumber(n) {
  if (justCalculated) { current = '0'; justCalculated = false; }
  current = current === '0' ? n : current + n;
}

function inputDot() {
  if (justCalculated) { current = '0'; justCalculated = false; }
  if (!current.includes('.')) current += '.';
}

function chooseOperator(op) {
  if (operator && !justCalculated && previous !== null) {
    previous = calculate(previous, parseFloat(current), operator);
    current = format(previous);
  } else {
    previous = parseFloat(current);
  }
  operator = op;
  justCalculated = true;
}

function equals() {
  if (operator === null || previous === null) return;
  const result = calculate(previous, parseFloat(current), operator);
  current = format(result);
  previous = null;
  operator = null;
  justCalculated = true;
}

keys.addEventListener('click', (e) => {
  const btn = e.target.closest('button');
  if (!btn) return;

  if (btn.dataset.num !== undefined) inputNumber(btn.dataset.num);
  else if (btn.dataset.op) chooseOperator(btn.dataset.op);
  else if (btn.dataset.action === 'dot') inputDot();
  else if (btn.dataset.action === 'equals') equals();
  else if (btn.dataset.action === 'delete') current = current.length > 1 ? current.slice(0, -1) : '0';
  else if (btn.dataset.action === 'clear') { current = '0'; previous = null; operator = null; }

  updateDisplay();
});

Giải thích những chỗ dễ nhầm

  • Event delegation: thay vì gắn 19 bộ lắng nghe, ta gắn một bộ vào vùng chứa và dùng e.target.closest('button') để biết nút nào được bấm.
  • Số thực trong JavaScript: 0.1 + 0.2 không đúng bằng 0.3 do cách máy tính biểu diễn số thập phân nhị phân. Việc làm tròn khi hiển thị là cách xử lý đơn giản cho máy tính nhỏ; với tiền tệ, nên tính bằng số nguyên của đơn vị nhỏ nhất.
  • Chia cho 0: ta trả về NaN và hiển thị "Lỗi" thay vì Infinity.
  • Vì sao không dùng eval: eval chạy bất kỳ đoạn mã nào, nên nếu chuỗi đến từ người dùng sẽ rất nguy hiểm. Hãy tính bằng hàm riêng như trên.

Thử thách mở rộng

  1. Cho phép nhập bằng bàn phím bằng sự kiện keydown.
  2. Thêm nút phần trăm và đổi dấu.
  3. Thêm lịch sử các phép tính vừa thực hiện.
  4. Viết một vài bài kiểm thử cho hàm calculate với các trường hợp biên.

Kết luận

Bạn vừa luyện CSS Grid, event delegation, quản lý trạng thái và những cạm bẫy của số thực. Những kỹ năng này lặp lại trong hầu hết giao diện web có tính tương tác.

Nếu muốn thực hành JavaScript cùng các dự án có hướng dẫn chi tiết, hãy xem các khóa học tại codebem.com.

author_photo
Nguyễn Đình Cường

0 comment