Portée de Grokking en JavaScript

L'étendue, ou l'ensemble de règles qui déterminent l'emplacement de vos variables, est l'un des concepts les plus fondamentaux de tout langage de programmation. C'est tellement fondamental, en fait, qu'il est facile d'oublier à quel point les règles peuvent être subtiles!

Comprendre exactement comment le moteur JavaScript "pense" à propos de la portée vous empêchera d'écrire les bugs courants que le levage peut causer, vous prépare à bien comprendre les fermetures et vous rapproche encore plus de ne jamais écrire des bugs. déjà encore.

… Bon, ça vous aidera à comprendre le levage et les fermetures, de toute façon. 

Dans cet article, nous allons examiner:

  • les bases des portées en JavaScript
  • comment l'interprète décide quelles variables appartiennent à quelle portée
  • comment hisser vraiment travaux
  • comment les mots-clés ES6 laisser et const changer le jeu

Plongeons dedans.

Si vous souhaitez en savoir plus sur ES6 et comment utiliser la syntaxe et les fonctionnalités pour améliorer et simplifier votre code JavaScript, pourquoi ne pas consulter ces deux cours:

Portée lexicale

Si vous avez déjà écrit une ligne de JavaScript, vous saurez que où tu définir vos variables déterminent où vous pouvez utilisation leur. Le fait que la visibilité d’une variable dépend de la structure de votre code source s’appelle lexical portée.

Il existe trois façons de créer une portée en JavaScript:

  1. Créer une fonction. Les variables déclarées dans des fonctions ne sont visibles que dans cette fonction, y compris dans des fonctions imbriquées..
  2. Déclarer des variables avec laisser ou const à l'intérieur d'un bloc de code. De telles déclarations ne sont visibles qu'à l'intérieur du bloc.
  3. Créer un capture bloc. Croyez-le ou non, cela en fait Est-ce que créer une nouvelle portée!
"use strict"; var mr_global = "Mr Global"; fonction foo () var mrs_local = "Mrs Local"; console.log ("Je peux voir" + mr_global + "et" + mrs_local + "."); function bar () console.log ("Je peux aussi voir" + mr_global + "et" + mrs_local + ".");  foo (); // Fonctionne comme prévu try console.log ("Mais / je / ne peux pas voir" + mrs_local + ".");  catch (err) console.log ("Vous venez de recevoir un" + err + ".");  let foo = "toto"; const bar = "bar"; console.log ("je peux utiliser" + foo + bar + "dans son bloc ...");  try console.log ("Mais pas en dehors de ça.");  catch (err) console.log ("Vous venez de recevoir un autre" + err + ".");  // Lance ReferenceError! console.log ("Notez que" + err + "n'existe pas en dehors de 'catch'!") 

L'extrait ci-dessus illustre les trois mécanismes de portée. Vous pouvez l'exécuter dans Node ou Firefox, mais Chrome ne joue pas bien avec laisser, encore.

Nous allons parler de chacun d’eux dans les moindres détails. Commençons par un aperçu détaillé de la manière dont JavaScript détermine quelles variables appartiennent à quelle portée..

Le processus de compilation: une vue à vol d'oiseau

Lorsque vous exécutez un morceau de JavaScript, deux choses se passent pour le faire fonctionner.

  1. Tout d'abord, votre source est compilée.
  2. Ensuite, le code compilé est exécuté.

Pendant la compilation étape, le moteur JavaScript:

  1. prend note de tous vos noms de variables
  2. les enregistre dans le champ d'application approprié
  3. réserve un espace pour leurs valeurs

C'est seulement pendant exécution que le moteur JavaScript définit en réalité la valeur des références de variable égale à leurs valeurs d’assignation. Jusque là, ils sont indéfini

Étape 1: Compilation

// Je peux utiliser first_name n'importe où dans ce programme var first_name = "Peleke"; function popup (first_name) // Je ne peux utiliser que last_name à l'intérieur de cette fonction var last_name = "Sengstacke"; alerte (prénom + "+ nom +); popup (prénom);

Passons en revue ce que fait le compilateur.

