JavaScript المتقدم (ES6+)

Iterators و Generators - التحكم المتقدم في التكرار

13 دقيقة الدرس 24 من 40

Iterators و Generators - التحكم المتقدم في التكرار

Iterators و Generators هي ميزات قوية قُدمت في ES6 تمنحك تحكماً دقيقاً في التكرار. تحدد Iterators كيفية المرور على الكائنات، بينما توفر Generators طريقة بسيطة لإنشاء Iterators وتنفيذ أنماط التقييم الكسول.

فهم Iterators

Iterator هو كائن ينفذ بروتوكول iterator بوجود طريقة next() تُرجع كائناً بخاصيتين:

  • value: القيمة التالية في التسلسل
  • done: قيمة منطقية تشير إلى ما إذا كان التكرار قد اكتمل
حقيقة مهمة: Arrays و Strings و Maps و Sets و NodeLists كلها قابلة للتكرار مدمجة. الكائنات العادية ليست قابلة للتكرار افتراضياً.

إنشاء Iterator مخصص

يمكنك إنشاء Iterators مخصصة بتنفيذ بروتوكول iterator:

// Iterator يدوي const numberIterator = { current: 1, last: 5, next() { if (this.current <= this.last) { return { value: this.current++, done: false }; } else { return { done: true }; } } }; console.log(numberIterator.next()); // {value: 1, done: false} console.log(numberIterator.next()); // {value: 2, done: false} console.log(numberIterator.next()); // {value: 3, done: false} console.log(numberIterator.next()); // {value: 4, done: false} console.log(numberIterator.next()); // {value: 5, done: false} console.log(numberIterator.next()); // {done: true}

جعل الكائنات قابلة للتكرار

لجعل كائن قابلاً للتكرار (قابل للاستخدام مع for...of)، نفّذ طريقة Symbol.iterator:

// كائن قابل للتكرار const range = { start: 1, end: 5, [Symbol.iterator]() { let current = this.start; const last = this.end; return { next() { if (current <= last) { return { value: current++, done: false }; } return { done: true }; } }; } }; // الآن يمكننا استخدام for...of for (const num of range) { console.log(num); // 1, 2, 3, 4, 5 } // ومشغل النشر console.log([...range]); // [1, 2, 3, 4, 5] // و Array.from() console.log(Array.from(range)); // [1, 2, 3, 4, 5] // والتفكيك const [first, second, ...rest] = range; console.log(first, second, rest); // 1 2 [3, 4, 5]

مقدمة إلى Generators

Generators هي دوال خاصة يمكنها إيقاف التنفيذ مؤقتاً واستئنافه لاحقاً. تنشئ تلقائياً Iterators:

// دالة Generator (لاحظ النجمة *) function* simpleGenerator() { yield 1; yield 2; yield 3; } const gen = simpleGenerator(); console.log(gen.next()); // {value: 1, done: false} console.log(gen.next()); // {value: 2, done: false} console.log(gen.next()); // {value: 3, done: false} console.log(gen.next()); // {value: undefined, done: true} // Generators قابلة للتكرار for (const value of simpleGenerator()) { console.log(value); // 1, 2, 3 }
ملاحظة بناء الجملة: يمكن وضع النجمة (*) بجوار function أو اسم الدالة أو كليهما: function* gen() أو function *gen() أو function*gen() كلها صحيحة.

بناء جملة وميزات Generator

توفر Generators طريقة أنظف لإنشاء Iterators:

// Generator يستبدل Iterator اليدوي function* rangeGenerator(start, end) { for (let i = start; i <= end; i++) { yield i; } } console.log([...rangeGenerator(1, 5)]); // [1, 2, 3, 4, 5] // Generator مع بيان return function* generatorWithReturn() { yield 1; yield 2; return 3; // قيمة الإرجاع مضمنة لكن done تصبح true yield 4; // لن يتم الوصول إليها أبداً } const gen2 = generatorWithReturn(); console.log(gen2.next()); // {value: 1, done: false} console.log(gen2.next()); // {value: 2, done: false} console.log(gen2.next()); // {value: 3, done: true} console.log(gen2.next()); // {value: undefined, done: true} // ملاحظة: for...of لا تتضمن قيمة الإرجاع console.log([...generatorWithReturn()]); // [1, 2]

