Closures en JavaScript

applications internet utilitaires

Ce site ne sera plus alimenté de contenu après août 2014. Tous les nouveaux articles seront redigés pour www.waitingforcode.com
Dans la plupart des cas, la connaissance basique de JavaScript nous suffira à développer des applications plus user-friendly (surtout avec des frameworks du type jQuery). Malgré cela, JavaScript reste un langage de programmation. Un langage beaucoup plus complexe qu'on y croit.

Cet article va présenter la notion de closure en JavaScript. Au début on verra ce qui se cache derrière ce mot. Dans la deuxième partie on passera à un exemple d'utilisation. Pour illustrer la mise en place de closure, on utilisera la géolocalisation avec la version 3 de GoogleMaps.

Closures en JavaScript
Il arrive souvent qu'on parcourt une dizaine d'éléments dans la boucle for et qu'on veut utiliser la valeur incrémentée/décrémentée plus loin dans le code (par exemple suite au chargement d'un fichier). Pour éviter le problème où la valeur récupérée correspond toujours à la valeur maximale, on utilisera un closure. Voici l'exécution d'une méthode se basant sur la variable incrémentée i:


$(document).ready(function() {
for(var i = 0; i < 4; i++)
{
window.setTimeout(function() { alert(i) }, 1000);
}
});

Le test affiche tout le temps le chiffre 4. On a donc l'impression que la boucle et la méthode alert(i) s'exécutent dans deux fils distincts. Le problème peut être évité grâce au closure qui "protégera" la variable i. Voici le code corrigé :


$(document).ready(function() {
for(var i = 0; i < 4; i++)
{
(function(i) {
window.setTimeout(function() { alert(i) }, 1000);
})(i);
}
});

Maintenant on voit bien les fenêtres d'alerte avec i incrémenté.

Pour comprendre ce qui s'est passé, on va remonter dans le temps et se rappeler quelle est la portée des variables dans JavaScript.

<body>
<script type="text/javascript" src="http://code.jquery.com/jquery-latest.js"></script>
<script type="text/javascript">
var a = "a";
$(document).ready(function() {
outerMethod();
$("#global").append("
a =========> " + typeof a);
$("#global").append("
b =========> " + typeof b);
$("#global").append("
c =========> " + typeof c);
});

function outerMethod()
{
var b = "b";
$("#outer").append("
a =========> " + typeof a);
$("#outer").append("
b =========> " + typeof b);
$("#outer").append("
c =========> " + typeof c);
function innerOfOuter()
{
var c = "b";
$("#inner").append("
a =========> " + typeof a);
$("#inner").append("
b =========> " + typeof b);
$("#inner").append("
c =========> " + typeof c);
}
innerOfOuter();
}
</script>
<p id="global"><strong>GLOBAL</p>
<p id="outer"><strong>outerMethod</p>
<p id="inner"><strong>innerOfOuter</p>
<!-- the results are :
GLOBAL
a =========> string
b =========> undefined
c =========> undefined

outerMethod
a =========> string
b =========> string
c =========> undefined

innerOfOuter
a =========> string
b =========> string
c =========> string
-->
</body>

On voit clairement que la portée des variables en JavaScript ressemble un peu à une arbre. Plus on descend, plus de variables on peut manipuler. Si l'on rajoute une fonction anonyme (Self-Invoking Anonymous Function - SIAF) à cette logique, on obtiendra un closure.

Une fonction anonyme, qu'est-ce que c'est ? La fonction anonyme est une fonction sans le nom défini, qui peut être appelée seulement une fois, au moment de sa déclaration. Les paramètres de cette fonction sont privés et donc indépendants d'autres fils d'exécution.

Voici donc comment se présente un squelette de closure :

(function(param1, param2) {
// do something with two params
})("first param value", "second param value");

A part de mettre cette fonction anonyme dans une boucle, il nous est incapable de l'appeler plusieurs fois avec des paramètres différents.

Closures dans GoogleMaps
Où les closures peuvent trouver leur utilité ? On peut l'employer par exemple dans l'usage des API, comme GoogleMaps. C'est d'ailleurs ce web service qu'on utilisera pour illustrer l'utilisation des closures dans le monde réel.

Imaginons qu'on charge 10 adresses différentes qu'on veut marquer sur une carte avec des markers. Pour le faire, on doit récupérer d'abord les coordonnées géographiques de ces adresses. Et c'est un processus qui nécessite un calcul de la part du service Geocoder. Cette opération prend du temps et ressemble un peu à l'exemple évoqué au tout début, avec l'appel du setTimeout() et l'affichage de "alert(i);". Voici comment se présente le code de création des markers :


<body>
<script type="text/javascript" src="http://code.jquery.com/jquery-latest.js"></script>
<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false"></script>
<script type="text/javascript">
var geocoder;
var map;
var marker;
var infowindow = new google.maps.InfoWindow();
var address = new Array();
var addressInfo = new Array();
$(document).ready(function() {
$.ajax({
type: "GET",
url: "/closures/address.php",
dataType: "json",
success: function(ret)
{
for(var i=0; i < ret.address.length; i++)
{
address[i] = ret.address[i];
addressInfo[i] = ret.info[i];
}
},
error: function(jqXHR, textStatus, errorThrown)
{
alert("An error occured");
}
});
});

function makeMap()
{
geocoder = new google.maps.Geocoder();
var myOptions = {
zoom: 11,
mapTypeId: google.maps.MapTypeId.ROADMAP
};
map = new google.maps.Map(document.getElementById("map"), myOptions);
createMap();
}

function makeMarker(location, i)
{
map.setCenter(location);
var marker = new google.maps.Marker({
map: map,
position: location,
title : ""+address[i]
});
marker.set("bulkContent", addressInfo[i]);
google.maps.event.addListener(marker,"click",function() {
infowindow.setContent(this.get("bulkContent"));
infowindow.open(map, this);
});
}

function createMap()
{
for(var i=0; i < address.length; i++)
{
if(geocoder) {
(function(i) {
geocoder.geocode({'address': address[i]}, function(results, status) {
if(status == google.maps.GeocoderStatus.OK)
{
makeMarker(results[0].geometry.location, i);
}
});
})(i);
}
}
}

<a href="#" onclick="javascript:makeMap();return false;">init map</a>
<div id="map" style="width:500px; height:500px;"></div>
</body>



// address.php
$address = array(
"1, rue de Belleville, Paris",
"1, rue Georges Berger, Paris",
"1, rue Lafayette, Paris",
"1, rue de Clichy, Paris",
"1, rue de Londres, Paris",
"1, rue de Lisbonne, Paris",
"1, rue Faraday, Paris",
"1, rue Duret, Paris",
"1, rue Vernet, Paris",
"1, rue Hautefeuille, Paris",
"1, rue Christoph Colomb, Paris"
);

$info = array(
"Bellevile",
"George Berger",
"Lafayette",
"Clichy",
"Londres",
"Lisbonne",
"Faraday",
"Duret",
"Vernet",
"Hautefeuille",
"Christop Colomb"
);

echo json_encode(array("address" => $address, "info" => $info));
die();

Les adresses sont mises correctement sur la carte. Maintenant prenez le code et essayez de gérer les adresses différemment, par exemple avec un champ caché contenant la clé de l'adresse à insérer :
<body>
<script type="text/javascript" src="http://code.jquery.com/jquery-latest.js"></script>
<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false"></script>
<script type="text/javascript">
var geocoder;
var map;
var marker;
var infowindow = new google.maps.InfoWindow();
var address = new Array();
var addressInfo = new Array();
$(document).ready(function() {
$.ajax({
type: "GET",
url: "/closures/address.php",
dataType: "json",
success: function(ret)
{
for(var i=0; i < ret.address.length; i++)
{
address[i] = ret.address[i];
addressInfo[i] = ret.info[i];
}
},
error: function(jqXHR, textStatus, errorThrown)
{
alert("An error occured");
}
});
});

function makeMap()
{
geocoder = new google.maps.Geocoder();
var myOptions = {
zoom: 11,
mapTypeId: google.maps.MapTypeId.ROADMAP
};
map = new google.maps.Map(document.getElementById("map"), myOptions);
createMap();
}

function makeMarker(location)
{
var i = parseInt($("#i").val());
map.setCenter(location);
var marker = new google.maps.Marker({
map: map,
position: location,
title : ""+address[i]
});
marker.set("bulkContent", addressInfo[i]);
google.maps.event.addListener(marker,"click",function() {
infowindow.setContent(this.get("bulkContent"));
infowindow.open(map, this);
});
$("#i").val(i+1)
}

function createMap()
{
for(var i=0; i < address.length; i++)
{
if(geocoder) {
geocoder.geocode({'address': address[$("#i").val()]}, function(results, status) {
if(status == google.maps.GeocoderStatus.OK)
{
makeMarker(results[0].geometry.location);
}
});
}
}
}

<a href="#" onclick="javascript:makeMap();return false;">init map</a>
<div id="map" style="width:500px; height:500px;"></div>
<input type="hidden" name="i" id="i" value="0" />
</body>

Vous verrez que tous les markers sont placés dans un seul endroit et, qu'en plus, la fenêtre d'information contient le dernier libellé récupéré (Christoph Colomb).

Closures sont donc un élément important dans le traitement d'un grand nombre de données dans quelques opérations simultanément. N'hésitez donc pas à penser à eux car ils peuvent s'avérer la solution à beaucoup de problèmes.
Bartosz KONIECZNY 13-06-2012 20:22 applications web
Moi

Développeur d'applications Internet et journaliste passionné par l'adjectif français. Un aigle polonais orienté vers la progression, volant très haut et écoutant du zouk après les matches du foot français.

Vous appréciez mon travail ?

Pour contribuer au développement de ce site, ou pour remercier pour des articles rédigés, vous pouvez faire un don.

Un conseil Symfony1

Comment inclure un template commun pour plusieurs modules ?

Le fichier à inclure (par exemple _menu.php) devrait être stocké dans le répertoire templates de l'application en question.

Ensuite, dans notre fichier de layout (par exemple layout.php), il suffit d'appeler le helper include_partial : Le répertoire global signifie que le template est global et n'appartient pas à un module particulier.