Article JavaScript

Un article de WikiRaph.

Jump to: navigation, search

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