Espressioni di Valutazione in JavaScript

Concetti Chiave
  • Le espressioni di valutazione in JavaScript sono utilizzate per valutare stringhe di codice JavaScript e produrre valori.
  • La funzione eval() è utilizzata per eseguire codice JavaScript rappresentato come stringa.
  • L'uso di eval() può comportare rischi per la sicurezza e dovrebbe essere evitato quando possibile.
  • eval() può influenzare le prestazioni del codice, poiché disabilita alcune ottimizzazioni.
  • eval() può rendere il codice più difficile da comprendere e mantenere.

Espressioni di Valutazione

Come molti linguaggi di programmazione interpretati, JavaScript ha la capacità di interpretare stringhe di codice sorgente JavaScript, valutandole per produrre un valore. JavaScript fa questo con la funzione globale eval():

eval("3+2")    // => 5

La valutazione dinamica di stringhe di codice sorgente è una caratteristica potente del linguaggio che non è quasi mai necessaria nella pratica. Se ci troviamo a usare eval(), dovremmo pensare attentamente se abbiamo davvero bisogno di usarla. In particolare, eval() può essere una falla di sicurezza, e non dovremmo mai passare alcuna stringa derivata da input dell'utente a eval(). Con un linguaggio complicato come JavaScript, non c'è modo di sanificare l'input dell'utente per renderlo sicuro da usare con eval(). A causa di questi problemi di sicurezza, alcuni server web usano l'header HTTP "Content-Security-Policy" per disabilitare eval() per un intero sito web.

Le sottosezioni che seguono spiegano l'uso base di eval() e spiegano due versioni ristrette di essa che hanno meno impatto sull'ottimizzatore.

Il problema di eval

eval() è una funzione, ma è inclusa nella sezione sulle espressioni perché avrebbe dovuto essere un operatore. Le prime versioni del linguaggio definivano una funzione eval(), e da allora, i progettisti del linguaggio e gli scrittori di interpreti hanno posto restrizioni su di essa che la rendono sempre più simile a un operatore. Gli interpreti JavaScript moderni eseguono molte analisi e ottimizzazioni del codice. In generale, se una funzione chiama eval(), l'interprete non può ottimizzare quella funzione. Il problema con la definizione di eval() come funzione è che può essere assegnata ad altri nomi:

let f = eval;
let g = f;

Se questo è consentito, allora l'interprete non può sapere con certezza quali funzioni chiamano eval(), quindi non può ottimizzare in modo aggressivo. Questo problema avrebbe potuto essere evitato se eval() fosse stato un operatore (e una parola riservata). Impareremo delle restrizioni poste su eval() per renderla più simile a un operatore.

Come funziona eval()

eval() si aspetta un argomento. Se passiamo un valore diverso da una stringa, restituisce semplicemente quel valore. Se passiamo una stringa, tenta di analizzare la stringa come codice JavaScript, lanciando un SyntaxError se fallisce. Se analizza con successo la stringa, allora valuta il codice e restituisce il valore dell'ultima espressione o istruzione nella stringa o undefined se l'ultima espressione o istruzione non aveva valore. Se la stringa valutata lancia un'eccezione, quell'eccezione si propaga dalla chiamata a eval().

La cosa fondamentale riguardo a eval() (quando invocata in questo modo) è che usa l'ambiente delle variabili del codice che la chiama. Cioè, cerca i valori delle variabili e definisce nuove variabili e funzioni nello stesso modo in cui fa il codice locale. Se una funzione definisce una variabile locale x e poi chiama eval("x"), otterrà il valore della variabile locale. Se chiama eval("x=1"), cambia il valore della variabile locale. E se la funzione chiama eval("var y = 3;"), dichiara una nuova variabile locale y. D'altra parte, se la stringa valutata usa let o const, la variabile o costante dichiarata sarà locale alla valutazione e non sarà definita nell'ambiente chiamante.

Allo stesso modo, una funzione può dichiarare una funzione locale con codice come questo:

eval("function f() { return x+1; }");

Se chiamiamo eval() dal codice di livello superiore, opera su variabili globali e funzioni globali, ovviamente.

Notiamo che la stringa di codice che passiamo a eval() deve avere senso sintattico di per sé: non possiamo usarla per incollare frammenti di codice in una funzione. Non ha senso scrivere eval("return;"), per esempio, perché return è legale solo all'interno delle funzioni, e il fatto che la stringa valutata usa lo stesso ambiente delle variabili della funzione chiamante non la rende parte di quella funzione. Se la nostra stringa avrebbe senso come script autonomo (anche uno molto breve come x=0), è legale passarla a eval(). Altrimenti, eval() lancerà un SyntaxError.

eval Globale

È la capacità di eval() di modificare variabili locali che risulta così problematica per gli ottimizzatori JavaScript. Come soluzione alternativa, però, gli interpreti semplicemente eseguono meno ottimizzazioni su qualsiasi funzione che chiama eval(). Ma cosa dovrebbe fare un interprete JavaScript, tuttavia, se uno script definisce un alias per eval() e poi chiama quella funzione con un altro nome? La specifica JavaScript dichiara che quando eval() viene invocata con qualsiasi nome diverso da "eval", dovrebbe valutare la stringa come se fosse codice globale di livello superiore. Il codice valutato può definire nuove variabili globali o funzioni globali, e può impostare variabili globali, ma non userà o modificherà alcuna variabile locale alla funzione chiamante, e non interferirà, quindi, con le ottimizzazioni locali.

Una "eval diretta" è una chiamata alla funzione eval() con un'espressione che usa il nome esatto e non qualificato "eval" (che sta iniziando a sembrare una parola riservata). Le chiamate dirette a eval() usano l'ambiente delle variabili del contesto chiamante. Qualsiasi altra chiamata, ad esempio una chiamata indiretta, usa l'oggetto globale come suo ambiente delle variabili e non può leggere, scrivere o definire variabili o funzioni locali. (Sia le chiamate dirette che quelle indirette possono definire nuove variabili solo con var. Gli usi di let e const dentro una stringa valutata creano variabili e costanti che sono locali alla valutazione e non alterano l'ambiente chiamante o globale).

Il seguente codice dimostra quanto appena detto:

const evalGlobale = eval;               // Usare un altro nome fa una eval globale
let x = "globale", y = "globale";       // Due variabili globali
function f() {                          // Questa funzione fa una eval locale
    let x = "locale";                   // Definisce una variabile locale
    eval("x += 'modificata';");         // L'eval diretta imposta la variabile locale
    return x;                           // Restituisce la variabile locale modificata
}
function g() {                          // Questa funzione fa una eval globale
    let y = "locale";                   // Una variabile locale
    evalGlobale("y += 'modificata';");  // L'eval indiretta imposta la variabile globale
    return y;                           // Restituisce la variabile locale non modificata
}
console.log(f(), x); // Variabile locale modificata: stampa "localemodificata globale":
console.log(g(), y); // Variabile globale modificata: stampa "locale globalemodificata":

Notiamo che la capacità di fare una eval globale non è solo un adattamento alle esigenze dell'ottimizzatore; è in realtà una caratteristica tremendamente utile che ci permette di eseguire stringhe di codice come se fossero script indipendenti di livello superiore. Come notato all'inizio di questa sezione, è raro avere veramente bisogno di valutare una stringa di codice. Ma se troviamo necessario farlo, è più probabile che vogliamo fare una eval globale piuttosto che una eval locale.

eval in modalità strict

La modalità strict impone ulteriori restrizioni sul comportamento della funzione eval() e persino sull'uso dell'identificatore "eval". Quando eval() viene chiamata da codice in modalità strict, o quando la stringa di codice da valutare inizia essa stessa con una direttiva "use strict", allora eval() esegue una valutazione locale con un ambiente di variabili privato. Questo significa che in modalità strict, il codice valutato può interrogare e impostare variabili locali, ma non può definire nuove variabili o funzioni nell'ambito locale.

Inoltre, la modalità strict rende eval() ancora più simile a un operatore rendendo effettivamente "eval" una parola riservata. Non è consentito sovrascrivere la funzione eval() con un nuovo valore. E non è consentito dichiarare una variabile, funzione, parametro di funzione o parametro di blocco catch con il nome "eval".