Tout d'abord, il lit la ligne var first_name = "Peleke". Ensuite, il détermine quoi portée enregistrer la variable dans. Parce que nous sommes au plus haut niveau du script, il se rend compte que nous sommes dans le portée globale. Ensuite, il enregistre la variable Prénom au niveau global et initialise sa valeur à indéfini.

Deuxièmement, le compilateur lit la ligne avec fonction popup (prenom). Parce que le une fonction mot-clé est la première chose sur la ligne, il crée une nouvelle portée pour la fonction, enregistre la définition de la fonction dans la portée globale, et jette un coup d'œil à l'intérieur pour trouver des déclarations de variable.

Effectivement, le compilateur en trouve un. Depuis que nous avons var last_name = "Sengstacke" dans la première ligne de notre fonction, le compilateur enregistre la variable nom de famille au champ d'application apparaitre-ne pas à la portée globale - et définit sa valeur à indéfini

Comme il n'y a plus de déclaration de variable dans la fonction, le compilateur revient dans la portée globale. Et puisqu'il n'y a plus de déclarations de variables , cette phase est terminée.

Notez que nous n'avons pas réellement courir rien encore. À ce stade, le travail du compilateur consiste simplement à s'assurer qu'il connaît le nom de tout le monde. il s'en fiche quoi ils font. 

À ce stade, notre programme sait que:

  1. Il y a une variable appelée Prénom dans le cadre global.
  2. Il y a une fonction appelée apparaitre dans le cadre global.
  3. Il y a une variable appelée nom de famille dans le viseur de apparaitre.
  4. Les valeurs des deux Prénom et nom de famille sont indéfini.

Peu importe que nous ayons affecté ces valeurs de variables ailleurs dans notre code. Le moteur prend soin de cela dans exécution.

Étape 2: exécution

Au cours de l'étape suivante, le moteur lit à nouveau notre code, mais cette fois-ci, exécute il. 

Tout d'abord, il lit la ligne, var first_name = "Peleke". Pour ce faire, le moteur recherche la variable appelée Prénom. Étant donné que le compilateur a déjà enregistré une variable portant ce nom, le moteur la recherche et définit sa valeur sur "Peleke".

Ensuite, il lit la ligne, fonction popup (prenom). Puisque nous ne sommes pas l'exécution la fonction ici, le moteur n'est pas intéressé et saute dessus.

Enfin, il lit la ligne popup (prenom). Depuis que nous sont en exécutant une fonction ici, le moteur:

  1. cherche la valeur de apparaitre
  2. cherche la valeur de Prénom
  3. exécute apparaitre en tant que fonction, en passant la valeur de Prénom en paramètre

Quand il s'exécute apparaitre, il passe par ce même processus, mais cette fois à l'intérieur de la fonction apparaitre. Il:

  1. recherche la variable nommée nom de famille
  2. ensembles nom de familleLa valeur de est égale à "Sengstacke"
  3. lève les yeux alerte, l'exécuter comme une fonction avec "Peleke Sengstacke" comme paramètre

Il se trouve qu'il y a beaucoup plus de choses sous le capot qu'on aurait pu le penser!

Maintenant que vous comprenez comment JavaScript lit et exécute le code que vous écrivez, nous sommes prêts à aborder un problème un peu plus près de chez vous: comment fonctionne le levage.

Levée au microscope

Commençons avec du code.

bar(); function bar () if (! foo) alert (foo + "? C'est étrange…");  var foo = "bar";  cassé(); // Erreur-type! var cassé = function () alert ("Cette alerte ne s'affichera pas!"); 

Si vous exécutez ce code, vous remarquerez trois choses:

  1. Vous pouvez faire référence à foo avant de lui attribuer, mais sa valeur est indéfini.
  2. Vous pouvez appel cassé avant de le définir, mais vous aurez un Erreur-type.
  3. Vous pouvez appel bar avant de le définir, et cela fonctionne comme vous le souhaitez.

Levage fait référence au fait que JavaScript rend disponibles tous nos noms de variables déclarés partout dans leurs domaines, y compris avant nous leur assignons.

Les trois cas dans l'extrait sont les trois que vous devez connaître dans votre propre code. Nous allons donc les examiner un par un..

Déclarations variables de levage

Rappelez-vous, quand le compilateur JavaScript lit une ligne comme var foo = "bar", il:

  1. enregistre le nom foo à la portée la plus proche
  2. définit la valeur de foo à indéfini

La raison pour laquelle nous pouvons utiliser foo avant que nous lui attribuions est parce que, lorsque le moteur recherche la variable avec ce nom, il Est-ce que exister. C'est pourquoi il ne jette pas un ReferenceError

Au lieu de cela, il obtient la valeur indéfini, et essaie de l'utiliser pour faire tout ce que vous lui avez demandé. Habituellement, c'est un bug.

Gardant cela à l’esprit, nous pourrions imaginer que ce que JavaScript voit dans notre fonction bar est plus comme ça:

fonction bar () var foo; // undefined if (! foo) //! undefined est vrai, alors alerte alert (foo + "? C'est étrange…");  foo = "bar"; 

C'est le Première règle de levage, si vous voulez: des variables sont disponibles tout au long de leur portée, mais avoir la valeur indéfini jusqu'à ce que votre code leur assigne.

Un langage JavaScript courant consiste à écrire l’ensemble de vos var Déclarations en haut de leur portée, au lieu de leur première utilisation. Pour paraphraser Doug Crockford, cela aide votre code lis plus comme ça court.

Quand vous y réfléchissez, cela a du sens. C'est assez clair pourquoi bar se comporte comme il le fait lorsque nous écrivons notre code comme JavaScript le lit, n'est-ce pas? Alors pourquoi ne pas simplement écrire comme ça tout le temps?  

Fonctions de levage

Le fait que nous ayons un Erreur-type quand nous avons essayé d'exécuter cassé avant que nous définissions il est juste un cas spécial de la première règle de Hoisting.

Nous avons défini une variable, appelée cassé, que le compilateur enregistre dans la portée globale et définit égal à indéfini. Lorsque nous essayons de l'exécuter, le moteur recherche la valeur de cassé, trouve que c'est indéfini, et tente d'exécuter indéfini en tant que fonction.

Évidemment, indéfini n'est pas une fonction-c'est pourquoi nous obtenons un Erreur-type!

Déclarations de fonction de levage

Enfin, rappelons que nous avons pu appeler bar avant de le définir. Ceci est dû à la Deuxième règle de levage: Lorsque le compilateur JavaScript trouve une déclaration de fonction, il donne son nom et définition disponible en haut de son champ. Réécrire notre code encore une fois:

function bar () if (! foo) alert (foo + "? C'est étrange…");  var foo = "bar";  var cassé; // barre indéfinie (); // bar est déjà défini, exécute fine broken (); // Impossible d'exécuter undefined! broken = function () alert ("Cette alerte ne s'affichera pas!"); 

 Encore une fois, cela a beaucoup plus de sens quand vous écrire comme JavaScript lit, tu ne crois pas?

Réviser:

  1. Les noms des déclarations de variable et des expressions de fonction sont disponibles dans toute leur portée, mais leur valeurs sont indéfini jusqu'à la cession.
  2. Les noms et les définitions des déclarations de fonctions sont disponibles dans tout leur champ d'application, même avant leurs définitions.

Voyons maintenant deux nouveaux outils qui fonctionnent un peu différemment: laisser et const.

laisserconst, et la zone morte temporelle

contrairement à var déclarations, variables déclarées avec laisser et const ne pas se faire hisser par le compilateur.

Du moins pas exactement. 

Rappelez-vous comment nous avons pu appeler cassé, mais a obtenu Erreur-type parce que nous avons essayé d'exécuter indéfini? Si nous avions défini cassé avec laisser, nous aurions eu un ReferenceError, au lieu:

"use strict"; // Vous devez "utiliser strict" pour essayer ceci dans Node broken (); // ReferenceError! let broken = function () alert ("Cette alerte ne s'affichera pas!"); 

Lorsque le compilateur JavaScript enregistre les variables à leurs portées lors de sa première passe, il traite laisser et const différemment que ce qu'il fait var

Quand il trouve un var déclaration, nous enregistrons le nom de la variable dans sa portée et initialisons immédiatement sa valeur à indéfini.

