راهنمای جامع توابع generator جاوا اسکریپت و پروتکل iterator. نحوه ایجاد ایتریتورهای سفارشی و بهبود برنامههای جاوا اسکریپت خود را بیاموزید.
توابع Generator در جاوا اسکریپت: تسلط بر پروتکل Iterator
توابع generator جاوا اسکریپت، که در ECMAScript 6 (ES6) معرفی شدند، مکانیزم قدرتمندی برای ایجاد ایتریتورها به روشی خلاصهتر و خواناتر فراهم میکنند. آنها به طور یکپارچه با پروتکل iterator ادغام میشوند و به شما امکان میدهند ایتریتورهای سفارشی بسازید که میتوانند ساختارهای داده پیچیده و عملیات ناهمزمان را به راحتی مدیریت کنند. این مقاله به بررسی جزئیات توابع generator، پروتکل iterator و مثالهای عملی برای نشان دادن کاربرد آنها میپردازد.
درک پروتکل Iterator
قبل از پرداختن به توابع generator، درک پروتکل iterator که اساس ساختارهای داده قابل تکرار (iterable) در جاوا اسکریپت را تشکیل میدهد، بسیار مهم است. پروتکل iterator نحوه تکرار روی یک شیء را تعریف میکند، به این معنی که عناصر آن میتوانند به صورت متوالی قابل دسترسی باشند.
پروتکل Iterable
یک شیء iterable (قابل تکرار) در نظر گرفته میشود اگر متد @@iterator (Symbol.iterator) را پیادهسازی کند. این متد باید یک شیء iterator را برگرداند.
مثالی از یک شیء iterable ساده:
const myIterable = {
data: [1, 2, 3],
[Symbol.iterator]() {
let index = 0;
return {
next() {
if (index < myIterable.data.length) {
return { value: myIterable.data[index++], done: false };
} else {
return { value: undefined, done: true };
}
}
};
}
};
for (const item of myIterable) {
console.log(item); // خروجی: 1, 2, 3
}
پروتکل Iterator
یک شیء iterator باید یک متد next() داشته باشد. متد next() یک شیء با دو ویژگی را برمیگرداند:
value: مقدار بعدی در توالی.done: یک مقدار بولی که نشان میدهد آیا iterator به پایان توالی رسیده است یا خیر.trueبه معنای پایان است؛falseیعنی مقادیر بیشتری برای بازیابی وجود دارد.
پروتکل iterator به ویژگیهای داخلی جاوا اسکریپت مانند حلقههای for...of و عملگر spread (...) اجازه میدهد تا به طور یکپارچه با ساختارهای داده سفارشی کار کنند.
معرفی توابع Generator
توابع Generator روشی زیباتر و خلاصهتر برای ایجاد ایتریتورها فراهم میکنند. آنها با استفاده از سینتکس function* تعریف میشوند.
سینتکس توابع Generator
سینتکس پایه یک تابع generator به شرح زیر است:
function* myGenerator() {
yield 1;
yield 2;
yield 3;
}
const iterator = myGenerator();
console.log(iterator.next()); // خروجی: { value: 1, done: false }
console.log(iterator.next()); // خروجی: { value: 2, done: false }
console.log(iterator.next()); // خروجی: { value: 3, done: false }
console.log(iterator.next()); // خروجی: { value: undefined, done: true }
ویژگیهای کلیدی توابع generator:
- آنها با
function*به جایfunctionتعریف میشوند. - آنها از کلمه کلیدی
yieldبرای متوقف کردن اجرا و برگرداندن یک مقدار استفاده میکنند. - هر بار که
next()روی ایتریتور فراخوانی میشود، تابع generator اجرا را از جایی که متوقف شده بود تا رسیدن به عبارتyieldبعدی یا پایان تابع، از سر میگیرد. - وقتی اجرای تابع generator به پایان میرسد (یا با رسیدن به انتها یا با برخورد به عبارت
return)، ویژگیdoneشیء بازگشتیtrueمیشود.
چگونه توابع Generator پروتکل Iterator را پیادهسازی میکنند
وقتی یک تابع generator را فراخوانی میکنید، بلافاصله اجرا نمیشود. در عوض، یک شیء ایتریتور برمیگرداند. این شیء ایتریتور به طور خودکار پروتکل iterator را پیادهسازی میکند. هر عبارت yield یک مقدار برای متد next() ایتریتور تولید میکند. تابع generator وضعیت داخلی را مدیریت کرده و پیشرفت خود را پیگیری میکند، که این امر ایجاد ایتریتورهای سفارشی را سادهتر میکند.
مثالهای عملی از توابع Generator
بیایید چند مثال عملی را بررسی کنیم که قدرت و تطبیقپذیری توابع generator را نشان میدهند.
۱. تولید یک توالی از اعداد
این مثال نشان میدهد چگونه یک تابع generator ایجاد کنیم که توالیای از اعداد را در یک محدوده مشخص تولید میکند.
function* numberSequence(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}
const sequence = numberSequence(10, 15);
for (const num of sequence) {
console.log(num); // خروجی: 10, 11, 12, 13, 14, 15
}
۲. پیمایش یک ساختار درختی
توابع Generator به ویژه برای پیمایش ساختارهای داده پیچیده مانند درختها مفید هستند. این مثال نشان میدهد چگونه گرههای یک درخت دودویی را پیمایش کنیم.
class TreeNode {
constructor(value) {
this.value = value;
this.left = null;
this.right = null;
}
}
function* treeTraversal(node) {
if (node) {
yield* treeTraversal(node.left); // فراخوانی بازگشتی برای زیردرخت چپ
yield node.value; // Yield کردن مقدار گره فعلی
yield* treeTraversal(node.right); // فراخوانی بازگشتی برای زیردرخت راست
}
}
// ایجاد یک درخت دودویی نمونه
const root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);
// پیمایش درخت با استفاده از تابع generator
const treeIterator = treeTraversal(root);
for (const value of treeIterator) {
console.log(value); // خروجی: 4, 2, 5, 1, 3 (پیمایش میانترتیب)
}
در این مثال، از yield* برای واگذاری به یک ایتریتور دیگر استفاده میشود. این برای تکرار بازگشتی بسیار مهم است و به generator اجازه میدهد تا کل ساختار درخت را پیمایش کند.
۳. مدیریت عملیات ناهمزمان
توابع Generator را میتوان با Promiseها ترکیب کرد تا عملیات ناهمزمان را به روشی متوالیتر و خواناتر مدیریت کنند. این امر به ویژه برای کارهایی مانند دریافت داده از یک API مفید است.
async function fetchData(url) {
const response = await fetch(url);
const data = await response.json();
return data;
}
function* dataFetcher(urls) {
for (const url of urls) {
try {
const data = yield fetchData(url);
yield data;
} catch (error) {
console.error("Error fetching data from", url, error);
yield null; // یا خطا را به شکل مورد نیاز مدیریت کنید
}
}
}
async function runDataFetcher() {
const urls = [
"https://jsonplaceholder.typicode.com/todos/1",
"https://jsonplaceholder.typicode.com/posts/1",
"https://jsonplaceholder.typicode.com/users/1"
];
const dataIterator = dataFetcher(urls);
for (const promise of dataIterator) {
const data = await promise; // منتظر ماندن برای promise بازگشتی توسط yield
if (data) {
console.log("Fetched data:", data);
} else {
console.log("Failed to fetch data.");
}
}
}
runDataFetcher();
این مثال تکرار ناهمزمان را به نمایش میگذارد. تابع generator dataFetcher، Promiseهایی را yield میکند که به دادههای دریافت شده resolve میشوند. سپس تابع runDataFetcher این promiseها را پیمایش کرده و قبل از پردازش دادهها، منتظر هر یک میماند. این رویکرد با همزمانتر نشان دادن کد ناهمزمان، آن را سادهتر میکند.
۴. توالیهای نامتناهی
Generatorها برای نمایش توالیهای نامتناهی، یعنی توالیهایی که هرگز تمام نمیشوند، عالی هستند. از آنجا که آنها فقط در صورت درخواست مقادیر را تولید میکنند، میتوانند توالیهای بینهایت طولانی را بدون مصرف حافظه بیش از حد مدیریت کنند.
function* fibonacciSequence() {
let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const fibonacci = fibonacciSequence();
// دریافت ۱۰ عدد اول فیبوناچی
for (let i = 0; i < 10; i++) {
console.log(fibonacci.next().value); // خروجی: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
}
این مثال نحوه ایجاد یک توالی فیبوناچی نامتناهی را نشان میدهد. تابع generator به طور نامحدود به yield کردن اعداد فیبوناچی ادامه میدهد. در عمل، معمولاً تعداد مقادیر بازیابی شده را محدود میکنید تا از حلقه بینهایت یا اتمام حافظه جلوگیری کنید.
۵. پیادهسازی یک تابع Range سفارشی
یک تابع range سفارشی شبیه به تابع داخلی range پایتون با استفاده از generatorها ایجاد کنید.
function* range(start, end, step = 1) {
if (step > 0) {
for (let i = start; i < end; i += step) {
yield i;
}
} else if (step < 0) {
for (let i = start; i > end; i += step) {
yield i;
}
}
}
// تولید اعداد از ۰ تا ۵ (غیرشامل)
for (const num of range(0, 5)) {
console.log(num); // خروجی: 0, 1, 2, 3, 4
}
// تولید اعداد از ۱۰ تا ۰ (غیرشامل) به ترتیب معکوس
for (const num of range(10, 0, -2)) {
console.log(num); // خروجی: 10, 8, 6, 4, 2
}
تکنیکهای پیشرفته توابع Generator
۱. استفاده از `return` در توابع Generator
دستور return در یک تابع generator به معنای پایان تکرار است. هنگامی که با دستور return مواجه میشویم، ویژگی done متد next() ایتریتور برابر true، و ویژگی value برابر مقداری که توسط دستور return بازگردانده شده (در صورت وجود) تنظیم میشود.
function* myGenerator() {
yield 1;
yield 2;
return 3; // پایان تکرار
yield 4; // این اجرا نخواهد شد
}
const iterator = myGenerator();
console.log(iterator.next()); // خروجی: { value: 1, done: false }
console.log(iterator.next()); // خروجی: { value: 2, done: false }
console.log(iterator.next()); // خروجی: { value: 3, done: true }
console.log(iterator.next()); // خروجی: { value: undefined, done: true }
۲. استفاده از `throw` در توابع Generator
متد throw روی شیء ایتریتور به شما امکان میدهد یک استثنا (exception) را به داخل تابع generator تزریق کنید. این میتواند برای مدیریت خطاها یا سیگنال دادن به شرایط خاص در داخل generator مفید باشد.
function* myGenerator() {
try {
yield 1;
yield 2;
} catch (error) {
console.error("Caught an error:", error);
}
yield 3;
}
const iterator = myGenerator();
console.log(iterator.next()); // خروجی: { value: 1, done: false }
iterator.throw(new Error("Something went wrong!")); // تزریق یک خطا
console.log(iterator.next()); // خروجی: { value: 3, done: false }
console.log(iterator.next()); // خروجی: { value: undefined, done: true }
۳. واگذاری به یک Iterable دیگر با `yield*`
همانطور که در مثال پیمایش درخت مشاهده شد، سینتکس yield* به شما امکان میدهد تا به یک iterable دیگر (یا یک تابع generator دیگر) واگذار کنید. این یک ویژگی قدرتمند برای ترکیب ایتریتورها و سادهسازی منطق تکرار پیچیده است.
function* generator1() {
yield 1;
yield 2;
}
function* generator2() {
yield* generator1(); // واگذاری به generator1
yield 3;
yield 4;
}
const iterator = generator2();
for (const value of iterator) {
console.log(value); // خروجی: 1, 2, 3, 4
}
مزایای استفاده از توابع Generator
- خوانایی بهبود یافته: توابع Generator کد ایتریتور را در مقایسه با پیادهسازیهای دستی ایتریتور، خلاصهتر و قابل فهمتر میکنند.
- برنامهنویسی ناهمزمان سادهتر: آنها با اجازه دادن به شما برای نوشتن عملیات ناهمزمان به سبکی همزمانتر، کد ناهمزمان را ساده میکنند.
- بهرهوری حافظه: توابع Generator مقادیر را بر حسب تقاضا تولید میکنند، که به ویژه برای مجموعه دادههای بزرگ یا توالیهای نامتناهی مفید است. آنها از بارگذاری کل مجموعه داده در حافظه به یکباره جلوگیری میکنند.
- قابلیت استفاده مجدد کد: شما میتوانید توابع generator قابل استفاده مجدد ایجاد کنید که میتوانند در بخشهای مختلف برنامه شما استفاده شوند.
- انعطافپذیری: توابع Generator روشی انعطافپذیر برای ایجاد ایتریتورهای سفارشی فراهم میکنند که میتوانند ساختارهای داده و الگوهای تکرار مختلف را مدیریت کنند.
بهترین شیوهها برای استفاده از توابع Generator
- از نامهای توصیفی استفاده کنید: نامهای معنیدار برای توابع generator و متغیرهای خود انتخاب کنید تا خوانایی کد را بهبود بخشید.
- خطاها را به درستی مدیریت کنید: مدیریت خطا را در توابع generator خود پیادهسازی کنید تا از رفتار غیرمنتظره جلوگیری شود.
- توالیهای نامتناهی را محدود کنید: هنگام کار با توالیهای نامتناهی، اطمینان حاصل کنید که مکانیزمی برای محدود کردن تعداد مقادیر بازیابی شده دارید تا از حلقههای بینهایت یا اتمام حافظه جلوگیری کنید.
- عملکرد را در نظر بگیرید: در حالی که توابع generator به طور کلی کارآمد هستند، به پیامدهای عملکردی، به ویژه هنگام کار با عملیاتهای محاسباتی سنگین، توجه داشته باشید.
- کد خود را مستند کنید: مستندات واضح و مختصر برای توابع generator خود فراهم کنید تا به سایر توسعهدهندگان در درک نحوه استفاده از آنها کمک کند.
موارد استفاده فراتر از جاوا اسکریپت
مفهوم generatorها و ایتریتورها فراتر از جاوا اسکریپت است و در زبانها و سناریوهای مختلف برنامهنویسی کاربرد دارد. برای مثال:
- پایتون: پایتون پشتیبانی داخلی از generatorها با استفاده از کلمه کلیدی
yieldدارد که بسیار شبیه به جاوا اسکریپت است. آنها به طور گسترده برای پردازش کارآمد داده و مدیریت حافظه استفاده میشوند. - سیشارپ: سیشارپ از ایتریتورها و عبارت
yield returnبرای پیادهسازی تکرار مجموعههای سفارشی استفاده میکند. - جریانسازی داده (Data Streaming): در خطوط لوله پردازش داده، میتوان از generatorها برای پردازش جریانهای بزرگ داده به صورت تکهتکه استفاده کرد، که باعث بهبود کارایی و کاهش مصرف حافظه میشود. این امر به ویژه هنگام کار با دادههای لحظهای از سنسورها، بازارهای مالی یا رسانههای اجتماعی اهمیت دارد.
- توسعه بازی: از generatorها میتوان برای ایجاد محتوای رویهای (procedural)، مانند تولید زمین یا توالیهای انیمیشن، بدون پیشمحاسبه و ذخیره کل محتوا در حافظه استفاده کرد.
نتیجهگیری
توابع generator جاوا اسکریپت ابزاری قدرتمند برای ایجاد ایتریتورها و مدیریت عملیات ناهمزمان به روشی زیباتر و کارآمدتر هستند. با درک پروتکل iterator و تسلط بر کلمه کلیدی yield، میتوانید از توابع generator برای ساخت برنامههای جاوا اسکریپت خواناتر، قابل نگهداریتر و با عملکرد بهتر استفاده کنید. از تولید توالی اعداد گرفته تا پیمایش ساختارهای داده پیچیده و مدیریت وظایف ناهمزمان، توابع generator راهحلی همهکاره برای طیف گستردهای از چالشهای برنامهنویسی ارائه میدهند. توابع generator را بپذیرید تا امکانات جدیدی را در گردش کار توسعه جاوا اسکریپت خود باز کنید.