Grokking Scope in JavaScript

Scope, of de set regels die bepalen waar uw variabelen leven, is een van de meest elementaire concepten van elke programmeertaal. Het is zelfs zo fundamenteel dat je gemakkelijk vergeet hoe subtiel de regels kunnen zijn!

Als u precies begrijpt hoe de JavaScript-engine over scope denkt, hoeft u niet de meest voorkomende bugs te schrijven die hijsen kan veroorzaken, bereidt u zich voor op het sluiten van sluitingen en komt u dichter bij het schrijven van bugs ooit nog een keer.

... Nou, het zal je hoe dan ook helpen om hijs- en sluitingen te begrijpen. 

In dit artikel zullen we kijken naar:

  • de basis van scopes in JavaScript
  • hoe de interpreter bepaalt welke variabelen bij welk bereik horen
  • hoe hijsen werkelijk werken
  • hoe de ES6-sleutelwoorden laat en const verander het spel

Laten we erin duiken.

Als u meer wilt weten over ES6 en hoe u de syntaxis en functies kunt gebruiken om uw JavaScript-code te verbeteren en te vereenvoudigen, kunt u deze twee cursussen bekijken:

Lexical Scope

Als je al eerder een regel JavaScript hebt geschreven, weet je dat waar jij bepalen je variabelen bepalen waar je kunt gebruik hen. Het feit dat de zichtbaarheid van een variabele afhankelijk is van de structuur van uw broncode, wordt genoemd lexicale strekking.

Er zijn drie manieren om bereik in JavaScript te creëren:

  1. Maak een functie. In interne functies gedeclareerde variabelen zijn alleen zichtbaar binnen die functie, inclusief in geneste functies.
  2. Declareer variabelen met laat of const in een codeblok. Dergelijke verklaringen zijn alleen zichtbaar binnen het blok.
  3. Maak een vangst blok. Geloof het of niet, dit eigenlijk doet maak een nieuwe scope!
"gebruik strikt"; var mr_global = "Mr Global"; function foo () var mrs_local = "Mrs Local"; console.log ("Ik kan zien" + mr_global + "en" + mrs_local + "."); functiebalk () console.log ("Ik kan ook" + mr_global + "en" + mrs_local + ".");  foo (); // Werkt zoals verwacht, probeer console.log ("But / I / can not see" + mrs_local + ".");  catch (err) console.log ("Je hebt zojuist een" + err + "gekregen.");  let foo = "foo"; const bar = "bar"; console.log ("Ik kan" + foo + bar + "gebruiken in zijn blok ...");  probeer console.log ("Maar niet daarbuiten.");  catch (err) console.log ("Je hebt zojuist een nieuwe" + err + "gekregen.");  // Throws ReferenceError! console.log ("Merk op dat" + err + "niet bestaat buiten 'catch'!") 

Het bovenstaande fragment demonstreert alle drie scoopmechanismen. Je kunt het in Node of Firefox uitvoeren, maar Chrome speelt niet leuk met laat, nog.

We zullen over elk van deze in prachtige details praten. Laten we beginnen met een gedetailleerd overzicht van de manier waarop JavaScript uitzoekt welke variabelen bij welk bereik horen.

Het compilatieproces: een vogelperspectief

Wanneer u JavaScript uitvoert, gebeuren er twee dingen om het te laten werken.

  1. Eerst wordt je bron gecompileerd.
  2. Vervolgens wordt de gecompileerde code uitgevoerd.

Gedurende de compilatie stap, de JavaScript-engine:

  1. neemt nota van al uw variabelenamen
  2. registreert ze in de juiste scope
  3. behoudt ruimte voor hun waarden

Het is alleen tijdens uitvoering dat de JavaScript-engine de waarde van variabele referenties eigenlijk gelijk aan hun toewijzingswaarden instelt. Tot die tijd zijn ze dat onbepaald

Stap 1: Compilatie

// Ik kan first_name overal in dit programma gebruiken var first_name = "Peleke"; functie popup (first_name) // Ik kan alleen achternaam gebruiken in deze functie var last_name = "Sengstacke"; alert (first_name + "+ last_name); popup (first_name);

Laten we doorlopen wat de compiler doet.

