JavaScript'te var fonksiyon kapsamlıdır, aynı kapsamda tekrar bildirilebilir ve bildirildiği satırdan önce okunduğunda undefined verir. let ve const ise blok kapsamlıdır, aynı kapsamda tekrar bildirilemez ve bildirimden önce erişildiğinde ReferenceError fırlatır. const ek olarak ilk değer ister ve yeniden atanamaz. Modern kodda varsayılan seçim const, değeri değişecek değişkenler için let olmalıdır.

Bu yazıda üç anahtar kelimenin farklarını çalıştırılabilir örneklerle inceliyoruz: kapsam, yeniden bildirim, yeniden atama, global nesne üzerindeki etki ve geçici ölü bölge (TDZ). Bildirimlerin kod çalışmadan önce nasıl işlendiğini ise hoisting ve TDZ yazısında ayrıca ele aldık.

Kısa karşılaştırma

Kısa karşılaştırma: Kapsam, Aynı kapsamda yeniden bildirim, Yeniden atama, İlk değer zorunlu mu
Özellikvarletconst
Kapsamfonksiyon (yoksa modül veya global)blokblok
Aynı kapsamda yeniden bildirimizinliSyntaxErrorSyntaxError
Yeniden atamaizinliizinliTypeError
İlk değer zorunlu muhayırhayırevet
Bildirimden önce erişimundefinedReferenceError (TDZ)ReferenceError (TDZ)
Betik kökünde globalThis özelliği oluşturur muevethayırhayır
Tek başına if gövdesi olabilir mievethayırhayır

Kapsam: fonksiyon mu, blok mu?

var ile tanımlanan değişken, onu saran en yakın fonksiyon gövdesine (veya sınıfların static başlatma bloğuna) aittir. Hiçbiri yoksa modül ya da global kapsama düşer. if, for, try...catch veya düz bir { } bloğu var için yeni bir kapsam açmaz. let ve const ise içinde bulundukları bloğa bağlıdır.

javascript
function varOrnegi() {
  var x = 1;
  {
    var x = 2; // aynı değişken
    console.log(x); // 2
  }
  console.log(x); // 2
}

function letOrnegi() {
  let x = 1;
  {
    let x = 2; // farklı değişken
    console.log(x); // 2
  }
  console.log(x); // 1
}

varOrnegi();
letOrnegi();

Aynı fark döngülerde de görülür. for başlığında var ile tanımlanan sayaç döngü bittikten sonra da erişilebilir kalır; let ile tanımlanan sayaç ise yalnızca döngünün içinde yaşar.

javascript
for (var i = 0; i < 3; i++) {}
console.log(i); // 3

for (let j = 0; j < 3; j++) {}
console.log(typeof j); // undefined (j döngü dışında tanımlı değil)

Döngü içindeki geri çağrılarda let farkı

Farkın en çok hissedildiği yer, döngü içinde daha sonra çalışacak fonksiyonlar oluşturduğunuz durumdur. var ile tüm fonksiyonlar tek bir değişkeni paylaşır ve döngü bittiğinde o değişkenin son değerini görür. let ise her yinelemede yeni bir bağlama (binding) oluşturur, böylece her fonksiyon kendi değerini hatırlar.

javascript
const varIle = [];
for (var i = 0; i < 3; i++) {
  varIle.push(() => i);
}
console.log(varIle.map((f) => f())); // [ 3, 3, 3 ]

const letIle = [];
for (let k = 0; k < 3; k++) {
  letIle.push(() => k);
}
console.log(letIle.map((f) => f())); // [ 0, 1, 2 ]

Bu davranışın arkasındaki mekanizma closure'dır; ayrıntıları closure nedir yazısında bulabilirsiniz. Zamanlayıcı ile çalışan geri çağrıların ne zaman çalıştığını ise event loop yazısı açıklıyor.

Yeniden bildirim

Yeniden bildirim

var ile aynı adı ikinci kez bildirmek hata vermez, strict mode'da bile. İlk değer verilmeyen ikinci bildirim mevcut değeri silmez. let ve const ise aynı kapsamda başka hiçbir bildirimle (let, const, var, function, class veya import) yan yana duramaz. Bu bir SyntaxError olduğu için kod hiç çalışmaya başlamaz.

