Buggy JavaScript Kodu: JavaScript Geliştiricilerinin Yaptığı En Yaygın 10 Hata
Yayınlanan: 2022-03-11Bugün JavaScript, neredeyse tüm modern web uygulamalarının merkezinde yer almaktadır. Özellikle son birkaç yıl, tek sayfalı uygulama (SPA) geliştirme, grafik ve animasyon ve hatta sunucu tarafı JavaScript platformları için çok çeşitli JavaScript tabanlı kitaplıkların ve çerçevelerin çoğalmasına tanık oldu. JavaScript, web uygulaması geliştirme dünyasında gerçekten yaygın hale geldi ve bu nedenle, ustalaşmak için giderek daha önemli bir beceri haline geldi.
İlk bakışta JavaScript oldukça basit görünebilir. Ve gerçekten de, bir web sayfasına temel JavaScript işlevselliği oluşturmak, JavaScript konusunda yeni olsalar bile, deneyimli yazılım geliştiricileri için oldukça basit bir iştir. Yine de dil, başlangıçta inanılacak olandan çok daha incelikli, güçlü ve karmaşıktır. Aslında JavaScript'in inceliklerinin çoğu, JavaScript'in usta bir geliştiricisi olma arayışında farkında olunması ve kaçınılması gereken - 10 tanesini burada tartışacağız - çalışmasını engelleyen bir dizi yaygın soruna yol açar.
Yaygın Hata #1: this
yanlış referanslar
Bir keresinde bir komedyenin şöyle dediğini duydum:
Ben gerçekten burada değilim, çünkü burada, orada yanında, 't' olmadan ne var?
Bu şaka birçok yönden geliştiriciler için JavaScript'in this
anahtar kelimesiyle ilgili olarak var olan kafa karışıklığını karakterize eder. Yani, this
gerçekten bu mu, yoksa tamamen başka bir şey mi? Yoksa tanımsız mı?
JavaScript kodlama teknikleri ve tasarım kalıpları yıllar içinde giderek daha karmaşık hale geldikçe, oldukça yaygın bir "şu/bu karışıklık" kaynağı olan geri aramalar ve kapatmalar içinde kendi kendine referans alan kapsamların çoğalmasında buna karşılık gelen bir artış oldu.
Bu örnek kod parçacığını göz önünde bulundurun:
Game.prototype.restart = function () { this.clearLocalStorage(); this.timer = setTimeout(function() { this.clearBoard(); // what is "this"? }, 0); };
Yukarıdaki kodu çalıştırmak aşağıdaki hatayla sonuçlanır:
Uncaught TypeError: undefined is not a function
Niye ya?
Her şey bağlamla ilgili. Yukarıdaki hatayı almanızın nedeni, setTimeout()
çağırdığınızda, aslında window.setTimeout()
çağırıyor olmanızdır. Sonuç olarak, setTimeout()
işlevine geçirilen anonim işlev, clearBoard()
yöntemine sahip olmayan window
nesnesi bağlamında tanımlanır.
Geleneksel, eski tarayıcı uyumlu bir çözüm, this
referansınızı daha sonra kapatma tarafından devralınabilecek bir değişkene kaydetmektir; Örneğin:
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); };
Alternatif olarak, daha yeni tarayıcılarda, uygun referansı iletmek için bind()
yöntemini kullanabilirsiniz:
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'! };
Yaygın Hata #2: Blok düzeyinde bir kapsam olduğunu düşünmek
JavaScript İşe Alma Kılavuzumuzda tartışıldığı gibi, JavaScript geliştiricileri arasında yaygın bir kafa karışıklığı kaynağı (ve dolayısıyla yaygın bir hata kaynağı), JavaScript'in her kod bloğu için yeni bir kapsam oluşturduğunu varsaymaktır. Bu, diğer birçok dilde doğru olsa da, JavaScript'te doğru değildir . Örneğin, aşağıdaki kodu göz önünde bulundurun:
for (var i = 0; i < 10; i++) { /* ... */ } console.log(i); // what will this output?
console.log()
çağrısının ya undefined
çıktı vereceğini ya da bir hata vereceğini tahmin ediyorsanız, yanlış tahmin etmişsinizdir. İster inanın ister inanmayın, çıktı 10
. Niye ya?
Diğer dillerin çoğunda, i
değişkeninin "ömrü" (yani kapsamı) for
bloğu ile sınırlandırılacağından, yukarıdaki kod bir hataya yol açacaktır. Ancak JavaScript'te durum böyle değildir ve i
değişkeni, for
döngüsü tamamlandıktan sonra bile kapsamda kalır ve döngüden çıktıktan sonra son değerini korur. (Bu davranış tesadüfen değişken kaldırma olarak bilinir).
Bununla birlikte, blok düzeyinde kapsamlar için desteğin yeni let
anahtar sözcüğü aracılığıyla JavaScript'e girdiğini belirtmekte fayda var. let
anahtar sözcüğü JavaScript 1.7'de zaten mevcuttur ve ECMAScript 6'dan itibaren resmi olarak desteklenen bir JavaScript anahtar sözcüğü olması planlanmıştır.
JavaScript'te yeni misiniz? Kapsamlar, prototipler ve daha fazlasını okuyun.
Yaygın Hata #3: Bellek sızıntıları oluşturma
Bunları önlemek için bilinçli olarak kodlama yapmıyorsanız, bellek sızıntıları neredeyse kaçınılmaz JavaScript sorunlarıdır. Bunların meydana gelmesinin sayısız yolu vardır, bu yüzden sadece birkaç yaygın olayını vurgulayacağız.
Bellek Sızıntısı Örnek 1: Eski nesnelere sarkan referanslar
Aşağıdaki kodu göz önünde bulundurun:
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
Yukarıdaki kodu çalıştırır ve bellek kullanımını izlerseniz, saniyede tam bir megabayt sızdıran büyük bir bellek sızıntısı olduğunu göreceksiniz! Ve manuel bir GC bile yardımcı olmuyor. Dolayısıyla, longStr
her çağrıldığında replaceThing
sızdırıyoruz gibi görünüyor. Ama neden?
Olayları daha detaylı inceleyelim:
Her theThing
nesnesi kendi 1MB longStr
nesnesini içerir. Her saniye, replaceThing
, priorThing içindeki önceki theThing
nesnesine bir başvuruya priorThing
. Ancak yine de bunun bir sorun olacağını düşünmeyiz, çünkü her seferinde daha önce başvurulan priorThing
başvuruda bulunulmaz ( priorThing
, priorThing = theThing;
aracılığıyla sıfırlandığında). Ayrıca, yalnızca replaceThing
ana gövdesinde ve aslında hiç kullanılmayan, unused
işlevde başvurulur.
Yani yine burada neden bir bellek sızıntısı olduğunu merak ediyoruz!?
Neler olduğunu anlamak için, JavaScript'te işlerin nasıl çalıştığını daha iyi anlamamız gerekiyor. Kapanışların uygulanmasının tipik yolu, her işlev nesnesinin sözlüksel kapsamını temsil eden sözlük stili bir nesneye bir bağlantısının olmasıdır. replaceThing
içinde tanımlanan her iki işlev de aslında priorThing
, priorThing
atanmış olsa bile her ikisinin de aynı nesneyi alması önemlidir, bu nedenle her iki işlev de aynı sözcük ortamını paylaşır. Ancak bir değişken herhangi bir kapatma tarafından kullanılır kullanılmaz, o kapsamdaki tüm kapanışlar tarafından paylaşılan sözlüksel ortamda sona erer. Ve bu küçük nüans, bu boğucu bellek sızıntısına yol açan şeydir. (Bununla ilgili daha fazla ayrıntı burada mevcuttur.)
Bellek Sızıntısı Örnek 2: Dairesel referanslar
Bu kod parçasını düşünün:
function addClickHandler(element) { element.click = function onClick(e) { alert("Clicked the " + element.nodeName) } }
Burada onClick
, element
referansı tutan bir kapatmaya sahiptir ( element.nodeName
aracılığıyla). Ayrıca onClick
element.click
atayarak dairesel referans oluşturulur; yani: element
-> onClick
-> element
-> onClick
-> element
…
İlginç bir şekilde, element
DOM'den kaldırılsa bile, yukarıdaki döngüsel öz başvuru, element
ve onClick
toplanmasını ve dolayısıyla bir bellek sızıntısını engeller.
Bellek Sızıntılarını Önlemek: Bilmeniz Gerekenler
JavaScript'in bellek yönetimi (ve özellikle çöp toplama) büyük ölçüde nesne erişilebilirliği kavramına dayanır.
Aşağıdaki nesnelerin erişilebilir olduğu varsayılır ve "kökler" olarak bilinir:
- Geçerli çağrı yığınındaki herhangi bir yerden başvurulan nesneler (yani, şu anda çağrılmakta olan işlevlerdeki tüm yerel değişkenler ve parametreler ve kapatma kapsamındaki tüm değişkenler)
- Tüm küresel değişkenler
Nesneler, en azından herhangi bir kökten bir referans veya bir referans zinciri yoluyla erişilebilir oldukları sürece bellekte tutulur.
Tarayıcıda, erişilemeyen nesneler tarafından işgal edilen belleği temizleyen bir Çöp Toplayıcı (GC) vardır; yani, nesneler ancak ve ancak GC onların erişilemez olduğuna inanırsa bellekten kaldırılacaktır. Ne yazık ki, aslında artık kullanılmayan, ancak GC'nin hala "ulaşılabilir" olduğunu düşündüğü, artık kullanılmayan "zombi" nesnelere sahip olmak oldukça kolaydır.
Yaygın Hata #4: Eşitlik hakkında kafa karışıklığı
JavaScript'teki kolaylıklardan biri, bir boole bağlamında başvurulan herhangi bir değeri otomatik olarak bir boole değerine zorlamasıdır. Ancak bunun uygun olduğu kadar kafa karıştırıcı olabileceği durumlar da vardır. Örneğin, aşağıdakilerden bazılarının birçok JavaScript geliştiricisini ısırdığı bilinmektedir:
// 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 ([]) // ...
Son ikisiyle ilgili olarak, boş olmalarına rağmen (ki bu, bir kişinin false
olarak değerlendirileceğine inanmasına neden olabilir), hem {}
hem de []
aslında nesnelerdir ve herhangi bir nesne JavaScript'te true
boolean değerine zorlanır, ECMA-262 spesifikasyonu ile uyumludur.
Bu örneklerin gösterdiği gibi, zorlama türünün kuralları bazen çamur kadar açık olabilir. Buna göre, tür zorlaması açıkça istenmedikçe, tür zorlamanın istenmeyen yan etkilerinden kaçınmak için genellikle en iyisi ===
ve !==
( ==
ve !=
yerine) kullanmaktır. ( ==
ve !=
iki şeyi karşılaştırırken otomatik olarak tür dönüştürmeyi gerçekleştirirken, ===
ve !==
aynı karşılaştırmayı tür dönüştürme olmadan yapar.)
Ve tamamen bir yan nokta olarak - ancak tür zorlaması ve karşılaştırmalardan bahsettiğimiz için - NaN
herhangi bir şeyle (hatta NaN
!) karşılaştırmanın her zaman false
döndüreceğini belirtmekte fayda var. Bu nedenle, bir değerin NaN
olup olmadığını belirlemek için eşitlik operatörlerini ( ==
, ===
, !=
, !==
) kullanamazsınız. Bunun yerine yerleşik global isNaN()
işlevini kullanın:
console.log(NaN == NaN); // false console.log(NaN === NaN); // false console.log(isNaN(NaN)); // true
Yaygın Hata #5: Verimsiz DOM manipülasyonu
JavaScript, DOM'yi değiştirmeyi (yani öğeleri eklemeyi, değiştirmeyi ve kaldırmayı) nispeten kolaylaştırır, ancak bunu verimli bir şekilde yapmayı teşvik etmek için hiçbir şey yapmaz.

Yaygın bir örnek, birer birer bir dizi DOM Öğesi ekleyen koddur. DOM öğesi eklemek pahalı bir işlemdir. Arka arkaya birden çok DOM öğesi ekleyen kod verimsizdir ve muhtemelen iyi çalışmayacaktır.
Birden çok DOM öğesinin eklenmesi gerektiğinde etkili bir alternatif, bunun yerine belge parçalarını kullanmak, böylece hem verimliliği hem de performansı artırmaktır.
Örneğin:
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));
Bu yaklaşımın doğal olarak geliştirilmiş verimliliğine ek olarak, bağlı DOM öğeleri oluşturmak pahalıdır, oysa bunları ayrılmış halde oluşturup değiştirmek ve ardından eklemek çok daha iyi performans sağlar.
Yaygın Hata #6: Döngüler for
işlev tanımlarının yanlış kullanımı
Bu kodu göz önünde bulundurun:
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); }; }
Yukarıdaki koda göre, 10 giriş öğesi olsaydı, bunlardan herhangi birine tıklandığında “Bu, 10 numaralı öğedir” görüntülenir! Bunun nedeni, öğelerden herhangi biri için onclick
çağrıldığında, yukarıdaki for döngüsü tamamlanmış olacak ve i
değeri zaten 10 olacaktır ( hepsi için).
İstenen davranışı elde etmek için yukarıdaki kod sorunlarını şu şekilde düzeltebiliriz:
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); }
Kodun bu gözden geçirilmiş versiyonunda, döngüden her makeHandler
hemen yürütülür, her seferinde o anki i+1
değerini alır ve onu kapsamlı bir num
değişkenine bağlar. Dış işlev, iç işlevi döndürür (bu kapsamlı num
değişkenini de kullanır) ve öğenin onclick
bu iç işleve ayarlanır. Bu, her bir onclick
uygun i
değerini (kapsamlı num
değişkeni aracılığıyla) almasını ve kullanmasını sağlar.
Yaygın Hata #7: Prototip mirasından uygun şekilde yararlanamamak
JavaScript geliştiricilerinin şaşırtıcı derecede yüksek bir yüzdesi, prototip kalıtımın özelliklerini tam olarak anlayamamakta ve dolayısıyla tam olarak kullanamamaktadır.
İşte basit bir örnek. Bu kodu göz önünde bulundurun:
BaseObject = function(name) { if(typeof name !== "undefined") { this.name = name; } else { this.name = 'default' } };
Oldukça basit görünüyor. Bir ad verirseniz, onu kullanın, aksi takdirde adı 'varsayılan' olarak ayarlayın; Örneğin:
var firstObj = new BaseObject(); var secondObj = new BaseObject('unique'); console.log(firstObj.name); // -> Results in 'default' console.log(secondObj.name); // -> Results in 'unique'
Ama ya şunu yapsaydık:
delete secondObj.name;
O zaman şunu alırdık:
console.log(secondObj.name); // -> Results in 'undefined'
Ancak bunun 'varsayılan'a dönmesi daha iyi olmaz mıydı? Orijinal kodu, prototip kalıtımdan yararlanmak için aşağıdaki gibi değiştirirsek, bu kolayca yapılabilir:
BaseObject = function (name) { if(typeof name !== "undefined") { this.name = name; } }; BaseObject.prototype.name = 'default';
Bu sürümle, BaseObject
name
özelliğini prototype
nesnesinden devralır ve burada (varsayılan olarak) 'default'
olarak ayarlanır. Bu nedenle, yapıcı bir ad olmadan çağrılırsa, ad varsayılan olarak default
olur. Ve benzer şekilde, name
özelliği bir BaseObject
örneğinden kaldırılırsa, prototip zinciri aranır ve name
özelliği, değerinin hala 'default'
olduğu prototype
nesnesinden alınır. Şimdi şunu elde ederiz:
var thirdObj = new BaseObject('unique'); console.log(thirdObj.name); // -> Results in 'unique' delete thirdObj.name; console.log(thirdObj.name); // -> Results in 'default'
Yaygın Hata #8: Örnek yöntemlerine yanlış referanslar oluşturma
Basit bir nesne tanımlayalım ve onun örneğini aşağıdaki gibi oluşturalım:
var MyObject = function() {} MyObject.prototype.whoAmI = function() { console.log(this === window ? "window" : "MyObj"); }; var obj = new MyObject();
Şimdi, kolaylık olması açısından, muhtemelen whoAmI
yöntemine bir referans oluşturalım, böylece daha uzun obj.whoAmI()
whoAmI()
ile erişebiliriz:
var whoAmI = obj.whoAmI;
Ve her şeyin uyumlu göründüğünden emin olmak için, hadi yeni whoAmI
değişkenimizin değerini yazdıralım:
console.log(whoAmI);
Çıktılar:
function () { console.log(this === window ? "window" : "MyObj"); }
Tamam iyi. İyi görünüyor.
Ama şimdi, obj.whoAmI()
ile kolaylık referansımız whoAmI()
çağırdığımızdaki farka bakın:
obj.whoAmI(); // outputs "MyObj" (as expected) whoAmI(); // outputs "window" (uh-oh!)
Ne yanlış gitti?
Buradaki saçmalık, atamayı yaptığımızda var whoAmI = obj.whoAmI;
, whoAmI
global ad alanında tanımlanmakta olan yeni değişken. Sonuç olarak, this
değeri, obj
MyObject
örneği değil , window
!
Bu nedenle, bir nesnenin mevcut bir yöntemine gerçekten bir başvuru oluşturmamız gerekiyorsa, this
öğesinin değerini korumak için bunu o nesnenin ad alanı içinde yaptığımızdan emin olmamız gerekir. Bunu yapmanın bir yolu, örneğin aşağıdaki gibi olacaktır:
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)
Yaygın Hata #9: setTimeout
veya setInterval
için ilk argüman olarak bir dize sağlama
Yeni başlayanlar için, burada bir şeyi açıklığa kavuşturalım: setTimeout
veya setInterval
için ilk argüman olarak bir dize sağlamak başlı başına bir hata değildir . Tamamen meşru JavaScript kodudur. Buradaki sorun daha çok performans ve verimlilikle ilgili. Nadiren açıklanan şey, başlık altında, setTimeout
veya setInterval
öğesine ilk argüman olarak bir dize iletirseniz, bunun yeni bir işleve dönüştürülmek üzere işlev yapıcısına iletileceğidir. Bu süreç yavaş ve verimsiz olabilir ve nadiren gereklidir.
Bu yöntemlere ilk argüman olarak bir dize geçirmenin alternatifi, bunun yerine bir işlev iletmektir . Bir örneğe bakalım.
O halde burada setInterval
ve setTimeout
oldukça tipik bir kullanımı olur ve ilk parametre olarak bir dize geçirilir:
setInterval("logTime()", 1000); setTimeout("logMessage('" + msgValue + "')", 1000);
İlk argüman olarak bir fonksiyon iletmek daha iyi bir seçim olacaktır; Örneğin:
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);
Yaygın Hata #10: "Sıkı mod" kullanılmaması
JavaScript İşe Alma Kılavuzumuzda açıklandığı gibi, "katı mod" (yani, JavaScript kaynak dosyalarınızın başında 'use strict';
dahil) gönüllü olarak JavaScript kodunuzda çalışma zamanında daha katı ayrıştırma ve hata işlemeyi zorunlu kılmanın bir yoludur. daha güvenli hale getirmek gibi.
Kabul etmek gerekir ki, katı modu kullanmamak başlı başına bir "hata" olmasa da, kullanımı giderek daha fazla teşvik edilmekte ve ihmal edilmesi giderek kötü bir biçim olarak kabul edilmektedir.
Katı modun bazı önemli avantajları şunlardır:
- Hata ayıklamayı kolaylaştırır. Aksi takdirde yok sayılacak veya sessizce başarısız olacak kod hataları artık hatalar üretecek veya istisnalar oluşturarak sizi kodunuzdaki sorunlar konusunda daha erken uyaracak ve sizi daha hızlı kaynaklarına yönlendirecektir.
- Kazara küreselleri önler. Kesin mod olmadan, bildirilmemiş bir değişkene değer atamak, otomatik olarak bu ada sahip bir global değişken oluşturur. Bu, JavaScript'teki en yaygın hatalardan biridir. Katı modda, bunu yapmaya çalışmak bir hata verir.
-
this
zorlamayı ortadan kaldırır . Katı mod olmadan,this
null veya undefined değerine yapılan bir başvuru otomatik olarak global'e zorlanır. Bu, birçok sahtekarlığa ve saçınızı çekecek türden böceklere neden olabilir. Katı modda,this
null veya undefined değerine aa başvurulması bir hataya neden olur. - Yinelenen özellik adlarına veya parametre değerlerine izin vermez. Katı mod, bir nesnede yinelenen adlandırılmış bir özellik (örneğin,
var object = {foo: "bar", foo: "baz"};
) veya bir işlev için yinelenen adlandırılmış bir bağımsız değişken (örneğin,function foo(val1, val2, val1){}
), böylece kodunuzdaki neredeyse kesinlikle bir hatayı yakalarsınız, aksi takdirde çok fazla zaman kaybedersiniz. - eval()'i daha güvenli hale getirir.
eval()
'in katı modda ve katı olmayan modda davranış biçiminde bazı farklılıklar vardır. En önemlisi, katı modda, bireval()
ifadesinin içinde bildirilen değişkenler ve işlevler, kapsayıcı kapsamda oluşturulmaz (bunlar , katı olmayan modda kapsayıcı kapsamda oluşturulur, bu da yaygın bir sorun kaynağı olabilir). - geçersiz
delete
kullanımında hata verir.delete
operatörü (nesnelerden özellikleri kaldırmak için kullanılır), nesnenin yapılandırılamayan özelliklerinde kullanılamaz. Yapılandırılamaz bir özelliği silme girişiminde bulunulduğunda, katı olmayan kod sessizce başarısız olur, oysa katı mod böyle bir durumda bir hata verir.
Sarmak
Her teknolojide olduğu gibi, JavaScript'in neden ve nasıl çalışıp çalışmadığını ne kadar iyi anlarsanız, kodunuz o kadar sağlam olur ve dilin gerçek gücünden o kadar etkin bir şekilde yararlanabilirsiniz. Tersine, JavaScript paradigmalarının ve kavramlarının doğru şekilde anlaşılmaması, aslında birçok JavaScript sorununun yattığı yerdir.
Dilin nüanslarına ve inceliklerine iyice alışmak, yeterliliğinizi geliştirmek ve üretkenliğinizi artırmak için en etkili stratejidir. JavaScript'iniz çalışmadığında birçok yaygın JavaScript hatasından kaçınmak yardımcı olacaktır.