Chương 02 · Variables, Types & Coercion

Biến, kiểu dữ liệu và type coercion

JavaScript là dynamic typed — biến không có type cố định, giá trị mới có. Khi 2 type khác nhau gặp nhau ở toán tử, JS dùng type coercion để convert ngầm. Hiểu rule này = không bị bug [] == ![] hay '5' - 3 === 2.

Độ dài: ~850 dòng Bài tập: 5 Quiz: 7 Prerequisites: Chương 1
🎯 Mục tiêu chương
  • Hiểu 3 cách khai báo biến: var, let, const và khi nào dùng cái nào.
  • Biết 7 primitive types và typeof trả về gì cho từng cái.
  • Phân biệt null vs undefined — khi nào JS sinh cái nào.
  • Master type coercion: implicit (+, ==, if) vs explicit (Number(), String(), Boolean()).
  • Tránh gotcha kinh điển: NaN !== NaN, 0.1 + 0.2, typeof null === 'object'.
  • Hiểu truthy/falsy và 6 falsy values.

1. Khai báo biến: var, let, const

JavaScript có 3 keyword khai báo biến. Hiểu khác nhau là quan trọng:

varletconst
Phiên bảnTừ ES1 (1997)ES6 (2015)ES6 (2015)
ScopeFunction scopeBlock scopeBlock scope
HoistingCó, init = undefinedCó nhưng ở TDZCó nhưng ở TDZ
ReassignOKOK❌ (lỗi)
Redeclare cùng scopeOK (silent)❌ (lỗi)❌ (lỗi)
Khuyến nghịTránhKhi cần reassignMặc định

Demo block scope:

Scope demo
if (true) {
  var a = 1;
  let b = 2;
  const c = 3;
}

console.log(a); // 1   ← var "leak" ra ngoài block
console.log(b); // ReferenceError: b is not defined
console.log(c); // ReferenceError: c is not defined

Demo hoisting + TDZ (Temporal Dead Zone):

Hoisting demo
// var — hoisted với value = undefined
console.log(x); // undefined (không lỗi)
var x = 5;

// let — hoisted nhưng ở TDZ → lỗi khi truy cập
console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 5;
🧠 Mental model — TDZ

Tưởng tượng JS đọc code 2 lượt. Lượt 1: ghi nhận mọi khai báo (var/let/const/function) — "có 1 biến tên x ở đây". Lượt 2: chạy code thật. Giữa "lượt 1 đã thấy" và "lượt 2 đến dòng khai báo" là TDZ.

var trong TDZ có value undefined (không lỗi). let/const trong TDZ throw ReferenceError. Mục đích: ép developer khai báo trước khi dùng.

Quy tắc cho code mới (2026):

  • Mặc định const. Hầu hết biến không cần reassign.
  • Đổi sang let chỉ khi thật sự cần reassign (counter loop, accumulator).
  • Không dùng var. Đọc code cũ thì biết là gì, nhưng đừng viết mới.
🔥 Gotcha — const không freeze object

const chỉ chặn reassign biến, không chặn mutate object/array.

const obj = { a: 1 };
obj.a = 2;        // OK! mutate property
obj.b = 3;        // OK! thêm property
obj = {};         // ❌ TypeError: Assignment to constant variable

Muốn freeze object thật sự: Object.freeze(obj). Nhưng nó chỉ shallow — nested object vẫn mutate được.

2. 7 Primitive types

JavaScript có 7 primitive types + 1 không-phải-primitive (Object). Primitive là immutable — bạn không thể "sửa" 1 primitive, chỉ tạo cái mới.

TypeVí dụtypeofGhi chú
string'abc', "hi", `hello`'string'UTF-16 internal
number42, 3.14, NaN, Infinity'number'IEEE 754 double
bigint9007199254740993n'bigint'ES2020. Cho số > MAX_SAFE_INTEGER
booleantrue, false'boolean'Chỉ 2 giá trị
undefinedundefined'undefined'JS tự sinh khi biến chưa init
nullnull'object' 🔥Developer explicit gán
symbolSymbol('id')'symbol'ES6. Unique identifier
Object{}, [], function'object' / 'function'Mọi thứ còn lại
🔥 Gotcha lịch sử — typeof null

typeof null === 'object' là một bug từ JavaScript 1995. Brendan Eich biết về nó nhưng không thể fix vì sẽ break web — quá nhiều code đã rely on bug này.

Để check null: x === null hoặc x == null (cũng match undefined).

