Цикъл проблем

Ако някога сте програмирали в JavaScript, има шанс да сте писали код, който изглежда така:

var colors = ["red", "green", "blue"];

for (var i = 0, len < colors.length; i < len; i++) {
    console.log(colors[i]);
}
			

Това е стандартен for цикъл, който създава променливата i за да следи индекса в colors масива. Стойността на i се увеличава всеки път в цикъла, ако тя не стане по-голяма от дължината на масива (съхраняван в len).

Това е един сравнително прост пример, сложността нараства, когато се правят вложени цикли и трябва да се следят няколко променливи. Тази допълнителна сложност може да доведе до грешки и шаблонния характер на for цикъла се поддава на повече грешки, когато подобен код се пише на няколко места. Това е проблема, който итераторите са предназначени да решат.

Какво са итераторите?

Итераторите са просто обекти със специфичен интерфейс, предназначен за повторение. Този интерфейс се състои от метод наречен next(), чийто резултат връща обект. Обекта резултат има две свойства, value, която е следващата стойност и done, който е булева стойност, която е true, когато няма повече стойности за връщане. Итератора поддържа вътрешен указател за местоположение в набора от стойности и с всяко извикване на next(), връща следващата подходяща стойност.

Ако извикаме next(), след последната стойност, която е била върната, метода връща done, като true и value съдържаща върнатата стойност за итератора. Тази върната стойност, не се счита за част от набора с данни, а по-скоро, като последната част от свързаните с тях данни или undefined, ако не съществуват такива данни. Върнатата стойност от итератора е подобна на върнатата стойност от функция в това, че е финален начин за предаване на информация към извикващия.

С това разбиране е сравнително лесно да се създаде итератор, използвайки ECMAScript 5, например:

function createIterator(items) {

    var i = 0;

    return {
        next: function() {

            var done = (i >= items.length);
            var value = !done ? items[i++] : undefined;

            return {
                done: done,
                value: value
            };

        }
    };
}

var iterator = createIterator([1, 2, 3]);

console.log(iterator.next());           // "{ value: 1, done: false }"
console.log(iterator.next());           // "{ value: 2, done: false }"
console.log(iterator.next());           // "{ value: 3, done: false }"
console.log(iterator.next());           // "{ value: undefined, done: true }"

// за всички по-нататъшни извиквания
console.log(iterator.next());           // "{ value: undefined, done: true }"
			

Функцията createIterator() връща обект с next() метод. Всеки път, когато метода се извика, се премества на следващата стойност в items масива и се връща, като value. Когато i е 3, done става true и третия условен оператор, който определя value се оценява на undefined. Тези два резултата отговарят на последния специален случай за итератори в ECMAScript 6, където next() се извиква върху итератора, след като последното парче от данни е било използвано.

Както показва този пример, писането на итератори, които се държат в съответствие с правилата, посочени в ECMAScript 6 е малко сложно.

За щастие, ECMAScript 6 предвижда също генератори, които правят създаването итератор обекти много по-просто.

Какво е генератори

Генератора е функция, която връща итератор. Генератор функциите са обозначени чрез вмъкване на характер звезда (*) след ключовата дума function и използват новата ключова дума yield. Няма значение дали звездата е в непосредствена близост до function или има някакво празно пространство между тях, както в този пример:

// generator
function *createIterator() {
    yield 1;
    yield 2;
    yield 3;
}
//генератора се извиква, като редовна функция, но се връща като итератор
let iterator = createIterator();

console.log(iterator.next().value);     // 1
console.log(iterator.next().value);     // 2
console.log(iterator.next().value);     // 3
			

Звездата * преди createIterator() прави тази функция генератор. Ключовата дума yield, също нова за ECMAScript 6, определя стойностите в резултата, които итератора трябва да върне в реда, по който трябва да се върнат, когато е извикан next(). Итератора генериран в този пример има три различни стойности, които да върне с последователно извикване на next() метода: първо 1, след това 2 и накрая 3. Генератора получава извикване, както всяка друга функция, както е показано при създаването на iterator.

Може би най-интересния аспект на генератор функцията е, че спира изпълнението след всяко yield изявление. Например, след като се изпълни yield 1 в този код, функцията не прави нищо друго, докато не се извика итератора на следващия next() метод. В този момент, се изпълнява yield 2. Тази способност да спира изпълнението по средата на една функция е изключително мощна и води до някои интересни приложения за генератор функции (обсъдени в раздела "Advanced Iterator Functionality").

Ключовата дума yield може да се използва с всяка стойност или израз, така че можете да напишете генератор функция, която добавя елементи на итератори, без изброяване на елементите един по един. Например, ето един начин, как бихте могли да използвате yield вътре в един for цикъл:

function *createIterator(items) {
    for (let i=0; i < items.length; i++) {
        yield items[i];
    }
}

let iterator = createIterator([1, 2, 3]);

console.log(iterator.next());           // "{ value: 1, done: false }"
console.log(iterator.next());           // "{ value: 2, done: false }"
console.log(iterator.next());           // "{ value: 3, done: false }"
console.log(iterator.next());           // "{ value: undefined, done: true }"

// за всички по-нататъшни извиквания
console.log(iterator.next());           // "{ value: undefined, done: true }"
			

Този пример, подава масив наречен items на createIterator() функцията генератор. Във вътрешността на функцията, for цикъл добива елементите от масива с прогреса на цикъла. Всеки път, където се среща yield, цикъла спира и всеки път, когато next() се извика върху итератора, цикъла продължава със следващия yield.

Генератор функциите са важна характеристика на ECMAScript 6 и тъй като са само функции, те могат да се използват във всички подобни места. Останалата част от този раздел се фокусира върху други полезни начини да се пишат генератори.

