Cách sử dụng đối tượng Iterables trong JavaScript

Javascript căn bản | by Học Javascript

Trong JavaScript, Iterables là một khái niệm quan trọng giúp chúng ta duyệt qua các tập hợp dữ liệu một cách dễ dàng. Các đối tượng như Array, String, Set, Map đều hỗ trợ cơ chế Iterable, cho phép sử dụng các vòng lặp như for...of, spread operator (...), hoặc chuyển đổi thành mảng bằng Array.from().

Việc hiểu cách hoạt động của Iterables không chỉ giúp lập trình viên làm việc hiệu quả hơn với dữ liệu mà còn giúp tận dụng tối đa các tính năng mạnh mẽ của JavaScript. Trong bài viết này, chúng ta sẽ tìm hiểu về khái niệm Iterables, cách sử dụng chúng trong JavaScript, cũng như cách tạo Iterable tùy chỉnh để xử lý dữ liệu theo nhu cầu riêng.

Đối tượng Iterables trong JavaScript

Trong JavaScript, Iterable là một đối tượng có thể được duyệt (iterate) qua từng phần tử bằng cách sử dụng các cơ chế như for...of, spread operator (...), hoặc Array.from(). Một đối tượng được coi là Iterable khi nó tuân theo Iterable Protocol, nghĩa là nó có một phương thức đặc biệt Symbol.iterator.

Khi một đối tượng có phương thức Symbol.iterator(), phương thức này sẽ trả về một Iterator, giúp lặp qua các phần tử của đối tượng từng bước một.

Ví dụ về một Iterable trong JavaScript:

const array = [1, 2, 3]; // Mảng là một Iterable

for (let item of array) {
    console.log(item);
}
// Output:
// 1
// 2
// 3

Các đối tượng nào là Iterable?

Trong JavaScript, các đối tượng mặc định hỗ trợ Iterable bao gồm:

Array (Mảng)

  • Mảng là kiểu dữ liệu phổ biến nhất hỗ trợ Iterable.
const arr = [10, 20, 30];
for (let num of arr) {
    console.log(num);
}

String (Chuỗi ký tự)

  • Chuỗi là Iterable, cho phép duyệt qua từng ký tự.
const str = "Hello";
for (let char of str) {
    console.log(char);
}

Set (Tập hợp, không chứa phần tử trùng lặp)

  • Set là Iterable và cho phép duyệt qua từng phần tử duy nhất.
const mySet = new Set([1, 2, 3]);
for (let value of mySet) {
    console.log(value);
}

Map (Cấu trúc lưu trữ cặp key-value, có thứ tự)

  • Map là Iterable, cho phép duyệt qua các cặp key-value.
const myMap = new Map([
    ["name", "John"],
    ["age", 30]
]);
for (let [key, value] of myMap) {
    console.log(`${key}: ${value}`);
}

TypedArray (Mảng kiểu dữ liệu cố định)

  • TypedArray như Int8Array, Uint8Array,... cũng là Iterable.

Arguments Object

  • Đối tượng arguments trong một hàm có thể duyệt bằng for...of.

NodeList (Danh sách các phần tử DOM)

  • NodeList (kết quả của document.querySelectorAll) là Iterable.
const nodeList = document.querySelectorAll("p");
for (let node of nodeList) {
    console.log(node.textContent);
}

Lưu ý: Object thông thường (plain object) không phải là Iterable trừ khi ta tự định nghĩa phương thức Symbol.iterator().

Khi nào nên sử dụng Iterables?

Bạn nên sử dụng Iterables trong các trường hợp sau:

Khi muốn duyệt qua một tập hợp dữ liệu một cách tự nhiên

  • Ví dụ: Lặp qua mảng, Set, Map bằng for...of thay vì forEach() hoặc for.

Khi cần chuyển đổi dữ liệu thành mảng

  • Array.from(iterable) giúp dễ dàng chuyển đổi NodeList, arguments, hoặc Set thành Array để sử dụng các phương thức mảng.
