Išsamus vadovas, padedantis suprasti ir įdiegti JavaScript iteratoriaus protokolą, leidžiantis kurti pasirinktinius iteratorius patobulintam duomenų tvarkymui.
JavaScript iteratoriaus protokolo ir pasirinktinių iteratorių demistifikavimas
JavaScript iteratoriaus protokolas suteikia standartizuotą būdą peržvelgti duomenų struktūras. Šio protokolo supratimas leidžia programuotojams efektyviai dirbti su integruotais iteruojamais objektais, tokiais kaip masyvai ir eilutės, ir kurti savo pasirinktinius iteruojamus objektus, pritaikytus konkrečioms duomenų struktūroms ir taikomosios programos reikalavimams. Šis vadovas pateikia išsamų iteratoriaus protokolo tyrimą ir paaiškina, kaip įdiegti pasirinktinius iteratorius.
Kas yra iteratoriaus protokolas?
Iteratoriaus protokolas apibrėžia, kaip objektą galima peržvelgti (iteruoti), t. y. kaip jo elementus galima pasiekti nuosekliai. Jis susideda iš dviejų dalių: iteruojamo objekto (Iterable) protokolo ir iteratoriaus (Iterator) protokolo.
Iteruojamo objekto (Iterable) protokolas
Objektas laikomas iteruojamu (Iterable), jei jis turi metodą su raktu Symbol.iterator
. Šis metodas turi grąžinti objektą, atitinkantį iteratoriaus (Iterator) protokolą.
Iš esmės, iteruojamas objektas žino, kaip susikurti sau iteratorių.
Iteratoriaus (Iterator) protokolas
Iteratoriaus protokolas apibrėžia, kaip gauti reikšmes iš sekos. Objektas laikomas iteratoriumi, jei jis turi next()
metodą, kuris grąžina objektą su dviem savybėmis:
value
: Kita reikšmė sekoje.done
: Loginė (boolean) reikšmė, nurodanti, ar iteratorius pasiekė sekos pabaigą. Jeidone
yratrue
,value
savybę galima praleisti.
next()
metodas yra pagrindinis iteratoriaus protokolo variklis. Kiekvienas next()
iškvietimas perkelia iteratorių į priekį ir grąžina kitą sekos reikšmę. Kai visos reikšmės yra grąžintos, next()
grąžina objektą su done
reikšme, nustatyta į true
.
Integruoti iteruojami objektai
JavaScript turi keletą integruotų duomenų struktūrų, kurios yra savaime iteruojamos. Tai apima:
- Masyvus (Arrays)
- Eilutes (Strings)
- Žemėlapius (Maps)
- Aibes (Sets)
- Funkcijos argumentų objektą (Arguments object)
- Tipizuotus masyvus (TypedArrays)
Šiuos iteruojamus objektus galima tiesiogiai naudoti su for...of
ciklu, skleidimo sintakse (...
) ir kitomis konstrukcijomis, kurios remiasi iteratoriaus protokolu.
Pavyzdys su masyvais:
const myArray = ["apple", "banana", "cherry"];
for (const item of myArray) {
console.log(item); // Išvestis: apple, banana, cherry
}
Pavyzdys su eilutėmis:
const myString = "Hello";
for (const char of myString) {
console.log(char); // Išvestis: H, e, l, l, o
}
for...of
ciklas
for...of
ciklas yra galinga konstrukcija, skirta iteruoti per iteruojamus objektus. Jis automatiškai tvarko iteratoriaus protokolo sudėtingumą, todėl lengva pasiekti sekos reikšmes.
for...of
ciklo sintaksė yra:
for (const element of iterable) {
// Kodas, kuris bus vykdomas kiekvienam elementui
}
for...of
ciklas gauna iteratorių iš iteruojamo objekto (naudodamas Symbol.iterator
) ir pakartotinai kviečia iteratoriaus next()
metodą, kol done
tampa true
. Kiekvienoje iteracijoje element
kintamajam priskiriama value
savybė, grąžinta iš next()
.
Pasirinktinių iteratorių kūrimas
Nors JavaScript suteikia integruotus iteruojamus objektus, tikroji iteratoriaus protokolo galia slypi galimybėje apibrėžti pasirinktinius iteratorius savo duomenų struktūroms. Tai leidžia jums kontroliuoti, kaip jūsų duomenys yra peržvelgiami ir pasiekiami.
Štai kaip sukurti pasirinktinį iteratorių:
- Apibrėžkite klasę ar objektą, kuris atstovauja jūsų pasirinktinę duomenų struktūrą.
- Įdiekite
Symbol.iterator
metodą savo klasėje ar objekte. Šis metodas turėtų grąžinti iteratoriaus objektą. - Iteratoriaus objektas privalo turėti
next()
metodą, kuris grąžina objektą suvalue
irdone
savybėmis.
Pavyzdys: iteratoriaus kūrimas paprastam diapazonui
Sukurkime klasę pavadinimu Range
, kuri atstovauja skaičių diapazoną. Įdiegsime iteratoriaus protokolą, kad būtų galima iteruoti per skaičius diapazone.
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
[Symbol.iterator]() {
let currentValue = this.start;
const that = this; // Išsaugome 'this' kontekstą naudojimui iteratoriaus objekte
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); // Išvestis: 1, 2, 3, 4, 5
}
Paaiškinimas:
Range
klasė savo konstruktoriuje priimastart
irend
reikšmes.Symbol.iterator
metodas grąžina iteratoriaus objektą. Šis iteratoriaus objektas turi savo būseną (currentValue
) irnext()
metodą.next()
metodas patikrina, arcurrentValue
yra diapazono ribose. Jei taip, jis grąžina objektą su dabartine reikšme irdone
, nustatytu įfalse
. Jis taip pat padidinacurrentValue
kitai iteracijai.- Kai
currentValue
viršijaend
reikšmę,next()
metodas grąžina objektą sudone
, nustatytu įtrue
. - Atkreipkite dėmesį į
that = this
naudojimą. Kadangi `next()` metodas yra iškviečiamas kitame kontekste (angl. scope) (iš `for...of` ciklo), `this` `next()` viduje nenurodytų į `Range` egzempliorių. Norėdami tai išspręsti, mes išsaugome `this` reikšmę (`Range` egzempliorių) `that` kintamajame už `next()` konteksto ribų ir tada naudojame `that` `next()` viduje.
Pavyzdys: iteratoriaus kūrimas susietajam sąrašui
Apsvarstykime kitą pavyzdį: iteratoriaus kūrimas susietojo sąrašo (linked list) duomenų struktūrai. Susietasis sąrašas yra mazgų seka, kur kiekvienas mazgas turi reikšmę ir nuorodą (rodyklę) į kitą sąrašo mazgą. Paskutinis sąrašo mazgas turi nuorodą į null (arba 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
};
}
}
};
}
}
// Pavyzdinis naudojimas:
const myList = new LinkedList();
myList.append("Londonas");
myList.append("Paryžius");
myList.append("Tokijas");
for (const city of myList) {
console.log(city); // Išvestis: Londonas, Paryžius, Tokijas
}
Paaiškinimas:
LinkedListNode
klasė atstovauja vieną susietojo sąrašo mazgą, saugantįvalue
(reikšmę) ir nuorodą (next
) į kitą mazgą.LinkedList
klasė atstovauja patį susietąjį sąrašą. Ji turihead
savybę, kuri nurodo į pirmąjį sąrašo mazgą.append()
metodas prideda naujus mazgus į sąrašo pabaigą.Symbol.iterator
metodas sukuria ir grąžina iteratoriaus objektą. Šis iteratorius seka dabartinį lankomą mazgą (current
).next()
metodas patikrina, ar yra dabartinis mazgas (current
nėra null). Jei yra, jis paima reikšmę iš dabartinio mazgo, perkeliacurrent
rodyklę į kitą mazgą ir grąžina objektą su reikšme irdone: false
.- Kai
current
tampa null (tai reiškia, kad pasiekėme sąrašo pabaigą),next()
metodas grąžina objektą sudone: true
.
Generatoriaus funkcijos
Generatoriaus funkcijos suteikia glaustesnį ir elegantiškesnį būdą kurti iteratorius. Jos naudoja yield
raktažodį, kad pagal poreikį pateiktų reikšmes.
Generatoriaus funkcija apibrėžiama naudojant function*
sintaksę.
Pavyzdys: iteratoriaus kūrimas naudojant generatoriaus funkciją
Perrašykime Range
iteratorių naudodami generatoriaus funkciją:
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); // Išvestis: 1, 2, 3, 4, 5
}
Paaiškinimas:
Symbol.iterator
metodas dabar yra generatoriaus funkcija (atkreipkite dėmesį į*
).- Generatoriaus funkcijos viduje naudojame
for
ciklą, kad iteruotume per skaičių diapazoną. yield
raktažodis sustabdo generatoriaus funkcijos vykdymą ir grąžina dabartinę reikšmę (i
). Kai kitą kartą iškviečiamas iteratoriausnext()
metodas, vykdymas tęsiamas nuo tos vietos, kur buvo sustabdytas (poyield
sakinio).- Kai ciklas baigiasi, generatoriaus funkcija numanomai grąžina
{ value: undefined, done: true }
, signalizuodama iteracijos pabaigą.
Generatoriaus funkcijos supaprastina iteratorių kūrimą, automatiškai tvarkydamos next()
metodą ir done
vėliavėlę.
Pavyzdys: Fibonačio sekos generatorius
Kitas puikus generatoriaus funkcijų naudojimo pavyzdys yra Fibonačio sekos generavimas:
function* fibonacciSequence() {
let a = 0;
let b = 1;
while (true) {
yield a;
[a, b] = [b, a + b]; // Destrukturizuojantis priskyrimas vienu metu atnaujinimui
}
}
const fibonacci = fibonacciSequence();
for (let i = 0; i < 10; i++) {
console.log(fibonacci.next().value); // Išvestis: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
}
Paaiškinimas:
fibonacciSequence
funkcija yra generatoriaus funkcija.- Ji inicializuoja du kintamuosius,
a
irb
, į pirmuosius du Fibonačio sekos skaičius (0 ir 1). while (true)
ciklas sukuria begalinę seką.yield a
sakinys pateikia dabartinęa
reikšmę.[a, b] = [b, a + b]
sakinys, naudojant destrukturizuojantį priskyrimą, vienu metu atnaujinaa
irb
į kitus du sekos skaičius.fibonacci.next().value
išraiška gauna kitą reikšmę iš generatoriaus. Kadangi generatorius yra begalinis, reikia kontroliuoti, kiek reikšmių iš jo išgausite. Šiame pavyzdyje mes išgauname pirmas 10 reikšmių.
Iteratoriaus protokolo naudojimo privalumai
- Standartizavimas: Iteratoriaus protokolas suteikia nuoseklų būdą iteruoti per skirtingas duomenų struktūras.
- Lankstumas: Galite apibrėžti pasirinktinius iteratorius, pritaikytus jūsų specifiniams poreikiams.
- Skaitomumas:
for...of
ciklas daro iteracijos kodą skaitomesnį ir glaustesnį. - Efektyvumas: Iteratoriai gali būti „tingūs“ (lazy), o tai reiškia, kad jie generuoja reikšmes tik tada, kai jų prireikia, o tai gali pagerinti našumą dirbant su dideliais duomenų rinkiniais. Pavyzdžiui, aukščiau pateiktas Fibonačio sekos generatorius apskaičiuoja kitą reikšmę tik iškvietus `next()`.
- Suderinamumas: Iteratoriai sklandžiai veikia su kitomis JavaScript funkcijomis, tokiomis kaip skleidimo sintaksė ir destrukturizavimas.
Pažangios iteratorių technikos
Iteratorių sujungimas
Galite sujungti kelis iteratorius į vieną. Tai naudinga, kai reikia apdoroti duomenis iš kelių šaltinių vieningu būdu.
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); // Išvestis: 1, 2, 3, a, b, c, X, Y, Z
}
Šiame pavyzdyje `combineIterators` funkcija priima bet kokį skaičių iteruojamų objektų kaip argumentus. Ji iteruoja per kiekvieną iteruojamą objektą ir pateikia (`yield`) kiekvieną elementą. Rezultatas yra vienas iteratorius, kuris pateikia visas reikšmes iš visų įvesties iteruojamų objektų.
Iteratorių filtravimas ir transformavimas
Taip pat galite kurti iteratorius, kurie filtruoja arba transformuoja kito iteratoriaus pateikiamas reikšmes. Tai leidžia apdoroti duomenis konvejeriu (pipeline), taikant skirtingas operacijas kiekvienai reikšmei, kai ji yra generuojama.
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); // Išvestis: 4, 16, 36
}
Čia `filterIterator` priima iteruojamą objektą ir predikato funkciją. Ji pateikia (`yield`) tik tuos elementus, kuriems predikatas grąžina `true`. `mapIterator` priima iteruojamą objektą ir transformavimo funkciją. Ji pateikia (`yield`) rezultatą, gautą pritaikius transformavimo funkciją kiekvienam elementui.
Panaudojimas realiame pasaulyje
Iteratoriaus protokolas yra plačiai naudojamas JavaScript bibliotekose ir karkasuose, ir jis yra vertingas įvairiose realaus pasaulio programose, ypač dirbant su dideliais duomenų rinkiniais ar asinchroninėmis operacijomis.
- Duomenų apdorojimas: Iteratoriai yra naudingi efektyviam didelių duomenų rinkinių apdorojimui, nes leidžia dirbti su duomenimis dalimis, neįkeliant viso duomenų rinkinio į atmintį. Įsivaizduokite, kad analizuojate didelį CSV failą su klientų duomenimis. Iteratorius gali leisti apdoroti kiekvieną eilutę, neįkeliant viso failo į atmintį iš karto.
- Asinchroninės operacijos: Iteratoriai gali būti naudojami asinchroninėms operacijoms tvarkyti, pavyzdžiui, gaunant duomenis iš API. Galite naudoti generatoriaus funkcijas, kad sustabdytumėte vykdymą, kol duomenys bus pasiekiami, ir tada tęstumėte su kita reikšme.
- Pasirinktinės duomenų struktūros: Iteratoriai yra būtini kuriant pasirinktines duomenų struktūras su specifiniais peržvalgos reikalavimais. Apsvarstykite medžio duomenų struktūrą. Galite įdiegti pasirinktinį iteratorių, kad peržvelgtumėte medį tam tikra tvarka (pvz., gilyn arba platyn).
- Žaidimų kūrimas: Žaidimų kūrime iteratoriai gali būti naudojami žaidimo objektams, dalelių efektams ir kitiems dinamiškiems elementams valdyti.
- Vartotojo sąsajos bibliotekos: Daugelis vartotojo sąsajos bibliotekų naudoja iteratorius, kad efektyviai atnaujintų ir atvaizduotų komponentus, remiantis pagrindinių duomenų pasikeitimais.
Gerosios praktikos
- Teisingai įdiekite
Symbol.iterator
: Užtikrinkite, kad jūsųSymbol.iterator
metodas grąžintų iteratoriaus objektą, atitinkantį iteratoriaus protokolą. - Tiksliai tvarkykite
done
vėliavėlę:done
vėliavėlė yra labai svarbi signalizuojant iteracijos pabaigą. Įsitikinkite, kad ją teisingai nustatote savonext()
metode. - Apsvarstykite galimybę naudoti generatoriaus funkcijas: Generatoriaus funkcijos suteikia glaustesnį ir skaitomesnį būdą kurti iteratorius.
- Venkite šalutinių poveikių
next()
metode:next()
metodas pirmiausia turėtų sutelkti dėmesį į kitos reikšmės gavimą ir iteratoriaus būsenos atnaujinimą. Venkite sudėtingų operacijų ar šalutinių poveikiųnext()
viduje. - Kruopščiai testuokite savo iteratorius: Išbandykite savo pasirinktinius iteratorius su skirtingais duomenų rinkiniais ir scenarijais, kad užtikrintumėte, jog jie veikia teisingai.
Išvada
JavaScript iteratoriaus protokolas suteikia galingą ir lankstų būdą peržvelgti duomenų struktūras. Suprasdami iteruojamų objektų ir iteratorių protokolus bei naudodami generatoriaus funkcijas, galite kurti pasirinktinius iteratorius, pritaikytus jūsų specifiniams poreikiams. Tai leidžia efektyviai dirbti su duomenimis, pagerinti kodo skaitomumą ir padidinti jūsų programų našumą. Iteratorių įvaldymas atveria gilesnį JavaScript galimybių supratimą ir įgalina rašyti elegantiškesnį bei efektyvesnį kodą.