Grokking Omfattning i JavaScript

Räckvidd, eller uppsättningen regler som bestämmer var dina variabler bor, är ett av de mest grundläggande begreppen i vilket programmeringsspråk som helst. Det är så grundläggande faktiskt att det är lätt att glömma hur subtila reglerna kan vara!

Förstå exakt hur JavaScript-motorn "tänker" om omfattning kommer att hålla dig från att skriva de vanliga buggarna som hissar kan orsaka, förbereda dig för att sätta på huvudet runt stängningar och få dig så mycket närmare att du aldrig skriver buggar någonsin igen.

... Tja, det hjälper dig att förstå hiss och stängningar, hur som helst. 

I den här artikeln tar vi en titt på:

  • Grunderna för scopes i JavaScript
  • hur tolken bestämmer vilka variabler som hör till vilken omfattning
  • hur hissar verkligen Arbetar
  • hur ES6 nyckelord låta och const ändra spelet

Låt oss dyka in.

Om du är intresserad av att lära dig mer om ES6 och hur du utnyttjar syntaxen och funktionerna för att förbättra och förenkla JavaScript-koden, varför inte kolla in dessa två kurser:

Lexical Scope

Om du har skrivit en jämn linje av JavaScript innan vet du det var du definiera Dina variabler bestämmer var du kan använda sig av dem. Det faktum att en variabel sikt beror på strukturen i din källkod heter lexikalisk omfattning.

Det finns tre sätt att skapa utrymme i JavaScript:

  1. Skapa en funktion. Variabler som deklareras inom funktioner är synliga endast inom den funktionen, inklusive i kapslade funktioner.
  2. Förklara variabler med låta eller const inuti ett kodblock. Sådana deklarationer är bara synliga inuti blocket.
  3. Skapa en fånga blockera. Tro det eller inte, det här faktiskt gör skapa ett nytt omfång!
"använd strikt"; var mr_global = "Mr Global"; funktion foo () var mrs_local = "Mrs Local"; console.log ("Jag kan se" + mr_global + "och" + mrs_local + "."); funktionsfältet () console.log ("Jag kan också se" + mr_global + "och" + mrs_local + ".");  foo (); // fungerar som förväntat försök console.log ("men / jag / kan inte se" + mrs_local + ".");  fånga (err) console.log ("Du har bara en" + err + ".");  let foo = "foo"; const bar = "bar"; console.log ("Jag kan använda" + foo + bar + "i dess block ...");  försök console.log ("men inte utanför den.");  fånga (err) console.log ("Du har bara en annan" + err + ".");  // Kasta ReferenceError! console.log ("Observera att" + err + "existerar inte utanför" catch "!") 

Biten ovan visar alla tre räckviddsmekanismer. Du kan köra det i Node eller Firefox, men Chrome spelar inte bra med låta, än.

Vi pratar om var och en i utsökta detaljer. Låt oss börja med en detaljerad titt på hur JavaScript anger vilka variabler som hör till vilket omfattning.

Kompileringsprocessen: En fågelperspektiv

När du kör en bit av JavaScript råkar det hända att två saker gör att det fungerar.

  1. Först blir din källa sammanställd.
  2. Då utförs den sammanställda koden.

Under de kompilering steg, JavaScript-motorn:

  1. noterar alla dina variabla namn
  2. registrerar dem i lämplig räckvidd
  3. reserverar utrymme för sina värden

Det är bara under avrättning att JavaScript-motorn faktiskt anger värdet av variabla referenser lika med deras uppdelningsvärden. Fram till dess är de odefinierad

Steg 1: Sammanställning

// Jag kan använda förnamn någonstans i det här programmet var first_name = "Peleke"; funktion popup (first_name) // Jag kan bara använda sista namnet inuti denna funktion var last_name = "Sengstacke"; alert (first_name + "+ last_name); popup (first_name);

Låt oss gå igenom vad kompilatorn gör.

