Groking Scope in JavaScript

L'ambito o l'insieme di regole che determinano dove vivono le variabili, è uno dei concetti di base di qualsiasi linguaggio di programmazione. È così fondamentale, infatti, che è facile dimenticare quanto sottili possano essere le regole!

Capire esattamente come il motore JavaScript "pensa" all'ambito ti impedirà di scrivere gli errori comuni che il sollevamento può causare, prepararti a chiudere la testa intorno alle chiusure e avvicinarti così tanto a non scrivere mai bug mai ancora.

... beh, ti aiuterà a capire anche sollevamento e chiusure. 

In questo articolo, daremo un'occhiata a:

  • le basi degli ambiti in JavaScript
  • come l'interprete decide quali variabili appartengono a quale ambito
  • come sollevare veramente lavori
  • come le parole chiave ES6 permettereconst cambia il gioco

Tuffiamoci dentro.

Se sei interessato a saperne di più su ES6 e su come sfruttare la sintassi e le funzionalità per migliorare e semplificare il codice JavaScript, perché non dare un'occhiata a questi due corsi:

Ambito lessicale

Se hai già scritto una riga di JavaScript, lo saprai dove tu definire le tue variabili determinano dove puoi uso loro. Il fatto che la visibilità di una variabile dipende dalla struttura del tuo codice sorgente viene chiamata lessicale scopo.

Esistono tre modi per creare l'ambito in JavaScript:

  1. Crea una funzione. Le variabili dichiarate all'interno delle funzioni sono visibili solo all'interno di quella funzione, incluse le funzioni annidate.
  2. Dichiarare le variabili con permettereconst all'interno di un blocco di codice. Tali dichiarazioni sono visibili solo all'interno del blocco.
  3. Creare un catturare bloccare. Che ci crediate o no, questo in realtà fa creare un nuovo ambito!
"usare rigorosamente"; var mr_global = "Mr Global"; function foo () var mrs_local = "Mrs Local"; console.log ("Posso vedere" + mr_global + "e" + mrs_local + "."); function bar () console.log ("Posso anche vedere" + mr_global + "e" + mrs_local + ".");  foo (); // Funziona come previsto try console.log ("Ma / I / can not see" + mrs_local + ".");  catch (err) console.log ("Hai appena ottenuto un" + err + ".");  let foo = "foo"; const bar = "bar"; console.log ("Posso usare" + foo + bar + "nel suo blocco ...");  prova console.log ("Ma non al di fuori di esso.");  catch (err) console.log ("Hai appena ricevuto un altro" + err + ".");  // Genera ReferenceError! console.log ("Notare che" + err + "non esiste al di fuori di" catch "!") 

Lo snippet qui sopra mostra tutti e tre i meccanismi dello scope. Puoi eseguirlo in Nodo o Firefox, ma con Chrome non funziona permettere, ancora.

Parleremo di ognuno di questi in modo squisito. Iniziamo con uno sguardo dettagliato su come JavaScript calcola quali variabili appartengono a quale ambito.

Il processo di compilazione: una vista a volo d'uccello

Quando esegui un pezzo di JavaScript, succedono due cose per farlo funzionare.

  1. Innanzitutto, la tua fonte viene compilata.
  2. Quindi, il codice compilato viene eseguito.

Durante il compilazione passo, il motore JavaScript:

  1. prende nota di tutti i nomi delle variabili
  2. li registra nella misura appropriata
  3. riserva spazio per i loro valori

È solo durante esecuzione che il motore JavaScript imposta effettivamente il valore dei riferimenti variabili uguale ai loro valori di assegnazione. Fino ad allora, lo sono non definito

Passaggio 1: Compilazione

// Posso usare first_name ovunque in questo programma var first_name = "Peleke"; function popup (first_name) // Posso usare solo last_name all'interno di questa funzione var last_name = "Sengstacke"; alert (first_name + "+ last_name); popup (first_name);

Passiamo attraverso ciò che fa il compilatore.

