28 Ekim 2022

Closure

JavaScript fonksiyon yönelimli bir dildir. Çok bağımsızlık verir. Fonksiyon bir yerde yaratılıp sonra başka bir değişkene atanarak diğer bir fonksiyona argüman olarak gönderilebilir ve sonra tamamen farklı bir yerden çağrılabilir.

Bildiğiniz gibi fonksiyon kendi dışında olan değişkenlere ulaşabilir ve bu özelliklik oldukça fazla kullanılır.

Peki ya dışarıdaki değişken değişirse? Fonksiyon en son değerini mi alacak yoksa yaratıldığında var olan değeri mi?

Ayrıca diyelim ki fonksiyon başka bir yere gönderildi ve oradan çağrıldığında ne olur, yeni yerinden dışarıda bulunan değişkenlere erişebilir mi?

Bu sorulara farklı diller farklı cevaplar vermektedir, bu bölümde JavaScriptin bu sorulara cevabını öğreneceksiniz.

Birkaç soru

Örnek olması amacıyla iki soru formülize edilecek olursa, sonrasında içsel mekanizması parça parça incelenecektir, ileride daha karmaşık sorulara cevap verebilirsiniz.

  1. selamVer fonksiyonu dışarıda bulunan isim değişkenini kullanmaktadır. Fonksiyon çalıştığında, hangi isim değişkeni kullanılacaktır?

    let isim = "Ahmet";
    
    function selamVer() {
      alert("Merhaba, " + isim);
    }
    
    isim = "Mehmet";
    
    selamVer(); // "Ahmet" mi yoksa "Mehmet" mi gösterilecek?

    Böyle durumlara tarayıcı ve sunucu tabanlı geliştirmelerde oldukça sık karşılaşılır. Bir fonksiyon yaratıldığı anda değil de daha sonra çalışmak üzere programlanabilir. Örneğin bir kullanıcı aksiyonu veya ağ üzerinden istekler bu gruba girer.

    Öyleyse soru: son değişiklikleri alır mı?

  2. calisanYarat diğer bir fonksiyon yaratır ve bunu döner. Bu yeni fonksiyon herhangi bir yerden çağrılabilir. Peki yaratıldığı yerin dışındaki değişkenlere veya çağrılan yerin dışındaki değişkenlere veya ikisine birden erişebilecek mi?

    function calisanYarat() {
      let isim = "Mehmet";
    
      return function() {
        alert(isim);
      };
    }
    
    let isim = "Zafer";
    
    // fonksiyon yarat
    let is = calisanYarat();
    
    // çağır
    is(); // burada "Mehmet" mi yoksa "Zafer" mi gösterilecek ?

Sözcüksel ortam ( Lexical Environment )

Ne olduğunu anlamak için önce “değişken”'in tekniksel anlamı üzerinde tartışmak lazım

JavaScript’te çalışan her fonksiyon, kod bloğu bir bütün olarak “Sözcüksel Ortam” adında bir objeye sahiptir.

Bu “Sözcüksel Ortam” iki bölümden oluşur:

  1. Ortam Kaydı – tüm yerel değişkenleri ve özelliklerini ( ve ek özellikleri this gibi ) tutan objedir.
  2. Dış Sözcüksel Ortam’a referans genelde süslü parantezin dışındaki kod ile ilintilidir.

Öyleyse “değişken” içsel objedeki bir özelliktir, çevresel kayıtlar. “değişkeni almak veya değiştirmek” demek “o objenin özelliğini almak veya değiştirmek” demektir.

Örneğin, aşağıdaki kodda sadece bir tane Sözcüksel Ortam bulunmaktadır:

Buna evrensel sözcük ortamı denilmektedir, kodun tamamıyla alakalıdır. Tüm tarayıcılarda <script> etiketleri aynı evrensel ortamı paylaşır.

Yukarıdaki görselde, dikdörtgen ile gösterilen Çevresel Kayıt ( değişken kaynağı ) anlamına gelir ve ok işareti dışsal referanstır. Evrensel Sözcük Ortamından daha dış ortam bulunmamaktadır. Yani null dur.

Aşağıda let değişkenlerinin nasıl çalıştığı görsel ile açıklanmıştır:

Sağ tarafta bulunan dikdörtgenler evrensel Sözcük Ortamının çalışırkenki değişikliklerini gösterir.

  1. Kod çalışmaya başladığında, Sözcüksel Ortam boştur.
  2. let ifade tanımlaması görünür. İlk başta bir değeri bulunmamaktadır bundan undefined olarak saklanır.
  3. ifade'ye değer atanır.
  4. ifade yeni bir değere referans olur.

Her şey çok basit görünüyor değil mi?

Özetlemek gerekirse:

  • Değişken özel bir iç objenin özelliğidir. Bu obje o anda çalışan kod, fonksiyon ile bağlantılıdır.
  • Değişkenlerle çalışmak aslında o objenin özellikleri ile çalışmak demektir.

Fonksiyon tanımı

Fonksiyon tanımları özeldir. let değişkenlerine nazaran çalıştırıldıklarında değil de Sözcüksel Ortam yaratıldığında işlenirler, bu da kodun başladığı zamandır.