Först läser den linjen var first_name = "Peleke". Därefter bestämmer det vad omfattning för att spara variabeln till. Eftersom vi är på toppen av skriptet inser vi att vi är i globalt räckvidd. Sedan sparar den variabeln förnamn till det globala räckviddet och initierar sitt värde till odefinierad.

För det andra läser kompilatorn linjen med funktion popup (first_name). Eftersom det fungera sökord är det första på linjen, det skapar ett nytt utrymme för funktionen, registrerar funktionens definition till det globala räckviddet och tittar inuti för att hitta variabla deklarationer.

Visst nog, hittar kompilatorn en. Eftersom vi har var last_name = "Sengstacke" I den första raden av vår funktion sparar kompilatorn variabeln efternamn till omfattning av dyka upp-inte till det globala räckviddet - och sätter sitt värde till odefinierad

Eftersom det inte finns några variabla deklarationer i funktionen, går kompilatorn tillbaka i det globala räckviddet. Och eftersom det inte finns några variabla deklarationer där, denna fas är klar.

Observera att vi inte har faktiskt springa någonting ännu. Kompilatörens jobb vid denna punkt är bara för att se till att det känner till alla namn; det bryr sig inte om det Vad dom gör. 

Vid detta tillfälle vet vårt program att:

  1. Det finns en variabel som heter förnamn i det globala räckviddet.
  2. Det finns en funktion som heter dyka upp i det globala räckviddet.
  3. Det finns en variabel som heter efternamn inom ramen för dyka upp.
  4. Värdena för båda förnamn och efternamn är odefinierad.

Det bryr sig inte om att vi har tilldelat dessa variabler värden någon annanstans i vår kod. Motorn tar hand om det i avrättning.

Steg 2: Genomförande

Under nästa steg läser motorn vår kod igen, men den här gången, exekverar Det. 

Först läser den linjen, var first_name = "Peleke". För att göra detta ser motorn upp den variabel som heter förnamn. Eftersom kompilatorn redan har registrerat en variabel med det namnet, hittar motorn det och ställer in sitt värde till "Peleke".

Därefter läser den linjen, funktion popup (first_name). Eftersom vi inte är det exekvera funktionen här är motorn inte intresserad och hoppar över den.

Slutligen läser den linjen popup (first_name). Eftersom vi är utför en funktion här, motorn:

  1. ser upp värdet av dyka upp
  2. ser upp värdet av förnamn
  3. exekverar dyka upp som en funktion, som passerar värdet av förnamn som en parameter

När det körs dyka upp, Det går igenom samma process, men den här gången inuti funktionen dyka upp. Det:

  1. ser upp den variabel som heter efternamn
  2. uppsättningar efternamns värde lika med "Sengstacke"
  3. Kollar upp varna, exekvera det som en funktion med "Peleke Sengstacke" som dess parameter

Det visar sig mycket mer under kåpan än vi kanske trodde!

Nu när du förstår hur JavaScript läser och kör koden du skriver, är vi redo att ta itu med något lite närmare hemmet: hur hissar fungerar.

Lyftning under mikroskopet

Låt oss börja med någon kod.

bar(); funktionsfältet () if (! foo) alert (foo + "? Detta är konstigt ...");  var foo = "bar";  bruten (); // Skrivfel! var broken = function () alert ("Denna varning kommer inte dyka upp!"); 

Om du kör den här koden märker du tre saker:

  1. Du kan hänvisa till foo innan du tilldelar det, men dess värde är odefinierad.
  2. Du kan ring upp bruten innan du definierar det, men du får en Skrivfel.
  3. Du kan ring upp bar innan du definierar det, och det fungerar som önskat.

lyft hänvisar till det faktum att JavaScript gör alla våra deklarerade variabla namn tillgängliga överallt i sina omfattningar-inklusive innan vi tilldelar dem.

