Iterables

Opakovatelné objekty jsou zobecněním polí. To je koncept, který nám umožňuje, aby byl jakýkoli objekt použitelný v for..of smyčka.

Pole jsou samozřejmě iterovatelné. Ale existuje mnoho dalších vestavěných objektů, které jsou také iterovatelné. Například řetězce jsou také iterovatelné.

Pokud objekt není technicky pole, ale představuje kolekci (seznam, sadu) něčeho, pak for..of je skvělá syntaxe pro zacyklení, takže se podívejme, jak to udělat.

Symbol.iterator

Koncept iterovatelných můžeme snadno pochopit tak, že si vytvoříme jeden z našich.

Máme například objekt, který není polem, ale vypadá jako vhodný pro for..of .

Jako range objekt, který představuje interval čísel:

let range = {
 from: 1,
 to: 5
};

// We want the for..of to work:
// for(let num of range) ... num=1,2,3,4,5

Chcete-li vytvořit range objekt iterovatelný (a tedy nechat for..of work) potřebujeme přidat metodu do objektu s názvem Symbol.iterator (speciální vestavěný symbol jen pro to).

  1. Když for..of spustí, zavolá tuto metodu jednou (nebo chyby, pokud nejsou nalezeny). Metoda musí vrátit iterátor – objekt s metodou next .
  2. Dále, for..of funguje pouze s tímto vráceným objektem .
  3. Když for..of chce další hodnotu, zavolá next() na tomto objektu.
  4. Výsledek next() musí mít tvar {done: Boolean, value: any} , kde done=true znamená, že smyčka je dokončena, jinak value je další hodnota.

Zde je úplná implementace pro range s poznámkami:

let range = {
 from: 1,
 to: 5
};

// 1. call to for..of initially calls this
range[Symbol.iterator] = function() {

 // ...it returns the iterator object:
 // 2. Onward, for..of works only with the iterator object below, asking it for next values
 return {
 current: this.from,
 last: this.to,

 // 3. next() is called on each iteration by the for..of loop
 next() {
 // 4. it should return the value as an object {done:.., value :...}
 if (this.current <= this.last) {
 return { done: false, value: this.current++ };
 } else {
 return { done: true };
 }
 }
 };
};

// now it works!
for (let num of range) {
 alert(num); // 1, then 2, 3, 4, 5
}

Vezměte prosím na vědomí hlavní rys iterables:oddělení obav.

  • range sám o sobě nemá next() metoda.
  • Namísto toho je voláním range[Symbol.iterator]() vytvořen jiný objekt, takzvaný „iterátor“. a jeho next() generuje hodnoty pro iteraci.

Objekt iterátoru je tedy oddělený od objektu, přes který iteruje.

Technicky je můžeme sloučit a používat range sám jako iterátor, aby byl kód jednodušší.

Takhle:

let range = {
 from: 1,
 to: 5,

 [Symbol.iterator]() {
 this.current = this.from;
 return this;
 },

 next() {
 if (this.current <= this.to) {
 return { done: false, value: this.current++ };
 } else {
 return { done: true };
 }
 }
};

for (let num of range) {
 alert(num); // 1, then 2, 3, 4, 5
}

Nyní range[Symbol.iterator]() vrátí range samotný objekt:má nezbytný next() a pamatuje si aktuální průběh iterace v this.current . Kratší? Ano. A někdy je to také fajn.

Nevýhodou je, že nyní není možné mít dva for..of smyčky běžící nad objektem současně:budou sdílet stav iterace, protože existuje pouze jeden iterátor – samotný objekt. Ale dvě paralelní for-ofs jsou vzácná věc, dokonce i v asynchronních scénářích.

Nekonečné iterátory

Možné jsou také nekonečné iterátory. Například range se stane nekonečným pro range.to = Infinity . Nebo můžeme vytvořit iterovatelný objekt, který generuje nekonečnou posloupnost pseudonáhodných čísel. Také to může být užitečné.

Na next nejsou žádná omezení , může vracet stále více hodnot, to je normální.

Samozřejmě, for..of smyčka přes takovou iterovatelnou by byla nekonečná. Ale vždy to můžeme zastavit pomocí break .

Řetězec je iterovatelný

Pole a řetězce jsou nejrozšířenější vestavěné iterovatelné.

Pro řetězec for..of smyčky přes jeho znaky:

for (let char of "test") {
 // triggers 4 times: once for each character
 alert( char ); // t, then e, then s, then t
}

A s náhradními páry to funguje správně!

let str = '𝒳😂';
for (let char of str) {
 alert( char ); // 𝒳, and then 😂
}

Výslovné volání iterátoru

Pro hlubší pochopení se podívejme, jak používat iterátor explicitně.

Přes řetězec budeme iterovat přesně stejným způsobem jako for..of , ale s přímými hovory. Tento kód vytvoří iterátor řetězce a získá z něj hodnoty „ručně“:

let str = "Hello";

// does the same as
// for (let char of str) alert(char);

let iterator = str[Symbol.iterator]();

while (true) {
 let result = iterator.next();
 if (result.done) break;
 alert(result.value); // outputs characters one by one
}

