UpptÀck kraften i den nya JavaScript-iteratorhjÀlparen `scan`. LÀr dig hur den revolutionerar strömbearbetning, tillstÄndshantering och dataaggregering bortom `reduce`.
JavaScript-iteratorn scan: Den felande lÀnken för ackumulativ strömbearbetning
I det stÀndigt förÀnderliga landskapet av modern webbutveckling Àr data kung. Vi hanterar stÀndigt strömmar av information: anvÀndarhÀndelser, API-svar i realtid, stora datamÀngder och mer. Att bearbeta denna data effektivt och deklarativt Àr en avgörande utmaning. I Äratal har JavaScript-utvecklare förlitat sig pÄ den kraftfulla metoden Array.prototype.reduce för att reducera en array till ett enda vÀrde. Men vad hÀnder om du behöver se resan, inte bara destinationen? Vad hÀnder om du behöver observera varje mellanliggande steg i en ackumulering?
Det Àr hÀr ett nytt, kraftfullt verktyg Àntrar scenen: iteratorhjÀlparen scan. Som en del av TC39 Iterator Helpers-förslaget, för nÀrvarande pÄ Steg 3, Àr scan redo att revolutionera hur vi hanterar sekventiell och strömbaserad data i JavaScript. Det Àr den funktionella, eleganta motsvarigheten till reduce som ger den fullstÀndiga historiken för en operation.
Denna omfattande guide kommer att ta dig pÄ en djupdykning i scan-metoden. Vi kommer att utforska problemen den löser, dess syntax, dess kraftfulla anvÀndningsfall frÄn enkla löpande summor till komplex tillstÄndshantering, och hur den passar in i det bredare ekosystemet av modern, minneseffektiv JavaScript.
Den vÀlbekanta utmaningen: BegrÀnsningarna med `reduce`
För att verkligen uppskatta vad scan tillför mÄste vi först Äterbesöka ett vanligt scenario. FörestÀll dig att du har en ström av finansiella transaktioner och du behöver berÀkna det löpande saldot efter varje transaktion. Datan kan se ut sÄ hÀr:
const transactions = [100, -20, 50, -10, 75]; // InsÀttningar och uttag
Om du bara ville ha det slutliga saldot Àr Array.prototype.reduce det perfekta verktyget:
const finalBalance = transactions.reduce((balance, transaction) => balance + transaction, 0);
console.log(finalBalance); // Output: 195
Detta Àr koncist och effektivt. Men vad hÀnder om du behöver plotta kontosaldot över tid i ett diagram? Du behöver saldot efter varje transaktion: [100, 80, 130, 120, 195]. Metoden reduce döljer dessa mellanliggande steg för oss; den ger bara det slutliga resultatet.
SÄ, hur skulle vi lösa detta traditionellt? Vi skulle troligen falla tillbaka pÄ en manuell loop med en extern tillstÄndsvariabel:
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]
Detta fungerar, men det har flera nackdelar:
- Imperativ stil: Den Àr mindre deklarativ. Vi hanterar manuellt tillstÄndet (
currentBalance) och resultatsamlingen (runningBalances). - TillstÄndsbaserad och mÄngordig: Den krÀver hantering av muterbara variabler utanför loopen, vilket kan öka den kognitiva belastningen och risken för buggar i mer komplexa scenarier.
- Inte komponerbar: Det Àr inte en ren, kedjebar operation. Den bryter flödet av funktionell metodkedjning (som
map,filter, etc.).
Detta Àr exakt det problem som iteratorhjÀlparen scan Àr designad för att lösa med elegans och kraft.
Ett nytt paradigm: Iterator Helpers-förslaget
Innan vi hoppar rakt in i scan Ă€r det viktigt att förstĂ„ sammanhanget den verkar i. Iterator Helpers-förslaget syftar till att göra iteratorer till förstklassiga medborgare i JavaScript för databearbetning. Iteratorer Ă€r ett grundlĂ€ggande koncept i JavaScript â de Ă€r motorn bakom for...of-loopar, spread-syntaxen (...) och generatorer.
Förslaget lÀgger till en uppsÀttning vÀlbekanta, array-liknande metoder direkt pÄ Iterator.prototype, inklusive:
map(mapperFn): Transformerar varje element i iteratorn.filter(filterFn): Ger endast de element som klarar ett test.take(limit): Ger de första N elementen.drop(limit): Hoppar över de första N elementen.flatMap(mapperFn): Mappar varje element till en iterator och plattar ut resultatet.reduce(reducer, initialValue): Reducerar iteratorn till ett enda vÀrde.- Och, naturligtvis,
scan(reducer, initialValue).
Den största fördelen hÀr Àr lat evaluering. Till skillnad frÄn array-metoder, som ofta skapar nya, mellanliggande arrayer i minnet, bearbetar iteratorhjÀlpare element ett i taget, vid behov. Detta gör dem otroligt minneseffektiva för hantering av mycket stora eller till och med oÀndliga dataströmmar.
En djupdykning i scan-metoden
Metoden scan Àr konceptuellt lik reduce, men istÀllet för att returnera ett enda slutvÀrde, returnerar den en ny iterator som ger resultatet av reducer-funktionen vid varje steg. Den lÄter dig se hela ackumuleringens historik.
Syntax och parametrar
Metodsignaturen Àr rÀttfram och kommer att kÀnnas bekant för alla som har anvÀnt reduce.
iterator.scan(reducer [, initialValue])
reducer(accumulator, element, index): En funktion som anropas för varje element i iteratorn. Den tar emot:accumulator: VÀrdet som returnerades av det föregÄende anropet till reducern, ellerinitialValueom det angavs.element: Det aktuella elementet som bearbetas frÄn kÀlliteratorn.index: Index för det aktuella elementet.
accumulatorför nÀsta anrop och Àr ocksÄ det vÀrde somscanger.initialValue(valfritt): Ett initialt vÀrde att anvÀnda som den förstaaccumulator. Om det inte anges, anvÀnds det första elementet i iteratorn som initialvÀrde, och iterationen börjar frÄn det andra elementet.
Hur det fungerar: Steg för steg
LÄt oss spÄra vÄrt exempel med löpande saldo för att se scan i aktion. Kom ihÄg, scan arbetar pÄ iteratorer, sÄ först mÄste vi hÀmta en iterator frÄn vÄr array.
const transactions = [100, -20, 50, -10, 75];
const initialBalance = 0;
// 1. HÀmta en iterator frÄn arrayen
const transactionIterator = transactions.values();
// 2. AnvÀnd scan-metoden
const runningBalanceIterator = transactionIterator.scan(
(balance, transaction) => balance + transaction,
initialBalance
);
// 3. Resultatet Àr en ny iterator. Vi kan konvertera den till en array för att se resultaten.
const runningBalances = [...runningBalanceIterator];
console.log(runningBalances); // Output: [100, 80, 130, 120, 195]
HÀr Àr vad som hÀnder under huven:
scananropas med en reducer(a, b) => a + boch ettinitialValuepÄ0.- Iteration 1: Reducern anropas med
accumulator = 0(initialvÀrdet) ochelement = 100. Den returnerar100.scanger100. - Iteration 2: Reducern anropas med
accumulator = 100(föregÄende resultat) ochelement = -20. Den returnerar80.scanger80. - Iteration 3: Reducern anropas med
accumulator = 80ochelement = 50. Den returnerar130.scanger130. - Iteration 4: Reducern anropas med
accumulator = 130ochelement = -10. Den returnerar120.scanger120. - Iteration 5: Reducern anropas med
accumulator = 120ochelement = 75. Den returnerar195.scanger195.
Resultatet Àr ett rent, deklarativt och komponerbart sÀtt att uppnÄ exakt vad vi behövde, utan manuella loopar eller extern tillstÄndshantering.
Praktiska exempel och globala anvÀndningsfall
Kraften i scan strÀcker sig lÄngt bortom enkla löpande summor. Det Àr en grundlÀggande primitiv för strömbearbetning som kan tillÀmpas pÄ en mÀngd olika domÀner som Àr relevanta för utvecklare vÀrlden över.
Exempel 1: TillstÄndshantering och Event Sourcing
En av de mest kraftfulla tillÀmpningarna av scan Àr inom tillstÄndshantering, vilket speglar mönster som finns i bibliotek som Redux. FörestÀll dig att du har en ström av anvÀndarÄtgÀrder eller applikationshÀndelser. Du kan anvÀnda scan för att bearbeta dessa hÀndelser och producera din applikations tillstÄnd vid varje tidpunkt.
LÄt oss modellera en enkel rÀknare med ÄtgÀrder för att öka, minska och ÄterstÀlla.
// En generatorfunktion för att simulera en ström av ÄtgÀrder
function* actionStream() {
yield { type: 'INCREMENT' };
yield { type: 'INCREMENT' };
yield { type: 'DECREMENT', payload: 2 };
yield { type: 'UNKNOWN_ACTION' }; // Bör ignoreras
yield { type: 'RESET' };
yield { type: 'INCREMENT', payload: 5 };
}
// VÄr applikations initiala tillstÄnd
const initialState = { count: 0 };
// Reducer-funktionen definierar hur tillstÄndet Àndras som svar pÄ ÄtgÀrder
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; // VIKTIGT: Returnera alltid det aktuella tillstÄndet för ohanterade ÄtgÀrder
}
}
// AnvÀnd scan för att skapa en iterator över applikationens tillstÄndshistorik
const stateHistoryIterator = actionStream().scan(stateReducer, initialState);
// Logga varje tillstÄndsÀndring nÀr den sker
for (const state of stateHistoryIterator) {
console.log(state);
}
/*
Output:
{ count: 1 }
{ count: 2 }
{ count: 0 }
{ count: 0 } // d.v.s. tillstÄndet var oförÀndrat av UNKNOWN_ACTION
{ count: 0 } // efter RESET
{ count: 5 }
*/
Detta Àr otroligt kraftfullt. Vi har deklarativt definierat hur vÄrt tillstÄnd utvecklas och anvÀnt scan för att skapa en komplett, observerbar historik över det tillstÄndet. Detta mönster Àr grundlÀggande för "time-travel debugging", loggning och att bygga förutsÀgbara applikationer.
Exempel 2: Dataaggregering pÄ stora strömmar
FörestÀll dig att du bearbetar en massiv loggfil eller en ström av data frÄn IoT-sensorer som Àr för stor för att rymmas i minnet. HÀr briljerar iteratorhjÀlpare. LÄt oss anvÀnda scan för att spÄra det högsta vÀrdet som setts hittills i en ström av tal.
// En generator för att simulera en mycket stor ström av sensordata
function* getSensorReadings() {
yield 22.5;
yield 24.1;
yield 23.8;
yield 28.3; // Nytt max
yield 27.9;
yield 30.1; // Nytt max
// ... kunde ge miljontals fler
}
const readingsIterator = getSensorReadings();
// AnvÀnd scan för att spÄra den maximala avlÀsningen över tid
const maxReadingHistory = readingsIterator.scan((maxSoFar, currentReading) => {
return Math.max(maxSoFar, currentReading);
});
// Vi behöver inte skicka med ett initialValue hÀr. `scan` kommer att anvÀnda det första
// elementet (22.5) som initialt maxvÀrde och börja frÄn det andra elementet.
console.log([...maxReadingHistory]);
// Output: [ 24.1, 24.1, 28.3, 28.3, 30.1 ]
VÀnta, resultatet kan verka lite konstigt vid första anblicken. Eftersom vi inte angav ett initialvÀrde, anvÀnde scan det första elementet (22.5) som den initiala ackumulatorn och började ge resultat frÄn den första reduceringen. För att se historiken inklusive det initiala vÀrdet kan vi ange det explicit, till exempel 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 ]
Detta demonstrerar minneseffektiviteten hos iteratorer. Vi kan bearbeta en teoretiskt oÀndlig ström av data och fÄ det löpande maximumvÀrdet vid varje steg utan att nÄgonsin hÄlla mer Àn ett vÀrde i minnet Ät gÄngen.
Exempel 3: Kedjning med andra hjÀlpare för komplex logik
Den sanna kraften i Iterator Helpers-förslaget frigörs nÀr du börjar kedja ihop metoder. LÄt oss bygga en mer komplex pipeline. FörestÀll dig en ström av e-handelshÀndelser. Vi vill berÀkna den totala intÀkten över tid, men bara frÄn framgÄngsrikt slutförda bestÀllningar gjorda 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 }; // Inte VIP
yield { type: 'ORDER_COMPLETED', user: 'user999', amount: 250, isVip: true };
}
const revenueHistory = getECommerceEvents()
// 1. Filtrera för rÀtt hÀndelser
.filter(event => event.type === 'ORDER_COMPLETED' && event.isVip)
// 2. Mappa till endast orderbeloppet
.map(event => event.amount)
// 3. Scanna för att fÄ den löpande summan
.scan((total, amount) => total + amount, 0);
console.log([...revenueHistory]);
// LÄt oss spÄra dataflödet:
// - Efter filter: { amount: 120 }, { amount: 75 }, { amount: 250 }
// - Efter map: 120, 75, 250
// - Efter scan (yielded values):
// - 0 + 120 = 120
// - 120 + 75 = 195
// - 195 + 250 = 445
// Slutligt output: [ 120, 195, 445 ]
Detta exempel Àr en vacker demonstration av deklarativ programmering. Koden lÀses som en beskrivning av affÀrslogiken: filtrera för slutförda VIP-bestÀllningar, extrahera beloppet och berÀkna sedan den löpande summan. Varje steg Àr en liten, ÄteranvÀndbar och testbar del av en större, minneseffektiv pipeline.
scan() kontra reduce(): En tydlig skillnad
Det Ă€r avgörande att befĂ€sta skillnaden mellan dessa tvĂ„ kraftfulla metoder. Ăven om de delar en reducer-funktion, Ă€r deras syfte och resultat fundamentalt olika.
reduce()handlar om sammanfattning. Den bearbetar en hel sekvens för att producera ett enda, slutligt vÀrde. Resan Àr dold.scan()handlar om transformation och observation. Den bearbetar en sekvens och producerar en ny sekvens av samma lÀngd, som visar det ackumulerade tillstÄndet vid varje steg. Resan Àr resultatet.
HÀr Àr en sida-vid-sida-jÀmförelse:
| Egenskap | iterator.reduce(reducer, initial) |
iterator.scan(reducer, initial) |
|---|---|---|
| PrimÀrt mÄl | Att reducera en sekvens till ett enda sammanfattande vÀrde. | Att observera det ackumulerade vÀrdet vid varje steg i en sekvens. |
| ReturvÀrde | Ett enda vÀrde (Promise om asynkron) av det slutliga ackumulerade resultatet. | En ny iterator som ger varje mellanliggande ackumulerat resultat. |
| Vanlig analogi | Att berÀkna det slutliga saldot pÄ ett bankkonto. | Att generera ett kontoutdrag som visar saldot efter varje transaktion. |
| AnvÀndningsfall | Summera tal, hitta ett maximum, sammanfoga strÀngar. | Löpande summor, tillstÄndshantering, berÀkna glidande medelvÀrden, observera historisk data. |
KodjÀmförelse
const numbers = [1, 2, 3, 4].values(); // HĂ€mta en iterator
// Reduce: Destinationen
const sum = numbers.reduce((acc, val) => acc + val, 0);
console.log(sum); // Output: 10
// Du behöver en ny iterator för nÀsta operation
const numbers2 = [1, 2, 3, 4].values();
// Scan: Resan
const runningSum = numbers2.scan((acc, val) => acc + val, 0);
console.log([...runningSum]); // Output: [1, 3, 6, 10]
Hur man anvÀnder iteratorhjÀlpare idag
I skrivande stund Ă€r Iterator Helpers-förslaget pĂ„ Steg 3 i TC39-processen. Det betyder att det Ă€r mycket nĂ€ra att slutföras och inkluderas i en framtida version av ECMAScript-standarden. Ăven om det kanske Ă€nnu inte Ă€r tillgĂ€ngligt i alla webblĂ€sare eller Node.js-miljöer inbyggt, behöver du inte vĂ€nta med att börja anvĂ€nda det.
Du kan anvÀnda dessa kraftfulla funktioner idag genom polyfills. Det vanligaste sÀttet Àr att anvÀnda core-js-biblioteket, som Àr en omfattande polyfill för moderna JavaScript-funktioner.
För att anvÀnda det installerar du vanligtvis core-js:
npm install core-js
Och importerar sedan den specifika förslags-polyfillen vid ingÄngspunkten för din applikation:
import 'core-js/proposals/iterator-helpers';
// Nu kan du anvÀnda .scan() och andra hjÀlpare!
const result = [1, 2, 3].values()
.map(x => x * 2)
.scan((a, b) => a + b, 0);
console.log([...result]); // [2, 6, 12]
Alternativt, om du anvÀnder en transpiler som Babel, kan du konfigurera den för att inkludera nödvÀndiga polyfills och transformationer för Steg 3-förslag.
Slutsats: Ett nytt verktyg för en ny era av data
JavaScript-iteratorhjÀlparen scan Àr mer Àn bara en bekvÀm ny metod; den representerar en övergÄng mot ett mer funktionellt, deklarativt och minneseffektivt sÀtt att hantera dataströmmar. Den fyller ett kritiskt tomrum som reduce lÀmnat, vilket gör att utvecklare inte bara kan nÄ ett slutresultat utan ocksÄ kan observera och agera pÄ hela historiken av en ackumulering.
Genom att omfamna scan och det bredare Iterator Helpers-förslaget kan du skriva kod som Àr:
- Mer deklarativ: Din kod kommer tydligare att uttrycka vad du försöker uppnÄ, snarare Àn hur du uppnÄr det med manuella loopar.
- Mer komponerbar: Kedja ihop enkla, rena operationer för att bygga komplexa databearbetningspipelines som Àr lÀtta att lÀsa och resonera kring.
- Mer minneseffektiv: Utnyttja lat evaluering för att bearbeta massiva eller oÀndliga datamÀngder utan att överbelasta systemets minne.
NÀr vi fortsÀtter att bygga mer dataintensiva och reaktiva applikationer kommer verktyg som scan att bli oumbÀrliga. Det Àr en kraftfull primitiv som möjliggör sofistikerade mönster som event sourcing och strömbearbetning att implementeras inbyggt, elegant och effektivt. Börja utforska det idag, och du kommer att vara vÀl förberedd för framtidens datahantering i JavaScript.