Tiếng Việt

Hướng dẫn toàn diện về cách hiểu và triển khai Giao thức Iterator của JavaScript, giúp bạn tạo các iterator tùy chỉnh để xử lý dữ liệu nâng cao.

Giải mã Giao thức Iterator và Iterator Tùy chỉnh trong JavaScript

Giao thức Iterator của JavaScript cung cấp một cách tiêu chuẩn hóa để duyệt qua các cấu trúc dữ liệu. Việc hiểu rõ giao thức này giúp các nhà phát triển làm việc hiệu quả với các iterable tích hợp sẵn như mảng và chuỗi, cũng như tạo ra các iterable tùy chỉnh của riêng họ, phù hợp với các cấu trúc dữ liệu và yêu cầu ứng dụng cụ thể. Hướng dẫn này cung cấp một cái nhìn toàn diện về Giao thức Iterator và cách triển khai các iterator tùy chỉnh.

Giao thức Iterator là gì?

Giao thức Iterator định nghĩa cách một đối tượng có thể được lặp qua, tức là cách các phần tử của nó có thể được truy cập tuần tự. Nó bao gồm hai phần: giao thức Iterable và giao thức Iterator.

Giao thức Iterable

Một đối tượng được coi là Iterable (có thể lặp) nếu nó có một phương thức với khóa là Symbol.iterator. Phương thức này phải trả về một đối tượng tuân thủ giao thức Iterator.

Về cơ bản, một đối tượng iterable biết cách tạo ra một iterator cho chính nó.

Giao thức Iterator

Giao thức Iterator định nghĩa cách truy xuất các giá trị từ một chuỗi. Một đối tượng được coi là một iterator nếu nó có phương thức next() trả về một đối tượng với hai thuộc tính:

Phương thức next() là thành phần chính của giao thức Iterator. Mỗi lần gọi đến next() sẽ di chuyển iterator và trả về giá trị tiếp theo trong chuỗi. Khi tất cả các giá trị đã được trả về, next() sẽ trả về một đối tượng với done được đặt thành true.

Các Iterable Tích hợp sẵn

JavaScript cung cấp một số cấu trúc dữ liệu tích hợp sẵn có khả năng lặp tự nhiên. Chúng bao gồm:

Các iterable này có thể được sử dụng trực tiếp với vòng lặp for...of, cú pháp spread (...), và các cấu trúc khác dựa trên Giao thức Iterator.

Ví dụ với Mảng:


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

for (const item of myArray) {
  console.log(item); // Output: apple, banana, cherry
}

Ví dụ với Chuỗi:


const myString = "Hello";

for (const char of myString) {
  console.log(char); // Output: H, e, l, l, o
}

Vòng lặp for...of

Vòng lặp for...of là một cấu trúc mạnh mẽ để lặp qua các đối tượng iterable. Nó tự động xử lý sự phức tạp của Giao thức Iterator, giúp việc truy cập các giá trị trong một chuỗi trở nên dễ dàng.

Cú pháp của vòng lặp for...of là:


for (const element of iterable) {
  // Code to be executed for each element
}

Vòng lặp for...of truy xuất iterator từ đối tượng iterable (sử dụng Symbol.iterator), và lặp đi lặp lại việc gọi phương thức next() của iterator cho đến khi done trở thành true. Trong mỗi lần lặp, biến element được gán giá trị của thuộc tính value được trả về bởi next().

Tạo Iterator Tùy chỉnh

Mặc dù JavaScript cung cấp các iterable tích hợp sẵn, sức mạnh thực sự của Giao thức Iterator nằm ở khả năng định nghĩa các iterator tùy chỉnh cho các cấu trúc dữ liệu của riêng bạn. Điều này cho phép bạn kiểm soát cách dữ liệu của mình được duyệt và truy cập.

Đây là cách tạo một iterator tùy chỉnh:

  1. Định nghĩa một lớp hoặc đối tượng đại diện cho cấu trúc dữ liệu tùy chỉnh của bạn.
  2. Triển khai phương thức Symbol.iterator trên lớp hoặc đối tượng của bạn. Phương thức này nên trả về một đối tượng iterator.
  3. Đối tượng iterator phải có một phương thức next() trả về một đối tượng với các thuộc tính valuedone.

Ví dụ: Tạo một Iterator cho một Khoảng giá trị Đơn giản

Hãy tạo một lớp tên là Range đại diện cho một khoảng số. Chúng ta sẽ triển khai Giao thức Iterator để cho phép lặp qua các số trong khoảng đó.


class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  [Symbol.iterator]() {
    let currentValue = this.start;
    const that = this; // Capture 'this' for use inside the iterator object

    return {
      next() {
        if (currentValue <= that.end) {
          return {
            value: currentValue++,
            done: false,
          };
        } else {
          return {
            value: undefined,
            done: true,
          };
        }
      },
    };
  }
}

const myRange = new Range(1, 5);

for (const number of myRange) {
  console.log(number); // Output: 1, 2, 3, 4, 5
}

Giải thích:

Ví dụ: Tạo một Iterator cho Danh sách Liên kết

Hãy xem xét một ví dụ khác: tạo một iterator cho cấu trúc dữ liệu danh sách liên kết. Danh sách liên kết là một chuỗi các nút, trong đó mỗi nút chứa một giá trị và một tham chiếu (con trỏ) đến nút tiếp theo trong danh sách. Nút cuối cùng trong danh sách có một tham chiếu đến null (hoặc undefined).


class LinkedListNode {
    constructor(value, next = null) {
        this.value = value;
        this.next = next;
    }
}

