Menu
Playground'da Dene

JavaScript Iterator ve Generator: function* ve yield

JavaScript'te iterator protokolü nasıl çalışır, kendi nesnelerinizi nasıl iterable yaparsınız ve generator fonksiyonları bu işi nasıl kolaylaştırır?

Iterator Protokolü

JavaScript'te aslında birbirinden bağımsız gibi görünen pek çok özellik — for...of, spread (...), destructuring, Array.from, Promise.all — aynı temel mekanizmayı paylaşır: iterator protokolü. Bu protokolü bir kez kavradığınızda, saydığımız yapıların hepsinin aslında aynı fikrin farklı varyasyonları olduğunu göreceksiniz.

JavaScript'te iterator, { value, done } döndüren bir next() metoduna sahip herhangi bir nesnedir:

index.js
Output
Click Run to see the output here.

next() metodunu arka arkaya çağır. Her çağrı bir sonraki değeri ve bir done bayrağını döndürür. done true olduğunda dizi bitmiştir. Protokolün tamamı bu kadar — dört tuş vuruşu ve bir boolean.

Iterable ve iterator farkı

Burada birbiriyle bağlantılı ikinci bir kavram daha var. Iterable, iterator üretmeyi bilen her şeydir. Bunu da özel bir anahtar altında tutulan bir metotla yapar: Symbol.iterator.

index.js
Output
Click Run to see the output here.

Diziler (array) birer iterable'dır. numbers[Symbol.iterator]() çağrıldığında sıfırdan yeni bir iterator döner. String'ler, Map, Set ve arguments da iterable'dır — for...of hepsinde bu yüzden çalışır.

Burada ayrımı iyi oturtmak lazım: iterable koleksiyonun kendisi, iterator ise üzerinde gezinen imleç. Bir iterable'dan istediğiniz kadar bağımsız imleç talep edebilirsiniz.

for...of neden çalışıyor?

for...of aslında iterator protokolünün üstüne serpiştirilmiş bir sözdizimsel şekerden ibaret. Arka planda önce Symbol.iterator'ı çağırıyor, ardından done değeri true olana kadar next() metodunu tekrar tekrar çalıştırıyor:

index.js
Output
Click Run to see the output here.

Spread ile destructuring aslında aynı işi yapıyor: iterator bitene kadar tek tek ilerliyorlar:

index.js
Output
Click Run to see the output here.

Symbol.iterator metodunu uygulayan her nesne, bu özelliklerin tamamından otomatik olarak faydalanır.

Özel Iterable Nesne Oluşturma

Şimdi start değerinden end değerine kadar sayı üreten bir range nesnesi yazalım:

index.js
Output
Click Run to see the output here.

Dikkat etmeniz gereken birkaç nokta var:

  • [Symbol.iterator](), hesaplanmış bir metot adı kullanır. Anahtar, "Symbol.iterator" dizesi değil, sembolün kendisidir.
  • [Symbol.iterator]() her çağrıldığında, kendi current değerine sahip yepyeni bir iterator döner. range üzerinde iki kez döngü kurabilmenizi ve "tükenmemesini" sağlayan şey tam olarak budur.
  • Dönen iterator'ın yalnızca next() metoduna ihtiyacı vardır. Hepsi bu.

Çalışıyor, ama oldukça uzun. Çok daha şık bir yolu var.

Generator'lar sahneye çıkıyor

Generator fonksiyonu, function* ile tanımlanır (yıldıza dikkat). Baştan sona çalışmak yerine, bir yield ifadesinde durup sonra kaldığı yerden devam edebilir. Bu fonksiyonu çağırmak gövdeyi çalıştırmaz; size hem iterator hem de iterable olan bir generator nesnesi verir:

index.js
Output
Click Run to see the output here.

Her next() çağrısı, fonksiyonun gövdesini bir sonraki yield ifadesine kadar çalıştırır, orada duraklatır ve { value, done: false } döndürür. Fonksiyon tamamen bittiğinde ise { value: undefined, done: true } sonucunu alırsın.

Generator'lar aynı zamanda birer iterable olduğu için, bir önceki bölümde gördüğümüz her şeyle sorunsuz çalışırlar:

index.js
Output
Click Run to see the output here.

Aynı range Fonksiyonunu Generator ile Yazmak

Yukarıdaki uzun ve detaylı sürümü bir de şununla karşılaştır:

index.js
Output
Click Run to see the output here.

İşte bu kadar. [Symbol.iterator] önündeki *, onu bir generator metoduna dönüştürüyor. yield i ise elle yazdığımız tüm iterator nesnesinin yerine geçiyor. Ne next, ne done, ne de sınır hatası riski — push yerine yield kullanılan sıradan bir döngü işte.

Generator'ların var oluş sebebi tam olarak bu. "İterator yaz" işini "yield'leyen bir fonksiyon yaz"a indirgiyorlar.

yield ile return arasındaki fark

yield askıya alır; return ise bitirir. İstediğin kadar yield yapabilirsin — generator, kaldığı yerden devam eder:

index.js
Output
Click Run to see the output here.

Generatör içinde return kullandığınızda, sonlandıran çağrı { value: "done", done: true } şeklinde döner. Ancak for...of ve spread operatörü bu dönen değeri görmezden gelir — sadece done değeri false olan öğeleri tüketirler. Yani son bir öğeyi döngüye sokmak için return value kullanmaya kalkmayın; atlanır gider.

