JavaScript بعمق
المُكرِّرات والمولّدات: التسلسلات الكسولة في JavaScript
دليل عملي لبروتوكول التكرار في JavaScript — ما الذي يجعل شيئًا قابلًا للتكرار، وبروتوكول المُكرِّر، وfor...of ومعامل النشر، وكتابة دوال المولّد بـ function* وyield، والتسلسلات الكسولة واللانهائية، وتفويض المولّدات، والمُكرِّرات غير المتزامنة — مع تمارين عملية وحلولها.
في كل مرّة تكتب for...of، أو تنشر مصفوفة، أو تفكّك، تستخدم بروتوكول التكرار في JavaScript — عقد صغير أنيق يتيح لأي كائن وصف كيف يُكرَّر. والمولّدات (generators) هي الطريقة السهلة لإنتاج مُكرِّرات، وتفتح ما لا تستطيعه المصفوفات: تسلسلات كسولة تحسب القيم عند الطلب، شاملةً اللانهائية. هذا ركن من اللغة يبدو متقدّمًا لكنه يستند إلى واجهة بسيطة واحدة. (يبني على الكائنات والدوال من أساسيات JavaScript.)
الكائن قابل للتكرار إن كان له تابع يُرجِع مُكرِّرًا — كائنًا بـ
next()يردّ{ value, done }كل استدعاء.for...ofوالنشر والتفكيك كلها تتحدّث هذا البروتوكول الواحد، ولهذا تعمل بانتظام على المصفوفات والسلاسل وMap وSet وأي شيء تجعله قابلًا للتكرار.
بروتوكول التكرار
عقدان مترابطان. المُكرِّر كائن بتابع next() يُرجِع { value, done }. والقابل للتكرار كائن بتابع [Symbol.iterator]() يُرجِع مُكرِّرًا:
const iterator = {
current: 1,
last: 3,
next() {
return this.current <= this.last
? { value: this.current++, done: false }
: { value: undefined, done: true };
},
};
iterator.next(); // { value: 1, done: false }
iterator.next(); // { value: 2, done: false }
iterator.next(); // { value: 3, done: false }
iterator.next(); // { value: undefined, done: true }
القابلات للتكرار المدمجة — المصفوفات والسلاسل وMap وSet وarguments وNodeList — كلها تحمل [Symbol.iterator]. وهذا بالضبط ما يستدعيه for...of تحت الغطاء.
ماذا يمنحك "القابل للتكرار"
لأن صياغة كثيرة مبنية على هذا البروتوكول الواحد، جعل شيء قابلًا للتكرار يجعله يعمل في كل مكان دفعة:
const set = new Set([1, 2, 3]);
for (const x of set) { /* ... */ } // for...of
const arr = [...set]; // نشر
const [first, ...rest] = set; // تفكيك
Array.from(set); // Array.from
الأربعة تستخدم بروتوكول المُكرِّر. لاحظ أن for...of يعمل على القابلات للتكرار (مصفوفات، Sets، سلاسل) — لكن الكائنات العادية ليست قابلة للتكرار؛ استخدم Object.entries(obj) لتكرارها، أو for...in للمفاتيح.
المولّدات: مُكرِّرات بسهولة
كتابة المُكرِّرات يدويًّا مملّة. دالّة المولّد (function*) تُنتج مُكرِّرًا تلقائيًّا — كل yield يوقف الدالّة ويصدر قيمة؛ واستدعاء next() يستأنفها:
function* range(start, end) {
for (let i = start; i <= end; i++) {
yield i; // توقّف هنا، سلّم i
}
}
const r = range(1, 3);
r.next(); // { value: 1, done: false }
[...range(1, 3)]; // [1, 2, 3] — المولّدات قابلة للتكرار
for (const n of range(1, 5)) console.log(n); // 1 2 3 4 5
السحر هو الإيقاف: يعمل المولّد حتى yield، ثم يُعلَّق — حالته المحلّية مجمَّدة — حتى تطلب القيمة التالية. هذا ما يجعل التقييم الكسول ممكنًا.
التسلسلات الكسولة واللانهائية
لأن المولّدات تحسب قيمة واحدة في كل مرّة عند الطلب، تستطيع تمثيل تسلسلات لا تنتهي أبدًا — شيء لا تستطيع مصفوفة حرفيًّا حمله:
function* naturals() {
let n = 1;
while (true) yield n++; // لانهائي — لكنه يعمل فقط بقدر ما تسحب
}
const nums = naturals();
nums.next().value; // 1
nums.next().value; // 2
// خذ أول N من أي مُكرِّر (ربّما لانهائي)
function* take(iterable, count) {
let i = 0;
for (const x of iterable) {
if (i++ >= count) return;
yield x;
}
}
[...take(naturals(), 5)]; // [1, 2, 3, 4, 5]
الكسل هو المكسب الحقيقي: تستطيع وصف تسلسل غير محدود أو مكلف ودفع ثمن القيم التي تستهلكها فعلًا فقط — بلا مصفوفة مليون عنصر مبنية سلفًا لمجرّد قراءة أول عشرة.
تفويض المولّدات بـ yield*
yield* يفوّض إلى قابل تكرار آخر — يصدر كل قيمه في مكانه، ما يجعل تركيب المولّدات نظيفًا:
function* letters() { yield "a"; yield "b"; }
function* numbers() { yield 1; yield 2; }
function* combined() {
yield* letters(); // يصدر "a", "b"
yield* numbers(); // ثم 1, 2
}
[...combined()]; // ["a", "b", 1, 2]
جعل كائناتك قابلة للتكرار
أضف تابع [Symbol.iterator] — أسهل كمولّد — فينضمّ كائنك إلى for...of والنشر والبقية:
const range = {
from: 1,
to: 4,
*[Symbol.iterator]() { // تابع مولّد
for (let i = this.from; i <= this.to; i++) yield i;
},
};
[...range]; // [1, 2, 3, 4]
for (const n of range) { /* يعمل */ }
المُكرِّرات غير المتزامنة، باختصار
للتسلسلات التي تصل قيمها عبر الزمن (بيانات تدفّقية، واجهات مُصفّحة)، هناك نسخة غير متزامنة: async function* مع for await...of. كل قيمة وعد يُنتظَر أثناء التكرار:
async function* pages(url) {
let next = url;
while (next) {
const res = await fetch(next);
const data = await res.json();
yield data.items; // سلّم هذه الصفحة
next = data.nextPage;
}
}
for await (const items of pages("/api/list")) {
render(items); // عالِج كل صفحة فور وصولها
}
هذه الطريقة النظيفة لاستهلاك بيانات مُصفّحة أو متدفّقة — تُقرأ الحلقة طبيعيًّا بينما ينتظر كل تكرار القطعة التالية.
الأخطاء الشائعة
- محاولة
for...ofعلى كائن عادي — الكائنات ليست قابلة للتكرار؛ استخدمObject.entries/keys/values. - نشر أو
Array.fromلمولّد لانهائي — لا ينتهي أبدًا؛ استخدم حدًّا بنمطtake. - نسيان أن المولّد لمرّة واحدة — حين يُستنفَد، انتهى؛ استدعِ دالّة المولّد ثانيةً لتشغيل جديد.
- توقّع أن يُرجِع
yieldقيمة دون استخدام صيغةnext(value)ثنائية الاتجاه. - استخدام
functionبدلfunction*، فيكونyieldخطأ صياغة. - الخلط بين
yield(أصدِر واحدة) وyield*(فوّض إلى قابل تكرار كامل). - اللجوء للمولّدات حيث مصفوفة عادية أو
mapأبسط — تتألّق للحالات الكسولة أو اللانهائية.
تمارين
جرّب كلًّا منها قبل فتح الحل.
تمرين 1 — مولّد مدى
اكتب range(start, end) يصدر كل عدد صحيح شاملًا، واجمع range(1, 4) في مصفوفة.
إظهار الحل
function* range(start, end) {
for (let i = start; i <= end; i++) yield i;
}
[...range(1, 4)]; // [1, 2, 3, 4]
كل yield يصدر قيمة واحدة ويوقف؛ والنشر يقود next() حتى done، جامعًا كل القيم المُصدَرة.
تمرين 2 — خذ من اللانهاية
بمعطى مولّد naturals() لانهائي، احصل على أول 3 قيم بأمان.
إظهار الحل
function* naturals() { let n = 1; while (true) yield n++; }
const it = naturals();
const first3 = [it.next().value, it.next().value, it.next().value]; // [1, 2, 3]
تسحب ثلاث قيم بالضبط بـ next(). لا تنشر naturals() مباشرةً أبدًا — الكسل يعني أنه يعمل بقدر ما تطلب فقط.
تمرين 3 — اجعل كائنًا قابلًا للتكرار
اجعل const deck = { suits: ["♠","♥"], ranks: ["A","K"] } يكرّر كل تركيبات النوع+الرتبة.
إظهار الحل
const deck = {
suits: ["♠", "♥"],
ranks: ["A", "K"],
*[Symbol.iterator]() {
for (const s of this.suits)
for (const r of this.ranks)
yield r + s;
},
};
[...deck]; // ["A♠", "K♠", "A♥", "K♥"]
التابع المولّد يكرّر المصفوفتين بحلقة متداخلة ويصدر كل تركيبة، فيعمل deck الآن مع النشر وfor...of.
النموذج الذهني الذي تحتفظ به
تكرار JavaScript يستند إلى عقد واحد: القابل للتكرار يكشف [Symbol.iterator]()، الذي يُرجِع مُكرِّرًا يصدر next() الخاص به { value, done }. for...of والنشر والتفكيك وArray.from كلها تتحدّثه، فأي قابل للتكرار يعمل معها كلها. المولّدات (function* + yield) المصنع السهل للمُكرِّرات — وقوّتها الخارقة الإيقاف، الذي يتيح تسلسلات كسولة تحسب عند الطلب وحتى لانهائية، ما دمت تسحب ما تحتاج فقط. استخدم yield* لتركيب المولّدات، وأضف [Symbol.iterator] لجعل كائناتك قابلة للتكرار، والجأ إلى async function* + for await...of للقيم التي تصل عبر الزمن. الجأ إليها تحديدًا حين يهمّ الكسل أو التدفّق — للبيانات المحدودة في الذاكرة، تبقى المصفوفات العادية وmap أبسط.