Avec laisser, cependant, le compilateur Est-ce que enregistrer la variable à sa portée, mais ne fait pasinitialiser sa valeur à indéfini. Au lieu de cela, il laisse la variable non initialisée, jusqu'à ce que le moteur exécute votre instruction d'affectation. L'accès à la valeur d'une variable non initialisée jette un ReferenceError, ce qui explique pourquoi l'extrait ci-dessus jette lorsque nous l'exécutons.

L'espace entre le début de haut de la portée d'un laisser déclaration et la déclaration d'affectation est appelée la Zone morte temporelle. Le nom vient du fait que, même si le moteur sait à propos d'une variable appelée foo au sommet de la portée de bar, la variable est "morte", car elle n'a pas de valeur.

… Aussi parce que ça va tuer votre programme si vous essayez de l'utiliser tôt.

le const mot clé fonctionne de la même manière que laisser, avec deux différences principales:

  1. Vous doit attribuer une valeur lorsque vous déclarez avec const.
  2. Vous ne peux pas réaffecter des valeurs à une variable déclarée avec const.

Cela garantit que const volonté toujoursavoir la valeur que vous lui avez initialement attribuée.

// C'est légal const React = require ('react'); // Ceci n'est absolument pas légal. crypto = require ('crypto');

Portée du bloc

laisser et const sont différents de var d'une autre manière: la taille de leurs portées.

Lorsque vous déclarez une variable avec var, c'est visible aussi haut que possible dans la chaîne des portées - généralement, en haut de la déclaration de fonction la plus proche, ou dans la portée globale, si vous le déclarez au niveau supérieur. 

Lorsque vous déclarez une variable avec laisser ou const, cependant, il est visible en tant que localement comme possible-seulement dans le bloc le plus proche.

UNE bloc est une section de code définie par des accolades, comme vous le voyez avec si/autre des blocs, pour des boucles et des morceaux de code explicitement "bloqués", comme dans cet extrait.

"use strict"; let foo = "foo"; if (foo) const bar = "bar"; var foobar = foo + bar; console.log ("Je peux voir" + barre + "dans ce bloc.");  try console.log ("Je peux voir" + foo + "dans ce bloc, mais pas" + bar + ".");  catch (err) console.log ("Vous avez un" + err + ".");  essayer console.log (foo + bar); // Lance à cause de 'foo', mais les deux ne sont pas définis catch (err) console.log ("Vous venez de recevoir un" + err + ".");  console.log (foobar); // fonctionne bien

Si vous déclarez une variable avec const ou laisser à l'intérieur d'un bloc, c'est seulement visible à l'intérieur du bloc, et seulement après l'avoir assigné.

Une variable déclarée avec var, cependant, est visible aussi loin que possible-dans ce cas, dans la portée globale.

Si vous êtes intéressé par les détails de Nitty Gritty de laisser et const, Découvrez ce que le Dr Rauschmayer a à dire à leur sujet dans Exploring ES6: variables et périmètre, et consultez la documentation MDN qui s'y trouve..  

Lexical ce Fonctions de flèche

À la surface, ce ne semble pas avoir grand chose à voir avec la portée. Et, en fait, JavaScript ne ne pas résoudre le sens de ce selon les règles de portée dont nous avons parlé ici.

Du moins pas d'habitude. JavaScript, notoirement, ne ne pas résoudre le sens de la ce mot clé en fonction de l'endroit où vous l'avez utilisé:

var foo = nom: 'Foo', langues: ['espagnol', 'français', 'italien'], parle: fonction parle () this.languages.forEach (fonction (langue) console.log (this. nom + "parle" + langue + ".");); foo.speak ();

La plupart d'entre nous s'attendions à ce vouloir dire foo à l'intérieur de pour chaque boucle, parce que c'est ce que cela voulait dire juste en dehors de cela. En d'autres termes, nous nous attendons à ce que JavaScript résout le sens de ce lexicalement.

Mais ce n'est pas.

Au lieu de cela, il crée un Nouveau ce dans chaque fonction que vous définissez, et décide de ce que cela signifie en fonction de Comment vous appelez la fonction-not  vous l'avez défini.

