ugrás a tartalomhoz

Soha ne használd a for…in-t tömbök bejárására!

presidento · 2010. Szep. 27. (H), 07.52

Rejtélyesen viselkedik az éppen megírt JavaScript programom, és nem találom az okát. Látszólag mindent jól csináltam, és mégsem megy! Egyszer csak leesik… hogy erre nem gondoltam… de úgy látom, más sem, ezért leírom, mindannyiunk okulására.

Adott egy nagy mátrix, amiből kell csinálnom egy kisebb mátrixot úgy, hogy csak a megadott sorokat és oszlopokat veszem bele. Mi sem egyszerűbb:

originalMatrix.getParts = function( selected ) {
	var newMatrix = new Matrix( selected.length, selected.length );
	for( var row in selected ) {
		for( var col in selected ) {
			newMatrix.set( row, col, this.get( selected[row], selected[col] ) );
		}
	}
	return newMatrix;
};

(A selected egy egyszerű tömb, amibe push()-sal beraktam azoknak a soroknak/oszlopoknak a sorszámát, amit szeretnék átvinni a régiből az újba.) Miért ezt a módját választottam az iterációnak? Mert elegánsabbnak tartottam, a klasszikus for ciklust:

    for ( var row = 0; row < selected.length; row++ ) {
        for ( var col = 0; col < selected.length; col++ ) {
            newMatrix.set( row, col, this.get( selected[row], selected[col] ) );
        }
    }

Vagy, hogy mit ne mondjak, a .forEach() használatát:

    var thisMatrix = this;
    selected.forEach( function( oldRow, newRow ) {
        selected.forEach( function( oldCol, newCol ) {
            newMatrix.set(newRow, newCol, thisMatrix.get( oldRow, oldCol ) );
        });
    });

…és mekkorát tévedtem!

A mátrix „osztály”, amit használnom kellett, egyetlen tömbben tárolja az adatokat, az elérés függvényei egyszerűek:

//indexes start at zero
Matrix.prototype.get = function (r,c) {
 return this.data[r*this.cols + c];
}

Matrix.prototype.set = function (r,c,x) {
 this.data[r*this.cols + c]=x;
}

És ezen a ponton történtek a bajok.

Ugyanis a for-in a megkapott objektum felsorolható tulajdonságainak nevein megy végig, nem a megkapott tömb indexein. Egy tulajdonság neve pedig, akárhogy is nézzük, egy string. És ahogyan Török Gábor kifejtette nemrég, JavaScriptben sajnos ugyanazt az operátort használhatjuk összeadásra és string összefűzésre. Így alakult, hogy a newMatrix.set törzsében a következő kifejezés ](string)r * (number)this.cols + (string)c. Persze, hogy a data ezen attribútumait hiába állítgattam.

Sajnos ezt az apróságot nem szokás megemlíteni, sőt a Google-ban rákeresve az első helyeken mind tömbbel mutatják be a for-in használatát. Szomorú.

(Van még egy ok, ami miatt célszerű kerülni a használatát – de ezt néha megemlítik és könnyebb is rájönni a hiba okára, ha belefutunk –, nevezetesen ha az input tömb egyéb tulajdonságokkal is rendelkezik, akkor azok is megjelennek a felsorolásban.)

 
1

Rosszra kerestél

Török Gábor · 2010. Szep. 27. (H), 07.57
Ne a javascript for in-re keress rá, és akkor nem kapsz for-in-es találatokat. :)

Anno ugyanebbe már én is belefutottam. Érdekessége a for-in-nek, hogy a prototípus lánctól is örököl mindent, aminek kivédésére a hasOwnProperty() metódus használható.
3

Nem egészen

presidento · 2010. Szep. 27. (H), 09.08
Amire én szerettem volna kihegyezni a bejegyzést, azt te nem említetted: a for-in stringeket ad vissza, holott a tömböt egészekkel indexeljük!

Az alábbi példában a tömb páros indexű elemeihez hozzáadjuk az őt követő (páratlan indexű) elemet:
function test1() {
	var a = [0,1,2,3,4,5,6,7,8,9];
	for ( var i = 0; i < a.length; i++ ) {
		if (!(i%2)) a[i] += a[i+1];
	}
	return a; // [1,1,5,3,9,5,13,7,17,9]
}

function test2() {
	var a = [0,1,2,3,4,5,6,7,8,9];
	for ( var i in a ) {
		if (!(i%2)) a[i] += a[i+1];
	}
	return a; // [NaN,1,NaN,3,NaN,5,NaN,7,NaN,9]
}

alert( test1() );
alert( test2() );
A Google kereséssel nem az a bajom, hogy for-in-es találatokat ad, hanem hogy ezekben a for-in használatát mind tömbbel mutatják be, holott a for-in objektumokra való.
6