const mySet = new Set([1, 2, 3]);
const array = Array.from(mySet);
console.log(array); // [1, 2, 3]

Khi làm việc với dữ liệu động hoặc bất đồng bộ

  • Các Iterable có thể giúp duyệt qua dữ liệu theo nhu cầu mà không cần tải toàn bộ dữ liệu ngay lập tức (Lazy Iteration).

Khi sử dụng Spread Operator (...)

  • Dùng để sao chép hoặc hợp nhất các Iterable.
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5]; 
console.log(arr2); // [1, 2, 3, 4, 5]

Khi làm việc với Generators

  • Generator là một dạng Iterable giúp tạo dữ liệu từng phần thay vì tải toàn bộ vào bộ nhớ.

Cách hoạt động của Iterables trong JavaScript

Giải thích về giao thức Iterable (Iterable Protocol)

Trong JavaScript, giao thức Iterable (Iterable Protocol) là một quy tắc cho phép một đối tượng được lặp qua từng phần tử của nó một cách tuần tự. Một đối tượng được coi là Iterable nếu nó có phương thức Symbol.iterator.

Giao thức Iterable hoạt động như sau:

  • Một đối tượng có thể được duyệt qua bằng vòng lặp for...of, spread operator (...), Array.from(), v.v.
  • Khi vòng lặp hoặc phương thức yêu cầu một Iterable, nó sẽ tìm phương thức Symbol.iterator() trên đối tượng đó.
  • Phương thức Symbol.iterator() phải trả về một Iterator (một đối tượng có phương thức next() để lấy phần tử tiếp theo).
  • Mỗi lần gọi next(), Iterator trả về một object có hai thuộc tính:
    • value: Giá trị của phần tử hiện tại.
    • done: true nếu đã duyệt hết phần tử, false nếu vẫn còn phần tử tiếp theo.

Ví dụ một Iterator thủ công:

const customIterable = {
    data: [1, 2, 3],
    [Symbol.iterator]() {
        let index = 0;
        return {
            next: () => {
                if (index < this.data.length) {
                    return { value: this.data[index++], done: false };
                } else {
                    return { done: true };
                }
            }
        };
    }
};

const iterator = customIterable[Symbol.iterator]();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { done: true }

Giải thích:

  • Đối tượng customIterable có phương thức Symbol.iterator(), trả về một Iterator.
  • Mỗi lần gọi next(), nó trả về một giá trị và trạng thái done.
  • Khi hết dữ liệu, done chuyển thành true, báo hiệu kết thúc vòng lặp.

Phương thức Symbol.iterator và cách hoạt động

Symbol.iterator là một thuộc tính đặc biệt trong JavaScript, dùng để định nghĩa cách một đối tượng sẽ được lặp qua.
Khi một đối tượng có Symbol.iterator, nó sẽ trở thành một Iterable, có thể sử dụng với for...of, Array.from(), và các kỹ thuật lặp khác.

Ví dụ với mảng (Array), vốn có sẵn Symbol.iterator:

const arr = [10, 20, 30];
const iterator = arr[Symbol.iterator]();

console.log(iterator.next()); // { value: 10, done: false }
console.log(iterator.next()); // { value: 20, done: false }
console.log(iterator.next()); // { value: 30, done: false }
console.log(iterator.next()); // { done: true }

JavaScript tự động sử dụng Symbol.iterator() khi ta dùng các cơ chế duyệt dữ liệu.

Cách JavaScript sử dụng Iterables trong vòng lặp for...of và các phương thức khác

Vòng lặp for...of

Vòng lặp for...of tự động gọi Symbol.iterator() và duyệt qua từng phần tử của Iterable.

Ví dụ với một Set:

const mySet = new Set(["apple", "banana", "cherry"]);

for (let fruit of mySet) {
    console.log(fruit);
}

Output:

apple
banana
cherry

Lưu ý:

  • for...of chỉ hoạt động với các Iterable (Array, String, Set, Map, v.v.).
  • Không thể dùng for...of với một Object thông thường (trừ khi có Symbol.iterator).

spread operator (...)

