Utvecklingstips

Closures i JavaScript - när binds egentligen variablerna?

Allt mer utveckling både på klientsidan (förstås) och på serversidan sker med JavaScript och alltmer sker asynkront. Tidigare kunde man anropa funktioner som uträttar uppgifter en efter en och helt enkelt gå vidare till nästa steg när man var klar med det innan. Lätt att förstå och lätt att debugga, men inte särskilt anpassat till dagens arkitekturer där man ofta anropar externa tjänster och API:er och där gränssnittet helt enkelt inte kan vänta på svar. Istället gör man en funktion, en callback, som anropas när den externa uppgiften är klar.

Det är ett smart pattern, men kan ge upphov till klurigheter att lösa för utvecklaren. En sådan är vilket scope som callback-funktionen egentligen körs i, d.v.s. vilka variablerna man har tillgång till när svarsanropet väl kommer. Ett annat är att man kan råka ut för race conditions där värden av vissa variabler plötsligt beror på svarstiden för de externa anropen.

Här är ett exempel:

cars =  [ { reg: ‘ABC123’ }, { reg: ‘DEF456’ }, { reg: ‘GHI789’ }, { reg: ’JKL123’ } ];

for (i = 0; i < cars.length ; i++)
{	
	var currentCar = cars[i];

	// Långsamt anrop till externt api. Anropet tar två parametrar, första är registreringsnumret den andra är

	// callbackfunktionen som anropas när api:et svarar

	externalService.slowlyLookupCarBrand(
		car.reg,
                function (brandName) {
			cars[i].brand = brandName;
                }
	);
}

Vad händer när denna körs? Ett fel uppstår där det står att ”cars[i].brand is undefined”. Konstigt... Första tanken är att det är fel scope, att variabeln i inte finns i callbackfunktionen. Om man skriver ut värdet på i upptäcker man att så inte är fallet – istället är har i alltid värdet 4. Vad har hänt här egentligen?

Svaret är att när första callback-anropet görs (och de tre följande förstås) så har redan for-loopen körts igenom och värdet på i har redan räknats upp till sitt maxvärde 4. Så anropet i callbacken kommer alltid att bli cars[4].brand. Det vi skulle vilja är att värdet på i binds när callback-funktionen skapas, inte när den anropas.

JavaScript är ett magiskt språk med många funktioner som länge har funnits på teoretisk nivå inom datalogin men som sällan letar sig fram till praktisk användning. En sådan är högre ordningens funktioner – vilket betyder att funktioner fungerar som vilka andra typer som helst. Det ska vi använda för att lösa vårt problem. Vi vill skapa en s.k. closure, helt enkelt en bindning av variabelns värde vid deklarationen istället för vid körningen. Såhär går det till (detta är inte intuitivt och tar de flesta ganska mycket tankemöda innan man förstår vad som händer):

cars =  [ { reg: ‘ABC123’ }, { reg: ‘DEF456’ }, { reg: ‘GHI789’ }, { reg: ’JKL123’ } ];

for (i = 0; i < cars.length ; i++)
{	
	var currentCar = cars[i];
	
	// Långsamt anrop till externt api. Anropet tar två parametrar, första är registreringsnumret den andra är

	// callbackfunktionen som anropas när api:et svarar
	
	externalService.slowlyLookupCarBrand(
                car.reg,
                function (index) {
			return function (brandName) {	
				cars[index].brand = brandName;	
			}	
		} (i)	
	);
}

OK, vad betyder det här nu då? Istället för att direkt skapa en anonym funktion som callback så låter vi en yttre funktion skapa och returnera callback-funktionen. Det sista (i) efter } betyder ”anropa den yttre funktionen med parametervärdet i”. Det som händer då är att en closure skapas, en binding av variablers värden som de ser ut just nu. Parametern index i funktionen tilldelas värdet som vi har vid tillfället när anropet till slowlyLookupCarBrand görs, inte när svarsanropet till den inre funktionen kommer från den externa tjänsten. Det betyder att våra fyra externa anrop kommer att få fyra olika svarsfunktioner, var och en med ett värde på index som motsvarar värdet på i när det externa anropet sker.

Som sagt  - inte intuitivt och inte superenkelt att förstå vid första genomläsningen, men helt nödvändigt att använda i vissa tillfällen.

Andra bloggar om: 
 
Anders Bornholm

2012-05-03 kl. 15:15

Web Analytics