mehmet ege aydın
  • Yazılar
  • Projeler
  • Okuduklarım
  • Atlas
  • Galeri
  • Oyunlar
  • Hakkımda
  • İletişim
© 2026 mehmet ege aydın·RSS·GitHub·LinkedIn·E-posta
← Tüm yazılar

JavaScript'te Scope, Closure ve Lexical Environment Yapısı

27 Şubat 2026 · 20 dk okuma · 79 okuma

JavascriptYazılım GeliştirmeTeknoloji


Bir değişken tanımlıyoruz, daha sonra bu değişkeni kodumuzun başka bir yerinde kullanmaya çalışıyoruz. Bazen sorunsuz erişiyoruz, bazense undefined ya da ReferenceError ile karşılaşıyoruz. Ama neden? Bunun nedeni aslında tek bir soruya dayanıyor: Motor bu değişken adını gördüğünde ona karşılık gelen değeri nerede arayacak? İşte scope tam olarak bu sorunun cevabı.

1. Değişkene nasıl ulaşırız?

Şöyle bir örnek ele alalım: Bir yazılımcı YZ destekli bir internet sitesi tasarlıyor. Kafasındaki arayüzü YZ 1.0 modeline anlatıyor ve ondan bir çıktı alıyor. Aldığı bu çıktıyı beğenmeyen yazılımcı, aynı girdiyi YZ 2.0 modeline veriyor. 2.0 modelinden de istediği verimi alamayınca aynı girdiyi YZ 3.0 modeline de veriyor ve sonunda istediği çıktıyı alıyor. Burada yazılımcının istediği sonucu "kapasitesi daha yüksek" olan bir modelde arama düşüncesi çok kritik.

Çünkü JavaScript motoru da bir değişken adıyla karşılaştığında benzer bir şey yapıyor. Önce bulunduğu en yakın kapsama (scope) bakıyor. Bulamadıysa bir üst kapsama çıkıyor. Orada da yoksa bir üstüne daha çıkıyor. Ta ki ya değişkeni bulana kadar ya da en dış kapsama (global scope) ulaşıp "bu değişken yok" deyip hata fırlatana kadar.

Bunu daha iyi anlayabilmek için şu 3 örneğe bakalım:

// 1. Değişken aynı yerde - sorun yokfunction shoot() {  let damage = 10;  console.log(damage + " hasar verildi.");}

Değişkeni tanımladığımız yer ile kullandığımız yer aynı. Bu yüzden motor damage'ı arıyor ve hemen buluyor.

// 2. Değişken dışarıda — yine ulaşabiliyoruzlet damage = 10;​function shoot() {  console.log(damage + " hasar verildi.");}

damage fonksiyonun içinde yok, ama motor pes etmiyor. Bir üst kapsama çıkıyor ve aradığını buluyor. Tıpkı yazılımcının 1.0'da bulamadığı cevabı 2.0'da bulması gibi.

// 3. Değişken içeride — dışarıdan ulaşamıyoruzfunction shoot() {  let damage = 10;}​console.log(damage + " hasar verildi."); // ReferenceError ❌

damage fonksiyonun içinde tanımlı. Dışarıdan erişmeye çalışıyoruz ama motor bulamıyor. Arama her zaman içeriden dışarıya doğru çalışır - tersi mümkün değil.

Burada, üç örnekte de aynı soru var: "Motor bu değişkeni nerede arayacak ve bulabilecek mi?"

Bu arama kurallarının tamamına scope diyoruz. Bir değişkenin hangi kod bölgesinden erişilebilir olduğunu belirleyen kurallar bütünü.

Ama bu tanım tek başına yeterli değil. Çünkü hemen ardından başka sorular geliyor:

— Bu kurallar ne zaman belirleniyor? Kodu yazarken mi, çalıştırırken mi?
— Scope'un sınırları nereden geçiyor? Fonksiyondan mı, süslü parantezlerden mi?
— Bir iç fonksiyon dış değişkene erişebiliyorsa, o değişken bellekten silindiğinde ne olacak?
— Başka diller bu sorulara aynı cevabı mı veriyor?

