Les prototypes sont la base de la POO avec Flash, même en AS2. C'est pourquoi j'ai considéré qu'il pouvait être sympa de faire une explication détaillée de leur fonctionnement, dans un didacticiel rédigé pour MediaBox. En fait, l'idée de ce tutoriel m'est venue il y a un petit moment deja, quand dans un éclair, j'ai fini par comprendre pourquoi la méthode d'héritage préconisée par Macromedia faisait effectivement de l'héritage .... :D

Auteur : LAlex
Mail : lalex@flash-forum.net
URL : http://www.flash-forum.net
Date de création : 26/02/2004
Version : Flash 5 / MX / MX 2004

Pré requis
:arrow: Connaître les principes de la POO
:arrow: Pour appliquer dans Flash MX 2004 : De ActionScript 1 à ActionScript 2

Introduction

Le programmation orientée objet sous Flash est dite "prototype based", soit en français "basée sur les prototypes". Ce type de programmation est opposée à la programmation dite "class based", utilisée par la plupart des langages objets célèbres, tels que le C/C++/C# ou Java. Bien qu'essayant de simuler la même syntaxe que ces derniers langages, l'ActionScript 2 reste un langage de prototypes, transformé en ActionScript 1 lors de la compilation. C'est pourquoi il est également important de connaître les rouages de cette mécanique même si l'on utilise la dernière mouture de Flash.

Bien que limitant quelques possibilités très avancées de la POO, les prototypes sont également un outil fantastique, permettant une programmation plus souple, et une compréhension plus facile du fonctionnement interne de votre code. Nous allons ici faire le tour des spécificités de la programmation à base de prototypes, et plus particulièrement celle de Flash.

1. D'ou vient le prototype ?

Nous partirons du constat que toute fonction à un prototype, qui est créé en même temps que la fonction. Le prototype est un objet, contenant des propriétés et des méthodes. En fait, il n'est pas nécessaire d'en savoir plus sur ce point. D'ailleurs, l'aide de Flash nous donne peu d'informations sur ce qu'est le prototype, et se contente de dire Dans une fonction constructeur ActionScript 1, la propriété prototype fait référence à un objet qui est le prototype de la classe construite.Nous parlons ici de fonction constructeur, c'est à dire qui va servir à instancier un nouvel objet. Or, toute fonction peut être utilisée comme constructeur. ActionScript 2 fonctionne de la même manière, et les équivalences avec AS1 sont décrites dans le tutorial "D'ActionScript 1 à ActionScript 2".

Lorsque l'on crée une fonction, on peut considérer que Flash crée automatiquement un prototype associé, de type Object :

// Création d'une fonction
myFunction = function() {
   trace("myFunction a été appellée");
}

// Instruction implicite
// myFunction.prototype = {}
2. L'opérateur new() Elément de base de la POO, l'opérateur new() sert à créer un nouvel objet à partir d'une fonction constructeur. Son fonctionnement consiste à attribuer le prototype de la fonction constructeur à un objet nouvellement crée. Examinons les différentes étapes de la création de cet objet. - L'opérateur new() crée un nouvel objet - Il initialise la propriété système "__proto__" de ce nouvel objet avec le prototype de la fonction constructeur - Il initialise la propriété système "__constructor__" de ce nouvel objet avec la fonction constructeur - Il exécute la fonction sur l'objet nouvellement crée Nous pouvons reproduire le fonctionnement de new() de la manière suivante : // Déclaration du constructeur
function MaClasse(arg) {
   trace("Instance de MaClasse créée avec le paramètre : '" + arg + "'");
}

// Instruction new "classique"
var monInstance = new MaClasse("Argument");
// Même instruction décomposée
// On crée un nouvel objet
var monInstance = new Object();
// On lui attribue le prototype de la fonction
monInstance.__proto__ = MaClasse.prototype;
// On lui attribue le constructeur
monInstance.__constructor__ = MaClasse;
// On applique la fonction constructeur à l'objet
MaClasse.call(monInstance, "Argument");
3. __proto__ et la chaîne de prototypes
Nous avons vu précédemment que la construction d'un objet consiste à mettre un prototype dans sa propriété __proto__. Il s'agit maintenant de comprendre pourquoi l'on peut accéder aux méthodes et propriétés d'un objet, alors qu'elles sont créées dans le prototype. Nous allons prendre l'exemple d'un appel de méthode. En fait, lorsque l'on accède à la propriété d'un objet, Flash va chercher tout d'abord si cette méthode existe dans l'objet lui-même. Si c'est le cas, c'est cette méthode qui va être utilisée. Dans le cas contraire, Flash va rechercher dans la propriété __proto__ de l'objet si la méthode est présente, et l'utiliser s'il la trouve. S'il ne la trouve toujours pas, étant donné que __proto__ est un objet, il contient lui-même une propriété __proto__, dans laquelle Flash va continuer sa recherche, et ainsi de suite, jusqu'à ne plus avoir de __proto__ dans lequel rechercher. Voici une illustration du fonctionnement : // Classe "maison"
function MaClasse (arg) {
   trace("Instance créée avec l'argument '" + arg + "'");
}

