Előzmények: Objektumok létrehozása JavaScript-ben
Mai szösszenetemben megpróbáltam összeszedegetni, amit a JavaScript sort függvényéről megtudogattam. A JavaScript Array objektumának van egy sort() függvénye. Ez a tömböt alfabetikusan teszi sorrendbe. Ha egy elem objektum referencia, akkor az adott objektumra meghívja a toString() függvényt és annak az eredményét fogja hasonlítgatni.
Hasonlóan a perl nyelvhez, ennek a sort függvénynek is átadhatunk egy függvény referenciát, ami lehet akár anonymous (aka lambda) függvény is. Ez elég rugalmassá teheti a sort használatát.
Nézzünk egy egyszerű példát, amelyben számokat és szöveget teszünk sorrendbe (a console.log() függvényt csak firefox alatt használhatjuk. Ha máson nézzük, akkor használjuk a standard alert() függvényt!):
var n = [10, 1, 5, 4]; // numerikus
n.sort();
var s = ['c', 'A', 'C', 'a', '10', 5, 1]; // vegyes
s.sort();
console.log(n.join("; ") + "\n----\n" + s.join("; "));
Az eredmény némileg meglepő:
1; 10; 4; 5
----
1; 10; 5; A; C; a; c
Tudunk-e olyan sorrendezést csinálni, ami a számokat numerikusan hasonlítja össze, a szövegeket a kis és nagybetű figyelmen kívül hagyásával és a számok kerüljenek előre. Az egyebek meg kerüljenek a sor végére.
Hogyan tudjuk eldönteni, hogy egy tömb elem milyen típusú? Erre találták ki a typeof() függvényt (érdekes, hogy a typeof()-ról a w3school-on egy szó sem esik. De a delete föggvényről sem! Vajh miért?). De azért némi trükközés elkel. Mi az eredménye az alábbi szkript töredéknek?
var s = ['c', 'A', 'C', 'a', '10', new Date(), 5, 1, {a: 5, b:6}];
Ímhol az eredmény:
s.sort();
var t = "";
for (var i in s) { t += typeof(s[i]) + " "; }
console.log(s.join("; ") + "\n----\n" + t);
1; 10; 5; A; C; Wed May 09 2012 10:09:53 GMT+0100 (ope); [object Object]; a; c
----
number string number string string object object string string
Az első probléma, hogy a '10' szöveget nem számként kezeli, a második, hogy az object-re meghívta a toString() függvényt, ami string-et ad vissza és a szerint a nagy és kis betűk közé került az eredmény. Az elsőre problemára nem tudok triviális megoldást. Bár van egy isNaN() függvény, ami azt a célt szolgálja, hogy a bemenő paraméterről eldöntse, hogy szám-e (is-Not-A-Number). Helyesebben számmá alakítható-e. No és itt a trükk! Az anonymous objektumunkra azt mondja, hogy true, de a Date() objektum már számmá alakítható! És a true-is (amire typeof(true) == 'boolean'). Én azt a megoldást választottam, hogy ha az adott entitás typeof() == 'string', akkor megnézem, hogy isNaN() mit ad vissza. Ha true, akkor én megmondom, hogy ezt numerikusan hasonlítsa.
var s = ['c', 'A', 'C', 'a', '10', new Date(), 5, 11, {a: 5, b:6}];
Az eredmény nem tökéletes, de már jó úton járunk!
s.sort(function(a, b) {
var ta = typeof(a);
var tb = typeof(b);
if (ta == 'string' && !isNaN(a)) ta = 'number';
if (tb == 'string' && !isNaN(b)) tb = 'number';
if (ta == tb) {
if (ta == 'number') return a - b;
if (ta == 'string') {
a = a.toUpperCase();
b = b.toUpperCase();
}
return a < b ? -1 : a > b;
} else {
var typ = ['number', 'string', 'boolean', 'object', 'function',
'undefined'];
var ia = typ.indexOf(ta);
var ib = typ.indexOf(tb);
if (ia == -1) ia = typ.length;
if (ib == -1) ib = typ.length;
return ia - ib;
}
});
console.log(s.join("; "));
5; 10; 11; A; a; c; C; Wed May 09 2012 10:43:44 GMT+0100 (ope); [object Object]
Két gond van. Egyrészt a typeof() eredménye az alábbi lehet a doksi szerint: boolean, function, number, object, string, undefined. Ha a típusok különböznek, akkor ezt használom a sorrend eldöntésére. Másrészt, hogy string-ek esetében a kis és nagybetűs verziót véletlen szerűen rendezi be. Döntsük el, hogy melyiket szeretnénk előre és valósítsunk meg a ellenőrzést karakterről karakterre. Ezt mindenki házi feladatként oldja meg magának!
Egy picit trükk van még a fenti kódban. A string, vagy object esetén a return feltétele ez: "a < b ? -1 : a > b". Igazából ennek kellene lennie: "a < b ? -1 : a > b ? 1 : 0", de az "a > b" true/false-zal tér vissza, amit számmá konvertálva 1/0-t kapunk. Ezt a JS elvégzi helyettünk. Sajnos a perl-ös <=> operátort (és cmp()-t) nem implementálták, pedig milyen jól esne most!
Még egy szót a sort-ban levő anonymous függvény megér! A bemeneti változók lokálisak a függvényre nézve. De ha benne 'var' kulcsszó nélkül definiálunk változókat, akkor azok globálisak lesznek!
De nem is ezért kezdtem bele a mondókámba, hanem inkább vegyünk egy kicsit komplexebb példát. Tegyük fel, hogy van egy objektumokat tartalmazó tömbünk és egy adott mező alapján szeretnénk egy tömböt visszakapni, ami a tömb kulcsait tartalmazza az adott mező értékei szerint sorrendbe állítva. Ha az adott mező nincs definiálva, akkor nem kell felhasználni.
Példaként tekintsük az alábbi egyszerű tömböcskét, aminek a kulcsait szeretnénk a num mező alapján sorba rendezni:
var Arr = new Array;
Az első megközelítés ez lehet:
Arr['a'] = { num: 10 };
Arr['b'] = { };
Arr.c = { num: 5 };
Arr.d = { num: 2 };
Arr.e = { };
Arr.f = { num: 3 };
var s = new Array; // kimeneti tömb
for (var i in Arr) if (Arr[i].num != undefined) s.push(i);
s.sort(function(a,b){a=Arr[a].num;b=Arr[b].num;return a<b ? -1 : a>b});
console.log(s.join("; "));
Vessünk egy futó pillanatot a kimenetre:
d; f; c; a
És pont ezt szerettük volna!
Általánosítsunk! Egy objektum tetszőleges mezőjére szeretnénk ezt megoldani! Az egyszerűség kedvéért tételezzük fel, hogy numerikus adataink vannak. És rögvest adjuk hozzá hozzá az Array objektumokhoz, hogy más tömbökre is használhassuk! Nosza lássuk hát az eredményt:
Array.prototype.getKeysSortedBy = function(field) {
A sort az anonymous függvényében a this sajna nem használható, ezért kellett egy t hash-is is feltünteni. Mit kapunk az s kiíratáskor?
var s = new Array;
var t = {};
for (var i in this)
if (this[i][field] != undefined) {
s.push(i);
t[i] = this[i][field];
}
s.sort(function(a, b){a=t[a];b=t[b]; return a < b ? -1 : a > b;});
return s;
}
var s = Arr.getKeysSortedBy("num");
console.log(s.join("; "));
d; f; c; a
Azt, amit szeretnénk! Illetve sajnos ki kell ábrándítanom mindenkit, mégsem egészen azt! Nézzük meg, hogy milyen elemekből áll az Arr tömb:
for (var i in Arr) console.log("field: " + i);
eredménye:
field: a
field: b
field: c
field: d
field: e
field: f
field: getKeysSortedBy
MegaBrühühü! A kis genya hozzáadta a "class" függvényt is a mezőkhöz! Persze a for-on belül a typeof-fal manipulálhatnánk, és kiszűrhetnénk a class függvényeket és mezőket, de akkor már egyszerűbb, ha egy "normál" függvényt definiálunk erre a feladatra:function getKeysSortedBy(arr, field) {
var s = new Array;
var t = {};
for (var i in arr)
if (arr[i][field] != undefined)
s.push(i);
s.sort(function(a,b){a=arr[a][field];b=arr[b][field];return a<b?-1:a> b;});
return s;
}
var s = getKeysSortedBy(Arr, "num");
console.log(s.join("; "));
for (var i in Arr) console.log("field: " + i);
Szóval, ha bármikor is azt tervezzük, hogy for-in-nal lépkedünk végig egy tömb elemein, akkor ne definiáljunk Array.property-t, mert csúnyán ráfázhatunk!
No és ezzel majdnem készen is vagyunk. Még egy érdekesség. A "var"-ral definiált változók csak a befoglaló függvényen belül lesznek lokálisak. Egy for, vagy egy if utáni kód blokkból már "kilógnak", azaz a kód blokk után is definiáltak maradnak. Ez for esetén néha hasznos lehet, de (szerintem) nem szép megoldás és legalábbis zavaró lehet!
Írjunk kávét minden nap!
+jegyzések