صفا الفليج

الأعضاء
  • المساهمات

    47
  • تاريخ الانضمام

  • تاريخ آخر زيارة

السُّمعة بالموقع

4 Neutral

1 متابع

  1. تُتيح لنا المُكرّرات غير المتزامنة المرور على البيانات التي تأتينا على نحوٍ غير متزامن متى لزم، مثل حين نُنزّل شيئًا كتلةً كتلةً عبر الشبكة. المولّدات غير المتزامنة تجعل من ذلك أسهل فأسهل. لنرى مثالًا أولًا لنفهم الصياغة، بعدها نرى مثالًا من الحياة العملية. المكررات غير المتزامنة تتشابه المُكرّرات غير المتزامنة مع تلك العادية، بفروق بسيطة في الصياغة. فكائن المُكرّر العادي (كما رأينا في الفصل الكائنات المكرَّرة (Iterables) في جافاسكربت) يكون هكذا: let range = { from: 1, to: 5, // ‫تستدعي حلقة for..of هذه الدالّة مرة واحدة في البداية [Symbol.iterator]() { // ‫...ستعيد كائن مُكرُر: // ‫لاحقًا ستعمل حلقة for..of فقط مع ذلك الكائن. // ‫وتطلب منه القيم التالية باستخدام دالة next()‎ return { current: this.from, last: this.to, // ‫تُستدعى next()‎ في كلّ تكرار من خلال الحلقة for..of next() { // (2) // ‫يجب أن تعيد القيم ككائن {done:.., value :...} if (this.current <= this.last) { return { done: false, value: this.current++ }; } else { return { done: true }; } } }; } }; for(let value of range) { alert(value); // 1 then 2, then 3, then 4, then 5 } راجِع … لتعرف تفاصيل المُكرّرات العادية، لو لزم. ولنجعل الكائن مُتكرّرًا غير متزامنًا: علينا استعمال Symbol.asyncIterator بدل Symbol.iterator. على next()‎ إعادة وعد. علينا استعمال حلقة for await (let item of iterable)‎ لتكرار كائن معين. فلنصنع كائن range مُتكرّرًا (كما أعلاه) ولكن يُعيد القيم بنحوٍ غير متزامن، قيمةً واحدةً كلّ ثانية: let range = { from: 1, to: 5, // ‫تستدعي حلقة for..of هذه الدالّة مرة واحدة في البداية [Symbol.asyncIterator]() { // (1) // ‫...ستعيد كائن مُكرُر: // ‫لاحقًا ستعمل حلقة for..of فقط مع ذلك الكائن. // ‫وتطلب منه القيم التالية باستخدام دالة next()‎ return { current: this.from, last: this.to, // ‫تُستدعى next()‎ في كلّ تكرار من خلال الحلقة for..of async next() { // (2) // ‫يجب أن تعيد القيم ككائن {done:.., value :...} // وستُغلّف تلقائيًا في وعد غير متزامن // ‫يمكننا استخدام await لتنفيذ أشياء غير متزامنة: await new Promise(resolve => setTimeout(resolve, 1000)); // (3) if (this.current <= this.last) { return { done: false, value: this.current++ }; } else { return { done: true }; } } }; } }; (async () => { for await (let value of range) { // (4) alert(value); // 1,2,3,4,5 } })() كما ترى فالبنية تشبه المُكرّرات العادية: ليكون الكائن مُتكرّرًا على نحوٍ غير متزامن، يجب أن يحتوي التابِع Symbol.asyncIterator (لاحِظ (1)). على التابِع إعادة الكائن وهو يحتوي التابِع next()‎ الذي يُعيد وعدًا (لاحِظ (2)). ليس على التابِع next()‎ أن يكون غير متزامن async. فيمكن أن يكون تابِعًا عاديًا يُعيد وعدًا، إلّا أنّ async تُتيح لنا استعمال await وهذا أفضل لنا. هنا نؤخّر التنفيذ ثانيةً واحدةً فقط (لاحِظ (3)). ليحدث التكرار نستعمل for await(let value of range)‎ (لاحِظ (4))، أي نُضيف await قبل الحلقة for. هكذا تستدعي الحلقة range[Symbol.asyncIterator]()‎ مرةً واحدة، وبعدها تابِع next()‎ للقيم التالية. إليك ورقة صغيرة تغشّ منها: المُكرّرات المُكرّرات غير المتزامنة تابِع الكائن الذي يُقدّم المُكرّر Symbol.iterator Symbol.asyncIterator قيمة next() المُعادة هي أيّ قيمة وعد Promise to loop, use for..of for await..of تحذير: "لا يعمل معامل البقية ... بطريقة غير متزامنة" الميزات التي تتطلب تكرارات منتظمة ومتزامنة، لا تعمل مع تلك المتزامنة. على سبيل المثال، لن يعمل معامل البقية هنا: alert( [...range] ); // Error, no Symbol.iterator هذا أمر طبيعي، لأنه يتوقع العثور على Symbol.iterator، مثل for..of بدون await. ولكن ليس Symbol.asyncIterator. المولدات غير المتزامنة كما نعلم فلغة جافاسكربت تدعم المولّدات أيضًا، وهذه المولّدات مُتكرّرة. فلنتذكّر مولّد السلاسل من الفصل المولِّداتكان يولّد سلسلة من القيم من متغير البداية start إلى متغير النهاية end: function* generateSequence(start, end) { for (let i = start; i <= end; i++) { yield i; } } for(let value of generateSequence(1, 5)) { alert(value); // 1, then 2, then 3, then 4, then 5 } لا يمكننا في الدوال العادية استعمال await، فعلى كلّ القيم أن تأتي بنحوٍ متزامن وليس ممكنًا وضع أيّ تأخير في حلقة for..of، ولكن، ماذا لو أردنا استعمال await في المولّد؟ مثلًا لنُرسل الطلبات عبر الشبكة؟ لا مشكلة، نضع قبله async هكذا: async function* generateSequence(start, end) { for (let i = start; i <= end; i++) { // yay, can use await! await new Promise(resolve => setTimeout(resolve, 1000)); yield i; } } (async () => { let generator = generateSequence(1, 5); for await (let value of generator) { alert(value); // 1, then 2, then 3, then 4, then 5 } })(); الآن لدينا مولّد غير متزامن، وقابل للتكرار مع for await...of. إنها حقًا بسيطة للغاية. نضيف الكلمة المفتاحية async، ويستطيع المولّد الآن استخدام await بداخله، ويعتمد على الوعود والدوالّ غير متزامنة الأخرى. وتقنيًا، الفرق الآخر للمولّد غير المتزامن هو أنّ تابِع generator.next()‎ صار غير متزامنًا أيضًا ويُعيد الوعود. في المولّدات العادية نستعمل result = generator.next()‎ لنأخذ القيم، بينما في المولّدات غير المتزامنة نُضيف await هكذا: result = await generator.next(); // result = {value: ..., done: true/false} المكررات غير المتزامنة كما نعلم ليكون الكائن مُتكرّرًا علينا إضافة رمز Symbol.iterator إليه. let range = { from: 1, to: 5, [Symbol.iterator]() { return <object with next to make range iterable> } } هناك أسلوب شائع هو إعادة Symbol.iterator لمولّد بدل كائن صِرف يحمل التابِع next كما المثال أعلاه. لنستذكر معًا المثال من الفصل المولِّدات: let range = { from: 1, to: 5, *[Symbol.iterator]() { // ‫اختصارًا لـِ ‎[Symbol.iterator]: function*()‎ for(let value = this.from; value <= this.to; value++) { yield value; } } }; for(let value of range) { alert(value); // 1, then 2, then 3, then 4, then 5 } نرى الكائن المخصّص range هنا مُتكرّرًا، والمولّد ‎*[Symbol.iterator]‎ يؤدّي المنطق اللازم لسرد القيم. لو أردنا إضافة أيّ إجراءات غير متزامنة للمولّدة، فعلينا استبدال الرمز Symbol.iterator بالرمز Symbol.asyncIterator غير المتزامن. let range = { from: 1, to: 5, async *[Symbol.asyncIterator]() { // ‫مشابه لـِ ‎[Symbol.asyncIterator]: async function*()‎ for(let value = this.from; value <= this.to; value++) { // توقف بين القيم لانتظار شيء ما await new Promise(resolve => setTimeout(resolve, 1000)); yield value; } } }; (async () => { for await (let value of range) { alert(value); // 1, then 2, then 3, then 4, then 5 } })(); الآن تأتينا القيم متأخرة عن بعضها البعض ثانيةً واحدة. مثال من الحياة العملية حتى اللحظة كانت الأمثلة كلّها بسيطة، لنفهم القصة فقط. الآن حان الوقت لنُطالع مثالًا وحالةً من الواقع. نرى على الإنترنت خدمات كثيرة تقدّم لنا البيانات على صفحات (paginated). فمثلًا حين نطلب قائمة من المستخدمين يُعيد الطلب عددًا تحدّد مسبقًا (مثلًا 100 مستخدم)، أي ”صفحة واحدة“، ويعطينا أيضًا عنوان الصفحة التالية. هذا النمط شائع جدًا بين المطوّرين، وليس مستخدمًا للمستخدمين فقط بل لكلّ شيء. فمثلًا غِت‌هَب تتيح لك جلب الإيداعات بنفس الطريقة باستعمال الصفحات: عليك إرسال طلب إلى المسار https://api.github.com/repos//commits. يستجيب الخادوم بكائن JSON فيه 30 إيداعًا، ويُعطيك رابطًا يوصلك إلى الصفحة التالية في ترويسة Link. بعدها نستعمل ذلك الرابط للطلبات التالية لنجلب إيداعات أكثر، وهكذا. ولكن ما نريد هو واجهة برمجية أبسط قليلًا: أي كائنًا مُتكرّرًا فيه الإيداعات كي نمرّ عليها بهذا النحو: let repo = 'javascript-tutorial/en.javascript.info'; // المستودع الذي فيه الإيداعات التي نريد على غِت‌هَب for await (let commit of fetchCommits(repo)) { // نُعالج كلّ إيداع commit } ونريد كتابة الدالة fetchCommits(repo)‎ لتجلب تلك الإيداعات لنا وتؤدّي الطلبات اللازمة متى… لزم. كما ونترك في عهدتها مهمة الصفحات كاملةً. ما سنفعله من جهتنا هو أمر بسيط، for await..of فقط. باستعمال المولّدات غير المتزامنة فهذه المهمّة تصير سهلة: async function* fetchCommits(repo) { let url = `https://api.github.com/repos/${repo}/commits`; while (url) { const response = await fetch(url, { // (1) headers: {'User-Agent': 'Our script'}, }); const body = await response.json(); // (2) // (3) let nextPage = response.headers.get('Link').match(/<(.*?)>; rel="next"/); nextPage = nextPage && nextPage[1]; url = nextPage; for(let commit of body) { // (4) yield commit; } } } نستعمل تابِع fetch من المتصفّح لتنزيل العنوان البعيد. يُتيح لنا التابِع تقديم تصاريح الاستيثاق وغيرها من ترويسات مطلوبة. غِت‌هَب يطلب User-Agent. نحلّل نتيجة الجلب على أنّها كائن JSON، وهذا تابِع آخر خاصّ بالتابِع fetch. نستلم هكذا عنوان الصفحة التالية من ترويسة Link داخل الردّ. لهذا العنوان تنسيق خاص فنستعمل تعبيرًا نمطيًا لتحليله. يظهر عنوان الصفحة التالية على هذا الشكل: https://api.github.com/repositories/93253246/commits?page=2، وموقع غِت‌هَب هو من يولّده بنفسه. بعدها نُنتِج كلّ الإيداعات التي استلمناها، ومتى انتهت مثال على طريقة الاستعمال (تعرض من كتبَ الإيداعات في الطرفيّة): (async () => { let count = 0; for await (const commit of fetchCommits('javascript-tutorial/en.javascript.info')) { console.log(commit.author.login); if (++count == 100) { // لنتوقف عند 100 إيداع break; } } })(); كما نريد تمامًا. الطريقة التي تعمل بها الطلبات بهذا النوع لا تظهر للشيفرات الخارجية وتبقى تفاصيل داخلية. المهم لنا هي استعمال مولّد غير متزامن يُعيد الإيداعات. خلاصة تعمل المُكرّرات والمولّدات العادية كما نريد لو لم تأخذ البيانات التي نحتاج وقتًا حتّى تتولّد. وحين نتوقّع بأنّ البيانات ستأتي بنحوٍ غير متزامن بينها انقطاعات، فيمكن استعمال البدائل غير المتزامنة مثل for await..of بدل for..of. الفرق في الصياغة بين المُكرّرات العادية وغير المتزامنة: المتكرر المتكرر غير المتزامن التابِع الذي يُقدّم المُكرّر Symbol.iterator Symbol.asyncIterator قيمة next()‎ المُعادة هي {value:…, done: true/false} Promise والّتي تعوض لتصبح {value:…, done: true/false} الفرق في الصياغة بين المولّدات العادية وغير المتزامنة: المولدات المولدات غير المتزامنة التعريف function* async function* next()‎ القيمة المُعادة هي {value:…, done: true/false} Promise والّتي تعوض لتصبح {value:…, done: true/false} غالبًا ونحن نعمل على تطوير الوِب نواجه سيولًا كبيرة من البيانات، سيولًا تأتينا قطعًا قطعًا (مثل تنزيل أو رفع الملفات الكبيرة). يمكننا هنا استعمال المولّدات غير المتزامنة لمُعالجة هذه البيانات. كما يجدر بنا الملاحظة كيف أنّ لبعض البيئات (مثل المتصفّحات) واجهات برمجة أخرى باسم السيول (Stream) وهي تقدّم لنا واجهات خاصّة للعمل مع السيول هذه، ذلك لتحويل شكل البيانات ونقلها من سيل إلى آخر (مثل التنزيل من مكان وإرسال ذلك التنزيل إلى مكان آخر مباشرةً). ترجمة -وبتصرف- للفصل Async iterators and generators من كتاب The JavaScript language table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; }
  2. تُعيد الدوالّ العادية قيمة واحدة فقط لا غير (أو لا تُعيد شيئًا). بينما يمكن للمولّدات إعادة (أو إنتاج yeild) أكثر من قيمة واحدةً بعد الأخرى حسب الطلب. تعمل المولّدات عملًا جميلًا جدًا مع الكائنات المكرَّرة (Iterables) في جافاسكربت وتتيح لنا إنشاء سيول البيانات بسهولة بالغة. الدوال المولدة لإنشاء مولّد علينا استعمال صياغة مميّزة: function*‎ أو ما يسمّونه ”الدالة المولِّدة“. هذا شكلها: function* generateSequence() { yield 1; yield 2; return 3; } يختلف سلوك الدوال المولِّدة عن تلك العادية، فحين تُستدعى الدالة لا تُشغّل الشيفرة فيها، بل تُعيد كائنًا مميزًا نسمّيه ”كائن المولّد“ ليُدير عملية التنفيذ. خُذ نظرة: function* generateSequence() { yield 1; yield 2; return 3; } // تنشئ الدالة المولِّدة كائن مولّد let generator = generateSequence(); alert(generator); // [object Generator] الشيفرة الموجودة بداخل الدالّة لم تُنفذ بعد: التابِع الأساسي للمولّد هو next()‎. متى استدعيناه بدأ عملية التنفيذ حتّى يصل أقرب جملة yield <value> (يمكن ألّا نكتب value وستصير القيمة undefined). بعدها يتوقّف تنفيذ الدالة مؤقّتًا وتُعاد القيمة value إلى الشيفرة الخارجية. ناتج التابِع next()‎ لا يكون إلّا كائنًا له خاصيتين: value: القيمة التي أنتجها المولّد. done: القيمة true لو اكتملت شيفرة الدالة، وإلّا false. فمثلًا هنا نُنشِئ مولّدًا ونأخذ أوّل قيمة أنتجها: function* generateSequence() { yield 1; yield 2; return 3; } let generator = generateSequence(); let one = generator.next(); // هنا alert(JSON.stringify(one)); // {value: 1, done: false} حاليًا أخذنا القيمة الأولى فقط، وسير تنفيذ الدالة موجود في السطر الثاني: فلنستدعِ generator.next()‎ ثانيةً الآن. سنراه واصل تنفيذ الشيفرة وأعاد القيمة المُنتَجة yield التالية: let two = generator.next(); alert(JSON.stringify(two)); // {value: 2, done: false} والآن إن شغّلناه مرّة ثالثة سيصل سير التنفيذ إلى عبارة return ويُنهي الدالة: let three = generator.next(); alert(JSON.stringify(three)); // {value: 3, done: true} // ‫لاحِظ قيمة done الآن اكتمل المولّد بقيمة value:3 ويمكننا الآن معالجتها. عرفنا ذلك من done:true. الاستدعاءات اللاحقة على generator.next()‎ لن تكون منطقية الآن. ولو حصلت فستُعيد الدالة الكائن نفسه: {done: true}. ملاحظة: الصياغة function* f(…)‎ أم function *f(…)‎؟ في الحقيقة كِلاهما صحيح. ولكن عادةً تكون الصياغة الأولى مفضلة أكثر من الثانية. وتشير النجمة * على أنها دالة مولّد، إذ تصف النوع وليس الاسم، لذلك يجب أن ندمج الكلمة المفتاحية function بالنجمة. المولدات قابلة للتكرار نفترض أنّك توقّعت ذلك حين رأيت التابِع next()‎، يمكننا المرور على عناصره عبر for..of: function* generateSequence() { yield 1; yield 2; return 3; } let generator = generateSequence(); for(let value of generator) { alert(value); // ‏1 ثمّ 2 } هذا أجمل من استدعاء ‎.next().value، أم لا؟ ولكن… لاحِظ: يعرض المثال أعلاه 1 ثمّ 2 وفقط. لن يعرض 3 مطلقًا! هذا لأنّ عملية التكرار لـِ for..of تتجاهل قيمة value الأخيرة حين تكون done: true. لذا لو أردنا أن تظهر النتائج كلّها لعملية تكرار for..of، فعلينا إعادتها باستعمال yield: function* generateSequence() { yield 1; yield 2; yield 3; // هكذا } let generator = generateSequence(); for(let value of generator) { alert(value); // ‏1 ثمّ 2 ثمّ 3 } نظرًا من كون المولّدات قابلة للتكرار، يمكننا استدعاء جميع الدوالّ المتعلّقة بذلك، مثل: معامل «البقية» ...: function* generateSequence() { yield 1; yield 2; yield 3; } let sequence = [0, ...generateSequence()]; alert(sequence); // 0, 1, 2, 3 يحول التابع ‎...generateSequence()‎ في الشيفرة أعلاه كائن المولد القابل للتكرار إلى مصفوفة من العناصر (لمزيد من المعلومات أحيلك إلى هذا المقال "المُعاملات «البقية» ومُعامل التوزيع"). استعمال المولدات على أنها مكرّرات سابقًا في فصل "المُعاملات «البقية» ومُعامل التوزيع" أنشأنا كائن range يمكن تكراره والذي يعيد القيم بين قيميتين from..to. لنتذكّر الشيفرة معًا: let range = { from: 1, to: 5, // ‫تستدعي حلقة for..of هذه الدالّة مرة واحدة في البداية [Symbol.iterator]() { // ‫...ستعيد كائن مُكرُر // ‫لاحقًا ستعمل حلقة for..of مع ذلك الكائن وتطلب منه القيم التالية return { current: this.from, last: this.to, // ‫تستدعى next()‎ في كلّ تكرار من خلال الحلقة for..of next() { // ‫يجب أن تعيد القيم ككائن {done:.., value :...} if (this.current <= this.last) { return { done: false, value: this.current++ }; } else { return { done: true }; } } }; } }; // ‫عملية التكرار تمرُّ عبر range من range.from إلى range.to alert([...range]); // 1,2,3,4,5 يمكننا استعمال دالة المولِّدة كمكرّرات من خلال Symbol.iterator. إليك نفس الكائن range, ولكن بطريقة أكثر إيجازًا: let range = { from: 1, to: 5, *[Symbol.iterator]() { // ‫اختصارًا لـِ ‎[Symbol.iterator]: function*‎()‎ for(let value = this.from; value <= this.to; value++) { yield value; } } }; alert( [...range] ); // 1,2,3,4,5 الشيفرة تعمل إذ يُعيد range[Symbol.iterator]()‎ الآن مولّدًا، وما تتوقّعه for..of هي توابِع تلك المولّدات بعينها: إذ لها التابِع ‎.next()‎ وتُعيد القيم على النحو الآتي {value: ..., done: true/false} بالطبع هذه ليست مصادفة أضيفت المولّدات إلى لغة جافاسكربت مع الأخذ بعين الاعتبار للمكرّرات لتنفيذهم بسهولة. إن التنوع في المولدات أعطى شيفرة موجزة أكثر الشيفرة الأصلية لكائن range، وجميعهم لديهم نفس الخصائص الوظيفية. ملاحظة: المولّدات يمكن أن تولد قيمًا للأبد في الأمثلة أعلاه أنشأنا متتالية منتهية من القيم، ولكن باستخدام المولد نستطيع أن ننتج قيمًا إلى الأبد. على سبيل المثال لننشئ متتالية غير منتهية من الأرقام العشوائية الزائفة. ومن المؤكد أن هذا المولّد سيحتاجُ إلى طريقة لإيقافه مثل: break (أو return) في حلقة for..of. وإلا فإن الحلقة ستستمر إلى الأبد. تراكب المولّدات تراكب المولّدات (Generator composition) هي ميزة خاصّة للمولّدات تتيح لها ”تضمين“ المولّدات الأخرى فيها دون عناء. فمثلًا لدينا هذه الدالة التي تُولّد سلسلة من الأعداد: function* generateSequence(start, end) { for (let i = start; i <= end; i++) yield i; } نريد الآن إعادة استعمالها لتوليد سلسلة أعداد معقّدة أكثر من هذه أعلاه: أولًا الأرقام 0..9 (ورموز المحارف اليونيكوديّة هو من 48 إلى 57) ثمّ الأحرف الأبجدية الإنجليزية بالحالة الكبيرة A..Z (ورموزها من 65 إلى 90) ثمّ الأحرف الأبجدية الإنجليزية بالحالة الصغيرة a..z (ورموزها من 97 إلى 122) يمكننا استعمال هذه السلسلة لإنشاء كلمات السرّ باختيار المحارف منها مثلًا (ويمكننا إضافة المحارف الخاصّة أيضًا). ولكن لذلك علينا توليدها أولًا. علينا في الدوالّ العادية (لتضمين النواتج من دوالّ أخرى) استدعاء تلك الدوالّ وتخزين نواتجها ومن ثمّ ربطها في نهاية الدالة الأمّ. وفي المولّدات نستعمل الصياغة المميّزة yield* لتضمين (أو تركيب) مولّدين داخل بعض. المولّد المركّب: function* generateSequence(start, end) { for (let i = start; i <= end; i++) yield i; } function* generatePasswordCodes() { // 0..9 yield* generateSequence(48, 57); // A..Z yield* generateSequence(65, 90); // a..z yield* generateSequence(97, 122); } let str = ''; for(let code of generatePasswordCodes()) { str += String.fromCharCode(code); } alert(str); // 0..9A..Za..z توجه yield*‎ التنفيذ إلى مولّد آخر. مما يعني أن yield* gen ستكرر عبر المولّد gen وستُعيد نتائجه للخارج. أي كما او أنتجت هذه القيم بمولّد خارجي. النتيجة نفسها كما لو أننا ضمنَا الشيفرة من المولّدات المتداخلة: function* generateSequence(start, end) { for (let i = start; i <= end; i++) yield i; } function* generateAlphaNum() { // yield* generateSequence(48, 57); for (let i = 48; i <= 57; i++) yield i; // yield* generateSequence(65, 90); for (let i = 65; i <= 90; i++) yield i; // yield* generateSequence(97, 122); for (let i = 97; i <= 122; i++) yield i; } let str = ''; for(let code of generateAlphaNum()) { str += String.fromCharCode(code); } alert(str); // 0..9A..Za..z تراكب المولّدات هو فعلًا طريقة بديهيّة لإدخال أكثر من مولّد في مولّد واحد، ولا تستعمل ذاكرة إضافية لتخزين النواتج البينيّة. عبارة ”yield“ باتجاهين اثنين إلى هنا نرى المولّدات تشبه كائنات المكرّرات كثيرًا، فقط أنّ لها صياغة مميّزة لتوليد القيم. ولكن وعلى أرض الواقع، المولّدات أكثر مرونة وفاعليّة. إذ أنّ yield تعمل بالمجيء وبالإياب: فلا تُعيد الناتج إلى الخارج فحسب بل أيضًا يمكنها تمرير القيمة إلى داخل المولّد. لذلك علينا استدعاء generator.next(arg)‎ بتمرير وسيط واحد. هذا الوسيط سيكون ناتج yield. الأفضل لو نرى مثالًا: function* gen() { // نمرّر سؤالًا إلى الشيفرة الخارجية وننتظر إجابةً عليه let result = yield "2 + 2 = ?"; // (*) alert(result); } let generator = gen(); let question = generator.next().value; // <-- تُعيد yield القيمة generator.next(4); // --> نمرّر القيمة إلى المولّد يكون الاستدعاء الأوّل للتابِع generator.next()‎ دومًا دون تمرير أيّ وسيط. يبدأ الاستدعاء التنفيذَ ويُعيد ناتج أوّل yield "2+2=?‎". هنا يُوقف المولّد التنفيذ مؤقّتًا (على ذلك السطر). ثمّ (كما نرى في الصورة) يُوضع ناتج yield في متغير السؤال question في الشيفرة التي استدعت المولّد. وعند generator.next(4)‎ يُواصل المولّد عمله ويستلم 4 ناتجًا: let result = 4. لاحِظ أنّه ليس على الشيفرة الخارجية استدعاء next(4)‎ مباشرةً وفي الحال، بل يمكن أن تأخذ الوقت الذي تريد. سيبقى المولّد منتظرًا ولن تكون مشكلة. مثال: // نُواصل عمل المولّد بعد زمن معيّن setTimeout(() => generator.next(4), 1000); كما نرى فعلى العكس تمامًا من الدوال العادية، يمكن للمولّد ولشيفرة الاستدعاء تبادل النتائج بتمرير القيم إلى next/yield. ليتوضّح هذا أكثر سنرى مثالًا آخر فيه استدعاءات أكثر: function* gen() { let ask1 = yield "2 + 2 = ?"; alert(ask1); // 4 let ask2 = yield "3 * 3 = ?" alert(ask2); // 9 } let generator = gen(); alert( generator.next().value ); // "2 + 2 = ?" alert( generator.next(4).value ); // "3 * 3 = ?" alert( generator.next(9).done ); // true إليك صورة سير التنفيذ: استدعاء .next()‎ الأوّل يبدأ التنفيذ، ويصل إلى أوّل عبارة yield. يُعاد الناتج إلى الشيفرة الخارجية. يمرّر استدعاء .next(4)‎ الثاني القيمة 4 إلى المولّد ثانيةً على أنّها ناتج أوّل مُنتَج yield، ويُواصل التنفيذ. يصل التنفيذ إلى عبارة yield الثانية، وتصير هي ناتج الاستدعاء. يُمرّر next(9)‎ الثالث القيمة 9 إلى المولّد على أنّها ناتج ثاني مُنتَج yield ويُواصل التنفيذ حتّى يصل نهاية الدالة، بذلك تكون done: true. تشبه هذه لعبة تنس الطاولة، حيث يمرّر كلّ تابِع next(value)‎ (باستثناء الأوّل طبعًا) القيمة إلى المولّد فتصير ناتج المُنتَج yield الحالي، ومن ثمّ generator.throw يمكن للشيفرات الخارجية تمرير القيم إلى المولّدات على أنّها نواتج yield (كما لاحظنا من الأمثلة أعلاه). ويمكنها أيضًا بدء (أو رمي) خطأ أيضًا. هذا طبيعي إذ الأخطاء هي نواتج، نوعًا ما. علينا استدعاء التابِع yield لتمرير الأخطاء إلى عبارة generator.throw(err)‎. في هذه الحال يُرمى الخطأ err عند السطر فمثلًا تؤدّي عبارة "‎2 + 2 = ?‎" هنا إلى خطأ: function* gen() { try { let result = yield "2 + 2 = ?"; // (1) alert("The execution does not reach here, because the exception is thrown above"); } catch(e) { alert(e); // أظهر الخطأ } } let generator = gen(); let question = generator.next().value; generator.throw(new Error("The answer is not found in my database")); // (2) الخطأ الذي رميَ في المولد عند السطر (2) يقودنا إلى الخطأ في السطر (1) مع yield. في المثال أعلاه التقطت try..catch الخطأ وثمّ عرضته. لو لم نلتقطه سيكون مآله (مآل أيّ استثناء آخر غيره) أن ”يسقط“ من المولّد إلى الشيفرة التي استدعت المولّد. هل يمكننا التقاط الخطأ في سطر شيفرة الاستدعاء الّتي تحتوي على generator.throw، (المشار إليه (2))، هكذا؟ function* generate() { let result = yield "2 + 2 = ?"; // خطأ في هذا السطر } let generator = generate(); let question = generator.next().value; try { generator.throw(new Error("The answer is not found in my database")); } catch(e) { alert(e); // عرض الخطأ } إن لم نلتقط الخطأ هنا فعندئذ وكما هي العادة ستنهار الشيفرة الخارجية (إن كانت موجودة) وإذا لم يكتشف الخطأ أيضًا عندها سينهار السكربت بالكامل. خلاصة تُنشِئ الدوال المولِّدة function* f(…) {…}‎ المولّدات. يوجد المُعامل yield داخل المولدات (فقط). تتبادل الشيفرة الخارجية مع المولدات النتائج من خلال استدعاءات next/yield. نادرًا ما تستخدم المولّدات في الإصدار الحديث من جافاسكربت. لكن في بعض الأحيان نستخدمها لتبادل البيانات بين الشيفرة المستدعاة أثناء تنفيذ شيفرة وحيدة. وبالتأكيد إنها رائعة لتوليد أشياء قابلة للتكرار. في الفصل التالي، سنتعرف على مولدات غير متزامنة، والّتي تُستخدم لقراءة تدفقات البيانات غير المتزامنة (على سبيل المثال ، نرى على الإنترنت خدمات كثيرة تقدّم لنا البيانات على صفحات [paginated]) في حلقات for await ... of. في برمجة الوِب، غالبًا ما نعمل مع البيانات المتدفقة، لذا فهذه حالة استخدام أخرى مهمة جدًا. تمارين مولد أرقام شبه عشوائية نواجه كثيرًا من الأحيان حاجة ماسّة إلى بيانات عشوائية. إحدى هذه الأحيان هي إجراء الاختبارات، فنحتاج إلى بيانات عشوائية كانت نصوص أو أعداد أو أيّ شيء آخر لاختبار الشيفرات والبنى البرمجية. يمكننا في جافاسكربت استعمال Math.random()‎، ولكن لو حدث خطب ما فنودّ إعادة إجراء الاختبار باستعمال نفس البيانات بالضبط (كي نختبر هذه البيانات). لهذا الغرض نستعمل ما يسمّى ”بمولّدات الأعداد شبه العشوائية المزروعة“. تأخذ هذه المولّدات "البذرة“ والقيمة الأولى ومن ثمّ تولّد القيم اللاحقة باستعمال معادلة رياضية بحيث أنّ كلّ بذرة تُنتج نفس السلسلة دائمًا، وهكذا يمكننا ببساطة استنساخ التدفق بالكامل من خلال بذورها فقط. إليك مثال عن هذه المعادلة التي تولّد قيمًا موزّعة توزيعًا next = previous * 16807 % 2147483647 لو استعملنا 1 …، فستكون القيم كالآتي: 16807 282475249 1622650073 …وهكذا… مهمّة هذا التمرين هو إنشاء الدالة المولِّدة pseudoRandom(seed)‎ فتأخذ البذرة seed وتُنشِئ مولّدًا بالمعادلة أعلاه. طريقة الاستعمال: let generator = pseudoRandom(1); alert(generator.next().value); // 16807 alert(generator.next().value); // 282475249 alert(generator.next().value); // 1622650073 الحل function* pseudoRandom(seed) { let value = seed; while(true) { value = value * 16807 % 2147483647 yield value; } }; let generator = pseudoRandom(1); alert(generator.next().value); // 16807 alert(generator.next().value); // 282475249 alert(generator.next().value); // 1622650073 لاحِظ أننا نستطيع تأدية ذات الأمر بالدوال العادية هكذا: function pseudoRandom(seed) { let value = seed; return function() { value = value * 16807 % 2147483647; return value; } } let generator = pseudoRandom(1); alert(generator()); // 16807 alert(generator()); // 282475249 alert(generator()); // 1622650073 ستعمل هذه أيضًا. ولكن بعد ذلك سنفقدُ قابلية التكرار باستخدام for..of واستخدام المولّد المركب، وتلك ممكن أن تكون مفيدة في مكان آخر. ترجمة -وبتصرف- للفصل Generators من كتاب The JavaScript language
  3. توجد صياغة مميّزة للعمل مع الوعود بنحوٍ أكثر سهولة تُدعى async/await. فهمها أسهل من شرب الماء واستعمالها الدوال غير المتزامنة فلنبدأ أولًا بكلمة async المفتاحية. يمكننا وضعها قبل الدوال هكذا: async function f() { return 1; } وجود الكلمة ”async“ قبل (اختصار ”غير متزامنة“ بالإنجليزية) يعني أمرًا واحدًا: تُعيد الدالة وعدًا دومًا. فمثلًا تُعيد هذه الدالة وعدًا مُنجز فيه ناتج 1. فلنرى: async function f() { return 1; } f().then(alert); // 1 كما يمكننا أيضًا إعادة وعد صراحةً: async function f() { return Promise.resolve(1); } f().then(alert); // 1 هكذا تضمن لنا async بأنّ الدالة ستُعيد وعدًا وستُغلّف الأشياء التي ليست بوعود وعودًا. بسيطة صح؟ ليس هذا فحسب، بل هناك أيضًا الكلمة المفتاحية await التي تعمل فقط في الدوال غير المتزامنة async، وهي الأخرى جميلة. Await الصياغة: // لا تعمل إلّا في الدوال غير المتزامنة let value = await promise; الكلمة المفتاحية تجعل لغة جافاسكربت تنتظر حتى يُنجز الوعد ويعيد نتيجة. إليك مثالًا لوعد نُفذّ خلال ثانية واحدة: async function f() { let promise = new Promise((resolve, reject) => { setTimeout(() => resolve("done!"), 1000) // تم!‏ }); let result = await promise; // ننتظر ... الوعد (*) alert(result); // "done!" تم!‏ } f(); يتوقّف تنفيذ الدالة ”قليلًا“ عند السطر (*) ويتواصل متى ما أُنجز الوعد وصار result ناتجه. الشيفرة أعلاه تعرض ”تم!“ بعد ثانية واحدة. لنوضح أمرًا مهمًا: تجعل await لغة جافاسكربت تنتظر حتى يُنجز الوعد، وبعدها تذهب مع النتيجة. هذا لن يكلفنا أي موارد من المعالج لأن المحرك مُنشغل بمهام أخرى في الوقت نفسه، مثل: تنفيذ سكربتات أُخرى، التعامل مع الأحداث ..إلخ. هي مجرد صياغة أنيقة أكثر من صياغة promise.then للحصول على ناتج الوعد. كما أنها أسهل للقراءة والكتابة.. تحذير: لا يمكننا استخدام await في الدوال العادية إذا حاولنا استخدام الكلمة المفتاحية await في الدوال العادية فسيظهر خطأ في في الصياغة: function f() { let promise = Promise.resolve(1); let result = await promise; // Syntax error } سنحصل على هذا الخطأ إذا لم نضع async قبل الدالّة، كما قلنا await تعمل فقط في الدوالّ غير المتزامنة. لنأخذ مثال showAvatar()‎ من الفصل سَلسلة الوعود ونُعيد كتابته باستعمال async/await: علينا استبدال استدعاءات ‎.then ووضع await. علينا أيضًا تحويل الدالة لتكون غير متزامنة async كي تعمل await. async function showAvatar() { // ‫إقرأ ملفات JSON let response = await fetch('/article/promise-chaining/user.json'); let user = await response.json(); // ‫إقرأ مستخدم github let githubResponse = await fetch(`https://api.github.com/users/${user.name}`); let githubUser = await githubResponse.json(); // ‫أظهرالصورة الرمزية avatar let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.append(img); // انتظر 3 ثواني await new Promise((resolve, reject) => setTimeout(resolve, 3000)); img.remove(); return githubUser; } showAvatar(); شيفرة نظيفة وسهلة القراءة! أفضل من السابقة بأميال. ملاحظة: لن تعمل await في الشيفرة ذات المستوى الأعلى يميل المبتدئين إلى نسيان أن await لن تعمل في الشيفرة البرمجية ذات هرمية أعلى(ذات المستوى الأعلى). فمثلًا لن يعمل هذا: // خطأ صياغيّ في الشيفرة عالية المستوى let response = await fetch('/article/promise-chaining/user.json'); let user = await response.json(); يمكننا تغليفها بداخل دالّة متزامنة مجهولة، هكذا: (async () => { let response = await fetch('/article/promise-chaining/user.json'); let user = await response.json(); ... })(); ملاحظة: إن await تقبل "thenables" تسمح await باستخدام كائنات thenable (تلك التي تستخدم دالة then القابلة للاستدعاء) تمامًا مثل promise.then. الفكرة أنه يمكن ألا يكون الكائن الخارجي وعدًا ولكنه متوافق مع الوعد: إن كان يدعم ‎.then، وهذا يكفي لاستخدامه مع await. هنا مثال لاستخدام صنف Thenable والكلمة المفتاحية await أدناه ستقبلُ حالاتها: class Thenable { constructor(num) { this.num = num; } then(resolve, reject) { alert(resolve); // ‫نفذ عملية this.num*2 بعد ثانية واحدة setTimeout(() => resolve(this.num * 2), 1000); // (*) } }; async function f() { // انتظر لثانية واحدة. ثم النتيجة ستصبح 2 let result = await new Thenable(1); alert(result); } f(); إذا حصَلت await على كائن ليس وعدًا مع ‎.then فإنه يستدعي الدوالّ المضمنة في اللغة مثل: resolve و reject كوسطاء (كما يفعل المنفذّ للوعد العادي تمامًا). وثم تنتظر await حتى يستدعى أحدهم (في المثال أعلاه في السطر (*)) ثم يواصل مع النتيجة. ملاحظة: لتعريف دالّة صنف غير متزامنة إضفها مع الكلمة async: class Waiter { async wait() { return await Promise.resolve(1); } } new Waiter() .wait() .then(alert); // 1 لاحظ أن المعنى هو نفسه: فهو يضمن أن القيمة المرتجعة هي وعد وawait مفعّلة أيضًا. التعامل مع الأخطاء لو نُفذّ الوعد بنجاح فسيُعيد await promise الناتج، ولكن لو حصلت حالة رفض فسترمي الخطأ كما لو كانت إفادة throw مكتوبة. إليك الشيفرة: async function f() { await Promise.reject(new Error("Whoops!")); // لاحظ } و… هي ذاتها هذه: async function f() { throw new Error("Whoops!"); // هنا } في الواقع تأخذ الوعود وقتًا قبل أن تُرفض. في تلك لحالة فستكون هناك مهلة قبل أن يرمي await الخطأ. يمكننا التقاط ذاك الخطأ باستعمال try..catch كما استعملنا throw: async function f() { try { let response = await fetch('http://no-such-url'); } catch(err) { alert(err); // TypeError: failed to fetch خطأ في النوع: فشل الجلب } } f(); لو حدث خطأ فينتقل سير التنفيذ إلى كتلة catch. يمكننا أيضًا وضع أكثر من سطر واحد: async function f() { try { let response = await fetch('/no-user-here'); let user = await response.json(); } catch(err) { // ‫تلتقط أخطاء fetch وresponse.json معًا alert(err); } } f(); لو لم نضع try..catch فستكون حالة الوعد الذي نفّذه استدعاء الدالة غير المتزامنة f()‎ - يكون بحالة رفض. يمكننا هنا استعمال ‎.catch للتعامل معه: async function f() { let response = await fetch('http://no-such-url'); } // ‫يصير استدعاء f()‎ وعدًا مرفوضًا f().catch(alert); // TypeError: failed to fetch خطأ في النوع: فشل الجلب (*) لو نسينا هنا إضافة ‎.catch فسنتلقّى خطأ وعود لم نتعامل معه (يمكن أن نراه من الطرفية). يمكننا أيضًا استلام هذه الأخطاء باستعمال دالة مُعاملة الأحداث العمومية كما وضّحنا في الفصل …. ملاحظة: "async/await وpromise.then/catch" عندما نستخدم async/await نادرًا ما نحتاج إلى ‎.then وذلك لأن await تعالج عملية الانتظار. ويمكننا استخدام try..catch بدلًا من ‎.catch. وهذه عادةً (ليس دائمًا) ما تكون أكثر ملاءمة. ولكن في الشيفرات ذات هرمية أعلى (مستوى أعلى)، عندما نكون خارج أي دالّة غير متزامنة، يتعذر علينا استخدام await لذا من المعتاد إضافة .then/catch لمعالجة النتيجة النهائية أو الأخطاء المتساقطة. كما هو الحال في السطر (*) من المثال أعلاه. ملاحظة: "تعمل async/await مع Promise.all" عندما نحتاج لانتظار عدة وعود يمكننا أن نغلفها في تابع Promise.all وبعده await: // انتظر مصفوفة النتائج let results = await Promise.all([ fetch(url1), fetch(url2), ... ]); وإن حدث خطأ ما، فإنه سيُنقل من الوعد نفسه إلى التابع Promise.all، وبعدها يصبح استثناءً يمكننا التقاطه باستخدام try..catch حول الاستدعاء. خلاصة لكلمة async المفتاحية قبل الدوال تأثيرين اثنين: تحوّلها لتُعيد وعدًا دومًا. تتيح استعمال await فيها. حين يرى محرّك جافاسكربت الكلمة المفتاحية await قبل الوعود، ينتظر حتّى يُنجز الوعد ومن ثمّ: لو كان خطأ فسيُولِّد الاستثناء كما لو استعملنا throw error. وإلّا أعاد الناتج. تقدّم لنا هتين الكلمتين معًا إطار عمل رائع نكتب به شيفرات غير متزامنة تسهل علينا قراءتها كما وكتابتها. نادرًا ما نستعمل promise.then/catch بوجود async/await، ولكن علينا ألّا ننسى بأنّ الأخيرتين مبنيّتين على الوعود إذ نضطر أحيانًا (خارج الدوال مثلًا) استعمال promise.then/catch. كما وأنّ Promise.all جميل جدًا لننتظر أكثر من مهمّة في وقت واحد. تمارين إعادة الكتابة باستعمال async/await أعِد كتابة الشيفرة في المثال من الفصل سلسلة الوعود باستعمال async/await بدل ‎.then/catch: function loadJson(url) { return fetch(url) .then(response => { if (response.status == 200) { return response.json(); } else { throw new Error(response.status); } }) } loadJson('no-such-user.json') // (3) .catch(alert); // Error: 404 الحل ترى الملاحظات أسفل الشيفرة: async function loadJson(url) { // (1) let response = await fetch(url); // (2) if (response.status == 200) { let json = await response.json(); // (3) return json; } throw new Error(response.status); } loadJson('no-such-user.json') .catch(alert); // Error: 404 (4) ملاحظات: الدالّة loadJson تصبح async. جميع المحتوى في .then يستبدل بـِ await. يمكننا إعادة return response.json()‎ بدلًا من انتظارها. هكذا: if (response.status == 200) { return response.json(); // (3) } ثم الشيفرة الخارجية ستنتظر await لينفذ الوعد في حالتنا الأمر غير مهم. سيرمى الخطأ من التابع loadJson المعالج من قبل ‎.catch. لن نستطيع استخدام await loadJson(…)‎ هنا، وذلك لأننا لسنا في دالّة غير متزامنة. أعِد كتابة rethrow باستعمال async/await في الشيفرة أدناه مثلًا عن إعادة الرمي من فصل سلسلة الوعود. أعد كتابته باستخدام async/await بدلًا من ‎.then/catch. وتخلص من العودية لصالح الحلقة في demoGithubUser: مع استخدام async/await سيسهلُ الأمر. class HttpError extends Error { constructor(response) { super(`${response.status} for ${response.url}`); this.name = 'HttpError'; this.response = response; } } function loadJson(url) { return fetch(url) .then(response => { if (response.status == 200) { return response.json(); } else { throw new HttpError(response); } }) } // ‫اطلب اسم المستخدم إلى أن يعيد لك github مستخدم صحيح function demoGithubUser() { let name = prompt("Enter a name?", "iliakan"); return loadJson(`https://api.github.com/users/${name}`) .then(user => { alert(`Full name: ${user.name}.`); return user; }) .catch(err => { if (err instanceof HttpError && err.response.status == 404) { alert("No such user, please reenter."); return demoGithubUser(); } else { throw err; } }); } demoGithubUser(); الحل لا يوجد أي صعوبة هنا، إذ كل ما عليك فعله هو استبدال try...catch بدلًا من ‎.catch بداخل تابع demoGithubUser وأضف async/await عند الحاجة: class HttpError extends Error { constructor(response) { super(`${response.status} for ${response.url}`); this.name = 'HttpError'; this.response = response; } } async function loadJson(url) { let response = await fetch(url); if (response.status == 200) { return response.json(); } else { throw new HttpError(response); } } // ‫اطلب اسم المستخدم إلى أن يعيد لك github مستخدم صحيح async function demoGithubUser() { let user; while(true) { let name = prompt("Enter a name?", "iliakan"); try { user = await loadJson(`https://api.github.com/users/${name}`); break; // لا يوجد خطأ اخرج من الحلقة } catch(err) { if (err instanceof HttpError && err.response.status == 404) { // ‫تستمر الحلقة بعد alert alert("No such user, please reenter."); } else { // ‫خطأ غير معروف , أعد رميه rethrow throw err; } } } alert(`Full name: ${user.name}.`); return user; } demoGithubUser(); استدعاء async من دالة غير متزامنة لدينا دالة ”عادية“، ونريد استدعاء async منها واستعمال ناتجها، كيف؟ async function wait() { await new Promise(resolve => setTimeout(resolve, 1000)); return 10; } function f() { // ...ماذا نكتب هنا؟ // ‫علينا استدعاء async wait()‎ والانتظار حتّى تأتي 10 // ‫لا تنسَ، لا يمكن استعمال ”await“ هنا } ملاحظة: هذه المهمّة (تقنيًا) بسيطة جدًا، ولكنّ السؤال شائع بين عموم المطوّرين الجدد على async/await. الحل هنا تأتي فائدة معرفة طريقة عمل هذه الأمور. ليس عليك إلّا معاملة استدعاء async وكأنّه وعد ووضع تابِع ‎.then: async function wait() { await new Promise(resolve => setTimeout(resolve, 1000)); return 10; } function f() { // يعرض 10 بعد ثانية واحدة wait().then(result => alert(result)); } f(); ترجمة -وبتصرف- للفصل Async/await من كتاب The JavaScript language
  4. دوال المُعاملة للوعود ‎.then و ‎.catch و‎.finally هي دوال غير متزامنة، دومًا. فحتّى لو سويَ الوعد مباشرةً (أي سواءً أنُجز أو رُفض) فالشيفرة أسفل ‎.then و‎.catch و‎.finally ستُنفّذ حتّى قبل دوال المعاملة. لاحِظ: let promise = Promise.resolve(); promise.then(() => alert("promise done!")) // اكتمل الوعد alert("code finished"); // ‫نرى هذا النص أولًا (انتهت الشيفرة) لو شغّلته فسترى أولًا code finished وبعدها ترى promise done!. هذا… غريب إذ أن الوعد أنجز قطعًا في البداية. لماذا شُغّلت ‎.then بعدئذ؟ ما الّذي يحدث؟ طابور المهام السريعة تطلب الدوال غير المتزامنة عملية إدارة مضبوطة. ولهذا تحدّد مواصفة اللغة طابورًا داخليًا باسم PromiseJobs (غالبًا ما نسمّيه ”بطابور المهام السريعة“ Microtask Queue حسب مصطلح محرّك V8). تقول المواصفة: الطابور بمبدأ ”أوّل من يدخل هو أوّل من يخرج“: المهام التي تُضاف أولًا تُنفّذ أولًا. لا يبدأ تنفيذ المهمة إلّا لو لم يكن هناك شيء آخر يعمل. وبعبارة أبسط، فمتى جهز الوعد تُضاف دوال المعاملة ‎.then/catch/finally إلى الطابور، وتبقى هناك بلا تنفيذ. متى وجد محرّك جافاسكربت نفسه قد فرغ من الشيفرة الحالية، يأخذ مهمة من الطابور وينفّذها. لهذا السبب نرى ”اكتملت الشيفرة“ في المثال أعلاه أولًا. دوال معاملة الوعود تمرّ من الطابور الداخلي هذا دومًا. لو كانت في الشيفرة سلسلة من ‎.then/catch/finally فستُنفّذ كلّ واحدة منها تنفيذًا غير متزامن. أي أنّ الأولى تُضاف إلى الطابور وتُنفّذ متى اكتمل تنفيذ الشيفرة الحالية وانتهت دوال المُعاملة الّتي أُضيفت إلى الطابور مسبقًا. ولكن ماذا لو كان الترتيب يهمّنا؟ كيف نشغّل code finished بعد promise done؟ بسيطة، نضعها في الطابور باستعمال ‎.then: Promise.resolve() .then(() => alert("promise done!")) .then(() => alert("code finished")); هكذا صار الترتيب كما نريد. الرفض غير المعالج تذكر حدث unhandledrejection من فصل التعامل مع الأخطاء في الوعود. سنرى الآن كيف تعرف محرّكات جافاسكربت ما إن وُجدت حالة رفض لم يُتعامل معها، أم لا. تحدث ”حالة الرفض لم يُتعامل معها“ حين لا يتعامل شيء مع خطأ أنتجه وعد في آخر طابور المهام السريعة. عادةً لو كنّا نتوقّع حدوث خطأ نُضيف التابِع ‎.catch إلى سلسلة الوعود للتعامل معه: let promise = Promise.reject(new Error("Promise Failed!")); promise.catch(err => alert('caught')); // هكذا // لا يعمل هذا السطر إذ تعاملنا مع الخطأ window.addEventListener('unhandledrejection', event => alert(event.reason)); ولكن… لو نسينا وضع ‎.catch سيُشغّل المحرّك هذا الحدث متى فرغ طابور المهام السريعة: let promise = Promise.reject(new Error("Promise Failed!")); // Promise Failed! window.addEventListener('unhandledrejection', event => alert(event.reason)); وماذا لو تعاملنا مع الخطأ لاحقًا؟ هكذا مثلًا: let promise = Promise.reject(new Error("Promise Failed!")); setTimeout(() => promise.catch(err => alert('caught')), 1000); // لاحِظ // Error: Promise Failed! window.addEventListener('unhandledrejection', event => alert(event.reason)); إذا شغلناه الآن سنرى Promise Failed! أولًا ثم caught. لو بقينا نجهل طريقة عمل طابور المهام السريعة فسنتساءل: ”لماذا عملت دالة المُعاملة unhandledrejection؟ الخطأ والتقطناه!“. أمّا الآن فنعرف أنّه لا يُولّد unhandledrejection إلّا حين انتهاء طابور المهام السريعة: فيفحص المحرّك الوعود وإن وجد حالة ”رفض“ في واحدة، يشغّل الحدث. في المثال أعلاه، أضيفت .catch وشغّلت من قِبل setTimeout متأخرةً بعد حدوث unhandledrejection لذا فإن ذلك لم يغيّر أي شيء. خلاصة التعامل مع الوعود دومًا يكون غير متزامن، إذ تمرّ إجراءات الوعود في طابور داخلي ”لمهام الوعود“ أو ما نسمّيه ”بطابور المهام السريعة“ (مصطلح المحرّك V8). بهذا لا تُستدعى دوال المُعاملة ‎.then/catch/finally إلّا بعد اكتمال الشيفرة الحالية. ولو أردنا أن نضمن تشغيل هذه الأسطر بعينها بعد ‎.then/catch/finally فيمكننا إضافتها إلى استدعاء ‎.then في السلسلة. في معظم محركات جافاسكربت، بما في ذلك المتصفحات و Node.js، يرتبط مفهوم المهام السريعة ارتباطًا وثيقًا بـ "حلقة الأحداث" والمهام الكبيرة "macrotasks". نظرًا لأنها لا تملك علاقة مباشرة بالوعود، لذا فإننا شرحناها في جزء آخر من السلسلة التعليمية، في الفصل Event loop: microtasks and macrotasks. ترجمة -وبتصرف- للفصل Microtasks من كتاب The JavaScript language
  5. تحويل الدوال إلى وعود (Promisification) هي عملية تغليف الدالة التي تستلم ردّ نداء لتصبح دالة تُعيد وعدًا. وفي الحياة العملية فهذا النوع من التحويل مطلوب جدًا إذ تعتمد العديد من الدوال والمكتبات على ردود النداء. ولكن… الوعود أسهل وأفضل لذا من المنطقي تحويل تلك الدوال. لنأخذ مثلًا دالة loadScript(src, callback)‎ من الفصل مقدمة إلى ردود النداء callback: function loadScript(src, callback) { let script = document.createElement('script'); script.src = src; script.onload = () => callback(null, script); script.onerror = () => callback(new Error(`Script load error for ${src}`)); document.head.append(script); } // الاستعمال // loadScript('path/script.js', (err, script) => {...}) هيا نحوّلها. على الدالة الجديدة loadScriptPromise(src)‎ القيام بنفس ما تقوم به تلك، ولكن لا تقبل إلّا src وسيطًا (بدون callback) وتُعيد وعدًا. let loadScriptPromise = function(src) { return new Promise((resolve, reject) => { loadScript(src, (err, script) => { if (err) reject(err) else resolve(script); }); }) } // الاستعمال // loadScriptPromise('path/script.js').then(...) الآن يمكننا استعمال loadScriptPromise بسهولة بالغة في الشيفرات التي تعتمد الوعود. وكما نرى فالدالة تكلّف الدالة الأصلية loadScript بكلّ العمل اللازم، وما تفعله هو تقديم ردّ نداء من عندها تحوّله إلى وعد resolve/reject. نحتاج عمليًا إلى تحويل دوال عديدة وكثيرة لتعتمد الوعود، لذا من المنطقي استعمال دالة مساعِدة. لنسمّها promisify(f)‎ وستقبل دالة الأصل f اللازم تحويلها، وتُعيد دالة غالِفة. يؤدّي الغلاف نفس عمل الشيفرة أعلاه: يُعيد وعدًا ويمرّر النداء إلى الدالة الأصلية f متتّبعًا الناتج في ردّ نداء يصنعه بنفسه: function promisify(f) { return function (...args) { // يعيد التابع المغلّف return new Promise((resolve, reject) => { function callback(err, result) { // ‫رد النداء خاصتنا لـِ f if (err) { return reject(err); } else { resolve(result); } } args.push(callback); // ‫نضيف رد النداء خاصتنا إلى نهاية واسطاء f f.call(this, ...args); // استدعي التابع الأصلي }); }; }; // ‫طريقة الاستخدام: let loadScriptPromise = promisify(loadScript); loadScriptPromise(...).then(...); نفترض هنا بأنّ الدالة الأصلية تتوقّع استلام ردّ نداء له وسيطين (err, result)، وهذا ما نواجهه أغلب الوقت لهذا كتبنا ردّ النداء المخصّص بهذا التنسيق، وتعمل دالة promisify على أكمل وجه… لهذه الحالات. ولكن ماذا لو كانت تتوقّع f ردّ نداء له وسطاء أكثر callback(err, res1, res2, ...)‎؟ إليك نسخة promisify ذكية محسّنة: لو استدعيناها هكذا promisify(f, true)‎ فسيكون ناتج الوعد مصفوفة من نواتج ردود النداء [res1, res2,‎ ...‎]: // ‫التابع promisify(f, true)‎ لجب مصفوفة النتائج function promisify(f, manyArgs = false) { return function (...args) { return new Promise((resolve, reject) => { function callback(err, ...results) { // ‫رد النداء خاصتنا لـِ f if (err) { return reject(err); } else { // إجلب جميع ردود النداء وإذا حدّد أكثر من وسيط resolve(manyArgs ? results : results[0]); } } args.push(callback); f.call(this, ...args); }); }; }; // ‫طريقة الاستخدام: f = promisify(f, true); f(...).then(arrayOfResults => ..., err => ...) أمّا لتنسيقات ردود النداء الشاذّة (مثل التي بدون وسيط err أصلًا callback(result)‎) فيمكننا تحويلها يدويًا بدون استعمال الدالة المساعِدة. كما وهناك وحدات لها دوال تحويل مرنة أكثر في التعامل مثل es6-promisify. وفي Node.js نرى الدالة المضمّنة util.promisify لهذا الغرض. ملاحظة: يعدّ تحويل الدوال إلى وعود نهجًا رائعًا، خاصةً عند استخدام async/await (الّتي سنراها في الفصل التالي)، ولكن ليس بديلًا كليًا لردود النداء. تذكر أن الوعد له نتيجة واحدة فقط، ولكن تقنيًا ممكن أن تُستدعى ردود النداء عدة مرات. لذا فإن تحويل الدوال إلى وعود مخصصة للدوال التي تستدعي ردود النداء لمرة واحدة. وستُتجاهل جميع الاستدعاءات اللاحقة. ترجمة -وبتصرف- للفصل Promisification من كتاب The JavaScript language
  6. ثمّة 5 توابِع ثابتة (static) في صنف الوعود Promise. سنشرح الآن عن استعمالاتها سريعًا. Promise.all لنقل بأنّك تريد تنفيذ أكثر من وعد واحد في وقت واحد، والانتظار حتّى تجهز جميعها. مثلًا أن تُنزّل أكثر من عنوان URL في آن واحد وتُعالج المحتوى ما إن تُنزّل كلها. وهذا الغرض من وجود Promise.all. إليك صياغته: let promise = Promise.all([...promises...]); يأخذ التابِع Promise.all مصفوفة من الوعود (تقنيًا يمكن أن تكون أيّ … ولكنّها في العادة مصفوفة) ويُعيد وعدًا جديدًا. لا يُحلّ الوعد الجديد إلّا حين تستقرّ الوعود في المصفوفة، وتصير تلك المصفوفة التي تحمل نواتج الوعود - تصير ناتج الوعد الجديد. مثال على ذلك تابِع Promise.all أسفله إذ يستقرّ بعد 3 ثوان ويستلم نواتجه في مصفوفة [1, 2, 3]: Promise.all([ new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1 new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2 new Promise(resolve => setTimeout(() => resolve(3), 1000)) // 3 ]).then(alert); // 1,2,3 ما إن تجهز الوعود يُعطي كلّ واحد عنصرًا في المصفوفة لاحِظ كيف أنّ ترتيب عناصر المصفوفة الناتجة يتطابق مع ترتيب الوعود، فحتّى لو أخذ الوعد الأوّل وقتًا أطول من غيره حتّى يُحلّ، فسيظلّ العنصر الأول في مصفوفة النواتج. نستعمل عادةً حيلة صغيرة أن نصنع مصفوفة جديدة بخارطة تأخذ القديمة وتحوّلها إلى وعود، ثمّ نمرّر ذلك كلّه إلى Promise.all. فمثلًا لو لدينا مصفوفة من العناوين، يمكننا جلبها كلّها هكذا: let urls = [ 'https://api.github.com/users/iliakan', 'https://api.github.com/users/remy', 'https://api.github.com/users/jeresig' ]; // ‫نحوّل كلّ عنوان إلى وعد التابِع fetch let requests = urls.map(url => fetch(url)); // ‫ينتظر Promise.all حتّى تُحلّ كلّ المهام Promise.all(requests) .then(responses => responses.forEach( response => alert(`${response.url}: ${response.status}`) )); من أكبر الأمثلة على جلب المعلومات. هي جلب المعلومات لمجموعة من مستخدمي موقع GitHub من خلال اسمائهم (يمكننا جلب مجموعة من السلع بحسب رقم المعرف الخاص بها، منطق الحل متشابه في كِلا الحالتين): let names = ['iliakan', 'remy', 'jeresig']; let requests = names.map(name => fetch(`https://api.github.com/users/${name}`)); Promise.all(requests) .then(responses => { // استدعيت جميع الردود بنجاح for(let response of responses) { alert(`${response.url}: ${response.status}`); // ‫أظهر 200 من أجل كلّ عنوان url } return responses; }) // ‫اربط مصفوفة الردود مع مصفوفة response.json()‎ لقراءة المحتوى .then(responses => Promise.all(responses.map(r => r.json()))) // ‫تحلل جميع الإجابات : وتكوّن مصفوفة "users" منهم .then(users => users.forEach(user => alert(user.name))); لو رُفض أيّ وعد من الوعود، سيرفض الوعد الذي يُعيده Promise.all مباشرةً بذلك الخطأ. مثال: Promise.all([ new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)), // هنا new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)), new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)) ]).catch(alert); // Error: Whoops! نرى هنا أنّ الوعد الثاني يُرفض بعد ثانيتين، ويؤدّي ذلك إلى رفض Promise.all كاملًا، وهكذا يُنفّذ التابِع ‎.catch ويصير الخطأ ناتج Promise.all كلّه. تحذير: في حالة حدوث خطأ ما، تُتجاهل الوعود الأُخرى لو أن وعدًا ما رُفضَ، ستُرفض جميع الوعود من خلال تابع Promise.all مباشرةً، متناسيًا بذلك الوعود الأخرى في القائمة. وعندها ستُتجاهل نتائجها أيضًا. على سبيل المثال، إن كان العديد من استدعاءات fetch كما هو موضح في المثال أعلاه، وفشِل أحدها، فسيستمر تنفيذ الاستدعاءات الأخرى. ولكن لن يشاهدها تابع Promise.all بعد الآن. ربما ستُنجز بقية الوعود، ولكن نتائجها ستُتجاهل في نهاية المطاف. لن يُلغي التابع Promise.all الوعود الأخرى، إذ لا يوجد مثل هذا الفعل في الوعود، سنغطي في فصل آخر طريقة المتبعة في إلغاء الوعود وسيساعدنا التابع AbortController في ذلك، ولكنه ليس جزءًا من واجهة الوعود البرمجية. ملاحظة: يسمح التابع Promise.all(iterable)‎ لغير الوعود (القيم النظامية) في iterable. يقبل التابع Promise.all(...)‎ وعودًا قابلة للتكرار (مصفوفات في معظم الوقت). ولكن لو لم تكُ تلك الكائنات وعودًا. فستُمرّر النتائج كما هي. فمثلًا، النتائج هنا [1, 2, 3]: Promise.all([ new Promise((resolve, reject) => { setTimeout(() => resolve(1), 1000) }), 2, 3 ]).then(alert); // 1, 2, 3 يمكننا تمرير القيم الجاهزة لتابِع Promise.all عندما يكون ذلك مناسبًا. Promise.allSettled إضافة حديثة هذه إضافة حديثة للغة، وقد يحتاج إلى تعويض نقص الدعم في المتصفحات القديمة. لو رُفض أحد الوعود في Promise.all فسيُرفض كلّه مجتمعًا. هذا يفيدنا لو أردنا استراتيجية ”إمّا كل شيء أو لا شيء“، أي حين نطلب وجود النتائج كلها كي نواصل: Promise.all([ fetch('/template.html'), fetch('/style.css'), fetch('/data.json') ]).then(render); // تابِع التصيير يطلب نواتج كلّ توابِع الجلب بينما ينتظر Promise.allSettled استقرار كلّ الوعود. للمصفوفة الناتجة هذه العناصر: {status:"fulfilled", value:result} من أجل الاستجابات الناجحة. ‎{status:"rejected", reason:error}‎ من أجل الأخطاء. مثال على ذلك هو لو أردنا جلب معلومات أكثر من مستخدم واحد. فلو فشل أحد الطلبات ما زلنا نريد معرفة معلومات البقية. فلنستعمل Promise.allSettled: let urls = [ 'https://api.github.com/users/iliakan', 'https://api.github.com/users/remy', 'https://no-such-url' ]; Promise.allSettled(urls.map(url => fetch(url))) .then(results => { // (*) results.forEach((result, num) => { if (result.status == "fulfilled") { alert(`${urls[num]}: ${result.value.status}`); } if (result.status == "rejected") { alert(`${urls[num]}: ${result.reason}`); } }); }); ستكون قيمة results في السطر (*) أعلاه كالآتي: [ {status: 'fulfilled', value: ...ردّ...}, {status: 'fulfilled', value: ...ردّ...}, {status: 'rejected', reason: ...كائن خطأ...} ] هكذا نستلم لكلّ وعد حالته وقيمته أو الخطأ value/error. تعويض نقص الدعم لو لم يكن المتصفّح يدعم Promise.allSettled فمن السهل تعويض ذلك: if(!Promise.allSettled) { Promise.allSettled = function(promises) { return Promise.all(promises.map(p => Promise.resolve(p).then(value => ({ state: 'fulfilled', value }), reason => ({ state: 'rejected', reason })))); }; } في هذه الشيفرة يأخذ التابِع promises.map قيم الإدخال ويحوّلها إلى وعود (في حال وصله شيء ليس بالوعد) ذلك باستعمال p => Promise.resolve(p)‎، بعدها يُضيف دالة المُعاملة ‎.then لكلّ وعد. هذه الدالة تحوّل النواتج الناجحة v إلى {state:'fulfilled', value:v}، والأخطاء r إلى {state:'rejected', reason:r}. هذا التنسيق الذي يستعمله Promise.allSettled بالضبط. يمكننا لآن استعمال Promise.allSettled لجلب نتائج كلّ الوعود الممرّرة حتّى لو رفضت بعضها. Promise.race يشبه التابِع Promise.all إلّا أنّه ينتظر استقرار وعد واحد فقط ويأخذ ناتجه (أو الخطأ). صياغته هي: let promise = Promise.race(iterable); فمثلًا سيكون الناتج هنا 1: Promise.race([ new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)), new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)), new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)) ]).then(alert); // 1 أوّل الوعود هنا كان أسرعها وهكذا صار هو الناتج. ومتى ”فاز أوّل وعد مستقرّ بالسباق“، تُهمل بقية النواتج/الأخطاء. Promise.resolve/reject نادرًا ما نستعمل Promise.resolve و Promise.reject في الشيفرات الحديثة إذ صياغة async/await (نتكلم عنها في مقال لاحق) نشرحها هنا ليكون شرحًا كاملًا، وأيضًا لمن لا يستطيع استعمال async/await لسبب أو لآخر. Promise.resolve(value)‎ يُنشئ وعد مُنجز بالناتجvalue. ويشبه: let promise = new Promise(resolve => resolve(value)); يستخدم هذا التابع للتوافقية، عندما يُتوقع من تابع ما أن يُعيد وعدًا. فمثلًا، يجلب التابِع أدناه loadCached عنوان URL ويخزن المحتوى في الذاكرة المؤقتة. ومن أجل الاستدعاءات اللاحقة لنفس عنوان URL سيحصل على المحتوى فورًا من الذاكرة المؤقتة، ولكنه يستخدم التابع Promise.resolve لتقديم وعدًا بهذا الأمر. لتكون القيمة المرتجعة دائمًا وعدًا: let cache = new Map(); function loadCached(url) { if (cache.has(url)) { return Promise.resolve(cache.get(url)); // (*) } return fetch(url) .then(response => response.text()) .then(text => { cache.set(url,text); return text; }); } يمكننا كتابة loadCached(url).then(…)‎ لأننا نضمن أن التابِع سيُعيد وعدًا. كما يمكننا دائمًا استخدام ‎.then بعد تابِع loadCached. وهذا هو الغرض من Promise.resolve في السطر (*). Promise.reject Promise.reject(error)‎ ينشئ وعد مرفوض مع خطأ. ويشبه: let promise = new Promise((resolve, reject) => reject(error)); عمليًا لا نستعمل هذا التابِع أبدًا. خلاصة لصنف الوعود Promise خمس توابِع ثابتة: Promise.all(promises)‎ -- ينتظر جميع الوعود لتعمل ويعيد مصفوفة بنتائجهم. وإذا رُفض أي وعدٍ منهم، سيرجع Promise.all خطأً، وسيتجاهلُ جميع النتائج الأخرى. Promise.allSettled(promises)‎ -- (التابع مضاف حديثًا) ينتظر جميع الوعود لتُنجز ليُعيد نتائجها كمصفوفة من الكائنات تحتوي على: -state: الحالة وتكون إما "fulfilled" أو "rejected". -value: القيمة (إذا تحقق الوعد) أو reason السبب (إذا رُفض). Promise.race(promises)‎ -- ينتظر الوعد الأول ليُنجز وتكون نتيجته أو الخطأ الذي سيرميه خرج هذا التابِع. Promise.resolve(value)‎ -- ينشئ وعدًا منجزًا مع القيمة الممرّرة. Promise.reject(error)‎ -- ينشئ وعدًا مرفوضًا مع الخطأ المُمرّر. ومن بينها Promise.all هي الأكثر استعمالًا عمليًا. ترجمة -وبتصرف- للفصل Promise API من كتاب The JavaScript language
  7. تُعدّ سلاسل الوعود ممتازة في التعامل مع الأخطاء، فمتى رُفض الوعد ينتقل سير التحكّم إلى أقرب دالة تتعامل مع حالة الرفض، وهذا عمليًا يسهّل الأمور كثيرًا. فمثلًا نرى في الشيفرة أسفله أنّ العنوان المرّر إلى fetch خطأ (ما من موقع بهذا العنوان) ويتعامل التابِع ‎.catch مع الخطأ: fetch('https://no-such-server.blabla') // هذا الموقع من وحي الخيال العلمي .then(response => response.json()) .catch(err => alert(err)) // TypeError: failed to fetch (يمكن أن يتغيّر النصّ بتغيّر الخطأ) وكما ترى فليس ضروريًا أن يكون التابِع ‎.catch مباشرةً في البداية، بل يمكن أن يظهر بعد تابِع ‎.then واحد أو أكثر حتّى. أو قد يكون الموقع سليمًا ولكنّ الردّ ليس كائن JSON صالح. الطريقة الأسهل في هذه الحالة لالتقاط كلّ الأخطاء هي بإضافة تابِع ‎.catch إلى نهاية السلسلة: fetch('/article/promise-chaining/user.json') .then(response => response.json()) .then(user => fetch(`https://api.github.com/users/${user.name}`)) .then(response => response.json()) .then(githubUser => new Promise((resolve, reject) => { let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.append(img); setTimeout(() => { img.remove(); resolve(githubUser); }, 3000); })) .catch(error => alert(error.message)); // هنا الطبيعي هو ألّا يعمل تابِع ‎.catch مطلقًا، ولكن لو رُفض أحد الوعود أعلاه (بسبب مشكلة في الشبكة أو كائن غير صالح أو أيّ شيء آخر)، فسيستلم التابِع الخطأ. صياغة try..catch الضمنية تُحيط بشيفرتي مُنفّذ الوعود ودوال مُعاملة الوعود عبارة try..catch مخفية إن صحّ القول، فإن حدث استثناء يُلتقط ويتعامل معه المحرّك على أنّه حالة رفض. خُذ مثلًا هذه الشيفرة: new Promise((resolve, reject) => { throw new Error("Whoops!"); // لاحِظ }).catch(alert); // Error: Whoops! … وهل تعمل تمامًا مثل عمل هذه: new Promise((resolve, reject) => { reject(new Error("Whoops!")); // لاحِظ }).catch(alert); // Error: Whoops! تتلقّى عبارة try..catch المخفي المُحيطة بالمُنفّذ الأخطاءَ تلقائيًا وتحوّلها إلى وعود مرفوضة. ولا يحدث هذا في دالة المُنفّذ فحسب بل أيضًا في دوال المُعاملة. فلو رمينا شيئًا في دالة المُعاملة للتابِع throw فيعني ذلك بأنّ الوعد رُفض وينتقل سير التحكّم إلى أقرب دالة تتعامل مع الأخطاء. إليك مثالًا: new Promise((resolve, reject) => { resolve("ok"); }).then((result) => { throw new Error("Whoops!"); // نرفض الوعد }).catch(alert); // Error: Whoops! وهذا يحدث مع الأخطاء كافّة وليس فقط لتلك التي رُميت بعبارة throw. حتّى أخطاء المبرمجين، انظر: new Promise((resolve, reject) => { resolve("ok"); }).then((result) => { blabla(); // ما من دالة كهذه }).catch(alert); // ReferenceError: blabla is not defined تابِع ‎.catch الأخير لا يستلم حالات الرفض الصريحة فحسب، بل تلك التي تحدث أحيانًا في دوال المُعاملة أعلاه أيضًا. إعادة الرمي كما رأينا فوجود تابِع ‎.catch نهاية السلسلة شبيه بعبارة try..catch. يمكن أن نكتب ما نريد من دوال مُعاملة ‎.then وثمّ استعمال تابِع ‎.catch واحد في النهاية للتعامل مع أخطائها. في عبارات try..catch العاديّة نحلّل الخطأ ونُعيد رميه لو لم نستطع التعامل معه. ذات الأمر مع الوعود. فإن رمينا شيئًا داخل ‎.catch، ينتقل سير التحكّم إلى أقرب دالة تتعامل مع الأخطاء. وإن تعاملنا مع الخطأ كما يجب فيتواصل إلى أقرب دالة ‎.then. في المثال أسفله يتعامل التابِع ‎.catch مع الخطأ كما ينبغي: // ‫سلسلة التنفيذ: catch -> then new Promise((resolve, reject) => { throw new Error("Whoops!"); }).catch(function(error) { alert("The error is handled, continue normally"); }).then(() => alert("Next successful handler runs")); لا تنتهي هنا كتلة ‎.catch البرمجية بأيّ أخطاء، بذلك تُستدعى دالة المُعاملة في تابِع ‎.then التالي. نرى في المثال أسفله الحالة الثانية للتابِع ‎.catch. تلتقط دالة المُعاملة عند (*) الخطأ ولكن لا تعرف التعامل معه (مثلًا لا تعرف إلّا أخطاء URIError) فترميه ثانيةً: // ‫سلسلة التنفيذ: catch ‎→ catch → then new Promise((resolve, reject) => { throw new Error("Whoops!"); }).catch(function(error) { // (*) if (error instanceof URIError) { // handle it } else { alert("Can't handle such error"); *!* throw error; // ‫رمي هذا الخطأ أو أي خطأ آخر سينقُلنا إلى catch التالية */!* } }).then(function() { /* لن يعمل هنا */ }).catch(error => { // (**) alert(`The unknown error has occurred: ${error}`); // لن يعيد أي شيء=>عملية التنفيذ حدثت بطريقة عادية }); ينتقل سير التنفيذ من تابِع ‎.catch الأول عند (*) إلى التالي عند (**) في السلسلة. حالات رفض ماذا يحدث لو لم نتعامل مع الخطأ؟ فنقل مثلًا نسينا إضافة تابِع ‎.catch في نهاية السلسلة هكذا: new Promise(function() { noSuchFunction(); // ‫خطأ (لا يوجد هذا التابع) }) .then(() => { // معالجات الوعد الناجح سواءً واحدة أو أكثر }); // ‫بدون ‎.catch في النهاية! لو حدث خطأ يُرفض الوعد وعلى سير التنفيذ الانتقال إلى أقرب دالة تتعامل مع حالات الرفض. ولكن ما من دالة كهذه و”يعلق“ الخطأ إن صحّ التعبير إذ ما من شيفرة تتعامل معه. عمليًا يتشابه هذا مع الأخطاء التي لم نتعامل معها في الشيفرات، أي أنّ شيئًا مريعًا قد حدث. تتذكّر ما يحدث لو حدث خطأ عادي ولم تلتقطه عبارة try..catch؟ ”يموت“ النص البرمجي ويترك رسالةً في الطرفية. ذات الأمر يحدث مع حالات رفض الوعود التي لم يجري التعامل معها. يتعقّب محرّك جافاسكربت هذه الحالات ويولّد خطأً عموميًا في هذه الحالة. يمكنك أن تراه في الطرفية إن شغلت المثال أعلاه. يمكننا في المتصفّحات التقاط هذه الأخطاء باستعمال الحدث unhandledrejection: *!* window.addEventListener('unhandledrejection', function(event) { // ‫لدى كائن الحدث خاصيتين مميزتين: alert(event.promise); // ‫[object Promise] - الوعد الذي يولد الخطأ alert(event.reason); // ‫Error: Whoops!‎ - كائن الخطأ غير المعالج }); */!* new Promise(function() { throw new Error("Whoops!"); }); // ‫لا يوجد catch لمعالجة الخطأ إنّما الحدث هو جزء من معيار HTML. لو حدث خطأ ولم يكن هناك تابِع ‎.catch فيُشغّل معالج unhandledrejection ويحصل على كائن الحدث مع معلومات حول الخطأ حتى نتمكن من فعل شيء ما. مثل هذه الأخطاء عادةً ما تكون غير قابلة للاسترداد، لذلك أفضل طريقة للخروج هي إعلام المستخدم بالخطأ أو حتى إبلاغ الخادم بالخطأ. ثمّة في البيئات الأخرى غير المتصفّحات (مثل Node.js) طرائقَ أخرى لتعقّب الأخطاء التي لم نتعامل معها. خلاصة يتعامل ‎.catch مع الأخطاء في الوعود أيًا كانت: أكانت من استدعاءات reject()‎ أو من رمي الأخطاء في دوال المُعاملة. يجب أن نضع ‎.catch في الأماكن التي نريد أن نعالج الخطأ فيها ومعرفة كيفية التعامل معها. يجب على المعالج تحليل الأخطاء (الأصناف المخصصة تساعدنا بذلك) وإعادة رمي الأخطاء غير المعروفة (ربما تكون أخطاء برمجية). لا بأس بعدم استخدام ‎.catch مطلقًا، إن لم يكُ هنالك طريقة للاسترداد من الخطأ. على أية حال، يجب أن يكون لدينا معالج الأحداث unhandledrejection (للمتصفحات وللبيئات الأخرى) وذلك لتتبع الأخطاء غير المُعالجة وإعلام المستخدم (وربما إعلام الخادم) عنها. حتى لا يموت تطبيقنا مطلقًا. تمارين خطأ في تابِع setTimeout ما رأيك هل ستعمل .catch؟ وضح إجابتك. new Promise(function(resolve, reject) { setTimeout(() => { throw new Error("Whoops!"); }, 1000); }).catch(alert); الجواب: لا لن تعمل: new Promise(function(resolve, reject) { setTimeout(() => { throw new Error("Whoops!"); }, 1000); }).catch(alert); كما ذكرنا في هذا الفصل لدينا "صياغة try..catch ضمنية" حول تابع معيّن. لذلك تُعالج جميع الأخطاء المتزامنة. ولكن هنا الخطأ لا يُنشأ عندما يعمل المُنفذّ وإنما في وقت لاحق. لذا فإن الوعد لن يستطيع معالجته. ترجمة -وبتصرف- للفصل Error handling with promises من كتاب The JavaScript language
  8. طرحناها في الفصل "مقدمة إلى ردود النداء callbacks" مشكلةً ألا وهي أنّ لدينا تسلسلًا من المهام غير المتزامنة ويجب أن تُجرى واحدةً بعد الأخرى، مثلًا تحميل السكربتات. كيف نكتب شيفرة … لهذه المشكلة؟ تقدّم لنا الوعود طرائق مختلفة لهذا الغرض. وفي هذا الفصل سنتكلّم عن سَلسلة الوعود فقط. هكذا تكون: new Promise(function(resolve, reject) { setTimeout(() => resolve(1), 1000); // (*) }).then(function(result) { // (**) alert(result); // 1 return result * 2; }).then(function(result) { // (***) alert(result); // 2 return result * 2; }).then(function(result) { alert(result); // 4 return result * 2; }); الفكرة وما فيها هي تمرير الناتج في سلسلة توابِع ‎.then تابعًا تابعًا. هكذا تكون: يبدأ الوعد الأوّل ويُنجز خلال ثانية واحدة (*). بعدها يُستدعى معالج ‎.then (**). النتيجة التي ستعود ستمرر إلى معالج ‎.then التالي (***). وهكذا… . نظرًا لتمرير النتيجة على طول سلسلة المعالجات، يمكننا رؤية سلسلة من استدعاءات alert هكذا: 1 ← 2 ← 4. ويعود سبب هذا كلّه إلى أنّ استدعاء promise.then يُعيد وعدًا هو الآخر، بذلك يمكننا استدعاء التابِع ‎.then التالي على ذلك الوعد، وهكذا. حين تُعيد دالة المُعاملة قيمةً ما، تصير القيمة ناتج ذلك الوعد، بذلك يمكن استدعاء ‎.then عليه. خطأ شائع بين المبتدئين: تقنيًا يمكننا إضافة أكثر من تابِع ‎.then إلى وعد واحد. لا يُعدّ هذا سَلسلة وعود. مثلًا: let promise = new Promise(function(resolve, reject) { setTimeout(() => resolve(1), 1000); }); promise.then(function(result) { alert(result); // 1 return result * 2; }); promise.then(function(result) { alert(result); // 1 return result * 2; }); promise.then(function(result) { alert(result); // 1 return result * 2; }); هنا كتبنا أكثر من دالة مُعاملة لوعد واحد، وهذه التوابِع لا تمرّر القيمة إلى بعضها البعض، بل كلّ تعالجها على حدة. إليك الصورة (ووازِن بينها وبين السلسلة أعلاه): تتلقّى كلّ توابِع ‎.then في نفس الوعد ذات الناتج (أي ناتج الوعد) بذلك تعرض الشيفرة أعلاه نتائج alert متطابقة: 1. أمّا عمليًا فنادرًا ما نستعمل أكثر من دالة مُعاملة واحدة لكلّ وعد، على عكس السَلسلة التي يشيع استعمالها. إعادة الوعود يمكن لدالة المُعاملة (المستعملة في ‎.then(handler)‎) إنشاء وعد وإعادته. هنا تنتظر دوال المُعاملة الأخرى حتّى يكتمل الوعد وتستلم ناتجه. مثال على هذا: new Promise(function(resolve, reject) { setTimeout(() => resolve(1), 1000); }).then(function(result) { alert(result); // 1 // لاحِظ return new Promise((resolve, reject) => { // (*) setTimeout(() => resolve(result * 2), 1000); }); }).then(function(result) { // (**) alert(result); // 2 return new Promise((resolve, reject) => { setTimeout(() => resolve(result * 2), 1000); }); }).then(function(result) { alert(result); // 4 }); هنا يعرض أوّل تابِع .then القيمة 1 ويُعيد new Promise(…)‎ في السطر (*). بعد ثانية واحدة، … الوعد ويُمرّر ناتجه (أي وسيط التابِع resolve، في حالتنا هو result * 2) إلى دالة المُعاملة التالية في تابِع .then التالي. نرى كيف أنّ الدالة في السطر (**) تعرض 2 وتؤدّي ما أدّته دالة المُعاملة السابقة. بذلك نحصل على ما حصلنا عليه في المثال السابق: 1 ثمّ 2 ثمّ 4، الفرق هو التأخير لمدّة ثانية بين كلّ استدعاء من استدعاءات alert. بإعادة الوعود يمكننا بناء سلسلة من الإجراءات غير المتزامنة. مثال: loadScript لنستعمل هذه الميزة مع دالة loadScript (التي كتبناها في الفصل السابق) لنُحمّل النصوص البرمجية واحدًا تلو الآخر: loadScript("/article/promise-chaining/one.js") .then(function(script) { return loadScript("/article/promise-chaining/two.js"); }) .then(function(script) { return loadScript("/article/promise-chaining/three.js"); }) .then(function(script) { // نستعمل الدوال المعرّف عنها في النصوص البرمجية // ونتأكّد تمامًا بأنّها حُمّلت one(); two(); three(); }); يمكننا أيضًا تقصير الشيفرة قليلًا باستعمال الدوال السهميّة: loadScript("/article/promise-chaining/one.js") .then(script => loadScript("/article/promise-chaining/two.js")) .then(script => loadScript("/article/promise-chaining/three.js")) .then(script => { // اكتمل تحميل النصوص، يمكننا استعمال الدوال فيها الآن one(); two(); three(); }); نرى هنا أنّ كلّ استدعاء من استدعاءات loadScript يُعيد وعدًا، ويعمل تابِع ‎.then التالي في السلسلة متى … الوعد. بعدها تبدأ الدالة بتحميل النص البرمجي التالي، وهكذا تُحمّل كلّ النصوص واحدًا بعد آخر. ويمكننا إضافة ما نريد من إجراءات غير متزامنة إلى السلسلة، ولن يزيد طول الشيفرة إلى اليمين، بل إلى أسفل، ولن نُقابل وجه … ثانيةً. يمكننا تقنيًا إضافة تابِع ‎.then داخل دوال loadScript مباشرةً هكذا: loadScript("/article/promise-chaining/one.js").then(script1 => { loadScript("/article/promise-chaining/two.js").then(script2 => { loadScript("/article/promise-chaining/three.js").then(script3 => { // ‫يمكن أن تصل هذه الدالة إلى المتغيّرات script1 وscript2 وscript3 one(); two(); three(); }); }); }); وتؤدّي الشيفرة نفس العمل: تُحمّل 3 نصوص برمجية بالترتيب. المشكلة هي أنّ طولها يزيد نحو اليمين وهي نفس مشكلة ردود النداء. عادةً ما يجهل المبرمجون الجدد الذين يستعملون الوعود ميزة السَلسلة، فيكتبون الشيفرات هكذا. لكنّ سَلسلة الوعود هي الأمثل وغالبًا الأفضل. ولكنّ استعمال .then مباشرةً أحيانًا لا يكون بالمشكلة الكبيرة، إذ يمكن للدوال المتداخلة الوصول إلى … الخارجي. في المثال أعلاه مثلًا يمكن لآخر ردّ نداء متداخل الوصول إلى كلّ المتغيّرات script1 وscript2 وscript3، إلّا أنّ هذا استثناء عن القاعدة وليس قاعدة بحدّ ذاتها. ملاحظة: كائنات Thenables على وجه الدقة، لا تعيد المعالجات وعودًا وإنما تعيد كائن thenable - وهو كائن عشوائي له نفس توابع .then. ويتعامل معه بنفس طريقة التعامل مع الوعد. الفكرة أن مكتبات الخارجية تنفذ كائنات خاصة بها "متوافقة مع الوعد". ويمكن أن يملكوا مجموعة توابع موسّعة. ولكن يجب أن يتوافقوا مع الوعود الأصيلة، لأنهم ينفذون .then. وإليك مثالًا على كائن thenable: class Thenable { constructor(num) { this.num = num; } then(resolve, reject) { alert(resolve); // function() { native code } // ‫إنجاز الوعد وتحقيقه مع this.num*2 بعد ثانية setTimeout(() => resolve(this.num * 2), 1000); // (**) } } new Promise(resolve => resolve(1)) .then(result => { return new Thenable(result); // (*) }) .then(alert); // shows 2 after 1000ms تتحقق جافاسكربت من الكائن المُعاد من معالج .then في السطر (*): إن لديه تابع قابل للاستدعاء يدعى then عندها سيستدعي ذاك التابع مزودًا بذلك بالتوابع الأصيلة مثل: resolve و reject كوسطاء (مشابه للمنفذ) وينتظر حتى يستدعى واحدًا منهم. في المثال أعلاه تستدعى resolve(2) بعد ثانية انظر (**). بعدها تمرر النتيجة إلى أسفل السلسلة. تتيح لنا هذه المميزات دمج الكائنات المخصصة مع سلاسل الوعود دون الحاجة إلى الوراثة من الوعد Promise. مثال أضخم: fetch عادةً ما نستعمل الوعود في برمجة الواجهات الرسومية لطلبات الشبكة. لنرى الآن مثالًا أوسع مجالًا قليلًا. سنستعمل التابِع fetch لتحميل بعض المعلومات التي تخصّ المستخدم من الخادوم البعيد. لهذا التابِع معاملات كثيرة اختيارية كتبنا عنا في فصول مختلفة، إلّا أنّ صياغته الأساسية بسيطة إلى حدّ ما: let promise = fetch(url); هكذا نُرسل طلبًا شبكيًا إلى العنوان url ونستلم وعدًا. …… ما إن يردّ الخادم البعيد بترويسات الطلب، ولكن قبل تنزيل الردّ كاملًا. علينا استدعاء التابِع response.text()‎ لقراءة الردّ كاملًا، وهو يُعيد وعدًا … متى نُزّل النص الكامل من الخادوم البعيد، وناتجه يكون ذلك النص. تُرسل الشيفرة أسفله طلبًا إلى user.json وتحمّل نصّه من الخادوم: fetch('/article/promise-chaining/user.json') // ‫إن ‎.then تعمل عندما يستجيب الخادم البعيد .then(function(response) { // ‫إن التابع response.text()‎ يُعيد وعدًا جديدًا والذي يعاد مع كامل نص الاستجابة // عندما يُحمّل return response.text(); }) .then(function(text) { // ‫...وهنا سيكون محتوى الملف البعيد alert(text); // {"name": "iliakan", isAdmin: true} }); كما أنّ هناك التابِع response.json()‎ والذي يقرأ البيانات المستلمة البعيدة ويحلّلها على أنّها JSON. في حالتنا هذا أفضل وأسهل فهيًا نستعمله. كما وسنستعمل الدوال السهميّة للاختصار قليلًا: // ‫مشابه للمثال أعلاه ولكن التابع response.json()‎ يحلل المحتوى البعيد كملف JSON fetch('/article/promise-chaining/user.json') .then(response => response.json()) .then(user => alert(user.name)); // iliakan, got user name الآن لنصنع شيئًا بهذا المستخدم الذي حمّلناه. يمكننا مثلًا إجراء طلبات أكثر من غِت‎هَب وتحميل ملف المستخدم الشخصي وعرض صورته: // ‫أنشئ طلب لـِuser.json fetch('/article/promise-chaining/user.json') // ‫حمله وكأنه ملف json .then(response => response.json()) // ‫أنشئ طلب لـِ GitHub .then(user => fetch(`https://api.github.com/users/${user.name}`)) // ‫حمّل الرد كملف json .then(response => response.json()) // ‫أظهر الصورة الرمزية (avatar) من (githubUser.avatar_url) لمدة 3 ثواني .then(githubUser => { let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.append(img); setTimeout(() => img.remove(), 3000); // (*) }); الشيفرة تعمل على أكمل وجه (طالِع التعليقات لتعرف التفاصيل). ولكن هناك مشكلة فيه قد تحدث، وهي خطأ شائع يقع فيه من يستعمل الوعود أوّل مرّة. طالِع السطر (*): كيف يمكن أن نفعل مهمّة معينة متى اكتمل عرض الصورة وأُزيلت؟ فلنقل مثلًا سنعرض استمارة لتحرير ذلك المستخدم أو أيّ شيء آخر. حاليًا… ذلك مستحيل. لنقدر على مواصلة السلسلة علينا إعادة وعد المُنجز متى اكتمل عرض الصورة. هكذا: fetch('/article/promise-chaining/user.json') .then(response => response.json()) .then(user => fetch(`https://api.github.com/users/${user.name}`)) .then(response => response.json()) // هنا .then(githubUser => new Promise(function(resolve, reject) { // (*) let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.append(img); setTimeout(() => { img.remove(); resolve(githubUser); // (**) }, 3000); })) // يحدث بعد 3 ثوانٍ .then(githubUser => alert(`Finished showing ${githubUser.name}`)); هكذا صارت تُعيد دالة المُعاملة في .then عند السطر (*) كائنَ new Promise لا … إلّا بعد استدعاء resolve(githubUser)‎ في setTimeout عند (**). وسينتظر تابِع ‎.then التالي في السلسلة اكتمال ذلك. تُعد إعادة الإجراءات غير المتزامنة للوعود دومًا إحدى الممارسات الصحيحة في البرمجة. هكذا يسهّل علينا التخطيط للإجراءات التي ستصير بعد هذا، فحتّى لو لم نريد توسعة السلسلة الآن لربّما احتجنا إلى ذلك لاحقًا. وأخيرًا، يمكننا أيضًا تقسيم الشيفرة إلى دوال يمكن إعادة استعمالها: function loadJson(url) { return fetch(url) .then(response => response.json()); } function loadGithubUser(name) { return fetch(`https://api.github.com/users/${name}`) .then(response => response.json()); } function showAvatar(githubUser) { return new Promise(function(resolve, reject) { let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.append(img); setTimeout(() => { img.remove(); resolve(githubUser); }, 3000); }); } // نستعملها الآن: loadJson('/article/promise-chaining/user.json') .then(user => loadGithubUser(user.name)) .then(showAvatar) .then(githubUser => alert(`Finished showing ${githubUser.name}`)); // اكتمل عرض كذا // ... خلاصة إن أعادت دالة مُعاملة ‎.then (أو catch/finally، لا يهمّ حقًا) وعدًا، فتنتظر بقية السلسلة حتّى تُنجز متى حدث ذلك يُمرّر الناتج (أو الخطأ) إلى التي بعدها. إليك الصورة الكاملة: تمارين الوعد: then وcatch هل تؤدّي هاتين الشيفرتين نفس الغرض؟ أي هل يتطابق سلوكهما في الحالات المختلفة، وأيّما كانت دوال المُعاملة؟ promise.then(f1).catch(f2); مقابل: promise.then(f1, f2); الحل الجواب المختصر: لا ليسا متساويين: الفرق أنه إن حدث خطأ في f1 فستعالجها ‎.catch هنا: promise .then(f1) .catch(f2); …لكن ليس هنا: promise .then(f1, f2); وذلك بسبب تمرير الخطأ لأسفل السلسلة، وفي الجزء الثاني من الشيفرة لا يوجد سلسلة أقل من f1. بمعنى آخر يمرر ‎.then النتيجة أو الخطأ إلى ‎.then/catch التالية. لذا في المثال الأول يوجد catch بينما في المثال الثاني لا يوجد. ولذلك لم يعالج الخطأ. ترجمة -وبتصرف- للفصل Promises chaining من كتاب The JavaScript language
  9. لنقل بأنّك أنت هو عبد الحليم حافظ، ولنفترض بأنّ مُعجبوك من المحيط إلى الخليج يسألونك ليلًا نهارًا عن الأغنية الشاعرية التالية. وكي تُريح بالك تعدهم بإرسالها إليهم ما إن تُنشر. فتُعطي مُعجبيك قائمة يملؤون فيها عناوين بريدهم. ومتى ما نشرت الأغنية يستلمها كلّ من في تلك القائمة. ولو حصل مكروه (لا سمح الله) مثل أن شبّت النار والتهمت الأستديو ولم تقدر على نشر الأغنية - لو حصل ذلك فسيعلمون به أيضًا. وعاش الجميع بسعادة وهناء: أنت إذ لا يُزعجك الجميع بالتهديدات والتوعّدات، ومُعجبيك إذ لن تفوتهم أيّة رائعة من روائعك الفنية. إليك ما يشبه الأمور التي نفعلها في الحياة الواقعية - في الحياة البرمجية: ”شيفرة مُنتِجة“ تُنفّذ شيئًا وتأخذ الوقت. مثل الشيفرات التي تُحمّل البيانات عبر الشبكة. هذا أنت، ”المغنّي“. ”شيفرة مُستهلِكة“ تطلب ناتج ”الشيفرة المُنتِجة“ ما إن يجهز. وهناك عديد من الدوال تحتاج إلى هذا الناتج. هذه ”مُعجبوك“. الوعد (Promise) هو كائن فريد في جافاسكربت يربط بين ”الشيفرة المُنتِجة“ و”الشيفرة المُستهلِكة“. في الحياة العملية، الوعد هو ”قائمة الاشتراك“. يمكن أن تأخذ ”الشيفرة المُنتِجة“ ما تلزم من وقت لتقدّم لنا النتيجة التي وعدتنا بها، وسيُجهّزها لنا ”الوعد“ لأيّة شيفرة طلبتها متى جهزت. إن هذه المقاربة ليست دقيقة جدًا على الرغم من أنها جيدة كبداية ولكن وعود جافاسكربت أكثر تعقيدًا من قائمة اشتراك بسيطة بل لديها ميزات وقيود إضافية. هذه صياغة الباني لكائنات الوعد: let promise = new Promise(function(resolve, reject) { // ‫المُنفِّذ (الشيفرة المُنتجة، مثل ”المغنّي“) }); تُدعى الدالة الممرّرة إلى new Promise ”بالمُنفِّذ“. متى صُنع الوعد new Promise عملت الدالة تلقائيًا. يحتوي هذا المُنفِّذ الشيفرة المُنتجِة، ويمكن أن تقدّم لنا في النهاية ناتجًا. في مثالنا أعلاه، فالمُنفِّذ هذا هو ”المغنّي“. تقدّم جافاسكربت الوسيطين resolve و reject وهما ردود نداء. كما ولا نضع الشيفرة التي نريد تنفيذها إلا داخل المُنفِّذ. لا يهمّنا متى سيعرف المُنفِّذ الناتجَ (آجلًا كان ذلك أم عاجلًا)، بل أنّ عليه نداء واحدًا من ردود النداء هذه: resolve(value)‎: لو اكتملت المهمّة بنجاح. القيمة تسجّل في value. reject(error)‎: لو حدث خطأ. error هو كائن الخطأ. إذًا نُلخّص: يعمل المُنفِّذ تلقائيًا وعليه مهمّة استدعاء resolve أو reject. لكائن الوعد promise الذي أعاده الباني new Promise خاصيتين داخليتين: الحالة state: تبدأ بالقيمة "pending" وبعدها تنتقل إلى "fulfilled" متى استُدعت resolve، أو إلى "rejected" متى استُدعت reject. الناتج result: يبدأ أولًا غير معرّف undefined، وبعدها يتغيّر إلى value متى استُدعت resolve(value)‎ أو يتغيّر إلى error متى استُدعت reject(error)‎. وفي النهاية ينقل المُنفِّذ الوعدَ promise ليصير بإحدى الحالات الآتية: سنرى لاحقًا كيف سيشترك ”مُعجبونا“ بهذه التغييرات. إليك مثالًا عن بانيًا للوعود ودالة مُنفِّذ بسيطة فيها ”شيفرة مُنتجِة“ تأخذ بعض الوقت (باستعمال setTimeout): let promise = new Promise(function(resolve, reject) { // تُنفّ الدالة مباشرةً ما إن يُصنع الوعد // ‫وبعد ثانية واحدة نبعث بإشارة بأنّ المهمة انتهت والنتيجة هي ”تمت“ (done) setTimeout(() => resolve("done"), 1000); }); بتشغيل الشيفرة أعلاه، نرى أمرين اثنين: يُستدعى المُنفِّذ تلقائيًا ومباشرةً (عند استعمال new Promise). يستلم المُنفِّذ وسيطين: دالة الحلّ resolve ودالة الرفض reject، وهي دوال معرّفة مسبقًا في محرّك جافاسكربت، ولا داعٍ بأن نصنعها نحن، بل استدعاء واحدة ما إن تجهز النتيجة. بعد سنة من عملية ”المعالجة“ يستدعي المُنفِّذ الدالةَ resolve("done")‎ لتُنتج الناتج. هكذا تتغيّر حالة كائن promise: كان هذا مثالًا عن مهمّة اكتملت بنجاح، أو ”وعد تحقّق“. والآن سنرى مثالًا عن مُنفِّذ يرفض الوعد مُعيدًا خطأً: let promise = new Promise(function(resolve, reject) { // بعد ثانية واحدة نبعث بإشارة بأنّ المهمة انتهت ونُعيد خطأً setTimeout(() => reject(new Error("Whoops!")), 1000); }); باستدعاء reject(...)‎ ننقل حالة كائن الوعد إلى حالة الرفض "rejected": ملخّص القول هو أنّ على المُنفِّذ تنفيذ المهمة (أي ما يأخذ بعض الوقت ليكتمل) وثمّ يستدعي واحدةً من الدالتين resolve أو reject لتغيير حالة كائن الوعد المرتبط بالمُنفِّذ. يُسمّى الوعد الذي تحقّق أو نُكث الوعد المنُجز، على العكس من الوعد المعلّق. ملاحظة: إما أن تظهر نتيجة واحدة أو خطأ، يجب على المنفّذ أن يستدعي إما resolve أو reject. أي تغيير في الحالة يعدّ تغييرًا نهائيًا. وسيُتجاهل جميع الاستدعاءات اللاحقة سواءً أكانت resolve أو reject: let promise = new Promise(function(resolve, reject) { resolve("done"); reject(new Error("…")); // ستتجاهل setTimeout(() => resolve("…")); // ستتجاهل }); الفكرة هنا أن خرج عمل المنفذّ سيعرض إما نتيجة معينة أو خطأ. وتتوقع التعليمتين resolve/reject وسيطًا واحدًا مُررًا (أو بدون وسطاء نهائيًا) وأي وسطاء إضافية ستُتجاهل. ملاحظة: الرفض مع كائن Error في حال حدوث خطأ ما، يجب على المنفذّ أن يستدعي تعليمة reject. ويمكن تمرير أي نوع من الوسطاء (تمامًا مثل: resolve). ولكن يوصى باستخدام كائنات Error (أو أي كائنات ترث من Error). وقريبًا سنعرف بوضوح سبب ذلك. ملاحظة: استدعاء resolve/reject الفوري عمليًا عادة ينجز المنفذّ عمله بشكل متزامن ويستدعي resolve/reject بعد مرور بعض الوقت، ولكن الأمر ليس إلزاميًا، يمكننا استدعاء resolve أو reject فورًا، هكذا: let promise = new Promise(function(resolve, reject) { // يمكننا القيام بالمهمة مباشرة resolve(123); // أظهر مباشرة النتيجة: 123 }); على سبيل المثال من الممكن أن يحدث ذلك في حال البدء بمهمة معينة ولكن تكتشف بأن كلّ شيء أنجز وخزّن في الذاكرة المؤقتة. هذا جيد، فعندها يجب أن ننجز الوعد فورًا. ملاحظة: الحالة state و النتيجة result الداخليتين تكون خصائص الحالة state و النتيجة result لكائن الوعد داخلية. ولا يمكننا الوصول إليهم مباشرة. يمكننا استخدام التوابِع ‎.then/.catch/.finally لذلك والتي سنشرحُها أدناه. الاستهلاك: عبارات then وcatch وfinally كائن الوعد هو كالوصلة بين المُنفِّذ (أي ”الشيفرة المُنتِجة“ أو ”المغنّي“) والدوال المُستهلكة (أي ”المُعجبون“) التي ستسلم الناتج أو الخطأ. يمكن تسجيل دوال الاستهلاك (أو أن تشترك، كما في المثال العملي ذاك) باستعمال التوابِع ‎.then و‎.catch و‎.finally. then يُعدّ ‎.then أهمّها وعِماد القصة كلها. صياغته هي: promise.then( function(result) { /* نتعامل مع الناتج الصحيح */ }, function(error) { /* نتعامل مع الخطأ */ } ); الوسيط الأوّل من التابِع ‎.then يُعدّ دالة تُشغّل إن تحقّق الوعد، ويكون الوسيطُ الناتج. بينما الوسيط الثاني يُعدّ دالةً تُشغّل إن رُفض الوعد، ويكون الوسيطُ الخطأ. إليك مثال نتعامل فيه مع وعد تحقّق بنجاح: let promise = new Promise(function(resolve, reject) { setTimeout(() => resolve("done!"), 1000); }); // ‫تُنفِّذ resolve أول دالة في ‎.then promise.then( result => alert(result), // ‫ إظهار "done!‎" بعد ثانية error => alert(error) // هذا لا يعمل ); هكذا نرى الدالة الأولى هي التي نُفّذت. وإليك المثال في حالة الرفض: let promise = new Promise(function(resolve, reject) { setTimeout(() => reject(new Error("Whoops!")), 1000); }); // ‫تُنفِّذ reject ثاني دالة في ‎.then promise.then( result => alert(result), // لا تعمل error => alert(error) // ‫ إظهار "Error: Whoops!‎" بعد ثانية ); لو لم نُرِد إلّا حالات الانتهاء الناجحة، فيمكن أن نقدّم دالةً واحدة وسيطًا إلى ‎.then فقط: let promise = new Promise(resolve => { setTimeout(() => resolve("done!"), 1000); }); promise.then(alert); // ‫ إظهار "done!‎" بعد ثانية catch لو لم نكن نهتمّ إلّا بالأخطاء، فعلينا استعمال null وسيطًا أولًا: ‎.then(null, errorHandlingFunction)‎، أو نستعمل ‎.catch(errorHandlingFunction)‎ وهو يؤدّي ذات المبدأ تمامًا ولا فرق إلّا قصر الثانية مقارنة بالأولى: let promise = new Promise((resolve, reject) => { setTimeout(() => reject(new Error("Whoops!")), 1000); }); // .catch(f) is the same as promise.then(null, f) promise.catch(alert); // ‫إظهار "Error: Whoops!‎" بعد ثانية finally كما المُنغلِقة finally في عبارات try {...} catch {...}‎ العادية، فهناك مثلها في الوعود. استدعاء ‎.finally(f)‎ يشبه استدعاء ‎.then(f, f)‎، ووجه الشبه هو أنّ الدالة f تعمل دومًا متى …. الوعد، كان قد تحقّق أو نُكث. استعمال finally مفيد جدًا لتنظيف ما تبقّى من أمور مهمًا كان ناتج الوعد، مثل إيقاف أيقونات التحميل (فلم نعد نحتاجها). هكذا مثلًا: new Promise((resolve, reject) => { // ‫افعل شيئًا يستغرق وقتًا ثم استدع resolve/reject lre }) // runs when the promise is settled, doesn't matter successfully or not .finally(() => stop loading indicator) .then(result => show result, err => show error) ولكنها ليست متطابقة تمامًا مع then(f,f)‎، فهناك فروقات مهمّة: ليس لدالة المُعالجة finally أيّ وسطاء. أي لسنا نعلم في finally أكان الوعد تحقّق أو نُكث، وهذه ليست مشكلة إذ ما نريده عادةً هو تنفيذ بعض الأمور ”العامّة“ لنُنهي ما بدأنا به. يمرُّ مُعالج finally على النتائج والأخطاء وبعدها إلى المعالج التالي. مثال على ذلك هو الناتج الذي تمرّر من finally إلى then هنا: new Promise((resolve, reject) => { setTimeout(() => resolve("result"), 2000) }) .finally(() => alert("Promise ready")) .then(result => alert(result)); // <-- ‫‎.then ستعالج الناتج وهنا واجه الوعد خطأً، وتمرّر من finally إلى catch: new Promise((resolve, reject) => { throw new Error("error"); }) .finally(() => alert("Promise ready")) .catch(err => alert(err)); // <-- ‫‎.catch ستعالج كائن الخطأ error object هذا السلوك مفيد جدًا إذ لا يفترض بأن تتعامل finally مع ناتج الوعد، بل تمرّره إلى من يتعامل معه. سنتحدّث أكثر عن سَلسلة الوعود وتمرير النواتج بين … في الفصل اللاحق. أخيرًا وليس آخرًا، صياغة ‎.finally(f)‎ أسهل وأريح بكثير من صياغة ‎.then(f, f)‎ فلا داعٍ لتكرار الدالة f. ملاحظة: في الوعود المنجزة المُعالجات تعمل مباشرة إن كان الوعد مُعلقًا لسببٍ ما، فإن معالجات ‎.then/catch/finally ستنتظره. عدا ذلك (إن كان الوعد مُنجزًا) فإن المعالجات ستنفذّ مباشرةً: // يصبح الوعد منجزًا ومتحققًا بعد الإنشاء مباشرةً let promise = new Promise(resolve => resolve("done!")); promise.then(alert); // done! (تظهر الآن) الآن لنرى أمثلة عملية على فائدة الوعود في كتابة الشيفرات غير المتزامنة. تحميل السكربتات: الدالة loadScript أمامنا من الفصل الماضي الدالة loadScript لتحميل السكربتات. إليك الدالة بطريقة ردود النداء، لنتذكّرها لا أكثر ولا أقل: function loadScript(src, callback) { let script = document.createElement('script'); script.src = src; script.onload = () => callback(null, script); // خطأ في تحميل السكربت كذا script.onerror = () => callback(new Error(`Script load error for ${src}`)); document.head.append(script); } هيًا نُعد كتابتها باستعمال الوعود. لن تطلب دالة loadScript الجديدة أيّ ردود نداء، بل ستصنع كائن وعد يتحقّق متى اكتمل التحميل، وتُعيده. يمكن للشيفرة الخارجية إضافة الدوال المُعالجة (أي دوال الاشتراك) إليها باستعمال .then: function loadScript(src) { return new Promise(function(resolve, reject) { let script = document.createElement('script'); script.src = src; script.onload = () => resolve(script); script.onerror = () => reject(new Error(`Script load error for ${src}`)); document.head.append(script); }); } الاستعمال: let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"); promise.then( script => alert(`${script.src} is loaded!`), error => alert(`Error: ${error.message}`) ); promise.then(script => alert('Another handler...')); بنظرة خاطفة يمكن أن نرى فوائد هذه الطريقة موازنةً بطريقة ردود النداء: الوعود ردود النداء تتيح لنا الوعود تنفيذ الأمور بترتيبها الطبيعي أولًا نشغّل loadScript(script)‎ ومن بعدها ‎.then نكتب ما نريد فعله بالنتيجة. يجب أن يكون تابِع callback تحت تصرفنا عند استدعاء loadScript(script, callback)‎. بعبارة أخرى يجب أن نعرف ما سنفعله بالنتيجة قبل استدعاء loadScript. يمكننا استدعاء ‎.then في الوعد عدة مرات كما نريد. في كلّ مرة نضيف معجب جديدة "fan"، هنالك تابع سيضيف مشتركين جُدد إلى قائمة المشتركين. سنرى المزيد حول هذا الأمر في الفصل القادم يمكن أن يكون هنالك ردّ واحد فقط. table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } إذًا، فالوعود تقدّم لنا تحكمًا مرنًا بالشيفرة وسير تنفيذها، وما زالت هنالك الكثير من الأمور الرائعة التي سنتعرف عليها الفصل القادم. تمارين إعادة … الوعد؟ ما ناتج الشيفرة أدناه؟ let promise = new Promise(function(resolve, reject) { resolve(1); setTimeout(() => resolve(2), 1000); }); promise.then(alert); الحل الناتج هو: 1. يُهمل استدعاء resolve الثاني إذ لا يتهمّ المحرّك إلّا بأول استدعاء من reject/resolve، والباقي كلّه يُهمل. التأخير باستعمال الوعود تستعمل الدالة المضمّنة في اللغة setTimeout ردودَ النداء. اصنع واحدة تستعمل الوعود. على الدالة delay(ms)‎ إعادة وعد ويجب أن … هذا الوعد خلال ms مليثانية، ونُضيف تابِع .then إليه هكذا: function delay(ms) { // شيفرتك هنا } delay(3000).then(() => alert('runs after 3 seconds')); الحل function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } delay(3000).then(() => alert('runs after 3 seconds')); لاحظ أنّنا في هذا التمرين استدعينا resolve بلا وسطاء، ولم نُعد أيّ قيمة من delay بل … فقط صورة دائرة متحركة مع وعد أعِد كتابة الدالة showCircle في حلّ التمرين السابق لتُعيد وعدًا بدل أن تستلم ردّ نداء. ويكون استعمالها الجديد هكذا: showCircle(150, 150, 100).then(div => { div.classList.add('message-ball'); div.append("Hello, world!"); }); ليكن الحلّ في التمرين المذكور أساس المسألة الآن. الحل يمكنك مشاهدة الحل عبر المثال الحي. ترجمة -وبتصرف- للفصل Promise من كتاب The JavaScript language
  10. ملاحظة ابتدائية: لتوضيح طريقة استخدام ردود النداء callbacks والوعود promises والمفاهيم المجردة سنستخدم بعض توابِع المتصفح، تحديدًا سكربتات التحميل loading scripts وأدوات التلاعب بالمستندات البسيطة. إن لم تكُ على دراية بهذه الطرق، وكان استخدامها في الأمثلة مربكًا نوعًا ما، أو حتى إن رغبتَ فقط في فهمها فهمًا أفضل، فيمكنك قراءة بعض الفصول من الجزء التالي من الدورة التعليمية. وبالرغم من ذلك سنحاول توضيح الأمور بكل الأحوال ولن يكون هنالك شيء معقد في المتصفح. أغلب الإجراءات في جافاسكربت هي إجراءات غير متزامنة، أي أنّنا نشغّلها الآن ولكنّها تنتهي في وقت لاحق. مثال على ذلك هو حين نُجدول تلك الإجراءات باستعمال setTimeout. يُعدّ تحميل السكربتات والوحدات (سنشرحها في الفصول القادمة) أمثلة فعلية عن الإجراءات غير المتزامنة. لاحظ مثلًا الدالة loadScript(src)‎ أسفله، إذ تُحمّل سكربتًا من العنوان src الممرّر: function loadScript(src) { // ‫أنشئ وسم <script> وأضفه للصفحة // ‫فسيؤدي ذلك إلى بدء تحميل السكربت ذي الخاصية src ثم تنفيذه عند الاكتمال let script = document.createElement('script'); script.src = src; document.head.append(script); } تُضيف الدالة إلى المستند الوسم <script src=‎"…"‎> الجديد الذي وُلّد ديناميكيًا. حين يعمل المتصفّح ينفّذ الدالة. يمكننا استعمال هذه الدالة هكذا: // حمّل ونفّذ السكربت في هذا المكان loadScript('/my/script.js'); يُنفّذ هذا السكربت ”بلا تزامن“ إذ تحميله يبدأ الآن أمّا تشغيله فيكون لاحقًا متى انتهى الدالة. ولو كانت هناك شيفرة أسفل الدالة loadScript(…)‎ فلن ننتظر انتهاء تحميل السكربت. loadScript('/my/script.js'); // الشيفرة أسفل ‫loadScript // لا تنتظر انتهاء تحميل السكربت // ... فنقل بأنّنا نريد استعمال السكربت متى انتهى تحميله (فهو مثلًا يُصرّح عن دوال جديدة ونريد تشغيلها). ولكن لو حدث ذلك مباشرةً بعد استدعاء loadScript(…)‎، فلن تعمل الشيفرة: // في السكربت الدالة ‫"function newFunction() {…}‎" loadScript('/my/script.js'); newFunction(); // ما من دالة بهذا الاسم! الطبيعي هو أنّ ليس على المتصفّح انتظار مدّة من الزمن حتّى ينتهي تحميل السكربت. كما نرى فحاليًا لا تقدّم لنا الدالة loadScript أيّ طريقة لنعرف وقت اكتمال التحميل، بل يُحمّل السكربت ويُشغّل متى اكتمل، وفقط. ولكن ما نريده هو معرفة وقت ذلك كي نستعمل الدوال والمتغيرات الجديدة فيه. لنُضيف دالة ردّ نداء callback لتكون الوسيط الثاني لدالة loadScript، كي تُنفّذ متى انتهى تحميل السكربت: function loadScript(src, callback) { let script = document.createElement('script'); script.src = src; script.onload = () => callback(script); document.head.append(script); } الآن متى أردنا استدعاء الدوال الجديدة في السكربت، نكتبها في دالة ردّ النداء تلك: loadScript('/my/script.js', function() { // يعمل ردّ النداء بعد تحميل السكربت newFunction(); // الآن تعمل ... }); هذه الفكرة تمامًا: يُعدّ الوسيط الثاني دالةً (عادةً ما تكون مجهولة) تعمل متى اكتمل الإجراء. وإليك مثالًا حقيقيًا قابل للتشغيل: function loadScript(src, callback) { let script = document.createElement('script'); script.src = src; script.onload = () => callback(script); document.head.append(script); } *!* loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => { alert(`Cool, the script ${script.src} is loaded`); alert( _ ); // التابع معرف في السكربت المُحمّل }); */!* يُسمّى هذا الأسلوب في البرمجة غير المتزامنة ”الأسلوب المبني على ردود النداء“ (callback-based)، إذ على الدوال التي تنفّذ أمور غير متزامنة تقديمَ وسيط ردّ نداء callback نضع فيه الدالة التي ستُشغّل متى اكتملت تلك الأمور. ولقد فعلناها في loadScript ولكن بكلّ الأحوال هذا نهج عام. رد النداء داخل رد النداء وكيف نُحمّل سكربتين واحدًا تلو الآخر: يعمل الأول وبعدها حين ينتهي يعمل الثاني؟ الحلّ الطبيعي الذي سيفكّر به الجميع هو وضع استدعاء loadScript الثاني داخل ردّ النداء، هكذا: loadScript('/my/script.js', function(script) { // جميل، اكتمل تحميل كذا، هيًا نُحمّل الآخر alert(`Cool, the ${script.src} is loaded, let's load one more`); loadScript('/my/script2.js', function(script) { // جميل، اكتمل تحميل السكربت الثاني أيضًا alert(`Cool, the second script is loaded`); }); }); وبعدما تكتمل الدالة الخارجية loadScript، يُشغّل ردّ النداء الدالة الداخلية. ولكن ماذا لو أردنا سكربتًا آخر، أيضًا…؟ loadScript('/my/script.js', function(script) { loadScript('/my/script2.js', function(script) { loadScript('/my/script3.js', function(script) { // ...نواصل متى اكتملت كلّ السكربتات }); }) }); هكذا نضع كلّ إجراء جديد داخل ردّ نداء. لو كانت الإجراءات قليلة فليست مشكلة، ولكن لو زادت فستصير كارثية. سنرى ذلك قريبًا. التعامل مع الأخطاء لم نضع الأخطاء في الحسبان في هذه الأمثلة. ماذا لو فشل تحميل السكربت؟ يفترض أن تتفاعل دالة ردّ النداء بناءً على الخطأ. إليك نسخة محسّنة من الدالة loadScript تتعقّب أخطاء التحميل: function loadScript(src, callback) { let script = document.createElement('script'); script.src = src; // هنا script.onload = () => callback(null, script); // خطأ في تحميل السكربت كذا script.onerror = () => callback(new Error(`Script load error for ${src}`)); document.head.append(script); } هنا نستدعي callback(null, script)‎ لو اكتمل التحميل، ونستدعي callback(error)‎ لو لم يكتمل. طريقة الاستعمال: loadScript('/my/script.js', function(error, script) { if (error) { // نتعامل مع الخطأ } else { // تحمّل السكربت بنجاح } }); نُعيد، ما استعملناه هنا للدالة loadScript هي طريقة مشهورة جدًا، تُدعى بأسلوب ”ردّ نداء الأخطاء أولًا“. إليك ما اصطُلح عليه: يُحجز الوسيط الأوّل من دالة callback للخطأ إن حدث، ويُستدعى callback(err)‎. يكون الوسيط الثاني (وغيرها إن دعت الحاجة) للنتيجة الصحيحة، هكذا نستعمل الدالة callback فقط للأمرين: الإبلاغ عن الأخطاء وتمرير النتيجة. هرم العذابات (Pyramid of Doom) يمكن أن نرى أنّ البرمجة بنمط اللاتزامن مفيد جدًا، وهذا جليّ من النظرة الأولى. فحين نكون أمام استدعاء أو استدعاءين فقط، فأمورنا ممتازة. ولكن لو تتابعت الإجراءات غير المتزامنة واحدة تلو الأخرى، سنرى هذا: loadScript('1.js', function(error, script) { if (error) { handleError(error); } else { // ... loadScript('2.js', function(error, script) { if (error) { handleError(error); } else { // ... loadScript('3.js', function(error, script) { if (error) { handleError(error); } else { // ‫...نُواصل بعد اكتمال تحميل كل السكربتات (*) } }); } }) } }); إليك ما في الشيفرة أعلاه: نُحمّل ‎1.js ونرى لو لم تحدث أخطاء. نُحمّل ‎2.js ونرى لو لم تحدث أخطاء. نُحمّل ‎3.js، ولو لم تحدث أخطاء نفّذنا شيئًا (*). فكلّما تداخل الاستدعاءات أكثر أصبحت الشيفرة متشعّبة جدًا وأصعب في الإدارة كثيرًا، هذا خصوصًا لو كانت هناك شيفرة فعلية لا ... (مثل الحلقات والعبارات الشرطية وغيرها). يُسمّون هذا أحيانًا ”بجحيم ردود النداء“ (callback hell) أو ”هرم العذابات“. بزيادة الإجراءات غير المتزامنة واحدًا بعد آخر، يتشعّب ”هرم“ الاستدعاءات المتداخلة إلى اليمين أكثر فأكثر، ولن يمضي من الوقت الكثير حتى دخلتْ في دوّامة حلزونية محال تنظيمها. بهذا تُعدّ طريقة البرمجة هذه سيّئة. يمكننا في محاولة يائسة لتقليل وقع المشكلة تحويل كلّ إجراء إلى دالة منفردة، هكذا: loadScript('1.js', step1); function step1(error, script) { if (error) { handleError(error); } else { // ... loadScript('2.js', step2); } } function step2(error, script) { if (error) { handleError(error); } else { // ... loadScript('3.js', step3); } } function step3(error, script) { if (error) { handleError(error); } else { // ‫...نواصل بعد اكتمال تحميل كل السكربتات (*) } }; رأيت الفكرة؟ مبدأ الشيفرة واحد وليس هناك تداخلات متشعّبة إذ حوّلنا كلّ إجراء إلى دالة لا مشكلة في الشيفرة إذ هي تعمل، ولكنّها فعليًا مقطّعة إربًا إربًا، ويصعُب على الشخص قراءتها وغالبًا تنتقل بنظرك بين أجزاء الشيفرة وأنت تقرأها. ليس هذا بالأمر الجيد خصوصًا لو لم يكن قارئ الشيفرة يعلم بما تفعله أو إلى أين ينقل نظره. كما وأنّ الدوال بأسماء الخطوات step* هي لاستعمال واحد ولم نصنعها إلّا لتفادي ”هرم العذابات“. لن يأتي أحد لاحقًا ويُعيد استعمالها في أيّ شيء عدا سلسلة الإجراءات هذه. بهذا ”نلوّث“ فضاء الأسماء، إن جاز التعبير. نريد بالطبع ما هو أفضل من هذا الحل. ولحسن حظنا (كالعادة)، فهناك طرق أخرى نتجنّب بها الأهرام هذه، أفضلها هي استعمال ”الوعود“ وسنشرحها في الفصل القادم. تمارين دائرة تتحرك ولها رد نداء نرى في التمرين Animated circle دائرة يكبُر مقاسها في حركة جميلة. لنقل بأنّا لا نريد الدائرة فقط، بل أيضًا عرض رسالة فيها. يجب أن تظهر الرسالة بعدما ينتهي التحريك (أي تكون الدائرة بمقاسها الكبير الكامل)، وإلّا بدا النص قبيح المظهر. في حلّ ذاك التمرين، نرى الدالة showCircle(cx, cy, radius)‎ ترسم الدائرة ولكن لا تُقدّم لنا أيّ طريقة نعرف بها انتهاء الرسم. أضِف وسيط ردّ نداء showCircle(cx, cy, radius, callback)‎ يُستدعى متى اكتمل التحريك. على المُعامل callback استلام كائن <div> للدائرة وسيطًا له. إليك مثالًا: showCircle(150, 150, 100, div => { div.classList.add('message-ball'); div.append("Hello, world!"); }); تجربة حية: يمكنك اعتماد حل هذا التمرين منطلقًا للحل. الحل يمكنك معاينة الحل من خلال المثال الحي. ترجمة -وبتصرف- للفصل Introduction: callbacks من كتاب The JavaScript language
  11. متى نكون نطوّر البرامج نحتاج إلى أصناف أخطاء خاصة بنا لتوضّح تمامًا ما قد يحدث خطأً في المهام التي نقوم بها. فمثلًا لأخطاء الشبكة نستعمل HttpError، ولعمليات قواعد البيانات DbError ولعمليات البحث NotFoundError وهكذا. وعلى هذه الأخطاء أن تدعم الخاصيات الأساسية مثل الرسالة message والاسم name والمَكدس stack (يفضّل ذلك)، ولكن يمكن أن تحتوي على خصائص أخرى خاصة بها مثل خاصية statusCode لكائنات HttpError وتحمل قيمة من قيم رموز الحالة 404 أو 403 أو 500. تتيح لنا جافاسكربت استعمال throw بتمرير أيّ وسيط، لذا فأصناف الخطأ الخاصة بنا يمكن ألّا ترث (تقنيًا) من كائن الخطأ Error، ولكن لو ورثنا منها فيمكنن للجميع استعمال obj instanceof Error لاحقًا لتتعرّف على كائنات الخطأ، بذلك يكون أفضل لو ورثناها. وكلّما كبر التطبيق شكّلت الأخطاء التي خصّصناها شجرة، فمثلًا سيرث الصنف HttpTimeoutError الصنفَ HttpError، وهكذا دواليك. توسيع Error لنأخذ مثالًا دالةَ readUser(json)‎ تقرأ كائن JSON في بيانات المستخدم. وهذا مثال عن كائن json صالح: let json = `{ "name": "John", "age": 30 }`; سنستعمل في الشيفرة التابِع JSON.parse، وإن استلم كائن json معطوب رمى خطأ SyntaxError. ولكن، حتّى لو كان الكائن صحيحًا صياغيًا، فلا يعني هذا أنّ المستخدم صالحًا أيضًا، أم لا؟ لربّما لا يحتوي بعض البيانات مثل خاصيتي الاسم json والعمر name الضروريتين للمستخدمين. بهذا لن تقرأ الدالة readUser(json)‎ كائن JSON فحسب، بل ستفحص (”تتحقّق من“) البيانات، فلو لم تكن الحقول المطلوبة موجودة، أو كان تنسيق الكائن مغلوطًا، فهنا نكون أمام خطأ… وهذا الخطأ ليس خطأً صياغيًا SyntaxError إذ أنّ البيانات صحيحة صياغيًا، بل نوع آخر من الأخطاء. سنسمّي هذا النوع ValidationError ونصنع صنف له. على هذا النوع من الأخطاء احتواء ما يلزم من معلومات تخصّ الحقل المخالف. يفترض علينا وراثة الصنف المضمّن في اللغة Error لصنفنا ValidationError. إليك شيء عن شيفرة الصنف المضمّن لنعرف ما نحاول توسعته: // شيفرة مبسّطة لصنف الخطأ ‫Error المضمّن في لغة جافاسكربت نفسها class Error { constructor(message) { this.message = message; this.name = "Error"; // (تختلف الأسماء باختلاف أصناف الأخطاء المضمّنة) this.stack = <call stack>; // ليست قياسية، إلّا أنّ أغلب البيئات تدعمها } } الآن صار وقت أن يرث الصنف ValidationError منها: class ValidationError extends Error { constructor(message) { super(message); // (1) this.name = "ValidationError"; // خطأ في التحقّق (2) } } function test() { throw new ValidationError("Whoops!"); // آخ } try { test(); } catch(err) { alert(err.message); // آخ! alert(err.name); // خطأ في التحقّق alert(err.stack); // قائمة من الاستدعاءات المتداخلة في كلّ منها رقم السطر } لاحظ كيف أنّنا في السطر (1) استدعينا الباني الأب، إذ تطلب منا لغة جافاسكربت استدعاء super في البانيات الابنة، أي أنّه أمر إلزامي. يضبط الباني الأب خاصية الرسالة message. كما يضبط أيضًا خاصية الاسم name لتكون "Error"، ولذلك نُعدّلها إلى القيمة الصحيحة في السطر (2). فلنحاول الآن استعماله في readUser(json)‎: class ValidationError extends Error { constructor(message) { super(message); this.name = "ValidationError"; // خطأ في التحقّق } } // الاستعمال function readUser(json) { let user = JSON.parse(json); if (!user.age) { throw new ValidationError("No field: age"); // حقل غير موجود: العمر } if (!user.name) { throw new ValidationError("No field: name"); // حقل غير موجود: الاسم } return user; } // هنا نجرّب باستعمال try..catch try { let user = readUser('{ "age": 25 }'); } catch (err) { if (err instanceof ValidationError) { alert("Invalid data: " + err.message); // البيانات غير صالحة: حقل غير موجود: الاسم } else if (err instanceof SyntaxError) { // (*) alert("JSON Syntax Error: " + err.message); // ‫خطأ صياغي لكائن JSON } else { throw err; // خطأ لا نعرفه، علينا إعادة رميه (**) } } تتعامل كتلة try..catch في الشيفرة أعلاه النوعين من الأخطاء: ValidationError والخطأ المضمّن SyntaxError الذي يرميه التابِع JSON.parse. لاحِظ أيضًا كيف استعملنا instanceof لفحص نوع الخطأ في السطر (*). يمكننا أيضًا مطالعة err.name هكذا: // ... // بدل ‫(err instanceof SyntaxError) } else if (err.name == "SyntaxError") { // (*) // ... ولكنّ استعمال instanceof أفضل بكثير إذ يحدث ونوسّع مستقبلًا الصنف ValidationError بأصناف فرعية منه مثل PropertyRequiredError، والفحص عبر instanceof سيظلّ يعمل للأصناف الموروثة منه، كما من المهمّ أن تُعيد كتلة catch رمي الأخطاء التي لا تفهمها، كما في السطر (**). ليس على هذه الكتلة إلّا التعامل مع أخطاء التحقّق والصياغة، أمّا باقي الأخطاء (والتي قد تحدث بسبب الأخطاء المطبعية في الشيفرة أو غيرها من أمور غريبة عجيبة) فيجب أن تسقط أرضًا. تعميق الوراثة صنف الخطأ ValidationError عامٌ جدًا جدًا، إذ يمكن أن تحصل أمور كثيرة خطأً في خطأ. لربّما كانت الخاصية غير موجودة أو كان نسقها خطأ (مثل تقديم سلسلة نصية قيمةً للعمر age). لنصنع الصنف …. PropertyRequiredError ونستعمله فقط للخاصيات غير الموجودة، وسيحتوي على أيّة معلومات إضافية عن الخاصية الناقصة. class ValidationError extends Error { constructor(message) { super(message); this.name = "ValidationError"; // خطأ في التحقّق } } // هذا class PropertyRequiredError extends ValidationError { constructor(property) { super("No property: " + property); // خاصية غير موجودة this.name = "PropertyRequiredError"; // خطأ إذ الخاصية مطلوبة this.property = property; } } // الاستعمال function readUser(json) { let user = JSON.parse(json); if (!user.age) { throw new PropertyRequiredError("age"); // العمر } if (!user.name) { throw new PropertyRequiredError("name"); // الاسم } return user; } // هنا نجرّب باستعمال ‫try..catch try { let user = readUser('{ "age": 25 }'); } catch (err) { if (err instanceof ValidationError) { alert("Invalid data: " + err.message); // البيانات غير صالحة: خاصية غير موجودة: الاسم alert(err.name); // PropertyRequiredError alert(err.property); // name } else if (err instanceof SyntaxError) { alert("JSON Syntax Error: " + err.message); // خطأ صياغي لكائن ‫JSON } else { throw err; // خطأ لا نعرفه، علينا إعادة رميه } } يسهُل علينا استعمال الصنف الجديد PropertyRequiredError، فكلّ ما علينا تمريره هو اسم الخاصية: new PropertyRequiredError(property)‎، وسيُولّد الباني الرسالةَ message التي نفهمها نحن البشر. لاحظ كيف أنّنا أسندنا الخاصية this.name في باني PropertyRequiredError يدويًا، مرّة ثانية. قد ترى هذا الأمر مُتعبًا حيث ستُسند قيمة this.name = <class name>‎ في كلّ صنف خطأ تصنعه. يمكن تجنّب هذا العناء وإسناد قيمة مناسبة لصنف ”الخطأ الأساس“ this.name = this.constructor.name، وبعدها نرث من هذا الصنف كلّ أصناف الأخطاء المخصّصة. لنسمّه مثلًا MyError. إليك شيفرة MyError وغيرها من أصناف أخطاء مخصّصة، ولكن مبسّطة: class MyError extends Error { constructor(message) { super(message); this.name = this.constructor.name; // هنا } } class ValidationError extends MyError { } class PropertyRequiredError extends ValidationError { constructor(property) { super("No property: " + property); // خاصية غير موجودة this.property = property; } } // الاسم صحيح alert( new PropertyRequiredError("field").name ); // PropertyRequiredError الآن صارت شيفرات الأخطاء المخصّصة أقصر بكثير (خاصّة ValidationError) إذ حذفنا السطر "this.name = ...‎" في الباني. تغليف الاستثناءات هدف الدالة readUser في الشيفرة أعلاه هو ”قراءة بيانات المستخدم“، ويمكن أن تحدث مختلف الأخطاء أثناء تنفيذ ذلك. حاليًا نرى SyntaxError وValidationError فقط، ولكن في المستقبل العاجل ستصير الدالة readUser أكبر وأكبر وقد تُولّد لنا أنواع أخرى من الأخطاء. ومَن يتعامل مع هذه الأخطاء؟ الشيفرة التي تستدعي readUser! حاليًا لا تستعمل إلا بضعة تعابير شرطية if في كُتل catch (تفحص الصنف وتتعامل مع الأخطاء وتُعيد رمي ما لا تفهم)، وسيكون المخطط هكذا: try { ... readUser() // الخطأ الأساسي هنا ... } catch (err) { if (err instanceof ValidationError) { // ‫معالحة أخطاء ValidationError } else if (err instanceof SyntaxError) { // ‫معالجة الأخطاء الصياغية SyntaxError } else { throw err; // خطأ مجهول فلنُعد رميه من جديد } } نرى في الشيفرة البرمجية أعلاه نوعين من الأخطاء ولكن ممكن أن يكون أكثر من ذلك. ولكن متى ولّدت الدالة readUser أنواع أخرى من الأخطاء، فعلينا طرح السؤال: هل علينا حقًا فحص كلّ نوع من أنواع الأخطاء واحدًا واحدًا في كلّ شيفرة تستدعي readUser؟ عادةً ما يكون الجواب هو ”لا“، فالشيفرة الخارجية تفضّل أن تكون ”على مستوًى أعلى من ذلك المستوى“، أي أن تستلم ما يشبه ”خطأ في قراءة البيانات“، أمّا عن سبب حدوثه فلا علاقة لها به (طالما رسالة الخطأ تصف نفسها). أو ربّما (وهذا أفضل) تكون هناك طريقة لتحصل فيها على بيانات الخطأ، لو أرادت الشيفرة ذلك. تدعى الطريقة التي وصفناها بتغليف الاستثناءات. لنصنع الآن صنف ”خطأ في القراءة“ ReadError لنمثّل هذه الأخطاء. متى حدثت أخطاء داخل الدالة readUser، سنلتقطها فيها ونُولّد خطأ ReadError، بدلًا من ValidationError و SyntaxError. الكائن ReadError سيُبقي أيضًا إشارة إلى الخطأ الأصلي في خاصية السبب cause. لذا و هكذا لن يكون للشيفرة الخارجية (التي ستستدعي readUser) إلّا فحص ReadError. وليس كلّ نوع من أخطاء قراءة البيانات. وإن كان يحتاج لمزيد من التفاصيل عن الخطأ يمكنه العودة إلى الخاصية cause والتحقق منها. إليك الشيفرة التي نُعرّف فيها الخطأ ReadError ونمثّل طريقة استعماله في الدالة readUser وفي try..catch: class ReadError extends Error { constructor(message, cause) { super(message); this.cause = cause; this.name = 'ReadError'; // خطأ في القراءة } } class ValidationError extends Error { /*...*/ } class PropertyRequiredError extends ValidationError { /* ... */ } function validateUser(user) { if (!user.age) { throw new PropertyRequiredError("age"); // العمر } if (!user.name) { throw new PropertyRequiredError("name"); // الاسم } } function readUser(json) { let user; try { user = JSON.parse(json); } catch (err) { // هنا if (err instanceof SyntaxError) { throw new ReadError("Syntax Error", err); // خطأ صياغي } else { throw err; } } try { validateUser(user); } catch (err) { // وهنا if (err instanceof ValidationError) { throw new ReadError("Validation Error", err); // خطأ في التحقّق } else { throw err; } } } try { readUser('{bad json}'); } catch (e) { if (e instanceof ReadError) { // هنا alert(e); // Original error: SyntaxError: Unexpected token b in JSON at position 1 alert("Original error: " + e.cause); // الخطأ الأصل } else { throw e; } } في الشيفرة أعلاه تعمل الدالة readUser تمامًا كم وصفها، تلتقط أخطاء الصياغة والتحقّق وترمي أخطاء قراءة ReadError بدلها (كما وتُعيد رمي الأخطاء المجهولة أيضًا). هكذا ليس على الشيفرة الخارجية إلّا فحص instanceof ReadError فقط، لا داعٍ للتحقّق من كلّ خطأ يمكن أن يحصل في هذه المجرّة. يُسمّى هذا النهج ”بتغليف الاستثناءات“ حيث نستلم نحن ”الاستثناءات في المستوى المنخفض من البرمجة“ (low level) و”نُغلّفها“ داخل خطأ ReadError يكون أكثر بساطة وأسهل استعمالًا للشيفرات التي تنادي على الدوال. هذا النهج مستعمل بكثرة في البرمجة كائنية التوجّه. خلاصة يمكننا وراثة صنف الخطأ Error وغيرها من أخطاء مضمّنة كما الوراثة العادية. المهم أن ننتبه من خاصية الاسم name ولا ننسى استدعاء super. يمكننا استعمال instanceof لفحص ما نريد من أخطاء بدقّة، كما ويعمل المُعامل مع الوراثة. أحيانًا نستلم كائن خطأ من مكتبة طرف ثالث وما من طريقة سهلة لنعرف اسم صنفها. هنا يمكن استعمال خاصية الاسم name لإجراء هذا الفحص. أسلوب تغليف الاستثناءات هو أسلوب منتشر الاستعمال، فيه تتعامل الدالة مع الاستثناءات في المستوى المنخفض من البرمجة، وتصنع أخطاء مستواها عالٍ بدل تلك المنخفضة. وأحيانًا تصير الاستثناءات المنخفضة خصائص لكائن الخطأ (تمامًا مثل err.cause في الأمثلة أعلاه)، ولكنّ هذا ليس إلزاميًا أبدًا. تمارين الوراثة من SyntaxError الأهمية: 5 اصنع الصنف FormatError ليرث من الصنف المضمّن SyntaxError. يجب أن يدعم الصنف خصائص الاسم name والرسالة message والمَكدس stack. طريقة الاستعمال: let err = new FormatError("formatting error"); // خطأ في التنسيق alert( err.message ); // خطأ في التنسيق alert( err.name ); // FormatError alert( err.stack ); // المَكدس alert( err instanceof FormatError ); // true alert( err instanceof SyntaxError ); // ‫true (إذ يرث الصنف الصنفَ SyntaxError) الحل class FormatError extends SyntaxError { constructor(message) { super(message); this.name = this.constructor.name; } } let err = new FormatError("formatting error"); alert( err.message ); // خطأ في التنسيق alert( err.name ); // خطأ في التنسيق alert( err.stack ); // stack(المَكدس) alert( err instanceof SyntaxError ); // true ترجمة -وبتصرف- للفصل Custom errors, extending Error من كتاب The JavaScript language
  12. مهما كنّا عباقرة نحن معشر المبرمجين، فلا بدّ أن تكون في السكربتات مشكلة ما. تحدث هذه المشاكل إمّا بسببنا، أو بسبب شيء أدخله المستخدم لم نتوقّعه، أو بسبب ردّ فيه خطأ من الخادوم، أو بمليار سبب آخر. في العادة ”يموت“ السكربت (أي يتوقّف مباشرة) لو حدث خطأ، ويطبع ذلك في الطرفية. ولكنّ الصياغة try..catch تتيح لنا ”التقاط“ هذه الأخطاء والقيام بما هو مفيد بدل أن يموت السكربت. صياغة try..catch للتعبير try..catch كتلتين برمجيتين أساسيتين: التجربة try والالتقاط catch بعدها: try { // الشيفرة... } catch (err) { // التعامل مع الأخطاء } يعمل التعبير هكذا: أولًا، يجري تنفيذ الشيفرة في try {...}‎. لم نجح التنفيذ دون أيّ أخطاء، تُهمل الكتلة catch(err)‎ وتصل عملية التنفيذ إلى نهاية try وتواصل عملها بعد تجاهل catch. إن حدث أيّ خطأ يتوقّف التنفيذ داخل كتلة try وينتقل سير العملية إلى بداية الكتلة catch(err)‎. سيحتوي المتغير err (يمكننا استبداله بأيّ اسم آخر) كائن خطأ فيه تفاصيل عمّا حدث. بهذا لا تقتل الأخطاء داخل كتلة try {…}‎ السكربت، فما زال ممكنًا أن نتعامل معها في catch. وقت الأمثلة. إليك مثالًا ليس فيه أخطاء: يعرض alert السطرين (1) و (2): try { alert('Start of try runs'); // (1) <-- بداية التشغيلات // ...ما من أخطاء هنا alert('End of try runs'); // (2) <-- نهاية التشغيلات } catch(err) { // (3)‫ تُهمل catch إذ ليست هناك أخطاء alert('Catch is ignored, because there are no errors'); } مثال فيه خطأ: يعرض السطرين (1) و(3): try { alert('Start of try runs'); // (1) <-- بداية التشغيلات lalala; // error, variable is not defined! خطأ: المتغير غير معرّف! alert('End of try (never reached)'); // (2)‫ نهاية كتلة try (لا نصل إليها أبدًا) } catch(err) { alert(`Error has occurred!`); // (3) <-- حدث خطأ! } تحذير: لا يعمل تعبير try..catch إلّا مع الأخطاء أثناء التشغيل يجب أن تكون الشيفرة البرمجية صياغتها صحيحة لكي تعمل try..catch بعبارة أخرى، يجب أن تكون الشيفرة البرمجية خالية من أخطاء الصياغة. لن تعمل في هذه الحالة لأن هنالك أقواسًا مفتوحة بدون غُلاقاتها. try { {{{{{{{{{{{{ } catch(e) { alert("لا يمكن للمحرك فهم هذه الشيفرة فهي غير صحيحة"); } أولًا يقرأ محرك جافاسكربت الشيفرة البرمجية، ومن ثمّ يشغّلها. تدعى الأخطاء التي تحدث في مرحلة القراءة أخطاء التحليل (parse-time) ولا يمكن إصلاحها (من داخل الشيفرة البرمجية نفسها). وذلك لأن محرك لغة جافاسكربت لا يستطيع فهم الشيفرة البرمجية من الأساس. تحذير:تعمل الصياغة try..catch بشكل متزامن. إذا حدث خطأ ما في شيفرة برمجية مجدولة مثلما في setTimeout فلن تستطيع الصياغة try..catch أن تلتقطه: try { setTimeout(function() { noSuchVariable; // السكربت سيموت هنا }, 1000); } catch (e) { alert( "won't work" ); } وذلك لأن التابِع سيُنفذ لاحقًا، بينما يكون محرك جافاسكربت غادر باني try..catch. للتقاط خطأ بداخل تابِع مُجدوَل يجب أن تكون الصياغة try..catch في داخل هذا التابِع. setTimeout(function() { try { // صياغة ‫try..catch تُعالح الخطأ noSuchVariable; // try..catch handles the error! } catch { alert( "error is caught here!" ); } }, 1000); كائن الخطأ Error تولّد جافاسكربت -متى حدث الخطأ- كائنًا يحوي تفاصيل الخطأ كاملةً، بعدها تُمرّرها وسيطًا إلى catch: try { // ... } catch(err) { // ‫ هذا ”كائن الخطأ“، ويمكننا استعمال أيّ اسم نريد بدل err // ... } لكائن الخطأ (في حالة الأخطاء المضمّنة في اللغة) خاصيتين اثنتين: name: اسم الخطأ. فمثلًا لو كان المتغير غير معرّفًا فسيكون الاسم "ReferenceError". message: الرسالة النصية بتفاصيل الخطأ. كما هناك خاصيات أخرى غير قياسية في أغلب بيئات جافاسكربت. أكثرها استعمالًا ودعمًا هي: stack: مَكدس الاستدعاء الحالي: سلسلة نصية فيها معلومات عن سلسلة الاستدعاءات المتداخلة التي تسبّبت بالخطأ. نستعمل الخاصية للتنقيح فقط. مثال: try { *!* lalala; // خطأ المتغيّر غير معرّف */!* } catch(err) { alert(err.name); // ReferenceError(خطأ في الإشارة) alert(err.message); // المتغيّر ‫lalala غير معرف alert(err.stack); // ‫ReferenceError:(خطأ في الإشارة) المتغيّر ‫lalala غير معرّف في (...call stack) //يمكننا أيضًا عرض الخطأ بالكامل //تحول الخطأ إلى سلسلة نصية هكذا ‫:"name: message" alert(err); // ‫ReferenceError:(خطأ في الإشارة) المتغيّر ‫lalala غير معرّف } إسناد "catch" الاختياري تحذير:هذه إضافة حديثة للغة من الممكن أن تحتاج لترقيع الخطأ في المتصفحات القديمة لأن هذه الميّزة جديدة. لو لم تريد تفاصيل الخطأ فيمكنك إزالتها من catch: try { // ... } catch { // <-- بدون المتغير (err) // ... } استعمال "try..catch" هيًا نرى معًا مثالًا واقعيًا عن try..catch: كما نعلم فجافاسكربت تدعم التابِع JSON.parse(str)‎ ليقرأ القيم المرمّزة بِـ JSON. عادةً ما نستعمله لفكّ ترميز البيانات المستلمة من الشبكة، كانت آتية من خادوم أو من أيّ مصدر غيره. نستلم البيانات ونستدعي JSON.parse هكذا: let json = '{"name":"John", "age": 30}'; // بيانات من الخادوم // لاحظ let user = JSON.parse(json); // نحوّل التمثيل النصي إلى كائن جافاسكربت // الآن صار ‫user كائنًا فيه خاصيات أتت من السلسلة النصية alert( user.name ); // John alert( user.age ); // 30 يمكنك أن ترى تفاصيل أكثر عن JSON في درس JSON methods, toJSON. لو كانت سلسلة json معطوبة، فسيُولّد JSON.parse خطأً و”ويموت“ السكربت. هل نقبل بالأمر الواقع المرير؟ لا وألف لا! في هذا الحال لن يعرف الزائر ما يحدث لو حدث للبيانات أمر (ما لم يفتح طرفية المطوّرين). وعادةً ما لا يحب الناس ما ”يموت“ أمامهم دون أن يُعطيهم رسالة خطأ. فلنستعمل try..catch للتعامل مع هذه المعضلة: let json = "{ bad json }"; try { let user = JSON.parse(json); // <-- لو حدث خطأ... alert( user.name ); // فلن يعمل هذا } catch (e) { // ...تنتقل عملية التنفيذ إلى هنا alert( "Our apologies, the data has errors, we'll try to request it one more time." ); // المعذرة، في البيانات أخطاء. سنحاول طلبها مرة ثانية. alert( e.name ); alert( e.message ); } استعملنا هنا الكتلة catch لعرض رسالة لا أكثر، ولكن يمكن أن نستغلّها أفضل من هذا: مثل إرسال طلب إلى الشبكة، أو اقتراح عملية بديلة على الزائر أو إرسال معلومات الخطأ إلى … تسجيل، وغيرها… هذا كلّه أفضل من الموت والموت فقط. رمي أخطائنا نحن ماذا لو كان كائن json صحيح صياغيًا إلّا أنّ الخاصية المطلوبة name ليست فيه؟ هكذا مثلًا: let json = '{ "age": 30 }'; // البيانات ناقصة try { let user = JSON.parse(json); // <-- لا أخطاء alert( user.name ); // ما من اسم! } catch (e) { alert( "doesn't execute" ); // لا يتنفّذ السطر } هنا سيعمل التابِع JSON.parse كما يجب، لكن عدم وجود الخاصية name هي المشكلة والخطأ في هذه الحالة. لتوحيد طريقة معالجة الأخطاء، سنستخدم مُعامل throw. مُعامل "الرمي" throw يُولّد لنا مُعامل الرمي throw خطأً. صياغته هي: throw <error object> يمكننا تقنيًا استعمال ما نريد ليكون كائن خطأ، مثل الأنواع الأولية كالأعداد والسلاسل النصية. ولكن من الأفضل استعمال الكائنات ومن الأفضل أكثر أن تملك خاصيتي الاسم name والرسالة message (لتكون متوافقة إلى حدٍّ ما مع الأخطاء المضمّنة). في جافاسكربت مختلف البانيات المضمّنة للأخطاء القياسية: الخطأ Error والخطأ الصياغي SyntaxError والخطأ في الإشارة ReferenceError والخطأ في النوع TypeError وغيرها. يمكننا استعمال هذه البانيات أيضًا لإنشاء كائنات الخطأ. صياغتها هي: let error = new Error(message); // أو let error = new SyntaxError(message); let error = new ReferenceError(message); // ... في الأخطاء المضمّنة في اللغة (ولا أعني الكائنات أيًا كانت، الأخطاء بعينها) تكون قيمة الخاصية name هي ذاتها اسم الباني، وتُؤخذ الرسالة message من الوسيط. مثال: let error = new Error("Things happen o_O"); // غرائب وعجائب ة_ه alert(error.name); // خطأ alert(error.message); // غرائب وعجائب ة_ه لنرى نوع الخطأ الذي يُولّده التابِع JSON.parse: try { JSON.parse("{ bad json o_O }"); // جيسون شقي ة_ه } catch(e) { alert(e.name); // SyntaxError خطأ صياغي alert(e.message); // Unexpected token o in JSON at position 2 } كما رأينا فهو خطأ صياغي SyntaxError. وفي حالتنا نحن فعدم وجود الخاصية name هي خطأ، إذ لا بدّ أن يكون للمستخدم اسمًا. هيًا نرمِ الخطأ: let json = '{ "age": 30 }'; // البيانات ناقصة try { let user = JSON.parse(json); // <-- لا أخطاء if (!user.name) { throw new SyntaxError("Incomplete data: no name"); // (*) البيانات ناقصة: ليس هنالك اسم } alert( user.name ); } catch(e) { alert( "JSON Error: " + e.message ); // خطأ ‫JSON: البيانات ناقصة: ليس هنالك اسم } يُولّد مُعامل throw (في السطر (*)) خطأً صياغيًا SyntaxError له الرسالة الممرّرة message، تمامًا كما تُولّد جافاسكربت نفسها الخطأ. بهذا يتوقّف التنفيذ داخل try وينتقل سير العملية إلى catch. الآن صارت الكتلة catch هي المكان الذي نتعامل فيه مع الأخطاء فحسب، أكانت أخطاء JSON.parse أم أخطاء أخرى. إعادة الرمي يمكننا في المثال أعلاه استعمال try..catch للتعامل مع البيانات الخاطئة. ولكن هل يمكن أن يحدث خطأ آخر غير متوقّع في كتلة try {...}‎؟ مثلًا لو كان خطأ المبرمج (لم يعرّف المتغير) أو شيئًا آخر ليس أنّ ”البيانات خاطئة“؟ مثال: let json = '{ "age": 30 }'; // البيانات ناقصة try { user = JSON.parse(json); // ‫ نسينا ”let“ قبل user // ... } catch(err) { alert("JSON Error: " + err); // خطأ ‫JSON: خطأ في الإشارة: user غير معرّف (في الواقع، ليس خطأ JSON) } كلّ شيء ممكن كما تعلم! فالمبرمجون يخطؤون. حتّى في الأدوات مفتوحة المصدر والتي يستعملها ملايين البشر لسنين تمضي، لو اكتُشفت علة فجأةً ستؤدّي إلى اختراقات مأساوية. في حالتنا هذه على try..catch التقاط أخطاء ”البيانات الخاطئة“ فقط، ولكنّ طبيعة catch هي أن تلتقط كلّ الأخطاء من try. فهنا مثلًا التقطت خطأً غير متوقع ولكنها ما زالت تصرّ على رسالة "JSON Error". هذا ليس صحيحًا ويصعّب من تنقيح الشيفرة كثيرًا. لحسن الحظ فيمكننا معرفة الخطأ الذي التقطناه، من اسمه name مثلًا: try { user = { /*...*/ }; } catch(e) { alert(e.name); // ‫”خطأ في الإشارة” محاولة الوصول لمتغيّر غير معرّف } القاعدة ليست بالمعقّدة: على catch التعامل مع الأخطاء التي تعرفها فقط، و”إعادة رمي“ ما دونها. يمكننا توضيح فكرة ”إعادة الرمي“ بهذه الخطوات: تلتقط catch كلّ الأخطاء. نحلّل كائن الخطأ err في كتلة catch(err) {...}‎. لو لم نعرف طريقة التعامل معه، رميناه throw err. في الشيفرة أسفله استعملنا إعادة الرمي لتتعامل الكتلة catch مع أخطاء الصياغة فقط SyntaxError: let json = '{ "age": 30 }'; // البيانات ناقصة try { let user = JSON.parse(json); if (!user.name) { throw new SyntaxError("Incomplete data: no name"); // البيانات ناقصة: لا اسم } blabla(); // خطأ غير متوقع alert( user.name ); } catch(e) { if (e.name == "SyntaxError") { alert( "JSON Error: " + e.message ); // خطأ JSON: كذا كذا } else { throw e; // نُعيد رميه (*) } } الخطأ المرمي في السطر (*) داخل كتلة catch ”يسقط خارج أرض“ try..catch ويمكننا إمّا التقاطه باستعمال تعبير try..catch خارجي (لو كتبناه) أو سيقتل الخطأ السكربت. هكذا لا تتعامل الكتلة catch إلا مع ما تعرف مع أخطاء وتتخطّى (إن صحّ القول) الباقي. يوضّح المثال أسفله كيفية التقاط هذه الأخطاء بمستويات أخرى من try..catch: function readData() { let json = '{ "age": 30 }'; try { // ... blabla(); // خطأ! } catch (e) { // ... if (e.name != 'SyntaxError') { throw e; // ‫نُعيد رميه (لا ندري طريقة التعامل معه) } } } try { readData(); } catch (e) { alert( "External catch got: " + e ); // التقطناه أخيرًا! } هكذا لا تعرف الدالة readData إلّا طريقة التعامل مع أخطاء الصياغة SyntaxError، بينما تتصرّف تعابير try..catch الخارجية بغيرها من أخطاء (إن عرفتها). try..catch..finally لحظة… لم ننتهي بعد. يمكن أيضًا أن يحتوي تعبير try..catch على مُنغلقة أخرى: finally. لو كتبناها فستعمل في كلّ الحالات الممكنة: بعد try لو لم تحدث أخطاء وبعد catch لو حدثت أخطاء. هكذا هي الصياغة ”الموسّعة“: try { ... نحاول تنفيذ الشيفرة ... } catch(e) { ... نتعامل مع الأخطاء ... } finally { ... ننفّذه مهما كان الحال ... } جرّب تشغيل هذه الشيفرة: try { alert( 'try' ); if (confirm('Make an error?')) BAD_CODE(); // أتريد ارتكاب خطأ؟ } catch (e) { alert( 'catch' ); // أمسكناه } finally { alert( 'finally' ); // بعد ذلك } يمكن أن تعمل الشيفرة بطريقتين اثنتين: لو كانت الإجابة على ”أتريد ارتكاب خطأ؟“ ”نعم“، فستعمل هكذا try -> catch -> finally. لو رفضت ذلك، فستعمل هكذا try -> finally. نستعمل عادةً مُنغلقة finally متى ما شرعنا في شيء وأردنا إنهائه مهما كانت نتيجة الشيفرة. فمثلًا نريد قياس الوقت الذي تأخذه دالة أعداد فيبوناتشي fib(n)‎. الطبيعي أن نبدأ القياس قبل التشغيل ونُنهيه بعده، ولكن ماذا لو حدث خطأ أثناء استدعاء الدالة؟ مثلًا خُذ شيفرة الدالة fib(n)‎ أسفله وسترى أنّها تُعيد خطأً لو كانت الأعداد سالبة أو لم تكن أعدادًا صحيحة (not integer). مُنغلقة finally هي المكان الأمثل لإنهاء هذا القياس مهما كانت النتيجة، فتضمن لنا بأنّ الوقت سيُقاس كما ينبغي في الحالتين معًا، نجح تنفيذ أم لم ينجح وأدّى لخطأ: let num = +prompt("Enter a positive integer number?", 35) let diff, result; function fib(n) { if (n < 0 || Math.trunc(n) != n) { throw new Error("Must not be negative, and also an integer."); } return n <= 1 ? n : fib(n - 1) + fib(n - 2); } let start = Date.now(); try { result = fib(num); } catch (e) { result = 0; *!* } finally { diff = Date.now() - start; } */!* alert(result || "error occurred"); alert( `execution took ${diff}ms` ); يمكنك التأكّد بتشغيل الشيفرة وإدخال 35 في prompt، وستعمل كما يجب، أي تكون finally بعد try. وبعد ذلك جرّب إدخال ‎-1 وسترى خطأً مباشرةً وسيأخذ تنفيذ الشيفرة مدّة 0ms، وكِلا الزمنين المقيسين صحيحين. أي أنّه يمكن للدالة أن بعبارة return أو throw، لا مشكلة إذ ستُنفّذ مُنغلقة finally في أيّ من الحالتين. ملاحظة:المتغيّرات في صياغة try..catch..finally محلية. لاحظ أن المتغيرات result و diff في الشيفرة البرمجية أعلاه معرّفة قبل الصياغة try..catch. وبذلك إذا عرفنا let في الصياغة try فستبقى مرئية بداخلها فقط. ملاحظة: بخصوص finally و return فإن مُنغلقة finally تعمل مع أي خروج من صياغة try..catch والتي تتضمن خروج واضح من خلال تعليمة return. في المثال أدناه هنالك تعليمة return في try في هذه الحالة ستنفذّ finally قبل عودة عنصر التحكم إلى الشيفرة البرمجية الخارجية مباشرة. function func() { try { *!* return 1; */!* } catch (e) { /* ... */ } finally { *!* alert( 'finally' ); */!* } } alert( func() ); // ‫أولًا ستعمل تعليمة alert من المنغلقة finally ومن ثمّ هذه ملاحظة: إن باني try..finally بدون منغلقة catch مفيد أيضًا إذ يمكننا استخدامه عندما لا نريد معالجة الأخطاء وإنما نريد التأكد من أن العمليات التي بدأناها انتهت فعلًا. function func() { // ‫نبدأ بشيء يحتاج لإكمال (مثل عملية القياس)‎ try { // ... } finally { // نكمله هنا حتى وإن كلّ السكربت مات } } في الشيفرة البرمجية أعلاه، هنالك خطأ دائمًا يحدث في try لأنه لا يوجد catch. ومع ذلك ستعمل finally قبل الخروج من التابع. الالتقاط العمومي تحذير: مخصص لبيئة العمل إن المعلومات في هذه الفقرة ليست جزءًا من لغة جافاسكربت لنقل مثلًا أنْ حصل خطأ فادح خارج تعبير try..catch، ومات السكربت (مثلًا كان خطأ المبرمج أو أيّ شيء آخر مريب). أهناك طريقة يمكننا التصرّف فيها في هكذا حالات؟ لربّما أردنا تسجيل الخطأ أو عرض شيء معيّن على المستخدم (عادةً لا يرى المستخدمون رسائل الأخطاء) أو غيرها. لا نجد في المواصفة أيّ شيء عن هذا، إلّا أنّ بيئات اللغة تقدّم هذه الميزة لأهميتها البالغة. فمثلًا تقدّم Node.js الحدث process.on("uncaughtException")‎. ولو كنّا نُطوّر للمتصفّح فيمكننا إسناد دالة إلى خاصية window.onerror الفريدة (إذ تعمل متى حدث خطأ لم يُلتقط). الصياغة هي: window.onerror = function(message, url, line, col, error) { // ... }; message: رسالة الخطأ. url: عنوان URL للسكربت أين حدث الخطأ. line وcol: رقم السطر والعمود أين حدث الخطأ. error: كائن الخطأ. مثال: <script> window.onerror = function(message, url, line, col, error) { alert(`${message}\n At ${line}:${col} of ${url}`); }; function readData() { badFunc(); // Whoops, something went wrong! } readData(); </script> عادةً، لا يكون الهدف من …. العمومي window.onerror استعادة تنفيذ السكربت (إذ هو أمر مستحيل لو كانت أخطاء من المبرمج) بل يكون لإرسال هذه الرسائل إلى المبرمج. كما أنّ هناك خدمات على الوِب تقدّم مزايا لتسجيل الأخطاء في مثل هذه الحالات، أمثال https://errorception.com وhttp://www.muscula.com. وهكذا تعمل: نُسجّل في الخدمة ونأخذ منهم شيفرة جافاسكربت صغيرة (أو عنوانًا للسكربت) لنضعها في صفحاتنا. يضبط هذا السكربت دالة window.onerror مخصّصة. ومتى حدث خطأ أرسلَ طلب شبكة بالخطأ إلى الخدمة. ويمكننا متى أردنا الولوج إلى واجهة الوِب للخدمة ومطالعة تلك الأخطاء. خلاصة يتيح لنا تعبير try..catch التعامل مع الأخطاء أثناء تنفيذ الشيفرات. كما يدلّ اسمها فهي تسمح ”بتجربة“ تنفيذ الشيفرة و”والتقاط“ الأخطاء التي قد تحدث فيها. صياغتها هي: try { // شغّل الشيفرة } catch(err) { // انتقل إلى هنا لو حدث خطأ // ‫err هو كائن الخطأ } finally { // مهما كان الذي حدث، نفّذ هذا } يمكننا إزالة قسم catch أو قسم finally، واستعمال الصيغ الأقصر try..catch وtry..finally. لكائنات الأخطاء الخاصيات الآتية: message: رسالة الخطأ التي نفهمها نحن البشر. name: السلسلة النصية التي فيها اسم الخطأ (اسم باني الخطأ). stack (ليست قياسية ولكنّها مدعومة دعمًا ممتازًا): المَكدس في لحظة إنشاء الخطأ. لو لم تُرد كائن الخطأ فيمكنك إزالتها باستعمال catch { بدل catch(err) {‎. يمكننا أيضًا توليد الأخطاء التي نريد باستعمال مُعامل throw. تقنيًا يمكن أن يكون وسيط throw ما تريد ولكن من الأفضل لو كان كائن خطأ يرث صنف الأخطاء Error المضمّن في اللغة. سنعرف المزيد عن توسعة الأخطاء في القسم التالي. كما يُعدّ نمط ”إعادة الرمي“ البرمجي نمطًا مهمًا عند التعامل مع الأخطاء، فلا تتوقّع كتلة catch إلّا أنواع الأخطاء التي تفهمها وتعرف طريقة التعامل معها، والباقي عليها إعادة رميه لو لم تفهمه. حتّى لو لم نستعمل تعبير try..catch فأغلب البيئات تتيح لنا إعداد … أخطاء ”عمومي“ لالتقاط الأخطاء التي ”تسقط أرضًا“. هذا … في المتصفّحات اسمه window.onerror. تمارين أخيرًا أم الشيفرة؟ الأهمية: 5 وازِن بين الشيفرتين هتين. تستعمل الأولى finally لتنفيذ الشيفرة بعد try..catch: try { عمل عمل عمل } catch (e) { نتعامل مع الأخطاء } finally { ننظّف مكان العمل // هنا } الثانية تضع عملية التنظيف بعد try..catch: try { عمل عمل عمل } catch (e) { نتعامل مع الأخطاء } ننظّف مكان العمل // ‎هنا سنحتاج بكل بتأكيد للتنظيف بعد العمل. بغض النظر إن وجدَنا خطأً ما أم لا. هل هنالك ميزة إضافية لاستخدام finally؟ أم أن الشيفرتين السابقتين متساويتين؟ إن كان هنالك ميزّة اكتب مثلًا يوضحها. الحل عندما ننظر إلى الشيفرة الموجودة بداخل التابع يصبح الفرق جليًا. يختلف السلوك إذا كان هناك "قفزة" من "try..catch". على سبيل المثال، عندما يكون هناك تعليمة return بداخل صياغة try..catch. تعمل منغلقة finally في حالة وجود أي خروج من صياغة try..catch، حتى عبر تعليمة return: مباشرة بعد الانتهاء من try..catch ، ولكن قبل أن تحصل شيفرة الاستدعاء على التحكم. function f() { try { alert('start'); return "result"; } catch (e) { /// ... } finally { alert('cleanup!'); } } f(); // cleanup! … أو عندما يكون هناك throw، مثل هنا: function f() { try { alert('start'); throw new Error("an error"); } catch (e) { // ... if("can't handle the error") { throw e; } } finally { alert('cleanup!') } } f(); // cleanup! تضمنُ finally عملية التنظيف هنا. إذا وضعنا الشيفرة البرمجية في نهاية f ، فلن تشغّل في هذا السيناريو. ترجمة -وبتصرف- للفصل Error handling, "try..catch"‎ من كتاب The JavaScript language
  13. لا يمكننا في جافاسكربت إلّا أن نرث كائنًا واحدًا فقط. وليس هناك إلّا كائن [[Prototype]] واحد لأيّ كائن. ولا يمكن للصنف توسعة أكثر من صنف واحد. وأحيانًا يكون ذلك مُقيّدًا نوعًا ما. فنقل بأنّ لدينا صنف ”كنّاسة شوارع“ StreetSweeper وصنف ”دراجة هوائية“ Bicycle، وأردنا صنع شيء يجمعهما: ”كنّاسة شوارع بدراجة هوائية“ StreetSweepingBicycle. أو ربّما لدينا صنف المستخدم User وصنف ”مُطلق الأحدث“ EventEmitter حيث فيه خاصية توليد الأحداث، ونريد إضافة ميزة EventEmitter إلى User كي يُطلق المستخدمون الأحداث أيضًا. هناك مفهوم في اللغة يساعد في حلّ هذه وهو ”المخاليط“ (Mixins). كما تقول ويكيبيديا، المخلوط هو صنف يحتوي على توابِع يمكن للأصناف الأخرى استعمالها دون أن ترث ذاك الصنف. أي بعبارة أخرى، يُقدّم المخلوط توابِع فيها وظائف وسلوك معيّن، ولكنّا لا نستعمله لوحده بل نُضيف تلك الوظيفة أو ذاك السلوك إلى الأصناف الأخرى. مثال عن المخاليط أبسط طريقة لكتابة تنفيذ مخلوط في جافاسكربت هو صناعة كائن فيه توابِع تُفيدنا في أمور معيّنة كي ندمجها بسهولة داخل كائن prototype لأيّ صنف كان. لنقل مثلًا لدينا المخلوط sayHiMixin ليُضيف // مخلوط let sayHiMixin = { sayHi() { alert(`Hello ${this.name}`); // مرحبًا يا فلان }, sayBye() { alert(`Bye ${this.name}`); // وداعًا يا فلان } }; // الاستعمال: class User { constructor(name) { this.name = name; } } // ننسخ التوابِع Object.assign(User.prototype, sayHiMixin); // الآن يمكن أن يرحّب المستخدم بغيره new User("Dude").sayHi(); // Hello Dude! مرحبًا يا Dude! ما من وراثة هنا، بل نسخ بسيط للتوابِع. هكذا يمكن أن يرث صنف المستخدم من أيّ صنف آخر وأن يُضيف هذا المخلوط ليدمج فيه توابِع أخرى، هكذا: class User extends Person { // ... } Object.assign(User.prototype, sayHiMixin); يمكن أن تستفيد المخاليط من الوراثة بينها ذاتها المخاليط. فمثلًا، نرى هنا كيف يرث sayHiMixin المخلوطَ sayMixin: let sayMixin = { say(phrase) { alert(phrase); } }; let sayHiMixin = { __proto__: sayMixin, // ‫(أو نستعمل Object.create لضبط كائن prototype هنا) sayHi() { // نستدعي التابِع الأب super.say(`Hello ${this.name}`); // (*) }, sayBye() { super.say(`Bye ${this.name}`); // (*) } }; class User { constructor(name) { this.name = name; } } // ننسخ التوابِع Object.assign(User.prototype, sayHiMixin); // الآن يمكن أن يرحّب المستخدم بالناس new User("Dude").sayHi(); // مرحبًا يا ‫ Dude! لاحظ كيف يبحث استدعاء التابِع الأب super.say()‎ في sayHiMixin (في الأسطر عند (*)) - كيف يبحث عن التابِع داخل كائن prototype المخلوطَ وليس داخل الصنف. إليك صورة (طالع الجانب الأيمن فيها): يعزو ذلك إلى أنّ التابِعين sayHi وsayBye أُنشآ في البداية داخل sayHiMixin. فحتّى حين ننسخهما تُشير خاصية [[HomeObject]] الداخلية فيهما إلى sayHiMixin كما نرى في الصورة. وطالما يبحث super عن التوابِع الأب في [[HomeObject]].[[Prototype]] يكون البحث داخل sayHiMixin.[[Prototype]]‎ وليس داخل User.[[Prototype]]‎. مخلوط الأحداث EventMixin الآن حان وقت استعمال المخاليط في الحياة العملية. يُعدّ توليد الأحداث ميزة مهمّة جدًا للكائنات بشكل عام، وكائنات المتصفّحات بشكل خاص. وتُعدّ هذه الأحداث الطريقة المُثلى ”لنشر المعلومات“ لمن يريدها. لذا هيًا نصنع مخلوطًا يُتيح لنا إضافة دوال أحداث إلى أيّ صنف أو كائن. سيقدّم المخلوط تابِعًا باسم ‎.trigger(name, [...data])‎ ”يُولّد حدثًا“ متى حدث شيء مهم. وسيط الاسم name هو… اسم الحدث، وبعده تأتي بيانات الحدث إن احتجناها. كما والتابِع .on(name, handler)‎ الذي سيُضيف دالة ”معاملة“ handler لتستمع إلى الأحداث حسب الاسم الذي مرّرناه. ستُستدعى الدالة متى تفعّل الحدث بالاسم name وسيُمرّر لها الوسطاء في استدعاء .trigger. وأيضًا… التابِع ‎.off(name, handler)‎ الذي سيُزيل المستمع handler. بعدما نُضيف المخلوط سيقدر كائن المستخدم user على توليد حدث ولوج "login" متى ولج الزائر إلى الموقع. ويمكن لكائن آخر (لنقل التقويم calendar) الاستماع إلى هذا الحدث للقيام بمهمة ما (مثلًا تحميل تقويم المستخدم الذي ولج). أو يمكن أن تُولّد القائمة menu حدثَ الاختيار "select" متى اختار المستخدم عنصرًا منها، ويمكن للكائنات الأخرى إسناد ما تحتاج من …. للاستماع إلى ذلك الحدث. وهكذا. إليك الشيفرة: let eventMixin = { /** *طريقة الانضمام إلى الحدث * menu.on('select', function(item) { ... } */ on(eventName, handler) { if (!this._eventHandlers) this._eventHandlers = {}; if (!this._eventHandlers[eventName]) { this._eventHandlers[eventName] = []; } this._eventHandlers[eventName].push(handler); }, /** * طريقة إلغاء الانضمام إلى الحدث * menu.off('select', handler) */ off(eventName, handler) { let handlers = this._eventHandlers && this._eventHandlers[eventName]; if (!handlers) return; for (let i = 0; i < handlers.length; i++) { if (handlers[i] === handler) { handlers.splice(i--, 1); } } }, /** * توليد حدث من خلال الاسم والبيانات المعطاة * this.trigger('select', data1, data2); */ trigger(eventName, ...args) { if (!this._eventHandlers || !this._eventHandlers[eventName]) { return; // no handlers for that event name } // call the handlers this._eventHandlers[eventName].forEach(handler => handler.apply(this, args)); } }; ‎.on(eventName, handler)‎ -- يضبط الدالة handler لتُشغّل متى ما حدث الحدث بهذا الاسم. تقنيًا تُخزّن خاصية ‎_eventHandlerslrm مصفوفة من دوال المعاملة لكلّ حدث، وتُضيف الدالة إلى القائمة. ‎.off(eventName, handler)‎ -- يحذف الدالّة handler من القائمة. ‎.trigger(eventName, ...args)‎ -- يولّد حدث: كلّ المعاملات تُستدعى من ‎_eventHandlers[eventName]‎ مع قائمة الوسطاء ‎...args. الاستعمال: // أنشئ صنف class Menu { choose(value) { this.trigger("select", value); } } // أضف مخلوط مع التوابع المرتبط به Object.assign(Menu.prototype, eventMixin); let menu = new Menu(); // أضف معالج حدث من أجل أن يستدعى عند الإختيار *!* menu.on("select", value => alert(`Value selected: ${value}`)); */!* // ينطلق الحدث=> يُشغّل المعالج مباشرةً ويعرُض // القيمة المختارة:123 menu.choose("123"); يمكننا الآن أن نحوّل أيّ شيفرة لتتفاعل متى ما اخترنا شيئًا من القائمة بالاستماع إلى الحدث عبر menu.on(...)‎. كما ويُسهّل المخلوط eventMixin من إضافة هذا السلوك (أو ما يشابهه) إلى أيّ صنف نريد دون أن نتدخّل في عمل سلسلة الوراثة. خلاصة مصطلح المخلوط في البرمجة كائنية التوجّه: صنف يحتوي على توابِع نستعملها في أصناف أخرى. بعض اللغات تتيح لنا الوراثة من أكثر من صنف، أمّا جافاسكربت فلا تتيح ذلك، ولكن يمكننا التحايل على الأمر وذلك بنسخ التوابع إلى النموذج الأولي prototype. يمكننا استعمال المخاليط بطريقة لتوسيع الصنف وذلك بإضافة سلوكيات متعددة إليه، مثل: معالج الأحداث كما رأينا في المثال السابق. من الممكن أن تصبح المخاليط نقطة تضارب إذا قاموا بدون قصد بإعادة تعريف توابع الأصناف. لذا عمومًا يجب التفكير مليًا بأسماء المخاليط لتقليل احتمالية حدوث ذلك. ترجمة -وبتصرف- للفصل Mixins من كتاب The JavaScript language
  14. يُتيح لنا المُعامل instanceof (أهو سيرورة من) فحص هل الكائن ينتمي إلى الصنف الفلاني؟ كما يأخذ الوراثة في الحسبان عند الفحص. توجد حالات عديدة يكون فيها هذا الفحص ضروريًا. **سنستعمله هنا لصناعة دالة **، أي دالة تغيّر تعاملها حسب نوع الوسطاء الممرّرة لها. معامل instanceof صياغته هي: obj instanceof Class يُعيد المُعامل قيمة true لو كان الكائن obj ينتمي إلى الصنف Class أو أيّ صنف يرثه. مثال: class Rabbit {} let rabbit = new Rabbit(); // ‫هل هو كائن من الصنف Rabbit؟ alert( rabbit instanceof Rabbit ); // true نعم كما يعمل مع الدوال البانية: // ‫بدل استعمال class function Rabbit() {} alert( new Rabbit() instanceof Rabbit ); // true نعم كما والأصناف المضمّنة مثل المصفوفات Array: let arr = [1, 2, 3]; alert( arr instanceof Array ); // true نعم alert( arr instanceof Object ); // true نعم لاحظ كيف أنّ الكائن arr ينتمي إلى صنف الكائنات Object أيضًا، إذ ترث المصفوفات -عبر prototype- الكائناتَ. عادةً ما يتحقّق المُعامل instanceof من سلسلة prototype عند الفحص. كما يمكننا وضع المنطق الذي نريد لكلّ صنف في التابِع الثابت Symbol.hasInstance. تعمل خوارزمية obj instanceof Class بهذه الطريقة تقريبًا: لو وجدت التابِع الثابت Symbol.hasInstance تستدعيه وينتهي الأمر: Class[Symbol.hasInstance](obj)‎. يُعيد التابِع إمّا true وإمّا false. هكذا نخصّص سلوك المُعامل instanceof. مثال: // ‫ضبط instanceOf للتحقق من الافتراض القائل // ‫بأن كل شيء يملك الخاصية canEat هو حيوان class Animal { static [Symbol.hasInstance](obj) { if (obj.canEat) return true; } } let obj = { canEat: true }; alert(obj instanceof Animal); // true // ‫تستدعى Animal[Symbol.hasInstance](obj)‎ ليس لأغلب الأصناف التابِع Symbol.hasInstance. في هذه الحالة تستعمل المنطق العادي: يفحص obj instanceOf Class لو كان كائن Class.prototype مساويًا لأحد كائنات prototype في سلسلة كائنات prototype للكائن obj. وبعبارة أخرى ، وازن بينهم واحدًا تلو الآخر: obj.__proto__ === Class.prototype? obj.__proto__.__proto__ === Class.prototype? obj.__proto__.__proto__.__proto__ === Class.prototype? ... // ‫لو كانت إجابة أيًا منها true، فتُعيد true // ‫وإلّا متى وصلت نهاية السلسلة أعادت false في المثال أعلاه نرى rabbit.__proto__ === Rabbit.prototype، بذلك تُعطينا الجواب مباشرةً. أمّا لو كنّا في حالة وراثة، فستتوقّف عملية المطابقة عند الخطوة الثانية: class Animal {} class Rabbit extends Animal {} let rabbit = new Rabbit(); alert(rabbit instanceof Animal); // ‏(هنا) نعم // rabbit.__proto__ === Rabbit.prototype // rabbit.__proto__.__proto__ === Animal.prototype (تطابق!) إليك صورة توضّح طريقة موازنة rabbit instanceof Animal مع Animal.prototype: كما وهناك أيضًا التابِع objA.isPrototypeOf(objB)‎، وهو يُعيد true لو كان الكائن objA في مكان داخل سلسلة prototype للكائن objB. يعني أنّنا نستطيع كتابة الفحص هذا obj instanceof Class هكذا: Class.prototype.isPrototypeOf(obj)‎. الأمر مضحك إذ أنّ باني الصنف Class نفسه ليس لديه أيّ كلمة عن هذا الفحص! المهم هو سلسلة prototype وكائن Class.prototype فقط. يمكن أن يؤدّي هذا إلى عواقب مثيرة متى تغيّرت خاصية prototype للكائن بعد إنشائه. طالع: function Rabbit() {} let rabbit = new Rabbit(); // غيّرنا كائن prototype Rabbit.prototype = {}; // ...لم يعد أرنبًا بعد الآن! alert( rabbit instanceof Rabbit ); // false لا التابع Object.prototype.toString للأنواع نعلم بأنّ الكائنات العادية حين تتحوّل إلى سلاسل نصية تصير [object Object]: let obj = {}; alert(obj); // [object Object] alert(obj.toString()); // كما أعلاه يعتمد هذا على طريقة توفيرهم لتنفيذ التابِع toString. ولكن هناك ميزة مخفيّة تجعل هذا التابِع أكثر فائدة بكثير ممّا هو عليه، أن نستعمله على أنّه مُعامل typeof موسّع المزايا، أو بديلًا عن التابِع toString. تبدو غريبة؟ أليس كذلك؟ لنُزل الغموض. حسب المواصفة، فيمكننا استخراج التابِع toString المضمّن من الكائن وتنفيذه في سياق أيّ قيمة أخرى نريد، وسيكون ناتجه حسب تلك القيمة. لو كان عددًا، فسيكون [object Number] لو كان قيمة منطقية، فسيكون [object Boolean] لو كان null: ‏[object Null] لو كان undefined: ‏[object Undefined] لو كانت مصفوفة: [object Array] …إلى آخره (ويمكننا تخصيص ذلك). هيًا نوضّح: // ننسخ التابِع‫ toString إلى متغير ليسهل عملنا let objectToString = Object.prototype.toString; // ما هذا النوع؟ let arr = []; alert( objectToString.call(arr) ); // [object Array] مصفوفة! استعملنا هنا call كما وضّحنا في درس ”المزخرفات والتمرير“ لتنفيذ الدالة objectToString بسياق this=arr. تفحص خوارزمية toString داخليًا قيمة this وتُعيد الناتج الموافق لها. إليك أمثلة أخرى: let s = Object.prototype.toString; alert( s.call(123) ); // [object Number] alert( s.call(null) ); // [object Null] alert( s.call(alert) ); // [object Function] Symbol.toStringTag يمكننا تخصيص سلوك التابِع toString للكائنات باستعمال خاصية الكائنات Symbol.toStringTag الفريدة. مثال: let user = { [Symbol.toStringTag]: "User" // مستخدم }; alert( {}.toString.call(user) ); // [object User] // لاحظ لأغلب الكائنات الخاصّة بالبيئات خاصية مثل هذه. إليك بعض الأمثلة من المتصفّحات مثلًا: // تابِع ‫toStringTag للكائنات والأصناف الخاصّة بالمتصفّحات: alert( window[Symbol.toStringTag]); // window alert( XMLHttpRequest.prototype[Symbol.toStringTag] ); // XMLHttpRequest alert( {}.toString.call(window) ); // [object Window] alert( {}.toString.call(new XMLHttpRequest()) ); // [object XMLHttpRequest] كما نرى فالناتج هو تمامًا ما يقوله Symbol.toStringTag (لو وُجد) بغلاف [object ...‎]. في النهاية نجد أن لدينا "نوع من المنشطات" لا تعمل فقط على الأنواع الأولية للبيانات بل وحتى الكائنات المضمنة في اللغة ويمكننا تخصيصها أيضًا. يمكننا استخدام ‎{}.toString.call بدلًا من instanceof للكائنات المضمنة في اللغة عندما نريد الحصول على نوع البيانات كسلسلة بدلًا من التحقق منها. خلاصة لنلخّص ما نعرف عن التوابِع التي تفحص الأنواع: يعمل على يُعيد typeof الأنواع الأولية سلسلة نصية ‎{}.toString الأنواع الأولية والكائنات المضمّنة والكائنات التي لها Symbol.toStringTag سلسلة نصية instanceof الكائنات true/false كما نرى فعبارة ‎{}.toString هي تقنيًا typeof ولكن ”متقدّمة أكثر“. بل إن التابع instanceof يؤدي دور مميّز عندما نتعامل مع تسلسل هرمي للأصناف ونريد التحقق من صنفٍ ما مع مراعاة الوراثة. تمارين instnaceof غريب عجيب الأهمية: 5 في الشيفرة أسفله، لماذا يُعيد instanceof القيمة true. يتّضح جليًا بأنّ B()‎ لم يُنشِئ a. function A() {} function B() {} A.prototype = B.prototype = {}; let a = new A(); // هنا alert( a instanceof B ); // true الحل أجل، غريب عجيب حقًا. ولكن كما نعرف فلا يكترث المُعامل instanceof بالدالة، بل بكائن prototype لها حيث تُطابقه مع غيره في سلسلة prototype. وهنا نجد a.__proto__ == B.prototype، بذلك يُعيد instanceof القيمة true. إذًا فحسب منطق instanceof، كائن prototype هو الذي يُعرّف النوع وليس الدالة البانية. ترجمة -وبتصرف- للفصل Class checking: "instanceof"‎ من كتاب The JavaScript language table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; }
  15. يمكننا توسيع الأصناف المضمّنة مثل المصفوفات والخرائط وغيرها. فمثلًا يرث صنف PowerArray من المصفوفة Array الأصيلة: // نُضيف تابِعًا آخر إليها (أو أكثر لو أردنا‫) class PowerArray extends Array { isEmpty() { return this.length === 0; } } let arr = new PowerArray(1, 2, 5, 10, 50); alert(arr.isEmpty()); // false let filteredArr = arr.filter(item => item >= 10); alert(filteredArr); // 10, 50 alert(filteredArr.isEmpty()); // false من الجدير بالملاحظة أن التوابع المضمّنة في اللغة مثل: filter و map وغيرهم، تعيد كائنات جديدة من نفس النوع الموروث بالضبط PowerArray. وذلك لأن التطبيق الداخلي للتوابع يستخدم الباني المخصص لتلك الكائنات. في المثال أعلاه: arr.constructor === PowerArray متى استدعينا arr.filter()‎ أنشأ التابِع داخليًا مصفوفةً جديدة من النتائج باستعمال arr.constructor ذاتها، وليس المصفوفة العادية Array. هذا أمر رائع جدًا إذ يمكننا استعمال توابِع PowerArray على النتائج أيضًا. يمكننا حتّى تخصيص هذا السلوك كما نرغب. فنُضيف جالِبًا ثابتًا Symbol.species إلى الصنف. لو كان موجودًا فسيُعيد الباني الذي ستستعمله جافاسكربت داخليًا لإنشاء المدخلات الجديدة في التوابِع map و filter وغيرها. لو أردنا من التوابِع المضمّنة مثل map و filter - لو أردنا أن تُعيد المصفوفات الطبيعية، فعلينا إعادة صنف Array في رمز Symbol.species هكذا: class PowerArray extends Array { isEmpty() { return this.length === 0; } // ستستعمل التوابِع المضمّنة هذا الصنف ليكون بانيًا static get [Symbol.species]() { return Array; } } let arr = new PowerArray(1, 2, 5, 10, 50); alert(arr.isEmpty()); // false // يصنع التابِع ‫ filter مصفوفةً جديدة باستعمال arr.constructor[Symbol.species]‎ بانيًا let filteredArr = arr.filter(item => item >= 10); // نوع ‫ filteredArr ليس PowerArray بل Array alert(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function كما نرى فالآن يُعيد التابِع ‎.filter مصفوفةً Array. بذلك تلك الميزة الموسّعة لن تُمرّر أكثر وأكثر. ملاحظة: هنالك مجموعات أخرى تعمل بنفس الطريقة مثل: Map أو Set ويستخدمون أيضًا Symbol.species. الأنواع المضمّنة لا ترث الثوابت للكائنات المضمّنة أيضًا توابِع ثابتة مثل Object.keys و Array.isArray وغيرها. وكما نعلم فالأصناف الأصيلة تُوسّع نفسها. فمثلًا تُوسّع المصفوفات Array الكائناتَ Object. وعادةً متى وسّع الصنف صنفًا آخر ورث التوابِع الثابتة وغير الثابتة. شرحنا هذا بالتفصيل في المقال ”الخاصيات والتوابع الثابتة“. ولكن الأصناف المضمّنة في اللغة استثناء لهذا، ولا ترث الحقول الثابتة من بعضها البعض. مثال: ترث المصفوفات والتواريخ الكائناتَ، بذلك نرى لسيروراتها توابِع أتت من Object.prototype. ولكنّ كائن Array.[[Prototype]]‎ لا يُشير إلى Array، بذلك لا نرى توابِع ثابتة مثل Array.keys()‎ أوArray.keys()‎ مثلًا. تغني الصورة عن ألف كلمة، إليك واحدة توضّح بنية التواريخ Date والكائنات Object: كما ترى فليس هناك رابط بين التواريخ والكائنات، فهي مستقلة بذاتها. كائن Date.prototype فقط هو من يرث Object.prototype. هذا فرق مهمّ للوراثة بين الكائنات المضمّنة في اللغة وتلك التي نحصل عليها باستعمال extends. ترجمة -وبتصرف- للفصل Extending built-in classes من كتاب The JavaScript language