راهنمای جامع درک و پیادهسازی پروتکل Iterator جاوا اسکریپت که شما را قادر میسازد تا برای مدیریت بهتر دادهها، Iteratorهای سفارشی ایجاد کنید.
ابهامزدایی از پروتکل Iterator جاوا اسکریپت و Iteratorهای سفارشی
پروتکل Iterator جاوا اسکریپت یک روش استاندارد برای پیمایش ساختارهای داده فراهم میکند. درک این پروتکل به توسعهدهندگان این امکان را میدهد که به طور کارآمد با پیمایشپذیرهای داخلی (built-in iterables) مانند آرایهها و رشتهها کار کنند و پیمایشپذیرهای سفارشی خود را متناسب با ساختارهای داده و نیازهای برنامه خاص خود ایجاد کنند. این راهنما یک بررسی جامع از پروتکل Iterator و نحوه پیادهسازی پیمایشگرهای سفارشی ارائه میدهد.
پروتکل Iterator چیست؟
پروتکل Iterator تعریف میکند که چگونه یک شیء میتواند پیمایش شود، یعنی چگونه عناصر آن به صورت متوالی قابل دسترسی هستند. این پروتکل از دو بخش تشکیل شده است: پروتکل Iterable و پروتکل Iterator.
پروتکل Iterable
یک شیء زمانی Iterable (پیمایشپذیر) در نظر گرفته میشود که متدی با کلید Symbol.iterator
داشته باشد. این متد باید یک شیء مطابق با پروتکل Iterator را برگرداند.
در اصل، یک شیء پیمایشپذیر میداند که چگونه یک پیمایشگر (iterator) برای خود ایجاد کند.
پروتکل Iterator
پروتکل Iterator نحوه بازیابی مقادیر از یک دنباله را تعریف میکند. یک شیء زمانی پیمایشگر (iterator) در نظر گرفته میشود که متد next()
را داشته باشد که یک شیء با دو ویژگی برمیگرداند:
value
: مقدار بعدی در دنباله.done
: یک مقدار بولی که نشان میدهد آیا پیمایشگر به پایان دنباله رسیده است یا خیر. اگرdone
برابر باtrue
باشد، ویژگیvalue
میتواند حذف شود.
متد next()
بخش اصلی پروتکل Iterator است. هر فراخوانی next()
، پیمایشگر را به جلو میبرد و مقدار بعدی در دنباله را برمیگرداند. وقتی تمام مقادیر برگردانده شدند، next()
یک شیء با done
برابر با true
برمیگرداند.
پیمایشپذیرهای داخلی (Built-in)
جاوا اسکریپت چندین ساختار داده داخلی ارائه میدهد که به طور ذاتی پیمایشپذیر هستند. این موارد عبارتند از:
- آرایهها (Arrays)
- رشتهها (Strings)
- مپها (Maps)
- ستها (Sets)
- شیء Arguments یک تابع
- TypedArrays
این پیمایشپذیرها میتوانند مستقیماً با حلقه for...of
، سینتکس spread (...
) و سایر ساختارهایی که به پروتکل Iterator متکی هستند، استفاده شوند.
مثال با آرایهها:
const myArray = ["apple", "banana", "cherry"];
for (const item of myArray) {
console.log(item); // Output: apple, banana, cherry
}
مثال با رشتهها:
const myString = "Hello";
for (const char of myString) {
console.log(char); // Output: H, e, l, l, o
}
حلقه for...of
حلقه for...of
یک ساختار قدرتمند برای پیمایش اشیاء پیمایشپذیر است. این حلقه به طور خودکار پیچیدگیهای پروتکل Iterator را مدیریت میکند و دسترسی به مقادیر یک دنباله را آسان میسازد.
سینتکس حلقه for...of
به این صورت است:
for (const element of iterable) {
// Code to be executed for each element
}
حلقه for...of
پیمایشگر را از شیء پیمایشپذیر (با استفاده از Symbol.iterator
) بازیابی میکند و به طور مکرر متد next()
پیمایشگر را فراخوانی میکند تا زمانی که done
برابر با true
شود. در هر تکرار، متغیر element
به ویژگی value
که توسط next()
برگردانده شده است، اختصاص مییابد.
ایجاد Iteratorهای سفارشی
در حالی که جاوا اسکریپت پیمایشپذیرهای داخلی را فراهم میکند، قدرت واقعی پروتکل Iterator در توانایی آن برای تعریف پیمایشگرهای سفارشی برای ساختارهای داده خود شما نهفته است. این به شما امکان میدهد نحوه پیمایش و دسترسی به دادههای خود را کنترل کنید.
در اینجا نحوه ایجاد یک پیمایشگر سفارشی آمده است:
- یک کلاس یا شیء تعریف کنید که ساختار داده سفارشی شما را نمایندگی کند.
- متد
Symbol.iterator
را روی کلاس یا شیء خود پیادهسازی کنید. این متد باید یک شیء پیمایشگر برگرداند. - شیء پیمایشگر باید یک متد
next()
داشته باشد که یک شیء با ویژگیهایvalue
وdone
برگرداند.
مثال: ایجاد یک Iterator برای یک محدوده ساده
بیایید یک کلاس به نام Range
ایجاد کنیم که یک محدوده از اعداد را نشان میدهد. ما پروتکل Iterator را پیادهسازی خواهیم کرد تا امکان پیمایش اعداد در این محدوده فراهم شود.
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
[Symbol.iterator]() {
let currentValue = this.start;
const that = this; // Capture 'this' for use inside the iterator object
return {
next() {
if (currentValue <= that.end) {
return {
value: currentValue++,
done: false,
};
} else {
return {
value: undefined,
done: true,
};
}
},
};
}
}
const myRange = new Range(1, 5);
for (const number of myRange) {
console.log(number); // Output: 1, 2, 3, 4, 5
}
توضیح:
- کلاس
Range
مقادیرstart
وend
را در سازنده خود دریافت میکند. - متد
Symbol.iterator
یک شیء پیمایشگر برمیگرداند. این شیء پیمایشگر حالت خود را (currentValue
) و یک متدnext()
دارد. - متد
next()
بررسی میکند که آیاcurrentValue
در محدوده قرار دارد یا خیر. اگر چنین باشد، یک شیء با مقدار فعلی وdone
برابر باfalse
برمیگرداند. همچنینcurrentValue
را برای تکرار بعدی افزایش میدهد. - وقتی
currentValue
از مقدارend
فراتر رود، متدnext()
یک شیء باdone
برابر باtrue
برمیگرداند. - به استفاده از
that = this
توجه کنید. از آنجا که متد `next()` در یک scope متفاوت (توسط حلقه `for...of`) فراخوانی میشود، `this` در داخل `next()` به نمونه `Range` اشاره نمیکند. برای حل این مشکل، ما مقدار `this` (نمونه `Range`) را در `that` خارج از scope متد `next()` ذخیره کرده و سپس از `that` در داخل `next()` استفاده میکنیم.
مثال: ایجاد یک Iterator برای یک لیست پیوندی (Linked List)
بیایید مثال دیگری را در نظر بگیریم: ایجاد یک پیمایشگر برای ساختار داده لیست پیوندی. لیست پیوندی دنبالهای از گرهها (nodes) است که هر گره حاوی یک مقدار و یک ارجاع (اشارهگر) به گره بعدی در لیست است. آخرین گره در لیست به null (یا undefined) ارجاع میدهد.
class LinkedListNode {
constructor(value, next = null) {
this.value = value;
this.next = next;
}
}
class LinkedList {
constructor() {
this.head = null;
}
append(value) {
const newNode = new LinkedListNode(value);
if (!this.head) {
this.head = newNode;
return;
}
let current = this.head;
while (current.next) {
current = current.next;
}
current.next = newNode;
}
[Symbol.iterator]() {
let current = this.head;
return {
next() {
if (current) {
const value = current.value;
current = current.next;
return {
value: value,
done: false
};
} else {
return {
value: undefined,
done: true
};
}
}
};
}
}
// Example Usage:
const myList = new LinkedList();
myList.append("London");
myList.append("Paris");
myList.append("Tokyo");
for (const city of myList) {
console.log(city); // Output: London, Paris, Tokyo
}
توضیح:
- کلاس
LinkedListNode
یک گره واحد را در لیست پیوندی نشان میدهد که یکvalue
و یک ارجاع (next
) به گره بعدی را ذخیره میکند. - کلاس
LinkedList
خود لیست پیوندی را نشان میدهد. این کلاس شامل یک ویژگیhead
است که به اولین گره در لیست اشاره میکند. متدappend()
گرههای جدید را به انتهای لیست اضافه میکند. - متد
Symbol.iterator
یک شیء پیمایشگر ایجاد و برمیگرداند. این پیمایشگر گره فعلی که در حال بازدید است (current
) را ردیابی میکند. - متد
next()
بررسی میکند که آیا گره فعلی وجود دارد (current
تهی نیست). اگر وجود داشته باشد، مقدار را از گره فعلی بازیابی میکند، اشارهگرcurrent
را به گره بعدی منتقل میکند و یک شیء با مقدار وdone: false
برمیگرداند. - وقتی
current
تهی شود (یعنی به انتهای لیست رسیدهایم)، متدnext()
یک شیء باdone: true
برمیگرداند.
توابع مولد (Generator Functions)
توابع مولد روشی مختصرتر و زیباتر برای ایجاد پیمایشگرها ارائه میدهند. آنها از کلمه کلیدی yield
برای تولید مقادیر در صورت تقاضا استفاده میکنند.
یک تابع مولد با استفاده از سینتکس function*
تعریف میشود.
مثال: ایجاد یک Iterator با استفاده از یک تابع مولد
بیایید پیمایشگر Range
را با استفاده از یک تابع مولد بازنویسی کنیم:
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
*[Symbol.iterator]() {
for (let i = this.start; i <= this.end; i++) {
yield i;
}
}
}
const myRange = new Range(1, 5);
for (const number of myRange) {
console.log(number); // Output: 1, 2, 3, 4, 5
}
توضیح:
- متد
Symbol.iterator
اکنون یک تابع مولد است (به*
توجه کنید). - درون تابع مولد، از یک حلقه
for
برای پیمایش محدوده اعداد استفاده میکنیم. - کلمه کلیدی
yield
اجرای تابع مولد را متوقف کرده و مقدار فعلی (i
) را برمیگرداند. دفعه بعد که متدnext()
پیمایشگر فراخوانی شود، اجرا از جایی که متوقف شده بود (بعد از عبارتyield
) از سر گرفته میشود. - وقتی حلقه به پایان میرسد، تابع مولد به طور ضمنی
{ value: undefined, done: true }
را برمیگرداند که نشاندهنده پایان پیمایش است.
توابع مولد با مدیریت خودکار متد next()
و پرچم done
، ایجاد پیمایشگر را ساده میکنند.
مثال: مولد دنباله فیبوناچی
یک مثال عالی دیگر از استفاده از توابع مولد، تولید دنباله فیبوناچی است:
function* fibonacciSequence() {
let a = 0;
let b = 1;
while (true) {
yield a;
[a, b] = [b, a + b]; // Destructuring assignment for simultaneous update
}
}
const fibonacci = fibonacciSequence();
for (let i = 0; i < 10; i++) {
console.log(fibonacci.next().value); // Output: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
}
توضیح:
- تابع
fibonacciSequence
یک تابع مولد است. - این تابع دو متغیر
a
وb
را با دو عدد اول دنباله فیبوناچی (0 و 1) مقداردهی اولیه میکند. - حلقه
while (true)
یک دنباله بینهایت ایجاد میکند. - عبارت
yield a
مقدار فعلیa
را تولید میکند. - عبارت
[a, b] = [b, a + b]
با استفاده از تخصیص ساختارشکن (destructuring assignment)، مقادیرa
وb
را به طور همزمان به دو عدد بعدی در دنباله بهروزرسانی میکند. - عبارت
fibonacci.next().value
مقدار بعدی را از مولد بازیابی میکند. از آنجا که مولد بینهایت است، شما باید تعداد مقادیری را که از آن استخراج میکنید کنترل کنید. در این مثال، ما 10 مقدار اول را استخراج میکنیم.
مزایای استفاده از پروتکل Iterator
- استانداردسازی: پروتکل Iterator یک روش سازگار برای پیمایش ساختارهای داده مختلف فراهم میکند.
- انعطافپذیری: شما میتوانید پیمایشگرهای سفارشی متناسب با نیازهای خاص خود تعریف کنید.
- خوانایی: حلقه
for...of
کد پیمایش را خواناتر و مختصرتر میکند. - کارایی: پیمایشگرها میتوانند تنبل (lazy) باشند، به این معنی که فقط در صورت نیاز مقادیر را تولید میکنند، که میتواند عملکرد را برای مجموعههای داده بزرگ بهبود بخشد. به عنوان مثال، مولد دنباله فیبوناچی در بالا فقط مقدار بعدی را زمانی محاسبه میکند که `next()` فراخوانی شود.
- سازگاری: پیمایشگرها به طور یکپارچه با سایر ویژگیهای جاوا اسکریپت مانند سینتکس spread و ساختارشکنی (destructuring) کار میکنند.
تکنیکهای پیشرفته Iterator
ترکیب Iteratorها
شما میتوانید چندین پیمایشگر را در یک پیمایشگر واحد ترکیب کنید. این کار زمانی مفید است که نیاز به پردازش دادهها از چندین منبع به صورت یکپارچه دارید.
function* combineIterators(...iterables) {
for (const iterable of iterables) {
for (const item of iterable) {
yield item;
}
}
}
const array1 = [1, 2, 3];
const array2 = ["a", "b", "c"];
const string1 = "XYZ";
const combined = combineIterators(array1, array2, string1);
for (const value of combined) {
console.log(value); // Output: 1, 2, 3, a, b, c, X, Y, Z
}
در این مثال، تابع `combineIterators` هر تعداد پیمایشپذیر را به عنوان آرگومان میپذیرد. این تابع بر روی هر پیمایشپذیر تکرار کرده و هر آیتم را yield میکند. نتیجه یک پیمایشگر واحد است که تمام مقادیر از تمام پیمایشپذیرهای ورودی را تولید میکند.
فیلتر کردن و تبدیل Iteratorها
شما همچنین میتوانید پیمایشگرهایی ایجاد کنید که مقادیر تولید شده توسط یک پیمایشگر دیگر را فیلتر یا تبدیل میکنند. این به شما امکان میدهد دادهها را در یک خط لوله (pipeline) پردازش کنید و عملیات مختلفی را بر روی هر مقدار در حین تولید آن اعمال کنید.
function* filterIterator(iterable, predicate) {
for (const item of iterable) {
if (predicate(item)) {
yield item;
}
}
}
function* mapIterator(iterable, transform) {
for (const item of iterable) {
yield transform(item);
}
}
const numbers = [1, 2, 3, 4, 5, 6];
const evenNumbers = filterIterator(numbers, (x) => x % 2 === 0);
const squaredEvenNumbers = mapIterator(evenNumbers, (x) => x * x);
for (const value of squaredEvenNumbers) {
console.log(value); // Output: 4, 16, 36
}
در اینجا، `filterIterator` یک پیمایشپذیر و یک تابع предиکت (predicate) میگیرد. این تابع فقط آیتمهایی را که предиکت برای آنها `true` برمیگرداند، yield میکند. `mapIterator` یک پیمایشپذیر و یک تابع تبدیل میگیرد. این تابع نتیجه اعمال تابع تبدیل بر روی هر آیتم را yield میکند.
کاربردهای دنیای واقعی
پروتکل Iterator به طور گسترده در کتابخانهها و فریمورکهای جاوا اسکریپت استفاده میشود و در انواع برنامههای کاربردی دنیای واقعی، به ویژه هنگام کار با مجموعههای داده بزرگ یا عملیات ناهمزمان، ارزشمند است.
- پردازش داده: پیمایشگرها برای پردازش کارآمد مجموعههای داده بزرگ مفید هستند، زیرا به شما امکان میدهند با دادهها به صورت تکهتکه کار کنید بدون اینکه کل مجموعه داده را در حافظه بارگذاری کنید. تصور کنید در حال تجزیه یک فایل CSV بزرگ حاوی دادههای مشتری هستید. یک پیمایشگر میتواند به شما امکان دهد هر سطر را بدون بارگذاری کل فایل در حافظه به یکباره پردازش کنید.
- عملیات ناهمزمان: پیمایشگرها میتوانند برای مدیریت عملیات ناهمزمان، مانند واکشی داده از یک API، استفاده شوند. شما میتوانید از توابع مولد برای متوقف کردن اجرا تا زمان در دسترس قرار گرفتن داده و سپس از سرگیری با مقدار بعدی استفاده کنید.
- ساختارهای داده سفارشی: پیمایشگرها برای ایجاد ساختارهای داده سفارشی با الزامات پیمایش خاص، ضروری هستند. یک ساختار داده درختی را در نظر بگیرید. شما میتوانید یک پیمایشگر سفارشی برای پیمایش درخت به ترتیب خاص (مثلاً اول-عمق یا اول-سطح) پیادهسازی کنید.
- توسعه بازی: در توسعه بازی، پیمایشگرها میتوانند برای مدیریت اشیاء بازی، افکتهای ذرهای و سایر عناصر پویا استفاده شوند.
- کتابخانههای رابط کاربری: بسیاری از کتابخانههای UI از پیمایشگرها برای بهروزرسانی و رندر کارآمد کامپوننتها بر اساس تغییرات دادههای زیربنایی استفاده میکنند.
بهترین شیوهها (Best Practices)
Symbol.iterator
را به درستی پیادهسازی کنید: اطمینان حاصل کنید که متدSymbol.iterator
شما یک شیء پیمایشگر مطابق با پروتکل Iterator برمیگرداند.- پرچم
done
را با دقت مدیریت کنید: پرچمdone
برای نشان دادن پایان پیمایش حیاتی است. مطمئن شوید که آن را به درستی در متدnext()
خود تنظیم میکنید. - استفاده از توابع مولد را در نظر بگیرید: توابع مولد روشی مختصرتر و خواناتر برای ایجاد پیمایشگرها ارائه میدهند.
- از عوارض جانبی در
next()
خودداری کنید: متدnext()
باید عمدتاً بر روی بازیابی مقدار بعدی و بهروزرسانی حالت پیمایشگر تمرکز کند. از انجام عملیات پیچیده یا ایجاد عوارض جانبی در داخلnext()
خودداری کنید. - پیمایشگرهای خود را به طور کامل آزمایش کنید: پیمایشگرهای سفارشی خود را با مجموعههای داده و سناریوهای مختلف آزمایش کنید تا از رفتار صحیح آنها اطمینان حاصل کنید.
نتیجهگیری
پروتکل Iterator جاوا اسکریپت یک روش قدرتمند و انعطافپذیر برای پیمایش ساختارهای داده فراهم میکند. با درک پروتکلهای Iterable و Iterator و با استفاده از توابع مولد، میتوانید پیمایشگرهای سفارشی متناسب با نیازهای خاص خود ایجاد کنید. این به شما امکان میدهد تا به طور کارآمد با دادهها کار کنید، خوانایی کد را بهبود بخشید و عملکرد برنامههای خود را افزایش دهید. تسلط بر پیمایشگرها درک عمیقتری از قابلیتهای جاوا اسکریپت را باز میکند و شما را قادر میسازد کدی زیباتر و کارآمدتر بنویسید.