Błędny kod JavaScript: 10 najczęstszych błędów popełnianych przez programistów JavaScript

Opublikowany: 2022-03-11

Dzisiaj JavaScript jest rdzeniem praktycznie wszystkich nowoczesnych aplikacji internetowych. Zwłaszcza w ciągu ostatnich kilku lat byliśmy świadkami rozpowszechniania się szerokiej gamy potężnych bibliotek i frameworków opartych na JavaScript do tworzenia aplikacji jednostronicowych (SPA), grafiki i animacji, a nawet platform JavaScript po stronie serwera. JavaScript stał się naprawdę wszechobecny w świecie tworzenia aplikacji internetowych i dlatego jest coraz ważniejszą umiejętnością do opanowania.

Na pierwszy rzut oka JavaScript może wydawać się dość prosty. I rzeczywiście, wbudowanie podstawowych funkcji JavaScript na stronę internetową jest dość prostym zadaniem dla każdego doświadczonego programisty, nawet jeśli jest nowy w JavaScript. Jednak język jest znacznie bardziej zniuansowany, potężny i złożony, niż mogłoby się początkowo wydawać. Rzeczywiście, wiele subtelności JavaScript prowadzi do wielu typowych problemów, które uniemożliwiają jego działanie – 10 z nich omówimy tutaj – które są ważne, aby być świadomym i unikać w dążeniu do zostania mistrzem JavaScript.

Powszechny błąd nr 1: Nieprawidłowe odniesienia do this

Słyszałem kiedyś, jak komik powiedział:

Tak naprawdę nie ma mnie tutaj, bo co jest tutaj, poza tym, bez „t”?

Ten żart pod wieloma względami charakteryzuje rodzaj nieporozumień, jakie często pojawiają się wśród programistów w odniesieniu do słowa kluczowego this w JavaScript. Chodzi mi o to, czy this naprawdę to, czy jest to coś zupełnie innego? Czy jest to nieokreślone?

W miarę jak techniki kodowania JavaScript i wzorce projektowe stawały się z biegiem lat coraz bardziej wyrafinowane, nastąpił odpowiedni wzrost proliferacji zakresów autoreferencyjnych w wywołaniach zwrotnych i domknięciach, które są dość powszechnym źródłem „tego/tego zamieszania”.

