mardi 11 février 2014

Javascript et la création d'objets - Part I

C'est désormais connu depuis longtemps, Javascript est un langage à objets qui ne sont ni typés ni construits via des classes. Ce langage n'utilise pas le concept de classe comme modèle d'objets, mais celui d'objet comme modèle d'autres objets (on verra ce que cela signifie exactement plus tard). Ce qui est encore moins connu c'est le modèle d'objets que Javascript autorise à manipuler. Commençons donc par les choses simples : la création d'objets. Nous reviendrons sur l'héritage plus tard.

Javascript autorise différentes constructions permettant d'obtenir un objet de son choix, chacune des techniques ayant ses avantages et inconvénients. Avant toute chose précisons que les objets Javascript n'ont pas de structure statiquement déterminée; un objet est un agrégat dynamique. C'est particulièrement intéressant, car si quelque chose manque à un objet, il suffit de le rajouter opportunément; si quelque chose n'est pas ce qui était attendu on peut le changer; et si quelque chose est en trop on peut aussi le supprimer! Dans le cas de l'écriture d'un prototype c'est particulièrement agréable mais surtout très efficace. Ainsi donc, nous allons voir que la création d'un objet consiste d'abord à créer un objet vide puis à le décorer de ce qu'on l'on considère comme nécessaire.

Première façon de créer un objet.


Le plus simple est de partir d'un objet vide :

monObjet = {}

puis de lui ajouter les attributs dont on a besoin (ici un entier et une méthode) :

monObjet.valeur = 2
monObjet.getValeur = function () {
  return this.valeur
}
document.write(monObjet.getValeur())
document.write(monObjet.attributInconnu)

Attention : la syntaxe o.a, utilisée comme lvalue, permet d'ajouter, s'il n'existe pas, l'attribut a à l'objet désigné par o, puis de modifier sa valeur.

Note : dans le cas de l'utilisation en tant que rvalue, la valeur d'un attribut inexistant est undefined du type undefined (voir le second affichage).

Il est tout à fait possible d'obtenir un objet vide par appel à Object.create(null), mais on reviendra sur Object.create dans un autre article.

Note : en Javascript on ne devrait pas parler d'attribut mais de propriété (property). À une propriété peut-être associée une valeur primitive ou un objet. Ainsi une méthode Javascript n'est qu'une propriété dont la valeur est typée function (et oui, les fonctions sont des objets, on y reviendra aussi plus tard - voir les foncteurs C++).

Seconde technique.

Ici aussi, en partant d'un objet vide :

monObjet = {}
monObjet['valeur'] = 2
monObjet['getValeur'] = function () {
  return this.valeur
}
 

La construction est équivalente à la première. La syntaxe o['a'] est strictement équivalente à o.a. La (grande) différence est que, cette fois, la valeur de l'attribut est obtenue par utilisation d'une chaîne de caractères comme clé d'accès dans l'objet à la valeur associée; l'objet étant vu comme tableau associatif (l'API Cocoa d'Apple nomme cette construction le KVC, Key-Value Coding). Cette caractéristique est essentielle, car l'attribut auquel on accède n'est plus statiquement déterminé par un symbole du code mais dynamiquement déterminé par la valeur d'une variable. Comme par exemple dans :

att = "valeur"
...
monObjet[att]

on peut évidemment envisage créer une fonction permettant d'accéder à l'attribut d'un objet via une fonction paramétrée par le nom de cet attribut :

function getAttribut(o,n) {
  return o[n]
}

Troisième essai.


monObjet = {
  valeur: 2,
  getValeur: function () {
    return this.valeur
  }
}
document.write(monObjet.getValeur())

Ici l'objet est vu comme une structure créée de façon ad-hoc, on a donc affaire à une syntaxe légère pour l'obtention d'un singleton.

Quatrième idiome.


function Modele() {
  this.valeur = 2
  this.getValeur = function () {
    return this.valeur
  }
}
monObjet = new Modele()
document.write(monObjet.getValeur())

Ici on utilise une construction qui imite maladroitement la construction d'objet des langages orientés objets comme Java ou C++. L'instruction new est particulière, mais ce qu'il suffit de savoir ici est qu'elle procède en plusieurs étapes :

  1. elle créé un objet frais, vide et de type object
  2. elle met à jour le chaînage du prototype de l'objet nouvellement créé (ce point sera l'objet d'un article à venir)
  3. elle appelle la fonction de construction indiquée (avec paramètres effectifs éventuels); fonction dans laquelle l'objet nouvellement créé est manipulé via le symbole this
  4. elle renvoie l'objet fraîchement créé, à moins que la fonction ne décide de renvoyer une autre valeur (non primitive)