Toán tử ... có thể mở rộng một Iterable thành các phần tử riêng lẻ.
Ví dụ với Set:

const mySet = new Set([1, 2, 3]);
const arr = [...mySet];  
console.log(arr); // [1, 2, 3]

Array.from()

Phương thức Array.from(iterable) chuyển đổi một Iterable thành mảng.
Ví dụ với NodeList:

const nodeList = document.querySelectorAll("p");
const elementsArray = Array.from(nodeList);
console.log(elementsArray); // Chuyển thành mảng để dùng các phương thức như map(), filter()

Promise.all()Promise.race()

JavaScript cũng hỗ trợ Iterable trong các tác vụ bất đồng bộ.
Ví dụ: Chạy nhiều promise cùng lúc.

const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);

Promise.all([promise1, promise2, promise3]).then(console.log); // [1, 2, 3]
Promise.all() nhận một Iterable (mảng) gồm các Promise.

Các đối tượng có thể lặp (Built-in Iterables) trong JavaScript

Trong JavaScript, một số đối tượng có sẵn được thiết kế theo giao thức Iterable và có thể sử dụng với vòng lặp for...of hoặc các phương thức lặp khác như spread operator (...), Array.from(). Dưới đây là các loại Built-in Iterables phổ biến:

Mảng (Array)

Mảng (Array) là một trong những đối tượng phổ biến nhất hỗ trợ lặp (Iterable). Khi sử dụng for...of, JavaScript tự động gọi Symbol.iterator() để lặp qua từng phần tử.

Ví dụ: Lặp qua mảng bằng for...of

const fruits = ["apple", "banana", "cherry"];

for (let fruit of fruits) {
    console.log(fruit);
}

Output:

apple
banana
cherry

Lưu ý:

  • for...of lặp qua giá trị của từng phần tử trong mảng.
  • Nếu muốn lấy chỉ số (index), bạn có thể sử dụng forEach() hoặc for...in.

So sánh với forEach()

fruits.forEach((fruit, index) => {
    console.log(index, fruit);
});

Output:

0 apple
1 banana
2 cherry

forEach() cung cấp cả chỉ số và giá trị, còn for...of chỉ lấy giá trị.

Chuỗi (String)

Chuỗi (String) cũng là một Iterable, cho phép lặp qua từng ký tự.

Ví dụ: Lặp qua từng ký tự của chuỗi

const word = "Hello";

for (let char of word) {
    console.log(char);
}

Output:

H
e
l
l
o

Lưu ý:

Chuỗi cũng hỗ trợ spread operator để chuyển thành mảng ký tự:

console.log([...word]); // ["H", "e", "l", "l", "o"]

Các ký tự Unicode đặc biệt (như emoji) vẫn hoạt động bình thường với for...of.

Set: Lặp qua các phần tử không trùng lặp

Set là một Iterable chứa các giá trị duy nhất (không trùng lặp).

Ví dụ: Lặp qua Set bằng for...of

const mySet = new Set(["apple", "banana", "apple", "cherry"]);

for (let fruit of mySet) {
    console.log(fruit);
}

Output:

apple
banana
cherry

Lưu ý:

  • Set loại bỏ phần tử trùng lặp, vì vậy "apple" chỉ xuất hiện một lần.
  • Có thể sử dụng spread operator để chuyển Set thành mảng:
console.log([...mySet]); // ["apple", "banana", "cherry"]

Map: Lặp qua các cặp key-value

Map là một Iterable chứa các cặp key-value theo thứ tự chèn vào.

Ví dụ: Lặp qua Map bằng for...of

const myMap = new Map([
    ["name", "Alice"],
    ["age", 25],
    ["city", "New York"]
]);

for (let [key, value] of myMap) {
    console.log(`${key}: ${value}`);
}
Output:
name: Alice
age: 25
city: New York

Lưu ý:

  • Map duy trì thứ tự chèn vào, khác với Object, vốn không đảm bảo thứ tự key.
  • Nếu chỉ muốn lặp qua keys hoặc values, có thể dùng keys()values():
