closure

Gizem Korkmaz
6 min readOct 10, 2022

--

Bu yazı, Dan Abramov’un closure adlı makalesinin bir çevirisidir.

Closurelar kafa karıştırır çünkü “görünmez” kavramlardır.

Bir objeyi, bir değişkeni ya da bir fonksiyonu kullandığınızda bunu kasıtlı olarak yaparsınız. “Burada bir değişkene ihtiyacım olacak” diye düşünür ve onu kodunuza eklersiniz.

Closure ise farklıdır. Çoğu insan closurelara ilk adımlarını attıklarında, hali hazırda onları zaten pek çok kez kullanmış olurlar. Bu durumun şu an sizin için de geçerli olma ihtimali oldukça yüksek. Bu sebeple, closureları öğrenmek yeni bir kavramı öğrenmekten ziyade, bir süredir yapmakta olduğunuz bir şeyi daha yakından tanımak gibidir.

tl;dr (kısa özet)

Bir fonksiyonun, kapsamı dışında tanımlanmış değişkenlere erişmesi durumunda bir closure oluşur.

Örneğin, şu kod parçası içerisinde bir closure barındır:

let users = ['Alice', 'Dan', 'Jessica'];let query = 'A';let user = users.filter(user => user.startsWith(query));

=> user.startsWith(query)'nin kendisinin bir fonksiyon olduğuna dikkat edin. Bu fonksiyon query değişkenini kullanıyor. Ancak query değişkeni bu fonksiyonun kapsamı dışında tanımlanmış. Bu bir closure.

İsterseniz okumayı bu noktada bırakabilirsiniz. Makalenin geri kalanı closurelara farklı bir açıdan yaklaşıyor olacak. Closure’un ne demek olduğunu açıklamaktan ziyade, closureları adım adım keşfetmenizi sağlayacak. Tıpkı 1960larda, ilk programcıların yaptıkları gibi.

1. Adım: Fonksiyonlar, Kapsamı Dışında Tanımlanmış Değişkenlere Erişebilirler

Closureları anlamak için, önce değişkenlerlere ve fonksiyonlara bir şekilde aşina olmalıyız. Aşağıdaki örnekte eat fonksiyonu içerisinde bir food değişkeni tanımladık.

function eat() {let food = 'cheese';console.log(food + ' is good');}eat(); // Konsola 'cheese is good' loglar.

Peki ya daha sonra food değişkenini, eat fonksiyonunun kapsamı dışında değiştirmek istersek ne olur? Bunu yapmak için food değişkenini fonksiyonun dışına çıkarıp en tepeye alabiliriz:

let food = 'cheese'; // Dışarı aldık.function eat() {console.log(food + ' is good');}

Bu durum ne zaman istersek food'u “dışarıdan” değiştirmemize izin verir.

eat(); // Konsola 'cheese is good' loglar.food = 'pizza';eat(); // Konsola 'pizza is good' loglar.food = 'sushi';eat(); // Konsola 'sushi is good' loglar.

Başka bir deyişle, food değişkeni artık eat fonksiyonumuzun lokalinde değildir, ancak yine de eat fonksiyonumuzun ona erişmede herhangi bir sıkıntısı yoktur. Fonksiyonlar, kapsamları dışında tanımlanmış değişkenlere erişebilirler. Bir dakikalığına durun ve bu düşünce ile herhangi bir sorununuz olmadığından emin olun. Tam anlamıyla aklınıza yattığı anda ikinci adıma geçebilirsiniz.

2. Adım: Kodu Bir Fonksiyon Çağrısı İçine Almak

Bir parça kodumuz olduğunu düşünelim:

/* Bir parça kod */

Kodun ne yaptığının hiçbir önemi yok. Ancak, diyelim ki onu iki defa çalıştırmak istiyoruz.

Bunu yapmanın ilk yolu kopyala yapıştır yapmaktır.

/* Bir parça kod */
/* Bir parça kod */

Bir başka yolu ise bir döngü kullanmaktır:

for (let i = 0; i < 2; i++) {/* Bir parça kod */}

Üçüncü yolu ise, ki bu bizim bugün özellikle ilgilendiğimiz yol, onu bir fonksiyon içerisine almaktır.

function doTheThing() {/* Bir parça kod */}doTheThing();doTheThing();

