المحتوى عن 'إلكونت جافاسكريبت'.



مزيد من الخيارات

  • ابحث بالكلمات المفتاحية

    أضف وسومًا وافصل بينها بفواصل ","
  • ابحث باسم الكاتب

نوع المُحتوى


التصنيفات

  • الإدارة والقيادة
  • التخطيط وسير العمل
  • التمويل
  • فريق العمل
  • دراسة حالات
  • التعامل مع العملاء
  • التعهيد الخارجي
  • السلوك التنظيمي في المؤسسات
  • عالم الأعمال
  • التجارة والتجارة الإلكترونية
  • نصائح وإرشادات
  • مقالات ريادة أعمال عامة

التصنيفات

  • PHP
    • Laravel
    • ووردبريس
  • جافاسكريبت
    • لغة TypeScript
    • Node.js
    • React
    • AngularJS
    • Vue.js
    • jQuery
    • Cordova
  • HTML
    • HTML5
    • إطار عمل Bootstrap
  • CSS
    • Sass
  • SQL
  • لغة C#‎
    • ‎.NET
    • منصة Xamarin
  • لغة C++‎
  • لغة C
  • بايثون
    • Flask
    • Django
  • لغة روبي
    • إطار العمل Ruby on Rails
  • لغة Go
  • لغة جافا
  • لغة Kotlin
  • برمجة أندرويد
  • لغة R
  • الذكاء الاصطناعي
  • صناعة الألعاب
    • Unity3D
  • سير العمل
    • Git
  • سهولة الوصول
  • مقالات برمجة عامة

التصنيفات

  • تصميم تجربة المستخدم UX
  • تصميم واجهة المستخدم UI
  • الرسوميات
    • إنكسكيب
    • أدوبي إليستريتور
  • التصميم الجرافيكي
    • أدوبي فوتوشوب
    • أدوبي إن ديزاين
    • جيمب GIMP
    • كريتا Krita
  • التصميم ثلاثي الأبعاد
    • 3Ds Max
    • Blender
  • نصائح وإرشادات
  • مقالات تصميم عامة

التصنيفات

  • خوادم
    • الويب HTTP
    • قواعد البيانات
    • البريد الإلكتروني
    • DNS
    • Samba
  • الحوسبة السّحابية
    • Docker
  • إدارة الإعدادات والنّشر
    • Chef
    • Puppet
    • Ansible
  • لينكس
    • ريدهات (Red Hat)
  • خواديم ويندوز
  • FreeBSD
  • حماية
    • الجدران النارية
    • VPN
    • SSH
  • شبكات
    • سيسكو (Cisco)
  • مقالات DevOps عامة

التصنيفات

  • التسويق بالأداء
    • أدوات تحليل الزوار
  • تهيئة محركات البحث SEO
  • الشبكات الاجتماعية
  • التسويق بالبريد الالكتروني
  • التسويق الضمني
  • استسراع النمو
  • المبيعات
  • تجارب ونصائح
  • مبادئ علم التسويق

التصنيفات

  • إدارة مالية
  • الإنتاجية
  • تجارب
  • مشاريع جانبية
  • التعامل مع العملاء
  • الحفاظ على الصحة
  • التسويق الذاتي
  • مقالات عمل حر عامة

التصنيفات

  • الإنتاجية وسير العمل
    • مايكروسوفت أوفيس
    • ليبر أوفيس
    • جوجل درايف
    • شيربوينت
    • Evernote
    • Trello
  • تطبيقات الويب
    • ووردبريس
    • ماجنتو
  • أندرويد
  • iOS
  • macOS
  • ويندوز
  • الترجمة بمساعدة الحاسوب
    • omegaT
    • memoQ
    • Trados
  • أساسيات استعمال الحاسوب
  • مقالات عامة

التصنيفات

  • شهادات سيسكو
    • CCNA
  • شهادات مايكروسوفت
  • شهادات Amazon Web Services
  • شهادات ريدهات
    • RHCSA
  • شهادات CompTIA
  • مقالات عامة

أسئلة وأجوبة

  • الأقسام
    • أسئلة ريادة الأعمال
    • أسئلة العمل الحر
    • أسئلة التسويق والمبيعات
    • أسئلة البرمجة
    • أسئلة التصميم
    • أسئلة DevOps
    • أسئلة البرامج والتطبيقات
    • أسئلة الشهادات المتخصصة

التصنيفات

  • ريادة الأعمال
  • العمل الحر
  • التسويق والمبيعات
  • البرمجة
  • التصميم
  • DevOps