for (let key of myMap.keys()) {
    console.log(key);
}
for (let value of myMap.values()) {
    console.log(value);
}

TypedArray: Duyệt qua mảng kiểu dữ liệu cố định

TypedArray là một loại mảng đặc biệt trong JavaScript, chứa các giá trị số nguyên hoặc dấu chấm động với độ dài cố định (ví dụ: Int8Array, Uint16Array, Float32Array).

Ví dụ: Lặp qua TypedArray bằng for...of

const typedArray = new Uint8Array([10, 20, 30]);

for (let num of typedArray) {
    console.log(num);
}

Output:

10
20
30

Lưu ý:

  • TypedArray thường dùng trong xử lý đồ họa, WebGL, và dữ liệu nhị phân.
  • Hỗ trợ tất cả các phương thức giống như mảng (forEach(), map(), v.v.).

Arguments Object: Lặp qua danh sách tham số của hàm

Trong JavaScript, arguments là một đối tượng giống mảng chứa tất cả các tham số truyền vào một hàm. Nó không phải là một Array, nhưng Iterable.

Ví dụ: Lặp qua arguments bằng for...of

function sum() {
    let total = 0;
    for (let num of arguments) {
        total += num;
    }
    return total;
}

console.log(sum(1, 2, 3, 4)); // 10

Lưu ý:

  • arguments không có các phương thức của Array (map(), filter(), v.v.).
  • Để chuyển thành mảng, dùng Array.from(arguments).

Trong ES6, arguments thường được thay thế bằng Rest Parameters (...args):

function sum(...args) {
    return args.reduce((total, num) => total + num, 0);
}

Tạo đối tượng Iterable tùy chỉnh trong JavaScript

Mặc định, một số đối tượng trong JavaScript như Array, String, Set, và Map đã là Iterables vì chúng triển khai giao thức Iterable (Iterable Protocol). Tuy nhiên, chúng ta cũng có thể tự tạo một đối tượng Iterable tùy chỉnh bằng cách triển khai phương thức Symbol.iterator().

Cách tạo một đối tượng Iterable bằng Symbol.iterator()

Để một đối tượng trở thành Iterable, nó phải có một phương thức đặc biệt:

obj[Symbol.iterator] = function() {
    return {
        next() {
            // Trả về { value, done }
        }
    };
};

Cách hoạt động:

  • Đối tượng có một phương thức Symbol.iterator(), trả về một Iterator.
  • Iterator này có phương thức next() để trả về một cặp { value, done }:
    • value: Giá trị hiện tại của phần tử.
    • done: true nếu vòng lặp kết thúc, false nếu còn phần tử.

Ví dụ về Iterable tùy chỉnh

Ví dụ 1: Tạo một Iterable sinh dãy số từ 1 đến n

const range = {
    from: 1,
    to: 5,
    
    [Symbol.iterator]() {
        let current = this.from;
        let last = this.to;

        return {
            next() {
                if (current <= last) {
                    return { value: current++, done: false };
                } else {
                    return { done: true };
                }
            }
        };
    }
};

// Sử dụng vòng lặp for...of để lặp qua Iterable
for (let num of range) {
    console.log(num);
}
Output:
1
2
3
4
5

Cách hoạt động:

  1. Đối tượng range có thuộc tính fromto, xác định phạm vi lặp.
  2. Khi for...of gọi range[Symbol.iterator](), nó trả về một Iterator có phương thức next().
  3. next() tăng dần giá trị từ from đến to, và dừng lại khi vượt quá to.

Ứng dụng thực tế của Iterable tùy chỉnh

Ví dụ 2: Tạo Iterable sinh số Fibonacci

Dãy Fibonacci là một chuỗi số trong đó mỗi số là tổng của hai số trước đó. Ta có thể tạo một Iterable tùy chỉnh sinh ra các số Fibonacci:

const fibonacci = {
    [Symbol.iterator]() {
        let a = 0, b = 1;
        return {
            next() {
                let temp = a;
                a = b;
                b = temp + b;
                return { value: temp, done: false };
            }
        };
    }
};

