Menu

Number và BigInt trong JavaScript: độ chính xác và số nguyên lớn

Kiểu Number của JavaScript thực sự hoạt động thế nào: độ chính xác của số thực dấu phẩy động, MAX_SAFE_INTEGER và khi nào nên chuyển sang BigInt.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Một kiểu số cho mọi thứ (gần như vậy)

Phần lớn ngôn ngữ có kiểu riêng cho số nguyên và số thực. Về mặt lịch sử, JavaScript chỉ cho bạn một kiểu: Number. Dù bạn viết 42, 3.14 hay -0.001, bạn đều nhận được cùng một kiểu nguyên thủy: số thực dấu phẩy động độ chính xác kép 64 bit theo chuẩn IEEE 754.

Điều đó khá tiện: không phải ép kiểu giữa int và float, không bị tràn số ở 2^31. Nhưng cách biểu diễn số thực có những hệ quả, và chúng thường xuyên làm khó người mới. Một kiểu số thứ hai, BigInt, được thêm vào năm 2020 để xử lý những trường hợp mà Number không đáp ứng được.

Bất ngờ với số thực dấu phẩy động

Hãy chạy đoạn này:

Dòng đầu in ra 0.30000000000000004. Dòng thứ hai in ra false. Đây không phải điểm lạ riêng của JavaScript: Python, Java, C và mọi ngôn ngữ dùng số thực IEEE 754 đều cho kết quả như vậy.

Lý do: 0.1 và 0.2 không thể biểu diễn chính xác ở dạng nhị phân, giống như 1/3 không thể viết chính xác ở dạng thập phân. Máy lưu giá trị nhị phân gần đúng nhất, và các sai số nhỏ cộng dồn lại. Cách nghĩ dễ nhớ: hãy coi các giá trị Number có phần thập phân là giá trị gần đúng, chỉ là rất gần với những gì bạn đã viết.

Với tiền tệ, đừng lưu $19.99 thành 19.99. Hãy lưu số xu dưới dạng số nguyên, tức 1999, và chỉ định dạng lại khi hiển thị. Đó là thói quen tốt nhất để tránh bug số thực.

So sánh số thực một cách an toàn

Vì phép so sánh bằng không đáng tin, hãy so sánh với một sai số cho phép khi cần:

Number.EPSILON là hiệu nhỏ nhất giữa 1 và số biểu diễn được liền sau nó, một sai số mặc định hợp lý cho các giá trị gần 1. Với các giá trị rất lớn hoặc rất nhỏ, bạn sẽ cần một sai số thay đổi theo độ lớn của đầu vào.

Khoảng số nguyên an toàn

Các số nguyên tới một kích thước nhất định vẫn chính xác trong số thực 64 bit. Vượt qua ngưỡng đó, bạn bắt đầu mất dần độ chính xác từng bit một:

2^53 - 1 là số nguyên cuối cùng mà mọi số nguyên nhỏ hơn nó đều biểu diễn được. Vượt quá mức đó, một số số nguyên đơn giản là không tồn tại trong kiểu Number: chúng được làm tròn về số gần nhất. Đó là một bug làm hỏng dữ liệu âm thầm đang chờ xảy ra nếu bạn đọc ID 64 bit từ cơ sở dữ liệu dưới dạng số JSON thông thường.

Làm quen với BigInt

BigInt là một kiểu nguyên thủy riêng cho số nguyên có độ chính xác tùy ý. Bạn tạo nó bằng cách thêm n vào cuối một số nguyên, hoặc gọi BigInt(...):

BigInt không có giới hạn trên nào ngoài bộ nhớ khả dụng. Đây là công cụ phù hợp cho:

  • ID trong cơ sở dữ liệu hoặc ID snowflake của Twitter/X vượt quá 2^53.
  • Các phép toán mật mã học.
  • Mọi phép tính số nguyên mà kết quả chính xác quan trọng hơn tốc độ thuần túy.

Nó không phù hợp cho các bộ đếm thông thường, chỉ số mảng hay tiền tính bằng xu: Number thông thường nhanh hơn và làm việc được với mọi API trong ngôn ngữ.

Phép tính với BigInt

Mọi toán tử quen thuộc đều dùng được, miễn là cả hai toán hạng đều là BigInt:

Phép chia làm tròn về phía 0: không có BigInt nào có phần thập phân. Nếu cần phân số, bạn quay lại với Number (hoặc dùng một thư viện số thập phân).

Đừng trộn kiểu

Quy tắc khiến nhiều người vấp: bạn không thể trộn Number và BigInt trong cùng một biểu thức.

So sánh là ngoại lệ duy nhất: <, >, == tự chuyển đổi qua lại giữa hai kiểu:

