Scope, Hoisting ve Closure
Giriş — Görünmezin Mekaniği
JavaScript'te yazdığınız her değişken ve fonksiyon, belirli bir kapsam (scope) içinde yaşar. Hangi değişkene nereden erişebileceğinizi, aynı isimli değişkenlerin birbirine karışıp karışmayacağını ve kodun çalışma sırasının neden bazen beklediğinizden farklı olduğunu anlamak için bu konuyu bilmeniz gerekir.
Scope, hoisting ve closure kavramları JavaScript'in "görünmez mekaniği"dir — kodun yüzeyinde görmezsiniz ama her satırın davranışını belirlerler. Bu kavramları anlamadan ileri seviye JavaScript yazmak mümkün değildir. İş mülakatlarında da en çok sorulan konulardandır.
Analoji: Scope'u bir binanın katları olarak düşünün. Zemin katta (global scope) herkes her şeye erişebilir. Üst katlardaki daireler (fonksiyon/blok scope) kendi odalarını görür ama diğer dairelerin odalarını göremez. Ancak herkes zemin kattaki ortak alanlara erişebilir. Closure ise, bir daireden taşındıktan sonra bile o dairenin anahtarını elinizde tutmanızdır.
Execution Context (Çalıştırma Bağlamı)
JavaScript kodu çalıştırılmadan önce, motor bir execution context (çalıştırma bağlamı) oluşturur. Bu, kodun çalışması için gereken tüm ortamı hazırlar.
İki Aşamalı Süreç
// JavaScript motoru kodu iki aşamada işler:
// AŞAMA 1: Creation Phase (Oluşturma Aşaması)
// - Değişken ve fonksiyon tanımlamaları bellekte yer ayrılır
// - var → undefined ile başlatılır
// - let/const → başlatılmaz (TDZ)
// - function declaration → tamamen yüklenir
// AŞAMA 2: Execution Phase (Çalıştırma Aşaması)
// - Kod satır satır çalıştırılır
// - Değişkenlere değerler atanırBu iki aşamalı süreç, hoisting davranışının temelini oluşturur.
Call Stack (Çağrı Yığını)
JavaScript tek iş parçacıklıdır — aynı anda sadece bir şey çalışır. Hangi fonksiyonun şu an çalıştığını call stack takip eder:
function ucuncu() {
console.log("Üçüncü fonksiyon");
// Call stack: [global, birinci, ikinci, ucuncu]
}
function ikinci() {
console.log("İkinci fonksiyon");
ucuncu();
// ucuncu bitti, call stack: [global, birinci, ikinci]
}
function birinci() {
console.log("Birinci fonksiyon");
ikinci();
// ikinci bitti, call stack: [global, birinci]
}
birinci();
// birinci bitti, call stack: [global]Call Stack Görselleştirmesi:
┌──────────┐
│ ucuncu() │ ← En son giren, ilk çıkar (LIFO)
├──────────┤
│ ikinci() │
├──────────┤
│ birinci()│
├──────────┤
│ global │ ← İlk oluşturulan
└──────────┘💡 İpucu: "Stack overflow" hatası, fonksiyonların birbirini sonsuza kadar çağırmasından (veya kendini çağırmasından — recursive) kaynaklanır. Call stack dolunca tarayıcı "Maximum call stack size exceeded" hatası fırlatır.
Scope (Kapsam)
Scope, bir değişkenin erişilebilir olduğu alanı tanımlar. JavaScript'te üç tür scope vardır.
1. Global Scope
Herhangi bir fonksiyon veya bloğun dışında tanımlanan değişkenler global scope'tadır — her yerden erişilebilir:
// Global scope'ta tanımlanan değişkenler
let globalDegisken = "Herkese açık";
const SITE_ADI = "JavaScript Kursu";
function fonksiyonum() {
console.log(globalDegisken); // ✅ Erişilebilir
console.log(SITE_ADI); // ✅ Erişilebilir
}
if (true) {
console.log(globalDegisken); // ✅ Erişilebilir
}
fonksiyonum();⚠️ Dikkat: Global scope'u mümkün olduğunca az kullanın! Global değişkenler:
İsim çakışmalarına yol açar (farklı dosyalardaki aynı isimli değişkenler birbirini ezer)
Kodun hangi parçasının hangi değişkeni değiştirdiğini takip etmeyi zorlaştırır
Test edilebilirliği düşürür
// ❌ Kötü — global scope kirliliği
var kullaniciSayisi = 0;
var aktifKullanicilar = [];
// ✅ İyi — bir nesne/modül içinde kapsüle
const uygulama = {
kullaniciSayisi: 0,
aktifKullanicilar: []
};2. Function Scope (Fonksiyon Kapsamı)
Bir fonksiyonun içinde tanımlanan değişkenler, sadece o fonksiyonun içinden erişilebilir:
function hesapla() {
let sonuc = 42; // Function scope
var gecici = "temp"; // Function scope (var da!)
const PI = 3.14; // Function scope
console.log(sonuc); // ✅ 42
}
hesapla();
// console.log(sonuc); // ❌ ReferenceError
// console.log(gecici); // ❌ ReferenceError
// console.log(PI); // ❌ ReferenceError3. Block Scope (Blok Kapsamı) — ES6
let ve const ile tanımlanan değişkenler, tanımlandıkları bloğun ({}) dışından erişilemez. var ise block scope'a sahip değildir:
if (true) {
let blockLet = "let ile tanımlı";
const blockConst = "const ile tanımlı";
var blockVar = "var ile tanımlı";
}
// console.log(blockLet); // ❌ ReferenceError
// console.log(blockConst); // ❌ ReferenceError
console.log(blockVar); // ✅ "var ile tanımlı" — var block scope'a sahip DEĞİL!
// for döngüsünde en net fark
for (let i = 0; i < 3; i++) {
// i sadece bu blokta yaşar
}
// console.log(i); // ❌ ReferenceError
for (var j = 0; j < 3; j++) {
// j fonksiyon scope'ta (veya global'de) yaşar
}
console.log(j); // 3 — blok dışında erişilebilir!Scope Chain (Kapsam Zinciri)
Bir değişkene erişilmeye çalışıldığında, JavaScript motoru önce mevcut scope'ta arar, bulamazsa bir üst scope'a bakar, ta ki global scope'a ulaşana kadar. Buna scope chain denir:
let seviye1 = "Global";
function dis() {
let seviye2 = "Dış fonksiyon";
function orta() {
let seviye3 = "Orta fonksiyon";
function ic() {
let seviye4 = "İç fonksiyon";
// İç fonksiyon TÜM dış scope'lara erişebilir
console.log(seviye4); // ✅ Kendi scope'u
console.log(seviye3); // ✅ Bir üst scope
console.log(seviye2); // ✅ İki üst scope
console.log(seviye1); // ✅ Global scope
}
ic();
// console.log(seviye4); // ❌ İç scope'a erişilemez
}
orta();
}
dis();Scope Chain Görselleştirmesi:
Global Scope: seviye1
↑
dis() Scope: seviye2
↑
orta() Scope: seviye3
↑
ic() Scope: seviye4
Arama yönü: ↑ (içten dışa, aşağıdan yukarıya)Lexical Scope (Sözcüksel Kapsam)
JavaScript lexical scope kullanır — yani bir fonksiyonun scope'u, nerede çağrıldığına değil, nerede tanımlandığına göre belirlenir:
let x = 10;
function yazdir() {
console.log(x); // x nereden gelecek?
}
function calistir() {
let x = 20; // Bu x, yazdir'ın scope chain'inde DEĞİL
yazdir(); // yazdir() burada çağrılıyor ama...
}
calistir(); // 10 yazdırır, 20 DEĞİL!
// Çünkü yazdir() global scope'ta tanımlandı
// ve scope chain'i tanımlandığı yere göre belirlendiBu kavram closure'ı anlamak için kritiktir.
Hoisting (Yukarı Çekme)
Hoisting, JavaScript motorunun creation phase'de değişken ve fonksiyon tanımlamalarını scope'un başına "çekmesi"dir.
var Hoisting
console.log(isim); // undefined — tanımlama hoisting yapıldı, atama yapılmadı
var isim = "Ali";
console.log(isim); // "Ali"
// Motor bunu şöyle yorumlar:
// var isim; ← hoisting: tanımlama başa çekildi
// console.log(isim); ← undefined
// isim = "Ali"; ← atama yerinde kaldı
// console.log(isim); ← "Ali"let/const ve TDZ (Temporal Dead Zone)
// let ve const da teknik olarak hoisting yapılır
// AMA başlatılmaz — TDZ'de kalırlar
// console.log(a); // ❌ ReferenceError: Cannot access 'a' before initialization
let a = 10;
// TDZ (Temporal Dead Zone) — tanımlama ile atama arasındaki "ölü bölge"
{
// ===== TDZ başlangıcı =====
// console.log(b); // ❌ ReferenceError
// ===== TDZ sonu =====
let b = 20; // Burada TDZ sona erer
console.log(b); // ✅ 20
}Function Declaration Hoisting
// Function declaration tamamen hoisting yapılır
selamla(); // ✅ "Merhaba!" — tanımdan önce çağrılabilir
function selamla() {
console.log("Merhaba!");
}
// Function expression hoisting YAPMAZ
// hesapla(); // ❌ ReferenceError veya TypeError
const hesapla = function() {
return 42;
};Hoisting Tuzakları
// Tuzak 1: Fonksiyon içindeki var
var x = 1;
function test() {
console.log(x); // undefined — 1 DEĞİL!
var x = 2; // Bu x, fonksiyon scope'unda hoisting yapıldı
console.log(x); // 2
}
test();
console.log(x); // 1 — global x değişmedi
// Motor bunu şöyle görür:
// function test() {
// var x; ← hoisting
// console.log(x); ← undefined
// x = 2;
// console.log(x); ← 2
// }
// Tuzak 2: Döngüde var ve setTimeout
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 3, 3, 3 — hepsi 3!
}, 100);
}
// var function scope'ta, döngü bitince i = 3
// setTimeout çalıştığında hepsi aynı i'ye bakıyor
// ✅ Çözüm: let kullan (block scope)
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 0, 1, 2 — doğru!
}, 100);
}
// let her iterasyonda yeni bir i oluşturur⚠️ Dikkat: Hoisting'e güvenmeyin — değişkenleri ve fonksiyonları kullanmadan önce tanımlayın. let ve const kullanmak, hoisting'in neden olduğu hataların çoğunu otomatik olarak önler (TDZ sayesinde).
Closure (Kapanış)
Closure, JavaScript'in en güçlü ve en çok yanlış anlaşılan kavramlarından biridir. Ama aslında çok basit bir fikre dayanır.
Closure Nedir?
Bir fonksiyon, tanımlandığı scope'taki değişkenlere — o scope artık "aktif" olmasa bile — erişmeye devam edebilir. Buna closure denir.
function sayacOlustur() {
let sayac = 0; // Bu değişken fonksiyon bitince normalde yok olurdu
return function() {
sayac++;
return sayac;
};
}
const artir = sayacOlustur();
// sayacOlustur() bitti, ama sayac hâlâ yaşıyor!
console.log(artir()); // 1
console.log(artir()); // 2
console.log(artir()); // 3
// sayac değişkeni hâlâ erişilebilir — bu closure!Analoji: Bir restorandan taşındığınızı düşünün, ama aşçı (iç fonksiyon) restoranın tarif defterini (dış scope'un değişkenleri) yanına almış. Restoran artık yok ama aşçı o tarifleri hâlâ kullanabiliyor. Closure, fonksiyonun "doğduğu ortamın anılarını" taşımasıdır.
Closure Nasıl Çalışır?
function disaridakiFonksiyon(x) {
// x bu fonksiyonun parametresi — scope'unda yaşar
function iceridekiFonksiyon(y) {
// y kendi parametresi
// x'e scope chain üzerinden erişebilir (closure)
return x + y;
}
return iceridekiFonksiyon;
}
const topla5 = disaridakiFonksiyon(5);
// topla5 artık iceridekiFonksiyon'dur ve x=5 değerini "hatırlar"
console.log(topla5(3)); // 8 (5 + 3)
console.log(topla5(10)); // 15 (5 + 10)
const topla100 = disaridakiFonksiyon(100);
console.log(topla100(1)); // 101 (100 + 1)Closure'ın Pratik Kullanımları
1. Veri Gizleme (Data Privacy / Encapsulation)
function bankaHesabi(ilkBakiye) {
let bakiye = ilkBakiye; // "Özel" değişken — dışarıdan erişilemez
return {
paraCek(miktar) {
if (miktar > bakiye) {
return "Yetersiz bakiye!";
}
bakiye -= miktar;
return `${miktar} TL çekildi. Kalan: ${bakiye} TL`;
},
paraYatir(miktar) {
if (miktar <= 0) {
return "Geçersiz miktar!";
}
bakiye += miktar;
return `${miktar} TL yatırıldı. Toplam: ${bakiye} TL`;
},
bakiyeGor() {
return `Bakiye: ${bakiye} TL`;
}
};
}
const hesap = bankaHesabi(1000);
console.log(hesap.bakiyeGor()); // "Bakiye: 1000 TL"
console.log(hesap.paraCek(200)); // "200 TL çekildi. Kalan: 800 TL"
console.log(hesap.paraYatir(500)); // "500 TL yatırıldı. Toplam: 1300 TL"
// Doğrudan bakiye'ye erişim YOK — güvenli!
// console.log(hesap.bakiye); // undefined — closure koruyor2. Fonksiyon Fabrikası (Function Factory)
// Çarpan fonksiyonları oluşturucu
function carpanOlustur(carpan) {
return (sayi) => sayi * carpan;
}
const ikiKati = carpanOlustur(2);
const ucKati = carpanOlustur(3);
const yuzdeOn = carpanOlustur(0.1);
console.log(ikiKati(50)); // 100
console.log(ucKati(50)); // 150
console.log(yuzdeOn(250)); // 25
// URL oluşturucu
function apiEndpoint(baseUrl) {
return (path) => `${baseUrl}${path}`;
}
const api = apiEndpoint("https://api.example.com");
console.log(api("/users")); // "https://api.example.com/users"
console.log(api("/products")); // "https://api.example.com/products"
const testApi = apiEndpoint("http://localhost:3000");
console.log(testApi("/users")); // "http://localhost:3000/users"3. Memoization (Sonuç Önbellekleme)
function memoize(fn) {
const cache = {}; // Closure ile korunan önbellek
return function(...args) {
const anahtar = JSON.stringify(args);
if (cache[anahtar] !== undefined) {
console.log(`Cache'ten döndü: ${anahtar}`);
return cache[anahtar];
}
console.log(`Hesaplanıyor: ${anahtar}`);
const sonuc = fn(...args);
cache[anahtar] = sonuc;
return sonuc;
};
}
// Ağır bir hesaplama simülasyonu
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const hizliFib = memoize(fibonacci);
console.log(hizliFib(30)); // Hesaplanıyor — ilk çağrı
console.log(hizliFib(30)); // Cache'ten — anında döner!4. Event Handler'da Durum Yönetimi
function tikSayaci(butonId) {
let sayac = 0; // Her buton kendi sayacını "hatırlar"
return function() {
sayac++;
console.log(`${butonId} butonu ${sayac} kez tıklandı`);
};
}
// Her buton bağımsız bir closure'a sahip
const buton1Handler = tikSayaci("Beğen");
const buton2Handler = tikSayaci("Paylaş");
buton1Handler(); // "Beğen butonu 1 kez tıklandı"
buton1Handler(); // "Beğen butonu 2 kez tıklandı"
buton2Handler(); // "Paylaş butonu 1 kez tıklandı"
buton1Handler(); // "Beğen butonu 3 kez tıklandı"
// İki sayaç birbirinden bağımsız — her biri kendi closure'unda yaşıyorClosure'ın Yaygın Tuzakları
Tuzak: Döngüde closure ve var
Bu, JavaScript'teki en ünlü closure tuzağıdır:
// ❌ Klasik hata — var ile döngüde closure
function butonlarOlustur() {
var butonlar = [];
for (var i = 0; i < 5; i++) {
butonlar.push(function() {
console.log(`Buton ${i} tıklandı`);
});
}
return butonlar;
}
const btns = butonlarOlustur();
btns[0](); // "Buton 5 tıklandı" — 0 bekleniyordu!
btns[1](); // "Buton 5 tıklandı" — 1 bekleniyordu!
btns[2](); // "Buton 5 tıklandı" — 2 bekleniyordu!
// Hepsi 5! Çünkü var function scope'ta tek bir i var
// ve döngü bittiğinde i = 5
// ✅ Çözüm 1: let kullan (en basit)
function butonlarOlusturDoğru() {
const butonlar = [];
for (let i = 0; i < 5; i++) {
butonlar.push(function() {
console.log(`Buton ${i} tıklandı`);
});
}
return butonlar;
}
const btns2 = butonlarOlusturDoğru();
btns2[0](); // "Buton 0 tıklandı" ✅
btns2[3](); // "Buton 3 tıklandı" ✅
// ✅ Çözüm 2: IIFE ile (eski kodlarda görebilirsiniz)
function butonlarOlusturIIFE() {
var butonlar = [];
for (var i = 0; i < 5; i++) {
butonlar.push((function(index) {
return function() {
console.log(`Buton ${index} tıklandı`);
};
})(i)); // i'nin o anki değerini IIFE'ye geçir
}
return butonlar;
}Gerçek Dünya Örneği: Rate Limiter
Scope, closure ve higher-order functions'ı birleştiren gerçekçi bir örnek:
// API çağrılarını sınırlayan rate limiter
function rateLimiter(maxCagri, sureSaniye) {
let cagriSayisi = 0; // Closure ile korunan durum
let sonResetZamani = Date.now();
return function(fonksiyonAdi) {
const simdi = Date.now();
const gecenSure = (simdi - sonResetZamani) / 1000;
// Süre dolduysa sayacı sıfırla
if (gecenSure >= sureSaniye) {
cagriSayisi = 0;
sonResetZamani = simdi;
}
// Limit kontrolü
if (cagriSayisi >= maxCagri) {
const kalanSure = Math.ceil(sureSaniye - gecenSure);
console.log(`⛔ Rate limit aşıldı! ${kalanSure}s bekleyin.`);
return false;
}
// Çağrıya izin ver
cagriSayisi++;
console.log(`✅ ${fonksiyonAdi} çağrıldı (${cagriSayisi}/${maxCagri})`);
return true;
};
}
// Dakikada max 5 çağrıya izin ver
const apiLimiter = rateLimiter(5, 60);
apiLimiter("kullanicilariGetir"); // ✅ (1/5)
apiLimiter("siparisleriGetir"); // ✅ (2/5)
apiLimiter("urunleriGetir"); // ✅ (3/5)
apiLimiter("raporOlustur"); // ✅ (4/5)
apiLimiter("bildirimGonder"); // ✅ (5/5)
apiLimiter("fazlaCagri"); // ⛔ Rate limit aşıldı!
// Her rateLimiter çağrısı bağımsız bir closure oluşturur
const loginLimiter = rateLimiter(3, 300); // 5 dk'da 3 denemeBu örnekte:
cagriSayisivesonResetZamaniclosure sayesinde korunuyorDışarıdan bu değişkenlere erişim yok — güvenli
Her
rateLimiter()çağrısı bağımsız bir closure oluşturuyorFonksiyon fabrikası pattern'ı ile farklı limitler oluşturulabiliyor
Scope ve Closure Hata Örnekleri
// Hata 1: Global scope'u kirletme
// ❌
for (var i = 0; i < 10; i++) { /* ... */ }
console.log(i); // 10 — global scope'a sızdı!
// ✅
for (let i = 0; i < 10; i++) { /* ... */ }
// i erişilemez — blok scope'ta kaldı
// Hata 2: Closure'da yanlışlıkla referans paylaşımı
// ❌
function handler() {
var callbacks = [];
for (var i = 0; i < 3; i++) {
callbacks.push(() => i); // Hepsi aynı i'ye referans
}
return callbacks;
}
console.log(handler().map(fn => fn())); // [3, 3, 3]
// ✅
function handlerFixed() {
const callbacks = [];
for (let i = 0; i < 3; i++) {
callbacks.push(() => i); // Her biri kendi i'sine sahip
}
return callbacks;
}
console.log(handlerFixed().map(fn => fn())); // [0, 1, 2]Garbage Collection ve Closure
Closure'lar bellek yönetimi açısından dikkat gerektirir. Bir closure, referans verdiği dış değişkenleri bellekte tutar — çöp toplayıcı (garbage collector) onları temizleyemez:
// ❌ Bellek sızıntısı riski
function buyukVeriIsle() {
const buyukDizi = new Array(1000000).fill("veri"); // ~8MB
return function() {
// buyukDizi'nin tamamı bellekte kalır
// Sadece uzunluğuna ihtiyacımız olsa bile!
return buyukDizi.length;
};
}
const fn = buyukVeriIsle(); // buyukDizi bellekte kalmaya devam eder
// ✅ Sadece ihtiyacınız olanı tutun
function buyukVeriIsleIyi() {
const buyukDizi = new Array(1000000).fill("veri");
const uzunluk = buyukDizi.length; // Sadece ihtiyacımız olan bilgi
return function() {
return uzunluk; // buyukDizi artık garbage collect edilebilir
};
}💡 İpucu: Closure'larda büyük veri yapılarına gereksiz referans tutmamaya dikkat edin. İhtiyacınız olan değeri closure oluşturmadan önce çıkarın ve sadece onu tutun.
Özet
🔹 JavaScript lexical scope kullanır — scope, fonksiyonun nerede çağrıldığına değil, nerede tanımlandığına göre belirlenir
🔹 Scope chain, iç scope'tan dış scope'a doğru arama yapar — iç scope dıştakine erişebilir, dış scope içtekine erişemez
🔹 Hoisting:
var→undefinedile başlatılır,let/const→ TDZ'de kalır (hata verir), function declaration → tamamen yüklenir🔹 Closure, bir fonksiyonun tanımlandığı scope'taki değişkenlere — o scope artık aktif olmasa bile — erişmeye devam etmesidir
🔹 Closure'ın pratik kullanımları: veri gizleme (privacy), fonksiyon fabrikası, memoization, event handler'da durum yönetimi
🔹 Closure bellek tutar — büyük veri yapılarına gereksiz referans tutmamaya dikkat edin;
varile döngüde closure tuzağına düşmeyin,letkullanın
AI Asistan
Sorularını yanıtlamaya hazır