// Giới hạn chỉ lấy 10 số đầu tiên
let count = 0;
for (let num of fibonacci) {
    if (count++ >= 10) break;
    console.log(num);
}
Output:
0
1
1
2
3
5
8
13
21
34

Cách hoạt động:

  • Mỗi lần gọi next(), thuật toán cập nhật ab theo quy tắc Fibonacci.
  • Vòng lặp sẽ tiếp tục chạy vô hạn nếu không có điều kiện dừng (break).

Sử dụng Iterable tùy chỉnh trong các tình huống thực tế

Lặp qua một tập dữ liệu động

Trong thực tế, ta có thể sử dụng Iterable để lặp qua các tập dữ liệu động, ví dụ danh sách đơn hàng, danh sách người dùng trong database, hoặc API dữ liệu phân trang.

const orders = {
    data: ["Order #1", "Order #2", "Order #3"],
    
    [Symbol.iterator]() {
        let index = 0;
        return {
            next: () => index < this.data.length 
                ? { value: this.data[index++], done: false } 
                : { done: true }
        };
    }
};

for (let order of orders) {
    console.log(order);
}

Output:

Order #1
Order #2
Order #3

Ứng dụng: Có thể sử dụng với dữ liệu từ API, file JSON, hoặc danh sách dữ liệu bất kỳ.

Phân trang dữ liệu (Pagination)

Giả sử ta có một API trả về dữ liệu phân trang, ta có thể tạo một Iterable giúp lấy từng trang dữ liệu mà không cần gọi API thủ công nhiều lần.

const paginatedAPI = {
    currentPage: 1,
    totalPages: 3,

    [Symbol.iterator]() {
        return {
            next: () => {
                if (this.currentPage > this.totalPages) {
                    return { done: true };
                } else {
                    return { value: `Fetching page ${this.currentPage++}`, done: false };
                }
            }
        };
    }
};

// Lấy dữ liệu từng trang từ API giả định
for (let page of paginatedAPI) {
    console.log(page);
}

Output:

Fetching page 1
Fetching page 2
Fetching page 3

Ứng dụng: Có thể sử dụng để tải danh sách sản phẩm, bài viết, hoặc tin nhắn trong các ứng dụng web.

Kết hợp Iterable tùy chỉnh với for...of, Array.from(), Spread Operator

Dùng Array.from() để tạo mảng từ Iterable

const numbers = Array.from(range);
console.log(numbers); // [1, 2, 3, 4, 5]

Sử dụng Spread Operator ...

console.log([...range]); // [1, 2, 3, 4, 5]

Cả hai cách trên giúp ta dễ dàng chuyển Iterable thành mảng để sử dụng các phương thức như map(), filter(), reduce().

Sử dụng Iterables với Spread Operator và Destructuring

Sử dụng ... (Spread Operator) với Iterables

Trong JavaScript, spread operator (...) có thể được dùng để sao chép hoặc trích xuất giá trị từ các đối tượng Iterable, bao gồm:

  • Mảng (Array)
  • Chuỗi (String)
  • Set
  • Map
  • Iterable tùy chỉnh

Ví dụ 1: Sao chép và kết hợp mảng với Spread Operator

const numbers = [1, 2, 3];
const moreNumbers = [...numbers, 4, 5, 6];

console.log(moreNumbers); // [1, 2, 3, 4, 5, 6]

Giải thích: Spread Operator (...numbers) mở rộng mảng numbers và thêm các phần tử mới (4, 5, 6).

Ví dụ 2: Spread Operator với chuỗi

const str = "hello";
const letters = [...str];

console.log(letters); // ['h', 'e', 'l', 'l', 'o']

Giải thích: Spread Operator chia chuỗi thành từng ký tự riêng lẻ trong mảng.

Ví dụ 3: Spread Operator với Set (loại bỏ phần tử trùng lặp)

const set = new Set([1, 2, 3, 3, 4]);
const arrayFromSet = [...set];

console.log(arrayFromSet); // [1, 2, 3, 4]