Fonksiyon kullanmak bize son derece esneklik kazandırır çünkü bu fonksiyonu istediğimiz sayıda, istediğimiz zaman ve programımızda istediğimiz yerde kullanabiliriz.

Aslına bakarsanız, eğer istersek yeni fonksiyonumuzu sadece bir kere bile çağırabiliriz:

function doTheThing() {/* Bir parça kod */}doTheThing();

Bunun yukarıdaki kod parçasına denk olduğuna dikkat edin:

/* Bir parça kod */

Başka bir deyişle; eğer bir parça kodu alır, onu bir fonksiyon ile sarıp o fonksiyonu tek bir kere çağırırsak, bu kodun yaptığı şeyi hiçbir şekilde değiştirmemiş oluruz. Bu kuralın şimdilik görmezden geleceğimiz bazı istisnaları vardır ancak genel hatları itibariyle bunun mantıklı geliyor olması gerekir. Aklınıza yatana kadar bu fikir üzerine biraz düşünebilirsiniz.

3. Adım: Closureları Keşfetmek

İki farklı fikir ile yolumuza devam ediyoruz:

  • Fonksiyonlar, kapsamları dışındaki değişkenlere erişebilirler.
  • Bir kod parçasını fonksiyon ile sarmak ve bu fonksiyonu çağırmak sonucu değiştirmeyecektir.

Şimdi bu ikisini birleştirdiğimizde ne olduğuna bir bakalım.

İlk adımdaki kod örneğimizi tekrar ele alacağız:

let food = 'cheese';function eat() {console.log(food + ' is good');}eat();

Ardından tüm bu örneği, sadece bir kere çağıracağımız bir fonksiyon içerisine alacağız:

function liveADay() {let food = 'cheese';function eat() {console.log(food + ' is good');}eat();}liveADay();

İki kod parçasını bir kere daha okuyun ve ikisinin birbirine denk olduğundan emin olun.

Bu kod çalışıyor! Ancak biraz yakından bakın. eat fonksiyonunun, liveADay fonksiyonu içerisinde olduğuna dikkat edin. Böyle bir şeyi yapabilir miyiz? Bir fonksiyonu alıp başka bir fonksiyon içerisine koyabilir miyiz?

Bu tür bir kod yapısının geçerli olmadığı diller var. Örneğin, bu kod C dilinde geçerli olamaz (ki bu dilde closure da yoktur). Bu demek oluyor ki, C dilinde, ikinci çıkarımımız doğru değil. Öylece bir parça kodu alıp bir fonksiyon içerisinde saramayız. Ancak JavaScript bu kısıtlamaya dahil değildir.

Koda iyice bir tekrar bakın ve food değişkeninin nerede tanımlandığına ve kullanıldığına bakın:

function liveADay() {let food = 'cheese'; // `food` tanımlandıfunction eat() {console.log(food + ' is good'); // `food` okundu}eat();}liveADay();

Bu kodun üzerinden adım adım beraber bir geçelim. Öncelikle en tepede liveADay fonksiyonunu tanımladık. Ardından onu hemen çağırdık. İçerisinde food lokal değişkeni var. Aynı zamanda bir eat fonksiyonu içeriyor. Ardından bu eat fonksiyonunu çağırıyor. eat, liveADay'in içerisinde olduğu için tüm değişkenlerini “görebiliyor”. Bu sebeple de food değişkenini okuyabiliyor.

Buna closure adı veriliyor.

Bir fonksiyon (eat gibi) kapsamı dışında tanımlanmış (liveADay'in içerisinde olduğu gibi) bir değişkeni (food gibi) okuyup yazabiliyorsa orada bir closure vardır.

Bunu tekrar okumak için biraz zaman ayırın ve bahsedilenleri kod içerisinde yakalayabildiğinizden emin olun.

İşte, kısa özet kısmında bahsettiğimiz örnek:

let users = ['Alice', 'Dan', 'Jessica'];let query = 'A';let user = users.filter(user => user.startsWith(query));

Belki bunu fonksiyon ifadesi (function expression) ile yazarsak anlaşılması çok daha kolay olabilir:

let users = ['Alice', 'Dan', 'Jessica'];// 1. query değişkeni dışarıda tanımlanmış let query = 'A';let user = users.filter(function(user) {// 2. İç fonksiyondayız// 3. Ve query değişkenini okuyoruz (dışarıda tanımlanmıştı!) return user.startsWith(query);});

Ne zaman bir fonksiyon, kapsamı dışında tanımlanmış bir değişkene erişmek isterse, onun bir closure olduğunu söyleyebiliriz. Bu terim biraz geniş anlamlarda kullanılıyor. Bazıları yukarıdaki durumda closure’ın iç fonksiyonun kendisi olduğunu söyleyecektir. Bazıları closure’ı, dış değişkenlere ulaşma tekniği olarak görürler. Pratikte ise bunların bir önemi yoktur.

Bir Fonksiyon Çağrısının Hayaleti

Closurelar şu an aldatıcı derecede basit görünüyor olabilirler. Bu durum, onların bazı tuzakları olmadığı anlamına gelmiyor. Eğer detaylı düşünürseniz, bir fonksiyonun kapsamı dışındaki değişkenleri okuyup yazmasının aslında oldukça derin sonuçları olabilir. Örneğin, bu değişkenlerin iç fonksiyon çağırılabildiği sürece “hayatta kalacakları” anlamına gelir:

function liveADay() {let food = 'cheese';function eat() {console.log(food + ' is good');}// Beş saniye sonra eat çağırılır setTimeout(eat, 5000);}liveADay();

Burada food, liveADay() fonksiyon çağrısının içerisinde lokal bir değişkendir. liveADay içerisinden çıktığımız anda bunun “yok olduğunu” ve bizi daha sonra rahatsız etmeyeceğini düşünmek oldukça cezbedicidir.

Ancak, liveADay içerisinde tarayıcıya beş saniye içerisinde eat fonksiyonunu çalıştırmasını söylüyoruz. Böylece eat, food değişkenini okuyor. Yani JavaScript motorunun, food değişkenini mevcut liveADay çağrısında kullanılabilir durumda tutması gerekir.

Bu açıdan, closureları eski fonksiyon çağrılarından kalma bir tür “hayaletler” ya da “anılar” gibi düşünebiliriz. Her ne kadar liveADay() fonksiyon çağrımız uzun bir süre önce bitmiş olsa da, içteki eat fonksiyonu hala çağırılabilir durumda olduğu sürece değişkenleri var olmaya devam edecektir. Neyse ki, JavaScript bunu bizim için yapıyor, bu sebeple bunun üzerine düşünmemize gerek yok.

Neden Closure?

Son olarak, neden closurelara bu isim verildiğini merak ediyor olabilirsiniz. Sebebi çoğunlukla tarihsel. Bilgisayar bilimleri jargonu ile aşina olan biri user => user.startsWith(query)'nin bir “open binding’i” olduğunu söyleyebilir. Başka bir deyişle, user'ın ne olduğu bellidir (bir parametre) ancak tek başına query'nin ne ifade ettiği belli değildir. Biz, “aslında query dışarıda tanımladığımız değişkeni ifade ediyor.” dediğimizde, mevcut “open binding’i” kapatmış (closing) oluyoruz. Bir başka deyişle, bir closure elde ediyoruz.

Tüm diller closureları kullanmazlar. Örneğin, C gibi bazı dillerde iç içe fonksiyonlara dahi müsaade yoktur. Sonuç olarak, bir fonksiyon sadece lokal değişkenlerine ya da global değişkenlere erişebilir, ancak hiçbir durumda parent fonksiyonun lokal değişkenlerine erişemez. Doğal olarak tüm bu kısıtlama oldukça can sıkıcıdır.

Aynı zamanda, Rust gibi closureları kullanan ancak closurelar ve sıradan fonksiyonlar için ayrı bir yapı kullanan diller de vardır. Böylece, eğer bir fonksiyonun dışındaki bir değişkeni okumak istiyorsanız, bunu Rust’ta dahil etmeniz gerekir. Bunun nedeni, fonksiyon çağrılarından sonra bile closureların, motorun dış değişkenlerini (buna “environment” deniliyor) yakınlarda tutmasını gerektirebilmesidir. Bu ekstra maliyet JavaScript için kabul edilebilirdir ancak low-level diller için performans kaygısı oluşturabilir.

Ve tüm bunlarla beraber, umuyorum ki closure kavramı hakkında zihninizde açıkta kalan her şey kapanmıştır (closure)!

--

--