Skip to content

Lesson 0.3.2 — Equality ​

Bài 0.3.2 — Equality
==, ===, Object.is & Coercion • 26 phút
0:00 / 0:00

0. Metadata ​

FieldValue
Stage0 — JavaScript Language Foundation
Module0.3 — Coercion & Equality
Lesson0.3.2
CompetencyC01.3 — Coercion & Equality
DepthL2–L3 (Explain → Use)
PrerequisitesLesson 0.3.1 — Truthy & Falsy
Cognitive LoadMedium-High

1. Why This Exists (Vì sao cần học) ​

Bạn viết:

js
function findUser(users, id) {
  return users.find(u => u.id == id);
}

const users = [{ id: 1, name: "A" }];
const input = "1"; // từ form input, luôn là string

console.log(findUser(users, input)); // { id: 1, name: "A" }

Code chạy. Nhưng bạn không hiểu tại sao "1" == 1 lại đúng. Và bạn cũng không biết khi nào điều đó sẽ bất ngờ gây lỗi.

Bạn viết tiếp:

js
if (value == null) {
  console.log("Missing");
}

value là undefined. Vào nhánh. Đúng. Nhưng tại sao null == undefined lại đúng? Và tại sao null === undefined lại sai?

Rồi bạn đọc trên blog: "Luôn dùng ===, không bao giờ dùng ==." Nhưng bạn thấy senior trong team vẫn viết == null. Họ biết điều gì mà bạn không biết?

Lesson này không dạy bạn một quy tắc đơn giản. Lesson này dạy bạn cơ chế để tự quyết định khi nào dùng gì.

2. Prerequisites (Yêu cầu đầu vào) ​

Trước khi học bài này, bạn cần:

  • Biết danh sách 8 giá trị falsy (Lesson 0.3.1).
  • Hiểu primitive types và typeof (Lesson 0.2.3, 0.2.5).
  • Phân biệt object và primitive (Lesson 0.2.1, 0.2.4).

3. Learning Objectives (Mục tiêu học tập) ​

Sau bài này, bạn có thể:

  1. Giải thích sự khác biệt giữa == (loose equality) và === (strict equality) bằng cơ chế, không phải bằng quy tắc.
  2. Dự đoán kết quả của ít nhất 6 cặp so sánh phổ biến có coercion.
  3. Giải thích tại sao Object.is tồn tại và khi nào dùng nó.
  4. Viết code chọn đúng giữa ==, ===, và Object.is trong ít nhất 4 scenario.
  5. Nhận diện bug từ việc nhầm lẫn == và === trong production.

4. Mental Model (Mô hình tư duy) ​

Mental Model — Ba Câu Hỏi So Sánh

JavaScript có 3 cách để hỏi "hai thứ này có giống nhau không?" — và mỗi cách hỏi một câu khác nhau.

a == b hỏi: "Nếu tôi cố gắng chuyển đổi chúng về cùng type, chúng có tương đương không?" → Cho phép coercion (chuyển đổi ngầm) trước khi so sánh.

a === b hỏi: "Chúng có cùng type VÀ cùng value không?" → Không coercion. Nếu type khác nhau, ngay lập tức false.

Object.is(a, b) hỏi: "Chúng có giống hệt nhau không, kể cả các trường hợp đặc biệt như NaN và -0?" → Giống === nhưng xử lý NaN và signed zero khác biệt.

text
a == b        →  "Equivalent after coercion?"
a === b       →  "Same type AND same value?"
Object.is(a,b) → "Exactly the same, including NaN and -0?"

Code Review Lens

Khi thấy == trong code review, đừng tự động yêu cầu đổi thành ===. Hãy hỏi: "Tác giả có đang cố ý dùng coercion để match cả null và undefined không?"

5. Core Concepts (Các khái niệm cốt lõi) ​

Essential (Bắt buộc) ​