De tre fallen i snippet är de tre du behöver vara medvetna om i din egen kod, så vi kommer att gå igenom var och en av dem en efter en.

Höjningsvariabeldeklarationer

Kom ihåg, när JavaScript-kompilatorn läser en linje som var foo = "bar", Det:

  1. registrerar namnet foo till närmaste räckvidd
  2. anger värdet av foo till odefinierad

Anledningen till att vi kan använda foo innan vi tilldelar det är för att när motorn ser upp variabeln med det namnet, det gör existera. Det är därför det inte kastar en Reference

I stället blir värdet odefinierad, och försöker använda det för att göra vad du frågade om det. Vanligtvis är det en bugg.

Med tanke på detta kan vi föreställa oss att det som JavaScript ser i vår funktion bar är mer så här:

funktionsfältet () var foo; // odefinierad om (! foo) //! undefined är sant, så varning alert (foo + "? Detta är konstigt ...");  foo = "bar"; 

Det här är Första regel för lyftning, om du vill: Variabler är tillgängliga inom hela deras räckvidd, men har värdet odefinierad tills din kod tilldelar dem.

Ett vanligt JavaScript-idiom är att skriva alla dina var deklarationer högst upp i deras räckvidd, i stället för var du först använder dem. Att omskifta Doug Crockford hjälper det här med din kod läsa mer som det körningar.

När du tänker på det, är det meningsfullt. Det är ganska klart varför bar beter sig som det gör när vi skriver vår kod så som JavaScript läser det, eller hur? Så varför inte bara skriva så Allt tiden?  

Lyftfunktionsuttryck

Det faktum att vi fick en Skrivfel när vi försökte utföra bruten innan vi definierade det är bara ett speciellt fall av den första regeln för lyftning.

Vi definierade en variabel, kallad bruten, som kompilatorn registrerar i det globala räckviddet och motsvarar odefinierad. När vi försöker springa, ser motorn upp värdet av bruten, finner att det är odefinierad, och försöker att utföra odefinierad som en funktion.

Självklart, odefinierad är inte en funktion-det är därför vi får en Skrivfel!

Lyftfunktionsdeklarationer

Slutligen, erinra om att vi kunde ringa bar innan vi definierade det. Detta beror på Andra regeln för lyftning: När JavaScript-kompilatorn hittar en funktionsdeklaration gör den både sitt namn och definition tillgänglig överst i dess omfattning. Skriv om koden ännu en gång:

funktionsfältet () if (! foo) alert (foo + "? Detta är konstigt ...");  var foo = "bar";  var bruten // odefinierad bar (); // bar är redan definierad, exekverar finbruten (); // Kan inte utföras odefinierad! bruten = funktion () alert ("Den här varningen kommer inte dyka upp!"); 

 Återigen är det mycket mer meningsfullt när du skriva som JavaScript läser, tror du inte?

Att recensera:

  1. Namnen på både variabla deklarationer och funktionsuttryck är tillgängliga inom hela sitt räckvidd, men deras värden är odefinierad tills tilldelning.
  2. Namnen och Definitioner av funktionsdeklarationer finns tillgängliga inom hela deras räckvidd, även innan deras definitioner.

Låt oss nu titta på två nya verktyg som fungerar lite annorlunda: låta och const.

låtaconst, & Temporal Dead Zone

Till skillnad från var deklarationer, variabler deklarerade med låta och const inte hissas av kompilatorn.

Åtminstone, inte exakt. 

Kom ihåg hur vi kunde ringa bruten, men fick en Skrivfel eftersom vi försökte utföra odefinierad? Om vi ​​hade definierat bruten med låta, vi skulle ha fått en Reference, istället:

"använd strikt"; // Du måste "använda strikt" för att prova detta i Node broken (); // ReferenceError! låt bruten = funktion () alert ("Denna varning kommer inte dyka upp!"); 

När JavaScript-kompilatorn registrerar variabler till sina omfattningar i sitt första pass behandlar det låta och const annorlunda än vad den gör var

