Rajesh Babu

Följ

4 maj, 2018 – 9 min read

Om du har varit JavaScript-utvecklare de senaste två till fem åren har du definitivt stött på inlägg som handlar om generatorer och iteratorer. Även om generatorer och Iterators är naturligt förknippade med varandra verkar generatorer vara lite mer skrämmande än de andra.

Generator Turbine

Iteratorer är en implementering av Iterable-objekt, t.ex. kartor, matriser och strängar, som gör det möjligt för oss att iterera över dem med next(). De har många olika användningsområden inom generatorer, observabler och spridningsoperatörer.

Jag rekommenderar följande länk för dig som är nybörjare på iteratorer, Guide to Iterators.

För att kontrollera om ditt objekt överensstämmer med iterable-protokollet kan du verifiera det med hjälp av den inbyggda Symbol.iterator:

Generatorer som introducerades som en del av ES6 har inte genomgått några ändringar för de fortsatta JavaScript-versionerna och de är här för att stanna längre. Jag menar riktigt länge! Så det går inte att springa ifrån det. Även om ES7 och ES8 har en del nya uppdateringar har de inte samma omfattning av förändring som ES6 hade från ES5, som tog JavaScript till nästa nivå, så att säga.

I slutet av det här inlägget är jag säker på att du kommer att ha en solid förståelse för hur funktionsgeneratorer fungerar. Om du är ett proffs kan du hjälpa mig att förbättra innehållet genom att lägga till dina kommentarer i svaren. För det fall du har svårt att följa koden har jag också lagt till förklaringar till de flesta av koderna för att hjälpa dig att förstå bättre.

Funktioner i JavaScript, som vi alla vet, ”körs tills retur/slut”. Generatorfunktioner å andra sidan ”körs till yield/return/end”. Till skillnad från normala funktioner returnerar Generator Functions efter att ha anropats Generator Object, som innehåller hela Generator Iterable som kan itereras med hjälp av next() metoden eller for…of loop.

Varje next() anrop på generatorn exekverar varje kodrad fram till nästa yield som den möter och avbryter exekveringen temporärt.

Syntaktiskt identifieras de med en *, antingen funktion* X eller funktion *X, – båda betyder samma sak.

När de har skapats returnerar anrop av generatorfunktionen generatorobjektet. Detta generatorobjekt måste tilldelas en variabel för att hålla reda på de efterföljande next()-metoderna som anropas på sig själv. Om generatorn inte tilldelas en variabel kommer den alltid att ge endast till det första yield-uttrycket vid varje next().

Generatorfunktionerna byggs normalt upp med hjälp av yield-uttryck. Varje yield inom generatorfunktionen är en stoppunkt innan nästa exekveringscykel startar. Varje exekveringscykel utlöses med hjälp av next()-metoden på generatorn.

Vid varje next()-anrop returnerar yield-uttrycket sitt värde i form av ett objekt som innehåller följande parametrar.

{ value: 10, done: false } // assuming that 10 is the value of yield

  • Värde – är allt som står skrivet på den högra sidan av yield-nyckelordet, det kan vara ett funktionsanrop, ett objekt eller praktiskt taget vad som helst. För tomma yields är detta värde odefinierat.
  • Done – anger generatorns status, om den kan exekveras vidare eller inte. När done returnerar true betyder det att funktionen har avslutat sin körning.

(Om du känner att det är lite över ditt huvud kommer du att få mer klarhet när du ser exemplet nedan…)

Basisk generatorfunktion

Note: I exemplet ovan utförs generatorfunktionen som nås direkt utan en omslagsfunktion alltid endast fram till den första avkastningen. Därför måste du per definition tilldela generatorn till en variabel för att korrekt iterera över den.

Livscykel för en generatorfunktion

För att gå vidare ska vi ta en snabb titt på blockdiagrammet för generatorfunktionens livscykel:

Livscykel för en generatorfunktion

Varje gång en yield påträffas returnerar generatorfunktionen ett objekt som innehåller värdet på den påträffade yielden och statusen done. På samma sätt får vi, när en avkastning påträffas, värdet av avkastningen och även done status som true. När done status returneras som true betyder det i huvudsak att generatorfunktionen har avslutat sin körning och att ingen ytterligare yield är möjlig.

Allt efter den första returen ignoreras, inklusive andra yield-uttryck.

Läs vidare för att förstå blockdiagrammet bättre.

Assignering av yield till en variabel

I det föregående kodprovet såg vi en introduktion till hur man skapar en grundläggande generator med en yield. Och fick det förväntade resultatet. Låt oss nu anta att vi tilldelar hela yield-uttrycket till en variabel i koden nedan.

Assignering av yield till en variabel

Vad blir resultatet av hela yield-uttrycket som skickas till variabeln ? Ingenting eller odefinierat …

Varför? Från och med andra next() ersätts det föregående yield-uttrycket med argument som överförs till nästa funktion. Eftersom vi inte lämnar över något här i nästa metod, antas det att hela det tidigare yield-uttrycket är odefinierat.

Med detta i åtanke går vi vidare till nästa avsnitt för att förstå mer om att lämna över argument till nästa()-metoden.

Lämna över argument till nästa()-metoden

Med hänvisning till blockdiagrammet ovan ska vi tala om att lämna över argument till nästa funktion. Detta är en av de knepigaste delarna av hela generatorimplementationen.

Låt oss betrakta följande kodstycke, där yield tilldelas en variabel, men den här gången passerar vi ett värde i next()-metoden.

Låt oss titta på koden nedan i konsolen. Och förklaringen direkt efter det.

Passing arguments to the next()

Förklaring:

  1. När vi anropar den första next(20) skrivs varje kodrad fram till den första yield ut. Eftersom vi inte har något tidigare yield-uttryck kastas detta värde 20 bort. I utmatningen får vi yieldvärdet som i*10, vilket är 100 här. Dessutom stannar exekveringen med första yield och const j är ännu inte inställd.
  2. Det andra anropet next(10) ersätter hela det första yield-uttrycket med 10, tänk dig yield (i * 10) = 10, vilket fortsätter med att ställa in värdet på const j till 50 innan det andra yield-värdet returneras. Yield-värdet här är 2 * 50 / 4 = 25.
  3. Den tredje next(5), ersätter hela det andra yield-uttrycket med 5, vilket sätter värdet på k till 5. Och fortsätter vidare att utföra return statement och returnerar (x + y + z) => (10 + 50 + 5) = 65 som slutvärde tillsammans med done true.

Detta kan vara lite överväldigande för förstagångsläsare, men ta drygt 5 minuter på dig att läsa det om och om igen för att förstå helt och hållet.

Passing Yield as an Argument of a Function

Det finns n-antal användningsfall kring yield när det gäller hur det kan användas inuti en funktionsgenerator. Låt oss titta på koden nedan för en sådan intressant användning av yield, tillsammans med förklaringen.

Yield som argument för en funktion

Förklaring

  1. Den första next() ger odefinierat värde eftersom yield-uttrycket saknar värde.
  2. Den andra next() ger ”I am usless”, det värde som överlämnades. Och förbereder argumentet för funktionsanrop.
  3. Den tredje next(), anropar funktionen med ett odefinierat argument. Som nämnts ovan innebär metoden next() som anropas utan argument i princip att hela det föregående yield-uttrycket är odefinierat. Därför skrivs detta ut odefinierat och avslutar körningen.

Yield med ett funktionsanrop

Avse att returnera värden kan yield också anropa funktioner och returnera värdet eller skriva ut detsamma. Låt oss titta på koden nedan för att förstå bättre.

Yield som anropar en funktion

Koden ovan returnerar funktionens retur obj som yield-värde. Och avslutar körningen genom att ställa in undefined till const user.

