Протоколи перебору

Пара доповнень до ECMAScript 2015 є не новими вбудованими елементами чи синтаксисом, а протоколами. Ці протоколи можуть реалізовуватись будь-яким об'єктом, що відповідає певним правилам.

Існують два протоколи: протокол ітерабельного об'єкта і протокол ітератора.

Протокол ітерабельного об'єкта

Протокол ітерабельного об'єкта дозволяє об'єктам JavaScript визначати чи налаштовувати свою ітераційну поведінку, наприклад, через які значення буде проходити цикл у конструкції for..of. Деякі вбудовані типи є вбудованими ітерабельними об'єктами з визначеною за замовчуванням ітераційною поведінкою, наприклад, Array або Map, в той час, як інші типи (такі, як Object) не є ітерабельними.

Для того, щоб бути ітерабельним, об'єкт має реалізувати метод @@iterator, тобто, цей об'єкт (або один з об'єктів у його ланцюжку прототипів) повинен мати властивість з ключем @@iterator, доступну через константу Symbol.iterator:

Властивість Значення
[Symbol.iterator] Функція без аргументів, яка повертає об'єкт, що відповідає протоколу ітератора.

Коли виникає необхідність перебрати об'єкт (наприклад, на початку циклу for..of), його метод @@iterator викликається без аргументів, а ітератор, який він повертає, використовується для отримання значень, що перебираються.

Протокол ітератора

Протокол ітератора визначає стандартний спосіб створювати послідовності значень (скінченні або нескінченні).

Об'єкт є ітератором, коли реалізує метод next() з наступною семантикою:

Властивість Значення
next

Функція з нулем аргументів, яка повертає об'єкт з двома властивостями:

  • done (булеве значення)
    • Має значення true, якщо ітератор досяг кінця послідовності, що перебирається. В цьому випадку value може містити значення, що повертається ітератором. Значення, що повертаються, пояснюються тут.
    • Має значення false, якщо ітератор був здатний надати наступне значення послідовності. Це аналогічно тому, щоб взагалі не вказувати значення властивості done.
  • value - будь-яке значення JavaScript, що повертає ітератор. Його можна не вказувати, коли done дорівнює true.

Метод next завжди повинен повертати об'єкт з належними властивостями, в тому числі done та value. Якщо повертається значення, що не є об'єктом (наприклад, false чи undefined), буде викинуто помилку TypeError ("iterator.next() returned a non-object value").

Неможливо знати, чи певний об'єкт реалізує протокол ітератора, однак, можна легко створити об'єкт, який відповідає обом протоколам, ітератора та ітерабельного об'єкта (як показано нижче у прикладі). Це дозволяє використовувати ітератор там, де очікується ітерабельний об'єкт. Тому нечасто є потреба реалізовувати протокол ітератора, не реалізуючи також протокол ітерабельного об'єкта. 

var myIterator = {
    next: function() {
        // ...
    },
    [Symbol.iterator]: function() { return this }
};

Приклади застосування протоколів перебору

Об'єкт String є прикладом вбудованого ітерабельного об'єкта:

var someString = '13';
typeof someString[Symbol.iterator];          // "function"

Вбудований ітератор об'єкта String повертає коди символів рядка один за одним:

var iterator = someString[Symbol.iterator]();
iterator + '';                               // "[object String Iterator]"
 
iterator.next();                             // { value: "1", done: false }
iterator.next();                             // { value: "3", done: false }
iterator.next();                             // { value: undefined, done: true }

Деякі вбудовані конструкції, такі як оператор розпакування, використовують під капотом той самий протокол перебору:

[...someString]                              // ["1", "3"]

Ми можемо перевизначити поведінку під час перебору, надавши свій власний метод @@iterator:

var someString = new String('привіт');   // необхідно явно конструювати об'єкт String, щоб запобігти автопакуванню

someString[Symbol.iterator] = function() {
  return { // це ітератор, що повертає єдиний елемент, рядок "бувай"
    next: function() {
      if (this._first) {
        this._first = false;
        return { value: 'бувай', done: false };
      } else {
        return { done: true };
      }
    },
    _first: true
  };
};

Зверніть увагу, як перевизначення методу @@iterator впливає на поведінку вбудованих конструкцій, що використовують протокол перебору:

[...someString];                             // ["бувай"]
someString + '';                             // "привіт"

Приклади ітерабельних об'єктів

Вбудовані ітерабельні об'єкти

String, Array, TypedArray, Map та Set всі є вбудованими ітерабельними об'єктами, тому що кожний з їхніх прототипів реалізує метод @@iterator.

Створені користувачем ітерабельні об'єкти

Ми можемо створювати власні ітерабельні об'єкти наступним чином:

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

Вбудовані API, що приймають ітерабельні об'єкти

Існує багато API, які приймають ітерабельні об'єкти, наприклад: Map([iterable]), WeakMap([iterable]), Set([iterable]) and WeakSet([iterable]):

var myObj = {};
new Map([[1, 'а'], [2, 'б'], [3, 'в']]).get(2);               // "б"
new WeakMap([[{}, 'а'], [myObj, 'б'], [{}, 'в']]).get(myObj); // "б"
new Set([1, 2, 3]).has(3);                               // true
new Set('123').has('2');                                 // true
new WeakSet(function* () {
    yield {};
    yield myObj;
    yield {};
}()).has(myObj);                                         // true

Дивіться також Promise.all(iterable), Promise.race(iterable) та Array.from().

Синтаксис, що очікує на ітерабельний об'єкт

Деякі оператори та вирази очікують на ітерабельні об'єкти, наприклад, цикли for-of, оператор розпакування, yield* та деструктуризаційне присвоєння:

for(let value of ['а', 'б', 'в']){
    console.log(value);
}
// "а"
// "б"
// "в"

[...'абв']; // ["а", "б", "в"]

function* gen() {
  yield* ['а', 'б', 'в'];
}

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

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

Погано сформовані ітерабельні об'єкти

Якщо метод ітерабельного об'єкта @@iterator не повертає об'єкт ітератора, то це погано сформований ітерабельний об'єкт. Використання його в такому вигляді ймовірно призведе до викидання винятків під час виконання або помилкової поведінки:

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

Приклади ітераторів

Простий ітератор

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

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

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

Нескінченний ітератор

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

var it = idMaker();

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

З генератором

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

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

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



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

var gen = idMaker();

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

З класом ES2015

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

  [Symbol.iterator]() {
    return {
      next: () => {
        if (this.index < this.data.length) {
          return {value: this.data[this.index++], done: false};
        } else {
          this.index = 0; //Якщо ми хотіли б перебрати його знову, без примусового ручного оновлення індексу
          return {done: true};
        }
      }
    }
  };
}

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

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

Генератор є ітератором чи ітерабельним об'єктом?

Об'єкт генератор є одночасно ітератором та ітерабельним об'єктом:

var aGeneratorObject = function* () {
    yield 1;
    yield 2;
    yield 3;
}();
typeof aGeneratorObject.next;
// "function", бо він має метод next, отже, він ітератор
typeof aGeneratorObject[Symbol.iterator];
// "function", бо він має метод @@iterator, отже, він ітерабельний об'єкт
aGeneratorObject[Symbol.iterator]() === aGeneratorObject;
// true, бо його метод @@iterator повертає себе (ітератор),
// отже, він добре сформований ітерабельний об'єкт
[...aGeneratorObject];
// [1, 2, 3]

Специфікації

Специфікація Статус Коментар
ECMAScript 2015 (6th Edition, ECMA-262)
The definition of 'Iteration' in that specification.
Standard Початкова виознака.
ECMAScript Latest Draft (ECMA-262)
The definition of 'Iteration' in that specification.
Draft

Див. також