Maison >interface Web >js tutoriel >Les différences entre var, let et const à travers le prisme de la norme ECMAScript.

Les différences entre var, let et const à travers le prisme de la norme ECMAScript.

王林
王林original
2024-08-24 22:30:32548parcourir

The Differences Between var, let, and const Through the Prism of the ECMAScript Standard.

De nombreux articles expliquent les différences entre var, let et const en utilisant des termes tels que hoisting, Temporal Dead Zone (TDZ), fonctionnel et portée du bloc, etc., souvent sans faire référence à la norme. Certains de ces termes ne sont même pas inclus dans la norme linguistique. C'est parfaitement bien d'expliquer le sujet sans faire référence à la norme linguistique. Cependant, j'explique le sujet en y faisant référence pour ceux qui souhaitent approfondir un peu, car la compréhension de la norme ECMAScript est cruciale pour une compréhension complète de JavaScript.

ECMAScript

De nombreuses organisations ont leurs références pour JavaScript, telles que MDN Web Docs, javascript.info et autres. Il existe cependant une organisation de normalisation dont le seul objectif est de normaliser et de documenter les systèmes informatiques. Cette organisation est Ecma International, une autorité fiable dans le domaine. L'organisation maintient une norme appelée ECMA-262, le numéro interne d'une entreprise pour identifier la norme. Ecma International définit cette norme comme l'épine dorsale du langage de programmation généraliste ECMAScript, que nous appelons communément JavaScript. Comprendre cette norme est essentiel pour comprendre le langage lui-même. La dernière norme en date d'août 2024 est la 15e édition d'ECMAScript, également connue sous le nom de ECMAScript 2024.

Contexte d'exécution

Pour comprendre les différences entre var, let et const, il est essentiel de comprendre le concept de contexte d'exécution.

Le contexte d'exécution est une structure abstraite définie dans le standard ECMAScript. C'est l'environnement dans lequel le code actuel est exécuté. Pour simplifier les choses, nous pouvons supposer qu'il existe un contexte d'exécution global et un contexte d'exécution fonctionnel.

// Global Execution Context
let globalVariable = 'This is a global variable'

function outerFunction() {
  // Outer Function Execution Context
  let outerVariable = 'This is an outer variable'

  function innerFunction() {
    // Inner Function Execution Context
    let innerVariable = 'This is an inner variable'
  }

  innerFunction()
}

outerFunction()

Pour suivre l'exécution du code, le contexte d'exécution comprend plusieurs composants, appelés composants d'état. Parmi ceux-ci, LexicalEnvironment et VariableEnvironment sont cruciaux pour comprendre le comportement des mots-clés var, let et const.

LexicalEnvironment et VariableEnvironment sont tous deux des enregistrements d'environnement. Environment Record est également une structure de données abstraite définie dans la norme ECMAScript. Il établit l'association des identifiants à des variables et des fonctions spécifiques. Un identifiant est un nom qui fait référence à des valeurs, des fonctions, des classes et d'autres structures de données en JavaScript. Dans l'exemple suivant, soit variable = 42, variable est le nom de la variable (identifiant) qui stocke la valeur du nombre 42.

Chaque fois que le code est exécuté, le contexte d'exécution crée un nouvel enregistrement d'environnement. En plus de stocker les identifiants, l'enregistrement d'environnement possède un champ [[OuterEnv]], soit nul, soit une référence à un enregistrement d'environnement externe.

Graphiquement, le contexte d'exécution et l'enregistrement d'environnement de l'exemple précédent pourraient être représentés comme ceci :

// Global Execution Context
{
  // Environment Record
  {
    identifier: 'globalVariable'
    value: 'This is a global variable'
  }
  {
    identifier: 'outerFunction'
    value: Function
  }
  [[OuterEnv]]: null
}

// Outer Function Execution Context
{
  // Environment Record
  {
    identifier: 'outerVariable'
    value: 'This is an outer variable'
  }
  {
    identifier: 'innerFunction'
    value: Function
  }
  [[OuterEnv]]: Global Execution Context
}


// Inner Function Execution Context
{
  // Environment Record
  {
    identifier: 'innerVariable'
    value: 'This is an inner variable'
  }
  [[OuterEnv]]: Outer Function Execution Context
}

