Học bằng cách đọc theo thứ tự

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 — 12000.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.

Toán Tử Số Học và Kết Quả
a + bcộnga - btrừa * bnhâna / bchiaa % bphần dưa ** blũy thừa7 / 2→ 3.57 % 2→ 12 ** 10→ 1024
2 hàng trên là các toán tử; hàng dưới là kết quả thực tế. Chia để lại số thập phân, % 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ấy tổng và trung bình từ 3 đơn hàng trong ngày. Số tiền mỗi đơn — orderA, orderB, và orderC — đã được khai báo sẵn.

① Hiển thị tổng của cả 3 đơn.

② Hiển thị số tiền trung bình mỗi đơn.

③ Hiển thị phần dư khi tổng được đếm theo đơn vị 100.

(Nếu chạy đúng, phần giải thích sẽ xuất hiện.)

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

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.

3 Hàm Làm Tròn, 3 Kết Quả Khác Nhau
Math.floor(149.8)Làm tròn xuốngphần thập phân149Math.ceil(149.8)Làm tròn lênphần thập phân150Math.round(149.8)Làm tròn đếnsố nguyên gần nhất150
Truyền cùng 149.8 vào mỗi hàm, floor trả về 149, còn ceiling và round đều trả về 150.
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)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".

Tìm còn thiếu bao nhiêu để được miễn phí vận chuyển, và điểm tích lũy, từ thành tiền giỏ hàng. Thành tiền cartSubtotal và ngưỡng miễn phí vận chuyển freeShippingLine đã được khai báo sẵn.

① Hiển thị còn thiếu bao nhiêu để miễn phí vận chuyển — đừng để nó xuống âm khi thành tiền đã vượt ngưỡng.

② Hiển thị điểm tích lũy — 3.5% của thành tiền — làm tròn xuống thành số nguyên.

③ Hiển thị, làm tròn lên, cần thêm bao nhiêu món giá 250 nữa để đạt miễn phí vận chuyển.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

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.

toFixed và Math.round Trả Về Gì
toFixed(1)Căn chỉnh chữ số,trả về chuỗi"1903.3"toFixed(0)Căn chỉnh chữ số,trả về chuỗi"1903"Math.round(x * 10)/ 10Trả về sốđã làm tròn1903.3
Cả hai có thể trông giống nhau như cùng 1903.3, nhưng toFixed trả về chuỗi, còn Math.round trả về số.
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.

Định dạng thuế suất giảm giá của một sản phẩm sale thành thứ có thể hiển thị. Giá niêm yết listPrice và giá sale salePrice đã được khai báo sẵn.

① Hiển thị thuế suất giảm giá discountRate cho sẵn, chưa làm tròn.

② Hiển thị cùng tỷ lệ đó, căn chỉnh về một chữ số thập phân.

③ Hiển thị cùng tỷ lệ đó, căn chỉnh không có chữ số thập phân nào cả.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

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.10.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.

Điều Gì Xảy Ra Khi Số Thập Phân Được Lưu Ở Dạng Nhị Phân
0.1Không chia hếttrong nhị phânLưu thành giá trịgần bằng 0.10.2Không chia hếttrong nhị phânLưu thành giá trịgần bằng 0.20.1 + 0.2Cộng 2 giá trịgần đúngHơi lớn hơn0.3
Cả 0.1 và 0.2 đều đã hơi lệch ngay khi được lưu, nên tổng cũng trôi lệch khỏi 0.3.
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.

Cộng khối lượng sản phẩm cho một phép tính vận chuyển. Hai khối lượng, weightA và weightB, đã được khai báo sẵn.

① Hiển thị kết quả cộng trực tiếp 2 khối lượng.

② Hiển thị cùng tổng đó, làm tròn về 1 chữ số thập phân, dưới dạng số.

③ Nhân cả 2 khối lượng lên 10 lần thành số nguyên, cộng lại, rồi chia cho 10, và hiển thị kết quả.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

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ề trueNumber.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

NaNInfinity 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.

QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

Câu 1Điều gì xảy ra khi bạn chạy console.log(10 / 4);?

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ì?