Khái niệmÝ nghĩa
Loose equality (==)So sánh sau khi coerce cả hai về cùng type theo Abstract Equality Algorithm.
Strict equality (===)So sánh type trước. Type khác nhau → false. Không coercion.
Coercion trong ==Type khác nhau → engine chuyển đổi ngầm theo quy tắc spec.
Object.isSame-value equality. NaN === NaN (true). 0 !== -0 (false).
null == undefinedtrue. Đây là trường hợp == được dùng có chủ đích phổ biến nhất.

Supporting (Hỗ trợ) ​

  • Abstract Equality Algorithm (simplified): number ↔ string (string → number), boolean ↔ anything (boolean → number), object ↔ primitive (object → primitive).
  • NaN === NaN là false theo cả == và ===. Chỉ Object.is trả true.
  • 0 === -0 là true, nhưng Object.is(0, -0) là false.

Awareness (Biết tồn tại) ​

  • == với object sử dụng ToPrimitive (gọi valueOf rồi toString). Sẽ đào sâu ở Stage 2.
  • Object.is được dùng nội bộ bởi Map và Set để xác định key uniqueness.

Out of Scope (Không học trong bài này) ​

  • ToPrimitive algorithm chi tiết (valueOf, toString, Symbol.toPrimitive). Stage 2.
  • SameValueZero (dùng bởi Map/Set, coi -0 và 0 là bằng nhau). Stage 2.
  • Abstract Relational Comparison (<, > coercion). Sẽ đề cập ở các lesson operator.

6. Worked Example (Ví dụ phân tích từng bước) ​

Xét đoạn code sau:

js
console.log([] == false);
console.log("" == false);
console.log(0 == "");
console.log(null == undefined);
console.log(NaN === NaN);
console.log(Object.is(NaN, NaN));

Step 1 — [] == false

[] == false

Type khác: object vs boolean. Theo algorithm, boolean trước → chuyển false thành number 0.

[] == 0

Type khác: object vs number. Chuyển object → primitive. [] chuyển thành "" (empty string).

Note: Cơ chế chi tiết array → primitive sẽ học ở Stage 2 (Object Model).

"" == 0

Type khác: string vs number. Chuyển string → number. "" thành 0.

0 == 0

Cùng type, cùng value. Kết quả: true.

Step 2 — "" == false

"" == false

Boolean trước → false thành 0.

"" == 0

String → number. "" thành 0.

0 == 0

Kết quả: true.

Step 3 — 0 == ""

0 == ""

String → number. "" thành 0.

0 == 0

Kết quả: true.

Step 4 — null == undefined

null == undefined

Theo spec, null và undefined được coi là tương đương khi dùng ==. Không coercion thêm. Kết quả: true.

Step 5 — NaN === NaN

NaN === NaN

Cùng type (number), nhưng theo IEEE 754, NaN không bằng bất kỳ giá trị nào. Kết quả: false.

Step 6 — Object.is(NaN, NaN)

Object.is(NaN, NaN)

Object.is dùng SameValue algorithm, không dùng Strict Equality. NaN được coi là bằng NaN. Kết quả: true.

7. Prediction Exercise (Bài tập dự đoán) ​

Đừng chạy code. Dự đoán kết quả (true / false) và giải thích bằng cơ chế coercion hoặc strict comparison.

P1

js
console.log(0 == false);
console.log(0 === false);

P2

js
console.log("" == 0);
console.log("" === 0);

P3

js
console.log(null == undefined);
console.log(null === undefined);

P4

js
console.log("1" == 1);
console.log("1" === 1);

P5

js
console.log(NaN == NaN);
console.log(NaN === NaN);
console.log(Object.is(NaN, NaN));

P6

js
console.log(0 === -0);
console.log(Object.is(0, -0));

P7

js
console.log([1] == "1");
console.log([1, 2] == "1,2");
[Đáp án & Giải thích]

P1

  • 0 == false → true (boolean → number: false → 0, rồi 0 == 0).
  • 0 === false → false (khác type: number vs boolean).

P2

  • "" == 0 → true (string → number: "" → 0).
  • "" === 0 → false (khác type).

P3

  • null == undefined → true (spec-defined: null và undefined tương đương dưới ==).
  • null === undefined → false (khác type: null vs undefined).

