Grokking Scope i JavaScript

Omfang, eller sett med regler som bestemmer hvor variablene dine bor, er et av de mest grunnleggende konseptene i et hvilket som helst programmeringsspråk. Det er så grunnleggende at det er lett å glemme hvor subtile reglene kan være!

Forstå nøyaktig hvordan JavaScript-motoren "tenker" om omfanget, vil holde deg i å skrive de vanlige feilene som heiser kan forårsake, forberede deg til å vikle hodet rundt lukkene, og få deg så mye nærmere å aldri skrive feil noensinne en gang til.

... Vel, det vil hjelpe deg å forstå heising og nedleggelser, uansett. 

I denne artikkelen tar vi en titt på:

  • Grunnleggende om omfang i JavaScript
  • hvordan tolken bestemmer hvilke variabler som hører til hvilket omfang
  • hvordan heising egentlig virker
  • hvordan ES6 søkeordene la og konst endre spillet

La oss dykke inn.

Hvis du er interessert i å lære mer om ES6 og hvordan du kan utnytte syntaksen og funksjonene for å forbedre og forenkle JavaScript-koden, hvorfor ikke sjekke disse to kursene:

Lexical Scope

Hvis du har skrevet enda en linje med JavaScript før, vet du det Var du definere Dine variabler bestemmer hvor du kan bruk dem. Det faktum at en variabel sikt er avhengig av strukturen til kildekoden din kalles leksikalske omfang.

Det er tre måter å skape omfang på i JavaScript:

  1. Lag en funksjon. Variabler som er oppgitt i funksjoner, er kun synlige innenfor den funksjonen, inkludert i nestede funksjoner.
  2. Erklære variabler med la eller konst inne i en kodeblokk. Slike erklæringer er bare synlige inne i blokken.
  3. Lage en å fange blokkere. Tro det eller ei, dette faktisk gjør opprett et nytt omfang!
"bruk strenge"; var mr_global = "Mr Global"; funksjon foo () var mrs_local = "Mrs Local"; console.log ("Jeg kan se" + mr_global + "og" + mrs_local + "."); funksjonslinje () console.log ("Jeg kan også se" + mr_global + "og" + mrs_local + ".");  foo (); // Fungerer som forventet forsøk console.log ("Men / I / kan ikke se" + mrs_local + ".");  catch (err) console.log ("Du har nettopp fått en" + feil ".");  la foo = "foo"; const bar = "bar"; console.log ("Jeg kan bruke" + foo + bar + "i sin blokk ...");  prøv console.log ("men ikke utenfor det.");  catch (err) console.log ("Du har nettopp fått en annen" + feil + ".");  // Kaster ReferenceError! console.log ("Merk at" + err + "ikke eksisterer utenfor" catch "!") 

Utsnittet over viser alle tre anvendelsesmekanismer. Du kan kjøre den i Node eller Firefox, men Chrome spiller ikke fint med la, ennå.

Vi snakker om hver av disse i utsøkt detalj. La oss begynne med et detaljert blikk på hvordan JavaScript angir hvilke variabler som hører til hvilket omfang.

Sammensetningsprosessen: En fugleperspektiv

Når du kjører et stykke JavaScript, skjer det to ting for å få det til å fungere.

  1. Først blir kilden din kompilert.
  2. Så blir kompilert kode utført.

Under de kompilering skritt, JavaScript-motoren:

  1. noterer seg alle dine variabelnavn
  2. registrerer dem i passende omfang
  3. reserverer plass til sine verdier

Det er bare under henrettelse at JavaScript-motoren faktisk angir verdien av variable referanser som er lik deres oppføringsverdier. Inntil da er de det udefinert

Trinn 1: Samling

// Jeg kan bruke first_name hvor som helst i dette programmet var first_name = "Peleke"; funksjon popup (first_name) // Jeg kan bare bruke sistnavn inni denne funksjonen var last_name = "Sengstacke"; varsling (fornavn + "+ sistenavn); popup (first_name);