// Ajout d'une méthode dans le prototype
MaClasse.prototype.innerDoIt = function(arg) {
        trace("innerDoIt exécuté");
}
// Création d'une instance
monInstance = new MaClasse("Argument");
// Création d'une fonction dans l'instance
monInstance.doIt = function() {
        trace("doIt exécuté");
}
// Appel des différentes méthodes
monInstance.doIt();
monInstance.innerDoIt();
trace(monInstance.toString());
Instance créée avec l'argument 'Argument' doIt exécuté innerDoIt exécuté [object Object] Ce code a en fait exécuté les méthodes suivantes : - Constructeur - monInstance.doIt(); - monInstance.__proto__.innerDoIt(); - monInstance.__proto__.__proto__.toString() (méthode toString() de Object) Cette succession de recherche s'appelle "parcourir la chaîne de prototypes". La fin du parcours se fait lorsque la méthode est trouvée, ou lorsque la chaîne se termine. Nous reviendrons plus tard sur la raison pour laquelle la chaîne se termine.

4. L'héritage

Il existe deux méthodes pour implémenter l'héritage en ActionScript 1. L'instruction 'extends' d'AS2 est un mixage des deux, assez bizarre il faut bien le dire ... Pour rappel, l'héritage permet de profiter des fonctionnalités d'une classe mère dans une classe fille qui en hérite. Pour plus d'informations, rendez-vous sur le tutorial concernant les principes de la POO.
La méthode officielle de Macromedia
Elle consiste à faire du prototype de la classe fille une instance de la classe mère. Les instances de cette classe fille possèderont donc le prototype de la classe mère dans leur chaîne de prototypes. // Classe mère<br />
Parent = function() {<br />
      trace("Nouvelle instance de Parent");<br />
}<br />
// Classe fille<br />
Child = function() {<br />
       trace("Nouvelle instance de Child");<br />
}<br />
// La classe fille hérite de la classe mère<br />
Child.prototype = new Parent()<br />
// Instanciation de la classe fille<br />
monInstance = new Child();
Pourquoi cette instruction permet-elle de faire de l'héritage ? Penchons nous en détail dessus, en se servant de ce que nous avons vu ci-dessus concernant l'instruction new() ... Si l'on utilise la notation "équivalente" de new, nous obtiendrions : // Classe mere<br />
Parent = function() {<br />
   trace("Nouvelle instance de Parent");<br />
}<br />
// Classe fille<br />
Child = function() {<br />
   trace("Nouvelle instance de Child");<br />
}<br />
// "Traduction" de Child.prototype = new Parent()<br />
Child.prototype = new Object();<br />
Child.prototype.__proto__ = Parent.prototype;<br />
Child.prototype.__constructor__ = Parent;<br />
Parent.call(Child.prototype);<br />
// "Traduction" de monInstance = new Child()<br />
monInstance = new Object();<br />
monInstance.__proto__ = Child.prototype;<br />
monInstance.__constructor__ = Child;<br />
Child.call(monInstance);
Nous avons donc dans monInstance.__proto__ = Child.prototype, et plus haut, nous avions Child.prototype.__proto__ = Parent.prototype ... Donc, on peut en déduire que monInstance.__proto__.__proto__ = Parent.prototype ! 8) Lorsque l'on va accéder à une méthode de monInstance, conformément au parcours de la chaîne de prototypes, Flash va d'abord chercher dans monInstance, puis dans monInstance.__proto__ (prototype de Child), puis dans monInstance.__proto__.__proto__ (prototype de Parent). Nous avons donc bien de l'héritage !

