Cách Duyệt Dữ Liệu Linh Hoạt Trong JavaScript - Iterables & Iterators

Bạn có gặp tình huống này không: duyệt qua một mảng hoặc object phức tạp, nhưng code trở nên rối rắm với việc quản lý index và điều kiện dừng? Hay bạn muốn tùy chỉnh cách duyệt dữ liệu cho đối tượng riêng nhưng không biết bắt đầu từ đâu?

Từ ES6, IterablesIterators ra đời giúp bạn giải quyết vấn đề này - viết mã ngắn gọn, linh hoạt hơn với vòng lặp for...of tự nhiên. Hiểu rõ khái niệm này không chỉ giúp bạn duyệt dữ liệu dễ dàng mà còn tùy biến cách lặp cho đối tượng riêng, tối ưu hóa mã và giảm thiểu lỗi trong dự án.

#1. Iterables là gì?

#1.1. Khái niệm

Iterable là đối tượng có thể trả về một Iterator, tức là bạn có thể duyệt qua các phần tử của nó bằng cơ chế lặp. Một đối tượng được coi là Iterable nếu nó có Symbol.iterator, đây là “chìa khóa” cho phép for...of và các kỹ thuật lặp khác hoạt động.

#1.2. Ví dụ

1
2
const nameList = ["Heo", "Lung"];
console.log(nameList[Symbol.iterator]);

Kết quả:

1
ƒ values() { [native code] }

Giải thích:
Mảng nameList có sẵn thuộc tính [Symbol.iterator], nhờ vậy bạn có thể dùng for...of duyệt qua các phần tử một cách dễ dàng.

#1.3. Sử dụng for…of với mảng

1
2
3
4
5
6
const fruits = ['Apple', 'Banana', 'Orange'];

// Duyệt mảng với for...of
for (const fruit of fruits) {
  console.log(fruit);
}

Kết quả:

1
2
3
Apple
Banana
Orange

Giải thích:
Vòng lặp for...of tự động gọi fruits[Symbol.iterator]() và lấy từng giá trị thông qua next(). Bạn không cần quản lý index hay điều kiện dừng - mọi thứ diễn ra tự động.

So sánh với for truyền thống:

1
2
3
4
5
6
7
8
9
// ❌ Cách cũ: Phải quản lý index
for (let i = 0; i < fruits.length; i++) {
  console.log(fruits[i]);
}

// ✅ Cách mới: Ngắn gọn, dễ đọc hơn
for (const fruit of fruits) {
  console.log(fruit);
}

Trong thực tế: for...of không chỉ hoạt động với mảng mà còn với Map, Set, String, NodeList (từ DOM), và bất kỳ đối tượng nào có Symbol.iterator. Điều này giúp bạn viết code nhất quán cho nhiều loại dữ liệu khác nhau.

#2. Iterators là gì?

#2.1. Khái niệm

Iterator là đối tượng được trả về bởi thuộc tính [Symbol.iterator] của một Iterable. Nó chứa một phương thức next() trả về một đối tượng gồm hai thuộc tính:

  • value: Giá trị kế tiếp trong chuỗi phần tử.
  • done: Biến Boolean cho biết đã duyệt xong toàn bộ phần tử chưa.

Cách for…of hoạt động với Iterator:

View Mermaid diagram code
flowchart TD
    Start([for...of bắt đầu]) --> GetIterator["Gọi obj[Symbol.iterator]()"]
    GetIterator --> CallNext["Gọi iterator.next()"]
    CallNext --> CheckDone{done === true?}
    CheckDone -->|Không| UseValue[Sử dụng value]
    UseValue --> CallNext
    CheckDone -->|Có| End([Kết thúc vòng lặp])

#2.2. Ví dụ tạo Iterator cho một đối tượng

1
2
3
4
5
6
7
8
9
10
11
const pets = ["Heo", "Lung"];
const iterator = pets[Symbol.iterator]();

console.log(iterator.next());
// Kết quả: { value: "Heo", done: false }

console.log(iterator.next());
// Kết quả: { value: "Lung", done: false }

console.log(iterator.next());
// Kết quả: { value: undefined, done: true }

Giải thích:
Mỗi lần gọi next(), bạn lấy được phần tử kế tiếp. Khi không còn phần tử nào, done sẽ là true.

Trong thực tế: Bạn hiếm khi gọi next() thủ công như vậy - vòng lặp for...of tự động làm việc này cho bạn. Tuy nhiên, hiểu cách Iterator hoạt động giúp bạn debug hiệu quả khi gặp lỗi với custom iterables hoặc khi cần kiểm soát từng bước duyệt.