P4

  • "1" == 1 → true (string → number: "1" → 1).
  • "1" === 1 → false (khác type).

P5

  • NaN == NaN → false (IEEE 754 rule).
  • NaN === NaN → false (cùng rule).
  • Object.is(NaN, NaN) → true (SameValue algorithm).

P6

  • 0 === -0 → true (strict equality coi 0 và -0 là bằng).
  • Object.is(0, -0) → false (SameValue phân biệt signed zero).

P7

  • [1] == "1" → true (array [1] chuyển thành string "1").
  • [1, 2] == "1,2" → true (array [1, 2] chuyển thành string "1,2").

8. Implementation Lab (Bài lab thực hành) ​

Lab A — Guided (Có hướng dẫn) ​

Viết hàm isNullOrUndefined dùng == để kiểm tra một giá trị có phải null hoặc undefined không. Giải thích tại sao == ở đây là an toàn và chủ đích.

js
function isNullOrUndefined(value) {
  // TODO: dùng == để match cả null và undefined
}
[Đáp án tham khảo]
js
function isNullOrUndefined(value) {
  return value == null;
}

console.log(isNullOrUndefined(null));      // true
console.log(isNullOrUndefined(undefined)); // true
console.log(isNullOrUndefined(0));         // false
console.log(isNullOrUndefined(""));        // false

Tại sao an toàn: == null chỉ match null và undefined. Không có giá trị nào khác bị coerce thành null. Đây là pattern được cộng đồng chấp nhận rộng rãi.

Lab B — Partial Scaffold (Khung sườn) ​

Hoàn thành hàm sameValue mô phỏng Object.is bằng ===, nhưng xử lý đúng NaN và -0:

js
function sameValue(a, b) {
  // TODO: nếu cả hai đều là NaN, return true
  // TODO: nếu a === b, nhưng một là 0 và một là -0, return false
  // TODO: nếu không, dùng ===
  // implement here
}
[Đáp án tham khảo]
js
function sameValue(a, b) {
  if (Number.isNaN(a) && Number.isNaN(b)) return true;
  if (a === 0 && b === 0 && 1 / a !== 1 / b) return false; // phân biệt 0 và -0
  return a === b;
}

Lab C — Independent (Tự làm) ​

Viết hàm safeEquals so sánh hai giá trị theo logic:

  • Nếu cả hai đều là null hoặc undefined (kể cả mixed), trả về true.
  • Nếu một là null/undefined và một không, trả về false.
  • Nếu cả hai đều không phải null/undefined, dùng ===.
js
console.log(safeEquals(null, undefined)); // true
console.log(safeEquals(null, 0));         // false
console.log(safeEquals("1", 1));          // false
console.log(safeEquals(NaN, NaN));        // false (vì dùng ===)
[Đáp án tham khảo]
js
function safeEquals(a, b) {
  if (a == null && b == null) return true;
  return a === b;
}

9. Edge Cases (Các trường hợp ngoại lệ) ​

[] == false nhưng [] !== false

js
[] == false   // true
[] === false  // false

== coerce [] → "" → 0, và false → 0. === không coerce, thấy object vs boolean, trả false ngay. Đây là lý do tại sao == gây bất ngờ.

"" == false nhưng "" !== false

js
"" == false   // true
"" === false  // false

Empty string và false đều falsy, nhưng khác type. == coi chúng tương đương. === thì không.

null và undefined chỉ tương đương với nhau

js
null == undefined  // true
null == 0          // false
undefined == 0     // false
null == ""         // false

null và undefined là cặp đặc biệt trong spec. Chúng không tương đương với 0, "", hoặc false.

NaN không bằng bất kỳ thứ gì

js
NaN == NaN   // false
NaN === NaN  // false
NaN == 0     // false
NaN == null  // false

Chỉ Object.is(NaN, NaN) là true. Đây là lý do tại sao indexOf không tìm được NaN trong array.

10. Debug Lab (Bài lab gỡ lỗi) ​