Tembel (Lazy) ve Sonsuz Diziler

Generatörler değerleri talep üzerine, teker teker üretir. Bu sayede dizi (array) olarak ifade edilmesi imkânsız olan sekansları bile rahatça temsil edebilirsiniz:

index.js
Output
Click Run to see the output here.

Döngü kelimenin tam anlamıyla while (true) — ama program yine de sonlanıyor. Çünkü generator, yalnızca biri bir sonraki değeri istediğinde ilerliyor. İlk N elemanı alıp durabilirsin; gerisi hiç çalışmaz:

index.js
Output
Click Run to see the output here.

take kendisi de başka bir generator'ı saran bir generator. Generator'ları bu şekilde birbirine bağlayabilmek, onların en güçlü yanlarından biri — küçük parçalar, her biri tek bir iş yapıyor.

yield* ile yetki devri

Bir generator'ın başka bir iterable'daki tüm değerleri sırayla yield etmesi gerekiyorsa, yield* bu işi devreder:

index.js
Output
Click Run to see the output here.

yield*, herhangi bir iterable ile çalışır — diziler, set'ler, başka generator'lar — ve her bir öğeyi tek tek dışarı aktarır. Yani iterator'ların spread operatörü gibi düşünebilirsin.

Kısaca Async Generator'lar

async function* olarak tanımlanan bir generator, zaman alan değerleri yield edebilir — bir API üzerinden veri akışı yapmak ya da dosyayı parça parça okumak gibi senaryolarda çok işe yarar. Böyle bir generator'ı for await...of ile tüketirsin:

async function* paginate(url) {
  let next = url;
  while (next) {
    const res = await fetch(next);
    const page = await res.json();
    for (const item of page.items) yield item;
    next = page.nextUrl;
  }
}

for await (const item of paginate("/api/users")) {
  console.log(item);
}

Bu örnek burada doğrudan çalıştırılamaz (gerçek bir endpoint gerektiriyor) ama böyle bir yapının var olduğunu bilmek faydalı. Normal generator'ları kavradıktan sonra async generator'lar da aynı mantık; sadece araya await serpiştiriyorsun.

Ne Zaman Generator Kullanmalı?

Şu durumlarda generator'a başvurmak mantıklı:

  • Dizi sonsuz ya da sonsuz olabilir — ID'ler, zaman damgaları, retry gecikmeleri.
  • Tüm değerleri üretmek pahalı ve tüketen taraf yarı yolda durabilir.
  • Kendi nesnende Symbol.iterator implemente ediyorsan. { next() } nesnesini elle yazmaktan neredeyse her zaman daha kısa olur.
  • Ara diziler oluşturmadan streaming dönüşümler (take, filter, map) zincirlemek istediğinde.

Veri zaten bellekte ve küçükse düz dizi kullan. Generator'ların bedava olmadığını unutma — fonksiyonu duraklatıp devam ettiren mekanizmanın bir maliyeti var ve generator kodundan geçen stack trace'leri okumak daha zor olabiliyor.

Sırada: Symbol'ler

Çoğu geliştiricinin ilk tanıştığı sembol Symbol.iterator olur ama tek sembol bundan çok uzak. Symbol'ler tam da bu tür genişletme noktaları için tasarlanmış bir primitif tip — sıradan property adlarıyla çakışmadan dile ve kendi kodunuza nesnelere kanca takma imkânı veren eşsiz anahtarlar. Bir sonraki sayfanın konusu bu.

Sıkça Sorulan Sorular

JavaScript'te iterable ile iterator arasındaki fark nedir?

Iterable, içinde Symbol.iterator metodu olan ve bu metot çağrıldığında bir iterator döndüren nesnedir. Iterator ise asıl değerleri üreten nesnedir; next() metodu vardır ve her çağrıda { value, done } döner. Array, string, Map ve Set birer iterable'dır; bunların Symbol.iterator metodunu çağırdığınızda tek tek ilerleyebileceğiniz bir iterator elde edersiniz.

JavaScript'te generator fonksiyonu nedir?

function* ile tanımlanan ve yield kullanarak değerleri tembel (lazy) şekilde üreten fonksiyondur. Çağırdığınızda gövde hemen çalışmaz; size hem iterator hem de iterable olan bir generator nesnesi döner. Her next() çağrısı bir sonraki yield'e kadar çalışır, orada durur ve üretilen değeri geri verir.

Generator içinde yield ile return arasındaki fark nedir?

yield generator'ı duraklatıp bir değer geri verir; ama fonksiyon bir sonraki next() çağrısında kaldığı yerden devam edebilir. return ise generator'ı tamamen sonlandırır; done: true olur ve artık yeni değer gelmez. İstediğiniz kadar yield yapabilirsiniz, ama anlamlı bir return yalnızca bir kez olur.

Array yerine ne zaman generator kullanmalıyım?

Dizi sonsuzsa, değerleri hesaplamak pahalıysa ya da sadece bir kısmına ihtiyacınız varsa generator iyi bir seçimdir. Generator değerleri istendiğinde tek tek üretir; böylece sonsuz bir ID akışını veya sayfalı API sonuçlarını her şeyi baştan belleğe almadan ifade edebilirsiniz. Elinizde zaten küçük, sabit bir array varsa array'de kalın.

Coddy ile kodlamayı öğren

BAŞLA