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



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

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

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

نوع المُحتوى


التصنيفات

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

التصنيفات

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

التصنيفات

  • تجربة المستخدم UX
  • واجهة المستخدم UI
  • الرسوميات
    • إنكسكيب
    • أدوبي إليستريتور
    • كوريل درو
  • التصميم الجرافيكي
    • أدوبي فوتوشوب
    • أدوبي إن ديزاين
    • جيمب GIMP
  • التصميم ثلاثي الأبعاد
    • 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

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

  1. ملاحظة مهمة: هذه الشرح لميزات اللغة المتقدمة، إذ تتناول هذه المقالة موضوعًا متقدمًا، لفهم بعض الحالات الهامشية بطريقة أفضل. مع العلم أنها ليست مهمة. والعديد من المطورين ذوي الخبرة الجيدة ينفذون مشاريعهم بدون معرفتها. ولكن إذا أردت معرفة كيفية عمل الأشياء تحت الطاولة فتابع القراءة. إن استدعاء التابع المقيم ديناميكيًا يمكن أن يفقد قيمة this. فمثلًا: let user = { name: "John", hi() { alert(this.name); }, bye() { alert("Bye"); } }; user.hi(); // works // لنستدعي الآن ‫user.hi أو user.bye بحسب الاسم _(user.name == "John" ? user.hi : user.bye)(); // خطأ يوجد في السطر الأخير معامل شرطي يختار إما user.hi أو user.bye. في هذه الحالة تكون النتيجة user.hi. ثم تستدعى الدالّة ذات الأقواس () على الفور لكنها لن تعمل بطريقة صحيحة! كما تلاحظ، نتج عن الاستدعاء بالطريقة السابقة خطأ، وذلك لأن قيمة this داخل الاستدعاء تصبح غير معرفة undefined. نلاحظ هنا أن هذه الطريقة تعمل بصورة صحيحة (اسم الكائن ثم نقطة ثم اسم الدالّة): user.hi(); أما هذه الطريقة فلن تعمل (طريقة تقييم الدالّة): (user.name == "John" ? user.hi : user.bye)(); // Error! لماذا حدث ذلك؟ إذا أردنا أن نفهم سبب حدوث ذلك، فلنتعرف أولًا على كيفية عمل الاستدعاء obj.method()‎. شرح النوع المرجعي بالنظر عن كثب، قد نلاحظ عمليتين في العبارة obj.method()‎: أولًا، تسترجع النقطة '.' الخاصية obj.method. ثم تأتي الأقواس () لتنفّذها. إذًا كيف تنتقل المعلومات الخاصة بقيمة this من الجزء الأول إلى الجزء الثاني؟ إذا وضعنا هذه العمليات في سطور منفصلة، فسنفقدُ قيمة this بكل تأكيد: let user = { name: "John", hi() { alert(this.name); } } // جزّء استدعاء التوابع على سطرين let hi = user.hi; hi(); // خطأ لأنها غير معرّفة إن التعليمة hi = user.hi تضع الدالّة في المتغير، ثم الاستدعاء في السطر الأخير يكون مستقلًا تمامًا، وبالتالي لا يوجد قيمة لـ this. لجعل استدعاءات user.hi()‎ تعمل بصورة صحيحة، تستخدم لغة جافاسكربت خدعة - بأن لا تُرجع النقطة '.' دالة، وإنما قيمة من نوع مرجعي. إن القيمة من نوع مرجعي هو "نوع من المواصفات". لا يمكننا استخدامها بطريقة مباشرة، ولكن تستخدم داخليًا بواسطة اللغة. إن القيمة من نوع مرجعي هي مجموعة من ثلاث قيم (base, name, strict)، ويشير كلُّ منها إلى: base: وهو الكائن. name وهو اسم الخاصية. strict تكون قيمتها true إذا كان الوضع الصارم مفعلًا. إن نتيجة وصول الخاصية user.hi ليست دالة، ولكنها قيمة من نوع مرجعي. بالنسبة إلى user.hi في الوضع الصارم، تكون هكذا: // قيمة من نوع مرجعي (user, "hi", true) عندما تستدعى الأقواس () في النوع المرجعي، فإنها تتلقى المعلومات الكاملة حول الكائن ودواله، ويمكنهم تعيين قيمة this (ستكون user في حالتنا). النوع المرجعي هو نوع داخلي خاص "وسيط"، بهدف تمرير المعلومات من النقطة . إلى أقواس الاستدعاء (). أي عملية أخرى مثل الإسناد hi = user.hi تتجاهل نوع المرجع ككلّ، وتأخذ قيمة user.hi (كدالّة) وتمررها. لذا فإن أي عملية أخرى تفقد قيمة this. لذلك وكنتيجة لما سبق، تُمرر قيمة this بالطريقة الصحيحة فقط إذا استدعيت الدالّة مباشرة باستخدام نقطة obj.method()‎ أو الأقواس المربعة obj['method']()‎ (الصياغتين تؤديان الشيء نفسه). لاحقًا في هذا السلسلة التعليمية، سنتعلم طرقًا مختلفة لحل هذه المشكلة مثل الدالّة func.bind()‎. الخلاصة النوع المرجعي هو نوع داخلي في لغة جافاسكربت. فقراءة خاصية ما، مثل النقطة . فيobj.method()‎ لا تُرجع قيمة الخاصية فحسب، بل تُرجع قيمة من "نوع مرجعي" خاص والّتي تخزن فيها قيمة الخاصية والكائن المأخوذة منه. هذه طريقة لاستدعاء الدالة اللاحقة () للحصول على الكائن وتعيين قيمة this إليه. بالنسبة لجميع العمليات الأخرى، يصبح قيمة النوع المرجعي تلقائيًا قيمة الخاصية (الدالّة في حالتنا). جميع هذه الآليات مخفية عن أعيننا. ولا تهمنا إلا في الحالات الدقيقة، مثل عندما نريد الحصول على تابع ديناميكيًا من الكائن ما، باستخدام تعبير معيّن. المهام التحقق من الصياغة الأهمية: 2 ما هي نتيجة هذه الشيفرة البرمجية؟ let user = { name: "John", go: function() { alert(this.name) } } (user.go)() انتبه. هنالك فخ .anntional__paragraph { border: 3px solid #f5f5f5; margin: 20px 0 14px; position: relative; display: block; padding: 25px 30px; } الحل سينتج عن تنفيذ الشيفرة السابقة خطأ! جربها بنفسك: let user = { name: "John", go: function() { alert(this.name) } } (user.go)() // error! لا تعطينا رسالة الخطأ في معظم المتصفحات فكرة عن الخطأ الّذي حدث. يظهر الخطأ بسبب فقدان فاصلة منقوطة بعد user = {...}‎. إذ لا تُدرج لغة جافاسكربت تلقائيًا فاصلة منقوطة قبل قوس ‎(user.go)()‎، لذلك تُقرأ الشيفرة البرمجية هكذا: let user = { go:... }(user.go)() ثم يمكننا أن نرى أيضًا أن مثل هذا التعبير المشترك هو من الناحية التركيبية استدعاء للكائن ‎{ go: ... }‎ كدالة مع الوسيط (user.go). ويحدث ذلك أيضًا على نفس السطر مع let user، لذلك إن الكائن user لم يُعرّف حتى الآن، ومن هنا ظهر الخطأ. إذا أدخلنا الفاصلة المنقوطة، فسيكون كل شيء على ما يرام: let user = { name: "John", go: function() { alert(this.name) } }; (user.go)() // John يرجى ملاحظة أن الأقواس حول (user.go) لا تفعل شيئًا مميزًا هنا. وهي عادةً تعيد ترتيب العمليات، ولكن هنا تعمل النقطة . أولًا على أي حال، لذلك ليس هناك أي تأثير لها. الشيء الوحيد المهم هو الفاصلة المنقوطة. اشرح قيمة this الأهمية: 3 في الشيفرة أدناه، نعتزم استدعاء التابع obj.go()‎ لأربع مرات على متتالية. ولكن لماذا الندائين (1) و(2) يعملان بطريقة مختلفة عن الندائين (3) و(4)؟ let obj, method; obj = { go: function() { alert(this); } }; obj.go(); // (1) [object Object] (obj.go)(); // (2) [object Object] (method = obj.go)(); // (3) undefined (obj.go || obj.stop)(); // (4) undefined الحل إليك تفسير ما حدث. هذا هو الطريقة العادية لاستدعاء تابع الكائن. نفس الأمر يتكرر هنا، الأقواس لا تغير ترتيب العمليات هنا، النقطة هي الأولى على أي حال. هنا لدينا استدعاء أكثر تعقيدًا ‎(expression).method()‎. يعمل الاستدعاء كما لو كانت مقسمة على سطرين: f = obj.go; // يحسب نتيجة التعبير f(); // يستدعي ما لدينا هنا تُنفذ f()‎ كدالة، بدون this. مثل ما حدث مع الحالة (3) على يسار النقطة . لدينا تعبير. لشرح سلوك (3) و(4) نحتاج إلى أن نتذكر أن توابع الوصول (accessors) للخاصيات (النقطة أو الأقواس المربعة) تُعيد قيمة النوع المرجعي. أي عملية نطبقها عليها باستثناء استدعاء التابع (مثل عملية الإسناد = أو ||) تحولها إلى قيمة عادية، والّتي لا تحمل المعلومات التي تسمح لها بتعيين قيمة this. ترجمة -وبتصرف- للفصل Reference Type من كتاب The JavaScript language
  2. ملاحظة مهمة: هذه إضافة حديثة للغة، لذا قد تحتاج المتصفحات القديمة لترقيع هذا النقص. التسلسل الاختياري .? هو طريقة مقاومة للأخطاء للوصول إلى خصائص الكائن المتداخلة، حتى إذا كانت الخاصية الوسيطة غير موجودة. المشكلة إذا كنت قد بدأت للتو في قراءة هذه السلسلة التعليمية وتتعلم جافاسكربت، فلربما لم تواجه هذه المشكلة بعد، لكنها شائعة جدًا. فمثلًا، يمتلك بعض من المستخدمين لدينا يمتلكون عناوين، لكن هنالك قليل منهم لم يقدمها. لذا لا يمكننا قراءة user.address.street بأمان. هكذا: let user = {}; // تحدث للمستخدم ‫user في حالة كان ليس لديه عنوان alert(user.address.street); // خطأ!‫ أو عند تطويرنا لموقع وِب، ونرغب في الحصول على معلومات حول عنصر ما في الصفحة، لكنه قد لا يكون موجودًا: // إذا كان نتيجة ‫querySelector(...)‎ فارغًا let html = document.querySelector('.my-element').innerHTML; قبل ظهور "‎?.‎" في اللغة، كان يستخدم المعامل && للتغلب على المشكلة. فمثلًا: let user = {}; // إذا كان ‫user لا يملك عنوان alert( user && user.address && user.address.street ); // undefined (أي ليس خطأً) وكونها تتطلب كتابة طويلة للتأكد من وجود جميع المكونات، لذلك كان استخدامها مرهقًا. تسلسل اختياري التسلسل الاختياري "‎?.‎" يوقف التقييم ويعيد غير معرّف undefined إذا كان الجزء قبل "‎?.‎" غير معرّف undefined أو فارغ null. **للإيجاز سنفترض في هذه المقالة أن شيئًا ما "موجود" إذا لم تكن القيمة "فارغة" null أو غير معرّفة undefined. ** إليك الطريقة الآمنة للوصول إلى user.address.street: let user = {}; // إذا كان ‫user لا يملك عنوان alert( user?.address?.street ); // undefined (ليس خطأً) إن قراءة العنوان باستخدام هذه الطريقة user?.address ستعمل حتى ولو كان الكائن user غير موجود: let user = null; alert( user?.address ); // undefined alert( user?.address.street ); // undefined الرجاء ملاحظة أن: صياغة جملة .? تجعل القيمة الموجودة قبلها اختيارية، ولكن ليس القيمة الّتي تأتي بعدها. في المثال أعلاه، تسمح التعليمة "user?.‎" للكائن user فقط بأن يكون غير معرف أو فارغ "null/undefined". من ناحية أخرى، إذا كان الكائن user موجودًا، فيجب أن يحتوي على خاصية user.address، وإلا فإن user?.address.street ستُعطي خطأ في النقطة الثانية. ملاحظة: لا تفرط في استخدام التسلسل الاختياري. يجب أن نستخدم .? فقط في حالة عدم وجود شيئ ما. فمثلًا، بحسب منطق الشيفرة خاصتنا يجب أن يكون الكائن user موجودًا، ولكن الخاصية address اختيارية، بهذه الحالة سيكون user?.address.street أفضل. لذلك، إذا حدث أن كان الكائن user غير معرف بسبب خطأ ما، فسنُعرف عنه ونصلحه. خلاف ذلك، يمكن إسكات أخطاء الترميز عندما لا يكون ذلك مناسبًا، ويصبح تصحيحها أكثر صعوبة. ملاحظة: يجب التصريح عن المتغير الموجود قبل .? إذا لم يكن هناك متغير user على الإطلاق، فإن التعليمة user?.anything ستؤدي حتمًا إلى حدوث خطأ: // ReferenceError: إن المستخدم ‫user غير معرًف user?.address; يجب أن يكون هناك تعريف واضح للمتغير let / const / var user. لأن التسلسل الاختياري يعمل فقط للمتغيرات المصرح عنها. اختيار الطريق الأقصر كما قلنا سابقًا، ستوقف .? فورًا (أي سيحدث قصر في الدارة) إذا لم يكن الجزء الأيسر موجودًا. لذلك، إذا كان هناك أي استدعاءات دوالّ أخرى أو آثار جانبية، فلن تحدث: let user = null; let x = 0; user?.sayHi(x++); // لا يحدث شيئ alert(x); // ‫0, لم تزداد القيمة حالات أخرى ()?. و []?. إن التسلسل الاختياري ‎.? ليس مُعامل، ولكنه طريقة معينة لصياغة تعليمة، يعمل أيضًا مع الدوالّ والأقواس المربعة. فمثلًا، تُستخدم ().? لاستدعاء دالّة قد لا تكون موجودة. نلاحظ في الشيفرة أدناه، أنه لدى بعض مستخدمينا التابع admin والبعض ليس لديه: let user1 = { admin() { alert("I am admin"); } } let user2 = {}; user1.admin?.(); // I am admin user2.admin?.(); هنا، في كلا السطرين، نستخدم النقطة . أولًا للحصول على خاصية admin، لأن كائن المستخدم user يجب أن يكون موجودًا، لذا فهو آمن للقراءة منه. ثم يتحقق ().? من الجزء الأيسر: إذا كانت دالّة المسؤول موجودة، فستنفذ (على الكائن user1). وبخلاف ذلك (بالنسبة للكائن user2) يتوقف التقييم الشيفرة بدون أخطاء. تعمل الصياغة [].? أيضًا، إذا أردنا استخدام الأقواس [] للوصول إلى الخصائص بدلًا من النقطة .. على غرار الحالات السابقة، فإنه يسمح بقراءة خاصية بأمان من كائن قد لا يكون موجودًا. let user1 = { firstName: "John" }; let user2 = null; // Imagine, we couldn't authorize the user let key = "firstName"; alert( user1?.[key] ); // John alert( user2?.[key] ); // undefined alert( user1?.[key]?.something?.not?.existing); // undefined كما يمكننا استخدام .? مع delete: delete user?.name; // احذف المستخدم ‫user.name إذا كان موجودًا ملاحظة: يمكننا استخدام .? للقراءة الآمنة والحذف، ولكن ليس الكتابة التسلسل الاختياري .? ليس له أي فائدة في الجانب الأيسر من المهمة: // فكرة الشيفرة أدناه أن تكتب قيمة ‫user.name إذا لم تكن موجودة user?.name = "John"; // خطأ لن تعمل الشيفرة // لأن الإسناد بين ‫undefined = "John"‎ الخلاصة يتكون بناء جملة التسلسل الاختياري .? من ثلاثة أشكال: obj?.prop - ستُعيد obj.prop إذا كان الكائن obj موجودًا، وإلا ستعيد القيمة undefined. obj?.[prop]‎ - ستُعيد obj[prop]‎ إذا كان الكائن obj موجودًا، وإلا ستُعيد undefined. obj?.method()‎ - تستدعي obj.method()‎ إذا كان الكائن obj موجودًا، وإلا ستُعيد undefined. كما رأينا كل الطرق واضحة وسهلة الاستخدام. تتحقق .? من الجزء الأيسر بحثًا عن قيمة فارغة أو غير معرفة null/undefined ويسمح للتقييم بالمتابعة إذا لم يكن كذلك. تسمح سلسلة .? بالوصول الآمن إلى الخصائص المتداخلة. ومع ذلك، يجب أن نطبق .? بحذر، وفقط في الحالات الّتي يكون فيها الجزء الأيسر غير موجود، حتى لا نخفي عن أنفسنا الأخطاء البرمجية إذا حدثت. ترجمة -وبتصرف- للفصل Optional chaining '?.' من كتاب The JavaScript language
  3. أحد الاختلافات الأساسية بين الكائنات (objects) وأنواع البيانات الأولية (primitives) هو تخزينها ونسخها "بالطريقة المرجعية" (by reference). قيم أنواع البيانات الأولية: هي سلاسل وأرقام وقيم منطقية - تُسند أو تنسخ "كقيمة كاملة". فمثلًا: let message = "Hello!"; let phrase = message; نتيجة لتنفيذ الشيفرة السابقة لدينا متغيرين مستقلين، كلّ واحد يُخزن السلسلة "Hello!‎". أما الكائنات ليست كذلك. لا يخزن المتغيّر الكائن نفسه، وإنما "عنوانه في الذاكرة"، بمعنى آخر "مرجع" له. هذا هو وصف الكائن: let user = { name: "John" }; هنا يُخزن الكائن في مكان ما في الذاكرة. والمتغير user له "مرجع" له. عند نسخ متغير نوعه كائن - ينسخ المرجع، ولا يتم تكرار الكائن. فمثلًا: let user = { name: "John" }; let admin = user; // نسخ المرجع الآن لدينا متغيرين، كل واحد له إشارة مرجعية لنفس الكائن: يمكننا استخدام أي متغيّر للوصول إلى الكائن وتعديل محتوياته: let user = { name: 'John' }; let admin = user; admin.name = 'Pete'; // غيرت القيمة من خلال المتغير ‫admIn المرجعي alert(user.name); // ‫'Pete' ظهر التغيير على المتغير "user" المرجعي يوضح المثال أعلاه أن هناك كائنًا واحدًا فقط. كما لو كان لدينا خزانة لها مفتاحان واستخدمنا أحدهما وهو (admin) للوصول إليها. بعد ذلك، إذا استخدمنا لاحقًا مفتاحًا آخر وهو (user)، فيمكننا رؤية التغييرات. الموازنة بحسب المرجع إن معاملات المساواة == والمساواة الصارمة === الخاصة بالكائنات تعمل بأسلوب واحد. الكائنين يكونان متساويان فقط إذا كانا يشيران لنفس الكائن. هنا يشير المتغيرين لنفس الكائن، وبالتالي فإنهما متساويان: let a = {}; let b = a; // نسخ المرجع alert( a == b ); // ‫true, كِلا المتغيّرين يشيران إلى الكائن نفسه alert( a === b ); // true وهنا نلاحظ أن الكائنان مستقلان ولذلك غير متساويين،علمًا أن كلاهما فارغ: let a = {}; let b = {}; // كائنان منفصلان alert( a == b ); // false لإجراء المقارنات مثل obj1> obj2 أو المقارنة مع قيمة obj == 5 البدائي، تحولّ الكائنات لعناصر أولية. سوف ندرس كيفية عمل تحويل الكائنات قريبًا، ولكن في الحقيقة، نادرًا ما تحدث مثل هذه المقارنات، عادةً نتيجة لخطأ في الشيفرة البرمجية. الاستنساخ والدمج يؤدي نسخ متغير يخزن كائن إلى إنشاء مرجع آخر لنفس الكائن. ولكن ماذا لو احتجنا إلى تكرار كائنٍ ما؟ إنشاء نسخة مستقلة تمامًا، أي استنساخه؟ هذا ممكن أيضًا، ولكنه أكثر صعوبة نوعًا ما، لأنه لا توجد طريقة مضمنة في لغة جافاسكربت لتأدية ذلك. في الواقع، نادرًا ما تكون هناك حاجة لذلك. وإن نسخ المرجع جيد في معظم الأوقات. ولكن إذا كنا نريد ذلك حقًا، فنحن بحاجة لإنشاء كائن جديد وتكرار بُنية الكائن الذي نريد نسخه من خلال تكرار خصائصه ونسخها على مستوى قيم الأنواع البدائية. هكذا: let user = { name: "John", age: 30 }; let clone = {}; // كائن فارغ جديد // ‫لننسخ كلّ الخاصيات الموجودة في user إليه for (let key in user) { clone[key] = user[key]; } // الآن الكائن ‫clone هو نسخة مستقلة تمامًا لديه نفس خاصيات الكائن user clone.name = "Pete"; // غيرنا البيانات بداخله alert( user.name ); // ما يزال الاسم ‫John في الكائن الأساسي كما يمكننا استخدام الطريقة Object.assign لذلك. وتكون صياغتها هكذا: Object.assign(dest, [src1, src2, src3...]) الوسيط الأول dest هي الكائن المستهدف. الوسيط التالي src1, ..., srcN (يمكن أن تكون كثيرة بحسب الحاجة) هي كائنات مصدر. تُنسخ خصائص جميع الكائنات المصدر src1، ...، srcN إلى الهدف dest. بمعنى آخر، تنسخ خصائص جميع الوسطاء الّتي تبدأ من الثانية في الكائن الأول. يعيد الاستدعاء الكائن dest. فمثلًا، يمكننا استخدامه لدمج عدة كائنات في واحد: let user = { name: "John" }; let permissions1 = { canView: true }; let permissions2 = { canEdit: true }; // نسخ جميع الخاصيات من ‫permissions1 و permissions2 إلى user Object.assign(user, permissions1, permissions2); // now user = { name: "John", canView: true, canEdit: true } إذا كان اسم الخاصية المنسوخة موجودًا بالفعل، فعندها سيُستبدل: let user = { name: "John" }; Object.assign(user, { name: "Pete" }); alert(user.name); // now user = { name: "Pete" } يمكننا أيضًا استخدام التابع Object.assign لاستبدال حلقة for..in للاستنساخ البسيط: let user = { name: "John", age: 30 }; let clone = Object.assign({}, user); تُنسخ جميع خصائص الكائن user في الكائن الفارغ ويُعاد. الاستنساخ متداخل حتى الآن افترضنا أن جميع خصائص الكائن user تكون قيم أولية. لكن ماذا عن الخصائص يمكن أن تكون مراجع لكائنات أخرى؟ كيف سنتعامل معها؟ هكذا: let user = { name: "John", sizes: { height: 182, width: 50 } }; alert( user.sizes.height ); // 182 الآن لا يكفي نسخ clone.sizes = user.sizes، لأن user.sizes كائن، سينسخه بالطريقة المرجعية. لذا فإن clone وuser سيتشاركان نفس الأحجام: هكذا: let user = { name: "John", sizes: { height: 182, width: 50 } }; let clone = Object.assign({}, user); alert( user.sizes === clone.sizes ); // ‫true, نفس الكائن // إن ‫user و clone لديهما نفس الحجم user.sizes.width++; // غير خاصية من مكان معين alert(clone.sizes.width); // ‫51, تفقد هل الخاصية تغيرت في المكان الآخر (بالتأكيد) لإصلاح ذلك، يجب علينا استخدام حلقة الاستنساخ والّتي تفحص كلّ قيمة من قيم user[key]‎، فإذا كانت كائنًا، فعندها تُكرر هيكلها أيضًا. وهذا ما يسمى الاستنساخ العميق (Deep Cloning). هناك خوارزمية قياسية للاستنساخ العميق تتعامل مع الحالة المذكورة أعلاه وحالات أكثر تعقيدًا، تسمى خوارزمية الاستنساخ المهيكل (Structured cloning algorithm). كما يمكننا أيضًا استخدام العودية لتنفيذها. أو ببساطة ليس علينا إعادة اختراع العجلة، إذ يمكننا أخذ تطبيق جاهز لهذا الأمر، مثل التابع ‎_.cloneDeep(obj)‎ الموجود في مكتبة Lodash. الخلاصة تُسند الكائنات وتنسخ بالطريقة المرجعية. بمعنى آخر، لا يخزن المتغير "قيمة الكائن"، ولكن "المكان المرجعي" (العنوان في الذاكرة) للقيمة. لذا فإن نسخ مثل هذا المتغير أو تمريره كوسيط لدالة يُنسخ هذا المرجع وليس الكائن. تُنفذُ جميع العمليات من خلال المراجع المنسوخة (مثل إضافة / إزالة الخاصيات) على نفس الكائن الفردي. لعمل "نسخة حقيقية" (نسخة مستقلة)، يمكننا استخدام Object.assign لما يسمى "بالنسخة السطحية" (إذ تُنسخ الكائنات المُتداخلة بالطريقة المرجعية) أو يمكننا النسخ من خلال دالّة "الاستنساخ العميق"، مثل الدالّة ‎_.cloneDeep(obj)‎. ترجمة -وبتصرف- للفصل Object copying, references من كتاب The JavaScript language
  4. ملاحظة مهمة: هذه إضافة حديثة للغة، لذلك تحتاج بعض المتصفحات القديمة لترقيع هذا النقص لأن ما سنشرحه هو إضافة حديثة للغة. يوفر عامل الاستبدال اللاغي ?? (Nullish coalescing operator) صيغة قصيرة لاختيار أول متغير معرّف (defined) من القائمة. نتيجة a ?? b هو: سيُعيد a إذا لم تكن فارغة null أو غير معرّفة undefined، وإلا سيُعيد b. إذًا x = a ?? b هي اختصار: x = (a !== null && a !== undefined) ? a : b; إليك مثال أطول لتوضيح الأمر. تخيل أن لدينا مستخدم، وهناك متغيرات FirstName وlastName أو nickName لاسمهم الأول واسم العائلة أو اللقب. وويمكن أن تكون جميعها غير معرفة (undefined)، إذا قرر المستخدم عدم إدخال أي قيمة. نرغب في عرض اسم المستخدم: في حال أدخل أحد هذه المتغيرات الثلاثة، أو إظهار الإسم "مجهول" إذا لم يعيّن أي شيئ. دعنا نستخدم العامل ؟؟ لتحديد أول متغيّر معرّف: let firstName = null; let lastName = null; let nickName = "Supercoder"; // سيعرض أول قيمة غير فارغة أو غير معرفة alert(firstName ?? lastName ?? nickName ?? "Anonymous"); // Supercoder استخدام العامل || يمكن استخدام العامل || بنفس طريقة استخدام ??. في الواقع، أي يمكننا استعمال || مكان ؟؟ في الشيفرة أعلاه والحصول على نفس النتيجة، كما شرحناها في مقال العاملات المنطقية في لغة جافاسكربت. الفرق المهم هو: إن || تُعيد القيمة الحقيقية الأولى. بينما تُعيد ?? أول قيمة معرّفة. هذا مهم جدًا عندما نرغب في التعامل مع القيم غير المعرفة أو الفارغة (null/undefined) بطريقة مختلفة عن القيمة 0. فمثلًا، إليك هذا الحالة: height = height ?? 100; تعيّن هذه الشيفرة البرمجية المتغير height بالقيمة 100 في حال كان غير معرّف. دعونا نوازنه بالعامل ||: let height = 0; alert(height || 100); // 100 alert(height ?? 100); // 0 هنا، height || 100 يعامل الارتفاع الصفري على أنه غير معرّف، تمامًا مثل القيمة الفارغة null أو غير المعرفة undefined أو أي قيمة خاطئة أخرى. إذًا ستكون النتيجة هي 100. أما height ?? 100 يُعيد 100 فقط إذا كان المتغيّر height فارغًا null أو غير معرّف undefined. لذلك ستعرضُ الشيفرة السابقة قيمة الارتفاع 0 كما هي. يعتمد السلوك الأفضل على حالة الاستخدام الّتي نواجهها. عندما يكون الارتفاع صفر تكون القيمة صالحة، فمن الأفضل أن يكون العامل ??. أولوية العامل ?? إن أولوية العامل ?? منخفضة نوعًا ما: 5 في جدول MDN. إذًا تقيّم ?? بعد معظم العمليات الأخرى، ولكن قبل = و ?. إذا احتجنا إلى اختيار قيمة بالعامل ?? في تعبير معقد، ففكر في إضافة الأقواس، هكذا: let height = null; let width = null; // هام: استخدم الأقواس let area = (height ?? 100) * (width ?? 50); alert(area); // 5000 وإلا إذا حذفنا الأقواس، فإن عملية الضرب * لها أسبقية أعلى عامل ?? وستُنفذ قبلها. سيكون ذلك مشابه لهذا المثال: // يمكن أن تكون غير صحيحة let area = height ?? (100 * width) ?? 50; هناك أيضًا قيود متعلقة باللغة. لأسباب تتعلق بالسلامة، يُحظر استخدام العامل ?? مع العاملات && و ||. لاحظ الخطأ في الصياغة الموجود في الشيفرة أدناه: let x = 1 && 2 ?? 3; // خطأ في الصياغة بالتأكيد إن القيد قابل للنقاش، ولكنه أُضيف إلى المواصفات القياسية للغة بغرض تجنب الأخطاء البرمجية، إذ يبدأ الناس في التبديل من || إلى ??. استخدم الأقواس الصريحة لتجنب الأمر، هكذا: let x = (1 && 2) ?? 3; // تعمل دون أخطاء alert(x); // 2 الخلاصة يوفر عامل الاستبدال اللاغي ?? طريقة مختصرة لاختيار قيمة "معرّفة" من القائمة. يستخدم لتعيين القيم الافتراضية للمتغيرات: // أسند القيمة 100 إلى المتغير ‫height إذا كان هذا الأخير فارغًا أو غير معرًف height = height ?? 100; عامل ?? له أولوية منخفضة جدًا، وأعلى قليلًا من العاملات ? و=. يحظر استخدامه مع العاملات || أو && بدون أقواس صريحة. ترجمة -وبتصرف- للفصل Nullish coalescing operator '??'‎ من كتاب The JavaScript language
  5. تحذير: إضافة حديثة للغة هذه إضافة حديثة للغة. يمكنك العثور على الحالة الحالية للدعم من هنا. الأعداد الكبيرة BigInt هو متغيّر عدديّ خاص، يوفر دعمًا للأعداد الصحيحة ذات الطول العشوائي. تُنشأ الأعداد الكبيرة من خلال إلحاق الحرف n بنهاية العدد العادي، أو من خلال استدعاء الدالّة BigInt والّتي بدورها ستُنشئ عدد كبير من السلاسل أو الأعداد العادية وما إلى ذلك. const bigint = 1234567890123456789012345678901234567890n; const sameBigint = BigInt("1234567890123456789012345678901234567890"); const bigintFromNumber = BigInt(10); // ‫مشابه تمامًا للطريقة 10n المعاملات الرياضية عمومًا يمكننا استخدام العدد الكبير مثل العدد العادي، فمثلًا: alert(1n + 2n); // 3 alert(5n / 2n); // 2 الرجاء ملاحظة أن القسمة 5/2 تُعيد نتيجة مُقرّبة للصفر، بدون الجزء العشري. جميع العمليات على الأعداد الكبيرة ستُعيد أعداد كبيرة. لا يمكننا جمع الأعداد الكبيرة مع الأعداد العادية: alert(1n + 2); // ‫خطأ: لايمكننا جمع الأعداد الكبيرة مع الأعداد العادية يجب علينا تحويلها بطريقة واضحة إن لزم الأمر: باستخدام BigInt()‎ أو Number()‎، هكذا: let bigint = 1n; let number = 2; // تحويل عدد عادي إلى عدد كبير alert(bigint + BigInt(number)); // 3 // تحويل عدد كبير إلى عدد عادي alert(Number(bigint) + number); // 3 تكون عمليات التحويل صامتة دائمًا، ولا تخطئ أبدًا، ولكن إذا كانت الأعداد الكبيرة ذات حجم كبير جدًا بحيث لن تتناسب مع العدد العادي، فستُحذف البتات الإضافية من العدد الكبير، لذلك يجب أن نكون حذرين عند إجراء مثل هذه التحويلات. ملاحظة: معامل الجمع الأحادي لا يطبق على الأعداد الكبيرة المعامل ‎+value: هو طريقة معروفة لتحويل المتغير value إلى رقم. لا تدعم الأعداد الكبيرة هذه الطريقة لتجنب الفوضى: let bigint = 1n; alert( +bigint ); // خطأ لذلك يجب أن نستخدم Number()‎ لتحويل العدد الكبير إلى عدد عادي. عمليات الموازنة تعمل عمليات الموازنة، مثل: < و> مع الأعداد الكبيرة والعادية على حدٍ سواء: alert( 2n > 1n ); // true alert( 2n > 1 ); // true لاحظ أنه نظرًا لأن الأعداد العادية والأعداد الكبيرة تنتميان لأنواع مختلفة، فيمكن أن تكون متساوية ==، ولكن ليست متساوية تمامًا ===: alert( 1 == 1n ); // true alert( 1 === 1n ); // false العمليات المنطقية تتصرف الأعداد الكبيرة مثل الأعداد العادية عندما تكون fداخل الجملة الشرطية if أو أي عمليات منطقية الأخرى. فمثلًا، في الجملة الشرطية if أدناه، تكون قيمة 0n خاطئة، والقيم الأخرى صحيحة: if (0n) { // لن يُشغّل مطلقًا } تعمل المعاملات المنطقية مثل: || و&& وغيرهما مع الأعداد الكبيرة بطريقة مشابهة للأعداد العادية: alert( 1n || 2 ); // 1 (1n is considered truthy) alert( 0n || 2 ); // 2 (0n is considered falsy) ترقيع مشاكل نقص الدعم تعد عملية ترقيع الدعم للأعداد الكبيرة صعبة بعض الشيء. والسبب هو أن العديد من معاملات جافاسكربت، مثل: + و - وما إلى ذلك تتصرف بطريقة مختلفة مع الأعداد الكبيرة بالموازنة مع الأعداد العادية. فمثلًا، تُعيد عملية القسمة على الأعداد الكبيرة دائمًا أعداد كبيرة (وتقربها إن لزم الأمر). لتنفيذ مثل هذه السلوك عن طريق ترقيع نقص الدعم في هذه الحالة سنحتاج لتحليل الشيفرة واستبدال جميع هذه المعاملات بالدوالّ المناسبة. لكن القيام بذلك أمر مُرهق وسيُكون على حساب انخفاض الأداء. بالإضافة إلى ذلك لا يوجد طريقة شائعة ومعتمد بين مجتمع المطورين لترقيع نقص الدعم. على الرغم من ذلك، هنالك بعض المحاولات الجيدة لحل هذه المشكلة مثل المكتبة JSBI. تزودنا هذه المكتبة بالأعداد الكبيرة من خلال توابعها الخاصة. ويمكننا استخدامها بدلًا من الأعداد الكبيرة الأصيلة -بدون أي طريقة لترقيع لنقص الدعم-: العملية في مكتبة JSBI العملية في الأعداد الكبيرة الأصيلة العملية a = JSBI.BigInt(789)‎ a = BigInt(789)‎ إنشاء عدد كبير من عدد عادي c = JSBI.add(a, b)‎ c = a + b الجمع c = JSBI.subtract(a, b)‎ c = a - b الطرح ... ... ... … ثم نستخدم لترقيع الدعم ملحقات إضافية (مثل الملحق الإضافي Babel) وذلك لتحويل استدعاءات المكتبة JSBI إلى الأعداد الكبيرة الأصيلة للمتصفحات التي تدعمها. بتعبير آخر، يقترح هذا النهج أن نكتب الشيفرة في JSBI بدلًا من الأعداد الكبيرة الأصيلة. وتتعامل مكتبة JSBI مع الأعداد العادية والأعداد الكبيرة داخليًا، إذ تحاكي طريقة الاستخدام الصحيحة باتباع المواصفات عن كثب، لذلك ستكون الشيفرة جاهزة للتعامل مع الأعداد الكبيرة. يمكننا استخدام شيفرة البرمجية للمكتبة JSBI على حالها للمحركات التي لا تدعم الأعداد الكبيرة والّتي تدعمها على حدٍ سواء - إذ ستُرقع نقص الدعم بتحويل الاستدعاءات إلى الأعداد الكبيرة الأصيلة. المصادر موقع مطوري موزيلا MDN. المواصفات القياسية للمتغيّر. ترجمة -وبتصرف- للفصل BigInt من كتاب The JavaScript language table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; }
  6. مفهوم Currying هو تقنية متقدمة للعمل مع الدوالّ. يستخدم في العديد من اللغات البرمجية الأخرى من بينهم جافاسكربت. Currying عبارة عن طريقة لتحويل الدوالّ التي تقيم الدالّة ذات الاستدعاء-أكثر من وسيط- f (a، b، c)‎ لتصبح قابلة للاستدعاء -بوسيط واحد- هكذا f(a)(b)(c)‎. تحول تقنية Currying الدالّة فقط ولا تستدعها. لنرى في البداية مثالًا، لفهم ما نتحدث عنه فهمًا أفضل، وبعدها ننتقل للتطبيقات العملية. سننشئ دالة مساعدة باسم curry (f)‎ والتي ستُنفذّ تقنية Currying على الدالّة f التي تقبل وسيطين. بتعبير آخر، تحول الدالة curry(f)‎ الدالّة f(a, b)‎ ذات الوسيطين إلى دالة تعمل كوسيط واحدf(a)(b)‎: function curry(f) { // ‫الدالة curry(f)‎ هي من ستُنفذّ تحويل currying return function(a) { return function(b) { return f(a, b); }; }; } // طريقة الاستخدام function sum(a, b) { return a + b; } let curriedSum = curry(sum); alert( curriedSum(1)(2) ); // 3 كما نرى، فإن التنفيذ بسيط: إنه مجرد مغلفين للوسطاء. نتيجة curry(func)‎ هي دالة مغلّفة function(a)‎. عندما تسمى هكذا curriedSum(1)‎، تُحفظ الوسطاء في البيئة اللغوية للجافاسكربت (وهي نوع مواصفات اللغة تستخدم لتعريف ارتباط المعرّفات بالمتغيّرات والدوالّ المحددة وذلك بناءً على بنية الترابط اللغوية في شيفرة ECMAScript)، وتعيد غلاف جديد function(b)‎. ثمّ يُسمى هذا المغلّف باسم 2 نسبةً لوسطائه، ويُمرّر الاستدعاء إلى الدالّة sum(a,b)‎ الأصليّة. من الأمثلة المتقدمة باستخدام تقنية currying هو _curry من مكتبة Lodash، والتي تُعيد غِلافًا الّذي يسمح باستدعاء الدالّة طبيعيًا وجزئيًا: function sum(a, b) { return a + b; } let curriedSum = _.curry(sum); // ‫استخدام ‎_.curry من مكتبة lodash alert( curriedSum(1, 2) ); // ‫النتيجة: 3، لايزال بإمكاننا استدعاؤه طبيعيًا alert( curriedSum(1)(2) ); // ‫النتيجة: 3، الاستدعاء الجزئي لماذا نحتاج لتقنية currying؟ لابد لنا من مثالٍ واقعي لفهم فوائد هذه التقنية. مثلًا، ليكن لدينا دالّة التسجيل log(date, importance, message)‎ والّتي ستُنسّق المعلومات وتعرضها. مثل هذه الدوالّ مفيدة جدًا في المشاريع الحقيقة مثل: إرسال السجلات عبر الشبكة، في مثالنا سنستخدم فقط alert: function log(date, importance, message) { alert(`[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`); } لنُنفذ تقنية currying عليها! log = _.curry(log); بعد ذلك ستعمل دالّة log وفق المطلوب: log(new Date(), "DEBUG", "some debug"); // log(a, b, c) … ولكنها تعمل أيضًا بعد تحويلها بتقنية currying: log(new Date())("DEBUG")("some debug"); // log(a)(b)(c) الآن يمكننا بسهولة إنشاء دالّة مناسبة للسجلات الحالية: // ‫logNow سيكون دالة جزئية من log مع وسيط أول ثابت let logNow = log(new Date()); // استخدامه logNow("INFO", "message"); // [HH:mm] INFO message الآن logNow هو نفس الدالة log بوسيط أول ثابت، بمعنى آخر "دالّة مطبقة جزئيًا" أو "جزئية" للاختصار. يمكننا المضي قدمًا وإنشاء دالّة مناسبة لسجلات تصحيح الأخطاء الحالية: let debugNow = logNow("DEBUG"); debugNow("message"); // [HH:mm] DEBUG message إذا: لم نفقد أي شيء بعد التحويل بتقنية currying: ولا يزال يمكننا أيضًا استدعاء الدالّة log طبيعيًا. يمكننا بسهولة إنشاء دوالّ جزئية مثل: سجلات اليوم. الاستخدام المتقدم لتقنية currying في حالة رغبتك في الدخول في التفاصيل، إليك طريقة الاستخدام المتقدمة لتقنية currying للدوال ذات الوسطاء المتعددة والّتي يمكننا استخدامها أعلاه. وهي مختصرة جدًا: function curry(func) { return function curried(...args) { if (args.length >= func.length) { return func.apply(this, args); } else { return function(...args2) { return curried.apply(this, args.concat(args2)); } } }; } إليك مثالًا لطريقة استخدامه: function sum(a, b, c) { return a + b + c; } let curriedSum = curry(sum); alert( curriedSum(1, 2, 3) ); // 6, still callable normally alert( curriedSum(1)(2,3) ); // 6, currying of 1st arg alert( curriedSum(1)(2)(3) ); // 6, full currying تبدو تقنية currying للوهلة الأولى معقدةً، ولكنها في الحقيقة سهلة الفهم جدًا. نتيجة استدعاء curry(func)‎ هي دالّة مُغلّفة curried والّتي تبدو هكذا: // func is the function to transform function curried(...args) { if (args.length >= func.length) { // (1) return func.apply(this, args); } else { return function pass(...args2) { // (2) return curried.apply(this, args.concat(args2)); } } }; عند تشغيله، هناك فرعين للتنفيذ من الجملة الشرطية if: سيكون الاستدعاء الآن هكذا: إن كان عدد الوسطاء args المُمرّرة هو نفس العدد الدالة الأصليّة المعرّفة لدينا (func.length) أو أكثر، عندها نمرّر الاستدعاء له فقط. وإلا سيكون الاستدعاء جزئيًا: لم تُستدعى الدالّة func بعد. وإنما أعيد بدلًا منها دالّة المغلِّفة أخرى pass، والتي ستُعيد تطبيق الدالة curried مع تقديم الوسطاء السابقين مع الوسطاء الجدد. وثمّ في استدعاء الجديد سنحصل إما على دالة جزئية جديدة (إن لم يكُ عدد الوسطاء كافي) أو النتيجة النهائية. لنرى مثلًا ما يحدث في حال الاستدعاء الدالة هكذا sum(a, b, c)‎. أي بثلاث وسطاء، وبذلك يكون sum.length = 3. عند استدعاء curried(1)(2)(3)‎: الاستدعاء الأول curried (1)‎ تحفظ 1 في بيئته اللغوية، ويُعيد دالّة المغلف pass. يُستدعى المغلّف pass مع الوسيط المُمرّر (2): إذ يأخذ الوسطاء السابقين (1)، ويدمجهم مع الوسيط الذي حصل عليه وهو (2) ويستدعي الدالّة curried(1, 2)‎ مع استخدام جميع ما حصل عليه من وسطاء. وبما أن عدد الوسطاء لا يزال أقل من 3 ، فإن الدالّة curry ستُعيد الدالّة pass. يُستدعى المغلّف pass مرة أخرى مع الوسيط المُمرّر (3): ومن أجل الاستدعاء التالي pass (3)‎ سيأخذ الوسطاء السابقين (1, 2) ويضيف لهم الوسيط 3، ليكون الاستدعاء هكذا curried(1, 2, 3)‎- أخيرًا لدينا ثلاث وسطاء، والّذين سيمرّروا للدالّة الأصلية. إذا لم تتوضح الفكرة حتى الآن ، فما عليك إلا تتبع تسلسل الاستدعاءات في عقلك أو على الورقة وستتوضح الأمور أكثر. ملاحظة: تعمل مع الدوالّ ثابتة الطول فقط يجب أن يكون للدالّة عدد ثابت من الوسطاء لتطبيق تقنية currying. إن استخدمت دالّةّ ما معاملات البقية، مثل: f(...args)‎، فلا يمكن معالجتها بهذه التقنية. ملاحظة: أكثر بقليل من مجرد تقنية تحويل انطلاقًا من التعريف، يجب على تقنية currying تحويل الدالّة sum(a, b, c)‎ إلى sum(a)(b)(c)‎. لكن غالبية تطبيقات هذه التقنية في جافاسكربت متقدمة، وكما وضحنا سابقًا: فهي تحافظ على الدالّة قابلة للاستدعاء بعدة تنويعات للوسطاء المُمرّرة. خلاصة تقنية Currying هو عملية تحويل تجعل f(a,b,c)‎ قابلة للاستدعاء كـ f(a)(b)(c)‎. عادةً ما تحافظ تطبيقات الجافاسكربت على الدوالّ بحيث تكون قابلة للاستدعاء بالشكل الطبيعي أو الجزئي إن كان عدد الوسطاء غير كافٍ. كما تسمح لنا هذه التقنية أيضًا بالحصول على دوالّ جزئية بسهولة. كما رأينا في مثال التسجيل، بعد تنفيذ هذه التقنية على الدالّة العالمية ذات الثلاث وسطاء log(date, importance, message)‎ فإن ذلك سيمنحنا دوالّ جزئية عند استدعاؤها باستخدام وسيط واحد (هكذا log(date)‎) أو وسيطين (هكذا log(date, importance)‎). ترجمة -وبتصرف- للفصل Currying من كتاب The JavaScript language
  7. تنفذ الدالّة Eval المضمّنة في اللغة الشيفرات البرمجية المُمرّرة لها كسلسلة نصية string. وصياغتها هكذا: let result = eval(code); فمثلًا: let code = 'alert("Hello")'; eval(code); // Hello يمكن أن تكون الشيفرة المُمررة للدالّة كبيرة وتحتوي على فواصل أسطر وتعريف دوالّ ومتغيّرات، وما إلى ذلك. ولكن نتيجة الدالّة Eval هي نتيجة أخر عبارة منفذة في الشيفرة. وإليك المثال التالي: let value = eval('1+1'); alert(value); // 2 let value = eval('let i = 0; ++i'); alert(value); // 1 تُنفذّ الشيفرة في البيئة الحالية للدالّة، ولذا فيمكنها رؤية المتغيرات الخارجية: let a = 1; function f() { let a = 2; eval('alert(a)'); // 2 } f(); كما يمكنها تعديل المتغيّرات الخارجية أيضًا: let x = 5; eval("x = 10"); alert(x); // النتيجة: ‫10، تعدلت القيمة بنجاح في الوضع الصارم، تملك الدالّة Eval بيئة متغيّرات خاصة بها. لذا فلن تظهر الدوالّ والمتغيرات، المعرفة -داخل الدالة- للخارج وإنما ستبقى بداخلها: // تذكر أن في الوضع الصارم يُشغّلُ تلقائيًا في الأمثلة الحيّة eval("let x = 5; function f() {}"); alert(typeof x); // undefined (المتحول غير مرئي هنا) // ‫الدالّة f غير مرئية هنا أيضًا بدون تفعيل "الوضع صارم"، لن يكون للدالّة Eval بيئة متغيرات خاصة بها، ولذلك سنرى المتغيّر x والدالّة f من خارج الدالّة. استخدامات الدالة "Eval" في طرق البرمجة الحديثة، نادرًا ما تستخدم الدالّة Eval. وغالبًا ما يقال عنها أنها أصل الشرور. والسبب بسيط: إذ كانت لغة جافاسكربت منذ زمن بعيد أضعف بكثير من الآن، ولم يكُ بالإمكان فعل إيّ شيء إلا باستخدام الدالّة Eval. ولكن ذلك الوقت مضى عليه عقد من الزمن. حاليًا، لا يوجد سبب وجيه لاستخدامها. ولو أن شخصًا يستخدمها الآن فلديه الإمكانية لاستبدالها بالبنية الحديثة للغة أو [بالوحدات](). لاحظ أن إمكانية وصول الدالة eval للمتغيرات الخارجية لها عواقب سيئة. إن عملية تصغير الشيفرة (هي الأدوات تستخدم لتصغير شيفرة الجافاسكربت قبل نشرها وذلك لتصغير حجمها أكثر من ذي قبل) تعيد تسمية المتغيّرات المحلية لأسماء أقصر (مثل a وb وما إلى ذلك) لتصغير الشيفرة. وعادةً ما تكون هذه العلمية آمنة، ولكن ليس في حال استخدام الدالّة Eval، إذ يمكننا الوصول للمتغيّرات المحلية من الشيفرة المُمررة للدالّة. لذا، لن تصغّر المتغيرات التي يحتمل أن تكون مرئية من الدالة Eval. مما سيُؤثر سلبًا على نسبة ضغط الشيفرة. يُعدّ استخدام المتغيّرات المحلية في الشيفرة بداخل الدالّة Eval من الممارسات البرمجية السيئة، لأنه يزيد صعوبة صيانة الشيفرة. هناك طريقتان لضمان الأمان الكامل عند مصادفتك مثل هذه المشاكل. إذا لم تستخدم الشيفرة الممررة للدالّة المتغيرات الخارجية، فمن الأفضل استدعاء الدالّة هكذا: window.eval(...)‎ بهذه الطريقة ستُنفذّ الشيفرة في النطاق العام: let x = 1; { let x = 5; window.eval('alert(x)'); // 1 (global variable) } إن احتاجت الشيفرة الممررة للدالة Eval لمتغيّرات خارجية، فغيّر Eval لتصبح new Function ومرّر المتغير كوسيط. هكذا: let f = new Function('a', 'alert(a)'); f(5); // 5 شرحنا في مقالٍ سابق تعلمنا كيفية استخدام [صياغة «الدالة الجديدة» new Function](). إذ باستخدام هذه الصياغة ستُنشأ دالة جديدة من السلسلة (String)، في النطاق العام. لذا لن تتمكن من رؤية المتغيرات المحلية. ولكن من الواضح أن تمريرها المتغيرات صراحة كوسطاء سيحلّ المشكلة، كما رأينا في المثال أعلاه. خلاصة سيُشغّل استدعاء الدالّة eval(code)‎ الشيفرة البرمجية المُمرّرة ويعيد نتيجة العبارة الأخيرة. نادرًا ما تستخدم هذه الدالّة في الإصدارات الحديثة للغة، إذ لا توجد حاجة ماسّة لها. يمكننا الوصول دائمًا للمتغيّرات الخارجية في الدالّة eval. ولكن يعدّ ذلك من الممارسات السيئة. بدلًا من ذلك يمكننا استخدام الدالة eval في النطاق العام، هكذا window.eval(code)‎. أو، إذا كانت الشيفرة الخاصة بك تحتاج لبعض البيانات من النطاق الخارجي، فاستخدم صياغة الدالّة الجديدة ومرّر لها المتغيرات كوسطاء. التمارين آلة حاسبة باستخدام الدالة Eval الأهمية: 4 أنشئ آلة حاسبة تطالب بتعبير رياضي وتُعيد نتيجته. لا داعي للتحقق من صحة التعبير في هذا التمرين. فقط قيّم التعبير وأعد نتيجته. لرؤية المثال الحي الحل لنستخدم الدالة eval لحساب التعبير الرياضي: let expr = prompt("Type an arithmetic expression?", '2*3+2'); alert( eval(expr) ); يستطيع المستخدم أيضًا إدخال أي نص أو شيفرة. لجعل الشيفرة آمنة، وحصرها للعمليات الرياضية فحسب، سنتحقق من expr باستخدام التعابير النمطية، لكي لا تحتوي إلا على الأرقام والمعاملات رياضية. ترجمة -وبتصرف- للفصل Eval: run a code string من كتاب The JavaScript language
  8. يغلّف كائن الوكيل Proxy كائنًا آخر ويعترض عملياته مثل: خاصيات القراءة أو الكتابة وغيرهما. ويعالجها اختياريًا بمفرده، أو يسمح بشفافية للكائن التعامل معها بنفسه. تستخدم العديد من المكتبات وأُطر عمل المتصفح الوسطاء. وسنرى في هذا الفصل العديد من تطبيقاته العملية. الوسيط Proxy صياغته: let proxy = new Proxy(target, handler) target -- وهو الكائن الّذي سنغلِّفه يمكن أي يكون أي شيء بما في ذلك التوابع. handler -- لإعداد الوسيط: وهو كائن يحتوي على "الاعتراضات"، أي دوالّ اعتراض العمليات. مثل: الاعتراض get لاعتراض خاصية القراءة في الهدف target، الاعتراض set لاعتراض خاصية الكتابة في الهدف target، وهكذا. سيراقب الوسيط العمليات فإن كان لديه اعتراض مطابق في المعالج handler للعملية المنفذة عند الهدف، فعندها سينفذ الوسيط هذه العملية ويعالجها وإلا ستُنفذ هذه العملية من قِبل الهدف نفسه. لنأخذ مثالًا بسيطًا يوضح الأمر، لننشئ وسيطًا بدون أي اعتراضات: let target = {}; let proxy = new Proxy(target, {}); // المعالج فارغ proxy.test = 5; // ‫الكتابة على الوسيط (1) alert(target.test); // النتيجة: ‫5، ظهرت الخاصية في كائن الهدف ! alert(proxy.test); // النتيجة: ‫5، يمكننا قرائتها من الوسيط أيضًا (2) for(let key in proxy) alert(key); // النتيجة: ‫test, عملية التكرار تعمل (3) انطلاقًا من عدم وجود اعتراضات سيُعاد توجيه جميع العمليات في الوسيط proxy إلى كائن الهدف target. تحدد عملية الكتابة proxy.test=‎ القيمة عند الهدف target، لاحظ السطر (1). تعيد عملية القراءة proxy.test القيمة من عند الهدف target. لاحظ السطر (2). تعيد عملية التكرار على الوسيط proxy القيم من الهدف target. لاحظ السطر (3). كما نرى، الوسيط proxy في هذه الحالة عبارة عن غِلاف شفاف حول الكائن الهدف target. لنُضف بعض الاعتراضات من أجل تفعيل المزيد من الإمكانيات. ما الذي يمكننا اعتراضه؟ بالنسبة لمعظم العمليات على الكائنات، هناك ما يسمى "الدوالّ الداخلية" في مواصفات القياسية في اللغة، والّتي تصف كيفية عمل الكائن عند أدنى مستوى. فمثلًا الدالة الداخلية [[Get]] لقراءة خاصية ما، والدالة الداخلية [[Set]] لكتابة خاصية ما، وهكذا. وتستخدم هذه الدوال من قبل المواصفات فقط، ولا يمكننا استدعاؤها مباشرة من خلال أسمائها. اعتراضات الوسيط تتدخلّ عند استدعاء هذه الدوالّ. وهي مدرجة في المواصفات القياسية للوسيط وفي الجدول أدناه. يوجد اعتراض مخصص (دالّة معالجة) لكل دالّة داخلية في هذا الجدول: يمكننا إضافة اسم الدالّة إلى المعالج handler من خلال تمريرها كوسيط new Proxy لاعتراض العملية. الدالة الداخلية الدالة المعالجة ستعمل عند [[Get]] get قراءة خاصية [[Set]] set الكتابة على خاصية [[HasProperty]] has التأكد من وجود خاصية ما in [[Delete]] deleteProperty عملية الحذف delete [[Call]] apply استدعاء تابِع [[Construct]] construct عملية الإنشاء -الباني- new [[GetPrototypeOf]] getPrototypeOf Object.getPrototypeOf [[SetPrototypeOf]] setPrototypeOf Object.setPrototypeOf [[IsExtensible]] isExtensible Object.isExtensible [[PreventExtensions]] preventExtensions Object.preventExtensions [[DefineOwnProperty]] defineProperty Object.defineProperty Object.defineProperties [[GetOwnProperty]] getOwnPropertyDescriptor Object.getOwnPropertyDescriptor for..in Object.keys/values/entries [[OwnPropertyKeys]] ownKeys Object.getOwnPropertyNames Object.getOwnPropertySymbols for..in Object/keys/values/entries table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } تحذير: التعامل مع الثوابت تفرض علينا لغة جافاسكربت بعض الثوابت - الشروط الواجب تحقيقها من قِبل الدوالّ الداخلية والاعتراضات. معظمها للقيم المُعادة من الدوالّ: يجب أن تعيد الدالّة [[Set]] النتيجة true إذا عُدلت القيمة بنجاح، وإلا ستُعيد false. يجب أن تعيد الدالّة [[Delete]] النتيجة true إذا حُذفت القيمة بنجاح، وإلا ستُعيد false. …وهكذا، سنرى المزيد من الأمثلة لاحقًا. هنالك بعض الثوابت الأخرى، مثل: يجب أن تُعيد الدالة [[GetPrototypeOf]] المطبقة على كائن الوسيط نفس القيمة الّتي ستُعيدها الدلّة [[GetPrototypeOf]] المطبقة على كائن الهدف لكائن الوسيط، بتعبير آخر، يجب أن تعرض دائمًا قراءة النموذج الأولي لكائن الوسيط نفس قراءة النموذج الأولي لكائن الهدف. يمكن للاعتراضات التدخل في هذه العمليات ولكن لابد لها من اتباع هذه القواعد. تضمن هذه الثوابت السلوك الصحيح والمُتسق لمميّزات اللغة. وجميع هذه الثوابت موجودة في المواصفات القياسية للغة. وغالبًا لن تكسرَهم إن لم تنفذّ شيئًا غريبًا. لنرى كيف تعمل في مثال عملي. إضافة اعتراض للقيم المبدئية تعدُّ خاصيات القراءة / الكتابة من أكثر الاعتراضات شيوعًا. لاعتراض القراءة، يجب أن يكون لدى المعالج handler دالّة get(target, property, receiver)‎. وتُشغّل عند قراءة خاصية ما، وتكون الوسطاء: target - هو كائن الهدف، والذي سيمرر كوسيط أول لِـ new Proxy، property - اسم الخاصية، receiver - إذا كانت الخاصية المستهدفة هي الجالِب (getter)، فإن receiver هو الكائن الذي سيُستخدم على أنه بديل للكلمة المفتاحية this في الاستدعاء. عادةً ما يكون هذا هو كائن proxy نفسه (أو كائن يرث منه، في حال ورِثنا من الوسيط). لا نحتاج الآن هذا الوسيط، لذلك سنشرحها بمزيد من التفصيل لاحقًا. لنستخدم الجالِب get لجلب القيم الافتراضية لكائن ما. سننشئ مصفوفة رقمية تُعيد القيمة 0 للقيم غير الموجودة. عادةً عندما نحاول الحصول على عنصر من مصفوفة، وكان هذا العنصر غير موجود، سنحصل على النتيجة غير معرّف undefined، لكننا هنا سنُغلف المصفوفة العادية داخل الوسيط والّذي سيعترضُ خاصية القراءة ويعيد 0 إذا لم تكُ الخاصية المطلوبة موجودة في المصفوفة: let numbers = [0, 1, 2]; numbers = new Proxy(numbers, { get(target, prop) { if (prop in target) { return target[prop]; } else { return 0; // القيمة الافتراضية } } }); alert( numbers[1] ); // 1 alert( numbers[123] ); // 0 (هذا العنصر غير موجود) كما رأينا، من السهل جدًا تنفيذ ذلك باعتراض الجالِب get. يمكننا استخدام الوسيط proxy لتنفيذ أي منطق للقيم "الافتراضية". تخيل أن لدينا قاموسًا (يربط القاموس مفاتيح مع قيم على هيئة أزواج) يربط العبارات مع ترجمتها: let dictionary = { 'Hello': 'Hola', 'Bye': 'Adiós' }; alert( dictionary['Hello'] ); // Hola alert( dictionary['Welcome'] ); // undefined مبدئيًا إن حاولنا الوصول إلى عبارة غير موجودة في القاموس فسيُعيد غير معرّف undefined، ولكن عمليًا إن ترك العبارة بدون مترجمة أفضل من undefined، لذا لنجعلها تُعيد العبارة بدون مترجمة بدلاً من undefined. لتحقيق ذلك، سنغلّف القاموس dictionary بالوسيط ليعترض عمليات القراءة: let dictionary = { 'Hello': 'Hola', 'Bye': 'Adiós' }; dictionary = new Proxy(dictionary, { get(target, phrase) { // اعترض خاصية القراءة من القاموس if (phrase in target) { // إن كانت موجودة في القاموس return target[phrase]; // أعد الترجمة } else { // وإلا أعدها بدون ترجمة return phrase; } } }); // ‫ابحث عن عبارة عشوائية في القاموس! // بأسوء حالة سيُعيد العبارة غير مترجمة alert( dictionary['Hello'] ); // Hola alert( dictionary['Welcome to Proxy']); // Welcome to Proxy (يعيد العبارة بدون ترجمة) لاحظ كيف أن الوسيط يعد الكتابة على المتغير الافتراضي: dictionary = new Proxy(dictionary, ...); يجب على الوسيط استبدال كائن الهدف بالكامل في كل مكان. ولا يجب أن يشير أي شيء للكائن الهدف بعد أن يستبدلهُ الوسيط. وإلا فذلك سيُحدثُ فوضى عارمة. تدقيق المدخلات باعتراض الضابِط "set" لنفترض أننا نريد إنشاء مصفوفة مخصصة للأرقام فقط. وإن أضيفت قيمة من نوع آخر، يجب أن يظهر خطأ. يُشغّل اعتراض الضابِط set عند الكتابة على الخاصية. set(target, property, value, receiver)‎ target - الكائن الهدف، الذي سنمرره كوسيط أول new Proxy، property - اسم الخاصية، value - قيمة الخاصية، receiver - مشابه للدالّة get، ولكن هنا لضبط الخاصيات فقط. يجب أن يُعيد الاعتراض set القيمة true إذا نجح ضبط القيمة في الخاصية، وإلا يعيد القيمة false (يُشغّل خطأ من نوع TypeError). لنأخذ مثالًا عن كيفية استخدامها للتحقق من القيم الجديدة: let numbers = []; numbers = new Proxy(numbers, { // (*) set(target, prop, val) { // لاعتراض خاصية ضبط القيمة if (typeof val == 'number') { target[prop] = val; return true; } else { return false; } } }); numbers.push(1); // أضيفت بنجاح numbers.push(2); // أضيفت بنجاح alert("Length is: " + numbers.length); // 2 numbers.push("test"); // ‫TypeError (الدالّة 'set' في الوسيط أعادت القيمة false) alert("This line is never reached (error in the line above)"); لاحظ أن الدوال المدمجة للمصفوفات ما تزال تعمل! إذ تضاف القيم عن طريق push. فتزداد خاصية length تلقائيًا عند إضافة هذه القيم. لذا فإن الوسيط لن يكسر أي شيء. يجب علينا ألا نعيد كتابة الدوالّ المُدمجة للمصفوفات الّتي تضيف القيم مثل push وunshift، وما إلى ذلك، إن كان هدفنا إضافة عمليات تحقق، لأنهم يستخدمون داخليًا دالّة [[Set]] والّتي ستُعترضُ من قِبل الوسيط. إذن الشيفرة نظيفة ومختصرة. لا تنسَ أن تعيد القيمة true عند نجاح عملية الكتابة. كما ذكر أعلاه، هناك ثوابت ستعقّد. بالنسبة للضابط set، يجب أن يُرجع true عند نجاح عملية الكتابة. إذا نسينا القيام بذلك أو إعادة أي قيمة زائفة، فإن العملية تؤدي إلى خطأ TypeError. التكرار باستخدام تابع "ownKeys" و"getOwnPropertyDescriptor" إن الدالة Object.keys وحلقة التكرار for..in ومعظم الطرق الأخرى الّتي تستعرض خاصيات الكائن، وتستخدم الدالّة الداخلية [[OwnPropertyKeys]] للحصول على قائمة بالخاصيات يمكننا اعتراضها من خلال ownKeys. تختلف هذه الدوال وحلقات التكرار على الخاصيات تحديدًا في: Object.getOwnPropertyNames (obj)‎: تُعيد الخاصيات غير الرمزية. Object.getOwnPropertySymbols (obj)‎: تُعيد الخاصيات الرمزية. Object.keys/values()‎: تعيد الخاصيات/القيم غير الرمزية والتي تحمل راية قابلية الاحصاء enumerable (شرحنا في مقال سابق ما هي رايات الخواص وواصفاتها يمكنك الاطلاع عليه لمزيد من التفاصيل). حلقات for..in: تمرّ على الخاصيات غير الرمزية التي تحمل راية قابلية الاحصاء enumerable، وكذلك تمرّ على خاصيات النموذج الأولي (prototype). … لكن كلهم يبدأون بهذه القائمة. في المثال أدناه ، نستخدم اعتراض ownKeys لجعل حلقةfor..in تمر على user، وكذلكObject.keys وObject.values، وتتخطى الخاصيات التي تبدأ بشرطة سفلية _: let user = { name: "John", age: 30, _password: "***" }; user = new Proxy(user, { ownKeys(target) { return Object.keys(target).filter(key => !key.startsWith('_')); } }); // ‫الاعتراض "ownKeys" سيزيل الخاصّية ‎_password for(let key in user) alert(key); // name, then: age // ‫نفس التأثير سيحدث على هذه التوابع: alert( Object.keys(user) ); // name,age alert( Object.values(user) ); // John,30 حتى الآن، لا يزال مثالنا يعمل وفق المطلوب. على الرغم من ذلك، إذا أضفنا خاصية ما غير موجودة في الكائن الأصلي وذلك بإرجاعها من خلال الوسيط، فلن يعيدها التابع Object.keys: let user = { }; user = new Proxy(user, { ownKeys(target) { return ['a', 'b', 'c']; } }); alert( Object.keys(user) ); // <empty> هل تسأل نفسك لماذا؟ السبب بسيط: تُرجع الدالّة Object.keys فقط الخاصيات الّتي تحمل راية قابلية الإحصاء enumerable. وهي تحقق من رايات الخاصيات لديها باستدعاء الدالّة الداخلية [[GetOwnProperty]] لكل خاصية للحصول على واصِفها. وهنا، نظرًا لعدم وجود الخاصّية، فإن واصفها فارغ، ولا يوجد راية قابلية الإحصاء enumerable، وبناءً عليه تخطت الدالّة الخاصّية. من أجل أن ترجع الدالّة Object.keys الخاصية، فيجب أن تكون إما موجودة في الكائن الأصلي، وتحمل راية قابلية الإحصاء enumerable، أو يمكننا وضع اعتراض عند استدعاء الدالة الداخلية [[GetOwnProperty]] (اعتراض getOwnPropertyDescriptor سيحقق المطلوب)، وإرجاع واصف لخاصية قابلية الإحصاء بالإيجاب هكذا enumerable: true. إليك المثال ليتوضح الأمر: let user = { }; user = new Proxy(user, { ownKeys(target) { // يستدعى مرة واحدة عند طلب قائمة الخاصيات return ['a', 'b', 'c']; }, getOwnPropertyDescriptor(target, prop) { // تستدعى من أجل كلّ خاصّية return { enumerable: true, configurable: true /* ...يمكننا إضافة رايات أخرى مع القيم المناسبة لها..." */ }; } }); alert( Object.keys(user) ); // a, b, c نعيد مرة أخرى: نضيف اعتراض [[GetOwnProperty]] إذا كانت الخاصّية غير موجودة في الكائن الأصلي. الخاصيات المحمية والاعتراض "deleteProperty" وغيره هناك إجماع كبير في مجتمع المطورين بأن الخاصيات والدوالّ المسبوقة بشرطة سفلية _ هي للاستخدام الداخلي. ولا ينبغي الوصول إليها من خارج الكائن. هذا ممكن تقنيًا: let user = { name: "John", _password: "secret" }; // لاحظ أن في الوضع الطبيعي يمكننا الوصول لها alert(user._password); // secret لنستخدم الوسيط لمنع الوصول إلى الخاصيات الّتي تبدأ بشرطة سفلية _. سنحتاج استخدام الاعتراضات التالية: get لإلقاء خطأ عند قراءة الخاصية، set لإلقاء خطأ عند الكتابة على الخاصية، deleteProperty لإلقاء خطأ عند حذف الخاصية، ownKeys لاستبعاد الخصائص التي تبدأ بشرطة سفلية _ من حلقة for..in، والدوالّ التي تستعرض الخاصيات مثل: Object.keys. هكذا ستكون الشيفرة: let user = { name: "John", _password: "***" }; user = new Proxy(user, { get(target, prop) { if (prop.startsWith('_')) { throw new Error("Access denied"); } let value = target[prop]; return (typeof value === 'function') ? value.bind(target) : value; // (*) }, set(target, prop, val) { // لاعتراض عملية الكتابة على الخاصّية if (prop.startsWith('_')) { throw new Error("Access denied"); } else { target[prop] = val; return true; } }, deleteProperty(target, prop) { // لاعتراض عملية حذف الخاصّية if (prop.startsWith('_')) { throw new Error("Access denied"); } else { delete target[prop]; return true; } }, ownKeys(target) { // لاعتراض رؤية الخاصية من خلال الحلقات أو الدوالّ return Object.keys(target).filter(key => !key.startsWith('_')); } }); // ‫الاعتراض "get" سيمنعُ قراءة الخاصّية ‎_password try { alert(user._password); // Error: Access denied } catch(e) { alert(e.message); } // ‫ الاعتراض "set" سيمنع الكتابة على الخاصّية ‎_password try { user._password = "test"; // Error: Access denied } catch(e) { alert(e.message); } // ‫ الاعتراض "deleteProperty" سيمنعُ حذف الخاصّية ‎_password try { delete user._password; // Error: Access denied } catch(e) { alert(e.message); } // ‫ الاعتراض "ownKeys" سيمنعُ إمكانية رؤية الخاصّية ‎_password في الحلقات for(let key in user) alert(key); // name لاحظ التفاصيل المهمة في الاعتراض get، وذلك في السطر (*): get(target, prop) { // ... let value = target[prop]; return (typeof value === 'function') ? value.bind(target) : value; // (*) } لماذا نحتاج لدالّة لاستدعاء value.bind(target)‎؟ وسبب ذلك هو أن دوال الكائن مثل: user.checkPassword()‎ يجب تحافظ على إمكانية الوصول للخاصّية ‎_password: user = { // ... checkPassword(value) { // ‫دوال الكائن يجب أن تحافظ على إمكانية الوصول للخاصية ‎_password return value === this._password; } } تستدعي الدالة user.checkPassword()‎ وسيط الكائن user ليحلّ محلّ this (الكائن قبل النقطة يصبح بدل this)، لذلك عندما نحاول الوصول إلى الخاصّية this._password، سينُشّط الاعتراض get ( والّذي يُشغلّ عند قراءة خاصية ما) ويلقي الخطأ. لذلك نربط سياق الدوال الخاصة بالكائن مع دوالّ الكائن الأصلي، أيّ الكائن الهدف target لاحظ السطر (*). بعد ذلك، الاستدعاءات المستقبلية ستستخدم target بدلّ this، دون الاعتراضات. هذا الحل ليس مثاليًا ولكنه يفي بالغرض، ولكن يمكن لدالّةٍ ما أن تُمرّر الكائن غير المغَلّف بالوسيط (أي الكائن الأصلي) إلى مكان آخر، وبذلك ستخرّب الشيفرةولن نستطع الإجابة على أسئلة مثل: أين يستخدم الكائن الأصلي؟ وأين يستخدم الكائن الوسيط؟ أضف إلى ذلك، يمكن للكائن أن يغلف بأكثر من وسيط (تضيف عدة وسطاء "تعديلات" مختلفة على الكائن الأصلي)، وإن مرّرنا كائن غير مغلّف بالوسيط إلى دالّة ما، ستكون هناك عواقب غير متوقعة. لذا، لا ينبغي استخدام هذا الوسيط في كل الحالات. ملاحظة: الخاصيات الخاصة بالصنف تدعم محركات جافاسكربت الحديثة الخاصيات الخاصّة بالأصناف، وتكون مسبوقة بـ #. تطرقنا لها سابقًا في مقال الخصائص والتوابع الخاصّة والمحمية. وبدون استخدام أيّ الوسيط. على الرغم من ذلك هذه الخاصيات لها مشاكلها الخاصة أيضًا. وتحديدًا، مشكلة عدم إمكانية توريث هذه الخاصيات. استخدام الاعتراض "In range" مع "has" لنرى مزيدًا من الأمثلة. لدينا الكائن range: let range = { start: 1, end: 10 }; نريد استخدام المعامل in للتحقق من أن الرقم موجود في range. إن الاعتراض has سيعترض استدعاءات in. has(target, property)‎ target - هو الكائن الهدف، الذي سيمرر كوسيط أولnew Proxy`، property - اسم الخاصية إليك المثال: let range = { start: 1, end: 10 }; range = new Proxy(range, { has(target, prop) { return prop >= target.start && prop <= target.end; } }); alert(5 in range); // true alert(50 in range); // false تجميلٌ لغويٌّ رائع، أليس كذلك؟ وتنفيذه بسيط جدًا. تغليف التوابع باستخدام "apply" يمكننا أيضًا تغليف دالّة ما باستخدام كائن الوسيط. يعالج الاعتراض apply(target, thisArg, args)‎ استدعاء كائن الوسيط كتابع: target: الكائن الهدف (التابع - أو الدالّة - هي كائن في لغة جافا سكربت)، thisArg: قيمة this. args: قائمة الوسطاء. فمثلًا، لنتذكر المُزخرف delay(f, ms)‎، الذي أنشأناه في مقال المزخرفات والتمرير. في تلك المقالة أنشأناه بدون استخدام الوسيط. عند استدعاء الدالة delay(f, ms)‎ ستُعيد دالّة أخرى، والّتي بدورها ستوجه جميع الاستدعاءات إلى f بعد ms ملّي ثانية. إليك التطبيق المثال بالاعتماد على التوابع: function delay(f, ms) { // ‫يعيد المغلف والذي بدوره سيوجه الاستدعاءات إلى f بعد انتهاء مهلة زمنية معينة return function() { // (*) setTimeout(() => f.apply(this, arguments), ms); }; } function sayHi(user) { alert(`Hello, ${user}!`); } // ‫بعد عملية التغليف استدعي الدالّة sayHi بعد 3 ثواني sayHi = delay(sayHi, 3000); sayHi("John"); // Hello, John! (بعد 3 ثواني) كما رأينا، غالبًا ستعمل هذه الطريقة وفق المطلوب. تُنفذّ الدالّة المغلفة في السطر (*) استدعاءً بعد انتهاء المهلة. لكن دالة المغلف لا تعيد توجيه خاصيات القراءة أو الكتابة للعمليات أو أيّ شيء آخر. بعد عملية التغليف، يُفقدُ إمكانية الوصول لخاصيات التوابع الأصلية، مثل: name وlength وغيرها: function delay(f, ms) { return function() { setTimeout(() => f.apply(this, arguments), ms); }; } function sayHi(user) { alert(`Hello, ${user}!`); } alert(sayHi.length); // ‫1 (طول الدالة هو عدد الوسطاء في تعريفها) sayHi = delay(sayHi, 3000); alert(sayHi.length); // ‫0 (إن عدد وسطاء عند تعريف المغلّف هو 0) في الحقيقة إن إمكانيات الوسيط Proxy أقوى بكثير من ذلك، إذ إنه يعيد توجيه كل شيء إلى الكائن الهدف. لنستخدم الوسيط Proxy بدلاً من الدالّة المُغلِّفة: function delay(f, ms) { return new Proxy(f, { apply(target, thisArg, args) { setTimeout(() => target.apply(thisArg, args), ms); } }); } function sayHi(user) { alert(`Hello, ${user}!`); } sayHi = delay(sayHi, 3000); alert(sayHi.length); // ‫1 (*) سيعيد الوسيط توجيه عملية "get length" إلى الهدف sayHi("John"); // Hello, John! (بعد 3 ثواني) نلاحظ أن النتيجة نفسها، ولكن الآن ليس مجرد استدعاءات فقط، وإنما كل العمليات في الوسيط يُعاد توجيها إلى التوابع الأصلية. لذلك سيعيد الاستدعاء sayHi.length النتيجة الصحيحة بعد التغليف في السطر (*). وبذلك حصلنا على مُغلِّف أغنى بالمميزات من الطريقة السابقة. هنالك العديد من الاعتراضات الأخرى: يمكنك العودة لبداية المقال لقراءة القائمة الكاملة للاعتراضات. كما أن طريقة استخدامها مشابه كثيرًا لما سبق. الانعكاس الانعكاس Reflect هو عبارة عن كائن مضمّن في اللغة يبسط إنشاء الوسيط Proxy. ذكرنا سابقًا أن الدوالّ الداخلية، مثل: [[Get]] و[[Set]] وغيرها مخصصة للاستخدام في مواصفات اللغة فقط، ولا يمكننا استدعاؤها مباشرة. يمكن لكائن المنعكس Reflect من فعل ذلك إلى حد ما. إذ أن الدوالّ الخاصة به عبارة مُغلِّفات صغيرة حول الدوالّ الداخلية. فيما يلي أمثلة للعمليات واستدعاءات المنعكس Reflect الّتي ستُؤدي نفس المهمة: الدالة الداخلية الدالة المقابلة في المنعكس العملية [[Get]] Reflect.get(obj, prop)‎ obj[prop]‎ [[Set]] Reflect.set(obj, prop, value)‎ obj[prop] = value [[Delete]] Reflect.deleteProperty(obj, prop)‎ delete obj[prop]‎ [[Construct]] Reflect.construct(F, value)‎ new F(value)‎ … … … فمثلًا: let user = {}; Reflect.set(user, 'name', 'John'); alert(user.name); // John ‎ تحديدًا، يتيح لنا المنعكس Reflect استدعاء العمليات (new, delete…‎) كتوابع هكذا (‎Reflect.construct,Reflect.deleteProperty, …‎). وهذه الامكانيات مثيرة للاهتمام، ولكن هنالك شيء آخر مهم. لكل دالّة داخلية، والّتي يمكننا تتبعها من خلال الوسيط Proxy، يوجد دالّة مقابلة لها في المنعكس Reflect، بنفس الاسم والوسطاء أي مشابه تمامًا للاعتراض في الوسيط Proxy. لذا يمكننا استخدام المنعكس Reflect لإعادة توجيه عملية ما إلى الكائن الأصلي. في هذا المثال، سيكون كلًا من الاعتراضين get وset شفافين (كما لو أنهما غير موجودين) وسيُوجهان عمليات القراءة والكتابة إلى الكائن، مع إظهار رسالة: let user = { name: "John", }; user = new Proxy(user, { get(target, prop, receiver) { alert(`GET ${prop}`); return Reflect.get(target, prop, receiver); // (1) }, set(target, prop, val, receiver) { alert(`SET ${prop}=${val}`); return Reflect.set(target, prop, val, receiver); // (2) } }); let name = user.name; // shows "GET name" user.name = "Pete"; // shows "SET name=Pete" في الشيفرة السابقة: Reflect.get: يقرأ خاصية الكائن. Reflect.set: يكتب خاصية الكائن، سيُعيد true إن نجحت، وإلا سيُعيد false. أي أن كل شيء بسيط: إذا كان الاعتراض يُعيد توجيه الاستدعاء إلى الكائن، فيكفي استدعاء Reflect.<method>‎ بنفس الوسطاء. في معظم الحالات ، يمكننا فعل الشيء نفسه بدون Reflect، على سبيل المثال، يمكن استبدال Reflect.get(target, prop, receiver)‎ بـ target[prop]‎. يوجد فروقٍ بينهم ولكن لا تكاد تذكر. استخدام الوسيط مع الجالِب لنرى مثالاً يوضح لماذا Reflect.get أفضل. وسنرى أيضًا سبب وجود الوسيط الرابع في دوالّ get/set، تحديدًا receiver والّذي لم نستخدمه بعد. لدينا كائن user مع خاصية ‎_name وجالب مخصص لها. لنُغلِّفه باستخدام الوسيط: let user = { _name: "Guest", get name() { return this._name; } }; let userProxy = new Proxy(user, { get(target, prop, receiver) { return target[prop]; } }); alert(userProxy.name); // Guest إن الاعتراض get في هذا المثال "شفاف"، فهو يعيد الخاصية الأصلية ولا يفعل أي شيء آخر. هذا يكفي لمثالنا. يبدو أن كل شيء على ما يرام. لكن لنُزد تعقيد المثال قليلًا. بعد وراثة كائن admin من الكائن user، نلاحظ السلوك الخاطئ: let user = { _name: "Guest", get name() { return this._name; } }; let userProxy = new Proxy(user, { get(target, prop, receiver) { return target[prop]; // (*) target = user } }); let admin = { __proto__: userProxy, _name: "Admin" }; // ‫نتوقع ظهور الكلمة: Admin alert(admin.name); // ‫النتيجة: Guest (لماذا؟) إن قراءة admin.name يجب أن تُعيد كلمة "Admin" وليس "Guest"! ما الذي حدث؟ لعلنا أخطأنا بشيء ما في عملية الوراثة؟ ولكن إذا أزلنا كائن الوسيط، فسيكون كل شيء على ما يرام. إذًا المشكلة الحقيقية في الوسيط، تحديدًا في السطر (*). عندما نقرأ خاصّية الاسم admin.name من كائن admin، لا يحتوي كائن admin على هذه الخاصية، وبذلك ينتقل للبحث عنها في النموذج الأولي الخاص به. النموذج الأولي الخاص به هو userProxy. عند قراءة الخاصية name من الوسيط، سيُشغّل اعتراض get الخاص به، وسيُعيدih من الكائن الأصلي هكذا target[prop]‎ في السطر (*). يؤدي الاستدعاء target [prop]‎، عندما يكون قيمة prop هي الجالب (getter)، سيؤدي ذلك لتشغيل الشيفرة بالسياقِ this = target. لذلك تكون النتيجة this._name من الكائن الأصلي للهدف target، أي: من user. لإصلاح مثل هذه المواقف، نحتاج إلى receiver، الوسيط الثالث للاعتراض get. إذ سيُحافظ على قيمة this الصحيحة لتُمرر بعد ذلك إلى الجالِب (getter). في حالتنا تكون قيمتها هي admin. كيفية يمرر سياق الاستدعاء للحصول الجالِب الصحيح؟ بالنسبة للتابع العادي، يمكننا استخدام call/apply، ولكن بالنسبة للجالِب فلن يستدعى بهذه الطريقة. لنستخدم الدالّة Reflect.get والتي يمكنها القيام بذلك. وكل شيء سيعمل مثلما نريد. وإليك الشكل الصحيح: let user = { _name: "Guest", get name() { return this._name; } }; let userProxy = new Proxy(user, { get(target, prop, receiver) { // receiver = admin return Reflect.get(target, prop, receiver); // (*) } }); let admin = { __proto__: userProxy, _name: "Admin" }; alert(admin.name); // Admin الآن يحافظ receiver بمرجع لقيمة this الصحيحة (وهي admin)، والّتي ستُمرر من خلال Reflect.get في السطر (*). يمكننا إعادة كتابة الاعتراض بطريقة أقصر: get(target, prop, receiver) { return Reflect.get(...arguments); } استدعاءات المنعكس Reflect لها نفس أسماء اعتراضات الوسيط وتقبل نفس وسطائه أيضًا. إذ صُمّمت خصيصًا لهذا الغرض. لذا ، فإن استخدام المنعكس return Reflect...‎ يزودنا بالأمان والثقة لتوجيه العملية والتأكد تمامًا من أننا لن ننسَ أي شيء متعلقٌ بها. قيود الوسيط لدى الوسيط طريقة فريدة لتغيير أو تعديل سلوك الكائنات الموجودة عند أدنى مستوى. ومع ذلك، هذه الطريقة ليست مثالية. وإنما هناك قيود. كائنات مضمّنة: فتحات داخلية تستخدم العديد من الكائنات المضمنة، مثل الكائنات Map وSet وDate وPromise وغيرها ما يسمى بـ" الفتحات الداخلية ". وهي مشابهة للخاصيات، لكنها محفوظة للأغراض داخلية فقط، وللمواصفات القياسية للغة فقط. فمثلًا تخزن Map العناصر في الفتحة الداخلية [[MapData]]. وتستطيع الدوال المُضمّنة الوصول إليها مباشرةً، وليس عبر الدوالّ الداخلية مثل [[Get]]/[[Set]]. لذا فإن الوسيط Proxy لا يمكنه اعتراض ذلك. ولكن ما سبب اهتمامنا بذلك؟ إنها بكلّ الأحوال داخلية! حسنًا ، إليك المشكلة. بعد أن يغلف الكائن المضمن مثل: Map باستخدام الوسيط، لن يمتلك الوسيط هذه الفتحات الداخلية، لذلك ستفشل الدوالّ المضمنة. إليك مثالًا يوضح الأمر: let map = new Map(); let proxy = new Proxy(map, {}); proxy.set('test', 1); // Error داخليًا ، تخزن Map جميع البيانات في الفتحة الداخلية [[MapData]]. ولكن الوسيط ليس لديه مثل هذه الفتحة. تحاول الدالّة المضمنة Map.prototype.set الوصول إلى الخاصية الداخلية this.[[MapData]]‎، ولكن لأن this=proxy، لا يمكن العثور عليه في الوسيط proxy مما سيؤدي لفشل العملية. لحسن الحظ، هناك طريقة لإصلاحها: let map = new Map(); let proxy = new Proxy(map, { get(target, prop, receiver) { let value = Reflect.get(...arguments); return typeof value == 'function' ? value.bind(target) : value; } }); proxy.set('test', 1); alert(proxy.get('test')); // 1 (works!) الآن تعمل وفق المطلوب، لأن اعتراض get يربط خاصيات الدوالّ، مثل map.set، بالكائن الهدف (map) نفسه. بخلاف المثال السابق، فإن قيمة this داخلproxy.set (...)‎ لن تكون proxy، وإنما map الأصلية. لذا عند التطبيق الداخلي لـ set سيحاول الوصول إلى الفتحة الداخلية هكذا this.[[MapData]]‎، ولحسن الحظ سينجح. ملاحظة: المصفوفة العادية Array ليس لها فتحات داخلية استثناء ملحوظ: لا تستخدم المصفوفات المضمنة Array الفتحات الداخلية. هذا لأسباب تاريخية، إذ إنها ظهرت منذ وقت طويل. لذلك لا توجد مشكلة عند تغليف المصفوفة إلى باستخدام الوسيط. الخاصيات الخاصة يحدث شيء مشابه للأمر مع خاصيات الصنف الخاصة. فمثلًا، يمكن للدالّة getName()‎ الوصول إلى الخاصية الخاصًة ‎#name بدون استخدام الوسيط، ولكن بعد تغليفنا للكائن باستخدام الوسيط ستتوقف إمكانية وصول الدالّة السابقة للخاصّية الخاصّة: class User { #name = "Guest"; getName() { return this.#name; } } let user = new User(); user = new Proxy(user, {}); alert(user.getName()); // Error وذلك بسبب أن التنفيذ الفعلي للخاصّيات الخاصّة يكون باستخدام الفتحات داخلية. ولا تستخدم لغة جافاسكربت الدوالّ [[Get]]/[[Set]] للوصول إليها. في استدعاء getName()‎، تكون قيمة this هي كائن user المغلّف بالوسيط، ولا يحتوي -هذا الكائن- على فتحة داخلية مع هذه الخاصّيات الخاصّة. وللمرة الثانية، يكون ربط الدالّة بالكائن من سيحل الأمر: class User { #name = "Guest"; getName() { return this.#name; } } let user = new User(); user = new Proxy(user, { get(target, prop, receiver) { let value = Reflect.get(...arguments); return typeof value == 'function' ? value.bind(target) : value; } }); alert(user.getName()); // Guest ومع ذلك، فإن لهذا الحل بعض العيوب، كما وضحنا سابقًا: سيعرض هذا الحل الكائن الأصلي لبعض الدوالّ، مما سيُسمح بتمريره لدوالٍ أكثر وبذلك كسر الدوالّ الأخرى المتعلقة بالوسيط. Proxy! = target إن كلًا من الوسيط والكائن الأصلي مختلفان. وهذا أمر طبيعي، أليس كذلك؟ لذلك إذا استخدمنا الكائن الأصلي كخاصية في المجموعة Set، ثم غلفناه بالوسيط، فعندئذ لن نتمكن من العثور على الوسيط: let allUsers = new Set(); class User { constructor(name) { this.name = name; allUsers.add(this); } } let user = new User("John"); alert(allUsers.has(user)); // true user = new Proxy(user, {}); // لاحظ alert(allUsers.has(user)); // false كما رأينا، بعد التغليف باستخدام الوسيط لن نتمكن من العثور على كائن user في المجموعة allUsers، لأن الوسيط هو كائن مختلف عن الكائن الأصلي. لا يمكن للوسطاء proxies اعتراض اختبار المساواة الصارم ===. يمكنها اعترلض العديد من العمليات الأخرى، مثل: new (مع build) وin (مع has) وdelete (مع deleteProperty) وما إلى ذلك. ولكن لا توجد طريقة لاعتراض اختبار المساواة الصارم للكائنات. الكائن يساوي نفسه تمامًا ولا يساوي أيّ كائنٍ آخر. لذا فإن جميع العمليات والأصناف المضمّنة في اللغة التي توازن بين الكائنات من أجل المساواة ستميز الفرق بين الكائن الأصلي والوسيط. ولا يوجد من يحل محله لإصلاح الأمر. الوسيط القابل للتعطيل وسيط revocable هو وسيط يمكن تعطيله. لنفترض أن لدينا موردًا ونود منع الوصول إليه في لحظةٍ ما. أحد الأشياء التي يمكننا فعلها هو تغليف هذا المورد بالوسيط القابل للتعطيل، بدون أي اعتراضات. بهذه الحالة سيُعيد الوسيط توجيه العمليات للكائن، ويمكننا تعطيله بأي لحظة نريدها. وصياغته: let {proxy, revoke} = Proxy.revocable(target, handler) الاستدعاء من خلال proxy سيُعيد الكائن والاستدعاء من خلال revoke سيعطل إمكانية الوصول إليه. إليك المثال لتوضيح الأمر: let object = { data: "Valuable data" }; let {proxy, revoke} = Proxy.revocable(object, {}); // مرر الوسيط لمكان آخر بدل الكائن alert(proxy.data); // Valuable data // لاحقا نستدعي التابع revoke(); // ‫لن يعمل الوسيط الآن (لأنه معطلّ) alert(proxy.data); // Error يؤدي استدعاء revoke()‎ لإزالة جميع المراجع الداخلية للكائن الهدف من الوسيط، وبذلك لم يعد متصل بأي شيء بعد الآن. كما يمكننا بعد ذلك كنس المخلفات من الذاكرة بإزالة الكائن الهدف. يمكننا أيضًا تخزين revoke في WeakMap، حتى نتمكن من العثور عليه بسهولة من خلال كائن الوسيط: let revokes = new WeakMap(); let object = { data: "Valuable data" }; let {proxy, revoke} = Proxy.revocable(object, {}); revokes.set(proxy, revoke); // ..لاحقًا في الشيفرة.. revoke = revokes.get(proxy); revoke(); alert(proxy.data); // Error (revoked) تفيدنا هذه الطريقة بأنه ليس علينا بعد الآن حمل revoke. وإنما يمكننا الحصول عليها من map من خلال الوسيط proxy عند الحاجة. نستخدم WeakMap بدلاً من Map هنا لأنه لن يمنع كنس المخلّفات في الذاكرة. إذا أصبح كائن الوسيط "غير قابل للوصول" (مثلًا، في حال لم يعد هناك متغيّر يشير إليه بعد الآن)، فإن WeakMap يسمح بمسحه من الذاكرة مع revoke خاصته والّتي لن نحتاج إليها بعد الآن. المصادر المواصفات القياسية للوسيط : Proxy. توثيق الوسيط الرسمي من مركز مطوري موزيلا MDN. خلاصة الوسيط Proxy عبارة عن غلاف حول كائن، يُعيد توجيه العمليات عليه إلى الكائن، ويحبس بعضها بشكل اختياري. يمكنه تغليف أي نوع من الكائنات، بما في ذلك الأصناف والدوالّ. صياغته: let proxy = new Proxy(target, { /* traps */ }); … ثم يجب علينا استخدام "الوسيط" في كل مكان بدلاً من كائن "الهدف". لا يمتلك الوكيل خاصيات أو توابع. يعترض عملية ما إذا زُودَ بالاعتراض المناسب، وإلا سيعيد توجيهها إلى كائن الهدف target. يمكننا اعتراض: قراءة (get) وكتابة (set) وحذف (deleteProperty) خاصية (حتى الخاصية غير موجودة). استدعاء دالّة ما (الاعتراض apply). المعامل new (الاعتراض construct). العديد من العمليات الأخرى (القائمة الكاملة في بداية المقال وفي التوثيق الرسمي). مما سيسمح لنا بإنشاء خاصيات ودوالّ "افتراضية"، وتطبيق قيم افتراضية، وكائنات المراقبة، وزخرفة الدوالّ، وأكثر من ذلك بكثير. يمكننا أيضًا تغليف كائن ما عدة مرات في وسطاء مختلفة، وزخرفته بمختلف أنواع الوظائف. صُممّت الواجهة البرمجية للمنعكس لتكمل عمل الوسيط. بالنسبة لأي اعتراض Proxy ، هناك استدعاء للمنعكس Reflect مقابل له بنفس الوسطاء. يجب علينا استخدامها لإعادة توجيه الاستدعاءات إلى الكائنات المستهدفة. لدى الوسيط بعض القيود: تحتوي الكائنات المضمّنة في اللغة على "فتحات داخلية"، ولا يمكن الوصول إلى تلك الأشياء بالوسيط. راجع الفقرة المخصصة لها أعلاه. وينطبق الشيء نفسه على خاصيات الصنف الخاصة، إذ تنفيذها داخليًا باستخدام الفتحات. لذا يجب أن تحتوي استدعاءات دوالّ الوسيط على الكائن المستهدف بدل this للوصول إليها. لا يمكن اعتراض اختبارات المساواة الصارمة للكائن ===. الأداء: تعتمد المقاييس على المحرك، ولكن عمومًا إن الوصول إلى الخاصية باستخدام وكيل بسيط سيستغرق وقتًا أطول بعض الشيء. عمليًا يهتم بها البعض لعدم حدوث اختناق في الأداء "عنق الزجاجة". تمارين خطأ في قراءة الخاصيات غير موجودة في الكائن الأصلي عادةً ، تؤدي محاولة قراءة خاصية غير موجودة إلى إعادة النتيجة undefined. أنشئ وسيطًا يعيد خطأ عند محاولة قراءة خاصية غير موجودة في الكائن الأصلي بدلًا من ذلك. يمكن أن يساعد ذلك في الكشف عن الأخطاء البرمجية مبكرًا. اكتب دالّة wrap(target)‎ والّتي تأخذ كائنًا target وتعيد وسيطًا والّذي سيُضيف خصائص وظيفية أخرى. هكذا يجب أن تعمل: let user = { name: "John" }; function wrap(target) { return new Proxy(target, { /* your code */ }); } user = wrap(user); alert(user.name); // John alert(user.age); // ReferenceError: Property doesn't exist "age" الحل let user = { name: "John" }; function wrap(target) { return new Proxy(target, { get(target, prop, receiver) { if (prop in target) { return Reflect.get(target, prop, receiver); } else { throw new ReferenceError(`Property doesn't exist: "${prop}"`) } } }); } user = wrap(user); alert(user.name); // John alert(user.age); // ReferenceError: Property doesn't exist "age" الوصول إلى الدليل [‎-1] في فهرس المصفوفة يمكننا الوصول إلى عناصر المصفوفة باستخدام الفهارس السلبية في بعض لغات البرمجة، محسوبةً بذلك من نهاية المصفوفة. هكذا: let array = [1, 2, 3]; array[-1]; // 3, آخر عنصر في المصفوفة array[-2]; // 2, خطوة للوراء من نهاية المصفوفة array[-3]; // 1, خطوتين للوراء من نهاية المصفوفة بتعبيرٍ آخر ، فإن array[-N]‎ هي نفس array[array.length - N]‎. أنشئ وسيطًا لتنفيذ هذا السلوك. هكذا يجب أن تعمل: let array = [1, 2, 3]; array = new Proxy(array, { /* your code */ }); alert( array[-1] ); // 3 alert( array[-2] ); // 2 // بقية الخصائص الوظيفية الأخرى يجب أن تبقى كما هي الحل let array = [1, 2, 3]; array = new Proxy(array, { get(target, prop, receiver) { if (prop < 0) { // ‫حتى وإن وصلنا للمصفوفة هكذا arr[1]‎ // ‫إن المتغيّر prop عبارة عن سلسلة نصية لذا نحتاج لتحويله إلى رقم prop = +prop + target.length; } return Reflect.get(target, prop, receiver); } }); alert(array[-1]); // 3 alert(array[-2]); // 2 المراقب أنشئ تابع makeObservable(target)‎ الّذي تجعل الكائن قابلاً للمراقبة 'من خلال إعادة وسيط. إليك كيفية العمل: function makeObservable(target) { /* your code */ } let user = {}; user = makeObservable(user); user.observe((key, value) => { alert(`SET ${key}=${value}`); }); user.name = "John"; // alerts: SET name=John وبعبارة أخرى، فإن الكائن الّذي سيعاد من خلال makeObservable يشبه تمامًا الكائن الأصلي، ولكنه يحتوي أيضًا على الطريقة observe(handler)‎ الّتي ستضبط تابع المُعالج ليستدعى عند أي تغيير في الخاصية. عندما تتغير خاصية ما، يستدعى handler(key, value)‎ مع اسم وقيمة الخاصية. ملاحظة. في هذا التمرين، يرجى الاهتمام بضبط الخاصيات فقط. يمكن تنفيذ عمليات أخرى بطريقة مماثلة. الحل يتكون الحل من جزئين: عندما يستدعى ‎.observe(handler)‎، نحتاج إلى حفظ المعالج في مكان ما، حتى نتمكن من الاتصال به لاحقًا. يمكننا تخزين المعالجات في الكائن مباشرة، باستخدام الرمز الخاص بنا كمفتاح خاصية. سنحتاج لوسيط مع الاعتراض set لاستدعاء المعالجات عند حدوث أي تغيير. let handlers = Symbol('handlers'); function makeObservable(target) { // 1. هيئ مخزّن المعالجات target[handlers] = []; // احتفظ بتوابع المعالج في مصفوفة للاستدعاءات اللاحقة target.observe = function(handler) { this[handlers].push(handler); }; // ‫2. أنشئ وسيط ليعالج التغييرات return new Proxy(target, { set(target, property, value, receiver) { let success = Reflect.set(...arguments); // وجه العملية إلى الكائن if (success) { // إن حدث خطأ ما في ضبط الخاصية // استدعي جميع المعالجات target[handlers].forEach(handler => handler(property, value)); } return success; } }); } let user = {}; user = makeObservable(user); user.observe((key, value) => { alert(`SET ${key}=${value}`); }); user.name = "John"; ترجمة -وبتصرف- للفصل Proxy and Reflect من كتاب The JavaScript language
  9. إن طريقة الاستيراد والتصدير التي تحدثنا عنها في الفصل السابق، تصدير الوحدات واستيرادها تدعى بالطريقة "الثابتة". إذ أنّ صياغتها بسيطة وصارمة للغاية. دعنا في البداية نوضح بعض الأمور، أولًا، لا يمكننا إنشاء أي وسطاء للتعليمة import إنشاءً ديناميكيًا. إذ يجب أن يكون مسار الوِحدة سلسلة أولية (primitive)، ولا يجب أن تكون استدعاءً لدالة معينة. فهذا لن ينجح: import ... from getModuleName(); // خطأ، مسموح استخدام السلاسل فقط ثانيًا، لا يمكننا استخدام الاستيراد المشروط (في حال حدوث شرط معين استورد مكتبة) أو الاستيراد أثناء التشغيل: if(...) { import ...; // ‫خطأ غير مسموح بذلك! } { import ...; // ‫خطأ، لا يمكننا وضع تعليمة import في أي كتلة } وذلك لأن تعليمتي import/export تهدفان لتوفير العمود الفقري لبنية الشيفرة. وهذا أمر جيد، إذ يمكننا تحليل بنية الشيفرة، وتجميع الوحدات وتحزيمها في ملف واحد من خلال أدوات خاصة، ويمكننا أيضًا إزالة عمليات التصدير غير المستخدمة (هزّ الشجرة -بهدف سقوط الأوراق اليابسة). هذا ممكن فقط لأن هيكلية التعليمتين import/export بسيطة وثابتة. ولكن كيف يمكننا استيراد وِحدة استيرادً ديناميكيًا بحسب الطلب؟ تعبير الاستيراد يُحمّل التعبير import (module)‎ الوِحدة ويُرجع وعدًا، والّذي يُستبدل بكائن الوِحدة، ويحتوي هذا الأخير على كافة عمليات التصدير الخاصة بالكائن. ويُستدعى من أي مكان في الشيفرة البرمجية. ونستطيع استخدامه ديناميكيًا في أي مكان من الشيفرة البرمجية، فمثلًا: let modulePath = prompt("Which module to load?"); import(modulePath) .then(obj => <module object>) .catch(err => <loading error, e.g. if no such module>) أو يمكننا استخدام let module = await import(modulePath)‎ إن كنا بداخل دالّة غير متزامنة. فمثلًا، ليكن لدينا الوِحدة التالية say.js: // ? say.js export function hi() { alert(`Hello`); } export function bye() { alert(`Bye`); } … ثم يكون الاستيراد الديناميكي هكذا: let {hi, bye} = await import('./say.js'); hi(); bye(); أو إذا كان say.js يحتوي على التصدير المبدئي: // ? say.js export default function() { alert("Module loaded (export default)!"); } … بعد ذلك، من أجل الوصول إليه، يمكننا استخدام الخاصية default لكائن الوِحدة: let obj = await import('./say.js'); let say = obj.default; // ‫أو بسطرٍ واحد هكذا:‎ // let {default: say} = await import('./say.js'); say(); إليك المثال الكامل: الملف say.js: export function hi() { alert(`Hello`); } export function bye() { alert(`Bye`); } export default function() { alert("Module loaded (export default)!"); } الملف index.html: <!doctype html> <script> async function load() { let say = await import('./say.js'); say.hi(); // Hello! say.bye(); // Bye! say.default(); // Module loaded (export default)! } </script> <button onclick="load()">Click me</button> وإليك النتائج في هذا المثال حي. لاحظ كيف تعمل عمليات الاستيراد الديناميكية في السكربتات العادية، ولا تتطلب script type ="module"‎. لاحظ أيضًا على الرغم من أن تعليمة import()‎ تشبه طريقة استدعاء دالّة، إلا أنها صياغة خاصة ويحدث هذا التشابه فقط لاستخدام الأقواس (على غرار super ()‎). لذلك لا يمكننا نسخ تعليمة import إلى متغير، أو استخدام call/apply معها. إذ هي ليست دالّة. ترجمة -وبتصرف- للفصل Dynamic imports من كتاب The JavaScript language
  10. لمُوجِّهات (تعليمات) الاستيراد والتصدير أكثر من صياغة برمجية واحدة رأينا في الفصل السابق، مقدمة إلى الوحدات استعمالًا بسيطًا له، فهيًا نرى بقية الاستعمالات. التصدير قبل التصريح يمكننا أن نقول لأيّ تصريح بأنّه مُصدّر بوضع عبارة export قبله، كان التصريح عن متغيّر أو عن دالة أو عن صنف. فمثلًا، التصديرات هنا كلّها صحيحة: // تصدير مصفوفة export let months = ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; // تصدير ثابت export const MODULES_BECAME_STANDARD_YEAR = 2015; // تصدير صنف export class User { constructor(name) { this.name = name; } } ملاحظة: لا يوجد فواصل منقوطة بعد تعليمة التصدير للأصناف أو الدوالّ لاحظ أن تعليمة export قبل الصنف أو الدالة لا يجعلها تعابير الدوالّ. ولو أنه يصُدرها، لكنه لا يزال تعريفًا للدالّة أو الصنف. لا توصي معظم الأدلة التعليمية بوضع فاصلة منقوطة بعد تعريف الدوال والأصناف. لهذا السبب لا داعي للفاصلة المنقوطة في نهاية التعليمة export class والتعليمة export function: export function sayHi(user) { alert(`Hello, ${user}!`); } // لاحظ لا يوجد فاصلة منقوطة في نهاية التعريف التصدير بعيدًا عن التصريح كما يمكننا وضع عبارة export لوحدها. هنا نصرّح أولًا عن الدالتين وبعدها نُصدّرهما: // ? say.js function sayHi(user) { alert(`Hello, ${user}!`); } function sayBye(user) { alert(`Bye, ${user}!`); } export {sayHi, sayBye}; // تصدير قائمة من المتغيرات أو… يمكننا تقنيًا وضع export أعلى الدوال أيضًا. عبارة استيراد كل شيء عادةً نضع قائمة بما نريد استيراده في أقواس معقوفة import {...}‎، هكذا: // ? main.js import {sayHi, sayBye} from './say.js'; sayHi('John'); // Hello, John! sayBye('John'); // Bye, John! ولكن لو أردنا استيراد وحدات كثيرة، فيمكننا استيراد كلّ شيء كائنًا واحدًا باستعمال import * as <obj>‎ هكذا: // ? main.js import * as say from './say.js'; say.sayHi('John'); say.sayBye('John'); يقول المرء من النظرة الأولى ”استيراد كلّ شيء فكرة جميلة جدًا، وكتابة الشيفرة سيكون أسرع. أساسًا لمَ نقول جهارةً ما نريد استيراده؟“ ذلك… لأسباب وجيهة. أدوات البناء الحديثة (مثل: webpack وغيرها) لنقل مثلًا بأنّا أضفنا مكتبة خارجية اسمها say.js إلى مشروعنا، وفيها دوالّ عديدة: // ? say.js export function sayHi() { ... } export function sayBye() { ... } export function becomeSilent() { ... } هكذا نستعمل واحدة فقط من دوالّ say.js في مشروعنا: // ? main.js import {sayHi} from './say.js'; …حينها تأتي أداة التحسين وترى ذلك، فتُزيل الدوال الأخرى من الشيفرة … بذلك يصغُر حجم الملف المبني. هذا ما نسميه هز الشجر (لتَسقطَ الأوراق اليابسة فقط). لو وضّحنا بالضبط ما نريد استيراده فيمكننا كتابته باسم أقصر: sayHi()‎ بدل say.sayHi()‎. بكتابة قائمة الاستيراد جهارةً نستطيع أن نفهم بنية الشيفرة دون الخوض في التفاصيل (أي نعرف ما نستعمل من وحدات، وأين نستعملها). هذا يسهّل دعم الشيفرة وإعادة كتابتها لو تطلّب الأمر. استيراد كذا بالاسم كذا as يمكننا كذلك استعمال as لاستيراد ما نريد بأسماء مختلفة. فمثلًا يمكننا استيراد الدالة sayHi في المتغير المحلي hi لنختصر الكلام، واستيراد sayBye على أنّها bye: // ? main.js import {sayHi as hi, sayBye as bye} from './say.js'; hi('John'); // Hello, John! bye('John'); // Bye, John! تصدير كذا بالاسم كذا as نفس صياغة الاستيراد موجودة أيضًا للتصدير export. فلنصدّر الدوال على أنّها hi وbye: // ? say.js ... export {sayHi as hi, sayBye as bye}; الآن صارت hi وbye هي الأسماء ”الرسمية“ للشيفرات الخارجية وستُستعمل عند الاستيراد: // ? main.js import * as say from './say.js'; // لاحِظ الفرق say.hi('John'); // Hello, John! say.bye('John'); // Bye, John! التصدير المبدئي في الواقع العملي، ثمّة نوعين رئيسين من الوحدات. تلك التي تحتوي مكتبة (أي مجموعة من الدوال) مثل وحدة say.js أعلاه. وتلك التي تصرّح عن كيانٍ واحد مثل وحدة user.js التي تُصدّر class User فقط. عادةً ما يُحبّذ استعمال الطريقة الثانية كي يكون لكلّ ”شيء“ وحدةً خاصة به. ولكن هذا بطبيعة الحال يطلب ملفات كثيرة إذ يطلب كلّ شيء وحدةً تخصّه باسمه، ولكنّ هذه ليست بمشكلة، أبدًا. بل على العكس هكذا يصير التنقل في الشيفرة أسهل (لو كانت تسمية الملفات مرضية ومرتّبة في مجلدات). توفر الوِحدات طريقة لصياغة عبارة export default (التصدير المبدئي) لجعل "سطر تصدير واحد لكلّ وِحدة" تبدو أفضل. ضَع export default قبل أيّ كيان لتصديره: // ? user.js export default class User { // ‫نُضيف ”default“ فقط constructor(name) { this.name = name; } } لكلّ ملف سطر تصدير export default واحد لا أكثر. وبعدها… نستورد الكيان بدون الأقواس المعقوفة: // ? main.js import User from './user.js'; // ‫لا نضع {User}، بل User new User('John'); أسطر الاستيراد التي لا تحتوي الأقواس المعقوفة أجمل من تلك التي تحتويها. يشيع خطأ نسيان تلك الأقواس حين يبدأ المطورون باستعمال الوِحدات. لذا تذكّر دائمًا، يطلب سطر الاستيراد import أقواس معقوفة للكيانات المُصدّرة والتي لها أسماء، ولا يطلبها لتلك المبدئية. التصدير الذي له اسم التصدير المبدئي export class User {...}‎ export default class User {...}‎ import {User} from ...‎ import User from ...‎ table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } يمكننا نظريًا وضع النوعين من التصدير معًا في نفس الوحدة (الذي له اسم والمبدئي)، ولكن عمليًا لا يخلط الناس عادةً بينها، بل للوِحدة إمّا تصديرات لها أسماء، أو التصدير المبدئي. ولأنّه لا يمكن أن يكون لكلّ ملف إلا تصديرًا مبدئيًا واحدًا، فيمكن للكيان الذي صُدّر ألّا يحمل أيّ اسم. فمثلًا التصديرات أسفله كلّها صحيحة مئة في المئة: export default class { // لا اسم للصنف constructor() { ... } } export default function(user) { // لا اسم للدالة alert(`Hello, ${user}!`); } // نُصدّر قيمةً واحدة دون صنع متغيّر export default ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; لا مشكلة بتاتًا بعدم كتابة الاسم إذ لا نرى export default إلّا مرّة في الملف، بهذا تعرف تمامًا أسطر import (بدون استعمال الأقواس المعقوفة) ما عليها استيراده. ولكن دون default فهذا التصدير سيُعطينا خطأً: export class { // Error! (non-default export needs a name) constructor() {} } الاسم المبدئي تُستعمل في حالات معيّنة الكلمة المفتاحية default للإشارة إلى التصدير المبدئي. فمثلًا لتصدير الدالة بنحوٍ منفصل عن تعريفها: function sayHi(user) { alert(`Hello, ${user}!`); } // ‫كما لو أضفنا ”export default“ قبل الدالة export {sayHi as default}; أو لنقل بأنّ الوحدة user.js تُصدّر شيئًا واحدًا ”مبدئيًا“ وأخرى لها أسماء (نادرًا ما يحدث، ولكنّه يحدث): // ? user.js export default class User { constructor(name) { this.name = name; } } export function sayHi(user) { alert(`Hello, ${user}!`); } هكذا نستورد التصدير المبدئي مع ذلك الذي لديه اسم: // ? main.js import {default as User, sayHi} from './user.js'; new User('John'); وأخيرًا، حين نستورد كلّ شيء * على أنّه كائن، فستكون خاصية default هي كما التصدير المبدئي: // ? main.js import * as user from './user.js'; let User = user.default; // the default export new User('John'); كلمتين بخصوص سوء التصديرات المبدئية التصديرات التي لها أسماء تكون صريحة، أي أنّها تقول تمامًا ما الّذي يجب أن نستورده، وبذلك يكون لدينا هذه المعلومات منهم، وهذا شيء جيد. تُجبرنا التصديرات التي لها أسماء باستعمال الاسم الصحيح كما هو بالضبط لاستيراد الوحدة: import {User} from './user.js'; // ‫ولن تعمل import {MyUser}‎ إذ يجب أن يكون الاسم {User} بينما في حالة التصدير المبدئي نختار نحن الاسم حين نستورد الوِحدة: import User from './user.js'; // works import MyUser from './user.js'; // works too // ‫ويمكن أيضًا أن تكون ”استورِد كل شيء“ import Anything... وستعمل بلا أدنى مشكلة هذا قد يؤدّي إلى أن يستعمل أعضاء الفريق أسماء مختلفة لاستيراد الشيء ذاته، وهذا طبعًا ليس بالجيد. عادةً ولنتجنّب ذلك ونُحافظ على اتساق الشيفرة، نستعمل القاعدة القائلة بأنّ أسماء المتغيرات المُستورَدة يجب أن تُوافق أسماء الملفات، هكذا مثلًا: import User from './user.js'; import LoginForm from './loginForm.js'; import func from '/path/to/func.js'; ... مع ذلك تنظُر بعض الفِرق لهذا الأمر على أنه عقبة للتصديرات المبدئية فتفضّل استعمال التصديرات التي لها اسم دومًا. فحتّى لو كانت نصدّر شيئًا واحدًا فقط فما زالت تُصدّره باسم دون استعمال default. كما يسهّل هذا إعادة التصدير (طالِع أسفله). إعادة التصدير تُتيح لنا صياغة ”إعادة التصدير“ export ... from ...‎ استيراد الأشياء وتصديرها مباشرةً (ربما باسم آخر) هكذا: export {sayHi} from './say.js'; // ‫نُعيد تصدير sayHi export {default as User} from './user.js'; // نُعيد تصدير المبدئي ولكن فيمَ نستعمل هذا أصلًا؟ لنرى مثالًا عمليًا. لنقل بأننا نكتب ”حزمة“، أي مجلدًا فيه وحدات كثيرة وأردنا تصدير بعض ميزاتها إلى الخارج (تتيح لنا الأدوات مثل NPM نشر هذه الحزم وتوزيعها)، ونعلم أيضًا أن الكثير من وحداتها ما هي إلّا وحدات مُساعِدة يمكن أن تكون بنية الملفات هكذا: auth/ index.js user.js helpers.js tests/ login.js providers/ github.js facebook.js ... ونريد عرض مزايا الحزمة باستعمال نقطة واحدة (أي الملف الأساسي auth/index.js) لتُستعمل هكذا: import {login, logout} from 'auth/index.js' الفكرة هي عدم السماح للغرباء (أي المطوّرين مستعملي الحزمة) بالتعديل على البنية الداخلية والبحث عن الملفات داخل مجلد الحزمة. نريد تصدير المطلوب فقط في auth/index.js وإخفاء الباقي عن أعين المتطفّلين. نظرًا لكون الوظيفة الفعلية المصدّرة مبعثرة بين الحزمة، يمكننا استيرادها إلى auth/index.js وتصديرها من هنالك أيضًا: // ? auth/index.js // ‫اِستورد login/logout وصدِرهن مباشرةً import {login, logout} from './helpers.js'; export {login, logout}; // ‫استورد الملف المبدئي كـ User وصدره من جديد import User from './user.js'; export {User}; ... والآن يمكن لمستخدمي الحزمة الخاصة بنا استيرادها هكذا import {login} from "auth/index.js"‎. إن الصياغة export ... from ...‎ ماهي إلا اختصار للاستيراد والتصدير: // ? auth/index.js // ‫اِستورد login/logout وصدِرهن مباشرةً export {login, logout} from './helpers.js'; // ‫استورد الملف المبدئي كـ User وصدره من جديد export {default as User} from './user.js'; ... إعادة تصدير التصديرات المبدئية يحتاج التصدير المبدئي لمعالجة منفصلة عند إعادة التصدير. لنفترض أن لدينا user.js، ونود إعادة تصدير الصنف User منه: // ? user.js export default class User { // ... } لن تعمل التعليمة export User from './user.js'‎. ما الخطأ الذي حدث؟ ولكن هذا الخطأ في صياغة! لإعادة تصدير الملفات المصدرة إفتراضيًا ، علينا كتابة export {default as User}‎ ، كما في المثال أعلاه. تعيد التعليمة export * from './user.js'‎ تصدير التصديرات الّتي لها أسماء فقط، ولكنها تتجاهل التصديرات المبدئية. إذا رغبنا في إعادة تصدير التصديرات المبدئية والتي لها أسماء أيضًا، فسنحتاج إلى العبارتين: export * from './user.js'; // لإعادة تصدير التصديرات الّتي لها أسماء export {default} from './user.js'; // لإعادة تصدير التصديرات المبدئية هذه الغرابة في طريقة إعادة تصدير التصديرات المبدئية هي من أحد الأسباب لجعل بعض المطورين لا يحبونها. خلاصة والآن سنراجع جميع أنواع طرق التصدير export التي تحدثنا عنها في هذا الفصل والفصول السابقة. تحقق من معلوماتك بقراءتك لهم وتذكر ما تعنيه كلُّ واحدةٍ منهم: قبل التعريف عن صنف / دالّة / ..: export [default] class/function/variable ...‎ تصدير مستقل: export {x [as y], ...}‎. إعادة التصدير: export {x [as y], ...} from "module"‎ export * from "module"‎ (لا يُعيد التصدير المبدئي). export {default [as y]} from "module"‎ (يعيد التصدير المبدئي). استيراد: الصادرات التي لها أسماء من الوِحدة: import {x [as y], ...} from "module"‎ التصدير المبدئي: import x from "module"‎ import {default as x} from "module"‎ استيراد كل شيء: import * as obj from "module"‎ استيراد الوحدة (وشغِّل شيفرتها البرمجية)، ولكن لا تُسندها لمتغير: import "module"‎ لا يهم مكان وضع عبارات (تعليمات) import/export سواءً في أعلى أو أسفل السكربت فلن يغير ذلك أي شيء. لذا تقنيًا تعدُّ هذه الشيفرة البرمجية لا بأس بها: sayHi(); // ... import {sayHi} from './say.js'; // اِستورد في نهاية الملف عمليًا عادة ما تكون تعليمات الاستيراد في بداية الملف فقط لتنسيق أفضل للشيفرة. لاحظ أن تعليمتي import/export لن يعملا إن كانا في داخل جملة شرطية. لن يعمل الاستيراد الشرطي مثل هذا المثال: if (something) { import {sayHi} from "./say.js"; // Error: import must be at top level } .. ولكن ماذا لو احتجنا حقًا لاستيراد شيء ما بشروط معينة؟ أو في وقتٍ ما؟ مثل: تحميل الوِحدة عند الطلب، عندما تكون هناك حاجة إليها حقًا؟ سنرى الاستيراد الديناميكي في المقالة التالية. ترجمة -وبتصرف- للفصل Export and Import من كتاب The JavaScript language
  11. سنرى سريعًا بينما تطبيقنا يكبُر حجمًا وتعقيدًا بأنّ علينا تقسيمه إلى ملفات متعدّدة، أو ”وحدات“ (module). عادةً ما تحتوي الوِحدة على صنف أو مكتبة فيها دوالّ. كانت محرّكات جافاسكربت تعمل لفترة طويلة جدًا دون أيّ صياغة وِحدات على مستوى اللغة، ولم تكن هذه بالمشكلة إذ أنّ السكربتات سابقًا كانت بسيطة وسهلة ولم يكن هناك داعٍ فعلي للوِحدات. ولكن كالعادة صارت السكربتات هذه أكثر تعقيدًا وأكبر، فكان على المجتمع اختراع طرائق مختلفة لتنظيم الشيفرات في وحدات (أو مكتبات خاصّة تُحمّل تلك الوِحدات حين الطلب). مثال: AMD: هذه إحدى نُظم المكتبات القديمة جدًا والتي كتبت تنفيذها بدايةً المكتبة require.js. CommonJS: نظام الوِحدات الذي صُنِع لخوادم Node.js. UMD: نظام وِحدات آخر (اقتُرح ليكون للعموم أجمعين) وهو متوافق مع AMD وCommonJS. أمّا الآن فهذه المكتبات صارت (أو تصير، يومًا بعد آخر) جزءًا من التاريخ، ولكن مع ذلك سنراها في السكربتات القديمة. ظهر نظام الوِحدات (على مستوى اللغة) في المعيار عام 2015، وتطوّر شيئًا فشيئًا منذئذ وصارت الآن أغلب المتصفّحات الرئيسة (كما و Node.js) تدعمه. لذا سيكون أفضل لو بدأنا دراسة عملها من الآن. ما الوحدة؟ الوِحدة هي ملف، فقط. كلّ نص برمجي يساوي وحدة واحدة. يمكن أن تُحمّل الوِحدات بعضها البعض وتستعمل توجيهات خاصة مثل التصدير export والاستيراد import لتتبادل الميزات فيما بينها وتستدعي الدوالّ الموجودة في وحدة ص، من وحدة س: تقول الكلمة المفتاحية export للمتغيرات والدوالّ بأنّ الوصول إليها من خارج الوِحدة الحالية هو أمر مُتاح. وتُتيح import استيراد تلك الوظائف من الوِحدات الأخرى. فمثلًا لو كان لدينا الملف sayHi.js وهو يُصدّر دالّةً من الدوالّ: // ? sayHi.js export function sayHi(user) { alert(`Hello, ${user}!`); } فيمكن لملف آخر استيراده واستعمالها: // ? main.js import {sayHi} from './sayHi.js'; alert(sayHi); // function... نوعها دالة sayHi('John'); // Hello, John! تتوجه تعليمة import للوِحدة ‎./sayHi.js عبر المسار النسبي المُمرر لها. ويسند التابع sayHi للمتغيّر الذي يحمل نفس اسم التابع. لنشغّل المثال في المتصفّح. تدعم الوِحدات كلمات مفتاحية ومزايا خاصة، لذلك علينا إخبار المتصفّح بأنّ هذا السكربت هو وِحدة ويجب أن يُعامل بهذا النحو، ذلك باستعمال الخاصية ‎<script type="module">‎. هكذا: ملف index.html: <!doctype html> <script type="module"> import {sayHi} from './say.js'; document.body.innerHTML = sayHi('John'); </script> ملف say.js: export function sayHi(user) { return `Hello, ${user}!`; } يجلب المتصفّح الوِحدة تلقائيًا ويقيم الشيفرة البرمجية بداخلها (ويستورد جميع الوحدات المتعلقة بها إن لزم الأمر)، وثمّ يشغلها وتكون نتيجة ما سبق: Hello, John! ميزات الوحدات الأساسية ولكن ما الفرق بين الوِحدات والسكربتات (الشيفرات) "العادية“ تلك؟ للوِحدات ميزات أساسية تعمل على محرّكات جافاسكربت للمتصفّحات وللخوادم على حدّ سواء. الوضع الصارم الإفتراضي تستخدم الوِحدات الوضع الصارم تلقائيًا فمثلًا إسناد قيمة لمتحول غير معرّف سينتج خطأ. <script type="module"> a = 5; // خطأ </script> النطاق على مستوى الوحدات كلّ وِحدة لها نطاق عالي المستوى خاص بها. بتعبيرٍ آخر، لن يُنظر للمتغيّرات والدوالّ من الوحدات الأخرى، وإنما يكون نطاق المتغيرات محلي. نرى في المثال أدناه أنّا حمّلنا نصّين برمجيين، ويحاول الملف hello.js استعمال المتغير user المصرّح عنه في الملف user.js ولا يقدر: ملف index.html: <!doctype html> <script type="module" src="user.js"></script> <script type="module" src="hello.js"></script> ملف user.js: let user = "John"; الملف hello.js: alert(user); لاحظ أن نتيجة ما سبق هي لا شيء لأن الدالة alert لم تتعرف على المتغير user الغير موجود، فكل وحدة لها متغيرات خاصة بها ويمكنها تصدير (عبر export) ما تريد للآخرين من خارجها رؤيته، واستيراد (عبر import) ما تحتاج استعماله. لذا علينا استيراد user.js وhello.js وأخذ المزايا المطلوبة منهما بدل الاعتماد على المتغيّرات العمومية. هذه النسخة الصحيحة من الشيفرة: ملف index.html: <!doctype html> <script type="module" src="hello.js"></script> ملف user.js: export let user = "John"; الملف hello.js: import {user} from './user.js'; document.body.innerHTML = user; // John يوجد في المتصفح نطاق مستقل عالي المستوى. وهو موجود أيضًا للوحدات ‎<script type="module">‎: <script type="module"> // سيكون المتغير مرئي في مجال هذه الوِحدة فقط let user = "John"; </script> <script type="module"> alert(user); // ‫خطأ: المتغير user غير معرّف </script> ولو أردنا أن ننشئ متغيرًا عامًا على مستوى النافذة يمكننا تعيينه صراحة للمتغيّر window ويمكننا الوصول إليه هكذا window.user. ولكن لابد من وجود سبب وجيهٍ لذلك. تقييم شيفرة الوِحدة لمرة واحدة فقط لو استوردتَ نفس الوِحدة في أكثر من مكان، فلا تُنفّذ شيفرتها إلّا مرة واحدة، وبعدها تُصدّر إلى من استوردها. ولهذا توابع مهمّ معرفتها. لنرى بعض الأمثلة. أولًا، لو كان لشيفرة الوِحدة التي ستُنفّذ أيّ تأثيرات (مثل عرض رسالة أو ما شابه)، فاستيرادها أكثر من مرّة سيشغّل ذلك التأثير مرة واحدة، وهي أول مرة فقط: // ? alert.js alert("Module is evaluated!"); // ‫نُفّذت شيفرة الوِحدة! // نستورد نفس الوِحدة من أكثر من ملف // ? 1.js import `./alert.js`; // ‫نُفّذت شيفرة الوِحدة! // ? 2.js import `./alert.js`; // (لا نرى شيئًا هنا) في الواقع، فشيفرات الوِحدات عالية المستوى في بنية البرمجية لا تُستعمل إلّا لتمهيد بنى البيانات الداخلية وإنشائها. ولو أردنا شيئًا نُعيد استعماله، نُصدّر الوِحدة. الآن حان وقت مثال مستواه متقدّم أكثر. لنقل بأنّ هناك وحدة تُصدّر كائنًا: // ? admin.js export let admin = { name: "John" }; لو استوردنا هذه الوِحدة من أكثر من ملف، فلا تُنفّذ شيفرة الوِحدة إلّا أول مرة، حينها يُصنع كائن المدير admin ويُمرّر إلى كلّ من استورد الوِحدة. وهكذا تستلم كلّ الشيفرات كائن مدير admin واحد فقط لا أكثر ولا أقل: // ? 1.js import {admin} from './admin.js'; admin.name = "Pete"; // ? 2.js import {admin} from './admin.js'; alert(admin.name); // Pete كِلا الملفين ‎1.js و ‎2.js سيستوردان نفس الكائن ‫والتغييرات الّتي ستحدثُ في الملف ‎1.js ستكون مرئية في الملف ‎2.js. ولنؤكد مجددًا، تُنفذّ الوِحدة لمرة واحدة فقط. وتُنشئ الوِحدات المراد تصديرها وتُشارك بين المستوردين لذا فإن تغير شيء ما في كائن admin فسترى الوِحدات الأخرى ذلك. يتيح لنا هذا السلوك ”ضبط“ الوِحدة عند أوّل استيراد لها، فنضبط خاصياتها المرة الأولى، ومتى ما استوُردت مرة أخرى تكون جاهزة. فمثلًا قد تقدّم لنا وحدة admin.js بعض المزايا ولكن تطلب أن تأتي امتيازات الإدارة من خارج كائن admin إلى داخله: // ? admin.js export let admin = { }; export function sayHi() { alert(`Ready to serve, ${admin.name}!`); } نضبط في init.js (أوّل نص برمجي لتطبيقنا) المتغير admin.name. بعدها سيراه كلّ من أراد بما في ذلك الاستدعاءات من داخل وحدة admin.js نفسها: // ? init.js import {admin} from './admin.js'; admin.name = "Pete"; ويمكن لوحدة أخرى استعمال admin.name: // ? other.js import {admin, sayHi} from './admin.js'; alert(admin.name); // Pete sayHi(); // Ready to serve, Pete! import.meta يحتوي الكائن import.meta على معلومات الوِحدة الحالية. ويعتمد محتواها على البيئة الحالية، ففي المتصفّحات يحتوي على عنوان النص البرمجي أو عنوان صفحة الوِب الحالية لو كان داخل HTML: <script type="module"> alert(import.meta.url); // ‫عنوان URL للسكربت (عنوان URL لصفحة HTML للسكربت الضمني) </script> this في الوِحدات ليست معرّفة قد تكون هذه الميزة صغيرة، ولكنّا سنذكرها ليكتمل هذا الفصل. في الوحدات، قيمة this عالية المستوى غير معرّفة. وازن بينها وبين السكربتات غير المعتمدة على الوحدات، إذ ستكون this كائنًا عامًا: <script> alert(this); // window </script> <script type="module"> alert(this); // غير معرّف </script> الميزات الخاصة بالمتصفحات كما أن هناك عدّة فروق تخصّ المتصفحات السكربتات (المعتمدة على الوحدات) بالنوع type="module"‎ موازنةً بتلك العادية. لو كنت تقرأ هذا الفصل لأول مرة، أو لم تكن تستعمل المحرّك في المتصفّح فيمكنك تخطّي هذا القسم. سكربتات الوِحدات مؤجلة دائمًا ما تكون سكربتات الوِحدات مؤجلة، ومشابهة لتأثير السِمة defer (الموضحة في هذا المقال)، لكل من السكربتات المضمّنة والخارجية. أي وبعبارة أخرى: تنزيل السكربتات المعتمدة على الوِحدات الخارجية ‎<script type="module" src=‎"...">‎ لا تُوقف معالجة HTML فتُحمّل بالتوازي مع الموارد الأخرى. تنتظر السكربتات المعتمدة على الوِحدات حتّى يجهز مستند HTML تمامًا (حتّى لو كانت صغيرة وحُمّلت بنحوٍ أسرع من HTML) وتُشغّل عندها. تحافظ على الترتيب النسبي للسكربتات: فالسكربت ذو الترتيب الأول ينفذّ أولًا. ويسبّب هذا بأن ”ترى“ السكربتات المعتمدة على الوِحدات صفحة HTML المحمّلة كاملة بما فيه عناصر الشجرة أسفلها. مثال: <script type="module"> alert(typeof button); // ‫كائن (object): يستطيع السكربت رؤية العناصر أدناه // بما أن الوِحدات مؤجلة. سيُشغل السكربت بعد تحميل كامل الصفحة </script> Compare to regular script below: <script> alert(typeof button); // ‫خطأ: الزر (button) غير معرّف. لن يستطيع السكربت رؤية العناصر أدناه // السكربت العادي سيُشغل مباشرة قبل أن يُستكمل تحميل الصفحة </script> <button id="button">Button</button> لاحِظ كيف أنّ النص البرمجي الثاني يُشغّل فعليًا قبل الأول! لذا سنرى أولًا undefined وبعدها object. وذلك بسبب كون عملية تشغيل الوِحدات مُؤجلة لذلك سننتظر لاكتمال معالجة المستند. نلاحظ أن السكربت العادي سيُشغلّ مباشرة بدون تأجيل ولذا سنرى نتائجه أولًا. علينا أن نحذر حين نستعمل الوِحدات إذ أنّ صفحة HTML تظهر بينما الوِحدات تُحمّل، وبعدها تعمل الوحدات. بهذا يمكن أن يرى المستخدم أجزاءً من الصفحة قبل أن يجهز تطبيق جافاسكربت، ويرى بأنّ بعض الوظائف في الموقع لا تعمل بعد. علينا هنا وضع ”مؤشّرات تحميل“ أو التثبّت من أنّ الزائر لن يتشتّت بهذا الأمر. خاصية Async على السكربتات المضمّنة بالنسبة للسكربتات غير المعتمدة على الوِحدات فإن خاصية async (اختصارًا لكلمة Asynchronous أي غير المتزامن) تعمل على السكربتات الخارجية فقط. وتُشغل السكربتات غير المتزامنة مباشرة عندما تكون جاهزة، بشكل مستقل عن السكربتات الأخرى أو عن مستند HTML. تعمل السكربتات المعتمدة على الوِحدات طبيعيًا في السكربتات المضمّنة. فمثلًا يحتوي السكربت المُضمن أدناه على الخاصية async، لذلك سيُشغّل مباشرة ولن ينتظر أي شيء. وهو ينفذ عملية الاستيراد (اجلب الملف ‎./analytics.js) وشغله عندما يصبح جاهزًا، حتى وإن لم ينتهِ مستند HTML بعد. أو السكربتات الأُخرى لا تزال معلّقة. وهذا جيد للتوابع المستقلة مثل العدادات والإعلانات ومستمع الأحداث على مستوى المستند. في المثال أدناه، جُلبت جميع التبعيات (من ضمنها analytics.js).‫ ومن ثمّ شُغّل السكربت ولم ينتظر حتى اكتمال تحميل المستند أو السكربتات الأخرى. <script async type="module"> import {counter} from './analytics.js'; counter.count(); </script> السكربتات الخارجية تختلف السكربتات الخارجية التي تحتوي على السمة type="module"‎ في جانبين: تنفذ السكربتات الخارجية التي لها نفس القيمة للخاصية src مرة واحدة فقط. فهنا مثلًا سيُجلب السكربت my.js وينفذ مرة واحدة فقط. <script type="module" src="my.js"></script> <script type="module" src="my.js"></script> تتطلب السكربتات الخارجية التي تجلب من مصدر مستقل (موقع مختلف عن الأساسي) ترويسات CORS والموضحة في هذا المقال. بتعبير آخر إن جُلِبَ سكربت يعتمد على الوِحدات من مصدر معين فيجب على الخادم البعيد أن يدعم ترويسات السماح بالجلب Access-Control-Allow-Origin. يجب أن يدعم المصدر المستقل Access-Control-Allow-Origin (في المثال أدناه المصدر المستقل هو another-site.com) وإلا فلن يعمل السكربت. <script type="module" src="http://another-site.com/their.js"></script> وذلك سيضمن لنا مستوى أمان أفضل إفتراضيًا. لا يُسمح بالوحدات المجردة في المتصفح، يجب أن تحصل تعليمة import على عنوان URL نسبي أو مطلق. وتسمى الوِحدات التي بدون أي مسار بالوحدات المجردة. وهي ممنوع في تعليمة import. لنأخذ مثالًا يوضح الأمر، هذا import غير صالح: import {sayHi} from 'sayHi'; // خطأ وِحدة مجردة // ‫يجب أن تمتلك الوِحدة مسارًا مثل: '‎./sayHi.js' أو مهما يكُ موقع هذه الوِحدة تسمح بعض البيئات، مثل Node.js أو أدوات تجميع الوِحدات باستخدام الوِحدات المجردة، دون أي مسار، حيث أن لديها طرقها الخاصة للعثور على الوِحدات والخطافات لضبطها. ولكن حتى الآن لا تدعم المتصفحات الوِحدات المجردة. التوافقية باستخدام "nomodule" لا تفهم المتصفحات القديمة طريقة استخدام الوِحدات في الصفحات type ="module"‎.بل وإنها تتجاهل السكربت ذو النوعٍ غير المعروف. بالنسبة لهم، من الممكن تقديم نسخة مخصصة لهم باستخدام السمة nomodule: <script type="module"> alert("Runs in modern browsers"); </script> <script nomodule> alert("Modern browsers know both type=module and nomodule, so skip this"): alert("Old browsers ignore script with unknown type=module, but execute this."); </script> ‫المتصفحات الحديثة تعرف type=module و nomodule لذا لن تنفذ الأخير بينما ستتجاهل ‫المتصفحات القديمة الوسم ذو السِمة type=module ولكن ستنفذ وسم nomodule. أدوات البناء في الحياة الواقعية، نادرًا ما تستخدم وحدات المتصفح في شكلها "الخام". بل عادةّ نجمعها مع أداة خاصة مثل Webpack وننشرها على خادم النشر. إحدى مزايا استخدام المجمعات - فهي تمنح المزيد من التحكم في كيفية التعامل مع الوحدات، مما يسمح بالوحدات المجردة بل وأكثر من ذلك بكثير، مثل وحدات HTML/CSS. تؤدي أدوات البناء بعض الوظائف منها: جلب الوِحدة الرئيسية main، وهي الوِحدة المراد وضعها في وسم ‎<script type ="module">‎ في ملف HTML. تحليل التبعيات: تحليل تعليمات الاستيراد الخاصة بالملف الرئيسي وثم للملفات المستوردة أيضًا وما إلى ذلك. إنشاء ملفًا واحدًا يحتوي على جميع الوِحدات (مع إمكانية تقسيمهُ لملفات متعددة)، مع استبدال تعليمة import الأصلية بتوابع الحزم لكي يعمل السكربت. كما تدعم أنواع وحدات "خاصة" مثل وحدات HTML/CSS. يمكننا تطبيق عمليات تحويل وتحسينات أخرى في هذه العملية مثل: إزالة الشيفرات الّتي يتعذر الوصول إليها. إزالة تعليمات التصدير غير المستخدمة (مشابهة لعملية هز الأشجار وسقوط الأوراق اليابسة). إزالة العبارات الخاصة بمرحلة التطوير مثل console وdebugger. تحويل شيفرة جافاسكربت الحديثة إلى شيفرة أقدم باستخدام وظائف مماثلة للحزمة Babel. تصغير الملف الناتج (إزالة المسافات، واستبدال المتغيرات بأسماء أقصر، وما إلى ذلك). عند استخدامنا لأدوات التجميع سيُجمع السكربت ليصبح في ملف واحد (أو ملفات قليلة) ، تُستبدل تعليمات import/export بداخل السكربتات بتوابع المُجمّع الخاصة. لذلك لا يحتوي السكربت "المُجَمّع" الناتج على أي تعليمات import/export، ولا يتطلب السِمة type="module"‎، ويمكننا وضعه في سكربت عادي: في المثال أدناه لنفترض أننا جمعّنا الشيفرات في ملف bundle.js باستخدام مجمع حزم مثل: Webpack. <script src="bundle.js"></script> ومع ذلك يمكننا استخدام الوِحدات الأصلية (في شكلها الخام). لذلك لن نستخدم هنا أداة Webpack: يمكنك التعرف عليها وضبطها لاحقًا. خلاصة لنلخص المفاهيم الأساسية: الوِحدة هي مجرد ملف. لجعل تعليمتي import/export تعملان، ستحتاج المتصفحات إلى وضع السِمة التالية ‎<script type ="module">‎. تحتوي الوِحدات على عدة مُميزات: مؤجلة إفتراضيًا. تعمل الخاصية Async على السكربتات المضمّنة. لتحميل السكربتات الخارجية من مصدر مستقل، يجب استخدام طريقة (المَنفذ / البروتوكول / المجال)، وسنحتاج لترويسات CORS أيضًا. ستُتجاهل السكربتات الخارجية المكررة. لكل وِحدة من الوِحدات نطاق خاص بها، وتتبادلُ الوظائف فيما بينها من خلال استيراد وتصدير الوِحدات import/export. تستخدم الوِحدات الوضع الصارم دومًا use strict. تُنفذ شيفرة الوِحدة لمرة واحدة فقط. وتُصدر إلى من استوردها لمرة واحدة أيضًا، ومن ثمّ تُشارك بين المستوردين. عندما نستخدم الوحدات، تنفذ كل وِحدة وظيفة معينة وتُصدرها. ونستخدم تعليمة import لاستيرادها مباشرة عند الحاجة. إذ يُحمل المتصفح السكربت ويقيّمه تلقائيًا. وبالنسبة لوضع النشر، غالبًا ما يستخدم الناس مُحزّم الوِحدات مثل Webpack لتجميع الوِحدات معًا لرفع الأداء ولأسباب أخرى. سنرى في الفصل التالي مزيدًا من الأمثلة عن الوِحدات، وكيفية تصديرها واستيرادها. ترجمة -وبتصرف- للفصل Modules, introduction من كتاب The JavaScript language
  12. تُتيح لنا المُكرّرات غير المتزامنة المرور على البيانات التي تأتينا على نحوٍ غير متزامن متى لزم، مثل حين نُنزّل شيئًا كتلةً كتلةً عبر الشبكة. المولّدات غير المتزامنة تجعل من ذلك أسهل فأسهل. لنرى مثالًا أولًا لنفهم الصياغة، بعدها نرى مثالًا من الحياة العملية. المكررات غير المتزامنة تتشابه المُكرّرات غير المتزامنة مع تلك العادية، بفروق بسيطة في الصياغة. فكائن المُكرّر العادي (كما رأينا في الفصل الكائنات المكرَّرة (Iterables) في جافاسكربت) يكون هكذا: let range = { from: 1, to: 5, // ‫تستدعي حلقة for..of هذه الدالّة مرة واحدة في البداية [Symbol.iterator]() { // ‫...ستعيد كائن مُكرُر: // ‫لاحقًا ستعمل حلقة for..of فقط مع ذلك الكائن. // ‫وتطلب منه القيم التالية باستخدام دالة next()‎ return { current: this.from, last: this.to, // ‫تُستدعى next()‎ في كلّ تكرار من خلال الحلقة for..of next() { // (2) // ‫يجب أن تعيد القيم ككائن {done:.., value :...} if (this.current <= this.last) { return { done: false, value: this.current++ }; } else { return { done: true }; } } }; } }; for(let value of range) { alert(value); // 1 then 2, then 3, then 4, then 5 } راجِع … لتعرف تفاصيل المُكرّرات العادية، لو لزم. ولنجعل الكائن مُتكرّرًا غير متزامنًا: علينا استعمال Symbol.asyncIterator بدل Symbol.iterator. على next()‎ إعادة وعد. علينا استعمال حلقة for await (let item of iterable)‎ لتكرار كائن معين. فلنصنع كائن range مُتكرّرًا (كما أعلاه) ولكن يُعيد القيم بنحوٍ غير متزامن، قيمةً واحدةً كلّ ثانية: let range = { from: 1, to: 5, // ‫تستدعي حلقة for..of هذه الدالّة مرة واحدة في البداية [Symbol.asyncIterator]() { // (1) // ‫...ستعيد كائن مُكرُر: // ‫لاحقًا ستعمل حلقة for..of فقط مع ذلك الكائن. // ‫وتطلب منه القيم التالية باستخدام دالة next()‎ return { current: this.from, last: this.to, // ‫تُستدعى next()‎ في كلّ تكرار من خلال الحلقة for..of async next() { // (2) // ‫يجب أن تعيد القيم ككائن {done:.., value :...} // وستُغلّف تلقائيًا في وعد غير متزامن // ‫يمكننا استخدام await لتنفيذ أشياء غير متزامنة: await new Promise(resolve => setTimeout(resolve, 1000)); // (3) if (this.current <= this.last) { return { done: false, value: this.current++ }; } else { return { done: true }; } } }; } }; (async () => { for await (let value of range) { // (4) alert(value); // 1,2,3,4,5 } })() كما ترى فالبنية تشبه المُكرّرات العادية: ليكون الكائن مُتكرّرًا على نحوٍ غير متزامن، يجب أن يحتوي التابِع Symbol.asyncIterator (لاحِظ (1)). على التابِع إعادة الكائن وهو يحتوي التابِع next()‎ الذي يُعيد وعدًا (لاحِظ (2)). ليس على التابِع next()‎ أن يكون غير متزامن async. فيمكن أن يكون تابِعًا عاديًا يُعيد وعدًا، إلّا أنّ async تُتيح لنا استعمال await وهذا أفضل لنا. هنا نؤخّر التنفيذ ثانيةً واحدةً فقط (لاحِظ (3)). ليحدث التكرار نستعمل for await(let value of range)‎ (لاحِظ (4))، أي نُضيف await قبل الحلقة for. هكذا تستدعي الحلقة range[Symbol.asyncIterator]()‎ مرةً واحدة، وبعدها تابِع next()‎ للقيم التالية. إليك ورقة صغيرة تغشّ منها: المُكرّرات المُكرّرات غير المتزامنة تابِع الكائن الذي يُقدّم المُكرّر Symbol.iterator Symbol.asyncIterator قيمة next() المُعادة هي أيّ قيمة وعد Promise to loop, use for..of for await..of تحذير: "لا يعمل معامل البقية ... بطريقة غير متزامنة" الميزات التي تتطلب تكرارات منتظمة ومتزامنة، لا تعمل مع تلك المتزامنة. على سبيل المثال، لن يعمل معامل البقية هنا: alert( [...range] ); // Error, no Symbol.iterator هذا أمر طبيعي، لأنه يتوقع العثور على Symbol.iterator، مثل for..of بدون await. ولكن ليس Symbol.asyncIterator. المولدات غير المتزامنة كما نعلم فلغة جافاسكربت تدعم المولّدات أيضًا، وهذه المولّدات مُتكرّرة. فلنتذكّر مولّد السلاسل من الفصل المولِّداتكان يولّد سلسلة من القيم من متغير البداية start إلى متغير النهاية end: function* generateSequence(start, end) { for (let i = start; i <= end; i++) { yield i; } } for(let value of generateSequence(1, 5)) { alert(value); // 1, then 2, then 3, then 4, then 5 } لا يمكننا في الدوال العادية استعمال await، فعلى كلّ القيم أن تأتي بنحوٍ متزامن وليس ممكنًا وضع أيّ تأخير في حلقة for..of، ولكن، ماذا لو أردنا استعمال await في المولّد؟ مثلًا لنُرسل الطلبات عبر الشبكة؟ لا مشكلة، نضع قبله async هكذا: async function* generateSequence(start, end) { for (let i = start; i <= end; i++) { // yay, can use await! await new Promise(resolve => setTimeout(resolve, 1000)); yield i; } } (async () => { let generator = generateSequence(1, 5); for await (let value of generator) { alert(value); // 1, then 2, then 3, then 4, then 5 } })(); الآن لدينا مولّد غير متزامن، وقابل للتكرار مع for await...of. إنها حقًا بسيطة للغاية. نضيف الكلمة المفتاحية async، ويستطيع المولّد الآن استخدام await بداخله، ويعتمد على الوعود والدوالّ غير متزامنة الأخرى. وتقنيًا، الفرق الآخر للمولّد غير المتزامن هو أنّ تابِع generator.next()‎ صار غير متزامنًا أيضًا ويُعيد الوعود. في المولّدات العادية نستعمل result = generator.next()‎ لنأخذ القيم، بينما في المولّدات غير المتزامنة نُضيف await هكذا: result = await generator.next(); // result = {value: ..., done: true/false} المكررات غير المتزامنة كما نعلم ليكون الكائن مُتكرّرًا علينا إضافة رمز Symbol.iterator إليه. let range = { from: 1, to: 5, [Symbol.iterator]() { return <object with next to make range iterable> } } هناك أسلوب شائع هو إعادة Symbol.iterator لمولّد بدل كائن صِرف يحمل التابِع next كما المثال أعلاه. لنستذكر معًا المثال من الفصل المولِّدات: let range = { from: 1, to: 5, *[Symbol.iterator]() { // ‫اختصارًا لـِ ‎[Symbol.iterator]: function*()‎ for(let value = this.from; value <= this.to; value++) { yield value; } } }; for(let value of range) { alert(value); // 1, then 2, then 3, then 4, then 5 } نرى الكائن المخصّص range هنا مُتكرّرًا، والمولّد ‎*[Symbol.iterator]‎ يؤدّي المنطق اللازم لسرد القيم. لو أردنا إضافة أيّ إجراءات غير متزامنة للمولّدة، فعلينا استبدال الرمز Symbol.iterator بالرمز Symbol.asyncIterator غير المتزامن. let range = { from: 1, to: 5, async *[Symbol.asyncIterator]() { // ‫مشابه لـِ ‎[Symbol.asyncIterator]: async function*()‎ for(let value = this.from; value <= this.to; value++) { // توقف بين القيم لانتظار شيء ما await new Promise(resolve => setTimeout(resolve, 1000)); yield value; } } }; (async () => { for await (let value of range) { alert(value); // 1, then 2, then 3, then 4, then 5 } })(); الآن تأتينا القيم متأخرة عن بعضها البعض ثانيةً واحدة. مثال من الحياة العملية حتى اللحظة كانت الأمثلة كلّها بسيطة، لنفهم القصة فقط. الآن حان الوقت لنُطالع مثالًا وحالةً من الواقع. نرى على الإنترنت خدمات كثيرة تقدّم لنا البيانات على صفحات (paginated). فمثلًا حين نطلب قائمة من المستخدمين يُعيد الطلب عددًا تحدّد مسبقًا (مثلًا 100 مستخدم)، أي ”صفحة واحدة“، ويعطينا أيضًا عنوان الصفحة التالية. هذا النمط شائع جدًا بين المطوّرين، وليس مستخدمًا للمستخدمين فقط بل لكلّ شيء. فمثلًا غِت‌هَب تتيح لك جلب الإيداعات بنفس الطريقة باستعمال الصفحات: عليك إرسال طلب إلى المسار https://api.github.com/repos//commits. يستجيب الخادوم بكائن JSON فيه 30 إيداعًا، ويُعطيك رابطًا يوصلك إلى الصفحة التالية في ترويسة Link. بعدها نستعمل ذلك الرابط للطلبات التالية لنجلب إيداعات أكثر، وهكذا. ولكن ما نريد هو واجهة برمجية أبسط قليلًا: أي كائنًا مُتكرّرًا فيه الإيداعات كي نمرّ عليها بهذا النحو: let repo = 'javascript-tutorial/en.javascript.info'; // المستودع الذي فيه الإيداعات التي نريد على غِت‌هَب for await (let commit of fetchCommits(repo)) { // نُعالج كلّ إيداع commit } ونريد كتابة الدالة fetchCommits(repo)‎ لتجلب تلك الإيداعات لنا وتؤدّي الطلبات اللازمة متى… لزم. كما ونترك في عهدتها مهمة الصفحات كاملةً. ما سنفعله من جهتنا هو أمر بسيط، for await..of فقط. باستعمال المولّدات غير المتزامنة فهذه المهمّة تصير سهلة: async function* fetchCommits(repo) { let url = `https://api.github.com/repos/${repo}/commits`; while (url) { const response = await fetch(url, { // (1) headers: {'User-Agent': 'Our script'}, }); const body = await response.json(); // (2) // (3) let nextPage = response.headers.get('Link').match(/<(.*?)>; rel="next"/); nextPage = nextPage && nextPage[1]; url = nextPage; for(let commit of body) { // (4) yield commit; } } } نستعمل تابِع fetch من المتصفّح لتنزيل العنوان البعيد. يُتيح لنا التابِع تقديم تصاريح الاستيثاق وغيرها من ترويسات مطلوبة. غِت‌هَب يطلب User-Agent. نحلّل نتيجة الجلب على أنّها كائن JSON، وهذا تابِع آخر خاصّ بالتابِع fetch. نستلم هكذا عنوان الصفحة التالية من ترويسة Link داخل الردّ. لهذا العنوان تنسيق خاص فنستعمل تعبيرًا نمطيًا لتحليله. يظهر عنوان الصفحة التالية على هذا الشكل: https://api.github.com/repositories/93253246/commits?page=2، وموقع غِت‌هَب هو من يولّده بنفسه. بعدها نُنتِج كلّ الإيداعات التي استلمناها، ومتى انتهت مثال على طريقة الاستعمال (تعرض من كتبَ الإيداعات في الطرفيّة): (async () => { let count = 0; for await (const commit of fetchCommits('javascript-tutorial/en.javascript.info')) { console.log(commit.author.login); if (++count == 100) { // لنتوقف عند 100 إيداع break; } } })(); كما نريد تمامًا. الطريقة التي تعمل بها الطلبات بهذا النوع لا تظهر للشيفرات الخارجية وتبقى تفاصيل داخلية. المهم لنا هي استعمال مولّد غير متزامن يُعيد الإيداعات. خلاصة تعمل المُكرّرات والمولّدات العادية كما نريد لو لم تأخذ البيانات التي نحتاج وقتًا حتّى تتولّد. وحين نتوقّع بأنّ البيانات ستأتي بنحوٍ غير متزامن بينها انقطاعات، فيمكن استعمال البدائل غير المتزامنة مثل for await..of بدل for..of. الفرق في الصياغة بين المُكرّرات العادية وغير المتزامنة: المتكرر المتكرر غير المتزامن التابِع الذي يُقدّم المُكرّر Symbol.iterator Symbol.asyncIterator قيمة next()‎ المُعادة هي {value:…, done: true/false} Promise والّتي تعوض لتصبح {value:…, done: true/false} الفرق في الصياغة بين المولّدات العادية وغير المتزامنة: المولدات المولدات غير المتزامنة التعريف function* async function* next()‎ القيمة المُعادة هي {value:…, done: true/false} Promise والّتي تعوض لتصبح {value:…, done: true/false} غالبًا ونحن نعمل على تطوير الوِب نواجه سيولًا كبيرة من البيانات، سيولًا تأتينا قطعًا قطعًا (مثل تنزيل أو رفع الملفات الكبيرة). يمكننا هنا استعمال المولّدات غير المتزامنة لمُعالجة هذه البيانات. كما يجدر بنا الملاحظة كيف أنّ لبعض البيئات (مثل المتصفّحات) واجهات برمجة أخرى باسم السيول (Stream) وهي تقدّم لنا واجهات خاصّة للعمل مع السيول هذه، ذلك لتحويل شكل البيانات ونقلها من سيل إلى آخر (مثل التنزيل من مكان وإرسال ذلك التنزيل إلى مكان آخر مباشرةً). ترجمة -وبتصرف- للفصل Async iterators and generators من كتاب The JavaScript language table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; }
  13. تُعيد الدوالّ العادية قيمة واحدة فقط لا غير (أو لا تُعيد شيئًا). بينما يمكن للمولّدات إعادة (أو إنتاج yeild) أكثر من قيمة واحدةً بعد الأخرى حسب الطلب. تعمل المولّدات عملًا جميلًا جدًا مع الكائنات المكرَّرة (Iterables) في جافاسكربت وتتيح لنا إنشاء سيول البيانات بسهولة بالغة. الدوال المولدة لإنشاء مولّد علينا استعمال صياغة مميّزة: function*‎ أو ما يسمّونه ”الدالة المولِّدة“. هذا شكلها: function* generateSequence() { yield 1; yield 2; return 3; } يختلف سلوك الدوال المولِّدة عن تلك العادية، فحين تُستدعى الدالة لا تُشغّل الشيفرة فيها، بل تُعيد كائنًا مميزًا نسمّيه ”كائن المولّد“ ليُدير عملية التنفيذ. خُذ نظرة: function* generateSequence() { yield 1; yield 2; return 3; } // تنشئ الدالة المولِّدة كائن مولّد let generator = generateSequence(); alert(generator); // [object Generator] الشيفرة الموجودة بداخل الدالّة لم تُنفذ بعد: التابِع الأساسي للمولّد هو next()‎. متى استدعيناه بدأ عملية التنفيذ حتّى يصل أقرب جملة yield <value> (يمكن ألّا نكتب value وستصير القيمة undefined). بعدها يتوقّف تنفيذ الدالة مؤقّتًا وتُعاد القيمة value إلى الشيفرة الخارجية. ناتج التابِع next()‎ لا يكون إلّا كائنًا له خاصيتين: value: القيمة التي أنتجها المولّد. done: القيمة true لو اكتملت شيفرة الدالة، وإلّا false. فمثلًا هنا نُنشِئ مولّدًا ونأخذ أوّل قيمة أنتجها: function* generateSequence() { yield 1; yield 2; return 3; } let generator = generateSequence(); let one = generator.next(); // هنا alert(JSON.stringify(one)); // {value: 1, done: false} حاليًا أخذنا القيمة الأولى فقط، وسير تنفيذ الدالة موجود في السطر الثاني: فلنستدعِ generator.next()‎ ثانيةً الآن. سنراه واصل تنفيذ الشيفرة وأعاد القيمة المُنتَجة yield التالية: let two = generator.next(); alert(JSON.stringify(two)); // {value: 2, done: false} والآن إن شغّلناه مرّة ثالثة سيصل سير التنفيذ إلى عبارة return ويُنهي الدالة: let three = generator.next(); alert(JSON.stringify(three)); // {value: 3, done: true} // ‫لاحِظ قيمة done الآن اكتمل المولّد بقيمة value:3 ويمكننا الآن معالجتها. عرفنا ذلك من done:true. الاستدعاءات اللاحقة على generator.next()‎ لن تكون منطقية الآن. ولو حصلت فستُعيد الدالة الكائن نفسه: {done: true}. ملاحظة: الصياغة function* f(…)‎ أم function *f(…)‎؟ في الحقيقة كِلاهما صحيح. ولكن عادةً تكون الصياغة الأولى مفضلة أكثر من الثانية. وتشير النجمة * على أنها دالة مولّد، إذ تصف النوع وليس الاسم، لذلك يجب أن ندمج الكلمة المفتاحية function بالنجمة. المولدات قابلة للتكرار نفترض أنّك توقّعت ذلك حين رأيت التابِع next()‎، يمكننا المرور على عناصره عبر for..of: function* generateSequence() { yield 1; yield 2; return 3; } let generator = generateSequence(); for(let value of generator) { alert(value); // ‏1 ثمّ 2 } هذا أجمل من استدعاء ‎.next().value، أم لا؟ ولكن… لاحِظ: يعرض المثال أعلاه 1 ثمّ 2 وفقط. لن يعرض 3 مطلقًا! هذا لأنّ عملية التكرار لـِ for..of تتجاهل قيمة value الأخيرة حين تكون done: true. لذا لو أردنا أن تظهر النتائج كلّها لعملية تكرار for..of، فعلينا إعادتها باستعمال yield: function* generateSequence() { yield 1; yield 2; yield 3; // هكذا } let generator = generateSequence(); for(let value of generator) { alert(value); // ‏1 ثمّ 2 ثمّ 3 } نظرًا من كون المولّدات قابلة للتكرار، يمكننا استدعاء جميع الدوالّ المتعلّقة بذلك، مثل: معامل «البقية» ...: function* generateSequence() { yield 1; yield 2; yield 3; } let sequence = [0, ...generateSequence()]; alert(sequence); // 0, 1, 2, 3 يحول التابع ‎...generateSequence()‎ في الشيفرة أعلاه كائن المولد القابل للتكرار إلى مصفوفة من العناصر (لمزيد من المعلومات أحيلك إلى هذا المقال "المُعاملات «البقية» ومُعامل التوزيع"). استعمال المولدات على أنها مكرّرات سابقًا في فصل "المُعاملات «البقية» ومُعامل التوزيع" أنشأنا كائن range يمكن تكراره والذي يعيد القيم بين قيميتين from..to. لنتذكّر الشيفرة معًا: let range = { from: 1, to: 5, // ‫تستدعي حلقة for..of هذه الدالّة مرة واحدة في البداية [Symbol.iterator]() { // ‫...ستعيد كائن مُكرُر // ‫لاحقًا ستعمل حلقة for..of مع ذلك الكائن وتطلب منه القيم التالية return { current: this.from, last: this.to, // ‫تستدعى next()‎ في كلّ تكرار من خلال الحلقة for..of next() { // ‫يجب أن تعيد القيم ككائن {done:.., value :...} if (this.current <= this.last) { return { done: false, value: this.current++ }; } else { return { done: true }; } } }; } }; // ‫عملية التكرار تمرُّ عبر range من range.from إلى range.to alert([...range]); // 1,2,3,4,5 يمكننا استعمال دالة المولِّدة كمكرّرات من خلال Symbol.iterator. إليك نفس الكائن range, ولكن بطريقة أكثر إيجازًا: let range = { from: 1, to: 5, *[Symbol.iterator]() { // ‫اختصارًا لـِ ‎[Symbol.iterator]: function*‎()‎ for(let value = this.from; value <= this.to; value++) { yield value; } } }; alert( [...range] ); // 1,2,3,4,5 الشيفرة تعمل إذ يُعيد range[Symbol.iterator]()‎ الآن مولّدًا، وما تتوقّعه for..of هي توابِع تلك المولّدات بعينها: إذ لها التابِع ‎.next()‎ وتُعيد القيم على النحو الآتي {value: ..., done: true/false} بالطبع هذه ليست مصادفة أضيفت المولّدات إلى لغة جافاسكربت مع الأخذ بعين الاعتبار للمكرّرات لتنفيذهم بسهولة. إن التنوع في المولدات أعطى شيفرة موجزة أكثر الشيفرة الأصلية لكائن range، وجميعهم لديهم نفس الخصائص الوظيفية. ملاحظة: المولّدات يمكن أن تولد قيمًا للأبد في الأمثلة أعلاه أنشأنا متتالية منتهية من القيم، ولكن باستخدام المولد نستطيع أن ننتج قيمًا إلى الأبد. على سبيل المثال لننشئ متتالية غير منتهية من الأرقام العشوائية الزائفة. ومن المؤكد أن هذا المولّد سيحتاجُ إلى طريقة لإيقافه مثل: break (أو return) في حلقة for..of. وإلا فإن الحلقة ستستمر إلى الأبد. تراكب المولّدات تراكب المولّدات (Generator composition) هي ميزة خاصّة للمولّدات تتيح لها ”تضمين“ المولّدات الأخرى فيها دون عناء. فمثلًا لدينا هذه الدالة التي تُولّد سلسلة من الأعداد: function* generateSequence(start, end) { for (let i = start; i <= end; i++) yield i; } نريد الآن إعادة استعمالها لتوليد سلسلة أعداد معقّدة أكثر من هذه أعلاه: أولًا الأرقام 0..9 (ورموز المحارف اليونيكوديّة هو من 48 إلى 57) ثمّ الأحرف الأبجدية الإنجليزية بالحالة الكبيرة A..Z (ورموزها من 65 إلى 90) ثمّ الأحرف الأبجدية الإنجليزية بالحالة الصغيرة a..z (ورموزها من 97 إلى 122) يمكننا استعمال هذه السلسلة لإنشاء كلمات السرّ باختيار المحارف منها مثلًا (ويمكننا إضافة المحارف الخاصّة أيضًا). ولكن لذلك علينا توليدها أولًا. علينا في الدوالّ العادية (لتضمين النواتج من دوالّ أخرى) استدعاء تلك الدوالّ وتخزين نواتجها ومن ثمّ ربطها في نهاية الدالة الأمّ. وفي المولّدات نستعمل الصياغة المميّزة yield* لتضمين (أو تركيب) مولّدين داخل بعض. المولّد المركّب: function* generateSequence(start, end) { for (let i = start; i <= end; i++) yield i; } function* generatePasswordCodes() { // 0..9 yield* generateSequence(48, 57); // A..Z yield* generateSequence(65, 90); // a..z yield* generateSequence(97, 122); } let str = ''; for(let code of generatePasswordCodes()) { str += String.fromCharCode(code); } alert(str); // 0..9A..Za..z توجه yield*‎ التنفيذ إلى مولّد آخر. مما يعني أن yield* gen ستكرر عبر المولّد gen وستُعيد نتائجه للخارج. أي كما او أنتجت هذه القيم بمولّد خارجي. النتيجة نفسها كما لو أننا ضمنَا الشيفرة من المولّدات المتداخلة: function* generateSequence(start, end) { for (let i = start; i <= end; i++) yield i; } function* generateAlphaNum() { // yield* generateSequence(48, 57); for (let i = 48; i <= 57; i++) yield i; // yield* generateSequence(65, 90); for (let i = 65; i <= 90; i++) yield i; // yield* generateSequence(97, 122); for (let i = 97; i <= 122; i++) yield i; } let str = ''; for(let code of generateAlphaNum()) { str += String.fromCharCode(code); } alert(str); // 0..9A..Za..z تراكب المولّدات هو فعلًا طريقة بديهيّة لإدخال أكثر من مولّد في مولّد واحد، ولا تستعمل ذاكرة إضافية لتخزين النواتج البينيّة. عبارة ”yield“ باتجاهين اثنين إلى هنا نرى المولّدات تشبه كائنات المكرّرات كثيرًا، فقط أنّ لها صياغة مميّزة لتوليد القيم. ولكن وعلى أرض الواقع، المولّدات أكثر مرونة وفاعليّة. إذ أنّ yield تعمل بالمجيء وبالإياب: فلا تُعيد الناتج إلى الخارج فحسب بل أيضًا يمكنها تمرير القيمة إلى داخل المولّد. لذلك علينا استدعاء generator.next(arg)‎ بتمرير وسيط واحد. هذا الوسيط سيكون ناتج yield. الأفضل لو نرى مثالًا: function* gen() { // نمرّر سؤالًا إلى الشيفرة الخارجية وننتظر إجابةً عليه let result = yield "2 + 2 = ?"; // (*) alert(result); } let generator = gen(); let question = generator.next().value; // <-- تُعيد yield القيمة generator.next(4); // --> نمرّر القيمة إلى المولّد يكون الاستدعاء الأوّل للتابِع generator.next()‎ دومًا دون تمرير أيّ وسيط. يبدأ الاستدعاء التنفيذَ ويُعيد ناتج أوّل yield "2+2=?‎". هنا يُوقف المولّد التنفيذ مؤقّتًا (على ذلك السطر). ثمّ (كما نرى في الصورة) يُوضع ناتج yield في متغير السؤال question في الشيفرة التي استدعت المولّد. وعند generator.next(4)‎ يُواصل المولّد عمله ويستلم 4 ناتجًا: let result = 4. لاحِظ أنّه ليس على الشيفرة الخارجية استدعاء next(4)‎ مباشرةً وفي الحال، بل يمكن أن تأخذ الوقت الذي تريد. سيبقى المولّد منتظرًا ولن تكون مشكلة. مثال: // نُواصل عمل المولّد بعد زمن معيّن setTimeout(() => generator.next(4), 1000); كما نرى فعلى العكس تمامًا من الدوال العادية، يمكن للمولّد ولشيفرة الاستدعاء تبادل النتائج بتمرير القيم إلى next/yield. ليتوضّح هذا أكثر سنرى مثالًا آخر فيه استدعاءات أكثر: function* gen() { let ask1 = yield "2 + 2 = ?"; alert(ask1); // 4 let ask2 = yield "3 * 3 = ?" alert(ask2); // 9 } let generator = gen(); alert( generator.next().value ); // "2 + 2 = ?" alert( generator.next(4).value ); // "3 * 3 = ?" alert( generator.next(9).done ); // true إليك صورة سير التنفيذ: استدعاء .next()‎ الأوّل يبدأ التنفيذ، ويصل إلى أوّل عبارة yield. يُعاد الناتج إلى الشيفرة الخارجية. يمرّر استدعاء .next(4)‎ الثاني القيمة 4 إلى المولّد ثانيةً على أنّها ناتج أوّل مُنتَج yield، ويُواصل التنفيذ. يصل التنفيذ إلى عبارة yield الثانية، وتصير هي ناتج الاستدعاء. يُمرّر next(9)‎ الثالث القيمة 9 إلى المولّد على أنّها ناتج ثاني مُنتَج yield ويُواصل التنفيذ حتّى يصل نهاية الدالة، بذلك تكون done: true. تشبه هذه لعبة تنس الطاولة، حيث يمرّر كلّ تابِع next(value)‎ (باستثناء الأوّل طبعًا) القيمة إلى المولّد فتصير ناتج المُنتَج yield الحالي، ومن ثمّ generator.throw يمكن للشيفرات الخارجية تمرير القيم إلى المولّدات على أنّها نواتج yield (كما لاحظنا من الأمثلة أعلاه). ويمكنها أيضًا بدء (أو رمي) خطأ أيضًا. هذا طبيعي إذ الأخطاء هي نواتج، نوعًا ما. علينا استدعاء التابِع yield لتمرير الأخطاء إلى عبارة generator.throw(err)‎. في هذه الحال يُرمى الخطأ err عند السطر فمثلًا تؤدّي عبارة "‎2 + 2 = ?‎" هنا إلى خطأ: function* gen() { try { let result = yield "2 + 2 = ?"; // (1) alert("The execution does not reach here, because the exception is thrown above"); } catch(e) { alert(e); // أظهر الخطأ } } let generator = gen(); let question = generator.next().value; generator.throw(new Error("The answer is not found in my database")); // (2) الخطأ الذي رميَ في المولد عند السطر (2) يقودنا إلى الخطأ في السطر (1) مع yield. في المثال أعلاه التقطت try..catch الخطأ وثمّ عرضته. لو لم نلتقطه سيكون مآله (مآل أيّ استثناء آخر غيره) أن ”يسقط“ من المولّد إلى الشيفرة التي استدعت المولّد. هل يمكننا التقاط الخطأ في سطر شيفرة الاستدعاء الّتي تحتوي على generator.throw، (المشار إليه (2))، هكذا؟ function* generate() { let result = yield "2 + 2 = ?"; // خطأ في هذا السطر } let generator = generate(); let question = generator.next().value; try { generator.throw(new Error("The answer is not found in my database")); } catch(e) { alert(e); // عرض الخطأ } إن لم نلتقط الخطأ هنا فعندئذ وكما هي العادة ستنهار الشيفرة الخارجية (إن كانت موجودة) وإذا لم يكتشف الخطأ أيضًا عندها سينهار السكربت بالكامل. خلاصة تُنشِئ الدوال المولِّدة function* f(…) {…}‎ المولّدات. يوجد المُعامل yield داخل المولدات (فقط). تتبادل الشيفرة الخارجية مع المولدات النتائج من خلال استدعاءات next/yield. نادرًا ما تستخدم المولّدات في الإصدار الحديث من جافاسكربت. لكن في بعض الأحيان نستخدمها لتبادل البيانات بين الشيفرة المستدعاة أثناء تنفيذ شيفرة وحيدة. وبالتأكيد إنها رائعة لتوليد أشياء قابلة للتكرار. في الفصل التالي، سنتعرف على مولدات غير متزامنة، والّتي تُستخدم لقراءة تدفقات البيانات غير المتزامنة (على سبيل المثال ، نرى على الإنترنت خدمات كثيرة تقدّم لنا البيانات على صفحات [paginated]) في حلقات for await ... of. في برمجة الوِب، غالبًا ما نعمل مع البيانات المتدفقة، لذا فهذه حالة استخدام أخرى مهمة جدًا. تمارين مولد أرقام شبه عشوائية نواجه كثيرًا من الأحيان حاجة ماسّة إلى بيانات عشوائية. إحدى هذه الأحيان هي إجراء الاختبارات، فنحتاج إلى بيانات عشوائية كانت نصوص أو أعداد أو أيّ شيء آخر لاختبار الشيفرات والبنى البرمجية. يمكننا في جافاسكربت استعمال Math.random()‎، ولكن لو حدث خطب ما فنودّ إعادة إجراء الاختبار باستعمال نفس البيانات بالضبط (كي نختبر هذه البيانات). لهذا الغرض نستعمل ما يسمّى ”بمولّدات الأعداد شبه العشوائية المزروعة“. تأخذ هذه المولّدات "البذرة“ والقيمة الأولى ومن ثمّ تولّد القيم اللاحقة باستعمال معادلة رياضية بحيث أنّ كلّ بذرة تُنتج نفس السلسلة دائمًا، وهكذا يمكننا ببساطة استنساخ التدفق بالكامل من خلال بذورها فقط. إليك مثال عن هذه المعادلة التي تولّد قيمًا موزّعة توزيعًا next = previous * 16807 % 2147483647 لو استعملنا 1 …، فستكون القيم كالآتي: 16807 282475249 1622650073 …وهكذا… مهمّة هذا التمرين هو إنشاء الدالة المولِّدة pseudoRandom(seed)‎ فتأخذ البذرة seed وتُنشِئ مولّدًا بالمعادلة أعلاه. طريقة الاستعمال: let generator = pseudoRandom(1); alert(generator.next().value); // 16807 alert(generator.next().value); // 282475249 alert(generator.next().value); // 1622650073 الحل function* pseudoRandom(seed) { let value = seed; while(true) { value = value * 16807 % 2147483647 yield value; } }; let generator = pseudoRandom(1); alert(generator.next().value); // 16807 alert(generator.next().value); // 282475249 alert(generator.next().value); // 1622650073 لاحِظ أننا نستطيع تأدية ذات الأمر بالدوال العادية هكذا: function pseudoRandom(seed) { let value = seed; return function() { value = value * 16807 % 2147483647; return value; } } let generator = pseudoRandom(1); alert(generator()); // 16807 alert(generator()); // 282475249 alert(generator()); // 1622650073 ستعمل هذه أيضًا. ولكن بعد ذلك سنفقدُ قابلية التكرار باستخدام for..of واستخدام المولّد المركب، وتلك ممكن أن تكون مفيدة في مكان آخر. ترجمة -وبتصرف- للفصل Generators من كتاب The JavaScript language
  14. توجد صياغة مميّزة للعمل مع الوعود بنحوٍ أكثر سهولة تُدعى async/await. فهمها أسهل من شرب الماء واستعمالها الدوال غير المتزامنة فلنبدأ أولًا بكلمة async المفتاحية. يمكننا وضعها قبل الدوال هكذا: async function f() { return 1; } وجود الكلمة ”async“ قبل (اختصار ”غير متزامنة“ بالإنجليزية) يعني أمرًا واحدًا: تُعيد الدالة وعدًا دومًا. فمثلًا تُعيد هذه الدالة وعدًا مُنجز فيه ناتج 1. فلنرى: async function f() { return 1; } f().then(alert); // 1 كما يمكننا أيضًا إعادة وعد صراحةً: async function f() { return Promise.resolve(1); } f().then(alert); // 1 هكذا تضمن لنا async بأنّ الدالة ستُعيد وعدًا وستُغلّف الأشياء التي ليست بوعود وعودًا. بسيطة صح؟ ليس هذا فحسب، بل هناك أيضًا الكلمة المفتاحية await التي تعمل فقط في الدوال غير المتزامنة async، وهي الأخرى جميلة. Await الصياغة: // لا تعمل إلّا في الدوال غير المتزامنة let value = await promise; الكلمة المفتاحية تجعل لغة جافاسكربت تنتظر حتى يُنجز الوعد ويعيد نتيجة. إليك مثالًا لوعد نُفذّ خلال ثانية واحدة: async function f() { let promise = new Promise((resolve, reject) => { setTimeout(() => resolve("done!"), 1000) // تم!‏ }); let result = await promise; // ننتظر ... الوعد (*) alert(result); // "done!" تم!‏ } f(); يتوقّف تنفيذ الدالة ”قليلًا“ عند السطر (*) ويتواصل متى ما أُنجز الوعد وصار result ناتجه. الشيفرة أعلاه تعرض ”تم!“ بعد ثانية واحدة. لنوضح أمرًا مهمًا: تجعل await لغة جافاسكربت تنتظر حتى يُنجز الوعد، وبعدها تذهب مع النتيجة. هذا لن يكلفنا أي موارد من المعالج لأن المحرك مُنشغل بمهام أخرى في الوقت نفسه، مثل: تنفيذ سكربتات أُخرى، التعامل مع الأحداث ..إلخ. هي مجرد صياغة أنيقة أكثر من صياغة promise.then للحصول على ناتج الوعد. كما أنها أسهل للقراءة والكتابة.. تحذير: لا يمكننا استخدام await في الدوال العادية إذا حاولنا استخدام الكلمة المفتاحية await في الدوال العادية فسيظهر خطأ في في الصياغة: function f() { let promise = Promise.resolve(1); let result = await promise; // Syntax error } سنحصل على هذا الخطأ إذا لم نضع async قبل الدالّة، كما قلنا await تعمل فقط في الدوالّ غير المتزامنة. لنأخذ مثال showAvatar()‎ من الفصل سَلسلة الوعود ونُعيد كتابته باستعمال async/await: علينا استبدال استدعاءات ‎.then ووضع await. علينا أيضًا تحويل الدالة لتكون غير متزامنة async كي تعمل await. async function showAvatar() { // ‫إقرأ ملفات JSON let response = await fetch('/article/promise-chaining/user.json'); let user = await response.json(); // ‫إقرأ مستخدم github let githubResponse = await fetch(`https://api.github.com/users/${user.name}`); let githubUser = await githubResponse.json(); // ‫أظهرالصورة الرمزية avatar let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.append(img); // انتظر 3 ثواني await new Promise((resolve, reject) => setTimeout(resolve, 3000)); img.remove(); return githubUser; } showAvatar(); شيفرة نظيفة وسهلة القراءة! أفضل من السابقة بأميال. ملاحظة: لن تعمل await في الشيفرة ذات المستوى الأعلى يميل المبتدئين إلى نسيان أن await لن تعمل في الشيفرة البرمجية ذات هرمية أعلى(ذات المستوى الأعلى). فمثلًا لن يعمل هذا: // خطأ صياغيّ في الشيفرة عالية المستوى let response = await fetch('/article/promise-chaining/user.json'); let user = await response.json(); يمكننا تغليفها بداخل دالّة متزامنة مجهولة، هكذا: (async () => { let response = await fetch('/article/promise-chaining/user.json'); let user = await response.json(); ... })(); ملاحظة: إن await تقبل "thenables" تسمح await باستخدام كائنات thenable (تلك التي تستخدم دالة then القابلة للاستدعاء) تمامًا مثل promise.then. الفكرة أنه يمكن ألا يكون الكائن الخارجي وعدًا ولكنه متوافق مع الوعد: إن كان يدعم ‎.then، وهذا يكفي لاستخدامه مع await. هنا مثال لاستخدام صنف Thenable والكلمة المفتاحية await أدناه ستقبلُ حالاتها: class Thenable { constructor(num) { this.num = num; } then(resolve, reject) { alert(resolve); // ‫نفذ عملية this.num*2 بعد ثانية واحدة setTimeout(() => resolve(this.num * 2), 1000); // (*) } }; async function f() { // انتظر لثانية واحدة. ثم النتيجة ستصبح 2 let result = await new Thenable(1); alert(result); } f(); إذا حصَلت await على كائن ليس وعدًا مع ‎.then فإنه يستدعي الدوالّ المضمنة في اللغة مثل: resolve و reject كوسطاء (كما يفعل المنفذّ للوعد العادي تمامًا). وثم تنتظر await حتى يستدعى أحدهم (في المثال أعلاه في السطر (*)) ثم يواصل مع النتيجة. ملاحظة: لتعريف دالّة صنف غير متزامنة إضفها مع الكلمة async: class Waiter { async wait() { return await Promise.resolve(1); } } new Waiter() .wait() .then(alert); // 1 لاحظ أن المعنى هو نفسه: فهو يضمن أن القيمة المرتجعة هي وعد وawait مفعّلة أيضًا. التعامل مع الأخطاء لو نُفذّ الوعد بنجاح فسيُعيد await promise الناتج، ولكن لو حصلت حالة رفض فسترمي الخطأ كما لو كانت إفادة throw مكتوبة. إليك الشيفرة: async function f() { await Promise.reject(new Error("Whoops!")); // لاحظ } و… هي ذاتها هذه: async function f() { throw new Error("Whoops!"); // هنا } في الواقع تأخذ الوعود وقتًا قبل أن تُرفض. في تلك لحالة فستكون هناك مهلة قبل أن يرمي await الخطأ. يمكننا التقاط ذاك الخطأ باستعمال try..catch كما استعملنا throw: async function f() { try { let response = await fetch('http://no-such-url'); } catch(err) { alert(err); // TypeError: failed to fetch خطأ في النوع: فشل الجلب } } f(); لو حدث خطأ فينتقل سير التنفيذ إلى كتلة catch. يمكننا أيضًا وضع أكثر من سطر واحد: async function f() { try { let response = await fetch('/no-user-here'); let user = await response.json(); } catch(err) { // ‫تلتقط أخطاء fetch وresponse.json معًا alert(err); } } f(); لو لم نضع try..catch فستكون حالة الوعد الذي نفّذه استدعاء الدالة غير المتزامنة f()‎ - يكون بحالة رفض. يمكننا هنا استعمال ‎.catch للتعامل معه: async function f() { let response = await fetch('http://no-such-url'); } // ‫يصير استدعاء f()‎ وعدًا مرفوضًا f().catch(alert); // TypeError: failed to fetch خطأ في النوع: فشل الجلب (*) لو نسينا هنا إضافة ‎.catch فسنتلقّى خطأ وعود لم نتعامل معه (يمكن أن نراه من الطرفية). يمكننا أيضًا استلام هذه الأخطاء باستعمال دالة مُعاملة الأحداث العمومية كما وضّحنا في الفصل …. ملاحظة: "async/await وpromise.then/catch" عندما نستخدم async/await نادرًا ما نحتاج إلى ‎.then وذلك لأن await تعالج عملية الانتظار. ويمكننا استخدام try..catch بدلًا من ‎.catch. وهذه عادةً (ليس دائمًا) ما تكون أكثر ملاءمة. ولكن في الشيفرات ذات هرمية أعلى (مستوى أعلى)، عندما نكون خارج أي دالّة غير متزامنة، يتعذر علينا استخدام await لذا من المعتاد إضافة .then/catch لمعالجة النتيجة النهائية أو الأخطاء المتساقطة. كما هو الحال في السطر (*) من المثال أعلاه. ملاحظة: "تعمل async/await مع Promise.all" عندما نحتاج لانتظار عدة وعود يمكننا أن نغلفها في تابع Promise.all وبعده await: // انتظر مصفوفة النتائج let results = await Promise.all([ fetch(url1), fetch(url2), ... ]); وإن حدث خطأ ما، فإنه سيُنقل من الوعد نفسه إلى التابع Promise.all، وبعدها يصبح استثناءً يمكننا التقاطه باستخدام try..catch حول الاستدعاء. خلاصة لكلمة async المفتاحية قبل الدوال تأثيرين اثنين: تحوّلها لتُعيد وعدًا دومًا. تتيح استعمال await فيها. حين يرى محرّك جافاسكربت الكلمة المفتاحية await قبل الوعود، ينتظر حتّى يُنجز الوعد ومن ثمّ: لو كان خطأ فسيُولِّد الاستثناء كما لو استعملنا throw error. وإلّا أعاد الناتج. تقدّم لنا هتين الكلمتين معًا إطار عمل رائع نكتب به شيفرات غير متزامنة تسهل علينا قراءتها كما وكتابتها. نادرًا ما نستعمل promise.then/catch بوجود async/await، ولكن علينا ألّا ننسى بأنّ الأخيرتين مبنيّتين على الوعود إذ نضطر أحيانًا (خارج الدوال مثلًا) استعمال promise.then/catch. كما وأنّ Promise.all جميل جدًا لننتظر أكثر من مهمّة في وقت واحد. تمارين إعادة الكتابة باستعمال async/await أعِد كتابة الشيفرة في المثال من الفصل سلسلة الوعود باستعمال async/await بدل ‎.then/catch: function loadJson(url) { return fetch(url) .then(response => { if (response.status == 200) { return response.json(); } else { throw new Error(response.status); } }) } loadJson('no-such-user.json') // (3) .catch(alert); // Error: 404 الحل ترى الملاحظات أسفل الشيفرة: async function loadJson(url) { // (1) let response = await fetch(url); // (2) if (response.status == 200) { let json = await response.json(); // (3) return json; } throw new Error(response.status); } loadJson('no-such-user.json') .catch(alert); // Error: 404 (4) ملاحظات: الدالّة loadJson تصبح async. جميع المحتوى في .then يستبدل بـِ await. يمكننا إعادة return response.json()‎ بدلًا من انتظارها. هكذا: if (response.status == 200) { return response.json(); // (3) } ثم الشيفرة الخارجية ستنتظر await لينفذ الوعد في حالتنا الأمر غير مهم. سيرمى الخطأ من التابع loadJson المعالج من قبل ‎.catch. لن نستطيع استخدام await loadJson(…)‎ هنا، وذلك لأننا لسنا في دالّة غير متزامنة. أعِد كتابة rethrow باستعمال async/await في الشيفرة أدناه مثلًا عن إعادة الرمي من فصل سلسلة الوعود. أعد كتابته باستخدام async/await بدلًا من ‎.then/catch. وتخلص من العودية لصالح الحلقة في demoGithubUser: مع استخدام async/await سيسهلُ الأمر. class HttpError extends Error { constructor(response) { super(`${response.status} for ${response.url}`); this.name = 'HttpError'; this.response = response; } } function loadJson(url) { return fetch(url) .then(response => { if (response.status == 200) { return response.json(); } else { throw new HttpError(response); } }) } // ‫اطلب اسم المستخدم إلى أن يعيد لك github مستخدم صحيح function demoGithubUser() { let name = prompt("Enter a name?", "iliakan"); return loadJson(`https://api.github.com/users/${name}`) .then(user => { alert(`Full name: ${user.name}.`); return user; }) .catch(err => { if (err instanceof HttpError && err.response.status == 404) { alert("No such user, please reenter."); return demoGithubUser(); } else { throw err; } }); } demoGithubUser(); الحل لا يوجد أي صعوبة هنا، إذ كل ما عليك فعله هو استبدال try...catch بدلًا من ‎.catch بداخل تابع demoGithubUser وأضف async/await عند الحاجة: class HttpError extends Error { constructor(response) { super(`${response.status} for ${response.url}`); this.name = 'HttpError'; this.response = response; } } async function loadJson(url) { let response = await fetch(url); if (response.status == 200) { return response.json(); } else { throw new HttpError(response); } } // ‫اطلب اسم المستخدم إلى أن يعيد لك github مستخدم صحيح async function demoGithubUser() { let user; while(true) { let name = prompt("Enter a name?", "iliakan"); try { user = await loadJson(`https://api.github.com/users/${name}`); break; // لا يوجد خطأ اخرج من الحلقة } catch(err) { if (err instanceof HttpError && err.response.status == 404) { // ‫تستمر الحلقة بعد alert alert("No such user, please reenter."); } else { // ‫خطأ غير معروف , أعد رميه rethrow throw err; } } } alert(`Full name: ${user.name}.`); return user; } demoGithubUser(); استدعاء async من دالة غير متزامنة لدينا دالة ”عادية“، ونريد استدعاء async منها واستعمال ناتجها، كيف؟ async function wait() { await new Promise(resolve => setTimeout(resolve, 1000)); return 10; } function f() { // ...ماذا نكتب هنا؟ // ‫علينا استدعاء async wait()‎ والانتظار حتّى تأتي 10 // ‫لا تنسَ، لا يمكن استعمال ”await“ هنا } ملاحظة: هذه المهمّة (تقنيًا) بسيطة جدًا، ولكنّ السؤال شائع بين عموم المطوّرين الجدد على async/await. الحل هنا تأتي فائدة معرفة طريقة عمل هذه الأمور. ليس عليك إلّا معاملة استدعاء async وكأنّه وعد ووضع تابِع ‎.then: async function wait() { await new Promise(resolve => setTimeout(resolve, 1000)); return 10; } function f() { // يعرض 10 بعد ثانية واحدة wait().then(result => alert(result)); } f(); ترجمة -وبتصرف- للفصل Async/await من كتاب The JavaScript language
  15. دوال المُعاملة للوعود ‎.then و ‎.catch و‎.finally هي دوال غير متزامنة، دومًا. فحتّى لو سويَ الوعد مباشرةً (أي سواءً أنُجز أو رُفض) فالشيفرة أسفل ‎.then و‎.catch و‎.finally ستُنفّذ حتّى قبل دوال المعاملة. لاحِظ: let promise = Promise.resolve(); promise.then(() => alert("promise done!")) // اكتمل الوعد alert("code finished"); // ‫نرى هذا النص أولًا (انتهت الشيفرة) لو شغّلته فسترى أولًا code finished وبعدها ترى promise done!. هذا… غريب إذ أن الوعد أنجز قطعًا في البداية. لماذا شُغّلت ‎.then بعدئذ؟ ما الّذي يحدث؟ طابور المهام السريعة تطلب الدوال غير المتزامنة عملية إدارة مضبوطة. ولهذا تحدّد مواصفة اللغة طابورًا داخليًا باسم PromiseJobs (غالبًا ما نسمّيه ”بطابور المهام السريعة“ Microtask Queue حسب مصطلح محرّك V8). تقول المواصفة: الطابور بمبدأ ”أوّل من يدخل هو أوّل من يخرج“: المهام التي تُضاف أولًا تُنفّذ أولًا. لا يبدأ تنفيذ المهمة إلّا لو لم يكن هناك شيء آخر يعمل. وبعبارة أبسط، فمتى جهز الوعد تُضاف دوال المعاملة ‎.then/catch/finally إلى الطابور، وتبقى هناك بلا تنفيذ. متى وجد محرّك جافاسكربت نفسه قد فرغ من الشيفرة الحالية، يأخذ مهمة من الطابور وينفّذها. لهذا السبب نرى ”اكتملت الشيفرة“ في المثال أعلاه أولًا. دوال معاملة الوعود تمرّ من الطابور الداخلي هذا دومًا. لو كانت في الشيفرة سلسلة من ‎.then/catch/finally فستُنفّذ كلّ واحدة منها تنفيذًا غير متزامن. أي أنّ الأولى تُضاف إلى الطابور وتُنفّذ متى اكتمل تنفيذ الشيفرة الحالية وانتهت دوال المُعاملة الّتي أُضيفت إلى الطابور مسبقًا. ولكن ماذا لو كان الترتيب يهمّنا؟ كيف نشغّل code finished بعد promise done؟ بسيطة، نضعها في الطابور باستعمال ‎.then: Promise.resolve() .then(() => alert("promise done!")) .then(() => alert("code finished")); هكذا صار الترتيب كما نريد. الرفض غير المعالج تذكر حدث unhandledrejection من فصل التعامل مع الأخطاء في الوعود. سنرى الآن كيف تعرف محرّكات جافاسكربت ما إن وُجدت حالة رفض لم يُتعامل معها، أم لا. تحدث ”حالة الرفض لم يُتعامل معها“ حين لا يتعامل شيء مع خطأ أنتجه وعد في آخر طابور المهام السريعة. عادةً لو كنّا نتوقّع حدوث خطأ نُضيف التابِع ‎.catch إلى سلسلة الوعود للتعامل معه: let promise = Promise.reject(new Error("Promise Failed!")); promise.catch(err => alert('caught')); // هكذا // لا يعمل هذا السطر إذ تعاملنا مع الخطأ window.addEventListener('unhandledrejection', event => alert(event.reason)); ولكن… لو نسينا وضع ‎.catch سيُشغّل المحرّك هذا الحدث متى فرغ طابور المهام السريعة: let promise = Promise.reject(new Error("Promise Failed!")); // Promise Failed! window.addEventListener('unhandledrejection', event => alert(event.reason)); وماذا لو تعاملنا مع الخطأ لاحقًا؟ هكذا مثلًا: let promise = Promise.reject(new Error("Promise Failed!")); setTimeout(() => promise.catch(err => alert('caught')), 1000); // لاحِظ // Error: Promise Failed! window.addEventListener('unhandledrejection', event => alert(event.reason)); إذا شغلناه الآن سنرى Promise Failed! أولًا ثم caught. لو بقينا نجهل طريقة عمل طابور المهام السريعة فسنتساءل: ”لماذا عملت دالة المُعاملة unhandledrejection؟ الخطأ والتقطناه!“. أمّا الآن فنعرف أنّه لا يُولّد unhandledrejection إلّا حين انتهاء طابور المهام السريعة: فيفحص المحرّك الوعود وإن وجد حالة ”رفض“ في واحدة، يشغّل الحدث. في المثال أعلاه، أضيفت .catch وشغّلت من قِبل setTimeout متأخرةً بعد حدوث unhandledrejection لذا فإن ذلك لم يغيّر أي شيء. خلاصة التعامل مع الوعود دومًا يكون غير متزامن، إذ تمرّ إجراءات الوعود في طابور داخلي ”لمهام الوعود“ أو ما نسمّيه ”بطابور المهام السريعة“ (مصطلح المحرّك V8). بهذا لا تُستدعى دوال المُعاملة ‎.then/catch/finally إلّا بعد اكتمال الشيفرة الحالية. ولو أردنا أن نضمن تشغيل هذه الأسطر بعينها بعد ‎.then/catch/finally فيمكننا إضافتها إلى استدعاء ‎.then في السلسلة. في معظم محركات جافاسكربت، بما في ذلك المتصفحات و Node.js، يرتبط مفهوم المهام السريعة ارتباطًا وثيقًا بـ "حلقة الأحداث" والمهام الكبيرة "macrotasks". نظرًا لأنها لا تملك علاقة مباشرة بالوعود، لذا فإننا شرحناها في جزء آخر من السلسلة التعليمية، في الفصل Event loop: microtasks and macrotasks. ترجمة -وبتصرف- للفصل Microtasks من كتاب The JavaScript language
  16. تحويل الدوال إلى وعود (Promisification) هي عملية تغليف الدالة التي تستلم ردّ نداء لتصبح دالة تُعيد وعدًا. وفي الحياة العملية فهذا النوع من التحويل مطلوب جدًا إذ تعتمد العديد من الدوال والمكتبات على ردود النداء. ولكن… الوعود أسهل وأفضل لذا من المنطقي تحويل تلك الدوال. لنأخذ مثلًا دالة loadScript(src, callback)‎ من الفصل مقدمة إلى ردود النداء callback: function loadScript(src, callback) { let script = document.createElement('script'); script.src = src; script.onload = () => callback(null, script); script.onerror = () => callback(new Error(`Script load error for ${src}`)); document.head.append(script); } // الاستعمال // loadScript('path/script.js', (err, script) => {...}) هيا نحوّلها. على الدالة الجديدة loadScriptPromise(src)‎ القيام بنفس ما تقوم به تلك، ولكن لا تقبل إلّا src وسيطًا (بدون callback) وتُعيد وعدًا. let loadScriptPromise = function(src) { return new Promise((resolve, reject) => { loadScript(src, (err, script) => { if (err) reject(err) else resolve(script); }); }) } // الاستعمال // loadScriptPromise('path/script.js').then(...) الآن يمكننا استعمال loadScriptPromise بسهولة بالغة في الشيفرات التي تعتمد الوعود. وكما نرى فالدالة تكلّف الدالة الأصلية loadScript بكلّ العمل اللازم، وما تفعله هو تقديم ردّ نداء من عندها تحوّله إلى وعد resolve/reject. نحتاج عمليًا إلى تحويل دوال عديدة وكثيرة لتعتمد الوعود، لذا من المنطقي استعمال دالة مساعِدة. لنسمّها promisify(f)‎ وستقبل دالة الأصل f اللازم تحويلها، وتُعيد دالة غالِفة. يؤدّي الغلاف نفس عمل الشيفرة أعلاه: يُعيد وعدًا ويمرّر النداء إلى الدالة الأصلية f متتّبعًا الناتج في ردّ نداء يصنعه بنفسه: function promisify(f) { return function (...args) { // يعيد التابع المغلّف return new Promise((resolve, reject) => { function callback(err, result) { // ‫رد النداء خاصتنا لـِ f if (err) { return reject(err); } else { resolve(result); } } args.push(callback); // ‫نضيف رد النداء خاصتنا إلى نهاية واسطاء f f.call(this, ...args); // استدعي التابع الأصلي }); }; }; // ‫طريقة الاستخدام: let loadScriptPromise = promisify(loadScript); loadScriptPromise(...).then(...); نفترض هنا بأنّ الدالة الأصلية تتوقّع استلام ردّ نداء له وسيطين (err, result)، وهذا ما نواجهه أغلب الوقت لهذا كتبنا ردّ النداء المخصّص بهذا التنسيق، وتعمل دالة promisify على أكمل وجه… لهذه الحالات. ولكن ماذا لو كانت تتوقّع f ردّ نداء له وسطاء أكثر callback(err, res1, res2, ...)‎؟ إليك نسخة promisify ذكية محسّنة: لو استدعيناها هكذا promisify(f, true)‎ فسيكون ناتج الوعد مصفوفة من نواتج ردود النداء [res1, res2,‎ ...‎]: // ‫التابع promisify(f, true)‎ لجب مصفوفة النتائج function promisify(f, manyArgs = false) { return function (...args) { return new Promise((resolve, reject) => { function callback(err, ...results) { // ‫رد النداء خاصتنا لـِ f if (err) { return reject(err); } else { // إجلب جميع ردود النداء وإذا حدّد أكثر من وسيط resolve(manyArgs ? results : results[0]); } } args.push(callback); f.call(this, ...args); }); }; }; // ‫طريقة الاستخدام: f = promisify(f, true); f(...).then(arrayOfResults => ..., err => ...) أمّا لتنسيقات ردود النداء الشاذّة (مثل التي بدون وسيط err أصلًا callback(result)‎) فيمكننا تحويلها يدويًا بدون استعمال الدالة المساعِدة. كما وهناك وحدات لها دوال تحويل مرنة أكثر في التعامل مثل es6-promisify. وفي Node.js نرى الدالة المضمّنة util.promisify لهذا الغرض. ملاحظة: يعدّ تحويل الدوال إلى وعود نهجًا رائعًا، خاصةً عند استخدام async/await (الّتي سنراها في الفصل التالي)، ولكن ليس بديلًا كليًا لردود النداء. تذكر أن الوعد له نتيجة واحدة فقط، ولكن تقنيًا ممكن أن تُستدعى ردود النداء عدة مرات. لذا فإن تحويل الدوال إلى وعود مخصصة للدوال التي تستدعي ردود النداء لمرة واحدة. وستُتجاهل جميع الاستدعاءات اللاحقة. ترجمة -وبتصرف- للفصل Promisification من كتاب The JavaScript language
  17. ثمّة 5 توابِع ثابتة (static) في صنف الوعود Promise. سنشرح الآن عن استعمالاتها سريعًا. Promise.all لنقل بأنّك تريد تنفيذ أكثر من وعد واحد في وقت واحد، والانتظار حتّى تجهز جميعها. مثلًا أن تُنزّل أكثر من عنوان URL في آن واحد وتُعالج المحتوى ما إن تُنزّل كلها. وهذا الغرض من وجود Promise.all. إليك صياغته: let promise = Promise.all([...promises...]); يأخذ التابِع Promise.all مصفوفة من الوعود (تقنيًا يمكن أن تكون أيّ … ولكنّها في العادة مصفوفة) ويُعيد وعدًا جديدًا. لا يُحلّ الوعد الجديد إلّا حين تستقرّ الوعود في المصفوفة، وتصير تلك المصفوفة التي تحمل نواتج الوعود - تصير ناتج الوعد الجديد. مثال على ذلك تابِع Promise.all أسفله إذ يستقرّ بعد 3 ثوان ويستلم نواتجه في مصفوفة [1, 2, 3]: Promise.all([ new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1 new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2 new Promise(resolve => setTimeout(() => resolve(3), 1000)) // 3 ]).then(alert); // 1,2,3 ما إن تجهز الوعود يُعطي كلّ واحد عنصرًا في المصفوفة لاحِظ كيف أنّ ترتيب عناصر المصفوفة الناتجة يتطابق مع ترتيب الوعود، فحتّى لو أخذ الوعد الأوّل وقتًا أطول من غيره حتّى يُحلّ، فسيظلّ العنصر الأول في مصفوفة النواتج. نستعمل عادةً حيلة صغيرة أن نصنع مصفوفة جديدة بخارطة تأخذ القديمة وتحوّلها إلى وعود، ثمّ نمرّر ذلك كلّه إلى Promise.all. فمثلًا لو لدينا مصفوفة من العناوين، يمكننا جلبها كلّها هكذا: let urls = [ 'https://api.github.com/users/iliakan', 'https://api.github.com/users/remy', 'https://api.github.com/users/jeresig' ]; // ‫نحوّل كلّ عنوان إلى وعد التابِع fetch let requests = urls.map(url => fetch(url)); // ‫ينتظر Promise.all حتّى تُحلّ كلّ المهام Promise.all(requests) .then(responses => responses.forEach( response => alert(`${response.url}: ${response.status}`) )); من أكبر الأمثلة على جلب المعلومات. هي جلب المعلومات لمجموعة من مستخدمي موقع GitHub من خلال اسمائهم (يمكننا جلب مجموعة من السلع بحسب رقم المعرف الخاص بها، منطق الحل متشابه في كِلا الحالتين): let names = ['iliakan', 'remy', 'jeresig']; let requests = names.map(name => fetch(`https://api.github.com/users/${name}`)); Promise.all(requests) .then(responses => { // استدعيت جميع الردود بنجاح for(let response of responses) { alert(`${response.url}: ${response.status}`); // ‫أظهر 200 من أجل كلّ عنوان url } return responses; }) // ‫اربط مصفوفة الردود مع مصفوفة response.json()‎ لقراءة المحتوى .then(responses => Promise.all(responses.map(r => r.json()))) // ‫تحلل جميع الإجابات : وتكوّن مصفوفة "users" منهم .then(users => users.forEach(user => alert(user.name))); لو رُفض أيّ وعد من الوعود، سيرفض الوعد الذي يُعيده Promise.all مباشرةً بذلك الخطأ. مثال: Promise.all([ new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)), // هنا new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)), new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)) ]).catch(alert); // Error: Whoops! نرى هنا أنّ الوعد الثاني يُرفض بعد ثانيتين، ويؤدّي ذلك إلى رفض Promise.all كاملًا، وهكذا يُنفّذ التابِع ‎.catch ويصير الخطأ ناتج Promise.all كلّه. تحذير: في حالة حدوث خطأ ما، تُتجاهل الوعود الأُخرى لو أن وعدًا ما رُفضَ، ستُرفض جميع الوعود من خلال تابع Promise.all مباشرةً، متناسيًا بذلك الوعود الأخرى في القائمة. وعندها ستُتجاهل نتائجها أيضًا. على سبيل المثال، إن كان العديد من استدعاءات fetch كما هو موضح في المثال أعلاه، وفشِل أحدها، فسيستمر تنفيذ الاستدعاءات الأخرى. ولكن لن يشاهدها تابع Promise.all بعد الآن. ربما ستُنجز بقية الوعود، ولكن نتائجها ستُتجاهل في نهاية المطاف. لن يُلغي التابع Promise.all الوعود الأخرى، إذ لا يوجد مثل هذا الفعل في الوعود، سنغطي في فصل آخر طريقة المتبعة في إلغاء الوعود وسيساعدنا التابع AbortController في ذلك، ولكنه ليس جزءًا من واجهة الوعود البرمجية. ملاحظة: يسمح التابع Promise.all(iterable)‎ لغير الوعود (القيم النظامية) في iterable. يقبل التابع Promise.all(...)‎ وعودًا قابلة للتكرار (مصفوفات في معظم الوقت). ولكن لو لم تكُ تلك الكائنات وعودًا. فستُمرّر النتائج كما هي. فمثلًا، النتائج هنا [1, 2, 3]: Promise.all([ new Promise((resolve, reject) => { setTimeout(() => resolve(1), 1000) }), 2, 3 ]).then(alert); // 1, 2, 3 يمكننا تمرير القيم الجاهزة لتابِع Promise.all عندما يكون ذلك مناسبًا. Promise.allSettled إضافة حديثة هذه إضافة حديثة للغة، وقد يحتاج إلى تعويض نقص الدعم في المتصفحات القديمة. لو رُفض أحد الوعود في Promise.all فسيُرفض كلّه مجتمعًا. هذا يفيدنا لو أردنا استراتيجية ”إمّا كل شيء أو لا شيء“، أي حين نطلب وجود النتائج كلها كي نواصل: Promise.all([ fetch('/template.html'), fetch('/style.css'), fetch('/data.json') ]).then(render); // تابِع التصيير يطلب نواتج كلّ توابِع الجلب بينما ينتظر Promise.allSettled استقرار كلّ الوعود. للمصفوفة الناتجة هذه العناصر: {status:"fulfilled", value:result} من أجل الاستجابات الناجحة. ‎{status:"rejected", reason:error}‎ من أجل الأخطاء. مثال على ذلك هو لو أردنا جلب معلومات أكثر من مستخدم واحد. فلو فشل أحد الطلبات ما زلنا نريد معرفة معلومات البقية. فلنستعمل Promise.allSettled: let urls = [ 'https://api.github.com/users/iliakan', 'https://api.github.com/users/remy', 'https://no-such-url' ]; Promise.allSettled(urls.map(url => fetch(url))) .then(results => { // (*) results.forEach((result, num) => { if (result.status == "fulfilled") { alert(`${urls[num]}: ${result.value.status}`); } if (result.status == "rejected") { alert(`${urls[num]}: ${result.reason}`); } }); }); ستكون قيمة results في السطر (*) أعلاه كالآتي: [ {status: 'fulfilled', value: ...ردّ...}, {status: 'fulfilled', value: ...ردّ...}, {status: 'rejected', reason: ...كائن خطأ...} ] هكذا نستلم لكلّ وعد حالته وقيمته أو الخطأ value/error. تعويض نقص الدعم لو لم يكن المتصفّح يدعم Promise.allSettled فمن السهل تعويض ذلك: if(!Promise.allSettled) { Promise.allSettled = function(promises) { return Promise.all(promises.map(p => Promise.resolve(p).then(value => ({ state: 'fulfilled', value }), reason => ({ state: 'rejected', reason })))); }; } في هذه الشيفرة يأخذ التابِع promises.map قيم الإدخال ويحوّلها إلى وعود (في حال وصله شيء ليس بالوعد) ذلك باستعمال p => Promise.resolve(p)‎، بعدها يُضيف دالة المُعاملة ‎.then لكلّ وعد. هذه الدالة تحوّل النواتج الناجحة v إلى {state:'fulfilled', value:v}، والأخطاء r إلى {state:'rejected', reason:r}. هذا التنسيق الذي يستعمله Promise.allSettled بالضبط. يمكننا لآن استعمال Promise.allSettled لجلب نتائج كلّ الوعود الممرّرة حتّى لو رفضت بعضها. Promise.race يشبه التابِع Promise.all إلّا أنّه ينتظر استقرار وعد واحد فقط ويأخذ ناتجه (أو الخطأ). صياغته هي: let promise = Promise.race(iterable); فمثلًا سيكون الناتج هنا 1: Promise.race([ new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)), new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)), new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)) ]).then(alert); // 1 أوّل الوعود هنا كان أسرعها وهكذا صار هو الناتج. ومتى ”فاز أوّل وعد مستقرّ بالسباق“، تُهمل بقية النواتج/الأخطاء. Promise.resolve/reject نادرًا ما نستعمل Promise.resolve و Promise.reject في الشيفرات الحديثة إذ صياغة async/await (نتكلم عنها في مقال لاحق) نشرحها هنا ليكون شرحًا كاملًا، وأيضًا لمن لا يستطيع استعمال async/await لسبب أو لآخر. Promise.resolve(value)‎ يُنشئ وعد مُنجز بالناتجvalue. ويشبه: let promise = new Promise(resolve => resolve(value)); يستخدم هذا التابع للتوافقية، عندما يُتوقع من تابع ما أن يُعيد وعدًا. فمثلًا، يجلب التابِع أدناه loadCached عنوان URL ويخزن المحتوى في الذاكرة المؤقتة. ومن أجل الاستدعاءات اللاحقة لنفس عنوان URL سيحصل على المحتوى فورًا من الذاكرة المؤقتة، ولكنه يستخدم التابع Promise.resolve لتقديم وعدًا بهذا الأمر. لتكون القيمة المرتجعة دائمًا وعدًا: let cache = new Map(); function loadCached(url) { if (cache.has(url)) { return Promise.resolve(cache.get(url)); // (*) } return fetch(url) .then(response => response.text()) .then(text => { cache.set(url,text); return text; }); } يمكننا كتابة loadCached(url).then(…)‎ لأننا نضمن أن التابِع سيُعيد وعدًا. كما يمكننا دائمًا استخدام ‎.then بعد تابِع loadCached. وهذا هو الغرض من Promise.resolve في السطر (*). Promise.reject Promise.reject(error)‎ ينشئ وعد مرفوض مع خطأ. ويشبه: let promise = new Promise((resolve, reject) => reject(error)); عمليًا لا نستعمل هذا التابِع أبدًا. خلاصة لصنف الوعود Promise خمس توابِع ثابتة: Promise.all(promises)‎ -- ينتظر جميع الوعود لتعمل ويعيد مصفوفة بنتائجهم. وإذا رُفض أي وعدٍ منهم، سيرجع Promise.all خطأً، وسيتجاهلُ جميع النتائج الأخرى. Promise.allSettled(promises)‎ -- (التابع مضاف حديثًا) ينتظر جميع الوعود لتُنجز ليُعيد نتائجها كمصفوفة من الكائنات تحتوي على: -state: الحالة وتكون إما "fulfilled" أو "rejected". -value: القيمة (إذا تحقق الوعد) أو reason السبب (إذا رُفض). Promise.race(promises)‎ -- ينتظر الوعد الأول ليُنجز وتكون نتيجته أو الخطأ الذي سيرميه خرج هذا التابِع. Promise.resolve(value)‎ -- ينشئ وعدًا منجزًا مع القيمة الممرّرة. Promise.reject(error)‎ -- ينشئ وعدًا مرفوضًا مع الخطأ المُمرّر. ومن بينها Promise.all هي الأكثر استعمالًا عمليًا. ترجمة -وبتصرف- للفصل Promise API من كتاب The JavaScript language
  18. تُعدّ سلاسل الوعود ممتازة في التعامل مع الأخطاء، فمتى رُفض الوعد ينتقل سير التحكّم إلى أقرب دالة تتعامل مع حالة الرفض، وهذا عمليًا يسهّل الأمور كثيرًا. فمثلًا نرى في الشيفرة أسفله أنّ العنوان المرّر إلى fetch خطأ (ما من موقع بهذا العنوان) ويتعامل التابِع ‎.catch مع الخطأ: fetch('https://no-such-server.blabla') // هذا الموقع من وحي الخيال العلمي .then(response => response.json()) .catch(err => alert(err)) // TypeError: failed to fetch (يمكن أن يتغيّر النصّ بتغيّر الخطأ) وكما ترى فليس ضروريًا أن يكون التابِع ‎.catch مباشرةً في البداية، بل يمكن أن يظهر بعد تابِع ‎.then واحد أو أكثر حتّى. أو قد يكون الموقع سليمًا ولكنّ الردّ ليس كائن JSON صالح. الطريقة الأسهل في هذه الحالة لالتقاط كلّ الأخطاء هي بإضافة تابِع ‎.catch إلى نهاية السلسلة: fetch('/article/promise-chaining/user.json') .then(response => response.json()) .then(user => fetch(`https://api.github.com/users/${user.name}`)) .then(response => response.json()) .then(githubUser => new Promise((resolve, reject) => { let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.append(img); setTimeout(() => { img.remove(); resolve(githubUser); }, 3000); })) .catch(error => alert(error.message)); // هنا الطبيعي هو ألّا يعمل تابِع ‎.catch مطلقًا، ولكن لو رُفض أحد الوعود أعلاه (بسبب مشكلة في الشبكة أو كائن غير صالح أو أيّ شيء آخر)، فسيستلم التابِع الخطأ. صياغة try..catch الضمنية تُحيط بشيفرتي مُنفّذ الوعود ودوال مُعاملة الوعود عبارة try..catch مخفية إن صحّ القول، فإن حدث استثناء يُلتقط ويتعامل معه المحرّك على أنّه حالة رفض. خُذ مثلًا هذه الشيفرة: new Promise((resolve, reject) => { throw new Error("Whoops!"); // لاحِظ }).catch(alert); // Error: Whoops! … وهل تعمل تمامًا مثل عمل هذه: new Promise((resolve, reject) => { reject(new Error("Whoops!")); // لاحِظ }).catch(alert); // Error: Whoops! تتلقّى عبارة try..catch المخفي المُحيطة بالمُنفّذ الأخطاءَ تلقائيًا وتحوّلها إلى وعود مرفوضة. ولا يحدث هذا في دالة المُنفّذ فحسب بل أيضًا في دوال المُعاملة. فلو رمينا شيئًا في دالة المُعاملة للتابِع throw فيعني ذلك بأنّ الوعد رُفض وينتقل سير التحكّم إلى أقرب دالة تتعامل مع الأخطاء. إليك مثالًا: new Promise((resolve, reject) => { resolve("ok"); }).then((result) => { throw new Error("Whoops!"); // نرفض الوعد }).catch(alert); // Error: Whoops! وهذا يحدث مع الأخطاء كافّة وليس فقط لتلك التي رُميت بعبارة throw. حتّى أخطاء المبرمجين، انظر: new Promise((resolve, reject) => { resolve("ok"); }).then((result) => { blabla(); // ما من دالة كهذه }).catch(alert); // ReferenceError: blabla is not defined تابِع ‎.catch الأخير لا يستلم حالات الرفض الصريحة فحسب، بل تلك التي تحدث أحيانًا في دوال المُعاملة أعلاه أيضًا. إعادة الرمي كما رأينا فوجود تابِع ‎.catch نهاية السلسلة شبيه بعبارة try..catch. يمكن أن نكتب ما نريد من دوال مُعاملة ‎.then وثمّ استعمال تابِع ‎.catch واحد في النهاية للتعامل مع أخطائها. في عبارات try..catch العاديّة نحلّل الخطأ ونُعيد رميه لو لم نستطع التعامل معه. ذات الأمر مع الوعود. فإن رمينا شيئًا داخل ‎.catch، ينتقل سير التحكّم إلى أقرب دالة تتعامل مع الأخطاء. وإن تعاملنا مع الخطأ كما يجب فيتواصل إلى أقرب دالة ‎.then. في المثال أسفله يتعامل التابِع ‎.catch مع الخطأ كما ينبغي: // ‫سلسلة التنفيذ: catch -> then new Promise((resolve, reject) => { throw new Error("Whoops!"); }).catch(function(error) { alert("The error is handled, continue normally"); }).then(() => alert("Next successful handler runs")); لا تنتهي هنا كتلة ‎.catch البرمجية بأيّ أخطاء، بذلك تُستدعى دالة المُعاملة في تابِع ‎.then التالي. نرى في المثال أسفله الحالة الثانية للتابِع ‎.catch. تلتقط دالة المُعاملة عند (*) الخطأ ولكن لا تعرف التعامل معه (مثلًا لا تعرف إلّا أخطاء URIError) فترميه ثانيةً: // ‫سلسلة التنفيذ: catch ‎→ catch → then new Promise((resolve, reject) => { throw new Error("Whoops!"); }).catch(function(error) { // (*) if (error instanceof URIError) { // handle it } else { alert("Can't handle such error"); *!* throw error; // ‫رمي هذا الخطأ أو أي خطأ آخر سينقُلنا إلى catch التالية */!* } }).then(function() { /* لن يعمل هنا */ }).catch(error => { // (**) alert(`The unknown error has occurred: ${error}`); // لن يعيد أي شيء=>عملية التنفيذ حدثت بطريقة عادية }); ينتقل سير التنفيذ من تابِع ‎.catch الأول عند (*) إلى التالي عند (**) في السلسلة. حالات رفض ماذا يحدث لو لم نتعامل مع الخطأ؟ فنقل مثلًا نسينا إضافة تابِع ‎.catch في نهاية السلسلة هكذا: new Promise(function() { noSuchFunction(); // ‫خطأ (لا يوجد هذا التابع) }) .then(() => { // معالجات الوعد الناجح سواءً واحدة أو أكثر }); // ‫بدون ‎.catch في النهاية! لو حدث خطأ يُرفض الوعد وعلى سير التنفيذ الانتقال إلى أقرب دالة تتعامل مع حالات الرفض. ولكن ما من دالة كهذه و”يعلق“ الخطأ إن صحّ التعبير إذ ما من شيفرة تتعامل معه. عمليًا يتشابه هذا مع الأخطاء التي لم نتعامل معها في الشيفرات، أي أنّ شيئًا مريعًا قد حدث. تتذكّر ما يحدث لو حدث خطأ عادي ولم تلتقطه عبارة try..catch؟ ”يموت“ النص البرمجي ويترك رسالةً في الطرفية. ذات الأمر يحدث مع حالات رفض الوعود التي لم يجري التعامل معها. يتعقّب محرّك جافاسكربت هذه الحالات ويولّد خطأً عموميًا في هذه الحالة. يمكنك أن تراه في الطرفية إن شغلت المثال أعلاه. يمكننا في المتصفّحات التقاط هذه الأخطاء باستعمال الحدث unhandledrejection: *!* window.addEventListener('unhandledrejection', function(event) { // ‫لدى كائن الحدث خاصيتين مميزتين: alert(event.promise); // ‫[object Promise] - الوعد الذي يولد الخطأ alert(event.reason); // ‫Error: Whoops!‎ - كائن الخطأ غير المعالج }); */!* new Promise(function() { throw new Error("Whoops!"); }); // ‫لا يوجد catch لمعالجة الخطأ إنّما الحدث هو جزء من معيار HTML. لو حدث خطأ ولم يكن هناك تابِع ‎.catch فيُشغّل معالج unhandledrejection ويحصل على كائن الحدث مع معلومات حول الخطأ حتى نتمكن من فعل شيء ما. مثل هذه الأخطاء عادةً ما تكون غير قابلة للاسترداد، لذلك أفضل طريقة للخروج هي إعلام المستخدم بالخطأ أو حتى إبلاغ الخادم بالخطأ. ثمّة في البيئات الأخرى غير المتصفّحات (مثل Node.js) طرائقَ أخرى لتعقّب الأخطاء التي لم نتعامل معها. خلاصة يتعامل ‎.catch مع الأخطاء في الوعود أيًا كانت: أكانت من استدعاءات reject()‎ أو من رمي الأخطاء في دوال المُعاملة. يجب أن نضع ‎.catch في الأماكن التي نريد أن نعالج الخطأ فيها ومعرفة كيفية التعامل معها. يجب على المعالج تحليل الأخطاء (الأصناف المخصصة تساعدنا بذلك) وإعادة رمي الأخطاء غير المعروفة (ربما تكون أخطاء برمجية). لا بأس بعدم استخدام ‎.catch مطلقًا، إن لم يكُ هنالك طريقة للاسترداد من الخطأ. على أية حال، يجب أن يكون لدينا معالج الأحداث unhandledrejection (للمتصفحات وللبيئات الأخرى) وذلك لتتبع الأخطاء غير المُعالجة وإعلام المستخدم (وربما إعلام الخادم) عنها. حتى لا يموت تطبيقنا مطلقًا. تمارين خطأ في تابِع setTimeout ما رأيك هل ستعمل .catch؟ وضح إجابتك. new Promise(function(resolve, reject) { setTimeout(() => { throw new Error("Whoops!"); }, 1000); }).catch(alert); الجواب: لا لن تعمل: new Promise(function(resolve, reject) { setTimeout(() => { throw new Error("Whoops!"); }, 1000); }).catch(alert); كما ذكرنا في هذا الفصل لدينا "صياغة try..catch ضمنية" حول تابع معيّن. لذلك تُعالج جميع الأخطاء المتزامنة. ولكن هنا الخطأ لا يُنشأ عندما يعمل المُنفذّ وإنما في وقت لاحق. لذا فإن الوعد لن يستطيع معالجته. ترجمة -وبتصرف- للفصل Error handling with promises من كتاب The JavaScript language
  19. طرحناها في الفصل "مقدمة إلى ردود النداء callbacks" مشكلةً ألا وهي أنّ لدينا تسلسلًا من المهام غير المتزامنة ويجب أن تُجرى واحدةً بعد الأخرى، مثلًا تحميل السكربتات. كيف نكتب شيفرة … لهذه المشكلة؟ تقدّم لنا الوعود طرائق مختلفة لهذا الغرض. وفي هذا الفصل سنتكلّم عن سَلسلة الوعود فقط. هكذا تكون: new Promise(function(resolve, reject) { setTimeout(() => resolve(1), 1000); // (*) }).then(function(result) { // (**) alert(result); // 1 return result * 2; }).then(function(result) { // (***) alert(result); // 2 return result * 2; }).then(function(result) { alert(result); // 4 return result * 2; }); الفكرة وما فيها هي تمرير الناتج في سلسلة توابِع ‎.then تابعًا تابعًا. هكذا تكون: يبدأ الوعد الأوّل ويُنجز خلال ثانية واحدة (*). بعدها يُستدعى معالج ‎.then (**). النتيجة التي ستعود ستمرر إلى معالج ‎.then التالي (***). وهكذا… . نظرًا لتمرير النتيجة على طول سلسلة المعالجات، يمكننا رؤية سلسلة من استدعاءات alert هكذا: 1 ← 2 ← 4. ويعود سبب هذا كلّه إلى أنّ استدعاء promise.then يُعيد وعدًا هو الآخر، بذلك يمكننا استدعاء التابِع ‎.then التالي على ذلك الوعد، وهكذا. حين تُعيد دالة المُعاملة قيمةً ما، تصير القيمة ناتج ذلك الوعد، بذلك يمكن استدعاء ‎.then عليه. خطأ شائع بين المبتدئين: تقنيًا يمكننا إضافة أكثر من تابِع ‎.then إلى وعد واحد. لا يُعدّ هذا سَلسلة وعود. مثلًا: let promise = new Promise(function(resolve, reject) { setTimeout(() => resolve(1), 1000); }); promise.then(function(result) { alert(result); // 1 return result * 2; }); promise.then(function(result) { alert(result); // 1 return result * 2; }); promise.then(function(result) { alert(result); // 1 return result * 2; }); هنا كتبنا أكثر من دالة مُعاملة لوعد واحد، وهذه التوابِع لا تمرّر القيمة إلى بعضها البعض، بل كلّ تعالجها على حدة. إليك الصورة (ووازِن بينها وبين السلسلة أعلاه): تتلقّى كلّ توابِع ‎.then في نفس الوعد ذات الناتج (أي ناتج الوعد) بذلك تعرض الشيفرة أعلاه نتائج alert متطابقة: 1. أمّا عمليًا فنادرًا ما نستعمل أكثر من دالة مُعاملة واحدة لكلّ وعد، على عكس السَلسلة التي يشيع استعمالها. إعادة الوعود يمكن لدالة المُعاملة (المستعملة في ‎.then(handler)‎) إنشاء وعد وإعادته. هنا تنتظر دوال المُعاملة الأخرى حتّى يكتمل الوعد وتستلم ناتجه. مثال على هذا: new Promise(function(resolve, reject) { setTimeout(() => resolve(1), 1000); }).then(function(result) { alert(result); // 1 // لاحِظ return new Promise((resolve, reject) => { // (*) setTimeout(() => resolve(result * 2), 1000); }); }).then(function(result) { // (**) alert(result); // 2 return new Promise((resolve, reject) => { setTimeout(() => resolve(result * 2), 1000); }); }).then(function(result) { alert(result); // 4 }); هنا يعرض أوّل تابِع .then القيمة 1 ويُعيد new Promise(…)‎ في السطر (*). بعد ثانية واحدة، … الوعد ويُمرّر ناتجه (أي وسيط التابِع resolve، في حالتنا هو result * 2) إلى دالة المُعاملة التالية في تابِع .then التالي. نرى كيف أنّ الدالة في السطر (**) تعرض 2 وتؤدّي ما أدّته دالة المُعاملة السابقة. بذلك نحصل على ما حصلنا عليه في المثال السابق: 1 ثمّ 2 ثمّ 4، الفرق هو التأخير لمدّة ثانية بين كلّ استدعاء من استدعاءات alert. بإعادة الوعود يمكننا بناء سلسلة من الإجراءات غير المتزامنة. مثال: loadScript لنستعمل هذه الميزة مع دالة loadScript (التي كتبناها في الفصل السابق) لنُحمّل النصوص البرمجية واحدًا تلو الآخر: loadScript("/article/promise-chaining/one.js") .then(function(script) { return loadScript("/article/promise-chaining/two.js"); }) .then(function(script) { return loadScript("/article/promise-chaining/three.js"); }) .then(function(script) { // نستعمل الدوال المعرّف عنها في النصوص البرمجية // ونتأكّد تمامًا بأنّها حُمّلت one(); two(); three(); }); يمكننا أيضًا تقصير الشيفرة قليلًا باستعمال الدوال السهميّة: loadScript("/article/promise-chaining/one.js") .then(script => loadScript("/article/promise-chaining/two.js")) .then(script => loadScript("/article/promise-chaining/three.js")) .then(script => { // اكتمل تحميل النصوص، يمكننا استعمال الدوال فيها الآن one(); two(); three(); }); نرى هنا أنّ كلّ استدعاء من استدعاءات loadScript يُعيد وعدًا، ويعمل تابِع ‎.then التالي في السلسلة متى … الوعد. بعدها تبدأ الدالة بتحميل النص البرمجي التالي، وهكذا تُحمّل كلّ النصوص واحدًا بعد آخر. ويمكننا إضافة ما نريد من إجراءات غير متزامنة إلى السلسلة، ولن يزيد طول الشيفرة إلى اليمين، بل إلى أسفل، ولن نُقابل وجه … ثانيةً. يمكننا تقنيًا إضافة تابِع ‎.then داخل دوال loadScript مباشرةً هكذا: loadScript("/article/promise-chaining/one.js").then(script1 => { loadScript("/article/promise-chaining/two.js").then(script2 => { loadScript("/article/promise-chaining/three.js").then(script3 => { // ‫يمكن أن تصل هذه الدالة إلى المتغيّرات script1 وscript2 وscript3 one(); two(); three(); }); }); }); وتؤدّي الشيفرة نفس العمل: تُحمّل 3 نصوص برمجية بالترتيب. المشكلة هي أنّ طولها يزيد نحو اليمين وهي نفس مشكلة ردود النداء. عادةً ما يجهل المبرمجون الجدد الذين يستعملون الوعود ميزة السَلسلة، فيكتبون الشيفرات هكذا. لكنّ سَلسلة الوعود هي الأمثل وغالبًا الأفضل. ولكنّ استعمال .then مباشرةً أحيانًا لا يكون بالمشكلة الكبيرة، إذ يمكن للدوال المتداخلة الوصول إلى … الخارجي. في المثال أعلاه مثلًا يمكن لآخر ردّ نداء متداخل الوصول إلى كلّ المتغيّرات script1 وscript2 وscript3، إلّا أنّ هذا استثناء عن القاعدة وليس قاعدة بحدّ ذاتها. ملاحظة: كائنات Thenables على وجه الدقة، لا تعيد المعالجات وعودًا وإنما تعيد كائن thenable - وهو كائن عشوائي له نفس توابع .then. ويتعامل معه بنفس طريقة التعامل مع الوعد. الفكرة أن مكتبات الخارجية تنفذ كائنات خاصة بها "متوافقة مع الوعد". ويمكن أن يملكوا مجموعة توابع موسّعة. ولكن يجب أن يتوافقوا مع الوعود الأصيلة، لأنهم ينفذون .then. وإليك مثالًا على كائن thenable: class Thenable { constructor(num) { this.num = num; } then(resolve, reject) { alert(resolve); // function() { native code } // ‫إنجاز الوعد وتحقيقه مع this.num*2 بعد ثانية setTimeout(() => resolve(this.num * 2), 1000); // (**) } } new Promise(resolve => resolve(1)) .then(result => { return new Thenable(result); // (*) }) .then(alert); // shows 2 after 1000ms تتحقق جافاسكربت من الكائن المُعاد من معالج .then في السطر (*): إن لديه تابع قابل للاستدعاء يدعى then عندها سيستدعي ذاك التابع مزودًا بذلك بالتوابع الأصيلة مثل: resolve و reject كوسطاء (مشابه للمنفذ) وينتظر حتى يستدعى واحدًا منهم. في المثال أعلاه تستدعى resolve(2) بعد ثانية انظر (**). بعدها تمرر النتيجة إلى أسفل السلسلة. تتيح لنا هذه المميزات دمج الكائنات المخصصة مع سلاسل الوعود دون الحاجة إلى الوراثة من الوعد Promise. مثال أضخم: fetch عادةً ما نستعمل الوعود في برمجة الواجهات الرسومية لطلبات الشبكة. لنرى الآن مثالًا أوسع مجالًا قليلًا. سنستعمل التابِع fetch لتحميل بعض المعلومات التي تخصّ المستخدم من الخادوم البعيد. لهذا التابِع معاملات كثيرة اختيارية كتبنا عنا في فصول مختلفة، إلّا أنّ صياغته الأساسية بسيطة إلى حدّ ما: let promise = fetch(url); هكذا نُرسل طلبًا شبكيًا إلى العنوان url ونستلم وعدًا. …… ما إن يردّ الخادم البعيد بترويسات الطلب، ولكن قبل تنزيل الردّ كاملًا. علينا استدعاء التابِع response.text()‎ لقراءة الردّ كاملًا، وهو يُعيد وعدًا … متى نُزّل النص الكامل من الخادوم البعيد، وناتجه يكون ذلك النص. تُرسل الشيفرة أسفله طلبًا إلى user.json وتحمّل نصّه من الخادوم: fetch('/article/promise-chaining/user.json') // ‫إن ‎.then تعمل عندما يستجيب الخادم البعيد .then(function(response) { // ‫إن التابع response.text()‎ يُعيد وعدًا جديدًا والذي يعاد مع كامل نص الاستجابة // عندما يُحمّل return response.text(); }) .then(function(text) { // ‫...وهنا سيكون محتوى الملف البعيد alert(text); // {"name": "iliakan", isAdmin: true} }); كما أنّ هناك التابِع response.json()‎ والذي يقرأ البيانات المستلمة البعيدة ويحلّلها على أنّها JSON. في حالتنا هذا أفضل وأسهل فهيًا نستعمله. كما وسنستعمل الدوال السهميّة للاختصار قليلًا: // ‫مشابه للمثال أعلاه ولكن التابع response.json()‎ يحلل المحتوى البعيد كملف JSON fetch('/article/promise-chaining/user.json') .then(response => response.json()) .then(user => alert(user.name)); // iliakan, got user name الآن لنصنع شيئًا بهذا المستخدم الذي حمّلناه. يمكننا مثلًا إجراء طلبات أكثر من غِت‎هَب وتحميل ملف المستخدم الشخصي وعرض صورته: // ‫أنشئ طلب لـِuser.json fetch('/article/promise-chaining/user.json') // ‫حمله وكأنه ملف json .then(response => response.json()) // ‫أنشئ طلب لـِ GitHub .then(user => fetch(`https://api.github.com/users/${user.name}`)) // ‫حمّل الرد كملف json .then(response => response.json()) // ‫أظهر الصورة الرمزية (avatar) من (githubUser.avatar_url) لمدة 3 ثواني .then(githubUser => { let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.append(img); setTimeout(() => img.remove(), 3000); // (*) }); الشيفرة تعمل على أكمل وجه (طالِع التعليقات لتعرف التفاصيل). ولكن هناك مشكلة فيه قد تحدث، وهي خطأ شائع يقع فيه من يستعمل الوعود أوّل مرّة. طالِع السطر (*): كيف يمكن أن نفعل مهمّة معينة متى اكتمل عرض الصورة وأُزيلت؟ فلنقل مثلًا سنعرض استمارة لتحرير ذلك المستخدم أو أيّ شيء آخر. حاليًا… ذلك مستحيل. لنقدر على مواصلة السلسلة علينا إعادة وعد المُنجز متى اكتمل عرض الصورة. هكذا: fetch('/article/promise-chaining/user.json') .then(response => response.json()) .then(user => fetch(`https://api.github.com/users/${user.name}`)) .then(response => response.json()) // هنا .then(githubUser => new Promise(function(resolve, reject) { // (*) let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.append(img); setTimeout(() => { img.remove(); resolve(githubUser); // (**) }, 3000); })) // يحدث بعد 3 ثوانٍ .then(githubUser => alert(`Finished showing ${githubUser.name}`)); هكذا صارت تُعيد دالة المُعاملة في .then عند السطر (*) كائنَ new Promise لا … إلّا بعد استدعاء resolve(githubUser)‎ في setTimeout عند (**). وسينتظر تابِع ‎.then التالي في السلسلة اكتمال ذلك. تُعد إعادة الإجراءات غير المتزامنة للوعود دومًا إحدى الممارسات الصحيحة في البرمجة. هكذا يسهّل علينا التخطيط للإجراءات التي ستصير بعد هذا، فحتّى لو لم نريد توسعة السلسلة الآن لربّما احتجنا إلى ذلك لاحقًا. وأخيرًا، يمكننا أيضًا تقسيم الشيفرة إلى دوال يمكن إعادة استعمالها: function loadJson(url) { return fetch(url) .then(response => response.json()); } function loadGithubUser(name) { return fetch(`https://api.github.com/users/${name}`) .then(response => response.json()); } function showAvatar(githubUser) { return new Promise(function(resolve, reject) { let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.append(img); setTimeout(() => { img.remove(); resolve(githubUser); }, 3000); }); } // نستعملها الآن: loadJson('/article/promise-chaining/user.json') .then(user => loadGithubUser(user.name)) .then(showAvatar) .then(githubUser => alert(`Finished showing ${githubUser.name}`)); // اكتمل عرض كذا // ... خلاصة إن أعادت دالة مُعاملة ‎.then (أو catch/finally، لا يهمّ حقًا) وعدًا، فتنتظر بقية السلسلة حتّى تُنجز متى حدث ذلك يُمرّر الناتج (أو الخطأ) إلى التي بعدها. إليك الصورة الكاملة: تمارين الوعد: then وcatch هل تؤدّي هاتين الشيفرتين نفس الغرض؟ أي هل يتطابق سلوكهما في الحالات المختلفة، وأيّما كانت دوال المُعاملة؟ promise.then(f1).catch(f2); مقابل: promise.then(f1, f2); الحل الجواب المختصر: لا ليسا متساويين: الفرق أنه إن حدث خطأ في f1 فستعالجها ‎.catch هنا: promise .then(f1) .catch(f2); …لكن ليس هنا: promise .then(f1, f2); وذلك بسبب تمرير الخطأ لأسفل السلسلة، وفي الجزء الثاني من الشيفرة لا يوجد سلسلة أقل من f1. بمعنى آخر يمرر ‎.then النتيجة أو الخطأ إلى ‎.then/catch التالية. لذا في المثال الأول يوجد catch بينما في المثال الثاني لا يوجد. ولذلك لم يعالج الخطأ. ترجمة -وبتصرف- للفصل Promises chaining من كتاب The JavaScript language
  20. لنقل بأنّك أنت هو عبد الحليم حافظ، ولنفترض بأنّ مُعجبوك من المحيط إلى الخليج يسألونك ليلًا نهارًا عن الأغنية الشاعرية التالية. وكي تُريح بالك تعدهم بإرسالها إليهم ما إن تُنشر. فتُعطي مُعجبيك قائمة يملؤون فيها عناوين بريدهم. ومتى ما نشرت الأغنية يستلمها كلّ من في تلك القائمة. ولو حصل مكروه (لا سمح الله) مثل أن شبّت النار والتهمت الأستديو ولم تقدر على نشر الأغنية - لو حصل ذلك فسيعلمون به أيضًا. وعاش الجميع بسعادة وهناء: أنت إذ لا يُزعجك الجميع بالتهديدات والتوعّدات، ومُعجبيك إذ لن تفوتهم أيّة رائعة من روائعك الفنية. إليك ما يشبه الأمور التي نفعلها في الحياة الواقعية - في الحياة البرمجية: ”شيفرة مُنتِجة“ تُنفّذ شيئًا وتأخذ الوقت. مثل الشيفرات التي تُحمّل البيانات عبر الشبكة. هذا أنت، ”المغنّي“. ”شيفرة مُستهلِكة“ تطلب ناتج ”الشيفرة المُنتِجة“ ما إن يجهز. وهناك عديد من الدوال تحتاج إلى هذا الناتج. هذه ”مُعجبوك“. الوعد (Promise) هو كائن فريد في جافاسكربت يربط بين ”الشيفرة المُنتِجة“ و”الشيفرة المُستهلِكة“. في الحياة العملية، الوعد هو ”قائمة الاشتراك“. يمكن أن تأخذ ”الشيفرة المُنتِجة“ ما تلزم من وقت لتقدّم لنا النتيجة التي وعدتنا بها، وسيُجهّزها لنا ”الوعد“ لأيّة شيفرة طلبتها متى جهزت. إن هذه المقاربة ليست دقيقة جدًا على الرغم من أنها جيدة كبداية ولكن وعود جافاسكربت أكثر تعقيدًا من قائمة اشتراك بسيطة بل لديها ميزات وقيود إضافية. هذه صياغة الباني لكائنات الوعد: let promise = new Promise(function(resolve, reject) { // ‫المُنفِّذ (الشيفرة المُنتجة، مثل ”المغنّي“) }); تُدعى الدالة الممرّرة إلى new Promise ”بالمُنفِّذ“. متى صُنع الوعد new Promise عملت الدالة تلقائيًا. يحتوي هذا المُنفِّذ الشيفرة المُنتجِة، ويمكن أن تقدّم لنا في النهاية ناتجًا. في مثالنا أعلاه، فالمُنفِّذ هذا هو ”المغنّي“. تقدّم جافاسكربت الوسيطين resolve و reject وهما ردود نداء. كما ولا نضع الشيفرة التي نريد تنفيذها إلا داخل المُنفِّذ. لا يهمّنا متى سيعرف المُنفِّذ الناتجَ (آجلًا كان ذلك أم عاجلًا)، بل أنّ عليه نداء واحدًا من ردود النداء هذه: resolve(value)‎: لو اكتملت المهمّة بنجاح. القيمة تسجّل في value. reject(error)‎: لو حدث خطأ. error هو كائن الخطأ. إذًا نُلخّص: يعمل المُنفِّذ تلقائيًا وعليه مهمّة استدعاء resolve أو reject. لكائن الوعد promise الذي أعاده الباني new Promise خاصيتين داخليتين: الحالة state: تبدأ بالقيمة "pending" وبعدها تنتقل إلى "fulfilled" متى استُدعت resolve، أو إلى "rejected" متى استُدعت reject. الناتج result: يبدأ أولًا غير معرّف undefined، وبعدها يتغيّر إلى value متى استُدعت resolve(value)‎ أو يتغيّر إلى error متى استُدعت reject(error)‎. وفي النهاية ينقل المُنفِّذ الوعدَ promise ليصير بإحدى الحالات الآتية: سنرى لاحقًا كيف سيشترك ”مُعجبونا“ بهذه التغييرات. إليك مثالًا عن بانيًا للوعود ودالة مُنفِّذ بسيطة فيها ”شيفرة مُنتجِة“ تأخذ بعض الوقت (باستعمال setTimeout): let promise = new Promise(function(resolve, reject) { // تُنفّ الدالة مباشرةً ما إن يُصنع الوعد // ‫وبعد ثانية واحدة نبعث بإشارة بأنّ المهمة انتهت والنتيجة هي ”تمت“ (done) setTimeout(() => resolve("done"), 1000); }); بتشغيل الشيفرة أعلاه، نرى أمرين اثنين: يُستدعى المُنفِّذ تلقائيًا ومباشرةً (عند استعمال new Promise). يستلم المُنفِّذ وسيطين: دالة الحلّ resolve ودالة الرفض reject، وهي دوال معرّفة مسبقًا في محرّك جافاسكربت، ولا داعٍ بأن نصنعها نحن، بل استدعاء واحدة ما إن تجهز النتيجة. بعد سنة من عملية ”المعالجة“ يستدعي المُنفِّذ الدالةَ resolve("done")‎ لتُنتج الناتج. هكذا تتغيّر حالة كائن promise: كان هذا مثالًا عن مهمّة اكتملت بنجاح، أو ”وعد تحقّق“. والآن سنرى مثالًا عن مُنفِّذ يرفض الوعد مُعيدًا خطأً: let promise = new Promise(function(resolve, reject) { // بعد ثانية واحدة نبعث بإشارة بأنّ المهمة انتهت ونُعيد خطأً setTimeout(() => reject(new Error("Whoops!")), 1000); }); باستدعاء reject(...)‎ ننقل حالة كائن الوعد إلى حالة الرفض "rejected": ملخّص القول هو أنّ على المُنفِّذ تنفيذ المهمة (أي ما يأخذ بعض الوقت ليكتمل) وثمّ يستدعي واحدةً من الدالتين resolve أو reject لتغيير حالة كائن الوعد المرتبط بالمُنفِّذ. يُسمّى الوعد الذي تحقّق أو نُكث الوعد المنُجز، على العكس من الوعد المعلّق. ملاحظة: إما أن تظهر نتيجة واحدة أو خطأ، يجب على المنفّذ أن يستدعي إما resolve أو reject. أي تغيير في الحالة يعدّ تغييرًا نهائيًا. وسيُتجاهل جميع الاستدعاءات اللاحقة سواءً أكانت resolve أو reject: let promise = new Promise(function(resolve, reject) { resolve("done"); reject(new Error("…")); // ستتجاهل setTimeout(() => resolve("…")); // ستتجاهل }); الفكرة هنا أن خرج عمل المنفذّ سيعرض إما نتيجة معينة أو خطأ. وتتوقع التعليمتين resolve/reject وسيطًا واحدًا مُررًا (أو بدون وسطاء نهائيًا) وأي وسطاء إضافية ستُتجاهل. ملاحظة: الرفض مع كائن Error في حال حدوث خطأ ما، يجب على المنفذّ أن يستدعي تعليمة reject. ويمكن تمرير أي نوع من الوسطاء (تمامًا مثل: resolve). ولكن يوصى باستخدام كائنات Error (أو أي كائنات ترث من Error). وقريبًا سنعرف بوضوح سبب ذلك. ملاحظة: استدعاء resolve/reject الفوري عمليًا عادة ينجز المنفذّ عمله بشكل متزامن ويستدعي resolve/reject بعد مرور بعض الوقت، ولكن الأمر ليس إلزاميًا، يمكننا استدعاء resolve أو reject فورًا، هكذا: let promise = new Promise(function(resolve, reject) { // يمكننا القيام بالمهمة مباشرة resolve(123); // أظهر مباشرة النتيجة: 123 }); على سبيل المثال من الممكن أن يحدث ذلك في حال البدء بمهمة معينة ولكن تكتشف بأن كلّ شيء أنجز وخزّن في الذاكرة المؤقتة. هذا جيد، فعندها يجب أن ننجز الوعد فورًا. ملاحظة: الحالة state و النتيجة result الداخليتين تكون خصائص الحالة state و النتيجة result لكائن الوعد داخلية. ولا يمكننا الوصول إليهم مباشرة. يمكننا استخدام التوابِع ‎.then/.catch/.finally لذلك والتي سنشرحُها أدناه. الاستهلاك: عبارات then وcatch وfinally كائن الوعد هو كالوصلة بين المُنفِّذ (أي ”الشيفرة المُنتِجة“ أو ”المغنّي“) والدوال المُستهلكة (أي ”المُعجبون“) التي ستسلم الناتج أو الخطأ. يمكن تسجيل دوال الاستهلاك (أو أن تشترك، كما في المثال العملي ذاك) باستعمال التوابِع ‎.then و‎.catch و‎.finally. then يُعدّ ‎.then أهمّها وعِماد القصة كلها. صياغته هي: promise.then( function(result) { /* نتعامل مع الناتج الصحيح */ }, function(error) { /* نتعامل مع الخطأ */ } ); الوسيط الأوّل من التابِع ‎.then يُعدّ دالة تُشغّل إن تحقّق الوعد، ويكون الوسيطُ الناتج. بينما الوسيط الثاني يُعدّ دالةً تُشغّل إن رُفض الوعد، ويكون الوسيطُ الخطأ. إليك مثال نتعامل فيه مع وعد تحقّق بنجاح: let promise = new Promise(function(resolve, reject) { setTimeout(() => resolve("done!"), 1000); }); // ‫تُنفِّذ resolve أول دالة في ‎.then promise.then( result => alert(result), // ‫ إظهار "done!‎" بعد ثانية error => alert(error) // هذا لا يعمل ); هكذا نرى الدالة الأولى هي التي نُفّذت. وإليك المثال في حالة الرفض: let promise = new Promise(function(resolve, reject) { setTimeout(() => reject(new Error("Whoops!")), 1000); }); // ‫تُنفِّذ reject ثاني دالة في ‎.then promise.then( result => alert(result), // لا تعمل error => alert(error) // ‫ إظهار "Error: Whoops!‎" بعد ثانية ); لو لم نُرِد إلّا حالات الانتهاء الناجحة، فيمكن أن نقدّم دالةً واحدة وسيطًا إلى ‎.then فقط: let promise = new Promise(resolve => { setTimeout(() => resolve("done!"), 1000); }); promise.then(alert); // ‫ إظهار "done!‎" بعد ثانية catch لو لم نكن نهتمّ إلّا بالأخطاء، فعلينا استعمال null وسيطًا أولًا: ‎.then(null, errorHandlingFunction)‎، أو نستعمل ‎.catch(errorHandlingFunction)‎ وهو يؤدّي ذات المبدأ تمامًا ولا فرق إلّا قصر الثانية مقارنة بالأولى: let promise = new Promise((resolve, reject) => { setTimeout(() => reject(new Error("Whoops!")), 1000); }); // .catch(f) is the same as promise.then(null, f) promise.catch(alert); // ‫إظهار "Error: Whoops!‎" بعد ثانية finally كما المُنغلِقة finally في عبارات try {...} catch {...}‎ العادية، فهناك مثلها في الوعود. استدعاء ‎.finally(f)‎ يشبه استدعاء ‎.then(f, f)‎، ووجه الشبه هو أنّ الدالة f تعمل دومًا متى …. الوعد، كان قد تحقّق أو نُكث. استعمال finally مفيد جدًا لتنظيف ما تبقّى من أمور مهمًا كان ناتج الوعد، مثل إيقاف أيقونات التحميل (فلم نعد نحتاجها). هكذا مثلًا: new Promise((resolve, reject) => { // ‫افعل شيئًا يستغرق وقتًا ثم استدع resolve/reject lre }) // runs when the promise is settled, doesn't matter successfully or not .finally(() => stop loading indicator) .then(result => show result, err => show error) ولكنها ليست متطابقة تمامًا مع then(f,f)‎، فهناك فروقات مهمّة: ليس لدالة المُعالجة finally أيّ وسطاء. أي لسنا نعلم في finally أكان الوعد تحقّق أو نُكث، وهذه ليست مشكلة إذ ما نريده عادةً هو تنفيذ بعض الأمور ”العامّة“ لنُنهي ما بدأنا به. يمرُّ مُعالج finally على النتائج والأخطاء وبعدها إلى المعالج التالي. مثال على ذلك هو الناتج الذي تمرّر من finally إلى then هنا: new Promise((resolve, reject) => { setTimeout(() => resolve("result"), 2000) }) .finally(() => alert("Promise ready")) .then(result => alert(result)); // <-- ‫‎.then ستعالج الناتج وهنا واجه الوعد خطأً، وتمرّر من finally إلى catch: new Promise((resolve, reject) => { throw new Error("error"); }) .finally(() => alert("Promise ready")) .catch(err => alert(err)); // <-- ‫‎.catch ستعالج كائن الخطأ error object هذا السلوك مفيد جدًا إذ لا يفترض بأن تتعامل finally مع ناتج الوعد، بل تمرّره إلى من يتعامل معه. سنتحدّث أكثر عن سَلسلة الوعود وتمرير النواتج بين … في الفصل اللاحق. أخيرًا وليس آخرًا، صياغة ‎.finally(f)‎ أسهل وأريح بكثير من صياغة ‎.then(f, f)‎ فلا داعٍ لتكرار الدالة f. ملاحظة: في الوعود المنجزة المُعالجات تعمل مباشرة إن كان الوعد مُعلقًا لسببٍ ما، فإن معالجات ‎.then/catch/finally ستنتظره. عدا ذلك (إن كان الوعد مُنجزًا) فإن المعالجات ستنفذّ مباشرةً: // يصبح الوعد منجزًا ومتحققًا بعد الإنشاء مباشرةً let promise = new Promise(resolve => resolve("done!")); promise.then(alert); // done! (تظهر الآن) الآن لنرى أمثلة عملية على فائدة الوعود في كتابة الشيفرات غير المتزامنة. تحميل السكربتات: الدالة loadScript أمامنا من الفصل الماضي الدالة loadScript لتحميل السكربتات. إليك الدالة بطريقة ردود النداء، لنتذكّرها لا أكثر ولا أقل: function loadScript(src, callback) { let script = document.createElement('script'); script.src = src; script.onload = () => callback(null, script); // خطأ في تحميل السكربت كذا script.onerror = () => callback(new Error(`Script load error for ${src}`)); document.head.append(script); } هيًا نُعد كتابتها باستعمال الوعود. لن تطلب دالة loadScript الجديدة أيّ ردود نداء، بل ستصنع كائن وعد يتحقّق متى اكتمل التحميل، وتُعيده. يمكن للشيفرة الخارجية إضافة الدوال المُعالجة (أي دوال الاشتراك) إليها باستعمال .then: function loadScript(src) { return new Promise(function(resolve, reject) { let script = document.createElement('script'); script.src = src; script.onload = () => resolve(script); script.onerror = () => reject(new Error(`Script load error for ${src}`)); document.head.append(script); }); } الاستعمال: let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"); promise.then( script => alert(`${script.src} is loaded!`), error => alert(`Error: ${error.message}`) ); promise.then(script => alert('Another handler...')); بنظرة خاطفة يمكن أن نرى فوائد هذه الطريقة موازنةً بطريقة ردود النداء: الوعود ردود النداء تتيح لنا الوعود تنفيذ الأمور بترتيبها الطبيعي أولًا نشغّل loadScript(script)‎ ومن بعدها ‎.then نكتب ما نريد فعله بالنتيجة. يجب أن يكون تابِع callback تحت تصرفنا عند استدعاء loadScript(script, callback)‎. بعبارة أخرى يجب أن نعرف ما سنفعله بالنتيجة قبل استدعاء loadScript. يمكننا استدعاء ‎.then في الوعد عدة مرات كما نريد. في كلّ مرة نضيف معجب جديدة "fan"، هنالك تابع سيضيف مشتركين جُدد إلى قائمة المشتركين. سنرى المزيد حول هذا الأمر في الفصل القادم يمكن أن يكون هنالك ردّ واحد فقط. table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } إذًا، فالوعود تقدّم لنا تحكمًا مرنًا بالشيفرة وسير تنفيذها، وما زالت هنالك الكثير من الأمور الرائعة التي سنتعرف عليها الفصل القادم. تمارين إعادة … الوعد؟ ما ناتج الشيفرة أدناه؟ let promise = new Promise(function(resolve, reject) { resolve(1); setTimeout(() => resolve(2), 1000); }); promise.then(alert); الحل الناتج هو: 1. يُهمل استدعاء resolve الثاني إذ لا يتهمّ المحرّك إلّا بأول استدعاء من reject/resolve، والباقي كلّه يُهمل. التأخير باستعمال الوعود تستعمل الدالة المضمّنة في اللغة setTimeout ردودَ النداء. اصنع واحدة تستعمل الوعود. على الدالة delay(ms)‎ إعادة وعد ويجب أن … هذا الوعد خلال ms مليثانية، ونُضيف تابِع .then إليه هكذا: function delay(ms) { // شيفرتك هنا } delay(3000).then(() => alert('runs after 3 seconds')); الحل function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } delay(3000).then(() => alert('runs after 3 seconds')); لاحظ أنّنا في هذا التمرين استدعينا resolve بلا وسطاء، ولم نُعد أيّ قيمة من delay بل … فقط صورة دائرة متحركة مع وعد أعِد كتابة الدالة showCircle في حلّ التمرين السابق لتُعيد وعدًا بدل أن تستلم ردّ نداء. ويكون استعمالها الجديد هكذا: showCircle(150, 150, 100).then(div => { div.classList.add('message-ball'); div.append("Hello, world!"); }); ليكن الحلّ في التمرين المذكور أساس المسألة الآن. الحل يمكنك مشاهدة الحل عبر المثال الحي. ترجمة -وبتصرف- للفصل Promise من كتاب The JavaScript language
  21. ملاحظة ابتدائية: لتوضيح طريقة استخدام ردود النداء callbacks والوعود promises والمفاهيم المجردة سنستخدم بعض توابِع المتصفح، تحديدًا سكربتات التحميل loading scripts وأدوات التلاعب بالمستندات البسيطة. إن لم تكُ على دراية بهذه الطرق، وكان استخدامها في الأمثلة مربكًا نوعًا ما، أو حتى إن رغبتَ فقط في فهمها فهمًا أفضل، فيمكنك قراءة بعض الفصول من الجزء التالي من الدورة التعليمية. وبالرغم من ذلك سنحاول توضيح الأمور بكل الأحوال ولن يكون هنالك شيء معقد في المتصفح. أغلب الإجراءات في جافاسكربت هي إجراءات غير متزامنة، أي أنّنا نشغّلها الآن ولكنّها تنتهي في وقت لاحق. مثال على ذلك هو حين نُجدول تلك الإجراءات باستعمال setTimeout. يُعدّ تحميل السكربتات والوحدات (سنشرحها في الفصول القادمة) أمثلة فعلية عن الإجراءات غير المتزامنة. لاحظ مثلًا الدالة loadScript(src)‎ أسفله، إذ تُحمّل سكربتًا من العنوان src الممرّر: function loadScript(src) { // ‫أنشئ وسم <script> وأضفه للصفحة // ‫فسيؤدي ذلك إلى بدء تحميل السكربت ذي الخاصية src ثم تنفيذه عند الاكتمال let script = document.createElement('script'); script.src = src; document.head.append(script); } تُضيف الدالة إلى المستند الوسم <script src=‎"…"‎> الجديد الذي وُلّد ديناميكيًا. حين يعمل المتصفّح ينفّذ الدالة. يمكننا استعمال هذه الدالة هكذا: // حمّل ونفّذ السكربت في هذا المكان loadScript('/my/script.js'); يُنفّذ هذا السكربت ”بلا تزامن“ إذ تحميله يبدأ الآن أمّا تشغيله فيكون لاحقًا متى انتهى الدالة. ولو كانت هناك شيفرة أسفل الدالة loadScript(…)‎ فلن ننتظر انتهاء تحميل السكربت. loadScript('/my/script.js'); // الشيفرة أسفل ‫loadScript // لا تنتظر انتهاء تحميل السكربت // ... فنقل بأنّنا نريد استعمال السكربت متى انتهى تحميله (فهو مثلًا يُصرّح عن دوال جديدة ونريد تشغيلها). ولكن لو حدث ذلك مباشرةً بعد استدعاء loadScript(…)‎، فلن تعمل الشيفرة: // في السكربت الدالة ‫"function newFunction() {…}‎" loadScript('/my/script.js'); newFunction(); // ما من دالة بهذا الاسم! الطبيعي هو أنّ ليس على المتصفّح انتظار مدّة من الزمن حتّى ينتهي تحميل السكربت. كما نرى فحاليًا لا تقدّم لنا الدالة loadScript أيّ طريقة لنعرف وقت اكتمال التحميل، بل يُحمّل السكربت ويُشغّل متى اكتمل، وفقط. ولكن ما نريده هو معرفة وقت ذلك كي نستعمل الدوال والمتغيرات الجديدة فيه. لنُضيف دالة ردّ نداء callback لتكون الوسيط الثاني لدالة loadScript، كي تُنفّذ متى انتهى تحميل السكربت: function loadScript(src, callback) { let script = document.createElement('script'); script.src = src; script.onload = () => callback(script); document.head.append(script); } الآن متى أردنا استدعاء الدوال الجديدة في السكربت، نكتبها في دالة ردّ النداء تلك: loadScript('/my/script.js', function() { // يعمل ردّ النداء بعد تحميل السكربت newFunction(); // الآن تعمل ... }); هذه الفكرة تمامًا: يُعدّ الوسيط الثاني دالةً (عادةً ما تكون مجهولة) تعمل متى اكتمل الإجراء. وإليك مثالًا حقيقيًا قابل للتشغيل: function loadScript(src, callback) { let script = document.createElement('script'); script.src = src; script.onload = () => callback(script); document.head.append(script); } *!* loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => { alert(`Cool, the script ${script.src} is loaded`); alert( _ ); // التابع معرف في السكربت المُحمّل }); */!* يُسمّى هذا الأسلوب في البرمجة غير المتزامنة ”الأسلوب المبني على ردود النداء“ (callback-based)، إذ على الدوال التي تنفّذ أمور غير متزامنة تقديمَ وسيط ردّ نداء callback نضع فيه الدالة التي ستُشغّل متى اكتملت تلك الأمور. ولقد فعلناها في loadScript ولكن بكلّ الأحوال هذا نهج عام. رد النداء داخل رد النداء وكيف نُحمّل سكربتين واحدًا تلو الآخر: يعمل الأول وبعدها حين ينتهي يعمل الثاني؟ الحلّ الطبيعي الذي سيفكّر به الجميع هو وضع استدعاء loadScript الثاني داخل ردّ النداء، هكذا: loadScript('/my/script.js', function(script) { // جميل، اكتمل تحميل كذا، هيًا نُحمّل الآخر alert(`Cool, the ${script.src} is loaded, let's load one more`); loadScript('/my/script2.js', function(script) { // جميل، اكتمل تحميل السكربت الثاني أيضًا alert(`Cool, the second script is loaded`); }); }); وبعدما تكتمل الدالة الخارجية loadScript، يُشغّل ردّ النداء الدالة الداخلية. ولكن ماذا لو أردنا سكربتًا آخر، أيضًا…؟ loadScript('/my/script.js', function(script) { loadScript('/my/script2.js', function(script) { loadScript('/my/script3.js', function(script) { // ...نواصل متى اكتملت كلّ السكربتات }); }) }); هكذا نضع كلّ إجراء جديد داخل ردّ نداء. لو كانت الإجراءات قليلة فليست مشكلة، ولكن لو زادت فستصير كارثية. سنرى ذلك قريبًا. التعامل مع الأخطاء لم نضع الأخطاء في الحسبان في هذه الأمثلة. ماذا لو فشل تحميل السكربت؟ يفترض أن تتفاعل دالة ردّ النداء بناءً على الخطأ. إليك نسخة محسّنة من الدالة loadScript تتعقّب أخطاء التحميل: function loadScript(src, callback) { let script = document.createElement('script'); script.src = src; // هنا script.onload = () => callback(null, script); // خطأ في تحميل السكربت كذا script.onerror = () => callback(new Error(`Script load error for ${src}`)); document.head.append(script); } هنا نستدعي callback(null, script)‎ لو اكتمل التحميل، ونستدعي callback(error)‎ لو لم يكتمل. طريقة الاستعمال: loadScript('/my/script.js', function(error, script) { if (error) { // نتعامل مع الخطأ } else { // تحمّل السكربت بنجاح } }); نُعيد، ما استعملناه هنا للدالة loadScript هي طريقة مشهورة جدًا، تُدعى بأسلوب ”ردّ نداء الأخطاء أولًا“. إليك ما اصطُلح عليه: يُحجز الوسيط الأوّل من دالة callback للخطأ إن حدث، ويُستدعى callback(err)‎. يكون الوسيط الثاني (وغيرها إن دعت الحاجة) للنتيجة الصحيحة، هكذا نستعمل الدالة callback فقط للأمرين: الإبلاغ عن الأخطاء وتمرير النتيجة. هرم العذابات (Pyramid of Doom) يمكن أن نرى أنّ البرمجة بنمط اللاتزامن مفيد جدًا، وهذا جليّ من النظرة الأولى. فحين نكون أمام استدعاء أو استدعاءين فقط، فأمورنا ممتازة. ولكن لو تتابعت الإجراءات غير المتزامنة واحدة تلو الأخرى، سنرى هذا: loadScript('1.js', function(error, script) { if (error) { handleError(error); } else { // ... loadScript('2.js', function(error, script) { if (error) { handleError(error); } else { // ... loadScript('3.js', function(error, script) { if (error) { handleError(error); } else { // ‫...نُواصل بعد اكتمال تحميل كل السكربتات (*) } }); } }) } }); إليك ما في الشيفرة أعلاه: نُحمّل ‎1.js ونرى لو لم تحدث أخطاء. نُحمّل ‎2.js ونرى لو لم تحدث أخطاء. نُحمّل ‎3.js، ولو لم تحدث أخطاء نفّذنا شيئًا (*). فكلّما تداخل الاستدعاءات أكثر أصبحت الشيفرة متشعّبة جدًا وأصعب في الإدارة كثيرًا، هذا خصوصًا لو كانت هناك شيفرة فعلية لا ... (مثل الحلقات والعبارات الشرطية وغيرها). يُسمّون هذا أحيانًا ”بجحيم ردود النداء“ (callback hell) أو ”هرم العذابات“. بزيادة الإجراءات غير المتزامنة واحدًا بعد آخر، يتشعّب ”هرم“ الاستدعاءات المتداخلة إلى اليمين أكثر فأكثر، ولن يمضي من الوقت الكثير حتى دخلتْ في دوّامة حلزونية محال تنظيمها. بهذا تُعدّ طريقة البرمجة هذه سيّئة. يمكننا في محاولة يائسة لتقليل وقع المشكلة تحويل كلّ إجراء إلى دالة منفردة، هكذا: loadScript('1.js', step1); function step1(error, script) { if (error) { handleError(error); } else { // ... loadScript('2.js', step2); } } function step2(error, script) { if (error) { handleError(error); } else { // ... loadScript('3.js', step3); } } function step3(error, script) { if (error) { handleError(error); } else { // ‫...نواصل بعد اكتمال تحميل كل السكربتات (*) } }; رأيت الفكرة؟ مبدأ الشيفرة واحد وليس هناك تداخلات متشعّبة إذ حوّلنا كلّ إجراء إلى دالة لا مشكلة في الشيفرة إذ هي تعمل، ولكنّها فعليًا مقطّعة إربًا إربًا، ويصعُب على الشخص قراءتها وغالبًا تنتقل بنظرك بين أجزاء الشيفرة وأنت تقرأها. ليس هذا بالأمر الجيد خصوصًا لو لم يكن قارئ الشيفرة يعلم بما تفعله أو إلى أين ينقل نظره. كما وأنّ الدوال بأسماء الخطوات step* هي لاستعمال واحد ولم نصنعها إلّا لتفادي ”هرم العذابات“. لن يأتي أحد لاحقًا ويُعيد استعمالها في أيّ شيء عدا سلسلة الإجراءات هذه. بهذا ”نلوّث“ فضاء الأسماء، إن جاز التعبير. نريد بالطبع ما هو أفضل من هذا الحل. ولحسن حظنا (كالعادة)، فهناك طرق أخرى نتجنّب بها الأهرام هذه، أفضلها هي استعمال ”الوعود“ وسنشرحها في الفصل القادم. تمارين دائرة تتحرك ولها رد نداء نرى في التمرين Animated circle دائرة يكبُر مقاسها في حركة جميلة. لنقل بأنّا لا نريد الدائرة فقط، بل أيضًا عرض رسالة فيها. يجب أن تظهر الرسالة بعدما ينتهي التحريك (أي تكون الدائرة بمقاسها الكبير الكامل)، وإلّا بدا النص قبيح المظهر. في حلّ ذاك التمرين، نرى الدالة showCircle(cx, cy, radius)‎ ترسم الدائرة ولكن لا تُقدّم لنا أيّ طريقة نعرف بها انتهاء الرسم. أضِف وسيط ردّ نداء showCircle(cx, cy, radius, callback)‎ يُستدعى متى اكتمل التحريك. على المُعامل callback استلام كائن <div> للدائرة وسيطًا له. إليك مثالًا: showCircle(150, 150, 100, div => { div.classList.add('message-ball'); div.append("Hello, world!"); }); تجربة حية: يمكنك اعتماد حل هذا التمرين منطلقًا للحل. الحل يمكنك معاينة الحل من خلال المثال الحي. ترجمة -وبتصرف- للفصل Introduction: callbacks من كتاب The JavaScript language
  22. متى نكون نطوّر البرامج نحتاج إلى أصناف أخطاء خاصة بنا لتوضّح تمامًا ما قد يحدث خطأً في المهام التي نقوم بها. فمثلًا لأخطاء الشبكة نستعمل HttpError، ولعمليات قواعد البيانات DbError ولعمليات البحث NotFoundError وهكذا. وعلى هذه الأخطاء أن تدعم الخاصيات الأساسية مثل الرسالة message والاسم name والمَكدس stack (يفضّل ذلك)، ولكن يمكن أن تحتوي على خصائص أخرى خاصة بها مثل خاصية statusCode لكائنات HttpError وتحمل قيمة من قيم رموز الحالة 404 أو 403 أو 500. تتيح لنا جافاسكربت استعمال throw بتمرير أيّ وسيط، لذا فأصناف الخطأ الخاصة بنا يمكن ألّا ترث (تقنيًا) من كائن الخطأ Error، ولكن لو ورثنا منها فيمكنن للجميع استعمال obj instanceof Error لاحقًا لتتعرّف على كائنات الخطأ، بذلك يكون أفضل لو ورثناها. وكلّما كبر التطبيق شكّلت الأخطاء التي خصّصناها شجرة، فمثلًا سيرث الصنف HttpTimeoutError الصنفَ HttpError، وهكذا دواليك. توسيع Error لنأخذ مثالًا دالةَ readUser(json)‎ تقرأ كائن JSON في بيانات المستخدم. وهذا مثال عن كائن json صالح: let json = `{ "name": "John", "age": 30 }`; سنستعمل في الشيفرة التابِع JSON.parse، وإن استلم كائن json معطوب رمى خطأ SyntaxError. ولكن، حتّى لو كان الكائن صحيحًا صياغيًا، فلا يعني هذا أنّ المستخدم صالحًا أيضًا، أم لا؟ لربّما لا يحتوي بعض البيانات مثل خاصيتي الاسم json والعمر name الضروريتين للمستخدمين. بهذا لن تقرأ الدالة readUser(json)‎ كائن JSON فحسب، بل ستفحص (”تتحقّق من“) البيانات، فلو لم تكن الحقول المطلوبة موجودة، أو كان تنسيق الكائن مغلوطًا، فهنا نكون أمام خطأ… وهذا الخطأ ليس خطأً صياغيًا SyntaxError إذ أنّ البيانات صحيحة صياغيًا، بل نوع آخر من الأخطاء. سنسمّي هذا النوع ValidationError ونصنع صنف له. على هذا النوع من الأخطاء احتواء ما يلزم من معلومات تخصّ الحقل المخالف. يفترض علينا وراثة الصنف المضمّن في اللغة Error لصنفنا ValidationError. إليك شيء عن شيفرة الصنف المضمّن لنعرف ما نحاول توسعته: // شيفرة مبسّطة لصنف الخطأ ‫Error المضمّن في لغة جافاسكربت نفسها class Error { constructor(message) { this.message = message; this.name = "Error"; // (تختلف الأسماء باختلاف أصناف الأخطاء المضمّنة) this.stack = <call stack>; // ليست قياسية، إلّا أنّ أغلب البيئات تدعمها } } الآن صار وقت أن يرث الصنف ValidationError منها: class ValidationError extends Error { constructor(message) { super(message); // (1) this.name = "ValidationError"; // خطأ في التحقّق (2) } } function test() { throw new ValidationError("Whoops!"); // آخ } try { test(); } catch(err) { alert(err.message); // آخ! alert(err.name); // خطأ في التحقّق alert(err.stack); // قائمة من الاستدعاءات المتداخلة في كلّ منها رقم السطر } لاحظ كيف أنّنا في السطر (1) استدعينا الباني الأب، إذ تطلب منا لغة جافاسكربت استدعاء super في البانيات الابنة، أي أنّه أمر إلزامي. يضبط الباني الأب خاصية الرسالة message. كما يضبط أيضًا خاصية الاسم name لتكون "Error"، ولذلك نُعدّلها إلى القيمة الصحيحة في السطر (2). فلنحاول الآن استعماله في readUser(json)‎: class ValidationError extends Error { constructor(message) { super(message); this.name = "ValidationError"; // خطأ في التحقّق } } // الاستعمال function readUser(json) { let user = JSON.parse(json); if (!user.age) { throw new ValidationError("No field: age"); // حقل غير موجود: العمر } if (!user.name) { throw new ValidationError("No field: name"); // حقل غير موجود: الاسم } return user; } // هنا نجرّب باستعمال try..catch try { let user = readUser('{ "age": 25 }'); } catch (err) { if (err instanceof ValidationError) { alert("Invalid data: " + err.message); // البيانات غير صالحة: حقل غير موجود: الاسم } else if (err instanceof SyntaxError) { // (*) alert("JSON Syntax Error: " + err.message); // ‫خطأ صياغي لكائن JSON } else { throw err; // خطأ لا نعرفه، علينا إعادة رميه (**) } } تتعامل كتلة try..catch في الشيفرة أعلاه النوعين من الأخطاء: ValidationError والخطأ المضمّن SyntaxError الذي يرميه التابِع JSON.parse. لاحِظ أيضًا كيف استعملنا instanceof لفحص نوع الخطأ في السطر (*). يمكننا أيضًا مطالعة err.name هكذا: // ... // بدل ‫(err instanceof SyntaxError) } else if (err.name == "SyntaxError") { // (*) // ... ولكنّ استعمال instanceof أفضل بكثير إذ يحدث ونوسّع مستقبلًا الصنف ValidationError بأصناف فرعية منه مثل PropertyRequiredError، والفحص عبر instanceof سيظلّ يعمل للأصناف الموروثة منه، كما من المهمّ أن تُعيد كتلة catch رمي الأخطاء التي لا تفهمها، كما في السطر (**). ليس على هذه الكتلة إلّا التعامل مع أخطاء التحقّق والصياغة، أمّا باقي الأخطاء (والتي قد تحدث بسبب الأخطاء المطبعية في الشيفرة أو غيرها من أمور غريبة عجيبة) فيجب أن تسقط أرضًا. تعميق الوراثة صنف الخطأ ValidationError عامٌ جدًا جدًا، إذ يمكن أن تحصل أمور كثيرة خطأً في خطأ. لربّما كانت الخاصية غير موجودة أو كان نسقها خطأ (مثل تقديم سلسلة نصية قيمةً للعمر age). لنصنع الصنف …. PropertyRequiredError ونستعمله فقط للخاصيات غير الموجودة، وسيحتوي على أيّة معلومات إضافية عن الخاصية الناقصة. class ValidationError extends Error { constructor(message) { super(message); this.name = "ValidationError"; // خطأ في التحقّق } } // هذا class PropertyRequiredError extends ValidationError { constructor(property) { super("No property: " + property); // خاصية غير موجودة this.name = "PropertyRequiredError"; // خطأ إذ الخاصية مطلوبة this.property = property; } } // الاستعمال function readUser(json) { let user = JSON.parse(json); if (!user.age) { throw new PropertyRequiredError("age"); // العمر } if (!user.name) { throw new PropertyRequiredError("name"); // الاسم } return user; } // هنا نجرّب باستعمال ‫try..catch try { let user = readUser('{ "age": 25 }'); } catch (err) { if (err instanceof ValidationError) { alert("Invalid data: " + err.message); // البيانات غير صالحة: خاصية غير موجودة: الاسم alert(err.name); // PropertyRequiredError alert(err.property); // name } else if (err instanceof SyntaxError) { alert("JSON Syntax Error: " + err.message); // خطأ صياغي لكائن ‫JSON } else { throw err; // خطأ لا نعرفه، علينا إعادة رميه } } يسهُل علينا استعمال الصنف الجديد PropertyRequiredError، فكلّ ما علينا تمريره هو اسم الخاصية: new PropertyRequiredError(property)‎، وسيُولّد الباني الرسالةَ message التي نفهمها نحن البشر. لاحظ كيف أنّنا أسندنا الخاصية this.name في باني PropertyRequiredError يدويًا، مرّة ثانية. قد ترى هذا الأمر مُتعبًا حيث ستُسند قيمة this.name = <class name>‎ في كلّ صنف خطأ تصنعه. يمكن تجنّب هذا العناء وإسناد قيمة مناسبة لصنف ”الخطأ الأساس“ this.name = this.constructor.name، وبعدها نرث من هذا الصنف كلّ أصناف الأخطاء المخصّصة. لنسمّه مثلًا MyError. إليك شيفرة MyError وغيرها من أصناف أخطاء مخصّصة، ولكن مبسّطة: class MyError extends Error { constructor(message) { super(message); this.name = this.constructor.name; // هنا } } class ValidationError extends MyError { } class PropertyRequiredError extends ValidationError { constructor(property) { super("No property: " + property); // خاصية غير موجودة this.property = property; } } // الاسم صحيح alert( new PropertyRequiredError("field").name ); // PropertyRequiredError الآن صارت شيفرات الأخطاء المخصّصة أقصر بكثير (خاصّة ValidationError) إذ حذفنا السطر "this.name = ...‎" في الباني. تغليف الاستثناءات هدف الدالة readUser في الشيفرة أعلاه هو ”قراءة بيانات المستخدم“، ويمكن أن تحدث مختلف الأخطاء أثناء تنفيذ ذلك. حاليًا نرى SyntaxError وValidationError فقط، ولكن في المستقبل العاجل ستصير الدالة readUser أكبر وأكبر وقد تُولّد لنا أنواع أخرى من الأخطاء. ومَن يتعامل مع هذه الأخطاء؟ الشيفرة التي تستدعي readUser! حاليًا لا تستعمل إلا بضعة تعابير شرطية if في كُتل catch (تفحص الصنف وتتعامل مع الأخطاء وتُعيد رمي ما لا تفهم)، وسيكون المخطط هكذا: try { ... readUser() // الخطأ الأساسي هنا ... } catch (err) { if (err instanceof ValidationError) { // ‫معالحة أخطاء ValidationError } else if (err instanceof SyntaxError) { // ‫معالجة الأخطاء الصياغية SyntaxError } else { throw err; // خطأ مجهول فلنُعد رميه من جديد } } نرى في الشيفرة البرمجية أعلاه نوعين من الأخطاء ولكن ممكن أن يكون أكثر من ذلك. ولكن متى ولّدت الدالة readUser أنواع أخرى من الأخطاء، فعلينا طرح السؤال: هل علينا حقًا فحص كلّ نوع من أنواع الأخطاء واحدًا واحدًا في كلّ شيفرة تستدعي readUser؟ عادةً ما يكون الجواب هو ”لا“، فالشيفرة الخارجية تفضّل أن تكون ”على مستوًى أعلى من ذلك المستوى“، أي أن تستلم ما يشبه ”خطأ في قراءة البيانات“، أمّا عن سبب حدوثه فلا علاقة لها به (طالما رسالة الخطأ تصف نفسها). أو ربّما (وهذا أفضل) تكون هناك طريقة لتحصل فيها على بيانات الخطأ، لو أرادت الشيفرة ذلك. تدعى الطريقة التي وصفناها بتغليف الاستثناءات. لنصنع الآن صنف ”خطأ في القراءة“ ReadError لنمثّل هذه الأخطاء. متى حدثت أخطاء داخل الدالة readUser، سنلتقطها فيها ونُولّد خطأ ReadError، بدلًا من ValidationError و SyntaxError. الكائن ReadError سيُبقي أيضًا إشارة إلى الخطأ الأصلي في خاصية السبب cause. لذا و هكذا لن يكون للشيفرة الخارجية (التي ستستدعي readUser) إلّا فحص ReadError. وليس كلّ نوع من أخطاء قراءة البيانات. وإن كان يحتاج لمزيد من التفاصيل عن الخطأ يمكنه العودة إلى الخاصية cause والتحقق منها. إليك الشيفرة التي نُعرّف فيها الخطأ ReadError ونمثّل طريقة استعماله في الدالة readUser وفي try..catch: class ReadError extends Error { constructor(message, cause) { super(message); this.cause = cause; this.name = 'ReadError'; // خطأ في القراءة } } class ValidationError extends Error { /*...*/ } class PropertyRequiredError extends ValidationError { /* ... */ } function validateUser(user) { if (!user.age) { throw new PropertyRequiredError("age"); // العمر } if (!user.name) { throw new PropertyRequiredError("name"); // الاسم } } function readUser(json) { let user; try { user = JSON.parse(json); } catch (err) { // هنا if (err instanceof SyntaxError) { throw new ReadError("Syntax Error", err); // خطأ صياغي } else { throw err; } } try { validateUser(user); } catch (err) { // وهنا if (err instanceof ValidationError) { throw new ReadError("Validation Error", err); // خطأ في التحقّق } else { throw err; } } } try { readUser('{bad json}'); } catch (e) { if (e instanceof ReadError) { // هنا alert(e); // Original error: SyntaxError: Unexpected token b in JSON at position 1 alert("Original error: " + e.cause); // الخطأ الأصل } else { throw e; } } في الشيفرة أعلاه تعمل الدالة readUser تمامًا كم وصفها، تلتقط أخطاء الصياغة والتحقّق وترمي أخطاء قراءة ReadError بدلها (كما وتُعيد رمي الأخطاء المجهولة أيضًا). هكذا ليس على الشيفرة الخارجية إلّا فحص instanceof ReadError فقط، لا داعٍ للتحقّق من كلّ خطأ يمكن أن يحصل في هذه المجرّة. يُسمّى هذا النهج ”بتغليف الاستثناءات“ حيث نستلم نحن ”الاستثناءات في المستوى المنخفض من البرمجة“ (low level) و”نُغلّفها“ داخل خطأ ReadError يكون أكثر بساطة وأسهل استعمالًا للشيفرات التي تنادي على الدوال. هذا النهج مستعمل بكثرة في البرمجة كائنية التوجّه. خلاصة يمكننا وراثة صنف الخطأ Error وغيرها من أخطاء مضمّنة كما الوراثة العادية. المهم أن ننتبه من خاصية الاسم name ولا ننسى استدعاء super. يمكننا استعمال instanceof لفحص ما نريد من أخطاء بدقّة، كما ويعمل المُعامل مع الوراثة. أحيانًا نستلم كائن خطأ من مكتبة طرف ثالث وما من طريقة سهلة لنعرف اسم صنفها. هنا يمكن استعمال خاصية الاسم name لإجراء هذا الفحص. أسلوب تغليف الاستثناءات هو أسلوب منتشر الاستعمال، فيه تتعامل الدالة مع الاستثناءات في المستوى المنخفض من البرمجة، وتصنع أخطاء مستواها عالٍ بدل تلك المنخفضة. وأحيانًا تصير الاستثناءات المنخفضة خصائص لكائن الخطأ (تمامًا مثل err.cause في الأمثلة أعلاه)، ولكنّ هذا ليس إلزاميًا أبدًا. تمارين الوراثة من SyntaxError الأهمية: 5 اصنع الصنف FormatError ليرث من الصنف المضمّن SyntaxError. يجب أن يدعم الصنف خصائص الاسم name والرسالة message والمَكدس stack. طريقة الاستعمال: let err = new FormatError("formatting error"); // خطأ في التنسيق alert( err.message ); // خطأ في التنسيق alert( err.name ); // FormatError alert( err.stack ); // المَكدس alert( err instanceof FormatError ); // true alert( err instanceof SyntaxError ); // ‫true (إذ يرث الصنف الصنفَ SyntaxError) الحل class FormatError extends SyntaxError { constructor(message) { super(message); this.name = this.constructor.name; } } let err = new FormatError("formatting error"); alert( err.message ); // خطأ في التنسيق alert( err.name ); // خطأ في التنسيق alert( err.stack ); // stack(المَكدس) alert( err instanceof SyntaxError ); // true ترجمة -وبتصرف- للفصل Custom errors, extending Error من كتاب The JavaScript language
  23. مهما كنّا عباقرة نحن معشر المبرمجين، فلا بدّ أن تكون في السكربتات مشكلة ما. تحدث هذه المشاكل إمّا بسببنا، أو بسبب شيء أدخله المستخدم لم نتوقّعه، أو بسبب ردّ فيه خطأ من الخادوم، أو بمليار سبب آخر. في العادة ”يموت“ السكربت (أي يتوقّف مباشرة) لو حدث خطأ، ويطبع ذلك في الطرفية. ولكنّ الصياغة try..catch تتيح لنا ”التقاط“ هذه الأخطاء والقيام بما هو مفيد بدل أن يموت السكربت. صياغة try..catch للتعبير try..catch كتلتين برمجيتين أساسيتين: التجربة try والالتقاط catch بعدها: try { // الشيفرة... } catch (err) { // التعامل مع الأخطاء } يعمل التعبير هكذا: أولًا، يجري تنفيذ الشيفرة في try {...}‎. لم نجح التنفيذ دون أيّ أخطاء، تُهمل الكتلة catch(err)‎ وتصل عملية التنفيذ إلى نهاية try وتواصل عملها بعد تجاهل catch. إن حدث أيّ خطأ يتوقّف التنفيذ داخل كتلة try وينتقل سير العملية إلى بداية الكتلة catch(err)‎. سيحتوي المتغير err (يمكننا استبداله بأيّ اسم آخر) كائن خطأ فيه تفاصيل عمّا حدث. بهذا لا تقتل الأخطاء داخل كتلة try {…}‎ السكربت، فما زال ممكنًا أن نتعامل معها في catch. وقت الأمثلة. إليك مثالًا ليس فيه أخطاء: يعرض alert السطرين (1) و (2): try { alert('Start of try runs'); // (1) <-- بداية التشغيلات // ...ما من أخطاء هنا alert('End of try runs'); // (2) <-- نهاية التشغيلات } catch(err) { // (3)‫ تُهمل catch إذ ليست هناك أخطاء alert('Catch is ignored, because there are no errors'); } مثال فيه خطأ: يعرض السطرين (1) و(3): try { alert('Start of try runs'); // (1) <-- بداية التشغيلات lalala; // error, variable is not defined! خطأ: المتغير غير معرّف! alert('End of try (never reached)'); // (2)‫ نهاية كتلة try (لا نصل إليها أبدًا) } catch(err) { alert(`Error has occurred!`); // (3) <-- حدث خطأ! } تحذير: لا يعمل تعبير try..catch إلّا مع الأخطاء أثناء التشغيل يجب أن تكون الشيفرة البرمجية صياغتها صحيحة لكي تعمل try..catch بعبارة أخرى، يجب أن تكون الشيفرة البرمجية خالية من أخطاء الصياغة. لن تعمل في هذه الحالة لأن هنالك أقواسًا مفتوحة بدون غُلاقاتها. try { {{{{{{{{{{{{ } catch(e) { alert("لا يمكن للمحرك فهم هذه الشيفرة فهي غير صحيحة"); } أولًا يقرأ محرك جافاسكربت الشيفرة البرمجية، ومن ثمّ يشغّلها. تدعى الأخطاء التي تحدث في مرحلة القراءة أخطاء التحليل (parse-time) ولا يمكن إصلاحها (من داخل الشيفرة البرمجية نفسها). وذلك لأن محرك لغة جافاسكربت لا يستطيع فهم الشيفرة البرمجية من الأساس. تحذير:تعمل الصياغة try..catch بشكل متزامن. إذا حدث خطأ ما في شيفرة برمجية مجدولة مثلما في setTimeout فلن تستطيع الصياغة try..catch أن تلتقطه: try { setTimeout(function() { noSuchVariable; // السكربت سيموت هنا }, 1000); } catch (e) { alert( "won't work" ); } وذلك لأن التابِع سيُنفذ لاحقًا، بينما يكون محرك جافاسكربت غادر باني try..catch. للتقاط خطأ بداخل تابِع مُجدوَل يجب أن تكون الصياغة try..catch في داخل هذا التابِع. setTimeout(function() { try { // صياغة ‫try..catch تُعالح الخطأ noSuchVariable; // try..catch handles the error! } catch { alert( "error is caught here!" ); } }, 1000); كائن الخطأ Error تولّد جافاسكربت -متى حدث الخطأ- كائنًا يحوي تفاصيل الخطأ كاملةً، بعدها تُمرّرها وسيطًا إلى catch: try { // ... } catch(err) { // ‫ هذا ”كائن الخطأ“، ويمكننا استعمال أيّ اسم نريد بدل err // ... } لكائن الخطأ (في حالة الأخطاء المضمّنة في اللغة) خاصيتين اثنتين: name: اسم الخطأ. فمثلًا لو كان المتغير غير معرّفًا فسيكون الاسم "ReferenceError". message: الرسالة النصية بتفاصيل الخطأ. كما هناك خاصيات أخرى غير قياسية في أغلب بيئات جافاسكربت. أكثرها استعمالًا ودعمًا هي: stack: مَكدس الاستدعاء الحالي: سلسلة نصية فيها معلومات عن سلسلة الاستدعاءات المتداخلة التي تسبّبت بالخطأ. نستعمل الخاصية للتنقيح فقط. مثال: try { *!* lalala; // خطأ المتغيّر غير معرّف */!* } catch(err) { alert(err.name); // ReferenceError(خطأ في الإشارة) alert(err.message); // المتغيّر ‫lalala غير معرف alert(err.stack); // ‫ReferenceError:(خطأ في الإشارة) المتغيّر ‫lalala غير معرّف في (...call stack) //يمكننا أيضًا عرض الخطأ بالكامل //تحول الخطأ إلى سلسلة نصية هكذا ‫:"name: message" alert(err); // ‫ReferenceError:(خطأ في الإشارة) المتغيّر ‫lalala غير معرّف } إسناد "catch" الاختياري تحذير:هذه إضافة حديثة للغة من الممكن أن تحتاج لترقيع الخطأ في المتصفحات القديمة لأن هذه الميّزة جديدة. لو لم تريد تفاصيل الخطأ فيمكنك إزالتها من catch: try { // ... } catch { // <-- بدون المتغير (err) // ... } استعمال "try..catch" هيًا نرى معًا مثالًا واقعيًا عن try..catch: كما نعلم فجافاسكربت تدعم التابِع JSON.parse(str)‎ ليقرأ القيم المرمّزة بِـ JSON. عادةً ما نستعمله لفكّ ترميز البيانات المستلمة من الشبكة، كانت آتية من خادوم أو من أيّ مصدر غيره. نستلم البيانات ونستدعي JSON.parse هكذا: let json = '{"name":"John", "age": 30}'; // بيانات من الخادوم // لاحظ let user = JSON.parse(json); // نحوّل التمثيل النصي إلى كائن جافاسكربت // الآن صار ‫user كائنًا فيه خاصيات أتت من السلسلة النصية alert( user.name ); // John alert( user.age ); // 30 يمكنك أن ترى تفاصيل أكثر عن JSON في درس JSON methods, toJSON. لو كانت سلسلة json معطوبة، فسيُولّد JSON.parse خطأً و”ويموت“ السكربت. هل نقبل بالأمر الواقع المرير؟ لا وألف لا! في هذا الحال لن يعرف الزائر ما يحدث لو حدث للبيانات أمر (ما لم يفتح طرفية المطوّرين). وعادةً ما لا يحب الناس ما ”يموت“ أمامهم دون أن يُعطيهم رسالة خطأ. فلنستعمل try..catch للتعامل مع هذه المعضلة: let json = "{ bad json }"; try { let user = JSON.parse(json); // <-- لو حدث خطأ... alert( user.name ); // فلن يعمل هذا } catch (e) { // ...تنتقل عملية التنفيذ إلى هنا alert( "Our apologies, the data has errors, we'll try to request it one more time." ); // المعذرة، في البيانات أخطاء. سنحاول طلبها مرة ثانية. alert( e.name ); alert( e.message ); } استعملنا هنا الكتلة catch لعرض رسالة لا أكثر، ولكن يمكن أن نستغلّها أفضل من هذا: مثل إرسال طلب إلى الشبكة، أو اقتراح عملية بديلة على الزائر أو إرسال معلومات الخطأ إلى … تسجيل، وغيرها… هذا كلّه أفضل من الموت والموت فقط. رمي أخطائنا نحن ماذا لو كان كائن json صحيح صياغيًا إلّا أنّ الخاصية المطلوبة name ليست فيه؟ هكذا مثلًا: let json = '{ "age": 30 }'; // البيانات ناقصة try { let user = JSON.parse(json); // <-- لا أخطاء alert( user.name ); // ما من اسم! } catch (e) { alert( "doesn't execute" ); // لا يتنفّذ السطر } هنا سيعمل التابِع JSON.parse كما يجب، لكن عدم وجود الخاصية name هي المشكلة والخطأ في هذه الحالة. لتوحيد طريقة معالجة الأخطاء، سنستخدم مُعامل throw. مُعامل "الرمي" throw يُولّد لنا مُعامل الرمي throw خطأً. صياغته هي: throw <error object> يمكننا تقنيًا استعمال ما نريد ليكون كائن خطأ، مثل الأنواع الأولية كالأعداد والسلاسل النصية. ولكن من الأفضل استعمال الكائنات ومن الأفضل أكثر أن تملك خاصيتي الاسم name والرسالة message (لتكون متوافقة إلى حدٍّ ما مع الأخطاء المضمّنة). في جافاسكربت مختلف البانيات المضمّنة للأخطاء القياسية: الخطأ Error والخطأ الصياغي SyntaxError والخطأ في الإشارة ReferenceError والخطأ في النوع TypeError وغيرها. يمكننا استعمال هذه البانيات أيضًا لإنشاء كائنات الخطأ. صياغتها هي: let error = new Error(message); // أو let error = new SyntaxError(message); let error = new ReferenceError(message); // ... في الأخطاء المضمّنة في اللغة (ولا أعني الكائنات أيًا كانت، الأخطاء بعينها) تكون قيمة الخاصية name هي ذاتها اسم الباني، وتُؤخذ الرسالة message من الوسيط. مثال: let error = new Error("Things happen o_O"); // غرائب وعجائب ة_ه alert(error.name); // خطأ alert(error.message); // غرائب وعجائب ة_ه لنرى نوع الخطأ الذي يُولّده التابِع JSON.parse: try { JSON.parse("{ bad json o_O }"); // جيسون شقي ة_ه } catch(e) { alert(e.name); // SyntaxError خطأ صياغي alert(e.message); // Unexpected token o in JSON at position 2 } كما رأينا فهو خطأ صياغي SyntaxError. وفي حالتنا نحن فعدم وجود الخاصية name هي خطأ، إذ لا بدّ أن يكون للمستخدم اسمًا. هيًا نرمِ الخطأ: let json = '{ "age": 30 }'; // البيانات ناقصة try { let user = JSON.parse(json); // <-- لا أخطاء if (!user.name) { throw new SyntaxError("Incomplete data: no name"); // (*) البيانات ناقصة: ليس هنالك اسم } alert( user.name ); } catch(e) { alert( "JSON Error: " + e.message ); // خطأ ‫JSON: البيانات ناقصة: ليس هنالك اسم } يُولّد مُعامل throw (في السطر (*)) خطأً صياغيًا SyntaxError له الرسالة الممرّرة message، تمامًا كما تُولّد جافاسكربت نفسها الخطأ. بهذا يتوقّف التنفيذ داخل try وينتقل سير العملية إلى catch. الآن صارت الكتلة catch هي المكان الذي نتعامل فيه مع الأخطاء فحسب، أكانت أخطاء JSON.parse أم أخطاء أخرى. إعادة الرمي يمكننا في المثال أعلاه استعمال try..catch للتعامل مع البيانات الخاطئة. ولكن هل يمكن أن يحدث خطأ آخر غير متوقّع في كتلة try {...}‎؟ مثلًا لو كان خطأ المبرمج (لم يعرّف المتغير) أو شيئًا آخر ليس أنّ ”البيانات خاطئة“؟ مثال: let json = '{ "age": 30 }'; // البيانات ناقصة try { user = JSON.parse(json); // ‫ نسينا ”let“ قبل user // ... } catch(err) { alert("JSON Error: " + err); // خطأ ‫JSON: خطأ في الإشارة: user غير معرّف (في الواقع، ليس خطأ JSON) } كلّ شيء ممكن كما تعلم! فالمبرمجون يخطؤون. حتّى في الأدوات مفتوحة المصدر والتي يستعملها ملايين البشر لسنين تمضي، لو اكتُشفت علة فجأةً ستؤدّي إلى اختراقات مأساوية. في حالتنا هذه على try..catch التقاط أخطاء ”البيانات الخاطئة“ فقط، ولكنّ طبيعة catch هي أن تلتقط كلّ الأخطاء من try. فهنا مثلًا التقطت خطأً غير متوقع ولكنها ما زالت تصرّ على رسالة "JSON Error". هذا ليس صحيحًا ويصعّب من تنقيح الشيفرة كثيرًا. لحسن الحظ فيمكننا معرفة الخطأ الذي التقطناه، من اسمه name مثلًا: try { user = { /*...*/ }; } catch(e) { alert(e.name); // ‫”خطأ في الإشارة” محاولة الوصول لمتغيّر غير معرّف } القاعدة ليست بالمعقّدة: على catch التعامل مع الأخطاء التي تعرفها فقط، و”إعادة رمي“ ما دونها. يمكننا توضيح فكرة ”إعادة الرمي“ بهذه الخطوات: تلتقط catch كلّ الأخطاء. نحلّل كائن الخطأ err في كتلة catch(err) {...}‎. لو لم نعرف طريقة التعامل معه، رميناه throw err. في الشيفرة أسفله استعملنا إعادة الرمي لتتعامل الكتلة catch مع أخطاء الصياغة فقط SyntaxError: let json = '{ "age": 30 }'; // البيانات ناقصة try { let user = JSON.parse(json); if (!user.name) { throw new SyntaxError("Incomplete data: no name"); // البيانات ناقصة: لا اسم } blabla(); // خطأ غير متوقع alert( user.name ); } catch(e) { if (e.name == "SyntaxError") { alert( "JSON Error: " + e.message ); // خطأ JSON: كذا كذا } else { throw e; // نُعيد رميه (*) } } الخطأ المرمي في السطر (*) داخل كتلة catch ”يسقط خارج أرض“ try..catch ويمكننا إمّا التقاطه باستعمال تعبير try..catch خارجي (لو كتبناه) أو سيقتل الخطأ السكربت. هكذا لا تتعامل الكتلة catch إلا مع ما تعرف مع أخطاء وتتخطّى (إن صحّ القول) الباقي. يوضّح المثال أسفله كيفية التقاط هذه الأخطاء بمستويات أخرى من try..catch: function readData() { let json = '{ "age": 30 }'; try { // ... blabla(); // خطأ! } catch (e) { // ... if (e.name != 'SyntaxError') { throw e; // ‫نُعيد رميه (لا ندري طريقة التعامل معه) } } } try { readData(); } catch (e) { alert( "External catch got: " + e ); // التقطناه أخيرًا! } هكذا لا تعرف الدالة readData إلّا طريقة التعامل مع أخطاء الصياغة SyntaxError، بينما تتصرّف تعابير try..catch الخارجية بغيرها من أخطاء (إن عرفتها). try..catch..finally لحظة… لم ننتهي بعد. يمكن أيضًا أن يحتوي تعبير try..catch على مُنغلقة أخرى: finally. لو كتبناها فستعمل في كلّ الحالات الممكنة: بعد try لو لم تحدث أخطاء وبعد catch لو حدثت أخطاء. هكذا هي الصياغة ”الموسّعة“: try { ... نحاول تنفيذ الشيفرة ... } catch(e) { ... نتعامل مع الأخطاء ... } finally { ... ننفّذه مهما كان الحال ... } جرّب تشغيل هذه الشيفرة: try { alert( 'try' ); if (confirm('Make an error?')) BAD_CODE(); // أتريد ارتكاب خطأ؟ } catch (e) { alert( 'catch' ); // أمسكناه } finally { alert( 'finally' ); // بعد ذلك } يمكن أن تعمل الشيفرة بطريقتين اثنتين: لو كانت الإجابة على ”أتريد ارتكاب خطأ؟“ ”نعم“، فستعمل هكذا try -> catch -> finally. لو رفضت ذلك، فستعمل هكذا try -> finally. نستعمل عادةً مُنغلقة finally متى ما شرعنا في شيء وأردنا إنهائه مهما كانت نتيجة الشيفرة. فمثلًا نريد قياس الوقت الذي تأخذه دالة أعداد فيبوناتشي fib(n)‎. الطبيعي أن نبدأ القياس قبل التشغيل ونُنهيه بعده، ولكن ماذا لو حدث خطأ أثناء استدعاء الدالة؟ مثلًا خُذ شيفرة الدالة fib(n)‎ أسفله وسترى أنّها تُعيد خطأً لو كانت الأعداد سالبة أو لم تكن أعدادًا صحيحة (not integer). مُنغلقة finally هي المكان الأمثل لإنهاء هذا القياس مهما كانت النتيجة، فتضمن لنا بأنّ الوقت سيُقاس كما ينبغي في الحالتين معًا، نجح تنفيذ أم لم ينجح وأدّى لخطأ: let num = +prompt("Enter a positive integer number?", 35) let diff, result; function fib(n) { if (n < 0 || Math.trunc(n) != n) { throw new Error("Must not be negative, and also an integer."); } return n <= 1 ? n : fib(n - 1) + fib(n - 2); } let start = Date.now(); try { result = fib(num); } catch (e) { result = 0; *!* } finally { diff = Date.now() - start; } */!* alert(result || "error occurred"); alert( `execution took ${diff}ms` ); يمكنك التأكّد بتشغيل الشيفرة وإدخال 35 في prompt، وستعمل كما يجب، أي تكون finally بعد try. وبعد ذلك جرّب إدخال ‎-1 وسترى خطأً مباشرةً وسيأخذ تنفيذ الشيفرة مدّة 0ms، وكِلا الزمنين المقيسين صحيحين. أي أنّه يمكن للدالة أن بعبارة return أو throw، لا مشكلة إذ ستُنفّذ مُنغلقة finally في أيّ من الحالتين. ملاحظة:المتغيّرات في صياغة try..catch..finally محلية. لاحظ أن المتغيرات result و diff في الشيفرة البرمجية أعلاه معرّفة قبل الصياغة try..catch. وبذلك إذا عرفنا let في الصياغة try فستبقى مرئية بداخلها فقط. ملاحظة: بخصوص finally و return فإن مُنغلقة finally تعمل مع أي خروج من صياغة try..catch والتي تتضمن خروج واضح من خلال تعليمة return. في المثال أدناه هنالك تعليمة return في try في هذه الحالة ستنفذّ finally قبل عودة عنصر التحكم إلى الشيفرة البرمجية الخارجية مباشرة. function func() { try { *!* return 1; */!* } catch (e) { /* ... */ } finally { *!* alert( 'finally' ); */!* } } alert( func() ); // ‫أولًا ستعمل تعليمة alert من المنغلقة finally ومن ثمّ هذه ملاحظة: إن باني try..finally بدون منغلقة catch مفيد أيضًا إذ يمكننا استخدامه عندما لا نريد معالجة الأخطاء وإنما نريد التأكد من أن العمليات التي بدأناها انتهت فعلًا. function func() { // ‫نبدأ بشيء يحتاج لإكمال (مثل عملية القياس)‎ try { // ... } finally { // نكمله هنا حتى وإن كلّ السكربت مات } } في الشيفرة البرمجية أعلاه، هنالك خطأ دائمًا يحدث في try لأنه لا يوجد catch. ومع ذلك ستعمل finally قبل الخروج من التابع. الالتقاط العمومي تحذير: مخصص لبيئة العمل إن المعلومات في هذه الفقرة ليست جزءًا من لغة جافاسكربت لنقل مثلًا أنْ حصل خطأ فادح خارج تعبير try..catch، ومات السكربت (مثلًا كان خطأ المبرمج أو أيّ شيء آخر مريب). أهناك طريقة يمكننا التصرّف فيها في هكذا حالات؟ لربّما أردنا تسجيل الخطأ أو عرض شيء معيّن على المستخدم (عادةً لا يرى المستخدمون رسائل الأخطاء) أو غيرها. لا نجد في المواصفة أيّ شيء عن هذا، إلّا أنّ بيئات اللغة تقدّم هذه الميزة لأهميتها البالغة. فمثلًا تقدّم Node.js الحدث process.on("uncaughtException")‎. ولو كنّا نُطوّر للمتصفّح فيمكننا إسناد دالة إلى خاصية window.onerror الفريدة (إذ تعمل متى حدث خطأ لم يُلتقط). الصياغة هي: window.onerror = function(message, url, line, col, error) { // ... }; message: رسالة الخطأ. url: عنوان URL للسكربت أين حدث الخطأ. line وcol: رقم السطر والعمود أين حدث الخطأ. error: كائن الخطأ. مثال: <script> window.onerror = function(message, url, line, col, error) { alert(`${message}\n At ${line}:${col} of ${url}`); }; function readData() { badFunc(); // Whoops, something went wrong! } readData(); </script> عادةً، لا يكون الهدف من …. العمومي window.onerror استعادة تنفيذ السكربت (إذ هو أمر مستحيل لو كانت أخطاء من المبرمج) بل يكون لإرسال هذه الرسائل إلى المبرمج. كما أنّ هناك خدمات على الوِب تقدّم مزايا لتسجيل الأخطاء في مثل هذه الحالات، أمثال https://errorception.com وhttp://www.muscula.com. وهكذا تعمل: نُسجّل في الخدمة ونأخذ منهم شيفرة جافاسكربت صغيرة (أو عنوانًا للسكربت) لنضعها في صفحاتنا. يضبط هذا السكربت دالة window.onerror مخصّصة. ومتى حدث خطأ أرسلَ طلب شبكة بالخطأ إلى الخدمة. ويمكننا متى أردنا الولوج إلى واجهة الوِب للخدمة ومطالعة تلك الأخطاء. خلاصة يتيح لنا تعبير try..catch التعامل مع الأخطاء أثناء تنفيذ الشيفرات. كما يدلّ اسمها فهي تسمح ”بتجربة“ تنفيذ الشيفرة و”والتقاط“ الأخطاء التي قد تحدث فيها. صياغتها هي: try { // شغّل الشيفرة } catch(err) { // انتقل إلى هنا لو حدث خطأ // ‫err هو كائن الخطأ } finally { // مهما كان الذي حدث، نفّذ هذا } يمكننا إزالة قسم catch أو قسم finally، واستعمال الصيغ الأقصر try..catch وtry..finally. لكائنات الأخطاء الخاصيات الآتية: message: رسالة الخطأ التي نفهمها نحن البشر. name: السلسلة النصية التي فيها اسم الخطأ (اسم باني الخطأ). stack (ليست قياسية ولكنّها مدعومة دعمًا ممتازًا): المَكدس في لحظة إنشاء الخطأ. لو لم تُرد كائن الخطأ فيمكنك إزالتها باستعمال catch { بدل catch(err) {‎. يمكننا أيضًا توليد الأخطاء التي نريد باستعمال مُعامل throw. تقنيًا يمكن أن يكون وسيط throw ما تريد ولكن من الأفضل لو كان كائن خطأ يرث صنف الأخطاء Error المضمّن في اللغة. سنعرف المزيد عن توسعة الأخطاء في القسم التالي. كما يُعدّ نمط ”إعادة الرمي“ البرمجي نمطًا مهمًا عند التعامل مع الأخطاء، فلا تتوقّع كتلة catch إلّا أنواع الأخطاء التي تفهمها وتعرف طريقة التعامل معها، والباقي عليها إعادة رميه لو لم تفهمه. حتّى لو لم نستعمل تعبير try..catch فأغلب البيئات تتيح لنا إعداد … أخطاء ”عمومي“ لالتقاط الأخطاء التي ”تسقط أرضًا“. هذا … في المتصفّحات اسمه window.onerror. تمارين أخيرًا أم الشيفرة؟ الأهمية: 5 وازِن بين الشيفرتين هتين. تستعمل الأولى finally لتنفيذ الشيفرة بعد try..catch: try { عمل عمل عمل } catch (e) { نتعامل مع الأخطاء } finally { ننظّف مكان العمل // هنا } الثانية تضع عملية التنظيف بعد try..catch: try { عمل عمل عمل } catch (e) { نتعامل مع الأخطاء } ننظّف مكان العمل // ‎هنا سنحتاج بكل بتأكيد للتنظيف بعد العمل. بغض النظر إن وجدَنا خطأً ما أم لا. هل هنالك ميزة إضافية لاستخدام finally؟ أم أن الشيفرتين السابقتين متساويتين؟ إن كان هنالك ميزّة اكتب مثلًا يوضحها. الحل عندما ننظر إلى الشيفرة الموجودة بداخل التابع يصبح الفرق جليًا. يختلف السلوك إذا كان هناك "قفزة" من "try..catch". على سبيل المثال، عندما يكون هناك تعليمة return بداخل صياغة try..catch. تعمل منغلقة finally في حالة وجود أي خروج من صياغة try..catch، حتى عبر تعليمة return: مباشرة بعد الانتهاء من try..catch ، ولكن قبل أن تحصل شيفرة الاستدعاء على التحكم. function f() { try { alert('start'); return "result"; } catch (e) { /// ... } finally { alert('cleanup!'); } } f(); // cleanup! … أو عندما يكون هناك throw، مثل هنا: function f() { try { alert('start'); throw new Error("an error"); } catch (e) { // ... if("can't handle the error") { throw e; } } finally { alert('cleanup!') } } f(); // cleanup! تضمنُ finally عملية التنظيف هنا. إذا وضعنا الشيفرة البرمجية في نهاية f ، فلن تشغّل في هذا السيناريو. ترجمة -وبتصرف- للفصل Error handling, "try..catch"‎ من كتاب The JavaScript language
  24. لا يمكننا في جافاسكربت إلّا أن نرث كائنًا واحدًا فقط. وليس هناك إلّا كائن [[Prototype]] واحد لأيّ كائن. ولا يمكن للصنف توسعة أكثر من صنف واحد. وأحيانًا يكون ذلك مُقيّدًا نوعًا ما. فنقل بأنّ لدينا صنف ”كنّاسة شوارع“ StreetSweeper وصنف ”دراجة هوائية“ Bicycle، وأردنا صنع شيء يجمعهما: ”كنّاسة شوارع بدراجة هوائية“ StreetSweepingBicycle. أو ربّما لدينا صنف المستخدم User وصنف ”مُطلق الأحدث“ EventEmitter حيث فيه خاصية توليد الأحداث، ونريد إضافة ميزة EventEmitter إلى User كي يُطلق المستخدمون الأحداث أيضًا. هناك مفهوم في اللغة يساعد في حلّ هذه وهو ”المخاليط“ (Mixins). كما تقول ويكيبيديا، المخلوط هو صنف يحتوي على توابِع يمكن للأصناف الأخرى استعمالها دون أن ترث ذاك الصنف. أي بعبارة أخرى، يُقدّم المخلوط توابِع فيها وظائف وسلوك معيّن، ولكنّا لا نستعمله لوحده بل نُضيف تلك الوظيفة أو ذاك السلوك إلى الأصناف الأخرى. مثال عن المخاليط أبسط طريقة لكتابة تنفيذ مخلوط في جافاسكربت هو صناعة كائن فيه توابِع تُفيدنا في أمور معيّنة كي ندمجها بسهولة داخل كائن prototype لأيّ صنف كان. لنقل مثلًا لدينا المخلوط sayHiMixin ليُضيف // مخلوط let sayHiMixin = { sayHi() { alert(`Hello ${this.name}`); // مرحبًا يا فلان }, sayBye() { alert(`Bye ${this.name}`); // وداعًا يا فلان } }; // الاستعمال: class User { constructor(name) { this.name = name; } } // ننسخ التوابِع Object.assign(User.prototype, sayHiMixin); // الآن يمكن أن يرحّب المستخدم بغيره new User("Dude").sayHi(); // Hello Dude! مرحبًا يا Dude! ما من وراثة هنا، بل نسخ بسيط للتوابِع. هكذا يمكن أن يرث صنف المستخدم من أيّ صنف آخر وأن يُضيف هذا المخلوط ليدمج فيه توابِع أخرى، هكذا: class User extends Person { // ... } Object.assign(User.prototype, sayHiMixin); يمكن أن تستفيد المخاليط من الوراثة بينها ذاتها المخاليط. فمثلًا، نرى هنا كيف يرث sayHiMixin المخلوطَ sayMixin: let sayMixin = { say(phrase) { alert(phrase); } }; let sayHiMixin = { __proto__: sayMixin, // ‫(أو نستعمل Object.create لضبط كائن prototype هنا) sayHi() { // نستدعي التابِع الأب super.say(`Hello ${this.name}`); // (*) }, sayBye() { super.say(`Bye ${this.name}`); // (*) } }; class User { constructor(name) { this.name = name; } } // ننسخ التوابِع Object.assign(User.prototype, sayHiMixin); // الآن يمكن أن يرحّب المستخدم بالناس new User("Dude").sayHi(); // مرحبًا يا ‫ Dude! لاحظ كيف يبحث استدعاء التابِع الأب super.say()‎ في sayHiMixin (في الأسطر عند (*)) - كيف يبحث عن التابِع داخل كائن prototype المخلوطَ وليس داخل الصنف. إليك صورة (طالع الجانب الأيمن فيها): يعزو ذلك إلى أنّ التابِعين sayHi وsayBye أُنشآ في البداية داخل sayHiMixin. فحتّى حين ننسخهما تُشير خاصية [[HomeObject]] الداخلية فيهما إلى sayHiMixin كما نرى في الصورة. وطالما يبحث super عن التوابِع الأب في [[HomeObject]].[[Prototype]] يكون البحث داخل sayHiMixin.[[Prototype]]‎ وليس داخل User.[[Prototype]]‎. مخلوط الأحداث EventMixin الآن حان وقت استعمال المخاليط في الحياة العملية. يُعدّ توليد الأحداث ميزة مهمّة جدًا للكائنات بشكل عام، وكائنات المتصفّحات بشكل خاص. وتُعدّ هذه الأحداث الطريقة المُثلى ”لنشر المعلومات“ لمن يريدها. لذا هيًا نصنع مخلوطًا يُتيح لنا إضافة دوال أحداث إلى أيّ صنف أو كائن. سيقدّم المخلوط تابِعًا باسم ‎.trigger(name, [...data])‎ ”يُولّد حدثًا“ متى حدث شيء مهم. وسيط الاسم name هو… اسم الحدث، وبعده تأتي بيانات الحدث إن احتجناها. كما والتابِع .on(name, handler)‎ الذي سيُضيف دالة ”معاملة“ handler لتستمع إلى الأحداث حسب الاسم الذي مرّرناه. ستُستدعى الدالة متى تفعّل الحدث بالاسم name وسيُمرّر لها الوسطاء في استدعاء .trigger. وأيضًا… التابِع ‎.off(name, handler)‎ الذي سيُزيل المستمع handler. بعدما نُضيف المخلوط سيقدر كائن المستخدم user على توليد حدث ولوج "login" متى ولج الزائر إلى الموقع. ويمكن لكائن آخر (لنقل التقويم calendar) الاستماع إلى هذا الحدث للقيام بمهمة ما (مثلًا تحميل تقويم المستخدم الذي ولج). أو يمكن أن تُولّد القائمة menu حدثَ الاختيار "select" متى اختار المستخدم عنصرًا منها، ويمكن للكائنات الأخرى إسناد ما تحتاج من …. للاستماع إلى ذلك الحدث. وهكذا. إليك الشيفرة: let eventMixin = { /** *طريقة الانضمام إلى الحدث * menu.on('select', function(item) { ... } */ on(eventName, handler) { if (!this._eventHandlers) this._eventHandlers = {}; if (!this._eventHandlers[eventName]) { this._eventHandlers[eventName] = []; } this._eventHandlers[eventName].push(handler); }, /** * طريقة إلغاء الانضمام إلى الحدث * menu.off('select', handler) */ off(eventName, handler) { let handlers = this._eventHandlers && this._eventHandlers[eventName]; if (!handlers) return; for (let i = 0; i < handlers.length; i++) { if (handlers[i] === handler) { handlers.splice(i--, 1); } } }, /** * توليد حدث من خلال الاسم والبيانات المعطاة * this.trigger('select', data1, data2); */ trigger(eventName, ...args) { if (!this._eventHandlers || !this._eventHandlers[eventName]) { return; // no handlers for that event name } // call the handlers this._eventHandlers[eventName].forEach(handler => handler.apply(this, args)); } }; ‎.on(eventName, handler)‎ -- يضبط الدالة handler لتُشغّل متى ما حدث الحدث بهذا الاسم. تقنيًا تُخزّن خاصية ‎_eventHandlerslrm مصفوفة من دوال المعاملة لكلّ حدث، وتُضيف الدالة إلى القائمة. ‎.off(eventName, handler)‎ -- يحذف الدالّة handler من القائمة. ‎.trigger(eventName, ...args)‎ -- يولّد حدث: كلّ المعاملات تُستدعى من ‎_eventHandlers[eventName]‎ مع قائمة الوسطاء ‎...args. الاستعمال: // أنشئ صنف class Menu { choose(value) { this.trigger("select", value); } } // أضف مخلوط مع التوابع المرتبط به Object.assign(Menu.prototype, eventMixin); let menu = new Menu(); // أضف معالج حدث من أجل أن يستدعى عند الإختيار *!* menu.on("select", value => alert(`Value selected: ${value}`)); */!* // ينطلق الحدث=> يُشغّل المعالج مباشرةً ويعرُض // القيمة المختارة:123 menu.choose("123"); يمكننا الآن أن نحوّل أيّ شيفرة لتتفاعل متى ما اخترنا شيئًا من القائمة بالاستماع إلى الحدث عبر menu.on(...)‎. كما ويُسهّل المخلوط eventMixin من إضافة هذا السلوك (أو ما يشابهه) إلى أيّ صنف نريد دون أن نتدخّل في عمل سلسلة الوراثة. خلاصة مصطلح المخلوط في البرمجة كائنية التوجّه: صنف يحتوي على توابِع نستعملها في أصناف أخرى. بعض اللغات تتيح لنا الوراثة من أكثر من صنف، أمّا جافاسكربت فلا تتيح ذلك، ولكن يمكننا التحايل على الأمر وذلك بنسخ التوابع إلى النموذج الأولي prototype. يمكننا استعمال المخاليط بطريقة لتوسيع الصنف وذلك بإضافة سلوكيات متعددة إليه، مثل: معالج الأحداث كما رأينا في المثال السابق. من الممكن أن تصبح المخاليط نقطة تضارب إذا قاموا بدون قصد بإعادة تعريف توابع الأصناف. لذا عمومًا يجب التفكير مليًا بأسماء المخاليط لتقليل احتمالية حدوث ذلك. ترجمة -وبتصرف- للفصل Mixins من كتاب The JavaScript language
  25. يُتيح لنا المُعامل instanceof (أهو سيرورة من) فحص هل الكائن ينتمي إلى الصنف الفلاني؟ كما يأخذ الوراثة في الحسبان عند الفحص. توجد حالات عديدة يكون فيها هذا الفحص ضروريًا. **سنستعمله هنا لصناعة دالة **، أي دالة تغيّر تعاملها حسب نوع الوسطاء الممرّرة لها. معامل instanceof صياغته هي: obj instanceof Class يُعيد المُعامل قيمة true لو كان الكائن obj ينتمي إلى الصنف Class أو أيّ صنف يرثه. مثال: class Rabbit {} let rabbit = new Rabbit(); // ‫هل هو كائن من الصنف Rabbit؟ alert( rabbit instanceof Rabbit ); // true نعم كما يعمل مع الدوال البانية: // ‫بدل استعمال class function Rabbit() {} alert( new Rabbit() instanceof Rabbit ); // true نعم كما والأصناف المضمّنة مثل المصفوفات Array: let arr = [1, 2, 3]; alert( arr instanceof Array ); // true نعم alert( arr instanceof Object ); // true نعم لاحظ كيف أنّ الكائن arr ينتمي إلى صنف الكائنات Object أيضًا، إذ ترث المصفوفات -عبر prototype- الكائناتَ. عادةً ما يتحقّق المُعامل instanceof من سلسلة prototype عند الفحص. كما يمكننا وضع المنطق الذي نريد لكلّ صنف في التابِع الثابت Symbol.hasInstance. تعمل خوارزمية obj instanceof Class بهذه الطريقة تقريبًا: لو وجدت التابِع الثابت Symbol.hasInstance تستدعيه وينتهي الأمر: Class[Symbol.hasInstance](obj)‎. يُعيد التابِع إمّا true وإمّا false. هكذا نخصّص سلوك المُعامل instanceof. مثال: // ‫ضبط instanceOf للتحقق من الافتراض القائل // ‫بأن كل شيء يملك الخاصية canEat هو حيوان class Animal { static [Symbol.hasInstance](obj) { if (obj.canEat) return true; } } let obj = { canEat: true }; alert(obj instanceof Animal); // true // ‫تستدعى Animal[Symbol.hasInstance](obj)‎ ليس لأغلب الأصناف التابِع Symbol.hasInstance. في هذه الحالة تستعمل المنطق العادي: يفحص obj instanceOf Class لو كان كائن Class.prototype مساويًا لأحد كائنات prototype في سلسلة كائنات prototype للكائن obj. وبعبارة أخرى ، وازن بينهم واحدًا تلو الآخر: obj.__proto__ === Class.prototype? obj.__proto__.__proto__ === Class.prototype? obj.__proto__.__proto__.__proto__ === Class.prototype? ... // ‫لو كانت إجابة أيًا منها true، فتُعيد true // ‫وإلّا متى وصلت نهاية السلسلة أعادت false في المثال أعلاه نرى rabbit.__proto__ === Rabbit.prototype، بذلك تُعطينا الجواب مباشرةً. أمّا لو كنّا في حالة وراثة، فستتوقّف عملية المطابقة عند الخطوة الثانية: class Animal {} class Rabbit extends Animal {} let rabbit = new Rabbit(); alert(rabbit instanceof Animal); // ‏(هنا) نعم // rabbit.__proto__ === Rabbit.prototype // rabbit.__proto__.__proto__ === Animal.prototype (تطابق!) إليك صورة توضّح طريقة موازنة rabbit instanceof Animal مع Animal.prototype: كما وهناك أيضًا التابِع objA.isPrototypeOf(objB)‎، وهو يُعيد true لو كان الكائن objA في مكان داخل سلسلة prototype للكائن objB. يعني أنّنا نستطيع كتابة الفحص هذا obj instanceof Class هكذا: Class.prototype.isPrototypeOf(obj)‎. الأمر مضحك إذ أنّ باني الصنف Class نفسه ليس لديه أيّ كلمة عن هذا الفحص! المهم هو سلسلة prototype وكائن Class.prototype فقط. يمكن أن يؤدّي هذا إلى عواقب مثيرة متى تغيّرت خاصية prototype للكائن بعد إنشائه. طالع: function Rabbit() {} let rabbit = new Rabbit(); // غيّرنا كائن prototype Rabbit.prototype = {}; // ...لم يعد أرنبًا بعد الآن! alert( rabbit instanceof Rabbit ); // false لا التابع Object.prototype.toString للأنواع نعلم بأنّ الكائنات العادية حين تتحوّل إلى سلاسل نصية تصير [object Object]: let obj = {}; alert(obj); // [object Object] alert(obj.toString()); // كما أعلاه يعتمد هذا على طريقة توفيرهم لتنفيذ التابِع toString. ولكن هناك ميزة مخفيّة تجعل هذا التابِع أكثر فائدة بكثير ممّا هو عليه، أن نستعمله على أنّه مُعامل typeof موسّع المزايا، أو بديلًا عن التابِع toString. تبدو غريبة؟ أليس كذلك؟ لنُزل الغموض. حسب المواصفة، فيمكننا استخراج التابِع toString المضمّن من الكائن وتنفيذه في سياق أيّ قيمة أخرى نريد، وسيكون ناتجه حسب تلك القيمة. لو كان عددًا، فسيكون [object Number] لو كان قيمة منطقية، فسيكون [object Boolean] لو كان null: ‏[object Null] لو كان undefined: ‏[object Undefined] لو كانت مصفوفة: [object Array] …إلى آخره (ويمكننا تخصيص ذلك). هيًا نوضّح: // ننسخ التابِع‫ toString إلى متغير ليسهل عملنا let objectToString = Object.prototype.toString; // ما هذا النوع؟ let arr = []; alert( objectToString.call(arr) ); // [object Array] مصفوفة! استعملنا هنا call كما وضّحنا في درس ”المزخرفات والتمرير“ لتنفيذ الدالة objectToString بسياق this=arr. تفحص خوارزمية toString داخليًا قيمة this وتُعيد الناتج الموافق لها. إليك أمثلة أخرى: let s = Object.prototype.toString; alert( s.call(123) ); // [object Number] alert( s.call(null) ); // [object Null] alert( s.call(alert) ); // [object Function] Symbol.toStringTag يمكننا تخصيص سلوك التابِع toString للكائنات باستعمال خاصية الكائنات Symbol.toStringTag الفريدة. مثال: let user = { [Symbol.toStringTag]: "User" // مستخدم }; alert( {}.toString.call(user) ); // [object User] // لاحظ لأغلب الكائنات الخاصّة بالبيئات خاصية مثل هذه. إليك بعض الأمثلة من المتصفّحات مثلًا: // تابِع ‫toStringTag للكائنات والأصناف الخاصّة بالمتصفّحات: alert( window[Symbol.toStringTag]); // window alert( XMLHttpRequest.prototype[Symbol.toStringTag] ); // XMLHttpRequest alert( {}.toString.call(window) ); // [object Window] alert( {}.toString.call(new XMLHttpRequest()) ); // [object XMLHttpRequest] كما نرى فالناتج هو تمامًا ما يقوله Symbol.toStringTag (لو وُجد) بغلاف [object ...‎]. في النهاية نجد أن لدينا "نوع من المنشطات" لا تعمل فقط على الأنواع الأولية للبيانات بل وحتى الكائنات المضمنة في اللغة ويمكننا تخصيصها أيضًا. يمكننا استخدام ‎{}.toString.call بدلًا من instanceof للكائنات المضمنة في اللغة عندما نريد الحصول على نوع البيانات كسلسلة بدلًا من التحقق منها. خلاصة لنلخّص ما نعرف عن التوابِع التي تفحص الأنواع: يعمل على يُعيد typeof الأنواع الأولية سلسلة نصية ‎{}.toString الأنواع الأولية والكائنات المضمّنة والكائنات التي لها Symbol.toStringTag سلسلة نصية instanceof الكائنات true/false كما نرى فعبارة ‎{}.toString هي تقنيًا typeof ولكن ”متقدّمة أكثر“. بل إن التابع instanceof يؤدي دور مميّز عندما نتعامل مع تسلسل هرمي للأصناف ونريد التحقق من صنفٍ ما مع مراعاة الوراثة. تمارين instnaceof غريب عجيب الأهمية: 5 في الشيفرة أسفله، لماذا يُعيد instanceof القيمة true. يتّضح جليًا بأنّ B()‎ لم يُنشِئ a. function A() {} function B() {} A.prototype = B.prototype = {}; let a = new A(); // هنا alert( a instanceof B ); // true الحل أجل، غريب عجيب حقًا. ولكن كما نعرف فلا يكترث المُعامل instanceof بالدالة، بل بكائن prototype لها حيث تُطابقه مع غيره في سلسلة prototype. وهنا نجد a.__proto__ == B.prototype، بذلك يُعيد instanceof القيمة true. إذًا فحسب منطق instanceof، كائن prototype هو الذي يُعرّف النوع وليس الدالة البانية. ترجمة -وبتصرف- للفصل Class checking: "instanceof"‎ من كتاب The JavaScript language table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; }