Expression versus structure de contrôle en JavaScript

Cet article de blog se penche sur une distinction syntaxique qui est malheureusement assez importante en JavaScript : la différence entre les expressions et les structures de contrôle (aussi appelées, dans certains cas, déclarations).

Expression versus structure de contrôle en JavaScript

Expressions et structures de contrôle

Le JavaScript est composé d'une suite d'instruction que l'on distingue en deux groupes : les expressions et les structures de contrôlestatements »).

Les expressions

Une expression produit une valeur et peut être écrite partout où une valeur est attendue (par exemple en tant qu'argument dans un appel de fonction). Chacune des lignes suivantes contient une expression :

>  /**
    * Accès explicite à une propriété
    * de l'objet global inexistante
    */
   window.globalAttribute // Expression
<· undefined // Résultat de type `undefined`
>  /**
    * Affectation d'une valeur dans une
    * propriété implicite de l'objet global
    */
   globalAttribute = 7 // Expression
<· 7 // Résultat de type `number`
>  /**
    * Accès implicite à une propriété
    * de l'objet global
    */
   globalAttribute // Expression
<· 7 // Résultat de type `number`
>  /**
    * opération arithmétique avec
    * l'opérateur binaire d'addition
    */
   3 + globalAttribute // Expression
<· 10 // Résultat de type `number`
>  /**
    * Exécution d'une fonction
    * (méthode de l'objet global)
    */
   parseInt('14', 10) // Expression
<· 14 // Résultat de type `number`

Une expression est composée d'une suite d'opérandes et d'opérateurs qui produisent une valeur. Il existent des opérateurs unaires (+, ., ,, etc.), qui se placent en amont ou après une opérande, des opérateurs binaires (+, -, *, /, etc.) qui s'entercalent entre deux opérandes et même un opérateur ternaires (?—:) qui en se basant sur une première opérande, permet d'en retourner une parmi deux autres. Elles ont des rôles divers et variés tel que l'affectation, la comparaison, l'arithmétique, la logique, la condition, le relationnel, etc.

Les structures de contrôle

Une structure de contrôle réalise une action.

La structure de contrôle de base est une simple instruction qui contient une unique expression (« expression statement ») et qui a pour action de l'analyser. Elle est implicite quand aucune autre structure de contrôle est utilisée ou est explicite quand elle se termine par le caractère ;.

Les autres structures de contrôle produisent d'autres actions comme :

  • les structures conditionnelles (if, if–else if—else, switch, …) qui sélectionnent les instructions,
  • les structures itératives (while, do–while, for, …) qui bouclent sur des mêmes instructions,
  • les déclarations de fonction qui groupent des instructions pour un usage ultérieur,
  • les déclarations de variables/constantes qui stoquent des valeurs finales d'expression ou
  • les simples blocs qui isôlent les comportements d'autres structures ou permettent d'atribuer des étiquettes aux expressions.

Un programme est essentiellement une séquence de stucture de contrôle pilotant l'ordre dans lequel les expressions seront exécutées : on nomme cela le flux de contrôle. Partout où le JavaScript attend une structure de contrôle, vous pouvez écrire à la place une expression (elle sera prise en charge par la structure de contrôle d'analyse d'expression). L'inverse n'est cependant pas possible : vous ne pouvez pas écrire une structure de contrôle là où le JavaScript attend obligatoirement une expression. Par exemple, une structure conditionnelle if, ne peut pas devenir l'argument d'une fonction.

Contrôle de flux

La différence entre les structures de contrôle et les expressions devient plus claire si nous examinons la syntaxe des deux catégories pour produire un flux de contrôle similaire. Effectivement, les opérateurs des expressions permettent également de contrôler le flux de donnée.

if—else versus ?—:

Voici l'exemple de la structure de contrôle conditionnelle if—else :

>  /**
    * Structure de contrôle conditionnelle `if—else`
    */
   if ('0' === 0) {
      'évalué à vrai' // Expression
   } else {
      'évalué à faux' // Expression
   }
<· "évalué à faux" // Résultat de type `string`, retourne la valeur de la dernière expression exécutée par le contrôle de flux

dont une équivalence sous forme d'expression, appelée opérateur ternaire conditionnel donne :

>  /**
    * Expression avec opérateur ternaire conditionnelle
    */
>  '0' === 0 ? 'évalué à vrai' : 'évalué à faux' // Expression
<· "évalué à faux" // Résultat de type `string`

Point-virgule versus virgule

En JavaScript, la structure de contrôle d'analyse d'expression utilise le point-virgule pour explicitement indiquer la fin de l'expression courante. Il sépare ainsi l'analyse de deux expressions.

>  /**
    * Structure de contrôle d'analyse
    * d'expression explicite suivi d'une
    * analyse d'expression implicite
    */
>  parseInt('4', 10); parseInt('voiture', 10)
<· NaN // Résultat de type `number`, retourne la valeur de la dernière expression exécutée par le contrôle de flux

dont une équivalence est l'opérateur binaire évaluant en une seule expression chaque partie de part et d'autre du caractère , et ne retourne que la valeur de la dernière partie :

>  /**
    * Expression avec opérateur binaire virgule
    */
>  parseInt('4', 10), parseInt('voiture', 10)
<· NaN // Résultat de type `number`, retourne la valeur de la dernière partie

Syntaxe identique

Certaines expressions ressemblent à des structures de contrôle alors qu'en réalité elles n'en sont pas. Nous examinerons pourquoi cela pose problème à la fin de cette section.

Objets littéraux versus blocs

Le code suivant, s'il est interprété à un endroit ou seule une expression est attendue, est un objet littéral qui est une expression parfaitement légale :

>  /**
    * Expression de déclaration d'un
    * objet littéral
    */
   (   // Une expression est attendue après cette position
       {
           expected: parseInt('3', 10)
       }
   )
<· {expected: 3} // Résultat de type `object`

Cependant, c'est aussi une structure de contrôle parfaitement légale :

>  /**
    * Contrôle de flux de type bloc
    */
   {   // Une structure de contrôle est attendue après cette position
       {
           unexpected: parseInt('3', 10)
       }
   }
<· 3 // Résultat de type `number`, le label est ignoré

avec ces éléments :

  • un bloc, qui est une structure de contrôle avec des accolades ;
  • une étiquette, que vous pouvez placer devant n'importe quelle structure de contrôle. Ici, l'étiquette est unexpected ;
  • une structure de contrôle d'analyse d'expression : ici l'appel de fonction parseInt('3', 10).

C'est cette ambuiguité d'usage des parenthèses à une place ou structure de contrôle ou expression sont autorisées qui peut mener à des résultats contre intuitifs tel que :

>  /**
    * Expression de création de tableau
    * avec opérateur binaire de concaténation
    * sur une création d'objet
    */
>  [] + {}
<· "[object Object]" // Résultat de type `string`

mais

>  /**
    * Structure de contrôle de type bloc
    * puis expression avec opérateur du plus
    * unaire de coercition sur une création de
    * tableau
    */
>  {} + []
<· 0 // Résultat de type `number`

Pour bien comprendre les deux exemples précédents, voici ce qu'il se produit.

Dans le cas de [] + {}, la totalité de la ligne est traitée comme une expression. Nous avons donc ici à faire à deux objets (typeof [] retourne object et typeof {} retourne object) : le premier est un tableau ([] instanceof Array retourne true) et le second est un simple objet (({}) instanceof Array retourne false). Dans cet exemple, l'expression est traitée comme suit : opérande opérateur opérande. Or l'opérateur de concaténation binaire + réclame de part et d'autre un type string. Le moteur JavaScript va donc faire appel au mécanisme de coercition de type implicite pour transformer le tableau en string (l'équivalent de [] de type object en string est la chaine de caractères vide "") et l'objet en string (l'équivalent de {} de type object en string est la chaine de caractères "[object Object]"). Cela est donc équivalent à demander "" + "[object Object]" aboutissant à la valeur de retour "[object Object]".