In primo luogo, legge la linea var first_name = "Peleke". Successivamente, determina cosa scopo per salvare la variabile in. Poiché siamo al livello più alto della sceneggiatura, ci rendiamo conto che siamo nel portata globale. Quindi, salva la variabile nome di battesimo all'ambito globale e inizializza il suo valore a non definito.

In secondo luogo, il compilatore legge la riga con funzione popup (first_name). Perché il funzione parola chiave è la prima cosa sulla linea, crea un nuovo ambito per la funzione, registra la definizione della funzione nell'ambito globale e fa capolino all'interno per trovare le dichiarazioni delle variabili.

Abbastanza sicuro, il compilatore ne trova uno. Dal momento che abbiamo var last_name = "Sengstacke" nella prima riga della nostra funzione, il compilatore salva la variabile cognome al ambito di applicazione apparire-non allo scopo globale e ne stabilisce il valore non definito

Poiché non ci sono più dichiarazioni variabili all'interno della funzione, il compilatore fa un passo indietro nello scope globale. E poiché non ci sono più dichiarazioni variabili , questa fase è fatta.

Nota che non abbiamo effettivamente correre ancora niente. Il compito del compilatore a questo punto è solo per assicurarsi che conosca il nome di tutti; non importa che cosa loro fanno. 

A questo punto, il nostro programma sa che:

  1. C'è una variabile chiamata nome di battesimo nell'ambito globale.
  2. C'è una funzione chiamata apparire nell'ambito globale.
  3. C'è una variabile chiamata cognome nell'ambito di apparire.
  4. I valori di entrambi nome di battesimocognome siamo non definito.

Non gli importa che abbiamo assegnato quei valori di variabili altrove nel nostro codice. Il motore si occupa di quello dentro esecuzione.

Step 2: Esecuzione

Durante il prossimo passaggio, il motore legge di nuovo il nostro codice, ma questa volta, esegue esso. 

In primo luogo, legge la linea, var first_name = "Peleke". Per fare ciò, il motore cerca la variabile chiamata nome di battesimo. Poiché il compilatore ha già registrato una variabile con quel nome, il motore la trova e ne imposta il valore "Peleke".

Successivamente, legge la linea, funzione popup (first_name). Dal momento che non lo siamo esecuzione la funzione qui, il motore non è interessato e salta sopra.

Finalmente, legge la linea popup (first_name). Da quando noi siamo eseguendo una funzione qui, il motore:

  1. cerca il valore di apparire
  2. cerca il valore di nome di battesimo
  3. esegue apparire come funzione, passando il valore di nome di battesimo come parametro

Quando viene eseguito apparire, passa attraverso questo stesso processo, ma questa volta all'interno della funzione apparire. It:

  1. cerca la variabile denominata cognome
  2. imposta cognomeIl valore è uguale a "Sengstacke"
  3. guarda su mettere in guardia, eseguendolo come una funzione con "Peleke Sengstacke" come il suo parametro

Si scopre che c'è molto di più in corso di discussione di quanto avremmo potuto pensare!

Ora che capisci come JavaScript legge e gestisce il codice che scrivi, siamo pronti ad affrontare qualcosa di un po 'più vicino a casa: come funziona il sollevamento.

Sollevamento sotto il microscopio

Iniziamo con un po 'di codice.

bar(); function bar () if (! foo) alert (foo + "? Questo è strano ...");  var foo = "bar";  rotto(); // TypeError! var broken = function () alert ("Questo avviso non verrà visualizzato!"); 

Se esegui questo codice, noterai tre cose:

  1. tu può fare riferimento a foo prima di assegnarlo, ma il suo valore è non definito.
  2. tu può chiamata rotto prima di definirlo, ma otterrai un TypeError.
  3. tu può chiamata bar prima di definirlo, e funziona come desiderato.

sollevamento si riferisce al fatto che JavaScript rende disponibili tutti i nostri nomi di variabili dichiarati ovunque nei loro scopi, incluso prima noi li assegniamo.

I tre casi nello snippet sono i tre che devi conoscere nel tuo codice, quindi li esamineremo uno per uno.

Dichiarazione di sollevamento delle variabili

