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)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 =====, 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 truefalse.
  • 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 của từng kiểu nguyên thủy
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

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:

Kiểm tra kiểu an toàn
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]"
▶ Thử chạy: typeof Explorer

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.

IEEE 754 và so sánh số thực
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!)
NaN, Infinity và isNaN vs Number.isNaN
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()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ó.

Ép kiểu tường minh (explicit)
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
Ép kiểu ngầm định và toán tử +
[] + [];        // ""               (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)
Ép boolean vớ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
▶ Thử chạy: Coercion Lab

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 xnull 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).

== vs === với các cặp khó
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)
Mẫu kiểm tra null an toàn với ==
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"
Object.is — đồng nhất hơn cả ===
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
▶ Thử chạy: Equality Lab

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:

  1. false
  2. 0 (số 0)
  3. -0 (âm 0)
  4. 0n (bigint 0)
  5. "" (chuỗi rỗng)
  6. null
  7. undefined
  8. NaN

Đ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 ??.

8 giá trị falsy trong thực tế
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)
Cạm bẫy: 0 và "" bị loại nhầm
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 và bẫy new Wrapper
// 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

Related Articles

Bài viết liên quan trong series

Lesson 2: Modern JavaScript: Destructuring, Spread & Optional Chaining Bài 2: JavaScript Hiện Đại: Destructuring, Spread & Optional Chaining Back to JavaScript Series Overview Quay lại Lộ trình JavaScript Series