Emq Mohammed

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

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

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

السُّمعة بالموقع

3 Neutral

1 متابع

  1. تُخَزَّن النصوص في 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) المقال السابق: الأعداد
  2. يوجد نوعان من الأعداد في 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) المقال السابق: توابع الأنواع الأولية
  3. تتيح لنا 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"
  4. نُنشِئ الكائنات باستخدام الصيغة الاعتيادية المختصرة {...}. لكننا نحتاج لإنشاء العديد من الكائنات المتشابهة غالبًا، مثل العديد من المستخدمين، أو عناصر لقائمة وهكذا. يمكن القيام بذلك باستخدام الدوال البانية (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; } اقرأ أيضًا المقال التالي: توابع الأنواع الأولية المقال السابق: التحويل من نوع كائن إلى نوع أولي
  5. ماذا يحدث عند جمع كائنين بالشكل 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
  6. تُنشّأ الكائنات عادة لتُمَثِّل أشياء من العالم الحقيقي مثل المستخدمين، والطلبات، وغيرها: 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)
  7. وفقًا للمواصفات، قد تكون مفاتيح خصائص الكائنات من نوع نصي (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 المقال السابق: كنس البيانات المهملة
  8. تُدَار الذاكرة في 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) المقال السابق: الكائنات
  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. تتطور لغة جافاسكربت باستمرار نتيجةً لوجود العديد من المقترحات المبنية على الحاجة إلى تطويرها دوريًا. تُحلَّل هذه المقترحات وتُضاف إن كانت جيدة إلى القائمة 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
  11. يُستخدَم الاختبار الآلي في الكثير من المهام، كما يستخدم بكثرة في المشاريع الحقيقية. لم نحتاج الاختبارات؟ عند كتابة دالة، يمكننا تخيل ما يجب أن تقوم به: ما هي المعاملات التي تعطي نتائج معينة. يمكننا فحص الدالة أثناء التطوير من خلال تشغيلها وموازنة مخرجاتها مع ما هو متوقع. مثلا يمكننا القيام بذلك في الطرفية. إن كان هناك خطأ، فإننا نُصلِح الشيفرة البرمجية، ونُعيد تشغيلها، ونفحص النتائج. وهكذا حتى تصبح صحيحة. لكن هذه الطريقة «إعادة التشغيل» غير مثالية. عند اختبار شيفرة برمجية عن طريق إعادة التشغيل اليدوية، فمن السهل نسيان شيئٍ ما. على سبيل المثال، عند إنشاء الدالة 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; } اقرأ أيضًا المقال التالي: تعويض نقص دعم المتصفحات لجافاسكربت المقال السابق: شيفرة النينجا البرمجية
  12. استخدم مبرمجو النينجا القدماء هذه الحيل لشحذ عقول مراجعو الشيفرات البرمجية. يبحث معلمو مراجعة الشيفرات البرمجية عن هذه الحيل في مهام الاختبار. أحيانًا، يستخدم المبرمجون المبتدئون هذه الحيل بصورة أفضل من مبرمجي النينجا. اقرأها جيدا ثم حدد من أنت - نينجا، مبتدئ، أو مُراجِع شيفرات برمجية؟ البلاغة في الإيجاز اختصر الشيفرة البرمجية قدر الإمكان. أظهر مدى ذكائك. فلتَقُدكَ ميزات اللغة الخفية. مثال: ألق نظرة على العامل الثلاثي التالي '?': // مأخوذ من مكتبة جافاسكربت شهيرة 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 المقال السابق: التعليقات
  13. كما نعلم من جزء بنية الشيفرة البرمجية، يمكن ان تكون التعليقات ذات سطر واحد: وتبدأ ب // أو متعددة الأسطر: /* ... */. تُستخدم التعليقات لوصف الغرض من الشيفرة البرمجية وآلية عملها. في أول مرة، قد تبدو التعليقات مهمة، لكن المبرمجين المبتدئين يستخدمونها بطريقة غير صحيحة. التعليقات السيئة يستخدم المبتدئون التعليقات لشرح "ماذا يحدث في الشيفرة البرمجية". كما يلي: // ستقوم هذه الشيفرة البرمجية بهذا الأمر (...) وذلك الأمر (...) // وربما أشياء أخرى 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; } اقرأ أيضًا المقال التالي: شيفرة النينجا البرمجية المقال السابق: نمط كتابة الشيفرة
  14. يجب أن تكون الشيفرة البرمجية مرتبة ونظيفة وسهلة القراءة قدر الإمكان. هذا ما يسمى بفن البرمجة وهو أخذ مهمة معقدة وبرمجتها بطريقة صحيحة ومقروءة في الوقت ذاته. يساعد نمط كتابة الشيفرة كثيرا في ذلك. الصياغة في الصورة أدناه يوجد صفحة تحوي بعض القواعد المهمة في تركيب الشيفرات البرمجية. سنناقش هذه القواعد وأسبابها بالتفصيل. ملاحظة: لا يوجد قواعد الزامية. لا يوجد قاعدة إلزامية لنمط التكويد. هذه القواعد تعد تفضيلات وليست أساسيات برمجية. الأقواس المعقوصة (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
  15. قبل كتابة شيفرات برمجية أكثر تعقيدا، لنتطرَّق إلى تنقيح الأخطاء. تنقيح الأخطاء هي عملية إيجاد الأخطاء في السكريبت وإصلاحها. تدعم جميع المتصفحات الحديثة وبعض البيئات الأخرى "تنقيح الأخطاء" -واجهة مستخدم خاصة في أدوات المطور والتي تجعل العثور على الأخطاء وتصحيحها أمرا سهلا. تُتيح هذه الواجهة أيضا تَتَبُّع الأكواد خطوة بخطوة لمعرفة ما يحدث فيها بالتفصيل. سنستخدم 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; } اقرأ أيضًا المقال التالي: نمط كتابة الشيفرة المقال السابق: مراجعة لما سبق