Ricorda, quando il compilatore JavaScript legge una riga come var foo = "bar", esso:

  1. registra il nome foo allo scopo più vicino
  2. imposta il valore di foo a undefined

Il motivo per cui possiamo usare foo prima di assegnarlo è perché, quando il motore cerca la variabile con quel nome, lo fa fa esistere. Questo è il motivo per cui non getta a ReferenceError

Invece, ottiene il valore non definito, e cerca di usarlo per fare tutto ciò che hai chiesto. Di solito, questo è un bug.

Tenendolo presente, potremmo immaginare ciò che JavaScript vede nella nostra funzione bar è più come questo:

barra delle funzioni () var foo; // undefined if (! foo) //! undefined è true, quindi alert (foo + "? Questo è strano ...");  foo = "bar"; 

Questo è il Prima regola di sollevamento, se vuoi: le variabili sono disponibili per tutto il loro scopo, ma hanno il valore non definito fino a quando il tuo codice non viene assegnato a loro.

Un idioma JavaScript comune è quello di scrivere tutto il tuo var dichiarazioni nella parte superiore del loro ambito, invece di dove le usi per la prima volta. Per parafrasare Doug Crockford, questo aiuta il tuo codice leggere più piace piste.

Quando ci pensi, questo ha senso. È abbastanza chiaro perché bar si comporta come quando scriviamo il nostro codice nel modo in cui JavaScript lo legge, non è vero? Quindi, perché non scrivere così? tutti il tempo?  

Espressioni di funzioni di sollevamento

Il fatto che abbiamo ottenuto un TypeError quando abbiamo cercato di eseguire rotto prima che lo definissimo è solo un caso speciale della Prima regola di sollevamento.

Abbiamo definito una variabile, chiamata rotto, che il compilatore registra nello scope globale e imposta uguale a non definito. Quando proviamo a eseguirlo, il motore cerca il valore di rotto, trova che sia non definito, e cerca di eseguire non definito come una funzione.

Ovviamente, non definito non è una funzione, ecco perché otteniamo un TypeError!

Dichiarazione di sollevamento

Infine, ricorda che siamo stati in grado di chiamare bar prima che lo definissimo. Questo è dovuto al Seconda regola di sollevamento: Quando il compilatore JavaScript trova una dichiarazione di funzione, ne fa il nome e definizione disponibile nella parte superiore del suo ambito. Riscrivi il nostro codice ancora una volta:

function bar () if (! foo) alert (foo + "? Questo è strano ...");  var foo = "bar";  var broken; // bar non definito (); // la barra è già definita, viene eseguita correttamente broken (); // Impossibile eseguire undefined! broken = function () alert ("Questo avviso non verrà visualizzato!"); 

 Di nuovo, ha molto più senso quando tu Scrivi come JavaScript legge, non pensi?

Revisionare:

  1. I nomi di entrambe le dichiarazioni di variabili e le espressioni di funzione sono disponibili in tutto il loro ambito, ma il loro valori siamo non definito fino all'assegnazione.
  2. I nomi e le definizioni delle dichiarazioni di funzione sono disponibili in tutto il loro ambito, anche prima le loro definizioni.

Ora diamo un'occhiata a due nuovi strumenti che funzionano in modo leggermente diverso: permettereconst.

permettereconst, e la zona morta temporale

diversamente da var dichiarazioni, variabili dichiarate con permettere e const non essere issato dal compilatore.

Almeno, non esattamente. 

Ricorda come siamo stati in grado di chiamare rotto, ma ho ottenuto un TypeError perché abbiamo cercato di eseguire non definito? Se avessimo definito rotto con permettere, avremmo ottenuto un ReferenceError, anziché:

"usare rigorosamente"; // Devi usare "strict" per provare questo in Node broken (); // ReferenceError! let broken = function () alert ("Questo avviso non verrà visualizzato!"); 

Quando il compilatore JavaScript registra le variabili ai loro ambiti nel suo primo passaggio, tratta permettereconst diversamente da come funziona var

Quando trova un var dichiarazione, registriamo il nome della variabile nel suo ambito e inizializziamo immediatamente il suo valore su non definito.