Symptom (Triệu chứng): Form nhập giá tiền hiển thị "Miễn phí" khi user nhập 0, nhưng cũng hiển thị "Miễn phí" khi user xóa trắng input.

Reproduction (Tái hiện lỗi):

js
function formatPrice(input) {
  if (input == 0) {
    return "Miễn phí";
  }
  return `$${input}`;
}

console.log(formatPrice(0));      // "Miễn phí"
console.log(formatPrice("0"));    // "Miễn phí"
console.log(formatPrice(""));     // "Miễn phí"  ← BUG: không phải 0
console.log(formatPrice(100));    // "$100"

Evidence (Bằng chứng):

  • "" == 0 là true (coercion).
  • User xóa trắng input (empty string) bị coi là giá 0.

Hypothesis (Giả thuyết): Developer dùng == 0 để check "giá là 0". Nhưng == coerce "" thành 0, nên empty string cũng match.

Verification (Xác minh):

js
console.log("" == 0);  // true
console.log("" === 0); // false

Root Cause (Nguyên nhân gốc rễ):== cho phép coercion giữa string và number. Empty string trong ngữ cảnh so sánh số tương đương 0. Để check giá trị số chính xác, cần đảm bảo cả type và value.

Fix (Sửa):

js
function formatPrice(input) {
  if (input === 0) {
    return "Miễn phí";
  }
  return `$${input}`;
}

Nếu input từ form là string, parse trước khi so sánh:

js
function formatPrice(input) {
  const num = Number(input);
  if (Number.isNaN(num)) return "Không hợp lệ";
  if (num === 0) return "Miễn phí";
  return `$${num}`;
}

Prevention (Phòng ngừa):

  • Khi so sánh với số, dùng === sau khi đã parse đúng type.
  • Không dùng == để check "giá trị số" từ input string.

11. Design Exercise (Bài tập thiết kế giải pháp) ​

Context

Bạn đang review một file utility. Có 3 hàm so sánh sau. Hãy đánh giá từng hàm: nên giữ nguyên, nên sửa, hay nên thay đổi hoàn toàn? Giải thích bằng cơ chế == / === / Object.is.

js
// Hàm 1: Kiểm tra field có tồn tại không
function isMissing(value) {
  return value == null;
}

// Hàm 2: Kiểm tra hai ID có giống nhau không
function sameId(a, b) {
  return a == b;
}

// Hàm 3: Kiểm tra giá trị có phải zero không
function isZero(value) {
  return value == 0;
}
[Đáp án tham khảo]
HàmĐánh giáLý do
isMissingGiữ nguyên== null là pattern chủ đích để match cả null và undefined. Không có giá trị khác bị nhầm.
sameIdSửa thành ===== sẽ coerce "1" thành 1, hoặc true thành 1. ID nên so sánh chặt chẽ về type và value.
isZeroSửa thành === và thêm type check"" == 0 là true, false == 0 là true. Nếu muốn check số zero, dùng typeof value === "number" && value === 0.

Quy tắc thực tế:

  • == null để check null/undefined: OK, phổ biến, intentional.
  • == cho so sánh chung: Tránh. Dùng === thay thế.
  • Object.is: Chỉ khi bạn cần xử lý NaN hoặc signed zero đặc biệt.

12. Production Scenario (Tình huống thực tế) ​

Một API trả về dữ liệu user:

json
{ "age": null }

Hoặc:

json
{ "age": 25 }

Hoặc:

json
{ }  // field bị xóa — không có key "age"

Frontend code:

js
function canDrink(user) {
  if (user.age == 18) {
    return "Vừa đủ tuổi";
  }
  if (user.age > 18) {
    return "Đủ tuổi";
  }
  return "Không đủ tuổi";
}

Câu hỏi:

  1. user.age là null thì sao? null > 18 cho kết quả gì?
  2. user.age là undefined thì sao? undefined == 18 cho kết quả gì?
  3. Viết lại hàm để xử lý đúng cả 3 trường hợp (null, undefined, number hợp lệ).