La oss gå gjennom hva kompilatoren gjør.

Først leser den linjen var first_name = "Peleke". Deretter bestemmer det hva omfang å lagre variabelen til. Fordi vi er på toppen av skriptet, innser vi at vi er i globalt omfang. Deretter sparer den variabelen fornavn til det globale omfanget og initierer sin verdi til udefinert.

For det andre leser kompilatoren linjen med funksjon popup (first_name). Fordi det funksjon Søkeordet er det første på linjen, det skaper et nytt omfang for funksjonen, registrerer funksjonens definisjon i det globale omfanget, og kikker inn for å finne variabeldeklarasjoner.

Sikker nok finner kompilatoren en. Siden vi har var last_name = "Sengstacke" I første linje av vår funksjon lagrer kompilatoren variabelen etternavn til omfanget av popup-ikke til det globale omfanget - og setter sin verdi til udefinert

Siden det ikke er flere variable deklarasjoner inne i funksjonen, går kompilatoren tilbake i det globale omfanget. Og siden det ikke er flere variable deklarasjoner der, denne fasen er ferdig.

Legg merke til at vi egentlig ikke har det løpe noe enda. Kompilatorens jobb på dette punktet er bare for å sikre at det kjenner alles navn; det bryr seg ikke hva de gjør. 

På dette punktet vet vårt program at:

  1. Det er en variabel som heter fornavn i det globale omfanget.
  2. Det kalles en funksjon popup i det globale omfanget.
  3. Det er en variabel som heter etternavn i omfanget av popup.
  4. Verdiene av begge fornavn og etternavn er udefinert.

Det bryr seg ikke om at vi har tilordnet disse variablene verdiene andre steder i vår kode. Motoren tar vare på det i henrettelse.

Trinn 2: Gjennomføring

Under neste trinn leser motoren vår kode igjen, men denne gangen, Utfører den. 

Først leser den linjen, var first_name = "Peleke". For å gjøre dette ser motoren opp variabelen som kalles fornavn. Siden kompilatoren allerede har registrert en variabel med det navnet, finner motoren det, og setter dets verdi til "Peleke".

Deretter leser linjen, funksjon popup (first_name). Siden vi ikke er det utførende funksjonen her, motoren er ikke interessert og hopper over den.

Til slutt leser den linjen popup (FIRST_NAME). Siden vi er utfører en funksjon her, motoren:

  1. ser opp verdien av popup
  2. ser opp verdien av fornavn
  3. Utfører popup som en funksjon, bestått verdien av fornavn som en parameter

Når den kjøres popup, Det går gjennom samme prosess, men denne gangen inne i funksjonen popup. Den:

  1. ser opp variabelen som heter etternavn
  2. settene etternavns verdi lik "Sengstacke"
  3. ser opp varsling, utfører det som en funksjon med "Peleke Sengstacke" som parameter

Det viser seg mye mer under hetten enn vi kanskje trodde!

Nå som du forstår hvordan JavaScript leser og kjører koden du skriver, er vi klare til å takle noe litt nærmere hjemme: hvordan heising virker.

Heising under mikroskopet

La oss starte med noen kode.

bar (); funksjonslinje () hvis (! foo) alert (foo + "? Dette er rart ...");  var foo = "bar";  gått i stykker(); // TypeError! var ødelagt = funksjon () alert ("Dette varselet vil ikke dukke opp!"); 

Hvis du kjører denne koden, vil du legge merke til tre ting:

  1. Du kan referere til foo før du tilordner det, men verdien er udefinert.
  2. Du kan anrop gått i stykker før du definerer det, men du får en Typeerror.
  3. Du kan anrop Bar før du definerer det, og det fungerer som ønsket.

heising refererer til det faktum at JavaScript gjør at alle våre deklarerte variable navn er tilgjengelige overalt i deres omfang - inkludert før Vi tilordner dem.

De tre tilfellene i koden er de tre du må være oppmerksom på i din egen kode, så vi vil gå gjennom hver av dem en etter en.

