Type Coercion & Metaprogramming trong JavaScript
Bạn đã bao giờ nhìn vào kết quả "5" + 3 = "53" mà không hiểu tại sao chưa? Hay tại sao new Date() - new Date() cho ra số, còn new Date() + new Date() lại cho ra chuỗi? Đây không phải bug - đây là type coercion, cơ chế JavaScript tự động chuyển đổi kiểu dữ liệu khi cần thiết.
Bài này sẽ giải thích toàn bộ cơ chế coercion từ operators cơ bản, ToPrimitive coercion trên objects, cho đến Symbol.toPrimitive - công cụ metaprogramming cho phép bạn kiểm soát hoàn toàn hành vi này.
#1. Type Coercion là gì
Type coercion là cơ chế JavaScript tự động chuyển đổi kiểu dữ liệu khi cần thiết. Thay vì báo lỗi khi bạn cộng số với chuỗi, JavaScript cố gắng làm cho phép tính “có nghĩa” nhất có thể.
Có 3 luồng coercion chính:
- ToNumber - chuyển sang số (khi dùng
*,/,-, unary+) - ToString - chuyển sang chuỗi (khi dùng
+với string) - ToBoolean - chuyển sang boolean (khi dùng trong
if,||,&&)
Tại sao JavaScript thiết kế vậy? Vì dữ liệu từ form HTML luôn là string. Nếu không có coercion, bạn phải tự chuyển kiểu mọi lúc. JavaScript làm điều đó tự động - nhưng đôi khi theo cách bạn không mong đợi.
#2. Coercion với Operators
#2.1. Nhân, chia, trừ: luôn chuyển về số
Các toán tử *, /, - luôn kích hoạt ToNumber coercion:
1
2
3
4
"5" * 3 // 15 ✅ string "5" chuyển thành số 5
"10" / 2 // 5 ✅
"8" - 3 // 5 ✅
"abc" * 3 // NaN ❌ không thể chuyển "abc" thành số#2.2. Cộng: trường hợp đặc biệt
Toán tử + có hành vi khác. JavaScript sẽ chuyển các operand thành primitive value trước (ToPrimitive), sau đó nếu một trong hai là string, nó thực hiện nối chuỗi thay vì cộng số:
1
2
3
"5" + 3 // "53" ← nối chuỗi, không phải cộng số!
3 + "5" // "35" ← tương tự
5 + 3 // 8 ← cả hai là số, cộng bình thườngĐây là nguồn gốc của rất nhiều bug khi xử lý dữ liệu từ form:
1
2
3
4
5
6
7
8
9
10
11
12
<input id="price" type="text" value="100" />
<button onclick="calculate()">Tính tổng</button>
<p id="result"></p>
<script>
function calculate() {
const price = document.getElementById("price").value; // "100" (string!)
const tax = 10;
document.getElementById("result").textContent = `Tổng: ${price + tax}đ`; // "10010đ" 😱
}
</script>#2.3. Manual coercion để tránh bug
Cách fix: chuyển kiểu thủ công trước khi tính toán.
1
2
3
4
5
6
7
8
// Cách 1 (ngắn gọn nhất): unary +
const price = +"100"; // 100 (number)
// Cách 2 (rõ ràng hơn): Number()
const price = Number("100"); // 100
// Cách 3 (khi string có ký tự thừa): parseInt / parseFloat
const price = parseInt("100px"); // 100 (bỏ qua "px")Fix lại bug ở trên:
1
2
3
4
5
6
7
8
9
10
11
12
<input id="price" type="text" value="100" />
<button onclick="calculate()">Tính tổng</button>
<p id="result"></p>
<script>
function calculate() {
const price = +document.getElementById("price").value; // unary + chuyển sang number
const tax = 10;
document.getElementById("result").textContent = `Tổng: ${price + tax}đ`; // 110đ ✅
}
</script>Giải thích:
- Unary
+ngắn gọn - dùng khi code ngắn Number()rõ ràng hơn - dùng trong code productionparseInt(str)linh hoạt hơn khi xử lý string có đơn vị như"100px"
#2.4. ToBoolean coercion
Khi JavaScript cần boolean (trong if, while, ||, &&), nó tự động chuyển giá trị sang boolean. Các giá trị falsy bị coi là false:
| Giá trị | Kiểu |
|---|---|
0, -0 | Number |
"" | String |
null | Null |
undefined | Undefined |
NaN | Number |
false | Boolean |
Tất cả giá trị còn lại đều là truthy, kể cả "0", [], {}.
Ngoài các giá trị trên,
document.alltrong browser cũng được coi là falsy vì lý do legacy - đây là falsy object duy nhất trong JavaScript theo ECMAScript spec.
1
2
3
4
5
6
7
8
9
10
11
12
// Validate form - nhận donation
function submitDonation(amount) {
if (!amount) {
console.log("Vui lòng nhập số tiền");
return;
}
console.log(`Cảm ơn bạn đã donate ${amount}đ`);
}
submitDonation(""); // "Vui lòng nhập số tiền"
submitDonation(0); // "Vui lòng nhập số tiền" ← cả 0 cũng bị bắt!
submitDonation(100); // "Cảm ơn bạn đã donate 100đ"Lưu ý: !amount bắt cả 0 lẫn "" - đôi khi đây không phải điều bạn muốn. Dùng === để phân biệt rõ từng trường hợp.
#3. Date Objects và ToPrimitive Coercion
Date là ví dụ điển hình của object coercion. Khi JavaScript cần dùng một object trong phép toán, nó sẽ gọi cơ chế @@toPrimitive để chuyển object thành primitive value.
1
2
3
4
5
const start = new Date("2026-01-01");
const end = new Date("2026-03-14");
console.log(end - start); // 6566400000 (milliseconds) ✅
console.log(end + start); // "Sat Mar 14 2026...Thu Jan 01 2026..." (string!) 🤔Tại sao - ra số còn + ra chuỗi? Vì khi JavaScript cần chuyển Date thành primitive:
- Với
-(cần số): gọiDate[@@toPrimitive]("number")→ trả về milliseconds - Với
+: gọiDate[@@toPrimitive]("default")→ nhưngDatexử lý hint"default"giống"string"(đây là đặc điểm riêng của Date theo spec) → trả về chuỗi
1
2
3
4
5
6
7
8
const now = new Date();
console.log(+now); // 1741910400000 - milliseconds từ 1/1/1970
// Đo thời gian thực thi
const t1 = new Date();
doSomethingExpensive();
const t2 = new Date();
console.log(`Thời gian: ${t2 - t1}ms`);#4. Symbols trong ES6
Symbol là kiểu primitive đặc biệt được giới thiệu trong ES6, mỗi Symbol luôn unique:
1
2
3
4
const s1 = Symbol("id");
const s2 = Symbol("id");
console.log(s1 === s2); // false - luôn unique dù cùng descriptionMục đích chính của Symbol là tạo property key không bị conflict:
1
2
3
4
5
6
7
8
9
const PRICE_KEY = Symbol("price");
const item = {
[PRICE_KEY]: 100, // private-ish property với Symbol key
name: "Coffee",
};
console.log(item[PRICE_KEY]); // 100
console.log(Object.keys(item)); // ["name"] - Symbol key không xuất hiện!JavaScript có một số well-known symbols mà engine tự động nhận biết - khi bạn gán chúng vào object, engine sẽ gọi chúng vào đúng thời điểm. Symbol.toPrimitive, Symbol.iterator, Symbol.hasInstance đều thuộc loại này. Đây là nền tảng của metaprogramming trong JavaScript.
#5. Symbol.toPrimitive - Metaprogramming
Bạn có thể tự định nghĩa hành vi coercion cho object của mình thông qua Symbol.toPrimitive. Vì đây là một Symbol, không thể gán bằng dot notation mà phải dùng bracket notation:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const product = {
name: "Laptop",
price: 25000000,
};
product[Symbol.toPrimitive] = function(hint) {
if (hint === "number") return this.price;
if (hint === "string") return this.name;
return this.price; // hint === "default"
};
console.log(+product); // 25000000 (hint: "number")
console.log(`${product}`); // "Laptop" (hint: "string")
console.log(product + 5000); // 25005000 (hint: "default" → number)Giải thích hint:
"number": khi dùng các phép toán số như-,*hoặc unary+"string": khi dùng template literal hoặcString()"default": khi dùng+hoặc==(không rõ ràng)
Ví dụ thực tế - một đối tượng Money tự xử lý coercion:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Money {
constructor(amount, currency) {
this.amount = amount;
this.currency = currency;
}
[Symbol.toPrimitive](hint) {
if (hint === "string") {
return `${this.amount.toLocaleString()} ${this.currency}`;
}
return this.amount;
}
}
const price = new Money(25000000, "VNĐ");
const discount = new Money(5000000, "VNĐ");
console.log(`Giá: ${price}`); // "Giá: 25,000,000 VNĐ"
console.log(price - discount); // 20000000
console.log(price > 10000000); // trueType coercion trong JavaScript không phải điều kỳ lạ - nó có quy tắc rõ ràng khi bạn hiểu 3 luồng chuyển đổi (ToNumber, ToString, ToBoolean). Luôn dùng Number() hay unary + để chủ động chuyển kiểu khi xử lý input từ người dùng. Với Symbol.toPrimitive, bạn không chỉ hiểu JavaScript mà còn có thể kiểm soát nó - đó là bước đầu tiên vào thế giới metaprogramming.