البحث في الموقع
المحتوى عن 'دليل تعلم جافاسكربت'.
-
لنقل بأنّك أنت هو عبد الحليم حافظ، ولنفترض بأنّ مُعجبوك من المحيط إلى الخليج يسألونك ليلًا نهارًا عن الأغنية الشاعرية التالية. وكي تُريح بالك تعدهم بإرسالها إليهم ما إن تُنشر. فتُعطي مُعجبيك قائمة يملؤون فيها عناوين بريدهم. ومتى ما نشرت الأغنية يستلمها كلّ من في تلك القائمة. ولو حصل مكروه (لا سمح الله) مثل أن شبّت النار والتهمت الأستديو ولم تقدر على نشر الأغنية - لو حصل ذلك فسيعلمون به أيضًا. وعاش الجميع بسعادة وهناء: أنت إذ لا يُزعجك الجميع بالتهديدات والتوعّدات، ومُعجبيك إذ لن تفوتهم أيّة رائعة من روائعك الفنية. إليك ما يشبه الأمور التي نفعلها في الحياة الواقعية - في الحياة البرمجية: ”شيفرة مُنتِجة“ تُنفّذ شيئًا وتأخذ الوقت. مثل الشيفرات التي تُحمّل البيانات عبر الشبكة. هذا أنت، ”المغنّي“. ”شيفرة مُستهلِكة“ تطلب ناتج ”الشيفرة المُنتِجة“ ما إن يجهز. وهناك عديد من الدوال تحتاج إلى هذا الناتج. هذه ”مُعجبوك“. الوعد (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 ستعالج الناتج كما ترى القيمة value المعادة من الوعد تُمرر عبر finally إلى then، وهذا السلوك مفيد جدًا إذ لا يفترض بأن تتعامل finally مع ناتج الوعد، بل تمرّره إلى من يتعامل معه، وهي مخصصة إلى إجراء عمليات ختامية معينة (مثل التنظيف) بغض النظر عن الناتج. وهنا واجه الوعد خطأً، وتمرّر من finally إلى catch: new Promise((resolve, reject) => { throw new Error("error"); }) .finally(() => alert("Promise ready")) .catch(err => alert(err)); // <-- .catch ستعالج كائن الخطأ error object ثالثًا، معالج finally لا يجب أن يعيد شيئًا، وإن فعل، فسيجري تجاهل القيمة المعادة، والاستثناء الوحيد هنا هو عندما يرمي معالج finally خطأً ويُمرر آنذاك الخطأ إلى المعالج التالي بغض النظر عما سبقه من مخرجات. الخلاصة، المعالج finally: لا يأخذ أي مخرجات من المعالج السابق، فليس لديه معاملات، بل تتخطاه المخرجات إلى المعالج التالي المناسب إن وُجد. لا يجب أن يعيد شيئًا وإن أعاد فلن يحصل شيء وتُتجاهل القيمة. إن رمى خطأً، فسيستلمه أقرب معالج أخطاء. ملاحظة: في الوعود المنجزة المُعالجات تعمل مباشرة إن كان الوعد مُعلقًا لسببٍ ما، فإن معالجات .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
-
يمكن أن يحتوي المتغير على أي نوع من أنواع البيانات في JavaScript. أي من الممكن أن يكون متغير من نوع سلسلة نصية في وقت ما، ثم يتغير محتواه إلى قيمة عددية وهكذا دواليك. // لا يوجد أي خطأ let message = "hello"; message = 123456; تسمى اللغات البرمجية التي تسمح بتغيير نوع القيم المسندة إلى المتغير بلغات برمجة «ديناميكية النوع» (dynamically typed)، ومعنى ذلك أنه توجد أنواع للبيانات ولكن لا يتم ربط المتغير بنوع معين منها. هنالك سبعة أنواع أساسية في لغة JavaScript. سنذكرها الآن بشكل عام، وسنتحدث في الفصول القادمة عن كل نوع منها بالتفصيل. الأعداد let n=123; n=12.345; يمكن تمثيل الأعداد، بما فيها الأعداد الصحيحة (integers) والعشرية (floating point)، في JavaScript عبر النوع number. هنالك العديد من العمليات التي يمكن تنفيذها على المتغيرات العددية، مثل، الضرب *، والقسمة /، والجمع +، والطرح -، وغيرها من العمليات الرياضية. كما أن هناك «قيم عددية خاصة»، بالإضافة إلى الأعداد العادية، والتي تنتمي أيضًا إلى هذا النوع من البيانات مثل: Infinity، و -Infinity، و NaN. وهاك شرحها: Infinity: وتمثل قيمة اللانهاية الرياضية، وهي قيمة خاصة أكبر من أي عدد موجب آخر. ومن الممكن أن نحصل عليها في عدة حالات منها قسمة عدد على الصفر: alert (1/0); // Infinity أو بالإمكان الإشارة إليها بشكل مباشر: alert (Infinity); // Infinity NaN: هذه القيمة ناتجة عن اختصارالعبارة "Not a Number" (ليس عددًا) وتمثل خطأً حسابيًّا، أو حالة عدم تعيين. وهي نتيجة عملية رياضية خاطئة أو غير معروفة. إليك المثال التالي: alert( "not a number"/2 ); // NaN, لا يمكن إجراء مثل عملية القسمة هذه وتتصف القيمة NaN بأنها «لاصقة» (sticky)، أي بمعنى عندما ُتنفَّذ أي عملية على NaN، فالقيمة الناتجة هي NaN أيضًا. alert ("not a number" / 2+5); // NaN لذلك عند وجود القيمة NaN في أي مكان من التعابير الرياضية، فستطغى على كامل النتيحة. وتنتمي القيم العددية الخاصة شكليًا فقط إلى نوع القيم العددية، لأنها في واقع الأمر لا تعبر عن أعداد بمفهومها الشائع. سنتكلم لاحقًا عن التعامل مع الأعداد في فصل (الأعداد). العمليات الرياضية «آمنة» ممارسة الرياضيات آمنة في JavaScript، وبإمكانك القيام بأي عملية حسابية، مثل: القسمة على صفر، والتعامل مع السلاسل الغير عددية كأعداد، وغيرها من العمليات. ثق تمامًا أن السكربت لن ينتهي بخطأ فادح (أو يتوقف عن العمل). أسوأ ما في الأمر أنك ستحصل على النتيجة NaN. السلاسل النصية (النصوص) تمثَّل النصوص (سنطلق عليها «سلاسل نصية» من الآن وصاعدًا) عبر النوع string. يجب أن تُحاط السلسلة النصية في JavaScript بإشارتي تنصيص. let str = "Hello"; let str2 = 'Single quotes are ok too'; let phrase = `can embed ${str}`; يوجد في لغة JavaScript ثلاثة أنواع من إشارات التنصيص وهي: المزدوجة: مثل "Hello". المفردة: مثل 'Hello'. المائلة: مثل Hello (إضافة الإشارة). إشارتي التنصيص المفرد والمزدوج هما عبارة عن إشارات تنصيص تتصف بأنها «بسيطة» ولا يوجد أي فرق بينها في JavaScript. أما إشارة التنصيص المائلة، فهي عبارة عن إشارات تنصيص لها «وظيفة إضافية»، إذ تسمح لك بإضافة متغيرات وتعابير إلى السلسلة النصية بعد إحاطتها بـ ${...}. فمثلًا: let name = "أحمد"; // تضمين متغير alert (`مرحبًا، ${name}!`); // مرحبًا، أحمد! // تضمين تعبير alert (`the result is ${1+2}`); // النتيجة هي 3 يُحسب التعبير داخل ${...} وتصبح نتيجته جزءًا من السلسلة النصية. بإمكاننا وضع أي شيء هناك مثل: المتغير name أو التعبير الرياضي 1 + 2 أو حتى تعابير برمجية أخرى أكثر تعقيدًا. ولكن أبقِ في ذهنك أن هذا مقتصر فقط على إشارات التنصيص المائلة. ولا تستطيع إشارات التنصيص الأخرى القيام بمثل هذا العمل! alert (" the result is ${1+2}" ); // ${1+2} النتيجة هي // فلا تفعل إشارات التنصيص المزدوجة أي شيء إذا كنت تشعر بصعوبة الأمر، لا تقلق. سنتحدث عنه لاحقًا بشكل موسع في فصل السلاسل النصية. المحارف ليس لها نوع مخصص للأسف يوجد في بعض اللغات البرمجية نوعًا خاصًّا من البيانات للمحرف المفرد (character) ويسمى char في لغة C و Java مثلًا. لا يوجد مثل هذا النوع في JavaScript. ويوجد فقط نوع واحد وهو string (سلسلة نصية) الذي من الممكن أن يحتوي على محرف واحد أو أكثر. النوع المنطقي (البولياني) يأخذ النوع boolean إحدى القيمتين: true (صح) أو false (خطأ) وتدعى هاتان القيمتان بالقيم المنطقية. من الشائع استخدام هذا النوع لحفظ البيانات التي لها القيم (نعم / لا): true تعني "نعم، صحيح"، و false تعني "لا، خطأ". إليك المثال التالي: let nameFieldChecked = true; // name نعم، جرى تحديد الحقل let ageFieldChecked = false; // غير محدَّد age لا، الحقل أضف إلى ذلك أن القيم المنطقية تعبِّر عن ناتج عمليات الموازنة: let isGraeter = 4>1; alert(isGreater); // true إذ نتيجة الموازنة محقَّقة أي القيمة الخالية: null لا تنتمي القيمة الخالية null إلى أي نوع من أنواع البيانات المذكورة سابقًا ولكن لها نوع خاص. يملك هذا النوع الخاص قيمة واحدة هي null: let age = null; في لغة JavaScript، لا يعبِّر النوع null عن مرجع لكائن غير موجود أو مؤشر خالي كما في لغات برمجية أخرى. ولكنه عبارة عن قيمة خاصة تمثِّل "لا شيء"، أو "فارغ"، أو "قيمة غير معلومة". فمثلًا، الشيفرة البرمجية السابقة تعدُّ المتغير age غير معلوم أو فارغ لسبب ما. القيمة الغير معرفة: undefined يمكن تمييز نوع آخر أيضًا للبيانات وهو «غير مُعرَّف» الذي يمثله النوع undefined. ويشكل هذا النوع نوعًا خاصًّا قائمًا بنفسه تمامًا مثل النوع null. أما معنى «غير معرّف» أي أنه لم تُسنَد أية قيمة للمتغير بعد: let x; alert (x); // "undefined" تُعرَض القيمة تقنيًا، من الممكن إسناد القيمة undefined لأي متغير: let x=123; x = undefined; alert(x); // "undefined" ولكن لا أنصحك بالقيام بذلك. نستخدم عادةً القيمة null لإسناد القيمة الخالية أو غير المعروفة لمتغير، ونستخدم undefined للتحقق من أن إذا كان للمتغير قيمة أو لا. الكائنات والرموز النوع object (كائن) هو نوعٌ خاصٌّ. تسمى جميع الأنواع السابقة بالأنواع «الأساسية» (primitive) لأن قيمها تحتوي على شيء واحد فقط (سلسلة نصية أو عدد أو أي شيء آخر). ولكن تُستخدَم الكائنات لتخزين مجموعة من البيانات والكيانات (entity) الأكثر تعقيدًا. سنتعامل معها في فصل الكائنات بعد الانتهاء من دراسة الأنواع الأساسية. يستخدم النوع symbol (رمز) لتشكيل معرّف (identifier) مُميَّز للكائنات. يُفضَّل دراسة هذا النوع بعد الانتهاء من الكائنات ولكن ذكره هنا ضروري لاستكمال جميع أنواع البيانات. المعامل typeof يُحدِّد المعامل typeof نوع الشيء المُمرَّر إليه. ويكون مفيدًا عندما تحتاج إلى معالجة القيم بطرق مختلفة حسب نوعها، أو فقط القيام بفحص سريع لنوع البيانات. ويمكن كتابة الصياغة بطريقتين: معامل: typeof x. تابع: typeof(x). بمعنى آخر، من الممكن استخدامه مع أو بدون الأقواس، وتكون النتيجة واحدة. نتيجة استدعاء typeof x هو سلسلة نصية تمثل اسم النوع: typeof undefined // "undefined" typeof 0 // "number" typeof true // "boolean" typeof "foo" // "string" typeof Symbol("id") // "symbol" typeof Math // "object" (1) typeof null // "object" (2) typeof alert // "function" (3) قد تحتاج لتوضيح نتائج الأسطر الثلاث الأخيرة: Math هو كائن مضمّن في JavaScript ويوفر العمليات الرياضية. سنتحدث عنه لاحقًا في فصل الأعداد، وذكرناه هنا فقط كمثال عن نوع الكائن. نتيجة typeof null هي "object" أي كائن. أليس هذا خطأ؟! بالتأكيد. وقد تم الاعتراف بذلك رسميًا، ولكن بقيت هذه النتيجة من أجل المحافظة على التوافقية. القيمة "null" ليست كائنًا بل هي قيمة لنوع منفصل بحد ذاته. لذلك، مرةً أخرى، نؤكد أنّ هذا خطأ من اللغة نفسها. نتيجة typeof alert هي "function" (دالة)، لأن alert هو دالة في لغة JavaScript. سنتحدث لاحقًا عن التوابع في الفصول القادمة وستلاحظ أنه لا يوجد نوع خاص اسمه "function" في لغة JavaScript. تنتمي التوابع إلى نوع الكائنات ولكن لها معاملة خاصة في typeof. شكليًا، هذا خطأ، ولكنه مفيد جدًا عند التطبيق العملي. الخلاصة هناك سبعة أنواع أساسية من أنواع البيانات في JavaScript هي: number: يمثِّل الأعداد بكل أنوعها: الصحيحة والعشرية. string: يمثِّل السلاسل النصية. ويمكن أن تحتوي السلسلة النصية على محرف واحد أو أكثر، ولكن لا يوجد نوع خاص من أجل المحرف الواحد. boolean: يمثِّل القيم المنطقية (صح / خطأ). null: يمثِّل القيم الخاوية. وهو نوع خاص يملك قيمة وحيدة وهي القيمة null. undefined: قيمة غير معرَّفة تكون قيمةً للمتغيرات التي لم تسند قيمة محدَّدة لها بعد. وهو نوع خاص يملك قيمة وحيدة وهي undefined. object: كائن من أجل بنى البيانات الأكثر تعقيدًا. symbol: رمز للمعرفات الخاصة. ويسمح المعامل typeof بمعرفة أنوع القيم المخزنة في المتغيرات: له شكلين: typeof x أو typeof(x). يعيد سلسلة نصية تحوي اسم النوع، مثل "string". من أجل null يعطي القيمة "object" وهذا خطأ في اللغة، فالقيمة الخالية ليست من نوع الكائن. سنركز في الفصل القادم على القيم الأساسية ثم ننتقل إلى الحديث عن الكائنات. .task__importance { color: #999; margin-left: 30px; } .task__answer { border: 3px solid #f7f6ea; margin: 20px 0 14px; position: relative; display: block; padding: 25px 30px; } code { background-color: rgb(250, 250, 250); border-radius: 3px; } تمارين 1. إشارات تنصيص السلاسل النصية الأهمية: 5 ما هو خرج هذا السكربت؟ let name " Ilya"; alert (`hello ${1}`); //? alert (`hello ${"name"}`); //? alert (`hello ${name}`); //? الحل تُضمن الفاصلة العليا الخلفية التعابير الموجودة داخل ${…} في السلسلة النصية: let name " Ilya"; // قيمة التعبير هو 1 alert (`hello ${1}`); // hello1 // "name" قيمة التعبير هي السلسلة النصية alert (`hello ${"name"}`); // hello name // name قيمة التعبير هو ما ضُمِّن بالمتغير alert (`hello ${name}`); // hello Ilya ترجمة -وبتصرف- للفصل Data types من كتاب The JavaScript Language انظر أيضًا المقال التالي: التحويل بين الأنواع المقال السابق: المتغيرات كامل مقالات دليل تعلم جافاسكربت
-
قبل كتابة شيفرات برمجية أكثر تعقيدا، لنتطرَّق إلى تنقيح الأخطاء. تنقيح الأخطاء هي عملية إيجاد الأخطاء في السكريبت وإصلاحها. تدعم جميع المتصفحات الحديثة وبعض البيئات الأخرى "تنقيح الأخطاء" -واجهة مستخدم خاصة في أدوات المطور والتي تجعل العثور على الأخطاء وتصحيحها أمرا سهلا. تُتيح هذه الواجهة أيضا تَتَبُّع الأكواد خطوة بخطوة لمعرفة ما يحدث فيها بالتفصيل. سنستخدم Chrome لأن لديه ميزات كافية لذلك، كما تتوفر هذه الميزات في معظم المتصفحات الأخرى. جزء الموارد "sources" قد يبدو إصدار Chrome لديك مختلفًا بعض الشيء، إلا أن المحتوى هو ذاته. افتح صفحة example في Chrome. شغِّل أدوات المطور بالضغط على F12 (على أجهزة Mac، استعمل الاختصار Cmd+Opt+I). اختر الجزء sources. إن كانت هذه هي مرتك الأولى للدخول إلى جزء sources، فهذا ما ستراه: يفتح هذا الزر علامة تبويب تحوي الملفات. اضغط عليها واختر hello.js من العرض الشجري "tree view". يجب أن ترى التالي: هنا يمكننا رؤية ثلاث مناطق: منطقة الموارد "Resources zone" والتي تعرض ملفات HTML، و JavaScritp، و CSS، وغيرها من الملفات بما في ذلك الصور المُلحقة بالصفحة. قد تظهر إضافات Chrome هنا أيضًا. منطقة المصدَر "Source zone" والتي تعرض الشيفرة البرمجية المصدرية. منطقة المعلومات والتحكم "Information and control zone" لتنقيح الأخطاء، سَنكتشفها أكثر فيما يلي يمكنك الضغط على الزر مُجددًا لِإخفاء قائمة الموارد وإعطاء الشيفرة البرمجية مساحة كافية. شاشة التحكم (Console) تظهر شاشة تحكم عند الضغط على Esc. يمكن كتابة الأوامر في شاشة التحكم ثم الضغط على Enter لتنفيذها. تظهر مخرجات تنفيذ أمر ما أسفله مباشرة في شاشة التحكم. مثال: كما في الصورة بالأسفل 1+2 ينتج 3، بينما hello("debugger") لا يُظهِر أي نتائج، لذلك فإننا نرى undefined: نقاط التوقف (Breakpoints) لنختبر ما يحدُث أثناء تنفيذ الشيفرة البرمجية في صفحة example. في الصفحة hello.js، اضغط على السطر رقم 4؛ نضغط على الرقم ذاته وليس السطر. هكذا تكون قد أنشأت نقطة توقف. اضغط على الرقم 8 أيضًا. يجب أن يبدو الشكل كما في الصورة التالية: نقطة التوقف هي نقطة يتوقف فيها مصحح الأخطاء عن تنفيذ JavaScript تلقائيًا. يمكننا فحص المتغيرات الحالية وتنفيذ الأوامر أو أي شيء آخر في لوحة التحكم أثناء توقف عمل الشيفرة البرمجية. أي أنه يمكننا تتبع الشيفرة البرمجية عند نقطة معينة عبر ايقافها والتأكد من أي شيء فيها كما نريد. يمكننا رؤية قائمة بالعديد من نقاط التوقف في الجزء الأيمن من الشاشة. يكون الأمر مفيدًا عند وجود عدة نقاط توقف في أكثر من ملف، وذلك يتيح لنا: التنقل بسرعة إلى نقاط التوقف في الشيفرة البرمجية (بالضغط عليها من الجزء الأيمن). إلغاء تفعيل نقاط التوقف مؤقتا بإلغاء تحديدها. حذف نقطة التوقف بالضغط عليها باليمين واختيار حذف "remove". وهكذا … نقاط التوقف الشرطية يتيح لك الضغط يمينَا على رقم السطر إنشاء نقطة توقف شرطية تُنَفَّذ عند تحقق الشرط المحدد فقط. يكون ذلك مفيدَا عندما تريد إيقاف التنفيذ لمعرفة قيمة أي متغير أو قيمة أي معامل في دالة. أمر Debugger يمكن أيضا إيقاف تنفيذ الشيفرة البرمجية بوضع الأمر debugger فيه كما يلي: function hello(name) { let phrase = `Hello, ${name}!`; debugger; // <-- يتوقف المنقح هنا say(phrase); } هذه الطريقة سهلة عندما نُعدِّل الشيفرة البرمجية باستخدم محرر الشيفرات البرمجية ولا نريد الانتقال إلى المتصفح وتشغيل السكربت في وضع أدوات المطور لإنشاء نقاط توقف. توقف وتحقق في المثال، يتم استدعاء الدالة hello() أثناء تحميل الصفحة، لذلك فإن أسهل طريقة لتفعيل مُنقِّح الأخطاء (بعد إعداد نقاط التوقف) هي إعادة تحميل الصفحة. اضغط على F5 (لمستخدمي ويندوز أو لينكس)، أو اضغط على Cmd+R (لمستخدمي Mac). سيتوقف تنفيذ الشيفرة البرمجية في السطر الرابع حيث تم إنشاء نقطة التوقف: افتح قوائم المعلومات المنسدلة على اليمين (موضحة بأسهم). تتيح هذه القوائم التحقق من حالة السكريبت الحالية: 1- Watch - تعرض القيم الحالية لأي تعابير. يمكنك الضغط على + وإدخال أي تعبير تريده. سيعرض المعالج قيمته في أي وقت وحساب قيمته تلقائيا أثناء التنفيذ. 2- Call Stack - تعرض سلسلة الاستدعاءات المتداخلة. في الوقت الحالي، المعالج وصل حتى استدعاء الدالة hello()، المُستدعاة من خلال السكريبت index.html (لا يوجد دوال أخرى لذلك سُمِّيَت "anonymous"). إن ضَغَطت على عنصر من الحزمة (stack) مثلا "anonymous"، فسينتقل المعالج مباشرة إلى الشيفرة البرمجية المُمَثِّل لهذا العنصر وستتمكن من فحص جميع متغيراته أيضا. 3- Scope - تعرض المتغيرات الحالية. Local تعرض متغيرات الدالة المحلية. يمكنك أيضا رؤية قيم هذه المتغيرات موضحة على اليمين. Global تعرض المتغيرات الخارجية (خارج نطاق أي دالة). يوجد أيضا الكلمة المفتاحية this والتي لم تُشرح بعد، لكن سيتم شرحها قريبا. تتبع التنفيذ يوجد بعض الأزرار لِتتبع التنفيذ أعلى يمين اللوحة. لِنتطرق إليها. - زر استمرار التنفيذ F8. يستأنف التنفيذ. إن لم توجد أي نقاط توقف، فإن التنفيذ سيستمر بعد الضغط على هذا الزر وسَيفقد مصحح الأخطاء السيطرة على السكريبت. هذا ما سنراه بعد الضغط عليه: تم استئناف التنفيذ، ووصل لنقطة توقف أخرى داخل الدالة say() وتوقف هناك. انظر في Call stack على اليمين. تم تنفيذ استدعاء آخر. وصل التنفيذ الآن حتى الدالة say(). - نفِّذ خطوة (نفّذ الأمر التالي)، لكن لا تنتقل إلى الدالة الأخرى، الوصول السريع F10. سيظهر alert إذا ضغطنا على هذا الزر الآن. الأمر المهم هنا هو أن alert قد يحوي على أي دالة، لذلك فإن التنفيذ سيتخطاها. - نفِّذ خطوة، الوصول السريع F11. يقوم بنفس آلية عمل الأمر السابق بالإضافة إلى تنفيذ الدوال المتداخلة. سَتتنفذ أوامر السكريبت بالضغط عليه خطوة بخطوة. - استمر بالتنفيذ حتى نهاية الدالة الحالية، باستخدام الزر Shift+F11. سيتوقف التنفيذ عند آخر سطر للدالة الحالية. يكون هذا الأمر مفيدا عند الدخول إلى استدعاء دالة مصادفةَ باستخدام الزر ، لكن تتبع هذه الدالة ليس أمرا مهما، لذلك نقوم بتخطي تتبعها. - تفعيل/تعطيل جميع نقاط التوقف. لا يقوم هذا الزر بمتابعة التنفيذ، إنما يُفَعِّل/يُعطِّل كمَا كبيرا من نقاط التوقف. - تفعيل/تعطيل التوقف التلقائي في حال حدوث خطأ. عند تفعيله في وضع أدوات المطور، فإن الشيفرة البرمجية تتوقف عن التنفيذ تلقائيا عند أي خطأ في السكريبت. ثم يمكننا تحليل المتغيرات لمعرفة سبب الخطأ. لذلك، إن أوقف خطأ مَا متابعة تنفيذ الشيفرة البرمجية، يمكننا فتح مصحح الأخطاء وتفعيل هذا الخيار وإعادة تحميل الصفحة لرؤية مكان توقف الشيفرة البرمجية وما هو المحتوى عند تلك النقطة. الاستمرار حتى هنا "Continue to here" الضغط يمينا على سطر من الشيفرة البرمجية يفتح قائمة السياق المحتوية على خيار مفيد يُدعى "Continue to here". يكون هذا الخيار مُفيدا عندما نريد التقدم بضع خطوات للأمام بدون وضع نقطة توقف. التسجيل (Logging) يمكن استخدام الدالة console.log لِعرض شيء على الشاشة كمُخرج من الشيفرة البرمجية. مثلا، يعرض الأمر التالي القيم من 0 حتى 4 إلى الشاشة: // open console to see for (let i = 0; i < 5; i++) { console.log("значение", i); } لا يرى المستخدم العادي هذه المخرجات. لرؤيتها، افتح علامة التبويب Console أو اضغط على Esc إن كنت في علامة تبويب أخرى هكذا تُفتًح الشاشة في الأسفل. إن كانت الشيفرة البرمجية تحتوي على أوامر console.log، فسنرى ما يحدث من خلال سجلات التتبع بدون الدخول إلى المُصحح. الخلاصة كما رأينا، فإن هناك ثلاث طرائق رئيسية لإيقاف السكريبت: باستخدام نقطة توقف. الأمر debugger. وجود خطأ (في حال كانت أداوت المطور مفتوحة وكان الزر مُفَعًّلَأ). عند توقف السكريبت، يمكننا فحص المتغيرات وتتبع الشيفرة البرمجية لرؤية أي أخطاء. يوجد العديد من الخيارات الأخرى في أدوات المطور أكثر مما تم شرحه سابقا. تجد الدليل كاملا على https://developers.google.com/web/tools/chrome-devtools. تُعَد المعلومات التي وُضِعَت كافية لبدء تتبع أي شيفرة برمجية وتنقيحها، لكنك ستحتاج للاطلاع على الدليل لاحقا لتعلم ميزات متقدمة في أدوات المطور، خاصة إن كنت تتعامل كثيرا مع المتصفح. يمكنك الضغط على عدة أماكن في أدوات المطور ورؤية ما يحدث. تُعد هذه أفضل طريقة لتعلم أدوات المطور. لا تنسَ الضغط يمينا وتجريب قوائم السياق. ترجمة -وبتصرف- للفصل Debugging in Chrome من كتاب The JavaScript Language. .task__importance { color: #999; margin-left: 30px; } .task__answer { border: 3px solid #f7f6ea; margin: 20px 0 14px; position: relative; display: block; padding: 25px 30px; } code { background-color: rgb(250, 250, 250); border-radius: 3px; } اقرأ أيضًا المقال التالي: نمط كتابة الشيفرة المقال السابق: مراجعة لما سبق
-
نُنشِئ الكائنات باستخدام الصيغة الاعتيادية المختصرة {...}. لكننا نحتاج لإنشاء العديد من الكائنات المتشابهة غالبًا، مثل العديد من المستخدمين، أو عناصر لقائمة وهكذا. يمكن القيام بذلك باستخدام الدوال البانية (constructor functions) لكائن والمُعامِل "new". الدالة البانية تقنيًا، الدوال البانية هي دوال عادية، لكن يوجد فكرتين متفق عليها: أنها تبدأ بأحرف كبيرة. يجب تنفيذها مع المُعامِل "new" فقط. إليك المثال التالي: function User(name) { this.name = name; this.isAdmin = false; } let user = new User("Jack"); alert(user.name); // Jack alert(user.isAdmin); // false عند تنفيذ دالة مع الُعامِل new، تُنَفَّذ الخطوات التالية: يُنشَأ كائن فارغ ويُسنَد إلى this. يُنَفَّذ محتوى الدالة. تقوم غالبًا بتعديل this، وإضافة خاصيات إليه. تُرجَع قيمة this. بمعنى آخر، تقوم new User(...) بشيء يشبه ما يلي: function User(name) { // this = {}; (implicitly) // this إضافة خاصيات إلى this.name = name; this.isAdmin = false; // this; (implicitly) إرجاع } إذًا، تُعطي let user = new User("Jack") النتيجة التالية ذاتها: let user = { name: "Jack", isAdmin: false }; الآن، إن أردنا إنشاء مستخدمين آخرين، يمكننا استدعاء new User("Ann")، و new User("Alice") وهكذا. تعدُّ هذه الطريقة في بناء الكائنات أقصر من الطريقة الاعتيادية عبر الأقواس فقط، وأيضًا أسهل للقراءة. هذا هو الغرض الرئيسي للبانيات، وهي تطبيق شيفرة قابلة لإعادة الاستخدام لإنشاء الكائنات. لاحظ أنَّه يمكن استخدام أي دالة لتكون دالة بانية تقنيًا. يعني أنه يمكن تنفيذ أي دالة مع new، وستُنَفَّذ باستخدام الخوارزمية أعلاه. استخدام الأحرف الكبيرة في البداية هو اتفاق شائع لتمييز الدالة البانية من غيرها وأنَّه يجب استدعاؤها مع new. new function() { … } إن كان لدينا العديد من الأسطر البرمجية، وجميعها عن إنشاء كائن واحد مُعَقَّد، فبإمكاننا تضمينها في دالة بانية، هكذا: let user = new function() { this.name = "John"; this.isAdmin = false; // ...شيفرة إضافية لإنشاء مستخدم // ربما منطق معقد أو أي جمل // متغيرات محلية وهكذا.. }; لا يمكن استدعاء المُنشِئ مجددًا، لأنه غير محفوظ في أي مكان، يُنشَأ ويُستدعى فقط. لذا فإن الخدعة تهدف لتضمين الشيفرة التي تُنشِئ كائنًا واحدًا، دون إعادة الاستخدام وتكرار العملية مستقبلًا. وضع اختبار الباني: new.target ميزة متقدمة: تُستخدم الصيغة في هذا الجزء نادرًا، ويمكنك تخطيها إلا إن كنت تُريد الإلمام بكل شيء. يمكننا فحص ما إن كانت الدالة قد استدعيت باستخدام new أو دونه من داخل الدالة، وذلك باستخدام الخاصية الخاصة new.target. تكون الخاصية فارغة في الاستدعاءات العادية، وتساوي الدالة البانية إذا استُدعِيَت باستخدام new: function User() { alert(new.target); } // "new" بدون: User(); // undefined // باستخدام "new": new User(); // function User { ... } يمكن استخدام ذلك بداخل الدالة لمعرفة إن استُدعِيَت مع new، "في وضع بناء كائن"، أو بدونه "في الوضع العادي". يمكننا أيضًا جعل كلًا من الاستدعاء العادي و new ينفِّذان الأمر ذاته -بناء كائن- هكذا: function User(name) { if (!new.target) { // new إن كنت تعمل بدون return new User(name); // new ...سأضيف } this.name = name; } let john = User("John"); // new User تُوَجِّه الاستدعاء إلى alert(john.name); // John يستخدم هذا الاسلوب في بعض المكتبات أحيانًا لجعل الصيغة أكثر مرونة حتى يتمكن الأشخاص من استدعاء الدالة مع new أو بدونه، وتظل تعمل. ربما ليس من الجيد استخدام ذلك في كل مكان، لأن حذف new يجعل ما يحدث أقل وضوحًا. لكن مع new، يعلم الجميع أن كائنًا جديدًا قد أُنشِئ. ما تُرجِعه الدوال البانية لا تملك الدوال البانية عادةً التعليمة return. فَمُهِمَتُهَا هي كتابة الأمور المهمة إلى this، وتصبح تلقائيًا هي النتيجة. لكن إن كان هناك التعليمة return فإن القاعدة بسيطة: إن استُدعِيَت return مع كائن، يُرجَع الكائن بدلًا من this. إن استُدعِيَت return مع متغير أولي، يُتَجاهَل. بمعنىً آخر، return مع كائن يُرجَع الكائن، وفي الحالات الأخرى تُرجَع this. مثلًا، يعاد في المثال التالي الكائن المرفق بعد return ويهمل الكائن المسنَد إلى this: function BigUser() { this.name = "John"; return { name: "Godzilla" }; // <-- تُرجِع هذا الكائن } alert( new BigUser().name ); // Godzilla, حصلنا على الكائن وهنا مثال على استعمال return فارغة (أو يمكننا وضع متغير أولي بعدها، لا فرق): function SmallUser() { this.name = "John"; return; // ← this تُرجِع } alert( new SmallUser().name ); // John لا تحوي الدوال البانية غالبًا على تعليمة الإعادة return. نذكر هنا هذا التصرف الخاص عند إرجاع الكائنات بغرض شمول جميع النواحي. حذف الاقواس بالمناسبة، يمكننا حذف أقواس new في حال غياب المعاملات مُعامِلات: let user = new User; // <-- لا يوجد أقوس // الغرض ذاته let user = new User(); لا يُعد حذف الأقواس أسلوبًا جيدَا، لكن الصيغة مسموح بها من خلال المواصفات. الدوال في الباني استخدام الدوال البانية لإنشاء الكائنات يُعطي مرونة كبيرة. قد تحوي الدالة البانية على مُعامِلات ترشد في بناء الكائن ووضعه، إذ يمكننا إضافة خاصيات ودوال إلى this بالطبع. مثلًا، تُنشِئ new User(name) في الأسفل كائنًا بالاسم المُعطَى name والدالة sayHi: function User(name) { this.name = name; this.sayHi = function() { alert( "My name is: " + this.name ); }; } let john = new User("John"); john.sayHi(); // My name is: John /* john = { name: "John", sayHi: function() { ... } } */ لإنشاء كائنات أكثر تعقيدًا، يوجد صيغة أكثر تقدمًا، الفئات، والتي سنغطيها لاحقًا. الخلاصة الدوال البانية، أو باختصار البانيات، هي دوال عادية، لكن يوجد اتفاق متعارف عليه ببدء اسمها بحرف كبير. يجب استدعاء الدوال البانية باستخدام new فقط. يتضمن هذا الاستدعاء إنشاء كائن فارغ وإسناده إلى this وبدء العملية ثم إرجاع هذا الكائن في نهاية المطاف. يمكننا استخدام الدوال البانية لإنشاء كائنات متعددة متشابهة. تزود JavaScript دوالًا بانية للعديد من الأنواع (الكائنات) المدمجة في اللغة: مثل النوع Date للتواريخ، و Set للمجموعات وغيرها من الكائنات التي نخطط لدراستها. عودة قريبة غطينا الأساسيات فقط عن الكائنات وبانياتها في هذا الفصل. هذه الأساسيات مهمة تمهيدًا لتعلم المزيد عن أنواع البيانات والدوال في الفصل التالي. بعد تعلم ذلك، سنعود للكائنات ونغطيها بعمق في فصل الخصائص، والوراثة، والأصناف. تمارين دالتين - كائن واحد الأهمية: 2 هل يمكن إنشاء الدالة A و B هكذا new A()==new B()؟ function A() { ... } function B() { ... } let a = new A; let b = new B; alert( a == b ); // true إن كان ممكنًا، وضح ذلك بمثال برمجي. الحل نعم يمكن ذلك. إن كان هناك دالة تُرجِع كائنًا فإن new تُرجِعه بدلًا من this. لذا فمن الممكن، مثلًا، إرجاع الكائن المعرف خارجيًا obj: let obj = {}; function A() { return obj; } function B() { return obj; } alert( new A() == new B() ); // true إنشاء حاسبة جديدة الأهمية: 5 إنشِئ دالة بانية باسم Calculator تنشئ كائنًا بثلاث دوال: read() تطلب قيمتين باستخدام سطر الأوامر وتحفظها في خاصيات الكائن. sum() تُرجِع مجموع الخاصيتين. mul() تُرجِع حاصل ضرب الخاصيتين. مثلًا: let calculator = new Calculator(); calculator.read(); alert( "Sum=" + calculator.sum() ); alert( "Mul=" + calculator.mul() ); الحل function Calculator() { this.read = function() { this.a = +prompt('a?', 0); this.b = +prompt('b?', 0); }; this.sum = function() { return this.a + this.b; }; this.mul = function() { return this.a * this.b; }; } let calculator = new Calculator(); calculator.read(); alert( "Sum=" + calculator.sum() ); alert( "Mul=" + calculator.mul() ); إنشاء مجمِّع الأهمية: 5 انشِئ دالة بانية باسم Accumulator(startingValue)، إذ يجب أن يتصف هذا الكائن بأنَّه: يخزن القيمة الحالية في الخاصية value. تُعَيَّن قيمة البدء عبر المعامل startingValue المعطى من الدالة البانية. يجب أن تستخدم الدالة read() الدالة prompt لقراءة رقم جديد وإضافته إلى value. بمعنى آخر، الخاصية value هي مجموع القيم المدخلة بواسطة المستخدم بالإضافة إلى القيمة الأولية startingValue. هنا مثال على ما يجب أن يُنَفَّذ: let accumulator = new Accumulator(1); // القيمة الأولية 1 accumulator.read(); // يضيف قيمة مدخلة بواسطة المستخدم accumulator.read(); // يضيف قيمة مدخلة بواسطة المستخدم alert(accumulator.value); // يعرض مجموع القيم الحل function Accumulator(startingValue) { this.value = startingValue; this.read = function() { this.value += +prompt('How much to add?', 0); }; } let accumulator = new Accumulator(1); accumulator.read(); accumulator.read(); alert(accumulator.value); ترجمة -وبتصرف- للفصل Constructor, operator "new" من كتاب The JavaScript Language .task__importance { color: #999; margin-left: 30px; } .task__answer { border: 3px solid #f7f6ea; margin: 20px 0 14px; position: relative; display: block; padding: 25px 30px; } code { background-color: rgb(250, 250, 250); border-radius: 3px; } اقرأ أيضًا المقال التالي: توابع الأنواع الأولية المقال السابق: التحويل من نوع كائن إلى نوع أولي
-
يقضي المبرمجون معظم وقتهم في العمل على أحد محررات الشيفرة البرمجية لكتابة وتطوير برامجهم. سنساعدك في هذا المقال على اختيار المحرر الأنسب لك. بدايةً، يوجد نوعان رئيسيان لمحررات الشيفرة البرمجية: «بيئات التطوير المتكاملة» (Integrated Development Environments وتدعى اختصارًا IDE) و «المحررات البسيطة» (lightweight editors). يستخدم العديد من المبرمجين محرِّرًا واحدًا من كل نوع. في بداية تعلمك للبرمجة، يمكنك استعمال المحررات البسيطة مثل المفكرة (notepad) في ويندوز والمحرر vim أو kate في لينكس ولكن ستحتاج مع تقدمك وتطور وكبر شيفرتك إلى بيئة تطوير متكاملة. فما هي بيئة التطوير المتكاملة؟ بيئة التطوير المتكاملة بيئة التطوير المتكاملة «IDE» عبارة عن محرر شيفرة برمجية قوي، ومزود بمزايا كثيرة ومتنوعة تكفي للعمل عادةً على كامل المشروع. وكما هو واضح من اسمها، فهي ليست كأي محرر عادي، ولكن «بيئة تطوير» شاملة. يمكنك تحميل المشروع بجميع ملفاته إلى بيئة التطوير (IDE)، مما يسمح بالتنقل بينها بسهولة، كما توفر هذه البيئة (IDE) ميزة الإكمال التلقائي أثناء كتابتك للشيفرة البرمجية كالتعليمات وأسماء المتغيرات بالنسبة لكامل المشروع (وليس ضمن الملف المفتوح فقط). وتتيح الاندماج مع نظام التحكم في النسخ مثل git، وبيئة الاختبار، وغيرها من الأمور التي تُنفذ على مستوى المشروع. بإمكانك الاطلاع على خيارات بيئات التطوير (IDE) التالية، لمساعدتك على اختيار واحدة منها للعمل عليها إن لم تحدد واحدة بعد: WebStorm: تستخدم لتطوير الواجهات الأمامية (Front-end development). توفر الشركة محررات أخرى للعديد من اللغات البرمجية ولكنها مدفوعة. NetBeans: بيئة متكاملة ومجانية وتعمل على مختلف أنظمة التشغيل. Visual Studio: خاص بنظام التشغيل «ويندوز»، (ميّز بينه وبين "Visual Studio Code"). وهو محرر قوي ومدفوع، ومناسب جدًا للعمل على منصة «NET.»، ويوجد نسخة مجانية منه تُسمى Visual Studio Community. أغلب بيئات التطوير المتكاملة متعددة المنصات، وتعمل على أنظمة تشغيل مختلفة ولكنها مدفوعة (لها فترة تجريبية مجانية)، وكلفتها متواضعة بالنسبة لدخل المطور المؤهل، لذلك ننصحك باختيار الأفضل والأنسب لك، دون القلق من كلفتها. المحررات البسيطة المحررات البسيطة (Lightweight editors) ليست قوية مثل بيئات التطوير المتكاملة، لكنها سريعة، وأنيقة، وبسيطة الاستخدام. تُستخدَم بشكل أساسي لفتح وتعديل الملف بطريقة سريعة وآنية. الاختلاف الرئيسي بين المحرر البسيط وبيئة التطوير المتكاملة، هو أنّ بيئة التطوير المتكاملة تعمل على «مستوى المشروع»، لذلك تُحمِّل الكثير من البيانات في البداية، وتكون قادرة على تحليل بنية المشروع عند الحاجة وغيرها من المهام. أمّا المحرر البسيط، يكون أسرع بشكل كبير عند الحاجة للعمل على ملف واحد فقط من المشروع. عمليًا، يمكن أن تملك المحررات البسيطة العديد من الإضافات التي تتضمن محللات ومدققات الصياغة على مستوى المشروع وميزة الإكمال التلقائي، وبالتالي لا توجد هناك حدود واضحة بين المحرر الخفيف وبيئة التطوير المتكاملة. لذلك وقبل أن تحسم رأيك باستخدام بيئة تطوير متكاملة، ننصحك بالاطلاع على الخيارات التالية للمحررات البسيطة: Visual Studio Code: مجاني ومتعدد المنصات ويملك العديد من المزايا المشابهة لمزايا بيئة التطوير المتكاملة. Atom: مجاني الاستخدام ومتعدد المنصات. Sublime Text: برنامج تجريبي ومتعدد المنصات ويدعم العديد من اللغات البرمجية (programming languages) واللغات التوصيفية (markup languages). Brackets: محرِّر مجاني ومفتوح المصدر وعابر للمنصات ومخصَّص بشكل عام لمطوري الويب وبشكل خاص لمصممي الويب ومطوري الواجهات الأمامية. ++Notepad: محرِّر مجاني وخاص بنظام التشغيل ويندوز. Vim و Emacs: محِّرران بسيطان، ويعدَّان خيارًا جيدًا إذا كنت تعرف كيفية استخدامهما. ما هو محرري المفضل الذي سأستخدمه؟ نصيحة مني لك هو تجريب الخيارين السابقين كلاهما؛ استخدم بيئة التطوير المتكاملة عند العمل على كامل المشروع، وأحد المحررات الخفيفة من أجل التعديلات السريعة والطفيفة في ملفات المشروع. يمكن في البداية استعمال المحررات الخفيفة (مثل VS code) التي تفي بالغرض في أغلب المشاريع ثمَّ الانتقال إلى البيئات المتكاملة (سواءً المجانية أو المدفوعة) في المستويات المتقدمة عندما تتولد الحاجة إليها. أسمعك تسألني ما الذي استخدمه؟ حسنًا، أنا استخدم: استعمل WebStorm - بيئة التطوير المتكاملة - عند العمل بلغة JavaScript، واستعمل خيارًا آخر من JetBrains مثل IntelliJ IDEA عند العمل بلغات برمجية مختلفة. واستخدم Sublime أو Atom كمحرر خفيف. قبل أن تختلف معي بشأن القائمة السابقة، فإن هذه المحررات هي إما محررات استخدمتها بنفسي أو استخدمها أحد أصدقائي من المطورين الجيدين لوقت طويل وكانوا سعداء باستخدامهم لها. هناك العديد من المحررات الجيدة أيضًا في عالمنا الكبير. لذلك، ننصحك بالعمل على المحرر المفضل لديك. فاختيار المحرر، مثل أي أداة أخرى، هو قرار خاص، ومعتمد على مشروعك، وعاداتك، وتفضيلاتك الشخصية. ولكي تعرف ما هو المحرر المفضل لديك، لا بد أن تجرب جميع المحرِّرات. ترجمة -وبتصرف- للفصل Code Editors من كتاب The JavaScript Language انظر أيضًا المقال التالي: المثال الأول: أهلًا بالعالم! المقال السابق: أدوات المطور كامل مقالات دليل تعلم جافاسكربت
-
الكائنات المُكرَّرة (Iterables) هي مفهوم أعمّ من المصفوفات. تتيح لنا هذه الكائنات تحويل أيّ كائن إلى ”كائن يمكن تكراره“ فيمكننا استعماله في حلقة for..of. بالطبع فالمصفوفات يمكن تكرارها، ولكن هناك كائنات أخرى (مضمّنة في أصل اللغة) يمكن تكرارها أيضًا، مثل السلاسل النصية. لو لم يكن الكائن مصفوفة تقنيًا، ولكن يمكننا تمثيله على أنّه تجميعة من العناصر (النوع list، والنوع set)، فصياغة for..of ممتازة لنمرّ على عناصره. لذا دعنا نرى كيف يمكن توظيف هذه ”المُكرَّرات“. Symbol.iterator يمكن لنا أن نُدرك هذا المفهوم -مفهوم المُكرَّرات أعني- بأن نصنع واحدًا بنفسنا. لنقل أنّ لدينا كائن وهو ليس بمصفوفة بأيّ شكل، ولكن يمكن توظيفه لحلقة for..of. مثلًا كائن المدى هذا range يُمثّل مجموعة متتالية من الأعداد. let range = { from: 1, to: 5 }; // نُريد أن تعمل for..of هكذا: // for(let num of range) ... num=1,2,3,4,5 لنُضيف خاصية التكرار إلى range (فتعمل بهذا for..of)، علينا إضافة تابِع إلى الكائن بالاسم Symbol.iterator (وهو رمز خاصّ في اللغة يتيح لنا هذه الميزة). حين تبدأ for..of، تنادي ذلك التابِع مرة واحدة (أو تعرض الأخطاء المعروفة لو لم تجدها). على هذا التابِع إعادة مُكرِّر/iterator، أي كائنًا له التابِع next. بعدها، تعمل for..of مع ذلك الكائن المُعاد فقط لا غير. حين تحتاج for..of القيمة التالية، تستدعي next() لذاك الكائن. يجب أن يكون ناتج next() بالشكل هذا {done: Boolean, value: any}، حيث لو كانت done=true فيعني أن التكرار اكتمل، وإلّا فقيمة value هي التالية. إليك النص الكامل لتنفيذ كائن range (مع الملاحظات): let range = { from: 1, to: 5 }; // 1. حين ننادي for..of فهي تنادي هذه range[Symbol.iterator] = function() { // ...وتُعيد الكائن المُكرِّر: // 2. بعد ذلك تعمل for..of مع هذا المُكرِّر، طالبةً منه القيم التالية return { current: this.from, last: this.to, // 3. يُستدعى next() في كلّ كرّة في حلقة for..of next() { // 4. يجب أن يُعيد القيمة كائنًا كهذا {done:.., value :...} if (this.current <= this.last) { return { done: false, value: this.current++ }; } else { return { done: true }; } } }; }; // والآن تعمل! for (let num of range) { alert(num); // 1، ثمّ 2 فَ 3 فَ 4 فَ 5 } لاحظ الميزة الأساس للمُكرَّرات: فصل الاهتمامات. عنصر range ذاته ليس له التابِع next(). بدل ذلك، يُنشأ كائن آخر (أي "المُكرَّر") عند استدعاء range[Symbol.iterator]()، وتابِعه next() يُولّد قيم التكرار. الخلاصة هي أنّ كائن المُكرَّر منفصل عن الكائن الذي يُكرِّره هذا المُكرَّر. نظريًا، يمكننا دمجهما معًا واستعمال كائن range نفسه مُتعدَّدًا لتبسيط الكود أكثر. هكذا تمامًا: let range = { from: 1, to: 5, [Symbol.iterator]() { this.current = this.from; return this; }, next() { if (this.current <= this.to) { return { done: false, value: this.current++ }; } else { return { done: true }; } } }; for (let num of range) { alert(num); // 1، ثمّ 2 فَ 3 فَ 4 فَ 5 الآن صار يُعيد range[Symbol.iterator]() كائن range نفسه: وهذا الكائن فيه تابِع next() اللازم كما ويتذكّر حالة التعداد الحالية في this.current. الشيفرة أبسط، أجل. وأحيانًا لا بأس به هكذا. المشكلة هنا هي استحالة وجود حلقتي for..of تعملان على الكائن في آن واحد، إذ سيتشاركان حالة التعداد؛ فليس هناك إلا مُتعدَّد واحد: الكائن نفسه. لكن أصلًا وجود حلقتي for..of نادر، حتى في العمليات غير المتزامَنة. مُكرَّرات لا تنتهي يمكن أيضًا ألا تنتهي المُكرَّرات أبدًا. فمثلًا لا ينتهي المدى range لو صار range.to = Infinity. يمكن أيضًا أن نصنع كائن مُتعدَّد يولّد أعدادًا شبه عشوائية (pseudorandom) لانهائية، ستفيد في حالات حرجة. ما من حدود مفروضة على ناتج next. يمكنه إعادة القيم مهما أراد وبالكم الذي أراد، لا مشكلة. طبعًا، المرور على هذا المُكرَّر بحلقة for..of لن ينتهي أبد الدهر، ولكن يمكننا إيقافها باستعمال break. السلاسل النصية مُكرَّرة تُعدّ المصفوفات والسلاسل النصية أكثر المُكرَّرات المقدَّمة من اللغة استعمالًا. بالنسبة إلى السلاسل النصية، فحلقة for..of تمرّ على محارفها: for (let char of "test") { // تتنفّذ أربع مرات: مرة لكلّ محرف alert( char ); // t فَ e فَ s فَ t } كما وتعمل -كما يجب!- مع الأزواج النائبة أو البديلة (Surrogate Pairs)! let str = '??'; for (let char of str) { alert( char ); // ? وثمّ ? } نداء المُكرَّر جهارة لنعرف المُكرَّرات معرفةً أعمق، لنرى كيف يمكن استعمالها جهارةً. سنمرّ على سلسلة نصية بنفس الطريقة التي يمرّ بها for..of، ولكن هذه المرة ستكون النداءات مباشرة. تُنشِئ هذه الشيفرة مُكرَّرًا لسلسلة نصية وتأخذ القيم منه "يدويًا": let str = "Hello"; // تنفّذ ما تنفّذه // for (let char of str) alert(char); *!* let iterator = str[Symbol.iterator](); */!* while (true) { let result = iterator.next(); if (result.done) break; alert(result.value); // تطبع المحارف واحدًا تلو الآخر } في الحياة الواقعية، نادرًا ما ستحتاج هذا. لكن المفيد أنّنا نتحكّم أكثر على عملية التكرار موازنةً بِـ for..of. فمثلًا يمكننا تقسيم عملية التكرار: نكرّر قليلًا، نتوقّف ونفعل شيئًا آخر، ثمّ نواصل التكرار. المُكرَّرات والشبيهات بالمصفوفات هذان المصطلحان الرسميان يبدوان متشابهين إلى حدّ ما، ولكنّهما مختلفين تمام الاختلاف. حاوِل إدراكهما إدراكًا صحيحًا لتتجنّب هذا الاختلاط لاحقًا. المُكرَّرات كائنات تُنفّذ التابِع Symbol.iterator، كما شرحنا أعلاه. الشبيهات بالمصفوفات كائنات لها فهارس وصفة طول length، وبهذا ”تشبه المصفوفات“… المصطلح يشرح نفسه. حين نستعمل جافاسكربت للمهام الحقيقية في المتصفحات وغيرها من بيئات، نقابل مختلف الكائنات أكانت مُكرَّرات أو شبيهات بالمصفوفات، أو كليهما معًا. السلاسل النصية مثلًا مُكرَّرة (يمكن استعمال for..of عليها)، وشبيهة بالمصفوفات أيضًا (لها فهارس عددية وصفة length). ولكن ليس من الضروري أن يكون المُكرَّر شبيه بالمصفوفة، والعكس صحيح (لا يكون الشبيه بالمصفوفة مُكرَّر). فالمدى range في المثال أعلاه مُكرَّر، ولكنه ليس شبيه بالمصفوفة إذ ليس فيه صفات فهارس وlength. إليك كائنًا شبيهًا بالمصفوفات وليس مُكرَّرًا: let arrayLike = { // فيه فهارس وطول => شبيه بالمصفوفات 0: "Hello", 1: "World", length: 2 }; // خطأ (ما من Symbol.iterator) for (let item of arrayLike) {} عادةً، لا تكون لا المُكرَّرات ولا الشبيهات بالمصفوفات مصفوفات حقًا، فليس لها push أو pop وغيرها. لكن هذا غير منطقي. ماذا لو كان لدينا كائن من هذا النوع وأردنا التعامل معه بأنه مصفوفة؟ لنقل أنّا سنعمل على range باستعمال توابِع المصفوفات، كيف السبيل؟ Array.from التابِع العام Array.from يأخذ مُكرَّرًا أو شبيهًا بالمصفوفات ويحوّله إلى مصفوفة "فعلية". بعدها ننادي توابِع المصفوفات التي نعرفها عليها. هكذا مثلًا: let arrayLike = { 0: "Hello", 1: "World", length: 2 }; let arr = Array.from(arrayLike); // (*) alert(arr.pop()); // تكتب World (أيّ أنّ التابِع عمل) يأخذ التابِع Array.from في سطر (*) الكائن، ويفحصه أكان مُكرَّرًا أو شبيهًا بالمصفوفات، ويصنع مصفوفة جديدة ينسخ قيم ذلك الكائن فيها. ذات الأمر للمُكرَّرات: // نأخذ range من المثال أعلاه let arr = Array.from(range); alert(arr); // تكتب 1,2,3,4,5 (تحويل toString للمصفوفة يعمل) والصياغة الكاملة للتابِع Array.from تتيح لنا تقديم دالة ”خريطة“ اختيارية: Array.from(obj[, mapFn, thisArg]) يمكن أن يكون الوسيط الاختياري الثاني mapFn دالةً تُطبّق على كلّ عنصر قبل إضافته للمصفوفة، ويتيح thisArg ضبط ماهيّة this للتابِع. مثال: // نأخذ range من المثال أعلاه // نُربّع كلّ عدد let arr = Array.from(range, num => num * num); alert(arr); // 1,4,9,16,25 هنا نستعمل Array.from لتحويل سلسلة نصية إلى مصفوفة من المحارف: let str = '??'; // يقسم str إلى مصفوفة من المحارف let chars = Array.from(str); alert(chars[0]); // ? alert(chars[1]); // ? alert(chars.length); // 2 على العكس من str.split، فهي هنا تعتمد على طبيعة تكراريّة السلسلة النصية، ولهذا تعمل كما ينبغي (كما تعمل for..of) مع الأزواج النائبة. هنا أيضًا تقوم بذات الفعل، نظريًا: let str = '??'; let chars = []; // داخليًا، تُنفّذ Array.from ذات الحلقة for (let char of str) { chars.push(char); } alert(chars); …ولكن تلك أقصر. يمكننا أيضًا صناعة تابِع slice مبني عليها يحترم الأزواج النائبة. function slice(str, start, end) { return Array.from(str).slice(start, end).join(''); } let str = '???'; alert( slice(str, 1, 3) ); // ?? // التابِع الأصيل/native في اللغة لا يدعم الأزواج النائبة alert( str.slice(1, 3) ); // يُولّد نصّ ”قمامة“ (قطعتين من أزواج نائبة مختلفة) خلاصة تُدعى الكائنات التي يمكن استعمالها في for..of بالمُكرَّرات (Iterables). على المُكرَّرات (تقنيًا) تنفيذ التابِع بالاسم Symbol.iterator. يُدعى ناتج obj[System.iterator] بالمُكرَّر. يتعامل المُكرَّر بعملية التكرار. يجب أن يحتوي المُكرَّر التابِع بالاسم next() حيث يُعيد كائن {done: Boolean, value: any}… تُشير done:true هنا بأنّ التكرار اكتمل، وإلّا فَـ value هي القيمة التالية. تُنادي الحلقة for..of التابِع Symbol.iterator تلقائيًا عند تنفيذها، ولكن يمكننا أيضًا فعل ذلك يدويًا. تُنفّذ المُكرَّرات المضمّنة في اللغة Symbol.iterator (مثل السلاسل النصية والمصفوفات). مُكرَّر السلاسل النصية يفهم الأزواج البديلة. تُدعى الكائنات التي فيها صفات فهارس وصفة طول length بالشبيهات بالمصفوفات. يمكن أيضًا أن تكون لها صفات وتوابِع أخرى، إلّا أنّ ليس فيها توابِع المصفوفات المضمّنة في بنية اللغة. لو نظرنا ورأينا مواصفات اللغة، فسنرى بأنّ أغلب التوابِع المضمّنة فيها تتعامل مع المصفوفات على أنّها مُكرَّرات أو شبيهات بالمصفوفات بدل أن تكون مصفوفات ”حقيقية“؛ هكذا تصير أكثر تجرّديّة (abstract). تصنع Array.from(obj[, mapFn, thisArg]) مصفوفةً Array حقيقية من المُكرَّر أو الشبيه بالمصفوفات obj، بهذا يمكن استعمال توابِع المصفوفات عليها. يتيح لنا الوسيطين mapFn وthisArg تقديم دالة لكلّ عنصر من عناصرها. ترجمة -وبتصرف- للفصل Iterables من كتاب The JavaScript language .task__importance { color: #999; margin-left: 30px; } .task__answer { border: 3px solid #f7f6ea; margin: 20px 0 14px; position: relative; display: block; padding: 25px 30px; } اقرأ أيضًا المقال التالي: النوع Map (الخرائط) والنوع Set (الأطقم) المقال السابق: توابع المصفوفات (Array methods)
-
دوال المُعاملة للوعود .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
-
طرحناها في الفصل "مقدمة إلى ردود النداء 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 ما إن يردّ الخادم البعيد بترويسات الطلب، ولكن قبل تنزيل الردّ كاملًا. علينا استدعاء التابِع response.text() لقراءة الردّ كاملًا، وهو يُعيد وعدًا يُحل resolved متى نُزّل النص الكامل من الخادوم البعيد، وناتجه يكون ذلك النص. تُرسل الشيفرة أسفله طلبًا إلى 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
-
تُتيح لك الكائنات تخزين القيم في مجموعات ذات مفاتيح، وهذا أمر طيّب. ولكنّك دومًا ما ستحتاج في عملك إلى مجموعة مرتّبة، أي أنّ العناصر مرتّبة: عنصر أوّل، عنصر ثانٍ، عنصر ثالث، وهكذا دواليك. تُفيدنا هذه الطريقة في تخزين أمور مثل: المستخدمين والبضائع وعناصر HTML وغيرها. هنا يكون استعمال الكائنات غير موفّق، إذ أنّها لا تقدّم لنا أيّ تابِع يتيح تحديد ترتيب العناصر، فلا يمكننا إضافة خاصيةً جديدةً تحلّ بين الخاصيات الموجودة. لم تُصنع الكائنات لهذا الغرض بتاتًا. توجد بنية بيانات أخرى باسم Array (ندعو هذا النوع بالمصفوفة) وهي تتيح لنا تخزين مجموعات العناصر مرتّبةً. التصريح توجد صياغتان اثنتان لإنشاء مصفوفة فارغة: let arr = new Array(); let arr = []; تحتاج في عملك أغلب الوقت (ونقول أغلب الوقت) الصياغةَ الثانية. يمكننا أيضًا تقديم عناصر أوليّة للمصفوفة نكتبها في أقواس: let fruits = ["Apple", "Orange", "Plum"]; لاحظ أنّ عناصر المصفوفات مرقّمة (مُفهرسة) بدءًا من الرقم صفر. ويمكننا أن نأخذ عنصرًا منها بكتابة ترتيبه في أقواس معقوفة: let fruits = ["Apple", "Orange", "Plum"]; alert( fruits[0] ); // Apple/تفاحة alert( fruits[1] ); // Orange/برتقالة alert( fruits[2] ); // Plum/برقوق يمكن أيضًا تعويض أحد العناصر بأخرى: fruits[2] = 'Pear'; // ["Apple", "Orange", "Pear"] …أو إضافة أخرى جديدة إلى المصفوفة: fruits[3] = 'Lemon'; // ["Apple", "Orange", "Pear", "Lemon"] نعرف باستعمال التابع length إجمالي العناصر في المصفوفة: let fruits = ["Apple", "Orange", "Plum"]; alert( fruits.length ); // 3 يمكننا أيضًا استعمال alert لعرض المصفوفة كاملةً. let fruits = ["Apple", "Orange", "Plum"]; alert( fruits ); // Apple,Orange,Plum كما يمكن للمصفوفات تخزين أيّ نوع من البيانات. مثلًا: // قيم مختلفة الأنواع let arr = [ 'Apple', { name: 'John' }, true, function() { alert('hello'); } ]; // خُذ الكائن ذا الفهرس 1 ثمّ اعرض اسمه alert( arr[1].name ); // John // خُذ الدالة في الفهرس 3 ثمّ شغّلها arr[3](); // hello الفاصلة نهاية الجملة كما الكائنات، يمكن أن نُنهي عناصر المصفوفات بفاصلة ,: let fruits = [ "Apple", "Orange", "Plum", ]; يُسهّل أسلوب الكتابة «بالفاصلة نهاية الجملة» إضافة العناصر وإزالتها، إذ أن الأسطر البرمجية كلها تصير متشابهة. توابِع الدفع والجلب من بين أنواع المصفوفات، تُعدّ الطوابير أكثرها استعمالًا. تعني الصفوف (في علوم الحاسوب) تجميعات العناصر المرتّبة والتي تدعم العمليتين هاتين: الدفع push: يُضيف عنصرًا نهاية الصفّ الأخذ shift: يأخذ عنصرًا من بداية الصفّ، فيتحرّك الصفّ ويصير العنصر الثاني هو الأول فيه. تدعم المصفوفات كلتا العمليتين هاتين. وفي الحياة العملية، استعمال هاتين العمليتين ضروري دومًا. نأخذ مثلًا مجموعة رسائل مرتّبة يجب عرضها على الشاشة، أو «صف رسائل». هناك طريقة أخرى لاستعمال المصفوفات، وهي بنية البيانات بالاسم «كومة». تدعم الأكوام عمليتين أيضًا: الدفع push: يُضيف عنصرًا نهاية الكومة. السحب pop: يأخذ عنصرًا من نهاية الكومة. أي أنّ العناصر الجديدة تُضاف دومًا إلى آخر الكومة، وتُزال أيضًا من نهايتها. عادةً ما نرسم هذه الأكوام مثل أكوام بطاقات اللعب: البطاقات الجديدة تُضاف أعلى الكومة، وتُأخذ من أعلاها أيضًا: في الأكوام، آخر عنصر ندفعه إليها يكون أوّل من يُأخذ، ويسمّى هذا بمبدأ «آخر من يدخل أول من يخرج» (Last-In-First-Out). أمّا في الطوابير، فهي «أول من يدخل أول من يخرج» (First-In-First-Out). تعمل المصفوفات في جافاسكربت بالطريقتين، صفوف أو أكوام. يمكنك استعمالها لإضافة العناصر وإزالتها من/إلى بداية المصفوفة ونهايتها. تُسمّى بنية البيانات هذه (في علم الحاسوب) باسم «الطوابير ذات الطرفين». التوابع التي تؤثّر على نهاية المصفوفة: pop: يستخرج آخر عنصر من المصفوفة ويُعيده: let fruits = ["Apple", "Orange", "Pear"]; alert( fruits.pop() ); // أزِل «Pear» بتنبيه عبر الدالة alert alert( fruits ); // Apple, Orange push: يُضيف العنصر إلى آخر المصفوفة: let fruits = ["Apple", "Orange"]; fruits.push("Pear"); alert( fruits ); // Apple, Orange, Pear باستدعاء fruits.push(...) كأنّما استدعيت fruits[fruits.length] = .... التوابِع التي تؤثّر على بداية المصفوفة: shift: يستخرج أوّل عنصر من المصفوفة وتُعيده: let fruits = ["Apple", "Orange", "Pear"]; alert( fruits.shift() ); // أزِل التفاحة واعرضها بِ alert alert( fruits ); // Orange, Pear unshift: يُضيف العنصر إلى أوّل المصفوفة: let fruits = ["Orange", "Pear"]; fruits.unshift('Apple'); alert( fruits ); // Apple, Orange, Pear يمكنك أيضًا إضافة أكثر من عنصر في استدعاء واحد من push وunshift: let fruits = ["Apple"]; fruits.push("Orange", "Peach"); fruits.unshift("Pineapple", "Lemon"); // ["Pineapple", "Lemon", "Apple", "Orange", "Peach"] alert( fruits ); داخليًا وخلف الكواليس المصفوفات هي كائنات، كائنات من نوع خاص. القوسان المعقوفان المستعملان للدخول إلى الخاصيات arr[0] هما فعليًا جزء من صياغة الكائنات. داخليًا، لا يفرق ذاك عن obj[key] (إذ arr هو الكائن والأرقام تلك مفاتيح). ما تفعله المصفوفات هو «توسعة» الكائنات بتقديم توابِع خاصّة تعمل مع البيانات والمجموعات المرتّبة، إضافةً إلى تقديم خاصية length، ولكنّ أساسها ما زال الكائنات. تذكّر أنّ هناك 7 أنواع أساسية في جافاسكربت، فقط. المصفوفة هي كائن، وتتصرّف بناءً على ذلك، ككائن. فمثلًا، عند نسخها تُنسخ بالمرجع (By reference): let fruits = ["Banana"] let arr = fruits; // انسخها بالمرجع (متغيران اثنان يُشيران إلى نفس المصفوفة) alert( arr === fruits ); // true arr.push("Pear"); // عدّل المصفوفة «بالمرجع» alert( fruits ); // صاروا الآن عنصرين: Banana, Pear …إلا أنّ المميز حقًا في المصفوفات هي آلية تمثيلها داخليًا، إذ يحاول المحرّك تخزين عناصرها متتابعةً في مساحة الذاكرة، أي واحدة بعد الأخرى، تمامًا مثلما وضّحت الرسوم في هذا الفصل. هناك أيضًا طُرق أخرى لتحسين (optimization) المصفوفات فتعمل بسرعة كبيرة حقًا. ولكن، لو لم نعمل مع المصفوفة على أنّها «تجميعة مرتّبة» بل وكأنّها كائن مثل غيرها، فسينهار هذا كله. يمكننا (تقنيًا) كتابة هذا: // نصنع مصفوفة let fruits = []; // نُسند خاصيةً لها فهرس أكبر من طول المصفوفة بكثير fruits[99999] = 5; // نُنشئ خاصيةً لها أيّ اسم fruits.age = 25; أجل، يمكننا فعل هذا، فالمصفوفات في أساسها كائنات، ويمكننا إضافة ما نريد من خاصيات لها. ولكن المحرّك هنا سيرى بأنّا نُعامل المصفوفة معاملة الكائن العادي. وبهذا -في هذه الحالة- لا تنفع أنواع التحسين المخصّصة للكائنات، وسيُعطّلها المحرّك، وتضيع كل فوائد المصفوفات. هذه طرائق يمكنك فيها إساءة استعمال المصفوفات: إضافة خاصيات ليست عددية مثل arr.test = 5. الفراغات، أي تُضيف arr[0] وبعدها arr[1000] (دون عناصر بينها). ملء المصفوفة بالعكس، أي arr[1000] ثم arr[999] وهكذا. نرجوك هنا أن تعتبر المصفوفات بنًى خاصّة تتعامل مع البيانات المرتّبة، فهي تقدّم لك توابِع خاصّة لهذا بالذات. يتغيّر تعامل محرّكات جافاسكربت حين تتعامل مع المصفوفات، فتعمل مع البيانات المرتّبة المتتابعة، فمن فضلك استعمِلها بهذه الطريقة. لو أردت مفاتيح لا عددية، أو مثلما في الحالات الثلاث أعلاه، فغالبًا لا تكون المصفوفة ما تبحث عنه، بل الكائنات العادية {}. الأداء يعمل التابِعان push/pop بسرعة، بينما shift/unshift بطيئان. لماذا يكون التعامل مع نهاية المصفوفة أسرع من التعامل مع بدايتها؟ لنأخذ نظرة عمًا يحدث أثناء تنفيذ الشيفرة: // خُذ عنصرًا واحدًا من الأوّل fruits.shift(); لا يكفي أن تأخذ العنصر ذا الفهرس 0 وتُزيله، بل عليك أيضًا إعادة ترقيم بقية العناصر وفقًا لذلك. ما تفعله عملية shift هي ثلاث أمور: إزالة العنصر ذا الفهرس 0. تحريك كل العناصر الأخرى إلى يسار المصفوفة، وإعادة ترقيمها من الفهرس رقم 1 إلى 0، ومن 2 إلى 1، وهكذا. تحديث خاصية الطول length. زِد العناصر في المصفوفات، تزيد الوقت اللازم لتحريكها، وتزيد عدد العمليات داخل الذاكرة. مثل shift، تفعل unshift نفس الأمور: فلنُضيف عنصرًا إلى بداية المصفوفة، علينا أولًا تحريك كل العناصر إلى اليمين، أي نزيد فهارسها كلها. وماذا عن push/pop؟ ليس عليها تحريك أيّ عنصر. فلاستخراج عنصر من النهاية، يمحي التابِع pop الفهرس ويعدّل الطول length فيقصّره. إجراءات عملية pop: // خُذ عنصرًا واحدًا من الآخر fruits.pop(); لا تحتاج عملية pop إلى تحريك ولا مقدار ذرة، لأنّ العناصر تبقى كما هي مع فهارسها. لهذا السبب سرعتها تفوق سرعة البرق، أي أقصى سرعة ممكنة. ذات الأمر للتابِع push. الحلقات هذه إحدى أقدم الطرق للمرور على عناصر المصفوفات، استعمال حلقة for بالمرور على فهارس المصفوفة: let arr = ["Apple", "Orange", "Pear"]; for (let i = 0; i < arr.length; i++) { alert( arr[i] ); } ولكن المصفوفات تسمح بطريقة أخرى للمرور عليها، for..of: let fruits = ["Apple", "Orange", "Plum"]; // المرور على عناصر المصفوفة for (let fruit of fruits) { alert( fruit ); } لا تتيح لك حلقة for..of الوصول إلى فهرس العنصر الحالي في الحلقة، بل قيمة العنصر فقط، وفي أغلب الأحيان هذا ما تحتاج، كما وأنّ الشيفرة أقصر. طالما المصفوفات كائنات، فيمكننا (نظريًا) استعمال for..in: let arr = ["Apple", "Orange", "Pear"]; for (let key in arr) { alert( arr[key] ); // Apple, Orange, Pear } ولكن الواقع أنّ الطريقة هذه سيئة، ففيها عدد من المشاكل: تمرّ الحلقة for..in على كل الخاصيات مجتمعةً، وليس العددية منها فقط. توجد في المتصفّح وغيرها من بيئات كائنات «شبيهة بالمصفوفات». أي أن لها خاصية الطول length وخاصيات الفهارس، ولكن لها أيضًا توابِع وخاصيات لا عددية أخرى لا نحتاجها أغلب الأحيان، إلّا أنّ حلقة for..in ستمرّ عليها هي أيضًا. لذا لو اضطررت للعمل مع الكائنات الشبيهة بالمصفوفات، فهذه الخاصيات «الأخرى» ستتسبّب بالمتاعب بلا شك. أداء حلقة for..in يكون بالنحو الأمثل على الكائنات العامة لا المصفوفات، ولهذا سيكون أبطأ 10 أو 100 مرة. طبعًا فالأداء سريع جدًا مع ذلك. هذه السرعة الإضافية ستنفع غالبًا في الحالات الحرجة (أي حين يجب أن يكون تنفيذ الحلقة بأسرع وقت ممكن). مع ذلك، الحرس واجب والاهتمام بهذا الاختلاف مهم. لكن في أغلب الأحيان، استعمال for..in للمصفوفات فكرة سيئة. كلمتان حول «الطول» تتحدّث خاصية الطول length تلقائيًا متى ما عدّلنا المصفوفة. وللدّقة، فهي ليست عدد القيم في المصفوفة، بل أكبر فهرس موجود زائدًا واحد. فمثلًا، لو كان لعنصر واحد فهرس كبير، فسيكون الطول كبيرًا أيضًا: let fruits = []; fruits[123] = "Apple"; alert( fruits.length ); // 124 لكنّنا لا نستعمل المصفوفات هكذا. سجّلها عندك. هناك ما هو عجيب حول خاصية length، ألا وهي أنّها تقبل الكتابة. لو زِدنا قيمتها يدويًا، لا نرى شيئًا تغيّر، ولكن لو أنقصناها، تُبتر المصفوفة حسب الطول، ولا يمكن العودة عن هذه العملية. طالِع هذا المثال: let arr = [1, 2, 3, 4, 5]; // نبتر المصفوفة ونُبقي عنصرين فقط arr.length = 2; alert( arr ); // [1, 2] // نُعيد الطول الذي كان في الأوّل arr.length = 5; // undefined القيم المبتورة لا تعود، وإنما تصبح alert( arr[3] ); إذًا، فالطريقة الأسهل والأبسط لمسح المصفوفة هي: arr.length = 0;. new Array() هناك صياغة أخرى يمكن استعمالها لإنشاء المصفوفات: let arr = new Array("Apple", "Pear", "etc"); ولكنّها نادرًا ما تُستعمل، فالأقواس المعقوفة [] أقصر. كما وأنّ هذه الطريقة تقدّم ميزة… مخادعة، إن صحّ التعبير. إن استدعيت new Array وفيها مُعامل واحد فقط (عددي)، فستُنشأ مصفوفة لا عناصر فيها، ولكن بالطول المحدّد. هاك طريقة يمكنك بها تخريب حياتك، لو أردت: let arr = new Array(2); // هل ستكون المصفوفة [2]؟ alert( arr[0] ); //غير معرّفة! ليس فيها عناصر. alert( arr.length ); // طولها 2 في هذا الشيفرة، كل عناصر new Array(number) لها القيمة undefined. ولهذا نستعمل الأقواس المعقوفة غالبًا، لنتجنّب هذه المفاجئات السارّة، إلّا لو كنت تعي حقًا ما تفعله. المصفوفات متعدّدة الأبعاد يمكن أن تكون عناصر المصفوفات مصفوفات أخرى أيضًا. نستغلّ هذه الميزة فنعمل مصفوفات متعدّدة الأبعاد لتخزين المصفوفات الرياضية مثلًا: let matrix = [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ]; alert( matrix[1][1] ); // 5، العنصر في الوسط تحويل المصفوفات إلى سلاسل نصية تُنفّذ المصفوفات تابِع toString خاصّ بها، فيُعيد قائمة من العناصر مفصولة بفواصل. خُذ هذا المثال: let arr = [1, 2, 3]; alert( arr ); // 1,2,3 alert( String(arr) === '1,2,3' ); // true جرّب هذه، أيضًا: alert( [] + 1 ); // "1" alert( [1] + 1 ); // "11" alert( [1,2] + 1 ); // "1,21" ليس للمصفوفات Symbol.toPrimitive ولا دالة valueOf، بل تُنفّذ التحويل toString فقط لا غير. هكذا تصير [] سلسلة نصية فارغة، و[1] تصير "1" و[1,2] تصير "1,2". متى ما أضاف مُعامل الجمع الثنائي "+" شيئًا إلى السلسلة النصية، حوّله إلى سلسلة نصية هو الآخر. هكذا هي الخطوة التالية: alert( "" + 1 ); // "1" alert( "1" + 1 ); // "11" alert( "1,2" + 1 ); // "1,21" ملخص المصفوفات نوع خاصّ من الكائنات، وهي مخصّصة لتخزين البيانات عناصر مرتّبة، كما وإدارتها أيضًا. التصريح // الأقواس المعقوفة (طبيعية) let arr = [item1, item2...]; // new Array (نادرة جدًا) let arr = new Array(item1, item2...); باستدعاء new Array(number) تُنشئ مصفوفة بالطول المحدّد، ولكن بلا أيّ عنصر. خاصية الطول length هي طول المصفوفة، أو للدّقة، آخر فهرس عددي زائدًا واحد. التوابِع المختلفة على المصفوفات تعدّل هذه الخاصية تلقائيًا. إن قصّرنا خاصية length يدويًا، فنحن نبتر المصفوفة حسب القيمة الجديدة. يمكننا استعمال المصفوفة كما الصفوف ذات الطرفين، بالعمليات الآتية: push(...items): تُضيف items إلى النهاية. pop(): تُزيل العنصر من النهاية وتُعيده. shift(): تُزيل العنصر من البداية وتُعيده. unshift(...items): تُضيف items إلى البداية. لتمرّ على عناصر المصفوفة: for (let i=0; i<arr.length; i++) -- تتنفّذ بسرعة، ومتوافقة مع المتصفحات القديمة. for (let item of arr) -- الصياغة الحديثة للعناصر فقط. for (let i in arr) -- إيّاك واستعمالها. سنرجع إلى المصفوفات لاحقًا ونتعلّم توابِع أخرى لإضافة العناصر وإزالتها واستخراجها، كما وترتيب المصفوفات. هذا كله في الفصل التالي، توابع المصفوفات. تمارين هل تُنسخ المصفوفات؟ الأهمية: 3 ما ناتج هذه الشيفرة؟ let fruits = ["Apples", "Pear", "Orange"]; // ادفع عنصرًا جديدًا داخل «النسخة» let shoppingCart = fruits; shoppingCart.push("Banana"); // ماذا في fruits؟ alert( fruits.length ); // ? الحل الناتج هو 4: let fruits = ["Apples", "Pear", "Orange"]; let shoppingCart = fruits; shoppingCart.push("Banana"); alert( fruits.length ); // 4 هذا لأنّ المصفوفات كائنات. فكِلا shoppingCart وfruits يُشيران إلى نفس المصفوفة ذاتها. العمليات على المصفوفات الأهمية: 5 فلنجرّب خمس عمليات على المصفوفات. أنشِئ مصفوفة باسم styles تحوي العنصرين «Jazz» و«Blues». أضِف «Rock-n-Roll» إلى نهايتها. استبدِل القيمة في الوسط بالقيمة «Classics». يجب أن تعمل الشيفرة الذي ستكتبه ليجد القيمة في الوسط مع أيّ مصفوفة كانت لو كان طولها عدد فردي. أزِل القيمة الأولى من المصفوفة واعرضها. أضِف «Rap» و«Reggae» إلى بداية المصفوفة. المصفوفة خلال العمليات هذه: Jazz, Blues Jazz, Blues, Rock-n-Roll Jazz, Classics, Rock-n-Roll Classics, Rock-n-Roll Rap, Reggae, Classics, Rock-n-Roll الحل let styles = ["Jazz", "Blues"]; styles.push("Rock-n-Roll"); styles[Math.floor((styles.length - 1) / 2)] = "Classics"; alert( styles.shift() ); styles.unshift("Rap", "Reggae"); النداء داخل سياق المصفوفة الأهمية: 5 ما الناتج؟ لماذا؟ let arr = ["a", "b"]; arr.push(function() { alert( this ); }) arr[2](); // ? الحل من ناحية الصياغة، فالاستدعاء arr[2]() هو نفسه النداء القديم obj[method]()، فبدل obj هناك arr، وبدل method هناك 2. إذًا فما أمامنا هو نداء الدالة arr[2] وكأنّها تابِع لكائن. وبالطبيعة، فهي تستلم this الذي يُشير إلى الكائن arrوتكتب المصفوفة ناتجًا: let arr = ["a", "b"]; arr.push(function() { alert( this ); }) arr[2](); // "a","b",function للمصفوفة ثلاث قيم: الاثنتين من البداية، مع الدالة. جمع الأعداد المُدخلة الأهمية: 4 اكتب دالة sumInput() تؤدّي الآتي: طلب القيم من المستخدم باستعمال prompt وتخزينها في مصفوفة. أن ينتهي الطلب لو أدخل المستخدم قيمة غير عددية، أو سلسلة نصية فارغة، أو ضغطَ «ألغِ». حساب مجموع عناصر المصفوفة وإعادتها. ملاحظة: الصفر 0 عدد مسموح، لذا لا تُوقف الطلب لو رأيته. الحل انتبه هنا على التفصيل الصغير في الحل، صغير ولكن مهمّ: لا يمكننا تحويل قيمة المتغير value إلى عدد مباشرةً بعد prompt، لأنّه بعدما نُجري value = +value، لن نفرّق بين السلسلة النصية الفارغة (أي علينا إيقاف الطلب) من الصفر (قيمة صالحة). عوض ذلك نؤجّل ذلك لما بعد. function sumInput() { let numbers = []; while (true) { let value = prompt("A number please?", 0); // هل نلغي الطلب؟ if (value === "" || value === null || !isFinite(value)) break; numbers.push(+value); } let sum = 0; for (let number of numbers) { sum += number; } return sum; } alert( sumInput() ); أكبر مصفوفة فرعية الأهمية: 2 البيانات المُدخلة هي مصفوفة من الأعداد، مثل arr = [1, -2, 3, 4, -9, 6]. والمهمة هي: البحث عن مصفوفة فرعية متتابعة في arr لها أكبر ناتج جمع. اكتب دالة getMaxSubSum(arr) لتُعيد ذلك الناتج. مثال: getMaxSubSum([-1, 2, 3, -9]) = 5 (مجموع 2+3) getMaxSubSum([2, -1, 2, 3, -9]) = 6 (مجموع 2+(-1)+2+3) getMaxSubSum([-1, 2, 3, -9, 11]) = 11 (وهكذا...) getMaxSubSum([-2, -1, 1, 2]) = 3 getMaxSubSum([100, -9, 2, -3, 5]) = 100 getMaxSubSum([1, 2, 3]) = 6 (نأخذها كلها) إن كانت القيم كلها سالبة فيعني هذا ألا نأخذ شيئا (المصفوفة الفرعية فارغة)، وبهذا يكون الناتج صفرًا: getMaxSubSum([-1, -2, -3]) = 0 يُحبّذ لو تفكّر -رجاءً- بحلّ سريع: O(n2) أو حتّى O(n) لو أمكنك. الحل النسخة البطيئة يمكننا حساب كلّ ناتج جمع فرعي ممكن. أبسط طريقة هي أخذ كلّ عنصر وحساب مجموع المصفوفات الفرعية بدءًا من مكان العنصر. فمثلًا إن كان لدينا [-1, 2, 3, -9, 11]: // نبدأ بِ -1: -1 -1 + 2 -1 + 2 + 3 -1 + 2 + 3 + (-9) -1 + 2 + 3 + (-9) + 11 // نبدأ بِ 2: 2 2 + 3 2 + 3 + (-9) 2 + 3 + (-9) + 11 // نبدأ بِ 3: 3 3 + (-9) 3 + (-9) + 11 // نبدأ بِ -9: -9 -9 + 11 // نبدأ بِ 11: 11 في الواقع فالشيفرة هي حلقات متداخلة، تمرّ الحلقة العلوية على عناصر المصفوفة، والسفلية تعدّ النواتج الفرعية بدءًا من العنصر الحالي. function getMaxSubSum(arr) { let maxSum = 0; // إن لم نأخذ أيّ عنصر، فسنُرجع الصفر 0 for (let i = 0; i < arr.length; i++) { let sumFixedStart = 0; for (let j = i; j < arr.length; j++) { sumFixedStart += arr[j]; maxSum = Math.max(maxSum, sumFixedStart); } } return maxSum; } alert( getMaxSubSum([-1, 2, 3, -9]) ); // 5 alert( getMaxSubSum([-1, 2, 3, -9, 11]) ); // 11 alert( getMaxSubSum([-2, -1, 1, 2]) ); // 3 alert( getMaxSubSum([1, 2, 3]) ); // 6 alert( getMaxSubSum([100, -9, 2, -3, 5]) ); // 100 مدى التعقيد الحسابي لهذا الحل هو O(n2). أي بعبارة أخرى، لو زِدت حجم المصفوفة مرتين اثنتين، فسيزيد وقت عمل الخوارزمية أربع مرات أكثر. يمكن أن تؤدّي هذه الخوازرميات للمصفوفات الكبيرة (نتحدّث عن 1000 و10000 وأكثر) إلى بطء شديد في التنفيذ. النسخة السريعة لنمرّ على عناصر المصفوفة ونحفظ ناتج جمع العناصر الحالي في المتغير s. متى ما صار s سالبًا، نعيّنه صفرًا s=0. إجابتنا على هذا هي أكبر قيمة من هذا المتغير s. لو لم يكن هذا الوصف منطقيًا، فيمكنك مطالعة الشيفرة، قصيرة للغاية: function getMaxSubSum(arr) { let maxSum = 0; let partialSum = 0; for (let item of arr) { // لكلّ item في arr partialSum += item; // نُضيفه إلى partialSum maxSum = Math.max(maxSum, partialSum); // نتذكّر أكبر قيمة if (partialSum < 0) partialSum = 0; // لو كانت سالبة فالصفر } return maxSum; } alert( getMaxSubSum([-1, 2, 3, -9]) ); // 5 alert( getMaxSubSum([-1, 2, 3, -9, 11]) ); // 11 alert( getMaxSubSum([-2, -1, 1, 2]) ); // 3 alert( getMaxSubSum([100, -9, 2, -3, 5]) ); // 100 alert( getMaxSubSum([1, 2, 3]) ); // 6 alert( getMaxSubSum([-1, -2, -3]) ); // 0 على الخوارزمية هنا أن تمرّ مرورًا واحدًا فقط على المصفوفة، أي أن التعقيد الحسابي هو O(n). يمكنك أن تجد معلومات مفصّلة أكثر عن الخوارزمية هنا: Maximum subarray problem. لو لم يكن هذا واضحًا بعد، فالأفضل لو تتعقّب ما تفعل الخوارزمية في الأمثلة أعلاه، وترى ما تفعله من حسابات. «التعقّب يغني عن ألف كلمة»… ربّما. ترجمة -وبتصرف- للفصل Arrays من كتاب The JavaScript language .task__importance { color: #999; margin-left: 30px; } .task__answer { border: 3px solid #f7f6ea; margin: 20px 0 14px; position: relative; display: block; padding: 25px 30px; } اقرأ أيضًا المقال التالي: توابع المصفوفات (Array methods) المقال السابق: السلاسل النصية (strings)
-
تطورت لغة JavaScript خلال فترة طويلة دون أي مشاكل في التوافق. وجرى التحقق من استمرار عمل جميع وظائفها في كل مرة تضاف إليها خصائص جديدة. وكان لذلك فائدة كبيرة في استمرار عمل الشيفرات البرمجية الموجودة دون تعطلها. ولكن كان له ناحية سلبية أيضًا، هي أنّ الأخطاء والخصائص التي أضافها مطورو اللغة بقيت مع اللغة إلى الأبد. واستمرت هذا الحال حتى ظهور ECMAScript 5 (اختصارًا ES5) في عام 2009. التي أضافت خصائص جديدة إلى اللغة وعدّلت على بعض الخصائص الموجودة سابقًا. ولضمان استمرار عمل الشيفرات البرمجية السابقة، فإنّ معظم هذه التعديلات كانت غير فعّالة بشكل افتراضي؛ ولاستخدامها، عليك تفعيلها بشكل صريح باستخدام الموجه الخاص: "use strict". الموجه "use strict" يبدو الموجه كسلسلة نصية: "use-strict" أو 'use strict'. وعندما يوضع في بداية السكربت، يُنفَّذ السكربت وفق الطريقة والمعايير الحديثة. إليك المثال التالي: "use strict"; //هذه الشيفرة البرمجية تعمل في الوضع الصارم … قريبًا، سندرس الدوال (طريقة لجمع الأوامر). ولكن في نطرة استباقية للموضوع، لاحظ أنه بالإمكان وضع "use strict" في بداية معظم أنواع الدوال عوضًا عن كامل السكربت. وبذلك يتم تفعيل الوضع الصارم (strict mode) ضمن الدوال فقط. لكن عادةً يستخدم المبرمجون هذا الوضع لكامل السكربت. تأكد من وضع "use script" في بداية السكربتات، وإلا فإنه من الممكن ألا يُفعَّل الوضع الصارم. فمثلًا، الوضع الصارم غير مفعّل في المثال التالي: alert("some code"); // إن لم يُستعمَل في أول سطر "use strict" سيجري تجاهل الموجه "use strict" // الوضع الصارم غير مُفعَّل بإمكانك كتابة التعليقات فقط قبل استخدام الموجه "use strict". لاحظ أيضًا أنه ليس هناك طريقة لإلغاء "use strict"، أي لا يوجد موجه آخر مثل "no use strict" والذي يثني المحرك عن عمله ويلغي تفعيل الوضع الصارم. لذلك عند بدء استخدام الوضع الصارم، لا توجد طريقة لإلغائه والعودة إلى الوضع الافتراضي. طرفية المتصفح أبقِ في ذهنك أن طرفية المتصفح (console) لا تستخدم الوضع الصارم لاختبار وتنفيذ الشيفرات المكتوبة فيها؛ أي أنها لا تستخدم "use strict" افتراضيًا. عندما يوجد هناك اختلاف بين "use strict" والنمط الافتراضي في بعض الأحيان، قد تحصل على ناتج خطأ. ولن تنجح محاولتك في تفعيل الوضع الصارم (strict) بالضغط على الاختصار Shift+Enter لإدخال عدة أسطر، ثم استخدام "use strict" في البداية وذلك بسبب طريقة معالجة الطرفية للشيفرة البرمجية داخليًا. الطريقة الأفضل للتأكد من عمل "use strict" هو كتابة الشيفرة البرمجية ضمن الطرفية كما يلي: (function() { 'use strict'; // … ضع شيفرتك هنا … })() استخدم "use strict" دومًا سنتعلم لاحقًا الاختلافات بين الوضع الصارم والوضع الافتراضي الذي تعمل ضمنه الشيفرات. ففي الفصل القادم، ستلاحظ الاختلافات بين هذين الوضعين أثناء تعلمك للخصائص الجديدة. من الجيد أنه لا يوجد الكثير من هذه الاختلافات، وفي حال وجودها فهي لتحسين عمل الشيفرة البرمجية. ولكن في الوقت الحالي تكفي معرفتك بهذه الأمور العامة: الموجه "use strict" يحول المحرك إلى النمط الحديث (modern)، مما يغير من طريقة تعامله مع بعض الخصائص الموجودة سابقًا. سندرس ذلك بالتفصيل في الأجزاء القادمة من هذه السلسلة التعليمية. يتم تفعيل الوضع الصارم بوضع الموجه "use strict" في بداية السكربت أو الدالة. العديد من خصائص اللغة، مثل الأصناف (classes) والوحدات (modules)، ُتفعِّل الوضع الصارم (strict) تلقائيًا. الوضع الصارم (strict) مدعوم من قبل جميع المتصفحات الحالية. أنصحك دائمًا بوضع "use strict" في بداية السكربت. جميع الأمثلة في هذه السلسلة التعليمية تفترض العمل ضمن الوضع الصارم إلا إذا ذُكر غير ذلك وهذا في بعض الحالات النادرة. ترجمة -وبتصرف- للفصل "Modern mode, "use strict من كتاب The JavaScript Language انظر أيضًا المقال التالي: المتغيرات المقال السابق: بنية الشيفرة البرمجية كامل مقالات دليل تعلم جافاسكربت
-
ملاحظة مهمة: هذه الشرح لميزات اللغة المتقدمة، إذ تتناول هذه المقالة موضوعًا متقدمًا، لفهم بعض الحالات الهامشية بطريقة أفضل. مع العلم أنها ليست مهمة. والعديد من المطورين ذوي الخبرة الجيدة ينفذون مشاريعهم بدون معرفتها. ولكن إذا أردت معرفة كيفية عمل الأشياء تحت الطاولة فتابع القراءة. إن استدعاء التابع المقيم ديناميكيًا يمكن أن يفقد قيمة this. فمثلًا: let user = { name: "John", hi() { alert(this.name); }, bye() { alert("Bye"); } }; user.hi(); // works // لنستدعي الآن user.hi أو user.bye بحسب الاسم _(user.name == "John" ? user.hi : user.bye)(); // خطأ يوجد في السطر الأخير معامل شرطي يختار إما user.hi أو user.bye. في هذه الحالة تكون النتيجة user.hi. ثم تستدعى الدالّة ذات الأقواس () على الفور لكنها لن تعمل بطريقة صحيحة! كما تلاحظ، نتج عن الاستدعاء بالطريقة السابقة خطأ، وذلك لأن قيمة this داخل الاستدعاء تصبح غير معرفة undefined. نلاحظ هنا أن هذه الطريقة تعمل بصورة صحيحة (اسم الكائن ثم نقطة ثم اسم الدالّة): user.hi(); أما هذه الطريقة فلن تعمل (طريقة تقييم الدالّة): (user.name == "John" ? user.hi : user.bye)(); // Error! لماذا حدث ذلك؟ إذا أردنا أن نفهم سبب حدوث ذلك، فلنتعرف أولًا على كيفية عمل الاستدعاء obj.method(). شرح النوع المرجعي بالنظر عن كثب، قد نلاحظ عمليتين في العبارة obj.method(): أولًا، تسترجع النقطة '.' الخاصية obj.method. ثم تأتي الأقواس () لتنفّذها. إذًا كيف تنتقل المعلومات الخاصة بقيمة this من الجزء الأول إلى الجزء الثاني؟ إذا وضعنا هذه العمليات في سطور منفصلة، فسنفقدُ قيمة this بكل تأكيد: let user = { name: "John", hi() { alert(this.name); } } // جزّء استدعاء التوابع على سطرين let hi = user.hi; hi(); // خطأ لأنها غير معرّفة إن التعليمة hi = user.hi تضع الدالّة في المتغير، ثم الاستدعاء في السطر الأخير يكون مستقلًا تمامًا، وبالتالي لا يوجد قيمة لـ this. لجعل استدعاءات user.hi() تعمل بصورة صحيحة، تستخدم لغة جافاسكربت خدعة - بأن لا تُرجع النقطة '.' دالة، وإنما قيمة من نوع مرجعي. إن القيمة من نوع مرجعي هو "نوع من المواصفات". لا يمكننا استخدامها بطريقة مباشرة، ولكن تستخدم داخليًا بواسطة اللغة. إن القيمة من نوع مرجعي هي مجموعة من ثلاث قيم (base, name, strict)، ويشير كلُّ منها إلى: base: وهو الكائن. name وهو اسم الخاصية. strict تكون قيمتها true إذا كان الوضع الصارم مفعلًا. إن نتيجة وصول الخاصية user.hi ليست دالة، ولكنها قيمة من نوع مرجعي. بالنسبة إلى user.hi في الوضع الصارم، تكون هكذا: // قيمة من نوع مرجعي (user, "hi", true) عندما تستدعى الأقواس () في النوع المرجعي، فإنها تتلقى المعلومات الكاملة حول الكائن ودواله، ويمكنهم تعيين قيمة this (ستكون user في حالتنا). النوع المرجعي هو نوع داخلي خاص "وسيط"، بهدف تمرير المعلومات من النقطة . إلى أقواس الاستدعاء (). أي عملية أخرى مثل الإسناد hi = user.hi تتجاهل نوع المرجع ككلّ، وتأخذ قيمة user.hi (كدالّة) وتمررها. لذا فإن أي عملية أخرى تفقد قيمة this. لذلك وكنتيجة لما سبق، تُمرر قيمة this بالطريقة الصحيحة فقط إذا استدعيت الدالّة مباشرة باستخدام نقطة obj.method() أو الأقواس المربعة obj['method']() (الصياغتين تؤديان الشيء نفسه). لاحقًا في هذا السلسلة التعليمية، سنتعلم طرقًا مختلفة لحل هذه المشكلة مثل الدالّة func.bind(). الخلاصة النوع المرجعي هو نوع داخلي في لغة جافاسكربت. فقراءة خاصية ما، مثل النقطة . فيobj.method() لا تُرجع قيمة الخاصية فحسب، بل تُرجع قيمة من "نوع مرجعي" خاص والّتي تخزن فيها قيمة الخاصية والكائن المأخوذة منه. هذه طريقة لاستدعاء الدالة اللاحقة () للحصول على الكائن وتعيين قيمة this إليه. بالنسبة لجميع العمليات الأخرى، يصبح قيمة النوع المرجعي تلقائيًا قيمة الخاصية (الدالّة في حالتنا). جميع هذه الآليات مخفية عن أعيننا. ولا تهمنا إلا في الحالات الدقيقة، مثل عندما نريد الحصول على تابع ديناميكيًا من الكائن ما، باستخدام تعبير معيّن. المهام التحقق من الصياغة الأهمية: 2 ما هي نتيجة هذه الشيفرة البرمجية؟ let user = { name: "John", go: function() { alert(this.name) } } (user.go)() انتبه. هنالك فخ .anntional__paragraph { border: 3px solid #f5f5f5; margin: 20px 0 14px; position: relative; display: block; padding: 25px 30px; } الحل سينتج عن تنفيذ الشيفرة السابقة خطأ! جربها بنفسك: let user = { name: "John", go: function() { alert(this.name) } } (user.go)() // error! لا تعطينا رسالة الخطأ في معظم المتصفحات فكرة عن الخطأ الّذي حدث. يظهر الخطأ بسبب فقدان فاصلة منقوطة بعد user = {...}. إذ لا تُدرج لغة جافاسكربت تلقائيًا فاصلة منقوطة قبل قوس (user.go)()، لذلك تُقرأ الشيفرة البرمجية هكذا: let user = { go:... }(user.go)() ثم يمكننا أن نرى أيضًا أن مثل هذا التعبير المشترك هو من الناحية التركيبية استدعاء للكائن { go: ... } كدالة مع الوسيط (user.go). ويحدث ذلك أيضًا على نفس السطر مع let user، لذلك إن الكائن user لم يُعرّف حتى الآن، ومن هنا ظهر الخطأ. إذا أدخلنا الفاصلة المنقوطة، فسيكون كل شيء على ما يرام: let user = { name: "John", go: function() { alert(this.name) } }; (user.go)() // John يرجى ملاحظة أن الأقواس حول (user.go) لا تفعل شيئًا مميزًا هنا. وهي عادةً تعيد ترتيب العمليات، ولكن هنا تعمل النقطة . أولًا على أي حال، لذلك ليس هناك أي تأثير لها. الشيء الوحيد المهم هو الفاصلة المنقوطة. اشرح قيمة this الأهمية: 3 في الشيفرة أدناه، نعتزم استدعاء التابع obj.go() لأربع مرات على متتالية. ولكن لماذا الندائين (1) و(2) يعملان بطريقة مختلفة عن الندائين (3) و(4)؟ let obj, method; obj = { go: function() { alert(this); } }; obj.go(); // (1) [object Object] (obj.go)(); // (2) [object Object] (method = obj.go)(); // (3) undefined (obj.go || obj.stop)(); // (4) undefined الحل إليك تفسير ما حدث. هذا هو الطريقة العادية لاستدعاء تابع الكائن. نفس الأمر يتكرر هنا، الأقواس لا تغير ترتيب العمليات هنا، النقطة هي الأولى على أي حال. هنا لدينا استدعاء أكثر تعقيدًا (expression).method(). يعمل الاستدعاء كما لو كانت مقسمة على سطرين: f = obj.go; // يحسب نتيجة التعبير f(); // يستدعي ما لدينا هنا تُنفذ f() كدالة، بدون this. مثل ما حدث مع الحالة (3) على يسار النقطة . لدينا تعبير. لشرح سلوك (3) و(4) نحتاج إلى أن نتذكر أن توابع الوصول (accessors) للخاصيات (النقطة أو الأقواس المربعة) تُعيد قيمة النوع المرجعي. أي عملية نطبقها عليها باستثناء استدعاء التابع (مثل عملية الإسناد = أو ||) تحولها إلى قيمة عادية، والّتي لا تحمل المعلومات التي تسمح لها بتعيين قيمة this. ترجمة -وبتصرف- للفصل Reference Type من كتاب The JavaScript language
-
ملاحظة مهمة: هذه إضافة حديثة للغة، لذا قد تحتاج المتصفحات القديمة لترقيع هذا النقص. التسلسل الاختياري .? هو طريقة مقاومة للأخطاء للوصول إلى خصائص الكائن المتداخلة، حتى إذا كانت الخاصية الوسيطة غير موجودة. المشكلة إذا كنت قد بدأت للتو في قراءة هذه السلسلة التعليمية وتتعلم جافاسكربت، فلربما لم تواجه هذه المشكلة بعد، لكنها شائعة جدًا. فمثلًا، يمتلك بعض من المستخدمين لدينا يمتلكون عناوين، لكن هنالك قليل منهم لم يقدمها. لذا لا يمكننا قراءة user.address.street بأمان. هكذا: let user = {}; // تحدث للمستخدم user في حالة كان ليس لديه عنوان alert(user.address.street); // خطأ! أو عند تطويرنا لموقع وِب، ونرغب في الحصول على معلومات حول عنصر ما في الصفحة، لكنه قد لا يكون موجودًا: // إذا كان نتيجة querySelector(...) فارغًا let html = document.querySelector('.my-element').innerHTML; قبل ظهور "?." في اللغة، كان يستخدم المعامل && للتغلب على المشكلة. فمثلًا: let user = {}; // إذا كان user لا يملك عنوان alert( user && user.address && user.address.street ); // undefined (أي ليس خطأً) وكونها تتطلب كتابة طويلة للتأكد من وجود جميع المكونات، لذلك كان استخدامها مرهقًا. تسلسل اختياري التسلسل الاختياري "?." يوقف التقييم ويعيد غير معرّف undefined إذا كان الجزء قبل "?." غير معرّف undefined أو فارغ null. **للإيجاز سنفترض في هذه المقالة أن شيئًا ما "موجود" إذا لم تكن القيمة "فارغة" null أو غير معرّفة undefined. ** إليك الطريقة الآمنة للوصول إلى user.address.street: let user = {}; // إذا كان user لا يملك عنوان alert( user?.address?.street ); // undefined (ليس خطأً) إن قراءة العنوان باستخدام هذه الطريقة user?.address ستعمل حتى ولو كان الكائن user غير موجود: let user = null; alert( user?.address ); // undefined alert( user?.address.street ); // undefined الرجاء ملاحظة أن: صياغة جملة .? تجعل القيمة الموجودة قبلها اختيارية، ولكن ليس القيمة الّتي تأتي بعدها. في المثال أعلاه، تسمح التعليمة "user?." للكائن user فقط بأن يكون غير معرف أو فارغ "null/undefined". من ناحية أخرى، إذا كان الكائن user موجودًا، فيجب أن يحتوي على خاصية user.address، وإلا فإن user?.address.street ستُعطي خطأ في النقطة الثانية. ملاحظة: لا تفرط في استخدام التسلسل الاختياري. يجب أن نستخدم .? فقط في حالة عدم وجود شيئ ما. فمثلًا، بحسب منطق الشيفرة خاصتنا يجب أن يكون الكائن user موجودًا، ولكن الخاصية address اختيارية، بهذه الحالة سيكون user?.address.street أفضل. لذلك، إذا حدث أن كان الكائن user غير معرف بسبب خطأ ما، فسنُعرف عنه ونصلحه. خلاف ذلك، يمكن إسكات أخطاء الترميز عندما لا يكون ذلك مناسبًا، ويصبح تصحيحها أكثر صعوبة. ملاحظة: يجب التصريح عن المتغير الموجود قبل .? إذا لم يكن هناك متغير user على الإطلاق، فإن التعليمة user?.anything ستؤدي حتمًا إلى حدوث خطأ: // ReferenceError: إن المستخدم user غير معرًف user?.address; يجب أن يكون هناك تعريف واضح للمتغير let / const / var user. لأن التسلسل الاختياري يعمل فقط للمتغيرات المصرح عنها. اختيار الطريق الأقصر كما قلنا سابقًا، ستوقف .? فورًا (أي سيحدث قصر في الدارة) إذا لم يكن الجزء الأيسر موجودًا. لذلك، إذا كان هناك أي استدعاءات دوالّ أخرى أو آثار جانبية، فلن تحدث: let user = null; let x = 0; user?.sayHi(x++); // لا يحدث شيئ alert(x); // 0, لم تزداد القيمة حالات أخرى ()?. و []?. إن التسلسل الاختياري .? ليس مُعامل، ولكنه طريقة معينة لصياغة تعليمة، يعمل أيضًا مع الدوالّ والأقواس المربعة. فمثلًا، تُستخدم ().? لاستدعاء دالّة قد لا تكون موجودة. نلاحظ في الشيفرة أدناه، أنه لدى بعض مستخدمينا التابع admin والبعض ليس لديه: let user1 = { admin() { alert("I am admin"); } } let user2 = {}; user1.admin?.(); // I am admin user2.admin?.(); هنا، في كلا السطرين، نستخدم النقطة . أولًا للحصول على خاصية admin، لأن كائن المستخدم user يجب أن يكون موجودًا، لذا فهو آمن للقراءة منه. ثم يتحقق ().? من الجزء الأيسر: إذا كانت دالّة المسؤول موجودة، فستنفذ (على الكائن user1). وبخلاف ذلك (بالنسبة للكائن user2) يتوقف التقييم الشيفرة بدون أخطاء. تعمل الصياغة [].? أيضًا، إذا أردنا استخدام الأقواس [] للوصول إلى الخصائص بدلًا من النقطة .. على غرار الحالات السابقة، فإنه يسمح بقراءة خاصية بأمان من كائن قد لا يكون موجودًا. let user1 = { firstName: "John" }; let user2 = null; // Imagine, we couldn't authorize the user let key = "firstName"; alert( user1?.[key] ); // John alert( user2?.[key] ); // undefined alert( user1?.[key]?.something?.not?.existing); // undefined كما يمكننا استخدام .? مع delete: delete user?.name; // احذف المستخدم user.name إذا كان موجودًا ملاحظة: يمكننا استخدام .? للقراءة الآمنة والحذف، ولكن ليس الكتابة التسلسل الاختياري .? ليس له أي فائدة في الجانب الأيسر من المهمة: // فكرة الشيفرة أدناه أن تكتب قيمة user.name إذا لم تكن موجودة user?.name = "John"; // خطأ لن تعمل الشيفرة // لأن الإسناد بين undefined = "John" الخلاصة يتكون بناء جملة التسلسل الاختياري .? من ثلاثة أشكال: obj?.prop - ستُعيد obj.prop إذا كان الكائن obj موجودًا، وإلا ستعيد القيمة undefined. obj?.[prop] - ستُعيد obj[prop] إذا كان الكائن obj موجودًا، وإلا ستُعيد undefined. obj?.method() - تستدعي obj.method() إذا كان الكائن obj موجودًا، وإلا ستُعيد undefined. كما رأينا كل الطرق واضحة وسهلة الاستخدام. تتحقق .? من الجزء الأيسر بحثًا عن قيمة فارغة أو غير معرفة null/undefined ويسمح للتقييم بالمتابعة إذا لم يكن كذلك. تسمح سلسلة .? بالوصول الآمن إلى الخصائص المتداخلة. ومع ذلك، يجب أن نطبق .? بحذر، وفقط في الحالات الّتي يكون فيها الجزء الأيسر غير موجود، حتى لا نخفي عن أنفسنا الأخطاء البرمجية إذا حدثت. ترجمة -وبتصرف- للفصل Optional chaining '?.' من كتاب The JavaScript language
-
أحد الاختلافات الأساسية بين الكائنات (objects) وأنواع البيانات الأولية (primitives) هو تخزينها ونسخها "بالطريقة المرجعية" (by reference). قيم أنواع البيانات الأولية: هي سلاسل وأرقام وقيم منطقية - تُسند أو تنسخ "كقيمة كاملة". فمثلًا: let message = "Hello!"; let phrase = message; نتيجة لتنفيذ الشيفرة السابقة لدينا متغيرين مستقلين، كلّ واحد يُخزن السلسلة "Hello!". أما الكائنات ليست كذلك. لا يخزن المتغيّر الكائن نفسه، وإنما "عنوانه في الذاكرة"، بمعنى آخر "مرجع" له. هذا هو وصف الكائن: let user = { name: "John" }; هنا يُخزن الكائن في مكان ما في الذاكرة. والمتغير user له "مرجع" له. عند نسخ متغير نوعه كائن - ينسخ المرجع، ولا يتم تكرار الكائن. فمثلًا: let user = { name: "John" }; let admin = user; // نسخ المرجع الآن لدينا متغيرين، كل واحد له إشارة مرجعية لنفس الكائن: يمكننا استخدام أي متغيّر للوصول إلى الكائن وتعديل محتوياته: let user = { name: 'John' }; let admin = user; admin.name = 'Pete'; // غيرت القيمة من خلال المتغير admIn المرجعي alert(user.name); // 'Pete' ظهر التغيير على المتغير "user" المرجعي يوضح المثال أعلاه أن هناك كائنًا واحدًا فقط. كما لو كان لدينا خزانة لها مفتاحان واستخدمنا أحدهما وهو (admin) للوصول إليها. بعد ذلك، إذا استخدمنا لاحقًا مفتاحًا آخر وهو (user)، فيمكننا رؤية التغييرات. الموازنة بحسب المرجع إن معاملات المساواة == والمساواة الصارمة === الخاصة بالكائنات تعمل بأسلوب واحد. الكائنين يكونان متساويان فقط إذا كانا يشيران لنفس الكائن. هنا يشير المتغيرين لنفس الكائن، وبالتالي فإنهما متساويان: let a = {}; let b = a; // نسخ المرجع alert( a == b ); // true, كِلا المتغيّرين يشيران إلى الكائن نفسه alert( a === b ); // true وهنا نلاحظ أن الكائنان مستقلان ولذلك غير متساويين،علمًا أن كلاهما فارغ: let a = {}; let b = {}; // كائنان منفصلان alert( a == b ); // false لإجراء المقارنات مثل obj1> obj2 أو المقارنة مع قيمة obj == 5 البدائي، تحولّ الكائنات لعناصر أولية. سوف ندرس كيفية عمل تحويل الكائنات قريبًا، ولكن في الحقيقة، نادرًا ما تحدث مثل هذه المقارنات، عادةً نتيجة لخطأ في الشيفرة البرمجية. الاستنساخ والدمج يؤدي نسخ متغير يخزن كائن إلى إنشاء مرجع آخر لنفس الكائن. ولكن ماذا لو احتجنا إلى تكرار كائنٍ ما؟ إنشاء نسخة مستقلة تمامًا، أي استنساخه؟ هذا ممكن أيضًا، ولكنه أكثر صعوبة نوعًا ما، لأنه لا توجد طريقة مضمنة في لغة جافاسكربت لتأدية ذلك. في الواقع، نادرًا ما تكون هناك حاجة لذلك. وإن نسخ المرجع جيد في معظم الأوقات. ولكن إذا كنا نريد ذلك حقًا، فنحن بحاجة لإنشاء كائن جديد وتكرار بُنية الكائن الذي نريد نسخه من خلال تكرار خصائصه ونسخها على مستوى قيم الأنواع البدائية. هكذا: let user = { name: "John", age: 30 }; let clone = {}; // كائن فارغ جديد // لننسخ كلّ الخاصيات الموجودة في user إليه for (let key in user) { clone[key] = user[key]; } // الآن الكائن clone هو نسخة مستقلة تمامًا لديه نفس خاصيات الكائن user clone.name = "Pete"; // غيرنا البيانات بداخله alert( user.name ); // ما يزال الاسم John في الكائن الأساسي كما يمكننا استخدام الطريقة Object.assign لذلك. وتكون صياغتها هكذا: Object.assign(dest, [src1, src2, src3...]) الوسيط الأول dest هي الكائن المستهدف. الوسيط التالي src1, ..., srcN (يمكن أن تكون كثيرة بحسب الحاجة) هي كائنات مصدر. تُنسخ خصائص جميع الكائنات المصدر src1، ...، srcN إلى الهدف dest. بمعنى آخر، تنسخ خصائص جميع الوسطاء الّتي تبدأ من الثانية في الكائن الأول. يعيد الاستدعاء الكائن dest. فمثلًا، يمكننا استخدامه لدمج عدة كائنات في واحد: let user = { name: "John" }; let permissions1 = { canView: true }; let permissions2 = { canEdit: true }; // نسخ جميع الخاصيات من permissions1 و permissions2 إلى user Object.assign(user, permissions1, permissions2); // now user = { name: "John", canView: true, canEdit: true } إذا كان اسم الخاصية المنسوخة موجودًا بالفعل، فعندها سيُستبدل: let user = { name: "John" }; Object.assign(user, { name: "Pete" }); alert(user.name); // now user = { name: "Pete" } يمكننا أيضًا استخدام التابع Object.assign لاستبدال حلقة for..in للاستنساخ البسيط: let user = { name: "John", age: 30 }; let clone = Object.assign({}, user); تُنسخ جميع خصائص الكائن user في الكائن الفارغ ويُعاد. الاستنساخ متداخل حتى الآن افترضنا أن جميع خصائص الكائن user تكون قيم أولية. لكن ماذا عن الخصائص يمكن أن تكون مراجع لكائنات أخرى؟ كيف سنتعامل معها؟ هكذا: let user = { name: "John", sizes: { height: 182, width: 50 } }; alert( user.sizes.height ); // 182 الآن لا يكفي نسخ clone.sizes = user.sizes، لأن user.sizes كائن، سينسخه بالطريقة المرجعية. لذا فإن clone وuser سيتشاركان نفس الأحجام: هكذا: let user = { name: "John", sizes: { height: 182, width: 50 } }; let clone = Object.assign({}, user); alert( user.sizes === clone.sizes ); // true, نفس الكائن // إن user و clone لديهما نفس الحجم user.sizes.width++; // غير خاصية من مكان معين alert(clone.sizes.width); // 51, تفقد هل الخاصية تغيرت في المكان الآخر (بالتأكيد) لإصلاح ذلك، يجب علينا استخدام حلقة الاستنساخ والّتي تفحص كلّ قيمة من قيم user[key]، فإذا كانت كائنًا، فعندها تُكرر هيكلها أيضًا. وهذا ما يسمى الاستنساخ العميق (Deep Cloning). هناك خوارزمية قياسية للاستنساخ العميق تتعامل مع الحالة المذكورة أعلاه وحالات أكثر تعقيدًا، تسمى خوارزمية الاستنساخ المهيكل (Structured cloning algorithm). كما يمكننا أيضًا استخدام العودية لتنفيذها. أو ببساطة ليس علينا إعادة اختراع العجلة، إذ يمكننا أخذ تطبيق جاهز لهذا الأمر، مثل التابع _.cloneDeep(obj) الموجود في مكتبة Lodash. الخلاصة تُسند الكائنات وتنسخ بالطريقة المرجعية. بمعنى آخر، لا يخزن المتغير "قيمة الكائن"، ولكن "المكان المرجعي" (العنوان في الذاكرة) للقيمة. لذا فإن نسخ مثل هذا المتغير أو تمريره كوسيط لدالة يُنسخ هذا المرجع وليس الكائن. تُنفذُ جميع العمليات من خلال المراجع المنسوخة (مثل إضافة / إزالة الخاصيات) على نفس الكائن الفردي. لعمل "نسخة حقيقية" (نسخة مستقلة)، يمكننا استخدام Object.assign لما يسمى "بالنسخة السطحية" (إذ تُنسخ الكائنات المُتداخلة بالطريقة المرجعية) أو يمكننا النسخ من خلال دالّة "الاستنساخ العميق"، مثل الدالّة _.cloneDeep(obj). ترجمة -وبتصرف- للفصل Object copying, references من كتاب The JavaScript language
-
ملاحظة مهمة: هذه إضافة حديثة للغة، لذلك تحتاج بعض المتصفحات القديمة لترقيع هذا النقص لأن ما سنشرحه هو إضافة حديثة للغة. يوفر عامل الاستبدال اللاغي ?? (Nullish coalescing operator) صيغة قصيرة لاختيار أول متغير معرّف (defined) من القائمة. نتيجة a ?? b هو: سيُعيد a إذا لم تكن فارغة null أو غير معرّفة undefined، وإلا سيُعيد b. إذًا x = a ?? b هي اختصار: x = (a !== null && a !== undefined) ? a : b; إليك مثال أطول لتوضيح الأمر. تخيل أن لدينا مستخدم، وهناك متغيرات FirstName وlastName أو nickName لاسمهم الأول واسم العائلة أو اللقب. وويمكن أن تكون جميعها غير معرفة (undefined)، إذا قرر المستخدم عدم إدخال أي قيمة. نرغب في عرض اسم المستخدم: في حال أدخل أحد هذه المتغيرات الثلاثة، أو إظهار الإسم "مجهول" إذا لم يعيّن أي شيئ. دعنا نستخدم العامل ؟؟ لتحديد أول متغيّر معرّف: let firstName = null; let lastName = null; let nickName = "Supercoder"; // سيعرض أول قيمة غير فارغة أو غير معرفة alert(firstName ?? lastName ?? nickName ?? "Anonymous"); // Supercoder استخدام العامل || يمكن استخدام العامل || بنفس طريقة استخدام ??. في الواقع، أي يمكننا استعمال || مكان ؟؟ في الشيفرة أعلاه والحصول على نفس النتيجة، كما شرحناها في مقال العاملات المنطقية في لغة جافاسكربت. الفرق المهم هو: إن || تُعيد القيمة الحقيقية الأولى. بينما تُعيد ?? أول قيمة معرّفة. هذا مهم جدًا عندما نرغب في التعامل مع القيم غير المعرفة أو الفارغة (null/undefined) بطريقة مختلفة عن القيمة 0. فمثلًا، إليك هذا الحالة: height = height ?? 100; تعيّن هذه الشيفرة البرمجية المتغير height بالقيمة 100 في حال كان غير معرّف. دعونا نوازنه بالعامل ||: let height = 0; alert(height || 100); // 100 alert(height ?? 100); // 0 هنا، height || 100 يعامل الارتفاع الصفري على أنه غير معرّف، تمامًا مثل القيمة الفارغة null أو غير المعرفة undefined أو أي قيمة خاطئة أخرى. إذًا ستكون النتيجة هي 100. أما height ?? 100 يُعيد 100 فقط إذا كان المتغيّر height فارغًا null أو غير معرّف undefined. لذلك ستعرضُ الشيفرة السابقة قيمة الارتفاع 0 كما هي. يعتمد السلوك الأفضل على حالة الاستخدام الّتي نواجهها. عندما يكون الارتفاع صفر تكون القيمة صالحة، فمن الأفضل أن يكون العامل ??. أولوية العامل ?? إن أولوية العامل ?? منخفضة نوعًا ما: 5 في جدول MDN. إذًا تقيّم ?? بعد معظم العمليات الأخرى، ولكن قبل = و ?. إذا احتجنا إلى اختيار قيمة بالعامل ?? في تعبير معقد، ففكر في إضافة الأقواس، هكذا: let height = null; let width = null; // هام: استخدم الأقواس let area = (height ?? 100) * (width ?? 50); alert(area); // 5000 وإلا إذا حذفنا الأقواس، فإن عملية الضرب * لها أسبقية أعلى عامل ?? وستُنفذ قبلها. سيكون ذلك مشابه لهذا المثال: // يمكن أن تكون غير صحيحة let area = height ?? (100 * width) ?? 50; هناك أيضًا قيود متعلقة باللغة. لأسباب تتعلق بالسلامة، يُحظر استخدام العامل ?? مع العاملات && و ||. لاحظ الخطأ في الصياغة الموجود في الشيفرة أدناه: let x = 1 && 2 ?? 3; // خطأ في الصياغة بالتأكيد إن القيد قابل للنقاش، ولكنه أُضيف إلى المواصفات القياسية للغة بغرض تجنب الأخطاء البرمجية، إذ يبدأ الناس في التبديل من || إلى ??. استخدم الأقواس الصريحة لتجنب الأمر، هكذا: let x = (1 && 2) ?? 3; // تعمل دون أخطاء alert(x); // 2 الخلاصة يوفر عامل الاستبدال اللاغي ?? طريقة مختصرة لاختيار قيمة "معرّفة" من القائمة. يستخدم لتعيين القيم الافتراضية للمتغيرات: // أسند القيمة 100 إلى المتغير height إذا كان هذا الأخير فارغًا أو غير معرًف height = height ?? 100; عامل ?? له أولوية منخفضة جدًا، وأعلى قليلًا من العاملات ? و=. يحظر استخدامه مع العاملات || أو && بدون أقواس صريحة. ترجمة -وبتصرف- للفصل Nullish coalescing operator '??' من كتاب The JavaScript language
-
تحذير: إضافة حديثة للغة هذه إضافة حديثة للغة. يمكنك العثور على الحالة الحالية للدعم من هنا. الأعداد الكبيرة BigInt هو متغيّر عدديّ خاص، يوفر دعمًا للأعداد الصحيحة ذات الطول العشوائي. تُنشأ الأعداد الكبيرة من خلال إلحاق الحرف n بنهاية العدد العادي، أو من خلال استدعاء الدالّة BigInt والّتي بدورها ستُنشئ عدد كبير من السلاسل أو الأعداد العادية وما إلى ذلك. const bigint = 1234567890123456789012345678901234567890n; const sameBigint = BigInt("1234567890123456789012345678901234567890"); const bigintFromNumber = BigInt(10); // مشابه تمامًا للطريقة 10n المعاملات الرياضية عمومًا يمكننا استخدام العدد الكبير مثل العدد العادي، فمثلًا: alert(1n + 2n); // 3 alert(5n / 2n); // 2 الرجاء ملاحظة أن القسمة 5/2 تُعيد نتيجة مُقرّبة للصفر، بدون الجزء العشري. جميع العمليات على الأعداد الكبيرة ستُعيد أعداد كبيرة. لا يمكننا جمع الأعداد الكبيرة مع الأعداد العادية: alert(1n + 2); // خطأ: لايمكننا جمع الأعداد الكبيرة مع الأعداد العادية يجب علينا تحويلها بطريقة واضحة إن لزم الأمر: باستخدام BigInt() أو Number()، هكذا: let bigint = 1n; let number = 2; // تحويل عدد عادي إلى عدد كبير alert(bigint + BigInt(number)); // 3 // تحويل عدد كبير إلى عدد عادي alert(Number(bigint) + number); // 3 تكون عمليات التحويل صامتة دائمًا، ولا تخطئ أبدًا، ولكن إذا كانت الأعداد الكبيرة ذات حجم كبير جدًا بحيث لن تتناسب مع العدد العادي، فستُحذف البتات الإضافية من العدد الكبير، لذلك يجب أن نكون حذرين عند إجراء مثل هذه التحويلات. ملاحظة: معامل الجمع الأحادي لا يطبق على الأعداد الكبيرة المعامل +value: هو طريقة معروفة لتحويل المتغير value إلى رقم. لا تدعم الأعداد الكبيرة هذه الطريقة لتجنب الفوضى: let bigint = 1n; alert( +bigint ); // خطأ لذلك يجب أن نستخدم Number() لتحويل العدد الكبير إلى عدد عادي. عمليات الموازنة تعمل عمليات الموازنة، مثل: < و> مع الأعداد الكبيرة والعادية على حدٍ سواء: alert( 2n > 1n ); // true alert( 2n > 1 ); // true لاحظ أنه نظرًا لأن الأعداد العادية والأعداد الكبيرة تنتميان لأنواع مختلفة، فيمكن أن تكون متساوية ==، ولكن ليست متساوية تمامًا ===: alert( 1 == 1n ); // true alert( 1 === 1n ); // false العمليات المنطقية تتصرف الأعداد الكبيرة مثل الأعداد العادية عندما تكون fداخل الجملة الشرطية if أو أي عمليات منطقية الأخرى. فمثلًا، في الجملة الشرطية if أدناه، تكون قيمة 0n خاطئة، والقيم الأخرى صحيحة: if (0n) { // لن يُشغّل مطلقًا } تعمل المعاملات المنطقية مثل: || و&& وغيرهما مع الأعداد الكبيرة بطريقة مشابهة للأعداد العادية: alert( 1n || 2 ); // 1 (1n is considered truthy) alert( 0n || 2 ); // 2 (0n is considered falsy) ترقيع مشاكل نقص الدعم تعد عملية ترقيع الدعم للأعداد الكبيرة صعبة بعض الشيء. والسبب هو أن العديد من معاملات جافاسكربت، مثل: + و - وما إلى ذلك تتصرف بطريقة مختلفة مع الأعداد الكبيرة بالموازنة مع الأعداد العادية. فمثلًا، تُعيد عملية القسمة على الأعداد الكبيرة دائمًا أعداد كبيرة (وتقربها إن لزم الأمر). لتنفيذ مثل هذه السلوك عن طريق ترقيع نقص الدعم في هذه الحالة سنحتاج لتحليل الشيفرة واستبدال جميع هذه المعاملات بالدوالّ المناسبة. لكن القيام بذلك أمر مُرهق وسيُكون على حساب انخفاض الأداء. بالإضافة إلى ذلك لا يوجد طريقة شائعة ومعتمد بين مجتمع المطورين لترقيع نقص الدعم. على الرغم من ذلك، هنالك بعض المحاولات الجيدة لحل هذه المشكلة مثل المكتبة JSBI. تزودنا هذه المكتبة بالأعداد الكبيرة من خلال توابعها الخاصة. ويمكننا استخدامها بدلًا من الأعداد الكبيرة الأصيلة -بدون أي طريقة لترقيع لنقص الدعم-: العملية في مكتبة JSBI العملية في الأعداد الكبيرة الأصيلة العملية a = JSBI.BigInt(789) a = BigInt(789) إنشاء عدد كبير من عدد عادي c = JSBI.add(a, b) c = a + b الجمع c = JSBI.subtract(a, b) c = a - b الطرح ... ... ... … ثم نستخدم لترقيع الدعم ملحقات إضافية (مثل الملحق الإضافي Babel) وذلك لتحويل استدعاءات المكتبة JSBI إلى الأعداد الكبيرة الأصيلة للمتصفحات التي تدعمها. بتعبير آخر، يقترح هذا النهج أن نكتب الشيفرة في JSBI بدلًا من الأعداد الكبيرة الأصيلة. وتتعامل مكتبة JSBI مع الأعداد العادية والأعداد الكبيرة داخليًا، إذ تحاكي طريقة الاستخدام الصحيحة باتباع المواصفات عن كثب، لذلك ستكون الشيفرة جاهزة للتعامل مع الأعداد الكبيرة. يمكننا استخدام شيفرة البرمجية للمكتبة JSBI على حالها للمحركات التي لا تدعم الأعداد الكبيرة والّتي تدعمها على حدٍ سواء - إذ ستُرقع نقص الدعم بتحويل الاستدعاءات إلى الأعداد الكبيرة الأصيلة. المصادر موقع مطوري موزيلا MDN. المواصفات القياسية للمتغيّر. ترجمة -وبتصرف- للفصل BigInt من كتاب 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; }
-
مفهوم Currying هو تقنية متقدمة للعمل مع الدوالّ. يستخدم في العديد من اللغات البرمجية الأخرى من بينهم جافاسكربت. Currying عبارة عن طريقة لتحويل الدوالّ التي تقيم الدالّة ذات الاستدعاء-أكثر من وسيط- f (a، b، c) لتصبح قابلة للاستدعاء -بوسيط واحد- هكذا f(a)(b)(c). تحول تقنية Currying الدالّة فقط ولا تستدعها. لنرى في البداية مثالًا، لفهم ما نتحدث عنه فهمًا أفضل، وبعدها ننتقل للتطبيقات العملية. سننشئ دالة مساعدة باسم curry (f) والتي ستُنفذّ تقنية Currying على الدالّة f التي تقبل وسيطين. بتعبير آخر، تحول الدالة curry(f) الدالّة f(a, b) ذات الوسيطين إلى دالة تعمل كوسيط واحدf(a)(b): function curry(f) { // الدالة curry(f) هي من ستُنفذّ تحويل currying return function(a) { return function(b) { return f(a, b); }; }; } // طريقة الاستخدام function sum(a, b) { return a + b; } let curriedSum = curry(sum); alert( curriedSum(1)(2) ); // 3 كما نرى، فإن التنفيذ بسيط: إنه مجرد مغلفين للوسطاء. نتيجة curry(func) هي دالة مغلّفة function(a). عندما تسمى هكذا curriedSum(1)، تُحفظ الوسطاء في البيئة اللغوية للجافاسكربت (وهي نوع مواصفات اللغة تستخدم لتعريف ارتباط المعرّفات بالمتغيّرات والدوالّ المحددة وذلك بناءً على بنية الترابط اللغوية في شيفرة ECMAScript)، وتعيد غلاف جديد function(b). ثمّ يُسمى هذا المغلّف باسم 2 نسبةً لوسطائه، ويُمرّر الاستدعاء إلى الدالّة sum(a,b) الأصليّة. من الأمثلة المتقدمة باستخدام تقنية currying هو _curry من مكتبة Lodash، والتي تُعيد غِلافًا الّذي يسمح باستدعاء الدالّة طبيعيًا وجزئيًا: function sum(a, b) { return a + b; } let curriedSum = _.curry(sum); // استخدام _.curry من مكتبة lodash alert( curriedSum(1, 2) ); // النتيجة: 3، لايزال بإمكاننا استدعاؤه طبيعيًا alert( curriedSum(1)(2) ); // النتيجة: 3، الاستدعاء الجزئي لماذا نحتاج لتقنية currying؟ لابد لنا من مثالٍ واقعي لفهم فوائد هذه التقنية. مثلًا، ليكن لدينا دالّة التسجيل log(date, importance, message) والّتي ستُنسّق المعلومات وتعرضها. مثل هذه الدوالّ مفيدة جدًا في المشاريع الحقيقة مثل: إرسال السجلات عبر الشبكة، في مثالنا سنستخدم فقط alert: function log(date, importance, message) { alert(`[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`); } لنُنفذ تقنية currying عليها! log = _.curry(log); بعد ذلك ستعمل دالّة log وفق المطلوب: log(new Date(), "DEBUG", "some debug"); // log(a, b, c) … ولكنها تعمل أيضًا بعد تحويلها بتقنية currying: log(new Date())("DEBUG")("some debug"); // log(a)(b)(c) الآن يمكننا بسهولة إنشاء دالّة مناسبة للسجلات الحالية: // logNow سيكون دالة جزئية من log مع وسيط أول ثابت let logNow = log(new Date()); // استخدامه logNow("INFO", "message"); // [HH:mm] INFO message الآن logNow هو نفس الدالة log بوسيط أول ثابت، بمعنى آخر "دالّة مطبقة جزئيًا" أو "جزئية" للاختصار. يمكننا المضي قدمًا وإنشاء دالّة مناسبة لسجلات تصحيح الأخطاء الحالية: let debugNow = logNow("DEBUG"); debugNow("message"); // [HH:mm] DEBUG message إذا: لم نفقد أي شيء بعد التحويل بتقنية currying: ولا يزال يمكننا أيضًا استدعاء الدالّة log طبيعيًا. يمكننا بسهولة إنشاء دوالّ جزئية مثل: سجلات اليوم. الاستخدام المتقدم لتقنية currying في حالة رغبتك في الدخول في التفاصيل، إليك طريقة الاستخدام المتقدمة لتقنية currying للدوال ذات الوسطاء المتعددة والّتي يمكننا استخدامها أعلاه. وهي مختصرة جدًا: function curry(func) { return function curried(...args) { if (args.length >= func.length) { return func.apply(this, args); } else { return function(...args2) { return curried.apply(this, args.concat(args2)); } } }; } إليك مثالًا لطريقة استخدامه: function sum(a, b, c) { return a + b + c; } let curriedSum = curry(sum); alert( curriedSum(1, 2, 3) ); // 6, still callable normally alert( curriedSum(1)(2,3) ); // 6, currying of 1st arg alert( curriedSum(1)(2)(3) ); // 6, full currying تبدو تقنية currying للوهلة الأولى معقدةً، ولكنها في الحقيقة سهلة الفهم جدًا. نتيجة استدعاء curry(func) هي دالّة مُغلّفة curried والّتي تبدو هكذا: // func is the function to transform function curried(...args) { if (args.length >= func.length) { // (1) return func.apply(this, args); } else { return function pass(...args2) { // (2) return curried.apply(this, args.concat(args2)); } } }; عند تشغيله، هناك فرعين للتنفيذ من الجملة الشرطية if: سيكون الاستدعاء الآن هكذا: إن كان عدد الوسطاء args المُمرّرة هو نفس العدد الدالة الأصليّة المعرّفة لدينا (func.length) أو أكثر، عندها نمرّر الاستدعاء له فقط. وإلا سيكون الاستدعاء جزئيًا: لم تُستدعى الدالّة func بعد. وإنما أعيد بدلًا منها دالّة المغلِّفة أخرى pass، والتي ستُعيد تطبيق الدالة curried مع تقديم الوسطاء السابقين مع الوسطاء الجدد. وثمّ في استدعاء الجديد سنحصل إما على دالة جزئية جديدة (إن لم يكُ عدد الوسطاء كافي) أو النتيجة النهائية. لنرى مثلًا ما يحدث في حال الاستدعاء الدالة هكذا sum(a, b, c). أي بثلاث وسطاء، وبذلك يكون sum.length = 3. عند استدعاء curried(1)(2)(3): الاستدعاء الأول curried (1) تحفظ 1 في بيئته اللغوية، ويُعيد دالّة المغلف pass. يُستدعى المغلّف pass مع الوسيط المُمرّر (2): إذ يأخذ الوسطاء السابقين (1)، ويدمجهم مع الوسيط الذي حصل عليه وهو (2) ويستدعي الدالّة curried(1, 2) مع استخدام جميع ما حصل عليه من وسطاء. وبما أن عدد الوسطاء لا يزال أقل من 3 ، فإن الدالّة curry ستُعيد الدالّة pass. يُستدعى المغلّف pass مرة أخرى مع الوسيط المُمرّر (3): ومن أجل الاستدعاء التالي pass (3) سيأخذ الوسطاء السابقين (1, 2) ويضيف لهم الوسيط 3، ليكون الاستدعاء هكذا curried(1, 2, 3)- أخيرًا لدينا ثلاث وسطاء، والّذين سيمرّروا للدالّة الأصلية. إذا لم تتوضح الفكرة حتى الآن ، فما عليك إلا تتبع تسلسل الاستدعاءات في عقلك أو على الورقة وستتوضح الأمور أكثر. ملاحظة: تعمل مع الدوالّ ثابتة الطول فقط يجب أن يكون للدالّة عدد ثابت من الوسطاء لتطبيق تقنية currying. إن استخدمت دالّةّ ما معاملات البقية، مثل: f(...args)، فلا يمكن معالجتها بهذه التقنية. ملاحظة: أكثر بقليل من مجرد تقنية تحويل انطلاقًا من التعريف، يجب على تقنية currying تحويل الدالّة sum(a, b, c) إلى sum(a)(b)(c). لكن غالبية تطبيقات هذه التقنية في جافاسكربت متقدمة، وكما وضحنا سابقًا: فهي تحافظ على الدالّة قابلة للاستدعاء بعدة تنويعات للوسطاء المُمرّرة. خلاصة تقنية Currying هو عملية تحويل تجعل f(a,b,c) قابلة للاستدعاء كـ f(a)(b)(c). عادةً ما تحافظ تطبيقات الجافاسكربت على الدوالّ بحيث تكون قابلة للاستدعاء بالشكل الطبيعي أو الجزئي إن كان عدد الوسطاء غير كافٍ. كما تسمح لنا هذه التقنية أيضًا بالحصول على دوالّ جزئية بسهولة. كما رأينا في مثال التسجيل، بعد تنفيذ هذه التقنية على الدالّة العالمية ذات الثلاث وسطاء log(date, importance, message) فإن ذلك سيمنحنا دوالّ جزئية عند استدعاؤها باستخدام وسيط واحد (هكذا log(date)) أو وسيطين (هكذا log(date, importance)). ترجمة -وبتصرف- للفصل Currying من كتاب The JavaScript language
-
تنفذ الدالّة Eval المضمّنة في اللغة الشيفرات البرمجية المُمرّرة لها كسلسلة نصية string. وصياغتها هكذا: let result = eval(code); فمثلًا: let code = 'alert("Hello")'; eval(code); // Hello يمكن أن تكون الشيفرة المُمررة للدالّة كبيرة وتحتوي على فواصل أسطر وتعريف دوالّ ومتغيّرات، وما إلى ذلك. ولكن نتيجة الدالّة Eval هي نتيجة أخر عبارة منفذة في الشيفرة. وإليك المثال التالي: let value = eval('1+1'); alert(value); // 2 let value = eval('let i = 0; ++i'); alert(value); // 1 تُنفذّ الشيفرة في البيئة الحالية للدالّة، ولذا فيمكنها رؤية المتغيرات الخارجية: let a = 1; function f() { let a = 2; eval('alert(a)'); // 2 } f(); كما يمكنها تعديل المتغيّرات الخارجية أيضًا: let x = 5; eval("x = 10"); alert(x); // النتيجة: 10، تعدلت القيمة بنجاح في الوضع الصارم، تملك الدالّة Eval بيئة متغيّرات خاصة بها. لذا فلن تظهر الدوالّ والمتغيرات، المعرفة -داخل الدالة- للخارج وإنما ستبقى بداخلها: // تذكر أن في الوضع الصارم يُشغّلُ تلقائيًا في الأمثلة الحيّة eval("let x = 5; function f() {}"); alert(typeof x); // undefined (المتحول غير مرئي هنا) // الدالّة f غير مرئية هنا أيضًا بدون تفعيل "الوضع صارم"، لن يكون للدالّة Eval بيئة متغيرات خاصة بها، ولذلك سنرى المتغيّر x والدالّة f من خارج الدالّة. استخدامات الدالة "Eval" في طرق البرمجة الحديثة، نادرًا ما تستخدم الدالّة Eval. وغالبًا ما يقال عنها أنها أصل الشرور. والسبب بسيط: إذ كانت لغة جافاسكربت منذ زمن بعيد أضعف بكثير من الآن، ولم يكُ بالإمكان فعل إيّ شيء إلا باستخدام الدالّة Eval. ولكن ذلك الوقت مضى عليه عقد من الزمن. حاليًا، لا يوجد سبب وجيه لاستخدامها. ولو أن شخصًا يستخدمها الآن فلديه الإمكانية لاستبدالها بالبنية الحديثة للغة أو [بالوحدات](). لاحظ أن إمكانية وصول الدالة eval للمتغيرات الخارجية لها عواقب سيئة. إن عملية تصغير الشيفرة (هي الأدوات تستخدم لتصغير شيفرة الجافاسكربت قبل نشرها وذلك لتصغير حجمها أكثر من ذي قبل) تعيد تسمية المتغيّرات المحلية لأسماء أقصر (مثل a وb وما إلى ذلك) لتصغير الشيفرة. وعادةً ما تكون هذه العلمية آمنة، ولكن ليس في حال استخدام الدالّة Eval، إذ يمكننا الوصول للمتغيّرات المحلية من الشيفرة المُمررة للدالّة. لذا، لن تصغّر المتغيرات التي يحتمل أن تكون مرئية من الدالة Eval. مما سيُؤثر سلبًا على نسبة ضغط الشيفرة. يُعدّ استخدام المتغيّرات المحلية في الشيفرة بداخل الدالّة Eval من الممارسات البرمجية السيئة، لأنه يزيد صعوبة صيانة الشيفرة. هناك طريقتان لضمان الأمان الكامل عند مصادفتك مثل هذه المشاكل. إذا لم تستخدم الشيفرة الممررة للدالّة المتغيرات الخارجية، فمن الأفضل استدعاء الدالّة هكذا: window.eval(...) بهذه الطريقة ستُنفذّ الشيفرة في النطاق العام: let x = 1; { let x = 5; window.eval('alert(x)'); // 1 (global variable) } إن احتاجت الشيفرة الممررة للدالة Eval لمتغيّرات خارجية، فغيّر Eval لتصبح new Function ومرّر المتغير كوسيط. هكذا: let f = new Function('a', 'alert(a)'); f(5); // 5 شرحنا في مقالٍ سابق تعلمنا كيفية استخدام [صياغة «الدالة الجديدة» new Function](). إذ باستخدام هذه الصياغة ستُنشأ دالة جديدة من السلسلة (String)، في النطاق العام. لذا لن تتمكن من رؤية المتغيرات المحلية. ولكن من الواضح أن تمريرها المتغيرات صراحة كوسطاء سيحلّ المشكلة، كما رأينا في المثال أعلاه. خلاصة سيُشغّل استدعاء الدالّة eval(code) الشيفرة البرمجية المُمرّرة ويعيد نتيجة العبارة الأخيرة. نادرًا ما تستخدم هذه الدالّة في الإصدارات الحديثة للغة، إذ لا توجد حاجة ماسّة لها. يمكننا الوصول دائمًا للمتغيّرات الخارجية في الدالّة eval. ولكن يعدّ ذلك من الممارسات السيئة. بدلًا من ذلك يمكننا استخدام الدالة eval في النطاق العام، هكذا window.eval(code). أو، إذا كانت الشيفرة الخاصة بك تحتاج لبعض البيانات من النطاق الخارجي، فاستخدم صياغة الدالّة الجديدة ومرّر لها المتغيرات كوسطاء. التمارين آلة حاسبة باستخدام الدالة Eval الأهمية: 4 أنشئ آلة حاسبة تطالب بتعبير رياضي وتُعيد نتيجته. لا داعي للتحقق من صحة التعبير في هذا التمرين. فقط قيّم التعبير وأعد نتيجته. لرؤية المثال الحي الحل لنستخدم الدالة eval لحساب التعبير الرياضي: let expr = prompt("Type an arithmetic expression?", '2*3+2'); alert( eval(expr) ); يستطيع المستخدم أيضًا إدخال أي نص أو شيفرة. لجعل الشيفرة آمنة، وحصرها للعمليات الرياضية فحسب، سنتحقق من expr باستخدام التعابير النمطية، لكي لا تحتوي إلا على الأرقام والمعاملات رياضية. ترجمة -وبتصرف- للفصل Eval: run a code string من كتاب The JavaScript language
-
يغلّف كائن الوكيل Proxy كائنًا آخر ويعترض عملياته مثل: خاصيات القراءة أو الكتابة وغيرهما. ويعالجها اختياريًا بمفرده، أو يسمح بشفافية للكائن التعامل معها بنفسه. تستخدم العديد من المكتبات وأُطر عمل المتصفح الوسطاء. وسنرى في هذا الفصل العديد من تطبيقاته العملية. الوسيط Proxy صياغته: let proxy = new Proxy(target, handler) target -- وهو الكائن الّذي سنغلِّفه يمكن أي يكون أي شيء بما في ذلك التوابع. handler -- لإعداد الوسيط: وهو كائن يحتوي على "الاعتراضات"، أي دوالّ اعتراض العمليات. مثل: الاعتراض get لاعتراض خاصية القراءة في الهدف target، الاعتراض set لاعتراض خاصية الكتابة في الهدف target، وهكذا. سيراقب الوسيط العمليات فإن كان لديه اعتراض مطابق في المعالج handler للعملية المنفذة عند الهدف، فعندها سينفذ الوسيط هذه العملية ويعالجها وإلا ستُنفذ هذه العملية من قِبل الهدف نفسه. لنأخذ مثالًا بسيطًا يوضح الأمر، لننشئ وسيطًا بدون أي اعتراضات: let target = {}; let proxy = new Proxy(target, {}); // المعالج فارغ proxy.test = 5; // الكتابة على الوسيط (1) alert(target.test); // النتيجة: 5، ظهرت الخاصية في كائن الهدف ! alert(proxy.test); // النتيجة: 5، يمكننا قرائتها من الوسيط أيضًا (2) for(let key in proxy) alert(key); // النتيجة: test, عملية التكرار تعمل (3) انطلاقًا من عدم وجود اعتراضات سيُعاد توجيه جميع العمليات في الوسيط proxy إلى كائن الهدف target. تحدد عملية الكتابة proxy.test= القيمة عند الهدف target، لاحظ السطر (1). تعيد عملية القراءة proxy.test القيمة من عند الهدف target. لاحظ السطر (2). تعيد عملية التكرار على الوسيط proxy القيم من الهدف target. لاحظ السطر (3). كما نرى، الوسيط proxy في هذه الحالة عبارة عن غِلاف شفاف حول الكائن الهدف target. لنُضف بعض الاعتراضات من أجل تفعيل المزيد من الإمكانيات. ما الذي يمكننا اعتراضه؟ بالنسبة لمعظم العمليات على الكائنات، هناك ما يسمى "الدوالّ الداخلية" في مواصفات القياسية في اللغة، والّتي تصف كيفية عمل الكائن عند أدنى مستوى. فمثلًا الدالة الداخلية [[Get]] لقراءة خاصية ما، والدالة الداخلية [[Set]] لكتابة خاصية ما، وهكذا. وتستخدم هذه الدوال من قبل المواصفات فقط، ولا يمكننا استدعاؤها مباشرة من خلال أسمائها. اعتراضات الوسيط تتدخلّ عند استدعاء هذه الدوالّ. وهي مدرجة في المواصفات القياسية للوسيط وفي الجدول أدناه. يوجد اعتراض مخصص (دالّة معالجة) لكل دالّة داخلية في هذا الجدول: يمكننا إضافة اسم الدالّة إلى المعالج handler من خلال تمريرها كوسيط new Proxy لاعتراض العملية. الدالة الداخلية الدالة المعالجة ستعمل عند [[Get]] get قراءة خاصية [[Set]] set الكتابة على خاصية [[HasProperty]] has التأكد من وجود خاصية ما in [[Delete]] deleteProperty عملية الحذف delete [[Call]] apply استدعاء تابِع [[Construct]] construct عملية الإنشاء -الباني- new [[GetPrototypeOf]] getPrototypeOf Object.getPrototypeOf [[SetPrototypeOf]] setPrototypeOf Object.setPrototypeOf [[IsExtensible]] isExtensible Object.isExtensible [[PreventExtensions]] preventExtensions Object.preventExtensions [[DefineOwnProperty]] defineProperty Object.defineProperty Object.defineProperties [[GetOwnProperty]] getOwnPropertyDescriptor Object.getOwnPropertyDescriptor for..in Object.keys/values/entries [[OwnPropertyKeys]] ownKeys Object.getOwnPropertyNames Object.getOwnPropertySymbols for..in Object/keys/values/entries 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; } تحذير: التعامل مع الثوابت تفرض علينا لغة جافاسكربت بعض الثوابت - الشروط الواجب تحقيقها من قِبل الدوالّ الداخلية والاعتراضات. معظمها للقيم المُعادة من الدوالّ: يجب أن تعيد الدالّة [[Set]] النتيجة true إذا عُدلت القيمة بنجاح، وإلا ستُعيد false. يجب أن تعيد الدالّة [[Delete]] النتيجة true إذا حُذفت القيمة بنجاح، وإلا ستُعيد false. …وهكذا، سنرى المزيد من الأمثلة لاحقًا. هنالك بعض الثوابت الأخرى، مثل: يجب أن تُعيد الدالة [[GetPrototypeOf]] المطبقة على كائن الوسيط نفس القيمة الّتي ستُعيدها الدلّة [[GetPrototypeOf]] المطبقة على كائن الهدف لكائن الوسيط، بتعبير آخر، يجب أن تعرض دائمًا قراءة النموذج الأولي لكائن الوسيط نفس قراءة النموذج الأولي لكائن الهدف. يمكن للاعتراضات التدخل في هذه العمليات ولكن لابد لها من اتباع هذه القواعد. تضمن هذه الثوابت السلوك الصحيح والمُتسق لمميّزات اللغة. وجميع هذه الثوابت موجودة في المواصفات القياسية للغة. وغالبًا لن تكسرَهم إن لم تنفذّ شيئًا غريبًا. لنرى كيف تعمل في مثال عملي. إضافة اعتراض للقيم المبدئية تعدُّ خاصيات القراءة / الكتابة من أكثر الاعتراضات شيوعًا. لاعتراض القراءة، يجب أن يكون لدى المعالج handler دالّة get(target, property, receiver). وتُشغّل عند قراءة خاصية ما، وتكون الوسطاء: target - هو كائن الهدف، والذي سيمرر كوسيط أول لِـ new Proxy، property - اسم الخاصية، receiver - إذا كانت الخاصية المستهدفة هي الجالِب (getter)، فإن receiver هو الكائن الذي سيُستخدم على أنه بديل للكلمة المفتاحية this في الاستدعاء. عادةً ما يكون هذا هو كائن proxy نفسه (أو كائن يرث منه، في حال ورِثنا من الوسيط). لا نحتاج الآن هذا الوسيط، لذلك سنشرحها بمزيد من التفصيل لاحقًا. لنستخدم الجالِب get لجلب القيم الافتراضية لكائن ما. سننشئ مصفوفة رقمية تُعيد القيمة 0 للقيم غير الموجودة. عادةً عندما نحاول الحصول على عنصر من مصفوفة، وكان هذا العنصر غير موجود، سنحصل على النتيجة غير معرّف undefined، لكننا هنا سنُغلف المصفوفة العادية داخل الوسيط والّذي سيعترضُ خاصية القراءة ويعيد 0 إذا لم تكُ الخاصية المطلوبة موجودة في المصفوفة: let numbers = [0, 1, 2]; numbers = new Proxy(numbers, { get(target, prop) { if (prop in target) { return target[prop]; } else { return 0; // القيمة الافتراضية } } }); alert( numbers[1] ); // 1 alert( numbers[123] ); // 0 (هذا العنصر غير موجود) كما رأينا، من السهل جدًا تنفيذ ذلك باعتراض الجالِب get. يمكننا استخدام الوسيط proxy لتنفيذ أي منطق للقيم "الافتراضية". تخيل أن لدينا قاموسًا (يربط القاموس مفاتيح مع قيم على هيئة أزواج) يربط العبارات مع ترجمتها: let dictionary = { 'Hello': 'Hola', 'Bye': 'Adiós' }; alert( dictionary['Hello'] ); // Hola alert( dictionary['Welcome'] ); // undefined مبدئيًا إن حاولنا الوصول إلى عبارة غير موجودة في القاموس فسيُعيد غير معرّف undefined، ولكن عمليًا إن ترك العبارة بدون مترجمة أفضل من undefined، لذا لنجعلها تُعيد العبارة بدون مترجمة بدلاً من undefined. لتحقيق ذلك، سنغلّف القاموس dictionary بالوسيط ليعترض عمليات القراءة: let dictionary = { 'Hello': 'Hola', 'Bye': 'Adiós' }; dictionary = new Proxy(dictionary, { get(target, phrase) { // اعترض خاصية القراءة من القاموس if (phrase in target) { // إن كانت موجودة في القاموس return target[phrase]; // أعد الترجمة } else { // وإلا أعدها بدون ترجمة return phrase; } } }); // ابحث عن عبارة عشوائية في القاموس! // بأسوء حالة سيُعيد العبارة غير مترجمة alert( dictionary['Hello'] ); // Hola alert( dictionary['Welcome to Proxy']); // Welcome to Proxy (يعيد العبارة بدون ترجمة) لاحظ كيف أن الوسيط يعد الكتابة على المتغير الافتراضي: dictionary = new Proxy(dictionary, ...); يجب على الوسيط استبدال كائن الهدف بالكامل في كل مكان. ولا يجب أن يشير أي شيء للكائن الهدف بعد أن يستبدلهُ الوسيط. وإلا فذلك سيُحدثُ فوضى عارمة. تدقيق المدخلات باعتراض الضابِط "set" لنفترض أننا نريد إنشاء مصفوفة مخصصة للأرقام فقط. وإن أضيفت قيمة من نوع آخر، يجب أن يظهر خطأ. يُشغّل اعتراض الضابِط set عند الكتابة على الخاصية. set(target, property, value, receiver) target - الكائن الهدف، الذي سنمرره كوسيط أول new Proxy، property - اسم الخاصية، value - قيمة الخاصية، receiver - مشابه للدالّة get، ولكن هنا لضبط الخاصيات فقط. يجب أن يُعيد الاعتراض set القيمة true إذا نجح ضبط القيمة في الخاصية، وإلا يعيد القيمة false (يُشغّل خطأ من نوع TypeError). لنأخذ مثالًا عن كيفية استخدامها للتحقق من القيم الجديدة: let numbers = []; numbers = new Proxy(numbers, { // (*) set(target, prop, val) { // لاعتراض خاصية ضبط القيمة if (typeof val == 'number') { target[prop] = val; return true; } else { return false; } } }); numbers.push(1); // أضيفت بنجاح numbers.push(2); // أضيفت بنجاح alert("Length is: " + numbers.length); // 2 numbers.push("test"); // TypeError (الدالّة 'set' في الوسيط أعادت القيمة false) alert("This line is never reached (error in the line above)"); لاحظ أن الدوال المدمجة للمصفوفات ما تزال تعمل! إذ تضاف القيم عن طريق push. فتزداد خاصية length تلقائيًا عند إضافة هذه القيم. لذا فإن الوسيط لن يكسر أي شيء. يجب علينا ألا نعيد كتابة الدوالّ المُدمجة للمصفوفات الّتي تضيف القيم مثل push وunshift، وما إلى ذلك، إن كان هدفنا إضافة عمليات تحقق، لأنهم يستخدمون داخليًا دالّة [[Set]] والّتي ستُعترضُ من قِبل الوسيط. إذن الشيفرة نظيفة ومختصرة. لا تنسَ أن تعيد القيمة true عند نجاح عملية الكتابة. كما ذكر أعلاه، هناك ثوابت ستعقّد. بالنسبة للضابط set، يجب أن يُرجع true عند نجاح عملية الكتابة. إذا نسينا القيام بذلك أو إعادة أي قيمة زائفة، فإن العملية تؤدي إلى خطأ TypeError. التكرار باستخدام تابع "ownKeys" و"getOwnPropertyDescriptor" إن الدالة Object.keys وحلقة التكرار for..in ومعظم الطرق الأخرى الّتي تستعرض خاصيات الكائن، وتستخدم الدالّة الداخلية [[OwnPropertyKeys]] للحصول على قائمة بالخاصيات يمكننا اعتراضها من خلال ownKeys. تختلف هذه الدوال وحلقات التكرار على الخاصيات تحديدًا في: Object.getOwnPropertyNames (obj): تُعيد الخاصيات غير الرمزية. Object.getOwnPropertySymbols (obj): تُعيد الخاصيات الرمزية. Object.keys/values(): تعيد الخاصيات/القيم غير الرمزية والتي تحمل راية قابلية الاحصاء enumerable (شرحنا في مقال سابق ما هي رايات الخواص وواصفاتها يمكنك الاطلاع عليه لمزيد من التفاصيل). حلقات for..in: تمرّ على الخاصيات غير الرمزية التي تحمل راية قابلية الاحصاء enumerable، وكذلك تمرّ على خاصيات النموذج الأولي (prototype). … لكن كلهم يبدأون بهذه القائمة. في المثال أدناه ، نستخدم اعتراض ownKeys لجعل حلقةfor..in تمر على user، وكذلكObject.keys وObject.values، وتتخطى الخاصيات التي تبدأ بشرطة سفلية _: let user = { name: "John", age: 30, _password: "***" }; user = new Proxy(user, { ownKeys(target) { return Object.keys(target).filter(key => !key.startsWith('_')); } }); // الاعتراض "ownKeys" سيزيل الخاصّية _password for(let key in user) alert(key); // name, then: age // نفس التأثير سيحدث على هذه التوابع: alert( Object.keys(user) ); // name,age alert( Object.values(user) ); // John,30 حتى الآن، لا يزال مثالنا يعمل وفق المطلوب. على الرغم من ذلك، إذا أضفنا خاصية ما غير موجودة في الكائن الأصلي وذلك بإرجاعها من خلال الوسيط، فلن يعيدها التابع Object.keys: let user = { }; user = new Proxy(user, { ownKeys(target) { return ['a', 'b', 'c']; } }); alert( Object.keys(user) ); // <empty> هل تسأل نفسك لماذا؟ السبب بسيط: تُرجع الدالّة Object.keys فقط الخاصيات الّتي تحمل راية قابلية الإحصاء enumerable. وهي تحقق من رايات الخاصيات لديها باستدعاء الدالّة الداخلية [[GetOwnProperty]] لكل خاصية للحصول على واصِفها. وهنا، نظرًا لعدم وجود الخاصّية، فإن واصفها فارغ، ولا يوجد راية قابلية الإحصاء enumerable، وبناءً عليه تخطت الدالّة الخاصّية. من أجل أن ترجع الدالّة Object.keys الخاصية، فيجب أن تكون إما موجودة في الكائن الأصلي، وتحمل راية قابلية الإحصاء enumerable، أو يمكننا وضع اعتراض عند استدعاء الدالة الداخلية [[GetOwnProperty]] (اعتراض getOwnPropertyDescriptor سيحقق المطلوب)، وإرجاع واصف لخاصية قابلية الإحصاء بالإيجاب هكذا enumerable: true. إليك المثال ليتوضح الأمر: let user = { }; user = new Proxy(user, { ownKeys(target) { // يستدعى مرة واحدة عند طلب قائمة الخاصيات return ['a', 'b', 'c']; }, getOwnPropertyDescriptor(target, prop) { // تستدعى من أجل كلّ خاصّية return { enumerable: true, configurable: true /* ...يمكننا إضافة رايات أخرى مع القيم المناسبة لها..." */ }; } }); alert( Object.keys(user) ); // a, b, c نعيد مرة أخرى: نضيف اعتراض [[GetOwnProperty]] إذا كانت الخاصّية غير موجودة في الكائن الأصلي. الخاصيات المحمية والاعتراض "deleteProperty" وغيره هناك إجماع كبير في مجتمع المطورين بأن الخاصيات والدوالّ المسبوقة بشرطة سفلية _ هي للاستخدام الداخلي. ولا ينبغي الوصول إليها من خارج الكائن. هذا ممكن تقنيًا: let user = { name: "John", _password: "secret" }; // لاحظ أن في الوضع الطبيعي يمكننا الوصول لها alert(user._password); // secret لنستخدم الوسيط لمنع الوصول إلى الخاصيات الّتي تبدأ بشرطة سفلية _. سنحتاج استخدام الاعتراضات التالية: get لإلقاء خطأ عند قراءة الخاصية، set لإلقاء خطأ عند الكتابة على الخاصية، deleteProperty لإلقاء خطأ عند حذف الخاصية، ownKeys لاستبعاد الخصائص التي تبدأ بشرطة سفلية _ من حلقة for..in، والدوالّ التي تستعرض الخاصيات مثل: Object.keys. هكذا ستكون الشيفرة: let user = { name: "John", _password: "***" }; user = new Proxy(user, { get(target, prop) { if (prop.startsWith('_')) { throw new Error("Access denied"); } let value = target[prop]; return (typeof value === 'function') ? value.bind(target) : value; // (*) }, set(target, prop, val) { // لاعتراض عملية الكتابة على الخاصّية if (prop.startsWith('_')) { throw new Error("Access denied"); } else { target[prop] = val; return true; } }, deleteProperty(target, prop) { // لاعتراض عملية حذف الخاصّية if (prop.startsWith('_')) { throw new Error("Access denied"); } else { delete target[prop]; return true; } }, ownKeys(target) { // لاعتراض رؤية الخاصية من خلال الحلقات أو الدوالّ return Object.keys(target).filter(key => !key.startsWith('_')); } }); // الاعتراض "get" سيمنعُ قراءة الخاصّية _password try { alert(user._password); // Error: Access denied } catch(e) { alert(e.message); } // الاعتراض "set" سيمنع الكتابة على الخاصّية _password try { user._password = "test"; // Error: Access denied } catch(e) { alert(e.message); } // الاعتراض "deleteProperty" سيمنعُ حذف الخاصّية _password try { delete user._password; // Error: Access denied } catch(e) { alert(e.message); } // الاعتراض "ownKeys" سيمنعُ إمكانية رؤية الخاصّية _password في الحلقات for(let key in user) alert(key); // name لاحظ التفاصيل المهمة في الاعتراض get، وذلك في السطر (*): get(target, prop) { // ... let value = target[prop]; return (typeof value === 'function') ? value.bind(target) : value; // (*) } لماذا نحتاج لدالّة لاستدعاء value.bind(target)؟ وسبب ذلك هو أن دوال الكائن مثل: user.checkPassword() يجب تحافظ على إمكانية الوصول للخاصّية _password: user = { // ... checkPassword(value) { // دوال الكائن يجب أن تحافظ على إمكانية الوصول للخاصية _password return value === this._password; } } تستدعي الدالة user.checkPassword() وسيط الكائن user ليحلّ محلّ this (الكائن قبل النقطة يصبح بدل this)، لذلك عندما نحاول الوصول إلى الخاصّية this._password، سينُشّط الاعتراض get ( والّذي يُشغلّ عند قراءة خاصية ما) ويلقي الخطأ. لذلك نربط سياق الدوال الخاصة بالكائن مع دوالّ الكائن الأصلي، أيّ الكائن الهدف target لاحظ السطر (*). بعد ذلك، الاستدعاءات المستقبلية ستستخدم target بدلّ this، دون الاعتراضات. هذا الحل ليس مثاليًا ولكنه يفي بالغرض، ولكن يمكن لدالّةٍ ما أن تُمرّر الكائن غير المغَلّف بالوسيط (أي الكائن الأصلي) إلى مكان آخر، وبذلك ستخرّب الشيفرةولن نستطع الإجابة على أسئلة مثل: أين يستخدم الكائن الأصلي؟ وأين يستخدم الكائن الوسيط؟ أضف إلى ذلك، يمكن للكائن أن يغلف بأكثر من وسيط (تضيف عدة وسطاء "تعديلات" مختلفة على الكائن الأصلي)، وإن مرّرنا كائن غير مغلّف بالوسيط إلى دالّة ما، ستكون هناك عواقب غير متوقعة. لذا، لا ينبغي استخدام هذا الوسيط في كل الحالات. ملاحظة: الخاصيات الخاصة بالصنف تدعم محركات جافاسكربت الحديثة الخاصيات الخاصّة بالأصناف، وتكون مسبوقة بـ #. تطرقنا لها سابقًا في مقال الخصائص والتوابع الخاصّة والمحمية. وبدون استخدام أيّ الوسيط. على الرغم من ذلك هذه الخاصيات لها مشاكلها الخاصة أيضًا. وتحديدًا، مشكلة عدم إمكانية توريث هذه الخاصيات. استخدام الاعتراض "In range" مع "has" لنرى مزيدًا من الأمثلة. لدينا الكائن range: let range = { start: 1, end: 10 }; نريد استخدام المعامل in للتحقق من أن الرقم موجود في range. إن الاعتراض has سيعترض استدعاءات in. has(target, property) target - هو الكائن الهدف، الذي سيمرر كوسيط أولnew Proxy`، property - اسم الخاصية إليك المثال: let range = { start: 1, end: 10 }; range = new Proxy(range, { has(target, prop) { return prop >= target.start && prop <= target.end; } }); alert(5 in range); // true alert(50 in range); // false تجميلٌ لغويٌّ رائع، أليس كذلك؟ وتنفيذه بسيط جدًا. تغليف التوابع باستخدام "apply" يمكننا أيضًا تغليف دالّة ما باستخدام كائن الوسيط. يعالج الاعتراض apply(target, thisArg, args) استدعاء كائن الوسيط كتابع: target: الكائن الهدف (التابع - أو الدالّة - هي كائن في لغة جافا سكربت)، thisArg: قيمة this. args: قائمة الوسطاء. فمثلًا، لنتذكر المُزخرف delay(f, ms)، الذي أنشأناه في مقال المزخرفات والتمرير. في تلك المقالة أنشأناه بدون استخدام الوسيط. عند استدعاء الدالة delay(f, ms) ستُعيد دالّة أخرى، والّتي بدورها ستوجه جميع الاستدعاءات إلى f بعد ms ملّي ثانية. إليك التطبيق المثال بالاعتماد على التوابع: function delay(f, ms) { // يعيد المغلف والذي بدوره سيوجه الاستدعاءات إلى f بعد انتهاء مهلة زمنية معينة return function() { // (*) setTimeout(() => f.apply(this, arguments), ms); }; } function sayHi(user) { alert(`Hello, ${user}!`); } // بعد عملية التغليف استدعي الدالّة sayHi بعد 3 ثواني sayHi = delay(sayHi, 3000); sayHi("John"); // Hello, John! (بعد 3 ثواني) كما رأينا، غالبًا ستعمل هذه الطريقة وفق المطلوب. تُنفذّ الدالّة المغلفة في السطر (*) استدعاءً بعد انتهاء المهلة. لكن دالة المغلف لا تعيد توجيه خاصيات القراءة أو الكتابة للعمليات أو أيّ شيء آخر. بعد عملية التغليف، يُفقدُ إمكانية الوصول لخاصيات التوابع الأصلية، مثل: name وlength وغيرها: function delay(f, ms) { return function() { setTimeout(() => f.apply(this, arguments), ms); }; } function sayHi(user) { alert(`Hello, ${user}!`); } alert(sayHi.length); // 1 (طول الدالة هو عدد الوسطاء في تعريفها) sayHi = delay(sayHi, 3000); alert(sayHi.length); // 0 (إن عدد وسطاء عند تعريف المغلّف هو 0) في الحقيقة إن إمكانيات الوسيط Proxy أقوى بكثير من ذلك، إذ إنه يعيد توجيه كل شيء إلى الكائن الهدف. لنستخدم الوسيط Proxy بدلاً من الدالّة المُغلِّفة: function delay(f, ms) { return new Proxy(f, { apply(target, thisArg, args) { setTimeout(() => target.apply(thisArg, args), ms); } }); } function sayHi(user) { alert(`Hello, ${user}!`); } sayHi = delay(sayHi, 3000); alert(sayHi.length); // 1 (*) سيعيد الوسيط توجيه عملية "get length" إلى الهدف sayHi("John"); // Hello, John! (بعد 3 ثواني) نلاحظ أن النتيجة نفسها، ولكن الآن ليس مجرد استدعاءات فقط، وإنما كل العمليات في الوسيط يُعاد توجيها إلى التوابع الأصلية. لذلك سيعيد الاستدعاء sayHi.length النتيجة الصحيحة بعد التغليف في السطر (*). وبذلك حصلنا على مُغلِّف أغنى بالمميزات من الطريقة السابقة. هنالك العديد من الاعتراضات الأخرى: يمكنك العودة لبداية المقال لقراءة القائمة الكاملة للاعتراضات. كما أن طريقة استخدامها مشابه كثيرًا لما سبق. الانعكاس الانعكاس Reflect هو عبارة عن كائن مضمّن في اللغة يبسط إنشاء الوسيط Proxy. ذكرنا سابقًا أن الدوالّ الداخلية، مثل: [[Get]] و[[Set]] وغيرها مخصصة للاستخدام في مواصفات اللغة فقط، ولا يمكننا استدعاؤها مباشرة. يمكن لكائن المنعكس Reflect من فعل ذلك إلى حد ما. إذ أن الدوالّ الخاصة به عبارة مُغلِّفات صغيرة حول الدوالّ الداخلية. فيما يلي أمثلة للعمليات واستدعاءات المنعكس Reflect الّتي ستُؤدي نفس المهمة: الدالة الداخلية الدالة المقابلة في المنعكس العملية [[Get]] Reflect.get(obj, prop) obj[prop] [[Set]] Reflect.set(obj, prop, value) obj[prop] = value [[Delete]] Reflect.deleteProperty(obj, prop) delete obj[prop] [[Construct]] Reflect.construct(F, value) new F(value) … … … فمثلًا: let user = {}; Reflect.set(user, 'name', 'John'); alert(user.name); // John تحديدًا، يتيح لنا المنعكس Reflect استدعاء العمليات (new, delete…) كتوابع هكذا (Reflect.construct,Reflect.deleteProperty, …). وهذه الامكانيات مثيرة للاهتمام، ولكن هنالك شيء آخر مهم. لكل دالّة داخلية، والّتي يمكننا تتبعها من خلال الوسيط Proxy، يوجد دالّة مقابلة لها في المنعكس Reflect، بنفس الاسم والوسطاء أي مشابه تمامًا للاعتراض في الوسيط Proxy. لذا يمكننا استخدام المنعكس Reflect لإعادة توجيه عملية ما إلى الكائن الأصلي. في هذا المثال، سيكون كلًا من الاعتراضين get وset شفافين (كما لو أنهما غير موجودين) وسيُوجهان عمليات القراءة والكتابة إلى الكائن، مع إظهار رسالة: let user = { name: "John", }; user = new Proxy(user, { get(target, prop, receiver) { alert(`GET ${prop}`); return Reflect.get(target, prop, receiver); // (1) }, set(target, prop, val, receiver) { alert(`SET ${prop}=${val}`); return Reflect.set(target, prop, val, receiver); // (2) } }); let name = user.name; // shows "GET name" user.name = "Pete"; // shows "SET name=Pete" في الشيفرة السابقة: Reflect.get: يقرأ خاصية الكائن. Reflect.set: يكتب خاصية الكائن، سيُعيد true إن نجحت، وإلا سيُعيد false. أي أن كل شيء بسيط: إذا كان الاعتراض يُعيد توجيه الاستدعاء إلى الكائن، فيكفي استدعاء Reflect.<method> بنفس الوسطاء. في معظم الحالات ، يمكننا فعل الشيء نفسه بدون Reflect، على سبيل المثال، يمكن استبدال Reflect.get(target, prop, receiver) بـ target[prop]. يوجد فروقٍ بينهم ولكن لا تكاد تذكر. استخدام الوسيط مع الجالِب لنرى مثالاً يوضح لماذا Reflect.get أفضل. وسنرى أيضًا سبب وجود الوسيط الرابع في دوالّ get/set، تحديدًا receiver والّذي لم نستخدمه بعد. لدينا كائن user مع خاصية _name وجالب مخصص لها. لنُغلِّفه باستخدام الوسيط: let user = { _name: "Guest", get name() { return this._name; } }; let userProxy = new Proxy(user, { get(target, prop, receiver) { return target[prop]; } }); alert(userProxy.name); // Guest إن الاعتراض get في هذا المثال "شفاف"، فهو يعيد الخاصية الأصلية ولا يفعل أي شيء آخر. هذا يكفي لمثالنا. يبدو أن كل شيء على ما يرام. لكن لنُزد تعقيد المثال قليلًا. بعد وراثة كائن admin من الكائن user، نلاحظ السلوك الخاطئ: let user = { _name: "Guest", get name() { return this._name; } }; let userProxy = new Proxy(user, { get(target, prop, receiver) { return target[prop]; // (*) target = user } }); let admin = { __proto__: userProxy, _name: "Admin" }; // نتوقع ظهور الكلمة: Admin alert(admin.name); // النتيجة: Guest (لماذا؟) إن قراءة admin.name يجب أن تُعيد كلمة "Admin" وليس "Guest"! ما الذي حدث؟ لعلنا أخطأنا بشيء ما في عملية الوراثة؟ ولكن إذا أزلنا كائن الوسيط، فسيكون كل شيء على ما يرام. إذًا المشكلة الحقيقية في الوسيط، تحديدًا في السطر (*). عندما نقرأ خاصّية الاسم admin.name من كائن admin، لا يحتوي كائن admin على هذه الخاصية، وبذلك ينتقل للبحث عنها في النموذج الأولي الخاص به. النموذج الأولي الخاص به هو userProxy. عند قراءة الخاصية name من الوسيط، سيُشغّل اعتراض get الخاص به، وسيُعيدih من الكائن الأصلي هكذا target[prop] في السطر (*). يؤدي الاستدعاء target [prop]، عندما يكون قيمة prop هي الجالب (getter)، سيؤدي ذلك لتشغيل الشيفرة بالسياقِ this = target. لذلك تكون النتيجة this._name من الكائن الأصلي للهدف target، أي: من user. لإصلاح مثل هذه المواقف، نحتاج إلى receiver، الوسيط الثالث للاعتراض get. إذ سيُحافظ على قيمة this الصحيحة لتُمرر بعد ذلك إلى الجالِب (getter). في حالتنا تكون قيمتها هي admin. كيفية يمرر سياق الاستدعاء للحصول الجالِب الصحيح؟ بالنسبة للتابع العادي، يمكننا استخدام call/apply، ولكن بالنسبة للجالِب فلن يستدعى بهذه الطريقة. لنستخدم الدالّة Reflect.get والتي يمكنها القيام بذلك. وكل شيء سيعمل مثلما نريد. وإليك الشكل الصحيح: let user = { _name: "Guest", get name() { return this._name; } }; let userProxy = new Proxy(user, { get(target, prop, receiver) { // receiver = admin return Reflect.get(target, prop, receiver); // (*) } }); let admin = { __proto__: userProxy, _name: "Admin" }; alert(admin.name); // Admin الآن يحافظ receiver بمرجع لقيمة this الصحيحة (وهي admin)، والّتي ستُمرر من خلال Reflect.get في السطر (*). يمكننا إعادة كتابة الاعتراض بطريقة أقصر: get(target, prop, receiver) { return Reflect.get(...arguments); } استدعاءات المنعكس Reflect لها نفس أسماء اعتراضات الوسيط وتقبل نفس وسطائه أيضًا. إذ صُمّمت خصيصًا لهذا الغرض. لذا ، فإن استخدام المنعكس return Reflect... يزودنا بالأمان والثقة لتوجيه العملية والتأكد تمامًا من أننا لن ننسَ أي شيء متعلقٌ بها. قيود الوسيط لدى الوسيط طريقة فريدة لتغيير أو تعديل سلوك الكائنات الموجودة عند أدنى مستوى. ومع ذلك، هذه الطريقة ليست مثالية. وإنما هناك قيود. كائنات مضمّنة: فتحات داخلية تستخدم العديد من الكائنات المضمنة، مثل الكائنات Map وSet وDate وPromise وغيرها ما يسمى بـ" الفتحات الداخلية ". وهي مشابهة للخاصيات، لكنها محفوظة للأغراض داخلية فقط، وللمواصفات القياسية للغة فقط. فمثلًا تخزن Map العناصر في الفتحة الداخلية [[MapData]]. وتستطيع الدوال المُضمّنة الوصول إليها مباشرةً، وليس عبر الدوالّ الداخلية مثل [[Get]]/[[Set]]. لذا فإن الوسيط Proxy لا يمكنه اعتراض ذلك. ولكن ما سبب اهتمامنا بذلك؟ إنها بكلّ الأحوال داخلية! حسنًا ، إليك المشكلة. بعد أن يغلف الكائن المضمن مثل: Map باستخدام الوسيط، لن يمتلك الوسيط هذه الفتحات الداخلية، لذلك ستفشل الدوالّ المضمنة. إليك مثالًا يوضح الأمر: let map = new Map(); let proxy = new Proxy(map, {}); proxy.set('test', 1); // Error داخليًا ، تخزن Map جميع البيانات في الفتحة الداخلية [[MapData]]. ولكن الوسيط ليس لديه مثل هذه الفتحة. تحاول الدالّة المضمنة Map.prototype.set الوصول إلى الخاصية الداخلية this.[[MapData]]، ولكن لأن this=proxy، لا يمكن العثور عليه في الوسيط proxy مما سيؤدي لفشل العملية. لحسن الحظ، هناك طريقة لإصلاحها: let map = new Map(); let proxy = new Proxy(map, { get(target, prop, receiver) { let value = Reflect.get(...arguments); return typeof value == 'function' ? value.bind(target) : value; } }); proxy.set('test', 1); alert(proxy.get('test')); // 1 (works!) الآن تعمل وفق المطلوب، لأن اعتراض get يربط خاصيات الدوالّ، مثل map.set، بالكائن الهدف (map) نفسه. بخلاف المثال السابق، فإن قيمة this داخلproxy.set (...) لن تكون proxy، وإنما map الأصلية. لذا عند التطبيق الداخلي لـ set سيحاول الوصول إلى الفتحة الداخلية هكذا this.[[MapData]]، ولحسن الحظ سينجح. ملاحظة: المصفوفة العادية Array ليس لها فتحات داخلية استثناء ملحوظ: لا تستخدم المصفوفات المضمنة Array الفتحات الداخلية. هذا لأسباب تاريخية، إذ إنها ظهرت منذ وقت طويل. لذلك لا توجد مشكلة عند تغليف المصفوفة إلى باستخدام الوسيط. الخاصيات الخاصة يحدث شيء مشابه للأمر مع خاصيات الصنف الخاصة. فمثلًا، يمكن للدالّة getName() الوصول إلى الخاصية الخاصًة #name بدون استخدام الوسيط، ولكن بعد تغليفنا للكائن باستخدام الوسيط ستتوقف إمكانية وصول الدالّة السابقة للخاصّية الخاصّة: class User { #name = "Guest"; getName() { return this.#name; } } let user = new User(); user = new Proxy(user, {}); alert(user.getName()); // Error وذلك بسبب أن التنفيذ الفعلي للخاصّيات الخاصّة يكون باستخدام الفتحات داخلية. ولا تستخدم لغة جافاسكربت الدوالّ [[Get]]/[[Set]] للوصول إليها. في استدعاء getName()، تكون قيمة this هي كائن user المغلّف بالوسيط، ولا يحتوي -هذا الكائن- على فتحة داخلية مع هذه الخاصّيات الخاصّة. وللمرة الثانية، يكون ربط الدالّة بالكائن من سيحل الأمر: class User { #name = "Guest"; getName() { return this.#name; } } let user = new User(); user = new Proxy(user, { get(target, prop, receiver) { let value = Reflect.get(...arguments); return typeof value == 'function' ? value.bind(target) : value; } }); alert(user.getName()); // Guest ومع ذلك، فإن لهذا الحل بعض العيوب، كما وضحنا سابقًا: سيعرض هذا الحل الكائن الأصلي لبعض الدوالّ، مما سيُسمح بتمريره لدوالٍ أكثر وبذلك كسر الدوالّ الأخرى المتعلقة بالوسيط. Proxy! = target إن كلًا من الوسيط والكائن الأصلي مختلفان. وهذا أمر طبيعي، أليس كذلك؟ لذلك إذا استخدمنا الكائن الأصلي كخاصية في المجموعة Set، ثم غلفناه بالوسيط، فعندئذ لن نتمكن من العثور على الوسيط: let allUsers = new Set(); class User { constructor(name) { this.name = name; allUsers.add(this); } } let user = new User("John"); alert(allUsers.has(user)); // true user = new Proxy(user, {}); // لاحظ alert(allUsers.has(user)); // false كما رأينا، بعد التغليف باستخدام الوسيط لن نتمكن من العثور على كائن user في المجموعة allUsers، لأن الوسيط هو كائن مختلف عن الكائن الأصلي. لا يمكن للوسطاء proxies اعتراض اختبار المساواة الصارم ===. يمكنها اعترلض العديد من العمليات الأخرى، مثل: new (مع build) وin (مع has) وdelete (مع deleteProperty) وما إلى ذلك. ولكن لا توجد طريقة لاعتراض اختبار المساواة الصارم للكائنات. الكائن يساوي نفسه تمامًا ولا يساوي أيّ كائنٍ آخر. لذا فإن جميع العمليات والأصناف المضمّنة في اللغة التي توازن بين الكائنات من أجل المساواة ستميز الفرق بين الكائن الأصلي والوسيط. ولا يوجد من يحل محله لإصلاح الأمر. الوسيط القابل للتعطيل وسيط revocable هو وسيط يمكن تعطيله. لنفترض أن لدينا موردًا ونود منع الوصول إليه في لحظةٍ ما. أحد الأشياء التي يمكننا فعلها هو تغليف هذا المورد بالوسيط القابل للتعطيل، بدون أي اعتراضات. بهذه الحالة سيُعيد الوسيط توجيه العمليات للكائن، ويمكننا تعطيله بأي لحظة نريدها. وصياغته: let {proxy, revoke} = Proxy.revocable(target, handler) الاستدعاء من خلال proxy سيُعيد الكائن والاستدعاء من خلال revoke سيعطل إمكانية الوصول إليه. إليك المثال لتوضيح الأمر: let object = { data: "Valuable data" }; let {proxy, revoke} = Proxy.revocable(object, {}); // مرر الوسيط لمكان آخر بدل الكائن alert(proxy.data); // Valuable data // لاحقا نستدعي التابع revoke(); // لن يعمل الوسيط الآن (لأنه معطلّ) alert(proxy.data); // Error يؤدي استدعاء revoke() لإزالة جميع المراجع الداخلية للكائن الهدف من الوسيط، وبذلك لم يعد متصل بأي شيء بعد الآن. كما يمكننا بعد ذلك كنس المخلفات من الذاكرة بإزالة الكائن الهدف. يمكننا أيضًا تخزين revoke في WeakMap، حتى نتمكن من العثور عليه بسهولة من خلال كائن الوسيط: let revokes = new WeakMap(); let object = { data: "Valuable data" }; let {proxy, revoke} = Proxy.revocable(object, {}); revokes.set(proxy, revoke); // ..لاحقًا في الشيفرة.. revoke = revokes.get(proxy); revoke(); alert(proxy.data); // Error (revoked) تفيدنا هذه الطريقة بأنه ليس علينا بعد الآن حمل revoke. وإنما يمكننا الحصول عليها من map من خلال الوسيط proxy عند الحاجة. نستخدم WeakMap بدلاً من Map هنا لأنه لن يمنع كنس المخلّفات في الذاكرة. إذا أصبح كائن الوسيط "غير قابل للوصول" (مثلًا، في حال لم يعد هناك متغيّر يشير إليه بعد الآن)، فإن WeakMap يسمح بمسحه من الذاكرة مع revoke خاصته والّتي لن نحتاج إليها بعد الآن. المصادر المواصفات القياسية للوسيط : Proxy. توثيق الوسيط الرسمي من مركز مطوري موزيلا MDN. خلاصة الوسيط Proxy عبارة عن غلاف حول كائن، يُعيد توجيه العمليات عليه إلى الكائن، ويحبس بعضها بشكل اختياري. يمكنه تغليف أي نوع من الكائنات، بما في ذلك الأصناف والدوالّ. صياغته: let proxy = new Proxy(target, { /* traps */ }); … ثم يجب علينا استخدام "الوسيط" في كل مكان بدلاً من كائن "الهدف". لا يمتلك الوكيل خاصيات أو توابع. يعترض عملية ما إذا زُودَ بالاعتراض المناسب، وإلا سيعيد توجيهها إلى كائن الهدف target. يمكننا اعتراض: قراءة (get) وكتابة (set) وحذف (deleteProperty) خاصية (حتى الخاصية غير موجودة). استدعاء دالّة ما (الاعتراض apply). المعامل new (الاعتراض construct). العديد من العمليات الأخرى (القائمة الكاملة في بداية المقال وفي التوثيق الرسمي). مما سيسمح لنا بإنشاء خاصيات ودوالّ "افتراضية"، وتطبيق قيم افتراضية، وكائنات المراقبة، وزخرفة الدوالّ، وأكثر من ذلك بكثير. يمكننا أيضًا تغليف كائن ما عدة مرات في وسطاء مختلفة، وزخرفته بمختلف أنواع الوظائف. صُممّت الواجهة البرمجية للمنعكس لتكمل عمل الوسيط. بالنسبة لأي اعتراض Proxy ، هناك استدعاء للمنعكس Reflect مقابل له بنفس الوسطاء. يجب علينا استخدامها لإعادة توجيه الاستدعاءات إلى الكائنات المستهدفة. لدى الوسيط بعض القيود: تحتوي الكائنات المضمّنة في اللغة على "فتحات داخلية"، ولا يمكن الوصول إلى تلك الأشياء بالوسيط. راجع الفقرة المخصصة لها أعلاه. وينطبق الشيء نفسه على خاصيات الصنف الخاصة، إذ تنفيذها داخليًا باستخدام الفتحات. لذا يجب أن تحتوي استدعاءات دوالّ الوسيط على الكائن المستهدف بدل this للوصول إليها. لا يمكن اعتراض اختبارات المساواة الصارمة للكائن ===. الأداء: تعتمد المقاييس على المحرك، ولكن عمومًا إن الوصول إلى الخاصية باستخدام وكيل بسيط سيستغرق وقتًا أطول بعض الشيء. عمليًا يهتم بها البعض لعدم حدوث اختناق في الأداء "عنق الزجاجة". تمارين خطأ في قراءة الخاصيات غير موجودة في الكائن الأصلي عادةً ، تؤدي محاولة قراءة خاصية غير موجودة إلى إعادة النتيجة undefined. أنشئ وسيطًا يعيد خطأ عند محاولة قراءة خاصية غير موجودة في الكائن الأصلي بدلًا من ذلك. يمكن أن يساعد ذلك في الكشف عن الأخطاء البرمجية مبكرًا. اكتب دالّة wrap(target) والّتي تأخذ كائنًا target وتعيد وسيطًا والّذي سيُضيف خصائص وظيفية أخرى. هكذا يجب أن تعمل: let user = { name: "John" }; function wrap(target) { return new Proxy(target, { /* your code */ }); } user = wrap(user); alert(user.name); // John alert(user.age); // ReferenceError: Property doesn't exist "age" الحل let user = { name: "John" }; function wrap(target) { return new Proxy(target, { get(target, prop, receiver) { if (prop in target) { return Reflect.get(target, prop, receiver); } else { throw new ReferenceError(`Property doesn't exist: "${prop}"`) } } }); } user = wrap(user); alert(user.name); // John alert(user.age); // ReferenceError: Property doesn't exist "age" الوصول إلى الدليل [-1] في فهرس المصفوفة يمكننا الوصول إلى عناصر المصفوفة باستخدام الفهارس السلبية في بعض لغات البرمجة، محسوبةً بذلك من نهاية المصفوفة. هكذا: let array = [1, 2, 3]; array[-1]; // 3, آخر عنصر في المصفوفة array[-2]; // 2, خطوة للوراء من نهاية المصفوفة array[-3]; // 1, خطوتين للوراء من نهاية المصفوفة بتعبيرٍ آخر ، فإن array[-N] هي نفس array[array.length - N]. أنشئ وسيطًا لتنفيذ هذا السلوك. هكذا يجب أن تعمل: let array = [1, 2, 3]; array = new Proxy(array, { /* your code */ }); alert( array[-1] ); // 3 alert( array[-2] ); // 2 // بقية الخصائص الوظيفية الأخرى يجب أن تبقى كما هي الحل let array = [1, 2, 3]; array = new Proxy(array, { get(target, prop, receiver) { if (prop < 0) { // حتى وإن وصلنا للمصفوفة هكذا arr[1] // إن المتغيّر prop عبارة عن سلسلة نصية لذا نحتاج لتحويله إلى رقم prop = +prop + target.length; } return Reflect.get(target, prop, receiver); } }); alert(array[-1]); // 3 alert(array[-2]); // 2 المراقب أنشئ تابع makeObservable(target) الّذي تجعل الكائن قابلاً للمراقبة 'من خلال إعادة وسيط. إليك كيفية العمل: function makeObservable(target) { /* your code */ } let user = {}; user = makeObservable(user); user.observe((key, value) => { alert(`SET ${key}=${value}`); }); user.name = "John"; // alerts: SET name=John وبعبارة أخرى، فإن الكائن الّذي سيعاد من خلال makeObservable يشبه تمامًا الكائن الأصلي، ولكنه يحتوي أيضًا على الطريقة observe(handler) الّتي ستضبط تابع المُعالج ليستدعى عند أي تغيير في الخاصية. عندما تتغير خاصية ما، يستدعى handler(key, value) مع اسم وقيمة الخاصية. ملاحظة. في هذا التمرين، يرجى الاهتمام بضبط الخاصيات فقط. يمكن تنفيذ عمليات أخرى بطريقة مماثلة. الحل يتكون الحل من جزئين: عندما يستدعى .observe(handler)، نحتاج إلى حفظ المعالج في مكان ما، حتى نتمكن من الاتصال به لاحقًا. يمكننا تخزين المعالجات في الكائن مباشرة، باستخدام الرمز الخاص بنا كمفتاح خاصية. سنحتاج لوسيط مع الاعتراض set لاستدعاء المعالجات عند حدوث أي تغيير. let handlers = Symbol('handlers'); function makeObservable(target) { // 1. هيئ مخزّن المعالجات target[handlers] = []; // احتفظ بتوابع المعالج في مصفوفة للاستدعاءات اللاحقة target.observe = function(handler) { this[handlers].push(handler); }; // 2. أنشئ وسيط ليعالج التغييرات return new Proxy(target, { set(target, property, value, receiver) { let success = Reflect.set(...arguments); // وجه العملية إلى الكائن if (success) { // إن حدث خطأ ما في ضبط الخاصية // استدعي جميع المعالجات target[handlers].forEach(handler => handler(property, value)); } return success; } }); } let user = {}; user = makeObservable(user); user.observe((key, value) => { alert(`SET ${key}=${value}`); }); user.name = "John"; ترجمة -وبتصرف- للفصل Proxy and Reflect من كتاب The JavaScript language
-
سوف تحتاج، بالتأكيد، في مرحلة ما من عملك على تطوير التطبيقات في لغة JavaScript للتعامل مع المعلومات. فمثلًا، بإمكانك تخيل العمل على التطبيقين التاليين: متجر إلكتروني – يمكن أن تتضمن المعلومات البضائع التي تباع وسلة التسوق. تطبيق محادثة فورية – يمكن أن تتضمن المعلومات، والمستخدمين، والرسائل وغيرها. إن أردنا تخزين هذه المعلومات، نحتاج إلى شيء يُخزِّنها ويحفظها لنا، وهذا ما تمثله المتغيرات تمامًا. فمثل المتغيرات كمثل الأوعية والآنية التي تحفظ وتُخزِّن كل ما يوضع فيها. المتغير المتغير هو «مَخزَن مُسمَى» (named storage) للبيانات، وهذا المخزن يقع في الذاكرة. بإمكانك استخدام المتغيرات لتخزين البضائع، والزوار وغيرها من البيانات. إن أردت إنشاء متغير جديد في لغة JavaScript، استخدم الكلمة المفتاحية let. تنشئ العبارة البرمجية التالية (أو بمعنى آخر تصرّح عن، أو تُعرِّف) متغيرًا جديدًا باسم "message": let message; الآن، نستطيع وضع بعض البيانات فيه باستخدام معامل الإسناد =: let message; message='Hello'; // 'Hello' خزِّن السلسلة النصية بعد تنفيذ هذه الشيفرة، تُحفَظ السلسلة النصية السابقة في منطقة من الذاكرة مرتبطة بهذا المتغير. ويمكننا الوصول إليها باستخدام اسم المتغير: let message mesaage = 'Hello!'; alert(message); // اظهار محتوى المتغير وللاختصار، من الممكن الدمج بين التصريح عن المتغير وإسناد قيمة معينة له في نفس السطر بالشكل التالي: let message = 'Hello!'; // التصريح عن المتغير وإسناد قيمة له alert(message); // Hello! كما من الممكن أيضًا التصريح عن عدة متغيرات في نفس السطر: let user = 'John', age = 25, message = 'Hello'; يبدو هذا أقصر، ولكن لا أنصحك باستخدام هذه الطريقة. استخدم سطرًا مستقلًا لكل متغير لتسهيل قراءة الشيفرة البرمجية. تبدو المتغيرات المتعددة الأسطر (المكتوبة على أسطر منفصلة) أطول قليلًا، ولكنها أسهل عند القراءة: let user = 'John'; let age =25; let message = 'Hello'; يصرِّح بعض المبرمجين عن عدة متغيرات بأسلوب الأسطر المتعددة بالشكل التالي: let user = 'John', age = 25, message = 'Hello'; أو حتى من الممكن استخدام أسلوب «الفاصلة أولًا» (comma-first): let user = 'John' , age = 25 , message = 'Hello'; برمجيًا، جميع هذه الحالات متماثلة وظيفيًّا. لذلك، يعود الأمر لك لتختار الأنسب والأفضل. ملاحظة حول استخدام var بدلًا من let: في السكربتات القديمة، من الممكن أن تلاحظ استخدام الكلمة المفتاحية var بدلًا من let للتصريح عن المتغيرات: var message = 'Hello'; وللكلمة المفتاحية var نفس عمل let تقريبًا. فهي تصرح أيضًا عن المتغيرات ولكنها تختلف عنها قليلًا، ومن الممكن أن تعتبرها، الطريقة «التقليدية» (old-school) للتصريح عن المتغيرات. هناك فروق دقيقة بين let و var، لكنها حاليًا غير مهمة بالنسبة لك. سندرس هذه الفروق بالتفصيل في فصل المتغير القديم. مثال واقعي للمتغيرات إن أردت فهم مبدأ عمل المتغيرات فهمًا جيدًا، فتخيل «صندوقًا»، وعليه لُصاقة باسم مميز. فمثلًا، بإمكانك تخيل المتغير message بصندوق مسمى بالاسم "message"، وداخل هذا الصندوق القيمة "Hello!" بإمكانك وضع أي قيمة في الصندوق، كما بإمكانك تغيير هذه القيمة مرارًا وتكرارًا: let message; message = 'Hello!'; message = 'World!'; // value changed alert(message); ولكن لاحظ أنه عندما تتغير القيمة، تُحذَف البيانات السابقة من المتغير. بإمكانك أيضًا التصريح عن متغيرين ونسخ البيانات من أحدهما إلى الآخر. let hello = 'Hello world!'; let message; // message إلى المتغير hello من المتغير 'Hello world' نسخ القيمة message = hello; // يملك المتغيران الآن البيانات نفسها alert(hello); //Hello world! alert(message); // Hello world! اللغات الوظيفية من الأمور التي تجدر ملاحظتها أيضًا أن اللغات البرمجية الوظيفية، مثل Scala أو Erlang، تمنع تغيير قيم المتغيرات بعد إسنادها إليها. أي عندما تخزن القيمة في الصندوق، في مثل تلك اللغات، فإنها تبقى فيه للأبد. وعند الحاجة إلى تخزين بيانات أخرى، تجبرك اللغة على خلق صندوق آخر (التصريح عن متغير جديد)، ولا يمكنك آنذاك إعادة استخدام الصندوق السابق ولا حتى إعادة الصندوق الحالي الذي أنشأته. هل يمكنك تخيل كومة الصناديق التي ستتراكم بعد فترة من الزمن؟! يبدو الأمر غريبًا وصعبًا قليلًا، ولكن فعليًا هذه اللغات قادرة على إجراء تطويرات جادة ومهمة. كما أنَ بعض المجالات تستغل هذه المحدودية وتعتبرها فائدة كما في الحوسبة المتوازية مثلًا. دراسة مثل هذه اللغات جيد لتوسيع مداركك وآفاقك (حتى لو كنت لا تخطط لاستخدامها قريبًا). تسمية المتغيرات هنالك شرطان لأسماء المتغيرات في JavaScript: يجب ألا يحتوي اسم المتغير إلا على حروف، و أرقام، و الرمزين $ و _ فقط. يمنع استخدام رقم في أول حرف من الاسم، أي يجب ألا يبدأ اسم المتغير برقم. أمثلة عن التسمية الصحيحة للمتغيرات: let userName; let test123; عندما يتألف الاسم من عدة كلمات، يستخدم عادةً أسلوب «سنام الجمل» (camelCase). حيث تكتب الكلمات متتالية دون أي فاصل، وتبدأ كل كلمة بحرف كبير: myVeryLongName. الأمر المميز الآخر هو أنه من الممكن استخدام رمز الدولار $ والشرطة التحتية _ في أسماء المتغيرات. وهما عبارة عن رمزين عاديين، كأي حرف آخر، وليس لهما معنًى خاص. أمثلة عن بعض الأسماء المسموحة: let $=1; // "$" التصريح عن متغير اسمه let _=2; // "_" والتصريح عن متغير آخر اسمه alert($ + _); // 3 وأمثلة عن بعض أسماء المتغيرات المكتوبة خطأً: let 1a; // لا يمكن بدء اسم المتغير برقم let my-name; // لا يسمح باستعمال الشرطة '-' في أسماء المتغيرات ملاحظة: حالة الأحرف مهمة فالمتغير الذي اسمه apple مختلفٌ تمامًا عن المتغير الذي اسمه AppLE. ملاحظة: استخدام أحرف من لغات أخرى غير الإنجليزية من الممكن استخدام أحرف من أي لغة غير الإنجليزية وحتى الأحرف العربية أو السيريلية أو الهيروغليفية، ولكن لا ننصحك بذلك. فمثلًا: let имя = '…'; let 我 = '…'; let رسالة = '…'; برمجيًا، لا توجد أية أخطاء هنا، ومثل هذه الأسماء ممكنة، ولكن هناك تقليد عالمي باستخدام أسماء متغيرات إنجليزية. فحتى لو كنت تكتب سكربتًا صغيرًا ولكن من الممكن أن يبقى فترة طويلة ويصل إلى مبرمجين من بلدان أخرى في وقت ما. الأسماء المحجوزة هناك قائمة من الكلمات المحجوزة، والتي ليس ممكنًا استخدامها كأسماء للمتغيرات لأنها مستخدمة من قبل اللغة نفسها. مثلًا، الكلمات التالية: let، و class، و return، و function محجوزة. والشيفرة البرمجية التالية سينتج عنها خطأ في الصياغة: let let = 5; // "let" خطأ! لا يمكن تسمية المتغير بالاسم let return = 5; // أيضًا "return" خطأ! لا يمكن تسمية المتغير بالاسم الإسناد بعيدًا عن الوضع الصارم نحتاج عادةً إلى التصريح عن متغير قبل استخدامه. ولكن سابقًا، كان من المسموح برمجيًا خلق المتغير فقط بإسناد قيمة له دون استخدام let أو أي كلمة مفتاحية أخرى. ما زال هذا ممكنًا، ولكن بشرط عدم استخدام الموجه "use strict" في السكربت وذلك لضمان استمرارية التوافق مع السكربتات السابقة. // هنا "use strict" انتبه إلى عدم استخدام الموجه num = 5; // إن لم يكن موجودًا "num" سيُنشَأ المتغير alert(num); // 5 ولكن أبقِ في ذهنك أنها طريقة سيئة، وسينتج عنها خطأ عند عملك ضمن الوضع الصارم (strict mode): "use strict" num = 5; // error: num is not defined الثوابت للتصريح عن ثابت، وهو المتغير الذي لا تتبدل قيمته، استخدم const بدلًا من let: const myBirthday = '18.04.1982'; المتغيرات التي يُصرَّح عنها باستخدام const تُسمَّى «الثوابت» ولا يمكن أن تتغير قيمتها؛ وأي محاولة للقيام بذلك ينتج عنها خطأ عند تنفيذ الشيفرة: const myBirthday = '18.0401982'; myBirthday = '01.01.2001'; // خطأ! لا يمكن إعادة إسناد قيمة لثابت يستخدم المبرمج الثابت عندما يكون متأكدًا أن قيمة المتغير لن تتبدل أو لا يجب أن تتبدل ضمن البرنامج، ويصرح عنه باستخدام const. أما الفائدة من ذلك فهي ضمان والتحقق من وصول هذه الحقيقة إلى الجميع. متى تستخدم الأحرف الكبيرة في تسمية الثوابت؟ هنالك عرف منتشر في استخدام الثوابت كأسماء مستعارة أو مرادفات للقيم صعبة الحفظ أو القيم العددية الثابتة المعروفة قبل التنفيذ. تسمى مثل هذه الثوابت باستخدام الأحرف الكبيرة والشرطة السفلية (_). كما في هذا المثال: const COLOR_RED = "#F00"; const COLOR_GREEN = '#0F0"; const COLOR_BLUE = '#00F"; const COLOR_ORANGE = '#FF7F00"; // .. عندما نريد انتقاء لون let color = COLOR_ORANGE; alert(color); // #FF7F00 والفائدة من ذلك: حفظ وتَذكُّر COLOR_ORANGE أسهل بكثير من "#FF7F00". احتمال ارتكاب خطأ بكتابة "#FF7F00" أكبر بكثير من كتابة COLOR_ORANGE. قراءة الاسم COLOR_ORANGE في الشيفرة البرمجية له معنًى جلي خلافًا لقراءة #FF7F00. ربما تتساءل الآن وأنت في حيرة، متى تستخدم الأحرف الكبيرة لتسمية الثوابت ومتى نسميها بالشكل الاعتيادي؟ حسنًا، سنوضح ذلك الآن. عندما تصرِّح عن ثابت، فهذا يعني أن قيمة هذا المتغير لن تتبدل أبدًا. ولكن ميز بين نوعين من الثوابت: الأول هو الثوابت التي تكون قيمتها معروفة قبل التنفيذ (كما في المثال السابق، فالقيمة الست عشرية للون الأحمر معروفة سابقًا وثابتة). أما النوع الثاني، فهو الثوابت التي تحسب قيمتها أثناء عمل السكربت، وخلال التنفيذ، ولكنها لا تتغير بعد إسناد قيمة لها. مثال على ذلك: const pageLoadTime = /* الوقت الذي تستغرقه الصفحة للتحميل */; قيمة الثابت pageLoadTime تكون غير معروفة قبل تحميل الصفحة، لذلك يتم تسمية الثابت بشكل عادي. ولكنه ثابت لأنَّ قيمته لا تتغير بعد إسنادها له. بمعنًى آخر، الثوابت المسماة بأحرف كبيرة تُستخدَم فقط كمرادفات للقيم صعبة الكتابة في الشيفرة البرمجية (hard-coded). تسمية الأشياء تسميةً صحيحةً بما أننا نتكلم عن المتغيرات، لابدّ أن نذكر أمرًا مهمًّا للغاية متعلق بتسمية المتغيرات. أرجوك أن تختار أسماء منطقية للمتغيرات، وأن تأخذ الوقت الكافي للتفكير بالأسماء المناسبة لها. لا تُعدُّ مهمة تسمية المتغيرات مهمةً بسيطةً، فهي واحدة من المهارات الأكثر أهمية وتعقيدًا في البرمجة. و بإمكانك بنظرة سريعة على أسماء المتغيرات المستخدمة في الشيفرة البرمجية معرفة مدى خبرة المطور الذي كتبه. في الواقع، وعند العمل على أي مشروع، فإن أغلب الوقت يمضي في تعديل وتوسيع الشيفرة البرمجية الموجودة بدلًا من كتابة شيفرة برمجية جديدة من الصفر. لذلك، عندما تعود إلى شيفرتك البرمجية بعد القيام بأشياء أخرى لفترة، يكون سهلًا بالنسبة إليك إيجاد المعلومات المسماة بشكل جيد. أو بمعنى آخر، عندما تملك المتغيرات أسماء جيدة. رجائي لك، مرةً أخرى، أن تمضي بعض الوقت في التفكير في الأسماء الصحيحة للمتغيرات قبل التصريح عنها. القيام بذلك سيعود عليك بالنفع بشكل رائع. وإليك بعض النصائح الجيدة لاتباعها في تسمية المتغيرات: استخدم أسماء يمكن للآخرين قرائتها وفهمها مثل userName أو shoppingCart. ابتعد عن الاختصارات أو الأسماء القصيرة مثل a، أو b، أو c إلا إذا كان العمل واضحًا لك بشكل جيد. اجعل أسماء المتغيرات تصف محتواها ولكن لا تبالغ كثيرًا، بل حاول أن تكون مختصرة أيضًا. وكمثال عن الأسماء السيئة data و value. مثل هذه الأسماء لا تعبر عن أي شيء. ومن الممكن استخدامها فقط إذا كان سياق الشيفرة البرمجية، يوضح بشكل بديهي واستثنائي، أي بيانات أو قيم يشير إليها المتغير. اتفق على شروط معينة ضمن فريقك أو حتى مع نفسك في تسمية المتغيرات. فإذا كان اسم زائر الموقع هو user، فإن اسم المتغيرات المرتبطة به تكون أسماؤها currentUser أو newUser عوضًا عن currentVisitor أو NewManInTown. يبدو ذلك بسيطًا، أليس كذلك؟ بالتأكيد، إنها عملية بسيطة. ولكن اختيار أسماء جيدة ومختصرة ليس بالأمر السهل في الحياة العملية، ولكن ننصحك بإيلاء أسماء المتغيرات أهمية كبيرة واختيارها بعناية مطلقة. هل تعيد استخدام المتغير أم تخلق متغيرًا جديدًا؟ أخيرًا وليس آخرًا، هناك بعض المبرمجين الكسالى الذين يفضلون إعادة استخدام المتغيرات الموجودة عوضًا عن خلق متغيرات جديدة. وبالنتيجة، تصبح متغيراتهم مثل الصناديق التي يرمي فيها الناس أشياء مختلفة من دون تغيير مسمياتهم. ماذا يوجد الآن داخل الصندوق؟ لا أحد يعلم. نحتاج إلى الاقتراب والتحقق من محتواه. قد يوفر هؤلاء المبرمجون بعض الوقت لعدم تصريحهم عن متغيرات جديدة. ولكنهم يخسرون عشرة أضعاف هذا الوقت عند استكشاف الأخطاء. المتغير الإضافي هو أمر جيد وليس سيء. وتعمل «مصغرات» (minifiers) لغة JavaScript الحالية والمتصفح على تحسين الشيفرة البرمجية بشكل جيد، وذلك لتفادي أية مشاكل في أدائها. كما أن استخدام المتغيرات المختلفة للقيم المختلفة يساعد المحرك أيضًا في تحسين شيفرتك البرمجية. الخلاصة بإمكاننا التصريح عن المتغيرات لتخزين البيانات باستخدام الكلمات المفتاحية var، أو let، أو const. let: تستخدم للتصريح عن المتغيرات في النسخة الحالية. كما يجب استخدام الوضع الصارم (strict mode) لاستخدام let في V8. var: هي الطريقة التقليدية للتصريح عن المتغيرات. عادةً، لا نستخدم هذه الطريقة على الإطلاق، ولكن سنتعرف على الفروقات الدقيقة بينها وبين let في مقال إفادة var القديمة وهذا أمر ضروري في حال اضطررت لاستخدامها. const: مثل let، ولكن قيمة المتغير لا يمكن تعديلها. كما يجب تسمية المتغيرات بطريقة تسمح لنا بفهم القيم المسندة إليها بسهولة. .task__importance { color: #999; margin-left: 30px; } .task__answer { border: 3px solid #f7f6ea; margin: 20px 0 14px; position: relative; display: block; padding: 25px 30px; } تمارين 1. العمل مع المتغيرات الأهمية: 2 صرّح عن المتغيرين: admin و name. أسند القيمة "John" إلى المتغير name. انسخ قيمة name إلى المتغير admin. اعرض قيمة المتغير admin باستخدام التنبيه alert (يجب أن يكون الخرج "John"). الحل: في الشيفرة البرمجية التالية، كل سطر يمثل أحد الأمور المطلوبة في قائمة المهام: let admin, name; // يمكننا التصريح عن متغيرين في الوقت نفسه name = "Mohammad"; admin= name; alert (admin); // "Mohammad" 2. اختيار الأسماء المناسبة الأهمية: 3 أنشئ متغيرًا وليكن اسمه كوكبنا (our planet). كيف يمكنك تسمية هذا المتغير؟ أنشئ متغيرًا لتخزين اسم الزائر الحالي للموقع. كيف بإمكانك اختيار اسم هذا المتغير؟ الحل: أولًا: المتغير باسم كوكبنا، هذا أمر سهل: let ourPlanetName = "Earth"; لاحظ أنه بالإمكان استخدام اسم أقصر من ذلك مثل planet، ولكن ذلك قد لا يكون واضحًا بالشكل الكافي، أي كوكب نقصد؟ لذلك من الجيد أن تكون أكثر تفصيلًا. على الأقل عندما يكون اسم المتغير ليس طويلًا isNotTooLong. ثانيًا: اسم المتغير الذي يحوي بيانات الزائر الحالي للموقع: let currnetUserName = "John"; مرةً أخرى، بالإمكان اختصار هذا الاسم إلى username، إذا كنت متأكدًا أن هذا الزائر هو الزائر الحالي. المحررات الحالية فيها ميزة الإكمال التلقائي والتي تجعل من السهل كتابة أسماء المتغيرات الطويلة. لا تختصر في اختيار أسماء المتغيرات، فأي اسم يتألف من 3 كلمات هو أمر عادي ومقبول. وإذا كانت ميزة الإكمال التلقائي في محررك سيئة أو غير مناسبة، لا تتردد في تغييره. 3. الثوابت بالأحرف الكبيرة؟ الأهمية: 4 تفحص الشيفرة البرمجية التالية: const birthday = '18.04.1982'; const age = someCode(birthday); هنا لدينا الثابت birthday الذي يحوي تاريخًا والثابت age الذي سيُحسَب من birthday عن طريق الشيفرة البرمجية someCode (لم يتم كتابتها للاختصار، ولأن هذه التفاصيل غير مهمة الآن). هل يكون صحيحًا استخدام الأحرف الكبيرة لتسمية الثابت birthday أو الثابت age أو كلاهما؟ const BIRTHDAY = '18.04.1982'; // هل يُكتَب اسم المتغير بالأحرف الكبيرة؟ const AGE = someCode(BIRTHDAY); // هل يُكتَب اسم المتغير بالأحرف الكبيرة؟ الحل: نستخدم عادةً الأحرف الكبيرة لتسمية الثوابت كمرادفات للقيم صعبة الكتابة في الشيفرة البرمجية (hard-coded). أو بمعنى آخر، عندما تكون قيمة الثابت معروفة قبل التنفيذ ومكتوبة بشكل مباشر في الشيفرة البرمجية. في هذه الشيفرة البرمجية، يمثل الثابت birthday هذه الحالة تمامًا. لذلك نستطيع تسميته بأحرف كبيرة. وبشكل معاكس، فإن الثابت age يُحسَب أثناء عمل السكربت، اليوم يكون لنا عمر معين ولكن يختلف عمرنا في السنة القادمة، هو ثابت من ناحية عدم تغير قيمته أثناء تنفيذ الشيفرة البرمجية ولكنه أقل ثباتًا من الثابت birthday. وبما أننا نقوم بحسابه، لذلك يفضل إبقاء اسمه بالأحرف الصغيرة. ترجمة -وبتصرف- للفصل Variables من كتاب The JavaScript Language انظر أيضًا المقال التالي: أنواع البيانات المقال السابق: الوضع الصارم: النمط الحديث لكتابة الشيفرات كامل مقالات دليل تعلم جافاسكربت
-
إن طريقة الاستيراد والتصدير التي تحدثنا عنها في الفصل السابق، تصدير الوحدات واستيرادها تدعى بالطريقة "الثابتة". إذ أنّ صياغتها بسيطة وصارمة للغاية. دعنا في البداية نوضح بعض الأمور، أولًا، لا يمكننا إنشاء أي وسطاء للتعليمة import إنشاءً ديناميكيًا. إذ يجب أن يكون مسار الوِحدة سلسلة أولية (primitive)، ولا يجب أن تكون استدعاءً لدالة معينة. فهذا لن ينجح: import ... from getModuleName(); // خطأ، مسموح استخدام السلاسل فقط ثانيًا، لا يمكننا استخدام الاستيراد المشروط (في حال حدوث شرط معين استورد مكتبة) أو الاستيراد أثناء التشغيل: if(...) { import ...; // خطأ غير مسموح بذلك! } { import ...; // خطأ، لا يمكننا وضع تعليمة import في أي كتلة } وذلك لأن تعليمتي import/export تهدفان لتوفير العمود الفقري لبنية الشيفرة. وهذا أمر جيد، إذ يمكننا تحليل بنية الشيفرة، وتجميع الوحدات وتحزيمها في ملف واحد من خلال أدوات خاصة، ويمكننا أيضًا إزالة عمليات التصدير غير المستخدمة (هزّ الشجرة -بهدف سقوط الأوراق اليابسة). هذا ممكن فقط لأن هيكلية التعليمتين import/export بسيطة وثابتة. ولكن كيف يمكننا استيراد وِحدة استيرادً ديناميكيًا بحسب الطلب؟ تعبير الاستيراد يُحمّل التعبير import (module) الوِحدة ويُرجع وعدًا، والّذي يُستبدل بكائن الوِحدة، ويحتوي هذا الأخير على كافة عمليات التصدير الخاصة بالكائن. ويُستدعى من أي مكان في الشيفرة البرمجية. ونستطيع استخدامه ديناميكيًا في أي مكان من الشيفرة البرمجية، فمثلًا: let modulePath = prompt("Which module to load?"); import(modulePath) .then(obj => <module object>) .catch(err => <loading error, e.g. if no such module>) أو يمكننا استخدام let module = await import(modulePath) إن كنا بداخل دالّة غير متزامنة. فمثلًا، ليكن لدينا الوِحدة التالية say.js: // ? say.js export function hi() { alert(`Hello`); } export function bye() { alert(`Bye`); } … ثم يكون الاستيراد الديناميكي هكذا: let {hi, bye} = await import('./say.js'); hi(); bye(); أو إذا كان say.js يحتوي على التصدير المبدئي: // ? say.js export default function() { alert("Module loaded (export default)!"); } … بعد ذلك، من أجل الوصول إليه، يمكننا استخدام الخاصية default لكائن الوِحدة: let obj = await import('./say.js'); let say = obj.default; // أو بسطرٍ واحد هكذا: // let {default: say} = await import('./say.js'); say(); إليك المثال الكامل: الملف say.js: export function hi() { alert(`Hello`); } export function bye() { alert(`Bye`); } export default function() { alert("Module loaded (export default)!"); } الملف index.html: <!doctype html> <script> async function load() { let say = await import('./say.js'); say.hi(); // Hello! say.bye(); // Bye! say.default(); // Module loaded (export default)! } </script> <button onclick="load()">Click me</button> وإليك النتائج في هذا المثال حي. لاحظ كيف تعمل عمليات الاستيراد الديناميكية في السكربتات العادية، ولا تتطلب script type ="module". لاحظ أيضًا على الرغم من أن تعليمة import() تشبه طريقة استدعاء دالّة، إلا أنها صياغة خاصة ويحدث هذا التشابه فقط لاستخدام الأقواس (على غرار super ()). لذلك لا يمكننا نسخ تعليمة import إلى متغير، أو استخدام call/apply معها. إذ هي ليست دالّة. ترجمة -وبتصرف- للفصل Dynamic imports من كتاب The JavaScript language
-
لمُوجِّهات (تعليمات) الاستيراد والتصدير أكثر من صياغة برمجية واحدة رأينا في الفصل السابق، مقدمة إلى الوحدات استعمالًا بسيطًا له، فهيًا نرى بقية الاستعمالات. التصدير قبل التصريح يمكننا أن نقول لأيّ تصريح بأنّه مُصدّر بوضع عبارة export قبله، كان التصريح عن متغيّر أو عن دالة أو عن صنف. فمثلًا، التصديرات هنا كلّها صحيحة: // تصدير مصفوفة export let months = ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; // تصدير ثابت export const MODULES_BECAME_STANDARD_YEAR = 2015; // تصدير صنف export class User { constructor(name) { this.name = name; } } ملاحظة: لا يوجد فواصل منقوطة بعد تعليمة التصدير للأصناف أو الدوالّ لاحظ أن تعليمة export قبل الصنف أو الدالة لا يجعلها تعابير الدوالّ. ولو أنه يصُدرها، لكنه لا يزال تعريفًا للدالّة أو الصنف. لا توصي معظم الأدلة التعليمية بوضع فاصلة منقوطة بعد تعريف الدوال والأصناف. لهذا السبب لا داعي للفاصلة المنقوطة في نهاية التعليمة export class والتعليمة export function: export function sayHi(user) { alert(`Hello, ${user}!`); } // لاحظ لا يوجد فاصلة منقوطة في نهاية التعريف التصدير بعيدًا عن التصريح كما يمكننا وضع عبارة export لوحدها. هنا نصرّح أولًا عن الدالتين وبعدها نُصدّرهما: // ? say.js function sayHi(user) { alert(`Hello, ${user}!`); } function sayBye(user) { alert(`Bye, ${user}!`); } export {sayHi, sayBye}; // تصدير قائمة من المتغيرات أو… يمكننا تقنيًا وضع export أعلى الدوال أيضًا. عبارة استيراد كل شيء عادةً نضع قائمة بما نريد استيراده في أقواس معقوفة import {...}، هكذا: // ? main.js import {sayHi, sayBye} from './say.js'; sayHi('John'); // Hello, John! sayBye('John'); // Bye, John! ولكن لو أردنا استيراد وحدات كثيرة، فيمكننا استيراد كلّ شيء كائنًا واحدًا باستعمال import * as <obj> هكذا: // ? main.js import * as say from './say.js'; say.sayHi('John'); say.sayBye('John'); يقول المرء من النظرة الأولى ”استيراد كلّ شيء فكرة جميلة جدًا، وكتابة الشيفرة سيكون أسرع. أساسًا لمَ نقول جهارةً ما نريد استيراده؟“ ذلك… لأسباب وجيهة. أدوات البناء الحديثة (مثل: webpack وغيرها) لنقل مثلًا بأنّا أضفنا مكتبة خارجية اسمها say.js إلى مشروعنا، وفيها دوالّ عديدة: // ? say.js export function sayHi() { ... } export function sayBye() { ... } export function becomeSilent() { ... } هكذا نستعمل واحدة فقط من دوالّ say.js في مشروعنا: // ? main.js import {sayHi} from './say.js'; …حينها تأتي أداة التحسين وترى ذلك، فتُزيل الدوال الأخرى من الشيفرة … بذلك يصغُر حجم الملف المبني. هذا ما نسميه هز الشجر (لتَسقطَ الأوراق اليابسة فقط). لو وضّحنا بالضبط ما نريد استيراده فيمكننا كتابته باسم أقصر: sayHi() بدل say.sayHi(). بكتابة قائمة الاستيراد جهارةً نستطيع أن نفهم بنية الشيفرة دون الخوض في التفاصيل (أي نعرف ما نستعمل من وحدات، وأين نستعملها). هذا يسهّل دعم الشيفرة وإعادة كتابتها لو تطلّب الأمر. استيراد كذا بالاسم كذا as يمكننا كذلك استعمال as لاستيراد ما نريد بأسماء مختلفة. فمثلًا يمكننا استيراد الدالة sayHi في المتغير المحلي hi لنختصر الكلام، واستيراد sayBye على أنّها bye: // ? main.js import {sayHi as hi, sayBye as bye} from './say.js'; hi('John'); // Hello, John! bye('John'); // Bye, John! تصدير كذا بالاسم كذا as نفس صياغة الاستيراد موجودة أيضًا للتصدير export. فلنصدّر الدوال على أنّها hi وbye: // ? say.js ... export {sayHi as hi, sayBye as bye}; الآن صارت hi وbye هي الأسماء ”الرسمية“ للشيفرات الخارجية وستُستعمل عند الاستيراد: // ? main.js import * as say from './say.js'; // لاحِظ الفرق say.hi('John'); // Hello, John! say.bye('John'); // Bye, John! التصدير المبدئي في الواقع العملي، ثمّة نوعين رئيسين من الوحدات. تلك التي تحتوي مكتبة (أي مجموعة من الدوال) مثل وحدة say.js أعلاه. وتلك التي تصرّح عن كيانٍ واحد مثل وحدة user.js التي تُصدّر class User فقط. عادةً ما يُحبّذ استعمال الطريقة الثانية كي يكون لكلّ ”شيء“ وحدةً خاصة به. ولكن هذا بطبيعة الحال يطلب ملفات كثيرة إذ يطلب كلّ شيء وحدةً تخصّه باسمه، ولكنّ هذه ليست بمشكلة، أبدًا. بل على العكس هكذا يصير التنقل في الشيفرة أسهل (لو كانت تسمية الملفات مرضية ومرتّبة في مجلدات). توفر الوِحدات طريقة لصياغة عبارة export default (التصدير المبدئي) لجعل "سطر تصدير واحد لكلّ وِحدة" تبدو أفضل. ضَع export default قبل أيّ كيان لتصديره: // ? user.js export default class User { // نُضيف ”default“ فقط constructor(name) { this.name = name; } } لكلّ ملف سطر تصدير export default واحد لا أكثر. وبعدها… نستورد الكيان بدون الأقواس المعقوفة: // ? main.js import User from './user.js'; // لا نضع {User}، بل User new User('John'); أسطر الاستيراد التي لا تحتوي الأقواس المعقوفة أجمل من تلك التي تحتويها. يشيع خطأ نسيان تلك الأقواس حين يبدأ المطورون باستعمال الوِحدات. لذا تذكّر دائمًا، يطلب سطر الاستيراد import أقواس معقوفة للكيانات المُصدّرة والتي لها أسماء، ولا يطلبها لتلك المبدئية. التصدير الذي له اسم التصدير المبدئي export class User {...} export default class User {...} import {User} from ... import User from ... 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; } يمكننا نظريًا وضع النوعين من التصدير معًا في نفس الوحدة (الذي له اسم والمبدئي)، ولكن عمليًا لا يخلط الناس عادةً بينها، بل للوِحدة إمّا تصديرات لها أسماء، أو التصدير المبدئي. ولأنّه لا يمكن أن يكون لكلّ ملف إلا تصديرًا مبدئيًا واحدًا، فيمكن للكيان الذي صُدّر ألّا يحمل أيّ اسم. فمثلًا التصديرات أسفله كلّها صحيحة مئة في المئة: export default class { // لا اسم للصنف constructor() { ... } } export default function(user) { // لا اسم للدالة alert(`Hello, ${user}!`); } // نُصدّر قيمةً واحدة دون صنع متغيّر export default ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; لا مشكلة بتاتًا بعدم كتابة الاسم إذ لا نرى export default إلّا مرّة في الملف، بهذا تعرف تمامًا أسطر import (بدون استعمال الأقواس المعقوفة) ما عليها استيراده. ولكن دون default فهذا التصدير سيُعطينا خطأً: export class { // Error! (non-default export needs a name) constructor() {} } الاسم المبدئي تُستعمل في حالات معيّنة الكلمة المفتاحية default للإشارة إلى التصدير المبدئي. فمثلًا لتصدير الدالة بنحوٍ منفصل عن تعريفها: function sayHi(user) { alert(`Hello, ${user}!`); } // كما لو أضفنا ”export default“ قبل الدالة export {sayHi as default}; أو لنقل بأنّ الوحدة user.js تُصدّر شيئًا واحدًا ”مبدئيًا“ وأخرى لها أسماء (نادرًا ما يحدث، ولكنّه يحدث): // ? user.js export default class User { constructor(name) { this.name = name; } } export function sayHi(user) { alert(`Hello, ${user}!`); } هكذا نستورد التصدير المبدئي مع ذلك الذي لديه اسم: // ? main.js import {default as User, sayHi} from './user.js'; new User('John'); وأخيرًا، حين نستورد كلّ شيء * على أنّه كائن، فستكون خاصية default هي كما التصدير المبدئي: // ? main.js import * as user from './user.js'; let User = user.default; // the default export new User('John'); كلمتين بخصوص سوء التصديرات المبدئية التصديرات التي لها أسماء تكون صريحة، أي أنّها تقول تمامًا ما الّذي يجب أن نستورده، وبذلك يكون لدينا هذه المعلومات منهم، وهذا شيء جيد. تُجبرنا التصديرات التي لها أسماء باستعمال الاسم الصحيح كما هو بالضبط لاستيراد الوحدة: import {User} from './user.js'; // ولن تعمل import {MyUser} إذ يجب أن يكون الاسم {User} بينما في حالة التصدير المبدئي نختار نحن الاسم حين نستورد الوِحدة: import User from './user.js'; // works import MyUser from './user.js'; // works too // ويمكن أيضًا أن تكون ”استورِد كل شيء“ import Anything... وستعمل بلا أدنى مشكلة هذا قد يؤدّي إلى أن يستعمل أعضاء الفريق أسماء مختلفة لاستيراد الشيء ذاته، وهذا طبعًا ليس بالجيد. عادةً ولنتجنّب ذلك ونُحافظ على اتساق الشيفرة، نستعمل القاعدة القائلة بأنّ أسماء المتغيرات المُستورَدة يجب أن تُوافق أسماء الملفات، هكذا مثلًا: import User from './user.js'; import LoginForm from './loginForm.js'; import func from '/path/to/func.js'; ... مع ذلك تنظُر بعض الفِرق لهذا الأمر على أنه عقبة للتصديرات المبدئية فتفضّل استعمال التصديرات التي لها اسم دومًا. فحتّى لو كانت نصدّر شيئًا واحدًا فقط فما زالت تُصدّره باسم دون استعمال default. كما يسهّل هذا إعادة التصدير (طالِع أسفله). إعادة التصدير تُتيح لنا صياغة ”إعادة التصدير“ export ... from ... استيراد الأشياء وتصديرها مباشرةً (ربما باسم آخر) هكذا: export {sayHi} from './say.js'; // نُعيد تصدير sayHi export {default as User} from './user.js'; // نُعيد تصدير المبدئي ولكن فيمَ نستعمل هذا أصلًا؟ لنرى مثالًا عمليًا. لنقل بأننا نكتب ”حزمة“، أي مجلدًا فيه وحدات كثيرة وأردنا تصدير بعض ميزاتها إلى الخارج (تتيح لنا الأدوات مثل NPM نشر هذه الحزم وتوزيعها)، ونعلم أيضًا أن الكثير من وحداتها ما هي إلّا وحدات مُساعِدة يمكن أن تكون بنية الملفات هكذا: auth/ index.js user.js helpers.js tests/ login.js providers/ github.js facebook.js ... ونريد عرض مزايا الحزمة باستعمال نقطة واحدة (أي الملف الأساسي auth/index.js) لتُستعمل هكذا: import {login, logout} from 'auth/index.js' الفكرة هي عدم السماح للغرباء (أي المطوّرين مستعملي الحزمة) بالتعديل على البنية الداخلية والبحث عن الملفات داخل مجلد الحزمة. نريد تصدير المطلوب فقط في auth/index.js وإخفاء الباقي عن أعين المتطفّلين. نظرًا لكون الوظيفة الفعلية المصدّرة مبعثرة بين الحزمة، يمكننا استيرادها إلى auth/index.js وتصديرها من هنالك أيضًا: // ? auth/index.js // اِستورد login/logout وصدِرهن مباشرةً import {login, logout} from './helpers.js'; export {login, logout}; // استورد الملف المبدئي كـ User وصدره من جديد import User from './user.js'; export {User}; ... والآن يمكن لمستخدمي الحزمة الخاصة بنا استيرادها هكذا import {login} from "auth/index.js". إن الصياغة export ... from ... ماهي إلا اختصار للاستيراد والتصدير: // ? auth/index.js // اِستورد login/logout وصدِرهن مباشرةً export {login, logout} from './helpers.js'; // استورد الملف المبدئي كـ User وصدره من جديد export {default as User} from './user.js'; ... إعادة تصدير التصديرات المبدئية يحتاج التصدير المبدئي لمعالجة منفصلة عند إعادة التصدير. لنفترض أن لدينا user.js، ونود إعادة تصدير الصنف User منه: // ? user.js export default class User { // ... } لن تعمل التعليمة export User from './user.js'. ما الخطأ الذي حدث؟ ولكن هذا الخطأ في صياغة! لإعادة تصدير الملفات المصدرة إفتراضيًا ، علينا كتابة export {default as User} ، كما في المثال أعلاه. تعيد التعليمة export * from './user.js' تصدير التصديرات الّتي لها أسماء فقط، ولكنها تتجاهل التصديرات المبدئية. إذا رغبنا في إعادة تصدير التصديرات المبدئية والتي لها أسماء أيضًا، فسنحتاج إلى العبارتين: export * from './user.js'; // لإعادة تصدير التصديرات الّتي لها أسماء export {default} from './user.js'; // لإعادة تصدير التصديرات المبدئية هذه الغرابة في طريقة إعادة تصدير التصديرات المبدئية هي من أحد الأسباب لجعل بعض المطورين لا يحبونها. خلاصة والآن سنراجع جميع أنواع طرق التصدير export التي تحدثنا عنها في هذا الفصل والفصول السابقة. تحقق من معلوماتك بقراءتك لهم وتذكر ما تعنيه كلُّ واحدةٍ منهم: قبل التعريف عن صنف / دالّة / ..: export [default] class/function/variable ... تصدير مستقل: export {x [as y], ...}. إعادة التصدير: export {x [as y], ...} from "module" export * from "module" (لا يُعيد التصدير المبدئي). export {default [as y]} from "module" (يعيد التصدير المبدئي). استيراد: الصادرات التي لها أسماء من الوِحدة: import {x [as y], ...} from "module" التصدير المبدئي: import x from "module" import {default as x} from "module" استيراد كل شيء: import * as obj from "module" استيراد الوحدة (وشغِّل شيفرتها البرمجية)، ولكن لا تُسندها لمتغير: import "module" لا يهم مكان وضع عبارات (تعليمات) import/export سواءً في أعلى أو أسفل السكربت فلن يغير ذلك أي شيء. لذا تقنيًا تعدُّ هذه الشيفرة البرمجية لا بأس بها: sayHi(); // ... import {sayHi} from './say.js'; // اِستورد في نهاية الملف عمليًا عادة ما تكون تعليمات الاستيراد في بداية الملف فقط لتنسيق أفضل للشيفرة. لاحظ أن تعليمتي import/export لن يعملا إن كانا في داخل جملة شرطية. لن يعمل الاستيراد الشرطي مثل هذا المثال: if (something) { import {sayHi} from "./say.js"; // Error: import must be at top level } .. ولكن ماذا لو احتجنا حقًا لاستيراد شيء ما بشروط معينة؟ أو في وقتٍ ما؟ مثل: تحميل الوِحدة عند الطلب، عندما تكون هناك حاجة إليها حقًا؟ سنرى الاستيراد الديناميكي في المقالة التالية. ترجمة -وبتصرف- للفصل Export and Import من كتاب The JavaScript language
-
سنرى سريعًا بينما تطبيقنا يكبُر حجمًا وتعقيدًا بأنّ علينا تقسيمه إلى ملفات متعدّدة، أو ”وحدات“ (module). عادةً ما تحتوي الوِحدة على صنف أو مكتبة فيها دوالّ. كانت محرّكات جافاسكربت تعمل لفترة طويلة جدًا دون أيّ صياغة وِحدات على مستوى اللغة، ولم تكن هذه بالمشكلة إذ أنّ السكربتات سابقًا كانت بسيطة وسهلة ولم يكن هناك داعٍ فعلي للوِحدات. ولكن كالعادة صارت السكربتات هذه أكثر تعقيدًا وأكبر، فكان على المجتمع اختراع طرائق مختلفة لتنظيم الشيفرات في وحدات (أو مكتبات خاصّة تُحمّل تلك الوِحدات حين الطلب). مثال: AMD: هذه إحدى نُظم المكتبات القديمة جدًا والتي كتبت تنفيذها بدايةً المكتبة require.js. CommonJS: نظام الوِحدات الذي صُنِع لخوادم Node.js. UMD: نظام وِحدات آخر (اقتُرح ليكون للعموم أجمعين) وهو متوافق مع AMD وCommonJS. أمّا الآن فهذه المكتبات صارت (أو تصير، يومًا بعد آخر) جزءًا من التاريخ، ولكن مع ذلك سنراها في السكربتات القديمة. ظهر نظام الوِحدات (على مستوى اللغة) في المعيار عام 2015، وتطوّر شيئًا فشيئًا منذئذ وصارت الآن أغلب المتصفّحات الرئيسة (كما و Node.js) تدعمه. لذا سيكون أفضل لو بدأنا دراسة عملها من الآن. ما الوحدة؟ الوِحدة هي ملف، فقط. كلّ نص برمجي يساوي وحدة واحدة. يمكن أن تُحمّل الوِحدات بعضها البعض وتستعمل توجيهات خاصة مثل التصدير export والاستيراد import لتتبادل الميزات فيما بينها وتستدعي الدوالّ الموجودة في وحدة ص، من وحدة س: تقول الكلمة المفتاحية export للمتغيرات والدوالّ بأنّ الوصول إليها من خارج الوِحدة الحالية هو أمر مُتاح. وتُتيح import استيراد تلك الوظائف من الوِحدات الأخرى. فمثلًا لو كان لدينا الملف sayHi.js وهو يُصدّر دالّةً من الدوالّ: // ? sayHi.js export function sayHi(user) { alert(`Hello, ${user}!`); } فيمكن لملف آخر استيراده واستعمالها: // ? main.js import {sayHi} from './sayHi.js'; alert(sayHi); // function... نوعها دالة sayHi('John'); // Hello, John! تتوجه تعليمة import للوِحدة ./sayHi.js عبر المسار النسبي المُمرر لها. ويسند التابع sayHi للمتغيّر الذي يحمل نفس اسم التابع. لنشغّل المثال في المتصفّح. تدعم الوِحدات كلمات مفتاحية ومزايا خاصة، لذلك علينا إخبار المتصفّح بأنّ هذا السكربت هو وِحدة ويجب أن يُعامل بهذا النحو، ذلك باستعمال الخاصية <script type="module">. هكذا: ملف index.html: <!doctype html> <script type="module"> import {sayHi} from './say.js'; document.body.innerHTML = sayHi('John'); </script> ملف say.js: export function sayHi(user) { return `Hello, ${user}!`; } يجلب المتصفّح الوِحدة تلقائيًا ويقيم الشيفرة البرمجية بداخلها (ويستورد جميع الوحدات المتعلقة بها إن لزم الأمر)، وثمّ يشغلها وتكون نتيجة ما سبق: Hello, John! ميزات الوحدات الأساسية ولكن ما الفرق بين الوِحدات والسكربتات (الشيفرات) "العادية“ تلك؟ للوِحدات ميزات أساسية تعمل على محرّكات جافاسكربت للمتصفّحات وللخوادم على حدّ سواء. الوضع الصارم الإفتراضي تستخدم الوِحدات الوضع الصارم تلقائيًا فمثلًا إسناد قيمة لمتحول غير معرّف سينتج خطأ. <script type="module"> a = 5; // خطأ </script> النطاق على مستوى الوحدات كلّ وِحدة لها نطاق عالي المستوى خاص بها. بتعبيرٍ آخر، لن يُنظر للمتغيّرات والدوالّ من الوحدات الأخرى، وإنما يكون نطاق المتغيرات محلي. نرى في المثال أدناه أنّا حمّلنا نصّين برمجيين، ويحاول الملف hello.js استعمال المتغير user المصرّح عنه في الملف user.js ولا يقدر: ملف index.html: <!doctype html> <script type="module" src="user.js"></script> <script type="module" src="hello.js"></script> ملف user.js: let user = "John"; الملف hello.js: alert(user); لاحظ أن نتيجة ما سبق هي لا شيء لأن الدالة alert لم تتعرف على المتغير user الغير موجود، فكل وحدة لها متغيرات خاصة بها ويمكنها تصدير (عبر export) ما تريد للآخرين من خارجها رؤيته، واستيراد (عبر import) ما تحتاج استعماله. لذا علينا استيراد user.js وhello.js وأخذ المزايا المطلوبة منهما بدل الاعتماد على المتغيّرات العمومية. هذه النسخة الصحيحة من الشيفرة: ملف index.html: <!doctype html> <script type="module" src="hello.js"></script> ملف user.js: export let user = "John"; الملف hello.js: import {user} from './user.js'; document.body.innerHTML = user; // John يوجد في المتصفح نطاق مستقل عالي المستوى. وهو موجود أيضًا للوحدات <script type="module">: <script type="module"> // سيكون المتغير مرئي في مجال هذه الوِحدة فقط let user = "John"; </script> <script type="module"> alert(user); // خطأ: المتغير user غير معرّف </script> ولو أردنا أن ننشئ متغيرًا عامًا على مستوى النافذة يمكننا تعيينه صراحة للمتغيّر window ويمكننا الوصول إليه هكذا window.user. ولكن لابد من وجود سبب وجيهٍ لذلك. تقييم شيفرة الوِحدة لمرة واحدة فقط لو استوردتَ نفس الوِحدة في أكثر من مكان، فلا تُنفّذ شيفرتها إلّا مرة واحدة، وبعدها تُصدّر إلى من استوردها. ولهذا توابع مهمّ معرفتها. لنرى بعض الأمثلة. أولًا، لو كان لشيفرة الوِحدة التي ستُنفّذ أيّ تأثيرات (مثل عرض رسالة أو ما شابه)، فاستيرادها أكثر من مرّة سيشغّل ذلك التأثير مرة واحدة، وهي أول مرة فقط: // ? alert.js alert("Module is evaluated!"); // نُفّذت شيفرة الوِحدة! // نستورد نفس الوِحدة من أكثر من ملف // ? 1.js import `./alert.js`; // نُفّذت شيفرة الوِحدة! // ? 2.js import `./alert.js`; // (لا نرى شيئًا هنا) في الواقع، فشيفرات الوِحدات عالية المستوى في بنية البرمجية لا تُستعمل إلّا لتمهيد بنى البيانات الداخلية وإنشائها. ولو أردنا شيئًا نُعيد استعماله، نُصدّر الوِحدة. الآن حان وقت مثال مستواه متقدّم أكثر. لنقل بأنّ هناك وحدة تُصدّر كائنًا: // ? admin.js export let admin = { name: "John" }; لو استوردنا هذه الوِحدة من أكثر من ملف، فلا تُنفّذ شيفرة الوِحدة إلّا أول مرة، حينها يُصنع كائن المدير admin ويُمرّر إلى كلّ من استورد الوِحدة. وهكذا تستلم كلّ الشيفرات كائن مدير admin واحد فقط لا أكثر ولا أقل: // ? 1.js import {admin} from './admin.js'; admin.name = "Pete"; // ? 2.js import {admin} from './admin.js'; alert(admin.name); // Pete كِلا الملفين 1.js و 2.js سيستوردان نفس الكائن والتغييرات الّتي ستحدثُ في الملف 1.js ستكون مرئية في الملف 2.js. ولنؤكد مجددًا، تُنفذّ الوِحدة لمرة واحدة فقط. وتُنشئ الوِحدات المراد تصديرها وتُشارك بين المستوردين لذا فإن تغير شيء ما في كائن admin فسترى الوِحدات الأخرى ذلك. يتيح لنا هذا السلوك ”ضبط“ الوِحدة عند أوّل استيراد لها، فنضبط خاصياتها المرة الأولى، ومتى ما استوُردت مرة أخرى تكون جاهزة. فمثلًا قد تقدّم لنا وحدة admin.js بعض المزايا ولكن تطلب أن تأتي امتيازات الإدارة من خارج كائن admin إلى داخله: // ? admin.js export let admin = { }; export function sayHi() { alert(`Ready to serve, ${admin.name}!`); } نضبط في init.js (أوّل نص برمجي لتطبيقنا) المتغير admin.name. بعدها سيراه كلّ من أراد بما في ذلك الاستدعاءات من داخل وحدة admin.js نفسها: // ? init.js import {admin} from './admin.js'; admin.name = "Pete"; ويمكن لوحدة أخرى استعمال admin.name: // ? other.js import {admin, sayHi} from './admin.js'; alert(admin.name); // Pete sayHi(); // Ready to serve, Pete! import.meta يحتوي الكائن import.meta على معلومات الوِحدة الحالية. ويعتمد محتواها على البيئة الحالية، ففي المتصفّحات يحتوي على عنوان النص البرمجي أو عنوان صفحة الوِب الحالية لو كان داخل HTML: <script type="module"> alert(import.meta.url); // عنوان URL للسكربت (عنوان URL لصفحة HTML للسكربت الضمني) </script> this في الوِحدات ليست معرّفة قد تكون هذه الميزة صغيرة، ولكنّا سنذكرها ليكتمل هذا الفصل. في الوحدات، قيمة this عالية المستوى غير معرّفة. وازن بينها وبين السكربتات غير المعتمدة على الوحدات، إذ ستكون this كائنًا عامًا: <script> alert(this); // window </script> <script type="module"> alert(this); // غير معرّف </script> الميزات الخاصة بالمتصفحات كما أن هناك عدّة فروق تخصّ المتصفحات السكربتات (المعتمدة على الوحدات) بالنوع type="module" موازنةً بتلك العادية. لو كنت تقرأ هذا الفصل لأول مرة، أو لم تكن تستعمل المحرّك في المتصفّح فيمكنك تخطّي هذا القسم. سكربتات الوِحدات مؤجلة دائمًا ما تكون سكربتات الوِحدات مؤجلة، ومشابهة لتأثير السِمة defer (الموضحة في هذا المقال)، لكل من السكربتات المضمّنة والخارجية. أي وبعبارة أخرى: تنزيل السكربتات المعتمدة على الوِحدات الخارجية <script type="module" src="..."> لا تُوقف معالجة HTML فتُحمّل بالتوازي مع الموارد الأخرى. تنتظر السكربتات المعتمدة على الوِحدات حتّى يجهز مستند HTML تمامًا (حتّى لو كانت صغيرة وحُمّلت بنحوٍ أسرع من HTML) وتُشغّل عندها. تحافظ على الترتيب النسبي للسكربتات: فالسكربت ذو الترتيب الأول ينفذّ أولًا. ويسبّب هذا بأن ”ترى“ السكربتات المعتمدة على الوِحدات صفحة HTML المحمّلة كاملة بما فيه عناصر الشجرة أسفلها. مثال: <script type="module"> alert(typeof button); // كائن (object): يستطيع السكربت رؤية العناصر أدناه // بما أن الوِحدات مؤجلة. سيُشغل السكربت بعد تحميل كامل الصفحة </script> Compare to regular script below: <script> alert(typeof button); // خطأ: الزر (button) غير معرّف. لن يستطيع السكربت رؤية العناصر أدناه // السكربت العادي سيُشغل مباشرة قبل أن يُستكمل تحميل الصفحة </script> <button id="button">Button</button> لاحِظ كيف أنّ النص البرمجي الثاني يُشغّل فعليًا قبل الأول! لذا سنرى أولًا undefined وبعدها object. وذلك بسبب كون عملية تشغيل الوِحدات مُؤجلة لذلك سننتظر لاكتمال معالجة المستند. نلاحظ أن السكربت العادي سيُشغلّ مباشرة بدون تأجيل ولذا سنرى نتائجه أولًا. علينا أن نحذر حين نستعمل الوِحدات إذ أنّ صفحة HTML تظهر بينما الوِحدات تُحمّل، وبعدها تعمل الوحدات. بهذا يمكن أن يرى المستخدم أجزاءً من الصفحة قبل أن يجهز تطبيق جافاسكربت، ويرى بأنّ بعض الوظائف في الموقع لا تعمل بعد. علينا هنا وضع ”مؤشّرات تحميل“ أو التثبّت من أنّ الزائر لن يتشتّت بهذا الأمر. خاصية Async على السكربتات المضمّنة بالنسبة للسكربتات غير المعتمدة على الوِحدات فإن خاصية async (اختصارًا لكلمة Asynchronous أي غير المتزامن) تعمل على السكربتات الخارجية فقط. وتُشغل السكربتات غير المتزامنة مباشرة عندما تكون جاهزة، بشكل مستقل عن السكربتات الأخرى أو عن مستند HTML. تعمل السكربتات المعتمدة على الوِحدات طبيعيًا في السكربتات المضمّنة. فمثلًا يحتوي السكربت المُضمن أدناه على الخاصية async، لذلك سيُشغّل مباشرة ولن ينتظر أي شيء. وهو ينفذ عملية الاستيراد (اجلب الملف ./analytics.js) وشغله عندما يصبح جاهزًا، حتى وإن لم ينتهِ مستند HTML بعد. أو السكربتات الأُخرى لا تزال معلّقة. وهذا جيد للتوابع المستقلة مثل العدادات والإعلانات ومستمع الأحداث على مستوى المستند. في المثال أدناه، جُلبت جميع التبعيات (من ضمنها analytics.js). ومن ثمّ شُغّل السكربت ولم ينتظر حتى اكتمال تحميل المستند أو السكربتات الأخرى. <script async type="module"> import {counter} from './analytics.js'; counter.count(); </script> السكربتات الخارجية تختلف السكربتات الخارجية التي تحتوي على السمة type="module" في جانبين: تنفذ السكربتات الخارجية التي لها نفس القيمة للخاصية src مرة واحدة فقط. فهنا مثلًا سيُجلب السكربت my.js وينفذ مرة واحدة فقط. <script type="module" src="my.js"></script> <script type="module" src="my.js"></script> تتطلب السكربتات الخارجية التي تجلب من مصدر مستقل (موقع مختلف عن الأساسي) ترويسات CORS والموضحة في هذا المقال. بتعبير آخر إن جُلِبَ سكربت يعتمد على الوِحدات من مصدر معين فيجب على الخادم البعيد أن يدعم ترويسات السماح بالجلب Access-Control-Allow-Origin. يجب أن يدعم المصدر المستقل Access-Control-Allow-Origin (في المثال أدناه المصدر المستقل هو another-site.com) وإلا فلن يعمل السكربت. <script type="module" src="http://another-site.com/their.js"></script> وذلك سيضمن لنا مستوى أمان أفضل إفتراضيًا. لا يُسمح بالوحدات المجردة في المتصفح، يجب أن تحصل تعليمة import على عنوان URL نسبي أو مطلق. وتسمى الوِحدات التي بدون أي مسار بالوحدات المجردة. وهي ممنوع في تعليمة import. لنأخذ مثالًا يوضح الأمر، هذا import غير صالح: import {sayHi} from 'sayHi'; // خطأ وِحدة مجردة // يجب أن تمتلك الوِحدة مسارًا مثل: './sayHi.js' أو مهما يكُ موقع هذه الوِحدة تسمح بعض البيئات، مثل Node.js أو أدوات تجميع الوِحدات باستخدام الوِحدات المجردة، دون أي مسار، حيث أن لديها طرقها الخاصة للعثور على الوِحدات والخطافات لضبطها. ولكن حتى الآن لا تدعم المتصفحات الوِحدات المجردة. التوافقية باستخدام "nomodule" لا تفهم المتصفحات القديمة طريقة استخدام الوِحدات في الصفحات type ="module".بل وإنها تتجاهل السكربت ذو النوعٍ غير المعروف. بالنسبة لهم، من الممكن تقديم نسخة مخصصة لهم باستخدام السمة nomodule: <script type="module"> alert("Runs in modern browsers"); </script> <script nomodule> alert("Modern browsers know both type=module and nomodule, so skip this"): alert("Old browsers ignore script with unknown type=module, but execute this."); </script> المتصفحات الحديثة تعرف type=module و nomodule لذا لن تنفذ الأخير بينما ستتجاهل المتصفحات القديمة الوسم ذو السِمة type=module ولكن ستنفذ وسم nomodule. أدوات البناء في الحياة الواقعية، نادرًا ما تستخدم وحدات المتصفح في شكلها "الخام". بل عادةّ نجمعها مع أداة خاصة مثل Webpack وننشرها على خادم النشر. إحدى مزايا استخدام المجمعات - فهي تمنح المزيد من التحكم في كيفية التعامل مع الوحدات، مما يسمح بالوحدات المجردة بل وأكثر من ذلك بكثير، مثل وحدات HTML/CSS. تؤدي أدوات البناء بعض الوظائف منها: جلب الوِحدة الرئيسية main، وهي الوِحدة المراد وضعها في وسم <script type ="module"> في ملف HTML. تحليل التبعيات: تحليل تعليمات الاستيراد الخاصة بالملف الرئيسي وثم للملفات المستوردة أيضًا وما إلى ذلك. إنشاء ملفًا واحدًا يحتوي على جميع الوِحدات (مع إمكانية تقسيمهُ لملفات متعددة)، مع استبدال تعليمة import الأصلية بتوابع الحزم لكي يعمل السكربت. كما تدعم أنواع وحدات "خاصة" مثل وحدات HTML/CSS. يمكننا تطبيق عمليات تحويل وتحسينات أخرى في هذه العملية مثل: إزالة الشيفرات الّتي يتعذر الوصول إليها. إزالة تعليمات التصدير غير المستخدمة (مشابهة لعملية هز الأشجار وسقوط الأوراق اليابسة). إزالة العبارات الخاصة بمرحلة التطوير مثل console وdebugger. تحويل شيفرة جافاسكربت الحديثة إلى شيفرة أقدم باستخدام وظائف مماثلة للحزمة Babel. تصغير الملف الناتج (إزالة المسافات، واستبدال المتغيرات بأسماء أقصر، وما إلى ذلك). عند استخدامنا لأدوات التجميع سيُجمع السكربت ليصبح في ملف واحد (أو ملفات قليلة) ، تُستبدل تعليمات import/export بداخل السكربتات بتوابع المُجمّع الخاصة. لذلك لا يحتوي السكربت "المُجَمّع" الناتج على أي تعليمات import/export، ولا يتطلب السِمة type="module"، ويمكننا وضعه في سكربت عادي: في المثال أدناه لنفترض أننا جمعّنا الشيفرات في ملف bundle.js باستخدام مجمع حزم مثل: Webpack. <script src="bundle.js"></script> ومع ذلك يمكننا استخدام الوِحدات الأصلية (في شكلها الخام). لذلك لن نستخدم هنا أداة Webpack: يمكنك التعرف عليها وضبطها لاحقًا. خلاصة لنلخص المفاهيم الأساسية: الوِحدة هي مجرد ملف. لجعل تعليمتي import/export تعملان، ستحتاج المتصفحات إلى وضع السِمة التالية <script type ="module">. تحتوي الوِحدات على عدة مُميزات: مؤجلة إفتراضيًا. تعمل الخاصية Async على السكربتات المضمّنة. لتحميل السكربتات الخارجية من مصدر مستقل، يجب استخدام طريقة (المَنفذ / البروتوكول / المجال)، وسنحتاج لترويسات CORS أيضًا. ستُتجاهل السكربتات الخارجية المكررة. لكل وِحدة من الوِحدات نطاق خاص بها، وتتبادلُ الوظائف فيما بينها من خلال استيراد وتصدير الوِحدات import/export. تستخدم الوِحدات الوضع الصارم دومًا use strict. تُنفذ شيفرة الوِحدة لمرة واحدة فقط. وتُصدر إلى من استوردها لمرة واحدة أيضًا، ومن ثمّ تُشارك بين المستوردين. عندما نستخدم الوحدات، تنفذ كل وِحدة وظيفة معينة وتُصدرها. ونستخدم تعليمة import لاستيرادها مباشرة عند الحاجة. إذ يُحمل المتصفح السكربت ويقيّمه تلقائيًا. وبالنسبة لوضع النشر، غالبًا ما يستخدم الناس مُحزّم الوِحدات مثل Webpack لتجميع الوِحدات معًا لرفع الأداء ولأسباب أخرى. سنرى في الفصل التالي مزيدًا من الأمثلة عن الوِحدات، وكيفية تصديرها واستيرادها. ترجمة -وبتصرف- للفصل Modules, introduction من كتاب The JavaScript language
-
تُتيح لنا المُكرّرات غير المتزامنة المرور على البيانات التي تأتينا على نحوٍ غير متزامن متى لزم، مثل حين نُنزّل شيئًا كتلةً كتلةً عبر الشبكة. المولّدات غير المتزامنة تجعل من ذلك أسهل فأسهل. لنرى مثالًا أولًا لنفهم الصياغة، بعدها نرى مثالًا من الحياة العملية. المكررات غير المتزامنة تتشابه المُكرّرات غير المتزامنة مع تلك العادية، بفروق بسيطة في الصياغة. فكائن المُكرّر العادي (كما رأينا في الفصل الكائنات المكرَّرة (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; }
-
تُعيد الدوالّ العادية قيمة واحدة فقط لا غير (أو لا تُعيد شيئًا). بينما يمكن للمولّدات إعادة (أو إنتاج 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()، إذ أن المولدات قابلة للتكرار iterable، فيمكننا المرور على عناصره عبر 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 (كما لاحظنا من الأمثلة أعلاه). ويمكنها أيضًا بدء (أو رمي) خطأ أيضًا. هذا طبيعي إذ الأخطاء هي نواتج، نوعًا ما. علينا استدعاء التابِع generator.throw(err) لتمرير الأخطاء إلى عبارة yield. في هذه الحال يُرمى الخطأ err عند السطر الذي فيه yield. فمثلًا تؤدّي عبارة "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
-
توجد صياغة مميّزة للعمل مع الوعود بنحوٍ أكثر سهولة تُدعى 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