Vậy == coi chúng là bằng nhau, còn === thì không. Nếu bạn đã dùng === ở mọi nơi (và bạn nên làm vậy), hãy coi việc so sánh số giữa hai kiểu này là dấu hiệu thiết kế chưa ổn: chọn một bên và chuyển đổi.

Chuyển đổi giữa hai kiểu

Hai chiều chuyển đổi, hai cái bẫy:

Chuyển Number → BigInt rất nghiêm ngặt: số thập phân và NaN sẽ ném lỗi. Chuyển BigInt → Number dễ dãi nhưng có thể mất dữ liệu: mọi giá trị vượt quá MAX_SAFE_INTEGER đều bị làm tròn. Nếu bạn đang chuyển đổi một BigInt nhận được từ máy chủ, hãy tự hỏi xem mình có thực sự cần làm vậy không.

Các giá trị số đặc biệt

Nhân tiện, có ba giá trị thuộc kiểu Number nhưng không phải là số theo nghĩa toán học:

Infinity và -Infinity xuất hiện khi bạn chia cho 0 hoặc vượt quá phạm vi số thực. NaN ("not a number", không phải số) xuất hiện khi một phép tính không cho ra kết quả có nghĩa.

NaN nổi tiếng vì không bằng chính nó: đây là một phần của chuẩn IEEE 754, không phải bug của JS. Hãy dùng Number.isNaN(x) để kiểm tra. Hàm toàn cục isNaN cũ hơn sẽ chuyển đổi đối số trước, nên cho kết quả sai (isNaN("hello") trả về true). Luôn ưu tiên phiên bản Number.isNaN.

Chuyển chuỗi thành số

Dữ liệu người dùng nhập và số trong JSON thường đến dưới dạng chuỗi. Có ba cách chuyển đổi:

Number() rất nghiêm ngặt: mọi thứ không phải số đều cho NaN, trừ chuỗi rỗng và khoảng trắng, vốn cho 0. parseInt và parseFloat thì dễ dãi: chúng đọc được tới đâu thì đọc rồi dừng. Hãy chọn cách phù hợp với ý định của bạn, và kiểm tra NaN trước khi dùng kết quả.

Để chuyển chuỗi thành BigInt, hãy dùng BigInt("123"): hàm này nghiêm ngặt và ném lỗi khi đầu vào không hợp lệ.

Bộ quy tắc nhanh

  • Với bộ đếm, phép toán, tọa độ và phần lớn các con số hằng ngày: dùng Number.
  • Với tiền tệ: đổi sang số nguyên đơn vị xu và dùng Number, hoặc dùng một thư viện số thập phân.
  • Với số nguyên lớn hơn 2^53 (ID cơ sở dữ liệu, mật mã, tổ hợp): dùng BigInt với hậu tố n.
  • So sánh số thực với một sai số cho phép, không dùng ===.
  • Kiểm tra kết quả không hợp lệ bằng Number.isNaN và Number.isFinite, không dùng các phiên bản toàn cục.
  • Đừng trộn Number và BigInt trong cùng một biểu thức: hãy chuyển đổi rõ ràng.

Tiếp theo: null và undefined

JavaScript có hai cách nói "không có giá trị", null và undefined, và chúng không thể thay thế cho nhau. Bài tiếp theo: ý nghĩa của từng giá trị, chúng khác nhau thế nào và khi nào nên dùng giá trị nào.

Câu hỏi thường gặp

Vì sao 0.1 + 0.2 không bằng 0.3 trong JavaScript?

Vì kiểu Number của JavaScript là số thực dấu phẩy động 64 bit theo chuẩn IEEE 754, và 0.1 cùng 0.2 không thể biểu diễn chính xác ở dạng nhị phân. Kết quả là 0.30000000000000004. Đây không phải bug của JavaScript: điều này xảy ra cả trong Python, Java và mọi ngôn ngữ dùng cùng định dạng số thực. Với tiền tệ, hãy đổi sang số nguyên (đơn vị xu) hoặc dùng một thư viện số thập phân.

BigInt trong JavaScript là gì và khi nào nên dùng?

BigInt là một kiểu số nguyên thủy riêng cho các số nguyên vượt quá Number.MAX_SAFE_INTEGER (2^53 - 1). Tạo BigInt bằng cách thêm n ở cuối, như 9007199254740993n, hoặc gọi BigInt(value). Hãy dùng nó cho ID 64 bit trong cơ sở dữ liệu, mật mã học hoặc bất kỳ phép tính số nguyên nào mà độ chính xác quan trọng hơn tốc độ.

Có thể trộn Number và BigInt trong JavaScript không?

Không. 1n + 1 ném ra TypeError: Cannot mix BigInt and other types. Hãy chuyển đổi rõ ràng bằng BigInt(n) hoặc Number(b). Các toán tử so sánh như < và == vẫn dùng được giữa hai kiểu, nhưng === trả về false vì kiểu dữ liệu khác nhau.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu