Författaren har valt Open Internet/Free Speech Fund för att få en donation som en del av programmet Write for DOnations.
Introduktion
I ECMAScript 2015 infördes generatorer i JavaScript-språket. En generator är en process som kan pausas och återupptas och som kan ge flera värden. En generator i JavaScript består av en generatorfunktion som returnerar ett iterbart Generator
objekt.
Generatorer kan upprätthålla tillstånd, vilket ger ett effektivt sätt att skapa iteratorer, och kan hantera oändliga dataströmmar, vilket kan användas för att implementera oändlig rullning på frontenden av en webbapplikation, för att arbeta med ljudvågsdata, med mera. Dessutom kan generatorer, när de används med Promises, efterlikna async/await
-funktionen, vilket gör att vi kan hantera asynkron kod på ett enklare och mer lättläst sätt. Även om async/await
är ett mer utbrett sätt att hantera vanliga, enkla asynkrona användningsfall, som att hämta data från ett API, har generatorer mer avancerade funktioner som gör att det lönar sig att lära sig använda dem.
I den här artikeln tar vi upp hur man skapar generatorfunktioner, hur man itererar över Generator
-objekt, skillnaden mellan yield
och return
inuti en generator och andra aspekter av att arbeta med generatorer.
Generatorfunktioner
En generatorfunktion är en funktion som returnerar ett Generator
-objekt och definieras av nyckelordet function
följt av en asterisk (*
), enligt följande:
// Generator function declarationfunction* generatorFunction() {}
Ibland ser du asterisken bredvid funktionsnamnet, i motsats till funktionsnyckelordet, till exempel function *generatorFunction()
. Detta fungerar på samma sätt, men function*
är en mer allmänt accepterad syntax.
Generatorfunktioner kan också definieras i ett uttryck, som reguljära funktioner:
// Generator function expressionconst generatorFunction = function*() {}
Generatorer kan till och med vara metoder för ett objekt eller en klass:
Exemplen i hela den här artikeln kommer att använda syntaxen för deklaration av generatorfunktioner.
Notera: Till skillnad från vanliga funktioner kan generatorer inte konstrueras med nyckelordet new
, och de kan inte heller användas tillsammans med pilfunktioner.
Nu när du vet hur man deklarerar generatorfunktioner, ska vi titta på de iterabla Generator
-objekten som de returnerar.
Generatorobjekt
Traditionellt sett körs funktioner i JavaScript till slut, och om man anropar en funktion returneras ett värde när den kommer fram till nyckelordet return
. Om nyckelordet return
utelämnas returnerar en funktion implicit undefined
.
I följande kod deklarerar vi till exempel en sum()
-funktion som returnerar ett värde som är summan av två heltalsargument:
// A regular function that sums two valuesfunction sum(a, b) { return a + b}
Ansamlingen av funktionen returnerar ett värde som är summan av argumenten:
const value = sum(5, 6) // 11
En generatorfunktion returnerar dock inte ett värde omedelbart utan returnerar i stället ett iterbart Generator
-objekt. I följande exempel deklarerar vi en funktion och ger den ett enda returvärde, som en vanlig funktion:
// Declare a generator function with a single return valuefunction* generatorFunction() { return 'Hello, Generator!'}
När vi anropar generatorfunktionen returnerar den Generator
-objektet, som vi kan tilldela en variabel:
// Assign the Generator object to generatorconst generator = generatorFunction()
Om det här var en vanlig funktion skulle vi förvänta oss att generator
skulle ge oss strängen som returneras i funktionen. Men vad vi faktiskt får är ett objekt i ett suspended
-tillstånd. Att anropa generator
kommer därför att ge utdata som liknar följande:
Det Generator
objekt som returneras av funktionen är en iterator. En iterator är ett objekt som har en next()
metod tillgänglig, som används för att iterera genom en sekvens av värden. next()
-metoden returnerar ett objekt med value
och done
egenskaper. value
representerar det returnerade värdet och done
anger om iteratorn har gått igenom alla sina värden eller inte.
Med detta i åtanke kan vi anropa next()
på vår generator
och få fram det aktuella värdet och tillståndet för iteratorn:
// Call the next method on the Generator objectgenerator.next()
Detta ger följande utdata:
Output{value: "Hello, Generator!", done: true}
Värdet som returneras efter att ha anropat next()
är Hello, Generator!
, och tillståndet för done
är true
, eftersom detta värde kom från en return
som stängde iteratorn. Eftersom iteratorn är avslutad ändras generatorfunktionens status från suspended
till closed
. Att anropa generator
igen kommer att ge följande:
OutputgeneratorFunction {<closed>}
Från och med nu har vi bara visat hur en generatorfunktion kan vara ett mer komplext sätt att få fram return
-värdet för en funktion. Men generatorfunktioner har också unika egenskaper som skiljer dem från vanliga funktioner. I nästa avsnitt lär vi oss om yield
-operatören och ser hur en generator kan pausa och återuppta utförandet.
yield-operatorer
Generatorer introducerar ett nytt nyckelord i JavaScript: yield
. yield
kan pausa en generatorfunktion och returnera det värde som följer efter yield
, vilket ger ett lättviktigt sätt att iterera genom värden.
I det här exemplet pausar vi generatorfunktionen tre gånger med olika värden och returnerar ett värde i slutet. Sedan tilldelar vi vårt Generator
-objekt till generator
-variabeln.
Nu, när vi anropar next()
på generatorfunktionen, kommer den att pausa varje gång den stöter på yield
. done
kommer att sättas till false
efter varje yield
, vilket indikerar att generatorn inte är klar. När den möter en return
, eller när det inte finns några fler yield
som möts i funktionen, kommer done
att slå om till true
, och generatorn kommer att vara klar.
Använd metoden next()
fyra gånger i rad:
// Call next four timesgenerator.next()generator.next()generator.next()generator.next()
Dessa kommer att ge följande fyra rader av utdata i tur och ordning:
Output{value: "Neo", done: false}{value: "Morpheus", done: false}{value: "Trinity", done: false}{value: "The Oracle", done: true}
Bemärk att en generator inte kräver ett return
; om det utelämnas kommer den sista iterationen att returnera {value: undefined, done: true}
, liksom alla efterföljande anrop av next()
efter det att en generator är klar.
Iterera över en generator
Med hjälp av next()
-metoden itererade vi manuellt genom Generator
-objektet och tog emot alla value
– och done
-egenskaper för det fullständiga objektet. Precis som Array
, Map
och Set
följer dock en Generator
iterationsprotokollet och kan itereras igenom med for...of
:
// Iterate over Generator objectfor (const value of generator) { console.log(value)}
Detta ger följande:
OutputNeoMorpheusTrinity
Spridningsoperatorn kan också användas för att tilldela värdena i en Generator
till en array.
// Create an array from the values of a Generator objectconst values = console.log(values)
Detta kommer att ge följande array:
Output(3)
Både spread och for...of
kommer inte att faktorisera return
i värdena (i det här fallet skulle det ha varit 'The Oracle'
).
Notera: Även om båda dessa metoder är effektiva för att arbeta med ändliga generatorer, kommer det inte att vara möjligt att använda spread eller for...of
direkt utan att skapa en oändlig slinga om en generator har att göra med en oändlig dataström.
Slutar en generator
Som vi har sett kan en generator få sin done
-egenskap inställd på true
och sin status inställd på closed
genom att iterera genom alla sina värden. Det finns ytterligare två sätt att omedelbart avbryta en generator: med metoden return()
och med metoden throw()
.
Med return()
kan generatorn avslutas när som helst, precis som om ett return
-uttalande hade funnits i funktionskroppen. Du kan lämna ett argument i return()
, eller lämna det tomt för ett odefinierat värde.
För att demonstrera return()
skapar vi en generator med några yield
-värden men utan return
i funktionsdefinitionen:
function* generatorFunction() { yield 'Neo' yield 'Morpheus' yield 'Trinity'}const generator = generatorFunction()
Den första next()
ger oss 'Neo'
, med done
inställt på false
. Om vi åberopar en return()
-metod på Generator
-objektet direkt efter det får vi nu det överlämnade värdet och done
är satt till true
. Varje ytterligare anrop till next()
kommer att ge det standardiserade avslutade generatorsvaret med ett odefinierat värde.
För att demonstrera detta kör du följande tre metoder på generator
:
generator.next()generator.return('There is no spoon!')generator.next()
Detta kommer att ge följande tre resultat:
Output{value: "Neo", done: false}{value: "There is no spoon!", done: true}{value: undefined, done: true}
Metoden return()
tvingade Generator
-objektet att komplettera och ignorera alla andra yield
nyckelord. Detta är särskilt användbart i asynkron programmering när du behöver göra funktioner avbrytbara, t.ex. avbryta en webbförfrågan när en användare vill utföra en annan åtgärd, eftersom det inte är möjligt att direkt avbryta en Promise.
Om kroppen av en generatorfunktion har ett sätt att fånga upp och hantera fel, kan du använda throw()
-metoden för att kasta in ett fel i generatorn. Detta startar generatorn, kastar in felet och avslutar generatorn.
För att demonstrera detta kommer vi att placera en try...catch
inne i generatorfunktionens kropp och logga ett fel om ett sådant hittas:
Nu kommer vi att köra next()
-metoden, följt av throw()
:
generator.next()generator.throw(new Error('Agent Smith!'))
Detta ger följande utdata:
Output{value: "Neo", done: false}Error: Agent Smith!{value: undefined, done: true}
Med hjälp av throw()
injicerade vi ett fel i generatorn, vilket fångades upp av try...catch
och loggades till konsolen.
Metoder och tillstånd för generatorobjekt
I följande tabell visas en lista över metoder som kan användas på Generator
-objekt:
Metod | Beskrivning |
---|---|
next() |
Returnerar nästa värde i en generator |
return() |
Returnerar ett värde i en generator och avslutar generatorn |
throw() |
Gör ett fel och avslutar generatorn |
Nästa tabell listar de möjliga tillstånden för ett Generator
-objekt:
Status | Beskrivning |
---|---|
suspended |
Generatorn har stoppat exekveringen men har inte avslutats |
closed |
Generatorn har avslutats genom att antingen stöta på ett fel, återgå, eller genom att iterera genom alla värden |
yield Delegation
Inom den vanliga yield
-operatören kan generatorer också använda yield*
-uttrycket för att delegera ytterligare värden till en annan generator. När yield*
påträffas i en generator kommer den att gå in i den delegerade generatorn och börja iterera genom alla yield
s tills den generatorn är stängd. Detta kan användas för att separera olika generatorfunktioner för att semantiskt organisera din kod, samtidigt som alla deras yield
s kan itereras i rätt ordning.
För att demonstrera kan vi skapa två generatorfunktioner, varav den ena yield*
opererar på den andra:
Nästan, låt oss iterera genom begin()
generatorfunktionen:
// Iterate through the outer generatorconst generator = begin()for (const value of generator) { console.log(value)}
Detta ger följande värden i den ordning de genereras:
Output1234
Den yttre generatorn gav värdena 1
och 2
och delegerade sedan till den andra generatorn med yield*
, som gav 3
och 4
.
yield*
kan också delegera till vilket objekt som helst som är iterbart, till exempel en Array eller en Map. Yield-delegering kan vara till hjälp för att organisera kod, eftersom alla funktioner inom en generator som ville använda yield
också måste vara en generator.
Oändliga dataströmmar
En av de användbara aspekterna av generatorer är förmågan att arbeta med oändliga dataströmmar och samlingar. Detta kan demonstreras genom att skapa en oändlig slinga inuti en generatorfunktion som ökar ett tal med ett.
I följande kodblock definierar vi denna generatorfunktion och startar sedan generatorn:
Nu itererar vi genom värdena med hjälp av next()
:
// Iterate through the valuescounter.next()counter.next()counter.next()counter.next()
Detta ger följande utdata:
Output{value: 0, done: false}{value: 1, done: false}{value: 2, done: false}{value: 3, done: false}
Funktionen returnerar successiva värden i den oändliga slingan samtidigt som done
-egenskapen förblir false
, vilket säkerställer att den inte avslutas.
Med generatorer behöver du inte oroa dig för att skapa en oändlig slinga, eftersom du kan stoppa och återuppta utförandet när du vill. Du måste dock fortfarande vara försiktig med hur du anropar generatorn. Om du använder spread eller for...of
på en oändlig dataström kommer du fortfarande att iterera över en oändlig slinga på en gång, vilket kommer att få miljön att krascha.
För ett mer komplext exempel på en oändlig dataström kan vi skapa en Fibonacci-generatorfunktion. Fibonacci-sekvensen, som kontinuerligt adderar de två föregående värdena tillsammans, kan skrivas med hjälp av en oändlig slinga inom en generator enligt följande:
För att testa detta kan vi slinga oss igenom ett ändligt antal och skriva ut Fibonacci-sekvensen till konsolen.
// Print the first 10 values of fibonacciconst fib = fibonacci()for (let i = 0; i < 10; i++) { console.log(fib.next().value)}
Detta kommer att ge följande:
Output0112358132134
Förmågan att arbeta med oändliga datamängder är en del av det som gör generatorer så kraftfulla. Detta kan vara användbart för exempel som att implementera oändlig rullning i frontend av ett webbprogram.
Passing Values in Generators
Under hela den här artikeln har vi använt generatorer som iteratorer, och vi har gett värden i varje iteration. Förutom att producera värden kan generatorer också konsumera värden från next()
. I det här fallet kommer yield
att innehålla ett värde.
Det är viktigt att notera att den första next()
som anropas inte kommer att överlämna ett värde, utan endast starta generatorn. För att demonstrera detta kan vi logga värdet på yield
och anropa next()
några gånger med vissa värden.
Detta kommer att ge följande utdata:
Output100200{value: "The end", done: true}
Det är också möjligt att sätta igång generatorn med ett startvärde. I följande exempel gör vi en for
-slinga och skickar varje värde till next()
-metoden, men skickar också ett argument till den initiala funktionen:
Vi hämtar värdet från next()
och ger ett nytt värde till nästa iteration, vilket är det föregående värdet gånger tio. Detta ger följande:
Output010203040
Ett annat sätt att hantera uppstarten av en generator är att linda in generatorn i en funktion som alltid anropar next()
en gång innan den gör något annat.
async/await med generatorer
En asynkron funktion är en typ av funktion som finns tillgänglig i ES6+ JavaScript och som gör det lättare att förstå att arbeta med asynkrona data genom att få det att framstå som synkront. Generatorer har ett mer omfattande utbud av funktioner än asynkrona funktioner, men kan replikera liknande beteenden. Genom att implementera asynkron programmering på det här sättet kan du öka flexibiliteten i din kod.
I det här avsnittet visar vi ett exempel på att reproducera async
/await
med hjälp av generatorer.
Låt oss bygga en asynkron funktion som använder API:et Fetch för att hämta data från API:et JSONPlaceholder (som tillhandahåller JSON-exemplariska data i testsyfte) och som loggar svaret till konsolen.
Startar du med att definiera en asynkron funktion kallad getUsers
som hämtar data från API:et och returnerar en array av objekt, anropar du sedan getUsers
:
Detta kommer att ge JSON-data som liknar följande:
Med hjälp av generatorer kan vi skapa något nästan identiskt som inte använder nyckelorden async
/await
. I stället kommer den att använda en ny funktion som vi skapar och yield
-värden i stället för await
-löften.
I följande kodblock definierar vi en funktion som heter getUsers
som använder vår nya asyncAlt
-funktion (som vi kommer att skriva senare) för att efterlikna async
/await
.
Som vi kan se är det nästan identiskt med async
/await
-implementationen, förutom att det är en generatorfunktion som skickas in och ger värden.
Nu kan vi skapa en asyncAlt
-funktion som liknar en asynkron funktion. asyncAlt
har en generatorfunktion som parameter, vilket är vår funktion som ger de löften som fetch
returnerar. asyncAlt
returnerar själv en funktion och löser upp varje löfte som den hittar fram till det sista:
Detta ger samma utdata som async
/await
-versionen:
Bemärk att den här implementationen är till för att demonstrera hur generatorer kan användas i stället för async
/await
, och att den inte är en produktionsklar design. Den har ingen felhantering inställd och har inte heller möjlighet att skicka in parametrar i de givna värdena. Även om den här metoden kan ge flexibilitet till din kod är async/await
ofta ett bättre val, eftersom den abstraherar implementeringsdetaljer och låter dig fokusera på att skriva produktiv kod.
Slutsats
Generatorer är processer som kan stoppa och återuppta exekveringen. De är en kraftfull och mångsidig funktion i JavaScript, även om de inte används så ofta. I den här handledningen lärde vi oss om generatorfunktioner och generatorobjekt, metoder som är tillgängliga för generatorer, operatörerna yield
och yield*
samt generatorer som används med ändliga och oändliga datamängder. Vi utforskade också ett sätt att implementera asynkron kod utan inbäddade callbacks eller långa löfteskedjor.
Om du vill lära dig mer om JavaScript-syntax kan du ta en titt på våra handledningar Förstå detta, binda, anropa och tillämpa i JavaScript och Förstå Map- och Set-objekt i JavaScript.