View this page in english

Aug 03 2011

Tout ce que vous avez toujours voulu savoir sur prototype sans jamais l’avoir cherché

Category: JavaScript,Node.js

Sauf mention contraire, tous les exemples de code donnés par la suite sont interprétés par V8 JavaScript Engine via NodeJS.

La propriété prototype

Tous les objets Javascript ont des méthodes et des propriétés et comme les fonctions sont des objets à part entière elles ont aussi leurs propres propriétés et méthodes :
> function foo(parameter){return !!parameter;}
> foo.length
1
> foo.constructor
[Function: Function]
> foo.toString()
function foo(parameter){return !!parameter;}
  
Les fonctions, et elles seules, possèdent nativement une propriété au fonctionnement aussi particulier qu'important : la propriété prototype. Nous allons détaillé dans cet article son fonctionnement ainsi que les bonnes pratiques à respecter lors de sa manipulation.

Quand est utilisée la propriété prototype ?

On peut affecter n'importe quelle valeur à la propriété prototype d'une fonction foo, cela n'aura strictement aucun effet lors d'un appel simple de la fonction par foo(), il en va tout autrement lors d'un appel par new foo() :
> function foo(parameter){return !!parameter;}
> foo.prototype.bar = "I'm foo.prototype.bar"
'I\'m foo.prototype.bar'
> foo().bar
> new foo().bar
'I\'m foo.prototype.bar'
  
Étudions donc les différences entre un appel de type foo(), dit appel procédural et new foo(), dit appel comme constructeur.

