Çoğu insan JavaScript'e bir şeyler inşa ederek başlar. Bir butona işlev kazandırırsınız, biraz veri çekersiniz ve DOM'un değişimini izlersiniz. Sonra soyutlamalar sızmaya başlar. Mantıksız görünen hatalar ortaya çıkar: değişkenler tanımlanmadan önce var olur, fonksiyonlar erişmemesi gereken değişkenleri hatırlar ve this anahtar kelimesi pencereye (window), bir butona veya hiçbir şeye işaret eder. Bu genellikle, kaputun altına bakmanız ve motorun gerçekte ne yaptığını anlamanız gerektiğini fark ettiğiniz andır.
Yürütme Bağlamı (Execution Context): İki Aşamalı Kurulum
Tarayıcınızdaki veya Node.js'deki JavaScript motoru bir betikle karşılaştığında, dosyayı bir insanın bir sayfayı taraması gibi sadece yukarıdan aşağıya okumaz. Bunun yerine, belirli bir kod parçasını çalıştırmak için gereken her şeyi tutan bir kapsayıcı olan bir yürütme bağlamı (execution context) oluşturur. Her yürütme bağlamı iki farklı aşamadan geçer.
Bellek Oluşturma Aşaması. Bu ilk geçiş sırasında motor, tüm kapsamı (scope) tarar ve bulduğu her değişken ve fonksiyon bildirimi için bellek ayırır. Eğer bir var görürse, yer ayırır ve yer tutucu olarak undefined değerini saklar. Eğer bir fonksiyon bildirimi görürse, fonksiyonun gövdesinin tamamını saklar. Geleneksel function anahtar kelimesiyle tanımlanan bir fonksiyonun, aynı kapsamdaki daha önceki satırlardan çağrılabiliyor olmasının sebebi budur. Motor, yürütmeye başlamadan önce ondan zaten haberdardır.
Kod Yürütme Aşaması. Şimdi motor kodunuzu satır satır çalıştırır. Atamalar burada gerçekleşir. İfadeler değerlendirilir. Fonksiyonlar çağrılır. Eğer var name = "Alice"; yazdıysanız, birinci aşamadaki yer tutucu sonunda dize (string) ile değiştirilir. Bu iki geçişli davranışı anlamak, şaşırtıcı derecede fazla kafa karışıklığını giderir. Motor dikkatsiz değildir; sadece katı bir kurulum rutini izlemektedir.
Değişkenler ve Geçici Ölü Bölge (Temporal Dead Zone)
var, let ve const arasında seçim yapmak sadece üslup tercihi değildir. var fonksiyon kapsamlıdır (function-scoped), yani süslü parantezleri tamamen görmezden gelir. Onu bir if bloğu içinde tanımlarsanız, dışarı sızar. Bu davranış erken JavaScript dönemlerinde mantıklı olabilirdi ancak modern uygulamalarda gerçek bakım zorluklarına neden olur. let ve const ise blok kapsamlıdır (block-scoped). Süslü parantezlere saygı duyarlar ve blok sona erdiğinde yok olurlar.
Ayrıca, hoisting mekanizmasının onlara nasıl davrandığı konusunda ince ama kritik bir fark vardır. var bildirimleri yukarı taşınır (hoisted) ve hemen undefined ile başlatılır. let ve const da teknik olarak yukarı taşınır; motor, bildirim satırına ulaşmadan önce onların var olduğunu bilir. Ancak başlatılmazlar. "Geçici Ölü Bölge" (Temporal Dead Zone) denilen bir belirsizlik durumunda beklerler. Eğer bildirim satırı çalıştırılmadan önce onları okumaya çalışırsanız, sinsi bir undefined yerine sert bir ReferenceError alırsınız. Bu çökme aslında yararlıdır; mantığın başlatılmamış veriler üzerinden ilerlemesini engeller.
Lexical Scope ve Closures
Kapsam (scope) basit bir soruyu yanıtlar: Bu değişkene nereden erişebilirim? JavaScript, lexical scope (sözdizimsel kapsam) kullanır; bu, bir fonksiyonun erişim haklarının fonksiyonun çağrıldığı yere göre değil, kaynak kodda fiziksel olarak yazıldığı yere göre belirlendiği anlamına gelir. Bir fonksiyonu başka bir fonksiyonun içinde tanımlarsanız, içteki fonksiyon değişkenleri okumak için dışarıdaki üst fonksiyona ulaşabilir. Dıştaki fonksiyon ise içeriye ulaşamaz. Bu ilişki statiktir. O iç fonksiyonu modüller arası taşıyabilir, global bir değişkende saklayabilir ve tamamen farklı bir dosyadan çağırabilirsiniz. Yine de doğduğu kapsamdaki değişkenleri hatırlar.
Bu davranış doğal olarak closures (kapsamlar) oluşturur. Bir iç fonksiyon, dış kapsamındaki bir değişkene referansını koruduğunda bir closure oluşur. Dış fonksiyon yürütmesini bitirip yerel değişkenleri çöp toplayıcı (garbage collector) tarafından temizlenmesi gerekse bile, JavaScript iç fonksiyon onlara hala ihtiyaç duyduğu için onları bellekte korur. İç fonksiyon, çevresindeki ortamı da beraberinde taşır.
Bu sadece akademik bir detay değildir. Closures, açık erişim belirleyicilerinin (access modifiers) bulunmadığı bir dilde size özel durum (private state) oluşturmanın pratik bir yolunu sunar.
function makeCounter() {
let count = 0;
return function() {
count = count + 1;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
Burada count gizlidir. Döndürülen fonksiyonun dışındaki hiçbir şey onu sıfırlayamaz veya doğrudan okuyamaz. Bu, yalnızca kapsam mekanikleriyle oluşturulmuş özel bir değişkendir.
Pratikte Hoisting
JavaScript'in "deklarasyonları yukarı taşıdığını" duymak yaygındır. Bu yararlı bir zihinsel modeldir ancak kod aslında yeniden yazılmaz. Bellek oluşturma aşamasında motor, yürütme başlamadan önce sadece deklarasyonları kaydeder. var x = 5; gibi bir ifade, deklarasyon ve başlatmanın birbirinden ayrılmış olduğu gibi davranır. var x; deklarasyonu erken işlenir ve undefined olarak başlatılır. x = 5; ataması ise tam olarak yazdığınız yerde kalır ve yürütme aşamasında çalışır.
Bu nedenle, var şaşırtıcı sonuçlara yol açabilir. Bir fonksiyonun en başında kullanılan bir değişken, en altında devasa bir atama olsa bile undefined değerini tutabilir. let ve const kullanmak bu özel hata riskini ortadan kaldırır; çünkü temporal dead zone, deklarasyonlarınızı kullanımınızın üzerinde tutmaya zorlar.
this Değerini Nasıl Alır
Eğer yürütme bağlamı (execution context) ve kapsam (scope), değişkenlerin nerede yaşadığını belirliyorsa; this, o anda hangi nesnenin yetkili olduğunu belirler. Lexical değişkenlerin aksine, this değerinin nerede belirleneceği fonksiyonun nerede yazıldığına bağlı değildir. Tamamen fonksiyonun nasıl çağrıldığına göre belirlenir.
Varsayılan bağlama (Default binding), düz ve bağımsız bir fonksiyonu çağırdığınızda gerçekleşir. Strict mode olmayan modda, this global nesneye geri döner. Bir tarayıcıda bu window nesnesidir. Herhangi bir bağlam olmadan bir fonksiyon çağırırsanız, farkında olmadan yanlışlıkla global durumu değiştiriyor olabilirsiniz.
Örtük bağlama (Implicit binding), bir fonksiyonu bir nesne üzerindeki metod olarak çağırdığınızda gerçekleşir. Eğer user.sayName() yazarsanız, nokta operatörü sessizce motora, o çağrı süresince this değerini user olarak ayarlamasını söyler. Çağrının yapıldığı yer, tanımın yapıldığı yerden daha önemlidir.
Açık bağlama (Explicit binding), her şeyi manuel olarak geçersiz kılmanıza olanak tanır. call() ve apply(), this değerini sağladığınız belirli bir nesne olmaya zorlarken bir fonksiyonu anında çağırır. Aralarındaki tek fark argümanların nasıl iletildiğidir: call virgülle ayrılmış bir liste alırken, apply bir dizi (array) alır. bind() ise farklı çalışır. Fonksiyonu hemen çağırmaz. Bunun yerine, this değerini sağladığınız değere kalıcı olarak kilitleyen yeni bir fonksiyon döndürür. Bu, etrafa dağıtıldığında bağlamını kaybedebilecek callback'ler için paha biçilemezdir.
New bağlaması (New binding), bir fonksiyon çağrısının önüne new anahtar kelimesini koyduğunuzda devreye girer. Motor yepyeni, boş bir nesne oluşturur, prototip bağlantısını ayarlar ve constructor içindeki this noktasını bu yeni örneğe (instance) yönlendirir.
Kalıcı Kod Yazmak
Mekanikleri anlamak işin sadece yarısıdır. Diğer yarısı ise altı ay sonra insanların okuyabileceği kodlar yazmaktır.
DRY (Don't Repeat Yourself - Kendini Tekrar Etme) kulağa bariz geliyor ancak sürekli ihlal ediliyor. Eğer kendinizi üç farklı dosyada aynı doğrulama mantığını veya API çağrısı desenini yazarken buluyorsanız, bunu dışarı çıkarın. Tek bir fonksiyon yazın. Tek bir doğruluk kaynağı, gereksinimler değiştiğinde güncellenecek tek bir yer anlamına gelir ve bu, ifade edilmesi zor olacak kadar çok zaman kazandırır.
KISS (Keep It Simple, Stupid - Basit Tut, Aptalca Olmasın), egoya karşı bir savunmadır. İç içe geçmiş ternary operatörleri ve tek satırlık closure'lar zekice hissettirebilir ancak hata ayıklama (debugging) sırasında saatler kaybettirir. Daha önce gösterdiğim closure deseni güçlüdür, ancak sadece yapabildiğiniz için beş seviye derinliğe inmek bir hatadır. Basit kod; ekip değişimlerine, canlı ortam olaylarına ve bir şeyler bozulduğunda kimsenin nedenini hatırlamadığı o gece yarısı uyarılarına karşı hayatta kalır.
Gerçek Kazanç
Yürütmeyi incelemek