javascript
var a = 1;
var a = 2;
console.log(a); // 2
var a;
console.log(a); // 2 (değer korunur)

// Aşağıdaki satırlar açılırsa dosya hiç çalışmaz:
// let b = 1;
// let b = 2; // SyntaxError: Identifier 'b' has already been declared
// const c = 1;
// var c = 2; // SyntaxError: Identifier 'c' has already been declared

İç içe bloklarda aynı adı kullanmak ise yeniden bildirim sayılmaz; içteki değişken dıştakini gölgeler (shadowing). Dikkat edilmesi gereken bir durum switch ifadesidir: tüm case dalları tek bir blok paylaştığı için iki dalda aynı adla let yazmak hata verir. Her dalı kendi süslü parantezine almak sorunu çözer.

javascript
const durum = 1;
switch (durum) {
  case 0: {
    let mesaj = "sıfır";
    console.log(mesaj);
    break;
  }
  case 1: {
    let mesaj = "bir";
    console.log(mesaj); // bir
    break;
  }
}

Yeniden atama ve const

const bir değişken adını bir değere kalıcı olarak bağlar. Bu bağlamayı değiştirmeye çalışmak çalışma anında TypeError fırlatır. İlk değer vermeden const yazmak ise doğrudan sözdizimi hatasıdır: const SABIT; satırı "Missing initializer in const declaration" mesajıyla reddedilir.

javascript
const PORT = 3000;
try {
  PORT = 8080;
} catch (e) {
  console.log(e.name + ": " + e.message); // TypeError: Assignment to constant variable.
}

Önemli ayrıntı şudur: const değeri değil, bağlamayı sabitler. Değer bir nesne veya dizi ise içeriği değiştirilebilir. Nesnenin kendisini de dondurmak istiyorsanız Object.freeze() kullanabilirsiniz. Dondurulmuş nesnede değişiklik denemesi strict mode dışında sessizce yok sayılır, strict mode'da ise TypeError fırlatır.

javascript
const ayar = { port: 80 };
ayar.port = 8080;      // izinli: nesnenin içeriği değişiyor
console.log(ayar.port); // 8080

const liste = [];
liste.push("a");
console.log(liste); // [ 'a' ]

const sabitAyar = Object.freeze({ port: 80 });
sabitAyar.port = 8080; // strict mode dışında sessizce yok sayılır
console.log(sabitAyar.port); // 80

Bildirimden önce erişim ve TDZ

var bildirimi kod çalışmadan önce işlenir ve değişken undefined değeriyle hazır bekler. let ve const de kapsamın başından itibaren var olur, ancak bildirim satırına ulaşılana kadar başlatılmamış durumdadır. Bu aralığa geçici ölü bölge (temporal dead zone, TDZ) denir ve bu sırada değişkene erişmek ReferenceError fırlatır.

javascript
{
  console.log(eski); // undefined
  try {
    console.log(yeni);
  } catch (e) {
    console.log(e.message); // Cannot access 'yeni' before initialization
  }
  var eski = 1;
  let yeni = 2;
}

TDZ'nin neden "geçici" olarak adlandırıldığı, typeof ile etkileşimi ve class bildirimlerindeki karşılığı hoisting ve TDZ yazısında ayrıntılı örneklerle anlatılıyor.

Global nesne üzerindeki etki

Klasik betik (script) modunda en üst düzeyde var ile tanımlanan değişken global nesnenin silinemeyen bir özelliği olur. let ve const ise global kapsamda olsalar bile globalThis üzerinde özellik oluşturmaz. Node.js'in CommonJS modüllerinde ve ES modüllerinde ise en üst düzeydeki bildirimler zaten modüle aittir ve global nesneye eklenmez.

javascript
// Betik modunda (ör. tarayıcı konsolunda veya node -e ile):
var x = 1;
let y = 2;
console.log(globalThis.x, globalThis.y); // 1 undefined