Feltételezés

Poetro · 2010. Szep. 27. (H), 10.55
Ez nem a for-in hibája, az mindig string-ként kezeli a tulajdonságot (mivel egyébként is minden tulajdonság neve string, de amennyiben az index neked számként kell, az könnyen megoldható:
function test3() { 
  var a = [0,1,2,3,4,5,6,7,8,9], i;  
  for (i in a) {  
    i = +i;
    if (!(i%2)) {
      a[i] += a[i+1];
    }
  }  
  return a; // [1,1,5,3,9,5,13,7,17,9]  
}
Még ha ez az Array-ben nem is látszik, minden tulajdonság neve string, ahogy az var x = []; x["3"] = 3 is teljesen valid.
Például:
function test4() { 
  var a = [0,1,2,3,4,5,6,7,8,9], i, j;  
  for (i in a) {          // i string 
    j = ( +i ) + 1;       // j szám
    if (String(j) in a) { // j tulajdonságot (mint string) keressük az a tömbben
      a[i] = a[j] + 0;    // i tulajdonágot módosítjuk arra, ami a j tulajdonság
    }
  }  
  return a; //  [1, 2, 3, 4, 5, 6, 7, 8, 9, 9]
}
8

Nem arra való

presidento · 2010. Szep. 27. (H), 11.22
Ilyen rosszul fogalmazok?

A for-in jól működik, nincs vele semmi gondom, viszont nem tömbökre való, hanem objektumokra. Az objektumok tulajdonságainak a nevein megy végig. A tömbnek meg indexei vannak.

Persze, megoldható, ezt hívják work-around-nak. De ki az, aki minden egyes for-in tömb bejárásnál megoldja, hogy a tulajdonság nevet tömb indexé alakítja? Szerintem senki. Mert aki tömb indexet akar, az for ciklust, vagy forEach-et használ. Vagy for-in-t, és hosszú időt tölt a hibakereséssel, amikor nagyon nem az történik, amit szeretett volna. Végignézi a kódját, elvileg mindent mindenhol jól írt… elvileg, mert becsapta magát az i (mint index) jelöléssel, mert az nem number, hanem string: k (mint key) kellett volna, ha már egy betűset akar. Nem a for-in hibája, hanem a programozóé. A for-in nem erre való.
2

hasOwnProperty

Poetro · 2010. Szep. 27. (H), 08.10
A hasOwnProperty használatával csak azok kerülnek felsorolásra amik a saját tulajdonságok, nem pedig a szülő tulajdonságai. Például:
for (var property in object) {
  if (object.hasOwnProperty(property)) {
    doStuff(object[property]);
  }
}
Az így felsorolt tulajdonságokat egyébként is érdemes szűrni. Underscore.js-ben találunk pár szép függvényt tömbök szűrésére, illetve objektumok bejárására.
var _ = require('underscore')._,
    needed = ['x', 'y', 'z'];
for (var property in object) {
  // Csak az x, y, z property kell
  if (object.hasOwnProperty(property) && _.indexOf(needed, property) !== -1) {
    doStuff(object[property]);
  }
}
És persze használhatjuk pont az ellenkezőjét, azaz kivesszük azokat, amik nem kellenek _.indexOf(needed, property) === -1.
12

Cross-browser

inf · 2010. Szep. 28. (K), 00.38
Cross-browser megoldás:

Object.prototype.hasOwnProperty=function (k)
{
	var p=this.constructor.prototype;
	return (k in this) && (!(k in p) || p[k]!==this[k]);
};

Object.prototype.x={};
Array.prototype.y={};

var a=[1,2];

for (var i in a)
{
	if (a.hasOwnProperty(i))
	{
		alert(i);
	}
}
Nem tudtam, hogy van beépített függvény erre, én már nagyon régóta használom for-in-hez.
20

parseInt esetleg?

aadaam · 2010. Szep. 29. (Sze), 13.30
hasOwnProperty && parseInt combo mar elvileg jo lenne szerintem.

BTW, en siman felulutom az Array.prototype.forEach-et ha nincs (MDC-n ott van az ES5-nek megfelelo feluluto kod), es vegigmegyek azzal.
26

Én nem szeretem a forEach-et,

inf · 2010. Okt. 4. (H), 01.27
Én nem szeretem a forEach-et, mert megkeveri a kontextust. Egyébként érdekes, hogy js mennyire a Visitor és nem az Iterator felé ment el, gondolom a lambda függvény támogatásnak köszönhetően.
Kisebb cilusokhoz még jó, de egymásba ágyazott hosszabb ciklusokhoz már gáz lehet. Én például már elég sokszor hagytam el a kontextust a végéről, hogy megunjam. Ami még zavaró, hogy a break és continue támogatása csak try-catch-el valósítható meg Visitorral.
4

ne is.. :)