Eerst leest het de regel var first_name = "Peleke". Vervolgens bepaalt het wat strekking om de variabele op te slaan. Omdat we op het hoogste niveau van het script zitten, realiseren we ons dat we ons in de wereldwijde reikwijdte. Vervolgens wordt de variabele opgeslagen Voornaam naar de globale scope en initialiseert de waarde ervan onbepaald.

Ten tweede leest de compiler de regel mee functie popup (first_name). Omdat het functie keyword is het eerste ding op de regel, het creëert een nieuwe scope voor de functie, registreert de definitie van de functie in de globale scope en gluurt naar binnen om variabele declaraties te vinden.

En ja hoor, de compiler vindt er een. Omdat we hebben var last_name = "Sengstacke" in de eerste regel van onze functie slaat de compiler de variabele op achternaam naar de reikwijdte van pop-up-niet naar de mondiale reikwijdte - en bepaalt de waarde ervan onbepaald

Omdat er binnen de functie geen variabele declaraties meer zijn, treedt de compiler terug in de globale scope. En aangezien er geen variabele declaraties meer zijn er, deze fase is voltooid.

Merk op dat we dat eigenlijk niet hebben gedaan rennen Al iets. De taak van de compiler is op dit moment alleen maar om ervoor te zorgen dat iedereen de naam kent; het maakt niet uit wat zij doen. 

Op dit punt weet ons programma dat:

  1. Er is een variabele genoemd Voornaam in de mondiale reikwijdte.
  2. Er is een functie genaamd pop-up in de mondiale reikwijdte.
  3. Er is een variabele genoemd achternaam in de reikwijdte van pop-up.
  4. De waarden van beide Voornaam en achternaam zijn onbepaald.

Het maakt ons niet uit dat we die variabelenwaarden elders in onze code hebben toegewezen. Daar zorgt de motor voor uitvoering.

Stap 2: Uitvoering

Tijdens de volgende stap leest de motor onze code opnieuw, maar deze keer, Voert het. 

Eerst leest het de regel, var first_name = "Peleke". Om dit te doen, kijkt de motor naar de variabele genaamd Voornaam. Omdat de compiler al een variabele met die naam heeft geregistreerd, vindt de engine deze en stelt hij de waarde ervan in "Peleke".

Vervolgens wordt de regel gelezen, functie popup (first_name). Omdat we dat niet zijn uitvoeren de functie hier, de motor is niet geïnteresseerd en springt er overheen.

Eindelijk, het leest de regel popup (voornaam). Sinds we zijn hier een functie uitvoeren, de motor:

  1. zoekt de waarde op van pop-up
  2. zoekt de waarde op van Voornaam
  3. Voert pop-up als een functie, waarbij de waarde van wordt doorgegeven Voornaam als een parameter

Wanneer het wordt uitgevoerd pop-up, het gaat door hetzelfde proces, maar deze keer binnen de functie pop-up. Het:

  1. zoekt de variabele genaamd op achternaam
  2. sets achternaamDe waarde is gelijk aan "Sengstacke"
  3. kijkt op alarm, het uitvoeren als een functie met "Peleke Sengstacke" als zijn parameter

Blijkt dat er veel meer onder de motorkap gebeurt dan we misschien hadden gedacht!

Nu u begrijpt hoe JavaScript de code leest en uitvoert die u schrijft, kunnen we iets dichter bij huis aanpakken: hoe hijsen werkt.

Hijsen onder de microscoop

Laten we beginnen met wat code.

bar(); functiebalk () if (! foo) alert (foo + "? Dit is raar ...");  var foo = "bar";  broken (); // Typefout! var broken = function () alert ("Deze waarschuwing verschijnt niet!"); 

Als u deze code uitvoert, ziet u drie dingen:

  1. U kan verwijzen naar foo voordat u het toewijst, maar de waarde is onbepaald.
  2. kan telefoontje gebroken voordat je het definieert, maar je krijgt een Typefout.
  3. U kan telefoontje bar voordat je het definieert, en het werkt zoals gewenst.

Hijsen verwijst naar het feit dat JavaScript al onze gedeclareerde variabelenamen beschikbaar maakt overal in hun scopes - inclusief voor we wijzen ze toe.

De drie gevallen in het fragment zijn de drie die u in uw eigen code moet kennen, dus we zullen ze stuk voor stuk doornemen.

Variabele verklaringen hijsen

