Lesson 0.4.6 — for...of vs for...in
0. Metadata (Thông tin bài học)
| Field | Value |
|---|---|
| Stage | 0 — JavaScript Language Foundation |
| Module | 0.4 — Operators & Control Flow |
| Lesson | 0.4.6 — for...of vs for...in |
| Competency | C01.4 — Control Flow |
| Depth | L3 (Use) |
| Prerequisites | 0.4.5 Loops, 0.2.4 Object Values, 0.6.2 Arrays |
| Estimated Cognitive Load | Medium |
1. Why This Exists (Vì sao cần học)
Bạn viết đoạn code sau để in các số trong mảng:
const scores = [85, 90, 78];
for (const key in scores) {
console.log(key);
}
// Output: "0", "1", "2" ❌Bạn mong đợi 85, 90, 78, nhưng nhận được "0", "1", "2". Tại sao?
Vì for...in không lặp qua giá trị. Nó lặp qua tên thuộc tính (property keys) của object. Với mảng, các key là "0", "1", "2".
Đây là một trong những bug phổ biến nhất của Junior JavaScript — dùng for...in trên mảng. Lesson này dạy bạn:
for...inlàm gì và tại sao nó không phù hợp với mảng.for...oflàm gì và tại sao nó phù hợp với mảng/string.- Khi nào
for...inthực sự hữu ích (object properties).
Preview
Bạn sẽ học: for...in lặp qua tên thuộc tính (keys), kể cả trên prototype. for...of lặp qua giá trị (values), chỉ hoạt động với iterable. Array là iterable, object thường không phải.
2. Prerequisites (Yêu cầu đầu vào)
Trước khi học lesson này, bạn phải:
- [ ] Viết được
forloop cơ bản (0.4.5). - [ ] Biết mảng là object có index là key (0.2.4, 0.6.2).
- [ ] Hiểu
Object.keystrả về gì (0.6.6). - [ ] Biết object có properties (0.6.5).
Nếu thiếu một trong các kỹ năng trên, quay lại lesson tương ứng trước.
3. Learning Objectives (Mục tiêu học tập)
Sau lesson này, bạn có thể:
- Phân biệt
for...in(keys/properties) vàfor...of(values). - Dự đoán output của cả hai với array, string, và object.
- Giải thích tại sao
for...intrên array là anti-pattern. - Chọn đúng loại vòng lặp cho từng kiểu dữ liệu.
- Nhận diện bug do nhầm lẫn
for...invớifor...of.
4. Mental Model (Mô hình tư duy)
Hãy hình dung một chiếc tủ đựng đồ:
for...in : "Tôi muốn biết tên từng ngăn kéo."
→ "topDrawer", "middleDrawer", "bottomDrawer"
for...of : "Tôi muốn lấy đồ từng ngăn kéo."
→ "socks", "shirts", "pants"Với mảng:
Array: ["apple", "banana", "cherry"]
for...in → "0", "1", "2" (tên ngăn — string keys)
for...of → "apple", "banana", "cherry" (đồ trong ngăn — values)Điểm then chốt
for...in lặp qua tất cả enumerable properties — không chỉ array elements. Nếu ai đó thêm Array.prototype.customMethod = ..., for...in sẽ duyệt qua cả "customMethod". Đây là lý do chính không dùng for...in trên array.
5. Core Concepts (Các khái niệm cốt lõi)
Essential (Bắt buộc)
| Khái niệm | Ý nghĩa | Dùng cho |
|---|---|---|
for...in | Lặp qua enumerable property keys (string) | Object properties |
for...of | Lặp qua iterable values | Array, String, Map, Set, v.v. |
| Enumerable | Property có thể được liệt kê bằng for...in | Quyết định for...in có "thấy" không |
| Iterable | Object có thể lặp qua từng giá trị | Quyết định for...of có hoạt động không |
Supporting (Hỗ trợ)
Object.hasOwn/hasOwnProperty: kiểm tra property thuộc object hay prototype (dùng vớifor...in).Object.keys(),Object.values(),Object.entries(): alternatives để lặp object.
Awareness (Biết tồn tại)
- Iterator Protocol (Stage 2): cơ chế bên dưới khiến
for...ofhoạt động. - Prototype chain (Stage 2): lý do
for...incó thể duyệt qua properties không mong muốn.
Out of Scope (Không học trong bài này)
- Custom iterators (Stage 2).
Symbol.iterator(Stage 2).- Prototype chain đào sâu (Stage 2).
- Generators (Stage 2).
6. Worked Example (Ví dụ phân tích từng bước)
Ví dụ 1: Array
const fruits = ["apple", "banana", "cherry"];Bước 1 — for...in:
for (const key in fruits) {
console.log(key, typeof key);
}
// "0" string
// "1" string
// "2" stringkey là string — "0", "1", "2". Không phải số. Nếu bạn cần index số, phải parse.
Bước 2 — for...of:
for (const fruit of fruits) {
console.log(fruit);
}
// "apple"
// "banana"
// "cherry"fruit là giá trị tại mỗi vị trí. Không cần dùng fruits[i].
Bước 3 — Khi nào cần cả index lẫn value?
for (const [index, fruit] of fruits.entries()) {
console.log(index, fruit);
}
// 0 "apple"
// 1 "banana"Hoặc dùng for thông thường nếu không cần destructuring.
Ví dụ 2: Object
const user = { name: "Alice", age: 30 };Bước 1 — for...in (phù hợp):
for (const key in user) {
console.log(key, user[key]);
}
// "name" "Alice"
// "age" 30Bước 2 — for...of (KHÔNG hoạt động):
for (const item of user) {
console.log(item);
}
// TypeError: user is not iterable ❌Object thông thường không iterable. Muốn dùng for...of, phải chuyển qua Object.entries().
for (const [key, value] of Object.entries(user)) {
console.log(key, value);
}
// "name" "Alice"
// "age" 30Ví dụ 3: String
const word = "hi";
for (const char of word) {
console.log(char);
}
// "h"
// "i"String là iterable — for...of duyệt qua từng ký tự.
Code Review Lens
Khi review code, nếu thấy for...in trên array, đây là red flag. Hỏi: "Bạn muốn key hay value? Nếu value, dùng for...of."
7. Prediction Exercise (Bài tập dự đoán)
WARNING
Đừng chạy code. Đoán output và giải thích.
Câu 1
const arr = [10, 20];
arr.foo = "extra";
for (const x in arr) {
console.log(x);
}Dự đoán của bạn:
- In ra những gì?
"foo"có xuất hiện không? Tại sao?
Câu 2
const arr = [10, 20];
arr.foo = "extra";
for (const x of arr) {
console.log(x);
}Dự đoán của bạn:
- In ra những gì?
"extra"có xuất hiện không? Tại sao?
Câu 3
const obj = { a: 1, b: 2 };
for (const x of obj) {
console.log(x);
}Dự đoán của bạn:
- Output?
- Tại sao?
Câu 4
const str = "AB";
for (const ch in str) {
console.log(ch);
}Dự đoán của bạn:
- Output?
chlà string hay number?
[Đáp án & Giải thích]
Câu 1:
- Output:
"0","1","foo" "foo"xuất hiện vìfor...inlặp qua tất cả enumerable properties, kể cả properties do ngườì dùng thêm vào mảng (array là object).
Câu 2:
- Output:
10,20 "extra"không xuất hiện.for...oflặp qua iterable values theo index số (0,1). Nó không "thấy"foovìfookhông phải array element.
Câu 3:
- Output:
TypeError: obj is not iterable - Object thông thường không implement iterable protocol.
for...ofkhông hoạt động trên plain object.
Câu 4:
- Output:
"0","1" chlà string. String là object-like, nênfor...induyệt qua index properties"0","1".
8. Implementation Lab (Bài lab thực hành)
Level 1 — Guided (Hướng dẫn)
Dùng for...of để tính tổng các số trong mảng:
const numbers = [1, 2, 3, 4, 5];
let sum = 0;
// Dùng for...of
for (/* implement */) {
/* implement */
}
console.log(sum); // 15[Gợi ý]
for (const num of numbers) {
sum += num;
}Level 2 — Partial Scaffold (Khung mẫu)
Hoàn thành hàm countProperties. Sử dụng for...in để đếm số lượng own properties của object, không tính các properties được kế thừa từ prototype.
function countProperties(obj) {
let count = 0;
// Dùng for...in + hasOwnProperty
for (/* implement */) {
if (/* implement */) {
count++;
}
}
return count;
}
// Test
console.log(countProperties({ a: 1, b: 2 })); // 2Đáp án tham khảo
function countProperties(obj) {
let count = 0;
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
count++;
}
}
return count;
}Giải thích
for...in duyệt qua các enumerable properties của object, bao gồm:
- properties thuộc trực tiếp
obj; - properties được kế thừa từ
prototype.
Vì bài yêu cầu chỉ đếm own properties, cần kiểm tra:
obj.hasOwnProperty(key);Method này trả về true khi key thuộc trực tiếp obj, và false khi key được kế thừa từ prototype.
Ví dụ:
const parent = {
age: 20
};
const obj = Object.create(parent);
obj.name = "An";
for (const key in obj) {
console.log(key);
}
// name
// ageỞ đây:
obj.hasOwnProperty("name"); // true
obj.hasOwnProperty("age"); // falseDo đó chỉ name được tính vào kết quả.
⚠️ Edge case của obj.hasOwnProperty()
Trong các object thông thường, cách viết:
obj.hasOwnProperty(key);hoạt động tốt và dễ đọc.
Tuy nhiên, không nên mặc định rằng mọi object đều có thể gọi hasOwnProperty() an toàn.
Có hai edge case quan trọng.
Edge case 1 — Object ghi đè hasOwnProperty
Object có thể có một property tên hasOwnProperty:
const obj = {
name: "An",
hasOwnProperty: "hello"
};
obj.hasOwnProperty("name");
// ❌ TypeError: obj.hasOwnProperty is not a functionLý do:
obj.hasOwnPropertykhông còn trỏ tới method được kế thừa từ Object.prototype.
Nó đã bị property "hello" của chính obj ghi đè.
Edge case 2 — Object không có Object.prototype
Object được tạo bằng Object.create(null) sẽ không có prototype:
const obj = Object.create(null);
obj.name = "An";
obj.hasOwnProperty("name");
// ❌ TypeErrorObject này không kế thừa hasOwnProperty() từ Object.prototype.
💡 Cách an toàn hơn
Có hai cách phổ biến để kiểm tra own property mà không phụ thuộc vào obj.hasOwnProperty().
1. Object.prototype.hasOwnProperty.call()
Object.prototype.hasOwnProperty.call(obj, key);Thay vì gọi:
obj.hasOwnProperty(key);ta lấy trực tiếp method từ Object.prototype rồi dùng call() để truyền obj vào.
Cách này xử lý được cả trường hợp object ghi đè hasOwnProperty:
const obj = {
name: "An",
hasOwnProperty: "hello"
};
Object.prototype.hasOwnProperty.call(obj, "name");
// trueVà cả object được tạo bằng Object.create(null):
const obj = Object.create(null);
obj.name = "An";
Object.prototype.hasOwnProperty.call(obj, "name");
// trueCách này đặc biệt hữu ích trong code cũ hoặc khi cần tương thích với các môi trường JavaScript cũ.
2. Object.hasOwn()
Từ ES2022, JavaScript cung cấp API:
Object.hasOwn(obj, key);Ví dụ:
const obj = {
name: "An"
};
Object.hasOwn(obj, "name");
// trueAPI này không phụ thuộc vào obj.hasOwnProperty, nên cũng xử lý được cả hai edge case:
const obj = {
name: "An",
hasOwnProperty: "hello"
};
Object.hasOwn(obj, "name");
// trueconst obj = Object.create(null);
obj.name = "An";
Object.hasOwn(obj, "name");
// trueVì vậy, trong code hiện đại, Object.hasOwn() thường là lựa chọn nên ưu tiên.
So sánh
| Cách | Object ghi đè hasOwnProperty | Object.create(null) | Khuyến nghị |
|---|---|---|---|
obj.hasOwnProperty(key) | ❌ | ❌ | Đơn giản, phù hợp bài học |
Object.prototype.hasOwnProperty.call(obj, key) | ✅ | ✅ | An toàn, tương thích rộng |
Object.hasOwn(obj, key) | ✅ | ✅ | Ưu tiên trong code mới |
Trong phạm vi bài học
Mục tiêu của bài này là làm quen với:
for...in
↓
duyệt enumerable properties
↓
hasOwnProperty()
↓
lọc chỉ own propertiesVì vậy, đáp án tham khảo vẫn dùng:
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
count++;
}
}Nhưng khi viết code thực tế, nên ưu tiên:
for (const key in obj) {
if (Object.hasOwn(obj, key)) {
count++;
}
}Ghi nhớ:
for...incó thể duyệt cả properties được kế thừa từprototype.hasOwnProperty()vàObject.hasOwn()giúp lọc ra chỉ những properties thuộc trực tiếp object.
Level 3 — Independent (Độc lập)
Viết hàm sumNumericValues. Nhận một object, dùng for...in để tính tổng tất cả own properties có giá trị là number. Bỏ qua properties không phải number.
function sumNumericValues(obj) {
// Implement here — dùng for...in + hasOwn + typeof
}
// Test
console.log(sumNumericValues({ a: 10, b: "hello", c: 20, d: true }));
// Expected: 30
console.log(sumNumericValues({ x: 5, y: 5 }));
// Expected: 10[Đáp án tham khảo]
function sumNumericValues(obj) {
let sum = 0;
for (const key in obj) {
if (Object.hasOwn(obj, key) && typeof obj[key] === "number") {
sum += obj[key];
}
}
return sum;
}9. Edge Cases (Các trường hợp ngoại lệ)
Edge 1: for...in trên array với prototype pollution
Array.prototype.bad = "pollution";
const arr = [1, 2];
for (const key in arr) {
console.log(key);
}
// "0", "1", "bad" ❌Tại sao fail: for...in duyệt qua tất cả enumerable properties, kể cả trên prototype. Đây là lý do chính for...in không dùng cho array.
Fix: Dùng for...of, for thông thường, hoặc array methods.
Edge 2: for...in trên string
for (const idx in "hi") {
console.log(idx, typeof idx);
}
// "0" string
// "1" stringCaveat: for...in trên string trả về index dạng string, không phải ký tự. Nếu cần ký tự, dùng for...of.
Edge 3: Sparse array
const arr = [1, , 3]; // index 1 là "empty"
for (const val of arr) {
console.log(val);
}
// 1
// undefined
// 3Caveat: for...of duyệt qua cả "empty slots" và trả về undefined. for...in thì bỏ qua empty slots (vì không có property tại index đó).
Edge 4: for...of trên null / undefined
for (const x of null) { }
// TypeError: null is not iterableTại sao fail: null và undefined không phải iterable. Phải guard trước khi dùng for...of.
10. Debug Lab (Bài lab gỡ lỗi)
Symptom (Triệu chứng): Hàm sumScores trả về kết quả sai khi mảng có method tùy chỉnh.
Reproduction (Tái hiện lỗi):
function sumScores(scores) {
let total = 0;
for (const item in scores) {
total += scores[item];
}
return total;
}
// Trong một file khác, library thêm vào Array.prototype
Array.prototype.last = function () { return this[this.length - 1]; };
console.log(sumScores([10, 20, 30]));
// Actual: "60function () { return this[this.length - 1]; }" (string)
// Expected: 60Evidence:
scores["last"]là function object. Khi cộng vàototal(number, lúc này đã là60), JavaScript chuyển function thành string ("function ()..."), rồi thực hiện string concatenation:"60" + "function ()..."→ kết quả là string kỳ lạ. Đây là coercion từ Module 0.3 quay lại.
Hypothesis (Giả thuyết): Dùng for...in trên array là anti-pattern. Phải đổi thành for...of.
Verification (Xác minh):
function sumScores(scores) {
let total = 0;
for (const score of scores) {
total += score;
}
return total;
}
console.log(sumScores([10, 20, 30]));
// → 60 ✅Root Cause (Nguyên nhân gốc rễ):for...in lặp qua enumerable properties của object. Array là object, nên for...in duyệt qua cả properties trên prototype chain. for...of chỉ lặp qua iterable values theo index số, không bị ảnh hưởng bởi prototype.
Fix (Sửa lỗi):
- Không bao giờ dùng
for...inđể lặp array. - Dùng
for...of,forthông thường, hoặc array methods (reduce,forEach).
Prevention (Phòng ngừa):
- Code review: cấm
for...intrên array. - Lint rule: ESLint
no-for-in-array. - Nếu buộc phải dùng
for...intrên object, luôn kiểm traObject.hasOwn.
11. Design Exercise (Bài tập thiết kế giải pháp)
INFO
Ở depth L3, bài tập thiết kế tập trung vào quyết định đơn giản giữa các lựa chọn.
Context (Bối cảnh): Bạn cần viết hàm printReport(data).
Constraints (Ràng buộc):
- Nếu
datalà array: in từng giá trị. - Nếu
datalà object: in từng cặpkey: value.
Question:
- Với array, bạn chọn
for...inhayfor...of? Giải thích. - Với object, bạn chọn
for...inhayfor...of? Giải thích. - Viết code cho cả hai nhánh.
[Đáp án tham khảo]
- Array →
for...of(lấy values, không bị prototype pollution). - Object →
for...in(lấy keys), hoặcObject.entries()+for...of. - Code:js
function printReport(data) { if (Array.isArray(data)) { for (const item of data) { console.log(item); } } else { for (const key in data) { if (Object.hasOwn(data, key)) { console.log(`${key}: ${data[key]}`); } } } }
12. Production Scenario (Tình huống thực tế)
Bối cảnh: Bạn review PR của đồng nghiệp. Họ viết:
function formatItems(items) {
const result = [];
for (const i in items) {
result.push(`Item ${i}: ${items[i]}`);
}
return result;
}Vấn đề:
itemslà array.ilà string"0","1"— không phải number.- Nếu một library nào đó thêm vào
Array.prototype, hàm này sẽ include cả method đó.
Câu hỏi:
- Tại sao
ilà string gây vấn đề? - Bạn refactor như thế nào?
- Nếu
itemscó thể là object hoặc array, bạn xử lý thế nào?
[Đáp án tham khảo]
ilà string nên"Item " + inối chuỗi — vẫn đúng, nhưng nếu cần tính toán số học (ví dụi + 1),"0" + 1 = "01"thay vì1.- Refactor:jsHoặc dùng
function formatItems(items) { const result = []; for (let i = 0; i < items.length; i++) { result.push(`Item ${i}: ${items[i]}`); } return result; }for...ofnếu không cần index. - Dùng
Array.isArray()để phân nhánh, hoặc dùngObject.entries()cho cả hai.
13. AI-assisted Exercise (Bài tập với AI)
Level B — Challenge (Thử thách)
- Tự viết trước: Viết một đoạn giải thích ngắn (3–5 câu) về tại sao không nên dùng
for...intrên array. Tập trung vào prototype và type của key. - Đưa cho AI review: Dùng prompt: "Review my explanation of why for...in should not be used on arrays in JavaScript. Is it accurate? What's missing?"
- So sánh: AI có nhắc đến enumerable properties không? AI có cảnh báo về string keys không?
- Verify: Kiểm tra lại bằng MDN — search "for...in".
Gợi ý
AI thường nói: "Use for...of for arrays, for...in for objects." Điều này đúng nhưng là rule-of-thumb. Hãy kiểm tra xem AI có giải thích tại sao for...in duyệt qua prototype không. AI cũng có thể không nhắc đến việc key là string, không phải number.
[Đáp án tham khảo]
Bạn nghĩ:
for...inlặp qua tất cả enumerable properties của object. Array là object, nên nó cũng lặp qua properties trên prototype chain. Nếu ai đó thêmArray.prototype.something, vòng lặp sẽ duyệt qua cả"something". Hơn nữa, key trả về là string"0","1"— không phải number. Nếu cần index số để tính toán, phải parse, dễ gây bug.AI trả lờì: "
for...initerates over all enumerable properties, including those on the prototype chain. It's better to usefor...offor arrays because it only iterates over values." — Đúng nhưng thiếu depth.So sánh: AI đúng về prototype, nhưng có thể không nhắc đến:
- String keys: AI hiếm khi nhấn mạnh
keylà string, không phải number. - Sparse array behavior: AI có thể không nhắc đến việc
for...inbỏ qua empty slots cònfor...ofthì không. - Performance: AI có thể không nhắc đến
for...inchậm hơn vì phải kiểm tra prototype chain.
- String keys: AI hiếm khi nhấn mạnh
Điểm AI nói sai hoặc quá mơ hồ:
- "Better to use" là subjective. Không giải thích mechanism (enumerable properties + prototype chain).
- AI có thể không nhắc đến
Object.hasOwnnhư cách an toàn nếu buộc phải dùngfor...in.
Kết luận: Nếu bạn chỉ ra được rằng
for...intrên array có 3 vấn đề (prototype pollution, string keys, sparse array inconsistency) — bạn đã hiểu sâu hơn AI. Điều này sẽ quay lại ở Stage 2 (Prototype chain), Stage 6 (TypeScript — iterating over object types), và Stage 8 (React — iterating over lists).
14. Teach Back (Dạy lại)
Yêu cầu: Giải thích cho một đồng nghiệp Junior trong 2 phút:
"Tại sao tôi không nên dùng
for...inđể lặp qua mảng? Và khi nàofor...inthực sự hữu ích?"
Dùng đúng terminology: enumerable properties, prototype chain, iterable, keys vs values.
Mô phỏng
- Bạn nói:
for...inlặp qua tên của mọi thuộc tính enumerable — không chỉ array elements. Nếu ai đó thêm method vàoArray.prototype,for...insẽ duyệt qua cả method đó. Key trả về cũng là string"0","1"— không phải number. Với mảng, dùngfor...ofđể lấy giá trị, hoặcforthông thường để lấy index số.for...inchỉ hữu ích khi bạn muốn duyệt qua properties của object thuần — ví dụ object config hoặc dictionary.
💡 Hãy tưởng tượng
for...innhư việc lục tung cả nhà kho để tìm đồ — bạn tìm thấy cả thứ không mong muốn.for...ofnhư việc lấy đồ từng ngăn theo thứ tự — chỉ lấy đúng thứ bạn cần.
Gợi ý đánh giá bản thân
- Đồng nghiệp có hiểu tại sao
for...intrên array có thể duyệt qua prototype properties không? - Bạn có thể dự đoán output của
for...invàfor...ofvới sparse array không? - Bạn có thể giải thích tại sao
for...ofkhông hoạt động trên plain object không?
15. Assessment (Đánh giá)
| Objective (Mục tiêu) | Hình thức đánh giá | Tiêu chí |
|---|---|---|
Phân biệt for...in vs for...of | Classification (Phân loại) | Chọn đúng ≥ 4/5 scenarios |
| Dự đoán output | Prediction (Dự đoán) | Đúng ≥ 8/10 cases |
| Giải thích anti-pattern | Explain (Giải thích) | Nhắc đến prototype và string keys |
| Chọn đúng vòng lặp | Decision (Quyết định) | Chọn đúng cho array/object/string |
| Nhận diện bug | Debug (Gỡ lỗi) | Tìm ra for...in trên array trong 2/2 cases |
16. Exit Criteria (Tiêu chí qua bài)
- [ ] Có thể phân biệt
for...in(keys/properties) vàfor...of(values) trong 4/5 tình huống. - [ ] Có thể dự đoán đúng output của cả hai với array, string, và object.
- [ ] Có thể giải thích tại sao
for...intrên array là anti-pattern — nhắc đến prototype chain và string keys. - [ ] Có thể chọn đúng loại vòng lặp cho array, string, và plain object.
- [ ] Có thể nhận diện và sửa bug do dùng
for...intrên array trong code thực tế.
17. Spiral Connection (Liên kết xoắn ốc)
Previous (Trước): Loops (0.4.5) → Object Values (0.2.4) → Arrays (0.6.2)
Current (Hiện tại):
for...ofvsfor...in— phân biệt lặp qua keys (enumerable properties) và values (iterable protocol).Next (Tiếp theo): Array Iteration methods (0.6.3) → Object Utilities (0.6.6) → Iterator Protocol & Prototype Chain (Stage 2) → React list rendering (Stage 8) → TypeScript narrowing with
inoperator (Stage 6)