2.1. number — chỉ có 1 type số

Khác Dart (int, double) hoặc Python (int arbitrary precision, float), JavaScript chỉ có 1 type duy nhất cho cả số nguyên và thập phân — number. Internal là IEEE 754 double-precision floating point (64-bit).

Number facts
typeof 42;        // 'number'
typeof 3.14;      // 'number' — không có 'float' riêng
typeof NaN;       // 'number' 🔥 (NaN là một loại number)
typeof Infinity;  // 'number'

Number.MAX_SAFE_INTEGER;  // 9007199254740991 (2^53 - 1)
Number.MIN_SAFE_INTEGER;  // -9007199254740991
Number.EPSILON;           // 2.220446049250313e-16

// Vượt MAX_SAFE_INTEGER → mất độ chính xác
9007199254740993 === 9007199254740992;  // true (!!)

Cho số lớn hơn MAX_SAFE_INTEGER (vd: id của Twitter, financial calculation), dùng BigInt:

BigInt
const big = 9007199254740993n;  // hậu tố `n`
const big2 = BigInt("9007199254740993");

// Không thể trộn BigInt với Number
big + 1;     // ❌ TypeError
big + 1n;    // ✅ 9007199254740994n
🔥 Gotcha — 0.1 + 0.2

0.1 + 0.2 === 0.3 trả về false. Vì IEEE 754 không biểu diễn được chính xác 0.1 hay 0.2 trong binary.

0.1 + 0.2;                              // 0.30000000000000004
0.1 + 0.2 === 0.3;                       // false

// Cách so sánh đúng:
Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON;  // true

// Hoặc dùng toFixed():
(0.1 + 0.2).toFixed(2);                  // '0.30' (string)

Cho tính tiền chính xác, dùng thư viện decimal.js, big.js hoặc Decimal của Money library (vd dinero.js).

2.2. string và template literal

3 cách tạo string. Backtick có khả năng đặc biệt:

String literals
const a = 'single quote';
const b = "double quote";
const c = `template literal`;

// Template literal hỗ trợ:
// 1. Interpolation
const name = 'Việt';
const greet = `Xin chào, ${name}!`;  // 'Xin chào, Việt!'

// 2. Multiline
const text = `Dòng 1
Dòng 2
Dòng 3`;

// 3. Expression bên trong
const sum = `${2 + 3}`;  // '5'

// 4. Function call
const upper = `${name.toUpperCase()}`;  // 'VIỆT'

String immutable — không sửa được ký tự, chỉ tạo string mới:

const s = 'abc';
s[0] = 'z';  // silent fail (strict mode: lỗi)
console.log(s); // 'abc' — không đổi

// Phải tạo mới:
const s2 = 'z' + s.slice(1);  // 'zbc'

2.3. null vs undefined — khi nào cái nào

Hai giá trị "rỗng" của JS. Quy ước:

undefinednull
Ai sinhJS tự sinhDeveloper explicit gán
Khi nào xuất hiệnBiến khai báo chưa init, missing arg, missing property, return ngầmKhi muốn "explicit empty"
typeof'undefined''object' 🔥
undefined examples
let x;
console.log(x); // undefined (chưa init)

function foo(a, b) { return [a, b]; }
foo(1);          // [1, undefined] — missing arg

const obj = { a: 1 };
console.log(obj.b); // undefined — missing property

function noReturn() {}
noReturn();          // undefined — implicit return
null examples
// document.getElementById trả null nếu không tìm thấy
const el = document.getElementById('foo');
if (el === null) { /* xử lý */ }

// Explicit set: "tôi cố ý không có value"
let selectedUser = null;
// ... sau này:
selectedUser = { id: 1, name: 'An' };

Check cả 2 cùng lúc với == null (chấp nhận coercion cho 1 case duy nhất):

// x == null match cả null và undefined
if (x == null) { /* x là null hoặc undefined */ }

// Tương đương:
if (x === null || x === undefined) { }

3. Truthy / Falsy

Khi JS cần boolean (vd: if, !, &&, ||), nó coerce value qua Boolean(value). Có 6 giá trị falsy:

  1. false
  2. 0 (và -0, 0n BigInt)
  3. '' (empty string)
  4. null
  5. undefined
  6. NaN

Mọi giá trị khác đều truthy, kể cả những trường hợp dễ nhầm:

Truthy gotcha
Boolean([]);        // true 🔥 (array rỗng vẫn truthy)
Boolean({});        // true 🔥 (object rỗng vẫn truthy)
Boolean('0');       // true (string '0' truthy, khác number 0)
Boolean('false');   // true (string 'false' truthy)
Boolean(Infinity);  // true
Boolean(function(){});  // true (function là object)
🔥 Bẫy hay gặp
// Bug: muốn check array có phần tử
const arr = [];
if (arr) {
  console.log('có phần tử');  // chạy! vì [] truthy
}

// Đúng: check length
if (arr.length > 0) { }

4. Type Coercion — implicit conversion

Là nguồn chính của bug "weird JS". Có 3 ngữ cảnh implicit coerce:

  1. String context+ với string, template literal, String().
  2. Number context — toán tử số (-, *, /, %), so sánh (<, >).
  3. Boolean contextif, !, &&, ||, ternary.

4.1. Toán tử + — string hay number?

Quy tắc: nếu bất kỳ operand là string, kết quả là string (concat). Còn lại thì number.

Plus coercion
1 + 2;         // 3
1 + '2';       // '12' 🔥 (number → string, concat)
'1' + 2;       // '12'
'1' + '2';     // '12'

1 + true;      // 2  (true → 1)
1 + false;     // 1  (false → 0)
1 + null;      // 1  (null → 0)
1 + undefined; // NaN (undefined → NaN)

// Trick: convert sang number bằng `+`
+'42';          // 42 (number)
+'42px';        // NaN

4.2. Toán tử khác — luôn number

'5' - 3;     // 2  ('5' → 5)
'5' * '2';   // 10 (cả 2 → number)
'10' / 2;    // 5
'abc' - 1;   // NaN (abc không là number hợp lệ)

4.3. == vs === — Đẳng thức

=== (strict): so sánh value + type. Không coerce. == (loose): coerce trước khi so sánh. Rules phức tạp.

Equality comparison
1 === 1;      // true
1 === '1';    // false (type khác)
1 == '1';     // true ('1' → 1, sau đó 1 == 1)

0 == false;   // true (false → 0)
0 == '';      // true ('' → 0)
0 == '0';     // true

null == undefined;  // true (chỉ 2 cái này == nhau)
null === undefined; // false
null == 0;         // false 🔥 (null không coerce sang 0 trong ==)

Bảng coercion == đầy đủ:

undefinednullnumberstringboolean
undefinedtruetruefalsefalsefalse
nulltruetruefalsefalsefalse
numberfalsefalseso sánh valueconvert string → numberconvert boolean → number
stringfalsefalseconvert string → numberso sánh valuecả 2 → number
booleanfalsefalseconvert boolean → numbercả 2 → numberso sánh value
💡 Quy tắc thực dụng

Luôn dùng ===. Chỉ ngoại lệ: x == null để check đồng thời null và undefined (vẫn có người không thích, viết x === null || x === undefined cho rõ ràng).

ESLint rule eqeqeq sẽ catch == tự động.

5. Explicit coercion — convert tường minh

3 function global:

Explicit conversion
// String() — convert sang string
String(42);          // '42'
String(true);        // 'true'
String(null);        // 'null'
String(undefined);   // 'undefined'
String([1,2,3]);     // '1,2,3'
String({a:1});       // '[object Object]' 🔥

// Number() — convert sang number
Number('42');        // 42
Number('42.5');      // 42.5
Number('42px');      // NaN (không parse partial)
Number('');          // 0 🔥
Number(' ');         // 0 🔥 (whitespace → 0)
Number(null);        // 0
Number(undefined);   // NaN
Number([]);          // 0 🔥
Number([42]);        // 42 (array 1 phần tử)
Number([1,2]);       // NaN

// Boolean() — convert sang boolean (theo rule truthy/falsy)
Boolean(0);         // false
Boolean('');        // false
Boolean('0');       // true (string '0' truthy)

// parseInt / parseFloat — parse partial
parseInt('42px');     // 42 (parse số đầu, ignore phần còn lại)
parseFloat('3.14em'); // 3.14
parseInt('abc');      // NaN
💡 Trick ngắn
+x;          // = Number(x)
'' + x;     // = String(x)
!!x;         // = Boolean(x)

Ngắn gọn, nhưng hơi cryptic. Trong code production khuyên dùng full function name.

6. NaN — "Not a Number" nhưng là number

NaN là kết quả của phép toán không hợp lệ. Tính chất đặc biệt: không bằng chính nó.

