Annexe

Héritage prototypage

À 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 d'utilisation des valeurs. Lorsqu'une recherche de méthode ou de propriété est appelée sur une primitive, JavaScript encapsule la primitive en arrière-plan et appelle la méthode ou effectue la recherche de propriétés sur l'objet wrapper.

Par exemple, un littéral de chaîne n'a pas de méthodes propres, mais vous pouvez appeler la méthode .toUpperCase() sur celui-ci 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 prototypal, qui hérite des propriétés et des méthodes 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 un littéral de chaîne: un objet qui contient non seulement la valeur de notre chaîne, mais aussi 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 obtenus 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 aboutit à 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 littéraux numériques:

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 aurez très rarement besoin d'utiliser ces constructeurs, car l'héritage prototypique intégré de JavaScript signifie qu'ils ne présentent aucun avantage pratique. La création de primitives à l'aide de constructeurs peut également entraîner des résultats inattendus, car il s'agit d'un objet et non d'un simple littéral:

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 de points-virgules (ASI)

Lors de l'analyse d'un script, les interpréteurs JavaScript peuvent utiliser une fonctionnalité appelée insertion automatique de points-virgules (ASI) pour essayer de corriger les instances de points-virgules 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 précédent par un saut de ligne.
  • Ce jeton est }.
  • Le jeton précédent est ), et le point-virgule inséré serait le point-virgule de fin d'une instruction do...while.

Pour en savoir plus, reportez-vous aux règles ASI.

Par exemple, l'omission de points-virgules après les instructions suivantes ne provoque pas d'erreur de syntaxe à cause d'ASI:

const myVariable = 2
myVariable + 3
> 5

Toutefois, 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 des erreurs, et non une sorte de flexibilité syntaxique intégrée à JavaScript. Assurez-vous d'utiliser des points-virgules, le cas échéant, afin de ne pas vous appuyer dessus pour produire un code correct.

Mode strict

Les normes qui régissent l'écriture de JavaScript ont bien évolué depuis la conception initiale de ce langage. Toute nouvelle modification du comportement attendu de JavaScript doit éviter de provoquer des erreurs sur les anciens sites Web.

ES5 résout certains problèmes de longue date liés à la sémantique JavaScript sans rompre les implémentations existantes en introduisant le "mode strict", un moyen d'activer un ensemble plus restrictif de règles de langage 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-virgule, 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 d'entrer en conflit avec les futures fonctionnalités du langage. Par exemple, les premières décisions de conception concernant le champ d'application des variables faisaient augmenter le risque pour les développeurs de "polluer" par erreur le champ d'application global lorsqu'ils déclaraient une variable, quel que soit le contexte parent, 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 d'endommager un site Web qui en dépend, que ce soit par erreur ou délibérément. Le code JavaScript moderne l'empêche en effet en permettant aux développeurs d'activer le mode strict pour les nouveaux travaux et de l'activer par défaut uniquement dans le contexte de nouvelles fonctionnalités de langage, où elles n'interrompront pas les anciennes implémentations:

(function() {
    "use strict";
    mySloppyGlobal = true;
}());
> Uncaught ReferenceError: assignment to undeclared variable mySloppyGlobal

Vous devez écrire "use strict" en tant que littéral de chaîne. Un littéral de modèle (use strict) ne fonctionne pas. Vous devez également inclure "use strict" avant tout code exécutable dans son contexte prévu. Sinon, l'interpréteur 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'un mappage, 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. De ce fait, la modification d'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 cette 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 également pour effectuer des comparaisons strictes, car une égalité stricte entre les objets nécessite que les deux variables fassent référence au même objet pour évaluer la valeur true. Ils ne peuvent pas faire référence à des objets différents, même s'ils sont structurellement identiques:

const myObject = {};
const myReferencedObject = myObject;
const myNewObject = {};

myObject === myNewObject;
> false

myObject === myReferencedObject;
> true

Allocation de mémoire

JavaScript utilise la gestion automatique de la mémoire, ce qui signifie que la mémoire n'a pas besoin d'être explicitement allouée ou libérée au cours du développement. Bien que les approches utilisées par les moteurs JavaScript concernant la gestion de la mémoire n'entrent pas dans le cadre de ce module, la compréhension de la façon dont la mémoire est allouée fournit un contexte utile pour utiliser les valeurs de référence.

Il y a deux "zones" en mémoire: la "pile" et le "tas de mémoire". 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. Le tas de mémoire stocke des objets, qui nécessitent un espace alloué de manière dynamique, car leur taille peut changer lors de l'exécution. La mémoire est libérée par un processus appelé "récupération de mémoire", qui supprime les objets sans références de la mémoire.

Le thread principal

JavaScript est un langage à thread unique avec un modèle d'exécution "synchrone", ce qui signifie qu'il ne peut exécuter qu'une tâche à la fois. Ce contexte d'exécution séquentiel est appelé thread principal.

Le thread principal est partagé par d'autres tâches du navigateur, telles que l'analyse du code HTML, l'affichage et le réaffichage de certaines parties de la page, l'exécution d'animations CSS et la gestion des interactions utilisateur, du plus simple (comme la mise en surbrillance du texte) au complexe (comme l'interaction avec les éléments du formulaire). Les fournisseurs de navigateurs ont trouvé des moyens d'optimiser les tâches effectuées par le thread principal. Toutefois, les scripts plus complexes peuvent toujours utiliser une trop grande partie des ressources du thread principal et avoir une incidence sur les performances globales de la page.

Certaines tâches peuvent être exécutées dans des threads d'arrière-plan appelés Web Workers, avec quelques restrictions:

  • Les threads de calcul ne peuvent agir que sur des fichiers JavaScript autonomes.
  • L'accès à la fenêtre du navigateur et à son interface utilisateur est fortement réduit, voire inexistant.
  • Leur manière de communiquer avec le thread principal est limitée.

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 active) est une liste appelée pile d'appel (souvent il s'agit simplement de la "pile"). Lorsqu'un script est exécuté pour la première fois, l'interpréteur JavaScript crée un "contexte d'exécution global" et le transfère vers la pile d'appel. Les instructions dans ce contexte global sont exécutées une par une, de haut en bas. Lorsque l'interpréteur rencontre un appel de fonction lors de l'exécution du contexte global, il place un "contexte d'exécution de la 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 cet appel est placé en haut de la pile, juste au-dessus du contexte d'exécution actuel. La pile d'appel fonctionne selon le principe du "premier entré, premier sorti", ce qui signifie que l'appel de fonction le plus récent, qui est le plus haut de la pile, est exécuté et se poursuit jusqu'à la résolution de l'appel. Une fois cette fonction terminée, l'interpréteur la supprime de la pile d'appel. Le contexte d'exécution contenant 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 définissent é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 rappel

Cette exécution séquentielle signifie que les tâches asynchrones qui incluent des fonctions de rappel, telles que l'extraction de données à partir d'un serveur, la réponse à l'interaction de l'utilisateur ou l'attente de minuteurs définis sur setTimeout ou setInterval, bloquent le thread principal jusqu'à la fin de cette tâche ou interrompront 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 résoudre ce problème, JavaScript gère les tâches asynchrones à l'aide d'un "modèle de simultanéité" basé sur des événements constitué de la "boucle d'événements" et de la "file d'attente de rappel" (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'appel. 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 rappel. S'il y a des tâches dans la file d'attente de rappel et que la boucle d'événements détermine que la pile d'appel est vide, les tâches de la file d'attente de rappel sont envoyées dans la pile pour être exécutées une par une.