Một ô báo cáo hiển thị 1.234,57, nhưng tổng tính từ dữ liệu vẫn chứa nhiều chữ số thập phân hơn. Đó có thể là hành vi đúng: định dạng cho người đọc và thay đổi dữ liệu là hai thao tác riêng. Lỗi thường bắt đầu khi ứng dụng đưa chuỗi đã định dạng quay lại phép tính.
Bài này đánh giá hai lựa chọn có sẵn trong JavaScript: toFixed và Intl.NumberFormat. Tiêu chí là kiểu kết quả, quyền kiểm soát số chữ số, hỗ trợ locale, cách tích hợp và giới hạn. Không có phép đo tốc độ hay kết luận lựa chọn nào luôn nhanh hơn.
Môi trường đối chiếu
Ví dụ chạy trên Node.js 24.18.1, ICU 78.3, ngày 11/11/2026. Tài liệu Node giải thích hỗ trợ quốc tế hóa phụ thuộc cách build ICU; vì vậy cần kiểm tra runtime đích trước khi chốt snapshot UI. Chưa chạy browser, Hermes hoặc thiết bị di động.
Hai API đều có sẵn, không thêm package trong ví dụ này. Tuy vậy, không thêm dependency không đồng nghĩa không cần kiểm thử. Dữ liệu locale và ký tự phân cách có thể khiến so sánh chuỗi quá cứng bị lỗi khi đổi môi trường.
1. Quan sát kiểu kết quả và dữ liệu ban đầu
const amount = 1234.567;
const fixed = amount.toFixed(2);
const formatter = new Intl.NumberFormat('vi-VN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
const localized = formatter.format(amount);
console.log(fixed, typeof fixed);
console.log(localized, typeof localized);
console.log(amount);
Fixture in 1234.57 string, 1.234,57 string, rồi 1234.567. Giá trị number ban đầu không bị gán lại. Cả hai chuỗi đã làm tròn phần thể hiện xuống hai chữ số thập phân, nhưng chỉ chuỗi thứ hai thể hiện quy tắc locale được chọn.
Nếu ô số phải luôn có hai chữ số sau dấu phân cách, hãy cấu hình cả minimum và maximum. Khi chỉ đặt maximum, giá trị nguyên có thể không được thêm hai số 0 như thiết kế mong muốn. Kiểm tra thêm 12 và 12.5 sẽ làm yêu cầu này rõ hơn một fixture duy nhất có ba chữ số lẻ.
2. Chọn theo hợp đồng đầu ra
toFixed(2) phù hợp khi bạn cần một chuỗi dạng thập phân cố định với dấu chấm, chẳng hạn một thông báo kỹ thuật được quy định rõ. Nó không tự thêm nhóm hàng nghìn theo tiếng Việt.
Intl.NumberFormat phù hợp cho nội dung hiển thị có locale. Bạn có thể tạo formatter ở nơi cấu hình giao diện rồi dùng lại cho các ô cùng quy tắc. Đây là cách tổ chức code trong ví dụ, không phải bằng chứng benchmark hay mức tiết kiệm CPU.
const formatter = new Intl.NumberFormat('vi-VN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
const rows = [12, 12.5, 1234.567];
const labels = rows.map(value => formatter.format(value));
console.log(labels);
console.log(Number(labels[2]));
Fixture cho các nhãn 12,00, 12,50, 1.234,57; dòng cuối là NaN. Number không phải bộ phân tích chuỗi số theo locale. Dùng parseFloat cũng không giải quyết được hợp đồng này: việc đọc được một phần chuỗi có thể tạo ra số sai mà không báo lỗi rõ ràng.
Giữ rows làm dữ liệu tính toán và labels làm dữ liệu hiển thị. Nếu cần nhận chuỗi người dùng nhập, hãy xây một luồng validation riêng với quy tắc locale cụ thể, thay vì tái sử dụng chuỗi đã render làm nguồn dữ liệu.
3. Kiểm tra cấu hình được áp dụng
const formatter = new Intl.NumberFormat('vi-VN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
const options = formatter.resolvedOptions();
console.log(options.minimumFractionDigits, options.maximumFractionDigits);
console.log(formatter.formatToParts(1234.567)
.filter(part => part.type === 'fraction')
.map(part => part.value));
Fixture in 2 2 và phần fraction 57. formatToParts hữu ích khi cần kiểm tra phần cấu thành thay vì tự cắt chuỗi bằng dấu chấm hoặc dấu phẩy. Đừng suy ra rằng mọi locale có cùng vị trí, cùng ký tự hay cùng kiểu nhóm chữ số.
Lỗi thường gặp và giới hạn
Định dạng hai chữ số không biến phép toán dấu phẩy động thành số thập phân chính xác cho nghiệp vụ tiền tệ. Bài này dùng một số giả lập để xem hành vi hiển thị; không đưa ra quy tắc kế toán, tỷ giá hay cách lưu tiền.
Thêm fixture cho số âm, số nguyên và giá trị không hữu hạn trước khi dùng chung formatter. Trong sản phẩm, nên quyết định rõ ô lỗi hiển thị thế nào thay vì để NaN đi qua toàn bộ bảng. Các phép kiểm tra hiện tại chỉ xác nhận đầu ra của runtime nêu trên.
Kết luận
Chọn API theo chuỗi mà người đọc cần thấy. Giữ số gốc riêng, nêu rõ locale và kiểm tra số chữ số; đừng parse ngược nhãn giao diện để tính tiếp.

