JAVASCRIPT’IN TARIHÇESI
Execution Context, Lexical Environment, Scope ve Clousure Anlamak
JS derinlerine inerek kodun JS tarafından bellekte nasıl işletildiğini anlamak için Execution Context (İşletilme bağlamı) ve Lexical Enviroment (Sözcük Çerçevesi), Scope ve Clousure’ danbahsediyor olacağım.
Javascript Tarihçesi yazıma ilk başlarken Neden ? var → let, const blog yazısını yazmıştım. O blog yazısının içerisinde şöyle bir kısım vardı.
Bu yazıdan sonra yazdığım yazılarda Pure Functions , IIFE , Data Types Immutable, Pass By Reference bahsettim. Bu kapsam da eksik kalan iki konu var
- Scoping , Hoisting, Lexical Scoping
- LHS, RHS(Left Hand, Right Hand)
Bu yazımda Lexical Environment ve Execution Context üzerinde yoğunlaşacağız.
JS Örnek Bir Kod
JS Örnek bir kod yazmak istediğimizde önce HTML içerisinde HTML elemanlarını (p, div vb..) tanımlayıp en sonda da script.js dosyasını load ederek JS bu DOM yapısı üzerinde çalışmasını sağlarız.
Burda her id elemana tek tek erişip kendisine tanımlanan kelimeleri ilgili elemana innerHTML olarak eşliyoruz.
JS Çalıştırılmasının Ana Akışı
- JS kaynak kodları → Parser tarafından işlenerek → Abstract Syntax Tree
- Abstract Syntax Tree → Interpreter tarafından işlenerek → ByteCode
- ByteCode üstünde çalıştırılan sistemin profiling bilgilerinide alarak → Optimizing Compiler işlenerek → Optimized Makine koduna dönüştürülür.
- Daha sonra çalıştırılan kod üzerinde de-optimize edilerek bytecode ve elde edilen optimization ve profiling verileri ile tekrardan
Syntax Parsing → Interprete → Compile
JS kodları işlendiğinde bunları Functions ve Variables olarak makine koduna çevirir ve adım adım ilerletir. İlerde bunu daha iyi analiz edeceğiz ama önce gelin Lexical Environment, Execution Context ve Object Model kavramlarını bir anlayalım ..
Temel Kavramlar
Lexical Environment
- JS kodundaki parçaların (Variable, Function, Blok) nerede olduğu önemlidir ?
- Kod parçacıkları neyin içerisinde , Veya çevresi ne ile sarılı ?
- Bu kapsama Lexical(Grammar) Environment yani (Sözcük Ortamı) denir.
- Bu ortam sizin hangi diğer sözcükler ile nasıl iletişim kurabileceğiniz konusunda, compiler karar vermesini sağlayacak. (var → function scope, let,const → block scope olması …)
Execution Context
- Lexical Environment fiziksel olarak tanımlaman alanların kod çalışırken hangi kapsamda olacağını belirten çalışma bağlamına Execution Context denir.
- Özetle kodlar çalışırken ilgili değişkenler , fonksiyon adresleri vs.. çalışan kod ile ilgili bağlamlar burda tutulur ve buradan erişilir.
Object Model
- JS Object’ler String key olacak şekilde Dictionary/Map gibi tutulur. Örneğin içerisinde x ve y değeri olan bir object tanımladığımızda Objenin her bir x ve y property için Value, Writable, Enumerable ve Configurable bilgilerini tutar. ( Bu konunun detayını Chrome JS Nasıl İşletir ? yazımı okumanızı öneririm)
- Object içerisinde key , value değerlerinde value → obje/array/primitif type olabilecek şekilde iç içe object yapıları oluşturabilir. Örneğin aşağıdaki Address objesinde olduğu gibi. Bu sayede çok karmaşık obje tanımlamarı yapabilirsiniz.
Execution Context Nasıl Çalışır ?
İster JS Tarayıcıda çalışsın, İster sunucu, ister local her ortamda JS Engine çalışma ortamına bir tane Global Execution Context oluşturur ve bu Global Scope içerisinden Tarayıcılar için WebAPI ye erişim imkanı sağlar. Bu ilk Global Execution Context içerisinde bir Global Object ve this nesnesi bulunur. Bunu JSEngine kendisi ilk JS çalıştırılacak ortamda ayağa kaltığı zaman yerleştirir.
Örneğin aşağıda Tarayıcıda çalışan JS kodları için Console this ve window Global Objeye karşılık gelir.
Peki aşağıdaki kod nasıl çalışıyor.
Adım 1. Hello World → a değişkenine ata…
Adım 2. b fonksiyonunu çağır ve ekrana Hello World yazdır.
Burda kodun sonuna geldiğimizde
a, b, this, this.a, this.b, window değelerine baktığımızda aşağıdaki
sonuçları görürüz.
Bu durumda Global Execution Context aynı zamanda bizim kodumuzda tutulmaya(Your Code) başladı. Outer Environment ise sonradan bahsedeceğimiz bir konu..
Hoisting Nedir?
Hoisting kod üzerinden anlatmak daha kolay o yüzden bende öyle başlıyorum Aşağıdaki örnekte
Adım 1: Hello World kelimesini → a atanır.
Adım 2: Ekrana Hello World yazdırılır.
Adım 3: b fonksiyonu çağrılır ve ekrana b yazdırılır.
Peki yukardaki örnekte 2 seçenek olabilirdi.
A durumu → console.log(a) a değişkenini tanımlamadığı için ilk baştan bir exception atabilirdi ama atmamış değeri undefined olarak ekrana yazmış . Bu nasıl olabilir diğer dillerde bu hata verirdi..
B durumu → bu daha da ilginç b() çağrımı sırasında ortada fonksiyon yok burda birde ekrana b yazmış. Bu daha da ilginç
A ve B durumu nasıl böyle çalışıyor ? JS Engine bu kodu nasıl algılıyor. İşte bu kısımda biraz daha derinlere inmek gerekiyor
Execution Context 2 Fazı Bulunur
Öncelike Execution Context (işleme bağlamının) 2 tane fazı bulunur. Bunlardan birisi Creation (Oluşturulma) diğeride Execution(Run) yani çalıştırılma fazı.
1. Creation Phase (Oluşturulma Fazı)
- Fonksiyonlar tümüyle bellek alanına konulur.
- Değişkenler de alınır ama bu aşamda undefined olarak atanır var x=undefined
- undefined bu değişkene henüz bir değer ataması yapılmadığı anlamına gelir.
2. Run Phase (Çalıştırılma Fazı)
- Çalıştırılması aşamasında kod satır satır işletilir.
- console.log(a) → undefined çünkü oluşturulma fazında bu değer bulunup değişkenin değeri olarak undefined atanmıştı.
- b() → fonksiyonu b yazıyor çünkü oluşturulma fazında bu fonksiyon bulunup referansı tutulmaya başlanmıştır. Bu sayede b fonksiyonu içerisinde değer ile birlikte ekrana yazdırılır.
Hoisting işte bu Creation(Oluşturma) fazında değişkenleri ve fonksiyonları yukarı taşıma özelliği sayesinde değişken ve fonksiyonlara çalıştırılan kodun ilerisinde olması durumunda bile kodun çalışabilmesi..
Peki çalıştırılma aşamasında kod nasıl çalıştırılır ?
Call Stack Nasıl Çalışır ?
- main() ile JS kodu çalıştırılıyor ve printSquare(4) fonksiyonu çağrılıyor
- main fonksiyonu kendi içerisindeki çalıştırılacak fonksiyonları sırası ile stack taşıyor. main → printSquare → square → multiply fonksiyonlarını stack yüklüyor
- LIFO (Son giren ilk çıkar mantığı ile son fonksiyona geldi ise artık bunları işleterek stack siler. Özetle kod blokları işletildikçe bu stack dolar ve boşalır..
- Stack üzerine konulan her bir eleman için yeni bir ExecutionContext oluşturulup bunlar bu stack eklenir.
Şimdi gelin bir ExecutionContext birbirlerine olan erişimlerine bakalım.
Scope
Örnek 1 Kod — Execution Context Run — (Variable Env)
Aşağıdaki örnekte iç içe birbirini çağıran fonksiyonlar var. a → b → c çağırıyor.
Bu durumda sıra ile gidersek önce
Adım 1: 1 değeri → x atanır. (Global Context x =1 değerindedir)
Adım 2: a fonksiyonu çağrılır bunun için A Context oluşturulup Stack üzerine atılır. A Context x=2 değerinde
Adım 3: a->b fonksiyonu çağırır. B Context oluşturulup Stack üzerine atılır B Context x=3 değerinde
Adım 4: b->c fonksiyonu çağırır. C Context oluşturulup Stack üzerine atılır C Context x=4 değerindeAdım 5: C fonksiyonu işletilir. Kendi Scope değer 4 olduğu için ekrana basar ve işi bittiği için Stack atılır.Adım 6: B fonksiyonu işletilir. Kendi Scope değer 3 olduğu için ekrana basar ve işi bittiği için Stack atılır.Adım 7: A fonksiyonu işletilir. Kendi Scope değer 2 olduğu için ekrana basar ve işi bittiği için Stack atılır.Adım 8: Global Context değer ekrana yazılır yani 1 ve kod exit exit eder.
Örnek 2 Kod — Execution Context Run — (Variable Env)
Burda a → b → c kodunu çağıralım. Ama bu sefer c fonksiyonu içerisinde x değişkeni bulunmasın. Bu durumda nasıl davranır. Neden aşağıdaki resimde olduğu gibi B context değeri değilde Global Context değeri okuyor ?
Yukarıdaki adımlar benzer şekilde işletilir. Fakat C contextinde değer yoksa kendi bir üst Scope yani Lexical(Sözcük) üzerindeki ExecutionContext değişkenlerine erişmeye başlar. c fonksiyonu nerde tanımlanmış Global alanda, ozaman Global Context alanına erişir.
Scope Nedir?
Özetle scope sözcük dizimi {} kısmında sizin değişkenlere ve fonksiyonlara erişebilme yetkinizdir.
- Örneğin Globalde çalışanlar b fonksiyonuna direk erişemez ama a fonksiyonuna direk erişebilir
- a fonksiyonu c fonksiyonuna direk erişemez b fonksiyonu erişebilir
- c fonksiyon ExecutionContext içerisinde bir x değeri bulamadığı durumda bir üst scope b executionContext sorar. Yoksa bir üst Scope yani Lexical Scope sorar.
Scope Chain Nedir ?
- Bu şekilde c kendi executationContext bulamaz ise bir üst Scope Context var mı
- c , b context ‘inde bulamadığı için bir üstüne sorar
- c, a context’inde de bulamadı bir üstüne sorar
- c global scope’ctaki x değerini =1 buldu onu kullanır
Bu üst Scope zinciri gezmeye Scope Chain Diyoruz.
Bu aşamada var , let , const tekrar hatırlamakta fayda var bunun için Neden ? var → let, const yazımı okuyunuz .
Closure
JS’de bir çok konunun temelini oluşturan diğer bir konuda Closure kavramıdır. Bu da Scope olayının farkı bir şekilde çalışması ile hem Callback temelini, hemde High Order Functions temelini oluşturur. Nasıl mı ?
Yine aşağıdaki örnek üzerinden anlatalım;
saySomething fonksiyonu bir fonksiyon ref dönüyor ve işini bitirip ExecutionContext siliniyor. Peki nasıl oluyorda return eden who parametresi alan fonksiyonu çağırdığımızda bu fonksiyon something değişkenindeki değeri hatırlayabiliyor ? Çünkü bunun işi bittiği için stack atılmış ve değişkeninde silinmiş olması gerekiyordu.
İşte JS yine burada biraz farklı şekilde düşünüyor. Stack ilgili ExecutionContext siline bile onu çevreleyen {} blockta bu değişkenlere ihtiyaç duyan fonksiyon bloğu var ise Closure yeneği sayesinde silinen ExecutionContext değerleri tutulmaya devam ediyor. Bu sayede aşağıdaki örnekte “Merhaba” değeri daha hala kullanılabiliyor oluyor.
Not: Biraz uzun bir yazı oldu ama JS temellerini anlamak için bu konuları anlamanız gerekiyor. Önerim kendinize bir kod deneme ortamı bulup bu konuları teker teker denemeniz olur 😄
Github Kod Örnekleri
Referanslar
- Javascript: Understanding the Weird Parts
Okumaya Devam Et 😃
Bu yazının devamı veya yazı grubundaki diğer yazılara erişmek için bu linke tıklayabilirsiniz.