Closure: Çevresini Hatırlayan Fonksiyon
JavaScript'te bir fonksiyon tanımladığınızda, o fonksiyon etrafındaki değişkenlere sessizce bir bağ kurar. Fonksiyon daha sonra — hatta bambaşka bir yerde — çalıştığında bile bu değişkenleri hâlâ görebilir. İşte closure budur.
En kısa örneğiyle:
makeGreeter çalışır, içerideki bir fonksiyonu döndürür ve biter. Normalde name adlı yerel değişkenin de onunla birlikte yok olmasını beklersin — sonuçta fonksiyon işini bitirdi. Ama içerideki fonksiyon hâlâ name'i kullandığı için JavaScript onu hayatta tutar. greetAda, "Ada"'yı hatırlar. greetBoris ise "Boris"'i. İki ayrı closure, iki ayrı hatırlanan değer.
Closure'ların Kaynağı: Lexical Scope
Closure'ların arkasındaki kurala lexical scope (sözsel kapsam) denir: bir fonksiyon, çağrıldığı yerdeki değil, yazıldığı yerdeki değişkenleri görebilir. "Lexical" kelimesi de zaten "kaynak kodda nerede durduğuna göre" anlamına geliyor.
show "Ben dışarıdayım" yazdırır, "Ben caller içindeyim" değil. Çünkü fonksiyon en dıştaki outer'ın yanında tanımlanmış, dolayısıyla gördüğü değişken odur. Kendi outer'ına sahip başka bir yerden çağrılması hiçbir şeyi değiştirmez.
Aslında closure dediğimiz şey, dış fonksiyon bittikten sonra da yaşamaya devam eden lexical scope'tan başka bir şey değil. Değişken yok olmuyor; çünkü hâlâ ona referans tutan biri var.
Her Çağrı Kendi Closure'ını Oluşturur
Dış fonksiyonu her çağırdığında yepyeni değişkenler oluşur ve o çağrıdan dönen iç fonksiyon o değişkenleri hatırlar. Yukarıdaki greetAda ile greetBoris'in birbirine karışmamasının sebebi de bu.
Bunun en klasik örneği sayaçtır:
a ve b, her biri kendi count değerini tutar. Döndürülen fonksiyonun dışındaki hiçbir şey bu değişkenlere erişemez — yani count tamamen özeldir. Bunu açıkça etkinleştirdiğimiz bir dil özelliği değil; doğrudan closure'ların çalışma şeklinden doğan bir sonuç.
Closure ile Private Değişkenler (Sınıf Kullanmadan)
Kapsanan değişkenlere yalnızca döndürülen fonksiyon üzerinden ulaşılabildiği için, closure'ları kullanarak gerçek anlamda private state'e sahip küçük nesneler oluşturabilirsiniz:
balance aslında döndürülen nesnenin bir özelliği değil — closure'ın içinde yaşıyor. Onu okumanın veya değiştirmenin tek yolu, dışarıya açtığın metotlardan geçmek. #private alanlara sahip class'lar da aynı işi görebilir ama closure yaklaşımı onlardan onlarca yıl eski ve hâlâ ekosistemin her yerinde karşına çıkıyor.
Döngülerde Closure: Klasik Tuzak
İnsanların closure konusunda en çok takıldığı yer döngüler. var ile neler olduğuna bir bak:
0, 1, 2 bekliyorsun ama 3, 3, 3 alıyorsun. Sebebi şu: var function-scoped'dur, yani tüm döngü boyunca tek bir i vardır. Üç closure da aynı değişkeni yakaladı ve fonksiyonlar çalıştığı anda döngü çoktan bitmiş, i de 3 olmuştu.
Şimdi let ile dene:
Artık sırayla 0, 1, 2 yazıyor. let block-scoped'tur — döngünün her turunda i için yepyeni bir bağlama (binding) oluşur, dolayısıyla her closure kendi değerini yakalar. Zaten var yerine let kullanmanın en büyük sebebi de tam olarak budur.
Closure değeri değil, değişkeni yakalar
İnce ama kritik bir nokta: bir closure, fonksiyon tanımlandığı andaki değerin bir anlık görüntüsünü değil, değişkenin kendisini tutar.
printMessage çalışırken message değişkenini o an okur, tanımlandığı andaki halini değil. Eğer anlık bir kopya istiyorsan, değeri önce lokal bir değişkene al; zaten let kullandığında for döngüsünün içinde olan da tam olarak budur.
Gerçek Hayattan Bir Örnek: Once
Bir fonksiyonun yalnızca tek seferliğine çalışmasını garantilemek için closure kullanan minik bir yardımcı fonksiyon:
called ve result, geri dönen fonksiyon hayatta kaldığı sürece yaşayan özel (private) durumdur. Ne global bir bayrağa ihtiyaç var ne de fazladan bir nesneye. Küçük yardımcı fonksiyon, özel durum ve closure üçlüsü — bu kalıp, JavaScript'in sunduğu en kullanışlı şeylerden biridir.
Bellek Konusuna Kısa Bir Not
Bir closure, kendisine bir referans kaldığı sürece yakaladığı değişkenleri canlı tutar. Çoğu zaman istediğimiz şey budur; ama closure'ı uzun ömürlü bir yere (örneğin bir DOM event listener'ına ya da global bir cache'e) bağlarsan ve içinde büyük bir şey yakalıyorsa, closure ortadan kalkmadan o şey de garbage collector tarafından temizlenemez.
function attach() {
const hugeData = new Array(1_000_000).fill("...");
document.addEventListener("click", () => {
console.log(hugeData.length);
});
}
Listener bağlı kaldığı sürece hugeData bellekte tutulmaya devam eder. Listener'ı kaldırırsan (ya da baştan ihtiyacın olmayan şeyi kapsama almazsan) referans temizlenir. Bunu takıntı hâline getirmene gerek yok — sadece closure ile bellek arasında bir bağ olduğunu aklında tut, yeter.
Akılda Kalması Gerekenler
- Closure, bir fonksiyon ile o fonksiyon tanımlandığı anda gördüğü değişkenlerin toplamıdır.
- Dış fonksiyon her çağrıldığında, içerideki closure'lar için yepyeni bir değişken seti oluşur.
- Closure sayesinde class'a ihtiyaç duymadan private state tutabilirsin.
- Döngü içinde
letkullan ki her iterasyon kendi binding'ine sahip olsun. - Closure, değişkenin kendisini yakalar; oluşturulduğu andaki değerini değil.
Sırada: this Anahtar Kelimesi
Closure'lar, bir fonksiyonun etrafındaki değişkenleri yönetir. Bir sonraki konu ise fonksiyonun hangi nesne üzerinden çağrıldığı — ki JavaScript'te bunu this belirler ve az önce gördüğümüz yakalanan değişkenlerden bambaşka bir şekilde davranır.
Sıkça Sorulan Sorular
JavaScript'te closure nedir?
Closure, tanımlandığı kapsamdaki (scope) değişkenleri hatırlayan bir fonksiyondur; dış fonksiyon çalışmasını bitirse bile bu değişkenlere erişmeye devam eder. Teknik olarak JavaScript'teki her fonksiyon bir closure'dır aslında — ama terim genelde bir fonksiyon return edildiğinde ya da başka bir yere geçildiğinde ve hâlâ orijinal kapsamındaki değişkenleri kullanmaya devam ettiğinde gündeme gelir.
Closure'lar neden işe yarar?
Closure'lar sayesinde bir fonksiyon, kendi özel (private) state'ini yanında taşıyabilir. Ne class yazmanız ne de global değişken kullanmanız gerekir — veriyi doğrudan fonksiyona bağlamış olursunuz. Sayaçlar, bir kez çalışan callback'ler, memoize edilmiş yardımcı fonksiyonlar ve iç detayları küçük bir API arkasında gizlemek en sık karşılaşılan kullanım alanları.
var ile kurulan döngülerde closure'lar neden garip davranır?
var ile kurulan döngülerde closure'lar neden garip davranır?var fonksiyon kapsamlıdır, yani döngünün her iterasyonu aynı değişkeni paylaşır. Döngü içinde oluşturulan tüm closure'lar o tek değişkene referans verir ve çalışma sırasında değişken artık son değerine ulaşmış olur. Çözüm basit: var yerine let kullanın. let blok kapsamlı olduğundan her iterasyon kendi bağımsız binding'ini alır.