JavaScript'te Scope, Closure ve Lexical Environment Yapısı
27 Şubat 202620 dk okuma51 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ı.
Şö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 yokfunctionshoot() {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;functionshoot() {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.
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.
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:
functionattack() {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:
functionspawnEnemy() {console.log(health); // undefined — hata yok ama değer de yokvar health = 100;console.log(health); // 100}
Motor bu kodu aslında şöyle yorumluyor:
functionspawnEnemy() {var health; // tanımlama yukarı kaldırıldı, değeri undefinedconsole.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:
functionspawnEnemy() {console.log(health); // ReferenceError ❌ — TDZlet 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.
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:
functionattack() {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.
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:
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 Scope
Dynamic Scope
Neye göre belirlenir?
Fonksiyonun yazıldığı yer
Fonksiyonun çağrıldığı yer
Ne zaman belli olur?
Kod yazım anında (parse time)
Çalışma anında (runtime)
Öngörülebilirlik
Yüksek
Düşük
Kullanan diller
JS, 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";functionstartMatch() {let mode = "deathmatch";functionspawnPlayer() {let health = 100;console.log(health); // 100 — kendi scope'unda bulduconsole.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:
Burada da showHUDhealth'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:
functioncreatePlayer(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:
İ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?
functioncreatePlayer(name) {let health = 100;let secret = "gizli-kod-42";returnfunctionhit(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:
Ö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:
functionsetupGrenade(delay) {returnfunction () {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.
Üç 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.
functioncreateEnemy(type) {let health = 100;let model = loadHeavyModel(type); // diyelim ki 50MB'lık 3D modelreturnfunctionhit(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 = [];functionspawnWave(count) {for (let i = 0; i < count; i++) {let enemyData = generateEnemy(); // her biri büyük veri enemies.push(functionattack() {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.
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.
İ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 derlenmezinthealth=100;Runnablehit= () -> { 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, sorunsuzfunctioncreatePlayer(name) {let health = 100;return {hit: (damage) => { health -= damage; },getHealth: () => health };}
// Java — aynı şeyi yapmak istiyorsan, obje üzerinden gitmeliyizint[] health = {100}; // array referansı effectively finalRunnablehit= () -> { 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ı
JavaScript
Java
Lambda/closure var mı?
Evet
Evet
Dış değişkeni yakalayabilir mi?
Evet
Evet, ama effectively final olmalı
Yakalanan değişkeni mutate edebilir mi?
Evet
Hayır (doğrudan)
Neden?
Single-threaded, güvenli
Multi-threaded, race condition riski
Tasarım felsefesi
Esneklik 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ı:
Kavram
Soru?
Cevap
Scope
Değişken nereden erişilebilir?
Tanımlandığı kod bölgesinden
Function vs Block Scope
Bu bölgenin sınırı nereden geçiyor?
var → fonksiyon, let/const → süslü parantez
Hoisting
Tanım satırından önce erişirsem ne olur?
var → undefined, let/const → ReferenceError (TDZ)
Lexical Scope
Bu 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
Closure
Dış fonksiyon bittikten sonra değişkenler nasıl yaşıyor?
İç fonksiyon lexical environment'a referans tutuyor
GC Etkileşimi
Bu 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.