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



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

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

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

نوع المُحتوى


التصنيفات

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

التصنيفات

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

التصنيفات

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

التصنيفات

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

التصنيفات

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

التصنيفات

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

التصنيفات

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

التصنيفات

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

أسئلة وأجوبة

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

التصنيفات

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

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

  1. أثناء البرمجة، نرى دائما مواقف حيث تريد أخذ شيء وتوسعته أكثر. فمثلًا لدينا كائن مستخدم user له خاصيات وتوابِع، وأردنا إنشاء نسخ عنه (مدراء admin وضيوف guest) لكن معدّلة قليلًا. سيكون رائعًا لو أعدنا استعمال الموجود في كائن المستخدم بدل نسخه أو إعادة كتابة توابِعه، سيكون رائعًا لو صنعنا كائنًا جديدًا فوق كائن user. الوراثة النموذجية (تدعى أيضًا الوراثة عبر كائن النموذج الأولي prototype)* هي الميزة الّتي تساعدنا في تحقيق هذا الأمر. الخاصية [[Prototype]] لكائنات جافاسكربت خاصية مخفية أخرى باسم [[Prototype]] (هذا اسمها في المواصفات القياسية للغة جافاسكربت)، وهي إمّا أن تكون null أو أن تشير إلى كائن آخر. نسمّي هذا الكائن بِـ”prototype“ (نموذج أولي). إن كائن النموذج الأولي ”سحريٌ“ إن صحّ القول، فحين نريد قراءة خاصية من كائن object ولا يجدها محرّك جافاسكربت، يأخذها تلقائيًا من كائن النموذج الأولي لذاك الكائن. يُسمّى هذا في علم البرمجة ”بالوراثة النموذجية“ (‏Prototypal inheritance)، وهناك العديد من المزايا الرائعة في اللغة وفي التقنيات البرمجية مبنية عليها. الخاصية [[Prototype]] هي خاصية داخلية ومخفية، إلّا أنّ هناك طُرق عديدة لنراها. ‎ إحداها استعمال __proto__ هكذا: let animal = { eats: true }; let rabbit = { jumps: true }; rabbit.__proto__ = animal; // هنا __proto__ هو الجالب والضابط القديم للخاصية [[Prototype]] كانت تستخدم قديمًا، ولكن في اللغة الحديثة استبدلت بالدالتين Object.getPrototypeOf/Object.setPrototypeOf وهي أيضًا تعمل عمل الجالب والضابط للنموذج الأولي (سندرس هذه الدوالّ لاحقًا في هذا الدرس). إن المتصفحات هي الوحيدة الّتي تدعم __proto__ وفقًا للمواصفات القياسية للغة، ولكن في الواقع جميع البيئات تدعمها حتى بيئات الخادم وذلك لأنها سهلة وواضحة. وهي الّتي سنستخدمها في الأمثلة. فمثلاً لو بحثنا الآن عن خاصية ما في كائن rabbit ولم تكُ موجودة، ستأخذها لغة جافاسكربت تلقائيًا من كائن animal. مثال على ذلك: let animal = { eats: true }; let rabbit = { jumps: true }; rabbit.__proto__ = animal; // (*) // الآن كلتا الخاصيتين في الأرنب: alert( rabbit.eats ); // true (**) alert( rabbit.jumps ); // true هنا نضبط (في السطر (*)) كائن animal ليكون النموذج الأولي (Prototype) للكائن rabbit. بعدها متى ما حاولت التعليمة alert قراءة الخاصية rabbit.eats (انظر (**))، ولم يجدها في كائن rabbit ستتبع لغة جافاسكربت الخاصية [[Prototype]] لمعرفة ما هو كائن النموذج الأولي لكائن rabbit، وسيجده كائن animal (البحث من أسفل إلى أعلى): يمكن أن نقول هنا بأنّ الكائن animal هو النموذج الأولي للكائن rabbit، أو كائن rabbit هو نسخة نموذجية من الكائن animal. وبهذا لو كان للكائن animal خاصيات وتوابِع كثيرة مفيدة، تصير مباشرةً موجودة عند كائن rabbit. نسمّي هذه الخاصيات بأنّها ”موروثة“. لو كان للكائن animal تابِعًا فيمكننا استدعائه في كائن rabbit: let animal = { eats: true, walk() { alert("Animal walk"); } }; let rabbit = { jumps: true, __proto__: animal }; // نأخذ ‫ walk من كائن النموذج الأولي rabbit.walk(); // Animal walk يُؤخذ التابِع تلقائيًا من كائن النموذج الأولي، هكذا: يمكن أيضًا أن تكون سلسلة الوراثة النموذجية (النموذج الأولي) أطول: let animal = { eats: true, walk() { alert("Animal walk"); } }; let rabbit = { jumps: true, __proto__: animal // (*) }; let longEar = { earLength: 10, __proto__: rabbit // (*) }; // نأخذ الدالّة ‫walk من سلسلة الوراثة النموذجية longEar.walk(); // Animal walk alert(longEar.jumps); // true (من rabbit) ولكن، هناك مُحددان للوراثة النموذجية وهما: لا يمكن أن تكون سلسلة الوراثة النموذجية دائرية (على شكل حلقة). ما إن تُسند __proto__ بطريقة دائرية فسترمي لغة جافاسكربت خطأً. يمكن أن تكون قيمة __proto__ إمّا كائنًا أو null، وتتجاهل لغة جافاسكربت الأنواع الأخرى. ومن الواضح جليًا أيضًا أي كائن سيرث كائن [[Prototype]] واحد وواحد فقط، لا يمكن للكائن وراثة كائنين. كائن النموذج الأولي للقراءة فقط لا يمكننا تعديل أو حذف خصائص أو دوالّ من كائن النموذج الأولي وإنما هو للقراءة فقط. وأيّة عمليات كتابة أو حذف تكون مباشرةً على الكائن نفسه وليس على كائن النموذج الأولي. في المثال أسفله نُسند التابِع walk إلى الكائن rabbit: let animal = { eats: true, walk() { /* لن يستعمل الكائن‫ `rabbit` هذا التابِع */ } }; let rabbit = { __proto__: animal }; rabbit.walk = function() { alert("Rabbit! Bounce-bounce!"); }; rabbit.walk(); // Rabbit! Bounce-bounce! من الآن فصاعدًا فستجد استدعاء التابع rabbit.walk()‎ سيكون من داخل كائن rabbit مباشرةً وتُنفّذه دون استعمال كائن النموذج الأولي: ولكن خاصيات الوصول استثناء للقاعدة، إذ يجري الإسناد على يد دالة الضابِط، أي أنّك بالكتابة في هذه الخاصية في الكائن الجديد ولكنّك استدعيت دالة الضابط الخاصة بكائن النموذج الأولي لإسناد هذه القيمة. لهذا السبب نرى الخاصية admin.fullName في الشيفرة أسفله تعمل كما ينبغي لها: let user = { name: "John", surname: "Smith", set fullName(value) { [this.name, this.surname] = value.split(" "); }, get fullName() { return `${this.name} ${this.surname}`; } }; let admin = { __proto__: user, isAdmin: true }; alert(admin.fullName); // John Smith (*) // عمل الضابِط! admin.fullName = "Alice Cooper"; // (**) هنا في السطر (*) نرى أن admin.fullName استدعت الجالِب داخل الكائن user، ولهذا استُدعيت الخاصية. وفي السطر (**) نرى عملية إسناد للخاصية admin.fullName ولهذا استدعيَ الضابِط داخل الكائن user. ماذا عن "this"؟ بعدما تتمعّن في المثال أعلاه، يمكن أن تتساءل ما قيمة this داخل set fullName(value)‎؟ أين كُتبت القيم الجديدة this.name و this.surname؟ داخل الكائن user أم داخل الكائن admin؟ جواب هذا السؤال المحيّر بسيط: لا تؤثّر كائنات النموذج الأولي على قيمة this. أينما كان التابِع موجودًا أكان في الكائن أو في كائن النموذج الأولي، سيكون تأثير this على الكائن الّذي قبل النقطة (الكائن المستدعى من خلاله هذه الخاصية) دائمًا وأبدًا. لهذا فالضابِط الّذي يستدعي admin.fullName=‎ يستعمل كائن admin عوضًا عن this وليس الكائن user. في الواقع فهذا أمر مهما جدًا جدًا إذ أنّ لديك ربما كائنًا ضخمًا فيه توابِع كثيرة جدًا، وهناك كائنات أخرى ترثه، وما إن تشغّل تلك الكائنات الموروثة التوابِعَ الموروثة، ستعدّل حالتها هي -أي الكائنات- وليس حالة الكائن الضخم ذاك. فمثلًا هنا، يمثّل كائن animal ”مخزّنَ توابِع“ وكائن rabbit يستغلّ هذا المخزن. فاستدعاء rabbit.sleep()‎ يضبط this.isSleeping على كائن rabbit: // للحيوان توابِع let animal = { walk() { if (!this.isSleeping) { alert(`I walk`); } }, sleep() { this.isSleeping = true; } }; let rabbit = { name: "White Rabbit", __proto__: animal }; // يعدّل rabbit.isSleeping rabbit.sleep(); alert(rabbit.isSleeping); // true alert(animal.isSleeping); // غير معرّف (لا يوجد خاصية معرفة في كائن النموذج الأولي بهذا الأسم)‫ الصورة الناتجة: لو كانت هناك كائنات أخرى (مثل الطيور bird والأفاعي snake وغيرها) ترث الكائنanimal، فسيمكنها الوصول إلى توابِع الكائن animal، إلّا أنّ قيمة this في كلّ استدعاء للتوابِع سيكون على الكائن الّذي استُدعيت منه، وستعرِفه لغة جافاسكربت أثناء الاستدعاء (أي سيكون الكائن الّذي قبل النقطة) ولن يكون animal. لذا متى كتبنا البيانات من خلال this، فستُخزّن في تلك الكائنات الّتي استدعيت عليها this. وبهذا نخلص إلى أنّ التوابِع مشتركة، ولكن حالة الكائن ليست مشتركة. حلقة for..in كما أنّ حلقة for..in تَمرُّ على الخاصيات الموروثة هي الأخرى. مثال: let animal = { eats: true }; let rabbit = { jumps: true, __proto__: animal }; // يُعيد التابع ‫Object.keys خصائص الكائن نفسه فقط alert(Object.keys(rabbit)); // jumps *!* // تدور حلقة‫ for..in على خصائص الكائن نفسه والخصائص الموروثة معًا for(let prop in rabbit) alert(prop); // jumps ثمّ eats */!* لو لم تكن هذه النتيجة ما نريد (أي نريد استثناء الخاصيات الموروثة)، فيمكن استعمال التابِع obj.hasOwnProperty(key) المضمّن في اللغة: إذ يُعيد true لو كان للكائن obj نفسه (وليس للموروث منه) خاصية بالاسم key. بهذا يمكننا ترشيح الخاصيات الموروثة (ونتعامل معها على حدة): let animal = { eats: true }; let rabbit = { jumps: true, __proto__: animal }; for(let prop in rabbit) { let isOwn = rabbit.hasOwnProperty(prop); if (isOwn) { alert(`Our: ${prop}`); // تخصّنا:‫ jumps } else { alert(`Inherited: ${prop}`); // ورثناها: ‫eats } } هنا نرى سلسلة الوراثة الآتية: يرث كائن rabbit كائنَ animal، والّذي يرثه هكذا Object.prototype (إذ أنّه كائن مجرّد {...}، وهذا السلوك المبدئي)، وبعدها يرث null: ملاحظة لطيفة في هذا السياق وهي: من أين أتى التابِع rabbit.hasOwnProperty؟ لم نعرّفه يدويًا! لو تتبّعناه في السلسلة لرأينا بأنّ كائن النموذج الأولي Object.prototype.hasOwnProperty هو من قدّم التابِع، أي بعبارة أخرى، ورث كائن rabbit هذا التابِع من كائن النموذج الأولي. ولكن لحظة… لماذا لم يظهر تابع hasOwnProperty في حلقة for..in كما ظهرت eats و jumps طالما تُظهر حلقات for..in الخاصيات الموروثة؟ الإجابة هنا بسيطة أيضًا: لإنه مُنع من قابلية العدّ (من خلال إسناده لقيمة الراية enumerable:false). في النهاية هي مِثل غيرها من الخاصيات في Object.prototype- تملك الراية enumerable:false، وحلقة for..in لا تمرّ إلّا على الخاصيات القابلة للعدّ. لهذا السبب لم نراها لا هي ولا خاصيات Object.prototype الأخرى. كلّ التوابِع الّتي تجلب المفتاح/القيمة تُهمل الخاصيات الموروثة، تقريبًا مثل تابِع Object.keys أو تابِع Object.values وما شابههم. إذ إنهم يتعاملون مع خصائص الكائن نفسه ولا يأخذون بعين الاعتبار الخصائص الموروثة خلاصة‎ لكلّ كائنات جافاسكربت خاصية [[Prototype]] مخفية قيمتها إمّا أحد الكائنات أو null. يمكننا استعمال obj.__proto__‎ للوصول إلى هذه الخاصية (وهي خاصية جالِب/ضابِطة). هناك طرق أخرى سنراها لاحقًا. الكائن الّذي تُشير إليه الخاصية [[Prototype]] يسمّى كائن النموذج الأولي. لو أردنا قراءة خاصية داخل كائن ما obj أو استدعاء تابِع، ولم تكن موجودة/يكن موجودًا، فسيحاول محرّك جافاسكربت البحث عنه/عنها في كائن النموذج الأولي. عمليات الكتابة والحذف تتطبّق مباشرة على الكائن المُستدعي ولا تستعمل كائن النموذج الأولي (إذ يعدّ أنّها خاصية بيانات وليست ضابِطًا). لو استدعينا التابِع ‎obj.method()‎‏ وأخذ المحرّك التابِع method من كائن النموذج الأولي، فلن تتغير إشارة this وسيُشير إلى obj، أي أنّ التوابِع تعمل على الكائن الحالي حتّى لو كانت التوابِع نفسها موروثة. تمرّ حلقة for..in على خاصيات الكائن والخاصيات الموروثة، بينما لا تعمل توابِع جلب المفاتيح/القيم إلّا على الكائن نفسه. تمارين العمل مع prototype الأهمية: 5 إليك شيفرة تُنشئ كائنين وتعدّلها. ما القيم الّتي ستظهر في هذه العملية؟ let animal = { jumps: null }; let rabbit = { __proto__: animal, jumps: true }; alert( rabbit.jumps ); // ? (1) delete rabbit.jumps; alert( rabbit.jumps ); // ? (2) delete animal.jumps; alert( rabbit.jumps ); // ? (3) يجب أن هنالك ثلاث إجابات. الحل ‏true، تأتي من rabbit. ‏null، تأتي من animal. ‏undefined، إذ ليس هناك خاصية بهذا الاسم بعد الآن. خوارزمية بحث الأهمية: 5 ينقسم هذا التمرين إلى قسمين. لديك الكائنات التالية: let head = { glasses: 1 }; let table = { pen: 3 }; let bed = { sheet: 1, pillow: 2 }; let pockets = { money: 2000 }; استعمل __proto__ لإسناد كائنات النموذج الأولي بحيث يكون البحث عن الخاصيات بهذه الطريقة: pockets ثمّ bed ثمّ table ثمّ head (من الأسفل إلى الأعلى على التتالي). فمثلًا، قيمة pockets.pen تكون 3 (من table)، وقيمة bed.glasses تكون 1 (من head). أجِب عن هذا السؤال: ما الأسرع، أن نجلب glasses هكذا pockets.glasses أم هكذا head.glasses؟ قِس أداء كلّ واحدة لو لزم. الحل لنُضيف خاصيات __proto__: let head = { glasses: 1 }; let table = { pen: 3, __proto__: head }; let bed = { sheet: 1, pillow: 2, __proto__: table }; let pockets = { money: 2000, __proto__: bed }; alert( pockets.pen ); // 3 alert( bed.glasses ); // 1 alert( table.money ); // undefined حين نتكلّم عن المحرّكات الحديثة، فليس هناك فرق (من ناحية الأداء) لو أخذنا الخاصية من الكائن أو من النموذج الأولي، فهي تتذكّر مكان الخاصية وتُعيد استعمالها عند طلبها ثانيةً. فمثلًا ستتذكّر التعليمة pockets.glasses بأنّها وجدت glasses في كائن head، وفي المرة التالية ستبحث هناك مباشرة. كما أنّها ذكية لتُحدّث ذاكرتها الداخلية ما إن يتغيّر شيء ما لذا فإن الأداء الأمثل في أمان. أين سيحدث التعديل؟ الأهمية: 5 لدينا الكائن rabbit يرث من الكائن animal. لو استدعينا rabbit.eat()‎ فأيّ الكائنين ستُعدل به الخاصية full، الكائن animal أم الكائن rabbit؟ let animal = { eat() { this.full = true; } }; let rabbit = { __proto__: animal }; rabbit.eat(); الحل الإجابة هي: الكائن rabbit. لأنّ قيمة this هي الكائن قبل النقطة، بذلك يُعدّل rabbit.eat()‎. عملية البحث عن الخاصيات تختلف تمامًا عن عملية تنفيذ تلك الخاصيات. نجد التابِع rabbit.eat سيُستدعى أولًا من كائن النموذج الأولي، وبعدها نُنفّذه على أنّ this=rabbit. لماذا أصابت التخمة كِلا الهامسترين؟ الأهمية: 5 لدينا هامسترين، واحد سريع speedy وآخر كسول lazy، والاثنين يرثان كائن الهامستر العمومي hamster. حين نُعطي أحدهما الطعام، نجد الآخر أُتخم أيضًا. لماذا ذلك؟ كيف نُصلح المشكلة؟ let hamster = { stomach: [], eat(food) { this.stomach.push(food); } }; let speedy = { __proto__: hamster }; let lazy = { __proto__: hamster }; // وجد هذا الهامستر الطعامَ قبل الآخر speedy.eat("apple"); alert( speedy.stomach ); // apple // هذا أيضًا وجده. لماذا؟ أصلِح الشيفرة. alert( lazy.stomach ); // apple الحل لنرى ما يحدث داخل الاستدعاء ‎speedy.eat("apple")‏ بدقّة. نجد التابِع speedy.eat في كائن النموذج الأولي الهامستر (=hamster)، وبعدها ننفّذه بقيمة this=speedy (الكائن قبل النقطة). بعدها تأتي مهمة البحث للتابِع this.stomach.push()‎ ليجد خاصية المعدة stomach ويستدعي عليها push. يبدأ البحث عن stomach في this (أي في speedy)، ولكنّه لا يجد شيئًا. بعدها يتبع سلسلة الوراثة ويجد المعدة stomach في hamster. ثمّ يستدعي push عليها ويذهب الطعام في معدة النموذج الأولي. بهذا تتشارك الهامسترات كلها معدةً واحدة! أكان lazy.stomach.push(...)‎ أم speedy.stomach.push()‎، لا نجد خاصية المعدة stomach إلّا في كائن النموذج الأولي (إذ ليست موجودة في الكائن نفسه)، بذلك ندفع البيانات الجديدة إلى كائن النموذج الأولي. لاحظ كيف أنّ هذا لا يحدث لو استعملنا طريقة الإسناد البسيط this.stomach=‎: let hamster = { stomach: [], eat(food) { // نُسند إلى this.stomach بدلًا من this.stomach.push this.stomach = [food]; } }; let speedy = { __proto__: hamster }; let lazy = { __proto__: hamster }; // وجد الهامستر السريع الطعام speedy.eat("apple"); alert( speedy.stomach ); // apple // معدة ذاك الكسول فارغة alert( lazy.stomach ); // <لا شيء> الآن يعمل كلّ شيء كما يجب، إذ لا تبحث عملية الإسناد this.stomach=‎ عن خاصية stomach، بل تكتبها مباشرةً في كائن الهامستر الّذي وجد الطعام (المستدعى قبل النقطة). ويمكننا تجنّب هذه المشكلة من الأساس بتخصيص معدة لكلّ هامستر (كما الطبيعي): let hamster = { stomach: [], eat(food) { this.stomach.push(food); } }; let speedy = { __proto__: hamster, stomach: [] // هنا }; let lazy = { __proto__: hamster, stomach: [] // هنا }; // وجد الهامستر السريع الطعام speedy.eat("apple"); alert( speedy.stomach ); // apple // معدة ذاك الكسول فارغة alert( lazy.stomach ); // <لا شيء> يكون الحلّ العام هو أن تُكتب الخاصيات كلّها الّتي تصف حالة الكائن المحدّد ذاته (مثل stomach أعلاه) - أن تُكتب في الكائن ذاته، وبهذا نتجنّب مشاكل تشارك المعدة. ترجمة -وبتصرف- للفصل Prototypal inheritance من كتاب The JavaScript language
  2. يوجد نوعين من الخاصيات. الأوّل هو خاصيات البيانات (Data Properties). نعرف جيدًا كيف نعمل مع هذا النوع إذ كلّ ما استعملناه من البداية إلى حدّ الساعة هي خاصيات بيانات. النوع الثاني هو الجديد، وهو خاصيات الوصول (Accessor Properties). هي في الأساس دوال تجلب القيم وتضبطها، ولكن في الشيفرة تظهرُ لنا وكأنها خاصيات عادية. الجالبات والضابطات خاصيات الوصول هذه هي توابِع ”جلب“ (getter) و”ضبط“ (setter). let obj = { get propName() { // ‫جالب، يُستعمَل جلب قيمة الخاصية obj.propName }, set propName(value) { // ‫ضابط يُستعمَل لضبط قيمة الخاصية obj.propName إلى value } }; يعمل الجالب متى ما طلبت قراءة الخاصية obj.propName، والضابط… متى ما أردت إسناد قيمة obj.propName = value. لاحظ مثلًا كائن user له خاصيتين: اسم name ولقب surname: let user = { name: "John", surname: "Smith" }; الآن نريد إضافة خاصية الاسم الكامل fullName، وهي "John Smith". طبعًا لا نريد نسخ المعلومات ولصقها، لذا سنُنفذها باستخدام خاصية الوصول (ِget): let user = { name: "John", surname: "Smith", // لاحظ get fullName() { return `${this.name} ${this.surname}`; } }; alert(user.fullName); // John Smith خارج الكائن لا تبدو خاصية الوصول إلا خاصية عادية، وهذا بالضبط الغرض من هذه الخاصيات، فلسنا نريد استدعاء user.fullName على أنّها دالة، بل قراءتها فحسب، ونترك الجالب يقوم بعمله خلف الكواليس. الآن ليس للخاصية fullName إلا جالبًا، لو حاولنا إسناد قيمة لها user.fullName=‎ فسنرى خطأً: let user = { get fullName() { return `...`; } }; user.fullName = "Test"; // خطأ (للخاصية جالب فقط) هيًا نُصلح الخطأ ونُضيف ضابطًا للخاصية user.fullName: let user = { name: "John", surname: "Smith", get fullName() { return `${this.name} ${this.surname}`; }, // هنا set fullName(value) { [this.name, this.surname] = value.split(" "); } }; // نضبط fullName كما النية بتمرير القيمة. user.fullName = "Alice Cooper"; alert(user.name); // Alice alert(user.surname); // Cooper وهكذا صار لدينا الخاصية ”الوهمية“ fullName. يمكننا قراءتها والكتابة عليها، ولكنها في واقع الأمر، غير موجودة. واصفات الوصول (Accessor Descriptors) واصِفات خاصيات الوصول (Accessor Properties) تختلف عن واصِفات خاصيات البيانات (Data Properties). فليس لخاصيات الوصول قيمة value أو راية writable، بل هناك دالة get ودالة set. أي يمكن لواصِف الوصول أن يملك مايلي: get -- دالة ليس لها وُسطاء تعمل متى ما قُرئت الخاصية. set -- دالة لها وسيط واحد تُستدعى متى ما ضُبطت الخاصية. enumerable -- خاصية قابلية الإحصاء وهي مشابهة لخاصيّات البيانات. configurable -- خاصية قابلية إعادة الضبط وهي مشابهة لخاصيّات البيانات. فمثلًا لننشئ خاصية الوصول fullName باستعمال التابِع defineProperty، يمكننا تمرير واصِفًا فيه دالة get ودالة set: let user = { name: "John", surname: "Smith" }; // هنا Object.defineProperty(user, 'fullName', { get() { return `${this.name} ${this.surname}`; }, set(value) { [this.name, this.surname] = value.split(" "); } }); alert(user.fullName); // John Smith for(let key in user) alert(key); // name, surname أُعيد بأنّ الخاصية إمّا تكون خاصية وصول (لها توابِع get/set) أو خاصية بيانات (لها قيمة value)، ولا تكون الاثنين معًا. فلو حاولنا تقديم get مع value في نفس الواصِف، فسنرى خطأً: // خطأ: واصِف الخاصية غير صالح Object.defineProperty({}, 'prop', { get() { return 1 }, value: 2 }); الجوالب والضوابط الذكية يمكننا استعمال الجوالب والضوابط كأغلفة للخاصيات ”الفعلية“، فتكون في يدنا السيطرة الكاملة على العمليات التي تؤثّر عليها. فمثلًا لو أردنا منع الأسماء القصيرة للاسم user فيمكن كتابة الضابِط name وترك القيمة في خاصية منفصلة باسم ‎_name: let user = { get name() { return this._name; }, set name(value) { if (value.length < 4) { alert("Name is too short, need at least 4 characters"); // الاسم قصير جدًا. أقلّ طول هو 4 محارف return; } this._name = value; } }; user.name = "Pete"; alert(user.name); // Pete user.name = ""; // الاسم قصير جدًا... هكذا نخزّن الاسم في الخاصية ‎_name والوصول يكون عبر الجالب والضابط. عمليًا يمكن للشيفرة الخارجية الوصول إلى الاسم مباشرةً باستعمال user._name، ولكن هناك مفهوم شائع بين المطوّرين هو أنّ الخاصيات التي تبدأ بشرطة سفلية "_" هي خاصيات داخلية وممنوع التعديل عليها من خارج الكائن. استعمالها لغرض التوافقية إحدى استعمالات خاصيات الوصول هذه هي إتاحة الفرصة للتحكّم بخاصية بيانات ”عادية“ متى أردنا واستبدالها بدالتي جلب وضبط وتعديل سلوكها. لنقل مثلًا بأنّا بدأنا المشروع حيث كانت كائنات المستخدمين تستعمل خاصيات البيانات: الاسم name والعمر age: function User(name, age) { this.name = name; this.age = age; } let john = new User("John", 25); alert( john.age ); // 25 ولكن الأمور لن تبقى على حالها وإنما ستتغير، عاجلًا أم آجلًا. فبدل العمر age نقول بأنّا نريد تخزين تاريخ الميلاد birthday إذ هو أكثر دقّة وسهولة في الاستعمال: function User(name, birthday) { this.name = name; this.birthday = birthday; } let john = new User("John", new Date(1992, 6, 1)); ولكن… كيف سنتعامل مع الشيفرة القديمة الّتي مازالت تستعمل خاصية age؟ يمكن أن نبحث في كلّ أمكان استخدام الخاصية age وتغييرها بخاصية جديدة مناسبة، ولكنّ ذلك يأخذ وقتًا ويمكن أن يكون صعبًا لو عدة مبرمجين يعملون على هذه الشيفرة، كما وأنّ وجود عمر المستخدم كخاصية age أمر جيّد، أليس كذلك؟ إذًا لنُبقي الخاصية كما هي، ونُضيف جالبًا للخاصية تحلّ لنا المشكلة: function User(name, birthday) { this.name = name; this.birthday = birthday; // العمر هو الفرق بين التاريخ اليوم وتاريخ الميلاد Object.defineProperty(this, "age", { get() { let todayYear = new Date().getFullYear(); return todayYear - this.birthday.getFullYear(); } }); } let john = new User("John", new Date(1992, 6, 1)); alert( john.birthday ); // تاريخ الميلاد موجود alert( john.age ); // ...وعمر المستخدم أيضًا هكذا بقيت الشيفرة القديمة تعمل كما نريد، وأضفنا الخاصية الإضافية. ترجمة -وبتصرف- للفصل Property getters and setters من كتاب The JavaScript language
  3. كما نعلم فالكائنات تُخزّن داخلها خاصيات (properties) تصفها. وحتى الآن لم تكن الخاصية إلا زوجًا من مفاتيح وقيم، ولكن خاصية الكائن يمكن أن تكون أكثر قوّة ومرونة من هذا. سنتعلم في هذا المقال بعض خصائص الضبط الأخرى، وفي الفصل الّذي يليه سنرى كيف نحوّلها إلى دوال جلب/ضبط (Setters/Getters) أيضًا. رايات الخاصيات لخصائص الكائنات (إضافةً إلى القيمة الفعلية لها) ثلاث سمات أخرى مميزة (أو ”رايات“ flags): قابلية التعديل -- لو كانت بقيمة true فيمكننا تغيير القيمة وتعديلها، ولو لم تكن فالقيمة للقراءة فقط. قابلية الإحصاء -- لو كانت بقيمة true، فستقدر الحلقات على المرور على عناصرها، وإلا فلن تقدر. قابلية إعادة الضبط -- لو كانت بقيمة true فيمكن حذف الخاصية وتعديل هذه السمات، وإلا فلا يمكن. لم نتطرّق إلى هذه الرايات قبلًا إذ لا تظهر عادةً في الشيفرات، فحين ننشئ خاصية "بالطريقة العادية" فكلّ هذه السمات بقيمة true، ولكن يمكننا طبعًا تغييرها متى أردنا. أولًا لنعرف كيف سنرى هذه الرايات. يتيح لنا التابِع Object.getOwnPropertyDescriptor الاستعلامَ عن المعلومات الكاملة الخاصة بأيّ خاصية. وهذه صياغته: let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName); obj: الكائن الّذي سنجلب معلوماته. propertyName: اسم الخاصية الّتي نريد. نسمّي القيمة المُعادة بكائن ”واصِف الخاصيات“ (Property Descriptor)، وهو يحتوي على القيمة وجميع الرايات الّتي سبق لنا شرحها. إليك مثالًا: let user = { name: "John" }; let descriptor = Object.getOwnPropertyDescriptor(user, 'name'); alert( JSON.stringify(descriptor, null, 2 ) ); /* واصف الخاصية: { "value": "John", "writable": true, "enumerable": true, "configurable": true } */ يمكننا استعمال التابِع Object.defineProperty لتغيير الرايات. إليك صياغته: Object.defineProperty(obj, propertyName, descriptor) obj و propertyName: الكائن الّذي سنطبّق عليه الواصِف، واسم الخاصية. descriptor: واصِف الخاصيات الّذي سنطبّقه على الكائن. لو كانت الخاصية موجودة فسيُحدّث التابع defineProperty راياتها. وإلّا فسيُنشئ الخاصية بهذه القيمة الممرّرة والرايات كذلك، وفي هذه الحالة لو لم يجد قيمة لأحد الرايات، فسيعدّه بقيمة false. مثلًا هنا نُنشئ الخاصية name حيث تكون راياتها كلّها بقيمة false: let user = {}; Object.defineProperty(user, "name", { value: "John" }); let descriptor = Object.getOwnPropertyDescriptor(user, 'name'); alert( JSON.stringify(descriptor, null, 2 ) ); /* { "value": "John", // لاحظ هنا "writable": false, "enumerable": false, "configurable": false } */ وازِن هذه الخاصية بتلك الّتي صنعناها أعلاه user.name (كالعادة): وأصبحت قيمة جميع الرايات false. لو لم يكن هذا ما تريده فربّما الأفضل ضبطها على true في كائن descriptor. لنرى الآن تأثيرات هذه الرايات في هذا المثال. منع قابلية التعديل لنمنع قابلية التعديل على الخاصية user.name (أي استحالة إسناد قيمة لها) وذلك بتغيير راية writable: let user = { name: "John" }; Object.defineProperty(user, "name", { writable: false // هنا }); user.name = "Pete"; // خطأ: لا يمكن إسناد القيم إلى الخاصية ‫ `name` إذ هي للقراءة فقط الآن يستحيل على أيّ شخص تعديل اسم هذا المستخدم إلّا لو طبّقوا تابِع defineProperty من طرفهم ليُلغي ما فعلناه نحن. لا تظهر الأخطاء إلّا في الوضع الصارم لا تظهر أي أخطاء عند تعديل قيمة خاصية في وضع غير الصارم إلّا أنها لن تتغير قيمتها بطبيعة الحال، وذلك لأن خطأ خرق الراية لن يظهر إلا في الوضع الصارم. إليك نفس المثال ولكن دون إنشاء الخاصية من الصفر: let user = { }; Object.defineProperty(user, "name", { value: "John", // لو كانت الخاصيات جديدة فعلينا إسناد قيمها إسنادًا صريحًا enumerable: true, configurable: true }); alert(user.name); // John user.name = "Pete"; // Error منع قابلية الإحصاء الآن لنُضيف تابِع toString مخصّص على كائن user. عادةً لا يمكننا استخدام التابع toString على الكائنات، وذلك لأنها غير قابلة للإحصاء، ولذلك فلا يمكن تمريرها على حلقة for..in. ولكن إن أردنا تغيير ذلك يدويًا (كما في المثال التالي) عندها يمكننا تمريرها إلى حلقة for..in. let user = { name: "John", toString() { return this.name; } }; // مبدئيًا، ستعرض الشيفرة الخاصيتين معًا: for (let key in user) alert(key); // name, toString لو لم نرد ذلك فيمكن ضبط enumerable:false حينها لن نستطع أن نمرر الكائن في حلقات for..in كما في السلوك المبدئي: let user = { name: "John", toString() { return this.name; } }; Object.defineProperty(user, "toString", { enumerable: false // هنا }); // الآن اختفى تابِع toString: for (let key in user) alert(key); // name كما أنّ التابِع Object.keys يستثني الخاصيات غير القابلة للإحصاء: alert(Object.keys(user)); // name منع قابلية إعادة الضبط أحيانًا ما نرى راية ”قابلية إعادة الضبط“ ممنوعة (أي configurable:false) في بعض الكائنات والخاصيات المضمّنة في اللغة. لا يمكن حذف هذه الخاصية لو كانت ممنوعة (أي configurable:false). فمثلًا المتغيّر المضمّن في اللغة Math.PI يمنع قابلية التعديل والإحصاء وإعادة الضبط عليه: let descriptor = Object.getOwnPropertyDescriptor(Math, 'PI'); alert( JSON.stringify(descriptor, null, 2 ) ); /* { "value": 3.141592653589793, "writable": false, "enumerable": false, "configurable": false } */ هكذا لا يقدر المبرمج على تغيير قيمة Math.PI ولا الكتابة عليها. Math.PI = 3; // خطأ // delete Math.PI لن تعمل أيضًا إن تفعيل خاصيّة منع قابلية إعادة الضبط هو قرار لا عودة فيه، فلا يمكننا تغيير الراية (إتاحة قابلية إعادة الضبط) باستعمال التابِع defineProperty. وللدقّة فهذا المنع يضع تقييدات أخرى على defineProperty: منع تغيير راية قابلية إعادة الضبط configurable. منع تغيير راية قابلية الإحصاء enumerable. منع تغيير راية قابلية التعديل writable: false إلى القيمة true (ولكن العكس ممكن). منع تغيير ضابط وجالب واصف الوصول get/set (ولكن يمكن إسناد قيم إليه). هنا سنحدّد الخاصية user.name لتكون ثابتة للأبد : let user = { }; Object.defineProperty(user, "name", { value: "John", writable: false, configurable: false }); Object.defineProperty(user, "name", {writable: true}); // خطأ نلاحظ عدم إمكانية تغيير الخاصيّة user.name ولا حتى راياتها ولن نستطيع تطبيق هذه العمليات عليها: الكتابة عليها user.name = "Pete"‎. حذفها delete user.name. تغيير قيمتها باستخدام التابع defineProperty هكذا: defineProperty(user, "name", { value: "Pete" })‎. ”إن منع قابلية إعادة الضبط“ ليس ”منعًا لقابلية التعديل“ إن فكرة منع قابلية إعادة الضبط هي في الحقيقة لمنع تغيير رايات هذه الخاصية أو حذفها، وليس تغيير قيمة الخاصية بحد ذاتها. ملاحظة: في المثال السابق جعلنا قابلية التعديل ممنوعة يدويًا. التابع Object.defineProperties هناك أيضًا التابِع Object.defineProperties إذ يُتيح تعريف أكثر من خاصية في وقت واحد. صياغته هي: Object.defineProperties(obj, { prop1: descriptor1, prop2: descriptor2 // ... }); مثال عليه: Object.defineProperties(user, { name: { value: "John", writable: false }, surname: { value: "Smith", writable: false }, // ... }); أي أنّنا نقدر على ضبط أكثر من خاصية معًا. التابع Object.getOwnPropertyDescriptors يمكننا استعمال التابِع Object.getOwnPropertyDescriptors(obj)‎ لجلب كلّ واصفات الخاصيات معًا. ويمكن استعماله بدمجه مع Object.defineProperties لنسخ الكائنات ”ونحن على علمٍ براياتها“: let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj)); فعادةً حين ننسخ كائنًا نستعمل الإسناد لنسخ الخاصيات، هكذا: for (let key in user) { clone[key] = user[key] } ولكن… هذا لا ينسخ معه الرايات. لذا يفضّل استعمال Object.defineProperties لو أردنا نُسخةً ”أفضل“ عن الكائن. الفرق الثاني هو أنّ حلقة for..in تتجاهل الخاصيات الرمزية (Symbolic Properties)، ولكنّ التابِع Object.getOwnPropertyDescriptors يُعيد كلّ واصِفات الخاصيات بما فيها الرمزية. إغلاق الكائنات على المستوى العام تعمل واصِفات الخاصيات على مستوى الخاصيات منفردةً. هناك أيضًا توابِع تقصر الوصول إلى الكائن كلّه: Object.preventExtensions(obj)‎ يمنع إضافة خاصيات جديدة إلى الكائن. Object.seal(obj)‎ يمنع إضافة الخاصيات وإزالتها، فهو يمنع قابلية إعادة الضبط configurable: false على كلّ الخاصيات الموجودة. Object.freeze(obj)‎ يمنع إضافة الخاصيات أو إزالتها أو تغييرها، إذ يمنع قابلية التعديل writable: false وقابلية إعادة الضبط configurable: false على كلّ الخاصيات الموجودة. كما أنّ هناك توابِع أخرى تفحص تلك المزايا: Object.isExtensible(obj)‎ يُعيد false لو كان ممنوعًا إضافة الخاصيات، وإلا true. Object.isSealed(obj)‎ يُعيد true لو كان ممنوعًا إضافة الخاصيات أو إزالتها، وكانت كلّ خاصيات الكائن الموجودة ممنوعة من قابلية إعادة الضبط configurable: false. Object.isFrozen(obj)‎ يُعيد true لو كان ممنوعًا إضافة الخاصيات أو إزالتها أو تغييرها، وكانت كلّ خاصيات الكائن الموجودة ممنوعة أيضًا من قابلية التعديل writable: false أو إعادة الضبط configurable: false. أمّا على أرض الواقع، فنادرًا ما نستعمل هذه التوابِع. ترجمة -وبتصرف- للفصل Property flags and descriptors من كتاب The JavaScript language
  4. سبق وأن مررنا على الدوال السهمية مرورًا سريعًا في مقال الدوال في الكائنات واستعمالها this ( وننصحك بالرجوع إليه وقراءته إن لم تكن قد قرأته مسبقًا) وتعرفنا على كيفية استخدامها استخدامًا أساسيًا وسنتعمق الآن فيها تعمقًا أكبر. لو ظننت الدوال السهمية هي طريقة مختصرة لكتابة الشيفرات القصيرة، فأنت مخطئ، إذ لهذه الدوال مزايا تختلف عن غيرها وتفيدنا جدًا. كثيرًا ما نواجه المواقف (في جافاسكربت بالتحديد) التي نريد أن نكتب فيها دالة صغيرة وننفّذها في مكان آخر. مثال: ‎arr.forEaoch(func)‎: تُنفّذ ‎forEach‎ الدالة ‎func‎ لكلّ عنصر في المصفوفة. ‎setTimeut(func)‎: يُنفّذ المجدول الداخلي في البيئة دالة ‎func‎. …وغيرها وغيرها. هذا هو جوهر اللغة، أن نصنع دالة في مكان ونمرّرها إلى مكان آخر. وفي هذه الدوال عادةً ما لا نريد أن نترك سياقها الحالي، وهنا تأتي الفائدة المخفية للدوال السهمية. ليس للدوال السهمية مفهوم الأنا this كما نذكر من فصل دوال الكائنات، this فليس في الدوال السهمية مفهوم ‎this‎، ولو حاولت الوصول إلى قيمة ‎this‎ فستأخذها الدالة من الخارج. فمثلًا يمكننا استعمالها للمرور على العناصر داخل تابِع للكائن: let group = { title: "Our Group", students: ["John", "Pete", "Alice"], showList() { this.students.forEach( student => alert(this.title + ': ' + student) ); } }; group.showList(); استعملنا هنا في ‎forEach‎ الدالة السهمية، وقيمة ‎this.title‎ فيها هي تمامًا القيمة التي يراها التابِع الخارجي ‎showList‎، أي ‎group.title‎. لو استعملنا هنا الدوال العادية فسنواجه خطأً: let group = { title: "Our Group", students: ["John", "Pete", "Alice"], showList() { this.students.forEach(function(student) { // ‫خطأ: تعذّرت قراءة الخاصية 'title' لغير المعرّف undefined alert(this.title + ': ' + student) }); } }; group.showList(); سبب هذا الخطأ هو أنّ التابِع ‎forEach‎ يشغّل الدوال بتمرير ‎this=undefined‎ مبدئيًا، وبذلك تحاول الشيفرة الوصول إلى ‎undefined.title‎. ليس لهذا أيّ تأثير على الدوال السهمية إذ ليس لها ‎this‎ أساسًا. لا يمكن تشغيل الدوال السهمية باستعمال ‎new‎ بطبيعة الحال فدون ‎this‎ تواجه حدًّا آخر: لا يمكنك استعمال الدوال السهمية على أنّها مُنشِئات دوال، أي لا يمكنك استدعاءها باستعمال ‎new‎. الدوال السهمية والربطات هناك فرق بسيط بين الدالة السهمية ‎=>‎ والدالة العادية التي نستدعيها باستعمال ‎.bind(this)‎: يُنشئ التابِع ‎.bind(this)‎ «نسخة مربوطة» من تلك الدالة. لا يصنع السهم ‎=>‎ أيّ نوع من الربطات. الدالة ليس فيها ‎this‎، فقط. يبحث المحرّك عن قيمة ‎this‎ كما يبحث عن أيّ قيمة متغير آخر: في البيئة المُعجمية الخارجية للدالة السهمية. ليس للدوال السهمية معاملات كما وأنّ الدوال السهمية ليس فيها متغير مُعاملات ‎arguments‎. وهذا أمر رائع حين نتعامل مع المُزخرِفات إذ نُمرّر الاستدعاء حاملًا قيمة ‎this‎ الحالية مع المُعاملات ‎arguments‎. فمثلًا هنا تأخذ ‎defer(f, ms)‎ دالةً وتُعيد غِلافًا (Wrapper) عليها تُؤجّل الاستدعاء بالمليثوان ‎ms‎ الممرّرة: function defer(f, ms) { return function() { setTimeout(() => f.apply(this, arguments), ms) }; } function sayHi(who) { alert('Hello, ' + who); } let sayHiDeferred = defer(sayHi, 2000); sayHiDeferred("John"); // ‫Hello, John بعد مرور ثانيتين يمكن كتابة نفس الشيفرة دون استعمال دالة سهمية هكذا: function defer(f, ms) { return function(...args) { let ctx = this; setTimeout(function() { return f.apply(ctx, args); }, ms); }; } هنا لزم أن نصنع المتغيرين الإضافيين ‎args‎ و ‎ctx‎ لتقدر الدالة في ‎setTimeout‎ على أخذ قيمهما. ملخص ليس للدوال السهمية: لا ‎this‎. ولا ‎arguments‎. ولا يمكن استدعائها باستعمال ‎new‎. وليس فيها ‎super‎… لم نشرح ذلك بعد ولكنّا سنفعل في الفصل «وراثة الأصناف». ليس فيها هذا كله لأنّ الغرض منها كتابة شيفرات قصيرة ليس لها سياق تعتمد عليه بل سياقًا تأخذه، وهنا حين تتألّق هذه الدوال. ترجمة -وبتصرف- للفصل Arrow functions revisited من كتاب The JavaScript language
  5. ثمّة مشكلة معروفة تواجهنا متى مرّرنا توابِع الكائنات على أنّها ردود نداء (كما نفعل مع ‎setTimeout‎)، هي ضياع هويّة الأنا ‎this‎. سنرى في هذا الفصل طرائق إصلاح هذه المشكلة. ضياع الأنا (الكلمة المفتاحية this) رأينا قبل الآن أمثلة كيف ضاعت قيمة ‎this‎. فما نلبث أن مرّرنا التابِع إلى مكان آخر منفصلًا عن كائنه، ضاع ‎this‎. إليك ظواهر هذه المشكلة باستعمال ‎setTimeout‎ مثلًا: let user = { firstName: "John", sayHi() { alert(`‎Hello, ${this.firstName}!‎`); } }; setTimeout(user.sayHi, 1000); // Hello, undefined! كما رأينا في ناتج الشيفرة، لم نرحّب بالأخ John (كما أردنا باستعمال ‎this.firstName‎)، بل بالأخ غير المعرّف ‎undefined‎! هذا لأنّ التابِع ‎setTimeout‎ استلم الدالة ‎user.sayHi‎ منفصلةً عن كائنها. يمكن أن نكتب السطر الأخير هكذا: let f = user.sayHi; setTimeout(f, 1000); // ‫ضاع سياق المستخدم user بالمناسبة فالتابِع ‎setTimeout‎ داخل المتصفّحات يختلف قليلًا، إذ يضبط ‎this=window‎ حين نستدعي الدالة (بينما في Node.js يصير ‎this‎ هو ذاته كائن المؤقّت، ولكنّ هذا ليس بالأمر المهم الآن). يعني ذلك بأنّ ‎this.firstName‎ هنا هي فعليًا ‎window.firstName‎، وهذا المتغير غير موجود. عادةً ما تصير ‎this‎ غير معرّفة ‎undefined‎ في الحالات الأخرى. كثيرًا ما نواجه هذه المسألة ونحن نكتب الشيفرة: نريد أن نمرّر تابِع الدالة إلى مكان آخر (مثل هنا، مرّرناه للمُجدول) حيث سيُستدعى من هناك. كيف لنا أن نتأكّد بأن يُستدعى في سياقه الصحيح؟ الحل رقم واحد: نستعمل دالة مغلفة أسهل الحلول هو استعمال دالة غالِفة Wrapping function: let user = { firstName: "John", sayHi() { alert(`‎Hello, ${this.firstName}!‎`); } }; setTimeout(function() { user.sayHi(); // Hello, John! }, 1000); الآن اكتملت المهمة إذ استلمنا المستخدم ‎user‎ من البيئة المُعجمية الخارجية، وثمّ استدعينا التابِع كما العادة. إليك ذات المهمة بأسطر أقل: setTimeout(() => user.sayHi(), 1000); // Hello, John! ممتازة جدًا، ولكن ستظهر لنا نقطة ضعف في بنية الشيفرة. ماذا لو حدث وتغيّرت قيمة ‎user‎ قبل أن تعمل ‎setTimeout‎؟ (لا تنسَ التأخير، ثانية كاملة!) حينها سنجد أنّا استدعينا الكائن الخطأ دون أن ندري! let user = { firstName: "John", sayHi() { alert(`‎Hello, ${this.firstName}!‎`); } }; setTimeout(() => user.sayHi(), 1000); // ‫...تغيّرت قيمة user خلال تلك الثانية user = { sayHi() { alert("Another user in setTimeout!"); } }; // setTimeout! هناك مستخدم آخر داخل التابِع‏ الحل الثاني سيضمن لنا ألّا تحدث هكذا أمور غير متوقّعة. الحل رقم اثنين: ربطة تقدّم لنا الدوال تابِعًا مضمّنًا في اللغة باسم bind يتيح لنا ضبط قيمة ‎this‎. إليك صياغته الأساسية: // ستأتي الصياغة المعقّدة لاحقًا لا تقلق let boundFunc = func.bind(context); ناتِج التابِع ‎func.bind(context)‎ هو «كائن دخيل» يشبه الدالة ويمكن لنا استدعائه على أنّه دالة، وسيمرّر هذا الاستدعاء إلى ‎func‎ بعدما يضبط ‎this=context‎ من خلف الستار. أي بعبارة أخرى، لو استدعينا ‎boundFunc‎ فكأنّما استدعينا ‎func‎ بعدما ضبطنا قيمة ‎this‎. إليك مثالًا تمرّر فيه ‎funcUser‎ الاستدعاء إلى ‎func‎ بضبط ‎this=user‎: let user = { firstName: "John" }; function func() { alert(this.firstName); } let funcUser = func.bind(user); funcUser(); // John رأينا «النسخة الرابطة» من ‎func‎، ‏‎func.bind(user)‎ بعد ضبط ‎this=user‎. كما أنّ المُعاملات كلّها تُمرّر إلى دالة ‎func‎ الأًصلية «كما هي». مثال: let user = { firstName: "John" }; function func(phrase) { alert(phrase + ', ' + this.firstName); } // ‫نربط this إلى user let funcUser = func.bind(user); funcUser("Hello"); // ‫Hello, John (مُرّر المُعامل "Hello" كما وُضبط this=user) فلنجرّب الآن مع تابع لكائن: let user = { firstName: "John", sayHi() { alert(`‎Hello, ${this.firstName}!‎`); } }; let sayHi = user.sayHi.bind(user); // (*) // يمكن أن نشغّلها دون وجود كائن sayHi(); // Hello, John! setTimeout(sayHi, 1000); // Hello, John! // ‫حتّى لو تغيّرت قيمة user خلال تلك الثانية // ‫فما زالت تستعمل sayHi القيمة التي ربطناها قبلًا user = { sayHi() { alert("Another user in setTimeout!"); } }; أخذنا في السطر ‎(*)‎ التابِع ‎user.sayHi‎ وربطناه مع المستخدم ‎user‎. ندعو الدالة ‎sayHi‎ بالدالة «المربوطة» حيث يمكن أن نستدعيها لوحدها هكذا أو نمرّرها إلى ‎setTimeout‎. مهما فعلًا فسيكون السياق صحيحًا كما نريد. نرى هنا أنّ المُعاملات مُرّرت «كما هي» وما ضبطه ‎bind‎ هو قيمة ‎this‎ فقط: let user = { firstName: "John", say(phrase) { alert(`‎${phrase}, ${this.firstName}!‎`); } }; let say = user.say.bind(user); say("Hello"); // ‫Hello, John!‎ (مُرّر المُعامل "Hello" إلى say) say("Bye"); // ‫Bye, John!‎ (مُرّر المعامل "Bye" إلى say) تابِع مفيد: ‎bindAll‎ لو كان للكائن توابِع كثيرة وأردنا تمريرها هنا وهناك بكثرة، فربّما نربطها كلّها في حلقة: for (let key in user) { if (typeof user[key] == 'function') { user[key] = user[key].bind(user); } } كما تقدّم لنا مكتبات جافاسكربت دوال للربط الجماعي لتسهيل الأمور، مثل ‎_.bindAll(obj)‎ في المكتبة lodash. الدوال الجزئية طوال هذه الفترة لم نُناقش شيئًا إلّا ربط ‎this‎. لنُضيف شيئًا آخر على الطاولة. يمكن أيضًا أن نربط المُعاملات وليس ‎this‎ فحسب. صحيح أنّا نادرًا ما نفعل ذلك إلّا أنّ الأمر مفيد في أحيان عصيبة. صياغة ‎bind‎ الكاملة: let bound = func.bind(context, [arg1], [arg2], ...); وهي تسمح لنا بربط السياق ليكون ‎this‎ والمُعاملات الأولى في الدالة. نرى مثالًا: دالة ضرب ‎mul(a, b)‎: function mul(a, b) { return a * b; } فلنستعمل ‎bind‎ لنصنع دالة «ضرب في اثنين» ‎double‎ تتّخذ تلك أساسًا لها: function mul(a, b) { return a * b; } let double = mul.bind(null, 2); alert( double(3) ); // = mul(2, 3) = 6 alert( double(4) ); // = mul(2, 4) = 8 alert( double(5) ); // = mul(2, 5) = 10 يصنع استدعاء ‎mul.bind(null, 2)‎ دالةً جديدة ‎double‎ تُمرّر الاستدعاءات إلى ‎mul‎ وتضبط ‎null‎ ليكون السياق و‎2‎ ليكون المُعامل الأول. الباقي من مُعاملات يُمرّر «كما هو». هذا ما نسمّيه باستعمال الدوال الجزئية -- أن نصنع دالة بعد ضبط بعض مُعاملات واحدة غيرها. لاحظ هنا بأنّا لا نستعمل ‎this‎ هنا أصلًا… ولكنّ التابِع ‎bind‎ يطلبه فعلينا تقديم شيء (وكان ‎null‎ مثلًا). الدالة ‎triple‎ أسفله تضرب القيمة في ثلاثة: function mul(a, b) { return a * b; } let triple = mul.bind(null, 3); alert( triple(3) ); // = mul(3, 3) = 9 alert( triple(4) ); // = mul(3, 4) = 12 alert( triple(5) ); // = mul(3, 5) = 15 ولكن لماذا نصنع الدوال الجزئية أصلًا، وعادةً؟! الفائدة هي إنشاء دالة مستقلة لها اسم سهل القراءة (‎double‎ أو ‎triple‎)، فنستعملها دون تقديم المُعامل الأول في كلّ مرة إذ ضبطنا قيمته باستعمال ‎bind‎. وهناك حالات أخرى يفيدنا الاستعمال الجزئي هذا حين نحتاج نسخة أكثر تحديدًا من دالة عامّة جدًا، ليسهُل استعمالها فقط. فمثلًا يمكن أن نصنع الدالة ‎send(from, to, text)‎. وبعدها في كائن المستخدم ‎user‎ نصنع نسخة جزئية عنها: ‎sendTo(to, text)‎ تُرسل النصّ من المستخدم الحالي. الجزئية، بدون السياق ماذا لو أردنا أن نضبط بعض المُعاملات ولكن دون السياق ‎this‎؟ مثلًا نستعملها لتابِع أحد الكائنات. تابِع ‎bind‎ الأصيل في اللغة لا يسمح بذلك، ومستحيل أن نُزيل السياق ونضع المُعاملات فقط. لكن لحسن الحظ فيمكننا صنع دالة مُساعدة ‎partial‎ تربط المُعاملات فقط. هكذا تمامًا: function partial(func, ...argsBound) { return function(...args) { // (*) return func.call(this, ...argsBound, ...args); } } // الاستعمال: let user = { firstName: "John", say(time, phrase) { alert(`‎[${time}] ${this.firstName}: ${phrase}!‎`); } }; // نُضيف تابِعًا جزئيًا بعد ضبط الوقت user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes()); user.sayNow("Hello"); // وسيظهر ما يشبه الآتي: // [10:00] John: Hello! ناتِج استدعائنا للدالة ‎partial(func[, arg1, arg2...])‎ هو غِلاف ‎(*)‎ يستدعي الدالة ‎func‎ هكذا: يترك ‎this‎ كما هو (فتكون قيمته ‎user‎ داخل الاستدعاء ‎user.sayNow‎) ثمّ يمرّر لها ‎...argsBound‎: أي المُعاملات من استدعاء ‎partial‎ ‏(‎"10:00"‎) وثمّ يمرّر لها ‎...args‎: المُعاملات الممرّرة للغِلاف (‎"Hello"‎) ساعدنا مُعامل التوزيع كثيرًا هنا، أم لا؟ كما أنّ هناك شيفرة ‎_.partial في المكتبة lodash. ملخص يُعيد التابِع ‎func.bind(context, ...args)‎ «نسخة مربوطة» من الدالة ‎func‎ بعد ضبط سياقها ‎this‎ ومُعاملاتها الأولى (في حال مرّرناها). عادةً ما نستعمل ‎bind‎ لنضبط ‎this‎ داخل تابِع لأحد الكائنات، فيمكن أن نمرّر التابِع ذلك إلى مكان آخر، مثلًا إلى ‎setTimeout‎. وحين نضبط بعضًا من مُعاملات إحدى الدوال، يكون الناتج (وهو أكثر تفصيلًا) دالةً ندعوها بالدالة الجزئية أو المطبّقة بنحوٍ جزئي partially applied. تُفيدنا هذه الدوال الجزئية حين لا نريد تكرار ذات الوسيط مرارًا وتكرارًا، مثل دالة ‎send(from, to)‎ حيث يجب أن يبقى ‎from‎ كما هو في مهمّتنا هذه، فنأخذ دالة جزئية ونتعامل بها. تمارين دالة ربط على أنها تابِع الأهمية: 5 ما ناتج هذه الشيفرة؟ function f() { alert( this ); // ؟ } let user = { g: f.bind(null) }; user.g(); الحل الجواب هو: ‎null‎. سياق دالة الربط مكتوب في الشيفرة (hard-coded) ولا يمكن تغييره لاحقًا بأيّ شكل من الأشكال. فحتّى لو شغّلنا ‎user.g()‎ فستُستدعى الدالة الأصلية بضبط ‎this=null‎. ربطة ثانية الأهمية: 5 هي يمكن أن نغيّر قيمة ‎this‎ باستعمال ربطة إضافية؟ ما ناتج هذه الشيفرة؟ function f() { alert(this.name); } f = f.bind( {name: "John"} ).bind( {name: "Ann" } ); f(); الحل الجواب هو: John. function f() { alert(this.name); } f = f.bind( {name: "John"} ).bind( {name: "Pete"} ); f(); // John لا يتذكّر كائن دالة الربط «الدخيل» (الذي يُعيده ‎f.bind(...)‎) السياق (مع الوُسطاء إن مُرّرت) - لا يتذكّر هذا كلّه إلى وقت إنشاء الكائن. أي: لا يمكن إعادة ربط الدوال. خاصية الدالة بعد الربط الأهمية: 5 تمتلك خاصية إحدى الدوال قيمة ما. هل ستتغيّر بعد ‎bind‎؟ نعم، لماذا؟ لا، لماذا؟ function sayHi() { alert( this.name ); } sayHi.test = 5; let bound = sayHi.bind({ name: "John" }); alert( bound.test ); // ما الناتج؟ لماذا؟ الحل الجواب هو: ‎undefined‎. ناتِج ‎bind‎ هو كائن آخر، وليس في هذا الكائن خاصية ‎test‎. أصلِح هذه الدالة التي يضيع this منها الأهمية: 5 على الاستدعاء ‎askPassword()‎ في الشيفرة أسفله فحص كلمة السر، ثمّ استدعاء ‎user.loginOk/loginFail‎ حسب نتيجة الفحص. ولكن أثناء التنفيذ نرى خطأً. لماذا؟ أصلِح الجزء الذي فيه ‎(*)‎ لتعمل الشيفرة كما يجب (تغيير بقية الأسطر ممنوع). function askPassword(ok, fail) { let password = prompt("Password?", ''); if (password == "rockstar") ok(); else fail(); } let user = { name: 'John', loginOk() { alert(`‎${this.name} logged in‎`); }, loginFail() { alert(`‎${this.name} failed to log in‎`); }, }; askPassword(user.loginOk, user.loginFail); // (*) الحل سبب الخطأ هو أنّ الدالة ‎ask‎ تستلم الدالتين ‎loginOk/loginFail‎ دون كائنيهما. فمتى ما استدعتهما، تُعدّ ‎this=undefined‎ بطبيعتها. علينا ربط السياق! function askPassword(ok, fail) { let password = prompt("Password?", ''); if (password == "rockstar") ok(); else fail(); } let user = { name: 'John', loginOk() { alert(`‎${this.name} logged in‎`); }, loginFail() { alert(`‎${this.name} failed to log in‎`); }, }; // (*)\maskPassword(user.loginOk.bind(user), user.loginFail.bind(user)); الآن صارت تعمل. أو، بطريقة أخرى: //... askPassword(() => user.loginOk(), () => user.loginFail()); هذه الشيفرة تعمل وعادةً ما تكون سهلة القراءة أيضًا. ولكنّها في حالات أكثر تعقيدًا تصير أقلّ موثوقية، مثل لو تغيّر المتغير ‎user‎ بعدما استُدعيت الدالة ‎askPassword‎ وقبل أن يُجيب الزائر على الاستدعاء ‎() => user.loginOk()‎. استعمال الدوال الجزئية لولوج المستخدم هذا التمرين معقّد أكثر من سابقه، بقليل. هنا تعدّل كائن ‎user‎، فصار فيه بدل الدالتين ‎loginOk/loginFail‎ دالة واحدة ‎user.login(true/false)‎. ما الأشياء التي نمرّرها إلى ‎askPassword‎ في الشيفرة أسفله فتستدعي ‎user.login(true)‎ باستعمال ‎ok‎ وتستدعي ‎user.login(false)‎ باستعمال ‎fail‎؟ function askPassword(ok, fail) { let password = prompt("Password?", ''); if (password == "rockstar") ok(); else fail(); } let user = { name: 'John', login(result) { alert( this.name + (result ? ' logged in' : ' failed to log in') ); } }; askPassword(?, ?); // ؟ (*) يجب أن تعدّل الجزء الذي عليه ‎(*)‎ فقط لا غير. الحل نستعمل دالة غالِفة… سهمية لو أردنا التفصيل: askPassword(() => user.login(true), () => user.login(false)); هكذا تأخذ ‎user‎ من المتغيرات الخارجية وتُشغّل الدوال بالطريقة العادية. أو نصنع دالة جزئية من ‎user.login‎ تستعمل ‎user‎ سياقًا لها ونضع مُعاملها الأول كما يجب: askPassword(user.login.bind(user, true), user.login.bind(user, false)); ترجمة -وبتصرف- للفصل Function binding من كتاب The JavaScript language
  6. تقدّم لنا لغة جافاسكربت مرونة عالية غير مسبوقة في التعامل مع الدوال، إذ يمكننا تمريرها أو استعمالها على أنّها كائنات. والآن سنرى كيف نمرر الاستدعاءات بينها وكيف نزخرفها. خبيئة من خلف الستار لنقل بأنّ أمامنا الدالة الثقيلة على المعالج ‎slow(x)‎ بينما نتائجها مستقرة، أي لنقل بأنّنا لو مرّرنا ذات ‎x‎، فسنجد ذات النتيجة دومًا. لو استدعينا هذه الدالة مرارًا وتكرارًا، فالأفضل لو خبّئنا (أي تذكّرنا) ناتجها لئلا يذهب الوقت سدًى لإجراء ذات الحسابات. ولكن، بدل إضافة هذه الميزة في دالة ‎slow()‎ نفسها، سنُنشئ دالة غالِفة تُضيف ميزة الخبيئة هذه. سنرى أسفله مدى فوائد هذا الأمر. إليك الشيفرة أولًا، وبعدها الشرح: function slow(x) { // هنا مهمّة ثقيلة تُهلك المعالج alert(`‎Called with ${x}‎`); return x; } function cachingDecorator(func) { let cache = new Map(); return function(x) { if (cache.has(x)) { // لو وجدنا هذا المفتاح في الخبيئة return cache.get(x); // نقرأ النتيجة منها } let result = func(x); // وإلّا نستدعي الدالة cache.set(x, result); // ثمّ نُخبّئ (نتذكّر) ناتجها return result; }; } slow = cachingDecorator(slow); alert( slow(1) ); // خبّأنا slow(1) alert( "Again: " + slow(1) ); // ذات الناتج alert( slow(2) ); // ‫خبّأنا slow(2)‎ alert( "Again: " + slow(2) ); // ذات ناتج السطر السابق في الشيفرة أعلاه، ندعو ‎cachingDecorator‎ بالمُزخرِف (decorator): وهي دالة خاصّة تأخذ دالة أخرى مُعاملًا وتعدّل على سلوكها. الفكرة هنا هي استدعاء ‎cachingDecorator‎ لأيّ دالة أردنا، وستُعيد لنا غِلاف الخبيئة ذاك. الفكرة هذه رائعة إذ يمكن أن نكون أمام مئات من الدوال التي يمكن أن تستغلّ هذه الميزة، وكلّ ما علينا فعله هو إضافة ‎cachingDecorator‎ عليها. كما وأنّا نحافظ على الشيفرة أبسط بفصل ميزة الخبيئة عن مهمّة الدالة الفعلية. ناتج ‎cachingDecorator(func)‎ هو «غِلاف» يُعيد الدالة ‎function(x)‎ التي «تُغلّف» استدعاء ‎func(x)‎ داخل شيفرة الخبيئة: الشيفرات الخارجية لا ترى أيّ تغيير على دالة ‎slow‎ المُغلّفة. ما فعلناه هو تعزيز سلوكها بميزة الخبيئة. إذًا نُلخّص: ثمّة فوائد عدّة لاستعمال ‎cachingDecorator‎ منفصلًا بدل تعديل شيفرة الدالة ‎slow‎ نفسها: إعادة استعمال ‎cachingDecorator‎، فنُضيفه على دوال أخرى. فصل شيفرة الخبيئة فلا تزيد من تعقيد دالة ‎slow‎ نفسها (هذا لو كانت معقّدة). إمكانية إضافة أكثر من مُزخرف عند الحاجة (سنرى ذلك لاحقًا). استعمال ‎func.call‎ لأخذ السياق لا ينفع مُزخرِف الخبيئة الذي شرحناه مع توابِع الكائنات. فمثلًا في الشيفرة أسفله، سيتوقّف عمل ‎worker.slow()‎ بعد هذه الزخرفة: // ‫هيًا نُضف ميزة الخبيئة إلى worker.slow let worker = { someMethod() { return 1; }, slow(x) { // أمامنا مهمّة ثقيلة على المعالج هنا alert("Called with " + x); return x * this.someMethod(); // (*) } }; // نفس الشيفرة أعلاه function cachingDecorator(func) { let cache = new Map(); return function(x) { if (cache.has(x)) { return cache.get(x); } let result = func(x); // (**) cache.set(x, result); return result; }; } alert( worker.slow(1) ); // التابِع الأصلي يعمل كما ينبغي worker.slow = cachingDecorator(worker.slow); // وقت الخبيئة alert( worker.slow(2) ); // ‫لاااا! خطأ: تعذّرت قراءة الخاصية 'someMethod' في undefined مكان الخطأ هو السطر ‎(*)‎ الذي يحاول الوصول إلى ‎this.someMethod‎ ويفشل فشلًا ذريعًا. هل تعرف السبب؟ السبب هو أنّ الغِلاف يستدعي الدالة الأصلية هكذا ‎func(x)‎ في السطر ‎(**)‎. وحين نستدعيها هكذا تستلم الدالة ‎this = undefined‎. سنرى ما يشبه هذا الخطأ لو شغّلنا هذه الشيفرة: let func = worker.slow; func(2); إذًا… يُمرّر الغِلاف الاستدعاء إلى التابِع الأصلي دون السياق ‎this‎، بهذا يحصل الخطأ. وقت الإصلاح. ثمّة تابِع دوال مضمّن في اللغة باسم func.call(context, …args)‎ يتيح لنا استدعاء الدالة صياغته هي: func.call(context, arg1, arg2, ...) يُشغّل التابِع الدالةَ ‎func‎ بعد تمرير المُعامل الأول (وهو ‎this‎) وثمّ مُعاملاتها. للتبسيط، هذين الاستدعاءين لا يفرقان بشيء في التنفيذ: func(1, 2, 3); func.call(obj, 1, 2, 3) فكلاهما يستدعي ‎func‎ بالمُعاملات ‎1‎ و‎2‎ و‎3‎. الفرق الوحيد هو أنّ ‎func.call‎ تضبط قيمة ‎this‎ على ‎obj‎ علاوةً على ذلك. لنأخذ مثالًا. في الشيفرة أسفله نستدعي ‎sayHi‎ بسياق كائنات أخرى: يُشغّل ‎sayHi.call(user)‎ الدالةَ ‎sayHi‎ ويُمرّر ‎this=user‎، ثمّ في السطر التالي يضبط ‎this=admin‎: function sayHi() { alert(this.name); } let user = { name: "John" }; let admin = { name: "Admin" }; // ‫نستعمل call لنمرّر مختلف الكائنات على أنّها this: sayHi.call( user ); // this = John sayHi.call( admin ); // this = Admin وهنا نستدعي ‎call‎ لتستدعي ‎say‎ بالسياق والعبارة المُمرّرتين: function say(phrase) { alert(this.name + ': ' + phrase); } let user = { name: "John" }; // ‫الكائن user يصير this وتصير Hello المُعامل الأول say.call( user, "Hello" ); // John: Hello في حالتنا نحن، يمكن استعمال ‎call‎ في الغِلاف ليُمرّر السياق إلى الدالة الأصلية: let worker = { someMethod() { return 1; }, slow(x) { alert("Called with " + x); return x * this.someMethod(); // (*) } }; function cachingDecorator(func) { let cache = new Map(); return function(x) { if (cache.has(x)) { return cache.get(x); } let result = func.call(this, x); // ‫هكذا نُمرّر «this» كما ينبغي cache.set(x, result); return result; }; } worker.slow = cachingDecorator(worker.slow); // والآن نُضيف الخبيئة alert( worker.slow(2) ); // يعمل alert( worker.slow(2) ); // ‫يعمل ولا يستدعي التابِع الأصلي (إذ القيمة مُخبّأة) الآن يعمل كلّ شيء كما نريد. لنوضّح الأمر أكثر، لنرى بالتفصيل المملّ تمريرات ‎this‎ من هنا إلى هناك: بعد الزخرفة، يصير ‎worker.slow‎ الغِلاف ‎function (x) { ... }‎. لذا حين نُنفّذ ‎worker.slow(2)‎، يأخذ الغِلاف القيمةَ ‎2‎ وسيطًا ويضبط ‎this=worker‎ (وهو الكائن قبل النقطة). في الغِلاف (باعتبار أنّ النتيجة لم تُخبّأ بعد)، تُمرّر ‎func.call(this, x)‎ قيمة ‎this‎ الحالية (وهي ‎worker‎) مع المُعامل الحالي (‎2‎) - كلّه إلى التابِع الأصلي. استعمال أكثر من وسيط داخل func.apply الآن صار وقت تعميم ‎cachingDecorator‎ على العالم. كنّا إلى هنا نستعملها مع الدوال التي تأخذ مُعاملًا واحدًا فقط. وماذا لو أردنا تخبئة التابِع ‎worker.slow‎ الذي يأخذ أكثر من مُعامل؟ let worker = { slow(min, max) { return min + max; // نُعدّها عملية تستنزف المعالج } }; // علينا تذكّر الاستدعاءات بنفس المُعامل هنا worker.slow = cachingDecorator(worker.slow); كنّا سابقًا نستعمل ‎cache.set(x, result)‎ (حين تعاملنا مع المُعامل الوحيد ‎x‎) لنحفظ الناتج، ونستعمل ‎cache.get(x)‎ لنجلب الناتج. أمّا الآن فعلينا تذكّر ناتج مجموعة مُعاملات ‎(min,max)‎. الخارطة ‎Map‎ لا تأخذ المفاتيح إلّا بقيمة واحدة. ثمّة أمامنا أكثر من حلّ: كتابة بنية بيانات جديدة تشبه الخرائط (أو استعمال واحدة من طرف ثالث) يمكن استعمالها لأكثر من أمر وتسمح لنا بتخزين أكثر من مفتاح. استعمال الخرائط المتداخلة: تصير ‎cache.set(min)‎ خارطة تُخزّن الزوجين ‎(max, result)‎. ويمكن أن نأخذ الناتج ‎result‎ باستعمال ‎cache.get(min).get(max)‎. دمج القيمتين في واحدة. في حالتنا هذه يمكن استعمال السلسلة النصية ‎"min,max"‎ لتكون مفتاح ‎Map‎. ويمكن أن نقدّم للمُزخرِف دالة عنونة Hashing يمكنها صناعة قيمة من أكثر من قيمة، فيصير الأمر أسهل. أغلب التطبيقات العملية تَعدّ الحل الثالث كافيًا، ولهذا سنستعمله هنا. علينا أيضًا استبدال التابِع ‎func.call(this, x)‎ بالتابِع ‎func.call(this, ...arguments)‎ كي نُمرّر كلّ المُعاملات إلى استدعاء الدالة المُغلّفة لا الأولى فقط. رحّب بالمُزخرف ‎cachingDecorator‎ الجديد، أكثر قوة وأناقة: let worker = { slow(min, max) { alert(`‎Called with ${min},${max}‎`); return min + max; } }; function cachingDecorator(func, hash) { let cache = new Map(); return function() { let key = hash(arguments); // (*) if (cache.has(key)) { return cache.get(key); } let result = func.call(this, ...arguments); // (**) cache.set(key, result); return result; }; } function hash(args) { return args[0] + ',' + args[1]; } worker.slow = cachingDecorator(worker.slow, hash); alert( worker.slow(3, 5) ); // يعمل alert( "Again " + worker.slow(3, 5) ); // ‫نفس الناتج (خبّأناه) الآن صار يعمل مهما كان عدد المُعاملات (ولكن علينا تعديل دالة العنونة لتسمح هي أيضًا بالمُعاملات أيًا كان عددها. سنشره أسفله إحدى الطرائق الجميلة لإنجاز هذه المهمة). أمامنا تعديلان اثنان: في السطر‎(*)‎، نستدعي ‎hash‎ لتصنع مفتاحًا واحدًا من ‎arguments‎. نستعمل هنا دالة «دمج» بسيطة تحوّل المُعاملان ‎(3, 5)‎ إلى المفتاح ‎"3,5"‎. لو كانت الحالة لديك أكثر تعقيدًا، فتحتاج إلى دوال عنونة أخرى. ثمّ يستعمل ‎(**)‎ التابِع ‎func.call(this, ...arguments)‎ لتمرير السياق وكلّ المُعاملات التي استلمها الغِلاف (وليس الأول فقط) - كله إلى الدالة الأصلية. يمكننا بدل استعمال ‎func.call(this, ...arguments)‎ استغلال ‎func.apply(this, arguments)‎. صياغة هذا التابِع المبني في اللغة func.apply هي: func.apply(context, args) يُشغّل التابِع الدالةَ ‎func‎ بضبط ‎this=context‎ واستعمال الكائن الشبيه بالمصفوفات ‎args‎ قائمةً بالمُعطيات للدالة. الفارق الوحيد بين ‎call‎ و‎apply‎ هي أنّ الأوّل يتوقّع قائمة بالمُعطيات بينما الثاني يأخذ كائنًا شبيهًا بالمصفوفات يحويها. أي أنّ الاستدعاءين الآتين متساويين تقريبًا: func.call(context, ...args); // نمرّر الكائن قائمةً بمُعامل التوزيع func.apply(context, args); // ‫نفس الفكرة باستعمال apply ولكن هناك فرق بسيط واحد: يُتيح لنا مُعامل التوزيع ‎...‎ تمرير المُتعدَّد ‎args‎ قائمةً إلى ‎call‎. لا يقبل ‎apply‎ إلّا مُعامل ‎args‎ شبيه بالمصفوفات. أي أنّ هذين الاستدعاءين يُكمّلان بعضهما البعض. لو توقّعنا وصول مُتعدَّد فنستعمل ‎call‎، ولو توقّعنا شبيهًا بالمصفوفات نستعمل ‎apply‎. أمّا الكائنات المُتعدَّدة والشبيهة بالمصفوفات (مثل المصفوفات الحقيقية)، فيمكننا نظريًا استعمال أيّ من الاثنين، إلّا أنّ ‎apply‎ سيكون أسرع غالبًا إذ أنّ مُعظم محرّكات جافاسكربت تحسّن أدائه داخليًا أكثر من ‎call‎. يُدى تمرير كافة المُعاملات (مع السياق) من دالة إلى أخرى بتمرير الاستدعاء. إليك أبسط صوره: let wrapper = function() { return func.apply(this, arguments); }; حين تستدعي أيّ شيفرة خارجية ‎wrapper‎ محال أن تفرّق بين استدعائها واستدعاء الدالة الأصلية ‎func‎. استعارة التوابِع أمّا الآن لنحسّن دالة العنونة قليلًا: function hash(args) { return args[0] + ',' + args[1]; } لا تعمل الدالة حاليًا إلّا على مُعاملين اثنين، وسيكون رائعًا لو أمكن أن ندمج أيّ عدد من ‎args‎. أوّل حلّ نفكّر به هو استعمال التابِع arr.join: function hash(args) { return args.join(); } ولكن… للأسف فهذا لن ينفع، إذ نستدعي ‎hash(arguments)‎ بتمرير كائن المُعاملات ‎arguments‎ المُتعدَّد والشبيه بالمصفوفات… إلّا أنّه ليس بمصفوفة حقيقية. بذلك استدعاء ‎join‎ سيفشل كما نرى أسفله: function hash() { alert( arguments.join() ); // ‫خطأ: arguments.join ليست بدالة } hash(1, 2); مع ذلك فما زال هناك طريقة سهلة لضمّ عناصر المصفوفة: function hash() { alert( [].join.call(arguments) ); // 1,2 } hash(1, 2); ندعو هذه الخدعة باستعارة التوابِع. فيها نأخذ (أي نستعير) تابِع الضمّ من المصفوفات العادية (‎[].join‎) ونستعمل ‎[].join.call‎ لتشغيله داخل سياق ‎arguments‎. ولكن، لمَ تعمل أصلًا؟ هذا بسبب بساطة الخوارزمية الداخلية للتابِع الأصيل ‎arr.join(glue)‎ في اللغة. أقتبس -بتصرّف خفيف جدًا- من مواصفات اللغة: لمّا أنّ ‎glue‎ هو المُعامل الأول، ولو لم تكن هناك مُعاملات فهو ‎","‎. لمّا أنّ ‎result‎ هي سلسلة نصية فارغة. أضِف ‎this[0]‎ إلى نهاية ‎result‎. أضِف ‎glue‎ و‎this[1]‎. أضِف ‎glue‎ و‎this[2]‎. …كرّر حتّى يتنهي ضمّ العناصر الـ ‎this.length‎. أعِد ‎result‎. إذًا فهو يأخذ ‎this‎ ويضمّ ‎this[0]‎ ثمّ ‎this[1]‎ وهكذا معًا. كتب المطوّرون التابِع بهذه الطريقة عمدًا ليسمح أن تكون ‎this‎ أيّ شبيه بالمصفوفات (ليست مصادفة إذ تتبع كثير من التوابِع هذه الممارسة). لهذا يعمل التابِع حين يكون ‎this=arguments‎. المزخرفات decorators‌ وخاصيات الدوال استبدال الدوال أو التوابِع بأخرى مُزخرفة هو أمر آمن عادةً، ولكن باستثناء صغير: لو احتوت الدالة الأًلية على خاصيات (مثل ‎func.calledCount‎) فلن تقدّمها الدالة المُزخرفة، إذ أنّها غِلاف على الدالة الأصلية. علينا بذلك أن نحذر في هذه الحالة. نأخذ المثال أعلاه مثالًا، لو احتوت الدالة ‎slow‎ أيّ خاصيات فلن يحتوي الغِلاف ‎cachingDecorator(slow)‎ عليها. يمكن أن تقدّم لنا بعض المُزخرِفات خاصيات خاصة بها. فمثلًا يمكن أن يعدّ المُزخرِف كم مرّة عملت الدالة وكم من وقت أخذ ذلك، وتقدّم لنا خاصيات لنرى هذه لمعلومات. توجد طريقة لإنشاء مُزخرِفات تحتفظ بميزة الوصول إلى خاصيات الدوال، ولكنّها تطلب استعمال الكائن الوسيط ‎Proxy‎ لتغليف الدوال. سنشرح هذا الكائن لاحقًا في قسم تغليف الدوال: ‎apply‎. ملخص تُعدّ المزخرفات أغلفة حول الدوال فتعدّل سلوكها، بينما المهمة الأساس مرهونة بالدالة نفسها. يمكن عدّ المُزخرِفات «مزايا» نُضيفها على الدالة، فنُضيف واحدة أو أكثر، ودون تغيير أيّ سطر في الشيفرة! رأينا التوابِع الآتية لنعرف كيفية إعداد المُزخرِف ‎cachingDecorator‎: func.call(context, arg1, arg2…)‎ -- يستدعي ‎func‎ حسب السياق والمُعاملات الممرّرة. func.apply(context, args)‎ -- يستدعي ‎func‎ حيث يُمرّر ‎context‎ بصفته ‎this‎ والكائن الشبيه بالمصفوفات ‎args‎ في قائمة المُعاملات. عادةً ما نكتب تمرير الاستدعاءات باستعمال ‎apply‎: let wrapper = function() { return original.apply(this, arguments); }; كما رأينا مثالًا عن استعارة التوابِع حيث أخذنا تابِعًا من كائن واستدعيناه ‎call‎ في سياق كائن آخر غيره. يشيع بين المطوّرين أخذ توابِع المصفوفات وتطبيقها على المُعاملات ‎arguments‎. لو أردت بديلًا لذلك فاستعمل كائن المُعاملات البقية إذ هو مصفوفة حقيقية. ستجد في رحلتك المحفوفة بالمخاطر مُزخرِفات عديدة. حاوِل التمرّس عليها بحلّ تمارين هذا الفصل. تمارين مزخرف تجسس الأهمية: 5 أنشِئ المُزخرِف ‎spy(func)‎ ليُعيد غِلافًا يحفظ كلّ استدعاءات تلك الدالة في خاصية ‎calls‎ داخله. احفظ كلّ استدعاء على أنّه مصفوفة من الوُسطاء. مثال: function work(a, b) { alert( a + b ); // ‫ليست work إلّا دالة أو تابِعًا لسنا نعرف أصله } work = spy(work); // (*) work(1, 2); // 3 work(4, 5); // 9 for (let args of work.calls) { alert( 'call:' + args.join() ); // "call:1,2", "call:4,5" } ملاحظة: نستفيد من هذا المُزخرِف أحيانًا لاختبار الوحدات. يمكن عدّ ‎sinon.spy‎ في المكتبة Sinon.JS صورةً متقدّمةً عنه. الحل سيُخزّن الغِلاف الذي أعادته spy(f)‎ كلّ الوُسطاء، بعدها يستعمل f.apply لتمرير الاستدعاء. ….. مزخرف تأخير الأهمية: 5 أنشِئ المُزخرف ‎delay(f, ms)‎ ليُؤخّر كلّ استدعاء من ‎f‎ بمقدار ‎ms‎ مليثانية. مثال: function f(x) { alert(x); } // أنشِئ الغِلافات let f1000 = delay(f, 1000); let f1500 = delay(f, 1500); f1000("test"); // ‫يعرض «test» بعد 1000 مليثانية f1500("test"); // ‫يعرض «test» بعد 1500 مليثانية أي أنّ المُزخرِف ‎delay(f, ms)‎ يُعيد نسخة عن ‎f‎ «تأجّلت ‎ms‎». الدالة ‎f‎ في الشيفرة أعلاه تقبل وسيطًا واحدًا، ولكن على الحل الذي ستكتبه تمرير كلّ الوُسطاء والسياق ‎this‎ كذلك. الحل function delay(f, ms) { return function() { setTimeout(() => f.apply(this, arguments), ms); }; } let f1000 = delay(alert, 1000); f1000("test"); // ‫يعرض test بعد 1000 مليثانية لاحظ بأنّا استعملنا الدالة السهمية هنا. كما نعلم فالدوال السهمية لا تملك لا ‎this‎ ولا ‎arguments‎، لذا يأخذ ‎f.apply(this, arguments)‎ كِلا ‎this‎ و‎arguments‎ من الغِلاف. لو مرّرنا دالة عادية فسيستدعيها ‎setTimeout‎ بدون المُعاملات ويضبط ‎this=window‎ (باعتبار أنّا في بيئة المتصفّح). مع ذلك يمكننا تمرير قيمة ‎this‎ الصحيحة باستعمال متغيّر وسيط ولكنّ ذلك سيكون تعبًا لا داعٍ له: function delay(f, ms) { return function(...args) { let savedThis = this; // خزّنه في متغير وسيط setTimeout(function() { f.apply(savedThis, args); // استعمل الوسيط هنا }, ms); }; } مزخرف إزالة ارتداد اصنع المُزخرِف ‎debounce(f, ms)‎ ليُعيد غِلافًا يُمرّر الاستدعاء إلى ‎f‎ مرّة واحدة كلّ ‎ms‎ مليثانية. بعبارة أخرى: حين ندعو الدالة «بأنّ ارتدادها أُزيل» Debounce فهي تضمن لنا بأنّ الاستدعاءات التي ستحدث في أقلّ من ‎ms‎ مليثانية بعد الاستدعاء السابق - ستُهمل. مثال: let f = debounce(alert, 1000); f(1); // يعمل مباشرةً f(2); // يُهمل setTimeout( () => f(3), 100); // ‫يُهمل ( لم تمرّ إلّا 100 ملي ثانية ) setTimeout( () => f(4), 1100); // يعمل setTimeout( () => f(5), 1500); // ‫يُهمل (لم تمرّ الـ 1000 مليثانية من آخر تشغيل) عمليًا في الشيفرات، نستعمل ‎debounce‎ للدوال التي تستلم أو تُحدّث شيئًا ما نعرف مسبقًا بأنّ لا شيء جديد سيحدث له في هذه الفترة القصيرة، فالأفضل أن نُهمله ولا نُهدر الموارد. الحل function debounce(f, ms) { let isCooldown = false; return function() { if (isCooldown) return; f.apply(this, arguments); isCooldown = true; setTimeout(() => isCooldown = false, ms); }; } استدعاء ‎debounce‎ يُعيد غِلافًا. ثمّة حالتين اثنتين لهذا الغِلاف: ‎isCooldown = false‎ -- يمكن أن تعمل الدالة. ‎isCooldown = true‎ -- ننتظر انتهاء المهلة. في أوّل استدعاء يكون ‎isCooldown‎ بقيمة ‎false‎ فيعمل الاستدعاء، وتتغيّر الحالة إلى ‎true‎. نُهمل أيّ استدعاء آخر طالما ‎isCooldown‎ صحيحة. بعدها يعكس ‎setTimeout‎ الحالة إلى ‎false‎ بعد مرور فترة التأجيل. مزخرف خنق الأهمية: 5 أنشِئ مُزخرِف «الخنق/throttle» ‏‎throttle(f, ms)‎ ليُعيد غِلافًا يُمرّر الاستدعاء إلى ‎f‎ مرّة كلّ ‎ms‎ مليثانية. والاستدعاءات التي تحدث في فترة «الراحة» تُهمل. الفرق بين هذه وبين ‎debounce‎ هي أنّه لو كان الاستدعاء المُهمل هو آخر الاستدعاءات أثناء فترة الراحة، فسيعمل متى انتهت تلك الفترة. لنطالع هذا التطبيق من الحياة العملية لنعرف أهمية هذا الشيء الغريب العجيب وما أساسه أصلًا. لنقل مثلًا أنّا نريد تعقّب تحرّك الفأرة. يمكن أن نضبط دالة (في المتصفّح) لتعمل كلّما تحرّكت الفأرة وتأخذ مكان المؤشّر أثناء هذه الحركة. لو كنت تستعمل الفأرة فعادةً ما تعمل الدالة هذه بسرعة (ربما تكون 100 مرّة في الثانية، أي كلّ 10 مليثوان). نريد تحديث بعض المعلومات في صفحة الوِب أثناء حركة المؤشّر. …ولكن تحديث الدالة ‎update()‎ عملية ثقيلة ولا تنفع لكلّ حركة فأرة صغيرة. كما وليس منطقيًا أصلًا التحديث أكثر من مرّة كلّ 100 مليثانية. لذا نُغلّف الدالة في مُزخرف: نستعمل ‎throttle(update, 100)‎ على أنّها دالة التشغيل كلّما تحرّكت الفأرة بدلًا من الدالة ‎update()‎ الأصلية. سيُستدعى المُزخرِف كثيرًا صحيح، ولكنّها لن يمرّر الاستدعاءات هذه إلى ‎update()‎ إلّا مرّة كلّ 100 مليثانية. هكذا سيظهر للمستخدم: في أوّل تحريك للفأرة، تُمرّر نسختنا المُزخرفة من الدالة الاستدعاء مباشرةً إلى ‎update‎، وهذا مهمّ إذ يرى المستخدم كيف تفاعلت الصفحة مباشرةً مع تحريكه الفأرة. ثمّ يُحرّك المستخدم الفأرة أكثر، ولا يحدث شيء طالما لم تمرّ ‎100ms‎. نسختنا المُزخرفة الرائعة تُهمل تلك الاستدعاءات. بعد نهاية ‎100ms‎ يعمل آخر استدعاء ‎update‎ حاملًا الإحداثيات الأخيرة. وأخيرًا تتوقّف الفأرة عن الحراك. تنتظر الدالة المُزخرفة حتى تمضي ‎100ms‎ وثمّ تشغّل ‎update‎ حاملةً آخر الإحداثيات. وهكذا نُعالج آخر حركة للفأرة، وهذا مهم مثال عن الشيفرة: function f(a) { console.log(a); } // تمرّر f1000 الاستدعاءات إلى f مرّة كلّ 1000 مليثانية كحدّ أقصى let f1000 = throttle(f, 1000); f1000(1); // تعرض 1 f1000(2); // (مخنوقة، لم تمض 1000 مليثانية بعد) f1000(3); // (مخنوقة، لم تمض 1000 مليثانية بعد) // ‫حين تمضي 1000 مليثانية... // ‫...تطبع 3، إذ القيمة 2 الوسطية أُهملت ملاحظة: يجب تمرير المُعاملات والسياق ‎this‎ المُمرّرة إلى ‎f1000‎- تمريرها إلى ‎f‎ الأصلية. الحل function throttle(func, ms) { let isThrottled = false, savedArgs, savedThis; function wrapper() { if (isThrottled) { // (2) savedArgs = arguments; savedThis = this; return; } func.apply(this, arguments); // (1) isThrottled = true; setTimeout(function() { isThrottled = false; // (3) if (savedArgs) { wrapper.apply(savedThis, savedArgs); savedArgs = savedThis = null; } }, ms); } return wrapper; } يُعيد استدعاء ‎throttle(func, ms)‎ الغِلاف ‎wrapper‎. أثناء الاستدعاء الأول، يُشغّل ‎wrapper‎ ببساطة الدالة ‎func‎ ويضبط حالة الراحة (‎isThrottled = true‎). في هذه الحالة نحفظ كلّ الاستدعاءات في ‎savedArgs/savedThis‎. لاحظ بأنّ السياق والوُسطاء مهمّان ويجب حفظهما كلاهما، فنحتاجهما معًا لنُعيد ذلك الاستدعاء كما كان ونستدعيه حقًا. بعد مرور ‎ms‎ مليثانية، يعمل ‎setTimeout‎، بهذا تُزال حالة الراحة (‎isThrottled = false‎) ولو كانت هناك استدعاءات مُهملة، نُنفّذ ‎wrapper‎ بآخر ما حفظنا من وُسطاء وسياق. لا نشغّل في الخطوة الثالثة ‎func‎ بل ‎wrapper‎ إذ نريد تنفيذ ‎func‎ إضافةً إلى دخول حالة الراحة ثانيةً وضبط المؤقّت لتصفيرها. ترجمة -وبتصرف- للفصل Decorators and forwarding, call/apply من كتاب The JavaScript language
  7. وأنت تكتب الشيفرة، ستقول في نفسك «أريد تشغيل هذه الدالة بعد قليل وليس الآن الآن. هذا ما نسمّيه "بجدولة الاستدعاءات" (scheduling a call). إليك تابِعين اثنين لهذه الجدولة: يتيح لك ‎setTimeout‎ تشغيل الدالة مرّة واحدة بعد فترة من الزمن. يتيح لك ‎setInterval‎ تشغيل الدالة تكراريًا يبدأ ذلك بعد فترة من الزمن ويتكرّر كلّ فترة حسب تلك الفترة التي حدّدتها. صحيح أنّ هذين التابِعين ليسا في مواصفة لغة جافاسكربت إلّا أنّ أغلب البيئات فيها مُجدوِل داخلي يقدّمهما لنا. وللدقّة، فكلّ المتصّفحات كما وNode.js تدعمهما. تابع تحديد المهلة setTimeout الصياغة: let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...) المُعاملات: func|code: ما يجب تنفيذه أكان دالة أو سلسلة نصية فيها شيفرة. عادةً هي دالة ولكن كعادة الأسباب التاريخية (أيضًا) يمكن تمرير سلسلة نصية فيها شيفرة، ولكنّ ذلك ليس بالأمر المستحسن. delay: التأخير قبل بدء التنفيذ بالمليثانية (1000 مليثانية = ثانية واحدة). مبدئيًا يساوي 0. arg1‎, ‎arg2…: وُسطاء الدالة (ليست مدعومة في IE9-‎) إليك هذه الشيفرة التي تستدعي ‎sayHi()‎ بعد ثانيةً واحدة: function sayHi() { alert('Hello'); } setTimeout(sayHi, 1000); مع المُعاملات: function sayHi(phrase, who) { alert( phrase + ', ' + who ); } setTimeout(sayHi, 1000, "Hello", "John"); // Hello, John لو كان المُعامل الأول سلسلة نصية فستصنع جافاسكربت دالة منها. أي أنّ هذا سيعمل: setTimeout("alert('Hello')", 1000); ولكن استعمال السلاسل النصية غير مستحسن. استعمل الدوال السهمية بدلًا عنها: setTimeout(() => alert('Hello'), 1000); مرّر الدالة لكن لا تشغّلها يُخطئ المبرمجون المبتدئون أحيانًا فيُضيفون أقواس ‎()‎ بعد الدالة: // هذا خطأ! setTimeout(sayHi(), 1000); لن يعمل ذلك إذ يتوقّع ‎setTimeout‎ إشارة إلى الدالة، بينما هنا ‎sayHi()‎ يشغّل الدالة وناتج التنفيذ هو الذي يُمرّر إلى ‎setTimeout‎. في حالتنا ناتج ‎sayHi()‎ ليس معرّفًا ‎undefined‎ (إذ لا تُعيد الدالة شيئًا)، ويعني ذلك أنّ عملنا ذهب سدًى ولم نُجدول أي شيء. الإلغاء باستعمال clearTimeout نستلمُ حين نستدعي ‎setTimeout‎ «هويّةَ المؤقّت» ‎timerId‎ ويمكن استعمالها لإلغاء عملية التنفيذ. صياغة الإلغاء: let timerId = setTimeout(...); clearTimeout(timerId); في الشيفرة أسفله نُجدول الدالة ثمّ نُلغيها (غيّرنا الخطّة العبقرية)، بهذا لا يحدث شيء: let timerId = setTimeout(() => alert("never happens"), 1000); alert(timerId); // هويّة المؤقّت clearTimeout(timerId); alert(timerId); // ‫ذات الهويّة (لا تصير null بعد الإلغاء) يمكن أن نرى من ناتج التابِع ‎alert‎ أنّ هويّة المؤقّت (في المتصفّحات) هي عدد. يمكن أن تكون في البيئات الأخرى أيّ شيء آخر. فمثلًا في Node.js نستلم كائن مؤقّت فيه توابِع أخرى. نُعيد بأن ليس هناك مواصفة عالمية متّفق عليها لهذه التوابِع، فما من مشكلة في هذا. يمكنك مراجعة مواصفة HTML5 للمؤقّتات (داخل المتصفّحات) في فصل المؤقّتات. setInterval صياغة التابِع ‎setInterval‎ هي ذات ‎setTimeout‎: let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...) ولكلّ المُعاملات ذات المعنى. ولكن على العكس من ‎setTimeout‎ فهذا التابِع يشغّل الدالة مرّة واحدة ثمّ أخرى وأخرى وأخرى تفصلها تلك الفترة المحدّدة. يمكن أن نستدعي ‎clearInterval(timerId)‎ لنُوقف الاستدعاءات اللاحقة. سيعرض المثال الآتي الرسالة كلّ ثانيتين اثنتين، وبعد خمس ثوان يتوقّف ناتجها: // نكرّر التنفيذ بفترة تساوي ثانيتين let timerId = setInterval(() => alert('tick'), 2000); // وبعد خمس ثوان نُوقف الجدولة setTimeout(() => { clearInterval(timerId); alert('stop'); }, 5000); الوقت لا يتوقّف حين تظهر مُنبثقة ‎alert‎ تُواصل عقارب ساعة المؤقّت الداخلي (في أغلب المتصفّحات بما فيها كروم وفَيَرفُكس) بالمضيّ حتّى حين عرض ‎alert/confirm/prompt‎. لذا متى ما شغّلت الشيفرة أعلاه ولم تصرف نافذة ‎alert‎ بسرعة، فسترى نافذة ‎alert‎ الثانية بعد ذلك مباشرةً، بذلك تكون الفترة الفعلية بين التنبيهين أقلّ من ثانيتين. تداخل setTimeout لو أردنا تشغيل أمر كلّ فترة، فهناك طريقتين اثنتين. الأولى هي ‎setInterval‎. والثانية هي ‎setTimeout‎ متداخلة هكذا: /** بدل كتابة: let timerId = setInterval(() => alert('tick'), 2000); */ let timerId = setTimeout(function tick() { alert('tick'); timerId = setTimeout(tick, 2000); // (*) }, 2000); تابِع ‎setTimeout‎ أعلاه يُجدول الاستدعاء التالي ليحدث بعد نهاية الأول (لاحظ ‎(*)‎). كتابة توابِع ‎setTimeout‎ متداخلة يعطينا شيفرة مطواعة أكثر من ‎setInterval‎. بهذه الطريقة يمكن تغيير جدولة الاستدعاء التالي حسب ناتج الحالي. فمثلًا علينا كتابة خدمة تُرسل طلب بيانات إلى الخادوم كلّ خمس ثوان، ولكن لو كان الخادوم مُثقلًا بالعمليات فيجب أن تزداد الفترة إلى 10 فَـ 20 فَـ 40 ثانية وهكذا… إليك فكرة عن الشيفرة: let delay = 5000; let timerId = setTimeout(function request() { ...نُرسل الطلب... if (لو فشل الطلب لوجود ضغط على الخادوم) { // نزيد الفترة حتّى الطلب التالي delay *= 2; } timerId = setTimeout(request, delay); }, delay); ولو كانت الدوال التي نُجدولها ثقيلة على المعالج فيمكن أن نقيس الزمن الذي أخذتها عملية التنفيذ الحالية ونؤجّل أو نقدّم الاستدعاء التالي. يتيح لنا تداخل التوابِع ‎setTimeout‎ بضبط الفترة بين عمليات التنفيذ بدقّة أعلى ممّا تقدّمه ‎setInterval‎. لنرى الفرق بين الشيفرتين أسفله. الأولى تستعمل ‎setInterval‎: let i = 1; setInterval(function() { func(i++); }, 100); الثانية تستعمل ‎setTimeout‎ متداخلة: let i = 1; setTimeout(function run() { func(i++); setTimeout(run, 100); }, 100); سيُشغّل المُجدول الداخلي ‎func(i++)‎ كلّ 100 مليثانية حسب ‎setInterval‎: هل لاحظت ذلك؟ التأخير الفعلي بين استدعاءات ‎func‎ التي ينفّذها ‎setInterval‎ أقل مما هي عليه في الشيفرة! هذا طبيعي إذ أنّ الوقت الذي يأخذه تنفيذ ‎func‎ يستهلك بعضًا من تلك الفترة أيضًا. يمكن أيضًا بأن يصير تنفيذ ‎func‎ أكبر ممّا توقعناه على حين غرّة ويأخذ وقتًا أطول من 100 مليثانية. في هذه الحال ينتظر المحرّك انتهاء ‎func‎ ثمّ يرى المُجدول: لو انقضى الوقت يشغّل الدالة مباشرةً. دومًا ما تأخذ الدالة وقتًا أطول من ‎delay‎ مليثانية في هذه الحالات الهامشية، إذ تجري الاستدعاءات واحدةً بعد الأخرى دون هوادة. وإليك صورة ‎setTimeout‎ المتداخلة: تضمن ‎setTimeout‎ المتداخلة لنا التأخير الثابت (100 مليثانية في حالتنا). ذلك لأنّ الاستدعاء التالي لا يُجدول إلا بعد انتهاء السابق. كنس المهملات وردود نداء التابِعين setInterval و setTimeout تُنشأ إشارة داخلية إلى الدالة (وتُحفظ في المُجدول) متى مرّرتها إلى إلى ‎setInterval/setTimeout‎، وهذا يمنع كنس الدالة على أنّها مهملات، حتّى لو لم تكن هناك إشارات إليها. // ‫تبقى الدالة في الذاكرة حتّى يُستدعى `‎clearInterval‎`. setTimeout(function() {...}, 100); ولكن هناك تأثير جانبي لذلك كالعادة، فالدوال تُشير إلى بيئتها المُعجمية الخارجية. لذا طالما «تعيش»، تعيش معها المتغيرات الخارجية أيضًا، وهي أحيانًا كبيرة تأخذ ذاكرة أكبر من الدالة ذاتها. لذا، متى ما لم ترد تلك الدالة المُجدولة فالأفضل أن تُلغيها حتّى لو كانت صغيرة جدًا. جدولة setTimeout بتأخير صفر إليك الحالة الخاصة: ‎setTimeout(func, 0)‎ أو ‎setTimeout(func)‎. يُجدول هذا التابِع ليحدث تنفيذ ‎func‎ بأسرع ما يمكن، إلّا أن المُجدول لن يشغّلها إلا بعد انتهاء السكربت الذي يعمل حاليًا. أي أنّ الدالة تُجدول لأن تعمل «مباشرةً بعد» السكربت الحالي. فمثلًا تكتب هذه الشيفرة "Hello" ثم مباشرة "World": setTimeout(() => alert("World")); alert("Hello"); يعني السطر الأوّل «ضع الاستدعاء في التقويم بعد 0 مليثانية»، إلّا أنّ المُجدول لا «يفحص تقويمه» إلّا بعد انتهاء السكربت الحالي، بهذا تصير ‎"Hello"‎ أولًا وبعدها تأتي ‎"World"‎. كما أنّ هناك استعمالات متقدّمة خصّيصًا للمتصفّحات للمهلة بالتأخير صفر هذه، وسنشرحها في الفصل «حلقة الأحداث: المهام على المستويين الجُسيمي والذرّي». في الواقع، فالتأخير الصفر هذا ليس صفرًا (في المتصفّحات) تحدّ المتصفّحات من التأخير بين تشغيل المؤقّتات المتداخلة. تقول مواصفة HTML5: «بعد المؤقّتات المتداخلة الخمسة الأولى، تُجبر الفترة لتكون أربع مليثوان على الأقل.». لنرى ما يعني ذلك بهذا المثال أسفله. يُعيد استدعاء ‎setTimeout‎ جدولة نفسه بمدّة تأخير تساوي صفر، ويتذكّر كل استدعاء الوقت الفعلي بينه وبين آخر استدعاء في مصفوفة ‎times‎. ولكن، ما هي التأخيرات الفعلية؟ لنرى بأعيننا: let start = Date.now(); let times = []; setTimeout(function run() { times.push(Date.now() - start); // نحفظ التأخير من آخر استدعاء if (start + 100 < Date.now()) alert(times); // نعرض التأخيرات بعد 100 مليثانية else setTimeout(run); // وإلّا نُعيد الجدولة }); // ‫إليك مثالًا عن الناتج: // 1,1,1,1,9,15,20,24,30,35,40,45,50,55,59,64,70,75,80,85,90,95,100 تعمل المؤقّتات الأولى مباشرةً (كما تقول المواصفة)، وبعدها نرى ‎9, 15, 20, 24...‎. تلك الأربع مليثوان الإضافية هي التأخير المفروض بين الاستدعاءات. حتّى مع ‎setInterval‎ بدل ‎setTimeout‎، ذات الأمر: تعمل الدالة ‎setInterval(f)‎ أوّل ‎f‎ مرّة بمدّة تأخير صفر، وبعدها تزيد أربع مليثوان لباقي الاستدعاءات. سبب وجود هذا الحدّ هو من العصور الحجرية (متعوّدة دايمًا) وتعتمد شيفرات كثيرة على هذا السلوك. بينما مع نسخة الخواديم من جافاسكربت فهذا الحدّ ليس له وجود، وهناك أيضًا طُرق أخرى لبدء المهام التزامنية مباشرةً، مثل setImmediate للغة Node.js، هذا قلنا بأنّ هذا يخصّ المتصفّحات فقط. ملخص يتيح لنا التابِعان ‎setTimeout(func, delay, ...args)‎ و‎setInterval(func, delay, ...args)‎ تشيل الدالة ‎func‎ مرّة أو كلّ فترة حسب كذا مليثانية (‎delay‎). لإلغاء التنفيذ علينا استدعاء ‎clearTimeout/clearInterval‎ بالقيمة التي أعاداها ‎setTimeout/setInterval‎. يُعدّ استدعاء الدوال ‎setTimeout‎ تداخليًا خيارًا أفضل من ‎setInterval‎ إذ يُتيح لنا ضبط الوقت بين كلّ عملية استدعاء بدقّة. الجدولة بضبط التأخير على الصفر باستعمال ‎setTimeout(func, 0)‎ (كما واستعمال ‎setTimeout(func)‎) يكون حين نريدها «بأقصى سرعة ممكنة، متى انتهى السكربت الحالي». يَحدّ المتصفّح من أدنى تأخير بعد استدعاء ‎setTimeout‎ أو ‎setInterval‎ المتداخل الخامس (أو أكثر) - يَحدّه إلى 4 مليثوان، وهذا لأسباب تاريخية لاحظ بأنّ توابِع الجدولة لا تضمن التأخير كما هو حرفيًا. فمثلًا يمكن أن تكون مؤقّتات المتصفّحات أبطأ لأسباب عديدة: المعالج مُثقل بالعمليات. لسان المتصفّح يعمل في الخلفية. يعمل الحاسوب المحمول على البطارية. يمكن لهذا كله رفع دقّة المؤقّت الدنيا (أي أدنى تأخير ممكن) لتصير 300 مليثانية أو حتى 1000 مليثانية حسب المتصفّح وإعدادات الأداء في نظام التشغيل. تمارين اكتب الناتج كل ثانية الأهمية: 5 اكتب الدالة ‎printNumbers(from, to)‎ لتكتب عددًا كلّ ثانية بدءًا بِـ ‎from‎ وانتهاءً بِـ ‎to‎. اصنع نسختين من الحل. واحدةً باستعمال ‎setInterval‎. واحدةً باستعمال ‎setTimeout‎ متداخلة. الحل باستعمال ‎setInterval‎: function printNumbers(from, to) { let current = from; let timerId = setInterval(function() { alert(current); if (current == to) { clearInterval(timerId); } current++; }, 1000); } // ‫الاستعمال: printNumbers(5, 10); باستعمال ‎setTimeout‎ متداخلة: function printNumbers(from, to) { let current = from; setTimeout(function go() { alert(current); if (current < to) { setTimeout(go, 1000); } current++; }, 1000); } // ‫الاستعمال: printNumbers(5, 10); لاحظ كلا الحلّين: هناك تأخير أولي قبل أول عملية كتابة إذ تُستدعى الدالة بعد ‎1000ms‎ في أوّل مرة. لو أردت تشغيل الدالة مباشرةً فعليك كتابة استدعاء إضافي في سطر آخر هكذا: function printNumbers(from, to) { let current = from; function go() { alert(current); if (current == to) { clearInterval(timerId); } current++; } go(); // هنا let timerId = setInterval(go, 1000); } printNumbers(5, 10); ماذا سيعرض setTimeout؟ الأهمية: 5 جدول أحدهم في الشيفرة أسفله استدعاء ‎setTimeout‎، وثمّ كتب عملية حسابية ثقيلة لتعمل (وهي تأخذ أكثر من 100 مليثانية حتى تنتهي). متى ستعمل الدالة المُجدولة؟ بعد الحلقة؟ قبل الحلقة؟ في بداية الحلقة؟ ما ناتج ‎alert‎؟ let i = 0; setTimeout(() => alert(i), 100); // ? // عُدّ بأنّ الوقت اللازم لتنفيذ هذه الدالة يفوق 100 مليثانية for(let j = 0; j < 100000000; j++) { i++; } الحل لن يُشغّل أيّ تابِع ‎setTimeout‎ إلا بعدما تنتهي الشيفرة الحالية. ستكون قيمة ‎i‎ هي القيمة الأخيرة: ‎100000000‎. let i = 0; setTimeout(() => alert(i), 100); // 100000000 // عُدّ بأنّ الوقت اللازم لتنفيذ هذه الدالة يفوق 100 مليثانية for(let j = 0; j < 100000000; j++) { i++; } ترجمة -وبتصرف- للفصل Scheduling: setTimeout and setInterval من كتاب The JavaScript language
  8. توجد (أيضًا) طريقة أخرى لإنشاء الدوال. صحيح هي نادرة الاستعمال ولكن لا مفرّ منها في حالات معيّنة. الصياغة إليك صياغة إنشاء الدالة: let func = new Function ([arg1, arg2, ...argN], functionBody); نصنع الدالة بالوُسطاء ‎arg1...argN‎ ونمرّر متنها ‎functionBody‎. «هات الشيفرة وقلّل ثرثرتك»… صحيح، هذا أسهل. إليك الدالة وفيها وسيطين اثنين: let sum = new Function('a', 'b', 'return a + b'); alert( sum(1, 2) ); // 3 وهنا دالة بلا وُسطاء فيها متنها فقط: let sayHi = new Function('alert("Hello")'); sayHi(); // Hello الفرق الأساس بين هذه الطريقة والطرائق الأخرى هي أنّا نصنع الدالة هنا (كما لاحظت) من سلسلة نصية حرفيًا، ونمرّرها في وقت تشغيل الشيفرة. ألزمتنا التصريحات السابقة كلها - ألزمتنا نحن المطوّرين أن نكتب شيفرة الدالة في السكربت. ولكن صياغة ‎new Function‎ تسمح لنا بأن نحوّل أيّ سلسلة نصية لتصير دالة. فمثلًا يمكن أن نستلم دالة جديدة من أحد الخواديم وننفّذها: let str = ... نستلم الشيفرة ديناميكيًا من الخادوم ... let func = new Function(str); func(); لا نستعمل هذه إلا في حالات خاصّة، مثل لو استلمنا الشيفرة من الخادوم أو صنعنا الدالة ديناميكًا من قالب (في تطبيقات الوِب المعقّدة). المنغلقات عادةً ما تتذكّر الدالة مكان ولادتها في الخاصية المميّزة ‎[[Environment]]‎، فتُشير إلى البيئة المُعجمية حين صُنعت الدالة (شرحنا هذا في فصل «المُنغِلقات»). ولكن حين نصنع الدالة باستعمال ‎new Function‎ فتُضبط خاصية ‎[[Environment]]‎ على البيئة المُعجمية العمومية لا الحالية. أي أنّ هذه الدوال لا يمكن أن ترى المتغيرات الخارجية بل تلك العمومية فقط. function getFunc() { let value = "test"; let func = new Function('alert(value)'); return func; } getFunc()(); // ‫خطأ: value غير معرّف وازن بين هذا والسلوك الطبيعي: function getFunc() { let value = "test"; let func = function() { alert(value); }; return func; } getFunc()(); // ‫«test»، من بيئة getFunc المُعجمية صحيح أنّ الميزة الخاصة للصياغة ‎new Function‎ غريبة بعض الشيء، ولكنها عمليًا مفيدة جدًا. تخيّل الآن بأنّنا صنعنا دالة من سلسلة نصية. شيفرة هذه الدالة ليست معروفة ونحن نكتب السكربت (ولهذا لم نستعمل الدوال العادية)، بل ستكون معروفة حين تنفيذه. كما أسلفنا يمكن أن نستلم الدالة من الخادوم أو أيّ مكان آخر. الآن، على دالتنا هذه التفاعل مع السكربت الرئيس. لكن ماذا لو أمكن لها أن ترى المتغيرات الخارجية؟ المشكلة هي أنّه قبل أن ننشر شيفرة جافاسكربت لنستعملها، نستعمل المُصغِّرات (minifiers) لضغطها. تقلّص هذه المُصغِّرات حجم الشيفرة بإزالة التعليقات والمسافات الزائدة، كما (وهذا مهم) تُغيّر تسمية المتغيرات المحلية إلى أسماء أقصر. فمثلًا لو كان في الدالة ‎let userName‎ فيستبدلها المُصغِّر إلى ‎let a‎ (أو أيّ حرف آخر لو هناك من أخذ الاسم)، وينفّذ هذا في كلّ مكان آخر. عادةً لا يضرّ ذلك إذ أنّ المتغير محلي ولا يمكن لما خارج الدالة رؤيته، بينما يستبدل المُصغِّر كلّ مرة يرد فيها المتغير داخل الدالة. هذه الأدوات ذكية فهي تحلّل بنية الشيفرة لألا تُعطبها، وليست كأدوات البحث والاستبدال الهمجية. لذا لو أرادت ‎new Function‎ أن تستعمل المتغيرات الخارجية فلن تعرف بوجود ‎userName‎ الذي تغيّر اسمه. لو أمكن للدوال ‎new Function‎ أن ترى المتغيرات الخارجية لكانت ستواجه مشاكل جمّة مع المُصغِّرات. كما وأنّ الشيفرات من هذا النوع ستكون سيّئة من حيث البنية وعُرضة للأخطاء والمشاكل. لو أردت تمرير شيء للدالة ‎new Function‎ فعليك استعمال مُعاملاتها. ملخص الصياغة: let func = new Function ([arg1, arg2, ...argN], functionBody); ويمكن تمرير المُعاملات (لأسباب تاريخية أيضًا) في قائمة مفصولة بفواصل. هذه التصريحات الثلاث لا تفرق عن بعضها البعض: new Function('a', 'b', 'return a + b'); // الصياغة الأساس new Function('a,b', 'return a + b'); // مفصولة بفواصل new Function('a , b', 'return a + b'); // مفصولة بفواصل ومسافات تُشير خاصية ‎[[Environment]]‎ للدوال ‎new Function‎ إلى البيئة المُعجمية العمومية لا الخارجية. بهذا لا يمكن لهذه الدوال استعمال المتغيرات الخارجية. إلّا أنّ ذلك أمر طيّب إذ تؤمّن لنا خطّ حماية لألا نصنع الأخطاء والمشاكل، فتمرير المُعاملات جهارةً أفضل بكثير من حيث بنية الشيفرة ولا تتسبّب مشاكل مع المُصغِّرات. ترجمة -وبتصرف- للفصل The "new Function" syntax من كتاب The JavaScript language
  9. كما نعلم فالدوال في لغة جافاسكربت تُعدّ قيمًا. ولكلّ قيمة في هذه اللغة نوع. ولكن ما نوع الدالة نفسها؟ تُعدّ الدوال كائنات في جافاسكربت. يمكننا تخيّل الدوال على أنّها «كائنات إجرائية» يمكن استدعائها. لا يتوقّف الأمر عند الاستدعاء أيضًا بل يمكن حتّى أن نُعاملها معاملة الكائنات فنُضيف الخاصيات ونُزيلها، أو نمرّرها بالإشارة وغيرها من أمور. خاصية الاسم name تحتوي كائنات الدوال على خاصيات يمكننا استعمالها. فمثلًا يمكن أن نعرف اسم الدالة من خاصية الاسم ‎name‎: function sayHi() { alert("Hi"); } alert(sayHi.name); // sayHi والمُضحك في الأمر هو أنّ منطق اللغة في إسناد الاسم ذكيّ (الذكاء الاصطناعي مبهر)، فهو يُسند اسم الدالة الصحيح حتّى لو أنشأناها بدون اسم ثمّ أسندناها إلى متغير مباشرةً: let sayHi = function() { alert("Hi"); }; alert(sayHi.name); // ‫sayHi (للدالة اسم!) كما ويعمل المنطق أيضًا لو كانت عملية الإسناد عبر قيمة مبدئية: function f(sayHi = function() {}) { alert(sayHi.name); // ‫sayHi (تعمل أيضًا!) } f(); تُدعى هذه الميزة في توصيف اللغة «بالاسم السياقي». فلو لم تقدّم الدالة اسمًا لها فيحاول المحرّك معرفته من السياق مع أوّل عملية إسناد. كما ولتوابِع الكائنات أسماء أيضًا: let user = { sayHi() { // ... }, sayBye: function() { // ... } } alert(user.sayHi.name); // sayHi alert(user.sayBye.name); // sayBye ولكن ليس للسحر مكان هنا، فهناك حالات يستحيل على المحرّك معرفة الاسم الصحيح منها، بهذا تكون خاصية الاسم فارغة، كما في هذه الشيفرة: // نُنشئ دالة في مصفوفة let arr = [function() {}]; alert( arr[0].name ); // <سلسلة نصية فارغة> // ما من طريقة يعرف بها المحرّك الاسم الصحيح، فباختصار، ليس هناك اسم! ولكن عمليًا، لكل الدوال أسماء أغلب الوقت. خاصية الطول length توجد خاصية أخرى مضمّنة في اللغة باسم ‎length‎ وهي تُعيد عدد مُعاملات الدالة. مثال: function f1(a) {} function f2(a, b) {} function many(a, b, ...more) {} alert(f1.length); // 1 alert(f2.length); // 2 alert(many.length); // 2 نرى هنا بأن المُعاملات البقية لم تُحسب. يستعمل المطوّرون خاصية ‎length‎ أحيانًا لإجراء التحقّق الداخلي داخل الدوال التي تعتمد في تشغيلها على التحكّم بدوال أخرى. في الشيفرة أسفله، تقبل دالة ‎ask‎ سؤالًا ‎question‎ تطرحه وعددًا غير محدّد من دوال المعالجة ‎handler‎ لتستدعيها دالة السؤال. فما إن ينزل الوحي على المستخدم ويُعطينا إجابة تستدعي الدالة المُعالجات. يمكننا تمرير نوعين اثنين من المُعالجات هذه: دالة ليس لها وُسطاء لا تُنشأ إلا عندما يُعطيها المستخدم إجابة بالإيجاب. دالة لها وُسطاء تُستدعى في بقية الحالات وتُعيد إجابة المستخدم. علينا فحص خاصية ‎handler.length‎ لنستدعي ‎handler‎ بالطريقة السليمة. الفكرة هنا هي أن تستعمل الشيفرة صياغة مُعالجة بسيطة وبدون وُسطاء لحالات الإيجاب (وهذا الشائع)، إضافةً على دعم المُعالجات العامة أيضًا: function ask(question, ...handlers) { let isYes = confirm(question); for(let handler of handlers) { if (handler.length == 0) { if (isYes) handler(); } else { handler(isYes); } } } // يُعاد كِلا المُعالجان لو كانت الإجابة بالإيجاب // ولو كانت بالسلب، فالثاني فقط ask("Question?", () => alert('You said yes'), result => alert(result)); هذه حالة من حالات التعدّدية الشكلية، أي حين يتغيّر تعاملنا مع الوُسطاء حسب أنواعها… في حالتنا فهي حسب أطوالها ‎length‎. لهذه الفكرة استعمال فعليّ في مكتبات جافاسكربت. خاصيات مخصصة يمكننا أيضًا إضافة ما نريد من خاصيات. فهنا نُضيف خاصية العدّاد ‎counter‎ ليسجّل إجمالي عدد الاستدعاءات: function sayHi() { alert("Hi"); // لنعدّ كم من مرّة شغّلناه sayHi.counter++; } sayHi.counter = 0; // القيمة الأولية sayHi(); // Hi sayHi(); // Hi alert( `‎Called ${sayHi.counter} times‎` ); // Called 2 times الخاصيات ليست متغيرات لا تعرّف الخاصية المُسندة إلى الدوال مثل ‎sayHi.counter = 0‎ متغيرًا محليًا فيها (‎counter‎ في حالتنا). أي أنّ لا علاقة تربط الخاصية ‎counter‎ بالمتغير ‎let counter‎ البتة. يمكننا التعامل مع الدوال على أنها كائنات فنخزّن فيها الخاصيات، ولكن هذا لا يؤثّر على طريقة تنفيذها. ليست المتغيرات خاصيات للدالة ولا العكس، كلاهما منفصلين يسيران في خطّين مُحال أن يتقاطعا. يمكننا أحيانًا استعمال خاصيات الدوال بدل المُنغلِقات. فمثلًا يمكن إعادة كتابة تمرين دالة العدّ في فصل «المُنغلِقات» فنستعمل خاصية دالة: function makeCounter() { // بدل: // let count = 0 function counter() { return counter.count++; }; counter.count = 0; return counter; } let counter = makeCounter(); alert( counter() ); // 0 alert( counter() ); // 1 هكذا خزّنا الخاصية ‎count‎ في الدالة مباشرةً وليس في بيئتها المُعجمية الخارجية. أهذه أفضل أم المنغلقات أفضل؟ الفرق الرئيس هو: لو كانت قيمة ‎count‎ «تحيا» في متغير خارجي فلا يمكن لأي شيفرة خارجية الوصول إليها، بل الدوال المتداخلة فقط من يمكنها تعديلها، ولو ربطناها بدالة فيصير هذا ممكنًا: function makeCounter() { function counter() { return counter.count++; }; counter.count = 0; return counter; } let counter = makeCounter(); // هذا counter.count = 10; alert( counter() ); // 10 إذًا فالخيار يعود لنا: ماذا نريد وما الغاية. تعابير الدوال المسماة كما اسمها، فتعابير الدوال المسمّاة (Named Function Expression) هي تعابير الدوال التي لها اسم… بسيطة. لنأخذ مثلًا تعبير دالة مثل أي تعبير تراه في حياتك البرمجية التعيسة: let sayHi = function(who) { alert(`‎Hello, ${who}‎`); }; والآن نُضيف اسمًا له: let sayHi = function func(who) { alert(`‎Hello, ${who}‎`); }; هل حللنا أزمة عالمية هنا؟ ما الداعي من هذا الاسم ‎"func"‎؟ أولًا، ما زال أمامنا تعبير دالة، فإضافة الاسم ‎"func"‎ بعد ‎function‎ لم يجعل الجملة تصريحًا عن دالة إذ ما زلنا نصنع الدالة داخل جزء من تعبير إسناد. كما وإضافة الاسم لم يُعطب الدالة بأي شكل. يمكن أن ندخل الدالة هكذا ‎sayHi()‎: let sayHi = function func(who) { alert(`‎Hello, ${who}‎`); }; sayHi("John"); // Hello, John ثمّة أمرين مميزين بهذا الاسم ‎func‎ وهما السبب وراء كل هذا: يتيح الاسم بأن تُشير الدالة إلى نفسها داخليًا. ولا يظهر الاسم لما خارج الدالة. فمثلًا تستدعي الدالة ‎sayHi‎ أسفله نفسها ثانيةً بالوسيط ‎"Guest"‎ لو لم نمرّر لها ‎who‎ من البداية: let sayHi = function func(who) { if (who) { alert(`‎Hello, ${who}‎`); } else { func("Guest"); // ‫نستعمل func لنستدعي نفسنا ثانيةً } }; sayHi(); // Hello, Guest // ولكن هذا لن يعمل: func(); // ‫ويعطينا خطأً بأنّ func غير معرّفة (فالدالة لا تظهر لما خارجها) ولكن لمَ نستعمل ‎func‎ أصلًا؟ ألا يمكن أن نستعمل ‎sayHi‎ لذلك الاستدعاء المتداخل؟ للصراحة، يمكن ذلك في حالات عديدة: let sayHi = function(who) { if (who) { alert(`‎Hello, ${who}‎`); } else { sayHi("Guest"); } }; مشكلة تلك الشيفرة هي احتمالية تغيّر ‎sayHi‎ في الشيفرة الخارجية. فلو أُسندت الدالة إلى متغير آخر بدل ذاك فستبدأ الأخطاء تظهر: let sayHi = function(who) { if (who) { alert(`‎Hello, ${who}‎`); } else { sayHi("Guest"); // ‫خطأ: sayHi ليست بدالة } }; let welcome = sayHi; sayHi = null; welcome(); // ‫خطأ: لم يعد الاستدعاء المتداخل sayHi يعمل بعد الآن! سبب ذلك هو أنّ الدالة تأخذ ‎sayHi‎ من بيئتها المُعجمية الخارجية إذ لا تجد ‎sayHi‎ محليًا فيها فتستعمل المتغير الخارجي. وفي لحظة الاستدعاء تلك يكون ‎sayHi‎ الخارجي قد صار ‎null‎. وهذا الداعي من ذلك الاسم الذي نضعه في تعبير الدالة، أن يحلّ هذه المشاكل بذاتها. هيًا نُصلح شيفرتنا: let sayHi = function func(who) { if (who) { alert(`‎Hello, ${who}‎`); } else { func("Guest"); // الآن تعمل كما يجب } }; let welcome = sayHi; sayHi = null; welcome(); // ‫Hello, Guest (الاستدعاءات المتداخلة تعمل) الآن صارت تعمل إذ الاسم ‎"func"‎ محليّ للدالة فقط ولا تأخذها من الخارج (ولا تظهر للخارج أيضًا). تضمن لنا مواصفات اللغة بأنّها ستُشير دومًا وأبدًا إلى الدالة الحالية. مع ذلك فما زالت الشيفرة الخارجية تملك المتغيرين ‎sayHi‎ و‎welcome‎، بينما ‎func‎ هو «اسم الدالة داخليًا» أي كيف تستدعي الدالة نفسها من داخلها. ما من ميزة كهذه للتصريح عن الدوال ميزة «الاسم الداخلي» هذه التي شرحناها هنا مُتاحة لتعابير الدوال فقط وليست متاحة للتصريحات عن الدوال. فليس لهذه الأخيرة أية صياغة برمجية لإضافة اسم «داخلي» لها. لكن أحيانًا نرى حاجة بوجود اسم داخلي نعتمد عليه، حينها يكون السبب وجيهًا بأن نُعيد كتابة التصريح عن الدالة إلى صيغة تعبير الدالة المسمّى. ملخص تُعدّ الدوال كائنات. شرحنا في الفصل خصائصها: اسمها ‎name‎ -- غالبًا ما يأتي من تعريف الدالة. لكن لو لم يكن هناك واحد فيحاول المحرّك تخمينه من السياق (مثلًا من عبارة الإسناد). عدد مُعاملتها في تعريف الدالة ‎length‎ -- لا تُحسب المُعاملات البقية. لو عرّفنا الدالة باستعمال تعبير عن دالة (وليس في الشيفرة الأساس)، وكان لهذه الدالة اسم فنُسمّيها بتعبير الدالة المسمّى. كما يمكن أن تحمل الدوال خاصيات إضافية، وتستغل الكثير من مكتبات جافاسكربت المعروفة هذه الميزة أيّما استغلال. إذ تُنشئ دالة «رئيسة» بعدها تُرفق دوال أخرى «مُساعِدة» إليها. فمثلًا تُنشئ مكتبة jQuery الدالة بالاسم ‎$‎، وتُنشئ مكتبة lodash الدالة بالسم ‎_‎ ثمّ تُضيف خاصياتها ‎_.clone‎ و ‎_.keyBy‎ وغيرها (طالع docs متى أردت معرفتها أكثر). ما تفعله هذه الدوال يعود إلى أنّها (في الواقع) تحاول حدّ «التلوّث» في المجال العمومي فلا تستعمل المكتبة إلّا متغيرًا عموميًا واحدًا. وهذا يُقلّل من أدنى إمكانية لتضارب الأسماء. إذًا، فالدالة تؤدي عملًا رائعًا كما هي، وأيضًا تحوي على وظائف أخرى خاصيات لها. تمارين ضبط قيمة العداد وإنقاصها الأهمية: 5 عدّل شيفرة الدالة ‎makeCounter()‎ بحيث يُنقص العدّاد قيمتها إضافةً إلى ضبطها: على ‎counter()‎ إعادة العدد التالي (كما في الأمثلة السابقة). على ‎counter.set(value)‎ ضبط قيمة العدّاد لتكون ‎value‎. على ‎counter.decrease()‎ إنقاص قيمة العدّاد واحدًا (1). طالِع الشيفرة أدناه كي تعرف طريقة استعمال الدالة: function makeCounter() { let count = 0; // ... شيفرتك هنا ... } let counter = makeCounter(); alert( counter() ); // 0 alert( counter() ); // 1 counter.set(10); // نضبط العدد الجديد alert( counter() ); // 10 counter.decrease(); // نُنقص العدد واحدًا 1 alert( counter() ); // ‫10 (بدل 11) ملاحظة: يمكنك استعمال مُنغلِق أو خاصية دالة لحفظ العدد الحالي، أو لو أردت فاكتب الحل بالطريقتين. الحل يستعمل الحل المتغير ‎count‎ محليًا، كما وتوابِع أخرى نكتبها داخل الدالة ‎counter‎. تتشارك هذه التوابِع ذات البيئة المُعجمية الخارجية كما وترى أيضًا قيمة ‎count‎ الحالية. مجموع ما في الأقواس أيًا كان عدد الأقواس الأهمية: 5 اكتب الدالة ‎sum‎ لتعمل كالآتي: sum(1)(2) == 3; // 1 + 2 sum(1)(2)(3) == 6; // 1 + 2 + 3 sum(5)(-1)(2) == 6 sum(6)(-1)(-2)(-3) == 0 sum(0)(1)(2)(3)(4)(5) == 15 تريد تلميحًا؟ ربما تكتب كائنًا مخصّصًا يُحوّل الأنواع الأولية لتُناسب الدالة. أيّما كانت الطريقة التي سنستعملها ليعمل هذا الشيء، فلا بدّ أن تُرجع ‎sum‎ دالة. على تلك الدالة أن تحفظ القيمة الحالية بين كلّ استدعاء والآخر داخل الذاكرة. حسب المهمّة المُعطاة، يجب أن تتحول الدالة إلى عدد حين نستعملها في ‎==‎. الدوال كائنات لذا فعملية التحويل ستنفع كما شرحنا في فصل «التحويل من كائن إلى قيمة أولية»، ويمكن أن نقدّم تابِعًا خاصًا يُعيد ذلك العدد. إلى الشيفرة: function sum(a) { let currentSum = a; function f(b) { currentSum += b; return f; } f.toString = function() { return currentSum; }; return f; } alert( sum(1)(2) ); // 3 alert( sum(5)(-1)(2) ); // 6 alert( sum(6)(-1)(-2)(-3) ); // 0 alert( sum(0)(1)(2)(3)(4)(5) ); // 15 لاحظ بأنّ دالة ‎sum‎ تعمل مرّة واحدة فقط لا غير، وتُعيد الدالة ‎f‎. وبعدها في كلّ استدعاء يليها، تُضيف ‎f‎ المُعامل إلى المجموع ‎currentSum‎ وتُعيد نفسها. لا نستعمل التعاود في آخر سطر من ‎f‎. هذا شكل التعاود: function f(b) { currentSum += b; return f(); // <-- استدعاء تعاودي } بينما في حالتنا نُعيد الدالة دون استدعائها: function f(b) { currentSum += b; return f; // <-- لا تستدعي نفسها، بل تُعيد نفسها } وستُستعمل ‎f‎ هذه في الاستدعاء التالي، وتُعيد نفسها ثانيةً مهما لزم. وبعدها حين نستعمل العدد أو السلسلة النصية، يُعيد التابِع ‎toString‎ المجموع ‎currentSum‎. يمكن أيضًا أن نستعمل ‎Symbol.toPrimitive‎ أو ‎valueOf‎ لإجراء عملية التحويل. ترجمة -وبتصرف- للفصل Function object, NFE من كتاب The JavaScript language
  10. تُنشّأ الكائنات عادة لتُمَثِّل أشياء من العالم الحقيقي مثل المستخدمين، والطلبات، وغيرها: let user = { name: "John", age: 30 }; يمكن للمستخدم في العالم الحقيقي أن يقوم بعدة تصرفات: مثل اختيار شيء من سلة التسوق، تسجيل الدخول، والخروج …إلخ. تُمَثَّل هذه التصرفات في لغة JavaScript بإسناد دالة إلى خاصية وتدعى الدالة آنذاك بالتابع (method، أي دالة تابعة لكائن). أمثلة على الدوال بدايةً، لنجعل المستخدم user يقول مرحبًا: let user = { name: "John", age: 30 }; user.sayHi = function() { alert("Hello!"); }; user.sayHi(); // Hello! استخدمنا هنا تعبير الدالة لإنشاء دالة تابع للكائن user وربطناها بالخاصية user.sayHi ثم استدعينا الدالة. هكذا أصبح بإمكان المستخدم التحدث! الآن أصبح لدى الكائن user الدالة sayHi. يمكننا أيضًا استخدام دالة معرفة مسبقًا بدلًا من ذلك كما يلي: let user = { // ... }; // أولا، نعرف دالة function sayHi() { alert("Hello!"); }; // أضِف الدالة للخاصية لإنشاء تابع user.sayHi = sayHi; user.sayHi(); // Hello! البرمجة الشيئية (Object-oriented programming) يسمى كتابة الشيفرة البرمجية باستخدام الكائنات للتعبير عن الاشياء «بالبرمجة الشيئية/كائنية» (object-oriented programming، تُختَصَر إلى "OOP"). OOP هو موضوع كبيرجدًا، فهو علم مشوق ومستقل بذاته. يعلمك كيف تختار الكائنات الصحيحة؟ كيف تنظم التفاعل فيما بينها؟ كما يعد علمًا للهيكلة ويوجد العديد من الكتب الأجنبية الجيدة عن هذا الموضوع مثل كتاب “Design Patterns: Elements of Reusable Object-Oriented Software” للمؤلفين E.Gamma، و R.Helm، و R.Johnson، و J.Vissides أو كتاب “Object-Oriented Analysis and Design with Applications” للمؤلف G.Booch، وغيرهما. اختصار الدالة يوجد طريقة أقصر لكتابة الدوال في الكائنات المعرفة تعريفًا مختصرًا باستعمال الأقواس تكون بالشكل التالي: // يتصرف الكائنان التاليان بالطريقة نفسها user = { sayHi: function() { alert("Hello"); } }; // يبدو شكل الدالة المختصر أفضل، أليس كذلك؟ user = { sayHi() { // مثل "sayHi: function()" alert("Hello"); } }; يمكننا حذف الكلمة المفتاحية "function" وكتابة sayHi()‎ كما هو موضح. حقيقةً، التعبيرين ليسا متطابقين تمامًا، يوجد اختلافات خفية متعلقة بالوراثة في الكائنات (سيتم شرحها لاحقًا)، لكن لا يوجد مشكلة الآن. يفضل استخدام الصياغة الأقصر في كل الحالات تقريبًا. الكلمة المفتاحية "this" في الدوال من المتعارف أن الدوال تحتاج للوصول إلى المعلومات المخزنة في الكائن لِتنفذ عملها. مثلًا، قد تحتاج الشيفرة التي بداخل user.sayHi()‎ لِاسم المستخدم user. هنا، يمكن للدالة استخدام الكلمة المفتاحية this للوصول إلى نسخة الكائن التي استدعتها. أي، قيمة this هي الكائن "قبل النقطة" الذي استُخدِم لاستدعاء الدالة. مثلًا: let user = { name: "John", age: 30, sayHi() { // هو الكائن الحالي "this" alert(this.name); } }; user.sayHi(); // John أثناء تنفيذ user.sayHi()‎ هنا، ستكون قيمة this هي الكائن user. عمليًا، يمكن الوصول إلى الكائن بدون استخدام this بالرجوع إليه باستخدام اسم المتغير الخارجي: let user = { name: "John", age: 30, sayHi() { alert(user.name); // "user" يدلًا من "this" } }; لكن، لا يمكن الاعتماد على الطريقة السابقة. فإذا قررنا نسخ الكائن user إلى متغير آخر، مثلا: admin = user وغيرنا محتوى user لشيء آخر، فسيتم الدخول إلى الكائن الخطأ كما هو موضح في المثال التالي: let user = { name: "John", age: 30, sayHi() { alert( user.name ); // يتسبب في خطأ } }; let admin = user; user = null; // تغيير المحتوى لتوضيح الأمر admin.sayHi(); // يُرجِع خطأ sayHi() استخدام الاسم القديم بِداخل إن استخدمنا this.name بدلًا من user.name بداخل alert، فستعمل الشيفرة عملًا صحيحًا. "this" غير محدودة النطاق الكلمة المفتاحية this في JavaScript تتصرف تصرفًا مختلفًا عن باقي اللغات البرمجية. فيمكن استخدامها في أي دالة. انظر إلى المثل التالي، إذ لا يوجد خطأ في الصياغة: function sayHi() { alert( this.name ); } تُقَيَّم قيمة this أثناء تنفيذ الشيفرة بالاعتماد على السياق. مثلًا، في المثال التالي، تم تعيين الدالة ذاتها إلى كائنين مختلفين فيصبح لكل منهما قيمة مختلفة لـ "this" أثناء الاستدعاء: let user = { name: "John" }; let admin = { name: "Admin" }; function sayHi() { alert( this.name ); } // استخدام الدالة ذاتها مع كائنين مختلفين user.f = sayHi; admin.f = sayHi; // this لدى الاستدعائين قيمة مختلفة لـ // التي بداخل الدالة تعني المتغير الذي قبل النقطة "this" user.f(); // John (this == user) admin.f(); // Admin (this == admin) admin['f'](); // Admin (يمكن الوصول إلى الدالة عبر الصيغة النقطية أو الأقواس المربعة – لا يوجد مشكلة في ذلك) القاعدة ببساطة: إذا استُدعِيَت الدالة obj.f()‎، فإن this هي obj أثناء استدعاء f؛ أي إما user أو admin في المثال السابق. استدعاءٌ دون كائن: this == undefined يمكننا استدعاء الدالة دون كائن: function sayHi() { alert(this); } sayHi(); // undefined - غير معرَّف في هذه الحالة ستكون قيمة this هي undefined في الوضع الصارم. فإن حاولنا الوصول إلى this.name سيكون هناك خطأ. في الوضع غير الصارم، فإن قيمة this في هذه الحالة ستكون المتغير العام (في المتصفح window والتي سَنشرحها في فصل المتغيرات العامة). هذا السلوك زمني يستخدم إصلاحات الوضع الصارم "use strict". يُعد هذا الاستدعاء خطأً برمجيًا غالبًا. فإن وًجِدت this بداخل دالة، فمن المتوقع استدعاؤها من خلال كائن. الأمور المترتبة على this الغير محدودة النطاق إن أتيت من لغة برمجية أخرى، فمن المتوقع أنك معتاد على "this المحدودة" إذ يمكن لِلدوال المعرَّفة في الكائن استخدام this التي ترجع للكائن. تستخدم this بحرية في JavaScript، وتُقَيَّم قيمتها أثناء التنفيذ ولا تعتمد على المكان حيث عُرِّفت فيه، بل على الكائن الذي قبل النقطة التي استدعت الدالة. يوجد ايجابيات وسلبيات لمبدأ تقييم this أثناء وقت التشغيل. فمن ناحية، يمكن إعادة استخدام الدالة مع عدة كائنات، ومن الناحية الأخرى، المرونة الأكثر تعطي فرصًا أكثر للخطأ. لسنا بصدد الحكم على تصميم اللغة ونعته بالجيد أم سيء، بل نحاول فهم طريقة عملها وكيفية الاستفادة من ميزاتها وتجنب الأخطاء. ميزة داخلية: النوع المرجعي يُغطي هذا الجزء ميزة متقدمة -من ميزات اللغة-لفهم أفضل لحالة معينة. إن كنت على عجلة من أمرك، يمكنك تخطي أو تأجيل هذا الجزء. يمكن لاستدعاء الدالة المعقد أن يفقد this، فمثلًا: let user = { name: "John", hi() { alert(this.name); }, bye() { alert("Bye"); } }; user.hi(); // John (يعمل الاستدعاء البسيط) // 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)(); // خطأ! لماذا؟ إن أردنا فهم سبب حدوث ذلك، لِنكشف الغطاء عن كيفية عمل الاستدعاء obj.method()‎. عند النظر عن قرب، يمكننا ملاحظة عمليتين في التعليمة obj.method()‎: 1- أولا، النقطة '.' تسترجع الخاصية obj.method. 2- ثم الأقواس () تنفذ الدالة. إذًا، كيف تُمرَّر المعلومات عن this من الجزء الأول للثاني؟ إن وضعنا العمليتين في سطرين منفصلين، فَسنفقد this بالتأكيد: let user = { name: "John", hi() { alert(this.name); } } // فصل الحصول على الدالة واستدعائها في سطرين منفصلين let hi = user.hi; hi(); // غير مُعَرَّفَة this خطأ، لأن تُسنِد التعليمة hi = user.hi الدالة إلى المتغير، ثم، في السطر الأخير تصبح مستقلة، فلا يوجد this هنا ضمن النطاق. تستخدم JavaScript خدعة لجعل user.hi()‎ تعمل - صيغة النقطة '.' لا تُرجِع دالة، بل قيمة من النوع المرجعي الخاص النوع المرجعي هو "نوع للتخصيص". لا يمكننا استخدام هذا النوع بشكل واضح، بل يُسخدَم داخليًا بواسطة اللغة. تُشَكَّل قيمة النوع المرجعي من ثلاث قيم (base, name, strict)، إذ: base هي الكائن. name هو اسم الخاصية. strict تساوي "true" إن كان الوضع الصارم use strict مُفعلًا. النتيجة من الوصول إلى خاصية user.hi ليست دالة، إنما قيمة من النوع المرجعي. بالنسبة لـ user.hi في الوضع الصارم تكون: // قيمة من النوع المرجعي (user, "hi", true) عند استدعاء الأقواس () في النوع المرجعي فإنها تستقبل المعلومة كاملة عن الكائن والدلة، وتتمكن من تعيين this بطريقة صحيحة (في هذه الحالة user). النوع المرجعي هو نوع "وسيط" داخلي، وغرضه هو تمرير المعلومات من الصيغة النُقطية . إلى أقواس الاستدعاء (). أي عملية أخرى مثل الإسناد hi = user.hi تُلغي النوع المرجعي ككل، فهي تأخذ قيمة الدالة user.hi وتُمررها. فَتفقد العمليات التالية this. لذا، ونتيجة لذلك، تُمرَّر قيمة this بالطريقة الصحيحة إن كانت الدالة مُستدعاه مباشرة باستخدام صيغة النقطة obj.method()‎ أو الأقواس المربعة obj['method']()‎ (يؤديان العمل ذاته). سنتعلم طرائق أخرى لحل هذه المشكلة لاحقًا، مثل استخدام func.bind(). الدوال السهمية لا تحوي "this" الدوال السهمية (Arrow function) هي دوال خاصة: فهي لا تملك this مخصصة لها. إن وضعنا this في إحدى هذه الدوال فَستؤخذ قيمة this من الدالة الخارجية. مثلًا، تحصل الدالة arrow()‎ على قيمة this من الدالة الخارجية user.sayHi()‎: let user = { firstName: "Ilya", sayHi() { let arrow = () => alert(this.firstName); arrow(); } }; user.sayHi(); // Ilya يُعد ذلك إحدى ميزات دوال الدوال السهمية، وهي مفيدة عندما لا نريد استخدام this مستقلة، ونريد أخذها من السياق الخارجي بدلًا من ذلك. سَنتعمق في موضوع الدوال السهمية لاحقًا في مقال «نظرة تفصيلية على الدوال السهمية Arrow functions». الخلاصة الدوال المخزنة في الكائنات تسمى «توابع» (methods). تسمح هذه الكائنات باستدعائها بالشكل object.doSomething()‎. يمكن للدوال الوصول إلى الكائن المعرفة فيه (أو النسخة التي استدعته المشتقة منه) باستخدام الكلمة المفتاحيةthis. تُعَرَّف قيمة this أثناء التنفيذ. قد نستخدم this عند تعريف دالة، لكنها لا تملك أي قيمة حتى استدعاء الدالة. يمكن نسخ دالة بين الكائنات. عند استدعاء دالة بالصيغة object.method()‎، فإن قيمة this أثناء الاستدعاء هي object. لاحظ أن الدوال السهمية مختلفة تتعامل تعاملًا مختلفًا مع this إذ لا تملك قيمة لها. عند الوصول إلى this بداخل دالة سهمية فإن قيمتها تؤخذ من النطاق الموجودة فيه. تمارين فحص الصياغة الأهمية: 2 ما نتيجة هذه الشيفرة؟ let user = { name: "John", go: function() { alert(this.name) } } (user.go)() ملاحظة: يوجد فخ الحل خطأ! جرب تشغيل الشيفرة: let user = { name: "John", go: function() { alert(this.name) } } (user.go)() // خطأ! لا تعطي مُعظم رسائل الخطأ في المتصفحات توضيح لسبب الخطأ. سبب الخطأ هو فاصلة منقوطة مفقودة بعد user = {...}‎. لا تقوم JavaScript بوضع فاصلة منقوطة قبل القوس ‎(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 استدعينا الدالة user.go()‎ في الشيفرة التي بالأسفل 4 مرات متتالية. لكن الاستدعاءان (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) غير معرف (obj.go || obj.stop)(); // (4) غير معرف الحل هنا التوضيح: 1- يُعد استدعاء دالة عادي. 2- مثل 1 تمامًا، لا تغير الأقواس ترتيب العمليات هنا، تعمل النقطة أولًا على أي حال. 3- هنا لدينا استدعاء أكثر تعقيدًا ‎(expression).method()‎. يعمل الاستدعاء كما لو تم فصله إلى سطرين: f = obj.go; // حساب التعبير f(); // الاستدعاء تُنَفَّذ f()‎ هنا كدالة، دون this. 4- مشابة ل (3)، لدينا تعبيرًا يسار النقطة .. لشرح سلوك الاستدعائين (3) و (4)، نحتاج لإعادة استدعاء معاملات الوصول لتلك الخاصية (النقطة أو الأقواس المربعة) التي ترجع قيمة من النوع المرجعي. أي عملية عليها عدا استدعاء الدالة (مثل التعيين = أو ||) تُرجِعُها إلى قيمة عادية، والتي لا تحمل المعلومات التي تسمح بتعيين this. استخدام this في الكائن معرَّف باختصار عبر الأقواس الأهمية: 5 تُرجِع الدالة makeUser كائنًا هنا. ما النتيجة من الدخول إلى ref الخاص بها؟ ولماذا؟ function makeUser() { return { name: "John", ref: this }; }; let user = makeUser(); alert( user.ref.name ); // ما النتيجة؟ الحل الإجابة: ظهور خطأ. جربها: function makeUser() { return { name: "John", ref: this }; }; let user = makeUser(); alert( user.ref.name ); // ِلِقيمة غير معرفة 'name' خطأ: لا يمكن قراءة الخاصية ذلك لأن القواعد التي تعين this لا تنظر إلى تعريف الكائن. ما يهم هو وقت الاستدعاء. قيمة this هنا بداخل makeUser()‎ هي undefined، لأنها استُدعيَت كدالة منفصلة، وليس كدالة بصياغة النقطة. قيمة this هي واحدة للدالة ككل، ولا تؤثر عليها أجزاء الشيفرة ولا حتى الكائنات. لذا فإن ref: this تأخذ this الحالي للدالة. هنا حالة معاكسة تمامًا: function makeUser() { return { name: "John", ref() { return this; } }; }; let user = makeUser(); alert( user.ref().name ); // John أصبحت تعمل هنا لأن user.ref()‎ هي دالة، وقيمة this تعَيَّن للكائن الذي قبل النقطة '.'. إنشاء آلة حاسِبة الأهمية: 5 أنشئ كائنًا باسم calculator يحوي الدوال الثلاث التالية: read()‎ تطلب قيمتين وتحفظها كخصائص الكائن. sum()‎ تُرجِع مجموع القيم المحفوظة. mul()‎ تضرب القيم المحفوظة وتُرجِع النتيجة. let calculator = { // ... ضع شيفرتك هنا... }; calculator.read(); alert( calculator.sum() ); alert( calculator.mul() ); الحل let calculator = { sum() { return this.a + this.b; }, mul() { return this.a * this.b; }, read() { this.a = +prompt('a?', 0); this.b = +prompt('b?', 0); } }; calculator.read(); alert( calculator.sum() ); alert( calculator.mul() ); التسلسل الأهمية: 2 لدينا الكائن ladder (سُلَّم) الذي يتيح الصعود والنزول: let ladder = { step: 0, up() { this.step++; }, down() { this.step--; }, showStep: function() { // يعرض الخطوة الحالية alert( this.step ); } }; الآن، إن أردنا القيام بعدة استدعاءات متتالية، يمكننا القيام بما يلي: ladder.up(); ladder.up(); ladder.down(); ladder.showStep(); // 1 عَدِّل الشيفرة الخاصة بالدوال up، و down، و showStep لجعل الاستدعاءات متسلسلة كما يلي: ladder.up().up().down().showStep(); // 1 يُستخدم هذا النمط بنطاق واسع في مكتبات JavaScript. الحل الحل هو إرجاع الكائن نفسه من كل استدعاء. let ladder = { step: 0, up() { this.step++; return this; }, down() { this.step--; return this; }, showStep() { alert( this.step ); return this; } } ladder.up().up().down().up().down().showStep(); // 1 يمكننا أيضا كتابة استدعاء مستقل في كل سطر ليصبح سهل القراءة بالنسبة للسلاسل الأطول: ladder .up() .up() .down() .up() .down() .showStep(); // 1 ترجمة -وبتصرف- للفصل Object methods, "this" من كتاب The JavaScript Language .task__importance { color: #999; margin-left: 30px; } .task__answer { border: 3px solid #f7f6ea; margin: 20px 0 14px; position: relative; display: block; padding: 25px 30px; } code { background-color: rgb(250, 250, 250); border-radius: 3px; } اقرأ أيضًا المقال التالي: التحويل من نوع كائن إلى نوع أولي المقال السابق: النوع الرمزي (Symbol)
  11. فلنعد الآن إلى الدوال ونرى أمرها بتمعّن وتعمّق أكثر. سنتكلم أولًا عن التعاود (Rescursion). لو كنت ذا علم بالبرمجة فالأغلب أنّك تعرف ما هذا التعاود ويمكنك تخطّي هذا الفصل. يُعدّ التعاود (Rescursion) نمطًا برمجيًا نستعمله حين يمكن تقسيم المهمة الكبيرة جدًا إلى مهام أبسط منها متشابهة، أو حين يمكن تبسيط المهمة الواحدة إلى عملية بعضها بسيط وآخر يتشابه بين بعضه، أو نستعمله (كما سنرى قريبًا) للتعامل مع أنواعٍ محدّدة من بنى البيانات. يمكن للدالة حين تحاول إجراء مهمّة ما نداءَ دوال أخرى. أحيانًا يمكن أن تنادي تلك الدالة نفسها ثانيةً. هذا ما ندعوه بالتعاود. نهجان في التطوير لنبدأ بما هو أبسط. لنكتب دالة ‎pow(x, n)‎ ترفع ‎x‎ إلى الأسّ الطبيعي ‎n‎. بعبارة أخرى، تضرب ‎x‎ بنفسه ‎n‎ مرّة. pow(2, 2) = 4 pow(2, 3) = 8 pow(2, 4) = 16 يمكننا تنفيذ هذه الدالة بطريقتين اثنتين. التفكير بالتكرار: حلقة ‎for‎: function pow(x, n) { let result = 1; // ‫نضرب الناتج في x - ‏n مرّة داخل الحلقة for (let i = 0; i < n; i++) { result *= x; } return result; } alert( pow(2, 3) ); // 8 التفكير بالتعاود: تبسيط المهمة ونداء «الذات»: function pow(x, n) { if (n == 1) { return x; } else { return x * pow(x, n - 1); } } alert( pow(2, 3) ); // 8 لاحظ كيف أنّ تلك الشيفرة التعاودية مختلفة جذريًا عن سابقتها. حين تُستدعى ‎pow(x, n)‎ تنقسم عملية التنفيذ إلى فرعين: if n==1 = x / pow(x, n) = \ else = x * pow(x, n - 1) حين ‎n == 1‎، نرى كل شيء كالعادة. نسمّي تلك الحالة بأساس التعاود، لأنها تُعطينا الناتج البديهي مباشرة: ‎pow(x, 1)‎ تساوي ‎x‎. عدى تلك فيمكننا تمثيل ‎pow(x, n)‎ على أنها ‎x * pow(x, n - 1)‎. يمكنك رياضيًا كتابة x<sup>n</sup> = x * x<sup>n-1</sup>. نسمّي هذه خطوة تعاودية: فنعدّل مهمة الأس الكبيرة لتصير عملية أبسط (الضرب في ‎x‎) ونستعمل استدعاءً أبسط لمهمة الأس (‎pow‎ ولكن ‎n‎ أقل). في الخطوات اللاحقة تصير أبسط وأبسط إلى أن تصل ‎n‎ إلى ‎1‎. يمكن أيضًا أن نقول بأن ‎pow‎ تستدعي نفسها تعاوديًا حتى تكون ‎n == 1‎. فمثلًا كي نحسب قيمة ‎pow(2, 4)‎ على التعاود إجراء هذه المهام: 1. pow(2, 4) = 2 * pow(2, 3) 2. pow(2, 3) = 2 * pow(2, 2) 3. pow(2, 2) = 2 * pow(2, 1) 4. pow(2, 1) = 2 للتلخيص، يبسّط التعاود استدعاء الدالة إلى استدعاءً آخر أبسط، وبعدها أبسط، وأبسط، وأبسط، حتّى يظهر الناتج ويصير معلومًا. غالبًا ما تكون شيفرة التعاود أقصر عادةً ما يكون الحل باستعمال التعاود أقصر من التكرار بالحلقات. يمكننا هنا مثلًا إعادة كتابة نفس الشيفرة ولكن باستعمال المُعامل الشرطي ‎?‎ بدل ‎if‎ لتصير ‎pow(x, n)‎ أقصر أكثر وتبقى مقروءةً لنا: function pow(x, n) { return (n == 1) ? x : (x * pow(x, n - 1)); } يُسمّى أقصى عدد من الاستدعاءات المتداخلة (بما في ذلك أول استدعاء) بعمق التعاود (Rescursion Depth). في حالتنا هنا سيكون هذا العمق ‎n‎. يحدّ محرّك جافاسكربت من أقصى عمق تعاودي ممكن. يمكن أن نقول بأنّ 10000 هو الحدّ الذي يمكننا الاعتماد عليه (ولو أنّ بعض المحرّكات ترفع هذا الحدّ أكثر). أجل، هناك تحسينات تلقائية تحاول رفع هذا الحدّ («تحسينات نهاية الاستدعاء») ولكنّها ليست مدعومة في كلّ مكان ولا تعمل إلّا على الحالات البسيطة. يقصّر هذا من تطبيقات استعمال التعاود، ولكنّه مع ذلك مستعمل بشدة، إذ هناك مهام كثيرة لو استعملت عليها التعاود لأعطتك شيفرة أقصر وأسهل للصيانة. سياق التنفيذ والمكدس لنرى الآن كيف يعمل التعاود أصلًا، ولذلك لا بدّ من أن نرى ما خلف كواليس الدوال هذه. تُخزّن المعلومات حول عملية تنفيذ الدالة (حين تعمل) في سياقها التنفيذي (execution context). يُعدّ سياق التنفيذ بنيةَ بيانات داخلية تحوي التفاصيل التي تخصّ عملية تنفيذ الدالة: إلى أين وصلت الآن؟ ما المتغيرات الحالية؟ ما قيمة ‎this‎ (لا نستعملها هنا) وتفاصيل أخرى داخلية. لكلّ استدعاء دالة سياق تنفيذي واحد مرتبط بها. حين تستدعي الدالة دوال أخرى متداخلة، يحدث: تتوقف الدالة الحالية مؤقتًا. يُحفظ سياق التنفيذ المرتبط بها في بنية بيانات خاصّة تسمى مكدس سياق التنفيذ execution context stack. يتنفّذ الاستدعاء المتداخل. ما إن ينتهي، يجلب المحرّك التنفيذ القديم ذاك من المكدس، وتواصل الدالة الخارجية عملها حيث توقفت. لنرى ما يحدث أثناء استدعاء ‎pow(2, 3)‎. pow(2, 3)‎ يخزّن سياق التنفيذ (في بداية استدعاء ‎pow(2, 3)‎) المتغيرات هذه: ‎x = 2, n = 3‎ وأنّ سير التنفيذ هو في السطر رقم ‎1‎ من الدالة. يمكن أن نرسمه هكذا: السياق: { x: 2, n: 3, عند السطر 1 } pow(2, 3)‎ .function-execution-context { border: 1px solid black; font-family: 'DejaVu Sans Mono', 'Lucida Console', 'Menlo', 'Monaco', monospace; padding: 4px 6px; margin: 0 4px; } .function-execution-context-call { color: gray; } هذا ما يجري حين يبدأ تنفيذ الدالة. بعد أن يصير الشرط ‎n == 1‎ خطأً، ينتقل سير التنفيذ إلى الفرع الثاني من الإفادة ‎if‎: function pow(x, n) { if (n == 1) { return x; } else { return x * pow(x, n - 1); } } alert( pow(2, 3) ); ما زالت المتغيرات كما هي، ولكن السطر تغيّر. بذلك يصير السياق الآن: السياق: { x: 2, n: 3, عند السطر 5 } pow(2, 3)‎ علينا لحساب ‎x * pow(x, n - 1)‎ استدعاء ‎pow‎ فرعيًا بالوُسطاء الجديدة ‎pow(2, 2)‎. pow(2, 2)‎ كي يحدث الاستدعاء المتداخل، يتذكّر محرّك جافاسكربت سياق التنفيذ الحالي داخل مكدس سياق التنفيذ. هنا نستدعي ذات الدالة ‎pow‎، ولكن ذلك لا يهم إذ أنّ العملية هي ذاتها لكلّ الدوال: «يتذكّر المحرّك» السياقَ الحالي أعلى المكدس. يَصنع سياقًا جديدًا للاستدعاء الفرعي. متى انتهى الاستدعاء الفرعي يُطرح (pop) السياق السابق من المكدس ويتواصل التنفيذ. هذا مكدس السياق حين ندخل الاستدعاء الفرعي ‎pow(2, 2)‎: السياق: { x: 2, n: 2, عند السطر 1 } pow(2, 2)‎ السياق: { x: 2, n: 3, عند السطر 5 } pow(2, 3)‎ سياق التنفيذ الحالي والجديد هو الأعلى (بالخط الثخين) وأسفله السياقات التي تذكّرها المحرّك سابقًا. حين ننتهي من الاستدعاء الفرعي يمكننا بسهولة بالغة مواصلة السياق السابق، إذ أنّ متغيراته ومكان توقف الشيفرة محفوظان بالضبط في السياق. ملاحظة: نرى في الصورة أنّا استعملنا كلمة «سطر» إذ ليس في المثال إلّا استدعاءً فرعيًا واحدًا في السطر، ولكن يمكن أن تحتوي الشيفرات ذات السطر الواحد (بصفة عامة) على أكثر من استدعاءً فرعيًا، هكذا: ‎pow(…) + pow(…) + somethingElse(…)‎. لذا سنكون أدقّ لو قلنا بأن عملية التنفيذ تتواصل «بعد الاستدعاء الفرعي مباشرةً». pow(2, 1)‎ تتكرّر العملية: يُصنع استدعاء فرعي جديد في السطر ‎5‎ بالوسطاء الجديدة ‎x=2‎ و ‎n=1‎. صنعنا سياقًا جديدًا، إذًا ندفع (push) الأخير أعلى المكدس: السياق: { x: 2, n: 1, عند السطر 1 } pow(2, 1)‎ السياق: { x: 2, n: 2, عند السطر 5 } pow(2, 2)‎ السياق: { x: 2, n: 3, عند السطر 5 } pow(2, 3)‎ الآن هناك سياقين اثنين قديمين، وواحد يعمل للاستدعاء ‎pow(2, 1)‎. المخرج نرى الشرط ‎n == 1‎ صحيحًا أثناء تنفيذ الاستدعاء ‎pow(2, 1)‎ (عكس ما سبقه من مرّات)، إذًا فالفرع الأول من إفادة ‎if‎ سيعمل هنا: function pow(x, n) { if (n == 1) { return x; } else { return x * pow(x, n - 1); } } لا استدعاءات متداخلة من هنا، بذلك تنتهي الدالة وتُعيد ‎2‎. وحين تنتهي الدالة لا يكون هناك حاجة لسياق التنفيذ فيُزال من الذاكرة، وبعدها يرجع السياق السابق أعلى المكدس: السياق: { x: 2, n: 2, عند السطر 5 } pow(2, 2)‎ السياق: { x: 2, n: 3, عند السطر 5 } pow(2, 3)‎ يتواصل تنفيذ الاستدعاء ‎pow(2, 2)‎، وفيه ناتج الاستدعاء الفرعي ‎pow(2, 1)‎ لذا يُنهي أيضًا تنفيذ ‎x * pow(x, n - 1)‎ فيُعيد ‎4‎. بعدها يستعيد المحرّك السياقَ السابق: السياق: { x: 2, n: 3, عند السطر 5 } pow(2, 3)‎ وحين ينتهي، يكون عندنا ناتج ‎pow(2, 3) = 8‎. عمق التعاود في هذه الحالة هو: 3. كما نرى في الصور أعلاه فعمق التعاود يساوي أقصى عدد من السياقات في المكدس. لكن اعلم بأنّ السياقات تطلب الذاكرة. في حالتنا هنا نرفع العدد للأسّ ‎n‎، وبذلك نحتاج ما يكفي من ذاكرة تسع لتخزين ‎n‎ سياق لكل القيم الأصغر من ‎n‎. خوارزميات الحلقات والتكرار أفضل من حيث استعمال الذاكرة: function pow(x, n) { let result = 1; for (let i = 0; i < n; i++) { result *= x; } return result; } تستعمل دالة ‎pow‎ المتكرّرة سياقًا واحدًا تغيّر فيه المتغيران ‎i‎ و‎result‎ أثناء عملها، كما وأنّ احتياجاتها للذاكرة قليلة وثابتة ولا تعتمد على قيمة ‎n‎. يمكن كتابة التعاودات أيًا كانت بصيغة الحلقات، وغالبًا ما تكون تلك الحلقات أفضل أداءً. …ولكن أحيانًا ما تكون إعادة الكتابة تلك تافهة وبلا قيمة حقيقية خصوصًا حين تستعمل الدالة استدعاءاتَ دوال تعاودية تختلف حسب شروط معيّنة، أو أن تدمج الدالة نتائج الاستدعاءات أو حين يصير تفريع الدالة أصعب أكثر فأكثر. حينها سيكون ذلك التحسين من المحرّك بلا داعٍ ولا حاجة لبذل كل ذلك المجهود له. هكذا يعطينا التعاود شيفرة أقصر سهلة الفهم ويمكننا دعمها بلا عناء. لا نحتاج إلى هذا «التحسين» في كلّ مكان؛ ما نريد هو فقط شيفرة جيدة، ولهذا نستعمل التعاود. مسح الأشجار تعاوديًا مسح الأشجار تعاوديًا Recursive Traversal هو تطبيق آخر عن روعة التعاود. لنقل بأنّ لدينا شركة ويمكن أن نمثّل بنية موظّفيها في هذا الكائن: let company = { sales: [{ name: 'John', salary: 1000 }, { name: 'Alice', salary: 600 }], development: { sites: [{ name: 'Peter', salary: 2000 }, { name: 'Alex', salary: 1800 }], internals: [{ name: 'Jack', salary: 1300 }] } }; أي أنّ في الشركة أقسام عدّة. يمكن أن يحتوي كل قسم على مصفوفة من العاملين. فمثلًا لقسم المبيعات ‎sales‎ عاملين اثنين: John وAlice. أو أن ينقسم القسم إلى أقسام فرعية، مثل قسم التطوير ‎development‎ له فرعان: تطوير المواقع ‎sites‎ والبرمجيات الداخلية ‎internals‎. ولكلّ من الفرعين موظفين منفصلين. يمكن أيضًا أن يكبُر القسم الفرعي ويصير فروع من القسم الفرعي (أي «فِرَق»). مثلًا قسم المبيعات ‎sites‎ سيتطوّر ويتحسّن وينقسم مستقبلًا إلى فرعين ‎siteA‎ و ‎siteB‎. وبعدها ربما (لو عمل فريق التسويق بجدّ) ينقسم أكثر أيضًا. طبعًا هذا تخيّل فقط وليس في الصورة تلك. الآن، ماذا لو أردنا دالة تعطينا مجموع كل الرواتب؟ كيف السبيل؟ لو جرّبنا بالتكرار فسيكون أمرًا عسيرًا إذ أنّ البنية ليست ببسيطة. أول فكرة على البال هي حلقة ‎for‎ تمرّ على الشركة ‎company‎ وداخلها حلقات فرعية على الأقسام بالمستوى الأول. ولكن هكذا سنحتاج حلقات فرعية متداخلة أيضًا لتمرّ على الموظفين في الأقسام بالمستوى الثاني مثل قسم ‎sites‎… وبعدها حلقات أخرى داخل تلك فوقها للأقسام بالمستوى الثالث إن عمل فريق التسويق كما يجب… لو وضعنا 3-4 من هذه الحلقات الفرعية المتداخلة في شيفرة لتعمل جولة مسح على كائن واحد، فستنتج لنا شيفرة قبيحة حقًا. لنجرّب التعاود الآن. كما رأينا، حين تُلاقي الدالة قسمًا عليها جمع رواتبه، تواجه حالتين اثنتين: إمّا يكون قسمًا «بسيطًا» فيه مصفوفة من الناس، وهكذا تجمع رواتبهم في حلقة بسيطة. أو تجد كائنًا فيه ‎N‎ من الأقسام الفرعية، حينها تصنع ‎N‎ من الاستدعاءات المتعاودة لتحصي مجموع كلّ قسم فرعي وتدمج النتائج كلها. الحالة الأولى هي أساس التعاود، أي عملنا العادي حين نستلم مصفوفة. الحالة الثانية (حين نرى كائنًا) هي خطوة في التعاود. يمكن تقسيم تلك المهمة المعقّدة إلى مهام فرعية لكلّ قسم. ربّما تنقسم تلك المهام الفرعية ثانيةً، ولكنّها عاجلًا أم آجلًا ستنتهي بالحالة (1) لا محالة. ربّما… يكون أسهل لو قرأت الخوارزمية من الشيفرة ذاتها: // الكائن كما هو، ضغطناه لألا نُطيل القصة فقط let company = { sales: [{name: 'John', salary: 1000}, {name: 'Alice', salary: 600 }], development: { sites: [{name: 'Peter', salary: 2000}, {name: 'Alex', salary: 1800 }], internals: [{name: 'Jack', salary: 1300}] } }; // الدالة التي ستنفّذ هذا العمل function sumSalaries(department) { if (Array.isArray(department)) { // ‫حالة (1) return department.reduce((prev, current) => prev + current.salary, 0); // نجمع عناصر المصفوفة } else { // ‫حالة (2) let sum = 0; for (let subdep of Object.values(department)) { // نستدعي الأقسام الفرعية تعاوديًا، ونجمع النتائج sum += sumSalaries(subdep); } return sum; } } alert(sumSalaries(company)); // 6700 الشيفرة قصيرة وسهل فهمها (كما هو أملي). هنا تظهر قوّة التعاود، فسيعمل على أيّ مستوى من الأقسام الفرعية المتداخلة. إليك رسمة توضّح الاستدعاءات: الفكرة بسيطة للغاية: لو كان كائنًا ‎{...}‎، نستعمل الاستدعاءات الفرعية، ولو كانت مصفوفات ‎[...]‎ هي آخر «أوراق» شجرة التعاود، فتعطينا الناتج مباشرةً. لاحظ كيف أنّ الشيفرة تستعمل مزايا ذكيّة ناقشناها سابقًا: التابِع ‎arr.reduce‎ في الفصل «توابِع المصفوفات» لنجمع الرواتب. الحلقة ‎for(val of Object.values(obj))‎ للمرور على قيم الكائن إذ يُعيد التابِع ‎Object.values‎ مصفوفة بالقيم. بنى التعاود بنية البيانات التعاودية (أيّ التي يحدّد أساسها التعاود) هي بنية تكرّر نفسها على أجزاء منفصلة. رأينا لتوّنا مثالًا عن هذه البنية: بنية الشركة أعلاه. القسم في الشركة هو إمّا: مصفوفة من الناس. أو كائنًا فيه أقسام أخرى. لو كنت مطوّر وِب فالأمثلة التي تعرفها وتُدركها هي مستندات HTML وXML. ففي مستندات HTML، يمكن أن يحتوي وسم HTML على قائمة من: أجزاء من نصوص. تعليقات HTML. وسوم HTML أخرى (أي ما يمكن أن يحتوي على أجزاء من نصوص أو تعليقات أو وسوم أخرى وهكذا). وهذا ما نسمّيه بالبنى التعاوديّة. لنفهم التعاود أكثر سنشرح بنية تعاود أخرى تسمّى «القوائم المترابطة» (Linked List). يمكن أن تكون هذه القوائم أحيانًا بديلًا أفضل موازنةً بالمصفوفات. القوائم المترابطة لنقل بأنّا نريد تخزين قائمة كائنات مرتّبة. ستصنع مصفوفة كالعادة: let arr = [obj1, obj2, obj3]; ولكن… هناك مشكلة تخصّ المصفوفات. عمليات «حذف العنصر» و«إدراج العنصر» مُكلفة. فمثلًا على عملية ‎arr.unshift(obj)‎ إعادة ترقيم كلّ العناصر للكائن الجديد ‎obj‎، ولو كانت المصفوفة كبيرة فستأخذ العملية وقتًا طويلًا. الأمر نفسه ينطبق لعملية ‎arr.shift()‎. التعديلات على بنية البيانات (التي لا تحتاج إلى إعادة الترقيم بالجملة) هي تلك التي تؤثّر على نهاية المصفوفة: ‎arr.push/pop‎. لذا يمكن أن تكون المصفوفة بطيئة حقًا لو كانت الطوابير طويلة حين نعمل مع المصفوفات من عناصرها الأولى. يمكننا عوض ذلك استعمال بنية بيانات أخرى لو أردنا إدخال البيانات وحذفها سريعًا. تُدعى هذه البنية بالقائمة المترابطة. يُعرّف عنصر القائمة المترابطة تعاوديًا على أنّه كائن فيه: قيمة ‎value‎. خاصية «التالي» ‎next‎ تُشير إلى عنصر القائمة المترابطة التالي أو إلى ‎null‎ لو كانت هذه نهاية القائمة. مثال: let list = { value: 1, next: { value: 2, next: { value: 3, next: { value: 4, next: null } } } }; إليك التمثيل البصري لهذه القائمة: هذه شيفرة أخرى لنصنع القائمة: let list = { value: 1 }; list.next = { value: 2 }; list.next.next = { value: 3 }; list.next.next.next = { value: 4 }; list.next.next.next.next = null; هنا نرى بوضوح أكثر كيف أنّ هناك كائنات متعدّدة لكلّ منها خاصية ‎value‎ وأخرى ‎next‎ تُشير إلى العنصر بقرب «هذا». متغيّر ‎list‎ هو أول الكائنات في السلسلة وبهذا لو اتّبعنا إشارات ‎next‎ بدءًا منها سنصل إلى أيّ عنصر آخر نريد. يمكننا قسمة القائمة إلى أجزاء عدّة ودمجها لاحقًا لو أردنا: let secondList = list.next.next; list.next.next = null; للدمج: list.next.next = secondList; وطبعًا يمكننا إدخال العناصر إلى أي مكان وإزالتها من أي مكان. فمثلًا لو أردنا إضافة قيمة جديدة للبداية فعلينا تحديث رأس القائمة: let list = { value: 1 }; list.next = { value: 2 }; list.next.next = { value: 3 }; list.next.next.next = { value: 4 }; // نُضيف قيمة جديدة إلى بداية القائمة list = { value: "new item", next: list }; ولنُزيل قيمة من المنتصف نعدّل خاصية ‎next‎ للكائن الذي يسبق الجديد: list.next = list.next.next; هكذا أجبرنا ‎list.next‎ بأن «تقفز» فوق ‎1‎ لتصل ‎2‎، بهذا استثنينا القيمة ‎1‎ من السلسلة. وطالما أنّها ليست مخزّنة في أيّ مكان آخر فستُزال من الذاكرة تلقائيًا. وعلى عكس المصفوفات فلسنا هنا نُعيد الترقيم بالجملة، أي أنّ إعادة ترتيب العناصر أسهل. بالطبع فالقوائم ليست أفضل من المصفوفات دومًا وإلا فاستعملناها هي دومًا وما احتجنا المصفوفات أبدًا. السلبية الأساس هي أنّ الوصول إلى العنصر حسب رقمه ليس سهلًا كما في المصفوفات حيث نستعمل الإشارة المباشرة ‎arr[n]‎. ولكن في القوائم علينا البدء من العنصر الأول والانتقال ‎N‎ مرّة عبر ‎next‎ لنصل إلى العنصر بالرقم N. …ولكننا لا نحتاج دومًا إلى هذه العمليات فمثلًا حين نريد طابورًا أو حتّى طابورًا متعدّد الطرفين فيجب أن نستعمل بنية مرتّبة تتيح بإضافة/إزالة العناصر من الجهتين بسرعة فائقة، وليس بالضروري أن نعرف ما في وسطها. يمكننا تحسين القوائم هكذا: إضافة الخاصية ‎prev‎ مع الخاصية ‎next‎ للإشارة إلى العنصر السابق، لننتقل وراءً بسهولة أكبر. إضافة متغيّر بالاسم ‎tail‎ يُشير إلى آخر عنصر من القائمة (وتحديثه متى أضفنا/أزلنا عناصر من النهاية). …يمكن أن تتغيّر بنية البيانات حسب متطلباتنا واحتياجاتنا. ملخص المصطلحات: التعاود* هو مصطلح برمجي يعني استدعاء دالة من داخلها. يمكن استعمال الدوال التعاودية لحلّ المهام المختلفة بطرق ذكية نظيفة. حين تستدعي الدالة نفسها نسمّي ذلك خطوة تعاود. تُعدّ وُسطاء الدالة التي تبسّط المهمّة إلى أقصى درجة بحيث لا تستدعي الدالة أيّ شيء بعدها - تُعدّ أساس التعاود. بنية البيانات التعاودية هي أيّة بنية بيانات تُحدّد نفسها بنفسها. فمثلًا يمكن تعريف القائمة المترابطة على أنّها بنية بيانات تحتوي على كائن يُشير إلى قائمة (أو يُشير إلى null). list = { value, next -> list } تُعدّ الأشجار مثل شجرة عناصر HTML أو شجرة الأقسام في هذا الفصل كائنات تعاودية بطبعها، فهي تتفرّع ولكلّ فرع فروع أخرى. يمكن استعمال الدوال التعاودية للمرور فيها كما رأينا في مثال ‎sumSalary‎. يمكن إعادة كتابة أيّ دالة تعاودية لتصير دالة تستعمل التكرار، وغالبًا ما نفعل هذا لتحسين أداء الدوال. ولكن هناك مهام عديدة يكون الحلّ التعاودي سريعًا كفايةً وأسهل كتابةً ودعمًا. تمارين اجمع كلّ الأعداد إلى أن تصل للممرّر الأهمية: 5 اكتب الدالة ‎sumTo(n)‎ التي تجمع الأعداد ‎‎1 + 2 + ... + n‎. مثال: sumTo(1) = 1 sumTo(2) = 2 + 1 = 3 sumTo(3) = 3 + 2 + 1 = 6 sumTo(4) = 4 + 3 + 2 + 1 = 10 ... sumTo(100) = 100 + 99 + ... + 2 + 1 = 5050 اكتب 3 شيفرات: واحدة باستعمال حلقة for. واحدة باستعمال التعاود، إذ أنّ ‎sumTo(n) = n + sumTo(n-1)‎ طالما ‎n > 1‎. واحدة باستعمال المتتاليات الحسابية. مثال عن الناتج: function sumTo(n) { /*... شيفرتك هنا ... */ } alert( sumTo(100) ); // 5050 ملاحظة: أيّ الشيفرات أسرع من الأخرى؟ وأيها أبطأ؟ ولماذا؟ ملاحظة أخرى: هل يمكن أن نستعمل التعاود لحساب ‎sumTo(100000)‎؟ الحل الحلّ باستعمال الحلقة: function sumTo(n) { let sum = 0; for (let i = 1; i <= n; i++) { sum += i; } return sum; } alert( sumTo(100) ); الحلّ باستعمال التعاود: function sumTo(n) { if (n == 1) return 1; return n + sumTo(n - 1); } alert( sumTo(100) ); الحلّ باستعمال المعادلة ‎sumTo(n) = n*(n+1)/2‎: function sumTo(n) { return n * (n + 1) / 2; } alert( sumTo(100) ); عن الملاحظة: بالطبع فالمعادلة هي أسرع الحلول فلا تستعمل إلا ثلاث عمليات لكلّ عدد ‎n‎. الرياضيات إلى جانبنا هنا! الشيفرة باستعمال الحلقة تأتي في المرتبة الثانية من ناحية السرعة. لو تلاحظ فنحن هنا نجمع الأعداد نفسها في الشيفرتين التعاودية والحلقية، إلّا أنّ التعاودية فيها استدعاءات متداخلة أكثر وإدارةً لمكدس التنفيذ، هذا ما يستهلك موارد أكثر فتصير الشيفرة أبطأ. عن الملاحظة الأخرى: تدعم بعض المحرّكات «تحسين نهاية الاستدعاء» (tail call optimization)، ويعني أنّه لو كان الاستدعاء التعاودي هو آخر ما في الدالة (مثلما في الدالة ‎sumTo‎ أعلاه)، فلن تواصل الدالة الخارجية عملية التنفيذ كي لا يتذكّر المحرّك سياقها التنفيذي. يتيح هذا للمحرّك إزالة قضية الذاكرة بذلك يكون ممكنًا عدّ ‎sumTo(100000)‎. ولكن، لو لم يدعم محرّك جافاسكربت هذا النوع من التحسين (وأغلبها لا تدعم) فستواجه الخطأ: تخطّيت أكبر حجم في المكدس، إذ يُفرض -عادةً- حدّ على إجمالي حجم المكدس. احسب المضروب الأهمية: 4 المضروب هو عدد طبيعي مضروب بِ‍ ‎«العدد ناقصًا واحد»‎ وثمّ بِ‍ ‎«العدد ناقصًا اثنين»‎ وهكذا إلى أن نصل إلى ‎1‎. نكتب مضروب ‎n‎ بهذا الشكل: n!‎ يمكننا كتابة تعريف المضروب هكذا: n! = n * (n - 1) * (n - 2) * ...*1 قيم المضاريب لأكثر من ‎n‎: 1! = 1 2! = 2 * 1 = 2 3! = 3 * 2 * 1 = 6 4! = 4 * 3 * 2 * 1 = 24 5! = 5 * 4 * 3 * 2 * 1 = 120 مهمّتك هي كتابة الدالة ‎factorial(n)‎ لتحسب ‎n!‎‎ باستعمال الاستدعاءات التعاودية. alert( factorial(5) ); // 120 ملاحظة وفائدة: يمكنك كتابة ‎n!‎‎ هكذا ‎n * (n-1)!‎‎ مثلًا: ‎3! = 3*2! = 3*2*1! = 6 الحل حسب التعريف فيمكن كتابة المضروب ‎n!‎‎ هكذا ‎n * (n-1)!‎. أي أنّه يمكننا حساب ناتج ‎factorial(n)‎ على أنّه ‎n‎ مضروبًا بناتج ‎factorial(n-1)‎. ويمكن أن ينخفض استدعاء ‎n-1‎ أنزل وأنزل إلى أن يصل ‎1‎. function factorial(n) { return (n != 1) ? n * factorial(n - 1) : 1; } alert( factorial(5) ); // 120 القيمة الأساس للتعاود هي ‎1‎. يمكننا أن نجعل ‎0‎ هي الأساس ولكنّ ذلك لا يهم، ليست إلا خطوة تعاود أخرى: function factorial(n) { return n ? n * factorial(n - 1) : 1; } alert( factorial(5) ); // 120 أعداد فيبوناتشي الأهمية: 5 لمتتالية فيبوناتشي الصيغة F<sub>n</sub> = F<sub>n-1</sub> + F<sub>n-2</sub>. أي أنّ العدد التالي هو مجموع العددين الذين سبقاه. أوّل عددين هما ‎1‎، وبعدها ‎‎2(1+1)‎ ثمّ ‎‎3(1+2)‎ ثمّ ‎‎5(2+3)‎ وهكذا: ‎‎1, 1, 2, 3, 5, 8, 13, 21...‎‎. ترتبط أعداد فيبوناتشي بالنسبة الذهبية وبظواهر طبيعية أخرى عديدة حولنا من كلّ مكان. اكتب الدالة ‎fib(n)‎ لتُعيد عدد فيبوناتش ‎n-th‎. مثال لطريقة عملها: function fib(n) { /* شيفرتك هنا */ } alert(fib(3)); // 2 alert(fib(7)); // 13 alert(fib(77)); // 5527939700884757 ملاحظة: يجب أن تعمل الدالة بسرعة. يجب ألا يأخذ استعداء ‎fib(77)‎ أكثر من جزء من الثانية. الحل أوّل حلّ نفكّر به هو الحلّ بالتعاود. أعداد فيبوناتشي تعاودية حسب تعريفها: function fib(n) { return n <= 1 ? n : fib(n - 1) + fib(n - 2); } alert( fib(3) ); // 2 alert( fib(7) ); // 13 // fib(77); // سيكون استدعاءً أبطأ من السلحفاة …ولكن لو كانت قيمة ‎n‎ كبيرة فسيكون بطيئًا جدًا. يمكن أن يعلّق الاستدعاء ‎fib(77)‎ محرّك جافاسكربت لفترة من الوقت بينما يستهلك موارد المعالج كاملةً. يعزو ذلك إلى أنّ الدالة تؤدّي استدعاءات فرعية كثيرة، وتُعيد تقدير (evaluate) القيم ذاتها مرارًا وتكرارًا. لنرى مثلًا جزءًا من حسابات ‎fib(5)‎: ... fib(5) = fib(4) + fib(3) fib(4) = fib(3) + fib(2) ... يمكننا أن نرى هنا بأنّ قيمة ‎fib(3)‎ مفيدة للاستدعائين ‎fib(5)‎ و‎fib(4)‎. لذا فستُستدعى ‎fib(3)‎ وتُقدّر قيمتها مرتين كاملتين منفصلتين عن بعضهما البعض. إليك شجرة التعاود كاملةً: نرى بوضوح كيف أنّ ‎fib(3)‎ تُقدّر مرتين اثنتين و‎fib(2)‎ تُقدّر ثلاث مرات. إجمالي الحسابات يزداد أسرع مما تزداد قيمة ‎n‎، ما يجعل الحسابات مهولة حين نصل ‎n=77‎. يمكننا تحسين أداء الشيفرة بتذكّر القيم التي قدّرنا ناتجها قبل الآن: لو حسبنا قيمة ‎fib(3)‎ مثلًا، فيمكننا إعادة استعمالها في أيّ حسابات مستقبلية. أو، يمكن أن نترك التعاود كله ونحاول استعمال خوارزمية مختلفة جذريًا تعتمد على الحلقات. فبدلًا من أن نبدأ بِ ‎n‎ وننطلق نحو أسفل، يمكن أن نصنع حلقة تبدأ من ‎1‎ و‎2‎ ثمّ تسجّل ناتج ذلك على أنّه ‎fib(3)‎، وناتج القيمتين السابقتين على أنّه ‎fib(4)‎ وهكذا دواليك إلى أن تصل إلى القيمة المطلوبة. هكذا لا نتذكّر في كلّ خطوة إلى قيمتين سابقتين فقط. إليك خطوات الخوارزمية الجديدة هذه بالتفصيل الممل. البداية: // ‫a = fib(1)‎، ‏b = fib(2)‎، هذه القيم حسب التعريف رقم 1 let a = 1, b = 1; // ‫نأخذ c = fib(3)‎ ليكون مجموعها let c = a + b; /* ‫لدينا الآن fib(1)‎ و fib(2)‎ و fib(3)‎ a b c 1, 1, 2 */ الآن نريد معرفة ‎fib(4) = fib(2) + fib(3)‎. لنحرّك ما في المتغيّرات إلى الجانب: ‎a,b‎ سيكونان ‎fib(2),fib(3)‎ و‎c‎ سيكون مجموعهما: a = b; // now a = fib(2) b = c; // now b = fib(3) c = a + b; // c = fib(4) /* الآن لدينا المتتابعة: a b c 1, 1, 2, 3 */ الخطوة التالية تعطينا عددًا آخر في السلسلة: a = b; // ‫الآن صار a = fib(3)‎ b = c; // ‫الآن صار b = fib(4)‎ c = a + b; // c = fib(5) /* ‫الآن لدينا المتتابعة (أضفنا عددًا آخر): a b c 1, 1, 2, 3, 5 */ …وهكذا إلى أن نصل إلى القيمة المطلوبة. وهذا أسرع بكثير من التعاود وليس فيه أيّة حسابات متكرّرة. الشيفرة كاملةً: function fib(n) { let a = 1; let b = 1; for (let i = 3; i <= n; i++) { let c = a + b; a = b; b = c; } return b; } alert( fib(3) ); // 2 alert( fib(7) ); // 13 alert( fib(77) ); // 5527939700884757 تبدأ الحلقة بالقيمة ‎i=3‎ إذ أنّ قيمتا المتتابعة الأولى والثانية مكتوبتان داخل المتغيّران ‎a=1‎ و ‎b=1‎. يُدعى هذا الأسلوب بالبرمجة الديناميكية من أسفل إلى أعلى. طباعة قائمة مترابطة لنقل بأنّ أمامنا القائمة المترابطة هذه: let list = { value: 1, next: { value: 2, next: { value: 3, next: { value: 4, next: null } } } }; اكتب الدالة ‎printList(list)‎ لتطبع لنا عناصر القائمة واحدةً واحدة. اصنع نسختين من الحل: واحدة باستعمال الحلقات وواحدة باستعمال التعاود. أيّ الحلّين أفضل؟ بالتعاود أو بدون؟ الحل نسخة الحلقات هذا الحلّ باستعمال الحلقات: let list = { value: 1, next: { value: 2, next: { value: 3, next: { value: 4, next: null } } } }; function printList(list) { let tmp = list; while (tmp) { alert(tmp.value); tmp = tmp.next; } } printList(list); لاحظ كيف استعملنا المتغير المؤقت ‎tmp‎ للمرور على عناصر القائمة. يمكننا نظريًا استعمال مُعامل الدالة ‎list‎ بدل ذلك: function printList(list) { while(list) { // (*) alert(list.value); list = list.next; } } ولكن… سنندم على ذلك لاحقًا إذ قد نحتاج إلى توسيع عمل الدالة وإجراء عملية أخرى غير هذه على القائمة، ولو بدّلنا ‎list‎ فلن نقدر على ذلك حتمًا. وعلى سيرة الحديث عن تسمية المتغيرات «كما ينبغي»، فهنا تُعدّ القائمةُ ‎list‎ ذاتَ القائمة، أي العنصر الأوّل من تلك القائمة، ويجب أن يبقى الاسم كما هو هكذا، مقروءًا وواضحًا. بينما لا يعدو دور ‎tmp‎ إلّا أداةً لمسح القائمة، تمامًا مثل ‎i‎ في حلقات ‎for‎. نسخة التعاود مفهوم النسخة التعاودية من الدالة ‎printList(list)‎ بسيط: علينا -كي نطبع قائمةً- طباعةَ العنصر الحالي ‎list‎ وتكرار ذلك على كلّ ‎list.next‎: let list = { value: 1, next: { value: 2, next: { value: 3, next: { value: 4, next: null } } } }; function printList(list) { alert(list.value); // نطبع العنصر الحالي if (list.next) { printList(list.next); // ذات الحركة لكلّ عنصر باقٍ في القائمة } } printList(list); أمّا الآن، فأيّ النسختين أفضل؟ نظريًا تُعدّ نسخة الحلقات أفضل أداءً. صحيح أنّ الاثنتين عملهما واحد إلّا أن الحلقات لا تستهلك الموارد بتداخل استدعاءات الدوال. ولكن لو نظرنا للجهة الأخرى من الكأس فالنسخة التعاودية أقصر وأسهل فهمًا أحيانًا. طباعة قائمة مترابطة بالعكس الأهمية: 5 اطبع القائمة المترابطة من التمرين السابق، ولكن بعكس ترتيب العناصر. اصنع نسختين من الحل: واحدة باستعمال الحلقات وواحدة باستعمال التعاود. الحل نسخة التعاود هنا توجد خدعة في فكرة التعاود، إذ علينا أوّلًا طباعة الباقي من القائمة وبعدها طباعة القائمة الحالية: let list = { value: 1, next: { value: 2, next: { value: 3, next: { value: 4, next: null } } } }; function printReverseList(list) { if (list.next) { printReverseList(list.next); } alert(list.value); } printReverseList(list); نسخة الحلقات نسخة الحلقات هنا أكثر تعقيدًا (بقليل) عن سابقتها. ما من طريقة لنأخذ آخر قيمة في قائمتنا ‎list‎، ولا يمكننا أن «نعود» فيها. لذا يمكننا أوّلًا المرور على العناصر بالترتيب المباشر وحِفظها في مصفوفة، بعدها طباعة ما حفظناه بالعكس: let list = { value: 1, next: { value: 2, next: { value: 3, next: { value: 4, next: null } } } }; function printReverseList(list) { let arr = []; let tmp = list; while (tmp) { arr.push(tmp.value); tmp = tmp.next; } for (let i = arr.length - 1; i >= 0; i--) { alert( arr[i] ); } } printReverseList(list); لاحظ كيف أنّ الحل باستعمال التعاود هو بالضبط كما باستعمال الحلقات، إذ يتبع القائمة ويحفظ العناصر في سلسلة من الاستدعاءات المتداخلة (في مكدس سياق التنفيذ)، وبعدها يطبع القيم. ترجمة -وبتصرف- للفصل Recursion and stack من كتاب The JavaScript language
  12. تتوقّع العديد من دوال جافاسكربت المضمّنة في اللغة عددًا من الوُسطاء لا ينتهي. مثال: ‎Math.max(arg1, arg2, ..., argN)‎ -- يُعيد أكبر وسيط من الوُسطاء. ‎Object.assign(dest, src1, ..., srcN)‎ -- ينسخ الخصائص من ‎src1..N‎ إلى ‎dest‎. …وهكذا. سنتعلّم في هذا الفصل كيف نفعل ذلك أيضًا. كما وكيف نمرّر المصفوفات إلى هذه الدوال على أنّها مُعاملات. المعاملات «البقية» ... يمكن أن ننادي الدالة بأيّ عدد من الوُسطاء كيفما كانت معرّفة الدالة. هكذا: function sum(a, b) { return a + b; } alert( sum(1, 2, 3, 4, 5) ); لن ترى أيّ خطأ بسبب تلك الوُسطاء «الزائدة». ولكن طبعًا فالنتيجة لن تأخذ بالحسبان إلا أوّل اثنين. يمكن تضمين بقية المُعاملات في تعريف الدالة باستعمال الثلاث نقاط ‎...‎ ثمّ اسم المصفوفة التي ستحتويهم. تعني تلك النقط حرفيًا «اجمع المُعاملات الباقية في مصفوفة». فمثلًا لجمع كلّ الوُسطاء في المصفوفة ‎args‎: function sumAll(...args) { // ‫اسم المصفوفة هو args let sum = 0; for (let arg of args) sum += arg; return sum; } alert( sumAll(1) ); // 1 alert( sumAll(1, 2) ); // 3 alert( sumAll(1, 2, 3) ); // 6 يمكن لو أردنا أن نأخذ المُعاملات الأولى في متغيّرات ونجمع البقية فقط. هنا نأخذ الوسيطين الأوليين في متغيرات والباقي نرميه في المصفوفة ‎titles‎: function showName(firstName, lastName, ...titles) { alert( firstName + ' ' + lastName ); // Julius Caesar // ‫الباقي نضعه في مصفوفة الأسماء titles // ‫مثلًا titles = ["Consul", "Imperator"]‎ alert( titles[0] ); // Consul alert( titles[1] ); // Imperator alert( titles.length ); // 2 } showName("Julius", "Caesar", "Consul", "Imperator"); يجب أن تُترك المُعاملات البقية إلى النهاية تجمع المُعاملات البقية كلّ الوُسطاء التي بقيت. وبهذا فالآتي ليس منطقيًا وسيتسبّب بخطأ: function f(arg1, ...rest, arg2) { // ‫الوسيط arg2 بعد ...البقية؟! // خطأ } يجب أن يكون ‎...rest‎ الأخير دومًا. متغير الوسطاء arguments هناك كائن آخر شبيه بالمصفوفات يُدعى ‎arguments‎ ويحتوي على كلّ الوُسطاء حسب ترتيب فهارسها. مثال: function showName() { alert( arguments.length ); alert( arguments[0] ); alert( arguments[1] ); // المصفوفة مُتعدَّدة // for(let arg of arguments) alert(arg); } // ‫تعرض: 2, Julius, Caesar showName("Julius", "Caesar"); // ‫تعرض: 1, Ilya, undefined (ما من مُعطى ثانٍ) showName("Ilya"); قديمًا لم تكن المُعاملات البقية موجودة في اللغة ولم يكن لدينا سوى استعمال ‎arguments‎ لنجلب كلّ مُعاملات الدالة. وما زالت تعمل الطريقة إلى يومنا هذا ويمكن أن تراها في الشيفرات القديمة. ولكن السلبية هنا هي أنّ ‎arguments‎ ليست مصفوفة (على الرغم من أنّها شبيهة بالمصفوفات ومُتعدّدة). بهذا لا تدعم توابِع المصفوفات فلا ينفع أن نستدعي عليها ‎arguments.map(...)‎ مثلًا. كما وأنّها تحتوي على كل الوُسطاء دومًا. لا يمكن أن نأخذ منها ما نريد كما نفعل مع المُعاملات البقية. لهذا متى ما احتجنا إلى ميزة كهذه، فالأفضل استعمال المُعاملات البقية بدلًا من ‎arguments‎. ليس للدوال السهمية ‎"arguments"‎ لو حاولت الوصول إلى كائن الوُسطاء ‎arguments‎ من داخل الدالة السهمية، فستستلم الناتج من الدالة «الطبيعية» الخارجية. إليك مثالًا: function f() { let showArg = () => alert(arguments[0]); showArg(); } f(1); // 1 كما نذكر فليس للدوال السهمية قيمة ‎this‎ تخصّها، أمّا الآن صرنا نعلم بأنّ ليس لها كائن ‎arguments‎ أيضًا. مُعامل التوزيع رأينا كيف نأخذ مصفوفة من قائمة من المُعطيات. ولكن ماذا لو أردنا العكس من ذلك؟ فمثلًا لنقل أردنا استعمال الدالة المبنية في اللغة Math.max والتي تُعيد أكبر عدد من القائمة: alert( Math.max(3, 5, 1) ); // 5 لنقل أنّ لدينا المصفوفة ‎[3, 5, 1]‎. كيف نستدعي ‎Math.max‎ عليها؟ لا ينفع تمريرها «كما هي» لأنّ ‎Math.max‎ يتوقّع قائمةً بالوُسطاء العددية لا مصفوفة واحدة: let arr = [3, 5, 1]; alert( Math.max(arr) ); // NaN وطبعًا لا يمكن أن نفكّ عناصر القائمة يدويًا في الشيفرة ‎Math.max(arr[0], arr[1], arr[2])‎ لأنّنا في حالات لا نعرف كم من عنصر هناك أصلًا. وما إن يتنفّذ السكربت يمكن أن يكون فيه أكبر مما كتبناه أو حتّى لا شيء أصلًا، وسنحصد لاحقًا ما جنته هذه الشيفرة. عاش مُنقذنا مُعامل التوزيع! عاش عاش عاش! من بعيد نراه مشابهًا تمامًا للمُعاملات البقية، كما ويستعمل ‎...‎، إلّا أنّ وظيفته هي العكس تمامًا. فحين نستعمل ‎‎...arr‎ في استدعاء الدالة، «يتوسّع» الكائن المُتعدَّد ‎...arr‎ إلى قائمة من الوُسطاء. فمثلًا نعود إلى ‎Math.max‎: let arr = [3, 5, 1]; // (يحوّل التوزيع المصفوفة إلى قائمة من الوُسطاء) alert( Math.max(...arr) ); // 5 يمكن أيضًا أن نمرّر أكثر من مُتعدَّد واحد بهذه الطريقة: let arr1 = [1, -2, 3, 4]; let arr2 = [8, 3, -8, 1]; alert( Math.max(...arr1, ...arr2) ); // 8 أو حتّى ندمج مُعامل التوزيع مع القيم العادية: let arr1 = [1, -2, 3, 4]; let arr2 = [8, 3, -8, 1]; alert( Math.max(1, ...arr1, 2, ...arr2, 25) ); // 25 كما يمكن أن نستعمل مُعامل التوزيعة لدمج المصفوفات: let arr = [3, 5, 1]; let arr2 = [8, 9, 15]; let merged = [0, ...arr, 2, ...arr2]; alert(merged); // ‫0,3,5,1,2,8,9,15 (0 ثمّ arr ثمّ 2 ثمّ arr2) استعملنا في الأمثلة أعلاه مصفوفة لنشرح مُعامل التوزيع، إلّا أنّ المُتعدَّدات أيًا كانت تنفع أيضًا. فمثلًا نستعمل هنا مُعامل التوزيع لنحوّل السلسلة النصية إلى مصفوفة محارف: let str = "Hello"; alert( [...str] ); // H,e,l,l,o يستعمل مُعامل التوزيع هذا داخليًا المُعدِّدات لجمع العناصر، كما تفعل حلقة ‎for..of‎. لذا لو استلمت ‎for..of‎ سلسلةً نصيّة فتُعيد لنا المحارف وتصير ‎‎...str‎ بالقيمة ‎"H","e","l","l","o"‎. وهكذا تُمرّر قائمة المحارف إلى مُهيّئ المصفوفة ‎[...str]‎. يمكننا أيضًا لهذه المهمة استعمال ‎Array.from‎ إذ أنّه يحوّل المُتعدَّد (مثل السلاسل النصية) إلى مصفوفة: let str = "Hello"; // ‫يُحوّل Array.from المُتعدَّد إلى مصفوفة alert( Array.from(str) ); // H,e,l,l,o ناتجه هو ذات ناتج ‎[‎...str]‎. ولكن… هناك فرق ضئيل بين ‎Array.from(obj)‎ و‎[...obj]‎: يعمل ‎Array.from‎ على الشبيهات بالمصفوفات والمُتعدَّدات. ويعمل مُعامل التوزيع على المُتعدَّدات فقط لا غير. لذا لو أردت تحويل شيء إلى مصفوفة فالتابِع ‎Array.from‎ أكثر استعمالًا وشيوعًا. ملخص متى رأينا ‎"..."‎ في الشيفرة نعرف أنّه إمّا المُعاملات البقية وأمّا مُعامل التوزيع. إليك طريقة بسيطة للتفريق بينهما: حين ترى ‎...‎ موجودة في نهاية مُعاملات الدالة فهي «المُعاملات البقية» وستجمع بقية قائمة الوُسطاء في مصفوفة. وحين ترى ‎...‎ في استدعاء دالة أو ما شابهه فهو «مُعامل توزيع» يوسّع المصفوفة إلى قائمة. طُرق الاستعمال: تُستعمل المُعاملات البقية لإنشاء دوال تقبل أيّ عدد كان من الوُسطاء. يُستعمل مُعامل التوزيع لتمرير مصفوفة إلى دوال تطلب (عادةً) قائمة طويلة من الوُسطاء. كلا الميزتين تساعدك في التنقل بين القائمة ومصفوفة المُعاملات بسهولة ويُسر. يمكنك أيضًا أن ترى كل وُسطاء استدعاء الدالة «بالطريقة القديمة» ‎arguments‎ وهو كائن مُتعدَّد شبيه بالمصفوفات. ترجمة -وبتصرف- للفصل Rest parameters and spread syntax من كتاب The JavaScript language
  13. تقدّم الكائنات العمومية متغيراتَ ودوال يمكن استعمالها من أي مكان. هذه الكائنات مضمّنة في بنية اللغة أو البيئة مبدئيًا. في المتصفّحات تُدعى بالنافذة ‎window‎ وفي Node.js تُدعى بالعموميات ‎global‎ وفي باقي البيئات تُدعى بأيّ اسم مناسب يراه مطوّروها. أُضيف حديثًا الكائن ‎globalThis‎ إلى اللغة ليكون اسم قياسيًا للكائن العمومي على أن تدعمه كلّ البيئات. ولكن بعض المتصفّحات (وبالخصوص عدا Chromium Edge) لا تدعم هذا الكائن بعد، ولكن يمكنك «ترقيعه تعدّديًا» بسهولة تامة. سنستعمل هنا ‎window‎ على فرضية بأنّ البيئة هي المتصفّح نفسه. لو كنت ستشغّل السكربت الذي تكتبه في بيئات أخرى فربما تستعمل ‎globalThis‎ بدل النافذة تلك. يمكننا طبعًا الوصول إلى كافة خصائص الكائن العمومي مباشرةً: alert("Hello"); // تتطابق تمامًا مع window.alert("Hello"); يمكنك في المتصفّحات التصريح عن الدوال العمومية والمتغيرات باستعمال ‎var‎ (وليس ‎let/const‎ !) لتصير خاصيات للكائن العمومي: var gVar = 5; alert(window.gVar); // ‫5 (تصير خاصية من خاصيات الكائن العمومي) ولكن أرجوك ألا تعتمد على هذا الأمر! هذا السلوك موجود للتوافقية لا غير. تستعمل السكربتات الحديثة «وحداتَ جافاسكربت» (نشرحها في وقت لاحق) حيث لا يحدث هكذا أمر. لن يحدث هذا لو استعملنا ‎let‎ هنا: let gLet = 5; alert(window.gLet); // ‫غير معرّف (لا تصير خاصية للكائن العمومي) لو كانت القيمة هامّة جدًا جدًا وأردت أن تدخل عليها من أيّ مكان عمومي فاكتبها على أنّها خاصية مباشرةً: // نجعل من معلومات المستخدم الحالي عمومية لتصل إليها كلّ السكربتات window.currentUser = { name: "John" }; // وفي مكان آخر يريدها أحد alert(currentUser.name); // John // ‫أو (لو كان هناك المتغير المحلي ذا الاسم «currentUser» // فنأخذها جهارةً من النافذة (وهذا آمن!) alert(window.currentUser.name); // John نختم هنا بأنّ استعمال المتغيرات العمومية غير محبّذ بالمرة ويجب أن يكون عددها بأقل ما يمكن. يُعدّ مبدأ تصميم الشيفرات حين تأخذ الدالة المتغيرات «الداخلة» وتُعطينا «نواتج» معيّنة - يُعدّ هذا المبدأ أفضل وأقلّ عُرضة للأخطاء وأسهل للاختبار موازنةً بالمتغيرات الخارجية أو العمومية. استعمالها للترقيع تعدديًا المجال الذي نستعمل الكائنات العمومية فيه هو اختبار لو كانت البيئة تدعم مزايا اللغة الحديثة. فمثلًا يمكننا اختبار لو كانت كائنات الوعود ‎Promise‎ المضمّنة في اللغة مضمّنة حقًا (لم تكن كذلك في المتصفحات العتيقة): if (!window.Promise) { alert("Your browser is really old!"); // ‫تستعمل يا صاح متصفّحا من زمن الطيبين! } لو لم نجد هذه الكائنات (مثلًا نستعمل متصفّحًا قديمًا) فيمكننا «ترقيعه تعدّديًا»: أي إضافة الدوال التي لا تدعمها البيئة بينما هي موجودة في معيار اللغة الحديث. if (!window.Promise) { window.Promise = ... // شيفرة نكتبها بنفسنا تؤدّي الميزة الحديثة في اللغة هذه } ملخص يحمل الكائن العمومي تلك المتغيرات التي يلزم أن نصل إليها أينما كنّا في الشيفرة. تشمل المتغيرات هذه كل ما هو مضمّن في بنية لغة جافاسكربت مثل المصفوفات ‎Array‎ والقيم المخصّصة للبيئة مثل ‎window.innerHeight‎ (ارتفاع نافذة المتصفّح). للكائن العمومي اسم عام في المواصفة: ‎globalThis‎. ولكن… دومًا ما نُشير إليه بالأسماء «الأثرية» حسب كل بيئة مثل ‎window‎ (في المتصفحات) و‎global‎ (في Node.js)، إذ أنّ ‎globalThis‎ هو مُقترح جديد على اللغة وليس مدعومًا في المتصفّحات عدة Chromium Edge (ولكن يمكننا ترقيعه تعدّديًا). علينا ألا نخزّن القيم في الكائن العمومي إلّا لو كانت حقًا وفعلًا عمومية للمشروع الذي نعمل عليه. كما ويجب أن يبقى عددها بأقل ما يمكن. حين نطوّر لاستعمال الشيفرات في المتصفّحات (لو لم نستعمل الوحدات)، تصير الدوال العمومية والمتغيرات باستعمال ‎var‎ خاصيات للكائن العمومي. علينا استعمال خاصيات الكائن العمومي مباشرةً (مثل ‎window.x‎) لتكون الشيفرة سهلة الصيانة مستقبلًا وأسهل فهمًا. ترجمة -وبتصرف- للفصل Global object من كتاب The JavaScript language
  14. ذكرنا في أوائل الفصول حين تكلمنا عن المتغيرات - ذكرنا ثلاث طرائق للتصريح عنها: let const var تتصرّف كلا الإفادتين ‎let‎ و‎const‎ بذات الطريقة (بالمقايسة مع البيئات المُعجمية). بينما ‎var‎ فهو وحش آخر مختلف جذريًا ويعود في أصله إلى قرون سحيقة. لا نستعمله عادةً في السكربتات الحديثة ولكنّك ستجده حتمًا خلف إحدى صخور السكربتات القديمة. لو لم ترغب بالتعرّف على هذه السكربتات فيمكنك تخطّي هذا الفصل أو تأجيله لوقت لاحق. ولكن لا تنسَ احتمالية ندمك لاحقًا فيغدر بك هذا الوحش. من أول وهلة نرى بأنّ تصرّف ‎var‎ يشابه تصرّف ‎let‎، أي أنّه يُصرّح (مثل الثاني) عن متغير: function sayHi() { // ‫متغير محلي، استعملنا «var» بدل «let» var phrase = "Hello"; alert(phrase); // Hello } sayHi(); alert(phrase); // ‫خطأ، phrase غير معرّف ولكن… ما خفي كان أعظم. إليك الفروق. ليس لإفادة var نطاقًا كتليًا حين نصرّح عن المتغيرات باستعمال ‎var‎ نكون جعلناها معروفة للدالة كاملةً (لو كانت في دالة) أو عمومية في السكربت. يمكنك أن ترى تلك المتغيرات إن اخترقت «جدران» الكُتل. مثال: if (true) { var test = true; // ‫نستعمل «var» بدل «let» } alert(test); // ‫الناتج true، أي أنّ المتغير «حيّ يُرزق» بعد إفادة if تجاهل ‎var‎ كتل الشيفرة، وبهذا صار متغير ‎test‎ عموميًا. لو استعملنا ‎let test‎ بدل ‎var test‎ فسيكون المتغير ظاهرًا لباقي الشيفرة داخل إفادة ‎if‎ فقط لا غير: if (true) { let test = true; // ‫نستعمل «let» } alert(test); // ‫خطأ: لم يُعرّف عن test يسري الأمر ذاته على الحلقات فلا يمكن أن يكون ‎var‎ محليًا حسب الكتلة أو حسب الحلقة: for (var i = 0; i < 10; i++) { // ... } alert(i); // ‫10، ظهر «i» بعد الحلقة فهو متغير عمومي لو كتبت كتلة شيفرة في دالة فسيصير ‎var‎ متغيرًا على مستوى الدالة كاملةً. function sayHi() { if (true) { var phrase = "Hello"; } alert(phrase); // يمكننا فعل هذا } sayHi(); alert(phrase); // ‫خطأ: phrase غير معرّف (طالِع مِعراض المطوّر) كما نرى فإفادة ‎var‎ تخترق كُتل ‎if‎ و‎for‎ وغيرها من كُتل شيفرة. يعزو ذلك إلى أنّه في الزمن الماضي الجميل لم تكن لكُتل جافاسكربت بيئات مُعجمية. و‎var‎ إحدى آثار ذلك الزمن. تعالج التصريحات باستعمال ‎var‎ عند بدء الدالة تُعالج التصريحات باستعمال ‎var‎ متى ما بدأت الدالة (أو بدأ السكربت، للمتغيرات العمومية). أي أنّ متغيرات ‎var‎ تُعرّف من بداية الدالة مهما كان مكان تعريفها (هذا لو لم يكن التعريف في دالة متداخلة أخرى). يعني ذلك أنّ هذه الشيفرة: function sayHi() { phrase = "Hello"; alert(phrase); var phrase; } sayHi(); متطابقة تقنيًا مع هذه (بتحريك ‎var phrase‎ إلى أعلى): function sayHi() { var phrase; phrase = "Hello"; alert(phrase); } sayHi(); أو حتى هذه (لا تنسَ بأنّ كُتل الشيفرات مُهملة): function sayHi() { phrase = "Hello"; // (*) if (false) { var phrase; } alert(phrase); } sayHi(); يدعو الناس هذا السلوك بسلوك «الطفو» hoisting (أو الرفع) إذ أنّ متغيرات ‎var‎ «تطفو» إلى أعلى الدالة (أو ترتفع إلى أعلاها). أي أنّه في المثال أعلاه، الفرع ‎if (false)‎ من الإفادة لا يعمل قط ولكن هذا ليس بمهم، إذ أنّ ‎var‎ داخله سيُعالج في بداية الدالة، وحين تصل عملية التنفيذ إلى ‎(*)‎ سيكون المتغير موجودًا لا محالة. التصريحات تطفو صحيح، ولكنّ ليس عبارات الإسناد. الأفضل لو نمثّل ذلك في هذا المثال: function sayHi() { alert(phrase); var phrase = "Hello"; } sayHi(); في السطر ‎var phrase = "Hello"‎ إجراءان اثنان: التصريح عن المتغير باستعمال var إسناد قيمة للمتغير باستعمال ‎=‎. يتعامل المحرّك مع التصريحات متى بدء تنفيذ الدالة (إذ التصريحات تطفو)، ولكنّ عبارة الإسناد لا تعمل إلّا حيثما ظهرت، فقط. إذًا فالشيفرة تعمل بهذا النحو فعليًا: function sayHi() { var phrase; // ‫بادئ ذي بدء، يعمل التصريح... alert(phrase); // غير معرّف phrase = "Hello"; // ‫...هنا. } sayHi(); يُعالج المحرّك التصريحات ‎var‎ حين تبدأ الدوال، وبهذا يمكننا الإشارة إليها أينما أردنا في الشيفرة. ولكن انتبه فالمتغيرات غير معرّفة حتى تُسند إليها قيم. في الأمثلة أعلاه عمل التابِع ‎alert‎ دون أيّ أخطاء إذ أن المتغير ‎phrase‎ موجود. ولكن لم تُسند فيه قيمة بعد فعرض ‎undefined‎. ملخص هناك فرقين جوهرين بين ‎var‎ موازنةً بِـ ‎let/const‎: ليس لمتغيرات ‎var‎ نطاقًا كتليًا وأصغر نطاق لها هو في الدوال. تُعالج التصريحات باستعمال ‎var‎ عند بدء الدالة (أو بدء السكربت، للمتغيرات العمومية). هناك فرق آخر صغير يتعلّق بالكائن العمومي وسنشرحه في الفصل التالي. بهذا، غالبًا ما يكون استعمال ‎var‎ أسوأ بكثير من ‎let‎ بعدما عرفت الفروق بينها، فالمتغيرات على مستوى الكُتل أمر رائع جدًا ولهذا السبب تمامًا أُضيفت ‎let‎ إلى معيار اللغة منذ زمن وصارت الآن الطريقة الأساسية (هي و‎const‎) للتصريح عن متغير. ترجمة -وبتصرف- للفصل The old "var"‎ من كتاب The JavaScript language
  15. لغة جافاسكربت هي لغة داليّة التوجّه إلى أقصى حدّ، فتعطينا أقصى ما يمكن من حريّة. يمكننا إنشاء الدوال ديناميكيًا ونسخها إلى متغيرات أخرى أو تمريرها كوسيط إلى دالة أخرى واستدعائها من مكان آخر تمامًا لاحقًا حين نريد. كما نعلم بأنّ الدوال تستطيع الوصول إلى المتغيرات خارجها. نستعمل هذه الميزة كثيرًا. ولكن، ماذا يحدث حين يتغيّر المتغيّر الخارجي؟ هل تستلم الدالة أحدث قيمة له أو تلك التي كانت موجودة لحظة إنشاء الدالة؟ كما وماذا يحدث حين تنتقل الدالة إلى مكان آخر في الشيفرة واستُدعت من ذلك المكان: هل يمكنها الوصول إلى المتغيرات الخارجية في المكان الجديد؟ يختلف سلوك اللغات عن بعضها من هذه الناحية. في هذا الفصل سنتحدّث عن سلوك جافاسكربت. أسئلة تحتاج أجوبة لنبدأ أولًا بحالتين اثنتين ندرس بهما الآلية الداخلية للغة خطوةً بخطوة، بهذا ستملك ما يكفي لتُجيب على الأسئلة الآتية وأخرى غيرها أكثر تعقيدًا في المستقبل. تستعمل الدالة ‎sayHi‎ المتغير الخارجي ‎name‎. ما القيمة التي ستستعملها الدالة حين تعمل؟ let name = "John"; function sayHi() { alert("Hi, " + name); } name = "Pete"; sayHi(); // ‫ماذا ستعرض؟ «John» أم «Pete»؟ يشيع وجود هذه الحالات في المتصفّحات كما وفي الخواديم عند التطوير. يمكن أن تعمل الدالة بعدما تُنشأ بفترة لو أراد المطوّر (مثلًا بعد أن يتفاعل المستخدم أو يستلم المتصفّح طلبًا من الشبكة). إذًا فالسؤال هو: هل تستعمل آخر التعديلات؟ تصنع الدالة ‎makeWorker‎ دالةً أخرى وتُعيدها، ويمكن أن نستعدي تلك الدالة الجديدة من أيّ مكان آخر نريد. السؤال هو: هل يمكنها الوصول إلى المتغيرات الخارجية تلك التي من مكان إنشائها الأصلي، أم تلك التي في المكان الجديد، أم من المكانين معًا؟ function makeWorker() { let name = "Pete"; return function() { alert(name); }; } let name = "John"; // نصنع الدالة let work = makeWorker(); // نستدعيها work(); ‫ماذا ستعرض الدالة work()‎؟ «Pete» (الاسم الذي تراه عند الإنشاء) أم «John» (الاسم الذي تراه عند الاستدعاء)؟ البيئات المعجمية علينا أولًا أن نعرف ما هو «المتغير» هذا أصلًا لنُدرك ما يجري بالضبط. في لغة جافاسكربت، تملك كلّ دالة عاملة أو كتلة شفرات ‎{...}‎ أو حتّى السكربت كلّه - تملك كائنًا داخليًا مرتبطًا بها (ولكنّه مخفي) يُدعى بالبيئة المُعجمية Lexical Environment. تتألّف كائنات البيئات المُعجمية هذه من قسمين: سجلّ مُعجمي Environment Record: وهو كائن يخزّن كافة المتغيرات المحلية على أنّها خاصيات له (كما وغيرها من معلومات مثل قيمة ‎this‎). إشارة إلى البيئة المُعجمية الخارجية - أي المرتبطة مع الشيفرة الخارجية للكائن المُعجمي. ليس «المتغير» إلا خاصية لإحدى الكائنات الداخلية الخاصة: السجل المُعجمي ‎Environment Record‎. وحين نعني «بأخذ المتغير أو تغيير قيمته» فنعني «بأخذ خاصية ذلك الكائن أو تغيير قيمتها». إليك هذه الشيفرة البسيطة مثالًا (فيها بيئة مُعجمية واحدة فقط): هذا ما نسمّيه البيئة المُعجمية العمومية (global) وهي مرتبطة بالسكربت كاملًَا. نعني بالمستطيل (في الصورة أعلاه) السجل المُعجمي (أي مخزن المتغيرات)، ونعني بالسهم الإشارة الخارجية له. وطالما أنّ البيئة المُعجمية العمومية ليس لها إشارة خارجية، فذاك السهم يُشير إلى ‎null‎. وهكذا تتغيّر البيئة حين تعرّف عن متغيّر وتُسند له قيمة: نرى في المستطيلات على اليمين كيف تتغيّر البيئة المُعجمية العمومية أثناء تنفيذ الشيفرة: حين تبدأ الشيفرة، تكون البيئة المُعجمية فارغة. بعدها يظهر التصريح ‎let phrase‎، لكن لم تُسند للمتغيّر أيّ قيمة، لذا تخزّن البيئة ‎undefined‎. تُسند للمتغير ‎phrase‎ قيمة. وهنا تتغيّر قيمة ‎phrase‎. بسيط حتّى الآن، أم لا؟ نلخّص الموضوع: المتغير هو فعليًا خاصية لإحدى الكائنات الداخلية الخاصة، وهذا الكائن مرتبط بالكتلة أو الدالة أو السكربت الذي يجري تنفيذه حاليًا. حين نعمل مع المتغيرات نكون في الواقع نعمل مع خصائص ذلك الكائن. التصريح بالدوال لم نرى حتّى اللحظة إلا المتغيرات. حان وقت التصريحات بالدوال. الدوال على عكس متغيرات ‎let‎، فليست تُهيّأ تمامًا حين تصلها عملية التنفيذ، لا، بل قبل ذلك حين تُنشأ البيئة المُعجمية. وحين نتكلم عن أعلى الدوال مستوًى، فنعني ذلك لحظة بدء السكربت. ولهذا السبب يمكننا استدعاء الدوال التي صرّحناها حتّى قبل أن نرى ذاك التعريف. نرى في الشيفرة أدناه كيف أنّ البيئة المُعجمية تحتوي شيئًا منذ بداية التنفيذ (وليست فارغة)، وما تحتويه هي ‎say‎ إذ أنّها تصريح عن دالة. وبعدها تسجّل ‎phrase‎ المُصرّح باستعمال ‎let‎: البيئات المُعجمية الداخلية والخارجية الآن لنتعمّق ونرى ما يحدث حين تحاول الدالة الوصول إلى متغير خارجي. تستعمل ‎say()‎ أثناء الاستعداء المتغير الخارجي ‎phrase‎. لنرى تفاصيل ما يجري بالضبط. تُنشأ بيئة مُعجمية تلقائيًا ما إن تعمل الدالة وتخزّن المتغيرات المحلية ومُعاملات ذلك الاستدعاء فمثلًا هكذا تبدو بيئة استدعاء ‎say("John")‎ (وصل التنفيذ السطر الذي عليه سهم): let phrase = "Hello"; function say(name) { alert( `‎${phrase}, ${name}‎` ); } say("John"); // Hello, John إذًا… حين نكون داخل استدعاءً لأحد الدوال نرى لدينا بيئتين مُعجميتين: الداخلية (الخاصة باستدعاء الدالة) والخارجية (العمومية): ترتبط البيئة المُعجمية الداخلية مع عملية التنفيذ الحالية للدالة ‎say‎. تملك خاصية واحدة فقط: ‎name‎ (وسيط الدالة). ونحن استدعينا ‎say("John")‎ بهذا تكون قيمة ‎name‎ هي ‎"John"‎. البيئة المُعجمية الخارجية وهي هنا البيئة المُعجمية العمومية. تملك متغير ‎phrase‎ والدالة ذاتها. للبيئة المُعجمية الداخلية إشارة إلى تلك «الخارجية». حين تريد الشيفرة الوصول إلى متغير من المتغيرات، يجري البحث أولًا في البيئة المُعجمية الداخلية، وبعدها الخارجية، والخارجية أكثر وأكثر وكثر حتى نصل العمومية. لو لم يوجد المتغير في عملية البحث تلك فسترى خطأً (لو استعملت النمط الصارم Strict Mode). لو لم تستعمل ‎use strict‎ فسيُنشئ الإسناد إلى متغير غير موجود (مثل ‎user = "John"‎) متغيرًا عموميًا جديدًا باسم ‎user‎. سبب ذلك هو التوافق مع الإصدارات السابقة. لنرى عملية البحث تلك في مثالنا: حين تحاول ‎alert‎ في دالة ‎say‎ الوصول إلى المتغير ‎name‎ تجده مباشرةً في البيئة المُعجمية للدالة. وحين تحاول الوصول إلى متغير ‎phrase‎ ولا تجده محليًا، تتبع الإشارة في البيئة المحلية وتصل البيئة المُعجمية خارجها، وتجد المتغير فيها. يمكننا أخيرًا تقديم إجابة على السؤال في أول الفصل. تأخذ الدالة المتغيرات الخارجية من مكانها الآن، أي أنها تستعمل أحدث القيم. القيم القديمة لا تُحفظ في أي مكان مهما بحثت. فحين تريد إحدى الدوال متغيرًا ما تأخذ قيمته الحالية من بيئتها المُعجمية هي أو الخارجية بالنسبة لها. إذًا، إجابة السؤال الأول هي ‎Pete‎: let name = "John"; function sayHi() { alert("Hi, " + name); } name = "Pete"; // (*) sayHi(); // (*) Pete سير تنفيذ الشيفرة أعلاه: للبيئة المُعجمية العمومية ‎name: "John"‎. في السطر ‎(*)‎ يتغيّر المتغير العمومي ويصير الآن ‎name: "Pete"‎. تأخذ الدالة ‎sayHi()‎ حين تتنفّذ قيمة ‎name‎ من الخارج (أي البيئة المُعجمية العمومية) حيث صارت الآن ‎"Pete"‎. لكلّ استدعاء منك، بيئة مُعجمية من اللغة لاحظ بأنّ محرّك اللغة يُنشئ بيئة مُعجمية جديدة للدالة في كلّ مرة تعمل فيها الدالة. ولو استدعيت الدالة أكثر من مرة فلكلّ استدعاء منها بيئة مُعجمية خاصة بها مستقلة المتغيرات المحلية والمُعاملات، ومخصّصة فقط لذلك الاستدعاء. البيئات المُعجمية كائن في توصيف اللغة كائن «البيئة المُعجمية» (Lexical Environment) هو كائن في توصيف اللغة، أي أنّه موجود «نظريًا» فقط في توصيف اللغة لشرح طريقة عمل الأمور، ولا يمكننا أخذ هذا الكائن في الشيفرة ولا التعديل عليه مباشرةً. كما يمكن أن تُحسّن محرّكات جافاسكربت هذا الكائن أو تُهمل المتغيرات غير المستخدمة فتحفظ الذاكرة أو غيرها من خُدع داخلية، كلّ هذا بمنأًى عن السلوك الظاهر لنا فيظلّ كما هو. الدوال المتداخلة تكون الدالة «متداخلة» متى صنعتها داخل دالة أخرى. ويمكنك بسهولة بالغة فعل ذلك داخل جافاسكربت. يمكننا استعمال هذه الميزة لتنظيم الشيفرة الإسباغيتية، هكذا: function sayHiBye(firstName, lastName) { // دالة مساعدة متداخلة نستعملها أسفله function getFullName() { return firstName + " " + lastName; } alert( "Hello, " + getFullName() ); alert( "Bye, " + getFullName() ); } صنعنا هنا الدالة المتداخلة ‎getFullName()‎ لتسهّل حياتنا علينا، فيمكنها هي الوصول إلى المتغيرات الخارجية وإعادة اسم الشخص الكامل. كثيرًا ما نستعمل الدوال المتداخلة في جافاسكربت. والممتع أكثر هو أنّه يمكننا إعادة الدوال المتداخلة، إمّا باعتبارها خاصية لكائن جديد (لو كانت الدالة الخارجية تصنع كائنًا له توابع) أو أن تكون نتيجةً للدالة مستقلة بذاتها. ويمكننا لاحقًا استعمالها أينما أردنا، وأيّما كان مكانها الجديد فلديها الحقّ بالوصول إلى ذات المتغيرات الخارجية تلك. مثال على ذلك: أسندنا الدالة المتداخلة إلى كائن جديد باستعمال دالة مُنشئة: // تُعيد الدالة المُنشئة كائنًا جديدًا function User(name) { // نصنع تابِع الكائن على أنّه دالة متداخلة this.sayHi = function() { alert(name); }; } let user = new User("John"); // ‫يمكن أن تصل شيفرة تابِع الكائن «sayHi» إلى «name» الخارجي user.sayHi(); وهنا أنشأنا دالة «عدّ» وأعدناها، لا أكثر: function makeCounter() { let count = 0; return function() { // ‫يمكنها الوصول إلى متغير «count» الخارجي return count++; }; } let counter = makeCounter(); alert( counter() ); // 0 alert( counter() ); // 1 alert( counter() ); // 2 لنتفحّص مثال ‎makeCounter‎. تصنع الشيفرة دالة «العدّ» وتُعيد العدد التالي كلّما استدعيناها. صحيح أنّ الدالة بسيطة لكن بتعديلها قليلًا يمكن استعمالها لأمور عديدة مفيدة مثل مولّدات الأعداد شبه العشوائية وغيرها. ولكن كيف يعمل هذا العدّاد داخليًا؟ متى ما عملت الدالة الداخلية، تبدأ بالبحث عن المتغير في ‎count++‎ بدءًا منها وانطلاقًا إلى خارجها. فهكذا سيكون الترتيب في المثال أعلاه: المتغيرات المحلية للدالة المتداخلة… المتغيرات المحلية للدالة الخارجية… وهكذا حتى نصل المتغيرات العمومية. في هذا المثال وجدنا المتغير ‎count‎ في الخطوة الثانية. فلو عُدّلت قيمة المتغير الخارجي فيحدث هذا التعديل في المكان الذي وجدنا المتغير فيه. لهذا تجد ‎count++‎ المتغير الخارجي وتزيد قيمته في البيئة المُعجمية التي ينتمي المتغير إليها، تمامًا كما لو استعملنا ‎let count = 1‎. إليك سؤالين تفكّر بهما (أيضًا): هل يمكننا بطريقة أو بأخرى تصفير العدّاد ‎count‎ من الشيفرة التي لا تنتمي إلى ‎makeCounter‎؟ مثلًا بعد استدعاءات ‎alert‎ في المثال أعلاه. حين نستعدي ‎makeCounter()‎ أكثر من مرة تُعيد لنا دوال ‎counter‎ كثيرة. فهل هي مستقلة بذاتها أم تتشارك ذات متغير ‎count‎؟ حاول حلّ السؤالين قبل مواصلة القراءة. انتهيت؟ إذًا حان وقت الإجابات. ما من طريقة أبدًا: متغير ‎count‎ هو متغير محلي داخل إحدى الدوال ولا يمكننا الوصول إليه من الخارج. كلّ استدعاء من ‎makeCounter()‎ يصنع بيئة مُعجمية جديدة للدالة لها متغير ‎count‎ خاص بها. لذا فدوال ‎counter‎ الناتج مستقلة عن بعضها البعض. إليك شيئًا تجربّه بنفسك: function makeCounter() { let count = 0; return function() { return count++; }; } let counter1 = makeCounter(); let counter2 = makeCounter(); alert( counter1() ); // 0 alert( counter1() ); // 1 alert( counter2() ); // ‫0 (مستقل) آمل بأنّ الصورة الآن صارت أوضح أكثر. صحيح أنّا تكلمنا فقط عن المتغيرات الخارجية ولكن إدراك هذا القدر فقط يكفي أغلب الأحيان. هناك طبعًا تفاصيل أخرى في مواصفات اللغة لم نتحدّث عنها للإيجاز. في القسم التالي سنتكلم عن هذه التفاصيل أكثر. البيئات بالتفصيل الممل إليك ما يجري في مثال ‎makeCounter‎ خطوةً بخطوة. احرص على اتباعه لتحرص على فهم آلية عمل البيئات بالتفصيل. لاحظ أنّا شرحنا الخاصية الإضافية ‎[[Environment]]‎ هنا، ولم نشرحها سابقًا للتبسيط. حين يبدأ السكربت لا يكون هناك إلى بيئة مُعجمية عمومية: في تلك اللحظة ليس هناك إلا دالة ‎makeCounter‎ إذ أنها تصريح عن دالة، ولم يبدأ تشغيلها بعد. تستلم كافة الدوال «لحظة إفاقتها للحياة» خاصية مخفية باسم ‎[[Environment]]‎ فيها إشارة إلى البيئة المُعجمية حيث أُنشئت. لم نتحدّث عن هذه قبلًا، وهي الطريقة التي تعرف الدالة فيها مكان صناعتها الأولي. هنا أُنشأت ‎makeCounter‎ في البيئة المُعجمية العمومية، ولهذا فتُبقي ‎[[Environment]]‎ إشارة إليها. أي وبعبارة أخرى، «نطبع» على الدالة إشارةً للبيئة المُعجمية التي نشأت فيها، وخاصية ‎[[Environment]]‎ هي الخاصية الدالية المخفية التي تسجّل تلك الإشارة. تبدأ أخيرًا الشيفرة بالعمل، ويرى المحرّك متغيرا عموميًا جديدًا بالاسم ‎counter‎ صرّحنا عنه وقيمته هي ناتج استعداء ‎makeCounter()‎. إليك نظرة على اللحظة التي تكون فيها عملية التنفيذ على أول سطر داخل ‎makeCounter()‎: تُنشأ بيئة مُعجمية لحظة استدعاء ‎makeCounter()‎ لتحمل متغيراتها ومُعاملاتها. وكما الحال مع البيئات هذه فهي تخزّن أمرين: سجلّ بيئي فيه المتغيرات المحلية. في حالتنا هنا متغير ‎count‎ هو الوحيد المحلي (يظهر حين يُنفّذ سطر ‎let count‎). الإشارة إلى البيئة المُعجمية الخارجية (وتُضبط قيمة لخاصية ‎[[Environment]]‎ للدالة). تُشير هنا ‎[[Environment]]‎ للدالة ‎makeCounter‎ إلى البيئة المُعجمية العمومية. إذًا لدينا بيئتين مُعجميتين اثنتين: الأولى عمومية والثانية مخصّصة لاستدعاء ‎makeCounter‎ الحالي، بينما الإشارة الخارجية لها هي البيئة العمومية. تُصنع -أثناء تنفيذ ‎makeCounter()‎- دالة صغيرة متداخلة. لا يهمّنا إن كان التصريح عن الدالة أم تعبير الدالة هو من أنشأ… الدالة، فالخاصية ‎[[Environment]]‎ تُضاف لكل الدوال، وتُشير إلى البيئة المُعجمية التي صُنعت فيها تلك الدوال. وبطبيعة الحال فهذه الدالة الصغيرة المتداخلة لديها نصيب من الكعكة. قيمة الخاصية ‎[[Environment]]‎ للدالة المتداخلة هذه هي البيئة المُعجمية الحالية للدالة ‎makeCounter()‎ (مكان صناعة الدالة المتداخلة): لاحظ أنّ الدالة الداخلية (في هذه الخطوة) أُنشئت صحيح ولكن لم نستعدها بعد. الشيفرة في ‎return count++;‎ لا تعمل. تُواصل عملية التنفيذ العمل وينتهي استدعاء ‎makeCounter()‎ ويُسند ناتجها (وهو الدالة المتداخلة الصغيرة) إلى المتغير العمومي ‎counter‎: ليس لتلك الدالة إلا سطرًا واحدًا: ‎return count++‎ وسيُنفّذ ما إن نشغّل الدالة. وحين استدعاء ‎counter()‎ تُنشأ بيئة مُعجمية جديدة، لكنّها فارغة إذ أن ليس للدالة ‎counter‎ متغيرات محلية فيها، إلّا أنّ لخاصية الدالة ‎counter‎‏ ‎[[Environment]]‎ فائدة ففيها الإشارة «الخارجية» للدالة وهي التي تتيح لنا الوصول إلى متغيرات استدعاء ‎makeCounter()‎ السابق متى ما أنشأناه: أما الآن فحين يبحث الاستدعاء عن متغير ‎count‎ فهو يبحث أولًا في بيئته المُعجمية (الفارغة)، فلو لم يجدها بحث في البيئة المُعجمية لاستدعاء ‎makeCounter()‎ الخارجي، ويجد المتغير فيه. لاحظ آلية إدارة الذاكرة هنا. صحيح أنّ استدعاء ‎makeCounter()‎ انتهى قبل فترة إلا أن بيئته المُعجمية بقيت في الذاكرة لأنّ الدالة المتداخلة تحمل الخاصية ‎[[Environment]]‎ التي تُشير إلى تلك البيئة. يمكن القول بصفة عامة بأنّ البيئة المُعجمية لا تموت طالما يمكن لدالة من الدوال استعمالها. وحين لا توجد هكذا دالة - حينها تُمسح البيئة. لا يُعيد استدعاء ‎counter()‎ قيمة الخاصية ‎count‎ فحسب، بل أيضًا يزيدها واحدًا. لاحظ كيف أنّ التعديل حدث «في ذات مكانه» In place، فتعدّلت قيمة ‎count‎ في البيئة ذاتها التي وجدناه فيها. تعمل استدعاءات ‎counter()‎ التالية بنفس الطريقة. أفترض الآن بأنّ إجابة السؤال الثاني في أول الفصل ستكون جليّة. دالة ‎work()‎ في الشيفرة أدناه تأخذ الاسم ‎name‎ من مكانه الأصل عبر إشارة البيئة المُعجمية الخارجية إليه: إذًا، فالناتج هنا هو ‎"Pete"‎. ولكن لو لم نكتب ‎let name‎ في ‎makeWorker()‎ فسينتقل البحث إلى خارج الدالة تلك ويأخذ القيمة العمومية كما نرى من السلسلة أعلاه. في تلك الحالة سيكون الناتج ‎"John"‎. المنغلقات هناك مصطلح عام يُستعمل في البرمجة باسم «المُنغلِق» Clousure ويُفترض أن يعلم به المطوّرون. المُنغِلق هو دالة تتذكّر متغيراتها الخارجية كما ويمكنها أن تصل إليها. هذا الأمر -في بعض اللغات- مستحيل، أو أنّه يلزم كتابة الدالة بطريقة معيّنة ليحدث ذلك. ولكن كما شرحنا أعلاه ففي لغة جافاسكربت، كلّ الدوال مُنغلِقات بطبيعتها (وطبعًا ثمّة استثناء واحد أوحد نشرحه في فصل «صياغة الدالة الجديدة»). يعني ذلك بأنّ الدوال تتذكّر أين أُنشئت باستعمال خاصية ‎[[Environment]]‎ المخفية، كما ويمكن للدوال كافة الوصول إلى متغيراتها الخارجية. لو كنت عزيزي مطوّر الواجهات في مقابلةً وأتاك السؤال «ما هو المُنغلِق؟» فيمكنك أن تقدّم تعريفه شرحًا، كما وتُضيف بأنّ الدوال في جافاسكربت كلّها مُنغلِقات، وربما شيء من عندك تفاصيل تقنية مثل خاصية ‎[[Environment]]‎ وطريقة عمل البيئات المُعجمية. كُتل الشفرات والحلقات، تعابير الدوال الآنية ركّزتُ في الأمثلة أعلاه على الدوال، إلا أنّ البيئة المُعجمية موجودة لكلّ كتلة شيفرات ‎{...}‎. تُنشأ البيئة المُعجمية حين تعمل أيّ كتلة شيفرات فيها متغيرات تُعدّ محلية لهذه الكتلة. إليك بعض الأمثلة. الجملة الشرطية If في المثال أسفله نرى المتغير ‎user‎ موجودًا فقط داخل كتلة ‎if‎: let phrase = "Hello"; if (true) { let user = "John"; alert(`‎${phrase}, ${user}‎`); // Hello, John } alert(user); // ‫خطأ! لا أرى هذا المتغير! حين تصل عملية التنفيذ داخل كتلة ‎if‎ يُنشئ المحرك البيئة المُعجمية «فقط وفقط إذا كذا…». لهذه البيئة إشارة إلى البيئة الخارجية، بهذا يمكن أن تجد المتغير ‎phrase‎. ولكن على العكس فالمتغيرات وتعابير الدوال المصرَّح عنها داخل ‎if‎ في تلك البيئة المُعجمية لا يمكن أن نراها من الخارج. فمثلًا بعدما تنتهي إفادة ‎if‎ لن يرى التابِع ‎alert‎ أسفلها متغير ‎user‎، وهذا سبب الخطأ. حلقة «كرّر طالما» لكلّ دورة في حلقة التكرار بيئة مُعجمية خاصة بها. وأيضًا لو صرّحت عن متغير في ‎for(let ...)‎ فسيكون موجودًا فيها: for (let i = 0; i < 10; i++) { // لكلّ دورة بيئة مُعجمية خاصة بها // {i: value} } alert(i); // خطأ، ما من متغير كهذا لاحظ كيف أنّ الإفادة ‎let i‎ خارج كتلة ‎{...}‎ بصريًا. مُنشئ حلقة ‎for‎ خاص نوعًا ما: لكلّ دورة من الحقة بيئة مُعجمية خاصة بها تحمل قيمة ‎i‎ الحالية فيها أيضًا. وكما مع ‎if‎ فبعد انتهاء الحلقة لا نرى ‎i‎ خارجها. كتل الشفرات يمكننا أيضًا استعمال كتلة شفرات‎{…}‎ «مجرّدة» لنعزل المتغيرات في «نطاق محلي» خاص بها. فمثلًا في متصفّح الوب تتشارك كل السكربتات (عدا التي فيها ‎type="module"‎) نفس المساحة العمومية. لذا لو أنشأنا متغيرًا عموميًا في واحد من السكربتات يمكن أن تراه البقية. هذا الأمر يتسبب بمشكلة لو استعمل سكربتان اثنان نفس اسم المتغير وبدأ كلّ منهما بتعويض الذي عند الآخر. يمكن أن يحدث هذا لو كان اسم المتغير كلمة شائعة (مثلًا ‎name‎) ولا يدري مطورو السكربتات ما يفعله الغير. يمكن أن نستعمل كتلة شيفرات لغول السكربت كاملًا أو جزءًا منه حتى لو أردنا تجنّب هذه المشكلة: { // نُجري أمرًا على المتغيرات المحلية يُمنع على ما خارجنا رؤيته let message = "Hello"; alert(message); // Hello } alert(message); // ‫خطأ: message غير معرّف لا ترى الشيفرات خارج تلك الكتلة (أو حتى الموجودة في سكربت آخر) المتغيرات داخل الكتلة إذ أنّ لها بيئتها المُعجمية الخاصة بها. تعابير الدوال الآنية IIFE سابقًا لم تكن هناك بيئات مُعجمية للكُتل في جافاسكربت. وكما «الحاجة أمّ الاختراع»، فكان على المطوّرين حلّ ذلك، وهكذا صنعوا ما سمّوه «تعابير الدوال آنيّة الاستدعاء» Immediately-Invoked Function Expressions. لست تريد أن تكتب هذا النوع من الدوال في وقتنا الآن، ولكن يمكن أن تصادفك وأنت تطالع السكربتات القديمة فالأفضل لو تعرف كيف تعمل من الآن. إليك شكل الدوال الآنية هذه: (function() { let message = "Hello"; alert(message); // Hello })(); هنا أنشأنا تعبير دالة واستدعيناه مباشرةً/آنيًا. لذا فتعمل الشيفرة في لحظتها كما وفيها متغيراتها الخاصة بها. الطريقة هي أن نُحيط تعبير الدالة بأقواس ‎(function {...})‎ إذ أنّ محرّك جافاسكربت حين يقابل ‎"function"‎ في الشيفرة الأساسية يفهمها وكأنّها تصريح عن دالة. ولكن، التصريح عن الدوال يحتاج اسمًا لها، بهذا فهذه الشيفرة ستَسبّب بخطأ: // نحاول التصريح عن الدالة واستدعائها آنيًا function() { // <-- Error: Unexpected token ( let message = "Hello"; alert(message); // Hello }(); حتّى لو قلنا «طيب لنضيف ذلك الاسم» فلن ينفع إذ أنّ محرّك جافاسكربت لا يسمح باستدعاء التصاريح عن الدوال آنيًا: // خطأ صياغي بسبب الأقواس أسفله function go() { }(); // <-- لا يمكن أن نستدعي التصريح عن الدوال آنيًا إذًا، فالأقواس حول الدالة ما هي إلا خدعة لنضحك على محرّك جافاسكربت ونُقنعه بأنّا أنشأنا الدالة في سياق تعبير آخر وبهذا فهي تعبير دالة… أي لا تحتاج اسمًا ويمكن استدعائها آنيًا. ثمّة طرق أخرى دون الأقواس لنُقنع المحرّك بأنّ ما نعني هو تعبير الدالة: // طرائق إنشاء هذه التعابير الآنية (function() { alert("أقواس تحيط بالدالة"); }) (); (function() { alert("أقواس تحيط بكامل الجملة"); }() ); ! function() { alert("عملية الأعداد الثنائية NOT أوّل التعبير"); }(); + function() { alert("عملية الجمع الأحادية أوّل التعبير"); }(); في كلّ الحالات أعلاه: صرّحنا عن تعبير دالة واستدعيناها آنيًا. لنوضّح ذلك ثانيةً: لم يعد هناك أيّ داع لنكتب هكذا شيفرات في وقتنا الحاضر. كنس المهملات عادةً ما تُمسح وتُحذف البيئة المُعجمية بعدما تعمل الدالة. مثال: function f() { let value1 = 123; let value2 = 456; } f(); هنا القيمتين (تقنيًا) خاصيتين للبيئة المُعجمية. ولكن حين تنتهي ‎f()‎ لا يمكن أن نصل إلى تلك البيئة بأيّ طريقة فتُحذف من الذاكرة. …ولكن لو كانت هناك دالة متداخلة يمكن أن نصل إليها بعدما تنتهي ‎f‎ (ولديها خاصية ‎[[Environment]]‎ التي تُشير إلى البيئة المُعجمية الخارجية)، لو كانت فيمكن أن نصل إليها: function f() { let value = 123; function g() { alert(value); } return g; // (*) } let func = f(); // ‫يمكن أن تصل func الآن بإشارة إلى g // بذلك تبقى في الذاكرة، ومعها بيئتها المُعجمية الخارجية لاحظ بأنّه لو استدعينا ‎f()‎ أكثر من مرة، فسوف تُحفظ الدوال الناتجة منها وتبقى كائنات البيئة المُعجمية لكلّ واحدة منها في الذاكرة. إليك ثلاثة منها في الشيفرة أدناه: function f() { let value = Math.random(); return function() { alert(value); }; } // في المصفوفة ثلاث دوال تُشير كلّ منها إلى البيئة المُعجمية // ‫في عملية التنفيذ f()‎ المقابلة لكلّ واحدة let arr = [f(), f(), f()]; يموت كائن البيئة المُعجمية حين لا يمكن أن يصل إليه شيء (كما الحال مع أيّ كائن آخر). بعبارة أخرى فهو موجود طالما ثمّة دالة متداخلة واحدة (على الأقل) في الشيفرة تُشير إليه. في الشيفرة أسفله، بعدما تصير ‎g‎ محالة الوصول تُمسح بيئتها المُعجمية فيها (ومعها متغير ‎value‎) من الذاكرة: function f() { let value = 123; function g() { alert(value); } return g; } // ‫طالما يمكن أن تصل func بإشارة إلى g، ستظلّ تشغل حيّزًا في الذاكرة let func = f(); // ...والآن لم تعد كذلك ونكون قد نظّفنا الذاكرة func = null; التحسينات على أرض الواقع كما رأينا، فنظريًا طالما الدالة «حيّة تُرزق» تبقى معها كل متغيراتها الخارجية. ولكن عمليًا تُحاول محرّكات جافاسكربت تحسين أداء ذلك. فهي تحلّل استعمال المتغيرات فلو كان واضحًا لها في الشيفرة بأنّ المتغير الخارجي لم يعد مستعملًا، تحذفه. ثمّة -في محرّك V8 (كروم وأوبرا)- تأثير مهمّ ألا وهو أنّ هذا المتغير لن يكون مُتاحًا أثناء التنقيح. جرّب تشغيل المثال الآتي في «أدوات المطوّرين» داخل متصفّح كروم. ما إن يُلبث تنفيذ الشيفرة، اكتب ‎alert(value)‎ في الطرفية. function f() { let value = Math.random(); function g() { debugger; // ‫اكتب في المِعراض: alert(value);‎ ما من متغير كهذا! } return g; } let g = f(); g(); كما رأينا، ما من متغير كهذا! يُفترض نظريًا أن نصل إليه ولكنّ المحرّك حسّن أداء الشيفرة وحذفه. يؤدّي ذلك أحيانًا إلى مشاكل مضحكة (هذا إن لم تجلس عليها اليوم بطوله لحلّها) أثناء التنقيح. إحدى هذه المشاكل هي أن نرى المتغير الخارجي بدل الذي توقّعنا أن نراه (يحمل كلاهما نفس الاسم): let value = "Surprise!"; function f() { let value = "the closest value"; function g() { debugger; // ‫اكتب في المِعراض: alert(value);‎ إليك Surprise! } return g; } let g = f(); g(); إلى فصل آخر! من المفيد معرفة هذه الميزة في معيار V8. متى ما بدأت التنقيح في كروم أو أوبرا، فستراها شئت أم أبيت. هذه ليست علّة في المنقّح بل هي ميزة خاصة في معيار V8. ربما تتغير لاحقًا من يدري. يمكنك أن تتحقّق منها متى أردت بتجربة الأمثلة في هذه الصفحة. تمارين هل العدّادات مستقلة عن بعضها البعض؟ الأهمية: 5 صنعنا هنا عدّادين اثنين ‎counter‎ و ‎counter2‎ باستعمال ذات الدالة ‎makeCounter‎. هل هما مستقلان عن بعضهما البعض؟ ما الذي سيعرضه العدّاد الثاني؟ ‎0,1‎ أم ‎2,3‎ أم ماذا؟ function makeCounter() { let count = 0; return function() { return count++; }; } let counter = makeCounter(); let counter2 = makeCounter(); alert( counter() ); // 0 alert( counter() ); // 1 alert( counter2() ); // ؟ alert( counter2() ); // ؟ الحل الإجابة هي: 0,1. صنعنا الدالتين ‎counter‎ و ‎counter2‎ باستدعاءين ‎makeCounter‎ مختلفين تمامًا. لذا فلكلّ منهما بيئات مُعجمية خارجية مستقلة عن بعضها، ولكلّ منهما متغير ‎count‎ مستقل عن الثاني. كائن عد الأهمية: 5 هنا صنعنا كائن عدّ بمساعدة دالة مُنشئة Constructor Function. هل ستعمل؟ ماذا سيظهر؟ function Counter() { let count = 0; this.up = function() { return ++count; }; this.down = function() { return --count; }; } let counter = new Counter(); alert( counter.up() ); // ؟ alert( counter.up() ); // ؟ alert( counter.down() ); // ؟ الحل طبعًا، ستعمل كما يجب. صُنعت الدالتين المتداخلتين في نفس البيئة المُعجمية الخارجية، بهذا تتشاركان نفس المتغير ‎count‎ وتصلان إليه: function Counter() { let count = 0; this.up = function() { return ++count; }; this.down = function() { return --count; }; } let counter = new Counter(); alert( counter.up() ); // 1 alert( counter.up() ); // 2 alert( counter.down() ); // 1 دالة في شرط if طالِع الشيفرة أسفله. ما ناتج الاستدعاء في آخر سطر؟ let phrase = "Hello"; if (true) { let user = "John"; function sayHi() { alert(`‎${phrase}, ${user}‎`); } } sayHi(); الحل الناتج هو: خطأ. صُرّح عن الدالة ‎sayHi‎ داخل الشرط ‎if‎ وتعيش فيه فقط لا غير. ما من دالة ‎sayHi‎ خارجية. المجموع باستعمال المُنغلِقات الأهمية: 4 اكتب الدالة ‎sum‎ لتعمل هكذا: ‎sum(a)(b) = a+b‎. نعم عينك سليمة، هكذا تمامًا باستعمال قوسين اثنين (ليست خطأً مطبعيًا). مثال: sum(1)(2) = 3 sum(5)(-1) = 4 الحل ليعمل القوسين الثانيين، يجب أن يُعيد الأوليين دالة. هكذا: function sum(a) { return function(b) { return a + b; // ‫تأخذ «a» من البيئة المُعجمية الخارجية }; } alert( sum(1)(2) ); // 3 alert( sum(5)(-1) ); // 4 الترشيح عبر دالة الأهمية: 5 نعلم بوجود التابِع ‎arr.filter(f)‎ للمصفوفات. ووظيفته هي ترشيح كلّ العناصر عبر الدالة ‎f‎. لو أرجعت ‎true‎ فيُعيد التابِع العنصر في المصفوفة الناتجة. اصنع مجموعة مرشّحات «جاهزة لنستعملها مباشرة»: ‎inBetween(a, b)‎ -- بين ‎a‎ و‎b‎بما فيه الطرفين (أي باحتساب ‎a‎ و‎b‎). ‎inArray([...])‎ -- في المصفوفة الممرّرة. هكذا يكون استعمالها: ‎arr.filter(inBetween(3,6))‎ -- تحدّد القيم بين 3 و6 فقط. ‎arr.filter(inArray([1,2,3]))‎ -- تحدّد العناصر المتطابقة مع أحد عناصر ‎[1,2,3]‎ فقط. مثال: // .. ‫شيفرة الدالتين inBetween وinArray let arr = [1, 2, 3, 4, 5, 6, 7]; alert( arr.filter(inBetween(3, 6)) ); // 3,4,5,6 alert( arr.filter(inArray([1, 2, 10])) ); // 1,2 الحل المرشّح inBetween function inBetween(a, b) { return function(x) { return x >= a && x <= b; }; } let arr = [1, 2, 3, 4, 5, 6, 7]; alert( arr.filter(inBetween(3, 6)) ); // 3,4,5,6 المرشّح inArray function inArray(arr) { return function(x) { return arr.includes(x); }; } let arr = [1, 2, 3, 4, 5, 6, 7]; alert( arr.filter(inArray([1, 2, 10])) ); // 1,2 الترشيح حسب حقل الاستمارة الأهمية: 5 أمامنا مصفوفة كائنات علينا ترتيبها: let users = [ { name: "John", age: 20, surname: "Johnson" }, { name: "Pete", age: 18, surname: "Peterson" }, { name: "Ann", age: 19, surname: "Hathaway" } ]; الطريقة الطبيعية هي الآتي: // ‫حسب الاسم (Ann, John, Pete) users.sort((a, b) => a.name > b.name ? 1 : -1); // ‫حسب العمر (Pete, Ann, John) users.sort((a, b) => a.age > b.age ? 1 : -1); هل يمكن أن تكون بحروف أقل، هكذا مثلًا؟ users.sort(byField('name')); users.sort(byField('age')); أي، بدل أن نكتب دالة، نضع ‎byField(fieldName)‎ فقط. اكتب الدالة ‎byField‎ لنستعملها هكذا. الحل let users = [ { name: "John", age: 20, surname: "Johnson" }, { name: "Pete", age: 18, surname: "Peterson" }, { name: "Ann", age: 19, surname: "Hathaway" } ]; function byField(field) { return (a, b) => a[field] > b[field] ? 1 : -1; } users.sort(byField('name')); users.forEach(user => alert(user.name)); // Ann, John, Pete users.sort(byField('age')); users.forEach(user => alert(user.name)); // Pete, Ann, John جيش عرمرم من الدوال الأهمية: 5 تصنع الشيفرة الآتية مصفوفة من مُطلقي النار ‎shooters‎. يفترض أن تكتب لنا كلّ دالة رقم هويّتها، ولكن ثمّة خطب فيها… function makeArmy() { let shooters = []; let i = 0; while (i < 10) { let shooter = function() { // دالة مُطلق النار alert( i ); // المفترض أن ترينا رقمها }; shooters.push(shooter); i++; } return shooters; } let army = makeArmy(); army[0](); // مُطلق النار بالهويّة 0 يقول أنّه 10 army[5](); // ‫مُطلق النار بالهويّة 5 يقول أنّه 10... // ... كلّ مُطلقي النار يقولون 10 بدل هويّاتهم 0 فَـ 1 فَـ 2 فَـ 3... لماذا هويّة كلّ مُطلق نار نفس البقية؟ أصلِح الشيفرة لتعمل كما ينبغي أن تعمل. الحل لنُجري مسحًا شاملًا على ما يجري في ‎makeArmy‎، حينها يظهر لنا الحل جليًا. تُنشئ مصفوفة ‎shooters‎ فارغة: let shooters = []; تملأ المصفوفة في حلقة عبر ‎shooters.push(function...)‎. كلّ عنصر هو دالة، بهذا تكون المصفوفة الناتجة هكذا: shooters = [ function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); } ]; تُعيد الدالة المصفوفة. لاحقًا، يستلم استدعاء ‎army[5]()‎ العنصر ‎army[5]‎ من المصفوفة، وهي دالة فيستدعيها. الآن، لماذا تعرض كلّ هذه الدوال نفس الناتج؟ يعزو ذلك إلى عدم وجود أيّ متغير محلي باسم ‎i‎ في دوال ‎shooter‎. فحين تُستدعى هذه الدالة تأخذ المتغير ‎i‎ من البيئة المُعجمية الخارجية. وماذا ستكون قيمة ‎i‎؟ لو رأينا مصدر القيمة: function makeArmy() { ... let i = 0; while (i < 10) { let shooter = function() { // دالة مُطلق النار alert( i ); // المفترض أن ترينا رقمها }; ... } ... } كما نرى… «تعيش» القيمة في البيئة المُعجمية المرتبطة بدورة ‎makeArmy()‎ الحالية. ولكن متى استدعينا ‎army[5]()‎، تكون دالة ‎makeArmy‎ قد أنهت مهمّتها فعلًا وقيمة ‎i‎ هي آخر قيمة، أي ‎10‎ (قيمة نهاية حلقة ‎while‎). وبهذا تأخذ كلّ دوال ‎shooter‎ القيمة من البيئة المُعجمية الخارجية، ذات القيمة الأخيرة ‎i=10‎. يمكن أن نُصلح ذلك بنقل تعريف المتغير إلى داخل الحلقة: function makeArmy() { let shooters = []; // (*) for(let i = 0; i < 10; i++) { let shooter = function() { // دالة مُطلق النار alert( i ); // المفترض أن ترينا رقمها }; shooters.push(shooter); } return shooters; } let army = makeArmy(); army[0](); // 0 army[5](); // 5 الآن صارت تعمل كما يجب إذ في كلّ مرة تُنفّذ كتلة الشيفرة في ‎for (let i=0...) {...}‎، يُنشئ المحرّك بيئة مُعجمية جديدة لها فيها متغير ‎i‎ المناسب لتلك الكتلة. إذًا لنلخّص: قيمة ‎i‎ صارت «تعيش» أقرب للدالة من السابق. لم تعد في بيئة ‎makeArmy()‎ المُعجمية بل الآن في تلك البيئة المخصّصة لدورة الحلقة الحالية. هكذا صارت تعمل كما يجب. أعدنا كتابة الشيفرة هنا وعوّضنا ‎while‎ بحلقة ‎for‎. يمكننا أيضًا تنفيذ حيلة أخرى. لنراها لنفهم الموضوع أكثر: function makeArmy() { let shooters = []; let i = 0; while (i < 10) { let j = i; // (*) let shooter = function() { // دالة مُطلق النار alert( j ); // (*) المفترض أن ترينا رقمها }; shooters.push(shooter); i++; } return shooters; } let army = makeArmy(); army[0](); // 0 army[5](); // 5 كما حلقة ‎for‎، فحلقة ‎while‎ تصنع بيئة مُعجمية جديدة لكلّ دورة، وهكذا نتأكّد بأن تكون قيمة ‎shooter‎ صحيحة. باختصار ننسخ القيمة ‎let j = i‎ وهذا يصنع المتغير ‎j‎ المحلي داخل الحلقة وينسخ قيمة ‎i‎ إلى نفسه. تُنسخ الأنواع الأولية «حسب قيمتها» By value، لذا بهذا نأخذ نسخة كاملة مستقلة تمامًا عن ‎i‎، ولكنّها مرتبطة بالدورة الحالية في الحلقة. ترجمة -وبتصرف- للفصل Closure من كتاب The JavaScript language
  16. لنقل بأنّ لدينا كائن معقّد البنية ونريد تحويله إلى سلسلة نصية؛ كي نُرسله عبر الشبكة أو نطبعه في الطرفية لتسجيل المخرجات. الطبيعي هو أن تكون في هذه السلسلة النصية كلّ الخاصيات المهمة. يمكننا إجراء هذا التحويل بهذه الطريقة: let user = { name: "John", age: 30, toString() { return `{name: "${this.name}", age: ${this.age}}`; } }; alert(user); // {name: "John", age: 30} ولكن… أثناء التطوير، نُضيف خاصيات جديدة ونُغيّر أسماء القديمة أو نحذفها حتّى. تحديث ذلك، مثل التابع toString، كلّ مرّة سيكون جحيمًا حقيقيًا. يمكن أن نمرّ على الخاصيات في الكائن، ولكن ماذا لو كان معقّدًا وفيه كائنات وخاصيات متداخلة؟ حينها سنحتاج إجراء تحويل لتلك أيضًا. لحسن حظّنا فكتابة تلك الشيفرة لهذه المعضلة ليس له داعٍ، فهناك من حلّها بالفعل. JSON.stringify نسق JSON (صيغة كائنات جافاسكربت JavaScript Object Notation) هو نسق عام لتمثيل القيم والكائنات، ويوثّقه المعيار RFC 4627. في بادئ الأمر كان غرض كتابته هو لاستعماله في جافاسكربت، ولكن رويدًا رويدًا بدأت اللغات الأخرى صناعة مكتبات تتعامل معه أيضًا. لهذا يسهل لنا استعمال JSON لتبادل البيانات حين يستعمل جهاز العميل جافاسكربت بينما الخادوم مكتوب بلغة روبي/PHP/جافا/أي لغة خنفشارية أخرى. تقدّم جافاسكربت التوابِع الآتية: JSON.stringify لتحويل الكائنات إلى صياغة JSON. JSON.parse لإرجاع بيانات مصاغة بصياغة JSON إلى كائن كما كان. فمثلًا هنا نستعمل JSON.stringify على طالب student: let student = { name: 'John', age: 30, isAdmin: false, courses: ['html', 'css', 'js'], wife: null }; let json = JSON.stringify(student); alert(typeof json); // حصلنا على سلسلة نصيةّ alert(json); /* ‫كائن مرمّز بِـJSON: { "name": "John", "age": 30, "isAdmin": false, "courses": ["html", "css", "js"], "wife": null } */ يأخذ التابِع JSON.stringify(student)‎ الكائن ويحوّله إلى سلسلة نصية. تُسمّى سلسلة json النصية الناتج بكائن مرمّز بِـJSON (JSON-encoded)‎ أو مُسلسل (serialized) أو stringified أو marshalled. صرنا مستعدّين لإرسال الكائن عبر الوِب أو تخزينه في مخزن بيانات خام. لاحظ من فضلك الاختلافات المهمة بين الكائن المرمّز بِـJSON من الكائن العادي الحرفي: تستعمل السلاسل النصية علامات اقتباس مزدوجة. لا مكان لعلامات الاقتباس المفردة أو الفواصل ` في JSON. بهذا يصير 'John' هكذا "John". حتّى خاصيات الكائنات تُحاط بعلامات اقتباس مزدوجة، ولا مناص من ذلك. بهذا يصير age:30 هكذا "age":30. يمكن استعمال JSON.stringify على الأنواع الأولية أيضًا. تدعم JSON أنواع البيانات الآتية: الكائنات { ... } المصفوفات [ ... ] الأنواع الأولية: السلاسل النصية, الأعداد, القيم المنطقية true/false, قيمة اللاشيء null. مثال: // ‫العدد في JSON ليس إلّا عددًا alert( JSON.stringify(1) ) // 1 // ‫السلسلة النصية في JSON ليست إلّا سلسلة نصيّة، بين علامات اقتباس مزودجة alert( JSON.stringify('test') ) // "test" alert( JSON.stringify(true) ); // true alert( JSON.stringify([1, 2, 3]) ); // [1,2,3] مواصفة JSON هي مواصفة مستقلّة لغويًا وتحمل البيانات فقط. لذا يُهمِل JSON.stringify خاصيات الكائنات الخاصّة بجافاسكربت. نذكر منها: خاصيات الدوال (التوابِع). الخاصيات الرمزية. الخاصيات التي تُخزّن undefined. let user = { sayHi() { // ignored alert("Hello"); }, [Symbol("id")]: 123, // يتجاهلها something: undefined // يتجاهلها }; alert( JSON.stringify(user) ); // ‫{} (كائن فارغ) غالبًا، لا مانع من ذلك. لو كان هناك مانع فسنرى قريبًا طريقة تخصيص عملية السَلسلة هذه. بفضل دهاء المبرمجين، فالكائنات المتداخلة مدعومة وستُحوَّل تلقائيًا. مثال: let meetup = { title: "Conference", room: { number: 23, participants: ["john", "ann"] } }; alert( JSON.stringify(meetup) ); /* البنية كاملة تتحوّل إلى سلسلة نصية: { "title":"Conference", "room":{"number":23,"participants":["john","ann"]}, } */ إليك التقييد: وجود الإشارات التعاودية (circular references) ممنوع. مثال: let room = { number: 23 }; let meetup = { title: "Conference", participants: ["john", "ann"] }; meetup.place = room; // ‫يُشير الاجتماع إلى الغرفة (meetup -> room) room.occupiedBy = meetup; // ‫تُشير الغرفة إلى الاجتماع (room -> meetup) JSON.stringify(meetup); // ‫خطأ تحاول تحويل بنية تعاوية إلى JSON هنا فشل التحويل بسبب الإشارات التعاودية: فتُشير room.occupiedBy إلى meetup وmeetup.place إلى room: الاستثناءات وتعديل الكائنات: آلة الاستبدال إليك الصياغة الكاملة للتابِع JSON.stringify: let json = JSON.stringify(value[, replacer, space]) المعاملات: value: القيمة التي ستُرمّز. replacer: : مصفوفة من الخاصيات لترميزها، أو دالة ربط (mapping‏) بالشكل function(key, value)‎. space: عدد المسافات لاستعمالها لتنسيق السلسلة النصية. في أغلب الوقت نستعمل JSON.stringify بتمرير المُعامل الأول فقط. ولكن لو أردنا تعديل عملية الاستبدال مثل تعديل الإشارات التعاودية، فيمكننا استعمال المُعامل الثاني للتابِع. لو مرّرنا مصفوفة فيها خاصيات، فستُرمّز تلك الخاصيات فقط. مثال: let room = { number: 23 }; let meetup = { title: "Conference", participants: [{name: "John"}, {name: "Alice"}], place: room // يُشير الاجتماع إلى الغرفة }; room.occupiedBy = meetup; // room references meetup alert( JSON.stringify(meetup, ['title', 'participants']) ); // {"title":"Conference","participants":[{},{}]} ربّما نكون هنا صارمين كثيرًا، فقائمة الخاصيات تُطبّق على كامل بنية الكائن، بهذا الكائنات في participants فارغة إذ أنّ name ليست في القائمة. لنضمّن في تلك القائمة كلّ خاصية عدا room.occupiedBy إذ ستتسبّب بإشارة تعاودية: let room = { number: 23 }; let meetup = { title: "Conference", participants: [{name: "John"}, {name: "Alice"}], place: room // يُشير الاجتماع إلى الغرفة }; room.occupiedBy = meetup; // تُشير الغرفة إلى الاجتماع alert( JSON.stringify(meetup, ['title', 'participants', 'place', 'name', 'number']) ); /* { "title":"Conference", "participants":[{"name":"John"},{"name":"Alice"}], "place":{"number":23} } */ الآن سَلسلنا كلّ ما في occupiedBy، ولكن قائمة الخاصيات صارت طويلة. لحسن حظّنا يمكننا استعمال دالة بدل المصفوفة لتكون آلة الاستبدال replacer. ستُستدعى الدالة لكلّ زوج (key, value) ويجب أن تُعيد القيمة ”المُستبدَلة“ التي ستحلّ مكان الأصلية، أو undefined لو أردنا إهمال الخاصية. في حالتنا هذه سنُعيد القيمة value ”كما هي“ لكل الخاصيات باستثناء occupiedBy. لنُهمل occupiedBy ستُعيد الشيفرة undefined: let room = { number: 23 }; let meetup = { title: "Conference", participants: [{name: "John"}, {name: "Alice"}], place: room // يُشير الاجتماع إلى الغرفة }; room.occupiedBy = meetup; // تُشير الغرفة إلى الاجتماع alert( JSON.stringify(meetup, function replacer(key, value) { alert(`${key}: ${value}`); return (key == 'occupiedBy') ? undefined : value; })); /* أزواج ‫key:value التي تدخل آلة الاستبدال: : [object Object] title: Conference participants: [object Object],[object Object] 0: [object Object] name: John 1: [object Object] name: Alice place: [object Object] number: 23 */ لاحِظ بأنّ الدالة replacer تأخذ كلّ زوج ”مفتاح/قيمة“ بما في ذلك الكائنات المتداخلة وعناصر المصفوفات، فهي تتطبّق تكراريًا. وقيمة this داخل replacer هي الكائن الذي يحتوي على الخاصية الحالية. الاستدعاء الأوّل خاصّ قليلًا، فهو يستلم ”كائن تغليف“: {"": meetup}. بعبارة أخرى فأوّل زوج (key, value) يكون مفتاحه فارغًا وقيمته هي الكائن الهدف كلّه. لهذا نرى السطر الأول في المثال أعلاه: ":[object Object]". الغرض هو تقديم كلّ ما أمكن من ”تسلّط“ لأداة الاستبدال، بهذا يمكنها تحليل الكائنات كاملةً واستبدالها أو إهمالها لو تطلّب الأمر. التنسيق: المسافات المُعامل الثالث للتابِع JSON.stringify(value, replacer, space)‎ هو عدد المسافات التي ستُستعمل لتنسيقها تنسيقًا جميلًا (Pretty format). في المثال السابق، لم يكن للكائنات المُسلسلة (stringified objects) أيّة مسافات أو مسافات بادئة. لا بأس لو كنّا سنرسل الكائن عبر الشبكة، فالمُعامل space يُستعمل فقط لتجميل الناتج. هنا بعبارة space = 2 نقول لجافاسكربت بأن تعرض الكائنات المتداخلة على عدّة أسطر، بمسافتين بادئتين داخل كل كائن: let user = { name: "John", age: 25, roles: { isAdmin: false, isEditor: true } }; alert(JSON.stringify(user, null, 2)); /* ‫إزاحة بمسافتين: { "name": "John", "age": 25, "roles": { "isAdmin": false, "isEditor": true } } */ /* ‫بينما JSON.stringify(user, null, 4)‎ يعطينا إزاحة أكبر: { "name": "John", "age": 25, "roles": { "isAdmin": false, "isEditor": true } } */ نستعمل المُعامل space فقط لغرض الناتج الجميل وعمليات تسجيل المخرجات. تابِع ”toJSON“ مخصّص كما يوجد toString للتحويل إلى سلاسل نصية، يمكن للكائنات أيضًا تقديم تابِع toJSON للتحويل إلى JSON. تستدعي JSON.stringify ذاك التابِع تلقائيًا لو وجدته. مثال: let room = { number: 23 }; let meetup = { title: "Conference", date: new Date(Date.UTC(2017, 0, 1)), room }; alert( JSON.stringify(meetup) ); /* { "title":"Conference", "date":"2017-01-01T00:00:00.000Z", // (1) "room": {"number":23} // (2) } */ نرى هنا بأنّ date (في (1)) صار سلسلة نصية. هذا لأنّ التواريخ كلّها توفّر تنفيذًا للتابِع toJSON مضمّنًا فيها، وهو يُعيد سلاسل نصية بهذا التنسيق. لنُضيف الآن تابِع toJSON مخصّص للكائن room (في (2)): let room = { number: 23, toJSON() { return this.number; } }; let meetup = { title: "Conference", room }; alert( JSON.stringify(room) ); // 23 alert( JSON.stringify(meetup) ); /* { "title":"Conference", "room": 23 } */ كما نرى، استُعمِل التابِع toJSON مرتين، مرة حين استدعاه JSON.stringify(room)‎ مباشرةً، ومرة حين كانت الخاصية room داخل كائن مرمّز آخر. التابع JSON.parse لنفكّ ترميز سلسلة JSON نصية، سنحتاج تابِعًا آخر بالاسم JSON.parse. صياغته هي: let value = JSON.parse(str, [reviver]); المعاملات: str: سلسلة JSON النصية التي سيُحلّلها. reviver: دالة اختيارية function(key,value)‎ تُستدعى لكلّ زوج (key, value) ويمكن لها تعديل القيمة. مثال: // ‫مصفوفة مُسلسلة (stringified array) let numbers = "[0, 1, 2, 3]"; numbers = JSON.parse(numbers); alert( numbers[1] ); // 1 أو حين استعمالها للكائنات المتداخلة: let userData = '{ "name": "John", "age": 35, "isAdmin": false, "friends": [0,1,2,3] }'; let user = JSON.parse(userData); alert( user.friends[1] ); // 1 يمكن أن يكون كائن JSON بالتعقيد اللازم مهمًا كان. يمكن أن تحتوي الكائنات والمصفوفات كائناتَ ومصفوفات أخرى، ولكنّ لزامٌ عليها أن تلتزم بنفس نسق JSON. إليك بعض المشاكل الشائعة حين كتابة JSON يدويًا (أحيانًا نفعل ذلك لأغراض تنقيح الشيفرات): let json = `{ name: "John", // خطأ: اسم خاصية بدون علامات اقتباس "surname": 'Smith', // خطأ: علامات اقتباس مُفردة في القيمة (يجب أن تكون مزودجة)‏ 'isAdmin': false // خطأ: علامات اقتباس مُفردة في المفتاح (يجب أن تكون مزدوجة)‏ "birthday": new Date(2000, 2, 3), // ‫خطأ: استعمال "new" ممنوع، فقط وفقط قيم "friends": [0,1,2,3] // هنا لا بأس }`; وأجل، لا تدعم JSON التعليقات، فلو أضفتها سيتحوّل الكائن إلى كائن غير صالح. هناك نسق آخر بالاسم JSON5 ويُتيح لنا عدم إحاطة المفاتيح بعلامات اقتباس، وكتابة التعليقات وغيرها. إلّا أنّها مكتبة مستقلة وليست في مواصفة لغة جافاسكربت. لم يصنع المطوّرون كائنات JSON العادية لتكون بهذه الصرامة لأنّهم كسالى، بل لنُعوّل على شيفرات خوارزميات التحليل، إضافة إلى عملها بسرعة فائقة. استعمال آلة الإحياء تخيّل أنّنا استلمنا كائن meetup مُسلسل من الخادوم، وهذا شكله: // title: (meetup title), date: (meetup date) let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}'; …نريد الآن فكّ ترميزه، أي إعادته إلى كائن جافاسكربت عادي. يكون ذلك باستدعاء JSON.parse: let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}'; let meetup = JSON.parse(str); alert( meetup.date.getDate() ); // خطأ‫! لحظة… خطأ؟! قيمة الخاصية meetup.date هي سلسلة نصية وليست كائن تاريخ Date. كيف سيعرف JSON.parse بأنّ عليه تعديل تلك السلسلة النصية لتصير Date؟ لنمرّر الآن إلى JSON.parse دالة ”آلة الإحياء“ في المُعامل الثاني، وستُحيي كلّ القيم ”كما هي“، عدا date ستعدّلها لتكون تاريخًا: let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}'; let meetup = JSON.parse(str, function(key, value) { if (key == 'date') return new Date(value); return value; }); alert( meetup.date.getDate() ); // ‫الآن صار يعمل! وأجل، تعمل الشيفرة للكائنات المتداخلة أيضًا: let schedule = `{ "meetups": [ {"title":"Conference","date":"2017-11-30T12:00:00.000Z"}, {"title":"Birthday","date":"2017-04-18T12:00:00.000Z"} ] }`; schedule = JSON.parse(schedule, function(key, value) { if (key == 'date') return new Date(value); return value; }); alert( schedule.meetups[1].date.getDate() ); // ‫يعمل! ملخص تنسيق JSON هو تنسيق بيانات يستقلّ بمعياره ومكتباته في غالبية لغات البرمجة. يدعم JSON الكائنات العادية والمصفوفات والسلاسل النصية والأعداد والقيم المنطقية وnull. تقدّم جافاسكربت التوابِع JSON.stringify لسَلسلة الكائنات إلى JSON، وJSON.parse للقراءة من JSON. يدعم كلا التابِعين دوال تعديل لتكون القراءة والكتابة ”ذكيّة“. لو كان في الكائن تابِع toJSON، فسيستدعيه JSON.stringify. تمارين تحويل الكائن إلى JSON وتحويله كما كان الأهمية: 5 حوّل الكائن user إلى JSON واقرأه ثانيةً ليكون متغيرًا آخرًا. let user = { name: "John Smith", age: 35 }; الحل let user = { name: "John Smith", age: 35 }; let user2 = JSON.parse(JSON.stringify(user)); استثناء الإشارات السابقة الأهمية: 5 يمكننا في الحالات العادية من الإشارات التعاودية استثناء خاصية محدّدة لألا تُسلسل، حسب اسمها. ولكن أحيانًا لا نستطيع استعمال الاسم إذ يُستعمل في الإشارات التعاودية زائدًا الخاصيات العادية. يمكننا هنا فحص الخاصية حسب قيمتها. اكتب دالة replacer تُسلسل كل شيء ولكن تُزيل الخاصيات التي تُشير إلى meetup: let room = { number: 23 }; let meetup = { title: "Conference", occupiedBy: [{name: "John"}, {name: "Alice"}], place: room }; // إشارات تعاودية room.occupiedBy = meetup; meetup.self = meetup; alert( JSON.stringify(meetup, function replacer(key, value) { /* شيفرتك هنا*/ })); /* ‫هكذا النتيجة المطلوبة: { "title":"Conference", "occupiedBy":[{"name":"John"},{"name":"Alice"}], "place":{"number":23} } */ الحل let room = { number: 23 }; let meetup = { title: "Conference", occupiedBy: [{name: "John"}, {name: "Alice"}], place: room }; room.occupiedBy = meetup; meetup.self = meetup; alert( JSON.stringify(meetup, function replacer(key, value) { return (key != "" && value == meetup) ? undefined : value; })); /* { "title":"Conference", "occupiedBy":[{"name":"John"},{"name":"Alice"}], "place":{"number":23} } */ علينا هنا (أيضًا) فحص key==""‎ لنستثني أوّل نداء إذ لا مشكلة بأن تكون القيمة value هي meetup. ترجمة -وبتصرف- للفصل JSON methods, toJSON من كتاب The JavaScript language .task__importance { color: #999; margin-left: 30px; } .task__answer { border: 3px solid #f7f6ea; margin: 20px 0 14px; position: relative; display: block; padding: 25px 30px; } اقرأ أيضًا المقال التالي: توابع الأنواع الأولية المقال السابق: النوع Date: تمثيل التاريخ والوقت
  17. حان وقت الحديث عن كائن آخر مضمّن في اللغة: التاريخ Date. يخزّن هذا الكائن التاريخ والوقت ويقدّم توابِع تُدير أختام التاريخ والوقت. يمكننا مثلًا استعماله لتخزين أوقات الإنشاء/التعديل أو حساب الوقت أو طباعة التاريخ الحالي في الطرفية. الإنشاء استدعِ new Date()‎ بتمرير واحدًا من الوُسطاء الآتية فتصنع كائن Date جديد: الدالة new Date()‎ تنشئ هذه الدالة بلا وُسطاء كائن Date بالتاريخ والوقت الحاليين: let now = new Date(); alert( now ); // نعرض التاريخ والوقت الحاليين إليك كيفية إنشاء كائن Date: new Date(milliseconds)‎ يُنشئ كائن Date إذ تساوي قيمته عدد المليثوان الممرّرة (المليثانية هي 1/1000 من الثاني) حسابًا من بعد الأول من يناير عام ١٩٧٠ بتوقيت UTC+0. // UTC+0 // 01.01.1970 نعني بـ 0 التاريخ let Jan01_1970 = new Date(0); alert( Jan01_1970 ); // نضيف الآن 24 ساعة لنحصل على 02.01.1970 let Jan02_1970 = new Date(24 * 3600 * 1000); alert( Jan02_1970 ); يُسمّى العدد الصحيح الذي يمثّل عدد المليثوان التي مرّت من بداية عام 1970 بالختم الزمني (بصمة وقت). وهو يمثّل التاريخ بنحوٍ عددي خفيف (lightweight). يمكننا إنشاء التواريخ من الأختام الزمنية باستعمال new Date(timestamp)‎ وتحويل كائن التاريخ Date الموجود إلى ختم زمني باستعمال التابِع date.getTime()‎ (طالع أسفله). والتواريخ قبل الأول من يناير 1970 أختامها سالبة: // ‫31 ديسمبر 1969 let Dec31_1969 = new Date(-24 * 3600 * 1000); alert( Dec31_1969 ); new Date(datestring)‎ لو كان هناك وسيط واحد وكان سلسلة نصيّة، فسيحلّله المحرّك تلقائيًا. الخوازرمية هنا هي ذات التي يستعملها Date.parse. لا تقلق، سنتكلم عنه لاحقًا. let date = new Date("2017-01-26"); alert(date); نجد في هذا المثال أن الوقت غير محدد لذا يكون بتوقيت GMT منتصف الليل، ويحدد وفقًا للمنطقة الزمنية التي تنفذ الشيفرة ضمنها، فالنتيجة يمكن أن تكون Thu Jan 26 2017 11:00:00 للبلدان ذات المنطقة الزمنية GMT+1100 أو يمكن أن تكون Wed Jan 25 2017 16:00:00 للبلدان الواقعة في المنطقة الزمنية GMT-0800. new Date(year, month, date, hours, minutes, seconds, ms)‎ يُنشئ تاريخًا بالمكوّنات الممرّرة حسب المنطقة الزمنية المحلية. أوّل وسيطين إلزاميين أما البقية اختيارية. يجب أن يكون العام year بأربع خانات: 2013 صح، 98 خطأ. يبدأ الشهر month بالرقم 0 (يناير) وينتهي بالعدد 11 (ديسمبر). مُعامل التاريخ date هو رقم اليوم من الشهر. لو لم يكن موجودًا فسيعدّه الكائن 1. لو لم تكن مُعاملات الساعة والدقيقة والثانية والمليثانية hours/minutes/seconds/ms موجودة، فسيعدّها الكائن 0. مثال: new Date(2011, 0, 1, 0, 0, 0, 0); // 1 Jan 2011, 00:00:00 new Date(2011, 0, 1); // نفس تلك. الساعات والدقائق وغيرها 0 مبدئيًا أدنى دقّة للتاريخ هي مليثانية واحدة (واحد من ألف من الثانية): let date = new Date(2011, 0, 1, 2, 3, 4, 567); alert( date ); // 1.01.2011, 02:03:04.567 الوصول إلى مكوّنات التاريخ إليك التوابِع التي تتيح لك الوصول إلى العام والشهر وغيرها داخل كائن Date: getFullYear()‎: يجلب العام (٤ خانات) getMonth()‎: يجلب الشهر، من 0 إلى 11. getDate()‎: يجلب رقم اليوم من الشهر، من 1 إلى 31. قد يبدو الاسم غريبًا قليلًا لك. التوابع getHours()‎ وgetMinutes()‎ وgetSeconds()‎ وgetMilliseconds()‎: تجلب مكوّنات الوقت حسب كل تابِع. (الساعة/الدقيقة/الثانية/المليثانية) إياك بـ getYear()‎ بل getFullYear()‎ تقدّم الكثير من محرّكات جافاسكربت التابِع غير القياسي getYear()‎. هذا التابِع أصبح بائدًا، فهو يُعيد العام بخانتين أحيانًا. من فضلك لا تستعمله أبدًا، بل getFullYear()‎ لتجلب العام. كما يمكن أيضًا جلب رقم اليوم من الشهر: التابع getDay()‎ يجلب رقم اليوم من الأسبوع، بدءًا بِـ 0 (الأحد) وحتى 6 (السبت). أوّل يوم هو الأحد دومًا. صحيح أنّ في بعض الدول هذا غير صحيح، لكن لا يمكن تغيير القيمة إطلاقًا. تُعيد كلّ التوابِع أعلاه المكوّنات حسب المنطقة الزمنية المحلية. توجد أيضًا مثيلاتها بنظام UTC حيث تُعيد اليوم والشهر والعام وغيرها في المنطقة الزمنية UTF+0:‏ getUTCFullYear()‎ وgetUTCMonth()‎ وgetUTCDay()‎. ضع كلمة "UTC" بعد "get" وستجد المثيل المناسب. لو كانت منطقتك الزمنية المحلية بعيدة عن UTC، فستعرض الشيفرة أدناه الساعات مختلفة عن بعضها البعض: // التاريخ الحالي let date = new Date(); // الساعة حسب المنطقة الزمنية التي أنت فيها alert( date.getHours() ); // ‫الساعة حسب المنطقة الزمنية بتوقيت UTC+0 (أي توقيت لندن بدون التوقيت الصيفي) alert( date.getUTCHours() ); هناك (إضافةً إلى هذه التوابِع) تابِعان آخران مختلفان قليلًا ليس لهما نُسخ بتوقيت UTC: التابع getTime()‎ يُعيد ختم التاريخ الزمني، أي عدد المليثوان التي مرّت منذ الأول من يناير عام 1970 بتوقيت UTC+0. التابع getTimezoneOffset()‎ يُعيد الفرق بين المنطقة الزمنية الحالية وتوقيت UTC (بالدقيقة): // ‫لو كانت منطقتك الزمنية UTC-1، فالناتج 60 // لو كانت منطقتك الزمنية ‫UTC+3، فالناتج ‎-180 alert( new Date().getTimezoneOffset() ); ضبط مكوّنات التاريخ تتيح لك التوابِع الآتية ضبط مكوّنات التاريخ والوقت: العام: setFullYear(year, [month], [date])‎ الشهر: setMonth(month, [date])‎ التاريخ: setDate(date)‎ الساعة: setHours(hour, [min], [sec], [ms])‎ الدقيقة: setMinutes(min, [sec], [ms])‎ الثانية: setSeconds(sec, [ms])‎ المليثانية: setMilliseconds(ms)‎ الوقت بالمليثانية: setTime(milliseconds)‎ (تضبط التاريخ كلّه حسب عدد المليثوان منذ 01.01.1970 UTC) لدى كلّ تابع منها نسخة بتوقيت UTC (عدا setTime()‎). مثال: setUTCHours()‎. كما رأيت فيمكن لبعض التوابِع ضبط عدّة مكوّنات في آن واحد مثل setHours. المكوّنات التي لا تُمرّر لا تُعدّل. مثال: let today = new Date(); today.setHours(0); alert(today); // ما زال اليوم نفسه، ولكن الساعة تغيّرت إلى 0 today.setHours(0, 0, 0, 0); alert(today); // ما زال اليوم نفسه، ولكنّا عند 00:00:00 تمامًا. التصحيح التلقائي ميزة التصحيح التلقائي في كائنات التواريخ Date مفيدة جدًا لنا، إذ يمكن أن نضع قيم تاريخ لامنطقية (مثل الخمسون من هذا الشهر) وسيُعدّلها الكائن بنفسه. مثال: let date = new Date(2013, 0, 32); // ‫الثاني والثلاثين من يناير 2013؟! alert(date); // ‫...آه، تقصد الأول من فبراير 2013! تترتّب المكوّنات اللامنطقية تلقائيًا. فمثلًا لو أضفت على التاريخ ”28 فبراير 2016“ يومين اثنين، فيمكن أن يكون ”الثاني من مارس“ أو ”الأول من مارس“ لو كانت السنة كبيسة. بدل أن نفكّر بهذا الحساب، نُضيف يومين ونترك الباقي على كائن Date: let date = new Date(2016, 1, 28); date.setDate(date.getDate() + 2); alert( date ); // ‫1 مارس 2016 غالبًا ما تُستعمل هذه الميزة لنجلب التاريخ بعد فترة محدّدة من الزمن. فلنقل مثلًا نريد تاريخ ”70 ثانية من الآن“: let date = new Date(); date.setSeconds(date.getSeconds() + 70); alert( date ); // يعرض التاريخ الصحيح يمكننا أيضًا ضبط القيمة لتكون صفرًا أو حتّى بالسالب. مثال: let date = new Date(2016, 0, 2); // ‫2 يناير 2016 date.setDate(1); // نضبط التاريخ على أول يوم من الشهر alert( date ); date.setDate(0); // أقل يوم ممكن هو 1، إذًا فيعدّ الكائن أنّ 0 هو آخر يوم من الشهر الماضي alert( date ); // ‫31 ديسمبر 2015 تحويل التاريخ إلى عدد، والفرق بين تاريخين حين يتحوّل كائن Date إلى عدد يصير ختمًا زمنيًا مطابقًا لختم date.getTime()‎: let date = new Date(); alert(+date); // ‫عدد المليثوان، نفس ناتج date.getTime()‎ تأثير هذا المهم والخطير هو أنّك تستطيع طرح التواريخ من بعض، والناتج سيكون الفرق بينهما بالمليثانية. يمكن استعمال الطرح لحساب الأوقات: let start = new Date(); // نبدأ قياس الوقت // إلى العمل for (let i = 0; i < 100000; i++) { let doSomething = i * i * i; } let end = new Date(); // ننتهي من قياس الوقت alert( `The loop took ${end - start} ms` ); التاريخ الآن لو أردنا قياس الوقت فقط فلا نحتاج كائن Date، بل هناك تابِعًا خاصًا باسم Date.now()‎ يُعيد لنا الختم الزمني الحالي. يُكافئ هذا التابِع الجملةَ new Date().getTime()‎ إلّا أنّه لا يُنشئ كائن Date يتوسّط العملية، ولهذا هو أسرع ولا يزيد الضغط على عملية كنس المهملات. غالبًا ما يُستعمل التابِع لأنّه أسهل أو لأنّ الأداء في تلك الحالة مهم، مثلما في الألعاب بلغة جافاسكربت أو التطبيقات المتخصّصة الأخرى. ولهذا قد يكون الأفضل كتابة الشيفرة أدناه بدل تلك: let start = Date.now(); // ‫تبدأ المليثوان من تاريخ 1 يناير 1970 // إلى العمل for (let i = 0; i < 100000; i++) { let doSomething = i * i * i; } *!* let end = Date.now(); // انتهينا */!* alert( `The loop took ${end - start} ms` ); // نطرح الأعداد لا التواريخ قياس الأداء لو أردنا قياس أداء دالة شرهة في استعمال المعالج، فعلينا أن نكون حذرين، هذا لو أردنا التعويل على القياس. فلنقيس مثلًا دالتين اثنتين تحسبان الفرق بين تاريخين: أيهما أسرع؟ نُطلق على قياسات الأداء هذه… قياسات أداء ”Benchmark“. // ‫أمامنا date1 وdate2، أيّ دالة ستُعيد الفرق بينهما (بالمليثانية) أسرع من الأخرى؟‫ هذه... function diffSubtract(date1, date2) { return date2 - date1; } // ‫أم هذه... function diffGetTime(date1, date2) { return date2.getTime() - date1.getTime(); } وظيفة الدالتين متطابقة تمامًا، إلّا أن الثانية تستعمل التابِع date.getTime()‎ الصريح لتجلب التاريخ بالمليثانية، بينما الأخرى تعتمد على تحويل التاريخ إلى عدد. الناتج متطابق دومًا. إذًا بهذه المعطيات، أيّ الدالتين أسرع؟ أوّل فكرة تخطر على البال هو تشغيل كلّ واحدة مرات عديدة متتابعة وقياس فرق الوقت. الدوال (في حالتنا هذه) بسيطة جدًا، ولهذا علينا تشغيل كلّ واحدة مئة ألف مرة على الأقل. هيًا نقيس الأداء: function diffSubtract(date1, date2) { return date2 - date1; } function diffGetTime(date1, date2) { return date2.getTime() - date1.getTime(); } function bench(f) { let date1 = new Date(0); let date2 = new Date(); let start = Date.now(); for (let i = 0; i < 100000; i++) f(date1, date2); return Date.now() - start; } alert( 'Time of diffSubtract: ' + bench(diffSubtract) + 'ms' ); alert( 'Time of diffGetTime: ' + bench(diffGetTime) + 'ms' ); عجبًا! استعمال التابِع getTime()‎ أسرع بكثير! يعزو ذلك بسبب انعدام وجود تحويل للنوع (type conversion)، وهذا يسهّل على المحرّكات تحسين الأداء. جميل، وصلنا إلى شيء، ولكنّ هذا القياس ليس قياسًا طيبًا بعد. تخيّل أنّ المعالج كان ينفّذ أمرًا ما بالتوازي مع تشغيل bench(diffSubtract)‎ وكان يستهلك الموارد، وما إن شغّلنا bench(diffGetTime)‎ كان ذلك الأمر قد اكتمل. هذا التخيّل هو تخيّل طبيعي لأمر واقعيّ جدًا حيث اليوم أنظمة التشغيل متعدّدة المهام. بهذا يكون لمرة القياس الأولى موارد معالجة أقل من المرة الثانية، ما قد يؤدّي إلى نتائج قياس خطأ. إن أردنا التعويل على قياس الأداء، علينا إعادة تشغيل كل قياسات الأداء الموجودة أكثر من مرّة. هكذا مثلًا: function diffSubtract(date1, date2) { return date2 - date1; } function diffGetTime(date1, date2) { return date2.getTime() - date1.getTime(); } function bench(f) { let date1 = new Date(0); let date2 = new Date(); let start = Date.now(); for (let i = 0; i < 100000; i++) f(date1, date2); return Date.now() - start; } let time1 = 0; let time2 = 0; // ‫نشغّل bench(upperSlice)‎ وbench(upperLoop)‎ عشر مرات مرّة بمرّة for (let i = 0; i < 10; i++) { time1 += bench(diffSubtract); time2 += bench(diffGetTime); } alert( 'Total time for diffSubtract: ' + time1 ); alert( 'Total time for diffGetTime: ' + time2 ); لا تبدأ محرّكات جافاسكربت الحديثة بتطبيق التحسينات المتقدّمة إلّا على ”الشيفرات الحرجة“ التي تتنفّذ أكثر من مرّة (لا داعٍ بتحسين شيفرة نادرة التنفيذ). بهذا في المثال الأول، قد لا تكون مرات التنفيذ الأولى محسّنة كما يجب، وربما علينا إضافة تحمية سريعة: // أضفناه لـ”تحمية“ المحرّك قبل الحلقة الأساس bench(diffSubtract); bench(diffGetTime); // الآن نقيس for (let i = 0; i < 10; i++) { time1 += bench(diffSubtract); time2 += bench(diffGetTime); } الزم الحذر متى ما أجريت قياسات أداء على المستوى الذرّي. تُنفّذ محرّكات جافاسكربت الحديثة عددًا كبيرًا من التحسينات، وقد تُغيّر نتائج ”الاختبارات الصناعية“ موازنةً ”بالاستعمال الطبيعي لها“، خصوصًا حين نقيس أداء ما هو صغير للغاية مثل طريقة عمل مُعامل رياضي، أو دالة مضمّنة في اللغة نفسها. لهذا، لو كنت تريد حقًا فهم الأداء كما يجب، فمن فضلك تعلّم طريقة عمل محرّك جافاسكربت. حينها ربّما لن تحتاج هذه القياسات على المستوى الذرّي، أبدًا. يمكنك أن تقرأ بعض المقالات الرائعة حول V8 هنا http://mrale.ph. تحليل سلسلة نصية باستعمال Date.parse يمكن أن يقرأ التابِع Date.parse(str)‎ تاريخًا من سلسلة نصية. يجب أن يكون تنسيق تلك السلسلة هكذا: YYYY-MM-DDTHH:mm:ss.sssZ، إذ تعني: YYYY-MM-DD -- التاريخ: اليوم-الشهر-العام. يُستعمل المحرف "T" فاصِلًا. HH:mm:ss.sss -- الوقت: المليثانية والثانية والدقيقة والساعة. يمثّل الجزء الاختياري 'Z' المنطقة الزمنية حسب التنسيق +-hh:mm. لو وضعت Z فقط فذلك يعني UTC+0. يمكنك أيضًا استعمال تنسيقات أقصر مثل YYYY-MM-DD أو YYYY-MM أو حتّى YYYY. باستدعاء Date.parse(str) فالسلسلة النصية تُحلّل حسب التنسيق فيها ويُعيد التابِع الختم الزمني (رقم المليثوان منذ الأول من يناير 1970 بتوقيت UTC+0). لو كان التنسيق غير صحيح فيُعيد NaN. إليك مثالًا: let ms = Date.parse('2012-01-26T13:51:50.417-07:00'); alert(ms); // ‫1327611110417 (ختم زمني) يمكننا إنشاء كائن new Date مباشرةً من الختم الزمني: let date = new Date( Date.parse('2012-01-26T13:51:50.417-07:00') ); alert(date); ملخص يُمثّل التاريخ والوقت في جافاسكربت بكائن Date. لا يمكننا إنشاء ”تاريخ فقط“ أو ”وقتًا فقط“، فعلى كائنات التاريخ Date احتواء الاثنين معًا. تُعدّ الأشهر بدءًا بالصفر (يناير هو الشهر صفر، نعم). يُعدّ رقم اليوم من الأسبوع في getDay()‎ من الصفر أيضًا (وهو يوم الأحد). يصحّح كائن التاريخ نفسه تلقائيًا حين تُضبط مكوّناته بقيم لا منطقية. يفيدنا لجمع/طرح الأيام والأشهر والأعوام. يمكن طرح التواريخ ومعرفة الفرق بينها بالمليثانية، ذلك لأنّ كائن التاريخ يتحوّل إلى ختم زمني حين يتحوّل إلى عدد. استعمل Date.now()‎ لو أردت جلب الختم الزمني الحالي بسرعة. لاحظ بأنّ الأختام الزمنية في جافاسكربت هي بالمليثانية، على العكس من أنظمة عديدة أخرى. نجد نفسنا بين الحين والآخر قياسات وقت دقيقة. للأسف فلا توفّر جافاسكربت نفسها طريقة لحساب الوقت بالنانوثانية (1 على مليون من الثانية)، ولكن أغلب بيئاتها توفّر ذلك. فمثلًا تملك المتصفّحات التابِع performance.now()‎ إذ يُعيد عدد المليثوان منذ بدأ تحميل الصفحة بقدّة تصل إلى المايكروثانية (ثلاث خانات بعد الفاصلة): alert(`Loading started ${performance.now()}ms ago`); // "Loading started 34731.26000000001ms ago" تعني ”‎.26“ هنا المايكروثوان (260 مايكروثانية)، فلو زِدت على ثلاث خانات بعد الفاصلة فستجد أخطاءً في دقّة الحساب. أوّل ثلاثة هي الصحيحة فقط. تملك لغة Node.js أيضًا وحدة microtime وأخرى غيرها. يمكن (تقنيًا) لأيّ جهاز أو بيئة أن تعطينا دقّة وقت أعلى، Date لا تقدّم ذلك لا أكثر. تمارين إنشاء تاريخ الأهمية: 5 أنشِئ كائن Date لهذا التاريخ: 20 فبراير 2012، 3:12 صباحًا. المنطقة الزمنية هي المحلية. اعرض التاريخ باستعمال alert. الحل يستعمل مُنشِئ new Date المنطقة الزمنية الحالية. عليك ألا تنسى بأنّ الأشهر تبدأ من الصفر. إذًا ففبراير هو الشهر رقم 1. let d = new Date(2012, 1, 20, 3, 12); alert( d ); اعرض اسم اليوم من الأسبوع الأهمية: 5 اكتب دالة getWeekDay(date)‎ تعرض اسم اليوم من الأسبوع بالتنسيق الإنكليزي القصير: 'MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'. مثال: let date = new Date(2012, 0, 3); // ‫3 يناير 2012 alert( getWeekDay(date) ); // ي‫جب أن يطبع "TU" الحل يُعيد التابِع date.getDay()‎ رقم اليوم من الأسبوع، بدءًا من يوم الأحد. لنصنع مصفوفة فيها أيام الأسبوع لنعرف اليوم الصحيح من رقمه: function getWeekDay(date) { let days = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA']; return days[date.getDay()]; } let date = new Date(2014, 0, 3); // ‫3 يناير 2014 alert( getWeekDay(date) ); // FR اليوم من الأسبوع في أوروبا الأهمية: 4 في الدول الأوروبية، يبدأ الأسبوع بيوم الإثنين (رقم 1) وثمّ الثلاثاء (رقم 2) وحتّى الأحد (رقم 7). اكتب دالة getLocalDay(date) تُعيد يوم الأسبوع ”الأوروبي“ من التاريخ date. let date = new Date(2012, 0, 3); // ‫3 يناير 2012 alert( getLocalDay(date) ); // يكون يوم ثلاثاء، يجب أن تعرض 2 الحل function getLocalDay(date) { let day = date.getDay(); if (day == 0) { // يوم الأحد 0 في أوروبا هو الأخير (7)‏ day = 7; } return day; } ما هو التاريخ الذي كان قبل كذا يوم؟ الأهمية: 4 أنشِئ دالة getDateAgo(date, days)‎ تُعيد بتمرير التاريخ date اسم اليوم من الشهر قبل فترة days يوم. مثال: لو كان اليوم العشرون من الشهر، فتُعيد getDateAgo(new Date(),1 )‎ التاسع عشر وgetDateAgo(new Date(), 2)‎ الثامن عشر. يجب أن نعوّل بأن تعمل الدالة في حال days=356 وأكثر حتّى: let date = new Date(2015, 0, 2); alert( getDateAgo(date, 1) ); // ‫1، (1 يناير 2015) alert( getDateAgo(date, 2) ); // ‫31، (31 ديسمبر 2014) alert( getDateAgo(date, 365) ); // ‫2، (2 يناير 2014) ملاحظة: يجب ألّا تُعدّل الدالة التاريخ date المُمرّر. الحل الفكرة بسيطة، أن نطرح عدد الأيام من التاريخ date: function getDateAgo(date, days) { date.setDate(date.getDate() - days); return date.getDate(); } ولكن… يجب ألّا تُعدّل الدالة على date. هذا مهم إذ أنّ الشيفرة خارج الدالة التي تُعطينا التاريخ لا تريد منّا تغييره. لننفّذ ذلك، علينا نسخ التاريخ هكذا أولًا: function getDateAgo(date, days) { let dateCopy = new Date(date); dateCopy.setDate(date.getDate() - days); return dateCopy.getDate(); } let date = new Date(2015, 0, 2); alert( getDateAgo(date, 1) ); // ‫1، (1 يناير 2015) alert( getDateAgo(date, 2) ); // ‫31، (31 ديسمبر 2014) alert( getDateAgo(date, 365) ); // ‫2، (2 يناير 2014) آخر يوم من الشهر كذا؟ الأهمية: 5 اكتب دالة getLastDayOfMonth(year, month)‎ تُعيد آخر يوم من الشهر. أحيانًا يكون الثلاثين، أو الحادي والثلاثين أو الثامن/التاسع عشر من فبراير. المُعاملات: year -- العام بأربع خانات، مثلًا 2012. month -- الشهر من 0 إلى 11. مثال: getLastDayOfMonth(2012, 1) = 29 (سنة كبيسة، فبراير). الحل فلنصنع تاريخًا باستعمال الشهر التالي، ولكنّ نمرّر الصفر ليكون رقم اليوم: function getLastDayOfMonth(year, month) { let date = new Date(year, month + 1, 0); return date.getDate(); } alert( getLastDayOfMonth(2012, 0) ); // 31 alert( getLastDayOfMonth(2012, 1) ); // 29 alert( getLastDayOfMonth(2013, 1) ); // 28 عادةً ما تبدأ التواريخ بالواحد، لكن يمكننا (تقنيًا) تمرير أيّ عدد وسيُعدّل التاريخ نفسه. لذا حين نمرّر 0 نعني بذلك ”يومًا واحد قبل الأول من الشهر“، أي ”اليوم الأخير من الشهر الماضي“. كم من ثانية مضت اليوم؟ الأهمية: 5 اكتب دالة getSecondsToday()‎ تُعيد عدد الثواني منذ بداية هذا اليوم. فمثلًا لو كانت الساعة الآن 10:00 am، وبدون التوقيت الصيفي، فستعطينا الدالة: getSecondsToday() == 36000 // (3600 * 10) يجب أن تعمل الدالة مهما كان اليوم. أيّ ألا تحتوي على قيمة داخلها بتاريخ ”اليوم“… اليوم. الحل لنعرف عدد الثواني يمكننا توليد تاريخًا باستعمال اليوم الحالي والساعة 00:00:00، وثمّ نطرح منها ”الوقت والتاريخ الآن“. سيكون الفرق حينها بعدد المليثوان منذ بداية هذا اليوم، فنقسمه على 1000 لنعرف الثواني فقط: function getSecondsToday() { let now = new Date(); // أنشِئ كائنًا باستعمال اليوم والشهر والسنة حاليًا let today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); let diff = now - today; // الفرق بالمليثانية return Math.round(diff / 1000); // نحوّله إلى ثوان } alert( getSecondsToday() ); الحل الآخر هو جلب الساعة والدقيقة والثانية وتحويلها إلى عدد الثواني: function getSecondsToday() { let d = new Date(); return d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds(); } كم من ثانية بقت حتّى الغد؟ الأهمية: 5 أنشِئ دالة getSecondsToTomorrow()‎ تُعيد عدد الثواني حتّى يحلّ الغد. فمثلًا لو كان الوقت الآن 23:00، تُعيد لنا: getSecondsToTomorrow() == 3600 ملاحظة: يجب أن تعمل الدالة مهما كان اليوم، وألا تعتبر ”اليوم“ هذا اليوم. الحل لنعرف عدد المليثوان حتّى قدوم الغد، يمكننا أن نطرح من ”الغد 00:00:00“ التاريخ اليوم. أوّلًا، نولّد هذا ”الغد“ وثمّ ننفّذ الطرح: function getSecondsToTomorrow() { let now = new Date(); // تاريخ الغد let tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate()+1); let diff = tomorrow - now; // الفرق بالمليثانية return Math.round(diff / 1000); // نحوّله إلى ثوان } حل بديل: function getSecondsToTomorrow() { let now = new Date(); let hour = now.getHours(); let minutes = now.getMinutes(); let seconds = now.getSeconds(); let totalSecondsToday = (hour * 60 + minutes) * 60 + seconds; let totalSecondsInADay = 86400; return totalSecondsInADay - totalSecondsToday; } لاحظ أنّ هناك دولًا كثيرة تستعمل التوقيت الصيفي، لذا ستجد هناك أيام فيها 23 أو 25 ساعة. يمكن أن نتعامل مع هذه الأيام بنحوٍ منفصل. تنسيق التاريخ نسبيًا الأهمية: 4 اكتب دالة formatDate(date)‎ تُنسّق التاريخ date حسب الآتي: لو مرّت أقلّ من ثانية من date، فتُعيد "right now". وإلّا، لو مرّت أقلّ من دقيقة من date، فتُعيد "n sec. ago". وإلّا، لو أقل من ساعة، فتُعيد "m min. ago". وإلّا، فتُعيد التاريخ كاملًا بالتنسيق "DD.MM.YY HH:mm"، أي (شَكلًا): الدقيقة:الساعة العام:الشهر:اليوم (كلها بخانتين). مثل: 31.12.16 10:00. أمثلة: alert( formatDate(new Date(new Date - 1)) ); // "right now" alert( formatDate(new Date(new Date - 30 * 1000)) ); // "30 sec. ago" alert( formatDate(new Date(new Date - 5 * 60 * 1000)) ); // "5 min. ago" // ‫تاريخ الأمس، مثلًا ‎31.12.16, 20:00 alert( formatDate(new Date(new Date - 86400 * 1000)) ); الحل لنجلب الوقت المنقضي منذ date وحتّى الآن، سنطرح التاريخين. function formatDate(date) { let diff = new Date() - date; // الفرق بالمليثانية if (diff < 1000) { // أقل من ثانية واحدة return 'right now'; } let sec = Math.floor(diff / 1000); // نحوّل الفرق إلى ثوان if (sec < 60) { return sec + ' sec. ago'; } let min = Math.floor(diff / 60000); // نحوّل الفرق إلى دقائق if (min < 60) { return min + ' min. ago'; } // ننسّق التاريخ // ونُضيف أصفارًا لو كان اليوم/الشهر/الساعة/الدقيقة بخانة واحدة let d = date; d = [ '0' + d.getDate(), '0' + (d.getMonth() + 1), '' + d.getFullYear(), '0' + d.getHours(), '0' + d.getMinutes() ].map(component => component.slice(-2)); // نأخذ الخانتين الأخيرتين من كلّ مكوّن // ندمج المكوّنات في تاريخ return d.slice(0, 3).join('.') + ' ' + d.slice(3).join(':'); } alert( formatDate(new Date(new Date - 1)) ); // "right now" alert( formatDate(new Date(new Date - 30 * 1000)) ); // "30 sec. ago" alert( formatDate(new Date(new Date - 5 * 60 * 1000)) ); // "5 min. ago" // ‫تاريخ الأمس، مثلًا ‎31.12.16, 20:00 alert( formatDate(new Date(new Date - 86400 * 1000)) ); حل بديل: function formatDate(date) { let dayOfMonth = date.getDate(); let month = date.getMonth() + 1; let year = date.getFullYear(); let hour = date.getHours(); let minutes = date.getMinutes(); let diffMs = new Date() - date; let diffSec = Math.round(diffMs / 1000); let diffMin = diffSec / 60; let diffHour = diffMin / 60; // التنسيق year = year.toString().slice(-2); month = month < 10 ? '0' + month : month; dayOfMonth = dayOfMonth < 10 ? '0' + dayOfMonth : dayOfMonth; if (diffSec < 1) { return 'right now'; } else if (diffMin < 1) { return `${diffSec} sec. ago` } else if (diffHour < 1) { return `${diffMin} min. ago` } else { return `${dayOfMonth}.${month}.${year} ${hour}:${minutes}` } } لاحظ بأنّ هذه الطريقة سيّئة لو أردت دعم اللغات دعمًا صحيحًا (في العربية هناك ثانية واحدة وثانيتين وثلاث ثوان وخمسون ثانية وهكذا). .task__importance { color: #999; margin-left: 30px; } .task__answer { border: 3px solid #f7f6ea; margin: 20px 0 14px; position: relative; display: block; padding: 25px 30px; } اقرأ أيضًا المقال التالي: صيغة JSON وتوابعها المقال السابق: الإسناد بالتفكيك (Destructuring assignment) ترجمة -وبتصرف- للفصل Date and time من كتاب The JavaScript language
  18. في جافاسكربت، الكائنات والمصفوفات هي أكثر بنى البيانات المستعملة. تُتيح لنا الكائنات إنشاء كيان واحد يُخزّن عناصر البيانات حسب مفاتيحها، وتُتيح لنا المصفوفات بجمع مختلف عناصر البيانات في تجميعة مرتّبة (ordered collection). ولكن حين نُمرّرها هذه الكائنات والمصفوفات إلى دالة، غالبًا ما لا نريد كامل الكائن/المصفوفة، بل بعضًا منها لا أكثر. صياغة الإسناد بالتفكيك (Destructuring assignment) هي صياغة خاصّة تُتيح لنا ”فكّ“ المصفوفات أو الكائنات إلى مجموعة من المتغيرات إذ تكون أحيانًا أكثر منطقية. يفيدنا التفكيك أيضًا مع الدوال المعقّدة التي تحتوي على مُعاملات كثيرة وقيم مبدئية وغيرها وغيرها. تفكيك المصفوفات إليك مثال عن تفكيك مصفوفة إلى مجموعة من المتغيرات: // معنا مصفوفة فيها اسم الشخص واسم عائلته let arr = ["Ilya", "Kantor"] // يضبط الإسناد بالتفكيك // ‫هذه firstName = arr[0]‎ // ‫وهذه surname = arr[1]‎ let [firstName, surname] = arr; alert(firstName); // Ilya alert(surname); // Kantor يمكننا الآن العمل مع تلك المتغيرات عوض عناصر المصفوفة. وما إن تجمع تابِع split وغيرها من توابِع تُعيد مصفوفات، سترى بريق هذا التفكيك يتألق: let [firstName, surname] = "Ilya Kantor".split(' '); ”التفكيك“ (Destructuring) لا يعني ”التكسير“ (destructive) نُسمّيه "الإسناد بالتفكيك" (destructuring assignment) لأنّه "يفكّك" العناصر بنسخها إلى متغيرات. أمّا المصفوفة نفسها فتبقى دون تعديل. كتابة هذه الشيفرة أسهل من تلك الطويلة (ندعها لك تتخيّلها): // let [firstName, surname] = arr; let firstName = arr[0]; let surname = arr[1]; أهمِل العناصر باستعمال الفواصل يمكنك ”رمي“ وتجاهل العناصر التي لا تريدها بإضافة فاصلة أخرى: // لا نريد العنصر الثاني let [firstName, , title] = ["Julius", "Caesar", "Consul", "of the Roman Republic"]; alert( title ); // Consul في الشيفرة هذه تخطّينا العنصر الثاني في المصفوفة وأسندنا الثالث إلى المتغير title، كما وتخطّينا أيضًا باقي عناصر المصفوفة (ما من متغيّرات لها). تعمل الميزة مع المُتعدَّدات حين تكون على اليمين …الواقع هو أنّنا نستطيع استعمالها مع أيّ مُكرَّر وليس المصفوفات فقط: let [a, b, c] = "abc"; // ["a", "b", "c"] let [one, two, three] = new Set([1, 2, 3]); أسنِدها إلى ما تريد على اليسار يمكن أن نستعمل أيّ متغيّر يمكن إسناده على الجانب الأيسر من سطر الإسناد. لاحظ مثلًا إسناد خاصية لكائن: let user = {}; [user.name, user.surname] = "Ilya Kantor".split(' '); alert(user.name); // Ilya المرور على العناصر عبر ‎.entries()‎ رأينا في الفصل الماضي التابِع Object.entries(obj)‎. يمكننا استعماله مع التفكيك للمرور على مفاتيح الكائنات وقيمها: let user = { name: "John", age: 30 }; // نمرّ على المفاتيح والقيم for (let [key, value] of Object.entries(user)) { alert(`${key}:${value}`); // name:John, then age:30 } …وذات الأمر للخارطة: let user = new Map(); user.set("name", "John"); user.set("age", "30"); for (let [key, value] of user) { alert(`${key}:${value}`); // name:John, then age:30 } الباقي ”…“ لو أردنا أخذ القيم الأولى إضافةً إلى كل ما يليها، فنُضيف مُعاملًا آخر يجلب ”الباقي“ باستعمال ثلاث نقاط "...": let [name1, name2, ...rest] = ["Julius", "Caesar", "Consul", "of the Roman Republic"]; alert(name1); // Julius alert(name2); // Caesar // ‫انتبه أنّ المتغير rest مصفوفة. alert(rest[0]); // Consul alert(rest[1]); // of the Roman Republic alert(rest.length); // 2 ستكون قيمة المتغير rest مصفوفةً فيها عناصر الباقية في المصفوفة الأولى. يمكننا استعمال أيّ اسم آخر بدل rest، المهم أن يكون قبله ثلاث نقاط ويكون الأخير في جملة الإسناد بالتفكيك. القيم المبدئية لو كانت القيم في المصفوفة أقلّ من تلك في جملة الإسناد فلن يحدث أيّ خطأ. يُعدّ المحرّك القيم ”الغائبة“ غير معرّفة: let [firstName, surname] = []; alert(firstName); // undefined alert(surname); // undefined لو أردنا قيمة مبدئية تعوّض تلك الناقصة فيمكننا تقديمها باستعمال =: // القيم المبدئية let [name = "Guest", surname = "Anonymous"] = ["Julius"]; alert(name); // ‫Julius (من المصفوفة) alert(surname); // ‫Anonymous (المبدئي) يمكن أن تكون القيم المبدئية تعابيرَ معقّدة أو استدعاءات دوال حتّى. لن يقدّر ناتجها المحرّك إلّا لو لم تمرّر القيم تلك. فمثلًا يمكننا استعمال الدالة promot لأخذ قيمتين مبدئيتين. أمّا هنا فستسأل عن القيمة الناقصة فقط: // ‫لا تطلب إلا اسم العائلة surname let [name = prompt('name?'), surname = prompt('surname?')] = ["Julius"]; alert(name); // ‫Julius (نأخذه من المصفوفة) alert(surname); // ‫نحترم هنا ما يقول promot تكفيك الكائنات الإسناد بالتفكيك يدعم أيضًا الكائنات. هذه صياغته الأساس: let {var1, var2} = {var1:…, var2:…} على اليمين الكائن الموجود والذي نريد تقسيمه على عدّة متغيرات، وعلى اليسار نضع ”نمط“ الخاصيات المقابِلة له. لو كان الكائن بسيطًا، فهذا النمط هو قائمة باسم المتغيرات داخل {...}. مثال: let options = { title: "Menu", width: 100, height: 200 }; let {title, width, height} = options; alert(title); // Menu alert(width); // 100 alert(height); // 200 تُسند الخاصيات options.title وoptions.width وoptions.height إلى المتغيرات المقابِلة لها. كما وأنّ الترتيب غير مهم: يمكنك فعل هذا أيضًا: // ‫غيّرنا الترتيب داخل ‪let {...} let {height, width, title} = { title: "Menu", height: 200, width: 100 } يمكن أن يكون النمط على اليسار معقّدًا أكثر ومُحدّدًا فيحدّد طريقة ترابط الخاصيات بالمتغيرات عبر الخارطة (mapping). لو أردنا إسناد خاصية إلى متغير له اسم آخر فعلينا استعمال النقطتين الرأسيتين لذلك (مثلًا options.width يصير في المتغير w): let options = { title: "Menu", width: 100, height: 200 }; // { sourceProperty: targetVariable } let {width: w, height: h, title} = options; // width -> w // height -> h // title -> title alert(title); // Menu alert(w); // 100 alert(h); // 200 تعني النقطتان الرأسيتان ”هذا : يصير هذا“. في المثال أعلاه، تصير الخاصية width بالاسم w، والخاصية height بالاسم h والخاصية title كما هي title. يمكننا هنا أيضًا وضع قيمة مبدئية للخاصيات الناقصة باستعمال "=" هكذا: let options = { title: "Menu" }; let {width = 100, height = 200, title} = options; alert(title); // Menu alert(width); // 100 alert(height); // 200 كما يمكن أن تكون هذه القيم المبدئية أيّة تعابير أو استدعاءات دوال كما مقابلاتها في المصفوفات ومُعاملات الدوال، ولن يقدّر المحرّك قيمتها إلّا لو لم تقدّم قيمة للدالة. في الشيفرة أدناه، تطلب الدالة promot قيمة width ولا تطلب قيمة title: let options = { title: "Menu" }; *!* let {width = prompt("width?"), title = prompt("title?")} = options; */!* alert(title); // Menu alert(width); // (promot هنا نحترم أيضًا ما يقول) يمكننا أيضًا جمع النقطتان الرأسيتان والقيم المبدئية: let options = { title: "Menu" }; let {width: w = 100, height: h = 200, title} = options; alert(title); // Menu alert(w); // 100 alert(h); // 200 لو كان لدينا كائنًا معقّدًا فيه خاصيات كثيرة، فيمكننا استخراج ما نريد منه فقط: let options = { title: "Menu", width: 100, height: 200 }; // ‫استخرج العنوان title ليكون متغيرًا هو فقط let { title } = options; alert(title); // Menu نمط الباقي ”…“ ماذا لو كان للكائن خاصيات أكثر من المتغيرات التي لدينا؟ هل يمكننا أخذها وإسنادها في متغيّر ”rest“ أيضًا؟ أجل يمكننا استعمال نمط الباقي تمامًا مثل المصفوفات. بعض المتصفحات القديمة لا تدعمه (مثل إنترنت إكسبلورر، استعمل Babel لترقيعه polyfill، أي لتعويض نقص الدعم)، إلّا أن الحديثة تدعمه. هكذا نفعلها: let options = { title: "Menu", height: 200, width: 100 }; // ‫title = خاصية بالاسم title // ‫rest = كائن فيه باقي الخاصيات let {title, ...rest} = options; // ‫صار الآن title="Menu", rest={height: 200, width: 100} alert(rest.height); // 200 alert(rest.width); // 100 انتبه لو لم تضع let في المثال أعلاه، صرّحنا عن المتغيرات على يمين جملة الإسناد: let {…} = {…}‎. يمكننا طبعًا استعمال متغيرات موجودة دون let، ولكن هناك أمر، فهذا لن يعمل: let title, width, height; // سترى خطأ في هذا السطر {title, width, height} = {title: "Menu", width: 200, height: 100}; المشكلة هي أنّ جافاسكربت تتعامل مع {...} في سياق الشيفرة الأساس (أي ليس داخل تعبير آخر) على أنّها بنية شيفرة (Code Block). يمكن استعمال بنى الشيفرة هذه لجمع التعليمات البرمجية، هكذا: { // بنية شيفرة let message = "Hello"; // ... alert( message ); } وهنا يظنّ محرّك جافاسكربت بأنّ هذه بنية شيفرة فيعطينا الخطأ أعلاه، بينما ما نريد هو التفكيك. ولنقول للمحرّك بأنّ هذه ليست بنية شيفرة، نضع التعبير بين قوسين (...): let title, width, height; // الآن جيد ({title, width, height} = {title: "Menu", width: 200, height: 100}); alert( title ); // Menu تفكيك المتغيرات المتداخلة لو كان في الكائن أو المصفوفة كائنات ومصفوفات أخرى داخله، فيمكننا استعمال أنماط معقّدة على يسار جملة الإسناد لنستخرج تلك المعلومات. في الشيفرة أدناه، نجد داخل الكائن options كائنًا آخر في الخاصية size، ومصفوفة في الخاصية items. النمط على يسار جملة الإسناد لديه ذات البنية تلك لتستخرج هذه القيم من الكائن على يمينه: let options = { size: { width: 100, height: 200 }, items: ["Cake", "Donut"], extra: true }; // نقسم الإسناد بالتفكيك على أكثر من سطر لتوضيح العملية let { size: { // هنا يكون المقاس width, height }, items: [item1, item2], // وهنا نضع العناصر title = "Menu" // ليست موجودة في الكائن (ستُستعمل القيمة المبدئية)‏ } = options; alert(title); // Menu alert(width); // 100 alert(height); // 200 alert(item1); // Cake alert(item2); // Donut هكذا تُسند كلّ خاصيات options (عدا extra الناقصة يسار عبارة الإسناد) إلى المتغيرات المقابلة لها: وفي النهاية يكون لدينا المتغيّرات width وheight وitem1 وitem2 وtitle من تلك القيمة المبدئية. لاحظ ألّا وجود لمتغيّرات تنسخ size وitems إذ ما نريد هو محتواها لا هي. مُعاملات الدوال الذكية أحيانًا وأنت تعمل تجد نفسك تكتب دالة لها مُعاملات كثيرة وأغلبها اختيارية. يحدث هذا غالبًا مع دوال واجهات المستخدم. عُدّ أنّ لديك دالة تُنشئ قائمة، وللقائمة عَرض وارتفاع وعنوان وقائمة عناصر وغيرها. هكذا تصنع تلك الدالة بالأسلوب الخطأ: function showMenu(title = "Untitled", width = 200, height = 100, items = []) { // ... } تكمن المشكلة (في الحياة الواقعية) في تذكّر ترتيب تلك الوُسطاء. صحيح أنّ بيئات التطوير تفيدنا هنا عادةً -خصوصًا لو كان المشروع موثّق توثيقًا ممتازًا- ولكن مع ذلك فالمشكلة الأخرى هي طريقة استدعاء الدالة لو كانت كلّ مُعاملاتها المبدئية مناسبة لنا. نستدعيها هكذا؟ // نضع ‫undefined لو كانت القيم المبدئية تقوم بالغرض showMenu("My Menu", undefined, undefined, ["Item1", "Item2"]) جريمة بحقّ الجمال. ورويدًا رويدًا تصير مستحيلة القراءة حين نُضيف مُعاملات أخرى. التفكيك لنا بالمرصاد… أعني للعون! فيمكننا تمرير المُعاملات بصيغة كائن، وستُفكّكها الدالة حالًا في متغيرات: // نمرّر كائنًا إلى الدالة let options = { title: "My menu", items: ["Item1", "Item2"] }; // ‫...ومباشرة تفكّها وتضعها في متغيرات function showMenu({title = "Untitled", width = 200, height = 100, items = []}) { // ‫title, items – هذه من options // ‫width, height – نستعمل القيم المبدئية alert( `${title} ${width} ${height}` ); // My Menu 200 100 alert( items ); // Item1, Item2 } showMenu(options); يمكننا أيضًا استعمال التفكيك الأكثر تعقيدًا (مع الكائنات المتداخلة وتغيير الأسماء بالنقطتين الرأسيتين): let options = { title: "My menu", items: ["Item1", "Item2"] }; function showMenu({ title = "Untitled", width: w = 100, // نضع ‫width في w height: h = 200, // ‫ونضع height في h items: [item1, item2] // ‫أوّل عنصر في items يصير item1، وثاني عنصر يصير item2 }) { alert( `${title} ${w} ${h}` ); // My Menu 100 200 alert( item1 ); // Item1 alert( item2 ); // Item2 } showMenu(options); صياغة الدالة الكاملة تتطابق مع صياغة الإسناد بالتفكيك: function({ incomingProperty: varName = defaultValue ... }) وحينها متى ما تمرّر كائن على أساس أنّه مُعامل، نضع الخاصية incomingProperty في المتغير varName وقيمته المبدئية هي defaultValue. لاحظ بأنّ هذا النوع من التفكيك ينتظر مُعاملًا واحدًا على الأقل في الدالة showMenu()‎. لو أردنا أن تكون كلّ القيم كما هي مبدئيًا، فعلينا تقديم كائن فارغ: showMenu({}); // هكذا، كل القيم كما هي مبدئيًا showMenu(); // هذا سيصرخ علينا بخطأ يمكننا إصلاح هذه المشكلة بتحديد {} قيمةً مبدئيةً لكامل الكائن الذي يحوي المُعاملات: function showMenu({ title = "Menu", width = 100, height = 200 }*!* = {}*/!*) { alert( `${title} ${width} ${height}` ); } showMenu(); // Menu 100 200 ملخص يتيح الإسناد بالتفكيك ربط الكائن أو المصفوفة مع متغيرات عديدة أخرى، وآنيًا. صياغة الكائن الكاملة هي: let {prop : varName = default, ...rest} = object ويعني هذا بأنّ الخاصية prop تصير في المتغيّر varName، وفي حال لم توجد هذه الخاصية فستُستعمل القيمة المبدئية default. تُنسح حاصيات الكائنات التي لا ترتبط إلى الكائن rest. صياغة المصفوفة الكاملة هي: let [item1 = default, item2, ...rest] = array يصير أوّل عنصر في item1 وثاني عنصر في item2 وباقي المصفوفة يصير باقيًا في rest. يمكن أيضًا استخراج البيانات من المصفوفات/الكائنات المتداخلة، ويلزم أن تتطابق بنية على يسار الإسناد تلك على يمينه. تمارين الإسناد بالتفكيك الأهمية: 5 لدينا هذا الكائن: let user = { name: "John", years: 30 }; اكتب إسنادًا بالتفكيك يقرأ: خاصية name ويضعها في المتغير name. خاصية years ويضعها في المتغير age. خاصية isAdmin ويضعها في المتغير isAdmin (تكون false لو لم تكن موجودة) إليك مثالًا بالقيم بعد إجراء الإسناد: let user = { name: "John", years: 30 }; // ض‫ع شيفرتك على الجانب الأيسر: // ... = user alert( name ); // John alert( age ); // 30 alert( isAdmin ); // false الحل let user = { name: "John", years: 30 }; let {name, years: age, isAdmin = false} = user; alert( name ); // John alert( age ); // 30 alert( isAdmin ); // false أكبر راتب الأهمية: 5 إليك كائن الرواتب salaries: let salaries = { "John": 100, "Pete": 300, "Mary": 250 }; اكتب دالة topSalary(salaries) تُعيد اسم الشخص الأكثر ثراءً وراتبًا. لو كان salaries فارغًا فيجب أن تُعيد null. لو كان هناك أكثر من شخص متساوي الراتب، فتُعيد أيًا منهم. ملاحظة: استعمل Object.entries والإسناد بالتفكيك للمرور على أزواج ”مفاتيح/قيم“. الحل function topSalary(salaries) { let max = 0; let maxName = null; for(const [name, salary] of Object.entries(salaries)) { if (max < salary) { max = salary; maxName = name; } } return maxName; } ترجمة -وبتصرف- للفصل Destructuring assignment من كتاب The JavaScript language .task__importance { color: #999; margin-left: 30px; } .task__answer { border: 3px solid #f7f6ea; margin: 20px 0 14px; position: relative; display: block; padding: 25px 30px; } اقرأ أيضًا المقال التالي: النوع Date: تمثيل التاريخ والوقت المقال السابق: مفاتيح الكائنات وقيمها ومدخلاتها
  19. لنأخذ راحة صغيرة بعيدًا عن بنى البيانات ولنتحدّث عن طريقة المرور على عناصرها. رأينا في الفصل السابق التوابِع map.keys()‎ وmap.values()‎ وmap.entries()‎. هذه التوابِع عامّة وقد اتّفق معشر المطوّرين على استعمالها عند التعامل مع بنى البيانات. ولو أنشأنا بنية بيانات من الصفر بيدنا، فعلينا توفير "تنفيذ" تلك التوابِع أيضًا. هي أساسًا مدعومة لكلّ من: الخرائط Map الأطقم Set المصفوفات Array كما وتدعم الكائنات العادية توابِع كتلك التوابِع باختلاف بسيط في صياغتها. التوابِع keys وvalues وentries هذه هي التوابِع المتاحة للتعامل مع الكائنات العادية: Object.keys(obj)‎ -- يُعيد مصفوفة من المفاتيح. Object.values(obj)‎ -- يُعيد مصفوفة من القيم. Object.entries(obj)‎ -- يُعيد مصفوفة من أزواج [key, value]. لاحظ رجاءً الفروق بينها وبين الخارطة مثلًا: الخارطة الكائن صياغة الاستدعاء map.keys() Object.keys(obj)‎ لكن ليس obj.keys()‎ قيمة الإعادة مُكرَّر مصفوفة ”حقيقية“ أوّل فرق واضح جليّ: علينا استدعاء Object.keys(obj)‎ لا obj.keys()‎. ولكن لماذا؟ السبب الأساس هو مرونة الاستعمال. لا تنسَ بأنّ الكائنات هي أساس كلّ بنية بيانات معقّدة في جافاسكربت. يحدث بأنّ لدينا كائن طوّرناه ليحمل بيانات data محدّدة، وفيه التابِع data.values()‎، ولكنّا نريد أيضًا استدعاء Object.values(data)‎ عليه. الفرق الثاني هو أنّ التوابِع Object.* تُعيد كائنات مصفوفات ”فعلية“ لا مُتعدَّدات فقط. يعزو ذلك لأسباب تاريخية بحتة. خُذ هذا المثال: let user = { name: "John", age: 30 }; Object.keys(user) = ["name", "age"]‎ Object.values(user) = ["John", 30]‎ Object.entries(user) = [ ["name","John"], ["age",30] ]‎ وهذا مثال آخر عن كيف نستعمل Object.values للمرور على قيم الخاصيات: let user = { name: "John", age: 30 }; // نمرّ على القيم for (let value of Object.values(user)) { alert(value); // ‫John ثمّ 30 } تتجاهل هذه التوابِع الخاصيات الرمزية كما تتجاهل حلقة for..in الخاصيات التي تستعمل Symbol(...)‎ مفاتيح لها، فهذه التوابِع أعلاه تتجاهلها أيضًا غالبًا يكون هذا ما نريد، ولكن لو أردت المفاتيح الرمزية أيضًا، فعليك استعمال التابِع المنفصل Object.getOwnPropertySymbols إذ يُعيد مصفوفة بالمفاتيح الرمزية فقط. هناك أيضًا التابِع Reflect.ownKeys(obj) إذ يُعيد المفاتيح كلها. تعديل محتوى الكائنات ليس للكائنات تلك التوابِع المفيدة المُتاحة للعناصر (مثل map وfilter وغيرها). لو أردنا تطبيق هذه التوابِع على الكائنات فيجب أوّلًا استعمال Object.entries وبعدها Object.fromEntries: استعمل Object.entries(obj)‎ لتأخذ مصفوفة لها أزواج ”مفتاح/قيمة“ من الكائن obj. استعمل توابِع المصفوفات على تلك المصفوفة (مثلًا map). استعمل Object.fromEntries(array)‎ على المصفوفة الناتج لتُحوّلها ثانيةً إلى كائن. إليك مثالًا لدينا كائنًا فيه تسعير البضائع، ونريد مضاعفتها (إذ ارتفع الدولار): let prices = { banana: 1, orange: 2, meat: 4, }; let doublePrices = Object.fromEntries( // ‫نحوّله إلى مصفوفة، ثمّ نستعمل الطقم، ثمّ يُعيد إلينا fromEntries الكائن المطلوب Object.entries(prices).map(([key, value]) => [key, value * 2]) ); alert(doublePrices.meat); // 8 ربّما تراه صعبًا أوّل وهلة، ولكن لا تقلق فسيصير أسهل أكثر متى ما بدأت استعمالها مرّة واثنتان وثلاث. يمكن أن نصنع سلسلة فعّالة من التعديلات بهذه الطريقة: تمارين مجموع الخاصيات الأهمية: 5 أمامك كائن salaries وفيه بعض الرواتب. اكتب دالة sumSalaries(salaries) تُعيد مجموع كلّ الرواتب، باستعمال Object.values وحلقة for..of. لو كان الكائن فارغًا فيجب أن يكون الناتج صفرًا 0. مثال: let salaries = { "John": 100, "Pete": 300, "Mary": 250 }; alert( sumSalaries(salaries) ); // 650 الحل function sumSalaries(salaries) { let sum = 0; for (let salary of Object.values(salaries)) { sum += salary; } return sum; // 650 } let salaries = { "John": 100, "Pete": 300, "Mary": 250 }; alert( sumSalaries(salaries) ); // 650 أو يمكننا (لو أردنا) معرفة المجموع باستعمال Object.values والتابِع reduce: // ‫يمرّ reduce على مصفوفة من الرواتب، // ويجمعها مع بعضها ويُعيد الناتج function sumSalaries(salaries) { return Object.values(salaries).reduce((a, b) => a + b, 0) // 650 } عدد الخاصيات الأهمية: 5 اكتب دالة باسم count(obj)‎ تُعيد عدد الخاصيات داخل الكائن: let user = { name: 'John', age: 30 }; alert( count(user) ); // 2 حاوِل أن تكون الشيفرة بأصغر ما أمكن. ملاحظة: أهمِل الخاصيات الرمزية وعُدّ فقط تلك ”العادية“. الحل function count(obj) { return Object.keys(obj).length; } ترجمة -وبتصرف- للفصل Object.keys, values, entries من كتاب 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; } .task__importance { color: #999; margin-left: 30px; } .task__answer { border: 3px solid #f7f6ea; margin: 20px 0 14px; position: relative; display: block; padding: 25px 30px; } اقرأ أيضًا المقال التالي: الإسناد بالتفكيك (Destructuring assignment) المقال السابق: النوع WeakMap والنوع WeakSet: الخرائط والأطقم ضعيفة الإشارة
  20. كما عرفنا من فصل «كنس المهملات»، فمُحرّك جافاسكربت يخُزّن القيمة في الذاكرة طالما يمكن أن يصل لها شيء (أي يمكن استعمالها لاحقًا). هكذا: let john = { name: "John" }; // ‫يمكننا الوصول إلى الكائن، فـ john هو الإشارة إليه // عوّض تلك الإِشارة john = null; // سيُزال الكائن من الذاكرة عادةً ما تكون خاصيات الكائن أو عناصر المصفوفة أو أية بنية بيانات أخرى - عادةً ما تُعدّ "مُتاحة لباقي الشيفرة" ويُبقيها المحرّك في الذاكرة طالما بنية البيانات نفسها في الذاكرة. لنفترض أنّا وضعنا كائنًا في مصفوفة، طالما المصفوفة موجودة ومُشار إليها، فسيكون الكائن موجودًا هو الآخر حتّى لو لم يكن هناك ما يُشير إليه. مثلما في هذه الشيفرة: let john = { name: "John" }; let array = [ john ]; john = null; // عوّض الإشارة // ‫الكائن john مخزّن داخل مصفوفة ولن يُكنس باعتباره مهملات // ‫إذ يمكننا أخذه بهذه: array[0]‎ وبنفس المفهوم، لو استعملنا كائنًا ليكون مفتاحًا في خارطة Map عادية، فسيبقى هذا الكائن موجدًا طالما الخارطة تلك موجودة، ويشغل الذاكرة مانعًا عملية كنس المهملات من تحريرها. إليك هذا المثال: let john = { name: "John" }; let map = new Map(); map.set(john, "..."); john = null; // عوّض الإشارة // ‫الكائن john مخزّن داخل خارطة // ‫ويمكننا أخذه بهذه: map.keys()‎ على العكس فالخارطة ضعيفة الإشارة WeakMap مختلفة جذريًا عن هذا، فلا تمنع كنس مهملات أيٍّ من مفاتيحها الكائنات. لنأخذ بعض الأمثلة لتُدرك القصد هنا. WeakMap أولى اختلافات الخارطة ضعيفة الإشارة WeakMap عن تلك العادية Map هي أنّها تُلزم مفاتيحها بأن تكون كائنات لا أنواع أولية: let weakMap = new WeakMap(); let obj = {}; weakMap.set(obj, "ok"); // لا مشاكل (المفتاح كائن) // لا يمكن استعمال السلسلة النصية مفتاحًا weakMap.set("test", "Whoops"); // ‫خطأ، لأنّ ”test“ ليس كائنًا بعد ذلك لو استعملنا أحد الكائنات ليكون مفتاحًا فيها، ولم يكن هناك ما يُشير إلى هذا الكائن، فسيُزال الكائن من الذاكرة (والخارطة) تلقائيا. let john = { name: "John" }; let weakMap = new WeakMap(); weakMap.set(john, "..."); john = null; // عوّض الإشارة // ‫أُزيل الكائن john من الذاكرة! وازِن هذه الشيفرة بشيفرة الخارطة Map أعلاه. الآن حتى لو لم يكن john موجودًا إلا مفتاحًا لِـ WeakMap، فسيُحذف تلقائيًا من الخارطة (ومن الذاكرة). لا تدعم الخارطة ضعيفة الإشارة WeakMap التكرار (iteration) ولا التوابِع keys()‎ أو values()‎ أو entries()‎، ولهذا لا نقدر على أخذ كلّ المفاتيح أو القيم التي فيها. بل أنّ للخارطة WeakMap التوابِع الآتية: weakMap.get(key)‎ weakMap.set(key, value)‎ weakMap.delete(key)‎ weakMap.has(key)‎ تفكّر بسبب وجود هذا التقييد؟ الجواب هو: أسباب تقنية. عُدّ الكائن الآن قد فقد كلّ إشارة له (مثلما حصل مع الكائن john في الشيفرة أعلاه)، بهذا ستُكنس مهملاته تلقائيًا، ولكن… وقت حدوث هذا الكنس غير موضّح تقنيًا. الواقع أنّ محرّك جافاسكربت يُحدّد ذلك: هو يُحدّد متى يمسح الذاكرة، الآن حالًا أو بعد قليل حتّى تحدث عمليات حذف أخرى. لذا فعدد العناصر الحالي داخل WeakMap غير معلوم تقنيًا، ربما يكون المحرّك حذفها كلها أو لم يحذفها، أو حذف بعضها، لا نعلم. لهذا السبب لا تدعم اللغة التوابِع التي تحاول الوصول إلى كلّ القيم والعناصر. الآن بعدما عرفناها، في أيّ حالات نستعمل هذه البنية من البيانات؟ استعمالاتها: بيانات إضافية المجال الرئيسي لتطبيقات WeakMap هي تخزين البيانات الإضافية. لو كنّا نتعامل مع كائن ”ينتمي“ إلى شيفرة أخرى (وحتّى مكتبة من طرف ثالث) وأردنا تخزين بيانات معيّنة لترتبط بها، وهذه البيانات لا تكون موجودة إلا لو كان الكائن موجودًا، فَـ WeakMap هي ما نريد تمامًا: نضع البيانات في خارطة بإشارة ضعيفة WeakMap (مستعملين الكائن مفتاحًا لها). متى ما كُنس الكائن باعتباره مهملات، ستختفي تلك البيانات معه أيضًا. weakMap.set(john, "secret documents"); // ‫إن مات john فستُدمّر تلك المستندات فائقة السرية تلقائيًا لنرى مثالًا يوضّح الصورة. عُدّ بأنّ لدينا شيفرة تسجّل عدد زيارات المستخدمين - تسجّلها في خارطة، حيث كائن المستخدم هو مفتاحها وعدد زياراته هي القيمة. لا نريد أن نُسجّل عدد زياراته فيما لو غادر المستخدم (أي أنّ عملية كنس المهملات كنست ذاك الكائن). إليك مثالًا آخر عن دالة عَدّ باستعمال Map: // ? visitsCount.js let visitsCountMap = new Map(); // خارطة: المستخدم => عدد زياراته // تزيد عدد الزيارات function countUser(user) { let count = visitsCountMap.get(user) || 0; visitsCountMap.set(user, count + 1); } وهذا الجزء الثاني من الشيفرة (يمكن أن يستعمل هذا الملف ذاك): // ? main.js let john = { name: "John" }; countUser(john); // عُدّ الزوّار countUser(john); // ‫بعدها يغادر john الحفلة john = null; هكذا ”يُفترض“ أن يُكنس الكائن john باعتباره مهملات، لكنّه سيبقى في الذاكرة إذ تستعمله الخارطة visitsCountMap مفتاحًا فيها. علينا مسح visitsCountMap حين نُزيل المستخدمين وإلا فسيزيد حجمها في الذاكرة إلى آباد الآبدين. لو كانت بنية البرمجية معقّدة، فستكون عملية المسح هذه مرهقة جدًا وغير عملية. لهذا يمكننا تجنّب التعب واستعمال WeakMap بدل العادية: // ? visitsCount.js let visitsCountMap = new WeakMap(); // خارطة بإشارة ضعيفة: المستخدم => عدد زياراته // تزيد عدد الزيارات function countUser(user) { let count = visitsCountMap.get(user) || 0; visitsCountMap.set(user, count + 1); } هكذا لا نمسح visitsCountMap يدويًا بل نترك للمحرّك القرار: لو لم يكن هناك ما يُشير إلى الكائن john عدا مفتاح WeakMap، سيحرّره من الذاكرة مع المعلومات التي في ذلك المفتاح داخل الخارطة ضعيفة الإشارة WeakMap. استعمالاتها: الخبيئة يكثُر أيضًا استعمال الخرائط للخبيئة، أي حين علينا تذكّر ناتج الدالة (تخبئته ”cached“) كي يستعمل أيّ استدعاء لاحِق على هذا العنصر تلك الخبيئة. يمكن أن نستعمل الخارطة Map لتخزين النتائج: // ? cache.js let cache = new Map(); // نحسب النتيجة ونتذكرها function process(obj) { if (!cache.has(obj)) { let result = /* حسابات الكائن هذا */ obj; cache.set(obj, result); } return cache.get(obj); } // الآن نستعمل ‫process()‎ في ملف آخر: // ? main.js let obj = {/* فلنفترض وجود هذا الكائن*/}; let result1 = process(obj); // حسبنا القيمة // ‫...بعدها، في مكان آخر من الشيفرة... let result2 = process(obj); // تُأخذ النتيجة تلك من الخبيئة // ‫...بعدها، لو لم نرد الكائن بعد الآن: obj = null; alert(cache.size); // 1 (لاا! ما زال الكائن في الخبيئة ويستهلك الذاكرة) لو استدعينا process(obj)‎ أكثر من مرّة بتمرير نفس الكائن، فستحسب الشيفرة النتيجة أوّل مرة فقط، وفي المرات القادمة تأخذها من الكائن cache. مشكلة هذه الطريقة هي ضرورة مسح cache متى ما انتفت حاجتنا من الكائن. لكن، لو استبدلنا Map وعوّضناها بِـ WeakMap فستختفي المشكلة تمامًا، وتُزال النتيجة المُخبّأة من الذاكرة تلقائيًا متى ما كُنس الكائن على أنّه مهملات. // ? cache.js *!* let cache = new WeakMap(); */!* // نحسب النتيجة ونتذكرها function process(obj) { if (!cache.has(obj)) { let result = /* حسابات الكائن هذا */ obj; cache.set(obj, result); } return cache.get(obj); } // ? main.js let obj = {/* كائن من الكائنات */}; let result1 = process(obj); let result2 = process(obj); // ‫...بعدها، لو لم نرد الكائن بعد الآن: obj = null; هنا، ‫لا يمكن أن نعرف cache.size إذ أنها خارطة بإشارة ضعيفة، ولكن الحجم صفر، أو سيكون صفر قريبًا؛ فما أن تبدأ عملية كنس المهملات على الكائن، ستُزال البيانات المُخبّأة هي الأخرى. WeakSet حتّى الأطقم ضعيفة الإشارة WeakSet تسلك ذات السلوك: تشبه الأطقم العادية Set ولكن لا يمكننا إلّا إضافة الكائنات إلى WeakSet (وليس الأنواع الأولية). يبقى الكائن موجودًا في الطقم طالما هناك ما يصل إليه. ويدعم -كما تدعم Set- التوابِع add وhas وdelete، ولكن لا تدعم size أو keys()‎ أو التعداد. هي الأخرى تخدمنا نحن المطورون في تخزين البيانات الإضافية (إذ أنّ الإشارة إليها ”ضعيفة“)، ولكنها ليست لأيّ بيانات كانت، بل فقط التي تُعطي إجابة ”نعم/لا“. لو كان الكائن موجودًا داخل طقم بإشارة ضعيفة، فلا بدّ أنّه موجود لداعٍ. يمكننا مثلًا إضافة المستخدمين إلى طقم بإشارة ضعيفة WeakSet لنسجّل من زار موقعنا: let visitedSet = new WeakSet(); let john = { name: "John" }; let pete = { name: "Pete" }; let mary = { name: "Mary" }; visitedSet.add(john); // زارنا ‫John visitedSet.add(pete); // وبعده ‫Pete visitedSet.add(john); // وعاد ‫John // ت‫حتوي visitedSet الآن على مستخدمين اثنين // ه‫ل زارنا John؟ alert(visitedSet.has(john)); // true // ‫هل زارتنا Mary؟ alert(visitedSet.has(mary)); // false john = null; // ستُنظّف ‫visitedSet تلقائيًا التقييد الأهم في هذه الأنواع WeakSet وWeakMap هي عدم موجود المُكرَّرات واستحالة أخذ محتواها كله. لربّما ترى ذلك غباءً، إلّا أنّه لا يمنع هذه الأنواع من إجراء مهامها التي صُنعت لها: مخزن "إضافي" من البيانات للكائنات المخزّنة (أو المُدارة) في مكان آخر. خلاصة الخارطة ضعيفة الإشارة هي تجميعة تشبه الخرائط العادية، ولا تتيح إلا استعمال الكائنات مفاتيحٍ فيها، كما وتُزيلها هي والقيمة المرتبطة بها ما إن تنعدم الإشارة إليها. الطقم ضعيفة الإشارة هي تجميعة تشبه الأطقم العادية، ولا تخزّن إلا الكائنات فيها، كما وتُزيلها ما إن تنعدم الإشارة إليها. كِلا النوعان لا يدعمان التوابِع والخاصيات التي تُشير إلى كل المفاتيح فيهما، أو حتى عددها. المسموح فقط هو العمليات على العناصر فيها عنصرًا بعنصر. يُستعمل هذان النوعان WeakMap وWeakSet على أنّهما بنى بيانات ”ثانوية“ إلى جانب تلك ”الأساسية“ لتخزين العناصر. لو أُزيل الكائن من التخزين الأساسي، ولم يوجد له أي إشارة إلا مفتاحًا في WeakMap أو عنصرًا في WeakSet، مسحهُ المحرّك تلقائيًا. تمارين تخزين رايات ”غير مقروءة“ الأهمية: 5 لديك مصفوفة من الرسائل: let messages = [ {text: "Hello", from: "John"}, {text: "How goes?", from: "John"}, {text: "See you soon", from: "Alice"} ]; ويمكن للشيفرة عندك الوصول إليها، إلّا أنّ شيفرة أحدهم تُدير تلك الرسائل، فتُضيف رسائل جديدة وتُزيل قديمة، ولا تعرف متى يحدث هذا بالضبط. السؤال هو: أيّ بنية من بنى البيانات تستعمل لتخزّن هذه المعلومة لكلّ رسالة: ”هل قُرأت؟“. يجب أن تكون البنية التي اخترتها مناسبة لتردّ على سؤال ”هل قُرأت؟“ لكلّ كائن رسالة. ملاحظة: حين تُزال رسالة من مصفوفة messages، يجب أن تختفي من بنية البيانات لديك هي الأخرى. ملاحظة أخرى: يجب ألّا نُعدّل كائنات الرسائل ولا نُضيف خاصيات من عندنا إليها؛ فيمكن أن يؤدّي هذا إلى عواقب وخيمة إذ لسنا من نديرها بل أحد آخر. الحل لنجرّب تخزين الرسائل المقروءة في طقم بإشارة ضعيفة WeakSet: let messages = [ {text: "Hello", from: "John"}, {text: "How goes?", from: "John"}, {text: "See you soon", from: "Alice"} ]; let readMessages = new WeakSet(); // قرأ المستخدم رسالتين اثنتين readMessages.add(messages[0]); readMessages.add(messages[1]); // في ‫readMessages الآن عنصرين // ‫...هيًا نُعيد قراءة أول رسالة! readMessages.add(messages[0]); // ما زالت في ‫readMessages عنصرين فريدين // ‫الجواب: هل قُرئتmessage [0]‎؟ alert("Read message 0: " + readMessages.has(messages[0])); // نعم ‫true messages.shift(); // الآن في ‫readMessages عنصر واحد (تقنيًا فستُنظّف الذاكرة فيما بعد) يتيح لنا الطقم ضعيفة الإشارة تخزينَ مجموعة من الرسائل والتأكّد من وجود كلّ منها بسهولة تامة. كما وأنّها تمسح نفسها بنفسها. للأسف بهذا نُضحّي بميزة التكرار، فلا يمكن أن نجلب ”كلّ الرسائل المقروءة“ منها مباشرةً، ولكن… يمكننا المرور على عناصر كل الرسائل في messages وترشيح تلك التي في الطقم لدينا. يمكن أن يكون الحل الآخر هو إضافة خاصية مثل message.isRead=true إلى الرسالة بعد قراءتها. ولكننّا لسنا من نُدير هذه الكائنات بل أحد آخر، ولهذا لا يُوصى بذلك بصفة عامة. ولكن، يمكننا استعمال خاصيّة رمزية فنتجنّب أي مشكلة أو تعارض. هكذا: // الخاصية الرمزية معروفة في الشيفرة لدينا، فقط let isRead = Symbol("isRead"); messages[0][isRead] = true; "لربما" الآن لن تعرف شيفرة الطرف الثالث بخاصيتنا الجديدة. صحيح أن الرموز تتيح لنا تقليل احتمال حدوث المشاكل، إلّا أنّ استعمال WeakSet أفضل بعين بنية البرمجية. تخزين تواريخ القراءة الأهمية: 5 لديك مصفوفة من الرسائل تشبه تلك في التمرين السابق، والفكرة هنا متشابهة قليلًا. let messages = [ {text: "Hello", from: "John"}, {text: "How goes?", from: "John"}, {text: "See you soon", from: "Alice"} ]; السؤال: أيّ بنية بيانات تستعمل لتخزين هذه المعلومة: " متى قُرئت هذه الرسالة؟". كان عليك (في التمرين السابق) تخزين معلومة "نعم/لا" فقط، أمّا الآن فعليك تخزين التاريخ، ويجب أن يبقى في الذاكرة إلى أن تُكنس الرسالة على أنّها مهملات. ملاحظة: تُخزّن التواريخ كائنات بصنف Date المضمّن في اللغة، وسنتكلم عنه لاحقًا. الحل يمكن أن نستعمل الخارطة ضعيفة الإشارة لتخزين التاريخ: let messages = [ {text: "Hello", from: "John"}, {text: "How goes?", from: "John"}, {text: "See you soon", from: "Alice"} ]; let readMap = new WeakMap(); readMap.set(messages[0], new Date(2017, 1, 1)); // سنرى أمر كائن التاريخ لاحقًا ترجمة -وبتصرف- للفصل WeakMap and WeakSet من كتاب The JavaScript language .task__importance { color: #999; margin-left: 30px; } .task__answer { border: 3px solid #f7f6ea; margin: 20px 0 14px; position: relative; display: block; padding: 25px 30px; } اقرأ أيضًا المقال التالي: مفاتيح الكائنات وقيمها ومدخلاتها المقال السابق: النوع Map (الخرائط) والنوع Set (الأطقم)
  21. تعلّمنا حتّى الآن بنى البيانات المعقّدة هذه: الكائنات Object: لتخزين التجميعات ذات المفاتيح. المصفوفات Array: لتخزين التجميعات المرتّبة. ولكن في الحياة الواقعية، هذا لا يكفي. ولهذا تقدّم لك اللغة نوعيين آخرين: الخارطة Map والطقم Set. الخارطة Map تُعدّ الخارطة تجميعة ذات مفاتيح من عناصر البيانات، تمامًا مثل الكائنات Object، مع فرق بسيط، هو أنّ الخارطة Map تتيح استعمال المفاتيح مهمًا كان نوعها. هذه توابِعها وخاصياتها: new Map()‎ -- يُنشِئ خارطة. map.set(key, value)‎ -- يضبط القيمة حسب مفتاحها. map.get(key)‎ -- يجلب القيمة حسب مفتاحها، وundefined لو لم يوجد key في الخارطة. map.has(key)‎ -- يُعيد true لو كان key موجودًا، وإلا فَـ false. map.delete(key)‎ -- يُزيل القيمة حسب مفتاحها. map.clear()‎ -- يُزيل كل شيء من الخارطة. map.size -- يُعيد عدد العناصر الحالي. إليك المثال الآتي: let map = new Map(); map.set('1', 'str1'); // المفتاح سلسلة نصية map.set(1, 'num1'); // المفتاح عدد map.set(true, 'bool1'); // المفتاح قيمة منطقية // أتذكر كيف أنّ الكائن العادي يُحوّل المفاتيح لأي سلاسل نصية؟ // ‫الخارطة هنا تحترم النوع، وهذان السطران مختلفان: alert( map.get(1) ); // 'num1' alert( map.get('1') ); // 'str1' alert( map.size ); // 3 كما ترى، فالمفاتيح لا تُحوّل إلى سلاسل نصية (على العكس من الكائنات). يمكنك أن تضع أيّ نوع من المفاتيح تريد. يمكن أن تستعمل الخارطة الكائناتَ نفسها مفاتيح. مثال: let john = { name: "John" }; // لنخزّن عدد زيارات كل زائر لنا let visitsCountMap = new Map(); // ‫كائن john هو مفتاح الخارطة visitsCountMap.set(john, 123); alert( visitsCountMap.get(john) ); // 123 يُعدّ استعمال الكائنات على أنّها مفاتيح أحدُ أهمّ صفات Map. لو أردت المفاتيح سلاسل نصية، فالكائنات Object تكفيك وزيادة، لكن لو أردت المفاتيح كائنات، فسيخونك Object للأسف. لنرى: let john = { name: "John" }; let visitsCountObj = {}; // نحاول استعمال كائن visitsCountObj[john] = 123; // ‫ونحاول استعمال كائن john مفتاحًا فيه // ‫وهذا ما وجدناه مكتوبًا! alert( visitsCountObj["[object Object]"] ); // 123 المتغيّر visitsCountObj من نوع ”كائن“، ولهذا يحوّل كلّ المفاتيح (مثل john) إلى سلاسل نصية. وبهذا قدّم لنا المفتاح بالسلسلة النصية "[object Object]". ليس ما نريد قطعًا. كيف تُوازن الخارطة Map المفاتيح تستعمل Map الخوارزمية SameValueZero لتختبر تساوي المفتاح مع الآخر. تتشابه هذه الخوارزمية تقريبًا مع المساواة الصارمة === بفارق أنّ NaN تساوي NaN في نظرها. يعني ذلك بأنك تستطيع استعمال NaN كمفتاح هو الآخر. لا يمكن تغيير هذه الخوارزمية ولا تخصيصها. سِلسلة الاستدعاءات كلّما نادينا map.set أعاد لنا التابِع الخارطة نفسها، وبهذا يمكن أن نستدعي التابع على ناتج الاستدعاء السابق: map.set('1', 'str1') .set(1, 'num1') .set(true, 'bool1'); المرور على خارطة هناك ثلاث طرائق للمرور على عناصر Map وتطبيق عملية عليها: map.keys()‎ -- يُعيد مُتعدَّدًا للمفاتيح، map.values()‎ -- يُعيد مُتعدَّدًا للقيم، map.entries()‎ -- يُعيد مُتعدَّدًا للمدخلات [key, value]، وهي التي تستعملها for..of مبدئيًا. مثال: let recipeMap = new Map([ ['cucumber', 500], ['tomatoes', 350], ['onion', 50] ]); // نمرّ على المفاتيح (الخضراوات)‏ for (let vegetable of recipeMap.keys()) { alert(vegetable); // cucumber, tomatoes, onion } // نمرّ على قيم المفاتيح (عدد الخضراوات)‏ for (let amount of recipeMap.values()) { alert(amount); // 500, 350, 50 } // ‫نمرّ على مدخلات [key, value] for (let entry of recipeMap) { // ‫مثل recipeMap.entries()‎ alert(entry); // ‫cucumber,500 (وهكذا) } ترتيب الإدخال هو المستعمل يسير التعداد بنفس الترتيب الذي أُدخلت به الكائنات، فالخارطة تحفظ هذا الترتيب على العكس من الكائنات Object. علاوةً على ذلك، فتملك الخارطة Map التابِع المضمّن فيها forEach، كما المصفوفات Array: // ‫تُنفّذ الدالة على كلّ زوج (key, value) recipeMap.forEach( (value, key, map) => { alert(`${key}: ${value}`); // ‫cucumber: 500 إلخ إلخ }); Object.entries: صنع خارطة من كائن متى ما أُنشأت خارطة Map نستطيع تمرير مصفوفة (أو مُتعدَّدًا آخرًا) لها أزواج ”مفاتيح/قيم“ لتهيئتها، هكذا تمامًا: // ‫مصفوفة من أزواج [key, value] let map = new Map([ ['1', 'str1'], [1, 'num1'], [true, 'bool1'] ]); alert( map.get('1') ); // str1 لو كان أمامنا كائنًا عاديًا ونريد صناعة Map منه، فيمكننا استعمال التابِع المضمّن في اللغة Object.entries(obj) إذ يُعيد مصفوفة مكوّنة من أزواج ”مفاتيح/قيم“ للكائن، بنفس الصيغة التي يطلبها ذاك التابِع. ولهذا يمكن أن نصنع خارطة من كائن بهذه الطريقة: let obj = { name: "John", age: 30 }; let map = new Map(Object.entries(obj)); alert( map.get('name') ); // John نرى هنا التابِع Object.entries يُعيد مصفوفة بأزواج ”مفاتيح/قيم“: [ ["name","John"], ["age", 30] ]، وهذا ما تحتاجه الخارطة. Object.fromEntries: صنع كائن من خارطة رأينا كيف نصنع خارطة Map من كائنٍ عاديّ باستعمال Object.entries(obj)‎. على العكس منه فالتابع Object.fromEntries يأخذ خارطة فيها أزواج [key, value] ويصنع كائنًا منها: let prices = Object.fromEntries([ ['banana', 1], ['orange', 2], ['meat', 4] ]); // prices = { banana: 1, orange: 2, meat: 4 } alert(prices.orange); // 2 يمكننا استعمال Object.fromEntries لنصنع كائنًا عاديًا من Map. يُفيدنا هذا مثلًا في تخزين البيانات في خارطة، بينما نريد تمريرها إلى شيفرة من طرف ثالثة تريد كائنًا عاديًا لا خارطة. هذه الشيفرة المنشودة: let map = new Map(); map.set('banana', 1); map.set('orange', 2); map.set('meat', 4); let obj = Object.fromEntries(map.entries()); // ن‫صنع كائنًا عاديًا (*) //و‫هكذا انتهينا! // obj = { banana: 1, orange: 2, meat: 4 } alert(obj.orange); // 2 متى ما استدعينا map.entries()‎ أعادت مصفوفة مؤلّفة من أزواج ”مفاتيح/قيم“ بنفس التنسيق الذي يطلبه Object.fromEntries تمامًا، لحسن الحظ. يمكننا تقصير السطر المعلّم (*) ذاك: let obj = Object.fromEntries(map); // ب‫دون ‎.entries()‎ النتيجة نفسها إذ أنّ التابِع Object.fromEntries يتوقّع كائنًا مُتعدَّدًا وسيطًا له، وليس مصفوفة بالضرورة. كما والتعداد القياسي للخارطة يتوقّع ذات أزواج ”مفاتيح/قيم“ التي يتوقّعها map.entries()‎، وهكذا نجد في يدنا كائنًا عاديًا له نفس ”مفاتيح/قيم“ الخارطة map. الطقم Set الأطقم (Set) هي نوع خاصّ من التجميعات ليس له مفاتيح ولا يمكن أن يحوي أكثر من قيمة متطابقة. يمكن عدّها كأطقم المجوهرات والأسنان، حيث لا تتكرّر أي قطعة مرتين. إليك توابِعه الرئيسة: new Set(iterable)‎ -- يصنع الطقم. في حال مرّرت كائن iterable (وهو عادةً مصفوفة)، فينسخ بياناته إلى الطقم. set.add(value)‎ -- يُضيف قيمة إلى الطقم ويُعيده ذاته. set.delete(value)‎ -- يُزيل القيمة ويُعيد true لو كانت القيمة value موجودة عند استدعاء التابِع، وإلّا يُعيد false. set.has(value)‎ -- يُعيد true لو كانت القيمة موجودة في الطقم، وإلّا يُعيد false. set.clear()‎ -- يُزيل كلّ شيء من الطقم. set.size -- خاصية عدد العناصر في الطقم. الميزة الأهمّ للأطقم هي أنّك لو استدعيت set.add(value)‎ أكثر من مرّة وبنفس القيمة، فكأنّك استدعيتهُ مرّة واحدة. لهذا تظهر كل قيمة في الطقم مرّة واحدة لا غير. عُدّ مثلًا أنّ زوّارًا قادمين إلى وليمة ونريد تذكّر كلّ واحد لإعداد ما يكفي من طعام… ولكن يجب ألّا نسجّل الزوّار مرتين، فالزائر ”واحد“ ونعدّه مرّة واحدة فقط. الطقم هنا هو الخيار الأمثل: let set = new Set(); let john = { name: "John" }; let pete = { name: "Pete" }; let mary = { name: "Mary" }; // زارنا الناس، وهناك من زارنا أكثر من مرة set.add(john); set.add(pete); set.add(mary); set.add(john); set.add(mary); // لا يحفظ الطقم إلا القيم الفريدة alert( set.size ); // 3 for (let user of set) { alert(user.name); // ‫John (ثمّ Pete وMary) } يمكن عوض الأطقم استعمال مصفوفة من المستخدمين، مع نصّ يتحقّق من البيانات عند إدخالها لألّا تحدث تكرارات (باستعمال arr.find). هذا ممكن نعم، لكن الأداء سيكون أشنع بكثير فتابِع البحث arr.find يمرّ على كامل المصفوفة فيفحص كلّ عنصر فيها. الطقم Set أفضل بمراحل فأداؤه في فحص تفرّد العناصر مُحسَّن داخل بنية اللغة. المرور على طقم يمكن لنا المرور على عناصر الطقم باستعمال حلقة for..of أو تابِع forEach: let set = new Set(["oranges", "apples", "bananas"]); for (let value of set) alert(value); // ‫نفس الأمر مع forEach: set.forEach((value, valueAgain, set) => { alert(value); }); ولكن لاحظ هنا طرافة التابِع: لدالة ردّ النداء المُمرّرة إلى forEach ثلاث وُسطاء: قيمة value، وذات القيمة الأولى valueAgain، والكائن الهدف. لاحظتَ كيف تكرّرت ذات القيمة في الوُسطاء مرّتين؟ يعزو هذا إلى توافق Set مع Map إذ لدالة ردّ التابع المُمرّرة إلى forEach الخارطة ثلاث وُسطاء أيضًا. معك حق، أمرها غريب، ولكنّها تفيد فتُسهّل حياتنا لو أردنا استبدال الخارطة بالطقم في حالات حرجة، كما العكس أيضًا. كما تدعم الأطقم نفس التوابِع التي تدعمها الخارطة للتعامل مع المُتعدَّدات: set.keys()‎ -- يُعيد كائنًا مُتعدَّدًا من القيم، set.values()‎ -- تمامًا مثل set.keys()‎ (موجود للتوافق مع Map)، set.entries()‎ -- يُعيد كائنًا مُتعدَّدًا من المُدخلات [value, value] (موجود للتوافق مع Map). ملخص الخارطة Map هي تجميعة ذات مفاتيح. توابعها وخاصياتها: new Map([iterable])‎ -- يصنع خريطة ويضع فيها أزواج [key,value] داخل المُتعدَّد iteratable الاختياري (يمكن أن يكون مثلًا مصفوفة). map.set(key, value)‎ -- يخزّن القيمة حسب مفتاحها. map.get(key)‎ -- يُعيد القيمة حسب مفتاحها، ويُعيد undefined لو لم يكن المفتاح key في الخارطة. map.has(key)‎ -- يُعيد true لو كان المفتاح key موجودًا، وإلا يُعيد false. map.delete(key)‎ -- يُزيل القيمة حسب مفتاحها. map.clear()‎ -- يُزيل كل ما في الخارطة. map.size -- يُعيد عدد العناصر في الخارطة الآن. اختلافاتها مع الكائنات العادية (Object): تدعم أنواع المفاتيح المختلفة، كما والكائنات نفسها أيضًا. فيها توابِع أخرى تفيدنا، كما وخاصية size. الطقم Set هو تجميعة من القيم الفريدة. توابعه وخاصياته: new Set([iterable])‎ -- يصنع طقمًا ويضع فيه أزواج [key, value] داخل المُتعدَّد الاختياري (يمكن أن يكون مثلًا مصفوفة). set.add(value)‎ -- يُضيف القيمة value (ولو كانت موجودة لا يفعل شيء) ثمّ يُعيد الطقم نفسه. set.delete(value)‎ -- يُزيل القيمة ويُعيد true لو كانت موجودة عند استدعاء التابِع، وإلا يُعيد false. set.has(value)‎ -- يُعيد true لو كانت القيمة في الطقم، وإلا يُعيد false. set.clear()‎ -- يُزيل كل ما في الطقم. set.size -- عدد عناصر الطقم. يسري ترتيب المرور على عناصر Map وSet بترتيب إدخالها فيهما دومًا، ولهذا لا يمكن أن نقول بأنّها تجميعات غير مرتّبة، بل أنّا لا نقدر على إعادة ترتيب عناصرها أو الحصول عليها بفهرسها فيها. تمارين ترشيح العناصر الفريدة في مصفوفة الأهمية: 5 عُدّ أنّ arr مصفوفة. أنشِئ دالة unique(arr)‎ تُعيد مصفوفة مؤلّفة من العناصر الفريدة في arr. مثال: function unique(arr) { /* هنا تكتب شيفرتك*/ } let values = ["Hare", "Krishna", "Hare", "Krishna", "Krishna", "Krishna", "Hare", "Hare", ":-O" ]; alert( unique(values) ); // Hare, Krishna, :-O لاحظ أنّ السلاسل النصية استُعملت هنا، ولكن يمكن أن تكون القيم بأيّ نوع آخر. غُشّ من هذه: استعمل Set لتخزين القيم الفريدة. الحل function unique(arr) { return Array.from(new Set(arr)); } ترشيح الألفاظ المقلوبة الأهمية: 4 تُسمّى الكلمات التي لها ذات الأحرف ولكن بترتيب مختلف ألفاظًا مقلوبة، مثل هذه: nap - pan ear - are - era cheaters - hectares - teachers أو العربية: ملّ - لمّ مسكين - سيكمن كاتب - اكتب - كتاب اكتب دالة aclean(arr)‎ تُعيد مصفوفةً بدون هذه الألفاظ المقلوبة. هكذا: let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"]; alert( aclean(arr) ); // "nap,teachers,ear" أو "PAN,cheaters,era" يجب أن يكون ناتج كلّ مجموعة ألفاظ كلمة واحدة فقط، ولا يهمّنا أيّ واحدة. الحل لو أردنا البحث عن كل الألفاظ المقلوبة، سنقسم كلّ كلمة إلى حروفها ونرتّبها. متى ما رتّبناها حسب الأحرف، فستكون الألفاظ كلها متطابقة. هكذا: nap, pan -> anp ear, era, are -> aer cheaters, hectares, teachers -> aceehrst ... سنستعمل كلّ قيمة مختلفة (ولكن متطابقة بترتيب أحرفها) لتكون مفاتيح خريطة فنخزّن لفظًا واحدًا لكل مفتاح فقط: function aclean(arr) { let map = new Map(); for (let word of arr) { // نقسم الكلمة بأحرفها، ونرّتب الأحرف ونجمعها ثانيةً let sorted = word.toLowerCase().split('').sort().join(''); // (*) map.set(sorted, word); } return Array.from(map.values()); } let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"]; alert( aclean(arr) ); نُنفّذ الترتيب حسب الأحرف بسلسلة استدعاءات كما في السطر (*). سنقسمها على أكثر من سطر ليسهل فهمها: let sorted = arr[i] // PAN .toLowerCase() // pan .split('') // ['p','a','n'] .sort() // ['a','n','p'] .join(''); // anp هكذا يكون لدى الكلمتين المختلفتين 'PAN' و'nap' ذات الشكل حين تُرتّب أحرفها: 'anp'. في السطر اللاحق نُضيف الكلمة إلى الخارطة. map.set(sorted, word); لو قابلنا بينما نمرّ على الكلمات كلمةً لها نفس الشكل حين تُرتّب أحرفها، فستعوّض القيمة السابقة التي لها نفس المفتاح في الخارطة. هكذا لن تزيد الكلمات لكلّ شكل على واحد، دومًا. وفي النهاية يأخذ Array.from(map.values())‎ متُعدَّدا يمرّ على قيم الخارطة (لا نريد مفاتيحها في ناتج الدالة) فيُعيد المصفوفة نفسها. يمكننا (في هذه المسألة) استعمال كائن عادي بدل الخارطة، إذ أنّ المفاتيح سلاسل نصية. هكذا سيبدو الحلّ لو اتبعنا هذا النهج: function aclean(arr) { let obj = {}; for (let i = 0; i < arr.length; i++) { let sorted = arr[i].toLowerCase().split("").sort().join(""); obj[sorted] = arr[i]; } return Object.values(obj); } let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"]; alert( aclean(arr) ); مفاتيح مُكرَّرة الأهمية: 5 نريد تسجيل المصفوفة الناتجة من map.keys()‎ في متغيّر وثمّ استدعاء توابِع تخصّ المصفوفات عليها مثل ‎.push. ولكنّ ذلك لم ينفع: let map = new Map(); map.set("name", "John"); let keys = map.keys(); // ‫خطأ: keys.push ليست دالة keys.push("more"); لماذا؟ وكيف يمكننا إصلاح الشيفرة ليعمل keys.push؟ الحل لأنّ التابِع map.keys()‎ يُعيد مُتعدَّدًا لا مصفوفة. يمكننا تحويله إلى مصفوفة باستعمال Array.from: let map = new Map(); map.set("name", "John"); let keys = Array.from(map.keys()); keys.push("more"); alert(keys); // name, more ترجمة -وبتصرف- للفصل Map and Set من كتاب The JavaScript language .task__importance { color: #999; margin-left: 30px; } .task__answer { border: 3px solid #f7f6ea; margin: 20px 0 14px; position: relative; display: block; padding: 25px 30px; } اقرأ أيضًا المقال التالي: النوع WeakMap والنوع WeakSet: الخرائط والأطقم ضعيفة الإشارة المقال السابق: الكائنات المكرَّرة (Iterables)
  22. الكائنات المُكرَّرة (Iterables) هي مفهوم أعمّ من المصفوفات. تتيح لنا هذه الكائنات تحويل أيّ كائن إلى ”كائن يمكن تكراره“ فيمكننا استعماله في حلقة for..of. بالطبع فالمصفوفات يمكن تكرارها، ولكن هناك كائنات أخرى (مضمّنة في أصل اللغة) يمكن تكرارها أيضًا، مثل السلاسل النصية. لو لم يكن الكائن مصفوفة تقنيًا، ولكن يمكننا تمثيله على أنّه تجميعة من العناصر (النوع list، والنوع set)، فصياغة for..of ممتازة لنمرّ على عناصره. لذا دعنا نرى كيف يمكن توظيف هذه ”المُكرَّرات“. Symbol.iterator يمكن لنا أن نُدرك هذا المفهوم -مفهوم المُكرَّرات أعني- بأن نصنع واحدًا بنفسنا. لنقل أنّ لدينا كائن وهو ليس بمصفوفة بأيّ شكل، ولكن يمكن توظيفه لحلقة for..of. مثلًا كائن المدى هذا range يُمثّل مجموعة متتالية من الأعداد. let range = { from: 1, to: 5 }; // ‫‏نُريد أن تعمل for..of هكذا: // for(let num of range) ... num=1,2,3,4,5 لنُضيف خاصية التكرار إلى range (فتعمل بهذا for..of)، علينا إضافة تابِع إلى الكائن بالاسم Symbol.iterator (وهو رمز خاصّ في اللغة يتيح لنا هذه الميزة). حين تبدأ for..of، تنادي ذلك التابِع مرة واحدة (أو تعرض الأخطاء المعروفة لو لم تجدها). على هذا التابِع إعادة مُكرِّر/iterator، أي كائنًا له التابِع next. بعدها، تعمل for..of مع ذلك الكائن المُعاد فقط لا غير. حين تحتاج for..of القيمة التالية، تستدعي next()‎ لذاك الكائن. يجب أن يكون ناتج next()‎ بالشكل هذا {done: Boolean, value: any}، حيث لو كانت done=true فيعني أن التكرار اكتمل، وإلّا فقيمة value هي التالية. إليك النص الكامل لتنفيذ كائن range (مع الملاحظات): let range = { from: 1, to: 5 }; // ‫‏1. حين ننادي for..of فهي تنادي هذه range[Symbol.iterator] = function() { // ‫‏...وتُعيد الكائن المُكرِّر: // ‫‏2. بعد ذلك تعمل for..of مع هذا المُكرِّر، طالبةً منه القيم التالية return { current: this.from, last: this.to, // ‫‏3. يُستدعى next()‎ في كلّ كرّة في حلقة for..of next() { // ‫‏4. يجب أن يُعيد القيمة كائنًا كهذا {done:.., value :...‎} if (this.current <= this.last) { return { done: false, value: this.current++ }; } else { return { done: true }; } } }; }; // ‫‏والآن تعمل! for (let num of range) { alert(num); // ‫‏1، ثمّ 2 فَ‍ 3 فَ‍ 4 فَ‍ 5 } لاحظ الميزة الأساس للمُكرَّرات: فصل الاهتمامات. عنصر range ذاته ليس له التابِع next()‎. بدل ذلك، يُنشأ كائن آخر (أي "المُكرَّر") عند استدعاء range[Symbol.iterator]()‎، وتابِعه next()‎ يُولّد قيم التكرار. الخلاصة هي أنّ كائن المُكرَّر منفصل عن الكائن الذي يُكرِّره هذا المُكرَّر. نظريًا، يمكننا دمجهما معًا واستعمال كائن range نفسه مُتعدَّدًا لتبسيط الكود أكثر. هكذا تمامًا: let range = { from: 1, to: 5, [Symbol.iterator]() { this.current = this.from; return this; }, next() { if (this.current <= this.to) { return { done: false, value: this.current++ }; } else { return { done: true }; } } }; for (let num of range) { alert(num); // ‏1، ثمّ 2 فَ‍ 3 فَ‍ 4 فَ‍ 5 الآن صار يُعيد range[Symbol.iterator]()‎ كائن range نفسه: وهذا الكائن فيه تابِع next()‎ اللازم كما ويتذكّر حالة التعداد الحالية في this.current. الشيفرة أبسط، أجل. وأحيانًا لا بأس به هكذا. المشكلة هنا هي استحالة وجود حلقتي for..of تعملان على الكائن في آن واحد، إذ سيتشاركان حالة التعداد؛ فليس هناك إلا مُتعدَّد واحد: الكائن نفسه. لكن أصلًا وجود حلقتي for..of نادر، حتى في العمليات غير المتزامَنة. مُكرَّرات لا تنتهي يمكن أيضًا ألا تنتهي المُكرَّرات أبدًا. فمثلًا لا ينتهي المدى range لو صار range.to = Infinity. يمكن أيضًا أن نصنع كائن مُتعدَّد يولّد أعدادًا شبه عشوائية (pseudorandom) لانهائية، ستفيد في حالات حرجة. ما من حدود مفروضة على ناتج next. يمكنه إعادة القيم مهما أراد وبالكم الذي أراد، لا مشكلة. طبعًا، المرور على هذا المُكرَّر بحلقة for..of لن ينتهي أبد الدهر، ولكن يمكننا إيقافها باستعمال break. السلاسل النصية مُكرَّرة تُعدّ المصفوفات والسلاسل النصية أكثر المُكرَّرات المقدَّمة من اللغة استعمالًا. بالنسبة إلى السلاسل النصية، فحلقة for..of تمرّ على محارفها: for (let char of "test") { // ‫تتنفّذ أربع مرات: مرة لكلّ محرف alert( char ); // ‫t فَ‍ e فَ‍ s فَ‍ t } كما وتعمل -كما يجب!- مع الأزواج النائبة أو البديلة (Surrogate Pairs)! let str = '??'; for (let char of str) { alert( char ); // ‫? وثمّ ? } نداء المُكرَّر جهارة لنعرف المُكرَّرات معرفةً أعمق، لنرى كيف يمكن استعمالها جهارةً. سنمرّ على سلسلة نصية بنفس الطريقة التي يمرّ بها for..of، ولكن هذه المرة ستكون النداءات مباشرة. تُنشِئ هذه الشيفرة مُكرَّرًا لسلسلة نصية وتأخذ القيم منه "يدويًا": let str = "Hello"; // تنفّذ ما تنفّذه // for (let char of str) alert(char); *!* let iterator = str[Symbol.iterator](); */!* while (true) { let result = iterator.next(); if (result.done) break; alert(result.value); // تطبع المحارف واحدًا تلو الآخر } في الحياة الواقعية، نادرًا ما ستحتاج هذا. لكن المفيد أنّنا نتحكّم أكثر على عملية التكرار موازنةً بِـ for..of. فمثلًا يمكننا تقسيم عملية التكرار: نكرّر قليلًا، نتوقّف ونفعل شيئًا آخر، ثمّ نواصل التكرار. المُكرَّرات والشبيهات بالمصفوفات هذان المصطلحان الرسميان يبدوان متشابهين إلى حدّ ما، ولكنّهما مختلفين تمام الاختلاف. حاوِل إدراكهما إدراكًا صحيحًا لتتجنّب هذا الاختلاط لاحقًا. المُكرَّرات كائنات تُنفّذ التابِع Symbol.iterator، كما شرحنا أعلاه. الشبيهات بالمصفوفات كائنات لها فهارس وصفة طول length، وبهذا ”تشبه المصفوفات“… المصطلح يشرح نفسه. حين نستعمل جافاسكربت للمهام الحقيقية في المتصفحات وغيرها من بيئات، نقابل مختلف الكائنات أكانت مُكرَّرات أو شبيهات بالمصفوفات، أو كليهما معًا. السلاسل النصية مثلًا مُكرَّرة (يمكن استعمال for..of عليها)، وشبيهة بالمصفوفات أيضًا (لها فهارس عددية وصفة length). ولكن ليس من الضروري أن يكون المُكرَّر شبيه بالمصفوفة، والعكس صحيح (لا يكون الشبيه بالمصفوفة مُكرَّر). فالمدى range في المثال أعلاه مُكرَّر، ولكنه ليس شبيه بالمصفوفة إذ ليس فيه صفات فهارس وlength. إليك كائنًا شبيهًا بالمصفوفات وليس مُكرَّرًا: let arrayLike = { // ‫فيه فهارس وطول => شبيه بالمصفوفات 0: "Hello", 1: "World", length: 2 }; // ‫خطأ (ما من Symbol.iterator) for (let item of arrayLike) {} عادةً، لا تكون لا المُكرَّرات ولا الشبيهات بالمصفوفات مصفوفات حقًا، فليس لها push أو pop وغيرها. لكن هذا غير منطقي. ماذا لو كان لدينا كائن من هذا النوع وأردنا التعامل معه بأنه مصفوفة؟ لنقل أنّا سنعمل على range باستعمال توابِع المصفوفات، كيف السبيل؟ Array.from التابِع العام Array.from يأخذ مُكرَّرًا أو شبيهًا بالمصفوفات ويحوّله إلى مصفوفة "فعلية". بعدها ننادي توابِع المصفوفات التي نعرفها عليها. هكذا مثلًا: let arrayLike = { 0: "Hello", 1: "World", length: 2 }; let arr = Array.from(arrayLike); // (*) alert(arr.pop()); // ‫تكتب World (أيّ أنّ التابِع عمل) يأخذ التابِع Array.from في سطر (*) الكائن، ويفحصه أكان مُكرَّرًا أو شبيهًا بالمصفوفات، ويصنع مصفوفة جديدة ينسخ قيم ذلك الكائن فيها. ذات الأمر للمُكرَّرات: // ‫نأخذ range من المثال أعلاه let arr = Array.from(range); alert(arr); // ‫تكتب 1,2,3,4,5 (تحويل toString للمصفوفة يعمل) والصياغة الكاملة للتابِع Array.from تتيح لنا تقديم دالة ”خريطة“ اختيارية: Array.from(obj[, mapFn, thisArg]) يمكن أن يكون الوسيط الاختياري الثاني mapFn دالةً تُطبّق على كلّ عنصر قبل إضافته للمصفوفة، ويتيح thisArg ضبط ماهيّة this للتابِع. مثال: // ‫نأخذ range من المثال أعلاه // ‫نُربّع كلّ عدد let arr = Array.from(range, num => num * num); alert(arr); // 1,4,9,16,25 هنا نستعمل Array.from لتحويل سلسلة نصية إلى مصفوفة من المحارف: let str = '??'; // يقسم ‫str إلى مصفوفة من المحارف let chars = Array.from(str); alert(chars[0]); // ? alert(chars[1]); // ? alert(chars.length); // 2 على العكس من str.split، فهي هنا تعتمد على طبيعة تكراريّة السلسلة النصية، ولهذا تعمل كما ينبغي (كما تعمل for..of) مع الأزواج النائبة. هنا أيضًا تقوم بذات الفعل، نظريًا: let str = '??'; let chars = []; // ‫داخليًا، تُنفّذ Array.from ذات الحلقة for (let char of str) { chars.push(char); } alert(chars); …ولكن تلك أقصر. يمكننا أيضًا صناعة تابِع slice مبني عليها يحترم الأزواج النائبة. function slice(str, start, end) { return Array.from(str).slice(start, end).join(''); } let str = '???'; alert( slice(str, 1, 3) ); // ?? // ‫التابِع الأصيل/native في اللغة لا يدعم الأزواج النائبة alert( str.slice(1, 3) ); // ‫يُولّد نصّ ”قمامة“ (قطعتين من أزواج نائبة مختلفة) خلاصة تُدعى الكائنات التي يمكن استعمالها في for..of بالمُكرَّرات (Iterables). على المُكرَّرات (تقنيًا) تنفيذ التابِع بالاسم Symbol.iterator. يُدعى ناتج obj[System.iterator]‎ بالمُكرَّر. يتعامل المُكرَّر بعملية التكرار. يجب أن يحتوي المُكرَّر التابِع بالاسم next()‎ حيث يُعيد كائن {done: Boolean, value: any}… تُشير done:true هنا بأنّ التكرار اكتمل، وإلّا فَـ value هي القيمة التالية. تُنادي الحلقة for..of التابِع Symbol.iterator تلقائيًا عند تنفيذها، ولكن يمكننا أيضًا فعل ذلك يدويًا. تُنفّذ المُكرَّرات المضمّنة في اللغة Symbol.iterator (مثل السلاسل النصية والمصفوفات). مُكرَّر السلاسل النصية يفهم الأزواج البديلة. تُدعى الكائنات التي فيها صفات فهارس وصفة طول length بالشبيهات بالمصفوفات. يمكن أيضًا أن تكون لها صفات وتوابِع أخرى، إلّا أنّ ليس فيها توابِع المصفوفات المضمّنة في بنية اللغة. لو نظرنا ورأينا مواصفات اللغة، فسنرى بأنّ أغلب التوابِع المضمّنة فيها تتعامل مع المصفوفات على أنّها مُكرَّرات أو شبيهات بالمصفوفات بدل أن تكون مصفوفات ”حقيقية“؛ هكذا تصير أكثر تجرّديّة (abstract). تصنع Array.from(obj[, mapFn, thisArg])‎ مصفوفةً Array حقيقية من المُكرَّر أو الشبيه بالمصفوفات obj، بهذا يمكن استعمال توابِع المصفوفات عليها. يتيح لنا الوسيطين mapFn وthisArg تقديم دالة لكلّ عنصر من عناصرها. ترجمة -وبتصرف- للفصل Arrays من كتاب The JavaScript language .task__importance { color: #999; margin-left: 30px; } .task__answer { border: 3px solid #f7f6ea; margin: 20px 0 14px; position: relative; display: block; padding: 25px 30px; } اقرأ أيضًا المقال التالي: النوع Map (الخرائط) والنوع Set (الأطقم) المقال السابق: توابع المصفوفات (Array methods)
  23. تقدّم المصفوفات توابِع عديدة تُسهِّل التعامل معها. ولتبسيطها سنقسّمها إلى مجموعات بحسب الوظيفة في هذا الفصل ونشرح كل منها على حدة. إضافة العناصر وإزالتها عرفنا من الفصل الماضي بالتوابِع التي تُضيف العناصر وتُزيلها من بداية أو نهاية المصفوفة: arr.push(...items)‎ -- يُضيف العناصر إلى النهاية، arr.pop()‎ -- يستخرج عنصرًا من النهاية، arr.shift()‎ -- يستخرج عنصرًا من البداية، arr.unshift(...items)‎ -- يُضيف العناصر إلى البداية. وهذه أخرى غيرها. الوصل splice يا ترى كيف نحذف أحد عناصر المصفوفة؟ المصفوفات كائنات، يمكننا تجربة delete وربما تنجح: let arr = ["I", "go", "home"]; delete arr[1]; // ‫أزِل "go" alert( arr[1] ); // undefined // ‫صارت المصفوفة الآن arr = ["I", , "home"];‎ alert( arr.length ); // 3 أُزيل العنصر صحيح، ولكنّ ما زال في المصفوفة ثلاثة عناصر، كما نرى في arr.length == 3. هذا طبيعي، إذ يُزيل delete obj.key القيمة بمفتاحها key… وهذا فقط. ينفع للكائنات ربّما، لكنّا نريدها للمصفوفات أن تنتقل كل العناصر على اليمين وتأخذ الفراغ الجديد. أي أننا نتوقع أن تصغر المصفوفة الآن. لهذا السبب علينا استعمال توابِع خاصّة لذلك. يمكننا تشبيه التابِع arr.splice(start)‎ بالتابِع «بتاع كُلّو» للمصفوفات (كما يُقال بالعامية). يمكنه أن يُجري ما تريد للعناصر: إدراج، إزالة، استبدال. هذه صياغته: arr.splice(index[, deleteCount, elem1, ..., elemN]) يبدأ التابِع من عند العنصر ذي الفهرس index، فيُزيل deleteCount من العناصر ويُدرج العناصر elem1, ..., elemN المُمرّرة إليه مكانها. أخيرًا يُعيد المصفوفة بالعناصر المُزالة. فهم هذا التابِع بالأمثلة أبسط. فلنبدأ أولًا بالحذف: let arr = ["I", "study", "JavaScript"]; // أزِل من العنصر ذا الفهرس 1 عنصرًا واحدًا (1)‏ arr.splice(1, 1); alert( arr ); // ["I", "JavaScript"] رأيت؟ سهلة. نبدأ من العنصر ذي الفهرس 1 ونُزيل عنصرًا واحدًا (1). الآن، نُزيل ثلاثة عناصر ونستبدلها بعنصرين آخرين: let arr = ["I", "study", "JavaScript", "right", "now"]; // أزِل الثلاث عناصر الأولى وعوّضها بتلك الأخرى arr.splice(0, 3, "Let's", "dance"); alert( arr ) // ‫["Let's", "dance", "right", "now"] أمّا هنا فكيف يُعيد splice مصفوفةً بالعناصر المُزالة. let arr = ["I", "study", "JavaScript", "right", "now"]; // أزِل أوّل عنصرين let removed = arr.splice(0, 2); alert( removed ); // "I", "study" <-- قائمة بالعناصر المُزالة يمكن أن يُدرج تابِع splice العناصر دون إزالة أيّ شيء أيضًا. كيف؟ نضع deleteCount يساوي الصفر 0: let arr = ["I", "study", "JavaScript"]; arr.splice(2, 0, "complex", "language"); alert( arr ); // "I", "study", "complex", "language", "JavaScript" الفهارس السالبة ممكنة أيضًا يمكننا هنا وفي توابِع المصفوفات الأخرى استعمال الفهارس السالبة. وظيفتها تحديد المكان بدءًا من نهاية المصفوفة، هكذا: let arr = [1, 2, 5]; arr.splice(-1, 0, 3, 4); alert( arr ); // 1,2,3,4,5 القطع slice التابِع arr.slice أبسط بكثير من شبيهه arr.splice. صياغته هي: arr.slice([start], [end]) وهو يُعيد مصفوفة جديدةً بنسخ العناصر من الفهرس start إلى end (باستثناء end). يمكن أن تكون start وحتّى end سالبتان، بهذا يُعدّ المحرّك القيمتان أماكن بدءًا من نهاية المصفوفة. هذا التابِع يشبه تابِع السلاسل النصية str.slice، ولكن بدل السلاسل النصية الفرعية، يُعيد المصفوفات الفرعية. إليك المثال الآتي: let arr = ["t", "e", "s", "t"]; // (نسخة تبدأ من 1 وتنتهي عند 3) alert( arr.slice(1, 3) ); // e,s // ‫(نسخة تبدأ من ‎-2 وتنتهي في النهاية) alert( arr.slice(-2) ); // s,t يمكننا أيضًا استدعائها بلا وُسطاء: يُنشئ arr.slice()‎ نسخة عن arr. نستعمل هذا غالبًا لأخذ نسخة وإجراء تعديلات عليها دون تعديل المصفوفة الأصلية، وتركها كما هي. الربط concat يُنشئ التابِع arr.concat مصفوفةً جديدة فيها القيم الموجودة في المصفوفات والعناصر الأخرى. صياغته هي: arr.concat(arg1, arg2...) وهو يقبل أيّ عدد من الوُسطاء، أكانت مصفوفات أو قيم. أمّا ناتجه هو مصفوفة جديدة تحوي العناصر من arr، ثم arg1 فَـ arg2 وهكذا دواليك. لو كان الوسيط argN نفسه مصفوفة، فستُنسخ كل عناصره، وإلّا فسيُنسخ الوسيط نفسه. لاحِظ هذا المثال: let arr = [1, 2]; // ‫اصنع مصفوفة فيها العنصرين: arr و [3,4] alert( arr.concat([3, 4]) ); // 1,2,3,4 // ‫اصنع مصفوفة فيها العناصر: arr و[3,4] و[5,6] alert( arr.concat([3, 4], [5, 6]) ); // 1,2,3,4,5,6 // ‫اصنع مصفوفة فيها العنصرين: arr و[3,4]، بعدها أضِف القيمتين 5 و 6 alert( arr.concat([3, 4], 5, 6) ); // 1,2,3,4,5,6 عادةً تنسخ المصفوفة عناصر المصفوفات الأخرى. بينما الكائنات الأخرى (حتّى لو كانت مثل المصفوفات) فستُضاف كتلة كاملة. let arr = [1, 2]; let arrayLike = { 0: "something", length: 1 }; alert( arr.concat(arrayLike) ); // 1,2,[object Object] … ولكن لو كان للكائن الشبيه بالمصفوفات خاصية Symbol.isConcatSpreadable، فستتعامل معه concat مثلما تتعامل مع المصفوفات: ستُضاف عناصره بدل كيانه: let arr = [1, 2]; let arrayLike = { 0: "something", 1: "else", [Symbol.isConcatSpreadable]: true, length: 2 }; alert( arr.concat(arrayLike) ); // 1,2,something,else التكرار: لكلّ forEach يتيح لنا التابِع arr.forEach تشغيل إحدى الدوال على كلّ عنصر من عناصر المصفوفة. الصياغة: arr.forEach(function(item, index, array) { // ... استعملهما فيما تريد }); مثال على عرض كلّ عنصر من عناصر المصفوفة: // ‫لكلّ عنصر، استدعِ دالة التنبيه alert ["Bilbo", "Gandalf", "Nazgul"].forEach(alert); بينما هذه الشيفرة تحبّ الكلام الزائد ومكانها في المصفوفة المحدّدة: ["Bilbo", "Gandalf", "Nazgul"].forEach((item, index, array) => { alert(`${item} is at index ${index} in ${array}`); }); ناتج التابِع (لو أعادَ شيئًا أصلًا) يُهمل ويُرمى. البحث في المصفوفات أمّا الآن لنرى التوابع التي تبحث في المصفوفة. التوابِع indexOf و lastIndexOf و includes للتوابِع arr.indexOf و arr.lastIndexOf و arr.includes نفس الصياغة ووظيفتها هي ذات وظيفة تلك بنسخة النصوص النصية، الفرق أنها هنا تتعامل مع العناصر بدل المحارف: arr.indexOf(item, from)‎ -- يبحث عن العنصر item بدءًا من الفهرس from، ويُعيد فهرسه حيث وجده. ولو لم يجده، يُعيد -1. arr.lastIndexOf(item, from)‎ -- نفسه، ولكن البحث يبدأ من اليمين وينتهي في اليسار. arr.includes(item, from)‎ -- يبحث عن العنصر item بدءًا من الفهرس from، ويُعيد true إن وجدته. مثال: let arr = [1, 0, false]; alert( arr.indexOf(0) ); // 1 alert( arr.indexOf(false) ); // 2 alert( arr.indexOf(null) ); // -1 alert( arr.includes(1) ); // true لاحظ أنّ التوابِع تستعمل الموازنة بِـ ===. لذا لو كنّا نبحث عن false، فستبحث هي عن false نفسها وليس الصفر. لو أردت معرفة فيما كانت تحتوي المصفوفة على عنصر معيّن، ولا تريد معرفة فهرسه، فدالة arr.includes مناسبة لك. وهناك أيضًا أمر، تختلف includes عن سابقاتها indexOf/lastIndexOf بأنّها تتعامل مع NaN كما ينبغي: const arr = [NaN]; alert( arr.indexOf(NaN) ); // ‫يُعيد ‎-1 (الصحيح هو 0 إلّا أنّ الموازنة === لا تعمل مع NaN) alert( arr.includes(NaN) );// true (الآن صحيح) البحث عبر find و findIndex لنقل أنّ لدينا مصفوفة من الكائنات، كيف نجد الكائن حسب شرط معيّن؟ هنا يمكننا استغلال التابِع arr.find(fn). صياغته هي: let result = arr.find(function(item, index, array) { // ‫لو أُعيدت القيمة true، فيُعاد العنصر ويتوقّف التعداد // ‫لو لم نجد ما نريد نُعيد undefined }); تُستدعى الدالة على كل عنصر من عناصر المصفوفة، واحدًا بعد الآخر: item: العنصر. index: الفهرس. array: المصفوفة نفسها. لو أعادت true، يتوقّف البحث ويُعاد العنصر item. إن لم يوجد شيء فيُعاد undefined. نرى في هذا المثال مصفوفة من المستخدمين، لكلّ مستخدم حقلان id وname. نريد الذي يتوافق مع الشرط id == 1: let users = [ {id: 1, name: "John"}, {id: 2, name: "Pete"}, {id: 3, name: "Mary"} ]; let user = users.find(item => item.id == 1); alert(user.name); // John في الحياة العملية، يكثُر استعمال الكائنات في المصفوفات، ولهذا فالتابِع find مفيد جدًا لنا. يمكنك ملاحظة بأنّا في المثال مرّرنا للتابِع find الدالة item => item.id == 1 وفيها وسيط واحد. هذا طبيعي فنادرًا ما نستعمل الوُسطاء البقية في هذه الدالة. يتشابه التابِع arr.findIndex كثيرًا مع هذا، عدا على أنّه يُعيد فهرس العنصر الذي وجده بدل العنصر نفسه، ويُعيد ‎-1 لو لم يجد شيئًا. الترشيح filter يبحث التابِع find عن أوّل عنصر (واحد فقط) يُحقّق للدالة شرطها فتُعيد true. لو أردت إعادة أكثر من واحد فيمكن استعمال arr.filter(fn)‎. تشبه صياغة filter التابِع find، الفرق هو إعادته لمصفوفة بكلّ العناصر المتطابقة: let results = arr.filter(function(item, index, array) { // ‫لو كانت true فتُضاف القائمة إلى مصفوفة النتائج ويتواصل التكرار // يُعيد مصفوفة فارغة إن لم يجد شيئًا }); مثال: let users = [ {id: 1, name: "John"}, {id: 2, name: "Pete"}, {id: 3, name: "Mary"} ]; // يُعيد مصفوفة تحتوي على أوّل مستخدمَين اثنين let someUsers = users.filter(item => item.id < 3); alert(someUsers.length); // 2 التعديل على عناصر المصفوفات لنرى الآن التوابِع التي تُعدّل المصفوفة وتُعيد ترتيبها. الخارطة map يُعدّ التابِع arr.map أكثرها استخدامًا وفائدةً أيضًا. ما يفعله هو استدعاء الدالة على كلّ عنصر من المصفوفة وإعادة مصفوفة بالنتائج. صياغته هي: let result = arr.map(function(item, index, array) { // يُعيد القيمة الجديدة عوض العنصر }); مثلًا، هنا نعدّل كل عنصر فنحوّله إلى طوله: let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length); alert(lengths); // 5,7,6 sort(fn)‎ نُرتّب باستدعاء arr.sort()‎ المصفوفة كما هي دون نسخها فنغيّر ترتيب عناصرها. هي الأخرى تُعيد المصفوفة المُرتّبة، ولكن غالبًا ما نُهمل القيمة المُعادة فالمصفوفة arr هي التي تتغيّر. مثال: let arr = [ 1, 2, 15 ]; // يعيد التابع ترتيب محتوى المصفوفة arr.sort(); alert( arr ); // 1, 15, 2 هل لاحظت بأنّ الناتج غريب؟ صار ‎1, 15, 2. ليس هذا ما نريد. ولكن، لماذا؟ مبدئيًا، تُرتّب العناصر وكأنها سلاسل نصية. بالمعنى الحرفي للكلمة: تُحوّل كل العناصر إلى سلاسل نصية عند الموازنة. والترتيب المعجماتي هو المتّبع لترتيب السلاسل النصية، ‎"2" > "15"‎ صحيحة حقًا. علينا لاستعمال الترتيب الذي نريده تمريرَ دالة تكون وسيطًا للتابِع arr.sort()‎. على الدالة موازنة قيمتين اثنتين (أيًا كانتا) وإعادة الناتج: function compare(a, b) { // لو كانت القيمة الأولى أكبر من الثانية if (a > b) return 1; // لو تساوت القيمتين if (a == b) return 0; // لو كانت القيمة الأولى أصغر من الثانية if (a < b) return -1; } مثال عن الترتيب لو كانت القيم أعدادًا: function compareNumeric(a, b) { if (a > b) return 1; if (a == b) return 0; if (a < b) return -1; } let arr = [ 1, 2, 15 ]; arr.sort(compareNumeric); alert(arr); // 1, 2, 15 الآن صارت تعمل كما نريد. لنتوقف لحظة ونفكّر فيما يحدث تمامًا. أنتّفق بأنّ المصفوفة arr يمكن أن تحتوي أيّ شيء؟ أيّ شيء من الأعداد أو السلاسل النصية أو الكائنات أو غيرها. كلّ ما لدينا هو مجموعة من العناصر. لترتيبها نحتاج دالة ترتيب تعرف طرقة مقارنة عناصر المصفوفة. مبدئيًا، الترتيب يكون بالسلاسل النصية. يُنفِّذ التابع arr.sort(fn)‎في طيّاته خوارزمية فرز عامّة. لسنا نكترث كيف تعمل هذه الخوارزمية خلف الكواليس (وهي غالبًا quicksort محسّنة)، بل نكترث بأنّها ستمرّ على المصفوفة، تُوازن عناصرها باستعمال الدالة المقدّمة أعلاه وتُعيد ترتيبها. نكترث بأن نقدّم دالة fn التي ستؤدّي الموازنة. بالمناسبة، لو أردت معرفة العناصر التي تُوازنها الدالة حاليًا، فلا بأس. لن يقتلك أحد لو عرضتها: [1, -2, 15, 2, 0, 8].sort(function(a, b) { alert( a + " <> " + b ); }); يمكن أن تقارن الخوارزمية العنصر مع غيره من العناصر، ولكنّها تحاول قدر الإمكان تقليص عدد الموازنات. يمكن أن تُعيد دالة الموازنة أيّ عدد في الواقع، ليس على دالة الموازنة إلّا إعادة عدد موجب بدلالة «هذا أكبر من ذاك» وسالب بدلالة «هذا أصغر من ذاك». يمكننا هكذا كتابة الدوال بأسطر أقل: That allows to write shorter functions: let arr = [ 1, 2, 15 ]; arr.sort(function(a, b) { return a - b; }); alert(arr); // 1, 2, 15 تحيا الدوال السهمية أتذكر الدوال السهمية من فصل تعابير الدوال والدوال السهمية؟ يمكننا استعمالها أيضًا لتبسيط كود الفرز: arr.sort( (a, b) => a - b ); لا تفرق هذه عن تلك الطويلة بشيء، البتة. العكس reverse يعكس التابِع arr.reverse ترتيب العناصر في المصفوفة arr. مثال: let arr = [1, 2, 3, 4, 5]; arr.reverse(); alert( arr ); // 5,4,3,2,1 كما ويُعيد المصفوفة arr بعد عكسها. التقسيم split والدمج join إليك موقفًا من الحياة العملية. تحاول الآن برمجة تطبيق مراسلة، ويُدخل المستخدم قائمة المستلمين بفاصلة بين كلّ واحد: John, Pete, Mary. ولكن لنا نحن المبرمجين، فالمصفوفة التي تحتوي الأسماء أسهل بكثير من السلسلة النصية. كيف السبيل إذًا؟ هذا ما يفعله التابِع str.split(delim)‎. يأخذ السلسلة النصية ويقسمها إلى مصفوفة حسب محرف القاسِم delim المقدّم. في المثال أعلاه نقسم حسب «فاصلة بعدها مسافة»: let names = 'Bilbo, Gandalf, Nazgul'; let arr = names.split(', '); for (let name of arr) { alert( `A message to ${name}.` ); // A message to Bilbo (والبقية) } للتابِع split وسيطًا عدديًا اختياريًا أيضًا، وهو يحدّ طول المصفوفة. لو قدّمته فستُهمل العناصر الأخرى. ولكن في الواقع العملي، نادرًا ما ستفيدك هذا: let arr = 'Bilbo, Gandalf, Nazgul, Saruman'.split(', ', 2); alert(arr); // Bilbo, Gandalf التقسيم إلى أحرف لو ناديت split(s)‎ وتركت s فارغًا فستُسقم السلسلة النصية إلى مصفوفة من الأحرف: let str = "test"; alert( str.split('') ); // t,e,s,t على العكس من split فنداء arr.join(glue)‎ يُنشئ سلسلة نصية من عناصر arr مجموعةً معًا «باللاصق». مثال: let arr = ['Bilbo', 'Gandalf', 'Nazgul']; let str = arr.join(';'); alert( str ); // Bilbo;Gandalf;Nazgul التابِعان reduce و reduceRight متى ما أردنا أن نمرّ على عناصر المصفوفة، استعملنا forEach أو for أو for..of. ومتى ما أردنا أن نمرّ ونُعيد بيانات كلّ عنصر، استعملنا map. نفس الحال مع التابعين arr.reduce وarr.reduceRight، إلّا أنهما ليسا بالسهولة نفسها. يُستعمل هذان التابعان لحساب قيمة واحدة حسب عناصر المصفوفة. هذه الصياغة: let value = arr.reduce(function(previousValue, item, index, array) { // ... }, [initial]); تُطبّق الدالة على كل عناصر المصفوفة واحدًا بعد الآخر، و«تنقل» النتيجة إلى النداء التالي لها: وُسطاء الدالة: previousValue - نتيجة النداء السابق للدالة. يُساوي قيمة initial في أوّل نداء (لو قُدّمت أصلًا). item -- العنصر الحالي في المصفوفة. index -- مكان العنصر. array -- المصفوفة نفسها. حين تُطبّق الدالة، تُمرّر إليها نتيجة النداء السابق في أوّل وسيط. أجل، معقّد قليلًا، لكن ليس كما تتخيّل لو قلنا أنّ الوسيط الأول بمثابة «ذاكرة» تخزّن النتيجة النهائية من إجراءات التنفيذ التي سبقتها. وفي آخر نداء تصير نتيجة التابِع reduce. ربّما نقدّم مثالًا لتسهيل المسألة. هنا نعرف مجموعة عناصر المصفوفة في سطر برمجي واحد: let arr = [1, 2, 3, 4, 5]; let result = arr.reduce((sum, current) => sum + current, 0); alert(result); // 15 الدالة المُمرّرة إلى reduce تستعمل وسيطين اثنين فقط، وهذا كافٍ عادةً. لنرى تفاصيل النداءات. في أوّل مرّة، قيمة sum هي قيمة initial (آخر وسيط في reduce) وتساوي 0، وcurrent هي أوّل عنصر في المصفوفة وتساوي 1. إذًا فناتج الدالة هو 1. في النداء التالي، sum = 1 ونُضيف العنصر الثاني في المصفوفة (2) ونُعيد القيمة. في النداء الثالث، sum = 3، ونُضيف العنصر التالي في المصفوفة، وهكذا دواليك إلى آخر نداء… هذا سير العملية الحسابية: وهكذا نمثّلها في جدول (كلّ صف يساوي نداء واحد للدالة على العنصر التالي في المصفوفة): sum current الناتج أوّل نداء 0 1 1 ثاني نداء 1 2 3 ثالث نداء 3 3 6 رابع نداء 6 4 10 خامس نداء 10 5 15 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 arr = [1, 2, 3, 4, 5]; // ‫أزلنا القيمة الأولية من التابِع reduce (اختفت القيمة 0) let result = arr.reduce((sum, current) => sum + current); alert( result ); // 15 وستكون النتيجة متطابقة، إذ أنّ reduce تأخذ أول عنصر من المصفوفة على أنّه قيمة أولية (لو لم نقدّم نحن قيمة أولية) وتبدأ العملية من العنصر الثاني. جدول العملية الحسابية مُطابق للجدول أعلاه، لو حذفنا أول سطر فيه. ولكن عليك أن تحترس حين لا تقدّم تلك القيمة. لو كانت المصفوفة فارغة فنداء reduce بدون القيمة الأولية سيعطيك خطأً. مثال على ذلك: let arr = []; arr.reduce((sum, current) => sum + current); الشيفرة السابقة ستطلق خطأ، إذ لا يمكن استدعاء reduce مع مصفوفة فارغة دون قيمة أولية، وتحل المشكلة بتوفير قيمة أولية، وستعاد آنذاك. لذا خُذ هذه النصيحة وحدّد قيمة أولية دومًا. لا يختلف التابِع arr.reduceRight عن هذا أعلاه إلا بأنّه يبدأ من اليمين وينتهي على اليسار. Array.isArray المصفوفات ليست نوعًا منفصلًا في اللغة، بل هي مبنيّة على الكائنات. لذا typeof لن تفيدك في التفريق بين الكائن العادي والمصفوفة: alert(typeof {}); // ‫كائن object alert(typeof []); // كائن أيضًا …ولكن، المصفوفات تستعمل كثيرًا جدًا لدرجة تقديم تابِع خاص لهذا الغرض: Array.isArray(value)‎. يُعيد هذا التابِع true لو كانت value مصفوفة حقًا، وfalse لو لم تكن. alert(Array.isArray({})); // false alert(Array.isArray([])); // true تدعم أغلب التوابِع thisArg تقبل أغلب توابِع المصفوفات تقريبًا، التوابع التي تستدعي دوالًا (مثل find وfilter وmap، عدا sort) - تقبل المُعامل الاختياري thisArg. لم نشرح هذا المُعامل في الأقسام أعلاه إذ أنّه نادرًا ما يُستعمل. ولكن علينا الحديث عنه لألا يكون الشرح ناقصًا. هذه الصياغة الكاملة لهذه التوابِع: arr.find(func, thisArg); arr.filter(func, thisArg); arr.map(func, thisArg); // ... // ‫الوسيط thisArg هو آخر وسيط اختياري تكون قيمة المُعامل thisArg للدالة func تساوي this. هنا مثلًا نستعمل تابِع كائن army على أنّه مرشّح، والوسيط thisArg يمرّر سياق التنفيذ وذلك لإيجاد المستخدمين الذين يعيد التابع army.canJoin القيمة true: let army = { minAge: 18, maxAge: 27, canJoin(user) { return user.age >= this.minAge && user.age < this.maxAge; } }; let users = [ {age: 16}, {age: 20}, {age: 23}, {age: 30} ]; *!* let soldiers = users.filter(army.canJoin, army); */!* alert(soldiers.length); // 2 alert(soldiers[0].age); // 20 alert(soldiers[1].age); // 23 لو استعملنا في المثال أعلاه users.filter(army.canJoin)‎ فسيُستدعى التابِع army.canJoin كدالة مستقلة بذاتها حيث this=undefined، ما سيؤدي إلى خطأ. يمكن استبدال استدعاء users.filter(army.canJoin, army)‎ بالتعليمة التي تُؤدّي ذات الغرض users.filter(user => army.canJoin(user))‎. نستعمل الأولى أكثر من الثانية إذ أنّ الناس تفهمها أكثر من تلك. ملخص ورقة فيها كل توابِع الدوال (غُشّ منها): لإضافة العناصر وإزالتها: push(...items)‎ -- تُضيف العناصر items إلى النهاية، pop()‎ -- تستخرج عنصرًا من النهاية، shift()‎ -- تستخرج عنصرًا من البداية، unshift(...items)‎ -- تُضيف العناصر items إلى البداية. splice(pos, deleteCount, ...items)‎ -- بدءًا من العنصر ذي الفهرس pos، احذف deleteCount من العناصر وأدرِج مكانه العناصر items. slice(start, end)‎ -- أنشِئ مصفوفة جديدة وانسخ عناصرها بدءًا من start وحتّىend(ولكن دونend). concat(...items)‎ -- أعِد مصفوفة جديدة: انسخ كل عناصر المصفوفة الحالية وأضَِف إليها العناصر items. لو كانت واحدة من عناصر items مصفوفة أيضًا، فستُنسخ عناصرها بدل. لتبحث عن العناصر: indexOf/lastIndexOf(item, pos)‎ -- ابحث عن العنصر item بدءًا من العنصر ذي الفهرس pos وأعِد فهرسه أو أعِد ‎-1 لو لم تجده. includes(value)‎ -- أعِد القيمة true لو كان العنصر value في المصفوفة، وإلا أعِد false. find/filter(func)‎ -- رشّح العناصر عبر دالة وأعِد أوّل قيمة (أو كل القيم) التي تُعيد الدالة قيمة true لو مُرّر ذلك العنصر لها. findIndex يشبه find، ولكن يُعيد الفهرس بدل القيمة. للمرور على عناصر المصفوفة: forEach(func)‎ -- يستدعي func لكلّ عنصر ولا يُعيد أيّ شيء. لتعديل عناصر المصفوفة: map(func)‎ -- أنشِئ مصفوفة جديدة من نتائج استدعاء func لكلّ من عناصر المصفوفة. sort(func)‎ -- افرز المصفوفة كما هي وأعِد ناتج الفرز. reverse()‎ -- اعكس عناصر المصفوفة كما هي وأعِد ناتج العكس. split/join -- حوّل المصفوفة إلى سلسلة نصية، والعكس أيضًا. reduce(func, initial)‎ -- احسب قيمة من المصفوفة باستدعاء func على كلّ عنصر فيها وتمرير الناتج بين كلّ استدعاء وآخر. وأيضًا: Array.isArray(arr)‎ يفحص لو كانت arr مصفوفة أم لا. لاحظ أنّ التوابِع sort وreverse وsplice تُعدّل المصفوفة نفسها. هذه التوابِع أعلاه هي أغلب ما تحتاج وما تريد أغلب الوقت (99.99%). ولكن هناك طبعًا غيرها: arr.some(fn)‎/arr.every(fn)‎ تفحص المصفوفة. تُنادى الدالة fn على كلّ عنصر من المصفوفة (مثل map). لو كانت أيًا من (أو كل) النتائج true، فيُعيد true، وإلًا يُعيد false. arr.fill(value, start, end)‎‎ -- يملأ المصفوفة بالقيمة المتكرّرة value من الفهرس start إلى الفهرس end. arr.copyWithin(target, start, end)‎‎ -- ينسخ العناصر من العنصر ذا الفهرس start إلى ذا الفهرس end ويلصقها داخلها عند الفهرس target (تعوّض ما هو موجود مكانها في المصفوفة). طالِع الكتيّب لقائمة فيها كل شيء. من أول وهلة سترى بأنّ عدد التوابِع لا ينتهي ومهمة حفظها مستحيلة، ولكن الواقع هي أنّها بسيطة جدًا. طالِع «ورقة الغشّ» لتعرف ما تفعل كلًا منها، ثمّ حُلّ مهام هذا الفصل لتتدرّب عليها وتكون خبيرًا كفاية بتوابِع الدوال. بعدها، لو احتجت التعامل مع المصفوفات ولا تدري ما تفعل، تعال هنا وابحث في ورقة الغشّ عن التابِع المناسب لحالتك. الأمثلة الموجودة ستفيدك فتكتبها كما ينبغي. وسريعًا ما ستتذكّر كل التوابِع من تلقاء نفسك ودون بذل أيّ جهد. تمارين حوّل «border-left-width» إلى «borderLeftWidth» الأهمية: 5 اكتب دالة camelize(str)‎ تغيّر الكلمات المقسومة بِشَرطات مثل «my-short-string» إلى عبارات بتنسيق «سنام الجمل»: «myShortString». بعبارة أخرى: أزِل كلّ الشرطات وحوّل أوّل حرف من كلّ كلمة بعدها إلى الحالة الكبيرة. أمثلة: camelize("background-color") == 'backgroundColor'; camelize("list-style-image") == 'listStyleImage'; camelize("-webkit-transition") == 'WebkitTransition'; تلميح: استعمل split لتقسيم السلسلة النصية إلى مصفوفة، ثمّ عدّل عناصرها وأعِد ربطها بتابِع join. الحل function camelize(str) { return str .split('-') // splits 'my-long-word' into array ['my', 'long', 'word'] .map( // كبر الحروف الأولى لجميع عناصر المصفوفة باستثناء أول عنصر // ['my', 'long', 'word'] --> ['my', 'Long', 'Word'] (word, index) => index == 0 ? word : word[0].toUpperCase() + word.slice(1) ) .join(''); // joins ['my', 'Long', 'Word'] into 'myLongWord' } نطاق ترشيح الأهمية: 4 اكتب دالة filterRange(arr, a, b)‎ تأخذ المصفوفة arr، وتبحث في عناصرها بين a وb وتُعيد مصفوفة بها. يجب ألّا تُعدّل الدالة المصفوفة، بل إعادة مصفوفة جديدة. مثال: let arr = [5, 3, 8, 1]; let filtered = filterRange(arr, 1, 4); alert( filtered ); // 3,1 alert( arr ); // 5,3,8,1 الحل function filterRange(arr, a, b) { // أضفنا الأقواس حول التعبير لتسهيل القراءة return arr.filter(item => (a <= item && item <= b)); } let arr = [5, 3, 8, 1]; let filtered = filterRange(arr, 1, 4); alert( filtered ); // 3,1 alert( arr ); // 5,3,8,1 نطاق ترشيح «كما هو» الأهمية: 4 اكتب دالة filterRangeInPlace(arr, a, b)‎ تأخذ المصفوفة arr وتُزيل منها كل القيم عدا تلك بين a وb. الشرط هو: ‎a ≤ arr ≤ b. يجب أن تُعدّل الدالة المصفوفة، ولا تُعيد شيئًا. مثال: let arr = [5, 3, 8, 1]; // حذف جميع الأعداد باستثناء الواقعة بين 1 و 4 filterRangeInPlace(arr, 1, 4); alert( arr ); // [3, 1] الحل function filterRangeInPlace(arr, a, b) { for (let i = 0; i < arr.length; i++) { let val = arr[i]; // إزالة إن كانت خارج النطاق if (val < a || val > b) { arr.splice(i, 1); i--; } } } let arr = [5, 3, 8, 1]; filterRangeInPlace(arr, 1, 4); // removed the numbers except from 1 to 4 alert( arr ); // [3, 1] الفرز بالترتيب التنازلي الأهمية: 4 let arr = [5, 2, 1, -10, 8]; // ... شيفرة ترتيب العناصر تنازليًا alert( arr ); // 8, 5, 2, 1, -10 الحل let arr = [5, 2, 1, -10, 8]; arr.sort((a, b) => b - a); alert( arr ); نسخ المصفوفة وفرزها الأهمية: 5 في يدنا مصفوفة من السلاسل النصية arr. نريد نسخة مرتّبة عنها وترك arr بلا تعديل. أنشِئ دالة copySorted(arr)‎ تُعيد هذه النسخة. let arr = ["HTML", "JavaScript", "CSS"]; let sorted = copySorted(arr); alert( sorted ); // CSS, HTML, JavaScript alert( arr ); // HTML, JavaScript, CSS الحل يمكن أن نستعمل slice()‎ لأخذ نسخة ونفرز المصفوفة: function copySorted(arr) { return arr.slice().sort(); } let arr = ["HTML", "JavaScript", "CSS"]; *!* let sorted = copySorted(arr); */!* alert( sorted ); alert( arr ); خارطة بالأسماء الأهمية: 5 لدينا مصفوفة من كائنات user، لكلّ منها صفة user.name. اكتب كودًا يحوّلها إلى مصفوفة من الأسماء. مثال: let john = { name: "John", age: 25 }; let pete = { name: "Pete", age: 30 }; let mary = { name: "Mary", age: 28 }; let users = [ john, pete, mary ]; let names = /* شيفرتك هنا */ alert( names ); // John, Pete, Mary الحل let john = { name: "John", age: 25 }; let pete = { name: "Pete", age: 30 }; let mary = { name: "Mary", age: 28 }; let users = [ john, pete, mary ]; let names = users.map(item => item.name); alert( names ); // John, Pete, Mary أنشِئ آلة حاسبة يمكن توسعتها لاحقًا الأهمية: 5 أنشِئ دالة إنشاء باني «constructor»‏ Calculator تُنشئ كائنات من نوع «آلة حاسبة» يمكن لنا «توسعتها». تنقسم هذه المهمة إلى جزئين اثنين: أولًا، نفّذ تابِع calculate(str)‎ يأخذ سلسلة نصية (مثل "1 + 2") بالتنسيق «عدد مُعامل عدد» (أي مقسومة بمسافات) ويُعيد الناتج. يجب أن يفهم التابِع الجمع + والطرح -. مثال عن الاستعمال: let calc = new Calculator; alert( calc.calculate("3 + 7") ); // 10 بعدها أضِف تابِع addMethod(name, func)‎ يُعلّم الآلة الحاسبة عمليّة جديدة. يأخذ التابِع المُعامل name ودالة func(a,b)‎ بوسيطين تُنفّذ هذه العملية. كمثال على ذلك سنُضيف عمليات الضرب * والقسمة / والأُسّ **: let powerCalc = new Calculator; powerCalc.addMethod("*", (a, b) => a * b); powerCalc.addMethod("/", (a, b) => a / b); powerCalc.addMethod("**", (a, b) => a ** b); let result = powerCalc.calculate("2 ** 3"); alert( result ); // 8 في هذه المهمة ليس هناك أقواس رياضية أو تعابير معقّدة. تفصل الأعداد والمُعامل مسافة واحدة فقط. يمكنك التعامل مع الأخطاء لو أردت. الحل لاحظ طريقة تخزين التوابِع، حيث تُضاف إلى صفة this.methods فقط. كلّ الشروط والتحويلات العددية موجودة في التابِع calculate. يمكننا في المستقبل توسيعه ليدعم تعابير أكثر تعقيدًا. function Calculator() { this.methods = { "-": (a, b) => a - b, "+": (a, b) => a + b }; this.calculate = function(str) { let split = str.split(' '), a = +split[0], op = split[1], b = +split[2] if (!this.methods[op] || isNaN(a) || isNaN(b)) { return NaN; } return this.methods[op](a, b); } this.addMethod = function(name, func) { this.methods[name] = func; }; } خارطة بالكائنات فرز المستخدمين حسب أعمارهم الأهمية: 5 اكتب دالة sortByAge(users)‎ تأخذ مصفوفة من الكائنات بالصفة age وتُرتبّها حسب أعمارهم age. مثال: let john = { name: "John", age: 25 }; let pete = { name: "Pete", age: 30 }; let mary = { name: "Mary", age: 28 }; let arr = [ pete, john, mary ]; sortByAge(arr); // [john, mary, pete] alert(arr[0].name); // John alert(arr[1].name); // Mary alert(arr[2].name); // Pete الحل function sortByAge(arr) { arr.sort((a, b) => a.age > b.age ? 1 : -1); } let john = { name: "John", age: 25 }; let pete = { name: "Pete", age: 30 }; let mary = { name: "Mary", age: 28 }; let arr = [ pete, john, mary ]; sortByAge(arr); // [john, mary, pete] alert(arr[0].name); // John alert(arr[1].name); // Mary alert(arr[2].name); // Pete خلط المصفوفات الأهمية: 3 اكتب دالة shuffle(array)‎ تخلط عناصر المصفوفة (أي ترتّبها عشوائيًا). يمكن بتكرار نداء shuffle إعادة العناصر بترتيب مختلف. مثال: let arr = [1, 2, 3]; shuffle(arr); // arr = [3, 2, 1] shuffle(arr); // arr = [2, 1, 3] shuffle(arr); // arr = [3, 1, 2] // ... يجب أن تكون جميع احتمالات ترتيب العناصر متساوية. فمثلًا يمكن إعادة ترتيب [1,2,3] لتكون [1,2,3] أو [1,3,2] أو [3,1,2] أو أو أو، واحتمال حدوث كلّ حالة متساوٍ. الحل هذا هو الحل البسيط: function shuffle(array) { array.sort(() => Math.random() - 0.5); } let arr = [1, 2, 3]; shuffle(arr); alert(arr); تعمل هذه الشيفرة (نوعًا ما) إذ أنّ Math.random() - 0.5 عددٌ عشوائي ويمكن أن يكون موجبًا أم سالبًا، بذلك تُعيد دالة الفرز ترتيب العناصر عشوائيًا. ولكن ليس هذه الطريقة التي تعمل فيها دوال الفرز، إذ ليس لكلّ حالات التبديل الاحتمال نفسه. فمثلًا في الشيفرة أعلاه، تُنفّذ shuffle ‏1000000 مرّة وتعدّ مرّات ظهور النتائج الممكنة كلّها: function shuffle(array) { array.sort(() => Math.random() - 0.5); } // نعدّ مرّات ظهور كلّ عمليات التبديل الممكنة let count = { '123': 0, '132': 0, '213': 0, '231': 0, '321': 0, '312': 0 }; for (let i = 0; i < 1000000; i++) { let array = [1, 2, 3]; shuffle(array); count[array.join('')]++; } // نعرض عدد عمليات التبديل الممكنة for (let key in count) { alert(`${key}: ${count[key]}`); } إليك عيّنة عن الناتج (إذ يعتمد على محرّك جافاسكربت): 123: 250706 132: 124425 213: 249618 231: 124880 312: 125148 321: 125223 نرى تحيّز الشيفرة بوضوح شديد، إذ تظهر 123 و213 أكثر بكثير من البقية. يختلف ناتج هذه الشيفرة حسب محرّكات جافاسكربت ولكن هذا يكفي لنقول بأنّ هذه الطريقة ليست موثوقة. ولكن لمَ لا تعمل الشيفرة؟ بشكل عام فتابِع sort أشبه ”بالصندوق الأسود“: نرمي فيه مصفوفة ودالة موازنة وننتظر أن تفرز لنا المصفوفة. ولكن بسبب عشوائية الموازنة يختلّ ذكاء الصندوق الأسود، وهذا الاختلال يعتمد على طريقة كتابة كلّ محرّك للشيفرة الخاصة به. ثمّة طرق أخرى أفضل لهذه المهمّة، مثل الخوارزمية خلّاط فِشر ييتس الرائعة. فكرتها هي المرور على عناصر المصفوفة بالعكس وتبديل كلّ واحد بآخر قبله عشوائيًا: function shuffle(array) { for (let i = array.length - 1; i > 0; i--) { let j = Math.floor(Math.random() * (i + 1)); // ‫رقم عشوائي من 0 إلى i // let t = array[i]; array[i] = array[j]; array[j] = t [array[i], array[j]] = [array[j], array[i]]; } } بدلنا بالمثال هذا العنصرين array‎ و array[j]‎ وذلك نستعمل صياغة ”الإسناد بالتفكيك" وستجد تفاصيل أكثر عن هذه الصياغة في فصول لاحقة. لنختبر الطريقة هذه بنفس ما اختبرنا تلك: function shuffle(array) { for (let i = array.length - 1; i > 0; i--) { let j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } } // نعدّ مرّات ظهور كلّ عمليات التبديل الممكنة let count = { '123': 0, '132': 0, '213': 0, '231': 0, '321': 0, '312': 0 }; for (let i = 0; i < 1000000; i++) { let array = [1, 2, 3]; shuffle(array); count[array.join('')]++; } // نعرض عدد عمليات التبديل الممكنة for (let key in count) { alert(`${key}: ${count[key]}`); } عيّنة عن الناتج: 123: 166693 132: 166647 213: 166628 231: 167517 312: 166199 321: 166316 الآن كل شيء سليم: لكلّ عمليات التبديل ذات الاحتمال. كما أنّ خوارزمية ”فِشر ييتس“ أفضل من ناحية الأداء إذ ليس علينا تخصيص الموارد لعملية ”الفرز“. ما متوسّط الأعمار؟ الأهمية: 4 اكتب دالة getAverageAge(users)‎ تأخذ مصفوفة من كائنات لها الصفة age وتُعيد متوسّط الأعمار. معادلة المتوسّط: ‎(age1 + age2 + ... + ageN) / N. مثال: let john = { name: "John", age: 25 }; let pete = { name: "Pete", age: 30 }; let mary = { name: "Mary", age: 29 }; let arr = [ john, pete, mary ]; alert( getAverageAge(arr) ); // (25 + 30 + 29) / 3 = 28 الحل function getAverageAge(users) { return users.reduce((prev, user) => prev + user.age, 0) / users.length; } let john = { name: "John", age: 25 }; let pete = { name: "Pete", age: 30 }; let mary = { name: "Mary", age: 29 }; let arr = [ john, pete, mary ]; alert( getAverageAge(arr) ); // 28 ترشيح العناصر الفريدة في المصفوفة الأهمية: 4 لمّا أنّ arr مصفوفة، أنشِئ دالة unique(arr)‎ تُعيد مصفوفة فيها عناصر arr غير مكرّرة. مثال: function unique(arr) { /* شيفرة هنا */ } let strings = ["Hare", "Krishna", "Hare", "Krishna", "Krishna", "Krishna", "Hare", "Hare", ":-O" ]; alert( unique(strings) ); // Hare, Krishna, :-O الحل ما سنفعل هو المرور على عناصر المصفوفة: سنفحص كلّ عنصر ونرى إن كان في المصفوفة الناتجة. إن كان كذلك… نُهمله، وإن لم يكن، نُضيفه إلى المصفوفة. function unique(arr) { let result = []; for (let str of arr) { if (!result.includes(str)) { result.push(str); } } return result; } let strings = ["Hare", "Krishna", "Hare", "Krishna", "Krishna", "Krishna", "Hare", "Hare", ":-O" ]; alert( unique(strings) ); // Hare, Krishna, :-O صحيح أنّ الكود يعمل، إلّا أنّ فيه مشكلة أداء محتملة. خلف الكواليس، يمرّ التابِع result.includes(str)‎ على المصفوفة result ويقارن كلّ عنصر مع str ليجد المطابقة المنشودة. لذا لو كان في result مئة 100 عنصر وما من أيّ مطابقة مع str، فعليها المرور على جُلّ result وإجراء 100 حالة مقارنة كاملة. ولو كانت result كبيرة مثل 10000 فيعني ذلك 10000 حالة مقارنة. إلى هنا لا مشكلة، لأنّ محرّكات جافاسكربت سريعة جدًا، والمرور على 1000 عنصر في المصفوفة يحدث في بضعة ميكروثوان. ولكنّا هنا في حلقة for نُجري هذه الشروط لكلّ عنصر من arr. فإن كانت arr.length تساوي 10000 فيعني أنّا سنُجري 10000*10000 = مئة مليون حالة مقارنة. كثير جدًا. إذًا، فهذا الحل ينفع للمصفوفات الصغيرة فقط. سنرى لاحقًا في الفصل كيف نحسّن هذا الكود. ترجمة -وبتصرف- للفصل Array methods من كتاب The JavaScript language .task__importance { color: #999; margin-left: 30px; } .task__answer { border: 3px solid #f7f6ea; margin: 20px 0 14px; position: relative; display: block; padding: 25px 30px; } اقرأ أيضًا المقال التالي: الكائنات المكرَّرة (Iterables) المقال السابق: المصفوفات (arrays)
  24. تُتيح لك الكائنات تخزين القيم في مجموعات ذات مفاتيح، وهذا أمر طيّب. ولكنّك دومًا ما ستحتاج في عملك إلى مجموعة مرتّبة، أي أنّ العناصر مرتّبة: عنصر أوّل، عنصر ثانٍ، عنصر ثالث، وهكذا دواليك. تُفيدنا هذه الطريقة في تخزين أمور مثل: المستخدمين والبضائع وعناصر HTML وغيرها. هنا يكون استعمال الكائنات غير موفّق، إذ أنّها لا تقدّم لنا أيّ تابِع يتيح تحديد ترتيب العناصر، فلا يمكننا إضافة خاصيةً جديدةً تحلّ بين الخاصيات الموجودة. لم تُصنع الكائنات لهذا الغرض بتاتًا. توجد بنية بيانات أخرى باسم Array (ندعو هذا النوع بالمصفوفة) وهي تتيح لنا تخزين مجموعات العناصر مرتّبةً. التصريح توجد صياغتان اثنتان لإنشاء مصفوفة فارغة: let arr = new Array(); let arr = []; تحتاج في عملك أغلب الوقت (ونقول أغلب الوقت) الصياغةَ الثانية. يمكننا أيضًا تقديم عناصر أوليّة للمصفوفة نكتبها في أقواس: let fruits = ["Apple", "Orange", "Plum"]; لاحظ أنّ عناصر المصفوفات مرقّمة (مُفهرسة) بدءًا من الرقم صفر. ويمكننا أن نأخذ عنصرًا منها بكتابة ترتيبه في أقواس معقوفة: let fruits = ["Apple", "Orange", "Plum"]; alert( fruits[0] ); // Apple/تفاحة alert( fruits[1] ); // Orange/برتقالة alert( fruits[2] ); // Plum/برقوق يمكن أيضًا تعويض أحد العناصر بأخرى: fruits[2] = 'Pear'; // ‫ ["Apple", "Orange", "Pear"] …أو إضافة أخرى جديدة إلى المصفوفة: fruits[3] = 'Lemon'; // ["Apple", "Orange", "Pear", "Lemon"] نعرف باستعمال التابع length إجمالي العناصر في المصفوفة: let fruits = ["Apple", "Orange", "Plum"]; alert( fruits.length ); // 3 يمكننا أيضًا استعمال alert لعرض المصفوفة كاملةً. let fruits = ["Apple", "Orange", "Plum"]; alert( fruits ); // Apple,Orange,Plum كما يمكن للمصفوفات تخزين أيّ نوع من البيانات. مثلًا: // قيم مختلفة الأنواع let arr = [ 'Apple', { name: 'John' }, true, function() { alert('hello'); } ]; // خُذ الكائن ذا الفهرس 1 ثمّ اعرض اسمه alert( arr[1].name ); // John // خُذ الدالة في الفهرس 3 ثمّ شغّلها arr[3](); // hello الفاصلة نهاية الجملة كما الكائنات، يمكن أن نُنهي عناصر المصفوفات بفاصلة ,: let fruits = [ "Apple", "Orange", "Plum", ]; يُسهّل أسلوب الكتابة «بالفاصلة نهاية الجملة» إضافة العناصر وإزالتها، إذ أن الأسطر البرمجية كلها تصير متشابهة. توابِع الدفع والجلب من بين أنواع المصفوفات، تُعدّ الطوابير أكثرها استعمالًا. تعني الصفوف (في علوم الحاسوب) تجميعات العناصر المرتّبة والتي تدعم العمليتين هاتين: الدفع push: يُضيف عنصرًا نهاية الصفّ الأخذ shift: يأخذ عنصرًا من بداية الصفّ، فيتحرّك الصفّ ويصير العنصر الثاني هو الأول فيه. تدعم المصفوفات كلتا العمليتين هاتين. وفي الحياة العملية، استعمال هاتين العمليتين ضروري دومًا. نأخذ مثلًا مجموعة رسائل مرتّبة يجب عرضها على الشاشة، أو «صف رسائل». هناك طريقة أخرى لاستعمال المصفوفات، وهي بنية البيانات بالاسم «كومة». تدعم الأكوام عمليتين أيضًا: الدفع push: يُضيف عنصرًا نهاية الكومة. السحب pop: يأخذ عنصرًا من نهاية الكومة. أي أنّ العناصر الجديدة تُضاف دومًا إلى آخر الكومة، وتُزال أيضًا من نهايتها. عادةً ما نرسم هذه الأكوام مثل أكوام بطاقات اللعب: البطاقات الجديدة تُضاف أعلى الكومة، وتُأخذ من أعلاها أيضًا: في الأكوام، آخر عنصر ندفعه إليها يكون أوّل من يُأخذ، ويسمّى هذا بمبدأ «آخر من يدخل أول من يخرج» (Last-In-First-Out). أمّا في الطوابير، فهي «أول من يدخل أول من يخرج» (First-In-First-Out). تعمل المصفوفات في جافاسكربت بالطريقتين، صفوف أو أكوام. يمكنك استعمالها لإضافة العناصر وإزالتها من/إلى بداية المصفوفة ونهايتها. تُسمّى بنية البيانات هذه (في علوم الحاسوب) باسم «الطوابير ذات الطرفين». التوابع التي تؤثّر على نهاية المصفوفة: pop: يستخرج آخر عنصر من المصفوفة ويُعيده: let fruits = ["Apple", "Orange", "Pear"]; alert( fruits.pop() ); // ‫أزِل «Pear» بتنبيه عبر الدالة alert alert( fruits ); // Apple, Orange push: يُضيف العنصر إلى آخر المصفوفة: let fruits = ["Apple", "Orange"]; fruits.push("Pear"); alert( fruits ); // Apple, Orange, Pear باستدعاء fruits.push(...)‎ كأنّما استدعيت fruits[fruits.length] = ...‎. التوابِع التي تؤثّر على بداية المصفوفة: shift: يستخرج أوّل عنصر من المصفوفة وتُعيده: let fruits = ["Apple", "Orange", "Pear"]; alert( fruits.shift() ); // ‫أزِل التفاحة واعرضها بِ‍ alert alert( fruits ); // Orange, Pear unshift: يُضيف العنصر إلى أوّل المصفوفة: let fruits = ["Orange", "Pear"]; fruits.unshift('Apple'); alert( fruits ); // Apple, Orange, Pear يمكنك أيضًا إضافة أكثر من عنصر في استدعاء واحد من push وunshift: let fruits = ["Apple"]; fruits.push("Orange", "Peach"); fruits.unshift("Pineapple", "Lemon"); // ["Pineapple", "Lemon", "Apple", "Orange", "Peach"] alert( fruits ); داخليًا وخلف الكواليس المصفوفات هي كائنات، كائنات من نوع خاص. القوسان المعقوفان المستعملان للدخول إلى الخاصيات arr[0]‎ هما فعليًا جزء من صياغة الكائنات. داخليًا، لا يفرق ذاك عن obj[key]‎ (إذ arr هو الكائن والأرقام تلك مفاتيح). ما تفعله المصفوفات هو «توسعة» الكائنات بتقديم توابِع خاصّة تعمل مع البيانات والمجموعات المرتّبة، إضافةً إلى تقديم خاصية length، ولكنّ أساسها ما زال الكائنات. تذكّر أنّ هناك 7 أنواع أساسية في جافاسكربت، فقط. المصفوفة هي كائن، وتتصرّف بناءً على ذلك، ككائن. فمثلًا، عند نسخها تُنسخ بالمرجع (By reference): let fruits = ["Banana"] let arr = fruits; // انسخها بالمرجع (متغيران اثنان يُشيران إلى نفس المصفوفة)‏ alert( arr === fruits ); // true arr.push("Pear"); // ‫عدّل المصفوفة «بالمرجع» alert( fruits ); // صاروا الآن عنصرين: ‫Banana, Pear …إلا أنّ المميز حقًا في المصفوفات هي آلية تمثيلها داخليًا، إذ يحاول المحرّك تخزين عناصرها متتابعةً في مساحة الذاكرة، أي واحدة بعد الأخرى، تمامًا مثلما وضّحت الرسوم في هذا الفصل. هناك أيضًا طُرق أخرى لتحسين (optimization) المصفوفات فتعمل بسرعة كبيرة حقًا. ولكن، لو لم نعمل مع المصفوفة على أنّها «تجميعة مرتّبة» بل وكأنّها كائن مثل غيرها، فسينهار هذا كله. يمكننا (تقنيًا) كتابة هذا: // نصنع مصفوفة let fruits = []; // نُسند خاصيةً لها فهرس أكبر من طول المصفوفة بكثير fruits[99999] = 5; // نُنشئ خاصيةً لها أيّ اسم fruits.age = 25; أجل، يمكننا فعل هذا، فالمصفوفات في أساسها كائنات، ويمكننا إضافة ما نريد من خاصيات لها. ولكن المحرّك هنا سيرى بأنّا نُعامل المصفوفة معاملة الكائن العادي. وبهذا -في هذه الحالة- لا تنفع أنواع التحسين المخصّصة للكائنات، وسيُعطّلها المحرّك، وتضيع كل فوائد المصفوفات. هذه طرائق يمكنك فيها إساءة استعمال المصفوفات: إضافة خاصيات ليست عددية مثل arr.test = 5. الفراغات، أي تُضيف arr[0]‎ وبعدها arr[1000]‎ (دون عناصر بينها). ملء المصفوفة بالعكس، أي arr[1000]‎ ثم arr[999]‎ وهكذا. نرجوك هنا أن تعتبر المصفوفات بنًى خاصّة تتعامل مع البيانات المرتّبة، فهي تقدّم لك توابِع خاصّة لهذا بالذات. يتغيّر تعامل محرّكات جافاسكربت حين تتعامل مع المصفوفات، فتعمل مع البيانات المرتّبة المتتابعة، فمن فضلك استعمِلها بهذه الطريقة. لو أردت مفاتيح لا عددية، أو مثلما في الحالات الثلاث أعلاه، فغالبًا لا تكون المصفوفة ما تبحث عنه، بل الكائنات العادية {}. الأداء يعمل التابِعان push/pop بسرعة، بينما shift/unshift بطيئان. لماذا يكون التعامل مع نهاية المصفوفة أسرع من التعامل مع بدايتها؟ لنأخذ نظرة عمًا يحدث أثناء تنفيذ الشيفرة: // خُذ عنصرًا واحدًا من الأوّل fruits.shift(); لا يكفي أن تأخذ العنصر ذا الفهرس 0 وتُزيله، بل عليك أيضًا إعادة ترقيم بقية العناصر وفقًا لذلك. ما تفعله عملية shift هي ثلاث أمور: إزالة العنصر ذا الفهرس 0. تحريك كل العناصر الأخرى إلى يسار المصفوفة، وإعادة ترقيمها من الفهرس رقم 1 إلى 0، ومن 2 إلى 1، وهكذا. تحديث خاصية الطول length. زِد العناصر في المصفوفات، تزيد الوقت اللازم لتحريكها، وتزيد عدد العمليات داخل الذاكرة. مثل shift، تفعل unshift نفس الأمور: فلنُضيف عنصرًا إلى بداية المصفوفة، علينا أولًا تحريك كل العناصر إلى اليمين، أي نزيد فهارسها كلها. وماذا عن push/pop؟ ليس عليها تحريك أيّ عنصر. فلاستخراج عنصر من النهاية، يمحي التابِع pop الفهرس ويعدّل الطول length فيقصّره. إجراءات عملية pop: // خُذ عنصرًا واحدًا من الآخر fruits.pop(); لا تحتاج عملية pop إلى تحريك ولا مقدار ذرة، لأنّ العناصر تبقى كما هي مع فهارسها. لهذا السبب سرعتها تفوق سرعة البرق، أي أقصى سرعة ممكنة. ذات الأمر للتابِع push. الحلقات هذه إحدى أقدم الطرق للمرور على عناصر المصفوفات، استعمال حلقة for بالمرور على فهارس المصفوفة: let arr = ["Apple", "Orange", "Pear"]; for (let i = 0; i < arr.length; i++) { alert( arr[i] ); } ولكن المصفوفات تسمح بطريقة أخرى للمرور عليها، for..of: let fruits = ["Apple", "Orange", "Plum"]; // المرور على عناصر المصفوفة for (let fruit of fruits) { alert( fruit ); } لا تتيح لك حلقة for..of الوصول إلى فهرس العنصر الحالي في الحلقة، بل قيمة العنصر فقط، وفي أغلب الأحيان هذا ما تحتاج، كما وأنّ الشيفرة أقصر. طالما المصفوفات كائنات، فيمكننا (نظريًا) استعمال for..in: let arr = ["Apple", "Orange", "Pear"]; for (let key in arr) { alert( arr[key] ); // Apple, Orange, Pear } ولكن الواقع أنّ الطريقة هذه سيئة، ففيها عدد من المشاكل: تمرّ الحلقة for..in على كل الخاصيات مجتمعةً، وليس العددية منها فقط. توجد في المتصفّح وغيرها من بيئات كائنات «شبيهة بالمصفوفات». أي أن لها خاصية الطول length وخاصيات الفهارس، ولكن لها أيضًا توابِع وخاصيات لا عددية أخرى لا نحتاجها أغلب الأحيان، إلّا أنّ حلقة for..in ستمرّ عليها هي أيضًا. لذا لو اضطررت للعمل مع الكائنات الشبيهة بالمصفوفات، فهذه الخاصيات «الأخرى» ستتسبّب بالمتاعب بلا شك. أداء حلقة for..in يكون بالنحو الأمثل على الكائنات العامة لا المصفوفات، ولهذا سيكون أبطأ 10 أو 100 مرة. طبعًا فالأداء سريع جدًا مع ذلك. هذه السرعة الإضافية ستنفع غالبًا في الحالات الحرجة (أي حين يجب أن يكون تنفيذ الحلقة بأسرع وقت ممكن). مع ذلك، الحرس واجب والاهتمام بهذا الاختلاف مهم. لكن في أغلب الأحيان، استعمال for..in للمصفوفات فكرة سيئة. كلمتان حول «الطول» تتحدّث خاصية الطول length تلقائيًا متى ما عدّلنا المصفوفة. وللدّقة، فهي ليست عدد القيم في المصفوفة، بل أكبر فهرس موجود زائدًا واحد. فمثلًا، لو كان لعنصر واحد فهرس كبير، فسيكون الطول كبيرًا أيضًا: let fruits = []; fruits[123] = "Apple"; alert( fruits.length ); // 124 لكنّنا لا نستعمل المصفوفات هكذا. سجّلها عندك. هناك ما هو عجيب حول خاصية length، ألا وهي أنّها تقبل الكتابة. لو زِدنا قيمتها يدويًا، لا نرى شيئًا تغيّر، ولكن لو أنقصناها، تُبتر المصفوفة حسب الطول، ولا يمكن العودة عن هذه العملية. طالِع هذا المثال: let arr = [1, 2, 3, 4, 5]; // نبتر المصفوفة ونُبقي عنصرين فقط arr.length = 2; alert( arr ); // [1, 2] // نُعيد الطول الذي كان في الأوّل arr.length = 5; // undefined القيم المبتورة لا تعود، وإنما تصبح alert( arr[3] ); إذًا، فالطريقة الأسهل والأبسط لمسح المصفوفة هي: arr.length = 0;‎. new Array()‎ هناك صياغة أخرى يمكن استعمالها لإنشاء المصفوفات: let arr = new Array("Apple", "Pear", "etc"); ولكنّها نادرًا ما تُستعمل، فالأقواس المعقوفة [] أقصر. كما وأنّ هذه الطريقة تقدّم ميزة… مخادعة، إن صحّ التعبير. إن استدعيت new Array وفيها مُعامل واحد فقط (عددي)، فستُنشأ مصفوفة لا عناصر فيها، ولكن بالطول المحدّد. هاك طريقة يمكنك بها تخريب حياتك، لو أردت: let arr = new Array(2); // هل ستكون المصفوفة [2]؟ alert( arr[0] ); //‫غير معرّفة! ليس فيها عناصر. alert( arr.length ); // طولها 2 في هذا الشيفرة، كل عناصر new Array(number)‎ لها القيمة undefined. ولهذا نستعمل الأقواس المعقوفة غالبًا، لنتجنّب هذه المفاجئات السارّة، إلّا لو كنت تعي حقًا ما تفعله. المصفوفات متعدّدة الأبعاد يمكن أن تكون عناصر المصفوفات مصفوفات أخرى أيضًا. نستغلّ هذه الميزة فنعمل مصفوفات متعدّدة الأبعاد لتخزين المصفوفات الرياضية مثلًا: let matrix = [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ]; alert( matrix[1][1] ); // ‫5، العنصر في الوسط تحويل المصفوفات إلى سلاسل نصية تُنفّذ المصفوفات تابِع toString خاصّ بها، فيُعيد قائمة من العناصر مفصولة بفواصل. خُذ هذا المثال: let arr = [1, 2, 3]; alert( arr ); // 1,2,3 alert( String(arr) === '1,2,3' ); // true جرّب هذه، أيضًا: alert( [] + 1 ); // "1" alert( [1] + 1 ); // "11" alert( [1,2] + 1 ); // "1,21" ليس للمصفوفات Symbol.toPrimitive ولا دالة valueOf، بل تُنفّذ التحويل toString فقط لا غير. هكذا تصير [] سلسلة نصية فارغة، و[1] تصير "1" و[1,2] تصير "1,2". متى ما أضاف مُعامل الجمع الثنائي "+" شيئًا إلى السلسلة النصية، حوّله إلى سلسلة نصية هو الآخر. هكذا هي الخطوة التالية: alert( "" + 1 ); // "1" alert( "1" + 1 ); // "11" alert( "1,2" + 1 ); // "1,21" ملخص المصفوفات نوع خاصّ من الكائنات، وهي مخصّصة لتخزين البيانات عناصر مرتّبة، كما وإدارتها أيضًا. التصريح // الأقواس المعقوفة (طبيعية)‏ let arr = [item1, item2...]; // ‫new Array (نادرة جدًا) let arr = new Array(item1, item2...); باستدعاء new Array(number)‎ تُنشئ مصفوفة بالطول المحدّد، ولكن بلا أيّ عنصر. خاصية الطول length هي طول المصفوفة، أو للدّقة، آخر فهرس عددي زائدًا واحد. التوابِع المختلفة على المصفوفات تعدّل هذه الخاصية تلقائيًا. إن قصّرنا خاصية length يدويًا، فنحن نبتر المصفوفة حسب القيمة الجديدة. يمكننا استعمال المصفوفة كما الصفوف ذات الطرفين، بالعمليات الآتية: push(...items)‎: تُضيف items إلى النهاية. pop()‎: تُزيل العنصر من النهاية وتُعيده. shift()‎: تُزيل العنصر من البداية وتُعيده. unshift(...items)‎: تُضيف items إلى البداية. لتمرّ على عناصر المصفوفة: for (let i=0; i<arr.length; i++)‎ -- تتنفّذ بسرعة، ومتوافقة مع المتصفحات القديمة. for (let item of arr)‎ -- الصياغة الحديثة للعناصر فقط. for (let i in arr)‎ -- إيّاك واستعمالها. سنرجع إلى المصفوفات لاحقًا ونتعلّم توابِع أخرى لإضافة العناصر وإزالتها واستخراجها، كما وترتيب المصفوفات. هذا كله في الفصل التالي، توابع المصفوفات. تمارين هل تُنسخ المصفوفات؟ الأهمية: 3 ما ناتج هذه الشيفرة؟ let fruits = ["Apples", "Pear", "Orange"]; // ‫ادفع عنصرًا جديدًا داخل «النسخة» let shoppingCart = fruits; shoppingCart.push("Banana"); // ماذا في ‫fruits؟ alert( fruits.length ); // ? الحل الناتج هو 4: let fruits = ["Apples", "Pear", "Orange"]; let shoppingCart = fruits; shoppingCart.push("Banana"); alert( fruits.length ); // 4 هذا لأنّ المصفوفات كائنات. فكِلا shoppingCart وfruits يُشيران إلى نفس المصفوفة ذاتها. العمليات على المصفوفات الأهمية: 5 فلنجرّب خمس عمليات على المصفوفات. أنشِئ مصفوفة باسم styles تحوي العنصرين «Jazz» و«Blues». أضِف «Rock-n-Roll» إلى نهايتها. استبدِل القيمة في الوسط بالقيمة «Classics». يجب أن تعمل الشيفرة الذي ستكتبه ليجد القيمة في الوسط مع أيّ مصفوفة كانت لو كان طولها عدد فردي. أزِل القيمة الأولى من المصفوفة واعرضها. أضِف «Rap» و«Reggae» إلى بداية المصفوفة. المصفوفة خلال العمليات هذه: Jazz, Blues Jazz, Blues, Rock-n-Roll Jazz, Classics, Rock-n-Roll Classics, Rock-n-Roll Rap, Reggae, Classics, Rock-n-Roll الحل let styles = ["Jazz", "Blues"]; styles.push("Rock-n-Roll"); styles[Math.floor((styles.length - 1) / 2)] = "Classics"; alert( styles.shift() ); styles.unshift("Rap", "Reggae"); النداء داخل سياق المصفوفة الأهمية: 5 ما الناتج؟ لماذا؟ let arr = ["a", "b"]; arr.push(function() { alert( this ); }) arr[2](); // ? الحل من ناحية الصياغة، فالاستدعاء arr[2]()‎ هو نفسه النداء القديم obj[method]()‎، فبدل obj هناك arr، وبدل method هناك 2. إذًا فما أمامنا هو نداء الدالة arr[2]‎ وكأنّها تابِع لكائن. وبالطبيعة، فهي تستلم this الذي يُشير إلى الكائن arrوتكتب المصفوفة ناتجًا: let arr = ["a", "b"]; arr.push(function() { alert( this ); }) arr[2](); // "a","b",function للمصفوفة ثلاث قيم: الاثنتين من البداية، مع الدالة. جمع الأعداد المُدخلة الأهمية: 4 اكتب دالة sumInput()‎ تؤدّي الآتي: طلب القيم من المستخدم باستعمال prompt وتخزينها في مصفوفة. أن ينتهي الطلب لو أدخل المستخدم قيمة غير عددية، أو سلسلة نصية فارغة، أو ضغطَ «ألغِ». حساب مجموع عناصر المصفوفة وإعادتها. ملاحظة: الصفر 0 عدد مسموح، لذا لا تُوقف الطلب لو رأيته. الحل انتبه هنا على التفصيل الصغير في الحل، صغير ولكن مهمّ: لا يمكننا تحويل قيمة المتغير value إلى عدد مباشرةً بعد prompt، لأنّه بعدما نُجري value = +value، لن نفرّق بين السلسلة النصية الفارغة (أي علينا إيقاف الطلب) من الصفر (قيمة صالحة). عوض ذلك نؤجّل ذلك لما بعد. function sumInput() { let numbers = []; while (true) { let value = prompt("A number please?", 0); // هل نلغي الطلب؟ if (value === "" || value === null || !isFinite(value)) break; numbers.push(+value); } let sum = 0; for (let number of numbers) { sum += number; } return sum; } alert( sumInput() ); أكبر مصفوفة فرعية الأهمية: 2 البيانات المُدخلة هي مصفوفة من الأعداد، مثل arr = [1, -2, 3, 4, -9, 6]‎. والمهمة هي: البحث عن مصفوفة فرعية متتابعة في arr لها أكبر ناتج جمع. اكتب دالة getMaxSubSum(arr)‎ لتُعيد ذلك الناتج. مثال: getMaxSubSum([-1, 2, 3, -9]) = 5 (مجموع 2+3) getMaxSubSum([2, -1, 2, 3, -9]) = 6 (مجموع 2+(-1)+2+3) getMaxSubSum([-1, 2, 3, -9, 11]) = 11 ‫(وهكذا...) getMaxSubSum([-2, -1, 1, 2]) = 3 getMaxSubSum([100, -9, 2, -3, 5]) = 100 getMaxSubSum([1, 2, 3]) = 6 (نأخذها كلها) إن كانت القيم كلها سالبة فيعني هذا ألا نأخذ شيئا (المصفوفة الفرعية فارغة)، وبهذا يكون الناتج صفرًا: getMaxSubSum([-1, -2, -3]) = 0 يُحبّذ لو تفكّر -رجاءً- بحلّ سريع: O(n2) أو حتّى O(n) لو أمكنك. الحل النسخة البطيئة يمكننا حساب كلّ ناتج جمع فرعي ممكن. أبسط طريقة هي أخذ كلّ عنصر وحساب مجموع المصفوفات الفرعية بدءًا من مكان العنصر. فمثلًا إن كان لدينا ‎[-1, 2, 3, -9, 11]‎: // ‫نبدأ بِ‍ ‎-1: -1 -1 + 2 -1 + 2 + 3 -1 + 2 + 3 + (-9) -1 + 2 + 3 + (-9) + 11 // ‫نبدأ بِ‍ 2: 2 2 + 3 2 + 3 + (-9) 2 + 3 + (-9) + 11 // نبدأ بِ‍‏‫ 3: 3 3 + (-9) 3 + (-9) + 11 // نبدأ بِ‍‏‫ ‎-9: -9 -9 + 11 // نبدأ بِ‍‏‫ 11: 11 في الواقع فالشيفرة هي حلقات متداخلة، تمرّ الحلقة العلوية على عناصر المصفوفة، والسفلية تعدّ النواتج الفرعية بدءًا من العنصر الحالي. function getMaxSubSum(arr) { let maxSum = 0; // إن لم نأخذ أيّ عنصر، فسنُرجع الصفر 0 for (let i = 0; i < arr.length; i++) { let sumFixedStart = 0; for (let j = i; j < arr.length; j++) { sumFixedStart += arr[j]; maxSum = Math.max(maxSum, sumFixedStart); } } return maxSum; } alert( getMaxSubSum([-1, 2, 3, -9]) ); // 5 alert( getMaxSubSum([-1, 2, 3, -9, 11]) ); // 11 alert( getMaxSubSum([-2, -1, 1, 2]) ); // 3 alert( getMaxSubSum([1, 2, 3]) ); // 6 alert( getMaxSubSum([100, -9, 2, -3, 5]) ); // 100 مدى التعقيد الحسابي لهذا الحل هو O(n2). أي بعبارة أخرى، لو زِدت حجم المصفوفة مرتين اثنتين، فسيزيد وقت عمل الخوارزمية أربع مرات أكثر. يمكن أن تؤدّي هذه الخوازرميات للمصفوفات الكبيرة (نتحدّث عن 1000 و10000 وأكثر) إلى بطء شديد في التنفيذ. النسخة السريعة لنمرّ على عناصر المصفوفة ونحفظ ناتج جمع العناصر الحالي في المتغير s. متى ما صار s سالبًا، نعيّنه صفرًا s=0. إجابتنا على هذا هي أكبر قيمة من هذا المتغير s. لو لم يكن هذا الوصف منطقيًا، فيمكنك مطالعة الشيفرة، قصيرة للغاية: function getMaxSubSum(arr) { let maxSum = 0; let partialSum = 0; for (let item of arr) { // لكلّ ‫item في arr partialSum += item; // نُضيفه إلى ‫partialSum maxSum = Math.max(maxSum, partialSum); // نتذكّر أكبر قيمة if (partialSum < 0) partialSum = 0; // لو كانت سالبة فالصفر } return maxSum; } alert( getMaxSubSum([-1, 2, 3, -9]) ); // 5 alert( getMaxSubSum([-1, 2, 3, -9, 11]) ); // 11 alert( getMaxSubSum([-2, -1, 1, 2]) ); // 3 alert( getMaxSubSum([100, -9, 2, -3, 5]) ); // 100 alert( getMaxSubSum([1, 2, 3]) ); // 6 alert( getMaxSubSum([-1, -2, -3]) ); // 0 على الخوارزمية هنا أن تمرّ مرورًا واحدًا فقط على المصفوفة، أي أن التعقيد الحسابي هو O(n)‎. يمكنك أن تجد معلومات مفصّلة أكثر عن الخوارزمية هنا: Maximum subarray problem. لو لم يكن هذا واضحًا بعد، فالأفضل لو تتعقّب ما تفعل الخوارزمية في الأمثلة أعلاه، وترى ما تفعله من حسابات. «التعقّب يغني عن ألف كلمة»… ربّما. ترجمة -وبتصرف- للفصل Arrays من كتاب The JavaScript language .task__importance { color: #999; margin-left: 30px; } .task__answer { border: 3px solid #f7f6ea; margin: 20px 0 14px; position: relative; display: block; padding: 25px 30px; } اقرأ أيضًا المقال التالي: توابع المصفوفات (Array methods) المقال السابق: السلاسل النصية (strings)
  25. تُخَزَّن النصوص في JavaScript كسلاسل نصية أي سلاسل من المحارف (string of charecter). لا يوجد نوع بيانات مستقل للحرف الواحد (char). الصيغة الداخلية للنصوص هي UTF-16 دائمًا، ولا تكون مرتبطة بتشفير الصفحة. علامات الاقتباس "" لنراجع أنواع علامات التنصيص (الاقتباس). يمكن تضمين النصوص إما في علامات الاقتباس الأحادية، أو الثنائية أو الفاصلة العليا المائلة: let single = 'single-quoted'; let double = "double-quoted"; let backticks = `backticks`; علامات التنصيص الفردية والثنائية تكون متماثلة. أما الفاصلة العليا المائلة، فَتُتيح لنا تضمين أي تعبير في السلسلة النصية، عبر تضمينها في ‎${…}‎: function sum(a, b) { return a + b; } alert(`1 + 2 = ${sum(1, 2)}.`); // 1 + 2 = 3. الميزة الأخرى لاستخدام الفاصلة العلوية المائلة هي إمكانية فصل السلسلة النصية إلى عدة أسطر: let guestList = `Guests: * John * Pete * Mary `; // قائمة بالضيوف في أسطر منفصلة alert(guestList); يبدو الأمر طبيعيًا أليس كذلك؟ لكن علامات التنصيص الفردية والثنائية لا تعمل بهذه الطريقة. إن حاولنا استخدامها في نص متعدد الأسطر، سنحصل على خطأ: let guestList = "Guests: // خطأ: رمز غير متوقع * John"; أتى استخدام علامات الاقتباس الفردية والثنائية في أوقات مبكرة من إنشاء اللغة، عندما لم يُؤخَذ بالحسبان الحاجة إلى نص متعدد الأسطر. ظهرت الفاصلة العلوية المائلة مؤخرًا ولذا فإنها متعددة الاستعمالات. تتيح لنا أيضا الفاصلة العلوية المائلة تحديد "دالة كنموذج" قبل الفاصلة العلوية المائلة الأولى. تكون الصيغة كما يلي: func`string`‎. تُستَدعى الدالة func تلقائيًا، وتستقبل النص والتعابير المُضَمَّنة وتعالجها. يسمى هذا ب "القوالب الملحقة". تجعل هذه الميزة من السهل تضمين قوالب مخصصة، لكنها تستخدم بشكل نادر عمليًا. يمكنك قراءة المزيد عنها في هذا الدليل. الرموز الخاصة ما زال بالإمكان كتابة نصوص متعددة الأسطر باستخدام علامات الاقتباس الأحادية والثنائية باستخدام ما يسمى ب "رمز السطر الجديد"، والذي يُكتَب ‎\n، ويرمز لسطر جديد: let guestList = "Guests:\n * John\n * Pete\n * Mary"; alert(guestList); // قائمة متعددة الأسطر بالضيوف مثلًا، السطرين التاليين متماثلان، لكنهما مكتوبين بطريقة مختلفة: // سطران باستخدام رمز السطر الجديد let str1 = "Hello\nWorld"; // سطران باستخدام سطر جديد عادي والفواصل العليا المائلة let str2 = `Hello World`; alert(str1 == str2); // true يوجد رموز خاصة أخرى أقل انتشارًا. هذه القائمة كاملة: المحرف الوصف ‎\n محرف السطر الجديد (Line Feed). ‎\r محرف العودة إلى بداية السطر (Carriage Return)، ولا يستخدم بمفرده. تستخدم ملفات ويندوز النصية تركيبة من رمزين ‎\r\n لتمثيل سطر جديد. ‎'\ , "\ علامة اقتباس مزدوجة ومفردة. \\ شرطة مائلة خلفية ‎\t مسافة جدولة "Tab" ‎\b, \f, \v فراغ خلفي (backspace)، محرف الانتقال إلى صفحة جديد (Form Feed)، مسافة جدولة أفقية (Vertical Tab) على التوالي – تُستعمَل للتوافق، ولم تعد مستخدمة. ‎\xXX صيغة رمز يونيكود مع عدد ست عشري مُعطى XX، مثال: '‎ \x7A' هي نفسها 'z'. ‎\uXXXX صيغة رمز يونيكود مع عدد ست عشرية XXXX في تشفير UTF-16، مثلًا، ‎\u00A9 – هو اليونيكود لرمز حقوق النسخ ©. يجب أن يكون مكون من 6 خانات ست عشرية. ‎\u{X…XXXXXX}‎ (1 إلى 6 أحرف ست عشرية) رمز يونيكود مع تشفير UTF-32 المعطى. تُشَفَّر بعض الرموز الخاصة برمزي يونيكود، فتأخذ 4 بايت. هكذا يمكننا إدخال شيفرات طويلة. 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; } أمثلة باستخدام حروف يونيكود: alert( "\u00A9" ); // © // (رمز نادر من الهيروغليفية الصينية (يونيكود طويل alert( "\u{20331}" ); // 佫 // (رمز وجه مبتسم (يونيكود طويل آخر alert( "\u{1F60D}" ); // ? لاحظ بدء جميع الرموز الخاصة بشرطة مائلة خلفية \. تدعى أيضا ب "محرف التهريب" (escape character). يمكننا استخدامها أيضًا إن أردنا تضمين علامة اقتباس في النص: مثلًا: alert( 'I\'m the Walrus!' ); // I'm the Walrus! يجب إلحاق علامة الاقتباس الداخلية بالشرطة المائلة الخلفية ‎\'‎، وإلا فستُعتَبر نهاية السلسلة النصية. لاحظ أن الشرطة المائلة الخلفية \ تعمل من أجل تصحيح قراءة السلسلة النصية بواسطة JavaScript. ومن ثم تختفي، لذا فإن النص في الذاكرة لا يحتوي على \. يمكننا رؤية ذلك بوضوح باستخدام alert على المثال السابق. يجب استخدام محرف التهريب في حالة استخدام علامة الاقتباس المحيطة بالنص نفسها، لذا فإن الحل الأمثل هو استخدام علامات اقتباس مزدوجة أو فواصل عليا مائلة في مثل هذه الحالة: alert( `I'm the Walrus!` ); // I'm the Walrus! لكن ماذا إن أردنا عرض شرطة مائلة خلفية ضمن النص؟ يمكن ذلك، لكننا نحتاج إلى تكرارها هكذا \\: alert( `The backslash: \\` ); // The backslash: \ طول النص تحمل الخاصية length طول النص: alert( `My\n`.length ); // 3 لاحظ أن n\ هو رمز خاص، لذا يكون طول السلسلة الفعلي هو 3. length هي خاصية يُخطِئ بعض الأشخاص ذوي الخلفيات بلغات برمجية أخرى و يستدعون str.length()‎ بدلًا من استدعاء str.length فقط. لذا لا يعمل هذا التابع لعدم وجوده. فلاحظ أن str.length هي خاصية عددية، وليس تابعًا ولا حاجة لوضع قوسين بعدها. الوصول إلى محارف سلسلة للحصول على حرف في مكان معين من السلسلة النصية pos، استخدم الأقواس المعقوفة [pos] أو استدعِ التابع str.charAt(pos)‎. يبدأ أول حرف في الموضع رقم صفر: let str = `Hello`; // الحرف الأول alert( str[0] ); // H alert( str.charAt(0) ); // H // الحرف الأخير alert( str[str.length - 1] ); // o الأقواس المعقوفة هي طريقة جديدة للحصول على حرف، بينما التابع charAt موجود لأسباب تاريخية. الاختلاف الوحيد بينهما هو إن لم تجد الأقواس المربعة [] الحرف تُرجِع القيمة undefined بينما يُرجِع charAt نصًا فارغًا: let str = `Hello`; alert( str[1000] ); // undefined alert( str.charAt(1000) ); // '' (سلسلة نصية فارغ) يمكننا أيضا التنقل خلال جميع محارف سلسلة باستخدام for..of: for (let char of "Hello") { alert(char); // H,e,l,l,o } النصوص ثابتة لا يمكن تغيير النصوص في JavaScript، فمن المستحيل تغيير حرف داخل سلسلة نصية فقط. لنجرب الأمر للتأكد من أنه لن يعمل: let str = 'Hi'; str[0] = 'h'; // خطأ alert( str[0] ); // لا تعمل الطريقة المعتادة هي إنشاء نص جديد وإسناده للمتغير str بدلًا من النص السابق. مثلًا: let str = 'Hi'; str = 'h' + str[1]; // تستبدل كامل السلسلة النصية alert( str ); // hi سنرى المزيد من الأمثلة عن ذلك في الأجزاء التالية. تغيير حالة الأحرف الأجنبية يقوم التابع toLowerCase()‎ والتابع toUpperCase()‎ بِتغيير حالة الأحرف الأجنبية: alert( 'Interface'.toUpperCase() ); // INTERFACE alert( 'Interface'.toLowerCase() ); // interface أو إن أردنا بتغيير حالة حرف واحد فقط: alert( 'Interface'[0].toLowerCase() ); // 'i' البحث عن جزء من النص يوجد العديد من الطرق للبحث عن جزء من النص ضمن السلسلة النصية. str.indexOf التابع الأول هو str.indexOf(substr, pos)‎. يبحث التابع عن substr في str بدءًا من الموضع المحدد pos، ثم يُرجِع الموضع الذي تطابق مع النص أو يُرجِع ‎ -1 إن لم تعثر على تطابق. مثلًا: let str = 'Widget with id'; alert( str.indexOf('Widget') ); // 0 alert( str.indexOf('widget') ); // -1 alert( str.indexOf("id") ); // 1 لم تعثر على شيء في حالة البحث الثانية، إذ البحث هنا حساس لحالة الأحرف. يتيح لنا المُعامِل الثاني الاختياري البحث من الموضع المُعطَى. مثلًا في الحالة الثالثة، أول ظهور ل "id" هو في الموضع 1. لِلبحث عن الظهور التالي له نبدأ البحث من الموضع 2: let str = 'Widget with id'; alert( str.indexOf('id', 2) ) // 12 إن كنت مهتمًا بجميع المواضع التي يظهر فيها نص معين، يمكنك استخدام indexOf في حلقة. يتم كل استدعاء جديد من الموضِع التالي لِلموضع السابق الذي تطابق مع النص: let str = 'As sly as a fox, as strong as an ox'; let target = 'as'; // لنبحث عنها let pos = 0; while (true) { let foundPos = str.indexOf(target, pos); if (foundPos == -1) break; alert( `Found at ${foundPos}` ); pos = foundPos + 1; // استمر بالبحث من الموضع التالي } يمكن تقصير الخوارزمية: let str = "As sly as a fox, as strong as an ox"; let target = "as"; let pos = -1; while ((pos = str.indexOf(target, pos + 1)) != -1) { alert( pos ); } str.lastIndexOf(substr, position)‎ يوجد أيضًا تابع مشابه str.lastIndexOf(substr, position)‎ والذي يبدأ البحث من نهاية السلسلة النصية حتى بدايتها. أي أنه يعيد موضع ظهور النص المبحوث عنه انطلاقًا من نهاية السلسلة. يوجد خلل طفيف عند استخدام indexOf في if. فلا يمكن وضعها بداخل if بالطريقة التالية: let str = "Widget with id"; if (str.indexOf("Widget")) { alert("We found it"); // لا تعمل! } لا يتحقق الشرط في المثال السابق لأن str.indexOf("Widget")‎ يُرجِع 0 (ما يعني وجود تطابق في الموضع الأول) رغم عثور التابع على الكلمة، لكن if تعد القيمة 0 على أنها false. لذا يجب أن نفحص عدم وجود القيمة -‎ 1 هكذا: let str = "Widget with id"; if (str.indexOf("Widget") != -1) { alert("We found it"); // تعمل الآن } خدعة NOT على مستوى البِت إحدى الخدع القديمة هي العامل الثنائي NOT ~ الذي تعمل على مستوى البِت. فهو يُحَوِّل العدد إلى عدد صحيح بصيغة 32-بِت (يحذف الجزء العشري إن وجد) ثم يُحوِّل جميع البتات إلى تمثيلها الثنائي. عمليًا، يعني ذلك شيئًا بسيطًا: بالنسبة للأعداد الصحيحة بصيغة 32-بِت ‎~n تساوي ‎-(n+1)‎. مثلًا: alert( ~2 ); // -3 == -(2+1) alert( ~1 ); // -2 == -(1+1) alert( ~0 ); // -1 == -(0+1) alert( ~-1 ); // 0 == -(-1+1) كما نرى، يكون ‎~‎n صفرًا فقط عندما تكون n == -1 (وذلك لأي عدد صحيح n ذي إشارة). لذا، يكون ناتج الفحص if ( ~str.indexOf("...") )‎ صحيحًا إذا كانت نتيجة indexOf لا تساوي ‎-1. بمعنى آخر تكون القيمة true إذا وُجِد تطابق. الآن، يمكن استخدام هذه الحيلة لتقصير الفحص باستخدام indexOf: let str = "Widget"; if (~str.indexOf("Widget")) { alert( 'Found it!' ); // تعمل } لا يكون من المستحسن غالبًا استخدام ميزات اللغة بطريقة غير واضحة، لكن هذه الحيلة تُستخدم بكثرة في الشيفرات القديمة، لذا يجب أن نفهمها. تذكر أن الشرط if (~str.indexOf(...))‎ يعمل بالصيغة «إن وُجِد». حتى نكون دقيقين، عندما تُحَوَّل الأرقام إلى صيغة 32-بِت باستخدام المعامل ~ يوجد أعداد أخرى تُعطي القيمة 0، أصغر هذه الأعداد هي ‎~4294967295 == 0. ما يجعل هذا الفحص صحيحًا في حال النصوص القصيرة فقط. لا نجد هذه الخدعة حاليًا سوى في الشيفرات القديمة، وذلك لأن JavaScript وفرت التابع ‎.includes (ستجدها في الأسفل). includes, startsWith, endsWith يُرجِع التابع الأحدث str.includes(substr, pos)‎ القيمة المنطقية true أو false وفقًا لما إن كانت السلسلة النصية str تحتوي على السلسلة النصية الفرعية substr. هذه هي الطريقة الصحيحة في حال أردنا التأكد من وجود تطابق جزء من سلسلة ضمن سلسلة أخرى، ولا يهمنا موضعه: alert( "Widget with id".includes("Widget") ); // true alert( "Hello".includes("Bye") ); // false المُعامِل الثاني الاختياري للتابع str.includes هو الموضع المراد بدء البحث منه: alert( "Widget".includes("id") ); // true alert( "Widget".includes("id", 3) ); // false يعمل التابعان str.startsWith و str.endsWith بما هو واضح من مسمياتهما، "سلسلة نصية تبدأ بـ"، و "سلسلة نصية تنتهي بـ" على التوالي: alert( "Widget".startsWith("Wid") ); // true, alert( "Widget".endsWith("get") ); // true, جلب جزء من نص يوجد 3 توابع في JavaScript لجلب جزء من سلسلة نصية هي: substring، وsubstr، وslice. str.slice(start [, end])‎ يُرجِع جزءًا من النص بدءًا من الموضع start وحتى الموضع end (بما لا يتضمن end). مثلًا: let str = "stringify"; alert( str.slice(0, 5) ); // 'strin' alert( str.slice(0, 1) ); // 's' إن لم يكن هناك مُعامل ثانٍ، فسيقتطع التابعslice الجزء المحدد من الموضع start وحتى نهاية النص: let str = "stringify"; alert( str.slice(2) ); // ringify يمكن أيضًا استخدام عدد سالبًا مع start أو end، وذلك يعني أن الموضع يُحسَب بدءًا من نهاية السلسلة النصية: let str = "stringify"; // تبدأ من الموضع الرابع من اليمين، إلى الموضع الأول من اليمين alert( str.slice(-4, -1) ); // gif str.substring(start [, end])‎ يُرجِع هذا التابع جزءًا من النص الواقع بين الموضع start والموضع end. يشبه هذا التابع تقريبًا التابع slice، لكنه يسمح بكون المعامل start أكبر من end. مثلًا: let str = "stringify"; // substring الأمرين التاليين متماثلين بالنسبة لـ alert( str.substring(2, 6) ); // "ring" alert( str.substring(6, 2) ); // "ring" // slice لكن ليس مع alert( str.slice(2, 6) ); // "ring" (نفس النتيجة السابقة) alert( str.slice(6, 2) ); // "" (نص فارغ) بعكس slice، القيم السالبة غير مدعومة ضمن المعاملات، وتقيَّم إلى 0 إن مُرِّرت إليه. str.substr(start [, length])‎ يُرجِع هذا التابع الجزء المطلوب من النص، بدءًا من start وبالطول length المُعطى. بعكس التوابع السابقة، يتيح لنا هذا التابع تحديد طول النص المطلوب بدلًا من موضع نهايته: let str = "stringify"; // خذ 4 أحرف من الموضع 2 alert( str.substr(2, 4) ); // ring يمكن أن يكون المُعامِل الأول سالبًا لتحديد الموضع بدءًا من النهاية: let str = "stringify"; // حرفين ابتداءًا من الموضع الرابع alert( str.substr(-4, 2) ); // gi التابع يقتطع ... المواضع السالبة slice(start, end)‎ من الموضع start إلى الموضع end (بما لا يتضمن end) مسموحة لكلا المعاملين substring(start, end)‎ ما بين الموضع start والموضع end غير مسموحة وتصبح 0 substr(start, length)‎ أرجع الأحرف بطول length بدءًا من start مسموحة للمعامل start أيها تختار؟ يمكن لجميع التوابع تنفيذ الغرض المطلوب. لدى التابع substr قصور بسيط رسميًا: فهو غير ذكورة في توثيق JavaScript الرسمي، بل في Annex B والذي يغطي ميزات مدعومة في المتصفحات فقط لأسباب تاريخية، لذا فإن أي بيئة لا تعمل على المتصفح ستفشل في دعم هذا التابع، لكنه يعمل عمليًا في كل مكان. ما بين الخيارين الآخرين، slice هو أكثر مرونة، فهو يسمح بتمرير مُعامِلات سالبة كما أنه أقصر في الكتابة. لذا، من الكافِ تذكر slice فقط من هذه التوابع الثلاث. موازنة النصوص توازن السلاسل النصية حرفًا حرفًا بترتيب أبجدي كما عرفنا في فصل معاملات الموازنة. بالرغم من ذلك، يوجد بعض الحالات الشاذة. 1- الحرف الأجنبي الصغير دائما أكبر من الحرف الكبير: alert( 'a' > 'Z' ); // true 2- الأحرف المُشَكَلَة خارج النطاق: alert( 'Österreich' > 'Zealand' ); // true alert( 'سوريا' < 'تُونس' ); // false قد يقود ذلك إلى نتائج غريبة إن رتبنا مثلًا بين أسماء بلدان، فيتوقع الناس دائمًا أن Zealand تأتي بعد Österreich في القائمة وأن تونس تأتي قبل سوريا وهكذا. لفهم ما يحدث، لنراجع تمثيل النصوص الداخلي في JavaScript. جميع النصوص مشفرة باستخدام UTF-16. يعني أن: لكل حرف رمز عددي مقابل له. يوجد دوال خاصة تسمح بالحصول على الحرف من رمزه والعكس. str.codePointAt(pos)‎ يُرجِع هذا التابع الرمز العددي الخاص بالحرف المعطى في الموضع pos: // لدى الأحرف المختلفة في الحالة رموز مختلفة alert( "z".codePointAt(0) ); // 122 alert( "Z".codePointAt(0) ); // 90 String.fromCodePoint(code)‎ يُنشِئ حرفًا من رمزه العددي code: alert( String.fromCodePoint(90) ); // Z يمكننا إضافة حرف يونيكود باستخدام رمزه بواسطة ‎\u متبوعة بالرمز الست عشري: alert( '\u005a' ); // Z يُمثَّل العدد العشري 90 بالعدد 5a في النظام الست عشري. لنرَ الآن الأحرف ذات الرموز 65..220 (الأحرف اللاتينية وأشياء إضافية) عبر إنشاء نصوص منها: let str = ''; for (let i = 65; i <= 220; i++) { str += String.fromCodePoint(i); } alert( str ); // ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~������ // ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁ ÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜ تبدأ الأحرف الكبيرة كما ترى، ثم أحرف خاصة، ثم الأحرف الصغيرة، ثم Ö بالقرب من نهاية المخرجات. يصبح الآن واضحًا لم a > Z. أي توازن الأحرف بواسطة قيمها العددية. فالرمز العددي الأكبر يعني أن الحرف أكبر. الرمز للحرف a هو 97‎ وهو أكبر من الرمز العددي للحرف Z الذي هو 90. تأتي الأحرف الصغيرة بعد الأحرف الكبيرة دائمًا لأن رموزها العددية دائمًا أكبر. تكون بعض الأحرف مثل Ö بعيدة عن الأحرف الهجائية. هنا، قيمة الحرف هذا أكبر من أي حرف بين a و z. موازنات صحيحة الخوارزمية الصحيحة لموازنة النصوص أكثر تعقيدًا مما يبدو عليه الأمر، لأن الأحرف تختلف باختلاف اللغات. لذا، يحتاج المتصفح لمعرفة اللغة لموازنة نصوصها موازنةً صحيحة. لحسن الحظ، تدعم جميع المتصفحات الحديثة المعيار العالمي ECMA 402(IE10- الذي يتطلب المكتبة الاضافية Intl.JS)، إذ يوفر تابعًا خاصًا لموازنة النصوص بلغات متعددة، وفقًا لقواعدها. يُرجِع استدعاء التابع str.localeCompare(str2)‎ عددًا يحدد ما إن كان النص str أصغر، أو يساوي، أو أكبر من النص str2 وفقًا لقواعد اللغة المحلية: يُرجِع قيمة سالبة إن كان str أصغر من str2. يُرجِع قيمة موجبة إن كان str أكبر من str2. يُرجِع 0 إن كانا متساويين. إليك المثال التالي: alert( 'Österreich'.localeCompare('Zealand') ); // -1 في الحقيقة، لهذه الدالة مُعامِلين إضافيين كما في توثيقها على MDN، إذ يسمح هذان المُعاملان بتحديد اللغة (تؤخذ من بيئة العمل تلقائيًا، ويعتمد ترتيب الأحرف على اللغة) بالإضافة إلى إعداد قواعد أخرى مثل الحساسية تجاه حالة الأحرف، أو ما إن كان يجب معاملة "a" و "á" بالطريقة نفسها …الخ. ما خلف الستار، يونيكود معلومات متقدمة يتعمق الجزء التالي في ما يقبع خلف ستار النصوص التي تراها، وهذه المعلومات ستكون قيمة إن كنت تخطط للتعامل مع الرموز التعبيرية، أو الأحرف الرياضية النادرة أو الهيروغليفية أو أي رموز نادرة أخرى. يمكنك تخطي هذا الجزء إن لم تكن مهتمًا به. أزواج بديلة (Surrogate pairs) لكل الأحرف المستخدمة بكثرة رموز عددية (code) مؤلفة من 2-بايت. لدى أحرف اللغات الأوروبية، والأرقام، وحتى معظم الرموز الهيروغليفية تمثيل من 2-بايت. لكن، نحصل من 2-بايت 65536 على تركيبًا فقط وذلك غير كافٍ لكل الرموز (symbol) المُحتَمَلَة، لذا فإن الرموز (symbol) النادرة مرمزة بزوج من المحارف بحجم 2-بايت يسمى "أزواج بديلة" (Surrogate pairs). طول كل رمز هو 2: // في الرياضيات X الحرف alert( '?'.length ); // 2 // وجه ضاحك بدموع alert( '?'.length ); // 2 // حرف صيني هيروغليفي نادر alert( '?'.length ); // 2 لاحظ أن الأزواج البديلة لم تكن موجودة منذ إنشاء JavaScript، ولذا لا تعالج بشكل صحيح بواسطة اللغة. في النصوص السابقة لدينا رمز واحد فقط، لكن طول النص length ظهر على أنه 2. التابعان String.fromCodePoint و str.codePointAt نادران وقليلا الاستخدام، إذ يتعاملان مع الأزواج البديلة بصحة. وقد ظهرت مؤخرًا في اللغة. في السابق كان هنالك التابعان String.fromCharCode و str.charCodeAt فقط. هذان التابعان يشبهان fromCodePoint و codePointAt، لكنهما لا يتعاملان مع الأزواج البديلة. قد يكون الحصول على رمز (symbol) واحد صعبًا، لأن الأزواج البديلة تُعامَل معاملة حرفين: alert( '?'[0] ); // رموز غريبة alert( '?'[1] ); // أجزاء من الزوج البديل لاحظ أن أجزاء الزوج البديل لا تحمل أي معنى إذا كانت منفصلة عن بعضها البعض. لذا فإن ما يعرضه مر alert في الأعلى هو شيء غير مفيد. يمكن تَوَقُّع الأزواج البديلة عمليًا بواسطة رموزها: إن كان الرمز العددي لحرف يقع في المدى 0xd800..0xdbff، فإنه الجزء الأول من الزوج البديل. أما الجزء الثاني فيجب أن يكون في المدى 0xdc00..0xdfff. هذا المدى محجوز للأزواج البديلة وفقًا للمعايير المتبعة. وفقًا للحالة السابقة، سنستعمل التابع charCodeAt الذي لا يتعامل مع الأزواج البديلة، لذا فإنه يُرجِع أجزاء الرمز: alert( '?'.charCodeAt(0).toString(16) ); // d835، ما بين 0xd800 و 0xdbff alert( '?'.charCodeAt(1).toString(16) ); // dcb3، ما بين 0xdc00 و 0xdfff نجد أن العدد الست عشري الأول d835 يقع بين 0xd800 و 0xdbff، والعدد الست عشري الثاني يقع بين 0xdc00 و 0xdfff وهذا يؤكد أنها من الأزواج البديلة. ستجد المزيد من الطرق للتعامل مع الأزواج البديلة لاحقًا في الفصل Iterables. يوجد أيضًا مكاتب خاصة لذلك، لكن لا يوجد شيء شهير محدد لِاقتراحه هنا. علامات التشكيل وتوحيد الترميز يوجد حروف مركبة في الكثير من اللغات والتي تتكون من الحرف الرئيسي مع علامة فوقه/تحته. مثلًا، يمكن للحرف a أن يكون أساسًا للأحرف التالية: àáâäãåā. لدى معظم الحروف المركبة رمزها الخاص بها في جدول UTF-16. لكن ليس جميعها، وذلك لوجود الكثير من الاحتمالات. لدعم التراكيب الأساسية، تتيح لنا UTF-16 استخدام العديد من حروف يونيكود: الحرف الرئيسي متبوعًا بعلامة أو أكثر لتشكيله. مثلًا، إن كان لدينا S متبوعًا بالرمز الخاص "النقطة العلوية" (التي رمزها ‎ \u0307). فسيُعرَض ك Ṡ. alert( 'S\u0307' ); // Ṡ إن احتجنا إلى رمز آخر فوق أو تحت الحرف فلا مشكلة، أضِف العلامة المطلوبة فقط. مثلًا، إن ألحقنا حرف "نقطة بالأسفل" (رمزها ‎ \u0323)، فسنحصل على "S بنقاط فوقه وتحته"، Ṩ: alert( 'S\u0307\u0323' ); // Ṩ هذا يوفر مرونة كبيرة، لكن مشكلة كبيرة أيضًا: قد يظهر حرفان بالشكل ذاته، لكن يمثلان بتراكيب يونيكود مختلفة. مثلًا: // S + نقطة في الأعلى + نقطة في الأسفل let s1 = 'S\u0307\u0323'; // Ṩ // S + نقطة في الأسفل + نقطة في الأعلى let s2 = 'S\u0323\u0307'; // Ṩ, alert( `s1: ${s1}, s2: ${s2}` ); alert( s1 == s2 ); // خطأ بالرغم من أن الحرفين متساويان ظاهريًا لحل ذلك، يوجد خوارزمية تدعى "توحيد ترميز اليونيكود" (unicode normalization) والتي تُعيد كل نص إلى الصيغة الطبيعية المستقلة له. هذه الخوارزمية مُضَمَّنة في التابع str.normalize()‎. alert( "S\u0307\u0323".normalize() == "S\u0323\u0307".normalize() ); // true من المضحك في حالتنا أن normalize()‎ تجمع سلسلة من 3 أحرف مع بعضها بعضًا إلى حرف واحد: ‎ \u1e68 (الحرف S مع النقطتين). alert( "S\u0307\u0323".normalize().length ); // 1 alert( "S\u0307\u0323".normalize() == "\u1e68" ); // true في الواقع، هذه ليست الحالة دائمًا. وذلك لأن الرمز Ṩ متعارف بكثرة، فضَمَّنّهُ مُنشِئوا UTF-16 في الجدول الرئيسي وأعطوه رمزًا خاصًا. إن أردت تعلم المزيد عن قواعد التوحيد واختلافاتها - فستجدها في ملحق معايير اليونيكود: نماذج توحيد ترميز اليونيكود، لكن للأغراض العملية المتعارفة فالمعلومات السابقة تفي بالغرض. الخلاصة يوجد 3 أنواع لِعلامات الاقتباس. تسمح الشرطات العلوية المائلة للنص بالتوسع لأكثر من سطر وتضمين التعبير ‎${…}‎. النصوص في JavaScript مُشَفَّرة بواسطة UTF-16. يمكننا استخدام أحرف خاصة مثل ‎ \n وإدخال أحرف باستخدام رمز يونيكود الخاص بها باستخدام ‎\u...‎. استخدم [] للحصول على حرف ضمن سلسلة نصية. للحصول على جزء من النص، استخدم: slice أو substring. للتحويل من أحرف كبيرة/صغيرة، استخدم: toLowerCase أو toUpperCase. للبحث عن جزء من النص، استخدم: indexOf، أو includes أو startsWith أو endsWith للفحص البسيط. لموازنة النصوص وفقًا للغة، استخدم: localeCompare، وإلا فستوازن برموز الحروف. يوجد الكثير من التوابع الأخرى المفيدة في النصوص: str.trim()‎ تحذف ("تقتطع") المسافات الفارغة من بداية ونهاية النص. str.repeat(n)‎ تُكرِّر النص n مرة. والمزيد، يمكن الاطلاع عليها في موسوعة حسوب. هنالك توابع أخرى للنصوص أيضًا تعمل على البحث/الاستبدال مع التعابير النمطية (regular expressions). لكن ذلك موضوع كبير، لذا فقد شُرِحَ في فصل مستقل، التعابير النمطية. تمارين حول الحرف الأول إلى حرف كبير الأهمية: 5 اكتب دالة باسم ucFirst(str)‎ تُرجِع النص str مع تكبير أول حرف فيه، مثلًا: ucFirst("john") == "John"; الحل لا يمكننا استبدال الحرف الأول، لأن النصوص في JavaScript غير قابلة للتعديل. لكن، يمكننا إنشاء نص جديد وفقًا للنص الموجود، مع تكبير الحرف الأول: let newStr = str[0].toUpperCase() + str.slice(1) لكن، يوجد مشكلة صغيرة، وهي إن كان str فارغًا، فسيصبح str[0]‎ قيمة غير معرفة undefined، ولأن undefined لا يملك الدالة toUpperCase()‎ فسيظهر خطأ. يوجد طريقتين بديلتين هنا: 1- استخدام str.charAt(0)‎، لأنها تُرجِع نصًا دائمًا (ربما نصًا فارغًا). 2- إضافة اختبار في حال كان النص فارغًا. هنا الخيار الثاني: function ucFirst(str) { if (!str) return str; return str[0].toUpperCase() + str.slice(1); } alert( ucFirst("john") ); // John فحص وجود شيء مزعج الأهمية: 5 اكتب دالة باسم checkSpam(str)‎ تُرجِع true إن كان str يحوي 'viagra' أو 'XXX'، وإلا فتُرجِع false. يجب أن لا تكون الدالة حساسة لحالة الأحرف: checkSpam('buy ViAgRA now') == true checkSpam('free xxxxx') == true checkSpam("innocent rabbit") == false الحل لجعل البحث غير حساس لحالة الأحرف، نحوِّل النص إلى أحرف صغيرة ومن ثم نبحث فيه على النص المطلوب: function checkSpam(str) { let lowerStr = str.toLowerCase(); return lowerStr.includes('viagra') || lowerStr.includes('xxx'); } alert( checkSpam('buy ViAgRA now') ); alert( checkSpam('free xxxxx') ); alert( checkSpam("innocent rabbit") ); قص النص الأهمية: 5 انشئ دالة باسم truncate(str, maxlength)‎ تفحص طول النص str وتستبدل نهايته التي تتجاوز الحد maxlength بالرمز "…" لجعل طولها يساوي maxlength بالضبط. يجب أن تكون مخرجات الدالة النص المقصوص (في حال حدث ذلك). مثلًا: truncate("What I'd like to tell on this topic is:", 20) = "What I'd like to te…" truncate("Hi everyone!", 20) = "Hi everyone!" الحل الطول الكلي هو maxlength، لذا فإننا نحتاج لقص النص إلى أقصر من ذلك بقليل لإعطاء مساحة للنقط "…". لاحظ أن هناك حرف يونيكود واحد للحرف "…". وليست ثلاث نقاط. function truncate(str, maxlength) { return (str.length > maxlength) ? str.slice(0, maxlength - 1) + '…' : str; } استخراج المال الأهمية: 4 لدينا قيمة بالشكل "‎ $120"، إذ علامة الدولار تأتي أولًا ومن ثم العدد. أنشِئ دالة باسم extractCurrencyValue(str)‎ تستخرج القيمة العددية من نصوص مشابهة وإرجاعها. مثال: alert( extractCurrencyValue('$120') === 120 ); // true الحل function extractCurrencyValue(str) { return +str.slice(1); } ترجمة -وبتصرف- للفصل Strings من كتاب The JavaScript Language .task__importance { color: #999; margin-left: 30px; } .task__answer { border: 3px solid #f7f6ea; margin: 20px 0 14px; position: relative; display: block; padding: 25px 30px; } اقرأ أيضًا المقال التالي: المصفوفات (arrays) المقال السابق: الأعداد