[Đáp án tham khảo]
  1. null > 18 → false (null coerce thành 0, 0 > 18 là false). Nhưng message trả về là "Không đủ tuổi" — đúng về mặt logic nhưng gây confusion vì null không phải tuổi 0.
  2. undefined == 18 → false. undefined > 18 → false (undefined coerce thành NaN, so sánh với NaN luôn false).
  3. Fix:
js
function canDrink(user) {
  if (user.age == null) {  // cả null và undefined
    return "Chưa có thông tin tuổi";
  }
  if (typeof user.age !== "number" || Number.isNaN(user.age)) {
    return "Tuổi không hợp lệ";
  }
  if (user.age >= 18) {
    return "Đủ tuổi";
  }
  return "Không đủ tuổi";
}

13. AI-assisted Exercise (Bài tập với AI) ​

Level B — Challenge (Thách thức AI)

  1. Tự trả lời trước: Viết ra giấy: tại sao == null được nhiều senior chấp nhận nhưng == 0 thì bị coi là nguy hiểm? Dùng từ "coercion" và "closed set".
  2. Hỏi AI: "Should I always use === in JavaScript? Is == ever acceptable?"
  3. So sánh: AI có nhắc đến == null như một ngoại lệ hợp lệ không? Hay AI chỉ nói "always use ===, never use =="?
  4. Verify: Đọc MDN page "Equality comparisons and sameness". Xem documentation có nhắc đến Object.is và các use case của nó không?

Gợi ý

Hãy để ý xem AI có phân biệt được giữa "general advice" và "intentional exception" không. Nhiều AI sẽ đưa ra lời khuyên quá đơn giản ("always use ===") mà không giải thích tại sao == null là an toàn (vì null và undefined là closed pair trong spec, không có giá trị nào khác bị coerce thành null).

Đáp án tham khảo
  • Bạn nghĩ

    • == null an toàn vì spec quy định chỉ có null và undefined tương đương dưới == với nhau. Không có giá trị nào khác bị nhầm.
    • == 0 nguy hiểm vì nhiều giá trị khác nhau (empty string, false, array rỗng trong một số context) có thể bị coerce thành 0.
    • === là default tốt, nhưng không phải absolute rule.
  • AI trả lời

    • AI thường nói: "Best practice is to always use === and !== to avoid unexpected type coercion. Only use == when you explicitly want type coercion."
    • AI có thể thêm: "Some developers use == null as a shorthand for checking both null and undefined."
    • Tuy nhiên, AI hiếm khi giải thích tại sao == null là an toàn ở mức spec. AI thường trình bày như một "style preference" thay vì "technical guarantee".
  • So sánh

    • AI đúng về general advice.
    • AI thiếu: cơ chế spec đằng sau sự an toàn của == null. Nó không phải "preference" — nó là guarantee từ Abstract Equality Algorithm.
  • Điểm AI nói sai hoặc quá mơ hồ

    • AI thường nói "avoid unexpected coercion" mà không liệt kê expected coercion nào. Coercion không phải lúc nào cũng "unexpected" — nó là behavior deterministic.
    • AI có thể gợi ý dùng === kể cả khi so sánh với null, dẫn đến code dài dòng: value === null || value === undefined.
  • Kết luận

    • Nếu bạn chỉ ra được rằng == null là intentional, spec-backed exception chứ không phải "lười biếng", bạn đã hiểu sâu hơn AI.
    • Concept này sẽ quay lại ở Stage 6 (TypeScript: == null vẫn hợp lệ trong TS strict mode) và Stage 8 (React: so sánh props).

14. Teach Back (Dạy lại) ​

Giải thích cho một đồng nghiệp junior trong 2 phút, không dùng từ "nó thế":

"Tại sao [] == false là true nhưng [] === false là false? Và tại sao tôi nên hiểu cơ chế này thay vì chỉ nhớ 'luôn dùng ==='?"

Yêu cầu:

  • Dùng đúng từ: coercion, type, Abstract Equality Algorithm, strict equality.
  • Giải thích từng bước coercion trong [] == false.
