بروتوكول الـ Iterator
كثير من ميزات JavaScript — مثل for...of و عامل الـ spread (...) و الـ destructuring و Array.from و Promise.all — تشترك جميعها في آلية واحدة خفية تحت الغطاء: iterator protocol. وبمجرّد أن تفهم هذا البروتوكول، ستبدو لك كل هذه الميزات مجرّد أوجه مختلفة للفكرة نفسها.
الـ iterator ببساطة هو أي كائن (object) يملك دالة next() تُعيد { value, done }:
استدعِ next() بشكل متكرر. كل استدعاء يُرجع القيمة التالية مع العلم تم. وعندما تصبح قيمة تم هي true، تكون السلسلة قد انتهت. هذا هو البروتوكول بأكمله — أربع ضغطات مفاتيح وقيمة منطقية واحدة.
الفرق بين iterable و iterator
هناك مفهوم ثانٍ مرتبط بما سبق. الـ iterable هو أي شيء يعرف كيف يُنتج iterator، وذلك عبر دالة (method) مخزَّنة تحت مفتاح خاص اسمه Symbol.iterator.
المصفوفات كائنات قابلة للتكرار (iterables). فلو ناديت numbers[Symbol.iterator]() سيرجع لك iterator جديد تمامًا. ونفس الكلام ينطبق على النصوص وMap وSet وحتى arguments — كلها iterables، ولهذا السبب تشتغل معها حلقة for...of بدون مشاكل.
التفريق هنا مهم جدًا: الـ iterable هو المجموعة نفسها، بينما الـ iterator هو المؤشر الذي يتنقل داخلها. ويمكنك أن تطلب من أي iterable عددًا غير محدود من المؤشرات المستقلة.
لماذا تعمل حلقة for...of؟
حلقة for...of ما هي إلا صياغة مبسّطة فوق الـ iterator protocol. فهي خلف الكواليس تستدعي Symbol.iterator، ثم تواصل مناداة next() حتى تصبح قيمة تم مساوية لـ true:
كلٌّ من الـ spread والـ destructuring يقوم بنفس الفكرة — يمرّ على الـ iterator حتى ينتهي:
أي كائن تبنيه وتطبّق فيه Symbol.iterator بيشتغل تلقائيًا مع كل المزايا دي من غير أي مجهود إضافي.
إزاي تعمل iterable مخصص في javascript
خلينا نبني كائن range يُرجِع أرقام من start لحد end:
بعض الأشياء التي تستحق الملاحظة:
[Symbol.iterator]()يستخدم اسم method محسوب. المفتاح هنا هو الـ symbol نفسه، وليس النص"Symbol.iterator".- كل استدعاء لـ
[Symbol.iterator]()يُرجع iterator جديدًا تمامًا ولهcurrentخاص به. هذا ما يسمح لك بالمرور علىrangeمرتين دون أن "يُستهلك". - الـ iterator المُرجَع يحتاج فقط إلى
next(). لا أكثر.
هذا الأسلوب يعمل، لكنه طويل ومُرهِق. وهناك طريقة أفضل بكثير.
الدخول إلى عالم الـ Generators
تُعرَّف generator function باستخدام function* (لاحظ النجمة). فبدلًا من أن تعمل حتى النهاية، يمكنها أن تتوقف مؤقتًا عند تعبير yield ثم تستأنف لاحقًا. واستدعاؤها لا يُشغّل جسم الدالة — بل يُعيد generator object يعمل في آنٍ واحد كـ iterator و iterable:
كل استدعاء لـ next() يُشغّل جسم الدالة حتى يصل إلى yield، فيتوقف مؤقتًا ويُعيد { value, done: false }. وعند انتهاء الدالة، تحصل على { value: undefined, done: true }.
وبما أن الـ generators تُعدّ iterables، فهي تعمل مع كل ما تناولناه في القسم السابق:
إعادة كتابة range باستخدام generator
قارن النسخة الطويلة السابقة بهذه:
هذا كل شيء. وجود * قبل [Symbol.iterator] يحوّل التابع إلى generator method. والسطر yield i يغنيك عن كتابة كائن الـ iterator يدوياً بالكامل. لا حاجة إلى next ولا إلى تم ولا قلق من أخطاء الفهرسة بفارق واحد — مجرد حلقة عادية استبدلنا فيها push بـ yield.
لهذا السبب وُجدت الـ generators في javascript. فهي تحوّل مهمة "اكتب iterator" إلى مجرد "اكتب دالة تُنتج القيم عبر yield".
الفرق بين yield و return
yield يُعلّق التنفيذ، أما return فينهيه. يمكنك استخدام yield عدداً غير محدود من المرات، وفي كل مرة يستأنف الـ generator من النقطة التي توقف عندها:
عبارة return داخل الـ generator تظهر على هيئة { value: "done", done: true } في الاستدعاء الذي ينهيه. أما for...of وعامل الـ spread فـ يتجاهلان هذه القيمة المُرجعة، لأنهما يستهلكان فقط العناصر التي تكون فيها تم مساوية لـ false. لذا لا تستخدم return value لتمرير عنصر أخير داخل حلقة، لأنه ببساطة لن يُقرأ.
التسلسلات الكسولة واللانهائية في javascript
تُنتج الـ generators في javascript القيم عند الطلب، قيمةً تلو الأخرى. هذا يعني أنك تستطيع تمثيل تسلسلات يستحيل التعامل معها كمصفوفات:
الحلقة هنا هي while (true) حرفيًا، ومع ذلك البرنامج ينتهي بشكل طبيعي — والسبب إن الـ generator ما بيتقدّم خطوة إلا لما حد يطلب منه القيمة التالية. تقدر تاخد أول N عنصر وتوقف، والباقي ببساطة ما بينفّذ أبدًا:
take نفسها generator بتلفّ generator تانية. وتركيب الـ generators بهذا الأسلوب من أهم مميزاتها — كل قطعة صغيرة تؤدي مهمة واحدة فقط.
تفويض المهمة باستخدام yield*
لما تحتاج generator تُخرج كل القيم من iterable آخر، الكلمة yield* بتفوّض العمل له:
yield* يشتغل مع أي iterable — سواء كانت مصفوفات أو sets أو generators أخرى — ويمرّر العناصر واحدًا تلو الآخر. تقدر تعتبره النسخة المكافئة لعامل الـ spread لكن على مستوى الـ iterators.
لمحة سريعة عن الـ Async Generators
أي generator معرَّف بصيغة async function* يقدر يعمل yield لقيم تحتاج وقتًا لتجهيزها — وهذا مفيد لما تكون بتستقبل بيانات متدفقة من API أو تقرأ ملفًا على دفعات. وتستهلكه باستخدام for await...of:
async function* paginate(url) {
let next = url;
while (next) {
const res = await fetch(next);
const page = await res.json();
for (const item of page.items) yield item;
next = page.nextUrl;
}
}
for await (const item of paginate("/api/users")) {
console.log(item);
}
الكود هذا ما يشتغل هنا مباشرة (لأنه يحتاج endpoint حقيقي)، لكن يكفيك تعرف أن الشكل موجود. بمجرد ما تستوعب الـ generators العادية، راح تلاقي الـ async generators هي نفس الفكرة بالضبط مع إضافة await هنا وهناك.
متى تستخدم الـ generator؟
استخدمها في الحالات التالية:
- لما تكون السلسلة لا نهائية أو يحتمل تكون كذلك — مثل IDs وtimestamps وتأخيرات إعادة المحاولة.
- لما يكون توليد كل القيم مكلّف والمستهلك ممكن يوقف في المنتصف.
- لما تطبّق
Symbol.iteratorعلى object مخصّص. دائماً تقريباً تطلع أقصر من كتابة كائن{ next() }بيدك. - لما تبي تركّب تحويلات متدفقة (
take,filter,map) بدون ما تبني arrays وسيطة في الذاكرة.
خلّك مع المصفوفات العادية إذا كانت البيانات موجودة بالذاكرة وحجمها صغير. الـ generators ما تجي ببلاش — الآلية اللي توقف الدالة وترجّعها فيها تكلفة، كما أن تتبّع الأخطاء (stack traces) داخل كود الـ generator ممكن يكون أصعب في القراءة.
التالي: الـ Symbols
Symbol.iterator هو أول symbol يقابله أغلب الناس، لكنه أبعد ما يكون الوحيد. الـ Symbols نوع بدائي (primitive) مصمَّم تحديداً لهذا النوع من نقاط التوسيع — مفاتيح فريدة تسمح للغة ولكودك الخاص بالارتباط بالكائنات دون التصادم مع أسماء الخصائص العادية. هذا موضوع الصفحة القادمة.
الأسئلة الشائعة
ما الفرق بين iterable وiterator في JavaScript؟
الـ iterable هو أي كائن يملك الميثود Symbol.iterator التي تُرجع iterator. أما الـ iterator فهو الكائن الذي يُنتج القيم فعلياً عبر ميثود next() التي تُرجع { value, done }. المصفوفات والسلاسل النصية وMap وSet كلها iterables، وعند استدعاء Symbol.iterator عليها تحصل على iterator تستطيع التنقل بين قيمه خطوة خطوة.
ما هي دالة الـ generator في JavaScript؟
هي دالة مُعرَّفة بـ function* تُنتج القيم بشكل كسول (lazy) باستخدام yield. استدعاؤها لا يُنفّذ جسم الدالة مباشرةً، بل يُرجع كائن generator يعمل كـ iterator وiterable في آن واحد. كل استدعاء لـ next() يُشغّل الكود حتى أقرب yield، ثم يتوقف ويُعيد القيمة.
ما الفرق بين yield وreturn داخل الـ generator؟
الكلمة yield توقف الـ generator مؤقتاً وتُعيد قيمة، لكن الدالة تستطيع استكمال العمل من حيث توقفت عند استدعاء next() التالي. أما return فتُنهي الـ generator نهائياً، حيث تضبط done: true ولن تخرج أي قيم بعدها. تستطيع استخدام yield مرات كثيرة، بينما return تُستخدم مرة واحدة فقط بشكل فعّال.
متى أستخدم generator بدلاً من مصفوفة؟
عندما تكون السلسلة لا نهائية، أو حسابها مكلف، أو تحتاج بعض القيم فقط منها. الـ generator يُنتج العناصر واحداً تلو الآخر عند الطلب، فتستطيع تمثيل تدفق لا نهائي من المعرّفات أو نتائج API مُجزّأة (pagination) دون تحميل كل شيء في الذاكرة. أما إذا كانت لديك مصفوفة صغيرة ثابتة، فاستخدم المصفوفة مباشرةً.