ironwill · 2010. Szep. 27. (H), 10.17
Párszor már én is belefutottam.. :)
Ha asszociatív tömböt akarsz használni, akkor használj Hash táblát.
(gyakorlatilag JSON)

var tomb = {};
tomb["one"] = "one";
tomb.two = "two";

alert(tomb.one);    //one
alert(tomb["two"]); //two
5

biztos hogy jol ertetted,

Tyrael · 2010. Szep. 27. (H), 10.23
biztos hogy jol ertetted, hogy mi volt fmate problemaja?
sehol nem lattam a topicban, hogy asszociativ tombot szeretet volna letrehozni.
pont hogy normal integerrel indexeltet.
a gond az volt, hogy a for i in szerkezetben az indexek integer helyett string tipussal jottek, ami bekavart a kodban.

ha asszociativ tombot(objectet...) hasznalt volna, akkor nem jott volna elo ez a problema, de a postnak nem ez a lenyege.

Tyrael
7

persze.. :)

ironwill · 2010. Szep. 27. (H), 11.02
Csak mire a cikk végére értem, el is felejtettem a valódi kérdést.. :)
(Ami megzavart, hogy miért akar valaki for in-el végig iterálni egy normál tömbbön? Tényleg.. Miért is?)
9

Megszokta

Török Gábor · 2010. Szep. 27. (H), 12.59
Mert megszokta. Például Pythonban a for-in operátor való tömbök bejárására.
10

Azt hittem ennél

ironwill · 2010. Szep. 27. (H), 15.58
Azt hittem ennél szofisztikáltabb a válasz, de elfogadom.. :) Köszi a választ!
(*tömbön - nehezen indult a reggel.. :P
11

Ha asszociatív tömböt akarsz

tgr · 2010. Szep. 27. (H), 17.29
Ha asszociatív tömböt akarsz használni, akkor használj Hash táblát.


Pontosan az a probléma, hogy javascriptben a tömb is valójában hash tábla (néhány extra függvénnyel), míg más nyelvekben a rendes tömb és az asszociatív tömb külön típusok, és a for operátor mindegyikre a logikus módon viselkedik.
13

JS ismeret hiánya

anti81 · 2010. Szep. 28. (K), 11.16
Szerintem aki ismeri a JS működését, nem fut bele ebbe a hibába. Tömbökről tudjuk, hogy egyben objektumok. for-in pedig objektum bejáró, így változókon kívül metódusneveket is visszaad. Ez ilyen egyszerű.
14

Infinite

Török Gábor · 2010. Szep. 28. (K), 11.58
Nem tudom, én például Máté írásáig nem tudtam azt, hogy JavaScriptben az 1/0 kifejezés végtelent ad vissza.
15

Érthető

anti81 · 2010. Szep. 28. (K), 12.27
Hisz' a nullával való osztás hasznossága megkérdőjelezhető, és rendszerint zsigerből kerüljük, mert van olyan nyelv, ahol ez végzetes hiba. Nem is gondolom, hogy egy súlycsoportban lenne a for-in működési mechanizmusának ismeretével.
A soraid között is olvastam, így exkuzálnám magam, amennyiben előző hozzászólásomból bármiféle minősítés lett volna kihallható. Mások az ismereteink, így más dolgokat találunk evidensnek.
16

Egyetértek

presidento · 2010. Szep. 28. (K), 13.18
Egyetértek, aki pontosan ismeri az ECMAScript működését (és hogy az egyes JavaScript motorok miben térnek el a specifikációban foglaltaktól), az semmilyen hibába nem fut bele – hacsak nem volt figyelmetlen. :)

A for-in az esetek messze túlnyomó többségében tökéletes választás lehet a tömbök esetén is (hiszen azt csak meg tudjuk jegyezni, adtunk-e hozzá új metódust vagy tulajdonságot), a gond azzal van, hogy a maradék néhány esetben viszont nagyon könnyű figyelmetlennek lenni…
17

Én sokáig úgy voltam vele,

inf · 2010. Szep. 28. (K), 20.15
Én sokáig úgy voltam vele, hogy Máté értelmetlen blogbejegyzéseket tesz ki, de belátom, hogy tévedtem. :-) Lehet, hogy a for-in működése triviális, de mondjuk előkerült a hasOwnProperty, aminek a létezéséről nem is tudtam.
18

Csak óvatosan

rszaloki · 2010. Szep. 28. (K), 23.48
Csak óvatosan a tömbökkel!

Javascriptben a tömb olyan objektum, ami speciálisan kezel bizonyos property neveket:
- egy property név akkor tömb index, ha ToString(ToUint32(P)) === ToUint32(P)
- van neki length nevű property-je, aminek az értéke eggyel nagyobb, mint a legnagyobb tömb index property neve
(15.4, ECMA-262, 3rd edition, December 1999)