När den finner a var förklaring, registrerar vi namnet på variabelns omfattning och omedelbart initierar dess värde till odefinierad.

Med låta, dock kompilatorn gör registrera variabelns omfattning, men gör inteinitiera dess värde till odefinierad. I stället lämnar variabeln uninitialiserad, fram tills motorn utför ditt uppdragsdeklaration. Åtkomst av värdet för en oinitierad variabel kastar a Reference, vilket förklarar varför koden ovan kastar när vi kör det.

Utrymmet mellan början av toppen av omfattningen av a låta deklarationen och uppdragsdeklarationen kallas Temporal Dead Zone. Namnet kommer från det faktum att även om motorn vet om en variabel som heter foo högst upp till bar, variabeln är "död", eftersom den inte har ett värde.

... Även för att det kommer att döda ditt program om du försöker använda det tidigt.

De const sökord fungerar på samma sätt som låta, med två viktiga skillnader:

  1. Du måste Tilldela ett värde när du förklarar const.
  2. Du kan inte Tilldela värden till en variabel som deklarerats med const.

Detta garanterar att const kommer alltidha det värde som du ursprungligen tilldelade det.

// Detta är juridisk const React = kräver ('reagera'); // Detta är helt inte juridisk const crypto; krypto = kräver ('krypto');

Blockera omfattningen

låta och const skiljer sig från var på ett annat sätt: storleken på deras omfång.

När du deklarerar en variabel med var, det är synligt så högt upp på kedjan som möjligt - vanligen ovanpå närmaste funktionsdeklaration eller i det globala räckviddet om du förklarar det på toppnivå. 

När du deklarerar en variabel med låta eller const, Det är dock synligt som lokalt som möjligt-endast inom närmaste kvarter.

en blockera är en del av koden avstängd med lockiga axlar, som du ser med om/annan block, för slingor och i uttryckligen "blockerade" bitar av kod, som i det här snippet.

"använd strikt"; let foo = "foo"; om (foo) const bar = "bar"; var foobar = foo + bar; console.log ("Jag kan se" + bar + "i den här blocket.");  försök console.log ("jag kan se" + foo + "i det här blocket, men inte" + bar + ".");  fånga (err) console.log ("Du har en" + err + ".");  försök console.log (foo + bar); // Kasta på grund av "foo", men båda är odefinierade fånga (err) console.log ("Du har bara en" + err + ".");  console.log (foobar); // Fungerar bra

Om du deklarerar en variabel med const eller låta inuti ett block är det endast synlig inuti blocket, och endast efter att du har tilldelat den.

En variabel deklarerad med var, Det är dock synligt så långt bort som möjligt-i det här fallet, i det globala räckviddet.

Om du är intresserad av nitty-gritty detaljer om låta och const, kolla vad Dr Rauschmayer har att säga om dem i Exploring ES6: Variables and Scoping och titta på MDN-dokumentationen på dem.  

Lexikalisk detta & Pilfunktioner

På ytan, detta verkar inte ha en hel del att göra med räckvidd. Och i själva verket gör JavaScript inte lösa betydelsen av detta enligt de regler som vi har pratat om här.

Åtminstone, vanligtvis inte. JavaScript, berömd, gör det inte lösa betydelsen av detta sökord baserat på var du använde det:

var foo = namn: "Foo", språk: ['spanska', 'franska', 'italienska'], tala: funktion tala () this.languages.forEach (funktion (språk) console.log namn + "talar" + språk + ".";;); foo.speak ();

De flesta av oss skulle förvänta oss detta att mena foo inuti för varje loop, för det var vad det menade precis utanför det. Med andra ord, vi förväntar oss JavaScript för att lösa betydelsen av detta lexikaliskt.

Men det gör det inte.

