ugrás a tartalomhoz

Javascript – a tömb rejtett értékei

inf · 2009. Jún. 9. (K), 07.02
JavaScriptben arra a problémára kerestem a megoldást, hogy egy objektumokat tartalmazó tömbben hogyan lehet egy objektum/függvény értékű tulajdonság alapján a leghatékonyabban keresni. Ez elég specifikus téma, mert legtöbbször már létrehozott DOM objektumokkal dolgozunk JavaScriptben, szóval az objektumok felvétele egy adott tömbbe és kikeresésük már adott (lásd getElementsByTagName()).

Az újrahasznosíthatóság jegyében az ember úgy áll neki megírni egy ilyen objektum kulcsú adattárolót, hogy az általánosan használható legyen, tehát a tulajdonság, ami alapján hozzáad és keres, példányonként változtatható legyen.

Normál esetben a (hibás) megoldás így néz ki:

var reference = function (key)
{
    var store = [];

    var indexOf = function (identifier_object)
    {
        for (var j = 0; j < store.length; j++)
        {
            var data_object = store[j];

            if (data_object[key] == identifier_object) {
                return j;
            }
        }

        return -1;
    };

    this.set = function (data_object)
    {
        var index = indexOf(data_object[key]);

        store[index == -1 ? store.length : index] = data_object;

        return this;
    };

    this.get = function (identifier_object)
    {
        var index = indexOf(identifier_object);

        if (index != -1) {
            return store[index];
        }
     };
};
A set() függvénnyel adunk hozzá egy elemet a listához vagy módosítunk egy már létező elemet. Ehhez nyilván be kell járni a tömböt, le kell kérni a tárolt objektumoktól a key tulajdonságot, ami az azonosító objektumokat tartalmazza, és az azonosító objektumokat össze kell hasonlítani a beállítandó objektum azonosító objektumával.

Nyilván ez a logikus megoldás, viszont nem ez a legjobb, mert a ciklusban a szöveges kulcs alapján történő kikeresés jelentősen lelassítja a függvény futását.

Az egészen úgy lehet gyorsítani, hogy kiemeljük az azonosító objektumot, és egy tömbben tároljuk le az azonosítót és az adatot:

var reference = function (key)
{
    var store = [];

    var indexOf = function (identifier_object)
    {
        for (var j = 0; j < store.length; j++) {
            var current_identifier_object = store[j][0];

            if (current_identifier_object === identifier_object) {
                return j;
            }
        }

        return -1;
    };

    this.set = function (data_object)
    {
        var index = indexOf(data_object[key]);
        store[index == -1 ? store.length : index] = [data_object[key], data_object];
        return this;
    };

    this.get = function (identifier_object)
    {
        var index = indexOf(identifier_object);

        if (index != -1) {
            return store[index][1];
        }
    };
};
Így a ciklusban a szöveges adatelérést a jóval gyorsabb szám alapú adateléréssel helyettesítjük. Az ember nem is gondolná, hogy mennyit számít mindez.

Amivel teszteltem:

<script type="text/javascript" src="store.js"></script>
<script type="text/javascript">
    var d1       = new Date();
    var instance = new reference('identifier_object');

    for (var i = 0; i < 1000; i++) {
        var data_object = {identifier_object: {}};
        instance.set(data_object);
    }

    var d2 = new Date();
    alert((d2 - d1) + ' milliseconds');
</script>
Az első esetben 350 ms körüli értékeket kaptam, a második esetben pedig 280 ms körülieket. A különbség a nagyobb érték 20%-a, ami már igen jelentős.

Ennek a fajta megközelítésnek a hatékonysága még jobban tükröződik az olyan helyzetekben, amikor

[
    {
        kulcs: kulcs_objektum,
        adat:  adat_objektum,
    },

    ⋮

]
típusú tárolást hajtunk végre.

Nagyon sok helyen láttam már azt a mintát (prototype.js-ben azt hiszem szintén előfordult), hogy ilyen esetben a kulcsnak és az adatnak is szép, beszédes nevet adnak. Ez a lehető leghibásabb megközelítés, ami a keresési sebességét körülbelül a felére csökkenti.

Ilyen esetben érdemesebb egy előre megírt objektum kulcsú osztályt használni a tárolásra.

[
    [kulcs_objektum, adat_objektum],

    ⋮

]
 
1

Extremitások

vbence · 2009. Jún. 9. (K), 08.06
Kicsit nagy ugrás van az első „rossz” és a második „jó” példa között. Amég az első esetben gyakorlatilag bármilyen objektumokat használhatunk a tömb elemeként (akár magukat a DOM node-okat), a másodikban kizárólag Array szerepelhet méghozzá meghatározva, hogy a nulladik index a kulcsé.

Érdekes lenne megnézni a bejegyzés végén felvetett saját konténerobjektumos megoldás sebességét.

Valamint talán életszerűbb lenne egy set(kev, value) típusú megoldás ahol elzárjuk a usertől a konténerobjektumokat – ilyenkor ugyebár teljes uralmat élvezel a konténerobjektum fölött olyan rendszerközeli lehet ahogy csak szeretnéd.
4

:-)

inf · 2009. Jún. 9. (K), 13.02
Szia!

Nem nagyon értem mire gondolsz :-)

A set(value,key) sorrendet azért javaslom, mert sokkal általánosabb, mint a set(key,value). Tömböknél nyilván egyszerűbb, ha lehagyod a kulcsot a hozzáfűzéshez. Persze a key, value sorrend is előfordulhat, csak úgy nehezebb jelölni ha a tömbhöz akarsz hozzáfűzni. Mondjuk set(-1,value) lehetne egy ilyen jelölés.
A dologgal kapcsolatban az each is szerepet játszik, mondjuk egy sima tömbre (break, continue kihagyásával) az each:

{
...
	each: function (method,context)
	{
		if (!context)
		{
			context=this;
		}
		for (var j=0; j<this.length; j++)
		{
			method.call(context,this[j], j);
		}
		return context;
	}
...
}
Az eachnél az adott értéket használjuk csak az esetek nagy részében, ezért jobb előrevenni, viszont ha az eachben előreveszem, akkor a setben is nyilván, hogy a sorrend azonos legyen.

Alapvetően JS-ben 3 alapszintű gyűjteménnyel szoktam dolgozni a 3 kulcstípus miatt:
  1. integer – Array
  2. string – Hash
  3. object – Reference


Kicsit nagy ugrás van az első „rossz” és a második „jó” példa között. Amég az első esetben gyakorlatilag bármilyen objektumokat használhatunk a tömb elemeként (akár magukat a DOM node-okat), a másodikban kizárólag Array szerepelhet méghozzá meghatározva, hogy a nulladik index a kulcsé.

Ez így nem igaz szerintem. Itt az a lényeg, hogy objektum kulccsal tároljak le adatot. A DOM object az adat, a tárolási forma meg közömbös, mert úgysem látod.
5

logika, tárolás

vbence · 2009. Jún. 9. (K), 13.15
Amennyire én edig tapasztaltam a key, value sorrend az általános, ez követi a legöbb programnyelv értékadási logikáját:

var a = "alma";
container.set("a", "alma");
Én úgy értettem, hogy egy konténert építesz, amiben kulcs-érték párokat tárolsz, ahol a kulcs lehet objektum is. Két megközelítést tuok elképzelni:

A) A felhasználó számára direkt elérhetővé teszed a valódi (natív) tömböt, ami a konténer alapját képezi. Ilyenkor a lehető legáltalánosabb konvenciókat használnám (például kulcsként tetszóleges tulajdonság szerepelhet, ahogy az első példában). Ekkor a hibamentességet a maximális flexibilitás garantálja.

B) A tárolásra használt töböt az objektumom belsejében elrejtem, és kizárólag a saját metódusaimon kereszül teszem elérhetővé. Ez garantálja a hibementességet, és ilyenkor a belső feléptés lehet olyan rendszerközeli, amit holnap már én magam sem értek, ha elég gyors. - Természetesen ilyenkor a módostás újraírást jelent.

Gyorsabb lesz a kódod, ha a DOM node azt a tulajdonságát kiszeded, ami szerint rákeresnél, és bepakolod egy két elemű tömbbe.


No, itt a mondat első fele programtervezési feladat, a második pedig az eszköz késztőjéé. Logikusan hangzik hogy gyorsabb stringekkel indexelni, mint objektumokkal. Ha emellett döntöttél, akkor választhatsz egy oylan konténert ami garantáltan stringgel indexel (például egy objektum tulajdonságaként tárolni), vagy egy általános konténert aminek indexei akár objektumok is lehetnek.

Ez két különböző eszköz. A string-indexű konténer nem jobb semnem rosszabb az objektum indexűnél. Az előbbi előnye a sebesség a másodiké a flexibilitás. Egy önkényesen kiválasztott felhasználás tükrében nincs értelme magukat a tárolási módszereket összehasonlítani.
6

Yepp

inf · 2009. Jún. 9. (K), 13.45
én edig tapasztaltam a key, value sorrend az általános

Lehetséges, akkor ez az én egyedi megközelítésem, ami valószínűleg azért alakult ki, mert az append-et és a replace-et összevontam egy függvénybe. Hash-nél ezt nyilván gond nélkül megtehetem, tömbnél a kulcsot egyszerűen le tudom ellenőrizni, szóval ott sincs gond vele. Úgy látszik a probléma az object kulcsú tárolásnál jött elő.

Ami konkrétan ehhez vezetett az az, hogy próbáltam a 3 típus (Objektum kulcsú,Hash,Array) felületét egységesíteni, és ezért összevontam a felülírást és hozzáfűzést. A második kettőnél ez a módszer bevált, viszont az Objektum típusúnál már erősen lassít. Jobbat viszont egyelőre nem találtam ki.

Egy önkényesen kiválasztott felhasználás tükrében nincs értelme magukat a tárolási módszereket összehasonlítani.

Nem emlékszem, hogy az object kulcsú tárolást bárhol összehasonlítottam volna a Hash-el. Mindkét megoldás a bejegyzésben arra irányult, hogy az objektum kulcsú tárolást hogyan lehet a leggyorsabban megoldani, és engem őszintén szólva meglepett, hogy ebben a speciális esetben gyorsabb kiszedni a kulcsot tömbbe, mint bennhagyni tulajdonságként, mert magának a kulcsnak a kiolvasása az objektumból nagyon lassú.
7

Harmadik út :)

vbence · 2009. Jún. 9. (K), 13.46
Ezért írtam az első bejegyzésben, hogy érdemes lenne ugyanezzel a teszttel kipróbálni egy olyan megközelítést, ahol saját objektumokban tárolod a kulcs-érték párokat (tehát ami most sok kételemű tömb). Megéri-e numerikus indexekkel foglalkozni ha mondjuk szintén fix "key" tulajdonság lookupja ugyanolyan gyors.

A másik, hogy én elrejteném a user elől ezeket a kulcs-érték párokat megtestesítő objektumokat, bármi is legyen a tárolás formája.
8

Igen

inf · 2009. Jún. 9. (K), 15.56
Megéri-e numerikus indexekkel foglalkozni ha mondjuk szintén fix "key" tulajdonság lookupja ugyanolyan gyors.
Pont ez a lényeg, hogy messze nem olyan gyors, mint a tömbös megoldás. Úgy látszik rosszul fogalmaztam meg a blog bejegyzést, vagy már nagyon le voltam fáradva :-)

A másik, hogy én elrejteném a user elől ezeket a kulcs-érték párokat megtestesítő objektumokat, bármi is legyen a tárolás formája.
Jelenlegi formában el vannak rejtve, de általában nem ezt a megoldást szoktam használni, mert ha a prototype-ba rakom a függvényeket, akkor sokkal gyorsabb egy-egy objektum felépítése. Szóval javascriptben sajnos csak publikusra lehet megírni. Majd szerintem még erről is lesz bejegyzésem, hogy miben más a javascript OOPje a többi oop-hez képest.


Egyébként engem érdekelne, hogy te hogyan oldanád meg a set,get,each metódusokat mindhárom fajta tároláshoz?
A probléma, amit nem tudtam áthidalni, és ami miatt összevontam a set-be az összes értékadást:

collection.prototype.assimilate=function(arg)
{
	var self=this;
	arg.each(function (value,key)
	{
		self.push(value);
	});
};
A lényeg, hogy egy egységes felületet akarok adni mindhárom típusnak, és csinálni hozzájuk közös függvényeket. Nyilván a közös függvényeket az egységes felületre alapoznám.
A fenti példában egy adott értéket kell hozzáadni valamilyen gyűjteményből egy tetszőleges típusú másik gyűjteménybe. A gond vele az, hogy Hash és Reference(nevezzük ennek az objektum kulcsút, ha nem tudsz jobbat) esetében kell a kulcs a hozzáfűzéshez, tömb esetében viszont szükségtelen, sőt kifejezetten káros.
Ezért csináltam a set(value,key) függvényt, hogy áthidaljam a problémát.
2

Még gyorsítható…

presidento · 2009. Jún. 9. (K), 08.46
…a memóriahasználat ellenében, ha hash-jelleggel letároljuk a kulcsokat.
Teszteredmények (tiéd vs. enyém; FF 3.0, WinVista):
771 msec vs. 36 msec (!), ha minden kulcs különböző
654 msec vs. 330 msec, ha minden kulcs egyezik (mint a Te példádban is).
var reference2=function (key)  
{
	var store=[];  
	var indexes = Object();
	
	var indexOf=function (identifier_object)  
	{
		var arr = indexes[ identifier_object ];
		if ( !arr ) return -1;
		for (var j=0; j<arr.length; j++)  
		{  
			var current_identifier_object=store[arr[j]][0];  
			if (current_identifier_object===identifier_object)  
			{  
				return arr[j];  
			}  
		}  
		return -1;  
	};  

	this.set=function (data_object)  
	{  
		var k = data_object[key];
		var index=indexOf(key);
		
		if ( index == -1 ) {
			index = store.length;
			if (!indexes[k]) indexes[k] = new Array();
			indexes[k].push(index);
		}
		store[index]=[k,data_object];  
		return this;
	};

	this.get=function (identifier_object)  
	{
		var index=indexOf(identifier_object);  
		if (index!= -1)  
		{  
			return store[index][1];  
		}
	};
};

function test1( instance ) {
	for (var i=0; i<1000; i++)  
	{
		var key = new Date().toString() + i;
		var data_object={identifier_object: key};
		instance.set(data_object).get(key);
	}
}
function test2( instance ) {
	for (var i=0; i<1000; i++)  
	{
		var key = {};
		var data_object={identifier_object: key};
		instance.set(data_object).get(key);
	}
}

var d1 = new Date();
test1(new reference("identifier_object"));
var d2=new Date();
test1(new reference2("identifier_object"));
var d3=new Date();
test2(new reference("identifier_object"));
var d4=new Date();
test2(new reference2("identifier_object"));
var d5=new Date();
alert("Eredeti megoldas \t Hash-el\n" +
		(d2-d1)+"msecs \t\t"+(d3-d2)+"msecs \t(kulonbozo kulcsok)\n"+
		(d4-d3)+"msecs \t\t"+(d5-d4)+"msecs \t(egyezo kulcsok)");
3

Hibás

inf · 2009. Jún. 9. (K), 12.54
Szia!

Alapvetően a legnagyobb hiba a megoldásoddal, hogy a toString-re nem lehet alapozni az objektumok összehasonlítását, mert a toString értéke attól függetlenül megváltozhat, hogy a gyűjteményhez hozzányúlnánk.
Mondjuk az egyik dátumnál megváltoztatom az évszámot stb....

function test3(instance)
{
	var key=new Date();
	var data_object={identifier_object: key}; 
	instance.set(data_object);
	key.setFullYear(2010,0,14);
	alert(instance.get(key))
}
9

vélemények

Pusztai Tibor · 2009. Jún. 9. (K), 18.28
Szia!

Én sem feltétlen értek azzal egyet, hogy az első megoldást kinevezted hibásnak, bár tény, hogy a második gyorsabb. A teszt viszont elég veszélyes, nem méred, hogy a kiolvasás mivel jár. Ki kéne találni, hogy az írások-olvasások aránya milyen lesz használat közben és ez alapján mérni a futásidőt. Ha nincs erről információd akkor mondjuk feltételezheted, hogy ugyanannyi az írások mint az olvasások száma.
Hogy a Te vonaladat kövessem itt egy 3., még gyorsabb megoldás:

var reference=function (key)  
{  
    var keys=[];		
    var values=[];
    var indexOf=function (identifier_object)  
    {  
        for (var j=0; j<keys.length; j++)  
        {  
            if (keys[j]===identifier_object)  
            {  
                return j;  
            }  
        }  
        return -1;  
    };  
      
    this.set=function (data_object)  
    {  
        var index=indexOf(data_object[key]);  
        index = index==-1?keys.length:index;
        keys[index]=data_object[key];
        values[index]=data_object;
        return this;  
    };  
      
    this.get=function (identifier_object)  
    {  
        var index=indexOf(identifier_object);  
        if (index!=-1)  
        {  
            return values[index];  
        }  
    };  
      
};
A 2. megoldásodban az összes mapben lévő kulcshoz létrehozol egy új objektumot, ráadásul kulcsütközésnél fel sem használod, hanem újat készítesz. Ez nem tesz jót a szemétgyüjtőnek. Az én megoldásomnál láthatod, hogy többször nem jön létre új objektum, csak példányosításkor a reference objektum és két tömb.
Viszont az alapvető probléma ezekkel a megoldásokkal, hogy a műveletidő erősen függ a mapben lévő elemek számától. Az első ezer elemet jóval gyorsabb belerakni, mint a második ezret. Viszont ezen sajnos a kulcsobjektumok érintése nélkül nem lehet gyorsítani (ill. javítani a műveletigény aszimptotikus nagyságrendjén). Viszont meg lehet úgy oldani, hogy a végrehajtott módosítás általános legyen, ne pedig map-specifikus. Azaz, ha feltételezzük, hogy használható a következő függvény:

var hash = (function() {
	var last = 0;
	return function(object) {
		return object.__hash || (object.__hash = ++last);
	};
})();
akkor az alábbi megoldás messzemenően gyorsabb (és rövidebb) akármelyik előbbinél:

var reference=function (key)  
{  
    var values={};
      
    this.set=function (data_object)  
    {  
        values[hash(data_object[key])]=data_object;
        return this;  
    };  
      
    this.get=function (identifier_object)  
    {  
        return values[hash(identifier_object)];
    };  
      
}; 
Egyébként szerintem elég furán nevezed el a változókat, találod ki a szignatúrát. Bár nem ekvivalens, de szerintem sokkal tisztább lenne valahogy így:

var ReferenceMap = function() {
		
    var ...
		
    this.set = function(key, value) {
        ...
    };
		
    this.get = function(key) {
        ...
    };
		
};
Aztán nyugodtan lehet készíteni egy StringMap vagy általános ObjectMap-et is ugyanilyan interfésszel. StringMap: a kulcsok stringek lehetnek (pont, mint egy Object), ObjectMap: a kulcsok objektumok lehetnek. Az ObjectMap megfelelő vieselkedéséhez viszont a kulcsoknak implementálnia kell egy hashString (vagy ahogy nevezed) függvényt.
10

Szerintem

inf · 2009. Jún. 10. (Sze), 13.40
Szia!

Az elsővel egyetértek, hogy a 2 tömb gyorsabb, mint az egy. A beállítás valóban gyorsabb vele, és takarékosabb, viszont a törlési sebességet felére lassítja, mert 2 tömbön kell áthelyezni az elemeket, nem csak az egyiken. Szóval ez is olyan, hogy valamit valamiért, viszont set-et biztos, hogy többet használja az ember, mint a remove-t.

Hash-el sajnos nem lehet összekötni, csak speciális helyzetben. Ha két külön gyűjteménybe rakod be ugyanazokat az objecteket, akkor felülírják egymás hash tulajdonságait. Persze ezt is tovább lehet bonyolítani, ha a __hash egy tömb, és minden reference kap egy azonosító számot. A törlés ebben az esetben még problémásabb lesz, szóval javaslom, hogy mérjük a törlési sebességet is, ha ilyen irányban megyünk el.

Az ObjectMap valóban jobb név. HashString függvény írása javascript objectekre szerintem lehetetlen. Már ha egy objecthez egyedi azonosítót generáló függvényre gondolsz. Ha létezne ilyen, akkor nem kéne a tömbökkel foglalkozni.
11

félreértesz

Pusztai Tibor · 2009. Jún. 10. (Sze), 15.54
(Bocs, véletlen nem a válaszra kattintottam és utólag már nem tudom mozgatni a hozzászólást.)

Nézd csak meg jobban a hash függvényt, teljesen független attól, hogy milyen mapról van szó. Sőt, más is használhatja, nem csak egy map. Próbáltam hangsúlyozni ezt az előző hozzászólásomban, azért írtam külön blokkban a hash függvényt. Vagy ha nem esik le, próbálj ki valamit különböző mapekbe tenni és nézegesd firebug-ban a mapek reprezentációt. A törlés is nagyon egyszerű, csak egy hash hívás és egy delete. Megmutatom, ha nem egyértelmű.

Visszatérve a másik, tömbös megoldásnál a törlésre: el kell dönteni, hogy hogyan szeretnél törölni. A teljes tömb átrendezése műveletigényes, lehet, hogy gyorsabb egy null-t vagy undefined-edet (nem tudom olcsóbb-e valamelyik és ha igen melyik) rakni a törlendő helyére. De ez megintcsak attól függ, hogyan fogod használni a mapet.

