دليل شامل لفهم وتطبيق بروتوكول المُكرِّر في JavaScript، يمكّنك من إنشاء مُكرِّرات مخصصة لتحسين التعامل مع البيانات.
إزالة الغموض عن بروتوكول المُكرِّر (Iterator) في JavaScript والمُكرِّرات المخصصة
يوفر بروتوكول المُكرِّر في JavaScript طريقة موحدة لاجتياز هياكل البيانات. إن فهم هذا البروتوكول يمكّن المطورين من العمل بكفاءة مع الكائنات القابلة للتكرار المدمجة مثل المصفوفات والسلاسل النصية، وإنشاء كائناتهم القابلة للتكرار المخصصة المصممة لهياكل بيانات ومتطلبات تطبيقات معينة. يقدم هذا الدليل استكشافًا شاملًا لبروتوكول المُكرِّر وكيفية تنفيذ المُكرِّرات المخصصة.
ما هو بروتوكول المُكرِّر؟
يحدد بروتوكول المُكرِّر كيفية التكرار على كائن ما، أي كيفية الوصول إلى عناصره بشكل تسلسلي. يتكون من جزأين: بروتوكول الكائن القابل للتكرار (Iterable) وبروتوكول المُكرِّر (Iterator).
بروتوكول الكائن القابل للتكرار (Iterable)
يعتبر الكائن قابلاً للتكرار (Iterable) إذا كان يحتوي على دالة بمفتاح Symbol.iterator
. يجب أن تعيد هذه الدالة كائنًا يتوافق مع بروتوكول المُكرِّر (Iterator).
في جوهره، يعرف الكائن القابل للتكرار كيفية إنشاء مُكرِّر لنفسه.
بروتوكول المُكرِّر (Iterator)
يحدد بروتوكول المُكرِّر (Iterator) كيفية استرداد القيم من تسلسل. يعتبر الكائن مُكرِّرًا إذا كان لديه دالة next()
تعيد كائنًا بخاصيتين:
value
: القيمة التالية في التسلسل.done
: قيمة منطقية (boolean) تشير إلى ما إذا كان المُكرِّر قد وصل إلى نهاية التسلسل. إذا كانتdone
تساويtrue
، فيمكن حذف خاصيةvalue
.
تعتبر دالة next()
هي المحرك الأساسي لبروتوكول المُكرِّر. كل استدعاء للدالة next()
يتقدم بالمُكرِّر ويعيد القيمة التالية في التسلسل. عندما يتم إرجاع جميع القيم، تعيد next()
كائنًا تكون فيه قيمة done
هي true
.
الكائنات المدمجة القابلة للتكرار
توفر JavaScript العديد من هياكل البيانات المدمجة التي هي بطبيعتها قابلة للتكرار. وتشمل هذه:
- المصفوفات (Arrays)
- السلاسل النصية (Strings)
- الكائنات من نوع Map
- الكائنات من نوع Set
- كائن arguments الخاص بالدالة
- المصفوفات المكتوبة (TypedArrays)
يمكن استخدام هذه الكائنات القابلة للتكرار مباشرة مع حلقة for...of
، وصيغة النشر (spread syntax) (...
)، وغيرها من البنى التي تعتمد على بروتوكول المُكرِّر.
مثال مع المصفوفات:
const myArray = ["apple", "banana", "cherry"];
for (const item of myArray) {
console.log(item); // Output: apple, banana, cherry
}
مثال مع السلاسل النصية:
const myString = "Hello";
for (const char of myString) {
console.log(char); // Output: H, e, l, l, o
}
حلقة for...of
تعتبر حلقة for...of
بنية قوية للتكرار على الكائنات القابلة للتكرار. فهي تتعامل تلقائيًا مع تعقيدات بروتوكول المُكرِّر، مما يسهل الوصول إلى القيم في التسلسل.
صيغة حلقة for...of
هي:
for (const element of iterable) {
// الكود الذي سيتم تنفيذه لكل عنصر
}
تقوم حلقة for...of
باسترداد المُكرِّر من الكائن القابل للتكرار (باستخدام Symbol.iterator
)، وتستدعي دالة next()
الخاصة بالمُكرِّر بشكل متكرر حتى تصبح done
تساوي true
. في كل تكرار، يتم تعيين قيمة خاصية value
التي أعادتها next()
للمتغير element
.
إنشاء مُكرِّرات مخصصة
بينما توفر JavaScript كائنات مدمجة قابلة للتكرار، تكمن القوة الحقيقية لبروتوكول المُكرِّر في قدرته على تحديد مُكرِّرات مخصصة لهياكل البيانات الخاصة بك. يتيح لك هذا التحكم في كيفية اجتياز بياناتك والوصول إليها.
إليك كيفية إنشاء مُكرِّر مخصص:
- عرّف فئة (class) أو كائنًا يمثل هيكل البيانات المخصص الخاص بك.
- نفّذ دالة
Symbol.iterator
على فئتك أو كائنك. يجب أن تعيد هذه الدالة كائن المُكرِّر. - يجب أن يحتوي كائن المُكرِّر على دالة
next()
تعيد كائنًا يحتوي على خاصيتيvalue
وdone
.
مثال: إنشاء مُكرِّر لنطاق بسيط
لنقم بإنشاء فئة تسمى Range
تمثل نطاقًا من الأرقام. سنقوم بتنفيذ بروتوكول المُكرِّر للسماح بالتكرار على الأرقام في النطاق.
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
[Symbol.iterator]() {
let currentValue = this.start;
const that = this; // Capture 'this' for use inside the iterator object
return {
next() {
if (currentValue <= that.end) {
return {
value: currentValue++,
done: false,
};
} else {
return {
value: undefined,
done: true,
};
}
},
};
}
}
const myRange = new Range(1, 5);
for (const number of myRange) {
console.log(number); // Output: 1, 2, 3, 4, 5
}
الشرح:
- تأخذ فئة
Range
قيمتيstart
وend
في المُنشئ (constructor). - تعيد دالة
Symbol.iterator
كائن مُكرِّر. يحتوي كائن المُكرِّر هذا على حالته الخاصة (currentValue
) ودالةnext()
. - تتحقق دالة
next()
مما إذا كانتcurrentValue
ضمن النطاق. إذا كانت كذلك، فإنها تعيد كائنًا يحتوي على القيمة الحالية وتكونdone
قيمتهاfalse
. كما أنها تزيدcurrentValue
للتكرار التالي. - عندما تتجاوز
currentValue
قيمةend
، تعيد دالةnext()
كائنًا تكون فيهdone
قيمتهاtrue
. - لاحظ استخدام
that = this
. نظرًا لأن دالة `next()` يتم استدعاؤها في نطاق مختلف (بواسطة حلقة `for...of`)، فإن `this` داخل `next()` لن تشير إلى نسخة `Range`. لحل هذه المشكلة، نلتقط قيمة `this` (نسخة `Range`) في المتغير `that` خارج نطاق `next()` ثم نستخدم `that` داخل `next()`.
مثال: إنشاء مُكرِّر لقائمة مرتبطة (Linked List)
لنفكر في مثال آخر: إنشاء مُكرِّر لهيكل بيانات القائمة المرتبطة. القائمة المرتبطة هي سلسلة من العقد (nodes)، حيث تحتوي كل عقدة على قيمة ومرجع (مؤشر) إلى العقدة التالية في القائمة. العقدة الأخيرة في القائمة لديها مرجع إلى null (أو undefined).
class LinkedListNode {
constructor(value, next = null) {
this.value = value;
this.next = next;
}
}
class LinkedList {
constructor() {
this.head = null;
}
append(value) {
const newNode = new LinkedListNode(value);
if (!this.head) {
this.head = newNode;
return;
}
let current = this.head;
while (current.next) {
current = current.next;
}
current.next = newNode;
}
[Symbol.iterator]() {
let current = this.head;
return {
next() {
if (current) {
const value = current.value;
current = current.next;
return {
value: value,
done: false
};
} else {
return {
value: undefined,
done: true
};
}
}
};
}
}
// Example Usage:
const myList = new LinkedList();
myList.append("London");
myList.append("Paris");
myList.append("Tokyo");
for (const city of myList) {
console.log(city); // Output: London, Paris, Tokyo
}
الشرح:
- تمثل فئة
LinkedListNode
عقدة واحدة في القائمة المرتبطة، حيث تخزنvalue
ومرجعًا (next
) إلى العقدة التالية. - تمثل فئة
LinkedList
القائمة المرتبطة نفسها. تحتوي على خاصيةhead
، التي تشير إلى العقدة الأولى في القائمة. تضيف دالةappend()
عقدًا جديدة إلى نهاية القائمة. - تقوم دالة
Symbol.iterator
بإنشاء وإرجاع كائن مُكرِّر. يتتبع هذا المُكرِّر العقدة الحالية التي تتم زيارتها (current
). - تتحقق دالة
next()
مما إذا كانت هناك عقدة حالية (current
ليست null). إذا كانت موجودة، فإنها تسترد القيمة من العقدة الحالية، وتقدم مؤشرcurrent
إلى العقدة التالية، وتعيد كائنًا يحتوي على القيمة وdone: false
. - عندما تصبح
current
قيمتها null (مما يعني أننا وصلنا إلى نهاية القائمة)، تعيد دالةnext()
كائنًا تكون فيهdone
قيمتهاtrue
.
الدوال المولِّدة (Generator Functions)
توفر الدوال المولِّدة طريقة أكثر إيجازًا وأناقة لإنشاء المُكرِّرات. تستخدم الكلمة المفتاحية yield
لإنتاج القيم عند الطلب.
يتم تعريف الدالة المولِّدة باستخدام الصيغة function*
.
مثال: إنشاء مُكرِّر باستخدام دالة مولِّدة
لنعد كتابة مُكرِّر Range
باستخدام دالة مولِّدة:
class Range {
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 Range(1, 5);
for (const number of myRange) {
console.log(number); // Output: 1, 2, 3, 4, 5
}
الشرح:
- أصبحت دالة
Symbol.iterator
الآن دالة مولِّدة (لاحظ علامة*
). - داخل الدالة المولِّدة، نستخدم حلقة
for
للتكرار على نطاق الأرقام. - توقف الكلمة المفتاحية
yield
تنفيذ الدالة المولِّدة مؤقتًا وتعيد القيمة الحالية (i
). في المرة التالية التي يتم فيها استدعاء دالةnext()
للمُكرِّر، يُستأنف التنفيذ من حيث توقف (بعد عبارةyield
). - عندما تنتهي الحلقة، تعيد الدالة المولِّدة ضمنيًا
{ value: undefined, done: true }
، مما يشير إلى نهاية التكرار.
تبسط الدوال المولِّدة عملية إنشاء المُكرِّرات عن طريق التعامل مع دالة next()
وعلامة done
تلقائيًا.
مثال: مولِّد متتالية فيبوناتشي
مثال رائع آخر على استخدام الدوال المولِّدة هو إنشاء متتالية فيبوناتشي:
function* fibonacciSequence() {
let a = 0;
let b = 1;
while (true) {
yield a;
[a, b] = [b, a + b]; // Destructuring assignment for simultaneous update
}
}
const fibonacci = fibonacciSequence();
for (let i = 0; i < 10; i++) {
console.log(fibonacci.next().value); // Output: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
}
الشرح:
- الدالة
fibonacciSequence
هي دالة مولِّدة. - تقوم بتهيئة متغيرين،
a
وb
، إلى أول رقمين في متتالية فيبوناتشي (0 و 1). - تنشئ حلقة
while (true)
تسلسلاً لا نهائيًا. - تنتج عبارة
yield a
القيمة الحالية للمتغيرa
. - تقوم عبارة
[a, b] = [b, a + b]
بتحديثa
وb
في وقت واحد إلى الرقمين التاليين في التسلسل باستخدام التعيين بالتفكيك (destructuring assignment). - يسترد التعبير
fibonacci.next().value
القيمة التالية من المولِّد. نظرًا لأن المولِّد لا نهائي، فأنت بحاجة إلى التحكم في عدد القيم التي تستخرجها منه. في هذا المثال، نستخرج أول 10 قيم.
فوائد استخدام بروتوكول المُكرِّر
- التوحيد القياسي: يوفر بروتوكول المُكرِّر طريقة متسقة للتكرار على هياكل البيانات المختلفة.
- المرونة: يمكنك تحديد مُكرِّرات مخصصة مصممة لاحتياجاتك الخاصة.
- سهولة القراءة: تجعل حلقة
for...of
كود التكرار أكثر قابلية للقراءة والإيجاز. - الكفاءة: يمكن أن تكون المُكرِّرات "كسولة"، مما يعني أنها تولد القيم فقط عند الحاجة إليها، مما يمكن أن يحسن الأداء لمجموعات البيانات الكبيرة. على سبيل المثال، مولِّد متتالية فيبوناتشي أعلاه يحسب فقط القيمة التالية عند استدعاء `next()`.
- التوافق: تعمل المُكرِّرات بسلاسة مع ميزات JavaScript الأخرى مثل صيغة النشر والتفكيك.
تقنيات متقدمة للمُكرِّرات
دمج المُكرِّرات
يمكنك دمج عدة مُكرِّرات في مُكرِّر واحد. هذا مفيد عندما تحتاج إلى معالجة البيانات من مصادر متعددة بطريقة موحدة.
function* combineIterators(...iterables) {
for (const iterable of iterables) {
for (const item of iterable) {
yield item;
}
}
}
const array1 = [1, 2, 3];
const array2 = ["a", "b", "c"];
const string1 = "XYZ";
const combined = combineIterators(array1, array2, string1);
for (const value of combined) {
console.log(value); // Output: 1, 2, 3, a, b, c, X, Y, Z
}
في هذا المثال، تأخذ دالة `combineIterators` أي عدد من الكائنات القابلة للتكرار كوسائط. تقوم بالتكرار على كل كائن قابل للتكرار وتنتج كل عنصر. والنتيجة هي مُكرِّر واحد ينتج جميع القيم من جميع الكائنات القابلة للتكرار المُدخلة.
تصفية وتحويل المُكرِّرات
يمكنك أيضًا إنشاء مُكرِّرات تقوم بتصفية أو تحويل القيم التي ينتجها مُكرِّر آخر. يتيح لك هذا معالجة البيانات في خط أنابيب، وتطبيق عمليات مختلفة على كل قيمة أثناء إنشائها.
function* filterIterator(iterable, predicate) {
for (const item of iterable) {
if (predicate(item)) {
yield item;
}
}
}
function* mapIterator(iterable, transform) {
for (const item of iterable) {
yield transform(item);
}
}
const numbers = [1, 2, 3, 4, 5, 6];
const evenNumbers = filterIterator(numbers, (x) => x % 2 === 0);
const squaredEvenNumbers = mapIterator(evenNumbers, (x) => x * x);
for (const value of squaredEvenNumbers) {
console.log(value); // Output: 4, 16, 36
}
هنا، يأخذ `filterIterator` كائنًا قابلاً للتكرار ودالة شرطية (predicate). ينتج فقط العناصر التي تعيد الدالة الشرطية لها `true`. يأخذ `mapIterator` كائنًا قابلاً للتكرار ودالة تحويل. ينتج نتيجة تطبيق دالة التحويل على كل عنصر.
تطبيقات في العالم الحقيقي
يستخدم بروتوكول المُكرِّر على نطاق واسع في مكتبات وأطر عمل JavaScript، وهو ذو قيمة في مجموعة متنوعة من التطبيقات الواقعية، خاصة عند التعامل مع مجموعات بيانات كبيرة أو عمليات غير متزامنة.
- معالجة البيانات: المُكرِّرات مفيدة لمعالجة مجموعات البيانات الكبيرة بكفاءة، لأنها تتيح لك العمل مع البيانات على شكل أجزاء دون تحميل مجموعة البيانات بأكملها في الذاكرة. تخيل تحليل ملف CSV كبير يحتوي على بيانات العملاء. يمكن للمُكرِّر أن يسمح لك بمعالجة كل صف دون تحميل الملف بأكمله في الذاكرة مرة واحدة.
- العمليات غير المتزامنة: يمكن استخدام المُكرِّرات للتعامل مع العمليات غير المتزامنة، مثل جلب البيانات من واجهة برمجة تطبيقات (API). يمكنك استخدام الدوال المولِّدة لإيقاف التنفيذ مؤقتًا حتى تتوفر البيانات ثم استئنافه بالقيمة التالية.
- هياكل البيانات المخصصة: المُكرِّرات ضرورية لإنشاء هياكل بيانات مخصصة بمتطلبات اجتياز محددة. فكر في هيكل بيانات الشجرة. يمكنك تنفيذ مُكرِّر مخصص لاجتياز الشجرة بترتيب معين (مثل البحث بالعمق أولاً أو البحث بالعرض أولاً).
- تطوير الألعاب: في تطوير الألعاب، يمكن استخدام المُكرِّرات لإدارة كائنات اللعبة، وتأثيرات الجسيمات، والعناصر الديناميكية الأخرى.
- مكتبات واجهة المستخدم: تستخدم العديد من مكتبات واجهة المستخدم المُكرِّرات لتحديث وعرض المكونات بكفاءة بناءً على تغييرات البيانات الأساسية.
أفضل الممارسات
- تنفيذ
Symbol.iterator
بشكل صحيح: تأكد من أن دالةSymbol.iterator
الخاصة بك تعيد كائن مُكرِّر يتوافق مع بروتوكول المُكرِّر. - التعامل مع علامة
done
بدقة: تعتبر علامةdone
حاسمة للإشارة إلى نهاية التكرار. تأكد من تعيينها بشكل صحيح في دالةnext()
الخاصة بك. - فكر في استخدام الدوال المولِّدة: توفر الدوال المولِّدة طريقة أكثر إيجازًا وقابلية للقراءة لإنشاء المُكرِّرات.
- تجنب الآثار الجانبية في
next()
: يجب أن تركز دالةnext()
بشكل أساسي على استرداد القيمة التالية وتحديث حالة المُكرِّر. تجنب إجراء عمليات معقدة أو آثار جانبية داخلnext()
. - اختبر مُكرِّراتك جيدًا: اختبر مُكرِّراتك المخصصة مع مجموعات بيانات وسيناريوهات مختلفة للتأكد من أنها تعمل بشكل صحيح.
الخلاصة
يوفر بروتوكول المُكرِّر في JavaScript طريقة قوية ومرنة لاجتياز هياكل البيانات. من خلال فهم بروتوكولات الكائن القابل للتكرار والمُكرِّر، ومن خلال الاستفادة من الدوال المولِّدة، يمكنك إنشاء مُكرِّرات مخصصة مصممة لاحتياجاتك الخاصة. يتيح لك هذا العمل بكفاءة مع البيانات، وتحسين قابلية قراءة الكود، وتعزيز أداء تطبيقاتك. إن إتقان المُكرِّرات يفتح فهمًا أعمق لإمكانيات JavaScript ويمكّنك من كتابة كود أكثر أناقة وكفاءة.