NaN gotchas
NaN === NaN;    // false 🔥
NaN == NaN;     // false
typeof NaN;     // 'number' 🔥

// Khi NaN xuất hiện
0 / 0;            // NaN
Math.sqrt(-1);    // NaN
Number('abc');    // NaN
undefined + 1;    // NaN

// Check NaN — dùng Number.isNaN (chính xác hơn isNaN cũ)
Number.isNaN(NaN);          // true
Number.isNaN('abc');        // false 🔥 ('abc' không phải NaN, nó là string)
isNaN('abc');              // true ❌ (global isNaN coerce trước → bug)
🔥 isNaN vs Number.isNaN

Global isNaN(x) chạy Number(x) trước rồi check NaN — gây false positive. Number.isNaN(x) (ES6) check chính xác: chỉ true khi x thực sự là NaN.

Luôn dùng Number.isNaN.

7. Optional chaining ?. và Nullish coalescing ??

2 toán tử ES2020 cực kỳ hữu ích, tránh được rất nhiều bug.

7.1. ?. — Optional chaining

Truy cập property/method an toàn. Nếu giữa chừng có null/undefined, kết quả là undefined thay vì throw.

Optional chaining
const user = { name: 'An', address: { city: 'Hà Nội' } };
const user2 = { name: 'Bình' };

// Cũ (verbose):
const city = user2 && user2.address && user2.address.city;

// Mới:
const city2 = user2?.address?.city;  // undefined (không throw)

// Áp dụng cho method call
user.greet?.();          // nếu greet không tồn tại, không lỗi

// Áp dụng cho array index
const arr = null;
const first = arr?.[0];     // undefined

7.2. ?? — Nullish coalescing

Trả vế phải chỉ khi vế trái là null hoặc undefined. Khác || ở chỗ || còn match mọi falsy (0, '', false).

?? vs ||
// Default value pattern
const count = userInput ?? 0;
const name = userName ?? 'Anonymous';

// Khác || ở case 0, '', false:
0 || 5;       // 5  (0 falsy)
0 ?? 5;       // 0  (0 không phải null/undefined)

'' || 'x';   // 'x'
'' ?? 'x';   // ''

false || true; // true
false ?? true; // false
💡 Default value — chọn cái nào?

Khi 0, '', false là giá trị hợp lệ (vd: count = 0, prefix rỗng) — dùng ??. Khi muốn fallback cho tất cả falsy — dùng ||. Trong app modern, ?? phù hợp hơn 90% case.

Bài tập

Bài 1 — Đoán typeof

Không chạy, đoán kết quả của:

console.log(typeof null);
console.log(typeof undefined);
console.log(typeof NaN);
console.log(typeof []);
console.log(typeof function(){});
console.log(typeof Symbol());
console.log(typeof 42n);

Sau đó chạy thử và đối chiếu.

Đáp án
  • typeof null === 'object' 🔥 (bug lịch sử)
  • typeof undefined === 'undefined'
  • typeof NaN === 'number' 🔥 (NaN là number)
  • typeof [] === 'object' (array là object)
  • typeof function(){} === 'function' (special)
  • typeof Symbol() === 'symbol'
  • typeof 42n === 'bigint'

Bài 2 — isEmpty function

Viết function isEmpty(value) trả true nếu value là:

  • null hoặc undefined
  • String rỗng ''
  • Array rỗng []
  • Object rỗng {}

Test với ít nhất 10 case, bao gồm cả truthy (0, false) — không nên match.

Đáp án mẫu
function isEmpty(value) {
  if (value == null) return true;          // null hoặc undefined
  if (typeof value === 'string') return value === '';
  if (Array.isArray(value)) return value.length === 0;
  if (typeof value === 'object') {
    return Object.keys(value).length === 0;
  }
  return false;  // number, boolean, function: không "empty"
}

isEmpty(null);       // true
isEmpty(undefined);  // true
isEmpty('');         // true
isEmpty([]);         // true
isEmpty({});         // true
isEmpty(0);          // false
isEmpty(false);      // false
isEmpty('0');        // false
isEmpty([0]);        // false
isEmpty({a:1});      // false

Bài 3 — Strict integer check

Viết function isStrictInteger(x) trả true chỉ khi x là số nguyên thật sự (không phải string số, không phải float).

Test:

  • isStrictInteger(5) → true
  • isStrictInteger(5.0) → true (vì 5.0 === 5)
  • isStrictInteger(5.5) → false
  • isStrictInteger('5') → false
  • isStrictInteger(NaN) → false