Onthoud dat wanneer de JavaScript-compiler een regel zoals leest var foo = "bar", het:

  1. registreert de naam foo naar de dichtstbijzijnde scope
  2. stelt de waarde in van foo naar undefined

De reden die we kunnen gebruiken foo voordat we het toewijzen, is omdat, wanneer de motor de variabele met die naam opzoekt, het doet bestaan. Dit is waarom het niet gooit ReferenceError

In plaats daarvan krijgt het de waarde onbepaald, en probeert dat te gebruiken om te doen wat je erom vroeg. Meestal is dat een fout.

Als we dat in ons achterhoofd houden, kunnen we ons voorstellen dat wat JavaScript in onze functie ziet bar is meer als volgt:

functiebalk () var foo; // undefined if (! foo) //! undefined is true, dus alert alert (foo + "? Dit is raar ...");  foo = "bar"; 

Dit is de Eerste regel van hijsen, als je wilt: variabelen zijn beschikbaar binnen hun bereik, maar hebben de waarde onbepaald totdat uw code deze toekent.

Een veelvoorkomend JavaScript-idioom is het schrijven van al uw var verklaringen bovenaan hun toepassingsgebied, in plaats van waar u ze voor het eerst gebruikt. Om Doug Crockford te parafraseren, helpt dit je code lezen meer leuk vinden runs.

Als je erover nadenkt, is dat logisch. Het is vrij duidelijk waarom bar gedraagt ​​zich zoals het doet wanneer we onze code schrijven zoals JavaScript het leest, is het niet? Dus waarom niet gewoon zo schrijven allemaal de tijd?  

Hoisting Function Expressions

Het feit dat we een hebben Typefout toen we probeerden uit te voeren gebroken voordat we het hebben gedefinieerd, is het slechts een speciaal geval van de eerste regel van hijsen.

We definieerden een variabele, genaamd gebroken, die de compiler registreert in de globale scope en sets gelijk aan onbepaald. Wanneer we het proberen uit te voeren, wordt de waarde van de motor opgezocht gebroken, vindt dat het zo is onbepaald, en probeert uit te voeren onbepaald als een functie.

Duidelijk, onbepaald is niet een functie - daarom krijgen we een Typefout!

Hoisting Function Declarations

Vergeet tot slot niet dat we konden bellen bar voordat we het definieerden. Dit komt door de Tweede regel van hijsen: Wanneer de JavaScript-compiler een functie-declaratie vindt, maakt deze beide de naam en definitie beschikbaar aan de top van de reikwijdte. Onze code opnieuw herschrijven:

functiebalk () if (! foo) alert (foo + "? Dit is raar ...");  var foo = "bar";  var gebroken; // undefined bar (); // -balk is al gedefinieerd, voert fijne onderverdeling uit (); // Can not execute undefined! broken = function () alert ("Deze waarschuwing verschijnt niet!"); 

 Nogmaals, het is veel logischer als je schrijven als JavaScript leest, vind je niet??

Beoordelen:

  1. De namen van beide variabele declaraties en functie-uitdrukkingen zijn overal beschikbaar, maar hun waarden zijn onbepaald tot opdracht.
  2. De namen en definities van functieverklaringen zijn overal beschikbaar, zelfs voordat hun definities.

Laten we nu eens kijken naar twee nieuwe tools die een beetje anders werken: laat en const.

laatconst, & de tijdelijke dode zone

anders var aangiften, variabelen gedeclareerd met laat en const niet doen worden gehesen door de compiler.

Tenminste, niet precies. 

Weet je nog hoe we konden bellen gebroken, maar kreeg een Typefout omdat we hebben geprobeerd uit te voeren onbepaald? Als we hadden gedefinieerd gebroken met laat, we zouden een gekregen hebben ReferenceError, in plaats daarvan:

"gebruik strikt"; // Je moet "strict gebruiken" om dit te proberen in Node broken (); // ReferenceError! laat broken = function () alert ("Deze waarschuwing verschijnt niet!"); 

Wanneer de JavaScript-compiler variabelen in hun scopes registreert tijdens de eerste doorvoer, wordt deze behandeld laat en const anders dan het doet var

Wanneer het een vindt var verklaring, we registreren de naam van de variabele in zijn scope en initialiseren onmiddellijk de waarde ervan onbepaald.

