This programming guide is only available in Vietnamese. Switch to Vietnamese to read the full article.
Trước khi chạm tới Closures, Prototype hay Event Loop, mọi lập trình viên JavaScript đều phải nắm vững
nền tảng tuyệt đối: hệ thống kiểu dữ liệu (type system). JavaScript là ngôn ngữ
động (dynamically typed) và yếu (weakly typed), nghĩa là biến không
bị ràng buộc kiểu khi khai báo, và engine sẵn sàng tự động ép kiểu (coercion) giá trị
từ kiểu này sang kiểu khác. Chính sự "linh hoạt" này là nguồn gốc của hàng loạt bug kinh điển. Bài học
nền tảng này sẽ mổ xẻ tường tận 7 kiểu nguyên thủy, các cạm bẫy số học IEEE 754, cơ chế ép kiểu, sự
khác biệt giữa == và ===, truthy/falsy và boxing — từ cấp độ người mới đến
chuyên gia.
1. 7 Kiểu Nguyên Thủy (Primitive) + Object
Đặc tả ECMAScript định nghĩa 7 kiểu nguyên thủy (primitive types) và một kiểu tham chiếu duy nhất là Object. Một giá trị primitive là bất biến (immutable) và được so sánh theo giá trị, trong khi Object được so sánh theo tham chiếu (địa chỉ bộ nhớ). Bảy kiểu nguyên thủy gồm:
- undefined — giá trị mặc định của biến đã khai báo nhưng chưa gán; biểu thị "chưa có giá trị".
- null — giá trị "rỗng có chủ đích", do lập trình viên gán để nói "không có đối tượng nào".
- boolean — chỉ gồm
truevàfalse. - number — số dấu phẩy động 64-bit theo chuẩn IEEE 754 (cả số nguyên lẫn số thực đều dùng chung kiểu này).
- string — chuỗi ký tự UTF-16, bất biến.
-
bigint — số nguyên lớn tùy ý, hậu tố
n(ví dụ9007199254740993n), bổ sung từ ES2020. - symbol — giá trị duy nhất và không thể trùng lặp, thường dùng làm khóa thuộc tính ẩn, bổ sung từ ES2015.
Toán tử typeof trả về một chuỗi mô tả kiểu của toán hạng. Tuy nhiên có một "lỗi lịch sử"
nổi tiếng: typeof null === 'object'. Đây là một bug từ phiên bản JavaScript đầu tiên (1995), nơi giá trị được lưu cùng một thẻ kiểu
(type tag) với object. Bug này không bao giờ được sửa vì sẽ phá vỡ vô số đoạn mã hiện hữu trên web.
Một điểm cần nhớ khác: typeof đối với hàm trả về 'function' (dù về bản chất
hàm là một Object đặc biệt callable), còn typeof với mảng trả về 'object' —
muốn kiểm tra mảng phải dùng Array.isArray().
typeof undefined; // "undefined"
typeof null; // "object" ← bug lịch sử nổi tiếng
typeof true; // "boolean"
typeof 42; // "number"
typeof "hello"; // "string"
typeof 10n; // "bigint"
typeof Symbol("id"); // "symbol"
typeof {}; // "object"
typeof []; // "object" ← mảng cũng là object
typeof function(){}; // "function" ← biệt lệ tiện lợi
Vì typeof null không đáng tin cậy để phân biệt null với object, ta cần các kỹ thuật kiểm
tra an toàn hơn. Để phân biệt một cách chắc chắn null, undefined, mảng và object thuần, hãy kết hợp
nhiều phép kiểm tra:
function kind(v) {
if (v === null) return "null";
if (Array.isArray(v)) return "array";
return typeof v; // undefined | boolean | number | string | bigint | symbol | object | function
}
kind(null); // "null"
kind([1, 2, 3]); // "array"
kind({}); // "object"
kind(undefined); // "undefined"
// Cách lấy "type tag" nội bộ chính xác nhất:
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call(/re/); // "[object RegExp]"
2. Kiểu Number & Các Cạm Bẫy
Trong JavaScript, mọi number (kể cả số "nguyên" như 3) đều được biểu diễn
bằng số dấu phẩy động độ chính xác kép 64-bit theo chuẩn IEEE 754. Định dạng này dùng
1 bit dấu, 11 bit số mũ (exponent) và 52 bit phần định trị (mantissa). Hệ quả là số nguyên chỉ được
biểu diễn chính xác tuyệt đối trong khoảng an toàn từ -(2^53 - 1) đến
2^53 - 1, tức Number.MAX_SAFE_INTEGER === 9007199254740991. Vượt ngoài
khoảng này, các số nguyên bắt đầu bị làm tròn — đây chính là lý do bigint ra đời.
Cạm bẫy nổi tiếng nhất là 0.1 + 0.2 !== 0.3. Kết quả thực tế là 0.30000000000000004. Nguyên nhân: hệ nhị phân không thể biểu diễn
chính xác các phân số thập phân như 0.1 hay 0.2 (giống như hệ thập phân không biểu diễn chính xác được
1/3). Sai số làm tròn cực nhỏ tích lũy và lộ ra. Để so sánh số thực an toàn, ta dùng
Number.EPSILON — khoảng cách nhỏ nhất giữa 1 và số kế tiếp lớn hơn 1 —
làm ngưỡng dung sai.
Hai giá trị đặc biệt cần nắm: NaN (Not-a-Number) phát sinh từ phép toán
vô nghĩa như 0/0 hay "abc" * 2; điểm kỳ lạ là NaN là giá trị
duy nhất không bằng chính nó (NaN === NaN trả về false).
Infinity phát sinh từ 1/0 hoặc khi số vượt
Number.MAX_VALUE. Để kiểm tra NaN, hãy ưu tiên Number.isNaN() thay vì hàm
toàn cục isNaN() cũ — bởi isNaN() ép kiểu đối số sang number trước khi kiểm
tra, dẫn đến kết quả sai như isNaN("abc") === true.
0.1 + 0.2; // 0.30000000000000004
0.1 + 0.2 === 0.3; // false (!)
// So sánh an toàn bằng Number.EPSILON
function nearlyEqual(a, b) {
return Math.abs(a - b) < Number.EPSILON;
}
nearlyEqual(0.1 + 0.2, 0.3); // true
Number.MAX_SAFE_INTEGER; // 9007199254740991
9007199254740991 + 1; // 9007199254740992
9007199254740991 + 2; // 9007199254740992 (sai do làm tròn!)
0 / 0; // NaN
"abc" * 2; // NaN
1 / 0; // Infinity
-1 / 0; // -Infinity
NaN === NaN; // false — NaN không bằng chính nó
isNaN("abc"); // true ← ép "abc" -> NaN trước (gây hiểu lầm)
Number.isNaN("abc"); // false ← đúng: "abc" không phải kiểu number là NaN
Number.isNaN(NaN); // true ← chỉ true với đúng giá trị NaN
3. Type Coercion (Ép Kiểu)
Ép kiểu (coercion) là quá trình chuyển đổi một giá trị từ kiểu này sang kiểu khác. Có hai dạng:
tường minh (explicit) khi lập trình viên chủ động gọi Number(),
String(), Boolean(); và ngầm định (implicit) khi engine tự
động chuyển đổi trong các phép toán như +, ==, hay khi dùng giá trị trong
ngữ cảnh boolean. Bên dưới, engine chạy các thuật toán trừu tượng của đặc tả:
ToString, ToNumber, ToBoolean và quan trọng nhất là
ToPrimitive.
Thuật toán ToPrimitive được gọi khi một object cần biến thành giá trị nguyên thủy. Nó
thử lần lượt hai phương thức valueOf() và toString() (thứ tự phụ thuộc
"hint": "number", "string" hay "default"). Với hầu hết object,
valueOf() trả về chính object đó (không phải primitive), nên engine rơi xuống dùng
toString(). Đây là gốc rễ của những kết quả "kỳ quái" đến nỗi trở thành meme trong cộng
đồng JS.
Toán tử + đặc biệt nhập nhằng: nếu một trong hai toán hạng là string
(sau ToPrimitive), nó thực hiện nối chuỗi; ngược lại nó thực hiện
cộng số. Vì vậy [] + [] cho ra chuỗi rỗng "" (mảng rỗng
ToString thành ""), còn [] + {} cho ra "[object Object]". Các
toán tử -, *, / luôn ép sang number, nên
'5' - 3 === 2. Toán tử !! (hai lần phủ định) là cách viết tắt phổ biến để ép
một giá trị bất kỳ sang boolean tương ứng với truthy/falsy của nó.
Number("42"); // 42
Number(""); // 0 ← chuỗi rỗng thành 0
Number(" 12 "); // 12 ← cắt khoảng trắng
Number("12px"); // NaN
Number(true); // 1
Number(null); // 0
Number(undefined); // NaN
String(123); // "123"
String(null); // "null"
String([1, 2, 3]); // "1,2,3"
Boolean(0); // false
Boolean("0"); // true ← chuỗi "0" KHÁC số 0
[] + []; // "" (mảng rỗng -> "")
[] + {}; // "[object Object]"
1 + "2"; // "12" (nối chuỗi vì có string)
"5" - 3; // 2 (- luôn ép sang number)
+"5" + 3; // 8 (+"5" ép "5"->5 trước)
true + 1; // 2 (true -> 1)
[1, 2] + [3, 4];// "1,23,4" (cả hai -> "1,2" và "3,4" rồi nối)
!!""; // false
!!"text"; // true
!!0; // false
!!42; // true
!!null; // false
!![]; // true ← mọi object đều truthy, kể cả mảng rỗng
Bảng tóm tắt các quy tắc ép kiểu thường gặp:
| Giá trị gốc | ToNumber | ToString | ToBoolean |
|---|---|---|---|
undefined |
NaN |
"undefined" |
false |
null |
0 |
"null" |
false |
true |
1 |
"true" |
true |
"" |
0 |
"" |
false |
"12" |
12 |
"12" |
true |
[] |
0 |
"" |
true |
[3] |
3 |
"3" |
true |
{} |
NaN |
"[object Object]" |
true |
4. == vs === & Abstract Equality Algorithm
JavaScript có hai toán tử so sánh bằng.
=== (Strict Equality / so sánh nghiêm ngặt) trả về true chỉ
khi cùng kiểu VÀ cùng giá trị, không bao giờ ép kiểu.
== (Loose / Abstract Equality / so sánh lỏng) sẽ
ép kiểu hai toán hạng về cùng kiểu trước khi so sánh, theo một thuật toán nhiều bước
trong đặc tả ECMAScript.
Tóm tắt Abstract Equality Algorithm cho x == y: nếu cùng kiểu thì so
sánh như ===; null == undefined trả về true (và chúng không
lỏng-bằng bất cứ thứ gì khác); nếu một bên là number, một bên là string thì string được ToNumber; nếu
một bên là boolean thì boolean được ToNumber; nếu một bên là object và bên kia là primitive thì object
được ToPrimitive. Đây là lý do xuất hiện những kết quả gây sốc như 0 == "" (cả hai
ToNumber thành 0) hay "" == false (đều thành 0).
Quy tắc thực dụng: luôn dùng === để loại bỏ ép kiểu bất ngờ. Trường hợp
== được coi là an toàn và hữu ích duy nhất là kiểm tra "null hoặc undefined" trong một
lần: x == null đúng khi và chỉ khi x là null hoặc
undefined. Riêng NaN không bằng chính nó với cả == lẫn
===. Để so sánh "đồng nhất hơn cả ===", ES2015 cung cấp
Object.is(): nó coi NaN bằng NaN, nhưng phân biệt +0 với
-0 (điều mà === không làm).
0 == ""; // true ("" -> 0)
0 == "0"; // true ("0" -> 0)
"" == "0"; // false (cùng string, khác giá trị)
false == ""; // true (cả hai -> 0)
null == undefined; // true (quy tắc đặc biệt)
null == 0; // false (null chỉ lỏng-bằng undefined)
0 === ""; // false (khác kiểu, không ép)
null === undefined; // false (khác kiểu)
function greet(name) {
// Bắt cả null lẫn undefined trong một lần
if (name == null) {
return "Xin chào, khách!";
}
return "Xin chào, " + name;
}
greet(null); // "Xin chào, khách!"
greet(undefined); // "Xin chào, khách!"
greet("An"); // "Xin chào, An"
NaN === NaN; // false
Object.is(NaN, NaN); // true ← Object.is coi chúng bằng nhau
-0 === 0; // true
Object.is(-0, 0); // false ← phân biệt +0 và -0
Object.is(1, 1); // true
Object.is({}, {}); // false (hai tham chiếu khác nhau)
Bảng so sánh nhanh ba cơ chế:
| Biểu thức | == |
=== |
Object.is |
|---|---|---|---|
0, "" |
true | false | false |
null, undefined |
true | false | false |
NaN, NaN |
false | false | true |
+0, -0 |
true | true | false |
1, 1 |
true | true | true |
5. Truthy / Falsy
Khi một giá trị được dùng trong ngữ cảnh boolean (điều kiện if, vòng lặp
while, toán tử &&, ||, ?:), engine ngầm
gọi ToBoolean. Cần thuộc lòng chính xác 8 giá trị falsy — tất cả
những thứ còn lại đều là truthy:
false0(số 0)-0(âm 0)0n(bigint 0)""(chuỗi rỗng)nullundefinedNaN
Điều bất ngờ với người mới: "0" (chuỗi chứa số 0), [] (mảng rỗng),
{} (object rỗng) và function(){} đều là truthy. Cạm bẫy
thực tế: khi kiểm tra if (count) để xem biến có giá trị, bạn vô tình loại bỏ luôn giá trị
hợp lệ 0; tương tự if (name) loại bỏ chuỗi rỗng "". Trong các
tình huống này nên kiểm tra tường minh như if (count !== undefined) hoặc dùng toán tử
nullish ??.
const falsy = [false, 0, -0, 0n, "", null, undefined, NaN];
falsy.forEach(v => console.log(v, "->", Boolean(v))); // tất cả -> false
// Mọi thứ còn lại là truthy:
Boolean("0"); // true
Boolean([]); // true
Boolean({}); // true
Boolean(" "); // true (chuỗi chứa khoảng trắng)
function showCount(count) {
if (count) return "Có " + count + " mục"; // BUG: count = 0 bị bỏ qua
return "Không có dữ liệu";
}
showCount(0); // "Không có dữ liệu" ← sai ý đồ!
// Sửa: kiểm tra tường minh hoặc dùng ??
function showCountFixed(count) {
if (count == null) return "Không có dữ liệu";
return "Có " + count + " mục";
}
showCountFixed(0); // "Có 0 mục" ← đúng
6. Boxing & Wrapper Objects
Các kiểu nguyên thủy string, number, boolean,
symbol, bigint không phải object nên về lý thuyết không có phương thức. Vậy
tại sao "hello".toUpperCase() chạy được? Câu trả lời là
autoboxing (đóng hộp tự động): khi bạn truy cập một thuộc tính hoặc gọi một phương
thức trên một primitive, engine tạm thời tạo ra một wrapper object tương ứng
(String, Number, Boolean...), gọi phương thức trên đó, rồi
vứt bỏ object tạm thời ngay sau khi xong.
Vì wrapper object là tạm thời, mọi cố gắng gán thuộc tính lên primitive đều thất bại âm thầm:
"abc".foo = 1 không ném lỗi (ở chế độ thường) nhưng "abc".foo vẫn là
undefined. Quan trọng:
đừng bao giờ tạo wrapper bằng new (như new String("x")), vì
kết quả là một object thực sự — typeof trả về "object", và nó luôn truthy kể
cả khi bọc giá trị falsy như new Boolean(false). Hãy chỉ dùng String(),
Number(), Boolean() không có new để ép kiểu.
// Autoboxing: primitive mượn phương thức của wrapper rồi vứt object tạm
"hello".toUpperCase(); // "HELLO"
(42).toFixed(2); // "42.00"
"abc".length; // 3
// Gán thuộc tính lên primitive không có tác dụng (object tạm bị vứt)
let s = "abc";
s.foo = 123;
console.log(s.foo); // undefined
// Đừng dùng new — tạo ra object thật, gây lỗi logic
const wrong = new Boolean(false);
typeof wrong; // "object"
if (wrong) console.log("chạy vào đây dù bọc false!"); // truthy!
const ok = Boolean(false); // false primitive (đúng cách)
typeof ok; // "boolean"
7. Câu Hỏi Trắc Nghiệm Ôn Tập
Kiểm tra mức độ nắm vững của bạn với ba câu hỏi sau:
Trắc nghiệm 1: typeof null
Biểu thức typeof null trả về giá trị nào?
Trắc nghiệm 2: So sánh lỏng
Biểu thức 0 == "" cho kết quả gì, và tại sao?
Trắc nghiệm 3: NaN và Object.is
Cách nào kiểm tra ĐÚNG rằng một biến x chính là giá trị NaN?
Tải file code thực hành minh họa bài học
Chúng tôi đã viết sẵn một file script hoàn chỉnh tích hợp khám phá typeof, cạm bẫy IEEE 754, các ví dụ ép kiểu, so sánh == vs === vs Object.is, danh sách falsy và autoboxing:
Tải về js_fundamentals.js
Comments
Bình luận