Heise Variable Declarations

Husk når JavaScript-kompilatoren leser en linje som var foo = "bar", den:

  1. registrerer navnet foo til nærmeste omfang
  2. setter verdien av foo til udefinert

Grunnen til at vi kan bruke foo før vi tilordner det er fordi når motoren ser opp variabelen med det navnet, det gjør eksistere. Det er derfor det ikke kaster en ReferenceError

I stedet blir det verdien udefinert, og prøver å bruke det for å gjøre hva du spurte om det. Vanligvis er det en feil.

Når vi tenker på det, kan vi tenke oss at det JavaScript ser i vår funksjon Bar er mer som dette:

funksjonslinje () var foo; // undefined hvis (! foo) //! undefined er sant, så varselvarsel (foo + "? Dette er rart ...");  foo = "bar"; 

Dette er Første løfteregel, hvis du vil: Variabler er tilgjengelige gjennom hele deres omfang, men har verdien udefinert til koden tilordner dem.

Et vanlig JavaScript-idiom er å skrive alle dine Var erklæringer øverst i deres omfang, i stedet for hvor du først bruker dem. For å omskrive Doug Crockford, hjelper dette koden din lese mer som det runs.

Når du tenker på det, er det fornuftig. Det er ganske klart hvorfor Bar oppfører seg som det gjør når vi skriver vår kode som JavaScript leser det, ikke sant? Så hvorfor ikke bare skrive slik alle tiden?  

Hoisting Function Expressions

Det faktum at vi fikk en Typeerror da vi prøvde å utføre gått i stykker før vi har definert det, er det bare et spesielt tilfelle av den første heisregelen.

Vi definerte en variabel, kalt gått i stykker, som kompilatoren registrerer i det globale omfanget og setter lik udefinert. Når vi prøver å kjøre den, ser motoren opp verdien av gått i stykker, finner at det er udefinert, og prøver å utføre udefinert som en funksjon.

Åpenbart, udefinert er ikke en funksjon-det er derfor vi får en Typeerror!

Løftefunksjonsdeklarasjoner

Til slutt husker vi at vi kunne ringe Bar før vi definerte det. Dette skyldes Andre heiseregler: Når JavaScript-kompilatoren finner en funksjonsdeklarasjon, gjør den både navnet sitt og definisjon tilgjengelig på toppen av sitt omfang. Skriv om koden enda en gang:

funksjonslinje () hvis (! foo) alert (foo + "? Dette er rart ...");  var foo = "bar";  var ødelagt // undefined bar (); // bar er allerede definert, utfører fint ødelagt (); // Kan ikke utføre udefinert! ødelagt = funksjon () alert ("Dette varselet vil ikke dukke opp!"); 

 Igjen, det gir mye mer mening når du skrive som JavaScript leser, tror du ikke?

For å gjennomgå:

  1. Navnene på både variable deklarasjoner og funksjonsuttrykk er tilgjengelige gjennom deres omfang, men deres verdier er udefinert til oppdrag.
  2. Navnene og definisjoner av funksjonserklæringer er tilgjengelige gjennom deres omfang, selv før deres definisjoner.

La oss nå se på to nye verktøy som fungerer litt annerledes: la og konst.

lakonst, & Temporal Dead Zone

I motsetning til Var erklæringer, variabler deklarert med la og konst ikke bli hevet av kompilatoren.

I hvert fall ikke akkurat. 

Husk hvordan vi kunne ringe gått i stykker, men fikk en Typeerror fordi vi prøvde å utføre udefinert? Hvis vi hadde definert gått i stykker med la, vi hadde fått en ReferenceError, i stedet:

"bruk strenge"; // Du må "bruke strenge" for å prøve dette i Node broken (); // ReferenceError! la broken = function () alert ("Dette varselet vil ikke dukke opp!"); 

Når JavaScript-kompilatoren registrerer variabler i deres rekkevidde i sin første pass, behandler den la og konst annerledes enn det gjør Var

