javascript - Är variabler deklarerade med let eller const hoistade?


Jag har lekt med ES6 ett tag och jag märkte att medan variabler deklarerade med var flyttas upp som förväntat...

console.log(typeof name); // undefined
var name = "John";

...variabler som deklareras med let eller const verkar ha några problem med hoisting:

console.log(typeof name); // ReferenceError
let name = "John";

och

console.log(typeof name); // ReferenceError
const name = "John";

Betyder detta att variabler som deklareras med let eller const inte hoistas? Vad händer egentligen här? Finns det någon skillnad mellan let och const i detta avseende?

Svar #1

@thefourtheye har rätt i att dessa variabler inte kan nås innan de deklareras. Men det är lite mer komplicerat än så.

Blir variabler deklarerade med let eller const inte hoistade? Vad händer egentligen här?

Alla deklarationer (var, let, const, function, function*, class) höjs upp i JavaScript. Detta betyder att om ett namn deklareras i ett scope, i det scopet kommer identifieraren alltid att referera till just den variabeln:

x = "global";
// function scope:
(function() {
    x; // not "global"

    var/let/… x;
}());
// block scope (not for `var`s):
{
    x; // not "global"

    let/const/… x;
}

Detta är sant både för funktions- och blocksammanhang1.

Skillnaden mellan var/function/function*-deklarationer och let/const/class-deklarationer är initialiseringen.
De förstnämnda initialiseras med undefined eller (generator)funktionen direkt när bindningen skapas i början av scopet. De lexikalt deklarerade variablerna förblir däremot oinitialiserade. Detta betyder att ett ReferenceError-undantag kastas när du försöker få åtkomst till den. Den kommer endast att initialiseras när let/const/class-satsen utvärderas, allt som sker före (ovanpå) detta kallas temporal dead zone.

x = y = "global";
(function() {
    x; // undefined
    y; // Reference error: y is not defined

    var x = "local";
    let y = "local";
}());

Observera att ett let y;-uttalande initialiserar variabeln med undefined på samma sätt som let y=undefined; skulle ha gjort.

Den temporala dödzonen är inte en syntaktisk plats, utan snarare tiden mellan variabelns (scope) skapande och initialisering. Det är inte ett fel att referera till variabeln i kod ovanför deklarationen så länge den koden inte exekveras (t.ex. ett funktionsblock eller helt enkelt död kod), och det kommer att kasta ett undantag om du försöker komma åt variabeln innan initialiseringen även om koden som accessar variabeln ligger under deklarationen (t.ex. i en hoistad funktionsdeklaration som kallas för tidigt).

Finns det någon skillnad mellan let och const i denna fråga?

Nej, de fungerar på samma sätt när det gäller hoisting. Den enda skillnaden mellan dem är att en constant måste och bara kan tilldelas i initialiseringsdelen av deklarationen (const one=1;, både const one; och senare omtilldelningar som one=2 är ogiltiga).

1: var-deklarationer fungerar fortfarande endast på funktionsnivå, självklart

Svar #2

Citerar ECMAScript 6 (ECMAScript 2015) specifikationens avsnitt om let och const-deklarationer,

Variablerna skapas när deras innehållande lexikala miljö instansieras men kan inte nås på något sätt förrän variabelns LexicalBinding utvärderas.

Så, för att svara på din fråga, ja, let och const höjs upp men du kan inte komma åt dem innan den faktiska deklarationen utvärderas vid körning.

Svar #3

ES6 introducerar Let-variabler som kommer med blocknivå-scoping. Fram till ES5 hade vi inte blocknivå-scoping, så de variabler som deklareras inuti ett block blir alltid hoistade till funktionsnivå-scoping.

I princip refererar Scope till var i ditt program dina variabler är synliga, vilket bestämmer var du får använda de variabler du har deklarerat. I ES5 har vi global scope, function scope och try/catch scope, med ES6 får vi också blocknivå-skopning genom att använda Let.

  • När du definierar en variabel med var-nyckelordet är den känd i hela funktionen från det ögonblick den definieras.
  • När du definierar en variabel med let-satsen är den endast känd i blocket där den definieras.

     function doSomething(arr){
         //i is known here but undefined
         //j is not known here
    
         console.log(i);
         console.log(j);
    
         for(var i=0; i<arr.length; i++){
             //i is known here
         }
    
         //i is known here
         //j is not known here
    
         console.log(i);
         console.log(j);
    
         for(let j=0; j<arr.length; j++){
             //j is known here
         }
    
         //i is known here
         //j is not known here
    
         console.log(i);
         console.log(j);
     }
    
     doSomething(["Thalaivar", "Vinoth", "Kabali", "Dinesh"]);
    

