Nouveautés dans JavaScript 1.7
Un article de MDC.
Cet article traite de fonctionnalités introduites dans Firefox 2
JavaScript 1.7 est une mise à jour du langage qui introduit plusieurs nouvelles fonctionnalités, en particulier : des générateurs et des itérateurs, la définition de tableaux par compréhension, les expressions let et l'assignation déstructurante. Il reprend également toutes les fonctionnalités de JavaScript 1.6.
JavaScript 1.7 est disponible à partir de Firefox 2 bêta 1, ainsi que sur le tronc (Minefield).
Les exemples de code fournis dans cet article peuvent être essayés dans le shell JavaScript. Consultez Introduction au shell JavaScript pour apprendre comment mettre en place et utiliser le shell.
Sommaire |
[modifier] Utilisation de JavaScript 1.7
Afin de pouvoir utiliser certaines des nouvelles fonctionnalités de JavaScript 1.7, il est nécessaire de spécifier qu'on utilise JavaScript 1.7. Dans du code HTML ou XUL, on utilisera :
<script type="application/javascript;version=1.7"/>
Si vous utilisez le shell JavaScript, vous devrez définir la version utilisée à l'aide de la fonction version() :
version(170);
Pour utiliser les fonctionnalités nécessitant l'utilisation des nouveaux mots-clés « yield » et « let », il est obligatoire de spécifier qu'on utilise la version 1.7, les scripts existants pouvant utiliser ces mots-clés comme noms de variables ou de fonctions. Les fonctionnalités n'utilisant pas de nouveaux mots-clés (assignations déstructurantes et définitions de tableaux par compréhension) peuvent être utilisées sans préciser la version de JavaScript.
[modifier] Générateurs et itérateurs
Au cours du développement de code utilisant un algorithme itératif (comme le parcours d'une liste, ou des calculs répétitifs sur le même jeu de données), on rencontre souvent des variables d'état dont les valeurs doivent être conservées tout au long du processus de calcul. Traditionnellement, on utilise une fonction de callback pour obtenir les valeurs intermédiaires d'un algorithme itératif.
[modifier] Générateurs
Prenons cet algorithme itératif calculant les nombres de Fibonacci :
function do_callback(num) {
document.write(num + "<br>\n");
}
function fib() {
var i = 0, j = 1, n = 0;
while (n < 10) {
do_callback(i);
var t = i;
i = j;
j += t;
n++;
}
}
fib();
Ce code utilise une routine de callback pour réaliser des opérations à chaque étape itérative de l'algorithme. Dans ce cas, chaque nombre de Fibonacci est simplement affiché dans la console.
Les nouveaux générateurs et itérateurs fonctionnent ensemble pour fournir une meilleure manière de faire ceci. Voyons à quoi ressemble la routine des nombres de Fibonacci lorsqu'on utilise un générateur :
function fib() {
var i = 0, j = 1;
while (true) {
yield i;
var t = i;
i = j;
j += t;
}
}
var g = fib();
for (var i = 0; i < 10; i++) {
document.write(g.next() + "<br>\n");
}
La fonction contenant le mot-clé yield est un générateur. Lorsqu'on l'appelle, ses paramètres formels sont liés aux arguments effectifs, mais son corps n'est pas évalué. Au lieu de cela, un générateur-itérateur est renvoyé. Chaque appel à la méthode next() du générateur-itérateur provoque un nouveau passage au travers de l'algorithme itératif. La valeur à chaque étape est celle spécifiée par le mot-clé yield. Pensez à yield comme la version générateur-itérateur de return, indiquant la limite entre chaque itération de l'algorithme. Chaque fois que next() est appelée, le code du générateur reprend à l'instruction suivant le yield.
On cycle dans un générateur-itérateur en appelant à plusieurs reprises sa méthode next() jusqu'à ce que la condition résultante désirée soit atteinte. Dans cet exemple, on peut obtenir autant de nombres de Fibonacci que l'on veut en continuant à appeler g.next() jusqu'à ce que le nombre de résultats voulu soit
atteint.
[modifier] Reprise d'un générateur à un certain point
Une fois qu'un générateur a été lancé en appelant sa fonction next(), il est possible d'utiliser send(), en passant une valeur particulière qui sera traitée comme si elle était le résultat du dernier yield. Le générateur renverra alors l'opérande du yield suivant.
Il n'est pas possible de démarrer un générateur à un point arbitraire ; il doit d'abord être démarré avec next() avant de pouvoir lui envoyer une valeur spécifique avec send().
send(undefined) est équivalent à un appel à next(). Cependant, en démarrant un nouveau générateur avec toute autre valeur qu'undefined par un appel de send() provoquera une exception TypeError.[modifier] Exceptions dans les générateurs
Il est possible d'obliger un générateur à déclencher une exception en appelant sa méthode throw(), en lui passant la valeur d'exception à déclencher. Cette exception sera déclenchée depuis le contexte courant suspendu du générateur, comme si le yield qui est actuellement suspendu était plutôt une instruction throw valeur.
Si aucun yield n'est rencontré au cours du traitement de l'exception, celle-ci se propagera vers le haut au travers de l'appel à throw(), et les appels à next() subséquents résulteront en une exception StopIteration.
[modifier] Fermeture d'un générateur
Les générateurs ont une méthode close() qui les forcent à se fermer eux-mêmes. Les effets de la fermeture d'un générateur sont :
- Tous les clauses
finallyactives dans la fonction du générateur sont exécutées. - Si une clause
finallydéclenche une exception autre queStopIteration, celle-ci se propage à l'appelant de la méthodeclose(). - Le générateur se termine.
[modifier] Exemple de générateur
Ce code conduit un générateur qui fera un yield toutes les 100 itérations.
var gen = generator();
function driveGenerator() {
if (gen.next()) {
window.setTimeout(driveGenerator, 0);
} else {
gen.close();
}
}
function generator() {
while (i < quelquechose) {
/** d'autres choses **/
++i;
/** 100 boucles par yield **/
if ((i % 100) == 0) {
yield true;
}
}
yield false;
}
[modifier] Itérateurs
Un itérateur est un objet spécial qui permet un parcours itératif de données.
Dans leur utilisation normale, les objets itérateurs sont « invisibles » ; il n'est pas nécessaire de les utiliser explicitement, mais on utilisera plutôt les instructions JavaScript for...in et for each...in pour boucler de manière naturelle sur les clés et/ou valeurs des objets.
var objectWithIterator = getObjectSomehow();
for (var i in objectWithIterator)
{
document.write(objectWithIterator[i] + "<br>\n");
}
Si vous implémentez votre propre objet itérateur, ou avez besoin pour une autre raison de manipuler des itérateurs, vous devrez connaître la méthode next, l'exception StopIteration et la propriété __iterator__.
On peut créer un itérateur pour un objet en appelant Iterator(nomdobjet) ; l'itérateur d'un objet peut être trouvé en utilisant sa propriété __iterator__, qui par défaut implémente l'itération selon le modèle habituel for...in et for each...in. Si vous voulez fournir un itérateur personnalisé, vous devez écraser l'accesseur pour __iterator__ afin qu'il renvoie une instance de votre itérateur personnalisé. Pour obtenir un itérateur d'objet depuis un script, vous devriez utiliser Iterator(obj) plutôt qu'accéder à la propriété __iterator__ directement.
Une fois l'itérateur obtenu, il est facile de passer à l'élément suivant dans l'objet en appelant la méthode next() de l'itérateur. S'il n'y a plus de donnée à parcourir, l'exception StopIteration est déclenchée.
Voici un exemple simple de manipulation directe d'un itérateur :
var obj = {name:"Jack Bauer", username:"JackB", id:12345, agency:"CTU", region:"Los Angeles"};
var it = Iterator(obj);
try {
while (true) {
document.write(it.next() + "<br>\n");
}
} catch (err if err instanceof StopIteration) {
document.write("End of record.<br>\n");
} catch (err) {
document.write("Unknown error: " + err.description + "<BR>\n");
}
Le résultat affiché par ce programme ressemble à ceci :
name,Jack Bauer username,JackB id,12345 agency,CTU region,Los Angeles End of record.
Il est possible, optionnellement, de spécifier un second paramètre à la création de l'itérateur, qui est une valeur booléenne indiquant si seules les clés doivent être renvoyées à chaque fois que la méthode next() est appelée. Si on change var it = Iterator(obj); en var it = Iterator(obj, true); dans l'exemple ci-dessus, on obtient le résultat suivant :
name username id agency region End of record.
Dans les deux cas, l'ordre réel dans lequel les données sont renvoyées peut varier selon l'implémentation. Aucun ordre n'est garanti dans les données.
Les itérateurs sont une manière pratique de parcourir les données dans des objets, même les objets qui peuvent contenir des données dont vous ne connaissez pas l'existence. C'est particulièrement utile dans le cas où des données non attendues par votre application doivent toutefois être préservées.
[modifier] Définition de tableaux par compréhension
Les tableaux définis par compréhension sont une utilisation des générateurs fournissant une manière pratique et puissante d'initialiser des tableaux. Par exemple :
function range(begin, end) {
for (let i = begin; i < end; ++i) {
yield i;
}
}
range() est un générateur renvoyant toutes les valeurs entre begin et end. Une fois cette fonction définie, on peut l'utiliser comme ceci :
var ten_squares = [i * i for (i in range(0, 10))];
Un nouveau tableau ten_squares est préinitialisé pour contenir les carrés des valeurs dans l'intervalle 0..9.
On peut préciser n'importe quelle condition à l'initialisation du tableau. Si vous voulez initialiser un tableau pour contenir les nombres pairs entre 0 et 20, vous pouvez utiliser ce code :
var evens = [i for (i in range(0, 21)) if (i % 2 == 0)];
Avant JavaScript 1.7, il aurait été nécessaire d'écrire quelque chose de semblable à ceci :
var evens = [];
for (var i=0; i <= 20; i++) {
if (i % 2 == 0)
evens.push(i);
}
La notation par compréhension est non seulement plus compacte, mais elle est également plus facile à lire une fois familiarisé avec le concept.
[modifier] Règles de portée
Les définitions de tableaux par compréhension ont un bloc implicite autour d'elles, contenant tout ce qui se trouve entre les crochets droits, ainsi que des déclarations let implicites.
Plus de détails sont nécessaires.
[modifier] Portée des variables avec let
Il y a plusieurs manières d'utiliser let pour gérer la porté de bloc de données et de fonctions :
- L'instruction
letest une manière d'associer des valeurs avec des variables, constantes et fonctions au sein d'un bloc sans affecter les valeurs de variables portant le même nom en dehors du bloc. - L'expression
letpermet de mettre en place des variables dont la portée se limite à une seule expression. - La définition avec
letdéfinit des variables, constantes et fonctions dont la portée est limitée au bloc dans lequel elles sont définies. Cette syntaxe est très similaire à celle utilisée avecvar. - Il est également possible d'utiliser
letpour mettre en place des variables existant uniquement dans le contexte d'une bouclefor.
[modifier] L'instruction let
L'instruction let fournit une portée locale pour les variables, constantes et fonctions. Elle fonctionne en liant un ensemble de variables à la portée lexicale d'un seul bloc de code. La valeur de terminaison de l'instruction let est celle du bloc.
Par exemple :
var x = 5;
var y = 0;
let (x = x+10, y = 12) {
document.write(x+y + "<br>\n");
}
document.write(x+y + "<br>\n");
L'affichage produit par ce programme sera :
27 5
Les règles pour le bloc de code sont les mêmes que pour n'importe quel autre bloc de code en JavaScript. Il peut avoir ses propres variables locales déclarées à l'aide de let.
let sont ici obligatoires. Leur absence résulterait en une erreur de syntaxe.[modifier] Règles de portée
La portée des variables définies à l'aide de let est le bloc let lui-même, ainsi que tout bloc interne contenu à l'intérieur de celui-ci, à moins que ces blocs définissent des variables ayant les mêmes noms.
[modifier] Expressions avec let
On peut utiliser let pour définir des variables dont la portée se limite à une seule expression :
var x = 5; var y = 0; document.write( let(x = x + 10, y = 12) x+y + "
\n"); document.write(x+y + "
\n");
L'affichage produit est :
27 5
Dans ce cas, la liaison entre les valeurs de x et y, et les expressions x+10 et 12 a une portée limitée uniquement à l'expression x+y.
[modifier] Règles de portée
Avec une expression let donnée :
let (decls) expr
Un bloc implicite est créé autour de expr.
[modifier] Définitions avec let
Le mot-clé let peut également être utilisé pour définir des variables, constantes et fonctions au sein d'un bloc.
let, n'hésitez pas à les ajouter ici.
if (x > y)
{
let gamma = 12.7 + y;
i = gamma * x;
}
Les instructions, expressions et définitions let rendent parfois le code plus propre lorsque des fonctions internes sont utilisées.
var list = document.getElementById("list");
for (var i = 1; i <= 5; i++) {
var item = document.createElement("LI");
item.appendChild(document.createTextNode("Item " + i));
let j = i;
item.onclick = function (ev) {
alert("Item " + j + " is clicked.");
};
list.appendChild(item);
}
L'exemple ci-dessus fonctionne comme prévu parce que les cinq instances de la fonction interne (anonyme) font référence à cinq instances différentes de la variable j. Notez qu'il ne fonctionne pas comme prévu si vous remplacez let par var ou si vous supprimez la variable j et utilisez simplement la variable i dans la fonction interne.
[modifier] Règles de portée
Les variables déclarées avec let ont leur portée limitée au bloc dans lequel elles sont définies, ainsi que dans tout sous-bloc dans lequel elles ne sont pas redéfinies. Dans ce sens, let fonctionne tout à fait comme var. La différence principale est que la portée d'une variable déclarée avec var est la fonction contenante entière :
function varTest() {
var x = 31;
if (true) {
var x = 71; // même variable !
alert(x); // 71
}
alert(x); // 71
}
function letTest() {
let x = 31;
if (true) {
let x = 71; // variable différente
alert(x); // 71
}
alert(x); // 31
}
L'expression à la droite du signe = est à l'intérieur du bloc. Ce n'est pas ainsi que les expressions et instructions avec let traitent la porté :
function letTests() {
let x = 10;
// instruction let
let (x = x + 20) {
alert(x); // 30
}
// expression let
alert(let (x = x + 20) x); // 30
// définition let
{
let x = x + 20; // x est évalué ici à undefined
alert(x); // undefined + 20 ==> NaN
}
}
Dans les programmes et classes, let ne crée pas de propriétés sur l'objet global comme le ferait var ; au lieu de cela, des propriétés sont créées dans un bloc implicite créé pour l'évaluation des instructions dans ces contextes. Essentiellement, cela signifie que let n'écrasera pas de variables définies précédemment avec var. Par exemple :
** Ne fonctionne pas dans Firefox 2.0 b1. Renvoie "42" au lieu de "global". var x = 'global'; let x = 42; document.write(this.x + "
\n");
Ce code affichera « global », et non « 42 ».
Un bloc implicite est un bloc non entouré de crochets ; il est créé implicitement par le moteur JavaScript.
Dans les fonctions, let lorsqu'il est exécuté par eval() ne crée pas de propriétés sur l'objet de la variable (objet d'activation ou de liaison interne) comme var le ferait ; au lieu de cela, des propriétés sont créées dans un bloc implicite créé pour l'évaluation d'instuctions dans le programme. Il s'agit d'une conséquence du comportement d'eval() sur des programmes combiné avec la règle qui précède.
Autrement dit, lorsqu'eval() est utilisé pour exécuter du code, ce code est traité comme un programme indépendant, qui a son propre bloc implicite autour de son code.
[modifier] Variables déclarées avec let dans des boucles for
Le mot-clé let peut être utilisé pour lier des variables localement dans la portée de boucles for, exactement comme avec var.
var i=0;
for ( let i=i ; i < 10 ; i++ )
document.write(i + "<br>\n");
for ( let &[name,value] in obj )
document.write("Name: " + name + ", Value: " + value + "<br>\n");
[modifier] Règles de portée
for (let expr1; expr2; expr3) instruction;
Dans cet exemple, expr2, expr3 et instruction font partie d'un bloc implicite contenant les variables de bloc déclarées avec let expr1. C'est ce qui est montré dans la première boucle de l'exemple ci-dessus.
for (let expr1 in expr2) instruction; for each(let expr1 in expr2) instruction;
Dans ces deux cas, instruction fait partie d'un bloc implicite. Le premier de ces cas est présenté dans la seconde boucle de l'exemple ci-dessus.
[modifier] Assignation déstructurante
L'assignation déstructurante rend possible l'extraction de données depuis des tableaux ou des objets en utilisant une syntaxe reproduisant celle des déclarations littérales de tableaux et d'objets.
Les expressions littérales d'objet et de tableau forment une manière facile de créer des paquets de données ad-hoc. Une fois ces paquets créés, ils peuvent être utilisés de n'importe quelle manière. Ils peuvent même être renvoyés par des fonctions.
Une application particulièrement utile de l'assignation déstructurante est de lire une structure entière en une seule instruction, bien qu'il y ait un bon nombre d'autre choses intéressantes comme le montrent les exemples développés plus bas.
Cette possibilité est similaire à celles qui sont présentes dans des langages comme Perl et Python.
[modifier] Exemples
L'assignation déstructurante s'explique le mieux au travers d'exemples, en voici donc quelques uns qui vous permettront de comprendre.
[modifier] Économie de variables temporaires
L'assignation déstructurante peut par exemple être utilisée pour échanger des valeurs :
var a = 1; var b = 3; [a, b] = [b, a];
Après exécution de ce code, b vaut 1 et a vaut 3. Sans l'assignation déstructurante, l'échange de deux valeurs nécessite une variable temporaire (ou, dans certains langages de bas niveau, l'astuce de l'échange XOR (en)).
De même, on peut l'utiliser pour effectuer la rotation de trois variables ou plus :
var a = 'o';
var b = "<span style='color:green;'>o</span>";
var c = 'o';
var d = 'o';
var e = 'o';
var f = "<span style='color:blue;'>o</span>";
var g = 'o';
var h = 'o';
for (lp = 0; lp < 40; lp++) {
[a, b, c, d, e, f, g, h] = [b, c, d, e, f, g, h, a];
document.write(a+''+b+''+c+''+d+''+e+''+f+''+g+''+h+''+"<br />");
}
Après exécution de ce code, un affichage visuel coloré de la rotation de variables apparaitra.
Pour en revenir à notre exemple de générateur de nombres de Fibonacci, on peut éliminer la variable temporaire t en calculant les nouvelles valeurs de i et j en une seule instruction d'assignation groupée :
function fib() {
var i = 0, j = 1;
while (true) {
yield i;
[i, j] = [j, i + j];
}
}
var g = fib();
for (let i = 0; i < 10; i++)
print(g.next());
[modifier] Renvoi de valeurs multiples
Grâce à l'assignation déstructurante, les fonctions peuvent renvoyer plusieurs valeurs. Bien qu'il ait toujours été possible de renvoyer un tableau, on a ici un degré de flexibilité supplémentaire :
function f() {
return [1, 2];
}
Comme vous pouvez le voir, le renvoi de résultats se fait à l'aide d'une notation semblable à un tableau, avec toutes les valeurs à renvoyer entourées de crochets droits. N'importe quel nombre de résultats peut être renvoyé de cette manière. Dans cet exemple, f() renvoie les valeurs [1, 2].
var a, b;
[a, b] = f();
document.write ("A vaut " + a + " B vaut " + b + "
\n");
La commande [a, b] = f() assigne le résultat de la fonction aux variables entre crochets, dans l'ordre : a est positionnée à 1 et b est positionnée à 2.
Les valeurs de retour peuvent également être récupérées dans un tableau :
var a = f();
document.write ("A vaut " + a);
Dans ce cas, a est un tableau contenant les valeurs 1 et 2.
[modifier] Parcours d'objets
L'assignation déstructurante peut également être utilisée pour extraire des données d'un objet :
var obj = { largeur: 3, longueur: 1.5, couleur: "orange" };
for (let[nom, valeur] in obj) {
document.write ("Nom : " + nom + ", valeur : " + valeur + "
\n");
}
On parcourt ici toutes les paires clé/valeur de l'objet obj pour les afficher. La sortie ressemble à ceci :
Nom : largeur, valeur : 3 Nom : longueur, valeur : 1.5 Nom : couleur, valeur : orange
[modifier] Parcours des valeurs dans un tableau d'objets
Il est possible de parcourir un tableau d'objets, en retirant les champs qui nous intéressent de chaque objet :
var people = [
{
name: "Mike Smith",
family: {
mother: "Jane Smith",
father: "Harry Smith",
sister: "Samantha Smith"
},
age: 35
},
{
name: "Tom Jones",
family: {
mother: "Norah Jones",
father: "Richard Jones",
brother: "Howard Jones"
},
age: 25
}
];
for each (let {name: n, family: { father: f } } in people) {
document.write ("Name: " + n + ", Father: " + f + "<br>\n");
}
Ici, on récupère le champ name dans la variable n et le champ family.father dans la variable f, avant de les afficher. Cette opération est répétée pour chaque objet du tableau people. La sortie ressemble à ceci :
Name: Mike Smith, Father: Harry Smith Name: Tom Jones, Father: Richard Jones
[modifier] Ignorer certaines valeurs renvoyées
Vous pouvez également ignorer les valeurs de retour qui ne vous intéressent pas :
function f() {
return [1, 2, 3];
}
var [a, , b] = f();
document.write ("A vaut " + a + ", B vaut " + b + "<br>\n");
Après exécution de ce code, a vaut 1 et b vaut 3. La valeur 2 est ignorée. Il est possible d'ignorer n'importe quelle valeur, ou même toute les valeurs renvoyées de cette manière. Par exemple :
[,,,] = f();
[modifier] Récupération de valeurs depuis un résultat d'expression rationnelle
Lorsque la méthode exec() d'une expression rationnelle rencontre une occurrence, elle renvoie un tableau contenant d'abord l'occurrence complète de la chaine, et ensuite les parties de la chaines correspondant à chaque groupe de parenthèses de l'expression rationnelle. L'assignation déstructurante permet de récupérer les parties de ce tableau facilement, en ignorant l'occurrence complète si elle n'est pas nécessaire.
// Expression rationnelle simple pour reconnaitre les URL de style http / https / ftp. var parsedURL = /^(\w+)\:\/\/([^\/]+)\/(.*)$/.exec(url); if (!parsedURL) return null; var [, protocol, fullhost, fullpath] = parsedURL;