pl:
z=[];
z[3]=1;
alert(z.length) // 4
z[2]=2;
alert(z.length) // továbbra is 4
alert(z[1]) // undefined
alert(z.toString()) // ",,2,2"

Tehát igen, jól látja az író, a for ... in szerkezet nem való tömbök bejárására (sőt, ha 0-tól length-ig iterál a ciklus, akkor is kell ellenőrizni, hogy egy adott elem undefined-e)
19

Undefined

Poetro · 2010. Szep. 29. (Sze), 00.53
Azért meggondolandó, hogy undefined-ra kell-e ellenőrízni.
var f = function () {},
    a = [undefined, f(), 1, 2],
    i, al,
    u = f() + ""; // "undefined"

delete a[2];
a[5] = 4;
al = a.length;
console.log(a);

console.log('Check for `in`');
for (i = 0; i < al; i += 1) {
  if (i in a) {
     console.log(i, ' in a is ', a[i]);
  }
}

console.log('Check for `undefined`');
for (i = 0; i < al; i += 1) {
  if (typeof a[i] !== u) {
     console.log(i, ' in a is not undefined ', a[i]);
  }
}
[undefined, undefined, undefined, 2, undefined, 4]
Check for `in`
0 in a is undefined
1 in a is undefined
3 in a is 2
5 in a is 4
Check for `undefined`
3 in a is not undefined 2
5 in a is not undefined 4
A 2. elemet ugye töröltük tömbből. A 4. elem pedig nem jött létre. Ugyanakkor az undefined is hordoz értéket, a 0. és 1. elemünk fontos információkat hordozhat a program működésében, és léteznek a tömbben.
21

Re:Soha ne használd a for…in-t tömbök bejárására!

dkis · 2010. Okt. 2. (Szo), 21.36
Szia,

A rejtélyes alatt mit értesz?

Szerintem simán be lehetne járni tömböket a for(i in tomb)-el. Akkor szokott gond lenni, ha ez nem teljesen tömb, hanem vm keretrendszer speciális eleme (prototype, jquery stb), mert ilyenkor a keretrendszer az általa használt objektumokba beletesz mindenféle saját maga által használt segédváltozót, függvényt stb. és a for .. in ezt is felolvassa.


Dani
22

es most olvasd vegig a cikket

Tyrael · 2010. Okt. 2. (Szo), 23.43
es most olvasd vegig a cikket es a hozzaszolasokat.

Tyrael
23

Jíííháá, támadnak a write

inf · 2010. Okt. 3. (V), 03.34
Jíííháá, támadnak a write only huszárok. :D
24

itt egy kis script, ami

dkis · 2010. Okt. 3. (V), 21.06
itt egy kis script, ami tömböt olvas fel for..in-el és tökéletesen működik, van length property-je is, amit a for in olvasásnál nem olvas fel, tehát tökéletesen megy a tömbre a for..in

var tomb = [ 10,20,30 ];

r = "";
for(i in tomb) {
   r+= " tomb["+i+"] = "+tomb[i]+"\n";
}
alert(r);
alert(tomb.length);
Szóval a probléma onnan ered, hogy az eredeti kérdésben nem tömböt akar bejárni, hanem objektumot, ami látszatra úgy néz ki, mint egy tömb, de valójában objektumról van szó. Attól, hogy egy keretrendszer sorszámozza az elemeket, length, vagy size() elemet ad hozzá nem jelenti azt, hogy a javascript azt valódi tömbként kezeli.

A másik felvetésre, aki beletesz a tömbbe néhány elemet utána az x-ediknek ad értéket és a length megnő x+1-re a magyarázat egyszerű, ott tényleg tömbről van szó és mit tömb, ha adunk értéket az x-edik elemnek, akkor a rendszer az addig lévő összes elemet kénytelen valamivel feltölteni (undefined-el).
25

Failed.

inf · 2010. Okt. 4. (H), 00.59

Object.prototype.getClass=function ()
{
	return this.constructor;
};

for (var i in [])
{
	alert(i); // getClass
}
27

string, nem number

presidento · 2010. Okt. 5. (K), 07.10
Írd ki egy nem informatikus ismerősödnek a tömböd elemeit – aki fel nem tudja fogni, miért akarnál nullától indexelni:
var tomb = [ 10,20,30 ];  
  
r = "";  
for(i in tomb) {  
   r+= "A tomb "+(1+i)+". eleme: "+tomb[i]+"\n";  
}  
alert(r);  // 10. 11. és 12. elem
Lásd még: http://weblabor.hu/blog/20100927/javascript-tomb-bejaras#comment-71076