Encore une fois, l'objet créé est vide puis agrémenté de ce qui est nécessaire mais cette fois de l'intérieur par utilisation d'un constructeur et du mot-clé this. Le terme constructeur est assez adéquat puisqu'on construit effectivement l'objet brique par brique; alors qu'en Java ou C++ ce que l'on appelle constructeur est en réalité un initialiseur (une fonction d'initalisation).
Le point 4 est particulièrement intéressant car il permet d'obtenir un singleton via l'opérateur new :

function Singleton() {
  if (!Singleton.s) {
    Singleton.s = {
      valeur : 2,
      getValeur : function () {
        return this.valeur
      }
    }
  }
  return Singleton.s
}
o1 = new Singleton()
o2 = new Singleton()
document.write(o1.getValeur()+" "+o2.getValeur())
o1.valeur = 345
document.write(o1.getValeur()+" "+o2.getValeur())


Cette construction n'est pas sans rappeler la possibilité offerte par la surcharge de l'opérateur new en C++ ou encore les idiomes de construction d'objets en Objective-C (où il est autorisé de modifier self!). En Java, pour obtenir un singleton, il est nécessaire de cacher les constructeurs et de fournir une usine (factory); il est impossible en Java d'obtenir un singleton via l'opérateur new (sauf pour quelques classes internes très particulières - comme String).

Cinquième construction.


Là, on utilise simplement ce que nous venons de voir pour créer un objet par copie d'un objet original :

function clone(o) {
  var n = {}
  for (var p in o) {
    n[p] = o[p]
  }
  return n
}
o2 = clone(o1)

La fonction clone se contente de créer un objet vide, de parcourir l'ensemble des propriétés de l'objet passé en paramètre (les objets sont munis d'un itérateur sur l'ensemble des clés de leurs propriétés) et de créer pour chaque propriété originelle, une propriété de même nom et de même valeur dans l'objet frais. On notera, chose intéressante, que cette fonction peut-être appelée au choix brutalement clone(o) ou via l'opérateur new clone(o), ce qui était déjà le cas du Singleton précédent).

DjiBee

9 commentaires:

  1. Il est aussi possible d’avoir des getters & setters sur les objets.

    RépondreSupprimer
    Réponses
    1. Ca fera un sujet à part plus tard, en fait je souhaitais me concentrer sur l'héritage en JS mais je me suis rendu compte que les constructions ne sont pas toujours claires, alors comme ça a pris de la place...

      Supprimer
    2. Allez voir du côté de Douglas Crockford concernant votre fonction `clone`, qui ici n’est pas vraiment de l’héritage, on la réécrirait plutôt comme ça :

      function clone(o) {
      var F = function(){};
      F.prototype = o;
      return new F();
      }

      Supprimer
    3. Non car pour l'instant j'ai laissé le prototypage de côté. Le prototypage est plein de pièges...

      Supprimer
  2. Ce commentaire a été supprimé par l'auteur.

    RépondreSupprimer
  3. Il semblerait qu'il y a ait 2 coquilles dans le code du 4ème idiome.

    => Utiliser : à la place de = dans l'object literal

    Singleton.s = {
    valeur : 2,
    getValeur : function () {
    return this.valeur
    }
    }


    => Il faut inverser les deux lignes car l'objet monObjet n'existe pas encore.

    document.write(monObjet.getValeur())
    monObjet = new Modele()



    En prime, quelques remarques hors sujet :
    - toujours déclarer ses variables avec le mot clé var
    - utiliser la directive "use strict;"
    - penser au point virgule
    - pour l'exemple du singleton, la ligne document.write(o1 === o2); vous confirmera bien que les deux objets ont les mêmes valeur et le même type
    - ...

    Ne pas oublier que ce n'est pas parce que le javascript est permissif qu'il faut tout se permettre, bien au contraire.

    RépondreSupprimer
    Réponses
    1. Coquilles corrigées. pour le reste c'est-à-dire var, use strict, ;, c'est de la décoration sémantique qui ne sert pas le propos ici. Pour ===, je préfère éviter aussi ici, car la sémantique des opérateurs de test est un peu particulière et je souhaite rester dans le pas trop confus...

      Supprimer
  4. Aussi, utilisez console.log au lieu de document.write

    RépondreSupprimer
    Réponses
    1. le document.write se justifie pour des raisons de compatibilité navigateur même si des hacks existent pour palier l'absence de cette fonction

      Supprimer