Con permettere, comunque, il compilatore fa registra la variabile nel suo ambito, ma noninizializzare il suo valore a non definito. Invece, lascia la variabile non inizializzata, fino a il motore esegue la tua dichiarazione di incarico. L'accesso al valore di una variabile non inizializzata genera a ReferenceError, che spiega perché il frammento sopra getta quando lo eseguiamo.

Lo spazio tra l'inizio della parte superiore dello scopo di a permettere dichiarazione e la dichiarazione di assegnazione è chiamata il Zona Morta Temporale. Il nome deriva dal fatto che, nonostante il motore conosce su una variabile chiamata foo in cima allo scopo di bar, la variabile è "morta", perché non ha un valore.

... Anche perché ucciderà il tuo programma se proverai ad usarlo prima.

Il const la parola chiave funziona allo stesso modo di permettere, con due differenze chiave:

  1. tu dovere assegna un valore quando dichiari con const.
  2. tu non può riassegnare i valori a una variabile dichiarata con const.

Questo lo garantisce const volontà sempreavere il valore inizialmente assegnato ad esso.

// Questo è legale const React = require ('react'); // Questo non è assolutamente legale const crypto; crypto = require ('crypto');

Block Scope

permettereconst sono diversi da var in un altro modo: la dimensione dei loro scopi.

Quando dichiari una variabile con var, è visibile in alto sulla catena dell'ambito il più possibile, in genere, nella parte superiore della dichiarazione di funzione più vicina o nell'ambito globale, se lo dichiari nel livello più alto. 

Quando dichiari una variabile con permettereconst, tuttavia, è visibile come localmente il più possibile-solo all'interno del blocco più vicino.

UN bloccare è una sezione di codice impostata da parentesi graffe, come si vede con Se/altro blocchi, per loop, e in blocchi di codice esplicitamente "bloccati", come in questo frammento.

"usare rigorosamente"; let foo = "foo"; if (foo) const bar = "bar"; var foobar = foo + bar; console.log ("Posso vedere" + bar + "in questo blocco.");  prova console.log ("Posso vedere" + foo + "in questo blocco, ma non" + bar + ".");  catch (err) console.log ("Hai ottenuto un" + err + ".");  prova console.log (pippo + barra); // Getta a causa di "pippo", ma entrambi sono indefiniti catch (err) console.log ("Hai appena ottenuto un" + err + ".");  console.log (foobar); // Funziona bene

Se dichiari una variabile con constpermettere all'interno di un blocco, lo è solo visibile all'interno del blocco, e solo dopo averlo assegnato.

Una variabile dichiarata con var, tuttavia, è visibile il più lontano possibile-in questo caso, nell'ambito globale.

Se ti interessano i dettagli nitty di permettereconst, scopri cosa ha da dire il Dr Rauschmayer su di loro in Exploring ES6: Variables and Scoping, e dai un'occhiata alla documentazione MDN su di loro.  

Lessicale Questo E funzioni freccia

Sulla superficie, Questo non sembra avere molto a che fare con la portata. E, in effetti, JavaScript lo fa non risolvere il significato di Questo secondo le regole di portata di cui abbiamo parlato qui.

Almeno, non di solito. JavaScript, notoriamente, lo fa non risolvere il significato del Questo parola chiave basata su dove lo hai usato:

var foo = nome: 'Foo', lingue: ['Spanish', 'French', 'Italian'], speak: function speak () this.languages.forEach (function (language) console.log (this. nome + "parla" + lingua + ".");); foo.speak ();

La maggior parte di noi si aspetterebbe Questo intendere foo dentro il per ciascuno loop, perché questo è ciò che significava proprio al di fuori di esso. In altre parole, ci aspetteremmo che JavaScript risolva il significato di Questo lessicalmente.

Ma non è così.

Invece, crea a nuovo Questo all'interno di ogni funzione che definisci e decide in base a cosa significa Come tu chiami la funzione no dove lo hai definito.

Questo primo punto è simile al caso della ridefinizione qualunque variabile in un ambito figlio:

function foo () var bar = "bar"; function baz () // Riutilizzare nomi di variabili come questo è chiamato "shadowing" var bar = "BAR"; console.log (bar); // BAR baz ();  foo (); // BAR

Sostituire bar con Questo, e l'intera cosa dovrebbe chiarirsi all'istante!

Tradizionalmente, ottenendo Questo lavorare come ci aspettiamo che le semplici vecchie variabili con scope lessicale funzionino richiede uno di due soluzioni:

var foo = nome: 'Foo', lingue: ['Spanish', 'French', 'Italian'], speak_self: function speak_s () var self = this; self.languages.forEach (funzione (lingua) console.log (self.name + "parla" + lingua + ".");), speak_bound: funzione speak_b () this.languages.forEach (funzione (lingua ) console.log (this.name + "speaks" + language + "."); .bind (foo)); // Più comunemente: .bind (this); ;

Nel speak_self, salviamo il significato di Questo alla variabile se stesso, e usare quello variabile per ottenere il riferimento che vogliamo. Nel speak_bound, noi usiamo legare a permanentemente punto Questo ad un dato oggetto.

ES2015 ci offre una nuova alternativa: le funzioni di freccia.

A differenza delle funzioni "normali", le funzioni freccia lo fanno non ombreggia il loro scopo genitore Questo valore impostando il proprio. Piuttosto, risolvono il suo significato lessicalmente. 

In altre parole, se usi Questo in una funzione a freccia, JavaScript cerca il suo valore come farebbe con qualsiasi altra variabile.

Innanzitutto, controlla l'ambito locale per a Questo valore. Poiché le funzioni a freccia non ne impostano una, non la troverà. Successivamente, controlla il genitore portata per a Questo valore. Se ne trova uno, lo userà, invece.

Questo ci consente di riscrivere il codice qui sopra in questo modo:

var foo = nome: 'Foo', lingue: ['Spanish', 'French', 'Italian'], speak: function speak () this.languages.forEach ((lingua) => console.log (questo .name + "parla" + lingua + "."););   

Se desideri maggiori dettagli sulle funzioni delle frecce, dai un'occhiata al corso Envato Tuts + dell'istruttore Dan Wellman su JavaScript ES6 Fundamentals, nonché alla documentazione MDN sulle funzioni delle frecce.

Conclusione

Abbiamo coperto molto terreno finora! In questo articolo, hai imparato che:

  • Le variabili sono registrate ai loro ambiti durante compilazione, e associati ai loro valori di assegnazione durante esecuzione.
  • Riferendosi alle variabili dichiarate conpermettereconst prima dell'assegnazione lancia a ReferenceError, e che tali variabili sono circoscritte al blocco più vicino.
  • Funzioni della frecciaci permettono di ottenere il legame lessicale di Questo, e bypassare il binding dinamico tradizionale.

Hai anche visto le due regole di sollevamento:

  • Il Prima regola di sollevamento: Quella funzione espressioni e var le dichiarazioni sono disponibili in tutti gli ambiti in cui sono definite, ma hanno il valore non definito fino a quando non vengono eseguite le istruzioni di assegnazione.
  • Il Seconda regola di sollevamento: Che i nomi delle dichiarazioni di funzione e i loro corpi sono disponibili in tutti gli ambiti in cui sono definiti.

Un buon passo successivo è quello di usare la tua nuova conoscenza degli ambiti di JavaScript per avvolgere la tua mente intorno alle chiusure. Per questo, dai un'occhiata a Scope & Chiusure di Kyle Simpson.

Infine, c'è ancora molto da dire Questo di quanto sia stato in grado di coprire qui. Se la parola chiave sembra ancora tanta magia nera, dai un'occhiata a questo e ai prototipi degli oggetti per capirlo.

Nel frattempo, prendi ciò che hai imparato e scrivi meno bug!

Scopri JavaScript: la guida completa

Abbiamo creato una guida completa per aiutarti a imparare JavaScript, sia che tu stia appena iniziando come sviluppatore web o che desideri esplorare argomenti più avanzati.