class LinkedList {
    constructor() {
        this.head = null;
    }

    append(value) {
        const newNode = new LinkedListNode(value);
        if (!this.head) {
            this.head = newNode;
            return;
        }

        let current = this.head;
        while (current.next) {
            current = current.next;
        }
        current.next = newNode;
    }

    [Symbol.iterator]() {
        let current = this.head;

        return {
            next() {
                if (current) {
                    const value = current.value;
                    current = current.next;
                    return {
                        value: value,
                        done: false
                    };
                } else {
                    return {
                        value: undefined,
                        done: true
                    };
                }
            }
        };
    }
}

// Example Usage:
const myList = new LinkedList();
myList.append("London");
myList.append("Paris");
myList.append("Tokyo");

for (const city of myList) {
    console.log(city); // Output: London, Paris, Tokyo
}

Giải thích:

Hàm Generator

Hàm generator cung cấp một cách ngắn gọn và thanh lịch hơn để tạo ra các iterator. Chúng sử dụng từ khóa yield để tạo ra các giá trị theo yêu cầu.

Một hàm generator được định nghĩa bằng cú pháp function*.

Ví dụ: Tạo một Iterator bằng Hàm Generator

Hãy viết lại iterator Range bằng cách sử dụng một hàm generator:


class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  *[Symbol.iterator]() {
    for (let i = this.start; i <= this.end; i++) {
      yield i;
    }
  }
}

const myRange = new Range(1, 5);

for (const number of myRange) {
  console.log(number); // Output: 1, 2, 3, 4, 5
}

Giải thích:

Hàm generator đơn giản hóa việc tạo iterator bằng cách tự động xử lý phương thức next() và cờ done.

Ví dụ: Bộ tạo Dãy Fibonacci

Một ví dụ tuyệt vời khác về việc sử dụng hàm generator là tạo ra dãy Fibonacci:


function* fibonacciSequence() {
  let a = 0;
  let b = 1;

  while (true) {
    yield a;
    [a, b] = [b, a + b]; // Destructuring assignment for simultaneous update
  }
}

const fibonacci = fibonacciSequence();

for (let i = 0; i < 10; i++) {
  console.log(fibonacci.next().value); // Output: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
}

Giải thích:

Lợi ích của việc sử dụng Giao thức Iterator

Các Kỹ thuật Iterator Nâng cao

Kết hợp các Iterator

Bạn có thể kết hợp nhiều iterator thành một iterator duy nhất. Điều này hữu ích khi bạn cần xử lý dữ liệu từ nhiều nguồn một cách thống nhất.


function* combineIterators(...iterables) {
  for (const iterable of iterables) {
    for (const item of iterable) {
      yield item;
    }
  }
}

const array1 = [1, 2, 3];
const array2 = ["a", "b", "c"];
const string1 = "XYZ";

const combined = combineIterators(array1, array2, string1);

for (const value of combined) {
  console.log(value); // Output: 1, 2, 3, a, b, c, X, Y, Z
}

Trong ví dụ này, hàm `combineIterators` nhận bất kỳ số lượng iterable nào làm đối số. Nó lặp qua mỗi iterable và yield từng mục. Kết quả là một iterator duy nhất tạo ra tất cả các giá trị từ tất cả các iterable đầu vào.

Lọc và Biến đổi các Iterator

Bạn cũng có thể tạo các iterator lọc hoặc biến đổi các giá trị được tạo ra bởi một iterator khác. Điều này cho phép bạn xử lý dữ liệu theo một quy trình (pipeline), áp dụng các hoạt động khác nhau cho mỗi giá trị khi nó được tạo ra.


function* filterIterator(iterable, predicate) {
  for (const item of iterable) {
    if (predicate(item)) {
      yield item;
    }
  }
}

function* mapIterator(iterable, transform) {
  for (const item of iterable) {
    yield transform(item);
    }
}

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

const evenNumbers = filterIterator(numbers, (x) => x % 2 === 0);
const squaredEvenNumbers = mapIterator(evenNumbers, (x) => x * x);

for (const value of squaredEvenNumbers) {
    console.log(value); // Output: 4, 16, 36
}

Ở đây, `filterIterator` nhận một iterable và một hàm vị từ (predicate). Nó chỉ yield các mục mà hàm vị từ trả về `true`. `mapIterator` nhận một iterable và một hàm biến đổi (transform). Nó yield kết quả của việc áp dụng hàm biến đổi cho mỗi mục.

Ứng dụng trong Thực tế

Giao thức Iterator được sử dụng rộng rãi trong các thư viện và framework JavaScript, và nó có giá trị trong nhiều ứng dụng thực tế, đặc biệt là khi xử lý các tập dữ liệu lớn hoặc các hoạt động bất đồng bộ.

Các Thực hành Tốt nhất

Kết luận

Giao thức Iterator của JavaScript cung cấp một cách mạnh mẽ và linh hoạt để duyệt qua các cấu trúc dữ liệu. Bằng cách hiểu rõ các giao thức Iterable và Iterator, và bằng cách tận dụng các hàm generator, bạn có thể tạo ra các iterator tùy chỉnh phù hợp với nhu cầu cụ thể của mình. Điều này cho phép bạn làm việc hiệu quả với dữ liệu, cải thiện khả năng đọc mã và nâng cao hiệu suất của các ứng dụng của bạn. Việc thành thạo các iterator mở ra một sự hiểu biết sâu sắc hơn về các khả năng của JavaScript và giúp bạn viết mã thanh lịch và hiệu quả hơn.