صفا الفليج

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

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

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

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

0 Neutral
  1. لنقل بأنّ لدينا كائن معقّد البنية ونريد تحويله إلى سلسلة نصية؛ كي نُرسله عبر الشبكة أو نطبعه في الطرفية لتسجيل المخرجات. الطبيعي هو أن تكون في هذه السلسلة النصية كلّ الخاصيات المهمة. يمكننا إجراء هذا التحويل بهذه الطريقة: let user = { name: "John", age: 30, toString() { return `{name: "${this.name}", age: ${this.age}}`; } }; alert(user); // {name: "John", age: 30} ولكن… أثناء التطوير، نُضيف خاصيات جديدة ونُغيّر أسماء القديمة أو نحذفها حتّى. تحديث ذلك، مثل التابع toString، كلّ مرّة سيكون جحيمًا حقيقيًا. يمكن أن نمرّ على الخاصيات في الكائن، ولكن ماذا لو كان معقّدًا وفيه كائنات وخاصيات متداخلة؟ حينها سنحتاج إجراء تحويل لتلك أيضًا. لحسن حظّنا فكتابة تلك الشيفرة لهذه المعضلة ليس له داعٍ، فهناك من حلّها بالفعل. JSON.stringify نسق JSON (صيغة كائنات جافاسكربت JavaScript Object Notation) هو نسق عام لتمثيل القيم والكائنات، ويوثّقه المعيار RFC 4627. في بادئ الأمر كان غرض كتابته هو لاستعماله في جافاسكربت، ولكن رويدًا رويدًا بدأت اللغات الأخرى صناعة مكتبات تتعامل معه أيضًا. لهذا يسهل لنا استعمال JSON لتبادل البيانات حين يستعمل جهاز العميل جافاسكربت بينما الخادوم مكتوب بلغة روبي/PHP/جافا/أي لغة خنفشارية أخرى. تقدّم جافاسكربت التوابِع الآتية: JSON.stringify لتحويل الكائنات إلى صياغة JSON. JSON.parse لإرجاع بيانات مصاغة بصياغة JSON إلى كائن كما كان. فمثلًا هنا نستعمل JSON.stringify على طالب student: let student = { name: 'John', age: 30, isAdmin: false, courses: ['html', 'css', 'js'], wife: null }; let json = JSON.stringify(student); alert(typeof json); // حصلنا على سلسلة نصيةّ alert(json); /* ‫كائن مرمّز بِـJSON: { "name": "John", "age": 30, "isAdmin": false, "courses": ["html", "css", "js"], "wife": null } */ يأخذ التابِع JSON.stringify(student)‎ الكائن ويحوّله إلى سلسلة نصية. تُسمّى سلسلة json النصية الناتج بكائن مرمّز بِـJSON (JSON-encoded)‎ أو مُسلسل (serialized) أو stringified أو marshalled. صرنا مستعدّين لإرسال الكائن عبر الوِب أو تخزينه في مخزن بيانات خام. لاحظ من فضلك الاختلافات المهمة بين الكائن المرمّز بِـJSON من الكائن العادي الحرفي: تستعمل السلاسل النصية علامات اقتباس مزدوجة. لا مكان لعلامات الاقتباس المفردة أو الفواصل ` في JSON. بهذا يصير 'John' هكذا "John". حتّى خاصيات الكائنات تُحاط بعلامات اقتباس مزدوجة، ولا مناص من ذلك. بهذا يصير age:30 هكذا "age":30. يمكن استعمال JSON.stringify على الأنواع الأولية أيضًا. تدعم JSON أنواع البيانات الآتية: الكائنات { ... } المصفوفات [ ... ] الأنواع الأولية: السلاسل النصية, الأعداد, القيم المنطقية true/false, قيمة اللاشيء null. مثال: // ‫العدد في JSON ليس إلّا عددًا alert( JSON.stringify(1) ) // 1 // ‫السلسلة النصية في JSON ليست إلّا سلسلة نصيّة، بين علامات اقتباس مزودجة alert( JSON.stringify('test') ) // "test" alert( JSON.stringify(true) ); // true alert( JSON.stringify([1, 2, 3]) ); // [1,2,3] مواصفة JSON هي مواصفة مستقلّة لغويًا وتحمل البيانات فقط. لذا يُهمِل JSON.stringify خاصيات الكائنات الخاصّة بجافاسكربت. نذكر منها: خاصيات الدوال (التوابِع). الخاصيات الرمزية. الخاصيات التي تُخزّن undefined. let user = { sayHi() { // ignored alert("Hello"); }, [Symbol("id")]: 123, // يتجاهلها something: undefined // يتجاهلها }; alert( JSON.stringify(user) ); // ‫{} (كائن فارغ) غالبًا، لا مانع من ذلك. لو كان هناك مانع فسنرى قريبًا طريقة تخصيص عملية السَلسلة هذه. بفضل دهاء المبرمجين، فالكائنات المتداخلة مدعومة وستُحوَّل تلقائيًا. مثال: let meetup = { title: "Conference", room: { number: 23, participants: ["john", "ann"] } }; alert( JSON.stringify(meetup) ); /* البنية كاملة تتحوّل إلى سلسلة نصية: { "title":"Conference", "room":{"number":23,"participants":["john","ann"]}, } */ إليك التقييد: وجود الإشارات التعاودية (circular references) ممنوع. مثال: let room = { number: 23 }; let meetup = { title: "Conference", participants: ["john", "ann"] }; meetup.place = room; // ‫يُشير الاجتماع إلى الغرفة (meetup -> room) room.occupiedBy = meetup; // ‫تُشير الغرفة إلى الاجتماع (room -> meetup) JSON.stringify(meetup); // ‫خطأ تحاول تحويل بنية تعاوية إلى JSON هنا فشل التحويل بسبب الإشارات التعاودية: فتُشير room.occupiedBy إلى meetup وmeetup.place إلى room: الاستثناءات وتعديل الكائنات: آلة الاستبدال إليك الصياغة الكاملة للتابِع JSON.stringify: let json = JSON.stringify(value[, replacer, space]) المعاملات: value: القيمة التي ستُرمّز. replacer: : مصفوفة من الخاصيات لترميزها، أو دالة ربط (mapping‏) بالشكل function(key, value)‎. space: عدد المسافات لاستعمالها لتنسيق السلسلة النصية. في أغلب الوقت نستعمل JSON.stringify بتمرير المُعامل الأول فقط. ولكن لو أردنا تعديل عملية الاستبدال مثل تعديل الإشارات التعاودية، فيمكننا استعمال المُعامل الثاني للتابِع. لو مرّرنا مصفوفة فيها خاصيات، فستُرمّز تلك الخاصيات فقط. مثال: let room = { number: 23 }; let meetup = { title: "Conference", participants: [{name: "John"}, {name: "Alice"}], place: room // يُشير الاجتماع إلى الغرفة }; room.occupiedBy = meetup; // room references meetup alert( JSON.stringify(meetup, ['title', 'participants']) ); // {"title":"Conference","participants":[{},{}]} ربّما نكون هنا صارمين كثيرًا، فقائمة الخاصيات تُطبّق على كامل بنية الكائن، بهذا الكائنات في participants فارغة إذ أنّ name ليست في القائمة. لنضمّن في تلك القائمة كلّ خاصية عدا room.occupiedBy إذ ستتسبّب بإشارة تعاودية: let room = { number: 23 }; let meetup = { title: "Conference", participants: [{name: "John"}, {name: "Alice"}], place: room // يُشير الاجتماع إلى الغرفة }; room.occupiedBy = meetup; // تُشير الغرفة إلى الاجتماع alert( JSON.stringify(meetup, ['title', 'participants', 'place', 'name', 'number']) ); /* { "title":"Conference", "participants":[{"name":"John"},{"name":"Alice"}], "place":{"number":23} } */ الآن سَلسلنا كلّ ما في occupiedBy، ولكن قائمة الخاصيات صارت طويلة. لحسن حظّنا يمكننا استعمال دالة بدل المصفوفة لتكون آلة الاستبدال replacer. ستُستدعى الدالة لكلّ زوج (key, value) ويجب أن تُعيد القيمة ”المُستبدَلة“ التي ستحلّ مكان الأصلية، أو undefined لو أردنا إهمال الخاصية. في حالتنا هذه سنُعيد القيمة value ”كما هي“ لكل الخاصيات باستثناء occupiedBy. لنُهمل occupiedBy ستُعيد الشيفرة undefined: let room = { number: 23 }; let meetup = { title: "Conference", participants: [{name: "John"}, {name: "Alice"}], place: room // يُشير الاجتماع إلى الغرفة }; room.occupiedBy = meetup; // تُشير الغرفة إلى الاجتماع alert( JSON.stringify(meetup, function replacer(key, value) { alert(`${key}: ${value}`); return (key == 'occupiedBy') ? undefined : value; })); /* أزواج ‫key:value التي تدخل آلة الاستبدال: : [object Object] title: Conference participants: [object Object],[object Object] 0: [object Object] name: John 1: [object Object] name: Alice place: [object Object] number: 23 */ لاحِظ بأنّ الدالة replacer تأخذ كلّ زوج ”مفتاح/قيمة“ بما في ذلك الكائنات المتداخلة وعناصر المصفوفات، فهي تتطبّق تكراريًا. وقيمة this داخل replacer هي الكائن الذي يحتوي على الخاصية الحالية. الاستدعاء الأوّل خاصّ قليلًا، فهو يستلم ”كائن تغليف“: {"": meetup}. بعبارة أخرى فأوّل زوج (key, value) يكون مفتاحه فارغًا وقيمته هي الكائن الهدف كلّه. لهذا نرى السطر الأول في المثال أعلاه: ":[object Object]". الغرض هو تقديم كلّ ما أمكن من ”تسلّط“ لأداة الاستبدال، بهذا يمكنها تحليل الكائنات كاملةً واستبدالها أو إهمالها لو تطلّب الأمر. التنسيق: المسافات المُعامل الثالث للتابِع JSON.stringify(value, replacer, space)‎ هو عدد المسافات التي ستُستعمل لتنسيقها تنسيقًا جميلًا (Pretty format). في المثال السابق، لم يكن للكائنات المُسلسلة (stringified objects) أيّة مسافات أو مسافات بادئة. لا بأس لو كنّا سنرسل الكائن عبر الشبكة، فالمُعامل space يُستعمل فقط لتجميل الناتج. هنا بعبارة space = 2 نقول لجافاسكربت بأن تعرض الكائنات المتداخلة على عدّة أسطر، بمسافتين بادئتين داخل كل كائن: let user = { name: "John", age: 25, roles: { isAdmin: false, isEditor: true } }; alert(JSON.stringify(user, null, 2)); /* ‫إزاحة بمسافتين: { "name": "John", "age": 25, "roles": { "isAdmin": false, "isEditor": true } } */ /* ‫بينما JSON.stringify(user, null, 4)‎ يعطينا إزاحة أكبر: { "name": "John", "age": 25, "roles": { "isAdmin": false, "isEditor": true } } */ نستعمل المُعامل space فقط لغرض الناتج الجميل وعمليات تسجيل المخرجات. تابِع ”toJSON“ مخصّص كما يوجد toString للتحويل إلى سلاسل نصية، يمكن للكائنات أيضًا تقديم تابِع toJSON للتحويل إلى JSON. تستدعي JSON.stringify ذاك التابِع تلقائيًا لو وجدته. مثال: let room = { number: 23 }; let meetup = { title: "Conference", date: new Date(Date.UTC(2017, 0, 1)), room }; alert( JSON.stringify(meetup) ); /* { "title":"Conference", "date":"2017-01-01T00:00:00.000Z", // (1) "room": {"number":23} // (2) } */ نرى هنا بأنّ date (في (1)) صار سلسلة نصية. هذا لأنّ التواريخ كلّها توفّر تنفيذًا للتابِع toJSON مضمّنًا فيها، وهو يُعيد سلاسل نصية بهذا التنسيق. لنُضيف الآن تابِع toJSON مخصّص للكائن room (في (2)): let room = { number: 23, toJSON() { return this.number; } }; let meetup = { title: "Conference", room }; alert( JSON.stringify(room) ); // 23 alert( JSON.stringify(meetup) ); /* { "title":"Conference", "room": 23 } */ كما نرى، استُعمِل التابِع toJSON مرتين، مرة حين استدعاه JSON.stringify(room)‎ مباشرةً، ومرة حين كانت الخاصية room داخل كائن مرمّز آخر. التابع JSON.parse لنفكّ ترميز سلسلة JSON نصية، سنحتاج تابِعًا آخر بالاسم JSON.parse. صياغته هي: let value = JSON.parse(str, [reviver]); المعاملات: str: سلسلة JSON النصية التي سيُحلّلها. reviver: دالة اختيارية function(key,value)‎ تُستدعى لكلّ زوج (key, value) ويمكن لها تعديل القيمة. مثال: // ‫مصفوفة مُسلسلة (stringified array) let numbers = "[0, 1, 2, 3]"; numbers = JSON.parse(numbers); alert( numbers[1] ); // 1 أو حين استعمالها للكائنات المتداخلة: let userData = '{ "name": "John", "age": 35, "isAdmin": false, "friends": [0,1,2,3] }'; let user = JSON.parse(userData); alert( user.friends[1] ); // 1 يمكن أن يكون كائن JSON بالتعقيد اللازم مهمًا كان. يمكن أن تحتوي الكائنات والمصفوفات كائناتَ ومصفوفات أخرى، ولكنّ لزامٌ عليها أن تلتزم بنفس نسق JSON. إليك بعض المشاكل الشائعة حين كتابة JSON يدويًا (أحيانًا نفعل ذلك لأغراض تنقيح الشيفرات): let json = `{ name: "John", // خطأ: اسم خاصية بدون علامات اقتباس "surname": 'Smith', // خطأ: علامات اقتباس مُفردة في القيمة (يجب أن تكون مزودجة)‏ 'isAdmin': false // خطأ: علامات اقتباس مُفردة في المفتاح (يجب أن تكون مزدوجة)‏ "birthday": new Date(2000, 2, 3), // ‫خطأ: استعمال "new" ممنوع، فقط وفقط قيم "friends": [0,1,2,3] // هنا لا بأس }`; وأجل، لا تدعم JSON التعليقات، فلو أضفتها سيتحوّل الكائن إلى كائن غير صالح. هناك نسق آخر بالاسم JSON5 ويُتيح لنا عدم إحاطة المفاتيح بعلامات اقتباس، وكتابة التعليقات وغيرها. إلّا أنّها مكتبة مستقلة وليست في مواصفة لغة جافاسكربت. لم يصنع المطوّرون كائنات JSON العادية لتكون بهذه الصرامة لأنّهم كسالى، بل لنُعوّل على شيفرات خوارزميات التحليل، إضافة إلى عملها بسرعة فائقة. استعمال آلة الإحياء تخيّل أنّنا استلمنا كائن meetup مُسلسل من الخادوم، وهذا شكله: // title: (meetup title), date: (meetup date) let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}'; …نريد الآن فكّ ترميزه، أي إعادته إلى كائن جافاسكربت عادي. يكون ذلك باستدعاء JSON.parse: let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}'; let meetup = JSON.parse(str); alert( meetup.date.getDate() ); // خطأ‫! لحظة… خطأ؟! قيمة الخاصية meetup.date هي سلسلة نصية وليست كائن تاريخ Date. كيف سيعرف JSON.parse بأنّ عليه تعديل تلك السلسلة النصية لتصير Date؟ لنمرّر الآن إلى JSON.parse دالة ”آلة الإحياء“ في المُعامل الثاني، وستُحيي كلّ القيم ”كما هي“، عدا date ستعدّلها لتكون تاريخًا: let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}'; let meetup = JSON.parse(str, function(key, value) { if (key == 'date') return new Date(value); return value; }); alert( meetup.date.getDate() ); // ‫الآن صار يعمل! وأجل، تعمل الشيفرة للكائنات المتداخلة أيضًا: let schedule = `{ "meetups": [ {"title":"Conference","date":"2017-11-30T12:00:00.000Z"}, {"title":"Birthday","date":"2017-04-18T12:00:00.000Z"} ] }`; schedule = JSON.parse(schedule, function(key, value) { if (key == 'date') return new Date(value); return value; }); alert( schedule.meetups[1].date.getDate() ); // ‫يعمل! ملخص تنسيق JSON هو تنسيق بيانات يستقلّ بمعياره ومكتباته في غالبية لغات البرمجة. يدعم JSON الكائنات العادية والمصفوفات والسلاسل النصية والأعداد والقيم المنطقية وnull. تقدّم جافاسكربت التوابِع JSON.stringify لسَلسلة الكائنات إلى JSON، وJSON.parse للقراءة من JSON. يدعم كلا التابِعين دوال تعديل لتكون القراءة والكتابة ”ذكيّة“. لو كان في الكائن تابِع toJSON، فسيستدعيه JSON.stringify. تمارين تحويل الكائن إلى JSON وتحويله كما كان الأهمية: 5 حوّل الكائن user إلى JSON واقرأه ثانيةً ليكون متغيرًا آخرًا. let user = { name: "John Smith", age: 35 }; الحل let user = { name: "John Smith", age: 35 }; let user2 = JSON.parse(JSON.stringify(user)); استثناء الإشارات السابقة الأهمية: 5 يمكننا في الحالات العادية من الإشارات التعاودية استثناء خاصية محدّدة لألا تُسلسل، حسب اسمها. ولكن أحيانًا لا نستطيع استعمال الاسم إذ يُستعمل في الإشارات التعاودية زائدًا الخاصيات العادية. يمكننا هنا فحص الخاصية حسب قيمتها. اكتب دالة replacer تُسلسل كل شيء ولكن تُزيل الخاصيات التي تُشير إلى meetup: let room = { number: 23 }; let meetup = { title: "Conference", occupiedBy: [{name: "John"}, {name: "Alice"}], place: room }; // إشارات تعاودية room.occupiedBy = meetup; meetup.self = meetup; alert( JSON.stringify(meetup, function replacer(key, value) { /* شيفرتك هنا*/ })); /* ‫هكذا النتيجة المطلوبة: { "title":"Conference", "occupiedBy":[{"name":"John"},{"name":"Alice"}], "place":{"number":23} } */ الحل let room = { number: 23 }; let meetup = { title: "Conference", occupiedBy: [{name: "John"}, {name: "Alice"}], place: room }; room.occupiedBy = meetup; meetup.self = meetup; alert( JSON.stringify(meetup, function replacer(key, value) { return (key != "" && value == meetup) ? undefined : value; })); /* { "title":"Conference", "occupiedBy":[{"name":"John"},{"name":"Alice"}], "place":{"number":23} } */ علينا هنا (أيضًا) فحص key==""‎ لنستثني أوّل نداء إذ لا مشكلة بأن تكون القيمة value هي meetup. ترجمة -وبتصرف- للفصل JSON methods, toJSON من كتاب The JavaScript language .task__importance { color: #999; margin-left: 30px; } .task__answer { border: 3px solid #f7f6ea; margin: 20px 0 14px; position: relative; display: block; padding: 25px 30px; } اقرأ أيضًا المقال التالي: توابع الأنواع الأولية المقال السابق: النوع Date: تمثيل التاريخ والوقت
  2. حان وقت الحديث عن كائن آخر مضمّن في اللغة: التاريخ Date. يخزّن هذا الكائن التاريخ والوقت ويقدّم توابِع تُدير أختام التاريخ والوقت. يمكننا مثلًا استعماله لتخزين أوقات الإنشاء/التعديل أو حساب الوقت أو طباعة التاريخ الحالي في الطرفية. الإنشاء استدعِ new Date()‎ بتمرير واحدًا من الوُسطاء الآتية فتصنع كائن Date جديد: الدالة new Date()‎ تنشئ هذه الدالة بلا وُسطاء كائن Date بالتاريخ والوقت الحاليين: let now = new Date(); alert( now ); // نعرض التاريخ والوقت الحاليين إليك كيفية إنشاء كائن Date: new Date(milliseconds)‎ يُنشئ كائن Date إذ تساوي قيمته عدد المليثوان الممرّرة (المليثانية هي 1/1000 من الثاني) حسابًا من بعد الأول من يناير عام ١٩٧٠ بتوقيت UTC+0. // UTC+0 // 01.01.1970 نعني بـ 0 التاريخ let Jan01_1970 = new Date(0); alert( Jan01_1970 ); // نضيف الآن 24 ساعة لنحصل على 02.01.1970 let Jan02_1970 = new Date(24 * 3600 * 1000); alert( Jan02_1970 ); يُسمّى العدد الصحيح الذي يمثّل عدد المليثوان التي مرّت من بداية عام 1970 بالختم الزمني (بصمة وقت). وهو يمثّل التاريخ بنحوٍ عددي خفيف (lightweight). يمكننا إنشاء التواريخ من الأختام الزمنية باستعمال new Date(timestamp)‎ وتحويل كائن التاريخ Date الموجود إلى ختم زمني باستعمال التابِع date.getTime()‎ (طالع أسفله). والتواريخ قبل الأول من يناير 1970 أختامها سالبة: // ‫31 ديسمبر 1969 let Dec31_1969 = new Date(-24 * 3600 * 1000); alert( Dec31_1969 ); new Date(datestring)‎ لو كان هناك وسيط واحد وكان سلسلة نصيّة، فسيحلّله المحرّك تلقائيًا. الخوازرمية هنا هي ذات التي يستعملها Date.parse. لا تقلق، سنتكلم عنه لاحقًا. let date = new Date("2017-01-26"); alert(date); نجد في هذا المثال أن الوقت غير محدد لذا يكون بتوقيت GMT منتصف الليل، ويحدد وفقًا للمنطقة الزمنية التي تنفذ الشيفرة ضمنها، فالنتيجة يمكن أن تكون Thu Jan 26 2017 11:00:00 للبلدان ذات المنطقة الزمنية GMT+1100 أو يمكن أن تكون Wed Jan 25 2017 16:00:00 للبلدان الواقعة في المنطقة الزمنية GMT-0800. new Date(year, month, date, hours, minutes, seconds, ms)‎ يُنشئ تاريخًا بالمكوّنات الممرّرة حسب المنطقة الزمنية المحلية. أوّل وسيطين إلزاميين أما البقية اختيارية. يجب أن يكون العام year بأربع خانات: 2013 صح، 98 خطأ. يبدأ الشهر month بالرقم 0 (يناير) وينتهي بالعدد 11 (ديسمبر). مُعامل التاريخ date هو رقم اليوم من الشهر. لو لم يكن موجودًا فسيعدّه الكائن 1. لو لم تكن مُعاملات الساعة والدقيقة والثانية والمليثانية hours/minutes/seconds/ms موجودة، فسيعدّها الكائن 0. مثال: new Date(2011, 0, 1, 0, 0, 0, 0); // 1 Jan 2011, 00:00:00 new Date(2011, 0, 1); // نفس تلك. الساعات والدقائق وغيرها 0 مبدئيًا أدنى دقّة للتاريخ هي مليثانية واحدة (واحد من ألف من الثانية): let date = new Date(2011, 0, 1, 2, 3, 4, 567); alert( date ); // 1.01.2011, 02:03:04.567 الوصول إلى مكوّنات التاريخ إليك التوابِع التي تتيح لك الوصول إلى العام والشهر وغيرها داخل كائن Date: getFullYear()‎: يجلب العام (٤ خانات) getMonth()‎: يجلب الشهر، من 0 إلى 11. getDate()‎: يجلب رقم اليوم من الشهر، من 1 إلى 31. قد يبدو الاسم غريبًا قليلًا لك. التوابع getHours()‎ وgetMinutes()‎ وgetSeconds()‎ وgetMilliseconds()‎: تجلب مكوّنات الوقت حسب كل تابِع. (الساعة/الدقيقة/الثانية/المليثانية) إياك بـ getYear()‎ بل getFullYear()‎ تقدّم الكثير من محرّكات جافاسكربت التابِع غير القياسي getYear()‎. هذا التابِع أصبح بائدًا، فهو يُعيد العام بخانتين أحيانًا. من فضلك لا تستعمله أبدًا، بل getFullYear()‎ لتجلب العام. كما يمكن أيضًا جلب رقم اليوم من الشهر: التابع getDay()‎ يجلب رقم اليوم من الأسبوع، بدءًا بِـ 0 (الأحد) وحتى 6 (السبت). أوّل يوم هو الأحد دومًا. صحيح أنّ في بعض الدول هذا غير صحيح، لكن لا يمكن تغيير القيمة إطلاقًا. تُعيد كلّ التوابِع أعلاه المكوّنات حسب المنطقة الزمنية المحلية. توجد أيضًا مثيلاتها بنظام UTC حيث تُعيد اليوم والشهر والعام وغيرها في المنطقة الزمنية UTF+0:‏ getUTCFullYear()‎ وgetUTCMonth()‎ وgetUTCDay()‎. ضع كلمة "UTC" بعد "get" وستجد المثيل المناسب. لو كانت منطقتك الزمنية المحلية بعيدة عن UTC، فستعرض الشيفرة أدناه الساعات مختلفة عن بعضها البعض: // التاريخ الحالي let date = new Date(); // الساعة حسب المنطقة الزمنية التي أنت فيها alert( date.getHours() ); // ‫الساعة حسب المنطقة الزمنية بتوقيت UTC+0 (أي توقيت لندن بدون التوقيت الصيفي) alert( date.getUTCHours() ); هناك (إضافةً إلى هذه التوابِع) تابِعان آخران مختلفان قليلًا ليس لهما نُسخ بتوقيت UTC: التابع getTime()‎ يُعيد ختم التاريخ الزمني، أي عدد المليثوان التي مرّت منذ الأول من يناير عام 1970 بتوقيت UTC+0. التابع getTimezoneOffset()‎ يُعيد الفرق بين المنطقة الزمنية الحالية وتوقيت UTC (بالدقيقة): // ‫لو كانت منطقتك الزمنية UTC-1، فالناتج 60 // لو كانت منطقتك الزمنية ‫UTC+3، فالناتج ‎-180 alert( new Date().getTimezoneOffset() ); ضبط مكوّنات التاريخ تتيح لك التوابِع الآتية ضبط مكوّنات التاريخ والوقت: العام: setFullYear(year, [month], [date])‎ الشهر: setMonth(month, [date])‎ التاريخ: setDate(date)‎ الساعة: setHours(hour, [min], [sec], [ms])‎ الدقيقة: setMinutes(min, [sec], [ms])‎ الثانية: setSeconds(sec, [ms])‎ المليثانية: setMilliseconds(ms)‎ الوقت بالمليثانية: setTime(milliseconds)‎ (تضبط التاريخ كلّه حسب عدد المليثوان منذ 01.01.1970 UTC) لدى كلّ تابع منها نسخة بتوقيت UTC (عدا setTime()‎). مثال: setUTCHours()‎. كما رأيت فيمكن لبعض التوابِع ضبط عدّة مكوّنات في آن واحد مثل setHours. المكوّنات التي لا تُمرّر لا تُعدّل. مثال: let today = new Date(); today.setHours(0); alert(today); // ما زال اليوم نفسه، ولكن الساعة تغيّرت إلى 0 today.setHours(0, 0, 0, 0); alert(today); // ما زال اليوم نفسه، ولكنّا عند 00:00:00 تمامًا. التصحيح التلقائي ميزة التصحيح التلقائي في كائنات التواريخ Date مفيدة جدًا لنا، إذ يمكن أن نضع قيم تاريخ لامنطقية (مثل الخمسون من هذا الشهر) وسيُعدّلها الكائن بنفسه. مثال: let date = new Date(2013, 0, 32); // ‫الثاني والثلاثين من يناير 2013؟! alert(date); // ‫...آه، تقصد الأول من فبراير 2013! تترتّب المكوّنات اللامنطقية تلقائيًا. فمثلًا لو أضفت على التاريخ ”28 فبراير 2016“ يومين اثنين، فيمكن أن يكون ”الثاني من مارس“ أو ”الأول من مارس“ لو كانت السنة كبيسة. بدل أن نفكّر بهذا الحساب، نُضيف يومين ونترك الباقي على كائن Date: let date = new Date(2016, 1, 28); date.setDate(date.getDate() + 2); alert( date ); // ‫1 مارس 2016 غالبًا ما تُستعمل هذه الميزة لنجلب التاريخ بعد فترة محدّدة من الزمن. فلنقل مثلًا نريد تاريخ ”70 ثانية من الآن“: let date = new Date(); date.setSeconds(date.getSeconds() + 70); alert( date ); // يعرض التاريخ الصحيح يمكننا أيضًا ضبط القيمة لتكون صفرًا أو حتّى بالسالب. مثال: let date = new Date(2016, 0, 2); // ‫2 يناير 2016 date.setDate(1); // نضبط التاريخ على أول يوم من الشهر alert( date ); date.setDate(0); // أقل يوم ممكن هو 1، إذًا فيعدّ الكائن أنّ 0 هو آخر يوم من الشهر الماضي alert( date ); // ‫31 ديسمبر 2015 تحويل التاريخ إلى عدد، والفرق بين تاريخين حين يتحوّل كائن Date إلى عدد يصير ختمًا زمنيًا مطابقًا لختم date.getTime()‎: let date = new Date(); alert(+date); // ‫عدد المليثوان، نفس ناتج date.getTime()‎ تأثير هذا المهم والخطير هو أنّك تستطيع طرح التواريخ من بعض، والناتج سيكون الفرق بينهما بالمليثانية. يمكن استعمال الطرح لحساب الأوقات: let start = new Date(); // نبدأ قياس الوقت // إلى العمل for (let i = 0; i < 100000; i++) { let doSomething = i * i * i; } let end = new Date(); // ننتهي من قياس الوقت alert( `The loop took ${end - start} ms` ); التاريخ الآن لو أردنا قياس الوقت فقط فلا نحتاج كائن Date، بل هناك تابِعًا خاصًا باسم Date.now()‎ يُعيد لنا الختم الزمني الحالي. يُكافئ هذا التابِع الجملةَ new Date().getTime()‎ إلّا أنّه لا يُنشئ كائن Date يتوسّط العملية، ولهذا هو أسرع ولا يزيد الضغط على عملية كنس المهملات. غالبًا ما يُستعمل التابِع لأنّه أسهل أو لأنّ الأداء في تلك الحالة مهم، مثلما في الألعاب بلغة جافاسكربت أو التطبيقات المتخصّصة الأخرى. ولهذا قد يكون الأفضل كتابة الشيفرة أدناه بدل تلك: let start = Date.now(); // ‫تبدأ المليثوان من تاريخ 1 يناير 1970 // إلى العمل for (let i = 0; i < 100000; i++) { let doSomething = i * i * i; } *!* let end = Date.now(); // انتهينا */!* alert( `The loop took ${end - start} ms` ); // نطرح الأعداد لا التواريخ قياس الأداء لو أردنا قياس أداء دالة شرهة في استعمال المعالج، فعلينا أن نكون حذرين، هذا لو أردنا التعويل على القياس. فلنقيس مثلًا دالتين اثنتين تحسبان الفرق بين تاريخين: أيهما أسرع؟ نُطلق على قياسات الأداء هذه… قياسات أداء ”Benchmark“. // ‫أمامنا date1 وdate2، أيّ دالة ستُعيد الفرق بينهما (بالمليثانية) أسرع من الأخرى؟‫ هذه... function diffSubtract(date1, date2) { return date2 - date1; } // ‫أم هذه... function diffGetTime(date1, date2) { return date2.getTime() - date1.getTime(); } وظيفة الدالتين متطابقة تمامًا، إلّا أن الثانية تستعمل التابِع date.getTime()‎ الصريح لتجلب التاريخ بالمليثانية، بينما الأخرى تعتمد على تحويل التاريخ إلى عدد. الناتج متطابق دومًا. إذًا بهذه المعطيات، أيّ الدالتين أسرع؟ أوّل فكرة تخطر على البال هو تشغيل كلّ واحدة مرات عديدة متتابعة وقياس فرق الوقت. الدوال (في حالتنا هذه) بسيطة جدًا، ولهذا علينا تشغيل كلّ واحدة مئة ألف مرة على الأقل. هيًا نقيس الأداء: function diffSubtract(date1, date2) { return date2 - date1; } function diffGetTime(date1, date2) { return date2.getTime() - date1.getTime(); } function bench(f) { let date1 = new Date(0); let date2 = new Date(); let start = Date.now(); for (let i = 0; i < 100000; i++) f(date1, date2); return Date.now() - start; } alert( 'Time of diffSubtract: ' + bench(diffSubtract) + 'ms' ); alert( 'Time of diffGetTime: ' + bench(diffGetTime) + 'ms' ); عجبًا! استعمال التابِع getTime()‎ أسرع بكثير! يعزو ذلك بسبب انعدام وجود تحويل للنوع (type conversion)، وهذا يسهّل على المحرّكات تحسين الأداء. جميل، وصلنا إلى شيء، ولكنّ هذا القياس ليس قياسًا طيبًا بعد. تخيّل أنّ المعالج كان ينفّذ أمرًا ما بالتوازي مع تشغيل bench(diffSubtract)‎ وكان يستهلك الموارد، وما إن شغّلنا bench(diffGetTime)‎ كان ذلك الأمر قد اكتمل. هذا التخيّل هو تخيّل طبيعي لأمر واقعيّ جدًا حيث اليوم أنظمة التشغيل متعدّدة المهام. بهذا يكون لمرة القياس الأولى موارد معالجة أقل من المرة الثانية، ما قد يؤدّي إلى نتائج قياس خطأ. إن أردنا التعويل على قياس الأداء، علينا إعادة تشغيل كل قياسات الأداء الموجودة أكثر من مرّة. هكذا مثلًا: function diffSubtract(date1, date2) { return date2 - date1; } function diffGetTime(date1, date2) { return date2.getTime() - date1.getTime(); } function bench(f) { let date1 = new Date(0); let date2 = new Date(); let start = Date.now(); for (let i = 0; i < 100000; i++) f(date1, date2); return Date.now() - start; } let time1 = 0; let time2 = 0; // ‫نشغّل bench(upperSlice)‎ وbench(upperLoop)‎ عشر مرات مرّة بمرّة for (let i = 0; i < 10; i++) { time1 += bench(diffSubtract); time2 += bench(diffGetTime); } alert( 'Total time for diffSubtract: ' + time1 ); alert( 'Total time for diffGetTime: ' + time2 ); لا تبدأ محرّكات جافاسكربت الحديثة بتطبيق التحسينات المتقدّمة إلّا على ”الشيفرات الحرجة“ التي تتنفّذ أكثر من مرّة (لا داعٍ بتحسين شيفرة نادرة التنفيذ). بهذا في المثال الأول، قد لا تكون مرات التنفيذ الأولى محسّنة كما يجب، وربما علينا إضافة تحمية سريعة: // أضفناه لـ”تحمية“ المحرّك قبل الحلقة الأساس bench(diffSubtract); bench(diffGetTime); // الآن نقيس for (let i = 0; i < 10; i++) { time1 += bench(diffSubtract); time2 += bench(diffGetTime); } الزم الحذر متى ما أجريت قياسات أداء على المستوى الذرّي. تُنفّذ محرّكات جافاسكربت الحديثة عددًا كبيرًا من التحسينات، وقد تُغيّر نتائج ”الاختبارات الصناعية“ موازنةً ”بالاستعمال الطبيعي لها“، خصوصًا حين نقيس أداء ما هو صغير للغاية مثل طريقة عمل مُعامل رياضي، أو دالة مضمّنة في اللغة نفسها. لهذا، لو كنت تريد حقًا فهم الأداء كما يجب، فمن فضلك تعلّم طريقة عمل محرّك جافاسكربت. حينها ربّما لن تحتاج هذه القياسات على المستوى الذرّي، أبدًا. يمكنك أن تقرأ بعض المقالات الرائعة حول V8 هنا http://mrale.ph. تحليل سلسلة نصية باستعمال Date.parse يمكن أن يقرأ التابِع Date.parse(str)‎ تاريخًا من سلسلة نصية. يجب أن يكون تنسيق تلك السلسلة هكذا: YYYY-MM-DDTHH:mm:ss.sssZ، إذ تعني: YYYY-MM-DD -- التاريخ: اليوم-الشهر-العام. يُستعمل المحرف "T" فاصِلًا. HH:mm:ss.sss -- الوقت: المليثانية والثانية والدقيقة والساعة. يمثّل الجزء الاختياري 'Z' المنطقة الزمنية حسب التنسيق +-hh:mm. لو وضعت Z فقط فذلك يعني UTC+0. يمكنك أيضًا استعمال تنسيقات أقصر مثل YYYY-MM-DD أو YYYY-MM أو حتّى YYYY. باستدعاء Date.parse(str) فالسلسلة النصية تُحلّل حسب التنسيق فيها ويُعيد التابِع الختم الزمني (رقم المليثوان منذ الأول من يناير 1970 بتوقيت UTC+0). لو كان التنسيق غير صحيح فيُعيد NaN. إليك مثالًا: let ms = Date.parse('2012-01-26T13:51:50.417-07:00'); alert(ms); // ‫1327611110417 (ختم زمني) يمكننا إنشاء كائن new Date مباشرةً من الختم الزمني: let date = new Date( Date.parse('2012-01-26T13:51:50.417-07:00') ); alert(date); ملخص يُمثّل التاريخ والوقت في جافاسكربت بكائن Date. لا يمكننا إنشاء ”تاريخ فقط“ أو ”وقتًا فقط“، فعلى كائنات التاريخ Date احتواء الاثنين معًا. تُعدّ الأشهر بدءًا بالصفر (يناير هو الشهر صفر، نعم). يُعدّ رقم اليوم من الأسبوع في getDay()‎ من الصفر أيضًا (وهو يوم الأحد). يصحّح كائن التاريخ نفسه تلقائيًا حين تُضبط مكوّناته بقيم لا منطقية. يفيدنا لجمع/طرح الأيام والأشهر والأعوام. يمكن طرح التواريخ ومعرفة الفرق بينها بالمليثانية، ذلك لأنّ كائن التاريخ يتحوّل إلى ختم زمني حين يتحوّل إلى عدد. استعمل Date.now()‎ لو أردت جلب الختم الزمني الحالي بسرعة. لاحظ بأنّ الأختام الزمنية في جافاسكربت هي بالمليثانية، على العكس من أنظمة عديدة أخرى. نجد نفسنا بين الحين والآخر قياسات وقت دقيقة. للأسف فلا توفّر جافاسكربت نفسها طريقة لحساب الوقت بالنانوثانية (1 على مليون من الثانية)، ولكن أغلب بيئاتها توفّر ذلك. فمثلًا تملك المتصفّحات التابِع performance.now()‎ إذ يُعيد عدد المليثوان منذ بدأ تحميل الصفحة بقدّة تصل إلى المايكروثانية (ثلاث خانات بعد الفاصلة): alert(`Loading started ${performance.now()}ms ago`); // "Loading started 34731.26000000001ms ago" تعني ”‎.26“ هنا المايكروثوان (260 مايكروثانية)، فلو زِدت على ثلاث خانات بعد الفاصلة فستجد أخطاءً في دقّة الحساب. أوّل ثلاثة هي الصحيحة فقط. تملك لغة Node.js أيضًا وحدة microtime وأخرى غيرها. يمكن (تقنيًا) لأيّ جهاز أو بيئة أن تعطينا دقّة وقت أعلى، Date لا تقدّم ذلك لا أكثر. تمارين إنشاء تاريخ الأهمية: 5 أنشِئ كائن Date لهذا التاريخ: 20 فبراير 2012، 3:12 صباحًا. المنطقة الزمنية هي المحلية. اعرض التاريخ باستعمال alert. الحل يستعمل مُنشِئ new Date المنطقة الزمنية الحالية. عليك ألا تنسى بأنّ الأشهر تبدأ من الصفر. إذًا ففبراير هو الشهر رقم 1. let d = new Date(2012, 1, 20, 3, 12); alert( d ); اعرض اسم اليوم من الأسبوع الأهمية: 5 اكتب دالة getWeekDay(date)‎ تعرض اسم اليوم من الأسبوع بالتنسيق الإنكليزي القصير: 'MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'. مثال: let date = new Date(2012, 0, 3); // ‫3 يناير 2012 alert( getWeekDay(date) ); // ي‫جب أن يطبع "TU" الحل يُعيد التابِع date.getDay()‎ رقم اليوم من الأسبوع، بدءًا من يوم الأحد. لنصنع مصفوفة فيها أيام الأسبوع لنعرف اليوم الصحيح من رقمه: function getWeekDay(date) { let days = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA']; return days[date.getDay()]; } let date = new Date(2014, 0, 3); // ‫3 يناير 2014 alert( getWeekDay(date) ); // FR اليوم من الأسبوع في أوروبا الأهمية: 4 في الدول الأوروبية، يبدأ الأسبوع بيوم الإثنين (رقم 1) وثمّ الثلاثاء (رقم 2) وحتّى الأحد (رقم 7). اكتب دالة getLocalDay(date) تُعيد يوم الأسبوع ”الأوروبي“ من التاريخ date. let date = new Date(2012, 0, 3); // ‫3 يناير 2012 alert( getLocalDay(date) ); // يكون يوم ثلاثاء، يجب أن تعرض 2 الحل function getLocalDay(date) { let day = date.getDay(); if (day == 0) { // يوم الأحد 0 في أوروبا هو الأخير (7)‏ day = 7; } return day; } ما هو التاريخ الذي كان قبل كذا يوم؟ الأهمية: 4 أنشِئ دالة getDateAgo(date, days)‎ تُعيد بتمرير التاريخ date اسم اليوم من الشهر قبل فترة days يوم. مثال: لو كان اليوم العشرون من الشهر، فتُعيد getDateAgo(new Date(),1 )‎ التاسع عشر وgetDateAgo(new Date(), 2)‎ الثامن عشر. يجب أن نعوّل بأن تعمل الدالة في حال days=356 وأكثر حتّى: let date = new Date(2015, 0, 2); alert( getDateAgo(date, 1) ); // ‫1، (1 يناير 2015) alert( getDateAgo(date, 2) ); // ‫31، (31 ديسمبر 2014) alert( getDateAgo(date, 365) ); // ‫2، (2 يناير 2014) ملاحظة: يجب ألّا تُعدّل الدالة التاريخ date المُمرّر. الحل الفكرة بسيطة، أن نطرح عدد الأيام من التاريخ date: function getDateAgo(date, days) { date.setDate(date.getDate() - days); return date.getDate(); } ولكن… يجب ألّا تُعدّل الدالة على date. هذا مهم إذ أنّ الشيفرة خارج الدالة التي تُعطينا التاريخ لا تريد منّا تغييره. لننفّذ ذلك، علينا نسخ التاريخ هكذا أولًا: function getDateAgo(date, days) { let dateCopy = new Date(date); dateCopy.setDate(date.getDate() - days); return dateCopy.getDate(); } let date = new Date(2015, 0, 2); alert( getDateAgo(date, 1) ); // ‫1، (1 يناير 2015) alert( getDateAgo(date, 2) ); // ‫31، (31 ديسمبر 2014) alert( getDateAgo(date, 365) ); // ‫2، (2 يناير 2014) آخر يوم من الشهر كذا؟ الأهمية: 5 اكتب دالة getLastDayOfMonth(year, month)‎ تُعيد آخر يوم من الشهر. أحيانًا يكون الثلاثين، أو الحادي والثلاثين أو الثامن/التاسع عشر من فبراير. المُعاملات: year -- العام بأربع خانات، مثلًا 2012. month -- الشهر من 0 إلى 11. مثال: getLastDayOfMonth(2012, 1) = 29 (سنة كبيسة، فبراير). الحل فلنصنع تاريخًا باستعمال الشهر التالي، ولكنّ نمرّر الصفر ليكون رقم اليوم: function getLastDayOfMonth(year, month) { let date = new Date(year, month + 1, 0); return date.getDate(); } alert( getLastDayOfMonth(2012, 0) ); // 31 alert( getLastDayOfMonth(2012, 1) ); // 29 alert( getLastDayOfMonth(2013, 1) ); // 28 عادةً ما تبدأ التواريخ بالواحد، لكن يمكننا (تقنيًا) تمرير أيّ عدد وسيُعدّل التاريخ نفسه. لذا حين نمرّر 0 نعني بذلك ”يومًا واحد قبل الأول من الشهر“، أي ”اليوم الأخير من الشهر الماضي“. كم من ثانية مضت اليوم؟ الأهمية: 5 اكتب دالة getSecondsToday()‎ تُعيد عدد الثواني منذ بداية هذا اليوم. فمثلًا لو كانت الساعة الآن 10:00 am، وبدون التوقيت الصيفي، فستعطينا الدالة: getSecondsToday() == 36000 // (3600 * 10) يجب أن تعمل الدالة مهما كان اليوم. أيّ ألا تحتوي على قيمة داخلها بتاريخ ”اليوم“… اليوم. الحل لنعرف عدد الثواني يمكننا توليد تاريخًا باستعمال اليوم الحالي والساعة 00:00:00، وثمّ نطرح منها ”الوقت والتاريخ الآن“. سيكون الفرق حينها بعدد المليثوان منذ بداية هذا اليوم، فنقسمه على 1000 لنعرف الثواني فقط: function getSecondsToday() { let now = new Date(); // أنشِئ كائنًا باستعمال اليوم والشهر والسنة حاليًا let today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); let diff = now - today; // الفرق بالمليثانية return Math.round(diff / 1000); // نحوّله إلى ثوان } alert( getSecondsToday() ); الحل الآخر هو جلب الساعة والدقيقة والثانية وتحويلها إلى عدد الثواني: function getSecondsToday() { let d = new Date(); return d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds(); } كم من ثانية بقت حتّى الغد؟ الأهمية: 5 أنشِئ دالة getSecondsToTomorrow()‎ تُعيد عدد الثواني حتّى يحلّ الغد. فمثلًا لو كان الوقت الآن 23:00، تُعيد لنا: getSecondsToTomorrow() == 3600 ملاحظة: يجب أن تعمل الدالة مهما كان اليوم، وألا تعتبر ”اليوم“ هذا اليوم. الحل لنعرف عدد المليثوان حتّى قدوم الغد، يمكننا أن نطرح من ”الغد 00:00:00“ التاريخ اليوم. أوّلًا، نولّد هذا ”الغد“ وثمّ ننفّذ الطرح: function getSecondsToTomorrow() { let now = new Date(); // تاريخ الغد let tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate()+1); let diff = tomorrow - now; // الفرق بالمليثانية return Math.round(diff / 1000); // نحوّله إلى ثوان } حل بديل: function getSecondsToTomorrow() { let now = new Date(); let hour = now.getHours(); let minutes = now.getMinutes(); let seconds = now.getSeconds(); let totalSecondsToday = (hour * 60 + minutes) * 60 + seconds; let totalSecondsInADay = 86400; return totalSecondsInADay - totalSecondsToday; } لاحظ أنّ هناك دولًا كثيرة تستعمل التوقيت الصيفي، لذا ستجد هناك أيام فيها 23 أو 25 ساعة. يمكن أن نتعامل مع هذه الأيام بنحوٍ منفصل. تنسيق التاريخ نسبيًا الأهمية: 4 اكتب دالة formatDate(date)‎ تُنسّق التاريخ date حسب الآتي: لو مرّت أقلّ من ثانية من date، فتُعيد "right now". وإلّا، لو مرّت أقلّ من دقيقة من date، فتُعيد "n sec. ago". وإلّا، لو أقل من ساعة، فتُعيد "m min. ago". وإلّا، فتُعيد التاريخ كاملًا بالتنسيق "DD.MM.YY HH:mm"، أي (شَكلًا): الدقيقة:الساعة العام:الشهر:اليوم (كلها بخانتين). مثل: 31.12.16 10:00. أمثلة: alert( formatDate(new Date(new Date - 1)) ); // "right now" alert( formatDate(new Date(new Date - 30 * 1000)) ); // "30 sec. ago" alert( formatDate(new Date(new Date - 5 * 60 * 1000)) ); // "5 min. ago" // ‫تاريخ الأمس، مثلًا ‎31.12.16, 20:00 alert( formatDate(new Date(new Date - 86400 * 1000)) ); الحل لنجلب الوقت المنقضي منذ date وحتّى الآن، سنطرح التاريخين. function formatDate(date) { let diff = new Date() - date; // الفرق بالمليثانية if (diff < 1000) { // أقل من ثانية واحدة return 'right now'; } let sec = Math.floor(diff / 1000); // نحوّل الفرق إلى ثوان if (sec < 60) { return sec + ' sec. ago'; } let min = Math.floor(diff / 60000); // نحوّل الفرق إلى دقائق if (min < 60) { return min + ' min. ago'; } // ننسّق التاريخ // ونُضيف أصفارًا لو كان اليوم/الشهر/الساعة/الدقيقة بخانة واحدة let d = date; d = [ '0' + d.getDate(), '0' + (d.getMonth() + 1), '' + d.getFullYear(), '0' + d.getHours(), '0' + d.getMinutes() ].map(component => component.slice(-2)); // نأخذ الخانتين الأخيرتين من كلّ مكوّن // ندمج المكوّنات في تاريخ return d.slice(0, 3).join('.') + ' ' + d.slice(3).join(':'); } alert( formatDate(new Date(new Date - 1)) ); // "right now" alert( formatDate(new Date(new Date - 30 * 1000)) ); // "30 sec. ago" alert( formatDate(new Date(new Date - 5 * 60 * 1000)) ); // "5 min. ago" // ‫تاريخ الأمس، مثلًا ‎31.12.16, 20:00 alert( formatDate(new Date(new Date - 86400 * 1000)) ); حل بديل: function formatDate(date) { let dayOfMonth = date.getDate(); let month = date.getMonth() + 1; let year = date.getFullYear(); let hour = date.getHours(); let minutes = date.getMinutes(); let diffMs = new Date() - date; let diffSec = Math.round(diffMs / 1000); let diffMin = diffSec / 60; let diffHour = diffMin / 60; // التنسيق year = year.toString().slice(-2); month = month < 10 ? '0' + month : month; dayOfMonth = dayOfMonth < 10 ? '0' + dayOfMonth : dayOfMonth; if (diffSec < 1) { return 'right now'; } else if (diffMin < 1) { return `${diffSec} sec. ago` } else if (diffHour < 1) { return `${diffMin} min. ago` } else { return `${dayOfMonth}.${month}.${year} ${hour}:${minutes}` } } لاحظ بأنّ هذه الطريقة سيّئة لو أردت دعم اللغات دعمًا صحيحًا (في العربية هناك ثانية واحدة وثانيتين وثلاث ثوان وخمسون ثانية وهكذا). .task__importance { color: #999; margin-left: 30px; } .task__answer { border: 3px solid #f7f6ea; margin: 20px 0 14px; position: relative; display: block; padding: 25px 30px; } اقرأ أيضًا المقال التالي: صيغة JSON وتوابعها المقال السابق: الإسناد بالتفكيك (Destructuring assignment) ترجمة -وبتصرف- للفصل Date and time من كتاب The JavaScript language
  3. في جافاسكربت، الكائنات والمصفوفات هي أكثر بنى البيانات المستعملة. تُتيح لنا الكائنات إنشاء كيان واحد يُخزّن عناصر البيانات حسب مفاتيحها، وتُتيح لنا المصفوفات بجمع مختلف عناصر البيانات في تجميعة مرتّبة (ordered collection). ولكن حين نُمرّرها هذه الكائنات والمصفوفات إلى دالة، غالبًا ما لا نريد كامل الكائن/المصفوفة، بل بعضًا منها لا أكثر. صياغة الإسناد بالتفكيك (Destructuring assignment) هي صياغة خاصّة تُتيح لنا ”فكّ“ المصفوفات أو الكائنات إلى مجموعة من المتغيرات إذ تكون أحيانًا أكثر منطقية. يفيدنا التفكيك أيضًا مع الدوال المعقّدة التي تحتوي على مُعاملات كثيرة وقيم مبدئية وغيرها وغيرها. تفكيك المصفوفات إليك مثال عن تفكيك مصفوفة إلى مجموعة من المتغيرات: // معنا مصفوفة فيها اسم الشخص واسم عائلته let arr = ["Ilya", "Kantor"] // يضبط الإسناد بالتفكيك // ‫هذه firstName = arr[0]‎ // ‫وهذه surname = arr[1]‎ let [firstName, surname] = arr; alert(firstName); // Ilya alert(surname); // Kantor يمكننا الآن العمل مع تلك المتغيرات عوض عناصر المصفوفة. وما إن تجمع تابِع split وغيرها من توابِع تُعيد مصفوفات، سترى بريق هذا التفكيك يتألق: let [firstName, surname] = "Ilya Kantor".split(' '); ”التفكيك“ (Destructuring) لا يعني ”التكسير“ (destructive) نُسمّيه "الإسناد بالتفكيك" (destructuring assignment) لأنّه "يفكّك" العناصر بنسخها إلى متغيرات. أمّا المصفوفة نفسها فتبقى دون تعديل. كتابة هذه الشيفرة أسهل من تلك الطويلة (ندعها لك تتخيّلها): // let [firstName, surname] = arr; let firstName = arr[0]; let surname = arr[1]; أهمِل العناصر باستعمال الفواصل يمكنك ”رمي“ وتجاهل العناصر التي لا تريدها بإضافة فاصلة أخرى: // لا نريد العنصر الثاني let [firstName, , title] = ["Julius", "Caesar", "Consul", "of the Roman Republic"]; alert( title ); // Consul في الشيفرة هذه تخطّينا العنصر الثاني في المصفوفة وأسندنا الثالث إلى المتغير title، كما وتخطّينا أيضًا باقي عناصر المصفوفة (ما من متغيّرات لها). تعمل الميزة مع المُتعدَّدات حين تكون على اليمين …الواقع هو أنّنا نستطيع استعمالها مع أيّ مُكرَّر وليس المصفوفات فقط: let [a, b, c] = "abc"; // ["a", "b", "c"] let [one, two, three] = new Set([1, 2, 3]); أسنِدها إلى ما تريد على اليسار يمكن أن نستعمل أيّ متغيّر يمكن إسناده على الجانب الأيسر من سطر الإسناد. لاحظ مثلًا إسناد خاصية لكائن: let user = {}; [user.name, user.surname] = "Ilya Kantor".split(' '); alert(user.name); // Ilya المرور على العناصر عبر ‎.entries()‎ رأينا في الفصل الماضي التابِع Object.entries(obj)‎. يمكننا استعماله مع التفكيك للمرور على مفاتيح الكائنات وقيمها: let user = { name: "John", age: 30 }; // نمرّ على المفاتيح والقيم for (let [key, value] of Object.entries(user)) { alert(`${key}:${value}`); // name:John, then age:30 } …وذات الأمر للخارطة: let user = new Map(); user.set("name", "John"); user.set("age", "30"); for (let [key, value] of user) { alert(`${key}:${value}`); // name:John, then age:30 } الباقي ”…“ لو أردنا أخذ القيم الأولى إضافةً إلى كل ما يليها، فنُضيف مُعاملًا آخر يجلب ”الباقي“ باستعمال ثلاث نقاط "...": let [name1, name2, ...rest] = ["Julius", "Caesar", "Consul", "of the Roman Republic"]; alert(name1); // Julius alert(name2); // Caesar // ‫انتبه أنّ المتغير rest مصفوفة. alert(rest[0]); // Consul alert(rest[1]); // of the Roman Republic alert(rest.length); // 2 ستكون قيمة المتغير rest مصفوفةً فيها عناصر الباقية في المصفوفة الأولى. يمكننا استعمال أيّ اسم آخر بدل rest، المهم أن يكون قبله ثلاث نقاط ويكون الأخير في جملة الإسناد بالتفكيك. القيم المبدئية لو كانت القيم في المصفوفة أقلّ من تلك في جملة الإسناد فلن يحدث أيّ خطأ. يُعدّ المحرّك القيم ”الغائبة“ غير معرّفة: let [firstName, surname] = []; alert(firstName); // undefined alert(surname); // undefined لو أردنا قيمة مبدئية تعوّض تلك الناقصة فيمكننا تقديمها باستعمال =: // القيم المبدئية let [name = "Guest", surname = "Anonymous"] = ["Julius"]; alert(name); // ‫Julius (من المصفوفة) alert(surname); // ‫Anonymous (المبدئي) يمكن أن تكون القيم المبدئية تعابيرَ معقّدة أو استدعاءات دوال حتّى. لن يقدّر ناتجها المحرّك إلّا لو لم تمرّر القيم تلك. فمثلًا يمكننا استعمال الدالة promot لأخذ قيمتين مبدئيتين. أمّا هنا فستسأل عن القيمة الناقصة فقط: // ‫لا تطلب إلا اسم العائلة surname let [name = prompt('name?'), surname = prompt('surname?')] = ["Julius"]; alert(name); // ‫Julius (نأخذه من المصفوفة) alert(surname); // ‫نحترم هنا ما يقول promot تكفيك الكائنات الإسناد بالتفكيك يدعم أيضًا الكائنات. هذه صياغته الأساس: let {var1, var2} = {var1:…, var2:…} على اليمين الكائن الموجود والذي نريد تقسيمه على عدّة متغيرات، وعلى اليسار نضع ”نمط“ الخاصيات المقابِلة له. لو كان الكائن بسيطًا، فهذا النمط هو قائمة باسم المتغيرات داخل {...}. مثال: let options = { title: "Menu", width: 100, height: 200 }; let {title, width, height} = options; alert(title); // Menu alert(width); // 100 alert(height); // 200 تُسند الخاصيات options.title وoptions.width وoptions.height إلى المتغيرات المقابِلة لها. كما وأنّ الترتيب غير مهم: يمكنك فعل هذا أيضًا: // ‫غيّرنا الترتيب داخل ‪let {...} let {height, width, title} = { title: "Menu", height: 200, width: 100 } يمكن أن يكون النمط على اليسار معقّدًا أكثر ومُحدّدًا فيحدّد طريقة ترابط الخاصيات بالمتغيرات عبر الخارطة (mapping). لو أردنا إسناد خاصية إلى متغير له اسم آخر فعلينا استعمال النقطتين الرأسيتين لذلك (مثلًا options.width يصير في المتغير w): let options = { title: "Menu", width: 100, height: 200 }; // { sourceProperty: targetVariable } let {width: w, height: h, title} = options; // width -> w // height -> h // title -> title alert(title); // Menu alert(w); // 100 alert(h); // 200 تعني النقطتان الرأسيتان ”هذا : يصير هذا“. في المثال أعلاه، تصير الخاصية width بالاسم w، والخاصية height بالاسم h والخاصية title كما هي title. يمكننا هنا أيضًا وضع قيمة مبدئية للخاصيات الناقصة باستعمال "=" هكذا: let options = { title: "Menu" }; let {width = 100, height = 200, title} = options; alert(title); // Menu alert(width); // 100 alert(height); // 200 كما يمكن أن تكون هذه القيم المبدئية أيّة تعابير أو استدعاءات دوال كما مقابلاتها في المصفوفات ومُعاملات الدوال، ولن يقدّر المحرّك قيمتها إلّا لو لم تقدّم قيمة للدالة. في الشيفرة أدناه، تطلب الدالة promot قيمة width ولا تطلب قيمة title: let options = { title: "Menu" }; *!* let {width = prompt("width?"), title = prompt("title?")} = options; */!* alert(title); // Menu alert(width); // (promot هنا نحترم أيضًا ما يقول) يمكننا أيضًا جمع النقطتان الرأسيتان والقيم المبدئية: let options = { title: "Menu" }; let {width: w = 100, height: h = 200, title} = options; alert(title); // Menu alert(w); // 100 alert(h); // 200 لو كان لدينا كائنًا معقّدًا فيه خاصيات كثيرة، فيمكننا استخراج ما نريد منه فقط: let options = { title: "Menu", width: 100, height: 200 }; // ‫استخرج العنوان title ليكون متغيرًا هو فقط let { title } = options; alert(title); // Menu نمط الباقي ”…“ ماذا لو كان للكائن خاصيات أكثر من المتغيرات التي لدينا؟ هل يمكننا أخذها وإسنادها في متغيّر ”rest“ أيضًا؟ أجل يمكننا استعمال نمط الباقي تمامًا مثل المصفوفات. بعض المتصفحات القديمة لا تدعمه (مثل إنترنت إكسبلورر، استعمل Babel لترقيعه polyfill، أي لتعويض نقص الدعم)، إلّا أن الحديثة تدعمه. هكذا نفعلها: let options = { title: "Menu", height: 200, width: 100 }; // ‫title = خاصية بالاسم title // ‫rest = كائن فيه باقي الخاصيات let {title, ...rest} = options; // ‫صار الآن title="Menu", rest={height: 200, width: 100} alert(rest.height); // 200 alert(rest.width); // 100 انتبه لو لم تضع let في المثال أعلاه، صرّحنا عن المتغيرات على يمين جملة الإسناد: let {…} = {…}‎. يمكننا طبعًا استعمال متغيرات موجودة دون let، ولكن هناك أمر، فهذا لن يعمل: let title, width, height; // سترى خطأ في هذا السطر {title, width, height} = {title: "Menu", width: 200, height: 100}; المشكلة هي أنّ جافاسكربت تتعامل مع {...} في سياق الشيفرة الأساس (أي ليس داخل تعبير آخر) على أنّها بنية شيفرة (Code Block). يمكن استعمال بنى الشيفرة هذه لجمع التعليمات البرمجية، هكذا: { // بنية شيفرة let message = "Hello"; // ... alert( message ); } وهنا يظنّ محرّك جافاسكربت بأنّ هذه بنية شيفرة فيعطينا الخطأ أعلاه، بينما ما نريد هو التفكيك. ولنقول للمحرّك بأنّ هذه ليست بنية شيفرة، نضع التعبير بين قوسين (...): let title, width, height; // الآن جيد ({title, width, height} = {title: "Menu", width: 200, height: 100}); alert( title ); // Menu تفكيك المتغيرات المتداخلة لو كان في الكائن أو المصفوفة كائنات ومصفوفات أخرى داخله، فيمكننا استعمال أنماط معقّدة على يسار جملة الإسناد لنستخرج تلك المعلومات. في الشيفرة أدناه، نجد داخل الكائن options كائنًا آخر في الخاصية size، ومصفوفة في الخاصية items. النمط على يسار جملة الإسناد لديه ذات البنية تلك لتستخرج هذه القيم من الكائن على يمينه: let options = { size: { width: 100, height: 200 }, items: ["Cake", "Donut"], extra: true }; // نقسم الإسناد بالتفكيك على أكثر من سطر لتوضيح العملية let { size: { // هنا يكون المقاس width, height }, items: [item1, item2], // وهنا نضع العناصر title = "Menu" // ليست موجودة في الكائن (ستُستعمل القيمة المبدئية)‏ } = options; alert(title); // Menu alert(width); // 100 alert(height); // 200 alert(item1); // Cake alert(item2); // Donut هكذا تُسند كلّ خاصيات options (عدا extra الناقصة يسار عبارة الإسناد) إلى المتغيرات المقابلة لها: وفي النهاية يكون لدينا المتغيّرات width وheight وitem1 وitem2 وtitle من تلك القيمة المبدئية. لاحظ ألّا وجود لمتغيّرات تنسخ size وitems إذ ما نريد هو محتواها لا هي. مُعاملات الدوال الذكية أحيانًا وأنت تعمل تجد نفسك تكتب دالة لها مُعاملات كثيرة وأغلبها اختيارية. يحدث هذا غالبًا مع دوال واجهات المستخدم. عُدّ أنّ لديك دالة تُنشئ قائمة، وللقائمة عَرض وارتفاع وعنوان وقائمة عناصر وغيرها. هكذا تصنع تلك الدالة بالأسلوب الخطأ: function showMenu(title = "Untitled", width = 200, height = 100, items = []) { // ... } تكمن المشكلة (في الحياة الواقعية) في تذكّر ترتيب تلك الوُسطاء. صحيح أنّ بيئات التطوير تفيدنا هنا عادةً -خصوصًا لو كان المشروع موثّق توثيقًا ممتازًا- ولكن مع ذلك فالمشكلة الأخرى هي طريقة استدعاء الدالة لو كانت كلّ مُعاملاتها المبدئية مناسبة لنا. نستدعيها هكذا؟ // نضع ‫undefined لو كانت القيم المبدئية تقوم بالغرض showMenu("My Menu", undefined, undefined, ["Item1", "Item2"]) جريمة بحقّ الجمال. ورويدًا رويدًا تصير مستحيلة القراءة حين نُضيف مُعاملات أخرى. التفكيك لنا بالمرصاد… أعني للعون! فيمكننا تمرير المُعاملات بصيغة كائن، وستُفكّكها الدالة حالًا في متغيرات: // نمرّر كائنًا إلى الدالة let options = { title: "My menu", items: ["Item1", "Item2"] }; // ‫...ومباشرة تفكّها وتضعها في متغيرات function showMenu({title = "Untitled", width = 200, height = 100, items = []}) { // ‫title, items – هذه من options // ‫width, height – نستعمل القيم المبدئية alert( `${title} ${width} ${height}` ); // My Menu 200 100 alert( items ); // Item1, Item2 } showMenu(options); يمكننا أيضًا استعمال التفكيك الأكثر تعقيدًا (مع الكائنات المتداخلة وتغيير الأسماء بالنقطتين الرأسيتين): let options = { title: "My menu", items: ["Item1", "Item2"] }; function showMenu({ title = "Untitled", width: w = 100, // نضع ‫width في w height: h = 200, // ‫ونضع height في h items: [item1, item2] // ‫أوّل عنصر في items يصير item1، وثاني عنصر يصير item2 }) { alert( `${title} ${w} ${h}` ); // My Menu 100 200 alert( item1 ); // Item1 alert( item2 ); // Item2 } showMenu(options); صياغة الدالة الكاملة تتطابق مع صياغة الإسناد بالتفكيك: function({ incomingProperty: varName = defaultValue ... }) وحينها متى ما تمرّر كائن على أساس أنّه مُعامل، نضع الخاصية incomingProperty في المتغير varName وقيمته المبدئية هي defaultValue. لاحظ بأنّ هذا النوع من التفكيك ينتظر مُعاملًا واحدًا على الأقل في الدالة showMenu()‎. لو أردنا أن تكون كلّ القيم كما هي مبدئيًا، فعلينا تقديم كائن فارغ: showMenu({}); // هكذا، كل القيم كما هي مبدئيًا showMenu(); // هذا سيصرخ علينا بخطأ يمكننا إصلاح هذه المشكلة بتحديد {} قيمةً مبدئيةً لكامل الكائن الذي يحوي المُعاملات: function showMenu({ title = "Menu", width = 100, height = 200 }*!* = {}*/!*) { alert( `${title} ${width} ${height}` ); } showMenu(); // Menu 100 200 ملخص يتيح الإسناد بالتفكيك ربط الكائن أو المصفوفة مع متغيرات عديدة أخرى، وآنيًا. صياغة الكائن الكاملة هي: let {prop : varName = default, ...rest} = object ويعني هذا بأنّ الخاصية prop تصير في المتغيّر varName، وفي حال لم توجد هذه الخاصية فستُستعمل القيمة المبدئية default. تُنسح حاصيات الكائنات التي لا ترتبط إلى الكائن rest. صياغة المصفوفة الكاملة هي: let [item1 = default, item2, ...rest] = array يصير أوّل عنصر في item1 وثاني عنصر في item2 وباقي المصفوفة يصير باقيًا في rest. يمكن أيضًا استخراج البيانات من المصفوفات/الكائنات المتداخلة، ويلزم أن تتطابق بنية على يسار الإسناد تلك على يمينه. تمارين الإسناد بالتفكيك الأهمية: 5 لدينا هذا الكائن: let user = { name: "John", years: 30 }; اكتب إسنادًا بالتفكيك يقرأ: خاصية name ويضعها في المتغير name. خاصية years ويضعها في المتغير age. خاصية isAdmin ويضعها في المتغير isAdmin (تكون false لو لم تكن موجودة) إليك مثالًا بالقيم بعد إجراء الإسناد: let user = { name: "John", years: 30 }; // ض‫ع شيفرتك على الجانب الأيسر: // ... = user alert( name ); // John alert( age ); // 30 alert( isAdmin ); // false الحل let user = { name: "John", years: 30 }; let {name, years: age, isAdmin = false} = user; alert( name ); // John alert( age ); // 30 alert( isAdmin ); // false أكبر راتب الأهمية: 5 إليك كائن الرواتب salaries: let salaries = { "John": 100, "Pete": 300, "Mary": 250 }; اكتب دالة topSalary(salaries) تُعيد اسم الشخص الأكثر ثراءً وراتبًا. لو كان salaries فارغًا فيجب أن تُعيد null. لو كان هناك أكثر من شخص متساوي الراتب، فتُعيد أيًا منهم. ملاحظة: استعمل Object.entries والإسناد بالتفكيك للمرور على أزواج ”مفاتيح/قيم“. الحل function topSalary(salaries) { let max = 0; let maxName = null; for(const [name, salary] of Object.entries(salaries)) { if (max < salary) { max = salary; maxName = name; } } return maxName; } ترجمة -وبتصرف- للفصل Destructuring assignment من كتاب The JavaScript language .task__importance { color: #999; margin-left: 30px; } .task__answer { border: 3px solid #f7f6ea; margin: 20px 0 14px; position: relative; display: block; padding: 25px 30px; } اقرأ أيضًا المقال التالي: النوع Date: تمثيل التاريخ والوقت المقال السابق: مفاتيح الكائنات وقيمها ومدخلاتها
  4. لنأخذ راحة صغيرة بعيدًا عن بنى البيانات ولنتحدّث عن طريقة المرور على عناصرها. رأينا في الفصل السابق التوابِع map.keys()‎ وmap.values()‎ وmap.entries()‎. هذه التوابِع عامّة وقد اتّفق معشر المطوّرين على استعمالها عند التعامل مع بنى البيانات. ولو أنشأنا بنية بيانات من الصفر بيدنا، فعلينا توفير "تنفيذ" تلك التوابِع أيضًا. هي أساسًا مدعومة لكلّ من: الخرائط Map الأطقم Set المصفوفات Array كما وتدعم الكائنات العادية توابِع كتلك التوابِع باختلاف بسيط في صياغتها. التوابِع keys وvalues وentries هذه هي التوابِع المتاحة للتعامل مع الكائنات العادية: Object.keys(obj)‎ -- يُعيد مصفوفة من المفاتيح. Object.values(obj)‎ -- يُعيد مصفوفة من القيم. Object.entries(obj)‎ -- يُعيد مصفوفة من أزواج [key, value]. لاحظ رجاءً الفروق بينها وبين الخارطة مثلًا: الخارطة الكائن صياغة الاستدعاء map.keys() Object.keys(obj)‎ لكن ليس obj.keys()‎ قيمة الإعادة مُكرَّر مصفوفة ”حقيقية“ أوّل فرق واضح جليّ: علينا استدعاء Object.keys(obj)‎ لا obj.keys()‎. ولكن لماذا؟ السبب الأساس هو مرونة الاستعمال. لا تنسَ بأنّ الكائنات هي أساس كلّ بنية بيانات معقّدة في جافاسكربت. يحدث بأنّ لدينا كائن طوّرناه ليحمل بيانات data محدّدة، وفيه التابِع data.values()‎، ولكنّا نريد أيضًا استدعاء Object.values(data)‎ عليه. الفرق الثاني هو أنّ التوابِع Object.* تُعيد كائنات مصفوفات ”فعلية“ لا مُتعدَّدات فقط. يعزو ذلك لأسباب تاريخية بحتة. خُذ هذا المثال: let user = { name: "John", age: 30 }; Object.keys(user) = ["name", "age"]‎ Object.values(user) = ["John", 30]‎ Object.entries(user) = [ ["name","John"], ["age",30] ]‎ وهذا مثال آخر عن كيف نستعمل Object.values للمرور على قيم الخاصيات: let user = { name: "John", age: 30 }; // نمرّ على القيم for (let value of Object.values(user)) { alert(value); // ‫John ثمّ 30 } تتجاهل هذه التوابِع الخاصيات الرمزية كما تتجاهل حلقة for..in الخاصيات التي تستعمل Symbol(...)‎ مفاتيح لها، فهذه التوابِع أعلاه تتجاهلها أيضًا غالبًا يكون هذا ما نريد، ولكن لو أردت المفاتيح الرمزية أيضًا، فعليك استعمال التابِع المنفصل Object.getOwnPropertySymbols إذ يُعيد مصفوفة بالمفاتيح الرمزية فقط. هناك أيضًا التابِع Reflect.ownKeys(obj) إذ يُعيد المفاتيح كلها. تعديل محتوى الكائنات ليس للكائنات تلك التوابِع المفيدة المُتاحة للعناصر (مثل map وfilter وغيرها). لو أردنا تطبيق هذه التوابِع على الكائنات فيجب أوّلًا استعمال Object.entries وبعدها Object.fromEntries: استعمل Object.entries(obj)‎ لتأخذ مصفوفة لها أزواج ”مفتاح/قيمة“ من الكائن obj. استعمل توابِع المصفوفات على تلك المصفوفة (مثلًا map). استعمل Object.fromEntries(array)‎ على المصفوفة الناتج لتُحوّلها ثانيةً إلى كائن. إليك مثالًا لدينا كائنًا فيه تسعير البضائع، ونريد مضاعفتها (إذ ارتفع الدولار): let prices = { banana: 1, orange: 2, meat: 4, }; let doublePrices = Object.fromEntries( // ‫نحوّله إلى مصفوفة، ثمّ نستعمل الطقم، ثمّ يُعيد إلينا fromEntries الكائن المطلوب Object.entries(prices).map(([key, value]) => [key, value * 2]) ); alert(doublePrices.meat); // 8 ربّما تراه صعبًا أوّل وهلة، ولكن لا تقلق فسيصير أسهل أكثر متى ما بدأت استعمالها مرّة واثنتان وثلاث. يمكن أن نصنع سلسلة فعّالة من التعديلات بهذه الطريقة: تمارين مجموع الخاصيات الأهمية: 5 أمامك كائن salaries وفيه بعض الرواتب. اكتب دالة sumSalaries(salaries) تُعيد مجموع كلّ الرواتب، باستعمال Object.values وحلقة for..of. لو كان الكائن فارغًا فيجب أن يكون الناتج صفرًا 0. مثال: let salaries = { "John": 100, "Pete": 300, "Mary": 250 }; alert( sumSalaries(salaries) ); // 650 الحل function sumSalaries(salaries) { let sum = 0; for (let salary of Object.values(salaries)) { sum += salary; } return sum; // 650 } let salaries = { "John": 100, "Pete": 300, "Mary": 250 }; alert( sumSalaries(salaries) ); // 650 أو يمكننا (لو أردنا) معرفة المجموع باستعمال Object.values والتابِع reduce: // ‫يمرّ reduce على مصفوفة من الرواتب، // ويجمعها مع بعضها ويُعيد الناتج function sumSalaries(salaries) { return Object.values(salaries).reduce((a, b) => a + b, 0) // 650 } عدد الخاصيات الأهمية: 5 اكتب دالة باسم count(obj)‎ تُعيد عدد الخاصيات داخل الكائن: let user = { name: 'John', age: 30 }; alert( count(user) ); // 2 حاوِل أن تكون الشيفرة بأصغر ما أمكن. ملاحظة: أهمِل الخاصيات الرمزية وعُدّ فقط تلك ”العادية“. الحل function count(obj) { return Object.keys(obj).length; } ترجمة -وبتصرف- للفصل Object.keys, values, entries من كتاب The JavaScript language table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } .task__importance { color: #999; margin-left: 30px; } .task__answer { border: 3px solid #f7f6ea; margin: 20px 0 14px; position: relative; display: block; padding: 25px 30px; } اقرأ أيضًا المقال التالي: الإسناد بالتفكيك (Destructuring assignment) المقال السابق: النوع WeakMap والنوع WeakSet: الخرائط والأطقم ضعيفة الإشارة
  5. كما عرفنا من فصل «كنس المهملات»، فمُحرّك جافاسكربت يخُزّن القيمة في الذاكرة طالما يمكن أن يصل لها شيء (أي يمكن استعمالها لاحقًا). هكذا: let john = { name: "John" }; // ‫يمكننا الوصول إلى الكائن، فـ john هو الإشارة إليه // عوّض تلك الإِشارة john = null; // سيُزال الكائن من الذاكرة عادةً ما تكون خاصيات الكائن أو عناصر المصفوفة أو أية بنية بيانات أخرى - عادةً ما تُعدّ "مُتاحة لباقي الشيفرة" ويُبقيها المحرّك في الذاكرة طالما بنية البيانات نفسها في الذاكرة. لنفترض أنّا وضعنا كائنًا في مصفوفة، طالما المصفوفة موجودة ومُشار إليها، فسيكون الكائن موجودًا هو الآخر حتّى لو لم يكن هناك ما يُشير إليه. مثلما في هذه الشيفرة: let john = { name: "John" }; let array = [ john ]; john = null; // عوّض الإشارة // ‫الكائن john مخزّن داخل مصفوفة ولن يُكنس باعتباره مهملات // ‫إذ يمكننا أخذه بهذه: array[0]‎ وبنفس المفهوم، لو استعملنا كائنًا ليكون مفتاحًا في خارطة Map عادية، فسيبقى هذا الكائن موجدًا طالما الخارطة تلك موجودة، ويشغل الذاكرة مانعًا عملية كنس المهملات من تحريرها. إليك هذا المثال: let john = { name: "John" }; let map = new Map(); map.set(john, "..."); john = null; // عوّض الإشارة // ‫الكائن john مخزّن داخل خارطة // ‫ويمكننا أخذه بهذه: map.keys()‎ على العكس فالخارطة ضعيفة الإشارة WeakMap مختلفة جذريًا عن هذا، فلا تمنع كنس مهملات أيٍّ من مفاتيحها الكائنات. لنأخذ بعض الأمثلة لتُدرك القصد هنا. WeakMap أولى اختلافات الخارطة ضعيفة الإشارة WeakMap عن تلك العادية Map هي أنّها تُلزم مفاتيحها بأن تكون كائنات لا أنواع أولية: let weakMap = new WeakMap(); let obj = {}; weakMap.set(obj, "ok"); // لا مشاكل (المفتاح كائن) // لا يمكن استعمال السلسلة النصية مفتاحًا weakMap.set("test", "Whoops"); // ‫خطأ، لأنّ ”test“ ليس كائنًا بعد ذلك لو استعملنا أحد الكائنات ليكون مفتاحًا فيها، ولم يكن هناك ما يُشير إلى هذا الكائن، فسيُزال الكائن من الذاكرة (والخارطة) تلقائيا. let john = { name: "John" }; let weakMap = new WeakMap(); weakMap.set(john, "..."); john = null; // عوّض الإشارة // ‫أُزيل الكائن john من الذاكرة! وازِن هذه الشيفرة بشيفرة الخارطة Map أعلاه. الآن حتى لو لم يكن john موجودًا إلا مفتاحًا لِـ WeakMap، فسيُحذف تلقائيًا من الخارطة (ومن الذاكرة). لا تدعم الخارطة ضعيفة الإشارة WeakMap التكرار (iteration) ولا التوابِع keys()‎ أو values()‎ أو entries()‎، ولهذا لا نقدر على أخذ كلّ المفاتيح أو القيم التي فيها. بل أنّ للخارطة WeakMap التوابِع الآتية: weakMap.get(key)‎ weakMap.set(key, value)‎ weakMap.delete(key)‎ weakMap.has(key)‎ تفكّر بسبب وجود هذا التقييد؟ الجواب هو: أسباب تقنية. عُدّ الكائن الآن قد فقد كلّ إشارة له (مثلما حصل مع الكائن john في الشيفرة أعلاه)، بهذا ستُكنس مهملاته تلقائيًا، ولكن… وقت حدوث هذا الكنس غير موضّح تقنيًا. الواقع أنّ محرّك جافاسكربت يُحدّد ذلك: هو يُحدّد متى يمسح الذاكرة، الآن حالًا أو بعد قليل حتّى تحدث عمليات حذف أخرى. لذا فعدد العناصر الحالي داخل WeakMap غير معلوم تقنيًا، ربما يكون المحرّك حذفها كلها أو لم يحذفها، أو حذف بعضها، لا نعلم. لهذا السبب لا تدعم اللغة التوابِع التي تحاول الوصول إلى كلّ القيم والعناصر. الآن بعدما عرفناها، في أيّ حالات نستعمل هذه البنية من البيانات؟ استعمالاتها: بيانات إضافية المجال الرئيسي لتطبيقات WeakMap هي تخزين البيانات الإضافية. لو كنّا نتعامل مع كائن ”ينتمي“ إلى شيفرة أخرى (وحتّى مكتبة من طرف ثالث) وأردنا تخزين بيانات معيّنة لترتبط بها، وهذه البيانات لا تكون موجودة إلا لو كان الكائن موجودًا، فَـ WeakMap هي ما نريد تمامًا: نضع البيانات في خارطة بإشارة ضعيفة WeakMap (مستعملين الكائن مفتاحًا لها). متى ما كُنس الكائن باعتباره مهملات، ستختفي تلك البيانات معه أيضًا. weakMap.set(john, "secret documents"); // ‫إن مات john فستُدمّر تلك المستندات فائقة السرية تلقائيًا لنرى مثالًا يوضّح الصورة. عُدّ بأنّ لدينا شيفرة تسجّل عدد زيارات المستخدمين - تسجّلها في خارطة، حيث كائن المستخدم هو مفتاحها وعدد زياراته هي القيمة. لا نريد أن نُسجّل عدد زياراته فيما لو غادر المستخدم (أي أنّ عملية كنس المهملات كنست ذاك الكائن). إليك مثالًا آخر عن دالة عَدّ باستعمال Map: // ? visitsCount.js let visitsCountMap = new Map(); // خارطة: المستخدم => عدد زياراته // تزيد عدد الزيارات function countUser(user) { let count = visitsCountMap.get(user) || 0; visitsCountMap.set(user, count + 1); } وهذا الجزء الثاني من الشيفرة (يمكن أن يستعمل هذا الملف ذاك): // ? main.js let john = { name: "John" }; countUser(john); // عُدّ الزوّار countUser(john); // ‫بعدها يغادر john الحفلة john = null; هكذا ”يُفترض“ أن يُكنس الكائن john باعتباره مهملات، لكنّه سيبقى في الذاكرة إذ تستعمله الخارطة visitsCountMap مفتاحًا فيها. علينا مسح visitsCountMap حين نُزيل المستخدمين وإلا فسيزيد حجمها في الذاكرة إلى آباد الآبدين. لو كانت بنية البرمجية معقّدة، فستكون عملية المسح هذه مرهقة جدًا وغير عملية. لهذا يمكننا تجنّب التعب واستعمال WeakMap بدل العادية: // ? visitsCount.js let visitsCountMap = new WeakMap(); // خارطة بإشارة ضعيفة: المستخدم => عدد زياراته // تزيد عدد الزيارات function countUser(user) { let count = visitsCountMap.get(user) || 0; visitsCountMap.set(user, count + 1); } هكذا لا نمسح visitsCountMap يدويًا بل نترك للمحرّك القرار: لو لم يكن هناك ما يُشير إلى الكائن john عدا مفتاح WeakMap، سيحرّره من الذاكرة مع المعلومات التي في ذلك المفتاح داخل الخارطة ضعيفة الإشارة WeakMap. استعمالاتها: الخبيئة يكثُر أيضًا استعمال الخرائط للخبيئة، أي حين علينا تذكّر ناتج الدالة (تخبئته ”cached“) كي يستعمل أيّ استدعاء لاحِق على هذا العنصر تلك الخبيئة. يمكن أن نستعمل الخارطة Map لتخزين النتائج: // ? cache.js let cache = new Map(); // نحسب النتيجة ونتذكرها function process(obj) { if (!cache.has(obj)) { let result = /* حسابات الكائن هذا */ obj; cache.set(obj, result); } return cache.get(obj); } // الآن نستعمل ‫process()‎ في ملف آخر: // ? main.js let obj = {/* فلنفترض وجود هذا الكائن*/}; let result1 = process(obj); // حسبنا القيمة // ‫...بعدها، في مكان آخر من الشيفرة... let result2 = process(obj); // تُأخذ النتيجة تلك من الخبيئة // ‫...بعدها، لو لم نرد الكائن بعد الآن: obj = null; alert(cache.size); // 1 (لاا! ما زال الكائن في الخبيئة ويستهلك الذاكرة) لو استدعينا process(obj)‎ أكثر من مرّة بتمرير نفس الكائن، فستحسب الشيفرة النتيجة أوّل مرة فقط، وفي المرات القادمة تأخذها من الكائن cache. مشكلة هذه الطريقة هي ضرورة مسح cache متى ما انتفت حاجتنا من الكائن. لكن، لو استبدلنا Map وعوّضناها بِـ WeakMap فستختفي المشكلة تمامًا، وتُزال النتيجة المُخبّأة من الذاكرة تلقائيًا متى ما كُنس الكائن على أنّه مهملات. // ? cache.js *!* let cache = new WeakMap(); */!* // نحسب النتيجة ونتذكرها function process(obj) { if (!cache.has(obj)) { let result = /* حسابات الكائن هذا */ obj; cache.set(obj, result); } return cache.get(obj); } // ? main.js let obj = {/* كائن من الكائنات */}; let result1 = process(obj); let result2 = process(obj); // ‫...بعدها، لو لم نرد الكائن بعد الآن: obj = null; هنا، ‫لا يمكن أن نعرف cache.size إذ أنها خارطة بإشارة ضعيفة، ولكن الحجم صفر، أو سيكون صفر قريبًا؛ فما أن تبدأ عملية كنس المهملات على الكائن، ستُزال البيانات المُخبّأة هي الأخرى. WeakSet حتّى الأطقم ضعيفة الإشارة WeakSet تسلك ذات السلوك: تشبه الأطقم العادية Set ولكن لا يمكننا إلّا إضافة الكائنات إلى WeakSet (وليس الأنواع الأولية). يبقى الكائن موجودًا في الطقم طالما هناك ما يصل إليه. ويدعم -كما تدعم Set- التوابِع add وhas وdelete، ولكن لا تدعم size أو keys()‎ أو التعداد. هي الأخرى تخدمنا نحن المطورون في تخزين البيانات الإضافية (إذ أنّ الإشارة إليها ”ضعيفة“)، ولكنها ليست لأيّ بيانات كانت، بل فقط التي تُعطي إجابة ”نعم/لا“. لو كان الكائن موجودًا داخل طقم بإشارة ضعيفة، فلا بدّ أنّه موجود لداعٍ. يمكننا مثلًا إضافة المستخدمين إلى طقم بإشارة ضعيفة WeakSet لنسجّل من زار موقعنا: let visitedSet = new WeakSet(); let john = { name: "John" }; let pete = { name: "Pete" }; let mary = { name: "Mary" }; visitedSet.add(john); // زارنا ‫John visitedSet.add(pete); // وبعده ‫Pete visitedSet.add(john); // وعاد ‫John // ت‫حتوي visitedSet الآن على مستخدمين اثنين // ه‫ل زارنا John؟ alert(visitedSet.has(john)); // true // ‫هل زارتنا Mary؟ alert(visitedSet.has(mary)); // false john = null; // ستُنظّف ‫visitedSet تلقائيًا التقييد الأهم في هذه الأنواع WeakSet وWeakMap هي عدم موجود المُكرَّرات واستحالة أخذ محتواها كله. لربّما ترى ذلك غباءً، إلّا أنّه لا يمنع هذه الأنواع من إجراء مهامها التي صُنعت لها: مخزن "إضافي" من البيانات للكائنات المخزّنة (أو المُدارة) في مكان آخر. خلاصة الخارطة ضعيفة الإشارة هي تجميعة تشبه الخرائط العادية، ولا تتيح إلا استعمال الكائنات مفاتيحٍ فيها، كما وتُزيلها هي والقيمة المرتبطة بها ما إن تنعدم الإشارة إليها. الطقم ضعيفة الإشارة هي تجميعة تشبه الأطقم العادية، ولا تخزّن إلا الكائنات فيها، كما وتُزيلها ما إن تنعدم الإشارة إليها. كِلا النوعان لا يدعمان التوابِع والخاصيات التي تُشير إلى كل المفاتيح فيهما، أو حتى عددها. المسموح فقط هو العمليات على العناصر فيها عنصرًا بعنصر. يُستعمل هذان النوعان WeakMap وWeakSet على أنّهما بنى بيانات ”ثانوية“ إلى جانب تلك ”الأساسية“ لتخزين العناصر. لو أُزيل الكائن من التخزين الأساسي، ولم يوجد له أي إشارة إلا مفتاحًا في WeakMap أو عنصرًا في WeakSet، مسحهُ المحرّك تلقائيًا. تمارين تخزين رايات ”غير مقروءة“ الأهمية: 5 لديك مصفوفة من الرسائل: let messages = [ {text: "Hello", from: "John"}, {text: "How goes?", from: "John"}, {text: "See you soon", from: "Alice"} ]; ويمكن للشيفرة عندك الوصول إليها، إلّا أنّ شيفرة أحدهم تُدير تلك الرسائل، فتُضيف رسائل جديدة وتُزيل قديمة، ولا تعرف متى يحدث هذا بالضبط. السؤال هو: أيّ بنية من بنى البيانات تستعمل لتخزّن هذه المعلومة لكلّ رسالة: ”هل قُرأت؟“. يجب أن تكون البنية التي اخترتها مناسبة لتردّ على سؤال ”هل قُرأت؟“ لكلّ كائن رسالة. ملاحظة: حين تُزال رسالة من مصفوفة messages، يجب أن تختفي من بنية البيانات لديك هي الأخرى. ملاحظة أخرى: يجب ألّا نُعدّل كائنات الرسائل ولا نُضيف خاصيات من عندنا إليها؛ فيمكن أن يؤدّي هذا إلى عواقب وخيمة إذ لسنا من نديرها بل أحد آخر. الحل لنجرّب تخزين الرسائل المقروءة في طقم بإشارة ضعيفة WeakSet: let messages = [ {text: "Hello", from: "John"}, {text: "How goes?", from: "John"}, {text: "See you soon", from: "Alice"} ]; let readMessages = new WeakSet(); // قرأ المستخدم رسالتين اثنتين readMessages.add(messages[0]); readMessages.add(messages[1]); // في ‫readMessages الآن عنصرين // ‫...هيًا نُعيد قراءة أول رسالة! readMessages.add(messages[0]); // ما زالت في ‫readMessages عنصرين فريدين // ‫الجواب: هل قُرئتmessage [0]‎؟ alert("Read message 0: " + readMessages.has(messages[0])); // نعم ‫true messages.shift(); // الآن في ‫readMessages عنصر واحد (تقنيًا فستُنظّف الذاكرة فيما بعد) يتيح لنا الطقم ضعيفة الإشارة تخزينَ مجموعة من الرسائل والتأكّد من وجود كلّ منها بسهولة تامة. كما وأنّها تمسح نفسها بنفسها. للأسف بهذا نُضحّي بميزة التكرار، فلا يمكن أن نجلب ”كلّ الرسائل المقروءة“ منها مباشرةً، ولكن… يمكننا المرور على عناصر كل الرسائل في messages وترشيح تلك التي في الطقم لدينا. يمكن أن يكون الحل الآخر هو إضافة خاصية مثل message.isRead=true إلى الرسالة بعد قراءتها. ولكننّا لسنا من نُدير هذه الكائنات بل أحد آخر، ولهذا لا يُوصى بذلك بصفة عامة. ولكن، يمكننا استعمال خاصيّة رمزية فنتجنّب أي مشكلة أو تعارض. هكذا: // الخاصية الرمزية معروفة في الشيفرة لدينا، فقط let isRead = Symbol("isRead"); messages[0][isRead] = true; "لربما" الآن لن تعرف شيفرة الطرف الثالث بخاصيتنا الجديدة. صحيح أن الرموز تتيح لنا تقليل احتمال حدوث المشاكل، إلّا أنّ استعمال WeakSet أفضل بعين بنية البرمجية. تخزين تواريخ القراءة الأهمية: 5 لديك مصفوفة من الرسائل تشبه تلك في التمرين السابق، والفكرة هنا متشابهة قليلًا. let messages = [ {text: "Hello", from: "John"}, {text: "How goes?", from: "John"}, {text: "See you soon", from: "Alice"} ]; السؤال: أيّ بنية بيانات تستعمل لتخزين هذه المعلومة: " متى قُرئت هذه الرسالة؟". كان عليك (في التمرين السابق) تخزين معلومة "نعم/لا" فقط، أمّا الآن فعليك تخزين التاريخ، ويجب أن يبقى في الذاكرة إلى أن تُكنس الرسالة على أنّها مهملات. ملاحظة: تُخزّن التواريخ كائنات بصنف Date المضمّن في اللغة، وسنتكلم عنه لاحقًا. الحل يمكن أن نستعمل الخارطة ضعيفة الإشارة لتخزين التاريخ: let messages = [ {text: "Hello", from: "John"}, {text: "How goes?", from: "John"}, {text: "See you soon", from: "Alice"} ]; let readMap = new WeakMap(); readMap.set(messages[0], new Date(2017, 1, 1)); // سنرى أمر كائن التاريخ لاحقًا ترجمة -وبتصرف- للفصل WeakMap and WeakSet من كتاب The JavaScript language .task__importance { color: #999; margin-left: 30px; } .task__answer { border: 3px solid #f7f6ea; margin: 20px 0 14px; position: relative; display: block; padding: 25px 30px; } اقرأ أيضًا المقال التالي: مفاتيح الكائنات وقيمها ومدخلاتها المقال السابق: النوع Map (الخرائط) والنوع Set (الأطقم)
  6. تعلّمنا حتّى الآن بنى البيانات المعقّدة هذه: الكائنات Object: لتخزين التجميعات ذات المفاتيح. المصفوفات Array: لتخزين التجميعات المرتّبة. ولكن في الحياة الواقعية، هذا لا يكفي. ولهذا تقدّم لك اللغة نوعيين آخرين: الخارطة Map والطقم Set. الخارطة Map تُعدّ الخارطة تجميعة ذات مفاتيح من عناصر البيانات، تمامًا مثل الكائنات Object، مع فرق بسيط، هو أنّ الخارطة Map تتيح استعمال المفاتيح مهمًا كان نوعها. هذه توابِعها وخاصياتها: new Map()‎ -- يُنشِئ خارطة. map.set(key, value)‎ -- يضبط القيمة حسب مفتاحها. map.get(key)‎ -- يجلب القيمة حسب مفتاحها، وundefined لو لم يوجد key في الخارطة. map.has(key)‎ -- يُعيد true لو كان key موجودًا، وإلا فَـ false. map.delete(key)‎ -- يُزيل القيمة حسب مفتاحها. map.clear()‎ -- يُزيل كل شيء من الخارطة. map.size -- يُعيد عدد العناصر الحالي. إليك المثال الآتي: let map = new Map(); map.set('1', 'str1'); // المفتاح سلسلة نصية map.set(1, 'num1'); // المفتاح عدد map.set(true, 'bool1'); // المفتاح قيمة منطقية // أتذكر كيف أنّ الكائن العادي يُحوّل المفاتيح لأي سلاسل نصية؟ // ‫الخارطة هنا تحترم النوع، وهذان السطران مختلفان: alert( map.get(1) ); // 'num1' alert( map.get('1') ); // 'str1' alert( map.size ); // 3 كما ترى، فالمفاتيح لا تُحوّل إلى سلاسل نصية (على العكس من الكائنات). يمكنك أن تضع أيّ نوع من المفاتيح تريد. يمكن أن تستعمل الخارطة الكائناتَ نفسها مفاتيح. مثال: let john = { name: "John" }; // لنخزّن عدد زيارات كل زائر لنا let visitsCountMap = new Map(); // ‫كائن john هو مفتاح الخارطة visitsCountMap.set(john, 123); alert( visitsCountMap.get(john) ); // 123 يُعدّ استعمال الكائنات على أنّها مفاتيح أحدُ أهمّ صفات Map. لو أردت المفاتيح سلاسل نصية، فالكائنات Object تكفيك وزيادة، لكن لو أردت المفاتيح كائنات، فسيخونك Object للأسف. لنرى: let john = { name: "John" }; let visitsCountObj = {}; // نحاول استعمال كائن visitsCountObj[john] = 123; // ‫ونحاول استعمال كائن john مفتاحًا فيه // ‫وهذا ما وجدناه مكتوبًا! alert( visitsCountObj["[object Object]"] ); // 123 المتغيّر visitsCountObj من نوع ”كائن“، ولهذا يحوّل كلّ المفاتيح (مثل john) إلى سلاسل نصية. وبهذا قدّم لنا المفتاح بالسلسلة النصية "[object Object]". ليس ما نريد قطعًا. كيف تُوازن الخارطة Map المفاتيح تستعمل Map الخوارزمية SameValueZero لتختبر تساوي المفتاح مع الآخر. تتشابه هذه الخوارزمية تقريبًا مع المساواة الصارمة === بفارق أنّ NaN تساوي NaN في نظرها. يعني ذلك بأنك تستطيع استعمال NaN كمفتاح هو الآخر. لا يمكن تغيير هذه الخوارزمية ولا تخصيصها. سِلسلة الاستدعاءات كلّما نادينا map.set أعاد لنا التابِع الخارطة نفسها، وبهذا يمكن أن نستدعي التابع على ناتج الاستدعاء السابق: map.set('1', 'str1') .set(1, 'num1') .set(true, 'bool1'); المرور على خارطة هناك ثلاث طرائق للمرور على عناصر Map وتطبيق عملية عليها: map.keys()‎ -- يُعيد مُتعدَّدًا للمفاتيح، map.values()‎ -- يُعيد مُتعدَّدًا للقيم، map.entries()‎ -- يُعيد مُتعدَّدًا للمدخلات [key, value]، وهي التي تستعملها for..of مبدئيًا. مثال: let recipeMap = new Map([ ['cucumber', 500], ['tomatoes', 350], ['onion', 50] ]); // نمرّ على المفاتيح (الخضراوات)‏ for (let vegetable of recipeMap.keys()) { alert(vegetable); // cucumber, tomatoes, onion } // نمرّ على قيم المفاتيح (عدد الخضراوات)‏ for (let amount of recipeMap.values()) { alert(amount); // 500, 350, 50 } // ‫نمرّ على مدخلات [key, value] for (let entry of recipeMap) { // ‫مثل recipeMap.entries()‎ alert(entry); // ‫cucumber,500 (وهكذا) } ترتيب الإدخال هو المستعمل يسير التعداد بنفس الترتيب الذي أُدخلت به الكائنات، فالخارطة تحفظ هذا الترتيب على العكس من الكائنات Object. علاوةً على ذلك، فتملك الخارطة Map التابِع المضمّن فيها forEach، كما المصفوفات Array: // ‫تُنفّذ الدالة على كلّ زوج (key, value) recipeMap.forEach( (value, key, map) => { alert(`${key}: ${value}`); // ‫cucumber: 500 إلخ إلخ }); Object.entries: صنع خارطة من كائن متى ما أُنشأت خارطة Map نستطيع تمرير مصفوفة (أو مُتعدَّدًا آخرًا) لها أزواج ”مفاتيح/قيم“ لتهيئتها، هكذا تمامًا: // ‫مصفوفة من أزواج [key, value] let map = new Map([ ['1', 'str1'], [1, 'num1'], [true, 'bool1'] ]); alert( map.get('1') ); // str1 لو كان أمامنا كائنًا عاديًا ونريد صناعة Map منه، فيمكننا استعمال التابِع المضمّن في اللغة Object.entries(obj) إذ يُعيد مصفوفة مكوّنة من أزواج ”مفاتيح/قيم“ للكائن، بنفس الصيغة التي يطلبها ذاك التابِع. ولهذا يمكن أن نصنع خارطة من كائن بهذه الطريقة: let obj = { name: "John", age: 30 }; let map = new Map(Object.entries(obj)); alert( map.get('name') ); // John نرى هنا التابِع Object.entries يُعيد مصفوفة بأزواج ”مفاتيح/قيم“: [ ["name","John"], ["age", 30] ]، وهذا ما تحتاجه الخارطة. Object.fromEntries: صنع كائن من خارطة رأينا كيف نصنع خارطة Map من كائنٍ عاديّ باستعمال Object.entries(obj)‎. على العكس منه فالتابع Object.fromEntries يأخذ خارطة فيها أزواج [key, value] ويصنع كائنًا منها: let prices = Object.fromEntries([ ['banana', 1], ['orange', 2], ['meat', 4] ]); // prices = { banana: 1, orange: 2, meat: 4 } alert(prices.orange); // 2 يمكننا استعمال Object.fromEntries لنصنع كائنًا عاديًا من Map. يُفيدنا هذا مثلًا في تخزين البيانات في خارطة، بينما نريد تمريرها إلى شيفرة من طرف ثالثة تريد كائنًا عاديًا لا خارطة. هذه الشيفرة المنشودة: let map = new Map(); map.set('banana', 1); map.set('orange', 2); map.set('meat', 4); let obj = Object.fromEntries(map.entries()); // ن‫صنع كائنًا عاديًا (*) //و‫هكذا انتهينا! // obj = { banana: 1, orange: 2, meat: 4 } alert(obj.orange); // 2 متى ما استدعينا map.entries()‎ أعادت مصفوفة مؤلّفة من أزواج ”مفاتيح/قيم“ بنفس التنسيق الذي يطلبه Object.fromEntries تمامًا، لحسن الحظ. يمكننا تقصير السطر المعلّم (*) ذاك: let obj = Object.fromEntries(map); // ب‫دون ‎.entries()‎ النتيجة نفسها إذ أنّ التابِع Object.fromEntries يتوقّع كائنًا مُتعدَّدًا وسيطًا له، وليس مصفوفة بالضرورة. كما والتعداد القياسي للخارطة يتوقّع ذات أزواج ”مفاتيح/قيم“ التي يتوقّعها map.entries()‎، وهكذا نجد في يدنا كائنًا عاديًا له نفس ”مفاتيح/قيم“ الخارطة map. الطقم Set الأطقم (Set) هي نوع خاصّ من التجميعات ليس له مفاتيح ولا يمكن أن يحوي أكثر من قيمة متطابقة. يمكن عدّها كأطقم المجوهرات والأسنان، حيث لا تتكرّر أي قطعة مرتين. إليك توابِعه الرئيسة: new Set(iterable)‎ -- يصنع الطقم. في حال مرّرت كائن iterable (وهو عادةً مصفوفة)، فينسخ بياناته إلى الطقم. set.add(value)‎ -- يُضيف قيمة إلى الطقم ويُعيده ذاته. set.delete(value)‎ -- يُزيل القيمة ويُعيد true لو كانت القيمة value موجودة عند استدعاء التابِع، وإلّا يُعيد false. set.has(value)‎ -- يُعيد true لو كانت القيمة موجودة في الطقم، وإلّا يُعيد false. set.clear()‎ -- يُزيل كلّ شيء من الطقم. set.size -- خاصية عدد العناصر في الطقم. الميزة الأهمّ للأطقم هي أنّك لو استدعيت set.add(value)‎ أكثر من مرّة وبنفس القيمة، فكأنّك استدعيتهُ مرّة واحدة. لهذا تظهر كل قيمة في الطقم مرّة واحدة لا غير. عُدّ مثلًا أنّ زوّارًا قادمين إلى وليمة ونريد تذكّر كلّ واحد لإعداد ما يكفي من طعام… ولكن يجب ألّا نسجّل الزوّار مرتين، فالزائر ”واحد“ ونعدّه مرّة واحدة فقط. الطقم هنا هو الخيار الأمثل: let set = new Set(); let john = { name: "John" }; let pete = { name: "Pete" }; let mary = { name: "Mary" }; // زارنا الناس، وهناك من زارنا أكثر من مرة set.add(john); set.add(pete); set.add(mary); set.add(john); set.add(mary); // لا يحفظ الطقم إلا القيم الفريدة alert( set.size ); // 3 for (let user of set) { alert(user.name); // ‫John (ثمّ Pete وMary) } يمكن عوض الأطقم استعمال مصفوفة من المستخدمين، مع نصّ يتحقّق من البيانات عند إدخالها لألّا تحدث تكرارات (باستعمال arr.find). هذا ممكن نعم، لكن الأداء سيكون أشنع بكثير فتابِع البحث arr.find يمرّ على كامل المصفوفة فيفحص كلّ عنصر فيها. الطقم Set أفضل بمراحل فأداؤه في فحص تفرّد العناصر مُحسَّن داخل بنية اللغة. المرور على طقم يمكن لنا المرور على عناصر الطقم باستعمال حلقة for..of أو تابِع forEach: let set = new Set(["oranges", "apples", "bananas"]); for (let value of set) alert(value); // ‫نفس الأمر مع forEach: set.forEach((value, valueAgain, set) => { alert(value); }); ولكن لاحظ هنا طرافة التابِع: لدالة ردّ النداء المُمرّرة إلى forEach ثلاث وُسطاء: قيمة value، وذات القيمة الأولى valueAgain، والكائن الهدف. لاحظتَ كيف تكرّرت ذات القيمة في الوُسطاء مرّتين؟ يعزو هذا إلى توافق Set مع Map إذ لدالة ردّ التابع المُمرّرة إلى forEach الخارطة ثلاث وُسطاء أيضًا. معك حق، أمرها غريب، ولكنّها تفيد فتُسهّل حياتنا لو أردنا استبدال الخارطة بالطقم في حالات حرجة، كما العكس أيضًا. كما تدعم الأطقم نفس التوابِع التي تدعمها الخارطة للتعامل مع المُتعدَّدات: set.keys()‎ -- يُعيد كائنًا مُتعدَّدًا من القيم، set.values()‎ -- تمامًا مثل set.keys()‎ (موجود للتوافق مع Map)، set.entries()‎ -- يُعيد كائنًا مُتعدَّدًا من المُدخلات [value, value] (موجود للتوافق مع Map). ملخص الخارطة Map هي تجميعة ذات مفاتيح. توابعها وخاصياتها: new Map([iterable])‎ -- يصنع خريطة ويضع فيها أزواج [key,value] داخل المُتعدَّد iteratable الاختياري (يمكن أن يكون مثلًا مصفوفة). map.set(key, value)‎ -- يخزّن القيمة حسب مفتاحها. map.get(key)‎ -- يُعيد القيمة حسب مفتاحها، ويُعيد undefined لو لم يكن المفتاح key في الخارطة. map.has(key)‎ -- يُعيد true لو كان المفتاح key موجودًا، وإلا يُعيد false. map.delete(key)‎ -- يُزيل القيمة حسب مفتاحها. map.clear()‎ -- يُزيل كل ما في الخارطة. map.size -- يُعيد عدد العناصر في الخارطة الآن. اختلافاتها مع الكائنات العادية (Object): تدعم أنواع المفاتيح المختلفة، كما والكائنات نفسها أيضًا. فيها توابِع أخرى تفيدنا، كما وخاصية size. الطقم Set هو تجميعة من القيم الفريدة. توابعه وخاصياته: new Set([iterable])‎ -- يصنع طقمًا ويضع فيه أزواج [key, value] داخل المُتعدَّد الاختياري (يمكن أن يكون مثلًا مصفوفة). set.add(value)‎ -- يُضيف القيمة value (ولو كانت موجودة لا يفعل شيء) ثمّ يُعيد الطقم نفسه. set.delete(value)‎ -- يُزيل القيمة ويُعيد true لو كانت موجودة عند استدعاء التابِع، وإلا يُعيد false. set.has(value)‎ -- يُعيد true لو كانت القيمة في الطقم، وإلا يُعيد false. set.clear()‎ -- يُزيل كل ما في الطقم. set.size -- عدد عناصر الطقم. يسري ترتيب المرور على عناصر Map وSet بترتيب إدخالها فيهما دومًا، ولهذا لا يمكن أن نقول بأنّها تجميعات غير مرتّبة، بل أنّا لا نقدر على إعادة ترتيب عناصرها أو الحصول عليها بفهرسها فيها. تمارين ترشيح العناصر الفريدة في مصفوفة الأهمية: 5 عُدّ أنّ arr مصفوفة. أنشِئ دالة unique(arr)‎ تُعيد مصفوفة مؤلّفة من العناصر الفريدة في arr. مثال: function unique(arr) { /* هنا تكتب شيفرتك*/ } let values = ["Hare", "Krishna", "Hare", "Krishna", "Krishna", "Krishna", "Hare", "Hare", ":-O" ]; alert( unique(values) ); // Hare, Krishna, :-O لاحظ أنّ السلاسل النصية استُعملت هنا، ولكن يمكن أن تكون القيم بأيّ نوع آخر. غُشّ من هذه: استعمل Set لتخزين القيم الفريدة. الحل function unique(arr) { return Array.from(new Set(arr)); } ترشيح الألفاظ المقلوبة الأهمية: 4 تُسمّى الكلمات التي لها ذات الأحرف ولكن بترتيب مختلف ألفاظًا مقلوبة، مثل هذه: nap - pan ear - are - era cheaters - hectares - teachers أو العربية: ملّ - لمّ مسكين - سيكمن كاتب - اكتب - كتاب اكتب دالة aclean(arr)‎ تُعيد مصفوفةً بدون هذه الألفاظ المقلوبة. هكذا: let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"]; alert( aclean(arr) ); // "nap,teachers,ear" أو "PAN,cheaters,era" يجب أن يكون ناتج كلّ مجموعة ألفاظ كلمة واحدة فقط، ولا يهمّنا أيّ واحدة. الحل لو أردنا البحث عن كل الألفاظ المقلوبة، سنقسم كلّ كلمة إلى حروفها ونرتّبها. متى ما رتّبناها حسب الأحرف، فستكون الألفاظ كلها متطابقة. هكذا: nap, pan -> anp ear, era, are -> aer cheaters, hectares, teachers -> aceehrst ... سنستعمل كلّ قيمة مختلفة (ولكن متطابقة بترتيب أحرفها) لتكون مفاتيح خريطة فنخزّن لفظًا واحدًا لكل مفتاح فقط: function aclean(arr) { let map = new Map(); for (let word of arr) { // نقسم الكلمة بأحرفها، ونرّتب الأحرف ونجمعها ثانيةً let sorted = word.toLowerCase().split('').sort().join(''); // (*) map.set(sorted, word); } return Array.from(map.values()); } let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"]; alert( aclean(arr) ); نُنفّذ الترتيب حسب الأحرف بسلسلة استدعاءات كما في السطر (*). سنقسمها على أكثر من سطر ليسهل فهمها: let sorted = arr[i] // PAN .toLowerCase() // pan .split('') // ['p','a','n'] .sort() // ['a','n','p'] .join(''); // anp هكذا يكون لدى الكلمتين المختلفتين 'PAN' و'nap' ذات الشكل حين تُرتّب أحرفها: 'anp'. في السطر اللاحق نُضيف الكلمة إلى الخارطة. map.set(sorted, word); لو قابلنا بينما نمرّ على الكلمات كلمةً لها نفس الشكل حين تُرتّب أحرفها، فستعوّض القيمة السابقة التي لها نفس المفتاح في الخارطة. هكذا لن تزيد الكلمات لكلّ شكل على واحد، دومًا. وفي النهاية يأخذ Array.from(map.values())‎ متُعدَّدا يمرّ على قيم الخارطة (لا نريد مفاتيحها في ناتج الدالة) فيُعيد المصفوفة نفسها. يمكننا (في هذه المسألة) استعمال كائن عادي بدل الخارطة، إذ أنّ المفاتيح سلاسل نصية. هكذا سيبدو الحلّ لو اتبعنا هذا النهج: function aclean(arr) { let obj = {}; for (let i = 0; i < arr.length; i++) { let sorted = arr[i].toLowerCase().split("").sort().join(""); obj[sorted] = arr[i]; } return Object.values(obj); } let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"]; alert( aclean(arr) ); مفاتيح مُكرَّرة الأهمية: 5 نريد تسجيل المصفوفة الناتجة من map.keys()‎ في متغيّر وثمّ استدعاء توابِع تخصّ المصفوفات عليها مثل ‎.push. ولكنّ ذلك لم ينفع: let map = new Map(); map.set("name", "John"); let keys = map.keys(); // ‫خطأ: keys.push ليست دالة keys.push("more"); لماذا؟ وكيف يمكننا إصلاح الشيفرة ليعمل keys.push؟ الحل لأنّ التابِع map.keys()‎ يُعيد مُتعدَّدًا لا مصفوفة. يمكننا تحويله إلى مصفوفة باستعمال Array.from: let map = new Map(); map.set("name", "John"); let keys = Array.from(map.keys()); keys.push("more"); alert(keys); // name, more ترجمة -وبتصرف- للفصل Map and Set من كتاب The JavaScript language .task__importance { color: #999; margin-left: 30px; } .task__answer { border: 3px solid #f7f6ea; margin: 20px 0 14px; position: relative; display: block; padding: 25px 30px; } اقرأ أيضًا المقال التالي: النوع WeakMap والنوع WeakSet: الخرائط والأطقم ضعيفة الإشارة المقال السابق: الكائنات المكرَّرة (Iterables)
  7. الكائنات المُكرَّرة (Iterables) هي مفهوم أعمّ من المصفوفات. تتيح لنا هذه الكائنات تحويل أيّ كائن إلى ”كائن يمكن تكراره“ فيمكننا استعماله في حلقة for..of. بالطبع فالمصفوفات يمكن تكرارها، ولكن هناك كائنات أخرى (مضمّنة في أصل اللغة) يمكن تكرارها أيضًا، مثل السلاسل النصية. لو لم يكن الكائن مصفوفة تقنيًا، ولكن يمكننا تمثيله على أنّه تجميعة من العناصر (النوع list، والنوع set)، فصياغة for..of ممتازة لنمرّ على عناصره. لذا دعنا نرى كيف يمكن توظيف هذه ”المُكرَّرات“. Symbol.iterator يمكن لنا أن نُدرك هذا المفهوم -مفهوم المُكرَّرات أعني- بأن نصنع واحدًا بنفسنا. لنقل أنّ لدينا كائن وهو ليس بمصفوفة بأيّ شكل، ولكن يمكن توظيفه لحلقة for..of. مثلًا كائن المدى هذا range يُمثّل مجموعة متتالية من الأعداد. let range = { from: 1, to: 5 }; // ‫‏نُريد أن تعمل for..of هكذا: // for(let num of range) ... num=1,2,3,4,5 لنُضيف خاصية التكرار إلى range (فتعمل بهذا for..of)، علينا إضافة تابِع إلى الكائن بالاسم Symbol.iterator (وهو رمز خاصّ في اللغة يتيح لنا هذه الميزة). حين تبدأ for..of، تنادي ذلك التابِع مرة واحدة (أو تعرض الأخطاء المعروفة لو لم تجدها). على هذا التابِع إعادة مُكرِّر/iterator، أي كائنًا له التابِع next. بعدها، تعمل for..of مع ذلك الكائن المُعاد فقط لا غير. حين تحتاج for..of القيمة التالية، تستدعي next()‎ لذاك الكائن. يجب أن يكون ناتج next()‎ بالشكل هذا {done: Boolean, value: any}، حيث لو كانت done=true فيعني أن التكرار اكتمل، وإلّا فقيمة value هي التالية. إليك النص الكامل لتنفيذ كائن range (مع الملاحظات): let range = { from: 1, to: 5 }; // ‫‏1. حين ننادي for..of فهي تنادي هذه range[Symbol.iterator] = function() { // ‫‏...وتُعيد الكائن المُكرِّر: // ‫‏2. بعد ذلك تعمل for..of مع هذا المُكرِّر، طالبةً منه القيم التالية return { current: this.from, last: this.to, // ‫‏3. يُستدعى next()‎ في كلّ كرّة في حلقة for..of next() { // ‫‏4. يجب أن يُعيد القيمة كائنًا كهذا {done:.., value :...‎} if (this.current <= this.last) { return { done: false, value: this.current++ }; } else { return { done: true }; } } }; }; // ‫‏والآن تعمل! for (let num of range) { alert(num); // ‫‏1، ثمّ 2 فَ‍ 3 فَ‍ 4 فَ‍ 5 } لاحظ الميزة الأساس للمُكرَّرات: فصل الاهتمامات. عنصر range ذاته ليس له التابِع next()‎. بدل ذلك، يُنشأ كائن آخر (أي "المُكرَّر") عند استدعاء range[Symbol.iterator]()‎، وتابِعه next()‎ يُولّد قيم التكرار. الخلاصة هي أنّ كائن المُكرَّر منفصل عن الكائن الذي يُكرِّره هذا المُكرَّر. نظريًا، يمكننا دمجهما معًا واستعمال كائن range نفسه مُتعدَّدًا لتبسيط الكود أكثر. هكذا تمامًا: let range = { from: 1, to: 5, [Symbol.iterator]() { this.current = this.from; return this; }, next() { if (this.current <= this.to) { return { done: false, value: this.current++ }; } else { return { done: true }; } } }; for (let num of range) { alert(num); // ‏1، ثمّ 2 فَ‍ 3 فَ‍ 4 فَ‍ 5 الآن صار يُعيد range[Symbol.iterator]()‎ كائن range نفسه: وهذا الكائن فيه تابِع next()‎ اللازم كما ويتذكّر حالة التعداد الحالية في this.current. الشيفرة أبسط، أجل. وأحيانًا لا بأس به هكذا. المشكلة هنا هي استحالة وجود حلقتي for..of تعملان على الكائن في آن واحد، إذ سيتشاركان حالة التعداد؛ فليس هناك إلا مُتعدَّد واحد: الكائن نفسه. لكن أصلًا وجود حلقتي for..of نادر، حتى في العمليات غير المتزامَنة. مُكرَّرات لا تنتهي يمكن أيضًا ألا تنتهي المُكرَّرات أبدًا. فمثلًا لا ينتهي المدى range لو صار range.to = Infinity. يمكن أيضًا أن نصنع كائن مُتعدَّد يولّد أعدادًا شبه عشوائية (pseudorandom) لانهائية، ستفيد في حالات حرجة. ما من حدود مفروضة على ناتج next. يمكنه إعادة القيم مهما أراد وبالكم الذي أراد، لا مشكلة. طبعًا، المرور على هذا المُكرَّر بحلقة for..of لن ينتهي أبد الدهر، ولكن يمكننا إيقافها باستعمال break. السلاسل النصية مُكرَّرة تُعدّ المصفوفات والسلاسل النصية أكثر المُكرَّرات المقدَّمة من اللغة استعمالًا. بالنسبة إلى السلاسل النصية، فحلقة for..of تمرّ على محارفها: for (let char of "test") { // ‫تتنفّذ أربع مرات: مرة لكلّ محرف alert( char ); // ‫t فَ‍ e فَ‍ s فَ‍ t } كما وتعمل -كما يجب!- مع الأزواج النائبة أو البديلة (Surrogate Pairs)! let str = '??'; for (let char of str) { alert( char ); // ‫? وثمّ ? } نداء المُكرَّر جهارة لنعرف المُكرَّرات معرفةً أعمق، لنرى كيف يمكن استعمالها جهارةً. سنمرّ على سلسلة نصية بنفس الطريقة التي يمرّ بها for..of، ولكن هذه المرة ستكون النداءات مباشرة. تُنشِئ هذه الشيفرة مُكرَّرًا لسلسلة نصية وتأخذ القيم منه "يدويًا": let str = "Hello"; // تنفّذ ما تنفّذه // for (let char of str) alert(char); *!* let iterator = str[Symbol.iterator](); */!* while (true) { let result = iterator.next(); if (result.done) break; alert(result.value); // تطبع المحارف واحدًا تلو الآخر } في الحياة الواقعية، نادرًا ما ستحتاج هذا. لكن المفيد أنّنا نتحكّم أكثر على عملية التكرار موازنةً بِـ for..of. فمثلًا يمكننا تقسيم عملية التكرار: نكرّر قليلًا، نتوقّف ونفعل شيئًا آخر، ثمّ نواصل التكرار. المُكرَّرات والشبيهات بالمصفوفات هذان المصطلحان الرسميان يبدوان متشابهين إلى حدّ ما، ولكنّهما مختلفين تمام الاختلاف. حاوِل إدراكهما إدراكًا صحيحًا لتتجنّب هذا الاختلاط لاحقًا. المُكرَّرات كائنات تُنفّذ التابِع Symbol.iterator، كما شرحنا أعلاه. الشبيهات بالمصفوفات كائنات لها فهارس وصفة طول length، وبهذا ”تشبه المصفوفات“… المصطلح يشرح نفسه. حين نستعمل جافاسكربت للمهام الحقيقية في المتصفحات وغيرها من بيئات، نقابل مختلف الكائنات أكانت مُكرَّرات أو شبيهات بالمصفوفات، أو كليهما معًا. السلاسل النصية مثلًا مُكرَّرة (يمكن استعمال for..of عليها)، وشبيهة بالمصفوفات أيضًا (لها فهارس عددية وصفة length). ولكن ليس من الضروري أن يكون المُكرَّر شبيه بالمصفوفة، والعكس صحيح (لا يكون الشبيه بالمصفوفة مُكرَّر). فالمدى range في المثال أعلاه مُكرَّر، ولكنه ليس شبيه بالمصفوفة إذ ليس فيه صفات فهارس وlength. إليك كائنًا شبيهًا بالمصفوفات وليس مُكرَّرًا: let arrayLike = { // ‫فيه فهارس وطول => شبيه بالمصفوفات 0: "Hello", 1: "World", length: 2 }; // ‫خطأ (ما من Symbol.iterator) for (let item of arrayLike) {} عادةً، لا تكون لا المُكرَّرات ولا الشبيهات بالمصفوفات مصفوفات حقًا، فليس لها push أو pop وغيرها. لكن هذا غير منطقي. ماذا لو كان لدينا كائن من هذا النوع وأردنا التعامل معه بأنه مصفوفة؟ لنقل أنّا سنعمل على range باستعمال توابِع المصفوفات، كيف السبيل؟ Array.from التابِع العام Array.from يأخذ مُكرَّرًا أو شبيهًا بالمصفوفات ويحوّله إلى مصفوفة "فعلية". بعدها ننادي توابِع المصفوفات التي نعرفها عليها. هكذا مثلًا: let arrayLike = { 0: "Hello", 1: "World", length: 2 }; let arr = Array.from(arrayLike); // (*) alert(arr.pop()); // ‫تكتب World (أيّ أنّ التابِع عمل) يأخذ التابِع Array.from في سطر (*) الكائن، ويفحصه أكان مُكرَّرًا أو شبيهًا بالمصفوفات، ويصنع مصفوفة جديدة ينسخ قيم ذلك الكائن فيها. ذات الأمر للمُكرَّرات: // ‫نأخذ range من المثال أعلاه let arr = Array.from(range); alert(arr); // ‫تكتب 1,2,3,4,5 (تحويل toString للمصفوفة يعمل) والصياغة الكاملة للتابِع Array.from تتيح لنا تقديم دالة ”خريطة“ اختيارية: Array.from(obj[, mapFn, thisArg]) يمكن أن يكون الوسيط الاختياري الثاني mapFn دالةً تُطبّق على كلّ عنصر قبل إضافته للمصفوفة، ويتيح thisArg ضبط ماهيّة this للتابِع. مثال: // ‫نأخذ range من المثال أعلاه // ‫نُربّع كلّ عدد let arr = Array.from(range, num => num * num); alert(arr); // 1,4,9,16,25 هنا نستعمل Array.from لتحويل سلسلة نصية إلى مصفوفة من المحارف: let str = '??'; // يقسم ‫str إلى مصفوفة من المحارف let chars = Array.from(str); alert(chars[0]); // ? alert(chars[1]); // ? alert(chars.length); // 2 على العكس من str.split، فهي هنا تعتمد على طبيعة تكراريّة السلسلة النصية، ولهذا تعمل كما ينبغي (كما تعمل for..of) مع الأزواج النائبة. هنا أيضًا تقوم بذات الفعل، نظريًا: let str = '??'; let chars = []; // ‫داخليًا، تُنفّذ Array.from ذات الحلقة for (let char of str) { chars.push(char); } alert(chars); …ولكن تلك أقصر. يمكننا أيضًا صناعة تابِع slice مبني عليها يحترم الأزواج النائبة. function slice(str, start, end) { return Array.from(str).slice(start, end).join(''); } let str = '???'; alert( slice(str, 1, 3) ); // ?? // ‫التابِع الأصيل/native في اللغة لا يدعم الأزواج النائبة alert( str.slice(1, 3) ); // ‫يُولّد نصّ ”قمامة“ (قطعتين من أزواج نائبة مختلفة) خلاصة تُدعى الكائنات التي يمكن استعمالها في for..of بالمُكرَّرات (Iterables). على المُكرَّرات (تقنيًا) تنفيذ التابِع بالاسم Symbol.iterator. يُدعى ناتج obj[System.iterator]‎ بالمُكرَّر. يتعامل المُكرَّر بعملية التكرار. يجب أن يحتوي المُكرَّر التابِع بالاسم next()‎ حيث يُعيد كائن {done: Boolean, value: any}… تُشير done:true هنا بأنّ التكرار اكتمل، وإلّا فَـ value هي القيمة التالية. تُنادي الحلقة for..of التابِع Symbol.iterator تلقائيًا عند تنفيذها، ولكن يمكننا أيضًا فعل ذلك يدويًا. تُنفّذ المُكرَّرات المضمّنة في اللغة Symbol.iterator (مثل السلاسل النصية والمصفوفات). مُكرَّر السلاسل النصية يفهم الأزواج البديلة. تُدعى الكائنات التي فيها صفات فهارس وصفة طول length بالشبيهات بالمصفوفات. يمكن أيضًا أن تكون لها صفات وتوابِع أخرى، إلّا أنّ ليس فيها توابِع المصفوفات المضمّنة في بنية اللغة. لو نظرنا ورأينا مواصفات اللغة، فسنرى بأنّ أغلب التوابِع المضمّنة فيها تتعامل مع المصفوفات على أنّها مُكرَّرات أو شبيهات بالمصفوفات بدل أن تكون مصفوفات ”حقيقية“؛ هكذا تصير أكثر تجرّديّة (abstract). تصنع Array.from(obj[, mapFn, thisArg])‎ مصفوفةً Array حقيقية من المُكرَّر أو الشبيه بالمصفوفات obj، بهذا يمكن استعمال توابِع المصفوفات عليها. يتيح لنا الوسيطين mapFn وthisArg تقديم دالة لكلّ عنصر من عناصرها. ترجمة -وبتصرف- للفصل Arrays من كتاب The JavaScript language .task__importance { color: #999; margin-left: 30px; } .task__answer { border: 3px solid #f7f6ea; margin: 20px 0 14px; position: relative; display: block; padding: 25px 30px; } اقرأ أيضًا المقال التالي: النوع Map (الخرائط) والنوع Set (الأطقم) المقال السابق: توابع المصفوفات (Array methods)
  8. تقدّم المصفوفات توابِع عديدة تُسهِّل التعامل معها. ولتبسيطها سنقسّمها إلى مجموعات بحسب الوظيفة في هذا الفصل ونشرح كل منها على حدة. إضافة العناصر وإزالتها عرفنا من الفصل الماضي بالتوابِع التي تُضيف العناصر وتُزيلها من بداية أو نهاية المصفوفة: arr.push(...items)‎ -- يُضيف العناصر إلى النهاية، arr.pop()‎ -- يستخرج عنصرًا من النهاية، arr.shift()‎ -- يستخرج عنصرًا من البداية، arr.unshift(...items)‎ -- يُضيف العناصر إلى البداية. وهذه أخرى غيرها. الوصل splice يا ترى كيف نحذف أحد عناصر المصفوفة؟ المصفوفات كائنات، يمكننا تجربة delete وربما تنجح: let arr = ["I", "go", "home"]; delete arr[1]; // ‫أزِل "go" alert( arr[1] ); // undefined // ‫صارت المصفوفة الآن arr = ["I", , "home"];‎ alert( arr.length ); // 3 أُزيل العنصر صحيح، ولكنّ ما زال في المصفوفة ثلاثة عناصر، كما نرى في arr.length == 3. هذا طبيعي، إذ يُزيل delete obj.key القيمة بمفتاحها key… وهذا فقط. ينفع للكائنات ربّما، لكنّا نريدها للمصفوفات أن تنتقل كل العناصر على اليمين وتأخذ الفراغ الجديد. أي أننا نتوقع أن تصغر المصفوفة الآن. لهذا السبب علينا استعمال توابِع خاصّة لذلك. يمكننا تشبيه التابِع arr.splice(start)‎ بالتابِع «بتاع كُلّو» للمصفوفات (كما يُقال بالعامية). يمكنه أن يُجري ما تريد للعناصر: إدراج، إزالة، استبدال. هذه صياغته: arr.splice(index[, deleteCount, elem1, ..., elemN]) يبدأ التابِع من عند العنصر ذي الفهرس index، فيُزيل deleteCount من العناصر ويُدرج العناصر elem1, ..., elemN المُمرّرة إليه مكانها. أخيرًا يُعيد المصفوفة بالعناصر المُزالة. فهم هذا التابِع بالأمثلة أبسط. فلنبدأ أولًا بالحذف: let arr = ["I", "study", "JavaScript"]; // أزِل من العنصر ذا الفهرس 1 عنصرًا واحدًا (1)‏ arr.splice(1, 1); alert( arr ); // ["I", "JavaScript"] رأيت؟ سهلة. نبدأ من العنصر ذي الفهرس 1 ونُزيل عنصرًا واحدًا (1). الآن، نُزيل ثلاثة عناصر ونستبدلها بعنصرين آخرين: let arr = ["I", "study", "JavaScript", "right", "now"]; // أزِل الثلاث عناصر الأولى وعوّضها بتلك الأخرى arr.splice(0, 3, "Let's", "dance"); alert( arr ) // ‫["Let's", "dance", "right", "now"] أمّا هنا فكيف يُعيد splice مصفوفةً بالعناصر المُزالة. let arr = ["I", "study", "JavaScript", "right", "now"]; // أزِل أوّل عنصرين let removed = arr.splice(0, 2); alert( removed ); // "I", "study" <-- قائمة بالعناصر المُزالة يمكن أن يُدرج تابِع splice العناصر دون إزالة أيّ شيء أيضًا. كيف؟ نضع deleteCount يساوي الصفر 0: let arr = ["I", "study", "JavaScript"]; arr.splice(2, 0, "complex", "language"); alert( arr ); // "I", "study", "complex", "language", "JavaScript" الفهارس السالبة ممكنة أيضًا يمكننا هنا وفي توابِع المصفوفات الأخرى استعمال الفهارس السالبة. وظيفتها تحديد المكان بدءًا من نهاية المصفوفة، هكذا: let arr = [1, 2, 5]; arr.splice(-1, 0, 3, 4); alert( arr ); // 1,2,3,4,5 القطع slice التابِع arr.slice أبسط بكثير من شبيهه arr.splice. صياغته هي: arr.slice([start], [end]) وهو يُعيد مصفوفة جديدةً بنسخ العناصر من الفهرس start إلى end (باستثناء end). يمكن أن تكون start وحتّى end سالبتان، بهذا يُعدّ المحرّك القيمتان أماكن بدءًا من نهاية المصفوفة. هذا التابِع يشبه تابِع السلاسل النصية str.slice، ولكن بدل السلاسل النصية الفرعية، يُعيد المصفوفات الفرعية. إليك المثال الآتي: let arr = ["t", "e", "s", "t"]; // (نسخة تبدأ من 1 وتنتهي عند 3) alert( arr.slice(1, 3) ); // e,s // ‫(نسخة تبدأ من ‎-2 وتنتهي في النهاية) alert( arr.slice(-2) ); // s,t يمكننا أيضًا استدعائها بلا وُسطاء: يُنشئ arr.slice()‎ نسخة عن arr. نستعمل هذا غالبًا لأخذ نسخة وإجراء تعديلات عليها دون تعديل المصفوفة الأصلية، وتركها كما هي. الربط concat يُنشئ التابِع arr.concat مصفوفةً جديدة فيها القيم الموجودة في المصفوفات والعناصر الأخرى. صياغته هي: arr.concat(arg1, arg2...) وهو يقبل أيّ عدد من الوُسطاء، أكانت مصفوفات أو قيم. أمّا ناتجه هو مصفوفة جديدة تحوي العناصر من arr، ثم arg1 فَـ arg2 وهكذا دواليك. لو كان الوسيط argN نفسه مصفوفة، فستُنسخ كل عناصره، وإلّا فسيُنسخ الوسيط نفسه. لاحِظ هذا المثال: let arr = [1, 2]; // ‫اصنع مصفوفة فيها العنصرين: arr و [3,4] alert( arr.concat([3, 4]) ); // 1,2,3,4 // ‫اصنع مصفوفة فيها العناصر: arr و[3,4] و[5,6] alert( arr.concat([3, 4], [5, 6]) ); // 1,2,3,4,5,6 // ‫اصنع مصفوفة فيها العنصرين: arr و[3,4]، بعدها أضِف القيمتين 5 و 6 alert( arr.concat([3, 4], 5, 6) ); // 1,2,3,4,5,6 عادةً تنسخ المصفوفة عناصر المصفوفات الأخرى. بينما الكائنات الأخرى (حتّى لو كانت مثل المصفوفات) فستُضاف كتلة كاملة. let arr = [1, 2]; let arrayLike = { 0: "something", length: 1 }; alert( arr.concat(arrayLike) ); // 1,2,[object Object] … ولكن لو كان للكائن الشبيه بالمصفوفات خاصية Symbol.isConcatSpreadable، فستتعامل معه concat مثلما تتعامل مع المصفوفات: ستُضاف عناصره بدل كيانه: let arr = [1, 2]; let arrayLike = { 0: "something", 1: "else", [Symbol.isConcatSpreadable]: true, length: 2 }; alert( arr.concat(arrayLike) ); // 1,2,something,else التكرار: لكلّ forEach يتيح لنا التابِع arr.forEach تشغيل إحدى الدوال على كلّ عنصر من عناصر المصفوفة. الصياغة: arr.forEach(function(item, index, array) { // ... استعملهما فيما تريد }); مثال على عرض كلّ عنصر من عناصر المصفوفة: // ‫لكلّ عنصر، استدعِ دالة التنبيه alert ["Bilbo", "Gandalf", "Nazgul"].forEach(alert); بينما هذه الشيفرة تحبّ الكلام الزائد ومكانها في المصفوفة المحدّدة: ["Bilbo", "Gandalf", "Nazgul"].forEach((item, index, array) => { alert(`${item} is at index ${index} in ${array}`); }); ناتج التابِع (لو أعادَ شيئًا أصلًا) يُهمل ويُرمى. البحث في المصفوفات أمّا الآن لنرى التوابع التي تبحث في المصفوفة. التوابِع indexOf و lastIndexOf و includes للتوابِع arr.indexOf و arr.lastIndexOf و arr.includes نفس الصياغة ووظيفتها هي ذات وظيفة تلك بنسخة النصوص النصية، الفرق أنها هنا تتعامل مع العناصر بدل المحارف: arr.indexOf(item, from)‎ -- يبحث عن العنصر item بدءًا من الفهرس from، ويُعيد فهرسه حيث وجده. ولو لم يجده، يُعيد -1. arr.lastIndexOf(item, from)‎ -- نفسه، ولكن البحث يبدأ من اليمين وينتهي في اليسار. arr.includes(item, from)‎ -- يبحث عن العنصر item بدءًا من الفهرس from، ويُعيد true إن وجدته. مثال: let arr = [1, 0, false]; alert( arr.indexOf(0) ); // 1 alert( arr.indexOf(false) ); // 2 alert( arr.indexOf(null) ); // -1 alert( arr.includes(1) ); // true لاحظ أنّ التوابِع تستعمل الموازنة بِـ ===. لذا لو كنّا نبحث عن false، فستبحث هي عن false نفسها وليس الصفر. لو أردت معرفة فيما كانت تحتوي المصفوفة على عنصر معيّن، ولا تريد معرفة فهرسه، فدالة arr.includes مناسبة لك. وهناك أيضًا أمر، تختلف includes عن سابقاتها indexOf/lastIndexOf بأنّها تتعامل مع NaN كما ينبغي: const arr = [NaN]; alert( arr.indexOf(NaN) ); // ‫يُعيد ‎-1 (الصحيح هو 0 إلّا أنّ الموازنة === لا تعمل مع NaN) alert( arr.includes(NaN) );// true (الآن صحيح) البحث عبر find و findIndex لنقل أنّ لدينا مصفوفة من الكائنات، كيف نجد الكائن حسب شرط معيّن؟ هنا يمكننا استغلال التابِع arr.find(fn). صياغته هي: let result = arr.find(function(item, index, array) { // ‫لو أُعيدت القيمة true، فيُعاد العنصر ويتوقّف التعداد // ‫لو لم نجد ما نريد نُعيد undefined }); تُستدعى الدالة على كل عنصر من عناصر المصفوفة، واحدًا بعد الآخر: item: العنصر. index: الفهرس. array: المصفوفة نفسها. لو أعادت true، يتوقّف البحث ويُعاد العنصر item. إن لم يوجد شيء فيُعاد undefined. نرى في هذا المثال مصفوفة من المستخدمين، لكلّ مستخدم حقلان id وname. نريد الذي يتوافق مع الشرط id == 1: let users = [ {id: 1, name: "John"}, {id: 2, name: "Pete"}, {id: 3, name: "Mary"} ]; let user = users.find(item => item.id == 1); alert(user.name); // John في الحياة العملية، يكثُر استعمال الكائنات في المصفوفات، ولهذا فالتابِع find مفيد جدًا لنا. يمكنك ملاحظة بأنّا في المثال مرّرنا للتابِع find الدالة item => item.id == 1 وفيها وسيط واحد. هذا طبيعي فنادرًا ما نستعمل الوُسطاء البقية في هذه الدالة. يتشابه التابِع arr.findIndex كثيرًا مع هذا، عدا على أنّه يُعيد فهرس العنصر الذي وجده بدل العنصر نفسه، ويُعيد ‎-1 لو لم يجد شيئًا. الترشيح filter يبحث التابِع find عن أوّل عنصر (واحد فقط) يُحقّق للدالة شرطها فتُعيد true. لو أردت إعادة أكثر من واحد فيمكن استعمال arr.filter(fn)‎. تشبه صياغة filter التابِع find، الفرق هو إعادته لمصفوفة بكلّ العناصر المتطابقة: let results = arr.filter(function(item, index, array) { // ‫لو كانت true فتُضاف القائمة إلى مصفوفة النتائج ويتواصل التكرار // يُعيد مصفوفة فارغة إن لم يجد شيئًا }); مثال: let users = [ {id: 1, name: "John"}, {id: 2, name: "Pete"}, {id: 3, name: "Mary"} ]; // يُعيد مصفوفة تحتوي على أوّل مستخدمَين اثنين let someUsers = users.filter(item => item.id < 3); alert(someUsers.length); // 2 التعديل على عناصر المصفوفات لنرى الآن التوابِع التي تُعدّل المصفوفة وتُعيد ترتيبها. الخارطة map يُعدّ التابِع arr.map أكثرها استخدامًا وفائدةً أيضًا. ما يفعله هو استدعاء الدالة على كلّ عنصر من المصفوفة وإعادة مصفوفة بالنتائج. صياغته هي: let result = arr.map(function(item, index, array) { // يُعيد القيمة الجديدة عوض العنصر }); مثلًا، هنا نعدّل كل عنصر فنحوّله إلى طوله: let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length); alert(lengths); // 5,7,6 sort(fn)‎ نُرتّب باستدعاء arr.sort()‎ المصفوفة كما هي دون نسخها فنغيّر ترتيب عناصرها. هي الأخرى تُعيد المصفوفة المُرتّبة، ولكن غالبًا ما نُهمل القيمة المُعادة فالمصفوفة arr هي التي تتغيّر. مثال: let arr = [ 1, 2, 15 ]; // يعيد التابع ترتيب محتوى المصفوفة arr.sort(); alert( arr ); // 1, 15, 2 هل لاحظت بأنّ الناتج غريب؟ صار ‎1, 15, 2. ليس هذا ما نريد. ولكن، لماذا؟ مبدئيًا، تُرتّب العناصر وكأنها سلاسل نصية. بالمعنى الحرفي للكلمة: تُحوّل كل العناصر إلى سلاسل نصية عند الموازنة. والترتيب المعجماتي هو المتّبع لترتيب السلاسل النصية، ‎"2" > "15"‎ صحيحة حقًا. علينا لاستعمال الترتيب الذي نريده تمريرَ دالة تكون وسيطًا للتابِع arr.sort()‎. على الدالة موازنة قيمتين اثنتين (أيًا كانتا) وإعادة الناتج: function compare(a, b) { // لو كانت القيمة الأولى أكبر من الثانية if (a > b) return 1; // لو تساوت القيمتين if (a == b) return 0; // لو كانت القيمة الأولى أصغر من الثانية if (a < b) return -1; } مثال عن الترتيب لو كانت القيم أعدادًا: function compareNumeric(a, b) { if (a > b) return 1; if (a == b) return 0; if (a < b) return -1; } let arr = [ 1, 2, 15 ]; arr.sort(compareNumeric); alert(arr); // 1, 2, 15 الآن صارت تعمل كما نريد. لنتوقف لحظة ونفكّر فيما يحدث تمامًا. أنتّفق بأنّ المصفوفة arr يمكن أن تحتوي أيّ شيء؟ أيّ شيء من الأعداد أو السلاسل النصية أو الكائنات أو غيرها. كلّ ما لدينا هو مجموعة من العناصر. لترتيبها نحتاج دالة ترتيب تعرف طرقة مقارنة عناصر المصفوفة. مبدئيًا، الترتيب يكون بالسلاسل النصية. يُنفِّذ التابع arr.sort(fn)‎في طيّاته خوارزمية فرز عامّة. لسنا نكترث كيف تعمل هذه الخوارزمية خلف الكواليس (وهي غالبًا quicksort محسّنة)، بل نكترث بأنّها ستمرّ على المصفوفة، تُوازن عناصرها باستعمال الدالة المقدّمة أعلاه وتُعيد ترتيبها. نكترث بأن نقدّم دالة fn التي ستؤدّي الموازنة. بالمناسبة، لو أردت معرفة العناصر التي تُوازنها الدالة حاليًا، فلا بأس. لن يقتلك أحد لو عرضتها: [1, -2, 15, 2, 0, 8].sort(function(a, b) { alert( a + " <> " + b ); }); يمكن أن تقارن الخوارزمية العنصر مع غيره من العناصر، ولكنّها تحاول قدر الإمكان تقليص عدد الموازنات. يمكن أن تُعيد دالة الموازنة أيّ عدد في الواقع، ليس على دالة الموازنة إلّا إعادة عدد موجب بدلالة «هذا أكبر من ذاك» وسالب بدلالة «هذا أصغر من ذاك». يمكننا هكذا كتابة الدوال بأسطر أقل: That allows to write shorter functions: let arr = [ 1, 2, 15 ]; arr.sort(function(a, b) { return a - b; }); alert(arr); // 1, 2, 15 تحيا الدوال السهمية أتذكر الدوال السهمية من فصل تعابير الدوال والدوال السهمية؟ يمكننا استعمالها أيضًا لتبسيط كود الفرز: arr.sort( (a, b) => a - b ); لا تفرق هذه عن تلك الطويلة بشيء، البتة. العكس reverse يعكس التابِع arr.reverse ترتيب العناصر في المصفوفة arr. مثال: let arr = [1, 2, 3, 4, 5]; arr.reverse(); alert( arr ); // 5,4,3,2,1 كما ويُعيد المصفوفة arr بعد عكسها. التقسيم split والدمج join إليك موقفًا من الحياة العملية. تحاول الآن برمجة تطبيق مراسلة، ويُدخل المستخدم قائمة المستلمين بفاصلة بين كلّ واحد: John, Pete, Mary. ولكن لنا نحن المبرمجين، فالمصفوفة التي تحتوي الأسماء أسهل بكثير من السلسلة النصية. كيف السبيل إذًا؟ هذا ما يفعله التابِع str.split(delim)‎. يأخذ السلسلة النصية ويقسمها إلى مصفوفة حسب محرف القاسِم delim المقدّم. في المثال أعلاه نقسم حسب «فاصلة بعدها مسافة»: let names = 'Bilbo, Gandalf, Nazgul'; let arr = names.split(', '); for (let name of arr) { alert( `A message to ${name}.` ); // A message to Bilbo (والبقية) } للتابِع split وسيطًا عدديًا اختياريًا أيضًا، وهو يحدّ طول المصفوفة. لو قدّمته فستُهمل العناصر الأخرى. ولكن في الواقع العملي، نادرًا ما ستفيدك هذا: let arr = 'Bilbo, Gandalf, Nazgul, Saruman'.split(', ', 2); alert(arr); // Bilbo, Gandalf التقسيم إلى أحرف لو ناديت split(s)‎ وتركت s فارغًا فستُسقم السلسلة النصية إلى مصفوفة من الأحرف: let str = "test"; alert( str.split('') ); // t,e,s,t على العكس من split فنداء arr.join(glue)‎ يُنشئ سلسلة نصية من عناصر arr مجموعةً معًا «باللاصق». مثال: let arr = ['Bilbo', 'Gandalf', 'Nazgul']; let str = arr.join(';'); alert( str ); // Bilbo;Gandalf;Nazgul التابِعان reduce و reduceRight متى ما أردنا أن نمرّ على عناصر المصفوفة، استعملنا forEach أو for أو for..of. ومتى ما أردنا أن نمرّ ونُعيد بيانات كلّ عنصر، استعملنا map. نفس الحال مع التابعين arr.reduce وarr.reduceRight، إلّا أنهما ليسا بالسهولة نفسها. يُستعمل هذان التابعان لحساب قيمة واحدة حسب عناصر المصفوفة. هذه الصياغة: let value = arr.reduce(function(previousValue, item, index, array) { // ... }, [initial]); تُطبّق الدالة على كل عناصر المصفوفة واحدًا بعد الآخر، و«تنقل» النتيجة إلى النداء التالي لها: وُسطاء الدالة: previousValue - نتيجة النداء السابق للدالة. يُساوي قيمة initial في أوّل نداء (لو قُدّمت أصلًا). item -- العنصر الحالي في المصفوفة. index -- مكان العنصر. array -- المصفوفة نفسها. حين تُطبّق الدالة، تُمرّر إليها نتيجة النداء السابق في أوّل وسيط. أجل، معقّد قليلًا، لكن ليس كما تتخيّل لو قلنا أنّ الوسيط الأول بمثابة «ذاكرة» تخزّن النتيجة النهائية من إجراءات التنفيذ التي سبقتها. وفي آخر نداء تصير نتيجة التابِع reduce. ربّما نقدّم مثالًا لتسهيل المسألة. هنا نعرف مجموعة عناصر المصفوفة في سطر برمجي واحد: let arr = [1, 2, 3, 4, 5]; let result = arr.reduce((sum, current) => sum + current, 0); alert(result); // 15 الدالة المُمرّرة إلى reduce تستعمل وسيطين اثنين فقط، وهذا كافٍ عادةً. لنرى تفاصيل النداءات. في أوّل مرّة، قيمة sum هي قيمة initial (آخر وسيط في reduce) وتساوي 0، وcurrent هي أوّل عنصر في المصفوفة وتساوي 1. إذًا فناتج الدالة هو 1. في النداء التالي، sum = 1 ونُضيف العنصر الثاني في المصفوفة (2) ونُعيد القيمة. في النداء الثالث، sum = 3، ونُضيف العنصر التالي في المصفوفة، وهكذا دواليك إلى آخر نداء… هذا سير العملية الحسابية: وهكذا نمثّلها في جدول (كلّ صف يساوي نداء واحد للدالة على العنصر التالي في المصفوفة): sum current الناتج أوّل نداء 0 1 1 ثاني نداء 1 2 3 ثالث نداء 3 3 6 رابع نداء 6 4 10 خامس نداء 10 5 15 table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } هكذا نرى بوضوح شديد كيف يصير ناتج النداء السابق أوّل وسيط في النداء الذي يلحقه. يمكننا أيضًا حذف القيمة الأولية: let arr = [1, 2, 3, 4, 5]; // ‫أزلنا القيمة الأولية من التابِع reduce (اختفت القيمة 0) let result = arr.reduce((sum, current) => sum + current); alert( result ); // 15 وستكون النتيجة متطابقة، إذ أنّ reduce تأخذ أول عنصر من المصفوفة على أنّه قيمة أولية (لو لم نقدّم نحن قيمة أولية) وتبدأ العملية من العنصر الثاني. جدول العملية الحسابية مُطابق للجدول أعلاه، لو حذفنا أول سطر فيه. ولكن عليك أن تحترس حين لا تقدّم تلك القيمة. لو كانت المصفوفة فارغة فنداء reduce بدون القيمة الأولية سيعطيك خطأً. مثال على ذلك: let arr = []; arr.reduce((sum, current) => sum + current); الشيفرة السابقة ستطلق خطأ، إذ لا يمكن استدعاء reduce مع مصفوفة فارغة دون قيمة أولية، وتحل المشكلة بتوفير قيمة أولية، وستعاد آنذاك. لذا خُذ هذه النصيحة وحدّد قيمة أولية دومًا. لا يختلف التابِع arr.reduceRight عن هذا أعلاه إلا بأنّه يبدأ من اليمين وينتهي على اليسار. Array.isArray المصفوفات ليست نوعًا منفصلًا في اللغة، بل هي مبنيّة على الكائنات. لذا typeof لن تفيدك في التفريق بين الكائن العادي والمصفوفة: alert(typeof {}); // ‫كائن object alert(typeof []); // كائن أيضًا …ولكن، المصفوفات تستعمل كثيرًا جدًا لدرجة تقديم تابِع خاص لهذا الغرض: Array.isArray(value)‎. يُعيد هذا التابِع true لو كانت value مصفوفة حقًا، وfalse لو لم تكن. alert(Array.isArray({})); // false alert(Array.isArray([])); // true تدعم أغلب التوابِع thisArg تقبل أغلب توابِع المصفوفات تقريبًا، التوابع التي تستدعي دوالًا (مثل find وfilter وmap، عدا sort) - تقبل المُعامل الاختياري thisArg. لم نشرح هذا المُعامل في الأقسام أعلاه إذ أنّه نادرًا ما يُستعمل. ولكن علينا الحديث عنه لألا يكون الشرح ناقصًا. هذه الصياغة الكاملة لهذه التوابِع: arr.find(func, thisArg); arr.filter(func, thisArg); arr.map(func, thisArg); // ... // ‫الوسيط thisArg هو آخر وسيط اختياري تكون قيمة المُعامل thisArg للدالة func تساوي this. هنا مثلًا نستعمل تابِع كائن army على أنّه مرشّح، والوسيط thisArg يمرّر سياق التنفيذ وذلك لإيجاد المستخدمين الذين يعيد التابع army.canJoin القيمة true: let army = { minAge: 18, maxAge: 27, canJoin(user) { return user.age >= this.minAge && user.age < this.maxAge; } }; let users = [ {age: 16}, {age: 20}, {age: 23}, {age: 30} ]; *!* let soldiers = users.filter(army.canJoin, army); */!* alert(soldiers.length); // 2 alert(soldiers[0].age); // 20 alert(soldiers[1].age); // 23 لو استعملنا في المثال أعلاه users.filter(army.canJoin)‎ فسيُستدعى التابِع army.canJoin كدالة مستقلة بذاتها حيث this=undefined، ما سيؤدي إلى خطأ. يمكن استبدال استدعاء users.filter(army.canJoin, army)‎ بالتعليمة التي تُؤدّي ذات الغرض users.filter(user => army.canJoin(user))‎. نستعمل الأولى أكثر من الثانية إذ أنّ الناس تفهمها أكثر من تلك. ملخص ورقة فيها كل توابِع الدوال (غُشّ منها): لإضافة العناصر وإزالتها: push(...items)‎ -- تُضيف العناصر items إلى النهاية، pop()‎ -- تستخرج عنصرًا من النهاية، shift()‎ -- تستخرج عنصرًا من البداية، unshift(...items)‎ -- تُضيف العناصر items إلى البداية. splice(pos, deleteCount, ...items)‎ -- بدءًا من العنصر ذي الفهرس pos، احذف deleteCount من العناصر وأدرِج مكانه العناصر items. slice(start, end)‎ -- أنشِئ مصفوفة جديدة وانسخ عناصرها بدءًا من start وحتّىend(ولكن دونend). concat(...items)‎ -- أعِد مصفوفة جديدة: انسخ كل عناصر المصفوفة الحالية وأضَِف إليها العناصر items. لو كانت واحدة من عناصر items مصفوفة أيضًا، فستُنسخ عناصرها بدل. لتبحث عن العناصر: indexOf/lastIndexOf(item, pos)‎ -- ابحث عن العنصر item بدءًا من العنصر ذي الفهرس pos وأعِد فهرسه أو أعِد ‎-1 لو لم تجده. includes(value)‎ -- أعِد القيمة true لو كان العنصر value في المصفوفة، وإلا أعِد false. find/filter(func)‎ -- رشّح العناصر عبر دالة وأعِد أوّل قيمة (أو كل القيم) التي تُعيد الدالة قيمة true لو مُرّر ذلك العنصر لها. findIndex يشبه find، ولكن يُعيد الفهرس بدل القيمة. للمرور على عناصر المصفوفة: forEach(func)‎ -- يستدعي func لكلّ عنصر ولا يُعيد أيّ شيء. لتعديل عناصر المصفوفة: map(func)‎ -- أنشِئ مصفوفة جديدة من نتائج استدعاء func لكلّ من عناصر المصفوفة. sort(func)‎ -- افرز المصفوفة كما هي وأعِد ناتج الفرز. reverse()‎ -- اعكس عناصر المصفوفة كما هي وأعِد ناتج العكس. split/join -- حوّل المصفوفة إلى سلسلة نصية، والعكس أيضًا. reduce(func, initial)‎ -- احسب قيمة من المصفوفة باستدعاء func على كلّ عنصر فيها وتمرير الناتج بين كلّ استدعاء وآخر. وأيضًا: Array.isArray(arr)‎ يفحص لو كانت arr مصفوفة أم لا. لاحظ أنّ التوابِع sort وreverse وsplice تُعدّل المصفوفة نفسها. هذه التوابِع أعلاه هي أغلب ما تحتاج وما تريد أغلب الوقت (99.99%). ولكن هناك طبعًا غيرها: arr.some(fn)‎/arr.every(fn)‎ تفحص المصفوفة. تُنادى الدالة fn على كلّ عنصر من المصفوفة (مثل map). لو كانت أيًا من (أو كل) النتائج true، فيُعيد true، وإلًا يُعيد false. arr.fill(value, start, end)‎‎ -- يملأ المصفوفة بالقيمة المتكرّرة value من الفهرس start إلى الفهرس end. arr.copyWithin(target, start, end)‎‎ -- ينسخ العناصر من العنصر ذا الفهرس start إلى ذا الفهرس end ويلصقها داخلها عند الفهرس target (تعوّض ما هو موجود مكانها في المصفوفة). طالِع الكتيّب لقائمة فيها كل شيء. من أول وهلة سترى بأنّ عدد التوابِع لا ينتهي ومهمة حفظها مستحيلة، ولكن الواقع هي أنّها بسيطة جدًا. طالِع «ورقة الغشّ» لتعرف ما تفعل كلًا منها، ثمّ حُلّ مهام هذا الفصل لتتدرّب عليها وتكون خبيرًا كفاية بتوابِع الدوال. بعدها، لو احتجت التعامل مع المصفوفات ولا تدري ما تفعل، تعال هنا وابحث في ورقة الغشّ عن التابِع المناسب لحالتك. الأمثلة الموجودة ستفيدك فتكتبها كما ينبغي. وسريعًا ما ستتذكّر كل التوابِع من تلقاء نفسك ودون بذل أيّ جهد. تمارين حوّل «border-left-width» إلى «borderLeftWidth» الأهمية: 5 اكتب دالة camelize(str)‎ تغيّر الكلمات المقسومة بِشَرطات مثل «my-short-string» إلى عبارات بتنسيق «سنام الجمل»: «myShortString». بعبارة أخرى: أزِل كلّ الشرطات وحوّل أوّل حرف من كلّ كلمة بعدها إلى الحالة الكبيرة. أمثلة: camelize("background-color") == 'backgroundColor'; camelize("list-style-image") == 'listStyleImage'; camelize("-webkit-transition") == 'WebkitTransition'; تلميح: استعمل split لتقسيم السلسلة النصية إلى مصفوفة، ثمّ عدّل عناصرها وأعِد ربطها بتابِع join. الحل function camelize(str) { return str .split('-') // splits 'my-long-word' into array ['my', 'long', 'word'] .map( // كبر الحروف الأولى لجميع عناصر المصفوفة باستثناء أول عنصر // ['my', 'long', 'word'] --> ['my', 'Long', 'Word'] (word, index) => index == 0 ? word : word[0].toUpperCase() + word.slice(1) ) .join(''); // joins ['my', 'Long', 'Word'] into 'myLongWord' } نطاق ترشيح الأهمية: 4 اكتب دالة filterRange(arr, a, b)‎ تأخذ المصفوفة arr، وتبحث في عناصرها بين a وb وتُعيد مصفوفة بها. يجب ألّا تُعدّل الدالة المصفوفة، بل إعادة مصفوفة جديدة. مثال: let arr = [5, 3, 8, 1]; let filtered = filterRange(arr, 1, 4); alert( filtered ); // 3,1 alert( arr ); // 5,3,8,1 الحل function filterRange(arr, a, b) { // أضفنا الأقواس حول التعبير لتسهيل القراءة return arr.filter(item => (a <= item && item <= b)); } let arr = [5, 3, 8, 1]; let filtered = filterRange(arr, 1, 4); alert( filtered ); // 3,1 alert( arr ); // 5,3,8,1 نطاق ترشيح «كما هو» الأهمية: 4 اكتب دالة filterRangeInPlace(arr, a, b)‎ تأخذ المصفوفة arr وتُزيل منها كل القيم عدا تلك بين a وb. الشرط هو: ‎a ≤ arr ≤ b. يجب أن تُعدّل الدالة المصفوفة، ولا تُعيد شيئًا. مثال: let arr = [5, 3, 8, 1]; // حذف جميع الأعداد باستثناء الواقعة بين 1 و 4 filterRangeInPlace(arr, 1, 4); alert( arr ); // [3, 1] الحل function filterRangeInPlace(arr, a, b) { for (let i = 0; i < arr.length; i++) { let val = arr[i]; // إزالة إن كانت خارج النطاق if (val < a || val > b) { arr.splice(i, 1); i--; } } } let arr = [5, 3, 8, 1]; filterRangeInPlace(arr, 1, 4); // removed the numbers except from 1 to 4 alert( arr ); // [3, 1] الفرز بالترتيب التنازلي الأهمية: 4 let arr = [5, 2, 1, -10, 8]; // ... شيفرة ترتيب العناصر تنازليًا alert( arr ); // 8, 5, 2, 1, -10 الحل let arr = [5, 2, 1, -10, 8]; arr.sort((a, b) => b - a); alert( arr ); نسخ المصفوفة وفرزها الأهمية: 5 في يدنا مصفوفة من السلاسل النصية arr. نريد نسخة مرتّبة عنها وترك arr بلا تعديل. أنشِئ دالة copySorted(arr)‎ تُعيد هذه النسخة. let arr = ["HTML", "JavaScript", "CSS"]; let sorted = copySorted(arr); alert( sorted ); // CSS, HTML, JavaScript alert( arr ); // HTML, JavaScript, CSS الحل يمكن أن نستعمل slice()‎ لأخذ نسخة ونفرز المصفوفة: function copySorted(arr) { return arr.slice().sort(); } let arr = ["HTML", "JavaScript", "CSS"]; *!* let sorted = copySorted(arr); */!* alert( sorted ); alert( arr ); خارطة بالأسماء الأهمية: 5 لدينا مصفوفة من كائنات user، لكلّ منها صفة user.name. اكتب كودًا يحوّلها إلى مصفوفة من الأسماء. مثال: let john = { name: "John", age: 25 }; let pete = { name: "Pete", age: 30 }; let mary = { name: "Mary", age: 28 }; let users = [ john, pete, mary ]; let names = /* شيفرتك هنا */ alert( names ); // John, Pete, Mary الحل let john = { name: "John", age: 25 }; let pete = { name: "Pete", age: 30 }; let mary = { name: "Mary", age: 28 }; let users = [ john, pete, mary ]; let names = users.map(item => item.name); alert( names ); // John, Pete, Mary أنشِئ آلة حاسبة يمكن توسعتها لاحقًا الأهمية: 5 أنشِئ دالة إنشاء باني «constructor»‏ Calculator تُنشئ كائنات من نوع «آلة حاسبة» يمكن لنا «توسعتها». تنقسم هذه المهمة إلى جزئين اثنين: أولًا، نفّذ تابِع calculate(str)‎ يأخذ سلسلة نصية (مثل "1 + 2") بالتنسيق «عدد مُعامل عدد» (أي مقسومة بمسافات) ويُعيد الناتج. يجب أن يفهم التابِع الجمع + والطرح -. مثال عن الاستعمال: let calc = new Calculator; alert( calc.calculate("3 + 7") ); // 10 بعدها أضِف تابِع addMethod(name, func)‎ يُعلّم الآلة الحاسبة عمليّة جديدة. يأخذ التابِع المُعامل name ودالة func(a,b)‎ بوسيطين تُنفّذ هذه العملية. كمثال على ذلك سنُضيف عمليات الضرب * والقسمة / والأُسّ **: let powerCalc = new Calculator; powerCalc.addMethod("*", (a, b) => a * b); powerCalc.addMethod("/", (a, b) => a / b); powerCalc.addMethod("**", (a, b) => a ** b); let result = powerCalc.calculate("2 ** 3"); alert( result ); // 8 في هذه المهمة ليس هناك أقواس رياضية أو تعابير معقّدة. تفصل الأعداد والمُعامل مسافة واحدة فقط. يمكنك التعامل مع الأخطاء لو أردت. الحل لاحظ طريقة تخزين التوابِع، حيث تُضاف إلى صفة this.methods فقط. كلّ الشروط والتحويلات العددية موجودة في التابِع calculate. يمكننا في المستقبل توسيعه ليدعم تعابير أكثر تعقيدًا. function Calculator() { this.methods = { "-": (a, b) => a - b, "+": (a, b) => a + b }; this.calculate = function(str) { let split = str.split(' '), a = +split[0], op = split[1], b = +split[2] if (!this.methods[op] || isNaN(a) || isNaN(b)) { return NaN; } return this.methods[op](a, b); } this.addMethod = function(name, func) { this.methods[name] = func; }; } خارطة بالكائنات فرز المستخدمين حسب أعمارهم الأهمية: 5 اكتب دالة sortByAge(users)‎ تأخذ مصفوفة من الكائنات بالصفة age وتُرتبّها حسب أعمارهم age. مثال: let john = { name: "John", age: 25 }; let pete = { name: "Pete", age: 30 }; let mary = { name: "Mary", age: 28 }; let arr = [ pete, john, mary ]; sortByAge(arr); // [john, mary, pete] alert(arr[0].name); // John alert(arr[1].name); // Mary alert(arr[2].name); // Pete الحل function sortByAge(arr) { arr.sort((a, b) => a.age > b.age ? 1 : -1); } let john = { name: "John", age: 25 }; let pete = { name: "Pete", age: 30 }; let mary = { name: "Mary", age: 28 }; let arr = [ pete, john, mary ]; sortByAge(arr); // [john, mary, pete] alert(arr[0].name); // John alert(arr[1].name); // Mary alert(arr[2].name); // Pete خلط المصفوفات الأهمية: 3 اكتب دالة shuffle(array)‎ تخلط عناصر المصفوفة (أي ترتّبها عشوائيًا). يمكن بتكرار نداء shuffle إعادة العناصر بترتيب مختلف. مثال: let arr = [1, 2, 3]; shuffle(arr); // arr = [3, 2, 1] shuffle(arr); // arr = [2, 1, 3] shuffle(arr); // arr = [3, 1, 2] // ... يجب أن تكون جميع احتمالات ترتيب العناصر متساوية. فمثلًا يمكن إعادة ترتيب [1,2,3] لتكون [1,2,3] أو [1,3,2] أو [3,1,2] أو أو أو، واحتمال حدوث كلّ حالة متساوٍ. الحل هذا هو الحل البسيط: function shuffle(array) { array.sort(() => Math.random() - 0.5); } let arr = [1, 2, 3]; shuffle(arr); alert(arr); تعمل هذه الشيفرة (نوعًا ما) إذ أنّ Math.random() - 0.5 عددٌ عشوائي ويمكن أن يكون موجبًا أم سالبًا، بذلك تُعيد دالة الفرز ترتيب العناصر عشوائيًا. ولكن ليس هذه الطريقة التي تعمل فيها دوال الفرز، إذ ليس لكلّ حالات التبديل الاحتمال نفسه. فمثلًا في الشيفرة أعلاه، تُنفّذ shuffle ‏1000000 مرّة وتعدّ مرّات ظهور النتائج الممكنة كلّها: function shuffle(array) { array.sort(() => Math.random() - 0.5); } // نعدّ مرّات ظهور كلّ عمليات التبديل الممكنة let count = { '123': 0, '132': 0, '213': 0, '231': 0, '321': 0, '312': 0 }; for (let i = 0; i < 1000000; i++) { let array = [1, 2, 3]; shuffle(array); count[array.join('')]++; } // نعرض عدد عمليات التبديل الممكنة for (let key in count) { alert(`${key}: ${count[key]}`); } إليك عيّنة عن الناتج (إذ يعتمد على محرّك جافاسكربت): 123: 250706 132: 124425 213: 249618 231: 124880 312: 125148 321: 125223 نرى تحيّز الشيفرة بوضوح شديد، إذ تظهر 123 و213 أكثر بكثير من البقية. يختلف ناتج هذه الشيفرة حسب محرّكات جافاسكربت ولكن هذا يكفي لنقول بأنّ هذه الطريقة ليست موثوقة. ولكن لمَ لا تعمل الشيفرة؟ بشكل عام فتابِع sort أشبه ”بالصندوق الأسود“: نرمي فيه مصفوفة ودالة موازنة وننتظر أن تفرز لنا المصفوفة. ولكن بسبب عشوائية الموازنة يختلّ ذكاء الصندوق الأسود، وهذا الاختلال يعتمد على طريقة كتابة كلّ محرّك للشيفرة الخاصة به. ثمّة طرق أخرى أفضل لهذه المهمّة، مثل الخوارزمية خلّاط فِشر ييتس الرائعة. فكرتها هي المرور على عناصر المصفوفة بالعكس وتبديل كلّ واحد بآخر قبله عشوائيًا: function shuffle(array) { for (let i = array.length - 1; i > 0; i--) { let j = Math.floor(Math.random() * (i + 1)); // ‫رقم عشوائي من 0 إلى i // let t = array[i]; array[i] = array[j]; array[j] = t [array[i], array[j]] = [array[j], array[i]]; } } بدلنا بالمثال هذا العنصرين array‎ و array[j]‎ وذلك نستعمل صياغة ”الإسناد بالتفكيك" وستجد تفاصيل أكثر عن هذه الصياغة في فصول لاحقة. لنختبر الطريقة هذه بنفس ما اختبرنا تلك: function shuffle(array) { for (let i = array.length - 1; i > 0; i--) { let j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } } // نعدّ مرّات ظهور كلّ عمليات التبديل الممكنة let count = { '123': 0, '132': 0, '213': 0, '231': 0, '321': 0, '312': 0 }; for (let i = 0; i < 1000000; i++) { let array = [1, 2, 3]; shuffle(array); count[array.join('')]++; } // نعرض عدد عمليات التبديل الممكنة for (let key in count) { alert(`${key}: ${count[key]}`); } عيّنة عن الناتج: 123: 166693 132: 166647 213: 166628 231: 167517 312: 166199 321: 166316 الآن كل شيء سليم: لكلّ عمليات التبديل ذات الاحتمال. كما أنّ خوارزمية ”فِشر ييتس“ أفضل من ناحية الأداء إذ ليس علينا تخصيص الموارد لعملية ”الفرز“. ما متوسّط الأعمار؟ الأهمية: 4 اكتب دالة getAverageAge(users)‎ تأخذ مصفوفة من كائنات لها الصفة age وتُعيد متوسّط الأعمار. معادلة المتوسّط: ‎(age1 + age2 + ... + ageN) / N. مثال: let john = { name: "John", age: 25 }; let pete = { name: "Pete", age: 30 }; let mary = { name: "Mary", age: 29 }; let arr = [ john, pete, mary ]; alert( getAverageAge(arr) ); // (25 + 30 + 29) / 3 = 28 الحل function getAverageAge(users) { return users.reduce((prev, user) => prev + user.age, 0) / users.length; } let john = { name: "John", age: 25 }; let pete = { name: "Pete", age: 30 }; let mary = { name: "Mary", age: 29 }; let arr = [ john, pete, mary ]; alert( getAverageAge(arr) ); // 28 ترشيح العناصر الفريدة في المصفوفة الأهمية: 4 لمّا أنّ arr مصفوفة، أنشِئ دالة unique(arr)‎ تُعيد مصفوفة فيها عناصر arr غير مكرّرة. مثال: function unique(arr) { /* شيفرة هنا */ } let strings = ["Hare", "Krishna", "Hare", "Krishna", "Krishna", "Krishna", "Hare", "Hare", ":-O" ]; alert( unique(strings) ); // Hare, Krishna, :-O الحل ما سنفعل هو المرور على عناصر المصفوفة: سنفحص كلّ عنصر ونرى إن كان في المصفوفة الناتجة. إن كان كذلك… نُهمله، وإن لم يكن، نُضيفه إلى المصفوفة. function unique(arr) { let result = []; for (let str of arr) { if (!result.includes(str)) { result.push(str); } } return result; } let strings = ["Hare", "Krishna", "Hare", "Krishna", "Krishna", "Krishna", "Hare", "Hare", ":-O" ]; alert( unique(strings) ); // Hare, Krishna, :-O صحيح أنّ الكود يعمل، إلّا أنّ فيه مشكلة أداء محتملة. خلف الكواليس، يمرّ التابِع result.includes(str)‎ على المصفوفة result ويقارن كلّ عنصر مع str ليجد المطابقة المنشودة. لذا لو كان في result مئة 100 عنصر وما من أيّ مطابقة مع str، فعليها المرور على جُلّ result وإجراء 100 حالة مقارنة كاملة. ولو كانت result كبيرة مثل 10000 فيعني ذلك 10000 حالة مقارنة. إلى هنا لا مشكلة، لأنّ محرّكات جافاسكربت سريعة جدًا، والمرور على 1000 عنصر في المصفوفة يحدث في بضعة ميكروثوان. ولكنّا هنا في حلقة for نُجري هذه الشروط لكلّ عنصر من arr. فإن كانت arr.length تساوي 10000 فيعني أنّا سنُجري 10000*10000 = مئة مليون حالة مقارنة. كثير جدًا. إذًا، فهذا الحل ينفع للمصفوفات الصغيرة فقط. سنرى لاحقًا في الفصل كيف نحسّن هذا الكود. ترجمة -وبتصرف- للفصل Array methods من كتاب The JavaScript language .task__importance { color: #999; margin-left: 30px; } .task__answer { border: 3px solid #f7f6ea; margin: 20px 0 14px; position: relative; display: block; padding: 25px 30px; } اقرأ أيضًا المقال التالي: الكائنات المكرَّرة (Iterables) المقال السابق: المصفوفات (arrays)
  9. تُتيح لك الكائنات تخزين القيم في مجموعات ذات مفاتيح، وهذا أمر طيّب. ولكنّك دومًا ما ستحتاج في عملك إلى مجموعة مرتّبة، أي أنّ العناصر مرتّبة: عنصر أوّل، عنصر ثانٍ، عنصر ثالث، وهكذا دواليك. تُفيدنا هذه الطريقة في تخزين أمور مثل: المستخدمين والبضائع وعناصر HTML وغيرها. هنا يكون استعمال الكائنات غير موفّق، إذ أنّها لا تقدّم لنا أيّ تابِع يتيح تحديد ترتيب العناصر، فلا يمكننا إضافة خاصيةً جديدةً تحلّ بين الخاصيات الموجودة. لم تُصنع الكائنات لهذا الغرض بتاتًا. توجد بنية بيانات أخرى باسم Array (ندعو هذا النوع بالمصفوفة) وهي تتيح لنا تخزين مجموعات العناصر مرتّبةً. التصريح توجد صياغتان اثنتان لإنشاء مصفوفة فارغة: let arr = new Array(); let arr = []; تحتاج في عملك أغلب الوقت (ونقول أغلب الوقت) الصياغةَ الثانية. يمكننا أيضًا تقديم عناصر أوليّة للمصفوفة نكتبها في أقواس: let fruits = ["Apple", "Orange", "Plum"]; لاحظ أنّ عناصر المصفوفات مرقّمة (مُفهرسة) بدءًا من الرقم صفر. ويمكننا أن نأخذ عنصرًا منها بكتابة ترتيبه في أقواس معقوفة: let fruits = ["Apple", "Orange", "Plum"]; alert( fruits[0] ); // Apple/تفاحة alert( fruits[1] ); // Orange/برتقالة alert( fruits[2] ); // Plum/برقوق يمكن أيضًا تعويض أحد العناصر بأخرى: fruits[2] = 'Pear'; // ‫ ["Apple", "Orange", "Pear"] …أو إضافة أخرى جديدة إلى المصفوفة: fruits[3] = 'Lemon'; // ["Apple", "Orange", "Pear", "Lemon"] نعرف باستعمال التابع length إجمالي العناصر في المصفوفة: let fruits = ["Apple", "Orange", "Plum"]; alert( fruits.length ); // 3 يمكننا أيضًا استعمال alert لعرض المصفوفة كاملةً. let fruits = ["Apple", "Orange", "Plum"]; alert( fruits ); // Apple,Orange,Plum كما يمكن للمصفوفات تخزين أيّ نوع من البيانات. مثلًا: // قيم مختلفة الأنواع let arr = [ 'Apple', { name: 'John' }, true, function() { alert('hello'); } ]; // خُذ الكائن ذا الفهرس 1 ثمّ اعرض اسمه alert( arr[1].name ); // John // خُذ الدالة في الفهرس 3 ثمّ شغّلها arr[3](); // hello الفاصلة نهاية الجملة كما الكائنات، يمكن أن نُنهي عناصر المصفوفات بفاصلة ,: let fruits = [ "Apple", "Orange", "Plum", ]; يُسهّل أسلوب الكتابة «بالفاصلة نهاية الجملة» إضافة العناصر وإزالتها، إذ أن الأسطر البرمجية كلها تصير متشابهة. توابِع الدفع والجلب من بين أنواع المصفوفات، تُعدّ الطوابير أكثرها استعمالًا. تعني الصفوف (في علوم الحاسوب) تجميعات العناصر المرتّبة والتي تدعم العمليتين هاتين: الدفع push: يُضيف عنصرًا نهاية الصفّ الأخذ shift: يأخذ عنصرًا من بداية الصفّ، فيتحرّك الصفّ ويصير العنصر الثاني هو الأول فيه. تدعم المصفوفات كلتا العمليتين هاتين. وفي الحياة العملية، استعمال هاتين العمليتين ضروري دومًا. نأخذ مثلًا مجموعة رسائل مرتّبة يجب عرضها على الشاشة، أو «صف رسائل». هناك طريقة أخرى لاستعمال المصفوفات، وهي بنية البيانات بالاسم «كومة». تدعم الأكوام عمليتين أيضًا: الدفع push: يُضيف عنصرًا نهاية الكومة. السحب pop: يأخذ عنصرًا من نهاية الكومة. أي أنّ العناصر الجديدة تُضاف دومًا إلى آخر الكومة، وتُزال أيضًا من نهايتها. عادةً ما نرسم هذه الأكوام مثل أكوام بطاقات اللعب: البطاقات الجديدة تُضاف أعلى الكومة، وتُأخذ من أعلاها أيضًا: في الأكوام، آخر عنصر ندفعه إليها يكون أوّل من يُأخذ، ويسمّى هذا بمبدأ «آخر من يدخل أول من يخرج» (Last-In-First-Out). أمّا في الطوابير، فهي «أول من يدخل أول من يخرج» (First-In-First-Out). تعمل المصفوفات في جافاسكربت بالطريقتين، صفوف أو أكوام. يمكنك استعمالها لإضافة العناصر وإزالتها من/إلى بداية المصفوفة ونهايتها. تُسمّى بنية البيانات هذه (في علوم الحاسوب) باسم «الطوابير ذات الطرفين». التوابع التي تؤثّر على نهاية المصفوفة: pop: يستخرج آخر عنصر من المصفوفة ويُعيده: let fruits = ["Apple", "Orange", "Pear"]; alert( fruits.pop() ); // ‫أزِل «Pear» بتنبيه عبر الدالة alert alert( fruits ); // Apple, Orange push: يُضيف العنصر إلى آخر المصفوفة: let fruits = ["Apple", "Orange"]; fruits.push("Pear"); alert( fruits ); // Apple, Orange, Pear باستدعاء fruits.push(...)‎ كأنّما استدعيت fruits[fruits.length] = ...‎. التوابِع التي تؤثّر على بداية المصفوفة: shift: يستخرج أوّل عنصر من المصفوفة وتُعيده: let fruits = ["Apple", "Orange", "Pear"]; alert( fruits.shift() ); // ‫أزِل التفاحة واعرضها بِ‍ alert alert( fruits ); // Orange, Pear unshift: يُضيف العنصر إلى أوّل المصفوفة: let fruits = ["Orange", "Pear"]; fruits.unshift('Apple'); alert( fruits ); // Apple, Orange, Pear يمكنك أيضًا إضافة أكثر من عنصر في استدعاء واحد من push وunshift: let fruits = ["Apple"]; fruits.push("Orange", "Peach"); fruits.unshift("Pineapple", "Lemon"); // ["Pineapple", "Lemon", "Apple", "Orange", "Peach"] alert( fruits ); داخليًا وخلف الكواليس المصفوفات هي كائنات، كائنات من نوع خاص. القوسان المعقوفان المستعملان للدخول إلى الخاصيات arr[0]‎ هما فعليًا جزء من صياغة الكائنات. داخليًا، لا يفرق ذاك عن obj[key]‎ (إذ arr هو الكائن والأرقام تلك مفاتيح). ما تفعله المصفوفات هو «توسعة» الكائنات بتقديم توابِع خاصّة تعمل مع البيانات والمجموعات المرتّبة، إضافةً إلى تقديم خاصية length، ولكنّ أساسها ما زال الكائنات. تذكّر أنّ هناك 7 أنواع أساسية في جافاسكربت، فقط. المصفوفة هي كائن، وتتصرّف بناءً على ذلك، ككائن. فمثلًا، عند نسخها تُنسخ بالمرجع (By reference): let fruits = ["Banana"] let arr = fruits; // انسخها بالمرجع (متغيران اثنان يُشيران إلى نفس المصفوفة)‏ alert( arr === fruits ); // true arr.push("Pear"); // ‫عدّل المصفوفة «بالمرجع» alert( fruits ); // صاروا الآن عنصرين: ‫Banana, Pear …إلا أنّ المميز حقًا في المصفوفات هي آلية تمثيلها داخليًا، إذ يحاول المحرّك تخزين عناصرها متتابعةً في مساحة الذاكرة، أي واحدة بعد الأخرى، تمامًا مثلما وضّحت الرسوم في هذا الفصل. هناك أيضًا طُرق أخرى لتحسين (optimization) المصفوفات فتعمل بسرعة كبيرة حقًا. ولكن، لو لم نعمل مع المصفوفة على أنّها «تجميعة مرتّبة» بل وكأنّها كائن مثل غيرها، فسينهار هذا كله. يمكننا (تقنيًا) كتابة هذا: // نصنع مصفوفة let fruits = []; // نُسند خاصيةً لها فهرس أكبر من طول المصفوفة بكثير fruits[99999] = 5; // نُنشئ خاصيةً لها أيّ اسم fruits.age = 25; أجل، يمكننا فعل هذا، فالمصفوفات في أساسها كائنات، ويمكننا إضافة ما نريد من خاصيات لها. ولكن المحرّك هنا سيرى بأنّا نُعامل المصفوفة معاملة الكائن العادي. وبهذا -في هذه الحالة- لا تنفع أنواع التحسين المخصّصة للكائنات، وسيُعطّلها المحرّك، وتضيع كل فوائد المصفوفات. هذه طرائق يمكنك فيها إساءة استعمال المصفوفات: إضافة خاصيات ليست عددية مثل arr.test = 5. الفراغات، أي تُضيف arr[0]‎ وبعدها arr[1000]‎ (دون عناصر بينها). ملء المصفوفة بالعكس، أي arr[1000]‎ ثم arr[999]‎ وهكذا. نرجوك هنا أن تعتبر المصفوفات بنًى خاصّة تتعامل مع البيانات المرتّبة، فهي تقدّم لك توابِع خاصّة لهذا بالذات. يتغيّر تعامل محرّكات جافاسكربت حين تتعامل مع المصفوفات، فتعمل مع البيانات المرتّبة المتتابعة، فمن فضلك استعمِلها بهذه الطريقة. لو أردت مفاتيح لا عددية، أو مثلما في الحالات الثلاث أعلاه، فغالبًا لا تكون المصفوفة ما تبحث عنه، بل الكائنات العادية {}. الأداء يعمل التابِعان push/pop بسرعة، بينما shift/unshift بطيئان. لماذا يكون التعامل مع نهاية المصفوفة أسرع من التعامل مع بدايتها؟ لنأخذ نظرة عمًا يحدث أثناء تنفيذ الشيفرة: // خُذ عنصرًا واحدًا من الأوّل fruits.shift(); لا يكفي أن تأخذ العنصر ذا الفهرس 0 وتُزيله، بل عليك أيضًا إعادة ترقيم بقية العناصر وفقًا لذلك. ما تفعله عملية shift هي ثلاث أمور: إزالة العنصر ذا الفهرس 0. تحريك كل العناصر الأخرى إلى يسار المصفوفة، وإعادة ترقيمها من الفهرس رقم 1 إلى 0، ومن 2 إلى 1، وهكذا. تحديث خاصية الطول length. زِد العناصر في المصفوفات، تزيد الوقت اللازم لتحريكها، وتزيد عدد العمليات داخل الذاكرة. مثل shift، تفعل unshift نفس الأمور: فلنُضيف عنصرًا إلى بداية المصفوفة، علينا أولًا تحريك كل العناصر إلى اليمين، أي نزيد فهارسها كلها. وماذا عن push/pop؟ ليس عليها تحريك أيّ عنصر. فلاستخراج عنصر من النهاية، يمحي التابِع pop الفهرس ويعدّل الطول length فيقصّره. إجراءات عملية pop: // خُذ عنصرًا واحدًا من الآخر fruits.pop(); لا تحتاج عملية pop إلى تحريك ولا مقدار ذرة، لأنّ العناصر تبقى كما هي مع فهارسها. لهذا السبب سرعتها تفوق سرعة البرق، أي أقصى سرعة ممكنة. ذات الأمر للتابِع push. الحلقات هذه إحدى أقدم الطرق للمرور على عناصر المصفوفات، استعمال حلقة for بالمرور على فهارس المصفوفة: let arr = ["Apple", "Orange", "Pear"]; for (let i = 0; i < arr.length; i++) { alert( arr[i] ); } ولكن المصفوفات تسمح بطريقة أخرى للمرور عليها، for..of: let fruits = ["Apple", "Orange", "Plum"]; // المرور على عناصر المصفوفة for (let fruit of fruits) { alert( fruit ); } لا تتيح لك حلقة for..of الوصول إلى فهرس العنصر الحالي في الحلقة، بل قيمة العنصر فقط، وفي أغلب الأحيان هذا ما تحتاج، كما وأنّ الشيفرة أقصر. طالما المصفوفات كائنات، فيمكننا (نظريًا) استعمال for..in: let arr = ["Apple", "Orange", "Pear"]; for (let key in arr) { alert( arr[key] ); // Apple, Orange, Pear } ولكن الواقع أنّ الطريقة هذه سيئة، ففيها عدد من المشاكل: تمرّ الحلقة for..in على كل الخاصيات مجتمعةً، وليس العددية منها فقط. توجد في المتصفّح وغيرها من بيئات كائنات «شبيهة بالمصفوفات». أي أن لها خاصية الطول length وخاصيات الفهارس، ولكن لها أيضًا توابِع وخاصيات لا عددية أخرى لا نحتاجها أغلب الأحيان، إلّا أنّ حلقة for..in ستمرّ عليها هي أيضًا. لذا لو اضطررت للعمل مع الكائنات الشبيهة بالمصفوفات، فهذه الخاصيات «الأخرى» ستتسبّب بالمتاعب بلا شك. أداء حلقة for..in يكون بالنحو الأمثل على الكائنات العامة لا المصفوفات، ولهذا سيكون أبطأ 10 أو 100 مرة. طبعًا فالأداء سريع جدًا مع ذلك. هذه السرعة الإضافية ستنفع غالبًا في الحالات الحرجة (أي حين يجب أن يكون تنفيذ الحلقة بأسرع وقت ممكن). مع ذلك، الحرس واجب والاهتمام بهذا الاختلاف مهم. لكن في أغلب الأحيان، استعمال for..in للمصفوفات فكرة سيئة. كلمتان حول «الطول» تتحدّث خاصية الطول length تلقائيًا متى ما عدّلنا المصفوفة. وللدّقة، فهي ليست عدد القيم في المصفوفة، بل أكبر فهرس موجود زائدًا واحد. فمثلًا، لو كان لعنصر واحد فهرس كبير، فسيكون الطول كبيرًا أيضًا: let fruits = []; fruits[123] = "Apple"; alert( fruits.length ); // 124 لكنّنا لا نستعمل المصفوفات هكذا. سجّلها عندك. هناك ما هو عجيب حول خاصية length، ألا وهي أنّها تقبل الكتابة. لو زِدنا قيمتها يدويًا، لا نرى شيئًا تغيّر، ولكن لو أنقصناها، تُبتر المصفوفة حسب الطول، ولا يمكن العودة عن هذه العملية. طالِع هذا المثال: let arr = [1, 2, 3, 4, 5]; // نبتر المصفوفة ونُبقي عنصرين فقط arr.length = 2; alert( arr ); // [1, 2] // نُعيد الطول الذي كان في الأوّل arr.length = 5; // undefined القيم المبتورة لا تعود، وإنما تصبح alert( arr[3] ); إذًا، فالطريقة الأسهل والأبسط لمسح المصفوفة هي: arr.length = 0;‎. new Array()‎ هناك صياغة أخرى يمكن استعمالها لإنشاء المصفوفات: let arr = new Array("Apple", "Pear", "etc"); ولكنّها نادرًا ما تُستعمل، فالأقواس المعقوفة [] أقصر. كما وأنّ هذه الطريقة تقدّم ميزة… مخادعة، إن صحّ التعبير. إن استدعيت new Array وفيها مُعامل واحد فقط (عددي)، فستُنشأ مصفوفة لا عناصر فيها، ولكن بالطول المحدّد. هاك طريقة يمكنك بها تخريب حياتك، لو أردت: let arr = new Array(2); // هل ستكون المصفوفة [2]؟ alert( arr[0] ); //‫غير معرّفة! ليس فيها عناصر. alert( arr.length ); // طولها 2 في هذا الشيفرة، كل عناصر new Array(number)‎ لها القيمة undefined. ولهذا نستعمل الأقواس المعقوفة غالبًا، لنتجنّب هذه المفاجئات السارّة، إلّا لو كنت تعي حقًا ما تفعله. المصفوفات متعدّدة الأبعاد يمكن أن تكون عناصر المصفوفات مصفوفات أخرى أيضًا. نستغلّ هذه الميزة فنعمل مصفوفات متعدّدة الأبعاد لتخزين المصفوفات الرياضية مثلًا: let matrix = [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ]; alert( matrix[1][1] ); // ‫5، العنصر في الوسط تحويل المصفوفات إلى سلاسل نصية تُنفّذ المصفوفات تابِع toString خاصّ بها، فيُعيد قائمة من العناصر مفصولة بفواصل. خُذ هذا المثال: let arr = [1, 2, 3]; alert( arr ); // 1,2,3 alert( String(arr) === '1,2,3' ); // true جرّب هذه، أيضًا: alert( [] + 1 ); // "1" alert( [1] + 1 ); // "11" alert( [1,2] + 1 ); // "1,21" ليس للمصفوفات Symbol.toPrimitive ولا دالة valueOf، بل تُنفّذ التحويل toString فقط لا غير. هكذا تصير [] سلسلة نصية فارغة، و[1] تصير "1" و[1,2] تصير "1,2". متى ما أضاف مُعامل الجمع الثنائي "+" شيئًا إلى السلسلة النصية، حوّله إلى سلسلة نصية هو الآخر. هكذا هي الخطوة التالية: alert( "" + 1 ); // "1" alert( "1" + 1 ); // "11" alert( "1,2" + 1 ); // "1,21" ملخص المصفوفات نوع خاصّ من الكائنات، وهي مخصّصة لتخزين البيانات عناصر مرتّبة، كما وإدارتها أيضًا. التصريح // الأقواس المعقوفة (طبيعية)‏ let arr = [item1, item2...]; // ‫new Array (نادرة جدًا) let arr = new Array(item1, item2...); باستدعاء new Array(number)‎ تُنشئ مصفوفة بالطول المحدّد، ولكن بلا أيّ عنصر. خاصية الطول length هي طول المصفوفة، أو للدّقة، آخر فهرس عددي زائدًا واحد. التوابِع المختلفة على المصفوفات تعدّل هذه الخاصية تلقائيًا. إن قصّرنا خاصية length يدويًا، فنحن نبتر المصفوفة حسب القيمة الجديدة. يمكننا استعمال المصفوفة كما الصفوف ذات الطرفين، بالعمليات الآتية: push(...items)‎: تُضيف items إلى النهاية. pop()‎: تُزيل العنصر من النهاية وتُعيده. shift()‎: تُزيل العنصر من البداية وتُعيده. unshift(...items)‎: تُضيف items إلى البداية. لتمرّ على عناصر المصفوفة: for (let i=0; i<arr.length; i++)‎ -- تتنفّذ بسرعة، ومتوافقة مع المتصفحات القديمة. for (let item of arr)‎ -- الصياغة الحديثة للعناصر فقط. for (let i in arr)‎ -- إيّاك واستعمالها. سنرجع إلى المصفوفات لاحقًا ونتعلّم توابِع أخرى لإضافة العناصر وإزالتها واستخراجها، كما وترتيب المصفوفات. هذا كله في الفصل التالي، توابع المصفوفات. تمارين هل تُنسخ المصفوفات؟ الأهمية: 3 ما ناتج هذه الشيفرة؟ let fruits = ["Apples", "Pear", "Orange"]; // ‫ادفع عنصرًا جديدًا داخل «النسخة» let shoppingCart = fruits; shoppingCart.push("Banana"); // ماذا في ‫fruits؟ alert( fruits.length ); // ? الحل الناتج هو 4: let fruits = ["Apples", "Pear", "Orange"]; let shoppingCart = fruits; shoppingCart.push("Banana"); alert( fruits.length ); // 4 هذا لأنّ المصفوفات كائنات. فكِلا shoppingCart وfruits يُشيران إلى نفس المصفوفة ذاتها. العمليات على المصفوفات الأهمية: 5 فلنجرّب خمس عمليات على المصفوفات. أنشِئ مصفوفة باسم styles تحوي العنصرين «Jazz» و«Blues». أضِف «Rock-n-Roll» إلى نهايتها. استبدِل القيمة في الوسط بالقيمة «Classics». يجب أن تعمل الشيفرة الذي ستكتبه ليجد القيمة في الوسط مع أيّ مصفوفة كانت لو كان طولها عدد فردي. أزِل القيمة الأولى من المصفوفة واعرضها. أضِف «Rap» و«Reggae» إلى بداية المصفوفة. المصفوفة خلال العمليات هذه: Jazz, Blues Jazz, Blues, Rock-n-Roll Jazz, Classics, Rock-n-Roll Classics, Rock-n-Roll Rap, Reggae, Classics, Rock-n-Roll الحل let styles = ["Jazz", "Blues"]; styles.push("Rock-n-Roll"); styles[Math.floor((styles.length - 1) / 2)] = "Classics"; alert( styles.shift() ); styles.unshift("Rap", "Reggae"); النداء داخل سياق المصفوفة الأهمية: 5 ما الناتج؟ لماذا؟ let arr = ["a", "b"]; arr.push(function() { alert( this ); }) arr[2](); // ? الحل من ناحية الصياغة، فالاستدعاء arr[2]()‎ هو نفسه النداء القديم obj[method]()‎، فبدل obj هناك arr، وبدل method هناك 2. إذًا فما أمامنا هو نداء الدالة arr[2]‎ وكأنّها تابِع لكائن. وبالطبيعة، فهي تستلم this الذي يُشير إلى الكائن arrوتكتب المصفوفة ناتجًا: let arr = ["a", "b"]; arr.push(function() { alert( this ); }) arr[2](); // "a","b",function للمصفوفة ثلاث قيم: الاثنتين من البداية، مع الدالة. جمع الأعداد المُدخلة الأهمية: 4 اكتب دالة sumInput()‎ تؤدّي الآتي: طلب القيم من المستخدم باستعمال prompt وتخزينها في مصفوفة. أن ينتهي الطلب لو أدخل المستخدم قيمة غير عددية، أو سلسلة نصية فارغة، أو ضغطَ «ألغِ». حساب مجموع عناصر المصفوفة وإعادتها. ملاحظة: الصفر 0 عدد مسموح، لذا لا تُوقف الطلب لو رأيته. الحل انتبه هنا على التفصيل الصغير في الحل، صغير ولكن مهمّ: لا يمكننا تحويل قيمة المتغير value إلى عدد مباشرةً بعد prompt، لأنّه بعدما نُجري value = +value، لن نفرّق بين السلسلة النصية الفارغة (أي علينا إيقاف الطلب) من الصفر (قيمة صالحة). عوض ذلك نؤجّل ذلك لما بعد. function sumInput() { let numbers = []; while (true) { let value = prompt("A number please?", 0); // هل نلغي الطلب؟ if (value === "" || value === null || !isFinite(value)) break; numbers.push(+value); } let sum = 0; for (let number of numbers) { sum += number; } return sum; } alert( sumInput() ); أكبر مصفوفة فرعية الأهمية: 2 البيانات المُدخلة هي مصفوفة من الأعداد، مثل arr = [1, -2, 3, 4, -9, 6]‎. والمهمة هي: البحث عن مصفوفة فرعية متتابعة في arr لها أكبر ناتج جمع. اكتب دالة getMaxSubSum(arr)‎ لتُعيد ذلك الناتج. مثال: getMaxSubSum([-1, 2, 3, -9]) = 5 (مجموع 2+3) getMaxSubSum([2, -1, 2, 3, -9]) = 6 (مجموع 2+(-1)+2+3) getMaxSubSum([-1, 2, 3, -9, 11]) = 11 ‫(وهكذا...) getMaxSubSum([-2, -1, 1, 2]) = 3 getMaxSubSum([100, -9, 2, -3, 5]) = 100 getMaxSubSum([1, 2, 3]) = 6 (نأخذها كلها) إن كانت القيم كلها سالبة فيعني هذا ألا نأخذ شيئا (المصفوفة الفرعية فارغة)، وبهذا يكون الناتج صفرًا: getMaxSubSum([-1, -2, -3]) = 0 يُحبّذ لو تفكّر -رجاءً- بحلّ سريع: O(n2) أو حتّى O(n) لو أمكنك. الحل النسخة البطيئة يمكننا حساب كلّ ناتج جمع فرعي ممكن. أبسط طريقة هي أخذ كلّ عنصر وحساب مجموع المصفوفات الفرعية بدءًا من مكان العنصر. فمثلًا إن كان لدينا ‎[-1, 2, 3, -9, 11]‎: // ‫نبدأ بِ‍ ‎-1: -1 -1 + 2 -1 + 2 + 3 -1 + 2 + 3 + (-9) -1 + 2 + 3 + (-9) + 11 // ‫نبدأ بِ‍ 2: 2 2 + 3 2 + 3 + (-9) 2 + 3 + (-9) + 11 // نبدأ بِ‍‏‫ 3: 3 3 + (-9) 3 + (-9) + 11 // نبدأ بِ‍‏‫ ‎-9: -9 -9 + 11 // نبدأ بِ‍‏‫ 11: 11 في الواقع فالشيفرة هي حلقات متداخلة، تمرّ الحلقة العلوية على عناصر المصفوفة، والسفلية تعدّ النواتج الفرعية بدءًا من العنصر الحالي. function getMaxSubSum(arr) { let maxSum = 0; // إن لم نأخذ أيّ عنصر، فسنُرجع الصفر 0 for (let i = 0; i < arr.length; i++) { let sumFixedStart = 0; for (let j = i; j < arr.length; j++) { sumFixedStart += arr[j]; maxSum = Math.max(maxSum, sumFixedStart); } } return maxSum; } alert( getMaxSubSum([-1, 2, 3, -9]) ); // 5 alert( getMaxSubSum([-1, 2, 3, -9, 11]) ); // 11 alert( getMaxSubSum([-2, -1, 1, 2]) ); // 3 alert( getMaxSubSum([1, 2, 3]) ); // 6 alert( getMaxSubSum([100, -9, 2, -3, 5]) ); // 100 مدى التعقيد الحسابي لهذا الحل هو O(n2). أي بعبارة أخرى، لو زِدت حجم المصفوفة مرتين اثنتين، فسيزيد وقت عمل الخوارزمية أربع مرات أكثر. يمكن أن تؤدّي هذه الخوازرميات للمصفوفات الكبيرة (نتحدّث عن 1000 و10000 وأكثر) إلى بطء شديد في التنفيذ. النسخة السريعة لنمرّ على عناصر المصفوفة ونحفظ ناتج جمع العناصر الحالي في المتغير s. متى ما صار s سالبًا، نعيّنه صفرًا s=0. إجابتنا على هذا هي أكبر قيمة من هذا المتغير s. لو لم يكن هذا الوصف منطقيًا، فيمكنك مطالعة الشيفرة، قصيرة للغاية: function getMaxSubSum(arr) { let maxSum = 0; let partialSum = 0; for (let item of arr) { // لكلّ ‫item في arr partialSum += item; // نُضيفه إلى ‫partialSum maxSum = Math.max(maxSum, partialSum); // نتذكّر أكبر قيمة if (partialSum < 0) partialSum = 0; // لو كانت سالبة فالصفر } return maxSum; } alert( getMaxSubSum([-1, 2, 3, -9]) ); // 5 alert( getMaxSubSum([-1, 2, 3, -9, 11]) ); // 11 alert( getMaxSubSum([-2, -1, 1, 2]) ); // 3 alert( getMaxSubSum([100, -9, 2, -3, 5]) ); // 100 alert( getMaxSubSum([1, 2, 3]) ); // 6 alert( getMaxSubSum([-1, -2, -3]) ); // 0 على الخوارزمية هنا أن تمرّ مرورًا واحدًا فقط على المصفوفة، أي أن التعقيد الحسابي هو O(n)‎. يمكنك أن تجد معلومات مفصّلة أكثر عن الخوارزمية هنا: Maximum subarray problem. لو لم يكن هذا واضحًا بعد، فالأفضل لو تتعقّب ما تفعل الخوارزمية في الأمثلة أعلاه، وترى ما تفعله من حسابات. «التعقّب يغني عن ألف كلمة»… ربّما. ترجمة -وبتصرف- للفصل Arrays من كتاب The JavaScript language .task__importance { color: #999; margin-left: 30px; } .task__answer { border: 3px solid #f7f6ea; margin: 20px 0 14px; position: relative; display: block; padding: 25px 30px; } اقرأ أيضًا المقال التالي: توابع المصفوفات (Array methods) المقال السابق: السلاسل النصية (strings)
  10. كثيرًا ما يتكرّر هذا السيناريو: يأتي مطوّر وِب بنيّة إضفاء ”العصريّة“ لأحد المواقع (أي ليحسّن تجربته على المحمول أو يعدّل لوحة الألوان المستعملة فيه أو الخطوط أو أيّ شيء آخر)، وينتهي الأمر بأن يهدم إحدى أكثر المشاريع التجارية ازدهارًا على الإنترنت – أن يقتل الموقع بأخطاء السيو SEO (اختصار Search Engine Optimization أو ”تحسين محرّكات البحث لظهور الموقع“). وحين أقول ”كثيرًا“ فلستُ أبالغ أو أضخّم الأمر، بل أقصدها. فحين يراسلني زبونين اثنين مستقلّين يواجهان المشكلة ذاتها وفي أسبوع واحد، تكون ”كثيرًا“ حقًا. إليك ما يصف هذان الزبونان معًا وصفًا عادلًا: إذًا، ما سبب المشكلة؟ في الحالتين أعلاه: انعدام كفاءة مطوّر ووردبريس الذي أعاد تطوير الموقع، وتدميره لسيو الموقع (أي ترتيبه في عمليات البحث الطبيعيّة) وهو يعمل على التصميم الجديد. ولكن ولحسن الحظ، فأخطاء مطوّري ووردبريس تتشابه. هذا المقال مُوجّه لمُلّاك المواقع والمطوّرين على حدّ سواء، ويحاول شرح الطرائق الثلاثة الرئيسيّة التي ينفّذها مطوّر ووردبريس بلا قصد، فيُضرّ بترتيب عمليات البحث الطبيعيّة للموقع. سأحاول تقديم الطريقة التي يقدر بها مُلّاك المواقع حماية أنفسهم من أخطاء المطوّرين، كما ويعلم بها المطوّرين لئلا يقعوا في أيّة مشاكل شبيهة. أخطاء ثلاثة لتتجنّبها: كيف يقتل المطوّر سيو الموقع غالبًا ما يُضرّ المطوّر عديم الكفاءة بترتيب موقع ووردبريس في نتائج البحث – يُضرّه بثلاث طرائق رئيسية. بالطبع هناك أخرى غيرها ولكن هذه هي الأساسية. (ولو كنت مهتمًا، فالزبونان اللذان راسلاني هذا الأسبوع واجها نتائج الخطأين #1 و #2 بالترتيب). نفصّل أدناه كلّ مشكلة من هذه المشاكل بذكر: وصف عام لها، وطرائق حدوثها، وعواقبها وطريقة تجنّبها في مشاريعك على الوِب. لو كنت مطوّرًا فاقرأ الآتي واحرص على ألا تتسبّب بهذه المشاكل كي لا يضع الزبائن على اسمك علامة حمراء. ولو كنت مالكًا لموقع فحاول أن تُدرك ماهيّة هذه المشاكل وأعراضها خصوصًا لو بدأت تشكّ بأن ثمة مشاكل في أداء الموقع في عمليات البحث الطبيعية. وطبعًا، كيف تحمي نفسك منها. 1. ترك خيار ”منع محركات البحث من أرشفة هذا الموقع“ مؤشّرًا في موقع ووردبريس حين يُنشر لو أمكنني فقط إزالة هذا المربّع من ووردبريس، لو أمكنني… وظيفة الخيار ”منع محركات البحث من أرشفة هذا الموقع“ (Discourage Search Engines) هي كما يقول بالضبط، فهو يُخبر محرّكات البحث بأن تتجاهل الموقع تمامًا. لو أردت تفاصيل أكثر عن طريقة عمله فرجاءً اقرأ مقالي حول الموضوع. كيف تحدث المشكلة التأشير الخطأ لهذا المربّع يحصل في حالات عدّة مختلفة، إلّا أنّ الحالة المنتشرة هي تأشيره بينما يجري تطوير الموقع، ونسيان إلغاء تأشيره حين يُنشر. حين يُؤشّر على هذا المربّع يعرض ووردبريس تنبيهًا صغيرًا يقول ”Search Engines Discouraged“، ويمكن لمالك الموقع والمطوّر ألّا يعلمان به لشهور أو حتّى سنين. نتيجة هذه المشكلة هي أنّ جوجل سيُلغي فهرسة (كما طلبت تمامًا) موقعك و”ينسى وجوده“. ما إن يرى جوجل أنّ هذا المربّع مؤشّر فستخرج من نتائج عمليات البحث الطبيعيّة تمامًا. تبعات المشكلة النتيجة؟ بالضبط ما فعلناه: سيختفي الموقع من كل عملية بحث طبيعية وتحت أيّ ظرف كان. وبالنسبة إلى عمليات التحسين هذه، فهذا يعني ”الصفر المطلق“ وليس هناك ما هو أسوأ من هذا. ولو كنت تراقب حركة الناس في موقعك بانتظام ستلاحظ تأثير هذا بسرعة. فخلال أسبوع أو اثنين (ومع احترام جوجل طلبك بعدم فهرسة الموقع) سترى حركة الناس الطبيعية (أي من نتائج البحث) انخفضت إلى حدود الصفر أو الصفر حتى. بنظرة أولى سترى أن الحركة بشكل عام انخفضت 50 أو حتى 70 بالمئة بدون أن تعرف لذلك سببًا واضحًا. ولكن، إلغاء تأشير المربّع لن يحلّ المشكلة، فجوجل يعمل بالأقدميّة. أي أن المواقع التي كان لها تقييم س لعدد من عمليّات البحث لأشهر وسنوات متواصلة ستبقى كذلك لهذه العمليات، ذلك لأنّها موجودة منذ زمن طويل، وهذا سبب من الأسباب. التقييمات تتوارث -إن صح التعبير-، فحتّى الشهر أو الشهرين بعد إلغاء الفهرسة بالإجبار سيضرّ بتقييمات الموقع ويصعّب من إصلاحه بسرعة إلى حدّ الإستحالة. يمكن القول أنّ ”مكانك في الطابور ضاع“. صحيح أنّك غادرته لبرهة قصيرة، ولكنّك الآن ستبدأ من آخره. فعلتُ هذا مرّة لأحد زبائني حين بدأت عملي، وكان أمرًا شنيعًا. إيّاك تأشير ”Discourage Search Engines“ بنفسك ودرّبها -أي نفسك- لتكون يقظة وواعية لهذا الخيار في المواقع المنشورة التي تعمل عليها. الوقاية مهما حصل وفي أيّ زمان ومكان، لا تؤشّر ”Discourage Search Engines“. لو كنت مالكًا لأحد المواقع فاطلب من المطوّر صنع ”صفحة قريبًا“ تُخفي المحتوى كما يُخفيه الخيار ذاك. كما وتيقّظ إلى ذلك التنبيه الصغير بأنه ”تمّ منع محركات البحث من أرشفة موقعك“ (Search Engines Discouraged). 2. عدم تحويل الروابط 301 حين تغيير الروابط الدائمة إن كان موقعك يحظى بحركة بحث مهولة، فعليك أن تتيقّظ لأمر آخر: تغيّرت الروابط الدائمة؟ نسبة التوتّر: ارتفعي! يشير مصطلح ”الروابط الدائمة“ إلى روابط منشوراتك وصفحاتك ”الدائمة“. هذه بعض التغييرات عليها: تنتقل صفحة النبذة من http://mysite.com/about إلى http://mysite.com/about-us. تنتقل المدوّنات من نسق http://mysite.com/article-title إلى نسق http://mysite.com/month/year/article-title. ينتقل موقعك من http://mysite.com إلى https://mysite.com. ينتقل موقعك من https://mysite.com إلى https://mynewsite.com. تكفي هذه الأمثلة، وتذكّر: التغييرات على الروابط الدائمة تحدث متى ما تغيّرت عنواين الروابط الحاليّة، ولأيّ سبب كان. وهذه ”العناوين“ هي حقًا ما يستعمله جوجل لسرد المحتوى في موقعك، تمامًا كما عنوان منزلك حيث يصل المرء إليه، وعنوان بريدك الإلكتروني حيث تصل الرسائل إليه. كيف تحدث المشكلة لنعرف كيف يمكن أن تقتل التغييرات على الروابط الدائمة سيو موقعك، نطرح السؤال: ماذا يحدث إن تغيّر أحد العناوين ولم يعرف جوجل بهذا التغيير؟ الإجابة: العنوان القديم ”يضيع“، فما من محتوًى فيه بعد الآن. وجوجل لا يعلم أين ذهب هذا المحتوى، فلا يبالي لذلك وينقل صفحات المواقع الأخرى أعلى التقييمات – المواقع التي فيها الكلمات المفتاحية التي كانت موجودة في العنوان ”الضائع“. يمكن أن تحدث هذه المشكلة في داخل موقع ووردبريس، أو بينما تنقل موقعًا من نظام آخر إلى ووردبريس. لو كنت في موقع ووردبريس موجود فعلًا فيمكن أن تغيّر أنت مالك الموقع أو أحد مطوّريك – يمكن أن تغيّر بنية روابط الموقع الدائمة، أو تغيّر رابط أحد المنشورات (أو أكثر) وتكون ضحيّة لإحدى المزايا غير المتّسقة لووردبريس بخصوص طريقة تحويله للروابط هذه. (فمثلًا، يحوّل ووردبريس تلقائيًا المنشورات ويترك الصفحات، أي أنّك إن لم تحوّلها يدويًا فستضيع تقييمات صفحاتك.) أمّا عند الانتقال من نظام آخر إلى ووردبريس، فيمكن أن ينسى المطوّر (أو لا يعلم) بأنّ عليه تحويل الروابط القديمة: about-us.html إلى store/product-name.asp، أو store/product-name.asp إلى /shop/product-name/، وهكذا دواليك. في هذه الحال ستعطب كل الروابط القديمة، وهذه مشكلة عامّة وشنيعة جدًا. لو أردت معرفة ما على المطوّر إدراكه بخصوص الشغل التقني وراء تحويل الروابط الدائمة، فقد فصّلت هذا أدناه: تبعات المشكلة تختلف تبعات هذه المشكلة حسب مقدار المحتوى الذي لم يُحوّل وكم هو مهمّ هو. التبعة المتوسّطة لهذه المشكلة هي أن تفقد ترتيب بعض العناوين غير المهمّة كثيرًا. مثلًا تنتقل /privacy-policy/ إلى /our-privacy-policy/، يأتي جوجل ويُدرك الرابط بعد أسابيع قليلة، مع ذلك فذاك غير مهمّ إذ أنّ الصفحة القديمة لن تكون بتقييم أصلًا (من يريد صفحة بيان الخصوصية؟!). أمّا التبعة الأشدّ وقعًا هي أنّك تملك موقعًا معروفًا قديمًا وتعطب أو تضيع أو ينسى جوجل روابطها كلّها. إن تركت الأمر دون علاج فأنت ببساطة تبدأ موقعك من الصّفر بنظر السيو. فكلّ ”العناوين“ الجيّدة التي وَثِق بها جوجل اختفت، والآن عليك أن تكسب ثقته من جديد بعناوين جديدة كليًا لا تترابط مع القديمة بأيّ شكل من الأشكال. يمكن للحالة الثانية أن ترمي بمبيعات إحدى المشاريع التجارية الأكثر ازدهارًا إلى القعر، إلى الصفر. هذه هي الحال التي يواجهها أحد زبائني كما وضّحت في بداية المقال. أكرّر، لو كنت تراقب من حركة الناس في الموقع بانتظام ستلاحظ تأثير هذا بسرعة خلال أسبوع أو اثنين، وسترى انخفاض الحركة بشكل عام 50 بالمئة أو أكثر. ولو دقّقت أكثر فسترى أنّ الحركة من عمليات البحث الطبيعيّة صارت فجأة صفرًا أو ما يقارب الصفر. الوقاية حتّى لو لم تدرك التقاصيل التقنيّة فعليك أن تكون دومًا يقظًا لأيّ من أراد تغيير روابط موقعك. هذا اختبار بسيط يمكنك إجرائه: اكتب الرابط الذي سيتغيّر على ورقة (أو عددًا كبيرًا منها) وركّز على صفحات موقعك الرئيسية، أي تلك التي فيها كلمات مفتاحية تساعد على ترتيب الموقع مثل أقوى المقالات تأثيرًا أو صفحات المنتجات الرئيسية في الموقع. بعد أن تحدث التغييرات، اكتب الروابط القديمة وانظر لو ظهرت الصفحات الجديدة تلقائيًا. لو ظهرت فأنت في أمان. لو لم تظهر واحدة أو أكثر وظهرت صفحة خطأ (أكان فيها ”404“ أم لا)، فلا تسكت إلّا حين حلّ المشكلة. والأفضل لو كان هناك شخصين تقنيّين يفحصان التحويلات فيضمنان أنّ التغييرات حصلت كما يجب. بصفة عامّة، تحقّق مرة واثنتان من أيّ تغيير يحصل على الروابط الدائمة. للأسف فتحويلات 301 معقّدة: فمثلًا مصاعب إصدارات http وhttps من الموقع، وإصدارات www وغير www من الموقع والشرطات المائلة النهائية تحتاج اطّلاعًا واسعًا وتيقّظًا لتعمل كما يجب. لهذا يفضّل جلب شخصين تقنيّين لذلك. فالمئة دولار التي ستدفعها لتوظّف مستقلًا مدرّبًا يفحص تحويلات 301 أثناء الترحيل سيجنّبك مئات وآلاف بل ربّما ملايين الدولارات التي ستخسرها في تقييمات البحث التي حافظ عليها الموقع. 3. إعادة كتابة المحتوى بلا اكتراث لكلماته المفتاحيّة هذه المشكلة عامّة وشاملة أكثر. صحيح أنّ المشكلتين الأولى والثانية يمكن أن يقتلا سيو الموقع، إلّا أنّ هذه تسمّمه لا أكثر. بعبارة أخرى، تظهر تبعات هذه المشكلة ببطئ أكثر وعلى نحو أقلّ، ويمكن أن يكون حلّها أسهل. ولكنّ ملاحظتها وتحرّيها أصعب بكثير، ويمكن ألّا يعلم مالك الموقع تمامًا ما يجري، أو لا يعلما بوجود مشكلة من الأساس. كيف تحدث المشكلة كيف تحدث هذه؟ كالآتي: تعيد تطوير موقع يعتمد في تقييماته على كلمات مفتاحيّة مهمّة. لا يكترث الموقع الجديد بالطريقة التي كان يُقيّم بها المحتوى القديم. يتوقّف (بمرور الوقت) تقييم الموقع على هذه الكلمات. كيف يحدث هذا؟ بطرق شتّى فظيعة. أمّا أرجحها وأجسمها هي أن المطوّر الجديد أعاد كتابة كلّ محتوى الموقع بنفسه بلا أن يهتمّ، أو يفّكر ويأمل بأداء الموقع الحالي في عمليات البحث. فلنرى معًا هذا المثال. لنقل أنّ الكلمة المفتاحيّة الأهمّ في الموقع هي ”المزارع الطبيعيّة اليابانيّة“. ولكنّ المطور لم يبحث في الكلمات المفتاحية، كما وأنّه يحبّ عبارة ”الغابات الاستوائيّة“ لنفس المنتجات بتلك الكلمة. ورأى بأنّ على الموقع استقطاب كلّ من هو في شرق آسيا، وليس فقط اليابان. وهكذا، يغيّر كلّ مرّة تتكرّر فيها ”المزارع الطبيعيّة اليابانيّة“ في الموقع إلى ”الغابات الاستوائيّة الآسيويّة“. وبعدها تضيع حركة الناس (من عمليّات البحث الطبيعيّة) في عمق الغابة. وطبعًا هذه ليست الطريقة الوحيدة بل هناك طرائق عدّة وشتّى يمكن أن يُقتل بها سيو الموقع، وهي تتخطّى الطرق الصحيحة ليبقى فيه سليمًا - تتخطّاها بمئات بل آلاف الطرائق. فلنقل مثلًا أنّ أغلب المحتوى بقي كما هو، ولكنّك بدّلت ترويسات النصوص في الموقع مثل <h1> و<h2> وعوّضتها لتكون رسومات جميلة جذّابة مصمّمة بِحِرفيّة عالية، وفيها نفس النص، لكنّك لم تضع وسوم alt لهذه الصور. بهذا سيكون احتمال تعرّض الموقع لنكسة سيو عالٍ، ولن يعرف أحدٌ أسباب ذلك إلّا المطوّرين الدارسين للأمر أو محترفي السيو. تبعات المشكلة هنا، تعتمد التبعات على طريقة تقييم المحتوى السابقة، إضافةً إلى مدى سوء المحتوى الجديد (حسب السيو) موازنةً بالقديم. (طبعًا، لو كان المحتوى الجديد أفضل من القديم فسيكون هذا الفرق حسنًا! وهذا ما نأمله… إلّا لو كنت تتبّع استراتيجية محتوى تعتمد على السيو وانتقل إلى أخرى لا تعتمد عليه، هنا لن يكون التحسين حسنًا أبدًا. لحظة، تحسين؟) يمكن ألّا يُلحظ الفرق أبدًا لو قلّ مقدار كلمات صفحة ”النبذة“ المفتاحية بسبب إعادة كتابتها بلا اكتراث، ويمكن أن يكون الفرق صاعقًا لو قرّر أحد بأنّ أكثر منتجات السوق الإلكتروني مبيعًا لا تحتاج بالضرورة إلى عنوان أو وصف، بل صور فقط… وهكذا ينزل تقييم كل منتج ستّين مرّة. كما أنّه من الصعب التكهّن بمدى تعرّض الموقع لهذه الأمور، أكان سريعًا أو بنحوٍ أبطأ. ستأتي يومًا وترى بأنّ ”التقييم صار صعبًا“ فجأةً دون سبب، ولا تدري أكان هذا بسبب عبارة البحث التي يزيد عدد المتنافسين عليها، أو أكان ثمّة خطب في الموقع تقنيًا (وسوم <title>؟ وسوم schema.org؟ سرعة الموقع، أو حتّى ”ما هذا الذي يجري بموقعي؟!“. وكأنّه يتجرّع السمّ قطرة قطرة ولا يقدر أيًّا من العامّة تشخيص علتّه. ولهذا السبب بالذات لزامٌ عليك أن تتجنّب الوقوع في هذا الوحل من الأساس. الوقاية نصيحتي هنا لتتجنّب هذه الكوارث البيئيّة هي بأنّ توظّف كاتبي محتوى مهرة يفهمون السيو. لو كان المحتوى الحالي ذا تقييم محترم في البحث فيجب أن تكون التغييرات كلّها بلا استثناء على يد شخص يعرف السيو قلبًا وقالبًا. لو لم يكن الشخص الذي يكتب محتوى موقعك محترفًا في السيو، فعليه أن يكون على اطّلاع كافٍ به كي يكون اختصاصه لاحقًا لو أراد. ولكن للأسف فالسيو نفسه (كما وتطوير الوِب) هي وظائف ينقصها الرقابة الجيدة وليست مستقرّة، وبذلك فالمعيار أنزل بكثير ممّا يجب أن يكون. وهنا يدخل الإثبات بالتعارف إلى الموضوع. فعليك أن تقبل بكاتب محتوًى رشّحه أحد الثِّقات بأنّ الكاتب يفهم السيو جيدًا، أكان من رشّحه لك مطوّرًا أو مسوّقًا تقنيًا أو كاتب إعلانات أو مالك إحدى المواقع أو سيو آخر أو أيًا كان على هذه البسيطة. إن لم تعرف مَن يمكنه أن يرشّح لك شخصًا فابحث عن سيو ذو سمعة محترمة وأجرٍ عالٍ وادفع ثمن ساعة من وقته فيتحدّث مع الشخص الذي سيدقّق الموقع كي يتأكّد أكان الشخص أهلًا بالمهمة أم لا. أو يمكنك سؤاله (هذا ذو الأجر العالي) لو يعرف أحدًا يحوّلك إليه. ابتعد عن متاهات السيو! توظيف مطوّري الوِب عمليّة فيها خطورة. فأولئك الذي يُعيدون تصميم مطعمك لا يحرقوه أبدًا، على العكس تمامًا مع مطوّري الوِب! قصّتان في أسبوع واحد عن مشاريع تجارية مزدهرة رجعت شهورًا أو أعوامًا حتّى، والسبب أخطاء المطوّرين. آمل أن تكون الآن على دراية أكبر بكوارث السيو التي عليك تجنّبها أكنت مالكًا لأحد المواقع أو مطوّرًا. كما آمل أن تكون دائمًا مُدركًا لما سيحصل من أمور آتية. خُذها من أخطاء مجرّب: لن تكسب جائزة ”لا تخف أنا ذكي وسأكون على خير حال“ لو أخذت مسؤولية سيو المشاريع التجارية الحقّة على الإنترنت. وأخيرًا، أخطّط فعلًا لمقال يواصل على هذا يشرح ما تفعل لو تضرّر موقعك من هذه المشاكل أعلاه، فترقّبه. ما هي أخطاء السيو التي أضرّت بك أو بأحد زبائنك؟ شاركنا النقاش! ترجمة -وبتصرف- للمقال ‎“My Developer Ruined My Site’s SEO”: Three Huge SEO Mistakes and How to Avoid Them لصاحبه Fred Meyer
  11. الهواتف الذكيّة والسيارات، الحواسيب الخارقة والأجهزة المنزلية، أجهزة سطح المكتب وخواديم الشركات - كلها تستعمل نظام التشغيل لينكس، النظام الذي ستراه حولك أينما أدرت وجهك. بدأ نظام التشغيل لينكس منذ منتصف التسعينات تقريبًا وقد حقّق منذئذ قاعدة مستخدمين ضخمة تجوب العالم. وكما مستخدميه، فلينكس في كل مكان: أجهزتك المحمولة، منظّمات الحرارة، السيارات، الثلاجات، أجهزة Roku لبثّ الوسائط المتعدّدة، أجهزة التلفاز، وغيرها وغيرها. ولا ننسى أنّ أغلب شبكة الإنترنت تعمل عليه، إضافةً إلى أقوى 500 حاسوب خارق في العالم، كما والبورصة أيضًا. ولكن لو نسينا لبرهة أنّه الخيار الأمثل لتشغيل الأجهزة المكتبيّة والخواديم والأنظمة المضمّنة، فنظام التشغيل لينكس هو أكثر نظام من بين الأنظمة الموجودة - أكثرها أمانًا وثقةً ويُسرًا دون متاعب تُذكر. كتبنا لك أدناه كل ما تحتاجه لتكون عارفًا وعالمًا بمنصّة لينكس. فلو كنت مبتدئًا، فلا تقلقنّ البتة. ما هو نظام التشغيل لينكس أصلًا؟ ”وندوز“ و”آي‌أو‌إس“ و”ماك أو‌إس“ هي أنظمة تشغيل، ولينكس أيضًا نظام تشغيل. وفي هذا السياق، أتعلم أنّ أكثر منصّة رائجة في كوكبنا الأرض ”أندرويد“ تعمل بنظام التشغيل لينكس؟ نظام التشغيل (ويُختصر بِـ OS) هو البرمجية التي تُدير كلّ ما يتعلّق بالعتاد في حاسوبك المكتبي أو المحمول. لنبسّطها أكثر، نظام التشغيل يُدير الاتصالات بين البرمجيّات (Software) والعتاد (Hardware) على جهازك. ودون نظام التشغيل فلن تعمل أيًا من البرامج. يعتمد أساس نظام التشغيل لينكس على هذه الأجزاء المختلفة: محمِّل الإقلاع (Bootloader): أي البرمجية التي تُدير عمليّة إقلاع/تشغيل حاسوبك. حين يرى أغلب المستخدمين شاشة البداية التي تظهر أمامهم وتختفي بعد فترة لتفتح نظام التشغيل، تعرف أنّهم قد رأوا هذا المحمِّل. النواة (Kernel): في الواقع فَإن نظام التشغيل لينكس مكوّن من أجزاء عديدة، النواة هذه واحدة منها. كما يدلّ اسمها، فهي نواة النظام والتي تُدير المعالج والذاكرة والأجهزة الداخليّة والخارجيّة. تعد النواة المستوى الأدنى من مستويات نظام التشغيل وهي حلقة الوصل الفعليّة بين البرمجيّات والعتاد. نظام التمهيد (Init System): هذا أحد الأنظمة الفرعيّة التي ”تُمهّد“ المساحة المخصّصة للمستخدم، إضافةً إلى إدارة العفاريت. أكثر هذه الأنظمة استعمالًا هو systemd ويتصادف أنّه أيضًا أكثرها جدلًا بين المستخدمين. تقع على عاتق هذا النظام إدارةَ عمليّة الإقلاع ما إن يُسلِّم محمّل الإقلاع (مثل GRUB أي ”محمِّل الإقلاع الموحّد الأعظم“ [GRand Unified Bootloader]) عمليّة الإقلاع الأولي. العفاريت (Daemons): الخدمات التي تجري في الخلفيّة (مثل الطباعة والصوت وترتيب مهام النظام وغيرها) تُسمّى بالعفاريت، ويمكن أن تبدأ إمّا أثناء الإقلاع أو بعد الدخول إلى سطح المكتب. تعمل هذه الخدمات بمنأًى عن تحكّم المستخدم المباشر، ويمكن أن تنفّذ مختلف الوظائف والعمليات والأوامر. خادوم الرسوميّات (Graphical Server): هذا أحد الأنظمة الفرعيّة الأخرى التي تعرض أيّ رسوم ورسوميّات على الشاشة لديك. النسخة الأولية منه كانت ”خادوم X“ (أو إكس/X اختصارًا)، أما الآن فالتوجه نحو Wayland وهو أفضل من نواحٍ عدّة عن سلفه ”خادوم X“. بيئة سطح المكتب (Desktop Environment): هذه هي ما يتفاعل المستخدمين معه حقًا. توجد أعداد كبيرة من بيئات سطح المكتب ليختار المستخدم ما يُريحه منها (جنوم أو القرفة أو متّة أو بانيثون أو إنلايتمنت أو كدي أو إكسفسي أو غيرها). وفي كلّ بيئة سطح مكتب عدد من التطبيقات المثبّتة مبدئيًا (مثل مدراء الملفات وأدوات الضبط ومتصفّحات الوِب والألعاب). التطبيقات: بالطبع، لا تقدّم بيئات سطح المكتب كلّ ما تحتاج من تطبيقات. فكما نظامي وندوز وماك أو‌إس، يقدّم نظام التشغيل لينكس أيضًا آلافًا مؤلّفة من البرمجيات الموثوقة عالية الكفاءة وسهلة الوصول والتثبيت. أصبحت تحتوي أغلب توزيعات لينكس الحديثة (نتطرّق إليها أسفله) أدوات تشبه ”أسواق البرامج“ تحاول تبسيط تثبيت البرمجيات وتوحيدها في مكان واحد. فمثلًا تحتوي توزيعة أوبنتو لينكس على ”مركز برمجيات أوبونتو“ (نسخة من «برمجيّات جنوم») وهو يقدّم لك آليّة سهلة للبحث بسرعة بين آلاف التطبيقات وتثبيتها، كلّه في مكان واحد. ما هي مزايا نظام التشغيل لينكس؟ يطرح أغلب الأشخاص هذا السؤال بالذات. لماذا أُتعب نفسي وأتعلّم بيئة حاسوبيّة مختلفة تمامًا عمّا أعرف، طالما نظام التشغيل -المثبّت في أغلب الأجهزة المكتبية والمحولة والخواديم- يعمل دون مشاكل تُذكر؟ لأُجيبك على هذا السؤال سأطرح سؤالًا آخر. هل حقًا يعمل نظام التشغيل الذي تستعمله الآن ”دون مشاكل تُذكر“؟ أمّ أنّك تصارع (بل وتكافح ضدّ) المصاعب المتوالية من فيروسات وبرامج ضارّة وحالات بطء وانهيارات وإصلاحات باهظة للنظام ورسوم تراخيص ثقيلة؟ لو كنت حقًا كذلك فربّما تكون لينكس المنصّة الأمثل لاحتياجاتك! فقد تطوّر نظام التشغيل لينكس وصار واحدًا من الأنظمة البيئيّة الثِّقة للحواسيب على هذه البسيطة. هذه الثِّقة زائدًا تكلفة الصفر قرش ستكون حقًا أمثل حلّ لمنصّات أسطح المكتب. نظام تشغيل مجاني كما قلتُ تمامًا، صفر قرش… أي مجانًا. يمكنك تثبيت نظام التشغيل لينكس على الحواسيب التي تريد (مهما كان العدد) دون أن تدفع ولو قرشًا لتراخيص البرمجيّات أو الخواديم. تعال نرى تكلفة خادوم لينكس موازنةً بخادوم وندوز 2016. تكلفة Standard edition من Windows Server 2016 هي 882 دولارًا (نشتريها من مايكروسوفت مباشرةً). هذا لا يشمل ترخيص الوصول للعميل (CAL) ورُخص البرمجيات الأخرى التي سيشّغلها الخادوم (مثل قاعدة البيانات وخادوم الوِب وخادوم البريد وغيرها). فمثلًا يُكلّف ترخيص CAL لمستخدم واحد لِـ Windows Server 2016 38 دولارًا. لو اضطررت إلى إضافة 10 مستخدمين فتزيد عليها 388 أخرى فقط لترخيص برمجيات الخادوم. لو كنت على خادوم لينكس فسيكون كلّ هذا مجانيًا وسهل التثبيت. بل أنّ تثبيت خادوم وِب كامل متكامل (ويشمل خادوم قواعد بيانات) عمليّة قوامها بعض النقرات أو الأوامر (لتعرف مدى بساطة ذلك طالع ”كيفية تثبيت حزم LAMP“). ألم تقتنع بعد بالصفر قرش؟ إذًا لو أخبرتك بنظام تشغيل سيعمل دون أيّة مشكلة تُذكر مهما طال استخدامك له، ما تقول؟ استعملت شخصيًا نظام التشغيل لينكس طوال 20 سنة أو يزيد (أكان لجهاز المكتب أم لمنصّة الخادوم) ولم أواجه قط أيّ مشكلة مع برامج الفِدية أو البرامج الضارة أو الفيروسات أو غيرها. فلينكس (وبصفة عامة) أقلّ تعرضًا لهذه الأخطار والاعتداءات من غيره. وحين نتحدّث عن إعادة إقلاع الخادوم، فهي مطلوبة فقط وفقط عند تحديث النواة. ولو سمعت بأنّ خواديم لينكس تعمل لسنوات وسنوات دون إعادة إقلاعها فلا تستغرب، هذا طبيعي. لو أخذت بالتحديثات المستحسن تثبيتها دوريًا، فتأكّد من ثبات النظام واستقلاليّته، وليس هذا حبرًا على ورق، بل كلام عمليّ مُثبت. بل أنّ منهجيّة ”اضبطه مرّة وانساه كل مرّة“ (Set it and forget it) هي المتّبعة في أوساط خواديم لينكس. مفتوح المصدر كما وأنّ نظام التشغيل لينكس يُوزّع برخصة مفتوحة المصدر. وهذا ما تضمنه لك فلسفات البرمجيات مفتوحة المصدر: حريّتك في تشغيل البرنامج لأيّ سبب أردته. حريّتك في دراسة أسلوب وطريقة عمل البرنامج، كما وتغييرها لتتناسب مع رغباتك. حريّتك في إعادة توزيع نُسخ البرنامج لتُساعد جارك في الانتقال. حريّتك في توزيع نُسخ البرنامج التي عدّلتها للآخرين. إدراك هذه النقاط ضروري لتفهم طبيعة المجتمع الذي يعمل على تطوير منصّة لينكس. ولا شكّ بأنّ لينكس هو نظام ”طوّره الناس، ليستعمله الناس“. كما أنّ هذه الفلسفات هي من الأسباب الرئيسية لتفضيل الناس لينكس على غيره. الحريّة، حريّة الاستخدام، حريّة الخيار، هذا ما يسعى وراءه المرء. ما هي توزيعات نظام التشغيل لينكس؟ توجد عديد من الإصدارات المختلفة من نظام التشغيل لينكس الهادفة إلى تقديم الخيار لجميع الناس مهمًا كانوا، أكانوا مستخدمين جدد أو معمّرين. أيًا كنت فستجد ”نكهة“ من لينكس تناسب متطلباتك واحتياجاتك. هذه الإصدارات هي ما نسمّيه التوزيعات (Distribution). يمكن تنزيل كلّ توزيعة من توزيعات لينكس مجانًا (إلى حدّ ما)، وحرقها على اسطوانة (أو جهاز USB) وتثبيتها (على أيّ جهاز أردت، بل وأجهزة أيضًا). من التوزيعات الشائعة للينكس: لينكس مِنت (Linux Mint) مانجارو (Manjaro) دِبيان (Debian) أوبونتو (Ubuntu) أنتيرغوس (Antergos) سولوس (Solus) فيدورا (Fedora) إليمنتري أو‌ إس (elementary OS) أوبن سوزة (openSUSE) لكلّ توزيعة منها توجّه مختلف على ماهيّة سطح المكتب. بعضها تفضّل واجهات المستخدم الحديثة (مثل جنوم وبانيثون في ”نظام تشغيل إليمنتري“)، والأخرى تفضّل البقاء مع بيئات سطح المكتب التقليدية (مثل أوبن سوزة التي تستخدم كدي). يمكنك أيضًا مطالعة موقع Distrowatch لتعرف أفضل 100 توزيعة لينكس من جهة نظر المجتمع. ولو فكّرت للحظة بأنّ الخواديم ليس لها نصيب من هذا، فأنت مخطئ! انظر: Red Hat Enterprise Linux Ubuntu Server CentOS SUSE Enterprise Linux بعض هذه التوزيعات المخصّصة للخواديم مجّاني (مثل Ubuntu Server وCentOS)، وأخرى تطلب ثمنًا لقائها (مثل Red Hat Enterprise Linux وSUSE Enterprise Linux). هذه التي تطلب الثمن تقدّم الدعم الفنّي أيضًا. ما هي توزيعة لينكس التي تناسبني؟ إجابتك على هذه الأسئلة الثلاثة تحدّد التوزيعة الأنسب لك: مدى خبرتك بالأمور التقنية والحاسوبية. أكنت تريد واجهة مستخدم حديثة أم قياسية عادية. أكنت تريده للخادوم أو للأجهزة المكتبية. لو كانت خبرتك ومهاراتك بالحاسوب بسيطة، فالأفضل أن تتّجه نحو توزيعة تناسب المبتدئين مثل Linux Mint أو Ubuntu أو Elementary OS أو Deepin. ولو كانت مهاراتك فوق المتوسّط، فستكون التوزيعات أمثال Debian وFedora أفضل لك. أمّا لو كنت ”احترفت مِهنة الحاسوب“ وإدارة الأنظمة، فتوزيعة جنتو وأمثالها طُوّرت لك خصّيصًا. لكن، لو أردت تحدّيًا لمهاراتك الفذّة أخي الخبير، فاصنع توزيعة لينكس خاصّة بك أنت، طبعًا مستعينًا بِـ Linux From Scratch. في حال كنت تبحث عن توزيعة للخواديم فقط، فالسؤال الرابع هو لو أردت واجهة سطح مكتب أم أنّك ستُدير الخادوم عبر سطر الأوامر دون واجهة. فمثلًا Ubuntu Server لا تحتوي واجهة رسوميّة، وهذا يعطيك ميزتين. الأولى هي إزاحة ثقل تحميل الرسوميات عن الخادوم، والثانية هي تعلّم سطر أوامر لينكس تعلّمًا عميقًا. يمكنك مع ذلك تثبيت حزمة واجهة رسوميّة في Ubuntu Server بأمر بسيط مثل sudo apt-get install ubuntu-desktop. غالبًا ما يختار مدراء الأنظمة التوزيعات حسب الميزات التي تقدّمها. فهل تريد توزيعة متخصّصة للخواديم تقدّم لك كل ما تحتاجه لخادومك على طبق من فضّة؟ لو أردت فستكون CentOS فضل الخيارات. أو ربما تريد توزيعة لسطح المكتب تُضيف ما تريد من برامج وحزم فيها؟ لو كان كذلك فستقدّم لك Debian أو Ubuntu Linux ما تحتاج. ننصحك بالرجوع إلى مقالة الدليل النهائي لاختيار توزيعة لينكس لمزيد من التفاصيل حول التوزيعات واختيارها. تثبيت لينكس لسان حال أغلب الناس عن تثبيت نظام التشغيل هو ”ماذا؟ صعب! مستحيل! محال!“. بينما الواقع (صدّقته أم لا) هو أنّ تثبيت نظام التشغيل لينكس هو الأسهل من بين كلّ أنظمة التشغيل. بل أنّ أغلب نُسخ لينكس تقدّم لك ما نسمّيه ”التوزيعة الحيّة“ (Live Distribution)، أي التي تشغّل منها نظام التشغيل وهو على اسطوانة CD/DVD أو على إصبع USB (فلاش ميموري) دون أن تُعدّل أيّ بايت في القرص الصلب. هكذا تعرف بمزايا التوزيعة كاملةً دون تثبتيها حتّى، وما إن تجرّبها وترتاح لها، وتقرّر وتقول ”هذه هي توزيعة أحلامي“، تنقر مرّتين أيقونة ”ثبِّت“ وسيأخذك مُرشد التثبيت في رحلة التثبيت البسيطة (فقط!). غالبًا ما تكون رحلة مُرشد التثبيت هي هذه الخطوات (نوضّح هنا عملية تثبيت Ubuntu Linux): التحضير: التأكّد من استيفاء الجهاز لمتطلّبات التثبيت. يمكن أن يسألك هذا لو أردت تثبيت برمجيّات من الأطراف الثالثة (مثل ملحقات تشغيل MP3 ومرمزات الفيديوهات وغيرها). إعداد الاتصال اللاسلكي (لو أردته): في حال كنت تستعمل حاسوبًا محمولًا (أو جهازًا يدعم الاتصال لاسلكيًا) فقد يُطلب منك الاتصال بالشبكة لتثبيت برمجيات الأطراف الثالثة والتحديثات. تخصيص المساحة للقرص الصلب: تُتيح لك هذه الخطوة اختيار المكان الذي سيُثبّت فيه نظام التشغيل، وهل تريد تثبيت لينكس مع نظام تشغيل آخر (أي ”الإقلاع المزدوج“) أو استعمال القرص الصلب كلّه أو ترقية نسخة لينكس المثبّتة أو تثبيت لينكس عوض النسخة التي لديك. المكان: أي، مكانك على الخريطة. تخطيط لوحة المفاتيح: لغات الكتابة التي تريدها في النظام. إعداد المستخدم: أي اسم المستخدم وكلمة السر. طبعًا، يمكنك ألّا تحذف كامل القرص بما فيه (مثلما ترى في الصورة أعلاه ”تحذير“)، بل تعديل بعضه وإبقاء الآخر عبر الخيار الآخر Something else (ويفضّل طبعًا تحديد مساحة لقرص التبديل/Swap تتناسب وحجم الذاكرة). وعلى سبيل الطرفة، كثير من مستعملي لينكس يواجهون هذه المشكلة أول مرة ويمسحون جميع بياناتهم المخزنة على القرص، حتّى أنا مترجم المقال فكن حذرًا إني لك من الناصحين :-). لكن هذا ليس سببًا بأنّ لينكس نظام سيئ (وبالعامية ”قمامة“)، لكنه سببٌ ”لأقرأ كامل السؤال قبل الإجابة“ :). يمكنك أيضًا تعمية القرص لتكون آمنًا من أيّ هجمات محتملة. فقط. هذا فقط. وما إن ينتهي تثبيت النظام، أعِد التشغيل واشرع العمل! لو أردت دليلًا لتثبيت لينكس معمّقًا أكثر، فطالع ” أسهل وأأمن طريقة لتثبيت لينكس وتجربته“ أو نزّل كتيّب PDF عن تثبيت لينكس من مؤسّسة لينكس نفسها. تثبيت البرمجيات في لينكس كما كان تثبيت نظام التشغيل سهلًا، فتثبيت التطبيقات سهل أيضًا. تحتوي أغلب توزيعات لينكس على ما يمكن تسميته بسوق للبرمجيات، وهو مكان موحّد يمكنك البحث فيه عن ما تريد من برامج وتثبيتها. تعتمد Ubuntu Linux (وغيرها الكثير) على «برمجيّات جنوم»، وتستعمل Elementary OS مركز AppCenter، أمّا Deepin فَـ Deepin Software Center، و openSUS أداة AppStore، وثمّة توزيعات تعتمد على Synaptic. مهما كان الاسم فالوظيفة واحدة: مكان واحد لتبحث عن برامج لينكس فيه، وتثبّتها أيضًا. وبالطبع، فهذه البرمجيات تعتمد على الواجهة الرسومية، لو لم تكن موجودة لما عملت. فلو لديك خادوم بدون واجهة فعليك استعمال واجهة سطر الأوامر لتثبيت البرامج. ولهذا سنأخذ نظرة سريعة على أداتين مختلفين لنوضّح سهولة التثبيت من سطر الأوامر، أسهل من شرب الماء ربما. تُستعمل هاتين الأداتين في التوزيعات المبنيّة على Debian، والمبنيّة على Fedora تستعمل الأولى (توزيعات دبيان) أداة apt-get لتثبيت البرمجيات، أمّا توزيعات فيدورا تستعمل أداة yum، ولكلتا الأداتين آلية عمل متشابهة سنوضّحها باستعمال أمر apt-get. فلنقل فرضًا أنّك تريد تثبيت أداة wget (أداة مفيدة جدًا لتنزيل الملفات عبر سطر الأوامر). لتثبيت هذه الأداة باستعمال apt-get، ستُدخل هذا الأمر: sudo apt-get install wget نُضيف أمر sudo لأنّ تثبيت البرمجيات يطلب امتيازات المستخدم الفائقة (مدير الجهاز). وكما دبيان، فتثبيت البرامج في توزيعات فيدورا على خطوتين: أولا su للامتيازات تلك (أي أن تكتب الأمر su وتُدخل كلمة سرّ الجذر)، بعدها: yum install wget هذا كل ما تحتاجه لتثبيت البرامج في جهاز لينكس. أكان صعبًا ومحالًا كما تخيّلته؟ لا تقل لي أنّك متردّد بعد؟! أتذكر ”كيفيّة تثبيت حزم LAMP“ التي تكلمنا عنها؟ بأمر واحد: sudo taskel بأمر واحد تُثبّت خادوم LAMP (اختصار Linux Apache MySQL PHP أي ”حزمة لينكس وأباتشي وMySQL وPHP“) متكامل على توزيعتك كانت للخواديم أم للأجهزة المكتبية. أجل، بأمر واحد فقط. سهل صحيح؟ مصادر أخرى للاستزادة توزيعات لينكس هي حقًا الخيار الأمثل لك لو أردت منصّة آمنة ثِقة لجهازك المكتبي وخادومك على الشبكة. كن على ثقة بأنّ لينكس سيُريحك وسطح المكتب من مختلف المشاكل، ويُشغّل لك خواديمك دون عناء، ويقلّل من طلباتك بحثًا للدعم إلى أدنى درجة. إليك بعض الروابط المفيدة لتستزيد عن لينكس بمعلومات تخدمك طول فترة استعمالك له: Linux.com: كل ما تحتاج لتعرف لينكس حقّ المعرفة (أخبار ودروس وغيرها). Howtoforge: دروس عن لينكس. مشروع توثيق لينكس: أدلّة تعليميّة وأسئلة شائعة وإجابات ”كيف أستطيع كذا“. قاعدة معلومات لينكس ودروس عنه: دروس وأدلّة عديدة وتدخل في الصميم. LWN.net: أخبار عن نواة لينكس وأكثر. قسم لينكس في الأكاديمية: مقالات متنوعة عن نظام التشغيل لينكس وكيفية تثبيت العديد من الخدمات والتطبيقات. مجتمع لينكس العربي كتاب سطر أوامر لينكس كتاب دليل إدارة خوادم أوبنتو كتاب دفتر مدير ديبيان كتاب الإدارة المتقدمة لجنو/لينكس كتاب أوبنتو ببساطة (تنويه: إصدار أوبنتو الذي يشرحه الكتاب قديم ولكن يمكن أخذ الفكرة العامة عنه وعن لينكس عمومًا) ترجمة -وبتصرف- للمقال What is linux?‎ من موقع linux.com