2. Scope'un sınırları: Function Scope vs Block Scope

Adım 1'de motorun değişkeni ararken içeriden dışarıya doğru ilerlediğini gördük. Peki bu aramanın sınırları nereden geçiyor? Yani bir scope nerede başlıyor ve nerede bitiyor?

JavaScript'te bu sorunun cevabı, değişkeni nasıl tanımladığımıza göre değişiyor. Ve burada var, let ve const arasındaki fark devreye giriyor.

var — Function Scope

var ile tanımlanan bir değişken, içinde bulunduğu fonksiyonun tamamını kendi alanı olarak görür. Süslü parantezler (if, for, while) onun için bir sınır çizmez.

function attack() {  if (true) {    var damage = 50;  }  console.log(damage); // 50}

damage bir if bloğunun içinde tamımlanmasına rağmen var bunu umursamıyor. Bu yüzden damage'a attack fonksiyonunun her yerinden erişilebiliyor.

Ama bir de şuna bakalım:

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

Buradaki olay i döngü bittiğinde ölmüyor. var ile tanımlandığı için fonksiyon boyunca yaşamaya devam ediyor. Bu bazen istediğimiz bir şey olsa da, çoğu zaman farkında olmadan bug'a davetiye çıkarıyor.

var : Hoisting

Burada değinilmesi gereken başka bir nokta ise var ile tanımlanan değişkenlerin kodda tanım satırına ulaşmadan önce bile erişilebilir olması. Motor, fonksiyonun başına çalıştırılmadan önce bir göz atar ve tüm var tanımlamalarını scope'un en tepesine "kaldırır" (hoist eder). Ama sadece tanımlamayı hoist eder, değeri değil! Bu yüzden değişken var görünür ama değeri undefined olur:

function spawnEnemy() {  console.log(health); // undefined — hata yok ama değer de yok  var health = 100;  console.log(health); // 100}

Motor bu kodu aslında şöyle yorumluyor:

function spawnEnemy() {  var health;            // tanımlama yukarı kaldırıldı, değeri undefined  console.log(health);   // undefined  health = 100;          // atama yerinde kaldı  console.log(health);   // 100}

Aslında let ve const da teknik olarak hoist ediliyor ama davranışları biraz daha farklı: Sadece tanım satırına ulaşmadan erişmeye çalışınca undefined yerine ReferenceError döndürüyor. Buna Temporal Dead Zone (TDZ) deniyor, değişken scope'ta var ama henüz "aktif" değil:

function spawnEnemy() {  console.log(health); // ReferenceError ❌ — TDZ  let health = 100;}

Özetlemek gerekirse: var değer atamasından önce undefined döner, let/const açıkça hata fırlatır.

let ve const — Block Scope

let ve const ile tanımlanan değişkenler ise en yakın süslü parantez çiftini ({}) kendi sınırları olarak kabul eder. Buna block scope denir.

function attack() {  if (true) {    let damage = 50;    const critical = true;  }  console.log(damage);    // ReferenceError ❌  console.log(critical);  // ReferenceError ❌}

Burada damage ve critical sadece if bloğunun içinde var. Blok bittiğinde erişim de bitiyor.

Döngüdeki fark daha da belirgin:

function attack() {  for (let i = 0; i < 3; i++) {    // ...  }  console.log(i); // ReferenceError ❌}

Peki neden iki farklı davranış var?

var JavaScript'in en başından beri var olan tanımlama biçimidir. O zaman block scope kavramı dilde yoktu, tek sınır fonksiyondu. Bu da döngü değişkenlerinin sızması, yanlışlıkla üzerine yazması gibi sorunlara yol açıyordu.

let ve const ES6 (2015) ile geldi ve daha dar, daha öngörülebilir bir scope sundu.

Artık scope'un sınırlarını biliyoruz -> fonksiyon ya da blok. Ama bir soru hâlâ cevapsız: bu sınırlar ne zaman belirleniyor? Kodu yazarken mi, yoksa çalıştırırken mi? İşte bu soru bizi lexical scope kavramına götürüyor.

3. Lexical Scope vs Dynamic Scope

Önceki bölümde scope'un sınırlarının nereden geçtiğini gördük. Şimdi bir adım geri çekilip daha temel bir soruya bakalım: bu sınırlar ne zaman ve neye göre belirleniyor?

İki farklı yaklaşım var. JavaScript bunlardan birini kullanıyor ama ikisini de anlamak, JavaScript'in neden böyle davrandığını kavramak için önemli.

Lexical Scope (Statik Scope)

Lexical scope'ta bir fonksiyonun hangi değişkenlere erişebileceği, o fonksiyonun kaynak kodda nerede yazıldığına göre belirlenir. Çalışma anında nereden çağrıldığının hiç bir önemi olmaz.

let weapon = "m4a4";​function showWeapon() {  console.log(weapon);}​function pickUpDrop() {  let weapon = "ak-47";  showWeapon();}​pickUpDrop(); // "m4a4"

Kodda showWeapon fonksiyonu pickUpDrop içinden çağrılıyor. pickUpDrop'un kendi weaponı var: "ak-47". Ama showWeapon bunu görmüyor çünkü showWeapon kaynak kodda global scope'ta bulunuyor. Motor da showWeaponın dış scope'unu belirlerken çağrıldığı yere değil, yazıldığı yere bakıyor. Yazıldığı yerin dış scope'u global olduğu için weapon değeri "m4a4" olarak alınıyor.

Bu karar kod çalışmadan önce, daha parse aşamasında belirleniyor. Kodu okuyarak hangi değişkenin nereden geleceği anlaşılabiliyor, bu yüzden buna "statik" scope da deniyor.

Dynamic Scope

Dynamic scope'ta ise kurallar farklı. Bir fonksiyon bir değişkene erişmeye çalıştığında, motor yazıldığı yere değil çağrıldığı yere, yani o anki call stack'e, bakar.

Mesela bash dynamic scope kullanan bir dil. Aynı senaryoyu bir de bash'le yazalım:

#!/bin/bash​weapon="m4a4"​show_weapon() {  echo "$weapon"}​pick_up_drop() {  local weapon="ak-47"  show_weapon}​pick_up_drop   # "ak-47"show_weapon    # "m4a4"

Burada show_weapon kendi içinde weapon tanımlamıyor. Ama pick_up_drop içinden çağrıldığında, Bash call stack'te yukarı bakıyor ve pick_up_drop'un weapon'ını buluyor: "ak-47". Aynı fonksiyon doğrudan çağrıldığında ise global weaponı görüyor: "m4a4".

Aynı fonksiyon, aynı kod, farklı sonuç : tek fark nereden çağrıldığı. JavaScript'te bu mümkün değil çünkü showWeapon her zaman yazıldığı yerdeki scope'a bağlı.

Burada parantez açmakta fayda var ki bu ne Bash'in bir eksikliği ne de JavaScript'in bir kısıtlaması. İkisi de bilinçli tasarım tercihleri. Bash gibi script dilleri, çalışma anında esneklik sağlamak için dynamic scope'u tercih ediyor. JavaScript ise öngörülebilirliği ve güvenli kod yazmayı ön plana koyduğu için lexical scope'la çalışıyor.

Kısa bir özet:

Lexical ScopeDynamic Scope
Neye göre belirlenir?Fonksiyonun yazıldığı yerFonksiyonun çağrıldığı yer
Ne zaman belli olur?Kod yazım anında (parse time)Çalışma anında (runtime)
ÖngörülebilirlikYüksekDüşük
Kullanan dillerJS, Python, Java, C, Rust...Bash, Emacs Lisp...

Artık iki önemli şeyi biliyoruz: Scope'un sınırları nereden geçiyor (function vs block) ve bu sınırlar ne zaman belirleniyor (lexical vs dynamic). Ama bir fonksiyon iç içe tanımlandığında ve dış scope'taki bir değişkene erişmeye çalıştığında, motor bu zincirlemeyi nasıl yönetiyor?

4. Scope Chain

Girişte motorun bir değişkeni ararken içeriden dışarıya doğru ilerlediğinden bahsetmiştik. Scope chain tam olarak bu aramanın resmi adı: her scope, bir üst scope'a referans tutar ve motor bu zinciri adım adım takip ederek değişkeni arar.

Bunu bir örnekle ele alalım:

let server = "EU-West";​function startMatch() {  let mode = "deathmatch";​  function spawnPlayer() {    let health = 100;​    console.log(health);  // 100 — kendi scope'unda buldu    console.log(mode);    // "deathmatch" — bir üst scope'a çıktı    console.log(server);  // "EU-West" — iki üst scope'a çıktı  }​  spawnPlayer();}​startMatch();

Motor health'i arıyor ve kendi scope'unda hemen buluyor. mode'u arıyor ama bu sefer kendi scope'unda bulamıyor, bir üste çıkıyor ve startMatch'te buluyor. server'ı arıyor: Onu da kendi scope'unda bulamıyor, bir üstte de bulamıyor; iki üste çıkıyor ve en son global'de buluyor.

Bunun yanında dikkat etmemiz gereken başka bir konu da:

function startMatch() {  function spawnPlayer() {    let health = 100;  }​  function showHUD() {    console.log(health); // ReferenceError ❌  }​  spawnPlayer();  showHUD();}

Burada da showHUD health'e erişemiyor çünkü health aynı scope'ta olan başka bir fonksiyonun (spawnPlayer) scope'unda. Zincir sadece kendi üst çizgisinde yukarı doğru gider. Yan dallara bakmaz.

Zincir ne zaman oluşuyor?

Lexical scope bölümünden hatırlayalım: scope, fonksiyonun yazıldığı yere göre belirleniyor. Scope chain de aynı şekilde fonksiyon tanımlandığı anda, o anki lexical environment'a bir referans tutuyor. Çağrıldığında değil, yazıldığında bağlanıyor.

Bu detay şu an küçük görünebilir ama bir sonraki bölümde bu referansın ne anlama geldiğini ve neden bellekten silinmediğini göreceğiz. Scope chain'i anlamak, closure'u anlamak için gerekli olan son parçaydı.

5. Closure

Scope chain bölümünde bir fonksiyonun tanımlandığı anda dış scope'a referans tuttuğunu söyledik. Closure, işte bu referansın yarattığı bir sonuç: Bir iç fonksiyon return edildiğinde veya başka bir yere aktarıldığında, tanımlandığı scope'taki değişkenlere hâlâ erişebilir! Dış fonksiyon çoktan bitmiş olsa bile...

Bu ilk bakışta mantıklı gelmiyor. Bir fonksiyon bittiyse, onun değişkenleri de ölmüş olmalı değil mi? Bi' bakalım:

function createPlayer(name) {  let health = 100;​  return {    hit: function (damage) {      health -= damage;      console.log(name + ": " + health + " HP");    },    heal: function (amount) {      health += amount;      console.log(name + ": " + health + " HP");    }  };}​let player1 = createPlayer("Kenji");let player2 = createPlayer("Raven");​player1.hit(30);   // "Kenji: 70 HP"player1.hit(20);   // "Kenji: 50 HP"player2.hit(10);   // "Raven: 90 HP"player1.heal(15);  // "Kenji: 65 HP"

Önce createPlayer çağrıldı, health ve name değişkenlerini tanımladı, bir obje döndürdü ve bitti. Normalde bu değişkenlerin bellekten silinmesini bekleriz. Ama hit ve heal fonksiyonları hâlâ health'e erişiyor ve onu değiştirebiliyor. Üstelik player1 ve player2 birbirinden bağımsız: Her biri kendi healthini taşıyor.

Peki nasıl oluyor bu?

Closure'un mekaniği

createPlayer her çağrıldığında yeni bir lexical environment oluşuyor. Bu environment, o çağrıya ait değişkenleri (name, health) tutan bir obje. Döndürülen hit ve heal fonksiyonları da bu environment'a bir referans tutuyor.

Heap Memory:

┌─────────────────────────┐
│  Lexical Environment    │
│  { name: "Kenji",       │
│    health: 70 }         │  ← bu obje yaşamaya devam ediyor
└───────────┬─────────────┘
            │ referenced by
            ▼
   player1.hit, player1.heal

createPlayer fonksiyonu bitmiş olsa bile, döndürdüğü fonksiyonlar bu environment'a referans tuttuğu sürece garbage collector objeyi temizlemiyor. Böylece environment hayatta kalıyor çünkü ona bağlı hâlâ bir fonksiyon var.

player2 için de durum aynı ama ayrı bir environment:

Heap Memory:
                                                      
┌─────────────────────────┐                        ┌─────────────────────────┐
│  Lexical Environment    │                        │  Lexical Environment    │
│  { name: "Kenji",       │                        │  { name: "Raven",       │
│    health: 70 }         │                        │    health: 90 }         │
│                         │                        │                         │
└────────┬────────────────┘                        └────────┬────────────────┘
         │ referenced by                                    │ referenced by
         ▼                                                  ▼
  player1.hit, player1.heal                          player2.hit, player2.heal

İki oyuncunun health değerleri karışmıyor çünkü her createPlayer çağrısı kendi environment'ını yaratıyor.

Peki environment'taki her değişken mi hayatta kalıyor?

Şimdiye kadar closure'ın dış scope'taki değişkenlere erişim sağladığını gördük. Ama ya dış scope'ta birden fazla değişken varsa ve biz sadece birini kullanıyorsak?

function createPlayer(name) {  let health = 100;  let secret = "gizli-kod-42";​  return function hit(damage) {    health -= damage;    console.log(name + ": " + health + " HP");  };}​let player = createPlayer("Kenji");player(30); // "Kenji: 70 HP"

Mesela burada hit fonksiyonu health ve namei kullanıyor ama secreta hiç dokunmuyor. Bu durumda secret bellekte yaşamaya devam ediyor mu?

Teorik olarak: Closure, lexical environment'ın tamamına referans tutar. Yani secret da o environment'ın parçası. Bu yüzden teknik olarak hayatta.

Pratikte: Modern JavaScript motorları (V8, SpiderMonkey) bu konuda akıllı davranıyor. Motor, hit fonksiyonunun hangi değişkenleri gerçekten kullandığını analiz ediyor. Ve eğer secret'a hiçbir referans yoksa, motor onu environment'tan çıkarabiliyor. Buna genellikle "dead variable elimination" deniyor.

Ama buna güvenerek kod yazmamalıyız. Optimizasyon motorun iç kararı, yani bir garanti değil. Eğer büyük bir veri yapısını closure'un erişebildiği bir scope'ta bırakırsak ve ona ihtiyacımız yoksa, işimiz bittiğinde null'a atamak iyi bir pratik olur:

function loadMap(mapId) {  let mapData = fetchHugeMapData(mapId); // büyük veri  let spawnPoint = mapData.spawn;​  mapData = null; // artık ihtiyacımız yok, referansı kaldırıyoruz​  return function getSpawn() {    return spawnPoint;  };}

Özetle: closure environment'a referans tutar, motor kullanılmayan değişkenleri optimize edebilir ama bu garanti edilmez. Büyük verilerle çalışırken bilinçli olmakta fayda var.

Closure nedir, ne değildir?

Closure özel bir syntax ya da anahtar kelime değil. Onu "yaratmak" için özel bir şey yapmıyoruz. Closure, lexical scope ve scope chain'in doğal bir sonucu: Bir fonksiyon tanımlandığı yerdeki environment'a referans tutar, bu referans da environment'ı canlı tutar. Voilà!

Daha kısa söylersek: closure, bir fonksiyonun tanımlandığı lexical environment'a olan referansını korumasıdır.

Daha basit bir örnek

Closure'u görmek için illa karmaşık bir yapı gerekmez:

function setupGrenade(delay) {  return function () {    console.log(delay + "ms sonra patlıyor!");  };}​let frag = setupGrenade(3000);let flash = setupGrenade(1500);​frag();   // "3000ms sonra patlıyor!"flash();  // "1500ms sonra patlıyor!"

setupGrenade çoktan bitmiş. Ama döndürülen fonksiyon delaye hâlâ erişiyor. Her çağrı kendi closure'ını taşıyor — frag 3000'i, flash 1500'ü hatırlıyor.

6. Klasik tuzak: Döngüde var vs let

Closure'u anladıktan sonra JavaScript'in en bilinen tuzaklarından birine bakalım.

Diyelim ki oyunda bir geri sayım yapıyoruz. Üç saniyelik bir bomba, her saniye ekrana kalan süreyi yazdıracak:

for (var i = 1; i <= 3; i++) {  setTimeout(function () {    console.log(i + ". saniye");  }, i * 1000);}​// Beklenen Çıktı:  "1. saniye", "2. saniye", "3. saniye"// Gerçek Çıktı:    "4. saniye", "4. saniye", "4. saniye" 

Neden 4? Çünkü var block scope oluşturmuyor. var i döngü bloğuna değil, dış scope'a ait. Yani üç setTimeout callback'i de aynı i değişkenine referans tutuyor. Callback'ler çalıştığında döngü çoktan bitmiş ve i artık 4 olmuş oluyor.

Yani üç closure var ama hepsi aynı lexical environment'ı paylaşıyor. O environment'ta tek bir i var ve değeri 4.

Tek bir Lexical Environment:
┌──────────────┐
│  { i: 4 }    │  ← üç callback de buraya bakıyor
└──────┬───────┘
       │ referenced by
       ▼
  callback 1, callback 2, callback 3

let ile çözüm

for (let i = 1; i <= 3; i++) {  setTimeout(function () {    console.log(i + ". saniye");  }, i * 1000);}​// "1. saniye", "2. saniye", "3. saniye" ✅

Tek bir kelime değişti: var → let. Ama davranış tamamen farklı. Neden?

let block scope oluşturduğu için döngünün her iterasyonunda yeni bir lexical environment yaratılıyor. Her callback kendi environment'ına referans tutuyor ve her environment'ta inin o anki değeri sabit kalıyor.

Iterasyon 1:                Iterasyon 2:                Iterasyon 3:
┌──────────────┐            ┌──────────────┐            ┌──────────────┐
│  { i: 1 }    │            │  { i: 2 }    │            │  { i: 3 }    │
└──────┬───────┘            └──────┬───────┘            └──────┬───────┘
       │                           │                           │
       ▼                           ▼                           ▼
  callback 1                  callback 2                  callback 3

Üç ayrı environment, üç ayrı i, üç ayrı closure. Her callback kendi değerini hatırlıyor.

Bu bir "tuzak" mı, yoksa tutarlı bir davranış mı?

Aslında bu bir bug ya da garip bir davranış değil. Önceki bölümlerde öğrendiğimiz her şey bu sonucu zaten öngörüyor:

— var function scope oluşturur, block scope oluşturmaz (Bölüm 2).

— Scope sınırları yazım anında belirlenir (Bölüm 3).

— İç fonksiyonlar dış environment'a referans tutar (Bölüm 4).

— Bu referans environment'ı canlı tutar ve değişkenin o anki değerine bakar, kopyasına değil (Bölüm 5).

Hepsini bir araya koyduğumuzda var ile döngüde 4, 4, 4 görmek kaçınılmaz.

7. Closure ve Garbage Collection

JavaScript'te bellek yönetimi otomatiktir. Garbage collector (GC), hiçbir yerden erişilemeyen objeleri tespit edip bellekten temizler. Bir değişkene hiçbir referans kalmadıysa, o değişkenin tuttuğu veri doğrudan GC'nin hedefi olur.

Ama daha önce de bahsettiğimiz gibi closure bu denklemi değiştiriyor. Bir fonksiyon dış scope'taki environment'a referans tuttuğu sürece, o environment GC tarafından temizlenemiyor ve fonksiyon yaşadıkça environment yaşıyor.

function createEnemy(type) {  let health = 100;  let model = loadHeavyModel(type); // diyelim ki 50MB'lık 3D model​  return function hit(damage) {    health -= damage;    console.log(type + ": " + health + " HP");  };}​let enemy = createEnemy("tank");enemy(25); // "tank: 75 HP"

Bu örnekte de yine hit fonksiyonu health ve typea createEnemy'den sonra erişmeye devam ediyor. Bu yüzden lexical environment hayatta kalıyor.

Ama dikkat: hit fonksiyonu model'e hiç dokunmuyor. E o zaman 50MB'lık bir veri bellekte gereksiz yere mi duruyor?

Lexical Environment:
┌─────────────────────────────┐
│  type: "tank"               │  ← hit kullanıyor  ✅
│  health: 75                 │  ← hit kullanıyor  ✅
│  model: [50MB 3D data]      │  ← hit kullanMIYOR ❓
└──────────────┬──────────────┘
               │ referenced by
               ▼
           hit fonksiyonu

Aslında Bölüm 5'te bu soruya değinmiştik: modern motorlar kullanılmayan değişkenleri optimize edebilir ama bu garanti edilmez. GC bağlamında bu optimizasyon kritik hale geliyor çünkü bahsettiğimiz artık birkaç byte'lık string değil, megabyte'larca veri.

Peki bellek sızıntısı nasıl oluşur?

Closure tek başına bellek sızıntısı yaratmaz! Closure'a olan referansın gereğinden uzun süre tutulması yaratır. Tipik bir senaryoyu ele alalım:

let enemies = [];​function spawnWave(count) {  for (let i = 0; i < count; i++) {    let enemyData = generateEnemy(); // her biri büyük veri​    enemies.push(function attack() {      console.log(enemyData.name + " saldırıyor!");    });  }}​spawnWave(100);// 100 closure, her biri kendi enemyData'sına referans tutuyor// Düşman öldüğünde enemies array'inden çıkarmazsak// 100 environment bellekte kalmaya devam eder

100 düşman spawn oldu, her birinin attack fonksiyonu kendi enemyData'sına closure tutuyor. Düşman oyundan çıktığında enemies array'inden kaldırmazsak, GC bu environment'ları temizleyemez. Array referansı duruyor → fonksiyon yaşıyor → environment yaşıyor → veri bellekte kalıyor.

Ama çözüm basit: İşi biten referansı kaldır.

function killEnemy(index) {  enemies[index] = null; // referansı kaldır, GC temizleyebilir}

Özet: GC ve closure ilişkisi

Closure environment'ı canlı tutar, environment da içindeki verileri canlı tutar. GC ancak closure'a olan son referans koptuğunda devreye girebilir. Bu bir sorun değil, tamamen closure'ın doğası. Sorun, ihtiyacımız kalmayan closure referanslarını gereksiz yere tutmaya devam etmemiz.

Önceki bölümlerdeki bilgiyle bağlarsak: scope sınırları neyin nerede yaşadığını belirler (Bölüm 2), lexical scope bu sınırları yazım anında sabitler (Bölüm 3), scope chain referansları oluşturur (Bölüm 4), closure bu referansları canlı tutar (Bölüm 5) — ve GC, ancak bu referans zinciri koptuğunda işini yapabilir.

8. Java Lambdaları: Onlar da birer Closure...mı?

Yaygın bir yanılgı var: "Java'da closure yoktur." Bu yanlış. Java lambdaları da aslında closure'dır, çünkü dış scope'taki değişkenleri yakalar (capture) ve isterlerse kullanabilirler. Ama JavaScript'ten farklı olarak Java bu yakalanan değişkenlere bir kısıtlama getiriyor: Yakalanan değişkenler effectively final olmalılar.

Effectively final ne demek?

Java'da bir değişken tanımlandıktan sonra bir daha değeri değiştirilmiyorsa effectively final sayılır. final anahtar kelimesiyle işaretlenmesine gerek yoktur çünkü derleyici bunu zaten kendisi analiz eder.

// Çalışır — weapon effectively finalString weapon = "tüfek";Runnable announce = () -> System.out.println(weapon);​// ❌ Derleme hatası — weapon artık effectively final değilString weapon = "tüfek";weapon = "pompalı";Runnable announce = () -> System.out.println(weapon);

İkinci örnekte weapon'a yeniden atama yaptık. Bu onu effectively final olmaktan çıkarıyor ve derleyici lambda'nın bu değişkeni yakalamasına izin vermiyor.

JavaScript'te ise böyle bir kısıtlama yok. Bölüm 5'teki createPlayer örneğini hatırlayalım — hit fonksiyonu healthi hem okuyor hem değiştiriyordu. Java'da bu mümkün değil:

// ❌ Java'da bu derlenmezint health = 100;Runnable hit = () -> {    health -= 30;  // derleme hatası — captured variable mutate edilemez    System.out.println(health);};

Neden böyle bir kısıtlama var?

Öncelikle bu bir teknik yetersizlik değil, Bilinçli bir tasarım kararı. Sebebi de Java'nın çok iş parçacıklı (multi-threaded) doğasında yatıyor.

Java'da thread'ler arasında paylaşılan mutable state, race condition'lara yol açabilir. İki thread aynı anda aynı değişkeni değiştirmeye çalışırsa sonuç öngörülemez hale gelir. Java bu riski lambda seviyesinde kesiyor

Giren değişkeni değiştiremezsen, thread'ler arası paylaşımda sorun çıkmaz.

JavaScript ise single-threaded çalışır. Event loop modeli sayesinde aynı anda iki kod parçası aynı değişkene erişemez. Bu yüzden mutable captured state JavaScript'te güvenli ve dil buna izin veriyor.

// JavaScript — closure ile mutation, sorunsuzfunction createPlayer(name) {  let health = 100;  return {    hit: (damage) => { health -= damage; },    getHealth: () => health  };}
// Java — aynı şeyi yapmak istiyorsan, obje üzerinden gitmeliyizint[] health = {100};  // array referansı effectively finalRunnable hit = () -> {    health[0] -= 30;   // array'in İÇERİĞİNİ değiştiriyoruz, referansını değil    System.out.println(health[0]);};

Elbette Java'da bu kısıtlamayı aşmanın bir yolu var: Referans tipi bir değişken (array, obje) kullanmak. Böylece referansın kendisi değişmiyor (effectively final) ama içeriği değişebiliyor.

İki yaklaşımın karşılaştırması

JavaScriptJava
Lambda/closure var mı?EvetEvet
Dış değişkeni yakalayabilir mi?EvetEvet, ama effectively final olmalı
Yakalanan değişkeni mutate edebilir mi?EvetHayır (doğrudan)
Neden?Single-threaded, güvenliMulti-threaded, race condition riski
Tasarım felsefesiEsneklik ve ifade gücüGüvenlik ve öngörülebilirlik

9. Özet

Bu yazı boyunca aslında tek bir soruyu farklı açılardan cevapladık: "Motor bu değişkeni nerede arayacak ve bulabilecek mi?"

Her bölüm bu sorunun bir katmanını açtı ve bir sonrakine zemin hazırladı:

KavramSoru?Cevap
ScopeDeğişken nereden erişilebilir?Tanımlandığı kod bölgesinden
Function vs Block ScopeBu bölgenin sınırı nereden geçiyor?var → fonksiyon, let/const → süslü parantez
HoistingTanım satırından önce erişirsem ne olur?var → undefined, let/const → ReferenceError (TDZ)
Lexical ScopeBu sınırlar ne zaman belirleniyor?Yazım anında, kaynak koddaki konuma göre
Scope Chainİç scope bulamazsa ne yapıyor?Dış scope'a çıkıyor, zincir boyunca arıyor
ClosureDış fonksiyon bittikten sonra değişkenler nasıl yaşıyor?İç fonksiyon lexical environment'a referans tutuyor
GC EtkileşimiBu referans bellek üzerinde ne anlama geliyor?Closure yaşadıkça environment yaşıyor
Java KarşılaştırmasıBaşka diller aynı şeyi nasıl yapıyor?Closure var ama mutable capture yasak — bilinçli bir trade-off

Bu kavramlar ayrı ayrı ezberlenmesi gereken maddeler değil. Her biri bir öncekinin doğal bir sonucu ve hepsi birlikte, JavaScript'in değişkenleri nasıl yönettiğinin tam resmini oluşturuyorlar.


← Tüm yazılar