Ce premier point est similaire au cas de la redéfinition tout variable dans une portée enfant:

fonction foo () var bar = "bar"; function baz () // La réutilisation de noms de variable comme ceci s'appelle "shadowing" var bar = "BAR"; console.log (barre); // BAR baz ();  foo (); // BAR

Remplacer bar avec ce, et le tout devrait éclaircir instantanément!

Traditionnellement, obtenir ce pour fonctionner comme nous nous attendons à ce que de vieilles variables simples à portée lexicale fonctionnent, vous devez utiliser l'une des deux solutions suivantes:

var foo = name: 'Foo', langues: ['espagnol', 'français', 'italien'], speak_self: function speak_s () var self = this; self.languages.forEach (fonction (langue) console.log (self.name + "parle" + langue + ".");), talk_bound: fonction speak_b () this.languages.forEach (fonction ) console.log (this.name + "parle" + langue + "."); .bind (foo)); // Plus communément: .bind (this); ;

Dans se parler, nous sauvons le sens de ce à la variable soi, et utilise cette variable pour obtenir la référence que nous voulons. Dans speak_bound, nous utilisons lier à en permanence point ce à un objet donné.

ES2015 nous apporte une nouvelle alternative: les fonctions de flèche.

Contrairement aux fonctions "normales", les fonctions de flèche font ne pas l'ombre de la portée de leurs parents ce valeur en définissant leur propre. Plutôt, ils résolvent sa signification lexicalement. 

En d'autres termes, si vous utilisez ce dans une fonction de flèche, JavaScript recherche sa valeur comme n'importe quelle autre variable.

Tout d'abord, il vérifie l'étendue locale d'un ce valeur. Puisque les fonctions de flèche n'en définissent pas un, il n'en trouvera pas. Ensuite, il vérifie la parent la possibilité d'un ce valeur. S'il en trouve un, il l'utilisera à la place.

Cela nous permet de réécrire le code ci-dessus comme ceci:

var foo = nom: 'Foo', langues: ['espagnol', 'français', 'italien'], parle: fonction speak () this.languages.forEach ((language) => console.log (this .name + "parle" + langue + "."););   

Si vous souhaitez plus de détails sur les fonctions de flèche, jetez un coup d'œil à l'excellent cours du formateur Dan Wellman sur les bases de JavaScript ES6 en JavaScript, ainsi qu'à la documentation MDN sur les fonctions de flèche..

Conclusion

Nous avons couvert beaucoup de terrain jusqu'à présent! Dans cet article, vous avez appris que:

  • Les variables sont enregistrées dans leurs champs lors de compilation, et associés à leurs valeurs d'affectation au cours exécution.
  • En référence aux variables déclarées aveclaisser ou const avant l'affectation jette un ReferenceError, et que ces variables sont étendues au bloc le plus proche.
  • Fonctions de flèchenous permettent de réaliser la liaison lexicale de ce, et contourner la liaison dynamique traditionnelle.

Vous avez également vu les deux règles du levage:

  • le Première règle de levage: Cette expressions de fonction et var les déclarations sont disponibles dans toutes les portées où elles sont définies, mais ont la valeur indéfini jusqu'à ce que vos instructions d'affectation soient exécutées.
  • le Deuxième règle de levage: Que les noms des déclarations de fonction et leurs corps sont disponibles à travers les portées où ils sont définis.

Une bonne étape suivante consiste à utiliser votre connaissance nouvelle des domaines de JavaScript pour vous envelopper des fermetures. Pour cela, consultez Scopes & Closures de Kyle Simpson.

Enfin, il y a beaucoup plus à dire sur ce que j'ai pu couvrir ici. Si le mot-clé vous semble toujours être une magie noire, jetez un coup d'œil à ceci et aux prototypes d'objet pour mieux vous comprendre..

En attendant, prenez ce que vous avez appris et écrivez moins de bugs!

Apprendre JavaScript: le guide complet

Nous avons créé un guide complet pour vous aider à apprendre le JavaScript, que vous soyez débutant en tant que développeur Web ou que vous souhaitiez explorer des sujets plus avancés..