المكررات والمولدات
بروتوكول التكرار في JavaScript
توفر JavaScript طريقة موحدة للكائنات لتعريف أو تخصيص سلوك التكرار الخاص بها. يعرف هذا ببروتوكول التكرار (Iteration Protocol)، ويتكون من بروتوكولين فرعيين: بروتوكول القابلية للتكرار (Iterable Protocol) وبروتوكول المكرر (Iterator Protocol). هذه البروتوكولات ليست تطبيقات أو صيغ مدمجة -- إنها اتفاقيات يمكن لأي كائن اتباعها. عندما يتبع كائن هذه الاتفاقيات، يمكن استخدامه مع ميزات اللغة مثل حلقات for...of، وعامل الانتشار (...)، والتفكيك (Destructuring)، والعديد من واجهات البرمجة المدمجة التي تتوقع بيانات قابلة للتكرار.
قبل تقديم بروتوكول التكرار في ES6، لم تكن هناك طريقة موحدة للتكرار على هياكل البيانات المختلفة. المصفوفات استخدمت حلقات قائمة على الفهرس، والكائنات استخدمت for...in، وهياكل البيانات المخصصة تطلبت طرق تكرار خاصة بها. يحل بروتوكول التكرار هذه المشكلة من خلال توفير واجهة واحدة ومتسقة تشترك فيها جميع الكائنات القابلة للتكرار. فهم المكررات والمولدات أمر ضروري لكتابة كود نظيف وفعال يعمل بسلاسة مع آليات التكرار المدمجة في JavaScript.
Symbol.iterator -- مفتاح القابلية للتكرار
يتطلب بروتوكول القابلية للتكرار أن ينفذ الكائن دالة يمكن الوصول إليها من خلال المفتاح Symbol.iterator. يجب أن تعيد هذه الدالة كائن مكرر. عندما تحتاج JavaScript للتكرار على كائن (مثلا في حلقة for...of)، تستدعي دالة [Symbol.iterator]() الخاصة بالكائن للحصول على مكرر، ثم تستخدم ذلك المكرر لاسترداد القيم واحدة تلو الأخرى.
مثال: كيف تعمل الكائنات القابلة للتكرار المدمجة
// المصفوفات قابلة للتكرار
const colors = ['red', 'green', 'blue'];
// الحصول على المكرر من المصفوفة
const iterator = colors[Symbol.iterator]();
// استدعاء next() يدويا للحصول على القيم
console.log(iterator.next()); // { value: 'red', done: false }
console.log(iterator.next()); // { value: 'green', done: false }
console.log(iterator.next()); // { value: 'blue', done: false }
console.log(iterator.next()); // { value: undefined, done: true }
// السلاسل النصية أيضا قابلة للتكرار
const word = 'Hello';
const strIterator = word[Symbol.iterator]();
console.log(strIterator.next()); // { value: 'H', done: false }
console.log(strIterator.next()); // { value: 'e', done: false }
// Map و Set أيضا قابلة للتكرار
const map = new Map([['a', 1], ['b', 2]]);
const set = new Set([10, 20, 30]);
for (const [key, value] of map) {
console.log(key + ' = ' + value);
}
for (const item of set) {
console.log(item);
}
{}) ليست قابلة للتكرار افتراضيا. ليس لديها دالة [Symbol.iterator]. إذا حاولت استخدام for...of على كائن عادي، ستحصل على TypeError. ومع ذلك، يمكنك جعل أي كائن قابلا للتكرار من خلال تنفيذ دالة [Symbol.iterator]، كما سنرى قريبا.الكائنات القابلة للتكرار -- ما الذي يمكن تكراره
تحتوي JavaScript على عدة أنواع مدمجة قابلة للتكرار. فهم أي الأنواع قابلة للتكرار يساعدك في معرفة ما يعمل مع for...of وعامل الانتشار.
مثال: الكائنات القابلة للتكرار المدمجة
// 1. المصفوفات
for (const item of [1, 2, 3]) {
console.log(item); // 1, 2, 3
}
// 2. السلاسل النصية
for (const char of 'abc') {
console.log(char); // 'a', 'b', 'c'
}
// 3. Map
const map = new Map([['x', 10], ['y', 20]]);
for (const [key, val] of map) {
console.log(key, val); // 'x' 10, 'y' 20
}
// 4. Set
const set = new Set(['apple', 'banana', 'cherry']);
for (const fruit of set) {
console.log(fruit);
}
// 5. TypedArrays
const typed = new Uint8Array([255, 128, 0]);
for (const byte of typed) {
console.log(byte); // 255, 128, 0
}
// 6. كائن arguments (داخل الدوال)
function showArgs() {
for (const arg of arguments) {
console.log(arg);
}
}
showArgs('a', 'b', 'c');
// استخدام الكائنات القابلة للتكرار مع الانتشار والتفكيك
const letters = ['a', 'b', 'c', 'd', 'e'];
const [first, second, ...rest] = letters;
console.log(first); // 'a'
console.log(second); // 'b'
console.log(rest); // ['c', 'd', 'e']
const merged = [...new Set([1, 2, 2, 3, 3, 3])];
console.log(merged); // [1, 2, 3]
بروتوكول المكرر -- دالة next()
المكرر هو أي كائن ينفذ بروتوكول المكرر. يتطلب هذا البروتوكول دالة واحدة: next(). كل استدعاء لـ next() يعيد كائنا بخاصيتين: value (القيمة الحالية في التكرار) و done (قيمة منطقية تشير إلى ما إذا كان التكرار قد انتهى). عندما تكون done هي true، يكون التكرار قد اكتمل، وvalue تكون عادة undefined.
مثال: بناء مكرر من الصفر
function createCountdown(start) {
let current = start;
return {
next: function() {
if (current >= 0) {
return { value: current--, done: false };
}
return { value: undefined, done: true };
}
};
}
const countdown = createCountdown(3);
console.log(countdown.next()); // { value: 3, done: false }
console.log(countdown.next()); // { value: 2, done: false }
console.log(countdown.next()); // { value: 1, done: false }
console.log(countdown.next()); // { value: 0, done: false }
console.log(countdown.next()); // { value: undefined, done: true }
console.log(countdown.next()); // { value: undefined, done: true }
إنشاء كائنات قابلة للتكرار مخصصة
لجعل كائن يعمل مع for...of وعامل الانتشار والتفكيك، تحتاج لتنفيذ دالة [Symbol.iterator] عليه. يجب أن تعيد هذه الدالة كائن مكرر (كائن يحتوي على دالة next()). يتحكم المكرر في القيم التي يتم إنتاجها وبأي ترتيب.
مثال: كائن نطاق قابل للتكرار مخصص
class Range {
constructor(start, end, step = 1) {
this.start = start;
this.end = end;
this.step = step;
}
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
const step = this.step;
return {
next() {
if (current <= end) {
const value = current;
current += step;
return { value: value, done: false };
}
return { value: undefined, done: true };
}
};
}
}
const range = new Range(1, 10, 2);
// يعمل مع for...of
for (const num of range) {
console.log(num); // 1, 3, 5, 7, 9
}
// يعمل مع الانتشار
const numbers = [...new Range(0, 5)];
console.log(numbers); // [0, 1, 2, 3, 4, 5]
// يعمل مع التفكيك
const [a, b, c] = new Range(100, 500, 100);
console.log(a, b, c); // 100 200 300
// يعمل مع Array.from()
const evens = Array.from(new Range(2, 20, 2));
console.log(evens); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
مثال: قائمة مترابطة قابلة للتكرار
class Node {
constructor(value, next = null) {
this.value = value;
this.next = next;
}
}
class LinkedList {
constructor() {
this.head = null;
this.size = 0;
}
append(value) {
const newNode = new Node(value);
if (!this.head) {
this.head = newNode;
} else {
let current = this.head;
while (current.next) {
current = current.next;
}
current.next = newNode;
}
this.size++;
return this;
}
[Symbol.iterator]() {
let current = this.head;
return {
next() {
if (current) {
const value = current.value;
current = current.next;
return { value: value, done: false };
}
return { value: undefined, done: true };
}
};
}
}
const list = new LinkedList();
list.append('first').append('second').append('third');
// التكرار مع for...of
for (const item of list) {
console.log(item); // 'first', 'second', 'third'
}
// التحويل إلى مصفوفة مع الانتشار
const arr = [...list];
console.log(arr); // ['first', 'second', 'third']
// استخدام مع التفكيك
const [head, ...tail] = list;
console.log(head); // 'first'
console.log(tail); // ['second', 'third']
[Symbol.iterator]، يمكن لكائن المكرر المعاد أن يكون قابلا للتكرار بذاته من خلال إضافة دالة [Symbol.iterator] تعيد this. هذا نمط شائع يسمح باستخدام المكررات مباشرة في حلقات for...of. جميع المكررات المدمجة تتبع هذا النمط.المولدات -- بناء جملة function*
المولدات هي نوع خاص من الدوال في JavaScript يمكنها إيقاف التنفيذ مؤقتا واستئنافه لاحقا. يتم تعريفها باستخدام بناء جملة function* (لاحظ النجمة) وتستخدم الكلمة المفتاحية yield لإنتاج القيم. عند استدعائها، لا تنفذ دالة المولد جسمها فورا. بدلا من ذلك، تعيد كائن مولد يتوافق مع كل من بروتوكول القابلية للتكرار وبروتوكول المكرر. هذا يجعل المولدات أداة قوية بشكل لا يصدق لإنشاء مكررات بأقل كود ممكن.
مثال: أول مولد لك
function* simpleGenerator() {
console.log('Start');
yield 1;
console.log('After first yield');
yield 2;
console.log('After second yield');
yield 3;
console.log('End');
}
const gen = simpleGenerator();
// لم يتم تسجيل أي شيء بعد -- جسم الدالة لم ينفذ
console.log(gen.next());
// يسجل: "Start"
// يعيد: { value: 1, done: false }
console.log(gen.next());
// يسجل: "After first yield"
// يعيد: { value: 2, done: false }
console.log(gen.next());
// يسجل: "After second yield"
// يعيد: { value: 3, done: false }
console.log(gen.next());
// يسجل: "End"
// يعيد: { value: undefined, done: true }
الكلمة المفتاحية yield
الكلمة المفتاحية yield هي قلب المولدات. إنها توقف تنفيذ المولد مؤقتا وتنتج قيمة للمستدعي. عندما يتم استدعاء next() مرة أخرى، يستأنف التنفيذ من النقطة التالية مباشرة بعد yield الذي أوقفه. يمكن للكلمة المفتاحية yield أيضا استقبال قيم من خارج المولد، وهو ما سنستكشفه قريبا.
مثال: yield مع التعبيرات والمنطق
function* fibonacci() {
let a = 0;
let b = 1;
while (true) {
yield a;
const next = a + b;
a = b;
b = next;
}
}
const fib = fibonacci();
// الحصول على أول 10 أرقام فيبوناتشي
const first10 = [];
for (let i = 0; i < 10; i++) {
first10.push(fib.next().value);
}
console.log(first10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
// yield يمكنها استقبال قيم من next()
function* accumulator() {
let total = 0;
while (true) {
const value = yield total;
if (value === null || value === undefined) break;
total += value;
}
return total;
}
const acc = accumulator();
console.log(acc.next()); // { value: 0, done: false } -- أولي
console.log(acc.next(10)); // { value: 10, done: false }
console.log(acc.next(20)); // { value: 30, done: false }
console.log(acc.next(5)); // { value: 35, done: false }
console.log(acc.next(null)); // { value: 35, done: true }
next() على مولد يبدأ التنفيذ من بداية الدالة حتى أول yield. أي قيمة تمرر لأول استدعاء next() يتم تجاهلها لأنه لا يوجد تعبير yield ينتظر استقبالها. القيم الممررة لاستدعاءات next() اللاحقة تصبح نتيجة تعبير yield الذي أوقف المولد مؤقتا.المولد كمكرر
كائنات المولد تنفذ تلقائيا كلا من بروتوكول القابلية للتكرار وبروتوكول المكرر. هذا يعني أنه يمكنك استخدامها مباشرة مع حلقات for...of وعامل الانتشار والتفكيك، مما يجعل المولدات أبسط طريقة لإنشاء كائنات قابلة للتكرار مخصصة.
مثال: تكرار مدعوم بالمولدات
function* range(start, end, step = 1) {
for (let i = start; i <= end; i += step) {
yield i;
}
}
// استخدام مع for...of
for (const num of range(1, 5)) {
console.log(num); // 1, 2, 3, 4, 5
}
// استخدام مع الانتشار
const nums = [...range(0, 100, 10)];
console.log(nums); // [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
// استخدام مع التفكيك
const [first, second, third] = range(5, 50, 5);
console.log(first, second, third); // 5 10 15
// مقارنة: تنفيذ Range بمولد مقابل يدويا
class EasyRange {
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 EasyRange(1, 5);
console.log([...myRange]); // [1, 2, 3, 4, 5]
console.log([...myRange]); // [1, 2, 3, 4, 5] -- يعمل عدة مرات!
المولد مع return
يمكن للمولد استخدام عبارة return لإنهاء التكرار وتوفير قيمة نهائية اختياريا. عندما يعيد المولد قيمة، استدعاء next() الناتج ينتج { value: returnValue, done: true }. ومع ذلك، قيمة الإعادة لا يتم تضمينها عند استخدام for...of أو عامل الانتشار، لأنها تتوقف بمجرد أن تكون done هي true.
مثال: return في المولدات
function* generatorWithReturn() {
yield 'first';
yield 'second';
return 'final'; // هذا ينهي المولد
yield 'never reached'; // هذا السطر لا ينفذ أبدا
}
const gen = generatorWithReturn();
console.log(gen.next()); // { value: 'first', done: false }
console.log(gen.next()); // { value: 'second', done: false }
console.log(gen.next()); // { value: 'final', done: true }
console.log(gen.next()); // { value: undefined, done: true }
// مهم: for...of لا تتضمن قيمة الإعادة
for (const val of generatorWithReturn()) {
console.log(val);
}
// المخرجات: 'first', 'second'
// 'final' لا يتم تسجيلها لأن done هي true
// الانتشار أيضا يتجاهل قيمة الإعادة
console.log([...generatorWithReturn()]); // ['first', 'second']
// دالة return() يمكنها إغلاق مولد بالقوة
function* counting() {
let i = 0;
try {
while (true) {
yield i++;
}
} finally {
console.log('Generator closed at ' + i);
}
}
const counter = counting();
console.log(counter.next()); // { value: 0, done: false }
console.log(counter.next()); // { value: 1, done: false }
console.log(counter.return('done'));
// يسجل: "Generator closed at 2"
// يعيد: { value: 'done', done: true }
تفويض yield*
تعبير yield* يفوض التكرار إلى كائن قابل للتكرار أو مولد آخر. عندما يصادف المحرك yield*، يكرر على الكائن القابل للتكرار المعطى وينتج كل قيمة واحدة تلو الأخرى. هذا قوي لتركيب مولدات من أجزاء أصغر وتسطيح الكائنات القابلة للتكرار المتداخلة.
مثال: yield* للتفويض
function* inner() {
yield 'a';
yield 'b';
return 'inner done'; // قيمة إعادة المولد المفوض
}
function* outer() {
yield 1;
const result = yield* inner(); // تفويض إلى inner
console.log('inner returned: ' + result);
yield 2;
}
const gen = outer();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 'a', done: false }
console.log(gen.next()); // { value: 'b', done: false }
// يسجل: "inner returned: inner done"
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: undefined, done: true }
// yield* يعمل مع أي كائن قابل للتكرار، ليس فقط المولدات
function* concatIterables(...iterables) {
for (const iterable of iterables) {
yield* iterable;
}
}
const combined = [...concatIterables([1, 2], 'ab', new Set([true, false]))];
console.log(combined); // [1, 2, 'a', 'b', true, false]
// عملي: تسطيح المصفوفات المتداخلة بشكل تكراري
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]]]];
console.log([...flatten(nested)]); // [1, 2, 3, 4, 5, 6, 7]
yield* يقيم إلى قيمة إعادة المولد المفوض (القيمة الممررة لعبارة return الخاصة به). هذا يمكن أن يكون مفيدا لتركيب مولدات تتواصل بالنتائج مرة أخرى إلى المولد المفوض.المولدات اللانهائية
لأن المولدات كسولة -- إنها تنتج القيم فقط عند الطلب -- يمكنها تمثيل تسلسلات لانهائية دون استهلاك ذاكرة لانهائية. كل استدعاء لـ next() يحسب ويعيد قيمة واحدة فقط. هذا يجعل المولدات مثالية لتمثيل التسلسلات الرياضية وتدفقات البيانات المستمرة ومولدات المعرفات الفريدة ومصادر البيانات الأخرى التي قد تكون غير محدودة.
مثال: المولدات اللانهائية
// عداد لانهائي
function* naturalNumbers() {
let n = 1;
while (true) {
yield n++;
}
}
// مولد معرفات لانهائي
function* idGenerator(prefix = 'id') {
let counter = 0;
while (true) {
yield prefix + '_' + (++counter).toString().padStart(6, '0');
}
}
const ids = idGenerator('user');
console.log(ids.next().value); // "user_000001"
console.log(ids.next().value); // "user_000002"
console.log(ids.next().value); // "user_000003"
// يجب أن تحد من استهلاك المولدات اللانهائية!
function take(n, iterable) {
const result = [];
let count = 0;
for (const item of iterable) {
if (count >= n) break;
result.push(item);
count++;
}
return result;
}
console.log(take(5, naturalNumbers())); // [1, 2, 3, 4, 5]
// مولد أعداد أولية لانهائي
function* primes() {
const found = [];
let candidate = 2;
while (true) {
let isPrime = true;
for (const prime of found) {
if (prime * prime > candidate) break;
if (candidate % prime === 0) {
isPrime = false;
break;
}
}
if (isPrime) {
found.push(candidate);
yield candidate;
}
candidate++;
}
}
console.log(take(10, primes())); // [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
Array.from() على مولد لانهائي بدون تحديده أولا. تعبيرات مثل [...naturalNumbers()] ستحاول استهلاك التسلسل اللانهائي بأكمله، مما يتسبب في تجمد برنامجك ونفاد الذاكرة في النهاية. استخدم دائما دالة تحديد مثل take() أو حلقة for مع شرط break عند استهلاك المولدات اللانهائية.التقييم الكسول مع المولدات
التقييم الكسول يعني حساب القيم فقط عند الحاجة إليها، بدلا من حسابها جميعا مقدما. تدعم المولدات التقييم الكسول بشكل طبيعي لأنها تنتج القيم عند الطلب. هذا قيم بشكل خاص عند العمل مع مجموعات بيانات كبيرة أو حسابات مكلفة حيث قد لا تحتاج لجميع النتائج.
مثال: خط أنابيب كسول مع المولدات
// دوال مساعدة قائمة على المولدات لخطوط الأنابيب الكسولة
function* map(iterable, fn) {
for (const item of iterable) {
yield fn(item);
}
}
function* filter(iterable, predicate) {
for (const item of iterable) {
if (predicate(item)) {
yield item;
}
}
}
function* takeWhile(iterable, predicate) {
for (const item of iterable) {
if (!predicate(item)) return;
yield item;
}
}
function* chunk(iterable, size) {
let batch = [];
for (const item of iterable) {
batch.push(item);
if (batch.length === size) {
yield batch;
batch = [];
}
}
if (batch.length > 0) {
yield batch;
}
}
// بناء خط أنابيب كسول: أعداد طبيعية -> مربعات -> تصفية الفردية -> أخذ ما هو أقل من 1000
function* naturalNumbers() {
let n = 1;
while (true) yield n++;
}
const pipeline = takeWhile(
filter(
map(naturalNumbers(), function(n) { return n * n; }),
function(n) { return n % 2 !== 0; }
),
function(n) { return n < 1000; }
);
// لا يتم أي حساب حتى نستهلك خط الأنابيب
const result = [...pipeline];
console.log(result);
// [1, 9, 25, 49, 81, 121, 169, 225, 289, 361, 441, 529, 625, 729, 841, 961]
// مثال التقطيع
const batches = [...chunk([1, 2, 3, 4, 5, 6, 7], 3)];
console.log(batches); // [[1, 2, 3], [4, 5, 6], [7]]
المولدات للأنماط غير المتزامنة (مقدمة)
قبل إضافة async/await إلى JavaScript، استخدمت المولدات كأساس لكتابة كود غير متزامن يبدو متزامنا. بينما حل async/await محل هذا النمط إلى حد كبير، فإن فهم كيفية تعامل المولدات مع العمليات غير المتزامنة يمنحك نظرة أعمق في كيفية عمل async/await تحت الغطاء، لأنه مبني أساسا على نفس مفهوم إيقاف واستئناف التنفيذ.
مثال: المولدات والوعود (مفهوميا)
// محاكاة عمليات غير متزامنة
function fetchUser(id) {
return new Promise(function(resolve) {
setTimeout(function() {
resolve({ id: id, name: 'User ' + id });
}, 100);
});
}
function fetchPosts(userId) {
return new Promise(function(resolve) {
setTimeout(function() {
resolve([
{ id: 1, title: 'Post by user ' + userId },
{ id: 2, title: 'Another post by user ' + userId }
]);
}, 100);
});
}
// تدفق غير متزامن قائم على المولدات
function* userFlow(userId) {
const user = yield fetchUser(userId);
console.log('Got user:', user.name);
const posts = yield fetchPosts(user.id);
console.log('Got posts:', posts.length);
return { user: user, posts: posts };
}
// منفذ يشغل المولد خطوة بخطوة
function runGenerator(generatorFn) {
const args = Array.prototype.slice.call(arguments, 1);
const gen = generatorFn.apply(null, args);
return new Promise(function(resolve, reject) {
function step(nextFn) {
let result;
try {
result = nextFn();
} catch (e) {
return reject(e);
}
if (result.done) {
return resolve(result.value);
}
Promise.resolve(result.value).then(
function(value) {
step(function() { return gen.next(value); });
},
function(err) {
step(function() { return gen.throw(err); });
}
);
}
step(function() { return gen.next(); });
});
}
// الاستخدام -- يبدو متزامنا لكنه غير متزامن تماما
runGenerator(userFlow, 42).then(function(data) {
console.log('Final result:', data);
});
// المكافئ الحديث باستخدام async/await:
// async function userFlow(userId) {
// const user = await fetchUser(userId);
// const posts = await fetchPosts(user.id);
// return { user, posts };
// }
async/await. عندما تكتب async function، ينشئ محرك JavaScript أساسا آلة حالة مشابهة لمولد. الكلمة المفتاحية await تعمل مثل yield -- توقف التنفيذ مؤقتا حتى يتحقق الوعد، ثم تستأنف بالقيمة المحققة. فهم هذه العلاقة يعمق معرفتك بكيفية عمل JavaScript غير المتزامنة.أمثلة من العالم الحقيقي
المولدات والمكررات ليست مجرد مفاهيم نظرية. لديها تطبيقات عملية في تطوير JavaScript اليومي. إليك عدة أمثلة من العالم الحقيقي توضح قوتها.
مولد تسلسل فيبوناتشي
مثال: فيبوناتشي مع التخزين المؤقت
function* fibonacci() {
let prev = 0;
let curr = 1;
yield prev;
yield curr;
while (true) {
const next = prev + curr;
yield next;
prev = curr;
curr = next;
}
}
// الحصول على أرقام فيبوناتشي محددة
function nthFibonacci(n) {
let count = 0;
for (const fib of fibonacci()) {
if (count === n) return fib;
count++;
}
}
console.log(nthFibonacci(0)); // 0
console.log(nthFibonacci(1)); // 1
console.log(nthFibonacci(10)); // 55
console.log(nthFibonacci(20)); // 6765
// توليد نطاق من أرقام فيبوناتشي
function fibRange(start, count) {
const result = [];
let index = 0;
for (const fib of fibonacci()) {
if (index >= start && result.length < count) {
result.push(fib);
}
if (result.length === count) break;
index++;
}
return result;
}
console.log(fibRange(5, 8)); // [5, 8, 13, 21, 34, 55, 89, 144]
جالب بيانات مرقمة
مثال: مولد لبيانات API مرقمة
// محاكاة API مرقم
function fetchPage(page, pageSize) {
const totalItems = 47;
const start = (page - 1) * pageSize;
const items = [];
for (let i = start; i < Math.min(start + pageSize, totalItems); i++) {
items.push({ id: i + 1, name: 'Item ' + (i + 1) });
}
return Promise.resolve({
data: items,
page: page,
totalPages: Math.ceil(totalItems / pageSize),
hasMore: page * pageSize < totalItems
});
}
// مولد غير متزامن للبيانات المرقمة (ES2018)
async function* paginatedFetch(pageSize) {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetchPage(page, pageSize);
yield* response.data;
hasMore = response.hasMore;
page++;
}
}
// الاستخدام: التكرار على جميع العناصر عبر جميع الصفحات بسلاسة
async function processAllItems() {
let count = 0;
for await (const item of paginatedFetch(10)) {
count++;
if (count <= 3) {
console.log(item);
}
}
console.log('Total items processed: ' + count);
}
processAllItems();
// { id: 1, name: 'Item 1' }
// { id: 2, name: 'Item 2' }
// { id: 3, name: 'Item 3' }
// Total items processed: 47
مولد اجتياز الأشجار
مثال: مولد لاجتياز الأشجار
class TreeNode {
constructor(value, children = []) {
this.value = value;
this.children = children;
}
// اجتياز العمق أولا باستخدام مولد
*depthFirst() {
yield this.value;
for (const child of this.children) {
yield* child.depthFirst();
}
}
// اجتياز العرض أولا باستخدام مولد
*breadthFirst() {
const queue = [this];
while (queue.length > 0) {
const node = queue.shift();
yield node.value;
for (const child of node.children) {
queue.push(child);
}
}
}
}
const tree = new TreeNode('root', [
new TreeNode('A', [
new TreeNode('A1'),
new TreeNode('A2')
]),
new TreeNode('B', [
new TreeNode('B1', [
new TreeNode('B1a'),
new TreeNode('B1b')
]),
new TreeNode('B2')
]),
new TreeNode('C')
]);
console.log('Depth-first:');
console.log([...tree.depthFirst()]);
// ['root', 'A', 'A1', 'A2', 'B', 'B1', 'B1a', 'B1b', 'B2', 'C']
console.log('Breadth-first:');
console.log([...tree.breadthFirst()]);
// ['root', 'A', 'B', 'C', 'A1', 'A2', 'B1', 'B2', 'B1a', 'B1b']
خط أنابيب معالجة ذو حالة
مثال: محلل ملفات السجل مع المولدات
// محاكاة قراءة سطور السجل
function* logLines() {
yield '2025-01-15T10:30:00 INFO Server started on port 3000';
yield '2025-01-15T10:30:05 DEBUG Connection pool initialized';
yield '2025-01-15T10:31:00 ERROR Database connection timeout';
yield '2025-01-15T10:31:01 WARN Retrying database connection';
yield '2025-01-15T10:31:02 INFO Database connected';
yield '2025-01-15T10:32:00 ERROR File not found: /api/data';
yield '2025-01-15T10:33:00 INFO Request processed in 45ms';
}
// تحليل كل سطر إلى بيانات منظمة
function* parseLog(lines) {
for (const line of lines) {
const match = line.match(
/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})\s+(INFO|DEBUG|WARN|ERROR)\s+(.+)$/
);
if (match) {
yield {
timestamp: match[1],
level: match[2],
message: match[3]
};
}
}
}
// تصفية حسب مستوى السجل
function* filterByLevel(entries, level) {
for (const entry of entries) {
if (entry.level === level) {
yield entry;
}
}
}
// خط أنابيب كسول: قراءة -> تحليل -> تصفية
const errors = filterByLevel(parseLog(logLines()), 'ERROR');
for (const error of errors) {
console.log(error.timestamp + ': ' + error.message);
}
// 2025-01-15T10:31:00: Database connection timeout
// 2025-01-15T10:32:00: File not found: /api/data
دوال مساعدة للمكررات
بناء مكتبة صغيرة من أدوات المكررات يجعل المولدات أكثر قوة. هذه الدوال القابلة للتركيب تتيح لك بناء خطوط أنابيب معالجة بيانات معقدة تبقى كسولة وفعالة في الذاكرة.
مثال: أدوات مكررات قابلة لإعادة الاستخدام
// أخذ أول n عنصر
function* take(n, iterable) {
let count = 0;
for (const item of iterable) {
if (count >= n) return;
yield item;
count++;
}
}
// تخطي أول n عنصر
function* skip(n, iterable) {
let count = 0;
for (const item of iterable) {
if (count >= n) {
yield item;
}
count++;
}
}
// دمج عدة كائنات قابلة للتكرار معا
function* zip(...iterables) {
const iterators = iterables.map(function(it) {
return it[Symbol.iterator]();
});
while (true) {
const results = iterators.map(function(it) {
return it.next();
});
if (results.some(function(r) { return r.done; })) return;
yield results.map(function(r) { return r.value; });
}
}
// ترقيم -- إنتاج أزواج [فهرس، قيمة]
function* enumerate(iterable, start = 0) {
let index = start;
for (const item of iterable) {
yield [index++, item];
}
}
// أمثلة الاستخدام
function* naturals() {
let n = 1;
while (true) yield n++;
}
// أخذ 5 عناصر بدءا من العاشر
const page = [...take(5, skip(9, naturals()))];
console.log(page); // [10, 11, 12, 13, 14]
// دمج المصفوفات معا
const names = ['Alice', 'Bob', 'Charlie'];
const scores = [95, 87, 72];
const grades = ['A', 'B', 'C'];
for (const [name, score, grade] of zip(names, scores, grades)) {
console.log(name + ': ' + score + ' (' + grade + ')');
}
// Alice: 95 (A)
// Bob: 87 (B)
// Charlie: 72 (C)
// الترقيم
for (const [i, name] of enumerate(names, 1)) {
console.log(i + '. ' + name);
}
// 1. Alice
// 2. Bob
// 3. Charlie
تمرين عملي
ابنِ نظام معالجة بيانات كاملا باستخدام المكررات والمولدات. أولا، أنشئ صنف Range ينفذ [Symbol.iterator] باستخدام دالة مولد ويدعم معاملات start و end و step. تحقق من أنه يعمل مع for...of وعامل الانتشار والتفكيك. ثانيا، اكتب مولدا لانهائيا يسمى fibonacci() ينتج تسلسل فيبوناتشي. أنشئ دالة مساعدة take(n, iterable) لاستهلاك أول n قيمة بأمان. استخدمها للحصول على أول 15 رقم فيبوناتشي. ثالثا، نفذ خط أنابيب كسول قائم على المولدات مع دوال map و filter و reduce التي تقبل مولدات كمدخلات. استخدم خط الأنابيب لأخذ أول 20 عددا طبيعيا، وتربيعها، وتصفية القابلة للقسمة على ثلاثة، وحساب مجموعها. رابعا، أنشئ مولدا غير متزامن يسمى paginatedData() يحاكي جلب صفحات بيانات من API. يجب أن تعيد كل صفحة خمسة عناصر ويجب أن يكون هناك أربع صفحات إجمالا. استخدم for await...of للتكرار على جميع العناصر. خامسا، اكتب مولد flatten() يسطح المصفوفات المتداخلة بعمق بشكل تكراري باستخدام تفويض yield*. اختبره مع مصفوفة متداخلة بأربعة مستويات. أخيرا، أنشئ بنية بيانات شجرية ونفذ كلا من اجتياز العمق أولا والعرض أولا باستخدام المولدات.