Rozważ ten przykładowy fragment kodu:

 Game.prototype.restart = function () { this.clearLocalStorage(); this.timer = setTimeout(function() { this.clearBoard(); // what is "this"? }, 0); };

Wykonanie powyższego kodu powoduje następujący błąd:

 Uncaught TypeError: undefined is not a function

Czemu?

Wszystko zależy od kontekstu. Powodem, dla którego otrzymujesz powyższy błąd, jest to, że kiedy wywołujesz setTimeout() , faktycznie wywołujesz window.setTimeout() . W rezultacie funkcja anonimowa przekazywana do setTimeout() jest definiowana w kontekście obiektu window , który nie posiada metody clearBoard() .

Tradycyjne, zgodne ze starą przeglądarką rozwiązanie polega po prostu na zapisaniu odniesienia do this w zmiennej, która może być następnie dziedziczona przez zamknięcie; np:

 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); };

Alternatywnie, w nowszych przeglądarkach, możesz użyć metody bind() , aby przekazać odpowiednią referencję:

 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'! };

Powszechny błąd nr 2: myślenie, że istnieje zakres na poziomie bloku

Jak omówiono w naszym Przewodniku rekrutacji JavaScript, powszechnym źródłem nieporozumień wśród programistów JavaScript (a zatem częstym źródłem błędów) jest założenie, że JavaScript tworzy nowy zakres dla każdego bloku kodu. Chociaż jest to prawdą w wielu innych językach, nie dotyczy to JavaScript. Rozważmy na przykład następujący kod:

 for (var i = 0; i < 10; i++) { /* ... */ } console.log(i); // what will this output?

Jeśli zgadniesz, że wywołanie console.log() undefined lub wygeneruje błąd, zgadłeś niepoprawnie. Wierz lub nie, ale wygeneruje 10 . Czemu?

W większości innych języków powyższy kod prowadziłby do błędu, ponieważ „życie” (tj. zasięg) zmiennej i byłoby ograniczone do bloku for . Jednak w JavaScript tak nie jest i zmienna i pozostaje w zasięgu nawet po zakończeniu pętli for , zachowując swoją ostatnią wartość po wyjściu z pętli. (To zachowanie znane jest, nawiasem mówiąc, jako zmienne podnoszenie).

Warto jednak zauważyć, że obsługa zakresów na poziomie bloków trafia do JavaScriptu poprzez nowe słowo kluczowe let . Słowo kluczowe let jest już dostępne w JavaScript 1.7 i ma stać się oficjalnie obsługiwanym słowem kluczowym JavaScript od ECMAScript 6.

Nowy w JavaScript? Przeczytaj o zakresach, prototypach i nie tylko.

Powszechny błąd nr 3: tworzenie wycieków pamięci

Wycieki pamięci są prawie nieuniknionymi problemami JavaScript, jeśli nie kodujesz świadomie, aby ich uniknąć. Istnieje wiele sposobów ich występowania, więc przedstawimy tylko kilka z ich bardziej powszechnych przypadków.

Przykład wycieku pamięci 1: Wiszące odniesienia do nieistniejących obiektów

Rozważ następujący kod:

 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

Jeśli uruchomisz powyższy kod i monitorujesz użycie pamięci, odkryjesz, że masz ogromny wyciek pamięci, wyciekający cały megabajt na sekundę! I nawet ręczna GC nie pomaga. Wygląda więc na to, że przeciekamy longStr każdym razem, gdy wywoływana jest replaceThing . Ale dlaczego?

Przyjrzyjmy się bardziej szczegółowo:

Każdy obiekt theThing zawiera swój własny obiekt longStr 1 MB. Co sekundę, gdy wywołujemy replaceThing , utrzymuje się odwołanie do wcześniejszego obiektu theThing w priorThing . Ale nadal nie sądzimy, że byłby to problem, ponieważ za każdym razem poprzednia priorThing byłaby wyłuskana (gdy priorThing zostanie zresetowany przez priorThing = theThing; ). Co więcej, jest przywoływany tylko w głównym korpusie replaceThing oraz w funkcji unused , która w rzeczywistości nigdy nie jest używana.

Więc znowu zastanawiamy się, dlaczego jest tu przeciek pamięci!?

Aby zrozumieć, co się dzieje, musimy lepiej zrozumieć, jak coś działa w JavaScript pod maską. Typowy sposób implementacji domknięć polega na tym, że każdy obiekt funkcji ma łącze do obiektu w stylu słownika reprezentującego jego zakres leksykalny. Jeśli obie funkcje zdefiniowane wewnątrz replaceThing faktycznie używają priorThing , ważne byłoby, aby obie otrzymały ten sam obiekt, nawet jeśli priorThing jest wielokrotnie przypisywana do funkcji priorThing, więc obie funkcje współdzielą to samo środowisko leksykalne. Ale gdy tylko zmienna zostanie użyta przez jakiekolwiek domknięcie, trafia do środowiska leksykalnego współdzielonego przez wszystkie domknięcia w tym zakresie. I ten mały niuans jest tym, co prowadzi do tego ogromnego wycieku pamięci. (Więcej informacji na ten temat można znaleźć tutaj.)

Przykład wycieku pamięci 2: Odwołania cykliczne

Rozważ ten fragment kodu:

 function addClickHandler(element) { element.click = function onClick(e) { alert("Clicked the " + element.nodeName) } }

Tutaj onClick ma zamknięcie, które przechowuje odniesienie do element (poprzez element.nodeName ). Poprzez przypisanie onClick również do element.click , tworzone jest odwołanie cykliczne; czyli: element -> onClick -> element -> onClick -> element

Co ciekawe, nawet jeśli element zostanie usunięty z DOM, powyższe cykliczne odniesienie do siebie uniemożliwiłoby zbieranie element i onClick , a tym samym wyciek pamięci.

Unikanie przecieków pamięci: co musisz wiedzieć

Zarządzanie pamięcią w JavaScript (a w szczególności wyrzucanie śmieci) jest w dużej mierze oparte na pojęciu osiągalności obiektu.

Zakłada się, że osiągalne są następujące obiekty, które są znane jako „root”:

  • Obiekty przywoływane z dowolnego miejsca w bieżącym stosie wywołań (to znaczy wszystkie lokalne zmienne i parametry w aktualnie wywoływanych funkcjach oraz wszystkie zmienne w zakresie zamknięcia)
  • Wszystkie zmienne globalne

Obiekty są przechowywane w pamięci przynajmniej tak długo, jak są dostępne z dowolnego źródła poprzez odniesienie lub łańcuch odniesień.

W przeglądarce znajduje się Garbage Collector (GC), który czyści pamięć zajmowaną przez nieosiągalne obiekty; tj. obiekty zostaną usunięte z pamięci wtedy i tylko wtedy, gdy GC uzna, że ​​są nieosiągalne. Niestety, dość łatwo jest skończyć z nieistniejącymi obiektami „zombie”, które w rzeczywistości nie są już używane, ale które GC nadal uważa za „osiągalne”.

Powiązane: Najlepsze praktyki i wskazówki dotyczące JavaScript autorstwa Toptal Developers

Powszechny błąd nr 4: zamieszanie dotyczące równości

Jedną z udogodnień w JavaScript jest to, że automatycznie przekonwertuje każdą wartość, do której odwołuje się kontekst logiczny, na wartość logiczną. Ale są przypadki, w których może to być tak mylące, jak i wygodne. Niektóre z poniższych, na przykład, były znane wielu programistom 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 ([]) // ...

W odniesieniu do dwóch ostatnich, pomimo tego, że są puste (co może prowadzić do przekonania, że ​​ich wynikiem będzie false ), zarówno {} , jak i [] są w rzeczywistości obiektami i każdy obiekt zostanie skonwertowany do wartości logicznej true w JavaScript, zgodne ze specyfikacją ECMA-262.

Jak pokazują te przykłady, zasady przymusu typu mogą czasami być jasne jak błoto. W związku z tym, o ile wymuszenie typu nie jest wyraźnie pożądane, zazwyczaj najlepiej jest użyć === i !== (zamiast == i != ), aby uniknąć wszelkich niezamierzonych skutków ubocznych wymuszania typu. ( == i != automatycznie wykonują konwersję typu podczas porównywania dwóch rzeczy, podczas gdy === i !== wykonują to samo porównanie bez konwersji typu.)

I zupełnie na marginesie – ale skoro mówimy o koercji typu i porównaniach – warto wspomnieć, że porównywanie NaN z czymkolwiek (nawet NaN !) zawsze zwróci false . W związku z tym nie można użyć operatorów równości ( == , === , != , !== ) do określenia, czy wartość jest NaN , czy nie. Zamiast tego użyj wbudowanej globalnej funkcji isNaN() :

 console.log(NaN == NaN); // false console.log(NaN === NaN); // false console.log(isNaN(NaN)); // true

Powszechny błąd nr 5: Nieefektywna manipulacja DOM

JavaScript sprawia, że ​​stosunkowo łatwo jest manipulować DOM (tj. dodawać, modyfikować i usuwać elementy), ale nie robi nic, aby promować robienie tego wydajnie.

Typowym przykładem jest kod, który dodaje pojedynczo serię elementów DOM. Dodanie elementu DOM jest kosztowną operacją. Kod, który dodaje wiele elementów DOM kolejno, jest nieefektywny i prawdopodobnie nie będzie działał dobrze.

Jedną ze skutecznych alternatyw, gdy trzeba dodać wiele elementów DOM, jest użycie fragmentów dokumentów, co poprawia zarówno wydajność, jak i wydajność.

Na przykład:

 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));

