Oppdag kraften i den nye JavaScript Iterator `scan`-hjelperen. Lær hvordan den revolusjonerer strømprosessering, tilstandshåndtering og dataaggregering utover `reduce`.
JavaScript Iterator `scan`: Det manglende leddet for akkumulativ strømprosessering
I det stadig utviklende landskapet av moderne webutvikling er data konge. Vi håndterer kontinuerlig strømmer av informasjon: brukerhendelser, sanntids-API-responser, store datasett og mer. Å behandle disse dataene effektivt og deklarativt er en overordnet utfordring. I årevis har JavaScript-utviklere stolt på den kraftige Array.prototype.reduce-metoden for å destillere en array ned til en enkelt verdi. Men hva om du trenger å se reisen, ikke bare destinasjonen? Hva om du trenger å observere hvert mellomtrinn i en akkumulering?
Det er her et nytt, kraftig verktøy kommer inn på scenen: Iterator scan-hjelperen. Som en del av TC39 Iterator Helpers-forslaget, som for tiden er på trinn 3, er scan satt til å revolusjonere hvordan vi håndterer sekvensielle og strømbaserte data i JavaScript. Det er den funksjonelle, elegante motparten til reduce som gir hele historien til en operasjon.
Denne omfattende guiden vil ta deg med på et dypdykk i scan-metoden. Vi vil utforske problemene den løser, syntaksen, de kraftige brukstilfellene fra enkle løpende totaler til kompleks tilstandshåndtering, og hvordan den passer inn i det bredere økosystemet av moderne, minneeffektiv JavaScript.
Den kjente utfordringen: Begrensningene til `reduce`
For å virkelig sette pris på hva scan bringer til bordet, la oss først se på et vanlig scenario. Tenk deg at du har en strøm av finansielle transaksjoner, og du må beregne den løpende saldoen etter hver transaksjon. Dataene kan se slik ut:
const transactions = [100, -20, 50, -10, 75]; // Innskudd og uttak
Hvis du bare ønsket den endelige saldoen, er Array.prototype.reduce det perfekte verktøyet:
const finalBalance = transactions.reduce((balance, transaction) => balance + transaction, 0);
console.log(finalBalance); // Output: 195
Dette er konsist og effektivt. Men hva om du trenger å plotte kontosaldoen over tid på et diagram? Du trenger saldoen etter hver transaksjon: [100, 80, 130, 120, 195]. reduce-metoden skjuler disse mellomtrinnene fra oss; den gir bare det endelige resultatet.
Så, hvordan ville vi løst dette tradisjonelt? Vi ville sannsynligvis falle tilbake på en manuell løkke med en ekstern tilstandsvariabel:
const transactions = [100, -20, 50, -10, 75];
const runningBalances = [];
let currentBalance = 0;
for (const transaction of transactions) {
currentBalance += transaction;
runningBalances.push(currentBalance);
}
console.log(runningBalances); // Output: [100, 80, 130, 120, 195]
Dette fungerer, men det har flere ulemper:
- Imperativ stil: Det er mindre deklarativt. Vi administrerer manuelt tilstanden (
currentBalance) og resultat-samlingen (runningBalances). - Tilstandsfølsom og verbose: Det krever administrering av mutable variabler utenfor løkken, noe som kan øke kognitiv belastning og potensial for feil i mer komplekse scenarier.
- Ikke komponerbart: Det er ikke en ren, kjedbar operasjon. Det bryter flyten av funksjonell metodekjedning (som
map,filter, osv.).
Dette er nettopp problemet som Iterator scan-hjelperen er designet for å løse med eleganse og kraft.
Et nytt paradigme: Iterator Helpers-forslaget
Før vi hopper rett inn i scan, er det viktig å forstå konteksten den lever i. Iterator Helpers-forslaget har som mål å gjøre iteratorer til førsteklasses borgere i JavaScript for databehandling. Iteratorer er et grunnleggende konsept i JavaScript – de er motoren bak for...of-løkker, spredningssyntaksen (...) og generatorer.
Forslaget legger til en suite av kjente, array-lignende metoder direkte på Iterator.prototype, inkludert:
map(mapperFn): Transformerer hvert element i iteratoren.filter(filterFn): Returnerer bare elementene som består en test.take(limit): Returnerer de første N elementene.drop(limit): Hopper over de første N elementene.flatMap(mapperFn): Mapper hvert element til en iterator og flater ut resultatet.reduce(reducer, initialValue): Reduserer iteratoren til en enkelt verdi.- Og selvfølgelig
scan(reducer, initialValue).
Hovedfordelen her er lat evaluering. I motsetning til array-metoder, som ofte oppretter nye, mellomliggende arrayer i minnet, behandler iteratorhjelpere elementer én om gangen, ved behov. Dette gjør dem utrolig minneeffektive for håndtering av svært store eller til og med uendelige datastrømmer.
Et dypdykk i `scan`-metoden
scan-metoden er konseptuelt lik reduce, men i stedet for å returnere en enkelt endelig verdi, returnerer den en ny iterator som gir resultatet av reduseringsfunksjonen i hvert trinn. Den lar deg se hele historien til akkumuleringen.
Syntaks og parametere
Metodesignaturen er enkel og vil føles kjent for alle som har brukt reduce.
iterator.scan(reducer [, initialValue])
reducer(accumulator, element, index): En funksjon som kalles for hvert element i iteratoren. Den mottar:accumulator: Verdien returnert av forrige påkalling av redusereren, ellerinitialValuehvis angitt.element: Det nåværende elementet som behandles fra kildeiteratoren.index: Indeksen til det nåværende elementet.
accumulatorfor neste kall og er også verdien somscangir.initialValue(valgfritt): En startverdi som skal brukes som den førsteaccumulator. Hvis ikke oppgitt, brukes det første elementet i iteratoren som startverdi, og iterasjonen starter fra det andre elementet.
Hvordan det fungerer: Trinn for trinn
La oss spore vårt løpende saldoeksempel for å se scan i aksjon. Husk at scan opererer på iteratorer, så først må vi få en iterator fra arrayen vår.
const transactions = [100, -20, 50, -10, 75];
const initialBalance = 0;
// 1. Få en iterator fra arrayen
const transactionIterator = transactions.values();
// 2. Bruk scan-metoden
const runningBalanceIterator = transactionIterator.scan(
(balance, transaction) => balance + transaction,
initialBalance
);
// 3. Resultatet er en ny iterator. Vi kan konvertere den til en array for å se resultatene.
const runningBalances = [...runningBalanceIterator];
console.log(runningBalances); // Output: [100, 80, 130, 120, 195]
Her er hva som skjer under panseret:
scankalles med en reduserer(a, b) => a + bog eninitialValuepå0.- Iterasjon 1: Redusereren kalles med
accumulator = 0(startverdien) ogelement = 100. Den returnerer100.scangir100. - Iterasjon 2: Redusereren kalles med
accumulator = 100(forrige resultat) ogelement = -20. Den returnerer80.scangir80. - Iterasjon 3: Redusereren kalles med
accumulator = 80ogelement = 50. Den returnerer130.scangir130. - Iterasjon 4: Redusereren kalles med
accumulator = 130ogelement = -10. Den returnerer120.scangir120. - Iterasjon 5: Redusereren kalles med
accumulator = 120ogelement = 75. Den returnerer195.scangir195.
Resultatet er en ren, deklarativ og komponerbar måte å oppnå nøyaktig det vi trengte, uten manuelle løkker eller ekstern tilstandshåndtering.
Praktiske eksempler og globale brukstilfeller
Kraften til scan strekker seg langt utover enkle løpende totaler. Det er en grunnleggende primitiv for strømprosessering som kan brukes på et bredt spekter av domener som er relevante for utviklere over hele verden.
Eksempel 1: Tilstandshåndtering og hendelseskilde
En av de kraftigste bruksområdene til scan er i tilstandshåndtering, og speiler mønstre som finnes i biblioteker som Redux. Tenk deg at du har en strøm av brukerhandlinger eller applikasjonshendelser. Du kan bruke scan til å behandle disse hendelsene og produsere tilstanden til applikasjonen din på hvert tidspunkt.
La oss modellere en enkel teller med inkrement-, dekrement- og tilbakestillingshandlinger.
// En generatorfunksjon for å simulere en strøm av handlinger
function* actionStream() {
yield { type: 'INCREMENT' };
yield { type: 'INCREMENT' };
yield { type: 'DECREMENT', payload: 2 };
yield { type: 'UNKNOWN_ACTION' }; // Bør ignoreres
yield { type: 'RESET' };
yield { type: 'INCREMENT', payload: 5 };
}
// Starttilstanden til applikasjonen vår
const initialState = { count: 0 };
// Reduseringsfunksjonen definerer hvordan tilstanden endres som svar på handlinger
function stateReducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + (action.payload || 1) };
case 'DECREMENT':
return { ...state, count: state.count - (action.payload || 1) };
case 'RESET':
return { count: 0 };
default:
return state; // VIKTIG: Returner alltid gjeldende tilstand for ubehandlede handlinger
}
}
// Bruk scan for å opprette en iterator av applikasjonens tilstandshistorikk
const stateHistoryIterator = actionStream().scan(stateReducer, initialState);
// Logg hver tilstands endring når den skjer
for (const state of stateHistoryIterator) {
console.log(state);
}
/*
Output:
{ count: 1 }
{ count: 2 }
{ count: 0 }
{ count: 0 } // a.k.a state was unchanged by UNKNOWN_ACTION
{ count: 0 } // after RESET
{ count: 5 }
*/
Dette er utrolig kraftig. Vi har deklarativt definert hvordan tilstanden vår utvikler seg og brukt scan til å opprette en komplett, observerbar historie over den tilstanden. Dette mønsteret er grunnleggende for tidsreisedebugging, logging og bygging av forutsigbare applikasjoner.
Eksempel 2: Dataaggregering på store strømmer
Tenk deg at du behandler en massiv loggfil eller en strøm av data fra IoT-sensorer som er for stor til å passe inn i minnet. Iteratorhjelpere skinner her. La oss bruke scan til å spore maksimalverdien som er sett så langt i en strøm av tall.
// En generator for å simulere en veldig stor strøm av sensoravlesninger
function* getSensorReadings() {
yield 22.5;
yield 24.1;
yield 23.8;
yield 28.3; // Ny maks
yield 27.9;
yield 30.1; // Ny maks
// ... kan gi millioner flere
}
const readingsIterator = getSensorReadings();
// Bruk scan til å spore maksimal avlesning over tid
const maxReadingHistory = readingsIterator.scan((maxSoFar, currentReading) => {
return Math.max(maxSoFar, currentReading);
});
// Vi trenger ikke å sende en initialValue her. `scan` vil bruke den første
// elementet (22.5) som den første maks og starte fra det andre elementet.
console.log([...maxReadingHistory]);
// Output: [ 24.1, 24.1, 28.3, 28.3, 30.1 ]
Vent, utgangen kan virke litt av ved første øyekast. Siden vi ikke oppga en startverdi, brukte scan det første elementet (22.5) som den første akkumulatoren og begynte å gi fra seg resultatet av den første reduksjonen. For å se historikken inkludert startverdien, kan vi oppgi den eksplisitt, for eksempel med -Infinity.
const maxReadingHistoryWithInitial = getSensorReadings().scan(
(maxSoFar, currentReading) => Math.max(maxSoFar, currentReading),
-Infinity
);
console.log([...maxReadingHistoryWithInitial]);
// Output: [ 22.5, 24.1, 24.1, 28.3, 28.3, 30.1 ]
Dette demonstrerer minneeffektiviteten til iteratorer. Vi kan behandle en teoretisk uendelig datastrøm og få den løpende maks på hvert trinn uten å noen gang holde mer enn én verdi i minnet om gangen.
Eksempel 3: Kjedet med andre hjelpere for kompleks logikk
Den sanne kraften til Iterator Helpers-forslaget låses opp når du begynner å kjede metoder sammen. La oss bygge en mer kompleks pipeline. Tenk deg en strøm av e-handelshendelser. Vi ønsker å beregne den totale inntekten over tid, men bare fra vellykket fullførte bestillinger plassert av VIP-kunder.
function* getECommerceEvents() {
yield { type: 'PAGE_VIEW', user: 'guest' };
yield { type: 'ORDER_PLACED', user: 'user123', amount: 50, isVip: false };
yield { type: 'ORDER_COMPLETED', user: 'user456', amount: 120, isVip: true };
yield { type: 'ORDER_FAILED', user: 'user789', amount: 200, isVip: true };
yield { type: 'ORDER_COMPLETED', user: 'user101', amount: 75, isVip: true };
yield { type: 'PAGE_VIEW', user: 'user456' };
yield { type: 'ORDER_COMPLETED', user: 'user123', amount: 30, isVip: false }; // Ikke VIP
yield { type: 'ORDER_COMPLETED', user: 'user999', amount: 250, isVip: true };
}
const revenueHistory = getECommerceEvents()
// 1. Filtrer for de riktige hendelsene
.filter(event => event.type === 'ORDER_COMPLETED' && event.isVip)
// 2. Kartlegg til bare bestillingsbeløpet
.map(event => event.amount)
// 3. Scan for å få den løpende totalen
.scan((total, amount) => total + amount, 0);
console.log([...revenueHistory]);
// La oss spore dataflyten:
// - Etter filter: { amount: 120 }, { amount: 75 }, { amount: 250 }
// - Etter kart: 120, 75, 250
// - Etter scan (returnerte verdier):
// - 0 + 120 = 120
// - 120 + 75 = 195
// - 195 + 250 = 445
// Endelig Output: [ 120, 195, 445 ]
Dette eksemplet er en vakker demonstrasjon av deklarativ programmering. Koden leses som en beskrivelse av forretningslogikken: filtrer for fullførte VIP-bestillinger, trekk ut beløpet, og beregn deretter den løpende totalen. Hvert trinn er en liten, gjenbrukbar og testbar del av en større, minneeffektiv pipeline.
`scan()` vs. `reduce()`: Et tydelig skille
Det er avgjørende å befeste forskjellen mellom disse to kraftige metodene. Mens de deler en reduseringsfunksjon, er deres formål og utdata fundamentalt forskjellige.
reduce()handler om oppsummering. Den behandler en hel sekvens for å produsere en enkelt, endelig verdi. Reisen er skjult.scan()handler om transformasjon og observasjon. Den behandler en sekvens og produserer en ny sekvens av samme lengde, og viser den akkumulerte tilstanden i hvert trinn. Reisen er resultatet.
Her er en side-ved-side-sammenligning:
| Funksjon | iterator.reduce(reducer, initial) |
iterator.scan(reducer, initial) |
|---|---|---|
| Primært mål | Å destillere en sekvens ned til en enkelt oppsummeringsverdi. | Å observere den akkumulerte verdien i hvert trinn av en sekvens. |
| Returverdi | En enkelt verdi (Promise hvis asynkron) av det endelige akkumulerte resultatet. | En ny iterator som gir hvert mellomliggende akkumulerte resultat. |
| Vanlig analogi | Beregning av den endelige saldoen på en bankkonto. | Generering av en kontoutskrift som viser saldoen etter hver transaksjon. |
| Brukstilfelle | Summering av tall, finne en maksimum, sammenkjetting av strenger. | Løpende totaler, tilstandshåndtering, beregning av glidende gjennomsnitt, observasjon av historiske data. |
Kode sammenligning
const numbers = [1, 2, 3, 4].values(); // Få en iterator
// Reduce: Destinasjonen
const sum = numbers.reduce((acc, val) => acc + val, 0);
console.log(sum); // Output: 10
// Du trenger en ny iterator for neste operasjon
const numbers2 = [1, 2, 3, 4].values();
// Scan: Reisen
const runningSum = numbers2.scan((acc, val) => acc + val, 0);
console.log([...runningSum]); // Output: [1, 3, 6, 10]
Hvordan bruke Iterator Helpers i dag
Per skrivende stund er Iterator Helpers-forslaget på trinn 3 i TC39-prosessen. Dette betyr at det er veldig nært å bli fullført og inkludert i en fremtidig versjon av ECMAScript-standarden. Selv om det kanskje ikke er tilgjengelig i alle nettlesere eller Node.js-miljøer naturlig ennå, trenger du ikke vente med å begynne å bruke det.
Du kan bruke disse kraftige funksjonene i dag gjennom polyfills. Den vanligste måten er å bruke core-js-biblioteket, som er en omfattende polyfill for moderne JavaScript-funksjoner.
For å bruke det, vil du vanligvis installere core-js:
npm install core-js
Og deretter importere den spesifikke forslaget polyfill ved inngangspunktet til applikasjonen din:
import 'core-js/proposals/iterator-helpers';
// Nå kan du bruke .scan() og andre hjelpere!
const result = [1, 2, 3].values()
.map(x => x * 2)
.scan((a, b) => a + b, 0);
console.log([...result]); // [2, 6, 12]
Alternativt, hvis du bruker en transpiler som Babel, kan du konfigurere den til å inkludere de nødvendige polyfills og transformasjonene for trinn 3-forslag.
Konklusjon: Et nytt verktøy for en ny data-æra
JavaScript Iterator scan-hjelperen er mer enn bare en praktisk ny metode; den representerer et skifte mot en mer funksjonell, deklarativ og minneeffektiv måte å håndtere datastrømmer på. Den fyller et kritisk gap etterlatt av reduce, slik at utviklere ikke bare kan komme frem til et endelig resultat, men også observere og handle på hele historikken til en akkumulering.
Ved å omfavne scan og det bredere Iterator Helpers-forslaget, kan du skrive kode som er:
- Mer deklarativ: Koden din vil tydeligere uttrykke hva du prøver å oppnå, snarere enn hvordan du oppnår det med manuelle løkker.
- Mer komponerbar: Kjed sammen enkle, rene operasjoner for å bygge komplekse databehandlings-pipelines som er enkle å lese og resonnere om.
- Mer minneeffektiv: Utnytt lat evaluering for å behandle massive eller uendelige datasett uten å overvelde systemets minne.
Ettersom vi fortsetter å bygge mer dataintensive og reaktive applikasjoner, vil verktøy som scan bli uunnværlige. Det er en kraftig primitiv som muliggjør sofistikerte mønstre som hendelseskilde og strømprosessering som kan implementeres naturlig, elegant og effektivt. Begynn å utforske det i dag, og du vil være godt forberedt på fremtiden for datahåndtering i JavaScript.