… Ve bundan dolayı bir fonksiyon tanımından önce çağırılabilir.

Aşağıdaki kodda Sözcüksel Ortam başlangıçta boş değildir. say'e sahiptir çünkü bu bir fonksiyon tanımıdır. Sonrasında ifade alır ve bunu let ile tanımlar:

İç ve dış Sözcüksel Ortamlar

say() fonksiyonu çağrısı sırasında dış değişkenler çağrılır, bu olaya daha detaylı bakacak olursak.

Fonksiyon ilk çalıştığında yeni bir Sözcüksel Ortam otomatik olarak yaratılır. Bu tüm fonksiyonlar için genel bir kuraldır. Bu Sözcüksel Ortam yerel değişkenlerin tutulması ve çağrının tüm parametrelerini tutar.

say("Ahmet") fonksiyonu çalıştığı sırada Sözcüksel Ortam aşağıdaki gibi olur:

Fonksiyon çağrıldığında ise iki tane sözcüksel ortam bulunmaktadır: içte olan(fonksiyon çağrısı için) ve dışta olan(evrensel):

  • İçte olan sözcüksel ortam say fonksiyonunun o anki durumuna bakar, o anda tek adi degiskeni bulunmaktadır. say("Ahmet") çağrıldığından dolayı idi değişkeninin değeri "Ahmet" olur.
  • Dış Sözcük Ortamı ise bu durumda Evrensel Sözcük Ortamıdır.

İç Sözcük ortamı outer ile Dış Sözcük Ortamına referans olur.

Kod değişkene ulaşmak istediğinde – önce İç Sözcük ortamında arar, daha sonra dış sözcüm ortamına bakar ve daha sonra daha dıştakine bakar bu şekilde zincirin en sonuna kadar devam eder

Eğer değişken hiçbir yerde bulunamazsa, sıkı modda hata verir. use strict kullanılmazsa tanımsız değişken yeni bir global değişken yaratır.

Arama olayı bizim yazdığımız kodlarda nasıl işliyor buna bakalım:

  • say içindeki alert adi değişkenine erişmek istediğinde, anında Sözcük Ortamında bulabilir.
  • ifade'ye erişmek istediğinde önce fonksiyonun içine bakar fakat orada da bulamayacağından outer referansı takip ederek evrensel sözcük ortamından bu değişkene erişebilir.

Şimdi bölümün ilk başında sorulan sorulara cevap bulunabilir.

Bir fonksiyon dışta bulunan değişkenin en son değerini alır.

Bunun nedeni tanımlanan mekanizmadan dolayıdır. Eski değişkenler bir yere kaydedilmezler. Fonksiyon bunları istediğinde iç sözcük ortamından veya dış sözcük ortamından o anki değeri alır.

Bundan dolayı ilk sorunun cevabı Mehmet olacaktır:

let adi = "Ahmet";

function selamVer() {
  alert("Merhaba, " + adi);
}

adi = "Mehmet"; // (*)

selamVer(); // Mehmet

Çalışma akışı şu şekildedir:

  1. Evrensel Sözcük ortamında adi:"Ahmet" bulunmaktadır.
  2. (*) satırında evrensel değişken değişir, şimdi adi:"Mehmet" bulunmaktadır.
  3. selamVer() fonksiyonu çalıştığında adi değişkenini dışarıdan alır. Bu dış sözcüksel ortamda değişkenin değeri "Mehmet"tir.
Bir Çağrı – Bir Sözcüksel Ortam

Fonksiyon Sözcük Ortamı her fonksiyon çağrıldığında yeniden yaratılır.

Eğer fonksiyon bir kaç defa çağırılırsa her çağrıldığında kendine ait ayrı bir Sözcüksel Ortamı olur, tabi bu ortam o anki çağırılmaya ait yerel değişkenleri ve parametreleri tutar.

Sözcüksel Ortam Şartname Objesidir

“Sözcüksel Ortam” bir şartname objesidir. Bu objeyi alıp düzenleyemezsiniz veya doğrudan kullanamazsınız. JavaScript motoru yapabildiğince bu değişkenleri optimize etmeye çalışır, kullanılmayan değişkenleri saf dışı bırakabilir fakat görülen davranışları yukarıda anlatıldığı gibi olmalıdır.

İç içe fonksiyonlar

Bir fonksiyon diğer bir fonksiyon içerisinde yaratılırsa buna iç içe fonksiyon denir.

Teknik olarak bu mümkündür.

Kodu organize etmek için şu şekilde kullanabilirsiniz:

function selamYolcu(adi, soyadi) {

  // yardımcı iç içe fonksiyon.
  function tamIsim() {
    return adi + " " + soyadi;
  }

  alert( "Merhaba, " + tamIsim() );
  alert( "Güle Güle, " + tamIsim() );

}

iç içe fonksiyon tamIsim() kullanım kolaylığı sağlaması amacıyla yapılmıştır. Dışta bulunan değişkenlere erişebilir ve tam ismi döndürebilir.

Daha ilginci, iç içe bir fonksiyon geri döndürülebilir: Bu yeni objenin bir özelliği olarak veya sonucun kendisi dönebilir. Sonra başka yerde kullanılabilir. Nerede olduğu önemli olmaksızın, hala aynı dış değişkene erişebilir.

Bunun örneği yapıcı ( constructor ) fonksiyondur ( Yapıcı, "new" operatörü bölümünden inceleyebilirsiniz. )

// yapıcı fonksiyon yeni bir obje dönderir.
function Kullanici(isim) {

  // obje metodu iç içe fonksiyon olarak yaratıldı.
  this.Kullanici = function() {
    alert(isim);
  };
}

let kullanici = new Kullanici("Ahmet");
kullanici.selamYolcu(); // metod dışarıda bulunan "isim" değişkenine erişebilir.

Fonksiyonun döndürülmesi örneği:

function sayacUret() {
  let sayac = 0;

  return function() {
    return sayac++; // dışarıda bulunan sayac değişkenine erişimi bulunmaktadır.
  };
}

let sayac = sayacUret();

alert( sayac() ); // 0
alert( sayac() ); // 1
alert( sayac() ); // 2

sayacUret örneğine bakılacak olursa. “sayac” fonksiyonunu bir sonraki sayı ile döndürür. Basit olmasının yanında biraz modifiye edilmiş hali pratikte kullanılmaktadır pseudorandom number generator. Yani çok suni bir örnek değildir.

Peki sayaç içeride nasıl çalışmakta?

İçteki fonksiyon çalıştığında sayac++ içeriden dışarıya kadar sayac değişkenini arar. Yukarıdaki örneğe bakılacak olursa, sıralama şu şekilde olacaktır:

  1. İçte bulunan fonksiyonun yerel değişkenleri.
  2. Dışta bulunan fonksiyonların değişkenleri.
  3. …Bu evrensel değişkenlere kadar gider.

sayac örneğinde 2. adımda bulundu. Dıştaki değişken değiştirildiğinde, bulunduğu yerde değişiklik olur. Bundan dolayı sayac++ dıştaki değşikeni bulur ve dıştaki değişkenin Sözcüksel Ortamında bu değişkenin değerini değiştirir. Sanki let sayac = 1 yapıyormuş gibi.

Size iki tane sorum var:

  1. sayacUret'e ait olmayan bir koddan sayac değişkeni sıfırlanabilir mi? Mesela yukarıdaki örnekte alert sonrasında.
  2. Eğer sayacUret()'i bir kaç defa çağırırsanız – birçok sayac fonksiyonu döndürür. Bunlar birbirinden bağımsız mıdır yoksa aynı sayac'ı mı kullanılar?

Okumaya devam etmeden yukarıdaki sorulara cevap vermeye çalışın.

…Bitti mi?

Peki o zaman, şimdi cevaplar.

  1. Hayır sıfırlayamaz. sayac yerel bir değişkendir ve dışarıdan erişilemez.
  2. Her sayacUret çağrısı o fonksiyona ait Sözcüksel Ortam üretir, bunun da kendine ait sayac değişkeni bulunmaktadır. Öyleyse sayac değişkenleri her fonksiyon için bağımsızdır denebilir.

Örneğin:

function sayacUret() {
  let sayac = 0;

  return function() {
    return sayac++; // dışarıda bulunan sayac değişkenine erişimi bulunmaktadır.
  };
}

let sayac1 = sayacUret();
let sayac2 = sayacUret();

alert( sayac1() ); // 0
alert( sayac1() ); // 1

alert( sayac2() ); // 0 (independant)

Muhetemelen, aklınızda dış değişkenlerin nasıl çalıştığı açıklığa kavuştu. Fakat daha karmaşık olaylar için daha derine inmeye gerek var.

Detaylı şekilde ortamların incelenmesi.

Şu anda clouse’ların genel olarak nasıl çalıştığını biliyorsunuz, artık daha derinine inme vakti geldi.

Aşağıda sayacUret fonksiyonunun adımları gösterilmektedir, her şeyi anladığınıza emin olun. Basamaklarda göreceğiniz [[Environment]] henüz işlenmedi.

  1. Kod çalışmaya başkadığında sadece bir tane Sözcüksel Ortam bulunmaktadır:

    Başlangıçta sadece sayacUret fonksiyonu bulunmaktadır, çünkü bu fonksiyon tanımıdır. Henüz çalışmadı.

    Tüm fonksiyonlar başlangıçta gizli bir [[Environment]] değişkeni alırlar, bu yaratılmaya dair üretilecek Sözcüksel Ortama referans olur. Bunun hakkında henüz bilgi verilmedi, fakat teknik olarak bunu fonksiyonun nerede yaratıldığını bilmesi olarak anlayabilirsiniz.

    Burada sayacUret Evrensel Sözcüksel Ortamda yaratıldı. Bundan dolayı [[Environment]] bu ortamın referansıdır.

    Diğer bir değişle fonksiyon üretildiğinde Sözcüksel Ortama ait bir “baskı” ile üretilir. Bu [[Environment]] gizli bir özellik olarak burayı referans verir.

  2. Sonrasında kod sayacUret() çağrısını yapıyor. Aşağıda sayacUret()'in ilk satırı çalıştığındaki durumu gösterilmektedir.

    sayacUret() fonksiyonu çağrıldığında, bu fonksiyonun değişkenlerini ve argümanlarını tutmak için Sözcüksel Ortam yaratılır.

    Her Sözcüksel Ortam iki şeyi tutar:

    1. Yerel değişkenlere ait Ortamsal Kayıtlar. Bu durumda let sayac çalıştırıldığında yerel değişken olarak sayac tutulmaktadır.

    2. Dış sözcüksel referans, bu fonksiyonun [[Environment]]'i dir. Burada sayacUret fonksiyonunun [[Environment]]'i evrensel sözcüksel ortama referans verir.

    Öyleyse şimdi iki tane sözcüksel ortam bulunmaktadır: evrensel olan ve sayacUret çağrısını yapan( dış referans verir).

  3. sayacUret() fonksiyonu çalıştığında küçük bir iç fonksiyon yaratılır.

    Fonksiyonun nasıl yaratıldığı yani Fonksiyon Tanımıyla mı yoksa Fonksiyon İfadesiyle mi yaratıldığı önemli değildir. Tüm fonksiyonlar bulunduğu sözcüksel ortama referans eden [[Environment]] özelliği ile yaratılırlar. Bundan dolayı en küçük fonksiyon bile bu özelliği içerir.

    İçte olan yeni fonksiyon için [[Environment]] değişkeni var olan sayacUret'in Sözcüksel Ortamıdır.( Doğduğu yer )

    Dikkat ederseniz bu basamakta iç fonksiyon yaratıldı fakat çağırılmadı. İçindeki kod function() { return sayac++; } çalışmadı, bu kod döndürülecek.

  4. Çalışma devam ettiğinde sayacUret() biter, sonuc olarak ( küçük iç fonksiyon ) global counter değişkenine atanıyor.

    Bu fonksiyonun sadece bir satır kodu var: return sayac++, sadece bu çalışacaktır.

  5. sayac() çağrıldığında, “boş” bir Sözcüksel Ortam yaratılır. hiçbir yerel değişkeni yoktur. Fakat sayac'ın [[Environment]]'i dış referans olarak kullanılır. Bundan dolayı, daha önceden yapılan sayacUret()'in değişkenlerine erişebilir. Oluşturulduğu yer:

    Değişkene erişmesi gerekirse önce kendi yerel sözcüksel ortamına(boş), sonra daha önce çağrılan sayacUret()'in sözcüksel ortamına, en son evrensel ortama bakar.

    sayac için arama yaptığında, en yakınında sayacUret'in sözcüksel ortamı bulunmaktadır.

    Buradaki hafıza yönetimine dikkat ederseniz. sayacUret() çağrısı bittikten bir süre sonra, Sözcüksel ortam hafızada tutulur, çünkü içte bulunan fonksiyonun [[Environment]]'i sayacUret'e referans vermektedir.

    Genel olarak, sözcüksel ortam objesi fonksiyon kullanılabilir olduğu sürece yaşar. Fonksiyon kullanılmadığında silinir.

  6. sayac() fonksiyonu sadece sayac değişkenini döndürmekle kalmaz, artırırda. Dikkat ederseniz değişiklik sadece “olduğu yerde” yapıldı. Var olan sayac değişkeni bulunduğu ortamda değiştirildi.

    Öyleyse bir önceki adıma tek değişiklikle geri dönülmektedir – sayac'ın yeni değeri. Devam eden çağrılar da aynı şekilde çalışırlar.

  7. Sonraki sayac() da aynısını yapar.

Başlangıçta sorulan ikinci sorunun cevabı şimdi açıklık kazanmış olmalı.

Aşağıda isim özelliği calisanUret() fonksiyonu tarafından bulunduğu ortamdan kullanılmıştır:

Sonuç görüşdüğü gibi "Pete" olacaktır.

…Fakat eğer calisanUret() fonksiyonu içerisinde let name tanımlanmamış olsaydı, bu durumda değişkeni arayış evrensel değişkenler ile devam edecekti ve bu durumda sonuç "John" olacaktı.

Closure

Genel programlama tanımlarında “closure” adında bir tanım bulunmaktadır. Bunun ile closure dıştaki değişkenleri hatırlayabilen ve bunlara erişebilen fonksiyon anlaşılmalıdır. Bazı dillerde, bu mümkün değildir veya fonksiyonun özel bir biçimde yazılması gerekmektedir. Fakat yukarıda görüldüğü üzere tüm fonksiyonlar doğal olarak closure’dur ( bunun sadece bir tane istisnası bulunmaktadır bunu "new Function" yazımı bölümünde inceleyebilirsiniz.)

Ön yüz için bir görüşmeye gittiğinizde “Closure nedir?” diye sorulursa doğru cevap closure’un tanımın verilip tüm JavaScript fonksiyonlarının aslında closure olduğunun anlatılması ve sonrasında [[Environment]] özelliğinden, Sözcüksel Ortamdan bahsedilmesi yeterli olacaktır.

