Omanda JavaScripti iteraatori protokoll. Muuda objekte itereeritavaks, kontrolli `for...of` tsükleid ja rakenda kohandatud iteratsiooni keerukatele andmestruktuuridele.
Kohandatud iteratsiooni avamine JavaScriptis: sügav sukeldumine iteraatori protokolli
Iteratsioon on üks programmeerimise fundamentaalsemaid kontsepte. Alates loendielementide töötlemisest kuni andmevoogude lugemiseni töötame pidevalt järjestikuse informatsiooniga. JavaScriptis on meil võimsad ja elegantsed tööriistad nagu for...of tsükkel ja levitamissüntaks (...), mis muudavad sisseehitatud tüüpide (nt massiivid, stringid ja Map-objektid) itereerimise sujuvaks kogemuseks.
Kuid kas olete kunagi hetkeks peatunud ja mõelnud, mis teeb need objektid nii eriliseks? Miks saate kirjutada for (const char of "hello"), kuid mitte for (const prop of {a: 1, b: 2})? Vastus peitub ECMAScripti standardi võimsas, kuid sageli valesti mõistetud funktsioonis: iteraatori protokollis.
See protokoll ei ole lihtsalt sisemine mehhanism JavaScripti sisseehitatud objektidele. See on avatud standard, leping, mida iga objekt saab vastu võtta. Selle protokolli rakendamisega saate õpetada JavaScripti itereerima oma kohandatud objektide üle, muutes need keeles esmaklassilisteks kodanikeks. Saate avada sama süntaktilise elegantsi nagu for...of oma kohandatud andmestruktuuride jaoks, olgu selleks siis binaarpuu, aheldatud loend, mängu käikude jada või sündmuste ajatelg.
Selles põhjalikus juhendis demüstifitseerime iteraatori protokolli. Jagame selle põhikomponentideks, läbime kohandatud iteraatorite loomise nullist, uurime edasijõudnute kasutusjuhtumeid, nagu lõpmatud jadad, ja lõpuks avastame kaasaegse, lihtsustatud lähenemise, kasutades generaatorfunktsioone. Lõpuks saate mitte ainult aru, kuidas iteratsioon kulisside taga töötab, vaid ka volituse kirjutada väljendusrikkamat, korduvkasutatavamat ja idiomaatilisemat JavaScripti koodi.
Iteratsiooni tuum: mis on JavaScripti iteraatori protokoll?
Kõigepealt on oluline mõista, et "iteraatori protokoll" ei ole üks klass, mida laiendate, ega spetsiifiline funktsioon, mida kutsute. See on reeglite või kokkulepete komplekt, mida objekt peab järgima, et seda saaks pidada "itereeritavaks" ja et see saaks toota "iteraatori". Seda on kõige parem käsitleda kui lepingut. Kui teie objekt sellele lepingule alla kirjutab, lubab JavaScripti mootor teada, kuidas selle üle tsükeldada.
See leping on jagatud kaheks erinevaks osaks:
- Itereeritav protokoll: See määrab kõigepealt, kas objekt on üldse itereeritav.
- Iteraatori protokoll: See määratleb mehhanismid, kuidas objekti itereeritakse, üks väärtus korraga.
Vaatleme selle lepingu iga osa üksikasjalikult.
Lepingu esimene pool: Itereeritav protokoll
Itereeritav protokoll on üllatavalt lihtne. Sellel on ainult üks nõue:
An object is considered iterable if it has a specific, well-known property that provides a method for retrieving an iterator. This well-known property is accessed using Symbol.iterator.
So, for an object to be iterable, it must have a method accessible via the key [Symbol.iterator]. When this method is called, it must return an iterator object (which we'll cover in the next section).
You might be asking, "What is Symbol, and why not just use a string name like 'iterator'?" A Symbol is a unique and immutable primitive data type introduced in ES6. Its primary purpose is to serve as a unique key for object properties, preventing accidental name collisions. If the protocol used a simple string like 'iterator', your own code might define a property with the same name for a different purpose, leading to unpredictable bugs. By using Symbol.iterator, the language specification guarantees a unique, standardized key that won't clash with other code.
Seda saame hõlpsasti kontrollida sisseehitatud itereeritavate objektide puhul:
const anArray = [1, 2, 3];
const aString = "global";
const aMap = new Map();
console.log(typeof anArray[Symbol.iterator]); // "function"
console.log(typeof aString[Symbol.iterator]); // "function"
console.log(typeof aMap[Symbol.iterator]); // "function"
// A plain object is not iterable by default
const anObject = { a: 1, b: 2 };
console.log(typeof anObject[Symbol.iterator]); // "undefined"
Lepingu teine pool: Iteraatori protokoll
Kui objekt on tõestanud, et see on itereeritav, pakkudes meetodit [Symbol.iterator](), nihkub fookus selle meetodi tagastatud objektile: iteraatorile. Iteraator on tõeline tööloom; see on objekt, mis tegelikult haldab iteratsiooniprotsessi ja toodab väärtuste jada.
Iteraatori protokoll on samuti väga lihtne. Sellel on üks nõue:
Objekt on iteraator, kui sellel on meetod nimega next(). See next() meetod, kui seda kutsutakse, peaks tagastama objekti, millel on kaks spetsiifilist omadust:
done(boolean): See omadus annab märku iteratsiooni staatusest. See onfalse, kui jadas on veel väärtusi. See muutubtrue-ks, kui iteratsioon on lõpule viidud.value(mis tahes tüüp): See omadus sisaldab järjestikust väärtust. Kuidoneontrue, onvalueomadus valikuline ja sisaldab tavaliseltundefined.
Vaatame iseseisvat, käsitsi loodud iteraatorit, et seda tegevuses näha, täiesti eraldi mis tahes itereeritavast objektist. See iteraator loendab lihtsalt 1-st 3-ni.
const manualCounterIterator = {
count: 1,
next: function() {
if (this.count <= 3) {
return { value: this.count++, done: false };
} else {
return { value: undefined, done: true };
}
}
};
// Kutsume next() korduvalt iga väärtuse saamiseks
console.log(manualCounterIterator.next()); // { value: 1, done: false }
console.log(manualCounterIterator.next()); // { value: 2, done: false }
console.log(manualCounterIterator.next()); // { value: 3, done: false }
console.log(manualCounterIterator.next()); // { value: undefined, done: true }
console.log(manualCounterIterator.next()); // { value: undefined, done: true } - See jääb lõpetatuks
See on fundamentaalne mehaanika, mis käivitab iga for...of tsükli. Kui kirjutate for (const item of iterable), teeb JavaScripti mootor kulisside taga järgmist:
- See kutsub
[Symbol.iterator]()meetodiiterableobjektil, et saada iteraator. - Seejärel kutsub see korduvalt
next()meetodit sellel iteraatoril. - Iga tagastatud objekti puhul, kus
doneonfalse, määrab seevalueteie tsükli muutujale (item) ja täidab tsükli keha. - Kui
next()tagastab objekti, kusdoneontrue, tsükkel lõpeb.
Loomine nullist: praktiline juhend kohandatud iteratsioonile
Nüüd, kui oleme teooriast aru saanud, paneme selle praktikasse. Loome kohandatud klassi nimega Timeline. See klass haldab ajalooliste sündmuste kogumit ja meie eesmärk on muuta see otse itereeritavaks, võimaldades meil sündmuste üle kronoloogilises järjekorras tsükeldada.
Kasutusjuhtum: `Timeline` klass
Meie Timeline klass salvestab sündmusi, millest igaüks on objekt, millel on year ja description. Soovime kasutada for...of tsüklit nende sündmuste üle itereerimiseks, sorteerituna aasta järgi.
class Timeline {
constructor() {
this.events = [];
}
addEvent(year, description) {
this.events.push({ year, description });
}
}
const myTimeline = new Timeline();
myTimeline.addEvent(1995, "JavaScript is created");
myTimeline.addEvent(2009, "Node.js is introduced");
myTimeline.addEvent(1997, "ECMAScript standard is first published");
myTimeline.addEvent(2015, "ES6 (ECMAScript 2015) is released");
// Eesmärk: Panna järgmine kood tööle
// for (const event of myTimeline) {
// console.log(`${event.year}: ${event.description}`);
// }
Samm-sammult rakendamine
Oma eesmärgi saavutamiseks peame rakendama iteraatori protokolli. See tähendab [Symbol.iterator]() meetodi lisamist meie Timeline klassi.
See meetod peab tagastama uue objekti – iteraatori –, mis sisaldab next() meetodit ja haldab iteratsiooni olekut (nt millise sündmuse juures me praegu oleme). On kriitiline disainiprintsiip, et iteratsiooni olek peaks asuma iteraatoril, mitte itereeritaval objektil endal. See võimaldab sama ajatelje üle korraga mitut sõltumatut iteratsiooni.
class Timeline {
constructor() {
this.events = [];
}
addEvent(year, description) {
// Lisame lihtsa kontrolli andmete terviklikkuse tagamiseks
if (typeof year !== 'number' || typeof description !== 'string') {
throw new Error("Invalid event data");
}
this.events.push({ year, description });
}
// 1. samm: Rakenda itereeritav protokoll
[Symbol.iterator]() {
// Sorteeri sündmused kronoloogiliselt iteratsiooniks.
// Loome koopia, et mitte muuta algse massiivi järjestust.
const sortedEvents = [...this.events].sort((a, b) => a.year - b.year);
let currentIndex = 0;
// 2. samm: Tagasta iteraatori objekt
return {
// 3. samm: Rakenda iteraatori protokoll next() meetodiga
next: () => { // Kasutades noolfunktsiooni `sortedEvents` ja `currentIndex` jäädvustamiseks
if (currentIndex < sortedEvents.length) {
// On veel sündmusi, mille üle itereerida
const currentEvent = sortedEvents[currentIndex];
currentIndex++;
return { value: currentEvent, done: false };
} else {
// Oleme jõudnud sündmuste lõppu
return { value: undefined, done: true };
}
}
};
}
}
Maagia tunnistajaks: Meie kohandatud itereeritava kasutamine
Protokolli õige rakendamisega on meie Timeline objekt nüüd täieõiguslik itereeritav. See integreerub sujuvalt JavaScripti iteratsioonipõhiste keelefunktsioonidega. Vaatame seda tegutsemas.
const myTimeline = new Timeline();
myTimeline.addEvent(1995, "JavaScript is created");
myTimeline.addEvent(2009, "Node.js is introduced");
myTimeline.addEvent(1997, "ECMAScript standard is first published");
myTimeline.addEvent(2015, "ES6 (ECMAScript 2015) is released");
console.log("--- Kasutades for...of tsüklit ---");
for (const event of myTimeline) {
console.log(`${event.year}: ${event.description}`);
}
// Väljund:
// 1995: JavaScript is created
// 1997: ECMAScript standard is first published
// 2009: Node.js is introduced
// 2015: ES6 (ECMAScript 2015) is released
console.log("\n--- Kasutades levitamissüntaksit ---");
const eventsArray = [...myTimeline];
console.log(eventsArray);
// Väljund: Massiiv sündmuse objektidest, sorteeritud aasta järgi
console.log("\n--- Kasutades Array.from() ---");
const eventsFrom = Array.from(myTimeline);
console.log(eventsFrom);
// Väljund: Massiiv sündmuse objektidest, sorteeritud aasta järgi
console.log("\n--- Kasutades destruktureerivat omistamist ---");
const [firstEvent, secondEvent] = myTimeline;
console.log(firstEvent);
// Väljund: { year: 1995, description: 'JavaScript is created' }
console.log(secondEvent);
// Väljund: { year: 1997, description: 'ECMAScript standard is first published' }
See on protokolli tõeline jõud. Järgides standardset lepingut, oleme oma kohandatud objekti muutnud ühilduvaks tohutu hulga olemasolevate ja tulevaste JavaScripti funktsioonidega ilma igasuguse lisatööta.
Iteratsioonioskuste täiustamine
Nüüd, kui olete põhitõed omandanud, uurime mõningaid edasijõudnute kontsepte, mis annavad teile veelgi suurema kontrolli ja paindlikkuse.
Olekute ja sõltumatute iteraatorite tähtsus
Meie Timeline näites olime väga ettevaatlikud, et paigutada iteratsiooni olek (currentIndex ja sortedEvents koopia) iteraatori objekti sisse, mille tagastas [Symbol.iterator](). Miks see nii oluline on? Sest see tagab, et iga kord, kui alustame iteratsiooni, saame *uue, sõltumatu iteraatori*.
See võimaldab mitmel tarbijal itereerida sama itereeritava objekti üle, üksteist segamata. Kujutage ette, kui currentIndex oleks Timeline eksemplari omadus ise – see oleks kaos!
const sharedTimeline = new Timeline();
sharedTimeline.addEvent(1, 'Event A');
sharedTimeline.addEvent(2, 'Event B');
sharedTimeline.addEvent(3, 'Event C');
const iterator1 = sharedTimeline[Symbol.iterator]();
const iterator2 = sharedTimeline[Symbol.iterator]();
console.log(iterator1.next().value); // { year: 1, description: 'Event A' }
console.log(iterator2.next().value); // { year: 1, description: 'Event A' } (Alustab oma iteratsiooni)
console.log(iterator1.next().value); // { year: 2, description: 'Event B' } (iterator2 ei mõjuta)
Lõpmatusse: Lõputute jadade loomine
Iteraatori protokoll ei nõua, et iteratsioon kunagi lõppeks. Omadus done võib lihtsalt jääda igaveseks false. See võimaldab meil modelleerida lõpmatuid jadasid, mis võivad olla uskumatult kasulikud ülesannete jaoks, nagu unikaalsete ID-de genereerimine, juhuslike andmete voogude loomine või matemaatiliste jadade modelleerimine.
Loome iteraatori, mis genereerib Fibonacci jada lõputult.
const fibonacciSequence = {
[Symbol.iterator]() {
let a = 0, b = 1;
return {
next() {
[a, b] = [b, a + b];
return { value: a, done: false };
}
};
}
};
// Me ei saa siin kasutada levitamissüntaksit ega Array.from() meetodit, kuna see tekitaks lõputu tsükli ja vea!
// const fibArray = [...fibonacciSequence]; // OHT: Lõputu tsükkel!
// Peame seda tarbima ettevaatlikult, pakkudes oma lõpetamise tingimuse.
console.log("Esimesed 10 Fibonacci numbrit:");
let count = 0;
for (const number of fibonacciSequence) {
console.log(number);
count++;
if (count >= 10) {
break; // Oluline on tsüklist väljuda!
}
}
Valikulised iteraatorimeetodid: `return()`
Keerukamate stsenaariumide, eriti ressursside haldamisega (nagu failikäepidemed või võrguühendused) seotud olukordade jaoks võib iteraatoril valikuliselt olla meetod return(). Seda meetodit kutsub JavaScripti mootor automaatselt, kui iteratsioon peatatakse ennetähtaegselt. See võib juhtuda, kui `break`, `return`, `throw` lause väljub `for...of` tsüklist enne selle lõppemist.
See annab teie iteraatorile võimaluse teha puhastustöid.
function createResourceIterator() {
let resourceIsOpen = true;
console.log("Resource opened.");
let i = 0;
return {
next() {
if (i < 3) {
return { value: ++i, done: false };
} else {
console.log("Iterator finished naturally.");
resourceIsOpen = false;
console.log("Resource closed.");
return { done: true };
}
},
return() {
if (resourceIsOpen) {
console.log("Iterator terminated early. Closing resource.");
resourceIsOpen = false;
}
return { done: true }; // Peab tagastama kehtiva iteraatori tulemuse
}
};
}
console.log("--- Varajase väljumise stsenaarium ---");
const resourceIterable = { [Symbol.iterator]: createResourceIterator };
for (const value of resourceIterable) {
console.log(`Processing value: ${value}`);
if (value > 1) {
break; // See käivitab return() meetodi
}
}
Märkus: Olemas on ka throw() meetod veateavituseks, kuid seda kasutatakse peamiselt generaatorfunktsioonide kontekstis, mida käsitleme järgmisena.
Kaasaegne lähenemine: lihtsustamine generaatorfunktsioonidega
Nagu oleme näinud, nõuab iteraatori protokolli käsitsi rakendamine hoolikat olekuhaldust ja korduvat koodi iteraatori objekti loomiseks ja objektide { value, done } tagastamiseks. Kuigi selle protsessi mõistmine on oluline, tutvustas ES6 palju elegantsemat lahendust: generaatorfunktsioonid.
Generaatorfunktsioon on eriline funktsiooni tüüp, mida saab peatada ja jätkata, võimaldades sellel aja jooksul väärtuste jada toota. See lihtsustab iteraatorite loomist tohutult.
Põhisüntaks:
function*: Tärn deklareerib funktsiooni generaatorina.yield: See märksõna peatab generaatori täitmise ja "annab" väärtuse. Kui iteraatorinext()meetodit uuesti kutsutakse, jätkab funktsioon sealt, kus see pooleli jäi.
Kui kutsute generaatorfunktsiooni, ei täida see oma keha kohe. Selle asemel tagastab see iteraatori objekti, mis vastab täielikult protokollile. JavaScripti mootor käsitleb olekumasinat, next() meetodit ja objektide { value, done } loomist automaatselt teie eest.
Meie `Timeline` näite refaktoreerimine
Vaatame, kui dramaatiliselt saavad generaatorfunktsioonid meie Timeline rakendust lihtsustada. Loogika jääb samaks, kuid kood muutub palju loetavamaks ja veaohtlikumaks.
class Timeline {
constructor() {
this.events = [];
}
addEvent(year, description) {
this.events.push({ year, description });
}
// Refaktoreeritud generaatorfunktsiooniga!
*[Symbol.iterator]() { // Tärn teeb sellest generaatorimeetodi
// Loo sorteeritud koopia
const sortedEvents = [...this.events].sort((a, b) => a.year - b.year);
// Tsükli kaudu sorteeritud sündmused
for (const event of sortedEvents) {
// yield peatab funktsiooni ja tagastab väärtuse
yield event;
}
// Kui funktsioon lõpeb, märgitakse iteraator automaatselt "done" olekusse
}
}
// Kasutus on täpselt sama, kuid implementatsioon on puhtam!
const myGenTimeline = new Timeline();
myGenTimeline.addEvent(2002, "The Euro currency is introduced");
myGenTimeline.addEvent(1998, "Google is founded");
for (const event of myGenTimeline) {
console.log(`${event.year}: ${event.description}`);
}
Vaadake erinevust! Keeruline iteraatori objekti käsitsi loomine on kadunud. Olek (millise sündmuse juures me oleme) hallatakse generaatorfunktsiooni peatatud oleku kaudu kaudselt. See on kaasaegne, eelistatud viis iteraatori protokolli rakendamiseks.
`yield*` võimsus
Generaatorfunktsioonidel on veel üks superjõud: yield* (yield star). See võimaldab generaatoril delegeerida iteratsiooniprotsessi teisele itereeritavale objektile. See on uskumatult võimas tööriist iteraatorite loomiseks mitmest allikast.
Kujutage ette, et meil on `Project` klass, millel on mitu `Timeline` objekti (nt üks disainiks, teine arenduseks). Saame muuta `Project` ise itereeritavaks ja see itereerib sujuvalt kõigi oma ajatelgede kõigi sündmuste üle järjestikku.
class Project {
constructor(name) {
this.name = name;
this.designTimeline = new Timeline();
this.devTimeline = new Timeline();
}
*[Symbol.iterator]() {
console.log(`Iterating through events for project: ${this.name}`);
console.log("--- Design Events ---");
yield* this.designTimeline; // Delegeerib disaini ajatelje iteraatorile
console.log("--- Development Events ---");
yield* this.devTimeline; // Seejärel delegeerib arenduse ajatelje iteraatorile
}
}
const websiteProject = new Project("Global Website Relaunch");
websiteProject.designTimeline.addEvent(2023, "Initial wireframes created");
websiteProject.designTimeline.addEvent(2024, "Final brand guide approved");
websiteProject.devTimeline.addEvent(2024, "Backend API developed");
websiteProject.devTimeline.addEvent(2025, "Frontend deployment");
for (const event of websiteProject) {
console.log(` - ${event.year}: ${event.description}`);
}
Suur pilt: miks iteraatori protokoll on kaasaegse JavaScripti nurgakivi
Iteraatori protokoll on palju enamat kui akadeemiline uudishimu või funktsioon teegi autoritele. See on fundamentaalne disainimuster, mis soodustab koostalitlusvõimet ja elegantset koodi. Mõelge sellele kui universaalsele adapterile. Muutes oma objektid sellele standardile vastavaks, ühendate need tohutusse keelefunktsioonide ökosüsteemi, mis on loodud töötama mis tahes andmejadaga.
Itereeritavale protokollile tuginevate funktsioonide loend on ulatuslik ja kasvav:
- Tsüklid:
for...of - Massiivi loomine/liitmine: Levitamissüntaks (
[...iterable]) jaArray.from(iterable) - Andmestruktuurid: Konstruktorid
new Map(iterable),new Set(iterable),new WeakMap(iterable)janew WeakSet(iterable)aktsepteerivad kõik itereeritavaid objekte. - Asünkroonsed operatsioonid:
Promise.all(iterable),Promise.race(iterable)jaPromise.any(iterable)töötavad Promiside itereeritavate objektidega. - Destruktureerimine: Destruktureerivat omistamist saate kasutada mis tahes itereeritavaga:
const [first, second] = myIterable; - Uued API-d: Kaasaegsed API-d nagu
Intl.Segmenterteksti segmenteerimiseks tagastavad samuti itereeritavaid objekte.
Kui muudate oma kohandatud andmestruktuurid itereeritavaks, ei luba te lihtsalt for...of tsüklit; te muudate need ühilduvaks kogu selle võimsa tööriistakomplektiga, tagades, et teie kood on nii edasiühilduv kui ka lihtne teistele arendajatele kasutada ja mõista.
Järeldus: teie järgmised sammud iteratsioonis
Oleme rännanud itereeritavate ja iteraatoriprotokollide alusreeglitest oma kohandatud iteraatorite loomiseni ja lõpuks generaatorfunktsioonide puhta, kaasaegse süntaksini. Nüüd on teil teadmised, kuidas õpetada JavaScripti läbima mis tahes andmestruktuuri, mida suudate ette kujutada.
Selle protokolli omandamine on oluline samm teie teekonnal JavaScripti arendajana. See viib teid keelefunktsioonide tarbijast loojaks, kes saab keele põhivõimalusi laiendada vastavalt oma spetsiifilistele vajadustele.
Tegutsemisjuhised globaalsetele arendajatele
- Auditeerige oma koodi: Otsige oma praegustes projektides objekte, mis esindavad andmete jada. Kas te itereerite nende üle kohandatud, mittestandardsete meetoditega nagu
.forEachItem()või.getItems()? Kaaluge nende refaktoreerimist, et rakendada standardset iteraatori protokolli parema koostalitlusvõime tagamiseks. - Võtke omaks laiskus: Kasutage iteraatoreid ja eriti generaatoreid suurte või isegi lõpmatute andmekogude esindamiseks. See võimaldab teil andmeid töödelda vastavalt vajadusele, mis toob kaasa olulisi parandusi mälu tõhususes ja jõudluses. Te arvutate ainult seda, mida vajate, siis kui seda vajate.
- Prioriseerige generaatoreid: Iga uue objekti puhul, mis peaks olema itereeritav, tehke generaatorfunktsioonidest (
function*) oma vaikimisi valik. Need on kokkuvõtlikumad, vähem altid olekuhalduse vigadele ja loetavamad kui käsitsi implementatsioon. - Mõelge jadades: Hakake programmeerimisprobleeme vaatama jadade vaatenurgast. Kas keerukat äriprotsessi, andmetöötlustorustikku või kasutajaliidese oleku üleminekut saab modelleerida sammude jadana? Kui jah, siis iteraator võib olla selleks ideaalne, elegantne tööriist.
Integreerides iteraatori protokolli oma arendustööriistakomplekti, kirjutate puhtamat, võimsamat ja idiomaatilisemat JavaScripti, millest saavad aru ja mida hindavad arendajad kõikjal maailmas.