Istället skapar det en ny detta inuti varje funktion du definierar och bestämmer vad det betyder baserat på på vilket sätt du ringer funktionen-inte var du definierade det.

Den första punkten liknar omdefinieringen några variabel i ett barnomfattning:

funktion foo () var bar = "bar"; funktion baz () // Återanvändning av variabla namn så här heter "shadowing" var bar = "BAR"; console.log (bar); // BAR baz ();  foo (); // BAR

Byta ut bar med detta, och hela saken ska rensa upp direkt!

Traditionellt att få detta att arbeta som vi förväntar oss att vanliga gamla lexiskt scoped variabler ska fungera kräver en av två lösningar:

var foo = namn: "Foo", språk: ['spanska', 'franska', 'italienska'], speak_self: function speak_s () var själv = detta; self.languages.forEach (funktion (språk) console.log (self.name + "talar" + språk + ".";;), speak_bound: function speak_b () this.languages.forEach (funktion ) console.log (this.name + "talar" + språk + "."; .bind (foo)); // Mer vanligt: ​​.bind (detta); ;

I speak_self, vi räddar betydelsen av detta till variabeln själv, och använda den där variabel för att få den referens vi vill ha. I speak_bound, vi använder binda till permanent punkt detta till ett visst objekt.

ES2015 ger oss ett nytt alternativ: pilfunktioner.

Till skillnad från "normala" funktioner gör pilfunktionerna inte skugga deras överordnade omfattning s detta värde genom att ställa in egna. I stället löser de sin mening lexikaliskt. 

Med andra ord, om du använder detta i en pilfunktion ser JavaScript upp sitt värde som det skulle vara någon annan variabel.

För det första kontrollerar den lokala räckvidden för a detta värde. Eftersom pilfunktionerna inte anger en, kommer den inte att hitta någon. Därefter kontrollerar den förälder utrymme för a detta värde. Om den hittar en, kommer den att använda det istället.

Det här låter oss skriva om koden ovan så här:

var foo = namn: "Foo", språk: ['spanska', 'franska', 'italienska'], tala: funktion tala () this.languages.forEach ((language) => console.log .name + "talar" + språk + ".";;);   

Om du vill ha mer information om pilfunktioner, ta en titt på Envato Tuts + Instructor Dan Wellmans utmärkta kurs på JavaScript ES6 Fundamentals, samt MDN-dokumentationen om pilfunktioner.

Slutsats

Vi har täckt mycket av marken hittills! I den här artikeln har du lärt dig att:

  • Variabler är registrerade till deras omfång under kompilering, och i samband med deras uppdragsvärden under avrättning.
  • Med hänvisning till variabler deklarerade medlåta eller const innan uppdrag kastar a Reference, och att sådana variabler scoped till närmaste block.
  • Pilfunktionertillåta oss att uppnå lexisk bindning av detta, och kringgå traditionell dynamisk bindning.

Du har också sett de två reglerna för hissning:

  • De Första regel för lyftning: Att funktionsuttryck och var deklarationer är tillgängliga i hela de områden där de definieras men har värdet odefinierad tills dina uppdragsuppdrag utförs.
  • De Andra regeln för lyftning: Att namnen på funktionsdeklarationer och deras kroppar är tillgängliga i hela de områden där de definieras.

Ett bra nästa steg är att använda din nyskapade kunskap om JavaScript-områdena för att linda huvudet runt stängningar. För det, kolla Kyle Simpsons Scopes & Closures.

Slutligen finns det mycket mer att säga om detta än jag kunde täcka här. Om sökordet fortfarande verkar som så mycket svart magi, ta en titt på detta och Objekt Prototyper för att få huvudet runt det.

Under tiden ta vad du har lärt dig och skriv färre buggar!

Lär dig JavaScript: Den fullständiga guiden

Vi har byggt en komplett guide för att hjälpa dig att lära dig JavaScript, oavsett om du precis börjat som webbutvecklare eller vill utforska mer avancerade ämnen.