Norsk

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:

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:

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:

  1. Definer en klasse eller et objekt som representerer din egendefinerte datastruktur.
  2. Implementer Symbol.iterator-metoden på klassen eller objektet ditt. Denne metoden skal returnere et iteratorobjekt.
  3. Iteratorobjektet må ha en next()-metode som returnerer et objekt med value- og done-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:

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:

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:

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:

Fordeler med å bruke iteratorprotokollen

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.

Beste praksis

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.

Avmystifisering av JavaScripts iteratorprotokoll og egendefinerte iteratorer | MLOG