Ứng dụng: Loại bỏ phần tử trùng lặp trong mảng bằng cách chuyển Set → Array.

Dùng Destructuring để trích xuất giá trị từ Iterable

Destructuring Assignment giúp trích xuất giá trị từ các đối tượng Iterable một cách nhanh chóng.

Ví dụ 4: Destructuring mảng

const numbers = [10, 20, 30, 40];
const [first, second, ...rest] = numbers;

console.log(first);  // 10
console.log(second); // 20
console.log(rest);   // [30, 40]

Giải thích:

  • first = 10, second = 20.
  • ...rest lấy phần còn lại của mảng.

Ví dụ 5: Destructuring với chuỗi

const [first, second, ...remaining] = "JavaScript";

console.log(first);     // 'J'
console.log(second);    // 'a'
console.log(remaining); // ['v', 'a', 'S', 'c', 'r', 'i', 'p', 't']

Ứng dụng: Có thể dùng để trích xuất dữ liệu từ chuỗi hoặc mảng động.

Sử dụng Iterables với for...of, Map(), và Set() trong JavaScript

Cách sử dụng for...of với Iterables

for...of giúp lặp qua từng phần tử của một đối tượng Iterable dễ dàng.

Ví dụ 6: Lặp qua mảng với for...of

const numbers = [10, 20, 30];

for (let num of numbers) {
    console.log(num);
}

Output:

10
20
30

Ưu điểm: Dễ đọc hơn forEach() khi làm việc với Iterables.

Ví dụ 7: Lặp qua chuỗi với for...of

const str = "Code";

for (let char of str) {
    console.log(char);
}

Output:

C
o
d
e

Ứng dụng: Lặp từng ký tự trong chuỗi mà không cần charAt().

Dùng Array.from() để chuyển đổi một Iterable thành Array

Phương thức Array.from() giúp biến đổi một đối tượng Iterable thành mảng, thuận tiện khi cần áp dụng các phương thức mảng như map(), filter(), reduce().

Ví dụ 8: Chuyển đổi Set thành mảng

const set = new Set(["apple", "banana", "orange"]);
const arrayFromSet = Array.from(set);

console.log(arrayFromSet); // ["apple", "banana", "orange"]

Ứng dụng: Dùng khi cần loại bỏ phần tử trùng lặp hoặc chuyển đổi dữ liệu.

Áp dụng Iterables trong Map() và Set()

Lặp qua Map với for...of

Map là Iterable, cho phép lặp qua từng cặp key-value.

const userMap = new Map([
    ["name", "John"],
    ["age", 25],
    ["city", "New York"]
]);

for (let [key, value] of userMap) {
    console.log(`${key}: ${value}`);
}

Output:

name: John
age: 25
city: New York

Ứng dụng: Lặp qua dữ liệu từ database, API, hoặc đối tượng cấu hình.

Lặp qua Set với for...of

Set giúp loại bỏ phần tử trùng lặp và vẫn có thể lặp qua như mảng.

const uniqueNumbers = new Set([1, 2, 3, 3, 4]);

for (let num of uniqueNumbers) {
    console.log(num);
}

Output:

1
2
3
4

Ứng dụng: Xử lý tập dữ liệu duy nhất, như danh sách email không trùng lặp.

Kết bài

Trong JavaScript, Iterables đóng vai trò quan trọng trong việc duyệt và xử lý dữ liệu một cách linh hoạt. Bằng cách sử dụng vòng lặp for...of, spread operator (...), destructuring, và các phương thức như Array.from(), chúng ta có thể tối ưu hóa mã nguồn, giúp xử lý dữ liệu hiệu quả hơn.

Bên cạnh đó, các đối tượng như Array, String, Set, Map đều hỗ trợ giao thức Iterable, giúp việc lặp qua dữ liệu trở nên trực quan hơn mà không cần sử dụng chỉ mục như for hoặc while. Đặc biệt, với khả năng tạo Iterable tùy chỉnh, chúng ta có thể mở rộng tính năng của JavaScript để phù hợp với nhu cầu thực tế.

Bài viết liên quan

  • 2