Yukarıdaki kodu bir dosyaya yazıp Node.js ile çalıştırırsanız dosya CommonJS modülü olarak yüklendiği için iki değer de undefined çıkar. Modül kapsamı hakkında daha fazlası için ES modülleri ve scope (kapsam) yazılarına bakabilirsiniz.

Tek satırda birden fazla bildirim ve destructuring

Üç anahtar kelime de virgülle ayrılmış bir bildirim listesi kabul eder. Buradaki virgüller virgül operatörü, eşittir işaretleri de atama operatörü değildir; listenin bir parçasıdır. Listede sonra gelen değişkenin ilk değeri, kendisinden önce tanımlanan değişkeni kullanabilir. Eşittir işaretinin sol tarafına bir destructuring kalıbı da yazabilirsiniz; böylece bir nesneden veya diziden aynı anda birden fazla const değişkeni oluşturursunuz.

javascript
const kenar = 4, alan = kenar * kenar;
console.log(alan); // 16

const kisi = { ad: "Deniz", yas: 30 };
const { ad, yas } = kisi;
console.log(ad, yas); // Deniz 30

Destructuring sözdiziminin tamamı destructuring ve spread yazısında anlatılıyor.

Hangisini ne zaman kullanmalısınız?

Hangisini ne zaman kullanmalısınız? Varsayılan olarak const, Değer değişecekse let, var kullanmayın
  • Varsayılan olarak const: MDN'in kod stili rehberi de dahil olmak üzere pek çok stil rehberi, kapsamı içinde yeniden atanmayan değişkenler için const önerir. Okuyan kişi değerin değişmeyeceğini hemen anlar.
  • Değer değişecekse let: sayaçlar, birikimli toplamlar ve koşula göre atanan değişkenler için.
  • var kullanmayın: MDN, blok kapsamına uymaması gibi şaşırtıcı davranışları nedeniyle var'ın modern kodda önerilmediğini belirtir. Eski kodu okurken bilmeniz yeterli.

Sık yapılan hatalar

  • const ile tanımlanan nesnenin değişmez olduğunu sanmak: yalnızca bağlama sabittir; içerik için Object.freeze() gerekir.
  • const değişkene sonradan değer vermeye çalışmak: ilk değer bildirimle birlikte verilmelidir.
  • switch dallarında aynı adla let yazmak: her case gövdesini { } içine alın.
  • Tek satırlık if gövdesinde bildirim yapmak: if (kosul) let a = 1; bir SyntaxError verir, çünkü let ve const deyim değil bildirim başlatır.
  • Döngüde var ile geri çağrı oluşturmak: tüm fonksiyonlar aynı değişkeni görür; let kullanın.

Sık Sorulan Sorular

const ile tanımlanan dizi neden değiştirilebiliyor?

const değişkenin başka bir değere bağlanmasını engeller, değerin kendisini dondurmaz. Diziye push() ile eleman eklemek aynı diziyi değiştirir; değişken hâlâ aynı diziyi gösterdiği için hata oluşmaz. Diziye yeni bir dizi atamaya çalışırsanız TypeError alırsınız.

let ile tanımlanan değişken hoisting'e uğrar mı?

Bu bir tanım tartışmasıdır. let değişkeni kapsamın başından itibaren vardır, ancak bildirim satırına kadar erişilemez. MDN bu yüzden let ve const bildirimlerinin genellikle hoisting'e uğramayan bildirimler olarak görüldüğünü belirtir.

İç blokta aynı adla tekrar let yazmak hata mı?

Hayır. Farklı bir blok yeni bir kapsam açtığı için içteki değişken dıştakinden bağımsızdır ve onu gölgeler. Hata yalnızca aynı kapsamda iki bildirim olduğunda ortaya çıkar.

Eski bir projede var'ları let ile değiştirmek güvenli mi?

Çoğu durumda evet, ancak kör bir bul-değiştir yapmayın. Değişken bildirildiği bloğun dışında kullanılıyorsa, bildirimden önce okunuyorsa veya aynı ad tekrar bildiriliyorsa davranış değişir ya da sözdizimi hatası oluşur. Her değişikliği kapsamına bakarak yapın.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: JavaScript öğrenme rehberi · Hoisting ve TDZ · Scope (kapsam) · Closure nedir · Strict mode