En komplett guide til JavaScripts iteratorprotokoll. Lær å lage egendefinerte iteratorer for å forbedre datahåndteringen din.
Avmystifisering av JavaScripts iteratorprotokoll og egendefinerte iteratorer
JavaScripts iteratorprotokoll gir en standardisert måte å traversere datastrukturer på. Å forstå denne protokollen gir utviklere muligheten til å jobbe effektivt med innebygde itererbare objekter som arrayer og strenger, og til å lage sine egne egendefinerte itererbare objekter skreddersydd for spesifikke datastrukturer og applikasjonskrav. Denne guiden gir en omfattende utforskning av iteratorprotokollen og hvordan man implementerer egendefinerte iteratorer.
Hva er iteratorprotokollen?
Iteratorprotokollen definerer hvordan et objekt kan itereres over, det vil si hvordan elementene kan aksesseres sekvensielt. Den består av to deler: Iterable-protokollen og Iterator-protokollen.
Iterable-protokollen
Et objekt anses som Iterable (itererbart) hvis det har en metode med nøkkelen Symbol.iterator
. Denne metoden må returnere et objekt som samsvarer med Iterator-protokollen.
I hovedsak vet et itererbart objekt hvordan det skal lage en iterator for seg selv.
Iterator-protokollen
Iterator-protokollen definerer hvordan man henter verdier fra en sekvens. Et objekt anses som en iterator hvis det har en next()
-metode som returnerer et objekt med to egenskaper:
value
: Den neste verdien i sekvensen.done
: En boolsk verdi som indikerer om iteratoren har nådd slutten av sekvensen. Hvisdone
ertrue
, kanvalue
-egenskapen utelates.
next()
-metoden er arbeidshesten i iteratorprotokollen. Hvert kall til next()
flytter iteratoren fremover og returnerer den neste verdien i sekvensen. Når alle verdiene er returnert, returnerer next()
et objekt med done
satt til true
.
Innebygde itererbare objekter
JavaScript har flere innebygde datastrukturer som er itererbare fra starten av. Disse inkluderer:
- Arrayer
- Strenger
- Maps
- Sets
- Arguments-objektet til en funksjon
- TypedArrays
Disse itererbare objektene kan brukes direkte med for...of
-løkken, spread-syntaksen (...
), og andre konstruksjoner som er avhengige av iteratorprotokollen.
Eksempel med arrayer:
const myArray = ["apple", "banana", "cherry"];
for (const item of myArray) {
console.log(item); // Utskrift: apple, banana, cherry
}
Eksempel med strenger:
const myString = "Hello";
for (const char of myString) {
console.log(char); // Utskrift: H, e, l, l, o
}
for...of
-løkken
for...of
-løkken er en kraftig konstruksjon for å iterere over itererbare objekter. Den håndterer automatisk kompleksiteten i iteratorprotokollen, noe som gjør det enkelt å få tilgang til verdiene i en sekvens.
Syntaksen til for...of
-løkken er:
for (const element of iterable) {
// Kode som skal utføres for hvert element
}
for...of
-løkken henter iteratoren fra det itererbare objektet (ved hjelp av Symbol.iterator
), og kaller gjentatte ganger iteratorens next()
-metode til done
blir true
. For hver iterasjon blir element
-variabelen tildelt value
-egenskapen som returneres av next()
.
Å lage egendefinerte iteratorer
Selv om JavaScript har innebygde itererbare objekter, ligger den virkelige kraften i iteratorprotokollen i muligheten til å definere egendefinerte iteratorer for dine egne datastrukturer. Dette lar deg kontrollere hvordan dataene dine traverseres og aksesseres.
Slik lager du en egendefinert iterator:
- Definer en klasse eller et objekt som representerer din egendefinerte datastruktur.
- Implementer
Symbol.iterator
-metoden på klassen eller objektet ditt. Denne metoden skal returnere et iteratorobjekt. - Iteratorobjektet må ha en
next()
-metode som returnerer et objekt medvalue
- ogdone
-egenskaper.
Eksempel: Lage en iterator for et enkelt tallområde
La oss lage en klasse kalt Range
som representerer et tallområde. Vi vil implementere iteratorprotokollen for å kunne iterere over tallene i området.
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
[Symbol.iterator]() {
let currentValue = this.start;
const that = this; // Fanger opp 'this' for bruk inne i iteratorobjektet
return {
next() {
if (currentValue <= that.end) {
return {
value: currentValue++,
done: false,
};
} else {
return {
value: undefined,
done: true,
};
}
},
};
}
}
const myRange = new Range(1, 5);
for (const number of myRange) {
console.log(number); // Utskrift: 1, 2, 3, 4, 5
}
Forklaring:
Range
-klassen tar imotstart
- ogend
-verdier i sin constructor.Symbol.iterator
-metoden returnerer et iteratorobjekt. Dette iteratorobjektet har sin egen tilstand (currentValue
) og ennext()
-metode.next()
-metoden sjekker omcurrentValue
er innenfor området. Hvis den er det, returnerer den et objekt med den nåværende verdien ogdone
satt tilfalse
. Den øker ogsåcurrentValue
for neste iterasjon.- Når
currentValue
overstigerend
-verdien, returnerernext()
-metoden et objekt meddone
satt tiltrue
. - Merk bruken av
that = this
. Fordinext()
-metoden kalles i et annet scope (avfor...of
-løkken), villethis
inne inext()
ikke referere tilRange
-instansen. For å løse dette, fanger vi oppthis
-verdien (Range
-instansen) ithat
utenfor scopet tilnext()
og bruker deretterthat
inne inext()
.
Eksempel: Lage en iterator for en lenket liste
La oss se på et annet eksempel: å lage en iterator for en lenket liste-datastruktur. En lenket liste er en sekvens av noder, der hver node inneholder en verdi og en referanse (peker) til neste node i listen. Den siste noden i listen har en referanse til null (eller undefined).
class LinkedListNode {
constructor(value, next = null) {
this.value = value;
this.next = next;
}
}
class LinkedList {
constructor() {
this.head = null;
}
append(value) {
const newNode = new LinkedListNode(value);
if (!this.head) {
this.head = newNode;
return;
}
let current = this.head;
while (current.next) {
current = current.next;
}
current.next = newNode;
}
[Symbol.iterator]() {
let current = this.head;
return {
next() {
if (current) {
const value = current.value;
current = current.next;
return {
value: value,
done: false
};
} else {
return {
value: undefined,
done: true
};
}
}
};
}
}
// Eksempel på bruk:
const myList = new LinkedList();
myList.append("London");
myList.append("Paris");
myList.append("Tokyo");
for (const city of myList) {
console.log(city); // Utskrift: London, Paris, Tokyo
}
Forklaring:
LinkedListNode
-klassen representerer en enkelt node i den lenkede listen, og lagrer envalue
og en referanse (next
) til neste node.LinkedList
-klassen representerer selve den lenkede listen. Den inneholder enhead
-egenskap, som peker til den første noden i listen.append()
-metoden legger til nye noder på slutten av listen.Symbol.iterator
-metoden lager og returnerer et iteratorobjekt. Denne iteratoren holder styr på den nåværende noden som besøkes (current
).next()
-metoden sjekker om det finnes en nåværende node (current
er ikke null). Hvis det gjør det, henter den verdien fra den nåværende noden, flyttercurrent
-pekeren til neste node, og returnerer et objekt med verdien ogdone: false
.- Når
current
blir null (som betyr at vi har nådd slutten av listen), returnerernext()
-metoden et objekt meddone: true
.
Generatorfunksjoner
Generatorfunksjoner gir en mer konsis og elegant måte å lage iteratorer på. De bruker yield
-nøkkelordet for å produsere verdier ved behov.
En generatorfunksjon defineres ved hjelp av function*
-syntaksen.
Eksempel: Lage en iterator med en generatorfunksjon
La oss skrive om Range
-iteratoren ved hjelp av en generatorfunksjon:
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
*[Symbol.iterator]() {
for (let i = this.start; i <= this.end; i++) {
yield i;
}
}
}
const myRange = new Range(1, 5);
for (const number of myRange) {
console.log(number); // Utskrift: 1, 2, 3, 4, 5
}
Forklaring:
Symbol.iterator
-metoden er nå en generatorfunksjon (legg merke til*
).- Inne i generatorfunksjonen bruker vi en
for
-løkke for å iterere over tallområdet. yield
-nøkkelordet pauser utførelsen av generatorfunksjonen og returnerer den nåværende verdien (i
). Neste gang iteratorensnext()
-metode kalles, gjenopptas utførelsen der den slapp (etteryield
-setningen).- Når løkken er ferdig, returnerer generatorfunksjonen implisitt
{ value: undefined, done: true }
, som signaliserer slutten på iterasjonen.
Generatorfunksjoner forenkler opprettelsen av iteratorer ved å håndtere next()
-metoden og done
-flagget automatisk.
Eksempel: Generator for Fibonacci-sekvensen
Et annet godt eksempel på bruk av generatorfunksjoner er å generere Fibonacci-sekvensen:
function* fibonacciSequence() {
let a = 0;
let b = 1;
while (true) {
yield a;
[a, b] = [b, a + b]; // Destructuring assignment for samtidig oppdatering
}
}
const fibonacci = fibonacciSequence();
for (let i = 0; i < 10; i++) {
console.log(fibonacci.next().value); // Utskrift: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
}
Forklaring:
fibonacciSequence
-funksjonen er en generatorfunksjon.- Den initialiserer to variabler,
a
ogb
, til de to første tallene i Fibonacci-sekvensen (0 og 1). while (true)
-løkken skaper en uendelig sekvens.yield a
-setningen produserer den nåværende verdien ava
.[a, b] = [b, a + b]
-setningen oppdaterer samtidiga
ogb
til de to neste tallene i sekvensen ved hjelp av destructuring assignment.fibonacci.next().value
-uttrykket henter den neste verdien fra generatoren. Fordi generatoren er uendelig, må du kontrollere hvor mange verdier du henter ut fra den. I dette eksempelet henter vi ut de første 10 verdiene.
Fordeler med å bruke iteratorprotokollen
- Standardisering: Iteratorprotokollen gir en konsistent måte å iterere over forskjellige datastrukturer på.
- Fleksibilitet: Du kan definere egendefinerte iteratorer som er skreddersydd for dine spesifikke behov.
- Lesbarhet:
for...of
-løkken gjør iterasjonskode mer lesbar og konsis. - Effektivitet: Iteratorer kan være "lazy", noe som betyr at de bare genererer verdier når det er nødvendig, noe som kan forbedre ytelsen for store datasett. For eksempel beregner Fibonacci-sekvensgeneratoren ovenfor bare den neste verdien når `next()` kalles.
- Kompatibilitet: Iteratorer fungerer sømløst med andre JavaScript-funksjoner som spread-syntaks og destructuring.
Avanserte iteratorteknikker
Kombinere iteratorer
Du kan kombinere flere iteratorer til én enkelt iterator. Dette er nyttig når du trenger å behandle data fra flere kilder på en enhetlig måte.
function* combineIterators(...iterables) {
for (const iterable of iterables) {
for (const item of iterable) {
yield item;
}
}
}
const array1 = [1, 2, 3];
const array2 = ["a", "b", "c"];
const string1 = "XYZ";
const combined = combineIterators(array1, array2, string1);
for (const value of combined) {
console.log(value); // Utskrift: 1, 2, 3, a, b, c, X, Y, Z
}
I dette eksempelet tar `combineIterators`-funksjonen et hvilket som helst antall itererbare objekter som argumenter. Den itererer over hvert itererbare objekt og yielder hvert element. Resultatet er én enkelt iterator som produserer alle verdiene fra alle input-iteratorene.
Filtrering og transformering av iteratorer
Du kan også lage iteratorer som filtrerer eller transformerer verdiene som produseres av en annen iterator. Dette lar deg behandle data i en pipeline, der du anvender forskjellige operasjoner på hver verdi etter hvert som den genereres.
function* filterIterator(iterable, predicate) {
for (const item of iterable) {
if (predicate(item)) {
yield item;
}
}
}
function* mapIterator(iterable, transform) {
for (const item of iterable) {
yield transform(item);
}
}
const numbers = [1, 2, 3, 4, 5, 6];
const evenNumbers = filterIterator(numbers, (x) => x % 2 === 0);
const squaredEvenNumbers = mapIterator(evenNumbers, (x) => x * x);
for (const value of squaredEvenNumbers) {
console.log(value); // Utskrift: 4, 16, 36
}
Her tar `filterIterator` et itererbart objekt og en predikatfunksjon. Den yielder kun de elementene som predikatet returnerer `true` for. `mapIterator` tar et itererbart objekt og en transformeringsfunksjon. Den yielder resultatet av å anvende transformeringsfunksjonen på hvert element.
Bruksområder i den virkelige verden
Iteratorprotokollen er mye brukt i JavaScript-biblioteker og -rammeverk, og den er verdifull i en rekke virkelige applikasjoner, spesielt når man jobber med store datasett eller asynkrone operasjoner.
- Databehandling: Iteratorer er nyttige for å behandle store datasett effektivt, da de lar deg jobbe med data i biter uten å laste hele datasettet inn i minnet. Tenk deg å parse en stor CSV-fil med kundedata. En iterator kan la deg behandle hver rad uten å laste hele filen inn i minnet på en gang.
- Asynkrone operasjoner: Iteratorer kan brukes til å håndtere asynkrone operasjoner, som å hente data fra et API. Du kan bruke generatorfunksjoner for å pause utførelsen til dataene er tilgjengelige, og deretter gjenoppta med neste verdi.
- Egendefinerte datastrukturer: Iteratorer er essensielle for å lage egendefinerte datastrukturer med spesifikke traverseringskrav. Se for deg en tre-datastruktur. Du kan implementere en egendefinert iterator for å traversere treet i en bestemt rekkefølge (f.eks. dybde-først eller bredde-først).
- Spillutvikling: I spillutvikling kan iteratorer brukes til å administrere spillobjekter, partikkeleffekter og andre dynamiske elementer.
- Brukergrensesnittbiblioteker: Mange UI-biblioteker bruker iteratorer for å effektivt oppdatere og rendre komponenter basert på endringer i underliggende data.
Beste praksis
- Implementer
Symbol.iterator
korrekt: Sørg for at dinSymbol.iterator
-metode returnerer et iteratorobjekt som er i samsvar med iteratorprotokollen. - Håndter
done
-flagget nøyaktig:done
-flagget er avgjørende for å signalisere slutten på iterasjonen. Pass på å sette det korrekt i dinnext()
-metode. - Vurder å bruke generatorfunksjoner: Generatorfunksjoner gir en mer konsis og lesbar måte å lage iteratorer på.
- Unngå sideeffekter i
next()
:next()
-metoden bør primært fokusere på å hente neste verdi og oppdatere iteratorens tilstand. Unngå å utføre komplekse operasjoner eller sideeffekter inne inext()
. - Test iteratorene dine grundig: Test dine egendefinerte iteratorer med forskjellige datasett og scenarioer for å sikre at de oppfører seg korrekt.
Konklusjon
JavaScripts iteratorprotokoll gir en kraftig og fleksibel måte å traversere datastrukturer på. Ved å forstå Iterable- og Iterator-protokollene, og ved å utnytte generatorfunksjoner, kan du lage egendefinerte iteratorer skreddersydd for dine spesifikke behov. Dette lar deg jobbe effektivt med data, forbedre kodens lesbarhet og øke ytelsen til applikasjonene dine. Å mestre iteratorer låser opp en dypere forståelse av JavaScripts kapabiliteter og gir deg muligheten til å skrive mer elegant og effektiv kode.