Dans le cas de {} + [], on a ici la partie {} qui est traitée comme une structure de contrôle de type bloc qui ne possède ni étiquette, ni instruction : cela ne produira rien. Puis vient la partie + [] qui est traitée comme l'expression opérateur opérande. Or l'opérateur de coercition unaire + réclame à sa droite une valeur de type number. Le moteur JavaScript va donc faire appel au mécanisme de coercition de type implicite pour transformer le tableau en number (l'équivalent de [] de type object en number est la valeur numérique 0). Cela est donc équivalent à demander + 0 aboutissant à la valeur de retour 0.

Le JavaScript a des stuctures de contrôle de type bloc. Vous serez peut-être surpris d'apprendre que le JavaScript comporte des blocs qui peuvent exister seuls (par opposition aux structures de boucle ou de condition). Les codes suivants illustrent des cas d'utilisation de ces blocs.

Vous pouvez leur donner une étiquette et les séparer :

>  /**
    * Structure de contrôle de
    * type déclaration de fonction
    * avec définition utilisant une
    * étiquette
    */
   function test(printTwo) {
       printing: {
           console.log('One');
           if (!printTwo) break printing;
           console.log('Two');
       }
       console.log('Three');
    }
<· undefined // Résultat de type `undefined`

que l'on peut utiliser comme suit

>  test(false)
<  One
<  Three
<· undefined // Résultat de type `undefined`

ou comme suit

>  test(true)
<  One
<  Two
<  Three
<· undefined // Résultat de type `undefined`

Vous pouvez également isoler la portée d'une variable :

>  /**
    * Structure de contrôle de
    * type déclaration de fonction
    * avec définition utilisant une
    * étiquette
    */
   {
       var external = 'external'
       let internal = 'internal'
   }

   external // Retourne `'external'`
   internal // Error
<· « ReferenceError : internal n'est pas défini »

Déclaration de fonction versus expression de fonction

Quand une fonction est déclarée dans un endroit ou une structule de contrôle est attendue, la déclaration de cette fonction est elle-même une structure de contrôle. Elle ne retourne donc aucune valeur et est ajoutée à son objet d'activation lors de la phase d'entrée dans la fonction parent. Il en résulte donc que lors de l'excécution de sa fonction parente, une fonction déclarée plus tard peut-être exécutée avant.

>  /**
    * Expression de type
    * exécution de fonction
    */
   predestination() // Aucune erreur car cette fonction existe bien dans l'objet des variables

   /**
    * Structure de contrôle de
    * type déclaration de fonction
    */
   function predestination() {}
<· undefined // Résultat de type `undefined`

Cependant, quand une expression de fonction est utilisée dans un endroit ou une expression est attendue, celle-ci se retourne elle-même comme valeur, prête à être exécutée.

>  /**
    * Expression de type
    * déclaration de fonction
    */
   (   // Une expression est attendue après cette position
       function () {}
   )
<· ƒ () {} // Résultat de type `function`, valeur retournée par l'opérateur de groupement de l'expression

Aussi on peut exécuté le code immédiatement puisqu'une valeur est retournée

>  (
       function () {}
   )() // Opérateur d'exécution de fonction
<· undefined // Résultat de type `undefined`, valeur retournée par la fonction exécutée de l'expression

Il est également possible de créer des expressions de fonction nommée en attribuant un nom facultatif qui pourra servir en interne seulement :

>  var external = function internal(x) {
       // L'expression de fonction 
       // est accessible via son nom `internal`
       return x <= 1 ? 1 : x * internal(x - 1) 
   }

   // L'expression de fonction est
   // accessible via sa variable `external`
   external(10)

   // L'expression de fonction n'est pas
   // accessible via son nom `internal`
   internal(10)
<· « ReferenceError : internal n'est pas défini »

La syntaxe d'une expression de fonction nommée (une expression) est indicernable d'une déclaration de fonction (une structure de contrôle). Mais leurs effets sont différents : une expression de fonction produit une valeur (la fonction elle-même). Une déclaration de fonction conduit à une action, la création d'une variable stockée dans l'objet d'activation dont la valeur est la fonction elle-même. En outre, seule une expression de fonction peut être immédiatement invoquée, mais pas une déclaration de fonction.

L'ambuïguité des { }

Nous avons vu que certaines expressions sont indicernables des structures de contrôle. Cela signifie que le même code fonctionne différemment selon qu'il apparaît dans un contexte d'expression ou dans un contexte de structure de contrôle. Normalement, les deux contextes sont clairement séparés.

Cependant, dans le cas des structures de contrôle d'analyse d'expression (« expression statements »), il y a un chevauchement puisque les expressions apparaissent là où une structure de contrôle est également possible. Afin d'éviter toute ambiguïtée, la grammaire JavaScript interdit aux expressions de commencer par une accolade ou par le mot-clé function là où une structure de contrôle est possible :

Pseudo-code

ExpressionStatement : // Là où une structure de contrôle est possible...
    [lookahead ∉ {"{", "function"}] Expression ; // ...une expression ne peut pas commencer par `{` ou `function`

Alors que faire si vous voulez écrire une expression qui commence par l'un de ces deux caractères à une telle position ? Vous pouvez utiliser l'opérateur de groupement ( ), ce qui ne change pas son résultat, mais garantit qu'il apparaît dans un contexte ou seule les expressions sont autorisées. Examinons deux exemples : les codes d'évaluation intégrée eval et les expressions de fonction immédiatement invoquée.

Note : cette règle s'applique uniquement pour "function" mais pas pour "{" dans la console JavaScript des navigateurs dans le contexte global.

>  {} + {}
<· "[object Object][object Object]" // Résultat de type `string`

mais

>  eval('{} + {}')
<· NaN // Résultat de type `number`

Codes d'évaluation intégrée

eval analyse son argument dans un contexte de structure de contrôle sans ambuguïté : il analyse donc prioritairement le code commençant par { ou function comme une structure de contrôle de type bloc ou déclaration de fonction. Si vous voulez que eval renvoi un objet, vous devez utiliser (par exemple) l'opérateur de groupement autour d'un objet littéral.

>  /**
    * Évaluer comme la structure
    * de contrôle de type bloc
    * avec le label `example`
    */
   eval("{ example: 123 }")
<· 123 // Résultat de type `number`

mais

>  /**
    * Évaluer comme une
    * expression contenant
    * un objet littéral
    */
   eval('({ example: 123 })')
<· {example: 123} // Résultat de type `object`

D'autres opérateurs peuvent forcer le moteur JavaScript à lever l'ambuïguité entre expression et structure de contrôle. Voyez plus loin.

Expressions de fonction immédiatement invoquée

Nous avons vu plus avant que les expressions de fonction pouvaient être invoquées directement. Entrons un peu plus dans les détails ici.

>  /**
    * Expression de fonction
    * immédiatement invoquée
    */
   (function () { return 'abc' }())
<· "abc" // Résultat de type `string`

Si vous omettez les parenthèses, vous obtenez une erreur de syntaxe :

>  /**
    * Déclaration de fonction
    * sans nom (erreur)
    */
   function () { return 'abc' }()
<· « Uncaught SyntaxError : les déclarations de fonction nécessitent un nom de fonction »

Si vous ajoutez un nom, vous obtenez également une erreur de syntaxe :

>  /**
    * Déclaration de fonction
    * immédiatement invoquée (erreur)
    */
   function foo() { return 'abc' }()
<· « Uncaught SyntaxError : caractère ')' non attendu »

Une autre façon de garantir qu'une expression est analysée dans un contexte d'expression est d'utiliser un opérateur unaire tel que + ou ! qui force l'analyse en tant qu'expression. Mais, contrairement aux parenthèses, ces opérateurs modifient la valeur retournée par l'expression (ce qui n'est pas grave, si vous n'en avez pas besoin) :

>  /**
    * Expression de fonction
    * immédiatement invoquée avec
    * opérateur `+` unaire
    */
   +function () { return 'Hello' }()
<· NaN // Résultat de type `number`

NaN est la coercition de l'utilisation du + unaire sur une string qui ne représente aucun nombre (ici 'Hello'). Un autre opérateur utilisable pourrait également être l'opérateur void, qui force l'expression à renvoyer la valeur undefined dans tous les cas.

>  /**
    * Expression de fonction
    * immédiatement invoquée avec
    * opérateur `void`
    */
   void function () { return 'Hello' }()
<· undefined // Résultat de type `undefined`

Vous trouverez plus d'explications sur les expressions de fonction immédiatement invoquée dans ce billet sur les fonctions.

Concaténation

Soyez méfiant avec l'utilisation des opérateurs de groupement ( ) et les expressions de fonction. Regardez cette exemple avec deux expressions de fonction immédiatement invoquée.

>  /**
    * Plusieurs expressions de fonction
    * immédiatement invoquée
    */
   (function () {}())
   (function () {}())
<· « TypeError: `undefined` n'est pas une fonction »

Ce code produit une erreur, car le moteur JavaScript interprète la seconde ligne comme une tentative d'exécution de la valeur retournée par la première ligne (qui est une fonction) avec un opérateur () d'exécution de fonction à qui on aurait passé en argument une expression de fonction.

La solution consiste alors à ajouter un point-virgule pour séparer les deux expressions :

>  /**
    * Plusieurs expressions de fonction
    * immédiatement invoquée
    */
   (function () {}());
   (function () {}())
<· undefined // Résultat de type `undefined`

Une autre solution consiste à utiliser des opérateurs qui lève l'anbuguïté comme , ou void :

>  /**
    * Plusieurs expressions de fonction
    * immédiatement invoquée
    */
   void function () {}()
   void function () {}()
<· undefined // Résultat de type `undefined`

Dans ce cas, le JavaScript se comporte « comme si » un point-virgule avait implicitement été utilisé car void n'est pas un élément valide pour une structure de contrôle et commence nécessairement une expression.

>  /**
    * Expression de plusieurs fonctions
    * immédiatement invoquées
    */
   0,
   function () {}(),
   function () {}()
<· undefined // Résultat de type `undefined`

Notez également que 0 peut être remplacé par n'importe quel opérande valide (null, undefined, 'Ici commence mes exécutions de fonctions', etc.)

Mot de la fin

J'espère que cet article vous aura aidé à comprendre quelque comportement mal compris de JavaScript et que cela sera plus clair pour vous. N'hésitez pas à partager vos techniques pour lever les ambuguïtés d'interprétation entre expression et structure de contrôle en commentaire !

Cet article s'inspire de ce billet anglais qui aura servi de plan pour exposer les différences entre structures de contrôle et expressions. Tout en étant plus étoffé, le présent article n'en est pas à proprement une traduction.

Lire dans une autre langue