Iterators and generators

You’re reading the English version of this content since no translation exists yet for this locale. Help us translate this article!

Việc xử lý mỗi phần tử trong một tập hợp là thao tác rất phổ biến. JavaScript cung cấp một số cách để duyệt qua một tập hợp, từ đơn giản với lệnh lặp for đến map()filter(). Iterators và Generators đem đến khái niệm của iteration vào nhân của ngôn ngữ và cung cấp cách để tùy biến cơ chế của vòng lặp for...of.

Tham khảo thêm:

Iterators

Trong JavaScript một iterator là một object mà nó định nghĩa một trình tự và giá trị có thể trả về tiếp theo trước khi kết thúc. Một cách cụ thể hơn một iterator là một object bất kỳ mà nó cài đặt giao thức Iterator với việc cài đặt phương thức next() mà trả về một object với thuộc tính:  value, giá trị kế tiếp in chuỗi giá trị; và done, mà là true nếu giá trị  cuối cùng trong chuỗi giá trị đã được sử dụng. Nếu value trả về cùng với thuộc tính done, nó là giá trị trả về bởi lệnh return trong iterator đó.

Mỗi lần được tạo, một iterator có thể được duyệt tường minh bởi việc gọi lại nhiều lần phương thức next(). Việc duyệt qua iterator được gọi là sử dụng iterator, bởi vì nó chỉ có thể làm một lần. Sau khi giá trị kết thúc được trả về thì lần gọi next() luôn trả về {done: true}.

Iterator thông dụng nhất trong JavaScript là iterator mãng, mà đơn giản là trả về mỗi giá trị trong associated array theo thứ tự. Trong khi nó thì dễ dàng tưởng tượng rằng tất cả iterators được biểu diễn như mãng, điều này không đúng. Mảng phải được cấp phát toàn bộ, nhưng iterators được sử dụng chỉ khi cần thiết và vì vậy có thể biểu diễn một chuỗi không có giới hạn kích thước, như là một dãy các số nguyên từ 0 cho đến vô cực.

Sau đây là một ví dụ minh họa cho điều đó. Bạn có thể tạo ra một iterator khoảng giá trị đơn giản mà nó định nghĩa một dãy các số nguyên từ start (đã bao gồm) đến end (không bao gồm) với bước nhảy step. Giá trị trả về cuối cùng là kích thước của dãy giá trị mà nó đã tạo, ghi nhận trong biến iterationCount.

function makeRangeIterator(start = 0, end = Infinity, step = 1) {
    let nextIndex = start;
    let iterationCount = 0;

    const rangeIterator = {
       next: function() {
           let result;
           if (nextIndex <= end) {
               result = { value: nextIndex, done: false }
               nextIndex += step;
               iterationCount++;
               return result;
           }
           return { value: iterationCount, done: true }
       }
    };
    return rangeIterator;
}

Việc sử dụng iterator được minh họa như trong đoạn mã sau:

let it = makeRangeIterator(1, 10, 2);

let result = it.next();
while (!result.done) {
 console.log(result.value); // 1 3 5 7 9
 result = it.next();
}

console.log("Iterated over sequence of size: ", result.value); // 5

Bạn không biết chắc một đối tượng cụ thể là một iterator hay không. Nếu bạn cần làm điều này, thì hãy dùng Iterables.

Generator functions

Iterator tùy biến là công cụ hữu ích, tuy nhiên việc tạo chúng đòi hỏi sự cẩn thận vì chúng ta cần phải tự quản lý trạng thái của chúng. Hàm Generator cung cấp một cách thức tốt hơn để thay thế việc đó: chúng cho phép bạn định nghĩa giải thuật duyệt bằng cách viết một hàm đơn mà sự thực thi của nó không diễn ra liên tục. Hàm Generator được viết bằng cách sử dụng cú pháp function*. Khi gọi lần đầu, hàm generator không thực thi bất kỳ đoạn mã nào bên trong, thay vào đó nó trả về kiểu của iterator được gọi là generator. Khi một giá trị được dùng để trả về bởi lời gọi phương thức next() của generator, hàm generator thực thi cho đến khi nó bắt gặp từ khóa  yield.

Hàm có thể được gọi bao nhiêu lần cũng được và nó trả về một generator cho mỗi lần gọi, tuy nhiên generator có thể chỉ được duyệt một lần.

Chúng ta có thể điều chỉnh ví dụ trên. Chúng ta thấy đoạn mã sau cũng thực thi cùng chức năng, nhưng nó dễ viết và dễ đọc hơn.

function* makeRangeIterator(start = 0, end = Infinity, step = 1) {
    let iterationCount = 0;
    for (let i = start; i < end; i += step) {
        iterationCount++;
        yield i;
    }
    return iterationCount;
}

Iterables

Một đối tượng là khả duyệt nếu nó định nghĩa cơ chế duyệt của nó, chẳng hạn như giá trị gì được duyệt qua trong hàm dựng for...of. Một vài kiểu dựng sẳn, như  Array hoặc Map, có cơ chế duyệt mặc định, trong khi những kiểu khác (như là Object) thì không có.

Để có thể khả duyệt, một object phải cái đặt phương thức của @@iterator, nghĩa là object (hoặc đối tượng cha trong chuỗi  prototype) phải có một thuộc tính Symbol.iterator.
It may be possible to iterate over an iterable more than once, or only once. It is up to the programmer to know which is the case. Iterables which can iterate only once (e.g. Generators) customarily return this from their @@iterator method, where those which can be iterated many times must return a new iterator on each invocation of @@iterator.
Bạn có thể cho phép duyệt qua nhiều lần, hoặc chỉ một lần. Điều này phụ thuộc vào theo yêu cầu của mỗi ứng dụng. Đối tượng khả duyệt mà chỉ duyệt qua một lần (ví dụ Generators) đơn giản là nó chỉ trả về this từ phương thức  @@iterator, trong khi để khả duyệt nhiều lần bạn phải trả về một iterator mới cho mỗi lần gọi @@iterator.

Tự định nghĩa đối tượng khả duyệt

Chúng ta có thể tự tạo đối tượng khả duyệt như sau:

var myIterable = {
    *[Symbol.iterator]() {
        yield 1;
        yield 2;
        yield 3;
    }
}

for (let value of myIterable) { 
    console.log(value); 
}
// 1
// 2
// 3

or

[...myIterable]; // [1, 2, 3]

Những iterable tạo sẵn

String, Array, TypedArray, Map and Set là những iterable đã được tạo sẵn, bởi vì đối tượng prototype của chúng đã có phương thức Symbol.iterator.

Cú pháp đòi hỏi đối tượng khả duyệt

Một vài câu lệnh và biểu thức đòi hỏi toán hạng hoặc đối số phải là đối tượng khả duyệt, ví dụ lệnh lặp for-of, yield*

for (let value of ['a', 'b', 'c']) {
    console.log(value);
}
// "a"
// "b"
// "c"

[...'abc']; // ["a", "b", "c"]

function* gen() {
  yield* ['a', 'b', 'c'];
}

gen().next(); // { value: "a", done: false }

[a, b, c] = new Set(['a', 'b', 'c']);
a; // "a"

Generator Cao Cấp

Generator tính toán giá trị trả ra tùy biến theo nhu cầu, nó cho phép biển diễn chuỗi giá trị một cách hiệu quả mà tốn kém để tính toán, hoặc thậm chí cho phép chuỗi giá trị vô tận như đã minh họa ở trên.
 

The next() method also accepts a value which can be used to modify the internal state of the generator. A value passed to next() will be treated as the result of the last yield expression that paused the generator.
Phương  thức next() cũng chấp nhận một đối số mà được sử dụng để thay đổi trạng thái bên trong của generator. Một giá trị được truyền vào next() sẽ được dùng như kết quả của biểu thức yield cuối cùng khi tạm dừng generator.

Here is the fibonacci generator using next(x) to restart the sequence:

function* fibonacci() {
  var fn1 = 0;
  var fn2 = 1;
  while (true) {  
    var current = fn1;
    fn1 = fn2;
    fn2 = current + fn1;
    var reset = yield current;
    if (reset) {
        fn1 = 0;
        fn2 = 1;
    }
  }
}

var sequence = fibonacci();
console.log(sequence.next().value);     // 0
console.log(sequence.next().value);     // 1
console.log(sequence.next().value);     // 1
console.log(sequence.next().value);     // 2
console.log(sequence.next().value);     // 3
console.log(sequence.next().value);     // 5
console.log(sequence.next().value);     // 8
console.log(sequence.next(true).value); // 0
console.log(sequence.next().value);     // 1
console.log(sequence.next().value);     // 1
console.log(sequence.next().value);     // 2

Bạn có thể ép một generator phát sinh lỗi bằng cách gọi phương thức throw() của generator và chỉ định giá trị lỗi mà nó cần phát sinh. Lỗi này sẽ được phát sinh từ ngữ cảnh hiện tại của generator, tạo điểm mà yield đang tam dừng, thay vì lệnh throw value như cách thông thường.

Nếu lỗi không được bắt từ bên trong generator, nó sẽ được đẩy lên cấp cao hơn thông qua lời gọi hàm throw(), khi đó lời gọi tiếp theo tới next() sẽ trả về done với giá trị true.

Generator có một phương thức return(value) mà giá trị được truyền vào và kết thúc chính nó.