Bí ẩn đằng sau phép tính 0.1 + 0.2 trong JavaScript: Lỗi hay tính năng?
Bạn đã bao giờ làm việc với những con số như 1/3, với kết quả là 0.33333… và cứ tiếp tục mãi mãi chưa? Là con người, chúng ta thường làm tròn những con số như vậy, nhưng bạn đã bao giờ tự hỏi máy tính xử lý chúng như thế nào? Trong bài viết này, bạn sẽ khám phá cách máy tính quản lý các giá trị liên tục, bao gồm cả khái niệm sai số chính xác.
Chúng ta sẽ xem xét vấn đề số học dấu phẩy động, một vấn đề phổ biến ảnh hưởng đến nhiều ngôn ngữ lập trình. Chúng ta sẽ tập trung cụ thể vào cách JavaScript giải quyết thách thức này. Ngoài ra, bạn sẽ tìm hiểu cách các phép toán nhị phân hoạt động đằng sau hậu trường, ngưỡng mà JavaScript cắt bớt các số dựa trên tiêu chuẩn IEEE 754 và giới thiệu BigInt như một giải pháp để xử lý chính xác các số lớn hơn mà không bị mất độ chính xác.
Đầu tiên, hãy xem xét một ví dụ. Bạn có thể đoán được kết quả của phép toán này không?
console.log(0.1 + 0.2);
Bạn có thể nghĩ rằng câu trả lời là 0.3, phải không? Nhưng không, kết quả thực tế là:
Output: 0.30000000000000004
Bạn hẳn đang thắc mắc tại sao điều này lại xảy ra. Tại sao lại có nhiều số 0 thừa như vậy và tại sao nó lại kết thúc bằng số 4? Câu trả lời rất đơn giản: các số 0.1 và 0.2 không thể được biểu diễn chính xác trong JavaScript (nghĩa là "chính xác" hoặc "tuyệt đối"). Nghe có vẻ đơn giản phải không? Nhưng lời giải thích thì phức tạp hơn một chút. Vậy, bạn nghĩ sao—lỗi hay tính năng? Vâng, đó không phải là một lỗi. Nó thực sự là một vấn đề cơ bản về cách máy tính xử lý các con số, đặc biệt là các số dấu phẩy động.
Tại sao điều này lại xảy ra?
Hãy cùng tìm hiểu điều này với toán học cơ bản. Khi bạn cộng 0.1 và 0.2, nó sẽ bằng 1/3 ở dạng phân số, tức là 0.33333… ở dạng thập phân và nó không bao giờ kết thúc.
Điều này có nghĩa là số 3 lặp lại vô hạn. Chúng ta không thể viết ra chính xác, vì vậy chúng ta làm tròn nó thành một số gần đúng như 0.333 hoặc 0.333333 để tiết kiệm thời gian và không gian.
Tương tự, trong máy tính, chúng ta cũng phải làm tròn vì 1/3 hoặc 0.3333… sẽ là một số rất lớn và chiếm dung lượng vô hạn (mà chúng ta không có). Điều này dẫn đến cái mà chúng ta gọi là vấn đề số học dấu phẩy động.
Nói một cách đơn giản, số dấu phẩy động là những số không thể viết ra chính xác, vì vậy chúng ta làm tròn chúng. Trong máy tính, kiểu làm tròn này có thể dẫn đến các sai số chính xác nhỏ, mà chúng ta gọi là vấn đề số học dấu phẩy động.
Giải thích nhị phân
Bây giờ chúng ta đã đề cập đến lời giải thích đơn giản, hãy cùng tìm hiểu điều này theo thuật ngữ nhị phân. JavaScript xử lý mọi thứ ở dạng nhị phân đằng sau hậu trường.
Nhị phân là một hệ thống số chỉ sử dụng hai chữ số: 0 và 1.
Tại sao 0,1 và 0,2 không thể được biểu diễn chính xác trong hệ nhị phân?
Vấn đề cốt lõi là không phải tất cả các số thập phân đều có thể được biểu diễn hoàn hảo dưới dạng phân số nhị phân. Hãy lấy 0.1 làm ví dụ. Khi bạn cố gắng biểu diễn 0.1 ở dạng nhị phân, bạn sẽ phát hiện ra rằng nó không thể được biểu thị dưới dạng phân số nhị phân hữu hạn.
Thay vào đó, nó trở thành một phân số tuần hoàn, giống như cách 1/3 ở dạng thập phân trở thành 0.333…, lặp lại mãi mãi. Trong hệ nhị phân, 0.1 trở thành:
0.0001100110011001100110011001100110011... (repeating infinitely)
Vì máy tính có bộ nhớ hạn chế, nên chúng không thể lưu trữ chính xác chuỗi vô hạn này. Thay vào đó, chúng phải cắt số tại một số điểm nào đó, điều này dẫn đến một sai số làm tròn nhỏ. Đây là lý do tại sao 0.1 trong hệ nhị phân chỉ là một giá trị gần đúng của giá trị thực tế. Giống như 0.1, 0.2 cũng không thể được biểu diễn chính xác trong hệ nhị phân. Nó trở thành:
0.00110011001100110011001100110011... (repeating infinitely)
Một lần nữa, máy tính cắt bớt (cắt bỏ một phần của một số để phù hợp với giới hạn hoặc loại bỏ các chữ số thừa) chuỗi nhị phân vô hạn này, dẫn đến một sai số nhỏ trong biểu diễn. Vậy điều gì xảy ra khi chúng ta cộng 0.1 + 0.2? Khi bạn cộng 0.1 + 0.2 trong JavaScript, các giá trị gần đúng nhị phân cho 0.1 và 0.2 được cộng lại với nhau.
Nhưng vì cả hai giá trị đều chỉ là giá trị gần đúng, nên kết quả cũng là một giá trị gần đúng. Thay vì nhận được chính xác 0.3, bạn nhận được một giá trị gần bằng:
console.log(0.1 + 0.2); // Output: 0.30000000000000004
Sai số nhỏ này xảy ra vì cả 0.1 và 0.2 đều không thể được biểu diễn chính xác trong hệ nhị phân, do đó kết quả cuối cùng có một sai số làm tròn nhỏ.
JavaScript cắt bớt số như thế nào?
Bây giờ, câu hỏi đặt ra là: JavaScript biết khi nào cần cắt bớt giá trị như thế nào?
(Cắt bớt có nghĩa là cắt bớt hoặc rút ngắn một số bằng cách loại bỏ các chữ số thừa vượt quá một điểm nhất định. )
Có giới hạn tối đa và tối thiểu cho việc này.
Để xử lý vấn đề này trong thế giới máy tính, chúng ta có một tiêu chuẩn xác định cách lưu trữ và tính toán số dấu phẩy động.
Tiêu chuẩn IEEE 754
JavaScript sử dụng tiêu chuẩn IEEE 754 để xử lý số học dấu phẩy động.
Tiêu chuẩn này xác định giới hạn số nguyên an toàn cho Numberkiểu trong JavaScript mà không làm mất độ chính xác:
- Số nguyên an toàn tối đa: 2^53 - 1 hoặc 9007199254740991
- Số nguyên an toàn tối thiểu: -(2^53 - 1) hoặc -9007199254740991
Ngoài những giới hạn này, JavaScript không thể biểu diễn chính xác số nguyên do cách thức hoạt động của số học dấu phẩy động.
Vì lý do này, JavaScript cung cấp hai hằng số để biểu diễn các giới hạn này:
- Number.MAX_SAFE_INTEGER
- Number.MIN_SAFE_INTEGER
Nếu tôi cần số lớn hơn thì sao?
Nếu bạn cần làm việc với các số lớn hơn Số nguyên an toàn tối đa (như những số được sử dụng trong mật mã hoặc tài chính), JavaScript có giải pháp: BigInt.
BigInt là một đối tượng tích hợp cho phép bạn làm việc với các số nguyên vượt quá giới hạn số nguyên an toàn. Nó cho phép bạn biểu diễn các số lớn hơn 9007199254740991, do đó bạn không cần phải lo lắng về lỗi độ chính xác ở đây!
Để sử dụng BigInt, chỉ cần thêm "n" vào cuối một số nguyên:
const bigNumber = 1234567890123456789012345678901234567890n;
Ngoài ra, bạn có thể sử dụng hàm tạo BigInt:
const bigNumber = BigInt("1234567890123456789012345678901234567890");
Các hoạt động với BigInt
Bạn có thể thực hiện các phép tính số học với BigInt, như phép cộng, phép trừ, phép nhân và thậm chí là phép lũy thừa. Tuy nhiên, có một điều đáng lưu ý: bạn không thể kết hợp BigInt với Number các kiểu thông thường trong các phép tính số học mà không chuyển đổi rõ ràng giữa chúng.
Ví dụ, điều này sẽ không hiệu quả:
let result = bigNumber + 5; // Error: cannot mix BigInt and other types
Bạn sẽ cần phải chuyển đổi Number thành BigInt đầu tiên:
let result = bigNumber + BigInt(5); // Now it works!
Chúng ta sử dụng BigInt ở đâu?
BigIntđặc biệt hữu ích trong các lĩnh vực đòi hỏi độ chính xác, chẳng hạn như:
- Thuật toán mật mã
- Xử lý các tập dữ liệu lớn
- Tính toán tài chính đòi hỏi sự chính xác
Tóm tắt
Giới hạn số nguyên an toàn trong JavaScript đảm bảo biểu diễn số chính xác cho các số nguyên từ -(2^53 - 1) đến 2^53 - 1.
Lỗi độ chính xác xảy ra do phép tính số dấu phẩy động khi xử lý một số số nhất định (như 0,1 + 0,2).
Nếu bạn cần số lớn hơn giới hạn an toàn, BigInt thì đây là bạn của bạn. Nhưng hãy nhớ rằng, việc trộn BigInt và Number các loại yêu cầu chuyển đổi rõ ràng.
All rights reserved