Az ObjectMap elnevezés félreérthető, ez egy ReferenceMap. Nem mindegy! Itt egy példa, amin láthatod a különbséget:

var hash = (function() {  
    var last = 0;  
    return function(object) {  
        return object.__hash || (object.__hash = ++last);  
    };  
})();  

var ReferenceMap = function() {  
          
    var values = {}; 
          
    this.set = function(key, value) {  
        values[hash(key)] = value;
    };  
          
    this.get = function(key) {  
        return values[hash(key)];
    };  
          
};

var ObjectMap = function() {  
          
    var values = {}; 
          
    this.set = function(key, value) {  
        values[key.hashString()] = value;
    };  
          
    this.get = function(key) {  
        return values[key.hashString()];
    };  
          
};

var ComplexNumber = function(real, imaginery) {

    this.real      = real      || 0;
    this.imaginery = imaginery || 0;
    
    this.toString = function() {
        return this.real + " + " + this.imaginery + "i";
    };
    
    this.hashString = function() {
        return "ComplexNumber:"+this;
    };
		
    this.plus = function(b) {
        return new ComplexNumber(
            this.real      + b.real,
            this.imaginery + b.imaginery
        );
    };

};

var a = new ComplexNumber(3, 0);
var b = new ComplexNumber(0, 3);

var testMap = function(map, nameToShow) {
    map.set(a.plus(b), "value for a+b");
    console.log("value for key b+a in %s: %o", nameToShow,
        map.get(b.plus(a))
    );
};

testMap(new ReferenceMap(), "referenceMap");
testMap(new ObjectMap(),    "objectMap");
Remélem látod, hogy ha mindkét esetben a.plus(b)-t írok, akkor is előjön a különbség. Azért írtam felcserélve, hogy lásd, egyáltalán nem mondvacsinált ez a dolog, sokszor szükség van erre.
Egyébként az én ReferenceMap-em nem viselkedik pont ugyanúgy, mint a Te reference-ed. Mégpedig olyankor amikor primitív típust (string literál, szám literál, logikai érték) használsz kulcsnak, amiknél ugye nem létezik referencia (azoknál nem is referenciát hasonlít össze az === operátor). Tehát azok kapcsán nem sok értelme van ReferenceMap-ről beszélni.

Miért ne lehetne hashString-et írni javascriptben? Általánosat csak nehezen, kompromisszumokkal, de általában az osztálytól függ, hogy mitől számít két objektum egyenlőnek. (A fenti példánál ugyebár az összeadás kommutatív, de az osztás már nem lenne az.) Ha pedig olyanra van szükséged, ami referenciánként ad egyedi értéket és független a műveletigénye ez eddig kiszámolt értékek darabszámától, akkor azt csak az objektum módosításával lehet elérni (mert nincs hozzáférésed a referencia értékéhez). Erre viszont már mutattam is Neked egy függvényt, és azt hiszem ilyenre gondoltál ez alatt: "egy objecthez egyedi azonosítót generáló függvény".
12

Benéztem

inf · 2009. Jún. 10. (Sze), 16.33
Ja sorry, benéztem, nem íródik felül a __hash, és így minden objecthez van egyedi kulcs.
Azt hiszem még ennyivel kiegészíteném:

var Exception=Error;
var InvalidArgumentException=Exception;

var hash = (function()
{    
	var last = 0;    
	return function(object)
	{
		if (object===null || typeof(object)!="object" && typeof(object)!="function")
		{
			throw new InvalidArgumentException;
		}
		return object.__hash || (object.__hash = ++last);    
	};    
})(); 
Ez tetszik, azt hiszem ebből a blog bejegyzésből én tanultam a legtöbbet :-D
13

Nem lehetsz benne biztos...

zzrek · 2009. Jún. 11. (Cs), 14.07
Nem lehetsz benne biztos, hogy te tanultál belőle a legtöbbet :-)
Jó volt az írásod, több szempontból is, még több ilyet, köszi!
14

Köszi

inf · 2009. Jún. 11. (Cs), 18.11
Én köszönöm a bíztatást! Valahol el kell kezdeni úgy vagyok vele. Majd még írok javascripttel kapcsolatban komolyabbat is, ha lement ez a hónap. (Vizsgaidőszak sajnos odatesz a szabadidőnek.)