Kod blokları ve döngüler, IIFE

Yukarıdaki örnekler fonksiyonlara odaklanmıştır. Fakat Sözcüksel Ortam {...} süslü parantez içerisinde de geçerlidir.

Bir kod bloğu çalıştığında oluşturulur ve blok seviyesinde yerel değişkenleri tutar. Aşağıda bir kaç örneği bulunmaktadır.

If

Aşağıdaki örnekte işlem blok çalıştığında if bloğunun içine girer, yeni Sözcüksel Ortam “if-only” için yaratılmıştır:

Yeni sözcüksel ortam bilgileri dış çevreden alabilir, bundan dolayı ifade erişilebilirdir. Fakat if içerisindeki tüm değişkenler ve Fonksiyonel ifadeler kendi Sözcüksel Ortamdan erişilebilir, dışarıdan erişilemez.

Örneğin if bittikten sonra kullanici değişkeni görünmez olacaktır.

For, while

Her bir döngü kendine ait Sözcüksel Ortama sahiptir. Eğer değişken for içerisinde tanımlanmışsa o sözcüksel ortama yereldir.

for(let i = 0; i < 10; i++) {
  // Her döngü kendisine ait sözcüksel ortama sahiptir.
  // {i: deger}
}

alert(i); // Hata, böyle bir değişken yoktur.

Bu aslında istisnadır, çünkü let i, görünürde {...} dışındadır. Fakat her döngü kendine ait sözcüksel ortamında i'nin o anki değerini içermektedir.

Döngüden sonra i görünmez olur.

Kod Blokları

“yalın” kod bloğu {...} ile değişkenler “yerel kapsama” tamlanabilir.

Örneğin, bir tarayıcıda tüm kodlar evrensel alanları paylaşabilir. Eğer bir kod bloğu içerisinde evrensel alanda bir değişken yaratılırsa, kodun tamamında kullanılabilir. Fakat bu çatışmalara neden olabilir, örneğin aynı değişkenler farklı yerlerde yazılabilirler ve birbirlerinin bilgilerini silebilirler.

Bu değişken isimleri genel kullanılırsa ve kod yazan kişi diğer değişkenin kullanıldığını bilmiyor ise yaşanılacak bir olaydır.

Bunlardan kaçınmak için bir kod bloğu oluşturarak dışarıda bulunan evrensel ortamdan izole edilebilir:

{
  // yerel değişkenler ile dışarıdaki değişkenlere etki etmeden istenilen şekilde izolasyon yapılabilir.
  let mesaj = "Merhaba";

  alert(mesaj); // Merhaba
}

alert(mesaj); // Hata: mesaj tanımlı değildir.

Bloğun dışındaki kod içerideki değişkeni göremez. Çünkü bir her kod bloğu kendine ait sözcüksel ortama sahiptir.

IIFE

Eski kodları arasanız “anında çalışan fonksiyon ifadeleri” ( IIFE ) bu amaçla kullanılmıştır.

Aşağıdaki gibidirler:

(function() {

  let mesaj = "Merhaba";

  alert(mesaj); // Merhaba

})();

Burada bir fonksiyon ifadesi yaratıldı ve doğrudan çağırıldı. Kod hemen çalışır ve kendine ait değişkenlere sahiptir.

Fonksiyon ifadesi parantez içine alınmıştır (function {...}), çünkü eğer JavaScript ana kod akışında "function" görürse bunu Fonksiyon Tanımı olarak algılar. Fakat Fonksiyon Tanımının ismi olmalıdır ve ismi olmadığından dolayı bu kod parantez içine alınmaz ise hata verir.

// Error: Unexpected token (
function() { // <-- JavaScript fonksiyon ismini bulamadı. ('i gördü ve hemen hata verdi.

  let mesaj = "Merhaba";

  alert(mesaj); // Merhaba

}();

“Tamam, önemli değil, hadi Fonksiyon tanımı yapmak için bir ad verelim” derseniz bu da çalışmaz. Çünkü JavaScript Fonksiyon Tanımlarının anında çalışmasına izin vermez:

// Bu defa aşağıdaki parantez hata verecektir.
function go() {

}(); // <-- Fonskyion Tanımı anında çalıştırılamaz.

…Bundan dolayı parantez bu fonksiyonun başka bir ifade kaynağında yaratıldığını ifade eder ve bu da Fonksiyon İfadesidir. İsme gerek duymaksızın doğrudan çalıştırılır.

JavaScript’e başka yollarla da Fonksiyon İfadesini belirtmek mümkündür.

//  IIFE yaratmanın yolları.

(function() {
  alert("Fonksiyon etrafındaki parantezler");
})();

(function() {
  alert("Her şeyin etrafında parantez");
}());

!function() {
  alert("Lojik NOT kapısıyla ifadenin başlaması.");
}();

+function() {
  alert("Matematiksel toplama işareti ile ifadenin başlaması.");
}();

Yukarıdaki tüm durumlarda Fonksiyon İfadesi tanımlanır ve doğrudan çalıştırılır.

Garbage Koleksiyonu

