Nouvelles Chroniques d'Amethyste

Penser au Sens, pas au Verbe

Les expressions de fonction immédiatement invoquées en JavaScript

Poster un commentaire

Il y a quelques temps je suis tombé sur un bout de code Javascript qui m’a laissé un peu perplexe. On pourrait le résumer par:

(function() { …})();

Je n’ai jamais rencontré cette écriture auparavant.

La seule chose à faire dans ce cas c’est d’enquêter. J’ai récupéré les spécification EcmaScript et farfouillé un peu partout dans Google.

Je suis tombé assez vite sur deux articles intéressants qui ont bien débroussaillés le terrain:

Named function expressions demystified

Immediatly Invoked Function Expression

Vous pouvez effectivement aller directement à la fin de l’article. Mais c’est dommage, vous perdez l’occasion d’apprendre quelque chose.

Voici donc le résultat de mes recherches.

Première chose, en JavaScript nommer une fonction n’a rien d’obligatoire. Un peu comme les fonctions anonymes de C#. Le nom d’une fonction ne sert qu’à avoir un nom descriptif dans un débogueur.

Expression de fonction contre déclaration de fonction

La norme EcmaScript distingue en particulier deux moyens de créer une fonction: les expressions de fonction et les déclarations de fonction. La différence entre les deux est plutôt subtile, mais se clarifie facilement.

Retenez ceci: les déclarations de fonction ont toujours un identifiant (un nom de fonction) tandis que celui-ci est facultatif pour les expression de fonction:

Déclaration de fonction:
function Identifiant ( FormalParameterList opt ){ FunctionBody }

Expression de fonction:
function Identifiant opt ( FormalParameterList opt ){ FunctionBody }

On peut voir qu’en l’absence de l’identifiant, ce qui reste ne peut être qu’une expression. Mais quid lorsque l’identifiant est là? Comment peut t’on savoir si on a affaire à une expression de fonction ou bien une déclaration de fonction? Après tout il n’y a pas de grandes différences.

La différence est faite par le contexte. Si un function foo(){} se retrouve dans une affectation, on dira qu’il s’agit d’une expression de fonction. D’un autre côté si function foo(){} se retrouve dans le corps d’une fonction ou bien au niveau supérieur d’un programme, on parlera de déclaration de fonction.

  function foo(){} // declaration, car c’est une partie d’un programme
  var bar = function foo(){}; // expression, car on à affaire à une expression d’affectation

  new function bar(){}; // expression, car c’est une partie d’une déclaration new

  (function(){
    function bar(){} // declaration, car on est dans le corps d’une fonction
  })();

Un exemple plus mystérieux d’expression de fonction est celui où l’expression est encadrée de parenthèses: (function foo(){}).

Cette fois encore le contexte indique que l’on a affaire à une expression puisque ( et ) sont des opérateurs de regroupement. Un opérateur de regroupement ne peut contenir que des expressions.

Voici des exemples:

  function foo(){} // déclaration de fonction
  (function foo(){}); // expression de fonction expression (opérateur de regroupement)
  
  try {
               (var x = 5); // un opérateur d’expression ne peut contenir QUE des expression, pas de déclaration ce que le mot clef var est 
  } catch(err) {
    // Erreur de syntaxe
  }

On peut aussi se souvenir que lorsque l’on évalue une expression Json avec eval, la chaîne est en général encadrée par des parenthèses: eval('(' + json + ')').

On fait bien sûr cela pour les mêmes raisons: les parenthèses agissent comme un opérateur de regroupement ce qui force les accolades Json à être interprétées comme une expression plutôt qu’un bloc:

  try {
             { « x »: 5 }; // « { » et »} » sont analysés comme des blocs
  } catch(err) {
    // SyntaxError
  }
 
  ({ « x »: 5 }); // l’opérateur de regroupement force « { » et « } » à être analysé comme un littéral d’objet.

