Протоколи перебору
Пара доповнень до ECMAScript 2015 є не новими вбудованими елементами чи синтаксисом, а протоколами. Ці протоколи можуть реалізовуватись будь-яким об'єктом, що відповідає певним правилам.
Існують два протоколи: протокол ітерабельного об'єкта і протокол ітератора.
Протокол ітерабельного об'єкта
Протокол ітерабельного об'єкта дозволяє об'єктам JavaScript визначати чи налаштовувати свою ітераційну поведінку, наприклад, через які значення буде проходити цикл у конструкції for..of. Деякі вбудовані типи є вбудованими ітерабельними об'єктами з визначеною за замовчуванням ітераційною поведінкою, наприклад, Array або Map, в той час, як інші типи (такі, як Object) не є ітерабельними.
Для того, щоб бути ітерабельним, об'єкт має реалізувати метод @@iterator, тобто, цей об'єкт (або один з об'єктів у його ланцюжку прототипів) повинен мати властивість з ключем @@iterator, доступну через константу :Symbol.iterator
| Властивість | Значення |
|---|---|
[Symbol.iterator] |
Функція без аргументів, яка повертає об'єкт, що відповідає протоколу ітератора. |
Коли виникає необхідність перебрати об'єкт (наприклад, на початку циклу for..of), його метод @@iterator викликається без аргументів, а ітератор, який він повертає, використовується для отримання значень, що перебираються.
Протокол ітератора
Протокол ітератора визначає стандартний спосіб створювати послідовності значень (скінченні або нескінченні).
Об'єкт є ітератором, коли реалізує метод next() з наступною семантикою:
| Властивість | Значення |
|---|---|
next |
Функція з нулем аргументів, яка повертає об'єкт з двома властивостями:
Метод |
Неможливо знати, чи певний об'єкт реалізує протокол ітератора, однак, можна легко створити об'єкт, який відповідає обом протоколам, ітератора та ітерабельного об'єкта (як показано нижче у прикладі). Це дозволяє використовувати ітератор там, де очікується ітерабельний об'єкт. Тому нечасто є потреба реалізовувати протокол ітератора, не реалізуючи також протокол ітерабельного об'єкта.
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 (ECMA-262) The definition of 'Iteration' in that specification. |
Living Standard |
Див. також
- Більше інформації щодо генераторів ES2015 дивіться у документації по function*.