#3. Tùy biến Iterable cho đối tượng

#3.1. Sử dụng for...of với đối tượng không hỗ trợ Iterable

Khi bạn dùng for...of với một đối tượng thường (Object), bạn sẽ gặp lỗi vì Object không hỗ trợ Iterable mặc định.

1
2
3
4
5
const cat = { name: 'Heo' };

for (const value of cat) {
  console.log(value);
}

Kết quả:

1
TypeError: cat is not iterable

Cách khắc phục: Bạn cần tự định nghĩa [Symbol.iterator] cho đối tượng.

#3.2. Tạo Iterable cho đối tượng

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const myPets = {
  petsList: [
    { name: 'Heo', age: '5 years old' },
    { name: 'Lung', age: '1 year old' }
  ],
  [Symbol.iterator]() {
    const list = this.petsList;
    const total = list.length;
    let index = 0;

    return {
      next() {
        if (index < total) {
          return { value: list[index++], done: false };
        }
        return { done: true };
      }
    };
  }
};

for (const value of myPets) {
  console.log(value);
}

Kết quả:

1
2
{ name: 'Heo', age: '5 years old' }
{ name: 'Lung', age: '1 year old' }

Giải thích:
Bằng cách tự định nghĩa [Symbol.iterator], bạn đã biến đối tượng myPets thành Iterable, cho phép for...of hoạt động mượt mà.

Trong thực tế: Kỹ thuật này rất hữu ích khi bạn làm việc với API trả về dữ liệu dạng object chứa danh sách bên trong (ví dụ: {items: [...], total: 10}). Thay vì luôn phải truy cập data.items, bạn có thể duyệt trực tiếp qua object data với for...of.

#3.3. Lỗi thường gặp và cách khắc phục

#Lặp vô hạn (Infinite loop)

Nếu bạn quên cập nhật biến đếm hoặc không có điểm dừng, bạn sẽ rơi vào vòng lặp vô hạn:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ❌ SAI: Quên tăng index
const badIterator = {
  data: [1, 2, 3],
  [Symbol.iterator]() {
    const list = this.data;
    let index = 0;
    return {
      next() {
        // Quên index++ - lặp vô hạn!
        return { value: list[index], done: false };
      }
    };
  }
};

// Browser sẽ bị treo khi chạy:
// for (const item of badIterator) {
//   console.log(item); // In mãi số 1
// }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ✅ ĐÚNG: Nhớ tăng index và trả về done: true
const goodIterator = {
  data: [1, 2, 3],
  [Symbol.iterator]() {
    const list = this.data;
    const total = list.length;
    let index = 0;
    return {
      next() {
        if (index < total) {
          return { value: list[index++], done: false };
        }
        return { done: true };
      }
    };
  }
};

for (const item of goodIterator) {
  console.log(item); // In 1, 2, 3 rồi dừng
}

Trong thực tế: Luôn đảm bảo có điều kiện dừng rõ ràng (done: true) và cập nhật biến đếm (index++). Nếu browser bị treo khi test, đây thường là nguyên nhân. Hãy kiểm tra DevTools để force stop script nếu cần.

#4. Áp dụng thực tế trong dự án

Khi bạn làm việc với dữ liệu phức tạp (ví dụ: gọi API, dữ liệu từ file JSON), tùy biến Iterable giúp bạn duyệt dữ liệu theo cách riêng, dễ đọc và dễ kiểm soát.

Ví dụ, bạn có thể kết hợp [...myPets] (spread operator) để chuyển đối tượng Iterable thành mảng, sau đó dùng filter() hay map() dễ dàng:

1
2
const filteredPets = [...myPets].filter(pet => pet.name === 'Heo');
console.log(filteredPets);

Kết quả:

1
[{ name: 'Heo', age: '5 years old' }]

Điều này giúp bạn linh hoạt hơn, không chỉ phụ thuộc vào mảng, Set hay Map. Bất kỳ đối tượng nào cũng có thể trở thành Iterable, tối ưu hóa luồng dữ liệu trong ứng dụng phức tạp.

Vậy là bạn đã nắm vững Iterables và Iterators - từ khái niệm đến ứng dụng thực tế. Trong thực tế, khi làm việc với dữ liệu phức tạp từ API hoặc các cấu trúc tùy chỉnh, hãy tận dụng [Symbol.iterator] để duyệt dữ liệu linh hoạt và tối ưu hơn. Áp dụng ngay vào dự án của bạn để trải nghiệm sức mạnh của tính năng ES6 này nhé!