worning
Ключовата дума yield може да се използва само в рамките на генератори. Използването на yield на друго място е синтактична грешка, включително и при функции, които са вътре в генератори, като например:
function *createIterator(items) {
    items.forEach(function(item) {
            //syntax error
            yield item + 1;
	});
}       
			
Въпреки че yield е технически вътре в createIterator(), този код е синтактична грешка, защото yield не може да пресече границите на функцията. По този начин, yield е подобен на return, в това, че вложената функция не може да върне стойност за своята съдържаща функция.

Generator Function Expressions

Можете да използвате функционален израз за да създадете генератор, като просто поставите характера звезда (*) между думата function и отварящите скоби.

let createIterator = function *(items) {
    for (let i=0; i < items.length; i++) {
        yield items[i];
    }
};

let iterator = createIterator([1, 2, 3]);

console.log(iterator.next());           // "{ value: 1, done: false }"
console.log(iterator.next());           // "{ value: 2, done: false }"
console.log(iterator.next());           // "{ value: 3, done: false }"
console.log(iterator.next());           // "{ value: undefined, done: true }"

// за всички по-нататъшни извиквания
console.log(iterator.next());           // "{ value: undefined, done: true }"
			

В този код, createIterator() е по-скоро израз на генератор функция, от колкото декларация на функция. Тъй като израза на функцията е анонимен, звездата е между ключовата дума function и отварящите скоби. В противен случай, този пример ще е същия, като предишния вариант на createIterator(), който използва for цикъл.

info
Невъзможно е да се създаде функция стрела, която да е генератор.

Методи на генератор обекта

Тъй като генераторите са само функции, те могат да бъдат добавени към обекти, също. Например, можем да направим генератор в ECMAScript 5 обект с функция израз:

var o = {

    createIterator: function *(items) {
        for (let i=0; i < items.length; i++) {
            yield items[i];
        }
    }
};

let iterator = o.createIterator([1, 2, 3]);
				
			

Можем да използваме ECMAScript 6 метода за краткия запис, като поставим звезда (*) пред името на метода:

var o = {

    *createIterator(items) {
        for (let i=0; i < items.length; i++) {
            yield items[i];
        }
    }
};

let iterator = o.createIterator([1, 2, 3]);
			

Този пример е функционално еквивалентен на предишния, като единствената разлика е използването на различен синтаксис. Тъй като createIterator() метода се дефинира с помощта на краткия запис без ключовата дума function, звездата се поставя непосредствено преди името на метода, макар че може да има празно пространство между звездата и метода.

Iterables и for-of

Тясно свързан с концепцията за итератори е iterable обект със Symbol.iterator свойство. Well-known Symbol.iterator определя функция, която връща итератор за дадения обект. Всички обекти колекции, включително масиви, sets и maps, както и strings, са iterables в ECMAScript 6 и така имат зададен итератор по подразбиране. Iterables са предназначени да бъдат използвани с новото допълнение в ECMAScript: for-of цикъл.

info
Всички итератори създадени от генератори са iterables, тъй като генераторите присвояват свойството Symbol.iterator по подразбиране.

В началото на тази глава, споменах проблема с проследяването на индекс в един for цикъл. Итераторите са първата част от решението на този проблем. Цикъла for-of е втората част: той изцяло премахва необходимостта да се следи индекс в колекция, като ви оставя да се съсредоточите върху работата със съдържанието на колекцията.

Цикъла for-of извиква next() върху iterable всеки път при изпълнението на цикъла и съхранява value от обекта на резултата в променлива. Цикъла продължава този процес, докато свойството done от върнатия обект не стане true. Например:

let values = [1, 2, 3];

for (let num of values) {
    console.log(num);
}
			

Този код извежда следното:

				

1
2
3

Цикъла for-of в този пример първо извиква Symbol.iterator метода върху values масива за извличане на итератор. (Извикването на Symbol.iterator се случва зад кулисите в самия двигател на JavaScript). После, се извиква iterator.next() и value свойството в обекта на резултата, се чете от num. Така променливата num първо е 1, после 2 и на края 3. Когато done в обекта на резултата стане true, цикъла излиза, така че num никога няма да присвои стойността undefined.

Предимството на for-of за разлика от традиционния for цикъл е, че никога не трябва да следите индекса в колекцията. Цикъла for-of обикновено е по-малко склонен към грешки, защото има по-малко условия за следене. Запазването на традиционния for цикъл е за по-сложни условия на контрол.

worning
for-of изявлението ще хвърли грешка, когато се използва върху non-iterable обект, null или undefined.

Достъп до Default Iterator

Можете да използвате Symbol.iterator, за да получите достъп до default итератор на даден обект, като това:

let values = [1, 2, 3];
let iterator = values[Symbol.iterator]();

console.log(iterator.next());           // "{ value: 1, done: false }"
console.log(iterator.next());           // "{ value: 2, done: false }"
console.log(iterator.next());           // "{ value: 3, done: false }"
console.log(iterator.next());           // "{ value: undefined, done: true }"
			

Този код получава default итератор за values и използва това за обхождане на елементите в масива. Това е същия процес, който се случва зад кулисите, когато се използва for-of цикъл.

Тъй като Symbol.iterator определя default итератор, можете да го използвате, за да разберете дали даден обект е iterable, както следва:

function isIterable(object) {
    return typeof object[Symbol.iterator] === "function";
}

console.log(isIterable([1, 2, 3]));     // true
console.log(isIterable("Hello"));       // true
console.log(isIterable(new Map()));     // true
console.log(isIterable(new Set()));     // true
console.log(isIterable(new WeakMap())); // false
console.log(isIterable(new WeakSet())); // false
			

