مقدمه

Generatorها یکی از امکانات مهم و مدرن جاوااسکریپت هستند که به شما اجازه می‌دهند کنترل کاملی بر اجرای تابع داشته باشید. با generatorها می‌توانید روند اجرا را هرجا بخواهید متوقف کنید، سپس ادامه دهید، و مقادیر را یکی‌یکی و به صورت lazy تولید کنید. این قابلیت برای ساخت دنباله‌های بی‌نهایت، تولید داده به تدریج، قطع و ادامه محاسبات سنگین یا حتی شبیه‌سازی state machine فوق‌العاده کاربرد دارد. با generator می‌توانید کدهایی بنویسید که قبلاً تنها با الگوریتم‌های پیچیده یا استفاده از callbackها و stateهای متعدد ممکن بود. امروزه در توسعه فریم‌ورک‌ها، کار با داده‌های حجیم و پردازش streamها، generatorها بسیار مورد توجه هستند.

تابع Generator چیست؟

تابع generator یک نوع تابع خاص در جاوااسکریپت است که می‌تواند چندین بار متوقف و دوباره اجرا شود. این توابع با علامت * تعریف می‌شوند و به جای return از yield برای بازگرداندن مقدار استفاده می‌کنند. هر بار که generator را صدا می‌زنید، اجرای آن تا اولین yield پیش می‌رود و مقدار را بازمی‌گرداند؛ سپس دفعه بعد از همان نقطه ادامه می‌دهد. این رفتار باعث می‌شود بتوانید دنباله‌ای از مقادیر را به صورت مرحله‌ای تولید کنید.

در کد زیر یک تابع generator تعریف شده است.

Copy Icon JAVASCRIPT
function* genNumbers() {
  yield 10;
  yield 20;
  yield 30;
}
let g = genNumbers();
console.log(g.next()); // {value: 10, done: false}
console.log(g.next()); // {value: 20, done: false}
console.log(g.next()); // {value: 30, done: false}
console.log(g.next()); // {value: undefined, done: true}

در این مثال، هر بار که متد next() را روی generator صدا می‌زنید، اجرای تابع تا اولین yield پیش می‌رود و مقدار بعدی را بازمی‌گرداند. وقتی همه yieldها اجرا شدند، مقدار done: true بازمی‌گردد و generator خاتمه می‌یابد.

generator با ورودی (پارامتر)

می‌توانیم generatorهایی با پارامتر بسازیم و رفتار yield را با آرگومان‌ها کنترل کنیم. مثال زیر را ببینید.

Copy Icon JAVASCRIPT
function* countTo(n) {
  for (let i = 1; i <= n; i++) {
    yield i;
  }
}
for (let num of countTo(3)) {
  console.log(num); // 1, 2, 3
}

در این مثال، تابع generator با پارامتر n تعداد دفعات yield را کنترل می‌کند. با هر بار اجرای حلقه for، مقدار i بازگردانده می‌شود و می‌توانید با حلقه for...of تمام مقادیر تولید شده را دریافت کنید. این روش برای تولید دنباله‌های عددی یا هر نوع داده مرحله‌ای بسیار مناسب است.

استفاده از yield و کنترل جریان

گفتیم که yield اجرای generator را متوقف و مقدار فعلی را بازمی‌گرداند. تابع generator تا اولین yield اجرا می‌شود و با هر next تا yield بعدی ادامه پیدا می‌کند. بین yieldها می‌توانیم هر نوع منطقی قرار دهیم (حلقه، شرط، دریافت ورودی و ...).

در واقع، yield اجرای تابع را موقتاً متوقف می‌کند و اجازه می‌دهد دفعه بعد از همان نقطه ادامه دهیم اما return تابع را کاملاً خاتمه می‌دهد. می‌توانید در یک generator فقط یک return نهایی داشته باشید تا پایان دنباله را مشخص کند.

فرستادن مقدار به generator از بیرون

با استفاده از پارامتر متد next() می‌توانیم از بیرون مقداری به درون generator ارسال کنیم و رفتار تابع را کنترل کنیم. مثال زیر را ببینید.

Copy Icon JAVASCRIPT
function* inputGen() {
  let x = yield "enter value";
  console.log("got:", x);
}
let it = inputGen();
console.log(it.next());         // {value: "enter value", done: false}
console.log(it.next(42));  // got: 42

