Commençons par une question simple :
<script type="text/javascript">
alerte(i); // ?
var je = 1;
</script>
Le résultat de sortie n'est pas défini. Ce phénomène est appelé « pré-analyse » : le moteur JavaScript analysera d'abord les variables var et les définitions de fonctions. Le code n’est exécuté qu’une fois la pré-analyse terminée. Si un flux de document contient plusieurs segments de code de script (code js séparé par des balises de script ou des fichiers js importés), l'ordre d'exécution est le suivant :
étape 1. Lire le premier segment de code.
étape 2. Effectuez une analyse syntaxique. S'il y a une erreur, une erreur de syntaxe sera signalée (telle que des crochets incompatibles, etc.) et passerez à l'étape 5.
étape 3. Effectuez une "pré-analyse" des définitions de variables et de fonctions var (aucune erreur ne sera jamais signalée, car seules les déclarations correctes sont analysées)
étape 4. Exécutez le segment de code et signalez une erreur s'il y a une erreur (par exemple, la variable n'est pas définie)
étape 5. S'il y a un autre segment de code, lisez le segment de code suivant et répétez l'étape 2.
étape 6. À la fin de l'analyse ci-dessus, elle a pu expliquer de nombreux problèmes, mais j'ai toujours l'impression qu'il manque quelque chose. Par exemple, à l'étape 3, qu'est-ce que la « pré-analyse » ? Et à l'étape 4, regardez l'exemple suivant :
<script type="text/javascript">
alert(i); // erreur : i n'est pas défini.
je = 1 ;
</script>
Pourquoi la première phrase provoque-t-elle une erreur ? En JavaScript, les variables n’ont-elles pas besoin d’être indéfinies ?
Le temps du processus de compilation est passé comme un cheval blanc, et j'ai ouvert les « Principes de compilation » à côté de la bibliothèque comme si c'était un monde à part. Il y avait cette note dans l'espace vide familier mais inconnu :
Pour les langages compilés traditionnels. , les étapes de compilation sont divisées en : analyse lexicale et analyse syntaxique, vérification sémantique, optimisation du code et génération d'octets.
Mais pour les langages interprétés, une fois l’arbre syntaxique obtenu grâce à l’analyse lexicale et à l’analyse syntaxique, l’interprétation et l’exécution peuvent commencer.
En termes simples, l'analyse lexicale consiste à convertir un flux de caractères (flux de caractères) en un flux de jetons (flux de jetons), par exemple en convertissant c = a - b en :
NOM "c"
;
ÉGAL
NOM "un"
MOINS
NOM "b"
Point-virgule
Les exemples ci-dessus ne sont que des exemples. Pour plus d'informations, veuillez consulter l'analyse lexicale
du chapitre 2 du « Guide définitif de JavaScript » qui parle de la structure lexicale, qui est également décrite dans ECMA-262. La structure lexicale est la base d’une langue et est facile à maîtriser. Quant à la mise en œuvre de l’analyse lexicale, c’est un autre domaine de recherche qui ne sera pas exploré ici.
Nous pouvons utiliser l'analogie avec le langage naturel. L'analyse lexicale est une traduction difficile un à un. Par exemple, si un paragraphe de l'anglais est traduit mot par mot en chinois, nous obtenons un ensemble de flux symboliques, ce qui est difficile. comprendre. Une traduction ultérieure nécessite une analyse grammaticale. La figure suivante est un arbre syntaxique d'une instruction conditionnelle :
Lors de la construction de l'arbre de syntaxe, s'il s'avère qu'il ne peut pas être construit, comme if(a { i = 2; }, une erreur de syntaxe sera signalée et l'analyse de l'intégralité du bloc de code se terminera. Il s'agit de l'étape 2 à Au début de cet article.
Grâce à l'analyse syntaxique, construire Après l'arbre syntaxique, la phrase traduite peut encore être ambiguë et une vérification sémantique supplémentaire est nécessaire. Pour les langages traditionnels fortement typés, la partie principale de la vérification sémantique est la vérification de type, comme le. paramètres réels des fonctions et si les types de paramètres formels correspondent. Pour les langages faiblement typés, cette étape peut ne pas être disponible (j'ai une énergie limitée et je n'ai pas le temps d'examiner l'implémentation du moteur JS, donc je ne suis pas sûr qu'il existe un étape de vérification sémantique dans le moteur JS)
. Il s'avère que pour les moteurs JavaScript, il doit y avoir une analyse lexicale et une analyse syntaxique, puis il peut y avoir des étapes telles que la vérification sémantique et l'optimisation du code une fois ces étapes de compilation terminées (n'importe quel langage l'a fait). un processus de compilation, mais les langages interprétés ne sont pas compilés en code binaire), le code commencera à s'exécuter.
Le processus de compilation ci-dessus ne peut toujours pas expliquer la "pré-analyse" au début de l'article. processus du code JavaScript.
Zhou Aimin a déclaré dans « L'essence du langage JavaScript ». La deuxième partie de « Pratique de programmation » en fait une analyse très minutieuse. Voici quelques-unes de mes idées :
Grâce à la compilation, le code JavaScript a été. traduit en un arbre syntaxique, puis exécuté immédiatement selon l'arbre syntaxique,
ce qui nécessite une exécution ultérieure. Comprendre le mécanisme de portée de JavaScript. JavaScript utilise la portée lexicale. En termes simples, la portée des variables JavaScript est déterminée lorsqu'elles sont définies. plutôt que du moment où ils sont exécutés. C'est-à-dire que la portée lexicale dépend du code source. Le compilateur peut la déterminer par analyse statique, donc la portée lexicale est également appelée portée statique. Cependant, il convient de noter que la sémantique de with. Et eval ne peut pas être réalisé uniquement par la technologie statique. En fait, nous ne pouvons parler que du mécanisme de portée de JS. Très proche de la portée lexicale.
Lorsque le moteur JS exécute chaque instance de fonction, il crée un contexte d'exécution. Objet d'appel. L'objet d'appel est une structure scriptObject utilisée pour enregistrer la table de variables interne. Les structures d'analyse de syntaxe telles que varDecls, la table de fonctions intégrée funDecls et la liste de références parent (remarque : les informations telles que varDecls et funDecls sont obtenues lors de l'exécution de l'objet d'appel. étape d'analyse syntaxique et sont enregistrés dans l'arborescence syntaxique. Lorsque l'instance de fonction est exécutée, ces informations seront copiées de l'arborescence syntaxique vers scriptObject). scriptObject est un système statique lié à la fonction, cohérent avec le cycle de vie de l'instance de fonction. .
La portée lexicale est le mécanisme de portée de JS, et vous devez également comprendre sa méthode d'implémentation. Il s'agit de la chaîne de portée. La chaîne de portée est un mécanisme de recherche de nom. Elle recherche d'abord le scriptObject dans l'environnement d'exécution actuel, s'il n'est pas trouvé, il suit la valeur ascendante jusqu'au scriptObject parent et recherche l'objet global.
Lorsqu'une instance de fonction est exécutée, une fermeture est créée ou associée à celle-ci. scriptObject est utilisé pour enregistrer statiquement les tables de variables liées aux fonctions, tandis que la fermeture enregistre dynamiquement ces tables de variables et leurs valeurs en cours d'exécution pendant l'exécution. Le cycle de vie d'une fermeture peut être plus long que celui d'une instance de fonction. L'instance de fonction sera automatiquement détruite une fois que la référence active sera vide, et la fermeture sera recyclée par le moteur JS une fois que la référence de données sera vide (dans certains cas, elle ne sera pas automatiquement recyclée, ce qui entraînera une fuite de mémoire).
Ne soyez pas intimidé par l'ensemble des noms ci-dessus. Une fois que vous avez compris les concepts d'environnement d'exécution, d'objet d'appel, de fermeture, de portée lexicale et de chaîne de portée, de nombreux phénomènes dans le langage JS peuvent être facilement résolus.
Résumé À ce stade, les questions du début de l'article peuvent être expliquées très clairement :
ce que l'on appelle la « pré-analyse » à l'étape 3 est en fait terminée lors de l'étape d'analyse syntaxique de l'étape 2 et stockée dans l'arborescence syntaxique. Lorsqu'une instance de fonction est exécutée, varDelcs et funcDecls seront copiés de l'arborescence syntaxique vers le scriptObject de l'environnement d'exécution.
À l'étape 4, les variables non définies signifient qu'elles ne peuvent pas être trouvées dans la table des variables de scriptObject. Le moteur JS recherchera vers le haut la valeur de scriptObject. Si aucune n'est trouvée, l'opération d'écriture i = 1 sera finalement équivalente à window. i = 1 ; ajoute un nouvel attribut à l'objet fenêtre. Pour les opérations de lecture, si le scriptObject remontant à l'environnement d'exécution global est introuvable, une erreur d'exécution se produit.
Après avoir compris, le brouillard s'est dissipé et les fleurs ont fleuri et le ciel est devenu clair.
Enfin, je vous laisse avec une question :
<script type="text/javascript">
var argument = 1 ;
fonction foo(arg) {
alerte(argument);
var argument = 2 ;
}
foto(3);
</script>
Quel est le résultat de l’alerte ?