Mert Özen Her Satırda Daha İleri
Frontend JavaScript İleri — #06

JavaScript Iterator ve Generator: Symbol.iterator, yield ve Iterator Helper'lar

Mert Özen 3 Kas 2026 8 dk 5 görüntülenme
JavaScript Iterator ve Generator: Symbol.iterator, yield ve Iterator Helper'lar

Iterable ve iterator protokolünü, Symbol.iterator ile kendi iterable'ını yazmayı, generator ve yield'in nasıl duraklayıp devam ettiğini, sonsuz dizileri, ES2025 iterator helper'larını ve ES2026'nın Iterator.concat'ini görüyoruz.

Sürüm notu: Bu yazıdaki kodlar ES2026 ve güncel tarayıcılar (Chrome, Edge, Firefox, Safari) içindir.

Önce Tahmin Et

Yıldızlı bir fonksiyon, iki yield ve birkaç console.log. Bu kod konsola hangi sırayla ne yazdırır?

function* numbers() {
  console.log("started");
  yield 1;
  yield 2;
}

const it = numbers();
console.log("ready");
console.log(it.next());
console.log([...it]);

Okumaya devam etmeden önce bir tahmin yap. Özellikle iki şeyi düşün: "started" ne zaman yazılır, ve son satırdaki array'de kaç element olur?

İki Protokol: Iterable ve Iterator

Daha önce for...of ile array'leri, string'leri, Map ve Set'leri dolaştık. Spread, array destructuring ve Array.from da aynı yapıları kabul ediyordu. Bunların hepsi tek bir sözleşmeye dayanır ve o sözleşmenin iki parçası var.

Iterable, Symbol.iterator anahtarında bir method taşıyan object'tir. Bu method çağrıldığında bir iterator döndürür. Iterator ise next() method'u olan bir object'tir; her çağrıda { value, done } şeklinde bir sonuç verir. done true olduğunda iş bitmiştir. Bir banka şubesindeki sıra makinesini düşün: makinenin kendisi iterable'dır, her müşteri için yeni bir sıra başlatabilir. Elindeki fiş destesi iterator'dır; her basışta bir sonraki numarayı verir ve deste bitince "kalmadı" der.

Protokolü elle çalıştırmak, for...of'un perde arkasında ne yaptığını gösterir. Konsolda dene:

const letters = ["a", "b"];
const iterator = letters[Symbol.iterator]();

console.log(iterator.next()); // { value: "a", done: false }
console.log(iterator.next()); // { value: "b", done: false }
console.log(iterator.next()); // { value: undefined, done: true }

for...of tam olarak bunu yapar: Symbol.iterator'ı çağırır, done true olana kadar next()'i çağırır ve her value'yu döngü değişkenine verir. Düz object'ler bu protokolü uygulamaz; for (const x of { a: 1 }) yazarsan object'in iterable olmadığını söyleyen bir TypeError alırsın.

Kendi Iterable'ını Yazmak

Protokol basit olduğu için herhangi bir object'i iterable yapabilirsin. Bir sayı aralığı tutan object'i for...of ile dolaşılabilir hâle getirelim:

const range = {
  from: 1,
  to: 5,
  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;
    return {
      next() {
        if (current <= last) {
          return { value: current++, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
};

console.log([...range]); // [1, 2, 3, 4, 5]
for (const n of range) console.log(n);

Dikkat et, range object'inin kendisi saymıyor. Her Symbol.iterator çağrısı kendi current değişkenine sahip yeni bir iterator üretiyor. Bu yüzden range'i istediğin kadar dolaşabilirsin; her döngü baştan başlar. İstersen iterator'a bir return() method'u da ekleyebilirsin: döngü break ile ya da bir hatayla erken biterse for...of onu çağırır ve temizlik yapma şansı verir.

Generator Fonksiyonları: function* ve yield

Yukarıdaki kod çalışıyor ama biraz törensel: bir object, içinde bir method, o method'un döndürdüğü bir object, onun içinde bir method daha. Generator'lar aynı işi çok daha doğal yazmanı sağlar:

const range2 = {
  from: 1,
  to: 5,
  *[Symbol.iterator]() {
    for (let n = this.from; n <= this.to; n++) {
      yield n;
    }
  }
};

console.log([...range2]); // [1, 2, 3, 4, 5]

Generator fonksiyonu function* ile tanımlanır. Onu çağırmak gövdeyi çalıştırmaz; duraklatılmış bir generator object'i döndürür. Her next() çağrısında gövde bir sonraki yield'e kadar çalışır, yield'in yanındaki değeri teslim eder ve orada durur. Motor bu sırada fonksiyonun bütün yerel değişkenlerini ve nerede kaldığını saklar; bir sonraki next() tam o noktadan devam eder. Normal bir fonksiyon bir kez girip bir kez çıkar. Generator ise kaldığı yeri hatırlayan bir kitap ayracı gibidir.

Şimdi baştaki soruya dönelim. Çıktı sırası şöyle: "ready", "started", { value: 1, done: false } ve [2]. numbers() çağrısı gövdeyi hiç çalıştırmadı, bu yüzden önce "ready" yazıldı. İlk next() gövdeyi başlattı, "started" yazıldı ve ilk yield'de durdu. Spread ise iterator'ı kaldığı yerden sonuna kadar tüketti; geriye sadece 2 kalmıştı. Generator object'i hem iterator hem de iterable'dır, Symbol.iterator'ı kendisini döndürür; spread'in onu kabul etmesi bu yüzden.

Generator'ların iki kullanışlı özelliği daha var. yield*, başka bir iterable'ın bütün değerlerini sırayla teslim eder. next(value) ise generator'ın içine değer gönderir; gönderilen değer, duraklanan yield ifadesinin sonucu olur:

function* inner() {
  yield "b";
  yield "c";
}
function* outer() {
  yield "a";
  yield* inner();
  yield "d";
}
console.log([...outer()]); // ["a", "b", "c", "d"]

function* dialog() {
  const name = yield "What's your name?";
  yield `Hello, ${name}!`;
}
const chat = dialog();
console.log(chat.next().value); // "What's your name?"
console.log(chat.next("Ada").value); // "Hello, Ada!"

Sonsuz Diziler ve Tembel Değerlendirme

Generator değerleri istendikçe ürettiği için hiç bitmeyen bir diziyi tarif edebilirsin. Bellekte bir array yoktur; sadece "sıradaki değeri nasıl üretirim" bilgisi vardır. Buna tembel değerlendirme (lazy evaluation) denir:

function* fibonacci() {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

for (const n of fibonacci()) {
  if (n > 100) break;
  console.log(n);
}
// 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89

while (true) korkutucu görünebilir ama sorun yok; döngü her yield'de durur ve devam etmek için senin next() çağırmanı bekler. break geldiğinde for...of generator'ın return() method'unu çağırır ve generator kapanır. Generator'ın içinde try...finally varsa finally bloğu bu anda çalışır; açtığın bir kaynağı kapatmak için doğru yer burasıdır:

function* withCleanup() {
  try {
    yield 1;
    yield 2;
  } finally {
    console.log("cleaned up");
  }
}

for (const x of withCleanup()) {
  console.log(x);
  break;
}
// 1
// "cleaned up"

Iterator Helper'lar: map, filter, take ve Arkadaşları

Array'lerde map ve filter kullanmaya alışkınız. Peki sonsuz bir generator'ın ilk beş tek sayısının karesini nasıl alırsın? Array'e çeviremezsin, çünkü dizi hiç bitmez. ES2025 ile gelen iterator helper'lar tam bu boşluğu doldurur. Bütün iterator'ların ortak prototype'ı olan Iterator.prototype'a map, filter, take, drop, flatMap, reduce, toArray, forEach, some, every ve find method'ları eklendi. Güncel Chrome, Edge, Firefox ve Safari'nin hepsinde çalışıyorlar:

function* naturals() {
  let n = 1;
  while (true) yield n++;
}

const squares = naturals()
  .filter((n) => n % 2 === 1)
  .map((n) => n * n)
  .take(5)
  .toArray();

console.log(squares); // [1, 9, 25, 49, 81]

Array method'larından en büyük farkları tembel olmalarıdır. Array'deki map bütün array'i dolaşıp yeni bir array üretir, sonra filter o array'i baştan dolaşır. Iterator helper'lar ise her değeri zincirin sonuna kadar tek tek taşır ve yeterince değer toplandığında durur. Bunu gözünle gör:

const result = [1, 2, 3, 4].values()
  .map((n) => {
    console.log("map", n);
    return n * 2;
  })
  .filter((n) => {
    console.log("filter", n);
    return n > 2;
  })
  .take(1)
  .toArray();

console.log(result);
// map 1, filter 2, map 2, filter 4, [4]

3 ve 4 hiç işlenmedi; take(1) ilk uygun değeri alınca zinciri kapattı. Array'lerde helper'lara ulaşmak için values() ile bir iterator almamız gerektiğine dikkat et; [1, 2, 3].map hâlâ array'in kendi, hevesli (eager) map'idir. Map'in keys(), values() ve entries() method'ları zaten iterator döndürdüğü için helper'lar doğrudan çalışır.

Bir de Iterator.from var. Yukarıda elle yazdığımız range iterator'ı Iterator.prototype'tan miras almadığı için helper'lara sahip değil. Iterator.from(range), herhangi bir iterable'ı ya da iterator'ı helper'ları olan gerçek bir iterator'a sarar: Iterator.from(range).map((n) => n * 10).toArray() sana [10, 20, 30, 40, 50] verir.

Iterator.concat: Birden Fazla Kaynağı Uç Uca Eklemek

ES2026 ile gelen Iterator.concat, birden fazla iterable'ı tek bir iterator'da sırayla birleştirir. Array'lerin concat'inden farkı yine tembellik: hiçbir şeyi kopyalamaz, sırası gelen kaynağı sırası geldiğinde dolaşır. Örnek, bir önceki bölümdeki naturals generator'ını kullanıyor.

const all = Iterator.concat(
  ["a", "b"],
  new Set(["c"]),
  naturals().take(2)
);

console.log([...all]); // ["a", "b", "c", 1, 2]

Son kaynağın sonsuz bir generator'dan take(2) ile kesilmiş bir parça olduğuna dikkat et; concat ona geldiğinde sadece iki değer çekti. Bir ayrıntı: argümanların object olması gerekir. Iterator.concat("ab") yazarsan string bir primitive olduğu için TypeError alırsın; string'i önce [..."ab"] ile bir array'e çevir. Iterator.concat 2026 baharından beri bütün ana tarayıcılarda var, yani oldukça yeni; daha eski sürümleri de destekleyeceksen önce varlığını kontrol et ya da aynı işi yield* kullanan küçük bir generator'la yap.

Sık Düşülen Tuzaklar

Bir iterator'ı iki kez kullanmaya çalışmak. Iterator tek kullanımlıktır; tükendikten sonra hep done: true döner. const gen = numbers(); [...gen]; [...gen]; yazarsan ikinci spread boş bir array verir. Array'ler ve range gibi iterable'lar ise her seferinde yeni bir iterator ürettiği için tekrar tekrar dolaşılabilir. Bir değeri birden fazla kez dolaşman gerekiyorsa iterator'ı değil, onu üreten iterable'ı ya da fonksiyonu sakla.

Sonsuz bir generator'ı spread etmek. [...naturals()] hiç bitmez ve sekmeyi kilitler. Sonsuz kaynakları her zaman take, break ya da bir koşulla sınırla.

Helper zincirinin sonucunu array sanmak. naturals().map(fn) bir array değil, yeni bir iterator'dır; konsola yazdırdığında değerleri değil bir object görürsün. Değerleri istiyorsan zinciri toArray(), for...of ya da spread ile tüket.

Generator'ı arrow function ile yazmaya çalışmak. Arrow function'ların generator versiyonu yoktur. Method kısayolunda *name() {}, fonksiyonda function* name() {} yazarsın.

Küçük Bir Deneme

★☆☆ Çalıştırmadan önce tahmin et: function* g() { yield 1; return 2; yield 3; } console.log([...g()]); Ardından aynı generator'da next()'i dört kez elle çağır ve return'ün değerinin nerede göründüğüne bak.

★★☆ Bir iterable'ı verilen boyutta parçalara bölen chunks(iterable, size) generator'ını yaz. [...chunks([1, 2, 3, 4, 5], 2)] sonucu [[1, 2], [3, 4], [5]] olsun. Sonra onu naturals() ile ve take helper'ıyla birlikte kullanıp ilk üç parçayı al.

★★★ Ağaç şeklindeki bir object'i (her düğümde name ve children array'i olan) derinlemesine dolaşan bir *walk(node) generator'ı yaz; yield* kullan. Sonra iterator helper'larla ağaçta adı "a" ile başlayan ilk iki düğümü bul ve dolaşmanın ağacın geri kalanına hiç girmediğini console.log ile kanıtla.

Bu yazıdaki bütün değerler hazırdı; generator onları istediğimiz anda, senkron olarak üretti. Peki her değer ağdan, birkaç yüz milisaniye bekleyerek geliyorsa? Bir sonraki yazıda iterator'ın asenkron kardeşine geçiyoruz: Symbol.asyncIterator, async generator'lar, for await...of ve sayfalı bir API'yi tek bir döngüyle akış gibi okumak.