Menu

Ép kiểu trong JavaScript: chuyển đổi ngầm định và tường minh

JavaScript ép kiểu giá trị như thế nào: những quy tắc ngầm định hay gây nhầm lẫn, các phép chuyển đổi tường minh nên ưu tiên và khi nào mỗi loại xảy ra.

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.

Hai kiểu ép kiểu

JavaScript khá dễ dãi về kiểu dữ liệu. Khi một toán tử nhận một giá trị "sai" kiểu, ngôn ngữ không ném lỗi mà chuyển đổi nó. Việc chuyển đổi đó gọi là ép kiểu (coercion), và có hai dạng:

  • Tường minh: bạn chủ động yêu cầu: Number("42"), String(99), Boolean(value).
  • Ngầm định: một toán tử kích hoạt nó mà bạn không viết phép chuyển đổi nào: "5" - 2, "" == 0, if (value).

Cả hai cuối cùng đều dùng chung các quy tắc chuyển đổi bên dưới. Khác biệt nằm ở chỗ bạn hay ngôn ngữ quyết định chuyển đổi. Hầu như mọi kết quả khó hiểu trong JavaScript, như "5" + 1 === "51", [] == false, null == undefined, đều đến từ việc ép kiểu ngầm định khiến bạn bất ngờ.

Cách nghĩ đơn giản: nếu bạn gõ Number(...) hoặc String(...), bạn biết mình đang yêu cầu gì. Nếu không, mỗi toán tử lại có ý kiến riêng về việc phải làm gì, và những ý kiến đó chính là nơi bug ẩn náu.

Ba kiểu đích

Ép kiểu luôn chuyển về một trong ba kiểu nguyên thủy: chuỗi, số hoặc boolean. (Còn một kiểu thứ tư là bigint, nhưng các kiểu khác không bao giờ tự động bị ép sang nó.) Mọi quy tắc còn lại đều suy ra từ việc toán tử đang nhắm tới kiểu đích nào.

Ép sang chuỗi là kiểu dễ dãi nhất: giá trị nào cũng có dạng chuỗi. Để ý rằng đối tượng được chuyển thành chuỗi "[object Object]" chẳng mấy hữu ích, đó là lý do việc in một đối tượng nối với chuỗi ("user: " + user) gần như không bao giờ cho thấy điều bạn muốn. Hãy dùng JSON.stringify hoặc template literal với các trường cụ thể.

Ép sang số

Ép sang số thì chặt chẽ hơn. Chuỗi phải thực sự trông giống một con số, nếu không bạn sẽ nhận được NaN:

Những điều bất ngờ được đánh dấu trong ngoặc là những điểm nên ghi nhớ:

  • Chuỗi rỗng và chuỗi chỉ có khoảng trắng trở thành 0, không phải NaN.
  • null trở thành 0, nhưng undefined trở thành NaN.
  • Mảng rỗng trở thành 0; mảng một phần tử được ép theo chính phần tử đó; mảng nhiều phần tử trở thành NaN.

Nếu bạn muốn đọc một con số từ chuỗi có thêm ký tự thừa, hãy dùng parseInt hoặc parseFloat thay cho Number:

Luôn truyền cơ số (10) cho parseInt. Nếu không, chuỗi bắt đầu bằng "0x" sẽ được đọc theo hệ thập lục phân, hiếm khi là điều bạn muốn.

Ép sang boolean

Ép sang boolean là đơn giản nhất trong ba loại. Một danh sách ngắn các giá trị được ép thành false, mọi thứ còn lại là true.

Các giá trị falsy gồm:

  • false
  • 0, -0, 0n
  • "" (chuỗi rỗng)
  • null
  • undefined
  • NaN

Chỉ vậy thôi. Mọi giá trị khác, kể cả "false", "0", [] và {}, đều là truthy.

Kết quả với mảng rỗng và đối tượng rỗng hay làm khó những ai đến từ Python, nơi các tập hợp rỗng là falsy. Trong JavaScript, chúng là truthy: nếu bạn muốn hỏi "mảng này có rỗng không?", hãy kiểm tra tường minh arr.length === 0.

Ép sang boolean xảy ra bất cứ khi nào một giá trị xuất hiện ở chỗ cần boolean: if (...), while (...), toán tử ba ngôi ? : và các toán tử logic &&, ||, !.

Toán tử + là trường hợp đặc biệt

Hầu hết các toán tử số học ép toán hạng của chúng sang số. + thì khác: nếu một trong hai vế là chuỗi, + thực hiện nối chuỗi. Nếu không, nó thực hiện phép cộng số.

Biểu thức được tính từ trái sang phải. 1 + 2 + "3" trước tiên tính 1 + 2 = 3, rồi 3 + "3" = "33". Còn "1" + 2 + 3 bắt đầu là chuỗi và giữ nguyên là chuỗi: "12", rồi "123".

Đó là lý do việc ghép chuỗi bằng + rất dễ hỏng. Template literal không gặp vấn đề này:

Template literal tính count + 1 như một biểu thức số riêng, rồi mới chèn kết quả vào. Không có phép ép kiểu bất ngờ nào.

Ưu tiên chuyển đổi tường minh

