← Kursa Dön
📄 Text · 30 min

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ır

Bu 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);     // ❌ ReferenceError

3. 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 belirlendi

Bu 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 koruyor

2. 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şıyor

Closure'ı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 deneme

Bu örnekte:

  • cagriSayisi ve sonResetZamani closure sayesinde korunuyor

  • Dışarıdan bu değişkenlere erişim yok — güvenli

  • Her rateLimiter() çağrısı bağımsız bir closure oluşturuyor

  • Fonksiyon 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: varundefined ile 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; var ile döngüde closure tuzağına düşmeyin, let kullanın