Yield med löften

Yield med löften följer samma tillvägagångssätt som funktionsanropet ovan, i stället för att returnera ett värde från funktionen returneras ett löfte som kan utvärderas vidare för att avgöra om det är lyckat eller misslyckat. Låt oss titta på koden nedan för att förstå hur det fungerar.

Yield med löften

ApiCall returnerar löftena som yield-värde, när de löses upp efter 2 sekunder skrivs det värde vi behöver ut.

Yield*

So långt har vi tittat på användningsområdena för yield-uttryck, nu ska vi titta på ett annat uttryck som kallas yield*. Yield* när det används inuti en generatorfunktion delegerar en annan generatorfunktion. Enkelt uttryckt avslutar det synkront generatorfunktionen i sitt uttryck innan det går vidare till nästa rad.

Låt oss titta på koden och förklaringen nedan för att förstå bättre. Koden kommer från MDN:s webbdokumentation.

Basic yield*

Förklaring

  1. Det första anropet av next() ger ett värde på 1.
  2. Det andra next() anropet är dock ett yield*-uttryck, vilket i sig innebär att vi kommer att slutföra en annan generatorfunktion som anges i yield*-uttrycket innan vi fortsätter den aktuella generatorfunktionen.
  3. I ditt sinne kan du anta att koden ovan ersätts som den nedan
function* g2() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
}

Detta kommer att fortsätta för att avsluta generatorkörningen. Det finns dock en distinkt förmåga hos yield* som du bör ha i åtanke när du använder return, i nästa avsnitt.

Yield* med return

Yield* med return beter sig lite annorlunda än den normala yield*. När yield* används med en retursats utvärderas den till det värdet, vilket innebär att hela yield*-funktionen() blir lika med det värde som returneras från den tillhörande generatorfunktionen.

Låt oss titta på koden och förklaringen nedan för att förstå den bättre.

Yield* med retur

Förklaring

  1. I den första next() går vi direkt till yield 1 och returnerar dess värde.
  2. Den andra next() ger 2.
  3. Den tredje next(), returnerar ”foo” och går vidare till yield the ”the end” och tilldelar ”foo” till const-resultatet på vägen.
  4. Den sista next() avslutar körningen.

Yield* med ett inbyggt iterbart objekt

Det finns ytterligare en intressant yield*-egenskap som är värd att nämna, i likhet med returvärdet kan yield* också iterera över iterbara objekt som Array, String och Map.

Låt oss titta på hur det fungerar i realtid.

Yield över inbyggda iterables

Här i koden itererar yield* över alla möjliga iterable objekt som skickas som sitt uttryck. Jag antar att själva koden är självförklarande.

Bästa praxis

Ovanpå allt detta kan varje iterator/generator itereras över en for…of-slinga. I likhet med vår next()-metod som anropas explicit går for…of-slingan internt vidare till nästa iteration baserat på nyckelordet yield. Och den itererar bara fram till den sista yield och behandlar inte return statements som next() metoden.

Du kan verifiera samma sak i koden nedan.

Yield med for…of

Det slutgiltiga returvärdet skrivs inte ut, eftersom for…of-slingan itererar bara fram till den sista yield. Så det är bästa praxis att undvika return statements i en generatorfunktion eftersom det skulle påverka funktionens återanvändbarhet när den itereras över en for…of-slinga.

Slutsats

Jag hoppas att det här täcker de grundläggande användningsområdena för generatorfunktioner och jag hoppas verkligen att det gav en bättre förståelse för hur generatorer fungerar i JavaScript ES6 och senare. Om du gillar mitt innehåll så lämna gärna 1, 2, 3 eller till och med 50 klappar :).

Följ mig på mitt GitHub-konto för fler JavaScript- och Full-Stack-projekt:

Lämna ett svar

Din e-postadress kommer inte publiceras.