La portée et le contexte de JavaScript sont uniques au langage, en partie grâce à la flexibilité qu'ils apportent. Chaque fonction a un contexte et une portée variables différents. Ces concepts sous-tendent de puissants modèles de conception en JavaScript. Cependant, cela apporte également une grande confusion aux développeurs. Ce qui suit révèle de manière exhaustive les différences entre le contexte et la portée en JavaScript, ainsi que la manière dont les différents modèles de conception les utilisent.
contexte vs portée
La première chose qu’il convient de clarifier est que le contexte et la portée sont des concepts différents. Au fil des années, j'ai remarqué que de nombreux développeurs confondent souvent ces deux termes, décrivant à tort l'un comme l'autre. Pour être honnête, ces termes sont devenus très confus.
Chaque appel de fonction est associé à une portée et à un contexte. Fondamentalement, la portée est basée sur la fonction et le contexte est basé sur les objets. En d’autres termes, la portée est liée à l’accès aux variables à chaque appel de fonction, et chaque appel est indépendant. Le contexte est toujours la valeur du mot-clé this, qui est une référence à l'objet qui appelle le code exécutable actuel.
portée variable
Les variables peuvent être définies dans des étendues locales ou globales, ce qui entraîne un accès aux variables d'exécution à partir de différentes étendues. Les variables globales doivent être déclarées en dehors du corps de la fonction, exister tout au long du processus en cours et peuvent être consultées et modifiées dans n'importe quelle portée. Les variables locales ne sont définies que dans le corps de la fonction et ont une portée différente pour chaque appel de fonction. Ce sujet concerne l'affectation, l'évaluation et le fonctionnement des valeurs uniquement dans le cadre de l'appel, et les valeurs en dehors de la portée ne sont pas accessibles.
Actuellement, JavaScript ne prend pas en charge la portée au niveau du bloc. La portée au niveau du bloc fait référence à la définition de variables dans des blocs d'instructions, telles que les instructions if, les instructions switch, les instructions de boucle, etc. Cela signifie que les variables ne sont pas accessibles en dehors du bloc d'instructions. Actuellement, toutes les variables définies dans un bloc d'instructions sont accessibles en dehors du bloc d'instructions. Cependant, cela va bientôt changer, puisque le mot-clé let a été officiellement ajouté à la spécification ES6. Utilisez-le à la place du mot-clé var pour déclarer les variables locales comme portée au niveau du bloc.
"ce" contexte
Le contexte dépend généralement de la manière dont une fonction est appelée. Lorsqu'une fonction est appelée en tant que méthode sur un objet, ceci est défini sur l'objet sur lequel la méthode est appelée :
Copiez le code comme suit :
var objet = {
foo : fonction(){
alert(this === objet);
}
} ;
objet.foo(); // vrai
Le même principe s'applique lors de l'appel d'une fonction pour créer une instance d'un objet à l'aide de l'opérateur new. Lorsqu'il est appelé de cette façon, la valeur de this sera définie sur l'instance nouvellement créée :
Copiez le code comme suit :
fonction foo(){
alerte(ce);
}
foo() // fenêtre
nouveau foo() // foo
Lors de l’appel d’une fonction indépendante, celle-ci sera définie par défaut sur le contexte global ou l’objet fenêtre (si dans un navigateur). Cependant, si la fonction est exécutée en mode strict ("use strict"), la valeur de this sera définie par défaut sur undefined.
Contexte d’exécution et chaîne de portée
JavaScript est un langage monothread, ce qui signifie qu'il ne peut faire qu'une seule chose à la fois dans le navigateur. Lorsque l'interpréteur JavaScript exécute initialement du code, il utilise d'abord par défaut le contexte global. Chaque appel à une fonction crée un nouveau contexte d'exécution.
La confusion se produit souvent ici. Le terme « contexte d'exécution » signifie ici la portée, et non le contexte comme indiqué ci-dessus. C'est une mauvaise dénomination, mais le terme est défini par la spécification ECMAScript et n'a d'autre choix que de s'y conformer.
Chaque fois qu'un nouveau contexte d'exécution est créé, il est ajouté en haut de la chaîne de portée et devient la pile d'exécution ou d'appel. Le navigateur s'exécute toujours dans le contexte d'exécution actuel en haut de la chaîne de portée. Une fois terminé, il (le contexte d'exécution actuel) est supprimé du haut de la pile et le contrôle est renvoyé au contexte d'exécution précédent. Par exemple:
Copiez le code comme suit :
fonction d'abord(){
deuxième();
fonction seconde(){
troisième();
fonction troisième(){
quatrième();
fonction quatrième(){
// faire quelque chose
}
}
}
}
d'abord();
L'exécution du code précédent entraînera l'exécution des fonctions imbriquées de haut en bas jusqu'à la quatrième fonction. À ce stade, la chaîne de portée de haut en bas est : quatrième, troisième, deuxième, premier, globale. La quatrième fonction peut accéder aux variables globales et à toutes les variables définies dans les première, deuxième et troisième fonctions, tout comme ses propres variables. Une fois l'exécution de la quatrième fonction terminée, le quatrième contexte sera supprimé du haut de la chaîne de portée et l'exécution reviendra à la troisième fonction. Ce processus se poursuit jusqu'à ce que tout le code soit terminé.
Les conflits de noms de variables entre différents contextes d'exécution sont résolus en remontant la chaîne de portée, du local au global. Cela signifie que les variables locales portant le même nom ont une priorité plus élevée dans la chaîne de portée.
En termes simples, chaque fois que vous essayez d'accéder à une variable dans le contexte d'exécution de la fonction, le processus de recherche démarre toujours à partir du propre objet variable. Si la variable que vous recherchez ne se trouve pas dans votre propre objet variable, poursuivez la recherche dans la chaîne de portées. Il gravira la chaîne de portée et examinera chaque objet variable de contexte d'exécution pour trouver une valeur qui correspond au nom de la variable.
fermeture
Une fermeture est formée lorsqu'on accède à une fonction imbriquée en dehors de sa définition (portée) afin qu'elle puisse être exécutée après le retour de la fonction externe. Elle (la fermeture) maintient (dans la fonction interne) l'accès aux variables locales, aux arguments et aux déclarations de fonction dans la fonction externe. L'encapsulation nous permet de masquer et de protéger le contexte d'exécution de la portée externe, tout en exposant l'interface publique à travers laquelle d'autres opérations peuvent être effectuées. Un exemple simple ressemble à ceci :
Copiez le code comme suit :
fonction foo(){
var local = 'variable privée';
retourner la barre de fonction(){
retour local ;
}
}
var getLocalVariable = foo();
getLocalVariable() // variable privée
L'un des types de fermetures les plus populaires est le modèle de module bien connu. Il permet de se moquer des membres publics, privés et privilégiés :
Copiez le code comme suit :
var Module = (fonction(){
var privateProperty = 'foo';
fonction privateMethod(args){
//faire quelque chose
}
retour {
propriété publique : "",
méthode publique : fonction (arguments) {
//faire quelque chose
},
méthode privilégiée : fonction (arguments) {
méthodeprivée(arguments);
}
}
})();
Les modules sont en fait quelque peu similaires aux singletons, ajoutant une paire de parenthèses à la fin et les exécutant immédiatement après que l'interpréteur ait fini de les interpréter (exécuter la fonction immédiatement). Les seuls membres externes disponibles du contexte d'exécution de fermeture sont les méthodes et propriétés publiques de l'objet renvoyé (telles que Module.publicMethod). Cependant, toutes les propriétés et méthodes privées existeront tout au long du cycle de vie du programme, car le contexte d'exécution est protégé (fermetures) et l'interaction avec les variables se fait via des méthodes publiques.
Un autre type de fermeture est appelé expression de fonction IIFE immédiatement invoquée, qui n'est rien de plus qu'une fonction anonyme auto-invoquée dans le contexte de la fenêtre.
Copiez le code comme suit :
fonction(fenêtre){
var a = 'foo', b = 'bar';
fonction privée(){
// faire quelque chose
}
fenêtre.Module = {
public : fonction(){
// faire quelque chose
}
} ;
})(ce);
Cette expression est très utile pour protéger l'espace de noms global. Toutes les variables déclarées dans le corps de la fonction sont des variables locales et persistent dans tout l'environnement d'exécution via des fermetures. Cette façon d'encapsuler le code source est très populaire pour les programmes et les frameworks, exposant généralement une seule interface globale pour interagir avec le monde extérieur.
Appelez et postulez
Ces deux méthodes simples, intégrées à toutes les fonctions, permettent d'exécuter des fonctions dans un contexte personnalisé. La fonction call nécessite une liste de paramètres tandis que la fonction apply vous permet de transmettre les paramètres sous forme de tableau :
Copiez le code comme suit :
utilisateur de fonction (premier, dernier, âge) {
// faire quelque chose
}
user.call (fenêtre, 'John', 'Doe', 30);
user.apply(window, ['John', 'Doe', 30]);
Le résultat de l'exécution est le même, la fonction utilisateur est appelée sur le contexte de la fenêtre et les trois mêmes paramètres sont fournis.
ECMAScript 5 (ES5) a introduit la méthode Function.prototype.bind pour contrôler le contexte, qui renvoie une nouvelle fonction liée en permanence au premier paramètre de la méthode bind, quelle que soit la manière dont la fonction est appelée. Il corrige le contexte de la fonction via des fermetures. Voici une solution pour les navigateurs qui ne le supportent pas :
Copiez le code comme suit :
if(!('bind' dans Function.prototype)){
Function.prototype.bind = fonction(){
var fn = this, contexte = arguments[0], args = Array.prototype.slice.call(arguments, 1);
fonction de retour(){
return fn.apply(contexte, args);
}
}
}
Il est couramment utilisé dans la perte de contexte : orienté objet et traitement événementiel. Cela est nécessaire car la méthode addEventListener du nœud conserve toujours le contexte d'exécution de la fonction en tant que nœud auquel le gestionnaire d'événements est lié, ce qui est important. Toutefois, si vous utilisez des techniques avancées orientées objet et devez conserver le contexte de la fonction de rappel en tant qu'instance de la méthode, vous devez ajuster manuellement le contexte. C'est la commodité apportée par bind :
Copiez le code comme suit :
fonction MaClasse(){
this.element = document.createElement('div');
this.element.addEventListener('click', this.onClick.bind(this), false);
}
MaClasse.prototype.onClick = fonction(e){
// faire quelque chose
} ;
En regardant le code source de la fonction bind, vous remarquerez peut-être la ligne de code relativement simple suivante, appelant une méthode sur Array :
Copiez le code comme suit :
Array.prototype.slice.call(arguments, 1);
Fait intéressant, il est important de noter ici que l'objet arguments n'est pas réellement un tableau, mais il est souvent décrit comme un objet de type tableau, un peu comme la liste de nœuds (le résultat renvoyé par la méthode document.getElementsByTagName()). Ils contiennent des propriétés de longueur et les valeurs peuvent être indexées, mais ce ne sont toujours pas des tableaux car ils ne prennent pas en charge les méthodes de tableau natives telles que slice et push. Cependant, comme elles se comportent de la même manière que les tableaux, les méthodes de tableau peuvent être appelées et détournées. Si vous souhaitez exécuter des méthodes de tableau dans un contexte de type tableau, suivez l'exemple ci-dessus.
Cette technique d'appel de méthodes d'autres objets est également appliquée à l'orientation objet, lors de l'émulation de l'héritage classique (héritage de classe) en JavaScript :
Copiez le code comme suit :
MaClasse.prototype.init = function(){
// appelle la méthode init de la superclasse dans le contexte de l'instance "MyClass"
MySuperClass.prototype.init.apply(this, arguments);
}
Nous pouvons reproduire ce modèle de conception puissant en appelant les méthodes de la superclasse (MySuperClass) dans les instances de la sous-classe (MyClass).
en conclusion
Il est très important de comprendre ces concepts avant de commencer à apprendre des modèles de conception avancés, car la portée et le contexte jouent un rôle important et fondamental dans le JavaScript moderne. Qu'il s'agisse de fermetures, d'orientation objet et d'héritage ou de diverses implémentations natives, le contexte et la portée jouent un rôle important. Si votre objectif est de maîtriser le langage JavaScript et d’acquérir une compréhension approfondie de ses composants, la portée et le contexte devraient être votre point de départ.
Supplément du traducteur
La fonction bind implémentée par l'auteur est incomplète. Les paramètres ne peuvent pas être transmis lors de l'appel de la fonction renvoyée par bind. Le code suivant résout ce problème :
Copiez le code comme suit :
if(!('bind' dans Function.prototype)){
Function.prototype.bind = fonction(){
var fn = this, contexte = arguments[0], args = Array.prototype.slice.call(arguments, 1);
fonction de retour(){
return fn.apply(context, args.concat(arguments));//fixé
}
}
}