Emq Mohammed

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

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

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

كل منشورات العضو Emq Mohammed

  1. تُنشّأ الكائنات عادة لتُمَثِّل أشياء من العالم الحقيقي مثل المستخدمين، والطلبات، وغيرها: 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)
  2. تُخَزَّن النصوص في 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) المقال السابق: الأعداد
  3. يوجد نوعان من الأعداد في JavaScript: أعداد عادية تخزَّن بصيغة 64-بت IEEE-754، تُعرف أيضًا ب "الأعداد العشرية مضاعفة الدقة" (double precision floating point numbers). هذا النوع هو ما سنستعلمه أغلب الوقت وسنسلط عليه الضوء في هذا الفصل. أعداد صحيحة كبيرة (BigInt numbers) تمثِّل عددًا صحيحًا متغير الحجم، إذ قد نلجأ إليها أحيانًا لأن النوع السابق لا يمكن أن يتجاوز القيمة 2^53 أو أن تقل عن -2^53، وسنخصص لهذا النوع فصلًا خاصًا به نظرًا للحاجة إليه في حالات خاصة. حاليًا، لِنتوسع عن ما نعرفه عنها، وننتقل إلى الحديث عن النوع الأول، الأعداد العادية. طرائق أخرى لكتابة عدد تخيل أننا نريد كتابة 1 بليون. الطريقة الواضحة هي: let billion = 1000000000; لكن، نتجنب غالبًا كتابة سلسلة طويلة من الأصفار في الحياة الواقعية لأنه من السهل الخطأ في ذلك ولكون ذلك الأمر يأخذ وقتًا أكثر. نكتب غالبًا شيئا مثل "1bn" بدلًا من بليون أو "7.3bn" بدلًا من 7 بليون و 300 مليون. يمكن تطبيق الأمر ذاته مع الأعداد الكبيرة. نُقَصِّر أرقام الأعداد في JavaScript بإضافة الحرف "e" للعدد وتحديد عدد الأصفار فيه: // بليون، حرفيًا: 1 وجانبه 9 أصفار let billion = 1e9; alert( 7.3e9 ); // 7,300,000,000 بمعنى آخر، يضرب الشكل "XeY" العدد X في 1 متبوعًا بعدد Y من الأصفار. 1e3 = 1 * 1000 1.23e6 = 1.23 * 1000000 لنكتب الآن شيئَا صغيرًا جدًا. مثلًا، جزء من المليون من الثانية: let ms = 0.000001; كما قمنا سابقًا، يمكن استخدام "e" لتجنب كتابة الأصفار، يمكننا القول: let ms = 1e-6; // ستة أصفار على يسار 1 إن قمنا بعد الأصفار في 0.000001، سنجد عددها 6. لذا يكون الرقم 1e-6. بمعنى آخر، وجود رقم سالب بعد "e" يعني القسمة على 1 متبوعًا بِعدد الأصفار المعطى: // بالقسمة على 1 متبوعًا ب 3 أصفار 1e-3 = 1 / 1000 (=0.001) // بالقسمة على 1 متبوعًا ب 6 أصفار 1.23e-6 = 1.23 / 1000000 (=0.00000123) الأعداد الست عشرية، والثنائية والثمانية تُستخدَم الأعداد الست عشرية بكثرة في JavaScript لتمثيل الألوان، وتشفير الأحرف ولأشياء أخرى عديدة. لذا فإن هناك طريقة أقصر لكتابتها وذلك بوضع السابقة 0x ثم الرقم. مثلًا: alert( 0xff ); // 255 alert( 0xFF ); // 255 (العدد ذاته, لا يوجد اختلاف باختلاف حالة الأحرف) تستخدم الأنظمة الثنائية والثمانية نادرًا، لكنها مدعومة أيضًا باستخدام السابقة 0b والسابقة 0o على التوالي: let a = 0b11111111; // الهيئة الثنائية لِلعدد 255 let b = 0o377; // الهيئة الثمانية لِلعدد 255 alert( a == b ); // صحيح، العدد ذاته 255 في كلا الجانبين يوجد ثلاثة أنظمة عددية فقط مدعومة بالشكل السابق. يجب استخدام الدالة parseInt لباقي الأنواع (سنشرحها لاحقًا في هذا الفصل). toString(base)‎ يُرجِع التابع num.toString(base)‎ تمثيلًا نصيًا للمتغير num إلى النظام العددي المُعطى base. مثلًا: let num = 255; alert( num.toString(16) ); // ff alert( num.toString(2) ); // 11111111 يمكن أن تختلف قيمة base من 2 حتى 36، والقيمة الافتراضية هي 10. حالات الاستخدام الشائعة: base=16: تستخدم للألوان الست عشرية، وتشفير الأحرُف وغيرها، قد تحوي الخانات الأرقام 0..9 أو الأحرف A..F. base=2: يستخدم بكثرة في تصحيح العمليات الدقيقة، يمكن أن يحوي الرقمين 0 أو 1. base=36: هو الحد الأعلى، يمكن أن يحوي الأرقام 0..9 أو الأحرُف A..Z. يمكن استخدام جميع الأحرف اللاتينية لتمثيل عدد. قد يبدو أمرًا ممتعًا لكن يكون مفيدًا في حال احتجنا لتحويل معرف عددي طويل إلى عدد أقصر، مثلًا، لتقصير رابط url. يمكن تمثيله بالنظام العددي ذي الأساس 36: alert( 123456..toString(36) ); // 2n9c نقطتين لِاستدعاء تابع لاحظ أن النقطتين في ‎123456..toString(36)‎ ليست خطأ كتابي. إن أردنا استدعاء تابع مباشرة على عدد/ مثل التابع toString في المثال أعلاه، فسنحتاج إلى نقطتين بعد العدد ... إن وضعنا نقطة واحدة فقط ‎123456.toString(36)‎ فسيكون هناك خطأ، لأن JavaScript سَتعتبر أن النقطة هي فاصلة عشرية وأن ما بعدها هو جزء عشري للعدد. فإذا وضعنا نقطة أخرى فستعرف أن الجزء العشري فارغ وتنتقل إلى الدالة. يمكن كتابتها بهذه الطريقة أيضًا ‎(123456).toString(36)‎. التقريب (Rounding) أحد الخصائص الأكثر استخدامًا عند التعامل مع الأعداد هي التقريب. يوجد العديد من الدوال المدمجة للتقريب: Math.floor: تقريب للجزء الأصغر: 3.1 تصبح 3، و -1.1 تصبح -2. Math.ceil: تقريب للجزء الأكبر: 3.1 تصبح 4، و -1.1 تصبح -1. Math.round: تقريب لأقرب عدد صحيح: 3.1 تصبح 3، 3.6 تصبح 4 و -1.1 تصبح -1. Math.trunc (ليست مدعومة بواسطة المتصفح Internet Explorer): تحذف أي شيء بعد الفاصلة العشرية بدون تقريب: 3.1 تصبح 3، -1.1 تصبح -1. يختصر الجدول في الأسفل الاختلافات بين هذه التوابع: Math.floor Math.ceil Math.round Math.trunc 3.1 3 4 3 3 3.6 3 4 4 3 -1.1 -2 -1 -1 -1 -1.6 -2 -1 -2 -1 تُعَطِّي هذه التوابع جميع الاحتمالات الممكنة للتعامل مع الجزء العشري للعدد، لكن ماذا إن كنا نريد تقريب العدد إلى خانة محدَّدة بعد الفاصلة العشرية؟ مثلًا، لدينا العدد 1.2345 ونريد تقريب إلى خانتين لنحصل على 1.23 فقط. يوجد طريقتين للقيام بذلك: 1- الضرب والقسمة: مثلًا، لتقريب الرقم إلى الخانة الثانية بعد الفاصلة العشرية، يمكننا ضرب العدد في 100، ثم نستدعي تابع التقريب ثم نقسم على نفس العدد. let num = 1.23456; alert( Math.floor(num * 100) / 100 ); // 1.23456 -> 123.456 -> 123 -> 1.23 2- يقرب التابع toFixed(n)‎ العدد المستدعى معه إلى الخانة n بعد الفاصلة العشرية ويُرجِع تمثيلًا نصيًا للنتيجة. let num = 12.34; alert( num.toFixed(1) ); // "12.3" يعمل التابع على تقريب العدد للأكبر أو الأصغر وفقًا إلى أقرب قيمة، مثل التابع Math.round: let num = 12.36; alert( num.toFixed(1) ); // "12.4" لاحظ أن مخرجات التابع toFixed هي نص. إن كان الجزء العشري أقل من المطلوب، تُضاف الأصفار إلى نهاية الرقم: let num = 12.34; alert( num.toFixed(5) ); // "12.34000", أصفار مضافة لجعل عدد الخانات 5 يمكننا تحويل المخرجات إلى عدد باستخدام الجمع الأحادي أو باستدعاء الدالة Number(): +num.toFixed(5)‎. حسابات غير دقيقة يُمَثَّل العدد داخليًا بصيغة 64-بِت IEEE-754، لذا يوجد 64 بِت لتخزين العدد: تستخدم 52 منها لتخزين أرقام العدد، و 11 منها لتخزين مكان الفاصلة العشرية (تكون أصفارًا للاعداد الصحيحة)، و 1 بِت لإشارة العدد. إن كان العدد كبيرًا جدًا، فَسيزداد عن مساحة التخزين 64-بِت، معطيًا ما لا نهاية: alert( 1e500 ); // ما لا نهاية ما قد يكون أقل وضوحًا، ويحدث غالبًا هو ضياع الفاصلة. لاحظ الاختبار الخطأ التالي: alert( 0.1 + 0.2 == 0.3 ); // خطأ الجملة السابقة تحدث فعليًا، إن فحصنا ما إن كان مجموع 0.1 و 0.2 هو 0.3، نحصل على false. غريب أليس كذلك؟! ما النتيجة إذًا إن لم تكن 0.3؟ alert( 0.1 + 0.2 ); // 0.30000000000000004 يوجد خطأ آخر هنا غير الموازنة الخطأ. تخيل أننا نقوم بموقع للتسوق الالكتروني، ووضع الزائر بضائع بقيم $0.10 و $0.20 في السلة. سيكون مجموع الطلب $0.30000000000000004. مما قد يفاجئ أي أحد. لكن السؤال الأهم، لم يحدث هذا؟ يُخَزَّن العدد في الذاكرة بهيئته الثنائية، سلسلة من البِت - واحدات وأصفار. لكن الأجزاء مثل 0.1 و 0.2 والتي تبدو بسيطة بالنسبة للنظام العددي العشري هي في الحقيقة أجزاء غير منتهية في النظام الثنائي. بمعنى آخر، ما هو 0.1؟ هو واحد مقسوم على عشرة 1/10، عُشر. ويكون من السهل تمثيلة بنظام الأعداد العشري. موازنة بالثلث: 1/3. الذي يصبح بكسور غير منتهية ‎0.33333(3)‎. لذا، فإن من المؤكد أن تعمل الأعداد المقسومة على مضاعفات العدد 10 في النظام العشري، ولا تعمل المقسومة على 3. وكذلك أيضًا في النظام الثنائي، تعمل الأعداد المقسومة على مضاعفات العدد 2، لكن يصبح العدد 1/10 كسورًا ثنائية غير منتهية. لا يوجد طريقة لتخزين العدد ذاته 0.1 أو 0.2 بالضبط باستخدام النظام الثنائي، كما لا يمكن تخزين الثلث كجزء عشري. يحل نمط الأعداد IEEE-754 هذه المشكلة بتقريب العدد إلى أقرب عدد ممكن. لا تتيح لنا قواعد التقريب هذه رؤية خسارة الأجزاء الصغيرة، لكنها تكون موجودة. يمكننا رؤية ذلك فعليًا: alert( 0.1.toFixed(20) ); // 0.10000000000000000555 وعند جمع عددين، فإن الأجزاء المفقودة تظهر. هذا يفسر لِمَ 0.1 + 0.2 لا تساوي 0.3 بالضبط. ليس فقط في JavaScript هذه المشكلة موجودة في العديد من اللغات البرمجية الأخرى مثل PHP، و Java، و C، و Perl، و Ruby تُعطي النتيجة ذاتها، لأنها تعتمد على الصيغة العددية ذاتها. هل يمكننا تجنب المشكلة؟ بالطبع، أفضل طريقة هي بتقريب النتيجة بمساعدة التابع toFixed(n)‎: let sum = 0.1 + 0.2; alert( sum.toFixed(2) ); // 0.30 يرجى ملاحظة أن toFixed تُرجِع نصًا دائمًا. وتتأكد من وجود خانتين فقط بعد العلامة العشرية. هذا يجعل الأمر مريحًا إن كان لدينا موقع تسوق إلكتروني وأردنا عرض $0.30. يمكننا استخدام الجمع الأحادي في الحالات الأخرى لتحويله إلى عدد: let sum = 0.1 + 0.2; alert( +sum.toFixed(2) ); // 0.3 يمكننا أيضا ضرب الأعداد مؤقتًا في 100 أو أي عدد أكبر لتحويل إلى أعداد صحيحة ثم إعادة قسمتها على العدد المضروب فيه. هكذا قد ترتفع نسبة الخطأ كما لو كنا نتعامل مع أعداد صحيحة لكننا سنتخلص منها بالقسمة: alert( (0.1 * 10 + 0.2 * 10) / 10 ); // 0.3 alert( (0.28 * 100 + 0.14 * 100) / 100); // 0.4200000000000001 لذا، يقلل نهج الضرب والقسمة الخطأ لكنه لا يزيلة كليًا. قد نحاول تجنب الأجزاء كليًا في بعض الأحيان. كما لو كنا نتعامل مع متجر إلكتروني، حتى نتمكن من تخزين الأسعار بالسنت بدلًا من الدولار. لكن ماذا إن وضعنا تخفيضًا ب 30%؟ لنكن أكثر دقة، يكون تجنب الأجزاء كليًا نادرًا جدًا. قم بتقريبها فقط لتتخلص من الأجزاء الغير المرغوبة عند الحاجة. شر البلية ما يضحك جرب تشغيل ما يلي: // مرحبًا! أنا عدد يزداد من تلقاء نفسه! alert( 9999999999999999 ); // يظهر 10000000000000000 هذه الحالة تعاني من المشكلة ذاتها: ضياع الدقة. يوجد 64 بِت للعدد، يمكن استخدام 52 منها لتخزين العدد، لكنها غير كافية. لذا يظهر الرقم الأخير. لا تعرض JavaScript خطأ في مثل هذه الحالات فهي تقوم بأفضل ما لديها لملائمة العدد للصيغة المطلوبة. لكن، لسوء الحظ هذه الصيغة ليست كبيرة كفاية. صفران نتيجة أخرى سلبية للتمثيل الداخلي للأعداد هو وجود صفرين 0 و -0. ذلك لأن الإإشارة تُمَثَّل ببِت مستقل، لذا فيمكن لأي عدد أن يكون سالبًا أو موجبًا بما في ذلك الصفر. لا يكون هذا الفرق ملحوظًا في أغلب الحالات، لأن المعامِلات مُعَدَّة لتعاملهما كعدد واحد. الفحص: isFinite و isNaN هل تذكر القيم العددية الخاصة التالية؟ Infinity (و ‎-Infinity) هي قيمة عددية خاصة تكون أكبر أو أصغر من أي شيء. NaN تُمَثِّل وجود خطأ (ليس عددًا Not a Number). تنتمي هذه القيم إلى النوع number، لكنها ليست أعداد، لذا يوجد توابع خاصة لفحصها: isNaN(value)‎ يُحوِّل المُعامل إلى عدد ثم يفحص ما إن كان NaN: alert( isNaN(NaN) ); // true alert( isNaN("str") ); // true لكن هل نحتاج لهذا التابع؟ أليس من الممكن استخدام الموازنة فقط === NaN؟ الإجابة للأسف هي لا. القيمة NaN هي فريدة ولا يمكن أن تساوي أي شيء، حتى نفسها: alert( NaN === NaN ); // false isFinite(value)‎ يُحوِّل مُعامله إلى عدد ويُرجِع القيمة true إن كان عددًا عاديًا، أي لا يكون NaN أو Infinity أو ‎-Infinity: alert( isFinite("15") ); // true alert( isFinite("str") ); // false, NaN alert( isFinite(Infinity) ); // false, Infinity تستخدم isFinite أحيانًا للتحقق ما إن كان النص عددًا عاديًا: let num = +prompt("Enter a number", ''); // أو قيمة غير عددية -Infinity أو Infinity سيكون صحيحًا إلا إن أدخلت alert( isFinite(num) ); يرجى ملاحظة أن الفراغ أو المسافة الواحدة تُعامل معاملة الصفر 0 في جميع التوابع العددية بما فيها isFinite. الموازنة باستخدام Object.is يوجد تابع خاص مدمج في اللغة يدعى Object.is يوزان القيم كما === لكنه أكثر موثوقية لسببين: أنه يعمل مع NaN: أي "Object.is(NaN, NaN) === true" وهذا أمر جيد. القيمتان 0 و -0 مختلفتان: "Object.is(0, -0) === false"، الأمر صحيح تقنيًا، لأن العدد لديه إشارة داخليًا مما يجعل القيم مختلفة حتى لو كانت باقي الخانات أصفارًا. يكون التابع Object.is(a, b)‎ نفس a === b في باقي الحالات. تُستخدم طريقة الموازنة هذه غالبًا في توصيف JavaScript. عندما تحتاج خوارزمية لموازنة كون قيمتين متطابقتان تمامًا فإنها تستخدم Object.is (تُسَمَّى داخليًا القيمة ذاتها "SameValue"). parseInt و parseFloat التحويل العددي باستخدام الجمع + أو Number()‎ يعد صارمًا. إن لم تكن القيمة عددًا فإنها تفشل: alert( +"100px" ); // NaN الاستثناء الوحيد هو المسافات الفارغة ببداية أو نهاية النص، إذ يتم تجاهلها. لكن، يوجد لدينا في الواقع قيمًا بالوحدات، مثل "100px" أو "12pt" في CSS. في العديد من الدول أيضا، يُلحَق رمز العملة بالقيمة، فمثًلا، لدينا "19€" ونريد استخراج قيمة عددية من ذلك. هذا ما يقوم به التابعان parseInt و parseFloat، إذ يقرآن العدد من النص المعطى حتى تعجزان على ذلك فتتوقف العملية. في حال وجود خطأ، يعيدان العدد المُجَمَّع. فيعيد التابع parseInt عددًا صحيحًا، بينما يعيد التابع parseFloat عددًا عشريًا: alert( parseInt('100px') ); // 100 alert( parseFloat('12.5em') ); // 12.5 // يُرجَع العدد الصحيح فقط alert( parseInt('12.3') ); // 12 // تُوقِف النقطة الثانية عملية التحليل alert( parseFloat('12.3.4') ); // 12.3 يوجد بعض الحالات يرجع فيها التابع parseInt أو parseFloat القيمة NaN وذلك عندما لا يوجد أي رقم لإرجاعه. ففي المثال التالي، يوقف الحرف الأول العملية: alert( parseInt('a123') ); // NaN المُعامِل الثاني للتابع parseInt(str, radix)‎ لدى التابع parseInt()‎ مُعامِل ثانٍ اختياري. والذي يحدد نوع النظام العددي، لذا يمكن للتابع parseInt تحويل النصوص ذات الأعداد الست عشرية، الثنائية وهكذا: alert( parseInt('0xff', 16) ); // 255 alert( parseInt('ff', 16) ); // 255 alert( parseInt('2n9c', 36) ); // 123456 دوال رياضية أخرى تحتوي JavaScript على الكائن المُدمَج Math الذي يحتوي على مكتبة صغيرة بالدوال والثوابت الرياضية. إليك بعض الأمثلة: Math.random()‎: تُرجِع عددًا عشوائيًا من 0 إلى 1 (لا تتضمن 1): alert( Math.random() ); // 0.1234567894322 alert( Math.random() ); // 0.5435252343232 alert( Math.random() ); // ... (أي رقم عشوائي) Math.max(a, b, c...) / Math.min(a, b, c...)‎: تُرجِع القيمة الأكبر أو الأصغر من المُعامِلات: alert( Math.max(3, 5, -10, 0, 1) ); // 5 alert( Math.min(1, 2) ); // 1 Math.pow(n, power)‎: تُرجع العدد n مرفوعًا إلى الأُس المُعطى: alert( Math.pow(2, 10) ); // 2^10 = 1024 يوجد المزيد من الدوال والثوابت في الكائن Math، بما فيها علم المُثَلَّثات، والتي يمكنك ايجادها في توثيق الكائن Math. الخلاصة لكتابة أعداد كبيرة: أضِف "e" مع عدد الأصفار الخاصة بالعدد المطلوب، مثل: 123e6 هو 123 مع 6 أصفار. قيمة سالبة بعد "e" تقسم العدد على 1 مع عدد الأصفار المُعطى. لِأنظمة العد المختلفة: يمكن كتابة الأعداد مباشرة بالنظام الستعشري (0x)، أو الثُماني (0o)، أو الثُنائي (0b). parseInt(str, base)‎ تحوِّل النص str إلى عدد صحيح بالنظام العددي المُعطى base، و ‎2 ≤ base ≤ 36. num.toString(base)‎ تحوِّل العدد إلى نص بالنظام العددي المُعطى base. لتحويل القيم مثل 12pt and 100px إلى عدد: استخدم parseInt أو parseFloat لتحويل سلس، والتي تقرأ العدد من نص وتُرجِع القيمة التي استطاعت قرائتها قبل حصول أي خطأ. للأجزاء: التقريب باستخدام Math.floor، أو Math.ceil، أو Math.trunc، أو Math.round أو num.toFixed(precision)‎. تذكر وجود ضياع في دقة الجزء العشري عند التعامل مع الكسور. للمزيد من الدوال الرياضية: اطلع على الكائن Math عندما تحتاج ذلك، هذه المكتبة صغيرة جدًا، لكنها تغطي الاحتياجات الأساسية. المهام جمع الأعداد من الزائر الأهمية: 5 انشِئ سكربت يتيح للمستخدم ادخال رقمين ثم أعرض مجموعهما. الحل let a = +prompt("The first number?", ""); let b = +prompt("The second number?", ""); alert( a + b ); لاحظ عامل الجمع الأحادي + قبل prompt. يحوِّل القيم إلى أعداد. وإلا فإن a و b ستكون نصوصًا وسيكون مجموعهما بدمجهما: "1" + "2" = "12". لماذا ‎6.35.toFixed(1) == 6.3؟ الأهمية: 4 تُدَوِّر كلًا من Math.round و toFixed العدد إلى أقرب عدد له وفقًا للتوثيق: الأجزاء من 0..4 تُدَوَّر للأسفل، بينما الأجزاء 5..9 تثدَوَّر للأعلى. مثلًا: alert( 1.35.toFixed(1) ); // 1.4 في المثال المشابه أدناه، لِمَ تُدَوَّر 6.35 إلى 6.3، وليس 6.4؟ alert( 6.35.toFixed(1) ); // 6.3 كيف نُدَوِّر 6.35 بالطريقة الصحيحة؟ الحل الجزء 6.35 هو عبارة عن عدد غير منتهي في الصيغة الثنائية. وكجميع الحالات المشابهة، يُخَزَّن مع ضياع في الدقة. لنرَ: alert( 6.35.toFixed(20) ); // 6.34999999999999964473 قد يتسبب ضياع الدقة في زيادة أو نقصان أي عدد. يكون العدد في هذه الحالة أقل بقليل من قيمته الفعلية، ولهذا يُدَوَّر للأسفل. ماذا عن العدد 1.35؟ alert( 1.35.toFixed(20) ); // 1.35000000000000008882 جعل ضياع الدقة هذا الرقم أكبر بقليل مما هو عليه مما تسبب في تقريبه للأعلى. كيف يمكننا حل مشكلة تقريب العدد 6.35 حتى يُدَوَّر بالشكل الصحيح يجب أن نحوله إلى عدد صحيح قبل التقريب: alert( (6.35 * 10).toFixed(20) ); // 63.50000000000000000000 لاحظ عدم وجود أي ضياع في دقة العدد 63.5. ذلك لأن الجزء العشري 0.5 يساوي 1/2. يمكن تمثيل الأجزاء المقسومة على 2 تُمَثَّل بشكل صحيح في النظام الثنائي. يمكننا تقريب العدد الآن: alert( Math.round(6.35 * 10) / 10); // 6.35 -> 63.5 -> 64(rounded) -> 6.4 كرر حتى يصبح المُدخَل عددًا الأهمية: 5 أنشِئ الدالة readNumber والتي تطلب من الزائر إدخال عدد حتى يقوم بإدخال قيمة عددية صحيحة. يجب أن تكون القيمة المُرجَعة عددًا. يمكن للزائر إيقاف العملية بإدخال سطر فارغ أو الضغط على "CANCEL". يجب أن تُرجِع الدالة null في هذه الحالة. الحل function readNumber() { let num; do { num = prompt("Enter a number please?", 0); } while ( !isFinite(num) ); if (num === null || num === '') return null; return +num; } alert(`Read: ${readNumber()}`); طريقة الحل معقدة قليلًا حتى نتمكن من معالجة حالات الأسطر الفارغة/null. لذا فإن الشيفرة تستقبل المدخلات حتى تكون عددًا عاديًا. تُطَبِّق كلًا من null (cancel)‎ والسطر الفارغ شروط الأعداد كونها تساوي القيمة العددية 0. بعد توقف الشيفرة يجب معاملة null والأسطر الفارغة بطريقة خاصة (إرجاع null). لأن تحويلها إلى أعداد يُرجِع 0. حلقة غير منتهية أحيانًا الأهمية: 4 الحلقة التالية غير منتهية، ولا تتوقف أبدًا. لماذا؟ let i = 0; while (i != 10) { i += 0.2; } الحل ذلك لأن i لن يساوي 10 أبدًا. نفذ الشيفرة التالية لرؤية قيم i: let i = 0; while (i < 11) { i += 0.2; if (i > 9.8 && i < 10.2) alert( i ); } لا توجد قيمة تساوي 10 تمامًا. تحدث مثل هذه الأمور بسبب ضياع الدقة عند إضافة الأجزاء مثل 0.2. الخلاصة، تجنب التحقق من المساواة عند التعامل مع الأجزاء العشرية. رقم عشوائي من العدد الأدنى إلى الأقصى الأهمية: 2 تُنشِئ الدالة Math.random()‎ المُضَمَنَة في اللغة قيمة عشوائية بين 0 و 1 (ليس بما في ذلك 1). اكتب الدالة random(min, max)‎ لتوليد عدد عشري عشوائي من min إلى max (بما لا يتضمن max). أمثلة عن عملها: alert( random(1, 5) ); // 1.2345623452 alert( random(1, 5) ); // 3.7894332423 alert( random(1, 5) ); // 4.3435234525 الحل نريد تعيين جميع القيم من الفترة 0…1 إلى القيم من min إلى max. يمكن القيام بذلك في مرحلتين: إذا ضربنا قيمة عشوائية من 0…1 في max-min. فإن فترة القيم الممكنة تزيد 0..1 إلى 0..max-min. إذا أضفنا min الآن، تصبح الفترة من min إلى max. الدالة: function random(min, max) { return min + Math.random() * (max - min); } alert( random(1, 5) ); alert( random(1, 5) ); alert( random(1, 5) ); قيمة صحيحة عشوائية من min إلى max الأهمية: 2 أنشِئ دالة randomInteger(min, max)‎ تقوم بتوليد قيمة صحيحة عشوائية من min إلى max بما في ذلك min و max. يجب أن يظهر كل رقم من الفترة min..max بفرص متساوية. مثال على طريقة العمل: alert( randomInteger(1, 5) ); // 1 alert( randomInteger(1, 5) ); // 3 alert( randomInteger(1, 5) ); // 5 يمكنك استخدام حل المثال السابق كأساس لهذه المهمة. الحل الطريقة السهلة والخطأ الحل الأسهل لكنه خطأ سيكون بتوليد قيمة من min إلى max وتقريبها: function randomInteger(min, max) { let rand = min + Math.random() * (max - min); return Math.round(rand); } alert( randomInteger(1, 3) ); الدالة تعمل لكنها خطأ. احتمال ظهور القيم الطرفية min و max أقل بمرتين من باقي القيم. إن شغلنا المثال أعلاه لعدة مرات، فينرى ظهور 2 بصورة أكبر. يحدث ذلك لأن Math.round()‎ تأخذ رقما من الفترة 1..3 وتُدَوِرها كما يلي: values from 1 ... to 1.4999999999 become 1 values from 1.5 ... to 2.4999999999 become 2 values from 2.5 ... to 2.9999999999 become 3 نلاحظ الآن أن لدى 1 قيم أقل بمرتين من 2 وكذلك 3. الطريقة الصحيحة يوجد العديد من الطرق الصحيحة لحل هذه المهمة. إحداها هو بتعديل حدود الفترة. للتأكد من وجود فرص متساوية، نُوَلِّد قيمًا من 0.5 إلى 3.5، ثم إضافة الاحتمالات الممكنة للأطراف: function randomInteger(min, max) { // (max+0.5) إلى (min-0.5) التقريب الآن من let rand = min - 0.5 + Math.random() * (max - min + 1); return Math.round(rand); } alert( randomInteger(1, 3) ); طريقة بديلة هي استخدام الدالة Math.floor لرقم عشوائي من min إلى max+1: function randomInteger(min, max) { // (max+1) إلى min التقريب الآن من let rand = min + Math.random() * (max + 1 - min); return Math.floor(rand); } alert( randomInteger(1, 3) ); جميع الفترات أصبحت متوازنة الآن: values from 1 ... to 1.9999999999 become 1 values from 2 ... to 2.9999999999 become 2 values from 3 ... to 3.9999999999 become 3 لدى جميع الفترات الطول ذاته مما يجعل التوزيع النهائي موحدًا. ترجمة -وبتصرف- للفصل Numbers من كتاب 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; } اقرأ أيضًا المقال التالي: السلاسل النصية (strings) المقال السابق: توابع الأنواع الأولية
  4. قبل كتابة شيفرات برمجية أكثر تعقيدا، لنتطرَّق إلى تنقيح الأخطاء. تنقيح الأخطاء هي عملية إيجاد الأخطاء في السكريبت وإصلاحها. تدعم جميع المتصفحات الحديثة وبعض البيئات الأخرى "تنقيح الأخطاء" -واجهة مستخدم خاصة في أدوات المطور والتي تجعل العثور على الأخطاء وتصحيحها أمرا سهلا. تُتيح هذه الواجهة أيضا تَتَبُّع الأكواد خطوة بخطوة لمعرفة ما يحدث فيها بالتفصيل. سنستخدم Chrome لأن لديه ميزات كافية لذلك، كما تتوفر هذه الميزات في معظم المتصفحات الأخرى. جزء الموارد "sources" قد يبدو إصدار Chrome لديك مختلفًا بعض الشيء، إلا أن المحتوى هو ذاته. افتح صفحة example في Chrome. شغِّل أدوات المطور بالضغط على F12 (على أجهزة Mac، استعمل الاختصار Cmd+Opt+I). اختر الجزء sources. إن كانت هذه هي مرتك الأولى للدخول إلى جزء sources، فهذا ما ستراه: يفتح هذا الزر علامة تبويب تحوي الملفات. اضغط عليها واختر hello.js من العرض الشجري "tree view". يجب أن ترى التالي: هنا يمكننا رؤية ثلاث مناطق: منطقة الموارد "Resources zone" والتي تعرض ملفات HTML، و JavaScritp، و CSS، وغيرها من الملفات بما في ذلك الصور المُلحقة بالصفحة. قد تظهر إضافات Chrome هنا أيضًا. منطقة المصدَر "Source zone" والتي تعرض الشيفرة البرمجية المصدرية. منطقة المعلومات والتحكم "Information and control zone" لتنقيح الأخطاء، سَنكتشفها أكثر فيما يلي يمكنك الضغط على الزر مُجددًا لِإخفاء قائمة الموارد وإعطاء الشيفرة البرمجية مساحة كافية. شاشة التحكم (Console) تظهر شاشة تحكم عند الضغط على Esc. يمكن كتابة الأوامر في شاشة التحكم ثم الضغط على Enter لتنفيذها. تظهر مخرجات تنفيذ أمر ما أسفله مباشرة في شاشة التحكم. مثال: كما في الصورة بالأسفل 1+2 ينتج 3، بينما hello("debugger")‎ لا يُظهِر أي نتائج، لذلك فإننا نرى undefined: نقاط التوقف (Breakpoints) لنختبر ما يحدُث أثناء تنفيذ الشيفرة البرمجية في صفحة example. في الصفحة hello.js، اضغط على السطر رقم 4؛ نضغط على الرقم ذاته وليس السطر. هكذا تكون قد أنشأت نقطة توقف. اضغط على الرقم 8 أيضًا. يجب أن يبدو الشكل كما في الصورة التالية: نقطة التوقف هي نقطة يتوقف فيها مصحح الأخطاء عن تنفيذ JavaScript تلقائيًا. يمكننا فحص المتغيرات الحالية وتنفيذ الأوامر أو أي شيء آخر في لوحة التحكم أثناء توقف عمل الشيفرة البرمجية. أي أنه يمكننا تتبع الشيفرة البرمجية عند نقطة معينة عبر ايقافها والتأكد من أي شيء فيها كما نريد. يمكننا رؤية قائمة بالعديد من نقاط التوقف في الجزء الأيمن من الشاشة. يكون الأمر مفيدًا عند وجود عدة نقاط توقف في أكثر من ملف، وذلك يتيح لنا: التنقل بسرعة إلى نقاط التوقف في الشيفرة البرمجية (بالضغط عليها من الجزء الأيمن). إلغاء تفعيل نقاط التوقف مؤقتا بإلغاء تحديدها. حذف نقطة التوقف بالضغط عليها باليمين واختيار حذف "remove". وهكذا … نقاط التوقف الشرطية يتيح لك الضغط يمينَا على رقم السطر إنشاء نقطة توقف شرطية تُنَفَّذ عند تحقق الشرط المحدد فقط. يكون ذلك مفيدَا عندما تريد إيقاف التنفيذ لمعرفة قيمة أي متغير أو قيمة أي معامل في دالة. أمر Debugger يمكن أيضا إيقاف تنفيذ الشيفرة البرمجية بوضع الأمر debugger فيه كما يلي: function hello(name) { let phrase = `Hello, ${name}!`; debugger; // <-- يتوقف المنقح هنا say(phrase); } هذه الطريقة سهلة عندما نُعدِّل الشيفرة البرمجية باستخدم محرر الشيفرات البرمجية ولا نريد الانتقال إلى المتصفح وتشغيل السكربت في وضع أدوات المطور لإنشاء نقاط توقف. توقف وتحقق في المثال، يتم استدعاء الدالة hello()‎ أثناء تحميل الصفحة، لذلك فإن أسهل طريقة لتفعيل مُنقِّح الأخطاء (بعد إعداد نقاط التوقف) هي إعادة تحميل الصفحة. اضغط على F5 (لمستخدمي ويندوز أو لينكس)، أو اضغط على Cmd+R (لمستخدمي Mac). سيتوقف تنفيذ الشيفرة البرمجية في السطر الرابع حيث تم إنشاء نقطة التوقف: افتح قوائم المعلومات المنسدلة على اليمين (موضحة بأسهم). تتيح هذه القوائم التحقق من حالة السكريبت الحالية: 1- Watch - تعرض القيم الحالية لأي تعابير. يمكنك الضغط على + وإدخال أي تعبير تريده. سيعرض المعالج قيمته في أي وقت وحساب قيمته تلقائيا أثناء التنفيذ. 2- Call Stack - تعرض سلسلة الاستدعاءات المتداخلة. في الوقت الحالي، المعالج وصل حتى استدعاء الدالة hello()‎، المُستدعاة من خلال السكريبت index.html (لا يوجد دوال أخرى لذلك سُمِّيَت "anonymous"). إن ضَغَطت على عنصر من الحزمة (stack) مثلا "anonymous"، فسينتقل المعالج مباشرة إلى الشيفرة البرمجية المُمَثِّل لهذا العنصر وستتمكن من فحص جميع متغيراته أيضا. 3- Scope - تعرض المتغيرات الحالية. Local تعرض متغيرات الدالة المحلية. يمكنك أيضا رؤية قيم هذه المتغيرات موضحة على اليمين. Global تعرض المتغيرات الخارجية (خارج نطاق أي دالة). يوجد أيضا الكلمة المفتاحية this والتي لم تُشرح بعد، لكن سيتم شرحها قريبا. تتبع التنفيذ يوجد بعض الأزرار لِتتبع التنفيذ أعلى يمين اللوحة. لِنتطرق إليها. - زر استمرار التنفيذ F8. يستأنف التنفيذ. إن لم توجد أي نقاط توقف، فإن التنفيذ سيستمر بعد الضغط على هذا الزر وسَيفقد مصحح الأخطاء السيطرة على السكريبت. هذا ما سنراه بعد الضغط عليه: تم استئناف التنفيذ، ووصل لنقطة توقف أخرى داخل الدالة say()‎ وتوقف هناك. انظر في Call stack على اليمين. تم تنفيذ استدعاء آخر. وصل التنفيذ الآن حتى الدالة say()‎. - نفِّذ خطوة (نفّذ الأمر التالي)، لكن لا تنتقل إلى الدالة الأخرى، الوصول السريع F10. سيظهر alert إذا ضغطنا على هذا الزر الآن. الأمر المهم هنا هو أن alert قد يحوي على أي دالة، لذلك فإن التنفيذ سيتخطاها. - نفِّذ خطوة، الوصول السريع F11. يقوم بنفس آلية عمل الأمر السابق بالإضافة إلى تنفيذ الدوال المتداخلة. سَتتنفذ أوامر السكريبت بالضغط عليه خطوة بخطوة. - استمر بالتنفيذ حتى نهاية الدالة الحالية، باستخدام الزر Shift+F11. سيتوقف التنفيذ عند آخر سطر للدالة الحالية. يكون هذا الأمر مفيدا عند الدخول إلى استدعاء دالة مصادفةَ باستخدام الزر ، لكن تتبع هذه الدالة ليس أمرا مهما، لذلك نقوم بتخطي تتبعها. - تفعيل/تعطيل جميع نقاط التوقف. لا يقوم هذا الزر بمتابعة التنفيذ، إنما يُفَعِّل/يُعطِّل كمَا كبيرا من نقاط التوقف. - تفعيل/تعطيل التوقف التلقائي في حال حدوث خطأ. عند تفعيله في وضع أدوات المطور، فإن الشيفرة البرمجية تتوقف عن التنفيذ تلقائيا عند أي خطأ في السكريبت. ثم يمكننا تحليل المتغيرات لمعرفة سبب الخطأ. لذلك، إن أوقف خطأ مَا متابعة تنفيذ الشيفرة البرمجية، يمكننا فتح مصحح الأخطاء وتفعيل هذا الخيار وإعادة تحميل الصفحة لرؤية مكان توقف الشيفرة البرمجية وما هو المحتوى عند تلك النقطة. الاستمرار حتى هنا "Continue to here" الضغط يمينا على سطر من الشيفرة البرمجية يفتح قائمة السياق المحتوية على خيار مفيد يُدعى "Continue to here". يكون هذا الخيار مُفيدا عندما نريد التقدم بضع خطوات للأمام بدون وضع نقطة توقف. التسجيل (Logging) يمكن استخدام الدالة console.log لِعرض شيء على الشاشة كمُخرج من الشيفرة البرمجية. مثلا، يعرض الأمر التالي القيم من 0 حتى 4 إلى الشاشة: // open console to see for (let i = 0; i < 5; i++) { console.log("значение", i); } لا يرى المستخدم العادي هذه المخرجات. لرؤيتها، افتح علامة التبويب Console أو اضغط على Esc إن كنت في علامة تبويب أخرى هكذا تُفتًح الشاشة في الأسفل. إن كانت الشيفرة البرمجية تحتوي على أوامر console.log، فسنرى ما يحدث من خلال سجلات التتبع بدون الدخول إلى المُصحح. الخلاصة كما رأينا، فإن هناك ثلاث طرائق رئيسية لإيقاف السكريبت: باستخدام نقطة توقف. الأمر debugger. وجود خطأ (في حال كانت أداوت المطور مفتوحة وكان الزر مُفَعًّلَأ). عند توقف السكريبت، يمكننا فحص المتغيرات وتتبع الشيفرة البرمجية لرؤية أي أخطاء. يوجد العديد من الخيارات الأخرى في أدوات المطور أكثر مما تم شرحه سابقا. تجد الدليل كاملا على https://developers.google.com/web/tools/chrome-devtools. تُعَد المعلومات التي وُضِعَت كافية لبدء تتبع أي شيفرة برمجية وتنقيحها، لكنك ستحتاج للاطلاع على الدليل لاحقا لتعلم ميزات متقدمة في أدوات المطور، خاصة إن كنت تتعامل كثيرا مع المتصفح. يمكنك الضغط على عدة أماكن في أدوات المطور ورؤية ما يحدث. تُعد هذه أفضل طريقة لتعلم أدوات المطور. لا تنسَ الضغط يمينا وتجريب قوائم السياق. ترجمة -وبتصرف- للفصل Debugging in Chrome من كتاب The JavaScript Language. .task__importance { color: #999; margin-left: 30px; } .task__answer { border: 3px solid #f7f6ea; margin: 20px 0 14px; position: relative; display: block; padding: 25px 30px; } code { background-color: rgb(250, 250, 250); border-radius: 3px; } اقرأ أيضًا المقال التالي: نمط كتابة الشيفرة المقال السابق: مراجعة لما سبق
  5. يجب أن تكون الشيفرة البرمجية مرتبة ونظيفة وسهلة القراءة قدر الإمكان. هذا ما يسمى بفن البرمجة وهو أخذ مهمة معقدة وبرمجتها بطريقة صحيحة ومقروءة في الوقت ذاته. يساعد نمط كتابة الشيفرة كثيرا في ذلك. الصياغة في الصورة أدناه يوجد صفحة تحوي بعض القواعد المهمة في تركيب الشيفرات البرمجية. سنناقش هذه القواعد وأسبابها بالتفصيل. ملاحظة: لا يوجد قواعد الزامية. لا يوجد قاعدة إلزامية لنمط التكويد. هذه القواعد تعد تفضيلات وليست أساسيات برمجية. الأقواس المعقوصة (Curly Braces) تُكتب الأقواس المعقوصة في معظم مشاريع JavaScript المعقوصة بالطريقة "المصرية" بوضع قوس الفتح في نفس السطر الذي يحوي الكلمة المفتاحية - ليس في سطر جديد. يجب وضع مسافة قبل القوس الافتتاحي كما يلي: if (condition) { // افعل هذا //…. وذاك //…. وذاك } التعليمة المكونة من سطر واحد مثل if(condition) doSomething()‎ تعد حالة طرفية مهمة، فهل يجب استخدام الأقواس فيها؟ فيما يلي بعض البدائل المشروحة. يمكنك قراءتها والحكم على درجة سهولة قراءتها بنفسك: 1- يقوم بعض المبتدئون أحيانًا بما يلي. ما يعد ممارسة خاطئة. فلا حاجة لِلأقواس المعقوصة هنا: if (n < 0) {alert(`Power ${n} is not supported`);} 2- الانتقال إلى سطر جديد بدون استخدام أقواس. تجنب هذا الأمر لأنه يسبب بعض الأخطاء: if (n < 0) alert(`Power ${n} is not supported`); 3- سطر واحد بدون أقواس يُعد مقبولا في حال كان السطر قصيرًا: if (n < 0) alert(`Power ${n} is not supported`); 4- أفضل الطرق: if (n < 0) { alert(`Power ${n} is not supported`); } يمكن استخدام سطر واحد في حالة الشيفرات البرمجية المختصرة مثل: if (cond) return null. لكن استخدام شيفرة برمجية كتلية (كما في رقم 4) هو الأفضل من ناحية سهولة القراءة. طول السطر لا يحب أحدٌ قراءة سطر برمجي طويل. أصبح فصل الأسطر الطويلة ممارسة عامة لدى الجميع. إليك المثال التالي: // تسمح أقواس الفاصلة العليا المائلة ` بتقسيم النص إلى عدة أسطر let str = ` Ecma International's TC39 is a group of JavaScript developers, implementers, academics, and more, collaborating with the community to maintain and evolve the definition of JavaScript. `; بالنسبة للتعليمة البرمجية if: if ( id === 123 && moonPhase === 'Waning Gibbous' && zodiacSign === 'Libra' ) { letTheSorceryBegin(); } يتم الاتفاق على الحد الأقصى لطول السطر البرمجي على مستوى فريق العمل. يكون طول السطر البرمجي غالبا 80 أو 120 محرفَا. مسافة البادئة يوجد نوعان من البادئة "Indents": البادئة الأفقية: 2 أو 4 مسافات: تُصنع البادئة الأفقية بوضع 2 أو 4 مسافات أو باستخدام رمز البادئة الأفقية (الزر Tab). وُجدت اختلافات قديمة حول أيهما أفضل، لكن المسافات هي الأكثر شيوعا هذه الأيام. تتميز المسافات عن الزر Tab بكونها أكثر مرونة أثناء التعديل. مثلًا، يمكننا إزاحة المتغيرات داخل قوس مفتوح كالتالي: show(parameters, aligned, // 5 spaces padding at the left one, after, another ) { // ... } البادئة العمودية: الأسطر الفارغة لفصل الشيفرات البرمجية إلى أجزاء منطقية: يمكن تقسيم حتى دالة بحد ذاتها إلى أجزاء منطقية لتسهل قرائتها. في المثال أدناه، تم تقسيم تعريف المتغيرات وحلقة التكرار الرئيسية والنتيجة عموديا: function pow(x, n) { let result = 1; // <-- for (let i = 0; i < n; i++) { result *= x; } // <-- return result; } يمكنك وضع سطر فارغ حيثما تريد لجعل الشيفرة البرمجية مقروءة بسهولة. لا يجب أن يوجد أكثر من 9 أسطر بدون بادئة عمودية. الفواصل المنقوطة ";" يجب أن توضع فاصلة منقوطة بعد كل تعليمة برمجية، حتى وإن كان من الممكن عدم إضافتها. يوجد بعض اللغات حيث يكون استخدام الفاصلة المنقوطة اختياريا وتُستخدم نادرا. لكن في بعض الحالات في JavaScript لا يحل السطر الجديد محل الفاصلة المنقوطة مما يجعل الشيفرة البرمجية عرضة للخطأ. يمكنك الاطلاع أكثر عن ذلك في جزء بنية الشيفرة البرمجية. إن كنت مبرمجًا متمرسًا في JavaScriptK، يمكنك اختيار نمط تكويد بدون فاصلة منقوطة مثل StandardJS. أو يُفَضَّل استخدام فواصل منقوطة لتجنب الأخطاء. أغلب المبرمجين يضعون فواصل منقوطة. مستويات التداخل تجنب تداخل الشيفرة البرمجية للعديد من المستويات. مثلًا، في الحلقة المتكررة يُفَضَّل استخدام التعليمة continue لتجنب التداخل العميق. مثلا، بدلا من إضافة if شرطية داخلية كالتالي: for (let i = 0; i < 10; i++) { if (cond) { ... // <- مستوى تشعيب إضافي } } يمكننا كتابة: for (let i = 0; i < 10; i++) { if (!cond) continue; ... // <- لا مزيد من التشعيبات } يمكن استخدام الأسلوب ذاته مع if/else و return. مثلًا، نحتاج لجزئين في المثال أدناه. خيار 1: function pow(x, n) { if (n < 0) { alert("Negative 'n' not supported"); } else { let result = 1; for (let i = 0; i < n; i++) { result *= x; } return result; } } خيار 2: function pow(x, n) { if (n < 0) { alert("Negative 'n' not supported"); return; } let result = 1; for (let i = 0; i < n; i++) { result *= x; } return result; } يُعد الخيار 2 أسهل قراءةً من الخيار 1 لأن الحالة الخاصة n < 0 فُحصت مسبقًا. بعد فحص قيمتها يمكننا الانتقال إلى جزء الشيفرة البرمجية الرئيسية بدون الحاجة لتداخل أكثر. موضِع الدوال في حال كنت تكتب العديد من الدوال المساعدة والتي تستخدمها الشيفرة البرمجية، فإن هناك 3 طرق لتنظيم هذه الدوال. 1- تعريف الدوال أعلى الشيفرة البرمجية التي تستخدمها: // تعريفات الدوال function createElement() { ... } function setHandler(elem) { ... } function walkAround() { ... } // الشيفرة التي تستخدمها let elem = createElement(); setHandler(elem); walkAround(); 2- الشيفرة البرمجية أولا ثم الدوال: // الشيفرة التي تستخدم الدوال let elem = createElement(); setHandler(elem); walkAround(); // --- دوال مساعدة --- function createElement() { ... } function setHandler(elem) { ... } function walkAround() { ... } 3- الطريقة المختلطة: تُعرَّف الدالة في أول مكان تستخدم فيه. الخيار الثاني هو الأفضل غالبا. ذلك لأنه عند قراءة شيفرة برمجية، فإننا نريد معرفة ما تقوم به أولا. إن كانت الشيفرة البرمجية في البداية فإن غرضها يكون واضحا مباشرة. أيضا، قد لا نحتاج لقراءة الدوال، خاصة إن كانت وظائفها واضحة من مسمياتها. شروحات لأنماط كتابة الشيفرة يحتوي دليل أنماط التكويد قواعد عامة حول كيفية كتابة شيفرة برمجية. مثلًا، الأقواس المستخدمة، وعدد مسافات البادئة، وأقصى طول للسطر، …الخ. والعديد من التفاصيل الدقيقة. عندما يستخدم جميع أعضاء الفريق الدليل نفسه لنمط التكويد، ستبدو الشيفرة البرمجية موحدة بغض النظر عمن قام بكتابتها. يمكن لأي الفريق الكتابة بنمط تكويد خاص بهم، لكن لا حاجة لذلك لوجود العديد من المعايير العالمية للاختيار منها. بعض الخيارات الشهيرة: Google JavaScript Style Guide Airbnb JavaScript Style Guide Idiomatic.JS StandardJS وغيرها في حال كنت مطورًا مبتدئًا، ابدأ بالشرح المزود هنا. ثم يمكنك الانتقال إلى دليل آخر مما ذكرناه لتتعرف على المزيد من التفاصيل وتختار الأسلوب الأنسب لك. منقحات الصياغة التلقائية (Automated Linters) منقحات الصياغة (Linters): هي عبارة عن أدوات يمكنها فحص نمط الشيفرة البرمجية تلقائيا واقتراح تعديلات لتحسينها. الأمر الذي يجعلها مفيدة أكثر هو قدرتها على العثور على بعض الأخطاء، مثل الخطأ باسم متغير أو دالة ما، لهذا فإنه من المستحسن استخدام منقح صياغة (Linter) حتى لو لم تُرد اتباع نمط تكويد معين. هنا بعض أدوات تنقيح الصياغة المعروفة مثل: JSLint – تُعد من أدوات تنقيح الصياغة الأولى. JSHint – تحوي اعدادات أكثر من JSLint. ESLint – الأحدث تقريبا. كلها تؤدي الغرض ذاته. الكاتب هنا يستخدم ESLint. معظم هذه الأدوات تكون مدمجة مع العديد من المحررات الشهيرة: يجب عليك أن تُفَعِّل الإضافة في المحرر وتحدد نمط التكويد الذي تريده. مثلا، لاستخدام ESLint اتَّبع ما يلي: ثبت Node.js. ثبت ESLint باستخدام الأمر npm install -g eslint (يُعد npm مُثَبِّت حزم JavaScript). أنشئ ملف إعداد وسمِّه "‎.eslintrc" في ملف مشروع JavaScript الرئيسي (الملف الذي يحتوي على جميع الملفات). ثبت/فعِّل الإضافة لمحررك الذي يدعم ESLint. معظم المحررات تدعم ESLint. هنا مثال على ملف "‎.eslintrc": { "extends": "eslint:recommended", "env": { "browser": true, "node": true, "es6": true }, "rules": { "no-console": 0, "indent": ["warning", 2] } } التعليمة "extends" هنا تعني أن الإعداد يعتمد على مجموعة إعدادات ESLint الافتراضية "eslint:recommended". يُمكننا تعديل الإعدادات التي نريدها لاحقًا. يمكن أيضا تنزيل مجموعة قواعد نمط التكويد وتوسيعها بدلا من ذلك. انظر في الرابط http://eslint.org/docs/user-guide/getting-started للمزيد من التفاصيل حول كيفية التثبيت. تحتوي بعض المحررات على منقح صياغة مدمج فيها إلا أنها ليست بدقة ESLint. الخلاصة تهدف جميع قواعد بناء الجمل في هذا الفصل (وفي باقي مراجع أنماط التكويد) لرفع مستوى سهولة قراءة الأكواد. وجميع القواعد قابلة للنقاش. عند التفكير في كتابة شيفرة برمجية بشكل أفضل، يجب أن نسأل أنفسنا: "ما الذي يجعل الشيفرة البرمجية أسهل للقراءة والفهم؟" و "ما الذي قد يساعدنا لتجنب الأخطاء؟" يوجد العديد من الأشياء التي يجب الانتباه لها أثناء اختيار نمط تكويد معين. سيتيح لك قراءة العديد من أنماط التكويد معرفة أحدث الأفكار عن أنماط التكويد وأفضل الممارسات. تمارين نمط تكويد سيء الأهمية: 4 ما الخطأ في نمط التكويد أدناه؟ function pow(x,n) { let result=1; for(let i=0;i<n;i++) {result*=x;} return result; } let x=prompt("x?",''), n=prompt("n?",'') if (n<=0) { alert(`Power ${n} is not supported, please enter an integer number greater than zero`); } else { alert(pow(x,n)) } قم بإصلاحه. الحل يمكنك ملاحظة ما يلي: function pow(x,n) // <- لا يوجد مسافات بين المُعاملات { // <- قوس الفتح في سطر مستقل let result=1; // <- عدم وجود مسافات قبل أو بعد = for(let i=0;i<n;i++) {result*=x;} // <- لا يوجد مسافات // يجب أن يكون محتوى { ... } في سطر جديد return result; } let x=prompt("x?",''), n=prompt("n?",'') // <-- ممكنة تقنيا، // لكن يُفَضَّل جعلها في سطرين، بالإضافة إلى عدم وجود مسافات وعدم وجود ; // if (n<0) // <- لا يوجد مسافات بين (n < 0), ويجب وجود سطر فارغ قبلها { // <- قوس الفتح في سطر مستقل // يمكن فصل الأسطر الطويلة في الأسفل حتى تصبح سهلة القراءة alert(`Power ${n} is not supported, please enter an integer number greater than zero`); } else // <- يمكن كتابتها في سطر واحد هكذا: "} else {" { alert(pow(x,n)) // لا يوجد مسافات ولا يوجد ; } بعد تصحيح الأخطاء تصبح الشيفرة البرمجية كما يلي: function pow(x, n) { let result = 1; for (let i = 0; i < n; i++) { result *= x; } return result; } let x = prompt("x?", ""); let n = prompt("n?", ""); if (n < 0) { alert(`Power ${n} is not supported, please enter an integer number greater than zero`); } else { alert( pow(x, n) ); } ترجمة -وبتصرف- للفصل Coding Style من كتاب 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; } اقرأ أيضًا المقال التالي: التعليقات المقال السابق: تنقيح الأخطاء في Chrome
  6. كما نعلم من جزء بنية الشيفرة البرمجية، يمكن ان تكون التعليقات ذات سطر واحد: وتبدأ ب // أو متعددة الأسطر: /* ... */. تُستخدم التعليقات لوصف الغرض من الشيفرة البرمجية وآلية عملها. في أول مرة، قد تبدو التعليقات مهمة، لكن المبرمجين المبتدئين يستخدمونها بطريقة غير صحيحة. التعليقات السيئة يستخدم المبتدئون التعليقات لشرح "ماذا يحدث في الشيفرة البرمجية". كما يلي: // ستقوم هذه الشيفرة البرمجية بهذا الأمر (...) وذلك الأمر (...) // وربما أشياء أخرى very; complex; code; يجب أن يكون عدد التعليقات التوضيحية أقل في الشيفرات البرمجية الجيدة، لأن الشيفرة البرمجية يجب أن تكون مفهومة بدون تعليقات. يوجد قاعدة مهمة حول هذا الأمر:"إن كانت الشيفرة البرمجية غير مفهومة لدرجة أنها تحتاج إلى التعليقات لشرحها، يجب أن تُكتب من جديد بدلًا من التعليقات التوضيحية الكثيرة". طريقة: أخرج الدوال أحيانا، يكون استبدال جزء من الشيفرة البرمجية أمرًا مهمًا كما يلي: function showPrimes(n) { nextPrime: for (let i = 2; i < n; i++) { // check if i is a prime number for (let j = 2; j < i; j++) { if (i % j == 0) continue nextPrime; } alert(i); } } البديل الأفضل هو إخراج دالة isPrime: function showPrimes(n) { for (let i = 2; i < n; i++) { if (!isPrime(i)) continue; alert(i); } } function isPrime(n) { for (let i = 2; i < n; i++) { if (n % i == 0) return false; } return true; } بهذه الطريقة يمكننا فهم الشيفرة البرمجية بشكل أفضل لأن الدالة أصبحت بديلة عن التعليق. هذه الشيفرة البرمجية تسمى شيفرة برمجية موصوفة بحد ذاتها. طريقة: أنشئ دوالًا إن كان هناك شيفرة برمجية طويلة كما يلي: // here we add whiskey for(let i = 0; i < 10; i++) { let drop = getWhiskey(); smell(drop); add(drop, glass); } // here we add juice for(let t = 0; t < 3; t++) { let tomato = getTomato(); examine(tomato); let juice = press(tomato); add(juice, glass); } // ... فيُفضل إخراجه إلى دالة كما يلي: addWhiskey(glass); addJuice(glass); function addWhiskey(container) { for(let i = 0; i < 10; i++) { let drop = getWhiskey(); //... } } function addJuice(container) { for(let t = 0; t < 3; t++) { let tomato = getTomato(); //... } } مرة اخرى.. يجب أن يكون عمل الدالة واضحا من اسمها وموضعها. هكذا لن نحتاج إلى الكثير من التعليقات. وستكون بنية الشيفرة البرمجية أفضل عند تجزئتها. وسيصبح من الواضح ماهي آلية عمل الدالة، وماذا تستقبل وماذا تُخرِج. في الواقع لا يمكننا تجنب التعليقات التوضيحية. فهناك بعض الخوارزميات المعقدة وأدوات التعديل والتحسين الذكية (smart tweaks) للشيفرة البرمجية. لكن يجب أن نحاول أن نجعل الشيفرة البرمجية سهلا ويشرح نفسه. التعليقات الجيدة إن كانت التعليقات التوضيحية سيئة، فأي التعليقات هو الجيد؟ وصف الهيكلة يُعطي نظرة عامة عن المكونات، وطريقة تفاعلها، وطريقة تدفق التحكم في مختلف الحالات، وما إلى ذلك. باختصار، ما يُسمى بالنظرة الشاملة. يوجد لغة خاصة UML لبناء رسوم هيكلية عالية المستوى لشرح الشيفرة البرمجية. هذه اللغة تستحق التعلم فعلا. توثيق مُعامِلات الدوال واستخدامها يوجد تركيب خاص بالجمل البرمجية يُسمَّى JSDoc لتوثيق الدوال: استخدامها، مُعامِلاتها، والقيم التي تُرجِعُها. إليك المثال التالي: /** * Returns x raised to the n-th power. * * @param {number} x The number to raise. * @param {number} n The power, must be a natural number. * @return {number} x raised to the n-th power. */ function pow(x, n) { ... } تتيح لنا هذه التعليقات فهم الغرض من هذه الدالة واستخدامها مباشرة دون الحاجة للنظر إلى تفاصيل الشيفرة البرمجية فيها. يمكن للعديد من المُحررات فهم هذه التعليقات أيضا واستخدامها في الإكمال التلقائي وبعض حالات فحص الشيفرة التلقائي. يوجد أيضا بعض الأدوات مثل JSDoc 3 والتي يمكنها توليد توثيق HTML من هذه التعليقات. للمزيد من المعلومات حول JSDoc اقرأ هنا http://usejsdoc.org/. لم تم حل هذه المهمة بهذه الطريقة؟ ما هو مكتوب هو مهم، لكن قد يكون ما هو غير مكتوب أكثر أهمية لفهم ما يجري. لا تستطيع الشيفرة البرمجية الإجابة عن السؤال "لم تم حل هذه المهمة بهذه الطريقة؟". إن كان هناك العديد من الطرق لحل هذه المهمة. لم تم اختيار هذه الطريقة؟ خاصة عندما لا تكون هذه الطريقة هي الأفضل. بدون تعليقات توضح السبب يمكن حدوث التالي: ستفتح الشيفرة البرمجية المكتوبة منذ وقت طويل (أنت أو أي شخص آخر) وسترى أنها "غير مناسبة". ستفكر: "كم كنت غبيا حينها، وكم أنا ذكي الآن"، ثم ستُعيد كتابة الشيفرة البرمجية باستخدام البديل الذي تراه أكثر صحة وملائمة. فكرة إعادة الكتابة تكون سهلة، لكن عند القيام بها تكتشف أن الحل "الأمثل" لا يتناسب مع الغرض المطلوب. حينها قد تتذكر السبب لأنك جربت الأمر ذاته مسبقا بالفعل ولم يفلح. ثم ستعود إلى الخيار السابق لكن بعد ضياع الوقت. التعليقات التي توضح سبب استخدام طريقة ما هي مهمة جدا. فهي تساعد على الاستمرار بالتطوير مباشرة. أي ميزات غير ملحوظة في الشيفرة البرمجية ومكان استخدامها إن احتوت الشيفرة البرمجية على شيء غير ملحوظ أو شيء يُدرَك حدسيا، فيجب تعليقه. الخلاصة التعليقات هي علامة مهمة عن المطوِّر الجيد: وجودها أو عدم وجودها، إذ تتيح التعليقات الجيدة صيانة الشيفرة البرمجية جيدا، عند العودة لها بعد مدة من الوقت واستخدامها بكفاءة. علِّق ما يلي: الهيكلة العامة، النظرة عالية المستوى. استخدام الدوال. الحلول المهمة، خاصة تلك الغير ملحوظة مباشرة. تجنب التعليقات: التي تشرح آلية عمل الشيفرة البرمجية والغرض منها. ضع مثل هذه التعليقات عندما يكون من المستحيل جعل الشيفرة البرمجية سهلة القراءة وتصف نفسها وتحتاج لتوضيح فقط تستخدم التعليقات أيضا للأدوات التي تقوم بالتوثيق تلقائيا مثل JSDoc3: تقرأ هذه الأدوات التعليقات وتولِّد ملفات HTML أو بأي صيغة أخرى. ترجمة -وبتصرف- للفصل Comments من كتاب 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; } اقرأ أيضًا المقال التالي: شيفرة النينجا البرمجية المقال السابق: نمط كتابة الشيفرة
  7. استخدم مبرمجو النينجا القدماء هذه الحيل لشحذ عقول مراجعو الشيفرات البرمجية. يبحث معلمو مراجعة الشيفرات البرمجية عن هذه الحيل في مهام الاختبار. أحيانًا، يستخدم المبرمجون المبتدئون هذه الحيل بصورة أفضل من مبرمجي النينجا. اقرأها جيدا ثم حدد من أنت - نينجا، مبتدئ، أو مُراجِع شيفرات برمجية؟ البلاغة في الإيجاز اختصر الشيفرة البرمجية قدر الإمكان. أظهر مدى ذكائك. فلتَقُدكَ ميزات اللغة الخفية. مثال: ألق نظرة على العامل الثلاثي التالي '?': // مأخوذ من مكتبة جافاسكربت شهيرة i = i ? i < 0 ? Math.max(0, len + i) : i : 0; إن كتبت بهذه الطريقة، فإن أي مطور آخر يقرأ هذا السطر ويحاول فهم ما هي قيمة i سيأتي إليك باحثًا عن الإجابة. أخبرهم في ذلك الحين أن الاختصار أفضل دومًا ودلهم على طريق النينجا. المتغيرات ذات الحرف الواحد استخدام المتغيرات ذات الحرف الواحد هي طريقة أخرى لكتابة الشيفرة البرمجية بصورة أسرع مثل a، أو b، أو c. يختبئ المتغير ذو الحرف الواحد داخل الشيفرة البرمجية كما يختبئ النينجا الحقيقي في الغابة. لن يتمكن أي شخص من العثور عليه بسهولة باستخدام "البحث" المُدمج مع المحرر. حتى إن عثر شخص ما علبه فلن يفهم ما معنى المتغير a أو b. لكن، يوجد استثناء. مبرمج النينجا الحقيقي لن يستخدم المتغير i كعداد للحلقة "for". سيستخدمه في أي مكان عدا هنا. يوجد العديد من الأحرف الغريبة لاستخدامها كعداد الحلقة بدلا منه مثل x أو y. متغير بحرف غريب عدادًا للحلقة يُعد مناسبا إن كانت الحلقة تمتد إلى صفحة أو صفحتين. فإن أتى شخص ما للتعمق في هذه الحلقة فلن يعرف أن المتغير x هو عدَّاد هذه الحلقة. استخدم الاختصارات إن كانت قوانين الفريق تمنع استخدام المتغيرات ذات الحرف الواحد أو المتغيرات الغامضة، اختصر أسماء المتغيرات. list ‏← lst userAgent ‏← ua browser ‏← brsr وهكذا… سيفهم شخص فطن فقط الأسماء المستخدمة. حاول اختصار كل شيء. فقط شخص مؤهل يجب أن يُكمل تطوير شيفرتك البرمجية. حلق عاليا واستخدم التجريد حاول اختيار الكلمة الأكثر تجريدا عند اختيار اسم ما مثل obj، و data، و value، و item، و elem وهكذا. الاسم الأمثل لمتغير هو data. استخدمه حيثما استطعت. فبالحقيقة، كل متغير يحتوي على بيانات "data". لكن، ماذا إن كان هناك متغير بالاسم data بالفعل؟ جرب الاسم value، فهو عالمي أيضا، فكل متغير يحتوي على قيمة. سمِّ المتغير حسب نوعه: str، num… جرب هذه الطريقة. قد يتساءل أي شخص مبتدئ حول إن كانت مثل هذه الأسماء مفيدة لمبرمج النينجا؟ بالفعل هي مفيدة. اسم المتغير يُخبر عن شيء ما. يدل اسم المتغير في هذه الطريقة عن نوع البيانات التي يحويها: نص، رقم أو أي نوع آخر. لكن عندما يحاول شخص آخر فهم الشيفرة البرمجية، سيُصدَم بعدم توفر معلومات كافية لفهمها! وسيفشل تمامًا في تعديل شيفرتِك البرمجية. يمكن معرفة قيمة المتغير عبر تتبع الشيفرة البرمجية. لكن ما معنى المتغير؟ وماهو النص أو الرقم الذي يخزنة؟ لا يوجد طريقة لمعرفة ذلك بدون تأمل جيد! لكن ماذا إن لم يكن هناك المزيد من هذه الأسماء؟ فقط أضف رقمًا إليها مثل: data1، item2، elem5… اختبار الملاحظة يمكن لِمبرمج قوي الملاحظة فقط فهم شيفرتُك البرمجية. لكن، كيف تختبر ذلك؟ إحدى الطرق هي استخدام متغيرات بأسماء متشابهة مثل date و data. اخلُط ما بين المتغيرين حيث يمكن ذلك. ستصبح قراءة شيفرة برمجية بهذه الطريقة مستحيلة. وعند وجود خطأ كتابي فسيَعلق القارئ لوقت طويل. مرادفات ذكية على سبيل المثال، لنأخذ بالحسبان بوادِئ الدوال. إن كانت الدالة تعرض رسالة على الشاشة، ابدأها ب display…‎ مثل displayMessage. وإن كانت دالة أخرى تعرض شيئًا آخر على الشاشة مثل اسم المستخدم ابدأها ب show…‎ مثل showName. اجعل من يقرأ الشيفرة يظن أن هناك اختلاف خفي بين الدوال بينما لا يوجد أي اختلاف. اتفق مع فريق مبرمجي النينجا: إن بدأ أحمد دالة العرض ب display...‎ في شيفرته البرمجية، فيمكن لمحمد استخدام render...‎ ويمكن لِأمل استخدام paint...‎. لاحظ كيف ستصبح الشيفرة البرمجية أكثر اختلافًا وتشويقًا. الآن إلى خدعة القبعة! استخدم نفس البادئة ِلدالتين مهمتين بوظيفتين مختلفتين! مثلا، الدالة "printPage(page)‎" ستستخدم طابعة، بينما الدالة "printText(text)‎" ستضع النص على الشاشة. هكذا تجعل القارئ يفكر جيدا بالدالة المُسماة printMessage: "أين تضع هذه الدالة الرسالة؟ إلى الطابعة أو على الشاشة؟". لجعل الأمر أوضح، يجب أن تضع الدالة "printMessage(message)‎" الرسالة في النافذة الجديدة! (وظيفة مهمة بطريقة مختلفة). أعد استخدام الأسماء ضع متغيرًا جديدًا عند الحاجة فقط. عوضا عن ذلك، أعد استخدام الأسماء الموجودة. اكتب قيمًا جديدة إلى هذه المتغيرات. وفي الدوال، حاول استخدام المتغيرات المُمَرَّرة إلى الدالة كمُعاملات. ستُصعِّب بهذه الطريقة معرفة ما في المتغير الآن. ومن أين أتى هذا المتغير أيضا. الغرض من هذه الطريقة هو تطوير حدس وذاكرة قارِئ الشيفرة البرمجية. سيحتاج الشخص ذو الحدس الضعيف إلى تحليل الشيفرة البرمجية سطرًا تلو الاخر وتَعقَّب التغييرات في كل جزء من أجزاء الشيفرة البرمجية. إحدى الطرق المتقدمة هي باستبدال قيمة متغير ما بمحتوى آخر مختلف تماما عن السابق خفية داخل حلقة أو دالة. على سبيل المثال: function ninjaFunction(elem) { // elem عشرون سطرًا من الشيفرة يتعامل مع المتغير elem = clone(elem); // elem عشرون سطرًا إضافيًا يتعامل الآن مع نسخة من المتغير } سيتفاجئ المبرمج الآخر الذي يريد التعامل مع elem في الجزء الثاني من الدالة لأنه سيكتشف فقط أثناء التنفيذ والتعقب وبعد تفحص الشيفرة بأنه يتعامل مع elem بعد استنساخه! الشرطة السفلية للمتعة ضع الشرطات السفلية _ و __ قبل أسماء المتغيرات. مثل "‎_name" أو "‎__value". سيكون جيدا إن كنت أنت فقط من تعرف معنى ذلك، أو استخدمها للمتعة فقط دون معنى معين. أو استخدم معانٍ مختلفة في أماكن مختلفة. بهذا ترمي عصفورين بحجر. أولا، تصبح الشيفرة البرمجية أطول وأصعب للقراءة. ثانيا، سيأخذ القارئ وقتا لمعرفة معنى الشرطات السفلية. يضع مبرمج النينجا الذكي الشرطات السفلية في مكان معين من الشيفرة البرمجية ويتجنبها في أماكن أخرى. هذا يجعل الشيفرة البرمجية أكثر هشاشة ويزيد من احتمالية الخطأ المستقبلي. اظهر حبك أظهر للجميع مدى روعة المكونات التي تستخدمها! ستُبهِر الأسماء مثل superElement، megaFrame، و niceItem القارئ. يمكن كتابة: "super..‎"، و "mega..‎"، و"nice..‎" لكن بطريقة لا تُظهِر تفاصيلًا حول المعنى. قد يبحث القارئ عن معانِ خفية ويتأمل لساعة أو اثنتين من وقته. داخِل المتغيرات الخارجية استخدم أسماء المتغيرات نفسها داخل وخارج الدوال. ببساطة، لا حاجة لاختراع أسماء جديدة. let user = authenticateUser(); function render() { let user = anotherValue(); ... ...many lines... ... ... // <-- هنا user يريد مبرمجٌ ما التعامل مع المتغير ... } سيفشل المبرمج الذي يقفز بِداخل الدالة render في ملاحظة أن هناك متغير user محلي يغطي على المتغير الخارجي. سيتعامل مع user مُعتبِرًا أنه المتغير الخارجي نفسه، وبذلك ستكون نتائج الدالة "authenticateUser()‎" غير متوقعة وسينجح الفخ. آثار جانبية في كل مكان يوجد بعض الدوال التي تبدو كأنها لا تقوم بشيء مثل، "isReady()‎"، و "checkPermission()‎"، و "findTags()‎" …إلخ يُتَوَقَّع أنها تقوم بعمليات حسابية، تُوجِد وتٌرجِع قيم البيانات بدون تغيير شيء خارج نطاقها. بمعنى آخر، بدون آثار جانبية. إضافة حدث مفيد إليها سيكون خدعة جيدة، بجانب مهمتها الرئيسية. بالتأكيد، ستوسِّع نظرة الدهشة التي على وجه قارئ الشيفرة -عندما يرى دالة بالاسم "is..‎"، أو "check..‎"، أو "find..‎" وتُغَيِّر شيئا ما - آفاق المنطق لديك. طريقة أخرى هي إرجاع نتيجة غير قياسية. أظهر ذكائك! اجعل الدالة checkPermission تُرجِع كائنًا معقدًا يحتوي على نتيجة الفحص المُراد من الدالة. سَيتساءل المطورون الذين سيكتبون "if (checkPermission(..))‎" عن سبب عدم عملها. حينها أخبرهم أن يقرأوا التوثيق ووجههم لهذا المقال :). دوال قوية! لا تجعل الدالة محدودة بما يتضمنه اسمها. توسَّع أكثر. على سبيل المثال، يمكن للدالة "validateEmail(email)‎" (بالإضافة إلى فحص صحة البريد الالكتروني) عرض رسالة خطأ وطلب إعادة إدخال البريد الإلكتروني. لا يجب أن تكون باقي الوظائف واضحة من مسمى الدالة. مبرمج النينجا الحقيقي لن يجعل هذه الوظائف واضحة حتى في الشيفرة البرمجية. الخلاصة جميع الملاحظات السابقة هي من شيفرات برمجية حقيقية مكتوبة بواسطة مبرمجين محترفين، ربما أكثر احترافية منك اتبع بعض الملاحظات وستكون شيفرتك البرمجية مليئة بالمفاجآت. اتَّبِع اغلبها وستكون شيفرتك البرمجية حقا ملك لك أنت فقط، لن يريد أي شخص تغييرها. اتَّبِع جميعها وستصبح شيفرتك البرمجية درسًا قيمَا للمطورين المبتدئين الباحثين عن إرشاد. ترجمة -وبتصرف- للفصل Ninja code من كتاب 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; } اقرأ أيضًا المقال التالي: الاختبار الآلي باستخدام mocha المقال السابق: التعليقات
  8. يُستخدَم الاختبار الآلي في الكثير من المهام، كما يستخدم بكثرة في المشاريع الحقيقية. لم نحتاج الاختبارات؟ عند كتابة دالة، يمكننا تخيل ما يجب أن تقوم به: ما هي المعاملات التي تعطي نتائج معينة. يمكننا فحص الدالة أثناء التطوير من خلال تشغيلها وموازنة مخرجاتها مع ما هو متوقع. مثلا يمكننا القيام بذلك في الطرفية. إن كان هناك خطأ، فإننا نُصلِح الشيفرة البرمجية، ونُعيد تشغيلها، ونفحص النتائج. وهكذا حتى تصبح صحيحة. لكن هذه الطريقة «إعادة التشغيل» غير مثالية. عند اختبار شيفرة برمجية عن طريق إعادة التشغيل اليدوية، فمن السهل نسيان شيئٍ ما. على سبيل المثال، عند إنشاء الدالة f. نكتب فيها بعض الشيفرات البرمجية، ثم نَفحصها: "f(1)‎" تعمل لكن "f(2)‎" لا تعمل. صلِح الشيفرة حتى تعمل "f(2)‎". ثم تبدو الدالة كأنها مكتملة، لكننا ننسى إعادة اختبار "f(1)‎" مما قد يؤدي إلى خطأ. هذا الأمر وارد بكثرة. فعند تطوير أي شيء، نُبقِي العديد من الاحتمالات في الحسبان. لكنه من الصعب توقع أن يختبر المبرمج جميع هذه الحالات يدويا بعد كل تغيير، فيصبح من السهل إصلاح شيء ما وإفساد شيء آخر. يعني الاختبار الآلي أن الاختبارات تُكتب مستقلة، بالإضافة إلى الشيفرة البرمجية. تشغِّل هذه الاختبارات الدوال بعدة طرق وتوازنها مع النتائج المتوقعة. التطوير المستند إلى السلوك لنبدأ بتقنية تسمى التطوير المستند إلى السلوك Behavior Driven Development أو كاختصار BDD. هذه التقنية BDD هي 3 في 1: اختبارات وتوثيق وامثلة. سنجرب حالة تطوير عملية لفهم BDD. تطوير الدالة "pow": الوصف لنفترض أننا نريد إنشاء الدالة pow(x, n)‎ التي ترفع الأساس x إلى القوة n. مع الأخذ بالحسبان أن n≥0. هذه المهمة هي مجرد مثال: المعامل ** يقوم بهذه العملية في JavaScript، لكننا نركز هنا على تدفق التطوير الذي يمكن تطبيقه على مهام أكثر تعقيدا. يمكننا تخيل ووصف ما يجب أن تقوم به الدالة pow قبل إنشاء شيفرتِها البرمجية. هذا الوصف يُسمى "specification" أو باختصار "spec" ويحتوي على وصف حالات الاستخدام بالإضافة إلى اختبارات لهذه الحالات كالتالي: describe("pow", function() { it("raises to n-th power", function() { assert.equal(pow(2, 3), 8); }); }); تحتوي المواصفات على 3 أجزاء رئيسية كما ترى في الأعلى: describe("title", function() { ... }) ماهي الوظيفة التي نصفها، في هذه الحالة، نحن نصف الدالة pow. تستخدم بواسطة العاملين- أجزاء it. it("use case description", function() { ... }) نصف (نحن بطريقة مقروءة للبشر) حالة الاستخدام المخصصة في عنوان it، والمعامل الآخر عبارة عن دالة تفحص هذه الدالة. assert.equal(value1, value2) الشيفرة البرمجية بداخل it يجب أن تُنَفَّذ بدون أخطاء في حال كان التنفيذ صحيحًا. تستخدم الدوال assert.* لِفحص ما إن كانت الدالة pow تعمل بالشكل المتوقع أم لا. تستخدم إحدى هذه الدوال في هذا المثال - assert.equal، والتي توازن معاملَين وتُرجِع خطأ في حال عدم تساويهما. في المثال تفحص هذه الدالة إن كانت نتيجة تنفيذ الدالة pow(2, 3)‎ تساوي 8. كما يوجد العديد من أنواع التحقق والمقارنة والتي سنُضيفها لاحقا. يمكن تنفيذ الوصف، وسينفِّذ الفحص الموجود بداخل it كما سنرى لاحقا. تدفق التطوير يبدو تدفق التطوير غالبا كما يلي: يُكتب الوصف الأولي مع فحص للوظيفة الرئيسية. يُنشَئ تنفيذ أولي. للتأكد من صحة عمل التنفيذ، نُشَغِّل إطار التقييم Mocha الذي يُشَغِّل الوصف. ستظهر أخطاء في حال عدم اكتمال الوظائف. نُصحح الأخطاء حتى يصبح كل شيء صحيحًا. هكذا أصبح لدينا تنفيذ مبدئي يعمل كالمطلوب بالإضافة إلى فحصه. نضيف المزيد من حالات الاستخدام للوصف، ربما بعض هذه الميزات ليس مضمنا في التنفيذ بعد. حينها يبدأ الاختبار بالفشل. عُد للخطوة 3 وحدِّث التنفيذ إلى أن تختفي كل الأخطاء. كرر الخطوات 3-6 حتى تجهز كل الوظائف. إذا، تُعد عملية التطوير تكرارية. نكتب الوصف، ننفذه، نتأكد من اجتياز التنفيذ للفحص، ثم نكتب المزيد من الاختبارات، نتأكد من صحة عملها. حتى نحصل على تنفيذ صحيح مع اختباراته في الأخير. لنُجرب تدفق التطوير هذا على حالتنا العملية. الخطوة 1 أصبحت جاهزة: لدينا وصفًا مبدئيًّا للدالة pow. الآن وقبل التنفيذ، لِنستخدم بعض مكاتب جافاسكربت لتشغيل الاختبار حتى نتأكد من إن كانت تعمل (لن تعمل). المواصفات أثناء التنفيذ سنستخدم في هذا الشرح مكاتب جافاسكربت التالية للاختبار: Mocha - الإطار الرئيسي: يوفر دوال الفحص الأكثر استخدامًا ما يشمل describe و it بالإضافة إلى الدوال الرئيسية التي تُشَغِّل الاختبار. Chai - المكتبة المحتوية على دوال تأكيدية. تتيح لنا استخدام العديد من هذه الدوال، نحتاج الآن assert.equal فقط. Sinon - مكتبة للتجسس على الدوال، ومحاكاة الدوال المدمجة، والمزيد؛ سنحتاج هذه المكتبة لاحقا. تُعد هذه المكاتب مفيدة للاختبار في كل من المتصفح والخادم. سنأخذ بعين الاعتبار هنا جهة المتصفح. صفحة HTML كاملة مع هذه المكاتب ووصف الدالة pow: <!DOCTYPE html> <html> <head> <!-- add mocha css, to show results --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.css"> <!-- add mocha framework code --> <script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.js"></script> <script> mocha.setup('bdd'); // minimal setup </script> <!-- add chai --> <script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.5.0/chai.js"></script> <script> // chai has a lot of stuff, let's make assert global let assert = chai.assert; </script> </head> <body> <script> function pow(x, n) { /* function code is to be written, empty now */ } </script> <!-- the script with tests (describe, it...) --> <script src="test.js"></script> <!-- the element with id="mocha" will contain test results --> <div id="mocha"></div> <!-- run tests! --> <script> mocha.run(); </script> </body> </html> يمكن تقسيم الصفحة إلى 5 أجزاء: <head> - لإضافة مكاتب وأنماط للاختبارات. <script> يحتوي على الدالة التي سيتم اختبارها، في هذا المثال الشيفرة البرمجية للدالة pow. الاختبار - عبارة عن سكريبت خارجي في هذا المثال test.js يحتوي على describe("pow", ...)‎ الموضح سابقا. عنصر HTML ‏<div id="mocha"‎> والذي سيُستخدم بواسطة Mocha لعرض النتائج. يتم بدء الاختبارات باستخدام الأمر mocha.run()‎. النتائج: يفشل الاختبار ويظهر خطأ في الوقت الحالي. يُعد هذا منطقيا: فالدالة pow ما زالت فارغة، فإن pow(2,3)‎ تُرجع undefined بدلا من 8. لاحظ أن بإمكانك تشغيل أكثر من اختبار مختلف مستقبلا، فهناك مُشغلات عالية المستوى مثل karma وغيرها. التنفيذ الأولي لنقم بتنفيذ مبسط للدالة pow حتى تعمل الاختبارات: function pow(x, n) { return 8; // :) we cheat! } الآن ستعمل… تطوير الوصف ما قمنا به هو غش فقط، لا تعمل الدالة كالمطلوب: إن قمنا بحساب pow(3,4)‎ فسنحصل على قيمة غير صحيحة، لكنها ستجتاز الاختبارات. هذه الحالة غير عملية، وتحدث بكثرة. الدالة تتجاوز الاختبارات لكن آلية عملها خاطئة. هذا يعني أن الوصف غير مثالي. ونحتاج لإضافة المزيد من حالات استخدام الدالة إليه. لنضف اختبارًا آخر للتأكد ما إن كان pow(3, 4) = 81. يمكنُنا اختيار إحدى الطريقتين لتنظيم الاختبارات: الخيار الأول - إضافة assert إلى it: describe("pow", function() { it("raises to n-th power", function() { assert.equal(pow(2, 3), 8); assert.equal(pow(3, 4), 81); }); }); الخيار الآخر - عمل اختبارين: describe("pow", function() { it("2 raised to power 3 is 8", function() { assert.equal(pow(2, 3), 8); }); it("3 raised to power 3 is 27", function() { assert.equal(pow(3, 3), 27); }); }); يختلف المبدئ في أنه عند وجود خطأ في assert، فإن it تتوقف عن العمل. لذا ففي الخيار الأول عند فشل assert الأولى فلن نرى مخرجات assert الأخرى. يُعد الخيار الثاني أفضل للحصول على المزيد من المعلومات حول ما يحدث بعمل اختبارين منفصلين. بالإضافة إلى وجود قاعدة أخرى من الجيد اتباعها. كل اختبار يفحص شيئًا واحدًا فقط. إن وجدنا اختبارًا يفحص شيئين مختلفين فمن الأفضل فصلُهما إلى اختبارين. لنكمل باستخدام الخيار الثاني. النتائج: سيفشل الاختبار الثاني كَالمتوقع. فَالدالة تُرجع دوما 8، بينما تتوقع الدالة assert النتيجة 27. تطوير التنفيذ لنكتب شيئا أكثر واقعية لاجتياز الاختبارات: function pow(x, n) { let result = 1; for (let i = 0; i < n; i++) { result *= x; } return result; } للتأكد من صحة عمل الدالة K نختبرها لأكثر من قيمة. يمكننا القيام بذلك باستخدام الحلقة for بدلا من تكرار it يدويا: describe("pow", function() { function makeTest(x) { let expected = x * x * x; it(`${x} in the power 3 is ${expected}`, function() { assert.equal(pow(x, 3), expected); }); } for (let x = 1; x <= 5; x++) { makeTest(x); } }); النتيجة: دالة describe متداخلة سنضيف المزيد من الاختبارات، لكن، قبل ذلك لاحظ أنه يجب جمع الدالة المساعدة makeTest والدالة for. لن نَحتاج لاستخدام makeTest في باقي الاختبارات، نحتاجها فقط في for: لأن وظيفتهُما العامة هي فحص كيف ترفع الدالة pow قيمة ما إلى قوة معينة. يتم جمع الدالتين باستخدام الدالة describe المتداخلة: describe("pow", function() { describe("raises x to power 3", function() { function makeTest(x) { let expected = x * x * x; it(`${x} in the power 3 is ${expected}`, function() { assert.equal(pow(x, 3), expected); }); } for (let x = 1; x <= 5; x++) { makeTest(x); } }); // ... more tests to follow here, both describe and it can be added }); تُعرِّف describe الداخلية مجموعة فرعية جديدة من الاختبارات. يمكن ملاحظة الإزاحة في المخرجات: يمكننا إضافة المزيد من دوال it و describe في الطبقة العلوية مع دوال مساعدة لها، هذه الدوال لن ترى الدالة makeTest. before/after و beforeEach/afterEach يمكننا إعداد دوال before/after التي تُنَفَّذ قبل/بعد تنفيذ الاختبارات، كما يمكننا استخدام beforeEach/afterEach قبل/بعد كل it. على سبيل المثال: describe("test", function() { before(() => alert("Testing started – before all tests")); after(() => alert("Testing finished – after all tests")); beforeEach(() => alert("Before a test – enter a test")); afterEach(() => alert("After a test – exit a test")); it('test 1', () => alert(1)); it('test 2', () => alert(2)); }); تسلسل التنفيذ سيكون كالتالي: Testing started – before all tests (before) Before a test – enter a test (beforeEach) 1 After a test – exit a test (afterEach) Before a test – enter a test (beforeEach) 2 After a test – exit a test (afterEach) Testing finished – after all tests (after) تستخدم beforeEach/afterEach و before/after غالبا لتنفيذ الخطوات الأولية، العدادات التي تُخرج 0 أو للقيام بشيء ما بين الاختبارات أو مجموعة اختبارات. توسيع الوصف أصبحت الوظيفة الرئيسية للدالة pow مكتملة. تم تجهيز الدورة الأولى من التطوير. الآن يمكننا الاحتفال بالانتهاء من ذلك وبدء تطوير الدالة. كما ذكرنا مسبقا، فإن الدالة pow(x, n)‎ ستتعامل مع أرقام موجبة فقط n. تُرجع دوال جافاسكربت دائما NaN عند وجود خطأ حسابي. لنقم بهذا الأمر مع قيم n. أولا، نضيف هذا إلى الوصف: describe("pow", function() { // ... it("for negative n the result is NaN", function() { assert.isNaN(pow(2, -1)); }); it("for non-integer n the result is NaN", function() { assert.isNaN(pow(2, 1.5)); }); }); النتائج مع الاختبارات الجديدة: تفشل الاختبارات المُضافة مؤخرا وذلك لأن التنفيذ لا يدعمها. هكذا هي الطريقة التي يعمل بها BDD: نبدأ بكتابة الاختبارات التي نعلم بأنها ستفشل ثم نكتب التنفيذ الخاص بها. دوال تأكيد أخرى لاحظ أن الدالة assert.isNaN: تفحص وجود القيمة NaN. يوجد المزيد من دوال التأكيد في Chai، مثلا: assert.equal(value1, value2)‎ - تفحص التساوي value1 == value2. assert.strictEqual(value1, value2)‎ - تفحص التساوي التام value1 === value2. assert.notEqual، assert.notStrictEqual - تفحص عكس الدالتين أعلاه. assert.isTrue(value)‎ - تفحص أن value === true. assert.isFalse(value)‎ - تفحص أن value === false. يمكنك قراءة باقي الدوال في التوثيق لذا، يجب أن نضيف بعض الأسطر للدالة pow: function pow(x, n) { if (n < 0) return NaN; if (Math.round(n) != n) return NaN; let result = 1; for (let i = 0; i < n; i++) { result *= x; } return result; } الآن أصبحت تعمل وتَجتاز جميع الاختبارات: الخلاصة يكون الوصف في البداية في BDD، ثم يأتي التنفيذ. فنحصل في الأخير على كل من الوصف والشيفرة البرمجية. يمكن استخدام الوصف بثلاث طرق: اختبارات - تضمن صحة عمل الشيفرة البرمجية. توثيق - توضح العناوين في describe وit وظيفة الدالة. أمثلة - تُعد الاختبارات أمثلة فعالة تعرض كيف يمكن استخدام الدالة. يمكننا تطوير، تغيير، أو إعادة كتابة دالة من الصفر من خلال الوصف مع ضمان صحة عملها. يعد هذا الأمر مهمًا خاصة في المشاريع الكبيرة عند استخدام دالة في عدة أماكن. فعند تغيير هذه الدالة، يكون من الصعب التحقق من صحة عملها يدويًا في كل مكان. يوجد خيارين بدون اختبارات: تنفيذ التغيير بغض النظر عن النتائج. هكذا قد نواجه الكثير من الأخطاء عند الفحص اليدوي. يتجنب المطورون تحديث الشيفرة عند توقع حدوث أخطاء فادحة وعدم وجود اختبارات لفحص هذه الأخطاء فتبقى الشيفرة البرمجية بدون تحديث. يساعد الفحص الآلي على تجنب هذه المشاكل! إن كان المشروع مليئا بالاختبارات، فلن يكون هناك أي مشكلة إطلاقا. فبعد أي تغيير يمكننا تنفيذ الاختبارات لنرى العديد من التحقيقات أجريت في غضون ثوانٍ. علاوة على ذلك، الشيفرة البرمجية المختبرة جيدا تكون بهيكل أفضل. منطقيا، لأن الشيفرة البرمجية المختبرة سهلة التعديل والتطوير. مع وجود سبب آخر أيضا. يجب أن تكون الشيفرة البرمجية منظمة من أجل كتابة اختبار بحيث يكون لدى كل دالة وظيفة رئيسية موصوفة، ومدخلات ومخرجات معروفة جيدا. ذلك يعني 1 هيكلة جيدة منذ البداية. لا يكون الأمر بهذه السهولة في الواقع. ففي بعض الأحيان يكون من الصعب كتابة وصف للدالة قبل شيفرتها البرمجية لأن سلوكها لا يكون واضحًا بعد. لكن عموما، كتابة الاختبارات يجعل التطوير أسرع وأكثر استقرارًا. ستواجه الكثير من المهام التي تتضمن ذلك لاحقا في الشرح. فسترى المزيد من الأمثلة العملية. يتطلب كتابة الاختبارات معرفة جيدة بِجافاسكربت. لكننا في بداية تعلمها. فلترتيب كل شيء، لست مطالبًا بكتابة الاختبارات في الوقت الحالي، لكن يجب أن تكون قادرًا على قرائتها حتى إن كانت معقدة أكثر قليلا مما تم شرحه هنا. تمارين ما الخطأ في الاختبار التالي؟ الأهمية: 5 ما الخطأ في الاختبار للدالة pow أدناه؟ it("Raises x to the power n", function() { let x = 5; let result = x; assert.equal(pow(x, 1), result); result *= x; assert.equal(pow(x, 2), result); result *= x; assert.equal(pow(x, 3), result); }); الحل: يوضح الاختبار أحد الإغراءات التي يواجهها المطور أثناء كتابة اختبار. ما لدينا الآن هو ثلاثة اختبارات، لكنها مرتبة كدالة واحدة تتضمن ثلاثة تأكيدات. تكون هذه الطريقة أسهل في بعض الأحيان، لكن إن وُجِد أي خطأ، فلن يكون من السهل معرفة سبب ذلك الخطأ. إن حدث خطأ ما أثناء تنفيذ شيفرة معقدة، فَسنحتاج لمعرفة قيم البيانات عند تلك النقطة. أي أننا سنحتاج لتنقيح الاختبار. من الأفضل تجزئة الاختبار إلى أجزاء it متعددة مع مدخلات ومخرجات محددة بوضوح كما يلي: describe("Raises x to power n", function() { it("5 in the power of 1 equals 5", function() { assert.equal(pow(5, 1), 5); }); it("5 in the power of 2 equals 25", function() { assert.equal(pow(5, 2), 25); }); it("5 in the power of 3 equals 125", function() { assert.equal(pow(5, 3), 125); }); }); بدلنا it مكان describe وبعدد من it. إن فشل شيء ما الآن فَسَنتمكن من رؤية البيانات. يمكننا أيضًا عزل اختبار واحد وتشغيلة في وضع مستقل باستخدام it.only بدلًا من it: describe("Raises x to power n", function() { it("5 in the power of 1 equals 5", function() { assert.equal(pow(5, 1), 5); }); // Mocha will run only this block it.only("5 in the power of 2 equals 25", function() { assert.equal(pow(5, 2), 25); }); it("5 in the power of 3 equals 125", function() { assert.equal(pow(5, 3), 125); }); }); ترجمة -وبتصرف- للفصل Automated testing with mocha من كتاب 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; } اقرأ أيضًا المقال التالي: تعويض نقص دعم المتصفحات لجافاسكربت المقال السابق: شيفرة النينجا البرمجية
  9. يوجد سبعة أنواع للبيانات في JavaScript كما رأينا في فصل أنواع البيانات. ستة من هذه الأنواع تُدعى "أساسية" (primitive) لأنها تحوي قيمة شيء واحد فقط (سواء كان نصًا أو رقمًا أو أي شيء آخر). في المُقابل، تُستخدم الكائنات لتخزين مجموعة من البيانات المتنوعة والوحدات المعقدة المُرَمَّزة بمفاتيح. تُضمَّن الكائنات في ما يقارب جميع نواحي JavaScript، لذا يجب علينا أن نفهمها قبل التعمق في أي شيء آخر. يمكن إنشاء أي كائن باستخدام الأقواس المعقوصة {…} مع قائمة اختيارية بالخاصيات. الخاصية هي زوج من "مفتاح: قيمة" (key: value) إذ يكون المفتاح عبارة عن نص (يُدعى "اسم الخاصية")، والقيمة يمكن أن تكون أي شيء. يمكننا تخيل الكائن كخزانة تحوي ملفات. يُخزن كل جزء من هذه البيانات في الملف الخاص به باستخدام المفتاح. يمكن إيجاد، أو إضافة، أو حذف ملف باستخدام اسمه. يمكن إنشاء كائن فارغ (خزانة فارغة) باستخدام إحدى الصيغتين التاليتين: let user = new Object(); // (object constructor) صياغة باني كائن let user = {}; // (object literal) صياغة مختصرة لكائن عبر الأقواس تُستخدم الأقواس المعقوصة {...} عادة، وهذا النوع من التصريح يُسمى «الصياغة المختصرة لتعريف كائن» (object literal). القيم المُجرَّدة والخاصيات يمكننا إضافة بعض الخاصيات (properties) إلى الكائن المعرَّف بالأقواس {...} مباشرة بشكل أزواج "مفتاح: قيمة": let user = { // كائن name: "John", // name عبر المفتاح John خزِّن القيمة age: 30 // age خزِّن القيمة 30 عبر المفتاح }; لدى كل خاصية مفتاح (يُدعى أيضًا "اسم " أو "مُعَرِّف") قبل النقطتين ":" وقيمة لهذه الخاصية بعد النقطتين. يوجد خاصيتين في الكائن user: اسم الخاصية الأولى هو "name" وقيمتها هي "John". اسم الخاصية الثانية هو "age" وقيمتها هي "30". يمكن تخيل الكائن السابق user كخزانة بملفين مُسَمَّيان "name" و "age". يمكننا إضافة، وحذف، وقراءة الملفات من الخزانة في أي وقت. يمكن الوصول إلى قيم الخاصيات باستخدام الصيغة النُقَطية (dot notation): // الحصول على قيم خاصيات الكائن: alert( user.name ); // John alert( user.age ); // 30 يمكن للقيمة أن تكون من أي نوع، لِنُضِف قيمة من نوع بيانات منطقية (boolean): user.isAdmin = true; يمكننا استخدام المُعامِل delete لحذف خاصية: delete user.age; يمكننا أيضا استخدام خاصيات بأسماء تحوي أكثر من كلمة، لكن يجب وضعها بين علامات الاقتباس "": let user = { name: "John", age: 30, "likes birds": true // يجب أن تكون الخاصية ذات الاسم المُحتوي على أكثر من كلمة بين علامتي اقتباس }; يمكن إضافة فاصلة بعد آخر خاصية في القائمة: let user = { name: "John", age: 30, } وتُسمى بفاصلة "مُعَلِّقَة" أو "زائدة" فهي تجعل إضافة أو حذف أو التنقل بين الخاصيات أسهل لأن جميع الأسطر تُصبِح متشابهة. الأقواس المعقوفة لا تعمل طريقة الوصول إلى الخاصيات ذات الأسماء المحتوية على أكثر من كلمة باستخدام الصيغة النُقَطية: // تعرض هذه التعليمة وجود خطأ في الصياغة user.likes birds = true ذلك لأن الصيغة النُقطية تحتاج لاسم متغير صحيح. لا يحوي مسافات أو حدود أخرى. يوجد بديل يعمل مع أي نص "صيغة الأقواس المعقوفة" []: let user = {}; // تخزين user["likes birds"] = true; // استرجاع alert(user["likes birds"]); // true // حذف delete user["likes birds"]; يعمل كل شيء وفق المطلوب الآن. يُرجى ملاحظة أنَّ النص بداخل الأقواس مُحاط بعلامتي اقتباس (تعمل علامات التنصيص الأخرى بطريقة صحيحة أيضًا). تتيح لنا الأقواس المعقوفة أيضًا جلب اسم خاصية ناتجة عن قيمة أي تعبير - بدلًا من اسم الخاصية الفعلي - مثل استعمال اسم من متغير كما يلي: let key = "likes birds"; // user["likes birds"] = true; يماثل قول user[key] = true; يمكن حساب قيمة المتغير key أثناء التنفيذ أو قد تعتمد قيمته على مدخلات المستخدمين ثم نستخدمه للوصول إلى الخاصية مما يعطي مرونة كبيرة في التعامل. إليك المثال التالي: let user = { name: "John", age: 30 }; let key = prompt("What do you want to know about the user?", "name"); // الوصول باستخدام متغير alert( user[key] ); // John (if enter "name") لا يمكن استخدام الصيغة النُقطية بالطريقة نفسها: let user = { name: "John", age: 30 }; let key = "name"; alert( user.key ) // غير معروف الخاصيات المحسوبة يمكن استخدام الأقواس المعقوفة في كائن معرَّف بالأقواس، وهذا ما يسمى بالخاصيات المحسوبة. إليك المثال التالي: let fruit = prompt("Which fruit to buy?", "apple"); let bag = { [fruit]: 5, // fruit يؤخذ اسم الخاصية من المتغير }; alert( bag.apple ); // fruit="apple" قيمتها 5 إذا كانت معنى الخاصية المحسوبة سهل: تعني [fruit] أنَّ اسم الخاصية يجب أن يُؤخذ من fruit؛ لذا، إن أدخل الزائر "apple"، ستصبح قيمة bag هي {apple: 5}. يعمل الأمر السابق بالطريقة التالية ذاتها: let fruit = prompt("Which fruit to buy?", "apple"); let bag = {}; // fruit خُذ اسم الخاصية من المتغير bag[fruit] = 5; لكن شكله يبدو أفضل، أليس كذلك؟! يمكن استخدام تعابير أكثر تعقيدًا داخل الأقواس المعقوفة: let fruit = 'apple'; let bag = { [fruit + 'Computers']: 5 // bag.appleComputers = 5 }; الأقواس المعقوفة أكثر قوة من الصيغة النُقطية. فهي تسمح باستخدام أي اسم للخاصيات والمتغيرات. لكنها أكثر تعقيدا في الكتابة. لذا، إن كانت أسماء المتغيرات سهلة ومعروفة، تُستخدَم الصيغة النُقَطية غالبًا. وإن أردنا بعض التعقيد، نستخدم إلى الأقواس المعقوفة. يمكن استخدام الأسماء المحجوزة مع أسماء الخاصيات لا يمكن لمتغير أن يحمل اسم إحدى الكلمات المحجوزة في اللغة مثل "for"، أو "let"، أو "return"، …الخ. لكن لا تُطبَّق هذه القاعدة على أسماء خاصيات الكائنات. فيمكن استخدام أي اسم معها: let obj = { for: 1, let: 2, return: 3 }; alert( obj.for + obj.let + obj.return ); // 6 عمومًا، يمكن استخدام أي اسم، لكن هناك استثناء: "__proto__" لهذا الاسم معاملة خاصة لأسباب تاريخية. مثلًا، لا يمكننا استخدام الاسم على أنَّه قيمة لغير كائن: let obj = {}; obj.__proto__ = 5; alert(obj.__proto__); // لا تعمل وفق المطلوب [object Object] كما نرى في الشيفرة، تم تجاهل تخزين القيمة الأولية 5. تخزين أزواج الخاصيات التحكمية في كائن وإتاحة تحديد مفاتيح هذه الأزواج للزائر قد يجعل الشيفرة مصدرًا للأخطاء ومليئًا بالثغرات. في تلك الحالة، قد يختار الزائر - الخبير والماكر - الاسم __proto__ ليكون مفتاحًا ويخرِّب البنية المنطقية للشيفرة (كما في المثال أعلاه). يوجد طريقة لجعل الكائنات تتعامل مع __proto__ بِعدِّها خاصية عادية وسنتطرق لها لاحقًا بعد فهم الكائنات بشكل أعمق. يوجد أيضا هيكل بيانات آخر يدعى Map، والذي ستتعلمه في القسم الذي يتحدث عن نوعي البيانات Map و Set، اللذين يدعمان استعمال أي نوع مع المفاتيح. اختزال قيم الخاصيات نستخدم غالبا قيم متغيرات موجودة مسبقًا لتكون قيمًا لأسماء الخاصيات في الشيفرات الحقيقية. اطلع مثلًا على الشيفرة التالية: function makeUser(name, age) { return { name: name, age: age // ... خاصيات أخرى }; } let user = makeUser("John", 30); alert(user.name); // John تحمل الخاصيات في المثال السابق نفس أسماء المتغيرات. يُعدُّ إنشاء خاصية من متغير موجود مسبقًا حالة استخدام شائعة. أي أنه يوجد اختزال خاص لقيمة الخاصية لاختصار الشيفرة. بدلا من كتابة name:name، يمكننا كتابة name فقط، كما يلي: function makeUser(name, age) { return { name, // name: name يماثل كتابة age // age: age يماثل كتابة // ... }; } يمكننا استخدام كلًا من الخاصيات الاعتيادية والاختزال في الكائن ذاته: let user = { name, // name:name يماثل age: 30 }; فحص الكينونة قابلية الوصول إلى أي خاصية في الكائن هي إحدى مميزات الكائنات، ولكن ألَا يوجد أي خطأ في حال لم تكن الخاصية موجودة؟! عند محاولة الوصول إلى خاصية غير موجودة، تُرجَع القيمة undefined. مما يُعطي طريقة متعارفة لفحص كينونة (وجود) خاصية ما من عدمه بموازنتها مع القيمة "undefined" ببساطة: let user = {}; alert( user.noSuchProperty === undefined ); // تحقق هذه الموازنة يشير إلى عدم وجود الخاصية يوجد أيضا مُعامل خاص "in" لفحص تواجد أي خاصية. طريقة استخدام هذا المعامل كالتالي: "key" in object مثلا: let user = { name: "John", age: 30 }; alert( "age" in user ); // true, user.age موجود alert( "blabla" in user ); // false, user.blabla غير موجود لاحظ أنه من الضروري وجود اسم الخاصية على يسار in. ويكون عادة نصًا بين علامتي تنصيص. إن لم نستخدم علامتي التنصيص فهذا يعني فحص متغير يحمل الاسم ذاته مثل: let user = { age: 30 }; let key = "age"; alert( key in user ); // key إذ تؤخذ قيمة المتغير true تطبع القيمة // user ويُتحقق من وجود خاصية بذلك الاسم في الكائن استخدام "in" مع الخاصيات التي تُخزن القيمة undefined تفحص عملية الموازنة الصارمة "=== undefined" غالبًا وجود الخاصية وفق المطلوب. لكن يوجد حالة خاصة تفشل فيها هذه العملية، بينما لا يفشل المعامل in إن استعمل مكانها. هذه الحالة هي عند وجود الخاصية في الكائن لكنها تُخزن القيمة undefined: let obj = { test: undefined }; alert( obj.test ); // ولكن هل تُعدُّ الخاصية موجودة أم لا؟ undefined يطبع القيمة alert( "test" in obj ); // وتَعدُّ الخاصية موجودة في الكائن true تُطبع القيمة الخاصية obj.test موجودة فعليًا في الشيفرة أعلاه، لذا يعمل المُعامل in بصحة. تحدث مثل هذه الحالات نادرًا فقط لأن القيمة undefined لا تُستخدَم بكثرة. نستخدم غالبا القيمة null للقيم الفارغة أو الغير معرفة، لذلك يُعد المُعامل in قليل الاستخدام في الشيفرات. الحلقة for…in يوجد شكل خاص للحلقة for..in للمرور خلال جميع مفاتيح كائنٍ ما. هذه الحلقة مختلفة تمامًا عما درسناه سابقًا، أي الحلقة for(;;)‎. صياغة الحلقة تكون بالشكل التالي: for (key in object) { // يتنفذ ما بداخل الحلقة لكل مفتاح ضمن خاصيات الكائن } مثلا، لنطبع جميع خاصيات الكائن user: let user = { name: "John", age: 30, isAdmin: true }; for (let key in user) { // المفاتيح alert( key ); // name, age, isAdmin // قيم المفاتيح alert( user[key] ); // John, 30, true } لاحظ أن جميع تراكيب "for" تتيح لنا تعريف متغير التكرار بِداخل الحلقة، مثل let key في المثال السابق. يمكننا أيضًا استخدام اسم متغير آخر بدلًا من key. إليك مثال يُستخدم بكثرة: for (let prop in obj) الترتيب في الكائنات هل الكائنات مرتبة؟ بمعنى آخر، إن تنقلنا في حلقة خلال كائن، هل نحصل على جميع الخاصيات بنفس الترتيب الذي أُضيفت به؟ وهل يمكننا الاعتماد على هذا؟ الإجابة باختصار هي: "مرتب بطريقة خاصة": الخاصيات الرقمية يُعاد ترتيبها، تظهر باقي الخاصيات بترتيب الإنشاء ذاته كما في التفاصيل التالية. لنرَ مثالًا لكائن بِرموز الهاتف: let codes = { "49": "Germany", "41": "Switzerland", "44": "Great Britain", // .., "1": "USA" }; for (let code in codes) { alert(code); // 1, 41, 44, 49 } قد تُستخدم الشيفرة لاقتراح قائمة من الخيارات للمستخدم. إن كنا نبني موقعًا لزوار من ألمانيا فقد نريد أن تظهر 49 أولا. لكن، عند تشغيل الشيفرة، نرى شيئا مختلفًا تماما: تظهر USA (1)‎ أولًا ثم Switzerland (41)‎ وهكذا. تُستخدم رموز الهواتف بترتيب تصاعدي لأنها أعدادٌ، لذا نرى 1, 41, 44, 49. خاصيات عددية؟ ما هذا؟ تعني "الخاصية العددية" (integer property) نصًا يمكن تحويله من وإلى عدد دون أن يتغير. لذا فإن 49 هو اسم خاصية عددي لأنه عند تحويله إلى عدد وإرجاعه لنص يبقى كما هو. لكن "1.2" و "‎+49" ليست كذلك: // هي دالة تحذف الجزء العشري Math.trunc alert( String(Math.trunc(Number("49"))) ); // "49", الخاصية العددية ذاتها alert( String(Math.trunc(Number("+49"))) ); // ‏ "49" مختلفة عن "49+" => إذًا ليست خاصية عددية alert( String(Math.trunc(Number("1.2"))) ); // ‏ "1" مختلفة عن "1.2" => إذًا ليست خاصية عددية في المقابل، إن كانت المفاتيح غير عددية، فتُعرَض بالترتيب الذي أُنشِئت به. إليك مثال على ذلك: let user = { name: "John", surname: "Smith" }; user.age = 25; // add one more // تُعرض الخاصيات الغير رقمية بترتيب الإنشاء for (let prop in user) { alert( prop ); // name, surname, age } لذا، لحل مشكلة رموز الهواتف يمكننا التحايل وجعلها غير عددية بإضافة "+" قبل كل رمز كما يلي: let codes = { "+49": "Germany", "+41": "Switzerland", "+44": "Great Britain", // .., "+1": "USA" }; for (let code in codes) { alert( +code ); // 49, 41, 44, 1 } الآن تعمل وفق المطلوب! النسخ بالمرجع إحدى الاختلافات الرئيسية بين الكائنات والمتغيرات الأولية هي أنَّ الكائنات تُخزن وتُنسخ "بالمرجع". بينما تُخزن/تُنسخ القيم الأولية: النصوص، والأرقام، والقيم المنطقية بِعدِّها قيمةً كاملةً. إليك المثال التوضيحي التالي: let message = "Hello!"; let phrase = message; لدينا في هذه الشيفرة متغيرين مستقلين كلاهما يُخزن النص "Hello!‎ ". الكائنات ليست كذلك. لا يُخزِّن المتغير الكائن نفسه، وإنما "عنوانه في الذاكرة". بمعنى آخر، "مرجع للكائن" هنا صورة للكائن: let user = { name: "John" }; كما نرى، يُخزَّن المتغير في مكان ما في الذاكرة ويُخزِّن المتغير user مرجعًا إليه. عند نسخ متغير من نوع كائن، يُنسَخ المرجع الذي يشير إلى ذلك الكائن ولا يُنسَخ الكائن نفسه ويُكرَّر. إذا تخيلنا الكائن كخزانة، فإن المتغير هو مفتاح الخزانة ونسخ المتغير يكرِّر المفتاح وليس الخزانة ذاتها. إليك مثال يوضح ما ذكرناه: let user = { name: "John" }; let admin = user; // يُنسَخ المرجع الآن، أصبح لدينا متغيرين، كلاهما يحمل مرجعًا للكائن ذاته: يمكننا استخدام كلا المتغيرين (المفتاحين) للوصول إلى الخزانة وتعديل محتواها: let user = { name: 'John' }; let admin = user; admin.name = 'Pete'; // "admin" عُدِّلت باستخدام المرجع alert(user.name); // "user" أي يمكن رؤية التعديلات من المرجع 'Pete' تُعرَض القيمة يوضح المثال أعلاه وجود كائن واحد فقط. كما لو كان لدينا خزامة بمفتاحين، واستخدمنا أحدهما (admin) للوصول إلى الخزانة. ثم إذا استخدمنا الآخر (user) لاحقًا، فسنرى إنعكاس التغييرات التي أجراها الأول. الموازنة بالمرجع يعمل مُعاملي المساواة == والمساواة الصارمة === بنفس الطريقة للكائنات. يكون الكائنان متساويان إذا كانَا الكائن نفسه فقط. أي، إذا كان متغيران يشيران للكائن ذاته، فهما متساويان: let a = {}; let b = a; // نفس المرجع alert( a == b ); // true, كلا المتغيرين يشيران للكائن نفسه alert( a === b ); // true وهنا متغيران مستقلان ليسا متساويين أي يشيران إلى كائنين منفصلين حتى وإن كانا متماثلين تمامًا: let a = {}; let b = {}; // متغيران منفصلان alert( a == b ); // خطأ يُحوَّل الكائن إلى قيمة أولية (أساسية) في الموازنات مثل obj1 > obj2 أو obj == 5. سندرس كيفية تحويل الكائنات قريبًا، لكن، في الحقيقة، مثل هذه الموازنات تكون نادرة الضرورة وتنتج غالبًا من خطأ في كتابة الشيفرة. الكائنات الثابتة يمكن تغيير الكائن المُعَرَّف على أنَّه ثابت (أي وسم المتغير الذي يحوي الكائن بالكلمة المفتاحية const) دون حصول أي أخطاء. إليك الشيفرة التالية مثلًا: const user = { name: "John" }; user.age = 25; // (*) alert(user.age); // 25 قد يبدو أن السطر (*) سيسبب خطأ، لكن لا يوجد أي مشكلة البتة. ذلك لأنَّ const تحافظ على قيمة user ذاتها وتمنع تغييرها. ويُحزِّن user المرجع للكائن نفسه دائمًا ولا يتغيِّر بتعديل الكائن نفسه. يدخل السطر (*) إلى الكائن ولا يعيد تعيين قيمة المتغير user. يمكن أن يعطي const خطأ إن حاولنا تغيير قيمة المتغير user مثل: const user = { name: "John" }; // (user خطأ (لا يمكن تغيير قيمة المتغير user = { name: "Pete" }; لكن، ماذا إن أردنا إنشاء خاصيات ثابتة ضمن الكائن؟ سيُعطي user.age = 25 آنذاك خطأ، وذلك ممكن أيضًا. سيتم شرحه في الفصل رايات الخاصيات وواصفاتها. الاستنساخ والدمج إذًا، نسخ كائن يُنشِئ مرجعًا إضافيًّا له. لكن ماذا إن كنا نريد تكرار كائن فعليًّا أي إنشاء نسخة مستقلة منه؟ ذلك ممكن أيضًا، لكنه أصعب قليلًا لعدم وجود دالة تقوم بذلك في JavaScript ذلك لأنَّه لا حاجة لذلك بكثرة. النسخ بالمرجع كافٍ غالبًا. لكن إن أردنا ذلك فعلًا، فإننا نحتاج لإنشاء كائن وتكرار هيكل الكائن الموجود عبر التنقل خلال خاصياته ونسخها على المستوى الأولي. وإليك مثال على ذلك: let user = { name: "John", age: 30 }; let clone = {}; // الكائن الجديد الفارغ // إليه user ننسخ جميع خاصيات المتغير for (let key in user) { clone[key] = user[key]; } // الآن أصبحت النسخة مستقلة تماما clone.name = "Pete"; // تغيير البيانات في النسخة alert( user.name ); // في الكائن الأصلي John تظل يمكننا استخدام الدالة Object.assign للغرض ذاته. الصياغة الدالة هي: Object.assign(dest, [src1, src2, src3...]) تُعد المُعاملات dest، و src1، وحتى srcN كائنات (يمكن أن تكون بالعدد المُراد). تنسخ الدالة خاصيات جميع الكائنات src1, ..., srcN إلى الكائن dest. بمعنى آخر، تُنسخ جميع الخاصيات لجميع المُعاملات بدءًا من المُعامل الثاني إلى المُعامل الأول. ثم يتم إرجاع dest. مثلا، يمكننا استخدام الدالة لدمج عدة كائنات إلى كائن واحد: let user = { name: "John" }; let permissions1 = { canView: true }; let permissions2 = { canEdit: true }; // user إلى permissions2 و permissions1 تنسخ جميع الخاصيات من Object.assign(user, permissions1, permissions2); // now user = { name: "John", canView: true, canEdit: true } إن كان الكائن user يحوي أحد أسماء الخاصيات مسبقًا، فسيتم إعادة كتابة محتواها: let user = { name: "John" }; // isAdmin وإضافة name إعادة كتابة Object.assign(user, { name: "Pete", isAdmin: true }); // now user = { name: "Pete", isAdmin: true } يمكننا أيضًا استخدام الدالة Object.assign بدلًا من الحلقة للاستنساخ البسيط: let user = { name: "John", age: 30 }; let clone = Object.assign({}, user); تنسخ الدالة جميع خاصيات الكائن user إلى الكائن الفارغ وتُرجِعه كما في الحلقة لكن بشكل أقصر. حتى الآن، عدَدْنا جميع خاصيات user أولية (أساسية)، لكن قد تكون بعض الخاصيات مرجعًا لكائن آخر مثل الشيفرة التالية فما العمل؟ let user = { name: "John", sizes: { height: 182, width: 50 } }; alert( user.sizes.height ); // 182 في هذه الحالة نسخ clone.sizes = user.sizes ليس كافيًا لأنَّ user.sizes عبارة عن كائن، فسيُنسَخ على أنَّه مرجعٌ. هكذا، سيصبح لدى clone و user الحجم ذاته: let user = { name: "John", sizes: { height: 182, width: 50 } }; let clone = Object.assign({}, user); alert( user.sizes === clone.sizes ); // صحيح، الكائن ذاته // sizes الكائن الفرعي clone و user يتشارك الكائنان user.sizes.width++; // تغيير خاصية من مكان ما alert(clone.sizes.width); // 51, يعرض النتيجة من مكان آخر لإصلاح هذا، يجب استخدام حلقة الاستنساخ التي تفحص كل قيمة في user[key]‎، وإن كان كائنًا نستبدل الهيكل الخاص به أيضًا. هذه الطريقة تُسمى "استنساخ عميق" (deep cloning). يوجد خوارزمية عامة للاستنساخ العميق تنفِّذ الحالة السابقة بشكل صحيح، بالإضافة إلى حالات أكثر تعقيدًا. تُدعى هذه الخوارزمية خوارزمية الاستنساخ المُهيكلة. حتى لا نُعيد اختراع العجلة مجدَّدًا، يمكننا استخدام تنفيذ جاهز للحالة من مكتبة JavaScript lodash، تُدعى الدالة _.cloneDeep(obj). الخلاصة الكائنات عبارة عن مصفوفات ترابطية بميزات خاصة عديدة. تُخزن الكائنات خاصيات (أزواج مفتاح-قيمة)، بشرط أنه: يجب أن تكون مفاتيح الخاصيات نصوصًا أو رموزًا (غالبًا نصوص). يمكن أن تكون القيم من أي نوع. يمكننا استخدام ما يلي للوصول إلى خاصية: الصيغة النُقَطِيَّة: obj.property. صيغة الأقواس المعقوفة obj["property"]‎. تتيح لنا الأقواس المعقوفة أخذ مفتاح من متغير، مثل obj[varWithKey]‎. عمليات أخرى: لِحذف خاصية: delete obj.prop. لِفحص تواجد خاصية بمفتاح معين: "key" in obj. للتنقل خلال كائن: الحلقة for (let key in obj)‎. تُخَزَّن الكائنات وتُنسخ باستخدام المرجع. بمعنىً آخر، لا يُخزن المتغير قيمة الكائن (object value) لكنه يُخزن مرجعًا (reference) يمثِّل موقع قيمة الكائن في الذاكرة. لذا فإن نسخ هذا المتغير أو تمريره إلى دالة سَينسخ هذا المرجع وليس الكائن ككُل. جميع العمليات (مثل إضافة أو حذف خاصيات) المُنفَّذة على مرجع منسوخ تُنفَّذ على الكائن نفسه. لعمل نسخة حقيقية (الاستنساخ) يمكننا استخدام Object.assign أو _.cloneDeep(obj). يُسمى ما درسناه في هذا الفصل "كائن بسيط" أو كائن فقط. يوجد العديد من أنواع الكائنات الأخرى في JavaScript: الكائن Array (مصفوفة): لتخزين مجموعة البيانات المرتبة، الكائن Date (تاريخ): لتخزين معلومات عن الوقت والتاريخ، الكائن Error (خطأ): لتخزين معلومات عن خطأ ما. وغيرها من أنواع الكائنات. لدى هذه الأنواع ميزاتها الخاصة التي سيتم دراستها لاحقًا. يقول بعض الأشخاص أحيانًا شيئًا مثل "نوع مصفوفة" أو "نوع تاريخ" (الاسم الذي وضعته بين قوسين بجانب نوع الكائن)، لكن هذه الأنواع ليست أنواعًا مستقلة بحد ذاتها، إنما تنتمي إلى نوع البيانات Object (كائن) وتتفرع عنه بأشكال مختلفة. تُعد الكائنات في JavaScript قوية جدًا. درسنا في هذا الفصل جزءًا بسيطًا من موضوع هائل جدًا. سنتعامل مع الكائنات لاحقًا بصورة أقرب وسَنتعلم أكثر عنها في فصول أخرى. تمارين مرحبًا، بالكائن الأهمية: 5 اكتب الشيفرة البرمجية، سطر لكل متطلب: أنشئ كائنًا فارغًا باسم user. أضف الخاصية name بالقيمة John. أضف الخاصية surname بالقيمة Smith. غير قيمة الخاصية name إلى Pete. احذف الخاصية name من الكائن. الحل let user = {}; user.name = "John"; user.surname = "Smith"; user.name = "Pete"; delete user.name; التحقق من الفراغ اكتب الدالة isEmpty(obj)‎ التي تُرجع القيمة true إن كان الكائن فارغًا، وتُرجِع القيمة false في الحالات الأخرى. يجب أن تعمل كالتالي: let schedule = {}; alert( isEmpty(schedule) ); // true schedule["8:30"] = "get up"; alert( isEmpty(schedule) ); // false إليك تجربة حية للمثال. الحل قم بالمرور خلال الكائن ونفذ الأمر return false مباشرة إن عثرت على أي خاصية: function isEmpty(obj) { for (let key in obj) { // إن بدأت الحلقة بالعمل، فهناك خاصية في الكائن return false; } return true; } كائنات ثابتة؟ الأهمية: 5 هل من الممكن تغيير كائن صُرِّح عنه بالكلمة المفتاحية const؟ ما رأيك؟ const user = { name: "John" }; // هل تعمل؟ user.name = "Pete"; الحل بالفعل ستعمل بدون مشاكل. تحمي الكلمة المفتاحية const المتغير نفسه من التغيير فقط. بمعنى آخر، يخزن user مرجعًا للكائن ولا يمكن تغييره مع وجود التصريح عنه بالكلمة المفتاحية const لكن يمكن تغيير محتوى الكائن. const user = { name: "John" }; // تعمل user.name = "Pete"; // خطأ user = 123; جمع خاصيات الكائن لدينا كائن يُخزن رواتب الفريق: let salaries = { John: 100, Ann: 160, Pete: 130 } اكتب الشيفرة التي تجمع الرواتب وتُخزنها في المتغير sum. يجب أن يكون مجموع المثال أعلاه 390. إن كان salaries فارغًا، فإن الناتج سيكون 0. الحل let salaries = { John: 100, Ann: 160, Pete: 130 }; let sum = 0; for (let key in salaries) { sum += salaries[key]; } alert(sum); // 390 ضرب الخاصيات العددية بالقيمة 2 الأهمية: 3 أنشئ دالةً باسم multiplyNumeric(obj)‎ تضرب جميع الخاصيات العددية في الكائن obj في العدد 2. مثلا: // قبل الاستدعاء let menu = { width: 200, height: 300, title: "My menu" }; multiplyNumeric(menu); // بعد الاستدعاء menu = { width: 400, height: 600, title: "My menu" }; لاحظ أنَّ الدالة multiplyNumeric لا يجب أن تُرجِع أي شيء. يجب أن تُعدِّل القيم بداخل الكائن. ملاحظة: استخدام typeof لفحص الأعداد. إليك تجربة حية للتمرين. الحل function multiplyNumeric(obj) { for (let key in obj) { if (typeof obj[key] == 'number') { obj[key] *= 2; } } } ترجمة -وبتصرف- للفصل Objects من كتاب 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; } اقرأ أيضًا المقال التالي: كنس البيانات المهملة المقال السابق: تعويض نقص دعم المتصفحات لجافاسكربت
  10. تُدَار الذاكرة في JavaScript تلقائيًا في الخفاء. نحن ننشئ المتغيرات الأولية، والكائنات، والدوال وجميعها تأخذ مكانًا في الذاكرة ولكن هل سألت نفسك ماذا يحدث عندما يصبح أحد هذه الأشياء مهملًا وغير مهم؟ كيف يكتشف ذلك مُحرِّك JavaScript ويتخلص منه؟ هذا ما سنعرفه في هذا الفصل. قابلية الوصول المبدأ الرئيسي لإدارة الذاكرة في JavaScript هو قابلية الوصول (reachability). ببساطة، القيم "القابلة للوصول" هي القيم التي يمكن الوصول إليها واستخدامها بطريقة ما وهذه القيم مخولة لتُخزَّن في الذاكرة. 1- يوجد مجموعة من القيم القابلة للوصول بطبيعة الحال والتي لا يمكن التخلص منها لأسباب وجيهة مثل: المتغيرات المحلية والمُعاملات للدالة الحاليِّة. المتغيرات والمُعاملات لدوال أخرى ضمن السلسلة الحالية في الاستدعاء المُتداخل. المتغيرات العامة. (بالإضافة إلى بعض المتغيرات الأخرى الداخلية) تُدعى هذه القيم «جذورًا» (roots). 2- تكون أي قيمة أخرى قابلة للوصول إن كان بالإمكان الوصول إليها من جذر باستخدام مرجع أو سلسلة من المراجع. مثلًا، إن كان هناك كائن في متغير محلي، ولدى هذا الكائن خاصية تشير لكائن آخر، فإنَّ هذا الكائن قابل للوصول. وهذه الكائنات التي يُشار إليها قابلة للوصول أيضًا. ستجد أمثلة توضيحية لاحقًا. يوجد عملية خلفيَّة في محرك JavaScript تُدعَى «كانس المهملات» (garbage collector) تعمل على مراقبة الكائنات وحذف التي لم تَعُد قابلة للوصول. مثال بسيط أبسط مثال هو: // user يرجِع لكائن let user = { name: "John" }; يُصوِّر السهم في الصورة مرجعًا لكائن. المتغير العام "user" يشير للكائن {name: "John"‎} (سنُسميه John اختصارًا). الخاصية "name" للكائن John تُخزن قيمة أولية، لذ رُسمَت بداخل الكائن. إن استُبدلَت قيمة المتغير user، فَسَنفقد المرجع الذي يشير إلى الكائن John: user = null; أصبح الكائن John غير قابل للوصول الآن أي لا يوجد طريقة للوصول إليه. سيعمل كانس المهملات على حذف البيانات المتمثلة في الكائن John وتحرير الذاكرة التي يحتلها. مرجعَان لكائن لنفترض أننا نسخنا المرجع من المتغير user إلى متغير آخر باسم admin: // user يرجِع لكائن let user = { name: "John" }; let admin = user; إن قمنا بالأمر السابق ذاته الآن: user = null; فسيكون الكائن قابلًا للوصول من خلال المتغير العام admin، لذا يبقى في الذاكرة. إن استبدلنا محتوى المتغير admin أيضًا فسيُحذَف الكائن. الكائنات المتداخلة الآن ننتقل لمثال أكثر تعقيدًا. ألق نظرة على الشيفرة التالية: function marry(man, woman) { woman.husband = man; man.wife = woman; return { father: man, mother: woman } } let family = marry({ name: "John" }, { name: "Ann" }); تربط الدالة marry كائنين بجعل كلاهما يشير إلى الآخر ثم ترجِع كائنًا جديدًا يحوي كلاهما. هيكل الذاكرة الناتج يكون كالتالي: تكون جميع البيانات قابلة للوصول حتى الآن. دعنا نجرب حذف مرجعين الآن: delete family.father; delete family.mother.husband; ليس من الكافِ حذف أحد المرجِعين فقط، لأنَّ جميع الكائنات ستظل قابلة للوصول لكن إن حذفنا كلا المرجعين، فسنرى عدم وجود أي مرجع إلى الكائن John: لا يهم وجود مرجع من الكائن، إذ ما يجعله قابلًا للوصول هو المراجع التي تشير إليه، لذا فإنَّ الكائن John أصبح غير قابل للوصول وسيُحذَف من الذاكرة مع جميع بياناته التي أصبحت غير قابلة للوصول أيضًا. بعد تجميع البيانات الغير مرغوب بها، يبقى لدينا: جزيرة غير قابلة للوصول يمكن أن تصبح جزيرة من الكائنات المترابطة غير قابلة للوصول وتُحذَف من الذاكرة. الكائن الرئيسي هو الكائن أعلاه ذاته: family = null; تُصبح الصورة في الذاكرة كما يلي: يوضح هذا المثال أهمية مبدأ قابلية الوصول. من الواضح أن الكائنين John و Ann ما زالا مرتبطين ولكل منهما مراجع لبعضهما، لكن ذلك غير كافٍ. الكائن السابق "family" أصبح غير مربوط بالجذر، أي لم يعد هناك أي مرجع إليه لذا فإن الجزيرة كاملةً تصبح غير قابلة للوصول وتُحذَف. الخوارزميات الداخلية تُدعى الخوارزمية الأساسية لتجميع البيانات المهملة «الاستهداف والتمشيط» (mark-and-sweep)؛ تُنفَّذ خطوات جمع البيانات المهملة دوريًا وفق الخطوات التالية: يأخذ كانس المهملات الجذور ويحفظها (يُحدِّدها هدفًا له). ثم يُمشِّط جميع الإشارات الخارجة منها (مراجع لكائنات أخرى) ويحفظها لاستهدافها أيضًا. ثم يُمشِّط جميع الكائنات التي استهدفها مسبقًا ويحفظ مراجعها لاستهدفها لاحقًا. يُمشِّط جميع الكائنات بتلك الطريقة ويتذكرها لكي لا يُمشِّط أي كائن مرةً ثانية مستقبلًا. تستمر العملية مرارًا وتكرارًا حتى يصبح هناك مراجع لم تُمشَّط (غير قابلة للوصول من أي جذر). تُحذَف جميع الكائنات باستثناء تلك استُهدفَت وعُلِّمَت بأنَّها غير مهملة. مثلًا، ليكن هيكل الكائنات لدينا كما يلي: يمكن رؤية جزيرة غير قابلة للوصول في اليمين. الآن لنرى كيف يتعامل معها كانس المهملات وفق خوارزمية الاستهداف والتمشيط. الخطوة الأولى هي تحديد الجذور: ثم تُحدَّد المراجع التي تشير إليها: تُحدَّد المراجع التي تشير لها هذه الكائنات أيضًا: تُعدُّ الكائنات التي لم تُمشَّط أثناء العملية غير قابلة للوصول ويجري حذفها: هذه الآلية التي يعمل بها كانس المهملات. تطبق محركات JavaScript العديد من التحسينات لتحسين هذه الخوارزمية وتسريع عملها بطريقة لا تؤثر على سرعة التنفيذ. بعض التحسينات: التجميع وفق الجيل: تُقَسَّم الكائنات إلى مجموعتين: "كائنات جديدة" و "كائنات قديمة". تُنشَأ العديد من الكائنات ثمَّ تؤدي عملها ثمَّ تموت بسرعة، ويمكن تنظيفها بقوة. وتلك التي تنجو تصبح قديمة وتُفحَص بوتيرة أقل. التجميع التدريجي: إن وُجِد العديد من الكائنات وأردنا المرور خلال جميع الكائنات وتحديدها دفعة واحدة، فقد يأخذ ذلك وقتًا ويظهِِر آنذاك تأخيرٌ ملحوظٌ في التنفيذ، لذا يحاول المُحرِّك تقسيم عملية كنس البيانات المهملة إلى أجزاء ثم تنفيذ الأجزاء واحدًا تلو الآخر بشكل منفصل. قد يتطلب ذلك إجراء حسابات إضافية لتتبع التغييرات، لكن يصبح لدينا الكثير من التأخيرات الغير ملحوظة بدلًا من تأخير واحد كبير. التجميع وقت الخمول: يحاول كانس المهملات العمل عندما يكون المعالج غير مشغول لتقليل أي تأثيرات محتملة على التنفيذ. يوجد تحسينات وإضافات أخرى على خوارزمية كنس المهملات لكن يجب على التوقف هنا لأنَّ المحركات المختلفة تُطبِّق تقنيات مختلفة. والأهم من ذلك، تتغير الأمور بتطور المحركات، لذا فإنَّ التعمق مسبقًا دون الحاجة لذلك لا يستحق العناء. إلا إن كان ذلك رغبة شخصية، فَسنضع بعض الروابط في الأسفل. الخلاصة الأشياء التي يجب معرفتها: تُكنس البيانات المهملة تلقائيًا، وهو أمر لا يمكن فرضه أو تجنبه. تبقى الكائنات في الذاكرة طالما يمكن الوصول إليها من أي جذر. وجود مرجِع للكائن ليس مثل أن يكون قابلًا للوصول من جذر، ويمكن لمجموعة من الكائنات المترابطة أن تصبح غير قابلة للوصول. تستخدم المحركات الحديثة خوارزميات متطورة لكنس البيانات المهملة. يغطي كتاب "The Garbage Collection Handbook: The Art of Automatic Memory Management" (لمؤلفه R. Jones وغيره) بعضًا منها. إن كنت معتادًا على البرمجة بلغات ذات مستوى منخفض، يوجد معلومات مفصَّلة عن كانس المهملات في المُحرِّك V8 في المقال رحلة إلى V8: كنس البيانات المهملة. تنشر مدونة V8 أيضًا مقالات عن التغييرات في إدارة الذاكرة من وقت لآخر. لتتعلم عن كنس البيانات المهملة، يجب أن تتجهز بتعلم أمور V8 الداخلية كما يُفضَّل أن تقرأ مدونة Vyacheslav Egorov الذي عمل كأحد مهندسي V8. أنا أقول V8 لوجود الكثير من المقالات عنه على الإنترنت. العديد من الجوانب متشابهة بالنسبة لباقي المحركات، لكن يختلف كنس البيانات المهملة من عدة نواحي بينها. المعرفة العميقة بالمحركات جيدة عندما تحتاج إلى إجراء تحسين منخفض المستوى. فكِّر في ذلك وليكن خطوتك التالية بعد أن تعتاد على اللغة. ترجمة -وبتصرف- للفصل Garbage collection من كتاب 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. وفقًا للمواصفات، قد تكون مفاتيح خصائص الكائنات من نوع نصي (string) أو رمزي (Symbol)، ولا تكون أرقامًا ولا قيمًا منطقية، إما نصًا أو رمزًا فقط. حتى الآن، استخدمنا النوع النصي فقط. الآن، لنرى فوائد النوع الرمزي. الرموز يُمَثِّل الرمز مُعَرِّفًا فريدًا، ويمكن إنشاء قيمة من هذا النوع باستخدام Symbol()‎: // هو رمز جديد id let id = Symbol(); يمكننا إعطاء وصف للرمز أثناء الإنشاء (ما يُسمَّى أيضًا باسم الرمز)، ويكون مفيدًا لعملية تصحيح الأخطاء: // "id" هو رمز مع الوصف id let id = Symbol("id"); تتصف الرموز بكونها فريدة حتى إن أنشأنا أكثر من رمز بالوصف ذاته، تبقى الرموز قيمًا مختلفة. يُعد الوصف مجرد طابع ولا يؤثر على أي شيء. مثلًا، هنا رمزين بالوصف ذاته لكنهما غير متساويان: let id1 = Symbol("id"); let id2 = Symbol("id"); alert(id1 == id2); // false إن كانت لديك خلفية عن لغة Ruby أو أي لغة برمجية أخرى تستخدم الرموز فلا تخلط الأمور. فالرموز في JavaScript مختلفة. لا تتحول الرموز تلقائيًا إلى نص تتضمن أغلب القيم في JavaScript تحويلًا ضمنيًا إلى نص. مثلًا، يمكننا عرض أي قيمة تقريبا باستخدام alert وتعمل بشكل صحيح. لكن الرموز خاصة، فهي لا تتحول تلقائيًا. مثلا، أمر alert التالي سَيعرض خطأ: let id = Symbol("id"); alert(id); // TypeError: Cannot convert a Symbol value to a string يُعد هذا الأمر من حرَّاس اللغة لتجنب الخطأ، لأن النصوص والرموز مختلفة جذريًا ولا يجب أن يتحول نوع منهما للآخر عن طريق الخطأ. إن كنا نريد عرض رمز ما، يجب أن نستدعي الدالة .toString()‎. كما في المثال التالي: let id = Symbol("id"); alert(id.toString()); // Symbol(id), أصبحت تعمل الآن أو استخدم الخاصية symbol.description لعرض الوصف فقط: let id = Symbol("id"); alert(id.description); // id خاصيات "خفية" تتيح لنا الرموز إنشاء خاصيات "خفية" للكائن، والتي لا يمكن لأي شيفرة الوصول إليها وتغيير محتواها حتى عن طريق الخطأ. مثلا، إن كنا نتعامل مع كائنات تنتمي إلى شيفرة من طرف ثالث ونريد إضافة مُعرفات لها، نستخدم مفتاحًا رمزيًا لذلك: let user = { // ينتمي لِشيفرة أخرى name: "John" }; let id = Symbol("id"); user[id] = 1; alert( user[id] ); // يمكننا الوصول إلى البيانات باستخدام الرمز كمفتاح ما الفائدة من استخدام Symbol("id")‎ على النص "id"؟ بما أن كائنات user تنتمي لِشيفرة أخرى، وبما أن الشيفرة أعلاه تتعامل معها فليس آمنًا إضافة أي حقل لها. لكن لا يمكن الوصول إلى الرمز حتى عن طريق الخطأ، قد لا تراه شيفرة الطرف الثالث أيضًا، لذا فإنها الطريقة الصحيحة للقيام بذلك. تخيل أيضًا أن سكريبت آخر يريد إضافة معاملاته الخاصة بِداخل user لغرضه الخاص. قد يكون هذا السكريبت مكتبة JavaScript أخرى، لذا فإن كل سكريبت لا يعلم بتواجد الآخر بتاتًا، ثم يمكن لهذا السكريبت إنشاء رمزه الخاص Symbol("id")‎ هكذا: // ... let id = Symbol("id"); user[id] = "Their id value"; بهذه الطريقة، لن يوجد أي تعارض بين المُعاملات التي انشأناها ومُعاملات السكريبت الآخر لأن الرموز تختلف دومًا حتى إن كان لديها الاسم ذاته. لكن إن استخدمنا النص "id" بدلًا من الرمز للغرض ذاته، فَسيوجد تعارض: let user = { name: "John" }; // "id" يستخدم السكربت الذي أنشأناه الخاصية user.id = "Our id value"; // لغرض آخر "id" يريد سكريبت آخر استخدام الخاصية user.id = "Their id value" // !تعارض! تم تغيير المحتوى من قِبل السكريبت الآخر الرموز في التعريف المختصر لكائن إن أردنا استخدام الرموز عند تعريف كائن بالطريقة المختصرة عبر الأقواس {...} فَسنحتاج إلى تغليفها بأقواس مربعة هكذا: let id = Symbol("id"); let user = { name: "John", [id]: 123 // "id: 123" ليس }; ذلك لأننا نريد القيمة من المتغير id كمفتاح وليس النص "id". تتخطى for…in الرموز لا تشارك الخصائص الرمزية في الحلقة for..in. مثلًا: let id = Symbol("id"); let user = { name: "John", age: 30, [id]: 123 }; for (let key in user) alert(key); // name, age (لا يوجد رموز) // يعمل الوصول المباشر للرمز alert( "Direct: " + user[id] ); يتجاهل Object.keys(user)‎ الرموز أيضًا. يُعد هذا من جزءًا من مبدأ "إخفاء الخاصيات الرمزية". إن حاول سكريبت آخر أو مكتبة JavaScript الدخول إلى كائن والتنقل فيه، فلن يصل إلى الخاصيات الرمزية بتاتًا. في المقابل، تنسخ Object.assign كلًا من النصوص والخاصيات الرمزية: let id = Symbol("id"); let user = { [id]: 123 }; let clone = Object.assign({}, user); alert( clone[id] ); // 123 لا يوجد لَبسٌ هنا، فهذا الأمر ضمن التصميم. الفكرة هي أنه عند استنساخ كائن أو دمج كائنين، فإننا نريد أن تُنسَخ جميع الخصائص (بما فيها الرموز مثل id). تُحوَّل مفاتيح الخاصيات التي من نوع آخر غير الرمز إلى نصوص جبريًا يمكننا استخدام النصوص أو الرموز فقط كمفاتيح في الكائنات، ويُحوَّل أي نوع آخر إلى نص. مثلًا، الرقم 0 يصبح النص "0" عندما يستخدم كمفتاح لخاصية: let obj = { 0: "test" // "0": "test" مثل }; // "إلى الخاصية ذاتها أي يحوَّل الرقم 0 إلى نص "0 alert تصل الدالة alert( obj["0"] ); // اختبار alert( obj[0] ); // (اختبار (الخاصية ذاتها الرموز العامة جميع الرموز تكون مختلفة دائمًا كما رأينا، حتى إن كانت تحمل الأسماء ذاتها. لكن قد نريد أحيانا أن تكون الرموز التي تحمل الاسم ذاته هي نفس الكائنات. مثلًا، تحتاج أجزاء متعددة في التطبيق الوصول إلى الرمز "id" ما يعني الخاصيات ذاتها. لنتمكن من القيام بذلك، يوجد سجل للرموز العامة. يمكننا إنشاء رموز فيه والوصول إليها لاحقًا، كما يضمن أن الوصول المتكرر إلى الاسم ذاته يُرجِع الرمز ذاته في كل مرة. لقراءة، أو إنشاء رمز في حال عدم تواجده في السجل، نستخدم Symbol.for(key)‎. يفحص هذا الاستدعاء السجل العام للرموز، إن كان هناك رمزًا بالوصف key، فإنه يُرجِعه، وإن لم يجده فإنه يُنشئ رمزًا جديدًا بالاستدعاء Symbol(key)‎ ويُخزِّنه في السجل بالوصف المُعطى key. إليك المثال التالي: // يقرأ من السجل العام let id = Symbol.for("id"); // إن لم يجد الرمز، ينشئه // يُقرأ مجددًا، ربما من جزء آخر في الشيفرة let idAgain = Symbol.for("id"); // الرمز ذاته alert( id === idAgain ); // true تُدعى الرموز داخل السجل العام رموزًا عامة، ونستعلمها في حال أردنا رمزًا على مستوى تطبيق كامل، وقابلًا للوصول في الشيفرة. هذا الأمر مشابه لما في لغة Ruby يوجد في بعض اللغات البرمجية مثل Ruby رمزًا واحدًا لكل اسم. كما رأينا، في JavaScript الأمر ذاته صحيح بالنسبة للرموز العامة. Symbol.keyFor بالنسبة للرموز العامة، ليس فقط الدالة Symbol.for(key)‎ تُرجِع الرمز وفقًا لاسمه، بل يوجد استدعاء عكسي: Symbol.keyFor(sym)‎، والتي تعكس ما تقوم به الأخرى: تُرجِع اسمًا بواسطة رمز عام. مثلًا: // نَسترجع الرمز بالاسم let sym = Symbol.for("name"); let sym2 = Symbol.for("id"); // نسترجع الاسم بالرمز alert( Symbol.keyFor(sym) ); // name alert( Symbol.keyFor(sym2) ); // id تستخدم Symbol.keyFor سجل الرموز العام داخليًا للبحث عن مفتاح الرمز. لذا فإنها لا تعمل مع الرموز الغير عامة. إن كان الرمز غير عام، فلن يتمكن من العثور عليه وسيُرجِع undefined. يمكن القول أن أي رمز لدية الخاصية description. مثلا: let globalSymbol = Symbol.for("name"); let localSymbol = Symbol("name"); alert( Symbol.keyFor(globalSymbol) ); // name, رمز عام alert( Symbol.keyFor(localSymbol) ); // undefined, غير عام alert( localSymbol.description ); // name رموز النظام يوجد العديد من رموز "النظام" التي تستخدمها JavaScript داخليًا، ويمكن استخدامها لضبط نواحي متعددة من الكائنات بدقة. هذه الرموز مُدرَجة في وصف جدول الرموز المتعارف عليها: Symbol.hasInstance Symbol.isConcatSpreadable Symbol.iterator Symbol.toPrimitive وهكذا… مثلًا، يتيح لنا الرمز Symbol.toPrimitive وصف كائن إلى تحويل أولي. سنرى استخدامها قريبًا. ستعتاد على باقي الرموز عند دراسة ميزات اللغة المُقابلة. الخلاصة النوع Symbol هو نوع أولي للمُعاملات الفريدة. يتم إنشاء الرموز عبر استدعاء الدالة Symbol()‎ مع وصف اختياري (اسم). الرموز تكون دائمًا مختلفة القيم حتى إن كان لها الاسم ذاته، أي تتصف بأنها فريدة. إن أردنا أن يكون الرمز بالاسم ذاته يجمل القيمة ذاتها، يجب أن نستخدم السجل العام: تُرجِع Symbol.for(key)‎ (وتُنشِئ في حال الحاجة) رمزًا عامًا بالمفتاح كاسم. تُرجِع الاستدعاءات العديدة لِلدالة Symbol.for باستخدام المفتاح ذاته الرمز نفسه. لدى الرموز حالتي استخدام: خاصيات الكائن "الخفية". إن أردنا إضافة خاصية إلى كائن ينتمي إلى سكريبت أو مكتبة أخرى، يمكننا إنشاء رمز واستخدامه كمفتاح خاصية. لا تظهر الخاصية الرمزية في for..in، حتى لا تتم معالجته عن طريق الخطأ مع باقي الخاصيات. لن يتم الوصول إليه مباشرةً أيضًا لأن السكريبت الخارجي لا يحوي على هذا الرمز. هكذا تكون الخاصية محمية من الاستخدام الخارجي. لذا، يمكننا إخفاء شيء ما بشكل تام في الكائنات التي نحتاجها، والتي لا يجب أن يراها الآخرون باستخدام الخاصيات الرمزية. يوجد العديد من رموز النظام المستخدمة بواسطة JavaScript التي يمكن الوصول إليها بواسطة Symbol.*‎. يمكننا استخدامها لتعديل سلوك مُدمَج، مثلا، سنستخدم Symbol.iterato لاحقَا في الشرح للحلقات، و Symbol.toPrimitive لإعداد التحويل من كائن لقيمة أولية وهكذا… عمليًا، الرموز ليست خفية 100%. يوجد دالة مدمجة Object.getOwnPropertySymbols(obj)‎ تتيح لنا الوصول إلى جميع الرموز. كما يوجد دالة تُدعى Reflect.ownKeys(obj)‎ والتي تُرجِع جميع مفاتيح الكائن بما فيها الرموز. لذا فإن الرموز ليست مخفية فعلًا، لكن أغلب المكاتب، والدوال المدمجة والهياكل لا تستخدم هذه الدوال. ترجمة -وبتصرف- للفصل Symbol type من كتاب 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; } اقرأ أيضًا المقال التالي: الدوال في الكائنات واستعمالها this المقال السابق: كنس البيانات المهملة
  12. نُنشِئ الكائنات باستخدام الصيغة الاعتيادية المختصرة {...}. لكننا نحتاج لإنشاء العديد من الكائنات المتشابهة غالبًا، مثل العديد من المستخدمين، أو عناصر لقائمة وهكذا. يمكن القيام بذلك باستخدام الدوال البانية (constructor functions) لكائن والمُعامِل "new". الدالة البانية تقنيًا، الدوال البانية هي دوال عادية، لكن يوجد فكرتين متفق عليها: أنها تبدأ بأحرف كبيرة. يجب تنفيذها مع المُعامِل "new" فقط. إليك المثال التالي: function User(name) { this.name = name; this.isAdmin = false; } let user = new User("Jack"); alert(user.name); // Jack alert(user.isAdmin); // false عند تنفيذ دالة مع الُعامِل new، تُنَفَّذ الخطوات التالية: يُنشَأ كائن فارغ ويُسنَد إلى this. يُنَفَّذ محتوى الدالة. تقوم غالبًا بتعديل this، وإضافة خاصيات إليه. تُرجَع قيمة this. بمعنى آخر، تقوم new User(...)‎ بشيء يشبه ما يلي: function User(name) { // this = {}; (implicitly) // this إضافة خاصيات إلى this.name = name; this.isAdmin = false; // this; (implicitly) إرجاع } إذًا، تُعطي let user = new User("Jack")‎ النتيجة التالية ذاتها: let user = { name: "Jack", isAdmin: false }; الآن، إن أردنا إنشاء مستخدمين آخرين، يمكننا استدعاء new User("Ann")‎، و new User("Alice‎")‎ وهكذا. تعدُّ هذه الطريقة في بناء الكائنات أقصر من الطريقة الاعتيادية عبر الأقواس فقط، وأيضًا أسهل للقراءة. هذا هو الغرض الرئيسي للبانيات، وهي تطبيق شيفرة قابلة لإعادة الاستخدام لإنشاء الكائنات. لاحظ أنَّه يمكن استخدام أي دالة لتكون دالة بانية تقنيًا. يعني أنه يمكن تنفيذ أي دالة مع new، وستُنَفَّذ باستخدام الخوارزمية أعلاه. استخدام الأحرف الكبيرة في البداية هو اتفاق شائع لتمييز الدالة البانية من غيرها وأنَّه يجب استدعاؤها مع new. new function() { … }‎ إن كان لدينا العديد من الأسطر البرمجية، وجميعها عن إنشاء كائن واحد مُعَقَّد، فبإمكاننا تضمينها في دالة بانية، هكذا: let user = new function() { this.name = "John"; this.isAdmin = false; // ...شيفرة إضافية لإنشاء مستخدم // ربما منطق معقد أو أي جمل // متغيرات محلية وهكذا.. }; لا يمكن استدعاء المُنشِئ مجددًا، لأنه غير محفوظ في أي مكان، يُنشَأ ويُستدعى فقط. لذا فإن الخدعة تهدف لتضمين الشيفرة التي تُنشِئ كائنًا واحدًا، دون إعادة الاستخدام وتكرار العملية مستقبلًا. وضع اختبار الباني: new.target ميزة متقدمة: تُستخدم الصيغة في هذا الجزء نادرًا، ويمكنك تخطيها إلا إن كنت تُريد الإلمام بكل شيء. يمكننا فحص ما إن كانت الدالة قد استدعيت باستخدام new أو دونه من داخل الدالة، وذلك باستخدام الخاصية الخاصة new.target. تكون الخاصية فارغة في الاستدعاءات العادية، وتساوي الدالة البانية إذا استُدعِيَت باستخدام new: function User() { alert(new.target); } // "new" بدون: User(); // undefined // باستخدام "new": new User(); // function User { ... } يمكن استخدام ذلك بداخل الدالة لمعرفة إن استُدعِيَت مع new، "في وضع بناء كائن"، أو بدونه "في الوضع العادي". يمكننا أيضًا جعل كلًا من الاستدعاء العادي و new ينفِّذان الأمر ذاته -بناء كائن- هكذا: function User(name) { if (!new.target) { // new إن كنت تعمل بدون return new User(name); // new ...سأضيف } this.name = name; } let john = User("John"); // new User تُوَجِّه الاستدعاء إلى alert(john.name); // John يستخدم هذا الاسلوب في بعض المكتبات أحيانًا لجعل الصيغة أكثر مرونة حتى يتمكن الأشخاص من استدعاء الدالة مع new أو بدونه، وتظل تعمل. ربما ليس من الجيد استخدام ذلك في كل مكان، لأن حذف new يجعل ما يحدث أقل وضوحًا. لكن مع new، يعلم الجميع أن كائنًا جديدًا قد أُنشِئ. ما تُرجِعه الدوال البانية لا تملك الدوال البانية عادةً التعليمة return. فَمُهِمَتُهَا هي كتابة الأمور المهمة إلى this، وتصبح تلقائيًا هي النتيجة. لكن إن كان هناك التعليمة return فإن القاعدة بسيطة: إن استُدعِيَت return مع كائن، يُرجَع الكائن بدلًا من this. إن استُدعِيَت return مع متغير أولي، يُتَجاهَل. بمعنىً آخر، return مع كائن يُرجَع الكائن، وفي الحالات الأخرى تُرجَع this. مثلًا، يعاد في المثال التالي الكائن المرفق بعد return ويهمل الكائن المسنَد إلى this: function BigUser() { this.name = "John"; return { name: "Godzilla" }; // <-- تُرجِع هذا الكائن } alert( new BigUser().name ); // Godzilla, حصلنا على الكائن وهنا مثال على استعمال return فارغة (أو يمكننا وضع متغير أولي بعدها، لا فرق): function SmallUser() { this.name = "John"; return; // ← this تُرجِع } alert( new SmallUser().name ); // John لا تحوي الدوال البانية غالبًا على تعليمة الإعادة return. نذكر هنا هذا التصرف الخاص عند إرجاع الكائنات بغرض شمول جميع النواحي. حذف الاقواس بالمناسبة، يمكننا حذف أقواس new في حال غياب المعاملات مُعامِلات: let user = new User; // <-- لا يوجد أقوس // الغرض ذاته let user = new User(); لا يُعد حذف الأقواس أسلوبًا جيدَا، لكن الصيغة مسموح بها من خلال المواصفات. الدوال في الباني استخدام الدوال البانية لإنشاء الكائنات يُعطي مرونة كبيرة. قد تحوي الدالة البانية على مُعامِلات ترشد في بناء الكائن ووضعه، إذ يمكننا إضافة خاصيات ودوال إلى this بالطبع. مثلًا، تُنشِئ new User(name)‎ في الأسفل كائنًا بالاسم المُعطَى name والدالة sayHi: function User(name) { this.name = name; this.sayHi = function() { alert( "My name is: " + this.name ); }; } let john = new User("John"); john.sayHi(); // My name is: John /* john = { name: "John", sayHi: function() { ... } } */ لإنشاء كائنات أكثر تعقيدًا، يوجد صيغة أكثر تقدمًا، الفئات، والتي سنغطيها لاحقًا. الخلاصة الدوال البانية، أو باختصار البانيات، هي دوال عادية، لكن يوجد اتفاق متعارف عليه ببدء اسمها بحرف كبير. يجب استدعاء الدوال البانية باستخدام new فقط. يتضمن هذا الاستدعاء إنشاء كائن فارغ وإسناده إلى this وبدء العملية ثم إرجاع هذا الكائن في نهاية المطاف. يمكننا استخدام الدوال البانية لإنشاء كائنات متعددة متشابهة. تزود JavaScript دوالًا بانية للعديد من الأنواع (الكائنات) المدمجة في اللغة: مثل النوع Date للتواريخ، و Set للمجموعات وغيرها من الكائنات التي نخطط لدراستها. عودة قريبة غطينا الأساسيات فقط عن الكائنات وبانياتها في هذا الفصل. هذه الأساسيات مهمة تمهيدًا لتعلم المزيد عن أنواع البيانات والدوال في الفصل التالي. بعد تعلم ذلك، سنعود للكائنات ونغطيها بعمق في فصل الخصائص، والوراثة، والأصناف. تمارين دالتين - كائن واحد الأهمية: 2 هل يمكن إنشاء الدالة A و B هكذا new A()==new B()‎؟ function A() { ... } function B() { ... } let a = new A; let b = new B; alert( a == b ); // true إن كان ممكنًا، وضح ذلك بمثال برمجي. الحل نعم يمكن ذلك. إن كان هناك دالة تُرجِع كائنًا فإن new تُرجِعه بدلًا من this. لذا فمن الممكن، مثلًا، إرجاع الكائن المعرف خارجيًا obj: let obj = {}; function A() { return obj; } function B() { return obj; } alert( new A() == new B() ); // true إنشاء حاسبة جديدة الأهمية: 5 إنشِئ دالة بانية باسم Calculator تنشئ كائنًا بثلاث دوال: read()‎ تطلب قيمتين باستخدام سطر الأوامر وتحفظها في خاصيات الكائن. sum()‎ تُرجِع مجموع الخاصيتين. mul()‎ تُرجِع حاصل ضرب الخاصيتين. مثلًا: let calculator = new Calculator(); calculator.read(); alert( "Sum=" + calculator.sum() ); alert( "Mul=" + calculator.mul() ); الحل function Calculator() { this.read = function() { this.a = +prompt('a?', 0); this.b = +prompt('b?', 0); }; this.sum = function() { return this.a + this.b; }; this.mul = function() { return this.a * this.b; }; } let calculator = new Calculator(); calculator.read(); alert( "Sum=" + calculator.sum() ); alert( "Mul=" + calculator.mul() ); إنشاء مجمِّع الأهمية: 5 انشِئ دالة بانية باسم Accumulator(startingValue)‎، إذ يجب أن يتصف هذا الكائن بأنَّه: يخزن القيمة الحالية في الخاصية value. تُعَيَّن قيمة البدء عبر المعامل startingValue المعطى من الدالة البانية. يجب أن تستخدم الدالة read() الدالة prompt لقراءة رقم جديد وإضافته إلى value. بمعنى آخر، الخاصية value هي مجموع القيم المدخلة بواسطة المستخدم بالإضافة إلى القيمة الأولية startingValue. هنا مثال على ما يجب أن يُنَفَّذ: let accumulator = new Accumulator(1); // القيمة الأولية 1 accumulator.read(); // يضيف قيمة مدخلة بواسطة المستخدم accumulator.read(); // يضيف قيمة مدخلة بواسطة المستخدم alert(accumulator.value); // يعرض مجموع القيم الحل function Accumulator(startingValue) { this.value = startingValue; this.read = function() { this.value += +prompt('How much to add?', 0); }; } let accumulator = new Accumulator(1); accumulator.read(); accumulator.read(); alert(accumulator.value); ترجمة -وبتصرف- للفصل Constructor, operator "new" من كتاب The JavaScript Language .task__importance { color: #999; margin-left: 30px; } .task__answer { border: 3px solid #f7f6ea; margin: 20px 0 14px; position: relative; display: block; padding: 25px 30px; } code { background-color: rgb(250, 250, 250); border-radius: 3px; } اقرأ أيضًا المقال التالي: توابع الأنواع الأولية المقال السابق: التحويل من نوع كائن إلى نوع أولي
  13. تتيح لنا JavaScript التعامل مع أنواع البيانات الأولية (النصوص، الأرقام، وغيرها) كما لو أنها كائنات، كما تزودنا بتوابع (methods) لاستدعائها كما الكائنات. وسندرس هذه التوابع قريبًا، لكن لنرى أولًا كيف تعمل لأن الأنواع الأولية (primitives) ليست كائنات (أي object) وسنجعل الأمر واضحًا هنا. لنرى إلى الفروق الأساسية بين الأنواع الأولية والكائنات. النوع الأولي: هو قيمة من نوع أولي (primitive type). يوجد 6 أنواع أولية: نص string، رقم number، قيمة منطقية boolean، رمز symbol، قيمة فارغة null ، وقيمة غير معرفة undefined. الكائن: قادر على تخزين العديد من القيم ضمن خاصيات. يمكن إنشاؤه باستخدام {}، مثلًا: {name: "John", age: 30}. يوجد أنواع أخرى من الكائنات في JavaScript: مثلا، تعد الدوال كائنات. أحد أفضل الأشياء بالنسبة للكائنات هو إمكانية تخزين دالة في خاصية من خواص هذا الكائن. let john = { name: "John", sayHi: function() { alert("Hi buddy!"); } }; john.sayHi(); // Hi buddy! إذًا، أنشأنا الكائن john محتويًا الدالة sayHi. يوجد العديد من الكائنات المدمجة في اللغة، مثل التي تتعامل مع التواريخ، الأخطاء، عناصر HTML، وغيرها. ولديها خاصيات وتوابع مختلفة. لكن لهذه الميزات ثمن! الكائنات أثقل من المتغيرات الأولية، فهي تتطلب موارد (resources) أكثر لدعم آليتها الداخلية. نوع أولي مثل كائن هنا نجد التناقض الذي واجهه مُنشِئ JavaScript: يوجد الكثير من الأشياء التي قد يريد أحد القيام بها مع المتغيرات الأولية مثل النص أو الرقم. سيكون من الرائع الوصول إليها كتوابع. يجب أن تكون المتغيرات الأولية سريعة وخفيفة قدر الامكان. يبدو الحل صعبًا قليلًا، لكن هذا هو: تبقى المتغيرات الأولية كما هي، قيمة واحدة مثل المطلوب. تتيح لنا اللغة الوصول إلى توابع وخاصيات النصوص، الأرقام، القيم المنطقية، والرموز. حتى يعمل ذلك، يُنشَأ «كائن مغلِّف» (object wrapper) خاص يزود المتغيرات بالوظائف الإضافية، ثم يُدَمَّر. يختلف الكائن المغلِّف "object wrappers" من نوع أولي لآخر ويُسمَى: String، و Number، و Boolean، و Symbol، لذا فإنه يزود كل نوع بمجموعة مختلفة من التوابع الخاصة به. مثلا، يوجد دالة للنصوص str.toUpperCase()‎ والتي تُرجِع النص str بأحرف كبيرة. آلية عملها: let str = "Hello"; alert( str.toUpperCase() ); // HELLO الأمر بسيط، أليس كذلك؟ ما يحدث فعلا في الدالة str.toUpperCase()‎ هو كالتالي: 1- النص str هو متغير أولي، لذا فعند محاولة الوصول إلى خاصيتِه، يُنشَأ كائن خاص يعرف قيمة النص ويحتوي هذا الكائن على توابع مفيدة من بينها التابع toUpperCase()‎. 2- تعمل هذه الدالة وتُرجِع نصًا جديدًا (يمكن عرضه باستخدام alert). 3- يُدَمَّر الكائن الخاص تاركًا المتغير الأولي str. هكذا يمكن للمتغيرات الأولية أن تحوي توابعًا، وتبقى خفيفة في الوقت ذاته. يُحَسِّن محرك JavaScript هذه العملية بدرجة عالية. قد يتخطى إنشاء الكائن الإضافي، لكن يجب أن يظل قائمًا بالعمل المطلوب كما لو كان قد أنشأ الكائن. لدى الأعداد توابع خاصة بها، مثلا، toFixed(n)‎ تُقرِّب الرقم المُعطَى إلى الدقة المطلوبة: let n = 1.23456; alert( n.toFixed(2) ); // 1.23 سنرى المزيد من التوابع المخصصة في فصل النصوص والأعداد. البانيات String أو Number أو Boolean هي للاستخدام الداخلي فقط تتيح لنا بعض اللغات مثل Java إنشاء كائنات مغلِّفة "wrapper objects" بشكل صريح باستخدام صيغة مثل: new Number(1)‎ أو new Boolean(false)‎. ذلك ممكن أيضًا في JavaScript لأسباب تاريخية، لكنه غير مستحسن لأن الأمور قد تسير بشكل خاطئ في العديد من الأماكن. مثلا: alert( typeof 0 ); // "number" alert( typeof new Number(0) ); // "object"! تكون قيمة الكائنات دائمًا true في if، لذا سيتم عرض ما بداخل alert أدناه: let zero = new Number(0); if (zero) { // alert( "zero is truthy!?!" ); } تقييم قيمة المتغير zero هنا هي القيمة المنطقية true، لأنه كائن. بالمقابل، من الممكن استخدام التوابع String/Number/Boolean بدون new، إذ تقوم هذه التوابع بتحويل القيمة إلى النوع المقابل: إلى نص، أو رقم، أو قيمة منطقية (أولية). مثال: الأمر التالي صحيح تمامًا: let num = Number("123"); // تحوِّل النص إلى رقم ليس لدى القيمتان الأوليتان null/undefined توابعًا النوعان الأوليان null و undefined هما حالة استثناء، فليس لديها كائن مغلِّف (wrapper object) ولا توابع، إذ يعدان من الأنواع الأكثر أولية. ستتسبب المحاولة في الوصول إلى خاصية بظهور خطأ: alert(null.test); // error الخلاصة لدى الأنواع الأولية توابع مساعدة عدا null و undefined تسهل التعامل معها، سندرسها في الفصول اللاحقة. تعمل هذه التوابع عبر كائنات مؤقتة، لكن محرك JavaScript مُعد لتحسين العملية داخليًا. لذا فإن استدعاء الكائن لا يتطلب الكثير من الموارد. المهام هل من الممكن إضافة خاصية نصية؟ الأهمية: 5 خذ بالحسبان الشيفرة التالية: let str = "Hello"; str.test = 5; alert(str.test); ماذا تظن؟ هل ستعمل؟ هل ستُعرَض؟ الحل جرب تشغيلها: let str = "Hello"; str.test = 5; // (*) alert(str.test); يعتمد الأمر على إن كنت تستخدم use strict أم لا، قد تكون النتيجة أحد الخيارين: 1- undefined بدون استخدام الوضع الصارم 2- خطأ في الوضع الصارم لماذا؟ لنفكر فيمَ يحصل في السطر (*): 1- يُنشئ "wrapper object" عند محاولة الوصول إلى خاصية للمتغير str. 2- الكتابة إلى هذه الخاصية يُعَدُّ خطأ في الوضع الصارم. 3- في الحالة الأخرى، تستمر عملية التخزين في الخاصية، يحصل الكائن على الخاصية test. لكن، يُدمَّر الكائن بعد ذلك فلا يصبح لدى str مرجِعًا إليه مما يجعل قيمتها غير معرفة. يوضح هذا المثال أن المتغيرات الأولية ليست كائنات. ترجمة -وبتصرف- للفصل Methods of primitives من كتاب 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; } اقرأ أيضًا المقال التالي: الأعداد المقال السابق: الباني والعامل "new"
  14. ماذا يحدث عند جمع كائنين بالشكل obj1 + obj2، أو طرح كائنين obj1 - obj2 أو طباعة الكائنات باستخدام alert(obj)‎؟ تحوَّل الكائنات في مثل هذه الحالات إلى أنواع أولية (primitives) ثم تُنَفَّذ العملية. رأينا في فصل تحويل الأنواع قواعد التحويل بين الأنواع الأساسية مثل الارقام، النصوص أو القيم المنطقية، لكن بقيت فجوة متعلقة بالكائنات. الآن بعد أن تعرفنا على الرموز والدوال، يمكن ملء هذه الفجوة. 1- في السياق المنطقي (boolean)، جميع الكائنات تساوي القيمة true، إذ يوجد تحوَّل الكائنات إلى أرقام ونصوص فقط. 2- يحدث التحويل العددي (numeric) عند طرح الكائنات أو تنفيذ العمليات الرياضية عليها. مثلًا، يمكن طرح كائنات Date (سيتم شرحها في فصل التاريخ والوقت) ويكون ناتج طرح date1 - date2 هو الفارق الزمني بين التاريخين. 3- بالنسبة للتحويل إلى نص (string)، يحدث ذلك عادةً عند إخراج كائن بالطريقة alert(obj)‎ وأي سياق مشابه. التحويل إلى أنواع أساسية عبر ToPrimitive يمكننا ضبط التحويل إلى نص أو عدد باستخدام دوال خاصة بالكائنات. يوجد ثلاثة أنواع للتحويل بين الأنواع (يطلق عليها «النوع المخمَّن» [hint] الذي سيجري تحويل الكائن إليه)، مشروحة في هذه المواصفات: إلى سلسلة نصية "string" يُجرَى التحويل من كائن لنص عند القيام بعمليات على كائن تتوقع أن يكون الكائن نصًا، مثل alert: // المخرجات alert(obj); // استخدام كائن كمفتاح للخاصية anotherObj[obj] = 123; إلى عدد "number" يجري تحويل كائن لعدد عند القيام بعمليات حسابية: // تحويل صريح let num = Number(obj); // ( عمليات رياضية (عدى عملية الجمع الأحادي let n = +obj; // عملية جمع أحادية let delta = date1 - date2; // عملية موازنة أصغر/أكبر let greater = user1 > user2; إلى نوع افتراضي "default" يحدث في حالات نادرة عندما لا يكون المُشَغِّل متأكدًا من نوع البيانات المُتوَقَّع. فمثلًا، يمكن لِمُعامِل الجمع الثنائي + أن يتعامل مع كلًا من النصوص (دمج السلسلتين) ومع الأرقم (بجمعها)، لذا فإن كلًا من النصوص والأرقام تعمل بشكل صحيح. أيضًا، عند موازنة كائن بنص، أو رقم أو رمز، فلا يكون واضحًا أي نوع من التحويلات هو المطلوب. // الجمع الثنائي let total = car1 + car2; // obj == string/number/symbol if (user == 1) { ... }; يمكن لِمعاملات الموازنة أكبر/أصغر <> التعامل أيضًا مع كلً من النصوص والأعداد لكنها ترجح التحويل إلى عدد دون النظر إلى التحويل الافتراضي المرجح في هذه الحالة وذلك لأسباب تاريخية. عمليًا، تُطبِّق جميع الكائنات المُدمجة في اللغة التحويل الافتراضي "default" بنفس طريقة التحويل إلى عدد "number". وربما يجب أن نقوم بذلك أيضًا. (عدا الكائن Date، والذي سَنتعلمه لاحقَا.) لاحظ أن هناك ثلاثة أنواع للتحويل فقط، إذ الأمر بهذه البساطة. لا يوجد تحويل للنوع المنطقي (boolean، فجميع الكائنات تحمل القيمة true في السياق المنطقي) أو أي شيء آخر. وإن عاملنا كلًا من التحويل الافتراضي "default" والتحويل العددي "number" بالطريقة ذاتها كما تقوم أغلب الدوال المدمجة، فسيكون هنالك تحويلين فقط. تحاول JavaScript العثور على ثلاث دوال للكائن واستدعائها عند القيام بالتحويل استدعاء obj[Symbol.toPrimitive](hint)‎ - الدالة ذات المفتاح الرمزي Symbol.toPrimitive (رمز نظام)، إن كانت هذه الدالة موجودة. أو إن كان النوع المخمَّن هو نص "string" جرب obj.toString()‎ و obj.valueOf()‎، أيًا كانت متواجدة. إن كان hint هو عدد "number" أو "default" جرب obj.valueOf()‎ و obj.toString()‎ أيًا كانت متواجدة. تابع التحويل Symbol.toPrimitive لنبدأ من التابع الأول؛ يوجد رمز مُضَمَّن في JavaScript مُسَمَّى Symbol.toPrimitive والذي يجب استخدامه لتسمية تابع التحويل، هكذا: obj[Symbol.toPrimitive] = function(hint) { // يجب أن تُرجِع قيمة أولية // النوع المخمَّن هو إما نص أو عدد أو النوع الافتراضي }; مثلا، الكائن user هنا يتضمنها: let user = { name: "John", money: 1000, [Symbol.toPrimitive](hint) { alert(`hint: ${hint}`); return hint == "string" ? `{name: "${this.name}"}` : this.money; } }; // تجربة للتحويل: alert(user); // hint: string -> {name: "John"} alert(+user); // hint: number -> 1000 alert(user + 500); // hint: default -> 1500 كما يمكننا أن نرى في الشيفرة، أصبح user نصًا يصف نفسه أو كمية من المال وفقًا للتحويل. تقوم الدالة user[Symbol.toPrimitive]‎ بجميع حالات التحويل. التحويل إلى نص عبر toString أو valueOf ظهر التابعان toString و valueOf منذ وقت طويل، ولا يتبعان إلى نوع الرمز (symbol، إذ لم تكن الرموز موجودة في ذلك الحين)، لكنهما تابعين مسميان بأسماء توحي بارتباطهما بالنوع النصي، إذ كانت توفر آلية لعملية التحويل أصبحت قديمة النمط الآن. إن لم يكن هناك تنفيذًا للتابع Symbol.toPrimitive فتحاول JavaScript إيجاد هذه الدوال بالترتيب التالي: toString ->‏ valueOf للنصوص. valueOf ->‏ toString لباقي الأنواع. مثلا، يقوم الكائن user بنفس الغرض السابق باستخدام toString و valueOf: let user = { name: "John", money: 1000, // في حال النوع المخمَّن سلسلة نصية toString() { return `{name: "${this.name}"}`; }, // في حال النوع المخمَّن عدد أو النوع الافتراضي valueOf() { return this.money; } }; alert(user); // toString -> {name: "John"} alert(+user); // valueOf -> 1000 alert(user + 500); // valueOf -> 1500 كما رأينا، يتنفذ السلوك ذاته كما في المثال السابق الذي استخدم Symbol.toPrimitive. نحتاج غالبًا إلى مكان واحد يقوم بجميع التحويلات للأنواع الأولية (نص أو عدد). ففي هذه الحالة، يمكننا استخدام التابع toString فقط لينفِّذ جميع عمليات التحويل، كما يلي: let user = { name: "John", toString() { return this.name; } }; alert(user); // toString -> John alert(user + 500); // toString -> John500 سينفِّذ التابع toString بجميع التحويلات الأولية في غياب كلًا من Symbol.toPrimitive و valueOf. الأنواع المعادة الأمر المهم الذي يجب معرفته عن جميع دوال التحويل بين الأنواع الأولية هو أنها لا تُرجِع بالضرورة النوع المخمَّن (hint) الأولي ذاته. لا يوجد تحكم في ما إن كانت toString()‎ ترجع نصًا، أو أن Symbol.toPrimitive تُرجِع عددًا عند تحويل عدد. الأمر الوحيد المعروف والثابت هو أنَّ هذه الدوال تُرجِع نوعًا اوليًا وليس كائنًا. ملاحظة تاريخية لا يوجد خطأ إن أعاد التابع toString أو valueOf كائنًا، وذلك لأسباب تاريخية، لكن يتم تجاهل مثل هذه القيم (وكأن الدالة ليست موجودة). وذلك لعدم وجود مبدأ الخطأ الجيد (good error) في JavaScript حينها. بالمقابل، يجب أن يعيد التابع Symbol.toPrimitive قيمة أولية، وإلا فسيكون هناك خطأ. عمليات تحويل إضافية عرفنا مسبقًا أن جميع العوامل (operator) والدوال تجري عمليات تحويل على الأنواع التي تتعامل معها مثل معامل الضرب * يحول جميع المُعامَلات (operands) إلى أعداد. فإن مرَّرنا كائنًا عبر وسيط إلى إحدى الدوال أو العمليات، فستمر عملية التحويل على مرحلة أو مرحلتين هما: يحوَّل الكائن إلى نوع أولي (باستعمال القواعد التي تحدثنا عنها آنفًا) إن لم يكن النوع الأولي الناتج مطابقًا للنوع المطلوب، فيحوَّل إلى النوع المطلوب وفق مبدأ التحويل بين الأنواع الأولية إليك المثال التالي: let obj = { // جميع التحويلات في غياب باقي الدوال toString يجري التابع toString() { return "2"; } }; // حُوِّل الكائن إلى القيمةا لأولية "2", ثم جعلته عملية الضرب عددًا alert(obj * 2); // 4 حوَّلت عملية الضرب obj * 2 الكائن إلى نوع أولي، وكان النوع المعاد من عملية التحويل سلسلةً نصية، هي "2". جرى بعدئذٍ تحويل تلك السلسلة في العملية ‎"2" * 2، إلى عدد ليصبح 2 * 2. في المثال التالي، يقوم الجمع الثنائي بدمج النصوص في هذه الحالة والاكتفاء بعملية تحويل الكائن إلى سلسلة نصية: let obj = { toString() { return "2"; } }; // (أعادت عملية التحويل إلى نوع أولي نصًا => دمج) alert(obj + 2); // 22 الخلاصة تجرَى عملية التحويل من كائن لنوع أولي تلقائيًا بواسطة العديد من الدوال المدمجة في اللغة والمُعاملات التي تتوقع أن تتعامل مع قيم أولية. يخمَّن النوع الأولي المراد تحويل الكائن إليه إلى: سلسلة نصية "string" (للدالة alert والعمليات الأخرى التي تتعامل مع نصوص) عدد "number" (للعمليات الرياضية) -النوع الافتراضي "default" (لبعض العوامل) تُحدِّد المواصفات النوع المخمَّن (hint) الذي يستخدمه كل مُعامل بوضوح. يوجد القليل من العوامل التي لا تعلم ما النوع المتوقع وتستخدم النوع الافتراضي "default". يُعامل النوع الافتراضي "default" معامل العدد "number" في الكائنات أغلب الأحيان؛ لذا، يتم دمج النوعين عمليًا. خوارزمية التحويل كالتالي: استدعاء obj[Symbol.toPrimitive](hint)‎ إن وُجِدَت، أو إن كان النوع المخمَّن هو نص "string" جرب obj.toString()‎ و obj.valueOf()‎، أيًا كانت متواجدة. إن كان النوع المُخمَّن هو عدد "number" أو "default" جرب obj.valueOf()‎ و obj.toString()‎ أيًا كانت متواجدة. عمليًا، يكفي استخدام obj.toString()‎ فقط لإجراء جميع التحويلات، إذ تعيد "شيئًا مقروءًا" يمثِّل الكائن يستعمل هذا التمثيل لعملية التسجيل أو التنقيح. ترجمة -وبتصرف- للفصل Object to primitive conversion من كتاب 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; } اقرأ أيضًا المقال التالي: الباني والعامل "new" المقال السابق: الدوال في الكائنات واستعمالها this
  15. تتطور لغة جافاسكربت باستمرار نتيجةً لوجود العديد من المقترحات المبنية على الحاجة إلى تطويرها دوريًا. تُحلَّل هذه المقترحات وتُضاف إن كانت جيدة إلى القائمة https://tc39.github.io/ecma262/ ثم النظر في وصفها. لدى الفِرَق العاملة على محركات جافاسكربت أفكارها الخاصة حول ما يجب تنفيذه أولًا. قد تقرر هذه الفِرَق تنفيذ مقترح ما زال مسودة وتأجيل أشياء أصبح وصفها جاهزا لأنها قد تكون أقل أهمية أو صعبة التنفيذ. لذلك، نجد أن هذه المحركات تتضمن المعايير المتعارفة فقط. لترى الميزات التي تدعمها اللغة، انظر في هذه الصفحة https://kangax.github.io/compat-table/es6/ (يوجد الكثير من الميزات، فما زال لدينا الكثير لنتعلمه). Babel تفشل بعض المحركات في دعم بعض الشيفرات البرمجية عند استخدام ميزات اللغة الحديثة. فكما ذكرنا سابقا، ليست جميع الميزات مُضَمَّنة في كل مكان. هنا يأتي دور Babel لإصلاح الوضع. يُعد Babel مُفَسِّرًأ تحويليًا transpiler، إذ يعيد كتابة شيفرة جافاسكربت حديثة بإصدار سابق. في الواقع، يوجد جزئين في Babel: أولا: برنامج المُفَسِّر التحويلي (transpiler): يعيد كتابة الشيفرة. يُشَغله المطورون على أجهزتهم. يقوم يعيد البرنامج كتابة الشيفرة البرمجية كتابةً متوافقةً مع إصدار سابق للغة ثم يتم توصيل الكود للمستخدمين إلى الموقع. توفر مشاريع بناء الأنظمة الحديثة مثل webpack الوسائل المناسبة لتشغيل المُفَسِّرالتحويلي (transpiler) تلقائيا مع كل تغيير ِللشيفرة البرمجية ما يجعل الاندماج في عملية التطوير أسهل. ثانيا: برنامج لتعويض نقص دعم المتصفحات (Polyfills): قد تتضمن ميزات اللغة الجديدة بعض الوظائف المدمجة وهياكل الجُمَل. يعيد المُفَسِّر التحويلي كتابة الشيفرة البرمجية مُحولًا هياكل الجمل إلى إصدارات أقدم، لكن يجب تضمين الوظائف المُدمجة الجديدة. تُعد جافاسكربت لغة ديناميكية للغاية، وقد تضيف/تعدل السكريبتات أي دالة حتى تتعامل وفقا للمعايير الحديثة. السكريبت الذي يضيف/يحدث دالة جديدة يسمى "polyfill"، معوِّض نقص الدعم. فهو يغطي الفجوة وإضافة المحتوى المفقود. برامج تعويض نقص دعم المتصفحات التي قد تثير اهتمامك: core js تدعم الكثير وتصمح بتضمين الميزات المُرادة فقط. polyfill.io هي خدمة تزود السكريبت بوسائل تعويض نقص دعم المتصفحات وفقا للميزات والمتصفح. لذلك، إن احتجت استخدام ميزات اللغة الحديثة، فمن المهم استخدام مُفَسِّرتحويلي و برنامج دعم نقص المتصفحات. أمثلة من الشرح معظم الأمثلة قابلة للتشغيل في مكانها مثل: alert('Press the "Play" button in the upper-right corner to run'); لن تعمل الأمثلة التي تستخدم جافاسكربت حديثة إلا في المتصفحات التي تدعمها. يُعد متصفح جوجل كروم الأكثر حداثة دوما مع ميزات اللغة الحديثة، من الجيد تشغيل العروض الحديثة بدون أي مُفَسِّرتحويلي. تعمل العديد من المتصفحات الأخرى المُحدَّثة جيدا. ترجمة -وبتصرف- للفصل Polyfills من كتاب 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; } اقرأ أيضًا المقال التالي: الكائنات المقال السابق: الاختبار الآلي باستخدام mocha
  16. حزمة LEMP هي مجموعة من البرامج التي يمكن استخدامها لتٌقدّم صفحات الويب الديناميكية وتطبيقات الويب. وهو عبارة عن اختصار يصف نظام تشغيل لينكس مع خادم Nginx (يُنطق "Engine-X"). البيانات الخلفية تكون مخزنة في قاعدة بيانات MySQL وتقوم PHP بالمعالجة الديناميكية. يشرح هذا المقال كيفية تثبيت حزمة LEMP على خادم أوبونتو 18.04. يتكفل نظام أوبونتو بالمتطلب الأول. سنشرح هنا كيفية تجهيز باقي المكونات وتشغيلها. المتطلبات قبل البدء، تحتاج لمستخدم عادي غير مسؤول بصلاحيات sudo. يمكنك إعداد ذلك باتباع مقال الإعداد الأولي لخادم أوبونتو 18.04. عندما تنتهي من تجهيز المستخدم، يمكنك بدء الخطوات التالية. خطوة 1 - تثبيت خادم الويب Nginx كي نتمكن من عرض صفحات الويب لزوار الموقع، سنثبِّت خادم الويب الحديث والفعال Nginx. جميع البرامج المستخدمة في هذا المقال متوفرة في مخزن حزم أوبونتو الافتراضي. ما يعني أنه يمكننا استخدام نظام إدارة الحزم apt لإنهاء التثبيتات الهامة. في أول مرة نَستخدم فيها apt في أي جلسة، نبدأ بتحديث محتوى الحزمة ثم نثبت الخادم: $ sudo apt update $ sudo apt install nginx Nginx مُعد كي يعمل تلقائيا بعد التثبيت على أوبونتو 18.04. إن كان جدار حماية ufw يعمل لديك كما تم الشرح في مقال الإعداد الأولي، فستحتاج للسماح باتصالات Nginx. يُسجل Nginx في ufw تلقائيا خلال التثبيت ما يجعل العملية أسهل. يُستحسن السماح للمنفذ الذي سيتيح لحركة المرور بالوصول مع أعلى قيود ممكنة. لأننا لم نقم بإعداد SSL بعد، فستحتاج لإتاحة حركة المرور في المنفذ 80 فقط. لتفعيل ذلك: $ sudo ufw allow 'Nginx HTTP' للتأكد من التغييرات: $ sudo ufw status سَتعرض مخرجات هذا الأمر أن حركة المرور خلال HTTP متاحة: المخرجات: Status: active To Action From -- ------ ---- OpenSSH ALLOW Anywhere Nginx HTTP ALLOW Anywhere OpenSSH (v6) ALLOW Anywhere (v6) Nginx HTTP (v6) ALLOW Anywhere (v6) بعد إضافة هذا الإعداد إلى جدار الحماية يمكنك اختبار إن كان الخادم يعمل عبر الدخول إلى اسم نطاق الخادم أو عنوان بروتوكول الإنترنت العام للخادم عبر المتصفح. إن كنت لا تملك اسم نطاق محدد للخادم وكنت لا تعلم عنوان بروتوكول الإنترنت العام للخادم يمكنك إيجاده من خلال تنفيذ لأمر: $ ip addr show eth0 | grep inet | awk '{ print $2; }' | sed 's/\/.*$//' هذا الأمر سيزودك بمجموعة من عناوين بروتوكول الإنترنت، يمكنك تجريبها على متصفحك. كطريقة بديلة، يمكنك التحقق إن كان عنوان بروتوكول الإنترنت متاح من مكان آخر على الإنترنت: $ curl -4 icanhazip.com أدخِل عنوان بروتوكول الإنترنت الذي حصلت عليه في متصفحك، وستظهر لك صفحة Nginx الرئيسية: http://server_domain_or_IP (صورة) إن رأيت الصفحة التي في الأعلى، فإن Nginx مُثبّت بنجاح. خطوة 2 - تثبيت MySQL لإدارة بيانات الموقع الآن وبما أنه أصبح لديك خادم ويب، ستحتاج لِتثبيت MySQL (نظام إدارة قواعد بيانات) وإدارة بيانات الموقع. ثبّت MySQL باستخدام الأمر: $ sudo apt install mysql-server تم تثبيت قاعدة بيانات MySQL لكن إعدادها لم يكتمل بعد. لتأمين التثبيت، تُرفق MySQL بسكربت يسألك إن كنت تريد تغيير الإعدادات الافتراضية الغير آمنة. لبدء السكربت: $ sudo mysql_secure_installation سيسألك السكربت إن كنت تريد إعداد VALIDATE PASSWORD PLUGIN. تحذير: يجب أن تفهم هذه الميزة قبل تفعيلها. فعند تفعيل هذه الميزة، سيرفض MySQL أي كلمة مرور لا تطابق معاييره المحددة وسيظهر خطأ. مما يتسبب في بعض المشاكل في حال كنت تستخدم كلمة مرور ضعيفة بالإضافة إلى برنامج يُعِد بيانات مستخدمي MySQL تلقائيا مثل حزمة phpMyAdmin. يُفضل عدم تفعيل هذا الأمر لتفادي مثل هذه الأخطاء. لكن، يُفضَّل أن تستخدم كلمات مرور قوية وفريدة لمستخدمي قواعد البيانات دائما. أجب Y للتفعيل أو أي حرف آخر للاستمرار بدون تفعيل الإضافة. VALIDATE PASSWORD PLUGIN can be used to test passwords and improve security. It checks the strength of password and allows the users to set only those passwords which are secure enough. Would you like to setup VALIDATE PASSWORD plugin? Press y|Y for Yes, any other key for No: في حال اخترت تفعيل التحقق، فَسَيسألك السكربت لاختيار مستوى تحقق كلمة المرور. تذكر أنه إن اخترت 2 -أقوى مستوى - فسترى بعض الأخطاء عند إدخال كلمة مرور لا تحتوي على أرقام، أحرف كبيرة وصغيرة، ورموز أو أي كلمة اعتيادية من قاموس اللغة. There are three levels of password validation policy: LOW Length >= 8 MEDIUM Length >= 8, numeric, mixed case, and special characters STRONG Length >= 8, numeric, mixed case, special characters and dictionary file Please enter 0 = LOW, 1 = MEDIUM and 2 = STRONG: 1 ثم سيُطلب منك إدخال وتأكيد كلمة مرور مستخدم مسؤول: Please set the password for root here. New password: Re-enter new password: لباقي الأسئلة قم بالضغط على Y ثم ENTER في كل شاشة. بهذه الطريقة، سيتم حذف بعض المستخدمين المجهولين، وفحص قاعدة البيانات، وتعطيل تسجيل دخول المستخدمين المسؤولين عن بعد، ويعيد تحميل الإعدادات الجديدة كي يقوم MySQL بتطبيقها مبشرة. لاحظ أنه في اصدارات MySQL 5.7 وما يليها على أوبونتو يُعد المستخدم المسؤول للمصادقة باستخدام إضافة auth_socket افتراضيا بدلا من التصديق باستخدام كلمة مرور. هذه الميزة توفر أمانًا أكبر وسهولة استخدام في حالات متعددة، لكنها قد تكون معقدة عندما تريد السماح لبرامج خارجية مثل (phpMyAdmin) بالوصول للمستخدم. إن كان استخدام إضافة auth_socket للوصول إلى MySQL يتناسب مع طبيعة عملك يمكنك الاستمرار بالخطوة 3. لكن إن كنت تفضل استخدام كلمة المرور للاتصال ب MySQL كمستخدم مسؤول فستحتاج لتغيير طريقة مصادقته من auth_socket إلى mysql_native_password. للقيام بذلك، افتح شاشة اوامر MySQL: $ sudo mysql ثم قم بالتحقق من طريقة المصادقة التي يستخدمها كل حساب من مستخدمي MySQL باستخدام الأمر: mysql> SELECT user,authentication_string,plugin,host FROM mysql.user; المخرجات: +------------------+-------------------------------------------+-----------------------+-----------+ | user | authentication_string | plugin | host | +------------------+-------------------------------------------+-----------------------+-----------+ | root | | auth_socket | localhost | | mysql.session | *THISISNOTAVALIDPASSWORDTHATCANBEUSEDHERE | mysql_native_password | localhost | | mysql.sys | *THISISNOTAVALIDPASSWORDTHATCANBEUSEDHERE | mysql_native_password | localhost | | debian-sys-maint | *CC744277A401A7D25BE1CA89AFF17BF607F876FF | mysql_native_password | localhost | +------------------+-------------------------------------------+-----------------------+-----------+ 4 rows in set (0.00 sec) يمكنك ملاحظة أن المستخدم المسؤول يستخدم التصديق بإضافة auth_socket. لإعداد حساب المستخدم المسؤول للمصادقة بكلمة مرور، نفّذ أمر ALTER USER التالي. تأكد من استخدامك كلمة مرور قوية من اختيارك: mysql> ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password'; ثم نفّذ الأمر FLUSH PRIVILEGES والذي سيجعل الخادم يعيد جداوِل الصلاحيات حتى تعمل التغييرات التي أجريتها: mysql> FLUSH PRIVILEGES; تحقق من طرق التصديق لكل مستخدام مجددًا لتتأكد من أن المستخدم المسؤول لم يعد يستخدم المصادقة باضافة auth_socket: mysql> SELECT user,authentication_string,plugin,host FROM mysql.user; المخرجات: +------------------+-------------------------------------------+-----------------------+-----------+ | user | authentication_string | plugin | host | +------------------+-------------------------------------------+-----------------------+-----------+ | root | *3636DACC8616D997782ADD0839F92C1571D6D78F | mysql_native_password | localhost | | mysql.session | *THISISNOTAVALIDPASSWORDTHATCANBEUSEDHERE | mysql_native_password | localhost | | mysql.sys | *THISISNOTAVALIDPASSWORDTHATCANBEUSEDHERE | mysql_native_password | localhost | | debian-sys-maint | *CC744277A401A7D25BE1CA89AFF17BF607F876FF | mysql_native_password | localhost | +------------------+-------------------------------------------+-----------------------+-----------+ 4 rows in set (0.00 sec) يمكنك رؤية أن مستخدم MySQL المسؤول أصبح يستخدم المصادقة بكلمة المرور. بعد تأكدك من هذا على الخادم الخاص بك أغلق شاشة اوامر MySQL: mysql> exit ملاحظة: بعد تغييرك لطريقة مصادقة المستخدم المسؤول إلى كلمة مرور، فلن تستطيع الوصول إلى MySQL باستخدام الأمر sudo mysql المُستخدم سابقا، بدلا عن ذلك يجب أن تنفذ الأمر التالي: $ mysql -u root -p بعد ادخال كلمة المرور التي أعددتها قبل قليل، ستُفتح لك شاشة اوامر MySQL. هكذا يكون نظام قواعد البيانات جاهزًا ويمكنك الانتقال إلى خطوة تثبيت PHP. خطوة 3 - تثبيت PHP وإعداد Nginx لاستخدام معالج PHP أصبح لديك الآن Nginx لعرض صفحات الويب، و MySQL لتخزين وإدارة البيانات. لكن ليس لديك شيء يقوم بتوليد صفحات ويب ديناميكية. هنا يأتي دور PHP. لأن Nginx لا يحتوي على وحدة معالجة PHP مثل باقي خوادم الويب، ستحتاج لتثبيت php-fpm والتي تعني "fastCGI process manager". سنجعل Nginx يُمرر طلبات PHP إلى هذا البرنامج للمعالجة. ملاحظة: اعتمادًا على مزود الخدمة السحابية لديك، قد تحتاج لإضافة مخزن أوبونتو universe الذي يتضمن برامج مجانية ومفتوحة المصدر أنشئت بواسطة مجتمع أوبونتو؛ يمكنك القيام بذلك قبل تثبيت php-fp: $ sudo add-apt-repository universe ثبّت php-fpm مع الحزمة المساعدة php-mysql التي تسمح ل PHP بالتواصل مع قاعدة البيانات. سيجلب التثبيت ملفات PHP الرئيسية: $ sudo apt install php-fpm php-mysql الآن أصبح لديك جميع مكونات حزمة LEMP، لكن ما زلت تحتاج لتعديل بعض إعدادت التكوين حتى تجعل Nginx بتعامل مع معالج PHP لعرض المحتوى الديناميكي. يتم ذلك على مستوى أجزاء الخادم (أجزاء الخادم تشبه مُستضيفوا Apache الوهميون). للقيام بذلك، افتح ملف تكوين جزء خادم جديد في مجلد /etc/nginx/sites-available/. في هذا المثال، سنُسمي ملف جزء السيرفر example.com. يمكنك اختيار الاسم الذي تريده: $ sudo nano /etc/nginx/sites-available/example.com ستتمكن بتعديل ملف تكوين جزء خادم جديد بدلا من تعديل الجزء الافتراضي من استعادة الاعدادات الافتراضية في حال احتجتها. أضف المحتوى التالي المأخوذ من ملف تكوين جزء الخادم الرئيسي مع بعض التعديلات الطفيفة إلى ملف تكوين جزء الخادم الجديد: /etc/nginx/sites-available/example.com server { listen 80; root /var/www/html; index index.php index.html index.htm index.nginx-debian.html; server_name example.com; location / { try_files $uri $uri/ =404; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/var/run/php/php7.2-fpm.sock; } location ~ /\.ht { deny all; } } هذا ما تقوم به التوجيهات وأجزاء التحديد: listen - تُعرف المنفذ الذي يستمع له Nginx، في هذه الحالة يستمع Nginx إلى المنفذ 80، المنفذ الافتراضي ل HTTP. root - يُعرّف المجلد الرئيسي حيث تُخزن ملفات الموقع. index - يقوم بإعداد Nginx كي يجعل الأولوية لعرض الملفات المسماه index.php - إن كانت متاحة عند طلب ملف index. server_name - تعرّف الجزء الذي يجب أن يُستخدم لطلب ما للخادم. حدد هذه التوجيهة إلى اسم نطاق الخادم أو عنوان بروتوكول الإنترنت للخادم. location /‎ - أول جزء تحديد يحتوي توجيهة try_files والتي تتحقق من وجود ملفات تطابق طلب عنوان URI. إن لم يجد Nginx الملف المناسب، يقوم بإرجاع خطأ 404. "location ~ \.php$‎" - جزء التحديد هذا يهتم بالمعالجة الفعلية ل PHP عبر توجيه Nginx إلى ملف تكوين fastcgi-php.conf وملف php7.2-fpm.sock الذي يُعرّف الحزمة المرتبطة ب php-fpm. location ~ /\.ht - آخر جزء تحديد والذي يتعامل مع ملف .htaccess الذي لا يقوم Nginx بمعالجته. بإضافة توجيهة deny all فإن ملفات "‎.htaccess" التي قد تتواجد في المجلد الرئيسي لن تُعرض للزائر. بعد إضافة هذا المحتوى، احفظ الملف واغلقه. فعّل جزء الخادم الجديد بإنشاء رابط رمزي من ملف تكوين جزء الخادم الجديد في المجلد /etc/nginx/sites-available/ إلى المجلد /etc/nginx/sites-enabled/: $ sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/ ثم ألغِ ربط ملف التكوين الرئيسي من مجلد /sites-enabled/: $ sudo unlink /etc/nginx/sites-enabled/default ملاحظة: إن احتجت لاستعادة الإعدادت الافتراضية، يمكنك ذلك بإعادة إنشاء الرابط الرمزي: $ sudo ln -s /etc/nginx/sites-available/default /etc/nginx/sites-enabled/ افحص ملف التكوين الجديد من الأخطاء الإملائية : $ sudo nginx -t إن ظهرت أي أخطاء عد وتحقق من الملف قبل الاستمرار. عند انتهائك أعد تشغيل Nginx كي تُطبّق التغييرات: $ sudo systemctl reload nginx وبهذا ننتهي من تثبيت وإعداد حزمة LEMP. لكن ما زلنا لم نتأكد ما إن كانت جميع المكونات تتواصل ببعضها البعض. خطوة 4 - انشاء ملف PHP للتحقق من الاعدادات الآن، يجب أن تكون حزمة LEMP مُعدة بشكل تام. يمكنك اختبار ذلك للتحقق من أن Nginx يعالج ملفات "‎.php" من معالج PHP بطريقة صحيحة. للقيام بذلك استخدم محرر النصوص لإنشاء ملف PHP تجريبي وسمّه info.php في المجلد الرئيسي. $ sudo nano /var/www/html/info.php أدخل الأسطر التالية إلى الملف. هذا الكود هو كود PHP صحيح والذي سيعرض معلومات عن الخادم: الملف ‎/var/www/html/info.php: <?php phpinfo(); عند انتهائك احفظ الملف واغلقه. يمكن الآن زيارة هذه الصفحة من متصفحك بزيارة اسم نطاق الخادم أو عنوان بروتوكول الإنترنت العام للخادم متبوعا ب /info.php: http://your_server_domain_or_IP/info.php يجب أن ترى صفحة ويب تم توليدها باستخدام PHP تحتوي معلومات الخادم: (صورة) إن رأيت صفحة تشبه الصورة السابقة، فإن إعدادك لمعالج PHP مع Nginx صحيح. بعد التحقق من أن Nginx يعالج الصفحات بصورة صحيحة، يُفضل أن تحذف الملف الذي أنشاته لأنه قد يعطي المستخدمين المخولين بالوصول بعض المعلومات عن إعداد الخادم مما قد يساعدهم في محاولة اقتحامه. يمكنك توليد هذا الملف عند احتياجك له. قم بحذف الملف: $ sudo rm /var/www/html/info.php وبهذا تكون قد أعددت وشغّلت حزمة LEMP على خادم أوبونتو 18.04. الخلاصة تعد حزمة LEMP هيكل قوي يمكنك من بناء وتشغيل أي موقع أو تطبيق ويب تقريبا من الخادم الخاص بك. يوجد العديد من الخطوات التي يمكنك القيام بها بعد هذه الخطوة. مثلا، يجب أن تتأكد من أن الاتصالات إلى الخادم آمنة. ختاما، يمكنك تأمين تثبيت Nginx باستخدام تشفير Let's. باتباع هذا المقال، سوف تحصل على شهادة TLS/SSL للخادم الخاص بك كي يخدم المحتوى عبر HTTPS. ترجمة -وبتصرف- للمقال How To Install Linux, Nginx, MySQL, PHP (LEMP stack) on Ubuntu 18.04 لأصحابه الكتاب Mark Drake و Justin Ellingwood.
  17. MongoDB عبارة عن قاعدة بيانات NoSQL مستندات مجانية ومفتوحة المصدر تُستخدم غالبا في تطبيقات الويب الحديثة. في هذا المقال سنُثبِّت MongnDB، ونُدير خدماتها، ونحميها، ونُفعل الوصول البعيد لها. المتطلبات تحتاج لتطبيق هذا الدرس إلى خادم أوبونتو مُعَد باتباع مقال الإعداد الأولي، ويحتوي على مستخدم sudo غير مسؤول وجدار حماية. الجزء الأول: تثبيت MongoDB خطوة 1 - تثبيت MongoDB حزمة مخزن أوبونتو الرسمي يحتوي على نسخة مُحدثه من MongoDB، ما يعني أنه بإمكاننا تثبيت حزماتِه المهمة باستخدام apt. بداية حدِّث قائمة الحُزم كي تحصل على أحدث الاصدارات: $ sudo apt update الآن ثبّت حزمة MongoDB: $ sudo apt install -y mongodb يُثبّت هذا الأمر عدة حُزم تحتوي على أحدث إصدار من MongoDB بالاضافة إلى أدوات إدارة مساعدة لخادم MongoDB. يبدأ خادم قاعدة البيانات بالعمل تلقائيا بعد التثبيت. تاليًا، نتحقق ما إن كان الخادم بعمل بصورة صحيحة. خطوة 2 - التحقق من الخدمة وقاعدة البيانات قامت عملية تثبيت MongoDB بتشغيله تلقائيا، لكن لنتأكد من بدء الخدمة ومن صحة عمل قاعدة البيانات. أولا تحقق من حالة الخدمة: $ sudo systemctl status mongodb سترى هذه المخرجات: ● mongodb.service - An object/document-oriented database Loaded: loaded (/lib/systemd/system/mongodb.service; enabled; vendor preset: enabled) Active: active (running) since Sat 2018-05-26 07:48:04 UTC; 2min 17s ago Docs: man:mongod(1) Main PID: 2312 (mongod) Tasks: 23 (limit: 1153) CGroup: /system.slice/mongodb.service └─2312 /usr/bin/mongod --unixSocketPrefix=/run/mongodb --config /etc/mongodb.conf يصرِّح systemd أنَّ خادم MongoDB يعمل. يمكننا التحقق أكثر عن طريق الاتصال بخادم قاعدة البيانات وتنفيذ أمر للفحص .نفّذ الأمر التالي: $ mongo --eval 'db.runCommand({ connectionStatus: 1 })' المخرجات: MongoDB shell version v3.6.3 connecting to: mongodb://127.0.0.1:27017 MongoDB server version: 3.6.3 { "authInfo" : { "authenticatedUsers" : [ ], "authenticatedUserRoles" : [ ] }, "ok" : 1 } القيمة 1 للحقل ok تعني أن الخادم يعمل بطريقة صحيحة. بعد ذلك، سنتطرق إلى كيفية إدارة حالة الخادم. خطوة 3 - إدارة خدمة MongoDB تُثبت MongoDB كخدمة ضمن النظام، ما يعني أنه يُمكنك إدارتها باستخدام أوامر systemd القياسية مثل باقي خدمات النظام على أوبونتو. لتتأكد من حالة الخدمة نفذ الأمر: $ sudo systemctl status mongodb يمكنك إيقاف الخادم في أي وقت من خلال الأمر: $ sudo systemctl stop mongodb لبدء الخادم عندما يكون متوقفًا عن العمل: $ sudo systemctl start mongodb يمكن إعادة تشغيل الخادم باستخدام أمر واحد: $ sudo systemctl restart mongodb MongoDB مُعد لِلبدء تلقائيا مع بدء الخادم، لإيقاف البدء التلقائي: $ sudo systemctl disable mongodb لتفعيل الميزة مجددا: $ sudo systemctl enable mongodb تاليًا، إعداد جدار الحماية لعملية تثبيت MongoDB. خطوة 4 - إعداد جار الحماية (اختياري) بافتراض أنك اتبعت مقال الإعداد الأولي لخادم أوبونتو لِتفعيل جدار الحماية، فإن خادم MongoDB لا يمكن أن يُوصل من خلال الإنترنت. في حال كنت تنوي استخدام خادم MongoDB محليا مع التطبيقات على نفس الخادم فقط فهذا الإعداد هو الأمثل. لكن إن كنت تريد أن تتصل بخادم قاعدة بيانات MongoDB من خلال الانترنت فيجب أن تسمح للاتصالات الخارجية في ufw. للسماح بالوصول من أي مكان إلى MongoDB على المنفذ الافتراضي 27017 يمكنك استخدام sudo ufw allow 27017. لكن تفعيل وصول الإنترنت إلى خادم MongoDB في التثبيت الافتراضي يعطي دخول غير محدود بضوابط لِخادم قاعدة البيانات وبياناته. في معظم الحالات يجب الوصول إلى MongoDB من أماكن موثوقة معينة فقط، مثل خادم آخر يستضيف تطبيقا مًا. للقيام بذلك، يمكنك السماح بالوصول إلى منفذ MongoDB الافتراضي مع تحديد عنوان بروتوكول الإنترنت للخادم الآخر الذي سيتمكن من الوصول إلى خادم MongoDB مباشرة: $ sudo ufw allow from your_other_server_ip/32 to any port 27017 يمكنك التحقق من التغييرات في جدار الحماية باستخدام ufw: $ sudo ufw status يجب أن ترى أن حركة المرور للمنفذ 27017 متاحة في المخرجات "output": Status: active To Action From -- ------ ---- OpenSSH ALLOW Anywhere 27017 ALLOW Anywhere OpenSSH (v6) ALLOW Anywhere (v6) 27017 (v6) ALLOW Anywhere (v6) إن كنت قد قمت بتحديد عنوان بروتوكول إنترنت معين للاتصال بخادم MongoDB، فستراه بدلا من كلمة Anywhere في المخرجات. للمزيد من إعدادات جدار الحماية المتقدمة لحد الوصول إلى خدمات UFW الأساسية: قواعد وأوامر جدار الحماية الشائعة. على الرغم من أنَّ المنفذ مفتوح، إلا أن MongoDB يسمع فقط للعنوان المحلي 127.0.0.1. للسماح بالوصول من بعد، أضف عنوان بروتوكول الخادم العام و القابل للتوجيه إلى ملف mongod.conf. افتح ملف ضبط MongoDB: $ sudo nano /etc/mongodb.conf أضف عنوان بروتوكول الانترنت الخاص بالخادم إلى قيمة bindIP: ... logappend=true bind_ip = 127.0.0.1,your_server_ip #port = 27017 ... لا تنسَ وضع فاصلة بين عناوين بروتوكول الإنترنت الموجودة والعنوان الذي أضفته الآن. احفظ الملف واغلقه، ثم أعد تشغيل MongoDB: $ sudo systemctl restart mongodb الجزء الثاني: تأمين MongoDB كانت الاصدارات السابقة ل MongoDB مُعرَّضة للاختراق لأنَّها لا تتطلب أي مصادقة للتفاعل مع قاعدة البيانات. يمكن لأي مستخدم إنشاء وحذف قواعد بيانات وقراءة محتواها والكتابة فيها تلقائيا. أعدَّت أيضا هذه الاصدارات برنامج MongoDB خفي يسمع لجميع الواجهات تلقائيًا، مما يعني أن السكربتات التلقائية يمكن أن تكتشف حالات MongoDB الغير محمية بجدار حماية، وإن لم تكُن المصادقة مفعلة، يمكن لهذه السكربتات الحصول على وصول كامل إلى MongoDB. خُفِّفَت الحالة في الاصدار ‎3.x وبعض الاصدارات السابقة المُقدَّمة من بعض مديري الحُزم لأن البرنامج الخفي أصبح الآن مرتبط ب 127.0.0.1، لذلك سيقبل الاتصالات على حزمة يونكس فقط. أي أنه أصبح غير مفتوحًا للإنترنت. لأن المصادقة ما زالت مُعَطَّلة افتراضيا، فإن لدى أي مستخدم على النظام المحلي وصول كامل لقاعدة البيانات. سنُنشِئ مستخدمًا مسؤولًا لتأمين ذلك، ونُفَعِّل المصادقة ونتحقق من ذلك. خطوة 1 - إضافة مستخدم مسؤول سنتَّصِل بسطر أوامر Mongo لإضافة مستخدم: $ mongo تُحذِّر المُخرجات عندما نستخدم سطر أوامر Mongo من أن التحكم بالوصول معطل لقاعدة البيانات وأن وصول القراءة والكتابة للبيانات والإعدادت غير مقيدة. مخرجات تنفيذ الأمرالسابق هي: MongoDB shell version v3.4.2 connecting to: mongodb://127.0.0.1:27017 MongoDB server version: 3.4.2 Welcome to the MongoDB shell. For interactive help, type "help". For more comprehensive documentation, see http://docs.mongodb.org/ Questions? Try the support group http://groups.google.com/group/mongodb-user Server has startup warnings: 2017-02-21T19:10:42.446+0000 I STORAGE [initandlisten] 2017-02-21T19:10:42.446+0000 I STORAGE [initandlisten] ** WARNING: Using the XFS filesystem is strongly recommended with the WiredTiger storage engine 2017-02-21T19:10:42.446+0000 I STORAGE [initandlisten] ** See http://dochub.mongodb.org/core/prodnotes-filesystem 2017-02-21T19:10:42.534+0000 I CONTROL [initandlisten] 2017-02-21T19:10:42.534+0000 I CONTROL [initandlisten] ** WARNING: Access control is not enabled for the database. 2017-02-21T19:10:42.534+0000 I CONTROL [initandlisten] ** Read and write access to data and configuration is unrestricted. 2017-02-21T19:10:42.534+0000 I CONTROL [initandlisten] > يمكنك اختيار الاسم الذي تريده للمستخدم المسؤول لأنَّ الصلاحيات تأتي من إعطاء الدور userAdminAnyDatabase. تُحدد قاعدة البيانات admin أين تُخَزَّن بيانات الاعتماد. يمكنك الاطلاع أكثر عن المصادقة في جزء مصادقة أمنية MongoDB. حدد اسم مستخدم وتأكد من اختيار كلمة مرور آمنة واستبدِلهما في الأمر التالي: > use admin > db.createUser( > { > user: "AdminSammy", > pwd: "AdminSammy'sSecurePassword", > roles: [ { role: "userAdminAnyDatabase", db: "admin" } ] > } > ) عند بدء أمر db.createUser، فإن سطر الأوامر سيضع ثلاث نقاط قبل كل سطر حتى انتهاء الأمر. بعد ذلك، يظهر رد بأن المستخدم قد أُنشِئ كالتالي: المخرجات: > use admin switched to db admin > db.createUser( ... { ... user: "AdminSammy", ... pwd: "AdminSammy'sSecurePassword", ... roles: [ { role: "userAdminAnyDatabase", db: "admin" } ] ... } ... ) Successfully added user: { "user" : "AdminSammy", "roles" : [ { "role" : "userAdminAnyDatabase", "db" : "admin" } ] } اكتب "exit" ثم اضغط على ENTER أو اضغط على CTRL+C للخروج. سَيُتاح الآن للمستخدم إدخال بيانات الاعتماد، لكن لن يُطلَب هذا من المستخدام حتى تفعيل المصادقة وإعادة تشغيل برنامج MongoDB الخفي. خطوة 2 - تفعيل المصادقة المصادقة مفعلة في ملف mongod.conf. عند تفعيلها وإعادة تشغيل mongod، سيظل المستخدمون قادرون على الاتصال ب Mongo بدون مصادقة، لكن سيُطلَب منهم اسم مستخدم وكلمة مرور قبل أي تفاعل. لنفتح ملف الإعداد: $ sudo nano /etc/mongod.conf في الجزء ‎#security، ستُزيل التعليق من أمام security لتفعيل المقطع. ثم سنُضيف إعداد المصادقة. يجب أن تبدو الأسطر كما في الأسفل بعد إلغاء تعليقها: الملف mongodb.conf: . . . security: authorization: "enabled" . . . لاحظ أنَّ سطر "Security" لا يحوي مسافة في بدايته، بينما سطر "authorization" يجب أن يبدأ بِمسافتين. نُعيد تشغيل البرنامج الخفي بعد حفظ الملف وإغلاقه: $ sudo systemctl restart mongod إن أخطأنا في ملف الإعداد فلن يبدأ البرنامج الخفي بالعمل. ولأن systemctl لا تعرض أي مخرجات فَسنستخدم خيار status للتحقق من ذلك: $ sudo systemctl status mongod إن ظهر Active: active (running)‎ في المخرجات وانتهى بشيء كما يلي، فإن أمر restart قد نُفِّذ بنجاح: المخرجات: Jan 23 19:15:42 MongoHost systemd[1]: Started High-performance, schema-free document-oriented database. بعد التأكد من أن البرنامج الخفي يعمل، لنُجَرِّب المصادقة. خطوة 3 - التحقق من منع المستخدمين غير المخولين أولا، نحاول الاتصال بدون معلومات اعتماد للتحقق من أن الدخول ممنوع: $ mongo تم حل جميع التحذيرات بعد تفعيل المصادقة. المخرجات: MongoDB shell version v3.4.2 connecting to: mongodb://127.0.0.1:27017 MongoDB server version: 3.4.2 اتصلنا بقاعدة بيانات test. سَنتأكد من أن الوصول ممنوع باستخدام الأمر show dbs: > show dbs المخرجات: 2017-02-21T19:20:42.919+0000 E QUERY [thread1] Error: listDatabases failed:{ "ok" : 0, "errmsg" : "not authorized on admin to execute command { listDatabases: 1.0 }", "code" : 13, "codeName" : "Unauthorized" . . . لن نتمكن من إنشاء مستخدمين أو تنفيذ أوامر بصلاحيات مشابهة بدون مصادقة. نُغلِق سطر الأوامر للمتابعة: > exit ثانيًا، نتحقق من أن المستخدم المسؤول الذي أنشأناه يمكنه الوصول. خطوة 4 - التحقق من إمكانية وصول المستخدم المسؤول سَنَتَّصِل باستخدام المستخدم المسؤول الذي أنشأناه باستخدام الخيار ‎-u لتزويد اسم المستخدم والخيار ‎-p ليطلب كلمة مرور. سنحتاج أيضا إلى تزويد اسم قاعدة البيانات التي خُزِّنَت فيها بيانات اعتماد المستخدم باستخدام الخيار ‎--authenticationDatabase. $ mongo -u AdminSammy -p --authenticationDatabase admin سيُطلب منَّا كلمة المرور، لذلك ندخلها. سننتقل إلى سطر الأوامر عند ادخال كلمة المرور الصحيحة حيث يمكننا تنفيذ الأمر show dbs: المخرجات MongoDB shell version v3.4.2 Enter password: connecting to: mongodb://127.0.0.1:27017 MongoDB server version: 3.4.2 > يجب أن نرى قاعدة البيانات بدلًا من رفض الوصول: > show dbs المخرجات: admin 0.000GB local 0.000GB للخروج، أدخل exit أو اضغط CTRL+C. خاتمة ثبَّتنا الآن MongoDB وإجرينا عمليات التهيئة والحماية المطلوبة وتحققنا من عملها. يمكنك الآن بدء استعمالها وأنت مطمئن القلب ومرتاح البال. يمكنك العثور على المزيد من الدروس المتعمقة في كيفية إعداد واستخدام MongoDB في هذه المقالات من أكاديمية حسوب. يُعد التوثيق الرسمي ل MongoDB أيضا مصدر ممتاز للخيارات التي يقدمها MongoDB. كما يمكنك الاطلاع على توثيق MongoDB لتتعلم أكثر عن المصادقة، والتحكم بالوصول وفقا للدور، والمستخدمون والأدوار. ترجمة -وبتصرف- للمقال How to Install MongoDB on Ubuntu 18.04 لصاحبه الكاتب Mateusz Papiernik.
  18. Nginx هو أحد أشهر خوادم الويب عالميا وهو المسؤول عن استضافة بعض أكبر المواقع وأكثرها ازدحاما على الإنترنت. يُعد Nginx غالبا أفضل من Apache من ناحية ملاءمته للموارد، ويمكن استخدامه كخادم ويب أو وكيل عكسي (reverse proxy). في هذا المقال سنشرح كيفية تثبيت Nginx على خادم أوبونتو 18.04. المتطلبات قبل البدء، تحتاج لمستخدم عادي - غير مسؤول - بصلاحيات "sudo" معد على الخادم. يمكنك معرفة كيفية إعداد خادم عادي في مقال الإعداد الأولي لِخادم أوبونتو 18.04. إن كان لديك حساب متاح، سجل الدخول إلى المستخدم غير المسؤول للبدء. خطوة 1 - تثبيت Nginx يمكنك تثبيت Nginx باستخدام مدير الحزم "apt" وذلك لأنه متاح في مخزن أوبونتو الافتراضي. ولأنها أول مرة نَستخدم فيها نظام تحزيم "apt"، سنُحدِث الحزمة المحلية الرئيسية كي نحصل على قائمة بأحدث الحزم. ثم ننفذ أمر تثبيت nginx: $ sudo apt update $ sudo apt install nginx بعد الموافقة على الإجراءات سيُثبِّت apt Nginx وأي متعلقات يتطلبها الخادم. خطوة 2 - إعداد جدار الحماية قبل اختبار Nginx، يجب إعداد جدار الحماية كي يتيح الوصول للخدمة. يُسجل Nginx نفسه كخدمة في ufw أثناء التثبيت مما يجعل السماح بالدخول إلى Nginx أسهل. أدرج إعدادت التطبيق التي تعلم ufw كيف تتعامل معها باستخدام الأمر: $ sudo ufw app list يجب أن تحصل على قائمة بملفات تعريف التطبيق: المخرجات: Available applications: Nginx Full Nginx HTTP Nginx HTTPS OpenSSH يوجد ثلاثة ملفات تعريف ل Nginx متاحة كما ترى: Nginx Full: يفتح هذا الملف كِلا المنافذ 80 (حركة مرور الويب العادية وغير المشفرة)، والمنفذ 443 (حركة المرور المشفرة TLS/SSL) Nginx HTTP: يفتح هذا الملف المنفذ 80 فقط (حركة مرور الويب العادية وغير المشفرة) Nginx HTTPS: يفتح هذا الملف المنفذ 443 فقط (حركة المرور المشفرة TLS/SSL) يستحسن تفعيل الملف الأكثر صرامة والذي لا يزال يسمح بحركة المرور التي تم إعدادها. لأننا لم نقم بإعداد SSL للخادم بعد، نحتاج لتفعيل المنفذ 80 فقط. يمكنك تفعيله باستخدام الأمر: $ sudo ufw allow 'Nginx HTTP' للتحقق من التغييرات: $ sudo ufw status يجب أن ترى أن حركة المرور باستخدام HTTP مفعلة كما في المخرجات: المخرجات: Status: active To Action From -- ------ ---- OpenSSH ALLOW Anywhere Nginx HTTP ALLOW Anywhere OpenSSH (v6) ALLOW Anywhere (v6) Nginx HTTP (v6) ALLOW Anywhere (v6) خطوة 3 - فحص خادم الويب بعد انتهاء عملية التثبيت، سيُشغل أوبونتو Nginx. يجب أن يكون خادم الويب يعمل. يمكننا التحقق من ذلك باستخدام نظام التهيئة systemd عن طريق الأمر: $ systemctl status nginx المخرجات: ● nginx.service - A high performance web server and a reverse proxy server Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled) Active: active (running) since Fri 2018-04-20 16:08:19 UTC; 3 days ago Docs: man:nginx(8) Main PID: 2369 (nginx) Tasks: 2 (limit: 1153) CGroup: /system.slice/nginx.service ├─2369 nginx: master process /usr/sbin/nginx -g daemon on; master_process on; └─2380 nginx: worker process كما ترى في الأعلى، يظهر أن الخدمة بدأت بنجاح. لكن الطريقة المثلى لاختبار ذلك هو جلب صفحة من Nginx فعليا. يمكنك الوصول إلى صفحة Nginx الرئيسية لتتأكد من أن التطبيق يعمل بطريقة صحيحة عن طريق الدخول إلى عنوان بروتوكول الإنترنت (IP) الخاص بالخادم. يمكنك الحصول عليه بعدة طرق. جرب تنفيذ هذا الأمر على شاشة أوامر الخادم: $ ip addr show eth0 | grep inet | awk '{ print $2; }' | sed 's/\/.*$//' ستحصل على بعض الأسطر التي يمكنك تجريبها سطرًا تلو الآخر على متصفح الإنترنت لتتأكد إن كانت تعمل. الطريقة الأخرى هي عبر تنفيذ الأمر التالي، والذي سيعطيك عنوان بروتوكول الإنترنت العام للخادم كما يُرى على المواقع الآخرى على الإنترنت: $ curl -4 icanhazip.com عند حصولك على عنوان بروتوكول الإنترنت الخاص بالخادم، أدخله إلى شريط المتصفح: http://your_server_ip يجب أن ترى الصفحة الرئيسية ل Nginx: تُضمَّن هذه الصفحة مع Nginx لتأكيد أن Nginx يعمل بطريقة صحيحة. خطوة 4 - إدارة عملية Nginx الآن وبما أن خادم الويب أصبح يعمل بطريقة صحيحة، لنراجع بعض أوامر التحكم الأساسية. لإيقاف خادم الويب نفذ الأمر: $ sudo systemctl stop nginx لِتشغيل خادم الويب عندما يكون موقفا، نفذ الأمر: $ sudo systemctl start nginx لِإيقاف وإعادة تشغيل الخدمة استخدم الأمر: $ sudo systemctl restart nginx إن كنت تقوم بتعديل في إعدادت Nginx، فإنه يٌعيد التحميل بدون قطع اتصالاته. للقيام بذلك نفذ الأمر: $ sudo systemctl reload nginx Nginx مٌعد ليبدأ تلقائيا مع بدء تشغيل الخادم. إن كنت لا تريد ذلك؛ يمكنك إلغاء تفعيل هذه الميزة باستخدام الأمر: $ sudo systemctl disable nginx لإعادة تفعيل هذه الميزة: $ sudo systemctl enable nginx خطوة 5 - إعداد أجزاء الخادم (Server Blocks) (إعداد مستحسن) عند استخدام خادم Nginx، أجزاء الخادم (شبيهة بالمضيف الوهمي [virtual host] في Apache) تُستخدم لتغليف تفاصيل الإعدادات وتمكين استضافة أكثر من نطاق من خادم واحد. سنقوم بإعداد نطاق ونُسمية example.com، يمكنك استبدال هذا الاسم باسم النطاق الذي تريده. لدى Nginx جزء خادم مُفعَّل تلقائيا على أوبونتو ومُعَد لتشغيل الملفات في مجلد "‎/var/www/html". رغما من أن هذا يعمل بشكل جيد لموقع واحد؛ إلا أنه ليس عمليا إن كنت تستضيف عدة مواقع. عوضا عن التعديل على ‎/var/www/html؛ لنقم بإنشاء بينة هيكلية في ‎/var/www لموقع example.com ونترك ‎/var/www/html في مكانه كَمجلد رئيسي ليعمل في حال طلب أحد العملاء صفحة لا توافق بقية المواقع. أنشئ مجلدا ل example.com كما يلي مستخدما -p لإنشاء أي مجلدات ضرورية أخرى: $ sudo mkdir -p /var/www/example.com/html ثم قم بمنح ملكية الجلد باستخدام متغير البيئة ‎$USER: $ sudo chown -R $USER:$USER /var/www/example.com/html يجب أن تكون صلاحيات جذور الويب صحيحة إن كنت لم تقم بتعديل قيمة umask يمكنك التأكد من ذلك باستخدام الأمر: $ sudo chmod -R 755 /var/www/example.com قم بإنشاء صفحة كعينة index.html باستخدام الأمر nano على مُحررك المفضل: $ nano /var/www/example.com/html/index.html بداخل هذه الصفحة، ضع عينة HTML التالية: /var/www/example.com/html/index.html <html> <head> <title>Welcome to Example.com!</title> </head> <body> <h1>Success! The example.com server block is working!</h1> </body> </html> بعد الانتهاء احفظ وأغلق الملف. كي يقوم Nginx بمعالجة هذا المحتوى، فمن الضروري إنشاء جزء خادم (server block) بالموجهات الصحيحة. بدلا من القيام بذلك عبر تعديل ملفالإعداد الافتراضي مباشرة، لنُنشئ ملفًا جديدًا في ‎/etc/nginx/sites-available/example.com: $ sudo nano /etc/nginx/sites-available/example.com قم بنسخ جزء الإعداد التالي الشبيه بالافتراضي لكنه محدث للمجلد واسم المجلد الجديد: /etc/nginx/sites-available/example.com server { listen 80; listen [::]:80; root /var/www/example.com/html; index index.html index.htm index.nginx-debian.html; server_name example.com www.example.com; location / { try_files $uri $uri/ =404; } } لاحظ أننا قمنا بتحديث إعداد root للمجلد الجديد و server_name لاسم النطاق الجديد. ثم نقوم بتفعيل الملف من خلال إنشاء رابط منه إلى مجلد sites-enabled والذي يقوم Nginx بالقراءة منه أثناء التشغيل: $ sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/ الآن أصبح لدينا جزئي خادم مفعلين ومُعدين للاستجابة للطلبات وفقا لموجهي listen و server_name (يمكنك قراءة المزيد حول كيفية معالجة Nginx لهذه الموجهات في هذا المقال example.com: سيستجيب للطلب على example.com و www.example.com. default: سيستجيب لأي طلبات على المنفذ 80 والتي لا تطابق جزء آخر. لتجنب مشكلة "hash bucket memory" التي قد تطرأ من إضافة أسماء خوادم إضافية، من الضروري تعديل قيمة واحدة في ملف ‎/etc/nginx/nginx.conf. افتح هذا الملف: $ sudo nano /etc/nginx/nginx.conf ابحث عن الموجّه server_names_hash_bucket_size واحذف رمز # لإلغاء تعليق هذا السطر: /etc/nginx/nginx.conf ... http { ... server_names_hash_bucket_size 64; ... } ... ثم نتحقق من عدم وجود من أي أخطاء في جمل ملفات Nginx: $ sudo nginx -t احفظ وأغلق الملف بعد انتهائك. إن لم يكن هناك أي أخطاء أعد تشغيل Nginx لتفعيل التغييرات: $ sudo systemctl restart nginx يجب أن يقوم Nginx الآن بالاستجابة للطلبات على النطاق الجديد. للتحقق من ذلك، ادخل على http://example.com. هنا يجب أن ترى شيئا كالتالي: خطوة 6 - التعرف على مجلدات وملفات Nginx المهمة الآن، وبعد أن أصبحت تعرف كيف تُدير خدمة Nginx. يجب أن تأخذ لمحة سريعة حول بعض المجلدات والملفات المهمة. المحتوى ‎/var/www/html: محتوى الويب الفعلي ل Nginx والذي يحتوي فقط على صفحة Nginx الافتراضية التي رأيناها سابقا. تُعرض هذه الصفحة من مجلد ‎/var/www/html. يمكن تغيير هذا الإعداد عن طريق ملف إعداد Nginx. إعداد الخادم ‎/etc/nginx: مجلد إعداد Nginx والذي يحوي جميع ملفات التكوين. ‎/etc/nginx/nginx.conf: ملف تكوين Nginx الرئيسي. يمكن تعديل هذا الملف لتغيير الإعدادات العامة ل Nginx. ‎/etc/nginx/sites-available/‎: المجلد الذي يتم تخزين أجزاء الخادم فيه. لن يقوم Nginx باستخدام ملفات التكوين الموجودة في هذا المجلد إلا إذا تم ربطها لمجلد sites-enabled. بشكل عام، يتم تخزين ملفات إعداد أجزاء الخادم في هذا المجلد ثم تُفعّل بربطها بالمجلد الآخر. /etc/nginx/sites-enabled/: المجلد الذي يتم تخزين أجزاء الخادم لكل موقع (per-site server blocks) فيه. تُنشئ هذه المجلدات بربط ملفات التكوين الموجودة في مجلد sites-available. ‎/etc/nginx/snippets: يحتوي هذا المجلد على أجزاء التكوين التي يمكن تضمينها في أي مكان آخر في تكوين Nginx. أجزاء الإعداد المُكررة عن قصد تُعد بدائل جيدة لإعادة بناء أجزاء الأكواد البرمجية. سجلات الخادم ‎/var/log/nginx/access.log: يتم تخزين كل طلب يصل إلى خادم الويب في ملف السجل هذا إلا إن كان Nginx مُعد لعدم القيام بذلك. ‎/var/log/nginx/error.log: يتم تسجيل أي خطأ في Nginx في هذا السجل. الآن وبما أن خادم الويب أصبح مُعدًا لديك، فلديك عدة خيارات للمحتوى الذي يمكن أن يعالجه الخادم والتقنيات التي يمكن أن تستخدمها لتجربة أفضل. الخلاصة إن كنت تريد بناء حزمة تطبيق متكاملة، اطلع على هذا المقال عن كيفية إعداد حزمة LEMP على أوبونتو 18.04. ترجمة -وبتصرف- للمقال How To Install Nginx on Ubuntu 18.04 لصاحبيه Justin Ellingwood و Kathleen Juell.
  19. MySQL هو نظام مفتوح المصدر لإدارة قواعد البيانات، ويُثبت غالبا كجزء من الحزمة الشهيرة LAMP (Linux, Apache, MySQL, PHP/Python/Perl). يستخدم MySQL قواعد بيانات علائقية و SQL (لغة استعلام مهيكلة) لإدارة بياناته. الطريقة المختصرة لتثبيت MySQL سهلة جدا: حدث الحزمة الرئيسية، ثم ثبت حزمة mysql-server ثم شغِّل سكريبت الحماية المضمن. $ sudo apt update $ sudo apt install mysql-server $ sudo mysql_secure_installation متطلبات أولية لإكمال هذا الإعداد، ستحتاج إلى: خادم أوبونتو 18.04 مُعد كما في مقال الإعداد الأولي لِخادم أوبونتو. متضمنا مستخدم غير مسؤول (non-root user) بصلاحيات sudo وجدار حماية. خطوة 1 - تثبيت MySQL تتضمن تلقائيا حزمة مخزن APT في أوبونتو 18.04 أحدث إصدار فقط من MySQL. في الوقت الراهن، أحدث اصدار هو MySQL 5.7. للتثبيت، حدّث الحزمة الرئيسية على الخادم باستخدام الأمر apt: $ sudo apt update ثم ثبت الحزمة الافتراضية: $ sudo apt install mysql-server سيُثبت هذا الأمر MySQL لكنه لن يتيح لك إعداد كلمة مرور أو القيام بأي تعديل على الإعدادات. مما يجعل تثبيت MySQL غير آمن وسيتم شرح ذلك لاحقا. خطوة 2 - إعداد MySQL ستحتاج لِتشغيل سكريبت الحماية المضمن للحصول على تثبيت صحيح. الأمر الذي سيغير بعض الخيارات الأقل أمنا مثل دخول المستخدمين المسؤولين عن بعد أو عينات المستخدمين. كنا نقوم بإعداد هذه البيانات يدويا في إصدارات سابقة، لكن في هذا الإصدار يتم إعدادها تلقائيا. قم بتشغيل السكربت: $ sudo mysql_secure_installation ستنتقل عبر بعض الخيارات حيث يمكنك القيام ببعض التغييرات على إعدادات حماية MySQL المٌعدة أثناء التثبيت. الخيار الأول سيسألك إن كنت تريد إعداد إضافة التحقق بكلمة المرور والمستخدمة لقياس قوة كلمة مرور MySQL. بغض النظر عن خيارك، الخيار التالي سيكون لإعداد كلمة مرور للمستخدم المسؤول ل MySQL. أدخل وأكِد كلمة مرور آمنة. بعد ذلك يمكنك الضغط على Y ثم ENTER لقبول الإعداد الافتراضي لبقية الأسئلة التالية. سيقوم ذلك بحذف بعض المستخدمين المجهولين وقاعدة البيانات التجريبية، وإلغاء تفعيل دخول المستخدمين المسؤولين عن بعد، وسيُحمل الإعدادات الجديدة التي قمت بعملها على MySQL. لإعداد دليل بيانات MySQL يمكنك استخدام mysql_install_db للإصدارات قبل 5.7.6 و mysqld --initialize لتهيئة الإصدار 5.7.6 والإصدارات الأحدث. لكن إن كنت قد ثبتت MySQL من موزع Debian كما في الخطوة 1، فإن دليل البيانات قد تم إعداده تلقائيا ولن تحتاج للقيام بأي شيء. وبحال أو بآخر إن قمت بتنفيذ الأمر، فسترى الخطأ التالي: المخرجات: mysqld: Can't create directory '/var/lib/mysql/' (Errcode: 17 - File exists) . . . 2018-04-23T13:48:00.572066Z 0 [ERROR] Aborting لاحظ أنه حتى إن قمت بإعداد كلمة مرور لمستخدم MySQL المسؤول، فإن هذا المستخدم ليس معدًا للتحقق عبر كلمة المرور عند الاتصال بشاشة اوامر MySQL. إن أردت تغيير هذا الإعداد اتبع الخطوة 3. خطوة 3 - (اختيارية) إعداد مصادقة وصلاحيات المستخدم المستخدم المسؤول في أنظمة أوبونتو التي تستخدم MySQL 5.7 مُعد للمصادقة تلقائيا باستخدام اضافة auth_socket بدلا من كلمة المرور. توفر هذه الإضافة أمانًا أكبر وسهولةً في الاستخدام في حالات متعددة، لكنها تضيف بعض التعقيد عند حاجتك للسماح لبرنامج خارجي بالوصول للمستخدم (مثل phpMyAdmin). حتى تتمكن من الدخول إلى MySQL كمستخدم رئيسي باستخدام كلمة مرور، تحتاج لتبديل طريقة المصادقة من auth_socket إلى mysql_native_password. للقيام بذلك افتح شاشة اوامر MySQL: $ sudo mysql ثم تأكد من طريقة المصادقة لكل حساب مستخدم MySQL باستخدام الأمر التالي: mysql> SELECT user,authentication_string,plugin,host FROM mysql.user; المخرجات: +------------------+-------------------------------------------+-----------------------+-----------+ | user | authentication_string | plugin | host | +------------------+-------------------------------------------+-----------------------+-----------+ | root | | auth_socket | localhost | | mysql.session | *THISISNOTAVALIDPASSWORDTHATCANBEUSEDHERE | mysql_native_password | localhost | | mysql.sys | *THISISNOTAVALIDPASSWORDTHATCANBEUSEDHERE | mysql_native_password | localhost | | debian-sys-maint | *CC744277A401A7D25BE1CA89AFF17BF607F876FF | mysql_native_password | localhost | +------------------+-------------------------------------------+-----------------------+-----------+ 4 rows in set (0.00 sec) يمكنك ملاحظة أن المستخدم المسؤول في هذا المثال يستخدم المصادقة بواسطة إضافة auth_socket. لإعداد حساب المستخدم المسؤول ليستخدم المصادقة بكلمة المرور استخدم أمر ALTER USER التالي. تأكد من تغيير password إلى كلمة مرور قوية من اختيارك وتذكر أن هذا الأمر سيغير كلمة المرور التي وضعتها في خطوة 2: mysql> ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password'; ثم نفذ الأمر FLUSH PRIVILEGES الذي يوجه الخادم بإعادة تحميل جداول الصلاحيات كي تعمل التغييرات التي قمت بها: mysql> FLUSH PRIVILEGES; تحقق من طريقة المصادقة لكل حساب مستخدم مجددا للتأكد من أن المستخدم المسؤول لم يعد يستخدم المصادقة باستخدام إضافة auth_socket: mysql> SELECT user,authentication_string,plugin,host FROM mysql.user; المخرجات: +------------------+-------------------------------------------+-----------------------+-----------+ | user | authentication_string | plugin| host | +------------------+-------------------------------------------+-----------------------+-----------+ | root | *3636DACC8616D997782ADD0839F92C1571D6D78F | mysql_native_password | localhost | | mysql.session| *THISISNOTAVALIDPASSWORDTHATCANBEUSEDHERE | mysql_native_password | localhost | | mysql.sys| *THISISNOTAVALIDPASSWORDTHATCANBEUSEDHERE | mysql_native_password | localhost | | debian-sys-maint | *CC744277A401A7D25BE1CA89AFF17BF607F876FF | mysql_native_password | localhost | +------------------+-------------------------------------------+-----------------------+-----------+ 4 rows in set (0.00 sec) يمكنك رؤية أن المستخدم المسؤول أصبح يستخدم المصادقة بكلمة المرور في هذا المثال. بعد أن تتأكد من ذلك يمكنك الخروج من شاشة اوامر MySQL: mysql> exit بدلا من ذلك؛ قد يفضل البعض استخدام مستخدم خصص للاتصال ب MySQL. لِإنشاء مستخدم مخصص افتح شاشة اوامر MySQL مجددا: $ sudo mysql ملاحظة: إن كنت تستخدم المصادقة بكلمة المرور للمستخدم المسؤول كما تم الشرح سابقا؛ فستحتاج لاستخدام أمر آخر للدخول إلى شاشة اوامر MySQL. سيقوم الأمر التالي بتشغيل عميل MySQL مع صلاحيات مستخدم عادي. وستحصل على صلاحيات المسؤول فقط عن طريق المصادقة: $ mysql -u root -p بعد ذلك قم بإنشاء مستخدم بكلمة مرور قوية: mysql> CREATE USER 'sammy'@'localhost' IDENTIFIED BY 'password'; ثم امنح الصلاحيات المناسبة للمستخدم الجديد. كمثال، يمكنك منح المستخدم صلاحيات لجميع جداول قاعدة البيانات، بالإضافة إلى إمكانية إضافة وتعديل وحذف صلاحيات المستخدمين باستخدام الأمر التالي: mysql> GRANT ALL PRIVILEGES ON *.* TO 'sammy'@'localhost' WITH GRANT OPTION; لاحظ أنك لا تحتاج في هذه النقطة لاستخدام أمر FLUSH PRIVILEGES مرة أخرى. تحتاج لاستخدام هذا الأمر فقط عند تعديل جداول منح الصلاحيات باستخدام أحد الأوامر INSERT، أو UPDATE، أو DELETE. ولأنك قمت بإنشاء مستخدم هنا عوضا عن تعديل مستخدم موجود فإن الأمر FLUSH PRIVILEGES غير مهم هنا. أغلق شاشة اوامر MySQL: mysql> exit وأخيرا نختبر صحة تثبيت MySQL. خطوة 4 - اختبار MySQL يجب أن يبدأ MySQL بالعمل تلقائيا بغض النظر عن الطريقة التي قمت بتثبيته بها. للتحقق من ذلك، نقوم بفحص حالته. $ systemctl status mysql.service سترى مخرجات شبيهة بالمخرجات التالية: ● mysql.service - MySQL Community Server Loaded: loaded (/lib/systemd/system/mysql.service; enabled; vendor preset: en Active: active (running) since Wed 2018-04-23 21:21:25 UTC; 30min ago Main PID: 3754 (mysqld) Tasks: 28 Memory: 142.3M CPU: 1.994s CGroup: /system.slice/mysql.service └─3754 /usr/sbin/mysqld إن كان MySQL لا يعمل فيمكنك تشغيله باستخدام الأمر sudo systemctl start mysql. للمزيد من الفحص يمكنك محاولة الاتصال بقاعدة البيانات باستخدام أداة mysqladmin والتي تعتبر أداة عميل تتيح لك تنفيذ أوامر ذات صلاحيات مسؤول. كمثال، الأمر التالي يتصل ب MySQL كمستخدم مسؤول ("‎-u root") ويطلب كلمة مرور ("‎-p") ويعرض الإصدار. $ sudo mysqladmin -p -u root version يجب أن ترى مخرجات كالتالي: mysqladmin Ver 8.42 Distrib 5.7.21, for Linux on x86_64 Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Server version 5.7.21-1ubuntu1 Protocol version 10 Connection Localhost via UNIX socket UNIX socket /var/run/mysqld/mysqld.sock Uptime: 30 min 54 sec Threads: 1 Questions: 12 Slow queries: 0 Opens: 115 Flush tables: 1 Open tables: 34 Queries per second avg: 0.006 هذا يعني أن MySQL يعمل. الخلاصة الآن أصبح لديك الإعداد الأولي ل MySQL على الخادم الخاص بك. هنا بعض الأمثلة على بعض الخطوات اللاحقة: إعداد بعض الميزات الأمنية الاضافية نقل مجلد البيانات ترجمة -وبتصرف- للمقال How To Install MySQL on Ubuntu 18.04 لصاحبه الكاتب Mark Drake.
  20. Redis عبارة عن مخزن يعتمد على مفتاح وقيمته ويُعرف بمرونته، وأدائه، ودعمه العديد من اللغات. يوضح هذا المقال كيفية تثبيت وإعداد وتأمين Redis على خادم أوبونتو 18.04. المتطلبات ستحتاج إلى الدخول إلى خادم أوبونتو 18.04 والذي لا يملك صلاحيات مستخدم مسؤول ويملك صلاحيات sudo. بالإضافة إلى جدار حماية معد. يمكنك إعداد ذلك من خلال مقال التهيئة الأولية لِخادم أوبونتو. للبدء سجل دخولك إلى خادم أوبونتو 18.04 باستخدام مستخدم sudo وليس مستخدم مسؤول. خطوة 1 - تثبيت وإعداد Redis للحصول على أحدث إصدار من Redis، سنستخدم الأمر apt لتثبيته من متجر أوبونتو الرسمي. حدِّث ذاكرة التخزين المؤقتة لحزمة apt وثبت Redis باستخدام الأمرين التاليين: $ sudo apt update $ sudo apt install redis-server تنفيذ هذان الأمران سيؤدي إلى تنزيلRedis وتثبيته بالإضافة إلى تثبيت كل الأشياء المتعلقة به. بعد ذلك، يوجد إعداد مهم يجب تغييره في ملف إعداد Redis والذي وُلد تلقائيا أثناء التثبيت. افتح هذا الملف بأي محرر تفضله: $ sudo nano /etc/redis/redis.conf ابحث عن supervised في الملف. تتيح لك هذه التعليمة تعريف init في النظام لإضافة Redis كخدمة، مما يتيح لك تحكمًا أفضل بعملياته. تعليمة supervised تكون معطلة no بشكل افتراضي. غيِّر قيمة no إلى systemd؛ ذلك لأنك تستخدم أوبونتو المعتمد على نظام "systemd init". الملف ‎/etc/redis/redis.conf: . . . # If you run Redis from upstart or systemd, Redis can interact with your # supervision tree. Options: # supervised no - no supervision interaction # supervised upstart - signal upstart by putting Redis into SIGSTOP mode # supervised systemd - signal systemd by writing READY=1 to $NOTIFY_SOCKET # supervised auto - detect upstart or systemd method based on # UPSTART_JOB or NOTIFY_SOCKET environment variables # Note: these supervision methods only signal "process is ready." # They do not enable continuous liveness pings back to your supervisor. supervised systemd . . . حتى الآن هذا هو التعديل الوحيد الذي نحتاج القيام به في ملف إعداد Redis. احفظ الملف واغلقه بعد تعديله. ثم أعد تشغيل خدمة Redis لتصبح التغييرات التي أجريتها على الملف سارية المفعول. $ sudo systemctl restart redis.service وبهذا تكون قد أعددت Redis، لكن قبل استخدامة يجب التأكد من أنه يعمل بالطريقة الصحيحة. خطوة 2 - اختبار Redis يفضل التأكد من صحة عمل أي برنامج جديد قبل تغيير أي من إعداداته. في هذه الخطوة سنتعرف على بعض الطرق للتأكد من ذلك. نبدأ بالتأكد من أن خدمة Redis تعمل: $ sudo systemctl status redis ستظهر مخرجات مشابهة لما يلي في حال كان Redis يعمل بدون أي أخطاء: المخرجات: ● redis-server.service - Advanced key-value store Loaded: loaded (/lib/systemd/system/redis-server.service; enabled; vendor preset: enabled) Active: active (running) since Wed 2018-06-27 18:48:52 UTC; 12s ago Docs: http://redis.io/documentation, man:redis-server(1) Process: 2421 ExecStop=/bin/kill -s TERM $MAINPID (code=exited, status=0/SUCCESS) Process: 2424 ExecStart=/usr/bin/redis-server /etc/redis/redis.conf (code=exited, status=0/SUCCESS) Main PID: 2445 (redis-server) Tasks: 4 (limit: 4704) CGroup: /system.slice/redis-server.service └─2445 /usr/bin/redis-server 127.0.0.1:6379 . . . يمكنك رؤية أن Redis مُفعَّل وقيد التشغيل، ما يعني أنه معد كي يبدأ التشغيل مع بدء تشغيل الخادم. ملاحظة: هذا الإعداد مستحسن في أغلب حالات استخدام Redis. إن كنت تفضل تشغيل Redis يدويا في كل مرة تحتاج إليه. يمكنك القيام بذلك باستخدام الأمر: $ sudo systemctl disable redis لاختبار صحة أداء Redis، اتصل بالخادم باستخدام سطر أوامر العميل: $ redis-cli في الشاشة التالية، اختبر الاتصال باستخدام أمر ping: 127.0.0.1:6379> ping المخرجات: PONG تأكد هذه المخرجات أن اتصال الخادم مفعل. بعد ذلك، تأكد من إمكانية إعداد المفاتيح باستخدام الأمر: 127.0.0.1:6379> set test "It's working!" المخرجات: OK استرجِع القيمة باستخدام الأمر: 127.0.0.1:6379> get test ستتمكن من استرجاع القيمة التي أدخلتها في حال كان كل شيء يعمل بشكل صحيح: المخرجات: "It's working!" بعد تأكدك من إمكانية استرجاع القيمة، أغلق شاشة تنفيذ أوامر Redis: 127.0.0.1:6379> exit كاختبار أخير لفحص ما إن كان Redis قادرا على الاستمرار حتى بعد إبقافة أو إعادة تشغيله. لتطبيق هذا الاختبار نبدأ بإعادة تشغيل Redis: $ sudo systemctl restart redis اتصل بشاشة أوامر عميل Redis مرة أخرى وتأكد من بقاء القيمة التي خزنتها: $ redis-cli 127.0.0.1:6379> get test يجب أن تظل القيمة المخزنة متاحة: المخرجات: "It's working!" أخرج مجددَا بعد انتهائك: 127.0.0.1:6379> exit وبذلك، يكون Redis مثبتَا ويعمل بشكل صحيح وجاهز للاستخدام. بالرغم من ذلك، ما تزال بعض إعداداته الافتراضية غير آمنة وتوفر بعض الفرص للأشخاص المخترقون للهجوم على الخادم وبياناته. تشرح باقي الخطوات في هذا المقال بعض الطرق لتقليل هذه الثغرات الأمنية كما ذكر في موقع Redis الرسمي. هذه الخطوات اختيارية وسيستمر Redis بالعمل حتى لو لم تنفذها، لكن يفضل أن تكمل هذه الخطوات لتقوية أمن نظامك. خطوة 3 - الربط مع المضيف المحلي (localhost) Redis متاح تلقائيا عبر المضيف المحلي، لكن إن كنت قد ثبتت Redis بطريقة أخرى فربما تكون قد حدَّثت ملف الإعداد الخاص ب Redis الأمر الذي يتيح اتصالات Redis من أي مكان. هذا الأمر غير آمن كالربط بالمضيف المحلي. لتصحيح الأمر، افتح ملف تكوين Redis لتعديله: sudo nano /etc/redis/redis.conf إذهب إلى السطر التالي وتأكد من إلغاء تعليقه (احذِف # إن وجدت): الملف /etc/redis/redis.conf: bind 127.0.0.1 ::1 احفظ الملف واغلقه عند الانتهاء (اضغط على CTRL + X, Y, ثم ENTER). أعد تشغيل الخدمة للتأكد من قراءة النظام للتغييرات: $ sudo systemctl restart redis للتأكد من تأثير هذه التغييرات، نفِّذ أمر netstat التالي: $ sudo netstat -lnp | grep redis المخرجات: tcp 0 0 127.0.0.1:6379 0.0.0.0:* LISTEN 14222/redis-server tcp 0 0 ::1:6379 :::* LISTEN 14222/redis-server توضح هذه المخرجات أن برنامج خادم Redis مرتبط بالمضيف المحلي (127.0.0.1). مما يدل على انعكاس التغيير الذي أجريته على ملف التكوين. إن رأيت عنوان بروتوكول إنترنت آخر (مثل 0.0.0.0) فتأكد من إلغاء تعليق السطر المطلوب وأعد تشغيل خدمة Redis مجددَا. الآن وبعد أن أصبح Redis مربوطا بالمضيف المحلي فقط سيصبح من الصعب على جهات الاختراق تقديم الطلبات أو الوصول إلى الخادم. مع ذلك لا يزال Redis غير معد لِطلب المصادقة من المستخدمين قبل القيام بأي تغيير على إعداداته أو البيانات التي يحتفظ بها. لمعالجة هذا، يتيح لك Redis طلب تأكيد هوية من المستخدمين باستخدام كلمة مرور قبل إحداث ي تغيير وذلك باستخدام الأمر (redis-cli). خطوة 4 - إعداد كلمة مرور Redis يتيح لك إعداد كلمة مرور Redis تفعيل إحدى ميزات الحماية المدمجة فيه — الأمر auth يطلب من المستخدمين إثبات الهوية للوصول إلى قاعدة البيانات. يتم تكوين كلمة المرور مباشرة في ملف تكوين Redis "/etc/redis/redis.conf", لذلك؛ افتح الملف مجددا باستخدام محرِّرك المفضل: $ sudo nano /etc/redis/redis.conf انتقل إلى جزء "SECURITY" وابحث عن السطر التالي: الملف ‎/etc/redis/redis.conf: # requirepass foobared ألغِ تعليق هذا السطر بِإزالة "#" وغير "foobared" إلى كلمة مرور أكثر أمانًا. ملاحظة: يوجد تحذير معلق فوق الموجه "requirepass" في ملف "redis.conf": # Warning: since Redis is pretty fast an outside user can try up to # 150k passwords per second against a good box. This means that you should # use a very strong password otherwise it will be very easy to break. # لذلك، من المهم تحديد كلمة مرور طويلة. يمكنك استخدام الأمر "openssl" ِلتوليد كلمة مرور تلقائيا بدلا من القيام بذلك يدويا كما في المثال التالي. وذلك بتمرير مخرجات الأمر الأول إلى أمر "openssl" الثاني. سيزيل الأمر أي أسطر فارغة تم توليدها من الأمر الأول: $ openssl rand 60 | openssl base64 -A ستكون المخرجات مشابهة لما يلي: RBOJ9cCNoGCKhlEBwQLHri1g+atWgn4Xn4HwNUbtzoVxAYxkiYBi7aufl4MILv1nxBqR4L6NNzI0X6cE بعد نسخ ولصق المخرجات لتكون هي القيمة الجديدة ل "requirepass": /etc/redis/redis.conf requirepass RBOJ9cCNoGCKhlEBwQLHri1g+atWgn4Xn4HwNUbtzoVxAYxkiYBi7aufl4MILv1nxBqR4L6NNzI0X6cE بعد إضافة كلمة المرور، احفظ وأغلق الملف ثم أعد تشغيل Redis: $ sudo systemctl restart redis.service لِاختبار من عمل كلمة المرور، أدخل إلى سطر أوامر Redis: $ redis-cli الأوامر التالية تستخدم لاختبار ما إن كانت كلمة المرور تعمل أم لا. يحاول الأمر الأول إضافة مفتاح إلى قيمة قبل المصادقة: 127.0.0.1:6379> set key1 10 لن يتنفذ الأمر لعدم المصادقة وسيرجع Redis خطأ: المخرجات: (error) NOAUTH Authentication required. يقوم الأمر التالي بالمصادقة باستخدام كلمة المرور المحددة في ملف التكوين: 127.0.0.1:6379> auth your_redis_password يُأكد Redis المصادقة: المخرجات: ok بعد ذلك، سينجح الأمر السابق: 127.0.0.1:6379> set key1 10 المخرجات: ok الأمر "get key1" يطلب من Redis قيمة المفتاح الجديدة. 127.0.0.1:6379> get key1 المخرجات: ok يمكنك إغلاق "redis-cli" بعد تأكدك من أنه يمكنك تنفيذ أوامر عميل Redis: 127.0.0.1:6379> quit لاحقا، ستتعلم كيفية إعادة تسمية أوامر Redis والتي إن تم إدخالها بالخطأ أو من خلال جهة غير مُخَوًّلًة سيسبب الكثير من الأضرار لجهازك. خطوة 5 - إعادة تسمية الأوامر المهمة تتضمن ميزة الحماية الأخرى المدمجة في Redis إعادة تسمية بعض الأوامر التي تعتبر خطرة أو إلغاء تفعيلها. يمكن أن تستخدم بعض هذه الأوامر بواسطة مستخدمون غير مخولين بالدخول لإعادة إعداد، أو تدمير أو حذف بياناتك. سيتم إعادة تسمية الأوامر بنفس الجزء الذي تم تعديل كلمة المرور منه "SECURITY" من ملف "/etc/redis/redis.conf" بعض الأوامر المهمة والتي قد تكون ذات خطورة هي: FLUSHDB FLUSHALL KEYS PEXPIRE DEL CONFIG SHUTDOWN BGREWRITEAOF BGSAVE SAVE SPOP SREM RENAME DEBUG هذه القائمة ليست شاملة، لكن إعادة تسمية أو تعطيل هذه القائمة يعتبر بداية جيدة لتعزيز أمان خادم Redis. يعتمد إعادة تسمية أو تعطيل الأوامر على احتياجك لها لموقعك. إن كنت تعلم أنك لن تستخدم أمرًا ما؛ فيمكنك تعطيله. أو إعادة تسميته إن ان ذات أهمية لك. افتح ملف تكوين Redis لتفعيل أو تعطيل أي أمر: $ sudo nano /etc/redis/redis.conf تحذير: توضح الخطوات التالية كيفية تعطيل أو إعادة تسمية كأمثلة. يجب أن تختار تعطيل أو إعادة تسمية الأوامر التي تريدها فقط. يمكنك مراجعة قائمة الأوامر كاملة ومعرفة كيف يمكن أن تستخدم بطريقة ضارة على redis.io/commands. لِتعطيل أمر ما، أعد تسميته إلى نص فارغ (يشار إلى النص الفارغ بِعلامتي تنصيص "") كما في الأسفل: الملف ‎/etc/redis/redis.conf: . . . # It is also possible to completely kill a command by renaming it into # an empty string: # rename-command FLUSHDB "" rename-command FLUSHALL "" rename-command DEBUG "" . . . لإعادة تسمية أمر ما، اعطه اسما آخر كما في المثال بالأسفل. يجب أن تكون الأسماء صعبة التخمين للآخرين ولكن سهلة بالنسبة لك كي تتذكرها لاحقًا: الملف ‎/etc/redis/redis.conf: . . . # rename-command CONFIG "" rename-command SHUTDOWN SHUTDOWN_MENOT rename-command CONFIG ASC12_CONFIG . . . احفظ التغييرات وأغلق الملف. أعد تشغيل Redis بعد إعادة تسمية الأمر لتطبيق التغييرات: $ sudo systemctl restart redis.service أدخل إلى سطر أوامر Redis لاختبار الأمر الجديد: $ redis-cli قم بالمصادقة: 127.0.0.1:6379> auth your_redis_password المخرجات: OK لنفترض أنك أعدت تسمية الأمر "CONFIG" إلى "ASC12_CONFIG" كما في المثال السابق. أولا حاول استخدام الأمر الأصلي "CONFIG". يجب أن يفشل تنفيذ الأمر لأنك أعدت تسميته: 127.0.0.1:6379> config get requirepass المخرجات: (error) ERR unknown command 'config' سينجح الأمر عند استخدام الاسم الجديد. لا يهم حالة الأحرف. 127.0.0.1:6379> asc12_config get requirepass المخرجات: 1) "requirepass" 2) "your_redis_password" وأخيرا يمكنك الخروج من "redis-cli". 127.0.0.1:6379> exit ملاحظة: عندما تُعيد تشغيل سطر أوامر Redis، سيطلب منك إعادة المصادقة أو سيظهر لك الخطأ التالي عند محاولتك لتنفيذ أمر ما: المخرجات: NOAUTH Authentication required. ملاحظة: بالنسبة لإعادة تسمية الأوامر؛ يوجد جملة تحذيرية في نهاية جزء SECURITY في الملف "‎/etc/redis/redis.conf" والتي تنص على: Please note that changing the name of commands that are logged into the AOF file or transmitted to slaves may cause problems. يعني هذا التحذير أن إعادة تسمية الأوامر المحفوظة في ملف AOF أو المنقولة في ملف فرعي قد يتسبب ببعض المشاكل. ملاحظة: يستخدم Redis المصطلحات “master” و “slave” بينما يفضل DigitalOcean استخدام البدائل “primary” و “secondary”. ولتجنب الخلط بين المصطلحات فإننا نستخدم المصطلحات التي يستخدمها Redis في ملفات التوثيق الخاصة به. يعني ذلك أنه إن كانت الأوامر التي تمت إعادة تسميتها ليست في ملف AOF أو إن كانت فيه لكن لم يتم نقلها إلى مجلد فرعي فلن يكون هناك أية مشاكل. لذلك يجب أن تتذكر هذا عند قيامك بإعادة تسمية أمر ما. أفضل وقت لإعادة تسمية الأوامر هو عند عدم استخدامك ل AOF أو بعد التثبيت مباشرة قبل استخدام Redis. عند استخدامك ل AOF وتعاملك مع التثبيت بنوع master-slave، خذ بعين الاعتبار هذه الإجابة من صفحة المشروع على GitHub: الإجابة: تُسجَّل الأوامر في AOF ثم تُكرر إلى المجلدات الفرعية بنفس الطريقة التي تم إرسالها، لذلك إن شغَّلت AOF على نسخة لا تحتوي الأوامر التي تمت إعادة اسميتها ستواجه بعض التعارضات ولن تنفذ بعض الأوامر. لذلك فإن أفضل طريقة لإعادة تسمية لأوامر في مثل هذه الحالة هي عبر التأكد من أن الأوامر التي تمت إعادة تسميتها مطبقة على جميع النسخ في طريقة التثبيت باستخدام master-slave. الخلاصة ختاما، في هذا المقال، لقد ثبتته وإعداد Redis، وتأكدت من صحة عمل Redis، واستخدمت ميزات الحماية المدمجة فيه والتي تقلل عدد الثغرات الممكنة للهجوم من قبل المخترقين. تذكر أنه بمجرد تسجيل أحدهم الدخول إلى الخادم، فمن السهل عليه التحايل على إعدادت حماية Redis التي قمنا بها. لذلك فإن أهم ميزة أمان على الخادم هي جدار الحماية (الذي أعددته إن كنت تابعت المقال السابق حول التهيئة الأولية لخادم أوبونتو 18.04) الذي يجعل الاختراق صعبًا جدًا. ترجمة -وبتصرف- للمقال How To Install and Secure Redis on Ubuntu 18.04 لصاحبه الكاتب Mark Drake.
  21. عند إنشاءك خادم أوبونتو 18.04 للمرة الأولى، يجب عليك اتخاذ بعض الخطوات كجزء من الإعدادات الأولية. سيزيد ذلك من أمان وسهولة استخدام الخادم، ويمنحك أساسًا قويًا لإجراءات لاحقة. ملاحظة: يشرح المقال أدناه الخطوات التي نوصي بها لإكمال تهيئة خادم أوبنتو 18.04 يدويا. اتباع هذه الاعدادات اليدوية سيساعدك في تعلم بعض المهارات الأساسية لإدارة الخادم وكتمرين لفهم الإجراءات التي تحدث في الخادم بصورة عامة. إن كنت تريد تهيئة الخادم وبدء استخدامة بسرعة، يمكنك استخدام نصنا البرمجي لتهيئة الخادم الأولي والذي ينفذ خطوات التهيئة تلقائيا. خطوة 1 - تسجيل الدخول كمستخدم مسؤول (root) تحتاج لمعرفة عنوان بروتوكول الإنترنت العام (public IP) للخادم الخاص بك لتسجيل الدخول. تحتاج أيضا إلى كلمة المرور، أو المفتاح السري لمستخدم مسؤول (ما يسمى ب root users) إن كنت قد ثبتت مفتاح SSH للمصادقة. إن لم تكن قد سجلت الدخول إلى الخادم الخاص بك، فيمكنك اتباع المقال حول كيفية الاتصال ب Droplet باستخدام SSH. قم بتسجيل الدخول إلى الخادم كمستخدم مسؤول root إن كنت غير متصلا حتى الآن باستخدام الأمر التالي. استبدل النص المظلل بعنوان بروتوكول الإنترنت العام للخادم الخاص بك. $ ssh root@your_server_ip في حال ظهور تحذير عن موثوقية المضيف قم بقبوله. إن كنت تقوم بالدخول باستخدام إثبات الهوية؛ قم بتقديم كلمة المرور. أما إن كنت تستخدم مفتاح SSH المحمي بعبارة مرور (passphrase)، سيُطلب منك إدخال عبارة المرور في بداية كل جلسة. قد يُطلب منك أيضا تغيير كلمة المرور للمستخدم المسؤول root في أول مرة تقوم بالتسجيل بها. عن المستخدم root المستخدم المسؤول root في بيئة لينكس هو المتحكم والذي يمتلك صلاحيات عديدة. ونظرا لهذه الصلاحيات فلا ينصح باستخدامه دائمًا وذلك لتجنب القيام بتغييرات عن طريق الخطأ. الخطوة التالية هي إعداد مستخدم بديل بصلاحيات أقل للعمل اليومي. كما سنشرح كيفية إضافة صلاحيات في أي وقت تحتاج إليها. خطوة 2 - إنشاء مستخدم جديد بعد تسجيل دخولك كمستخدم مسؤول root، فأنت جاهز لإضافة حساب مستخدم جديد لاستخدامه من الآن فصاعدا. الأمر التالي يقوم بإنشاء مستخدم بالاسم sammy، يمكنك تسمية المستخدم بأي اسم تريده. # adduser sammy ستُعرض عليك بعض الاسئلة بدأَ بكلمة مرور للحساب الجديد. يجب أن تقوم بإدخال كلمة مرور قوية. ومن ثم تعبئة باقي المعلومات والتي تعتبر اختيارية ويمكنك تجاوزها بالضغط على زر ENTER. خطوة 3 - منح صلاحيات إدارية يمتلك الحساب الذي قمنا بإنشائه صلاحيات اعتيادية. لكن قد نحتاج في بعض الأحيان إلى صلاحيات إدارية. لتجنب الخروج من هذا المستخدم والدخول إلى المستخدم المسؤول root بكثرة؛ يمكننا إعداد ما يسمى بصلاحيات المستخدم الأعلى أو صلاحيات مسؤول root لحساب المستخدم العادي، مما يتيح للمستخدم العادي تنفيذ الأوامر التي تحتاج إلى صلاحيات إدارية عن طريق استخدام الكلمة "sudo" قبل كل أمر. لإضافة هذه الصلاحيات للمستخدم الجديد، نحتاج لإضافة هذا المستخدم إلى مجموعة sudo. يمكن للمستخدمين ضمن مجموعة sudo في اوبنتو 18.14 استخدام الأمر sudo. نفِّذ الأمر التالي لإضافة المستخدم الجديد إلى مجموعة sudo، يجب أن تكون متصلا بالخادم باستخدام حساب مسؤول root (استخدم اسم المستخدم الذي أنشأته مسبقا). # usermod -aG sudo sammy الآن يمكنك الدخول باستخدام الحساب الجديد وتنفيذ الأوامر ذات الصلاحيات العليا عن طريق كتابة "sudo" قبل هذه الأوامر. خطوة 4 - إعداد جدار حماية رئيسي يمكن لخادم أوبونتو 18.04 استخدام جدار الحماية Uncomplicated Firewall) UFW) -جدار الحماية في نظام لينكس- للتأكد من عدم الاتصال سوى بخدمات معينة. يمكن إعداد جدار حماية رئيسي بسهولة باستخدام تطبيق UFW. ملاحظة: إن كان الخادم الخاص بك يعمل على DigitalOcean؛ فيمكنك استخدام DigitalOcean Cloud Firewalls بدلا من تطبيق UFW وذلك يعد اختياريا. يستحسن استخدام جدار حماية واحد فقط لتجنب أي تعارضات والتي قد تكون صعبة التصحيح. يمكن لمختلف التطبيقات تسجيل ملفات التعريف الخاصة بها مع UFW أثناء تثبيتها. تسمح هذه الملفات لتطبيق UFW بإدارة هذه التطبيقات وفقا لاسم التطبيق. لدى خدمة OpenSSH (Open Secure Shell) -أداة الاتصال الأولى لتسجيل الدخول عن بُعد باستخدام بروتوكول SSH- التي تتيح لنا الاتصال بالخادم ملف تعريف مسجل لدى UFW. لرؤية ذلك نستخدم الأمر: # ufw app list المخرجات: Available applications: OpenSSH يجب التأكد من أن جدار الحماية يسمح لاتصالات SSH حتى نتمكن من تسجيل الدخول مرة أخرى. يمكن القيام بذلك باستخدام الأمر: # ufw allow OpenSSH ثم يمكننا تفعيل جدار الحماية: # ufw enable قم بإدخال الحرف "y" وقم بالضغط على "ENTER" للمتابعة، للتأكد من أن اتصالات SSH ما زالت تعمل نفذ الأمر: # ufw status المخرجات Status: active To Action From -- ------ ---- OpenSSH ALLOW Anywhere OpenSSH (v6) ALLOW Anywhere (v6) في حال قمت بإعداد خدمات إضافية، قم بتعديل إعدادات جدار الحماية للسماح باتصالات هذه الخدمات؛ كون جدار الحماية يقوم بمنع الاتصالات عدا اتصالات SSH. يمكنك اتباع هذا المقال لتعلم بعض عمليات UFW الشهيرة. خطوة 5 - السماح بالوصول الخارجي للمستخدم الجديد بما أنه أصبح لدينا مستخدم جديد للاستخدام اليومي، سنحتاج للتأكد من أنه يمكننا الدخول إلى هذا المستخدم عبر SSH. ملاحظة: للتأكد من ذلك، يمكنك الدخول إلى المستخدم الجديد باستخدام كلمة المرور واستخدام sudo. يستحسن البقاء متصلا باسم المستخدم المسؤول root. بهذه الطريقة يمكنك تصحيح أي مشاكل وإضافة أي تعديلات تحتاجها. إن تستخدم واجهتك أي مشكلة في الاتصال بالمستخدم المسؤول root باستخدام SSH وكنت تستخدم DigitalOcean Droplet فيمكنك الدخول إلى Droplet باستخدام شاشة أوامر DigitalOcean. عملية إعداد وصول SSH لمستخدم الجديد تعتمد على ما إن كان حساب المستخدم المسؤول root يستخدم كلمة مرور أو مفتاح SSH للمصادقة. في حال كان المستخدم root يستخدم المصادقة بكلمة المرور إن كنت مسجلا دخولك إلى حساب المستخدم المسؤول root باستخدام كلمة مرور، فإن عملية المصادقة باستخدام كلمة المرور مفعلة ل SSH. مما يمكنك من إضافة SSH لحساب المستخدم الجديد عن طريق تنفيذ الأمر التالي مستخدما اسم المستخدم الذي انشأته سابقا: $ ssh sammy@your_server_ip بعد إدخال كلمة المرور الخاصة بالمستخدم الجديد؛ ستكون مسجلًا إلى النظام بالحساب الجديد ويمكنك تنفيذ الأوامر الإدارية من خلاله عن طريق إضافةsudoقبل الأمر: $ sudo command_to_run سيطلب منك النظام إدخال كلمة المرور الخاصة بالمستخدم الحالي لأول مرة من كل جلسة ومن ثم في بعض الأوقات فقط. لتعزيز أمان الخادم الخاص بك، يستحسن أن تقوم بإعداد مفاتيح SSH بدلا من استخدام كلمة المرور للمصادقة. يمكنك اتباع هذا المقال عن كيفية إعداد مفاتيح SSH على أوبنتو 18.04 لتتعرف على طريقة إعداد المصادقة المعتمده على المفاتيح السرية. في حال كان المستخدم المسؤول root يستخدم المصادقة بمفتاح SSH في حال كنت مسجلا الى حساب المسؤول root باستخدام مفاتيح SSH، فإن المصادقة باستخدام كلمة المرور ل SSH معطلة. للدخول إلى حساب المستخدم الجديد بنجاح، تحتاج لإضافة نسخة من المفتاح العام المحلي إلى الملف "‎~/.ssh/authorized_keys". يمكنك أيضا نسخ الملف "‎~/.ssh/authorized_keys" - والذي يحتوي على المفتاح العام الخاص بك- من حساب المسؤول root إلى الحساب الجديد باستخدام الجلسة الحالية. لنسخ الملف بطريقة سهلة وبالملكية والصلاحيات الصحيحة؛ نستخدم الأمر "rsync". سيقوم هذا الأمر بنسخ مجلد "‎.ssh" للمستخدم المسؤول root ،يحافظ على الصلاحيات ويقوم بتعديل ملكية الملفات في مرة واحدة. تأكد من وضع اسم المستخدم الجديد الذي أنشأته مسبقا. ملاحظة يتعامل الأمر "rsync" مع المجلدات التي تنتهي بشرطة مائلة "/" بطريقة مختلفة عن المجلدات التي لا تنتهي بشرطة مائلة. تأكد من أن أسماء المجلدات (‎~/.ssh) لا تنتهي بشرطة مائلة عند استخدام هذا الأمر"‎~/.ssh/‎". في حال أضفت شرطة مائلة في نهاية اسم المجلد عن طريق الخطأ في هذا الأمر، فسيقوم بنسخ محتويات المجلد من حساب المستخدم المسؤول root إلى المجلد الرئيسي لمستخدم "sudo" بدلا من نسخ الهيكل كاملا للمجلد "‎~/.ssh". وهكذا تكون الملفات في المكان الخاطئ ولن يتمكن SSH من العثور عليها واستخدامها. # rsync --archive --chown=sammy:sammy ~/.ssh /home/sammy الآن يمكنك استخدام SSH مع المستخدم الجديد عن طريق الأمر التالي في شاشة الأوامر: $ ssh sammy@your_server_ip يجب أن تقوم بالدخول إلى المستخدم الجديد بدون استخدام كلمة مرور. تذكر، لتنفيذ أمر باستخدام صلاحيات إدارية استخدم "sudo" قبل الأمر: $ sudo command_to_run ماذا بعد؟ الآن أصبح لديك إعدادات قوية للخادم الخاص بك. يمكنك تثبيت اي برنامج تحتاجه. اطلع على قسم لينكس في الأكاديمية لقراءة دليل آخر أو تعلم تثبيت برامج أخرى. ترجمة -وبتصرف- للمقال Initial Server Setup with Ubuntu 18.04 لصاحبه الكاتب Justin Ellingwood.