العربية

دليل شامل لفهم وتطبيق بروتوكول المُكرِّر في JavaScript، يمكّنك من إنشاء مُكرِّرات مخصصة لتحسين التعامل مع البيانات.

إزالة الغموض عن بروتوكول المُكرِّر (Iterator) في JavaScript والمُكرِّرات المخصصة

يوفر بروتوكول المُكرِّر في JavaScript طريقة موحدة لاجتياز هياكل البيانات. إن فهم هذا البروتوكول يمكّن المطورين من العمل بكفاءة مع الكائنات القابلة للتكرار المدمجة مثل المصفوفات والسلاسل النصية، وإنشاء كائناتهم القابلة للتكرار المخصصة المصممة لهياكل بيانات ومتطلبات تطبيقات معينة. يقدم هذا الدليل استكشافًا شاملًا لبروتوكول المُكرِّر وكيفية تنفيذ المُكرِّرات المخصصة.

ما هو بروتوكول المُكرِّر؟

يحدد بروتوكول المُكرِّر كيفية التكرار على كائن ما، أي كيفية الوصول إلى عناصره بشكل تسلسلي. يتكون من جزأين: بروتوكول الكائن القابل للتكرار (Iterable) وبروتوكول المُكرِّر (Iterator).

بروتوكول الكائن القابل للتكرار (Iterable)

يعتبر الكائن قابلاً للتكرار (Iterable) إذا كان يحتوي على دالة بمفتاح Symbol.iterator. يجب أن تعيد هذه الدالة كائنًا يتوافق مع بروتوكول المُكرِّر (Iterator).

في جوهره، يعرف الكائن القابل للتكرار كيفية إنشاء مُكرِّر لنفسه.

بروتوكول المُكرِّر (Iterator)

يحدد بروتوكول المُكرِّر (Iterator) كيفية استرداد القيم من تسلسل. يعتبر الكائن مُكرِّرًا إذا كان لديه دالة next() تعيد كائنًا بخاصيتين:

تعتبر دالة next() هي المحرك الأساسي لبروتوكول المُكرِّر. كل استدعاء للدالة next() يتقدم بالمُكرِّر ويعيد القيمة التالية في التسلسل. عندما يتم إرجاع جميع القيم، تعيد next() كائنًا تكون فيه قيمة done هي true.

الكائنات المدمجة القابلة للتكرار

توفر JavaScript العديد من هياكل البيانات المدمجة التي هي بطبيعتها قابلة للتكرار. وتشمل هذه:

يمكن استخدام هذه الكائنات القابلة للتكرار مباشرة مع حلقة for...of، وصيغة النشر (spread syntax) (...)، وغيرها من البنى التي تعتمد على بروتوكول المُكرِّر.

مثال مع المصفوفات:


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 بنية قوية للتكرار على الكائنات القابلة للتكرار. فهي تتعامل تلقائيًا مع تعقيدات بروتوكول المُكرِّر، مما يسهل الوصول إلى القيم في التسلسل.

صيغة حلقة for...of هي:


for (const element of iterable) {
  // الكود الذي سيتم تنفيذه لكل عنصر
}

تقوم حلقة for...of باسترداد المُكرِّر من الكائن القابل للتكرار (باستخدام Symbol.iterator)، وتستدعي دالة next() الخاصة بالمُكرِّر بشكل متكرر حتى تصبح done تساوي true. في كل تكرار، يتم تعيين قيمة خاصية value التي أعادتها next() للمتغير element.

إنشاء مُكرِّرات مخصصة

بينما توفر JavaScript كائنات مدمجة قابلة للتكرار، تكمن القوة الحقيقية لبروتوكول المُكرِّر في قدرته على تحديد مُكرِّرات مخصصة لهياكل البيانات الخاصة بك. يتيح لك هذا التحكم في كيفية اجتياز بياناتك والوصول إليها.

إليك كيفية إنشاء مُكرِّر مخصص:

  1. عرّف فئة (class) أو كائنًا يمثل هيكل البيانات المخصص الخاص بك.
  2. نفّذ دالة Symbol.iterator على فئتك أو كائنك. يجب أن تعيد هذه الدالة كائن المُكرِّر.
  3. يجب أن يحتوي كائن المُكرِّر على دالة next() تعيد كائنًا يحتوي على خاصيتي value و done.

مثال: إنشاء مُكرِّر لنطاق بسيط

لنقم بإنشاء فئة تسمى Range تمثل نطاقًا من الأرقام. سنقوم بتنفيذ بروتوكول المُكرِّر للسماح بالتكرار على الأرقام في النطاق.


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
}

الشرح:

مثال: إنشاء مُكرِّر لقائمة مرتبطة (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*.

مثال: إنشاء مُكرِّر باستخدام دالة مولِّدة

لنعد كتابة مُكرِّر 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
}

الشرح:

فوائد استخدام بروتوكول المُكرِّر

تقنيات متقدمة للمُكرِّرات

دمج المُكرِّرات

يمكنك دمج عدة مُكرِّرات في مُكرِّر واحد. هذا مفيد عندما تحتاج إلى معالجة البيانات من مصادر متعددة بطريقة موحدة.


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` أي عدد من الكائنات القابلة للتكرار كوسائط. تقوم بالتكرار على كل كائن قابل للتكرار وتنتج كل عنصر. والنتيجة هي مُكرِّر واحد ينتج جميع القيم من جميع الكائنات القابلة للتكرار المُدخلة.

تصفية وتحويل المُكرِّرات

يمكنك أيضًا إنشاء مُكرِّرات تقوم بتصفية أو تحويل القيم التي ينتجها مُكرِّر آخر. يتيح لك هذا معالجة البيانات في خط أنابيب، وتطبيق عمليات مختلفة على كل قيمة أثناء إنشائها.


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`. يأخذ `mapIterator` كائنًا قابلاً للتكرار ودالة تحويل. ينتج نتيجة تطبيق دالة التحويل على كل عنصر.

تطبيقات في العالم الحقيقي

يستخدم بروتوكول المُكرِّر على نطاق واسع في مكتبات وأطر عمل JavaScript، وهو ذو قيمة في مجموعة متنوعة من التطبيقات الواقعية، خاصة عند التعامل مع مجموعات بيانات كبيرة أو عمليات غير متزامنة.

أفضل الممارسات

الخلاصة

يوفر بروتوكول المُكرِّر في JavaScript طريقة قوية ومرنة لاجتياز هياكل البيانات. من خلال فهم بروتوكولات الكائن القابل للتكرار والمُكرِّر، ومن خلال الاستفادة من الدوال المولِّدة، يمكنك إنشاء مُكرِّرات مخصصة مصممة لاحتياجاتك الخاصة. يتيح لك هذا العمل بكفاءة مع البيانات، وتحسين قابلية قراءة الكود، وتعزيز أداء تطبيقاتك. إن إتقان المُكرِّرات يفتح فهمًا أعمق لإمكانيات JavaScript ويمكّنك من كتابة كود أكثر أناقة وكفاءة.