اذهب إلى المحتوى

أسامة دمراني

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

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

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

  • عدد الأيام التي تصدر بها

    24

كل منشورات العضو أسامة دمراني

  1. سنتحدث في المقالات الباقية من هذه السلسلة عن متصفحات الويب، فبدونها لم تكن جافاسكربت لتكون أصلًا، وإذا وُجدت لسبب ما فلم يكن أحد ليلتفت إليها. بُنيت تقنية الشبكات لتكون غير مركزية من البداية، سواءً على الصعيد الفني أو بالنسبة للطريقة التي تطورت بها، فقد أضافت العديد من الجهات الموفِّرة للمتصفحات وظائفًا جديدةً لأغراض محددة بعينها أحيانًا، وبدون تفكير أحيانًا أخرى لتعطينا وظائف ومزايا جديدة في صور سيئة، ثم يدور الزمن بهذه وتلك إلى أن يتبناها أحد ما ثم تصير معيارًا قياسيًا في النهاية. ونرى أنّ هذا قد يكون نعمةً ونقمةً في الوقت نفسه، فمن الجيد ألا يكون لديك تحكم مركزي في نظام مثل الإنترنت ويكون تطوره على يد جهات مختلفة تتعاون أحيانًا فيما بينها بصورة طفيفة، وتكاد تناوش بعضها أحيانًا أخرى؛. لكن من الناحية الأخرى، تعني الطريقة العفوية التي تطور بها الإنترنت أنّ النظام الناتج لن يكون هو الآلية التي يتوقعها البعض من حيث الاتساق النظامي الداخلي، فبعض أجزاء هذا النظام مشوشة إلى حد الحيرة. الشبكات والإنترنت وُجِدت شبكات الحواسيب منذ خمسينات القرن الماضي، ذلك أنك إذا وصلت حاسوبين أو أكثر بسلك لنقل البيانات فيما بينها، فستستطيع فعل الأعاجيب بهذه الشبكة الصغيرة، وعليه فهذه الأعاجيب تزداد عجبًا حين نوصل جميع الحواسيب في العالم ببعضها البعض. وقد بدأت التقنية التي تطبق هذه الرؤية في التطوير في الثمانينيات، ثم حصلنا على الشبكة التي تسمى بالإنترنت، وقد كانت كما حلمنا بها بالضبط. يستخدِم الحاسوب هذه الشبكة ليرسل بِتَّات من البيانات إلى حاسوب آخر، ويجب على كلا الحاسوبين معرفة ماذا يفترض لهذه البِتَّات أن تكون وماذا تمثل من أجل تحقيق تواصل فعال، ويتوقف معنى أي تسلسل من البتات على نوع الشيء المراد التعبير عنه وعلى آلية الترميز encoding mechanism المستخدَمة. يصف بروتوكول الشبكة network protocol أسلوبًا من التواصل عبر أي شبكة، فهناك بروتوكولات لإرسال البريد الإلكتروني وجلبه ومشاركة الملفات، وحتى التحكم في الحواسيب التي قد تكون مصابةً ببرمجيات خبيثة، فيُستخدَم بروتوكول نقل النصوص الفائقة HTTP مثلًا -وهو اختصار لـ Hypertext Transfer Protocol- لجلب الموارد المسمّاة وكتل المعلومات مثل صفحات الويب أو الصور، كما يشترط على الجزء الذي سينشئ الطلب البدء بسطر يشبه السطر التالي مسميًا المورد وإصدار البروتوكول الذي يريد استخدامه: GET /index.html HTTP/1.1 هناك قواعد كثيرة تحكم الطريقة التي يمكن للطالب requester فيها إدخال بيانات أو معلومات في الطلب، والطريقة التي يحزِّم الطرف الآخر بها الموارد المطلوبة، وهو مستقبِل الطلب الذي يعيد هذه الموارد إلى طالبها.. تُبنى أغلب البروتوكولات على بروتوكولات أخرى، حيث يتعامل بروتوكول HTTP مع الشبكة كما لو كانت أداة يضع فيها البِتّات ويتوقع منها الوصول إلى الوجهة الصحيحة بالترتيب المتوقع لها، لكن الواقع المشاهَد يقول عكس ذلك كما رأينا في مقال البرمجة غير المتزامنة في جافاسكريبت. ويعالِج بروتوكول التحكم في النقل TCP -اختصارًا لـ Transmission Control Protocol- هذه المشكلة بما أن كل الحواسيب المتصلة بالإنترنت تفهمه، وقد بُنيت أغلب عمليات التواصل في الإنترنت عليه أصلًا. فكرة عمل بروتوكول TCP هي أنّ الحاسوب يجب أن يكون في وضع انتظار أو استماع للحواسيب الأخرى حتى تكلمه أو تراسله، كما يجب أن يكون لكل مستمِع رقمًا يسمى منفَذًا port ويرتبط به من أجل أن تستطيع استماع عمليات تواصل مختلفة في الوقت نفسه على آلة واحدة، كما تحدد أغلب البروتوكولات المنفَذ الذي يجب استخدامه افتراضيًا، فحين نريد إرسال بريد إلكتروني باستخدام بروتوكول SMTP مثلًا، فيجب أن تستمع الآلة التي نرسله من خلالها إلى المنفَذ 25، ثم سينشئ حاسوب آخر حينئذ اتصالًا من خلال التوصيل بالآلة الهدف مستخدمًا رقم المنفَذ الصحيح، فإذا استطاع الوصول إلى الآلة الهدف وكانت تستمع لهذا المنفَذ، فسنقول أنّ الاتصال تحقق بنجاح، كما يُسمى الحاسوب المستمع هنا بالخادم server والحاسوب المتصَل به بالعميل client. يعمل مثل ذلك الاتصال على أساس خط أنابيب ثنائي الاتجاه يمكن للبِتّات أن تتدفق فيه من كلا الاتجاهين، مما يعني استطاعة كلا الحاسوبين إرسال البيانات واستلامها عبره، وبمجرد أن تُنقل البِتّات بنجاح، فسيمكن للآلة التي على الطرف الآخر قراءتها مرةً أخرى، وهذا النموذج مريح جدًا، بل يمكن القول أنّ TCP هو النموذج المجرَّد للشبكة. الشبكة العنكبوتية العالمية The Web تُعَدّ الشبكة العنكبوتية العالمية -أو الويب- مجموعةً من البروتوكولات والصيغ التي تسمح لنا بزيارة صفحات الويب داخل متصفح ما، وتشير كلمة الشبكة العنكبوتية فيها إلى حقيقة إشارة هذه الصفحات إلى بعضها بعضًا وارتباطها ببعضها، وبالتالي تكوّن شبكةً عملاقةً يستطيع المستخدِم التنقل خلالها بحُرية، كما يجدر بنا الإشارة هنا إلى أنّ هذه الشبكة ليست هي كل الإنترنت، فالأخير أوسع وأشمل. لكي تكون جزءًا من هذه الشبكة، لا تحتاج إلا إلى توصيل آلة بالإنترنت وتجعلها تستمع إلى المنفَذ 80 ببروتوكول HTTP كي تستطيع الحواسيب الأخرى طلب مستندات وملفات منها، ويُسمى كل مستند على الشبكة بمحدِّد موقع الموارد الموحَّد Uniform Resource Locator -أو URL اختصارًا-، حيث يكون أشبه بما يلي: http://eloquentjavascript.net/13_browser.html | | | | protocol server path يخبرنا أول جزء أنّ هذا الرابط يستخدم بروتوكول HTTP على خلاف بروتوكول HTTP المشفَّر مثلًا التي سيكون هكذا: https://‎، ثم يعرِّف الجزءُ الذي يليه الخادمَ الذي نطلب منه المستند، ولدينا في النهاية مسار من سلسلة نصية يعرِّف المستند المحدَّد أو المورد الذي نريد. تحصل الآلات المتصلة بالإنترنت على عنوان IP يكون رقمًا يمكن استخدامه لإرسال رسائل إلى هذه الآلة، حيث يبدو هكذا: 149.210.142.219 أو 2001:4860:4860‎::‎8888 غير أن هذه الأرقام المتتالية يصعب حفظها وكتابتها، لهذا تستطيع تسجيل اسم نطاق لأحد تلك العناوين أو بعضها، وقد سجلنا eloquentjavascript.net مثلًا لنشير إلى عنوان IP لآلة نتحكم فيها ونستطيع استخدام اسم النطاق حينئذ لإرسال صفحات الويب لمن يطلبها. إذا كتبتَ محدِّد المواقع ذاك في شريط عنوان المتصفح، فسيحاول المتصفح جلب المستند الذي في ذلك الرابط وعرضه، فيبدأ بالنظر في العنوان الذي يشير إليه eloquentjavascript.net، ثم يستخدِم بروتوكول HTTP ليحقق اتصالًا بالخادم الذي في ذلك العنوان ويطلب المورِد ‎/13_browser.html، وإذا تم ذلك كله فسيرسل الخادم حينئذ مستندًا يعرضه لك المتصفح على الشاشة. HTML تُهيأ صفحات الويب التي تراها أمامك على الشاشة بتنسيق يتكون من تراكيب محددة، واللغة التي نكتب بها تلك التراكيب اسمها HTML أو Hypertext Markup Language وتعني لغة ترميز النصوص الفائقة، ويحتوي المستند العادي المكتوب بهذه اللغة على نصوص ووسوم tags، وتحدد التركيب البنائي لتلك النصوص كما تصف الأنواع المختلفة لها سواءً كانت روابط أو فقرات أو عناوين، وتبدو هذه اللغة هكذا: <!doctype html> <html> <head> <meta charset="utf-8"> <title>My home page</title> </head> <body> <h1>My home page</h1> <p>Hello, I am Marijn and this is my home page.</p> <p>I also wrote a book! Read it <a href="http://eloquentjavascript.net">here</a>.</p> </body> </html> توفِّر الوسوم معلومات عن بنية المستند وتحاط بقوسين محددين هما رمزا "أصغر من" و"أكبر من" في الرياضيات كما ترى في المثال أعلاه؛ أما بقية النص فيكون نصًا عاديًا، كما يبدأ المستند بوسم ‎<!doctype html>‎ الذي يخبر المتصفح أن يفسر الصفحة وفق لغة HTML الحديثة وليس الإصدارات القديمة المختلفة منها والتي تكون عادةً قبل HTML5. تمتلك مستندات HTML في الغالب ترويسةً head ومتن body، كما تحتوي الترويسة على معلومات عن المستند؛ أما المتن فيحتوي على النصوص التي في المستند نفسه، وفي هذه الحالة تقول الترويسة أنّ عنوان هذه المستند هو "My home page" وأنها تستخدِم ترميز UTF-8 الذي هو أسلوب نتبعه لترميز نصوص اليونيكود على أساس بيانات ثنائية. يحتوي متن المستند على ترويسة واحدة، وهي <h1> التي تعني الترويسة الأولى، و<h2> حتى <h6> لتشير إلى ترويسات فرعية، كما يحتوي على فقرتين اثنتين محدَّدتين بوسم <p>. تأتي الوسوم في أشكال كثيرة، فيبدأ العنصر مثل المتن أو الفقرة أو الرابط بوسم افتتاحي مثل <p>وينتهي بوسم غالق مثل ‎</p>‎، وقد تكون بعض الوسوم الافتتاحية فيها معلومات أكثر مثل وسم <a> في صورة أزواج من name="value"‎، وتسمى هذه المعلومات بالسمات attributes، وفي حالتنا فإن وجهة الرابط توضَّح بالآتي: href="http://eloquentjavascript.net"‎ وتشير href هنا إلى مرجع لنص فائق hypertext reference. قد لا تغلف بعض الوسوم أيّ شيء ولا تحتاج إلى إغلاقها، ويُعَدّ وسم البيانات الوصفية metadata الذي يأتي بصورة الآتية مثالًأ على ذلك: ‎<meta charset="utf-8">‎ إذا أردنا كتابة أقواس محددة <> داخل نص مستند لتظهر كما هي من غير تفسيرها على أنها محددات وسوم، فيجب كتابتها بصيغة خاصة بها، بحيث يُكتب القوس الابتدائي ‎<‎ الذي يمثل علامة "أقل من" في الرياضيات على الصورة ‎&lt;‎، وكذلك القوس الغالق ‎>‎ الذي يمثل علامة أكبر من في الرياضيات على الصورة ‎&gt;‎. وهكذا، نكتب محرف آمبرساند & ثم اسم المحرف أو رمزه ثم فاصلة منقوطة ;، وتُعرف هذه الصيغة باسم الوحدة entity، حيث تُستبدَل بالمحرف الذي تعبِّر عنه، وذلك مشابه للطريقة التي تُستخدَم بها الشرطة المائلة العكسية في سلاسل جافاسكربت النصية. بما أنّ هذه الآلية تعطي معنى خاصًا لمحارف &، فإنها بذاتها تحتاج إلى تهريب بأن تُكتب ‎&amp;‎ لتظهر بصورتها إذا احتجنا إلى إظهارها في النص، وبالمثل لما بين قيم السمات المغلفة بعلامات تنصيص مزدوجة، إذ يمكن استخدام ‎&quot;‎ لإدخال محرف علامة التنصيص ليظهر بصورته. تُحلَّل HTML بأسلوب يتسامح كثيرًا مع الأخطاء، فإذا كان أحد الوسوم مفقودًا من موضع ما أو نسينا كتابته، فسيكتبه المتصفح ليعالج الخلل، وقد اعتُمِد الأسلوب الذي يحدث به ذلك بحيث تستطيع الاعتماد على جميع المتصفحات الحديثة التي تفعل الشيء نفسه لمعالجة أخطاء الكتابة. انظر المستند التالي الذي سيعامَل على أنه المستند الذي كتبناه في المثال أعلاه رغم أخطاء الصياغة والبناء التي فيه: <!doctype html> <meta charset=utf-8> <title>My home page</title> <h1>My home page</h1> <p>Hello, I am Marijn and this is my home page. <p>I also wrote a book! Read it <a href=http://eloquentjavascript.net>here</a>. عرف المتصفح قد أنّ وسوم <html> و<head> و<body> ليست موجودة، وعرف أنّ الوسمين <meta> و<title> ينتميان إلى الترويسة، والوسم <h1> تعني أنّ متن المستند بدايته من هنا. كما لم يعد هناك حاجة إلى إغلاق الفقرات بصورة صريحة بما أنّ بدء فقرة جديدة أو إنهاء المستند سيغلقها ضمنيًا، ولعلك لاحظت إن كنت منتبهًا إلى أن علامات التنصيص التي حول قيم السمات ليست موجودة أيضًا. سنهمِل في هذه السلسلة وسوم <html> و<head> و<body> من الأمثلة للاختصار ولإبقاء الأمثلة بسيطة، لكن سنغلق الوسوم ونضيف علامات التنصيص حول السمات، كما قد نهمِل تصريح doctype وcharset، لكن لا يعني هذا أننا نشجعك على ذلك، فقد تجد المتصفح يتصرف بغرابة ويفعل أشياءً سخيفةً إذا أهملتها، لذلك نريدك أن تتصرف كما لو كانت البيانات الوصفية الخاصة بهما موجودة في الأمثلة حتى لو لم تكن ظاهرة فعلًا في النص أمامك. HTML وجافاسكربت ما يهمنا في HTML فيما يتعلق بهذه السلسلة هو وسم <script>، إذ يسمح لنا بإدخال شيفرة جافاسكربت داخل المستند. <h1>Testing alert</h1> <script>alert("hello!");</script> ستعمل مثل تلك الشيفرة عندما يرى المتصفح وسم <script> أثناء قراءة HTML، وستُظهر الصفحة نافذة منبثقة حين تُفتح، كما تُشبه دالة alert في المثال أعلاه prompt إلا أنها تخرج نافذة منبثقة تعرض رسالةً ما دون طلب إدخال أي بيانات. يمثِّل المثال السابق برنامجًا صغير الحجم؛ أما إدخال برامج كبيرة مباشرةً في HTML فهو غير عملي، وبدلًا من ذلك يمكن تزويد وسم <script> بسِمة src لجلب ملف سكربت خارجي من رابط URL ما، وهو ملف نصي يحتوي على برنامج جافاسكربت. انظر المثال التالي حيث يحتوي ملف code/hello.js على البرنامج السابق نفسه alert("hello!")‎. <h1>Testing alert</h1> <script src="code/hello.js"></script> حين تشير صفحة HTML إلى روابط أخرى لتكون أجزاء منها مثل صورة أو ملف سكربت، فستجلبها المتصفحات مباشرةً أثناء تحميل الصفحة لتعرضها لك مع الصفحة نفسها في الأماكن التي حددها ملف HTML. يجب إغلاق وسم <script> دومًا بوسمه الغالق ‎‏‎<‏‏‏‎/‎script‎‏‏‎‎‏‎‏‏‏>‎ حتى لو كان يشير إلى ملف سكربت لا يحتوي أي شيفرة، فإذا نسيت ذلك، فسستُفسَّر بقية الصفحة على أنها جزء من السكربت. تستطيع تحميل وحدات ES -التي وردت في مقال الوحدات Modules في جافاسكريبت- في المتصفح بتزويد وسم script بسِمة type="Module"‎، كما يمكن أن تعتمد مثل هذه الوحدات على وحدات أخرى باستخدام روابط متعلقة بها على أساس أسماء وحدات في تصريحات import. كذلك يمكن لبعض السمات أن تحتوي على برنامج جافاسكربت، فالوسم <button> الذي في المثال التالي والذي سيُظهر زرًا به سمة onclick، كما ستعمل قيمة السمة في كل مرة نضغط على الزر فيها. <button onclick="alert('Boom!');">لا تضغط هنا</button> لاحظ أننا اضطررنا لاستخدام علامات تنصيص مفردة للسلسلة النصية التي في سمة onclick لأن العلامات المزدوجة كانت مستخدِمة سلفًا للسمة ككل، لكن كان بإمكاني أن أستخدم ;quot‏&‎ كذلك. داخل صندوق الاختبارات sandbox لا شك أنّ تشغيل البرامج المحمَّلة من الإنترنت قد يكون خطيرًا، إذ أنك لا تدري مَن وراء أغلب المواقع التي تزورها، وقد يكون فيهم الخبيث واللئيم، كما أنه لا نريد اختراق حواسيبنا بسبب إغفالنا لأمر مثل هذا، لكن الجميل في الموضوع هنا أنك تستطيع تصفح الويب دون الحاجة إلى توكيد الثقة في كل صفحة تزورها، لهذا فإنّ المتصفحات تحدّ كثيرًا من الصلاحيات التي تكون لبرامج جافاسكربت، فلا تستطيع النظر في الملفات التي على حاسوبك أو تعديل أي شيء غير متعلق بالصفحة التي هي فيها. ويسمى هذا العزل لتلك البرامج بصندوق الرمل أو صندوق الاختبار Sandbox، وإن كانت الترجمة الحرفية له هي صندوق الرمل إلا أنّ وظيفته هي عزل البرمجيات التي لا نثق فيها إلى حين البَت في أمرها أو تنتفي الحاجة إليها، وتعمل هذه البرامج كما تشاء داخل هذا الصندوق دون أذى للحاسوب نفسه أو لبياناتك، فلو كان هذا الصندوق قفصًا حديديًا سميك الجدران، فستكون البرامج حينها هي الكائنات التي تعبث فيه كما تشاء دون أن تؤذي من هو خارج القفص، ودون أن تستطيع هي نفسها الخروج. لكن الأمر الصعب عند التعامل مع هذا الصندوق هو السماح لهذه البرامج بفسحة تكفي لتكون مفيدةً مع حجزها عن التسبب في أذى للحاسوب والبيانات التي عليه في الوقت نفسه، لكن احذر أن تشمل هذه الفسحة أمورًا مثل التواصل مع الخوادم الأخرى وقراءة محتويات حافظة النسخ واللصق، فهنا قد يتطور الأمر إلى منحى خطير على الخصوصية. قد يخرج علينا أحد بين الحين والآخر بطريقة جديدة للتحايل على حدود المتصفح والتسبب في اختراق وأذى للمستخدم من تسريب معلومات طفيفة عنه حتى السيطرة التامة على الجهاز المخترَق، كما يستجيب مطورو المتصفحات لهذه التهديدات بإصلاح الثغرات التي تتسبب فيها إلى حين ظهور مشكلة جديدة، لكن مكمن الخطر هنا هو عدم إدراك المطورين للمشكلة إذا لم تُفضَح وتنتشر، فإذا استغلها أحد أو شركة أو منظمة أو جهة حكومية دون الإبلاغ عنها فستكون هنا الخسائر المحتملة أكبر. التوافقية وحروب المتصفحات كان المتصفح المسيطر على الساحة في بدايات الويب يسمى موزايك Mosaic، ثم انتقل السوق إلى متصفح نِت سكيب Netscape الذي ما لبث أن أفرغ الساحة قهرًا لمتصفح إنترنت إكسبلورر Internet Explorer الشهير الخاص بمايكروسوفت. كانت الجهة التي تقف خلف المتصفح في كل مرة تشعر أنّ لها الحق في اختراع المزايا الجديدة التي تشاء للويب، وبما أن أغلب المستخدِمين كانوا يستخدِمون متصفحها، فستضطر المواقع إلى استخدام تلك المزايا مع غض النظر عن باقي المتصفحات، وهذا سبب الكثير من مشاكل التوافقية للمواقع مع المتصفحات المختلفة. نستطيع القول أنّ هذا كان هو العصر المظلم للتوافقية، والذي كان يُعرف بعصر حروب المتصفحات، كما كان المطورون يجدون أنفسهم بين منصتين أو ثلاث لا تتوافق أي منها مع الأخرى بدلًا من ويب واحدة، وزاد الطين بلة امتلاء المتصفحات التي كانت في السوق في بداية الألفية الثالثة -نحو 2003- بالزلات البرمجية Bugs والمشاكل، وكل متصفح له مشاكله، وهكذا كانت حياة المطورين أشبه بالحرب فعليًا. ظهر بعد ذلك متصفح موزيلا فاير فوكس Mozilla Firefox، وهو فرع لا يهدف للربح من نِت سكيب ليبارز إنترنت إكسبلورر في السوق في أواخر عام 2002، وقد سيطر على حصة كبيرة من السوق لعدم اهتمام مايكروسوفت بمتابعة التطوير وزهدها في مجال المتصفحات حينها، ثم خرجت جوجل بمتصفح خاص بها هو جوجل كروم Chrome، وكذلك شركة آبل بمتصفح سفاري Safari الذي اكتسب شهرةً بسبب أنه يأتي مع أجهزتها تلقائيًا، وبالتالي صار لدينا أربعة متصفحات رئيسية على الساحة بدلًا من واحد فقط. اتجهت هذه الهيئات الأربعة نحو وضع معايير موحَّدة وأساليب هندسية أفضل لتصميم هذه المتصفحات، لتعطينا متصفحات لا تعاني كثيرًا من الزلات البرمجية ولا مشاكل التوافقية، وقد رأت مايكروسوفت مؤخرًا أنّ حصتها في هذا السوق قد تهاوت كثيرًا، فتبنّت هذا الرأي في النهاية في متصفح إيدج Edge الخاص بها واستبدلت به متصفحها القديم. يصب كل هذا في مصلحة من يقرِّر تعلم تطوير الويب هذه الأيام، إذ أن الإصدارات الأخيرة من هذه المتصفحات تكاد تتصرف بالأسلوب نفسه تقريبًا وليس لديها الكثير من الزلات البرمجية. ترجمة -بتصرف- للفصل الثالث عشر من كتاب Elequent Javascript لصاحبه Marijn Haverbeke. اقرأ أيضًا المقال السابق: مشروع بناء لغة برمجة خاصة ما هي البرمجة ومتطلبات تعلمها؟ هيكل البرنامج في جافاسكريبت اكتشاف دعم المتصفحات لميزات HTML5 تنسيقات المتصفحات المخصصة ودعمها وأداءها في CSS
  2. من الضروري أن تكون البرامج التي نكتبها سهلة القراءة بمجرد النظر إليها، لأن أخلاط الرموز الغريبة المتجاورة والأقواس المتتالية والكلمات المبتورة من اللغة الإنجليزية؛ تكفي ليظن الناظر إلى الشيفرة أن طفلًا صغيرًا عبث بيده على لوحة المفاتيح، فإذا اخترنا أسماءً مفهومةً للمتغيرات تدل على وظائفها، ورتبنا الأسطر فجعلنا كل مجموعة منها تنفذ مهمةً ما بإزاحة خاصة بها، وكتبنا تعليقات توضح سبب اختيار الأرقام أو الرموز التي لا يبدو لها سبب منطقي، وقبل كل ذلك افتتحنا الملف ببيانات تعريفية له نعرف بها من كتبه وإصدار البرنامج وغير ذلك، وإذا اتبعنا مثل هذه السلوكيات -وهي المتبعة في البرمجة بالفعل-؛ فسنوفر كثيرًا من الجهد والوقت اللذين سيهدرَان في معرفة غرض كل سطر من البرنامج ومشاكله المستقبلية، ويسهل تحديثه لاحقًا وإضافة ميزات جديدة للبرنامج. التعليقات تحدثنا عن التعليقات في مقال بعض التسلسلات النصية المهمة لتعلم البرمجة، وسنلقي في هذا المقال الضوء على بعض تطبيقاتها واستخداماتها. سجل الإصدارات يجب على المبرمج أن يسهّل على من يقرأ برامجه معرفة التفاصيل الأساسية لكل ملف، مثل تاريخ إنشائه واسم مؤلفه وبيانات آخر تعديل فيه وإصداره ووصف عام لمحتوياته، وبقية المعلومات التي تدخل في سجل تغييراته، وهذه الكتلة النصية تُكتب في تعليق، بالشكل التالي: ############################# # Module: Spam.py # Author: A.J.Gauld # Version: Draft 0.4 ''' This module provides a Spam class which can be combined with any other type of Food object to create interesting meal combinations. ''' ############################### # Log: # 2015/09/01 AJG - File created # 2015/09/02 AJG - Fixed bug in pricing strategy # 2015/09/02 AJG - Did it right this time! # 2016/01/03 AJG - Added broiling method(cf Change Req #1234) ################################ import sys, string, food ... نرى في هذا المثال ملخصًا بسيطًا لما يحتويه الملف بمجرد فتحه، فنعرف ما الذي تغير فيه من ما قبله، ومن الذي نفذ هذه التغييرات ووقت إجرائها، وتبرز أهمية هذه المعلومات عند العمل على المشروع ضمن فريق، وذلك حين نحتاج إلى معرفة المسؤول عن التغيير الذي بين أيدينا أو تصميم البرنامج الذي تعمل عليه، وقد وضعنا الوصف بين زوج من علامات الاقتباس الثلاثية، وهذا أسلوب تتبعه بايثون يسمى سلسلة التوثيق، ويُستخدم لإضافة هذا التوثيق والوصف إلى الدالة help()‎ المضمَّنة فيها، كما سنرى بعد قليل. من المهم ملاحظة وجود أدوات تعمل مثل مستودعات للشيفرات البرمجية، مثل Git وSubversion وCVS وRCS، إذ تستطيع الاحتفاظ بمعلومات تلقائيًا، مثل اسم المؤلف واسم الملف وتفاصيل سجل الإصدار وننصح بالنظر في خصائص التوثيق في تلك المستودعات وتعلمها، لأنها توفر الكثير من الوقت الضائع في التسجيل اليدوي لهذه التعليقات. تعطيل الشيفرات الفاسدة تُستخدم التعليقات لعزل جزء فاسد أو غير صالح من الشيفرة عن باقي البرنامج، كأن يكون لدينا برنامج يقرأ بعض البيانات ويعالجها، ويطبع الخرج ثم يحفظ النتائج في ملف البيانات مرةً أخرى، ويجب منع حفظ البيانات الخاطئة أو الفاسدة في الملف وإفساده؛ إذا كانت النتائج على غير ما نريد أو نتوقع. وقد يقال أن الحل الأبسط هو حذف الشيفرة الفاسدة، وهذا صحيح؛ لكننا نستطيع عزلها وتعطيل تنفيذها بتحويلها إلى تعليق، لإصلاحها فيما بعد، وذلك كما يلي: data = readData(datafile) for item in data: results.append(calculateResult(item)) printResults(results) ###################### # تُعزل الشيفرة أدناه إلى حين إصلاح # calculateResult الزلة التي في # for item in results: # dataFile.save(item) ###################### print 'Program terminated' ثم نحذف علامات التعليق إذا أصلحنا ذلك الخلل، لجعل الشيفرة نشطةً مرةً أخرى. تحتوي العديد من المحررات البرمجية مثل IDLE، خاصيةً نستطيع فيها اختيار جزء من الشيفرة وتحويله إلى تعليق تلقائيًا، ثم إلغاء ذلك التحديد عند الحاجة، وسنجد هذه الخاصية في IDLE في القائمة Format ثم الخيار Comment Out Region. سلاسل التوثيق تسمح جميع اللغات البرمجية بإنشاء تعليقات لتسجيل ما تفعله الدالة أو الوحدة التي نكتبها، لكن قليلًا منها يزيد على ذلك بأن يسمح لنا بتوثيق الدالة بطريقة تستطيع اللغة أو البيئة نفسها أن تستفيد منها لتوفر لنا مساعدةً تفاعليةً أثناء البرمجة، ومن تلك اللغات: جافا وبايثون وSmalltalk، نستخدم هذه الخاصية في بايثون من خلال علامات الاقتباس الثلاثية التي على نمط """documentation""": class Spam: """A meat for combining with other foods It can be used with other foods to make interesting meals. It comes with lots of nutrients and can be cooked using many different techniques""" def __init__(self): pass # ie. it does nothing! help(Spam) لاحظ أننا نستطيع الوصول إلى سلسلة التوثيق باستخدام الدالة help()‎، وأن سلاسل التوثيق تصلح للوحدات والدوال والأصناف والتوابع، لننظر إلى الشيفرة التالية: >>> import sys >>> help (sys.exit) Help on built-in function exit: exit(...) exit([status]) Exit the interpreter by raising SystemExit(status). If the status is omitted or None, it defaults to zero (i.e., success). If the status is numeric, it will be used as the system exit status. If it is another kind of object, it will be printed and the system exit status will be one (i.e., failure). (END) إذا أردنا الخروج من وضع المساعدة الذي في المثال السابق عند رؤية END؛ التي تشير إلى نهاية التوثيق، فسنضغط على مفتاح Q في لوحة المفاتيح، وهو اختصار كلمة Quit؛ أما إذا كان التوثيق أكثر من صفحة، فسنضغط على مفتاح المسافة لتصفحه، ومن الجدير بالذكر أن كلمة END التي في نهاية التوثيق قد لا تظهر في بعض الطرفيات. توجد دالة مساعِدة أخرى هي dir()‎، والتي تعرض جميع المزايا التي تعرفها بايثون عن كائن ما، فإذا أردنا معرفة الدوال أو المتغيرات الموجودة في وحدة sys مثلًا، فسنكتب ما يلي: >>> import sys >>> dir(sys) [..... 'argv', 'builtin_module_names', 'byteorder', .... 'copyright', .... 'exit', ..... 'stderr', 'stdin', 'stdout', 'subversion', 'version', 'version_info', 'warnoptions', 'winver'] بعد ذلك سنختار ما نريد منها، ونستخدم دالة help()‎ للحصول على مزيد من المعلومات، وقد أهملنا العديد من المدخلات في المثال أعلاه لتوفير المساحة، كما نستفيد من وظيفة الدالة dir()‎ عند استخدام وحدة ليس لها توثيق جيد أو كافٍ، فقد نتعامل مع وحدات ليس لها توثيق خارجي أصلًا. إزاحة الكتل ذكرنا عدة مرات أن إزاحة الأسطر مهمة في بايثون، وتتوقف عليها الإجراءات التالية للأسطر المزاحة، مع أن أكثر لغات البرمجة لا تهتم بإزاحة الأسطر، لذا يزيح المبرمجون الأسطر في برامجهم وشيفراتهم لتسهيل قراءتها وتمييز أجزائها لا أكثر، فقد صار لكل مبرمج تقريبًا أسلوبه الخاص في الإزاحة وفق ما يراه مناسبًا، وقد ولّدت كثرة الأهواء والآراء في هذا الشأن جدالات حامية الوطيس في البرمجة منذ زمن، بلغت حد إجراء بدراسات للوقوف على الجوانب التي تؤثر فيها إزاحة الأسطر سوى المظهر الجمالي، كأن تساعد في فهم الشيفرة البرمجية بطريقة أسرع وأسهل، لننظر في المثال التالي من VBScript مثلًا: < script type="text/vbscript"> For I = 1 TO 10 MsgBox I Next </script> ثم لننظر إلى النسخة التالية منه، والتي لا تحوي إزاحات للأسطر: < script type="text/vbscript"> For I = 1 TO 10 MsgBox I Next </script> يكاد المثالان يتطابقان في نظرنا نحن البشر، والفرق بينهما هو أن النسخة الأولى أسهل في القراءة في نظرنا، أما في نظر مفسر VBScript فهما متطابقتان تمامًا ولا فرق بينهما، والشرط الحاكم للإزاحة هنا هو أن تعكس الهيكل المنطقي للشيفرة، فتتبع تدفق البرنامج في هيئتها المرئية، وقد أظهرت الدراسات التي أجريت على الشيفرات المزاحة أن الشيفرات المقسَّمة بصريًا إلى كتل تعكس البنية الكتلية المنطقية للبرنامج؛ أسهل في الفهم، وعلى ذلك تكون البنية التالية: XXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXX أسهل في فهم مرادها ووظيفتها من هذه البنية: XXXXXXXXXXXXXXXXXXXXX XXXXX XXXXXXXXXXXX XXXXXXXXXXXX XXXXXXXXXXXX XXXXX قد لا تظهر أهمية الإزاحة في الأمثلة البسيطة التي أوردناها إلى الآن، لكن الفرق يتضح عند التعامل مع برامج تحتوي على مئات الآلاف من الأسطر البرمجية. يتبقى الآن أن ننظر في مقدار تلك الإزاحة، وهو أمر طال الخلاف حوله في مجال البرمجة، ويكمن الاختلاف بين استخدام المسافات spacebar للإزاحة أو استخدام زر الجدول tab. ومع أن استخدام زر الجدول أسهل لأنه يؤدي الغرض بنقرة واحدة؛ غير أن هذه النقرة الواحدة تساوي 8 مسافات، وهي إزاحة كبيرة، كما يصعب تحديد آلية الإزاحة الموجودة في شيفرة ما، فهل نتجت عن استخدام زر المسافة أم زر الجدول، لأننا لا نستطيع استخدامهما معًا، فإما هذا أو ذاك، وتظهر هذه المشكلة لمن يتعامل مع برنامج كتبه مبرمج غيره، ولتجنب هذه المعضلة من بداية تعلمنا للبرمجة، يُفضّل تجنب استخدام زر الجدول في الإزاحة، لأن بعض الدراسات أظهرت أن أفضل صورة للشيفرة -من حيث سهولة القراءة- تتحقق إذا كانت الإزاحة محصورةً بين مسافتين وخمس مسافات؛ أما مستخدمو بايثون فقد اصطلحوا على استخدام أربع مسافات في الإزاحة، على الرغم من صلاحية الأمثلة التي استخدمناها إلى الآن، والتي كانت إزاحاتها أقل من ذلك، لأنها توفر بعض المساحة على الشاشة. أسماء المتغيرات لم نستخدم أسماءً لها معانٍ للمتغيرات في الأمثلة التي أوردناها في ما سبق، لأننا لم نقصد إلا شرح بعض التقنيات البرمجية، لكن يُفضل أن تعكس أسماء المتغيرات ما تمثله في الشيفرة، من حيث الوظيفة التي تنفذها، فقد استخدمنا المتغير multiplier في تدريب جدول الضرب مثلًا؛ والذي سبق أن عرضناه مثل متغير يوضح الجدول الذي نريد طباعته. علمًا أن هذه التسمية أفضل من اختيار اسم عشوائي أو رمزي مثل m الذي لن يسبب مشاكل في تنفيذ الشيفرة، بل سيريحنا في الكتابة بما أنه حرف واحد، لكنه بلا معنىً، وسيتسبب في مشاكل مستقبلية إذا أردنا معرفة وظيفة هذا المتغير، أو أراد مبرمج آخر ذلك. يُفضل في تسمية المتغيرات أن تكون مفهومةً على أن تكون سهلة الكتابة، وأفضل حل لأسماء المتغيرات هو أن تكون قصيرةً وذات معنىً في نفس الوقت، فالأسماء الطويلة مربكة وصعبة الكتابة، فمثلًا: نستطيع استخدام the_table_we_are_printing عوضًا عن multiplier، غير أن هذا الاسم طويل ويتجاوز المعقول في الكتابة البرمجية. حفظ البرامج ذكرنا سابقًا أننا نخسر كل شيء كتبناه في محث بايثون ‎>>>‎ بمجرد الخروج منه، فهو وإن كان ممتازًا في تجربة الأفكار بسرعة؛ إلا أنه غير مفيد على المدى البعيد، إذ نريد أن نكتب البرامج ونشغلها مرةً بعد مرة وقتما نشاء، فهذا هو المنطق، ولفعل ذلك في بايثون؛ ننشئ ملفًا نصيًا للبرنامج ونجعله بالامتداد ‎.py، وهو اصطلاح متبع، رغم أننا نستطيع استخدام أي امتداد نريده هنا، لكن يُفضل اتباع هذا الاصطلاح، ثم نستطيع تشغيل البرنامج من محث سطر أوامر النظام بكتابة ما يلي: C:\WINDOWS> python spam.py فيكون اسم برنامج بايثون هنا هو spam.py، ومحث نظام التشغيل هو C:\WINDOWS>‎. إحدى مزايا استخدام هذه الطريقة في برامج بايثون أننا نستطيع تشغيل البرنامج بمجرد النقر عليه في مدير الملفات، مثل أي ملف تنفيذي يمثل برنامجًا بما أن ويندوز يربط امتداد ‎.py بمفسر بايثون، كما أن استخدام الملفات لتخزين البرامج يسمح بتصحيح أخطائها دون الحاجة إلى إعادة كتابة الجزء الخاطئ من البرنامج مرةً أخرى في محث بايثون، أو التحرك بالمؤشر في IDLE حتى نتجاوز الخطأ كي نعيد تحديد الشيفرة، لأن IDLE تدعم فتح الملف لتعديله وتشغيله من القائمة Run، ثم خيار Run module، أو باستخدام مفتاح F5 على لوحة المفاتيح. لن نعرض محث بايثون ‎>>>‎ مرةً أخرى في الأمثلة البرمجية التي نشرحها، إذ سنفترض أنك تنشئ البرامج في ملف منفصل وتشغلها إما داخل IDLE أو من محث سطر الأوامر. ملاحظة لمستخدمي ويندوز يمكن إعداد ربط الملفات التي تنتهي بامتداد ‎.py داخل مدير الملفات، مما يسمح بتشغيل برامج بايثون بالنقر المزدوج على أيقونة الملف، والمفترض أن مثبت بايثون قد نفذ هذا الإجراء بالفعل، وللتحقق من ذلك؛ ابحث عن ملف بامتداد بايثون وجرب تشغيله، فإذا نجحت يكون الربط جاهزًا على حاسوبك، حتى لو أخرج لك رسالة خطأ. المشكلة التي قد تواجهها هنا هي أن الملفات التي أيقونتها شعار بايثون نفسه؛ إذ ستشَغل في صندوق DOS ثم تغلَق بسرعة بحيث لا تكاد تراها، ويمكن حل هذا بكتابة السطر التالي في نهاية البرنامج: input("Hit ENTER to quit") سيعرض هذا السطر رسالةً تخبرك أن تضغط زر ENTER للخروج، وينتظر حتى يحدث ذلك؛ أما في إصدارات بايثون الأخيرة فهناك أداة إضافية في ويندوز ستساعدك على تشغيل بايثون دون مشاكل، وهي py.exe، فإذا كتبنا: C:\WINDOWS> py spam.py فستبحث هذه الأداة عن بايثون، وتشغل الشيفرة تشغيلًا صحيحًا، كما تستفيد من أي سطر كامل المسار shebang line داخل الملف لتعرف أي إصدار تستخدمه من بايثون -إذا وُجد أكثر من إصدار مثبت على حاسوبك-. ملاحظة لمستخدمي يونكس يجب أن يحتوي أول سطر في ملف سكربت بايثون على التسلسل ‎#!‎ متبوعًا بالمسار الكامل لبايثون على حاسوبك، ويُعرف سطر المسار الكامل هذا أحيانًا باسم شيبانك shebang باللغة الإنجليزية، وهو السطر الذي يجعل شيفرة بايثون قابلةً للتنفيذ عبر مفسر بايثون المحدد بالمسار، والذي نعثر عليه بكتابة السطر التالي في محث الصدفة shell prompt: $ which python وقد يبدو ذلك المسار كما يلي: #!/usr/local/bin/python مما يسمح لنا بتشغيل الملف دون استدعاء بايثون، بعد إعدادها لتكون قابلةً للتشغيل من خلال chmod كما تعلم: $ spam.py يمكن استخدام ‎/usr/bin/env python بدل معلومات المسار، مثل أسلوب أفضل يمكن تنفيذه على أغلب أنظمة يونكس، بما فيها جميع توزيعات لينكس: #!/usr/bin/env python سيبحث هذا الأمر عن بايثون في مسارك تلقائيًا، غير أن عيبه يظهر عندما توجد عدة إصدارات من بايثون، ولا يعمل السكربت إلا مع إصدار واحد منها فقط عند استخدام ميزة جديدة في بايثون مثلًا، وهنا يُفضَّل استخدام أسلوب المسار الكامل، أو على الأقل تحديد إصدار بايثون المناسب، سواء كان python2 أو python3. أما سطر ‎#!‎ فلا يسبب مشاكل في نظامي ويندوز أو ماك لأنه سيبدو تعليقًا، لذا لا بأس بكتابته عند استخدام هذين النظامين، وذلك تحسبًا لاحتمال تشغيل الشيفرة على حاسوب يونكس يومًا ما، أو استخدام مطلِق py.exe الذي لا يتجاهل سطر المسار الكامل shebang. جافاسكربت وVBScript لا يحتاج من يبرمج مستخدمًا جافاسكربت وVBScript إلى الملاحظات التي ذكرناها أعلاه، لأن هذه البرامج تُحفظ في ملفات. خاتمة تعلمنا في هذا المقال ما يلي: استخدام التعليقات لعزل جزء من الشيفرة مؤقتًا عند الاختبارات أو تنقيح الشيفرة debugging، وكذلك استخدامها لتوفير ترويسة توضيحية يُذكر فيها سجل إصدارات الملف. كيفية استخدام سلاسل التوثيق في توفير معلومات فورية runtime-information عن وحدة ما، والكائنات التي داخلها. فائدة إزاحة كتل الشيفرات في توضيح الترتيب المنطقي لهيكل الشيفرة البرمجية، وتسهيل تتبع عين القارئ لذلك التسلسل المنطقي. حفظ برامج بايثون في ملفات لإعادة تشغيلها في أي وقت، بدلًا من كتابتها في محث بايثون ‎>>>‎ ثم ضياعها عند الخروج منه، وذلك بكتابة ‎$ python progname.py في سطر الأوامر، أو بتشغيل الملف في مدير الملفات بالنقر عليه. ترجمة -بتصرف- للفصل الثامن: Coding Style من كتاب Learning To Program لصاحبه Alan Gauld. اقرأ أيضًا المقال التالي: كيفية قراءة البرامج لمدخلات المستخدم المقال السابق: الحلقات التكرارية في البرمجة دليلك الشامل لتعلم البرمجة الوضع الصارم: النمط الحديث لكتابة شيفرات جافاسكربت تحسين الشيفرات المكتوبة بلغة Cpp وتشخيصها دليل Airbnb لتنسيق الشيفرة البرمجية للغة روبي Ruby
  3. يُعَدّ بناء لغة برمجة خاصة بك أمرًا ليس بالصعب على عكس ما تتوقع الغالبية الساحقة من الناس، خاصةً إن لم ترفع سقف توقعاتك كثيرًا، بل قد يكون فيه كثير من التعلم النافع أيضًا، والذي نريد الإشارة إليه في هذا المقال هو أنّ بناء لغة ليس ضربًا من السحر والشعوذة بسبب شعورنا بالإحساس الذي ينتاب المرء في مثل هذه الحالات، فقد جربناه بأنفسنا مع بعض الاختراعات البشرية التي رأينا فيها ذكاءً وصنعةً جعلتنا نظن أنه يستحيل علينا فهم طريقة عملها وبنائها، لكن القراءة والتجربة تفك كثيرًا من هذا السحر. سنبني في هذا المقال لغة برمجة سنسميها لغة البيض Egg، حيث ستكون صغيرةً وبسيطةً لكنها قوية بما يكفي لتعبِّر عن أيّ حسابات تفكر فيها، كما ستسمح بالتجريدات abstractions البسيطة المبنية على دوال. التحليل إن أول ما تراه عينك عند النظر إلى لغة برمجة هو بنيتها اللغوية syntax أو صيغتها notation، والمحلل parser هو برنامج يقرأ نصًا ما وينتج هيكل بيانات data structure، بحيث يعكس بنية البرنامج الموجود في ذلك النص، فإن لم يشكِّل النص برنامجًا صالحًا، فيجب أن يخبرنا المحلل بالخطأ الذي سبب ذلك. ستكون لغتنا ذات بنية لغوية بسيطة وموحدة، حيث سيكون كل شيء في Egg تعبيرًا، وقد يكون التعبير expresion اسم رابطة binding أو عددًا أو سلسلةً نصيةً string أو حتى تطبيقًا، كما تُستخدَم التطبيقات لاستدعاءات الدوال وللبنى اللغوية مثل if أو while. لن تَدعم السلاسل النصية في Egg أيّ شيء يشبه تهريب الشَرطة المائلة العكسية \، ذلك أنّ السلسلة النصية ببساطة هي سلسلة من محارف داخل اقتباسات مزدوجة، لكن لن تكون هي ذاتها اقتباسات مزدوجة، كما سيكون العدد سلسلةً من الأرقام، ويمكن أن تتكون أسماء الرابطات من أيّ محرف ليس مسافةً فارغةً وليس له معنى خاص في البنية التركيبية للغة. تُكتَب التطبيقات بالطريقة نفسها المكتوبة بها في جافاسكربت، وذلك بوضع أقواس بعد التعبير، ويمكنها امتلاك أيّ عدد من الوسائط arguments بين هذين القوسين مفصول بين كل منها بفاصلة أجنبية ,. do(define(x, 10), if(>(x, 5), print("large"), print("small"))) يعني توحيد لغة Egg أنّ الأشياء التي تُرى على أساس عوامل operators في جافاسكربت -مثل ‎>‎- هي رابطات عادية في هذه اللغة، وتُطبَّق مثل أيّ دالة أخرى، كما نحتاج إلى بنية do للتعبير عن تنفيذ عدة أشياء في تسلسل واحد، وذلك لعدم وجود مبدأ الكتل blocks في البنية التركيبية للغة. يتكون هيكل البيانات الذي سيستخدمه المحلل لوصف برنامج ما من كائنات تعابير، لكل منها خاصية type توضِّح نوع التعبير وخصائص أخرى تصف محتواه. تمثِّل التعابير التي من النوع "value" سلاسلًا نصيةً مجردةً أو أعدادًا، وتحتوي خاصية value لها على السلسلة النصية أو قيمة العدد الذي تمثله؛ أما التعابير التي من نوع "word" فتُستخدَم للمعرِّفات -أي الأسماء-، فمثل تلك الكائنات لها خاصية name تحمل اسم المعرِّف على أساس سلسلة نصية، وأخيرًا تمثِّل تعابير "apply" التطبيقات، إذ تملك خاصية operator التي تشير مرجعيًا إلى تعبير يُطَبَّق، بالإضافة إلى خاصية args التي تحمل مصفوفةً من تعابير الوسائط argument expressions. سيُمثَّل جزء ‎>(x, 5)‎ من البرنامج السابق كما يلي: { type: "apply", operator: {type: "word", name: ">"}, args: [ {type: "word", name: "x"}, {type: "value", value: 5} ] } تُسمى مثل هذه الهياكل من البيانات باسم شجرة البُنى syntax tree، فإذا تخيلنا الكائنات نقاطًا والروابط التي بينها خطوطًا بين هذه النقاط، فسيكون لدينا شكلًا يشبه الشجرة، فكما أنّ الشجرة بها أغصان تنقسم ويحمل كل منها أغصانًا أخرى؛ فبدوره يشبه احتواء التعابير على تعابير أخرى تحتوي بدورها على تعابير جديدة، وهكذا. هذا على عكس المحلل الذي كتبناه لصيغة ملف الإعدادات في مقال التعابير النمطية Regular Expressions في جافاسكريبت، والذي كانت بنيته بسيطةً نوعًا ما، إذ يُقسِّم الدخل إلى أسطر ويعالج هذه الأسطر واحدًا واحدًا، حيث فلم يكن ممكنًا للسطر الواحد إلا بضع صور بسيطة يمكنه أن يكون عليها؛ أما هنا فيجب إيجاد طريقة أخرى، فالتعابير ليست مقسمةً إلى أسطر، كما تملك بنيةً تعاوديةً recursive، وتحتوي تعابير التطبيقات على تعابير أخرى. يمكن حل هذه المشكلة بسهولة من خلال كتابة دالة محلل تعاودية على النحو الذي يعكس الطبيعة التعاودية للغة. نعرِّف الدالة parseExpression التي تأخذ سلسلةً نصيةً على أساس دخل لها، وتُعيد كائنًا يحتوي هيكل البيانات للتعبير في بداية السلسلة النصية مع الجزء المتبقي من السلسلة النصية بعد تحليل هذا التعبير، ويمكن استدعاء هذه الدالة مرةً أخرى حين نحلل التعابير الفرعية كما في حالة وسيط التطبيق مثلًا، وذلك لنحصل على التعبير الوسيط بالإضافة إلى النص المتبقي، وقد يحتوي هذا النص بدوره على وسائط أخرى، أو قد يكون هو قوس الإغلاق في نهاية قائمة الوسائط. يكون أول جزء في المحلل كالتالي: function parseExpression(program) { program = skipSpace(program); let match, expr; if (match = /^"([^"]*)"/.exec(program)) { expr = {type: "value", value: match[1]}; } else if (match = /^\d+\b/.exec(program)) { expr = {type: "value", value: Number(match[0])}; } else if (match = /^[^\s(),#"]+/.exec(program)) { expr = {type: "word", name: match[0]}; } else { throw new SyntaxError("Unexpected syntax: " + program); } return parseApply(expr, program.slice(match[0].length)); } function skipSpace(string) { let first = string.search(/\S/); if (first == -1) return ""; return string.slice(first); } يجب قص المسافات الفارغة من بداية السلسلة النصية للبرنامج بما أنّ لغة Egg التي نكتبها تشبه جافاسكربت في كونها تسمح لأيّ عدد من المسافات الفارغة بين عناصرها، وهنا يأتي دور دالة skipSpace. تَستخدم parseExpression بعد تخطي أي مسافة بادئة ثلاثة تعبيرات نمطية لتحديد العناصر الصغرى الثلاثة التي تدعمها Egg، وهي السلاسل والأعداد والكلمات، كما يبني المحلل نوعًا مختلفًا من هياكل البيانات وفقًا للتي تطابق منها، فإذا كان الإدخال لا يتطابق مع أحد هذه النماذج الثلاثة، فلا يكون تعبيرًا صالحًا ويرفع المحلل خطأً. نحن نستخدم SyntaxError بدلًا من Error على أساس باني اعتراضات exception، وهو نوع قياسي آخر للأخطاء لأنه أكثر تحديدًا، وهو أيضًا نوع الخطأ الذي يرمى عند محاولة تشغيل برنامج JavaScript غير صالح، ثم نقص الجزء المطابق من سلسلة البرنامج النصية ونمرره مع كائن التعبير إلى parseApply الذي ينظر هل التعبير تطبيق أم لا، فإذا كان تطبيقًا، فسيحلَِل قائمةً من الوسائط محصورةً بين قوسين. function parseApply(expr, program) { program = skipSpace(program); if (program[0] != "(") { return {expr: expr, rest: program}; } program = skipSpace(program.slice(1)); expr = {type: "apply", operator: expr, args: []}; while (program[0] != ")") { let arg = parseExpression(program); expr.args.push(arg.expr); program = skipSpace(arg.rest); if (program[0] == ",") { program = skipSpace(program.slice(1)); } else if (program[0] != ")") { throw new SyntaxError("Expected ',' or ')'"); } } return parseApply(expr, program.slice(1)); } إذا كان المحرف التالي في البرنامج ليس قوسًا بادئًا ) فلن يكون هذا تطبيقًا، وتُعيد parseApply التعبير المعطى لها، وإلا فستتخطى القوس البادئ وتنشئ كائن شجرة بُنى syntax tree object لتعبير التطبيق ذاك، ثم تستدعي parseExpression تعاوديًا لتحلِّل كل وسيط حتى تجد القوس الغالق، كما يكون التعاود هنا غير مباشر من خلال استدعاء parseApply لدالة parseExpression والعكس، ولأن تعبير التطبيق يمكن تطبيقه كما في multiplier(2)(1)‎، فيجب أن تستدعي parseApply نفسها مرةً أخرى لتنظر فيما إذا كان يوجد زوج آخر من الأقواس أم لا، وذلك بعد تحليل تطبيق ما. هذا كل ما نحتاجه لتحليل لغة Egg، إذ نغلفها في دالة parse لتتحقق أنها وصلت إلى نهاية سلسلة الدخل بعد تحليل التعبير -فبرنامج بلغة Egg هو تعبير وحيد-، ثم تعطينا هيكل البيانات للبرنامج. function parse(program) { let {expr, rest} = parseExpression(program); if (skipSpace(rest).length > 0) { throw new SyntaxError("Unexpected text after program"); } return expr; } console.log(parse("+(a, 10)")); // → {type: "apply", // operator: {type: "word", name: "+"}, // args: [{type: "word", name: "a"}, // {type: "value", value: 10}]} وهكذا تعمل اللغة بنجاح، رغم أنها لا تعطينا معلومات مفيدة حين تفشل أو تتعطل، كما لا تخزِّن السطر والعمود الذي يبدأ عنده كل تعبير -وهو الأمر الذي قد يكون مفيدًا عند الإبلاغ عن الأخطاء فيما بعد-، لكن لا يهم، فما أنجزناه إلى الآن كافي. المقيم لا شك في أننا نريد شجرة بنى لبرنامج ما من أجل تشغيلها، وهذه هي وظيفة المقيِّم evaluator، إذ تعطيه شجرة بنى وكائن نطاق يربط الأسماء بالقيم، وسيقيِّم التعبير الذي تمثله الشجرة، ثم يُعيد القيمة التي يخرجها. const specialForms = Object.create(null); function evaluate(expr, scope) { if (expr.type == "value") { return expr.value; } else if (expr.type == "word") { if (expr.name in scope) { return scope[expr.name]; } else { throw new ReferenceError( `Undefined binding: ${expr.name}`); } } else if (expr.type == "apply") { let {operator, args} = expr; if (operator.type == "word" && operator.name in specialForms) { return specialForms[operator.name](expr.args, scope); } else { let op = evaluate(operator, scope); if (typeof op == "function") { return op(...args.map(arg => evaluate(arg, scope))); } else { throw new TypeError("Applying a non-function."); } } } } يحتوي المقيِّم على شيفرة لكل نوع من أنواع التعابير، ويُخرج لنا تعبير القيمة مصنفة النوع literal value expression قيمته، كما في حالة التعبير 100 الذي يقيِّم إلى العدد 100 فقط؛ أما في حالة الرابطة، فيجب التحقق مما إذا كانت معرَّفةً على الحقيقة في النطاق أم لا، وإن كانت فسنجلب قيمتها. تُعَدّ التطبيقات أكثر تفصيلًا من ذلك، فإذا كانت صيغةً خاصةً مثل if، فلا نقيِّم أيّ شيء ونمرِّر التعابير الوسيطة مع النطاق إلى الدالة التي تعالج هذه الصيغة؛ أما إن كانت استدعاءً عاديًا، فسنقيِّم العامِل ونتحقق أنه دالة، ونستدعيها مع الوسائط المقيَّمة. نستخدِم قيمًا لدوال جافاسكربت عادية لنمثِّل قيم الدوال في Egg، كما سنعود إلى هذا بعد قليل حين تُعرَّف الصيغة الخاصة المسماة fun، ويمثِّل الهيكل التعاودي لـ evaluate هيكلًا مماثلًا للمحلِّل، وكلاهما يعكس هيكل اللغة نفسه، ومن الممكن مكاملة المحلِّل والمقيِّم أثناء التحليل أيضًا، لكن فصلهما عن بعضهما البعض بهذه الطريقة يجعل البرنامج أكثر نقاءً. هذا جل ما نحتاج إليه لتفسير لغة Egg ببساطة، لكن من ناحية أخرى، لا تستطيع فعل الكثير بهذه اللغة دون تعريف بعض الصيغ الخاصة الأخرى وإضافة بعض القيم المفيدة إلى البيئة. الصيغ الخاصة يُستخدَم كائن الصيغ الخاصة specialForms لتعريف البُنى الخاصة في لغة Egg، حيث يربط الكلمات بالدوال التي تقيِّم مثل تلك الصيغ، ولنضف if بما أنه فارغ الآن: specialForms.if = (args, scope) => { if (args.length != 3) { throw new SyntaxError("Wrong number of args to if"); } else if (evaluate(args[0], scope) !== false) { return evaluate(args[1], scope); } else { return evaluate(args[2], scope); } }; تتوقع بنية if ثلاثة وسائط بالضبط، وستقيّم الأول منها، فإذا لم تكن النتيجة هي القيمة false فستقيِّم الثاني، وإلا فالثالث هو الذي يُقيَّم، وتُعَد صيغة if هذه أقرب إلى عامل ‎?:‎ الثلاثي في جافاسكربت منها إلى عامل if في جافاسكربت أيضًا، فهو تعبير وليس تعليمة، ويُنتِج قيمةً تكون نتيجة الوسيط الثاني أو الثالث. تختلف كذلك لغة Egg عن جافاسكربت في كيفية معالجة القيمة الشرطية إلى if، فهي لا تعامل الصفر والسلسلة النصية الفارغة على أنها خطأ false، بل القيمة false حصرًا وفقط. السبب الذي يجعلنا في حاجة إلى تمثيل if على أساس صيغة خاصة بدلًا من دالة عادية، هو أن جميع وسائط الدوال تُقيَّم قبل استدعاء الدالة، بينما يجب ألا تقيِّم if إلا وسيطها الثاني أو الثالث بناءً على قيمة الوسيط الأول. صيغة while شبيهة بهذا، فبما أن undefined غير موجودة في لغة Egg، فسنُعيد false لعدم وجود نتيجة أفضل وأكثر فائدة، كما في المثال التالي: specialForms.while = (args, scope) => { if (args.length != 2) { throw new SyntaxError("Wrong number of args to while"); } while (evaluate(args[0], scope) !== false) { evaluate(args[1], scope); } return false; }; كذلك لدينا وحدة بنيوية أساسية أخرى هي do التي تنفِّذ كل وسائطها من الأعلى إلى الأسفل، وتكون قيمتها هي القيمة التي ينتجها الوسيط الأخير. specialForms.do = (args, scope) => { let value = false; for (let arg of args) { value = evaluate(arg, scope); } return value; }; ننشئ صيغةً نسميها define كي نستطيع إنشاء رابطات ونعطيها قيمًا جديدةً، حيث تتوقع هذه الصيغة للوسيط الأول لها أن يكون كلمةً وتعبيرًا ينتج القيمة التي سنسدها إلى تلك الكلمة لتكون الوسيط الثاني، وبما أنّ define ما هي إلا تعبير، فيجب أن تُعيد قيمةً ما، وسنجعلها تُعيد القيمة التي أُسندت إليها كما في حالة عامل = في جافاسكربت. specialForms.define = (args, scope) => { if (args.length != 2 || args[0].type != "word") { throw new SyntaxError("Incorrect use of define"); } let value = evaluate(args[1], scope); scope[args[0].name] = value; return value; }; البيئة يكون النطاق الذي تقبَله evaluate كائنًا له خصائص تتوافق أسماؤها مع أسماء الرابطات، كما تتوافق قيمها مع القيم التي ترتبط بهذه الرابطات، وسنعرِّف كائنًا ليمثل النطاق العام. يجب أن تكون لنا وصول إلى قيم بوليانية كي نستطيع استخدام بنية if التي عرفناها لتونا، وبما أنه لا يوجد إلا قيمتان منطقيتان فقط، فلا نحتاج إلى ترميز خاص لهما، بل نربطهما بالقيمتين true وfalse ثم نستخدمهما. const topScope = Object.create(null); topScope.true = true; topScope.false = false; نستطيع الآن تقييم تعبير بسيط يرفض قيمةً بوليانيةً. let prog = parse(`if(true, false, true)`); console.log(evaluate(prog, topScope)); // → false سنضيف بعض قيم الدوال إلى النطاق لتوفير العوامل الحسابية الأساسية وكذلك عوامل الموازنة، كما سنستخدم Function -بهدف تبسيط الشيفرة والحفاظ على قصرها- لتوليف مجموعة من دوال العوامل في حلقة تكرارية بدلًا من تعريف كل واحد منها على حدة. for (let op of ["+", "-", "*", "/", "==", "<", ">"]) { topScope[op] = Function("a, b", `return a ${op} b;`); } إذا كانت لدينا طريقة لإخراج القيم، فسيكون ذلك مفيدًا لنا بلا شك، لذا سنغلِّف console.log في دالة ونسميها print. topScope.print = value => { console.log(value); return value; }; يعطينا هذا الأدوات الأساسية اللازمة لكتابة برامج بسيطة، حيث توفر الدالة أدناه طريقةً سهلةً لتحليل برنامج ما وتشغيله في نطاق جديد: function run(program) { return evaluate(parse(program), Object.create(topScope)); } سنستخدم سلاسل كائن النموذج الأولي لتمثيل النطاقات المتشعِّبة nested scopes كي يتمكن البرنامج من إضافة روابط إلى نطاقه المحلي دون تغيير النطاق الأعلى top-level scope. run(` do(define(total, 0), define(count, 1), while(<(count, 11), do(define(total, +(total, count)), define(count, +(count, 1)))), print(total)) `); // → 55 ذلك هو البرنامج الذي رأيناه عدة مرات من قبل، والذي يحسب مجموع الأرقام من 1 إلى 10 مكتوبًا بلغة Egg، ومن الواضح أنه ليس أجمل من مثيله في جافاسكربت، لكنا نراه ممتازًا إذا نظرنا إلى حقيقة أنّ اللغة التي كُتب بها ليس فيها إلا 150 سطرًا من الشيفرات فقط. الدوال تُعَدّ لغة البرمجة التي ليس فيها دوال لغةً فقيرةً في إمكاناتها، لكن لحسن الحظ فليس من الصعب إضافة بنية مثل fun التي تعامِل وسيطها الأخير على أنه متن الدالة، وستستخدم جميع الوسائط التي قبله على أساس أسماء لمعامِلات الدالة. specialForms.fun = (args, scope) => { if (!args.length) { throw new SyntaxError("Functions need a body"); } let body = args[args.length - 1]; let params = args.slice(0, args.length - 1).map(expr => { if (expr.type != "word") { throw new SyntaxError("Parameter names must be words"); } return expr.name; }); return function() { if (arguments.length != params.length) { throw new TypeError("Wrong number of arguments"); } let localScope = Object.create(scope); for (let i = 0; i < arguments.length; i++) { localScope[params[i]] = arguments[i]; } return evaluate(body, localScope); }; }; تحصل الدوال في لغة Egg على نطاقاتها المحلية الخاصة بها، إذ تُنشِئ الدالة المنتَجة بواسطة صيغة fun هذا النطاق المحلي، وتضيف الرابطات الوسيطة إليه، ثم تقيِّم متن الدالة في هذا النطاق وتُعيد النتيجة. run(` do(define(plusOne, fun(a, +(a, 1))), print(plusOne(10))) `); // → 11 run(` do(define(pow, fun(base, exp, if(==(exp, 0), 1, *(base, pow(base, -(exp, 1)))))), print(pow(2, 10))) `); // → 1024 التصريف يسمى الذي بنيناه حتى الآن بالمفسِّر interpreter، والذي يتصرف مباشرةً أثناء التقييم على تمثيل البرنامج الذي أنتجه المحلِّل؛ أما التصريف compilation، فهو عملية إضافة خطوة أخرى بين التحليل وتشغيل البرنامج، إذ تحوِّل البرنامج إلى شيء يمكن تقييمه بكفاءة أكبر من خلال إضافة كل ما يمكن إضافته من عمل ومهام مقدمًا، فمن الواضح في اللغات متقَنة التصميم مثلًا استخدام كل رابطة وأيّ رابطة مشار إليها بدون تشغيل البرنامج فعليًا، حيث يُستخدَم هذا لتجنب البحث عن الرابطة باسمها في كل مرة يوصَل إليها، بدلًا من جلبها مباشرةً من موقع محدد سلفًا في الذاكرة. يتضمن التصريف عادةً تحويل البرنامج إلى لغة الآلة، وهي الصيغة الخام التي يستطيع معالج الحاسوب تنفيذها، لكن هذه ليست الصورة الوحيدة له، فأيّ عملية تحوِّل البرنامج إلى تمثيل آخر مختلف يمكن النظر إليها على أنها تصريف كذلك. سيكون من الممكن كتابة خطة تقييم بديلة للغة Egg، إذ تبدأ بتحويل البرنامج إلى برنامج جافاسكربت أولًا، ثم تستخدِم Function لاستدعاء مصرِّف جافاسكربت له، وبعدها سنحصل على النتيجة، فإذا كان هذا بكفاءة، فسيجعل لغة Egg تعمل بمعدل سريع جدًا مع الحفاظ على بساطة الاستخدام. الاحتيال لعلك لاحظت حين عرَّفنا if وwhile على أنهما ليستا سوى تغليف بسيط نوعًا ما حول نظيرتيهما في جافاسكربت، وبالمثل، فإنّ القيم في Egg ما هي إلا قيم جافاسكربت العادية. إذا وازنت استخدام Egg المبني على جافاسكربت مع كمية الجهد والتعقيد المطلوب لبناء لغة برمجة من الوظائف المجردة الخام التي يوفرها الحاسوب على أساس عتاد، فإنّ الفارق عظيم، وعلى كل حال يعطيك هذا المثال صورةً للطريقة التي تعمل بها لغة البرمجة. إذا وضعنا هذا في ميزان الإنجاز، فستكون الاستفادة من العجلة الموجودة أفضل من إعادة اختراعها وتنفيذ كل شيء بأنفسنا، رغم أنّ هذه اللغة التي أنشأناها في هذا المقال والتي تشبه لعب الأطفال لا تفعل أي شيء لا تفعله جافاسكربت بالكفاءة نفسها إن لم يكن أفضل، إلا أن هناك حالات سيكون من المفيد فيها كتابة برامج بسيطة لإنجاز المهام التي بين أيدينا، ومثل هذه اللغة ليس عليها أن تكون على صورة اللغة التقليدية، فإذا لم تأتي جافاسكربت بتعابير نمطية مثلًا، فستستطيع كتابة محلِّل خاص بك ومقيِّم كذلك من أجل هذه التعابير؛ أو لنقل أنك تبني ديناصورًا آليًا عملاقًا وتريد برمجة سلوكه، فقد لا تكون جافاسكربت حينها هي اللغة المثلى لذلك، وستجد أنك تحتاج إلى لغة تبدو هكذا: behavior walk perform when destination ahead actions move left-foot move right-foot behavior attack perform when Godzilla in-view actions fire laser-eyes launch arm-rockets يُطلَق على مثل هذه اللغة أنها لغة مختصة بمجال بعينه domain-specific، وهي لغة مفصَّلة لتعبِّر عن عناصر مجال معين من المعرفة، ولا تتعداه إلى غيره، كما تكون أكثر وصفًا وإفصاحًا عن اللغة الموجَّهة للأغراض العامة لأنها مصمَّمة لتصف الأشياء المراد وصفها حصرًا في هذا المجال وحسب. تدريبات المصفوفات اجعل لغة Egg تدعم المصفوفات من خلال إضافة الدوال الثلاثة التالية إلى النطاق العلوي top scope: array(...values)‎ لبناء مصفوفة تحتوي على قيم وسيطة. length(array)‎ للحصول على طول مصفوفة ما. element(array, n)‎ لجلب العنصر رقم n من مصفوفة ما. تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen. // عدِّل هذه التعريفات... topScope.array = "..."; topScope.length = "..."; topScope.element = "..."; run(` do(define(sum, fun(array, do(define(i, 0), define(sum, 0), while(<(i, length(array)), do(define(sum, +(sum, element(array, i))), define(i, +(i, 1)))), sum))), print(sum(array(1, 2, 3)))) `); // → 6 إرشادات للحل إن أسهل طريقة لإضافة الدعم هي تمثيل مصفوفات Egg بمصفوفات جافاسكربت، ويجب أن تكون القيم المضافة إلى النطاق العلوي دوالًا، كما يمكن أن يكون تعريف array بسيطًا جدًا إذا استَخدَمت وسيط rest مع الصيغة ثلاثية النقاط triple-dot notation. التغليف Closure تسمح لنا الطريقة التي عرَّفنا بها fun بإضافة دوال في Egg للإشارة مرجعيًا إلى النطاق المحيط، مما يسمح لمتن الدالة أن تستخدِم قيمًا محليةً كانت مرئيةً في وقت تعريف الدالة، تمامًا مثلما تفعل دوال جافاسكربت، ويوضِّح البرنامج التالي هذا، إذ تُعيد دالة f دالةً تضيف وسيطها إلى وسيط f، مما يعني أنها تحتاج إلى وصول للنطاق المحلي الموجود داخل f كي تستطيع استخدام الرابطة a. تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen. run(` do(define(f, fun(a, fun(b, +(a, b)))), print(f(4)(5))) `); // → 9 ارجع الآن إلى تعريف صيغة fun واشرح الآلية المسببة لعملها. إرشادات للحل سنستخدِم آليات جافاسكربت مرةً أخرى للحصول على مزايا مشابهة في Egg، حيث تُمرَّر الصيغ الخاصة إلى النطاق المحلي الذي تقيَّم فيه كي تقيِّم هي صيغها الفرعية في ذلك النطاق، ويكون للدالة التي تعيدها fun وصول إلى وسيط scope المعطى للدالة المغلِّفة، كما تستخدِم ذلك لإنشاء نطاق الدالة المحلي حين تُستدعى. يعني هذا أن النموذج الأولي للنطاق المحلي سيكون هو النطاق الذي أُنشَئت فيه الدالة مما يمكننا من الوصول إلى الرابطات التي في ذلك النطاق من خلال الدالة، وهذا كله من أجل استخدام التغليف closure رغم أنك في حاجة إلى مزيد من العمل لتُصرِّف ذلك بكفاءة. التعليقات أليس من الجميل أن تتمكن من كتابة تعليقات في لغة Egg؟ فلو جعلنا التعليق يبدأ بعلامة # مثلًا كما في حال علامتي // في جافاسكربت -ولا تقلق بشأن المحلِّل، إذ لن نجري أيّ تغييرات جوهرية فيه كي يدعم التعليقات-، فما علينا سوى تغيير skipspace كي تتخطى التعليقات كذلك، كما لو كانت مسافات فارغة، بحيث نتخطى التعليقات في كل مرة نستدعي فيها skipspace. الشيفرة أدناه تمثل skipspace، عدِّلها لتضيف دعم التعليقات في لغة Egg، مسترشدًا بما سبق. تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen. function skipSpace(string) { let first = string.search(/\S/); if (first == -1) return ""; return string.slice(first); } console.log(parse("# hello\nx")); // → {type: "word", name: "x"} console.log(parse("a # one\n # two\n()")); // → {type: "apply", // operator: {type: "word", name: "a"}, // args: []} إرشادات للحل تأكد من جعل حلّك قادرًا على معالجة التعليقات المتعددة في صف مع مسافات فارغة محتملة بينها أو بعدها. سيكون التعبير النمطي أسهل طريقة تستخدمها في هذا التمرين للحل، لهذا اكتب شيئًا يطابق "مسافةً فارغةً أو تعليقًا أو صفرًا أو أكثر من مرة". واستخدم التابع exec أوmatch، وانظر إلى طول أول عنصر في المصفوفة المعادة -أي التطابق الكامل- لتعرف كم عدد المحارف التي عليك قصها. إيجاد النطاق الطريقة الوحيدة حاليًا لإسناد قيمة إلى رابطة هي define، حيث تتصرف هذه البنية مثل طريقة لتعريف رابطات جديدة وإعطاء قيمة جديدة للرابطات الحالية. لكن هذا الإبهام يجعلك تقع في مشكلة تعريف رابطة محلية بالاسم نفسه في كل مرة تحاول إعطاء رابطة غير محلية قيمةً جديدةً، كما نستنكر هذه الطريقة في معالجة النطاقات رغم وجود لغات مصممة أصلًا على هذا الأساس. أضف صيغة set الخاصة التي تشبه define، وتعطي قيمةً جديدةً للرابطة لتحدِّث الرابطة في نطاق خارجي إذا لم تكن موجودةً سلفًا في النطاق الداخلي، وإن لم تكن هذه الرابطة معرفةً، فأبلغ بالخطأ ReferenceError الذي هو نوع خطأ قياسي. سيعيقك نوعًا ما أسلوب تمثيل النطاقات على أساس كائنات بسيطة من أجل السهولة ها هنا، خاصةً إذا أردت استخدام دالة Object.getPrototypeOf مثلًا التي تعيد النموذج الأولي لكائن ما، وتذكَّر أيضًا أنّ النطاقات لا تنحدر من Object.prototype، لذا فإن أردت استدعاء hasOWnProperty عليها، فعليك استخدام التعبير التالي: Object.prototype.hasOwnProperty.call(scope, name); specialForms.set = (args, scope) => { // ضع شيفرتك هنا. }; run(` do(define(x, 4), define(setx, fun(val, set(x, val))), setx(50), print(x)) `); // → 50 run(`set(quux, true)`); // → ReferenceError أحد صور تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen. إرشادات للحل سيكون عليك التكرار على نطاق واحد في كل مرة باستخدام Object.getPrototypeOf للذهاب إلى النطاق الخارجي اللاحق. واستخدِم hasOwnProperty لتعرف ما إذا كانت الرابطة الموضَّحة بخاصية name في أول وسيط لـ set موجودةً في ذلك النطاق أم لا، فإن كانت كذلك فاضبطها على نتيجة تقييم الوسيط الثاني لـ set ثم أعد تلك القيمة، وإذا وصلت إلى أقصى نطاق خارجي -بحيث تكون إعادة Object.getPrototypeOf هي null- ولم تعثر على الرابطة بعد، فستكون هذه الرابطة غير موجودة ويجب رمي خطأ هنا. ترجمة -بتصرف- للفصل الثاني عشر من كتاب Elequent Javascript لصاحبه Marijn Haverbeke. اقرأ أيضًا المقال السابق: البرمجة غير المتزامنة في جافاسكريبت دليلك الشامل لتعلم البرمجة النسخة الكامة من كتاب مدخل إلى الذكاء الاصطناعي وتعلم الآلة
  4. رأينا في التدريب الأخير من المقال السابق كيف طبعنا جزءًا من جدول ضرب العدد 12، لكن ذلك تطلب منا كثيرًا من الكتابة، وسيستغرق توسيعه وقتًا طويلًا، ولحسن الحظ توجد طريقة أسهل سترينا بداية المزايا القوية التي توفرها لنا لغات البرمجة في تنفيذ المهام وإنجازها. حلقات for تتيح لغة البرمجة تكرار تنفيذ أمر أو عدة أوامر لعدد محدَّد من المرات، في ما يسمى حلقة تكرار، حيث يمكن استعمال متغير تزيد قيمته مع كل دورة للحلقة، وستبدو هذه العملية في بايثون كما يلي: >>> for n in range(1,13): ... print( "%d x 12 = %d" % (n, n*12) ) ... 1 x 12 = 12 2 x 12 = 24 3 x 12 = 36 4 x 12 = 48 5 x 12 = 60 6 x 12 = 72 7 x 12 = 84 8 x 12 = 96 9 x 12 = 108 10 x 12 = 120 11 x 12 = 132 12 x 12 = 144 لندرس الشيفرة السابقة ونستخرج بعض الملاحظات منها: انتهى سطر for بنقطتين رأسيتين (:)، وهذا أمر مهم، إذ يخبر بايثون أن ما سيكتَب بعد هاتين النقطتين هو ما سيُكرَّر. كتبنا مجال الأعداد الذي نريده في دالة range()‎ بالشكل range(1,13)‎، رغم أننا نريد تكرار عملية الضرب إلى العدد 12 فقط، لأن دالة range()‎ تولد الأعداد من العدد الأول فيها إلى العدد الذي يسبق العدد الثاني، 13 في مثالنا، وهذا له أسبابه قطعًا وستعتاده مع الوقت، لكن تجدر الإشارة إلى أن هذه الدالة تستطيع توليد مجموعات معقدة من الأعداد، لكننا اخترنا هذا المثال البسيط لأنه يحقق الغرض. العامل for في بايثون هو عامل foreach في الواقع، إذ يطبق تسلسل الشيفرة التالي على كل عنصر في التجميعة، والتي هي في حالتنا قائمة الأعداد التي تولدها الدالة range()‎، ونستطيع استخدام قائمة أعداد صراحةً عوضًا عن الدالة range()‎، بالشكل التالي: >>> for n in [1,2,3,4,5,6,7,8,9,10,11,12]: ... print( "%d x 12 = %d" % (n, n*12) ) أُزيح سطر print موازنةً بسطر حلقة for الذي فوقه، وهذا مهم جدًا لأن هذه الإزاحة تخبر بايثون أن عليها تكرار القسم الخاص بـ print، ومن الممكن إزاحة أكثر من سطر، وستكرر بايثون هذه الأسطر المزاحة جميعًا لكل عنصر في التجميعة، ولا يهم مقدار الإزاحة المستخدمة طالما هي نفسها في كل الأسطر المزاحة، وستخبرنا بايثون إذا وجدت اختلافًا في الإزاحة. نحتاج إلى ضغط زر الإدخال Enter مرتين في المفسر التفاعلي لتشغيل البرنامج، لأن مفسر بايثون يضيف سطرًا جديدًا إلى شيفرة الحلقة التكرارية عندما نضغط مرةً واحدةً؛ أما مع الضغطة الثانية فستفترض بايثون أننا أنهينا إدخال الشيفرة، فتشغّل البرنامج. وبما أننا قد رأينا الآن هيكل حلقة for، لننظر في كيفية عملها خطوةً خطوةً على النحو الآتي: أولًا: تستخدم بايثون دالة range()‎ لتوليد قائمة أعداد من 1 إلى 12، وفقًا لآلية خاصة تسمى بالمولد generator، وتشبه القائمة التي تزيد من عناصرها عنصرًا واحدًا في كل مرة حسب الطلب، مما يوفر الذاكرة عندما تكون القائمة كبيرةً، وهو نفس سبب توليد القائمة صراحةً باستخدام list()‎ عندما طبعنا range()‎ أعلاه، وهذا شبيه بما فعلناه في مثال مفاتيح القاموس keys()‎ في المقال الخامس من هذه السلسلة والمتعلق بالمواد الخام للبرمجة. ثانيًا: تجعل بايثون قيمة n مساويةً للقيمة الأولى في القائمة، وهي 1 في هذه الحالة، ثم تنفذ الشيفرة المزاحة باستخدام القيمة n = 1: print( "%d x 12 = %d" % (1, 1*12) ) ثم تعود إلى سطر for وتضبط n على القيمة التالية في القائمة، وهي 2 في هذه الحالة، ثم تنفذ الشيفرة المزاحة مع القيمة n = 2: print( "%d x 12 = %d" % (2, 2*12) ) وتستمر في تكرار هذا التسلسل حتى تمر n على جميع القيم في القائمة، ثم تنتقل إلى الأمر التالي غير المزاح، في حالتنا لا توجد أي أوامر، لذا سيتوقف البرنامج. الحلقة التكرارية في VBScript تُعَد For...Next أبسط حلقة تكرارية في VBScript، وتُستخدم بالشكل التالي: <script type="text/vbscript"> For N = 1 To 12 MsgBox N & " x 12 = " & N*12 Next </script> يُعَد أسلوب VBScript أوضح وأسهل في فهم ما تفعله الشيفرة، إذ تتغير قيمة N من 1 إلى 12، وتنفَّذ الشيفرة التي توجد قبل الكلمة المفتاحية Next، وفي حالتنا تطبع الشيفرة النتيجة في صندوق حواري؛ أما إزاحة السطر هنا فاختيارية وليست شرطًا، لكنها تجعل الشيفرة أسهل في القراءة، وقد يحتوي متن الحلقة التكرارية على أكثر من سطر ينفَّذ على عناصر القائمة، كما في حالة بايثون التي رأيناها قبل قليل، ورغم أن VBScript تبدو أوضح للوهلة الأولى؛ إلا أن بايثون أكثر مرونةً كما سنرى بعد قليل. الحلقة التكرارية في جافاسكربت تستخدم جافاسكربت البُنية for الشائعة في كثير من لغات البرمجة الشبيهة بلغة C، وستبدو الحلقة كما يلي: <script type="text/javascript"> for (n=1; n <= 12; n++){ document.write(n + " x 12 = " + n*12 + "<BR>"); }; </Script> تبدو هذه الشيفرة معقدةً للوهلة الأولى، وهي تتكون من ثلاثة أجزاء بين القوسين () هي: جزء البداية: n = 1 الذي يُنفَّذ مرةً واحدةً قبل أي شيء آخر. جزء الاختبار: n <= 12 الذي يُنفَّذ قبل كل تكرار. جزء الزيادة: n++‎ وهو اختصار "زِد n بمقدار 1"، وينفَّذ بعد كل تكرار. لاحظ أن جافاسكربت تضع الشيفرة المكرَّرة، أي متن الحلقة التكرارية، بين قوسين معقوصين {}، وهذا كافٍ لتكون الحلقة صالحةً للتنفيذ، إلا أنه يفضَّل إزاحة الشيفرة التي داخل الأقواس المعقوصة لتحسين قابلية قراءة الشيفرة. لن ينفَّذ متن الحلقة loop body إلا إذا تحقق جزء الاختبار، أي كان true. وقد تحتوي هذه الأجزاء على شيفرات عشوائية، إلا أن الجزء الثاني الخاص بالاختبار يجب أن يعطي قيمةً بوليانيةً. مزيد من المعلومات حول حلقة for في بايثون تتكرّر حلقة for في بايثون على تسلسل، تذكر أن التسلسل الذي شرحناه في المقالات السابقة يشمل أشياء، مثل السلاسل النصية والقوائم والصفوف tuples، كما تستطيع بايثون أن تكرِّر عدة أنواع أخرى لكننا سنؤجل ذلك إلى وقت لاحق، وبناءً على ذلك نستطيع كتابة حلقات for تتعامل مع أي نوع من التسلسلات، ولنجرب مثلًا طباعة حروف كلمة حرفًا حرفًا باستخدام حلقة for مع سلسلة نصية: >>> for c in 'word': print( c ) ... w o r d لاحظ أننا طبعنا كل حرف على سطر منفصل، وأنه يمكننا إضافة متن الحلقة، عندما يكون سطرًا واحدًا، إلى نفس سطر الحلقة بوضعه بعد نقطتين رأسيتين :، إذ تخبران بايثون بوجود كتلة برمجية تليهما. ويمكن التكرار على صف tuple: >>> for word in ('one','word', 'after', 'another'): print (word) ... جعلنا في هذا المثال كل كلمة في سطر منفصل على خلاف المثال السابق، ويمكن وضعها جميعًا في سطر واحد باستخدام ميزة خاصة بدالة print()‎، إذ نستطيع إضافة وسيط argument بعد العنصر المراد طباعته كما يلي: >>> for word in ('one', 'word', 'after', 'another'): print( word, end='' ) ... لاحظ كيف ستظهر الكلمات الآن في سطر واحد، لأن الوسيط end=''‎ يخبر بايثون أن تستخدم سلسلةً نصيةً فارغةً '' لنهاية السطر عوضًا عن محرف السطر الجديد الذي تستخدمه افتراضيًا، لنجرب حلقة for مرةً أخرى مع القوائم: >>> for item in ['one', 2, 'three']: print( item ) ... ستظهر هنا مشكلة عند استخدام الحلقات التكرارية التي على نمط foreach، وهي أن الحلقة تعطينا نسخةً مما كان في التجميعة، دون أن نستطيع تعديل محتوياتها مباشرةً، فإذا احتجنا إلى تعديلها؛ فعلينا استخدام برنامج غريب الشكل نستخدم فيه فهرس التجميعة، كما يلي: myList = [1,2,3,4] for index in range(len(myList)): print( myList[index] ) myList[index] += 1 print( myList ) سيزيد هذا البرنامج كل فهرس في myList، ولو أننا لم نستخدم طريقة الفهرس تلك، لكنا زدنا العناصر المنسوخة فقط دون تغيير القائمة الأصلية، وقد صار لدينا متن متعدد الأسطر في حلقتنا التكرارية. myList = [1,2,3,4] for index, value in enumerate(myList): print( value ) myList[index] += 1 print( myList ) لاحظ أننا لم نستخدم محث بايثون التفاعلي ‎>>>‎ في هذا المثال، لذا عليك كتابته في ملف كما شرحنا في مقال المزيد من التسلسلات البرمجية وأمور أخرى؛ أما إذا حاولنا كتابته في محث بايثون، فسنحتاج إلى أسطر فارغة إضافية لنخبر بايثون بانتهاء كتلة برمجية، وذلك بعد السطر myList[index] += 1 مثلًا، وهذه الطريقة مفيدة لتعلّم بداية الكتل البرمجية ونهايتها، بكتابة الشيفرة وتخمين أماكن الحاجة إلى السطر الإضافي، إذ يجب أن يكون عند الموضع الذي تتغير فيه الإزاحة. لا بد من ملاحظة أمر آخر في حلقات foreach، وهو أننا لا نستطيع حذف العناصر من التجميعة التي نمر عليها، لأن ذلك يربك الحلقة نفسها، فهو يشبه قطع فرع من شجرة أثناء جلوسك عليه، والحل هو استخدام نوع مختلف من الحلقات التكرارية، كما سنرى في جزئية لاحقة من هذه السلسلة. ربما تجب الإشارة إلى أن كلًا من جافاسكربت وVBScript لديهما بنىً للتكرار على العناصر الموجودة في تجميعة ما، ولن نتحدث عنها بتفصيل هنا، لكن إذا أردت أن تبحث في الأمر بنفسك فانظر صفحات المساعدة لبنية for each...in...‎ في لغة VBScript، وبنية for...in...‎ في جافاسكربت. حلقات While التكرارية تتطلب حلقات for أن يعلم المبرمج عدد مرات التكرار التي يريد تنفيذها قبل بدء الحلقة نفسها، أو أن يستطيع حسابها على الأقل، لكن ماذا لو أردنا تنفيذ مهمة ما حتى وقوع حدث معين لا نعرف موعده؟ فقد نرغب في قراءة بيانات من المستخدم ومعالجتها مثلًا إلى أن يخبرنا المستخدم نفسه بالتوقف عن ذلك، كما لن نعرف عدد عناصر البيانات التي سنعالجها إلى أن يكتفي المستخدم ويخبرنا بذلك أيضًا. يُعَد تنفيذ مثل هذه المهام باستخدام حلقة for ممكنًا نظريًا لكنه معقد، لهذا لدينا نوع آخر من الحلقات التي تحل مثل هذه المشاكل، وهي حلقة while، وستبدو في بايثون بالشكل التالي: >>> j = 1 >>> while j <= 12: ... print( "%d x 12 = %d" % (j, j*12) ) ... j = j + 1 لنحلل هذه الشيفرة: نهيئ أولًا j لتأخذ القيمة 1، وهذه الخطوة مهمة للغاية، أي تهيئة متغير التحكم في حلقة while، لأن نسيان تهيئته يتسبب في أخطاء كثيرة. ننفذ تعليمة while نفسها، التي تختبر تعبيرًا بوليانيًا هو j<=12 في حالتنا. ننفذ الكتلة المزاحة التي تتبعها إذا كانت النتيجة True، وهو محقق في حالتنا بما أن قيمة j أقل من 12، لذا سننتقل إلى الكتلة. ننفذ تعليمة الطباعة لإخراج أول سطر من جدولنا. يزيد السطر التالي في الكتلة متغير التحكم j، وهو السطر الأخير في حالتنا، ليشير إلى نهاية كتلة while. نعود إلى تعليمة while ونكرر الخطوات من 4 إلى 6 بقيمة j الجديدة. نكرر هذا التسلسل إلى أن تصل قيمة j إلى 13. هنا يعيد اختبار while القيمة False، فنتخطى الكتلة المزاحة إلى السطر الجديد الذي يوازي تعليمة while في إزاحتها. يتوقف البرنامج في مثالنا لأنه لا توجد أسطر أخرى. تعليمة while واضحة وبسيطة كما رأينا، لكننا نريد الإشارة إلى النقطتين الرأسيتين (:) اللتين في نهاية سطر تعليمة while -وتعليمة for أيضًا-، إذ تخبر هاتان النقطتان بايثون أن ما يليهما كتلة برمجية أو مجموعة مترابطة من التعليمات، وكل لغة لها طريقتها في إخبار المفسر بجمع عدة أسطر معًا كما سنرى، ففي حالة بايثون مثلًا، سنستخدم النقطتين الرأسيتين وإزاحة الأسطر. حلقة While في VBScript فيما يلي حلقة while في لغة VBScript: <script type="text/vbscript"> DIM J J = 1 Do While J <= 12 MsgBox J & " x 12 = " & J*12 J = J + 1 Loop </script> يعطينا هذا المثال نفس النتيجة التي حصلنا عليها من مثال بايثون السابق، مع ملاحظة انتهاء كتلة الحلقة التكرارية بالكلمة المفتاحية Loop حلقة While في جافاسكربت <script type="text/javascript"> j = 1; while (j <= 12){ document.write(j + " x 12 = "+ j*12 + "<BR>"); j++; } </script> تتشابه هذه البنية مع ما سبق، لكن مع بعض الأقواس المعقوصة بدلًا من كلمة Loop التي في VBScript، لاحظ أن ++J تعني زيادة قيمة j بمقدار 1، كما ذكرنا في مقال سابق، وأن جافاسكربت وVBScript لا تشترطان إزاحة الأسطر، على عكس لغة بايثون، لكننا أزحنا الأسطر لتسهيل قراءة الشيفرة فقط. لنراجع الآن حلقة for في جافاسكربت: for (j=1; j<=12; j++){....} نلاحظ أنها تشبه حلقة while تمامًا، لكنها مكتوبة في سطر واحد بحيث يمكن رؤية المهيئ initializer وشرط الاختبار ومعدِّل الحلقة معًا، وبهذا يتبين أن حلقة for في جافاسكربت ما هي إلا حلقة while ولكن بصورة مضغوطة، وسنستنتج أن بعض اللغات قد تستغني عن حلقة for نهائيًا، وهو ما يحدث حقًا. حلقات تكرارية مرنة إذا عدنا إلى مثال جدول ضرب العدد 12 الذي ذكرناه في بداية هذا المقال، فسنجد أن الحلقة التي أنشأناها ستطبع جدول هذا العدد بكفاءة، لكن هل يمكننا تعديل الحلقة لجعلها تطبع جدول العدد 7 مثلًا؟ ينبغي أن تبدو الحلقة كما يلي: >>> for j in range(1,13): ... print( "%d x 7 = %d" % (j,j*7) ) وهذا يعني أن علينا تغيير 12 إلى 7 مرتين، وإذا أردنا قيمةً أخرى غيرهما فسنغير في موضعين من جديد، ولكن ألا توجد طريقة أفضل لإدخال مضاعف الضرب ذاك؟، يمكن ذلك باستخدام متغير آخر للقيم التي في سلسلة الطباعة، ثم ضبط ذلك المتغير قبل تشغيل الحلقة التكرارية: >>> multiplier = 12 >>> for j in range(1,13): ... print( "%d x %d = %d" % (j, multiplier, j*multiplier) ) وهذا جدول العدد 12 السابق، وإذا أردنا تغييره إلى العدد 7 مثلًا، فلا نغير إلا قيمة multiplier، جرب كتابة هذا البرنامج في ملف سكربت بايثون وتشغيله من محث سطر الأوامر، ثم عدِّل قيمة المضاعف multiplier إلى أعداد أخرى، لاحظ أننا جمعنا هنا بين التسلسل والحلقات التكرارية، فقد كتبنا أمرًا وحيدًا في البداية هو multiplier = 12، متبوعًا بحلقة for. تكرار الحلقة نفسها لنطور مثالنا السابق قليلًا، ولنفترض أننا نريد طباعة جميع جداول الضرب للأعداد من 2 حتى 12، سيكون كل ما نحتاجه هو ضبط متغير المضاعف ليكون جزءًا من الحلقة التكرارية، ونفعل بذلك بالشكل التالي: >>> for multiplier in range(2,13): ... for j in range(1,13): ... print( "%d x %d = %d" % (j,multiplier,j*multiplier) ) لاحظ أن الجزء المزاح داخل حلقة for الأولى هو نفس الحلقة التي بدأنا بها، وستنفَّذ الحلقة كما يلي: نضبط multiplier أولًا على القيمة الأولى 2 ثم ننتقل إلى الحلقة الثانية الداخلية. نعيد ضبط multiplier على القيمة التالية 3 ثم ننتقل إلى الحلقة الداخلية مرةً أخرى. نكرر هذا حتى نمر على جميع الأعداد. يُعرف هذا الأسلوب باسم الحلقات المتشعبة nesting loops، لكن من مساوئه أن جميع الجداول ستكون مدمجةً معًا، ونستطيع إصلاح هذه المشكلة بطباعة سطر فاصل في نهاية الحلقة الأولى، كما يلي: >>> for multiplier in range(2,13): ... for j in range(1,13): ... print( "%d x %d = %d" % (j,multiplier,j*multiplier) ) ... print( "------------------- " ) لاحظ أن تعليمة الطباعة الثانية لها نفس إزاحة تعليمة for الثانية، وهي ثاني تعليمة في تسلسل التكرار، فتسلسل الإزاحة مهم جدًا في بايثون كما ذكرنا. دعنا نرى الآن كيفية تنفيذ هذا التدريب في جافاسكربت، لرؤية الفرق بينهما فقط: <script type="text/javascript"> for (multiplier=2; multiplier < 13; multiplier++){ for (j=1; j <= 12 ; j++){ document.write(j, " x ", multiplier, " = ", j*multiplier, "<BR>"); } document.write("---------------<BR>"); } </script> حاول أن تجعل السطر الفاصل يشير إلى الجدول التابع له، يمكنك استخدام متغير المضاعف وسلسلة التنسيق التي في بايثون لفعل ذلك. حلقات أخرى توفر بعض اللغات الأخرى بنىً مختلفةً للتكرار، كما رأينا في مثال VBScript أعلاه، غير أنها لا تخلو من صورة ما لحلقتي for وwhile، ولا تحوي بعض اللغات، مثل Modula 2 وOberon، إلا حلقة while فقط، بما أننا نستطيع تمثيل سلوك for منها كما رأينا قبل قليل. ومن الحلقات التكرارية الموجودة في اللغات الأخرى: do-while: هذه الحلقة هي نفسها حلقة while لكن مع وجود الاختبار في نهايتها، بحيث تُنفَّذ الحلقة مرةً واحدةً على الأقل. repeat-until: شبيهة بالسابقة أيضًا لكن مع عكس المنطق. GOTO وJUMP وLOOP: هذه الحلقات التكرارية موجودة في اللغات القديمة، وهي تعين علامةً في الشيفرة ثم تقفز إليها مباشرةً. خاتمة لقد رأينا أن حلقات for تكرر مجموعةً من الأوامر لعدد محدد وثابت من المرات، وأن while تكرر تلك الأوامر إلى أن يتحقق شرط محدد في الحلقة، ولا تنفذ متن الحلقة أصلًا إذا كان شرط إنهاء الحلقة غير متحقق من البداية، أي أعطى اختباره النتيجة false، وعلمنا أنه توجد أنواع أخرى من الحلقات التكرارية؛ لكن اللغة التي تحتوي عليها، ستحتوي على حلقتي for وwhile أو إحداهما على الأقل، وأن حلقات for في بايثون ما هي إلا حلقات foreach حقيقةً، إذ تعمل على قائمة من العناصر، كما تعلمنا الحلقات التكرارية المتشعبة، بإدخال حلقة فرعية داخل أخرى أكبر منها. ترجمة -بتصرف- للفصل السابع: Looping - Or the art of repeating oneself من كتاب Learning To Program لصاحبه Alan Gauld. اقرأ أيضًا المقال التالي: أسلوب كتابة الشيفرات البرمجية وتحقيق سهولة قراءتها المقال السابق: بعض التسلسلات النصية المهمة لتعلم البرمجة ما هي البرمجة ومتطلبات تعلمها؟ الحلقات التكرارية Loops في Cpp بنى التحكم والحلقات التكرارية في PHP الحلقات التكرارية في لغة سي شارب #C حلقات for التكرارية ببساطة في جافاسكريبت
  5. إذا قرأت المقالات السابقة من هذه السلسلة فأنت على دراية بأننا تعلمنا فيها كيف نكتب أوامر بسيطةً وحيدة المدخلات في بايثون، وعرفنا أهمية البيانات وما نفعله بها، ثم كتبنا تسلسلات أطول قليلًا (5-10 أسطر). وها نحن نقترب من كتابة برامج مفيدة، لكن لا زالت لدينا عقبة هنا، وهي خسارة البيانات والبرامج كلما خرجنا من بايثون على الحاسوب، وإذا كنت تستخدم جافاسكربت أو VBScript فسترى أنك قد خزنت تلك الأمثلة في ملفات تستطيع تشغيلها وقتما أردت، ونريد أن نفعل ذلك مع بايثون أيضًا، وقد ذكرنا من قبل أن هذا ممكن باستخدام أي محرر نصي مثل notepad أو pico ، وذلك بحفظ ملف بامتداد ‎.py ثم تشغيل الملف من محث أوامر نظام التشغيل مع كتابة اسم السكربت مسبوقًا بكلمة python، يستخدم المثال التالي تسلسلًا من أوامر بايثون، فيه كل النقاط التي شرحناها من قبل: # File: firstprogram.py print( "hello world" ) print( "Here are the ten numbers from 0 to 9\n0 1 2 3 4 5 6 7 8 9" ) print( "I'm done!" ) # نهاية الملف لاحظ أن السطرين الأول والأخير ليسا ضروريين، وهما تعليقان سنشرحها في هذا المقال، وقد أضفناهما لنوضح ما يحدث في الملف. يمكن استخدام notepad أو أي محرر نصي لإنشاء الملف طالما سنحفظه بصيغة نصية مجردة، ولا نستخدم معالج نصوص متقدمًا مثل MS Word، لأنه يحفظ الملفات بصيغة ثنائية خاصة لا تستطيع بايثون فهمها، ولا نستخدم صيغة html التي تحوي كثيرًا من النصوص المنسقة التي لا تعني شيئًا لبايثون كذلك. لتشغيل هذا البرنامج افتح سطر أوامر نظام التشغيل الآن، يمكنك مراجعة مقال البداية في تعلم البرمجة لمعرفة سطر أوامر النظام، وانتقل إلى المجلد الذي حفظت فيه ملف بايثون لديك، ثم نفّذه بكتابة python قبله كما ذكرنا: D:\PROJECTS\Python> python firstprogram.py hello world Here are the ten numbers from 0 to 9 0 1 2 3 4 5 6 7 8 9 I'm done! D:\PROJECTS\Python> في السطر الأول محث أوامر ويندوز إضافةً إلى الأمر الذي كتبناه، ثم خرج البرنامج معروضًا قبل أن يظهر محث ويندوز مرةً أخرى، لكن توجد طريقة أسهل، فقد يكون لدينا أكثر من إصدار لبايثون على نفس الحاسوب إذا كان النظام نفسه يستخدم الإصدار الثاني منها لبعض المهام مثلًا، وإذا كتبنا python في تلك الحالة فقد نحصل على أخطاء لم نتوقعها، ولتجنب مثل تلك الحالة نلحق رقم إصدار بايثون الذي نريد استخدامه إلى الاسم، فإذا كنا نريد الإصدار الثالث نكتب: D:\PROJECTS\Python> python3 firstprogram.py لاحظ أنها صارت الآن python3 بدلًا من python فقط، وبذلك سنتجنب كثيرًا من الأخطاء على أغلب نظم التشغيل. مزايا استخدام البيئة المتكاملة يثبَّت مع بايثون تلقائيًا عند تثبيتها على الحاسوب تطبيق مفيد، وهو برنامج مكتوب بلغة بايثون أيضًا، ويسمى IDLE اختصارًا لبيئة التطوير المتكاملة Integrated Development Environment، وهو يعني أن فيه أدوات عديدةً مدمجةً فيه لمساعدة المبرمجين، ولن نتعمق فيه هنا، لكن نريد تسليط الضوء على مزيتين فيه، أولاهما أنه يوفر نسخةً محسنةً من محث بايثون ‎>>>‎ فيها تظليل للكلمات، حيث تُعرَض خصائص اللغة بألوان مختلفة لإبرازها، إضافةً إلى محرر نصي خاص ببايثون، يسمح لك بتشغيل ملفات برنامجك مباشرةً من داخل IDLE، وننصحك بتجربة IDLE إذا لم تكن قد فعلت، وإذا أردت شرحًا للبرنامج فانظر دليل داني يو Danny Yoo عنه. أما في حالة استخدام ويندوز فثمة خيار آخر هو PythonWin الذي يمكن تحميله ضمن حزمة PyWin32، والذي يسمح لنا بالوصول إلى جميع دوال البرمجة الدنيا لـمكتبة MFC الخاصة بويندوز، وهو بديل جيدًا جدًا لبيئة IDLE، لكنه لا يعمل إلا على ويندوز، وستحصل عليه تلقائيًا إذا حمّلت بايثون من ActiveState، فنسختهم فيها جميع مزايا PyWin32 بما فيها Pythonwin افتراضيًا. لكن من الناحية الأخرى فإن IDLE يأتي افتراضيًا في حزمة بايثون القياسية، وهو يعمل على أغلب أنظمة التشغيل، لذا يُستخدم أكثر، ولا تهم الأداة التي تستخدمها بأي حال طالما ليست notepad من ويندوز إذ الخيارات المتقدمة أفضل من مجرد محرر نصي بسيط. أخيرًا، إذا كنت تفضل الأسلوب البسيط في كتابة البرامج فستجد محررات عدةً تدعم البرمجة ببايثون، حيث يوفر محرر ViM مثلًا تظليلًا للكلمات المفتاحية، ويوفر emacs وضع تعديل كامل خاص ببايثون، كما يوفر محرر Scite الخفيف تظليل الكلمات المفتاحية لبايثون إضافةً إلى مزايا أخرى. وإذا اخترت طريق المحررات النصية فستجد نفسك مع الوقت تعمل أمام ثلاث نوافذ في نفس الوقت، هي نافذة المحرر التي تكتب فيها شيفرتك المصدرية وتحفظها، وجلسة بايثون التي تجرب فيها ما تكتب من خلال محث بايثون قبل إضافتها إلى برنامجك في المحرر، وسطر أوامر نظام التشغيل الذي تستخدمه لتشغيل البرنامج من أجل اختباره. أما أغلب المبتدئين فيفضلون أسلوب بيئات التطوير المتكاملة التي تأتي كلها في نافذة واحدة مثل IDLE، والأمر كله اختياري فيه سعة ولهذا اختر ما يناسبك، وإذا كنت تستخدم جافاسكربت أو VBScript؛ فنقترح استخدام أحد المحررات النصية المذكورة أعلاه، ومتصفح ويب حديث لجافاسكربت مثل كروم أو فاير فوكس؛ أما بالنسبة للغة VBScript فهذا يعني إنترنت إكسبلورر حصرًا، وليس متصفح Edge حتى، ويكون المتصفح مفتوحًا على الملف الذي تعمل عليه، فإذا أردت تجربة تغيير أو تعديل ما، فاضغط زر إعادة تحميل الصفحة. التعليقات السريعة إحدى أهم الأدوات البرمجية التي لا يشعر المبتدئون بأهميتها هي التعليقات، وهي أسطر داخل البرنامج تصف ما يحدث فيه، وليس لها أي تأثير على أداء البرنامج أو سير عملياته، فهي أسطر وصفية لما يحدث فقط، إلا أن دورها في غاية الأهمية، إذ تخبر المبرمج ما يفعله البرنامج في كل كتلة برمجية أو كل سطر، ولماذا يتصرف على هذا النحو، وتظهر أهمية التعليقات هنا إذا كان البرنامج الذي يقرؤه المبرمج قد كتبه شخص غيره، أو قد كتبه هو بنفسه لكن منذ مدة طويلة فنسي سبب اختياره لخاصية أو دالة بعينها. وسيشعر المبرمج بأهمية التعليقات حسنة الوصف مع الوقت، وقد أضفنا تعليقات إلى بعض الشيفرات التي استخدمناها في شرح هذه السلسلة إلى الآن، فهي تلك الأسطر المسبوقة بعلامة # في بايثون، أو بعلامة ' في VBScript، وسنبدأ تدريجيًا من الآن بكتابة شرح الشيفرة في التعليقات، إلى أن يقل ما نكتبه من الشرح خارج الشيفرة ثم يختفي. لكل لغة طريقة في ترميز التعليقات، ففي VBScript مثلًا تكون REM مثل اختصار إلى كلمة Remark أو ملاحظة، لكن يشيع استخدام العلامة ' لتؤدي نفس الوظيفة بحيث يتجاهل البرنامج أي شيء مكتوب بعدها. REM لن يُعرض هذا السطر ' ولا هذا msgBox "أما هذا السطر فسيُعرض" لاحظ أن استخدام علامة اقتباس مفردةً ' مثل محدد للتعليقات في VBScript هو سبب منع بدء السلسلة النصية بها، لئلا تظن اللغة أنها تعليق، أما بايثون فتستخدم رمز # مثل حدد للتعليقات فيها، وتتجاهل أي شيء يتبعه: v = 12 # القيمة 12 v أعط x = v*v # v تساوي مربع x غير أن هذا الأسلوب في التعليقات سيء للغاية، إذ لا يجب أن تصف التعليقات ما تفعله الشيفرة فحسب، فنحن نستطيع رؤية ذلك بأنفسنا، لكنها يجب أن تصف سبب اختيار المبرمج لهذه الدالة أو تلك القيمة: v = 3600 # 3600 عدد الثواني في الساعة الواحدة s = t*3600 # تحتوي الوقت المار بالساعة t # لذا نحولها إلى ثوانٍ. هذه النسخة من التعليقات أكثر فائدةً إذ تشرح سبب ضرب t في 3600. تستخدم جافاسكربت شرطتين مائلتين // مثل محدد للتعليقات، ثم تتجاهل أي شيء بعدهما، كما تسمح بعض اللغات بتعليقات متعددة الأسطر بين زوجين من الرموز، لكن هذا قد يؤدي إلى بعض الأخطاء الخفية إذا لم يوضع رمز الإغلاق، وتسمح جافاسكربت بهذا النوع من التعليقات باستخدام الرمز ‎/*‎ متبوعًا برمز الإغلاق ‎*/‎ بعد نهاية التعليقات: <script type="text/javascript"> document.write("هذا السطر يُطبع\n"); // تعليق من سطر واحد /* هنا تعليق نقسمه على عدة أسطر فيحتوي على هذا السطر وهذا السطر وذا أيضًا ولن يظهر أي من هذا في خرج السكربت فإذا أردنا إنهاء التعليق نعكس رمز البدء ليكون كما يلي: */ document.write("وهذا أيضًا"); </script> تبرز أهمية التعليقات كما ذكرنا في أنها تشرح الشيفرة لأي شخص يحاول قراءتها، حيث يجب الحرص على توضيح الأقسام الغامضة مثل القيم العشوائية المستخدمة، أو المعادلات الحسابية المعقدة. تذكر أن ذلك القارئ المحتار في فهم شيفرتك قد يكون أنت نفسك بعد بضعة أسابيع أو أشهر. التسلسلات باستخدام المتغيرات لقد شرحنا مفهوم المتغيرات في مقال مدخل إلى البيانات وأنواعها: أنواع البيانات الأساسية، وقلنا إنها عناوين نعلّم بها بياناتنا من أجل الإشارة إليها في المستقبل، ورأينا بعض الأمثلة عن استخدام المتغيرات في عدة أمثلة لقوائم ودفاتر عناوين، غير أن المتغيرات بالغة الأهمية في البرمجة إلى الحد الذي يجعلنا نعيد مراجعة كيفية استخدامها مرةً أخرى قبل أن ننتقل إلى جزء جديد. اكتب ما يلي في محث بايثون، سواءً في صدفة IDLE أو في نافذة أوامر دوس أو يونكس: >>> v = 7 >>> w = 18 >>> x = v + w # استخدم متغيرنا في عملية حسابية >>> print( x ) ما حدث في الشيفرة أعلاه هو أننا أنشأنا المتغيرات v وw وx وعدّلناها، وهذا أشبه بزرّ M على حاسبة الجيب القديمة التي تخزن نتيجةً لاستخدامها لاحقًا، يمكن تحسين تلك الشيفرة باستخدام سلسلة تنسيق لطباعة النتيجة: >>> print( "The sum of %d and %d is: %d" % (v,w,x) ) إحدى مزايا سلاسل التنسيق هنا أننا نستطيع تخزينها في متغيرات أيضًا: >>> s = "The sum of %d and %d is: %d" >>> print( s % (v,w,x) ) # هذا مفيد عند طباعة نفس الخرج بقيم مختلفة هذا يقصّر كثيرًا من طول تعليمة الطباعة، خاصةً حين تحتوي على قيم كثيرة، لكنه يجعلها أكثر غموضًا، وعندها تقرر بنفسك أيهما الأفضل من حيث القراءة: الأسطر الطويلة أم القصير؟ فإذا أبقينا سلسلة التنسيق هنا إلى جانب تعليمة الطباعة كما فعلنا فهذا حسن. يفضل تسمية المتغير بطريقة تشرح الغرض من وجوده، فبدلًا من تسمية سلسلة التنسيق باسم s، يفضل تسميتها sumFormat فتبدو الشيفرة كما يلي: >>> sumFormat = "The sum of %d and %d is: %d" >>> print( sumFormat % (v,w,x) ) # هذا مفيد عند طباعة نفس الخرج بقيم مختلفة يمكن معرفة أي تنسيق تمت طباعته بمجرد النظر إليه في هذا البرنامج لأنه لا يحتوي على سلاسل تنسيق كثيرة، لكن لا تزال فكرة التسمية المنطقية للمتغيرات ساريةً هنا، وسنحاول قدر الإمكان استخدام أسماء ذات معنى في ما يلي من الشرح؛ أما المتغيرات التي ذكرناها إلى الآن فلم يكن لها معنىً يستحق التسمية. أهمية الترتيب قد يظن المرء أن بنية التسلسل تلك مبالغ فيها لبداهتها، ولعل هذا صحيح بما أنها بديهية فعلًا، لكن الأمر ليس بتلك البساطة التي يبدو عليها، فقد تكون هناك مشاكل خفية كما في حالة الرغبة في ترقية جميع الترويسات في ملف HTML مثلًا، بمقدار رتبة واحدة، انظر المثال التالي، حيث تُميَّز الترويسات في HTML بإحاطة النص بوسم يشير إلى أنها ترويسة مع رتبتها: <h1> نص </h1> لترويسة المستوى الأول <h2> نص </h2> لترويسة المستوى الثاني <h3> نص </h3> لترويسة المستوى الثالث والمشكلة هنا أننا حين نصل إلى المستوى الخامس يصير نص الترويسة أصغر من نص المتن العادي نفسه، مما يبدو غريبًا على القارئ إذ يرى نص العنوان أصغر من متنه، وعلى ذلك نريد ترقية جميع الترويسات دفعةً واحدةً، يسهُل هذا بعملية بحث واستبدال في محرر نصي بسيط بأن نستبدل h2 بـ h1 مثلًا، لكن ماذا لو بدأنا بالأرقام الأكبر، مثل h4 إلى h3، ثم h3 إلى h2 وهكذا؛ سنجد في النهاية أن جميع الترويسات صارت من المستوى الأول h1، وهنا تبرز أهمية ترتيب الأحداث والإجراءات، وذلك ينطبق في حالة كتابتنا لبرنامج ينفذ عملية الاستبدال، وهو ما سنفعله على الأغلب بما أن ترقية الترويسات عملية واردة الحدوث، وقد رأينا أمثلةً أخرى نستخدم فيها المتغيرات والتسلسلات في مقالي مدخل إلى البيانات وأنواعها: أنواع البيانات الأساسية ومدخل إلى البيانات وأنواعها: التجميعات Collections، خاصةً أمثلة دفتر العناوين. جرب التفكير في أمثلة مشابهة لحلها، وإذا أتممت هذا فسننتقل إلى دراسة حالة جديدة نطورها بالتدريج كلما تقدمنا في هذه السلسلة، لنحسنها مع كل تقنية نتعلمها. جدول الضرب سنشرح الآن تدريبًا برمجيًا نطوره معنا أثناء دراسة المقالات التالية، وستتطور حلوله تدريجيًا مع تعلم تقنيات جديدة، ذكرنا أننا نستطيع كتابة سلاسل نصية طويلة بتغليفها بعلامات اقتباس ثلاثية، لنستخدم هذا الأسلوب لبناء جدول ضرب: >>> s = """ 1 x 12 = %d 2 x 12 = %d 3 x 12 = %d 4 x 12 = %d """ # احذر وضع تعليقات في السلاسل النصية >>> # إذ ستكون جزءًا من السلسلة نفسها >>> print( s % (12, 2*12, 3*12, 4*12) ) إذا وسعنا الشيفرة السابقة فسنتمكن من طباعة جدول ضرب العدد 12 كاملًا من 1 إلى 12، لكن هل من طريقة أفضل؟ بالتأكيد، وهي ما سنراها في الجزئية الموالية من هذه السلسلة. خاتمة عرفنا في هذا المقال أن IDLE هي بيئة تطوير خاصة بكتابة برامج بايثون وتطويرها، وأن التعليقات تسهل قراءة البرامج وفهمها، ولا تأثير لها على سير العمليات في البرنامج، وأن المتغيرات تستطيع تخزين نتائج بينية من أجل استخدامها لاحقًا. ترجمة -بتصرف- للفصل السادس: More Sequences and Other Things من كتاب Learning To Program لصاحبه Alan Gauld. اقرأ أيضًا المقال التالي: الحلقات التكرارية في البرمجة المقال السابق: مدخل إلى البيانات وأنواعها: التجميعات Collections تعلم البرمجة كيف تتعلم البرمجة كيف تتعلّم البرمجة: نصائح وأدوات لرحلتك في عالم البرمجة
  6. معالج الحاسوب هو تلك الرقاقة المركزية بين دارات الحاسوب المنفِّذة للخطوات الصغيرة التي تتشكل منها البرامج، وما كانت البرامج التي رأيناها حتى الآن إلا أشياء تبقيه مشغولًا إلى أن تنهي مهامها، كما تعتمد سرعة تنفيذ تعليمة برمجية مثل حلقة تكرارية تعالج بيانات على سرعة المعالج نفسه. تتواجد مع هذا عدة برامج تتفاعل مع أشياء خارج المعالج، فقد تتواصل عبر شبكة حاسوبية، أو تطلب بيانات من قرص صلب مثلًا، والذي قد يكون أبطأ من الحصول على المعلومة نفسها من القرص الصلب، وحين يحدث مثل ذلك، فعلينا معرفة ما إذا كان يليق بنا ترك المعالج يقبع ساكنًا دون عمل، ولهذا لا بد من وجود عمل يمكنه تنفيذه في ذلك الوقت، كما يعالج نظام تشغيلك ذلك جزئيًا، حيث سيجعل المعالج يتنقل بين عدة برامج عاملة، لكن هذا لا ينفع حين نريد لبرنامج واحد العمل بينما ينتظر ردًا من الشبكة. عدم التزامن Asynchronicity تحدث الأشياء في نموذج البرمجة المتزامنة synchronous واحدةً تلو الأخرى، فلا تعيد دالة تنفِّذ إجراءً يعمل على المدى الطويل إلا إذا وقع الإجراء وصار بإمكانه إعادة النتيجة، وبالتالي سيُوقِف هذا برنامجك إلى حين تنفيذ ذلك الإجراء؛ أما النموذج غير المتزامن asynchronous فسيسمح بحدوث عدة أشياء في الوقت نفسه، حيث سيستمر برنامجك بالعمل إذا بدأت بإجراء ما، كما سيُبلَّغ البرنامج حين ينتهي الإجراء ويحصل على وصول access إلى النتيجة مثل قراءة البيانات من القرص. نستطيع موازنة البرمجة المتزامنة وغير المتزامنة باستخدام مثال بسيط، وهو برنامج يجلب مَوردَين من الشبكة ثم يوازن النتيجة، بحيث تُعيد دالة الطلب في البيئة المتزامنة بعد إنهاء عملها. وأسهل طريقة لتنفيذ ذلك الإجراء هي جعل الطلبات واحدًا تلو الآخر، وهذا له تبعته، إذ لن يبدأ الطلب الثاني إلا حين ينتهي الطلب الأول، كما سيكون الوقت الكلي المستغرق مساويًا وقت رد الطلبَين معًا. يكون حل تلك المشكلة في النظام المتزامن ببدء خيوط تحكم control threads إضافية، وقد يكون الخيط هنا برنامجًا آخرًا عاملًا قد يتزامن تنفيذه مع برامج أخرى بواسطة نظام التشغيل، وبما أنّ أغلب الحواسيب الحديثة تحتوي على عِدة معالجات، فقد تعمل الخيوط المتعددة في الوقت نفسه، بل قد تعمل على معالجات مختلفة، فيبدأ خيط جديد الطلب الثاني، ثم ينتظر الخَيطان عودة نتائجهما، وبعد ذلك يُعيدا المزامنة من أجل دمج النتائج. تمثِّل الخطوط السميكة thick lines في المخطط التالي الوقت الذي ينفقه البرنامج في العمل العادي، في حين تمثِّل الخطوط الرفيعة thin lines وقت انتظار الشبكة، كما يكون الوقت الذي تأخذه الشبكة في النموذج المتزامن جزءًا من الخط الزمني timeline لخط تحكم ما؛ أما في النموذج غير المتزامن، فيتسبب بدء إجراء شبكة في حدوث انقسام split في الخط الزمني، كما يستمر البرنامج الذي ابتدأ الحدث بالعمل، ويقع الحدث معه أيضًا، ثم ينبه البرنامج حين ينتهي. هناك طريقة أخرى تصف الفرق بأنّ انتظار انتهاء الأحداث في النموذج المتزامن أمر ضمني implicit، بينما يكون صريحًا explicit وتحت تحكمنا في النموذج غير المتزامن. تتقاطع اللاتزامنية مع كلا الطريقتين، فهي تسهل التعبير عن البرامج التي لا تتوافق مع النموذج الخطي للتحكم، في حين قد تصعِّب التعبير عن البرامج التي تتبع النموذج الخطي، وسنرى بعض الطرق للنظر في تلك الغرابة في هذا المقال لاحقًا. تجعل المنصَّتان الهامتان لجافاسكربت -أي المتصفحات وNode.js- العمليات التي تستغرق وقتًا غير متزامنة، بدلًا من الاعتماد على الخيوط، وذلك جيد بما أنّ البرمجة بالخيوط صعبة جدًا بسبب صعوبة فهم مهمة البرنامج حين ينفِّذ أمورًا كثيرةً في وقت واحد. تقنية الغراب تستطيع الغربان استخدام الأدوات والتخطيط وتذكر الأشياء، وإيصال هذه الأشياء فيما بينهم، إذ أن شأنها في ذلك شأن غيرها من المخلوقات. لكن ما لا يعلمه الكثير عنها أنها قادرة على أمور أكثر من ذلك، إذ تُنشئ العديد من أسراب الغربان أجهزةً حاسوبيةً حيويةً مثلًا، فهي ليست إلكترونية مثل التي نصنعها، لكنها تتكون من حشرات دقيقة أشبه بالنمل الأبيض، إذ تُطوِّر معها علاقةً تكافليةً بحيث تزودها الغربان بالطعام مقابل أن تبني الحشرات تلك المستعمرات المعقدة وتشغلها، لتُجري حسابات لاحقًا بمساعدة كائنات حية داخلها. تقع مثل تلك المستعمرات عادةً في أعشاش كبيرة، حيث تعمل الطيور والحشرات معًا فيها لبناء هياكل طينية بصلية الشكل مخفية داخل أغصان الأعشاش تعيش فيها الحشرات وتعمل، وتَستخدِم تلك الآلات إشارات ضوئيةً للتواصل مع الأجهزة الأخرى، إذ تُدخِل الغربان قطعًا عاكسةً للضوء في سيقان خاصة بالتواصل، ثم توجه الحشرات تلك السيقان لعكس الضوء للأعشاش الأخرى، وهذا يعني أنّ الأعشاش التي بينها مجال بصري مفتوح هي التي تستطيع التواصل فقط. رسم أحد الخبراء خارطةً لشبكة أعشاش الغربان في قرية إييغ-سوغ-امبي Hières-sur-Amby الفرنسية، بحيث توضِّح أماكن الأعشاش واتصالاتها، كما في الشكل التالي: سنكتب في هذا المقال بعض الدوال الشبكية البسيطة لمساعدة الغربان على التواصل. ردود النداء Callbacks يمكن النظر إلى البرمجة غير المتزامنة على أنها تجعل الدوال التي تنفِّذ إجراءً بطيئًا تأخذ وسيطًا إضافيًا يكون دالة رد نداء callback function، حيث يبدأ الإجراء، ثم تُستدعى دالة رد النداء بالنتيجة حين ينتهي. لدينا مثلًا دالة setTimeout المتاحة في المتصفحات وNode.js على سواء، إذ تنتظر زمنًا بعينه يقاس بالميلي ثانية، ثم تَستدعي الدالة. setTimeout(() => console.log("Tick"), 500); لا يُعَدالانتظار عملًا مهمًا، لكن قد يكون مفيدًا حين نفعل أمرًا مثل تحديث رسم تحريكي أو التحقق من استغراق شيء وقتًا أطول من الوقت المسموح به، ويعني تنفيذ عدة أحداث غير متزامنة متتالية باستخدام ردود النداء؛ أنه علينا الاستمرار بتمرير دوال جديدة لمعالجة استمرارية الحوسبة بعد الحدث. تحتوي أغلب حواسيب أعشاش الغربان على حوصلة bulb تخزين بيانات طويل الأمد، حيث تُحفَر أجزاء المعلومات في الأغصان كي تُستخدَم لاحقًا، ويستغرق ذلك الحفر أو إيجاد جزء من البيانات وقتًا، لذا فإن واجهة التخزين طويل الأمد غير متزامنة، كما تَستخدِم دوال رد نداء. تخزِّن حواصل التخزين أجزاءً من بيانات JSON القابلة للتشفير تحت أسماء، وقد يخزِّن الغراب معلومات عن المكان الذي فيه الطعام المخبأ تحت اسم "food caches"، والذي قد يحمل مصفوفةً من الأسماء التي تشير إلى أجزاء أخرى من البيانات التي تصف الذاكرة المؤقتة الحقيقية، كما سيشغل الغراب مثل الشيفرة التالية للبحث عن مخزون طعام في حواصل التخزين storage bulbs في عش "Big Oak": import {bigOak} from "./crow-tech"; bigOak.readStorage("food caches", caches => { let firstCache = caches[0]; bigOak.readStorage(firstCache, info => { console.log(info); }); }); يُعَدّ هذا النسق من البرمجة قابلًا للتطبيق، لكنه يزيد من مستوى الإزاحة مع كل حدث غير متزامن، وذلك لأننا سنصل إلى دالة أخرى تفعل أمورًا أعقد من التي بين أيدينا لا محالة، مثل تنفيذ أحداث متعددة في الوقت نفسه، إذ سيكون الأمر غير مألوف. تُبنى حواسيب الغربان العشيَّة لتتواصل فيما بينها باستخدام أزواج من طلب-رد request-response، وهذا يعني أنّ أحد الأعشاش سيرسل رسالةً إلى عش آخر، ثم يرد الثاني مباشرةً مؤكدًا استلام الرسالة، وربما يرد أيضًا على الرسالة في الوقت نفسه، كما تُوسَم كل رسالة بنوع يحدد كيفية معالجتها، إذ تستطيع شيفرتنا تعريف المعالِجات handlers لكل نوع من أنواع الطلبات، ويُستدعى المعالج متى ما دخل ذلك الطلب لإنتاج رد. توفِّر الواجهة التي صدَّرتها وحدة ‎"./crow-tech"‎ دوالًا مبنيةً على ردود النداء من أجل التواصل، كما تحتوي الأعشاش على التابع send الذي يرسل الطلب، إذ يتوقع اسم العش الهدف ونوع الطلب ومحتواه على أساس أول ثلاثة وسائط، ثم يتوقع دالةً لتُستدعى حين يأتي الرد على أساس وسيط رابع وأخير. bigOak.send("Cow Pasture", "note", "Let's caw loudly at 7PM", () => console.log("Note delivered.")); لكن يجب تعريف نوع طلب أولًا باسم "note" لتمكين الأعشاش من استقبال ذلك الطلب، كما يجب أن تعمل الشيفرة التي تعالج الطلبات على جميع الأعشاش التي تستطيع استقبال رسالة من هذا النوع وليس على حاسوب العش فقط، حيث سنفترض أنّ الغراب سيطير هنا ليثبِّت شيفرة المعالج تلك على جميع الأعشاش. import {defineRequestType} from "./crow-tech"; defineRequestType("note", (nest, content, source, done) => { console.log(`${nest.name} received note: ${content}`); done(); }); تُعرِّف الدالة defineRequestType نوعًا جديدًا من الطلبات، كما يضيف المثال الدعم لطلبات "note" التي ترسِل تذكرة note إلى العش المعطى، ويَستدعي تطبيقنا console.log كي نستطيع توكيد وصول الطلب، كما تحتوي الأعشاش على الخاصية name التي تحمل أسماءها؛ أما الوسيط الرابع المعطى للمعالج فهو done، وهي دالة رد نداء يجب أن تستدعي عند انتهاء الطلب، فإذا استخدمنا قيمة الإعادة للمعالج على أساس قيمة رد، فهذا يعني عدم استطاعة معالج الطلب تنفيذ إجراءات غير متزامنة بنفسه، فالدالة التي تنفذ مهامًا غير متزامنة تُعيد قبل انتهاء العمل، كما تكون قد جهزت رد نداء عند انتهاءها، لذا فنحن نحتاج إلى آلية غير متزامنة، وهي دالة رد نداء أخرى في حالتنا لترسل إشعارًا حين يتاح ردًا. قد يكون اللاتزامن معديًا contagious نوعًا ما، فأيّ دالة تستدعي دالةً أخرى تعمل بأسلوب غير متزامن يجب أن تكون هي نفسها غير متزامنة، ومستخدمةً لرد نداء أو آليةً مشابهةً لتسليم نتيجتها؛ أما استدعاء رد النداء ففيه تفصيل أكثر من مجرد إعادة قيمة ما، كما يكون عرضةً للأخطاء، لذا فإنّ الحاجة إلى هيكلة أجزاء كبيرة من برنامجنا بهذه الطريقة ليست الأسلوب الأمثل. الوعود يسهل العمل مع المفاهيم المجردة حين يمكن تمثيلها بقيم، وفي حالة الإجراءات غير المتزامنة سنستطيع إعادة كائن يمثل الحدث المستقبلي، بدلًا من إعداد دالة لتُستدعى في نقطة ما في المستقبل، وهذه هي وظيفة الصنف Promise، فالوعد هو إجراء غير متزامن قد يحدث عند نقطة ما ويعطينا قيمة، ويستطيع إبلاغ أيّ أحد يريد إشعاره حين تكون القيمة متاحةً. يُعَدّ استدعاء Promise.resolve أسهل طريقة لإنشاء وعد، وهي دالة تضمن أنّ القيمة التي تعطيها إياها ستغلَّف داخل وعد، فإذا كان هي نفسها وعدًا، فستعاد ببساطة، وإلا فستحصل على وعد ينتهي مباشرةً بقيمتك على أساس نتيجة. let fifteen = Promise.resolve(15); fifteen.then(value => console.log(`Got ${value}`)); // → Got 15 نستطيع استخدام التابع then للحصول على وعد، إذ يسجل دالة رد نداء لتُستدعى حين ينتهي الوعد وينتج قيمةً، كما نستطيع إضافة عدة ردود نداء إلى وعد واحد، إذ ستُستدعى جميعًا حتى لو أضفتها بعد انتهاء الوعد، ومع هذا فلا زال التابع then يحتوي مزيدًا من الخصائص، إذ يُعيد وعدًا آخرًا ينتهي بالقيمة التي تعيدها دالة المعالِج، أو إذا أعاد ذلك وعدًا، فسينتظر ذلك الوعد وينتهي مُخرجًا نتيجته. يُعَدّ النظر للوعود على أنها جهاز لنقل القيم إلى بيئة غير متزامنة مفيدًا، فالقيمة العادية موجودة والقيمة الموعودة promised value قد تكون موجودة، كما قد تظهر في نقطة ما في المستقبل، كما تتصرف الحسابات المعرَّفة بالوعود على مثل هذه القيم المغلَّفة wrapped values وتنفَّذ تنفيذًا غير متزامن عندما تكون القيم متاحة. نستطيع استخدام Promise على أساس باني لكي ننشئ وعدًا، لكن هذا سيكون له واجهة غريبة نوعًا ما، إذ يتوقع الباني دالةً على أساس وسيط ويستدعيها مباشرةً، ثم يمرر إليها دالةً تستطيع استخدامها لحل الوعد وإنهائه، وسبب عملها بهذا الأسلوب بدلًا من استخدام التابع resolve مثلًا، هو اقتصار حل الوعد على الشيفرة التي أنشأته. يوضِّح المثال التالي كيفية إنشاء واجهة مبنية على وعد لدالة readStorage: function storage(nest, name) { return new Promise(resolve => { nest.readStorage(name, result => resolve(result)); }); } storage(bigOak, "enemies") .then(value => console.log("Got", value)); تُعيد هذه الدالة غير المتزامنة قيمةً ذات معنى، وهذه هي الميزة الأساسية للوعود، إذ تبسِّط استخدام الدوال غير المتزامنة، فبدلًا من الحاجة إلى تمرير ردود النداء كما تبدو الدوال المبنية على الوعود أشبه بالدوال العادية؛ فهي تأخذ الدخل على أساس وسائط وتُعيد مخرجاتها، ولا تختلف إلا أنّ الخرج قد يكون غير متاح بعد. الفشل Failure يمكن أن تفشل حسابات جافاسكربت العادية برفع استثناء exception، إذ تحتاج الحوسبة غير المتزامنة إلى ذلك عادةً، فقد يفشل طلب الشبكة أو شيفرة ترفع استثناءً وتكون جزءًا من الحوسبة غير المتزامنة. إحدى المشاكل المؤرقة في نمط رد النداء للبرمجة غير المتزامنة أنها تصعِّب ضمان الإبلاغ الصحيح لردود النداء بشأن حالات الفشل، وأحد الحلول المستخدَمة على نطاق واسع هو استخدام وسيط رد النداء الأول لتوضيح فشل الإجراء، بعدها سيحتوي الوسيط الثاني على القيمة التي أنتجها الإجراء حين كان ناجحًا. يجب أن تتحقق دوال ردود النداء هذه إذا كانت قد استقبلت استثناءً أم لا، كما عليها التأكد من أنّ أيّ مشكلة تسببها بما فيها الاستثناءات المرفوعة من قِبَل الدوال التي استدعتها، قد عُلم بها وسُلِّمت إلى الدالة المناسبة؛ أما الوعود فتيسِّر من هذه العملية كثيرًا، فهي إما محلولة -أي انتهى الإجراء بنجاح-، أو مرفوضة -أي فشلت-، إذ لا تُستدعى معالِجات الإنهاء resolve handlers التي سُجلت في then إلا عند نجاح الإجراء؛ أما عمليات الرفض فتُنقَل إلى الوعد الجديد الذي تُعيده then. وحين يرفع معالج استثناءً، فسيجعل هذا الوعد الذي أنتجه الاستدعاء إلى then مرفوضًا. لهذا إذا فشل أيّ عنصر في سلسلة إجراءات غير متزامنة، فسيحدَّد الناتج الكلي للسلسلة على أنه مرفوض، ولا تُستدعى معالِجات نجاح أبعد من النقطة التي فشلت فيها. كذلك فإنّ رَفْض وعد ما ينتج قيمةً تمامًا مثل التي ينتجها حل الوعد، كما يُطلق على تلك القيمة سبب الرفض، فإذا تسبب استثناء في دالة معالِجة بالرفض، فستُستخدِم قيمة الاستثناء على أساس سبب، وبالمثل، إذا أعاد معالج وعدًا مرفوضًا، فسينتقل ذلك الرفض إلى الوعد التالي، حيث لدينا دالة Promise.reject التي تنشئ وعدًا جديدًا مرفوضًا فورًا. تحتوي الوعود على التابع catch لمعالجة عمليات الرفض تلك صراحةً، إذ يسجِّل معالجًا ليُستدعى حين يُرفض الوعد مثلما تعالج معالجات then الحل العادي للوعود، كما تشبه then في أنها تُعيد وعدًا جديدًا، إذ يحل إلى قيمة الوعد الأصلية إذا حل حلًا عاديًا، وإلى نتيجة معالج catch في غير ذلك. يُرفَض الوعد الجديد كذلك إذا رفع معالج catch خطأً ما، وللاختصار، تقبل then معالج الرفض على أساس وسيط ثاني، لذا نستطيع تثبيت كلا النوعين من المعالجات في استدعاء تابع وحيد، كما تستقبل الدالة الممرَّرة إلى الباني Promise وسيطًا ثانيًا مع دالة حل resolve function لتستخدمها في رفض الوعد الجديد. يمكن النظر إلى سلاسل قيم الوعود المنشأة باستدعاءات إلى then وcatch على أساس خط أنابيب تتحرك فيه عمليات الفشل أو القيم غير المتزامنة، وبما أنّ تلك السلاسل قد أُنشئت بتسجيل معالجات، فسيكون لكل رابط معالج نجاح أو معالج فشل أو كليهما؛ حيث تُهمَل المعالِجات التي لا تطابق نوع الخرج -بنجاح أو فشل-؛ أما المعالجات التي تطابق فستُستدعى ويحدِّد خرجها نوع القيمة التي ستأتي تاليًا، بحيث تكوّن نجاحًا حين تُعيد قيمةً ليست وعدًا، وفشلًا حين ترفع استثناءً، وخرْج الوعد حين تُعيد أحد هذين. new Promise((_, reject) => reject(new Error("Fail"))) .then(value => console.log("Handler 1")) .catch(reason => { console.log("Caught failure " + reason); return "nothing"; }) .then(value => console.log("Handler 2", value)); // → Caught failure Error: Fail // → Handler 2 nothing تُعالَج الاستثناءات غير الملتقَطة بواسطة البيئة، إذ تستطيع بيئات جافاسكربت استشعار إذا لم يعالَج رفض الوعد، وستُبلِّغ هذا في صورة خطأ. صعوبة الشبكات قد تأتي أيام لا يكون فيها ضوء كافي لنظام المرايا الذي يستخدمه الغربان من أجل نقل إشارة، أو قد يحجب شيء ما مسار الإشارة، كذلك قد تُرسَل إشارة ولا تُستقبَل من الطرف الآخر، حيث سيتسبب ذلك في عدم استدعاء رد النداء المعطى إلى send، وهو الأمر الذي سيجعل البرنامج يتوقف دون أي ملاحظة حتى لوجود مشكلة، فلو بلَّغ الطلب بالفشل إذا مر زمن محدد دون رد، لكان أفضل. تكون حالات فشل الإرسال في العادة حوادث عرضية مثل تداخل الأضواء القادمة من مصابيح سيارة مع الإشارات الضوئية، وهنا سينجح الطلب بمجرد إعادة المحاولة، لذا سنجعل دالة الطلب تحاول تلقائيًا إعادة إرسال الطلب عدة مرات قبل أن تستسلم. وبما أننا عرفنا الآن أن الوعود أمر جيد، فسنجعل دالة الطلب تُعيد وعدًا، إذ يتشابه رد النداء والوعد من حيث ما يستطيعان التعبير عنه، كما يمكن تغليف الدوال المبنية على ردود النداء لكشف واجهة مبنية على وعد، والعكس صحيح. قد يشير الرد إلى فشل عند تسليم الطلب ورده بنجاح إذا حاول الطلب استخدام نوع طلب غير معرَّف أو رفع المعالج خطأ مثلًا. لدعم هذا يتَّبع كل من send وdefineRequestType الأسلوب المذكور أعلاه، حيث يكون الوسيط الأول الممرَّر إلى ردود النداء هو سبب الفشل إذا وُجد، في حين يكون الثاني هو النتيجة الحقيقية، ويمكن ترجمة هذا إلى حل الوعد ورفضه باستخدام المغلِّف الخاص بنا. class Timeout extends Error {} function request(nest, target, type, content) { return new Promise((resolve, reject) => { let done = false; function attempt(n) { nest.send(target, type, content, (failed, value) => { done = true; if (failed) reject(failed); else resolve(value); }); setTimeout(() => { if (done) return; else if (n < 3) attempt(n + 1); else reject(new Timeout("Timed out")); }, 250); } attempt(1); }); } سينجح ما فعلناه أعلاه لأنّ الوعود لا يمكن حلها أو رفضها إلا مرةً واحدة، إذ تُحدِّد المرة الأولى التي يُستدعى فيها resolve أو reject خرج الوعد، كما تُتجاهل الاستدعاءات التالية التي سببها طلب عائد بعد انتهاء طلب آخر. سنحتاج إلى استخدام دالة تعاودية لبناء حلقة غير متزامنة من أجل إعادة المحاولة التي ذكرناها، حيث لا تسمح الحلقة العادية بالتوقف وانتظار الإجراء غير المتزامن، وتنفِّذ الدالة attempt محاولةً واحدةً لإرسال الطلب وتضبط زمن مهلة محدد، فإذا لم تحصل على استجابة أو رد بعد ربع ثانية، فستبدأ المحاولة الثانية أو ترفض الوعد إذا كانت هذه هي المحاولة الثالثة، ويكون السبب هنا هو نسخة من Timeout. تُعَدّ المحاولة كل ربع ثانية وتركها إذا لم نتلق ردًا بعد ثلاثة أرباع ثانية نظامًا عشوائيًا نوعًا ما، فمن المحتمل إذا جاء الطلب لكن المعالج استغرق وقتًا أطول قليلًا أن تُسلَّم الطلبات أكثر من مرة، وسنكتب معالجاتنا واضعين تلك المشكلة في حساباتنا، فلا يجب أن تمثل الرسائل المكررة مشكلة. لن نبني شبكة عالية المستوى هنا، فسقف توقعات الغربان ليس عاليًا على أي حال حين يتعلق الأمر بالحوسبة، ولكي نعزل أنفسنا من ردود النداء كلها، فسنعرِّف مغلِّفًا لـ defineRequestType يسمح لدالة المعالِج أن تُعيد وعدًا أو قيمةً مجردةً plain value ويوصل ذلك لرد النداء من أجلنا. function requestType(name, handler) { defineRequestType(name, (nest, content, source, callback) => { try { Promise.resolve(handler(nest, content, source)) .then(response => callback(null, response), failure => callback(failure)); } catch (exception) { callback(exception); } }); } نستخدِم Promise.resolve لتحويل القيمة التي يُعيدها handler إلى وعد إذا لم تُحوَّل فعليًا. لاحظ أنه يجب تغليف الاستدعاء إلى handler في كتلة try لضمان توصيل أي استثناءات يرفعها مباشرةً إلى رد النداء، حيث يوضح هذا صعوبة معالجة الأخطاء التي فيها ردود نداء معالجة صحيحة، ومن السهل نسيان كيفية توجيه استثناءات مثل هذه، وإذا لم نفعل ذلك فلن يُبلّغ بحالات الفشل إلى رد نداء مناسب؛ أما الوعود فستجعل هذا آليًا تقريبًا ومن ثم يكون أقل عرضةً للأخطاء. تجميعات الوعود يحتفظ حاسوب العش بمصفوفة من الأعشاش الأخرى التي في نطاق الاتصال في الخاصية neighbors. سنكتب دالةً تحاول إرسال طلب "ping" إلى كل حاسوب لنرى أيها ترد علينا، وهو طلب يسأل ردًا ببساطة، وذلك لنرى إن كان مكن الوصول إلى أيّ من تلك الأعشاش. تُعَدّ الدالة Promise.all مفيدةً عند العمل مع تجميعات من الوعود التي تعمل في نفس الوقت، إذ تُعيد وعدًا ينتظر حل جميع الوعود التي في المصفوفة، ثم يُحل هو إلى مصفوفة من القيم التي أنتجتها تلك الوعود بترتيب المصفوفة الأصلية نفسها، وإذا رُفض أي وعد فستُرفَض نتيجة Promise.all كذلك. requestType("ping", () => "pong"); function availableNeighbors(nest) { let requests = nest.neighbors.map(neighbor => { return request(nest, neighbor, "ping") .then(() => true, () => false); }); return Promise.all(requests).then(result => { return nest.neighbors.filter((_, i) => result[i]); }); } لا نريد فشل الوعد المجمَّع إذا كان أحد الجيران غير متاح ونحن ما زلنا لا نعرف باقي البيانات، لذا تُلحِق الدالة التي رُبطت بمجموعة من الجيران لتحولهم إلى وعود طلبات، حيث أن المعالجات تصنع طلبات ناجحة تنتج true وطلبات فاشلة تنتج false. يُستخدَم filter في معالج الوعد المجمَّع لحذف العناصر من مصفوفة neighbors التي تكون قيمتها الموافقة هي false، إذ يستفيد هذا من كون filter يمرر فهرس مصفوفة العنصر الحالي على أساس وسيط ثاني إلى دالة الترشيح، كما يفعل map وsome وتوابع المصفوفات العليا المشابهة. إغراق الشبكة Network flooding يحد اقتصار تكلم الأعشاش لجيرانها كثيرًا من فائدة هذه الشبكة، وعلى ذلك فربما نحاول إعداد نوع من الطلبات يُعيد توجيه الطلبات تلقائيًا إلى الجيران، وذلك على أساس حل لنشر المعلومات إلى الشبكة كلها، ثم يُعيد هؤلاء الجيران توجيهها إلى جيرانهم، وهكذا حتى تستقبل الشبكة كلها تلك الرسالة. import {everywhere} from "./crow-tech"; everywhere(nest => { nest.state.gossip = []; }); function sendGossip(nest, message, exceptFor = null) { nest.state.gossip.push(message); for (let neighbor of nest.neighbors) { if (neighbor == exceptFor) continue; request(nest, neighbor, "gossip", message); } } requestType("gossip", (nest, message, source) => { if (nest.state.gossip.includes(message)) return; console.log(`${nest.name} received gossip '${ message}' from ${source}`); sendGossip(nest, message, source); }); يحتفظ كل عش بمصفوفة من سلاسل gossip النصية التي رآها فعليُا، وذلك لتجنب تكرار إرسال الرسالة عبر الشبكة للأبد، كما نستخدِم دالة everywhere لتعريف تلك المصفوفة، إذ تُشغِّل هذه الدالة الشيفرة على كل عش من أجل إضافة خاصية إلى كائن state الخاص بالعش، وهو المكان الذي سنحفظ فيه حالة العش المحلية. يتجاهل العش رسالة gossip مكرَّرة مرسَلة إليه، الأمر الذي يحدث عادةً حين يعيد الناس إرسالها دون وعي، لكن حين يستلم رسالةً جديدةً، فسيخبر جميع جيرانه عدا ذلك الذي أرسلها. سينشر هذا جزءًا جديدًا من gossip في الشبكة كما تنتشر بقعة الحبر في الماء، فحتى إذا لم تكن بعض الاتصالات عاملةً وقتها، فستصل gossip العش المعطى إذا كان ثمة طريق بديلة إليه. يسمى ذلك الأسلوب في التواصل الشبكي باسم الإغراق أو الغمر flooding، فهو يغرق الشبكة بجزء من المعلومات إلى أن تكون لدى جميع العقد. نستطيع استدعاء sendGossip لنرى تدفق الرسائل خلال القرية. sendGossip(bigOak, "Kids with airgun in the park"); توجيه الرسائل إذا أرادت عقدة ما التواصل مع عقدة أخرى فلن يكون أسلوب الإغراق هنا هو الأمثل، خاصةً حين تكون الشبكة كبيرة، فقد يؤدي هذا إلى كثير من نقل البيانات غير الضروري، والحل هنا هو إعداد طريقة تنتقل بها الرسائل من عقدة إلى عقدة حتى تصل إلى وجهتها. تكون الصعوبة في ذلك أنها ستحتاج إلى معرفة بتخطيط الشبكة، حيث من الضروري لإرسال طلب في اتجاه عش بعيد، معرفة أيّ الأعشاش الجارة قادرة على إيصاله بصورة أسرع إلى وجهته، حيث سنهدر كثيرًا من الموارد دون جدوى إذا أرسلناه في الاتجاه الخاطئ. لا يملك العش المعلومات التي يحتاجها لحساب الطريق لأنه لا يعرف إلا جيرانه المباشَرِين، وعليه يجب نشر معلومات الاتصال لكل الأعشاش بطريقة تسمح لها بالتغير مع مرور الوقت كلما هُجِر عش أو بُني آخر. نستطيع استخدام الإغراق هنا مرةً أخرى، لكننا سننظر هل تطابق مجموعة الجيران الجديدة لعش ما المجموعة الحالية التي لدينا، أم لا بدلًا من التحقق هل تم استلام الرسالة أم لا. requestType("connections", (nest, {name, neighbors}, source) => { let connections = nest.state.connections; if (JSON.stringify(connections.get(name)) == JSON.stringify(neighbors)) return; connections.set(name, neighbors); broadcastConnections(nest, name, source); }); function broadcastConnections(nest, name, exceptFor = null) { for (let neighbor of nest.neighbors) { if (neighbor == exceptFor) continue; request(nest, neighbor, "connections", { name, neighbors: nest.state.connections.get(name) }); } } everywhere(nest => { nest.state.connections = new Map(); nest.state.connections.set(nest.name, nest.neighbors); broadcastConnections(nest, nest.name); }); تستخدِم الموازنة JSON.stringify لأنّ == لن يُعيد القيمة true إلا إذا كان الاثنان نفس القيمة بالضبط سواءً كان على كائنات أو مصفوفات، وليس هذا ما نريده هنا، وبالتالي فقد تكون موازنة سلاسل JSON النصية بدائيةً قليلًا لكنها فعالة لموازنة محتواها. تبدأ العقد بنشر اتصالاتها مباشرةً، مما يعطي كل عش خريطةً بمخطط الشبكة الحالي، ما لم يوجد عش منها يستحيل الوصول إليه، كما نستطيع باستخدام ذلك المخطط إيجاد الطرق فيه كما رأينا في مقال مشروع تطبيقي لبناء رجل آلي (روبوت) عبر جافاسكريبت، فإذا كان لدينا طريق نحو وجهة رسالة ما، فسنستطيع معرفة أي اتجاه نرسلها فيه. تبحث الدالة findRoute -التي تشابه findRoute من مقال مشروع تطبيقي لبناء رجل آلي (روبوت) عبر جافاسكريبت- عن طريقة للوصول إلى عقدة معطاة في الشبكة، لكن بدلًا من إعادة الطريق كله، فهي تعيد الخطوة التالية فقط، وسيقرِّر العش التالي مستخدِمًا المعلومات المتوفرة لديه عن الشبكة أين سيرسل الرسالة تاليًا. function findRoute(from, to, connections) { let work = [{at: from, via: null}]; for (let i = 0; i < work.length; i++) { let {at, via} = work[i]; for (let next of connections.get(at) || []) { if (next == to) return via; if (!work.some(w => w.at == next)) { work.push({at: next, via: via || next}); } } } return null; } نستطيع الآن بناء دالة يمكنها إرسال رسائل بعيدة المدى، فإذا كانت مرسَلةً إلى جار مباشر، فستسلَّم مثل العادة؛ أما إذا كان إلى جار بعيد، فستُحزَّم في كائن وترسَل إلى جار قريب من الهدف باستخدام نوع الطلب "route" الذي سيجعل ذلك الجار يكرِّر السلوك نفسه. function routeRequest(nest, target, type, content) { if (nest.neighbors.includes(target)) { return request(nest, target, type, content); } else { let via = findRoute(nest.name, target, nest.state.connections); if (!via) throw new Error(`No route to ${target}`); return request(nest, via, "route", {target, type, content}); } } requestType("route", (nest, {target, type, content}) => { return routeRequest(nest, target, type, content); }); نستطيع الآن إرسال الرسالة إلى العش الذي في برج الكنيسة، حيث سنكون قد وفرنا أربع محطات من الطريق في الشبكة. routeRequest(bigOak, "Church Tower", "note", "Incoming jackdaws!"); لهذا نكون قد بنينا عدة طبقات من الوظائف فوق نظام اتصالات بدائي من أجل تسهيل استخدامه، وهذا نموذج جميل لكيفية عمل شبكات الحواسيب الحقيقية رغم بساطته أو سطحيته، لكن الخاصية التي تميز شبكات الحواسيب أنها لا يُعتمد عليها، فرغم استطاعتنا على بناء تجريدات abstractions فوقها لتساعدنا، إلا أنه لا يمكننا منع فشل الشبكة، لذا تدور برمجة الشبكات حول توقُّع حالات الفشل والتعامل معها. دوال async تنسخ الغربان المعلومات المهمة في الأعشاش من أجل الحفاظ عليها إذا هجم صقر ما على أحدها ودمره، فإذا أرادت استرجاع معلومات ليست موجودة في حوصلة التخزين الخاصة بها، فسيسأل حاسوب العش بقية الأعشاش في الشبكة حتى يجد واحدًا منها لديه تلك المعلومات. requestType("storage", (nest, name) => storage(nest, name)); function findInStorage(nest, name) { return storage(nest, name).then(found => { if (found != null) return found; else return findInRemoteStorage(nest, name); }); } function network(nest) { return Array.from(nest.state.connections.keys()); } function findInRemoteStorage(nest, name) { let sources = network(nest).filter(n => n != nest.name); function next() { if (sources.length == 0) { return Promise.reject(new Error("Not found")); } else { let source = sources[Math.floor(Math.random() * sources.length)]; sources = sources.filter(n => n != source); return routeRequest(nest, source, "storage", name) .then(value => value != null ? value : next(), next); } } return next(); } ولأن connections هي خارطة Map، فلن تعمل Object.keys عليها، لكن لديها التابع keys الذي يعيد مكرِّرًا iterator وليس مصفوفةً، كما يمكن تحويله إلى مصفوفة باستخدام الدالة Array.from، وهي شيفرة غير مألوفة حتى مع الوعود، إذ تُسلسَل عدة إجراءات غير متزامنة معًا بطرق غير واضحة، وسنحتاج إلى الدالة التعاودية next لوضع نموذج المرور الحلَقي على الأعشاش؛ أما ما تفعله الشيفرة على الحقيقة فهو سلوك خطي، إذ تنتظر تمام الإجراء السابق قبل بدء الذي يليه، وبالتالي ستكون أسهل في التعبير عنها في نموذج برمجة متزامنة. الجميل في الأمر هو سماح جافاسكربت بكتابة شيفرة وهمية متزامنة pseudo-synchronous لوصف الحوسبة غير المتزامنة، إذ تُعيد الدالة async -ضمنيًا- وعدًا وتنتظر await في متنها وعودًا أخرى بطريقة تبدو تزامنية. نُعيد كتابة findInStorage كما يلي: async function findInStorage(nest, name) { let local = await storage(nest, name); if (local != null) return local; let sources = network(nest).filter(n => n != nest.name); while (sources.length > 0) { let source = sources[Math.floor(Math.random() * sources.length)]; sources = sources.filter(n => n != source); try { let found = await routeRequest(nest, source, "storage", name); if (found != null) return found; } catch (_) {} } throw new Error("Not found"); } تُحدَّد الدالة async بكلمة async قبل كلمة function المفتاحية، كما يمكن جعل التوابع غير تزامنية بكتابة async قبل أسمائها. وإذا استدعيت دالةً مثل تلك؛ فستُعيد وعدًا، وكذلك الحال بالنسبة للتوابع، ويُحل الوعد عندما تُعيد الدالة شيئًا؛ أما إذا رفعت الدالة استثناءً فسيُرفض الوعد. findInStorage(bigOak, "events on 2017-12-21") .then(console.log); يمكن وضع كلمة await المفتاحية أمام تعبير ما داخل دالة async لجعله ينتظر حل وعد ما، ولا يتابع تنفيذ الدالة إلا بعد حل ذلك الوعد. لم تَعُد مثل تلك الدوال تعمل من البداية للنهاية على مرة واحدة، شأنها شأن أيّ دالة جافاسكربت عادية، إذ يمكن تجميدها frozen في أي نقطة توجد فيها await، وبعدها ستستعيد العمل في وقت لاحق؛ أما بالنسبة للشيفرات غير المتزامنة المهمة، فتلك الصيغة أسهل من استخدام الوعود مباشرةً، حتى لو احتجنا إلى فعل شيء لا يناسب النموذج المتزامن مثل تنفيذ عدة إجراءات في الوقت نفسه، إذ من السهل دمج await مع الاستخدام المباشر للوعود. المولدات Generators إن قدرة الدالة على التوقف ثم المتابعة مرةً أخرى ليست حكرًا على دوال async وحدها، حيث تحتوي جافاسكربت على خاصية اسمها دوال المولِّد generator functions التي تشبهها لكن مع استثناء الوعود، فحين نعرِّف دالةً باستخدام ‎function*‎ -أي أننا سنضع محرف النجمة بعد الكلمة function لستصير مولِّدًا، ويُعيد مكررًا عند استدعائه كما رأينا في مقال الحياة السرية للكائنات في جافاسكريبت. function* powers(n) { for (let current = n;; current *= n) { yield current; } } for (let power of powers(3)) { if (power > 50) break; console.log(power); } // → 3 // → 9 // → 27 تُجمَّد الدالة في بدايتها حين نستدعي powers، وتعمل في كل مرة تستدعي next على المكرِّر حتى تقابل تعبير yield الذي يوقفها ويجعل القيمة المحصَّلة هي القيمة التي ينتجها المكرِّر تاليًا، وحين تُعيد الدالة -علمًا أنّ دالة مثالنا لا تعيد- فسيكون المكرِّر قد انتهى. تُعَدّ كتابة المكرِّرات أسهل كثيرًا من استخدام الدوال المولِّدة، حيث يمكن كتابة مكرِّر الصنف Group من التدريب في مقال الحياة السرية للكائنات في جافاسكريبت بهذا المولِّد: Group.prototype[Symbol.iterator] = function*() { for (let i = 0; i < this.members.length; i++) { yield this.members[i]; } }; لم نعد في حاجة إلى إنشاء كائن ليحفظ حالة المكرِّر، إذ تحفظ المولِّدات حالتها المحلية تلقائيًا في كل مرة تُحصِّل فيها، وقد لا تقع تعبيرات مثل yield تلك إلا مباشرةً في دالة المولِّد نفسها وليس في دالة داخلية معرَّفة داخلها، والحالة التي يحفظها المولِّد عند التحصيل هي بيئته المحلية فقط والموضع الذي حَصَّل فيه؛ أما دالة async فهي نوع خاص من المولِّدات، فهي تُنتِج وعدًا عند استدعائها، كما يُحَل عند إعادتها -أي انتهائها-، في حين يُرفَض إذا رفعت استثناءً، ونحصل على ناتج الوعد سواء كان قيمة أو استثناءً مرفوعًا باستعمال التعبير awaits مع الوعد الذي تعيده الدالة. حلقة الحدث التكرارية تُنفَّذ البرامج غير المتزامنة جزءًا جزءًا، وقد يبدأ كل جزء ببعض الإجراءات ويجدول شيفرات لتنفيذها عند انتهاء الإجراء أو فشله؛ أما بين تلك الأجزاء فسيكون البرنامج ساكنًا منتظِرًا الإجراء التالي، وبالتالي لا تُستدعى ردود النداء من قِبَل الشيفرة التي جدولَتها، فإذا استدعينا setTimeout من داخل دالة، فستكون تلك الدالة قد أعادت بالوقت الذي تُستدعى فيه دالة رد النداء، وحين يُعيد رد النداء، فلن يعود التحكم إلى الدالة التي جدولته. يحدث السلوك غير المتزامن في مكدس استدعاء الدوال الفارغ الخاص به، وهذا أحد الأسباب التي تصعب معها إدارة الاستثناءات في الشيفرات غير المتزامنة في غياب الوعود، إذ لن تكون معالجات catch الخاصة بنا في المكدس حين ترفع استثناءً بما أنّ كل رد نداء يبدأ بمكدس شبه فارغ. try { setTimeout(() => { throw new Error("Woosh"); }, 20); } catch (_) { // لن يعمل هذا console.log("Caught!"); } مهما كانت أوقات وقوع الأحداث -مثل طلبات timeouts أو incoming- متقاربة، فلن تشغّل بيئة جافاسكربت إلا برنامجًا واحدًا في كل مرة. يمكن النظر إلى هذا السلوك على أنه يشغِّل حلقةً تكراريةً كبيرةً على برنامجنا يُطلق عليها حلقة الحدث event loop، فإذا لم يكن ثمة شيء لفعله فستتوقف الحلقة؛ أما إذا جاءت الأحداث، فستضاف إلى طابور queue وتنفَّذ شيفراتها واحدةً تلو الأخرى، ولأنه لا يمكن تنفيذ شيئين في الوقت نفسه، فستؤخر الشيفرة البطيئة التنفيذ معالجة الأحداث الأخرى. يضبط المثال أدناه زمن إنهاء timeout، لكن يتلكأ بعد ذلك إلى ما بعد النقطة الزمنية المحدَّدة له مسببًا تأخر زمن الإنهاء. let start = Date.now(); setTimeout(() => { console.log("Timeout ran at", Date.now() - start); }, 20); while (Date.now() < start + 50) {} console.log("Wasted time until", Date.now() - start); // → Wasted time until 50 // → Timeout ran at 55 تُحل الوعود دائمًا أو تُرفض على أساس حدث جديد، وحتى لو حُل الوعد، فسسيتسبب انتظاره في تشغيل رد النداء الخاص بك بعد انتهاء السكربت الحالية بدلًا من تشغيله مباشرةً. Promise.resolve("Done").then(console.log); console.log("Me first!"); // → Me first! // → Done سنرى في الفصول اللاحقة عدة أنواع من الأحداث التي تعمل على حلقة الحدث. زلات البرامج غير المتزامنة لا توجد تغيرات حادثة في الحالة إذا كان البرنامج يعمل بتزامن على مرة واحدة عدا تلك التي يصنعها البرنامج نفسه؛ أما بالنسبة للبرامج غير المتزامنة فالأمر مختلف، إذ قد تحتوي على فجوات gaps في تنفيذها يمكن لشيفرات أخرى العمل خلالها. لننظر إلى المثال التالي، إذ يهوى أحد الغربان عدّ الفراخ التي تفقس في القرية كل عام، حيث تخزِّن الأعشاش هذا العدد في حواصل التخزين الخاصة بها، وتحاول الشيفرة التالية حساب العدد من جميع الأعشاش لأحد الأعوام. function anyStorage(nest, source, name) { if (source == nest.name) return storage(nest, name); else return routeRequest(nest, source, "storage", name); } async function chicks(nest, year) { let list = ""; await Promise.all(network(nest).map(async name => { list += `${name}: ${ await anyStorage(nest, name, `chicks in ${year}`) }\n`; })); return list; } يُظهر الجزء async name‎ =>‎ أنّ الدوال السهمية يمكن أن تكون غير متزامنة إذا وضعنا كلمة async أمامها. لن يبدو أيّ شيء غريبًا على الشيفرة لأول وهلة، فهي توجه الدوال السهمية غير المتزامنة async خلال مجموعة من الأعشاش لتنشئ مصفوفةً من الوعود، ثم ستستخدِم Promise.all لتنتظر حل كل تلك الوعود قبل إعادتها القائمة التي جهزتها، غير أنّ هذا السلوك معيب، إذ يُعيد سطرًا واحدًا يحتوي دائمًا على العش الأبطأ -الذي يصل رده متأخرًا- في الإجابة. chicks(bigOak, 2017).then(console.log); تكمن المشكلة في العامِل ‎+=‎ الذي يأخذ قيمة list الحالية وقت بدء تنفيذ التعليمة، ثم يضبط رابطة list بعد انتهاء await لتكون تلك القيمة إضافةً إلى السلسلة النصية المضافة؛ أما في الوقت الذي يكون بين بدء تنفيذ التعليمة والوقت الذي تنتهي فيه، سيكون لدينا فجوة غير متزامنة. يعمل التعبير map قبل أي شيء أضيف إلى القائمة، لذا يبدأ كل عامل من العوامل ‎+=‎ بسلسلة فارغة، وينتهي حين تنتهي عملية استعادة التخزين بضبط قائمة list من سطر واحد، حيث تكون نتيجة إضافة السطر إلى السلسلة الفارغة. كان يمكن تجنب ذلك بسهولة بإعادة الأسطر من الوعود الموجَّهة واستدعاء join على نتيجة Promise.all بدلًا من بناء القائمة بتغيير رابطة ما، كما سيكون حساب قيمة جديدة أقل عرضةً للخطأ من تغيير القيم الموجودة سلفًا. async function chicks(nest, year) { let lines = network(nest).map(async name => { return name + ": " + await anyStorage(nest, name, `chicks in ${year}`); }); return (await Promise.all(lines)).join("\n"); } تحدث أخطاء مثل هذه بسهولة خاصةً عند استخدام await، كما يجب أن نكون على علم بأماكن حدوث الفجوات في شيفراتنا، وإحدى مزايا اللاتزامن الصريح في جافاسكربت، هي أنّ العثور على تلك الفجوات سهل نسبيًا، إذ سيكون إما باستخدام ردود النداء أو الوعود أو await. خاتمة تمكننا البرمجة غير التزامنية من التعبير عن الانتظار للإجراءات actions التي تعمل لوقت طويل دون تجميد البرنامج أثناء تلك الإجراءات، وتنفّذ بيئات جافاسكربت ذلك النمط من البرمجة باستخدام ردود النداء، وهي الدوال التي تُستدعى عند تمام الإجراءات. تُجدوِل حلقة الحدث ردود النداء لتُستدعى في الوقت المناسب واحدًا تلو الآخر كي لا يتداخل تنفيذها، كما صارت البرمجة غير التزامنية سهلةً بفضل الوعود التي هي كائنات تمثّل إجراءات قد تكتمل في المستقبل، ودوال async التي تسمح لنا بكتابة برنامج غير متزامن كما لو كان تزامنيًا. تدريبات تتبع المشرط تملك غربان القرية مشرطًا قديمًا تستخدِمه أحيانًا في المهام الخاصة مثل التحزيم أو قطع الأسلاك، كما تريد الحفاظ عليه لئلا يضيع، لذلك، فكلما نُقل المشرط إلى عش جديد أُضيف مُدخل جديد في ذاكرة العش القديم والجديد معًا تحت اسم مشرط "scalpel"، حيث تكون قيمته هي موقعه الجديد، ويعني هذا أنّ إيجاد المشرط مسألة اتباع أثر مدخلات الذاكرة حتى تجد عشًا يشير إلى نفسه. اكتب دالة async اسمها locateScalpel تفعل ذلك، بحيث تبدأ من العش الذي تشغّله، كما يمكنك استخدام الدالة anyStorage التي عرَّفناها من قبل للوصول إلى الذاكرة في أعشاش عشوائية، وافترض أن المشرط قد دار لمدة تكفي لكي يحتوي كل عش على مُدخل "scalpel" في ذاكرة بياناته، ثم اكتب الدالة نفسها مرةً أخرى دون استخدام async وawait. هل تظهر طلبات الفشل مثل رفض للوعود المعادة في كلا النسختين من الدالة؟ وكيف؟ تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen. async function locateScalpel(nest) { // شيفرتك هنا } function locateScalpel2(nest) { // شيفرتك هنا } locateScalpel(bigOak).then(console.log); // → Butcher Shop إرشادات للحل يمكن تنفيذ هذا التدريب بحلقة واحدة تبحث في الأعشاش وتنتقل من العش لغيره حين تجد قيمةً لا تطابق اسم العش الحالي، وتعيد اسم العش حين تجد قيمة مطابِقة، كما يمكن استخدام حلقة while أو for عادية في دالة async. عليك بناء حلقتك باستخدام دالة تعاودية لتنفيذ الشيء نفسه في الدالة المجردة plain function، وأسهل طريقة لذلك هي جعل تلك الدالة تعيد وعدًا باستدعاء then على الوعد الذي يجلب قيمة الذاكرة. سيُعيد المعالج تلك القيمة أو وعدًا مستقبليًا يُنشأ باستدعاء الدالة الحلقية مرةً أخرى بناءً على مطابقة القيمة لاسم العش الحالي. لا تنسى بدء الحلقة باستدعاء الدالة التعاودية مرة واحدة من الدالة الأساسية. تحوِّل await الوعود المرفوضة في دالة async إلى استثناءات، وحين ترفع دالة async استثناءً سيُرفض وعدها، وعليه تكون هذه الطريقة ناجحةً. إذا استخدمت دالة ليست async كما وضحنا سابقًا، فستتسبب الطريقة التي تعمل بها then تلقائيًا في وجود الفشل في الوعد المعاد، وإذا فشل الطلب فلن يُستدعى المعالِج الممرَّر إلى then، ويُرفض الوعد الذي يعيده للسبب نفسه. بناء Promise.all إذا كانت لدينا مصفوفة من الوعود، فإن Promise.all تعيد وعدًا ينتظر جميع الوعود الأخرى في المصفوفة حتى انتهائها، حيث سينجح ذلك الوعد ويُحصِّل مصفوفةً من القيم الناتجة، فإذا فشل وعد في المصفوفة، فسيفشل الوعد المعاد من all أيضًا، ويكون سبب الفشل هو الوعد الفاشل. استخدِم شيئًا مثل ذلك بنفسك في صورة دالة عادية اسمها Promise_all، وتذكَّر أنّ الوعد لا يمكن أن ينجح أو يفشل بعد نجاحه أو فشله أول مرة، وأنّ الاستدعاءات إلى الدوال التي تحله تُتَجاهل، إذ يُبسِّط ذلك الطريقة التي تعالج بها فشل وعدك. تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen. function Promise_all(promises) { return new Promise((resolve, reject) => { // شيفرتك هنا. }); } // شيفرة الاختبار. Promise_all([]).then(array => { console.log("This should be []:", array); }); function soon(val) { return new Promise(resolve => { setTimeout(() => resolve(val), Math.random() * 500); }); } Promise_all([soon(1), soon(2), soon(3)]).then(array => { console.log("This should be [1, 2, 3]:", array); }); Promise_all([soon(1), Promise.reject("X"), soon(3)]) .then(array => { console.log("We should not get here"); }) .catch(error => { if (error != "X") { console.log("Unexpected failure:", error); } }); إرشادات للحل ستُستدعى then من قِبَل الدالة التي تمرَّر إلى باني Promise وذلك على كل وعد في المصفوفة المعطاة، وعند نجاح أحد تلك الوعود، فيجب تخزين القيمة الناتجة في الموضع الصحيح في المصفوفة الناتجة، كما يجب التحقق هل كان ذلك هو الوعد الأخير المنتظِر أم لا، وننهي وعدنا الخاص إن كان. ويمكن تنفيذ ذلك التحقق الأخير بعدَّاد يبدأ بطول مصفوفة الدخل، ونطرح منه 1 في كل مرة ينجح فيها أحد الوعود، ونكون قد انتهينا عند وصوله إلى الصفر. تأكد أن تضع في حسابك موقفًا تكون فيه مصفوفة الدخل فارغة من الوعود، فكيف تُحل الوعود وهي غير موجودة؟ تتطلب معالجة حالات الفشل بعض التأني، لكنها بسيطة في الوقت نفسه، إذ ما عليك سوى تمرير دالة reject للوعد المغلِّف إلى كل الوعود الموجودة في المصفوفة على أساس معالِج catch، أو على أساس وسيط ثاني إلى then، فإذا حدث فشل في أحدها، فسيطلق رفضًا للوعد المغلِّف كله. ترجمة -بتصرف- للفصل الحادي عشر من كتاب Elequent Javascript لصاحبه Marijn Haverbeke. اقرأ أيضًا المقال السابق: الوحدات Modules في جافاسكريبت المكررات iterators والمولدات generators غير المتزامنة في جافاسكربت
  7. أشرنا في المقال السابق إلى عدة أنواع أساسية من البيانات البرمجية، وفي هذا المقال سنتابع مع ذكر نوع هام جدًا منها، وهو البيانات التجميعية collections. التجميعات Collections لقد شكلت علوم الحاسوب نظامًا كاملًا يدرس التجميعات وسلوكياتها المختلفة، والتي تسمى بالحاويات أحيانًا أو التسلسلات، وسننظر أولًا في التجميعات المدعومة في بايثون وجافاسكربت وVBScript، ثم نختم بملخص موجز لبعض أنواع التجميعات الأخرى التي قد نراها في لغات أخرى. القوائم Lists لا شك في أن القوائم غنية عن تعريفها، إذ نستخدمها في حياتنا كل يوم، فهي مجرد سلسلة من العناصر، حيث نستطيع إضافة عناصر إلى تلك السلسلة أو حذفها، وعادةً ما نضيف العناصر في نهاية القائمة إذا كانت مكتوبةً على الورق، فلا نستطيع إدخال عنصر في المنتصف مثلًا، على عكس القوائم الإلكترونية في برنامج معالج النصوص مثلًا، إذ نستطيع وضع العنصر الجديد في أي مكان، كما نستطيع البحث في القائمة لنتحقق من وجود شيء بعينه فيها، لكن يجب التنقل في القائمة من أعلاها لأسفلها عنصرًا عنصرًا متحققين من كل عنصر لنرى إذا كان ما نريده موجودًا أم لا، وتُعَد القوائم أحد أنواع التجميعات التي لا غنى عنها في أغلب لغات البرمجة الحديثة. لا تحتوي جافاسكربت ولا VBScript على قوائم مضمّنة فيها، لكن نستطيع محاكاة مزايا القوائم بواسطة المصفوفات في جافاسكربت، وبتجميعات بيانات أخرى في VBScript كما سنشرح لاحقًا؛ أما لغة بايثون فالقوائم مضمّنة تلقائيًا فيها، وتستطيع تنفيذ العمليات الأساسية التي تحدثنا عنها جميعًا، إضافةً إلى القدرة على فهرسة العناصر فيها، والفهرسة هي الإشارة إلى عنصر في القائمة برقم تسلسله، على فرض أن العنصر الأول يبدأ من الصفر، كما توفر بايثون عدة عمليات أخرى على التجميعات، وتنطبق كلها تقريبًا على القوائم، كما تنطبق مجموعة فرعية منها على أنواع أخرى من التجميعات بما في ذلك السلاسل النصية التي هي مجرد نوع خاص من القوائم، إذ هي قوائم من المحارف. تُستخدم الأقواس المربعة لإنشاء القوائم والوصول إليها في بايثون، ويمكن إنشاء قائمة فارغة باستخدام زوج من تلك الأقواس ليس بينهما شيء، أو إنشاء قائمة تحتوي على قيم تفصل بينها فاصلة إنجليزية ",": >>> aList = [] >>> another = [1,2,3] >>> print( another ) [1, 2, 3] نستطيع الوصول إلى العناصر المنفردة باستخدام رقم الفهرس، حيث يحمل العنصر الأول رقم 0 داخل الأقواس المربعة، فإذا أردنا الوصول إلى العنصر الثالث مثلًا -والذي سيحمل الترتيب 2 داخل الأقواس بما أننا نبدأ من الصفر-، فإننا نصل إليه كما يلي: >>> print( another[2] ) 3 كما نستطيع تغيير قيم العناصر في القائمة بطريقة مماثلة: >>> another[2] = 7 >>> print( another ) [1, 2, 7] لاحظ كيف تغير العنصر الثالث -الذي فهرسه 2- من 3 إلى 7. يمكن استخدام أرقام الفهرس السالبة للوصول إلى عناصر القائمة من نهايتها، فإذا أردنا العنصر الأخير من القائمة نستخدم ‎-1: >>> print( another[-1] ) 7 وتضاف العناصر الجديدة ملحقةً إلى نهاية القائمة باستخدام العملية append()‎: >>> aList.append(42) >>> print( aList ) [42] كما يمكن الاحتفاظ بقائمة داخل قائمة أخرى، فإذا ألحقنا القائمة الثانية بالقائمة الأولى فستكون على النحو التالي: >>> aList.append(another) >>> print( aList ) [42, [1, 2, 7]] لاحظ كيف تكون النتيجة قائمةً من عنصرين، لكن العنصر الثاني عبارة عن قائمة بحد ذاته كما نرى من القوسين المحيطين به، ونستطيع الآن الوصول إلى العنصر 7 باستخدام فهرس مزدوج: >>> print( aList[1][2] ) 7 حيث يستخرج الفهرس 1 -الفهرس الأول- العنصر الثاني من القائمة الأولى -الذي هو نفسه القائمة الثانية-، ثم يستخرج الفهرس 2 -الفهرس الثاني- العنصر الثالث في القائمة الثانية، وهو العنصر 7. هذا التداخل وإن بدا معقدًا إلا أنه مفيد للغاية، حيث يسمح ببناء جداول من البيانات بفعالية، كما يلي: >>> row1 = [1,2,3] >>> row2 = ['a','b','c'] >>> table = [row1, row2] >>> print( table ) [ [1,2,3], ['a','b','c'] ] >>> element2 = table[0][1] >>> print( element2 ) 2 يمكن استخدام هذه الطريقة في إنشاء دفتر عناوين يكون كل مدخل فيه عبارةً عن قائمة من الاسم والعنوان، ويوضح المثال التالي دفتر عناوين فيه مدخلان: >>> addressBook = [ ... ['Samy', '9 Some St',' Anytown', '0123456789'], ... ['Warda', '11 Nother St', 'SomePlace', '0987654321'] ... ] >>> لاحظ كيف أنه رغم إدخالنا لأربعة أسطر من النص، إلا أن بايثون تعاملت معها على أنها سطر واحد، كما نستنتج من محثات ...، وهذا لأن بايثون ترى أن عدد الأقواس الافتتاحية لا يطابق عدد أقواس الإغلاق، ولهذا تستمر في قراءة المدخلات إلى أن يحدث ذلك التطابق. قد تكون هذه طريقةً فعالةً للغاية في بناء هياكل بيانات معقدة بسرعة مع جعل الهيكل العام -أي قائمة القوائم- واضحًا للقارئ. تدريب استخرج رقم هاتف سامي -العنصر 3 من الصف الأول-، وتذكر أن الفهارس تبدأ من الصفر وليس الواحد، ثم أضف بعض المدخلات من عندك باستخدام عملية append()‎. لاحظ أننا نفقد بياناتنا عند الخروج من بايثون، لكن سنرى كيفية الاحتفاظ بها عند شرح الملفات في هذا الكتاب. يُستخدم الأمر del لحذف العناصر من القائمة: >>> del aList[1] >>> print( aList ) [42] لاحظ أن del لا يحتاج إلى أقواس حول القيمة، على عكس دالة الطباعة، لأنه أمر وليس دالةً، قد يكون هذا الفرق طفيفًا، لكن إذا وضعت أقواسًا حول القيمة فلا بأس إذ ستظل صالحةً. إذا أردنا ضم قائمتين معًا لجعلهما قائمةً واحدةً، فسنتخدم عامل الضم + الذي رأيناه في ضم السلاسل النصية من قبل: >>> newList = aList + another >>> print( newList ) [42, 1, 2, 7] لاحظ اختلاف هذا المثال عن سابقه الذي ألحقنا فيها القائمتين معًا، فقد كان لدينا عنصران، وكان العنصر الثاني منهما عبارةً عن قائمة؛ أما هنا فلدينا أربعة عناصر لأن عناصر القائمة الثانية أضيفت عنصرًا عنصرًا إلى newList، فإذا أردنا الوصول إلى العنصر 1 هذه المرة فسنحصل على 1، وهو العنصر الثاني هنا، بدلًا من الحصول على قائمة فرعية. >>> print( newList[1] ) 1 يمكن تطبيق إشارة عملية الضرب هنا مثل عامل تكرار لملء القائمة بمضاعفات نفس القيمة: >>> zeroList = [0] * 5 >>> print( zeroList ) [0, 0, 0, 0, 0] كما يمكن إيجاد فهرس عنصر ما في قائمة باستخدام العملية index()‎ كما يلي: >>> print( [1,3,5,7].index(5) ) 2 >>> print( [1,3,5,7].index(9) ) Traceback (most recent call last): File "<stdin>", line 1, in <module> ValueError: list.index(x): x not in list لاحظ أن محاولة العثور على فهرس لشيء غير موجود في القائمة ينتج خطأً، فإذا أردنا التحقق من وجود شيء في تجميعة ما فسنستخدم العامل in كما يلي: >>> print( 5 in [1,3,5,7] ) True >>> print( 9 in [1,3,5,7] ) False النتائج قيم بوليانية كما نرى، إما True أو False، وسنرى كيف نستخدم تلك النتائج في فصل لاحق. أخيرًا، يمكن معرفة طول القائمة باستخدام الدالة المضمَّنة len()‎: >>> print( len(aList) ) 1 >>> print( len(newList) ) 4 >>> print( len(zeroList) ) 5 لا تدعم جافاسكربت ولا VBScript نوع القائمة مباشرةً، وسنرى أن لديهما نوع بيانات يحاكي وظيفة القائمة هنا، وهو نوع المصفوفات arrays. الصف Tuple توفر بعض لغات البرمجة بنية بيانات اسمها صف tuple، وما هي إلا تجميعة عشوائية من القيم لكنها تعامَل مثل وحدة واحدة، وهي تشبه القوائم في نواحٍ كثيرة، لكن مع فرق أن وحدات tuple غير قابلة للتغيير، مما يعني أننا لا نستطيع التعديل عليها أو إلحاق شيء إليها بعد إنشائها أول مرة، تمثَّل وحدات tuple في بايثون بقائمة من القيم المفصول بينها بفواصل: >>> aTuple = 1,3,5 >>> print( aTuple[1] ) # استخدم الفهرسة مثل القوائم 3 >> aTuple[2] = 7 # tuple خطأ، لا يمكن تعديل Traceback (innermost last): File "<pyshell#20>", line 1, in ? aTuple[2] = 7 TypeError: object doesn't support item assignment من المعتاد إحاطة التسلسل بأقواس، وهذا يضمن عدم إخراج القيم من السياق، إضافةً إلى التذكير المرئي بأنها وحدة واحدة، أي وحدة صف، توضح البيانات التالية هذا الأمر: >>> t1 = 1,2,3 * 3 >>> t2 = (1,2,3) * 3 >>> print(t1) (1, 2, 9) >>> print( t2 ) (1, 2, 3, 1, 2, 3, 1, 2, 3) يطبَّق الضرب في الحالة الأولى على العنصر الأخير فقط في وحدة Tuple، أما في الحالة الثانية فتضاعَف المجموعة بالكامل، ولتجنب مثل هذا الغموض، سنستخدم الأقواس في كل مرة نصف فيها وحدات tuple. لعل أحد أهم الأمور التي يجب تذكرها هو أن الأقواس المربعة تستخدم لفهرسة وحدات tuple، بينما تُستخدم الأقواس العادية لتعريفها، ولا يمكن تغييرها بعد إنشائها؛ أما في سوى هذه الأمور فما ينطبق من العمليات على القوائم ينطبق أيضًا على الصفوف، ورغم أننا لا نستطيع تعديل وحدة الصف بعد إنشائها؛ إلا أننا نستطيع إضافة أعضاء جديدة إليها باستخدام عامل الإضافة، فهذا سينشئ صفًا جديدًا، كما يلي: >>> tup1 = (1,2,3) >>> tup2 = tup1 + (4,) # وليس عددًا صحيحًا tuple الفاصلة تجعل هذا وحدة >>> print( tup2 ) (1,2,3,4) إذا لم نستخدم الفاصلة اللاحقة بعد العدد 4 فستفسره بايثون على أنه العدد الصحيح 4 داخل أقواس وليس على أنه صف، لكن سنحصل على خطأ بأي حال بما أنه لا يمكن إضافة أعداد صحيحة إلى الصفوف، لذا سنضيف الفاصلة لنخبر بايثون بأن تعامل الأقواس على أنها صف، فبإضافة الفاصلة الزائدة نقنع بايثون بأن وحدة الصف التي فيها مدخل واحد هي صف فعلًا؛ أما بالنسبة للغتي جافاسكربت وVBScript فليس لديهما أي مفهوم للصف tuple. القاموس Dictionary أو Hash يحتوي هذا النوع من البيانات على قيمة ترتبط بمفتاح، كما يربط القاموس اللغوي المعنى بكلمة، وقد تكون تلك القيمة سلسلةً نصيةً أو لا، وتُجلب تلك القيمة بفهرسة القاموس بالمفتاح، ولا يلزم أن يكون المفتاح سلسلةً من الأحرف -رغم أنه غالبًا كذلك-، وهذا على خلاف القاموس اللغوي، فقد يكون أي نوع غير قابل للتغير، بما في ذلك الأعداد والصفوف، كما قد تحتوي القيم المرتبطة بالمفاتيح على أي نوع من البيانات. تُستخدم القواميس داخليًا بواسطة تقنية برمجة متقدمة تسمى بجداول Hash، لهذا يشار أحيانًا إلى القاموس باسم Hash، ونظرًا لأننا نتوصَّل إلى قيم القاموس عبر المفتاح، فلا توضع إلا عناصر ذات مفاتيح فريدة، رغم أن المفتاح قد يشير إلى قائمة من القيم. تُعَد القواميس مفيدةً للغاية في هيئة هياكل للبيانات، ونجدها في بايثون مثل نوع بيانات مضمَّن في اللغة نفسها، لكن قد نحتاج في بعض اللغات الأخرى إلى استخدام وحدة module، أو بناء واحدة بأنفسنا إذا أردنا استخدام القواميس، وتُستخدم القواميس بعدة طرق سنراها كثيرًا في الأمثلة التالية من الكتاب؛ أما الآن فسنرى أولًا كيف ننشئ قاموسًا في بايثون، ثم نملؤه ببعض المدخلات ونقرؤها. >>> dct = {} >>> dct['boolean'] = "A value which is either true or false" >>> dct['integer'] = "A whole number" >>> print( dct['boolean'] ) A value which is either true or false لاحظ أننا نبدأ القاموس بأقواس معقوصة ثم نستخدم الأقواس المربعة لإسناد القيم وقراءتها، كما تُستخدم دالة النوع dict()‎ لإعادة قاموس فارغ، ويمكن بدء القاموس أثناء إنشائه كما فعلنا في حالة القوائم، باستخدام الصيغة التالية: >>> addressBook = { ... 'Samy' : ['Samy', '9 Some St',' Anytown', '0123456789'], ... 'Warda' : ['Warda', '11 Nother St', 'SomePlace', '0987654321'] ... } >>> يُفصل المفتاح والقيمة بنقطتين رأسيتين، وتُفصل الأزواج بفواصل إنجليزية ,، كما يمكن تخصيص القاموس باستخدام صيغة مختلفة -انظر أدناه-، ويمكنك اختيار إحدى الطريقتين لاستخدامها. >>> book = dict(Samy=['Samy', '9 Some St',' Anytown', '0123456789'], ... Warda=['Warda', '11 Nother St', 'SomePlace', '0987654321']) >>> print( book['Samy'][3] ) 0123456789 لاحظ أننا لا نحتاج إلى علامات اقتباس حول المفتاح في التعريف، لأن بايثون تفترض أنها سلسلة نصية، لكننا نحتاج إليها بأي حال من أجل استخراج القيم، وهذا الأسلوب يَحُد من عملية الطريقة الثانية، لهذا يفضِّل المبرمجون استخدام الطريقة الأولى التي يستخدمون الأقواس المعقوصة فيها، وبهذا نكون قد أنشأنا دفتر عناوين من قاموس مفاتيحه هي الأسماء، ويخزن قوائمنا مثل قيم، ونريد أن نستخدم الاسم وحده لجلب كل المعلومات بدلًا من حساب الفهرس العددي، كما يلي: >>> print( addressBook['Warda'] ) ['Warda', '11 Nother St', 'SomePlace', '0987654321'] >>> print( addressBook['Samy'][3] ) 0123456789 في الحالة الثانية فهرسنا القيم المعادة من أجل الحصول على رقم هاتف فقط، لكن يمكن جعل هذا أسهل عبر إنشاء بعض المتغيرات وإسناد قيم الفهرسة المناسبة، كما يلي: >>> name = 0 >>> street = 1 >>> town = 2 >>> tel = 3 نستطيع الآن استخدام تلك المتغيرات لنعرف اسم مدينة Warda: >>> print( addressBook['Warda'][town] ) SomePlace لاحظ أن town ليست داخل علامات اقتباس لأنها اسم متغير، وستحوله بايثون إلى قيمة الفهرس التي أسندناها -وهي 2-، على عكس 'Warda' المحاطة بعلامات اقتباس لأن المفتاح هو سلسلة نصية، وهنا بدأ دفتر العناوين يتحول إلى ما يشبه التطبيق القابل للاستخدام بسبب مزايا القواميس، حيث لن نحتاج إلى كثير من العمل الإضافي لحفظ البيانات واستعادتها وإضافة محث استعلام ليسمح لنا بتحديد البيانات التي نريدها. يمكن استخدام قاموس لتخزين البيانات، وسيكون دفتر العناوين حينها قاموسًا مفاتيحه الأسماء، وستكون القيم قواميس مفاتيحها هي حقول الأسماء، كما يلي: addressBook = { ... 'Samy' : {'name': 'Samy', ... 'street': '9 Some St', ... 'town': 'Anytown', ... 'tel': '0123456789'}, ... 'Warda' : {'name': 'Warda', ... 'street': '11 Nother St', ... 'town': 'SomePlace', ... 'tel': '0987654321'} ... } لاحظ أن هذا التنسيق سهل القراءة رغم أنه يحتاج إلى الكثير من الكتابة، ويُشار إلى مثل هذه البيانات التي تخزَّن بتنسيق يجمع بين المعنى والمحتوى في تنسيق مقروء للبشر باسم البيانات الموثقة ذاتيًا. حين ندخِل هيكل بيانات داخل هيكل آخر مطابق له، كما في حالة إدخال قاموس داخل قاموس آخر هنا، فإنه يُسمى بالتداخل أو التشعب nesting، حيث يكون القاموس الداخلي قاموسًا متشعبًا من الخارجي، ويمكن الوصول إلى تلك البيانات بطريقة تشبه أسلوب القوائم ذات الفهارس المسماة: >>> print( addressBook['Warda']['town'] ) SomePlace وليس ثمة اختلاف إلا في الاقتباس الإضافي حول كلمة town التي هي ليست موجودةً في حالة القوائم، وميزة هذا الأسلوب أننا نستطيع إدخال حقول جديدة دون تعطيل الشيفرة الحالية، بينما نضطر في حالة الفهارس المسماة إلى العودة لتغيير جميع قيم الفهارس، وتبرز أهمية هذا الأسلوب عند استخدام نفس البيانات في عدة برامج، فيكون بذل قليل من الجهد هنا موفرًا لكثير من العمل لاحقًا في المستقبل. لا تدعم القواميس كثيرًا من عوامل التجميعات التي رأيناها من قبل، فلا تدعم عامل الضم ولا التكرار ولا الإلحاق، رغم إمكانية إسناد أزواج من المفاتيح والقيم مباشرةً كما رأينا في بداية هذا القسم. وتُستخدم العملية keys()‎ لتسهيل الوصول إلى مفاتيح القاموس، إذ تسمح لنا بالحصول على قائمة من جميع المفاتيح الموجودة في قاموس ما، فإذا أردنا الحصول على كل الأسماء الموجودة في دفتر العناوين الذي أنشأناه من قبل؛ فسيكون ذلك كما يلي: >>> print( list(addressBook.keys()) ) ['Samy','Warda'] لاحظ أننا استخدمنا list()‎ للحصول على قيم المفاتيح الحقيقية، أما إذا حذفنا list()‎ فسنحصل على نتيجة غريبة بعض الشيء لن نذكرها الآن. لاحظ أن القواميس لا تخزِّن مفاتيحها بنفس ترتيب إدخالها، فقد نرى أن المفاتيح تظهر بترتيب غريب عن ترتيبها الأول، بل قد يتغير الترتيب بتغير الوقت، لكن هذا لا يهم بما أننا نستطيع استخدام المفاتيح للوصول إلى البيانات المرتبطة بها، إذ سنحصل على القيم الصحيحة في كل مرة. يمكن الحصول على قائمة بجميع القيم أيضًا باستخدام العملية values()‎، فجرب ذلك على دفتر العناوين وانظر إن استطعت تنفيذها بنجاح، استخدم مثال keys()‎ السابق للاسترشاد به، ويمكن بصورة مماثلة استخدام العامل in على القاموس ليخبرنا هل توجد قيمة ما في صورة مفتاح للقاموس أم لا. قواميس VBScript توفر VBScript كائن قاموس يقدم تسهيلات تماثل قاموس بايثون، لكن استخدامه مختلف قليلًا عن حالة بايثون، حيث ننشئ القاموس هنا بالتصريح عن متغير يحمل الكائن، ثم ننشئ ذلك الكائن، وبعد ذلك نضيف المدخلات إلى القاموس الجديد كما يلي: Dim dict ' Create a variable. Set dict = CreateObject("Scripting.Dictionary") dict.Add "a", "Athens" ' Add some keys and items. dict.Add "b", "Belgrade" dict.Add "c", "Cairo" لاحظ أن دالة CreateObject توضح أننا ننشئ كائن "Scripting.Dictionary"، وهو كائن Dictionary من وحدة Scripting الخاصة بلغة VBScript، وسندرس ذلك بالتفصيل حين نأتي على الكائنات، لكننا نأمل بذكرها هنا أن تتذكر مفهوم استخدام كائن من وحدة ما كما ذكرنا في فصل التسلسلات البسيطة. يجب كذلك استخدام كلمة Set المفتاحية عند إسناد كائن إلى متغير في VBScript. نستطيع الآن أن نصل إلى البيانات كما يلي: item = dict.Item("c") ' Get the item. dict.Item("c") = "Casablanca" ' Change the item توجد عمليات أخرى لإزالة عنصر ما، والتحقق من وجود مفتاح ما، والحصول على قائمة بجميع المفاتيح وغير ذلك. كما يمكن تخزين أي نوع من الكائنات في القاموس إذ أن الأمر ليس مقصورًا على السلاسل النصية فقط، وهذا يعني أننا نستطيع إنشاء قواميس متشعبة بما أن القاموس نفسه ما هو إلا كائن. فيما يلي نسخة كاملة مبسطة عن مثال دفتر العناوين السابق، ولكن في VBScript: <script type="text/VBScript"> Dim addressBook Set addressBook = CreateObject("Scripting.Dictionary") addressBook.Add "Samy", "Samy, 9 Some St, Anytown, 0123456789" addressBook.Add "Warda", "Warda, 11 Nother St, SomePlace, 0987654321" MsgBox addressBook.Item("Warda") </script> ما فعلناه هنا هو تخزين جميع البيانات مثل سلسلة نصية واحدة بدلًا من استخدام قائمة -وهذا يصعّب استخراج الحقول المنفردة على عكس القائمة والقاموس-، ثم نستطيع الوصول إلى تفاصيل Warda ونطبعها في صندوق رسالة. قواميس جافاسكربت لا تحتوي جافاسكربت على كائن قاموس فيها، رغم أننا نستطيع الوصول إلى كائن Scripting.Dictionary الذي ذكرناه في VBScript باستخدام إنترنت إكسبلورر، لنتمكن حينئذ من استغلال جميع مزاياه، لكن لن نشرح ذلك هنا مرةً أخرى بما أنه نفس الكائن. تستطيع هنا الانتقال إلى الفصل التالي إذا كنت قد شعرت بالملل من طول هذا الفصل وتريد كتابة شيفرات وتجربتها، لكن تذكر أن تعود وتنهي هذا الفصل حين تتعرض لأنواع بيانات لم نذكرها حتى الآن. المصفوفات أو المتجهات Arrays المصفوفة هي أحد أنواع التجميعات التي يمتد تاريخها موازيًا لتاريخ الحوسبة، وما هي إلا مجموعة من العناصر المفهرسة بحيث يسهل جلبها بسرعة، والغالب أننا يجب أن نحدد عدد العناصر التي نريد تخزينها في المصفوفة مسبقًا، إذ يُخزَّن نوع واحد أيضًا من البيانات في المصفوفة عادةً، وهاتان الخاصيتان للمصفوفة هما اللتان تميزانها عن القوائم مثلًا. لاحظ أننا قد قلنا "عادة"، و"الغالب" للدلالة على غالب الحالات، ذلك أن لغات البرمجة المختلفة لها أساليبها الخاصة التي تصف المصفوفات، لذا يصعب وضع وصف يغطي جميع الحالات. تدعم لغة بايثون المصفوفات من خلال وحدة تسمى array، لكننا لا نحتاج إليها إلا نادرًا مع استثناء حالات الحساب الرياضي عالي الأداء؛ أما غير ذلك فنستخدم القوائم، وفي لغتي جافاسكربت وVBScript؛ تتكون المصفوفات مثل نوع بيانات، لذا سننظر في كيفية استخدامها فيهما. مصفوفات VBScript تكون المصفوفة في VBScript تجميعة بيانات ذات طول ثابت، حيث يمكن التوصَل إليها بواسطة فهرس عددي، ويصرَّح عنها ويوصَل إليها كما يلي: Dim AnArray(42) ' A 43! element array AnArray(0) = 27 ' index starts at 0 AnArray(1) = 49 AnArray(42) = 66 ' assign to last element myVariable = AnArray(1) ' read the value تُستخدم الكلمة المفتاحية Dim لتحديد أبعاد المتغير، وهي الطريقة التي نخبر بها VBScript عن المتغير، فهي تتوقع إذا بدأنا الشيفرة بـ OPTION EXPLICIT؛ أن نحدد أبعاد أي متغير نستخدمه من خلال Dim، وهذا سلوك محمود يؤدي إلى برامج ثابتة قوية الأداء، كما أننا حددنا آخر فهرس صالح 42، وهذا يعني أن المصفوفة فيها 43 عنصرًا بما أنها تبدأ من الصفر. تُستخدم الأقواس في VBScript لحساب أبعاد المصفوفة وفهرستها، وهذا على خلاف الأقواس المربعة المستخدمة في بايثون وجافاسكربت، حيث لا يوجد في VBScript نوع بيانات إلا النوع Variant، والذي يخزِّن أي نوع من أنواع القيم، وبناءً عليه فلا تخزِّن المصفوفات في هذه اللغة إلا المتغيرات بما أن عناصر المصفوفة تكون من نوع واحد، لكن في نفس الوقت، فإن المتغير هنا قد يحمل أي نوع من أنواع القيم، مما يعني مرةً أخرى أن المصفوفات في VBScript تخزن أي شيء، وهذا أمر مربك عند التفكير فيه. نستطيع أيضًا الإسناد إلى أي موضع في المصفوفة بما في ذلك الموضع الأخير، من غير أن نملأ المواضع التي قبله، وسيُضبط أي عنصر غير مهيأ إلى القيمة الخاصة Null. كما يمكن التصريح عن عدة مصفوفات متعددة الأبعاد لنمذجة جداول بيانات كما في قوائم بايثون إذا استخدمنا تدريب دفتر العناوين مثلًا. Dim MyTable(2,3) ' 3 rows, 4 columns MyTable(0,0) = "Samy" ' Populate Samy's entry MyTable(0,1) = "9 Some Street" MyTable(0,2) = "Anytown" MyTable(0,3) = "0123456789" MyTable(1,0) = "Warda" ' And now Warda... ...and so on... لكن لا توجد طريقة سهلة لملء البيانات دفعةً واحدةً كما فعلنا في قوائم بايثون، بل يجب أن نملأ كل حقل على حدة، وعلى الرغم من وجود الدالة Arrayفي VBScript والتي تستطيع ملء مصفوفة دفعةً واحدةً -مما يعني الحاجة إلى جعلها متشعبةً-؛ فستصبح معقدةً وصعبة القراءة. إذا دمجنا قواميس VBScript مع هذه الخاصية لمصفوفاتها، فسنحصل على نفس النتيجة التي حصلنا عليها في بايثون، كما يلي: <script type="text/VBScript"> Dim addressBook Set addressBook = CreateObject("Scripting.Dictionary") Dim Samy(3) Samy(0) = "Samy" Samy(1) = "9 Some St" Samy(2) = "Anytown" Samy(3) = "0123456789" addressBook.Add "Samy", Samy MsgBox addressBook.Item("Samy")(3) ' Print the Phone Number </script> وآخر ملاحظة متعلق بمصفوفات VBScript هو أنها لا تحتاج إلى أن تكون ثابتة الحجم، على أن هذا لا يعني استمرار إضافة العناصر إليها كما في القوائم، بل يجب أن نغير حجم المصفوفة صراحةً، بالتصريح عن مصفوفة ديناميكية، وذلك بإهمال ذكر الحجم ببساطة، كما يلي: Dim DynArray() ' no size specified ثم إذا أردنا تغيير حجمها نفعل ذلك كما يلي: <script type="text/vbscript"> Dim DynArray() ReDim DynArray(5) ' Initial size = 5 DynArray(0) = 42 DynArray(4) = 26 MsgBox "Before: " & DynArray(4) ' prove that it worked ' Resize to 21 elements keeping the data we already stored ReDim Preserve DynArray(20) DynArray(15) = 73 MsgBox "After Preserve: " & DynArray(4) & " " & DynArray(15)' Old and new still there ' Resize to 51 items but lose all data Redim DynArray(50) MsgBox "Without Preserve: " & DynArray(4) & " Oops, Where did it go?" </script> لاحظ أن السلوك الافتراضي هو حذف كل محتويات المصفوفة عند تغيير حجمها، فإذا أردنا الحفاظ على محتوياتها؛ فيجب أن نخبر VBScript أن تحفظ المحتويات باستخدام Preserve. هذا الأسلوب غير مريح موازنةً بالقوائم التي تغير حجمها تلقائيًا، لكنه يعطي المبرمج تحكمًا أفضل في كيفية تصرف البرنامج، مما يحسن الأمان -ضمن أمور أخرى- نظرًا لأن بعض الفيروسات قد تستغل مخازن البيانات القابلة لتغيير حجمها ديناميكيًا. مصفوفات جافاسكربت رغم أنه يطلَق على هذا النوع من البيانات في جافاسكربت مصفوفة؛ إلا أنها تسمية خاطئة، فهو مزيج غريب يجمع بين مزايا القوائم والقواميس والمصفوفات العادية، حيث نستطيع مثلًا التصريح عن مصفوفة من عشرة عناصر من نوع ما كما يلي: var items = new Array(10); لاحظ استخدام كلمة new المفتاحية، إذ تشبه دالة CreateObject()‎ التي استخدمناها في VBScript لإنشاء قاموس، كذلك فقد استخدمنا الأقواس لتحديد حجم المصفوفة، نستطيع الآن أن نملأ المصفوفة ونصل إليها كما يلي: items[4] = 42; items[7] = 21; var aValue = items[4]; وهنا نستخدم الأقواس المربعة للوصول إلى عناصر المصفوفة، وتبدأ الفهارس فيها من الصفر، لكن رغم هذا كله فمصفوفات جافاسكربت ليست مقيدة بتخزين نوع واحد من البيانات، بل يمكن إسناد أي شيء إلى عناصرها: items[9] = "A short string"; var msg = items[9]; كما يمكن إنشاء مصفوفات من خلال توفير قائمة بالعناصر: var moreItems = new Array("one","two","three",4,5,6); aValue = moreItems[3]; // -> 4 msg = moreItems[0]; // -> "one" كذلك يمكن تحديد طول المصفوفة باستخدام خاصية تدعى length، التي تستخدَم كما يلي: var size = items.length; وعلى غرار أسلوب بايثون في استدعاء الدوال في وحداتها، فإن الصياغة name.property هنا هي نفسها، لكن دون أقواس لاحقة. ورغم أن مصفوفات جافاسكربت تبدأ بالصفر كما ذكرنا؛ إلا أن فهارسها ليست مقصورةً على الأرقام وحدها، ولهذا يمكن استخدام السلاسل النصية أيضًا -والتي تكون هنا أشبه بالقواميس-، كما يمكن توسيع مصفوفة بإسناد قيمة إلى فهرس تكون أكثر من الحد الأقصى الحالي، مما يعني أننا لا نحتاج إلى تحديد الحجم عند إنشاء المصفوفة، رغم كون ذلك سلوكًا مستحسنًا في البرمجة، ويوضح المثال التالي خصائص مصفوفات جافاسكربت التي ذكرناها: <script type="text/javascript"> var items = new Array(10); var moreItems = new Array(1); items[42] = 7; moreItems["foo"] = 42; msg = moreItems["foo"]; document.write("msg = " + msg + " and items[42] = " + items[42] ); </script> إذا شغّلنا تلك الشيفرة في المتصفح فسنرى القيم مكتوبةً على الشاشة. تحقق إن كنت تستطيع متابعة الإسنادات في الشيفرة لتتأكد من سبب ظهور القيم التي تراها على الشاشة. لننظر أخيرًا في مثال دفتر العناوين مرةً أخرى، باستخدام مصفوفات جافاسكربت هذه المرة: <script type="text/javascript"> var addressBook = new Array(); addressBook["Samy"] = new Array("Samy", "9 Some St", "Anytown", "0123456789"); addressBook["Warda"] = new Array("Warda", "11 Nother St", "SomePlace", "0987654321"); document.write(addressBook.Warda); </script> يمكن الوصول إلى المفتاح كما لو كان خاصيةً مثل length، ويتبين مما سبق أن مصفوفات جافاسكربت ما هي إلا هياكل بيانات مرنة للغاية. المكدس Stack يكدِّس العاملون في المطاعم الأطباق النظيفة فوق بعضها، ثم تؤخَذ واحدًا تلو الآخرللعملاء بدءًا من الأعلى، فيكون الطبق الأخير هو آخر طبق يُستخدم، وفي نفس الوقت أقل طبق يُستخدم، يشبه هذا المثال مكدسات البيانات بالضبط، إذ نضع العنصر في قمة المكدس أو نأخذ عنصرًا منه، ويكون العنصر المأخوذ هو آخر عنصر مضاف إلى المكدس، ويطلَق على هذه الخاصية للمكدسات "آخرها دخولًا أولها خروجًا" أو LIFO - Last In First Out. من الخصائص المفيدة في المكدسات أننا نستطيع عكس قائمة العناصر عن طريق دفع القائمة في المكدس ثم إخراجها مرةً أخرى، فتكون النتيجة عكس القائمة الأولى. لا تأتي المكدسات مدمجةً في بايثون ولا VBScript ولا جافاسكربت، بل يجب كتابة بعض التعليمات البرمجية في البرنامج من أجل الحصول على سلوك المكدس، والقوائم هي أفضل نقطة بداية في الغالب للمكدسات بما أنه يمكن زيادة حجمها عند الحاجة. تدريب اكتب مكدسًا باستخدام قائمة في بايثون، وتذكر أننا نلحق العناصر في نهاية القائمة باستخدام append()‎، ونحذفها باستخدام فهارسها عن طريق del()‎، كما تستطيع استخدام ‎-1 مثل فهرس يشير إلى آخر عنصر في القائمة. يجب أن تكون قادرًا على كتابة برنامج -مستفيدًا من هذه الإرشادات- يدفع 4 محارف إلى قائمة ثم يخرجها مرةً أخرى، ثم اطبعها بعد ذلك. راقب الترتيب الذي تستدعي به print و del، وإذا نجحت في هذا، فجرب طباعتها بعكس الترتيب الذي أدخلتها به، استخدم العملية pop()‎ الخاصة بقوائم بايثون والتي تعيد العنصر الأخير وتحذفه في خطوة واحدة، من أجل تسهيل التدريب. الحقيبة Bag في سياق الحديث عن البيانات؛ تُعَد الحقيبة تجميعةً من العناصر التي ليس لها ترتيب محدد، ويمكن أن تحتوي على عناصر مكررة، وتكون لها عادةً عوامل تمكننا من إضافة تلك العناصر وحذفها وإيجادها، فما هي إلا قوائم في اللغات الثلاث التي نستخدمها في الكتاب. المجموعات Sets تخزن المجموعة نوعًا واحدًا من العناصر، ونستطيع التحقق مما إذا كان العنصر موجودًا في المجموعة أم لا -أي عضوًا فيها-، إلى جانب إضافة تلك العناصر وحذفها، بالإضافة إلى دمج مجموعتين معًا بطرق شتى تتوافق مع نظرية المجموعات في الرياضيات، مثل الاتحاد والتقاطع وغيرها، لكن على هذا فالمجموعات لا تكون مرتبةً ولا تهتم للترتيب. لا تستخدم جافاسكربت ولا VBScript المجموعات مباشرةً، لكن يمكن تنفيذ سلوك قريب باستخدام القواميس التي لا تحتوي إلا على مفاتيح بقيم فارغة؛ أما بايثون فتدعم المجموعات مثل نوع من أنواع البيانات فيها، وأحد الاستخدامات البسيطة لها ما يلي: >>> A = set() # أنشئ مجموعة فارغة >>> B = set([1,2,3]) # مجموعة من قائمة >>> C = {3,4,5} # تهيئة مثل [] في القوائم >>> D = {6,7,8} >>> # جرب الآن بعض عمليات القوائم >>> print( B.union(C) ) {1, 2, 3, 4, 5} >>> print( B.intersection(C) ) {3} >>> print( B.issuperset({2}) ) True >>> print( {3}.issubset(C) ) True >>> print( C.intersection(D) == A ) True لاحظ أننا نستطيع تهيئة المجموعة باستخدام الأقواس المعقوصة، لكن هذا قد يتشابه مع القواميس، لذا نفضل استخدام الصيغة set([...]])‎ رغم أنها تتطلب كتابةً أكثر، فيما يلي اختصارات لعمليات الاتحاد والتقاطع: >>> print( B & C ) # B.intersection(C) كما في >>> print( B | C ) # B.union(C) كما في نستطيع أخيرًا التحقق من وجود عنصر ما في مجموعة باستخدام العامل in: >>> print( 2 in B ) True هذه العمليات كافية إلى الآن لما نشرحه، رغم وجود عمليات أخرى على المجموعات. الطابور Queue يشبه الطابور أو قائمة الانتظار؛ المكدس، باستثناء أن العنصر الأول فيه الذي يخرج أولًا أيضًا، ويسمى هذا السلوك "الأول دخولًا الأول خروجًا" أو FIFO - First In First Out، ويُنشَأ الطابور باستخدام قائمة أو مصفوفة. تدريب جرب كتابة طابور باستخدام قائمة، وتذكر أنك تستطيع الإضافة إلى القائمة باستخدام append()‎، وأن تحذف من موضع ما باستخدام del()‎. أضف أربعة محارف إلى الطابور ثم استخرجها واطبعها، غذ يجب أن تُطبع المحارف بنفس الترتيب الذي دخلت به. توجد أنواع أخرى من تجميعات البيانات، لكن ما ذكرناه هنا يغطي أغلب الحالات التي ستراها أثناء البرمجة، بل لن نستخدم إلا قليلًا مما ذكرناه هنا في الشرح، لكن قد ترى بقية الأنواع في المقالات أو مجموعات النقاشات البرمجية. الملفات Files ينبغي أن يكون مستخدم الحاسوب معتادًا على التعامل مع الملفات بما أنها تمثل أساس كل شيء نفعله على الحواسيب، ولا غرو إذًا أن نكتشف أن أغلب لغات البرمجة توفر نوع file خاص من البيانات، لكن أهمية الملفات ومعالجتها ستجعلنا نرجئ الحديث عنها وشرحها إلى فصل لاحق خاص بها. وبهذا تكون قد تعرفت على أبرز البيانات البرمجية التي قد تحتاج إليها. ترجمة -بتصرف- للفصل الخامس: The Raw Materials من كتاب Learning To Program لصاحبه Alan Gauld. اقرأ أيضًا المقال التالي: بعض التسلسلات النصية المهمة لتعلم البرمجة المقال السابق: مدخل إلى البيانات وأنواعها: أنواع البيانات الأساسية مدخل إلى البيانات الوصفية microdata في HTML5 هياكل البيانات: الكائنات والمصفوفات في جافاسكريبت
  8. مدخل إلى البيانات وأنواعها: أنواع البيانات الأساسية يحتاج أي عمل إبداعي إلى ثلاثة مكونات أساسية هي: الأدوات التي يُنجَز بها، والخامات التي يتكون منها، والأساليب والتقنيات التي تُستغل الأدوات والخامات بها لإنجاز ذلك العمل، فأدوات الرسم مثلًا هي الفرش والأقلام ولوحات الرسم، كما أن من أساليب الرسم الخلط والرش وغيرها، ثم تأتي الخامات التي هي الدهانات والأوراق والماء ونحو ذلك. وبصورة مماثلة نجد أن أدوات البرمجة هي لغات البرمجة ونظم التشغيل وعتاد الحواسيب التي ستعمل عليها البرامج؛ أما الأساليب والتقنيات فهي هياكل البرامج التي تحدثنا عنها في المقال السابق، والخامات هي البيانات التي نعدل فيها ونعالجها، وهي ما سننظر فيه في هذا المقال. هذا المقال طويل بسبب طبيعة البيانات نفسها، لكننا كتبناه بحيث لا تجب قراءته كله من أجل إدراك الفهم الكامل للبياناتوتستطيع البدء فيه من البداية التي نمر فيها على الأنواع البسيطة للبيانات. البيانات Data من المعلوم بالمشاهدة في عالم البرمجة أن البيانات هي أكثر المصطلحات المستخدمة حاليًا، رغم قلة من يفهمها على حقيقتها، وما يهمنا في هذه السلسلة في موضوع تعريف البيانات نفسها؛ هو أن تعلم أنها الحقائق أو الأرقام التي يمكن استخلاص النتائج والمعلومات المفيدة منها، ورغم أن هذا يكاد يكون تسطيحًا لمفهومها، إلا أنه يعطينا نقطةً نبدأ منها على الأقل، وسنرى فيما يلي إن كنا نستطيع توضيح ذلك بالنظر في كيفية استخدام البيانات في البرمجة. البيانات هي تلك المعلومات الخام التي يعدّل فيها البرنامج ويغيرها، فلا تستطيع البرامج تنفيذ أي وظيفة ولا مهمة ذات قيمة بغير تلك البيانات، وتتوقف أنواع التعديلات التي تجريها البرامج على البيانات على تلك الأنواع نفسها، فكل نوع له عدد من العمليات التي يمكن إجراؤها عليه، فقد رأينا مثلًا أننا نستطيع جمع الأعداد معًا، إذ تحدث عملية الجمع على النوع العددي من البيانات، مما يعني أن البيانات تأتي في عدة صور وأشكال، وسننظر في كل نوع من أشهر تلك الأنواع، إضافةً إلى العمليات المتاحة على ذلك النوع. المتغيرات Variables تخزَّن البيانات في ذاكرة الحواسيب، ويمكن تشبيه تلك الذاكرة بذاكرة مكاتب البريد القديمة، إذ توضع صناديق صغيرة على حوائط تلك المكاتب لتصنيف الرسائل البريدية، ويجب كتابة الوجهة التي تُرسَل إليها رسائل كل صندوق على الصندوق نفسه، إذ لا قيمة للرسائل من دون وجهة تُرسل إليها، وهنا نُسقط مثالنا على البرمجة، إذ إن المتغيرات هي العناوين التي تكتَب على الصناديق داخل ذاكرة الحاسوب. بهذا نكون قد عرفنا نوع البيانات الآن، لكننا لا نستطيع تعديلها قبل أن تكون لنا صلاحية الوصول إليها، وهذه هي وظيفة المتغيرات، فنحن ننشئ نسخًا من أنواع البيانات ونسندها إلى المتغيرات، والنسخة instance هي مجرد جزء من البيانات وليكن 3 أو 5 مثلًا، وهي نُسخ لأعداد؛ أما المتغير فهو إشارة إلى منطقة محددة في ذاكرة الحاسوب تحتوي على البيانات. قد تتطلب بعض لغات البرمجة أن يُصرَّح عن المتغير declared ليطابق نوع البيانات التي يشير إليها، فإذا حاولنا إسناد نوع خاطئ من البيانات إليه، فسنحصل على خطأ، ويفضل بعض المبرمجين ذلك النوع المسمى بالكتابة الثابتة static typing، لأنه يحول دون ارتكاب الزلات البرمجية bugs الدقيقة التي يصعب اكتشافها، على أن جميع اللغات التي نستخدمها هنا مرنة إلى الحد الذي نستطيع معه إسناد أي نوع من البيانات إلى المتغيرات، وهو ما يُعرف بالكتابة الديناميكية dynamic typing. تتبع أسماء المتغيرات قواعدًا محددةً تختلف باختلاف لغة البرمجة التي تستخدمها، فلكل لغة قواعدها للأحرف التي يُسمح أو لا يُسمح بها، كما تشترط بعض اللغات مثل جافاسكربت وبايثون أن ينتبه المبرمج إلى حالة الأحرف التي يكتبها، بحيث تختلف python عن Python مثلًا، كما توجد لغات أخرى مثل VBScript لا تهتم لهذا، لكن يفضل أن يُتبَّع أسلوب ثابت في تسمية المتغيرات لتجنب الأخطاء الشائعة عند الانتقال من لغة لأخرى، ومن الأساليب المستخدمة في تسمية المتغيرات هو بدء اسم المتغير بحرف صغير، ثم استخدام حرف كبير لبداية كل كلمة تالية في اسمه، كما يلي: aVeryLongVariableNameWithCapitalisedStyle لن نتحدث هنا عن القواعد التي تحكم المحارف المسموح بها في لغات البرمجة التي سنستخدمها، لكن إذا كنت تستخدم الأسلوب الذي في المثال؛ فستجنب نفسك كثيرًا من المشاكل في هذا الشأن. ومن الأساليب المستخدمة في تسمية المتغيرات أيضًا فصل الكلمات في اسم المتغير بشرطة سفلية، كما في another_variable_name؛ أما في لغة بايثون فيأخذ المتغير نوع البيانات المسندة إليه، ويحتفظ بهذا النوع ويحذرك إذا حاولت خلط البيانات بطرق تختلف عنه، كأن تحاول إضافة سلسلة نصية إلى عدد مثلًا، وهي نفس حالة المثال الذي عرض لنا رسالة الخطأ في المقال السابق، ويمكن تغيير نوع البيانات التي يشير إليها المتغير عن طريق إعادة الإسناد إلى المتغير reassigning. >>> q = 7 # عددًا الآن q صار >>> print( q ) 7 >>> q = "Seven" # ليكون سلسلة نصية q أعد إسناد >>> print( q ) Seven لاحظ أن المتغير q ضُبط ليشير إلى العدد 7 ابتداءً، وقد حافظ على تلك القيمة إلى أن جعلناه يشير إلى سلسلة المحارف "Seven"، وعلى هذا نستنتج أن متغيرات بايثون تحافظ على النوع الذي تشير إليه، لكننا نستطيع تغيير ذلك عن طريق إعادة الإسناد إلى المتغير، وتلك هي الكتابة الديناميكية التي ذكرناها قبل قليل. ويمكن التحقق من نوع المتغير باستخدام الدالة type()‎ كما يلي: >>> print( type(q) ) <class 'str'> تُفقد البيانات الأصلية عند إعادة الإسناد، وتمسحها بايثون من الذاكرة ما لم يكن ثمة متغير آخر يشير إليها، وهذا المحو يُعرف باسم جمع المهملات Garbage Collection، والذي يمكن تشبيهه بموظف غرفة البريد الذي يأتي مرةً واحدةً كل فترة ويزيل أي طرود في صناديق البريد لا توجد عليها ملصقات، فإذا لم يستطع العثور على مالك لها أو لم يوجد عنوان على الطرد؛ فسيرميها في المهملات. لننظر الآن في بعض الأمثلة على أنواع البيانات. متغيرات جافاسكربت و VBSCript تختلف جافاسكربت عن أختها VBScript قليلًا في طريقة استخدام المتغيرات، فكلاهما ترى وجوب التصريح عن المتغير قبل استخدامه، وهي خاصية تشترك فيها اللغات المصرَّفة compiled مع اللغات ذات الكتابة الثابتة statically typed، بل حتى في اللغات الديناميكية مثل تلك التي نستخدمها، حيث نستفيد من التصريح عن المتغيرات في حالة حدوث خطأ إملائي عند استخدام متغير، إذ سسيدرك المترجم أن متغيرًا غير معروف قد استُخدم، وسيرفع خطأً؛ أما عيب هذا الأسلوب فهو الحاجة إلى مزيد من الكتابة من قِبل المبرمج. VBScript يصرَّح عن المتغير في لغة VBScript باستخدام التعليمة Dim، وهي اختصار لكلمة Dimension أو بُعد، وهي إشارة إلى الجذور الأولى للغة VBScript في لغة BASIC ولغات التجميع من قبلها، حيث كان يجب علينا إخبار المجمِّع بكمية الذاكرة التي سيستخدمها المتغير -أو أبعاده-، وظل الاختصار موجودًا حتى الآن. ويبدو التصريح عن المتغير في VBScript كما يلي: Dim aVariable نستطيع بمجرد التصريح عن المتغير أن نعين قيمًا له كما فعلنا في بايثون، ويمكن التصريح عن عدة متغيرات في تعليمة Dim واحدة بسردها جميعًا مع فصلها بفواصل إنجليزية "," كما يلي: Dim aVariable, another, aThird ويبدو الإسناد بعدها كما يلي: aVariable = 42 another = "هذه جملة قصيرة." aThird = 3.14159 قد نرى كلمةً مفتاحيةً أخرى هي Let، وهي من جذور BASIC أيضًا، لكننا لن نراها إلا نادرًا لقلة الحاجة إليها، وهي تُستخدم كما يلي: Let aVariable = 22 ولن نستخدمها على أي حال في هذه السلسلة. جافاسكربت نستطيع التصريح عن المتغيرات مسبقًا في جافاسكربت باستخدام الكلمة var، كما يمكن التصريح عن عدة متغيرات كما في VBScript في تعليمة var واحدة: var aVariable, another, aThird; كما تسمح جافاسكربت بتهيئة -أو تعريف- المتغيرات مثل جزء من تعليمة var، كما يلي: var aVariable = 42; var another = "A short phrase", aThird = 3.14159; يوفر هذا قليلًا من وقت الكتابة، لكنه لا يختلف عن أسلوب VBScript مع المتغيرات والمكون من خطوتين. يمكن التصريح عن متغيرات جافاسكربت وتهيئتها من غير استخدام var، بنفس الطريقة المتبعة في بايثون: aVariable = 42; غير أن المتعصبين لجافاسكربت يرون أن استخدام var أفضل، لذا سيكون هذا أسلوبنا في هذه السلسلة أيضًا. نأمل أن تكون تلك النبذة المختصر عن متغيرات جافاسكربت وVBScript قد أوضحت الفرق بين التصريح عن المتغيرات وبين تعريفها، أما متغيرات بايثون، فتعريفها يعني ضمنيًا التصريح عنها كذلك. أنواع الكتابة: صارمة أم متغيرة أم ثابتة لقد أشرنا إلى الكتابة الثابتة والديناميكية فيما سبق من المقال، وتوجد أنواع أخرى من الكتابة مثل الكتابة الصارمة strict -أو القوية أحيانًا أخرى-، والكتابة الفضفاضة loose أو الضعيفة، وسنحاول هنا أن نوضح الاختلافات بينها. تُنشأ المتغيرات مثل أنواع بيانات مرنة أو ثابتة، ويقال أن النوع الثابت مكتوب كتابةً ثابتةً أي أنه لا يتغير أبدًا-، أما المتغير الذي قد يشير إلى عدة أنواع من البيانات فيُكتب ديناميكيًا، لذا تشير البيانات الثابتة والديناميكية إلى أنواع البيانات التي يمكن تخزينها داخل المتغير. ويمكن دمج المتغيرات في العمليات المختلفة عند البرمجة، حيث ستتطلب كل عملية احتواء متغيراتها على أنواع بعينها، مثل الأعداد أو المحارف، فإذا رفضت اللغة السماح لعملية ما باستخدام أنواع خاطئة؛ فسيقال أن هذا النوع من الكتابة كتابةٌ صارمة. تحاول بعض اللغات إجبار القيم على الأنواع الصالحة المسموح بها، بينما تسمح لغات أخرى بأي نوع ثم تفشل العملية وتخرِج خطأً، وتلك هي الكتابة الفضفاضة، وعلى هذا تشير كل من الكتابة الصارمة والفضفاضة إلى أسلوب التحقق من أنواع المتغيرات المستخدَمة في العمليات، وبالنسبة للغات التي سنستخدمها في هذه السلسلة، فجميعها تستخدم كتابةً ديناميكيةً قويةً. الأنواع الأساسية للبيانات تسمى أنواع البيانات الأساسية primitive data types بهذا الاسم لأنها أبسط أنواع البيانات التي يمكن معالجتها والتعامل معها، أما الأنواع الأعقد من البيانات فما هي في الواقع إلا تجميعات من الأنواع الأساسية، فهي اللبنات الأساسية التي تكوَّن منها جميع الأنواع الأخرى، وبناءً عليه تكون هي أساس الحوسبة كلها، وتشمل الأنواع الأساسية الأرقام والأحرف ونوعًا آخر اسمه النوع البولياني boolean type. سلاسل المحارف Strings لقد رأينا هذه السلاسل من قبل، وهي أي سلسلة من المحارف التي يمكن طباعتها على الشاشة، وقد توجد فيها محارف تحكم لا تُطبع ولا تُرى على الشاشة. تُعرض تلك السلاسل في صورة أحرف، لكن الحاسوب يخزنها في صورة تسلسلات من الأرقام، ويطلق على ربط تلك الأرقام بالمحارف اسم الترميز encoding، وتوجد العديد من الترميزات المختلفة، رغم أن المنتشر حاليًا منها هو واحد من ثلاثة ترميزات معرَّفة بواسطة معيار الترميز الموحد Unicode Standard، وسننظر في الترميز الموحد أو اليونيكود في مقال لاحق، لكن يجب أن تعلم أن غرضه توفير مخطط ترميز يمثل أي حرف من أي أبجدية في أي مكان في العالم، إضافةً إلى المحارف الخاصة التي تحتاجها الحواسيب مثل الأقواس والتحكم والتهريب وغيرها. لن نعير هذا الترميز كثير الانتباه أثناء العمل، لكن قد نحتاج إلى التحويل بين الرموز الرقمية والأحرف، كما في مشكلة الاقتباسات في مثال VBScript من المقال السابق. ويمكن تمثيل السلاسل النصية في بايثون بعدة طرق: باستخدام علامات اقتباس مفردة: 'هذه سلسلة نصية' أو باستخدام علامات اقتباس مزدوجة: "وهذه سلسلة نصية تشبهها" أو بعلامات اقتباس ثلاثية مزدوجة: """ أما هذه فسلسلة نصية أطول ونستطيع إطالتها وتقسيمها على ما نشاء من الأسطر وستحافظ بايثون على هذه الأسطر """ يُستخدم النوع الأخير في إنشاء توثيقات لدوال بايثون التي ننشئها بأنفسنا كما سنرى في مقال تال، ويمكن استخدام علامات اقتباس ثلاثية مفردة، لكننا لا ننصح بهذا، إذ قد يصعب تحديد ما إذا كانت العلامات ثلاثيةً مفردةً أم علامةً مزدوجةً داخل أخرى مفردة. نستطيع الوصول إلى المحارف المنفردة في السلسلة النصية بمعاملتها مثل مصفوفة من المحارف -انظر المصفوفات أدناه-، كما توجد بعض العمليات التي توفرها لغة البرمجة لتساعدنا في التعامل مع السلاسل، مثل العثور على سلسلة فرعية أو ربط سلسلتين معًا، ونسخ واحدة إلى أخرى، وهكذا. تجدر الإشارة إلى أن بعض اللغات فيها نوع منفصل للمحارف، أي لحرف واحد فقط، وفي تلك الحالة تكون السلاسل مجرد مجموعات من قيم تلك الأحرف، لكن بايثون -على نقيض هذا-ـ إذ تستخدم سلسلةً طولها وحدة واحدة لتخزين الحرف الواحد، ولا يلزم وجود صياغة خاصة. العوامل النصية String Operators توجد عدة عمليات يمكن إجراؤها على السلاسل النصية، وبعضها مضمّن مسبقًا في بايثون، لكن يمكن توفير عمليات إضافية باستخدام الوحدات المستورَدة كما فعلنا مع وحدة sys في المقال السابق حول التسلسلات البسيطة. 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; } العامل الوصف S1 + S2 ضم كل من S1 و S2 S1 * N تكرار قدره N من S1 يمكننا أن نرى ذلك في الأمثلة التالية: >>> print( 'Again and ' + 'again' ) # ضم السلسلة Again and again >>> print( 'Repeat ' * 3 ) # تكرار السلسلة Repeat Repeat Repeat >>> print( 'Again ' + ('and again ' * 3) ) # '*' جمع '+' و Again and again and again and again كما يمكن إسناد سلاسل أحرف إلى متغيرات: >>> s1 = 'Again ' >>> s2 = 'and again ' >>> print( s1 + (s2 * 3) ) Again and again and again and again لاحظ أن المثالين الأخيرين أنتجا نفس الخرج. هناك الكثير من الأمور التي يمكن تنفيذها باستخدام السلاسل النصية، وسننظر فيها بتفصيل في مقالات تالية بعد أن نكتسب خبرةً جيدةً في المواضيع التي تسبقها. ربما تجب ملاحظة أن السلاسل النصية في بايثون لا يمكن تعديلها، أي أننا نستطيع إنشاء سلسلة نصية جديدة مع تغيير بعض الأحرف، لكن لا يمكن تعديل أي حرف داخل سلسلة تعديلًا مباشرًا، ويُعرف نوع البيانات ذاك الذي لا يمكن تغييره باسم النوع غير القابل للتغير immutable. متغيرات السلاسل النصية في VBScript تسمى المتغيرات في VBScript باسم المتغايرات variants، لأنها قد تحتوي على أي نوع من البيانات، وتحاول VBScript أن تحولها إلى النوع المناسب وفق الحاجة، وعليه فقد نسند عددًا إلى متغير ما، لكن إذا استخدمناه مثل سلسلة نصية فستحاول VBScript أن تحوله نيابةً عنا، وهذا يشبه -من الناحية العملية- سلوك أمر print في بايثون، غير أنه هنا يمتد إلى أي أمر VBSCript. ويمكن إعطاء VBScript لإشارة على أننا نريد قيمةً عدديةً في صورة سلسلة نصية من خلال تغليفها بعلامتي اقتباس مزدوجتين: <script type="text/vbscript"> MyString = "42" MsgBox MyString </script> يمكن دمج سلاسل VBScript معًا في عملية تسمى بالضم concatenation، باستخدام العامل &: <script type="text/vbscript"> MyString = "Hello" & "World" MsgBox MyString </script> سلاسل جافاسكربت النصية تحاط سلاسل جافاسكربت النصية بعلامات اقتباس مفردة أو مزدوجة، ويجب أن نصرّح عن المتغيرات قبل استخدامها، وذلك بكلمة var المفتاحية. في المثال التالي سنصرح عن متغيري سلاسل نصية ونعرّفهما: <script type="text/javascript"> var aString, another; aString = "Hello "; another = "World"; document.write(aString + another); </script> كما تسمح لنا جافاسكربت بإنشاء كائنات سلاسل نصية، ويمكن القول بأنها سلاسل نصية مع بعض المزايا الإضافية، والاختلاف الرئيسي بينهما هو طريقة إنشائها، والتي هي بالشكل: <script type="text/javascript"> var aStringObj, anotherObj; aStringObj = String("Hello "); anotherObj = String("World"); document.write(aStringObj + anotherObj); </script> قد تكون تلك كتابةً كثيرةً قياسًا على المثال السابق مع أنهما ينجزان العمل نفسه -وهذا صحيح نوعًا ما-، غير أن كائنات السلاسل تعطينا مزايا في بعض المواقف تجعلنا نفضل استخدامها عما سواها، كما سنرى لاحقًا. الأعداد الصحيحة Integers الأعداد الصحيحة هي الأعداد الكاملة غير الكسرية، من أكبر قيمة سالبة إلى أكبر قيمة موجبة، وهذا التحديد لمجال القيم مهم في البرمجة على عكس الرياضيات، إذ لا نعير اهتمامًا لقيمة العدد غالبًا طالما ينتمي إلى مجموعة أساسية أو المجموعة التي نعمل عليها، أو لم يكن يؤثر في الناتج؛ أما في البرمجة فهناك حدود عظمى وحدود دنيا، ويعرَف الحد الأعلى باسم MAXINT، كما يعتمد على عدد البتات bits المستخدَمة في حاسوبك لتمثيل عدد ما. يكون في أغلب الحواسيب ولغات البرمجة الحالية العدد 64-بت في المعماريات الحديثة، لذا فإن قيمة الحد الأعلى هنا MAXINT تساوي 10 مرفوعة إلى الأس 19، وتستطيع بايثون إخبارنا بقيمة maxint باستخدام وحدة sys، فإذا كانت معمارية النظام 64-بت مثلًا فستكون قيمة sys.maxsize هي 9223372036854775807، وهو رقم كبير؛ أما في VBScript فتكون تلك القيمة محدودةً بـ (‎+/- 32000‎) لأسباب تاريخية. تُعرَف الأعداد التي تحمل إشارةً موجبةً أو سالبةً بالأعداد الصحيحة ذات الإشارة signed integers، ومن الممكن استخدام أعداد صحيحة لا إشارة لها، غير أن هذا مقصور على الأعداد الموجبة، بما في ذلك الصفر، وهذا يعني أن الأعداد الصحيحة التي لا إشارة لها ضعف عدد ذوات الإشارة، بما أننا سنستغل المساحة التي كانت مخصصةً للأعداد السالبة لضمها إلى الأعداد التي لا إشارة لها، مما يعني زيادة قيمة الحد الأقصى إلى الضعف أيضًا "‎2 * MAXINT"، ونظرًا لأن حجم الأعداد الصحيحة مقيد بقيمة MAXINT، فإذا جمعنا عددين صحيحين وكان ناتج الجمع أكبر من الحد الأقصى؛ فسنحصل على خطأ، لكن قد تعيد بعض الأنظمة واللغات تلك القيمة الخاطئة كما هي مع رفع راية سرية secret flag نستطيع التحقق من وجودها إذا كنا نشك في الإجابة، وفي مثل هذه الحالة يكون التحقق عادةً هو برفع حالة خطأ، فإما أن يكون البرنامج حينها قادرًا على معالجة ذلك الخطأ فيعالجه، أو ينهي البرنامج نفسه إذا لم يستطع. تحوّل كل من جافاسكربت وVBScript العدد إلى صيغة تستطيعان التعامل معها، ولا تهتمان بالدقة هنا، إذ قد تختلف الصيغة الجديدة اختلافًا طفيفًا؛ أما لغة بايثون فتستخدم شيئًا اسمه العدد الصحيح الطويل Long Integer، وهي ميزة خاصة لها تسمح بأعداد صحيحة غير محدودة الحجم. >>> import sys >>> x = sys.maxsize * 1000000 >>> print(x) 9223372036854775807000000 >>> print (type(x)) <class 'int'> كما نرى فالنتيجة أكبر بكثير من القيمة التي تتوقعها من الحاسوب في العادة، رغم احتسابها int، أما إذا استخدمنا شيفرةً مكافئةً لهذا المثال في جافاسكربت وVBScript؛ فسيخرج لنا العدد بصيغة تختلف عن العدد الصحيح الذي نتوقعه، وسنتعلم المزيد عن هذا في قسم الأعداد الحقيقية. <script type="text/vbscript"> Dim x x = 123456700 * 34567338738999 MsgBox CStr(x) </script> العمليات الحسابية رأينا معظم العمليات الحسابية التي نحتاج إليها في المقال السابق في التسلسلات البسيطة، ولكن للتلخيص نعيدها مرةً أخرىً: مثال العامل الوصف M + N جمع M و N M - N طرح N من M M * N ضرب M في N M / N قسمة M على N، سيكون الناتج عددًا حقيقيًا كما سنبين لاحقًا M // N القسمة الصحيحة لـ M على N، ستكون النتيجة عددًا صحيحًا M % N باقي القسمة: أوجد ما تبقى من قسمة M على N M**N الأس: M مرفوعًا إلى الأس N بما أننا لم نرى المتغير الأخير من قبل، فلننظر في مثال على إنشاء بعض متغيرات الأعداد الصحيحة ونستخدم عامل الأس: >>> i1 = 2 # i1 أنشئ عددًا صحيحًا وأسنده إلى >>> i2 = 4 >>> i3 = i1**i2 # i3 أسند نتيجة 2 إلى أس 4 إلى >>> print( i3 ) 16 >>> print( 2**4 ) # أكد النتيجة 16 تحتوي الوحدة math في بايثون على بعض الدوال الرياضية الشائعة مثل sin و cos وغيرهما. عوامل الاختصار إحدى العمليات الشائعة في البرمجة هي زيادة قيمة المتغير التدريجية، فإذا كان لدينا المتغير x الذي قيمته 42، ونريد زيادة تلك القيمة إلى 43، فسيكون ذلك كما يلي: >>> x = 42 >>> print( x ) >>> x = x + 1 >>> print( x ) لاحظ السطر التالي: x = x + 1 لا معنىً لهذا السطر من حيث المنطق الرياضي، لكنه في البرمجة يعني أن x الجديدة تساوي قيمة x السابقة مضافًا إليها 1، فإذا كنت معتادًا على المنطق الرياضي؛ فستستغرق بعض الوقت لتعتاد على هذا النمط الجديد، لكن الفكرة هنا أن علامة = تُقرأ "ستكون" وليست "تساوي"، وعليه فإن السطر يعني أن x ستصبح قيمتها الجديدة هي x+1، وهذا النوع من العمليات شائع جدًا في الممارسة العملية إلى حد أن بايثون وجافاسكربت توفران عاملًا مختصرًا له من أجل توفير الكتابة على المبرمج: >>> x += 1 >>> print( x ) وهذان العاملان مساويان تمامًا لتعليمة الإسناد السابقة لكنهما أقصر، توجد عوامل اختصار لباقي العمليات الحسابية بنفس المنطق السابق، نبينها في الجدول التالي: مثال العامل الوصف M += N M = M + N M -= N M = M - N M *= N M = M * N M /= N M = M / N M //= N M = M // N M %= N M = M % N الأعداد الصحيحة في VBScript لقد قلنا إن الأعداد الصحيحة في VBScript محدودة بقيمة عظمى أقل من بايثون وجافاسكربت، وهي تقابل 16-بت على خلاف 64-بت التي رأيناها من قبل، أي حوالي "‎+/- 32000"، فإذا احتجنا إلى عدد صحيح أكبر من هذا؛ فنستخدم العدد long الذي يساوي حجمه حجم العدد الصحيح من معمارية 32-بت (أي 2 مليار)، كذلك لدينا النوع byte الذي هو عدد ثماني البتات، وحجمه الأكبر يساوي 255، وسيتضح بالممارسة العملية أن النوع القياسي للعدد الصحيح كافٍ لأغلب العمليات، فإذا كانت نتيجة العملية أكبر من MAXINT؛ فستحول VBSCript النتيجة تلقائيًا إلى عدد ذي فاصلة عائمة أو حقيقي، كما سنبين لاحقًا. ورغم دعم VBScript لجميع العمليات الحسابية المعتادة، إلا أن عملية الباقي modulo تمثَّل تمثيلًا مختلفًا، باستخدام العامل MOD، وقد رأينا ذلك في فصل التسلسلات البسيطة، كذلك فإن تمثيل الأسس مختلف، إذ يُستخدم محرف الإقحام ^ بدلًا من ** في بايثون. أعداد جافاسكربت تحتوي جافاسكربت على نوع عددي في صورة كائن كما سنصفه فيما بعد، وهي تسميه Number، لكن أحيانًا قد يكون عدد جافاسكربت "ليس عددًا" أو Not a Number، ويشار إليه بـ NaN اختصارًا، وهو ما نحصل عليه أحيانًا كنتيجة لبعض العمليات المستحيلة رياضيًا، والهدف منه هو السماح لنا بالتحقق من أنواع أخطاء بعينها دون تعطيل البرنامج، كما تحوي لغة بايثون كذلك النوع NaN أيضًا. تحتوي جافاسكربت على نسخ عددية خاصة لتمثيل اللانهاية الموجبة والسالبة، وهي ميزة نادرة نسبيًا في لغات البرمجة، وقد تكون كائنات جافاسكربت العددية إما أعدادًا صحيحةً أو حقيقيةً كما سنرى لاحقًا، وتَستخدم جافاسكربت نفس العوامل التي تستخدمها بايثون، باستثناء الإجراءات الأسية التي تستخدم كائنًا خاصًا بها اسمه Math. الأعداد الحقيقية Real Numbers الأعداد الحقيقية هي نفسها الأعداد الصحيحة لكن مع إضافة الكسور، وقد تمثل أعدادًا أكبر من الحد الأقصى MAXINT لكن مع دقة أقل، وهذا يعني أننا قد نرى عددين حقيقيين على أنهما متطابقين، لكن الحاسوب يراهما غير متطابقين عندما يوازنهما معًا، لأنه ينظر في أدق التفاصيل بينهما، حيث يمكن تمثيل العدد 5.0 في الحاسوب بإحدى الصورتين 4.9999999 أو (‎5.000000…01) مثلًا، وهذه التقاربات مناسبة لأغلب الاستخدامات، لكن قد نحتاج أحيانًا إلى تلك الدقة، فتذكر هذا الأمر إذا حصلت على نتيجة غريبة لا تتوقعها عند استخدام الأعداد الحقيقية. كما تُعرَف الأعداد الحقيقية أيضًا باسم أعداد الفاصلة العائمة floating point numbers، ولها نفس العمليات الحسابية التي تطبّق على الأعداد الصحيحة، مع زيادة هنا وهي قدرتها على اقتطاع العدد ليكون قيمة صحيحة. تدعم لغات البرمجة الثلاث بايثون وجافاسكربت وVBScript الأعداد الحقيقية، وننشئها في بايثون بتخصيص عدد بعلامة عشرية فيه كما رأينا في المقال السابق للتسلسلات البسيطة؛ أما جافاسكربت وVBScript فلا يوجد تمييز واضح بين الأعداد الصحيحة والحقيقية، فما عليك إلا استخدامها وستتعامل اللغة مع ما تدخله إليها وفق المناسب. الأعداد المركبة أو التخيلية Complex numbers إذا كانت لديك خلفية رياضية فقد تتساءل أين الأعداد المركبة complex في هذا الشرح، والواقع أننا لا نحتاج إليها غالبًا في البرمجة، لكن على أي حال فبعض اللغات مثل بايثون توفر دعمًا مضمّنا فيها للأعداد المركبة، بينما توفر بعض اللغات الأخرى مكتبةً من الدوال التي تستطيع التعامل مع الأعداد المركبة، ونفس الأمر ينطبق على المصفوفات. يمثَّل العدد المركب في بايثون كما يلي: (real+imaginaryj) وبناءً عليه تبدو عملية جمع بسيطة لعدد مركب كما يلي: >>> M = (2+4j) >>> N = (7+6j) >>> print( M + N ) (9+10j) تنطبق جميع عمليات الأعداد الصحيحة على الأعداد المركبة كذلك، كما توجد وحدة cmath التي تحتوي بعض الدوال الرياضية الشائعة التي تعمل على قيم الأعداد المركبة، ولا توفِّر جافاسكربت وVBScript دعمًا للأعداد المركبة. القيم البوليانية - True و False سُمي هذا النوع من القيم بهذا الاسم نسبةً إلى عالم الرياضيات George Boole الذي كان يدرس المنطق في القرن التاسع عشر، ولا يحوي هذا النوع من البيانات إلا قيمتين فقط، هما True عند التحقق أو الصحة، و false عند عدم التحقق أو الخطأ، وتدعم بعض اللغات هذا النوع دعمًا مباشرًا، بينما تستخدم بعض اللغات الأخرى اصطلاحًا تمثل فيه قيمةً عدديةً القيمة false، وتكون تلك القيمة غالبًا هي الصفر؛ بينما تمثَّل القيمة true بالعدد (1) أو (‎-1)، وقد كان هذا سلوك بايثون حتى الإصدار 2.2، لكنها صارت تدعم القيم البوليانية مباشرةً بعد ذلك باستخدام القيمتين True و False، وهما كما ترى تبدآن بحرف كبير، فتذكر أن بايثون حساسة لحالة الأحرف. تعرَف القيم البوليانية أحيانًا باسم قيم الحقيقة لأنها تُستخدم للتحقق مما إذا كان شيء ما صحيحًا أم لا، فإذا كتبنا برنامجًا يأخذ نسخةً احتياطيةً من جميع الملفات في مجلد ما مثلًا، فنستطيع نسخ كل ملف على حدة ثم نطلب اسم الملف التالي من النظام، وإذا لم تتبق ملفات لنسخها، فسيعيد النظام سلسلةً نصيةً فارغةً، حيث سنتحقق هنا مما إذا كان الاسم هو سلسلةً فارغةً أم لا، ونخزن النتيجة مثل قيمة بوليانية تكون قيمتها True إذا كان الاسم سلسلةً فارغةً، و False إذا لم يكن كذلك، وسنرى كيفية استخدام تلك النتيجة لاحقًا في هذه السلسلة. العوامل البوليانية أو المنطقية العامل الوصف الأثر A and B الإضافة أو AND صحيح إذا كان كل من A و B صحيحًا أو متحققًا، وخطأ في الحالات الأخرى A or B الاختيار أو OR صحيح إذا كان أحدهما أو كلاهما صحيحًا، وخطأ إذا كان كل من A و B خاطئًا A == B التساوي صحيح إذا كان A يساوي B A != B عدم التساوي صحيح إذا كانت A لا تساوي B not B النفي صحيح إذا لم يكن B صحيحًا تدعم VBScript القيم البوليانية كما تفعل بايثون على الصورة True و False، أما جافاسكربت فتدعمها لكن على الصورة true و false -لاحظ حالة الأحرف الصغيرة هنا-. كما ستجد أسماءً مختلفةً لنوع البيانات البولياني أو المنطقي في كل لغة، ففي بايثون ستجدها باسم bool، أما في جافاسكربت وVBScript فاسمه Boolean، ولا تشغل بالك بهذا لأننا لن ننشئ متغيرات من هذا النوع، لكننا سنستخدم نتائجه في الاختبارات. الوقت والتاريخ يُعطى التاريخ والوقت في العادة أنواعًا خاصةً بهما في البرمجة، لكن قد يمثَّلان أحيانًا في صورة عدد كبير (عدد الثواني منذ وقت أو تاريخ قديم، مثل الوقت الذي كُتب فيه نظام التشغيل)، لكن قد يكون نوع البيانات في أحيان أخرى هو ما يُعرف بالنوع المعقد الذي سنذكره في الفصل التالي، وهو يسهل استخراج الشهر واليوم والساعة… سننظر في استخدام بايثون لوحدة time في مقال تالٍ، أما جافاسكربت وVBScript فلديهما آليات خاصة للتعامل مع الوقت، لكننا لن ندرسها. النوع الذي يعرفه المستخدم User Defined Type قد لا تكفي الأنواع التي ذكرناها سابقًا لاحتياجاتنا البرمجية حتى لو دمجناها معًا، فقد نريد جمع عدة أجزاء من البيانات معًا ثم نعاملها مثل عنصر واحد، كما في وصف العنوان مثلًا، حيث يكون على هيئة "رقم المنزل والشارع والمدينة ، ثم الرمز البريدي". تسمح أغلب اللغات بجمع مثل هذه المعلومات معًا في سجل أو هيكل بيانات أو في هيئة صنف Class، وهذا الأخير في البرمجة كائنية التوجه Object Oriented. VBScript تبدو تعريفات السجلات في VBScript كما يلي: Class Address Public HsNumber Public Street Public Town Public ZipCode End Class تعني الكلمة المفتاحية Public أن البرنامج يستطيع الوصول إلى البيانات، وعليه توجد كلمة Private التي يمكن استخدامها لجعل البيانات خاصةً ومقصورةً على أجزاء بعينها من البرنامج، كما سنرى لاحقا. بايثون يختلف الأمر قليلًا في بايثون: >>> class Address: ... def __init__(self, Hs, St, Town, Zip): ... self.HsNumber = Hs ... self.Street = St ... self.Town = Town ... self.ZipCode = Zip ... سنترك شرح معنى كل من self و def __init__(...)‎ إلى حين الحديث عن البرمجة كائنية التوجه، أما الآن فنريد ملاحظة أن هناك شرطتين سفليتين حول init، وهذا أسلوب متبع في بايثون. لاحظ كذلك ضرورة استخدام المسافات الموضحة أعلاه، ذلك أن بايثون دقيقة في تحديد المسافات. قد يواجه البعض مشاكل أثناء كتابة ما يتشابه مع هذا المثال في محث بايثون، وستجد في نهاية هذا المقال شرحًا مختصرًا لهذه المشكلة، ثم سنشرحها بالتفصيل في جزء تالٍ من هذه السلسلة، فإذا أردت كتابة المثال أعلاه في محث بايثون؛ فتأكد من نسخ المسافات البادئة لكل سطر. والأمر الذي نريد التوكيد عليه هنا هو أننا جمعنا عدة أجزاء من بيانات مرتبطة ببعضها بعضًا ثم وضعناها في هيكل واحد هو Address، كما فعلنا في VBScript قبل قليل. جافاسكربت توفر جافاسكربت اسمًا غريبًا لهيكل بيانات العنوان في حالتنا، وهو function، إذ ترتبط الدوال عادةً بالعمليات وليس تجميعات البيانات، غير أن دوال جافاسكربت تستطيع التعامل مع كليهما، فإذا أردنا إنشاء كائن العنوان في جافاسكربت؛ فسنكتب الآتي: function Address(Hs,St,Town,Zip) { this.HsNum = Hs; this.Street = St; this.Town = Town; this.ZipCode = Zip; } نريد مرةً أخرى تجاهل الصياغة واستخدام الكلمة this هنا، والنظر إلى النتيجة التي ستكون مجموعةً من عناصر البيانات التي نستطيع القول بأنها تصف عنوانًا، ويمكننا معاملته على أنه وحدة واحدة. والآن، كيف نصل إلى تلك البيانات بعد إنشاء هياكلها؟ الوصول إلى الأنواع التي يعرفها المستخدم من الممكن إسناد نوع بيانات معقد إلى متغير، لكن لا يمكن الوصول إلى الحقول المنفردة دون استخدام بعض آليات الوصول الخاصة التي تحددها اللغة، وتكون نقطة . في الغالب. VBScript إذا استخدمنا حالة صنف العنوان الذي عرّفناه أعلاه، فسنكتب ما يلي: Dim Addr Set Addr = New Address Addr.HsNumber = 7 Addr.Street = "High St" Addr.Town = "Anytown" Addr.ZipCode = "123 456" MsgBox Addr.HsNumber & " " & Addr.Street & " " & Addr.Town نحدد هنا بُعد متغير جديد هو Addr باستخدام Dim، ثم نستخدم الكلمتين Set و New لإنشاء نسخة جديدة من صنف Address، ثم نسند القيم إلى حقول نسخة العنوان الجديدة، ثم نطبع العنوان في صندوق رسالة. بايثون على فرض أننا كتبنا تعريف الصنف أعلاه: >>> Addr = Address(7,"High St","Anytown","123 456") >>> print( Addr.HsNumber, Addr.Street, Addr.Town ) 7 High St Anytown ينشئ هذا نسخةً من نوع Address ويسند إليه المتغير Addr، ونستطيع تمرير قيم الحقل إلى الكائن الجديد عند إنشائه في بايثون، ثم نطبع حقول HsNumber و Street للنسخة المنشأة حديثًا باستخدام عامل النقطة .. كما يمكن إنشاء نسخ عنوان متعددة لكل منها قيمه المستقلة لأرقام البيوت والشوارع وغيرها. جرب هذا بنفسك لتتدرب عليه، فهل تستطيع معرفة كيفية استخدام هذا في مثال دفتر العناوين؟ جافاسكربت تشبه آلية جافاسكربت هنا ما سبق ذكره لكن مع بعض التعديل، غير أن الآلية الأساسية بسيطة ومباشرة: var addr = new Address(7, "High St", "Anytown", "123 456"); document.write(addr.HsNum + " " + addr.Street + " " + addr.Town); إحدى الآليات التي يمكن استخدامها في جافاسكربت أيضًا هي معاملة الكائن مثل قاموس واستخدام اسم الحقل كمفتاح: document.write( addr['HsNum'] + " " + addr['Street'] + " " + addr['Town']); وما لم نُعط اسم الحقل في هيئة سلسلة نصية كما في حالة قراءة ملف أو إدخال مستخدم للبرنامج، فلا ننصح باستخدام هذا النموذج. العوامل التي يعرفها المستخدم يمكن للأنواع التي يعرِّفها المستخدم أن تكون لها عمليات معرفة لها كذلك، وذلك أساس ما يعرف بالبرمجة كائنية التوجه. الكائن هو عبارة عن تجميعة من عناصر البيانات والعمليات المرتبطة بتلك البيانات، وهي مغلفة جميعًا في وحدة واحدة، حيث تستخدم بايثون الكائنات على نطاق واسع في مكتبتها القياسية للوحدات، كما تسمح لنا كوننا مبرمجين بإنشاء أنواع الكائنات الخاصة بنا. يحصل الوصول إلى عمليات الكائنات بنفس الطريقة التي يمكن الوصول بها إلى أعضاء البيانات لنوع معرَّف من قِبل المستخدم، أي من خلال عامل النقطة؛ أما غير هذا فتكون أشبه بالدوال، وتسمى تلك الدوال الخاصة بالتوابع methods، وقد رأيناها في حالة العملية append()‎ في القوائم، أينما تذكر أنه من أجل استخدامها يجب أن نربط استدعاء الدالة باسم المتغير. >>> listObject = [] # قائمة فارغة >>> listObject.append(42) # استدعاء تابع لكائن القائمة >>> print( listObject ) [42] يجب استيراد الوحدة عند توفير نوع كائن - يُسمى صنفًا- في وحدة بايثون كما فعلنا في sys من قبل، ثم كتابة اسم الوحدة قبل اسم نوع الكائن عند إنشاء نسخة يمكن تخزينها في متغير مع استخدام الأقواس طبعًا، بعد ذلك نستطيع استخدام المتغير دون استخدام اسم الوحدة، ولتوضيح هذا لنَعُد إلى وحدة array التي ذكرناها من قبل، فهي توفر الصنف array، ولنستورد هذه الوحدة ثم ننشئ نسخةً من array، ونسند اسم myArray إليها، ثم نستخدم myArray فقط للوصول إلى عملياتها وبياناتها كما يلي: >>> import array >>> myArray = array.array('d') # array of reals(d), use module name >>> myArray.append(42) # use array operation >>> print( myArray[0]] ) # access array content 42.0 >>> print( myArray.typecode ) # access array attribute 'd' نستورد الوحدة array في السطر الأول إلى البرنامج، ثم نستخدم تلك الوحدة في السطر الثاني لإنشاء نسخة من صنف array باستدعائها مثل دالة، ونحتاج هنا إلى توفير سلسلة الرموز النوعية typecode string التي توضح نوع البيانات التي يجب تخزينها. تذكر أن المصفوفات الخالصة تخزن نوعًا واحدًا من البيانات، حيث سنصل في السطر الثالث إلى إحدى عمليات صنف المصفوفة، وهي append()‎ التي تعامل الكائن myArray كأنه وحدة وكأن العملية داخل الوحدة، ثم نستخدم الفهرسة لجلب البيانات التي أضفناها، نستطيع في الأخير الوصول إلى بعض البيانات الداخلية، إذ يخزن typecode النوع الذي مررناه عند إنشاء المصفوفة، من داخل كائن myArray صياغةً تشبه صياغة الوحدة. لا يوجد فرق كبير بين استخدام الكائنات التي توفرها الوحدات، وبين الدوال الموجودة في الوحدات، باستثناء الحاجة إلى إنشاء نسخة instance، ويمكن النظر إلى اسم الكائن على أنه عنوان يحفظ الدوال والمتغيرات المرتبطة به مجموعةً معًا. إحدى الطرق الأخرى التي ننظر بها للأمر هي أن الكائنات تمثل أشياء من العالم الحقيقي ونحن نستخدمها أو نفعل بها أمورًا تنفعنا، وهذا المنظور على بساطته إلا أنه الفكرة التي نشأت بسببها الكائنات في البرمجة، فنحن نكتب -برمجيًا- محاكاةً لمواقف في العالم الحقيقي. تستطيع كل من جافاسكربت وVBScript أن تتعامل مع الكائنات، وقد كان هذا ما استخدمناه في كل أمثلة العناوين أعلاه، فقد عرفّنا صنفًا ثم أنشأنا نسخةً أسندناها إلى متغير كي نستطيع الوصول إلى خصائص تلك النسخة، ولهذا ارجع إن شئت إلى ما سبق من الشرح عن الأصناف والكائنات، وانظر كيف توفر الأصناف آليات لإنشاء أنواع جديدة من البيانات في برامجنا من خلال الربط بين بيانات النوع الجديد وعملياته معًا. العوامل الخاصة ببايثون إنّ الهدف من هذه السلسلة هو تعليم البرمجة للمبتدئين، ورغم أننا نستخدم بايثون في الشرح؛ إلا أنك تستطيع قراءة لغة برمجة أخرى واستخدامها بدلًا منها، بل إن هذا هو عين ما نتوقعه منك، إذ أنه لا توجد لغة برمجة بما فيها بايثون، تستطيع تنفيذ جميع المهام وحل كل المشاكل التي تواجهك برمجيًا، وبسبب ذات الهدف لن نشرح جميع الخصائص الموجودة في بايثون، بل سنركز على تلك التي يمكن إيجادها في لغات البرمجة الأخرى كذلك، ولهذا سنهمل شرح بعض الخصائص بالغة القوة في بايثون، والتي تميزها عما سواها، مثل العوامل الخاصة special operators، ذلك أن أغلب لغات البرمجة تدعم عمليات بعينها لا تدعمها لغات أخرى، وتلك العوامل الفريدة هي التي تسبب ظهور لغات برمجة جديدة، وهي من العوامل المهمة في تحديد شهرة تلك اللغات من حيث الاستخدام فيما بعد. حيث تدعم بايثون مثلًا عمليات غير شائعة نسبيًا في لغات البرمجة الأخرى، مثل تشريح القائمة spam[X:Y]‎ (إلى شرائح) list slicing من أجل استخراج شريحة من وسط القائمة أو السلسلة النصية أو الصف tuple، كما تدعم إسناد الصف Tuple الذي يسمح لنا بإسناد عدة قيم متغيرات مرةً واحدةً: X, Y = (12, 34)‎. كما تسهل تنفيذ العمليات على كل عضو من أعضاء تجميعة ما باستخدام الدالة map()‎ التي سنشرحها في مقال لاحق من هذه السلسلة، إضافةً إلى الكثير من الخصائص التي جعلت بايثون تشتهر بأنها تأتي "مع البطارية" إشارةً إلى كثرة الملحقات والمزايا التي تأتي معها. ارجع إلى توثيق بايثون للمزيد عن تلك المزايا. تجدر أخيرًا الإشارة إلى أنه رغم قولنا بأن تلك العوامل خاصة ببايثون؛ إلا أن هذا لا يعني أنها لا توجد في لغات برمجة أخرى، بل أنها لن توجد مجتمعةً في كل لغة، فالعوامل التي نشرحها في هذه السلسلة تكون متاحةً بشكل أو بآخر في أغلب لغات البرمجة الحديثة. يلخص هذا نظرتنا على المواد الخام للبرمجة، وسننتقل الآن إلى موضوع آخر نرى فيه كيف نستغل تلك المواد الخام في البرمجة الحقيقية. شرح لمثال العناوين رغم قولنا أننا سنشرح تفاصيل هذا المثال لاحقًا، إلا أننا رأينا صعوبة تنفيذ مثال بايثون من بعض القراء، ولهذا سيشرح هذا القسم شيفرة بايثون سطرًا سطرًا. ستبدو شيفرة المثال كاملة كما يلي: >>> class Address: ... def __init__(self, Hs, St, Town, Zip): ... self.HsNumber = Hs ... self.Street = St ... self.Town = Town ... self.Zip_Code = Zip ... >>> addr = Address(7,"High St","Anytown","123 456") >>> print( addr.HsNumber, addr.Street ) وسنشرح الآن سطرًا سطرًا: >>> class Address: تخبر التعليمة class بايثون أننا على وشك تعريف نوع جديد اسمه Address في هذه الحالة، وتشير النقطتان الرأسيتان إلى أن أي أسطر مزاحة تالية ستكون جزءًا من تعريف الصنف، وينتهي التعريف عند أول سطر غير مزاح، فإذا كنت تستخدم IDLE؛ فسترى أن المحرر قد أزاح السطر التالي تلقائيًا، أما إذا كنت تعمل من محث بايثون في سطر أوامر DOS مثلًا، فسيكون عليك إزاحة الأسطر كما هو موضح أعلاه، ولا تهتم بايثون بمقدار الإزاحة طالما أنها نفسها لكل سطر. أما السطر الثاني: ... def __init__(self, Hs, St, Town, Zip): يطلق على العنصر الأول في الصنف الخاص بنا "تعريف التابع method definition"، ويجب أن يكون حول الاسم شرطتان سفليتان على كل ناحية منه، وهي طريقة بايثون للأسماء التي لها أهمية خاصة، ويسمى ذلك التابع __init__، وهو عملية خاصة تنفذها بايثون حين ننشئ نسخةً من الصنف الجديد الخاص بنا كما سنرى بعد قليل؛ أما النقطتان الرأسيتان فتخبران بايثون أن مجموعة الأسطر المزاحة التالية ستكون هي التعريف الحقيقي للتابع. والسطر الثالث: ... self.HsNumber = Hs يُسنِد هذا السطر -والأسطر الثلاثة التالية- قيمًا إلى الحقول الداخلية للكائن الخاص بنا، وهذه الأسطر مزاحة من تعليمة def لتخبر بايثون أنها تشكل التعريف الحقيقي لعملية __init__، أما السطر الفارغ، فيخبر مفسر بايثون بأن تعريف الصنف قد انتهى، لنعود مرةً أخرى إلى محث بايثون المعتاد ‎>>>‎. >>> addr = Address(7,"High St","Anytown","123 456") ينشئ هذا نسخةً جديدةً من النوع Address، وتستخدم بايثون عملية __init__ المعرَّفة أعلاه لإسناد القيم التي أعطيناها إلى الحقول الداخلية، وتسنَد النسخة إلى متغير addr كما تسنَد أي نسخة لأي نوع بيانات آخر. >>> print( addr.HsNumber, addr.Street ) نطبع الآن قيم حقلين من الحقول الداخلية مستخدمين عامل النقطة للوصول إليهما. خاتمة يُعَد هذا المقال دسمًا وغنيًا بالمعلومات النظرية التي تؤسس لتعلم البرمجة على أسس سليمة، وسنعود إلى ما فيه مرات كثيرة أثناء الشرح، بما أن البيانات وأنواعها هي اللبنات التي تُصنع منها البرامج، لكن نريد التذكير هنا بأن بايثون تسمح لنا بإنشاء أنواع البيانات التي نريدها بأنفسنا ونستخدمها مثل أي نوع مضمّن فيها، وأن المتغيرات تشير إلى بيانات، وقد نحتاج إلى التصريح عنها قبل تعريفها، وهي تأتي في أنواع شتى، ويتوقف نجاح العملية التي نجريها على نوع البيانات الذي نستخدمه. ومن أنواع البيانات البسيطة سلاسل المحارف والأعداد والقيم البوليانية أو قيم "الحقيقة"، كما تشمل أنوع البيانات المعقدة التجميعات والملفات والبيانات وأنواع البيانات المعرّفة من قِبل المستخدم. توجد أيضًا العديد من العوامل في كل لغة برمجة، وتيُعَد تعلّم تلك العوامل جزءًا من التعود على أنواع بياناتها وعلى العمليات المتاحة لتلك الأنواع، وقد يكون العامل الواحد متاحًا لعدة أنواع، غير أن النتيجة قد لا تكون هي نفسها في كل مرة، بل ربما تختلف اختلافًا كبيرًا. سنتابع مع المقال الموالي مع ذكر نوع أخر مهم من البيانات، وهو التجميعات Collections. ترجمة -بتصرف- للفصل الخامس: The Raw Materials من كتاب Learning To Program لصاحبه Alan Gauld. اقرأ أيضًا المقال التالي: مدخل إلى البيانات وأنواعها: التجميعات Collections المقال السابق: التسلسلات البسيطة في البرمجة مدخل إلى البيانات الوصفية (microdata) في HTML5 هياكل البيانات: الكائنات والمصفوفات في جافاسكريبت
  9. إذا أردنا وصف البرنامج المثالي، فسيكون ذلك الذي له بنية واضحة مثل الكريستال، ويسهل وصف طريقة عمله، كما يملك كل جزء فيه دورًا محدَّدًا ومعروفًا. لكن هذا لا يكون في الواقع، بل تكون لدينا برامج تنمو نموًا عضويًا، بحيث تضاف إليها المزايا كلما دعت الحاجة إلى ذلك؛ أما الهيكلة البنائية لها والحفاظ على تلك الهيكلية فهي أمر آخر، إذ لا تُرى ثمرتها إلا في المستقبل حين يأتي شخص آخر ليعمل على البرنامج، وعلى ذلك فمن المغري للمبرمج إهمالها الآن لتصبح أجزاء البرنامج متشعبةً ومتداخلةً إلى حد فظيع. يتسبب هذا في مشكلتين حقيقيتَين، أولاهما أنّ فهم مثل هذا النظام صعب جدًا، إذ سيكون من العسير النظر لأيّ جزء فيه نظرة مستقِلة إذا كان كل شيء فيه يتصل بشيء آخر، حيث سنُجبَر على دراسة البرنامج كله من أجل فهم جزء واحد فقط؛ أما الثانية فهي إذا أردنا استخدام أيّ وظيفة من مثل هذا البرنامج في حالة أخرى، فستكون إعادة كتابتها من الصفر أسهل من استخراجها مستقِلةً من شيفرتها لتعمل في مكان آخر. يستخدم مصطلح "كرة الوحل" لمثل تلك البرامج التي لا هيكل لها، والتي يلتصق كل شيء فيها ببعضه، فإذا حاولت استخراج جزء، فستنهار الكرة كلها، ولا ينالك إلا تلطيخ يدك. الوحدات Modules تُعَدّ الوحدات محاولةً لتفادي مثل تلك المشاكل التي وصفناها، فالوحدة هي جزء من برنامج ما يحدِّد بوضوح ما هي الأجزاء الأخرى التي يعتمد عليها، وما هي الوظيفة -أي الواجهة الخاصة به- التي سيوفرها للوحدات الأخرى لتستخدمها. تشترك واجهات الوحدات مع واجهات الكائنات في أمور كثيرة كما رأينا في مقال الحياة السرية للكائنات في جافاسكريبت، إذ تجعل جزءًا من الوحدة متوفرًا للعالم الخارجي وتحافظ على خصوصية الباقي، كما يصبح النظام أشبه بقِطع الليجو LEGO في تقييد الطرق التي تتفاعل الوحدات بها مع بعضها البعض، فتتفاعل الأجزاء المختلفة من خلال وصلات connectors معرَّفة جيدًا، على عكس الوحل الذي يختلط فيه كل شيء. تسمى العلاقات التي بين الوحدات بالاعتماديات dependencies، فإذا احتاجت وحدة إلى جزء من وحدة أخرى، فسيقال أنها تعتمد على تلك الوحدة، وحين توصف تلك الحقيقة بوضوح داخل الوحدة نفسها، فيمكن استخدامها لمعرفة الوحدات الأخرى التي يجب أن تكون موجودةً من أجل استخدام وحدة ما، ولتحميل الاعتماديات تلقائيًا، كما ستحتاج كل واحدة منها إلى مجال خاص private scope إذا أردنا فصل الوحدات بهذه الطريقة. لا يكفي وضع شيفرة جافاسكربت في ملفات مختلفة لتلبية تلك المتطلبات، فلا زالت الملفات تتشارك فضاء الاسم العام global namespace نفسه، كما قد تتداخل عن قصد أو غير قصد مع رابطات بعضها bindings، وهكذا تظل هيكلية الاعتماديات غير واضحة، وسنرى بعد قليل كيف نعالج ذلك. قد يكون التفكير في تصميم وحدة ذات هيكل مناسب لبرنامج ما صعبًا بما أننا في مرحلة البحث عن المشكلة واستكشاف حلول لها، وذلك لنرى أيها يعمل، كما لا نريد شغل بالنا بتنظيم أحد الحلول إلا حين يثبت نجاحه. الحزم نستطيع استخدام نفس الجزء في برامج أخرى مختلفة، وهي إحدى المزايا التي نحصل عليها عند بناء برنامج من أجزاء منفصلة تستطيع العمل مستقِلة عن بعضها البعض. لكن كيف نُعِدّ هذا؟ لنقل أننا نريد استخدام الدالة parseINI من المقال السابق في برنامج آخر، فإذا كان ما تعتمد عليه الدالة واضحًا -لا تعتمد الدالة على شيء في هذه الحالة-، فلن نفعل أكثر من نسخ شيفرتها إلى المشروع الجديد واستخدامها؛ أما إذا رأينا خطأً في تلك الشيفرة، فسنصلحه في ذلك المشروع الجديد وننسى إصلاحه في البرنامج الأول الذي أخذنا منه الشيفرة، وبهذا نهدر الوقت والجهد في نسخ الشيفرات من هنا إلى هناك والحفاظ عليها محدَّثة. وهنا يأتي دور الحِزم packages، فهي قطعة من شيفرة يمكن نشرها وتوزيعها -أي نسخها وتثبيتها-، وقد تحتوي على وحدة واحدة أو أكثر، كما فيها معلومات عن الحِزم الأخرى التي تعتمد عليها، وتأتي مع توثيق يشرح وظيفتها كي يستطيع الناس استخدامها، فلا تكون مقتصرةً على من كتبها فقط. تُحدَّث حزمة ما إذا وُجدت مشكلة فيها أو أضيفت إليها ميزة جديدة، وعليه تكون البرامج التي تعتمد عليها -والتي قد تكون حِزمًا أيضًا- يمكنها الترقية إلى تلك النسخة الجديدة. يتطلب العمل بهذه الطريقة بنيةً تحتيةً، وذلك لحاجتنا إلى مكان لتخزين تلك الحِزم والبحث فيها، وإلى طريقة سهلة لتثبيتها وترقيتها كذلك، كما تُوفَّر هذه البنية التحتية في عالم جافاسكربت من قِبَل NPM وهي اختصار لـ Node Package Manager، التي تعني مدير الحِزم. يتكون مدير الحِزم NPM من شيئين، أولهما تقدِّم خدمة تنزيل ورفع الحزم عبر الإنترنت -أي أونلاين online-، وبرنامج -مضمَّن مع Node.js- يساعدك على تثبيت تلك الحزم وإدارتها، كما توجد أكثر من نصف مليون حزمة متاحة على NPM وقت كتابة هذه الكلمات (بنسختها الأجنبية)، وأكثرها لا فائدة منه، لكن الحزم المفيدة المتاحة للعامة موجودة هناك أيضًا. سنجد محلل ملفات INI مثلًا الذي يشبه ما بنيناه في المقال السابق في هيئة حزمة اسمها ini، كما سنرى في مقال لاحق كيف نُثبِّت مثل تلك الحزم محليًا باستخدام أمر npm في الطرفية. إتاحية مثل تلك الحزم عالية الجودة للتحميل مفيد جدًا، فهو يعني استطاعتنا تجنب إعادة اختراع برنامج كتبه مائة إنسان قبلنا، كما نتجاوز ذلك إلى استخدام حزمة مجرَّبة ومختبَرة جيدًا ببضع ضغطات على لوحة المفاتيح، ومن بداهة القول أنّ البرامج لا تكلف شيئًا في نَسخها، لكن كتابة البرامج أول مرة هي العمل الذي يكلف الجهد والوقت والمهارة، كما يماثلها الاستجابة لمن يجد مشاكل في الشيفرة، أو الاستجابة لمن يريد إدخال ميزات جديدة في البرنامج. مَن يكتب البرنامج يمتلك حقوقه افتراضيًا، ولا يستطيع أحد استخدامه إلا برخصة من المبرمج، لكن بما أنّ بعض البشر ذوو قلوب طيبة، ولأنّ نشر البرامج الجيدة سيبني لك سُمعة وسط المبرمجين، فستسمح صراحةً كثير من الحِزم تحت رخصة ما باستخدامها من أيّ كان. ترخَّص أغلب الشيفرات الموجودة في NPM بتلك الطريقة، في حين قد تشترط بعض الرخص نشر الشيفرة التي تبنيها على قمة الحزمة التي استخدمتها تحت رخصة الحزمة نفسها، وأخرى لا تريد أكثر من استخدام رخصة الحزمة نفسها حين تنشر برنامجك، وهي الرخصة التي يستخدمها أغلب مجتمع جافاسكربت، وبالتالي تأكد حين تستخدِم حِزمًا كتبها غيرك من قراءة الرخصة التي تأتي بها الحزمة. الوحدات المرتجلة Improvised modules لم يكن في جافاسكربت نظام وحدات مضمَّن حتى عام 2015، إذ لم يمنع ذلك الناس من بناء نظمًا كبيرة في جافاسكربت طيلة أكثر من عشر سنين، كما كانوا في أمسّ الحاجة إلى وحدات لهذا، وعليه فقد صمموا نظم وحداتهم الخاصة بهم فوق اللغة نفسها، حيث نستطيع استخدام دوال جافاسكربت لإنشاء نطاقات محلية local scopes وكائنات لتمثل واجهات الوحدات. لدينا فيما يلي وحدةً للانتقال بين أسماء الأيام وأرقامها -وذلك من إعادة التابع getDay الخاص بـ Date-، إذ تتكون واجهتها من weekDay.name وweekDay.number، كما تخفي رابطتها names المحلية داخل نطاق تعبير الدالة الذي يُستدعى فورًا. const weekDay = function() { const names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; return { name(number) { return names[number]; }, number(name) { return names.indexOf(name); } }; }(); console.log(weekDay.name(weekDay.number("Sunday"))); // → Sunday يوفِّر هذا النسق من الوحدات عزلًا إلى حد ما، لكنه لا يصرح عن الاعتماديات، كما يضع واجهته في النطاق العام global scope، ويَتوقع اعتمادياتها إذا وُجدت أن تحذو حذوه، في حين كان ذلك هو الأسلوب المتَّبع في برمجة الويب لمدة طويلة، لكنه صار مهجورًا الآن وقلّما يُستخدَم. إذا أردنا جعل علاقات الاعتماديات جزءًا من الشيفرة، فيجب التحكم في تحميل الاعتماديات، ويتطلب ذلك أن نستطيع تنفيذ السلاسل النصية على أساس شيفرات، إذ تستطيع جافاسكربت فعل ذلك لحسن الحظ. تقييم البيانات على أساس شيفرات هناك عدة طرق لأخذ البيانات -أي سلسلة نصية من شيفرات برمجية- وتشغيلها على أساس جزء من البرنامج الحالي، وأبسط طريقة هي العامل الخاص eval الذي سينفِّذ السلسلة النصية في النطاق الحالي current scope، لكن هذه فكرة سيئة لا يُنصح بها، إذ تعطِّل بعض الخصائص التي تكون عادةً للمجالات مثل توقعها للرابطة التي يشير إليها اسم ما بسهولة. const x = 1; function evalAndReturnX(code) { eval(code); return x; } console.log(evalAndReturnX("var x = 2")); // → 2 console.log(x); // → 1 يمكن تفسير البيانات على أساس شيفرة بطريقة أبسط باستخدام الباني Function الذي يأخذ وسيطين هما سلسلة نصية تحتوي قائمة من أسماء الوسائط مفصول بينها بفاصلة أجنبية، وسلسلة نصية تحتوي على الدالة نفسها. يغلف الباني الشيفرة داخل قيمة دالة كي تحصل على نطاقها الخاص، ولا تفعل أمورًا غريبة مع النطاقات الأخرى. let plusOne = Function("n", "return n + 1;"); console.log(plusOne(4)); // → 5 هذا هو المطلوب تحديدًا وما نحتاج إليه في نظام الوحدات، إذ نستطيع تغليف شيفرة الوحدة في دالة، ونستخدم نطاق تلك الدالة على أساس نطاق الوحدة. CommonJS لعل أكثر منظور لوحدات جافاسكربت المضافة إليها هو نظام وحدات جافاسكربت المشتركة common Javascript والذي يدعى بالاسم CommonJS، إذ تستخدِمه Node.js الذي هو النظام المستخدَم في أغلب الحِزم على NPM. تدور الفكرة العامة لوحدات CommonJS حول دالة اسمها require، فحين نستدعيها مع اسم وحدة الاعتمادية، فستضمن أن تحميل الوحدة وستعيد واجهتها. تحصل الوحدات على نطاقها المحلي الخاص بها تلقائيًا لأنّ المحمّل يغلف شيفرة الوحدة في دالة، وما عليها إلا استدعاء require كي تصل إلى اعتمادياتها، وتضع واجهاتها في الكائن المقيَّد بـ exports. توفِّر الوحدة المثال التالية دالةً لتنسيق التاريخ، إذ تستخدِم حزمتين من NPM هما ordinal لتحويل الأعداد إلى سلاسل نصية مثل "1st" و"2nd"، وdate-names للحصول على الأسماء الإنجليزية للشهور وأيام الأسبوع، ثم تصدِّر دالةً وحيدةً هي formatDate تأخذ كائن Date وسلسلة قالب template string. قد تحتوي سلسلة القالب على شيفرات توجه التنسيق مثل YYYY للسنة كاملة وDo لترتيب اليوم في الشهر، ومن الممكن إعطاؤها سلسلةً نصيةً مثل "MMMM Do YYYY" للحصول على خرج مثل "November 22nd 2017". const ordinal = require("ordinal"); const {days, months} = require("date-names"); exports.formatDate = function(date, format) { return format.replace(/YYYY|M(MMM)?|Do?|dddd/g, tag => { if (tag == "YYYY") return date.getFullYear(); if (tag == "M") return date.getMonth(); if (tag == "MMMM") return months[date.getMonth()]; if (tag == "D") return date.getDate(); if (tag == "Do") return ordinal(date.getDate()); if (tag == "dddd") return days[date.getDay()]; }); }; تتكون واجهة ordinal من دالة واحدة، بينما تصدِّر date-names كائنًا يحتوي على عدة أشياء، إذ تُعَدّ days وmonths مصفوفات من الأسماء، كما تُعَدّ عملية فك البنية الهيكلية سهلةً جدًا عند إنشاء رابطات للواجهات المستوردة. تضيف الوحدة كذلك دالة واجهتها إلى exports كي تصل إليها الوحدات التي تعتمد عليها، إذ نستطيع استخدام الوحدة كما يلي: const {formatDate} = require("./format-date"); console.log(formatDate(new Date(2017, 9, 13), "dddd the Do")); // → Friday the 13th نستطيع تعريف require في أبسط صورها كما يلي: require.cache = Object.create(null); function require(name) { if (!(name in require.cache)) { let code = readFile(name); let module = {exports: {}}; require.cache[name] = module; let wrapper = Function("require, exports, module", code); wrapper(require, module.exports, module); } return require.cache[name].exports; } تَقرأ الدالة المختلَقة readFile في المثال أعلاه ملفًا وتعيد محتوياته في سلسلة نصية؛ أما جافاسكربت فلا توفِّر مثل تلك الخاصية، لكن توفِّر بيئات جافاسكربت المختلفة مثل المتصفحات وNode.js طرقها الخاصة للوصول إلى الملفات، وقد كان المثال يفترض وجود دالة اسمها readFile. تحتفظ دالة require بذاكرة مؤقتة cache من الوحدات المحمَّلة بالفعل، وذلك لتجنب تحميل الوحدة نفسها عدة مرات، فإذا استُدعيت، فستنظر أولًا إن كانت الوحدة المطلوبة محمَّلة من قبل أم لا، ثم تُحمِّلها إن لم تكن محمَّلة، حيث يتطلب ذلك قراءة شيفرة الوحدة وتغليفها في دالة واستدعاءها. لم تكن واجهة حِزمة ordinal التي رأيناها من قبل كائنًا وإنما دالةً، ومن مزايا CommonJS أنه رغم إنشاء نظام الوحدات لكائن واجهة فارغ مقيَّد بـ exports، يمكننا استبدال أيّ قيمة بذلك الكائن عبر إعادة كتابة module.exports، حيث يتم ذلك بعدة وحدات لتصدير قيمة واحدة بدلًا من كائن واجهة؛ وإذا عرَّفنا require وexports وmodule على أساس معامِلات لدالة التغليف المولَّدة وتمرير القيم المناسبة عند استدعائها، فسيضمن المحمِّل إتاحية تلك الرابطات في نطاق الوحدة. تختلف الطريقة التي تُرجِمت بها السلسلة النصية التي أُعطيت إلى دالة require إلى اسم ملف حقيقي أو عنوان ويب باختلاف الأنظمة، فإذا بدأت بـ ‎"./"‎ أو ‎"../"‎، فستُفسَّر تفسيرًا مرتبطًا باسم ملف الوحدة، وبالتالي سيكون ‎"./format-date"‎ هو الملف المسمى format-date.js في المجلد نفسه؛ أما إذا لم يكن الاسم مرتبطًا بالوحدة، فستبحث Node.js عن حِزمة مثبَّتة تحمل الاسم نفسه، وسنفسر مثل تلك الأسماء التي في شيفرات أمثلة هذا المقال على أنها تشير إلى حِزم NPM، كما سننظر بالتفصيل في كيفية تثبيت واستخدام وحدات NPM لاحقًا في هذه السلسلة. نستطيع استخدام واحد من NPM الآن بدلًا من كتابة محلل ملف INI الخاص بنا. const {parse} = require("ini"); console.log(parse("x = 10\ny = 20")); // → {x: "10", y: "20"} وحدات ESCMAScript تظل وحدات CommonJS حلًا غير دائم وغير احترافي رغم عملها بكفاءة وسماحها لمجتمع جافاسكربت -مع NPM- بتشارك الشيفرات على نطاق واسع، وتُعَدّ صيغتها غريبةً قليلًا، إذ لا تكون الأشياء التي نضيفها إلى exports متاحةً في النطاق المحلي مثلًا، وبما أنّ require هي استدعاء دالة عادية تأخذ أيّ نوع من الوسطاء وليس القيم مصنَّفة النوع فقط، فمن الصعب تحديد اعتماديات الوحدة دون تشغيل شيفرتها. لهذا يُصدِر معيار جافاسكربت نظام وحداته الخاص منذ 2015، كما يطلق عليه غالبًا ES modules، حيث تشير ES إلى ECMAScript، ورغم أنّ المفاهيم الأساسية للاعتماديات والواجهات لا زالت كما هي، إلا أنه يختلف في التفاصيل، فصارت الصيغة الآن مدمجةً في اللغة، كما نستطيع استخدام الكلمة المفتاحية الخاصة import بدلًا من استدعاء دالة للوصول إلى اعتمادية ما. import ordinal from "ordinal"; import {days, months} from "date-names"; export function formatDate(date, format) { /* ... */ } تُستخدم بالمثل كلمة export المفتاحية لتصدير الأشياء، وقد تأتي قبل تعريف دالة أو صنف أو رابطة (let أو const أو var). لا تكون واجهة وحدة ES قيمةً واحدةً، بل مجموعة من الرابطات المسماة، كما تربط الوحدة السابقة formatDate بدالة، فإذا استوردنا من وحدة أخرى، فسنستورد الرابطة وليس القيمة، مما يعني أن الوحدة المصدِّرة قد تغير قيمة الرابطة في أي وقت، وسترى الوحدات المستوردة القيمة الجديدة. إذا وُجدت رابطة اسمها default فستُعامَل على أنها القيمة المصدَّرة الأساسية للوحدة، فإذا استوردنا وحدةً مثل ordinal التي في المثال دون الأقواس التي حول اسم الرابطة، فسنحصل على رابطة default، كما تستطيع مثل تلك الوحدات تصدير رابطات أخرى تحت أسماء مختلفة مع تصدير default الخاص بها. إذا أردنا إنشاء تصدير افتراضي، فسنكتب export default قبل التعبير أو تصريح الدالة أو تصريح الصنف. export default ["Winter", "Spring", "Summer", "Autumn"]; من الممكن إعادة تسمية الرابطات المستورَدة باستخدام الكلمة as. import {days as dayNames} from "date-names"; console.log(dayNames.length); // → 7 من الفروقات المهمة كذلك هو حدوث تصديرات وحدات ES قبل بدء تشغيل سكربت الوحدة، وبالتالي قد لا تظهر تصريحات import داخل الدوال أو الكتل، ويجب أن تكون أسماء الاعتماديات سلاسل نصية مقتَبسة وليست تعبيرات عشوائية. يعكف مجتمع جافاسكربت أثناء كتابة هذه الكلمات على اعتماد وتبني أسلوب الوحدات هذا، لكن يبدو أنها عملية طويلة، فقد استغرقنا بضع سنين بعد الاستقرار على الصياغة كي تدعمها المتصفحات وNode.js، ورغم أنها صارت مدعومةً إلى حد كبير إلا أنّ ذلك الدعم به مشاكل، ولا زال الجدل قائمًا حول الكيفية التي يجب توزيع تلك الوحدات بها في NPM. تُكتب مشاريع عديدة باستخدام وحدات ES، ثم تُحوَّل تلقائيًا إلى صيغة أخرى عند نشرها، فنحن في مرحلة انتقالية يُستخدم فيها نظامَي وحدات جنبًا إلى جنب، ومن المفيد أننا نستطيع قراءة وكتابة شيفرات بكليهما. البناء والتجميع إذا نظرنا إلى مشاريع جافاسكربت فسنجد العديد منها لا يُكتب بجافاسكربت من الأساس -تقنيًا-، فثَمة امتدادات مستَخدَمة على نطاق واسع مثل ذلك الذي تعرضنا له في مقال الزلات البرمجية والأخطاء في جافاسكريبت الذي يتحقق من اللهجة، وقد بدأت الناس باستخدام الامتدادات الجاهزة في اللغة قبل إضافتها إلى المنصات التي تشغِّل جافاسكربت بزمن طويل أصلًا، ولكي يستطيع المبرمج فعل ذلك فإنه يصرِّف compile شيفرته، ثم يترجمها من لهجة جافاسكربت التي استخدمها إلى جافاسكربت عادية، أو إلى نسخة أقدم من جافاسكربت كي تستطيع المتصفحات الأقدم تشغيلها. لكن إدخال برنامج يتكون من وحدات ومن 200 ملف مختلف إلى صفحة ويب له مشاكله، فإذا كان جلب الملف عبر الشبكة يستغرق 50 ميلي ثانية، فسيستغرق تحميل البرنامج كله 10 ثواني، أو نصف تلك المدة إن كان يحمِّل عدة ملفات في الوقت نفسه، وهذا وقت ضائع. بدأ مبرمجو الويب باستخدام أدوات تجمع برامجهم التي قضوا الساعات المضنية في تقسيمها إلى وحدات، ليكون البرنامج ملفًا واحدًا كبيرًا، ثم ينشرونه على الويب، ذلك أن جلب ملف واحد وإن كان كبيرًا عبر الويب سيكون أسرع من جلب الكثير من الملفات الصغيرة، ويطلق على مثل تلك الأدوات اسم المجمِّعات bundlers. يتحكم حجم الملفات في سرعة نقلها عبر الشبكة كذلك، فليس العدد وحده هو المعيار، وعليه فقد اخترع مجتمع جافاسكربت مصغِّرات minifiers، وهي أدوات تأخذ برنامج جافاسكربت وتجعله أصغر عبر حذف التعليقات والمسافات تلقائيًا، كما تعيد تسمية الرابطات bindings، وتستبدل شيفرات أصغر بالشيفرات التي تأخذ حجمًا كبيرًا؛ وهكذا فليس من الغريب إيجاد شيفرة في حزمة NPM أو شيفرة تعمل في صفحة ويب، وتكون قد مرت بعدة مراحل من التحويل من جافاسكربت الحديثة إلى نسخة أقدم، ومن صيغة وحدات ES إلى CommonJS، ثم جُمِّعت وصغِّرت كذلك، كما لن نخوض في تفاصيل تلك الأدوات في هذه السلسلة لأنها مملة وتتغير بسرعة، لكن من المهم معرفة أنّ شيفرة جافاسكربت التي تشغِّلها لا تكون بهيئتها التي كُتبت بها أول مرة. تصميم الوحدة تُعَدّ هيكلة البرامج واحدةً من أدق الأمور في البرمجة، إذ يمكن نمذجة أيّ وظيفة غريبة بعدة طرق، كما يُعَدّ القول بجودة تصميم برنامج ما أمرًا نسبيًا، فهناك حلول وسط دومًا، كما تتدخل التفضيلات الشخصية في الحكم على التصميم، وأفضل ما يمكن فعله لتعلُّم قيمة التصميم الجيد هو القراءة والعمل على برامج كثيرة، وملاحظة ما ينجح وما يفشل، وإذا رأيت شيفرات فوضوية فلا تركن إلى قولك أن هذا هو الواقع ولا مجال لتغييره، بل هناك مساحة لتطوير هيكل أيّ شيء تقريبًا بإعمال الفكر فيه. تُعَدّ سهولة الاستخدام أحد أركان تصميم الوحدات، فإذا كنت تصمم شيئًا ليستخدمه أشخاص عدة -أو حتى نفسك فقط-، فيُستحسن أن تكون واجهتك بسيطةً وسهلة الفهم والتوقع حين تنظر إليها بعد ثلاثة أشهر حين تنسى تفاصيل ما كتبته وما عملته، وقد يعني ذلك اتباع سلوكيات موجودة سلفًا، كما تُعَدّ حزمة ini مثالًا على ذلك، إذ تحاكي كائن JSON القياسي من خلال توفير دوال parse وstringify -لكتابة ملف INI-، كما تحوِّل بين السلاسل النصية والكائنات المجردة مثل JSON، وهكذا فإنّ الواجهة صغيرة ومألوفة، وستذكر كيف استخدمتها لمجرد عملك معها مرةً واحدةً. حتى لو لم تكن ثمة دالة قياسية أو حِزمة مستخدَمة على نطاق كبير لمحاكاتها، فستستطيع إبقاء وحداتك مألوفةً وسهلة التوقع باستخدام هياكل بيانات بسيطة تفعل أمرًا واحدًا فقط، كما توجد على NPM مثلًا وحدات عديدة لتحليل ملف-INI، إذ تحتوي على دالة تقرأ مثل هذا الملف من القرص الصلب مباشرة وتُحلله، ويجعل ذلك من المستحيل استخدام مثل تلك الوحدات في المتصفح، حيث لا يكون لنا وصولًا مباشرًا إلى نظام الملفات، كما يضيف تعقيدًا كان يمكن معالجته بأسلوب أفضل عبر تزويد composing الوحدة بدالة قارئة للملفات file-reading. يشير ذلك إلى منظور آخر مفيد في تصميم الوحدات، وهو سهولة تطعيم شيء ما بشيفرة خارجية، إذ تكون الوحدات المركزة التي تحسب قيمًا قابلةً للتطبيق في طيف واسع من البرامج، على عكس الوحدات الأكبر التي تنفِّذ إجراءات معقدة لها آثار جانبية، وقارئ ملف INI الذي يركز على قراءة الملف من القرص الصلب ليس له فائدة في سيناريو يكون فيه محتوى الملف من مصدر خارجي، وبالمثل فإنّ الكائنات الحالة stateful objects مفيدة أحيانًا، بل قد تكون ضرورية؛ لكن إذا كان يمكن تنفيذ أمر ما بدالة فمن الأفضل استخدام الدالة. توفِّر العديد من قارئات ملفات INI على NPM نمط واجهة interface style، إذ يحتاج منك إنشاء كائن أولً، ثم تحمِّل الملف إلى الكائن، وبعد ذلك تستخدِم توابع مخصصة للحصول على النتائج، وذلك الأمر شائع في البرمجة كائنية التوجه، كما هو أمر مرهق وخاطئ. علينا تنفيذ طقوس نقل الكائن خلال عدة مراحل بدلًا من تنفيذ استدعاء واحد لدالة، وبما أن البيانات مغلفة الآن في نوع كائن مخصص، فيجب على كل الشيفرات التي تتفاعل معها معرفة ذلك النوع، مما يجعل لدينا اعتماديات غير ضرورية. قد تكون لدينا حالات لا يمكن تجنب تعريف هياكل بيانات جديدة فيها، حيث لا توفر اللغة إلا بعض الهياكل البسيطة، كما ستكون العديد من أنواع البيانات معقدةً أكثر من مجرد مصفوفة أو خارطة map، لكن إذا كانت المصفوفة تكفي، فسنستخدِم المصفوفة. يمكن النظر إلى مثال المخطط الذي تعرضنا له في مقال مشروع تطبيقي لبناء رجل آلي (روبوت) عبر جافاسكريبت على أساس مثال على هيكل بيانات معقد، إذ لا توجد طريقة واضحة لتمثيل المخطط في جافاسكربت، وقد استخدمنا في ذلك المقال كائنًا تحمل خصائصه مصفوفات من السلاسل النصية، وهي العقد الأخرى التي يمكن الوصول إليها من تلك العقدة. توجد العديد من حِزم إيجاد المسار في NPM، لكن لا تستخدِم أيّ منها صيغة المخطط تلك، فهي تسمح عادةً لحدود المخطط أن يكون لها وزن يكون التكلفة أو المسافة المرتبطة به، ولم يكن ذلك ممكنًا في تمثيلنا. هناك حِزمة dijkstrajs مثلًا التي تستخدِم منظورًا شائعًا جدًا لإيجاد المسارات والذي يُسمى بخوارزمية ديكسترا Dijkstra's algorithm، إذ سُمي باسم إدزجر ديكسترا Edsger Dijkstra الذي كتبه، وهو مشابه لدالة findRoute الخاصة بنا، ومن الشائع أن تضاف اللاحقة js إلى اسم الحِزمة لتوضيح أنها مكتوبة بجافاسكربت. تستخدِم حزمة dijkstrajs صيغة مخطط تشبه صيغتنا، لكنها تستخدم كائنات تكون قيم خصائصها أعدادًا -أي أوزان الحدود- بدلًا من المصفوفات التي استخدمناها، فإذا أردنا استخدام تلك الحزمة، فسيكون علينا التأكد من تخزين المخطط بالصيغة التي تتوقعها الحزمة، كما يجب أن تحصل جميع الحدود على الوزن نفسه بما أنّ نموذجنا المبسط يعامِل كل طريق على أساس امتلاكه التكلفة نفسها -أي منعطف واحد-. const {find_path} = require("dijkstrajs"); let graph = {}; for (let node of Object.keys(roadGraph)) { let edges = graph[node] = {}; for (let dest of roadGraph[node]) { edges[dest] = 1; } } console.log(find_path(graph, "Post Office", "Cabin")); // → ["Post Office", "Salma’s House", "Cabin"] قد يكون ذلك حاجزًا معيقًا عن التركيب compositing، حيث تستخدِم حِزم عدة هياكل بيانات مختلفة لوصف أشياء متشابهة، فيكون جمعها معًا صعبًا، وبالتالي إذا أردنا تصميم شيء ليكون قابلًا للتركيب، فيجب علينا معرفة هياكل البيانات التي يستخدِمها الأشخاص أولًا، ثم نحذو حذوهم كلما تيسر. خاتمة توفر الوحدات هيكلًا لبرامج أكبر من خلال فصل الشيفرة إلى أجزاء صغيرة لها واجهات واضحة واعتماديات، كما تُعَدّ الواجهة الجزء المرئي من الوحدة للوحدات الأخرى، والاعتماديات هي الوحدات الأخرى التي تستخدِمها. ولأنّ جافاسكربت لم توفر نظامًا للوحدات في البداية، فقد بُني نظام CommonJS عليها، ثم حصلت جافاسكربت بعد مدة على نظام وحدات خاص بها، وهو الآن موجود إلى جانب CommonJS. تُعَدّ الحزمة مجموعة شيفرات يمكن توزيعها مستقِلة بذاتها، كما يُعَدّ NPM مستودعًا لحِزم جافاسكربت، ونستطيع تحميل شتى الحِزم منه سواء مفيدة أو غير مفيدة. تدريبات الروبوت التركيبي فيما يلي الرابطات التي ينشئها المشروع الذي في مقال مشروع تطبيقي لبناء رجل آلي (روبوت) عبر جافاسكريبت الذي أشرنا إليه بالأعلى. roads buildGraph roadGraph VillageState runRobot randomPick randomRobot mailRoute routeRobot findRoute goalOrientedRobot إذا أردت كتابة هذا المشروع على أساس برنامج تركيبي modular، فما الوحدات التي ستنشئها؟ وما الوحدات التي ستعتمد عليها كل وحدة؟ وكيف ستبدو واجهاتها؟ وأيّ الأجزاء ستكون متاحةً ومكتوبةً مسبقًا على NPM؟ وهل ستفضِّل استخدام حِزمة NPM أم تكتبها بنفسك؟ إرشادات للحل إليك ما كنا لنفعله بأنفسنا، لكن مرةً أخرى ليست هناك طريقة صحيحة وحيدة لتصميم وحدة معطاة: توجد الشيفرة المستخدَمة لبناء مخطط الطريق في وحدة graph، ولأننا نُفضِّل استخدام dijkstrajs من NPM بدلًا من شيفرة إيجاد المسار التي كتبناها، فسنجعل هذا المخطط قريبًا من الذي تتوقعه dijkstrajs. تصدِّر هذه الوحدة دالةً واحدةً هي buildGraph، كما سنجعل هذه الدالة تقبل مصفوفةً من مصفوفات ثنائية العناصر بدلًا من سلاسل نصية تحتوي على شخطات -، وذلك من أجل تقليل اعتماد الوحدة على صيغة الإدخال. تحتوي وحدة roads على بيانات الطريق الخام -أي مصفوفة roads- ورابطة roadGraph، كما تعتمد تلك الوحدة على ‎./graph وتصدر مخطط الطريق. يوجد صنف VillageState في وحدة state، حيث يعتمد على وحدة ‎./roads لأنه يحتاج إلى أن يكون قادرًا على التحقق من وجود طريق موجود فعليًا، كما يحتاج إلى randomPick، وبما أنّ هذه دالة من ثلاثة أسطر، فسنضعها في وحدة state على أساس دالة مساعدة داخلية، لكن randomRobot يحتاج إليها كذلك، لذا يجب تكرارها أو وضعها في وحدتها الخاصة، وبما أنّ تلك الدالة موجودة في NPM في حِزمة random-item، فمن الأفضل جعل كلا الوحدتين تعتمدان عليها، كما نستطيع إضافة دالة runRobot إلى تلك الوحدة أيضًا، بما أنها صغيرة ومرتبطة ارتباطًا وثيقًا بإدارة الحالة، في حين تصدِّر الوحدة كلا من الصنف VillageState ودالة runRobot. أخيرًا، يمكن دخول الروبوتات والقيم التي تعتمد عليها مثل mailRoute في وحدة example-robots التي تعتمد على ‎./roads وتصدِّر دوال الروبوت، ولكي يستطيع goalOrientedRobot البحث عن المسار، فستعتمد الوحدة على dijkstrajs أيضًا. صارت الشيفرة أصغر قليلًا من خلال نقل بعض المهام إلى وحدات NPM، حيث تنفِّذ كل وحدة مستقِلة شيئًا بسيطًا ويمكن قراءتها بنفسها، ويقترح تقسيم الشيفرات إلى وحدات على أساس تحسينات أكثر لتصميم البرنامج. يُعد اعتماد الروبوتات وVillageState على مخطط طريق بعينه غريبًا بعض الشيء، فربما يكون من الأفضل جعل المخطط وسيطًا لباني الحالة، ونجعل الروبوتات تقرؤه من كائن الحالة، فنقلل الاعتماديات -وهو أمر جيد-، ونجعل من الممكن إجراء عمليات محاكاة على خرائط مختلفة، وذلك خير وأفضل. هل من الجيد استخدام وحدات NPM لأمور كان يمكن كتابتها بأنفسنا؟ نظريًا، نعم، فمن المرجح أنك ستقع في أخطاء في الأمور المعقَّدة مثل دالة إيجاد المسار وتضيع وقتك في كتابتها بنفسك؛ أما بالنسبة للدوال الصغيرة مثل random-item فتكون كتابتها أمرًا يسيرًا، لكن إضافتها في أيّ مكان تحتاج إليها فيه سيعكر وحداتك. لكن بأي حال، لا تقلل من شأن الجهد المطلوب لإيجاد حِزمة NPM مناسبة، فحتى لو وجدتها فقد لا تعمل جيدًا أو تكون مفتقدةً إلى بعض المزايا التي تحتاجها، وفوق هذا يعني الاعتماد على حِزم NPM التأكد من أنها مثبَّتة، كما ستنشرها مع برنامجك، بل ربما يكون عليك ترقيتها دوريًا، فهذه قد تكون إحدى التبعات التي عليك تحملها، وعلى أيّ حال تستطيع اتخاذ القرار الذي يناسبك وفقًا لمقدار العون الذي تقدمه تلك الحزم لك. وحدة الطرق اكتب وحدة CommonJS بناءً على المثال الذي في مقال مشروع تطبيقي لبناء رجل آلي (روبوت) عبر جافاسكريبت، بحيث تحتوي على مصفوفة من الطرق وتصدِّر هيكل بيانات المخطط الذي يمثلها على أساس roadgraph، كما يجب أن تعتمد على وحدة ‎./graph التي تصدِّر الدالة buildGraph المستخدَمة لبناء المخطط، وتتوقع هذه الدالة مصفوفةً من مصفوفات ثنائية العناصر -أي نقاط البداية والنهاية للطرق-. تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen. // أضف اعتماديات وتصديرات. const roads = [ "Salma's House-Omar's House", "Salma's House-Cabin", "Salma's House-Post Office", "Omar's House-Town Hall", "Sara's House-Mostafa's House", "Sara's House-Town Hall", "Mostafa's House-Sama's House", "Sama's House-Farm", "Sama's House-Shop", "Marketplace-Farm", "Marketplace-Post Office", "Marketplace-Shop", "Marketplace-Town Hall", "Shop-Town Hall" ]; إرشادات للحل بما أنّ هذه وحدة CommonJS، فعليك استخدام require لاستيراد وحدة المخطط، وقد وُصف ذلك على أساس تصدير دالة buildGraph، إذ تستطيع استخراجه من كائن واجهته مع تصريح فك الهيكلة const. أضف خاصيةً إلى كائن exports من أجل تصدير roadGraph، كما يجب أن يكون فصل سلاسل الطريق النصية داخل وحدتك لأن buildGraph تأخذ هيكل بيانات لا يطابق roads بالضبط. الاعتماد المتبادل بين الوحدات تُعَدّ حالة الاعتماد المتبادل بين وحدتين ويدعى circular dependency حالةً تعتمد فيها الوحدة A على الوحدة B -أي يكون الاعتماد على شكل حلقة تبادلية بين وحدتين ولذلك سمي باللغة الإنجليزية circular dependency-، والعكس صحيح مباشرةً أو غير مباشرة، كما تمنع العديد من أنظمة الوحدات ذلك بسبب استطاعتك التأكد من تحميل اعتماديات وحدة قبل تشغيلها، بعض النظر عن الترتيب الذي تختاره لتحميل مثل تلك الوحدات. تسمح وحدات CommonJS بصورة محدودة من الاعتماديات الدورية cyclic dependencies تلك، طالما أنّ الوحدات لا تستبدل كائن exports الافتراضي الخاص بها، ولا يكون لها وصول إلى واجهة غيرها حتى تنتهي من التحميل. تدعم دالة require التي تعرضنا لها سابقًا في هذا المقال مثل ذلك النوع من تبادلية الاعتماديات dependency cycle، فهل يمكنك رؤية كيف تعالج هذه الحالة؟ وما الذي قد يحدث إذا استبدلت وحدة في تعتمد وحدة أخرى عليها (أي داخلة في دورة الاعتمادية)دورة، كائن exports الافتراضي الخاص بها؟ إرشادات للحل تدور الفكرة هنا حول إضافة require وحدات إلى ذاكرتها المؤقتة قبل بدء تحميل الوحدة، وهكذا فإذا حاول أيّ استدعاء require تحميلها أثناء تشغيلها، فسيكون معروفًا وتُعاد الواجهة الحالية بدلًا من تحميل الوحدة مرةً أخرى، وذلك يتسبب في طفحان المكدس؛ أما إذا أعادت وحدة كتابة قيمة module.exports الخاصة بها، فستحصل أيّ وحدة أخرى كانت قد استقبلت قيمة واجهتها قبل أن تنهي التحميل، على كائن الواجهة الافتراضي -الذي سيكون فارغًا على الأرجح- بدلًا من قيمة الواجهة المقصودة. ترجمة -بتصرف- للفصل العاشر من كتاب Elequent Javascript لصاحبه Marijn Haverbeke. اقرأ أيضًا مقدمة إلى الوحدات Modules في جافاسكربت الوحدات Modules والحزم Packages في بايثون الوحدات Modules في AngularJS
  10. إذا نظرنا إلى التقنيات والأدوات البرمجية المنتشرة، فسنرى أنّ التقنية أو الأداة المشهورة هي التي تُثبت كفاءتها العملية في مجالها، أو التي تتكامل تكاملًا ممتازًا مع تقنية أخرى مستخدَمة بكثرة، وليس لأنها الأجمل أو الأذكى.إرشادات للحل سنناقش هنا إحدى تلك الأدوات التي تدعى بالتعابير النمطية Regular Expressions، وهي طريقة لوصف الأنماط patterns في بيانات السلاسل النصية، إذ تُكوِّن هذه التعابير لغةً صغيرةً مستقلةً بذاتها، لكنها رغم ذلك تدخل في لغات برمجية أخرى مثل جافاسكربت، كما تدخل في العديد من الأنظمة. قد تكون صيغة التعابير النمطية غريبةً للناظر إليها، كما أنّ الواجهة التي توفرها جافاسكربت للتعامل معها ليست بالمثلى، لكن رغم هذا، فتلك التعابير تُعَد أداةً قويةً لفحص ومعالجة السلاسل النصية، كما سيعينك فهمها على كتابة حلول أكفأ للمشاكل التي تواجهك. إنشاء تعبير نمطي التعبير النمطي هو نوع من أنواع الكائنات؛ فإما يُنشأ باستخدام الباني RegExp، أو يُكتب على أساس قيمة مصنَّفة النوع literal value بتغليف النمط بشرطتين مائلتين أماميتين من النوع /. let re1 = new RegExp("abc"); let re2 = /abc/; يُمثِّل كائنا التعبيرات النمطية في المثال السابق النمط نفسه أي محرف a يتبعه b ثم c، ويُكتب النمط على أساس سلسلة نصية عادية عند استخدام الباني RegExp، لذا تطبَّق القواعد المعتادة للشرطات المائلة الخلفية \، على عكس الحالة الثانية التي نرى فيها النمط بين شرطتين مائلتين، إذ تعامَل الشرطات هنا تعاملًا مختلفًا. نحتاج إلى وضع شرطة خلفية قبل أي شرطة أمامية نريدها جزءًا من النمط نفسه، وذلك لأن الشرطة الأمامية تنهي النمط إذا وُجدت. كذلك فإن الشرطات الخلفية التي لا تكون جزءًا من ترميز خاص لمحرف -مثل ‎\n- لن تُتَجاهل كما نفعل في السلاسل النصية، وعليه ستتسبب في تغيير معنى النمط. تملك بعض المحارف مثل علامات الاستفهام وإشارات الجمع مَعاني خاصة في التعابير النمطية، حيث سنحتاج إلى وضع شرطة مائلة خلفية قبلها إذا أردنا لها تمثيل المحرف نفسه وليس معناه في التعابير النمطية. let eighteenPlus = /eighteen\+/; التحقق من المطابقات تملك كائنات التعابير النمطية توابع عديدة، وأبسط تلك التوابع هو التابع test الذي إن مرَّرنا سلسلةً نصيةً إليه، فسيُعيد قيمةً بوليانيةً تخبرنا هل تحتوي السلسلة على تطابق للنمط الذي في التعبير أم لا. console.log(/abc/.test("abcde")); // → true console.log(/abc/.test("abxde")); // → false يتكون التعبير النمطي من محارف عادية غير خاصة تمثِّل -ببساطة- ذلك التسلسل من المحارف، فإذا وُجد abc في أيّ مكان في السلسلة النصية التي نختبرها -ولا يُشترط وجودها في بدايتها-، فسيُعيد test القيمة true. مجموعات المحارف يمكن التحقق من وجود abc في سلسلة نصية باستدعاء التابع indexof. تسمح لنا التعابير النمطية بالتعبير عن أنماط أكثر تعقيدًا، فمثلًا كل ما علينا فعله لمطابقة عدد ما هو وضع مجموعة من المحارف بين قوسين مربعين لجعل ذلك الجزء من التعبير يطابق أيًا من المحارف الموجودة بين الأقواس. لننظر المثال التالي حيث يطابق التعبيران جميع السلاسل النصية التي تحتوي على رقم ما: console.log(/[0123456789]/.test("in 1992")); // → true console.log(/[0-9]/.test("in 1992")); // → true يمكن الإشارة إلى مجال من المحارف داخل القوسين المربعين باستخدام الشرطة - بين أول محرف فيه وآخر محرف، ويُحدَّد الترتيب في تلك المحارف برمز اليونيكود لكل محرف -كما ترى من المثال أعلاه الذي يشير إلى مجال الأرقام من 0 إلى 9-، وهذه الأرقام تحمل رمز 48 حتى 57 بالترتيب في اليونيكود، وعليه فإنّ المجال [0-9]‎‎ يشملها جميعًا، ويطابق أي رقم. تمتلك مجموعة محارف الأعداد اختصارات خاصة بها كما هو شأن العديد من مجموعات المحارف الشائعة، فإذا أردنا الإشارة إلى المجال من 0 حتى 9، فسنستخدِم الاختصار ‎\d. 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; } الاختصار الدلالة ‎\d أيّ محرف رقمي ‎\w محرف أبجدي أو رقمي، أي محرف الكلمة ‎\s أيّ محرف مسافة بيضاء، مثل المسافات الفارغة والجداول والأسطر الجديدة وما شابهها ‎\D محرف غير رقمي ‎\W محرف غير أبجدي وغير رقمي ‎\S محرف مغاير للمسافة البيضاء . أيّ محرف عدا السطر الجديد نستطيع مطابقة تنسيق التاريخ والوقت -كما في 01-30-2003 15:20- باستخدام التعبير التالي: let dateTime = /\d\d-\d\d-\d\d\d\d \d\d:\d\d/; console.log(dateTime.test("01-30-2003 15:20")); // → true console.log(dateTime.test("30-jan-2003 15:20")); // → false تعيق الشرطات المائلة الموجودة في المثال أعلاه قراءة النمط الذي نعبر عنه، وسنرى لاحقًا نسخةً أفضل في هذا المقال. يمكن استخدام رموز الشرطات المائلة تلك داخل أقواس مربعة، إذ تعني [‎\d.‎] مثلًا أيّ رقم أو محرف النقطة .، لكن تفقِد النقطة نفسها معناها المميز لها إذا كانت داخل أقواس مربعة، وبالمثل في حالة إشارة الجمع +؛ أما إذا أردنا عكس مجموعة محارف، أي إذا أردنا التعبير عن رغبتنا في مطابقة أيّ محرف عدا تلك المحارف التي في المجموعة، فنستخدم رمز الإقحام ^ بعد قوس الافتتاح. let notBinary = /[^01]/; console.log(notBinary.test("1100100010100110")); // → false console.log(notBinary.test("1100100010200110")); // → true تكرار أجزاء من النمط لقد بتنا الآن نعلم كيف نطابق رقمًا واحدًا، لكن كيف سنفعل ذلك إذا أردنا مطابقة عدد يحتوي على أكثر من رقم؟ إذا وضعنا إشارة الجمع + بعد شيء ما في تعبير نمطي، فستشير إلى أنّ هذا العنصر قد يكرَّر أكثر من مرة، بالتالي تعني ‎/\d+/‎ مطابقة محرف رقم أو أكثر. console.log(/'\d+'/.test("'123'")); // → true console.log(/'\d+'/.test("''")); // → false console.log(/'\d*'/.test("'123'")); // → true console.log(/'\d*'/.test("''")); // → true تحمل إشارة النجمة * معنى قريبًا من ذلك، حيث تسمح للنمط بالمطابقة على أي حال، فإذا لحقت إشارة النجمة بشيء ما، فلن تمنع النمط من مطابقته، إذ ستطابق نُسخًا صفرية zero instances حتى لو لم تجد نصًا مناسبًا لمطابقته؛ أما إذا جاءت علامة الاستفهام بعد محرف في نمط، فستجعل ذلك المحرف اختياريًا optional، أي قد يحدث مرةً واحدةً أو لا يحدث. يُسمح للمحرف u في المثال التالي بالحدوث، ويحقق النمط المطابقة حتى لو لم يكن u موجودًا أيضًا. let neighbor = /neighbou?r/; console.log(neighbor.test("neighbour")); // → true console.log(neighbor.test("neighbor")); // → true نستخدم الأقواس المعقوصة إذا أردنا حدوث النمط عددًا معينًا من المرات، فإذا وضعنا {4} بعد عنصر ما مثلًا، فسيجبره بالحدوث 4 مرات حصرًا، ومن الممكن تحديد المجال الذي يمكن للعنصر حدوثه فيه بكتابة {‎2,4‎} التي تشير إلى وجوب ظهور العنصر مرتين على الأقل، وأربع مرات على الأكثر. لدينا نسخة أخرى من نمط التاريخ والوقت، حيث تسمح بذكر الأيام برقم واحد -أو رقمين-، والأشهر، والساعات، إذ تُعَدّ أسهل قليلًا في قراءتها، وهي المثال الذي قلنا أننا سنعود إليه بنسخة أفضل. let dateTime = /\d{1,2}-\d{1,2}-\d{4} \d{1,2}:\d{2}/; console.log(dateTime.test("1-30-2003 8:45")); // → true نستطيع تحديد مجالات مفتوحة عند استخدام الأقواس بإهمال الرقم الموجود بعد الفاصلة، وبالتالي، تعني {‎5,‎} خمس مرات على الأقل. جمع التعبيرات الفرعية إذا أردنا استخدام عامل مثل *، أو + على أكثر من عنصر في المرة الواحدة، فيجب استخدام الأقواس، وسترى العوامل الجزء الذي داخل الأقواس من التعبير النمطي عنصرًا واحدًا. let cartoonCrying = /boo+(hoo+)+/i; console.log(cartoonCrying.test("Boohoooohoohooo")); // → true تطبَّق إشارتا الجمع الأولى والثانية على o الثانية فقط في boo، وhoo على الترتيب؛ أما علامة الجمع الثالثة فتطبَّق على المجموعة كلها (hoo+‎) مطابِقةً تسلسلًا واحدًا أو أكثر بهذا. يجعل محرف i -الذي في نهاية التعبير- هذا التعبير النمطي غير حساس لحالة المحارف، إذ يسمح بمطابقة المحرف B في سلسلة الدخل النصية رغم تكوّن النمط من محارف صغيرة. التطابقات والمجموعات يُعَدّ التابع test أبسط طريقة لمطابقة تعبير نمطي، إذ لا يخبرك إلا بمطابقة التعبير النمطي من عدمها وفقط، كذلك تملك التعبيرات النمطية تابعًا اسمه exec، حيث يُعيد القيمة null إذا لم يجد مطابقة، كما يُعيد كائنًا مع معلومات عن المطابقة إذا وجد تطابق. let match = /\d+/.exec("one two 100"); console.log(match); // → ["100"] console.log(match.index); // → 8 تكون للكائن المعاد من exec خاصية تدعى index، إذ تخبرنا أين تبدأ المطابقة الناجحة للسلسلة النصية؛ أما خلاف هذا فيبدو الكائن أشبه بمصفوفة من السلاسل النصية -وهو كذلك حقًا-، ويكون أول عنصر في تلك المصفوفة هو السلسلة المطابَقة، كما يكون ذلك هو تسلسل الأرقام الذي كنا نبحث عنه في المثال السابق. تحتوي قيم السلاسل النصية على التابع match الذي له سلوك مشابه: console.log("one two 100".match(/\d+/)); // → ["100"] حين يحتوي التعبير النمطي على تعبيرات فرعية مجمَّعة داخل أقواس، فسيظهر النص الذي يطابق تلك المجموعات في مصفوفة، ويكون العنصر الأول هو التطابق كله دومًا، في حين يكون العنصر التالي هو الجزء المطابَق بواسطة المجموعة الأولى التي يأتي قوس افتتاحها أولًا في التعبير، ثم المجموعة الثانية، وهكذا. let quotedText = /'([^']*)'/; console.log(quotedText.exec("she said 'hello'")); // → ["'hello'", "hello"] إذا لم تطابَق مجموعة ما مطلقًا -كأن تُتبَع بعلامة استفهام-، فسيكون موضعها في مصفوفة الخرج غير معرَّفًا undefined، وبالمثل، فإذا طابقت مجموعةً ما أكثر من مرة، فستكون المطابقة الأخيرة هي التي في المصفوفة فقط. console.log(/bad(ly)?/.exec("bad")); // → ["bad", undefined] console.log(/(\d)+/.exec("123")); // → ["123", "3"] تفيدنا المجموعات في استخراج أجزاء من سلسلة نصية، فإذا أردنا التحقق من احتواء السلسلة النصية على تاريخ، ومن ثم استخراج ذلك التاريخ وبناء كائن يمثله؛ فيمكننا إحاطة الأنماط الرقمية بأقواس، وأخذ التاريخ مباشرةً من نتيجة exec، لكن نحتاج قبل ذلك إلى النظر سريعًا على الطريقة المضمَّنة لتمثيل قيم التاريخ والوقت في جافاسكربت. صنف التاريخ تحتوي جافاسكربت على صنف قياسي لتمثيل البيانات -أو النقاط- في الزمن، ويسمى ذلك الصنف Date، فإذا أنشأنا كائن تاريخ باستخدام new، فسنحصل على التاريخ والوقت الحاليين. console.log(new Date()); // → Mon Nov 13 2017 16:19:11 GMT+0100 (CET) من الممكن إنشاء كائن لوقت محدد: console.log(new Date(2009, 11, 9)); // → Wed Dec 09 2009 00:00:00 GMT+0100 (CET) console.log(new Date(2009, 11, 9, 12, 59, 59, 999)); // → Wed Dec 09 2009 12:59:59 GMT+0100 (CET) تستخدِم جافاسكربت تقليدًا تبدأ فيه أعداد الشهور بالصفر -وعليه يكون شهر ديسمبر هو عدد 11-، بينما تبدأ أرقام الأيام بالواحد، وذلك أمر محيِّر وسخيف كذلك لكنه واقع، وإنما ذكرناه للتنبيه. تُعَدّ آخر أربعة وسائط arguments -أي الساعات والدقائق والثواني والميلي ثانية- وسائط اختيارية، وإذا لم تحدد قيمة أيّ منهم فتكون صفرًا افتراضيًا. تُخزَّن العلامات الزمنية Timestamps بعدد من الميلي ثانية منذ العام 1970 في منطقة UTC الزمنية -أي التوقيت العالمي-، ويتبع هذا اصطلاحًا ضُبِط بواسطة توقيت يونكس Unix time الذي اختُرع في تلك الفترة أيضًا، كما يمكن استخدام الأرقام السالبة للتعبير عن الأعوام التي سبقت 1970. إذا استُخدم التابع getTime على كائن تاريخ، فسيُعيد ذلك العدد، وهو عدد كبير كما هو متوقع. console.log(new Date(2013, 11, 19).getTime()); // → 1387407600000 console.log(new Date(1387407600000)); // → Thu Dec 19 2013 00:00:00 GMT+0100 (CET) إذا أعطينا الباني Date وسيطًا واحدًا، فسيعامَل الوسيط على أنه تعداد الميلي ثانية، ويمكن الحصول على القيمة الحالية لتعداد المللي ثانية بإنشاء كائن Date جديد، واستدعاء getTime عليه، أو استدعاء الدالة Date.now. توفِّر كائنات التاريخ توابعًا، مثل getFullYear وgetMonth وgetDate وgetHours وgetMinutes وgetSeconds، من أجل استخراج مكوناتها، كما يعطينا التابع getYear السنة بعد طرح 1900 منها -98، أو 119-، لكن لن نستخدِمه كثيرًا لعدم وجود فائدة حقيقية منه. نستطيع الآن إنشاء كائن تاريخ من سلسلة نصية بما أننا وضعنا أقواسًا حول أجزاء التعبير التي تهمنا، أي كما يلي: function getDate(string) { let [_, month, day, year] = /(\d{1,2})-(\d{1,2})-(\d{4})/.exec(string); return new Date(year, month - 1, day); } console.log(getDate("1-30-2003")); // → Thu Jan 30 2003 00:00:00 GMT+0100 (CET) تُهمَل رابطة الشرطة السفلية _، ولا تُستخدَم إلا لتجاوز عنصر المطابَقة التامة في المصفوفة التي يُعيدها التابع exec. حدود الكلمة والسلسلة النصية يستخرِج التابع getDate التاريخ 00-1-3000 من السلسلة النصية "‎100-1-30000‎"، وهو تاريخ غير منطقي لا شك، حيث تحدث المطابقة في أي موقع في السلسلة النصية، لذا تبدأ في حالتنا عند المحرف الثاني، وتنتهي عند المحرف الثاني من النهاية. نضيف العلامتين ^ و$ لإجبار المطابقة على النظر في السلسلة كلها، إذ تطابِق علامة الإقحام بداية سلسلة الدخل، بينما تطابِق علامة الدولار نهايتها، إذ تطابق ‎/^\d+$/‎ مثلًا سلسلةً مكونةً من رقم واحد أو أكثر، وتطابق ‎/^!/‎ أي سلسلة تبدأ بعلامة تعجب، بينما لا تطابق ‎/x^/‎ أي سلسلة نصية، إذ لا يمكن وجود x قبل بداية السلسلة. نستخدم العلامة ‎\b‎ للتأكد من أنّ التاريخ يبدأ وينتهي عند حدود كلمة، وقد يكون حد الكلمة بداية السلسلة، أو نهايتها، أو أي نقطة فيها تملك محرف كلمة -كما في ‎\w على أحد الجانبين، و محرف غير كلمي على الجانب الآخر. console.log(/cat/.test("concatenate")); // → true console.log(/\bcat\b/.test("concatenate")); // → false لاحظ أنّ علامة الحد لا تطابق محرفًا حقيقيًا، بل تضمن عدم مطابقة التعبير النمطي إلا عند حدوث حالة معينة في الموضع الذي يظهر فيه في النمط. أنماط الاختيار لنقل أننا نريد معرفة هل يحتوي جزء ما من النص على عدد متبوع بإحدى الكلمات التالية: horse، أو cow، أو chicken، أو أي صورة من صور الجمع لها، قد نكتب ثلاثة تعابير نمطية ونختبرها لكن ثَم طريقة أفضل، وذلك بوضع محرف الأنبوب | الذي يشير إلى خيار بين النمط الذي عن يمينه والنمط الذي عن يساره، وعليه نستطيع القول كما يلي: let animalCount = /\b\d+ (horse|cow|chicken)s?\b/; console.log(animalCount.test("15 horses")); // → true console.log(animalCount.test("15 horsechickens")); // → false يمكن استخدام الأقواس لتقييد جزء النمط الذي يطبَّق عليه عامل الأنبوب، ويمكن وضع عدة عوامل مثل هذا بجانب بعضها البعض للتعبير عن اختيار بين أكثر من بديلين اثنين. آلية المطابقة يبحث محرك التعبير نظريًا عند استخدام exec أو test عن تطابق في سلسلتنا النصية، وذلك بمحاولة مطابقة التعبير من بداية السلسلة أولًا، ثم من المحرف الثاني، وهكذا حتى يجد تطابقًا أو يصل إلى نهاية السلسلة، وعندئذ يُعيد أول تطابق وجده، أو يكون قد فشل في إيجاد تطابق أصلًا؛ أما في عملية المطابقة الفعلية، فيعامِل المحرك التعبير النمطي مثل مخطط تدفق flow diagram، وإذا استخدمنا مثالنا السابق عن الحيوانات، فسيبدو مخطط التعبير الخاص بها كما يلي: يكون تعبيرنا مطابقًا إذا استطعنا إيجاد مسار من جانب المخطط الأيسر إلى جانبه الأيمن، حيث نحفظ الموضع الحالي في السلسلة النصية، ونتأكد في كل حركة نتحركها خلال صندوق من أنّ جزء السلسلة التالي لموضعنا الحالي يطابق ذلك الصندوق. إذا كنا نحاول مطابقة "the 3 horses" من الموضع 4، فسيبدو مسار تقدمنا داخل المخطط كما يلي: يمكننا تجاوز الصندوق الأول لوجود حد كلمي word boundary عند الموضع 4. لا زلنا في الموضع 4 ووجدنا رقمًا، لذا نستطيع تجاوز الصندوق الثاني. يتكرر أحد المسارات إلى ما قبل الصندوق الثاني (الرقم) عند الموضع 5، بينما يتحرك الآخر للأمام خلال الصندوق، ويحمل محرف مسافة واحد؛ لذا يجب أخذ المسار الثاني لوجود مسافة وليس رقمًا. نحن الآن في الموضع 6، أي بداية horses وعند التفرع الثلاثي في المخطط، إذ لا نرى cow، ولا chicken هنا، لكن نرى horse، لذا سنأخذ ذلك الفرع. يتخطى أحد المسارات صندوق s عند الموضع 9 بعد التفرع الثلاثي، ويذهب مباشرةً إلى حد الكلمة الأخير، بينما يطابق المسار الآخر s، كما سنمر خلال صندوق s لوجود المحرف s وليس حدًا لكلمة. نحن الآن عند الموضع 10 وهو نهاية السلسلة النصية، ونستطيع مطابقة حد الكلمة فقط، وتُحسب نهاية السلسلة النصية على أنها حد كلمي، لذا سنمر خلال الصندوق الأخير ونكون قد طابقنا تلك السلسلة النصية بنجاح. التعقب الخلفي يطابق التعبير النمطي ‎/\b([01]+b|[\da-f]+h|\d+)\b/‎ عددًا ثنائيًا متبوعًا بـ b، أو عددًا ست عشريًا hexadecimal -وهو نظام رقمي تمثل فيه الأعداد من 10 إلى 15 بالأحرف a حتى f- متبوعًا بـ h، أو عددًا عشريًا عاديًا ليس له محرف لاحق، وفيما يلي المخطط الذي يصف ذلك: سيدخل الفرع العلوي الثنائي عند مطابقة ذلك التعبير حتى لو لم يحوي الدخل على عدد ثنائي، حيث سيصبح من الواضح عند المحرف 3 مثلًا أننا في الفرع الخاطئ عند مطابقة السلسلة "103"، فعلى الرغم تطابق السلسلة للتعبير، إلا أنها لا تطابق الفرع الذي نحن فيه. يبدأ هنا المطابِق matcher بالتعقب الخلفي، فيتذكر موضعه الحالي عند دخول فرع ما -وهو بداية السلسلة في هذه الحالة، بعد صندوق "حد الكلمة" الأول في المخطط-، وذلك ليستطيع العودة والنظر في فرع آخر إذا لم ينجح الفرع الحالي. سيجرب الفرع الخاص بالأرقام الست عشرية في حالة السلسلة النصية "103" إذا وصل إلى المحرف 3، لكنه سيفشل مجددًا لعدم وجود h بعد العدد، وهنا سيحاول في الفرع الخاص بالأعداد العشرية، وتنجح المطابقة، ويُبلَّغ بها. يتوقف المطابِق عندما يجد مطابقةً تامةً، وهذا يعني أنه حتى لو كان لدينا فروع متعددة يمكنها مطابقة سلسلة نصية ما، فهو لن يُستخدَم إلا الفرع الأول الذي ظهر وفقًا لترتيبه في التعبير النمطي. ويحدث التعقب الخلفي أيضًا لعوامل التكرار، مثل + و*، فإذا طابقنا ‎/^.*x/‎ مع "abcxe"، فسيحاول الجزء ‎.*‎ أخذ السلسلة كلها أولًا، لكن سيدرك المحرك أنه يحتاج إلى x كي يطابق النمط، وبما أنه لا توجد x بعد نهاية السلسلة، فسيحاول عامل النجمة المطابقة من غير المحرف الأخير، لكن لا يعثر المطابِق على x بعد abcx، فيعود أدراجه بالتعقب الخلفي ليطابق عامل النجمة مع abc فقط، وهنا يجد x حيث يحتاجها، ويبلِّغ بمطابقة ناجحة من الموضع 0 حتى 4. قد يحدث ونكتب تعبيرًا نمطيًا ينفذ الكثير من عمليات التعقب الخلفي، وهنا تحدث مشكلة حين يستطيع النمط مطابقة جزء من الدخل بطرق عديدة مختلفة، فإذا لم ننتبه عند كتابة تعبير نمطي لعدد ثنائي، فقد نكتب شيئًا مثل ‎/([01]+)+b/‎. عندما يحاول التعبير مطابقة سلسلة طويلة من الأصفار والآحاد التي ليس لها لاحقة b، فسيمر المطابِق على الحلقة الداخلية حتى تنتهي الأرقام، ثم يلاحظ عدم وجود المحرف b، فينفِّذ تعقبًا خلفيًا لموضع واحد فقط، ثم يمر على الحلقة الخارجية مرةً واحدةً قبل أن يستسلم ويعود أدراجه ليتعقب الحلقة الداخلية مرةً أخرى، كما سيظل يحاول جميع الطرق الممكنة عبر هاتين الحلقتين، وهذا يعني مضاعفة مقدار العمل مع كل محرف إضافي، فلو أضفنا بعض العشرات من المحارف، لاستغرقت عملية المطابقة إلى ما لا نهاية. التابع replace تحتوي قيم السلاسل النصية على التابع replace الذي يمكن استخدامه لاستبدال سلسلة نصية بجزء من سلسلة أخرى. console.log("papa".replace("p", "m")); // → mapa يمكن أن يكون الوسيط الأول تعبيرًا نمطيًا، وعندئذ يُستبدل التطابق الأول للتعبير النمطي؛ أما إذا أضيف الخيار g -اختصارًا لـ global- إلى التعبير النمطي، فتُستبدل جميع التطابقات. console.log("Borobudur".replace(/[ou]/, "a")); // → Barobudur console.log("Borobudur".replace(/[ou]/g, "a")); // → Barabadar لو كان لدينا وسيط إضافي للتابع replace بحيث نختار منه استبدال تطابق واحد أو جميع التطابقات، لكان أفضل من الاعتماد على خاصية للتعبير النمطي، بل لو كان من خلال توفير تابع مختلف باسم replaceAll لكان أفضل. ملاحظة: أضيف دعم حديث للغة جافاسكربت يحل النقطة السابقة وأصبحت تدعم التابع replaceAll لتبديل كل التطابقات في النص. يأتي مكمن القوة في استخدام التعابير النمطية مع التابع replace من حقيقة استطاعتنا الإشارة إلى المجموعات المطابَقة في السلسلة النصية البديلة، فمثلًا، لدينا سلسلة كبيرة تحتوي على أسماء أشخاص، بحيث يحتوي كل سطر على اسم واحد، ويبدأ بالاسم الأخير، ثم الاسم الأول، أي بالصورة: Lastname, Firstname إذا أردنا التبديل بين تلك الأسماء وحذف الفاصلة الأجنبية التي بين كل منها لنحصل على الصورة Firstname Lastname، فنستطيع استخدام الشيفرة التالية: console.log( "Mohsin, Samira\nFady, Eslam\nSahl, Hasan" .replace(/(\w+), (\w+)/g, "$2 $1")); // → Samira Mohsin // Eslam Fady // Hasan Sahl يشير كل من ‎$1، و‎$2 في السلسلة البديلة إلى المجموعات المحاطة بأقواس في النمط، ويحل النص الذي يطابق المجموعة الأولى محل ‎$1، كما يحل النص الذي يطابق المجموعة الثانية محل ‎$2، وهكذا حتى نصل إلى ‎$9؛ أما التطابق كله فيمكن الإشارة إليه باستخدام ‎$&‎. من الممكن تمرير دالة بدلًا من سلسلة نصية على أساس وسيط ثاني إلى التابع replace، حيث تُستدعى الدالة لكل استبدال مع المجموعات المطابَقة والتطابق كله على أساس وسائط arguments، وتُدخَل قيمتها المعادة في السلسلة الجديدة، أي كما في المثال التالي: let s = "the cia and fbi"; console.log(s.replace(/\b(fbi|cia)\b/g, str => str.toUpperCase())); // → the CIA and FBI وهذا مثال آخر: let stock = "1 lemon, 2 cabbages, and 101 eggs"; function minusOne(match, amount, unit) { amount = Number(amount) - 1; if (amount == 1) { // only one left, remove the 's' unit = unit.slice(0, unit.length - 1); } else if (amount == 0) { amount = "no"; } return amount + " " + unit; } console.log(stock.replace(/(\d+) (\w+)/g, minusOne)); // → no lemon, 1 cabbage, and 100 eggs يأخذ المثال أعلاه سلسلةً نصيةً، ويبحث عن حالات حدوث عدد متبوع بكلمة أبجدية رقمية، ويُعيد سلسلةً نصيةً، إذ تكون في كل حالة من تلك الحالات أُنقصت بمقدار 1. ستكون المجموعة ‎(\d+)‎ هي الوسيط amount للدالة، وتقيَّد المجموعة ‎(\w+)‎ بالوسيط unit، وتحوِّل الدالة الوسيط amount إلى عدد، وينجح ذلك بما أنه طابَق ‎\d+‎، كما تُجري بعض التعديلات في حالة إذا كان المتبقي صفر أو واحد فقط. الجشع Greed من الممكن استخدام replace لكتابة دالة تحذف جميع التعليقات من شيفرة جافاسكربت، لننظر في محاولة أولية لها: function stripComments(code) { return code.replace(/\/\/.*|\/\*[^]*\*\//g, ""); } console.log(stripComments("1 + /* 2 */3")); // → 1 + 3 console.log(stripComments("x = 10;// ten!")); // → x = 10; console.log(stripComments("1 /* a */+/* b */ 1")); // → 1 1 يطابق الجزء الذي يسبق العامل or محرفي شرطة مائلة متبوعتين بعدد من محارف لا تكون محارف سطر جديد؛ أما الجزء المتعلِّق بالتعليقات متعددة الأسطر فيملك بعض التفصيل، إذ نستخدم [^] -أيّ محرف ليس ضمن مجموعة المحارف الفارغة- على أساس طريقة لمطابقة أي محرف، غير أننا لا نستطيع استخدام محرف النقطة هنا لاحتواء التعليقات الكتلية على عدة أسطر، ولا يطابق محرف النقطة محارف السطر الجديد. لكن إذا نظرنا إلى خرج السطر الأخير فسنرى أنه خطأ نوعًا ما، وذلك أنّ الجزء ‎[^]*‎ من التعبير سيطابِق كل ما يستطيع مطابقته كما وضحنا في القسم الخاص بالتعقب الخلفي، فإذا تسبب ذلك في فشل الجزء التالي من التعبير، فسيعود المطابِق محرفًا واحدًا إلى الوراء، ثم يحاول مرةً أخرى من هناك، وهو في هذا المثال يحاول مطابقة بقية السلسلة، ثم يعود إلى الوراء من هناك. سيجد الحدث ‎*/‎ بعد العودة أربعة محارف إلى الوراء ويطابقها، وليس هذا ما أردنا، إذ كنا نريد مطابقة تعليق واحد، وليس العودة إلى نهاية الشيفرة لنجد نهاية آخر تعليق كتلي. نقول بسبب هذا السلوك أنّ عوامل التكرار مثل + و* و? و{} هي عوامل جشعة greedy، أي تطابق كل ما تستطيع مطابقته وتتعقب خلفيًا من هناك، لكن إذا وضعنا علامة استفهام بعد تلك العوامل لتصير هكذا ?+ و?* و?? و?{}، فسننفي عنها صفة الجشع لتطابق أقل ما يمكن، ولا تطابِق أكثر إلا كان النمط الباقي لا يناسب تطابقًا أصغر. هذا هو عين ما نريده في هذه الحالة، فبجعل عامل النجمة يطابق أصغر امتداد من المحارف التي تقودنا إلى ‎*/‎، فسنستهلك تعليقًا كتليًا واحدًا فقط. function stripComments(code) { return code.replace(/\/\/.*|\/\*[^]*?\*\//g, ""); } console.log(stripComments("1 /* a */+/* b */ 1")); // → 1 + 1 نستطيع على ذلك نسْب الكثير من الزلات البرمجية bugs في برامج التعابير النمطية إلى استخدام عامل جشِع من غير قصد، في حين أنّ استخدام عامل غير جشِع أفضل، لهذا يجب النظر في استخدام النسخة الغير جشعة من العامل أولًا عند استخدام أحد عوامل التكرار. إنشاء كائنات RegExp ديناميكيا ستكون لدينا حالات لا نعرف فيها النمط الذي يجب مطابقته عند كتابة الشيفرة، فمثلًا، نريد البحث عن اسم المستخدِم في جزء من النص، وإحاطته بمحرفي شرطة سفلية لإبرازه عما حوله، إذ لن نستطيع استخدام الترميز المبني على الشرطة المائلة لأننا لن نعرف الاسم إلا عند تشغيل البرنامج فعليًا، لكن نستطيع رغم ذلك بناء سلسلة نصية، واستخدام باني RegExp عليها، كما في المثال التالي: let name = "Saad"; let text = "Saad is a suspicious character."; let regexp = new RegExp("\\b(" + name + ")\\b", "gi"); console.log(text.replace(regexp, "_$1_")); // → _Saad_ is a suspicious character. يجب استخدام شرطتين خلفيتين مائلتين عند إنشاء علامات الحدود ‎\b، وذلك لعدم كتابتها في تعبير نمطي محاط بشرطات مائلة، وإنما في سلسلة نصية عادية، كذلك يحتوي الوسيط الثاني للباني RegExp على خيارات للتعبير النمطي، وهي "gi" في هذه الحالة لتشير إلى عمومها global وعدم حساسيتها لحالة المحارف case insensitive. إذا احتوى اسم المستخدِم على محارف غريبة مثل "dea+hl[]rd"، فسيتسبب هذا في تعبير نمطي غير منطقي، وسنحصل على اسم مستخدِم لا يطابق اسم المستخدِم الفعلي. سنضيف شرطات مائلة خلفية قبل أي محرف يملك معنىً خاص به لحل هذه المشكلة. let name = "dea+hl[]rd"; let text = "This dea+hl[]rd guy is super annoying."; let escaped = name.replace(/[\\[.+*?(){|^$]/g, "\\$&"); let regexp = new RegExp("\\b" + escaped + "\\b", "gi"); console.log(text.replace(regexp, "_$&_")); // → This _dea+hl[]rd_ guy is super annoying. التابع search لا يمكن استخدام تعبير نمطي لاستدعاء التابع indexOf على سلسلة نصية، والحل هو استخدام التابع search الذي يتوقع تعبيرًا نمطيًا، حيث يُعيد أول فهرس يجده التعبير كما يفعل indexOf، أو يُعيد ‎-1 إذا لم يجده. console.log(" word".search(/\S/)); // → 2 console.log(" ".search(/\S/)); // → -1 لا توجد طريقة في التابع search لاختيار بدء المطابقة عند إزاحة offset بعينها، على عكس indexOf الذي يملكها في الوسيط الثاني، إذ تُعَدّ مفيدةً في بعض الأحيان. خاصية lastIndex لا يوفر التابع exec طريقةً سهلةً لبدء البحث من موضع بعينه في السلسلة النصية، شأنه في ذلك شأن التابع search، والطريقة التي يوفرها لذلك موجودة، لكنها ليست سهلة. تحتوي كائنات التعبير النمطي على خصائص إحداها هي source التي تحتوي على السلسلة التي أُنشئ منها التعبير، وثمة خاصية أخرى هي lastIndex التي تتحكم في موضع بدء التطابق التالي، وإن كان في حالات محدودة، وحتى حينئذ يجب أن يكون كل من الخيار العام g وخيار y اللزج sticky مفعَّلين في التعبير النمطي، كما يجب وقوع التطابق من خلال التابع exec. كان من الممكن هنا السماح بتمرير وسيط إضافي إلى exec. let pattern = /y/g; pattern.lastIndex = 3; let match = pattern.exec("xyzzy"); console.log(match.index); // → 4 console.log(pattern.lastIndex); // → 5 إذا نجح التطابق، فسيحدِّث الاستدعاء إلى exec خاصية lastIndex إلى النقطة التي تلي التطابق تلقائيًا؛ أما إذا لم يُعثر على تطابق، فستُضبِط خاصية lastIndex على الصفر، حيث يكون هو القيمة في كائن التعبير النمطي الذي سيُبنى تاليًا. يكون الفرق بين الخيارين العام واللزج هو عدم نجاح التطابق حالة الخيار اللزج إلا عندما يبدأ من lastIndex مباشرةً؛ أما في حالة الخيار العام، فسيبحث عن موضع يمكن بدء التطابق عنده. let global = /abc/g; console.log(global.exec("xyz abc")); // → ["abc"] let sticky = /abc/y; console.log(sticky.exec("xyz abc")); // → null تتسبب تلك التحديثات التلقائية لخاصية lastIndex في مشاكل عند استخدام قيمة تعبير نمطي مشتركة لاستدعاءات exec متعددة، فقد يبدأ تعبيرنا النمطي عند فهرس من مخلَّفات استدعاء سابق. let digit = /\d/g; console.log(digit.exec("here it is: 1")); // → ["1"] console.log(digit.exec("and now: 1")); // → null كذلك من الآثار اللافتة للخيار العام أنه يغيِّر الطريقة التي يعمل بها التابع match على السلاسل النصية، إذ يبحث عن جميع تطابقات النمط في السلسلة النصية، ويُعيد مصفوفةً تحتوي على السلاسل المطابَقة عندما يُستدعى مع الخيار العام، بدلًا من إعادة مصفوفة تشبه التي يُعيدها exec. console.log("Banana".match(/an/g)); // → ["an", "an"] لهذا يجب الحذر عند التعامل مع التعابير النمطية العامة، واستخدامها في الحالات الضرورية فقط،، مثل الاستدعاءات إلى replace والأماكن التي تريد استخدام lastIndex فيها صراحةً. التكرار على التطابقات يُعَدّ البحث في جميع مرات حدوث النمط في سلسلة نصية بطريقة تعطينا وصولًا إلى كائن المطابقة في متن الحلقة loop body أمرًا شائعًا، ويمكن فعل ذلك باستخدام lastIndex، وexec. let input = "A string with 3 numbers in it... 42 and 88."; let number = /\b\d+\b/g; let match; while (match = number.exec(input)) { console.log("Found", match[0], "at", match.index); } // → Found 3 at 14 // Found 42 at 33 // Found 88 at 40 يستفيد هذا من كون قيمة تعبير الإسناد = هي القيمة المسندَة، لذا ننفذ التطابق عند بداية كل تكرار باستخدام match = number.exec(input)‎ على أساس شرط في تعليمة while، ثم نحفظ النتيجة في رابطة binding، ونوقف التكرار إذا لم نجد تطابقات أخرى. تحليل ملف INI لنقل أننا نكتب برنامجًا يجمع بيانات عن أعدائنا على الإنترنت، رغم أننا لن نكتبه حقًا وإنما يهمنا الجزء الذي يقرأ ملف التهيئة، على أساس مثال على مشكلة تحتاج إلى التعابير النمطية، إذ سيبدو ملف التهيئة كما يلي: searchengine=https://duckduckgo.com/?q=$1 spitefulness=9.7 ; تُسبق التعليقات بفاصلة منقوطة... ; يختص كل قسم بعدو منفصل [larry] fullname=Larry Doe type=kindergarten bully website=http://www.geocities.com/CapeCanaveral/11451 [davaeorn] fullname=Davaeorn type=evil wizard outputdir=/home/marijn/enemies/davaeorn تكون القواعد الحاكمة لهذه الصيغة -وهي صيغة مستخدمة بكثرة، ويطلق عليها اسم INI- كما يلي: تُتجاهل الأسطر الفارغة والأسطر البادئة بفاصلة منقوطة. تبدأ الأسطر المغلَّفة بالقوسين المعقوفين [ ] قسمًا جديدًا. تضيف الأسطر التي تحتوي على معرِّف أبجدي-رقمي متبوع بمحرف = إعدادًا setting إلى القسم الحالي. لا يُسمح بأي شيء غير ما سبق، ويُعَدّ ما سواه غير صالح. مهمتنا هنا هي تحويل سلسلة نصية مثل هذه إلى كائن تحمل خصائصه سلاسل نصية للإعدادات المكتوبة قبل ترويسة القسم الأول، وكائنات فرعية للأقسام، بحيث يحمل كل كائن فرعي إعدادات القسم الخاص به، وبما أنه يجب معالجة الصيغة سطرًا سطرًا، فمن الجيد تقسيم الملف إلى أسطر منفصلة، مستفيدين من التابع split الذي تعرضنا له في هياكل البيانات: الكائنات والمصفوفات في جافاسكريبت. لا تقتصر بعض أنظمة التشغيل على محرف السطر الجديد لفصل الأسطر، وإنما تستخدِم محرف الإرجاع carriage return متبوعًا بسطر جديد ‎"\r\n"‎، وبما أنّ التابع split يسمح بالتعبير النمطي على أساس وسيط، فنستطيع استخدام تعبير نمطي مثل ‎/\r?\n/‎ للتقسيم بطريقة تسمح بوجود كل من ‎"\n"‎ و‎"\r\n"‎ بين الأسطر. function parseINI(string) { // ابدأ بكائن ليحمل حقول المستوى العلوي let result = {}; let section = result; string.split(/\r?\n/).forEach(line => { let match; if (match = line.match(/^(\w+)=(.*)$/)) { section[match[1]] = match[2]; } else if (match = line.match(/^\[(.*)\]$/)) { section = result[match[1]] = {}; } else if (!/^\s*(;.*)?$/.test(line)) { throw new Error("Line '" + line + "' is not valid."); } }); return result; } console.log(parseINI(` name=Vasilis [address] city=Tessaloniki`)); // → {name: "Vasilis", address: {city: "Tessaloniki"}} تمر الشيفرة على أسطر الملف وتبني الكائن، كما تخزَّن الخصائص التي في القمة داخل الكائن مباشرةً، بينما تخزن الخصائص الموجودة في الأقسام داخل كائن قسم مستقل، كما تشير الرابطة section إلى كائن القسم الحالي. لدينا نوعان من الأسطر المميزة، وهما ترويسة الأقسام، أو أسطر الخصائص، فإذا كان السطر خاصيةً عاديةً، فسيخزَّن في الموضع الحالي؛ أما إذا كان ترويسةً لقسم، فسيُنشأ كائن قسم جديد وتُضبط section لتشير إليه. نضمن بالاستخدام المتكرر لمحرفي ^ و$، مطابقة التعبير للسطر كاملًا وليس جزءًا منه فقط، كما ستعمل الشيفرة عند إهمالهما، لكنها ستتصرف مع بعض المدخلات بغرابة، وهو الأمر الذي سيكون زلةً bug يصعب تعقبها وإصلاحها. يُعَدّ النمط ‎if (match = string.match(...))‎ شبيهًا بما سبق في شأن استخدام الإسناد على أساس شرط لتعليمة while، فلن نكون على يقين من نجاح استدعاء match، لذا لا نستطيع الوصول إلى الكائن الناتج إلا داخل تعليمة if تختبر ذلك، ولكي لا نقطع سلسلة الصيغ else if، فسنسند نتيجة التطابق إلى رابطة، وسنستخدم هذا التعيين على أساس اختبار لتعليمة if مباشرةً. تتحقق الدالة من السطر باستخدام التعبير ‎/^\s*(;.*)?$/‎ إذا لم يكن ترويسةً لقسم أو خاصيةً ما، إذ تتحقق من أنه تعليق أو سطر فارغ، حيث يطابق الجزء الذي بين الأقواس التعليقات، ثم تتأكد ? أنه يطابق الأسطر التي تحتوي على مسافة بيضاء فقط، وإذا وُجد سطر لا يطابق أي صيغة من الصيغ المتوقعة، فسترفع الدالة اعتراضًا exception. المحارف الدولية كان اتجاه تصميم جافاسكربت في البداية نحو سهولة الاستخدام، وقد ترسخ ذلك الاتجاه مع الوقت إلى أن صار هو السمة الأساسية للغة ومعيارًا لتحديثاتها، لكن أتت هذه السهولة بعواقب لم تكن في الحسبان وقتها، إذ تُعَدّ تعابير جافاسكربت النمطية غبيةً لغويًا، فالمحرف الكلمي بالنسبة لها هو واحد من 26 محرفًا فقط، وهي المحارف الموجودة في الأبجدية اللاتينية بحالتيها الصغرى والكبرى، أو أرقامًا عشريةً، أو محرف الشرطة السفلية _؛ أما بالنسبة لأيّ شيء غير ذلك، مثل é أو β، فلن تطابق ‎\w رغم أنها محارف كلمية، لكنها ستطابق الحالة الكبرى منها ‎\W التي تشير إلى التصنيف غير الكلمي nonword category. تجدر الإشارة إلى ملاحظة غريبة في شأن محرف المسافة البيضاء العادية ‎\s، إذ لا تعاني من هذه المشكلة، وتطابق جميع المحارف التي يَعُدّها معيار اليونيكود محارف مسافة بيضاء، بما في ذلك المسافة غير الفاصلة nonbreaking space، والفاصلة المتحركة المنغولية Mongolian vowel separator. أيضًا، من المشاكل التي سنواجهها مع التعابير النمطية في جافاسكربت أنها لا تعمل على المحارف الحقيقية وإنما تعمل على الأعداد البِتّية للمحارف code units كما ذكرنا في الدوال العليا في جافاسكريبت، وبالتالي سيكون سلوك المحارف المكونة من عددين بِتّيين غريبًا، وعلى خلاف ما نريد. console.log(/?{3}/.test("???")); // → false console.log(/<.>/.test("<?>")); // → false console.log(/<.>/u.test("<?>")); // → true المشكلة أنّ ? التي في السطر الأول تعامَل على أنها عددين بِتّيين، ولا يطبَّق الجزء {3} إلا على العدد الثاني. وبالمثل، تطابق النقطة عددًا بِتَيًا واحدًا، وليس العددين اللذَين يكونان الرمز التعبيري للوردة، كما يجب إضافة خيار اليونيكود u للتعبير النمطي كي يعامَل مثل تلك المحارف على الوجه الذي ينبغي. سيظل السلوك الخاطئ للتعبير النمطي هو الافتراضي للأسف، لأنّ التغيير قد يتسبب في مشاكل للشيفرة الموجودة والتي تعتمد عليه، ومن الممكن استخدام ‎\p في تعبير نمطي مفعّل فيه خيار اليونيكود لمطابقة جميع المحارف التي يسند اليونيكود إليها خاصية معطاة، رغم أنّ هذه الطريقة معتمدة حديثًا ولم تُستخدم كثيرًا بعد. console.log(/\p{Script=Greek}/u.test("α")); // → true console.log(/\p{Script=Arabic}/u.test("α")); // → false console.log(/\p{Alphabetic}/u.test("α")); // → true console.log(/\p{Alphabetic}/u.test("!")); // → false يعرِّف اليونيكود عددًا من الخصائص المفيدة رغم أنّ إيجاد الخاصية التي نحتاجها قد لا يكون أمرًا سهلًا في كل مرة، حيث يمكن استخدام الصيغة ‎\p{Property=Value}‎ لمطابقة أيّ محرف له قيمة معطاة لتلك الخاصية، وإذا أُهمل اسم الخاصية كما في ‎\p{Name}‎، فسيُفترض الاسم إما خاصيةً بِتّيةً مثل Alphabetic، أو فئةً مثل Number. خاتمة التعبيرات النمطية هي كائنات تمثل أنماطًا في السلاسل النصية، وتستخدم لغتها الخاصة للتعبير عن تلك الأنماط. التعبير النمطي دلالته /abc/ تسلسل من المحارف /[abc]/ أيّ محرف في مجموعة محارف ‎/[^abc]/‎ أيّ محرف ليس في مجموعة ما من المحارف /[0-9]/ أيّ محرف من مجال ما من المحارف ‎/x+/‎ مرة حدوث واحدة أو أكثر للنمط x ‎/x+?/‎ مرة حدوث أو أكثر غير جشعة ‎/x*/‎ حدوث صفري أو أكثر. ‎/x?/‎ حدوث صفري أو حدوث لمرة واحدة ‎/x{2,4}/‎ حدوث لمرتَين إلى أربعة مرات /(abc)/ مجموعة `/a b c/` أيّ نمط من بين أنماط متعددة ‎/\d/‎ أيّ محرف رقمي ‎/\w/‎ محرف أبجدي رقمي alphanumeric، أي محرف كلمة ‎/\s/‎ أيّ محرف مسافة بيضاء /./ أيّ محرف عدا الأسطر الجديدة ‎/\b/‎ حد كلِمي word boundary /^/ بداية الدخل /$/ نهاية الدخل يملك التعبير النمطي التابع test للتحقق هل السلسلة المعطاة مطابقة أم لا، كما يملك التابع exec الذي يُعيد مصفوفةً تحتوي على جميع المجموعات المطابِقة إذا وُجدت مطابقات، ويكون لتلك المصفوفة خاصية index التي توضِّح أين بدأت المطابقة. تملك السلاسل النصية التابع match الذي يطابقها مع تعبير نمطي، وتابع search الذي يبحث عن التعابير النمطية ثم يُعيد موضع بداية التطابق فقط، كما تملك السلاسل النصية تابعًا اسمه replace، حيث يستبدِل سلسلةً نصيةً أو دالةً بتطابقات النمط. يمكن امتلاك التعابير النمطية خيارات تُكتب بعد شرطة الإغلاق المائلة، إذ يجعل الخيار i التطابق حساسًا لحالة الأحرف، كما يجعل الخيار g التعبير عامًا global، ويمكِّن التابع replace من استبدال جميع النسخ بدلًا من النسخة الأولى فقط؛ أما الخيار y فيجعله لزجًا، أي لن يتجاوز جزءًا من السلسلة أثناء البحث عن تطابق، كذلك يفعِّل الخيار u وضع اليونيكود الذي يصلح لنا عددًا من المشاكل المتعلقة بمعالجة المحارف التي تأخذ أكثر من عددين بتيين. وهكذا فإنّ التعابير النمطية أشبه بسكين حاد لها مقبض غريب الشكل، فهي تيسِّر المهام التي ننفذها كثيرًا، لكن قد تصبح صعبة الإدارة حين نستخدمها في مشاكل معقدة، ومن الحكمة تجنب حشر الأشياء التي لا تستطيع التعبيرات النمطية التعبير عنها بسهولة. تدريبات ستجد نفسك لا محالةً أثناء العمل على هذه التدريبات أمام سلوكيات محيِّرة للتعابير النمطية، فمن المفيد عندئذ إدخال تعبيراتك النمطية في أداة مثل https://debuggex.com لترى إن كان تصورها المرئي يوافق السلوك الذي أردت أم لا، ولترى كيف تستجيب لسلاسل الدخل المختلفة. Regexp golf Code golf هو مصطلح مستخدم للعبة تحاول التعبير عن برنامج معيَّن بأقل عدد ممكن من المحارف، وبالمثل، يكون regexp golf عملية كتابة تعابير نمطية، بحيث تكون أصغر ما يمكن، وتطابق النمط المعطى فقط. اكتب تعبيرًا نمطيًا لكل عنصر مما يلي، بحيث يتحقق من حدوث أيّ سلسلة نصية فرعية داخل السلسلة النصية الأم، ويجب على التعبير النمطي مطابقة السلاسل المحتوية على إحدى السلاسل الفرعية التي ذكرناها. لا تشغل بالك بحدود الكلمات إلا إذا ذُكر ذلك صراحةً، وإذا نجح تعبيرك النمطي فانظر إن كنت تستطيع جعله أصغر. Car وcat. Pop وprop. Ferret وferry وferrari. أيّ كلمة تنتهي بـ ious. محرف مسافة بيضاء متبوع بنقطة، أو فاصلة أجنبية، أو نقطتين رأسيتين، أو فاصلة منقوطة. كلمة أكبر من ستة أحرف. كلمة ليس فيها الحرف e أو E. استرشد بالجدول الذي في خاتمة المقال أعلاه، واختبر كل حل ببعض السلاسل النصية. تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen. // املأ التعابير النمطية التالية verify(/.../, ["my car", "bad cats"], ["camper", "high art"]); verify(/.../, ["pop culture", "mad props"], ["plop", "prrrop"]); verify(/.../, ["ferret", "ferry", "ferrari"], ["ferrum", "transfer A"]); verify(/.../, ["how delicious", "spacious room"], ["ruinous", "consciousness"]); verify(/.../, ["bad punctuation ."], ["escape the period"]); verify(/.../, ["Siebentausenddreihundertzweiundzwanzig"], ["no", "three small words"]); verify(/.../, ["red platypus", "wobbling nest"], ["earth bed", "learning ape", "BEET"]); function verify(regexp, yes, no) { // تجاهل التدريبات غير المكتملة if (regexp.source == "...") return; for (let str of yes) if (!regexp.test(str)) { console.log(`Failure to match '${str}'`); } for (let str of no) if (regexp.test(str)) { console.log(`Unexpected match for '${str}'`); } } أسلوب الاقتباس تخيَّل أنك كتبت قصةً، واستخدمت علامات الاقتباس المفردة فيها لتحديد النصوص التي قالتها الشخصيات فيها، وتريد الآن استبدال علامات الاقتباس المزدوجة بكل تلك العلامات المفردة، لكن مع استثناء الكلمات التي تكون فيها العلامة المفردة لغرض مختلف مثل كلمة aren't. فكر في نمط يميز هذين النوعين من استخدامات الاقتباس، وصمم استدعاءً إلى التابع replace الذي ينفذ عملية الاستبدال المناسبة. تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen. let text = "'أنا الطاهي،' he said, 'إنها وظيفتي.' "; // غيِّر هذا الاستدعاء. console.log(text.replace(/A/g, "B")); // → "أنا الطاهي،" he said, "إنها وظيفتي." إرشادات للحل يكون الحل البديهي هنا هو استبدال محرف غير كلمي nonword بعلامات الاقتباس على الأقل من جانب واحد، كما في ‎/\W'|'\W/‎، لكن سيكون عليك أخذ بداية السطر ونهايته في حسابك. كذلك يجب ضمان أنّ الاستبدال سيشمل المحارف التي طابقها نمط ‎\W كي لا تُنسى، ويمكن فعل ذلك بتغليفها في أقواس، ثم تضمين مجموعاتها في السلسلة النصية البديلة (1$‎ و2$‎)، كما لا يُستبدل شيء بالمجموعات التي لم تطابَق. الأعداد مرة أخرى اكتب تعبيرًا لا يطابق إلا الأعداد التي لها نسق جافاسكربت، ويجب عليه دعم علامة + أو - قبل العدد، والعلامة العشرية، والصيغة الأسية -أي 5e-3، أو 1E10-، مع علامتي موجب أو سالب قبل الأس. لاحظ عدم اشتراط وجود أرقام قبل العلامة العشرية أو بعدها، لكن لا يمكن أن يكون العدد مكونًا من العلامة العشرية وحدها، أي يسمح بكل من .5 و5. في جافاسكربت، لكن لا يُسمح بـ .. تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen. // املأ التعبير النمطي التالي. let number = /^...$/; // الاختبارات: for (let str of ["1", "-1", "+15", "1.55", ".5", "5.", "1.3e2", "1E-4", "1e+12"]) { if (!number.test(str)) { console.log(`Failed to match '${str}'`); } } for (let str of ["1a", "+-1", "1.2.3", "1+1", "1e4.5", ".5.", "1f5", "."]) { if (number.test(str)) { console.log(`Incorrectly accepted '${str}'`); } } إرشادات للحل يجب عدم نسيان الشرطة المائلة الخلفية التي قبل النقطة. يمكن مطابقة العلامة الاختيارية التي قبل العدد وقبل الأس بواسطة ‎[+\-]?‎، أو ‎(\+|-|)‎، والتي تعني موجب، أو سالب، أو لا شيء. يبقى الجزء الأصعب من هذا التدريب مطابقة كل من ‎"5."‎ و‎".5"‎ دون مطابقة "."، وأحد الحلول الممتازة هنا هو استخدام العامل | لفصل الحالتين؛ فإما رقم واحد أو أكثر متبوع اختياريًا بنقطة وصفر، أو أرقام أخرى، أو نقطة متبوعة برقم واحد أو أكثر. وأخيرًا، نريد جعل e حساسة لحالتها؛ فإما نضيف الخيار i للتعبير النمطي أو نستخدم [eE]. ترجمة -بتصرف- للفصل التاسع من كتاب Elequent Javascript لصاحبه Marijn Haverbeke. اقرأ أيضًا المقال السابق: الزلات البرمجية والأخطاء في جافاسكريبت تطويع البيانات في جافاسكربت التخاطب بين نوافذ المتصفح عبر جافاسكريبت
  11. يتكون أبسط برنامج يمكن كتابته من سلسلة من بعض التعليمات statements المتتالية، وأصغرها هو الذي يحتوي تعليمةً برمجيةً واحدةً، حيث تُدخَل التعليمة عادةً في سطر واحد، كما يمكن أن تُقسم على عدة أسطر، وتعرَّف التعليمة بأنها مجموعة من الكلمات والرموز المكتوبة بلغة طبيعية، وسننظر الآن في بعضها، إذ تُظهر الأمثلة التالية ما يجب أن نكتبه في محث بايثون ‎>>>‎، كما تُظهر النتيجة، ثم نشرح ما حدث. عرض الخرج يجب أن نتعلم أولًا كيف نجعل بايثون تعرض المعلومات التي نريدها، فإذا لم تعرض لنا اللغة شيئًا، فكيف نعرف ما فعله الحاسوب؟ وما فائدة الجهد الذي نبذله في البرمجة حينها إذا كنا لا نعرف هل أصبنا أم أخطأنا؟ >>> print('Hello there!') Hello there! انتبه إلى الملاحظة الأولى في المثال أعلاه، وهي أن المسافة التي بين المحث وبين أمر print ليست ضرورية، إذ ستضعها بايثون تلقائيًا إذا نسيناها، وما سنكتبه هو السطر الأول فقط؛ أما السطر الثاني فهو نتيجة البرنامج الذي كتبناه في السطر الأول، وقد عرضه لنا المفسر. أما الملاحظة الثانية فهي أن بايثون حساسة لحالة الأحرف، حيث سنحصل على خطأ إذا كتبنا Print بدلًا من print لأن بايثون تراهما كلمتين مختلفتين، وكذلك الحال بالنسبة لجافاسكربت؛ أما لغة VBScript فلا تهتم لهذا، لكن يجب تعويد النفس على الاهتمام بحالة الأحرف كعادة برمجية حسنة لئلا تقع في أخطاء بسببها إذا انتقلت من لغة برمجية لأخرى. وقد عرض لنا المفسر النص المكتوب بين علامتي الاقتباس لأن الدالة print()‎ تخبر بايثون بعرض تسلسل المحارف H,e,l,l,o, ,t,h,e,r,e,!‎، وهو ما يُعرَف في الأوساط البرمجية باسم السلسلة النصية string، أو سلسلة المحارف النصية، حيث يجب أن تكون تلك المحارف داخل أقواس، وندل على السلسلة النصية بوضعها بين علامات الاقتباس، كما يمكن استخدام علامات الاقتباس المفردة أو المزدوجة في بايثون دون حرج، مما يسمح لنا بإدخال أحد نوعي الاقتباس في سلسلة نصية محاطة بالنوع الآخر، وهذا مفيد في حالة الفاصلة الإنجليزية العليا ': >>> print("Monty Python's Flying Circus has a ' within it...") Monty Python's Flying Circus has a ' within it... أما جافاسكربت وVBScript فحساستان لنوع علامات التنصيص التي يجب استخدامها، لهذا يُنصح فيهما بالالتزام بالعلامات المزدوجة ما أمكن. عرض النتائج الحسابية >>> print(6 + 5) 11 يطبع لنا المثال أعلاه نتيجة العملية الحسابية التي في السطر الأول، والتي جمعنا فيها 5 إلى 6، وقد عرفت بايثون أن هذه المحارف أرقام وعاملتها على هذا الأساس، كما تعرفت على علامة الجمع التي بينهما، فجمعت العددين وأخرجت لنا الناتج، لذا يمكن القول أن بايثون مفيدة كحاسبة للجيب، وهو أحد استخداماتها الكثيرة جدًا، جرب الآن بعض الحسابات الأخرى واستخدم العوامل الحسابية التالية: الطرح (-). الضرب (*). القسمة (/). يمكن دمج عدة تعبيرات حسابية كما يلي: >>> print( ((6 * 5) + (7 - 5)) / (7 + 1) ) 4.0 لاحظ الطريقة التي استخدمنا بها الأقواس لوضع الأعداد في مجموعات، لقد رأت بايثون تلك العملية السابقة كما يلي: ((6 * 5) + (7 - 5)) / (7 + 1) => (30 + 2) / 8 => 32 / 8 => 4 فماذا يحدث لو كتبنا نفس العمليات دون الأقواس؟ >>> print(6 * 5 + 7 - 5 / 7 + 1) 37.2857142857 حصلنا على هذه النتيجة لأن بايثون تنفذ عمليتي الضرب والقسمة قبل الجمع والطرح، لذا رأتها على النحو التالي: (6*5) + 7 - (5/7) + 1 => 30 + 7 - 0.7143 + 1 => 37 - 0.7143 + 1 => 38 - 0.7143 => 37.2857... ومع أن ترتيب بايثون لمعالجة العمليات الحسابية هو نفسه الذي تقضيه قوانين الرياضيات، إلا أنه يختلف عما يجب عليك توقعه كونك مبرمجًا، لأن بعض لغات البرمجة لها قوانينها في ترتيب معالجة العمليات الحسابية، وهو ما يعرف بأسبقية العوامل operator precedence، لذا تأكد من النظر أولًا في توثيق لغة البرمجة التي تتعامل معها لترى أسلوبها الخاص؛ أما بايثون فقاعدتها العامة هي ما يقتضيه الحدس والمنطق، لكن هذا له استثناءاته، مما يجعل استخدام الأقواس خيارًا آمنًا لضمان حصولنا على النتيجة التي نريدها، خاصةً عند التعامل مع حسابات طويلة مثل التي رأيناها في المثال السابق، ومن الأمور التي يجب ملاحظتها أننا إذا استخدمنا عامل القسمة / فسنحصل على النتيجة الدقيقة للعملية، كما يلي: >>> print(5/2) 2.5 فإذا أردنا الحفاظ على القسم الصحيح للناتج فقط فنستخدم عامليْ قسمة متتاليين // لنحصل على العدد الكامل في النتيجة، وستطبع بايثون ناتج القسمة كما يلي: >>> print(5//2) 2 أما إذا أردنا الحصول على باقي عملية القسمة فنستخدم محرف النسبة المئوية %: >>> print(5%2) 1 >>> print(7//4) 1 >>> print(7%4) 3 يُعرَف العامل % باسم عامل الباقي modulo، وقد يكون في بعض لغات البرمجة على الصورة MOD أو نحوها، أما في بايثون فنستطيع الحصول على ناتج القسمة والباقي معًا باستخدام الدالة divmod()‎: >>> print( divmod(7,4) ) (1, 3) هذه العمليات الحسابية للأعداد الصحيحة مهمة للغاية في البرمجة، فمنها نستطيع تحديد كون العدد زوجيًا أم فرديًا من خلال قسمته على 2 ورؤية الباقي (يكون زوجيًا إذا كان الباقي صفرًا)، كما يلي: >>> print( 47 % 2 ) 1 فنحن نعلم الآن أن العدد 47 هو عدد فردي، وقد يكون هذا أمرًا بدهيًا إذ يمكن معرفة هذا دون تكبد عناء لغة برمجية وانتظار خرج عملية فيها، لكن ماذا لو كنا نقرأ بيانات من ملف، أو كان المستخدم يدخل تلك البيانات، ثم يكون على برنامجنا معرفة هل العدد فردي أم زوجي من تلقاء نفسه، عندئذ لن نستطيع التدخل هنا لتقرير ذلك بالنظر في كل عدد، بل سنستخدم عامل الباقي كما فعلنا ليتحقق من المطلوب بدلًا منا، وتلك حالة واحدة فقط من الحالات التي تبرز فيها أهمية هذه العمليات، لذا تدرب عليها إلى أن تتقنها. دمج السلاسل النصية والأعداد >>> print( 'The total is: ', 23+45) The total is: 68 لقد طبعنا سلاسل نصيةً أوأعدادًا فيما سبق، أما الآن فسندمج الاثنين في تعليمة واحدة، فاصلين بينهما بفاصلة إنجليزية ,، ويمكن توسيع تلك الميزة بجمعها مع إحدى خصائص بايثون في إخراج البيانات، وهي سلسلة التنسيق format string: >>> print( "The sum of %d and %d is: %d" % (7,18,7+18)) The sum of 7 and 18 is: 25 تحتوي سلسلة التنسيق على علامات %، غير أنها تختلف عن عامل الباقي الذي تحدثنا عنه من قبل، بل يكون لها معنىً خاص حين تُستخدم في سلسلة كالتي بين أيدينا، وحالات الاستخدام المتعددة تلك تعني أن علينا قراءة المكتوب بانتباه لتحديد سياقه، ثم تحديد وظيفة % فيه، حيث يخبر الحرف d الذي بعد محرف % بايثون بوجوب وضع عدد عشري مكانه، والذي نحصل على قيمته -التي سنستخدمها لتحل محله- من القيم الموجودة داخل التعبير الذي بين الأقواس، والذي كتِب بعد علامة % المنفردة في نهاية العبارة، ومن المهم أن يكون عدد القيم التي في القوس النهائي مطابقًا لعدد علامات % الموجودة في السلسلة النصية. توجد حروف أخرى يمكن وضعها بعد علامة %، ويستعمَل كل منها لغرض مختلف، منها: ‎%S للسلاسل النصية. ‎%x للأعداد الست عشرية hexadecimal. %0.2f للأعداد الحقيقية التي لها منزلتان عشريتان كحد أقصى. %04d لإزاحة العدد بأصفار قبله ليكون من أربعة منازل عشرية. انظر توثيق بايثون للمزيد من المعلومات. ونستطيع طباعة أي كائن بايثون باستخدام دالة الطباعة، حتى لو كانت النتيجة على غير ما كنا نريد، فربما تكون وصفًا لنوع الكائن، لكننا نستطيع طباعة ذلك على أي حال. >>> print( "The sum of {:d} and {:d} is: {:d}".format(7,18,7+18) The sum of 7 and 18 is: 25 زيادة إمكانيات اللغة >>> import sys إذا كتبنا السطر أعلاه في محث بايثون ثم ضغطنا على زر الإدخال، فلن نرى شيئًا على الشاشة، غير أن هذا لا يعني عدم حدوث شيء ما في الخلفية، ولشرح ما حدث بعد كتابة هذه الأمر سننظر أولًا إلى معمارية بايثون نفسها، وحتى لو لم تكن تستخدم بايثون فتابع الشرح، إذ ستكون ثمة آليات مشابهة في اللغة التي تستخدمها. عند بدء لغة بايثون تتاح لنا عدة دوال وأوامر، وهذه الأوامر مدمجة في اللغة وتسمى بالمضمَّنات built-ins، لأنها مبنية داخل نسيج اللغة نفسها، غير أن بايثون تستطيع توسيع قائمة الدوال المتاحة بدمج وحدات توسيع extension modules فيها، وهو يشبه شراء المرء لأداة جديدة من متجر العُدد والآلات ليضيفها إلى مجموعته المنزلية، وفي مثالنا فإن الآلة هي sys، وقد وضعتها العملية import داخل صندوق الآلات الذي سنستخدمه؛ أما حقيقة ما يفعله هذا الأمر فهو إتاحة "أدوات" جديدة على شكل دوال للغة بايثون، تُعرَّف داخل وحدة اسمها sys، وتلك طريقة توسيع بايثون لتنفيذ أي شيء غير مدمج في النظام الأساسي، وستجد أكثر من مئة وحدة في المكتبة القياسية standard library التي تحصل عليها مع بايثون، كما يمكنك إنشاء وحداتك الخاصة واستيرادها واستخدامها مثل الوحدات التي وفرتها بايثون عند تثبيتها بالضبط، وسنعود إلى هذا الأمر لاحقًا. كذلك ستجد مزيدًا من الوحدات التي يمكن تحميلها من الإنترنت، فإذا بدأت مشروعًا لا تغطيه الوحدات التي في المكتبة القياسية، فانظر في الإنترنت أولًا فلعلك تجد شيئًا يساعدك. الخروج السريع لننظر الآن في كيفية استخدام تلك الأدوات التي أدخلناها، فإذا كتبنا الأمر التالي في محث بايثون؛ فسنجعل بايثون تنهي نفسها وتخرج، لأننا نكون قد نفّذنا الدالة exit المعرَّفة في وحدة sys. >>> sys.exit() لاحظ أننا نخرج من بايثون عادةً بكتابة محرف نهاية الملف End Of File واختصاره EOF في محث بايثون، وهو CTRL+Z على ويندوز أو CTRL+D على أنظمة يونكس Unix؛ أما إذا كنت تستخدم بيئة تطوير فتخرج من قائمة File ثم Exit، وإذا حاولنا تنفيذ هذا في أداة تطوير مثل IDLE؛ فإن الأداة تنتبه لمحاولة الخروج وتعرض رسالةً تقول شيئًا ما حول SystemExit، لكن لا تشغل بالك بها، فهذا يعني أن البرنامج يعمل وأن الأداة تحاول توفير الوقت عليك لئلا تبدأ من الصفر مرةً أخرى. لاحظ أن exit لها أقواس حولها، وذلك لأنها دالة معرفة في وحدة sys، حيث سنحتاج إلى وضع هذه الأقواس عند استدعاء دالة بايثون حتى لو لم تحتوي الأقواس نفسها على أي شيء، فإذا كتبنا sys.exit دون الأقواس، فستستجيب بايثون لنا بإخبارنا أن exit دالةٌ، بدلًا من تنفيذ الدالة نفسها. أخيرًا، لاحظ أن التعليمتين الأخيرتين مفيدتان عند استخدامهما معًا، أي أننا سنكتب ما يلي للخروج من بايثون بدلًا من EOF: >>> import sys >>> sys.exit() هكذا نكون قد كتبنا تسلسلًا بسيطًا من تعليمتين، ونكون قد خطونا قليلًا نحو البرمجة الحقيقية. استخدام جافاسكربت لا توجد طريقة سهلة في جافاسكربت لنكتب الأوامر ونراها تنفَّذ مباشرةً كما في بايثون، لكن نستطيع كتابة جميع الأوامر البسيطة أعلاه في ملف HTML واحد ثم تحميلها إلى المتصفح، وسنرى كيف تبدو حينئذ في جافاسكربت: <html><body> <script type="text/javascript"> document.write('Hello there!<br />'); document.write("Monty Python\'s Flying Circus has a \' within it<br />"); document.write(6+5); document.write("<br />"); document.write( ((8 * 4) + (7 - 3)) / (2 + 4) ); document.write("<br />"); document.write( 5/2 ); document.write("<br />"); document.write( 5 % 2 ); </script> </body></html> سيكون الخرج كما يلي: Hello there! Monty Python's Flying Circus has a ' within it 11 6 2.5 1 نستخدم document.write لإخراج النص إلى نافذة المتصفح، وهذا يكافئ تقريبًا دالة print في بايثون، وسنرى قريبًا طريقةً مختلفةً قليلًا في VBSCript لعرض المخرجات في المتصفح في المثال التالي. لاحظ كيف اضطررنا إلى كتابة ‎<br />‎ كي نجبر البرنامج أن يعرض الخرج التالي في سطر جديد، وذلك لأن جافاسكربت تكتب مخرجاتها في صورة HTML، والتي تعرض السطر بأقصى عرض تسمح به نافذة المتصفح لديك، فإذا أردنا قسرها على إنهاء السطر وبدء سطر جديد، فسنستخدم رمز HTML الخاص بالسطر الجديد، وهو ‎<br />‎. لاحظ كيف هرّبنا محارف الاقتباس المفردة بوضع شرطة مائلة خلفية \ قبل الاقتباس، سنشرح ذلك بالتفصيل حين نتحدث عن السلاسل النصية في فصل البيانات وأنواعها. VBScript كما ذكرنا بخصوص جافاسكربت؛ فيجب أن ننشئ ملفًا لوضع أوامر VBScript فيها ثم نفتحه في المتصفح، وإذا كتبنا الأوامر السابقة باستخدام VBScript فستبدو كما يلي: <html><body> <script type="text/vbscript"> MsgBox "Hello There!" MsgBox "Monty Python's Flying Circus has a ' in it" MsgBox 6 + 5 MsgBox ((8 * 4) + (7 - 3)) / (2 + 4) MsgBox 5/2 MsgBox 5 MOD 2 </script> </body></html> سنرى في الخرج كثيرًا من الصناديق الحوارية، يعرض كل منها خرجًا من أحد الأسطر التي في البرنامج، فإذا أردنا أن نجعل جافاسكربت تخرج لنا مثل تلك الصناديق الحوارية فسنستخدم alert("Hello There!")‎ بدلًا من document.write("Hello there!<br>")‎، فقد استخدمنا alert هنا بدلًا من MsgBox التي في VBScript. لاحظ أننا لا نستطيع بدء سلسلة نصية باستخدام علامة اقتباس مفردة في VBScript، لكن يمكننا إدراج اقتباسات مفردة داخل سلاسل نصية ذات علامات اقتباس مزدوجة، باستخدام الدالة Chr التي إذا أعطيناها رمزًا لمحرف أعادت لنا ذلك المحرف، وبما أن هذا المثال يبدو فوضويًا للغاية، لنلق نظرةً على المثال التالي الذي يوضح كيفية عملها: <script type="text/vbscript"> Dim qt qt = Chr(34) MsgBox qt & "Go Away!" & qt & " he cried" </script> لمعرفة رمز أي محرف تريده؛ تستطيع الاسترشاد بهذا الموقع، وأخذ الرمز العشري decimal منه، أو بالنظر في خريطة المحارف التي يوفرها نظام التشغيل الخاص بك، إذ توفر أغلب نظم التشغيل المشهورة بريمجًا لذلك؛ أما إذا لم ترد استخدام هذا ولا ذاك لسبب ما؛ فاستخدم الجزء التالي من لغة جافاسكربت واستبدل المحرف الذي تريده بمحرف الاقتباس المزدوج الذي في المثال: <script type="text/javascript"> var code, chr = '"'; // put the character of interest here code = chr.charCodeAt(0); document.write("<br />The character code of " + chr + " is " + code); </script> لا تشغل بالك الآن بمعنى الأرقام التي في المثال، إذ سنعود إليها في الفصل التالي، وإنما سقناها الآن من أجل استخدامها إذا اضطررنا إلى إيجاد قيمة محرف ما. خاتمة تلك كانت نظرتنا الأولى على البرمجة، ولعلها كانت سهلة الفهم والإدراك، لكن سنحتاج إلى أن ننظر في أنواع البيانات التي سنقابلها في البرمجة وسنحتاج إليها قبل الشروع في البرمجة الحقيقية، كما سنرى الأمور والعمليات التي سنجريها على تلك البيانات. وقد عرفنا في هذا الفصل أن البرنامج قد يكون صغيرًا للغاية، لذا فلا مانع من أن يكون مجرد أمر واحد، وأن أسلوب بايثون في إجراء العمليات الحسابية هو نفسه الأسلوب المتبع في الرياضيات، وإذا أردنا الحصول على نتيجة كسرية فيجب أن نستخدم أعدادًا كسريةً كذلك، وأننا نستطيع دمج النصوص والأرقام باستخدام عامل التنسيق %، وأخيرًا عرفنا أننا نخرج من بايثون بكتابة import sys; sys.exit()‎. ترجمة -بتصرف- للفصل الرابع من كتاب Learning To Program لصاحبه Alan Gauld. اقرأ أيضًا المقال التالي: مدخل إلى البيانات وأنواعها: أنواع البيانات الأساسية المقال السابق: بداية رحلة تعلم البرمجة ما هي البرمجة؟ تعلم البرمجة
  12. استخدام بايثون سنفترض أنك قد ثبّتّ إحدى إصدارات بايثون على حاسوبك، فإذا لم تفعل فاذهب إلى موقع بايثون واجلب النسخة الأحدث منها واتبع إرشادات التثبيت على حاسوبك، لاحظ أن بايثون متاحة لأغلب أنواع الحواسيب المتوفرة في السوق، وستجد ملفات تثبيت لنسختي 64 بت و 32 بت من ويندوز، وكذلك نظام MacOS؛ أما لينكس فستجدها من خلال مدير الحزم في توزيعتك، فإذا لم تكن تعلم معمارية حاسوبك فاختر نسخة 32 بت، وانتبه عند التثبيت إلى خيار إضافة بايثون إلى Path، وهو متغير بيئة سنحتاج أن نضيف بايثون إليه كي يراها ويندوز في سطر الأوامر، بالشكل التالي: أما إذا لم تفعل ذلك أو نسيته أو لم تكن نسخة التثبيت تحتوي على ذلك الخيار، فاتبع الإرشادات التي نذكرها في الفقرة التالية. سطر أوامر ويندوز لقد بدا منذ كتابة هذه السلسلة -بنسخته الأجنبية لأصلية- أن العديد من مستخدمي ويندوز ليسوا معتادين على التعامل مع سطر أوامر MS Dos، لذا سنشرح كيف يمكن الوصول إلى تلك الأداة دون الاستغراق في التفاصيل غير الضرورية. توجد عدة طرق للوصول إلى سطر الأوامر أبسطها الضغط باستمرار على زر ويندوز وحرف R من أجل فتح نافذة Run، ثم كتابة cmd في مربع الحوار الذي سيظهر لك ثم الضغط على OK، وهنا يجب أن ترى نافذةً سوداء فيها نص أبيض مثل هذا: C:\WINDOWS> يشير السطر أعلاه إلى المجلد الذي نحن فيه، فإذا كتبنا DIR وضغطنا على زر الإدخال Enter؛ فستعرَض لنا قائمة بجميع الملفات التي في ذلك المجلد؛ أما إذا كتبنا python فيجب أن نرى المحث ‎‎>>>‎ الخاص ببايثون. قد لا تستطيع CMD العثور على بايثون بسبب بعض إصداراته الأخيرة التي لم تأتي بتلك الإعدادات، ولهذا فإنا لم تستطع إيجادها فستحتاج إلى ضبط متغير بيئة اسمه Path، وذلك بفتح مدير الملفات في ويندوز إما بالطريقة العادية، أو بضغط زر ويندوز مع حرف E (لفتح المتصفح)، ثم الذهاب إلى قسم This PC، والنقر بالزر الأيمن لاختيار Properties التي ستفتح نافذة About الخاصة بحاسوبك، والتي تحوي المعلومات الأساسية عنه. ستجد في تلك الصفحة خيار Advaned System Settings، اضغط عليه لتفتح لك نافذة أخرى ثم اختر تبويب Advanced. سترى زر Environment Variables في أسفل النافذة، اضغط عليه لتذهب إلى نافذة جديدة، وسترى أن Path معرَّف بالفعل كمتغير للنظام. اختره ثم انقر على زر Edit في يمين النافذة. انتبه في هذه الخطوة لئلا تحذف المسار الموجود بالخطأ، فإذا مسحت شيئًا من غير قصد فاضغط زر Esc في لوحة المفاتيح، أو استخدم زر Cancel في النافذة المفتوحة لتخرج منها وتعيد المحاولة. اذهب إلى نهاية الحقل Variable Value وأضف فاصلةً منقوطة ;، ثم المسار الكامل لمجلد بايثون التنفيذي لديك، بعد ذلك اضغط زر الإدخال Enter لتكتمل العملية، فإذا لم تعرف المسار الكامل إلى بايثون فابحث في مدير الملفات عن الملف python3.exe، وستكون محتويات العمود In Folder الذي في شاشة البحث هي المسار الكامل، وقد تضطر إلى إعادة التشغيل كي يعتمد نظام التشغيل الإعدادات الجديدة. اكتب python في سطر الأوامر لنظام تشغيلك من أي مجلد شئت وسترى المحث الثلاثي لبايثون، ونستطيع من هنا كتابة CD إلى المجلد الحامل للسكربت الخاص بنا لننتقل إليه -حيث تشير CD إلى Change Directory-، كما ستجد قائمةً من الأوامر المتاحة التي يمكن كتابتها في سطر الأوامر في نظام المساعدة المدمج في CMD نفسها، من خلال كتابة help في سطر الأوامر؛ أما إذا أردت معرفة المزيد من العلومات عن أحد الأوامر، فاكتب اسم الأمر متبوعًا بـ ‎/?‎، فإذا أردنا مثلًا عرض صفحة المساعدة الخاصة بالأمر DIR فسنكتب ما يلي: C:\WINDOWS> DIR /? أخيرًا يمكن إنشاء اختصار على سطح المكتب بالنقر بالزر الأيمن على سطح المكتب، ثم اختيار جديد New، ثم اختصار Short-cut، واكتب cmd في صندوق الموقع location داخل صندوق الحوار الذي يظهر لك، ثم انقر على التالي Next، ثم غير الاسم إلى شيء مثل Command Prompt أو نحوها. حين تنتهي انقر على Finish لتظهر أيقونة جديدة على سطح المكتب مثل اختصار إلى سطر الأوامر. خذ وقتك في استعراض هذه الأداة، فرغم أنها قد تبدو بدائيةً إلا أنها أقوى مما تتخيل، وخاصةً في إنجاز المهام المتكررة، على عكس الأدوات ذات الواجهة الرسومية مثل مدير الملفات، حيث يمكنك قراءة المزيد عنها في Computer Hope للبدء في تعلمها. عودة إلى بايثون بما أن مستخدمي ويندوز قد ثبتوا بايثون باتباع الخطوات السابقة، فسنفترض أن مستخدمي لينكس متعودون على استخدام الطرفية، أما مستخدمي نظام ماك فيمكنهم تشغيل برنامج الطرفية بالنقر على الأيقونة كالمعتاد، وهنا يجب أن يظهر محث بايثون كما يلي: Python 3.6.2 (default, Jul 29 2017, 00:00:00) [GCC 4.8.4] on linux Type "copyright", "credits" or "license()" for more information. >>> وفي خيار بديل لسطر الأوامر ذاك؛ قد تجد اختصارًا إلى شيء اسمه IDLE، أو Python GUI في قائمة ابدأ لديك، وهو بيئة برمجة مخصصة لبايثون، وتوفر كثيرًا من الأدوات والأوامر المفيدة للمبرمجين، فإذا شغلته بدلًا من سطر الأوامر فستحصل على نافذة مستقلة لسطر الأوامر مع بعض الألوان المميزة للخطوط، وقد كتب Danny Yoo دليلًا مفصلًا لهذه البيئة يمكنك قراءته والاطلاع عليه إذا أردت استخدامه بدلًا من سطر الأوامر العادي، وهو يكرر بعض المعلومات التي ذكرناها من قبل، لكن تكرار التعليم لا يضر، كما تستطيع الاطلاع على التوثيق الرسمي لبيئة IDLE كذلك إن شئت. أما إذا كنت تفضل التدريب المرئي بالفيديو، فهناك الكثير من الفيديوهات المتعلقة بالبرمجة على يوتيوب، ويُرى أن تعلم المفاهيم واستخدام الأدوات من الفيديو أمر ممكن، شرط أن توقِف الفيديو مع كل سطر أوامر يُكتَب كي تتابع الشرح جيدًا، كما يوصى بكتابة الشيفرة وتشغيلها بنفسك لتتذكر ما تتعلمه، لذا فإن أردت مشاهدة الفيديو لفهم المبدأ فلا بأس، لكن عد إلى هنا مرةً أخرى واقرأ حول ذلك المفهوم واكتب الشيفرة وجربها بنفسك، ثم عدل فيها وجرب إلى أن تصل إلى توقع التغييرات التي ستحدث، وتلك هي الطريقة الوحيدة لتعلم البرمجة في رأيي. المثير في بيئة IDLE أنها برنامج مكتوب ببايثون، فهي تبين لك قوة اللغة بمثال حي، وتوضح ما يمكن تنفيذه بها؛ أما إذا حصلت على بايثون من ActiveState أو كنت قد حمّلت نسخًا مخصصةً لويندوز -مثل حزمة PyWin32-؛ فيمكنك الوصول إلى بيئة برمجة رسومية أخرى تشبه IDLE لكنها أكثر أناقةً منها تسمى Pythonwin. لا شك أن كلا البيئتين تسهلان البرمجة أكثر من سطر الأوامر العادي، لكننا نفضل استخدام الأدوات البسيطة في بداية التعلم لإدراك المفاهيم التي نشرحها إدراكًا أعمق. كلمة حول رسائل الخطأ سترى أثناء كتابة التعليمات البرمجية ببايثون رسائل خطأ لا محالةً، وستبدو مثل هذه: >>> print( 'fred' + 7 ) Traceback (most recent call last): File "<input>", line 1, in ? TypeError: cannot concatenate 'str' and 'int' objects لا تشغل بالك بالمعنى الدقيق لهذه الرسالة الآن، وإنما نريدك أن تنظر إلى هيكلها، فسطر ‎'>>> print ...'‎ مثلًا هو السطر الخاطئ، أما السطران التاليان فيصفان مكان الخطأ، وقد تتكون رسالة الخطأ من عدة أسطر في البرامج المعقدة، فقد تقترح كلمة Traceback مثلًا أن الرسالة تتضمّن أثرًا أو سجلًا لكل ما كان البرنامج يفعله عند وقوع الخطأ، وقد يكون هذا مربكًا للمبتدئين، لكن ثق أنك مع الخبرة ستسعد بوجود رسالة الخطأ تلك، وستعلم أن الأسلوب الأمثل لقراءتها حينئذ سيكون من الأسفل، وتصعد لأعلى بقدر حاجتك من الرسالة. نعود إلى رسالة الخطأ السابقة حيث يشير ‎'line 1 in ?'‎ إلى السطر رقم 1 في التعليمة التي نكتبها، فلو كان برنامجًا أطول وكان مخزّنًا في ملف مصدري فسيحل اسم الملف الحقيقي محل <input>، بينما يخبرنا السطر ‎'TypeError...'‎ بالخطأ الذي يراه المفسر، وقد يوجد أحيانًا محرف إقحام ^ يشير إلى الجزء الذي تراه بايثون خطأً، لكن هذا التقرير يكون غير صحيح في العادة، فقد يكون الخطأ الحقيقي في مكان سابق للموضع الذي أشارت إليه بايثون، أو حتى قبله بسطر أو سطرين، فالحواسيب آلات غبية بأي حال كما قلنا من قبل. تُستخدم بيانات الأخطاء تلك لمعرفة ما يحدث في الشيفرة التي بين أيدينا، وقد يكون سبب الخطأ البرمجي بشريًا في الغالب، فرغم أن الحواسيب آلات غبية، إلا أنها آلات غاية في الدقة، فلعلنا أخطأنا في كتابة كلمة أو تعليمة أو نسينا علامة اقتباس أو فاصلةً منقوطةً مثلًا، ولدينا عمومًا ثلاثة أنواع من الخطأ التي سنواجهها: الخطأ اللغوي syntax error، وهو يعني أن بايثون لا ترى أن ما أُدخل إليها صالح، وقد يكون سببه نسيان علامة ترقيم أو خطأً في هجاء كلمة. خطأ وقت التشغيل runtime error، وذلك حين يكون البرنامج صالحًا لكن لا يمكن تنفيذه لسبب ما، كما في محاولة تنفيذ عملية غير مسموح بها أو غير صالحة، مثل طلب قراءة ملف غير موجود. خطأ دلالي semantic، حيث يعمل البرنامج ولا تظهر أي رسالة خطأ، لكنه يخرج لنا مخرجات خاطئةً غير التي يفترض به إخراجها، فإذا كتبنا مثلًا برنامجًا ليخبرنا بعدد كلمات الملف الذي تقرؤه الآن، وأخرج لنا أنه يحوي خمس كلمات فقط، فستكون هذه النتيجة خاطئةً قولًا واحدًا، فهنا يكون خطأ البرنامج دلاليًا. ستكون أغلب الأخطاء التي تتعرض لها في البداية أخطاءً لغويةً أو أخطاء وقت تشغيل إلى أن تبدأ بتصميم برامجك الخاصة، فحينها سترتكب أخطاء دلالية، وتسمى تلك العملية حينئذ بالتنقيح debugging، وتُسمى الأخطاء التي سترتكبها آنذاك بالزلات البرمجية bugs، لكن الترجمة الحرفية لاسمها الأجنبي bug هي عثة، وهي حشرة صغيرة سميت الأخطاء البرمجية باسمها لأسباب تاريخية، فقد كانت أولى الأخطاء التي حدثت في الحواسيب الأولى بسبب عثة علقت داخل الحاسوب وتسببت في حرق بعض داراته الكهربائية، وعلى الرغم أن الكلمة نفسها قد استُخدمت للدلالة على الأخطاء البرمجية قبل هذا بعدة عقود، لكنها لم تثبت إلا مع تلك الحادثة، لكننا سنستخدم اصطلاح الزلات البرمجية لقربها من المفهوم العربي للفعل ذاته. وبالعودة إلى الخطأ الذي ارتكبناه في المثال السابق والذي تسبب في إخراج رسالة الخطأ لنا، فقد كان محاولة إضافة عدد إلى سلسلة محارف، وهو خطأ دلالي، لكنه ينتج لنا خطأ وقت تشغيل أيضًا، فليس هذا مسموحًا لك في بايثون، لذا كانت رسالة الخطأ تمثل اعتراضًا من اللغة على ما فعلناه، فأخبرتنا أن هناك TypeError، وسنتحدث عن هذه الأنواع في مقال لاحق من هذه السلسلة. وبغض النظر عن الأداة التي تريد استخدامها سواء كانت سطر الأوامر أو بيئة IDLE أو Pythonwin، فنحن الآن جمعنا عدتنا وجاهزون للبدء في إنشاء بعض البرامج البسيطة باستخدام بايثون. جافاسكربت إذا أردنا إنشاء برامج جافاسكربت في متصفح، فسنحتاج إلى مزيد من الإجراءات التي علينا تنفيذها، حيث سنحتاج مثلًا إلى إنشاء ملف HTML نستطيع تحميله إلى متصفح، وهذا الملف لا يحوي إلا نصًا مجردًا نستطيع إنشاءه في محرر نصي بسيط مثل Notepad أو أي محرر نصي آخر، ثم حفظه بالامتداد ‎.htm أو ‎.html، وسيبدو بالشكل الآتي: <html> <body> <script type="text/javascript"> document.write('Hello World\n'); </script> </body> </html> سيكون الجزء الواقع بين بداية الوسم <script> ونهايته هو برنامجنا، ولن تعرض جميع وسوم HTML في كل مرة أثناء شرحنا في هذه السلسلة، لذا عليك أن تنسخ هذا الملف في كل مرة مثل قالب ثم تستبدل الشيفرة التي تريد تجربتها بما هو موجود بين الوسمين <script> و‎</script>‎، ثم افتح ملف HTML ذاك في متصفح ويب عن طريق النقر عليه في مدير ملفاتك، يجب أن يشغّل نظامك عندئذ برنامج المتصفح ويحمّل الملف الذي سيتسبب بالتبعية في تنفيذ برنامجنا. VBScript يمكن القول أن VBScript هي نفسها جافاسكربت لكن مع استبدال الاسم "vbscript" الذي في type=‎ بالنص القديم الموجود "javascript"، كما يلي: <html> <body> <script type="text/vbscript"> MsgBox "Hello World" </script> </body> </html> لاحظ أن متصفح إنترنت إكسبلورر الخاص بمايكروسوفت هو الوحيد القادر على تشغيل VBScript، أما المتصفحات الأخرى فلا تدعم إلا جافاسكربت بما في ذلك متصفح Edge الخاص بويندوز، والذي استبدلت إنترنت إكسبلورر به، لذا يبدو أن أيام VBScript صارت معدودةً. أخطاء VBScript وجافاسكربت سنحصل على صندوق حوار في كل من جافاسكربت وVBScript يخبرنا برقم السطر الذي فيه الخطأ، كما سيكون لدينا عدة أخطاء غامضة غير معروفة، ويجب معاملة رقم السطر المذكور بأنه قد لا يكون دقيقًا في الغالب كما فعلنا مع بايثون، وسنحتاج بعد إصلاح الخطأ إلى إعادة تحميل الصفحة التي هو فيها. توجد في المتصفحات أدوات تنقيح متقدمة وموجهة للمطورين المحترفين، لكنها مخبأة داخل المتصفحات ولا تناسب احتياجاتنا الحالية بعد. خاتمة بغض النظر عن اللغة التي ستستخدمها في متابعة الشرح في هذه السلسلة، فنرجو أن تخرج من هذا المقال وأنت تعلم أنك تستطيع بدء بايثون بكتابة python في سطر الأوامر، وألا تخشى رسائل الخطأ، بل اقرأها بعناية، فهي تعطينا عادة المفاتيح التي نحتاجها لإصلاح الخطأ الواقع في البرنامج، غير أنها لا زالت مجرد مفاتيح، فإذا شككت في دقتها فانظر إلى الأسطر السابقة للسطر الذي تخبرك أن الخطأ فيه. ترجمة -بتصرف- للفصل الثالث من كتاب Learning To Program لصاحبه Alan Gauld. اقرأ أيضًا المقال التالي: التسلسلات البسيطة في البرمجة المقال السابق: ما هي البرمجة؟ تعلم البرمجة ما هي البرمجة ولماذا تصبح مبرمجًا
  13. يمكن القول إنك لن تحتاج لأكثر من اتصال بالإنترنت لتتعلم البرمجة من هذه السلسلة، ومع أن هذا يكفي من الناحية التقنية إلا أنه غير كافٍ لتعلم البرمجة، إذ يجب التنويه إلى أسلوب التفكير الذي ستتبعه في البرمجة، فلابد أن تتمتع بفضول للتعلم مع أسلوب منطقي لتفكير، وهما أمران لازمان لأي مبرمج ناجح. تبرز أهمية الفضول في البحث عن إجابات للمشاكل، وفي قدرتك على التجربة والتنقيب في الملفات بحثًا عن الأفكار والمعلومات المطلوبة لتنفيذ مهمة معينة، كما أن التفكير المنطقي مهم لأن الحواسيب آلات غبية بطبيعتها، ولا تستطيع فعل أي شيء سوى إضافة أرقام مفردة إلى جانب بعضها البعض، وتحريك بايتات من مكان لآخر. لقد كتب كثير من المبرمجين المهرة -لحسن الحظ- برمجيات تُخفي غباء الحواسيب هذا، لكنك بصفتك مبرمجًا ستتعرض لمواقف تواجه فيها ذلك الغباء الذي لم يعالجه أو يتعامل معه أحد قبلك، وهنا يجب أن تضع نفسك مكان الحاسوب وتفكر بدلًا عنه، وأن تعرف تحديدًا ما يجب عمله ببياناتك وزمن ذلك أيضًا. قد يكون هذا سردًا فلسفيًا لأمر يبدو تقنيًا وإلكترونيًا محضًا، غير أنك تحتاج إلى مثل تلك الفلسفة لتفهم هذا الشرح فهمًا جيدًا. بعد ذلك يأتي الجزء الخاص بالتدريب العملي الذي ستحتاج فيه إلى كتابة الأمثلة بيدك، أو نسخها من الكتاب إلى المحرر النصي عندك، ثم تشغيلها ورؤية النتائج، وستحتاج هنا إلى تثبيت بايثون Python على حاسوبك، وإلى متصفح قادر على تشغيل لغتي VBScript وJScript، مع الإشارة إلى أن أي متصفح حديث قادر على تشغيل جافاسكربت. بايثون Python يُعَد أحدث إصدار من لغة بايثون وقت كتابة هذه الكلمات -بلغتها الأصلية- هو 3.9، وحجم ملف تحميله من ActiveState يقارب 29 ميجا بايت لنسخة إصدار ويندوز (لا زالت ActiveState في الإصدار 3.8)، غير أنها تشمل كامل التوثيق والكثير من الأدوات التي سننظر في بعضها لاحقًا في هذه السلسلة، لذا تأكد من اختيار النسخة الموافقة لنظام تشغيلك. أما بالنسبة لنظام لينكس أو الأنظمة الشبيهة بيونكس عمومًا، فاطلب من مدير نظامك تثبيت النسخة المصدرية لبايثون على حاسوبك، لكن قد لا تحتاج إلى ذلك لأنها تأتي مدمجةً مسبقًا، ومثبتةً تلقائيًا في أغلب توزيعات لينكس، كما ستجدها في الإصدارات بتحزيمات موجهة للتوزيعات المشهورة مثل ريدهات Red Hat وديبيان Debian وأوبنتو Ubuntu وغيرها، بل قد تجد أن أغلب أدوات إدارة النظام في لينكس مكتوبة بلغة بايثون، ولا تقلق إذا كان إصدارك أقل من 3.6، فأي إصدار بعد 3.4 سيكون مناسبًا. تستطيع النظر في نسخ التحميل المختلفة لاختيار ما يناسبك من موقع التحميل الرئيسي لبايثون، أما مستخدمو ويندوز وماك فقد يفضلون نسخة ActiveState.com التي تأتي غالبًا مع بعض الأدوات الإضافية المدمجة في البرنامج نفسه، وقد تكون ActiveState متأخرةً قليلًا في إصدار نسخها الجديدة بسبب برنامجهم الخاص في التحزيم والاختبار، غير أنها تستحق الانتظار. دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن VBScript وجافاسكربت لقد سبق وأن قلنا إن أغلب المتصفحات تستطيع تشغيل جافاسكربت دون مشاكل، لكن VBScript عكس ذلك، فهي لن تعمل إلا في متصفح إنترنت إكسبلورر من مايكروسوفت، ولن تحتاج إلى تثبيت أي شيء إضافي لهاتين اللغتين، فإما أن تكونا لديك معًا إذا كنت تستخدم ويندوز، أو لن تكون لديك VBScript إذا كنت تستخدم ماك أو لينكس، والأمر الوحيد الذي عليك الانتباه إليه هنا هو أن بعض مديري الأنظمة القلقين بشأن الأمان قد يغلقون خاصية تشغيل السكربتات scripts في المتصفح لدواع أمنية، لكن هذا نادر الآن لأن أغلب المتصفحات تشغّل جافاسكربت. برمجة الحواسيب هي فن نجعل فيه الحاسوب ينفذ ما نرغب فيه بالضبط، وهي تتكون في أبسط صورها من سلسلة أوامر نعطيها للحاسوب لينفذها من أجل تحقيق هدف ما، وقد كان المستخدمون قديمًا أيام نظام دوس الخاص بويندوز، ينشئون ملفات نصيةً تحوي قوائم من تلك الأوامر، تُسمى ملفات الرُّقع أو باتش batch files (تدعى غالبًا سكربتات أو سكربت باتش)، وسبب تسميتها بذلك هو أنها تنفذ الأوامر مثل مجموعة أو رقعة واحدة، وكان امتدادها هو ‎.BAT، لذا أُطلق عليها اسم ملفات بات BAT، ولا يزال بإمكاننا كتابة مثل تلك الملفات في بيئات ويندوز هذه الأيام رغم ندرة استخدامها، فمثلًا إذا كنت تكتب مستند HTML مكوَّن من ملفات كثيرة مثل الدليل الذي تقرؤه الآن، فسينشئ برنامج معالجة النصوص الذي تستخدمه نسخًا احتياطيةً من كل ملف كلما حفظ نسخةً جديدةً منه. فإذا رغبت في أن تضع النسخة الحالية من المستند -وهي الإصدارات الأخيرة من جميع ملفاته- في مجلد نسخ احتياطي backup في آخر كل يوم ثم حذف النسخ الاحتياطية التي لدى معالج النصوص؛ فيمكن كتابة ملف BAT بسيط لتنفيذ لك، وسيكون كما يلي: COPY *.HTM BACKUP DEL *.BAK إذا كان اسم الملف هو SAVE.BAT فسنكتب SAVE في محث DOS (يطلق على المحث أيضًا موجه أوامر) في نهاية اليوم بعد انتهاء العمل، وستُحفظ الملفات وتُحذف النسخ الاحتياطية تلقائيًا، وهكذا تكون قد رأيت مثالًا عمليًا لبرنامج بسيط. لاحظ أن مستخدمي نظام لينكس وغيره، بل حتى مستخدمي الإصدارات الحديثة من ويندوز، لديهم نسختهم الخاصة بهم من أمثلة تلك الملفات، والتي تُعرف عادةً باسم سكربتات الصدفة shell scripts، وهي أقوى بكثير من ملفات BAT الخاصة بنظام دوس، وتدعم أغلب التقنيات البرمجية التي سنتحدث عنها في هذا المقال. دورة تطوير التطبيقات باستخدام لغة JavaScript تعلم البرمجة بلغة جافا سكريبت انطلاقًا من أبسط المفاهيم وحتى بناء تطبيقات حقيقية. اشترك الآن تعريف البرنامج مرة أخرى لقد ذكرنا أعلاه مثالًا عن برنامج، لنبين بساطة مفهوم البرمجة في حد ذاته، لكن إذا خشيت عدم فهم البرمجة، فاعلم أن البرنامج ما هو إلا مجموعةً من التعليمات التي تخبر الحاسوب كيف ينفذ مهمةً ما، وهو يشبه وصفة الطعام التي تتكون من مجموعة من التعليمات التي تخبر الطاهي كيف يصنع طبقًا ما، فهي تصف مقادير المكونات (البيانات) وترتيب خطوات التنفيذ (العملية) المطلوبة لتحويل تلك المكونات إلى كعكة أو غيرها، فالبرنامج يشبهها كثيرًا في هذا. نظرة تاريخية نحن نستخدم لغةً للحديث مع الحواسيب، كما نستخدم لغات بيننا نحن البشر، غير أن اللغة الوحيدة التي يتحدثها الحاسوب تسمى اللغة الثنائية binary والتي توجد عدة نسخ منها، لهذا لا يعمل برنامج موجه لنظام ماك على ويندوز والعكس. ويُعَد تعلّم هذه اللغة صعبًا جدًا بالنسبة للبشر، سواءً قراءتها أو كتابة برامج بها، لذلك يجب أن نستخدم لغةً وسيطةً ثم نترجمها إلى اللغة الثنائية، تمامًا مثل شخصين من دولتين مختلفتين يتحدثان معًا وبينهما مترجم وسيط يفسر كلام كل منهما للآخر، فقد يتحدث أحدهما العربية والآخر الإنجليزية، فيترجم المترجم الكلام العربي إلى الإنجليزية وينقله إلى متحدث الإنجليزية، ثم يعكس العملية إذا تحدث الإنجليزي مخاطبًا الشخص العربي، وما سنستخدمه ليترجم لغتنا البرمجية إلى الحاسوب يسمى مترجمًا أو مفسرًا interpreter. وكما نحتاج مفسرًا لكل لغة من لغات البشر، فإننا نحتاج مفسرًا لكل لغة برمجية ليترجمها إلى لغة الحاسوب، فنحتاج مفسرًا من بايثون إلى لغة الآلة الثنائية binary، وآخر من VBScript إلى لغة الآلة كذلك، غير أن المبرمجين الأوائل كانوا مضطرين إلى كتابة الشيفرات الثنائية تلك بأنفسهم، أي بلغة الآلة التي ذكرناها قبل قليل وهي صعبة الكتابة والتعلم. بعد ذلك أتت المرحلة التالية التي أُنشئ فيها مفسر يحول كلمات مكتوبةً بأحرف بشرية إلى ما يقابلها من اللغة الثنائية، فبدلًا من تذكر أن الرمز ‎001273 05 04‎ يعني جمع 4 إلى 5، نستطيع الآن أن نكتب شيئًا مثل ADD 5 4، وقد سهلت تلك النقلة البسيطة الكثير من عمل البرمجة والتطوير، حيث كانت تلك الأنظمة البسيطة من الشيفرات هي لغات البرمجة الأولى التي كانت تختلف باختلاف نوع الحاسوب، وتسمى باسم اللغات المجمِّعة Assembler Languages، ولا زالت البرمجة بلغة التجميع مستخدَمةً هذه الأيام في بعض المهام المتخصصة، لكن هذا التطور كان بدائيًا في أسلوب إخبار الحاسوب بما يجب فعله، مثل نقل البيانات من هذا الموضع من الذاكرة إلى ذلك الموضع، وإضافة هذا البايت إلى ذلك البايت وغيرها من العمليات. البايت Byte هو وحدة بيانات تتكون من ثمانية بتّات bits أصغر منه ومجموعة فيه، وتلك البتات تكون إما 1 أو 0، وقد استُخدم البايت في البداية لتمثيل محارف النصوص، بحيث يمثل كل بايت حرفًا واحدًا. وبما أن هذا النمط من البرمجة لا زال صعبًا في تعلمه والعمل به لإنجاز أبسط المهام، فقد طور علماء الحوسبة مع الوقت لغات تسهل عملية البرمجة، وكأن ذلك جاء في وقته إذ تطورت المشاكل التي يواجهها المستخدمون في ذلك الوقت بسبب تطور المهام في أعمالهم اليومية، وهم يريدون للحواسيب أن تحل لهم تلك المشاكل. ولا زال هذا التنافس قائمًا، كما لا زالت لغات البرمجة الجديدة تظهر على الساحة. هذا ومع جعل البرمجة أمرًا مثيرًا يتغير كل يوم، إلا أنه يجعلك كونك مبرمجًا في حاجة إلى استيعاب المفاهيم البرمجية وطرق تنفيذها وتطبيقها في لغة واحدة بعينها، وسنناقش بعض تلك المفاهيم فيما يلي، لكن يجب أن تجعلها حاضرةً عندك تعود إليها دائمًا أثناء قراءتك للسلسلة. المزايا المشتركة لجميع البرامج خرج إدزجر ديكسترا Edsger Dijkstra قبل زمن بمفهوم اسمه البرمجة الهيكلية Structured Programming، يتحدث فيه عن إمكانية هيكلة جميع البرامج بأربعة طرق، هي الآتية: سلاسل من التعليمات: يتحرك فيها البرنامج من خطوة لأخرى في تسلسل صارم. الفروع Branches: هنا يصل البرنامج إلى نقطة اتخاذ قرار، فإذا تحققت نتيجة الاختبار -أي كانت القيمة true-، فإن البرنامج سينفذ التعليمات التي في المسار 1، وإذا كانت false فسينفذ الإجراءات التي في المسار 2، ويُعرف هذا بالبنية الشرطية لأن سير البرنامج يتوقف على نتيجة اختبار شرطي. الحلقات التكرارية Loops: تُكرَّر خطوات البرنامج في هذه الطريقة إلى أن نصل إلى اختبار شرطي ما، ينتقل تحكم البرنامج بعدها من الحلقة التكرارية إلى الجزء التالي من منطق البرنامج. الوحدات Modules: هنا ينفذ البرنامج تسلسلًا متطابقًا من الخطوات عدة مرات، فتُجمع تلك الإجراءات في وحدة واحدة، وهي برنامج صغير الحجم يمكن تنفيذه من داخل البرنامج الرئيسي، وقد تُسمى الوحدات بأسماء أخرى مثل الدوال functions أو الإجراءات procedures أو البرامج الفرعية sub-routines. احتاجت البرامج إلى بعض المزايا الأخرى، إضافةً إلى ما سبق كي تكون مفيدة: البيانات العمليات: مثل الجمع والطرح والموازنة وغيرها. إمكانية الإدخال والإخراج: مثل عرض النتائج على شاشة مثلًا. وبمجرد أن تستطيع استيعاب المفاهيم أعلاه، والتعرُّف على كيفة تنفيذ اللغة البرمجية التي تستخدمها لها، فستكون قادرًا على كتابة برامج بتلك اللغة. توضيح بعض المصطلحات إذا قلنا إن البرمجة هي الفن الذي نجعل فيه الحواسيب تنفذ ما نريده منها، فما هو البرنامج إذًا؟ في الواقع لدينا مفهومان مختلفان عن البرنامج، أولهما من منظور المستخدم، وهو ملف تنفيذي يثبَّت على الحاسوب ثم يمكن تشغيله لتنفيذ مهمة ما، فمثلًا يقول المستخدم أنه "يشغِّل" برنامج معالجة النصوص؛ أما المفهوم الثاني فهو من منظور المبرمج، وهو هنا مجموعة من التعليمات النصية الموجهة إلى الحاسوب المكتوبة بلغة برمجة ما، ويمكن ترجمتها إلى ملف تنفيذي. لهذا يجب أن تكون مدركًا عن أي المفهومين تتحدث حين تستخدم كلمة برنامج. يكتب المبرمجون برامجهم عادةً بلغة برمجة عالية المستوى تفسَّر إلى بايتات يفهمها الحاسوب، أي يكتب المبرمج شيفرةً مصدريةً source code؛ أما المفسر فيولد تعليمات مُصرَّفة object code، وقد يطلق على هذه التعليمات الناتجة عن التصريف أسماء أخرى مثل ملف تنفيذي أو شيفرة تنفيذية أو محمولة P-Code أو بايت كود Byte Code أو شيفرة ثنائية binary code، أو شيفرة الآلة machine code. كما يطلق على المترجم الذي يترجم لغة البرمجة العالية إلى لغة الآلة عدة أسماء، فقد يسمى بالمفسر interpreter أحيانًا، وبالمصرِّف compiler أحيانًا أخرى، وتشير هذه المصطلحات إلى نوعين من التقنيات المستخدمة في توليد الشيفرات الكائنية من الشيفرة المصدرية، فالمصرِّفات تنتج شيفرةً كائنيةً يمكن تشغيلها مستقلةً بذاتها دون الحاجة إلى المصرِّف في كل مرة يعمل فيها البرنامج، ويكون البرنامج في صورة ملف تنفيذي executable file؛ أما المفسِّرات فيجب أن تكون حاضرةً لتشغيل برامجها أثناء تنفيذها. لكن الفرق بين هذين المصطلحين صار ضبابيًا هذه الأيام بما أن بعض المصرفات تحتاج إلى مفسرات تكون حاضرةً لتنفيذ التحويل النهائي، كما تصرِّف بعض المفسرات شيفرتها المصدرية إلى شيفرة كائنية مؤقتة وتنفذها؛ أما من منظور المبرمجين فليس هناك فرق حقيقي، إذ تُكتب الشيفرة المصدرية وتُستخدَم أداة تسمح للحاسوب بقراءة الشيفرة وترجمتها وتنفيذها. هيكل البرنامج يعتمد الهيكل الدقيق للبرنامج على لغة البرمجة والبيئة التي يعمل فيها، لكن توجد بعض المبادئ الأساسية عمومًا: المحمِّل Loader: يحتاج كل برنامج أن يُحمّله نظام التشغيل إلى الذاكرة، وهذه وظيفة المحمل الذي يُنشأ عادةً بواسطة المفسر. تعريفات البيانات: تعمل أغلب البرامج بناءً على بيانات، وسنحتاج في مرحلة ما في شيفرتنا المصدرية إلى تعريف نوع البيانات الذي نعمل معه تعريفًا دقيقًا، وهذا يختلف من لغة برمجة لأخرى. التعليمات statements: وهي صلب البرامج، وهي تعدّل البيانات التي نعرّفها وتنفذ الحسابات وتطبع الخرج لنا.. تتبع أغلب البرامج إحدى الهيكلين التاليين: برامج باتش Batch Programs تُبدأ تلك البرامج عادةً من سطر الأوامر أو بواسطة أداة جدولة، وتتبع النمط التالي في الغالب. يبدأ البرنامج بتعيين حالته الداخلية كأن يعين الإجماليات totals إلى صفر، ويفتح الملفات المطلوبة، وبمجرد أن يكون جاهزًا للعمل، فإنه يقرأ البيانات من المستخدم بعرض موجهات الأوامر أو المحثات على الشاشة أو من ملف بيانات، أو بالطريقتين معًا، حيث يعطي المستخدم اسم ملف البيانات ثم تُقرأ البيانات الفعلية من الملف، بعد ذلك يعالج البرنامج البيانات معالجةً تتضمن عمليات رياضيةً أو تحويلات للبيانات أو غير ذلك، أخيرًا، تُخرج النتائج إلى شاشة عرض أو تُكتب إلى ملف، وستكون جميع البرامج التي نكتبها في الأجزاء الأولى من السلسلة من هذا النوع، أي برامج باتش أو رُقع. البرامج الحدثية Event driven programs تُعَد أغلب الأنظمة ذات الواجهة الرسومية مثل ويندوز أو أندرويد، وأنظمة التحكم المدمجة Embedded control systems مثل جهاز المايكروويف لديك أو الكاميرا أو غيرهما، من البرامج الحدَثية، أي يتوقف سيرها على وقوع أحداث معينة، بحيث يرسِل نظام التشغيل أحداثًا إلى البرنامج فيستجيب لها وفقًا لوصولها إليه، وقد تكون تلك الأحداث أمورًا يفعلها المستخدم مثل نقر زر الفأرة أو ضغط زر ما، أو أمورًا يفعلها النظام نفسه مثل تعديل الساعة أو تحديث الشاشة، وتبدو البرامج الحدثية في الغالب كما يلي: يبدأ البرنامج هنا بتعيين حالته الداخلية، ثم ينتقل التحكم إلى بيئة التشغيل (تسمى أحيانًا وقت التشغيل runtime)، ويوفر نظام التشغيل تلك البيئة في الغالب، كما ينتظر البرنامج حلقة الحدث لتلتقط تفاعل المستخدم الذي يترجَم إلى أحداث بعدها، ثم ترسَل تلك الأحداث إلى البرنامج كي يتعامل معها حدثًا حدثًا، وينفذ المستخدِم الإجراء النهائي الذي ينهي البرنامج، فيُنشَأ حدث خروج ويُرسَل إلى البرنامج. خاتمة ذكرنا في هذا المقال أنك تحتاج إلى التفكير المنطقي والفضول من أجل تعلم البرمجة، ويمكن الرجوع في هذا إلى مقال الدليل الشامل في تعلم البرمجة من أكاديمية حسوب، ومقال حل المشكلات وأهميتها في احتراف البرمجة من الأكاديمية أيضًا، كما يجب أن نذكر أن كل لغات البرمجة المذكورة أعلاه متاحة للتحميل مجانًا، وبهذا لا يبقى سوى أن تأتي بذهن حاضر، مع قليل من حس الدعابة والمرح لنبدأ البرمجة، إذ تسمح لنا لغات البرمجة بالتحدث مع الحواسيب بلغة قريبة من لغات البشر المنطوقة، لكنها تختلف عن الطريقة التي تتحدث الحواسيب أو تفكر بها، وقد قلنا إن البرامج تعمل وفقًا للبيانات، وأنها إما برامج رُقع batch programs أو برامج حدثية Event driven تتصرف وفقًا للأحداث التي تُرسل إليها. ترجمة -بتصرف- للفصلين ?What do I need و?What is Programming من كتاب Learning to Program لصاحبه Alan Gauld. اقرأ أيضًا المقال التالي: بداية رحلة تعلم البرمجة تعلم البرمجة أسهل لغات البرمجة ما هي البرمجة ولماذا تصبح مبرمجا
  14. تُسمى الأخطاء في برامج الحاسوب عادةً بالزلات bugs، ونحن من نضع هذه الزلات في برامجنا بأيدينا حين نخطئ في شيء ما، أو ننسى رمزًا، أو محرفًا، أو نضع واحدًا في غير محله، وإن كان يحلو للكثير منا أن يظن بأنها تزحف من تلقاء نفسها إلى داخل الشيفرة، ولو قلنا أن البرنامج هو فكرة متبلورة في ذهن المبرمج، فاحتمالية حدوث ثغرة أو خطأ برمجي في هذا البرنامج لن تخرج من أحد شيئين: الفكرة نفسها معيبة أو مشوهة. حدوث خطأ أثناء ترجمة البرنامج من فكرة إلى شيفرة برمجية. والحالة الثانية أيسر في اكتشافها وحلها من الأولى، إذ يكون البرنامج سليمًا عدا هذه الثغرة أو الخطأ؛ أما إن كان البرنامج كله مشوهًا نتيجة عيب في كل من الفكرة والمبدأ اللذَين بُني عليهما، فسيكون ذلك أصعب بكثير، ويُطلق على عملية اكتشاف الأخطاء وتصحيحها في الاصطلاح الأجنبي debugging. اللغة سبب أغلب الأخطاء التي تحدث في البرامج التي نكتبها كما ذكرنا هو نحن المبرمجين، ولو كان الحاسوب يَعقل لقذف تلك المشاكل في وجوهنا، وإذا علمت مرونة جافاسكربت العالية لأدركت مدى صعوبة تنقيح الأخطاء الموجودة في شيفراتك التي تكتبها. تُعَدّ بنية الرابطات bindings، والخصائص properties مبهمةً إلى الحد الذي يندر معه اكتشاف الأخطاء الكتابية قبل تشغيل البرنامج، وحتى عند التشغيل، إذ تسمح لك بالقيام بأمور غير منطقية دون تنبيهك إليها مثل حساب `true * "monkey"‎. لكن رغم تلك المرونة الكبيرة في جافاسكربت، إلا أن لها حدودًا لا تتسامح معها، فمثلًا، ستجعل الحاسوب ينبهك فورًا إذا كتبت برنامجًا لا يتبع قواعدها وبنيتها اللغوية؛ كما سيُحدث استدعاء شيء ما غير الدوال أو البحث عن خاصية في قيمة غير معرفة خطأً يُرسَل في تقرير حين يحاول البرنامج تنفيذ هذا الإجراء -أي عند الاستدعاء أو البحث في هاتين الحالتين-. لكن الغالب أنه لن تُنتج حساباتك الغير منطقية سوى NaN -أي ليس عددًا Not A Number-، أو قيمة غير معرفة undefined value، وسيتابع البرنامج تنفيذه ظانًا أنه يقوم بشيء مفيد، إذ لن تظهر المشكلة إلا لاحقًا بعد مرور تلك القيمة الزائفة على عدة دوال، وقد لا تُطلق إنذار الخطأ على الإطلاق، لكنها تتسبب في خطأ الخرج الناتج من البرنامج في نفس الوقت! وبناءً على ذلك فمن الصعب العثور على مصدر مثل تلك المشاكل. الوضع الصارم يمكن تقييد جافاسكربت للحد من مرونتها العالية، وذلك من خلال تفعيل الوضع الصارم strict mode فيها، ويكون هذا بوضع السلسلة النصية "use strict" في رأس الملف أو متن الدالة، انظر مثالًا لذلك كما يلي: function canYouSpotTheProblem() { "use strict"; for (counter = 0; counter < 10; counter++) { console.log("Happy happy"); } } canYouSpotTheProblem(); // → ReferenceError: counter is not defined إذا نسيت وضع let قبل الرابطة، كما في حالة counter التي في المثال أعلاه، فستُنشِئ جافاسكربت رابطةً عامةً global binding وستستخدِمها؛ أما في الوضع الصارم فلا يحدث ذلك، بل تبلغك اللغة بالخطأ، وذلك أكثر فائدةً لك في البرمجة؛ لكن يجب الانتباه إلى أن هذا لا يحدث حين تكون الرابطة موجودة أصلًا على أساس رابطة عامة، ففي تلك الحالة ستظل الحلقة التكرارية تستبدل قيمة الرابطة. كما تحمل رابطة this في الوضع الصارم قيمةً غير معرفة undefined في الدوال التي لا تُستدعى على أساس توابع methods؛ أما في الاستدعاء العادي، فستشير this إلى كائن النطاق العام global scope object الذي تكون خصائصه هي الرابطات العامة، فإن استدعيت تابعًا أو بانيًا بالخطأ في الوضع الصارم، فستعطيك جافاسكربت الخطأ بمجرد محاولة قراءة شيء من this بدلًا من الكتابة في النطاق العام، فمثلًا، انظر الشيفرة التالية التي تستدعي دالة باني دون كلمة new المفتاحية كي لا تشير this فيها إلى كائن باني جديد: function Person(name) { this.name = name; } let osama = Person("Osama"); // oops console.log(name); // → Osama ينجح هنا هذا الاستدعاء الزائف إلى person، لكنه يعيد قيمةً غير معرَّفة، وينشئ رابطة name العامة؛ أما في الوضع الصارم فستكون النتيجة مختلفةً، انظر كما يلي: "use strict"; function Person(name) { this.name = name; } let osama = Person("Osama"); // forgot new // → TypeError: Cannot set property 'name' of undefined كما ترى فقد أخبرتنا اللغة مباشرةً بوجود خطأ ما، وهذا أكثر فائدةً لنا لا ريب. لحسن حظنا فستشتكي البواني constructors التي أُنشئت باستخدام صيغة class إذا استُدعيت من غير new، مما يجعل هذه المشكلة أقل إزعاجًا حتى في الوضع العادي أو خارج الوضع الصارم، وإضافةً إلى ما سبق، فيملك الوضع الصارم بعض الخصائص الأخرى، إذ يرفض إعطاء دالة ما عوامل متعددة بالاسم نفسه، كما يزيل بعض مزايا اللغة المسببة لمشاكل مثل تعليمة with التي لن نذكرها مرةً أخرى في هذه السلسلة لكثرة مشاكلها. لن يؤذيك ولن يضرك استخدام الوضع الصارم عن طريق كتابة `"use strict" في المجمل، وإنما ينفعك ويفيدك في اكتشاف المشاكل. الأنواع Types تريد بعض اللغات معرفة أنواع الرابطات والتعبيرات قبل تشغيل البرنامج، حيث ستخبرك مباشرةً إذا استُخدِم أحد الأنواع بصورة متناقضة؛ أما جافاسكربت فلا تنظر إلى الأنواع إلا عند تشغيل البرنامج، كما تحاول ضمنيًا تحويل القيم إلى الأنواع التي تتوقعها هي عادةً. لكن مع هذا، تقدم الأنواع إطار عمل framework مفيدًا عند الحديث عن البرامج، فتَنتج أكثر الأخطاء من الجهل بنوع القيمة الداخلة إلى دالة أو الخارجة منها، فإذا كانت عندك هذه المعلومات مكتوبةً، فسيقل احتمال حدوث تلك الأخطاء لا ريب، حيث تستطيع إضافة تعليق مثل الذي في الشيفرة التالية قبل دالة goalOrientedRobot التي أنشأناها في المقال السابق لتصف نوعها: // (VillageState, Array) → {direction: string, memory: Array} function goalOrientedRobot(state, memory) { // ... } هناك العديد من الاصطلاحات المتّبَعة لإدخال الأنواع في برامج جافاسكربت، وتحتاج الأنواع إلى تحديد مدى تعقيدها لتستطيع وصف ما يكفي من الشيفرة بحيث يكون مفيدًا، فماذا يكون النوع الخاص بدالة randomPick مثلًا التي تعيد عنصرًا عشوائيًا من مصفوفة ما؟ سنحتاج إلى تحديد متغير نوع type variable، وليكن T مثلًا الذي سيمثل أي نوع، وبذلك نستطيع إعطاء randomPick نوعًا مثل ‎([T]) → T، وهي دالة من مصفوفة مكونة من Ts إلى T. إذا كانت الأنواع الموجودة في البرنامج معروفةً، فسيتمكن الحاسوب من التحقق منها بدلًا عنك، وذلك ليُخرج لك الأخطاء قبل تشغيل البرنامج. هناك العديد من أشكال جافاسكربت التي تضيف الأنواع إلى اللغة وتتحقق منهم، لعل أشهرها لغة TypeScript، وننصحك بتجربتها إن كنت تريد إضافة بعض الصرامة إلى برامجك؛ أما في هذه السلسلة فسنعمل بجافاسكربت العادية. الاختبار إذا لم تعيننا اللغة على إيجاد الأخطاء، فيجب البحث عنهم بالطريقة الصعبة من خلال تشغيل البرنامج، وعندها سنرى إن كان سيعمل كما خططنا له أم لا، لكن تنفيذ هذا يدويًا مرةً بعد مرة ليس الطريقة المثلى للبرمجة، حيث تُعَدّ مزعجةً وغير عملية بما أنها تستغرق وقتًا طويلًا وجهدًا مضنيًا لاختبار كل شيء في كل مرة تغير فيها شيئًا واحدًا. كما سنستغل قدرة الحواسيب الهائلة في العمليات التكرارية، بما أن الاختبار في حد ذاته عملية تكرارية، فلمَ لا نجعل هذه العملية مؤتمتةً؟ وسيكون ذلك بكتابة برنامج اختبار يتحقق من البرنامج الخاص بنا. وقد تقف هنا لحظة لتقول ألم نرد وسيلةً لنقلل بها الجهد الواقع علينا؟ ونجيبك أن بلى، لكن الجهد الذي ستبذله مرةً واحدةً فقط في كتابة هذا الاختبار، سيغنيك طيلة العمل على البرنامج محل المشروع نفسه الذي بين يديك، وستشعر أن بيديك قوةً خارقةً لا تأخذ سوى بضع ثواني، حيث سيتحقق من عمل برنامجك بكفاءة في كل المواقف التي كتبت اختبارات لها، وستلاحظ مباشرةً فور تعطيلك لشيء ما بالخطأ، بدلًا من مرور الخطأ وعدم الشعور به إلا حين تقابله صدفةً مرةً أخرى أثناء العمل على شيء جديد. تأخذ الاختبارات عادةً صورة برامج صغيرة معنونة لتتحقق من أجزاء بعينها في شيفرتك، فمثلًا، ستكون بعض الاختبارات القياسية لتابع toUpperCase -والتي لعل أحدًا غيرنا اختبرها من قبل- على الصورة التالية: function test(label, body) { if (!body()) console.log(`Failed: ${label}`); } test("convert Latin text to uppercase", () => { return "hello".toUpperCase() == "HELLO"; }); test("convert Greek text to uppercase", () => { return "Χαίρετε".toUpperCase() == "ΧΑΊΡΕΤΕ"; }); test("don't convert case-less characters", () => { return "مرحبا".toUpperCase() == "مرحبا"; }); تُنتِج كتابة الاختبارات التي تحاكي المثال أعلاه شيفرات متكررةً وغريبةً، ولحل هذه المشكلة فلدينا برامج ستساعدك على بناء وتشغيل تجميعات مختلفة من الاختبارات (حِزم اختبارات)، وذلك من خلال توفير لغة مناسبة في شكل دوال وتوابع تناسب كتابة الاختبارات، وكذلك بإخراج معلومات مفيدة حينما يفشل أحد تلك الاختبارات، ويطلق على ذلك عادةً اسم منفِّذات الاختبارات test runners. كلما زاد عدد الكائنات الخارجية التي تتعامل الشيفرة معها، صعُب إعداد سياق لاختبارها فيه، وقد كان أسلوب البرمجة الذي عرضناه في المقال السابع أسهل في الاختبار، حيث استخدَم قيمًا ثابتةً persistent values عوضًا عن كائنات متغيرة. التنقيح Debugging إذا عرفت أن ثمة شيء في برنامجك يجعله يتصرف على نحو لم تتوقعه أو ينتج أخطاءً، فالخطوة التالية منطقيًا هي معرفة ما هو ذلك الشي أو هذه المشكلة، وقد تكون تلك المشكلة واضحةً أحيانًا، إذ تشير رسالة الخطأ إلى سطر بعينه في برنامجك، حيث سترى المشكلة إذا نظرت إلى وصف الخطأ وذلك السطر بنفسك. لكن أحيانًا يكون السطر المسبب للمشكلة ضحيةً لاستخدام قيمة متذبذبة وغير مستقرة عليه، بحيث تكون تلك القيمة منتَجةً في مكان آخر، وتُستخدم في هذا السطر بصورة خاطئة، ومن المحتمل رؤيتك لهذا إن جربت حل التمارين التي في الفصول السابقة من هذه السلسلة. يحول البرنامج في المثال التالي العدد الصحيح إلى سلسلة نصية في نظام ما سواءً كان ثنائيًا، أو عشريًا، أو غيرهما، وذلك بأخذ آخر رقم، ثم تقسيم العدد للتخلص من ذلك الرقم، وهكذا دواليك، لكن يشير الخرج الذي ينتجه البرنامج الآن إلى وجود خطأ ما. function numberToString(n, base = 10) { let result = "", sign = ""; if (n < 0) { sign = "-"; n = -n; } do { result = String(n % base) + result; n /= base; } while (n > 0); return sign + result; } console.log(numberToString(13, 10)); // → 1.5e-3231.3e-3221.3e-3211.3e-3201.3e-3191.3e-3181.3… لعلك انتبهت إلى المشكلة إذا نظرت إلى الشيفرة أعلاه، لكن نريدك التخيل للحظة أنك لا تعرفها ولم تلاحظها. لنبحث في سياق الحل والتنقيح الذي يجب عمله، إذ نعلم بعدم تصرف برنامجنا على النحو الذي نريده ونريد معرفة السبب، فهنا يجب مقاومة الرغبة في إجراء تغييرات عشوائية في الشيفرة من دون تفكير مسبق، وتحليل للقرار والتغييرات التي تجريها. نريدك الآن الوقوف للحظة، والتفكير، وتحليل الموقف وما يحدث مع البرنامج، وجمع الملاحظات حول ذلك، للخروج بنظرية حول سبب الخطأ، ثم اختبار تلك النظرية، وستكون إحدى طرق ذلك بوضع بعض استدعاءات console.log في البرنامج لتحصل على معلومات إضافية عما يفعله، كما نريد هنا في حالتنا لـ n أخذ القيم 13، و1، ثم 0. لنكتب قيمتها في بداية الحلقة التكرارية: 13 1.3 0.13 0.013 … 1.5e-323 لا تعطي قسمة 13 على 10 عددًا صحيحًا، لذلك نريد n = Math.floor(n / base)‎ بدلًا من n /= base كي يتحرك العدد إلى اليمين كما نريد. المزايا التي توفرها أداة المنقِّح debugger والتي تأتي مدمجةً في المتصفحات هي إحدى الوسائل التي نستطيع استخدامها كي ننظر في سلوك البرنامج ونختبره، إذ تكون في هذه المتصفحات مزية إنشاء نقطة توقف breakpoint عند سطر بعينه داخل البرنامج، حيث سيتوقف تنفيذ البرنامج عند وصوله إلى ذلك السطر كي تنظر أنت في قيم الرابطات عند هذه النقطة وتفحصها، ولأن المنقِّحات تختلف من متصفح لآخر، فلن نخوض بك في تفاصيل هذه المزايا أكثر من ذلك، إذ سنترك لك حرية النظر في أدوات المطور في متصفحك، أو البحث في شبكة الويب عن مزيد من المعلومات عن هذا الأمر. بالعودة إلى فحص سلوك البرنامج، فمن الممكن إنشاء نقطة توقف بوضع تعليمة debugger في برنامجك، وهي تتكون من تلك الكلمة المفتاحية فقط، فإن كانت أدوات المطور مفعَّلة في متصفحك، فسيتوقف البرنامج عند وصوله إلى هذه التعليمة. توليد الخطأ لا يمكن للمبرمج منع كل الأخطاء الوارد حدوثها في البرنامج، خاصةً إذا كان البرنامج يتواصل مع العالم الخارجي بأيّ طريقة كانت، إذ من الممكن تلقيه مدخلات خاطئة، أو تحميله بأحمال ومهام زائدة عن طاقته، أو تفشل الشبكة التي يتواصل من خلالها، وذلك على سبيل المثال لا الحصر. لا تشغل بالك بشأن تلك المشاكل إذا كنت تبرمج لنفسك فقط، حيث تستطيع تجاهلها إلى حين حدوثها، ثم تتعامل معها حينها، لكن إن كنت تبني برنامجًا أو أداةً لعميل لك، أو برنامجًا سيستخدمه غيرك، فيجب أن تضع في حساباتك احتمالات التصرفات غير السليمة وغير المتوقعة، فقد يكون الحل الأمثل أحيانًا بتجاهل المدخل ليتابع البرنامج العمل، أو تبليغ المستخدم برسالة تفيد ما حدث، لكن يتوجب على البرنامج فعل شيء ما على أساس استجابة للمشكلة الواقعة في أي حالة كانت. لنقل مثلًا أن لديك دالةً اسمها promptNumber، حيث تطلب من المستخدِم إدخال عدد ثم تعيده هي، فلو أدخل المستخدم كلمةً مثل "orange" مثلًا، فما الذي ستعيده هذه الدالة؟ أحد الخيارات المتاحة هي جعل الدالة تعيد قيمةً خاصةً، مثل null، أو undefined، أو ‎-1 كما يلي: function promptNumber(question) { let result = Number(prompt(question)); if (Number.isNaN(result)) return null; else return result; } console.log(promptNumber("How many trees do you see?")); يجب على أيّ شيفرة تستدعي promptNumber الآن التحقق من قراءة العدد الفعلي، وإذا فشلت فيجب إصلاح ذلك بطريقة ما، ربما بإعادة السؤال، أو بكتابة قيمة افتراضية، أو بإعادة قيمة خاصة إلى مستدعيها للإشارة إلى فشلها في فعل ما طُلب منها. وسترى أن مواقف عديدة يصلحها إعادة قيمة خاصة للإشارة إلى خطأ ما، خاصةً في حالة شيوع الخطأ ووجوب وضعه في الحسبان صراحةً؛ لكن هذا له سيئاته، فماذا لو كانت الدالة تستطيع إعادة جميع القيم الممكنة؟ فسيكون عليك فعل شيء ما عند التعامل مع تلك الدالة مثل تغليف النتيجة بكائن لتتمكن من تمييز حالة النجاح من الفشل. function lastElement(array) { if (array.length == 0) { return {failed: true}; } else { return {element: array[array.length - 1]}; } } والمشكلة الثانية عند إعادة قيم خاصة هي أن هذه الإعادة تؤدي إلى شيفرات غريبة، فإن استدعى جزء من الشيفرة الدالة promptNumber عشرة مرات، فعليه التحقق من إعادة null عشرة مرات أيضًا ، وإن كانت إجابته في التحقق من null هي إعادة null نفسها، فعلى من يستدعي الدالة التحقق منها بدورها، وهكذا. الاعتراضات Exceptions إذا لم تستطع دالة ما تنفيذ وظيفتها على النحو الذي صممت من أجله، فسيكون الحل هو إيقاف ما نفعله وننتقل فورًا إلى المكان الذي يعرف كيف يعالج هذه المشكلة وذلك العجز، وهذا هو دور معالجة الاعتراضات exception handling. الاعتراضات ما هي إلا آليات تمكّن الشيفرة التي تواجه مشاكل من رفع اعتراض أو تبلغ به، وقد يكون الاعتراض أي قيمة، ويمكن تشبيه هذا البلاغ أو الرفع بإعادة مشحونة نوعًا ما من الدالة، حيث تقفز من الدالة الحالية وممن استدعاها أيضًا لتصل إلى الاستدعاء الأول الذي بدأ التنفيذ الحالي، ويسمى هذا فك المكدس unwinding the stack، ولعلك تذكر مكدس استدعاءات الدالة الذي ذكرناه في المقال الثالث من هذه السلسلة، إذ يصغِّر الاعتراض هذا المكدس، ملقيًا لكل سياقات الاستدعاء التي يقابلها. لن تكون الاعتراضات ذات فائدة إن ذهبت مباشرةً إلى قاع المكدس، وما زادت على أن أتت بطريقة جديدة لبعثرة البرنامج، وإنما تظهر قوتها حين تضع عقبات obstacles لالتقاط هذه الاعتراضات وهي ماضية في المكدس، فبمجرد التقاطك لاعتراض ما، فستستطيع التعامل معه ومعالجته لرؤية أصل المشكلة، ثم تتابع تشغيل البرنامج، انظر مثلًا كما يلي: function promptDirection(question) { let result = prompt(question); if (result.toLowerCase() == "left") return "L"; if (result.toLowerCase() == "right") return "R"; throw new Error("Invalid direction: " + result); } function look() { if (promptDirection("Which way?") == "L") { return "a house"; } else { return "two angry bears"; } } try { console.log("You see", look()); } catch (error) { console.log("Something went wrong: " + error); } تُستخدم كلمة throw المفتاحية لرفع الاعتراضات، وتُلتقط بتغليف جزء من الشيفرة في كتلة try، متبوعةً بكلمة catch المفتاحية، وحين تتسبب الشيفرة التي في كتلة try في رفع اعتراض، فستُقيَّم كتلة catch مع ربط الاسم الموجود بين أقواس بقيمة الاعتراض، وإذا انتهت كتلة catch، أو try دون مشاكل، فسيتابع البرنامج سيره أسفل تعليمة try/catch بكاملها. استخدمنا في هذه الحالة بانيError لإنشاء قيمة الاعتراض الخاصة بنا، وهو باني جافاسكربت قياسي ينشئ كائنًا مع خاصية message، كما تَجمع نُسَخ هذا الباني في أغلب بيئات جافاسكربت معلومات عن مكدس الاستدعاء الذي كان موجودًا عند إنشاء الاعتراض فيما يسمى بتعقب المكدس stack trace، وتخزن هذه المعلومات في خاصية stack، كما يمكن الاستفادة منها حين محاولة تصحيح مشكلة ما، إذ تخبرنا بالدالة التي حدثت فيها المشكلة وأي الدوال نفّذت هذه الاستدعاء الفاشل. تتجاهل دالة look احتمال أن promptDirection قد تخطئ، وهذه مزية كبيرة للاعتراضات، إذ لا تكون شيفرة معالجة الخطأ ضروريةً إلا عند النقطة التي يحدث فيها الخطأ وعند النقطة التي يعالَج فيها؛ أما الدوال التي بين ذلك فلا تكاد تكون مهمةً. التنظيف وراء الاعتراضات يُعَدّ تأثير الاعتراض نوعًا آخرًا من تدفق التحكم، فكل حدث يسبب اعتراض -وهو كل استدعاء دالة تقريبًا وكل وصول لخاصية- قد يجعل التحكم يترك شيفرتك فجأة. فإن كانت الشيفرة بها عدة آثار جانبية، فقد يمنع اعتراض ما بعض تلك الآثار من الحدوث، حتى لو كان تدفق التحكم المنتظم لها يشير إلى احتمال حدوثها كلها، فمثلًا، انظر إلى المثال التالي لشيفرة مصرفية سيئة. const accounts = { a: 100, b: 0, c: 20 }; function getAccount() { let accountName = prompt("Enter an account name"); if (!accounts.hasOwnProperty(accountName)) { throw new Error(`No such account: ${accountName}`); } return accountName; } function transfer(from, amount) { if (accounts[from] < amount) return; accounts[from] -= amount; accounts[getAccount()] += amount; } تحوِّل دالة transfer مبلغًا من المال من حساب ما إلى حساب آخر، مع طلب اسم الحساب الآخر أثناء التحويل، وإذا أُدخل اسم حساب غير صالح، فسترفع getAccount اعتراضًا. تنقل transfer المال أولًا من الحساب الأول، ثم تستدعي getAccount قبل إضافة المال إلى حساب جديد، فإن توقف سير عملها بسبب رفع اعتراض، فسسيختفي المال ويضيع بين الحسابين! يمكن كتابة تلك الشيفرة بأسلوب أذكى من ذلك من خلال استدعاء getAccount قبل البدء بنقل المال مثلًا، لكن للأسف فقد تحدث المشاكل المشابهة لهذه المشكلة بطريقة غير واضحة، كما تكون أقل أثرًا من أن تُلاحظ بمجرد النظر، فحتى الدوال التي لا يُتوقع منها رفع اعتراضات، قد ترفعها في ظروف استثنائية أو حين تحتوي على خطأ المبرمج. إحدى الطرق التي يمكن معالجة ذلك بها، هي التقليل من استخدام الآثار الجانبية، باستخدام أسلوب برمجة يحسب القيم الجديدة بدلًا من تغيير البيانات الموجودة فعليًا، على سبيل المثال لا الحصر، فإذا توقف جزء من الشيفرة عن العمل أثناء إنشاء قيمة جديدة، فلن يرى أحد هذه القيمة غير الجاهزة، ولن نرى مشكلةً من الأساس. لكن هذا قد لا يكون عمليًا في كل مرة، لذا سننظر في ميزة أخرى في تعليمة try، إذ يمكن أن تُتبَع بكتلة finally بدلًا من كتلة catch أو بالإضافة إليها، وتقول كتلة finally "شغِّل هذه الشيفرة مهما حدث، وذلك بعد محاولة تشغيل الشيفرة في كتلة try". انظر كما يلي: function transfer(from, amount) { if (accounts[from] < amount) return; let progress = 0; try { accounts[from] -= amount; progress = 1; accounts[getAccount()] += amount; progress = 2; } finally { if (progress == 1) { accounts[from] += amount; } } } تتتبع هذه النسخة من الدالة مدى تقدمها وسيرها، وإذا تسبب في حالة غير مستقرة للبرنامج عند خروجها، فإنها تصلح أثر هذا الخلل الذي أحدثته. لاحظ أن شيفرة finally رغم تشغيلها عند رفع اعتراض في كتلة try، إلا أنها لا تتدخل في الاعتراض نفسه، وعليه فإن المكدس سيستمر في تفكيك نفسه بعد تشغيل كتلة finally. كتابة مثل هذه البرامج التي تعمل بكفاءة ويعتمد عليها حتى في حالات ظهور اعتراضات في أماكن غير متوقعة أمر صعب وليس سهلًا. لا يبالي الكثير من الناس بهذا، فقد لا تحدث المشكلة إلا نادرًا جدًا بحيث لا يمكن ملاحظتها، وذلك بسبب عدم ظهور الاعتراضات إلا في الحالات الاستثنائية، وإذا أردنا القول بأن هذا شيء جيد أو سيء جدًا، فيجب النظر أولًا إلى الأثر والتخريب الذي يحدث عند فشل البرنامج أو تعطله. الالتقاط الانتقائي تعالج البيئة الاعتراض الذي يمر في المكدس كله دون التقاطه، ويختلف هنا ما يحدث باختلاف البيئة نفسها، ففي المتصفحات مثلًا، يُكتب وصف الخطأ إلى طرفية جافاسكربت والتي يمكن الوصول إليها من خلال أدوات المتصفح أو قائمة المطورين Developers Menu؛ أما في Node.js فستكون بيئة جافاسكربت الغير موجودة في متصفح والتي سنناقشها في مقال قادم، أكثر حذرًا بشأن تدمير البيانات، فتُخرِج العملية كلها عند حدوث اعتراض غير معالَج unhandled. بالنسبة لأخطاء المبرمجين، فأفضل شيء يمكن فعله هو ترك الخطأ يمر بسلام دون لمسه، كما يمثل الاعتراض الغير معالَج طريقةً معقولةً ومنطقيةً للإشارة إلى وجود عطل في البرنامج، وستعطيك طرفية جافاسكربت في المتصفحات الحديثة بعض المعلومات عن استدعاءات الدالة التي كانت في المكدس حين حدوث المشكلة؛ أما بالنسبة للمشاكل المتوقع حدوثها أثناء الاستخدام العادي، فسيُنبِئ توقف البرنامج بسبب اعتراض غير معالَج استراتيجيةً سيئةً جدًا. كما تتسبب الاستخدامات غير الصحيحة للغة مثل الإشارة المرجعية لرابطة غير موجودة، أو البحث عن خاصية في null، أو استدعاء شيء غير الدوال، في رفع اعتراضات، كما يمكن التقاط تلك الاعتراضات. كل ما نستطيع معرفته عند دخول متن catch هو أن شيئًا ما داخل متن try قد تسبب في رفع اعتراض، لكن لا نستطيع معرفة ماهية الاعتراض نفسه أو ما فعله. لا توفر جافاسكربت -في إغفال صارخ- دعمًا مباشرًا لاعتراضات الالتقاط الانتقائي، فإما تلتقطها كلها أو لا تدرك منها شيئًا، وقد يحلو للمرء افتراض أن الاعتراض الذي حصل عليه هو الذي كان يفكر فيه ويتوقعه حين كتب كتلة catch، بسبب هذا الخطأ في اللغة، لكن سيخبرك الواقع أنه قد لا يحدث هذا دومًا معك، فلعله تم اختراق افتراض آخر، أو لعلك تسببت في زلة أحدثت اعتراض؛ ويحاول المثال التالي استدعاء promptDirection إلى أن يحصل على إجابة صالحة: for (;;) { try { let dir = promtDirection("Where?"); // ← typo! console.log("You chose ", dir); break; } catch (e) { console.log("Not a valid direction. Try again."); } } تُعَدّ بُنية (;;)for طريقةً متعمَّدة لإنشاء حلقة تكرارية لا تنهي نفسها، ولا نخرج منها إلا حين حصولنا على اتجاه صالح، لكننا أخطأنا في تهجئة promptDirection التي أعطتنا الخطأ "متغير غير معرَّف" undefined variable، وتتعامل كتلة catch تعاملًا خاطئًا مع خطأ الرابطة على أنه مؤشر إدخال غير صالح، وذلك بسبب تجاهلها قيمة اعتراضها (e) مفترضةً أنها تعرف المشكلة، كما لا يتسبب هذا في حلقة لا نهائية فحسب، بل يدفن رسالة الخطأ المفيدة التي نريدها عن الرابطة التي أُخطئ في هجائها. تقول القاعدة العامة لا تَلتقط الاعتراضات التقاطًا كليًا إلا إذا كان بغرض توجيهها إلى مكان ما مثل توجيهها عبر الشبكة مثلًا لإخبار نظام آخر بتعطل برنامجنا، وعليك التفكير مليًا حتى حينئذ حول كيفية إخفاء المعلومات، وعلى ذلك فإننا نريد التقاط نوع محدد من الاعتراضات من خلال التحقق داخل كتلة catch مما إذا كان الاعتراض الذي حصلنا عليه هو الذي نريده أم لا، وإن لم يكن فنعيد رفعه، لكن كيف نعرف الاعتراض الذي نريده أصلًا؟ نستطيع موازنة خاصية message التابعة له برسالة الخطأ التي نتوقعها، لكن ليست هذه هي الطريقة المثلى للبرمجة، ففعلنا هذا ما هو إلا استخدام لمعلومات مخصصة لاطلاع العنصر البشري عليها أي الرسالة، وذلك لبناء قرار برمجي على هذه المعلومات، فإذا غير أحد هذه الرسالة أو ترجمها، فستتعطل الشيفرة مرةً أخرى، والحل البديل هو تعريف نوع جديد من الأخطاء واستخدام instanceof لتعريفه: class InputError extends Error {} function promptDirection(question) { let result = prompt(question); if (result.toLowerCase() == "left") return "L"; if (result.toLowerCase() == "right") return "R"; throw new InputError("Invalid direction: " + result); } يوسع صنف الخطأ الجديد Error، حيث لا يعرّف بانيًا خاصًا به، مما يعني أنه يرث باني Error الذي يتوقع رسالة نصية على أساس وسيط، كما لا يعرِّف أي شيء أصلًا، وهو -أي الصنف- فارغ. تتصرف كائنات InputError مثل كائنات Error باستثناء امتلاكها لصنف مختلف يمكننا تمييزها به، وتستطيع الحلقة التكرارية الآن التقاط هؤلاء بطريقة أكثر حذرًا: for (;;) { try { let dir = promptDirection("Where?"); console.log("You chose ", dir); break; } catch (e) { if (e instanceof InputError) { console.log("Not a valid direction. Try again."); } else { throw e; } } } وهذا سيلتقط نُسخًا من InputError فقط ويترك الاعتراضات غير المرتبطة به تمر، فإذا أعدت إدخال خطأ الهجاء مرةً أخرى، فسيبَلَّغ عن خطأ رابطة غير معرَّفة هذه المرة. التوكيدات Assertions التوكيدات هي عمليات تحقق داخل البرنامج، حيث تنظر هل الشيء موجود على الصورة التي يفترض به أن يكون عليها أم لا، وتُستخدَم للبحث عن أخطاء المبرمجين، وليس لمعالجة مواقف يمكن حدوثها في التشغيل العادي، فمثلًا، إذا وُصف firstElement على أساس دالة لا يمكن استدعاؤها على مصفوفة فارغة، فربما نكتبها كما يلي: function firstElement(array) { if (array.length == 0) { throw new Error("firstElement called with []"); } return array[0]; } ستبعثِر الشيفرة السابقة برنامجك بمجرد إساءة استخدامها، بدلًا من إعادة undefined بصمت مثل التي تحصل عليها حين تقرأ خاصية مصفوفة غير موجودة، ويجعل ذلك من الصعب لمثل تلك الأخطاء المرور دون ملاحظتها من أحد، وأسهل في معرفة سببها عند وقوعها. لكن هذا لا يعني أننا ننصح بكتابة توكيدات لكل نوع من المدخلات الخاطئة، فهذا عمل كثير جدًا، كما سينشئ لنا شيفرة غير صافية وملأى بأكواد غير ضرورية، بل احفظ هذه التوكيدات من أجل أخطاء يسهل ارتكابها، أو أخطاء تقع فيها بنفسك. خاتمة المدخلات الخاطئة والأخطاء عمومًا هي أشياء لازمة لنا في الحياة، ومن المهم في البرمجة العثور عليها، وتشخيصها، وإصلاحها، حيث ستظهر هنا في البرمجة على صورة زلات برمجية bugs. وقد تصير المشاكل أسهل في ملاحظتها إذا كان لديك حزمة اختبار مؤتمتة، أو إذا أضفت توكيدات إلى برامجك. مشكلة الأخطاء البرمجية موجودة بكل اللغات ومن المهم معرفتها والتعامل معها. يمكنك الاستعانة بالفيديو الآتي لمعرفة الأخطاء في مجال البرمجة عمومًا: يجب معالجة المشاكل التي تحدث بسبب عوامل خارجة عن تحكم البرنامج بلطف وحكمة، فقد تكون القيم الخاصة المعادة هي الطريقة المثلى لتتبع هذه المشاكل ومعالجتها، وإلا فربما نود استخدام الاعتراضات. سيتسبب رفع الاعتراضات في فك مكدس الاستدعاء حتى يصل إلى كتلة try/catch أو إلى نهاية المكدس، وستُعطَى قيمة الاعتراض إلى كتلة catch التي تلتقطها، مما يؤكد لنا أن هذا هو نوع الاعتراض الذي نريده قبل إجراء أي فعل عليه، كما يمكن استخدام كتل finally للمساعدة في تحديد تدفقات التحكم غير المتوقعة التي تحدث بسبب الاعتراضات، وذلك من أجل التأكد من تشغيل جزء من الشيفرة بعد انتهاء الكتلة. تدريبات Retry لنقل أنه لديك دالة primitiveMultiply التي تضرب عددين معًا في 20 بالمائة من الحالات، وترفع اعتراضًا في الثمانين بالمائة الباقية من نوع MultiplicatorUnitFailure. اكتب دالة تغلف هذه الدالة وتظل تحاول حتى نجاح أحد الاستدعاءات، كما تعيد النتيجة بعد ذلك، وتأكد من معالجة الاعتراضات التي تريد معالجتها فقط. تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen. class MultiplicatorUnitFailure extends Error {} function primitiveMultiply(a, b) { if (Math.random() < 0.2) { return a * b; } else { throw new MultiplicatorUnitFailure("Klunk"); } } function reliableMultiply(a, b) { // شيفرتك هنا. } console.log(reliableMultiply(8, 8)); // → 64 إرشادات للحل يجب حدوث استدعاء primitiveMultiply داخل كتلة try قطعًا، ويجب على كتلة catch الموافقة لهذا رفع اعتراض مرةً أخرى إذا لم تكن نسخةً من MultiplicatorUnitFailure، كما تتأكد من إعادة محاولة الاستدعاء مرةً أخرى حين تكون نسخةً منها. استخدِم حلقةً تكراريةً لتكرار المحاولة، بحيث لا تتوقف إلا عند نجاح الاستدعاء، كما في مثال look الذي ذكرناه آنفًا في هذا المقال، أو استخدِم التعاودية recursion على أمل عدم الحصول على سلسلة نصية من الفشل لفترة طويلة بحيث تؤدي إلى طفحان المكدس، وهذا هو الخيار الآمن بالمناسبة. الصندوق المغلق انظر الكائن التالي: const box = { locked: true, unlock() { this.locked = false; }, lock() { this.locked = true; }, _content: [], get content() { if (this.locked) throw new Error("Locked!"); return this._content; } }; ما هذا إلا صندوق به قفل، وهناك مصفوفة داخل الصندوق، حيث لا تستطيع الوصول إليها إلا حين يُفتح الصندوق، وأنت ممنوع من الوصول إلى خاصية ‎_content الخاصة مباشرةً. اكتب دالة اسمها withBoxUnlocked تأخذ قيمة دالة على أساس وسيط، وتفتح الصندوق، كما تشغِّل الدالة، ثم تتأكد أن الصندوق مقفل مرةً أخرى قبل الإعادة، بغض النظر عن إعادة الدالة الوسيطة بطريقة طبيعية أو رفع اعتراض. تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen. const box = { locked: true, unlock() { this.locked = false; }, lock() { this.locked = true; }, _content: [], get content() { if (this.locked) throw new Error("Locked!"); return this._content; } }; function withBoxUnlocked(body) { // شيفرتك هنا. } withBoxUnlocked(function() { box.content.push("gold piece"); }); try { withBoxUnlocked(function() { throw new Error("Pirates on the horizon! Abort!"); }); } catch (e) { console.log("Error raised: " + e); } console.log(box.locked); // → true للمزيد من النقاط، حين يكون الصندوق مغلقًا، تأكد من بقائه مغلقًا إذا استدعيت withBoxUlocked. إرشادات للحل يستدعي هذا التدريب كتلة finally، ويجب على الدالة فتح الصندوق أولًا، ثم تستدعي الدالة الوسيطة من داخل متن كتلة try، وبعدها تقوم كتلة finally التي تليها بغلق الصندوق. للتأكد من عدم إغلاق الصندوق إذا لم يكن مغلقًا من البداية، تحقق من إغلاقه عند بدء الدالة، ولا تفتحه وتغلقه إلا إذا كان مغلقًا في البداية. ترجمة -بتصرف- للفصل الثامن من كتاب Elequent Javascript لصاحبه Marijn Haverbeke. اقرأ أيضًا المقال السابق: مشروع تطبيقي لبناء رجل آلي (روبوت) عبر جافاسكريبت التعامل مع الأخطاء، جرب... التقط try..catch في جافاسكربت البواني وتهيئة الكائنات Object Initialization في جافا
  15. يختلف هذا المقال قليلًا عن باقي الفصول، إذ سأدَع الحديث عن النظريات قليلًا لننشئ نحن وأنت برنامجًا سويًا، فالنظرية وإن كانت مهمة جدًا لتعلم كيفية البرمجة، إلا أنّ قراءة البرامج الحقيقية وفهم شيفراتها لا تقل أهمية، ومشروعنا في هذا المقال هو بناء روبوت ينفِذ مهمة في عالم افتراضي، وستكون تلك المهمة توصيل الطرود واستلامها. قرية المرج Meadowfield سيعمل الروبوت الخاص بنا في قرية صغيرة تُدعى قرية المرج، حيث يوجد فيها أحد عشر مكانًا بينها أربعة عشر طريقًا، ويمكن وصف القرية بالمصفوفة التالية: const roads = [ "Salma's House-Omar's House", "Salma's House-Cabin", "Salma's House-Post Office", "Omar's House-Town Hall", "Sara's House-Mostafa's House", "Sara's House-Town Hall", "Mostafa's House-Sama's House", "Sama's House-Farm", "Sama's House-Shop", "Marketplace-Farm", "Marketplace-Post Office", "Marketplace-Shop", "Marketplace-Town Hall", "Shop-Town Hall" ]; وتكوِّن شبكة الطرق في القرية مخططًا graph، وهو عبارة عن تجميعة من نقاط لتمثل الأماكن في القرية، مع خطوط تصل بينها بحيث تمثل الطرق، وسيكون هذا هو العالم الذي سيتحرك الروبوت فيه. لكن ليس من السهل التعامل مع مصفوفة السلاسل النصية السابقة، إذ لا نريد إلا الوجهات التي يمكننا الذهاب إليها من أي نقطة معطاة لنا، وعلى ذلك فسنحوِّل قائمة الطرق إلى هيكل بيانات يخبرنا بالمناطق التي نستطيع الذهاب إليها من كل نقطة أو مكان، انظركما يلي: function buildGraph(edges) { let graph = Object.create(null); function addEdge(from, to) { if (graph[from] == null) { graph[from] = [to]; } else { graph[from].push(to); } } for (let [from, to] of edges.map(r => r.split("-"))) { addEdge(from, to); addEdge(to, from); } return graph; } const roadGraph = buildGraph(roads); تُنشئ دالة buildGraph كائن خارطة map object عند إعطائها مصفوفة من الحدود edges، حيث تخزن فيها لكل عقدةٍ مصفوفةً من العقد المتصلة بها. كما تستخدِم تابع method للذهاب من سلسلة الطريق النصية التي تحمل الصيغة "Start-End" إلى مصفوفات من عنصرين تحتوي على البداية والنهاية على أساس سلسلتَين منفصلتين. المهمة سيتحرك الروبوت داخل القرية، إذ لديه طرود في أماكن مختلفة فيها، حيث يحمل كل طرد عنوانًا يجب نقله إليه؛ وسيلتقط الروبوت الطرودَ حين وصوله إليها، ثم يتركها عند وصوله إلى الوجهة المرسَل إليها، ويجب أن يقرر في كل نقطة وجهته التالية، ولا تنتهي مهمته إلا عند توصيل جميع الطرود. لكن أولًا يجب تعريف عالم افتراضي يصف هذه العملية، وذلك لنتمكّن من محاكاته، ويكون هذا النموذج قادر على إخبارنا بموقع الروبوت والطرود معًا، كما يجب تحديث هذا النموذج عندما يقرر الروبوت الذهاب إلى مكان جديد. إن كنت تفكر بأسلوب البرمجة كائنية التوجه، سيكون دافعك الأول هو البدء بتعريف كائنات للعناصر المختلفة في هذا العالم الذي أنشأناه، فصنف للروبوت، وآخر للطرد، وصنف ثالث للأماكن ربما، ثم تحمِل هذه الأصناف بعد ذلك خصائصًا تصف حالتها، كما في حالة كومة الطرود عند موقع ما، والتي يمكننا تغييرها عند إجراء تحديث لهذا العالم. لكن هذا خطأ في أغلب الحالات على الأقل، فكون شيء ما يحاكي الكائن لا يعني وجوب معاملته على أساس كائن في برنامجك؛ كما أنّ كتابة الأصناف في برنامجك دون داعي حقيقي لها، ستُنشئ تجميعةً من الكائنات المرتبطة ببعضها بعضًا لكل منها حالة داخلية متغيرة، وتكون مثل تلك البرامج صعبة الفهم وسهلة التعطل. لدينا أسلوبًا أفضل من ذلك، وهو ضغط حالة القرية إلى أقل فئة ممكنة من القيم التي تعرِّفها، فيكون لدينا الموقع الحالي للروبوت، وتجميعة الطرود غير المسلَّمة، والتي يحمل كل منها موقعه الحالي وعنوان التسليم، وحسبنا هذا! بينما نحن في ذلك، فعلينا التأكد أنه حين يتحرك الروبوت من موقعه، فعلينا حساب حالة جديدة وفق الموقف الذي يكون بعد التحرك، وذلك دون تغيير الحالة الأولى، انظر كما يلي: class VillageState { constructor(place, parcels) { this.place = place; this.parcels = parcels; } move(destination) { if (!roadGraph[this.place].includes(destination)) { return this; } else { let parcels = this.parcels.map(p => { if (p.place != this.place) return p; return {place: destination, address: p.address}; }).filter(p => p.place != p.address); return new VillageState(destination, parcels); } } } يتحقق التابع move أولًا إن كان ثمة طريق من الموقع الحالي إلى موقع الوِجهة، وإن لم يكن، فسيعيد الحالة القديمة بما أنّ هذه الخطوة غير صالحة، ثم يُنشئ حالةً جديدةً يكون فيها موقع الوِجهة هو الموقع الجديد للروبوت. لكن سيحمل هذا الروبوت معه طرودًا أخرى غير هذا الطرد، ويأخذها معه إلى موقع تسليم الطرد المسمى، ثم يتابع حملها معه بعد تسليم الطرد، ليذهب بكل منها إلى موقع تسليمه الذي يخصه، ولكي نحصل على بيانات تلك الطرود عند أي نقطة زمنية نريدها، فسنحتاج إلى إنشاء فئة set جديدة نضع فيها تلك الطرود التي يحملها إلى الموقع الجديد، ثم يترك الطرود الواجب تسليمها في موقع التسليم، أي نحذفها من فئة الطرود غير المسلَّمة. ويتكفل بعملية الانتقال استدعاء map، بينما نستدعي filter ليتولى عملية التسليم. لا تتغير كائنات الطرود عند نقلها، وإنما يعاد إنشاؤها، ويعطينا التابع move حالة جديدة للقرية مع ترك الحالة القديمة كما هي دون تغيير، انظر كما يلي: let first = new VillageState( "Post Office", [{place: "Post Office", address: "Salma's House"}] ); let next = first.move("Salma's House"); console.log(next.place); // → Salma's House console.log(next.parcels); // → [] console.log(first.place); // → Post Office تُسلَّم الطرود عند مواضع تسليمها مع حركة الروبوت بين تلك المواقع، ويُرى أثر ذلك في الحالة التالية، لكن ستظل الحالة الابتدائية تصف الموقف الذي يكون فيه الروبوت عند مكتب البريد ومعه الطرد الغير مسلَّم بعد. البيانات الثابتة Persistent Data تُسمى هياكل البيانات التي لا تتغير بالهياكل الثابتة persistent، أو غير القابلة للتغير immutable، وتحاكي السلاسل النصية والأرقام في بقائها كما هي، بدلًا من احتواء أشياء مختلفة في كل مرة. بما أن كل شيء في جافاسكربت قابل للتغير تقريبًا، فسيتطلب العمل مع كائنات أو بيانات ثابتة جهدًا، وعملًا إضافيًا، ونوعًا من التقييد، حيث لدينا دالة Object.freeze من أجل هذا، إذ تؤثر على الكائن وتجمده، وذلك لتجعله يتجاهل الكتابة على خصائصه، فتستطيع استخدام ذلك لضمان ثبات كائناتك إن أردت، لكن تذكر أن هذا التجميد يعني أن الحاسوب سيبذل مزيدًا من الجهد. نفضل إخبار العامة بترك الكائن الفلاني وشأنه وعدم العبث به، آملين تذكّرهم ذلك، بدلًا من إخبارهم بتجاهل تحديثات بعينها. let object = Object.freeze({value: 5}); object.value = 10; console.log(object.value); // → 5 لكن هنا يبرز سؤال إن كنت منتبهًا، فإن كانت اللغة نفسها تحثنا على جعل كل شيء متغيرًا وتتوقع منا ذلك، فلماذا نخرج عن هذا المسلك لنجعل بعض الكائنات ثابتةً لا تقبل التغيير؟ الإجابة بسيطة وهي أن هذا سيساعدنا في فهم برامجنا أكثر، إذ يتعلق الأمر بإدارة التعقيد لبرامجنا، فإن كانت الكائنات التي في نظامنا ثابتةً ومستقرةً، فيمكننا إجراء عمليات عليها إجراءً معزولًا -حيث سيعطينا التحرك إلى منزل سلمى مثلًا من أي حالة بدء معطاة الحالة الجديدة نفسها في كل مرة-،؛ أما إن كانت الكائنات تتغير مع الوقت، فسيضيف هذا بعدًا جديدًا من التعقيد إلى العمليات والتفكير المنطقي لحل المشكلة. وربما تقول أن مسألة توصيل الطرود، والروبوت، وهذه القرية الصغيرة، سهل أمرها ويمكن إدارتها، وربما أنت محق، لكن المعيار الذي يحدد نوع النظم الممكن بناؤها هو فهمنا نحن لتلك الأنظمة ومدى ما يمكننا فهمه مطلقًا، فأيّ شيء يسرّع فهم شيفرتك، فسيمهد لك الطريق لبناء نظم أكثر تطورًا. لكن رغم سهولة فهم النظم المبنية على هياكل بيانات ثابتة، فقد يكون من الصعب تصميم نظام مثل هذا، وخاصةً إن لم تكن لغة البرمجة التي تستخدمها مصمَّمةً لذلك، كما سنبحث عن فرص استخدام هياكل البيانات الثابتة في هذه السلسلة مع استخدامنا للهياكل المتغيرة كذلك. المحاكاة ينظر روبوت التوصيل إلى العالم، ويقرر الاتجاه الذي يريد السير فيه، وعليه فيمكننا القول أنّ الروبوت هو دالة تأخذ كائن villageState، وتعيد اسم مكان قريب. لأننا نريد الربوتات أن تكون قادرة على تذكر أشياء بعينها كي تنفِّذ الخطط الموضوعة لها من قِبلنا، فسنمرر إليها ذاكرتها، ونسمح لها بإعادة ذاكرة جديدة، ومن ثم يعيد الروبوت كائنًا يحتوي الاتجاه الذي يريد السير فيه، وقيمة ذاكرة تُعطى إليه في المرة التالية التي يُستدعى فيها. function runRobot(state, robot, memory) { for (let turn = 0;; turn++) { if (state.parcels.length == 0) { console.log(`Done in ${turn} turns`); break; } let action = robot(state, memory); state = state.move(action.direction); memory = action.memory; console.log(`Moved to ${action.direction}`); } } لننظر ما الذي يجب على الروبوت فعله كي "يحل" حالة ما: يجب عليه التقاط جميع الطرود أولًا من خلال الذهاب إلى كل موقع فيه طرد، ثم يسلِّم تلك الطرود بالذهاب إلى كل عنوان من العناوين المرسل إليها هذه الطرود، حيث لا يذهب إلى موقع التسليم إلا بعد التقاط الطرد الخاص به. تُرى ما هي أغبى خطة يمكن أن تنجح هنا؟ إنها العشوائية لا شك، حيث يتخِذ الروبوت اتجاهًا عشوائيًا عند كل منعطف، مما يعني أنه سيمر لا محالة على جميع الطرود بعد عدد من المحاولات، وسيصل كذلك إلى موقع تسليمها في مرحلة ما. انظر: function randomPick(array) { let choice = Math.floor(Math.random() * array.length); return array[choice]; } function randomRobot(state) { return {direction: randomPick(roadGraph[state.place])}; } تعيد Math.random()‎ عددًا بين الصفر والواحد، ويكون دومًا أقل من الواحد، كما يعطينا ضرب عدد مثل هذا في طول أي مصفوفة ثم تطبيق Math.floor عليه موقعًا عشوائيًا للمصفوفة. وبما أنّ هذا الروبوت لا يحتاج إلى تذكر أي شيء، فسيَتجاهل الوسيط الثاني له ويهمل خاصية memory في كائنه المعاد؛ وتذكّر أنّه يمكن استدعاء دوال جافاسكربت بوسائط إضافية دون آثار جانبية مقلقة. نحتاج الآن إلى طريقة لإنشاء حالة جديدة بها بعض الطرود، وذلك من أجل إطلاق هذا الروبوت للعمل، والتابع الساكن -المكتوب هنا بإضافة خاصيةٍ مباشرةً للمنشئ- مكان مناسب لوضع هذه الوظيفة. VillageState.random = function(parcelCount = 5) { let parcels = []; for (let i = 0; i < parcelCount; i++) { let address = randomPick(Object.keys(roadGraph)); let place; do { place = randomPick(Object.keys(roadGraph)); } while (place == address); parcels.push({place, address}); } return new VillageState("Post Office", parcels); }; لا نريد أي طرود مرسلة من المكان نفسه الذي ترسَل إليه، ولهذا تختار حلقة do التكرارية أماكنًا جديدةً في كل مرة تحصل على مكان مطابق للعنوان. دعنا نبدأ عالمًا افتراضيًا، كما يلي: runRobot(VillageState.random(), randomRobot); // → Moved to Marketplace // → Moved to Town Hall // → … // → Done in 63 turns كما نرى من المثال، إذ غيّر الروبوت اتجاهه ثلاثًا وستين مرة، وذلك الرقم الكبير بسبب عدم التخطيط الجيد للخطوة التالية، كما سننظر في هذا قريبًا. يمكنك استخدام دالة runRobotAnimation المتاحة في البيئة البرمجية لهذا المقال، حيث ستنفِّذ المحاكاة، وستعرض لك الروبوت وهو يتحرك في خارطة القرية، بدلًا من إخراج نص فقط. runRobotAnimation(VillageState.random(), randomRobot); سندع طريقة تنفيذ runRobotAnimation مبهمةً في الوقت الحالي، حيث ستستطيع معرفة كيفية عملها بعد قراءة المقال الرابع عشر من هذه السلسلة والذي نناقش فيه تكامل جافاسكربت في المتصفحات. طريق شاحنة البريد لا شك أنّ فكرة الروبوت هذه لتوصيل البريد فكرة بدائية، وأجدر بنا تطوير هذا البرنامج قليلًا، فلم لا ننظر إلى توصيل البريد في العالم الحقيقي لنستوحي منه أفكارًا لعالمنا الصغير؟ أحد هذه الحلول هو البحث عن طريق يمر على جميع الأماكن في القرية، وحينها يأخذ الروبوت هذه الطريق مرتين. انظر مثلًا على هذا الطريق بدءًا من مكتب البريد: const mailRoute = [ "Salma's House", "Cabin", "Salma's House", "Omar's House", "Town Hall", "Sara's House", "Mostafa's House", "Sama's House", "Shop", "Sama's House", "Farm", "Marketplace", "Post Office" ]; نحتاج إلى الاستفادة من ذاكرة الروبوت إذا أردنا استخدام الروبوت المتتبع للطريق route-following، حيث يحذف الروبوت العنصر الأول عند كل منعطف ويحتفظ ببقية الطريق: function routeRobot(state, memory) { if (memory.length == 0) { memory = mailRoute; } return {direction: memory[0], memory: memory.slice(1)}; } سيكون هذا الروبوت بهذا الأسلوب الجديد أسرع من الأسلوب الأول، إذ سيكون أقصى عدد من الاتجاهات التي يسلكها أو المنعطفات التي يأخذها هو 26 -وهو ضعف عدد الأماكن في طريق القرية-، وإن كان في العادة أقل من هذا العدد، فليست جميع الأماكن بها طرود بريد في كل مرة. اكتشاف الطريق Pathfinding لنبحث في طريقة أكثر تطورًا من المرور على جميع الأماكن في القرية في كل مرة، فلا شك في أن الروبوت سيكون أكثر كفاءة إذا عدّل سلوكه ليناسب العمل الحقيقي المراد تنفيذه؛ لذا يجب امتلاكه القدرة على التحرك نحو طرد معين أو موقع تسليم بعينه، وعليه نحتاج دالة لإيجاد الطريق المناسبة. تُعَدّ مشكلة إيجاد الطريق باستخدام مخطط هي مشكلة البحث، حيث نستطيع تحديد ما إذا كان الحل المعطى -أي الطريق- مناسبًا أم لا، لكن لا نستطيع حساب الحل بالطريقة نفسها عند حساب حاصل جمع 2+2 مثلًا، وإنما يجب إنشاء حلولًا محتملةً لإيجاد واحد صالح. تتضح هنا المشكلة أكثر، إذ لا نهايةً لعدد الاحتمالات الممكنة للطرق من خلال مخطط، لكن يمكن تضييق عدد الاحتمالات إذا أردنا الطرق المؤدية من نقطة أ إلى نقطة ب مثلًا، فعندئذ لن يهمنا سوى الطرق التي تبدأ من النقطة أ، كما لا نريد الطرق التي تمر المكان نفسه مرتين، إذ لا تُعَدّ هي الطرق الأكثر كفاءة في أي مكان، ويقلل هذا عدد الطرق التي يجب اعتمادها من قِبَل دالة البحث أكثر وأكثر. نريد في الواقع أقصر طريق فقط، وعليه يجب النظر في الطرق الأقصر أولًا قبل النظر في الطرق الأطول، وأحد الأساليب لفعل ذلك هو بدء طرق من نقطة تحرك الروبوت لتُستكشف جميع الأماكن التي يمكن الوصول إليها ولم يُذهب إليها بعد، حتى تصل إحدى تلك الطرق إلى المكان المراد تسليم الطرد إليه، وهكذا نستكشف الطرق التي يحتمل أن تكون مفيدة لنا، ونتخذ أقصرها أو واحدًا من أقصر تلك الطرق. كما في الدالة التالية: function findRoute(graph, from, to) { let work = [{at: from, route: []}]; for (let i = 0; i < work.length; i++) { let {at, route} = work[i]; for (let place of graph[at]) { if (place == to) return route.concat(place); if (!work.some(w => w.at == place)) { work.push({at: place, route: route.concat(place)}); } } } } يجب أن يتم الاستكشاف بترتيب صحيح، فالأماكن التي يصل إليها أولًا يجب استكشافها أولًا، لكن انتبه إلى أن مكان س مثلًا لا يُستكشف فور الوصول إليه، إذ سيعني هذا أنه علينا استكشاف أماكن يمكن الوصول إليها من س وهكذا، رغم احتمال وجود مسارات أقصر لم تُستكشف بعد. وعلى هذا فإن الدالة تحتفظ بقائمة عمل work list، وهي مصفوفة من الأماكن التي يجب استكشافها فيما بعد مع الطرق التي توصلنا إليها، حيث تبدأ بموقع ابتدائي للبدء منه وطريق فارغة، ثم يبدأ البحث بعدها بأخذ العنصر التالي في القائمة واستكشافه، مما يعني النظر في جميع الطرق الخارجة من هذا المكان، فإن كان أحدها هو الهدف المراد، فيُعاد طريق تامة finished road، وإلا فسيضاف عنصر جديد إلى القائمة إن لم يُنظر في هذا المكان من قبل. كذلك، إن كنا قد نظرنا فيه من قبل، فقد وجدنا إما طريق أطول إلى هذا المكان أو واحدة بنفس طول الطريق الموجودة -وبما أننا نبحث عن الطرق الأقصر أولًا- ولا نحتاج إلى استكشافها عندئذ. تستطيع تخيل هذا بصريًا على أساس شبكة من الطرق المعروفة، حيث تخرج من موقع ابتدائي، وتنمو بانتظام في جميع الجوانب، لكن لا تتشابك مع بعضها أو تنعكس على نفسها؛ وبمجرد وصول خيط من تلك الشبكة إلى الموقع الهدف، فيُتتبَّع ذلك الخيط إلى نقطة البداية ليعطينا الطريق التي نريدها. لا تعالج شيفرتنا الموقف الذي لا تكون فيه عناصر عمل على قائمة العمل، وذلك لعِلمنا باتصال المخطط الخاص بنا، أي يمكن الوصول إلى كل موقع من جميع المواقع الأخرى، وسنكون قادرين دائمًا على إيجاد طريق ممتدة بين نقطتين، فلا يفشل البحث الذي نجريه. function goalOrientedRobot({place, parcels}, route) { if (route.length == 0) { let parcel = parcels[0]; if (parcel.place != place) { route = findRoute(roadGraph, place, parcel.place); } else { route = findRoute(roadGraph, place, parcel.address); } } return {direction: route[0], memory: route.slice(1)}; } يستخدِم هذا الروبوت قيمة ذاكرته على أساس قائمة من الاتجاهات ليتحرك وفقًا لها، تمامًا مثل الروبوت المتتبع للطريق الذي ذكرناه آنفًا، وعليه معرفة الخطوة التالية إذا وجد القائمة فارغة؛ كما ينظر في أول طرد غير مسلَّم في الفئة التي معه، فإن لم يكن قد التقطه، فسيرسم طريقًا إليه، وإن كان قد التقطه، فسينشئ طريقًا إلى موقع التسليم، انظر كما يلي: runRobotAnimation(VillageState.random(), goalOrientedRobot, []); ينهي هذا الروبوت مهمة تسليم خمسة طرود في نحو ستة عشر منعطفًا، وهذا أفضل بقليل من routeRobot، لكن لا زال هناك فرصة للتحسين أكثر من ذلك. تدريبات معايرة الروبوت من الصعب موازنة الروبوتات بجعلها تحل بعض السيناريوهات البسيطة، فلعل أحدها حصل على مهام سهلة دون الآخر. اكتب دالة compareRobots التي تأخذ روبوتين مع ذاكرتهما الابتدائية، وتولد مائة مهمة، ثم اجعل كل واحد من الروبوتين يحل هذه المهام كلها واحدة واحدة، ويجب أن يكون الخرج عند انتهائهما هو العدد المتوسط للخطوات التي قطعها كل واحد لكل مهمة. تأكد من إعطاء المهمة نفسها لكلا الروبوتين في تلك المهام المئة لضمان العدل والدقة في النتيجة، بدلًا من توليد مهام مختلفة لكل روبوت. تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen. function compareRobots(robot1, memory1, robot2, memory2) { // شيفرتك هنا } compareRobots(routeRobot, [], goalOrientedRobot, []); إرشادات للحل سيكون عليك كتابة صورة من دالة runRobot، بحيث تعيد عدد الخطوات التي قطعها الروبوت لإتمام المهمة، بدلًا من تسجيل الأحداث في الطرفية. عندئذ تستطيع دالة القياس توليد حالات جديدة في حلقة تكرارية، وعدّ خطوات كل روبوت؛ وحين تولد قياسات كافية، فيمكنها استخدام console.log لإخراج المتوسط لكل روبوت، والذي سيكون ناتج قسمة العدد الكلي للخطوات المقطوعة على عدد القياسات. كفاءة الروبوت هل تستطيع كتابة روبوت ينهي مهمة التوصيل أسرع من goalOrientedRobot؟ ما الأشياء التي تبدو غبيةً بوضوح؟ وكيف يمكن تطويرها؟ إن حللت التدريبات السابقة، فربما تود استخدام دالة compareRobots التي أنشأتَها قبل قليل للتحقق إن كنت قد حسّنت الروبوت أم لا. تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen. // شيفرتك هنا runRobotAnimation(VillageState.random(), yourRobot, memory); إرشادات للحل إن القيد الرئيسي لـ goalOrientedRobot هو تعاملها مع طرد واحد في كل مرة، وستمضي ذهابًا وإيابًا في القرية، وذلك لأن الطرد الذي تريده موجود على الناحية الأخرى من الخارطة، حتى لو كان في طريقها طرود أخرى أقرب. أحد الحلول الممكنة هنا هو حساب طرق جميع الطرود ثم أخذ أقصرها، ويمكن الحصول على نتائج أفضل إذا كان لدينا عدة طرق قصيرة، فسنختار حينها الطرق التي فيها التقاط طرد بدلًا من التي فيها تسليم طرد. المجموعة الثابتة ستجد أغلب هياكل البيانات الموجودة في بيئة جافاسكربت القياسية لا تناسب الاستخدام الثابت، حيث تملك المصفوفات التابعَين slice وconcat اللذَين يسمحان لنا بإنشاء مصفوفات جديدة دون تدمير القديمة، لكن Set مثلًا ليس فيه توابع لإنشاء فئة جديدة فيها عنصر مضاف إلى الفئة الأولى أو محذوف منها. اكتب صنف جديد باسم PGroup يشبه الصنف Group من المقال السادس، حيث يخزن مجموعة من القيم، وتكون له التوابع add، وdelete، وhas، كما في الصنف Group تمامًا. يعيد التابع add فيه نسخةً جديدةً من PGroup مع إضافة العضو المعطى given member وترك القديم دون المساس به، وبالمثل، فيجب على التابع delete إنشاء نسخةً جديدةً ليس فيها العضو المعطى. يجب أن يعمل هذا الصنف مع أي نوع من القيم وليس السلاسل النصية فقط، ولا تُشترط كفاءته عند استخدامه مع كميات كبيرة من القيم؛ كذلك ليس شرطًا أن يكون الباني constructor جزءًا من واجهة الصنف رغم أنك تريد استخدام ذلك داخليًا، وإنما هناك نسخة فارغة PGroup.empty يمكن استخدامها على أساس قيمة ابتدائية. لماذا تظن أننا نحتاج إلى قيمة PGroup.empty واحدة فقط بدلًا من دالة تنشئ خارطةً جديدةً وفارغةً في كل مرة؟ تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen. class PGroup { // شيفرتك هنا } let a = PGroup.empty.add("a"); let ab = a.add("b"); let b = ab.delete("a"); console.log(b.has("b")); // → true console.log(a.has("b")); // → false console.log(b.has("a")); // → false إرشادات للحل ستكون أفضل طريقة لتمثيل مجموعة من القيم الأعضاء مصفوفةً لسهولة نسخها. تستطيع إنشاء مجموعةً جديدةً حين تضاف قيمة ما إليها، وذلك بنسخ المصفوفة الأصلية التي فيها القيمة المضافة -باستخدام concat مثلًا-، وتستطيع ترشيحها من المصفوفة حين تُحذف تلك القيمة. يستطيع باني الصنف أخذ مثل هذه المصفوفة على أساس وسيط، ويخزنها على أنها الخاصية الوحيدة للنسخة، ولا تُحدَّث هذه المصفوفة بعدها. يحب إضافة الخاصية empty إلى باني غير تابع بعد تعريف الصنف، مثل وسيط عادي. تحتاج إلى نسخة empty واحدة فقط بسبب تطابق المجموعات الفارغة وعدم تغير نُسَخ الصنف، كما تستطيع إنشاء مجموعات مختلفة عديدة من هذه المجموعة الفارغة دون التأثير عليها. ترجمة -بتصرف- للفصل السابع من كتاب Elequent Javascript لصاحبه Marijn Haverbeke.
  16. تحدثنا في المقال الرابع عن الكائنات في جافاسكربت، ولدينا في ثقافة البرمجة شيء يسمى بالبرمجة كائنية التوجه، وهي مجموعة تقنيات تستخدم الكائنات والمفاهيم المرتبطة بها مثل مبدأ مركزي لتنظيم البرامج. ورغم عدم وجود إجماع على التعريف الدقيق للبرمجة كائنية التوجه هذه، إلا أنها قد غيرت شكل لغات برمجة كثيرة من حيث تصميمها، بما فيها جافاسكربت، وسنتعرض في هذا المقال للطرق التي يمكن تطبيق أفكار هذا المفهوم في جافاسكربت. التغليف Encapsulation تتلخص فكرة البرمجة كائنية التوجه في تقسيم البرامج إلى أجزاء صغيرة وجعل كل جزء مسؤولًا عن إدارة حالته الخاصة، وهكذا يمكن حفظ المعلومات الخاصة بالأسلوب الذي يعمل به جزء ما من البرنامج داخل ذلك الجزء فقط محليًا، بحيث إذا عمل شخص ما على جزء آخر من البرنامج، فليس عليه معرفة أو إدراك حتى هذه البيانات والمعلومات؛ وإذا تغيرت تلك التفاصيل المحلية، فلن نحتاج سوى إلى تعديل جزء الشيفرة المتعلق بها فقط. ويُطلق على فصل الواجهة عن الاستخدام نفسه أو التطبيق بـالتغليف، وهو فكرة عظيمة. تتفاعل الأجزاء المختلفة من البرامج مع بعضها البعض من خلال واجهات interfaces، وهي مجموعات محدودة من الدوال أو الرابطات bindings التي توفر أداءً مفيدًا في المستويات العليا التجريدية التي تخفي استخدامها الدقيق والمباشر. كما نُمذِجت مثل تلك الأجزاء باستخدام كائنات، وواجهاتها مكونة من مجموعات محددة من التوابع والخصائص، حيث يوجد نوعان من هذه الخصائص، إما عامة عندما تكون جزءًا من الواجهة، أو خاصة يجب ألا يقربها أي شيء خارج الشيفرة. توفر الكثير من اللغات طريقةً للتمييز بين الخصائص العامة والخاصة، وذلك لمنع الشيفرات الخارجية من الوصول إلى الخصائص الخاصة؛ أما جافاسكربت فلا تفعل ما سبق، إذ تتبع أسلوبها البسيط في ذلك حاليًا، ويوجد ثمة أعمال لإضافة ذلك إليها. رغم عدم دعم اللغة لهذه الخاصية في التفرقة، إلا أنّ مبرمجي جافاسكربت يفعلون ذلك من حيث المبدأ، فالواجهة المتاحة موصوفة ومشروحة في التوثيق أو التعليقات، ومن الشائع كذلك وضع شرطة سفلية (_) في بداية أسماء الخصائص للإشارة إلى أنها "خاصة". التوابع Methods التوابع ليست إلا خصائص حاملة لقيم الدوال، انظر المثال التالي لتابع بسيط: let rabbit = {}; rabbit.speak = function(line) { console.log(`The rabbit says '${line}'`); }; rabbit.speak("I'm alive."); // → The rabbit says 'I'm alive.' يُتوقع من التابع فعل شيء بالكائن الذي استدعي له، فحين تُستدعى دالة على أساس تابع -يُبحث عنها على أساس خاصية، ثم تُستدعى مباشرةً كما في حالة object.method()‎-، ستشير الرابطة التي استدعت this في متنها مباشرةً إلى الكائن الذي استُدعي عليه. function speak(line) { console.log(`The ${this.type} rabbit says '${line}'`); } let whiteRabbit = {type: "white", speak}; let hungryRabbit = {type: "hungry", speak}; whiteRabbit.speak("Oh my ears and whiskers, " + "how late it's getting!"); // → The white rabbit says 'Oh my ears and whiskers, how // late it's getting!' hungryRabbit.speak("I could use a carrot right now."); // → The hungry rabbit says 'I could use a carrot right now.' فكر في this على أساس معامِل إضافي يُمرَّر في صورة مختلف، فإذا أردت تمريره صراحةً، فستستخدِم تابع call الخاص بالدالة والذي يأخذ قيمة this على أساس وسيطها الأول، وتعامِل الوسائط التالية على أساس معامِلات عادية. speak.call(hungryRabbit, "Burp!"); // → The hungry rabbit says 'Burp!' وبما أنّ كل دالة لها رابطة this الخاصة بها، والتي تعتمد قيمتها على الطريقة التي المًستدعاة بها، فلا تستطيع الإشارة إلى this بنطاق مغلِّف في دالة عادية معرَّفة بكلمة function المفتاحية؛ أما الدوال السهمية فتختلف في عدم ارتباط this الخاص بها، لكنها تستطيع رؤية رابطة this للنطاق الذي حولها، وعليه ستستطيع تنفيذ شيء مثل ما في الشيفرة التالية، حيث تشير إلى this مرجعيًا من داخل دالة محلية: function normalize() { console.log(this.coords.map(n => n / this.length)); } normalize.call({coords: [0, 2, 3], length: 5}); // → [0, 0.4, 0.6] فلو كتبنا الوسيط إلى map باستخدام كلمة function المفتاحية، فلن تعمل الشيفرة. الوراثة عبر سلسلة prototype انظر المثال التالي: let empty = {}; console.log(empty.toString); // → function toString(){…} console.log(empty.toString()); // → [object Object] أرأيت كيف سحبنا خاصيةً من كائن فارغ؟! حيث لم نزد على الطريقة التي تعمل بها كائنات جافاسكربت في حفظ البيانات الخاصة، لأن أغلب الكائنات ترث من سلسلة prototype بكل ما تحويه من دوال وخاصيات وكائنات أخرى إضافةً إلى مجموعة خصائصها، وتلك ما هي إلا كائنات أخرى مستخدَمة على أساس مصدر احتياطي fallback للخصائص، فإذا طُلِب من كائن خاصية لا يملكها، فسيُبحث في سلسلة prototype عن تلك الخاصية، ثم في سلاسل prototype الخاصة بكل واحدة فيها على حدى، وهكذا. طيب، ما سلسلة prototype لذاك الكائن الفارغ؟ إنه object.prototype الذي يسبق الكائنات كلها، فإذا قلنا أنّ علاقات سلاسل prototype في جافاسكربت تكوِّن هيكلًا شجريًا، فسيكون جذر تلك الشجرة هو Object.prototype، إذ يوفِّر بعضَ التوابع التي تظهر في جميع الكائنات الأخرى مثل toString الذي يحول الكائن إلى تمثيل نصي string representation. لا تملك العديد من الكائنات Object.prototype مثل نموذجها الأولي (يطلق على سلسلة prototype نموذج أولي مجازًا بوصفه أول نموذج يمثل خاصيات ودوال يرثه الكائن عند إنشائه)، بل يكون لها كائنٌ آخر يوفر مجموعةً مختلفةً من الخصائص الافتراضية. console.log(Object.getPrototypeOf({}) == Object.prototype); // → true console.log(Object.getPrototypeOf(Object.prototype)); // → null كما تتوقع من المثال السابق، سيُعيد Object.getPrototypeOf سلسلة prototype للكائن. تنحدر الدوال من Function.prototype أما المصفوفات فتنحدر من Array.prototype، كما في المثال التالي: console.log(Object.getPrototypeOf(Math.max) == Function.prototype); // → true console.log(Object.getPrototypeOf([]) == Array.prototype); // → true سيكون لكائن النموذج الأولي المشابه لهذا، نموذج أولي خاص به وهو Object.prototype غالبًا، وذلك لاستمراره بتوفير توابع مثل toString؛ وتستطيع استخدام Object.create لإنشاء كائن مع نموذج أولي بعينه، كما في المثال التالي: let protoRabbit = { speak(line) { console.log(`The ${this.type} rabbit says '${line}'`); } }; let killerRabbit = Object.create(protoRabbit); killerRabbit.type = "killer"; killerRabbit.speak("SKREEEE!"); // → The killer rabbit says 'SKREEEE!' تُعَدّ خاصية مثل speak(line)‎ في تعبير الكائن طريقةً مختصرةً لتعريف تابع ما، إذ تنشِئ خاصيةً اسمها speak، وتعطيها دالةً على أساس قيمة لها؛ كما يتصرف الأرنب "proto" في المثال السابق على أساس حاوية للخصائص التي تشترك فيها جميع الأرانب؛ أما في حالة مثل الأرنب القاتل killer rabbit، فيحتوي على خصائص لا تنطبق إلا عليه -نوعه في هذه الحالة-، كما يأخذ خصائصًا مشتركةً من نموذجه الأولي. الأصناف Classes يحاكي نظام النماذج الأولية في جافاسكربت (أي سلسلة prototype كما أشرنا في الأعلى) مفهوم الأصناف Classes في البرمجة كائنية التوجه، حيث تحدِّد هذه الأصناف الشكل الذي سيكون عليه نوع ما من كائن، وذلك بتحديد توابعه وخصائصه، كما يُدعى مثل ذلك الكائن بنسخة instance من الصنف. تُعَدّ النماذج الأولية مفيدةً هنا في تحديد الخصائص المشتركة بين جميع نُسَخ الصنف التي لها القيمة نفسها مثل التوابع؛ أما الخصائص المختلفة بين كل نسخة -كما في حالة خاصية type لأرنبنا في المثال السابق-، فيجب تخزينها في الكائن نفسه مباشرةً. لذا عليك إنشاء كائنًا مشتقًا من النموذج الأولي المناسب من أجل إنشاء نسخة من صنف ما، لكن في الوقت نفسه يجب التأكد من امتلاكه الخصائص الواجب وجودها في نُسَخ ذلك الصنف، وهذا ما يمثل وظيفة دالة الباني constructor، انظر ما يلي: function makeRabbit(type) { let rabbit = Object.create(protoRabbit); rabbit.type = type; return rabbit; } توفر جافاسكربت طريقةً لتسهيل تعريف هذا النوع من الدوال، فإذا وضعتَ كلمة new المفتاحية أمام استدعاء الدالة مباشرةً، فستُعامَل الدالة على أساس باني، وهذا يعني أنه سيُنشَأ الكائن الذي يحمل النموذج الأولي المناسب تلقائيًا، بحيث يكون مقيدًا بـ this في الدالة، ثم يُعاد في نهاية الدالة، ويمكن العثور على كائن النموذج الأولي المستخدَم عند بناء الكائنات من خلال أخذ خاصية protoype لدالة الباني. function Rabbit(type) { this.type = type; } Rabbit.prototype.speak = function(line) { console.log(`The ${this.type} rabbit says '${line}'`); }; let weirdRabbit = new Rabbit("weird"); تحصل البواني، بل كل الدوال، على خاصية اسمها prototype تحمل بدورها كائنًا فارغًا مشتقًا من Object.prototype، وتستطيع استبدال كائن جديد به إن شئت أو إضافة خصائص إلى الكائن الجديد كما في المثال. تتكوّن أسماء البواني من الحروف الكبيرة لتمييزها عما سواها، ومن المهم إدراك الفرق بين الطريقة التي يرتبط بها النموذج الأولي بالباني من خلال خاصية prototype، والطريقة التي يكون للكائنات فيها نماذج أولية -والتي يمكن إيجادها باستخدام Object.getPrototypeOf. Function.Prototype هو النموذج الأولي الفعلي للباني بما أنّ البواني ما هي إلا دوال في الأصل، وتحمل خاصية prototype الخاصة به النموذج الأولي المستخدَم للنسخ التي أنشِئت من خلاله. console.log(Object.getPrototypeOf(Rabbit) == Function.prototype); // → true console.log(Object.getPrototypeOf(weirdRabbit) == Rabbit.prototype); // → true صياغة الصنف Class Notation ذكرنا أن أصناف جافاسكربت ما هي إلا دوال بانية مع خاصية النموذج الأولي، وقد كان ذلك حتى عام 2015؛ أما الآن فقد تحسنت الصيغة التي صارت عليها كثيرًا، انظر إلى ما يلي: class Rabbit { constructor(type) { this.type = type; } speak(line) { console.log(`The ${this.type} rabbit says '${line}'`); } } let killerRabbit = new Rabbit("killer"); let blackRabbit = new Rabbit("black"); تبدأ كلمة class المفتاحية تصريح صنفٍ يسمح لنا بتعريف باني ومجموعة توابع في مكان واحد، كما يمكن كتابة أيّ عدد من التوابع بين قوسي التصريح، لكن يُعامَل التابع الحامل لاسم constructor معاملةً خاصةً، إذ يوفِّر وظيفة الباني الفعلية التي ستكون مقيدة بالاسم Rabbit، في حين تُحزَّم التوابع الأخرى في النموذج الأولي لذلك الباني، ومن ثم يكون تصريح الصنف الذي ذكرناه قبل قليل مكافئًا لتعريف الباني من القسم السابق، كونه يبدو أفضل للقارئ. ولا تسمح تصريحات الأصناف حاليًا إلا بإضافة التوابع إلى النموذج الأولي، وهي الخصائص التي تحمل دوالًا، رغم أن ذلك قد يكون مرهقًا إذا أردت حفظ قيمة غير دالّية non-function هناك، وقد يتحسن ذلك في الإصدار القادم من اللغة، لكن حتى ذلك الحين تستطيع إنشاء مثل تلك الخصائص بتغيير النموذج الأولي مباشرةً بعد تعريف الصنف. يمكن استخدام class في التعليمات والتعابير على حد سواء، وشأنها في ذلك شأن function، حيث لا تعرِّف رابطةً عند استخدامها على أساس تعبير، وإنما تنتج الباني كقيمة فقط. وتستطيع إهمال اسم الصنف في تعبير الصنف، كما في المثال التالي: let object = new class { getWord() { return "hello"; } }; console.log(object.getWord()); // → hello إعادة تعريف الخصائص المشتقة تُضاف الخاصية إلى الكائن نفسه عند إضافتها إليه سواءً كان موجودًا في النموذج الأولي أم غير موجود، فإن كان ثمة خاصية موجودة بالاسم نفسه في النموذج الأولي، فلن تؤثِّر هذه الخاصية في الكائن بما أنها مخفية الآن خلف الخاصية التي يملكها الكائن. Rabbit.prototype.teeth = "small"; console.log(killerRabbit.teeth); // → small killerRabbit.teeth = "long, sharp, and bloody"; console.log(killerRabbit.teeth); // → long, sharp, and bloody console.log(blackRabbit.teeth); // → small console.log(Rabbit.prototype.teeth); // → small يبيّن المخطط التالي الموقف بعد تشغيل الشيفرة السابقة، إذ يقبع النموذجين الأوليَين لـ Rabbit، وObject خلف killerRabbit على أساس حاجز خلفي له، بينما يُبحث عن الخصائص التي ليست موجودة في الكائن. وتبدو فائدة إعادة تعريف الخصائص overriding properties الموجودة في النموذج الأولي في التعبير عن الخصائص الاستثنائية في نُسَخ الأصناف العامة للكائنات، كما في مثال أسنان الأرنب rabbit teeth السابق، مع السماح للكائنات غير الاستثنائية بأخذ قيمة قياسية من نموذجها الأولي. كما يمكن استخدام إعادة التعريف لإعطاء تابع toString للنماذج الأولية للدالة والمصفوفة القياسيتين، بحيث يختلف عن النموذج الأساسي للكائن. console.log(Array.prototype.toString == Object.prototype.toString); // → false console.log([1, 2].toString()); // → 1,2 يعطي استدعاء toString على مصفوفة نتيجةً محاكيةً لاستدعاء join(",")‎. عليها، إذ تضع فواصل إنجليزية بين القيم الموجودة في المصفوفة؛ أما الاستدعاء المباشر لـ Object.prototype.toString مع مصفوفة، فينتج سلسلةً نصيةً مختلفةً، حيث تضع كلمة object واسم النوع بين أقواس مربعة، وذلك لعدم معرفة تلك الدالة بشأن المصفوفات، كما في المثال التالي: console.log(Object.prototype.toString.call([1, 2])); // → [object Array] الخرائط Maps استخدمنا كلمة map في المقال السابق في عملية تحويل هيكل البيانات بتطبيق دالة على عناصره، رغم بعد معنى الكلمة نفسها، التحويل، الدال عن الفعل الذي تنفذه، وهنا أيضًا وفي البرمجة عمومًا، فتُستخدَم هذه الكلمة كذلك لغرض مختلف لكنه قريب مما رأينا، وكلمة map على أساس اسم هي أحد أنواع هياكل البيانات الذي يربط القيم (المفاتيح) بقيم أخرى، فإذا أردت ربط الأسماء بأعمار مقابلة لها، فتستطيع استخدام كائنات لذلك، كما في المثال التالي: let ages = { Ziad: 39, Hasan: 22, Sumaia: 62 }; console.log(`Sumaia is ${ages["Sumaia"]}`); // → Sumaia is 62 console.log("Is Jack's age known?", "Jack" in ages); // → Is Jack's age known? false console.log("Is toString's age known?", "toString" in ages); // → Is toString's age known? true أسماء خصائص الكائن هنا هي أسماء الناس المذكورة في المثال، وقيم الخصائص هي أعمارهم، لكننا بالتأكيد لم نذكر أيّ شخص اسمه toString في تلك الرابطة، لكن لأن الكائنات العادية مشتقة من Object.prototype فيبدو الأمر وكأن الخاصية موجودة هناك، لهذا فمن الخطر معاملة الكائنات العادية مثل معاملة خرائط -النوع Map- هنا. لدينا عدة طرق مختلفة لتجنب هذه المشكلة، فمن الممكن مثلًا إنشاء كائنات بدون نموذج أولي، حتى إذا مرّرت null إلى Object.create، فلن يكون الكائن الناتج مشتقًا من Object.prototype، وعليه يمكن استخدامه بأمان على أساس خارطة. console.log("toString" in Object.create(null)); // → false يجب أن تكون أسماء خصائص الكائنات سلاسل نصية، فإن أردت ربطًا لا يمكن تحويل مفاتيحه بسهولة إلى سلاسل نصية -مثل الكائنات- فلا تستخدم كائنًا على أساس خارطة، ولحسن الحظ فتملك جافاسكربت صنفًا اسمه Map مكتوب لهذا الغرض خاصة، حيث يخزِّن حالة الربط ويسمح بأي نوع من المفاتيح. let ages = new Map(); ages.set("Ziad", 39); ages.set("Hasan", 22); ages.set("Sumaia", 62); console.log(`Sumaia is ${ages.get("Sumaia")}`); // → Sumaia is 62 console.log("Is Jack's age known?", ages.has("Jack")); // → Is Jack's age known? false console.log(ages.has("toString")); // → false تُعَدّ التوابع set، وget، وhas جزءًا من واجهة كائن Map، فليس من السهل كتابة هيكل بيانات لتحديث مجموعة كبيرة من القيم والبحث فيها، ولكن لا تقلق، فقد كفانا شخص آخر مؤنة ذلك، حيث نستطيع استخدام ما كتبه من خلال تلك الواجهة البسيطة. إذا أردت معاملة كائن عادي لديك على أساس خارطة (النوع Map) لسبب ما، فمن المهم معرفة أن Object.keys يعيد المفاتيح الخاصة بالكائن فقط، وليس تلك الموجودة في النموذج الأولي، كما تستطيع استخدام التابع hasOwnProperty على أساس بديل لعامِل in، حيث يتجاهل النموذج الأولي للكائن، كما في المثال التالي: console.log({x: 1}.hasOwnProperty("x")); // → true console.log({x: 1}.hasOwnProperty("toString")); // → false تعددية الأشكال Polymorphism إذا استدعيتَ دالة String -التي تحوِّل القيمة إلى سلسلة نصية- على كائن ما، فستستدعي التابع toString على ذلك الكائن لمحاولة إنشاء سلسلة نصية مفيدة منه. كما ذكرنا سابقًا، تعرِّف بعض النماذج الأولية القياسية (سلاسل prototype) إصدارًا من toString خاصًا بها، وذلك لتستطيع إنشاء سلسلة نصية تحتوي بيانات مفيدة أكثر من "[object Object]"، كما تستطيع فعل ذلك بنفسك إن شئت. Rabbit.prototype.toString = function() { return `a ${this.type} rabbit`; }; console.log(String(blackRabbit)); // → a black rabbit وهذه صورة بسيطة من مفهوم بالغ القوة والأثر، فإن كُتب جزء من شيفرة ما ليعمل مع كائنات بها واجهة معينة -تابع toString في هذه الحالة-، فيمكن إلحاق أي نوع من الكائنات الداعمة لتلك الواجهة بالشيفرة، حيث ستعمل دون مشاكل؛ وتسمى تلك التقنية بتعددية الأشكال، وتعمل الشيفرة المتعددة الأشكال مع قيم ذات أشكال مختلفة طالما أنها تدعم الواجهة التي تتوقعها. كما ذكرنا في المقال الرابع، تستطيع حلقة for/of التكرار على عدة أنواع من هياكل البيانات، وتلك حالة أخرى من تعددية الأشكال، حيث تتوقع مثل تلك الحلقات التكرارية من هيكل البيانات أن يكشف واجهة معينة، وهو ما تفعله المصفوفات والسلاسل النصية؛ كما نستطيع إضافة تلك الواجهة إلى كائناتنا الخاصة، لكننا نحتاج إلى معرفة ما هي الرموز symbols قبل فعل ذلك. الرموز Symbols تستطيع عدة واجهات استخدام اسم الخاصية نفسها لأشياء عدة، فمثلًا، نستطيع تعريف واجهة بحيث يحوِّل فيها التابع toString الكائن إلى قطعة من خيوط الغزل، لكن من غير الممكن لكائن أن يتوافق مع تلك الواجهة ومع الاستخدام القياسي لـ toString. هذه المشكلة سيئة لكنها لا تشغل بال من يكتب بجافاسكربت لأنها غير شائعة، ورغم هذا فقد وفر مصممو جافاسكربت لنا حلًا لهذه المشكلة، إذ أن تلك من وظيفتهم على أي حال. حين زعمنا أن أسماء الخصائص هي سلاسل نصية لم نكن محقين 100%، فرغم أنها حقًا سلاسل نصية إلا قد تكون رموزًا أيضًا، وهي -أي الرموز- قيم أنشِئت بواسطة دالة Symbol، كما تُعَدّ الرموز المنشَئة حديثًا فريدةً، على عكس السلاسل النصية، بحيث لا تستطيع إنشاء الرمز نفسه مرتين. let sym = Symbol("name"); console.log(sym == Symbol("name")); // → false Rabbit.prototype[sym] = 55; console.log(blackRabbit[sym]); // → 55 تُضمَّن السلسلة النصية الممررة إلى Symbol تلقائيًا حين تحوّلها إلى سلسلة نصية، كما تسهِّل التعرف على الرمز عند عرضه في الطرفية console مثلًا؛ ولأن الرموز فريدة ويمكن استخدامها على أساس أسماء للخصائص، فهي مناسبة لتعريف الواجهات التي يمكن وجودها مع الخصائص الأخرى مهما كانت أسماؤها. const toStringSymbol = Symbol("toString"); Array.prototype[toStringSymbol] = function() { return `${this.length} cm of blue yarn`; }; console.log([1, 2].toString()); // → 1,2 console.log([1, 2][toStringSymbol]()); // → 2 cm of blue yarn ومن الممكن إضافة خصائص رمز ما في الأصناف وتعبيرات الكائنات باستخدام أقواس مربعة حول اسم الخاصية، وبسبب ذلك سيُقيَّم اسم الخاصية مثل صيغة الوصول إلى الخاصية التي تستخدِم قوسين مربعين، حيث سيسمح لنا هذا بالإشارة إلى الرابطة التي تحمل الرمز. let stringObject = { [toStringSymbol]() { return "a jute rope"; } }; console.log(stringObject[toStringSymbol]()); // → a jute rope واجهة المكرر يُتوقع من الكائن المعطى لحلقة for/of قابليته للتكرار، ويعني ذلك أنّ به تابعًا مسمى مع الرمز Symbol.iterator، وهو قيمة رمز معرَّفة من قِبَل اللغة، ومخزَّنة على أساس خاصية لدالة Symbol، كما يجب على ذلك التابع إعادة كائن يوفر واجهةً ثانية تكون هي المكرِّر iterator الذي يقوم بعملية التكرار، ولديه تابع next الذي يعيد النتيجة التالية التي يجب أن تكون بدورها كائنًا مع خاصية value التي توفر القيمة التالية إن كانت موجودة، وخاصية done التي تعيد true إن لم تكن ثمة نتائج أخرى، وتعيد false إن كان ثَمَّ نتائج بعد. لاحظ أن أسماء الخصائص: next، وvalue، وdone، هي سلاسل نصية عادية وليست رموزًا؛ أما الرمز الوحيد هنا فهو Symbol.iterator، والذي سيضاف غالبًا إلى كائنات كثيرة، كما نستطيع استخدام تلك الواجهة بأنفسنا كما يلي: let okIterator = "OK"[Symbol.iterator](); console.log(okIterator.next()); // → {value: "O", done: false} console.log(okIterator.next()); // → {value: "K", done: false} console.log(okIterator.next()); // → {value: undefined, done: true} دعنا نطبق هنا هيكل بيانات قابلًا للتكرار، حيث سنبني صنفَ matrix يتصرف على أساس مصفوفة ثنائية الأبعاد. class Matrix { constructor(width, height, element = (x, y) => undefined) { this.width = width; this.height = height; this.content = []; for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { this.content[y * width + x] = element(x, y); } } } get(x, y) { return this.content[y * this.width + x]; } set(x, y, value) { this.content[y * this.width + x] = value; } } يخزِّن الصنف محتوياته في مصفوفة واحدة من عنصرين فقط، هما: العرض، والطول width*height، وتُخزَّن العناصر صفًا صفًا، فيُخزن العنصر الثالث في الصف الخامس مثلًا -باستخدام الفهرسة الصفرية التي تبدأ من الصفر- في الموضع ‎4 * width + 2‎. تأخذ دالة الباني العرض، والطول، ودالة element اختيارية ستُستخدم لكتابة القيم الابتدائية؛ أما لجلب العناصر وتحديثها في المصفوفة الثنائية، فلدينا التابعان get، وset. حين نكرر على مصفوفة ما، فنحن بحاجة إلى معرفة موضع العناصر إضافة إلى العناصر نفسها، لذا سنجعل المكرِّر ينتج كائنات لها خصائص x، وy، وvalue. class MatrixIterator { constructor(matrix) { this.x = 0; this.y = 0; this.matrix = matrix; } next() { if (this.y == this.matrix.height) return {done: true}; let value = {x: this.x, y: this.y, value: this.matrix.get(this.x, this.y)}; this.x++; if (this.x == this.matrix.width) { this.x = 0; this.y++; } return {value, done: false}; } } يتتبع الصنف سير التكرار على المصفوفة الثنائية في الخصائص x، وy، ويبدأ التابع next بالتحقق من الوصول لأسفل المصفوفة الثنائية، فإن لم يصل إليه، فسينشِئ الكائن الذي يحمل القيمة الحالية أولًا، ثم يحدِّث موضعه، وبعد ذلك ينقله إلى السطر التالي إن تطلب الأمر. دعنا نهيئ صنف Matrix ليكون قابلًا للتكرار، وانتبه إلى استخدامنا المعالجة اللاحقة للنموذج الأولي بين الحين والآخر في هذه السلسلة لإضافة توابع إلى الأصناف، وذلك لتبقى الأجزاء المفردة من الشيفرة صغيرةً ومستقِلة؛ أما في البرامج العادية التي لا تحتاج فيها إلى تقسيم الشيفرة إلى أجزاء صغيرة، فستصرِّح عن هذه التوابع مباشرةً في الصنف. Matrix.prototype[Symbol.iterator] = function() { return new MatrixIterator(this); }; نستطيع الآن تطبيق التكرار على مصفوفة ما باستخدام for/of. let matrix = new Matrix(2, 2, (x, y) => `value ${x},${y}`); for (let {x, y, value} of matrix) { console.log(x, y, value); } // → 0 0 value 0,0 // → 1 0 value 1,0 // → 0 1 value 0,1 // → 1 1 value 1,1 التوابع الجالبة والضابطة والساكنة تتكون الواجهات من التوابع غالبًا، وقد تتضمن خصائص بها قيم غير دالية، فمثلًا، تملك كائنات Map خاصية size، والتي تخبرك كم عدد المفاتيح المخزَّنة فيها. ليس من الضروري لمثل هذا الكائن أن يحسب ويخزن خاصية مشابهة لتلك مباشرةً في النسخة instance التي لديه، بل حتى الخصائص التي يمكن الوصول إليها مباشرةً قد تخفي استدعاءً إلى تابع، حيث تسمى مثل تلك التوابع بالتوابع الجالبة getters، وتُعرَّف بكتابة get أمام اسم التابع في تعبير الكائن أو تصريح الصنف. let varyingSize = { get size() { return Math.floor(Math.random() * 100); } }; console.log(varyingSize.size); // → 73 console.log(varyingSize.size); // → 49 يُستدعى التابع المرتبط بخاصية size للكائن كلما قرأ أحد من منها، وتستطيع تنفيذ شيء مشابه حين يكتب أحدهم في خاصية ما باستخدام تابع ضابط setter. class Temperature { constructor(celsius) { this.celsius = celsius; } get fahrenheit() { return this.celsius * 1.8 + 32; } set fahrenheit(value) { this.celsius = (value - 32) / 1.8; } static fromFahrenheit(value) { return new Temperature((value - 32) / 1.8); } } let temp = new Temperature(22); console.log(temp.fahrenheit); // → 71.6 temp.fahrenheit = 86; console.log(temp.celsius); // → 30 يسمح لك صنف Temperature في المثال أعلاه بقراءة درجة الحرارة وكتابتها سواءً بمقياس السليزيوس أو الفهرنهايت، لكنها تخزِّن داخلها درجات السليزيوس فقط، وتحوِّل من وإلى سليزيوس في التابع الجالب والضابط لـ fahrenheit تلقائيًا. قد تحتاج أحيانًا إلى إلحاق بعض الخصائص لدالة الباني الخاصة بك مباشرةً بدلًا من النموذج الأولي، ولا تملك مثل تلك التوابع وصولًا إلى نسخة صنف، لكن يمكن استخدامها لتوفير طرق بديلة وإضافية لإنشاء النسخ. تُخزَّن التوابع المكتوبة قبل اسمها static على الباني، وذلك داخل التصريح عن الصنف، وعليه فيسمح لك صنف Temperature بكتابة Temperature.fromFahrenheit(100)‎ لإنشاء درجة حرارة باستخدام مقياس فهرنهايت. الوراثة Inheritance تتميز بعض المصفوفات بأنها تماثلية symmetric، بحيث إذا عكست إحداها حول قطرها الذي يبدأ من أعلى اليسار، فستبقى كما هي ولا تتغير، أي ستبقى القيمة المخزنة في الموضع (x،y) كما هي في الموضع (y،x). تخيل أننا نحتاج إلى هيكل بيانات مثل Matrix، لكن يجب ضمان تماثلية المصفوفة وبقائها كذلك، وهنا نستطيع كتابة هذا من الصفر، لكننا سنكرر شيفرةً مشابهةً كثيرًا لما كتبناه سابقًا. يسمح نظام النموذج الأولي في جافاسكربت بإنشاء صنف جديد محاكي لصنف قديم لكن مع تعريفات جديدة لبعض خصائصه، ويكون النموذج الأولي للصنف الجديد مشتقًا من القديم لكن مع إضافة تعريف جديد إلى التابع set مثلًا، ويسمى ذلك بالاكتساب أو الوراثة inheritance، إذ يرث الصنف الجديد خصائصه وسلوكه من الصنف القديم. class SymmetricMatrix extends Matrix { constructor(size, element = (x, y) => undefined) { super(size, size, (x, y) => { if (x < y) return element(y, x); else return element(x, y); }); } set(x, y, value) { super.set(x, y, value); if (x != y) { super.set(y, x, value); } } } let matrix = new SymmetricMatrix(5, (x, y) => `${x},${y}`); console.log(matrix.get(2, 3)); // → 3,2 يشير استخدام كلمة extends إلى وجوب عدم اعتماد هذا الصنف على النموذج الأولي الافتراضي Object مباشرةً، وإنما على صنف آخر يسمى بالصنف الأب superclass؛ أما الصنف المشتق فيكون اسمه الصنف الفرعي، أو الابن subclass. يستدعي الباني لتهيئة نسخة من SymmetricMatrix باني صنف الأب من خلال كلمة super المفتاحية، وهذا ضروري لأنّ الكائن الجديد سيحتاج إلى خصائص النسخة التي تملكها المصفوفات، إذا تصرَّف مثل Matrix. كما يغلِّف الباني دالة element لتبديل إحداثيات القيم أسفل خط القطر، وذلك لضمان تماثل المصفوفة. يُستخدَم super مرةً أخرى من التابع set، وذلك لاستدعاء تابع معين من مجموعة توابع الصنف الأب؛ كما سنعيد تعريف set لكن لن نستخدم السلوك الأصلي، حيث لن ينجح استدعاؤه بسبب إشارة this.set إلى set الجديد، كذلك يوفر super الواقع داخل توابع الصنف، طريقةً لاستدعاء التوابع كما عُرِّفت في الصنف الأب. وتسمح لنا الوراثة ببناء أنواع بيانات مختلفة من أنواع موجودة مسبقًا بقليل من الجهد، وهذه -أي الوراثة- جزء أساسي في ثقافة البرمجة كائنية التوجه جنبًا إلى جنب مع التغليف وتعددية الأشكال، لكن لأن هذين الآخرَين يُعتد بهما كثيرًا في البرمجة على أساس أساليب مهمة ومفيدة، فإنّ الوراثة قد صارت محل نظر، ففي حين يُستخدَم كل من التغليف وتعددية الأشكال في فصل أجزاء الشيفرات عن بعضها مما يقلل من تعقيد البرنامج عمومًا، فالوراثة على العكس من ذلك، إذ تربط الأصناف معًا منشِئًة مزيدًا من التعقيد، لأن عليك في الغالب معرفة كيفية عمل ذلك الصنف حين تحتاج إلى الوراثة منه، بخلاف إن لم تفعل شيئًا سوى استخدامه. وإننا نستخدمه بين الحين والآخر في برامجنا، لكن لا يحملنك ذلك على التفكير فيه أول شيء، فليس من الحكمة جعل بناء هرميات من الأصناف (شجرة عائلة من الأصناف) خيارك الأول في حل المشاكل. عامل instanceof توفر جافاسكربت عاملًا ثنائيًا يسمى instanceof، حيث نستخدمه إذا أردنا معرفة إن كان الكائن مشتقًا من صنف بعينه. console.log( new SymmetricMatrix(2) instanceof SymmetricMatrix); // → true console.log(new SymmetricMatrix(2) instanceof Matrix); // → true console.log(new Matrix(2, 2) instanceof SymmetricMatrix); // → false console.log([1] instanceof Array); // → true سينظر العامل في الأنواع المكتسبة، وسيجد أن symmetricMatrix نسخةٌ من Matrix، كما يمكن استخدام العامل مع البواني القياسية مثل Array، فكل كائن تقريبًا ما هو إلا نسخة من Object. خاتمة لقد رأينا أنّ نطاق تأثيرالكائنات يتعدى حمل خصائصها، إذ لها نماذج أولية -والتي بدورها كائنات أيضًا-، وتتصرف كما لو كان لديها خصائص ليست لديها على الحقيقة طالما أن النموذج الأولي به تلك الخصائص، كما تمكّنا من معرفة الكائنات البسيطة لها Object.prototype على أساس نموذج أولي لها. يمكن استخدام البواني -وهي دوال تبدأ أسماؤها بحرف إنجليزي كبير- مع عامل new لإنشاء كائنات جديدة، وسيكون النموذج الأولي للكائن هو الكائن الموجود في خاصية prototype للباني، ونستطيع الاستفادة من ذلك بوضع جميع الخصائص التي تتشاركها القيم المعطاة -من النوع نفسه- في نماذجها الأولية. كذلك عرفنا صيغة class التي توفر طريقةً واضحةً لتعريف الباني ونموذجه الأولي. تستطيع تعريف الجالبات والضابطات لاستدعاء التوابع سرًا في كل مرة يصل فيها إلى خاصية كائن ما، وقد عرفنا أن التوابع الساكنة ما هي إلا توابع مخزَّنة في باني الصنف بدلًا من نموذجه الأولي، ثم شرحنا كيف أن عامل instanceof يستطيع إخبارك إن أعطيته كائنًا وبانيًا، وما إذا كان الكائن نسخةً من الباني أم لا. واعلم أنك تستطيع باستخدام الكائنات تحديد واجهة لها، وتخبر جميع أجزاء الشيفرة بأن عليهم التحدث إلى كائنك من خلال تلك الواجهة فقط، كما تُغلَّف بقية التفاصيل التي يتكون منها الكائن وتختفي خلف الواجهة. ويمكن لأكثر من نوع استخدام تلك الواجهة، فتعرف الشيفرة التي كُتبت لتستخدِم واجهةً ما كيف تعمل مع أي عدد من الكائنات المختلفة التي توفر الواجهة تلقائيًا ، وهذا ما يسمى بتعددية الأشكال. حين تستخدم عدة أصناف لا تختلف فيما بينها إلا في بعض التفاصيل، فيمكن كتابة أصناف جديدة منها على أساس أصناف فرعية، ترث جزءًا من سلوكها. تدريبات النوع المتجهي اكتب الصنف Vec الذي يمثل متجهًا في فضاء ثنائي الأبعاد، حيث يأخذ المعامِلين x، وy -وهما أرقام-، ويحفظهما في خصائص بالاسم نفسه. أعطِ النموذج الأولي للصنف Vec تابعَين، هما: plus، وminus، اللذان يأخذان متجهًا آخر على أساس معامِل، ويُعيدان متجهًا جديدًا له مجموع قيم x، وy للمتجهين (this، والمعامِل)؛ أو الفرق بينهما. أضف الخاصية الجالبة length إلى النموذج الأولي الذي يحسب طول المتجه، وهو المسافة بين النقطة (x,y) والإحداثيات الصفرية (0,0). تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen. // ضع شيفرتك هنا. console.log(new Vec(1, 2).plus(new Vec(2, 3))); // → Vec{x: 3, y: 5} console.log(new Vec(1, 2).minus(new Vec(2, 3))); // → Vec{x: -1, y: -1} console.log(new Vec(3, 4).length); // → 5 إرشادات للحل إذا لم تكن تعرف كيف تبدو تصريحات class، فانظر إلى مثال صنف Rabbit. يمكن إضافة خاصية جالبة إلى الباني من خلال وضع كلمة get قبل اسم التابع، ولحساب المسافة من (0,0) إلى (x,y)، فيمكن استخدام نظرية فيثاغورث التي تقول: أن مربع المسافة التي نريدها يساوي مجموع مربعي x و y، وعلى ذلك يكون ‎√(x2 + y2)‎ هو العدد الذي نريده، ويُحسَب الجذر التربيعي في جافاسكربت باستخدام Math.sqrt. المجموعات توفر بيئة جافاسكربت القياسية هيكل بيانات اسمه Set، إذ يحمل مجموعةً من القيم مثل نسخة من Map، لكن على عكس Map فهو لا يربط قيمًا أخرى بها، بل يتتبع القيم ليعرف أيها تكون جزءًا من المجموعة. ولا يمكن للقيمة الواحدة أن تكون جزءًا من مجموعة ما أكثر من مرة واحدة، ولا يحدث أي تأثير حين تضاف مرةً أخرى. اكتب صنفًا اسمه Group -بما أنّ Set مأخوذ من قبل-، واجعل له التوابع الآتية: add، وdelete، وhas، ليكون مثل Set، بحيثما ينشئ بانيه مجموعةً فارغةً، ويضيف add قيمةً إلى المجموعة فقط إن لم تكن عضوًا بالفعل في المجموعة، كما يحذف delete وسيطه من المجموعة إن كان عضوًا فيها، ويعيد has قيمةً بوليانيةً توضح هل وسيطه عضو في المجموعة أم لا. استخدم عامِل ===، أو شيئًا يحاكيه مثل indexof، لمعرفة ما إذا كانت قيمتان متطابقين، وأعط الصنف التابع الساكن from الذي يأخذ كائنًا قابلًا للتكرار على أساس وسيط، كما ينشئ مجموعةً تحتوي على جميع القيم المنتَجة من خلال التكرار عليها. تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen. class Group { // ضع شيفرتك هنا. } let group = Group.from([10, 20]); console.log(group.has(10)); // → true console.log(group.has(30)); // → false group.add(10); group.delete(10); console.log(group.has(10)); // → false إرشادات للحل تكون الطريقة الأسهل لحل هذا التدريب بتخزين مصفوفة من أعضاء المجموعة في خاصية لإحدى النُسَخ، ويمكن استخدام التابع includes، أو indexOf للتحقق من وجود قيمة ما في المصفوفة. ويمكن لباني الصنف الخاص بك إسناد تجميعة الأعضاء إلى مصفوفة فارغة، وعند استدعاء add فيجب التحقق هل القيمة المعطاة موجودة في المصفوفة أم يضيفها باستخدام push مثلًا. قد يكون حذف عنصر من مصفوفة في delete مبهمًا قليلًا، لكن تستطيع استخدام filter لإنشاء مصفوفة جديدة بدون القيمة، ولا تنس كتابة النسخة الجديدة من المصفوفة لتحل محل الخاصية التي تحمل الأعضاء. يمكن للتابع from استخدام حلقة for/of التكرارية للحصول على القيم من الكائن القابل للتكرار، ويستدعي add لوضعها في مجموعة منشأة حديثًا. المجموعات القابلة للتكرار أنشئ الصنف Group من التدريب السابق، واستعن بالقسم الخاص بواجهة المكرر من هذا المقال إن احتجت إلى رؤية الصيغة الدقيقة للواجهة. إذا استخدمت مصفوفةً لتمثيل أعضاء المجموعة، فلا تُعِد المكرِّر المنشَأ باستدعاء التابع Symbol.iterator على المصفوفة، فهذا وإن كان سيعمل بدون مشاكل، إلا أنه سينافي الهدف من التدريب. لا بأس إن تصرَّف المكرر الخاص بك تصرفًا غير مألوف عند تعديل المجموعة أثناء التكرار. تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen. // ضع شيفرتك هنا، والشيفرة التي من .المثال السابق for (let value of Group.from(["a", "b", "c"])) { console.log(value); } // → a // → b // → c إرشادات للحل ربما من الأفضل تعريف صنف GroupIterator جديد، كما يجب أن يكون لنُسَخ المكرر خاصية تتبع الموضع الحالي في المجموعة، بحيث تتحقق في كل مرة يُستدعى فيها next مما إذا كانت قد انتهت أم لا، فإن لم تنته فستتحرك متجاوزةً القيمة الحالية وتعيدها. يحصل الصنف Group على تابع يسمى من قِبل Symbol.iterator`، ويعيد عند استدعائه نسخةً جديدةً من صنف المكرر لتلك المجموعة. استعارة تابع ذكرنا أعلاه هنا أن hasOwnProperty لكائن يمكن استخدامه على أساس بديل قوي لعامِل in إذا أردت تجاهل خصائص النموذج الأولي، لكن ماذا لو كانت خارطتك map تحتاج إلى كلمة hasOwnProperty؟ لن تستطيع حينها استدعاء هذا التابع بما أن خاصية الكائن تخفي قيمة التابع. هل تستطيع التفكير في طريقة لاستدعاء hasOwnProperty على كائن له خاصية بهذا الاسم؟ تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen. let map = {one: true, two: true, hasOwnProperty: true}; // أصلح هذا الاستدعاء console.log(map.hasOwnProperty("one")); // → true إرشادات للحل تذكّر أن التوابع الموجودة في الكائنات المجردة تأتي من Object.prototype، كما تستطيع استدعاء دالة مع رابطة this خاصة من خلال استخدام التابع call. ترجمة -بتصرف- للفصل السادس من كتاب Elequent Javascript لصاحبه Marijn Haverbeke. اقرأ أيضًا المقال السابق: الدوال في جافاسكريبت. هياكل البيانات: الكائنات والمصفوفات في جافاسكريبت.
  17. تتكلف البرامج بموارد أكثر كلما زاد حجمها، وذلك ليس بسبب الوقت الذي تستغرقه من أجل بنائها، بل لأنّ الحجم الكبير يتبعه تعقيد أكثر، ويحيّر ذلك التعقيد المبرمجين العاملين عليه، حيث تجعلهم تلك الحيرة يرتكبون أخطاءً في صورة زلات برمجية Bugs، وعليه يكون البرنامج الكبير فرصةً كبيرةً لهذه الزلات بالاختفاء وسط الشيفرات، مما يصعِّب الوصول إليها. لنَعُدْ إلى المثالين الأخيرَين المذكورَين في مقدمة هذه السلسلة، حيث يحتوي المثال الأول منهما على ستة أسطر، وهو مستقل بذاته، انظر كما يلي: let total = 0, count = 1; while (count <= 10) { total += count; count += 1; } console.log(total); أما الثاني فيعتمد على دالتين خارجيتين، ويتكوّن من سطر واحد فقط، كما يلي: console.log(sum(range(1, 10))); برأيك، أيهما أكثر عرضةً لتكون فيه زلة برمجية؟ إذا حسبنا حجم تعريفي sum وrange، فسيكون البرنامج الثاني أكبر من الأول، لكن لا زلنا نراه أكثر صِحة، حيث عبَّر عن الحل بألفاظ تتوافق مع المشكلة المحلولة، فلا يتعلق استدعاء مجال من الأعداد بالحلقات التكرارية والعدادات بقدر ما يتعلق بالمجالات والمجموع الإجمالي؛ وتحتوي تعاريف هذه الألفاظ (دالتي sum، وrange) على حلقات تكرارية، وعدادات، وتفاصيل أخرى، وبما أنها تعبر عن مفاهيم أبسط من البرنامج ككل، فهي أدنى ألا تحتوي على أخطاء. التجريد تسمى هذه الأنواع من الألفاظ في السياقات البرمجية بالتجريدات Abstractions، وهي تخفي التفاصيل الدقيقة وتعطينا القدرة على الحديث عن المشاكل على مستوى أعلى أو أكثر تجريدًا. انظر هاتين الوصفتين أدناه لتقريب الأمر: الوصفة الأولى: الوصفة الثانية: لا شك أن الوصفة الثانية أقصر وأيسر في التفسير والفهم، لكن ستحتاج إلى فهم المصطلحات الخاصة بالطهي، مثل: النقع، والطهي، والتقطيع، والتجفيف (هذه المصطلحات للتقريب مثالًا، وشاهدها أن تكون على دراية بمصطلحات مجال المشكلة التي تريد حلها، وإلا فهي معروفة لكل أحد). يقع الكثير من المبرمجين في خطأ الوصفة الأولى عند سردهم للخطوات الدقيقة والصغيرة التي على الحاسوب تنفيذها خطوةً بخطوة، وذلك بسبب عدم ملاحظتهم للمفاهيم العليا في المشكلة التي بين أيديهم، ويُعَدّ الانتباه عند حلك لمشكلة بهذا الأسلوب مهارةً مفيدةً جدًا. تجريد التكرار تُعَدّ الدوال البسيطة طريقة ممتازة لبناء تجريدات، غير أنها تعجز عن ذلك أحيانًا، فمن الشائع أن يفعل البرنامج شيئًا ما بعدد معيّن من المرات باستخدام حلقة for، فمثلًا: for (let i = 0; i < 10; i++) { console.log(i); } فهل نستطيع تجريد مفهوم "فعل شيء ما عددًا من المرات قدره N" في صورة دالة؟ بدايةً، من السهل كتابة دالة تستدعي console.log عددًا من المرات قدره N: function repeatLog(n) { for (let i = 0; i < n; i++) { console.log(i); } } لكن ماذا لو أردنا فعل شيء آخر غير تسجيل الأعداد؟ بما أنّه يمكن تمثيل "فعل شيء ما" على أساس دالة، والدوال ما هي إلا قيم، فسنستطيع تمرير إجراءنا على أساس قيمة دالة، وذلك كما يلي: function repeat(n, action) { for (let i = 0; i < n; i++) { action(i); } } repeat(3, console.log); // → 0 // → 1 // → 2 لسنا في حاجة إلى تمرير دالة معرَّفة مسبقًا إلى repeat، فغالبًا من السهل إنشاء قيمة دالة عند الحاجة. let labels = []; repeat(5, i => { labels.push(`Unit ${i + 1}`); }); console.log(labels); // → ["Unit 1", "Unit 2", "Unit 3", "Unit 4", "Unit 5"] وكما ترى، فهذا مهيكل في صورة محاكية لحلقة for، إذ يصف نوع الحلقة التكرارية أولًا، ثم يوفر متنًا لها، غير أنّ المتن الآن مكتوب على أساس قيمة دالة مغلّفة بأقواس الاستدعاء إلى repeat، وهذا هو السبب الذي يجعل من الواجب إغلاقها بقوس إغلاق معقوص ‎}‎ وقوس إغلاق عادي ‎)‎ بأقواس إغلاق، وفي حالة هذا المثال عندما يكون المتن تعبيرًا واحدًا وصغيرًا، فيمكنك إهمال الأقواس المعقوصة وكتابة الحلقة في سطر واحد. الدوال العليا تسمى الدوال التي تعمل على دوال أخرى سواءً بأخذها على أساس وسائط أو بإعادتها لها، باسم الدوال العليا higher-order functions، وبما أنّ الدوال ما هي إلا قيم منتظمة، فلا شيء جديد في وجود هذا النوع منها، والمصطلح قادم من الرياضيات حين يؤخذ الفرق بين الدوال والقيم الأخرى على محمل الجد. وتسمح لنا الدوال العليا بعملية التجريد على القيم والإجراءات أيضًا، وتأتي في عدة أشكال وصور، فقد تنشِئ الدوال دوالًا أخرى جديدةً، كما يلي: function greaterThan(n) { return m => m > n; } let greaterThan10 = greaterThan(10); console.log(greaterThan10(11)); // → true وقد تغيّر الدوال دوالًا أخرى، كما في المثال التالي: function noisy(f) { return (...args) => { console.log("calling with", args); let result = f(...args); console.log("called with", args, ", returned", result); return result; }; } noisy(Math.min)(3, 2, 1); // → calling with [3, 2, 1] // → called with [3, 2, 1] , returned 1 كما توفر الدوال أنواعًا جديدةً من تدفق التحكم control flow: function unless(test, then) { if (!test) then(); } repeat(3, n => { unless(n % 2 == 1, () => { console.log(n, "is even"); }); }); // → 0 is even // → 2 is even يوفر تابع مصفوفة مدمج forEach شيئًا مثل حلقة for/of التكرارية على أساس دالة عليا، وذلك كما يلي: ["A", "B"].forEach(l => console.log(l)); // → A // → B مجموعات البيانات النصية تُعَدّ معالجة البيانات إحدى الجوانب التي تبرز فيها أهمية الدوال العليا، ولضرْب مثال على ذلك سنحتاج إلى بعض البيانات الفعلية، كما سنستخدم في هذا المقال مجموعة بيانات عن نصوص وأنظمة كتابة، مثل: اللاتينية، والعربية، والسيريلية (حروف اللغات الأوراسية، مثل: الروسية، والبلغارية). ترتبط أغلب محارف اللغات المكتوبة بنص معين، ولعلك تذكر حديثنا عن الترميز الموحد Unicode الذي يسند عددًا لكل محرف من محارف هذه اللغات، حيث يحتوي هذا المعيار على 140 نصًا مختلفًا، من بينها 81 لا تزال مستخدمةً، في حين صارت 59 منها مهملةً أو تاريخيةً، أي لم تَعُدْ مستخدمة؛ فعلى سبيل المثال، انظر هذه الكتابة من اللغة التاميلية: تحتوي مجموعة البيانات على بعض أجزاء البيانات من النصوص المئة والأربعين المعرَّفة في اليونيكود، وهي متاحة في صندوق اختبار هذا المقال على صورة رابطة SCRIPTS. { name: "Coptic", ranges: [[994, 1008], [11392, 11508], [11513, 11520]], direction: "ltr", year: -200, living: false, link: "https://en.wikipedia.org/wiki/Coptic_alphabet" } يخبرنا الكائن السابق بكل من: اسم النص، ومجالات اليونيكود المسنَدة إليه، واتجاه الكتابة، والزمن التقريبي لنشأة هذه اللغة، وما إذا كانت مستخدمةً أم لا، ورابط إلى مزيد من التفاصيل والبيانات عنها؛ وقد يكون اتجاه الكتابة من اليسار إلى اليمين "ltr"، أو من اليمين إلى اليسار "rtl"، كما في حالة اللغتين العربية والعبرية، أو من الأعلى إلى الأسفل "ttb" كما في حالة اللغة المنغولية. تحتوي خاصية ranges على مصفوفة من مجالات المحارف، حيث يكون كل منها مصفوفةً من عنصرين، هما الحدين الأدنى والأعلى، ويُسنَد أيّ رمز للمحارف بين هذه المجالات إلى النص، كما يُضمَّن الحد الأدنى فيها؛ أما الحد الأعلى فلا، أي يُعَدّ رمز 994 محرفًا قبطيًا Coptic في المثال السابق؛ أما الرمز 1008 فلا. ترشيح المصفوفات نستخدم دالة filter لإيجاد النصوص واللغات التي مازالت مستخدَمةً في مجموعات البيانات، إذ تُرشِّح عناصر المصفوفة التي لا تجتاز اختبارًا تجريه عليها: function filter(array, test) { let passed = []; for (let element of array) { if (test(element)) { passed.push(element); } } return passed; } console.log(filter(SCRIPTS, script => script.living)); // → [{name: "Adlam", …}, …] تستخدم الدالة وسيطًا اسمه test، وهو قيمة دالةٍ لملء الفراغ "gap" أثناء عملية اختيار العناصر. إذ تلاحظ كيف تبني دالة filter مصفوفةً جديدةً من العناصر التي تجتاز اختبارها بدلًا من حذف العناصر من المصفوفة القديمة، وهذا يشير إلى أنّ هذه الدالة دالةً نقيةً pure function، إذ لا تُعدِّل المصفوفة المُمرَرة إليها. تشبه هذه الدالة التابع forEach في كونها تابع مصفوفة قياسي، وقد عرَّف المثال السابق الدالة لتوضيح كيفية عملها من الداخل ليس إلا؛ أما من الآن فصاعدًا فسنستخدمها فقط كما يلي: console.log(SCRIPTS.filter(s => s.direction == "ttb")); // → [{name: "Mongolian", …}, …] التحويل مع map ليكن لدينا مصفوفة كائنات تمثِّل عدة نصوص، حيث أُنتجت بترشيح مصفوفة SCRIPTS، لكننا نريد مصفوفةً من الأسماء لأنها أسهل في البحث والتدقيق. هنا يأتي دور التابع map الذي يحوّل مصفوفةً بتطبيق دالة على جميع عناصرها، ثم يبني مصفوفةً جديدةً من القيم المعادة، وتكون المصفوفة الجديدة بطول المصفوفة المدخلة، مع إعادة توجيه محتوياتها في شكل form جديد بواسطة الدالة. function map(array, transform) { let mapped = []; for (let element of array) { mapped.push(transform(element)); } return mapped; } let rtlScripts = SCRIPTS.filter(s => s.direction == "rtl"); console.log(map(rtlScripts, s => s.name)); // → ["Adlam", "Arabic", "Imperial Aramaic", …] وبالمثل، يُعَدّ التابع map تابع مصفوفة قياسي، أي يحاكي كلًا من: filter، وforEach. التلخيص باستخدام reduce يُعَدّ إجراء حسابات على قيمة واحدة من المصفوفات من العمليات الشائعة على هذه المصفوفات، ومثالنا التعاودي الذي يستدعي تجميعةً من الأعداد هو مثال على هذا، وكذلك إيجاد النص الحاوي على أكبر عدد من المحارف. تُدعى العملية العليا الممثِلة لهذا النمط بـ reduce، وتُدعى fold أحيانًا، إذ تُنتِج قيمةً بتكرار أخذ عنصر ما من المصفوفة، ومن ثم جمعه مع القيمة الحالية، فتبدأ عند إيجاد مجموع الأعداد من الصفر، وتضيف كل عنصر إلى المجموع الإجمالي. تأخذ reduce جزءًا من المصفوفة، ودالة جامعة combining function، وقيمة بدء start value، على أساس معامِلات، وتختلف هذه الدالة عن دالتي filter، وmap المتّسمتين بالوضوح والمباشرة أكثر، كما في الدالة التالية: function reduce(array, combine, start) { let current = start; for (let element of array) { current = combine(current, element); } return current; } console.log(reduce([1, 2, 3, 4], (a, b) => a + b, 0)); // → 10 يملك تابع المصفوفة القياسي reduce الموافق لهذه الدالة خاصيةً مميزة، إذ يسمح لك بإهمال وسيط start إن كان في مصفوفتك عنصرًا واحدًا على الأقل، حيث سيأخذ العنصر الأول من المصفوفة على أساس قيمة بدء له، ويبدأ التقليل من العنصر الثاني، كما في المثال التالي: console.log([1, 2, 3, 4].reduce((a, b) => a + b)); // → 10 يمكننا استخدام reduce مرتين لحساب عدد محارف أو كلمات نص ما، كما في المثال التالي: function characterCount(script) { return script.ranges.reduce((count, [from, to]) => { return count + (to - from); }, 0); } console.log(SCRIPTS.reduce((a, b) => { return characterCount(a) < characterCount(b) ? b : a; })); // → {name: "Han", …} تقلل دالة charactercount المجالات المسندَة إلى نص ما بجمع أحجامها، حيث تلاحظ استخدام التفكيك في قائمة المعامِلات للدالة المقلِّلة، ثم يُستَدعى التابع reduce مرةً ثانية لإيجاد أكبر نص من خلال موازنة نصين في كل مرة وإعادة الأكبر بينهما. تحتوي لغات الهان -نظام الكتابة الصيني، والياباني، والكوري- على أكثر من 89 ألف محرف مسنَد إليها في معيار يونيكود، مما يجعلها أكبر نظام كتابة في مجموعة البيانات. وقد قرر مجمع الترميز الموحد Unicode Consortium معاملة تلك اللغات على أنها نظام كتابة واحد لتوفير رموز المحارف، رغم مضايقة هذا لبعض العامة، وسُمي ذلك القرار بتوحيد الهان Han Unification. قابلية التركيب لن يبدو مثال إيجاد أكبر نص سيئًا إذا كتبناه دون استخدام الدوال العليا فيه، كما في المثال التالي: let biggest = null; for (let script of SCRIPTS) { if (biggest == null || characterCount(biggest) < characterCount(script)) { biggest = script; } } console.log(biggest); // → {name: "Han", …} لا يزال البرنامج سهل القراءة على الرغم من استخدام أربع رابطات جديدة، وزيادة أربعة أسطر أخرى، حيث تبرز الدوال العليا عند الحاجة لإجراء عمليات تركيب، فمثلًا، دعنا نكتب شيفرة للبحث عن السنة المتوسطة لإنشاء لغة ما سواءً كانت حيةً أو ميتةً في مجموعة البيانات: function average(array) { return array.reduce((a, b) => a + b) / array.length; } console.log(Math.round(average( SCRIPTS.filter(s => s.living).map(s => s.year)))); // → 1165 console.log(Math.round(average( SCRIPTS.filter(s => !s.living).map(s => s.year)))); // → 204 نتبين مما سبق أنّ متوسط اللغات الميتة في اليونيكود أقدم من الحية، وهذا متوقع لا شك، كما أنّ الشيفرة السابقة ليست صعبة القراءة، إذ يمكن النظر إليها على أنها أنبوب، حيث نبدأ فيها بجميع اللغات، ثم نرشّح الحية منها أو الميتة، وبعدها نأخذ أعوام هؤلاء ونحسب المتوسط، ثم نقرب النتيجة لأقرب رقم صحيح. كما نستطيع كتابة هذه العملية الحسابية على صورة حلقة تكرارية واحدة كبيرة، كما يلي: let total = 0, count = 0; for (let script of SCRIPTS) { if (script.living) { total += script.year; count += 1; } } console.log(Math.round(total / count)); // → 1165 لكن من الصعب قراءة هذا الأسلوب لمعرفة ما الذي يُحسب فيه، وبما أنّ النتائج البينية غير ممثلة على أساس قيم مترابطة، فستدور حول نفسك لاستخراج شيء مثل average إلى دالة منفصلة. يختلف هذان المنظوران من حيث ما يفعله الحاسوب، إذ يبني الأول مصفوفات جديدة عند تشغيل filter، وmap؛ بينما يحسب الثاني بعض الأعداد فقط وينجز عملًا أقل، ولا شك في تفضيلك للشيفرة المقروءة، لكن إن كنت تعالج مصفوفات ضخمة بتواتر، فيكون الأسلوب الأقل تجريدًا هنا أفضل بسبب السرعة الزائدة. السلاسل النصية ورموز المحارف تُعَدّ معرفة اللغة التي يستخدمها نص ما، إحدى استخدامات مجموعات البيانات، حيث يمَكّننا رمز المحرف المعطى من كتابة دالة لإيجاد اللغة الموافقة لهذا المحرف إن وجِدت، وذلك بسبب ارتباط كل لغة بمصفوفة من مجالات رموز المحارف، أي كما في المثال التالي: function characterScript(code) { for (let script of SCRIPTS) { if (script.ranges.some(([from, to]) => { return code >= from && code < to; })) { return script; } } return null; } console.log(characterScript(121)); // → {name: "Latin", …} يُعَدّ تابع some أعلاه دالةً عليا، إذ تأخذ دالة اختبار لتخبرك إن كانت الدالة تعيد true لأيّ عنصر في المصفوفة، ولكن كيف سنحصل على رموز المحارف في سلسلة نصية؟ ذكرنا في المقال الأول أنّ سلاسل جافاسكربت النصية مرمّزة على أساس تسلسلات من أعداد 16-بت، وتسمى هذه الأعداد بالأعداد البِتّية لمحارف السلسلة code units، حيث صُممِّت رموز المحارف character code في اليونيكود لتتوافق مع وحدة unit -مثل التي تعطيك 65 ألف محرف-؛ ولكن عارض بعض العامة زيادة الذاكرة المخصصة لكل محرف بعدما تبين عدم كفاية هذا، فقد ابتُكِرت لمعالجة هذه المشكلة صيغة UTF-16 التي استخدمتها جافاسكربت، حيث تصف أكثر المحارف شيوعًا باستخدام عدد بِتّي لمحرف 16-بت واحد، لكنها تستخدم زوجًا من تلك الأعداد البِتّية لغيرها. تُعَدّ UTF-16 فكرةً سيئةً حاليًا، إذ يبدو أنّها اختُرعت لخلق أخطاء! فمن السهل كتابة برامج تدّعي أنّ الأعداد البِتّية للمحارف والمحارف هما الشيء نفسه، وإن لم تكن لغتك تستخدم محارف من وحدتين فلا بأس؛ لكن سينهار البرنامج عند محاولة أحدهم استخدامه مع المحارف الصينية الأقل شيوعًا، ولحسن الحظ، فقد بدأ الجميع مع اختراع الإيموجي (الرموز التعبيرية) باستخدام المحارف ذات الوحدتين. لكن العمليات الواضحة في سلاسل جافاسكربت النصية، مثل: الحصول على طولها باستخدام خاصية length، والوصول إلى محتواها باستخدام الأقواس المربعة، لا تتعامل إلا مع الأعداد البِتية للمحارف، أنظر إلى ما يلي: // محرفي إيموجي، حصان وحذاء let horseShoe = "??"; console.log(horseShoe.length); // → 4 console.log(horseShoe[0]); // → (Invalid half-character) console.log(horseShoe.charCodeAt(0)); // → 55357 (رمز لنصف محرف) console.log(horseShoe.codePointAt(0)); // → 128052 (الرمز الحقيقي لرمز الحصان) يعطينا تابع charCodeAt عداد بِتي للمحرف فقط وليس الرمز الكامل للمحرف؛ أما تابع codePointAt الذي أضيف لاحقًا فيعطي محرف يونيكود كامل، لذا نستطيع استخدامه للحصول على المحارف من سلسلة نصية، لكن لا يزال الوسيط الممرر إلى codePointAt فهرسًا داخل تسلسل من الأعداد البِتّية لمحارف السلسلة، لذا فلا زلنا في حاجة إلى النظر هل يأخذ المحرف وحدتين رمزيتين أم وحدةً واحدةً للمرور على جميع المحارف في سلسلة نصية ما. ذكرنا في المقال السابق أنه يمكن استخدام حلقة for/of التكرارية على السلاسل النصية، وقد أُدخل هذا النوع من الحلقات -شأنه في هذا شأن codePointAt- في الوقت الذي كانت العامة فيه على علم بمشكلة UTF-16، لذا سيعطيك محارف حقيقية حين استخدامه للتكرار على سلسلة نصية، بدلًا من أعداد بِتية لمحارف السلسلة. let roseDragon = "??"; for (let char of roseDragon) { console.log(char); } // → ? // → ? وإن كان لديك محرف -وما هو إلا سلسلة نصية من وحدة رمزية أو اثنتين-، فستستطيع استخدام codePointAt(0)‎ للحصول على رمزه. التعرف على النصوص لدينا دالة characterScript، وطريقةً للتكرار الصحيح على المحارف، فالخطوة التالية إذًا هي عدّ المحارف المنتمية لكل لغة، كما في التجريد أدناه للعد: function countBy(items, groupName) { let counts = []; for (let item of items) { let name = groupName(item); let known = counts.findIndex(c => c.name == name); if (known == -1) { counts.push({name, count: 1}); } else { counts[known].count++; } } return counts; } console.log(countBy([1, 2, 3, 4, 5], n => n > 2)); // → [{name: false, count: 2}, {name: true, count: 3}] تتوقع دالة countBy تجميعةً collection -من أي شيء نستطيع التكرار عليه باستخدام for/of-، ودالةً لحساب اسم المجموعة group للعنصر المعطى، حيث تعيد مصفوفةً من الكائنات وكل منها هو اسم لمجموعة، وتخبرك بعدد العناصر الموجودة في تلك المجموعة. تستخدِم هذه الدالة تابع مصفوفة اسمه findIndex، حيث يحاكي indexof نوعًا ما، لكنه يبحث في القيمة الأولى التي تعيد true في الدالة المعطاة بدلًا من البحث عن قيمة معينة، كما يتشابه معه في إعادة ‎-1 عند عدم وجود مثل هذا العنصر، ونستطيع باستخدام countBy كتابة الدالة التي تخبرنا أيّ اللغات مستخدمة في نص ما. function textScripts(text) { let scripts = countBy(text, char => { let script = characterScript(char.codePointAt(0)); return script ? script.name : "none"; }).filter(({name}) => name != "none"); let total = scripts.reduce((n, {count}) => n + count, 0); if (total == 0) return "No scripts found"; return scripts.map(({name, count}) => { return `${Math.round(count * 100 / total)}% ${name}`; }).join(", "); } console.log(textScripts('英国的狗说"woof", 俄罗斯的狗说"тяв"')); // → 61% Han, 22% Latin, 17% Cyrillic تَعُدّ الدالة أولًا المحارف من خلال الاسم باستخدام characterScript لتعيينها اسمًا وتعود إلى السلسلة "none" من أجل المحارف التي ليست جزءًا من أي لغة، ثم يحذف استدعاء filter الإدخال الخاص بـ "none" من المصفوفة الناتجة بما أننا لا نهتم بتلك المحارف. سنحتاج إلى العدد الإجمالي للمحارف المنتمية إلى لغة ما لنستطيع حساب النسب، ويمكن معرفة ذلك من خلال reduce، وإن لم نجد هذه المحارف، فستعيد الدالة سلسلةً نصيةً محدّدةً، وإلا فستحوِّل مدخلات العد إلى سلاسل نصية مقروءة باستخدام map، ومن ثم تدمجها باستخدام join. خاتمة تبين لنا مما سبق أنّ تمرير قيم دالة ما إلى دوال أخرى مفيد جدًا، إذ يسمح لنا بكتابة دوال تُنمذِج الحسابات التي بها فراغات، إذ تستطيع الشيفرة التي تستدعي هذه الدوال ملء تلك الفراغات بتوفير قيم الدوال؛ أما المصفوفات فتعطينا عددًا من التوابع العليا، ويمكننا استخدام forEach للتكرار على عناصر داخل مصفوفة ما؛ ويعيد تابع filter مصفوفةً جديدةً تحتوي العناصر التي تمرِّر دالة التوقّع predicate function؛ كما نستطيع تحويل مصفوفة ما من خلال وضع كل عنصر في دالة باستخدام map؛ وكذلك نستطيع استخدام reduce لجمع عناصر مصفوفة ما داخل قيمة واحدة؛ أما تابع some فينظر هل ثَمّ عنصر مطابق لدالة توقع معطاة أم لا؛ ويبحث findIndex عن موضع أول عنصر مطابق لتوقّع ما. تدريبات التبسيط استخدم تابع method، وconcat لتبسيط مصفوفة بها مصفوفات أخرى، إلى مصفوفة واحدة بها جميع العناصر الموجودة في تلك المصفوفات كلها. تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen. let arrays = [[1, 2, 3], [4, 5], [6]]; // ضع شيفرتك هنا. // → [1, 2, 3, 4, 5, 6] الحلقة التكرارية الخاصة بك اكتب دالة loop العليا التي تعطي شيئًَا مثل تعليمة حلقة for التكرارية، إذ تأخذ قيمةً، ودالة اختبار، ودالة تحديث، ومتن دالة. تستخدِم في كل تكرار دالة الاختبار أولًا على قيمة التكرار الحالية، وتتوقف إن لم تتحقق -أي أعادت false-، ثم تستدعي متن الدالة لتعطيه القيمة الحالية، وأخيرًا تستدعي دالة التحديث لإنشاء قيمة جديدة والبدء من جديد. تستطيع عند تعريف الدالة استخدام حلقة تكرارية عادية لتنفيذ التكرار الفعلي. تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen. // شيفرتك هنا. loop(3, n => n > 0, n => n - 1, console.log); // → 3 // → 2 // → 1 كل شيء تحتوي المصفوفات على تابع every بالتماثل مع تابع some، ويتحقق every إذا تحققت الدالة المعطاة لكل عنصر في المصفوفة، ويمكن النظر إلى some في سلوكه على المصفوفات على أنه عامِل ||، في حين يكون every عامِل &&. استخدم every على أساس دالة تأخذ مصفوفة ودالة توقّع على أساس معامِلات، واكتب نسختين، إحداهما باستخدام حلقة تكرارية، والأخرى باستخدام تابع some. تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen. function every(array, test) { // ضع شيفرتك هنا. } console.log(every([1, 3, 5], n => n < 10)); // → true console.log(every([2, 4, 16], n => n < 10)); // → false console.log(every([], n => n < 10)); // → true إرشادات للحل يستطيع التابع every إيقاف تقييم المزيد من العناصر بمجرد إيجاد عنصر واحد غير مطابق، تمامًا كما في حالة عامل &&، لذا تستطيع النسخة المبنية على الحلقة التكرارية القفز خارجها -باستخدام break، أو return- عند إيجاد العنصر الذي تعيد له دالة التوقّع false، فإذا انتهت الحلقة التكرارية دون مقابلة عنصر كهذا، فسنعرف بتطابق جميع العناصر ويجب لإعادة true. نستخدم قوانين دي مورجَن De Morgan لبناء every فوق some، والذي ينص على أنّ a && b تساوي ‎!(!a || !b)‎، ويمكن أن يُعمَّم هذا للمصفوفات، حيث تكون كل العناصر في المصفوفة مطابقةً إذا لم يكن في المصفوفة عنصرًا غير مطابق. اتجاه الكتابة السائد اكتب دالة تحسب اتجاه الكتابة السائد في نص ما، وتذكّر أنّه لدى كل كائن من كائنات اللغات خاصية direction، والتي من الممكن أن تكون: ltr، أو rtl، أو ttb، كما ذكرنا في سابق شرحنا هنا. الاتجاه السائد هو اتجاه أغلب المحارف المرتبطة بلغة ما، وستستفيد من دالتي: characterScript، وcountBy المعرَّفتَين في هذا المقال. تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen. function dominantDirection(text) { // ضع شيفرتك هنا. } console.log(dominantDirection("Hello!")); // → ltr console.log(dominantDirection("Hey, مساء الخير")); // → rtl إرشادات للحل قد يبدو حلك مثل النصف الأول من مثال textScripts، فعليك عدّ المحارف بمقياس مبني على characterScript، ثم ترشيح الجزء الذي يشير إلى المحارف غير المهمة (غير النصية). يمكن إيجاد الاتجاه الذي يحمل أعلى عدد من المحارف بواسطة reduce، فإذا لم يكن ذلك واضحًا، فارجع إلى المثال السابق في هذا المقال حيث استُخدِم reduce لإيجاد النص الذي فيه أكثر المحارف. ترجمة -بتصرف- للفصل الخامس من كتاب Elequent Javascript لصاحبه Marijn Haverbeke. اقرأ أيضًا الدوال في جافاسكريبت. التخاطب بين النوافذ في جافاسكريبت. القيم والأنواع والعوامل في جافاسكريبت
  18. تشكل الأعداد، والقيم البوليانية، والسلاسل، الذرات التي تُبنى منها هياكل البيانات في مجال هندسة البرمجيات وعلوم الحاسوب، وستحتاج عند عملك في البرمجة إلى أكثر من ذرة واحدة، إذ تسمح لنا الكائنات objects بتجميع القيم -بما في ذلك الكائنات الأخرى-، من أجل بناء هياكل أكثر تعقيدًا. كانت البرامج التي أنشأناها حتى الآن في فصول هذه السلسلة محدودة إذ عملت فقط على أنواع بسيطة من البيانات، وسنتحدّث في هذا المقال عن الهياكل الأساسية للبيانات، كما ستعرف بنهايته ما يكفيك للبدء بكتابة برامج مفيدة، وسنمر فيه على بعض الأمثلة الواقعية نوعًا ما، متعرضين للمفاهيم أثناء تطبيقها على المشاكل المطروحة، كما سنبني الشيفرة التوضيحية غالبًا على الدوال والروابط التي شُرحت من قبل. الإنسان المتحول إلى سنجاب يتوهم سمير أنه يتحوّل إلى قارض فروي صغير ذو ذيل كثيف، وذلك من حين لآخر، وغالبًا بين الساعة الثامنة والعاشرة مساءً. وهو سعيد نوعًا ما لأنه غير مصاب بالنوع الشائع للاكتيريا السريرية classic lycanthropy، أو الاستذئاب -وهي حالة تجعل الشخص يتوهم أنه يتحول إلى حيوان، ويكون ذئبًا في الغالب-، فالتحول إلى سنجاب أهون من التحول إلى ذئب! إذ ليس عليه القلق إلا من أن يؤكل من قطة جاره، بدلاً من خشية أكل جاره بالخطأ. لقد قرر إغلاق أبواب غرفته ونوافذها في الليل، ووضع بعض حبات البندق على الأرض، وذلك بعد مرتين من إيجاد نفسه يستيقظ على غصن رقيق غير مستقر في شجرة بلوط، عاري الجسد مشوش الذهن. ولعلّ ذلك تكفّل بمسألتي القطة وشجرة البلوط، غير أنّ سمير يريد معالجة نفسه من حالته هذه بالكليّة، وقد لاحظ أنّ حالات التحول تلك غير منتظمة، فلا بد من وجود شيء ما يستفزها أو يبدؤها، وقد ظن لفترة أنّ شجرة البلوط هي السبب، إذ حدثت بضع مرات بجانبها، لكن تبين له أنّ تجنب أشجار البلوط لم يوقف المشكلة. رأى سمير أن يغّير منهجه في التفكير إلى سلوك علمي، فبدأ بتسجيل يومي لكل ما يفعله في اليوم وما إن كان قد تحوّل أم لا، وهو يأمل بهذه البيانات حصر الظروف التي تؤدي إلى التحولات. والآن، بإسقاط ما سبق على موضوع هذا المقال، فأول شيء يحتاجه هو بنية بيانات لتخزين هذه المعلومات، أليس كذلك؟ مجموعات البيانات إذا أردت العمل مع كميات كبيرة من البيانات الرقمية، فعليك أولًا إيجاد طريقة لتمثيلها في ذاكرة الحواسيب، فعلى سبيل المثال، إذا أردنا تمثيل تجميعة من الأرقام 2، و3، و5، و7، و11، فسنستطيع حل الأمر بأسلوب مبتكر باستخدام السلاسل النصية -إذ لا حد لطول السلسلة النصية، وعليه نستطيع وضع بيانات كثيرة فيها- واعتماد ‎"2 3 5 7 11"‎ على أنه التمثيل الخاص بنا، لكن هذا منظور غريب ومستهجن، إذ يجب استخراج الأعداد بطريقة ما، وإعادة تحويلها إلى أعداد من أجل الوصول إليها. توفر جافاسكربت بدلًا من السلوك السابق، نوع بيانات يختص بتخزين سلاسل القيم، وهو المصفوفة array، والتي تُكتب على أساس قائمة من القيم بين قوسين مربعين، ومفصولة بفاصلات إنجليزية Comma، انظر كما يلي: let listOfNumbers = [2, 3, 5, 7, 11]; console.log(listOfNumbers[2]); // → 5 console.log(listOfNumbers[0]); // → 2 console.log(listOfNumbers[2 - 1]); // → 3 كذلك تَستخدِم الصيغة التي نحصل بها على العناصر داخل مصفوفة ما، الأقواس المربعة أيضًا، حيث يأتي قوسان مربعان بعد تعبير ما مباشرةً، ويحملان بينهما تعبيرًا آخرًا، إذ يبحثان في التعبير الأيسر عن عنصر يتوافق مع الفهرس index المعطى في التعبير المحصور بينهما. الفهرس الأول للمصفوفة هو الصفر وليس الواحد، لذا يُسترد العنصر الأول باستخدام listOfNumbers[0]‎، وإن كنت جديدًا على علوم الحاسوب، فسيمر وقت قبل اعتياد بدء العد على أساس الصفر، وهو تقليد قديم في التقنية وله منطق مبني عليه، لكن كما قلنا، ستأخذ وقتك لتتعود عليه؛ ولتريح نفسك، فكر في الفهرس على أنه عدد العناصر التي يجب تخطيها بدءًا من أول المصفوفة. الخصائص رأينا في الفصول السابقة بعض التعبيرات المثيرة للقلق، مثل: myString.lengthالتي نحصل بها على طول السلسلة النصية، وMath.max التي تشير إلى الدالة العظمى، حيث تصل هذه تعبيرات إلى خصائص قيمة ما. ففي الحالة الأولى نصل إلى خاصية الطول للقيمة الموجودة في myString، أما في الثانية فسنصل إلى الدالة العظمى max في كائن Math، وهو مجموعة من الثوابت والدوال الرياضية. تحتوي جميع قيم جافاسكربت تقريبًا على خصائص، باستثناء null، وundefined، إذ ستحصل على خطأ إذا حاولت الوصول إلى خاصية إحدى هذه القيم. null.length; // → TypeError: null has no properties الطريقتان الرئيسيتان للوصول إلى الخصائص في جافاسكربت، هما: النقطة .، والأقواس المربعة []، حيث تصل كل من value.x، وvalue[x]‎ مثلًا، إلى خاصية ما في value، لكن ليس إلى الخاصية نفسها بالضرورة، ويكمن الفرق في كيفية تفسير x، فحين نستخدم النقطة فإن الكلمة التي تليها هي الاسم الحرفي للخاصية؛ أما عند استخدام الأقواس المربعة فيُقيَّم التعبير الذي بين الأقواس للحصول على اسم الخاصية. في حين تجلب value.x خاصيةً اسمها x لـ value، فستحاول value[x]‎ تقييم التعبير x، وتستخدم النتيجة -المحوَّلة إلى سلسلة نصية- على أساس اسم للخاصية، لذا فإن كنت على علم بأنّ الخاصية التي تريدها تحمل الاسم "color"، فتقول value.color؛ أما إن أردت استخراج الخاصية المسماة بالقيمة المحفوظة في الرابطة i، فتقول value‎. واعلم أنّ أسماء الخصائص ما هي إلا سلاسل نصية، فقد تكون أي سلسلة نصية، لكن صيغة النقطة لا تعمل إلا مع الأسماء التي تبدو مثل أسماء رابطات صالحة. فإذا أردت الوصول إلى خاصية اسمها "2" أو "John Doh"، فيجب عليك استخدام الأقواس المربعة: value[2]‎، أو value ["John Doh"]‎. تُخزَّن العناصر في المصفوفة على أساس خصائص لها، باستخدام الأعداد على أساس أسماء للخصائص، وبما أنك لا تستطيع استخدام الصياغة النقطية مع الأرقام، وتريد استخدام رابطة تحمل الفهرس، فيجب عليك استخدام صيغة الأقواس للوصول إليها. تخبرنا خاصية length للمصفوفة كم عدد العناصر التي تحتوي عليها، واسم الخاصية ذاك هو اسم رابطة صالح، كما نعرِّف اسمه مسبقًا، لذلك نكتب array.length للعثور على طول المصفوفة، وذلك أسهل من كتابة array["length"]‎. التوابع Methods تحتوي قيم السلاسل النصية وقيم المصفوفات على عدد من الخصائص التي تحمل قيمًا للدالة، إضافةً إلى خاصية length، كما في المثال التالي: let doh = "Doh"; console.log(typeof doh.toUpperCase); // → function console.log(doh.toUpperCase()); // → DOH كل سلسلة لها خاصية toUpperCase، إذ تعيد عند استدعائها نسخةً من السلسلة التي تم فيها تحويل جميع الأحرف إلى أحرف كبيرة. وبالمثل، تسير خاصية toLowerCase في الاتجاه العكسي. ومن المثير أنّ الدالة لديها وصول لسلسلة "Doh" النصية، وهي القيمة التي استدعينا خاصيتها، رغم أن استدعاء toUpperCase لا يمرر أي وسائط، وسننظر في تفصيل كيفية حدوث ذلك في المقال السادس. تسمى الخصائص التي تحتوي على دوال توابعًا للقيم المنتمية إليها، فمثلًا، يُعَد toUpperCase تابعًا لسلسلة نصية، ويوضح المثال التالي تابعَيْن يمكنك استخدامهما للتعامل مع المصفوفات: let sequence = [1, 2, 3]; sequence.push(4); sequence.push(5); console.log(sequence); // → [1, 2, 3, 4, 5] console.log(sequence.pop()); // → 5 console.log(sequence); // → [1, 2, 3, 4] يضيف تابع push قيمًا إلى نهاية مصفوفة ما؛ أما تابع pop فيفعل العكس تمامًا، حيث يحذف القيمة الأخيرة في المصفوفة ويعيدها. وهذه الأسماء السخيفة هي المصطلحات التقليدية للعمليات على المكدِّس stack، والمكدِّس في البرمجة هو أحد هياكل البيانات التي تسمح لك بدفع القيم إليها وإخراجها مرة أخرى بالترتيب المعاكس، بحيث يُبتدأ بإزالة العنصر الذي أضيف آخر مرة، وذلك استنادًا على منطق "آخرهم دخولًا أولهم خروجًا". ولعلك تذكر دالة مكدس الاستدعاءات من المقال السابق الذي يشرح الفكرة نفسها. الكائنات Objects بالعودة إلى سمير المتحوِّل، فيمكن تمثيل مجموعة من المدخلات اليومية للسجل بمصفوفة، لكن مدخلات التسجيلات تلك فيها أكثر من مجرد عدد أو سلسلة نصية، فكل إدخال يحتاج إلى تخزين قائمة بالأنشطة، وقيمة بوليانية توضح ما إذا كان سمير قد تحول إلى سنجاب أم لا، ونحن نود تجميع ذلك في قيمة واحدة، ثم نضع تلك القيم المجمعة في مصفوفة من مدخلات السجل، وبما أن القيم التي من نوع object هي مجرد تجميعات عشوائية من الخصائص، فيمكن إنشاء كائن باستخدام الأقواس في صورة تعبير. انظر كما يلي: let day1 = { squirrel: false, events: ["work", "touched tree", "pizza", "running"] }; console.log(day1.squirrel); // → false console.log(day1.wolf); // → undefined day1.wolf = false; console.log(day1.wolf); // → false لدينا قائمة بالخصائص داخل الأقواس مفصولة بفواصل إنجليزية ,، ولكل خاصية اسم متبوع بنقطتين رأسيتين وقيمة، وحين يُكتب كائن في عدة أسطر، فإن وضع إزاحة بادئة له كما في المثال يجعل قراءته أيسر، والخصائص التي لا تحتوي أسماؤها على أسماء رابطات صالحة أو أرقام صالحة، يجب وضعها داخل علامتي اقتباس. انظر كما يلي: let descriptions = { work: "Went to work", "touched tree": "Touched a tree" }; هذا يعني أن الأقواس لها معنيان في جافاسكربت، فهي تبدأ بكتلة من التعليمات البرمجية إن جاءت في بداية تعليمة ما؛ أما إذا جاءت في موضع آخر، فستصف كائنًا ما. ولعلّ من حسن حظنا أننا نادرًا ما سنحتاج إلى بدء تعليمة بكائن داخل قوسين، لذا لا تشغل بالك كثيرًا بشأن هذا الغموض والإشكال. كذلك سيعطيك قراءة خاصية غير موجودة القيمة undefined، ونستطيع استخدام عامل = لإسناد قيمة إلى تعبيرِ خاصية ليغير القيمة الموجودة أصلًا، أو ينشئ خاصيةً جديدةً للكائن إن لم تكن. بالعودة إلى نموذجنا لمجسات الأخطبوط الذي ذكرناه سابقًا عن الرابطة Binding، فإن رابطات الخصائص متشابهة، فهي تلتقط القيم، لكن قد تكون بعض الرابطات والخصائص الأخرى ممسكة بتلك القيم نفسها، وعليه تستطيع النظر إلى الكائنات على أنها أخطبوطات لها عدد لا نهائي من المجسات، ولكل منها اسم منقوش عليها. يقطع عامل delete أحد المجسات من الأخطبوط السابق، وهذا العامل هو عاملٌ أحادي، كما يحذف الخاصية المسماة من الكائن حين يُطبَّق على خاصيته، وذلك ممكن رغم عدم شيوعه. let anObject = {left: 1, right: 2}; console.log(anObject.left); // → 1 delete anObject.left; console.log(anObject.left); // → undefined console.log("left" in anObject); // → false console.log("right" in anObject); // → true عند تطبيق العامل الثنائي inعلى سلسلة نصية وكائن، فسيخبرك إذا كان الكائن به خاصية باسم تلك السلسلة النصية، والفرق بين جعل الخاصية undefined وحذفها على الحقيقة، هو أنّ الكائن ما زال يحتفظ بالخاصية في الحالة الأولى مما يعني عدم حمله لقيمة ذات شأن؛ أما في الحالة الثانية فإن الخاصية لم تَعُدْ موجودة، وعليه فستعيد in القيمة false. تُستخدَم دالة Object.keys لمعرفة الخصائص التي يحتوي عليها الكائن، وذلك بإعطائها كائنًا، فتعيد مصفوفةً من السلاسل النصية التي تمثل أسماء خصائص الكائن. انظر كما يلي: console.log(Object.keys({x: 0, y: 0, z: 2})); // → ["x", "y", "z"] تُستخدَم دالة Object.assign لنسخ جميع الخصائص من كائن إلى آخر، انظر كما يلي: let objectA = {a: 1, b: 2}; Object.assign(objectA, {b: 3, c: 4}); console.log(objectA); // → {a: 1, b: 3, c: 4} وتكون المصفوفات حينئذ نوعًا من الكائنات المتخصصة في تخزين سلاسل من أشياء بعينها، وإذا قيَّمت typeof[]‎، فستُنتج "object"، وسترى هذه المصفوفات كأخطبوطات طويلة بمجساتها في صف أنيق له عناوين من الأعداد. انظر الآن إلى السجل journal الذي يحتفظ به سمير في صورة مصفوفة من الكائنات: let journal = [ {events: ["work", "touched tree", "pizza", "running", "television"], squirrel: false}, {events: ["work", "ice cream", "cauliflower", "lasagna", "touched tree", "brushed teeth"], squirrel: false}, {events: ["weekend", "cycling", "break", "peanuts", "juice"], squirrel: true}, /* and so on... */ ]; قابلية التغير Mutability إذا كنت قد قرأت الفصول السابقة، فسترى أنّ أنواع القيم التي تحدثنا عنها من أعداد، وسلاسل نصية، وقيم بوليانية، لا يمكن تغييرها؛ صحيح أنك تستطيع جمعها واستخراج قيم أخرى منها، لكن بمجرد أخذها قيمة لسلسلة نصية فلن تتغير بعدها، وسيبقى النص داخلها كما هو دون تغير، فمثلًا، إن كانت لديك سلسلة نصية تحتوي على "cat"، فلن تستطيع شيفرة أخرى تغيير محرف في هذه السلسلة لتكون "rat". أما الكائنات فلها شأن آخر، إذ تستطيع تغيير خصائصها، حيث تتخذ قيمة الكائن محتويات مختلفة في كل مرة، كما رأينا قبل قليل أنه يمكن تعديل قيم الكائنات. حين يكون لدينا عددان 120، و120، فسنقول أنهما نفس العددين سواءً أشارا إلى البتات الحقيقية نفسها أم لا، أما مع الكائنات فهناك فرق بين وجود مرجعين إلى الكائن نفسه، وبين وجود كائنين مختلفين يحتويان نفس الخصائص، انظر الشيفرة التالية: let object1 = {value: 10}; let object2 = object1; let object3 = {value: 10}; console.log(object1 == object2); // → true console.log(object1 == object3); // → false object1.value = 15; console.log(object2.value); // → 15 console.log(object3.value); // → 10 تلتقط رابطتيobject1، وobject2 الكائن نفسه، لهذا ستتغير قيمة object2 إذا تغير object1، فيقال أنّ لهما "الهوية" نفسها إن صح التعبير؛ أما الرابطة object3، فتشير إلى كائن آخر يحتوي على خصائص object1 نفسها، لكنه منفصل ومستقل بذاته. قد تكون الروابط نفسها متغيرة أو ثابتة، لكن هذا منفصل عن الطريقة التي تتصرف بها قيمها، ورغم أن القيم العددية لا تتغير، إلا أنك تستطيع استخدام الرابطة let لمتابعة عدد متغير من خلال تغيير القيمة التي تشير الرابطة إليها، وبالمثل، فرغم أن تعريف كائن بالرابطة const سيظل يشير إلى الكائن نفسه ولا يمكن تغييرها لاحقًا، إلا أن محتويات هذا الكائن قابلة للتغيير، كما في المثال التالي: const score = {visitors: 0, home: 0}; // This is okay score.visitors = 1; // This isn't allowed score = {visitors: 1, home: 1}; يوازن العامل == بين الكائنات من منظور هويتها، فلا يعطي true إلا إذا كان لكلا الكائنين القيمة نفسها تمامًا؛ أما عند موازنة كائنات مختلفة، فسيعطي false حتى ولو كان لهذه الكائنات الخصائص نفسها، وعليه فليس هناك عملية موازنة "عميقة" في جافاسكربت توازن بين الكائنات من خلال محتوياتها، لكن من الممكن كتابة ذلك بنفسك. سجل المستذئب نعود إلى سمير الذي يظن بأنّه يتحول إلى حيوان في الليل، إذ يبدأ مفسِّر جافاسكربت الخاص به، ويضبط البيئة التي يحتاجها من أجل سجله journal، انظر كما يأتي: let journal = []; function addEntry(events, squirrel) { journal.push({events, squirrel}); } لاحظ أنّ الكائن الذي أضيف إلى السجل يبدو غريبًا نوعًا ما، فبدلًا من التصريح عن الخصائص مثل events: events، فهو لا يزيد عن إعطاء اسم الخاصية فقط. ويُعَدّ هذا الأسلوب اختصارًا مشيرًا إلى الشيء نفسه، أي إذا كان اسم الخاصية موضوع بين قوسين وليس متبوعًا بقيمة، فستؤخذ قيمته من الرابطة التي تحمل الاسم نفسه؛ لذا، ففي كل ليلة عند العاشرة مساءً -أو في الصباح التالي أحيانًا-، يسجل سمير يومه كالتالي: addEntry(["work", "touched tree", "pizza", "running", "television"], false); addEntry(["work", "ice cream", "cauliflower", "lasagna", "touched tree", "brushed teeth"], false); addEntry(["weekend", "cycling", "break", "peanuts", "juice"], true); وهو ينوي اتباع أسلوب إحصائي عند حصوله على نقاط بيانات كافية، وذلك لرؤية أيَّ تلك الأحداث هي التي تحث تحوله إلى حيوان ليلًا. يختلف المتغير في الإحصاء عن المتغير البرمجي، إذ يكون لدينا مجموعة من المقاييس، بحيث يقاس كل متغير بها جميعًا، وتُمثَّل علاقة الترابط Correlation بين المتغيرات بقيمة بين -1، و1، وعلاقة الترابط هي مقياس اعتمادية المتغير الإحصائي على متغير آخر. فإذا كانت قيمة علاقة الترابط هذه صفرًا، فهذا يعني أنّ المتغيرين غير مرتبطان ببعضهما؛ أما إذا كان 1، فهذا يعني أنّ المتغيرين متطابقان تمامًا. بحيث إذا كنت تعرف أحدهما، فأنت تعرف الأخر يقينًا؛ أما إذا كانت قيمة علاقة الترابط تلك -1، فهذا يعني أنهما متطابقان لكنهما متقابلان، بحيث إن كان الأول true، فالآخر false. نستخدم معامِل فاي ϕ لحساب مقياس علاقة الترابط بين متغيرين بوليانيين، وهي معادلة يكون دخلها جدول تردد يحتوي على عدد المرات التي لوحظت مجموعات المتغيرات فيها؛ ويصف الخرج علاقة الترابط بينها بحيث يكون عددًا بين -1، و1. فمثلًا، سنأخذ حدث تناول البيتزا ونضع ذلك في جدول تردد مثل التالي، حيث يشير كل عدد إلى عدد المرات التي وقعت فيها هذه المجموعة في قياساتنا: فإذا سمينا هذا الجدول بجدول n مثلًا، فإننا سنستطيع حساب ϕ باستخدام المعادلة التالية: ولا تشغل نفسك بشأن الرياضيات كثيرًا ها هنا، حيث وُضِعت هذه المعادلة لتحويلها إلى جافاسكربت. تشير الصيغة n01 إلى عدد القياسات التي يكون فيها المتغير الأول squirrel "السنجاب" غير متحقق أو خطأ false، والمتغير الثاني pizza "البيتزا" متحقق أو صحيح true، ففي جدول البيتزا مثلًا، تكون قيمة n01 هي 9. تشير القيمة n1•‎ إلى مجموع القياسات التي كان فيها المتغير الأول متحققًا -أي true-، وهي 5 في الجدول المثال. بالمثل، تشير n•0‎ إلى مجموع القياسات التي كان فيها المتغير الثاني يساوي false. لذا سيكون الجزء الموجود أعلى خط الفصل في جدول البيتزا 1×76−4×9 = 40، وسيكون الجزء السفلي هو الجذر التربيعي لـ 5×85×10×80، أو 340000√، ونخرج من هذا أن قيمة فاي هي 0.069 تقريبًا، وهي قيمة ضئيلة قطعًا، وعليه فلا يبدو أنّ البيتزا لها تأثير على تحول سمير. حساب علاقة الترابط نستطيع تمثيل جدول من صفين وعمودين في جافاسكربت، باستخدام مصفوفة من أربعة عناصر (‎[76, 9, 4, 1]‎)، أو مصفوفة تحتوي على مصفوفتين، بحيث تتكون كل واحدة منهما من عنصرين (‎[[76, 9], [4, 1]]‎)، أو كائن له أسماء خصائص، مثل: "11"، و"01"`. غير أنّ المصفوفة المسطحة أسهل وتقصِّر طول التعبيرات التي تصل إلى الجدول، وسنفسر فهارس المصفوفة في صورة أعداد ثنائية مكوّنة من بِتَّين، حيث يشير الرقم الأيسر إلى متغير السنجاب، والأيمن إلى متغير الحدث. فمثلًا، يشير العدد الثنائي 10 إلى الحالة التي تحوّل فيها سمير إلى سنجاب، لكن حدث البيتزا مثلًا لم يقع، وقد حدث هذا أربع مرات؛ وبما أن العدد الثنائي 10 ما هو إلا العدد 2 في النظام العشري، فسننخزن هذا الرقم في الفهرس 2 من المصفوفة. انظر الدالة التي تحسب قيمة معامل ϕ من مثل هذه المصفوفة: function phi(table) { return (table[3] * table[0] - table[2] * table[1]) / Math.sqrt((table[2] + table[3]) * (table[0] + table[1]) * (table[1] + table[3]) * (table[0] + table[2])); } console.log(phi([76, 9, 4, 1])); // → 0.068599434 الشيفرة أعلاه هي ترجمة حرفية لمعادلة فاي الرياضية السابقة في جافاسكربت، وتكون فيها Math.sqrt هي دالة الجذر التربيعي التي يوفرها كائن Math في بيئة جافاسكربت القياسية، ويجب إضافة حقلين من الجدول لنحصل على حقول مثل n1•‎، ذلك أن مجموع الصفوف أو الأعمدة لا يُخزَّن في قاعدة بياناتنا مباشرةً وقد احتفظ سمير بسجله لمدة ثلاثة أشهر، وستجد النتائج لتلك الفترة متاحة في صندوق التجارب لهذا المقال، حيث تُخزَّن في رابطة JOURNAL وهي متاحة للتحميل. سنكرر الآن على كل الإدخالات، وسنسجّل عدد مرات وقوع حدث التحول إلى سنجاب، وذلك لاستخراج جدول بسطرين وعمودين، وذلك كما يلي: function tableFor(event, journal) { let table = [0, 0, 0, 0]; for (let i = 0; i < journal.length; i++) { let entry = journal[i], index = 0; if (entry.events.includes(event)) index += 1; if (entry.squirrel) index += 2; table[index] += 1; } return table; } console.log(tableFor("pizza", JOURNAL)); // → [76, 9, 4, 1] يتحقق تابع includes من وجود قيمة ما في المصفوفة، وتَستخدِم الدالة ذلك لتحديد وجود اسم الحدث الذي تريده في قائمة الأحداث في يوم ما. يبيّن متن الحلقة التكرارية في tableFor أيّ صندوق في الجدول يقع فيه إدخال السجل، من خلال النظر في احتواء الإدخال على حدث بعينه أم لا، والنظر هل وقع الحدث مع وقوع التحول السنجابي أم لا، ثم تزيد الحلقة التكرارية الصندوق الصحيح بمقدار 1 داخل الجدول. لدينا الآن الأدوات التي نحتاجها في حساب علاقة الترابط الفردية، والخطوة المتبقية هي إيجاد علاقة الترابط لكل نوع من الأحداث تم تسجيله، وننظر هل سنخرج بنتيجة أم لا. حلقات المصفوفات التكرارية لدينا حلقة تكرارية في دالة tableFor، وهي: for (let i = 0; i < JOURNAL.length; i++) { let entry = JOURNAL[i]; // Do something with entry } يكثر هذا النوع من الحلقات في جافاسكربت الكلاسيكية، إذ يشيع المرور على المصفوفات عنصرًا عنصرًا، وذلك باستخدام عدّادً على طول المصفوفة واختيار كل عنصر على حِدَة، لكن هناك طريقة أبسط لكتابة مثل تلك الحلقات التكرارية في جافاسكربت الحديثة، وذلك كما يلي: for (let entry of JOURNAL) { console.log(`${entry.events.length} events.`); } حين تُكتَب حلقة forمع الكلمة of بعد تعريفِ متغير، ستتكرر على عناصر القيمة المعطاة بعد of، ويصلح هذا في المصفوفات، والسلاسل النصية، وبعض هياكل البيانات الأخرى، كما سيأتي بيانه في المقال السادس. التحليل النهائي نحتاج الآن إلى حساب علاقة الترابط لكل نوع من الأحداث التي وقعت في مجموعة البيانات التي جمعها سمير، ويحتاج هذا الحساب أولًا إلى إيجاد كل نوع من أنواع الأحداث. انظر المثال الآتي: function journalEvents(journal) { let events = []; for (let entry of journal) { for (let event of entry.events) { if (!events.includes(event)) { events.push(event); } } } return events; } console.log(journalEvents(JOURNAL)); // → ["carrot", "exercise", "weekend", "bread", …] تجمع الدالة journalEvents كل أنواع الأحداث من خلال المرور على الأحداث وإضافة الغير موجود منها إلى المصفوفة events، ونستطيع رؤية كل الالتزامات من خلال الحلقة التالية: for (let event of journalEvents(JOURNAL)) { console.log(event + ":", phi(tableFor(event, JOURNAL))); } // → carrot: 0.0140970969 // → exercise: 0.0685994341 // → weekend: 0.1371988681 // → bread: -0.0757554019 // → pudding: -0.0648203724 // and so on... من هذه النتائج، نستطيع القول أن أغلب علاقات الترابط، مثل: أكل الجزر، والخبز، وغيرها، لا تستفز التحول الحيواني لدى سمير لأنها تقترب من الصفر، لكن من ناحية أخرى، فيبدو أنها تزيد في الإجازات الأسبوعية، وعليه سنرشّح النتائج أكثر لنرى أيَّ علاقات الترابط كانت أكبر من 0.1، أو أقل من -0.1. for (let event of journalEvents(JOURNAL)) { let correlation = phi(tableFor(event, JOURNAL)); if (correlation > 0.1 || correlation < -0.1) { console.log(event + ":", correlation); } } // → weekend: 0.1371988681 // → brushed teeth: -0.3805211953 // → candy: 0.1296407447 // → work: -0.1371988681 // → spaghetti: 0.2425356250 // → reading: 0.1106828054 // → peanuts: 0.5902679812 لدينا عاملان بهما علاقة ترابط أقوى مما سواهما، وهما: أكل الفول السوداني الذي له أثر إيجابي قوي على فرصة التحول، وغسل الأسنان الذي له تأثير قوي كذلك لكن في الاتجاه المعاكس. دعنا نجرب الشيفرة التالية: for (let entry of JOURNAL) { if (entry.events.includes("peanuts") && !entry.events.includes("brushed teeth")) { entry.events.push("peanut teeth"); } } console.log(phi(tableFor("peanut teeth", JOURNAL))); // → 1 هذه نتيجة قوية، إذ تحدث الظاهرة تحديدًا حين يأكل سمير الفول السوداني وينسى غسل أسنانه، وبما أنه عرف هذا، فقد قرر إيقاف أكل الفول السوداني بالكليّة، ووجد ظاهرة تحوله إلى سنجاب لم تتكرر بعدها! زيادة على المصفوفات نريد أن نعرّفك على بعض المفاهيم الأخرى المتعلقة بالكائنات قبل إنهاء هذا المقال، فقد رأينا في بداية هذا المقال push، وpop لإضافة العناصر وحذفها من نهاية مصفوفة ما؛ أما التابعان الموافقان لإضافة وحذف العناصر من بداية المصفوفة، فهما: unshift، وshift، وذلك كما يأتي: let todoList = []; function remember(task) { todoList.push(task); } function getTask() { return todoList.shift(); } function rememberUrgently(task) { todoList.unshift(task); } ينظم البرنامج أعلاه مجموعةً من المهام المرتبة في طابور، حيث تضيف مهامًا إلى نهاية الطابور باستدعاء remember("groceries")‎، وتَستدعِي getTask()‎ إذا أردت فعل شيء ما، وذلك لجلب -وحذف- العنصر الأمامي من الطابور، كما تضيف دالة rememberUrgently مهمةً إلى أول الطابور، وليس إلى آخره. توفر المصفوفات تابع indexOf الذي يبحث في المصفوفة من بدايتها إلى نهايتها عن قيمة معينة، ويعيد فهرس المكان الذي وجد عنده القيمة المطلوبة، وإذا أردت البحث من نهاية المصفوفة بدلًا من بدايتها، فلدينا تابع مماثل اسمه lastIndexOf، انظر كما يلي: console.log([1, 2, 3, 2, 1].indexOf(2)); // → 1 console.log([1, 2, 3, 2, 1].lastIndexOf(2)); // → 3 ويأخذ كلا التابعين indexOf، وlastIndexOf وسيطًا ثانيًا اختياريًا يوضح أين يجب أن يبدأ البحث. يُعَدّ التابع slice من التوابع الأساسية للمصفوفات، إذ يأخذ فهرس البداية والنهاية، ويعيد مصفوفةً تحوي العناصر المحصورة بين هذين الفهرسين، ويكون فهرس البداية موجودًا في هذه المصفوفة الناتجة، أما فهرس النهاية فلا. انظر المثال التالي: console.log([0, 1, 2, 3, 4].slice(2, 4)); // → [2, 3] console.log([0, 1, 2, 3, 4].slice(2)); // → [2, 3, 4] إذا لم يُعط فهرس النهاية لتابع slice، فسيأخذ كل العناصر التي تلي فهرس البداية، وإن لم تذكر فهرس البداية، فسينسخ المصفوفة كلها. يُستخدَم تابع concat للصق المصفوفات معًا لإنشاء مصفوفة جديدة، وهو في هذا يماثل وظيفة عامل + في السلاسل النصية. انظر المثال التالي للتابعين السابقين، إذ تأخذ الدالة remove مصفوفةً وفهرسًا، ثم تعيد مصفوفةً جديدةً، بحيث تكون نسخةً من الأصلية بعد حذف العنصر الموجود عند الفهرس المعطى: function remove(array, index) { return array.slice(0, index) .concat(array.slice(index + 1)); } console.log(remove(["a", "b", "c", "d", "e"], 2)); // → ["a", "b", "d", "e"] إذا مرّرنا وسيطًا ليس بمصفوفة إلى concat، فستضاف تلك القيمة إلى المصفوفة الجديدة كما لو كانت مصفوفة من عنصر واحد. السلاسل النصية وخصائصها نستطيع قراءة خصائص من قيم السلاسل النصية، مثل الخاصتين: length، وtoUpperCase، لكن إذا حاولت إضافة خاصية جديدة، فلن تبقى، انظر المثال التالي: let kim = "Kim"; kim.age = 88; console.log(kim.age); // → undefined ذلك أن قيم السلاسل النصية، والأعداد، والقيم البوليانية، ليست بكائنات. وعليه فلن تمنعك اللغة من وضع خصائص جديدة على هذه القيم، فهي لا تخزن تلك الخصائص على الحقيقة، إذ لا يمكن تغيير تلك القيم كما ذكرنا من قبل؛ غير أنّ هذه الأنواع لها خصائصها المدمجة فيها، فكل قيمة سلسلة نصية لها عدد من التوابع، لعلّ slice، وindexOf أكثرها نفعًا واستخدامًا، واللذين يشبهان في وظائفهما التابعَين المذكورَين قبل قليل، انظر المثال التالي: console.log("coconuts".slice(4, 7)); // → nut console.log("coconut".indexOf("u")); // → 5 الفرق بينهما أنه يستطيع تابع indexOf في السلسلة النصية، البحث عن سلسلة تحتوي على أكثر من محرف واحد؛ بينما تابع المصفوفة الذي يحمل الاسم نفسه لا يبحث إلا عن عنصر واحد، أي كما في المثال التالي: console.log("one two three".indexOf("ee")); // → 11 يحذف تابع trim المسافات البيضاء، مثل: المسافات، والأسطر الجديدة، وإزاحات الجداول، وما شابه ذلك، من بداية ونهاية السلسلة النصية، ومثال على ذلك: console.log(" okay \n ".trim()); // → okay الدالة zeroPad المستخدَمة في المقال السابق، موجودة هنا على أساس تابع أيضًا، ويسمى padStart، حيث يأخذ الطول المطلوب، ومحرف الحشو على أساس وسائط، كما في المثال التالي: console.log(String(6).padStart(3, "0")); // → 006 تستطيع تقسيم سلسلة نصية عند كل ظهور لسلسلة أخرى باستخدام تابع split، ثم دمجها مرةً أخرى باستخدام تابع join، أي كما في المثال التالي: let sentence = "Secretarybirds specialize in stomping"; let words = sentence.split(" "); console.log(words); // → ["Secretarybirds", "specialize", "in", "stomping"] console.log(words.join(". ")); // → Secretarybirds. specialize. in. stomping يمكن تكرار السلسلة النصية باستخدام تابع repeat، حيث ينشِئ سلسلةً نصيةً جديدةً تحتوي نسخًا متعددةً من السلسلة الأصلية، وملصقةً معًا. انظر المثال التالي: console.log("LA".repeat(3)); // → LALALA وبالنسبة لخاصية length للسلاسل النصية التي رأيناها من قبل، فيحاكي الوصول إلى المحرف داخل سلسلة نصية، الوصول إلى عناصر المصفوفة مع فارق بسيط سنناقشه في المقال الخامس. انظر المثال التالي: let string = "abc"; console.log(string.length); // → 3 console.log(string[1]); // → b معامل rest من المفيد لدالة قبول أي عدد من الوسائط، فمثلًا، تحسب الدالة Math.max القيمة العظمى لكل الوسائط المعطاة؛ إذ يمكننا تحقيق ذلك بوضع ثلاث نقاط قبل آخر معامِل للدالة، كما يلي: function max(...numbers) { let result = -Infinity; for (let number of numbers) { if (number > result) result = number; } return result; } console.log(max(4, 1, 9, -2)); // → 9 وحين تُستدعى هذه الدالة فإن معامل rest يكون ملزَمًا بمصفوفة تحتوي كل الوسائط الأخرى، وإذا كان ثمة معامِلات أخرى قبله، فلا تكون قيمها جزءًا من المصفوفة؛ أما حين يكون هو المعامل الوحيد كما في حالة max، فستحمل المصفوفة كل الوسائط. تستطيع استخدام صيغة النقاط الثلاثة لاستدعاء دالة مع مصفوفة وسائط، كما في المثال التالي: let numbers = [5, 1, 7]; console.log(max(...numbers)); // → 7 يوسع هذا المصفوفة إلى استدعاء الدالة ممررًا عناصرها على أساس وسائط منفصلة، ومن الممكن إضافة مصفوفة مثل هذه إلى جانب وسائط أخرى كما في max(9, ...numbers, 2)‎، كذلك تسمح صيغة الأقواس المربعة لمصفوفة، لعامل النقاط الثلاثة، بتوسيع مصفوفة أخرى داخل هذه المصفوفة الجديدة، كما في المثال التالي: let words = ["never", "fully"]; console.log(["will", ...words, "understand"]); // → ["will", "never", "fully", "understand"] الكائن Math كما رأينا سابقًا، فـ Math ما هو إلا حقيبةٌ من دوال التعامل مع الأعداد، مثل: Math.max للقيمة العظمى، وMath.min للقيمة الصغرى، وMath.sqrt للجذر التربيعي. يُستخدَم كائن Math على أساس حاوية لمجموعة من الوظائف المرتبطة ببعضها بعضًا، كما أنّه كائن وحيد، إذ لا يوجد كائن آخر يحمل الاسم نفسه، وهو غير مفيد أيضًا إن جاء على أساس قيمة، فهو يوفر فضاء اسم namespace لئلا تضطر الدوال والقيم لتكوّن روابط عامة global bindings، حيث تلوث كثرة هذه الروابط العامة فضاء الاسم، فكلما زاد عدد الأسماء المحجوزة زادت فرصة تغيير قيمة رابطة حالية بالخطأ، ولا بأس مثلًا بتسمية شيء ما باسم max في برنامج تكتبه، إذ أنّ دالة max المدمجة بجافاسكربت محفوظة بأمان داخل كائن Math، لذا فلن تتغير بفعل منك. لن توقفك أو تحذرك جافاسكربت -على عكس كثير من اللغات الأخرى- من تعريف رابطة باسم مأخوذ من قبل، إلا أن تكون رابطةً صرحْتَ عنها باستخدام let، أو const، أما الروابط القياسية أوالمصرَّح عنها باستخدام var، أو function فلا. ستحتاج إلى كائن Math إن أردت تنفيذ بعض العمليات المتعلِّقة بحساب المثلثات، وذلك لاحتوائه على دوال الجيب sin، وجيب التمام cos، والظل tan، إضافةً إلى دوالها المعكوسة، وهي: asin، وacos، وatan، كما أن العدد باي π متاح أيضًا في جافاسكربت في صورة Math.PI؛ وكُتِبت بحروف إنجليزية كبيرة تطبيقًا لعادة قديمة في البرمجة، إذ تُكتَب أسماء القيم الثابتة بالحروف الكبيرة. function randomPointOnCircle(radius) { let angle = Math.random() * 2 * Math.PI; return {x: radius * Math.cos(angle), y: radius * Math.sin(angle)}; } console.log(randomPointOnCircle(2)); // → {x: 0.3667, y: 1.966} ولا تقلق إن لم تكن قد تعرضت لهذه الدوال من قبل، حيث ستُشرح حين يأتي ذكرها في المقال الرابع عشر من هذه السلسلة، وقد استخدمنا الدالة Math.random في المثال أعلاه، إذ تعيد عددًا عشوائيًا وهميًا بين الصفر والواحد في كل مرة تستدعيها، مع استثناء الواحد نفسه فلا تعيده. انظر المثال التالي: console.log(Math.random()); // → 0.36993729369714856 console.log(Math.random()); // → 0.727367032552138 console.log(Math.random()); // → 0.40180766698904335 رغم أنّ الحواسيب آلات تعيينية، أي تتصرف بالطريقة نفسها إن أعطيتها المدخلات ذاتها، إلا أنّه من الممكن جعلها تنتج أعدادًا قد تبدو عشوائية، ولفعل ذلك تحتفظ الآلة بقيمة مخفيّة، وتُجري حسابات معقدة على هذه القيمة المخفيّة لإنشاء واحدة جديدة في كل مرة تسألها فيها إعطاءك عددًا عشوائيًا؛ كما تخزِّن القيمة الجديدة وتعيد عددًا مشتقًا منها، وهكذا تستطيع إنتاج أعداد بطريقة تبدو عشوائية ويصعب التنبؤ بها؛ أما إن أردت أعدادًا صحيحةً وعشوائيةً بدلًا من الأعداد الكسرية، فاستخدام Math.floor على الخرج الذي تحصل عليه من Math.random، حيث تقرِّب العدد إلى أقرب عدد صحيح. console.log(Math.floor(Math.random() * 10)); // → 2 سيعطيك ضرب العدد العشوائي في 10 عددًا أكبر من أو يساوي الصفر، وأصغر من العشرة؛ وبما أن Math.floor تقرِّبه، فسينتج هذا التعبيرأعدادًا من 0 إلى 9 باحتمالات متساوية. تقرِّب الدالة Math.ceil إلى عدد صحيح، كما تقرِّب الدالة Math.round إلى أقرب رقم صحيح، وتأخذ الدالة Math.abs القيمة المطلقة لعدد ما، أي تنفي القيمة السالبة وتترك القيمة الموجبة كما هي. التفكيك انظر الشيفرة التالية: function phi(table) { return (table[3] * table[0] - table[2] * table[1]) / Math.sqrt((table[2] + table[3]) * (table[0] + table[1]) * (table[1] + table[3]) * (table[0] + table[2])); } إذا عدنا إلى دالة phi السابقة، فإن أحد الأسباب التي يجعل هذه الدالة صعبةً في قراءتها، هو أنّه لدينا رابطة تشير إلى مصفوفتنا، بينما نريد رابطات لعناصر المصفوفة، أي let n00 = table[0]‎، وهكذا. لحسن الحظ لدينا طريقة مختصرة في جافاسكربت تفعل ذلك: function phi([n00, n01, n10, n11]) { return (n11 * n00 - n10 * n01) / Math.sqrt((n10 + n11) * (n00 + n01) * (n01 + n11) * (n00 + n10)); } هذا يصلح أيضًا للرابطات التي أنشئت باستخدام let، وvar، وconst، فإذا كانت القيمة التي تربطها مصفوفةً تستطيع استخدام الأقواس المربعة لتنظر داخلها لتربط محتوياتها. كذلك بالنسبة للكائنات إذ نستخدم الأقواس العادية بدلًا من المربعة، انظر المثال التالي: let {name} = {name: "Faraji", age: 23}; console.log(name); // → Faraji لاحظ أنك إذا حاولت تفكيك null، أو undefined، فستحصل على خطأ، وكذلك إن حاولت الوصول مباشرةً إلى إحدى خصائص هاتين القيمتين. صيغة JSON بما أن الخصائص تلتقط قيمها ولا تحتويها، فتُخزَّن المصفوفات والكائنات في ذاكرة الحاسوب على أساس سلاسل من البتات حاملةً عناوينًا لمحتوياتها، وهذه العناوين هي أماكن في الذاكرة، لذا فإن احتوت مصفوفة على مصفوفة أخرى داخلها، فستشغل من الذاكرة قطاعًا واحدًا على الأقل للمصفوفة الداخلية، وواحدًا آخرًا للمصفوفة الخارجية، حيث سيحتوي على عدد ثنائي يمثل موضع المصفوفة الداخلية، مع أشياء أخرى قطعًا. وإن أردت حفظ بيانات في ملف لاستخدامها لاحقًا أو إرسالها إلى حاسوب آخر عبر الشبكة، فعليك تحويل هذه العناوين المتشابكة إلى وصف يمكن تخزينه أو إرساله، وبهذا تستطيع إرسال ذاكرة حاسوبك كلها مع عنوان القيمة التي تريدها، رغم أن هناك طرق أفضل لهذا. والذي نستطيع فعله هو تحويل سَلسلة البيانات، بمعنى تحويلها إلى وصف بسيط، وثَمَّ استخدام طريقة مشهورة تسمى JSON -تنطق جَيسون- وهي اختصار لصيغة الكائنات في جافاسكربت JavaScipt Object Notation، وتُستخدَم استخدامًا واسعًا على أساس طريقة لتخزين البيانات، وصيغة للتواصل في الإنترنت حتى في اللغات الأخرى غير جافاسكربت. تحاكي صيغة JSON طريقة جافاسكربت في كتابة المصفوفات والكائنات مع بعض القيود، لذا فيجب إحاطة كل أسماء الخصائص بعلاماتي تنصيص مزدوجة، ولا يُسمح إلا بتعابير البيانات البسيطة، فلا استدعاءات لدوال، ولا روابط، ولا أي شيء فيه حوسبة حقيقية؛ والتعليقات ممنوعة أيضًا. وإذا عدنا -مرةً أخرى- إلى مثال سمير المتحوِّل، وأردنا تمثيل مدخلًا للسجل الذي يحتفظ به في صورة بيانات JSON، فسيبدو هكذا: { "squirrel": false, "events": ["work", "touched tree", "pizza", "running"] } تعطينا جافاسكربت دالتي JSON.stringify، وJSON.parse، لتحويل البيانات من وإلى هذه الصيغة، فالأولى تأخذ قيمة من جافاسكربت، وتعيد سلسلةً نصيةً مرمّزةً بصيغة JSON، والثانية تأخذ هذه السلسلة وتحولها إلى القيمة التي ترمِّزها، كما في المثال التالي: let string = JSON.stringify({squirrel: false, events: ["weekend"]}); console.log(string); // → {"squirrel":false,"events":["weekend"]} console.log(JSON.parse(string).events); // → ["weekend"] خاتمة توفر الكائنات والمصفوفات طريقًا لتجميع عدة قيم ووضعها في قيمة واحدة، ويسمح هذا نظريًا لنا بوضع بعض الأشياء المرتبطة ببعضها في حقيبة واحدة، وذلك لنتعامل مع الحقيبة كلها بدلًا من محاولة الإمساك بهذه الأشياء واحدةً واحدةً بأيدينا. تملك أغلب القيم في جافاسكربت خصائص، باستثناء: null، وundefined، ونستطيع الوصول إلى تلك الخصائص باستخدام value.prop، أو value["prop"]‎. تميل الكائنات إلى استخدام أسماء خصائصها وتخزين مجموعة منها؛ أما المصفوفات فتحتوي غالبًا على كميات مختلفة من القيم المتطابقة نظريًا وتستخدم الأعداد (بدءًا من الصفر) على أساس أسماء لخصائصها، ولدينا بعض الخصائص المسماة في المصفوفات مثل length، وعددًا من التوابع التي هي دوال تعيش داخل الخصائص وتتحكم عادةً في القيم التي تكون جزءًا منها. تستطيع تطبيق التكرار على المصفوفات باستخدام نوع خاص من حلقة for التكرارية، أي for(let element of array)‎. ترجمة -بتصرف- للفصل الرابع من كتاب Elequent Javascript لصاحبه Marijn Haverbeke. اقرأ أيضًا المقال التالي: الدوال العليا في جافاسكريبت المقال السابق: هيكل البرنامج في جافاسكريبت الدليل الشامل: علم البيانات
  19. يقول دونالد كنوث Donald Knuth لا غنى عن الدوال في لغة جافاسكربت، إذ نستخدمها في هيكلة البرامج الكبيرة لتقليل التكرار، ولربط البرامج الفرعية بأسماء، وكذا لعزل تلك البرامج الفرعية عن بعضها، ولعل أبرز تطبيق على الدوال هو إدخال مصطلحات جديدة في اللغة. حيث يمكن إدخال أيّ مصطلح إلى لغة البرمجة من أيّ مبرمج يعمل بها، وذلك على عكس لغات البشر المنطوقة التي يصعب إدخال مصطلحات إليها، إلا بعد مراجعات واعتمادات من مجامع اللغة. وفي الواقع المشاهد، يُعَدّ إدخال المصطلحات إلى اللغة على أساس دوال، ضرورةً حتميةً لاستخدامها في البرمجة وإنشاء برامج للسوق. فمثلًا، تحتوي اللغة الإنجليزية -وهي المكتوب بحروفها أوامر لغات البرمجة-، على نصف مليون كلمة تقريبًا، وقد لا يعلم المتحدث الأصلي لها إلا بـ 20 ألف كلمة منها فقط، وقَلَّ ما تجد لغةً من لغات البرمجة التي يصل عدد أوامرها إلى عشرين ألفًا. وعليه، ستكون المصطلحات المتوفرة فيها دقيقة المعنى للغاية، وبالتالي فهي جامدة وغير مرنة، ولهذا نحتاج إلى إدخال مصطلحات جديدة على هيئة دوال، وذلك بحسب حاجة كل مشروع أو برنامج. تعريف الدالة الدالة هي رابطة منتظمة، حيث تكون قيمة هذه الرابطة هي الدالة نفسها، إذ تُعرِّف الشيفرة التالية مثلًا، الثابت square لتشير إلى دالة تنتج مربع أي عدد مُعطَى: const square = function(x) { return x * x; }; console.log(square(12)); // → 144 وتُنشأ الدالة بتعبير يبدأ بكلمة function المفتاحية، كما يكون للدوال مجموعة معامِلات parameters -معامِل وحيد هو x حسب المثال السابق-، ومتن body لاحتواء التعليمات التي يجب تنفيذها عند استدعاء الدالة، كما يُغلَّف متن الدالة بقوسين معقوصين حتى ولو لم يكن فيه إلا تعليمة واحدة. كذلك يجوز للدالة أن يكون لها عدة معامِلات، أو لا يكون لها أيّ معامِل، ففي المثال التالي، لا تحتوي دالة makenoise على أيّ معاملات، بينما تحتوي power على معاملين اثنين: const makeNoise = function() { console.log("Pling!"); }; makeNoise(); // → Pling! const power = function(base, exponent) { let result = 1; for (let count = 0; count < exponent; count++) { result *= base; } return result; }; console.log(power(2, 10)); // → 1024 وتنتج بعض الدوال قيمًا، مثل: دالتي power، وsquare، ولكن هذا ليس قاعدة، إذ لا تعطي بعض الدوال الأخرى قيمةً، مثل دالة makenoise، ونتيجتها الوحيدة هي أثر جانبي side effect. تحدِّد تعليمة return القيمة التي تعيدها الدالة، فحين تمر بُنية تحكُّم control -مثل التعليمات الشرطية- على تعليمة مشابهة لهذه، فستقفز مباشرةً من الدالة الحالية، وتعطي القيمة المعادة إلى الشيفرة التي استدعت الدالة. وإن لم يتبع كلمة return المفتاحية أيّ تعبير، فستعيد الدالة قيمة غير معرفة undefined، كما تعيد الدوال التي ليس فيها تعليمة return قيمة غير معرفة undefined، مثل دالة makenoise. تتصرف معامِلات الدالة على أساس الرابطات المنتظمة regular bindings، غير أنّه يحدِّد مستدعي الدالة قيمتها الأولية، وليس الشيفرة التي بداخل الدالة. الرابطات Bindings والنطاقات Scopes نطاق الرابطة في البرنامج هو الجزء الذي تكون الرابطة ظاهرةً فيه، حيث كل رابطة لها نطاق. وإذا عرَّفنا الرابطة خارج دالة أو كتلة شيفرات، فيكون نطاق هذه الرابطة البرنامج كاملًا، ويمكنك الإشارة إلى مثل تلك الرابطات أينما تشاء، وتسمى رابطات عامة Global Bindings؛ أما الرابطات المنشأة لمعامِلات الدالة، أو المصرح عنها داخل دالة ما، فيمكن الإشارة إليها داخل تلك الدالة فقط، وعليه تسمّى رابطات محلية Local bindings، وتُنشأ نسخ جديدة من تلك الرابطات في كل مرة تُستدعَى الدالة فيها، وذلك يوفر نوعًا من العزل بين الدوال بما أنّ كل دالة تتصرف في عالمها الخاص -بيئتها المحلية-، وييسّر فهم المراد منها دون الحاجة إلى العلم بكل ما في البيئة العامة. كما تكون الرابطات المصرح عنها باستخدام let، وconst رابطات محلية لكتلة الشيفرة التي صُرح عن تلك الرابطات فيها، فإن أنشأت أحد تلك الرابطات داخل حلقة تكرارية، فلن تتمكن الشيفرات الموجودة قبل هذه الحلقة وبعدها، من رؤية تلك الرابطة. ولم يُسمح إنشاء نطاقات جديدة لغير الدوال في إصدارات جافاسكربت قبل 2015، لذا كانت الرابطات التي أُنشِئت باستخدام كلمة var المفتاحية، مرئيةً في كل الدالة التي تظهر فيها هذه الرابطات، أو في النطاق العام إن لم تكن داخل دالة ما. كما في المثال التالي: let x = 10; if (true) { let y = 20; var z = 30; console.log(x + y + z); // → 60 } // y is not visible here console.log(x + z); // → 40 يستطيع كل نطاق البحث في النطاق الذي يحيط به، لذا تكون x ظاهرة داخل كتلة الشيفرة في المثال السابق مع استثناء وجود عدة رابطات بالاسم نفسه، ففي تلك الحالة لا تستطيع الشيفرة إلا رؤية الأقرب لها، كما في المثال التالي، حيث تشير الشيفرة داخل دالة halve إلى n الخاصة بها وليس إلى n العامة: const halve = function(n) { return n / 2; }; let n = 10; console.log(halve(100)); // → 50 console.log(n); // → 10 النطاق المتشعب نستطيع إنشاء كتل شيفرات ودوال داخل كتل ودوال أخرى ليصبح لدينا عدة مستويات من المحلية، فمثلًا، تخرج الدالة التالية المكونات المطلوبة لصنع مقدار من الحمُّص، وتحتوي على دالة أخرى داخلها، أي كما يأتي: const hummus = function(factor) { const ingredient = function(amount, unit, name) { let ingredientAmount = amount * factor; if (ingredientAmount > 1) { unit += "s"; } console.log(`${ingredientAmount} ${unit} ${name}`); }; ingredient(1, "can", "chickpeas"); ingredient(0.25, "cup", "tahini"); ingredient(0.25, "cup", "lemon juice"); ingredient(1, "clove", "garlic"); ingredient(2, "tablespoon", "olive oil"); ingredient(0.5, "teaspoon", "cumin"); }; تستطيع شيفرة الدالة ingredient أن ترى رابطة factor من الدالة الخارجية، على عكس رابطتيها المحليتين الغير مرئيتين من الدالة الخارجية، وهما: unit، وingredientAmount. ويُحدِّد مكان كتلة الشيفرة في البرنامج الرابطات التي ستكون مرئيةً داخل تلك الكتلة، حيث يستطيع النطاق المحلي رؤية جميع النطاقات المحلية التي تحتويه، كما تستطيع جميع النطاقات رؤية النطاق العام، ويُسمّى هذا المنظور لمرئية الرابطات، المراقبة المُعجَمية Lexical Scoping. الدوال على أساس قيم تتصرف رابطة الدالة عادةً على أساس اسم لجزء بعينه من البرنامج، وتُعرَّف هذه الرابطة مرةً واحدةً ولا تتغير بعدها، ويسهّل علينا هذا، الوقوع في الخلط بين اسم الدالة والدالة نفسها، غير أنّ الاثنين مختلفان عن بعضهما، إذ تستطيع قيمة الدالة فعل كل ما يمكن للقيم الأخرى فعله، كما تستطيع استخدامها في تعبيرات عشوائية، وتخزينها في رابطة جديدة، وتمريرها كوسيط لدالة، وهكذا. وذلك إضافةً إلى إمكانية استدعاء تلك القيمة بلا شك. وبالمثل، لا تزال الرابطة التي تحمل الدالة مجرد رابطة منتظمة regular bindung، كما يمكن تعيين قيمة جديدة لها إذا لم تكن ثابتة constant، كما في المثال الآتي: let launchMissiles = function() { missileSystem.launch("now"); }; if (safeMode) { launchMissiles = function() {/* لا تفعل شيئًا */}; } وسنناقش في المقال الخامس بعض الأمور الشيقة التي يمكن فعلها بتمرير قيم الدالة إلى دوال أخرى. مفهوم التصريح توجد طريقة أقصر لإنشاء رابطة للدالة، حيث تُستخدم كلمة function المفتاحية في بداية التعليمة، أي كما يلي: function square(x) { return x * x; } ويسمى هذا بتصريح الدالة function declaration، فتعرِّف التعليمة الرابطة "square" وتوجهها إلى الدالة المعطاة، وذلك أسهل قليلًا في الكتابة، ولا يتطلب فاصلة منقوطة بعد الدالة، لكن قد يكون هذا الأسلوب من التصريح عن الدوال خدّاعًا: console.log("يقول لنا المستقبل", future()); function future() { return "لن تكون هناك سيارات تطير"; } وعلى الرغم من أن الدالة معرَّفة أسفل الشيفرة التي تستخدمها، إلا أنها صالحة وتعمل بكفاءة، وذلك لأن تصريح الدوال ليس جزءًا من مخطط السير العادي من الأعلى إلى الأسفل، بل يتحرك إلى قمة نطاقه، ويكون متاحًا للاستخدام من قِبَل جميع الشيفرات الموجودة في ذلك النطاق، ويفيدنا هذا أمر أحيانًا لأنه يوفر حرية ترتيب الشيفرة ترتيبًا منطقيًا ومفيدًا، دون القلق بشأن الحاجة إلى تعريف كل الدوال قبل استخدامها. الدوال السهمية Arrow Functions لدينا مفهوم ثالث للتصريح عن الدوال، وقد يبدو مختلفًا عن البقية، حيث يستخدِم سهمًا مكتوبًا في صورة إشارة التساوي ومحرف "أكبر من"، أي على الصورة (‎=>‎)، لهذا انتبه من الخلط بينها وبين محرف "أكبر من أو يساوي"، الذي يُكتب على الصورة (‎>=‎)، ويوضح المثال التالي هذا المفهوم: const power = (base, exponent) => { let result = 1; for (let count = 0; count < exponent; count++) { result *= base; } return result; }; يأتي السهم بعد قائمة المعامِلات ويُتبع بمتن الدالة، ويكون على صورة: "هذا الدخل (المعامِلات) يُنتِج هذا الخرج (المتن)"، وحين يكون لدينا اسم معامِل واحد، فيمكنك إهمال الأقواس المحيطة بقائمة المعامِلات، وإن كان المتن تعبيرًا واحدًا بدلًا من كتلة بين قوسين، فستعيد الدالة ذلك التعبير، وعليه ينفذ التعريفين التاليين لـ square، الشيء نفسه: const square1 = (x) => { return x * x; }; const square2 = x => x * x; وعندما تكون الدالة السهمية بدون معامِلات على الإطلاق، فستكون قائمة معامِلاتها مجرد قوسين فارغين، أي كما في المثال التالي: const horn = () => { console.log("Toot"); }; ليس ثمة سبب لوجود الدوال السهمية وتعبيرات function معًا في اللغة، إذ يملكان الوظيفة نفسها بصرف النظر عن التفاصيل الصغيرة، وسنتحدث عن ذلك في المقال السادس، كما لم تُضَف الدوال السهمية إلا في عام 2015، وذلك من أجل السماح بكتابة تعبيرات دوال صغيرة بأسلوب قليل الصياغة، حيث سنستخدمها كثيرًا في المقال الخامس. مكدس الاستدعاء The call stack قد تبدو طريقة تدفُق التحكم خلال الدوال متداخلة قليلا، انظر المثال التالي للتوضيح، حيث ينفذ بعض الاستدعاءات من الدوال المعطاة: function greet(who) { console.log("Hello " + who); } greet("Harry"); console.log("Bye"); وعند تشغيل البرنامج السابق فإن مخطط سيره يكون كالتالي: عند استدعاء greet، يقفز التحكم إلى بداية تلك الدالة (السطر الثاني في الشيفرة)، وتستدَعي بدورها console.log التي ينتقل التحكم إليها لتُنفّذ مهمتها، ثم تعود إلى المكان الذي استدعاها، وهو السطر الرابع. ثم يستدعي السطر الذي يليه، أي console.log مرةً أخرى، ويصل البرنامج إلى نهايته بعد إعادة ذلك. وإذا أردنا تمثيل مخطط تدفق التحكم لفظيًا، فسيكون كالتالي: خارج الدالة في greet في console.log في greet خارج الدالة في console.log خارج الدالة ونظرًا لوجوب قفز الدالة إلى المكان الذي استدعاها، فلابد للحاسوب من تذكُّر السياق الذي حدث منه الاستدعاء، ففي إحدى الحالات أعلاه، كان على console.log العودة إلى دالة greet عند انتهاء تنفيذها، بينما تعود إلى نهاية البرنامج في الحالة الأخرى. يسمى المكان الذي يخزن فيه الحاسوب هذا السياق، بمكدس الاستدعاء call stack، ويُخزَّن السياق الحالي في قمة ذلك المكدس في كل مرة تُستدعى دالة، كما تزيل السياق الأعلى من المكدس، وتستخدمه لمتابعة التنفيذ عندما تعود الدالة.، حيث يحتاج هذا المكدس إلى مساحة في ذاكرة الحاسوب، وبما أنّ تلك المساحة محدودة، فقد يعطيك الحاسوب رسالة فشل، مثل: عدم وجود ذاكرة كافية في المكدس "out of stack space"، أو تكرارات تفوق الحد المسموح به "too much recursion". حيث لدينا المثال الآتي: function chicken() { return egg(); } function egg() { return chicken(); } console.log(chicken() + " came first."); // → ?? إذ توضح الشيفرة السابقة هذا الأمر بسؤال الحاسوب أسئلة صعبة، تجعله يتنقّل بين الدالتين ذهابًا وإيابًا إلى ما لا نهاية، وفي حالة المكدس اللانهائي هذه، فستنفذ ذاكرة الحاسوب المتاحة، أو ستطيح بالمكدس blow the stack. الوسائط الاختيارية Optional arguments انظر الشيفرة التالية: function square(x) { return x * x; } console.log(square(4, true, "hedgehog")); // → 16 تسمح لغة جافاسكربت بتنفيذ الشيفرة السابقة دون أدنى مشكلة، ورغم أننا عرَّفنا فيها square بمعامِل واحد فقط، ثم استدعيناها بثلاثة معامِلات، فقد تجاهلت الوسائط الزائدة، وحسبت مربع الوسيط الأول فقط. وتستنتج من هذا أن جافاسكربت لديها سعة -إن صح التعبير- في شأن الوسائط التي تمررها إلى الدالة، فإن مررت وسائط أكثر من اللازم، فستُتجاهل الزيادة، أما إن مرّرت وسائط أقل من المطلوب، فتُسنَد المعامِلات المفقودة إلى القيمة undefined. وسيّئة ذلك أنك قد تُمرِّر عدد خاطئ من الوسائط، ولن تعرف بذلك، ولن يخبرك أحد ولا حتى جافاسكربت نفسها، أما حسنته فيمكن استخدام هذا السلوك للسماح لدالة أن تُستدعى مع عدد مختلف من الوسائط. انظر المثال التالي حيث تحاول دالة minus محاكاة معامِل - من خلال وسيط واحد أو وسيطين: function minus(a, b) { if (b === undefined) return -a; else return a - b; } console.log(minus(10)); // → -10 console.log(minus(10, 5)); // → 5 وإذا كتبنا عامل = بعد معامِل ما، ثم أتبعنا ذلك بتعبير، فستحل قيمة التعبير محل الوسيط إذا لم يكن معطى مسبقًا، إذ تجعل دالة الأس power مثلًا، وسيطها الثاني اختياريًا، فإن لم تعطها أنت ذلك الوسيط أو تمرر قيمة undefined، فسيتغير تلقائيًا إلى 2، وستتصرف الدالة مثل دالة التربيع square بالضبط، كما يأتي: function power(base, exponent = 2) { let result = 1; for (let count = 0; count < exponent; count++) { result *= base; } return result; } console.log(power(4)); // → 16 console.log(power(2, 6)); // → 64 سننظر في المقال التالي إلى طريقة يحصل بها متن الدالة على جميع الوسائط الممررة، ويفيدنا هذا في السماح للدالة بقبول أي عدد من الوسائط، كما في console.log، إذ تُخرج كل القيم المعطاة إليها، أي: console.log("C", "O", 2); // → C O 2 التغليف Closure إذا قلنا أننا نستطيع معاملة الدوال مثل قيم، وأنه يعاد إنشاء الرابطات المحلية في كل مرة تُستدعى فيها الدالة، فإننا نتساءل هنا عما يحدث لتلك الرابطات حين يصير الاستدعاء الذي أنشأها غير نشط؟ توضح الشيفرة التالية مثالًا على هذا، فهي تعرِّف دالة wrapValue، والتي تنشئ رابطةً محليةً، ثم تعيد دالةً تصل إلى تلك الرابطة وتعيدها. انظر: function wrapValue(n) { let local = n; return () => local; } let wrap1 = wrapValue(1); let wrap2 = wrapValue(2); console.log(wrap1()); // → 1 console.log(wrap2()); // → 2 يُعَدّ هذا الأسلوب جائزًا ومسموحًا به في جافاسكربت، ولا يزال بإمكانك الوصول إلى كلا النسختين، حيث يوضح ذلك المثال حقيقة أنّ الرابطات المحلية تُنشَأ من جديد في كل استدعاء، وأنّ الاستدعاءات المختلفة لا تدمر رابطات بعضها البعض. وتسمّى تلك الخاصية بالمغلِّف closure، أي خاصية القدرة على الإشارة إلى نسخة بعينها من رابطة محلية في نطاق محيط enclosing scope، وتسمى الدالة التي تشير إلى رابطات من نطاقات محلية حولها، بالمغلف closure. يحررك هذا السلوك من القلق بشأن دورة حياة تلك الرابطات، ويتيح لك استخدام قيم الدوال بطرق جديدة، فيمكِّننا تغيير بسيط، من قلب المثال السابق إلى طريقة لإنشاء دوال تضاعِف بقيمة عشوائية، أي كما يلي: function multiplier(factor) { return number => number * factor; } let twice = multiplier(2); console.log(twice(5)); // → 10 وبما أنّ المعامِل نفسه يُعَدّ رابطةً محليةً، فلم نعد بحاجة إلى الرابطة الصريحة local من دالة wrapValue السابقة. ويحتاج التفكير في برامج مثل هذا إلى بعض التمرس، والنموذج الذهني المعين على هذا هو التفكير في قيم الدالة على أنها تحتوي شيفرة المتن وبيئتها التي أُنشئَت فيها، وحين تُستدعى الدالة، يرى متن الدالة البيئة التي أنشئت فيها وليس البيئة التي استدعيَت فيها. وفي المثال السابق، تُستدعى الدالة multiplier، وتُنشئ بيئة يكون فيها المعامل factor مقيدًا بـ 2، وتتذكر قيمة الدالة التي تعيدها، وتكون مخزنة في twice، هذه البيئة، لذا حين تُستدعى فستضاعف وسيطها بمقدار 2. التعاود Recursion تستطيع الدالة استدعاء نفسها طالما أنها لا تكثر من ذلك إلى الحد الذي يطفح المكدس، وتسمى هذه الدالة المستدعية نفسها باسم العودية recursive، حيث يسمح التعاود لبعض الدوال بأن تُكتب في صور مختلفة كما في المثال التالي، إذ نرى استخدامًا مختلفًا لدالة الأس power: function power(base, exponent) { if (exponent == 0) { return 1; } else { return base * power(base, exponent - 1); } } console.log(power(2, 3)); // → 8 وهذا قريب من الطريقة التي يُعرَّف بها الأس عند الرياضيين، ويصف الفكرة أفضل من الصورة التكرارية looping، إذ تستدعي الدالة نفسها عدة مرات مع أسس مختلفة، لتحقيق عمليات الضرب المتكررة. ولكن ثمة مشكلة في هذا النموذج، إذ هو أبطأ بثلاث مرات من الصورة التكرارية في تطبيقات جافاسكربت، فالمرور على حلقة تكرارية أيسر، وأقل تكلفةً من استدعاء دالة عدة مرات. وهذه المسألة، أي مسألة السرعة مقابل الأناقة، لَمعضلة فريدة بين ما يناسب الإنسان وما يناسب الآلة، فلا شك أننا نستطيع زيادة سرعة البرنامج إذا زدنا حجمه وتعقيده، وإنما يقع على المبرمج تقدير كل موقف ليوازن بين هذا وذاك. وإذا عدنا إلى حالة دالة الأس السابقة power، فأسلوب التكرار looping وهو المنظور غير الأنيق هنا، هو أبسط وأيسر في القراءة، وليس من المنطق استبدال النسخة التعاودية recursive به وإحلالها محله، لكن اعلم أنّ اختيار الأسهل، والأرخص، والأقل تكلفةً في المال والموارد الأخرى، ليس القاعدة في البرمجة، ولا يجب أن يكون كذلك، فقد يُعرض لنا موقف نتخلى فيه عن هذه الكفاءة من السهولة والسرعة في سبيل جعل البرنامج بسيطًا وواضحًا، وقد يكون فرط التفكير في الكفاءة مشتتًا لك عن المطلوب من البرنامج في الأصل، فهذا عامل آخر يعطل تصميمه، وبحسب المرء تعقيد البرنامج ومطلوب العميل منه، فلا داعي لإضافة عناصر جديدة تزيد القلق إلى حد العجز عن التنفيذ وإتمام العمل. لهذا أنصحك بمباشرة أول كتابتك للبرنامج بكتابة شيفرة صحيحة عاملة وسهلة الفهم، وهذا رأيي في ما يجب عليك وضعه كهدف نصب عينيك، وإن أردت تسريع البرنامج فتستطيع ذلك لاحقًا بقياس أدائه ثم تحسين سرعته إن دعت الحاجة، وإن كان القلق بشأن بطء البرنامج غالبًا ليس في محله بما أن أغلب الشيفرات لا تُنفَّذ بالقدر الذي يجعلها تأخذ وقتًا ملحوظًا. فقد تجد أحيانًا مشكلات يكون حلها أسهل باستخدام التعاود عوضًا عن التكرار، وهي المشاكل التي تتطلب استكشاف عدة فروع أو معالجتها، إذ قد تحتوي تلك الفروع بدورها على فروع أخرى، وهكذا تجد أنّ التكرار قد يكون أقل كفاءةً من التعاود أحيانًا! يمكنك النظر إلى الأحجية التالية كمثال على هذا، فإذا بدأنا من العدد 1 وأضفنا 5 أو ضربنا في 3 باستمرار ، فسينتج لدينا مجموعة لا نهائية من الأعداد. كيف تكتب دالةً نعطيها عددًا فتحاول إيجاد تسلسل عمليات الجمع والضرب التي تنتج هذا العدد؟ إرشاد: يمكن التوصل إلى العدد 13 بضرب 1 في 3، ثم إضافة 5 مرتين، في حين أننا لن نستطيع الوصول إلى العدد 15 مطلقًا. انظر الحل الآن بأسلوب التعاود: function findSolution(target) { function find(current, history) { if (current == target) { return history; } else if (current > target) { return null; } else { return find(current + 5, `(${history} + 5)`) || find(current * 3, `(${history} * 3)`); } } return find(1, "1"); } console.log(findSolution(24)); // → (((1 * 3) + 5) * 3) لاحظ أنّ هذا البرنامج لا يزعج نفسه بالبحث عن أقصر تسلسل من العمليات، بل أي تسلسل يحقق المراد وحسب، ولأن هذا البرنامج مثال رائع على أسلوب التفكير التعاودي، فسأعيد شرحه مفصلًا إن لم تستوعب منطقه بمجرد النظر. تنفذ دالة find الداخلية التعاود الحقيقي، فتأخذ وسيطين: العدد الحالي، وسلسلة نصية string لتسجل كيف وصلنا إلى هذا العدد، فإن وجدت حلًا، فستعيد سلسلةً نصيةً توضح كيفية الوصول إلى الهدف؛ أما إن لم تجد حلا بالبدء من هذا العدد، فستعيد null. ولتحقيق ذلك، تنفِّذ الدالة أحد ثلاثة إجراءات: يُعاد العدد الحالي إن كان هو العدد الهدف، حيث يُعَد السجل الحالي طريقة للوصول إليه. تُعاد null إن كان العدد الحالي أكبر من الهدف، فليس من المنطق أن نبحث في هذا الفرع، حيث ستجعل عملية الإضافة أو الضرب، العدد أكبر مما هو عليه. تُعاد النتيجة إن كنا لا نزال أقل من العدد الهدف، فتحاول الدالة كلا الطريقتين اللتين تبدءان من العدد الحالي باستدعاء نفسها مرتين، واحدة للإضافة وأخرى للضرب، وتُعاد نتيجة الاستدعاء الأول إن كان أي شيء غير null، وإلا فيُعاد الاستدعاء الثاني بغض النظر عن إخراجها لسلسلة نصية أم null. ولفهم كيفية إخراج هذه الدالة للأثر الذي نريده، دعنا ننظر في الاستدعاءات التي تُجرى على دالة find، عند البحث عن حل للعدد 13: find(1, "1") find(6, "(1 + 5)") find(11, "((1 + 5) + 5)") find(16, "(((1 + 5) + 5) + 5)") too big find(33, "(((1 + 5) + 5) * 3)") too big find(18, "((1 + 5) * 3)") too big find(3, "(1 * 3)") find(8, "((1 * 3) + 5)") find(13, "(((1 * 3) + 5) + 5)") found! لاحظ أنّ الإزاحة في المثال السابق توضح عمق مكدس الاستدعاء. حيث تستدعي find في أول استدعاء لها باستدعاء نفسها للبحث عن حل يبدأ بـ (1+5)، وسيتعاود هذا الاستدعاء للبحث في كل حل ينتج عددًا أقل أو يساوي العدد الهدف. وتعيد null إلى الاستدعاء الأول بما أنها لن تجد ما يطابق الهدف، وهنا يتدخل عامل || ليتسبب في الاستدعاء الذي يبحث في (1*3)، ويكون هذا البحث هو أول استدعاء تعاودي داخل استدعاء تعاودي آخَر يصيب العدد الهدف. ويعيد آخر استدعاء فرعي سلسلة نصية، وتُمرر هذه السلسلة من قبل عامليْ || في الاستدعاء البيني intermediate call، مما يعيد لنا الحل في النهاية. الدوال النامية Growing Functions لدينا في البرمجة طريقتين لإدخال الدوال في البرامج، أولاهما أن تجد نفسك تكرر كتابة شيفرة بعينها عدة مرات، وهو أمر لا شك أنك لا تريد فعله، فيزيد وجود شيفرات كثيرة من احتمال ورود أخطاء أكثر في البرنامج، ومن الإرهاق المصاحب في البحث عنها، ووقتًا أطول في قراءة الشيفرة منك ومن غيرك ممن يحاول فهم برنامجك لتعديله أو للبناء عليه، لذا عليك أخذ تلك الشيفرة المتكررة وتسميها باسم يليق بها ويعجبك، ثم تضعها في دالة. أما الطريقة الثانية فهي حين تحتاج إلى بعض الوظائف التي لم تكتبها بعد، ويبدو أنها تستحق دالة خاصة بها، فتبدأ بتسمية هذه الدالة، ثم تشرع في كتابة متنها، وقد تبدأ في كتابة التعليمات البرمجية التي تستخدم الدالة قبل تعريف الدالةَ نفسها. واعلم أنّ مقياس وضوح المفهوم الذي تريد وضعه في هذه الدالة، هو مدى سهولة العثور على اسم مناسب للدالة! فكلما كان هدف الدالة واضحًا ومحددًا، سهل عليك تسميتها. ولنقل أنك تريد كتابة برنامج لطباعة عددين: عدد الأبقار، وعدد الدجاج في مزرعة، مع إتباع العدد بكلمة بقرة، وكلمة دجاجة بعده، ووضع أصفار قبل كلا العددين بحيث يكون طولهما دائمًا ثلاثة خانات، فهذا يتطلب دالةً من وسيطين، وهما: عدد الأبقار، وعدد الدجاج. 007 Cows 011 Chickens وهذا يتطلب دالة من وسيطين، هما: عدد الأبقار، وعدد الدجاج. function printFarmInventory(cows, chickens) { let cowString = String(cows); while (cowString.length < 3) { cowString = "0" + cowString; } console.log(`${cowString} Cows`); let chickenString = String(chickens); while (chickenString.length < 3) { chickenString = "0" + chickenString; } console.log(`${chickenString} Chickens`); } printFarmInventory(7, 11); إذا كتبنا length. بعد تعبير نصي، فسنحصل على طول هذا التعبير، أو هذه السلسلة النصية، وعليه ستضيف حلقات while التكرارية أصفارًا قبل سلاسل الأعداد النصية، لتكون ثلاثة محارف على الأقل. وهكذا، فقد تمت مهمتنا! ولكن لنفرض أنّ صاحبة المزرعة قد اتصلت بنا قبيل إرسال البرنامج إليها، وأخبرتنا بإضافة اسطبل إلى مزرعتها، حيث استجلبت خيولًا، وطلبت إمكانية طباعة البرنامج لبيانات الخيول أيضًا. هنا تكون الإجابة أننا نستطيع، لكن خطر لنا خاطر بينما نحن نوشك على نسخ هذه الأسطر الأربعة، ونلصقها مرةً أخرى، إذ لا بد من وجود طريقة أفضل، أي كما يأتي: function printZeroPaddedWithLabel(number, label) { let numberString = String(number); while (numberString.length < 3) { numberString = "0" + numberString; } console.log(`${numberString} ${label}`); } function printFarmInventory(cows, chickens, horses) { printZeroPaddedWithLabel(cows, "Cows"); printZeroPaddedWithLabel(chickens, "Chickens"); printZeroPaddedWithLabel(horses, "Horses"); } printFarmInventory(7, 11, 3); وهنا نجحت الشيفرة، غير أنّ اسم printZeroPaddedWithLabel محرج نوعًا ما، إذ يجمع في وظيفة واحدة، بين كل من: الطباعة، وإضافة الأصفار، وإضافة العنوان label، لذا بدلًا من إلغاء الجزء المكرر من البرنامج. دعنا نختر مفهومًا واحدًا فقط: function zeroPad(number, width) { let string = String(number); while (string.length < width) { string = "0" + string; } return string; } function printFarmInventory(cows, chickens, horses) { console.log(`${zeroPad(cows, 3)} Cows`); console.log(`${zeroPad(chickens, 3)} Chickens`); console.log(`${zeroPad(horses, 3)} Horses`); } printFarmInventory(7, 16, 3); حيث تسهل الدالة ذات الاسم الجميل والواضح مثل zeroPad، على الشخص الذي يقرأ الشيفرة معرفة ما تفعله، وهي مفيدة في مواقف أكثر من هذا البرنامج خاصة، إذ تستطيع استخدامها لطباعة جداول منسقة من الأعداد. لكن إلى أي حد يجب أن تكون الدالة التي تكتبها ذكية، بل إلى أي حد يجب أن تكون متعددة الاستخدامات؟ اعلم أنك تستطيع عمليًا كتابة أي شيء بدءًا من دالة بسيطة للغاية، حيث تحشو عددًا ليكون بطول ثلاثة محارف، إلى نظام تنسيق الأعداد المعمم والمعقد، والذي يتعامل مع الأعداد الكسرية، والأعداد السالبة، ومحاذاة الفواصل العشرية، والحشو بمحارف مختلفة، وغير ذلك. والقاعدة هنا، هي ألا تجعل الدالة تزيد في وظيفتها عن الحاجة، إلا إذا تأكدت يقينًا من حاجتك إلى تلك الوظيفة الزائدة، فقد يكون من المغري كتابة "أُطر عمل" frameworks عامة لكل جزء من الوظائف التي تصادفها، لكنا نهيب بك ألا تستجيب لهذه الرغبة، إذ لن تنجز أيّ عمل حقيقي لأيّ عميل ولا لنفسك حتى، وإنما ستكتب شيفرات لن تستخدمها أبدًا. الدوال والآثار الجانبية يمكن تقسيم الدوال إلى تلك التي تُستدعَى لآثارها الجانبية side effects، وتلك التي تُستدعَى لقيمتها المعادة -رغم أنه قد يكون للدالة آثار جانبية، وقيم معادة في الوقت نفسه-، فالدالة الأولى هي دالة مساعدة في مثال المزرعة السابق، حيث تُستدعَى printZeroPaddedWithLabel لأثرها الجانبي، فتطبع سطرًا؛ أما النسخة الثانية zeroPad، فتُستدعى لقيمتها المعادة. ولا شك أنّ الحالة الثانية مفيدة أكثر من الأولى، فتكون الدوال التي تنشئ قيمًا، أسهل في إدخالها وتشكيلها في صور جديدة عن تلك التي تنتج آثارًا جانبية مباشرة. ولدينا من ناحية أخرى، دالةً تسمى بالدالة النقية pure function، وهي نوع خاص من الدوال المنتجة للقيم، حيث لا تحتوي على آثار جانبية، كما لا تعتمد على الآثار الجانبية من شيفرة أخرى، فمثلًا، لا تقرأ هذه الدوال الرابطات العامة global bindings التي قد تتغير قيمتها. ولهذا النوع من الدوال خاصية فريدة، إذ تنتج القيمة نفسها إن استُدعيَت بالوسائط نفسها، ولا تفعل أي شيء آخر، وإضافةً إلى ما سبق، ولا يتغير معنى الشيفرة إن أزلنا استدعاء الدالة ووضعنا مكانه القيمة التي ستعيدها. وإن حدث وشككت في عمل دالة نقية، فيمكنك اختبارها ببساطة عن طريق استدعائها، واعلم أنها إذا عملت في هذا السياق، فستعمل في أي سياق آخر، إذ تحتاج الدوال غير النقية إلى دعامات أخرى لاختبارها. لكن مع هذا، فلا داعي للاستياء عند كتابة دوال غير نقية، أو تنفيذ عمليات تطهير لحذفها من شيفراتك، فقد تكون الآثار الجانبية مفيدة، وهذا يحدث في الغالب من حالات البرمجة. فمثلًا، لا توجد طريقة لكتابة نسخة نقية من console.log، ونحن نحتاج هذه الدالة أيما احتياج، كما سترى أثناء تمرسك في جافاسكربت لاحقًا، كذلك تسهل الآثار الجانبية من التعبير عن بعض العمليات بطريقة فعالة، لذا قد تكون الحاجة للسرعة سببًا لتجنب هذا النقاء في الدوال. خاتمة اطلعنا في هذا المقال على كيفية كتابة الدوال البرمجية التي تحتاجها عند تنفيذ مهمة، أو وظيفة متكررة في برامجك، وذلك باستخدام كلمة function المفتاحية التي تستطيع إنشاء قيمة دالة إذا استُخدمت على أساس تعبير؛ أما إذا استُخدمت على أساس تعليمة، فتكون للإعلان عن رابطة binding، وإعطائها دالة تكون قيمةً لها؛ كما نستطيع إنشاء الدوال أيضًا باستخدام الدوال السهمية. يُعَدّ أحد الجوانب الرئيسية في فهم الدوال هو فهم النطاقات، حيث تنشئ كل كتلة نطاقًا جديدًا، وتكون المعامِلات والرابطات المصرَّح عنها في نطاق معين محلية وغير مرئية من الخارج. كما تتصرف الرابطات المصرَّح عنها بـ var تصرفًا مختلفًا، حيث ينتهي بهم الأمر في أقرب نطاق دالي أو في النطاق العام. واعلم أنّ فصل المهام التي ينفذها برنامجك إلى دوال مختلفة يفيدك في انتفاء الحاجة إلى التكرار الزائد عن الحد، وسترى أنّ الدوال مفيدة في تنظيم البرنامج، إذ تجمع تجميع الشيفرة في أجزاء تنفذ أشياءً محددة. ترجمة -بتصرف- للفصل الثالث من كتاب Elequent Javascript لصاحبه Marijn Haverbeke.
  20. سنبدأ في هذا المقال بفعل الأشياء التي يطلَق عليها برمجة، حيث سنتوسع في أوامر جافاسكربت لنتجاوز الأسماء وأجزاء الجمل التي رأيناها حتى الآن، وذلك لنستطيع كتابة شيفرة مفيدة ذات هدف قابل للتحقيق. التعابير والتعليمات البرمجية كتبنا في المقال الأول قيمًا، وطبقنا عليها عواملًا لنحصل على قيم جديدة، حيث يُعَدّ إنشاء هذه القيم، المادة الأساسية لأي برنامج في جافاسكربت، لكن يجب وضع هذه المادة داخل هيكلٍ أكبر لتصبح مفيدة، وهذا ما سنفعله في هذا المقال. يُسمّى الجزء الذي يُنتِج قيمةً حقيقيةً في الشيفرة البرمجية، تعبيرًا expression، وتُعَدّ كل قيمة مكتوبة حرفيًّا، تعبيرًا، مثل: 22، أو psychoanalysis، كما يُعَدّ التعبير الموجود بين قوسين أيضًا تعبيرًا، كما هو الحال بالنسبة للعامل الثنائي المُطبَّق على تعبيرين، أو العامل الأحادي المُطبَّق على تعبير واحد. وهذا يُبيّن أحد أوجه الجمال في واجهة مبنية على لغة برمجية. حيث تحتوي التعبيرات على تعبيرات أخرى، وذلك بالطريقة ذاتها المتّبعة في سرد الجمل الفرعية في لغات البشر المنطوقة، إذ تحتوي الجملة الفرعية على جملة فرعية أخرى داخلها، مما يسمح لنا ببناء تعبيرات تصف الحسابات المعقدة. فإذا استجاب تعبير لجزء من جملة، فستستجيب تعليمة من جافاسكربت لجملة كاملة. وما البرنامج إلا مجموعةٌ من التعليمات الموضوعة في قائمة مرتبة! وأبسط نوع من التعليمات، هو تعبير متبوع بفاصلة منقوطة، حيث ستحصل بذلك على برنامج بسيط، لكن لا فائدة منه في الواقع العملي، كما تستطيع تعديل الشيفرة التالية وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen: 1; !false; ويُستخدَم التعبير لإنتاج قيمة لتستخدمها شيفرة البرنامج فيما بعد، أما التعليمة فهي مستقِلة بذاتها، لذا تُستخدَم إذا كانت ستؤثِّر بذاتها في الشيفرة البرمجية، كأن تعرض شيئًا على الشاشة، أو تغيّر الحالة الداخلية للآلة بطريقة تُؤثّر على التعليمات التي تليها. تُدعى هذه التغييرات آثارًا جانبية Side Effects، حيث تُنتِج التعليمات الموجودة في المثال السابق، القيمتين 1، وtrue، ثم تتخلص منهما فورًا، مما لا يحدث أيّ أثر أو تغيير، وعليه فلن تلاحظ أيّ شيء إذا شغّلت هذا البرنامج. تسمح جافاسكربت لك في بعض الحالات بإهمال الفاصلة المنقوطة الموجودة في نهاية التعليمة، ومع ذلك يجب وضعها في حالات أخرى، حيث سيُعامَل السطر التالي على أنه جزء من التعليمة نفسها، لأنّ القاعدة هنا معقدة قليلًا، ومن الوارد حدوث أخطاء بسببها، وعليه فستحتاج كل تعليمة إلى فاصلة منقوطة، كما سنكتب فاصلةً منقوطةً في كل تعليمة واردة في هذه السلسلة، وننصحك أن تحذو حذونا في هذا، على الأقل إلى حين تعلمك كيفية التعامل مع غياب هذه الفاصلة. الرابطة Binding كيف يحافظ البرنامج على حالته الداخلية، أو يتذكر أيّ شيء؟ رأينا إلى الآن كيف ننتج قيمًا جديدة من أخرى قديمة، لكن هذا لا يغيّر القيم القديمة، ويلزمنا استخدام القيم الجديدة، وإلا فستتبدد مرةً أخرى، كما توفّر جافاسكربت شيئًا اسمه المتغير variable، أو الرابطة binding، وذلك من أجل التقاط القيم والاحتفاظ بها، كما في المثال التالي: let caught = 5 * 5; وهذا هو النوع الثاني من التعليمات، إذ تشير الكلمة المفتاحية let إلى أنّ هذه الجملة ستُحدِّد رابطة، ويتبع هذه الكلمة اسم الرابطة، كما نستطيع إعطاء قيمة لها على الفور، وذلك بإضافة عامل = مع تعبير بعده. حيث تُنشِئ التعليمة السابقة رابطةً، اسمها: caught، وتستخدمها لتمسك بالعدد الناتج عن ضرب 5 بـ 5، كما يُستخدَم اسم الرابطة بعد تحديدها على أساس تعبير، وقيمته هي قيمة الرابطة التي يحملها حاليًا، كما في المثال التالي: let ten = 10; console.log(ten * ten); // → 100 وحين تشير رابطة ما إلى قيمة، فهذا لا يعني أنها مربوطة بتلك القيمة إلى الأبد، إذ يمكننا استخدام عامل = في أي وقت على أيّ رابطة موجودة، وذلك لفصلها عن قيمتها الحالية وربطها بقيمة جديدة. انظر إلى ما يلي: let mood = "light"; console.log(mood); // → light mood = "dark"; console.log(mood); // → dark وإذا أردت تخيُّل الرابطات هذه، فلا تفكر فيها على أنها صناديق مثلًا، فهي أقرب لمجسّات الأخطبوط، إذ لا تحتوي على قيم وإنما تلتقطها فقط، مما يعني إمكانية إشارة رابطتين إلى القيمة ذاتها، بحيث لا يستطيع البرنامج أن يصل إلى قيم ليس لديه مرجع إليها، فإذا أردت تذكّر شيء، فستَمدّ أحد المجسّات الجديدة لإمساكه، أو ستوجّه أحد المجسات الموجودة من قبل إليه. دعنا نأخذ مثالًا آخرًا، لنقل أننا نريد تذكّر عدد الدولارات التي ما زال حسن يدين بها لك، لذلك سننشِئ رابطًا لهذا، ثم حين يسدد حسن جزءًا منها، وليكن 35$ مثلًا، فإننا سنعطي هذه الرابطة قيمةً جديدة، أي كما يأتي: let HasanDebt = 140; HasanDebt = HasanDebt - 35; console.log(HasanDebt); // → 105 لا يستطيع المجس التقاط شيء إن لم تعط قيمةً للرابطة التي عرّفتها، وإذا طلبت قيمة رابطة فارغة، فستحصل على قيمة غير معرّفة undefined. كذلك يمكن لتعليمة let واحدة تعريف عدة رابطات، لكن يجب الفصل بين الرابطات بفاصلة أجنبية ,، أي كما يلي: let one = 1, two = 2; console.log(one + two); // → 3 كذلك تُستخدَم الكلمتان var، وconst، لإنشاء رابطات بطريقة قريبة من let، أي كما في المثال التالي: var name = "Ayda"; const greeting = "Hello "; console.log(greeting + name); // → Hello Ayda وكلمة var الأولى والمختَصرة من كلمة متغير variable بالإنجليزية، هي المُصرَّح بها عن الرابطات في جافاسكربت قبل 2015، وسنعود إلى الاختلاف بينها وبين let في المقال التالي، لكن نريدك أن تتذكر الآن أنها تفعل الشيء نفسه تقريبًا، رغم أننا لن نستخدمها في هذه السلسلة إلا نادرًا، وذلك بسبب خصائصها التي قد تربكك أثناء العمل. أما كلمة const، فهي مختصرة من كلمة ثابت الإنجليزية Constant، حيث تعرِّف رابطةً ثابتةً تشير إلى القيمة ذاتها دائمًا، وهذا مستَخدم في الرابطات التي تُسنِد اسمًا إلى قيمة، وذلك لتستطيع الإشارة إليها فيما بعد بسهولة. أسماء الرابطة يمكن تسمية الرابطة بأي كلمة كانت، ويجوز إدخال الأرقام في أسماء الرابطات مثلcatch22، لكن شريطة عدم بدء الاسم برقم. يمكن كذلك إدخال علامة الدولار $ والشرطة السفلية _ فيه، ولا يُسمح بأي علامة ترقيم، أو محرف خاص آخر. كما لا تُستَخدم الكلمات التي لها معنى خاص مثل let، على أساس اسم لرابطة بما أنها كلمات مفتاحية ومحجوزة، إضافةً إلى مجموعة من الكلمات المحجوزة للاستخدام في الإصدارات التالية من جافاسكربت، والقائمة الكاملة لها طويلة نوعًا ما، وهي: break case catch class const continue debugger default delete do else enum export extends false finally for function if implements import interface in instanceof let new package private protected public return static super switch this throw true try typeof var void while with yield ولا تكلِّف نفسك عناء حفظ تلك الكلمات، فإذا حدث وأنشأت رابطةً، ثم ظهر لك خطأ بناء الجملة syntax error غير متوقّع، فانظر هل كنت تعرِّف كلمةً محجوزةً أم لا. البيئة يُطلَق اسم البيئة environment، على مجموعة الرابطات وقيمها التي توجد في وقت ما، حيث تكون تلك البيئة غير فارغة عندما يبدأ برنامج ما، إذ تحتوي على رابطات تكون جزءًا من معيار اللغة، ورابطات توفر طُرقًا للتفاعل مع النظام المحيط باللغة، فمثلًا، إذا كنت تقرأ الآن من متصفِّح، فلديك دوالًا تتفاعل مع الموقع المُحمَّل حاليًّا، وتقرأ مدخلات الفأرة ولوحة المفاتيح. الدوال تملك العديد من القيم الموجودة في البيئة الافتراضية، نوع دالة function، والدالة هي قطعة من برنامج ملفوفة بقيمة ما، بحيث قد تُطبَّق تلك القيمة لتشغيل البرنامج الذي تلتف حوله، ففي بيئة المتصفِّح مثلًا، تحمل الرابطة المسمّاة prompt، دالةً تُظهِر صندوقًا حواريًا صغيرًا يطلب إدخالًا من المستخدِم، ويُستخدَم هكذا: prompt("Enter passcode"); وناتج تنفيذ الشيفرة السابقة (سواءً في codepen أو في طرفية متصفحك)، هو مربع حوار يشبه ما تعرضه الصورة التالية: يُسمّى تنفيذ دالة ما، استدعاءً، أو طلبًا، أو نداءً، أو تطبيقًا لها، كما تستطيع استدعاء دالة بوضع قوسين بعد تعبير ينتج قيمةَ دالة، وفي الغالب، ستجد نفسك تستخدم اسم الرابطة الذي يحمل الدالة. تُعطى القيم التي بين أقواس إلى البرنامج داخل الدالة، وفي المثال السابق، تستخدم دالة prompt السلسلة النصية التي نعطيها إياها، على أساس نص تظهره في الصندوق الحِواري. وتُسمّى القيم المعطاة للدوال، وُسَطاء arguments، فقد تحتاج الدوال المختلفة، عددًا مختلفًا، أو أنواعًا مختلفةً من الوسائط. ولا تُستخدَم دالة prompt كثيرًا في برمجة الويب الحديثة، وذلك بسبب السيطرة على الطريقة التي سيبدو بها الصندوق الحِواري، رغم أنها مفيدة في برامج الألعاب والتجارب. دالة console.log استخدمنا دالة console.log لإخراج القيم في الأمثلة السابقة، حيث تُوفِّر أغلب أنظمة جافاسكربت بما فيها المتصفحات الحديثة كلها، وNode.js، دالة console.log، حيث تكتب هذه الدالة وُسطاءها لتعرضها على أداة عرض نصية، ويكون الخرج في المتصفحات، في طرفية جافاسكربت، وهو جزء مخفي في المتصفح افتراضيًّا، حيث تستطيع الوصول إليه في أغلب المتصفِّحات إذا ضغطت على F12 في ويندوز، أو command-option-l في ماك، وإن لم تستطع الوصول إليه، فابحث في قوائم المتصفِّح عن عنصر اسمه أدوات المطوِر Developer Tools، أو ما يشابهها. وسيظهر خرج console.log عند تشغيل أمثلة هذه السلسلة في صفحاته، أو عند تشغيل شيفرات خاصة بك، وسيظهر بعد المثال بدلًا من طرفية جافاسكربت في المتصفح. let x = 30; console.log("the value of x is", x); // → the value of x is 30 ورغم عدم احتواء أسماء الرابطات على محرف النقطة، إلا أنّ console.log بها واحدة، وذلك لأنها ليست اسم رابطة بسيطة، بل تعبير يجلب السجل log من القيمة التي تحتفظ بها اسم الرابطة console، وستعرف معنى ذلك بالضبط في المقال الرابع. القيم المعادة يُعَدّ إظهار صناديق حوارية أو كتابة نصوص على الشاشة من الآثار الجانبية، والكثير من الدوال مفيدة وعملية بسبب تلك الآثار الجانبية التي تنتجها، كما تنتج الدوال قيمًا أيضًا، وعندها لا تحتاج أن يكون لها أثر جانبي، فهي نافعة بحد ذاتها بإنتاجها للقيم. على سبيل المثال، تأخذ دالة Math.max أي عدد من الوسائط العددية، وتعيد أكبرها، أي كما يأتي: console.log(Math.max(2, 4)); // → 4 يقال للدالة التي تعيد قيمةً ما، أنها تعيد تلك القيمة، والدالة بحد ذاتها تعبير في جافاسكربت، إذ يُعَد أيّ شيء ينتج قيمةً في جافاسكربت، تعبيرًا، وهكذا يمكن استخدام استدعاءات الدوال في تعبيرات أكبر كما في المثال التالي، لاستدعاء Math.min الذي يعطي عكس نتيجة Math.max، إذ نستخدمه على أساس جزء من تعبير إضافة: console.log(Math.min(2, 4) + 100); // → 102 سنشرح في هذا المقال كيف نكتب الدوال الخاصة بنا. تدفق التحكم حين يحتوي برنامجك على أكثر من تعليمة واحدة، فستُنفَّذ التعليمات على أساس قصة من الأعلى إلى الأسفل، والمثال التالي فيه تعليمتان، حيث تسأل الأولى المستخدِم عن عدد، وتُنفَّذ الثانية بعدها لتُظهِر مربع ذلك العدد. let theNumber = Number(prompt("اختر عددًا")); console.log("عددك هو الجذر التربيعي لـ" + theNumber * theNumber); حيث تحوِل دالة Number القيمة إلى عدد، ونحتاج هذا التحويل لأننا نريد عددًا ونتيجة prompt سلسلة نصية، كما توجد دوال مشابهة، اسمها: string، وBoolean، لتحويل القيم إلى تلك الأنواع أيضًا. انظر المخطط التالي الذي يوضح تدفُق التحكم لخط مستقيم: تنفيذ شرطي ليست كل البرامج طرقًا مستقيمة، فقد نود إنشاء طريق فرعية مثلًا، حيث يأخذ البرنامج الفرع المناسب وفقًا للموقف الذي بين يديه، ويُسمى هذا بالتنفيذ الشرطي. ينشأ التنفيذ الشرطي بكلمة if المفتاحية في جافاسكربت، وفي الحالة البسيطة من هذا التنفيذ الشرطي، فإننا نريد تنفيذ شيفرة عند تحقق شرط ما، فقد نود مثلًا إظهار مربع الدخل إذا كان الدخل عددًا، أي كما يأتي: let theNumber = Number(prompt("اختر عددًا")); if (!Number.isNaN(theNumber)) { console.log("عددك هو الجذر التربيعي للعدد " + theNumber * theNumber); } وبتعديل الدخل ليكون كلمةً مثل "ببغاء"، فلن تحصل على أيّ خرج. حيث تُنفِّذ كلمة if المفتاحية تعليمةً ما أو تتخطاها وفقًا لقيمة التعبير البولياني، إذ يُكتب التعبير المقرِّر بعد الكلمة المفتاحية بين قوسين ويُتبع بالتعليمة المطلوب تنفيذها. وتُعَّد دالة Number.isNaN دالةً قياسية في جافاسكربت، حيث تُعيد true إذا كان الوسيط المعطى لها ليس عددًا NaN، كما تُعيد دالة Number القيمة NaN إذا أعطيتها نصًا لا يُمثّل عددًا صالحًا، وعليه يُترجم الشرط إلى "إن كانت القيمة theNumber المدخلة عددًا، افعل هذا". حيث تُغلَّف التعليمة التي تلي if بين قوسين معقوصين، هما: {}، كما في المثال السابق، ويمكن استخدام الأقواس لجمع أيّ عدد من التعليمات في تعليمة واحدة، وتُسمّى تلك المجموعة بالكتلة Block، والتي تستطيع إهمالها جميعًا في ذلك المثال بما أنها تحمل تعليمةً واحدةً فقط، لكن يستخدمهما أغلب مبرمجي جافاسكربت في كلّ تعليمة مغلفة مثل هذه، وسنتّبع هذا الأسلوب في السلسلة غالبًا، إلا في حالات شاذة عندما تكون من سطر واحد، أي كما في المثال الآتي: if (1 + 1 == 2) console.log("صحيح"); // → صحيح سيكون لديك في الغالب شيفرة تنتظر التنفيذ عند تحقق الشرط، وكذلك شيفرة لمعالجة الحالة الأخرى عند عدم تحققه، حيث يُمثَّل هذا المسار البديل، بالسهم الثاني في الرسم التوضيحي السابق، إذ تستطيع استخدام كلمة else المفتاحية مع if لإنشاء مساري تنفيذ منفصلين، وكل منهما بديل عن الآخر. let theNumber = Number(prompt("اختر عددًا")); if (!Number.isNaN(theNumber)) { console.log("عددك هو الجذر التربيعي للعدد " + theNumber * theNumber); } else { console.log("لمَ لم تعطني عددًا؟"); } أما إن كان لديك أكثر من مسارين لتختار منهما، فيمكن استخدام سلسلة أزواج من if/else معًا، أي كما في المثال الآتي: let num = Number(prompt("اختر عددًا")); if (num < 10) { console.log("صغير"); } else if (num < 100) { console.log("وسط"); } else { console.log("كبير"); } سينظر البرنامج أولًا إن كان num أصغر من 10، فسيختار هذا الفرع، ويظهر لك "صغير" وانتهى الأمر؛ أما إن كان أكبر من 10، فسيأخذ مسار else الذي يحتوي على if أخرى أيضًا. فإن تحقق الشرط الثاني (‎< 100)، فهذا يعني أنّ العدد بين 10 و 100، وسيظهر لك "وسط"؛ أما إن لم يكن كذلك، فسيختار مسار else الثاني والأخير. ويوضَّح مسار هذا البرنامج بالمخطط التالي: حلقات while وdo لنقُل أنه لدينا برنامجًا يخرج كل الأرقام الزوجية من 0 حتى 12، حيث يُكتب هذا البرنامج كما يأتي: console.log(0); console.log(2); console.log(4); console.log(6); console.log(8); console.log(10); console.log(12); لكن الفكرة من كتابة البرنامج، هو ألا نجهد أنفسنا في العمل، إذ لن يصلح هذا المنظور قطعًا إذا أردنا كل الأرقام الزوجية الأصغر من ألف. وبالتالي، سنحتاج إلى طريقة لتشغيل جزء من شيفرة عدة مرات، ويُدعى تدفُق التحكم هذا، حلقة التكرار loop. يسمح لنا تدفق التحكم المتكرر بالعودة إلى نقطة ما في البرنامج كنا فيها من قبل، ثم تكرار حالة البرنامج الحالية، فإن جمعنا ذلك إلى اسم رابطة مفيد، وبذلك يمكننا تنفيذ ما يلي: let number = 0; while (number <= 12) { console.log(number); number = number + 2; } // → 0 // → 2 // … etcetera حيث تنشئ الكلمة المفتاحية while حلقةً تكرارية، ويتبع while تعبيرًا داخل أقواس، ثم تعبير (البنية عمومًا تشبه الشرط if)، وتستمر الحلقة في التكرار طالما أنّ خرج التعبير عند تُحويله إلى النوع البولياني، هو true. كما توضح رابطة number الطريقة التي يمكن لرابطة ما تتبُّع سير البرنامج، حيث يحصل number على قيمة أكثر من القيمة السابقة بمقدار 2 عند كل عملية تكرار، كما يُوازَن برقم 12 في بداية الحلقة ليقرر ما إذا كان عمل البرنامج قد انتهى أم لا. كما يمكننا كتابة برنامج يحسب قيمة 210 ( مرفوعة للأس العاشر) كمثال عن برنامج يُنفِّذ أمرًا نافعًا حقًا، وسنستخدم رابطتين لكتابة هذا البرنامج، الأولى لتتبّع سير النتيجة، والثانية لحساب عدد مرات ضرب النتيجة في 2. وتختبر حلقة التكرار وصول الرابطة الثانية إلى 10، حيث تحدِّث كلا الرابطتين طالما أن الرابطة الثانية أصغر من 10. let result = 1; let counter = 0; while (counter < 10) { result = result * 2; counter = counter + 1; } console.log(result); // → 1024 ويمكن أن يبدأ العدّاد (الرابطة الثانية) من 1، ويتحقق من شرط ‎<= 10، لكن الأفضل أن تعتاد على بدء العد من الصفر، وذلك لأسباب مذكورة في المقال الرابع. أما حلقة do فهي بنية تحكّم مماثلة لـ while، ولكن تختلف في نقطة واحدة فقط، حيث تُنفِّذ متنها مرةً واحدةً على الأقل، إذ تختبر شرط التوقف بعد أول تنفيذ، ويظهر ذلك الاختبار بعد متن الحلقة. كما في المثال التالي: let yourName; do { yourName = prompt("Who are you?"); } while (!yourName); console.log(yourName); حيث يجبرك البرنامج السابق على إدخال اسم ما، وسيسألك مرةً بعد مرة إلى أن تدخل نصًّا غير فارغ، كما سيحول عامل النفي ! القيمة إلى قيمة بوليانية قبل نفيها، وستُحوَّل جميع النصوص إلى true ما عدا " "، مما يعني استمرار الحلقة بالتكرار إلى أن تُدخل اسمًا غير فارغ. الشيفرة المزاحة أضفنا في الأمثلة السابقة، مسافات قبل التعليمات التي تكون جزءًا من تعليمات أكبر، وذلك للإشارة إلى تبعيتها لما قبلها من تلك التعليمات، وهذه المسافات ليست ضرورية، حيث سيقبل الحاسوب البرنامج بدون هذه المسافات، كما يُعَدّ الفاصل السطري اختياريًّا، فلو كتبت برنامجًا من سطر واحد طويل جدًا، فسيقبله الحاسوب منك. أمّا دور تلك الإزاحات داخل الكتل، فهو جعل بنية الشيفرة واضحةً وبارزة، فإذا كان لدينا شيفرة كبيرة وتداخلت الكتل ببعضها البعض، فمن الصعب رؤية بداية ونهاية كل كتلة، حيث تجعل الإزاحة المناسبة الشكل المرئي للبرنامج متوافقًا مع شكل الكتل بداخله، ونفضّل استخدام مسافتين لكل كتلة مفتوحة، بينما يفضّل البعض وضع أربع مسافات، في حين يستخدم البعض الآخر زرّ tab، ولكن المهم ليس عدد المسافات، وإنما إضافة العدد ذاته لكل كتلة. if (false != true) { console.log("That makes sense."); if (1 < 2) { console.log("No surprise there."); } } وستساعدك أغلب محررات الشيفرة في إزاحة السطور تلقائيًّا، وفقًا لما تكتبه، وبالمقدار المناسب. حلقات for تتبع أغلب الحلقات، النمط الموضح في أمثلة while، إذ تُنشأ رابطة عدّ (عدّاد) في البداية لتتبّع سير الحلقة، ثم تأتي حلقة while، وعادةً مع تعبير اختباري ليتحقق من وصول العدّاد إلى قيمته النهائية، وتُحدَّث قيمة العداد في نهاية متن الحلقة. ولأنّ هذا النمط شائع جدًا، فقد وفّرت جافاسكربت نموذجًا أقصر قليلًا، وأكثر تفصيلًا، ومتمثّلًا في حلقة for. for (let number = 0; number <= 12; number = number + 2) { console.log(number); } // → 0 // → 2 // … etcetera يطابق هذا البرنامج، المثال السابق لطباعة الأرقام الزوجية، لكن مع فرق أنّ كل التعليمات المتعلِّقة بحالة الحلقة، مجموعة معًا بعد for. حيث يجب احتواء الأقواس الموجودة بعد for على فاصلتين منقوطتين، ليُهيِِّئ الجزء الذي يسبق الفاصلة المنقوطة الأولى، الحلقة من خلال تعريف رابطة لها، كما يتحقق الجزء الثاني الذي يكون تعبيرًا، من استمرارية الحلقة، ويُحدِّث الجزء الاخير حالة الحلقة بعد كل تكرار. فنجد في معظم الحالات، هذه البنية أقصر وأوضح من بنية while. وتحسب الشيفرة التالية قيمة 210، باستخدام for بدلًا عن while: let result = 1; for (let counter = 0; counter < 10; counter = counter + 1) { result = result * 2; } console.log(result); // → 1024 الهروب من حلقة ستنتهي الحلقة من تلقاء نفسها إذا كان خرج التعبير الشرطي false، ولكنه ليس الطريق الوحيد لإنهاءها، حيث توجد تعليمة خاصة لها أثر القفز نفسه إلى خارج الحلقة، وتُسمّىbreak، ويتوضَّح عملها من خلال البرنامج التالي، حيث يجد أول عدد أكبر أو يساوي 20، ويقبل القسمة على 7 في الوقت نفسه. for (let current = 20; ; current = current + 1) { if (current % 7 == 0) { console.log(current); break; } } // → 21 واستخدام عامل الباقي % هنا يعد طريقة سهلة للتحقق إن كان رقم ما يقبل القسمة على رقم آخر أم لا، فإن كان فإن باقي قسمتهما يكون صفرًا، وليس هناك جزء يتحقق من نهاية حلقة for التي في هذا المثال، هذا يعني أن حلقة for لن تقف إلا حين تُنفذ تعليمة break، فإن حذفت تعليمة break تلك أو كتبت شرط نهاية ينتج true دائمًا فسيقع برنامجك في حلقة لا نهائية ولن يقف عن العمل، وهذا لا نريده. فإذا أنشأت حلقة لا نهائية في أحد الأمثلة السابقة أو التالية فستُسأل بعد بضع ثواني عما إن كنت تريد إيقاف الشيفرة، فإن فشل ذلك وكنت في المتصفح فيجب أن تغلق اللسان أو النافذة ثم تفتحها مرة أخرى، بل بعض المتصفحات تغلق نفسها بالكامل لتخرج من هذا الأمر. وبالمثل فإن كلمة continue المفتاحية تشبه break في كونها تؤثر على مجرى الحلقة، فإن وجدت الحلقة كلمة continue في متنها فإن التحكم يقفز من المتن لينتقل إلى التكرار التالي للحلقة. تحديث الرابطة بإيجاز يحتاج البرنامج وخاصةً في الحلقات التكرارية، إلى تحديث رابطة ما، وذلك ليحفظ قيمة بناءً على القيمة السابقة لتلك الرابطة. counter = counter + 1; توفّر جافاسكربت اختصارًا لهذا، ويُكتب كما يأتي: counter += 1; وبالمثل، فهناك اختصارات لعوامل أخرى، مثل: result *= 2 الذي يضاعف result، أو counter -= 1 الذي يَعُدّ تنازليًا. ويسمح هذا باختصار مثال العدّ السابق أكثر، بحيث يصبح كما يأتي: for (let number = 0; number <= 12; number += 2) { console.log(number); } أمّا بالنسبة لـ counter += 1، وcounter -= 1، فلدينا نسخ أكثر إيجازًا منهما، وهما: counter++‎ وcounter--‎. الإرسال إلى قيمة باستخدام التعليمة switch من المتوقع استخدام الشيفرة التالية: if (x == "value1") action1(); else if (x == "value2") action2(); else if (x == "value3") action3(); else defaultAction(); تُسمى هذه البنية باسم التبديل الشرطي switch، وصُمّمت للتعبير عن هذا الإرسال بطريقة مباشرة -وهي مكتسبة من أسلوب جافا وC-، لكن صياغة جافاسكربت تستخدمها بغرابة، ومن الأفضل استخدامها بدلًا من سلسلة متتالية من تعليمات if، أي كما في المثال التالي: switch (prompt("كيف حال الجو؟")) { case "ممطر": console.log("لا تنس إحضار شمسية"); break; case "مشمس": console.log("البس ثيابًا خفيفة"); case "غائم": console.log("اخرج لتتمشى"); break; default: console.log("هذا جو غير معروف!"); break; } حيث تستطيع وضع عدد لانهائي من الحالات case داخل الكتلة البادئة بـ switch، وسيبدأ البرنامج بالتنفيذ عند العنوان المطابق للقيمة المعطاة في switch، أو عند default في حال عدم وجود قيمة مطابقة، حيث سيكمل التنفيذ مرورًا على العناوين الأخرى إلى حين وصوله لتعليمة break. ولكن قد تجد حالةً مثل حالة "مشمس" في المثال السابق، إذ يمكن استخدامها لمشاركة جزء من الشيفرة بين الحالات -وهي اقتراح الخروج في الجو المشمس والغائم معًا-، ومن السهل نسيان كتابة تعليمة break، مما يؤدي إلى تنفيذ شيفرة لا تريدها. الحالة الكبيرة للأحرف لا تحتوي أسماء الرابطة على مسافات، ولكن من المفيد كتابة بضعة كلمات لوصف ما تمثله الرابطة بوضوح، والخيارات المتاحة لك في جافاسكربت هي غالبًا ما يلي: fuzzylittleturtle fuzzy_little_turtle FuzzyLittleTurtle fuzzyLittleTurtle فالأسلوب الأول من الصعب قراءته، ونفضِّل أسلوب الشرطة السفلية عنه، إذ تتّبع أغلب دوال جافاسكربت القياسية الأسلوب الأخير، وهو كتابة الحرف الأول من كل كلمة كبيرًا، عدا الحرف الأول من الكلمة الأولى، ويفعل كذلك مبرمجو اللغة ممن يستخدمونها أيضًا، وسنتّبع هذا الأسلوب في هذه السلسلة أيضًا، إذ يجعل خلط الأساليب قراءة الشيفرة محيّرًا، لكن في حالات قليلة مثل دالة Number، فالحرف الأول من اسم الرابطة كبير، وذلك لتحديدها بأنها دالة بانية constructor، كما سنتعرّض لهذا بالتفصيل في المقال السادس، لكن اعلم الآن أن المهم هو ألا تشغل نفسك بعدم التناغم هذا. التعليقات يُعَدّ التعليق جزءًا من البرنامج رغم تجاهل الحاسوب له تمامًا عند التنفيذ، إذ نستخدمه لاحتمال عدم فهم المبرمجين أو غيرهم، للرسالة أوالمَهمة التي سينفذها البرنامج عند قراءتهم للشيفرة، وقد تحتاج أنت نفسك أحيانًا إلى تسجيل بعض الملاحظات لتكون جزءًا من برنامجك، حيث يمكنك العودة إليه فيما بعد، أو لمعرفة سبب اختيارك لهذه الدالة أو تلك. وتملك جافاسكربت أسلوبين لكتابة التعليقات، إذ يمكنك كتابة تعليق من سطر واحد باستخدام اثنين من محارف الشرطة المائلة الأمامية //، ثم التعليق بعدها، أي كما في المثال التالي: let accountBalance = calculateBalance(account); // خذ من أخيك العفو واغفر ذنوبه ولا تك في كل الأمور تعاتبه accountBalance.adjust(); // فإنك لن تلقى أخاك مهذبًا وأي امرئ ينجو من العيب صاحبه let report = new Report(); // أخوك الذي لا ينقضُ النأيُ عهدَه ولا عند صرف الدهر يزوَرُّ جانبه addToReport(accountBalance, report); // وليس الذي يلقاك بالبشر والرضا وإن غبت عنه لسعتك عقاربه ويستمر التعليق البادئ بالشرطتين // إلى نهاية السطر، كما يُتجاهل جزء النص الواقع بين /*‎ و*/‎ بكامله، بغضّ النظر عن وقوعه في سطر واحد، أو في عدة أسطر، وهذا مفيد لإضافة كتل من المعلومات المتعلقة بملف أو بجزء من البرنامج. أي كما في المثال التالي: /* لقد وجدت هذا العدد يزحف إلى ظهر دفتري قبل مدة، ومن يومها وهو يظهر لي بين الحين والآخر. فمرةً في منتج أشتريه، ومرةً في جهات اتصالي. يبدو أنه صار ملازمًا لي! */ const myNumber = 11213; خاتمة اطلعنا على كيفية بناء البرنامج من تعليمات قد تحتوي هي نفسها على تعليمات أخرى، وقد تحتوي على تعبيرات ربما تتكون من تعبيرات أخرى. ويعطيك وضع التعليمات بالتتالي، برنامجًا يُنفَّذ من الأعلى إلى الأسفل، كما تستطيع إدخال مغيرات لتغيير تدفق التحكم هذا باستخدام التعليمات الشرطية، مثل: if، وelse، وswitch؛ وتعليمات الحلقات التكرارية، مثل: while، وdo، وfor. كما تستطيع استخدام الرابطة لأجزاء البيانات في الملف تحت اسم معين، وتستفيد من هذا في تتبع حالة سير برنامجك، وكذلك علمت أن البيئة هي مجموعة من الرابطات المعرَّفة، وتضع أنظمة جافاسكربت عددًا من الرابطات القياسية في بيئتك دائمًا. تعرّفنا أيضًا على الدوال التي هي عبارة عن قيم خاصة لتغليف جزء من برنامج ما، وتُستدعى بكتابة اسمها والوسائط التي تحملها، كما يُعَدّ استدعاؤها تعبيرًا، وقد ينتج قيمة. ترجمة -بتصرف- للفصل الثاني من كتاب Elequent Javascript لصاحبه Marijn Haverbeke.
  21. لا يُنظر في عالم الحاسوب إلى شيء سوى إلى البيانات، حيث يمكنك أن تقرأ البيانات، وتُعدّلها، وتُنشِئ الجديد منها، بينما يُغفَل ذكر ما سواها، وهي متشابهة في جوهرها، إذ أنها تُخزَّن في سلاسل طويلة من البِتَّات Bits. ويُعبَّر عن البِتَّات بأي زوج من القيم، وتُكتَب عادةً بصورة الصفر والواحد، وتأخذ داخل الحاسوب أشكالًا أخرى، مثل: شحنة كهربائية عالية أو منخفضة، أو إشارة قوية أو ضعيفة، أو ربما نقطة لامعة أو باهتة على سطح قرص مدمج CD. لذلك توصَّف أيّ معلومة فريدة، في سلسلة من الأصفار والواحدات، ومن ثم تُمثَّل في بِتَّات. فمثلًا: نمثِّل العدد 13 بالبِتَّات، بالطريقة المعتمَدة في النظام العشري، إلا أنه يوجد لكل بِتّ، قيمتان فقط بدلًا من عشر قيم مختلفة، بحيث يزداد وزن كل بِتّ، ابتداءًا من اليمين إلى اليسار بمعامل العدد 2. ونحصل على البِتَّات المقابلة للعدد 13 مع بيان وزن كل بِتّ أسفل منها، كما يأتي: | 1 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | وبذلك، مُثِّل العدد 13 بالعدد الثنائي 00001101، والذي نحصل عليه بجمع أوزان البِتّات الغير صفرية، أي بجمع: 8، و4، و1. القيم تخيّل أنّ ذاكرة الحاسوب بحر تتكوّن ذراته من بِتَّات صغيرة جدًا بدلًا من ذرّات الماء، حتى أنها أشبه بمحيط كبير يتكون من 30 مليار بِتّ في ذاكرته المتطايرة volatile فقط، وأكثر من هذا بكثير في ذاكرته الدائمة (الأقراص الصلبة وما شابهها). ولكي نستطيع العمل مع هذه الأعداد الهائلة من البِتّات دون التيه في متاهة منها، فإننا نقسّمها إلى قطع كبيرة تُمثِّل أجزاءً من معلومات، وتسمّى تلك القطع في لغة جافاسكربت، بالقيم Values، ورغم أن تلك القيم تتكوّن من بِتّات، إلا أنها تختلف في وظائفها اختلافًا كبيرًا، فكل قيمة لها نوع يُحدِّد وظيفتها ودورها، فقد تكون بعض تلك القيم أعدادًا، كما قد تكون نصًّا، أو دوالًا، وهكذا. وما عليك إلا استدعاء اسم القيمة لإنشائها، دون الحاجة إلى جمع مواد لها أو شرائها، بل بمجرّد استدعاء القيمة يجعلها بين يديك. وتُخزَّن تلك القيم في الذاكرة طبعًا، إذ يجب أن تكون في مكان ما، وقد تَنفد ذاكرة الحاسوب إذا استدعيت قيمًا كثيرة، وتحدث هذه المشكلة إذا احتجت جميع القيم التي لديك دفعةً واحدة. وتتبدّد القيم، بمجرّد انتهائك من استخدامها، تاركةً خلفها البِتّات التي شغلَتها، وذلك من أجل الأجيال المستقبلية من القيم. وسنشرح في هذا الفصل، العناصر الصغيرة جدًا في برامج جافاسكربت، وهي: أنواع القيم البسيطة، والعوامل Operators التي تعمل وفقًا لهذه القيم. الأعداد تُكتب القيم من النوع العددي في جافاسكربت على هيئة أعداد، أي على النحو التالي: 13 وإذا استخدمت القيمة أعلاه في برنامج ما، فسيُنشِئ النمط البِتّي لها في ذاكرة الحاسوب، وتستخدم جافاسكربت، لتخزين قيمة عددية واحدة، عددًا ثابتًا من البِتّات، ومقداره تحديدًا 64 بِتّ، مما يجعل عدد الأنماط التي يمكن إنشاؤها بأربع وستين بِتِّ، محدودًا نوعًا ما، وعدد الأعداد المختلفة التي يمكن تمثيلها، محدودًا أيضًا، إذ يمكنك تمثيل 10N عدد باستخدام رقم N من الأرقام العشرية، كما يمكنك تمثيل 264 عدد مختلِف باستخدام 64 رقم ثنائي أيضًا، ويُترجم هذا إلى نحو 18 كوينتليون (18 أمامها 18 صفرًا) عدد. ويُعَدّ حجم ذواكر الحاسوب قديمًا، موازنَةً بالأحجام والسعات الحالية، صغيرًا للغاية، حيث استخدم الناس، لتمثيل الأعداد، مجموعات من 8-بِتّ، أو 16-بِتّ، فكان من السهل جدًا تجاوز الحد المسموح به للأعداد التي يمكن تمثيلها فيها، لينتهي الأمر بعدد لا يمكن تمثيله ضمن العدد المعطى من البِتّات. أما الآن، فتملك الحواسيب التي تضعها في جيبك، ذواكرًا كبيرةً، مما يتيح لك استخدام مجموعات من 64-بت، ولا تقلق بشأن تجاوز ذلك الحد إلا إذا تعاملت مع أعداد كبيرة جدًا. ولكن رغم ما سبق، لا يمكن تمثيل كل الأعداد الأقل من 18 كوينتليون بعدد في جافاسكربت، حيث تُخزِّن البِتّات أيضًا أعدادًا سالبة، مما يعني أنه سيُخصَّص أحد البِتّات لتمثيل إشارة العدد، والمشكلة الأكبر هنا أنه يجب أيضًا تمثيل الأعداد الكسرية، وبالتالي سيُخصَّص بِتّ آخر لموضع الفاصلة العشرية، وذلك سيقلل عدد الأعداد التي يمكن تمثيلها بجافاسكربت، إلى 9 كوادريليون عدد (15 صفرًا هذه المرة)، وتُكتب الأعداد الكسرية fractional numbers على الصورة التالية: 9.81 وتستطيع استخدام الترميز العلمي لكتابة الأعداد الكبيرة جدًا أو الصغيرة جدًا، وذلك بإضافة e (للإشارة إلى وجود أس)، متبوعةً بأس العدد، حيث يُكتَب العدد 2.998 × 108 والمساوي لـ 299,800,000، بالشكل الآتي: 2.998e8 وتُضمَن الدقة في العمليات الحسابية المستَخدَمة مع الأعداد الصحيحة integers والأصغر من العدد المذكور آنفًا -9 كوادريليون-، على عكس العمليات الحسابية المستخدَمة مع الأعداد الكسرية، فكما أنه لا يمكن تمثيل الثابت باي π بعدد محدود من الأرقام العشرية decimal digits بعد الفاصلة، فإن كثيرًا من الأعداد تفقد دقتها عند تمثيلها بأربع وستين بِتّ فقط. ولكن لا يُمثِّل هذا مشكلةً إلا في حالات محددة. ومن المهم أن تكون على دراية بهذا، وتُعامِل الأعداد الكسرية معاملةً تقريبية، وليس على أنها قيم دقيقة. العمليات الحسابية العملية الحسابية هي العملية الأساسية المستَخدَمة مع الأعداد، وتأخذ المعاملات الحسابية arithmetic operations عددان، وتُنتِج عددًا جديدًا، مثل: الجمع، والضرب؛ وتُمثَّل هذه المعاملات في جافاسكربت، كما يأتي: 100 + 4 * 11 وتُسمّى الرموز + و* بالعوامل operators، فالعامل الأول هو عامل الجمع، والعامل الثاني هو عامل الضرب، ولدينا - للطرح، و / للقسمة، وبمجرّد وضع العامل بين قيمتين، تُنفَّذ العملية الحسابية وتَنتُج قيمة جديدة، ولكن هل يعني المثال السابق أن نضيف 4 إلى 100 ونضرب النتيجة في 11، أم أن الأولوية للضرب أولًا؟ لعلّك خمّنت أن الضرب أولًا، وهذا صحيح، لكن يمكنك تغيير هذا الترتيب مثل الرياضيات، وذلك بوضع عملية الجمع بين قوسين، مما يرفع من أولوية تنفيذها، كما يأتي: (100 + 4) * 11 ويُحدَّد ترتيب تنفيذ العوامل، عند ظهورها بدون أقواس، بأولوية تلك العوامل، فكما أن الضرب في المثال السابق له أولوية على الجمع، فللقسمة أولوية الضرب ذاتها، في حين يملك الجمع والطرح أولوية بعضهما، وتُنفَّذ العوامل بالترتيب من اليسار إلى اليمين، إذا كانت في رتبة التنفيذ نفسها، فمثلًا: إذا جاء الجمع والطرح معًا، فستكون الأولوية في التنفيذ لمن يبدأ من اليسار أولًا، أي كما في المثال التالي: 1 - 2 + 1 حيث تُنفَّذ العمليات من اليسار إلى اليمين، مثل عملية الطرح بين أقواس، أي هكذا: (1 - 2) + 1 ولا تقلق كثيرًا بشأن هذه الأولويات، فلو حدث ونسيت أولويةً ما، أو أردت تحقيق ترتيب معيّن، فضعه داخل أقواس وانتهى الأمر. لدينا عامل حسابي آخر قد لا تميّزه للوهلة الأولى، وهو الرمز %، والذي يُستخدَم لتمثيل عملية الباقي remainder، فيكون X % Y هو باقي قسمة X على Y، فمثلًا: نتيجة 314 % 100 هي 14، أما نتيجة 144 % 12 فتساوي 0. وأولوية عامل الباقي هي الأولوية ذاتها للضرب والقسمة، ويُشار عادةً إلى هذا العامل باسم modulo. الأعداد الخاصة لدينا ثلاثة قيم خاصة في جافاسكربت، ننظر إليها على أنها أعداد، ولكنها لا تُعَدّ أعدادًا طبيعية. وأول قيمتين هما: infinity، و-infinity، وتُمثِّلان اللانهاية بموجبها وسالبها، وبالمثل، فإن infinity -1 لا تزال تشير إلى اللانهاية. ولا تثق كثيرًا بالحسابات المبنيَّة على اللانهاية، لأنها ليست منطقيةً رياضيًا، وستقود إلى القيمة الخاصة التالية، وهي: NaN، والتي تُشير إلى "ليس عددًا" Not A Number، رغم كونه قيمةً من نوع عددي بذاته، وستحصل عليه مثلًا: إذا حاولت قسمة صفر على صفر، أو طرح لانهايتين، أو أيّ عدد من العمليات العددية التي لا تُنتِج قيمةً مفيدة. السلاسل النصية السلسلة النصية String هي النوع التالي من أنواع البيانات الأساسية، ويُستخدم هذا النوع لتمثيل النصوص، ويُمثَّل هذا النوع في جافاسكربت، بنص محاط بعلامات اقتباس، أي كالتالي: `Down on the sea` "Lie on the ocean" 'Float on the ocean' وتستطيع استخدام العلامة الخلفية `، أو علامات الاقتباس المفردة '، أو المزدوجة ''، لتحديد السلاسل النصية، طالما أن العلامة التي وضعتها في بداية السلسلة هي ذاتها الموجودة في نهايتها. وتُوضع تقريبًا جميع أنواع البيانات داخل علامات الاقتباس تلك، وستعاملها جافاسكربت على أنها سلسلة نصية، ولكن ستجد صعوبةً في التعامل مع بعض المحارف، فمثلًا: كيف تضع علامات اقتباس داخل علامات الاقتباس المحدِّدة للسلسلة النصية؟ وكذلك محرف السطر الجديد Newlines، وهو ما تحصل عليه حين تضغط زرّ الإدخال؟ ولكتابة هذه المحارف داخل سلسلة نصية، يُنفَّذ الترميز التالي: إذا وجدت شَرطةً مائلةً خلفيةً \ داخل نص مُقتَبس، فهذه تشير إلى أن المحرف الذي يليها، له معنى خاص، ويسمّى هذا تهريب المحرف Escaping the character؛ إذ لن تنتهي السلسلة النصية عند احتوائها على علامات الاقتباس المسبوقة بشرطة مائلة خلفية، بل ستكون جزءًا منها؛ وحين يقع محرف n بعد شرطة مائلة خلفية فإنه يُفسَّر على أنه سطر جديد، وبالمِثل، فإذا جاء محرف t بعد شرطة مائلة خلفية، فإنه يعني محرف الجدولة tab، وtd مثال على ذلك، لدينا السلسلة النصية التالية: "هذا سطر\nوهذا سطر جديد" حيث سيبدو النص بعد تفسيره، كما يأتي: هذا سطر وهذا سطر جديد كما ستحتاج في بعض المواقف إلى وضع شرطة مائلة خلفية داخل السلسلة النصية، لتكون مجرّد شرطة مائلة \، وليست مِحرفًا خاصًّا، فإذا جاءت شرطتان مائلتان خلفيتان متتابعتان، فستلغيان بعضهما، بحيث تظهر واحدة منهما فقط في القيمة الناتجة عن السلسلة النصية. فمثلًا، تُكتَب السلسلة النصية التالية: "يُكتَب محرف السطر الجديد هكذا "‎\n"." في جافاسكربت، كما يأتي: "يُكتب محرف السطر الجديد هكذا \"\\n\"." وينطبق هنا ما ذكرناه سابقًا في شأن البِتّات وتخزين الأعداد، حيث يجب تخزين السلاسل النصية على هيئة بِتّات داخل الحاسوب. تُخزِّن جافاسكربت السلاسل النصية بناءً على معيار يونيكود Unicode، الذي يُعيِِّن عددًا لكل محرف تقريبًا قد تحتاجه ، بما في ذلك المحارف التي في اللغة العربية، واليونانية، واليابانية، والأرمنية، وغيرها. وتُمثَّل السلسلة النصية بمجموعة من الأعداد، بما أنه لدينا عدد لكل محرف، وهذا ما تفعله جافاسكربت تحديدًا، لكن لدينا مشكلة، فتمثيل جافاسكربت يستخدِم 16بت لكل عنصر من عناصر السلسلة النصية، ما يعني أنه لدينا 216 محرفًا مختلفًا، ولكن يُعرِّف اليونيكود أكثر من ذلك، أي بمقدار الضعف تقريبًا هنا، لذا تشغل بعض المحارف مثل الصور الرمزية emoji، موقعين من مواقع المحارف في سلاسل جافاسكربت النصية، وسنعود لهذا مرةً أخرى في الفصل الخامس. ولا يمكن تقسيم السلسلة النصية أو ضربها أو الطرح منها، لكن يمكن استخدام عامل + عليها، حيث لا يضيف بعضها إلى بعض كما تتوقّع من +، وإنما يجمعها إلى بعضها ويسلسلها معًا، أو يلصق إن صحّ التعبير بعضها ببعض، فمثلًا، سينتج السطر التالي، كلمة "concatenate": "con" + "cat" + "e" + "nate" تملك القيم النصية عددًا من الدوال المصاحبة لها -التوابع methods- التي تُستخدَم لإجراء عمليات أخرى عليها، وسنذكُر هذا بمزيد من التفصيل في الفصل الرابع. حيث تتصرّف السلاسل النصية المحاطة بعلامات اقتباس مفردة أو مزدوجة تصرّفًا متشابهًا تقريبًا، والاختلاف الوحيد بينهما هو نوع الاقتباس الذي تحتاج تهريبه داخلها. أما السلاسل المحاطة بعلامة خلفية (`)، والتي تُسمّى عادةً بالقوالب المجرّدة template literals، فيمكن تنفيذ عمليات إضافية عليها، مثل الأسطر الجديدة التي ذكرناها، أو تضمين قيم أخرى، كما في المثال التالي: `half of 100 is ${100 / 2}` حين تكتب شيئًا داخل {}$ في قالب مجرّد، ستُحسب نتيجته، ثم تُحوَّل هذه النتيجة إلى سلسلة نصية وتُدمَج في ذلك الموضع، وعليه يخرج المثال السابق "half of 100 is 50". العوامل الأحادية تُكتَب بعض العوامل على هيئة كلمات، فليست كلها رموزًا، وأحد الأمثلة على ذلك هو عامل typeof، والذي يُنتِج قيمةً نصيةً تُمثِّل اسم نوع القيمة الممررة إليه. انظر الشيفرة التالية، تستطيع تعديلها وتشغيلها في طرفية المتصفِّح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen. console.log(typeof 4.5) // → number console.log(typeof "x") // → string استخدمنا console.log في المثال التوضيحي السابق، لبيان أننا نريد أن نرى نتيجة تقييم شيء ما، وسنبيُّن ذلك لاحقًا في الفصل التالي. وتُنفَّذ العوامل التي بيّناها في هذا الفصل اعتمادًا على قيمتين، لكن العامل typeof يأخذ قيمةً واحدةً فقط، وتُسمّى العوامل التي تستخدم قيمتين، بالعوامل الثنائية binary operators، أما تلك التي تأخذ عاملًا واحدًا فقط، فتسمى العوامل الأحادية unary operators، مع ملاحظة أن عامل الطرح - يمكن استخدامه كعامل أحادي أو ثنائي، كما في المثال التالي: console.log(- (10 - 2)) // → -8 القيم البوليانية حين يكون لدينا احتمالان، فمن المفيد إيجاد قيمة تفرّق بين الاحتمالين، مثل: "yes" و "no"، أو "on" و "off"، وتِستخدِم جافاسكربت النوع البولياني Boolean لهذا الغرض، ويتكون هذا النوع من قيمتين فقط، هما: القيمة true والقيمة false، وتُكتبان بهاتين الكلمتين فقط. الموازنة انظر الطريقة التالية لإنتاج قيم بوليانية: console.log(3 > 2) // → true console.log(3 < 2) // → false علامتي <، و> هما اللتان تعرفهما من الرياضيات للإشارة إلى الموازنة "أصغر من"، أو "أكبر من"، وكلاهما عاملان ثنائيّان، ويُنتِجان قيمةً بوليانيةً تُوضِّح هل الشرط مُتحقِق أم لا، ويمكن موازنة السلاسل النصية بالطريقة نفسها، كما في المثال التالي: console.log("Aardvark" < "Zoroaster") // → true تُعَدّ الطريقة التي تُرتَّب بها السلاسل النصية أبجدية في الغالب، لكن على خلاف ما قد تراه في القاموس، تكون الحروف الكبيرة أقل من الحروف الصغيرة، فمثلًا، Z أقل من a، والمحارف غير الأبجدية (!، -، …إلخ) مدمجة أيضًا في الترتيب، وتمر جافاسكربت على المحارف من اليسار إلى اليمين موازِنةً محارف يونيكود واحدًا تلو الآخر. والعوامل الأخرى التي تُستخدَم في الموازنة هي: ‎<=‎ (أقل من أو يساوي)، و =< (أكبر من أو يساوي)، و == (يساوي)، و=! (لا يساوي). console.log("Itchy" != "Scratchy") // → true console.log("Apple" == "Orange") // → false وتوجد قيمة واحدة في جافاسكربت لا تساوي نفسها، وهي NaN بمعنى "ليس عددًا"، أي كما يأتي: console.log(NaN == NaN) // → false وبما أن NaN تشير إلى نتيجة عملية حسابية غير منطقية، فهي لن تساوي أيّ نتيجة أخرى لحساب غير منطقي. العوامل المنطقية كذلك لدينا في جافاسكربت بعض العوامل التي قد تُطبَّق على القيم البوليانية نفسها، وتدعم جافاسكربت ثلاثةً منها، وهي: and، وor، وnot، ويمكن استخدامها في منطق القيم البوليانية. ويُمثَّل عامل "and" بالرمز &&، وهو عامل ثنائي نتيجته صحيحة true إن كانت القيمتان المعطتان صحيحتان معًا. console.log(true && false) // → false console.log(true && true) // → true أما عامل الاختيار "or"، فيُمثَّل بالرمز ||، ويُخرِج true إذا تحققت صحة إحدى القيمتين أو كليهما، كما في المثال التالي: console.log(false || true) // → true console.log(false || false) // → false أما "Not" فتُكتب على صورة تعجب !، وهي عامل أحادي يقلب القيمة المعطاة له، فالصحيح المتحقِّق منه true! يَخرج لنا خطأً غير متحقِّق false، والعكس بالعكس. وعند دمج هذه العوامل البوليانية مع العوامل الحسابية والعوامل الأخرى، فلن نستطيع تَبيُّن متى نضع الأقواس في كل حالة أو متى نحتاج إليها، والحل هنا يكون بالعلم بحال العوامل التي ذكرناها حتى الآن لشق طريقك في البرامج التي تكتبها، والشيفرات التي تقرؤها، إذ أن عامل الاختيار || هو أقل العوامل أولوية، ثم يليه عامل &&، ثم عوامل الموازنة (<، ==، …إلخ)، ثم بقية العوامل، واختيرت هذه الأسبقية أو الأولوية، كي يقل استخدام الأقواس إلى أدنى حد ممكن، انظر المثال التالي: 1 + 1 == 2 && 10 * 10 > 50 والعامل الأخير الذي لدينا ليس أحاديًّا ولا ثنائيًّا، بل i; عامل ثلاثي يعمل على ثلاث قيم، ويُكتب على صورة علامة استفهام ?، ثم نقطتين رأسيّتين :، أي على الصورة التالية: console.log(true ? 1 : 2); // → 1 console.log(false ? 1 : 2); // → 2 ويُسمّى هذا العامل بالعامل الشرطي، أو العامل الثلاثي بما أنه الثلاثيُّ الوحيد في جافاسكربت، وتُحدِّد القيمة التي على يسار علامة الاستفهام، نتيجة أو خرْج هذا العامل، لتكون النتيجة هي إحدى القيمتين الأخرتين، فإذا كانت هذه القيمة true فالخرج هو القيمة الوسطى، وإن كانت false فالنتيجة هي القيمة الأخيرة التي على يمين النقطتين الرأسيّتين. القيم الفارغة يوجد في جافاسكربت قيمتان خاصتان تُكتبان على الصيغة null، وundefined، وتُستخدمان للإشارة إلى قيمة لا معنى لها، أو غير مفيدة، وهما قيمتان في حد ذاتهما، لكنهما لا تحملان أيّ بيانات، وستجد عمليات عدّة في هذه اللغة لا تُنتِج قيمةً ذات معنى كما سترى لاحقًا، لكنها ستُخرج القيمة undefined لأنها يجب أن تُخرِج أيّ قيمة. ولا تشغل نفسك بالاختلاف بين undefined، وnull، فهما نتيجة أمر عارض أثناء تصميم جافاسكربت، ولا يهم غالبًا أيّ واحدة ستختار منهما، لذا عاملهما على أنهما قيمتان متماثلتان. التحويل التلقائي للنوع ذكرنا في المقدمة أن جافاسكربت تقبل أيَّ برنامج تعطيه إياها، حتى البرامج التي تُنفِّذ أمورًا غريبة، وتوضح التعبيرات التالية هذا المفهوم: console.log(8 * null) // → 0 console.log("5" - 1) // → 4 console.log("5" + 1) // → 51 console.log("five" * 2) // → NaN console.log(false == 0) // → true وحين يُطبَّق عامل ما على النوع الخطأ من القيم، فستُحوِّل جافاسكربت تلك القيمة إلى النوع المطلوب باستخدام مجموعة قواعد قد لا تريدها أو تتوقعها، ويسمّى ذلك تصحيح النوع القسري type coercion. إذ تُحوِّل null في التعبير الأول من المثال السابق إلى 0، وتُحوَّل "5" إلى 5 أي من سلسلة نصية إلى عدد، أما في التعبير الثالث الذي يحوي عامل الجمع + بين نص وعدد، فنفّذت جافاسكربت الربط Concatenation قبل الإضافة العددية، وحوّلت 1 إلى "1" أي من عدد إلى نص. أما عند تحويل قيمة لا تُعبِّر بوضوح على أنها عدد إلى عدد، مثل:"five" أو undefined، فسنحصل على NaN، ولذا فإن حصلت على هذه القيمة في مثل هذا الموقف، فابحث عن تحويلات نوعية من هذا القبيل. كذلك حين نوازن قيمتين من النوع نفسه باستخدام ==، فسيسهل توقّع الناتج، إذ يجب أن تحصل على true عند تطابق القيمتين إلا في حالة NaN، أما حين تختلف القيم، فتَستخدِم جافاسكربت مجموعة معقدّة من القواعد لتحديد الإجراء الذي يجب تنفيذه، وتحاول في أغلب الحالات أن تحوّل قيمةً أو أكثر إلى نوع القيمة الأخرى. لكن حين تكون إحدى القيمتين null، أو undefined، فستكون النتيجة تكون صحيحةً فقط إذا كان كل من الجانبين null أو undefined. كما في المثال التالي: console.log(null == undefined); // → true console.log(null == 0); // → false وهذا السلوك مفيد حين تريد اختبار أيُّ القيم فيها قيمةً حقيقيةً بدلًا من null أو undefined، فتوازنهما بعامل == أو =!. لكن إن أردت اختبار شيئ يشير إلى قيمة بعينها مثل false، فإن التعبيرات مثل ‎0 == false و‎" " == false تكون صحيحةً أيضًا، وذلك بسبب التحويل التلقائي للنوع، أما إذا كنت لا تريد حدوث أيّ تحويل نوعي، فاستخدم هذين العاملَيْن: ===، و ==!. حيث يَنظر أول هذين العاملَين هل القيمة مطابقة للقيمة الثانية المقابلة أم لا، والعامل الثاني ينظر أهي غير مطابقة أم لا، وعليه يكون التعبير ‎" " === false خطأ كما توقعت. وإني أنصح باستخدام العامل ذي المحارف الثلاثة تلقائيًّا، وذلك لتجنُّب حدوث أي تحويل نوعي يعطِّل عملك، لكن إن كنت واثقًا من الأنواع التي على جانبي العامل، فليس هناك ثمة مشكلة في استخدام العوامل الأقصر. قصر العوامل المنطقية يعالج العاملان && و|| القيم التي من أنواع مختلفة، معالجةً غريبة، إذ يحوِّلان القيمة التي على يسارهما إلى نوع بولياني لتحديد ما يجب فعله، لكنهما يعيدان إما القيمة الأصلية للجانب الأيسر أو قيمة الجانب الأيمن، وذلك وفقًا لنوع العامل، ونتيجة التحويل للقيمة اليسرى، سيُعيد عامل || مثلًا قيمة جانبه الأيسر إذا أمكن تحويله إلى true، وإلا فسيعيد قيمة جانبه الأيمن. يُحدِث هذا النتيجةَ المتوقّعة إن كانت القيم بوليانية، ونتيجةً مشابهةً إن كانت القيم من نوع آخر. كما في المثال الآتي: console.log(null || "user") // → user console.log("Agnes" || "user") // → Agnes نستطيع استخدام هذا السلوك على أنه طريقة للرجوع إلى القيمة الافتراضية، فإن كانت لديك قيمة قد تكون فارغةً، فيمكنك وضع || بعدها مع قيمة بدل، حيث إذا كان من الممكن تحويل القيمة الابتدائية إلى false فستحصل على البدل. وتنص قواعد تحويل النصوص والأعداد، إلى قيم بوليانية، على أن 0، وNaN، والنص الفارغ " "، جميعها خطأً false، بينما تُعَدّ القيم الأخرى true، لذا فإن ‎0 || -1 تخرج 1-، و ‎" " || "!?"‎ تخرج "?!". ويتصرّف عامل && تصرّفًا قريبًا من ذلك، ولكن بطريقة أخرى، فإذا كان من الممكن تحويل القيمة اليسرى إلى false فسعيد تلك القيمة، وإلا سيعيد القيمة التي على يمينه. وهذان العاملان لهما خاصيّةً أخرى مُهمة، وهي أن الجزء الذي على يمينهما يُقيَّم عند الحاجة فقط، ففي حالة true || x ستكون النتيجة القيمة true مهما كانت قيمة x، حتى لو كانت جزءًا من برنامج يُنفِّذ شيئًا مستَهجنًا، بحيث لا تُقيَّم x عندها، ويمكن قول الشيء نفسه فيما يخص false && x والتي ستعيد القيمة false دومًا وتتجاهل x. ويسمّى هذا بالتقييم المقصور Short-circuit Evaluation. إذ يتصرَّف العامل الشرطي تصرّفًا قريبًا من ذلك، فالقيمة المختارة من بين القيم الثلاثة هي التي تُقيَّم فقط. خاتمة اطلعنا في هذا الفصل على أربعة أنواع من قيم جافاسكربت، وهي: الأرقام، والسلاسل النصية، والقيم البوليانية، والغير معرَّفة، حيث تُنشَأ هذه القيم بكتابة اسمها، كما في: true، و null، أو قيمتها، كما في: 13، و"abc"، وتُحوَّل وتُجمَع هذه القيم باستخدام عوامل أحادية، أو ثنائية، أو ثلاثية. كما رأينا عوامل حسابية، مثل: +، و-، و*، و/، و%، وعامل الضم النصي +، وعوامل الموازنة، وهي: ==، و =!، و===، و==!، و>، و<، و=>، و=<، والعوامل المنطقية، وهي:&&، و||، إلى جانب تعرُّفنا على عدّة عوامل أحادية، مثل: -، الذي يجعل العدد سالبًا، أو !، المُستخدَم في النفي المنطقي، وtypeof لإيجاد نوع القيمة، وكذلك العامل الثلاثي :? الذي يختار إحدى القيمتين وفقًا لقيمة ثالثة. ويعطيك ما سبق ذكره بيانات كافيةً لتستخدم جافاسكربت على أساس حاسبة جيب صغيرة، وفي الفصل التالي، سنبدأ بربط هذه التعبيرات لنكتب برامج بسيطة بها. ترجمة -بتصرف- للفصل الأول من كتاب Elequent Javascript لصاحبه Marijn Haverbeke. اقرأ أيضًا المقال التالي: هيكل البرنامج في جافاسكريبت الدوال العليا في جافاسكريبت
  22. مرحبًا أخ بلال، أشكرك على إرشادك ونصحك وحرصك، ما الفقرات التي رأيت أنها يمكن التخلص منها؟
  23. يُعَدّ مفهوم التعهيد الخارجي أحد المفاهيم التي لابد للمستقل من الاطلاع عليها، ليصبح أقدر على إنجاز الأعمال بكفاءة عالية وفقًا لاحتياجات كل مشروع، فقد لا يحتاج المشروع إلى أكثر من مجرد جهد المستقل وحده، لكن ربما يتعاقد على مشروع كبير الحجم أو فيه أجزاء خارج مجال اختصاصه، فحينها تبرز أهمية التعهيد الخارجي وتوظيفه لمستقلين آخرين ينجزون له تلك المهام. والتعهيد الخارجي (Outsourcing) هو اتفاق بين الشركة وأشخاص من خارجها من أجل تنفيذ بعض الخدمات أو الأعمال، وذلك خلال فترة زمنية معينة وضمن تفاصيل متفق عليها بين الجهتين. ويمكن تلخيص مزايا التعهيد الخارجي فيما يلي: توفير التّكاليف: يساهم التّعهيد الخارجي في تخفيض تكاليف العمل، حيث يعمل المستقلون فقط حسب الوقت الذي يتطلّبه المشروع، بمعنى أنّه ليس هنالك حاجة للدفع مبلغ مقابل الوقت الّذي سيضيع عند توظيف شخص ما سواءً كان دوامًا جزئيًا أو كاملًا. ولا شك أن المستقلّين سينجزون العمل في وقت أقل وربما بكفاءة أفضل موازنة بالموظفين التقليديين نتيجة بيئة العمل وطبيعة السوق، إذ يريد المستقل إنهاء العمل بأسرع وقت من أجل التعاقد مع عميل جديد. توظيف الكفاءات: يوفر التعهيد الخارجي مزية توظيف كفاءات أكثر مهارة وخبرة والوصول إليهم بغضّ النّظر عن الدّولة الّتي يتواجدون فيها، بينما حين توظّف أشخاصًا عادييّن فأنت مضطر لتوظيف أشخاص ضمن مهارة أو خبرة معيّنة تكون هي المتاحة محليًا. والتعهيد خيار لا تستخدمه كمستقل إلا في مرحلة متقدمة في عملك الحر، حين تصبح لديك قاعدة عملاء وتأتيك مشاريع كبيرة الحجم تحتاج إلى أكثر من تخصص، فحينها قد تتواصل مع مستقل آخر لينجز لك ما تحتاج من خارج تخصصك من أجل تسليم المشروع في وقته وبأعلى جودة. أو تحتاج إلى مهام مثل كتابة مدونات تسويقية على مدونتك ونشرها بشكل منتظم، أو تصميم صور ومنشورات مرئية لنشرها في حساباتك الاجتماعية التسويقية، ومتابعة خطة ذلك النشر، أو قد تكون بعض الأعمال البسيطة مثل إدخال البيانات أو البحث في موضوع ما لكنك لا تملك الوقت الكافي لذلك، فحينئذ يكون خيار توكيل هذه المهمة إلى مستقل آخر مناسبًا. أما الحالة المتقدمة من التعهيد الخارجي هي أن تكون صاحب شركة ولديك مهام خارج مجال تخصصك أو لا تملك وقتًا لها، فتوظف مستقلين لينفذوها لك، وإن كان بشكل متكرر أو منتظم. وكل تلك الحالات متقدمة بالنسبة لمستقل مبتدئ أو دخل المجال حديثًا، وليس عليك القلق بشأنها أو التفكير فيها الآن. الفرق بين التعهيد والوساطة ربما يجب هنا أن نفرق بين التوظيف والوساطة، إذ هذه مشكلة تحدث في عالم العمل الحر بشكل متكرر، يتعاقد فيها صاحب المشروع أو العميل مع مستقل لينفذ له عملًا ما، لكن بدلًا من تنفيذ المستقل للعمل فإنه يتعاقد بدوره مع مستقل آخر كأنه هو العميل وينقل إليه طلب العميل على أنه طلبه هو، فيكون وسيطًا لتنفيذ العمل وليس منفذًا مباشرًا له. وإن هذا مما يجب مراعاته كي لا تقع فيه لسببين: أن بعض منصات العمل الحر تضع شرط التسجيل فيها أنك تنشئ حسابًا لشخصك أنت، وليس ممثلًا لجهة أخرى، وذلك من أجل الوضوح والشفافية مع العميل، فصورة الحساب ستكون لك، والتقييمات ستكون باسمك أنت، والتعامل سيكون معك، فلا يُغرَّر بالعميل ليظن أنه يتعامل معك في حين أنه يتعامل مع شخص ثالث أو مجموعة أشخاص توكل إليهم هذه الأعمال. والسبب الثاني هو أمانة العقد والحفاظ على سمعتك، إذ هي رأس مالك في العمل على الإنترنت، وقد ذكر لي جميل بيلوني -كان صاحبًا لمشروع على منصة مستقل- حادثة وقعت له مع أحد المستقلين، إذ أوكل إليه مشروعًا لتنفيذه بعد الاطلاع على عينة من العمل والموافقة عليها، ثم تفاجأ أن العمل الذي سلمه المستقل جودته أقل من جودة العينة الأولى! ولما أرسل إلى المستقل يخبره بذلك رد عليه أنه قد وقع له طارئ منعه من العمل، فأوكل العمل إلى شخص ثالث ينجزه له لئلا يخسر المشروع برمته وقد أخفى ذلك عليه، لكن هذا الفعل قد تسبب في خسارة جميل -وهو العميل هنا- لوقت ضيَّعه مع هذا المستقل، وربما مال أيضًا لولا تعاقده مع من خلال منصة عمل حر تضمن للطرفين حقهما مثل منصة مستقل. أضف إلى ذلك خسارة المستقل ذاك سمعته بهذا الفعل. وهذا مثال للخطأ الذي نحذر منه ها هنا، فالعميل قد شعر أن المستقل خدعه بتسليم عمل على خلاف جودة العينة الأولى، وأنه تعامل معه لشخصه لشرط المنصة التي يتعاملان عليها الذي ذكرناه قبل قليل أنها تشترط إنشاء الحساب لشخص المنشئ وليس لجهة يمثلها، فتوقع أنه هو من سيسلم العمل وليس شخصًا آخر. لكن الحالة التي تستطيع توكيل فيها آخرين والتعهيد إليهم بالمهام هي أن تكون على منصة تسمح بذلك، أو أن يأتيك العملاء من خلال قنوات أخرى مثل موقع شخصي لك أو لشركتك، أو حساب بريدي أو اجتماعي. وحينها لك أن تكلم العميل بصيغة الجمع "نحن" أو تخبره صراحة لو كانت هويتك التجارية شخصية أنك ستوكل بعض المهام إلى بعض أفراد فريقك إن غلب عليك الظن أنه جاءك لشخصك ولا يريد اطلاع شخص ثالث على البيانات لحساسيتها له، من باب أمانة التعامل وللحفاظ على سمعتك. ما يجب مراعاته في التعهيد وهنا تأتي إلى النقطة التي يكبر فيها عملك لتكون في حاجة إلى بناء فريق يساعدك، أو تكون صاحب شركة تحتاج من توكل إليه مهامًا لينفذها لك، ويلزمك هنا شيء من العلم بإدارة الأعمال والمهام لتستطيع الاستفادة من تعهيد هذه المهام إلى مستقلين آخرين. وسنذكر فيما يلي بعض الأمور التي يجب مراعاتها عند توظيف مستقلين آخرين أو التعامل معهم. إدارة المهام سنكتفي في هذه النقطة بالإشارة إلى ما ذكرناه في مقال الإدارة الفنية للمشروع، من أدوات لإدارة المهام وتنظيمها وغير ذلك، مثل خدمة أنا أو تريللو وخدمات التخزين السحابية وبرامج الاجتماعات، فلا حاجة لتكرار هذه النقاط، فما يصلح لإدارة مشروع مع عميل يصلح ها هنا أيضًا. توزيع الوظائف على خلاف النقطة السابقة، فإن علاقتك مع هؤلاء المستقلين الذين ستعمل معهم تختلف عن علاقتك مع العميل، إذ تنفذ أنت ما يطلبه العميل وتقبض أجرك، أما هنا فيكون العمل بين عدة أطراف يتعاونون على إخراج منتج واحد. فالواجب هنا تحديد مدير لكل مشروع، وسيكون في الغالب هو الموظِّف و"العميل" في نظر باقي المستقلين، وذلك في المشاريع البسيطة التي تحتاج فيها إلى بضعة مهام خارجية مثل كتابة محتوى وتصميم ملصق دعائي، فحينها يرسل كل من الكاتب والمصمم تقريرهما إليك عند انتهائهما. أما عند العمل على مشاريع أعقد من ذلك وفيها عدة أشخاص ينفذون مهامًا ترتبط ببعضها، فمن الأفضل حينها إذا كنت أنت من وظفهم أن تضع هيكلًا تنظيميًا لهم بحيث يرفع العاملون في كل قسم تقريرًا إلى واحد فيهم تختاره أنت ليكون المسؤول عن هذا القسم، وتتعامل بعدها مع هؤلاء المسؤولين، بدلًا من التعامل مع جميع الذين توظفهم واحدًا واحدًا. مفهوم المساعد الافتراضي يشير مصطلح المساعد الافتراضي -من لفظه- إلى الشّخص الّذي يساعد غيره في تنفيذ مهامه لكن عن بعد فلا يكون حاضرًا في نفس محل عمل الشخص الأول، ولا يتولى المساعد الافتراضي عادةً مشاريع كبيرة بل تكون مشاريع صغيرة لمهام روتينية بسيطة. وهذه المهام قد تكون بسيطة في ذاتها أو تحتاج إلى مهارات خاصة، لكن بشكل عام فهي مهام مساعدة وثانوية بالنسبة لصلب مجال عملك كمستقل، والأمثلة التالية تبين أنواع تلك المهام التي يمكنك توكيل مساعد افتراضي لها: إدخال بيانات تدوين الملاحظات إعداد تقارير الأبحاث لكن لا نظنك ستلجأ إلى توظيف مساعد افتراضي في أول عملك الحر ولا حتى بعد مرور سنوات، ولا نستطيع إخبارك متى بالتحديد ستحتاجها، بل شأنها كشأن رفع أجر خدمتك تمامًا، أنت الذي ستعرف الوقت المناسب لذلك وفقًا لنمط عملك. ذلك أنها جديدة على السوق العربي من ناحية، وأن السوق العربي للعمل الحر نفسه جديد نوعًا ما فلم يتطور ويتبلور إلى المستوى الذي تنتشر فيه خدمة كالمساعدة الافتراضية، لكننا ذكرناها من باب العلم بها وأنها موجودة في السوق الغربي والأجنبي بشكل أكبر. العقود مع العملاء هذه أيضًا من النقاط التي لم نشأ الإسهاب فيها لأنها غير ذات فائدة كبيرة للعاملين في الوطن العربي في الوقت الحالي، ذلك أن السوق بعد جديد كما ذكرنا، فلم تُسن بعد القوانين الحاكمة للتعامل بين أشخاص طبيعيين "أفراد" من دول مختلفة بشكل حر، ولصعوبة مقاضاة عميل من دولة أخرى بسبب 50 دولار مثلًا، فتكاليف المقاضاة ستكون أكثر من ذلك بكثير! لكن اعلم أنك في حاجة إلى ما يحفظ لك حقك مع العميل لتضمن حصولك على أجرك كاملًا، والحل الأسهل هو التعامل من خلال منصة عمل حر مثل مستقل أو خمسات، لكن لو أتاك العميل من قناة أخرى لك فحينها تكون أمام أحد خيارين: إما أن تطلب منه الانتقال إلى منصة عمل حر لتنجزا المشروع عليها. أو تطلب منه نصف أجرك مقدمًا كما سبق بياننا في ذلك في مقال الإدارة الفنية للمشروع، كضمان لجديته في طلبه، ثم النصف التالي بعد إتمام العمل، فحينها إن خدعك العميل ولم يعطك بقية مالك بعد تمام المشروع تكون قد خرجت بأقل الضررين. خلاصة المقال بعد الاطلاع على هذا المقال نأمل أن تستطيع اختيار شكل العمل الأنسب لك وفق الخيارات المتاحة، لتنجز مشاريعك بشكل أفضل، وتزيد من حجم عملك كمستقل مع الوقت، وتكون قد علمت متى تحتاج إلى توكيل غيرك ببعض المهام التي لديك، وتتجنب الوقوع في مخالفة شروط المنصات التي تعمل عليها. كتبت سارة شهيد المسودة الأولية لهذه المقالة. اقرأ أيضًا المقال التالي: تعرف على منصات العمل الحر والعمل عن بعد المقال السابق: الإدارة المالية في العمل الحر كرائد أعمال، لا يمكنك القيام بكل شيء دليلك لإنجاح عملية التعهيد الخارجي لتطوير البرمجيات التعهيد الخارجي، الخطأ المميت للشركات الناشئة كيف تزيد أرباحك بتوظيف مستقلين آخرين إدارة فريق افتراضي موزع عبر 5 مناطق زمنية النسخة الكاملة من كتاب دليل المستقل والعامل عن بعد
  24. لا شك أن هذا المقال يُعد القطعة المكملة لتاج العمل الحر، فنحن نفصِّل في كيفية التعامل مع العملاء وإحسان الإدارة الذاتية للنفس وبيئة العمل والحياة العملية وغير ذلك في الفصول الماضية من أجل الوصول إلى هذه النقطة، وهي جني ثمار كل الجهد والتعب السابقيْن. واعلم أن الإدارة المالية في عملك الحر ذات شأن عظيم، ليس لأن المال هو مطلوبك من العمل، بل لأن العمل الحر يختلف عن الوظيفة العادية في أنك تمثل هنا وزارة المالية والضمان الاجتماعي معًا! فالأعمال الحرة ليس لها تأمين صحي ولا معاش في الغالب لأنها لا تتبع جهة نظامية، ومثلها في ذلك مثل الأعمال الحرة الحرفية التي تراها من حولك من النجارة والحدادة وغيرها. وينبني على ذلك أن عليك التفكير من الآن بشكل مختلف جذريًا في المال الذي تكسبه من عملك ذاك، وألا تنظر إلى ورقة المئة دولار على أنها ربحك من عمل عملته، بل هذه المئة فيها جزء يجب أن يذهب في حساب خاص للتأمين الصحي والطوارئ، وجزء آخر يذهب لمعاشك حين تقرر التقاعد، وهكذا، لهذا فضلنا استخدام كلمة الإدارة المالية من بداية هذا المقال، إذ هي أقرب للمعنى المراد من كلمة أرباح أو مكاسب أو راتب. وسنبدأ هنا في شرح كيفية تسعير خدماتك، والطرق المشهورة في تسعير المشاريع وطلبات العملاء، لتختار نوع التسعير المناسب لك وفق كل حالة، وكيف ومتى تزيد من سعر خدمتك. ثم ننظر في الطرق المشهورة للتعاملات المالية في العمل عن بعد وبشكل حر على الإنترنت، لتعلم كيف تسحب أجرك من منصات العمل الحر أو من العملاء. وبعد ذلك نتكلم بقليل من التفصيل عن عقود العمل بينك وبين العملاء، والتأمين الصحي لك والطوارئ وحساب التقاعد. العوامل المؤثرة في التسعير لا شك في أن أمر حساب السعر المناسب للمشروع الذي ستعمل عليه يشغلك ويقلقك، إذ تخشى رفع السعر خوفًا من ترك العميل لك إلى غيرك، وتخشى خفضه فتبخس حقك أو يظن العميل سوء جودة تنفيذك، فما العمل؟ وما العوامل التي يجب النظر لها واعتبارها عند حساب سعر المشروع؟ الواقع أن سعر الأعمال في الأسواق الحرة لا يتبع قوانين حاكمة في الغالب خاصة في الصفقات التي تكون في الأعمال الحرة التي نقصدها من الكتابة والترجمة والتسويق والبرمجة والتصميم والاستشارات الإدارية وإدارة الأعمال والمحاسبة والمساعدة الافتراضية وغير ذلك. لكن نستطيع القول أن القاعدتين الحاكمتين اللتين يمكن الأخذ بهما هما السعر العام للخدمة في السوق، والسعر المناسب لك أنت وفقًا لحساب تكاليفك وأرباحك، وهما محل حديثنا هنا. فأما سعر السوق فتستطيع معرفته بالنظر في متوسط قيمة العروض على الخدمات المماثلة لخدمتك في مواقع العمل الحر أو متوسط ميزانيات العملاء لمثل تلك المشاريع، وإن لم يكن ذلك متاحًا من أجل توفير تنافس شريف على الأعمال كما في موقع مستقل، أو كانت ميزانيات العملاء لا تمثل القيمة الحقيقية للمشروع، فحينئذ تنظر في بعض من يقدم هذه الخدمة باحثًا عن السعر المناسب لما تفعله. فمثلًا، حين بدأت في الترجمة قبل نحو ست سنوات بعد أن كنت أعمل في التصميم المرئي وتصميم تجربة الاستخدام، لم أكن أعرف متوسط الأسعار التي يمكن طلبها في الترجمة للبلاد العربية، ونظرت في الأسعار العالمية للترجمة فلم أظنها تناسبني حينها. فاستشرت اثنين من الكتاب العرب الذين لهم أعمال في منصات العمل الحر التي قررت العمل فيها عن متوسط السعر المناسب لخدماتي، فأشاروا علي بالأسعار المناسبة وفق الجودة والسوق والعرض والطلب، ونفعني ذلك أيما نفع عند أول تعاملاتي مع عملاء من العرب. وأما حساب السعر وفقًا لتكاليفك أنت، فهذا تعرفه بحساب تكاليفك المختلفة من إيجار للمكتب إن كنت تعمل من مكتب، واشتراكات برمجية إن كنت تعمل على برامج تصميم كحزمة أدوبي مثلًا، وتكاليف الإنترنت والكهرباء والنقل وغيرها إن وجدت، ثم التكاليف التي تغطي حياة كريمة لك إن كنت تعتمد على العمل الحر بصورة كلية من سكن ونفقات ومأوى وطوارئ وتأمين ومعاش وغير ذلك، وتقسم مجموع ذلك على عدد ساعات عملك في الشهر، فتخرج بقيمة كل ساعة عمل لديك. وعليه، فإن كانت ساعة عملك تساوي 10$ مثلًا وكان العمل المطلوب منك يستغرق ثلاث ساعات فإن الثمن الذي ستضعه يجب ألا يقل عن 30$، وإن وجدت السوق كله ينفذ هذا العمل بخمسة دولارات فهذا لا يعنيك، إذ أن هذا هو السعر المناسب لك أنت، هذا ونحن لم نحسب عامل خبرتك في المجال إذ تزيد أو تُنقص من ذلك السعر أيضًا. طيب ماذا تفعل في هذه الحالة؟ الحل هنا أن تترك هذه الخدمة إلى غيرها أو تغير السوق الذي تتعامل معه أو تقلل تكاليفك ونفقاتك، فمن غير المنطقي أن تعمل بسعر لا يغطي تكاليفك! لكن هذه حالة متطرفة ولا تحدث إلا نادرًا جدًا، وفي غالب هذه النوادر يكون السبب هو وضع العميل لميزانية غير منطقية، وحينها تتفاوض معه على الميزانية المناسبة أو ترفض العمل. أنواع التسعير للمشاريع سننظر في ما يلي في بعض أشهر نظم تسعير الخدمات في السوق بشكل عام، والسوق العربي بشكل خاص لتختار بعدها ما يناسبك وفق المشروع ووفق نوع الخدمة التي تقدمها. التسعير بعدد الساعات في هذا النوع تحدد قيمة ساعة عملك وفقًا للتكاليف التي ذكرناها أعلاه، وتحسب سعر المشروع وفقًا لعدد الساعات التي تحتاجها لتنفيذه، لكن هذه الطريقة لا تصلح إلا للخبير في مجاله ولا تصلح للمبتدئين، إذ نفترض بهذا الأسلوب أنك تعرف تمامًا ما ستفعله في كل مشروع، فتكون الساعات الثلاث تلك كلها عمل، على عكس المبتدئ أو الأقل خبرة إذ سيكون نمط عمله أبطأ بقليل. التسعير بالوحدات في هذا النوع تحدد السعر لكل وحدة عمل، كأن تحدد قيمة معينة ولتكن س دولار لكل ألف كلمة تترجمها أو تكتبها، أو تصميم تنفذه، أو خاصية برمجية تضيفها، أو صفحة، أو جدول بيانات (spreadsheet)، وهكذا. وهذا النوع هو الأشهر في تحديد الأسعار في السوق، ولا نحتاج هنا أن نذكرك أن هذه القيمة يجب أن تناسب وقت تنفيذ المشروع وتغطي تكاليفك، وانتبه إلى أنه كلما طال وقت التنفيذ قلت قيمة كل ساعة عمل، فاحرص على إنهاء العمل قبل موعد تسليمه. التسعير الثابت هذا النوع مناسب للخدمات التي تنفذها لعملاء بشكل دوري ومتكرر، كأن يكون لديك عميل يرسل لك عددًا ثابتًا من الملفات للعمل عليها (ترجمة، تصميم، تعديل، …إلخ) كل شهر، فهنا تستطيع إما استخدام أحد أنواع التسعير السابقة، أو حساب متوسط تلك الملفات شهريًا ووضع سعر واحد للشهر وفقها. الطرق المشهورة للمعاملات المالية نأتي الآن لمرحلة ما بعد تنفيذ المشروع، يجب أن تكون قد حصلت على المال الآن، فأين هو، وكيف تحصل عليه؟ إن كنت تعمل على إحدى منصات العمل الحر فحينها يكون مالك في موقع تلك المنصة، وستقتطع منه المنصة نسبة كعمولة لها قد تصل إلى خمس المبلغ لقاء تأمين حصولك عليه وعدم تهرب العميل من الدفع، إذ تشترط المنصة على العميل إرسال كامل قيمة المشروع إلى المنصة قبل بدء التنفيذ، ولقاء توفير مكان يجمعك مع العميل والفصل في الخلافات التي قد تنشأ بينكما وغير ذلك من الخدمات التي قد تحتاج إليها في التعامل مع العملاء، خاصة عند البدء في العمل الحر بدون خلفية سابقة ولا قاعدة عملاء. وتحتفظ هذه المنصة بقيمة المشروع لديها مدة تتراوح بين أسبوع إلى أسبوعين من أجل إتمام إجراءات التحويلات البنكية من ناحية، وحفظ حق العميل من ناحية أخرى إذا حدث أن اشتكى العميل من سوء التنفيذ أو تلاعب المستقل به، فحينئذ تكون قيمة المشروع لا زالت في يد المنصة ويمكن إرجاعها للعميل، والعكس صحيح إذا ثبت الحق للمستقل. وبعد انتهاء مدة احتجاز الأرباح تلك، توفر منصة العمل الحر وسائل بنكية لسحب هذه الأموال، وأشهر هذه الوسائل هي باي بال (PayPal) وويسترن يونيون (Western Union)، لكن قد تجد منصات تقبل الدفع أو السحب بوسيلة بنكية أخرى، فالفيصل هو ما اختارت المنصة أن تتعامل به وفقًا لظروفها والمناطق الجغرافية التي تخدمها. وقد تكون في دولة أو بلد لديه مشاكل في سحب أموالك بالطريقة التي تتيحها المنصة، فما العمل؟ الحل أن تبحث عن وسيط (شخص أو شركة) يستطيع استلام تلك الأرباح ومن ثم تحويلها إليك، أو الاستعانة بأحد معارفك ممن تثق به خارج البلاد يسحبها لك ويرسلها إليك بطريقة أخرى، وستجد شرحًا مفصلًا لباي بال وطرق السحب منه في هذه المقالة من مدونة مستقل. التأمين الصحي وخطة التقاعد والآن، نأتي لمرحلة ما بعد العمل، فنفترض أنك قد تعاقدت مع عملاء وحصلت على مشاريع وعملت فيها وجنيت أرباحًا وصارت لك قاعدة وسوقًا نجحت فيه، ثم ماذا؟ يجب الآن أن تفكر في الطوارئ التي قد تحدث لك، ومتى ستوقف هذا العمل! نعم، فأنت لن تعمل طول عمرك، بل يجب أن يكون هذا تخطيطك حتى لو استطعت العمل، فأنت لم تخلق لتعمل وتعمل فقط، والوظائف التقليدية توفر هذا التفكير على العاملين فيها باقتطاع نسب ثابتة من رواتبهم كل شهر من أجل التأمينات الصحية والخدمات والمنافع الأخرى التي توفرها لهم، بما فيها المعاش أو الضمان الاجتماعي. أما وقد صرت تعمل بشكل حر الآن فإن عليك التفكير في هذه الأمور واحتسابها في تخطيط المالي لكل عام، وهذه النقطة تحتاج إلى استشارة متخصص في التأمينات الاجتماعية من أجل حساب دقيق لها، ليخبرك بالمبلغ الذي يجب تخصيصه كمعاش لك بعد التقاعد. لكن القاعدة العامة في هذا هي احتساب نفقاتك وتكاليفك كما ذكرنا في بداية المقال، ومحاولة ترشيد إنفاق تلك المصاريف واستهلاك الموارد، وما زاد على ذلك يُدخر للطوارئ والمعاش والاحتياجات المستقبلية. وأفضل من هذا أن تخطط لتعلم إدارة الأعمال -إن لم تكن قد فعلت- لأنك محتاج لها في العمل الحر من ناحية كما سيرد بيانه في المقال التالي: التعهيد الخارجي وتوظيف مستقلين، ولأنك تستطيع تأمين مصدر دخل خامل لك بإنشاء تجارة جانبية تنمو مع الوقت بينما تعمل أنت في عملك وخدماتك كما اعتدت، إلى أن تصل فيها تلك التجارة إلى مرحلة تغنيك عن العمل، فحينها تكون بديلًا عن الضمان الاجتماعي، إن لم تكن أفضل! إذ لا يشترط هنا أن تعمل بيديك فيها، بل قد توظف فيها من يعمل فيها لك. أيضًا، من الأفكار المعتبر بها في خطط التقاعد أن تختار تجارة أو شركة لتستثمر فيها بمالك دون أن تعمل فيها بشكل مباشر، أي في صورة أسهم فقط، وهذا من شأنه أن يعود عليك بأرباح شهرية أو سنوية تغنيك كذلك عن العمل بعد مدة معينة تصل فيها تلك الأرباح إلى الحد الذي يغطي نفقاتك وتكاليفك. خلاصة المقال وهكذا، نكون قد مررنا على أمور التسعير والتقاعد بشكل سريع، ونأمل أن تكون قد عرفت كيف تضع أسعارك في أعمالك وخدماتك دون رهبة من خسارة السوق أو تشويه لجودة عملك. كتبت سارة شهيد المسودة الأولية لهذه المقالة. اقرأ أيضًا المقال التالي: ما يلزم العامل المستقل معرفته عن التعهيد الخارجي المقال السابق: العناية ببيئة عمل العامل المستقل نصائح للمستقلين الذين يعملون في مجال تصميم وتطوير الويب للتوفير والادخار والتقاعد. كيف تسعّر مشاريع الويب. الطريقة الأمثل لإدارة ميزانيتك بكفاءة وتسجيل أرباح كعامل مستقل. النسخة الكاملة من كتاب دليل المستقل والعامل عن بعد
  25. بعد أن تكلمنا في المقال السابق عن الأثر الصحي للعمل على الحواسيب على المدى المتوسط والطويل، وتجنب الأذى المحتمل من هذا النمط من العمل بالرياضة وحسن إدارة الوقت وتنظيم النوم وغيرها، ننظر الآن في إعداد بيئة العمل نفسها لتعينك على تنفيذ النصائح التي أوردناها من قبل في شأن الإدارة الفنية للمشروع نفسه والعناية بصحتك وحياتك الاجتماعية والعملية على سواء. بناء بيئة العمل المثلى اعلم أن بيئة العمل التي ستكون فيها -وإن كانت تبدو بسيطة في عناصرها- إلا أنها تحتاج إلى نظر وتهيئة لتتجنب الأخطاء التي وقع أغلبنا فيها من فقدان للبيانات أو تعطل عن العمل نتيجة توقف الإنترنت أو الكهرباء أو أحد الأجهزة لديك. وسننظر فيما يلي في العناصر التي يجب الاهتمام بها عند تهيئة بيئة العمل وكيفية إعدادها، مراعين في ذلك الاحتياجات الضرورية للعامل المستقل الذي بدأ العمل الحر لتوه ولا يريد إنفاق مدخراته على معدات قد لا يحتاجها فيما بعد. لا شك أنك تقول الآن ألا يكفي حاسوبٌ محمولٌ واتصال جيد بالإنترنت؟ ونقول لك بلى، يكفي كحلٍّ مؤقت إلى حين تتأكد أن العمل الحر عن بعد مناسب لك فتستثمر في معدات أفضل، ذلك أن العمل لفترة طويلة على حاسوب محمول على سرير أو في حجرك سيؤذي ظهرك ورقبتك وأكتافك وعينيك أيضًا. المحيط العام يُفضل أن يكون لك مكتب خاص بالعمل منفصل عن المنزل، ولو كان في مساحة عمل مشتركة فإنه أفضل، لكن لعلمنا أن أغلب العاملين عن بعد يفضلون هذا النمط لأنهم يريدون العمل من المنزل أو مضطرون إلى ذلك، فإننا نهيب بك أن تخصص مكانًا في منزلك يكون للعمل فقط (أو ركنًا منفصلًا على الأقل)، لا تدخله إلا لذلك، ولا تبقى فيه بعد انتهاء العمل. لا تنسَ أن تعلم أهلك أو من تسكن معه بضرورة عدم الدخول إلى مكان عملك من دون إذنك واحرص على منع أي زيارات غير متوقعة وأخبر الجميع بلطف أنك تعمل وأنك غير متاح في أوقات عملك؛ فبما أنك في المنزل، قد يظن الأهل أو الأصدقاء أنك متاح في أي وقت وقد يُطلَب منك شراء بعض الحاجات التي قد تلزم المنزل بشكل مفاجئ الأمر الذي يؤدي إلى تشويشك ومقاطعتك عن العمل بين الحين والآخر وقد حصل هذا الأمر مع أغلبنا. فأخبر أهلك في مثل تلك الحالة بإعداد قائمة المشتريات أو الحاجات المراد قضاؤها لإنجازها بعد الانتهاء من العمل، وأعلِم الأصدقاء أنك تصبح متاحًا في الوقت الفلاني بعد الانتهاء من العمل، أي احرص على وضع حدود واضحة لأوقات عملك ومتى تكون متاحًا أو غير متاح؛ فلذلك السبب أشرنا في البداية إلى أنَّ استعمال مساحة عمل مشتركة أو مكتب عمل خارجي هو الحل الأفضل وسنفصل هذه الجزئية لاحقًا من هذا المقال في قسم «فصل بيئة العمل عن بيئة الراحة» لأهميتها الشديدة. الإضاءة الكافية وسواء كان هذا المكان غرفة كاملة أو ركنًا في غرفة، فيجب أن تكون الإضاءة كافية فيه للرؤية والعمل، ولو استطعت إضافة مصباح مكتبي لإزالة الظلال التي تلقيها إضاءة الغرفة على الكائنات والأغراض على المكتب فإنه يكون أفضل. الأثاث المكتبي بما أن عملك سيكون على حاسوب مكتبي فيجب أن نهيئ هذه البيئة لنتجنب آثارها التي ذكرناها في المقال السابق، وذلك باختيار المعدات المناسبة بالمواصفات الصحيحة. ونفضل أن يكون كرسي العمل مريحًا للجلوس فترات طويلة، فيجب أن يكون قابلًا للدوران على قاعدة عجلات كبيرة من أجل الاتزان، ويقبل تعديل ارتفاع المقعد والظهر ومسند الذراع من أجل الراحة في الجلسة وفقًا لطولك وهيئة جسدك. لكن إن لم يكن لديك ذلك الكرسي فاجتهد أن يكون كرسي عملك مريحًا إلى حين استثمارك في كرسي مناسب. أما مكتب العمل نفسه، فيجب أن يكون متينًا قادرًا على حمل الحاسوب وأدوات العمل الأخرى بشكل آمن، ويكون سطحه غير عاكس للضوء لتجنب إجهاد العينين، وارتفاعه مناسب بحيث لا يكون عاليًا فيجهد ذراعيك وكتفيك، أو منخفضًا فيجهد ظهرك ورقبتك. التهوية الجيدة من المهم أيضًا أن تحرص على نقاء الهواء الموجود في الغرفة، ولعلمنا أن العمل من المنزل يجعل المرء يتكاسل في هذه الشؤون البسيطة لصرفه النظر إلى مهام عمله فإننا نذكرك بها لأهميتها. ولا نقصد هنا مجرد تذكيرك بفتح النوافذ، فقد تكون في مدينة مزدحمة وهواؤها غير نظيف، فمن الحكمة هنا أن تتصرف بحكمة في تنقية هواء مكان العمل، إما بمرشح خاص لتنقية الهواء إن كان الهواء الخارجي غير نقي، أو بمجرد فتح النوافذ كل حين لتجديد الهواء. أيضًا، من المهم مراقبة حرارة الغرفة والاجتهاد في تنظيم حرارتها لتكون مناسبة للعمل، فلا تكون حارة جدًا بحيث تؤثر عليك وعلى حاسوبك، ولا باردة بحيث تصرف انتباهك عن العمل. المعدات الإلكترونية نأتي الآن لصلب معدات العمل، فبداية يجب أن يكون الحاسوب الذي تعمل عليه جيد المواصفات ليتحمل العمل لساعات ودون كلل أو بطء في التشغيل، خاصة إن كنت ستعمل في البرمجة أو التصميم. كذلك يجب أن تكون الشاشة التي تعمل عليها كبيرة بما يكفي لئلا تسبب لك إجهادًا من كثرة العمل، ولو كان حاسوبك الأساسي ذا شاشة صغيرة فخطط للاستثمار في شاشة كبيرة من أجل راحة عينيك أثناء العمل، ولتكن 15 بوصة على الأقل. انظر الصورة التالية من أحد مكاتب أفرد فريق شركة حسوب كمثال على مكتب مناسب للعمل. كما ترى فإن الحاسوب المحمول مرفوع إلى مستوى الشاشة الثانية، والمكتب كله به إضاءة كافية (بالإضافة إلى إضاءة أخرى مريحة للنفس D-: ) بحيث لا تُجهد العينان من أثر الوهج الأبيض من الشاشتين، ولوحة المفاتيح كبيرة ومريحة. نموذج عن محطة عمل صحية ومريحة للعامل البعيد من المنزل وإن لم يكن لديك حامل للحاسوب فبضعة كتب تكفي! ويمكن البدء بالحاسوب المحمول فقط دون الحاجة للشاشة الثانية وقولنا هذا لئلا تظن أنك لن تستطيع العمل إلا إن اشتريت هذا وحصلت على ذاك! ذلك، وإننا لنعلم أن كثيرًا من العاملين عن بعد يستخدمون الحاسوب المحمول كأداة أساسية للعمل، إلا أن الأولى الاستثمار في محطة عمل مناسبة (Workstation) كالتي في الصورة أعلاه أو حاسوب مكتبي، يتحمل العمل ولا يتعبك مثل الموضحة بالصورة أدناه، ومحطة العمل هي حاسوب بالنهاية، لكنها بمواصفات أفضل من الحاسوب الشخصي المعتاد الذي كنت تستخدمه بين الحين والآخر من أجل الألعاب وتصفح الإنترنت وكتابة بعض المستندات، والذي قلَّت الحاجة إليه منذ تطور مواصفات الهواتف الجوالة. ولا بأس أن تبدأ بالحاسوب الذي لديك الآن سواء كان مكتبيًا أو محمولًا، قديمًا أو حديثًا، وكلما زاد ضغط العمل وزاد دخلك استثمرت في معدات أفضل، ذلك أن الحاسوب المحمول وإن كانت مواصفاته الفنية تسمح بالعمل بكفاءة، إلا أن طبيعة حجمه وشكله ومكان لوحة المفاتيح ولوحة اللمس يجعلان العمل عليه مجهدًا إن كان لفترات طويلة وعلى مدى بعيد، فقد كان مخصصًا لأصحاب الأعمال الذين يتنقلون كثيرًا في بداية ظهوره قبل تطور مكوناته ووصولها إلى المواصفات التي عليها اليوم والتي تجعله خيارًا بديلًا عن الحاسوب الشخصي المكتبي. مكتب أحد أفراد فريق حسوب فيه كرسي صحي به مسند للرأس مناسب لارتفاع الشاشة ومسندان لليد متغيري الارتفاع لمناسبة ارتفاع طاولة المكتب لكن اعلم أنه ليس خيارًا مثاليًا إلا أن يكون مصممًا ليكون محطة عمل محمولة وليس مجرد حاسوب محمول (لابتوب)، فسيفتقر إلى إمكانيات كثيرة مثل إضافة وإزالة مساحات التخزين بحرية، ترقية مكوناته، استبدال التالف منها بسهولة، وهكذا، فالحاسوب المحمول أصعب في الاعتناء به وأعقد في إصلاحه من الحاسوب الشخصي. فصل بيئة العمل عن بيئة الراحة ذكرنا في بداية المقال هذه النقطة لمامًا في بيان المكان المخصص لعملك، والحقيقة أن أثرها يتعدى مجرد الفصل المكاني لوظيفة عن أخرى إلى الفصل النفسي لها، وكذلك الفصل الاجتماعي بين العمل والمنزل وإن كانا في مكان واحد. ذلك أن ثقافة العمل من المنزل جديدة على البشر بشكل عام، وعلينا نحن العرب بشكل خاص، فالمعتاد أن المرء يخرج من بيته في الصباح إلى مصنعه أو شركته أو مدرسته أو ورشته أو غير ذلك، وهو هناك في بيئة خاصة بالعمل لا يخالطها أي علاقة بالمنزل، فيصفو ذهنه من كل شيء إلا ما بين يديه من العمل حتى ينتهي منه فينصرف إلى منزله. أما حين تعمل من منزلك فإنك لا محالة واقع في بعض المواقف الاجتماعية اليومية التي لن تستطيع النجاة منها بمجرد القول أنك تعمل، فإن هذا الأمر جديد على العرب كما ذكرنا، وعليه فبما أنك في المنزل فأنت لا تعمل، وعليه فلم لا تساعد أخاك الصغير في واجبه المدرسي أو تشرح له درسًا يصعب عليه فهمه، وحبذا لو أخرجت القمامة من المنزل إلى الحاوية! ولا أريدك أن تسيء فهم قولنا على وجه العصيان وعقوق الأهل وغبنهم حقهم، لكن اعلم أن العمل يحتاج إلى عناية وتهيئة خاصة، فهل ترى أن لك حقًا في طلبك ممن معك في المنزل أن يخفضوا أصواتهم عن المعتاد في شؤونهم اليومية، أو يحولوا المنزل إلى مقر شركة من أجلك أنت؟ كلا، فهذا بيت، وهذا حق من حقوقهم! فما الحيلة إذَا وكيف المخرج من هذه المشكلة؟ الحل كما بيَّنا من قبل أن تفصل أنت بيئة العمل عن المنزل ما استطعت، فالوظيفة الطبيعية لغرفة المعيشة هي الراحة والاستجمام والتواصل العائلي، والوظيفة الأولى لغرفة النوم هي النوم، وكذلك المطبخ والحديقة، وقس على ذلك. فلا تتوقع أن تجد بيئة العمل المنزلية مريحة نفسيًا ومعينة على العمل بنفس القدر الذي تتوقعه من بيئة عمل صناعية مجهزة لهذا الغرض فقط. وإن هذه المسألة واحدة من أهم تحديات العمل الحر عن بعد، وفيها يقول كالفِن نيوبورت الذي أوردنا ذكره في المقال السابق في شأن العناية بالصحة النفسية وإدارة الوقت، أن تجعل بيئة العمل للعمل والمنزل لشؤون المنزل، فإن احتاج عملك ساعة إضافية فلا تأخذه معك للمنزل، بل ابق تلك الساعة الإضافية في محل العمل حتى تنهيه، ثم عد إلى المنزل ولا تنفذ أي مهام تتعلق بالعمل فيه، بل هو مكان الراحة والسكن. ولو تفكرت في ذلك لرأيت أن هذا هو المنطق وما كان عليه البشر، والواجب علينا نحن معشر العاملين عن بعد أن نوافق أنفسنا على فطرة البشر الطبيعية بدلًا من إجبار من حولنا على التكيف وفق ظروفنا. لهذا فإننا نكرر قولنا هذا لأهميته، أنك إذا استطعت استئجار مكتب خاص للعمل فهو أفضل شيء، ثم مكتب في مساحة عمل مشتركة (Co-Working Space) وهي شركات تؤجر مكاتب خالية للأعمال وللذين يعملون عن بعد وللشركات الصغيرة التي ليس لها مقر بعد وللاجتماعات وغير ذلك، وقد بدأت تنتشر في العالم العربي رويدًا رويدًا في المدن الكبرى. ثم إن لم تستطع إلا العمل من المنزل، فخصص عددًا معينًا من الساعات كل أسبوع وكل يوم، ومكانًا خاصًا لا تستخدمه إلا للعمل، فإن كان غرفة مستقلة فبها، وإلا فليكن مكتبًا في غرفة تخصصه للعمل فقط. كذلك إن استطعت فخصص حاسوبًا للعمل فقط كذلك، ليكون هذا نظامك وديدنك و"روتينك" اليومي كأنك في مقر شركة. وربما لا يعجب هذا بعض من يروقه العمل بحرية، ولا بأس في هذا إن كان يعي عواقب ما يفعل، إنما قولي هذا عن تجاربي في العمل الحر "بحرية" لا يضبطها نظام، وقد استنتجت هذا بعد أعوام من العمل الحر والعمل في شركات عن بعد، عملت فيها من مكاتب خصصتها للعمل فقط بعيدًا عن المنزل تارة، ومن المنزل تارة، وعلى شاطئ البحر تارة، ومن الحديقة، ومن السرير، وفي ثياب المنزل، وفي ثياب العمل، مرة في الصباح، ومرات في المساء، وربما في جوف الليل. ولعلك تستنتج من هذا النمط أني شخص لا يستطيع تحمل نمط واحد لفترة طويلة، ولعلك أصبت في هذا فقد قضيت سنين الدراسة ثم الخدمة العسكرية بين زي موحد وطوابير ومواعيد ثابتة وأعمال متكررة لا تتغير ولا تتبدل، فلعل ذلك أصابني بالملل من الروتين المتكرر فقضيت العقد الثالث من عمري لا أتحمل الروتين الثابت. لكني علمت بعد تقلبي في كل ذلك أن النجاح المهني والاستقرار المادي وبناء تجارة ونموذج عمل يؤمن حياة مستقرة تغنيك عن التفكير في المال كل يوم، لن يأتي إلا بالشيء الذي كرهته وهربت منه طيلة تلك الأعوام، ألا وهو الروتين. وإن هذا يرتبط بشكل ما بوهم الثقة الذي تحدثنا عنه في المقال السابق في شأن التسويف، أن المسوِّف قد يكون واقعًا في فخ الغرور والكِبر لظنه أنه يستطيع إن أراد أن يتم المهمة التي يريد إنجازها، وهو واهم قطعًا بسبب ما نراه من الواقع المشاهد، وبسبب تجاربي الكثيرة في ذلك على صعيد شخصي. عاقبة التسويف كبيرة خصوصًا مع ظن القدرة على إنجاز العمل بسهولة وسرعة، وهو ما نقع فيه أغلبنا خاصة في مثل هذه الأعمال التي تكون عن بعد وبغير ضابط ملزم مثل الوظيفة المنضبطة بميعاد محدد لكل شيء لا يمكن إتمام المهمة بعده أو قبله. فإن فعلتَ ذلك كله من تهيئة لبيئة مناسبة للعمل جيدة الإضاءة والتهوية، معزولة عن المشتتات الخارجية المعيقة للعمل، مجهزة العتاد الإلكتروني من حاسوب وإنترنت وغير ذلك، وحددت نظامك اليومي الثابت للعمل والإجازات والرد على الرسائل النصية ومواعيد المكالمات والاجتماعات مع العملاء والمراجعات وغير ذلك، فإننا نرجو أن تكون قد اجتزت أكبر عقبة تقف حجر عثرة في طريق العمل عن بعد. تنظيم الملفات والأجهزة نأتي الآن إلى نقطة فنية في إعداد مكتب العمل، وهي تنظيم الأجهزة والمعدات التي لديك لئلا تشغل حيزًا من وقتك في كل مرة تجلس إلى العمل، فيكون تركيزك ووقتك منصبًا على إتمام العمل وليس تهيئة البيئة في كل مرة. الأسلاك والتوصيلات الكهربية والراجح أن التوصيلات الكهربية لمكتبك لن يحدث فيها تغيير إلا مرة كل بضعة أشهر مثلًا لإضافة جهاز أو إصلاح تالف، فابدأ بها وخصص لها مجرى مناسب تمر فيه الأسلاك والتوصيلات الكهربية بعيدًا عن متناول يدك ومواضع الفأرة ولوحة المفاتيح وغير ذلك، ولو استطعت ربطها بشريط أو غالق بلاستيكي فإنه يكون أفضل لجمع تلك الأسلاك والحد من بعثرتها. الشاشات إن كنت ستستخدم أكثر من شاشة للعمل فيفضل أن تكون كلها من نفس الحجم والكثافة النقطية (Resolution) وعلى مستوى واحد، إلا إن كنت تحتاج إلى شاشة بالوضع الرأسي لمراجعة نصية أو برمجية مثلًا، وإن كانت الشاشات موصولة بحاسوب محمول فيفضل رفع الحاسوب على حامل -أو بضعة كتب- لتكون الشاشة في مستوى بقية الشاشات التي تستخدمها. ويكون الوضع المثالي لتلك الشاشات حين تكون قمة الشاشة في مستوى نظرك حين تجلس مستقيم الظهر والرقبة ناظرًا للأمام. بقية الأجهزة الأخرى إذا كنت تستخدم أجهزة أخرى مثل لاقط صوت وكاميرا للاجتماعات المرئية فخصص لهما مكانًا كذلك من أجل سهولة الوصول إليهما عند الحاجة بحيث يكونا مثبتين في المكتب أو الشاشة أو غير ذلك، لكن ماذا لو كانت تلك الأجهزة لا تُستخدم إلا قليلًا؟ حينئذ تُبعد تلك الأجهزة والمعدات عن مكتب العمل وتخصص لها مكتبًا خاصًا أو أدراجًا للتخزين، والقاعدة هنا أن أي جهاز أو كتاب أو مستند أو أداة لا تستخدمها كل يوم في العمل فإنك لا تجعلها على مساحة العمل اليومي في المكتب. تنظيم الملفات لقد كتبنا هذه النقطة لتتجنب الوقوع في صنع متاهة بيديك للملفات التي تعمل عليها للعملاء، وقد تبدو هذه بداهة لا يجب الحديث عنها، لكن وجب ذكره والتنبيه إليه من كثرة ما رأينا في هذا الشأن في مجالات الكتابة والترجمة والتصميم والبرمجة كذلك من غياب لهذا التفصيل البسيط. فعاملْ الملفات على حاسوبك على أنها أرشيف كبير أو مكتبة كبيرة وأنت الأمين عليها المراقب لما يدخل ويخرج، فيجب أن تخصص لكل ملف تستخدمه معرِّفًا فريدًا يميزه عن غيره لكن في نفس الوقت يخصص له مكانًا مرتبطًا في فئة تجمعه مع باقي الملفات المشابهة له. فمثلًا إن كنت كاتبًا وتكتب مقالة عن إدارة الملفات أو تترجمها فلا تجعل اسمها "إدارة الملفات"، وتحفظها على سطح المكتب ثم ترسلها إلى العميل، ذلك أنه قد يطلب منك تعديلات فتنفذها ثم ترسلها، فهل تترك اسمها كما هو أم تغيره؟ وإن غيرته فهل تجعله "إدارة الوقت -1" أم ماذا؟ وهذه المشكلة رأيتها بكثرة في مجال التصميم المرئي حين عملت فيه قبل بضعة أعوام، ذلك أني درسته في منحة من معهد مرموق كان المحاضر فيه على علم بما يحدث في السوق الذي سنخرج إليه، فكان يدقق كثيرًا على أسماء الملفات، بل اسم كل طبقة من طبقات العمل وترتيبها داخل الملف، حتى إذا ما أراد المصمم الرجوع إليها للتعديل فيها لم يجد بأسًا ولم يضيع وقتًا. ولعل هذه النقطة مضخمة كثيرًا في حالة المصممين خاصة لكثرة الطبقات التي قد تكون داخل التصميم الواحد، والتي قد تصل للمئات بعضها فوق بعض، ويجب أن تكون بترتيب محدد وإلا اختلف التصميم، والحق أني رأيت هذا وقتها تقعرًا لا داعي له. وحدث بعدها أني عملت بعدها في شركة برمجية، ولما تلقت هذه الشركة هويتها المرئية من شركة دعاية صممتها لها، رأيت أنها بحاجة إلى بعض التعديلات الطفيفة، فأرسلت إلى المصمم ليعدلها فلم ينجح في تنفيذها. ولعجبي من بساطة المطلوب وسوء التنفيذ، فإنني ظللت مع المصمم ذاك قرابة ثلاثة أشهر من أجل تنفيذ تلك التعديلات، ولما أعيتني الحيلة طلبت منه إرسال الملفات المصدرية لأعدل أنا عليها، فلما فتحت الملفات عرفت سبب فشله في تنفيذ تلك التعديلات، إذ كانت طبقات الملفات المصدرية فوضى من الكائنات والأشكال هنا وهناك، بلا أسماء تميزها ولا مجموعات تجمعها، فإن أردت تعديل جزء خربت أجزاءً أخرى، فحتى مع القدرة على الوصول إلى الجزء المراد تعديله بالنقر عليه مثلًا، فإن ترتيبه قد يكون على غير ما تريد بين الكائنات الأخرى. وهذه النقطة يعلم المصممون أنه يجب الاهتمام بها وفق عناصر التصميم، إذ قد يترتب على تعديل جزء ما تعديل عناصر أخرى ترتبط به، فعندئذ تكون تلك العناصر في مجموعة واحدة تتأثر بهذا السلوك، بدلًا من تناثرها هنا وهناك مع فوضوية التعديلات اللاحقة، مما يجعل العمل على الملف بعد أسبوع من تسليمه للعميل كأنه تفكيك لقنبلة لا تدري أي سلك هو الذي سيفجرها في وجهك، ناهيك عما إن عمل عليه زميل لك من بعدك، وتلك مشكلة يتجنبها المبرمجون بالتوثيق المفصل للبرمجيات لئلا يتعبوا من بعدهم، ولتسهيل صيانة البرنامج فيما بعد وتعديله، ولشرحه للعملاء والمستخدمين، وهكذا … . ومشكلة كهذه لا يفترض أن تخرج من مصمم يعمل على مشروع بقيمة 1500 دولار! فكان الأولى أن ينظم ملفاته ليسهل الرجوع إليها فيما بعد وتعديلها، أو حتى الرجوع إليها للاسترشاد في أعمال تالية، فليس من المنطقي إن كنت مترجمًا مثلًا أن تترجم نفس الملف عشرين مرة في حين أنه موجود بالفعل أو أجزاء منه مترجمة على حاسوبك، فإن نظمت ملفات عملك وعرفت ما فيها سهل عليك الرجوع إليها وإعادة استخدامها فيما بعد، بل وتعديلها للعملاء إن أرادوا بعد تسليم المشروع مثلًا، خاصة إن كانت مشاريع كبيرة وتمت في خلال أشهر مثلًا. الفحص الدوري والنسخ الاحتياطي ولا زلنا في الشق الفني لتهيئة بيئة العمل والعناية بها، لكن نعود إلى الجزء الإلكتروني منها، فيما يتعلق بفحص المعدات التي تستخدمها وصيانتها. فلا شك أن تعطل حاسوبك يشبه تعطل السيارة التي تستقلها إلى العمل، فهو أمر غير مقبول حدوثه، وكما أنك ستبحث عن سيارة بديلة في أسرع وقت فيجب أن تكون لديك خطط بديلة كذلك لتجنب حدوث هذا التعطل من ناحية، وللتعامل معه من ناحية أخرى. وهذا يكون بالفحص الدوري كل شهر لحاسوبك ولوحة المفاتيح وغيرها من الأجهزة والتوصيلات الكهربية للتأكد من سلامتها وكفاءة أدائها، بالنظر في المقابس الكهربية والأسلاك للتأكد من عدم وجود علامات مقلقة مثل السخونة الزائدة أو آثار حروق حول المقابس، وكذلك قطع الحاسوب وأزراره وأجزاء بقية العتاد لديك من الشواحن والكاميرا ولاقط الصوت ولوحة المفاتيح وأجهزة الاتصال بالانترنت، ومكتب العمل نفسه والكرسي وغير ذلك. يدخل في هذا الفحص الدوري إجراء نسخ احتياطي لملفاتك المهمة، فإنك لا تدري أي طارئ قد يقع لك أو لحاسوبك، فمن الأفضل إجراء هذا النسخ مرة كل أسبوع مثلًا، أو كل يوم إن استدعى الأمر، فقد خسرت مرة عمل عامين كاملين بسبب إهمالي في النسخ الاحتياطي للقرص الصلب عندي. خلاصة المقال وهكذا بعد أن مررنا سريعًا بهذا المقال، نرجو أن تكون قد عرفت الآن كيف تجهز بيئة العمل وتنظمها وفق ظروف عملك كي تصرف وقتك إلى تنفيذ العمل نفسه، فتوفر لنفسك وقتًا وعمرًا أنت أولى به. في المقال التالي ننظر في الإدارة المالية للمستقل في العمل الحر، كيف يحسب أسعار خدماته وكيف يدير حياته المالية بعد التقاعد. اقرأ أيضًا المقال التالي: الإدارة المالية في العمل الحر المقال السابق: العناية بالصحة الجسدية والنفسية للعامل المستقل دليل حسوب للموظفين: الإعداد الصحيح لمكان العمل. دليل حسوب للموظفين: كيفية الجلوس الصحيحة. كيف توازن بين العائلة والعمل 6 طرق بسيطة لتحسين إنتاجيتك في العمل التعامل مع مشاكل العمل عن بعد النسخة الكاملة من كتاب دليل المستقل والعامل عن بعد 8 طرق لتحقيق الاستفادة القصوى من العمل في البيت أبرز مشاكل العمل الحر والحلول المُقترحة لها
×
×
  • أضف...