Codice JavaScript buggy: i 10 errori più comuni commessi dagli sviluppatori JavaScript
Pubblicato: 2022-03-11Oggi, JavaScript è al centro di quasi tutte le moderne applicazioni web. Gli ultimi anni, in particolare, hanno assistito alla proliferazione di un'ampia gamma di potenti librerie e framework basati su JavaScript per lo sviluppo di applicazioni a pagina singola (SPA), grafica e animazione e persino piattaforme JavaScript lato server. JavaScript è diventato davvero onnipresente nel mondo dello sviluppo di app web ed è quindi un'abilità sempre più importante da padroneggiare.
A prima vista, JavaScript può sembrare abbastanza semplice. E in effetti, creare funzionalità JavaScript di base in una pagina Web è un compito abbastanza semplice per qualsiasi sviluppatore di software esperto, anche se non conosce JavaScript. Eppure il linguaggio è significativamente più sfumato, potente e complesso di quanto si potrebbe inizialmente pensare. In effetti, molte delle sottigliezze di JavaScript portano a una serie di problemi comuni che ne impediscono il funzionamento - 10 dei quali discutiamo qui - di cui è importante essere consapevoli ed evitare nella propria ricerca per diventare un maestro sviluppatore JavaScript.
Errore comune n. 1: riferimenti errati a this
Una volta ho sentito un comico dire:
Non sono proprio qui, perché cosa c'è qui, oltre a lì, senza la 't'?
Quella battuta in molti modi caratterizza il tipo di confusione che spesso esiste per gli sviluppatori riguardo a this
parola chiave di JavaScript. Voglio dire, è davvero this
o è qualcos'altro? O è indefinito?
Poiché le tecniche di codifica JavaScript e i modelli di progettazione sono diventati sempre più sofisticati nel corso degli anni, c'è stato un corrispondente aumento nella proliferazione di ambiti autoreferenziali all'interno di callback e chiusure, che sono una fonte abbastanza comune di "questa/quella confusione".
Considera questo frammento di codice di esempio:
Game.prototype.restart = function () { this.clearLocalStorage(); this.timer = setTimeout(function() { this.clearBoard(); // what is "this"? }, 0); };
L'esecuzione del codice precedente genera il seguente errore:
Uncaught TypeError: undefined is not a function
Come mai?
È tutta una questione di contesto. Il motivo per cui ottieni l'errore sopra è perché, quando invochi setTimeout()
, stai effettivamente invocando window.setTimeout()
. Di conseguenza, la funzione anonima passata a setTimeout()
viene definita nel contesto dell'oggetto window
, che non ha il metodo clearBoard()
.
Una soluzione tradizionale, conforme al vecchio browser, consiste semplicemente nel salvare il tuo riferimento a this
in una variabile che può quindi essere ereditata dalla chiusura; per esempio:
Game.prototype.restart = function () { this.clearLocalStorage(); var self = this; // save reference to 'this', while it's still this! this.timer = setTimeout(function(){ self.clearBoard(); // oh OK, I do know who 'self' is! }, 0); };
In alternativa, nei browser più recenti, puoi utilizzare il metodo bind()
per passare il riferimento corretto:
Game.prototype.restart = function () { this.clearLocalStorage(); this.timer = setTimeout(this.reset.bind(this), 0); // bind to 'this' }; Game.prototype.reset = function(){ this.clearBoard(); // ahhh, back in the context of the right 'this'! };
Errore comune n. 2: pensare che ci sia un ambito a livello di blocco
Come discusso nella nostra Guida all'assunzione di JavaScript, una fonte comune di confusione tra gli sviluppatori JavaScript (e quindi una fonte comune di bug) presuppone che JavaScript crei un nuovo ambito per ogni blocco di codice. Sebbene questo sia vero in molte altre lingue, non è vero in JavaScript. Si consideri, ad esempio, il seguente codice:
for (var i = 0; i < 10; i++) { /* ... */ } console.log(i); // what will this output?
Se ritieni che la chiamata console.log()
restituisca undefined
o generi un errore, hai indovinato in modo errato. Che ci crediate o no, produrrà 10
. Come mai?
Nella maggior parte degli altri linguaggi, il codice sopra porterebbe a un errore perché la "vita" (cioè l'ambito) della variabile i
sarebbe limitata al blocco for
. In JavaScript, tuttavia, non è così e la variabile i
rimane nell'ambito anche dopo il completamento del ciclo for
, conservando il suo ultimo valore dopo l'uscita dal ciclo. (Questo comportamento è noto, per inciso, come sollevamento variabile).
Vale la pena notare, tuttavia, che il supporto per gli ambiti a livello di blocco si sta facendo strada in JavaScript attraverso la parola chiave new let
. La parola chiave let
è già disponibile in JavaScript 1.7 ed è prevista per diventare una parola chiave JavaScript ufficialmente supportata a partire da ECMAScript 6.
Nuovo in JavaScript? Leggi gli ambiti, i prototipi e altro ancora.
Errore comune n. 3: creare perdite di memoria
Le perdite di memoria sono problemi JavaScript quasi inevitabili se non stai programmando consapevolmente per evitarli. Esistono numerosi modi in cui possono verificarsi, quindi evidenzieremo solo un paio delle loro occorrenze più comuni.
Perdita di memoria Esempio 1: riferimenti penzolanti a oggetti defunti
Considera il seguente codice:
var theThing = null; var replaceThing = function () { var priorThing = theThing; // hold on to the prior thing var unused = function () { // 'unused' is the only place where 'priorThing' is referenced, // but 'unused' never gets invoked if (priorThing) { console.log("hi"); } }; theThing = { longStr: new Array(1000000).join('*'), // create a 1MB object someMethod: function () { console.log(someMessage); } }; }; setInterval(replaceThing, 1000); // invoke `replaceThing' once every second
Se esegui il codice sopra e monitori l'utilizzo della memoria, scoprirai di avere un'enorme perdita di memoria, perdendo un megabyte intero al secondo! E anche un GC manuale non aiuta. Quindi sembra che stiamo perdendo longStr
ogni volta che viene chiamato replaceThing
. Ma perché?
Esaminiamo le cose più in dettaglio:
Ogni oggetto theThing
contiene il proprio oggetto longStr
da 1 MB. Ogni secondo, quando chiamiamo replaceThing
, mantiene un riferimento all'oggetto theThing
precedente in priorThing
. Ma non penseremmo comunque che questo sarebbe un problema, poiché ogni volta, priorThing
a cui si fa riferimento in precedenza verrebbe dereferenziato (quando priorThing
viene reimpostato tramite priorThing = theThing;
). E inoltre, è referenziato solo nel corpo principale di replaceThing
e nella funzione unused
che, di fatto, non è mai utilizzata.
Quindi ancora una volta ci chiediamo perché c'è una perdita di memoria qui!?
Per capire cosa sta succedendo, dobbiamo capire meglio come funzionano le cose in JavaScript sotto il cofano. Il modo tipico in cui vengono implementate le chiusure è che ogni oggetto funzione ha un collegamento a un oggetto in stile dizionario che rappresenta il suo ambito lessicale. Se entrambe le funzioni definite all'interno replaceThing
utilizzassero effettivamente priorThing
, sarebbe importante che entrambe ottengano lo stesso oggetto, anche se priorThing
viene assegnato a più e più volte, quindi entrambe le funzioni condividono lo stesso ambiente lessicale. Ma non appena una variabile viene utilizzata da qualsiasi chiusura, finisce nell'ambiente lessicale condiviso da tutte le chiusure in quell'ambito. E quella piccola sfumatura è ciò che porta a questa perdita di memoria nodosa. (Maggiori dettagli su questo sono disponibili qui.)
Perdita di memoria Esempio 2: Riferimenti circolari
Considera questo frammento di codice:
function addClickHandler(element) { element.click = function onClick(e) { alert("Clicked the " + element.nodeName) } }
Qui, onClick
ha una chiusura che mantiene un riferimento element
(tramite element.nodeName
). Assegnando anche onClick
a element.click
, viene creato il riferimento circolare; cioè: element
-> onClick
-> element
-> onClick
-> element
…
È interessante notare che, anche se l' element
viene rimosso dal DOM, l'autoreferenza circolare sopra impedirebbe la raccolta di element
e onClick
e, quindi, una perdita di memoria.
Evitare le perdite di memoria: cosa devi sapere
La gestione della memoria di JavaScript (e, in particolare, la raccolta dei rifiuti) si basa in gran parte sulla nozione di raggiungibilità degli oggetti.
Si presume che i seguenti oggetti siano raggiungibili e sono noti come "radici":
- Oggetti referenziati da qualsiasi punto dello stack di chiamate corrente (ovvero, tutte le variabili e i parametri locali nelle funzioni attualmente richiamate e tutte le variabili nell'ambito di chiusura)
- Tutte le variabili globali
Gli oggetti vengono mantenuti in memoria almeno fintanto che sono accessibili da una qualsiasi delle radici tramite un riferimento o una catena di riferimenti.
C'è un Garbage Collector (GC) nel browser che pulisce la memoria occupata da oggetti irraggiungibili; ovvero, gli oggetti verranno rimossi dalla memoria se e solo se il GC ritiene che siano irraggiungibili. Sfortunatamente, è abbastanza facile ritrovarsi con oggetti "zombi" defunti che in realtà non sono più in uso ma che il GC pensa ancora siano "raggiungibili".
Errore comune n. 4: confusione sull'uguaglianza
Una delle comodità di JavaScript è che costringerà automaticamente qualsiasi valore a cui viene fatto riferimento in un contesto booleano in un valore booleano. Ma ci sono casi in cui questo può essere tanto confuso quanto conveniente. Alcuni dei seguenti, ad esempio, sono stati conosciuti per mordere molti sviluppatori JavaScript:
// All of these evaluate to 'true'! console.log(false == '0'); console.log(null == undefined); console.log(" \t\r\n" == 0); console.log('' == 0); // And these do too! if ({}) // ... if ([]) // ...
Per quanto riguarda gli ultimi due, nonostante siano vuoti (il che potrebbe indurre a credere che valuterebbero false
), sia {}
che []
sono in effetti oggetti e qualsiasi oggetto sarà costretto a un valore booleano di true
in JavaScript, coerente con la specifica ECMA-262.
Come dimostrano questi esempi, le regole della coercizione di tipo a volte possono essere chiare come il fango. Di conseguenza, a meno che non si desideri esplicitamente la coercizione del tipo, in genere è meglio usare ===
e !==
(anziché ==
e !=
), in modo da evitare effetti collaterali non intenzionali della coercizione del tipo. ( ==
e !=
eseguono automaticamente la conversione del tipo quando si confrontano due cose, mentre ===
e !==
fanno lo stesso confronto senza la conversione del tipo.)
E completamente come un punto collaterale, ma dal momento che stiamo parlando di coercizione di tipo e confronti, vale la pena ricordare che confrontare NaN
con qualsiasi cosa (anche NaN
!) Restituirà sempre false
. Pertanto non è possibile utilizzare gli operatori di uguaglianza ( ==
, ===
, !=
, !==
) per determinare se un valore è NaN
o meno. Invece, usa la funzione globale isNaN()
integrata:
console.log(NaN == NaN); // false console.log(NaN === NaN); // false console.log(isNaN(NaN)); // true
Errore comune n. 5: manipolazione DOM inefficiente
JavaScript rende relativamente facile manipolare il DOM (cioè aggiungere, modificare e rimuovere elementi), ma non fa nulla per promuovere una tale operazione in modo efficiente.

Un esempio comune è il codice che aggiunge una serie di elementi DOM uno alla volta. L'aggiunta di un elemento DOM è un'operazione costosa. Il codice che aggiunge più elementi DOM consecutivamente è inefficiente e probabilmente non funzionerà bene.
Un'alternativa efficace quando è necessario aggiungere più elementi DOM consiste nell'utilizzare invece frammenti di documento, migliorando così sia l'efficienza che le prestazioni.
Per esempio:
var div = document.getElementsByTagName("my_div"); var fragment = document.createDocumentFragment(); for (var e = 0; e < elems.length; e++) { // elems previously set to list of elements fragment.appendChild(elems[e]); } div.appendChild(fragment.cloneNode(true));
Oltre all'efficienza intrinsecamente migliorata di questo approccio, la creazione di elementi DOM collegati è costosa, mentre crearli e modificarli mentre sono scollegati e quindi collegarli produce prestazioni molto migliori.
Errore comune n. 6: uso scorretto delle definizioni delle funzioni all'interno for
cicli for
Considera questo codice:
var elements = document.getElementsByTagName('input'); var n = elements.length; // assume we have 10 elements for this example for (var i = 0; i < n; i++) { elements[i].onclick = function() { console.log("This is element #" + i); }; }
Sulla base del codice sopra, se ci fossero 10 elementi di input, facendo clic su uno di essi verrebbe visualizzato "Questo è l'elemento n. 10"! Questo perché, nel momento in cui onclick
viene invocato per uno qualsiasi degli elementi, il ciclo for precedente sarà completato e il valore di i
sarà già 10 (per tutti ).
Ecco come possiamo correggere i problemi di codice di cui sopra, tuttavia, per ottenere il comportamento desiderato:
var elements = document.getElementsByTagName('input'); var n = elements.length; // assume we have 10 elements for this example var makeHandler = function(num) { // outer function return function() { // inner function console.log("This is element #" + num); }; }; for (var i = 0; i < n; i++) { elements[i].onclick = makeHandler(i+1); }
In questa versione rivista del codice, makeHandler
viene eseguito immediatamente ogni volta che passiamo attraverso il ciclo, ricevendo ogni volta il valore allora corrente di i+1
e legandolo a una variabile num
con ambito. La funzione esterna restituisce la funzione interna (che utilizza anche questa variabile num
con ambito) e l' onclick
dell'elemento è impostato su quella funzione interna. Ciò garantisce che ogni onclick
riceva e utilizzi il valore i
corretto (tramite la variabile num
con ambito).
Errore comune n. 7: incapacità di sfruttare correttamente l'eredità prototipica
Una percentuale sorprendentemente alta di sviluppatori JavaScript non riesce a comprendere appieno, e quindi a sfruttare appieno, le caratteristiche dell'ereditarietà prototipica.
Ecco un semplice esempio. Considera questo codice:
BaseObject = function(name) { if(typeof name !== "undefined") { this.name = name; } else { this.name = 'default' } };
Sembra abbastanza semplice. Se fornisci un nome, utilizzalo, altrimenti imposta il nome su 'predefinito'; per esempio:
var firstObj = new BaseObject(); var secondObj = new BaseObject('unique'); console.log(firstObj.name); // -> Results in 'default' console.log(secondObj.name); // -> Results in 'unique'
Ma se dovessimo fare questo:
delete secondObj.name;
Otterremmo quindi:
console.log(secondObj.name); // -> Results in 'undefined'
Ma non sarebbe meglio se questo tornasse a "predefinito"? Questo può essere fatto facilmente, se modifichiamo il codice originale per sfruttare l'ereditarietà del prototipo, come segue:
BaseObject = function (name) { if(typeof name !== "undefined") { this.name = name; } }; BaseObject.prototype.name = 'default';
Con questa versione, BaseObject
eredita la proprietà name
dal suo oggetto prototype
, dove è impostato (per impostazione predefinita) su 'default'
. Pertanto, se il costruttore viene chiamato senza un nome, il nome verrà impostato automaticamente su default
. E allo stesso modo, se la proprietà name
viene rimossa da un'istanza di BaseObject
, verrà quindi ricercata la catena del prototipo e la proprietà name
verrà recuperata dall'oggetto prototype
in cui il suo valore è ancora 'default'
. Quindi ora otteniamo:
var thirdObj = new BaseObject('unique'); console.log(thirdObj.name); // -> Results in 'unique' delete thirdObj.name; console.log(thirdObj.name); // -> Results in 'default'
Errore comune n. 8: creazione di riferimenti errati ai metodi di istanza
Definiamo un oggetto semplice e creiamolo e un'istanza, come segue:
var MyObject = function() {} MyObject.prototype.whoAmI = function() { console.log(this === window ? "window" : "MyObj"); }; var obj = new MyObject();
Ora, per comodità, creiamo un riferimento al metodo whoAmI
, presumibilmente così possiamo accedervi semplicemente da whoAmI()
piuttosto che dal più lungo obj.whoAmI()
:
var whoAmI = obj.whoAmI;
E per essere sicuri che tutto appaia copacetico, stampiamo il valore della nostra nuova variabile whoAmI
:
console.log(whoAmI);
Uscite:
function () { console.log(this === window ? "window" : "MyObj"); }
Ok bello. Sembra a posto.
Ma ora, guarda la differenza quando invochiamo obj.whoAmI()
rispetto al nostro riferimento di convenienza whoAmI()
:
obj.whoAmI(); // outputs "MyObj" (as expected) whoAmI(); // outputs "window" (uh-oh!)
Cosa è andato storto?
L'headfake qui è che, quando abbiamo eseguito l'assegnazione var whoAmI = obj.whoAmI;
, la nuova variabile whoAmI
è stata definita nello spazio dei nomi globale . Di conseguenza, this
suo valore è window
, non l'istanza obj
di MyObject
!
Pertanto, se abbiamo davvero bisogno di creare un riferimento a un metodo esistente di un oggetto, dobbiamo essere sicuri di farlo all'interno dello spazio dei nomi di quell'oggetto, per preservare il valore di this
. Un modo per farlo sarebbe, ad esempio, il seguente:
var MyObject = function() {} MyObject.prototype.whoAmI = function() { console.log(this === window ? "window" : "MyObj"); }; var obj = new MyObject(); obj.w = obj.whoAmI; // still in the obj namespace obj.whoAmI(); // outputs "MyObj" (as expected) obj.w(); // outputs "MyObj" (as expected)
Errore comune n. 9: fornire una stringa come primo argomento per setTimeout
o setInterval
Per cominciare, chiariamo qualcosa qui: fornire una stringa come primo argomento per setTimeout
o setInterval
non è di per sé un errore. È un codice JavaScript perfettamente legittimo. Il problema qui è più di prestazioni ed efficienza. Ciò che viene spiegato raramente è che, di nascosto, se si passa una stringa come primo argomento a setTimeout
o setInterval
, verrà passata al costruttore della funzione per essere convertita in una nuova funzione. Questo processo può essere lento e inefficiente e raramente è necessario.
L'alternativa al passaggio di una stringa come primo argomento a questi metodi consiste nel passare invece una funzione . Diamo un'occhiata a un esempio.
Ecco, quindi, un uso abbastanza tipico di setInterval
e setTimeout
, passando una stringa come primo parametro:
setInterval("logTime()", 1000); setTimeout("logMessage('" + msgValue + "')", 1000);
La scelta migliore sarebbe quella di passare una funzione come argomento iniziale; per esempio:
setInterval(logTime, 1000); // passing the logTime function to setInterval setTimeout(function() { // passing an anonymous function to setTimeout logMessage(msgValue); // (msgValue is still accessible in this scope) }, 1000);
Errore comune n. 10: mancato utilizzo della "modalità rigorosa"
Come spiegato nella nostra Guida all'assunzione di JavaScript, la "modalità rigorosa" (ovvero, includendo 'use strict';
all'inizio dei file di origine JavaScript) è un modo per imporre volontariamente un'analisi più rigorosa e una gestione degli errori sul codice JavaScript in fase di esecuzione, nonché come renderlo più sicuro.
Mentre, è vero, non utilizzare la modalità rigorosa non è un "errore" di per sé, il suo uso viene sempre più incoraggiato e la sua omissione viene sempre più considerata una cattiva forma.
Ecco alcuni vantaggi chiave della modalità rigorosa:
- Semplifica il debug. Gli errori di codice che altrimenti sarebbero stati ignorati o avrebbero fallito in modo invisibile ora genereranno errori o genereranno eccezioni, avvisandoti prima dei problemi nel tuo codice e indirizzandoti più rapidamente alla loro fonte.
- Impedisce globali accidentali. Senza la modalità rigorosa, l'assegnazione di un valore a una variabile non dichiarata crea automaticamente una variabile globale con quel nome. Questo è uno degli errori più comuni in JavaScript. In modalità rigorosa, il tentativo di farlo genera un errore.
- Elimina
this
coercizione . Senza la modalità rigorosa, un riferimento a un valorethis
di null o undefined viene automaticamente forzato al globale. Ciò può causare molti headfake e insetti che ti strappano i capelli. In modalità rigorosa, fare riferimento a aathis
valore di null o undefined genera un errore. - Non consente nomi di proprietà o valori di parametro duplicati. La modalità Strict genera un errore quando rileva una proprietà denominata duplicata in un oggetto (ad esempio,
var object = {foo: "bar", foo: "baz"};
) o un argomento denominato duplicato per una funzione (ad esempio,function foo(val1, val2, val1){}
), intercettando così quello che è quasi certamente un bug nel tuo codice che altrimenti avresti perso molto tempo a rintracciare. - Rende eval() più sicuro. Ci sono alcune differenze nel modo in cui
eval()
si comporta in modalità rigorosa e in modalità non rigorosa. Più significativamente, in modalità rigorosa, le variabili e le funzioni dichiarate all'interno di un'istruzioneeval()
non vengono create nell'ambito contenitore ( vengono create nell'ambito contenitore in modalità non rigorosa, che può anche essere una fonte comune di problemi). - Genera un errore sull'utilizzo non valido di
delete
. L'operatore didelete
(utilizzato per rimuovere le proprietà dagli oggetti) non può essere utilizzato su proprietà non configurabili dell'oggetto. Il codice non rigoroso fallirà silenziosamente quando viene effettuato un tentativo di eliminare una proprietà non configurabile, mentre la modalità rigorosa genererà un errore in questo caso.
Incartare
Come è vero con qualsiasi tecnologia, meglio capisci perché e come JavaScript funziona e non funziona, più solido sarà il tuo codice e più sarai in grado di sfruttare efficacemente la vera potenza del linguaggio. Al contrario, la mancanza di un'adeguata comprensione dei paradigmi e dei concetti di JavaScript è davvero il punto in cui si trovano molti problemi di JavaScript.
Familiarizzare a fondo con le sfumature e le sottigliezze della lingua è la strategia più efficace per migliorare le tue competenze e aumentare la tua produttività. Evitare molti errori JavaScript comuni aiuterà quando il tuo JavaScript non funziona.