Nguyễn Đình Cường
0 comment
09 Nov, 2026
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.
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).
<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>.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; }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();
});e.target.closest('button') để biết nút nào được bấm.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.NaN và hiển thị "Lỗi" thay vì Infinity.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.keydown.calculate với các trường hợp biê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.
Nguyễn Đình Cường
0 comment