Om du kör koden kan du se att variabeln j bara är känd i loopen och inte före eller efter. Ändå är vår variabel i känd i hela funktionen från det ögonblick den definieras och framåt.

Det finns en annan stor fördel med att använda let eftersom det skapar en ny lexikalisk miljö och binder ett nytt värde istället för att behålla en gammal referens.

for(var i=1; i<6; i++){
   setTimeout(function(){
      console.log(i);
   },1000)
}

for(let i=1; i<6; i++){
   setTimeout(function(){
      console.log(i);
   },1000)
}

Den första for-slingan skriver alltid ut det sista värdet, med let skapas ett nytt block och binder nya värden som skriver ut 1, 2, 3, 4, 5.

När det gäller constants, fungerar det i princip som let, den enda skillnaden är att deras värde inte kan ändras. I konstanter är mutation tillåten men omassignering är inte tillåten.

const foo = {};
foo.bar = 42;
console.log(foo.bar); //works

const name = []
name.push("Vinoth");
console.log(name); //works

const age = 100;
age = 20; //Throws Uncaught TypeError: Assignment to constant variable.

console.log(age);

Om en konstant refererar till ett object, kommer den alltid att referera till object men själva object kan ändras (om det är muterbart). Om du vill ha ett oföränderligt object kan du använda Object.freeze([])

Svar #4

Enligt ECMAScript® 2021

Let- och Const-deklarationer

  • let- och const-deklarationer definierar variabler som är begränsade till det körande exekveringskontextens LexicalEnvironment.
  • Variablerna skapas när deras innehållande miljöpost instansieras men kan inte nås på något sätt förrän variabelns LexicalBinding utvärderas.
  • En variabel som definieras av en LexicalBinding med en Initializer tilldelas värdet av dess Initializer's AssignmentExpression när LexicalBinding utvärderas, inte när variabeln skapas.
  • Om en LexicalBinding i en let-deklaration inte har en Initializer tilldelas variabeln värdet undefined när LexicalBinding utvärderas.

Blockdeklaration Instansiering

  • När ett Block eller CaseBlock utvärderas skapas en ny deklarativ miljöpost och bindningar för varje block-omfattad variabel, konstant, funktion eller klass som deklareras i blocket instansieras i miljöposten.
  • Oavsett hur kontroll lämnar blocket återställs alltid det lexikala miljön till sitt tidigare tillstånd.

Högsta nivån lexikalt deklarerade namn

På den högsta nivån av en funktion eller skript behandlas funktionsdeklarationer som var-deklarationer snarare än som lexikala deklarationer.

Slutsats

  • let och const hoistas men initialiseras inte.

    Att referera till variabeln i blocket innan variabeldeklarationen resulterar i ett ReferenceError, eftersom variabeln befinner sig i en "temporär dödzon" från början av blocket tills deklarationen har bearbetats.

Exemplen nedan klargör hur "let"-variabler beter sig i ett lexikalt sammanhang/nestade lexikala sammanhang.

Exempel 1

var a;
console.log(a); //undefined

console.log(b); //undefined
var b;


let x;
console.log(x); //undefined

console.log(y); // Uncaught ReferenceError: y is not defined
let y; 

Variabeln 'y' ger ett referenceError, det betyder inte att den inte är hoistad. Variabeln skapas när den innehållande miljön instansieras. Men den kanske inte kan nås på grund av att den befinner sig i en otillgänglig "temporär dödszon".

Exempel 2

let mylet = 'my value';
 
(function() {
  //let mylet;
  console.log(mylet); // "my value"
  mylet = 'local value';
})();

Exempel 3

let mylet = 'my value';
 
(function() {
  let mylet;   
  console.log(mylet); // undefined
  mylet = 'local value';
})();

I exempel 3 har den nydeklarerade "mylet"-variabeln inuti funktionen ingen initialiserare före loggningen, därför är värdet "undefined".

Källa

ECMA MDN

Relaterade artiklar