Sözcüksel Ortam objeleri aynı normal değerler gibi hafıza yönetimine konu olurlar.

  • Genelde, Sözcüksel Ortam fonksiyon çalıştıktan sonra temizlenir. Örneğin:

    function f() {
      let deger1 = 123;
      let deger2 = 456;
    }
    
    f();

    Buradaki iki değer teknik olarak Sözcük Ortamının özellikleridir. Fakat f() bittikten sonra bu Sözcük Ortamı erişilemez hale gelir, bundan dolayı hafızadan silinir.

  • … Fakat f den sonra hala iç içe fonksiyon var ise [[Environment]] dıştaki sözcük ortamını canlı tutar:

    function f() {
      let deger = 123;
    
      function g() { alert(deger); }
    
      return g;
    }
    
    let g = f(); // g ulaşılabilir ise, dıştaki sözcük ortamı canlı kalır.
  • Eğer f() birçok defa çağırılırsa ve sonuçları kaydedilirse bu kaydedilen Sözcüksel Ortam objeleri de hafızada kalır. Aşağıdaki 3 farklı kodda daha açık bir şekilde gösterilmiştir.

    function f() {
      let deger = Math.random();
    
      return function() { alert(deger); };
    }
    
    // Dizideki 3 fonksiyon da kendine ait sözcüksel ortama sahiptirler.
    //         LE   LE   LE
    let arr = [f(), f(), f()];
  • Sözcüksel Ortam objesi erişim olmayınca ölür. Bu iç içe fonksiyonların referansı kalmadığında meydana gelir. Aşağıdaki kodda g erişilemez olduğunda value'da hafızadan silinir.

    function f() {
      let value = 123;
    
      function g() { alert(value); }
    
      return g;
    }
    
    let g = f(); // g canlı olursa ona karşılık gelen Sözcüksel Ortam'da hayatta kalır.
    
    g = null; // şimdi hafıza temizlendi.

Gerçek-Hayat Optimizasyonu

Görüldüğü üzere, teoride bir fonksiyon hayatta olduğun sürece onun dışındaki ona bağlı değişkenler de hayatta kalır.

Pratikte ise, JavaScript motoru bunu optimize eder. Değişken kullanımını analiz eder ve eğer dışarıdaki fonksiyonun kullanılmadığı açık ise silinir.

Bunun V8 ( Chrome, Opera)'daki yan etkisi ise böyle değişkenlerin debugging sırasında da görünememesidir.

Aşağıdaki örneğin Chrome’da konsolu açarak test ediniz.

Durduğunda konsolda alert(deger) komutunu yazınız.

function f() {
  let deger = Math.random();

  function g() {
    debugger; // konsolda: alert(deger) yazdırın; Böyle bir değişken bulunamamaktadır.
  }

  return g;
}

let g = f();
g();

Gördüğünüz gibi böyle bir değişken bulunamamaktadır. Teoride, erişilebilir olmalıdır fakat JavaScript motoru bunu optimize etmiştir.

Bu komik debug problemlerine neden olabilir. Bunlardan biri – beklenenin aksine aynı isme sahip dış değişkenin görülmesi:

let deger = "Sürpriz!";

function f() {
  let deger = "En yakın değer";

  function g() {
    debugger; // in console: type alert( value ); Surprise!
  }

  return g;
}

let g = f();
g();
Görüşmek üzere!

V8’in bu özelliğini bilmekte fayda var. Eğer Chrome/Opera ile debugging yapıyorsanız, er ya da geç bu özellikle tanışacaksınız.

Bu bir debugger problemi değil, V8 motorunun bir özelliğidir. Belki ileride bu özellik değişebilir. Bu sayfayadaki örneği çalıştırarak her zaman bunu kontrol edebilirsiniz.

Görevler

önem: 5

sayac ve sayac2 aynı sayacUret fonksiyonu kullanmaktadır.

Bu sayaçlar birbirlerinden bağımsız mıdır? İkinci sayaç ne gösterecek ? 0,1 veya 2,3 veya tamamen başka bir şey mi?

function sayacUret() {
  let say = 0;

  return function() {
    return say++;
  };
}

let sayac = sayacUret();
let sayac2 = sayacUret();

alert( sayac() ); // 0
alert( sayac() ); // 1

alert( sayac2() ); // ?
alert( sayac2() ); // ?

Cevap: 0,1.

sayac ve sayac2 sayacUret'in farklı zamanlarda çağırılmasıyla oluşmuştur.

Bundan dolayı birbirinden bağımsız dış sözcüksel çevrelere sahiptirler. Yani her ikisi kendisine ait say değişkenine sahiptir.

önem: 5

Aşağıda yapıcı fonksiyon ile üretilmiş bir sayac objesi bulunmaktadır.

Çalışır mı? Çalışırsa ne gösterir?

function Sayac() {
  let say = 0;

  this.yukselt = function() {
    return ++say;
  };
  this.alcalt = function() {
    return --say;
  };
}

let sayac = new Sayac();

alert( sayac.yukselt() ); // ?
alert( sayac.yukselt() ); // ?
alert( sayac.alcalt() ); // ?

Kesinlikle çalışır