Un autre point important à retenir concernant le Contexte d'Exécution est qu'il comporte deux phases distinctes : la Phase de Création et la Phase d'Exécution. Ces deux phases sont essentielles pour comprendre la différence entre var et let ou const.

Déclarations Let et Const

Dans le paragraphe 14.3.1 Déclarations Let et Const du standard ECMAScript, ce qui suit est indiqué :

Les déclarations let et const définissent des variables qui sont limitées au LexicalEnvironment du contexte d'exécution en cours. Les variables sont créées lorsque l'enregistrement d'environnement qui les contient est instancié, mais ne sont accessibles d'aucune façon tant que le LexicalBinding de la variable n'est pas évalué. Une variable définie par un LexicalBinding avec un Initializer se voit attribuer la valeur de l'AssignmentExpression de son Initializer lorsque le LexicalBinding est évalué, et non lorsque la variable est créée. Si un LexicalBinding dans une déclaration let n'a pas d'initialiseur, la variable reçoit la valeur non définie lorsque le LexicalBinding est évalué.

Pour comprendre cette affirmation, je vais l'expliquer phrase par phrase.

Les déclarations let et const définissent des variables qui sont limitées au LexicalEnvironment du contexte d'exécution en cours.

Cela signifie que les variables créées avec les mots-clés let ou const sont limitées au bloc où elles ont été définies. Le bloc de code est n'importe quel code JavaScript entre accolades.

let condition = true
if (condition) {
  let blockScopedVariable = 'This is a block-scoped variable'
  console.log(blockScopedVariable) // This is a block-scoped variable
}

console.log(blockScopedVariable) // ReferenceError: blockScopedVariable is not defined

// Global Execution Context
{
  // Environment Record
  {
    identifier: 'condition'
    value: true
  }
  [[OuterEnv]]: null
  // Block Environment Record
  {
    identifier: 'variable'
    value: 'This is a block-scoped variable'
  }
  [[OuterEnv]]: Global Execution Context 
}

Les variables sont créées lorsque leur enregistrement d'environnement contenant est instancié, mais ne sont accessibles d'aucune manière tant que le LexicalBinding de la variable n'est pas évalué.

As previously mentioned, the Execution Context has two phases. This statement means that during the Creation Phase of the Execution Context, variables are stored in their corresponding Environment Record but have not yet been assigned any value. They are uninitialised.

console.log(varaible) // ReferenceError: Cannot access 'varaible' before initialization

let varaible = 42

// Global Execution Context Creation Phase
{
  // Environment Record
  {
    identifier: 'variable'
    value: uninitialised
  }
  [[OuterEnv]]: null
}

Because the variable is already created (instantiated) in the Environment Record, the Execution Context knows about it but can't access it before evaluation(the Execution Phase of the Execution context). The state of the variable being uninitialised is also known as a Temporary Dead Zone(TDZ). We would have a different error if the variable hadn't been created in the Environment Record.

console.log(varaible) // ReferenceError: varaible is not defined

// Global Execution Context Creation Phase
{
  // Environment Record
  {
  }
  [[OuterEnv]]: null
}

A variable defined by a LexicalBinding with an Initializer is assigned the value of its Initializer's AssignmentExpression when the LexicalBinding is evaluated, not when the variable is created.

LexicalBinding is a form of the Identifier, which represents the variable's name. The Initializer is the variable's value, and AssignmentExpression is the expression used to assign that value to the variable's name, such as the '=' sign in let variable = 42. Therefore, the statement above means that variables created with let or const keywords are assigned their value during the Execution Phase of the Execution Context.

let variable = 42

// Global Execution Context Creation Phase
{
  // Environment Record
  {
    identifier: 'variable'
    value: uninitialised
  }
  [[OuterEnv]]: null
}

// Global Execution Context Execution Phase
{
  // Environment Record
  {
    identifier: 'variable'
    value: 42
  }
  [[OuterEnv]]: null
}

If a LexicalBinding in a let declaration does not have an Initializer the variable is assigned the value undefined when the LexicalBinding is evaluated.

This means that if a let variable is created without an initial value, undefined is assigned to it during the Execution Phase of the Execution Context. Variables declared with the const keyword behave differently. I will explain it in a few paragraphs later.

