دليل شامل لوظائف مولد JavaScript وبروتوكول المُكرِّر. تعلم كيفية إنشاء مُكرِّرات مخصصة وتحسين تطبيقات JavaScript الخاصة بك.
وظائف مولد JavaScript: إتقان بروتوكول المُكرِّر
توفر وظائف مولد JavaScript، التي تم تقديمها في ECMAScript 6 (ES6)، آلية قوية لإنشاء المُكرِّرات بطريقة أكثر إيجازًا وقابلة للقراءة. تتكامل بسلاسة مع بروتوكول المُكرِّر، مما يتيح لك بناء مُكرِّرات مخصصة يمكنها التعامل مع هياكل البيانات المعقدة والعمليات غير المتزامنة بسهولة. ستتعمق هذه المقالة في تعقيدات وظائف المولد، وبروتوكول المُكرِّر، وأمثلة عملية لتوضيح تطبيقها.
فهم بروتوكول المُكرِّر
قبل الغوص في وظائف المولد، من الضروري فهم بروتوكول المُكرِّر، الذي يشكل الأساس لهياكل البيانات القابلة للتكرار في JavaScript. يحدد بروتوكول المُكرِّر كيفية تكرار كائن ما، مما يعني أنه يمكن الوصول إلى عناصره بالتسلسل.
بروتوكول التكرار
يعتبر الكائن قابلاً للتكرار إذا كان ينفذ طريقة @@iterator (Symbol.iterator). يجب أن تُرجع هذه الطريقة كائن مُكرِّر.
مثال على كائن قابل للتكرار بسيط:
const myIterable = {
data: [1, 2, 3],
[Symbol.iterator]() {
let index = 0;
return {
next() {
if (index < myIterable.data.length) {
return { value: myIterable.data[index++], done: false };
} else {
return { value: undefined, done: true };
}
}
};
}
};
for (const item of myIterable) {
console.log(item); // Output: 1, 2, 3
}
بروتوكول المُكرِّر
يجب أن يمتلك كائن المُكرِّر طريقة next(). تُرجع طريقة next() كائنًا له خاصيتان:
value: القيمة التالية في التسلسل.done: قيمة منطقية تشير إلى ما إذا كان المُكرِّر قد وصل إلى نهاية التسلسل. تشير قيمةtrueإلى النهاية؛ تعنيfalseوجود المزيد من القيم التي سيتم استردادها.
يسمح بروتوكول المُكرِّر لميزات JavaScript المضمنة مثل حلقات for...of ومشغل النشر (...) بالعمل بسلاسة مع هياكل البيانات المخصصة.
تقديم وظائف المولد
توفر وظائف المولد طريقة أكثر أناقة وإيجازًا لإنشاء المُكرِّرات. يتم الإعلان عنها باستخدام بناء الجملة function*.
بناء جملة وظائف المولد
بناء الجملة الأساسي لوظيفة المولد هو كما يلي:
function* myGenerator() {
yield 1;
yield 2;
yield 3;
}
const iterator = myGenerator();
console.log(iterator.next()); // Output: { value: 1, done: false }
console.log(iterator.next()); // Output: { value: 2, done: false }
console.log(iterator.next()); // Output: { value: 3, done: false }
console.log(iterator.next()); // Output: { value: undefined, done: true }
الخصائص الرئيسية لوظائف المولد:
- يتم الإعلان عنها باستخدام
function*بدلاً منfunction. - تستخدم كلمة
yieldالأساسية لإيقاف التنفيذ مؤقتًا وإرجاع قيمة. - في كل مرة يتم فيها استدعاء
next()على المُكرِّر، تستأنف وظيفة المولد التنفيذ من حيث توقفت حتى يتم مواجهة عبارةyieldالتالية، أو تعود الوظيفة. - عندما تنتهي وظيفة المولد من التنفيذ (إما بالوصول إلى النهاية أو مواجهة عبارة
return)، تصبح خاصيةdoneللكائن المُرجعtrue.
كيف تنفذ وظائف المولد بروتوكول المُكرِّر
عندما تستدعي وظيفة مولد، فإنها لا تنفذ على الفور. بدلاً من ذلك، تُرجع كائن مُكرِّر. ينفذ كائن المُكرِّر هذا بروتوكول المُكرِّر تلقائيًا. ينتج كل بيان yield قيمة لطريقة next() الخاصة بالمُكرِّر. تدير وظيفة المولد الحالة الداخلية وتتبع تقدمها، مما يبسط إنشاء المُكرِّرات المخصصة.
أمثلة عملية لوظائف المولد
دعنا نستكشف بعض الأمثلة العملية التي تعرض قوة وتنوع وظائف المولد.
1. إنشاء سلسلة من الأرقام
يوضح هذا المثال كيفية إنشاء وظيفة مولد تنشئ سلسلة من الأرقام ضمن نطاق محدد.
function* numberSequence(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}
const sequence = numberSequence(10, 15);
for (const num of sequence) {
console.log(num); // Output: 10, 11, 12, 13, 14, 15
}
2. التكرار على هيكل شجرة
تعد وظائف المولد مفيدة بشكل خاص لاجتياز هياكل البيانات المعقدة مثل الأشجار. يوضح هذا المثال كيفية التكرار على عقدة الشجرة الثنائية.
class TreeNode {
constructor(value) {
this.value = value;
this.left = null;
this.right = null;
}
}
function* treeTraversal(node) {
if (node) {
yield* treeTraversal(node.left); // Recursive call for left subtree
yield node.value; // Yield the current node's value
yield* treeTraversal(node.right); // Recursive call for right subtree
}
}
// Create a sample binary tree
const root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);
// Iterate over the tree using the generator function
const treeIterator = treeTraversal(root);
for (const value of treeIterator) {
console.log(value); // Output: 4, 2, 5, 1, 3 (In-order traversal)
}
في هذا المثال، يتم استخدام yield* للتفويض إلى مُكرِّر آخر. هذا أمر بالغ الأهمية للتكرار المتكرر، مما يسمح للمولد باجتياز هيكل الشجرة بأكمله.
3. التعامل مع العمليات غير المتزامنة
يمكن دمج وظائف المولد مع الوعود للتعامل مع العمليات غير المتزامنة بطريقة أكثر تسلسلية وقابلة للقراءة. هذا مفيد بشكل خاص لمهام مثل جلب البيانات من واجهة برمجة التطبيقات (API).
async function fetchData(url) {
const response = await fetch(url);
const data = await response.json();
return data;
}
function* dataFetcher(urls) {
for (const url of urls) {
try {
const data = yield fetchData(url);
yield data;
} catch (error) {
console.error("Error fetching data from", url, error);
yield null; // Or handle the error as needed
}
}
}
async function runDataFetcher() {
const urls = [
"https://jsonplaceholder.typicode.com/todos/1",
"https://jsonplaceholder.typicode.com/posts/1",
"https://jsonplaceholder.typicode.com/users/1"
];
const dataIterator = dataFetcher(urls);
for (const promise of dataIterator) {
const data = await promise; // Await the promise returned by yield
if (data) {
console.log("Fetched data:", data);
} else {
console.log("Failed to fetch data.");
}
}
}
runDataFetcher();
يعرض هذا المثال التكرار غير المتزامن. تُنتج وظيفة مولد dataFetcher وعودًا يتم حلها إلى البيانات التي تم جلبها. ثم تقوم الدالة runDataFetcher بالتكرار خلال هذه الوعود، في انتظار كل منها قبل معالجة البيانات. يعمل هذا النهج على تبسيط التعليمات البرمجية غير المتزامنة عن طريق جعلها تبدو أكثر تزامنًا.
4. التسلسلات اللانهائية
المولدات مثالية لتمثيل التسلسلات اللانهائية، وهي تسلسلات لا تنتهي أبدًا. نظرًا لأنها تنتج القيم فقط عند الطلب، فيمكنها التعامل مع تسلسلات طويلة إلى ما لا نهاية دون استهلاك ذاكرة مفرطة.
function* fibonacciSequence() {
let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const fibonacci = fibonacciSequence();
// Get the first 10 Fibonacci numbers
for (let i = 0; i < 10; i++) {
console.log(fibonacci.next().value); // Output: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
}
يوضح هذا المثال كيفية إنشاء سلسلة فيبوناتشي لانهائية. تستمر وظيفة المولد في إنتاج أرقام فيبوناتشي إلى أجل غير مسمى. في الممارسة العملية، يمكنك عادةً الحد من عدد القيم التي تم استردادها لتجنب حلقة لا نهائية أو استنفاد الذاكرة.
5. تنفيذ دالة نطاق مخصصة
قم بإنشاء دالة نطاق مخصصة مماثلة لدالة النطاق المضمنة في Python باستخدام المولدات.
function* range(start, end, step = 1) {
if (step > 0) {
for (let i = start; i < end; i += step) {
yield i;
}
} else if (step < 0) {
for (let i = start; i > end; i += step) {
yield i;
}
}
}
// Generate numbers from 0 to 5 (exclusive)
for (const num of range(0, 5)) {
console.log(num); // Output: 0, 1, 2, 3, 4
}
// Generate numbers from 10 to 0 (exclusive) in reverse order
for (const num of range(10, 0, -2)) {
console.log(num); // Output: 10, 8, 6, 4, 2
}
تقنيات وظائف المولد المتقدمة
1. استخدام return في وظائف المولد
تشير عبارة return في وظيفة المولد إلى نهاية التكرار. عند مواجهة عبارة return، سيتم تعيين خاصية done الخاصة بطريقة next() الخاصة بالمُكرِّر إلى true، وستتم تعيين خاصية value إلى القيمة التي تم إرجاعها بواسطة عبارة return (إن وجدت).
function* myGenerator() {
yield 1;
yield 2;
return 3; // End of iteration
yield 4; // This will not be executed
}
const iterator = myGenerator();
console.log(iterator.next()); // Output: { value: 1, done: false }
console.log(iterator.next()); // Output: { value: 2, done: false }
console.log(iterator.next()); // Output: { value: 3, done: true }
console.log(iterator.next()); // Output: { value: undefined, done: true }
2. استخدام throw في وظائف المولد
تتيح لك طريقة throw الموجودة في كائن المُكرِّر إدخال استثناء في وظيفة المولد. يمكن أن يكون هذا مفيدًا للتعامل مع الأخطاء أو الإشارة إلى حالات معينة داخل المولد.
function* myGenerator() {
try {
yield 1;
yield 2;
} catch (error) {
console.error("Caught an error:", error);
}
yield 3;
}
const iterator = myGenerator();
console.log(iterator.next()); // Output: { value: 1, done: false }
iterator.throw(new Error("Something went wrong!")); // Inject an error
console.log(iterator.next()); // Output: { value: 3, done: false }
console.log(iterator.next()); // Output: { value: undefined, done: true }
3. التفويض إلى شيء قابل للتكرار آخر باستخدام yield*
كما رأينا في مثال اجتياز الشجرة، يسمح لك بناء الجملة yield* بالتفويض إلى شيء آخر قابل للتكرار (أو وظيفة مولد أخرى). هذه ميزة قوية لتأليف المُكرِّرات وتبسيط منطق التكرار المعقد.
function* generator1() {
yield 1;
yield 2;
}
function* generator2() {
yield* generator1(); // Delegate to generator1
yield 3;
yield 4;
}
const iterator = generator2();
for (const value of iterator) {
console.log(value); // Output: 1, 2, 3, 4
}
فوائد استخدام وظائف المولد
- تحسين إمكانية القراءة: تجعل وظائف المولد كود المُكرِّر أكثر إيجازًا وأسهل في الفهم مقارنة بتنفيذ المُكرِّر اليدوي.
- تبسيط البرمجة غير المتزامنة: فهي تعمل على تبسيط التعليمات البرمجية غير المتزامنة من خلال السماح لك بكتابة عمليات غير متزامنة بأسلوب أكثر تزامنًا.
- كفاءة الذاكرة: تنتج وظائف المولد القيم عند الطلب، وهو أمر مفيد بشكل خاص لمجموعات البيانات الكبيرة أو التسلسلات اللانهائية. تتجنب تحميل مجموعة البيانات بأكملها في الذاكرة مرة واحدة.
- إعادة استخدام التعليمات البرمجية: يمكنك إنشاء وظائف مولد قابلة لإعادة الاستخدام يمكن استخدامها في أجزاء مختلفة من تطبيقك.
- المرونة: توفر وظائف المولد طريقة مرنة لإنشاء مُكرِّرات مخصصة يمكنها التعامل مع هياكل البيانات المختلفة وأنماط التكرار.
أفضل الممارسات لاستخدام وظائف المولد
- استخدم أسماء وصفية: اختر أسماء ذات معنى لوظائف المولد والمتغيرات لتحسين سهولة قراءة التعليمات البرمجية.
- التعامل مع الأخطاء بأناقة: قم بتنفيذ معالجة الأخطاء داخل وظائف المولد الخاصة بك لمنع السلوك غير المتوقع.
- الحد من التسلسلات اللانهائية: عند العمل مع التسلسلات اللانهائية، تأكد من أن لديك آلية للحد من عدد القيم التي تم استردادها لتجنب الحلقات اللانهائية أو استنفاد الذاكرة.
- ضع في اعتبارك الأداء: في حين أن وظائف المولد فعالة بشكل عام، كن على دراية بآثار الأداء، خاصة عند التعامل مع العمليات المكثفة حسابيًا.
- وثق التعليمات البرمجية الخاصة بك: قم بتوفير وثائق واضحة وموجزة لوظائف المولد الخاصة بك لمساعدة المطورين الآخرين على فهم كيفية استخدامها.
حالات الاستخدام تتجاوز JavaScript
يمتد مفهوم المولدات والمُكرِّرات إلى ما وراء JavaScript ويجد تطبيقات في لغات وسيناريوهات برمجة مختلفة. على سبيل المثال:
- Python: يحتوي Python على دعم مُضمّن للمولدات باستخدام الكلمة الأساسية
yield، على غرار JavaScript إلى حد كبير. تُستخدم على نطاق واسع لمعالجة البيانات وإدارة الذاكرة بكفاءة. - C#: تستخدم C# المُكرِّرات وعبارة
yield returnلتنفيذ تكرار مجموعة مخصصة. - تدفق البيانات: في خطوط أنابيب معالجة البيانات، يمكن استخدام المولدات لمعالجة التدفقات الكبيرة من البيانات على دفعات، مما يحسن الكفاءة ويقلل من استهلاك الذاكرة. هذا مهم بشكل خاص عند التعامل مع البيانات في الوقت الفعلي من المستشعرات أو الأسواق المالية أو وسائل التواصل الاجتماعي.
- تطوير الألعاب: يمكن استخدام المولدات لإنشاء محتوى إجرائي، مثل توليد التضاريس أو تسلسلات الرسوم المتحركة، دون حساب المحتوى بأكمله وتخزينه مسبقًا في الذاكرة.
الخلاصة
تعد وظائف مولد JavaScript أداة قوية لإنشاء المُكرِّرات والتعامل مع العمليات غير المتزامنة بطريقة أكثر أناقة وكفاءة. من خلال فهم بروتوكول المُكرِّر وإتقان الكلمة الأساسية yield، يمكنك الاستفادة من وظائف المولد لإنشاء تطبيقات JavaScript أكثر قابلية للقراءة والصيانة والأداء. من إنشاء تسلسلات الأرقام إلى اجتياز هياكل البيانات المعقدة والتعامل مع المهام غير المتزامنة، تقدم وظائف المولد حلاً متعدد الاستخدامات لمجموعة واسعة من تحديات البرمجة. استخدم وظائف المولد لفتح إمكانيات جديدة في سير عمل تطوير JavaScript الخاص بك.