فارسی

راهنمای جامع درک و پیاده‌سازی پروتکل Iterator جاوا اسکریپت که شما را قادر می‌سازد تا برای مدیریت بهتر داده‌ها، Iteratorهای سفارشی ایجاد کنید.

ابهام‌زدایی از پروتکل Iterator جاوا اسکریپت و Iteratorهای سفارشی

پروتکل Iterator جاوا اسکریپت یک روش استاندارد برای پیمایش ساختارهای داده فراهم می‌کند. درک این پروتکل به توسعه‌دهندگان این امکان را می‌دهد که به طور کارآمد با پیمایش‌پذیرهای داخلی (built-in iterables) مانند آرایه‌ها و رشته‌ها کار کنند و پیمایش‌پذیرهای سفارشی خود را متناسب با ساختارهای داده و نیازهای برنامه خاص خود ایجاد کنند. این راهنما یک بررسی جامع از پروتکل Iterator و نحوه پیاده‌سازی پیمایشگرهای سفارشی ارائه می‌دهد.

پروتکل Iterator چیست؟

پروتکل Iterator تعریف می‌کند که چگونه یک شیء می‌تواند پیمایش شود، یعنی چگونه عناصر آن به صورت متوالی قابل دسترسی هستند. این پروتکل از دو بخش تشکیل شده است: پروتکل Iterable و پروتکل Iterator.

پروتکل Iterable

یک شیء زمانی Iterable (پیمایش‌پذیر) در نظر گرفته می‌شود که متدی با کلید Symbol.iterator داشته باشد. این متد باید یک شیء مطابق با پروتکل Iterator را برگرداند.

در اصل، یک شیء پیمایش‌پذیر می‌داند که چگونه یک پیمایشگر (iterator) برای خود ایجاد کند.

پروتکل Iterator

پروتکل Iterator نحوه بازیابی مقادیر از یک دنباله را تعریف می‌کند. یک شیء زمانی پیمایشگر (iterator) در نظر گرفته می‌شود که متد next() را داشته باشد که یک شیء با دو ویژگی برمی‌گرداند:

متد next() بخش اصلی پروتکل Iterator است. هر فراخوانی next()، پیمایشگر را به جلو می‌برد و مقدار بعدی در دنباله را برمی‌گرداند. وقتی تمام مقادیر برگردانده شدند، next() یک شیء با done برابر با true برمی‌گرداند.

پیمایش‌پذیرهای داخلی (Built-in)

جاوا اسکریپت چندین ساختار داده داخلی ارائه می‌دهد که به طور ذاتی پیمایش‌پذیر هستند. این موارد عبارتند از:

این پیمایش‌پذیرها می‌توانند مستقیماً با حلقه 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 در توانایی آن برای تعریف پیمایشگرهای سفارشی برای ساختارهای داده خود شما نهفته است. این به شما امکان می‌دهد نحوه پیمایش و دسترسی به داده‌های خود را کنترل کنید.

در اینجا نحوه ایجاد یک پیمایشگر سفارشی آمده است:

  1. یک کلاس یا شیء تعریف کنید که ساختار داده سفارشی شما را نمایندگی کند.
  2. متد Symbol.iterator را روی کلاس یا شیء خود پیاده‌سازی کنید. این متد باید یک شیء پیمایشگر برگرداند.
  3. شیء پیمایشگر باید یک متد 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
}

توضیح:

مثال: ایجاد یک 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
}

توضیح:

توابع مولد (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
}

توضیح:

توابع مولد با مدیریت خودکار متد 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
}

توضیح:

مزایای استفاده از پروتکل Iterator

تکنیک‌های پیشرفته 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 به طور گسترده در کتابخانه‌ها و فریم‌ورک‌های جاوا اسکریپت استفاده می‌شود و در انواع برنامه‌های کاربردی دنیای واقعی، به ویژه هنگام کار با مجموعه‌های داده بزرگ یا عملیات ناهمزمان، ارزشمند است.

بهترین شیوه‌ها (Best Practices)

نتیجه‌گیری

پروتکل Iterator جاوا اسکریپت یک روش قدرتمند و انعطاف‌پذیر برای پیمایش ساختارهای داده فراهم می‌کند. با درک پروتکل‌های Iterable و Iterator و با استفاده از توابع مولد، می‌توانید پیمایشگرهای سفارشی متناسب با نیازهای خاص خود ایجاد کنید. این به شما امکان می‌دهد تا به طور کارآمد با داده‌ها کار کنید، خوانایی کد را بهبود بخشید و عملکرد برنامه‌های خود را افزایش دهید. تسلط بر پیمایشگرها درک عمیق‌تری از قابلیت‌های جاوا اسکریپت را باز می‌کند و شما را قادر می‌سازد کدی زیباتر و کارآمدتر بنویسید.