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ùngBigIntvớ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.isNaNvàNumber.isFinite, không dùng các phiên bản toàn cục. - Đừng trộn
NumbervàBigInttrong 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.