To je zřídka potřeba, ale poskytuje nám větší kontrolu nad procesem než for..of . Můžeme například rozdělit proces iterace:trochu iterovat, pak zastavit, udělat něco jiného a pak pokračovat později.

Iterables a pole-likes

Dva oficiální termíny vypadají podobně, ale jsou velmi odlišné. Ujistěte se, že jim dobře rozumíte, abyste předešli zmatkům.

  • Iterables jsou objekty, které implementují Symbol.iterator metodou, jak je popsáno výše.
  • Pole podobné jsou objekty, které mají indexy a length , takže vypadají jako pole.

Když používáme JavaScript pro praktické úkoly v prohlížeči nebo v jakémkoli jiném prostředí, můžeme se setkat s objekty, které jsou iterovatelné nebo podobné poli, nebo obojí.

Například řetězce jsou oba iterovatelné (for..of funguje na nich) a jako pole (mají číselné indexy a length ).

Ale iterovatelný nemusí být jako pole. A naopak, pole podobné nemusí být iterovatelné.

Například range ve výše uvedeném příkladu je iterovatelný, ale ne jako pole, protože nemá indexované vlastnosti a length .

A zde je objekt, který je podobný poli, ale nelze jej opakovat:

let arrayLike = { // has indexes and length => array-like
 0: "Hello",
 1: "World",
 length: 2
};

// Error (no Symbol.iterator)
for (let item of arrayLike) {}

Iterovatelné i poli podobné obvykle nejsou pole , nemají push , pop atd. To je poněkud nepohodlné, pokud máme takový objekt a chceme s ním pracovat jako s polem. Např. rádi bychom pracovali s range pomocí metod pole. Jak toho dosáhnout?

Pole.from

Existuje univerzální metoda Array.from, která přebírá iterovatelnou hodnotu nebo hodnotu podobnou poli a vytváří „skutečné“ Array z toho. Pak na něm můžeme volat metody pole.

Například:

let arrayLike = {
 0: "Hello",
 1: "World",
 length: 2
};

let arr = Array.from(arrayLike); // (*)
alert(arr.pop()); // World (method works)

Array.from na řádku (*) vezme objekt, zkontroluje, zda je iterovatelný nebo podobný poli, pak vytvoří nové pole a zkopíruje do něj všechny položky.

Totéž se stane pro iterovatelný:

// assuming that range is taken from the example above
let arr = Array.from(range);
alert(arr); // 1,2,3,4,5 (array toString conversion works)

Úplná syntaxe pro Array.from také nám umožňuje poskytovat volitelnou funkci „mapování“:

Array.from(obj[, mapFn, thisArg])

Volitelný druhý argument mapFn může být funkce, která bude aplikována na každý prvek před jeho přidáním do pole, a thisArg nám umožňuje nastavit this za to.

Například:

// assuming that range is taken from the example above

// square each number
let arr = Array.from(range, num => num * num);

alert(arr); // 1,4,9,16,25

Zde používáme Array.from pro přeměnu řetězce na pole znaků:

let str = '𝒳😂';

// splits str into array of characters
let chars = Array.from(str);

alert(chars[0]); // 𝒳
alert(chars[1]); // 😂
alert(chars.length); // 2

Na rozdíl od str.split , spoléhá na iterovatelnou povahu řetězce, a tak, stejně jako for..of , správně funguje s náhradními páry.

Technicky to zde dělá totéž jako:

let str = '𝒳😂';

let chars = []; // Array.from internally does the same loop
for (let char of str) {
 chars.push(char);
}

alert(chars);

…Ale je kratší.

Můžeme dokonce vytvořit náhradní slice na to:

function slice(str, start, end) {
 return Array.from(str).slice(start, end).join('');
}

let str = '𝒳😂𩷶';

alert( slice(str, 1, 3) ); // 😂𩷶

// the native method does not support surrogate pairs
alert( str.slice(1, 3) ); // garbage (two pieces from different surrogate pairs)

Shrnutí

Objekty, které lze použít v for..of se nazývají iterovatelné .

  • Technicky musí iterable implementovat metodu s názvem Symbol.iterator .
    • Výsledek obj[Symbol.iterator]() se nazývá iterátor . Zvládá další iterační proces.
    • Iterátor musí mít metodu s názvem next() který vrací objekt {done: Boolean, value: any} , zde done:true označuje konec iteračního procesu, jinak value je další hodnota.
  • Symbol.iterator metoda je volána automaticky pomocí for..of , ale můžeme to udělat také přímo.
  • Vestavěné iterovatelné prvky, jako jsou řetězce nebo pole, také implementují Symbol.iterator .
  • Iterátor řetězců ví o náhradních párech.

Objekty, které mají indexované vlastnosti a length se nazývají array-like . Takové objekty mohou mít také jiné vlastnosti a metody, ale postrádají vestavěné metody polí.

Pokud se podíváme do specifikace – uvidíme, že většina vestavěných metod předpokládá, že pracují s iterovatelnými nebo poli podobnými namísto „skutečných“ polí, protože to je abstraktnější.

Array.from(obj[, mapFn, thisArg]) dělá skutečný Array z iterovatelného nebo pole podobného obj , a pak na něm můžeme použít metody pole. Volitelné argumenty mapFn a thisArg nám umožňují aplikovat funkci na každou položku.