Câu 1Điều gì xảy ra khi bạn chạy console.log(10 / 4);?
Số và Phép Tính — Kiểu Number và Math
Học số trong JavaScript từ kiểu Number: phần dư và lũy thừa, làm tròn với Math, toFixed và sai số thập phân.
Rất nhiều phép tính trong một ứng dụng web quy về số — thành tiền đơn hàng, thuế bán hàng, phí vận chuyển, điểm tích lũy. JavaScript không có kiểu riêng cho số nguyên và số thập phân; nó xử lý cả hai bằng cùng một kiểu Number.
Bài này nói về các toán tử số học, gồm cả phần dư và lũy thừa, làm tròn với Math, căn chỉnh chữ số với toFixed, và các lỗi sai số thập phân gây rắc rối khi tính toán tiền bạc.
Một Kiểu Number — Number và Các Toán Tử Số Học
Một số ngôn ngữ tách số nguyên và số thập phân thành các kiểu riêng. Khái niệm "loại giá trị" này gọi là kiểu (type) (cách phân loại một giá trị — nó là số, văn bản, v.v.).
JavaScript không phân biệt số nguyên và số thập phân — 1200 và 0.08 đều là cùng kiểu Number (kiểu đại diện cho số). Bạn không bao giờ chọn kiểu khi khai báo biến, nhưng đánh đổi là chia hai số nguyên có thể để lại kết quả thập phân.
Các ký hiệu chỉ đạo một phép tính gọi là toán tử số học (arithmetic operators) (ký hiệu đại diện cho một phép tính giữa các giá trị). Ngoài cộng +, trừ -, nhân *, và chia /, còn có % cho phần dư của phép chia và ** cho lũy thừa.
% trả về phần dư, và ** trả về lũy thừa.console.log(1200 + 300); // 1500
console.log(1200 - 300); // 900
console.log(1200 * 3); // 3600
// Dù là 2 số nguyên, vẫn còn phần thập phân nếu chia không hết
console.log(7 / 2); // 3.5
// % là phần dư, ** là lũy thừa
console.log(7 % 2); // 1
console.log(2 ** 10); // 1024
% rất hữu ích để tìm phần còn lại, hay "phần dư khi chia thành các nhóm có kích thước cố định". Với tổng 4380, 4380 % 100 cho bạn 80 — phần còn lại sau khi đếm theo đơn vị 100.
** cho bạn lũy thừa, giúp tính những thứ như 2 ** 10 — số byte trong một kilobyte.
Làm Tròn Với Math — Floor, Ceiling, và Gần Nhất
Bạn không thể đưa thẳng số trung bình, 1903.3333333333333, lên hóa đơn. Số tiền và số lượng cần làm tròn thành số nguyên ở đâu đó, và làm tròn xuống, lên, hay đến giá trị gần nhất tùy vào quy tắc nghiệp vụ.
JavaScript gom các hàm làm tròn này dưới Math.
Hãy giới thiệu thuật ngữ hàm (function) trước (một đơn vị có tên chạy một quy trình cố định trên giá trị bạn truyền vào, và trả về kết quả). Nhận lại kết quả từ giá trị bạn truyền vào được gọi là "gọi" nó và để nó "trả về" một giá trị.
Một hàm dưới Math (tập hợp hàm số học có sẵn của JavaScript) được gọi bằng cách viết Math, một dấu chấm, rồi tên hàm — như Math.floor(1903.33).
Math.floor làm tròn xuống, Math.ceil làm tròn lên, và Math.round làm tròn đến số nguyên gần nhất. Cả ba chỉ trả về một số mới — biến bạn truyền vào giữ nguyên.
const points = 149.8; // Điểm tính từ số tiền mua hàng
console.log(Math.floor(points)); // 149 làm tròn xuống
console.log(Math.ceil(points)); // 150 làm tròn lên
console.log(Math.round(points)); // 150 làm tròn đến gần nhất
console.log(points); // 149.8 giá trị gốc không đổi
// max trả về giá trị nào trong 2 giá trị truyền vào lớn hơn
console.log(Math.max(0, 5000 - 5200)); // 0
Một hàm khác bạn sẽ dùng ngoài việc làm tròn là Math.max. Nó trả về giá trị nào trong các giá trị bạn truyền vào lớn hơn, nên Math.max(0, -120) là 0.
Điều đó cho bạn một mức sàn cho phép tính bạn không muốn tụt xuống dưới 0 — hữu ích cho những thứ như "còn thiếu bao nhiêu để được miễn phí vận chuyển".
Căn Chỉnh Chữ Số Với toFixed — Nó Trả Về Chuỗi
Đôi khi, như với số tiền trung bình, bạn muốn quyết định trước số chữ số hiển thị. Hiển thị 1903.3333333333333 nguyên trạng khó đọc, nên bạn làm gọn nó về một chữ số thập phân và hiện 1903.3.
Dùng toFixed cho việc đó.
toFixed là một phương thức (method) (một hàm bạn gọi bằng cách viết dấu chấm sau một giá trị) — bạn gọi nó với dấu chấm sau một số. Giá trị truyền vào bên trong dấu ngoặc gọi là đối số (argument) (giá trị truyền vào bên trong dấu ngoặc của hàm hoặc phương thức). Truyền cho nó số chữ số thập phân muốn giữ, như toFixed(1), và nó làm tròn ở chữ số tiếp theo rồi trả về kết quả giữ đúng số chữ số đó. Nhưng thứ trả về là một chuỗi, không phải số. Chỉ dùng nó ngay trước khi hiển thị giá trị — để tính toán tiếp, hãy dùng số gốc hoặc Math.round thay vào đó.
Math.floor(value) truyền một giá trị cho một hàm sống bên trong khối Math; value.toFixed(digits) gọi một hàm mà chính giá trị đó mang theo. Cái đứng trước dấu chấm cho bạn biết bạn đang thấy dạng nào.
const averageOrder = 1903.3333333333333; // Số tiền trung bình mỗi đơn
console.log(averageOrder.toFixed(1)); // 1903.3 trả về dưới dạng chuỗi
console.log(averageOrder.toFixed(0)); // 1903 trả về dưới dạng chuỗi
// Để tiếp tục tính toán, làm tròn mà vẫn giữ là số
console.log(Math.round(averageOrder * 10) / 10); // 1903.3 vẫn là số
Kết Quả Của toFixed Chỉ Để Hiển Thị
Vì thứ toFixed trả về là một chuỗi, cộng hay nhân kết quả đó sẽ hoạt động khác so với một số. Sự khác biệt giữa chuỗi và số, và cách kiểm tra bạn đang có cái nào, được nói đến trong bài về chuyển đổi kiểu.
Sai Số Thập Phân — Vì Sao 0.1 + 0.2 Không Phải 0.3
Nối đủ nhiều phép tính có số thập phân, và đáp án có thể trôi lệch một chút. Đây là nguyên nhân gây ra các lỗi như tổng tiền lệch một xu, nên hãy xem tại sao nó xảy ra và cách tránh trước khi nó gây rắc rối cho bạn.
Máy tính lưu số ở dạng nhị phân. 0.1 ở hệ thập phân không chia hết trong hệ nhị phân — nó trở thành một phân số lặp vô hạn — nên nó được lưu thành một giá trị khác, rất gần thay vào đó.
0.1 và 0.2 đều được lưu hơi lệch, nên cộng chúng lại cũng lệch khỏi 0.3 một chút.
console.log(0.1 + 0.2); // 0.30000000000000004
// Nhân lên thành số nguyên trước, tính, rồi chia trở lại — không lệch
console.log((0.1 * 10 + 0.2 * 10) / 10); // 0.3
Lưu Số Tiền Dưới Dạng Số Nguyên
Càng nối nhiều phép cộng thập phân, độ lệch càng cộng dồn. Với số tiền, hãy lưu chúng dưới dạng đơn vị số nguyên và chỉ chia ngay trước khi hiển thị — cách đó tránh hoàn toàn lỗi này.
Giá Trị Từ Phép Tính Thất Bại — NaN và Infinity
Khi một phép tính không ra kết quả, JavaScript không dừng lại — nó trả về một giá trị đặc biệt và tiếp tục chạy. Rất nhiều lỗi khi tổng tiền hiện trống hoặc số tiền hiển thị sai đến từ loại giá trị này len vào một phép tính cứ thế chạy tiếp.
Một kết quả không thể biểu diễn thành số là NaN (viết tắt của Not a Number — giá trị nghĩa là kết quả không thể trở thành số). 0 / 0, từ việc cố tính trung bình một ngày không đơn hàng nào, là một ví dụ.
Chia cho 0 theo hướng ngược lại, như 5710 / 0, trả về Infinity (giá trị đại diện cho vô cực). Không cái nào dừng lại với lỗi, nên bạn có thể không nhận ra cho đến khi nó hiện trên màn hình.
const orderTotal = 5710;
const orderCount = 0; // Ngày đó có 0 đơn hàng
console.log(orderTotal / orderCount); // Infinity
console.log(0 / 0); // NaN
// Một khi NaN lẫn vào, mọi phép tính sau đó cũng thành NaN
console.log(0 / 0 + 1000); // NaN
Giống như Math, Number gom lại tập hợp hàm liên quan đến số của riêng nó. Bạn có thể kiểm tra một giá trị có phải số nguyên hay không bằng Number.isInteger. Number.isInteger(3) trả về true và Number.isInteger(2.5) trả về false — một boolean (giá trị đại diện cho một trong hai lựa chọn: điều gì đó đúng hay không) — nên nó hữu ích để xác nhận một giá trị như số lượng không được phép có phần thập phân.
Cách kiểm tra hai giá trị có bằng nhau hay không sẽ nói ở bài tiếp theo, nhưng NaN là ngoại lệ duy nhất — nó thậm chí không bằng chính nó khi dùng cách đó.
Nên kiểm tra NaN dùng một cách kiểm tra riêng thay vào đó. Điều đó sẽ được nói đến, cùng với chuyển chuỗi sang số, trong bài về chuyển đổi kiểu.
console.log(Number.isInteger(3)); // true
console.log(Number.isInteger(2.5)); // false
console.log(Number.isInteger(6 / 2)); // true chia hết là số nguyên
NaN và Infinity chảy vào phép tính tiếp theo mà không bao giờ dừng lại với lỗi. Khi tổng tiền hay trung bình hiển thị sai, hãy nghi ngờ phép chia cho 0 trước tiên — đó thường là cách nhanh nhất để tìm ra nguyên nhân.
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Gọi toFixed(2) trên một số trả về gì?
Câu 3console.log(0.1 + 0.2); hiển thị gì?