İçte bulunan fonksiyonlar aynı dış Sözcüksel Çevreye sahip olduklarından dolayı aynı say değişkenine erişirler:

function Sayac() {
  let say = 0;

  this.yukselt = function() {
    return ++say;
  };
  this.alcalt = function() {
    return --say;
  };
}

let sayac = new Sayac();

alert( sayac.yukselt() ); // 1
alert( sayac.yukselt() ); // 2
alert( sayac.alcalt() ); // 1

Aşağıdaki son satır çalıştığında sonuç ne olur?

let sozcuk = "Merhaba";

if (true) {
  let kullanici = "Ahmet";

  function selamVer() {
    alert(`${sozcuk}, ${kullanici}`);
  }
}

selamVer();

Sonuç hata olacaktır.

selamVer fonksiyonu if'in içerisinde oluşturulmuştur. Bundan dolayı sadece içerisinde yaşar. Dışında selamVer diye bir fonksiyon bulunmamaktadır.

önem: 4

topla adında bir fonksiyon yaz ve şu şekilde çalışsın topla(a)(b) = a+b

Evet aynen bu şekilde, iki tane parantez ile ( yanlış yazım değil)

Örneğin:

sum(1)(2) = 3
sum(5)(-1) = 4

İkincisinin çalışabilmesi için ilkinin fonksiyon döndürmesi gerekmektedir.

Şunun gibi:

function topla(a) {

  return function(b) {
    return a + b; // dıştaki Sözcüksel Ortamdan "a" değişkenini almakta.
  };

}

alert( topla(1)(2) ); // 3
alert( topla(5)(-1) ); // 4
önem: 5

Diziler için arr.filter(f) adında JavaScript dahilinde bir metod bulunmaktadır. Bu metod tüm elemanları f fonksiyonu ile filtreler. Eğer true döner ise o eleman sonuç listesinde görünür.

“Kullanmaya hazır” filtreler yapınız:

  • arasinda(a, b)a ve b araında veya eşit.
  • diziIcinde([...]) – verilen dizi içerisinde

Kullanımları şu şekilde olacaktır:

  • arr.filter(arasinda(3,6)) – sadece 3 ile 6 arasındaki değerleri seçer.
  • arr.filter(diziIcinde([1,2,3])) – elemanlar eşitse onları seçeer. Bu durumda sadece [1,2,3] elemanları seçilecektir.

Örneğin:

/* .. arasinda ve diziIcinde için yazdığınız kodlar. */
let arr = [1, 2, 3, 4, 5, 6, 7];

alert( arr.filter(arasinda(3, 6)) ); // 3,4,5,6

alert( arr.filter(diziIcinde([1, 2, 10])) ); // 1,2

Testler ile korunaklı olan aç.

arasinda Filtresi

function arasinda(a, b) {
  return function(x) {
    return x >= a && x <= b;
  };
}

let arr = [1, 2, 3, 4, 5, 6, 7];
alert( arr.filter(arasinda(3, 6)) ); // 3,4,5,6

diziIcinde Filtresi

function diziIcinde(arr) {
  return function(x) {
    return arr.includes(x);
  };
}

let arr = [1, 2, 3, 4, 5, 6, 7];
alert( arr.filter(diziIcinde([1, 2, 10])) ); // 1,2
function diziIcinde(arr) {
  return x => arr.includes(x);
}

function arasinda(a, b) {
  return x => (x >= a && x <= b);
}

Çözümü testler korunaklı alanda olacak şekilde aç.

önem: 5

Sıralanacak obje dizisi bulunmaktadır:

let kullanicilar = [
  { ad: "Ahmet", yas: 20, soyad: "Zurnacı" },
  { ad: "Hideo", yas: 18, surname: "Konami" },
  { ad: "Jane", yas: 19, surname: "Hathaway" }
];

Bunu yapmanın en yaygın yolu:

// ad'a göre (Ann, Hideo, Jane)
kullanicilar.sort((a, b) => a.ad > b.ad ? 1 : -1);

// yaşa göre (Hideo, Jane, Ahmet)
kullanicilar.sort((a, b) => a.yas > b.yas ? 1 : -1);

Aşağıdaki şekle çevirmek mümkün mü?

kullaniclar.sort(alanIle('ad'));
kullaniclar.sort(alanIle('yas'));

Böylece fonksiyon yazmak yerine sadece alanIle(alanAdı) yazılabilir.

alanIle fonksiyonunu yazınız.

let kullanicilar = [
  { ad: "Ahmet", yas: 20, soyad: "Zurnacı" },
  { ad: "Hideo", yas: 18, soyad: "Konami" },
  { ad: "Jane", yas: 19, soyad: "Hathaway" }
];

function alanIle(alan) {
  return (a, b) => a[alan] > b[alan] ? 1 : -1;
}

kullanicilar.sort(alanIle('ad'));
kullanicilar.forEach(kullanici => alert(kullanici.ad)); // Ahmet, Hideo, Jane

kullanicilar.sort(alanIle('yas'));
kullanicilar.forEach(kullanici => alert(kullanici.ad)); // Hideo, Jane, Ahmet
önem: 5

