JavaScript, esnekliği ve gücüyle modern web geliştirmenin temel taşlarından biridir. Ancak bu esneklik, beraberinde bazı karmaşıklıkları ve “tuzakları” da getirir. Özellikle teknik mülakatlarda, adayların JavaScript’in bu ince detaylarını ne kadar iyi anladığını ölçmek için bu “gotcha” noktaları sıkça sorulur. Bu rehberde, JavaScript’in en yaygın tuzaklarını ve bunların üstesinden nasıl gelineceğini detaylı kod örnekleri ve pratik bilgilerle inceleyeceğiz.
JavaScript’te Kapsam (Scope) ve Hoisting Mekanizmaları
Kapsam (scope), değişkenlerin ve fonksiyonların erişilebilirliğini belirleyen bir kavramdır. JavaScript’te üç ana kapsam türü bulunur: global, fonksiyon (function) ve blok (block) kapsamı. Hoisting ise, kodunuz çalıştırılmadan önce değişken ve fonksiyon tanımlamalarının JavaScript motoru tarafından nasıl yukarı taşındığını ifade eder.
var, let ve const Arasındaki Farklar
var(Function Scope):varile tanımlanan değişkenler, içinde bulundukları fonksiyonun tamamında erişilebilir olur. Global kapsamda tanımlanmışsa, tüm kodda erişilebilir.varile tanımlanan değişkenler hoist edilir; yani, bildirimleri fiziksel olarak kodun en üstüne taşınır, ancak atamaları taşınmaz. Bu durum beklenmedik davranışlara yol açabilir.letveconst(Block Scope): ES6 ile gelenletveconst, blok kapsamına sahiptir. Bu, değişkenlerin yalnızca tanımlandıkları süslü parantezler{}içinde (if blokları, for döngüleri vb.) erişilebilir olduğu anlamına gelir. Her ikisi de hoist edilir, ancak “Temporal Dead Zone” (TDZ) adı verilen bir durum nedeniyle, tanımlandıkları satıra gelene kadar erişilemezler.const(Sabitlik):constdaletgibi blok kapsamlıdır ancak bir kez atama yapıldıktan sonra değeri yeniden değiştirilemez. Nesneler ve diziler için, referansın sabit olduğu anlamına gelir, içeriğinin değil.
Kod Örnekleri: Hoisting ve Kapsam
var ile Hoisting Tuzağı:
console.log(degiskenVar); // undefined (bildirim hoist edildi, atama edilmedi)
var degiskenVar = "Merhaba var!";
console.log(degiskenVar); // Merhaba var!
function ornekFonksiyon() {
console.log(icDegisken); // undefined
var icDegisken = "Fonksiyon içi var";
console.log(icDegisken); // Fonksiyon içi var
}
ornekFonksiyon();
// console.log(icDegisken); // ReferenceError: icDegisken is not defined (fonksiyon kapsamı)
let ile Temporal Dead Zone (TDZ):
// console.log(degiskenLet); // ReferenceError: Cannot access 'degiskenLet' before initialization (TDZ)
let degiskenLet = "Merhaba let!";
console.log(degiskenLet); // Merhaba let!
if (true) {
// console.log(blokDegisken); // ReferenceError: Cannot access 'blokDegisken' before initialization
let blokDegisken = "Blok içi let";
console.log(blokDegisken); // Blok içi let
}
// console.log(blokDegisken); // ReferenceError: blokDegisken is not defined (blok kapsamı)
Pratik Bilgi: Her zaman let ve const kullanmayı tercih edin. Bu, kodunuzu daha öngörülebilir hale getirir ve var‘ın neden olduğu yaygın hoisting ve kapsam sorunlarından kaçınmanıza yardımcı olur. Özellikle döngülerde var kullanmak, kapanış (closure) ile ilgili tuzaklara yol açabilir (aşağıda ele alınacak).
this Anahtar Kelimesinin Gizemleri
this anahtar kelimesi, JavaScript’teki en kafa karıştırıcı konulardan biridir. Değeri, fonksiyonun nasıl çağrıldığına bağlı olarak değişir. Yani, this‘in değeri dinamiktir ve çalışma zamanında belirlenir.
this Bağlamının Belirlenmesi
- Global Bağlam: Tarayıcıda
windownesnesine (katı moddaundefined), Node.js’deglobalnesnesine (katı moddaundefined) işaret eder. - Metot Çağrısı: Bir nesnenin metodu olarak çağrıldığında,
thiso nesneye işaret eder. - Basit Fonksiyon Çağrısı: Katı olmayan modda
window/globalnesnesine, katı modda ('use strict') iseundefined‘a işaret eder. - Yapıcı Fonksiyon (Constructor) Çağrısı (
newile): Yeni oluşturulan nesneye işaret eder. call(),apply(),bind()Metotları:thisdeğerini açıkça belirlememizi sağlarlar.- Arrow Fonksiyonlar: Kendi
thisbağlamlarını oluşturmazlar. Bunun yerine, tanımlandıkları lexical (sözcüksel) kapsamdakithisdeğerini miras alırlar. Bu,thisile ilgili birçok yaygın tuzağı çözer.
Kod Örnekleri: this Bağlamı
// 1. Global Bağlam
console.log(this === window); // Tarayıcıda: true (katı modda değilse)
// 2. Metot Çağrısı
const kisi = {
ad: "Ali",
selamla: function() {
console.log(Merhaba, ben ${this.ad});
}
};
kisi.selamla(); // Merhaba, ben Ali (this -> kisi)
// 3. Basit Fonksiyon Çağrısı (Metottan ayrılmış)
const selamlaFunc = kisi.selamla;
selamlaFunc(); // Merhaba, ben undefined (this -> window/global veya undefined strict modda)
// 4. Arrow Fonksiyonlar ile this
const kisi2 = {
ad: "Ayşe",
selamlaGecikmeli: function() {
// Geleneksel fonksiyon: this bağlamı setTimeout içinde değişir
setTimeout(function() {
console.log(Gecikmeli (geleneksel): Merhaba, ben ${this.ad}); // this -> window/global
}, 100);
// Arrow fonksiyon: this bağlamını dışarıdan (lexical scope) miras alır
setTimeout(() => {
console.log(Gecikmeli (arrow): Merhaba, ben ${this.ad}); // this -> kisi2
}, 200);
}
};
kisi2.selamlaGecikmeli();
// Çıktı:
// Gecikmeli (geleneksel): Merhaba, ben undefined
// Gecikmeli (arrow): Merhaba, ben Ayşe
Pratik Bilgi: this ile ilgili beklenmedik davranışlardan kaçınmak için şunları yapın:
- Sınıf metotlarında veya callback fonksiyonlarında
thisbağlamını korumak için arrow fonksiyonları kullanın. - Gerekirse
bind(),call()veyaapply()metotlarını kullanarakthisbağlamını açıkça belirtin.
Tip Dönüşümü (Type Coercion) ve Eşitlik Karşılaştırmaları
JavaScript, “gevşek tipli” (loosely typed) bir dildir; yani değişkenlerin türünü açıkça belirtmeniz gerekmez. Bu esneklik, bazen otomatik tip dönüşümlerine (type coercion) yol açar ve bu durum eşitlik karşılaştırmalarında beklenmedik sonuçlar doğurabilir.
== (Gevşek Eşitlik) vs. === (Katı Eşitlik)
==(Abstract Equality Comparison): Bu operatör, karşılaştırma yapmadan önce farklı türdeki değerleri otomatik olarak aynı türe dönüştürmeye çalışır. Bu, genellikle beklenmedik ve hata potansiyeli yüksek durumlara yol açar.===(Strict Equality Comparison): Bu operatör, hem değerin hem de türün aynı olmasını gerektirir. Otomatik tip dönüşümü yapmaz. Bu, daha güvenli ve öngörülebilir bir karşılaştırma yöntemidir.
Diğer Tip Dönüşümü Tuzakları
typeof null: JavaScript’in eski bir hatası nedeniyletypeof nulldeğeri"object"döndürür. Bu,null‘ın özel bir ilkel değer olmasına rağmen böyledir.NaNDavranışı:NaN(Not-a-Number) değeri, kendine bile eşit değildir (NaN === NaNher zamanfalse‘dur). Bir değerinNaNolup olmadığını kontrol etmek içinNumber.isNaN()veya eskiisNaN()fonksiyonunu kullanmalısınız.- Falsy Değerler: JavaScript’te
false,0,""(boş string),null,undefinedveNaNgibi değerler “falsy” kabul edilir. Bu, boolean bağlamında (örneğin birifkoşulunda)falseolarak değerlendirildikleri anlamına gelir.
Kod Örnekleri: Tip Dönüşümü ve Eşitlik
// == vs ===
console.log(1 == '1'); // true (string '1' sayı 1'e dönüştürülür)
console.log(1 === '1'); // false (türler farklı)
console.log(0 == false); // true
console.log(0 === false); // false
console.log(null == undefined); // true
console.log(null === undefined); // false
// typeof null
console.log(typeof null); // "object" (bir hata!)
console.log(null === null); // true (değer ve tür aynı)
// NaN Davranışı
console.log(NaN == NaN); // false
console.log(NaN === NaN); // false
console.log(Number.isNaN(NaN)); // true
console.log(Number.isNaN(123)); // false
console.log(Number.isNaN("abc")); // false (Number.isNaN sadece NaN kontrol eder)
console.log(isNaN("abc")); // true (isNaN önce sayıya dönüştürmeye çalışır)
// Falsy Değerler
if (0) { console.log("0 true"); } else { console.log("0 false"); } // 0 false
if ("") { console.log("boş string true"); } else { console.log("boş string false"); } // boş string false
if (null) { console.log("null true"); } else { console.log("null false"); } // null false
Pratik Bilgi: Her zaman === kullanın. Bu, kodunuzu daha güvenli, daha anlaşılır ve hatalara karşı daha dirençli hale getirir. typeof null tuzağını unutmayın ve NaN kontrolü için Number.isNaN() kullanın.
Kapanışlar (Closures) ve Asenkron JavaScript’in Tuzakları
Kapanışlar (closures) ve asenkron programlama, JavaScript’in gücünü gösteren ancak aynı zamanda yaygın hata kaynakları olabilen iki temel konudur.
Kapanışlar (Closures) ile var Tuzağı
Kapanış, bir fonksiyonun kendi dış kapsamındaki değişkenlere, dış fonksiyon sona ermiş olsa bile erişebilmesi durumudur. Bu güçlü bir özelliktir ancak var ile döngülerde kullanıldığında yaygın bir tuzağa yol açar.
Kod Örnekleri: Kapanış ve var Tuzağı
// var ile kapanış tuzağı
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(Döngü (var): ${i});
}, 100 * i);
}
// Beklenen çıktı: 0, 1, 2
// Gerçek çıktı: 3, 3, 3 (Çünkü setTimeout'lar çalıştığında döngü bitmiş ve i değeri 3 olmuştur.
// Her kapanış aynı 'i' değişkenine referans verir.)
// let ile çözüm
for (let j = 0; j < 3; j++) {
setTimeout(function() {
console.log(Döngü (let): ${j});
}, 100 * j);
}
// Beklenen ve Gerçek çıktı: 0, 1, 2 (Her iterasyonda 'j' için yeni bir blok kapsamı oluşturulur.)
Pratik Bilgi: Döngülerde zamanlanmış işlemler (örneğin setTimeout) veya olay dinleyicileri gibi kapanış gerektiren durumlarda var yerine let kullanın. let, her döngü iterasyonu için yeni bir değişken bağlamı oluşturarak bu tuzağı ortadan kaldırır.
Asenkron JavaScript ve Olay Döngüsü (Event Loop)
JavaScript tek iş parçacıklı (single-threaded) bir dildir ancak asenkron işlemleri (API çağrıları, setTimeout, Promise‘lar) Olay Döngüsü (Event Loop) sayesinde yönetir. Bu, bazı işlemlerin anında değil, belirli bir sıra ve gecikmeyle yürütüldüğü anlamına gelir.
Kod Örnekleri: Asenkronluk ve Olay Döngüsü
console.log("1. Başlangıç");
setTimeout(() => {
console.log("2. setTimeout (Makro Görev)");
}, 0); // 0 milisaniye bile olsa, bir sonraki olay döngüsü turunda çalışır.
Promise.resolve().then(() => {
console.log("3. Promise.resolve (Mikro Görev)");
});
console.log("4. Bitiş");
// Beklenen çıktı:
// 1. Başlangıç
// 4. Bitiş
// 3. Promise.resolve (Mikro görevler makro görevlerden önce işlenir)
// 2. setTimeout (En son işlenir)
Pratik Bilgi: Olay Döngüsü, Mikro Görev Kuyruğu (Promise’lar) ve Makro Görev Kuyruğu (setTimeout, setInterval) arasındaki farkı anlamak, asenkron kodunuzun yürütme sırasını tahmin etmek için hayati öneme sahiptir. Mikro görevler (Promise resolve/reject) her zaman bir sonraki makro görevden önce işlenir.
Sonuç: Sıkça Sorulan Sorular (SSS)
JavaScript’in bu “tuzaklarını” anlamak, sadece mülakatlarda başarılı olmanızı sağlamakla kalmaz, aynı zamanda daha sağlam, öngörülebilir ve hatasız kod yazmanıza yardımcı olur. İşte bu konularda sıkça karşılaşılan bazı sorular:
Neden var kullanmaktan kaçınmalıyız?
var, fonksiyon kapsamlıdır ve hoisting davranışı nedeniyle beklenmedik hatalara yol açabilir. Özellikle döngülerde kapanışlarla birlikte kullanıldığında yaygın sorunlar yaratır. let ve const, blok kapsamı ve Temporal Dead Zone (TDZ) ile bu sorunları çözerek daha güvenli ve öngörülebilir bir değişken yönetimi sunar.
null ve undefined arasındaki temel fark nedir?
undefined, bir değişkenin değer atanmadığını veya bir özelliğin var olmadığını gösteren bir ilkel tiptir. null ise, kasıtlı olarak bir “değer yokluğu”nu temsil etmek için atanan ilkel bir değerdir. typeof undefined => "undefined", typeof null => "object" (tarihsel bir hata).
Arrow fonksiyonları this bağlamını nasıl etkiler?
Arrow fonksiyonlar, kendi this bağlamlarını oluşturmazlar. Bunun yerine, tanımlandıkları sözcüksel (lexical) kapsamdaki this değerini miras alırlar. Bu, özellikle callback fonksiyonlarında this bağlamını korumak için çok kullanışlıdır ve bind() kullanma ihtiyacını azaltır.
JavaScript tek iş parçacıklı (single-threaded) ise asenkronluk nasıl çalışır?
JavaScript motoru tek iş parçacıklı olsa da, tarayıcılar (veya Node.js çalışma zamanları) Web API’leri (setTimeout, fetch vb.) gibi ek iş parçacıkları veya sistem işlemleri sağlar. Bu asenkron işlemler tamamlandığında, ilgili callback fonksiyonları Görev Kuyruğu’na (Task Queue) eklenir. JavaScript’in Olay Döngüsü, çağrı yığını (call stack) boşaldığında bu kuyruktan görevleri alır ve yürütür. Promise’lar ise Mikro Görev Kuyruğu’nu kullanarak, bir sonraki olay döngüsü turundan önce çalıştırılır.
Bir kapanış (closure) ne zaman kullanışlıdır?
Kapanışlar, özellikle veri gizliliği (private değişkenler oluşturma), fonksiyon fabrikaları (özelleştirilmiş fonksiyonlar döndürme), sayaçlar veya olay dinleyicileri gibi durumlarda çok kullanışlıdır. Örneğin, bir dış fonksiyonun yerel değişkenlerine erişebilen iç fonksiyonlar oluşturarak, bu değişkenleri dış dünyadan izole edebilirsiniz.