در اینجا مقدار ارسال‌شده به متد next() (در این مثال 42) به عنوان مقدار x داخل generator قرار می‌گیرد. اولین فراخوانی next() همیشه مقدار پارامتر را نادیده می‌گیرد و فقط اجرای تابع را تا اولین yield پیش می‌برد. اما از فراخوانی دوم به بعد، هر مقداری که به next() بدهید، به عنوان مقدار yield قبلی داخل تابع قرار می‌گیرد. این ویژگی به شما اجازه می‌دهد داده یا فرمان‌هایی را از بیرون به جریان generator تزریق کنید و رفتار آن را به شکل پویا تغییر دهید.

تولید دنباله بی‌نهایت (مثال فیبوناچی)

در حالت عادی اگر بخواهید دنباله‌ای بی‌نهایت بسازید، با آرایه‌ها یا توابع معمولی ممکن نیست چون حافظه پر می‌شود یا برنامه قفل می‌کند. اما با generator می‌توانید هر بار فقط مقدار بعدی را تولید کنید و هیچ محدودیتی ندارید. کافی است حلقه بی‌نهایت (while(true)) بنویسید و با yield مقدار را بازگردانید. این روش برای تولید دنباله‌های ریاضی، اعداد تصادفی یا هر نوع داده مرحله‌ای عالی است. مثال زیر دنباله فیبوناچی را تا هر جا بخواهیم، تولید می‌کند.

Copy Icon JAVASCRIPT
function* fib() {
  let a = 0, b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}
let f = fib();
for (let i = 0; i < 6; i++) {
  console.log(f.next().value);
}

در اینجا generator دنباله فیبوناچی را به صورت بی‌نهایت تولید می‌کند اما با استفاده از یک حلقه for فقط ۶ مقدار اول را دریافت می‌کنیم. این روش باعث می‌شود حافظه مصرف نشود و هر بار فقط مقدار مورد نیاز تولید شود. این قابلیت برای دنباله‌های طولانی یا نامحدود بسیار مفید است.

generator تو در تو با yield*

گاهی لازم است یک generator دیگر را در دل generator فعلی فراخوانی کنیم و تمام مقادیر آن را به صورت پشت سر هم yield کنیم. برای این کار از دستور yield* استفاده می‌شود. این دستور باعث می‌شود تمام مقادیر generator داخلی (یا هر iterable دیگر) به صورت متوالی yield شوند، انگار که بخشی از generator اصلی هستند. این روش برای ترکیب چند دنباله یا تقسیم منطق به generatorهای کوچکتر بسیار مفید است.

Copy Icon JAVASCRIPT
function* sub() {
  yield "a";
  yield "b";
}
function* main() {
  yield "start";
  yield* sub();
  yield "end";
}
for (let v of main()) {
  console.log(v); // start, a, b, end
}

generator و پیمایش با for...of

generator به طور پیش‌فرض iterable است، یعنی می‌توانید آن را مستقیماً با حلقه for...of پیمایش کنید. حلقه for...of پشت صحنه هر بار next را صدا می‌زند و مقدار yield شده را دریافت می‌کند تا به done: true برسد. این کار باعث می‌شود ساختار کد بسیار خوانا و شبیه به آرایه‌ها باشد. مثال زیر را ببینید.

Copy Icon JAVASCRIPT
function* gen() {
  yield 1;
  yield 2;
  yield 3;
}
for (let n of gen()) {
  console.log(n);
}

generatorها در جاوااسکریپت برای کنترل جریان داده، قطع و ادامه محاسبات سنگین، پیاده‌سازی الگوریتم‌های lazy evaluation و حتی شبیه‌سازی coroutine استفاده می‌شوند. یکی از کاربردهای رایج، پردازش مرحله‌ای داده‌های بزرگ بدون قفل کردن مرورگر است.

همیشه برای کاربردهایی که نیاز به قطع و ادامه عملیات یا تولید تدریجی داده دارید از generatorها استفاده کنید و برای داده‌های غیرهمزمان سراغ async generator بروید که در فصل ۱۱ و بعد از آشنایی با Promise و الگوی async/await به آن خواهیم پرداخت.