Héritage prototypique
À l'exception de null
et undefined
, chaque type de données primitif possède un prototype, un wrapper d'objet correspondant qui fournit des méthodes pour travailler avec les valeurs. Lorsqu'une recherche de méthode ou de propriété est appelée sur une primitive, JavaScript encapsule la primitive en coulisses et appelle la méthode ou effectue la recherche de propriété sur l'objet wrapper à la place.
Par exemple, une chaîne littérale n'a pas de méthodes propres, mais vous pouvez appeler la méthode .toUpperCase()
dessus grâce au wrapper d'objet String
correspondant:
"this is a string literal".toUpperCase();
> THIS IS A STRING LITERAL
C'est ce qu'on appelle l'héritage prototypique : héritage des propriétés et des méthodes à partir du constructeur correspondant d'une valeur.
Number.prototype
> Number { 0 }
> constructor: function Number()
> toExponential: function toExponential()
> toFixed: function toFixed()
> toLocaleString: function toLocaleString()
> toPrecision: function toPrecision()
> toString: function toString()
> valueOf: function valueOf()
> <prototype>: Object { … }
Vous pouvez créer des primitives à l'aide de ces constructeurs, au lieu de simplement les définir par leur valeur. Par exemple, l'utilisation du constructeur String
crée un objet de chaîne, et non une chaîne littérale: un objet qui contient non seulement notre valeur de chaîne, mais toutes les propriétés et méthodes héritées du constructeur.
const myString = new String( "I'm a string." );
myString;
> String { "I'm a string." }
typeof myString;
> "object"
myString.valueOf();
> "I'm a string."
Dans la plupart des cas, les objets qui en résultent se comportent comme les valeurs que nous avons utilisées pour les définir. Par exemple, même si la définition d'une valeur numérique à l'aide du constructeur new Number
génère un objet contenant toutes les méthodes et propriétés du prototype Number
, vous pouvez utiliser des opérateurs mathématiques sur ces objets comme vous le feriez avec des valeurs numériques littérales:
const numberOne = new Number(1);
const numberTwo = new Number(2);
numberOne;
> Number { 1 }
typeof numberOne;
> "object"
numberTwo;
> Number { 2 }
typeof numberTwo;
> "object"
numberOne + numberTwo;
> 3
Vous n'aurez que très rarement besoin d'utiliser ces constructeurs, car l'héritage prototypique intégré de JavaScript signifie qu'ils n'apportent aucun avantage pratique. La création de primitives à l'aide de constructeurs peut également entraîner des résultats inattendus, car le résultat est un objet, et non une simple valeur littérale:
let stringLiteral = "String literal."
typeof stringLiteral;
> "string"
let stringObject = new String( "String object." );
stringObject
> "object"
Cela peut compliquer l'utilisation d'opérateurs de comparaison stricts:
const myStringLiteral = "My string";
const myStringObject = new String( "My string" );
myStringLiteral === "My string";
> true
myStringObject === "My string";
> false
Insertion automatique du point-virgule (ASI)
Lors de l'analyse d'un script, les interpréteurs JavaScript peuvent utiliser une fonctionnalité appelée insertion automatique de point-virgule (ASI) pour essayer de corriger les cas de point-virgule omis. Si l'analyseur JavaScript rencontre un jeton non autorisé, il tente d'ajouter un point-virgule avant ce jeton pour corriger l'erreur de syntaxe potentielle, à condition qu'une ou plusieurs des conditions suivantes soient remplies:
- Ce jeton est séparé du jeton précédent par un saut de ligne.
- Ce jeton est
}
. - Le jeton précédent est
)
, et le point-virgule inséré correspond au point-virgule de fin d'une instructiondo
…while
.
Pour en savoir plus, consultez les Règles de l'ASI.
Par exemple, l'omission de points-virgules après les instructions suivantes ne provoque pas d'erreur de syntaxe en raison de l'ASI:
const myVariable = 2
myVariable + 3
> 5
Toutefois, l'ASI ne peut pas prendre en compte plusieurs instructions sur la même ligne. Si vous écrivez plusieurs instructions sur la même ligne, veillez à les séparer par des points-virgules:
const myVariable = 2 myVariable + 3
> Uncaught SyntaxError: unexpected token: identifier
const myVariable = 2; myVariable + 3;
> 5
L'ASI est une tentative de correction d'erreur, et non une sorte de flexibilité syntaxique intégrée à JavaScript. Veillez à utiliser des points-virgules là où c'est approprié afin de ne pas vous y fier pour produire du code correct.
Mode strict
Les normes qui régissent l'écriture de JavaScript ont évolué bien au-delà de tout ce qui a été envisagé lors de la conception initiale du langage. Chaque nouveau changement apporté au comportement attendu de JavaScript doit éviter de générer des erreurs dans les anciens sites Web.
ES5 résout certains problèmes de longue date liés à la sémantique JavaScript sans casser les implémentations existantes en introduisant le "mode strict", un moyen d'activer un ensemble de règles de langage plus restrictives pour un script entier ou une fonction individuelle. Pour activer le mode strict, utilisez le littéral de chaîne "use strict"
, suivi d'un point-virgul, sur la première ligne d'un script ou d'une fonction:
"use strict";
function myFunction() {
"use strict";
}
Le mode strict empêche certaines actions "non sécurisées" ou fonctionnalités obsolètes, génère des erreurs explicites à la place des erreurs "silencieuses" courantes et interdit l'utilisation de syntaxes susceptibles de se chevaucher avec de futures fonctionnalités de langage. Par exemple, les premières décisions de conception concernant le champ d'application des variables ont rendu plus probable que les développeurs "polluent" par erreur le champ d'application global lorsqu'ils déclarent une variable, quel que soit le contexte contenant, en omettant le mot clé var
:
(function() {
mySloppyGlobal = true;
}());
mySloppyGlobal;
> true
Les environnements d'exécution JavaScript modernes ne peuvent pas corriger ce comportement sans risquer de casser tout site Web qui s'y appuie, par erreur ou intentionnellement. Au lieu de cela, le JavaScript moderne l'empêche en permettant aux développeurs d'activer le mode strict pour le nouveau travail et en n'activant le mode strict par défaut que dans le contexte des nouvelles fonctionnalités de langage où elles ne perturberont pas les implémentations obsolètes:
(function() {
"use strict";
mySloppyGlobal = true;
}());
> Uncaught ReferenceError: assignment to undeclared variable mySloppyGlobal
Vous devez écrire "use strict"
sous la forme d'un littéral de chaîne.
Une chaîne littérale de modèle (use strict
) ne fonctionnera pas. Vous devez également inclure "use strict"
avant tout code exécutable dans le contexte prévu. Sinon, l'interprète l'ignore.
(function() {
"use strict";
let myVariable = "String.";
console.log( myVariable );
sloppyGlobal = true;
}());
> "String."
> Uncaught ReferenceError: assignment to undeclared variable sloppyGlobal
(function() {
let myVariable = "String.";
"use strict";
console.log( myVariable );
sloppyGlobal = true;
}());
> "String." // Because there was code prior to "use strict", this variable still pollutes the global scope
Par référence, par valeur
Toute variable, y compris les propriétés d'un objet, les paramètres de fonction et les éléments d'un tableau, d'un ensemble ou d'une carte, peut contenir une valeur primitive ou une valeur de référence.
Lorsqu'une valeur primitive est attribuée d'une variable à une autre, le moteur JavaScript crée une copie de cette valeur et l'attribue à la variable.
Lorsque vous attribuez un objet (instances de classe, tableaux et fonctions) à une variable, au lieu de créer une copie de cet objet, la variable contient une référence à la position stockée de l'objet en mémoire. Par conséquent, modifier un objet référencé par une variable modifie l'objet référencé, et pas seulement la valeur contenue par cette variable. Par exemple, si vous initialisez une nouvelle variable avec une variable contenant une référence d'objet, puis utilisez la nouvelle variable pour ajouter une propriété à cet objet, la propriété et sa valeur sont ajoutées à l'objet d'origine:
const myObject = {};
const myObjectReference = myObject;
myObjectReference.myProperty = true;
myObject;
> Object { myProperty: true }
Cela est important non seulement pour modifier des objets, mais aussi pour effectuer des comparaisons strictes, car l'égalité stricte entre les objets nécessite que les deux variables référencent le même objet pour évaluer true
. Ils ne peuvent pas faire référence à des objets différents, même si ces objets sont structurellement identiques:
const myObject = {};
const myReferencedObject = myObject;
const myNewObject = {};
myObject === myNewObject;
> false
myObject === myReferencedObject;
> true
Allocation de mémoire
JavaScript utilise une gestion automatique de la mémoire, ce qui signifie qu'il n'est pas nécessaire d'allouer ou de désallouer explicitement de la mémoire au cours du développement. Bien que les détails des approches des moteurs JavaScript en matière de gestion de la mémoire dépassent le cadre de ce module, comprendre comment la mémoire est allouée fournit un contexte utile pour travailler avec des valeurs de référence.
La mémoire comporte deux "zones" : la "pile" et la "tas". La pile stocke des données statiques (valeurs primitives et références à des objets), car la quantité fixe d'espace nécessaire pour stocker ces données peut être allouée avant l'exécution du script. La pile stocke des objets, qui ont besoin d'un espace alloué dynamiquement, car leur taille peut changer pendant l'exécution. La mémoire est libérée par un processus appelé "récupération de mémoire", qui supprime de la mémoire les objets sans référence.
Thread principal
JavaScript est un langage fondamentalement à thread unique avec un modèle d'exécution "synchrone", ce qui signifie qu'il ne peut exécuter qu'une seule tâche à la fois. Ce contexte d'exécution séquentielle est appelé thread principal.
Le thread principal est partagé par d'autres tâches du navigateur, telles que l'analyse du code HTML, le rendu et le re-rendu de parties de la page, l'exécution d'animations CSS et la gestion des interactions utilisateur, allant du simple (comme la mise en surbrillance du texte) au complexe (comme l'interaction avec les éléments de formulaire). Les fournisseurs de navigateurs ont trouvé des moyens d'optimiser les tâches effectuées par le thread principal, mais les scripts plus complexes peuvent toujours utiliser trop de ressources du thread principal et affecter les performances globales de la page.
Certaines tâches peuvent être exécutées dans des threads en arrière-plan appelés Web Workers, avec certaines limitations:
- Les threads de travail ne peuvent agir que sur des fichiers JavaScript autonomes.
- Ils ont un accès très limité ou aucun à la fenêtre du navigateur et à l'interface utilisateur.
- Elles sont limitées dans la façon dont elles peuvent communiquer avec le thread principal.
Ces limites les rendent idéales pour les tâches ciblées et gourmandes en ressources qui pourraient autrement occuper le thread principal.
Pile d'appel
La structure de données utilisée pour gérer les "contextes d'exécution" (le code en cours d'exécution) est une liste appelée pile d'appels (souvent simplement appelée "pile"). Lorsqu'un script est exécuté pour la première fois, l'interprète JavaScript crée un "contexte d'exécution global" et le pousse dans la pile d'appels, les instructions de ce contexte global étant exécutées une à la fois, de haut en bas. Lorsque l'interprète rencontre un appel de fonction lors de l'exécution du contexte global, il place un "contexte d'exécution de fonction" pour cet appel en haut de la pile, met en pause le contexte d'exécution global et exécute le contexte d'exécution de la fonction.
Chaque fois qu'une fonction est appelée, le contexte d'exécution de la fonction pour cet appel est placé en haut de la pile, juste au-dessus du contexte d'exécution actuel. La pile d'appels fonctionne sur la base du principe "dernier arrivé, premier sorti", ce qui signifie que l'appel de fonction le plus récent, qui se trouve en haut de la pile, est exécuté et se poursuit jusqu'à sa résolution. Lorsque cette fonction est terminée, l'interprète la supprime de la pile d'appels, et le contexte d'exécution qui contient cet appel de fonction redevient l'élément le plus élevé de la pile et reprend l'exécution.
Ces contextes d'exécution capturent toutes les valeurs nécessaires à leur exécution. Ils établissent également les variables et les fonctions disponibles dans le champ d'application de la fonction en fonction de son contexte parent, et déterminent et définissent la valeur du mot clé this
dans le contexte de la fonction.
Boucle d'événements et file d'attente de rappels
Cette exécution séquentielle signifie que les tâches asynchrones qui incluent des fonctions de rappel, telles que la récupération de données à partir d'un serveur, la réponse aux interactions utilisateur ou l'attente de minuteurs définis avec setTimeout
ou setInterval
, bloquent le thread principal jusqu'à ce que cette tâche soit terminée ou interrompent de manière inattendue le contexte d'exécution actuel au moment où le contexte d'exécution de la fonction de rappel est ajouté à la pile. Pour y remédier, JavaScript gère les tâches asynchrones à l'aide d'un "modèle de concurrence" basé sur les événements, composé de la "boucle d'événements" et de la "file d'attente de rappels" (parfois appelée "file d'attente de messages").
Lorsqu'une tâche asynchrone est exécutée sur le thread principal, le contexte d'exécution de la fonction de rappel est placé dans la file d'attente de rappel, et non au-dessus de la pile d'appels. La boucle d'événements est un modèle parfois appelé réacteur, qui interroge en permanence l'état de la pile d'appels et de la file d'attente de rappels. Si la file d'attente des rappels contient des tâches et que la boucle d'événements détermine que la pile d'appels est vide, les tâches de la file d'attente des rappels sont transmises à la pile une par une pour être exécutées.