Iteration protocols

Bản dịch này chưa hoàn thành. Xin hãy giúp dịch bài viết này từ tiếng Anh

Là một vài bổ sung cho ECMAScript 2015, Iteration protocols không phải là một tích hợp sẵn hay cú pháp mới mà là protocols. Các giao thức này có thể triển khai bởi bất kỳ đối tượng nào đơn giản bằng cách thực hiện theo một số quy ước.

Có 2 giao thức: iterable protocol và iterator protocol.

The iterable protocol

Iterable protocol cho phép các đối tượng của JavaScript xác định hoặc tuỳ chỉnh hành vi lặp của chúng, chẳng hạn như giá trị nào sẽ được lặp trong vòng lặp for...of. Một số kiểu tích hợp sẵn built-in iterables là hành vi lặp mặc định, chẳng hạn như Array hoặc Map, trong khi các kiểu khác (chẳng hạn Object) không có.

Để có thể triển khai giao thức iterable, một đối tượng phải có phương thức @@iterator, điều này có nghĩa là một đối tượng (hoặc một trong các đối tượng trên prototype chain của đối tượng đó) phải có một thuộc tính @@iterator và thao tác với thuộc tính đó thông qua hằng số Symbol.iterator:

Property Value
[Symbol.iterator] Một hàm không có tham số đầu vào trả ra một đối tượng phù hợp với iterator protocol.

Bất cứ khi nào một đối tượng thực hiện vòng lặp (chẳng hạn như sử dụng vòng lặp for...of), phương thức @@iterator sẽ được gọi mà không có tham số đầu vào, và trả ra iterator được sử dụng để thu được giá trị được lặp.

Lưu ý khi hàm không tham số đầu vào được gọi, nó sẽ được gọi như là một phương thức của iterable object. Do đó bên trong hàm, từ khoá this có thể được sử dụng để truy cập vào các thuộc tính của iterable object, để quyết định những gì được cung cấp trong quá trình lặp.

Hàm này có thể là một hàm bình thường hoặc nó có thể là một generator function, do đó khi được gọi, một iterator object sẽ được trả về. Bên trong của generator function, mỗi giá trị trả về có thể cung cấp bằng cách sử dụng yield.

The iterator protocol

Ierator protocol định nghĩa một cách tiêu chuẩn để tạo ra một chuỗi các giá trị (hưu hạn hoặc vô hạn), và  có thể trả về 1 giá trị khi tất cả các giá trị đã được tạo.

Một đối tượng là một iterator khi nó triển khai phương thức next() với ý nghĩa như sau:

Property Value
next()

Một hàm không tham số đầu vào trả ra một đối tượng có ít nhất 2 thuộc tính sau: 

done (boolean)

Có giá trị false nếu iterator có thể tạo ra giá trị tiếp theo trong chuỗi. (This is equivalent to not specifying the done property altogether.)

Có giá trị true nếu iterator kết thúc chuỗi. Trong trường hợp này, value có thể tuỳ chọn giá trị trả về cho iterator.

value
Bất kỳ giá trị JavaScript nào được trả về bởi iterator. ?Có thể bỏ qua khi done là true.

Phương thức next() phải luôn luôn trả về một đối tượng với các thuộc tính thích hợp bao gồm done và value. Nếu một giá trị không phải đối tượng được trả về (chẳng hạn như false hoặc undefined), một TypeError ("iterator.next() returned a non-object value") sẽ được đẩy ra.

Lưu ý: Không thể biết liệu một đối tượng cụ thể có triển khai giao thức iterator hay không. Tuy nhiên, dễ dàng để tạo ra một đối tượng mà có cả 2 giao thức iterator và iterable (như ví dụ dưới đây).

Làm như vậy cho phép một iterator có thể sử dụng các cú pháp đa dạng của iterables. Vì vậy, rất hiếm khi triển khai giao thức Iterator Protocol mà không triển khai Iterable.

// Satisfies both the Iterator Protocol and Iterable
let myIterator = {
    next: function() {
        // ...
    },
    [Symbol.iterator]: function() { return this; }
};

Examples using the iteration protocols

String là một ví dụ tích hợp sẵn iterable object:

let someString = 'hi';
console.log(typeof someString[Symbol.iterator]); // "function"

iterator mặc định của String  trả ra lần lượt từng mã của các ký tự:

let iterator = someString[Symbol.iterator]();
console.log(iterator + ''); // "[object String Iterator]"
 
console.log(iterator.next()); // { value: "h", done: false }
console.log(iterator.next()); // { value: "i", done: false }
console.log(iterator.next()); // { value: undefined, done: true }

Some built-in constructs—such as the spread syntax—use the same iteration protocol under the hood:

console.log([...someString]); // ["h", "i"]

