مقدمه
Generatorها یکی از امکانات مهم و مدرن جاوااسکریپت هستند که به شما اجازه میدهند کنترل کاملی بر
اجرای تابع داشته باشید. با generatorها میتوانید روند اجرا را هرجا بخواهید متوقف کنید، سپس ادامه
دهید، و مقادیر را یکییکی و به صورت lazy تولید کنید. این قابلیت برای ساخت دنبالههای بینهایت،
تولید داده به تدریج، قطع و ادامه محاسبات سنگین یا حتی شبیهسازی state machine فوقالعاده کاربرد
دارد.
با generator میتوانید کدهایی بنویسید که قبلاً تنها با الگوریتمهای پیچیده یا استفاده از
callbackها و stateهای متعدد ممکن بود. امروزه در توسعه فریمورکها، کار با دادههای حجیم و پردازش
streamها، generatorها بسیار مورد توجه هستند.
تابع Generator چیست؟
تابع generator یک نوع تابع خاص در جاوااسکریپت است که میتواند چندین بار متوقف و دوباره اجرا شود.
این توابع با علامت * تعریف میشوند و به جای return از yield برای بازگرداندن مقدار
استفاده
میکنند. هر بار که generator را صدا میزنید، اجرای آن تا اولین yield پیش میرود و مقدار
را
بازمیگرداند؛ سپس دفعه بعد از همان نقطه ادامه میدهد. این رفتار باعث میشود بتوانید دنبالهای از
مقادیر را به صورت مرحلهای تولید کنید.
در کد زیر یک تابع generator تعریف شده است.
JAVASCRIPT
function* genNumbers() {
yield 10;
yield 20;
yield 30;
}
let g = genNumbers();
console.log(g.next());
console.log(g.next());
console.log(g.next());
console.log(g.next());
در این مثال، هر بار که متد next() را روی generator صدا میزنید،
اجرای تابع تا اولین
yield
پیش میرود و مقدار بعدی را بازمیگرداند. وقتی همه yield
ها اجرا
شدند، مقدار done: true
بازمیگردد و generator خاتمه مییابد.
generator با ورودی (پارامتر)
میتوانیم generatorهایی با پارامتر بسازیم و رفتار yield را با آرگومانها کنترل کنیم. مثال زیر را
ببینید.
JAVASCRIPT
function* countTo(n) {
for (let i = 1; i <= n; i++) {
yield i;
}
}
for (let num of countTo(3)) {
console.log(num);
}
در این مثال، تابع generator با پارامتر n تعداد دفعات yield را کنترل میکند. با هر
بار اجرای حلقه
for، مقدار i بازگردانده میشود و میتوانید با حلقه for...of تمام مقادیر
تولید شده را دریافت
کنید. این روش برای تولید دنبالههای عددی یا هر نوع داده مرحلهای بسیار مناسب است.
استفاده از yield و کنترل جریان
گفتیم که yield اجرای generator را متوقف و مقدار فعلی را بازمیگرداند. تابع generator تا
اولین yield اجرا
میشود و با هر next تا yield بعدی ادامه پیدا میکند. بین yieldها میتوانیم
هر نوع منطقی قرار
دهیم (حلقه، شرط، دریافت ورودی و ...).
در واقع، yield اجرای تابع را موقتاً متوقف میکند و اجازه میدهد دفعه بعد از همان نقطه
ادامه دهیم اما return
تابع را کاملاً خاتمه میدهد. میتوانید در یک generator فقط یک return نهایی داشته باشید تا
پایان
دنباله را مشخص کند.
فرستادن مقدار به generator از بیرون
با استفاده از پارامتر متد next() میتوانیم از بیرون مقداری به درون
generator ارسال کنیم و رفتار تابع
را کنترل کنیم. مثال زیر را ببینید.
JAVASCRIPT
function* inputGen() {
let x = yield "enter value";
console.log("got:", x);
}
let it = inputGen();
console.log(it.next());
console.log(it.next(42));
در اینجا مقدار ارسالشده به متد next() (در این مثال
42
) به عنوان مقدار
x
داخل generator قرار میگیرد. اولین فراخوانی next()
همیشه مقدار
پارامتر را نادیده میگیرد و فقط اجرای تابع را تا اولین yield
پیش میبرد. اما از
فراخوانی دوم به بعد، هر مقداری که به next() بدهید، به عنوان مقدار
yield قبلی داخل
تابع قرار میگیرد. این ویژگی به شما اجازه میدهد داده یا فرمانهایی را از بیرون به جریان
generator تزریق کنید و رفتار آن را به شکل پویا تغییر دهید.
تولید دنباله بینهایت (مثال فیبوناچی)
در حالت عادی اگر بخواهید دنبالهای بینهایت بسازید، با آرایهها یا توابع معمولی ممکن نیست چون
حافظه پر میشود یا برنامه قفل میکند. اما با generator میتوانید هر بار فقط مقدار بعدی را تولید
کنید و هیچ محدودیتی ندارید. کافی است حلقه بینهایت (while(true)) بنویسید و با yield مقدار
را
بازگردانید. این روش برای تولید دنبالههای ریاضی، اعداد تصادفی یا هر نوع داده مرحلهای عالی است.
مثال زیر دنباله فیبوناچی را تا هر جا بخواهیم، تولید میکند.
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های
کوچکتر بسیار مفید است.
JAVASCRIPT
function* sub() {
yield "a";
yield "b";
}
function* main() {
yield "start";
yield* sub();
yield "end";
}
for (let v of main()) {
console.log(v);
}
generator و پیمایش با for...of
generator به طور پیشفرض iterable است، یعنی میتوانید آن را مستقیماً با حلقه for...of
پیمایش
کنید. حلقه for...of پشت صحنه هر بار next را صدا میزند و مقدار yield شده را
دریافت میکند تا به
done: true برسد.
این کار باعث میشود ساختار کد بسیار خوانا و شبیه به آرایهها باشد. مثال زیر را ببینید.
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 به آن خواهیم پرداخت.