Når den finner a Var erklæring, registrerer vi navnet på variabelen i omfanget og umiddelbart initierer verdien til udefinert.

Med la, men kompilatoren gjør registrer variabelen i omfang, men gjør ikkeinitialiserer verdien til udefinert. I stedet lar den variabelen uninitialized, før motoren utfører oppgaven din. Å få tilgang til verdien av en uninitialisert variabel kaster a ReferenceError, som forklarer hvorfor koden over kaster når vi kjører den.

Rommet mellom begynnelsen av toppen av omfanget av a la erklæring og oppdragserklæringen kalles Temporal Dead Zone. Navnet kommer fra det faktum at, selv om motoren vet om en variabel som heter foo øverst i omfanget av Bar, variabelen er "død", fordi den ikke har en verdi.

... Også fordi det vil drepe programmet hvis du prøver å bruke det tidlig.

De konst Søkeord fungerer på samme måte som la, med to hovedforskjeller:

  1. Du  tilordne en verdi når du erklære med konst.
  2. Du kan ikke Tilordne verdier til en variabel som er deklarert med konst.

Dette garanterer at konst vil alltidha verdien som du opprinnelig tildelte den.

// Dette er juridisk const React = kreve ('reagere'); // Dette er helt ikke lovlig const crypto; krypto = krever ('krypto');

Blokkere omfanget

la og konst er forskjellig fra Var på en annen måte: størrelsen på deres rekkevidde.

Når du erklærer en variabel med Var, det er synlig så høyt opp på omfanget av kjeden som mulig - typisk øverst på nærmeste funksjonsdeklarasjon, eller i det globale omfanget, hvis du erklærer det på toppnivå. 

Når du erklærer en variabel med la eller konst, Det er imidlertid synlig som lokalt som mulig-bare innen nærmeste blokk.

EN blokkere er en del av koden satt av krøllete braces, som du ser med hvis/ellers blokker, til sløyfer, og i eksplisitt "blokkert" stykker av kode, som i denne brikken.

"bruk strenge"; la foo = "foo"; hvis (foo) const bar = "bar"; var foobar = foo + bar; console.log ("Jeg kan se" + bar + "i denne blokken.");  prøv console.log ("Jeg kan se" + foo + "i denne blokken, men ikke" + bar + ".");  fangst (err) console.log ("Du har en" + feil + ".");  prøv console.log (foo + bar); // Kaster på grunn av 'foo', men begge er udefinerte catch (err) console.log ("Du har nettopp fått en" + feil + ".");  console.log (foobar); // Fungerer fint

Hvis du erklærer en variabel med konst eller la inne i en blokk, er det bare synlig inne i blokken, og bare etter at du har tildelt det.

En variabel erklært med Var, Det er imidlertid synlig så langt unna som mulig-i dette tilfellet, i det globale omfanget.

Hvis du er interessert i nitty-gritty detaljer av la og konst, sjekk ut hva Dr Rauschmayer har å si om dem i Utforsking av ES6: Variabler og Scoping, og se på MDN-dokumentasjonen på dem.  

leksikalske dette Og pilfunksjoner

På overflaten, dette ser ikke ut til å ha mye å gjøre med omfanget. Og faktisk gjør JavaScript ikke løse betydningen av dette i henhold til anvendelsesområdet har vi snakket om her.

I det minste, ikke vanligvis. JavaScript, beryktet, gjør det ikke løse betydningen av dette søkeord basert på hvor du brukte det:

var foo = navn: 'Foo', språk: ['spansk', 'fransk', 'italiensk'], snakk: funksjon tale () this.languages.forEach (funksjon (språk) console.log navn + "snakker" + språk + ".";;); foo.speak ();

De fleste av oss ville forvente dette å mene foo inne i for hver loop, fordi det var det det betydde rett utenfor det. Med andre ord, vi forventer JavaScript for å løse betydningen av dette leksikalsk.

Men det gjør det ikke.