You can redefine the iteration behavior by supplying our own @@iterator:

// need to construct a String object explicitly to avoid auto-boxing
let someString = new String('hi');

someString[Symbol.iterator] = function () {
  return {
    // this is the iterator object, returning a single element (the string "bye")
    next: function () {
      return this._first ? {
        value: 'bye',
        done: (this._first = false)
      } : {
        done: true
      }
    },
    _first: true
  };
};

Notice how redefining @@iterator affects the behavior of built-in constructs that use the iteration protocol:

console.log([...someString]); // ["bye"]
console.log(someString + ''); // "hi"

Iterable examples

Built-in iterables

String, Array, TypedArray, Map, and Set are all built-in iterables, because each of their prototype objects implements an @@iterator method.

User-defined iterables

You can make your own iterables like this:

let myIterable = {};
myIterable[Symbol.iterator] = function* () {
    yield 1;
    yield 2;
    yield 3;
};
console.log([...myIterable]); // [1, 2, 3]

Built-in APIs accepting iterables

There are many APIs that accept iterables. Some examples include:

new Map([[1, 'a'], [2, 'b'], [3, 'c']]).get(2); // "b"

let myObj = {};

new WeakMap([
    [{}, 'a'],
    [myObj, 'b'],
    [{}, 'c']
]).get(myObj);             // "b"

new Set([1, 2, 3]).has(3); // true
new Set('123').has('2');   // true

new WeakSet(function* () {
    yield {} 
    yield myObj 
    yield {} 
}()).has(myObj);           // true

See Also

Syntaxes expecting iterables

Some statements and expressions expect iterables, for example the for...of loops, the spread operator)}}, yield*, and destructuring assignment:

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

console.log([...'abc']);   // ["a", "b", "c"]

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

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

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

Non-well-formed iterables

If an iterable's @@iterator method doesn't return an iterator object, then it's considered a non-well-formed iterable.

Using one is likely to result in runtime errors or buggy behavior:

let nonWellFormedIterable = {};
nonWellFormedIterable[Symbol.iterator] = () => 1;
[...nonWellFormedIterable]; // TypeError: [] is not a function

Iterator examples

Simple iterator

function makeIterator(array) {
  let nextIndex = 0
  return {
    next: function() {
      return nextIndex < array.length ? {
        value: array[nextIndex++],
        done: false
      } : {
        done: true
      };
    }
  };
}

let it = makeIterator(['yo', 'ya']);

console.log(it.next().value); // 'yo'
console.log(it.next().value); // 'ya'
console.log(it.next().done);  // true

Infinite iterator

function idMaker() {
  let index = 0;
  return {
    next: function() {
      return {
        value: index++,
        done: false
      };
    }
  };
}

let it = idMaker();

console.log(it.next().value); // '0'
console.log(it.next().value); // '1'
console.log(it.next().value); // '2'
// ...

With a generator

function* makeSimpleGenerator(array) {
  let nextIndex = 0;
  while (nextIndex < array.length) {
    yield array[nextIndex++];
  }
}

let gen = makeSimpleGenerator(['yo', 'ya']);

console.log(gen.next().value); // 'yo'
console.log(gen.next().value); // 'ya'
console.log(gen.next().done);  // true

function* idMaker() {
  let index = 0;
  while (true) {
    yield index++;
  }
}

let gen = idMaker()

console.log(gen.next().value); // '0'
console.log(gen.next().value); // '1'
console.log(gen.next().value); // '2'
// ...

With ES2015 class

class SimpleClass {
  constructor(data) {
    this.data = data;
  }

  [Symbol.iterator]() {
    // Use a new index for each iterator. This makes multiple
    // iterations over the iterable safe for non-trivial cases,
    // such as use of break or nested looping over the same iterable.
    let index = 0;

    return {
      next: () => {
        if (index < this.data.length) {
          return {value: this.data[index++], done: false}
        } else {
          return {done: true}
        }
      }
    }
  }
}

const simple = new SimpleClass([1,2,3,4,5]);

for (const val of simple) {
  console.log(val); // '1' '2' '3' '4' '5' 
}

Is a generator object an iterator or an iterable?

A generator object is both iterator and iterable:

let aGeneratorObject = function* () {
  yield 1;
  yield 2;
  yield 3;
}();

console.log(typeof aGeneratorObject.next);
// "function", because it has a next method, so it's an iterator

console.log(typeof aGeneratorObject[Symbol.iterator]);
// "function", because it has an @@iterator method, so it's an iterable

console.log(aGeneratorObject[Symbol.iterator]() === aGeneratorObject);
// true, because its @@iterator method returns itself (an iterator), so it's an well-formed iterable

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

Specifications

Specification
ECMAScript (ECMA-262)
The definition of 'Iteration' in that specification.

See also