Différences entre foo() et new foo()

  • Cas d'un appel procédural à une fonction (e.g. foo())
    • la valeur retournée est celle retournée par le return présent dans le corps de la fonction elle-même (undefined si la fonction ne retourne rien) ;
    • dans le corps de la fonction, le mot clef this représente l'espace dans lequel le code s'exécute (le scope ; l'espace global étant window dans un navigateur, global dans NodeJS.
      Dans Firebug 
      >>> function foo(){return this === window;}
      undefined
      >>> foo()
      true


      Dans NodeJS 
      > function foo(){return this === global;}
      > foo()
      true


      Plus généralement :
      > var scope = {
      ... ini: function() {
      ... return this === scope;
      ... }
      ... };
      >
      > scope.ini();
      true
    • Comme nous l'avons déjà vu, la propriété prototype n'est pas utilisée dans le cas d'un appel procédural.
  • Cas d'un appel à une fonction comme un constructeur (e.g. new foo())
    • la valeur retournée est toujours un objet quelque soit la valeur retournée par la fonction elle-même ;
      > function foo(){return 1;}
      > new foo()
      {}
                  
    • dans le corps de la fonction, le mot clef this représente l'objet qui sera effectivement retourné :
      > function foo(){this.bar = 1;}
      > a = new foo()
      { bar: 1 }
      > typeof bar
      'undefined'
      > a.bar
      1
                  
    • tout objet retourné par un constructeur, via l'opérateur new donc, garde une référence cachée à la propriété prototype du constructeur. Cette référence cachée permet d'accéder au prototype du constructeur depuis l'objet lui-même.
      1. > function foo(){}
      2. > foo.prototype.aProperty = "I'm foo.prototype.aProperty"
      3. 'I\'m foo.prototype.aProperty'
      4. > foo.prototype.aMethod = function(){return "I'm foo.prototype.aMethod";}
      5. [Function]
      6. > a = new foo()
      7. {}
      8. > a.aProperty
      9. 'I\'m foo.prototype.aProperty'
      10. > a.aMethod()
      11. 'I\'m foo.prototype.aMethod'
      12. > foo.prototype.aMethod = function(){return "I have been modified";}
      13. [Function]
      14. > a.aMethod()
      15. 'I have been modified'
Cette dernière séquence de commandes impose plusieurs remarques.
  • on voit, ligne 6 et 7, que « a » est un objet vide, pourtant, ligne 8, on peut accéder à l'attribut a.aProperty défini dans le prototype du constructeur foo. Cela est dû au fait que, de façon interne, Javascript cherche dans l'objet lui même puis, s'il ne trouve pas ce qui lui est demandé, il cherche dans le prototype du constructeur. La preuve par l'exemple (clin d'œil aux logiciens) :
    > function foo(){this.name = "I'm this.name";}
    > a = new foo()
    { name: 'I\'m this.name' }
    > foo.prototype.name = "I'm foo.prototype.name"
    'I\'m foo.prototype.name'
    > a.name
    'I\'m this.name'
    > delete(a.name)
    true
    > a.name
    'I\'m foo.prototype.name'
    > a.name = "I'm a.name"
    'I\'m a.name'
    > a.name
    'I\'m a.name'
    > delete(a.name)
    true
    > a.name
    'I\'m foo.prototype.name'
    > delete(a.name)
    true
    > a.name
    'I\'m foo.prototype.name'
    > delete(a.constructor.prototype.name)
    true
    > a.name
    >
            
    Édifiant, n'est-il pas ?
    Dans le cas où la propriété, name en l'occurrence, appartient à l'objet lui-même on dit que c'est une propriété propre à l'objet. La méthode hasOwnProperty de tout objet permet de savoir si une propriété est propre à l'objet :
    > function foo(){this.name = "I'm this.name"}
    > foo.prototype.pName = "I'm foo.prototype.pName"
    'I\'m foo.prototype.pName'
    > a = new foo()
    { name: 'I\'m this.name' }
    > a.name
    'I\'m this.name'
    > a.pName
    'I\'m foo.prototype.pName'
    > a.hasOwnProperty('name')
    true
    > a.hasOwnProperty('pName')
    false
            
  • ligne 12, la modification de la méthode aMethod du prototype du constructeur (la fonction foo) se retrouve lors de l'appel de a.aMethod() alors que l'objet est déjà créé ; on pourrait en déduire hâtivement que a.aMethod pointe vers (est une référence à) a.consructor.prototype.aMethod, mais ce n'est pas le cas !
    > function foo(){}
    > foo.prototype.bar = 'bar'
    'bar'
    > a = new foo()
    {}
    > a.bar
    'bar'
    > a.constructor.prototype
    { bar: 'bar' }
    > a.constructor.prototype = null
    null
    > a.bar
    'bar'
            
    Le lien entre un objet et le prototype du constructeur n'est pas objet.constructor.prototype, c'est une référence caché nommé __proto__ :
    a.__proto__
    { bar: 'bar' }
    > a.__proto__ = {}
    {}
    > a.bar
    >
            
    Pas complètement convaincu ?
    > var a = {
    ...   sum: function () {
    ...     return this.x + this.y;
    ...   },
    ...   z: "Hello, I'm z"
    ... };
    >
    > var b = {
    ...   x: 10,
    ...   y: 20,
    ...   __proto__: a
    ... };
    >
    > b.z;
    'Hello, I\'m z'
    > b.sum();
    30
            
    ¡¡ Attention !!
    • __proto__ est une propriété de l'instance alors que prototype est une propriété du constructeur ;
    • la propriété __proto__ n'existe pas dans Internet Explorer et ne doit donc pas être utilisée dans vos développements WEB si vous souhaité que votre code soit compatible avec ce navigateur truc ;
    • les bonnes pratiques recommandent de ne pas utiliser la méthode __proto__ dans les scripts, sauf à de fins d'apprentissage.
Note : lorsque l'on définie une fonction qui est destinée à être utiliser comme un constructeur il est d'usage de commencer son nom par une majuscule. J'encourage vivement à respecter ce genre de recommandation et, personnellement, je m'efforce de respecter le style de codage préconisé par NodeJS.

Écrasement du prototype

Il faut prendre une précaution particulière lorsque l'on écrase le prototype d'un constructeur. En effet, dans le code suivant la valeur retournée par a.constructor === Foo est bien celle attendue :
> var Foo = function(){}
> Foo.prototype.sayHello = function(){console.log('Bonjour');}
[Function]
> var a = new Foo()
> a.constructor === Foo
true
    
Alors que dans le code suivant, où le prototype du constructeur est écrasée, la valeur de a.constructor === Foo n'est plus celle que l'on est en droit d'attendre :
> var Foo = function(){}
> Foo.prototype = {sayHello: function(){console.log('Bonjour');}}
{ sayHello: [Function] }
> var a = new Foo()
> a.constructor === Foo
false
    
Ainsi, lorsque l'on écrase le prototype d'un constructeur il faut aussi repositionner correctement la valeur du constructeur de ce prototype :
> var Foo = function(){}
> Foo.prototype = {sayHello: function(){console.log('Bonjour');}}
{ sayHello: [Function] }
> var a = new Foo()
> a.constructor === Foo
false
> Foo.prototype.constructor = Foo
[Function]
> a.constructor === Foo
true
    

Ce qu'il faut retenir

  • seules les fonctions possèdent nativement la propriété prototype, c'est un objet vide par défaut ;
  • la propriété prototype d'une fonction est utilisée seulement lors d'un appel avec l'opérateur new ;
  • tous les composants, méthodes et propriétés, de la propriété prototype d'un constructeur sont accessibles depuis tout objet instancié avec ce constructeur comme s'il s'agissait d'un composant lui étant propre, et ce, même si le composant est ajouté après la création de l'objet car prototype est partagé avec tous les objets instanciés par ce constructeur :
    > function Iam(name){this.name = name;}
    > a = new Iam('foo')
    { name: 'foo' }
    > b = new Iam('bar')
    { name: 'bar' }
    > a.name
    'foo'
    > b.name
    'bar'
    > Iam.prototype.shared = 'Shared with all the instances of Iam'
    'Shared with all the instances of Iam'
    > a.shared
    'Shared with all the instances of Iam'
    > b.shared
    'Shared with all the instances of Iam'
            
  • on peut ajouter des méthodes aux constructeurs natifs :
    > if (!String.reverse) {
    ...   String.prototype.reverse = function() {
    ...     return this.split('').reverse().join('');
    ...   };
    ... }
    > 'abc'.reverse()
    'cba'
            
  • on peut différentier une propriété propre d'un objet d'une propriété héritée du prototype grâce à la méthode hasOwnProperty de l'objet ;
  • si l'on écrase la propriété prototype d'un constructeur, il faut toujours repositionner la valeur de prototype.constructor du constructeur :
    > var Dog = function(){}
    > Dog.prototype = {paws: 4, hair: true}
    > Dog.prototype.constructor = Dog
    [Function]
            

Sources et compléments de lecture