Let there be a cool variable (JS)
Nemrégiben tettem fel facebookon a találós kérdést, hogy mi lesz a végeredménye ennek a kódnak:
for (var i=0; i < 3; i++) {
setTimeout(function() { console.log(i); }, 100 * i);
}
Érkezett helyes megoldás azonban a "miért" nem teljesen pontos. Szóval öntsünk tiszta vizet a pohárba.
A helyes válasz "természetesen" az hogy 3 3 3. Pedig sokan a 0 1 2 -őt várnák megoldásnak. Nem csoda, ez az egyik sarkalatos pontja a JavaScript nyelvnek ami miatt oly sokan utálják. Aki már jó ideje használja a JS-et az természetesen ismeri ezeket a "trükköket".
Na vizsgáljuk meg közelebbről, hogy mi történik valójában:
- A for ciklus, ciklusváltozója az i.
- A ciklusmagon belül kiadott setTimeout utasítás egy késleltetett futtatást tesz lehetővé.
- A benne megadott névtelen függvény kódja háromszor fog lefutni késleltetve. Az első 0 a második 100 míg a harmadik 200ms múlva.
- A névtelen függvény kódja kiírja a konzolra az i változó értékét.
Na de milyen értéket! Mind a három kiíratáskor az i ugyanazt a változót jelöli, ugyanarra a memóriaterületre "mutat". Azonban mindhárom kiíratás késleltetve fut le, akkor amikor az i változó értéke már 3. Tehát magyarán a ciklus hamarabb lefut mint a ciklusmagban lévő kiíratás, amikor is az i értéke már a végérték+1 -et tartalmazza.
Hogy miért 3 és miért nem 2?
Nos aki nálam tanult bevezetés a programozásba című tárgyat, az tudja, hogy a for ciklus, ciklusváltozója a ciklus lefutása után (szabályos befejezés esetén) azt az értéket tárolja
- amelyre utoljára futott le a ciklus
vagy
- amire már éppen nem futott le a ciklus.
Hogy melyik az igaz? Az attól függ. Nyelvenként eltérő. Mindenesetre a JS az utóbbit vallja.
Tehát 3 lesz az értéke, mert erre már nem fut le a ciklus. Nem teljesíti a feltételt.
Hogyan érhetem el a 0 1 2 eredményt?
Több eshetőség is van. Az első példában a leggyakoribb megoldást használjuk az IIFE (Immediately Invoked Function Expression) technikát. Ebben az esetben a ciklusmag kódját "bebugyoláljuk" egy függvénybe (zölddel jelölve a kódban) aminek az i ciklusváltozót átadjuk paraméterül, és ezt a szintén névtelen bebugyolált függvényt azonnal meg is hívjuk. Ezáltal az i hatásköre a függvényhez fog kötődni ami a megfelelő értéket adja neki mindhárom esetben.
Íme a kód:
for (var i=0; i < 3; i++) {
(function(i) {
setTimeout(function() { console.log(i); }, 100 * i);
})(i);
}
Tudom, nem túl szép, de működik az összes JS verzióval.
Szépítsünk picit és használjuk az új Ecmascript6 adta lehetőségeket:
for (let i=0; i < 3; i++) {
setTimeout(function() { console.log(i); }, 100 * i);
}
A var kulcsszó helyett a let-et használva egyből megoldódik a problémánk. Hát nem szép? De. Csak sok böngésző még nem támogatja.
Főleg az IE csak a 11-es verziótól kezdve. Bővebb infó itt: caniuse.com
Nos ha már Ecmascript6 akkor szépítsük tovább és használjuk az arrow függvényt. Ez most úgy is divatba jött több programnyelvnél is:
for (let i=0; i < 3; i++) {
setTimeout(() => console.log(i*100), 100 * i);
}
Ezzel végére is értünk a probléma elemzésének. Remélem sikerült mindent tisztázni ezzel kapcsolatban.
Zárógondolatként pedig egy link. Amikor JS-ben leírom a let kulcsszót, nekem mindig ez ugrik be