JavaScript günümüzde en popüler programlama dillerinden biridir. Geliştiriciler olarak çoğu zaman JavaScript’in arka planda nasıl çalıştığının farkında olmayız. Bazen yazdığımız JavaScript kodu beklentimizden çok daha farklı çalışır. Yazdığımız koddan eminizdir ama JavaScript’in çalışma yapısı farklı olduğundan farklı sonuçla karşılaşırız. Bu yüzden JavaScript’in nasıl çalıştığını anlamamız son derece önemlidir.
Bu yazıda amacım değişkenler, döngüler, diziler, karar yapıları vb. konuları anlatmak değil; JavaScript’in tarayıcıdaki çalışma yapısını anlatmaya çalışmak. Hepsini bir yazıya sığdırmak tabii ki mümkün değil. Elimden geldiğince kavramları özetleyip size farklı bir bakış açısı kazandırmaya çalışacağım. Bunları anlamanız yani yazdığınız kodun nasıl işlemlerden geçtiği hakkında fikir sahibi olmanız sizi daha iyi bir JavaScript geliştiricisi yapacaktır.
Bu yazıda değineceğim konular;
- Syntax Parser
- Lexical Environment
- Execution Context
- Hoisting
- Closures
Syntax Parser
Syntax parser JavaScript motorunun bir parçasıdır. Herhangi bir JavaScript kodu yazdığımızda bilgisayar bunu direkt olarak çalıştıramaz. JavaScript bir scripting dilidir. Çoğunlukla tarayıcılar içinde çalışır. Ama yorumlanan bir dil olduğundan yorumlayıcının olduğu her yerde de çalışır. Syntax parser kodunuzu karakter karakter okur ve syntax’ın geçerli olup olmadığını kontrol eder ve sonra bilgisayarın anlayacağı biçime dönüştürür. Syntax parser’ı kodumuz ve bilgisayar arasındaki yorumlayıcı ve çevirici olarak düşünebiliriz. Yani aşağıdaki gibi bir kod yazdığımızda;
function name() { var name = 'Cem Doğan'; }
Yorumlayıcı bu kodu karakter karakter okuyarak yani, f-u-n-c-t-i-o-n harfleri ile bir fonksiyonun tanımladığını anlar ve sonrasında bir boşluk görür. Paranteze kadar okuyarak fonksiyonun adını belirler. Daha sonra alt satıra geçerek değişkeni belirler. Bu şekilde devam ederek programımızı bilgisayarın anlayacağı biçime dönüştürür.
Lexical Environment
Lexical Environment, kodumuzu nerede yazdığımızla ilgilenir yani syntax parser aşamasından sonra kodumuzda bulunan fonksiyonların, değişkenlerin ve diğer tüm bileşenlerin nasıl etkileşime gireceklerini belirler. JavaScript kodumuzda bir scope açmak için kullanıdığımız süslü parantezi “{}” her kullandığımızda bir lexical environment oluşur. Bu süslü parantezler iç içe de olabilir. Yukarıdaki fonksyion örneğinde değişken, fonksiyonun oluşturduğu lexical environment’ta yer alır. Syntax parser’da bu scope’lara göre değişkenlerin fonksiyonların birbirleriyle ilişkilerini belirler.
var a = 2; var b = 4; function foo() { console.log('bar'); }
Aklınızda canlanması açısından yukarıdaki kodu incelediğimizde lexical environment’ı aşağıdakine benzer olur.
lexicalEnvironment = { a: 2, b: 4, foo: <ref. to foo function> }
Lexical environment’ı anlamamız, let ve var anahtar kelimelerinin arasındaki farkı da anlamamızı sağlar. let anahtar kelimesi değişkenleri kendi scope’larında saklar. var anahtar kelimesi ise değişkenlerin tanımını global scope’a taşır(hoists). Yani var kullanarak değişkeni tanımladığımızda bloktan bağımsız bir şekilde global kısımdan da erişilebilir hale gelir. Burada bir istisna var; fonksiyon bloklarında değişkeni var veya let ile tanımlamamız değişkeni o blokta kullanmaya zorlar.
Execution Context
Programlarımızda bir çok lexical environment oluşur. Ama bunlar hangi sırayla çalıştırılır? Bunları yönetmemizi sağlayan yapıya execution context denir. JavaScript’te execution context’ler üç tiptir;
- Global Execution Context: Bu context diğer tüm oluşacak contextleri kapsayan ana context’tir. Yazdığımız bir javaScript kodu herhangi bir fonksiyon içinde değilse global execution context’in içindedir. Bir programda sadece bir tane global execution context olabilir.
- Functional Execution Context: Yazdığımız her fonksiyon çağrısında oluşan context türüdür. Fonksiyon tanımlamalarında değil çağırıldıkları zaman oluşur.
- Eval Function Execution Context: Eval fonksiyonu içerisinde çalıştırılan context türüdür.
JavaScript engine, yazdığımız bir fonksiyon veya script gördüğünde yeni bir execution context oluşturur. Bu execution context’ler ise ilgili kod bloğunun lexical environment’ı tarafından tanımlanır.
function b() { } function a() { b(); } var myVar = 1; a();
Yukarıdaki kodu incelersek;
- Kodumuz tarayıcıya yüklendiği zaman JavaScript engine kodu yukarıdan aşağı okuyarak fonksiyon ve değişken tanımlamalarını memory’e koyar.
- Global execution context oluşturulur ve execution stack’in en altına yerleştirir.
- a fonksiyonu çağrıldığında yeni bir a execution context yaratılır ve global execution context’inin üzerine yerleşir.
- a fonksiyonunun içindeki b fonksiyonu çağrıldığında ise yeni bir b execution context’i oluşur ve a execution context’inin üzerine yerleşir.
- b fonksiyonu çalıştığında kendi context’i ile birlikte garbage collector tarafından execution stack’ten çıkarılır.
- Aynı şekilde a fonksiyonunun çalışmasıyla birlikte a context’i garbage collector tarafından execution stack’ten atılır ve akış global context’e gelir ve stack boşaltılır.
Execution stack, LIFO (Last in First out) yapısında çalışır. Stack’e en son giren ilk çıkacağından en alt context’i çalıştırmak için üstündeki context’lerin sırayla çalışmasının bitmesini bekler. Execution stack hakkında unutmamamız gerekenler; single thread çalışır, kodlar senkron olarak çalıştırılır, bir tane global context içerir, sonsuz tane fonksiyon çağrısı olabilir ve her fonksiyon çağrısı veya fonksiyonun kendisini çağırması durumunda yeni bir execution context oluşturur.
JavaScript kodunu tarayıcılarda çalıştırdığımızda oluşan window objesi bir global objedir. this anahtar kelimesi bu global objenin referansını tutar. Tabii ki JavaScript kodunuzu sunucu tarafında bir node.js uygulamasında çalıştırdığınız zaman window objesi oluşmayacaktır. Ama her zaman bir tane global execution context vardır. Tarayıcılarda yeni bir pencere açtığımız zaman her pencerenin kendine özel global execution context’i vardır.
Execution stack ve context’ini anlamamız yazının devamında değineceğimiz hoisting, scope ve closures kavramlarını anlamamız için son derece önemlidir.
b(); console.log(a); var a = "Merhaba Dünya!" function b() { console.log("b fonksiyonu") }
Yukarıdaki gibi bir kod düşünelim. Diğer programlama dillerinin aksine JavaScript’in farklı bir çalışma yapısı vardır. Kodlar yukarıdan aşağı okunduğundan b fonksiyonu ve a değişkeni daha tanımlanmadan kullanamamamız gerekir. Kodları çalıştırdığımızda aşağıdaki gibi bir sonuç alırız.
b fonksiyonu undefined
Fonksiyon düzgün çalıştı ve değişkenimizin değerinin olmasına rağmen neden undefined yazdı? Bu şekilde sonuç almamız fonksiyon ve değişken tanımlarının JavaScript engine’i tarafından fiziksel olarak yukarı taşındığı anlamına gelmez. Bu durum aslında tanımlanan değişkenlere varsayılan bir değer atama ve fonksiyon bildirimlerinin ise memory’ye yerleştirme işlemidir. Buna hoisting denir. Bu durumu daha iyi anlamak için execution context’in nasıl oluşturulduğuna bakalım. Execution context iki aşamada oluşur;
Creation Phase: Bu aşamada outer environment, this ve window oluşur. var ile tanımlanan tüm değişkenlere varsayılan değer olan undefined atanır ve fonksiyon tanımları da memory’e taşınır. let ve const ile tanımlanan değişkenlere ise herhangi bir değer atanmaz. Bu değişkenlere erişmek istediğimizde ReferenceError hatası alırız.
Execution Phase: Bu aşamada ise JavaScript engine kodumuzu satır satır çalıştırmaya başlar ve değişkenlerin memory’de bulunan tanımlarına gerçek değerlerini atar. Bu aşamaya kadar javascript engine değişkenlerin değerini bilmez.
Not: JavaScript’te undefined özel değeri değişkenin boş veya tanımlanmadığı anlamına gelmez. Memory’de yer işgal eden bir özel değerdir.
Buraya kadar anlatılanlardan execution context ve execution stack’i anladığımızı düşünüyorum. Şimdi de JavaScript’te yer alan scope chain ve outer environment kavramlarından bahsedelim.
function b() { console.log(myVar); } function a() { var myVar = 2; b(); } var myVar = 1; a();
Kodunu incelemeye başlayalım; kodu çalıştırdığımızda konsola “1” yazılır. Yani b fonksiyonu içinde myVar değişkeni olmamasına rağmen global seviyede olan myVar değişkenini ekrana yazdırır. Yukarıda da anlattığım gibi JavaScript engine bir tane global execution context ve her fonksiyon çağrısında yeni bir fonksiyon ait execution context oluşturur.
Program içinde değişkenleri kullandığımız zaman JavaScript ilk önce değişkenin bulunduğu aktif context içine bakar. Eğer orada değişkeni bulamaz ise referans verdiği outer environment’a yani bir üst scope’una bakar. Her execution context’in referans verdiği bir outer environment’ı vardır. Bizim örneğimizde ise b fonksiyonunun outer environment’ı global execution context’tir. Aynı durum a fonksiyonu için de geçerlidir. Fonksiyon çağrısı gerçekleştiğinde JavaScript engine o fonksiyon ile bir üst scope’u arasında outer referansını oluşturur.
function a() { function b() { console.log(myVar); } var myVar = 2; b(); } var myVar = 1; a();
Şimdi de fonksiyonları yukarıdaki gibi iç içe tanımlayalım. b fonksiyonu için dış referansı a fonksiyonu olur. Onun da dış referansı ise global execution context olur.
Yani bir değişkeni ararken bir üst scope’a çıkar orada bulamazsa onun da bir üstüne çıkar ta ki global execution context’e gelene kadar, orada da bulamazsa hata fırlatır. Fonksiyonların iç içe olması execution stack’teki yerlerini etkilemez hepsinin ayrı execution context’i vardır. Bizim örneğimizde ise bu durumda ekrana a fonksiyonu içinde tanımlı olan değişkenin değerini yani “2” yazar. İşte bu şekilde oluşan zincire scope chain denir.
Closures
Bu kısımda yine önemli konulardan biri olan closure’ları açıklamaya çalışacağım. Closure’lar, iyi bir JavaScript geliştiricisi için anlaşılması kesinlikle önemli olan konuları arasındadır. Bu konuyu anlamak için gerekli bilgilere sahibiz. Closure’lar memory’deki fonksiyonun çalıştırılıp execution stack’ten silindikten sonra lexical environment’ını korur yani iç scope’tan dış scope’a erişimemizi sağlar. Aşağıdaki örneği inceleyelim;
function greet(whattosay) { return function(name) { console.log(whattosay + ' ' + name); } } var sayHi = greet('Hi'); sayHi('Cem');
Kodumuzda fonksiyon döndüren bir fonksiyonumuz var. greet fonksiyonunu çağırdığımızda geriye tekrardan bir fonksiyon döndürüyor. Kodu çalıştırdığımızda ekrana “Hi Cem” yazar. İlk önce global context oluşur, ardından greet fonksiyonu için bir execution context ve en son greet fonksiyonunun içindeki anonim fonksiyon için execution context oluşur. Burada dikkat etmemiz gereken durum bu context’ler birbirinden tamamen ayrıdır. Dolayısıyla ilk çalışan greet fonksiyonu çalıştırıldıktan sonra execution stack’ten atıldığında ona ait değişken olan whattosay değişkeninin de yok olması gerekir. Ama içindeki fonksiyon hala o değişkeni kullanıyor. Bu durum tamamen closure’lar tarafından meydana gelir.
Yani, greet fonksiyonu çalıştığında execution context’i stack’ten atılır ve fonksiyona ait değişkenler greet fonksiyonunun kapladığı memory bölgesinde durmaya devam eder. sayHi fonksiyonu çağırıldığında ise scope chain konusunda bahsettiğim gibi içindeki değişkenleri kontrol eder. Kendi execution context’inde yok ise bir üst scope’u olan greet fonksiyonuna ait memory bölgesini kontrol eder. Yani closure’lar bir kapsam yaratarak sayHi fonksiyonu ve greet fonksiyonundan geriye kalan değişkeni sarmalar.
Closure’lar JavaScript programlama dilinin en önemli özelliklerinden biridir. JavaScript engine closure’ları yaratır ve biz de onların avantajlarından yararlanırız.
Sonuç
JavaScript’in nasıl çalıştığını anlamanın her geliştirici için önemli olduğunu düşünüyorum. Umarım JavaScript Engine’nin kodunuzu nasıl yorumladığı hakkında fikir sahibi olmuşsunuzdur. Yazının başında da dediğim gibi bu kavramları anlamak kodunuzda beklemediğiniz sonuçlarla karşılaştığınızda, kodunuzu değerlendirmeniz için son derece önemlidir. Bunların dışında yine çok önemli olan konulardan Prototype, Class, IIFE (Immediately Invoked Function Expression), Module Pattern, Asynchronous, Callback Function ve Promises gibi konuları da anlamak çok önemlidir. İnternette bunlarla ilgili bir sürü döküman mevcut.
Bu konularda daha geniş bilgiye sahip olmak için Eloquent JavaScript, JavaScript: The Good Parts ve You Don’t Know JS kitaplarını okumanızı tavsiye ederim. Kurs olarak ise internette bunlarla ilgili baya bir eğitim var ama benim de yazıyı yazarken yararlandığım JavaScript: Understanding the Weird Parts kursunu öneririm. Kursta yukarıda saydığım tüm kavramlar net bir şekilde açıklanıyor. Ayrıca Oğuz Kılıç’ın bu linkten ulaşabileceğiniz tarayıcıların JavaScript’i nasıl yorumladığını açıklayan çok güzel ve detaylı bir yazısı mevcut.