Met laat, echter, de compiler doet registreer de variabele in zijn scope, maar doet nietinitialiseer de waarde ervan onbepaald. In plaats daarvan laat het de variabele niet-geïnitialiseerd achter, tot de motor voert je opdrachtverklaring uit. Toegang tot de waarde van een niet-geïnitialiseerde variabele gooit een ReferenceError, wat verklaart waarom het bovenstaande fragment gooit wanneer we het uitvoeren.

De ruimte tussen het begin van de bovenkant van de reikwijdte van een laat verklaring en de toewijzingsinstructie wordt de Temporal Dead Zone. De naam komt van het feit dat, hoewel de motor weet over een variabele genaamd foo aan de top van de scope van bar, de variabele is "dood", omdat deze geen waarde heeft.

... Ook omdat het je programma zal doden als je het vroeg probeert te gebruiken.

De const sleutelwoord werkt op dezelfde manier als laat, met twee belangrijke verschillen:

  1. moet een waarde toewijzen wanneer u aangaf met const.
  2. kan niet wijs waarden opnieuw toe aan een variabele gedeclareerd met const.

Dit garandeert dat const zullen altijdhebben de waarde die u in eerste instantie hebt toegewezen.

// Dit is juridische const React = vereisen ('reageren'); // Dit is helemaal geen crypto voor juridische zaken; crypto = vereisen ('crypto');

Blokkeer bereik

laat en const zijn anders dan var op een andere manier: de grootte van hun scopes.

Wanneer u een variabele declareert bij var, het is zichtbaar zo hoog mogelijk in de scope-keten, meestal bovenaan de dichtstbijzijnde functie-declaratie, of in de globale scope, als u deze op het hoogste niveau declareert. 

Wanneer u een variabele declareert bij laat of const, het is echter zichtbaar als plaatselijk als mogelijk-enkel en alleen binnen het dichtstbijzijnde blok.

EEN blok is een gedeelte van de code dat wordt weergegeven door accolades, zoals u ziet met als/anders blokken, voor loops, en in expliciet "geblokkeerde" stukjes code, zoals in dit fragment.

"gebruik strikt"; let foo = "foo"; if (foo) const bar = "bar"; var foobar = foo + bar; console.log ("Ik kan zien" + bar + "in dit blok.");  probeer console.log ("Ik kan" + foo + "zien in dit blok, maar niet" + bar + ".");  catch (err) console.log ("Je hebt een" + err + ".");  probeer console.log (foo + bar); // Gooit vanwege 'foo', maar beide zijn undefined catch (err) console.log ("Je hebt zojuist een" + err + "gekregen.");  console.log (foobar); // Werkt prima

Als u een variabele declareert bij const of laat in een blok, het is enkel en alleen zichtbaar in het blok, en enkel en alleen nadat je het hebt toegewezen.

Een variabele verklaard met var, is echter zichtbaar zo ver mogelijk weg-in dit geval, in de globale reikwijdte.

Als je geïnteresseerd bent in de details van laat en const, Lees wat Dr. Rauschmayer te zeggen heeft over Exploration ES6: Variables and Scoping, en bekijk de MDN-documentatie hierover.  

lexicale deze & Pijlfuncties

Op het oppervlak, deze lijkt niet veel te maken te hebben met bereik. En eigenlijk doet JavaScript dat wel niet los de betekenis van op deze volgens de regels van de reikwijdte waar we het hier over hebben gehad.

Tenminste, meestal niet. JavaScript, berucht, doet niet los de betekenis van de deze sleutelwoord op basis van waar u het gebruikte:

var foo = name: 'Foo', talen: ['Spaans', 'Frans', 'Italiaans'], speak: function speak () this.languages.forEach (function (language) console.log (this. naam + "spreekt" + taal + ".");); foo.speak ();

De meesten van ons zouden verwachten deze om te betekenen foo binnen in de forEach loop, want dat is precies wat het betekende. Met andere woorden, we verwachten dat JavaScript de betekenis van deze lexicaal.

Maar dat doet het niet.

In plaats daarvan maakt het een nieuwe deze in elke functie die u definieert en bepaalt op basis van wat deze inhoudt hoe je noemt de functie-niet waar je hebt het gedefinieerd.