La méthode plus "propre".
Dans la "traduction" ci-dessus, on peut se rendre compte que toutes les lignes ne sont pas utiles. En effet, si l'on regarde la sortie générée par ce code, on peut voir Nouvelle instance de Parent Nouvelle instance de ChildEn effet, le constructeur de Parent a été appelé, alors qu'il n'est absolument pas nécessaire. Afin d'éviter cela, une méthode d'héritage préférable consiste à utiliser les lignes de traduction, mais uniquement celles qui sont nécessaires. L'instanciation de monInstance peut être refaite par l'opérateur new(), étant donné que le principe reste le même que ci dessus : // Classe mere<br />
Parent = function() {<br />
   trace("Nouvelle instance de Parent");<br />
}<br />
// Classe fille<br />
Child = function() {<br />
   trace("Nouvelle instance de Child");<br />
}<br />
// Héritage<br />
// ** Child.prototype = new Object();<br />
// Cette ligne n'est pas nécessaire, étant donné que<br />
// le prototype de Child est crée automatiquement à la<br />
// création de la fonction<br />
Child.prototype.__proto__ = Parent.prototype;<br />
Child.prototype.__constructor__ = Parent;<br />
// ** Parent.call(Child.prototype);<br />
// Cette ligne n'est pas nécessaire : il est inutile<br />
// d'exécuter le constructeur de la classe mère<br />
// "Traduction" de monInstance = new Child()<br />
monInstance = new Child()
Ainsi, les fonctionnalités de l'héritage sont préservées, sans qu'aucune instruction inutile ne soit executée. :idea: Les spécifications du format SWF v7 donnent l'implémentation de l'instruction extends en ActionScript 2: Subclass.prototype = new Object();<br />
Subclass.prototype.__proto__ = Superclass.prototype;<br />
Subclass.prototype.__constructor__ = Superclass;
Cette implémentation a fait débat, concernant le fait qu'en executant la première ligne, à priori inutile, elle détruit le prototype actuel, détruisant ainsi le prototype courant, et la propriété "constructor" de celui-ci ... Vous pouvez avoir plus d'informations sur la discussion en français qui a eu lieu sur le devblog de LAlex.

5. Pourquoi parle-t-on de "prototypes de MovieClip" ? Lorsque l'on parle de prototypes de MovieClip comme les petits utilitaires permettant de faire un mouvement élastique ou autre, il s'agit d'un abus de langage, issu des débuts de la programmation Flash durant lesquels le vocabulaire propre à la POO n'était pas des plus répandus chez les programmeur utilisant cette technologie. En réalité, pour être exact, faire un "prototype de MovieClip" signifie "rajouter une méthode au prototype de MovieClip". Etant donné que tous les clips de Flash on un __proto__ qui pointe vers MovieClip.prototype, ajouter une méthode à ce prototype revient à ajouter une méthode a chaque clip !!!

6. La cas particulier de Object
Nous parlions plus haut de parcourir la chaîne de prototypes jusqu'à ce qu'il n'y ai plus de prototype à parcourir. La question est de savoir à quel moment s'arrête cette chaîne. Le cas particulier est en fait la classe Object. Elle a pour particularité le fait que son prototype n'a pas de constructeur. Elle possède bien un prototype, comme toute fonction constructeur, c'est pourquoi il est possible d'utiliser new Object(). Par contre ce prototype n'est issu d'aucune classe, ce qui fait que le __proto__ de Object.prototype n'existe pas. C'est comme si l'instruction implicite (vue au début de ce tutoriel) à la création de la fonction "Object" n'avait pas été exécutée. En effet, cela se vérifie facilement : // Classe interne<br />
trace("== MovieClip ==");<br />
trace(MovieClip.prototype);<br />
trace(MovieClip.prototype.__proto__);<br />
// Classe "maison"<br />
MaClasse = function() {}<br />
trace("== MaClasse ==");<br />
trace(MaClasse.prototype);<br />
trace(MaClasse.prototype.__proto__);<br />
// Classe Object<br />
trace("== Object ==");<br />
trace(Object.prototye);<br />
trace(Object.prototype.__proto__);

== MovieClip==
[object Object]
[object Object]

== MaClasse ==
[object Object]
[object Object]

== Object ==
[object Object]
undefined

C'est la raison pour laquelle la remontée de la chaîne de prototypes s'arrête à un moment donné, lorsqu'elle rencontre un __proto__ qui n'existe pas. Cette constatation nous amène aussi au fait que toutes les classes héritent forcément de Object, étant donné que leurs prototypes sont soit de type Object, soit héritent eux-mêmes de Object. C'est d'ailleurs pour ca que toutes les classes, personnelles ou intégrées à Flash, possèdent une méthode "toString"

Conclusion

Une fois les prototypes compris, se balader dans les chaînes de prototypes devient beaucoup plus clair, et l'on arrive ainsi à une compréhension accrue du fonctionnement de son code, ce qui simplifie évidemment son élaboration ainsi que son debugage.
Les prototypes peuvent également poser des problématiques inédites, véritable gymnastique de l'esprit. Les manipuler peut amener à des fonctionnalités très pointues, directement au coeur du code, traitant les classes elles-mêmes comme l'on traite des objets "normaux".