مقدمه
پیمایش دادهها (iteration) یکی از پایهایترین مفاهیم برنامهنویسی است. هر وقت با یک کالکشن
مانند آرایه، Map یا Set
یا رشته کار میکنیم، معمولاً باید عناصر آن را یکییکی بخوانیم یا پردازش کنیم.
جاوااسکریپت در نسخههای جدید خود استانداردی برای پیمایش حرفهای با نام Iterator Pattern
معرفی کرده که کنترل کامل، سازگاری و توسعهپذیری را برای کار با دادهها فراهم میکند.
بدون پیمایش، دسترسی به عناصر یک مجموعه ممکن نیست یا باید به روشهای پیچیده متوسل شویم. iteration
امکان پیمایش استاندارد و خوانا برای همه نوع داده را میدهد و پایه بسیاری از تکنیکهای پیشرفتهتر
مثل generator و async iteration است.
مفهوم Iteration در جاوااسکریپت
iteration یعنی اجرای یک عملیات (مانند خواندن یا پردازش) روی تمام عناصر یک مجموعه داده. این کار
به کمک حلقهها ممکن است، اما جاوااسکریپت با پروتکل iterable، این مفهوم را به سطحی بالاتر برده
است تا بتوانیم هر ساختار دادهای را به شکل سفارشی پیمایش کنیم.
مثلاً آرایهها را میتوان با حلقه for...of پیمایش کرد، چون به طور پیشفرض iterable هستند:
JAVASCRIPT
let arr = [1, 2, 3];
for (let item of arr) {
console.log(item);
}
اما اگر بخواهیم یک شیء معمولی (که آرایه یا رشته یا کالکشن دیگری نیست) را هم قابل پیمایش کنیم،
باید پروتکل
iterable را خودمان پیادهسازی کنیم. برای این کار، کافیست متد [Symbol.iterator] را به شیء
اضافه کنیم تا
بتوانیم با for...of روی آن پیمایش انجام دهیم. این کار به ما اجازه میدهد هر ساختار
دادهای را به
روشی استاندارد و قابل استفاده در تمام ابزارهای جاوااسکریپت پیمایش کنیم.
پروتکل iterable
پس، هر شیء که متد [Symbol.iterator] داشته باشد، طبق استاندارد iterable است و میتواند
توسط
حلقه for...of یا ابزارهای دیگر پیمایش شود. آرایهها، رشتهها، Map و Set به
طور پیشفرض iterable
هستند، اما ما میتوانیم برای اشیاء دیگر هم این استاندارد را پیادهسازی کنیم.
به تفاوت بین iterable و iterator دقت کنید:
یک شیء iterable متدی به نام [Symbol.iterator] دارد که باید یک شیء iterator بازگرداند. شیء
iterator متدی به نام next() دارد که در هر بار فراخوانی، یک شیء با
دو پراپرتی value و done
بازمیگرداند. زمانی که done برابر true شود، پیمایش به انتها رسیده است.
به مثال زیر دقت کنید.
JAVASCRIPT
let myIterable = {
[Symbol.iterator]() {
let step = 0;
return {
next() {
step++;
if (step === 1) {
return { value: "one", done: false };
} else if (step === 2) {
return { value: "two", done: false };
}
return { done: true };
}
}
}
};
for (let val of myIterable) {
console.log(val);
}
در این مثال، شیء myIterable با پیادهسازی متد [Symbol.iterator] به یک iterable
تبدیل شده است. هر بار که حلقه for...of اجرا میشود، متد next فراخوانی شده و مقدار
value را
بازمیگرداند تا زمانی
که done برابر true شود. این ساختار به شما اجازه میدهد کنترل کامل بر نحوه تولید و
پایان دادهها داشته باشید.
با iteratorها میتوانید انواع دادهها (حتی دادههای بینهایت) را به صورت یکسان و استاندارد
پیمایش کنید. کافی است رفتار مورد نیازتان را در متد next تعریف کنید تا حلقه
for...of یا
سایر
ابزارها به راحتی از آن استفاده کنند.
به عنوان یک مثال دیگر، کد زیر یک شیء range را نشان میدهد که اعداد ۱ تا ۵ را تولید
میکند. با
حلقه for...of یا به طور دستی میتوانید عناصر را پیمایش کنید.
JAVASCRIPT
let range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from;
let last = this.to;
return {
next() {
return current <= last
? { value: current++, done: false }
: { done: true };
}
}
}
};
for (let num of range) {
console.log(num);
}
کاربرد حلقه for...of و ارتباط آن با Iterator
حالا که با مفهوم iterator و الگوی iterable آشنا شدیم، میتوانیم حلقه for...of را بهتر درک
کنیم.
حلقه for...of برای پیمایش هر شیء iterable طراحی شده و خودش متد [Symbol.iterator]
را صدا
میزند. سپس در هر گام، متد next اجرا میشود و مقدار value را دریافت میکند. زمانی
که done به
true رسید، حلقه پایان مییابد.
حلقه for...of میتواند به راحتی روی آرایهها، Map، Set، رشتهها و هر شیء iterable اجرا
شود. با
این قابلیت، کدهای شما خواناتر، سادهتر و حرفهایتر خواهد بود.
اما این امکان هم وجود دارد که پیمایش را خودمان با متد next و حلقه دلخواه انجام دهیم. این
کار کنترل کامل بر روند
پیمایش و مدیریت خطا یا توقف دلخواه را فراهم میکند. مثال زیر را ببینید.
JAVASCRIPT
let arr = [10, 20, 30];
let it = arr[Symbol.iterator]();
let result = it.next();
while (!result.done) {
console.log(result.value);
result = it.next();
}
سعی کنید همیشه از for...of برای پیمایش مجموعههای بزرگ و پیچیده استفاده کنید و در صورت نیاز به
کنترل بیشتر، iterator سفارشی خود را پیادهسازی نمایید. این کار کدنویسی شما را بسیار منعطف و
حرفهایتر میکند.
خاتمه زودهنگام پیمایش
گاهی لازم است پیمایش را قبل از رسیدن به انتهای کالکشن متوقف کنیم. این کار معمولاً با دستور
break در حلقه for...of انجام میشود. در این حالت، حلقه متوقف میشود و دیگر متد
next() فراخوانی نمیشود. مثال زیر را ببینید.
JAVASCRIPT
let arr = [1, 2, 3, 4, 5];
for (let num of arr) {
if (num === 3) break;
console.log(num);
}
در این مثال، پیمایش با رسیدن به مقدار ۳ متوقف میشود و عناصر بعدی خوانده نمیشوند.
اما اگر بخواهیم عمل خاتمهی زودهنگام را روی iterator سفارشی خودمان اجرا کنیم،
میتوانیم متد اختیاری return() را به شیء iterator اضافه کنیم تا
هنگام توقف زودهنگام فراخوانی شود.
مثال زیر را ببینید.
JAVASCRIPT
let myIterable = {
[Symbol.iterator]() {
let step = 0;
return {
next() {
step++;
if (step <= 3) {
return { value: step, done: false };
}
return { done: true };
},
return() {
console.log("Early Terminated!");
return { done: true };
}
}
}
};
for (let val of myIterable) {
if (val === 2) break;
console.log(val);
}
در این مثال، با توقف زودهنگام حلقه، متد return() اجرا میشود و
میتوانید عملیات پاکسازی
یا ثبت لاگ انجام دهید. این قابلیت برای مدیریت منابع یا بستن ارتباطات در پیمایشهای پیچیده بسیار
مفید است.
الگوی iterator یکی از مهمترین مفاهیم جاوااسکریپت مدرن است و پایه بسیاری از قابلیتهای
پیشرفتهتر مانند generator و async iteration به شمار میآید که در درسهای بعد به آنها
میپردازیم. با تسلط بر این مفهوم، کنترل کامل بر
نحوه پیمایش و تولید داده در برنامههای حرفهای خواهید داشت.