let variable
// Global Execution Context Creation Phase
{
  // Environment Record
  {
    identifier: 'variable'
    value: uninitialised
  }
  [[OuterEnv]]: null
}

// Global Execution Context Execution Phase
{
  // Environment Record
  {
    identifier: 'variable'
    value: undefined
  }
  [[OuterEnv]]: null
}

The standard also defines a subsection called 14.3.1.1 'Static Semantics: Early Errors,' which explains other essential aspects of variables defined with the let and const keywords.

LexicalDeclaration: LetOrConst BindingList;

  • It is a Syntax Error if the BoundNames of BindingList contains "let".
  • It is a Syntax Error if the BoundNames of BindingList contains any duplicate entries. LexicalBinding : BindingIdentifier Initializer
  • It is a Syntax Error if Initializer is not present and IsConstantDeclaration of the LexicalDeclaration containing this LexicalBinding is true.

LetOrConst is a grammar rule which specifies that variable declarations can start with the let or const keywords.
BindingList is a list of variables declared with let or const keywords. We could imagine BindingList as a data structure like this:

let a = 1
let b = 2
let c = 3
const d = 4
const e = 5

BindingList: [
    { 
        identifier: 'a',
        value: 1
    },
    { 
        identifier: 'b',
        value: 2
    },
    { 
        identifier: 'c',
        value: 3
    },
    { 
        identifier: 'd',
        value: 4
    },
    { 
        identifier: 'e',
        value: 5
    }
]

A Syntax Error is an error that breaks the language's grammatical rules. They occur before the code's execution. Let's analyse the first Syntax Error.

  • It is a Syntax Error if the BoundNames of BindingList contains "let".

The BoundNames of BindingList are the names of variables declared with let or const keywords.

let a = 1
let b = 2
let c = 3
const d = 4
const e = 5

BoundNames: ['a', 'b', 'c', 'd', 'e']

A Syntax Error will occur when the BoundNames list contains “let”.

let let = 1 // SyntaxError: let is disallowed as a lexically bound name
const let = 1 // SyntaxError: let is disallowed as a lexically bound name
  • It is a Syntax Error if the BoundNames of BindingList contains any duplicate entries.

It means we can't use the same names for variables declared with the let or const keywords if they are already used in that scope.

let a = 1
let a = 2 // SyntaxError: Identifier 'a' has already been declared
  • It is a Syntax Error if Initializer is not present and IsConstantDeclaration of the LexicalDeclaration containing this LexicalBinding is true.

IsConstantDeclaration is an abstract operation in the standard that checks if the variable is declared with the const keyword. This rule could be decrypted like that: if IsConstantDeclaration is true and the variable doesn't have an Initializer, a Syntax Error will be returned.

const x; // SyntaxError: Missing initializer in const declaration

Another vital thing only related to the const keyword: variables declared with the const keyword can't be reassigned. It is not stated explicitly in the standard, but we can get it from the IsConstantDeclaration operation and the syntax rule that variables declared with the const keyword should always be initialised with the Initializer

const variable = 42
variable = 46 // TypeError: Assignment to constant variable

Variable Statement

Before 2015, when the ECMAScript 2015 wasn't released yet, only the var keyword was available to create a variable in JavaScript.

In the paragraph 14.3.2 Variable Statement of ECMAScript standard the following is stated:

A var statement declares variables scoped to the running execution context's VariableEnvironment. Var variables are created when their containing Environment Record is instantiated and are initialized to undefined when created. Within the scope of any VariableEnvironment a common BindingIdentifier may appear in more than one VariableDeclaration but those declarations collectively define only one variable. A variable defined by a VariableDeclaration with an Initializer is assigned the value of its Initializer's AssignmentExpression when the VariableDeclaration is executed, not when the variable is created.

I again explain it sentence by sentence.

A var statement declares variables scoped to the running execution context's VariableEnvironment.

This means that variables declared with the var keyword are either function-scoped if declared inside a function or global-scoped if declared outside any function.

let condition = true
if (condition) {
    var globalVariable = 'This is a global variable'
}

console.log(globalVariable ) // This is a global variable

function outerFunction() {
  // Outer Function Execution Context
  var outerVariable = 'This is an outer variable'
}

outerFunction()

