Rajesh Babu

Follow

4. maj, 2018 – 9 min read

Hvis du har været JavaScript-udvikler i de sidste to til fem år, er du helt sikkert stødt på indlæg, der taler om generatorer og iteratorer. Mens Generators og Iterators i sagens natur er forbundet, virker Generators lidt mere skræmmende end de andre.

Generator Turbine

Iteratorer er en implementering af Iterable-objekter såsom maps, arrays og strings, som gør det muligt for os at iterere over dem ved hjælp af next(). De har en lang række anvendelsesmuligheder på tværs af Generators, Observables og Spread operators.

Jeg anbefaler følgende link til dem, der er nye med iteratorer, Guide to Iterators.

For at kontrollere, om dit objekt er i overensstemmelse med iterable-protokollen, skal du kontrollere ved hjælp af den indbyggede Symbol.iterator:

Generators, der blev indført som en del af ES6, har ikke gennemgået nogen ændringer til de yderligere JavaScript-udgivelser, og de er her for at blive længere. Jeg mener virkelig længe! Så der er ikke noget at løbe væk fra det. Selv om ES7 og ES8 har nogle nye opdateringer, har de ikke samme omfang af ændringer, som ES6 havde i forhold til ES5, der så at sige tog JavaScript til det næste niveau.

Ved slutningen af dette indlæg er jeg sikker på, at du vil have en solid forståelse af, hvordan funktionsgeneratorer fungerer. Hvis du er professionel, kan du hjælpe mig med at forbedre indholdet ved at tilføje dine kommentarer i svarene. Hvis du har svært ved at følge med i koden, har jeg også tilføjet en forklaring til det meste af koden for at hjælpe dig med at forstå bedre.

Funktioner i JavaScript, som vi alle ved, “kører indtil return/end”. Generatorfunktioner på den anden side “kører indtil yield/return/end”. I modsætning til normale funktioner returnerer Generatorfunktioner, når de er blevet kaldt, Generatorobjektet, som indeholder hele Generator Iterable, der kan itereres ved hjælp af next()-metoden eller for…of loop.

Hvert next()-kald på generatoren udfører hver kodelinje indtil det næste yield, den støder på, og suspenderer sin udførelse midlertidigt.

Syntaktisk identificeres de med en *, enten funktion* X eller funktion *X, – begge betyder det samme.

Når de er oprettet, returnerer kald af generatorfunktionen Generatorobjektet. Dette generatorobjekt skal tildeles en variabel for at holde styr på de efterfølgende next()-metoder, der kaldes på det selv. Hvis generatoren ikke tilknyttes en variabel, vil den altid kun give udbytte indtil det første yield-udtryk ved hver next().

Generatorfunktionerne er normalt opbygget ved hjælp af yield-udtryk. Hver yield inden for generatorfunktionen er et stoppunkt, før den næste eksekveringscyklus starter. Hver eksekveringscyklus udløses ved hjælp af next()-metoden på generatoren.

Ved hvert next()-kald returnerer yield-udtrykket sin værdi i form af et objekt, der indeholder følgende parametre.

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

  • Værdi – er alt det, der står på højre side af yield-keywordet, det kan være et funktionskald, et objekt eller praktisk talt hvad som helst. For tomme yields er denne værdi udefineret.
  • Done – angiver generatorens status, om den kan udføres yderligere eller ej. Når done returnerer true, betyder det, at funktionen har afsluttet sin kørsel.

(Hvis du føler, at det er lidt over dit hoved, vil du få mere klarhed, når du ser eksemplet nedenfor…)

Grundlæggende generatorfunktion

Bemærk: I ovenstående eksempel udføres den generatorfunktion, der tilgås direkte uden en wrapper, altid kun indtil det første udbytte. Derfor er du pr. definition nødt til at tildele generatoren til en variabel for at kunne iterere korrekt over den.

Livscyklus for en generatorfunktion

Hvor vi går videre, skal vi tage et hurtigt kig på blokdiagrammet for generatorfunktionens livscyklus:

Livscyklus for en generatorfunktion

Hver gang der støder på et yield, returnerer generatorfunktionen et objekt, der indeholder værdien af det fundne yield og status done. På samme måde får vi, når vi støder på et afkast, værdien af afkastet og også færdig status som sand. Når done-status returneres som true, betyder det i bund og grund, at generatorfunktionen har afsluttet sit forløb, og at der ikke er mulighed for yderligere yield.

Alt efter det første return ignoreres, herunder andre yield-udtryk.

Læs videre for at forstå blokdiagrammet bedre.

Tildeling af yield til en variabel

I det foregående kodeeksempel så vi en introduktion til oprettelse af en grundlæggende generator med yield. Og fik det forventede output. Lad os nu antage, at vi tildeler hele yield-udtrykket til en variabel i nedenstående kode.

Assignering af yield til en variabel

Hvad er resultatet af hele yield-udtrykket, der er overført til variablen ? Intet eller Udefineret …

Hvorfor ? Fra og med anden next() erstattes det foregående yield med argumenter, der overgives i den næste funktion. Da vi ikke afleverer noget her i next-metoden, antages det, at hele det “tidligere-yield-udtryk” er udefineret.

Med dette i tankerne skal vi springe til næste afsnit for at forstå mere om afgivelse af argumenter til next()-metoden.

Afgivelse af argumenter til next()-metoden

Med henvisning til blokdiagrammet ovenfor skal vi tale om afgivelse af argumenter til next-funktionen. Dette er en af de vanskeligste dele af hele generatorimplementeringen.

Lad os se på følgende stykke kode, hvor yield tildeles en variabel, men denne gang videregiver vi en værdi i next()-metoden.

Lad os se på nedenstående kode i konsollen. Og forklaringen lige bagefter.

Passing arguments to the next()

Forklaring:

  1. Når vi kalder den første next(20), bliver hver kodelinje indtil det første yield udskrevet. Da vi ikke har noget tidligere yield-udtryk, kasseres denne værdi 20. I output får vi yield-værdien som i*10, hvilket er 100 her. Udførelsestilstanden stopper også med første yield, og const j er endnu ikke indstillet.
  2. Det andet next(10)-kald erstatter hele det første yield-udtryk med 10, forestil dig yield (i * 10) = 10, som fortsætter med at indstille værdien af const j til 50, før den anden yield-værdi returneres. Yield-værdien her er 2 * 50 / 4 = 25.
  3. Den tredje next(5), erstatter hele det andet yield med 5, hvilket bringer værdien af k til 5. Og fortsætter yderligere med at udføre return statement og returnere (x + y + z) => (10 + 50 + 5) = 65 som den endelige yield-værdi sammen med done true.

Dette kan være lidt overvældende for førstegangs læsere, men tag godt 5 minutter til at læse det igen og igen for at forstå det helt.

Passing Yield som et argument i en funktion

Der er n-tal af use-cases omkring yield med hensyn til, hvordan det kan bruges inde i en funktionsgenerator. Lad os se på nedenstående kode for en sådan interessant brug af yield sammen med forklaringen.

Yield som argument for en funktion

Forklaring

  1. Den første next() giver udefineret værdi, fordi yield-udtrykket ikke har nogen værdi.
  2. Den anden next() giver “Jeg er ubrugelig”, den værdi, som blev overgivet. Og forbereder argumentet til funktionskaldet.
  3. Den tredje next(), kalder funktionen med et udefineret argument. Som nævnt ovenfor betyder next()-metoden, der kaldes uden argumenter, i det væsentlige, at hele det foregående yield-udtryk er udefineret. Derfor udskriver dette udefineret og afslutter kørslen.

Yield med et funktionskald

Afhængig af at returnere værdier kan yield også kalde funktioner og returnere værdien eller udskrive den samme. Lad os se på nedenstående kode og forstå den bedre.

Yield ved at kalde en funktion

Koden ovenfor returnerer funktionens return obj som yield-værdi. Og afslutter kørslen ved at sætte undefined til const user.

Yield med løfter

Yield med løfter følger samme fremgangsmåde som funktionskaldet ovenfor, men i stedet for at returnere en værdi fra funktionen returnerer den et løfte, som kan evalueres yderligere for succes eller fiasko. Lad os se på nedenstående kode for at forstå, hvordan det fungerer.

Yield with promises

ApiCall returnerer løfterne som yield-værdi, når de opløses efter 2 sekunder, udskrives den værdi, vi har brug for.

Yield*

Så langt har vi kigget på anvendelsesmulighederne for yield-udtryk, nu skal vi se på et andet udtryk kaldet yield*. Yield*, når det bruges inden for en generatorfunktion, delegerer en anden generatorfunktion. Det betyder ganske enkelt, at det synkront afslutter generatorfunktionen i sit udtryk, før det går videre til næste linje.

Lad os se på koden og forklaringen nedenfor for at forstå bedre. Denne kode er fra MDN-webdokumentationen.

Basic yield*

Forklaring

  1. Det første next()-kald giver en værdi på 1.
  2. Det andet next()-kald er imidlertid et yield*-udtryk, hvilket i sagens natur betyder, at vi vil afslutte en anden generatorfunktion, der er angivet i yield*-udtrykket, før vi fortsætter den aktuelle generatorfunktion.
  3. I dit hoved kan du antage, at ovenstående kode erstattes som nedenstående
function* g2() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
}

Dette vil gå videre for at afslutte generatorkørslen. Der er dog en tydelig evne ved yield*, som du skal huske på, når du bruger return, i næste afsnit.

Yield* med return

Yield* med return opfører sig en smule anderledes end den normale yield*. Når yield* bruges med en return-angivelse, evalueres den til denne værdi, hvilket betyder, at hele yield*-funktionen() bliver lig med den værdi, der returneres fra den tilknyttede generatorfunktion.

Lad os se på koden og forklaringen nedenfor for at forstå den bedre.

Yield* med return

forklaring

  1. I den første next() går vi direkte til yield 1 og returnerer dens værdi.
  2. Den anden next() giver 2.
  3. Den tredje next(), returnerer ‘foo’ og går videre til yield ‘the end’, idet vi tildeler ‘foo’ til const-resultatet undervejs.
  4. Den sidste next() afslutter kørslen.

Yield* med et indbygget iterbart objekt

Der er endnu en interessant yield*-egenskab, der er værd at nævne, i lighed med returværdien, kan yield* også iterere over iterable objekter som Array, String og Map.

Lad os se på, hvordan det fungerer i realtid.

Yield over indbyggede iterables

Her i koden iterer yield* over alle mulige iterable-objekter, der er overgivet som sit udtryk. Jeg tror, at selve koden er selvforklarende.

Bedste praksis

Oven i alt dette kan enhver iterator/generator itereres over en for…of-loop. I lighed med vores next()-metode, som kaldes eksplicit, går for…of-loop internt videre til den næste iteration baseret på yield-keywordet. Og den iterer kun indtil det sidste yield og behandler ikke return statements som next()-metoden.

Du kan verificere det samme i nedenstående kode.

Yield med for…of

Den endelige returværdi udskrives ikke, fordi for…of-sløjfen kun itererererer indtil det sidste yield. Så det hører under bedste praksis at undgå return statements inde i en generatorfunktion, da det vil påvirke funktionens genanvendelighed, når den itereres over en for…of-sløjfe.

Slutning

Jeg håber, at dette dækker de grundlæggende brugssituationer for generatorfunktioner, og jeg håber inderligt, at det gav en bedre forståelse af, hvordan generatorer fungerer i JavaScript ES6 og højere. Hvis du kan lide mit indhold, bedes du efterlade 1, 2, 3 eller endda 50 klapsalver :).

Følg mig venligst på min GitHub-konto for flere JavaScript- og Full-Stack-projekter:

Skriv et svar

Din e-mailadresse vil ikke blive publiceret.