Cod JavaScript Buggy: Cele mai frecvente 10 greșeli pe care le fac dezvoltatorii JavaScript
Publicat: 2022-03-11Astăzi, JavaScript se află în centrul aproape tuturor aplicațiilor web moderne. În ultimii câțiva ani, în special, au fost martorii proliferării unei game largi de biblioteci și cadre puternice bazate pe JavaScript pentru dezvoltarea de aplicații cu o singură pagină (SPA), grafică și animație și chiar platforme JavaScript pe partea de server. JavaScript a devenit cu adevărat omniprezent în lumea dezvoltării de aplicații web și, prin urmare, este o abilitate din ce în ce mai importantă de stăpânit.
La prima vedere, JavaScript poate părea destul de simplu. Și într-adevăr, pentru a construi funcționalitatea JavaScript de bază într-o pagină web este o sarcină destul de simplă pentru orice dezvoltator de software cu experiență, chiar dacă este nou în JavaScript. Cu toate acestea, limbajul este mult mai nuanțat, puternic și complex decât s-ar crede inițial. Într-adevăr, multe dintre subtilitățile JavaScript duc la o serie de probleme comune care îl împiedică să funcționeze – dintre care 10 le vom discuta aici – care sunt importante de conștient și de evitat în încercarea cuiva de a deveni un dezvoltator JavaScript maestru.
Greșeala comună #1: referințe incorecte la this
Am auzit odată un comedian spunând:
Nu sunt chiar aici, pentru că ce e aici, în afară de acolo, fără „t”?
Această glumă caracterizează în multe privințe tipul de confuzie care există adesea pentru dezvoltatori în ceea ce privește cuvântul cheie al JavaScript this
ului. Adică, this
este cu adevărat asta sau este cu totul altceva? Sau este nedefinit?
Pe măsură ce tehnicile de codare JavaScript și modelele de proiectare au devenit din ce în ce mai sofisticate de-a lungul anilor, a existat o creștere corespunzătoare a proliferării domeniilor de auto-referință în cadrul apelurilor și închiderilor, care sunt o sursă destul de comună de „aceasta/aceea confuzie”.
Luați în considerare acest exemplu de fragment de cod:
Game.prototype.restart = function () { this.clearLocalStorage(); this.timer = setTimeout(function() { this.clearBoard(); // what is "this"? }, 0); };
Executarea codului de mai sus are ca rezultat următoarea eroare:
Uncaught TypeError: undefined is not a function
De ce?
Totul tine de context. Motivul pentru care obțineți eroarea de mai sus este că, atunci când invocați setTimeout()
, de fapt invocați window.setTimeout()
. Ca rezultat, funcția anonimă transmisă setTimeout()
este definită în contextul obiectului window
, care nu are metoda clearBoard()
.
O soluție tradițională, compatibilă cu browserul vechi, este să salvați pur și simplu referința la this
într-o variabilă care poate fi apoi moștenită prin închidere; de exemplu:
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); };
Alternativ, în browserele mai noi, puteți utiliza metoda bind()
pentru a trece referința adecvată:
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'! };
Greșeala comună #2: Gândirea că există un domeniu de aplicare la nivel de bloc
După cum sa discutat în Ghidul nostru de angajare JavaScript, o sursă comună de confuzie în rândul dezvoltatorilor JavaScript (și, prin urmare, o sursă comună de erori) presupune că JavaScript creează un domeniu nou pentru fiecare bloc de cod. Deși acest lucru este adevărat în multe alte limbi, nu este adevărat în JavaScript. Luați în considerare, de exemplu, următorul cod:
for (var i = 0; i < 10; i++) { /* ... */ } console.log(i); // what will this output?
Dacă ghiciți că apelul console.log()
fie va scoate undefined
, fie va genera o eroare, ați ghicit incorect. Credeți sau nu, va ieși 10
. De ce?
În majoritatea celorlalte limbi, codul de mai sus ar duce la o eroare, deoarece „viața” (adică domeniul de aplicare) variabilei i
ar fi limitată la blocul for
. În JavaScript, totuși, acesta nu este cazul și variabila i
rămâne în domeniul de aplicare chiar și după finalizarea buclei for
, păstrându-și ultima valoare după ieșirea din buclă. (Acest comportament este cunoscut, de altfel, ca ridicare variabilă).
Totuși, merită remarcat faptul că suportul pentru domenii la nivel de bloc își face loc în JavaScript prin noul cuvânt cheie let
. Cuvântul cheie let
este deja disponibil în JavaScript 1.7 și este programat să devină un cuvânt cheie JavaScript acceptat oficial începând cu ECMAScript 6.
Sunteți nou în JavaScript? Citiți mai multe despre lunete, prototipuri și multe altele.
Greșeala comună nr. 3: Crearea scurgerilor de memorie
Scurgerile de memorie sunt probleme JavaScript aproape inevitabile dacă nu codificați în mod conștient pentru a le evita. Există numeroase moduri prin care acestea să apară, așa că vom evidenția doar câteva dintre aparițiile lor mai frecvente.
Scurgere de memorie Exemplul 1: referințe suspendate la obiecte defuncte
Luați în considerare următorul cod:
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
Dacă rulați codul de mai sus și monitorizați utilizarea memoriei, veți descoperi că aveți o scurgere masivă de memorie, scurgând un megaoctet complet pe secundă! Și nici măcar un GC manual nu ajută. Deci, se pare că longStr
fiecare dată când se replaceThing
. Dar de ce?
Să examinăm lucrurile mai detaliat:
Fiecare obiect theThing
conține propriul său obiect longStr
de 1MB. În fiecare secundă, când numim replaceThing
, se ține de o referință la obiectul anterior theThing
din priorThing
. Dar tot nu am crede că aceasta ar fi o problemă, deoarece de fiecare dată, priorThing
-ul referit anterior ar fi dereferențiat (când priorThing
este resetat prin priorThing = theThing;
). Și, mai mult, este menționat doar în corpul principal al replaceThing
și în funcția unused
care, de fapt, nu este niciodată folosită.
Deci, din nou, ne întrebăm de ce există o scurgere de memorie aici!?
Pentru a înțelege ce se întâmplă, trebuie să înțelegem mai bine cum funcționează lucrurile în JavaScript sub capotă. Modul tipic în care sunt implementate închiderile este că fiecare obiect funcție are o legătură către un obiect în stil dicționar care reprezintă domeniul său lexical. Dacă ambele funcții definite în interiorul replaceThing
au folosit de fapt priorThing
, ar fi important ca ambele să primească același obiect, chiar dacă priorThing
este atribuit din nou și din nou, astfel încât ambele funcții împărtășesc același mediu lexical. Dar de îndată ce o variabilă este utilizată de orice închidere, ea ajunge în mediul lexical împărtășit de toate închiderile din acel domeniu. Și această mică nuanță este ceea ce duce la această scurgere de memorie noduroasă. (Mai multe detalii despre acest lucru sunt disponibile aici.)
Scurgere de memorie Exemplul 2: Referințe circulare
Luați în considerare acest fragment de cod:
function addClickHandler(element) { element.click = function onClick(e) { alert("Clicked the " + element.nodeName) } }
Aici, onClick
are o închidere care păstrează o referință la element
(prin element.nodeName
). Atribuind și onClick
la element.click
, se creează referința circulară; adică: element
-> onClick
-> element
-> onClick
-> element
...
Interesant, chiar dacă element
este eliminat din DOM, auto-referința circulară de mai sus ar împiedica colectarea element
și a onClick
și, prin urmare, o scurgere de memorie.
Evitarea scurgerilor de memorie: Ce trebuie să știți
Gestionarea memoriei JavaScript (și, în special, colectarea gunoiului) se bazează în mare măsură pe noțiunea de accesibilitate a obiectelor.
Se presupune că următoarele obiecte sunt accesibile și sunt cunoscute ca „rădăcini”:
- Obiecte referite de oriunde în stiva de apeluri curente (adică toate variabilele și parametrii locali din funcțiile invocate în prezent și toate variabilele din domeniul de închidere)
- Toate variabilele globale
Obiectele sunt păstrate în memorie cel puțin atâta timp cât sunt accesibile din oricare dintre rădăcini printr-o referință sau un lanț de referințe.
În browser există un Garbage Collector (GC) care curăță memoria ocupată de obiecte inaccesibile; adică, obiectele vor fi șterse din memorie dacă și numai dacă GC consideră că nu sunt accesibile. Din păcate, este destul de ușor să ajungeți cu obiecte „zombie” defuncte care de fapt nu mai sunt folosite, dar despre care GC încă le consideră „accesibile”.
Greșeala comună #4: Confuzie despre egalitate
Una dintre avantajele JavaScript este că va constrânge automat orice valoare care este referită într-un context boolean la o valoare booleană. Dar există cazuri în care acest lucru poate fi pe cât de confuz, pe atât de convenabil. Unele dintre următoarele, de exemplu, se știe că mușcă mulți dezvoltatori 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 ([]) // ...
În ceea ce privește ultimele două, în ciuda faptului că sunt goale (ceea ce s-ar putea să creadă că ar evalua false
), atât {}
, cât și []
sunt de fapt obiecte și orice obiect va fi forțat la o valoare booleană true
în JavaScript, în concordanță cu specificația ECMA-262.
După cum demonstrează aceste exemple, regulile de constrângere de tip pot fi uneori clare ca noroiul. În consecință, cu excepția cazului în care se dorește în mod explicit constrângerea de tip, este de obicei cel mai bine să folosiți ===
și !==
(mai degrabă decât ==
și !=
), pentru a evita orice efecte secundare neintenționate ale constrângerii de tip. ( ==
și !=
efectuează automat conversia de tip atunci când compară două lucruri, în timp ce ===
și !==
fac aceeași comparație fără conversia de tip.)
Și complet ca un punct secundar – dar din moment ce vorbim de constrângere de tip și comparații – merită menționat că compararea NaN
cu orice (chiar și NaN
!) va returna întotdeauna false
. Prin urmare, nu puteți utiliza operatorii de egalitate ( ==
, ===
, !=
, !==
) pentru a determina dacă o valoare este NaN
sau nu. În schimb, utilizați funcția globală isNaN()
:
console.log(NaN == NaN); // false console.log(NaN === NaN); // false console.log(isNaN(NaN)); // true
Greșeala comună #5: manipulare ineficientă a DOM
JavaScript face relativ ușor manipularea DOM (adică adăugarea, modificarea și eliminarea elementelor), dar nu face nimic pentru a promova acest lucru în mod eficient.

Un exemplu comun este codul care adaugă o serie de elemente DOM pe rând. Adăugarea unui element DOM este o operațiune costisitoare. Codul care adaugă mai multe elemente DOM consecutiv este ineficient și probabil să nu funcționeze bine.
O alternativă eficientă atunci când trebuie adăugate mai multe elemente DOM este utilizarea fragmentelor de document, îmbunătățind astfel atât eficiența, cât și performanța.
De exemplu:
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));
Pe lângă eficiența în mod inerent îmbunătățită a acestei abordări, crearea elementelor DOM atașate este costisitoare, în timp ce crearea și modificarea lor în timp ce sunt detașate și apoi atașarea lor oferă performanțe mult mai bune.
Greșeala comună #6: Utilizarea incorectă a definițiilor funcției în interiorul buclelor for
Luați în considerare acest cod:
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); }; }
Pe baza codului de mai sus, dacă ar exista 10 elemente de intrare, făcând clic pe oricare dintre ele ar afișa „Acesta este elementul #10”! Acest lucru se datorează faptului că, în momentul în care onclick
este invocat pentru oricare dintre elemente, bucla for de mai sus se va fi finalizată și valoarea lui i
va fi deja 10 (pentru toate acestea).
Iată cum putem corecta problemele de cod de mai sus, totuși, pentru a obține comportamentul dorit:
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); }
În această versiune revizuită a codului, makeHandler
este executat imediat de fiecare dată când trecem prin buclă, de fiecare dată primind valoarea curentă a lui i+1
și leagă-o la o variabilă num
scoped. Funcția exterioară returnează funcția interioară (care folosește și această variabilă num
cu scop) și onclick
-ul elementului este setat la acea funcție internă. Acest lucru asigură că fiecare onclick
primește și utilizează valoarea i
adecvată (prin variabila num
scop).
Greșeala comună nr. 7: Eșecul de a valorifica în mod corespunzător moștenirea prototipală
Un procent surprinzător de mare de dezvoltatori JavaScript nu reușesc să înțeleagă pe deplin și, prin urmare, să valorifice pe deplin caracteristicile moștenirii prototipale.
Iată un exemplu simplu. Luați în considerare acest cod:
BaseObject = function(name) { if(typeof name !== "undefined") { this.name = name; } else { this.name = 'default' } };
Pare destul de simplu. Dacă furnizați un nume, utilizați-l, altfel setați numele la „implicit”; de exemplu:
var firstObj = new BaseObject(); var secondObj = new BaseObject('unique'); console.log(firstObj.name); // -> Results in 'default' console.log(secondObj.name); // -> Results in 'unique'
Dar dacă ar fi să facem asta:
delete secondObj.name;
Atunci am obține:
console.log(secondObj.name); // -> Results in 'undefined'
Dar nu ar fi mai frumos ca acest lucru să revină la „implicit”? Acest lucru se poate face cu ușurință, dacă modificăm codul original pentru a valorifica moștenirea prototipală, după cum urmează:
BaseObject = function (name) { if(typeof name !== "undefined") { this.name = name; } }; BaseObject.prototype.name = 'default';
Cu această versiune, BaseObject
moștenește proprietatea name
din obiectul său prototype
, unde este setată (în mod implicit) la 'default'
. Astfel, dacă constructorul este apelat fără nume, numele va fi implicit default
. Și, în mod similar, dacă proprietatea name
este eliminată dintr-o instanță de BaseObject
, lanțul prototip va fi apoi căutat și proprietatea name
va fi preluată din obiectul prototype
unde valoarea sa este încă 'default'
. Deci acum obținem:
var thirdObj = new BaseObject('unique'); console.log(thirdObj.name); // -> Results in 'unique' delete thirdObj.name; console.log(thirdObj.name); // -> Results in 'default'
Greșeala comună #8: Crearea de referințe incorecte la metodele de instanță
Să definim un obiect simplu și să creăm și o instanță a acestuia, după cum urmează:
var MyObject = function() {} MyObject.prototype.whoAmI = function() { console.log(this === window ? "window" : "MyObj"); }; var obj = new MyObject();
Acum, pentru comoditate, să creăm o referință la metoda whoAmI
, probabil ca să o putem accesa doar prin whoAmI()
și nu prin obj.whoAmI()
() mai lung:
var whoAmI = obj.whoAmI;
Și doar pentru a fi siguri că totul pare copacetic, să tipărim valoarea noii noastre variabile whoAmI
:
console.log(whoAmI);
Ieșiri:
function () { console.log(this === window ? "window" : "MyObj"); }
Bine, in regula. Arată bine.
Dar acum, uitați-vă la diferența când invocăm obj.whoAmI()
față de referința noastră de confort whoAmI()
:
obj.whoAmI(); // outputs "MyObj" (as expected) whoAmI(); // outputs "window" (uh-oh!)
Ce a mers prost?
Fake-ul aici este că, când am făcut sarcina var whoAmI = obj.whoAmI;
, noua variabilă whoAmI
a fost definită în spațiul de nume global . Ca rezultat, valoarea this
este window
, nu instanța obj
a MyObject
!
Astfel, dacă într-adevăr trebuie să creăm o referință la o metodă existentă a unui obiect, trebuie să ne asigurăm că o facem în spațiul de nume al acelui obiect, pentru a păstra valoarea this
. O modalitate de a face acest lucru ar fi, de exemplu, următoarea:
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)
Greșeala comună #9: Furnizarea unui șir ca prim argument pentru setTimeout
sau setInterval
Pentru început, să fim clari despre ceva aici: furnizarea unui șir ca prim argument pentru setTimeout
sau setInterval
nu este în sine o greșeală. Este un cod JavaScript perfect legitim. Problema aici este mai mult una de performanță și eficiență. Ceea ce se explică rar este că, sub capotă, dacă treceți un șir ca prim argument pentru setTimeout
sau setInterval
, acesta va fi transmis constructorului de funcție pentru a fi convertit într-o funcție nouă. Acest proces poate fi lent și ineficient și este rareori necesar.
Alternativa la trecerea unui șir ca prim argument la aceste metode este de a trece în schimb o funcție . Să aruncăm o privire la un exemplu.
Aici, atunci, ar fi o utilizare destul de tipică a setInterval
și setTimeout
, trecând un șir ca prim parametru:
setInterval("logTime()", 1000); setTimeout("logMessage('" + msgValue + "')", 1000);
Alegerea mai bună ar fi să treceți o funcție ca argument inițial; de exemplu:
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);
Greșeala comună #10: Neutilizarea „modului strict”
După cum este explicat în Ghidul nostru de angajare JavaScript, „modul strict” (adică, inclusiv 'use strict';
la începutul fișierelor sursă JavaScript) este o modalitate de a impune în mod voluntar o analiză mai strictă și o gestionare a erorilor asupra codului JavaScript în timpul execuției, de asemenea ca făcându-l mai sigur.
Deși, desigur, eșecul în utilizarea modului strict nu este o „greșeală” în sine, utilizarea acestuia este din ce în ce mai încurajată și omiterea sa devine din ce în ce mai considerată o formă proastă.
Iată câteva beneficii cheie ale modului strict:
- Ușurează depanarea. Erorile de cod care altfel ar fi fost ignorate sau ar fi eșuat în tăcere vor genera acum erori sau vor genera excepții, avertizându-vă mai devreme asupra problemelor din codul dvs. și direcționându-vă mai rapid către sursa lor.
- Previne globalurile accidentale. Fără modul strict, atribuirea unei valori unei variabile nedeclarate creează automat o variabilă globală cu acest nume. Aceasta este una dintre cele mai frecvente erori în JavaScript. În modul strict, încercarea de a face acest lucru aruncă o eroare.
- Elimină
this
constrângere . Fără modul strict, o referință lathis
valoare nulă sau nedefinită este forțată automat la global. Acest lucru poate provoca multe falsuri de cap și tip de bug-uri de smulgerea părului. În modul strict, referirea lathis
valoare nulă sau nedefinită aruncă o eroare. - Nu permite duplicarea numelor de proprietate sau a valorilor parametrilor. Modul strict aruncă o eroare atunci când detectează o proprietate numită duplicat într-un obiect (de exemplu,
var object = {foo: "bar", foo: "baz"};
) sau un argument numit duplicat pentru o funcție (de exemplu,function foo(val1, val2, val1){}
), prind astfel ceea ce este aproape sigur o eroare în codul dvs. pe care altfel ați fi pierdut mult timp urmărind. - Face eval() mai sigur. Există unele diferențe în modul în care
eval()
se comportă în modul strict și în modul non-strict. Cel mai semnificativ, în modul strict, variabilele și funcțiile declarate în interiorul unei instrucțiunieval()
nu sunt create în domeniul de aplicare ( sunt create în domeniul de aplicare în modul non-strict, care poate fi, de asemenea, o sursă comună de probleme). - Afișează o eroare la utilizarea nevalidă a
delete
. Operatorul dedelete
(utilizat pentru a elimina proprietățile din obiecte) nu poate fi utilizat pe proprietățile neconfigurabile ale obiectului. Codul non-strict va eșua în tăcere atunci când se încearcă ștergerea unei proprietăți neconfigurabile, în timp ce modul strict va genera o eroare într-un astfel de caz.
Învelire
Așa cum este adevărat în cazul oricărei tehnologii, cu cât înțelegeți mai bine de ce și cum funcționează și nu JavaScript, cu atât codul dvs. va fi mai solid și veți putea valorifica în mod eficient puterea reală a limbajului. În schimb, lipsa unei înțelegeri adecvate a paradigmelor și conceptelor JavaScript este într-adevăr locul în care se află multe probleme JavaScript.
Familiarizarea completă cu nuanțele și subtilitățile limbii este cea mai eficientă strategie pentru îmbunătățirea competenței și creșterea productivității. Evitarea multor greșeli JavaScript comune vă va ajuta atunci când JavaScript nu funcționează.