Mô phỏng
  • Bạn nói
    • "== và === hỏi hai câu khác nhau. == hỏi: 'Nếu tôi chuyển đổi chúng về cùng type, chúng có giống nhau không?' Còn === hỏi: 'Chúng có cùng type VÀ cùng value không?'"
    • "Với [] == false: type khác nhau — object và boolean. Theo Abstract Equality Algorithm, boolean được chuyển thành number trước. false thành 0."
    • "Giờ ta có [] == 0. Object được chuyển thành primitive. Array rỗng [] chuyển thành empty string ''."
    • "Giờ ta có '' == 0. String và number — string được chuyển thành number. '' thành 0."
    • "Cuối cùng 0 == 0 → true. Còn [] === false thấy object vs boolean, khác type, trả false ngay lập tức."
    • "Vì vậy, tôi không nhớ 'luôn dùng ===' như một quy tắc mù quáng. Tôi hiểu == có coercion, và tôi chỉ dùng == khi tôi chủ đích muốn coercion — ví dụ == null để bắt cả null và undefined."

💡 Liên tưởng: == giống như một thông dịch viên tự động dịch hai người nói ngôn ngữ khác nhau trước khi so sánh ý kiến. === giống như yêu cầu họ phải nói cùng một ngôn ngữ. Thông dịch viên đôi khi dịch sai hoặc dịch theo cách bạn không ngờ — nên mặc định bạn muốn mọi người nói cùng ngôn ngữ. Nhưng đôi khi bạn cần thông dịch viên, ví dụ khi biết chắc chỉ có hai cách nói duy nhất cho cùng một ý (null và undefined).

Gợi ý đánh giá bản thân

  • Đồng nghiệp có hỏi "vậy tại sao không sửa == để [] == false là false?" không? Nếu có, bạn đã truyền đạt đủ rõ để họ tò mò về spec backward compatibility.
  • Bạn có thể tự trace "" == false theo cùng algorithm không?

15. Assessment (Đánh giá) ​

Objective (Mục tiêu)Hình thức đánh giá
Giải thích == vs === bằng cơ chếExplanation (Giải thích)
Dự đoán coercion trong so sánhPrediction (Dự đoán)
Giải thích Object.isExplanation (Giải thích)
Chọn == / === / Object.is đúngDesign Decision (Quyết định thiết kế)
Debug bug từ == coercionDebug Lab (Gỡ lỗi)

16. Exit Criteria (Tiêu chí qua bài) ​

  • [ ] Có thể giải thích sự khác biệt giữa == (coercion allowed) và === (no coercion) bằng cơ chế spec.
  • [ ] Có thể dự đoán đúng 6/6 scenarios trong Prediction Exercise.
  • [ ] Có thể viết == null đúng context và giải thích tại sao nó an toàn.
  • [ ] Có thể debug trường hợp == coerce empty string thành 0 gây lỗi form.
  • [ ] Có thể chọn Object.is khi cần xử lý NaN hoặc signed zero.
  • [ ] Không còn nói "luôn dùng ===" như một quy tắc tuyệt đối mà không hiểu ngoại lệ.

17. Spiral Connection (Liên kết xoắn ốc) ​

Previous (Trước): Lesson 0.3.1 — Truthy & Falsy. Bạn đã biết boolean coercion và danh sách 8 falsy.

Current (Hiện tại): Lesson 0.3.2 — Equality. Bạn hiểu == (loose, coercion), === (strict, no coercion), và Object.is (SameValue). Bạn biết == null là intentional pattern.

Next (Tiếp theo):

  • Lesson 0.3.3 — Type Conversion (explicit conversion: Number(), String(), Boolean()).
  • Lesson 0.4.3 — Short-circuit Evaluation (&&, ||, ?? và sự khác biệt giữa falsy và nullish).
  • Stage 2 — Object Model: ToPrimitive algorithm chi tiết (dùng trong == object coercion).
  • Stage 6 — TypeScript: Type narrowing với === và type guards.
  • Stage 8 — React: React.memo so sánh props bằng Object.is (shallow equality).
📴 Offline Mode — Content served from cache