Đáp án
function isStrictInteger(x) {
  return Number.isInteger(x);
}

// Number.isInteger là method ES6, kiểm tra:
// - type number
// - không phải NaN/Infinity
// - là integer (không phần thập phân)

Bài 4 — Loop và closure

Code dưới in ra 3 3 3 sau 1 giây. Sửa thành 0 1 2 bằng 2 cách độc lập.

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 1000);
}
2 cách fix

Cách 1: đổi varlet. let tạo binding mới mỗi iteration → closure capture đúng giá trị.

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 1000);
}

Cách 2: IIFE để tạo scope riêng cho từng iteration.

for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(() => console.log(j), 1000);
  })(i);
}

Chi tiết closure ở chương 3.

Bài 5 — safeJsonParse

Viết safeJsonParse(str) parse JSON, nếu lỗi trả null.

Thử với:

  • safeJsonParse('{"a": 1}'){ a: 1 }
  • safeJsonParse('invalid')null
  • safeJsonParse('null') → ?
  • safeJsonParse('undefined') → ?
Đáp án + giải thích
function safeJsonParse(str) {
  try {
    return JSON.parse(str);
  } catch {
    return null;
  }
}
  • 'null'null (JSON parse hợp lệ, kết quả null).
  • 'undefined'null (vì undefined không phải JSON hợp lệ → throw → catch → return null).

Lưu ý: trả null cho cả "parse fail" và "parse được null" — caller không phân biệt. Pattern Result type (chương 7) giải quyết.

Quiz

Q1

typeof null trả về gì?

Xem đáp án
✓ Đáp án

'object'. Đây là bug từ JavaScript 1995 không thể fix vì backward compat. Cách check null đúng: x === null.

Q2

[] == false trả về gì?

Xem đáp án
✓ Đáp án

true. Coercion: []''0; false0; cuối cùng 0 == 0. Đây là lý do nên dùng ===.

Q3

const obj = { a: 1 }; obj.a = 2; có lỗi không?

Xem đáp án
✓ Đáp án

Không. const chỉ chặn reassign biến (obj = ...), không chặn mutate property bên trong. Muốn freeze: Object.freeze(obj).

Q4

if (x) với x = [] — chạy nhánh nào?

Xem đáp án
✓ Đáp án

Nhánh true. [] là object, mọi object đều truthy. Để check array có phần tử: arr.length > 0.

Q5

NaN === NaN trả về gì? Vì sao?

Xem đáp án
✓ Đáp án

false. NaN là "không xác định" — IEEE 754 rule: NaN không bằng bất kỳ thứ gì, kể cả chính nó. Check NaN: Number.isNaN(x).

Q6

Khác nhau giữa x || 'default'x ?? 'default'?

Xem đáp án
✓ Đáp án

|| trả vế phải khi vế trái falsy (0, '', false, null, undefined, NaN). ?? trả vế phải chỉ khi null hoặc undefined.

Ví dụ: 0 || 5 === 5 nhưng 0 ?? 5 === 0. Khi 0/'' là giá trị hợp lệ, dùng ??.

Q7

Khi nào nên dùng == thay ===?

Xem đáp án
✓ Đáp án

Gần như không bao giờ. Một ngoại lệ hợp lý: x == null để check cả nullundefined cùng lúc (loose equality giữa null và undefined là true). Nhưng nhiều code base vẫn cấm tuyệt đối qua ESLint rule eqeqeq.

Tổng kết

Sau chương 2, bạn nên đã master:

  • 3 keyword khai báo: default const, fallback let, tránh var.
  • 7 primitive types + 1 Object. typeof null là gotcha.
  • null vs undefined: undefined là JS sinh, null là explicit.
  • Truthy/falsy: 6 falsy values, mọi thứ khác truthy (kể cả [], {}).
  • Type coercion: implicit +/==/if phức tạp; ưu tiên explicit Number()/String()/Boolean().
  • NaN là number, không bằng chính nó — check bằng Number.isNaN.
  • ?.??: tools modern để handle null an toàn.

Kết nối

  • Chương 3 (Functions) — đào sâu hoisting và TDZ qua function declaration.
  • Chương 4 (Objects) — phân biệt primitive vs object reference rõ ràng.
  • Chương 9 (TS Types) — type của TypeScript đối chiếu với type runtime JS.
  • Dart Chương 2 — đối chiếu sound null safety Dart vs strict mode TS.