JavaScript, un langage compilé ? - JS - Sous Le Capot - Partie 1

Malgré le fait que le Javascript soit considéré dans l’imaginaire collectif comme un langage dynamique... Javascript est en fait un langage compilé !
Arthur.jpg
Arthur COMBE, JavaScript loverMis à jour le 10 Août 2020
javascript langage compilé

Bienvenue dans notre série d’articles : Javascript : sous le capot ! Le but de cette série est d’en apprendre un peu plus sur Javascript et de comprendre comment ça fonctionne réellement ! On démarre par un débat souvent animé : Javascript, un langage compilé ou interprété ?

1 - Javascript, un langage compilé !

Malgré le fait que le Javascript soit considéré dans l’imaginaire collectif comme un langage dynamique... Javascript est en fait un langage compilé !

Effectivement, Javascript n’est pas compilé autant à l’avance que des langages plus classiques comme le Java ou le .NET par exemple. Mais, il n’en reste pas moins que le moteur JS performe quasiment les mêmes étapes (si ce n’est plus) que lors d’une compilation d’un langage compilé classique.
Le plus grand enjeu de la compilation du Javascript vient du fait qu’elle se fait immédiatement avant l’exécution.
Elle n’a pas la chance d’avoir tout le temps qu’il lui faut pour build un exécutable type jar ou dll, qui lui permettrait d’optimiser tout au maximum.

La compilation JavaScript dans les grandes lignes

  1. Tokenizing (Tokenisation) : décomposer les chaines de caractères en des morceaux, appelés tokens, qui ont un sens pour le compilateur. Prenons le programme suivant : var a = 1; Lors de cette étape, les tokens suivants seront générés : var, a, =, 1, ;.
  2. Parsing (analyse) : transformer le tableau de tokens généré précédemment en un arbre d’éléments imbriqués, il représente la structure du programme. Il est appelé AST (Abstract Syntax Tree).
  3. Code-Generation : prend l’AST et le transformer en code exécutable.

Décomposition du code JS

A partir de maintenant, nous allons parler de 3 acteurs différents :

  1. Moteur : il s’occupe de tout, de la compilation à l’exécution de notre programme JS.
  2. Compilateur : il s’occupe de la compilation du programme.
  3. Scope : collecte et garde une liste des variables déclarées.

La discussion JS

Quand on voit le code var a = 1;, on pense le plus souvent qu’il y a ici une seule action. Seulement, ce n’est pas comme ça que le Moteur voit les choses ! En effet, il voit 1 action effectuée au moment de la compilation, et 1 autre, au moment de l’exécution. (En dehors de la tokenisation)

Essayons de décomposer ce code :

  • A l’encontre de var a, le Compilateur demande au Scope de regarder si la variable a existe déjà dans le scope en question.
    Si oui, le Compilateur ne fait rien et continue.
    Si non, le Compilateur demande à Scope de déclarer la variable a dans le scope en question.
  • Le Compilateur génère le code pour a = 2 qui sera exécuté par le Moteur plus tard : Le Moteur demandera au Scope si a existe.
    Si oui, il utilisera cette variable.
    Si non, une erreur sera renvoyée.
    Pour résumer, 2 actions peuvent être distinguées lors de l’assignation d’une variable : lors de la compilation, le Compilateur déclare la variable (si elle n’existe pas déjà), et dans un deuxième temps, lors de l’exécution, le Moteur demande la variable à Scope, et lui assigne la valeur.

L’exécution de Javascript

Pour rentrer encore plus en profondeur dans la compréhension, nous allons devoir introduire de nouveaux termes : LHS et RHS, Left-Hand Side et Right-Hand Side.
Quand le Moteur doit exécuter le code généré par le Compilateur, il va rechercher la variable a pour voir si elle est déclarée, et pour cela, il va demander au Scope.
Mais, il existe plusieurs types de recherche, et le résultat dépendra du type.
Dans notre cas précédent, c’était une recherche LHS de la variable a.
Pour simplifier la compréhension, RHS peut être également vu comme «Retrieve His Source», dans le sens où on veut la valeur de la variable en question.
On peut donc voir LHS comme « le reste », comme récupérer le conteneur de la variable en question au lieu de son contenu.

Quand on écrit :

console.log(a);

C’est donc une recherche RHS, puisqu’on veut connaitre la valeur de a pour la passer à console.log

Tandis qu’au contraire quand on a :

a = 2;

C’est une recherche LHS, car nous ne sommes pas intéressés par la valeur de a. On veut simplement son conteneur pour lui assigner la valeur 2.

Le scope

On disait jusqu’à maintenant que le Scope gardait une liste des variables déclarées. Seulement, il peut y avoir plusieurs Scopes.
De la même manière que les fonctions, les Scopes sont imbriqués les uns dans les autres. Si une variable ne peut pas être trouvée dans le scope immédiat, le Moteur consulte le scope du « dessus ». Tant qu’il ne trouve pas la variable, il continue à remonter jusque tout en haut (scope dit « global »).

Imaginons le programme suivant :

function foo(a) {
    console.log(a + b);
}

var b = 2;

foo(2); // 4

La RHS de b ne peut pas être résolue dans le Scope de foo, mais il peut l’être dans le Scope du dessus (en l’occurrence ici le Scope global).
Le Moteur va tout d’abord demander au Scope de foo, pour une RHS de b.
Le Scope va lui dire qu’il ne l’a pas, le Moteur va donc demander au Scope du dessus qui lui, va lui renvoyer la valeur souhaitée.

Et les fonctions dans tout ça ?

Il est tentant de penser qu’une déclaration de fonction est une LHS. En effet, on pourrait imaginer décomposer la déclaration de function foo(a) {} en var foo puis foo = function(a){}.
Cependant, ce n’est pas le cas. Pour des soucis de performance, les déclarations et assignations de fonctions sont faites au moment de la compilation. On ne peut donc pas vraiment parler de recherche LHS.

Et les erreurs ?

En fonction du type de recherche, le comportement sera différent dans le cas où elle échoue.

function foo(a) {
    console.log(a + b);
    b = a;
}
foo(2);

Quand la RHS est fait sur b, rien ne sera trouvé car la variable n’existe pas. Si une RHS échoue à trouver une variable, le résultat sera une erreur ReferenceError retourné par le Moteur.

A contrario, si une LHS échoue, et si le programme n’est pas en « Strict Mode », Scope va s’occuper de créer un conteneur pour la variable en question, et le retourner au Moteur.
Strict Mode a été ajouté dans ES5, et permet de rajouter des règles au JS qui ne sont pas présentes de base. L’une d’elle empêchant la création automatique de variable implicite. Dans ce cas-là, une LHS qui échoue renverra une ReferenceError.

Exemple :

Prenons un exemple un peu complet que nous pouvons décomposer pour voir toutes les recherches LHS et RHS qui sont faites (n’hésitez pas à chercher par vous-même avant de voir les réponses !).

function foo(a) {
    var b = a;
    return a + b;
}
var c = foo(2);

Il y a ici 3 LHS et 4 RHS.

LHS :

  • c = .. (Récupération du conteneur c)
  • a = 2 (Assignement implicite au paramètre de la fonction foo)
  • b = .. (Récupération du conteneur b)

RHS :

  • foo(2) (Récupération de la valeur de c)
  • .. = a (Récupération de la valeur de a)
  • a + .. (Récupération de la valeur de a)
  • .. + b (Récupération de la valeur de b)

Compilation Javascript - Pour aller plus loin

Cette première partie de notre série Javascript sous le capot est maintenant terminée ! Pour aller plus loin, voici les articles suivants :