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 للتعامل مع البيانات الثنائية!