JavaScript’te “Lexical Environment”, “Scope” ve “Scope Chain” Kavramları
Bu yazımızda, JavaScript’te bulunan “Lexical Environment”, “Scope” ve “Scope Chain” kavramları üzerine yoğunlaşacağız. Bu kavramlar, değişkenlerin, fonksiyonların ve diğer tanımlamaların erişilebilirliği ve yaşam döngüsü üzerinde derin bir anlayış kazanmamıza yardımcı olacağı için çok ama çok önemlidir.
“Lexical Environment”, “Scope” ve “Scope Chain” Kavramları
- “Lexical Environment” kavramı, bir fonksiyonun veya kod bloğunun değişkenlerini, fonksiyonlarını vb. sakladığı yerdir ve küme parantezlerinin (
{ }
) kullanıldığı her yerde otomatik olarak oluşturulur. - “Scope” (kapsam) kavramı, bir değişkenin veya fonksiyonun hangi bölgeden erişilebilir olduğunu tanımlar.
- “Scope Chain” (kapsam zinciri) kavramı, bir değişkeni veya fonksiyonu mevcut kapsamda bulamadığımızda nereye başvuracağımızı belirleyen yapıdır.
Tanımlar kafa karıştırıcı olabileceğinden ilgili kavramlarımızı daha iyi anlatabilmek adına uzay ve gezegenler metaforunu kullanacağım:
Öncelikle, “lexical environment” yapısını bir tür çevreleyici veya atmosfer olarak düşünmenizi istiyorum. Bu çevreleyici yapının sınırlarını, bir tür uzayın sınırları olarak kabul edelim. JavaScript’in tüm evrenini temsil eden en dıştaki yapı, en geniş “lexical environment” olarak düşünülebilir. Bu, tüm kodumuzun bulunduğu ana evreni temsil eder.
Bu büyük ve sınırsız evrenin içerisinde, farklı özelliklerde ve büyüklüklerde birçok gezegen bulunmaktadır. Her bir gezegenin kendi atmosferi vardır ve bu atmosferlerin de kendi içerisinde birer “lexical environment” olduğunu hayal edebilirsiniz.
Bu geniş JavaScript evrenindeki gezegenler, aslında kod bloğundan oluşmuş yapılardır. Başka bir deyişle, JavaScript’te küme parantezlerinin ({ }
) kullanıldığı her yerde, otomatik olarak bir “lexical environment” oluşturulur. Bu, her bir gezegenin kendi atmosferini oluşturması gibidir. Bu atmosferler, gezegenlerin – yani kod bloklarının – içerisindeki tüm değişkenleri, fonksiyonları ve diğer yapıları korur ve saklar.
Her bir gezegenin atmosferi içinde kalan yapılar da fonksiyon ya da kod bloğunu temsil ettiğini varsayalım. Yani gezegendeki dağlar, vadiler veya göller, o fonksiyon ya da kod bloğu içerisinde tanımlanmış değişkenleri, fonksiyonları ve diğer yapıları temsil etsin. İşte bu doğal yapıları “scope” yani kapsam olarak düşünebiliriz. Bir dağın zirvesindeki bir bitki veya taş, sadece o dağın zirvesinde bulunabilir. Eğer bu bitki veya taşı arıyorsanız, doğrudan o dağın zirvesine gitmeniz gerekir. Aynı şekilde, bir fonksiyon içerisinde tanımlı bir değişkeni sadece o fonksiyon içerisinden erişebiliriz.
Eğer bir dağın zirvesinde aradığınızı bulamazsanız, belki yanındaki diğer dağa veya vadiye bakmanız gerekebilir. Bu, bir fonksiyonun kapsamında aradığınızı bulamadığınızda, dış kapsamlara doğru bakma sürecini temsil eder. İşte bu, en yakından en uzağa, yani bir alt kümeden üst kümeye devam eden yolculuk, “scope chain” kavramının ne olduğunu anlatır. Aradığınız şeyi bulana kadar bir alt kümeden bir üst kümeye yolculuğunuz devam eder. Eğer tüm evrende, yani tüm “lexical environment”lerde aradığınızı bulamazsanız, bu, aradığınız şeyin bu evrende olmadığı, yani tanımlanmadığı anlamına gelir.
Görüleceği üzere “scope chain” (kapsam zinciri) konsepti, en içteki kapsamdan (örneğin bir fonksiyon içindeki kapsam) başlar ve gerektiğinde bir üst kapsama, daha sonra onun da üstüne doğru devam eder. Bu süreç, en dıştaki, genellikle global kapsama (en üst kapsam) kadar devam eder.
“Lexical Environment” Nedir?
“Lexical environment” (Sözcüksel Çevre), JavaScript’te kodun çalışma zamanında değişkenlerin, fonksiyonların ve diğer yapıların nasıl ve nerede saklandığını tanımlayan bir konsepttir. Temelde, her fonksiyon çalıştırıldığında veya bir kod bloğu (örneğin, bir if bloğu veya bir döngü) başlatıldığında, yeni bir “Lexical Environment” oluşturulur. Bu leksiksel çevre, o kapsamdaki tüm yerel değişkenleri, fonksiyonları ve mevcut scope chain’i saklar.
Bu yapı iki ana bileşenden oluşur:
- Environment Record: Mevcut kapsamdaki tüm değişkenleri, fonksiyonları ve diğer bağlam bilgilerini içerir.
- Outer Reference: Dıştaki (üstteki) sözcüksel çevreyi, yani bir üst kapsamı (eğer varsa) gösterir. Bu, “scope chain” konseptinin temelini oluşturur.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
let globalVariable = "Global"; function outerFunction() { let outerVariable = "Outer"; function innerFunction() { let innerVariable = "Inner"; console.log(innerVariable); // Inner console.log(outerVariable); // Outer console.log(globalVariable); // Global } innerFunction(); } outerFunction(); |
Kodumuzda, en başta globalVariable
adında global bir değişken tanımladık. Bu değişkenin değerini “Global” olarak atadık ve kodun en dışında yer aldığı için tüm fonksiyonlardan erişilebilir bir konumda bulundu.
Daha sonrasında, outerFunction
adında bir fonksiyon tanımladık. Bu fonksiyonun içerisinde, outerVariable
adında başka bir değişken daha tanımladık ve bu değişkenin değerini “Outer” olarak atadık. outerVariable
, outerFunction
fonksiyonunun sözcüksel çevresi içerisinde tanımlandığı için sadece bu fonksiyon ve içerisindeki, yani alt kümesi olan diğer fonksiyonlar tarafından erişilebilir oldu.
Bu fonksiyonun içinde, innerFunction
adında bir başka fonksiyon daha tanımladık. Bu iç fonksiyon, innerVariable
adında kendi değişkenini tanımladık ve bu değişkenin değerini “Inner” olarak atadık. innerFunction
, kendi sözcüksel çevresindeki değişkenlere, outerFunction
‘ın sözcüksel çevresindeki değişkenlere ve aynı zamanda global değişkenlere erişebildi. Bu nedenle, bu fonksiyon içerisinde üç farklı değişkenin değerini ekrana yazdırdı.
Son olarak, kodun en altında outerFunction
fonksiyonunu çağırdık. Bu çağrıyla birlikte, outerFunction
içerisinde tanımlı olan innerFunction
fonksiyonu da otomatik olarak çalıştırıldı. Bu sayede, üç değişkenin değeri sırasıyla ekrana basıldı.
“Scope” Nedir?
“Scope” ya da dilimize çevirirsek “kapsam”, JavaScript’te değişkenlerin, fonksiyonların ve nesnelerin hangi bölümde erişilebilir olduğunu tanımlayan bir konsepttir. Değişkenlerin nerede tanımlandığına göre erişilebilirliği ve yaşam süresi değişir. JavaScript’te iki ana tür kapsam vardır:
- Global Scope (Global Kapsam): Kodun en üst seviyesinde tanımlanan değişkenler ve fonksiyonlar global kapsamda bulunurlar ve kodun her yerinden erişilebilirler.
- Local Scope (Yerel Kapsam): Fonksiyonlar içinde tanımlanan değişkenler ve fonksiyonlar yerel kapsamdadır ve sadece o fonksiyonun içinden erişilebilirler.
Kodumuzda, en başta tanımlanan değişkenler ve fonksiyonlar global kapsamda yer alır. Bu, tüm fonksiyonlar ve iç içe fonksiyonlar tarafından erişilebilir olduğu anlamına gelir. Ancak, bir fonksiyon içerisinde tanımlanan değişkenler ve fonksiyonlar sadece o fonksiyonun kapsamında erişilebilir. Eğer bir fonksiyon içerisinde başka bir fonksiyon tanımlanırsa, içteki fonksiyon dıştaki fonksiyonun kapsamında tanımlı değişkenlere erişebilir. Ancak tersi doğru değildir; dıştaki fonksiyon, içteki fonksiyonun kapsamında tanımlı değişkenlere erişemez. Örneğin:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
let globalVariable = "Global Değişken"; function firstFunction() { let firstVariable = "Birinci Fonksiyon Değişkeni"; function secondFunction() { let secondVariable = "İkinci Fonksiyon Değişkeni"; console.log(secondVariable); // İkinci Fonksiyon Değişkeni console.log(firstVariable); // Birinci Fonksiyon Değişkeni console.log(globalVariable); // Global Değişken } secondFunction(); // console.log(secondVariable); // Hata! secondVariable bu kapsamda tanımlı değil. } firstFunction(); |
Bu kodda, globalVariable
adında global bir değişken tanımladık. Sonrasında, firstFunction
adında bir fonksiyon oluşturduk. Bu fonksiyonun içerisinde, firstVariable
adında bir değişken daha tanımladık. Aynı zamanda, bu fonksiyonun içerisinde secondFunction
adında başka bir fonksiyon daha tanımladık. secondFunction
fonksiyonu, kendi kapsamında tanımlı secondVariable
değişkenine, bir üst kapsamda olan firstFunction
fonksiyonunun kapsamında tanımlı firstVariable
değişkenine ve global kapsamda tanımlı globalVariable
değişkenine erişebilir.
Ancak firstFunction
fonksiyonu, sadece kendi kapsamında ve global kapsamda tanımlı değişkenlere erişebilir; secondFunction
fonksiyonunun kapsamında tanımlı değişkenlere erişemez. Bu nedenle, secondVariable
değişkenine firstFunction
fonksiyonunun içerisinden erişmeye çalıştığımızda bir hata alırız.
“Scope” hakkında daha detaylı bir bilgi için JavaScript’teki scope ve closure yazısını inceleyebilirisiniz.
“Scope Chain” Nedir?
JavaScript’teki “lexical environment” ve “scope” kavramlarını anladıysanız “scope chain” kavramını anlamak oldukça kolay olacaktır.
Değişkenlerin erişilebilirliğini belirlemek için kullanılan “scope chain”, bir değişkenin erişilebilirliğinin, o değişkenin tanımlandığı scope’tan (kapsamdan) başlayarak yukarı doğru hareket ederek kontrol edildiğini anlatır.
Yani “scope chain”, JavaScript’te bir değişkenin ya da fonksiyonun nerede aranacağını belirleyen bir mekanizmadır. Basitçe ifade edersek, bir değişkene ya da fonksiyona erişmeye çalıştığınızda, JavaScript motoru öncelikle bulunduğunuz kapsamda bu değişkeni ya da fonksiyonu arar. Eğer bulamazsa, bir üst kapsama geçer ve aramaya devam eder. Bu süreç, aranan değişken ya da fonksiyon bulunana kadar ya da en dıştaki global kapsama kadar devam eder. İşte bu kapsamlar arasındaki bağlantıya “Scope chain” denir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
let globalVar = "Global Değişken"; function firstFunc() { let firstVar = "Birinci Fonksiyon Değişkeni"; function secondFunc() { let secondVar = "İkinci Fonksiyon Değişkeni"; console.log(secondVar); // İkinci Fonksiyon Değişkeni console.log(firstVar); // Birinci Fonksiyon Değişkeni console.log(globalVar); // Global Değişken } secondFunc(); } firstFunc(); |
Bu kodda, secondFunc
fonksiyonu içerisinden secondVar
adlı değişkene doğrudan erişebiliriz çünkü bu değişken aynı kapsamda tanımlıdır. Eğer secondFunc
içerisinde tanımlı olmayan bir değişkeni ararsak, JavaScript motoru bu değişkeni bulmak için bir üst kapsama, yani firstFunc
‘a gider. Bu yüzden firstVar
adlı değişkene de erişebiliriz. Eğer bu kapsamda da aradığımızı bulamazsak, bu sefer global kapsama gidilir ve globalVar
adlı değişkene erişebiliriz.
Bu örnek, bir değişken ya da fonksiyon arama sürecinin nasıl işlediğini ve kapsamlar arasındaki bu bağlantının, yani “scope chain”in nasıl oluştuğunu göstermektedir.
Birbaşka örnek olarak aşağıdaki örnek, scope chain’in her bir fonksiyonu nasıl birbirine bağladığını gösterecektir:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// Scope chain function sayMyName() { var a = "a"; console.log(b, c); // returns error return function findName() { var b = "b"; console.log(a); // a console.log(c); // returns error return function printName() { var c = "c"; console.log(a, b); // a, b }; }; } sayMyName()()(); // |
Yukarıdaki örnek, iç içe yerleştirilmiş üç fonksiyonu kullanarak, değişkenlerin scope chain içinde nasıl hareket ettiğini gösterir.
İlk olarak sayMyName
adlı bir fonksiyon tanımlanır. Bu fonksiyon, içinde a
adlı bir değişken tanımlar ve bir console.log
ifadesi kullanarak, b
ve c
değişkenlerine erişmeye çalışır. Ancak, b
ve c
henüz tanımlanmadığından hata verir.
Daha sonra sayMyName
fonksiyonu içinde, findName
adlı bir başka fonksiyon tanımlanır. Bu fonksiyon, içinde b
adlı bir değişken tanımlar ve a
değişkenine erişir. Ancak, kendi kapsamı içinde c
henüz tanımlanmadığından, console.log
ifadesi hata verir.
Son olarak, findName
fonksiyonu içinde, printName
adlı bir başka fonksiyon tanımlanır. Bu fonksiyon, içinde c
adlı bir değişken tanımlar. Scope chain sayesinde hem a
hem de b
değişkenlerine erişir. Kabaca diyebiliriz ki, çocuklar atanın özelliklerini miras alabilir ama ata, çocuklarının özelliklerini miras alamaz.
Bu örnek, iç içe yerleştirilmiş fonksiyonların scope chain içinde nasıl hareket ettiğini ve değişkenlerin hangi kapsamlardan erişilebilir olduğunu gösterir. Değişkenlerin, tanımlandıkları kapsamdan başlayarak yukarı doğru hareket eden scope chain üzerinde arandığına dikkat edin. Ayrıca, var
anahtar sözcüğü ile tanımlanan değişkenlerin, deklare edildikleri kapsamın başında hoisted olduğunu unutmayın.