// Global Execution Context
{
  // Environment Record
  {
    identifier: 'condition'
    value: true
  }
  {
    identifier: 'globalVariable'
    value: 'This is a global variable'
  }
  {
    identifier: 'outerFunction'
    value: Function
  }
  [[OuterEnv]]: null
}

// Outer Function Execution Context
{
  // Environment Record
  {
    identifier: 'outerVariable'
    value: 'This is an outer variable'
  }
  [[OuterEnv]]: Global Execution Context
}

Var variables are created when their containing Environment Record is instantiated and are initialized to undefined when created.

During the Creation Phase of the Execution Context variables are assigned the undefined value. The process of assigning the undefined to a variable during the Creation Phase is often referred to as "hoisting" or declaration hoisting. It is worth mentioning that the terms "hoisting" or declaration hoisting are not included in the standard. However, it is a convention used by many developers to explain the availability of variables "before" they were declared.

console.log(globalVariable) // undefined

var globalVariable = 'This is a global variable'

// Global Execution Context Creation Phase
{
  // Environment Record
  {
    identifier: 'globalVariable'
    value: undefined
  }
  [[OuterEnv]]: null
}

Sometimes, it is explained that the code example above is possible because variables declared with the var keyword are "moved" to the top of the scope. However, nothing is moved anywhere; it is only possible by assigning the undefined value to the variable during the Creation Phase of Execution Context.

Within the scope of any VariableEnvironment a common BindingIdentifier may appear in more than one VariableDeclaration but those declarations collectively define only one variable.

BindingIdentifier is a more specific type of the Identifier. We used the Identifier term before to explain the name of a variable. While Identifier also refers to the variable's name, BindingIdentifier is only used in the context of the declaration of variables (function or other data structure).

let variable = 42 // BindingIdentifier
console.log(variable )  // Identifier

Now, let's go back to explaining the sentence's meaning.

BindingIdentifier may appear in more than one VariableDeclaration

In the same scope, we can create multiple variables with the same name using the var keyword, whilst all these "variables" reference only one variable.

var variable = 42
var variable = 66
var variable = 2015

// Execution context
{
    // Environment Record
    {
        identifier: 'variable '
        value: 2015
    }
    [[OuterEnv]]: null
}

It may appear we declared three variables with the BindingIdentifier variable, but we just reassigned the original variable variable twice. First, we reassigned it from 42 to 66, then from 66 to 2015

A variable defined by a VariableDeclaration with an Initializer is assigned the value of its Initializer's AssignmentExpression when the VariableDeclaration is executed, not when the variable is created.

The variable's value (Initializer) is assigned to it during the Execution Phase, not the Creation Phase of the Execution Context. Variables declared with the let and const keywords behave identically.

var variable = 42

// Global Execution Context Creation Phase
{
    // Environment Record
    {
        identifier: variable
        value: undefined
    }
    [[OuterEnv]]: null
}

// Global Execution Context Execution Phase
{
    // Environment Record
    {
        identifier: variable
        value: 42
    }
    [[OuterEnv]]: null
}

Diffrences

To sum up the article, I would like to highlight the following differences:

Scope

The first difference between variables created with var, let, and const keywords is how they are scoped. Variables created with let and const are scoped to the LexicalEnvironment, meaning they are available in the Environment Record of a block, function, or the Global Execution Context. In contrast, variables created with var are scoped to the VariableEnvironment, meaning they are only available in the Environment Record of a function or the Global Execution Context.

Creation Phase of the Execution Context

During the Execution Context's Creation Phase, variables created with let and const are uninitialised, whilst var variables are assigned the undefined value. The state of let and const being uninitialised is sometimes referenced as a Temporal Dead Zone or TDZ. Also, the behaviour of var being assigned the undefined value is usually known as “hoisting”.

Default Initializer value

Variables created with let and var keywords are assigned the undefined value if Initializer is not provided. Meanwhile, const variables must always have Initializer.

Dénomination des variables

Les variables créées avec le mot-clé var peuvent avoir des noms en double puisqu'elles font toutes référence à la même variable. Cependant, les variables let et const ne peuvent pas avoir de noms en double – cela entraînerait une erreur de syntaxe.

Réaffectation de l'initialiseur de variables

Les variables créées avec les mots-clés let et var peuvent réaffecter leur initialiseur (valeur) initial à un autre. Mais les variables const ne peuvent pas voir leur initialiseur réaffecté.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn