Article JavaScript
Un article de WikiRaph.
L'objectif de cet article est de présenter et démystifier JavaScript. Ce langage de programmation a été popularisé par Internet, le DHTML ("Dynamic HTML") et plus récemment AJAX ("Asynchronous JavaScript and XML"). Cependant il s'agit d'un langage objet puissant et protéiforme, qui mérite à être plus connu et reconnu.
Sommaire |
Un peu d'histoire
JavaScript est un langage de script objet inventé en 1995 au sein de Netscape Communications Corporation par Brendan Eich (désormais directeur technique de Mozilla Corporation). D'abord baptisé Mocha puis LiveScript, il a été renommé en JavaScript lors de son intégration à la première version de Netscape Navigator à supporter le langage Java de Sun Microsystems. Son succès comme langage de script Web coté client poussa Microsoft en 1996 à développer pour Internet Explorer un langage plus au moins compatible avec JavaScript : JScript.
Après soumission de la spécification JavaScript par Netscape à l'organisme de standardisation "Ecma International", la spécification ECMAScript vit le jour en 1997 (référence ECMA-262). Les technologies JavaScript et JScript visent désormais l'implémentation du standard tout en proposant de nouvelles fonctionnalités pas (encore) incluses dans ECMAScript.
| Implémentation | Éditeur | Langage | Licence | Remarques |
|---|---|---|---|---|
| SpiderMonkey | Mozilla | C | MPL/GPL/LGPL | Première implémentation, créée en 1995 |
| JScript | Microsoft | ?? | Propriétaire | Intégré en 1996 à Internet Explorer |
| Rhino | Mozilla | Java | MPL/GPL/LGPL | Existe depuis 1998, intégré à Java SE 6 depuis décembre 2006 |
| ActionScript | Adobe | ? | Propriétaire | Tamarin (cf. plus bas) |
| KDE's JavaScript engine (KJS) | KDE | C++ | GPL | Intégré à Konqueror en 2000 |
| JavaScriptCore | Apple | C++ | GPL | Basé sur KJS en 2002 et intégré à WebKit |
En novembre 2006, Adobe a fait don d'une partie de la machine virtuelle et du compilateur "Just In Time" (JIT) d'ActionScript au projet Mozilla. Ce code va être intégré à SpiderMonkey, sous le nom de Tamarin, pour constituer la nouvelle version du moteur JavaScript de Mozilla, compatible avec ECMASCript version 4 (ou plus communément JavaScript 2) et intégrée à Firefox 3.
Un langage orienté objet ?
Les puristes considèrent souvent que JavaScript n'est pas un langage respectant le paradigme de programmation orientée objet. Il ne faut pas pour autant passer à coté du fait que JavaScript tire sa véritable puissance de son modèle orienté objet par prototypes, inspiré du langage Self, et considéré par certains comme une alternative au modèle orienté objet pas classes.
Objets génériques et avec constructeurs
JavaScript est fondamentalement construit autour du concept d'objet. Ainsi tout y est objet ou référence vers un objet. Par exemple, les tableaux, les types, les fonctions sont des objets. Les objets contiennent des membres, appelés propriétés, sous forme de paires (nom, valeur). Les nom étant des chaînes de caractères et les valeurs des chaînes, des nombres, des booléens ou des objets (y compris des tableaux et des fonctions). On peut voir un objet comme un tableau associatif permettant de rapidement récupérer la valeur associée à un nom.
//Création d'un objet générique
var MonObjet = new Object;
//Déclaration d'un attribut
MonObjet.attribut = "Attribut";
//Déclaration d'une méthode
MonObjet.fonction = function() {
return "Fonction";
}
//Accès aux membres de l'objet
MonObjet.attribut; //retourne "Attribut"
MonObjet.fonction() ; //retourne "Fonction"
//Accès sous forme de tableau associatif
MonObjet['attribut']; //retourne "Attribut" (équivalent à MonObjet.attribut)
MonObjet['fonction']; : //retourne : function () { return "Fonction"; }
MonObjet['fonction'](); //retourne "Fonction" (équivalent à MonObjet.fonction())
Parmi les membres d'un objet, Javascript ne fait pas de différence entre les données (attributs) et les fonctions (méthodes). Les fonctions peuvent accéder aux autres membres de l'objet via la variable this.
Les objets plus complexes que les objets génériques sont créés à l'aide de constructeurs, des fonctions appelées et exécutées à la création de l'objet (via l'instruction new).
//Constructeur de l'objet Chien
function Chien(param) {
//Déclaration d'un attribut
this.nom = param;
//Déclaration d'une méthode
this.aboyer = function() {
return "ouaf !";
}
}
//Création d'un nouvel objet de type Chien
var MonChien = new Chien("Médor");
MonChien.name; //contient "Médor"
MonChien.aboyer(); //retourne "ouaf !"
Le constructeur d'un objet est accessible via la propriété constructor.
MonChien.constructor //retourne : function Chien(param) { this.nom = param; this.aboyer = function () {return "ouaf !";}; }
Typage des objets
JavaScript est un langage dynamiquement typé. Bien que le langage gère des types primaires (récupérable via l'opérateur typeof) il est plus utile de considérer que le type d'un objet est le nom de son constructeur (récupérable via la propriété constructor.name). Selon cette vision, les types gérés par JavaScript sont les suivants:
- Object : objet générique non typé ;
- Boolean : booléen (vaut true ou false) ;
- Number : nombre entier ou décimal ;
- String : chaîne de caractères ;
- Array : tableau ;
- Function : fonction contenant une portion de code ;
- [NomConstructeur] : type défini par l'utilisateur via une fonction constructeur.
var monNombre = 1;
//monNombre.constructor.name vaut : Number
var maChaine = "Salut";
//maChaine.constructor.name vaut : String
var monTableau = [1, "brt"];
//monTableau.constructor.name vaut : Array
var maFonction = function() {
return "coucou";
}
//maFonction.constructor.name vaut : Function
var monObjet1 = new Object();
//monObjet1.constructor.name vaut : Object
var monObjet2 = {nom: "Joe", prenom: "Black"};
//monObjet2.constructor.name vaut : Object
function Voiture(modele) {
this.modele = modele;
}
var maVoiture = new Voiture("coupé");
//maVoiture.constructor.name vaut : Voiture
Prototypes
Pour tout objet doté d'un constructeur, JavaScript gère un objet particulier, appelé prototype, qui sert de modèle à tous les objets créés via ce constructeur. Cet objet est accessible via la propriété prototype de l'objet doté du constructeur.
Ainsi dans l'exemple précédent, l'objet MonChien est créé à partir du prototype de l'objet Chien.
Lorsqu'une valeur est affectée à une propriété d'un objet, JavaScript créé la propriété au niveau de l'objet si elle n'existe pas déjà. En revanche lorsque la valeur dune propriété d'un objet est demandée, JavaScript réalise les opérations suivantes :
- si la propriété existe au niveau de l'objet sa valeur est retournée ;
- sinon, JavaScript accède au prototype de l'objet et retourne la valeur de la propriété du prototype si elle existe ;
- sinon, JavaScript accède au prototype du prototype, etc...
- en remontant ainsi la chaîne des prototypes, si la propriété n'est pas trouvée, JavaScript finit par arriver au prototype le plus haut, celui de l'objet générique Object et retourne undefined.
Le prototype d'un objet étant lui-même un objet, il peut être modifié dynamiquement. La modification d'un membre du prototype (attribut ou méthode) n'impacte pas ses clones. En revanche, l'ajout d'un nouveau membre est répercuté sur l'ensemble des clones, via le mécanisme de remontée de la chaîne des prototype présenté plus haut. Cela permet de modifier dynamiquement et de manière rétroactive tous les objets possédant le même prototype.
function Chien(param) {
this.nom = param;
this.aboyer = function() {
return "ouaf !";
}
}
var MonChien = new Chien("Médor");
Chien.prototype.dormir = function() {
return "Chut ! " + this.nom + " dort...";
}
MonChien.dormir(); //retourne "Chut ! Médor dort..."
Ceci est également applicable aux objets pré définis de JavaScript.
String.prototype.inverser = function() {
var inverse = "";
for (i = this.length - 1; i >= 0; i--) {
inverse += this.charAt(i);
}
return inverse;
}
var chaine = "Bonjour !";
chaine.inverser(); //retourne "! ruojnoB"
La méthode inverser() est devenue disponible pour toutes les chaînes de caractères !
Membres statiques
Les membres d'un objet déclarés hors du prototype (et donc hors du constructeur) ne sont pas clonés lors de la création de nouveaux objets fils. Cela permet d'implémenter l'équivalent des méthodes et des propriétés statiques de la programmation par classes (on parle parfois de méthodes et propriétés de classes).
function Chien(param) {
this.nom = param;
this.aboyer = function() {
return "ouaf !";
}
}
Chien.homonyme = function(chien1, chien2) {
return (chien1.nom == chien2.nom);
}
var monChien1 = new Chien("Médor");
var monChien2 = new Chien("Rocky");
Chien.homonyme(monChien1, monChien2); //Retourne : false
monChien2.nom = "Médor";
Chien.homonyme(monChien1, monChien2); //Retourne : true
Encapsulation
Un autre a priori courant est de penser que JavaScript, puisqu'il s'agit d'un langage interprété, ne sait pas gérer la notion de membres privés d'un objet et donc n'implémente pas le principe d'encapsulation. Or il se trouve que JavaScript permet de distinguer plusieurs niveaux de visibilité :
- composants publics : les composants (attributs ou méthodes) d'un objet sont, par défaut, accessibles à tous ;
function Objet1() {
this.attribut = "Attribut public";
}
MonObjet1.prototype.fonction = function() {
return "Fonction publique";
}
var MonObjet1 = new Objet1();
MonObjet1.attribut; //contient "Attribut public"
MonObjet1.fonction(); //retourne "Fonction publique"
- composants privés : les composants créés dans le constructeur sont accessibles seulement aux les méthodes privées de l'objet lui-même ;
function Objet2() {
var attribut = "Attribut privé";
function fonction() {
return "Fonction privée";
}
}
var MonObjet2 = new Objet2();
MonObjet2.attribut; //contient "undefined"
MonObjet2.fonction(); //retourne l'erreur "MonObjet2.fonction() is not a function"
- méthodes privilégiées : méthodes pouvant accéder aux composants privés de l'objet tout en étant accessibles depuis l'extérieur.
function Objet3() {
var attribut = "Attribut privé";
this.fonction = function() {
return(attribut);
};
}
var MonObjet3 = new Objet3();
MonObjet3.fonction(); //retourne "Attribut privé"
Héritage
Le modèle de programmation par prototypes n'utilise pas les notions de classes et d'instances de classes mais uniquement celle d'objets. L'héritage ne se fait donc pas entre classes selon un modèle défini de manière statique, mais directement entre prototype d'objets.
L'héritage entre deux objets est donc obtenu en assignant le prototype de l'objet parent au prototype de l'objet fils, ainsi, le fils hérite des propriétés du père. Il est ensuite possible d'ajouter de nouvelles propriétés au fils, voire de remplacer celles héritées du père.
//Objet père
function Animal() {
this.dormir = function() {
return this.nom + " dort...";
}
}
//Premier objet fils
function Chien(nom) {
this.nom = nom;
}
//qui hérite de Animal
Chien.prototype = new Animal();
//mais qui est de type Chien
Chien.prototype.constructor = Chien;
//Ajout d'une méthode au fils
Chien.prototype.aboyer = function() {
return this.nom + " aboit !";
}
//Deuxième objet fils
function Chat(nom) {
this.nom = nom;
}
//qui hérite de Animal
Chat.prototype = new Animal();
//mais qui est de type Chat
Chat.prototype.constructor = Chien;
//Ajout d'une méthode au fils
Chat.prototype.miauler = function() {
return this.nom + " miaule !";
}
var monChien = new Chien("Médor");
var monChat = new Chat("Felix");
//Ajout d'une méthode à l'objet père, après création des fils
Animal.prototype.manger = function() {
return this.nom + " mange";
}
//monChien.dormir() retourne : Médor dort...
//monChien.aboyer() retourne : Médor aboit !
//monChien.manger() retourne : Médor mange
//monChat.dormir() retourne : Felix dort...
//monChat.miauler() retourne : Felix miaule !
//monChat.manger() retourne : Felix mange
Remarquons que nous aurions pu remonter la propriété nom au niveau de l'objet père Animal, malheureusement JavaScript ne permet pas de passe des paramètres entre le constructeur du fils et celui du père.
Un moyen de contourner cette limitation serait d'utiliser la fonction call de JavaScript (qui permet d'appeler une fonction comme si elle appartenait à un objet) pour appeler le constructeur du père à la création du fils et lui passer le paramètre nom. Cependant cela brise la chaîne des prototypes, comme illustré cet exemple.
//Objet père
function Animal(nom) {
this.nom = nom;
this.dormir = function() {
return this.nom + " dort...";
}
}
//Objet fils
function Chien(nom) {
//Appel du constructeur de Animal
Animal.call(this, nom);
}
//Ajout d'une méthode au fils
Chien.prototype.aboyer = function() {
return this.nom + " aboit !";
}
var monChien = new Chien("Médor");
//Ajout d'une méthode à l'objet père, après création du fils
Animal.prototype.manger = function() {
return this.nom + "mange";
}
//monChien.dormir() retourne : Médor dort...
//monChien.aboyer() retourne : Médor aboit !
//monChien.manger() retourne l'erreur : monChien.manger is not a function
Bien que JavaScript ne supporte pas l'héritage multiple, il est possible de simuler en partie ce comportement, en passant par la fonction call évoquée précédemment mais avec le même inconvénient : la parte du lien dynamique entre les objets pères et fils.
//Premier objet père
function Loup() {
this.mordre = function() {
return this.nom + " vous à mordu !";
}
}
//Deuxième objet père
function Homme() {
this.parler = function() {
return this.nom + " vous parle...";
}
}
//Objet fils
function LoupGarou(nom) {
this.nom = nom;
Loup.call(this); //Appel du constructeur du premier père
Homme.call(this); //Appel du constructeur du deuxième père
this.metamorphoser = function() {
return this.nom + " s'est metamorphosé !";
}
}
var monLoupGarou = new LoupGarou("Jack");
//monLoupGarou.parler() retourne : Jack vous parle...
//monLoupGarou.metamorphoser() retourne : Jack s'est metamorphosé !
//monLoupGarou.mordre() retourne : Jack vous à mordu !
Closures
Le concept de closure est caractéristique très puissante de JavaScript, mais qui reste méconnue ou souvent mal comprise, ce qui peu mener à produire, parfois sans s'en rendre compte, du code défaillant.
En une phrase, il s'agit de la capacité pour une fonction interne d'accéder aux propriétés de la fonction qui la contient, et ce, même après que la fonction contenante ait fini son exécution. Mais détaillons un peu tout cela sous forme d'exemples.
Ce premier exemple utilise une closure pour implémenter une fonction de fonctions :
function CreerActionChien(action) {
return function() {
return this.nom + " est en train de " + action;
}
}
function Chien(nom) {
this.nom = nom;
this.manger = CreerActionChien("manger");
this.dormir = CreerActionChien("dormir");
this.mordre = CreerActionChien("mordre");
}
var monChien = new Chien();
monChien.nom = "Médor";
//monChien.manger() retourne : Médor est en train de manger
//monChien.dormir() retourne : Médor est en train de dormir
//monChien.mordre() retourne : Médor est en train de mordre
Voici un exemple de closure concernant plusieurs fonctions accédant à une propriété commune pour gérer un compteur global :
function initCompteur(debut) {
//Variable local
var compteur = debut;
//Crée les fonctions de manipulation du compteur
getCompteur = function() {
return compteur;
}
incCompteur = function() {
compteur++;
}
setCompteur = function(valeur) {
compteur = valeur;
}
resetCompteur = function(valeur) {
compteur = debut;
}
}
//Initialisation du compteur
initCompteur(0);
setCompteur(10);
incCompteur();
getCompteur(); //retourne : 11
resetCompteur();
getCompteur(); //retourne : 0
Un autre usage classique est de réduire la portée d'une variable grâce à une closure. Ainsi l'exemple de méthode privilégiée montré plus haut, fait usage d'une closure car la fonction interne fonction() accède à la propriété interne attribut de la fonction Objet3() après que Objet3() ait été appelée.
Usages
JavaScript intégré dans le navigateur
JavaScript a longtemps eu, et a encore aux yeux de certains, une mauvaise image. Ceci s'explique par au moins deux raisons :
- les fenêtres popups que JavaScript permet de créer et dont l'usage a été détourné au détriment des internautes ;
- la mauvaise portabilité du code due à des problèmes de compatibilité entre navigateurs, qui ne proviennent pas tant des différences d'implémentations du langage mais plutôt de celles du Document Object Model (DOM).
Ces deux problèmes sont désormais résolus grâce aux systèmes désormais intégrés aux les navigateurs pour bloquer les fenêtres popus et à l'apparition de frameworks masquant la diversité des différentes implémentations du DOM. Ajoutez à cela les applications phares de Google que sont Google Mail et Google Maps, et cela suffit à relancer l'intérêt et l'engouement pour la langage JavaScript, vu désormais comme base du Web 2.0.
Shells JavaScript
L'utilisation de JavaScript n'est pas limitée à l'intérieur d'un navigateur. Il existe par exemple des interpréteurs permettant d'exécuter du JavaScript à partir du système d'exploitation, soit à partir d'un fichier, soit en mode interactif. Pour ce faire, on peut utiliser par exemple :
- technologie Active Scripting de Microsoft, qui permet d'exécuter des scripts JScript sur un environnement Windows :
var texte = "Arguments : ";
var objArgs = WScript.Arguments;
for (var i=0; i < objArgs.length; i++) { //boucle sur les arguments passés au script
texte = texte + objArgs(i) + '\n'; //récupération de l'argument courant
}
WScript.Echo (texte); //affichage des arguments
WScript.Quit();
- shell natif js de Mozilla ou shell Java du projet Rhino de Mozilla :
$ js -e print('Bonjour !') #ou java org.mozilla.javascript.tools.shell.Main -e print('Bonjour !')
Bonjour !
$ js #ou java org.mozilla.javascript.tools.shell.Main
js> print('Bonjour !');
Bonjour !
js> function f() {
return a;
}
js> var a = "Bonjour !";
js> f();
Bonjour !
JavaScript coté serveur
Exemple :
- Alfresco : Rhino JavaScript en frontal d'un coeur en Java (ne fait pas partie de la norme ECMA)
- LiveConnect : à l'air prévu coté client (JavaScript et applet Java)
Un langage encore en évolution
Composants ECMAScript
La norme ECMA-290, publiée en juin 1999, définit les conventions à respecter pour définir et réutiliser du code ECMAScript sous forme de composants. Ceci est réalisé via la description en langage XML des interfaces des composants.
Il semble qu'aucune implémentation de cette norme n'ai été réalisée...
ECMAScript for XML (E4X)
La norme ECMA-357, dont la première version a été publiée en juin 2004 et la deuxième en décembre 2005, spécifie un certain nombre d'extensions à ECMAScript pour ajouter y le support natif de XML.
XML devient un nouveau type d'objet, ce qui simplifie grandement la manipulation de structures XML. Il devient ainsi possible de coder des choses du genre :
var chiens = new XML();
chiens = <chiens>
<chien nom="Médor" race="Saint Bernard"/>
<chien nom="Roxy" race="Berger Allemand"/>
<chien nom="Tchoupi" race="Teckel"/>
</chiens>;
alert(chiens.chien.(@nom == "Médor").@race);
//Affiche: Saint Bernard
for each(var nom in chiens..@nom) {
alert(nom);
}
//Affiche :
//Médor
//Roxy
//Tchoupi
E4X est supporté par les moteurs Mozilla (SpiderMonkey et Rhino), ainsi que par ActionScript 3 d'Adobe (intégré dans Flash Player 9). Pour pouboir utiliser E4X dans Firefox 1.5 et versions ultérieures, il faut ajouter "; e4x=1" à la fin de la valeur de l'attribut type de la balise script.
<script type="text/javascript; e4x=1"> ... </script>
JavaScript 2 ou ECMASCript version 4
La quatrième version de la norme ECMASCript est en cours de préparation et prévoit d'apporter des évolutions importantes au langage. Bien qu'elle ne soit pas encore publiée, voici les principales évolutions qu'elle devrait spécifier :
- modèle alternatif et optionel de programmation par classes : types, classes et interfaces explicites, prévus pour faciliter et sécuriser le travail de développeurs ignorant ou mal à l'aise avec le modèle de programmation par prototype, ce nouveau modèle pourra être utilisé en mode strict ou mixte (permettant de ne pas remettre en cause l'existant) ;
- support des espaces de noms (Namespaces) et des paquetages (packages) ;
- nouveaux types : int, double, decimal, Class, Type ;
- possibilité de restreindre la portée des variables aux blocs et aux expressions ;
- possibilité de redéfinir les opérateurs associés à un nouveau type ;
- itérateurs, générateurs et initialisations de tableaux inspirés du langage Python ;
- nouveau système de gestion des virgules flottantes : pour renforcer les fonctionnalités mathématiques du langage.
Outils
Debugger, IDE, ...
- Firebug : extension Firefox pour le déboguage et l'analyse de code JavaSCript
- Venkman : déboguage JavaScript
- JSLint : vérification de syntaxe
- JavaScript Linker (JSL) : éditeur de liens (projet Dojo)
- JSDoc : génération automatique de documentation à partir du code JavaScript
- Spket IDE : plugin Eclipse pour le développement JavaScript
Liens utiles
- Normes ECMAScript :
- ECMAScript (ECMA-262) : http://www.ecma-international.org/publications/standards/Ecma-262.htm
- Composants ECMAScript (ECMA-290) : http://www.ecma-international.org/publications/standards/Ecma-290.htm
- E4X (ECMA-357) : http://www.ecma-international.org/publications/standards/Ecma-357.htm