تمرير القيم إلى Generators

يمكن لـ Generators استقبال القيم من خلال طريقة next():

function* twoWayGenerator() { const x = yield 'أعطني X'; console.log('X هو:', x); const y = yield 'أعطني Y'; console.log('Y هو:', y); return x + y; } const gen = twoWayGenerator(); console.log(gen.next()); // {value: 'أعطني X', done: false} console.log(gen.next(10)); // X هو: 10, {value: 'أعطني Y', done: false} console.log(gen.next(20)); // Y هو: 20, {value: 30, done: true} // مثال عملي: مولد المعرفات function* idGenerator() { let id = 1; while (true) { const increment = yield id; if (increment !== undefined) { id += increment; } else { id++; } } } const ids = idGenerator(); console.log(ids.next().value); // 1 console.log(ids.next().value); // 2 console.log(ids.next(10).value); // 12 console.log(ids.next().value); // 13

تفويض Generator مع yield*

استخدم yield* للتفويض إلى generator آخر أو قابل للتكرار:

function* gen1() { yield 1; yield 2; } function* gen2() { yield 3; yield 4; } function* combinedGenerator() { yield* gen1(); // التفويض إلى gen1 yield* gen2(); // التفويض إلى gen2 yield 5; } console.log([...combinedGenerator()]); // [1, 2, 3, 4, 5] // التفويض إلى مصفوفة function* numberAndLetters() { yield* [1, 2, 3]; yield* 'ABC'; } console.log([...numberAndLetters()]); // [1, 2, 3, 'A', 'B', 'C'] // Generator تكراري مع تفويض function* flatten(arr) { for (const item of arr) { if (Array.isArray(item)) { yield* flatten(item); // تفويض تكراري } else { yield item; } } } const nested = [1, [2, 3, [4, 5]], 6, [7, [8, 9]]]; console.log([...flatten(nested)]); // [1, 2, 3, 4, 5, 6, 7, 8, 9]

التسلسلات اللانهائية مع Generators

تتفوق Generators في إنشاء تسلسلات لانهائية مع التقييم الكسول:

// مولد أرقام لانهائي function* infiniteNumbers() { let n = 1; while (true) { yield n++; } } // أخذ أول N قيم function take(iterable, n) { const result = []; for (const value of iterable) { result.push(value); if (result.length === n) break; } return result; } console.log(take(infiniteNumbers(), 5)); // [1, 2, 3, 4, 5] // تسلسل فيبوناتشي function* fibonacci() { let [a, b] = [0, 1]; while (true) { yield a; [a, b] = [b, a + b]; } } console.log(take(fibonacci(), 10)); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] // مولد أرقام عشوائية function* randomNumbers() { while (true) { yield Math.random(); } } console.log(take(randomNumbers(), 3)); // [0.234..., 0.876..., 0.123...]
تحذير: كن حذراً مع Generators اللانهائية! استخدم دائماً شرط إنهاء (مثل take()) أو بيان break لتجنب الحلقات اللانهائية.

أمثلة Generator العملية

// المثال 1: مكرر الصفحات function* paginate(items, pageSize) { for (let i = 0; i < items.length; i += pageSize) { yield items.slice(i, i + pageSize); } } const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; for (const page of paginate(data, 3)) { console.log(page); } // [1, 2, 3] // [4, 5, 6] // [7, 8, 9] // [10] // المثال 2: عمليات متسلسلة شبيهة بغير المتزامن function* taskRunner() { console.log('بدء المهام...'); yield 'اكتملت المهمة 1'; console.log('المعالجة...'); yield 'اكتملت المهمة 2'; console.log('الإنهاء...'); yield 'اكتملت جميع المهام'; } for (const status of taskRunner()) { console.log(status); } // المثال 3: اجتياز الشجرة class TreeNode { constructor(value, children = []) { this.value = value; this.children = children; } *[Symbol.iterator]() { yield this.value; for (const child of this.children) { yield* child; } } } const tree = new TreeNode(1, [ new TreeNode(2, [ new TreeNode(4), new TreeNode(5) ]), new TreeNode(3, [ new TreeNode(6) ]) ]); console.log([...tree]); // [1, 2, 4, 5, 3, 6] // المثال 4: map/filter كسول function* map(iterable, fn) { for (const value of iterable) { yield fn(value); } } function* filter(iterable, predicate) { for (const value of iterable) { if (predicate(value)) { yield value; } } } const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; const doubled = map(numbers, x => x * 2); const evens = filter(doubled, x => x % 2 === 0); console.log([...evens]); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

طرق Generator: return() و throw()

لدى Generators طرق إضافية للتحكم في التدفق:

function* controlledGenerator() { try { yield 1; yield 2; yield 3; } catch (error) { console.log('تم اصطياد الخطأ:', error); } finally { console.log('التنظيف'); } } // استخدام return() const gen1 = controlledGenerator(); console.log(gen1.next()); // {value: 1, done: false} console.log(gen1.return(99)); // التنظيف, {value: 99, done: true} console.log(gen1.next()); // {value: undefined, done: true} // استخدام throw() const gen2 = controlledGenerator(); console.log(gen2.next()); // {value: 1, done: false} console.log(gen2.throw(new Error('عفواً!'))); // تم اصطياد الخطأ: Error: عفواً! // التنظيف // {value: undefined, done: true}

Async Generators

قدمت ES2018 Async Generators للعمل مع التكرار غير المتزامن:

async function* asyncGenerator() { yield await Promise.resolve(1); yield await Promise.resolve(2); yield await Promise.resolve(3); } // استخدام for await...of async function example() { for await (const value of asyncGenerator()) { console.log(value); // 1, 2, 3 } } // Async generator لتقسيم API إلى صفحات async function* fetchPages(url) { let page = 1; let hasMore = true; while (hasMore) { const response = await fetch(`${url}?page=${page}`); const data = await response.json(); yield data.items; hasMore = data.hasNextPage; page++; } } // الاستخدام async function loadAllPages() { for await (const items of fetchPages('/api/data')) { console.log('عناصر الصفحة:', items); } }

تمرين عملي:

التحدي: أنشئ generator ينتج أعداداً أولية.

function* primeNumbers() { yield 2; let num = 3; while (true) { let isPrime = true; for (let i = 2; i <= Math.sqrt(num); i++) { if (num % i === 0) { isPrime = false; break; } } if (isPrime) { yield num; } num += 2; // تخطي الأرقام الزوجية } } // الاختبار function take(iterable, n) { const result = []; for (const value of iterable) { result.push(value); if (result.length === n) break; } return result; } console.log(take(primeNumbers(), 10)); // [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

جربه بنفسك: أنشئ generator للمربعات الكاملة أو قوى 2.

الملخص

في هذا الدرس، تعلمت:

  • تنفذ Iterators بروتوكول iterator بطريقة next()
  • تصبح الكائنات قابلة للتكرار بتنفيذ Symbol.iterator
  • Generators (function*) هي دوال خاصة تنشئ Iterators
  • yield توقف تنفيذ Generator مؤقتاً وتنتج قيماً
  • تدعم Generators الاتصال ثنائي الاتجاه عبر next(value)
  • yield* يفوض إلى Generators أخرى أو قوابل للتكرار
  • تتفوق Generators في التسلسلات اللانهائية والتقييم الكسول
  • تمكّن Async Generators التكرار غير المتزامن مع for await...of
التالي: في الدرس التالي، سنستكشف Typed Arrays و ArrayBuffer للتعامل مع البيانات الثنائية!