تمّ العثور على 6 نتائج

  1. تحدثنا في المقال الرابع عن الكائنات في جافاسكربت، ولدينا في ثقافة البرمجة شيء يسمى بالبرمجة كائنية التوجه، وهي مجموعة تقنيات تستخدم الكائنات والمفاهيم المرتبطة بها مثل مبدأ مركزي لتنظيم البرامج. ورغم عدم وجود إجماع على التعريف الدقيق للبرمجة كائنية التوجه هذه، إلا أنها قد غيرت شكل لغات برمجة كثيرة من حيث تصميمها، بما فيها جافاسكربت، وسنتعرض في هذا المقال للطرق التي يمكن تطبيق أفكار هذا المفهوم في جافاسكربت. التغليف 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 المفتاحية، فلن تعمل الشيفرة. النماذج الأولية Prototypes انظر المثال التالي: let empty = {}; console.log(empty.toString); // → function toString(){…} console.log(empty.toString()); // → [object Object] أرأيت كيف سحبنا خاصيةً من كائن فارغ!؟ حيث لم نزد على الطريقة التي تعمل بها كائنات جافاسكربت في حفظ البيانات الخاصة، لأن أغلب الكائنات لها نماذج أولية إضافةً إلى مجموعة خصائصها، وتلك النماذج الأولية ما هي إلا كائنات أخرى مستخدَمة على أساس مصدر احتياطي fallback للخصائص، فإذا طُلِب من كائن خاصية لا يملكها، فسيُبحث في نموذجه الأولي عن تلك الخاصية، ثم النموذج الأولي لذلك النموذج، وهكذا. طيب، ما النموذج الأولي لذاك الكائن الفارغ؟ إنه object.prototype الذي يسبق الكائنات كلها، فإذا قلنا أنّ علاقات النماذج الأولية في جافاسكربت تكوِّن هيكلًا شجريًا، فسيكون جذر تلك الشجرة هو Object.prototype، إذ يوفِّر بعضَ التوابع التي تظهر في جميع الكائنات الأخرى مثل toString الذي يحول الكائن إلى تمثيل نصي string representation. لا تملك العديد من الكائنات Object.prototype مثل نموذجها الأولي، بل يكون لها كائنٌ آخر يوفر مجموعةً مختلفةً من الخصائص الافتراضية. console.log(Object.getPrototypeOf({}) == Object.prototype); // → true console.log(Object.getPrototypeOf(Object.prototype)); // → null كما تتوقع من المثال السابق، سيُعيد Object.getPrototypeOf النموذج الأولي للكائن. تنحدر الدوال من 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 يحاكي نظام النماذج الأولية في جافاسكربت مفهوم الأصناف 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 على ذلك الكائن لمحاولة إنشاء سلسلة نصية مفيدة منه. كما ذكرنا سابقًا، تعرِّف بعض النماذج الأولية القياسية إصدارًا من 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. اقرأ أيضًا المقال السابق: الدوال في جافاسكريبت. هياكل البيانات: الكائنات والمصفوفات في جافاسكريبت.
  2. يقول دونالد كنوث 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.
  3. تتكلف البرامج بموارد أكثر كلما زاد حجمها، وذلك ليس بسبب الوقت الذي تستغرقه من أجل بنائها، بل لأنّ الحجم الكبير يتبعه تعقيد أكثر، ويحيّر ذلك التعقيد المبرمجين العاملين عليه، حيث تجعلهم تلك الحيرة يرتكبون أخطاءً في صورة زلات برمجية 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. اقرأ أيضًا الدوال في جافاسكريبت. التخاطب بين النوافذ في جافاسكريبت. القيم والأنواع والعوامل في جافاسكريبت
  4. تشكل الأعداد، والقيم البوليانية، والسلاسل، الذرات التي تُبنى منها هياكل البيانات في مجال البرمجة وعلوم الحاسوب، وستحتاج عند عملك في البرمجة إلى أكثر من ذرة واحدة، إذ تسمح لنا الكائنات 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. اقرأ أيضًا المقال التالي: الدوال العليا في جافاسكريبت المقال السابق: هيكل البرنامج في جافاسكريبت
  5. سنبدأ في هذا المقال بفعل الأشياء التي يطلَق عليها برمجة، حيث سنتوسع في أوامر جافاسكربت لنتجاوز الأسماء وأجزاء الجمل التي رأيناها حتى الآن، وذلك لنستطيع كتابة شيفرة مفيدة ذات هدف قابل للتحقيق. التعابير والتعليمات البرمجية كتبنا في المقال الأول قيمًا، وطبقنا عليها عواملًا لنحصل على قيم جديدة، حيث يُعَدّ إنشاء هذه القيم، المادة الأساسية لأي برنامج في جافاسكربت، لكن يجب وضع هذه المادة داخل هيكلٍ أكبر لتصبح مفيدة، وهذا ما سنفعله في هذا المقال. يُسمّى الجزء الذي يُنتِج قيمةً حقيقيةً في الشيفرة البرمجية، تعبيرًا 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.
  6. لا يُنظر في عالم الحاسوب إلى شيء سوى إلى البيانات، حيث يمكنك أن تقرأ البيانات، وتُعدّلها، وتُنشِئ الجديد منها، بينما يُغفَل ذكر ما سواها، وهي متشابهة في جوهرها، إذ أنها تُخزَّن في سلاسل طويلة من البِتَّات 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.