Oprócz nieodłącznie zwiększonej wydajności tego podejścia, tworzenie dołączonych elementów DOM jest drogie, podczas gdy tworzenie i modyfikowanie ich podczas odłączania, a następnie dołączania zapewnia znacznie lepszą wydajność.

Powszechny błąd nr 6: nieprawidłowe użycie definicji funkcji for pętlach

Rozważ ten kod:

 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); }; }

W oparciu o powyższy kod, jeśli było 10 elementów wejściowych, kliknięcie dowolnego z nich wyświetliło „To jest element nr 10”! Dzieje się tak, ponieważ do czasu wywołania onclick dla dowolnego z elementów powyższa pętla for zostanie zakończona i wartość i będzie już wynosić 10 (dla wszystkich ).

Oto jak możemy poprawić powyższe problemy z kodem, aby osiągnąć pożądane zachowanie:

 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); }

W tej poprawionej wersji kodu makeHandler jest natychmiast wykonywany za każdym razem, gdy przechodzimy przez pętlę, za każdym razem otrzymując aktualną wartość i+1 i wiążąc ją ze zmienną num o określonym zakresie. Funkcja zewnętrzna zwraca funkcję wewnętrzną (która również używa tej zmiennej num w zakresie), a element onclick jest ustawiony na tę funkcję wewnętrzną. Gwarantuje to, że każdy onclick otrzyma i użyje właściwej wartości i (za pośrednictwem zmiennej num w zakresie).