Il y a une différence subtile de comportement entre déclaration et expression.

Tout d’abord les déclarations de fonction sont analysées et évaluées avant toutes autres expression. Même si une déclaration est en dernière position dans le source, elle sera analysée avant la première expression contenue dans la même portée.

L’exemple suivant montre comment la fonction fn est exécutée, même si elle est déclarée plus loin:

  alert(fn());

  function fn() {
    return ‘Hello world!’;
  }

Par contre ceci ne fonctionne pas:

alert(foo()); // aaargh, foo pas encore chargé
var foo = function() { ‘Hello world!’ } // expression de fonction

Un autre point important est que déclarer conditionnellement une fonction n’est pas standard et le comportement peut dépendre du navigateur. On ne doit donc pas se fier à une fonction déclarée conditionnellement, mieux vaut utiliser une expression:

// Ne jamais faire
  // Certains navigateurs vont déclarer ‘foo’ comme la première déclaration et retourner ‘first’
  // par contre d’autres vont retourner ‘second’

  if (true) {
    function foo() {
      return ‘first’;
    }
  }
  else {
    function foo() {
      return ‘second’;
    }
  }
  foo();

  // il est préférable d’utiliser une expression
  var foo;
  if (true) {
    foo = function() {
      return ‘first’;
    };
  }
  else {
    foo = function() {
      return ‘second’;
    };
  }
  foo();

Immediatly Invoked Function Expression (IIFE)

On prononce « Aille-Fi » !

Cette expression a été introduite par Ben Alman en remplacement d’un terme moins correct: self executing anonymous function.

Vous trouverez un lien vers son article au tout début de celui-ci.

Vous savez qu’il est possible de lancer une fonction en invoquant son nom suivit de ():

function toto() {alert(‘toto’);};

toto();

On pourrait être tenté d’en faire de même avec une expression de fonction:

function(){ alert('toto'); }();

Ca ne marche pas.

Lorsque l’analyseur Javascript rencontre le mot clef function dans la portée globale (Global scope), il le traite comme une déclaration de fonction.
Seulement, si vous avez lu ce qui précède vous savez qu’une déclaration de fonction DOIT avoir un identifiant. D’où l’erreur d’exécution.

Alors on pourrait tenter ceci:

function toto(){ alert('toto'); }();

Cela ne fonctionne pas non plus. La raison est que les parenthèses placées APRES une déclaration sont considérées comme totalement séparées de la déclaration et sont analysés comme de simples opérateurs de regroupement. Le code précédent est en fait la même chose que ceci:

function toto(){ alert('toto'); };

();

La première ligne ne pose pas de problème, c’est la deuxième qui est incorrecte.

On peut en savoir plus en lisant ceci:

ECMA-262-3 in detail. Chapter 5. Functions

Comment s’en sortir? Comment faire pour invoquer notre fonction?

Le pattern standard consiste à placer l’expression elle-même entre parenthèses parce que en Javascript, les parenthèses ne peuvent contenir de déclaration.

Et on obtient ceci qui fonctionne bien:

(function (){ alert(‘toto’); })();

Certains auteurs (comme Crockford dans son livre: Javascript, the good parts) recommandent aussi ceci:

(function (){ alert(‘toto’); }() );

Les parenthèses ont pour objet d’indiquer à l’analyseur Javascript que l’on a affaire à une expression et non pas une déclaration. Dans certains cas c’est évident et du coup on peut se passer de parenthèses:

var i = function(){ return 10; }();  // définition d’une expression de fonction

true && function(){ /* code */ }();

Il existe d’autres patterns, pas forcément moins bons, mais je ne suis pas certains de leur lisibilité. Alors j’ai fais mon choix. Lisez l’article de Ben Alman qui est très clair.

Bref pour revenir au sujet de cet article, j’ai fait pour la première fois de ma vie la rencontre du pattern IIFE.

Publicités

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s