Khi bạn cần chuyển đổi kiểu, hãy viết rõ ra. Chỉ tốn thêm vài ký tự mà xóa sạch mọi mơ hồ cho người đọc sau:

Với boolean cũng vậy. !!value chạy được và khá phổ biến, nhưng Boolean(value) nói chính xác nó làm gì:

Một quy tắc hợp lý: dùng chuyển đổi tường minh trong logic của ứng dụng, và để dành các dạng viết tắt (+x, !!x) cho những chỗ cần ngắn gọn và ý định đã rõ ràng từ ngữ cảnh.

Toán tử == dựa rất nhiều vào ép kiểu

Các toán tử so sánh bằng là nguồn gây bất ngờ lớn nhất về ép kiểu. == ép kiểu trước khi so sánh; === thì không.

Mỗi kết quả true ở trên là kết quả của một chuỗi ép kiểu nhiều bước mà hầu hết lập trình viên không thể đọc thuộc lòng. Vấn đề nằm ở đó: code chạy đúng nhờ may mắn là code sẽ hỏng về sau. Chúng ta sẽ đi vào đầy đủ các quy tắc so sánh bằng ở trang tiếp theo; còn bây giờ, điều cần nhớ là: mặc định hãy dùng ===, và chỉ dùng == cho đúng một cách viết quen thuộc là x == null (bắt được cả null lẫn undefined).

Ghép tất cả lại

Một ví dụ hoàn chỉnh cho thấy khi nào ép kiểu có ích và khi nào nó gây hại:

Hãy để ý lời gọi thứ hai. Number("") trả về 0 chứ không phải NaN, nên parsePrice("") trả về 0, có lẽ không phải điều người dùng hàm này mong muốn. Nếu đầu vào rỗng cần bị từ chối, hãy thêm một phép kiểm tra tường minh:

Biết rõ những giá trị nào được ép thành 0 và những giá trị nào thành NaN chính là thứ giúp bạn tránh được một bug khó thấy ở phía sau.

Những điều cần nhớ

  • Ép kiểu chuyển giá trị sang chuỗi, số hoặc boolean tùy theo toán tử.
  • + với bất kỳ chuỗi nào sẽ thành phép nối chuỗi; mọi toán tử số học khác đều ép sang số.
  • Các giá trị falsy là một danh sách ngắn cố định: hãy thuộc lòng nó. Mọi thứ khác đều truthy, kể cả [] và {}.
  • Number("") là 0, Number([]) là 0, Number(null) là 0, nhưng Number(undefined) là NaN. Những điều này xuất hiện trong các bug thực tế.
  • Ưu tiên Number(x), String(x), Boolean(x) thay vì các mẹo ngầm định khéo léo. Bạn của tương lai sẽ biết ơn điều đó.

Tiếp theo: toán tử so sánh bằng

Mọi điều ép kiểu làm trong phép so sánh đều nằm bên trong ==. Trang tiếp theo sẽ đi qua ==, === và Object.is, cách viết duy nhất mà == vẫn còn hữu ích, và vì sao các linter mặc định cảnh báo dạng so sánh lỏng lẻo này.

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

Ép kiểu (type coercion) trong JavaScript là gì?

Ép kiểu là việc JavaScript tự động chuyển một giá trị từ kiểu này sang kiểu khác: số thành chuỗi, chuỗi thành số, hoặc bất cứ thứ gì thành boolean. Nó diễn ra ngầm định khi các toán tử như +, == hay if gặp một giá trị không đúng kiểu chúng cần, và diễn ra tường minh khi chính bạn gọi Number(x), String(x) hoặc Boolean(x).

Ép kiểu ngầm định và tường minh khác nhau thế nào?

Ép kiểu tường minh là khi bạn chủ động gọi một hàm chuyển đổi: Number("42"), String(99), Boolean(value). Ép kiểu ngầm định là khi một toán tử âm thầm kích hoạt việc chuyển đổi: "5" - 2 cho ra 3, "5" + 2 cho ra "52". Tường minh thì dễ đọc và dễ đoán; ngầm định là nguồn gốc của hầu hết các bug kiểu "sao lại ra NaN".

Làm sao chuyển chuỗi sang số trong JavaScript?

Dùng Number("42") để chuyển đổi chặt chẽ (trả về NaN nếu chuỗi không phải số hợp lệ), hoặc parseInt("42px", 10) / parseFloat("3.14em") nếu bạn muốn đọc một con số ở đầu một chuỗi lộn xộn hơn. Toán tử một ngôi + (+"42") làm việc giống Number() nhưng dễ bị bỏ sót khi đọc lướt code.

Vì sao [] + [] trả về chuỗi rỗng?

Toán tử + không có phép toán số nào có nghĩa cho hai mảng, nên JavaScript ép cả hai sang chuỗi. Mảng được chuyển thành chuỗi bằng cách nối các phần tử bằng dấu phẩy, và mảng rỗng nối lại thành "". Vậy [] + [] trở thành "" + "", tức là "". Đây là một mẹo vui để khoe, và cũng là lý do chính đáng để không bao giờ dựa vào + với các giá trị không phải kiểu nguyên thủy.

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