Dat eerste punt is vergelijkbaar met het geval van opnieuw definiëren ieder variabele in een child scope:

function foo () var bar = "bar"; function baz () // Hergebruik van namen van variabelen zoals deze wordt "shadowing" genoemd var bar = "BAR"; console.log (bar); // BAR baz ();  foo (); // BAR

Vervangen bar met deze, en het hele ding zou onmiddellijk moeten verdwijnen!

Traditioneel krijgen deze werken zoals we verwachten dat gewone oude variabelen met lexicisch bereik werken, vereist een van de twee oplossingen:

var foo = name: 'Foo', talen: ['Spaans', 'Frans', 'Italiaans'], speak_self: function speak_s () var self = this; self.languages.forEach (functie (taal) console.log (self.name + "spreekt" + taal + ".");), speak_bound: function speak_b () this.languages.forEach (function (language ) console.log (this.name + "spreekt" + taal + "."); .bind (foo)); // Meer algemeen: .bind (this); ;

In speak_self, we redden de betekenis van deze naar de variabele zelf, en gebruiken dat variabele om de gewenste referentie te krijgen. In speak_bound, we gebruiken binden naar blijvend punt deze naar een bepaald object.

ES2015 brengt ons een nieuw alternatief: pijlfuncties.

In tegenstelling tot de "normale" functies, doen pijlfuncties dat wel niet schaduw hun bovenliggende werkingssfeer deze waarde door hun eigen waarde in te stellen. Integendeel, ze lossen de betekenis ervan op lexicaal. 

Met andere woorden, als u gebruikt deze in een pijlfunctie zoekt JavaScript de waarde op zoals bij elke andere variabele.

Ten eerste controleert het de lokale ruimte voor een deze waarde. Omdat de pijlfuncties er geen instellen, vindt deze er geen. Vervolgens controleert het de ouder ruimte voor een deze waarde. Als het er een vindt, zal het dat gebruiken.

Dit laat ons de code hierboven herschrijven zoals dit:

var foo = name: 'Foo', talen: ['Spaans', 'Frans', 'Italiaans'], speak: function speak () this.languages.forEach ((language) => console.log (this .name + "spreekt" + taal + "."););   

Als u meer informatie wilt over pijlfuncties, bekijk dan de uitstekende cursus van Envato Tuts + Instructeur Dan Wellman over JavaScript ES6 Fundamentals, evenals de MDN-documentatie over pijlfuncties.

Conclusie

Tot nu toe hebben we veel terreinen bedekt! In dit artikel heb je geleerd dat:

  • Variabelen worden geregistreerd in hun scopes gedurende compilatie, en geassocieerd met hun toewijzingswaarden tijdens uitvoering.
  • Verwijzend naar variabelen gedeclareerd metlaat of const voor opdracht gooit a ReferenceError, en dat dergelijke variabelen naar het dichtstbijzijnde blok worden geschaald.
  • Pijl functiessta ons toe om lexicale binding van te bereiken deze, en omzeilen traditionele dynamische binding.

Je hebt ook de twee regels voor hijsen gezien:

  • De Eerste regel van hijsen: Die functie uitdrukkingen en var Verklaringen zijn overal in de bereiken beschikbaar waar ze zijn gedefinieerd, maar hebben de waarde onbepaald tot je toewijzingsopdrachten worden uitgevoerd.
  • De Tweede regel van hijsen: Dat de namen van functie-verklaringen en hun lichamen zijn beschikbaar in de scopes waar ze zijn gedefinieerd.

Een goede volgende stap is om uw nieuwe kennis van de scopes van JavaScript te gebruiken om uw hoofd rond sluitingen te wikkelen. Kijk daarvoor eens naar de Scopes & Closures van Kyle Simpson.

Eindelijk, er valt nog veel meer te vertellen over deze dan ik hier kon bedekken. Als het zoekwoord nog steeds zoveel zwarte magie lijkt, bekijk dan deze & Object Prototypes om je hoofd erover heen te krijgen.

Neem in de tussentijd wat u hebt geleerd en ga minder fouten schrijven!

Leer JavaScript: de complete gids

We hebben een complete handleiding samengesteld om u te helpen JavaScript te leren, of u net bent begonnen als een webontwikkelaar of dat u meer geavanceerde onderwerpen wilt verkennen.