Aşağıdaki kod nisancilar dizisi olusturmaktadır.

Her fonksiyon kendi numara çıktısını verir. Fakat bir hata var…

function orduYap() {
  let nisancilar = [];

  let i = 0;
  while (i < 10) {
    let nisanci = function() { // Nişancılar fonksiyonu
      alert( i ); // numara göstermeli
    };
    nisancilar.push(nisanci);
    i++;
  }

  return nisancilar;
}

let ordu = orduYap();

ordu[0](); // nisanci 0 fakat 10 gösteriyor.
ordu[5](); // nisancı 5 fakat yine 10 gösteriyor.
// ... tüm nişancılar kendi numaraları yerine 10 gösteriyorlar.

Neden tüm nişancılar aynı? Kodu olması gerektiği duruma getiriniz.

Testler ile korunaklı olan aç.

orduYap fonksiyonunun içine bakılacak olursa sonuç daha açık bir şekilde görülecektir.

  1. Boş nisancilar dizisi yaratır.

    let nisancilar = [];
  2. nisancilar.push(function..) döngüsü ile doldurur.

    Her bir elemanı fonksiyondur, buna göre sonuç dizisi aşağıdaki gibi olacaktır:

    nisancilar = [
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); }
    ];
  3. En nihayetinde dizi fonksiyondan döner.

Sonrasında ordu[5]() çalıştığında diziden ordu[5] elemanı alınır ( fonksiyon olacaktır ) ve çağırılır.

Peki neden tüm fonksiyonlar aynı şeyi gösterir?

Bunun nedeni nisancilar fonksiyonunun içinde i değişkeninin olmamasıdır. Böyle bir i fonksiyonu çağırıldığında i değeri dış ortamdan alınır.

i'nin değeri nedir ?

Yine kaynak koda bakılacak olursa:

function orduYap() {
  ...
  let i = 0;
  while (i < 10) {
    let nisanci = function() { // nisanci fonksiyonu
      alert( i ); // numarayı yazmalı
    };
    ...
  }
  ...
}

…Sizin de görebilecğeiniz gibi orduYap() ile aynı sözcüksel ortamda bulunmaktadır. Fakat ordu[5]() çağırıldığında, orduYap() işini bitirmiş ve son değeri olan 10'u almış oluyor.( while'ın sonu 10 olmaktadır.

Sonuç olarak tüm nisanci fonksiyonları dış sözcüksel ortamdan i=10 değerini alırlar.

Bu basit bir şekilde düzeltilebilir:

function orduYap() {

  let nisancilar = [];

  for(let i = 0; i < 10; i++) {
    let nisanci = function() { // nisanci fonksiyonu
      alert( i ); // numarayı yazmalı
    };
    nisancilar.push(shooter);
  }

  return nisancilar;
}

let ordu = orduYap();

ordu[0](); // 0
ordu[5](); // 5

Şimdi doğru çalışmaktadır çünkü for(...){...} bloğu her çalıştığında yeni bir Sözcüksel Ortam yaratılır ve o ortama ait i değeri tanımlanır.

Artık i değeri daha yakındır. orduYap() Sözcüksel Ortamından ayrılmışır, döngünün her adımında kendine has bir Sözcüksel Çevreye sahiptir. Her bir nisanci'da yaratıldığı ortamdaki değerlere bakar.

while döngüsünü for'a çevirerek bu problemi çözmüş olduk.

Bunu sağlayan başka bir yöntem daha vardır:

function orduYap() {
  let nisancilar = [];

  let i = 0;
  while (i < 10) {
    let j = i;
    let nisanci = function() { // nisanci fonksiyonu
      alert( j ); //  numarayı yazmalı
    };
    nisancilar.push(shooter);
    i++;
  }

  return nisancilar;
}

let ordu = orduYap();

ordu[0](); // 0
ordu[5](); // 5

while döngüsü değişkedi fakat for döngüsünde olduğu gibi değerler her döngüde oluşan Sözcüksel Ortama atanacak şekilde ayarlandı. Böylece her nisanci çağırıldığında doğru değeri alması garantilendi.

let j = i ile i nin yerel bir kopyasını oluşturmuş olduk. İlkel değişkenler “değer ile” kopyalandığından dolayı artık i den tamamen farklı bir değişkene sahip olduk.

function orduYap() {

  let nisancilar = [];

  for (let i = 0; i < 10; i++) {
    let nisanci = function () { // nisanci fonksiyonu
      alert(i); // numarayı yazmalı
    };
    nisancilar.push(shooter);
  }

  return nisancilar;
}

Çözümü testler korunaklı alanda olacak şekilde aç.

Eğitim haritası

Yorumlar

yorum yapmadan önce lütfen okuyun...
  • Eğer geliştirme ile alakalı bir öneriniz var ise yorum yerine github konusu gönderiniz.
  • Eğer makalede bir yeri anlamadıysanız lütfen belirtiniz.
  • Koda birkaç satır eklemek için <code> kullanınız, birkaç satır eklemek için ise <pre> kullanın. Eğer 10 satırdan fazla kod ekleyecekseniz plnkr kullanabilirsiniz)