Функцията isIterable() просто проверява дали съществува default итератор в обекта и дали е функция. Това е подобно на проверката, която for-of цикъла прави преди да се изпълни.

Досега примерите в този раздел показват начини за използване Symbol.iterator с вградени iterable видове, но можете да използвате Symbol.iterator свойството да създадете свои собствени iterables.

Създаване на Iterables

Програмно дефинираните обекти не са iterable по подразбиране, но можете да ги направите iterable чрез създаване на Symbol.iterator свойство съдържащо генератор. Например:

let collection = {
    items: [],
    *[Symbol.iterator]() {
        for (let item of this.items) {
            yield item;
        }
    }

};

collection.items.push(1);
collection.items.push(2);
collection.items.push(3)

for (let x of collection) {
    console.log(x);
}

// Output:
// 1
// 2
// 3
			

Първо, примера дефинира default iterator за обект наречен collection. Default итератор се създава със Symbol.iterator метода, който е генератор (обърнете внимание, че звездата все още идва преди името). Генератора след това използва for-of цикъл за обхождане на стойностите в this.items и използва yield за да върне всяка една. Така че, вместо ръчни итерации да определят стойности за default итератора на collection за връщане, обекта collection разчита на default итератора на this.items да свърши работата.

info
"Делегиране на генератори" по-късно в тази глава описва по-различен подход за използването на итератор на друг обект.

Досега видяхте някои употреби на default итератори за масиви, но има много повече вградени итератори в ECMAScript 6 за да направи работата с колекции от данни по-лесна.

Вградени итератори

Итераторите са важна част от ECMAScript 6 и като такива, не е нужно да създавате свои собствени итератори за много вградени типове, които езика включва по подразбиране. Ще ви се наложи да създавате итератори, когато откриете, че няма такива вградени, които да обслужват целите ви, най-често при определяне на ваши собствени обекти или класове. В противен случай, можете да разчитате на вградените итератори да си свършат работата. Може би най-често срещаните употреби на итератори са тези, които работят с колекции.

Итератори за колекции

ECMAScript 6 има три вида обекти колекции: arrays, maps и sets. И трите имат едни и същи вградени итератори, които да ви помогнат да навигирате в тяхното съдържание.

  • entries() - връща итератор, чиито стойности са двойка ключ-стойност.
  • values() - връща итератор, чиито стойности са стойностите на колекцията.
  • keys() - връща итератор, чийто, стойности са ключовете, които се съдържат в колекцията.

Можете да извлечете iterator за колекция, като извикате един от тези методи.

The entries() Iterator

Итератора entries() връща масив с два елемента с всяко извикване на next(). Дву-елементния масив представлява ключ и стойност за всеки елемент в колекцията. За масиви първият елемент е цифров индекс. За sets първия елемент е и стойността (понеже стойността се удвоява, като ключ в sets). За maps първия елемент е ключа.

Ето няколко примера, които използват този итератор:

let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();

data.set("title", "Understanding ECMAScript 6");
data.set("format", "ebook");

for (let entry of colors.entries()) {
    console.log(entry);
}

for (let entry of tracking.entries()) {
    console.log(entry);
}

for (let entry of data.entries()) {
    console.log(entry);
}
			

Извикването на console.log() извежда следното:

					

[0, "red"]
[1, "green"]
[2, "blue"]
[1234, 1234]
[5678, 5678]
[9012, 9012]
["title", "Understanding ECMAScript 6"]
["format", "ebook"]

Този код използва метода entries() върху всеки тип от колекцията за извличане на итератор и го използва в for-of цикъл за обхождане на елементите. От изхода на конзолата, можете да видите ключовете и стойностите, върнати по двойки за всеки елемент.

The values() Iterator

Итератора values() просто връща стойностите, тъй както те се съхраняват в колекцията. Например:

let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();

data.set("title", "Understanding ECMAScript 6");
data.set("format", "ebook");

for (let value of colors.values()) {
    console.log(value);
}

forfor (let value of tracking.values()) {
    console.log(value);
}

for (let value of data.values()) {
    console.log(value);
} 
                

Този пример извежда следното:

					

"red"
"green"
"blue"
1234
5678
9012
"Understanding ECMAScript 6"
"ebook"

Използвайки values() итератор, в този пример, връща точните данни, съдържащи се във всяка колекция без никаква информация за местоположението на данните в колекцията.

The keys() Iterator

Итератора keys() връща всеки ключ присъстващ в колекцията. За масиви, това са само цифровите ключове, той никога не връща други собствени свойства на масива. За sets, ключовете винаги са същите, като стойностите и така keys() и values() връщат същия итератор. За maps, това е всеки уникален ключ. Ето един пример, който показва всичките три:

let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();

data.set("title", "Understanding ECMAScript 6");
data.set("format", "ebook");

for (let key of colors.keys()) {
    console.log(key);
}

for (let key of tracking.keys()) {
    console.log(key);
}

for (let key of data.keys()) {
    console.log(key);
}
				

Този пример извежда следното:

					

0
1
2
1234
5678
9012
"title"
"format"

Итератора keys() извлича всеки ключ в colors, tracking и data и тези ключове се отпечатват вътре в три for-of цикъла. За обекта на масиви, се отпечатват само цифрови индекси, което ще се случи дори ако сте добавили имена на свойствата в масива. Това е различно от начина, по който for-in цикъла работи с масиви, тъй като итерациите на for-in цикъла минават над свойствата, а не само върху цифровите индекси.

Default Iterators for Collection Types

Всеки тип колекция има default итератор, който се използва от for-of всеки път, когато итератора не е изрично посочен. Метода values() е итератора по подразбиране за arrays и sets, а метода entries() е итератор по подразбиране за maps. Тези настройки правят малко по-лесно използването на обекти колекции в for-of. Например, да видим следния пример:

let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();

data.set("title", "Understanding ECMAScript 6");
data.set("format", "ebook");

//също, като използването на colors.values()
for (let value of colors) {
    console.log(value);
}

//също, като използването на tracking.values()
for (let num of tracking) {
    console.log(num);
}

//също, като използването на data.entries()
for (let entry of data) {
    console.log(entry);
}
				

Няма посочен итератор, така че ще се използват функциите на итератор по подразбиране. Итераторите по подразбиране за масиви, sets и maps са предназначени да отразяват как се инициализират тези обекти, така че този код извежда следното:

					

"red"
"green"
"blue"
1234
5678
9012
["title", "Understanding ECMAScript 6"]
["format", "ebook"]

Така масиви и sets връщат техните стойности по подразбиране, докато maps връща същия формат - масив, който може да бъде прехвърлен в Map конструктора. Weak sets и weak maps от друга страна, нямат вградени итератори. Управлението на weak референции означава, че няма начин да се знае точно колко стойности са в тези колекции, което също означава, че няма начин да се обхождат.

Destructuring и for-of цикли

Поведението на конструктора по подразбиране за maps също е полезно, когато се използва в for-of цикли с destructuring, като в този пример:

let data = new Map();

data.set("title", "Understanding ECMAScript 6");
data.set("format", "ebook");

// също, като използването на data.entries()
for (let [key, value] of data) {
    console.log(key + "=" + value);
}
				

Цикъла for-of в този пример, използва destructuring на масив, за присвояване key и value за всеки запис в map. По този начин можете лесно да работите с ключове и стойности едновременно без да се налага да получавате достъп до масива с двата елемента. Използването на destructured на масив в map прави for-of цикъла еднакво полезен, както за maps, така и за arrays и sets.

String итератори

Започвайки от ECMAScript 5, strings бавно се развиват да бъдат масиво-подобни. Например, ECMAScript 5 официално стандартизират скоби нотацията за достъп до характерите в string (text[0] за да получим първия характер и т.н.). За съжаление скоби нотацията работи върху кодови единици, вместо характери, така че не може да се използва за достъп до двубайтови характери правилно. Както този пример показва:

var message = "A ð ®• B";

for (let i=0; i < message.length; i++) {
    console.log(message[i]);
}
				

Този код използва скоби нотация и length свойство за обхождане и отпечатване на string, съдържащ Unicode характери. Изходът е малко неочаквано:

					

A
(blank)
(blank)
(blank)
(blank)
B

Тъй като, двубайтовите характери се третират, като две отделни кодови единици, има четири празни реда между А и В в продукцията.

За щастие, ECMAScript 6 има за цел да поддържа напълно Unicode (виж Глава 2) и default итератора за string е опит да се реши проблема със string итерацията. Като такъв, default итератора за string работи върху характери, а не върху кодови единици. Промяната на този пример, да използва default итератор за string с for-of цикъл има по-добър резултат. Ето и промения код:

var message = "A ð ®• B";

for (let c of message) {
    console.log(c);
}
				

Този код извежда следното:

					

A
(blank)
ð ®•
(blank)
B

Този изход е повече в съответствие с това, което очакваме, когато се работи с характери, цикъла успешно отпечатва Unicode характери, както и всички останали.

NodeList итератори

В Document Object Model (DOM), е типа NodeList, който представлява колекция от елементи в даден документ. За тези, които пишат JavaScript да се изпълнява в уеб-барузъри, разбирането на разликата между NodeList обекти и масиви, винаги е била малко трудна. И двата използват length свойство за да посочи броя на елементите и двата използват скоби нотация за достъп до отделните елементи. Обаче, вътрешно NodeList и масиви се държат съвсем различно и това довежда до голямо объркване.

С добавянето на default итератори в ECMAScript 6, DOM определението за NodeList (включено в спецификацията на HTML, а не само в ECMAScript 6) включва default итератор, който се държи по същия начин, както default итератора за масиви. Това означава, че можете да използвате NodeList в for-of цикъл или на всяко друго място, което използва default итератор върху даден обект. Например:

var divs = document.getElementsByTagName("div");

for (let div of divs) {
    console.log(div.id);
}
				

Този код използва getElementsByTagName() за извличане на NodeList, който представлява съвкупност от всички <div> елементи в документа. Цикъла for-of с итерации минава над всеки елемент и извежда техните ID номера ефективно, правейки кода да изглежда също, както при стандартен масив.

Оператора Spread и Non-Array Iterables

В Глава 7, видяхме как оператора spread (...), може да се използва за превръщане на set в масив. Например:

let set = new Set([1, 2, 3, 3, 3, 4, 5]),
    array = [...set];

console.log(array);            // [1,2,3,4,5]
				

Този код използва оператора spread вътре в масива за да го попълни със стойностите от set. Оператора spread работи на всички iterables и използва default итератор за да определи кои стойности да включи. Всички стойности се четат от итератора и след това се вкарват в масива в реда, по който се връщат от итератора. Този пример работи, защото sets е iterables, но може да работи еднакво добре на всеки iterable. Ето още един пример:

let map = new Map([ ["name", "Nicholas"], ["age", 25]]),
    array = [...map];

console.log(array);     // [ ["name", "Nicholas"], ["age", 25]]
				

Тука, map се превръща в масив от масиви с помощта на оператора spread. Тъй като default итератора за maps връща двойки ключ-стойност, резултата на масива прилича на масива, който се подава на new Map().

Няма ограничения, колко пъти може да използвате оператора spread и можете да го използвате навсякъде, където искате и да вмъкнете няколко елемента от iterable. Тези елементи просто ще се появят в новия масив на мястото на оператора spread. Например:

let smallNumbers = [1, 2, 3],
    bigNumbers = [100, 101, 102],
    allNumbers = [0, ...smallNumbers, ...bigNumbers];

console.log(allNumbers.length);     // 7
console.log(allNumbers);    // [0, 1, 2, 3, 100, 101, 102]
				

Тука, оператора spread се използва за създаване на allNumbers от стойностите на smallNumbers и bigNumbers. Стойностите са поставени в allNumbers в реда, по който са вкарани в масивите, когато allNumbers се създава: 0 е на първо място, следвана от стойностите на smallNumbers, следвани от стойностите на bigNumbers. Имайте в предвид, че оригиналните масиви са непроменени, само техните стойности са копирани в allNumbers.

Тъй като, оператора spread може да се използва на всеки iterable, това е най-лесният начин за превръщане iterable в масив. Можете да превърнете strings в масив от характери (а не кодови единици) и NodeList обекти в браузъра в масив от разклонения.

Сега знаете основите на това, как работят итераторите, включително с for-of и оператора spread и е време да разгледаме някои от по-сложните приложения на итератори.

Напреднала Iterator функционалност

Можете да постигнете много с основните функции на итераторите и удобството за тяхното създаване използвайки генератори. Въпреки това, итераторите са много по-мощни, когато се използват за задачи различни от просто повторение на колекция от стойности. По време на разработването на ECMAScript 6, многото появили се уникални идеи и модели, са причина за добавянето на по-голяма функционалност. Някои от тези допълнения са едва доловими, но когато се използват заедно, можем да постигнем някои интересни взаимодействия.

Подаване на параметри към итераторите

В тази глава видяхме, че итераторите могат да подават стойности чрез next() метода или използвайки yield на генератора. Но също така е възможно да подавате аргументи към итератора чрез next() метода. Когато един аргумент се подава към next() метода, той става стойност на yield изявлението вътре в генератора. Например:

function *createIterator() {
    let first = yield 1;
    let second = yield first + 2;       // 4 + 2
    yield second + 3;                   // 5 + 3
}

let iterator = createIterator();

console.log(iterator.next());           // "{ value: 1, done: false }"
console.log(iterator.next(4));          // "{ value: 6, done: false }"
console.log(iterator.next(5));          // "{ value: 8, done: false }"
console.log(iterator.next());           // "{ value: undefined, done: true }"
				

Първото извикване на next() е специален случай, където всеки аргумент подаден към него се губи. Тъй като аргументите подадени към next() стават стойността върната от yield, трябва да намерим начин за достъп до този аргумент преди първия yield във функцията на генератора. Това не е възможно, така че няма причина да подаваме аргумент при първото извикване на next().

При второто извикване на next(), стойността 4 се подава, като аргумент. 4 е възложена на променливата first вътре във функцията на генератора. В yield изявлението е включено прехвърляне от дясната страна на израза за оценяване от първото извикване на next() и от лявата страна се оценява второто извикване на next() преди функцията да продължи да се изпълнява. Тъй като, към второто извикване на next() е подадено 4, тази стойност се определя на first и след това изпълнението продължава.

Втория yield използва резултата на първия yield и добавя две, което означава, че връща стойност шест. Когато next() се извика трети път, се подава стойността 5, като аргумент. Тази стойност се възлага на променливата second и след това се използва в третото yield изявление, за да върне осем.

Малко по-лесно е да се мисли затова, което се случва, като се вземе в предвид кой код се изпълнява всеки път, когато изпълнението продължава вътре във функцията на генератора. Фигура 8-1 използва цветове, за да покаже кода, който се изпълнява преди yielding.

function *createIterator(){
	next()       let first =yield 1;
	next(4)      let second =yield first + 2;
	next(5)      yield second + 3;
}
    // Фигура 8-1: Изпълнение на кода вътре в генератора
				

Жълтия цвят представлява първото извикване на next() и целия код, който се изпълнява в рамките на генератора, като резултат. Зеления цвят представлява извикването на next(4) и кода който се изпълнява. Розовия цвят представлява извикването на next(5) и кода, който се изпълнява, като резултат. Най-сложната част е кода от дясната страна на всеки израз, изпълнението и спирането преди изпълнението на лявата страна. Това прави дебъгването на генератори малко по-ангажиращо, от колкото на редовни функции.

Досега видяхте, че yield може да действа, като return, когато се подаде стойност към next(). Все пак, това не е само трик при изпълнението вътре в генератора. Можете също така да накарате итератора да хвърля грешки.

Хвърляне на грешки в итераторите

Не само е възможно да подавате данни в итераторите, но също така е възможно да подавате условия за грешки. Итераторите могат да изберат да приложат throw() метод, който възлага на итератора да хвърли грешка, когато се задейства отново. Това е важна възможност за асинхронното програмиране, но също така и гъвкавост вътре в генераторите, където искате да бъде в състояние да се имитират двете стойности за връщане и хвърляне на грешки (двата начина за излизане от функция). Вие може да подадете обект за грешка към throw(), която трябва да бъде хвърлена, когато итератора продължава обработката. Например:

function *createIterator() {
    let first = yield 1;
    let second = yield first + 2;       // yield 4 + 2, след това хвърля грешка
    yield second + 3;                   // никога не се изпълнява
}

let iterator = createIterator();

console.log(iterator.next());                   // "{ value: 1, done: false }"
console.log(iterator.next(4));                  // "{ value: 6, done: false }"
console.log(iterator.throw(new Error("Boom"))); // грешка хвърлена от генератора
				