I stedet skaper det en ny dette inne i hver funksjon du definerer, og bestemmer hva det betyr basert på hvordan du ringer funksjonen-ikke hvor du definerte det.

Det første punktet ligner på omdefinering noen variabel i et barns omfang:

funksjon foo () var bar = "bar"; funksjon baz () // Gjenbruk av variable navn som dette kalles "shadowing" var bar = "BAR"; console.log (bar); // BAR baz ();  foo (); // BAR

Erstatte Bar med dette, og hele greien bør rydde opp umiddelbart!

Tradisjonelt, får dette å jobbe som vi forventer at vanlige gamle leksisk-scoped-variabler skal fungere, krever en av to løsninger:

var foo = navn: 'Foo', språk: ['spansk', 'fransk', 'italiensk'], speak_self: funksjon speak_s () var selv = dette; self.languages.forEach (funksjon (språk) console.log (self.name + "snakker" + språk + ".";), speak_bound: function speak_b () this.languages.forEach (funksjon ) console.log (dette.navnet + "snakker" + språk + "."; .bind (foo)); // Mer vanlig: .bind (dette); ;

I speak_self, vi redder meningen med dette til variabelen selv-, og bruk at variabel for å få referansen vi ønsker. I speak_bound, vi bruker binde til permanent punkt dette til et gitt objekt.

ES2015 gir oss et nytt alternativ: pilfunksjoner.

I motsetning til "normale" funksjoner, gjør pilfunksjoner ikke skygge deres foreldre omfang er dette verdi ved å sette sin egen. I stedet løser de sin mening leksikalsk. 

Med andre ord, hvis du bruker dette i en pilfunksjon ser JavaScript opp sin verdi som det ville være en hvilken som helst annen variabel.

Først sjekker den det lokale omfanget for a dette verdi. Siden pilfunksjonene ikke angir en, vil den ikke finne en. Deretter sjekker den forelder mulighet for a dette verdi. Hvis den finner en, vil den bruke det, i stedet.

Dette lar oss omskrive koden ovenfor slik:

var foo = navn: 'Foo', språk: ['spansk', 'fransk', 'italiensk'], snakk: funksjon tale () this.languages.forEach ((language) => console.log .name + "snakker" + språk + ".";;);   

Hvis du vil ha mer informasjon om pilfunksjoner, ta en titt på Envato Tuts + Instruktør Dan Wellmans fremragende kurs på JavaScript ES6 Fundamentals, samt MDN-dokumentasjonen på pilfunksjoner.

Konklusjon

Vi har dekket mye bakken så langt! I denne artikkelen har du lært at:

  • Variabler er registrert i deres omfang under kompilering, og knyttet til oppgavens verdier under henrettelse.
  • Med henvisning til variabler deklarert medla eller konst før oppdrag kaster a ReferenceError, og at slike variabler er scoped til nærmeste blokk.
  • Pilfunksjonertillat oss å oppnå leksikalsk binding av dette, og omgå tradisjonell dynamisk binding.

Du har også sett de to regler for heising:

  • De Første løfteregel: At funksjonsuttrykk og Var erklæringer er tilgjengelige i hele omfanget der de er definert, men har verdien udefinert til oppgavene dine utføres.
  • De Andre heiseregler: At navnene på funksjonsdeklarasjoner og Kroppene deres er tilgjengelige i hele omfanget der de er definert.

Et godt neste skritt er å bruke din nybegynnde kunnskap om JavaScript-målene for å pakke hodet rundt lukninger. For det, sjekk ut Kyle Simpsons Scopes & Closures.

Til slutt er det mye mer å si om dette enn jeg kunne dekke her. Hvis søkeordet fortsatt virker som så mye svart magi, ta en titt på dette og Objekt Prototyper for å få hodet rundt det.

I mellomtiden ta hva du har lært og gå skrive færre feil!

Lær JavaScript: Den komplette veiledningen

Vi har bygget en komplett guide for å hjelpe deg med å lære JavaScript, enten du er bare i gang som webutvikler eller du vil utforske mer avanserte emner.