Powszechny błąd nr 7: Niewłaściwe wykorzystanie dziedziczenia prototypowego

Zaskakująco wysoki odsetek programistów JavaScript nie w pełni rozumie, a zatem nie wykorzystuje w pełni funkcji dziedziczenia prototypowego.

Oto prosty przykład. Rozważ ten kod:

 BaseObject = function(name) { if(typeof name !== "undefined") { this.name = name; } else { this.name = 'default' } };

Wydaje się dość proste. Jeśli podasz nazwę, użyj jej, w przeciwnym razie ustaw nazwę na „domyślną”; np:

 var firstObj = new BaseObject(); var secondObj = new BaseObject('unique'); console.log(firstObj.name); // -> Results in 'default' console.log(secondObj.name); // -> Results in 'unique'

Ale co, gdybyśmy to zrobili:

 delete secondObj.name;

Otrzymalibyśmy wtedy:

 console.log(secondObj.name); // -> Results in 'undefined'

Ale czy nie byłoby przyjemniej, gdyby wróciło to do stanu „domyślnego”? Można to łatwo zrobić, jeśli zmodyfikujemy oryginalny kod, aby wykorzystać dziedziczenie prototypowe, w następujący sposób:

 BaseObject = function (name) { if(typeof name !== "undefined") { this.name = name; } }; BaseObject.prototype.name = 'default';

W tej wersji BaseObject dziedziczy właściwość name z obiektu prototype , w którym jest ustawiona (domyślnie) na 'default' . Tak więc, jeśli konstruktor zostanie wywołany bez nazwy, nazwą domyślną będzie default . I podobnie, jeśli właściwość name zostanie usunięta z instancji BaseObject , łańcuch prototypów zostanie przeszukany, a właściwość name zostanie pobrana z obiektu prototype , w którym jej wartość jest nadal 'default' . Więc teraz otrzymujemy:

 var thirdObj = new BaseObject('unique'); console.log(thirdObj.name); // -> Results in 'unique' delete thirdObj.name; console.log(thirdObj.name); // -> Results in 'default'

Powszechny błąd nr 8: Tworzenie niepoprawnych odniesień do metod instancji

Zdefiniujmy prosty obiekt i utwórzmy go oraz instancję w następujący sposób:

 var MyObject = function() {} MyObject.prototype.whoAmI = function() { console.log(this === window ? "window" : "MyObj"); }; var obj = new MyObject();

Teraz, dla wygody, stwórzmy odwołanie do metody whoAmI , prawdopodobnie po to, abyśmy mogli uzyskać do niej dostęp jedynie przez whoAmI() zamiast dłuższego obj.whoAmI() :

 var whoAmI = obj.whoAmI;

Aby mieć pewność, że wszystko wygląda dobrze, wydrukujmy wartość naszej nowej zmiennej whoAmI :

 console.log(whoAmI);

Wyjścia:

 function () { console.log(this === window ? "window" : "MyObj"); }

Ok fajnie. Wygląda w porządku.

Ale teraz spójrz na różnicę, kiedy wywołujemy obj.whoAmI() a naszą referencję dotyczącą wygody whoAmI() :

 obj.whoAmI(); // outputs "MyObj" (as expected) whoAmI(); // outputs "window" (uh-oh!)

Co poszło nie tak?

Podróbką tutaj jest to, że kiedy zrobiliśmy przypisanie var whoAmI = obj.whoAmI; , nowa zmienna whoAmI była definiowana w globalnej przestrzeni nazw. W rezultacie jego wartością this window , a nie instancja MyObject obj

Tak więc, jeśli naprawdę potrzebujemy utworzyć referencję do istniejącej metody obiektu, musimy zrobić to w przestrzeni nazw tego obiektu, aby zachować wartość this . Jednym ze sposobów na zrobienie tego byłoby na przykład:

 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)

Częsty błąd nr 9: podanie ciągu jako pierwszego argumentu w funkcji setTimeout lub setInterval

Na początek wyjaśnijmy coś tutaj: dostarczenie ciągu jako pierwszego argumentu do setTimeout lub setInterval samo w sobie nie jest błędem. Jest to całkowicie legalny kod JavaScript. Tutaj chodzi bardziej o wydajność i wydajność. Rzadko się wyjaśnia, że ​​pod maską, jeśli przekażesz łańcuch jako pierwszy argument do setTimeout lub setInterval , zostanie on przekazany do konstruktora funkcji w celu przekonwertowania na nową funkcję. Ten proces może być powolny i nieefektywny i rzadko jest konieczny.

Alternatywą przekazywania ciągu znaków jako pierwszego argumentu tych metod jest przekazanie funkcji . Spójrzmy na przykład.

Tutaj byłoby więc dość typowym użyciem setInterval i setTimeout , przekazując łańcuch jako pierwszy parametr:

 setInterval("logTime()", 1000); setTimeout("logMessage('" + msgValue + "')", 1000);

Lepszym wyborem byłoby przekazanie funkcji jako argumentu początkowego; np:

 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);

Powszechny błąd nr 10: nieużycie „trybu ścisłego”

Jak wyjaśniono w naszym przewodniku rekrutacyjnym JavaScript, „tryb ścisły” (tj. w tym 'use strict'; na początku plików źródłowych JavaScript) jest sposobem na dobrowolne wymuszenie bardziej rygorystycznego parsowania i obsługi błędów w kodzie JavaScript w czasie wykonywania jako bezpieczniejszy.

Chociaż, trzeba przyznać, niestosowanie trybu ścisłego nie jest „błędem” per se, jego stosowanie jest coraz częściej zachęcane, a jego pominięcie coraz częściej uważane jest za złą formę.

Oto kilka kluczowych zalet trybu ścisłego:

  • Ułatwia debugowanie. Błędy kodu, które w przeciwnym razie zostałyby zignorowane lub nie powiodłyby się po cichu, będą teraz generować błędy lub zgłaszać wyjątki, ostrzegając Cię wcześniej o problemach w kodzie i szybciej kierując do ich źródła.
  • Zapobiega przypadkowym globalom. Bez trybu ścisłego przypisanie wartości do niezadeklarowanej zmiennej automatycznie tworzy zmienną globalną o tej nazwie. To jeden z najczęstszych błędów w JavaScript. W trybie ścisłym próba wykonania tego zadania powoduje błąd.
  • Eliminuje this przymus . Bez trybu ścisłego odwołanie do this wartości null lub undefined jest automatycznie zmieniane na globalne. Może to powodować wiele podróbek głowy i wyrywania włosów. W trybie ścisłym odwoływanie się do this wartości null lub undefined generuje błąd.
  • Nie zezwala na zduplikowane nazwy właściwości lub wartości parametrów. Tryb ścisły zgłasza błąd, gdy wykryje duplikat nazwanej właściwości w obiekcie (np. var object = {foo: "bar", foo: "baz"}; ) lub zduplikowany nazwany argument dla funkcji (np. function foo(val1, val2, val1){} ), wyłapując w ten sposób to, co prawie na pewno jest błędem w kodzie, na którego śledzenie mógłbyś zmarnować mnóstwo czasu.
  • Sprawia, że ​​eval() jest bezpieczniejszy. Istnieją pewne różnice w sposobie, w jaki eval() zachowuje się w trybie ścisłym i nieścisłym. Co najważniejsze, w trybie ścisłym zmienne i funkcje zadeklarowane wewnątrz instrukcji eval() nie są tworzone w zakresie zawierającym ( tworzone w zakresie zawierającym w trybie nieścisłym, co również może być częstym źródłem problemów).
  • Zgłasza błąd w przypadku nieprawidłowego użycia polecenia delete . Operatora delete (używanego do usuwania właściwości z obiektów) nie można używać na niekonfigurowalnych właściwościach obiektu. Kod nieścisły zakończy się niepowodzeniem po cichu, gdy zostanie podjęta próba usunięcia niekonfigurowalnej właściwości, podczas gdy tryb ścisły zwróci w takim przypadku błąd.

Zakończyć

Tak jak w przypadku każdej technologii, im lepiej zrozumiesz, dlaczego i jak JavaScript działa, a tym bardziej nie działa, tym bardziej solidny będzie Twój kod i tym bardziej będziesz w stanie efektywnie wykorzystać prawdziwą moc języka. I odwrotnie, brak właściwego zrozumienia paradygmatów i koncepcji JavaScript jest rzeczywiście przyczyną wielu problemów JavaScript.

Dokładne zapoznanie się z niuansami i subtelnościami języka jest najskuteczniejszą strategią doskonalenia znajomości języka i zwiększania produktywności. Unikanie wielu typowych błędów JavaScript pomoże, gdy Twój JavaScript nie działa.

Powiązane: Obietnice JavaScript: samouczek z przykładami