В този пример, първите два yield израза се оценяват, като нормални, но когато се извика throw() е хвърлена грешка преди да се оцени втория let second. Това на практика спира изпълнението на кода, подобно на директно хвърляне на грешка. Единствената разлика е мястото, където се хвърля грешката. Фигура 8-2 показва кой код се изпълнява на всяка стъпка.

function *createIterator(){
	next()                let first =yield 1;
	next(4)               let second*yield first + 2;
	throw(new Error());   yield + 3;
}		
    // Фигура 8-2: Хвърляне на грешка вътре в генератора
		

В тази фигура, червеният цвят представлява кода, който се изпълнява, когато се извика throw() и червената звезда показва приблизително, кога грешката е хвърлена вътре в генератора. Първите две yield изявления се изпълняват и когато throw() се извика, грешката се хвърля преди изпълнението на всякакъв друг код.

Знаейки това, можем да уловим такава грешка вътре в генератора, с помощта на try-catch блок, като:

function *createIterator() {
    let first = yield 1;
    let second;

    try {
        second = yield first + 2;       // yield 4 + 2, след това хвърля грешка
    } catch (ex) {
        second = 6;                      // на грешката е присвоена различна стойност
    }
    yield second + 3;
}

let iterator = createIterator();

console.log(iterator.next());                   // "{ value: 1, done: false }"
console.log(iterator.next(4));                  // "{ value: 6, done: false }"
console.log(iterator.throw(new Error("Boom"))); // "{ value: 9, done: false }"
console.log(iterator.next());            // "{ value: undefined, done: true }"
				

В този пример, try-catch блока се увива около второто yield изявление. Докато този yield се изпълнява без грешка, грешката се хвърля преди някаква стойност да може да бъде присвоена на second, така че, catch блока му присвоява стойност шест. Изпълнението продължава към следващия yield и връща девет.

Също ще забележите, че се случва нещо интересно - throw() метода връща обект с резултата подобен на този върнат от next(). Тъй като, грешката е уловена във вътрешността на генератора и изпълнението на кода продължава към следващия yield и връща следващата стойност 9.

Това помага да се мисли за next() и throw(), като две инструкции на итератора: next() метода инструктира итератора да продължи изпълнението (евентуално с дадената стойност), а throw() инструктира итератора да продължи изпълнението с хвърляне на грешка. Какво се случва след тази точка, зависи от кода вътре в генератора.

Методите за контрол на изпълнението next() и throw() вътре в итератора са с използване на yield, но може също да използвате return изявлението. Въпреки, че то работи малко по-различно, от колкото в редовни функции, както ще видите в следващия раздел.

Return изявление на генератор

Тъй като генераторите са функции, можете да използвате return изявление за да излезете по-рано и да зададете сойност за връщане на последното извикване на next() метода. В повечето примери в тази глава, последното извикване на next() на итератора връща undefined, но можете да посочите алтернативна стойност, чрез използване на return, както бихте направили във всяка друга функция. В генератора, return показва, че цялата обработка е приключила, така че done свойството е настроено на true и стойността, ако е предвидена, се връща във value полето. Ето един пример, който излиза рано използвайки return:

function *createIterator() {
    yield 1;
   return;
    yield 2;
    yield 3;
}

let iterator = createIterator();

console.log(iterator.next());           // "{ value: 1, done: false }"
console.log(iterator.next());           // "{ value: undefined, done: true }"
				

В този код генератора има yield изявление последвано от return изявление. Return показва, че няма да идват повече стойности и останалата част от yield изявленията не се изпълнява (те са недостижими).

Можете също така да определите стойност за връщане, която да се покаже във value полето на върнатия обект. Например:

function *createIterator() {
    yield 1;
    return 42;
}

let iterator = createIterator();

console.log(iterator.next());           // "{ value: 1, done: false }"
console.log(iterator.next());           // "{ value: 42, done: true }"
console.log(iterator.next());           // "{ value: undefined, done: true }"
				

Тука, върнатата стойност е 42 във value полето от второто извикване на next() (което е първия път, когато done е true). Третото извикване на next() връща обект, чието value свойство отново е undefined. Всяка стойност посочена с return е достъпна само един път във върнатия обект, преди value полето да върне undefined.

info
Оператора spread и for-of цикъла, игнорират всяка стойност определена от return изявлението. Веднага след като видят, че done е true, те спират без да прочетат стойността.

Стойности за връщане на итератора са полезни, когато се делегират генератори.

Делегиране на генератори

В някои случаи е полезно да се комбинират стойностите от два итератора в един. Генератори могат да се делегират към други генератори, използвайки общата специална форма на yield със звезда (*). Както с дефиницията на генератор, няма значение къде се поставя звездата, стига да е между ключовата дума yield и името на функцията генератор. Например:

function *createNumberIterator() {
    yield 1;
    yield 2;
}

function *createColorIterator() {
    yield "red";
    yield "green";
}

function *createCombinedIterator() {
    yield *createNumberIterator();
    yield *createColorIterator();
    yield true;
}

var iterator = createCombinedIterator();

console.log(iterator.next());           // "{ value: 1, done: false }"
console.log(iterator.next());           // "{ value: 2, done: false }"
console.log(iterator.next());           // "{ value: "red", done: false }"
console.log(iterator.next());           // "{ value: "green", done: false }"
console.log(iterator.next());           // "{ value: true, done: false }"
console.log(iterator.next());           // "{ value: undefined, done: true }"
				

В този пример, генератора createCombinedIterator() делегира първо createNumberIterator() и след това createColorIterator(). Върнатия итератор се появява отвън, за да бъде един последователен итератор, който ще произведе всички стойности. Всяко извикване на next() се делегира на подходящ итератор, докато итераторите създадени от createNumberIterator() и createColorIterator() са празни. След финала yield се изпълнява, за да се върне true.

Делегирането на генератори също ви позволява по-нататъшно използване на генератора за връщане на стойности. Това е най-лесният начин за достъп до такива върнати стойности и може да бъде полезно при изпълнение на сложни задачи. Например:

function *createNumberIterator() {
    yield 1;
    yield 2;
    return 3;
}

function *createRepeatingIterator(count) {
    for (let i=0; i < count; i++) {
        yield "repeat";
    }
}

function *createCombinedIterator() {
    let result = yield *createNumberIterator();
    yield *createRepeatingIterator(result);
}

var iterator = createCombinedIterator();

console.log(iterator.next());           // "{ value: 1, done: false }"
console.log(iterator.next());           // "{ value: 2, done: false }"
console.log(iterator.next());           // "{ value: "repeat", done: false }"
console.log(iterator.next());           // "{ value: "repeat", done: false }"
console.log(iterator.next());           // "{ value: "repeat", done: false }"
console.log(iterator.next());           // "{ value: undefined, done: true }"
				

Тука, генератора createCombinedIterator() делегира createNumberIterator() и подава стойност за връщане към result. Тъй като createNumberIterator() съдържа return 3, върнатата стойност е 3. Променливата result се подава към createRepeatingIterator(), като аргумент, който показва, колко пъти да се приложи yield върху този string (в този случай 3 пъти).

Забележете, че стойността 3 не е изход от всяко извикване на next() метода, тя съществува единствено във вътрешността на createCombinedIterator() генератора. Но вие можете изведете тази стойност с добавяне на друго yield изявление, като този пример:

function *createNumberIterator() {
    yield 1;
    yield 2;
    return 3;
}

function *createRepeatingIterator(count) {
    for (let i=0; i < count; i++) {
        yield "repeat";
    }
}

function *createCombinedIterator() {
    let result = yield *createNumberIterator();
    yield result;
    yield *createRepeatingIterator(result);
}

var iterator = createCombinedIterator();

console.log(iterator.next());           // "{ value: 1, done: false }"
console.log(iterator.next());           // "{ value: 2, done: false }"
console.log(iterator.next());           // "{ value: 3, done: false }"
console.log(iterator.next());           // "{ value: "repeat", done: false }"
console.log(iterator.next());           // "{ value: "repeat", done: false }"
console.log(iterator.next());           // "{ value: "repeat", done: false }"
console.log(iterator.next());           // "{ value: undefined, done: true }"
				

В този код, допълнителното yield изявление изрично извежда върнатата стойност от генератора createNumberIterator().

Делегирания генератор използвайки стойността за връщане е много мощен пример, който дава възможност за много интересни възможности, особено когато се използва в асинхронни операции.

info
Можете да използвате yield * директно върху strings (като yield * "hello") и default итератора за string ще бъде използван.

Асинхронен task runner

Много от вълнението около генератори е пряко свързано с използването им в асинхронното програмиране. Асинхронното програмиране в JavaScript е нож с две остриета: много е лесно да се правят прости неща, докато сложните неща остават една поръчка в организацията на кода. Тъй като, генераторите позволяват ефективно да се слага пауза по средата на изпълнението на кода, това отваря много възможности свързани с асинхронната обработка.

Традиционният начин за извършване на асинхронни операции е да се извика функция, която има обратно извикване. Например, помислете за четене на файл от диск в Node.js:

let fs = require("fs");

fs.readFile("config.json", function(err, contents) {
   if(err) {
        throw err;
    }

    doSomethingWith(contents);
    console.log("Done");
});
				

Метода fs.readFile() се извиква с името на файла за четене и функция за обратно извикване. Когато операцията приключи, се извиква функцията за обратно извикване. Обратното извикване проверява дали има грешка и ако няма процеса се връща на contents. Това работи добре, когато имате малък ограничен брой асинхронни задачи за извършване, но се усложнява, когато трябва да влагате още обратни извиквания или последователна поредица от асинхронни задачи. Това е мястото, където генераторите и yield са полезни.

A Simple Task Runner

Понеже yield спира изпълнението и чака следващия метод next() да бъде извикан преди да започне отново, това осигурява начин за изпълнение на асинхронни повиквания, без управление на обратните извиквания. За да започнете ви трябва функция, която да извика генератор и да стартира итератора, също като:

function run(taskDef) {

    //създаване на итератор, предоставен на друго място
    let task = taskDef();

    //стартиране на task
    let result = task.next();

    //рекурсивна функция, която държи извикванията към next()
    function step() {

         // ако има още нещо за правене
        if (!result.done) {
            result = task.next();
            step();
        }
    }

    //старт на процеса
    step();

}
				

Функцията run() приема дефиницията на задачата (функцията генератор), като аргумент. Тя призовава генератора да създаде итератор и го съхранява в task. Променливата task е извън функцията, така че да може да бъде достъпна от други функции (причина, която ще стане ясна по-късно в този раздел). Първото извикване към next() стартира итератора и резултата се съхранява за по-нататъшна употреба. Функцията step() проверява дали result.done е false и ако е така извиква next() преди рекурсивно да извика себе си. Всяко извикване към next() съхранява върнатата стойност в result, който винаги се презаписва, за да съдържа най-новата информация. Първоначалното извикване на step() стартира процеса за търсене на result.done променливата, за да види дали има още нещо за правене.

С това изпълнение на run(), можете да стартирате генератор съдържащ множество yield изявления:

run(function*() {
    console.log(1);
    yield;
    console.log(2);
    yield;
    console.log(3);
});
				

Този пример просто извежда три номера на конзолата, което просто показва, че всички извиквания към next() се правят. Понеже, само получаване на номера няколко пъти не е нещо полезно, следващата стъпка е да се подават стойности във и извън итератора.

Task Running с данни

Най-лесният начин за подаване на данни е чрез подаване на стойността определена от yield на следващото извикване на next() метода. За да направите това, само трябва да подадете result.value, както в този код:

function run(taskDef) {

    //създаване на итератор, предоставен на друго място
    let task = taskDef();

    //стартиране на task
    let result = task.next();

    //рекурсивна функция, която държи извикванията към next()
    function step() {

         // ако има още нещо за правене
        if (!result.done) {
            result = task.next(result.value);
            step();
        }
    }

    //старт на процеса
    step();

}	
				

С тази промяна, когато result.value се подава на next(), като аргумент вече е възможно да се подават данни между yield извикванията, както в този пример:

run(function*() {
    let value = yield 1;
    console.log(value);         // 1

    value = yield value + 3;
    console.log(value);         // 4
}); 
            

Този пример извежда две стойности на конзолата: 1 и 4. Стойността 1 идва от yield 1, като 1 се прехвърля обратно към променливата value. 4 се изчислява чрез добавяне на 3 към value и подава този резултат обратно към value. Сега, когато потока от данни между извикванията на yield е осъществен, трябва само една малка промяна, за да се даде възможност за асинхронни извиквания.

Асинхронен task runner

В предишния пример подаваме статични данни назад и напред между yield извикванията, но очакване на асинхронен процес е малко по-различно. Task runner трябва да знае за обратните извиквания и как да ги използва. И тъй като yield изразите подават техните стойности в task runner, това означава, че всяко извикване на функция трябва да върне стойност, която по някакъв начин показва, че е асинхронна операция, която task runner трябва да се изчака.

Ето един начин за сигнализиране, че стойността е асинхронна операция:

function fetchData() {
    return function(callback) {
        callback(null, "Hi!");
    };
}
				

За целите на този пример, всяка функция трябва да се извика с task runner, който ще върне функция, която изпълнява обратното извикване. Функцията fetchData() връща функция, която приема функцията за обратно извикване, като аргумент. Когато върнатата функция се извика, тя изпълнява функцията за обратно извикване с едно парче данни (string "Hi!"). Аргумента callback трябва да дойде от task runner, за да се гарантира, че това извикване правилно взаимодейства с основния итератор. Докато функцията fetchData() е синхронна, може лесно да се удължи и да стане асинхронна, чрез извикване на обратното извикване с леко закъснение, като например:

function fetchData() {
    return function(callback) {
        setTimeout(function() {
            callback(null, "Hi!");
        }, 50);
    };
}           
            

Тази версия на fetchData() въвежда 50ms закъснение преди да се извика обратното извикване, което показва, че този модел работи еднакво добре, както за синхронен така и за асинхронен код. Просто трябва да се уверите, че всяка функция, която трябва да се извика с помощта на yield следва същия модел.

С добро разбиране на това, как една функция сигнализира, че това е асинхронен процес, можете да модифицирате task runner да вземе този факт в предвид. Винаги result.value е функция и task runner ще я изпълни, вместо просто да подаде тази стойност в next() метода. Това е актуализирания код:

function run(taskDef) {

    // създаване на итератор, предоставен на друго място
    let task = taskDef();

    // стартиране на task
    let result = task.next();

   // рекурсивна функция, която държи извикванията към next()
    function step() {

        // ако има още нещо за правене
        if (!result.done) {
            if (typeof result.value === "function") {
                result.value(function(err, data) {
                    if(err) {
                        result = task.throw(err);
                        return;
                    }

                    result = task.next(data);
                    step();
                });
            } else {
                result = task.next(result.value);
                step();
            }

        }
    }

        // старт на процеса
    step();

}
				

Когато result.value е функция (проверено с === оператора), тя се извиква с функцията за обратно извикване. Тази функция за обратно извикване следва конвенцията на Node.js за подаване на всяка възможна грешка, като първи аргумент (err) и резултатът, като втори аргумент. Ако се открие грешка се извиква task.throw() с обекта за грешка, вместо task.next(), така грешката е хвърлена на правилното място. Ако няма грешка, тогава данните се подават в task.next() и резултата се записва. След това, се извиква step(), за да продължи процеса. Когато result.value не е функция, тя се прехвърля директно в next() метода.

Тази нова версия на task runner е готова за всички асинхронни задачи. За да се четат данни от файл в Node.js, вие трябва да създадете обвивка около fs.readFile(), която връща функция подобна на функцията fetchData() от по-рано в тази секция. Например:

let fs = require("fs");

function readFile(filename) {
    return function(callback) {
        fs.readFile(filename, callback);
    };
}
				

Метода readFile() приема един аргумент, името на файла и връща функция, която извиква обратното извикване. Обратното извикване се подава директно към fs.readFile(), която ще изпълни обратното извикване при завършване. След това може да стартирате задачата с помощта на yield, както следва:

run(function*() {
    let contents = yield readFile("config.json");
    doSomethingWith(contents);
    console.log("Done");
});
				

Този код, извършва асинхронната операция readFile() без да се правят никакви обратни извиквания, видими в основния код. Освен yield, кода изглежда по същия начин, както синхронния код. Докато всички функции изпълняващи асинхронни операции, отговарят на един и същи интерфейс, можете да напишете логика, която се чете като синхронен код.

Разбира се има недостатъци на модела, използван в тези примери, а именно, че не винаги може да сме сигурни, че една функция, която връща функция е асинхронна. За сега, обаче е важно само да разберете теорията зад task running. Има по-мощни начини за правене на асинхронна задача за планиране с помощта на promises, които ще бъдат обхванати по-подробно в Глава 11.