صفا الفليج
الأعضاء-
المساهمات
47 -
تاريخ الانضمام
-
تاريخ آخر زيارة
نوع المحتوى
ريادة الأعمال
البرمجة
التصميم
DevOps
التسويق والمبيعات
العمل الحر
البرامج والتطبيقات
آخر التحديثات
قصص نجاح
أسئلة وأجوبة
كتب
دورات
كل منشورات العضو صفا الفليج
-
سبق وأن مررنا على الدوال السهمية مرورًا سريعًا في مقال الدوال في الكائنات واستعمالها this ( وننصحك بالرجوع إليه وقراءته إن لم تكن قد قرأته مسبقًا) وتعرفنا على كيفية استخدامها استخدامًا أساسيًا وسنتعمق الآن فيها تعمقًا أكبر. لو ظننت الدوال السهمية هي طريقة مختصرة لكتابة الشيفرات القصيرة، فأنت مخطئ، إذ لهذه الدوال مزايا تختلف عن غيرها وتفيدنا جدًا. كثيرًا ما نواجه المواقف (في جافاسكربت بالتحديد) التي نريد أن نكتب فيها دالة صغيرة وننفّذها في مكان آخر. مثال: arr.forEaoch(func): تُنفّذ forEach الدالة func لكلّ عنصر في المصفوفة. setTimeut(func): يُنفّذ المجدول الداخلي في البيئة دالة func. …وغيرها وغيرها. هذا هو جوهر اللغة، أن نصنع دالة في مكان ونمرّرها إلى مكان آخر. وفي هذه الدوال عادةً ما لا نريد أن نترك سياقها الحالي، وهنا تأتي الفائدة المخفية للدوال السهمية. ليس للدوال السهمية مفهوم الأنا this كما نذكر من فصل دوال الكائنات، this فليس في الدوال السهمية مفهوم this، ولو حاولت الوصول إلى قيمة this فستأخذها الدالة من الخارج. فمثلًا يمكننا استعمالها للمرور على العناصر داخل تابِع للكائن: let group = { title: "Our Group", students: ["John", "Pete", "Alice"], showList() { this.students.forEach( student => alert(this.title + ': ' + student) ); } }; group.showList(); استعملنا هنا في forEach الدالة السهمية، وقيمة this.title فيها هي تمامًا القيمة التي يراها التابِع الخارجي showList، أي group.title. لو استعملنا هنا الدوال العادية فسنواجه خطأً: let group = { title: "Our Group", students: ["John", "Pete", "Alice"], showList() { this.students.forEach(function(student) { // خطأ: تعذّرت قراءة الخاصية 'title' لغير المعرّف undefined alert(this.title + ': ' + student) }); } }; group.showList(); سبب هذا الخطأ هو أنّ التابِع forEach يشغّل الدوال بتمرير this=undefined مبدئيًا، وبذلك تحاول الشيفرة الوصول إلى undefined.title. ليس لهذا أيّ تأثير على الدوال السهمية إذ ليس لها this أساسًا. لا يمكن تشغيل الدوال السهمية باستعمال new بطبيعة الحال فدون this تواجه حدًّا آخر: لا يمكنك استعمال الدوال السهمية على أنّها مُنشِئات دوال، أي لا يمكنك استدعاءها باستعمال new. الدوال السهمية والربطات هناك فرق بسيط بين الدالة السهمية => والدالة العادية التي نستدعيها باستعمال .bind(this): يُنشئ التابِع .bind(this) «نسخة مربوطة» من تلك الدالة. لا يصنع السهم => أيّ نوع من الربطات. الدالة ليس فيها this، فقط. يبحث المحرّك عن قيمة this كما يبحث عن أيّ قيمة متغير آخر: في البيئة المُعجمية الخارجية للدالة السهمية. ليس للدوال السهمية معاملات كما وأنّ الدوال السهمية ليس فيها متغير مُعاملات arguments. وهذا أمر رائع حين نتعامل مع المُزخرِفات إذ نُمرّر الاستدعاء حاملًا قيمة this الحالية مع المُعاملات arguments. فمثلًا هنا تأخذ defer(f, ms) دالةً وتُعيد غِلافًا (Wrapper) عليها تُؤجّل الاستدعاء بالمليثوان ms الممرّرة: function defer(f, ms) { return function() { setTimeout(() => f.apply(this, arguments), ms) }; } function sayHi(who) { alert('Hello, ' + who); } let sayHiDeferred = defer(sayHi, 2000); sayHiDeferred("John"); // Hello, John بعد مرور ثانيتين يمكن كتابة نفس الشيفرة دون استعمال دالة سهمية هكذا: function defer(f, ms) { return function(...args) { let ctx = this; setTimeout(function() { return f.apply(ctx, args); }, ms); }; } هنا لزم أن نصنع المتغيرين الإضافيين args و ctx لتقدر الدالة في setTimeout على أخذ قيمهما. ملخص ليس للدوال السهمية: لا this. ولا arguments. ولا يمكن استدعائها باستعمال new. وليس فيها super… لم نشرح ذلك بعد ولكنّا سنفعل في الفصل «وراثة الأصناف». ليس فيها هذا كله لأنّ الغرض منها كتابة شيفرات قصيرة ليس لها سياق تعتمد عليه بل سياقًا تأخذه، وهنا حين تتألّق هذه الدوال. ترجمة -وبتصرف- للفصل Arrow functions revisited من كتاب The JavaScript language
-
ثمّة مشكلة معروفة تواجهنا متى مرّرنا توابِع الكائنات على أنّها ردود نداء (كما نفعل مع setTimeout)، هي ضياع هويّة الأنا this. سنرى في هذا الفصل طرائق إصلاح هذه المشكلة. ضياع الأنا (الكلمة المفتاحية this) رأينا قبل الآن أمثلة كيف ضاعت قيمة this. فما نلبث أن مرّرنا التابِع إلى مكان آخر منفصلًا عن كائنه، ضاع this. إليك ظواهر هذه المشكلة باستعمال setTimeout مثلًا: let user = { firstName: "John", sayHi() { alert(`Hello, ${this.firstName}!`); } }; setTimeout(user.sayHi, 1000); // Hello, undefined! كما رأينا في ناتج الشيفرة، لم نرحّب بالأخ John (كما أردنا باستعمال this.firstName)، بل بالأخ غير المعرّف undefined! هذا لأنّ التابِع setTimeout استلم الدالة user.sayHi منفصلةً عن كائنها. يمكن أن نكتب السطر الأخير هكذا: let f = user.sayHi; setTimeout(f, 1000); // ضاع سياق المستخدم user بالمناسبة فالتابِع setTimeout داخل المتصفّحات يختلف قليلًا، إذ يضبط this=window حين نستدعي الدالة (بينما في Node.js يصير this هو ذاته كائن المؤقّت، ولكنّ هذا ليس بالأمر المهم الآن). يعني ذلك بأنّ this.firstName هنا هي فعليًا window.firstName، وهذا المتغير غير موجود. عادةً ما تصير this غير معرّفة undefined في الحالات الأخرى. كثيرًا ما نواجه هذه المسألة ونحن نكتب الشيفرة: نريد أن نمرّر تابِع الدالة إلى مكان آخر (مثل هنا، مرّرناه للمُجدول) حيث سيُستدعى من هناك. كيف لنا أن نتأكّد بأن يُستدعى في سياقه الصحيح؟ الحل رقم واحد: نستعمل دالة مغلفة أسهل الحلول هو استعمال دالة غالِفة Wrapping function: let user = { firstName: "John", sayHi() { alert(`Hello, ${this.firstName}!`); } }; setTimeout(function() { user.sayHi(); // Hello, John! }, 1000); الآن اكتملت المهمة إذ استلمنا المستخدم user من البيئة المُعجمية الخارجية، وثمّ استدعينا التابِع كما العادة. إليك ذات المهمة بأسطر أقل: setTimeout(() => user.sayHi(), 1000); // Hello, John! ممتازة جدًا، ولكن ستظهر لنا نقطة ضعف في بنية الشيفرة. ماذا لو حدث وتغيّرت قيمة user قبل أن تعمل setTimeout؟ (لا تنسَ التأخير، ثانية كاملة!) حينها سنجد أنّا استدعينا الكائن الخطأ دون أن ندري! let user = { firstName: "John", sayHi() { alert(`Hello, ${this.firstName}!`); } }; setTimeout(() => user.sayHi(), 1000); // ...تغيّرت قيمة user خلال تلك الثانية user = { sayHi() { alert("Another user in setTimeout!"); } }; // setTimeout! هناك مستخدم آخر داخل التابِع الحل الثاني سيضمن لنا ألّا تحدث هكذا أمور غير متوقّعة. الحل رقم اثنين: ربطة تقدّم لنا الدوال تابِعًا مضمّنًا في اللغة باسم bind يتيح لنا ضبط قيمة this. إليك صياغته الأساسية: // ستأتي الصياغة المعقّدة لاحقًا لا تقلق let boundFunc = func.bind(context); ناتِج التابِع func.bind(context) هو «كائن دخيل» يشبه الدالة ويمكن لنا استدعائه على أنّه دالة، وسيمرّر هذا الاستدعاء إلى func بعدما يضبط this=context من خلف الستار. أي بعبارة أخرى، لو استدعينا boundFunc فكأنّما استدعينا func بعدما ضبطنا قيمة this. إليك مثالًا تمرّر فيه funcUser الاستدعاء إلى func بضبط this=user: let user = { firstName: "John" }; function func() { alert(this.firstName); } let funcUser = func.bind(user); funcUser(); // John رأينا «النسخة الرابطة» من func، func.bind(user) بعد ضبط this=user. كما أنّ المُعاملات كلّها تُمرّر إلى دالة func الأًصلية «كما هي». مثال: let user = { firstName: "John" }; function func(phrase) { alert(phrase + ', ' + this.firstName); } // نربط this إلى user let funcUser = func.bind(user); funcUser("Hello"); // Hello, John (مُرّر المُعامل "Hello" كما وُضبط this=user) فلنجرّب الآن مع تابع لكائن: let user = { firstName: "John", sayHi() { alert(`Hello, ${this.firstName}!`); } }; let sayHi = user.sayHi.bind(user); // (*) // يمكن أن نشغّلها دون وجود كائن sayHi(); // Hello, John! setTimeout(sayHi, 1000); // Hello, John! // حتّى لو تغيّرت قيمة user خلال تلك الثانية // فما زالت تستعمل sayHi القيمة التي ربطناها قبلًا user = { sayHi() { alert("Another user in setTimeout!"); } }; أخذنا في السطر (*) التابِع user.sayHi وربطناه مع المستخدم user. ندعو الدالة sayHi بالدالة «المربوطة» حيث يمكن أن نستدعيها لوحدها هكذا أو نمرّرها إلى setTimeout. مهما فعلًا فسيكون السياق صحيحًا كما نريد. نرى هنا أنّ المُعاملات مُرّرت «كما هي» وما ضبطه bind هو قيمة this فقط: let user = { firstName: "John", say(phrase) { alert(`${phrase}, ${this.firstName}!`); } }; let say = user.say.bind(user); say("Hello"); // Hello, John! (مُرّر المُعامل "Hello" إلى say) say("Bye"); // Bye, John! (مُرّر المعامل "Bye" إلى say) تابِع مفيد: bindAll لو كان للكائن توابِع كثيرة وأردنا تمريرها هنا وهناك بكثرة، فربّما نربطها كلّها في حلقة: for (let key in user) { if (typeof user[key] == 'function') { user[key] = user[key].bind(user); } } كما تقدّم لنا مكتبات جافاسكربت دوال للربط الجماعي لتسهيل الأمور، مثل _.bindAll(obj) في المكتبة lodash. الدوال الجزئية طوال هذه الفترة لم نُناقش شيئًا إلّا ربط this. لنُضيف شيئًا آخر على الطاولة. يمكن أيضًا أن نربط المُعاملات وليس this فحسب. صحيح أنّا نادرًا ما نفعل ذلك إلّا أنّ الأمر مفيد في أحيان عصيبة. صياغة bind الكاملة: let bound = func.bind(context, [arg1], [arg2], ...); وهي تسمح لنا بربط السياق ليكون this والمُعاملات الأولى في الدالة. نرى مثالًا: دالة ضرب mul(a, b): function mul(a, b) { return a * b; } فلنستعمل bind لنصنع دالة «ضرب في اثنين» double تتّخذ تلك أساسًا لها: function mul(a, b) { return a * b; } let double = mul.bind(null, 2); alert( double(3) ); // = mul(2, 3) = 6 alert( double(4) ); // = mul(2, 4) = 8 alert( double(5) ); // = mul(2, 5) = 10 يصنع استدعاء mul.bind(null, 2) دالةً جديدة double تُمرّر الاستدعاءات إلى mul وتضبط null ليكون السياق و2 ليكون المُعامل الأول. الباقي من مُعاملات يُمرّر «كما هو». هذا ما نسمّيه باستعمال الدوال الجزئية -- أن نصنع دالة بعد ضبط بعض مُعاملات واحدة غيرها. لاحظ هنا بأنّا لا نستعمل this هنا أصلًا… ولكنّ التابِع bind يطلبه فعلينا تقديم شيء (وكان null مثلًا). الدالة triple أسفله تضرب القيمة في ثلاثة: function mul(a, b) { return a * b; } let triple = mul.bind(null, 3); alert( triple(3) ); // = mul(3, 3) = 9 alert( triple(4) ); // = mul(3, 4) = 12 alert( triple(5) ); // = mul(3, 5) = 15 ولكن لماذا نصنع الدوال الجزئية أصلًا، وعادةً؟! الفائدة هي إنشاء دالة مستقلة لها اسم سهل القراءة (double أو triple)، فنستعملها دون تقديم المُعامل الأول في كلّ مرة إذ ضبطنا قيمته باستعمال bind. وهناك حالات أخرى يفيدنا الاستعمال الجزئي هذا حين نحتاج نسخة أكثر تحديدًا من دالة عامّة جدًا، ليسهُل استعمالها فقط. فمثلًا يمكن أن نصنع الدالة send(from, to, text). وبعدها في كائن المستخدم user نصنع نسخة جزئية عنها: sendTo(to, text) تُرسل النصّ من المستخدم الحالي. الجزئية، بدون السياق ماذا لو أردنا أن نضبط بعض المُعاملات ولكن دون السياق this؟ مثلًا نستعملها لتابِع أحد الكائنات. تابِع bind الأصيل في اللغة لا يسمح بذلك، ومستحيل أن نُزيل السياق ونضع المُعاملات فقط. لكن لحسن الحظ فيمكننا صنع دالة مُساعدة partial تربط المُعاملات فقط. هكذا تمامًا: function partial(func, ...argsBound) { return function(...args) { // (*) return func.call(this, ...argsBound, ...args); } } // الاستعمال: let user = { firstName: "John", say(time, phrase) { alert(`[${time}] ${this.firstName}: ${phrase}!`); } }; // نُضيف تابِعًا جزئيًا بعد ضبط الوقت user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes()); user.sayNow("Hello"); // وسيظهر ما يشبه الآتي: // [10:00] John: Hello! ناتِج استدعائنا للدالة partial(func[, arg1, arg2...]) هو غِلاف (*) يستدعي الدالة func هكذا: يترك this كما هو (فتكون قيمته user داخل الاستدعاء user.sayNow) ثمّ يمرّر لها ...argsBound: أي المُعاملات من استدعاء partial ("10:00") وثمّ يمرّر لها ...args: المُعاملات الممرّرة للغِلاف ("Hello") ساعدنا مُعامل التوزيع كثيرًا هنا، أم لا؟ كما أنّ هناك شيفرة _.partial في المكتبة lodash. ملخص يُعيد التابِع func.bind(context, ...args) «نسخة مربوطة» من الدالة func بعد ضبط سياقها this ومُعاملاتها الأولى (في حال مرّرناها). عادةً ما نستعمل bind لنضبط this داخل تابِع لأحد الكائنات، فيمكن أن نمرّر التابِع ذلك إلى مكان آخر، مثلًا إلى setTimeout. وحين نضبط بعضًا من مُعاملات إحدى الدوال، يكون الناتج (وهو أكثر تفصيلًا) دالةً ندعوها بالدالة الجزئية أو المطبّقة بنحوٍ جزئي partially applied. تُفيدنا هذه الدوال الجزئية حين لا نريد تكرار ذات الوسيط مرارًا وتكرارًا، مثل دالة send(from, to) حيث يجب أن يبقى from كما هو في مهمّتنا هذه، فنأخذ دالة جزئية ونتعامل بها. تمارين دالة ربط على أنها تابِع الأهمية: 5 ما ناتج هذه الشيفرة؟ function f() { alert( this ); // ؟ } let user = { g: f.bind(null) }; user.g(); الحل الجواب هو: null. سياق دالة الربط مكتوب في الشيفرة (hard-coded) ولا يمكن تغييره لاحقًا بأيّ شكل من الأشكال. فحتّى لو شغّلنا user.g() فستُستدعى الدالة الأصلية بضبط this=null. ربطة ثانية الأهمية: 5 هي يمكن أن نغيّر قيمة this باستعمال ربطة إضافية؟ ما ناتج هذه الشيفرة؟ function f() { alert(this.name); } f = f.bind( {name: "John"} ).bind( {name: "Ann" } ); f(); الحل الجواب هو: John. function f() { alert(this.name); } f = f.bind( {name: "John"} ).bind( {name: "Pete"} ); f(); // John لا يتذكّر كائن دالة الربط «الدخيل» (الذي يُعيده f.bind(...)) السياق (مع الوُسطاء إن مُرّرت) - لا يتذكّر هذا كلّه إلى وقت إنشاء الكائن. أي: لا يمكن إعادة ربط الدوال. خاصية الدالة بعد الربط الأهمية: 5 تمتلك خاصية إحدى الدوال قيمة ما. هل ستتغيّر بعد bind؟ نعم، لماذا؟ لا، لماذا؟ function sayHi() { alert( this.name ); } sayHi.test = 5; let bound = sayHi.bind({ name: "John" }); alert( bound.test ); // ما الناتج؟ لماذا؟ الحل الجواب هو: undefined. ناتِج bind هو كائن آخر، وليس في هذا الكائن خاصية test. أصلِح هذه الدالة التي يضيع this منها الأهمية: 5 على الاستدعاء askPassword() في الشيفرة أسفله فحص كلمة السر، ثمّ استدعاء user.loginOk/loginFail حسب نتيجة الفحص. ولكن أثناء التنفيذ نرى خطأً. لماذا؟ أصلِح الجزء الذي فيه (*) لتعمل الشيفرة كما يجب (تغيير بقية الأسطر ممنوع). function askPassword(ok, fail) { let password = prompt("Password?", ''); if (password == "rockstar") ok(); else fail(); } let user = { name: 'John', loginOk() { alert(`${this.name} logged in`); }, loginFail() { alert(`${this.name} failed to log in`); }, }; askPassword(user.loginOk, user.loginFail); // (*) الحل سبب الخطأ هو أنّ الدالة ask تستلم الدالتين loginOk/loginFail دون كائنيهما. فمتى ما استدعتهما، تُعدّ this=undefined بطبيعتها. علينا ربط السياق! function askPassword(ok, fail) { let password = prompt("Password?", ''); if (password == "rockstar") ok(); else fail(); } let user = { name: 'John', loginOk() { alert(`${this.name} logged in`); }, loginFail() { alert(`${this.name} failed to log in`); }, }; // (*)\maskPassword(user.loginOk.bind(user), user.loginFail.bind(user)); الآن صارت تعمل. أو، بطريقة أخرى: //... askPassword(() => user.loginOk(), () => user.loginFail()); هذه الشيفرة تعمل وعادةً ما تكون سهلة القراءة أيضًا. ولكنّها في حالات أكثر تعقيدًا تصير أقلّ موثوقية، مثل لو تغيّر المتغير user بعدما استُدعيت الدالة askPassword وقبل أن يُجيب الزائر على الاستدعاء () => user.loginOk(). استعمال الدوال الجزئية لولوج المستخدم هذا التمرين معقّد أكثر من سابقه، بقليل. هنا تعدّل كائن user، فصار فيه بدل الدالتين loginOk/loginFail دالة واحدة user.login(true/false). ما الأشياء التي نمرّرها إلى askPassword في الشيفرة أسفله فتستدعي user.login(true) باستعمال ok وتستدعي user.login(false) باستعمال fail؟ function askPassword(ok, fail) { let password = prompt("Password?", ''); if (password == "rockstar") ok(); else fail(); } let user = { name: 'John', login(result) { alert( this.name + (result ? ' logged in' : ' failed to log in') ); } }; askPassword(?, ?); // ؟ (*) يجب أن تعدّل الجزء الذي عليه (*) فقط لا غير. الحل نستعمل دالة غالِفة… سهمية لو أردنا التفصيل: askPassword(() => user.login(true), () => user.login(false)); هكذا تأخذ user من المتغيرات الخارجية وتُشغّل الدوال بالطريقة العادية. أو نصنع دالة جزئية من user.login تستعمل user سياقًا لها ونضع مُعاملها الأول كما يجب: askPassword(user.login.bind(user, true), user.login.bind(user, false)); ترجمة -وبتصرف- للفصل Function binding من كتاب The JavaScript language
-
تقدّم لنا لغة جافاسكربت مرونة عالية غير مسبوقة في التعامل مع الدوال، إذ يمكننا تمريرها أو استعمالها على أنّها كائنات. والآن سنرى كيف نمرر الاستدعاءات بينها وكيف نزخرفها. خبيئة من خلف الستار لنقل بأنّ أمامنا الدالة الثقيلة على المعالج slow(x) بينما نتائجها مستقرة، أي لنقل بأنّنا لو مرّرنا ذات x، فسنجد ذات النتيجة دومًا. لو استدعينا هذه الدالة مرارًا وتكرارًا، فالأفضل لو خبّئنا (أي تذكّرنا) ناتجها لئلا يذهب الوقت سدًى لإجراء ذات الحسابات. ولكن، بدل إضافة هذه الميزة في دالة slow() نفسها، سنُنشئ دالة غالِفة تُضيف ميزة الخبيئة هذه. سنرى أسفله مدى فوائد هذا الأمر. إليك الشيفرة أولًا، وبعدها الشرح: function slow(x) { // هنا مهمّة ثقيلة تُهلك المعالج alert(`Called with ${x}`); return x; } function cachingDecorator(func) { let cache = new Map(); return function(x) { if (cache.has(x)) { // لو وجدنا هذا المفتاح في الخبيئة return cache.get(x); // نقرأ النتيجة منها } let result = func(x); // وإلّا نستدعي الدالة cache.set(x, result); // ثمّ نُخبّئ (نتذكّر) ناتجها return result; }; } slow = cachingDecorator(slow); alert( slow(1) ); // خبّأنا slow(1) alert( "Again: " + slow(1) ); // ذات الناتج alert( slow(2) ); // خبّأنا slow(2) alert( "Again: " + slow(2) ); // ذات ناتج السطر السابق في الشيفرة أعلاه، ندعو cachingDecorator بالمُزخرِف (decorator): وهي دالة خاصّة تأخذ دالة أخرى مُعاملًا وتعدّل على سلوكها. الفكرة هنا هي استدعاء cachingDecorator لأيّ دالة أردنا، وستُعيد لنا غِلاف الخبيئة ذاك. الفكرة هذه رائعة إذ يمكن أن نكون أمام مئات من الدوال التي يمكن أن تستغلّ هذه الميزة، وكلّ ما علينا فعله هو إضافة cachingDecorator عليها. كما وأنّا نحافظ على الشيفرة أبسط بفصل ميزة الخبيئة عن مهمّة الدالة الفعلية. ناتج cachingDecorator(func) هو «غِلاف» يُعيد الدالة function(x) التي «تُغلّف» استدعاء func(x) داخل شيفرة الخبيئة: الشيفرات الخارجية لا ترى أيّ تغيير على دالة slow المُغلّفة. ما فعلناه هو تعزيز سلوكها بميزة الخبيئة. إذًا نُلخّص: ثمّة فوائد عدّة لاستعمال cachingDecorator منفصلًا بدل تعديل شيفرة الدالة slow نفسها: إعادة استعمال cachingDecorator، فنُضيفه على دوال أخرى. فصل شيفرة الخبيئة فلا تزيد من تعقيد دالة slow نفسها (هذا لو كانت معقّدة). إمكانية إضافة أكثر من مُزخرف عند الحاجة (سنرى ذلك لاحقًا). استعمال func.call لأخذ السياق لا ينفع مُزخرِف الخبيئة الذي شرحناه مع توابِع الكائنات. فمثلًا في الشيفرة أسفله، سيتوقّف عمل worker.slow() بعد هذه الزخرفة: // هيًا نُضف ميزة الخبيئة إلى worker.slow let worker = { someMethod() { return 1; }, slow(x) { // أمامنا مهمّة ثقيلة على المعالج هنا alert("Called with " + x); return x * this.someMethod(); // (*) } }; // نفس الشيفرة أعلاه function cachingDecorator(func) { let cache = new Map(); return function(x) { if (cache.has(x)) { return cache.get(x); } let result = func(x); // (**) cache.set(x, result); return result; }; } alert( worker.slow(1) ); // التابِع الأصلي يعمل كما ينبغي worker.slow = cachingDecorator(worker.slow); // وقت الخبيئة alert( worker.slow(2) ); // لاااا! خطأ: تعذّرت قراءة الخاصية 'someMethod' في undefined مكان الخطأ هو السطر (*) الذي يحاول الوصول إلى this.someMethod ويفشل فشلًا ذريعًا. هل تعرف السبب؟ السبب هو أنّ الغِلاف يستدعي الدالة الأصلية هكذا func(x) في السطر (**). وحين نستدعيها هكذا تستلم الدالة this = undefined. سنرى ما يشبه هذا الخطأ لو شغّلنا هذه الشيفرة: let func = worker.slow; func(2); إذًا… يُمرّر الغِلاف الاستدعاء إلى التابِع الأصلي دون السياق this، بهذا يحصل الخطأ. وقت الإصلاح. ثمّة تابِع دوال مضمّن في اللغة باسم func.call(context, …args) يتيح لنا استدعاء الدالة صياغته هي: func.call(context, arg1, arg2, ...) يُشغّل التابِع الدالةَ func بعد تمرير المُعامل الأول (وهو this) وثمّ مُعاملاتها. للتبسيط، هذين الاستدعاءين لا يفرقان بشيء في التنفيذ: func(1, 2, 3); func.call(obj, 1, 2, 3) فكلاهما يستدعي func بالمُعاملات 1 و2 و3. الفرق الوحيد هو أنّ func.call تضبط قيمة this على obj علاوةً على ذلك. لنأخذ مثالًا. في الشيفرة أسفله نستدعي sayHi بسياق كائنات أخرى: يُشغّل sayHi.call(user) الدالةَ sayHi ويُمرّر this=user، ثمّ في السطر التالي يضبط this=admin: function sayHi() { alert(this.name); } let user = { name: "John" }; let admin = { name: "Admin" }; // نستعمل call لنمرّر مختلف الكائنات على أنّها this: sayHi.call( user ); // this = John sayHi.call( admin ); // this = Admin وهنا نستدعي call لتستدعي say بالسياق والعبارة المُمرّرتين: function say(phrase) { alert(this.name + ': ' + phrase); } let user = { name: "John" }; // الكائن user يصير this وتصير Hello المُعامل الأول say.call( user, "Hello" ); // John: Hello في حالتنا نحن، يمكن استعمال call في الغِلاف ليُمرّر السياق إلى الدالة الأصلية: let worker = { someMethod() { return 1; }, slow(x) { alert("Called with " + x); return x * this.someMethod(); // (*) } }; function cachingDecorator(func) { let cache = new Map(); return function(x) { if (cache.has(x)) { return cache.get(x); } let result = func.call(this, x); // هكذا نُمرّر «this» كما ينبغي cache.set(x, result); return result; }; } worker.slow = cachingDecorator(worker.slow); // والآن نُضيف الخبيئة alert( worker.slow(2) ); // يعمل alert( worker.slow(2) ); // يعمل ولا يستدعي التابِع الأصلي (إذ القيمة مُخبّأة) الآن يعمل كلّ شيء كما نريد. لنوضّح الأمر أكثر، لنرى بالتفصيل المملّ تمريرات this من هنا إلى هناك: بعد الزخرفة، يصير worker.slow الغِلاف function (x) { ... }. لذا حين نُنفّذ worker.slow(2)، يأخذ الغِلاف القيمةَ 2 وسيطًا ويضبط this=worker (وهو الكائن قبل النقطة). في الغِلاف (باعتبار أنّ النتيجة لم تُخبّأ بعد)، تُمرّر func.call(this, x) قيمة this الحالية (وهي worker) مع المُعامل الحالي (2) - كلّه إلى التابِع الأصلي. استعمال أكثر من وسيط داخل func.apply الآن صار وقت تعميم cachingDecorator على العالم. كنّا إلى هنا نستعملها مع الدوال التي تأخذ مُعاملًا واحدًا فقط. وماذا لو أردنا تخبئة التابِع worker.slow الذي يأخذ أكثر من مُعامل؟ let worker = { slow(min, max) { return min + max; // نُعدّها عملية تستنزف المعالج } }; // علينا تذكّر الاستدعاءات بنفس المُعامل هنا worker.slow = cachingDecorator(worker.slow); كنّا سابقًا نستعمل cache.set(x, result) (حين تعاملنا مع المُعامل الوحيد x) لنحفظ الناتج، ونستعمل cache.get(x) لنجلب الناتج. أمّا الآن فعلينا تذكّر ناتج مجموعة مُعاملات (min,max). الخارطة Map لا تأخذ المفاتيح إلّا بقيمة واحدة. ثمّة أمامنا أكثر من حلّ: كتابة بنية بيانات جديدة تشبه الخرائط (أو استعمال واحدة من طرف ثالث) يمكن استعمالها لأكثر من أمر وتسمح لنا بتخزين أكثر من مفتاح. استعمال الخرائط المتداخلة: تصير cache.set(min) خارطة تُخزّن الزوجين (max, result). ويمكن أن نأخذ الناتج result باستعمال cache.get(min).get(max). دمج القيمتين في واحدة. في حالتنا هذه يمكن استعمال السلسلة النصية "min,max" لتكون مفتاح Map. ويمكن أن نقدّم للمُزخرِف دالة عنونة Hashing يمكنها صناعة قيمة من أكثر من قيمة، فيصير الأمر أسهل. أغلب التطبيقات العملية تَعدّ الحل الثالث كافيًا، ولهذا سنستعمله هنا. علينا أيضًا استبدال التابِع func.call(this, x) بالتابِع func.call(this, ...arguments) كي نُمرّر كلّ المُعاملات إلى استدعاء الدالة المُغلّفة لا الأولى فقط. رحّب بالمُزخرف cachingDecorator الجديد، أكثر قوة وأناقة: let worker = { slow(min, max) { alert(`Called with ${min},${max}`); return min + max; } }; function cachingDecorator(func, hash) { let cache = new Map(); return function() { let key = hash(arguments); // (*) if (cache.has(key)) { return cache.get(key); } let result = func.call(this, ...arguments); // (**) cache.set(key, result); return result; }; } function hash(args) { return args[0] + ',' + args[1]; } worker.slow = cachingDecorator(worker.slow, hash); alert( worker.slow(3, 5) ); // يعمل alert( "Again " + worker.slow(3, 5) ); // نفس الناتج (خبّأناه) الآن صار يعمل مهما كان عدد المُعاملات (ولكن علينا تعديل دالة العنونة لتسمح هي أيضًا بالمُعاملات أيًا كان عددها. سنشره أسفله إحدى الطرائق الجميلة لإنجاز هذه المهمة). أمامنا تعديلان اثنان: في السطر(*)، نستدعي hash لتصنع مفتاحًا واحدًا من arguments. نستعمل هنا دالة «دمج» بسيطة تحوّل المُعاملان (3, 5) إلى المفتاح "3,5". لو كانت الحالة لديك أكثر تعقيدًا، فتحتاج إلى دوال عنونة أخرى. ثمّ يستعمل (**) التابِع func.call(this, ...arguments) لتمرير السياق وكلّ المُعاملات التي استلمها الغِلاف (وليس الأول فقط) - كله إلى الدالة الأصلية. يمكننا بدل استعمال func.call(this, ...arguments) استغلال func.apply(this, arguments). صياغة هذا التابِع المبني في اللغة func.apply هي: func.apply(context, args) يُشغّل التابِع الدالةَ func بضبط this=context واستعمال الكائن الشبيه بالمصفوفات args قائمةً بالمُعطيات للدالة. الفارق الوحيد بين call وapply هي أنّ الأوّل يتوقّع قائمة بالمُعطيات بينما الثاني يأخذ كائنًا شبيهًا بالمصفوفات يحويها. أي أنّ الاستدعاءين الآتين متساويين تقريبًا: func.call(context, ...args); // نمرّر الكائن قائمةً بمُعامل التوزيع func.apply(context, args); // نفس الفكرة باستعمال apply ولكن هناك فرق بسيط واحد: يُتيح لنا مُعامل التوزيع ... تمرير المُتعدَّد args قائمةً إلى call. لا يقبل apply إلّا مُعامل args شبيه بالمصفوفات. أي أنّ هذين الاستدعاءين يُكمّلان بعضهما البعض. لو توقّعنا وصول مُتعدَّد فنستعمل call، ولو توقّعنا شبيهًا بالمصفوفات نستعمل apply. أمّا الكائنات المُتعدَّدة والشبيهة بالمصفوفات (مثل المصفوفات الحقيقية)، فيمكننا نظريًا استعمال أيّ من الاثنين، إلّا أنّ apply سيكون أسرع غالبًا إذ أنّ مُعظم محرّكات جافاسكربت تحسّن أدائه داخليًا أكثر من call. يُدى تمرير كافة المُعاملات (مع السياق) من دالة إلى أخرى بتمرير الاستدعاء. إليك أبسط صوره: let wrapper = function() { return func.apply(this, arguments); }; حين تستدعي أيّ شيفرة خارجية wrapper محال أن تفرّق بين استدعائها واستدعاء الدالة الأصلية func. استعارة التوابِع أمّا الآن لنحسّن دالة العنونة قليلًا: function hash(args) { return args[0] + ',' + args[1]; } لا تعمل الدالة حاليًا إلّا على مُعاملين اثنين، وسيكون رائعًا لو أمكن أن ندمج أيّ عدد من args. أوّل حلّ نفكّر به هو استعمال التابِع arr.join: function hash(args) { return args.join(); } ولكن… للأسف فهذا لن ينفع، إذ نستدعي hash(arguments) بتمرير كائن المُعاملات arguments المُتعدَّد والشبيه بالمصفوفات… إلّا أنّه ليس بمصفوفة حقيقية. بذلك استدعاء join سيفشل كما نرى أسفله: function hash() { alert( arguments.join() ); // خطأ: arguments.join ليست بدالة } hash(1, 2); مع ذلك فما زال هناك طريقة سهلة لضمّ عناصر المصفوفة: function hash() { alert( [].join.call(arguments) ); // 1,2 } hash(1, 2); ندعو هذه الخدعة باستعارة التوابِع. فيها نأخذ (أي نستعير) تابِع الضمّ من المصفوفات العادية ([].join) ونستعمل [].join.call لتشغيله داخل سياق arguments. ولكن، لمَ تعمل أصلًا؟ هذا بسبب بساطة الخوارزمية الداخلية للتابِع الأصيل arr.join(glue) في اللغة. أقتبس -بتصرّف خفيف جدًا- من مواصفات اللغة: لمّا أنّ glue هو المُعامل الأول، ولو لم تكن هناك مُعاملات فهو ",". لمّا أنّ result هي سلسلة نصية فارغة. أضِف this[0] إلى نهاية result. أضِف glue وthis[1]. أضِف glue وthis[2]. …كرّر حتّى يتنهي ضمّ العناصر الـ this.length. أعِد result. إذًا فهو يأخذ this ويضمّ this[0] ثمّ this[1] وهكذا معًا. كتب المطوّرون التابِع بهذه الطريقة عمدًا ليسمح أن تكون this أيّ شبيه بالمصفوفات (ليست مصادفة إذ تتبع كثير من التوابِع هذه الممارسة). لهذا يعمل التابِع حين يكون this=arguments. المزخرفات decorators وخاصيات الدوال استبدال الدوال أو التوابِع بأخرى مُزخرفة هو أمر آمن عادةً، ولكن باستثناء صغير: لو احتوت الدالة الأًلية على خاصيات (مثل func.calledCount) فلن تقدّمها الدالة المُزخرفة، إذ أنّها غِلاف على الدالة الأصلية. علينا بذلك أن نحذر في هذه الحالة. نأخذ المثال أعلاه مثالًا، لو احتوت الدالة slow أيّ خاصيات فلن يحتوي الغِلاف cachingDecorator(slow) عليها. يمكن أن تقدّم لنا بعض المُزخرِفات خاصيات خاصة بها. فمثلًا يمكن أن يعدّ المُزخرِف كم مرّة عملت الدالة وكم من وقت أخذ ذلك، وتقدّم لنا خاصيات لنرى هذه لمعلومات. توجد طريقة لإنشاء مُزخرِفات تحتفظ بميزة الوصول إلى خاصيات الدوال، ولكنّها تطلب استعمال الكائن الوسيط Proxy لتغليف الدوال. سنشرح هذا الكائن لاحقًا في قسم تغليف الدوال: apply. ملخص تُعدّ المزخرفات أغلفة حول الدوال فتعدّل سلوكها، بينما المهمة الأساس مرهونة بالدالة نفسها. يمكن عدّ المُزخرِفات «مزايا» نُضيفها على الدالة، فنُضيف واحدة أو أكثر، ودون تغيير أيّ سطر في الشيفرة! رأينا التوابِع الآتية لنعرف كيفية إعداد المُزخرِف cachingDecorator: func.call(context, arg1, arg2…) -- يستدعي func حسب السياق والمُعاملات الممرّرة. func.apply(context, args) -- يستدعي func حيث يُمرّر context بصفته this والكائن الشبيه بالمصفوفات args في قائمة المُعاملات. عادةً ما نكتب تمرير الاستدعاءات باستعمال apply: let wrapper = function() { return original.apply(this, arguments); }; كما رأينا مثالًا عن استعارة التوابِع حيث أخذنا تابِعًا من كائن واستدعيناه call في سياق كائن آخر غيره. يشيع بين المطوّرين أخذ توابِع المصفوفات وتطبيقها على المُعاملات arguments. لو أردت بديلًا لذلك فاستعمل كائن المُعاملات البقية إذ هو مصفوفة حقيقية. ستجد في رحلتك المحفوفة بالمخاطر مُزخرِفات عديدة. حاوِل التمرّس عليها بحلّ تمارين هذا الفصل. تمارين مزخرف تجسس الأهمية: 5 أنشِئ المُزخرِف spy(func) ليُعيد غِلافًا يحفظ كلّ استدعاءات تلك الدالة في خاصية calls داخله. احفظ كلّ استدعاء على أنّه مصفوفة من الوُسطاء. مثال: function work(a, b) { alert( a + b ); // ليست work إلّا دالة أو تابِعًا لسنا نعرف أصله } work = spy(work); // (*) work(1, 2); // 3 work(4, 5); // 9 for (let args of work.calls) { alert( 'call:' + args.join() ); // "call:1,2", "call:4,5" } ملاحظة: نستفيد من هذا المُزخرِف أحيانًا لاختبار الوحدات. يمكن عدّ sinon.spy في المكتبة Sinon.JS صورةً متقدّمةً عنه. الحل سيُخزّن الغِلاف الذي أعادته spy(f) كلّ الوُسطاء، بعدها يستعمل f.apply لتمرير الاستدعاء. ….. function spy(func) { function wrapper(...args) { // استعملنا arg... بدلًا من arguments للحصول على مصفوفة حقيقية في wrapper.calls wrapper.calls.push(args); return func.apply(this, args); } wrapper.calls = []; return wrapper; } مزخرف تأخير الأهمية: 5 أنشِئ المُزخرف delay(f, ms) ليُؤخّر كلّ استدعاء من f بمقدار ms مليثانية. مثال: function f(x) { alert(x); } // أنشِئ الغِلافات let f1000 = delay(f, 1000); let f1500 = delay(f, 1500); f1000("test"); // يعرض «test» بعد 1000 مليثانية f1500("test"); // يعرض «test» بعد 1500 مليثانية أي أنّ المُزخرِف delay(f, ms) يُعيد نسخة عن f «تأجّلت ms». الدالة f في الشيفرة أعلاه تقبل وسيطًا واحدًا، ولكن على الحل الذي ستكتبه تمرير كلّ الوُسطاء والسياق this كذلك. الحل function delay(f, ms) { return function() { setTimeout(() => f.apply(this, arguments), ms); }; } let f1000 = delay(alert, 1000); f1000("test"); // يعرض test بعد 1000 مليثانية لاحظ بأنّا استعملنا الدالة السهمية هنا. كما نعلم فالدوال السهمية لا تملك لا this ولا arguments، لذا يأخذ f.apply(this, arguments) كِلا this وarguments من الغِلاف. لو مرّرنا دالة عادية فسيستدعيها setTimeout بدون المُعاملات ويضبط this=window (باعتبار أنّا في بيئة المتصفّح). مع ذلك يمكننا تمرير قيمة this الصحيحة باستعمال متغيّر وسيط ولكنّ ذلك سيكون تعبًا لا داعٍ له: function delay(f, ms) { return function(...args) { let savedThis = this; // خزّنه في متغير وسيط setTimeout(function() { f.apply(savedThis, args); // استعمل الوسيط هنا }, ms); }; } مزخرف إزالة ارتداد اصنع المُزخرِف debounce(f, ms) ليُعيد غِلافًا يُمرّر الاستدعاء إلى f مرّة واحدة كلّ ms مليثانية. بعبارة أخرى: حين ندعو الدالة «بأنّ ارتدادها أُزيل» Debounce فهي تضمن لنا بأنّ الاستدعاءات التي ستحدث في أقلّ من ms مليثانية بعد الاستدعاء السابق - ستُهمل. إليك مثال (مأخوذ من مكتبة Lodash😞 let f = _.debounce(alert, 1000); f("a"); setTimeout( () => f("b"), 200); setTimeout( () => f("c"), 500); // alert("c") تنتظر 1000 م.ث بعد آخر استدعاء ثم تُنفذ عمليًا في الشيفرات، نستعمل debounce للدوال التي تستلم أو تُحدّث شيئًا ما نعرف مسبقًا بأنّ لا شيء جديد سيحدث له في هذه الفترة القصيرة، فالأفضل أن نُهمله ولا نُهدر الموارد. مثلًا، لنفكر بحالة عملية عندما نريد أن نرسل طلبًا للخادم كلما أدخل المستخدم حرفًا (للبحث مثلًا) فلا حاجة إلى إرسال طلب عند إدخال كل حرف والأفضل أن ننتظر قليلًا ريثما ينتهي المستخدم وبذلك نعالج الكلمة المدخلة كلها. وهذا يُطبق على كل حالات المعالجة للمدخلات لحقول الإدخال input. إن فكرت بالحل، قبل النظر للشيفرة التالية، فهو عبارة عن بضعة سطور! الحل function debounce(func, ms) { let timeout; return function() { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, arguments), ms); }; } استدعاء debounce يُعيد غِلافًا. عند استدعائها، تجدول الدالة الأصلية بعد عدد الثواني المحدد وتمسح أي استدعاء سابق مُجدول. مزخرف خنق الأهمية: 5 أنشِئ مُزخرِف «الخنق/throttle» throttle(f, ms) ليُعيد غِلافًا يُمرّر الاستدعاء إلى f مرّة كلّ ms مليثانية. والاستدعاءات التي تحدث في فترة «الراحة» تُهمل. الفرق بين هذه وبين debounce هي أنّه لو كان الاستدعاء المُهمل هو آخر الاستدعاءات أثناء فترة الراحة، فسيعمل متى انتهت تلك الفترة. لنطالع هذا التطبيق من الحياة العملية لنعرف أهمية هذا الشيء الغريب العجيب وما أساسه أصلًا. لنقل مثلًا أنّا نريد تعقّب تحرّك الفأرة. يمكن أن نضبط دالة (في المتصفّح) لتعمل كلّما تحرّكت الفأرة وتأخذ مكان المؤشّر أثناء هذه الحركة. لو كنت تستعمل الفأرة فعادةً ما تعمل الدالة هذه بسرعة (ربما تكون 100 مرّة في الثانية، أي كلّ 10 مليثوان). نريد تحديث بعض المعلومات في صفحة الوِب أثناء حركة المؤشّر. …ولكن تحديث الدالة update() عملية ثقيلة ولا تنفع لكلّ حركة فأرة صغيرة. كما وليس منطقيًا أصلًا التحديث أكثر من مرّة كلّ 100 مليثانية. لذا نُغلّف الدالة في مُزخرف: نستعمل throttle(update, 100) على أنّها دالة التشغيل كلّما تحرّكت الفأرة بدلًا من الدالة update() الأصلية. سيُستدعى المُزخرِف كثيرًا صحيح، ولكنّها لن يمرّر الاستدعاءات هذه إلى update() إلّا مرّة كلّ 100 مليثانية. هكذا سيظهر للمستخدم: في أوّل تحريك للفأرة، تُمرّر نسختنا المُزخرفة من الدالة الاستدعاء مباشرةً إلى update، وهذا مهمّ إذ يرى المستخدم كيف تفاعلت الصفحة مباشرةً مع تحريكه الفأرة. ثمّ يُحرّك المستخدم الفأرة أكثر، ولا يحدث شيء طالما لم تمرّ 100ms. نسختنا المُزخرفة الرائعة تُهمل تلك الاستدعاءات. بعد نهاية 100ms يعمل آخر استدعاء update حاملًا الإحداثيات الأخيرة. وأخيرًا تتوقّف الفأرة عن الحراك. تنتظر الدالة المُزخرفة حتى تمضي 100ms وثمّ تشغّل update حاملةً آخر الإحداثيات. وهكذا نُعالج آخر حركة للفأرة، وهذا مهم مثال عن الشيفرة: function f(a) { console.log(a); } // تمرّر f1000 الاستدعاءات إلى f مرّة كلّ 1000 مليثانية كحدّ أقصى let f1000 = throttle(f, 1000); f1000(1); // تعرض 1 f1000(2); // (مخنوقة، لم تمض 1000 مليثانية بعد) f1000(3); // (مخنوقة، لم تمض 1000 مليثانية بعد) // حين تمضي 1000 مليثانية... // ...تطبع 3، إذ القيمة 2 الوسطية أُهملت ملاحظة: يجب تمرير المُعاملات والسياق this المُمرّرة إلى f1000- تمريرها إلى f الأصلية. الحل function throttle(func, ms) { let isThrottled = false, savedArgs, savedThis; function wrapper() { if (isThrottled) { // (2) savedArgs = arguments; savedThis = this; return; } func.apply(this, arguments); // (1) isThrottled = true; setTimeout(function() { isThrottled = false; // (3) if (savedArgs) { wrapper.apply(savedThis, savedArgs); savedArgs = savedThis = null; } }, ms); } return wrapper; } يُعيد استدعاء throttle(func, ms) الغِلاف wrapper. أثناء الاستدعاء الأول، يُشغّل wrapper ببساطة الدالة func ويضبط حالة الراحة (isThrottled = true). في هذه الحالة نحفظ كلّ الاستدعاءات في savedArgs/savedThis. لاحظ بأنّ السياق والوُسطاء مهمّان ويجب حفظهما كلاهما، فنحتاجهما معًا لنُعيد ذلك الاستدعاء كما كان ونستدعيه حقًا. بعد مرور ms مليثانية، يعمل setTimeout، بهذا تُزال حالة الراحة (isThrottled = false) ولو كانت هناك استدعاءات مُهملة، نُنفّذ wrapper بآخر ما حفظنا من وُسطاء وسياق. لا نشغّل في الخطوة الثالثة func بل wrapper إذ نريد تنفيذ func إضافةً إلى دخول حالة الراحة ثانيةً وضبط المؤقّت لتصفيرها. ترجمة -وبتصرف- للفصل Decorators and forwarding, call/apply من كتاب The JavaScript language
-
وأنت تكتب الشيفرة، ستقول في نفسك «أريد تشغيل هذه الدالة بعد قليل وليس الآن الآن. هذا ما نسمّيه "بجدولة الاستدعاءات" (scheduling a call). إليك تابِعين اثنين لهذه الجدولة: يتيح لك setTimeout تشغيل الدالة مرّة واحدة بعد فترة من الزمن. يتيح لك setInterval تشغيل الدالة تكراريًا يبدأ ذلك بعد فترة من الزمن ويتكرّر كلّ فترة حسب تلك الفترة التي حدّدتها. صحيح أنّ هذين التابِعين ليسا في مواصفة لغة جافاسكربت إلّا أنّ أغلب البيئات فيها مُجدوِل داخلي يقدّمهما لنا. وللدقّة، فكلّ المتصّفحات كما وNode.js تدعمهما. تابع تحديد المهلة setTimeout الصياغة: let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...) المُعاملات: func|code: ما يجب تنفيذه أكان دالة أو سلسلة نصية فيها شيفرة. عادةً هي دالة ولكن كعادة الأسباب التاريخية (أيضًا) يمكن تمرير سلسلة نصية فيها شيفرة، ولكنّ ذلك ليس بالأمر المستحسن. delay: التأخير قبل بدء التنفيذ بالمليثانية (1000 مليثانية = ثانية واحدة). مبدئيًا يساوي 0. arg1, arg2…: وُسطاء الدالة (ليست مدعومة في IE9-) إليك هذه الشيفرة التي تستدعي sayHi() بعد ثانيةً واحدة: function sayHi() { alert('Hello'); } setTimeout(sayHi, 1000); مع المُعاملات: function sayHi(phrase, who) { alert( phrase + ', ' + who ); } setTimeout(sayHi, 1000, "Hello", "John"); // Hello, John لو كان المُعامل الأول سلسلة نصية فستصنع جافاسكربت دالة منها. أي أنّ هذا سيعمل: setTimeout("alert('Hello')", 1000); ولكن استعمال السلاسل النصية غير مستحسن. استعمل الدوال السهمية بدلًا عنها: setTimeout(() => alert('Hello'), 1000); مرّر الدالة لكن لا تشغّلها يُخطئ المبرمجون المبتدئون أحيانًا فيُضيفون أقواس () بعد الدالة: // هذا خطأ! setTimeout(sayHi(), 1000); لن يعمل ذلك إذ يتوقّع setTimeout إشارة إلى الدالة، بينما هنا sayHi() يشغّل الدالة وناتج التنفيذ هو الذي يُمرّر إلى setTimeout. في حالتنا ناتج sayHi() ليس معرّفًا undefined (إذ لا تُعيد الدالة شيئًا)، ويعني ذلك أنّ عملنا ذهب سدًى ولم نُجدول أي شيء. الإلغاء باستعمال clearTimeout نستلمُ حين نستدعي setTimeout «هويّةَ المؤقّت» timerId ويمكن استعمالها لإلغاء عملية التنفيذ. صياغة الإلغاء: let timerId = setTimeout(...); clearTimeout(timerId); في الشيفرة أسفله نُجدول الدالة ثمّ نُلغيها (غيّرنا الخطّة العبقرية)، بهذا لا يحدث شيء: let timerId = setTimeout(() => alert("never happens"), 1000); alert(timerId); // هويّة المؤقّت clearTimeout(timerId); alert(timerId); // ذات الهويّة (لا تصير null بعد الإلغاء) يمكن أن نرى من ناتج التابِع alert أنّ هويّة المؤقّت (في المتصفّحات) هي عدد. يمكن أن تكون في البيئات الأخرى أيّ شيء آخر. فمثلًا في Node.js نستلم كائن مؤقّت فيه توابِع أخرى. نُعيد بأن ليس هناك مواصفة عالمية متّفق عليها لهذه التوابِع، فما من مشكلة في هذا. يمكنك مراجعة مواصفة HTML5 للمؤقّتات (داخل المتصفّحات) في فصل المؤقّتات. setInterval صياغة التابِع setInterval هي ذات setTimeout: let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...) ولكلّ المُعاملات ذات المعنى. ولكن على العكس من setTimeout فهذا التابِع يشغّل الدالة مرّة واحدة ثمّ أخرى وأخرى وأخرى تفصلها تلك الفترة المحدّدة. يمكن أن نستدعي clearInterval(timerId) لنُوقف الاستدعاءات اللاحقة. سيعرض المثال الآتي الرسالة كلّ ثانيتين اثنتين، وبعد خمس ثوان يتوقّف ناتجها: // نكرّر التنفيذ بفترة تساوي ثانيتين let timerId = setInterval(() => alert('tick'), 2000); // وبعد خمس ثوان نُوقف الجدولة setTimeout(() => { clearInterval(timerId); alert('stop'); }, 5000); الوقت لا يتوقّف حين تظهر مُنبثقة alert تُواصل عقارب ساعة المؤقّت الداخلي (في أغلب المتصفّحات بما فيها كروم وفَيَرفُكس) بالمضيّ حتّى حين عرض alert/confirm/prompt. لذا متى ما شغّلت الشيفرة أعلاه ولم تصرف نافذة alert بسرعة، فسترى نافذة alert الثانية بعد ذلك مباشرةً، بذلك تكون الفترة الفعلية بين التنبيهين أقلّ من ثانيتين. تداخل setTimeout لو أردنا تشغيل أمر كلّ فترة، فهناك طريقتين اثنتين. الأولى هي setInterval. والثانية هي setTimeout متداخلة هكذا: /** بدل كتابة: let timerId = setInterval(() => alert('tick'), 2000); */ let timerId = setTimeout(function tick() { alert('tick'); timerId = setTimeout(tick, 2000); // (*) }, 2000); تابِع setTimeout أعلاه يُجدول الاستدعاء التالي ليحدث بعد نهاية الأول (لاحظ (*)). كتابة توابِع setTimeout متداخلة يعطينا شيفرة مطواعة أكثر من setInterval. بهذه الطريقة يمكن تغيير جدولة الاستدعاء التالي حسب ناتج الحالي. فمثلًا علينا كتابة خدمة تُرسل طلب بيانات إلى الخادوم كلّ خمس ثوان، ولكن لو كان الخادوم مُثقلًا بالعمليات فيجب أن تزداد الفترة إلى 10 فَـ 20 فَـ 40 ثانية وهكذا… إليك فكرة عن الشيفرة: let delay = 5000; let timerId = setTimeout(function request() { ...نُرسل الطلب... if (لو فشل الطلب لوجود ضغط على الخادوم) { // نزيد الفترة حتّى الطلب التالي delay *= 2; } timerId = setTimeout(request, delay); }, delay); ولو كانت الدوال التي نُجدولها ثقيلة على المعالج فيمكن أن نقيس الزمن الذي أخذتها عملية التنفيذ الحالية ونؤجّل أو نقدّم الاستدعاء التالي. يتيح لنا تداخل التوابِع setTimeout بضبط الفترة بين عمليات التنفيذ بدقّة أعلى ممّا تقدّمه setInterval. لنرى الفرق بين الشيفرتين أسفله. الأولى تستعمل setInterval: let i = 1; setInterval(function() { func(i++); }, 100); الثانية تستعمل setTimeout متداخلة: let i = 1; setTimeout(function run() { func(i++); setTimeout(run, 100); }, 100); سيُشغّل المُجدول الداخلي func(i++) كلّ 100 مليثانية حسب setInterval: هل لاحظت ذلك؟ التأخير الفعلي بين استدعاءات func التي ينفّذها setInterval أقل مما هي عليه في الشيفرة! هذا طبيعي إذ أنّ الوقت الذي يأخذه تنفيذ func يستهلك بعضًا من تلك الفترة أيضًا. يمكن أيضًا بأن يصير تنفيذ func أكبر ممّا توقعناه على حين غرّة ويأخذ وقتًا أطول من 100 مليثانية. في هذه الحال ينتظر المحرّك انتهاء func ثمّ يرى المُجدول: لو انقضى الوقت يشغّل الدالة مباشرةً. دومًا ما تأخذ الدالة وقتًا أطول من delay مليثانية في هذه الحالات الهامشية، إذ تجري الاستدعاءات واحدةً بعد الأخرى دون هوادة. وإليك صورة setTimeout المتداخلة: تضمن setTimeout المتداخلة لنا التأخير الثابت (100 مليثانية في حالتنا). ذلك لأنّ الاستدعاء التالي لا يُجدول إلا بعد انتهاء السابق. كنس المهملات وردود نداء التابِعين setInterval و setTimeout تُنشأ إشارة داخلية إلى الدالة (وتُحفظ في المُجدول) متى مرّرتها إلى إلى setInterval/setTimeout، وهذا يمنع كنس الدالة على أنّها مهملات، حتّى لو لم تكن هناك إشارات إليها. // تبقى الدالة في الذاكرة حتّى يُستدعى `clearInterval`. setTimeout(function() {...}, 100); ولكن هناك تأثير جانبي لذلك كالعادة، فالدوال تُشير إلى بيئتها المُعجمية الخارجية. لذا طالما «تعيش»، تعيش معها المتغيرات الخارجية أيضًا، وهي أحيانًا كبيرة تأخذ ذاكرة أكبر من الدالة ذاتها. لذا، متى ما لم ترد تلك الدالة المُجدولة فالأفضل أن تُلغيها حتّى لو كانت صغيرة جدًا. جدولة setTimeout بتأخير صفر إليك الحالة الخاصة: setTimeout(func, 0) أو setTimeout(func). يُجدول هذا التابِع ليحدث تنفيذ func بأسرع ما يمكن، إلّا أن المُجدول لن يشغّلها إلا بعد انتهاء السكربت الذي يعمل حاليًا. أي أنّ الدالة تُجدول لأن تعمل «مباشرةً بعد» السكربت الحالي. فمثلًا تكتب هذه الشيفرة "Hello" ثم مباشرة "World": setTimeout(() => alert("World")); alert("Hello"); يعني السطر الأوّل «ضع الاستدعاء في التقويم بعد 0 مليثانية»، إلّا أنّ المُجدول لا «يفحص تقويمه» إلّا بعد انتهاء السكربت الحالي، بهذا تصير "Hello" أولًا وبعدها تأتي "World". كما أنّ هناك استعمالات متقدّمة خصّيصًا للمتصفّحات للمهلة بالتأخير صفر هذه، وسنشرحها في الفصل «حلقة الأحداث: المهام على المستويين الجُسيمي والذرّي». في الواقع، فالتأخير الصفر هذا ليس صفرًا (في المتصفّحات) تحدّ المتصفّحات من التأخير بين تشغيل المؤقّتات المتداخلة. تقول مواصفة HTML5: «بعد المؤقّتات المتداخلة الخمسة الأولى، تُجبر الفترة لتكون أربع مليثوان على الأقل.». لنرى ما يعني ذلك بهذا المثال أسفله. يُعيد استدعاء setTimeout جدولة نفسه بمدّة تأخير تساوي صفر، ويتذكّر كل استدعاء الوقت الفعلي بينه وبين آخر استدعاء في مصفوفة times. ولكن، ما هي التأخيرات الفعلية؟ لنرى بأعيننا: let start = Date.now(); let times = []; setTimeout(function run() { times.push(Date.now() - start); // نحفظ التأخير من آخر استدعاء if (start + 100 < Date.now()) alert(times); // نعرض التأخيرات بعد 100 مليثانية else setTimeout(run); // وإلّا نُعيد الجدولة }); // إليك مثالًا عن الناتج: // 1,1,1,1,9,15,20,24,30,35,40,45,50,55,59,64,70,75,80,85,90,95,100 تعمل المؤقّتات الأولى مباشرةً (كما تقول المواصفة)، وبعدها نرى 9, 15, 20, 24.... تلك الأربع مليثوان الإضافية هي التأخير المفروض بين الاستدعاءات. حتّى مع setInterval بدل setTimeout، ذات الأمر: تعمل الدالة setInterval(f) أوّل f مرّة بمدّة تأخير صفر، وبعدها تزيد أربع مليثوان لباقي الاستدعاءات. سبب وجود هذا الحدّ هو من العصور الحجرية (متعوّدة دايمًا) وتعتمد شيفرات كثيرة على هذا السلوك. بينما مع نسخة الخواديم من جافاسكربت فهذا الحدّ ليس له وجود، وهناك أيضًا طُرق أخرى لبدء المهام التزامنية مباشرةً، مثل setImmediate للغة Node.js، هذا قلنا بأنّ هذا يخصّ المتصفّحات فقط. ملخص يتيح لنا التابِعان setTimeout(func, delay, ...args) وsetInterval(func, delay, ...args) تشيل الدالة func مرّة أو كلّ فترة حسب كذا مليثانية (delay). لإلغاء التنفيذ علينا استدعاء clearTimeout/clearInterval بالقيمة التي أعاداها setTimeout/setInterval. يُعدّ استدعاء الدوال setTimeout تداخليًا خيارًا أفضل من setInterval إذ يُتيح لنا ضبط الوقت بين كلّ عملية استدعاء بدقّة. الجدولة بضبط التأخير على الصفر باستعمال setTimeout(func, 0) (كما واستعمال setTimeout(func)) يكون حين نريدها «بأقصى سرعة ممكنة، متى انتهى السكربت الحالي». يَحدّ المتصفّح من أدنى تأخير بعد استدعاء setTimeout أو setInterval المتداخل الخامس (أو أكثر) - يَحدّه إلى 4 مليثوان، وهذا لأسباب تاريخية لاحظ بأنّ توابِع الجدولة لا تضمن التأخير كما هو حرفيًا. فمثلًا يمكن أن تكون مؤقّتات المتصفّحات أبطأ لأسباب عديدة: المعالج مُثقل بالعمليات. لسان المتصفّح يعمل في الخلفية. يعمل الحاسوب المحمول على البطارية. يمكن لهذا كله رفع دقّة المؤقّت الدنيا (أي أدنى تأخير ممكن) لتصير 300 مليثانية أو حتى 1000 مليثانية حسب المتصفّح وإعدادات الأداء في نظام التشغيل. تمارين اكتب الناتج كل ثانية الأهمية: 5 اكتب الدالة printNumbers(from, to) لتكتب عددًا كلّ ثانية بدءًا بِـ from وانتهاءً بِـ to. اصنع نسختين من الحل. واحدةً باستعمال setInterval. واحدةً باستعمال setTimeout متداخلة. الحل باستعمال setInterval: function printNumbers(from, to) { let current = from; let timerId = setInterval(function() { alert(current); if (current == to) { clearInterval(timerId); } current++; }, 1000); } // الاستعمال: printNumbers(5, 10); باستعمال setTimeout متداخلة: function printNumbers(from, to) { let current = from; setTimeout(function go() { alert(current); if (current < to) { setTimeout(go, 1000); } current++; }, 1000); } // الاستعمال: printNumbers(5, 10); لاحظ كلا الحلّين: هناك تأخير أولي قبل أول عملية كتابة إذ تُستدعى الدالة بعد 1000ms في أوّل مرة. لو أردت تشغيل الدالة مباشرةً فعليك كتابة استدعاء إضافي في سطر آخر هكذا: function printNumbers(from, to) { let current = from; function go() { alert(current); if (current == to) { clearInterval(timerId); } current++; } go(); // هنا let timerId = setInterval(go, 1000); } printNumbers(5, 10); ماذا سيعرض setTimeout؟ الأهمية: 5 جدول أحدهم في الشيفرة أسفله استدعاء setTimeout، وثمّ كتب عملية حسابية ثقيلة لتعمل (وهي تأخذ أكثر من 100 مليثانية حتى تنتهي). متى ستعمل الدالة المُجدولة؟ بعد الحلقة؟ قبل الحلقة؟ في بداية الحلقة؟ ما ناتج alert؟ let i = 0; setTimeout(() => alert(i), 100); // ? // عُدّ بأنّ الوقت اللازم لتنفيذ هذه الدالة يفوق 100 مليثانية for(let j = 0; j < 100000000; j++) { i++; } الحل لن يُشغّل أيّ تابِع setTimeout إلا بعدما تنتهي الشيفرة الحالية. ستكون قيمة i هي القيمة الأخيرة: 100000000. let i = 0; setTimeout(() => alert(i), 100); // 100000000 // عُدّ بأنّ الوقت اللازم لتنفيذ هذه الدالة يفوق 100 مليثانية for(let j = 0; j < 100000000; j++) { i++; } ترجمة -وبتصرف- للفصل Scheduling: setTimeout and setInterval من كتاب The JavaScript language
-
توجد (أيضًا) طريقة أخرى لإنشاء الدوال. صحيح هي نادرة الاستعمال ولكن لا مفرّ منها في حالات معيّنة. الصياغة إليك صياغة إنشاء الدالة: let func = new Function ([arg1, arg2, ...argN], functionBody); نصنع الدالة بالوُسطاء arg1...argN ونمرّر متنها functionBody. «هات الشيفرة وقلّل ثرثرتك»… صحيح، هذا أسهل. إليك الدالة وفيها وسيطين اثنين: let sum = new Function('a', 'b', 'return a + b'); alert( sum(1, 2) ); // 3 وهنا دالة بلا وُسطاء فيها متنها فقط: let sayHi = new Function('alert("Hello")'); sayHi(); // Hello الفرق الأساس بين هذه الطريقة والطرائق الأخرى هي أنّا نصنع الدالة هنا (كما لاحظت) من سلسلة نصية حرفيًا، ونمرّرها في وقت تشغيل الشيفرة. ألزمتنا التصريحات السابقة كلها - ألزمتنا نحن المطوّرين أن نكتب شيفرة الدالة في السكربت. ولكن صياغة new Function تسمح لنا بأن نحوّل أيّ سلسلة نصية لتصير دالة. فمثلًا يمكن أن نستلم دالة جديدة من أحد الخواديم وننفّذها: let str = ... نستلم الشيفرة ديناميكيًا من الخادوم ... let func = new Function(str); func(); لا نستعمل هذه إلا في حالات خاصّة، مثل لو استلمنا الشيفرة من الخادوم أو صنعنا الدالة ديناميكًا من قالب (في تطبيقات الوِب المعقّدة). المنغلقات عادةً ما تتذكّر الدالة مكان ولادتها في الخاصية المميّزة [[Environment]]، فتُشير إلى البيئة المُعجمية حين صُنعت الدالة (شرحنا هذا في فصل «المُنغِلقات»). ولكن حين نصنع الدالة باستعمال new Function فتُضبط خاصية [[Environment]] على البيئة المُعجمية العمومية لا الحالية. أي أنّ هذه الدوال لا يمكن أن ترى المتغيرات الخارجية بل تلك العمومية فقط. function getFunc() { let value = "test"; let func = new Function('alert(value)'); return func; } getFunc()(); // خطأ: value غير معرّف وازن بين هذا والسلوك الطبيعي: function getFunc() { let value = "test"; let func = function() { alert(value); }; return func; } getFunc()(); // «test»، من بيئة getFunc المُعجمية صحيح أنّ الميزة الخاصة للصياغة new Function غريبة بعض الشيء، ولكنها عمليًا مفيدة جدًا. تخيّل الآن بأنّنا صنعنا دالة من سلسلة نصية. شيفرة هذه الدالة ليست معروفة ونحن نكتب السكربت (ولهذا لم نستعمل الدوال العادية)، بل ستكون معروفة حين تنفيذه. كما أسلفنا يمكن أن نستلم الدالة من الخادوم أو أيّ مكان آخر. الآن، على دالتنا هذه التفاعل مع السكربت الرئيس. لكن ماذا لو أمكن لها أن ترى المتغيرات الخارجية؟ المشكلة هي أنّه قبل أن ننشر شيفرة جافاسكربت لنستعملها، نستعمل المُصغِّرات (minifiers) لضغطها. تقلّص هذه المُصغِّرات حجم الشيفرة بإزالة التعليقات والمسافات الزائدة، كما (وهذا مهم) تُغيّر تسمية المتغيرات المحلية إلى أسماء أقصر. فمثلًا لو كان في الدالة let userName فيستبدلها المُصغِّر إلى let a (أو أيّ حرف آخر لو هناك من أخذ الاسم)، وينفّذ هذا في كلّ مكان آخر. عادةً لا يضرّ ذلك إذ أنّ المتغير محلي ولا يمكن لما خارج الدالة رؤيته، بينما يستبدل المُصغِّر كلّ مرة يرد فيها المتغير داخل الدالة. هذه الأدوات ذكية فهي تحلّل بنية الشيفرة لألا تُعطبها، وليست كأدوات البحث والاستبدال الهمجية. لذا لو أرادت new Function أن تستعمل المتغيرات الخارجية فلن تعرف بوجود userName الذي تغيّر اسمه. لو أمكن للدوال new Function أن ترى المتغيرات الخارجية لكانت ستواجه مشاكل جمّة مع المُصغِّرات. كما وأنّ الشيفرات من هذا النوع ستكون سيّئة من حيث البنية وعُرضة للأخطاء والمشاكل. لو أردت تمرير شيء للدالة new Function فعليك استعمال مُعاملاتها. ملخص الصياغة: let func = new Function ([arg1, arg2, ...argN], functionBody); ويمكن تمرير المُعاملات (لأسباب تاريخية أيضًا) في قائمة مفصولة بفواصل. هذه التصريحات الثلاث لا تفرق عن بعضها البعض: new Function('a', 'b', 'return a + b'); // الصياغة الأساس new Function('a,b', 'return a + b'); // مفصولة بفواصل new Function('a , b', 'return a + b'); // مفصولة بفواصل ومسافات تُشير خاصية [[Environment]] للدوال new Function إلى البيئة المُعجمية العمومية لا الخارجية. بهذا لا يمكن لهذه الدوال استعمال المتغيرات الخارجية. إلّا أنّ ذلك أمر طيّب إذ تؤمّن لنا خطّ حماية لألا نصنع الأخطاء والمشاكل، فتمرير المُعاملات جهارةً أفضل بكثير من حيث بنية الشيفرة ولا تتسبّب مشاكل مع المُصغِّرات. ترجمة -وبتصرف- للفصل The "new Function" syntax من كتاب The JavaScript language
-
كما نعلم فالدوال في لغة جافاسكربت تُعدّ قيمًا. ولكلّ قيمة في هذه اللغة نوع. ولكن ما نوع الدالة نفسها؟ تُعدّ الدوال كائنات في جافاسكربت. يمكننا تخيّل الدوال على أنّها «كائنات إجرائية» يمكن استدعائها. لا يتوقّف الأمر عند الاستدعاء أيضًا بل يمكن حتّى أن نُعاملها معاملة الكائنات فنُضيف الخاصيات ونُزيلها، أو نمرّرها بالإشارة وغيرها من أمور. خاصية الاسم name تحتوي كائنات الدوال على خاصيات يمكننا استعمالها. فمثلًا يمكن أن نعرف اسم الدالة من خاصية الاسم name: function sayHi() { alert("Hi"); } alert(sayHi.name); // sayHi والمُضحك في الأمر هو أنّ منطق اللغة في إسناد الاسم ذكيّ (الذكاء الاصطناعي مبهر)، فهو يُسند اسم الدالة الصحيح حتّى لو أنشأناها بدون اسم ثمّ أسندناها إلى متغير مباشرةً: let sayHi = function() { alert("Hi"); }; alert(sayHi.name); // sayHi (للدالة اسم!) كما ويعمل المنطق أيضًا لو كانت عملية الإسناد عبر قيمة مبدئية: function f(sayHi = function() {}) { alert(sayHi.name); // sayHi (تعمل أيضًا!) } f(); تُدعى هذه الميزة في توصيف اللغة «بالاسم السياقي». فلو لم تقدّم الدالة اسمًا لها فيحاول المحرّك معرفته من السياق مع أوّل عملية إسناد. كما ولتوابِع الكائنات أسماء أيضًا: let user = { sayHi() { // ... }, sayBye: function() { // ... } } alert(user.sayHi.name); // sayHi alert(user.sayBye.name); // sayBye ولكن ليس للسحر مكان هنا، فهناك حالات يستحيل على المحرّك معرفة الاسم الصحيح منها، بهذا تكون خاصية الاسم فارغة، كما في هذه الشيفرة: // نُنشئ دالة في مصفوفة let arr = [function() {}]; alert( arr[0].name ); // <سلسلة نصية فارغة> // ما من طريقة يعرف بها المحرّك الاسم الصحيح، فباختصار، ليس هناك اسم! ولكن عمليًا، لكل الدوال أسماء أغلب الوقت. خاصية الطول length توجد خاصية أخرى مضمّنة في اللغة باسم length وهي تُعيد عدد مُعاملات الدالة. مثال: function f1(a) {} function f2(a, b) {} function many(a, b, ...more) {} alert(f1.length); // 1 alert(f2.length); // 2 alert(many.length); // 2 نرى هنا بأن المُعاملات البقية لم تُحسب. يستعمل المطوّرون خاصية length أحيانًا لإجراء التحقّق الداخلي داخل الدوال التي تعتمد في تشغيلها على التحكّم بدوال أخرى. في الشيفرة أسفله، تقبل دالة ask سؤالًا question تطرحه وعددًا غير محدّد من دوال المعالجة handler لتستدعيها دالة السؤال. فما إن ينزل الوحي على المستخدم ويُعطينا إجابة تستدعي الدالة المُعالجات. يمكننا تمرير نوعين اثنين من المُعالجات هذه: دالة ليس لها وُسطاء لا تُنشأ إلا عندما يُعطيها المستخدم إجابة بالإيجاب. دالة لها وُسطاء تُستدعى في بقية الحالات وتُعيد إجابة المستخدم. علينا فحص خاصية handler.length لنستدعي handler بالطريقة السليمة. الفكرة هنا هي أن تستعمل الشيفرة صياغة مُعالجة بسيطة وبدون وُسطاء لحالات الإيجاب (وهذا الشائع)، إضافةً على دعم المُعالجات العامة أيضًا: function ask(question, ...handlers) { let isYes = confirm(question); for(let handler of handlers) { if (handler.length == 0) { if (isYes) handler(); } else { handler(isYes); } } } // يُعاد كِلا المُعالجان لو كانت الإجابة بالإيجاب // ولو كانت بالسلب، فالثاني فقط ask("Question?", () => alert('You said yes'), result => alert(result)); هذه حالة من حالات التعدّدية الشكلية، أي حين يتغيّر تعاملنا مع الوُسطاء حسب أنواعها… في حالتنا فهي حسب أطوالها length. لهذه الفكرة استعمال فعليّ في مكتبات جافاسكربت. خاصيات مخصصة يمكننا أيضًا إضافة ما نريد من خاصيات. فهنا نُضيف خاصية العدّاد counter ليسجّل إجمالي عدد الاستدعاءات: function sayHi() { alert("Hi"); // لنعدّ كم من مرّة شغّلناه sayHi.counter++; } sayHi.counter = 0; // القيمة الأولية sayHi(); // Hi sayHi(); // Hi alert( `Called ${sayHi.counter} times` ); // Called 2 times الخاصيات ليست متغيرات لا تعرّف الخاصية المُسندة إلى الدوال مثل sayHi.counter = 0 متغيرًا محليًا فيها (counter في حالتنا). أي أنّ لا علاقة تربط الخاصية counter بالمتغير let counter البتة. يمكننا التعامل مع الدوال على أنها كائنات فنخزّن فيها الخاصيات، ولكن هذا لا يؤثّر على طريقة تنفيذها. ليست المتغيرات خاصيات للدالة ولا العكس، كلاهما منفصلين يسيران في خطّين مُحال أن يتقاطعا. يمكننا أحيانًا استعمال خاصيات الدوال بدل المُنغلِقات. فمثلًا يمكن إعادة كتابة تمرين دالة العدّ في فصل «المُنغلِقات» فنستعمل خاصية دالة: function makeCounter() { // بدل: // let count = 0 function counter() { return counter.count++; }; counter.count = 0; return counter; } let counter = makeCounter(); alert( counter() ); // 0 alert( counter() ); // 1 هكذا خزّنا الخاصية count في الدالة مباشرةً وليس في بيئتها المُعجمية الخارجية. أهذه أفضل أم المنغلقات أفضل؟ الفرق الرئيس هو: لو كانت قيمة count «تحيا» في متغير خارجي فلا يمكن لأي شيفرة خارجية الوصول إليها، بل الدوال المتداخلة فقط من يمكنها تعديلها، ولو ربطناها بدالة فيصير هذا ممكنًا: function makeCounter() { function counter() { return counter.count++; }; counter.count = 0; return counter; } let counter = makeCounter(); // هذا counter.count = 10; alert( counter() ); // 10 إذًا فالخيار يعود لنا: ماذا نريد وما الغاية. تعابير الدوال المسماة كما اسمها، فتعابير الدوال المسمّاة (Named Function Expression) هي تعابير الدوال التي لها اسم… بسيطة. لنأخذ مثلًا تعبير دالة مثل أي تعبير تراه في حياتك البرمجية التعيسة: let sayHi = function(who) { alert(`Hello, ${who}`); }; والآن نُضيف اسمًا له: let sayHi = function func(who) { alert(`Hello, ${who}`); }; هل حللنا أزمة عالمية هنا؟ ما الداعي من هذا الاسم "func"؟ أولًا، ما زال أمامنا تعبير دالة، فإضافة الاسم "func" بعد function لم يجعل الجملة تصريحًا عن دالة إذ ما زلنا نصنع الدالة داخل جزء من تعبير إسناد. كما وإضافة الاسم لم يُعطب الدالة بأي شكل. يمكن أن ندخل الدالة هكذا sayHi(): let sayHi = function func(who) { alert(`Hello, ${who}`); }; sayHi("John"); // Hello, John ثمّة أمرين مميزين بهذا الاسم func وهما السبب وراء كل هذا: يتيح الاسم بأن تُشير الدالة إلى نفسها داخليًا. ولا يظهر الاسم لما خارج الدالة. فمثلًا تستدعي الدالة sayHi أسفله نفسها ثانيةً بالوسيط "Guest" لو لم نمرّر لها who من البداية: let sayHi = function func(who) { if (who) { alert(`Hello, ${who}`); } else { func("Guest"); // نستعمل func لنستدعي نفسنا ثانيةً } }; sayHi(); // Hello, Guest // ولكن هذا لن يعمل: func(); // ويعطينا خطأً بأنّ func غير معرّفة (فالدالة لا تظهر لما خارجها) ولكن لمَ نستعمل func أصلًا؟ ألا يمكن أن نستعمل sayHi لذلك الاستدعاء المتداخل؟ للصراحة، يمكن ذلك في حالات عديدة: let sayHi = function(who) { if (who) { alert(`Hello, ${who}`); } else { sayHi("Guest"); } }; مشكلة تلك الشيفرة هي احتمالية تغيّر sayHi في الشيفرة الخارجية. فلو أُسندت الدالة إلى متغير آخر بدل ذاك فستبدأ الأخطاء تظهر: let sayHi = function(who) { if (who) { alert(`Hello, ${who}`); } else { sayHi("Guest"); // خطأ: sayHi ليست بدالة } }; let welcome = sayHi; sayHi = null; welcome(); // خطأ: لم يعد الاستدعاء المتداخل sayHi يعمل بعد الآن! سبب ذلك هو أنّ الدالة تأخذ sayHi من بيئتها المُعجمية الخارجية إذ لا تجد sayHi محليًا فيها فتستعمل المتغير الخارجي. وفي لحظة الاستدعاء تلك يكون sayHi الخارجي قد صار null. وهذا الداعي من ذلك الاسم الذي نضعه في تعبير الدالة، أن يحلّ هذه المشاكل بذاتها. هيًا نُصلح شيفرتنا: let sayHi = function func(who) { if (who) { alert(`Hello, ${who}`); } else { func("Guest"); // الآن تعمل كما يجب } }; let welcome = sayHi; sayHi = null; welcome(); // Hello, Guest (الاستدعاءات المتداخلة تعمل) الآن صارت تعمل إذ الاسم "func" محليّ للدالة فقط ولا تأخذها من الخارج (ولا تظهر للخارج أيضًا). تضمن لنا مواصفات اللغة بأنّها ستُشير دومًا وأبدًا إلى الدالة الحالية. مع ذلك فما زالت الشيفرة الخارجية تملك المتغيرين sayHi وwelcome، بينما func هو «اسم الدالة داخليًا» أي كيف تستدعي الدالة نفسها من داخلها. ما من ميزة كهذه للتصريح عن الدوال ميزة «الاسم الداخلي» هذه التي شرحناها هنا مُتاحة لتعابير الدوال فقط وليست متاحة للتصريحات عن الدوال. فليس لهذه الأخيرة أية صياغة برمجية لإضافة اسم «داخلي» لها. لكن أحيانًا نرى حاجة بوجود اسم داخلي نعتمد عليه، حينها يكون السبب وجيهًا بأن نُعيد كتابة التصريح عن الدالة إلى صيغة تعبير الدالة المسمّى. ملخص تُعدّ الدوال كائنات. شرحنا في الفصل خصائصها: اسمها name -- غالبًا ما يأتي من تعريف الدالة. لكن لو لم يكن هناك واحد فيحاول المحرّك تخمينه من السياق (مثلًا من عبارة الإسناد). عدد مُعاملتها في تعريف الدالة length -- لا تُحسب المُعاملات البقية. لو عرّفنا الدالة باستعمال تعبير عن دالة (وليس في الشيفرة الأساس)، وكان لهذه الدالة اسم فنُسمّيها بتعبير الدالة المسمّى. كما يمكن أن تحمل الدوال خاصيات إضافية، وتستغل الكثير من مكتبات جافاسكربت المعروفة هذه الميزة أيّما استغلال. إذ تُنشئ دالة «رئيسة» بعدها تُرفق دوال أخرى «مُساعِدة» إليها. فمثلًا تُنشئ مكتبة jQuery الدالة بالاسم $، وتُنشئ مكتبة lodash الدالة بالسم _ ثمّ تُضيف خاصياتها _.clone و _.keyBy وغيرها (طالع docs متى أردت معرفتها أكثر). ما تفعله هذه الدوال يعود إلى أنّها (في الواقع) تحاول حدّ «التلوّث» في المجال العمومي فلا تستعمل المكتبة إلّا متغيرًا عموميًا واحدًا. وهذا يُقلّل من أدنى إمكانية لتضارب الأسماء. إذًا، فالدالة تؤدي عملًا رائعًا كما هي، وأيضًا تحوي على وظائف أخرى خاصيات لها. تمارين ضبط قيمة العداد وإنقاصها الأهمية: 5 عدّل شيفرة الدالة makeCounter() بحيث يُنقص العدّاد قيمتها إضافةً إلى ضبطها: على counter() إعادة العدد التالي (كما في الأمثلة السابقة). على counter.set(value) ضبط قيمة العدّاد لتكون value. على counter.decrease() إنقاص قيمة العدّاد واحدًا (1). طالِع الشيفرة أدناه كي تعرف طريقة استعمال الدالة: function makeCounter() { let count = 0; // ... شيفرتك هنا ... } let counter = makeCounter(); alert( counter() ); // 0 alert( counter() ); // 1 counter.set(10); // نضبط العدد الجديد alert( counter() ); // 10 counter.decrease(); // نُنقص العدد واحدًا 1 alert( counter() ); // 10 (بدل 11) ملاحظة: يمكنك استعمال مُنغلِق أو خاصية دالة لحفظ العدد الحالي، أو لو أردت فاكتب الحل بالطريقتين. الحل يستعمل الحل المتغير count محليًا، كما وتوابِع أخرى نكتبها داخل الدالة counter. تتشارك هذه التوابِع ذات البيئة المُعجمية الخارجية كما وترى أيضًا قيمة count الحالية. مجموع ما في الأقواس أيًا كان عدد الأقواس الأهمية: 5 اكتب الدالة sum لتعمل كالآتي: sum(1)(2) == 3; // 1 + 2 sum(1)(2)(3) == 6; // 1 + 2 + 3 sum(5)(-1)(2) == 6 sum(6)(-1)(-2)(-3) == 0 sum(0)(1)(2)(3)(4)(5) == 15 تريد تلميحًا؟ ربما تكتب كائنًا مخصّصًا يُحوّل الأنواع الأولية لتُناسب الدالة. أيّما كانت الطريقة التي سنستعملها ليعمل هذا الشيء، فلا بدّ أن تُرجع sum دالة. على تلك الدالة أن تحفظ القيمة الحالية بين كلّ استدعاء والآخر داخل الذاكرة. حسب المهمّة المُعطاة، يجب أن تتحول الدالة إلى عدد حين نستعملها في ==. الدوال كائنات لذا فعملية التحويل ستنفع كما شرحنا في فصل «التحويل من كائن إلى قيمة أولية»، ويمكن أن نقدّم تابِعًا خاصًا يُعيد ذلك العدد. إلى الشيفرة: function sum(a) { let currentSum = a; function f(b) { currentSum += b; return f; } f.toString = function() { return currentSum; }; return f; } alert( sum(1)(2) ); // 3 alert( sum(5)(-1)(2) ); // 6 alert( sum(6)(-1)(-2)(-3) ); // 0 alert( sum(0)(1)(2)(3)(4)(5) ); // 15 لاحظ بأنّ دالة sum تعمل مرّة واحدة فقط لا غير، وتُعيد الدالة f. وبعدها في كلّ استدعاء يليها، تُضيف f المُعامل إلى المجموع currentSum وتُعيد نفسها. لا نستعمل التعاود في آخر سطر من f. هذا شكل التعاود: function f(b) { currentSum += b; return f(); // <-- استدعاء تعاودي } بينما في حالتنا نُعيد الدالة دون استدعائها: function f(b) { currentSum += b; return f; // <-- لا تستدعي نفسها، بل تُعيد نفسها } وستُستعمل f هذه في الاستدعاء التالي، وتُعيد نفسها ثانيةً مهما لزم. وبعدها حين نستعمل العدد أو السلسلة النصية، يُعيد التابِع toString المجموع currentSum. يمكن أيضًا أن نستعمل Symbol.toPrimitive أو valueOf لإجراء عملية التحويل. ترجمة -وبتصرف- للفصل Function object, NFE من كتاب The JavaScript language
-
تقدّم الكائنات العمومية متغيراتَ ودوال يمكن استعمالها من أي مكان. هذه الكائنات مضمّنة في بنية اللغة أو البيئة مبدئيًا. في المتصفّحات تُدعى بالنافذة window وفي Node.js تُدعى بالعموميات global وفي باقي البيئات تُدعى بأيّ اسم مناسب يراه مطوّروها. أُضيف حديثًا الكائن globalThis إلى اللغة ليكون اسم قياسيًا للكائن العمومي على أن تدعمه كلّ البيئات. ولكن بعض المتصفّحات (وبالخصوص عدا Chromium Edge) لا تدعم هذا الكائن بعد، ولكن يمكنك «ترقيعه تعدّديًا» بسهولة تامة. سنستعمل هنا window على فرضية بأنّ البيئة هي المتصفّح نفسه. لو كنت ستشغّل السكربت الذي تكتبه في بيئات أخرى فربما تستعمل globalThis بدل النافذة تلك. يمكننا طبعًا الوصول إلى كافة خصائص الكائن العمومي مباشرةً: alert("Hello"); // تتطابق تمامًا مع window.alert("Hello"); يمكنك في المتصفّحات التصريح عن الدوال العمومية والمتغيرات باستعمال var (وليس let/const !) لتصير خاصيات للكائن العمومي: var gVar = 5; alert(window.gVar); // 5 (تصير خاصية من خاصيات الكائن العمومي) ولكن أرجوك ألا تعتمد على هذا الأمر! هذا السلوك موجود للتوافقية لا غير. تستعمل السكربتات الحديثة «وحداتَ جافاسكربت» (نشرحها في وقت لاحق) حيث لا يحدث هكذا أمر. لن يحدث هذا لو استعملنا let هنا: let gLet = 5; alert(window.gLet); // غير معرّف (لا تصير خاصية للكائن العمومي) لو كانت القيمة هامّة جدًا جدًا وأردت أن تدخل عليها من أيّ مكان عمومي فاكتبها على أنّها خاصية مباشرةً: // نجعل من معلومات المستخدم الحالي عمومية لتصل إليها كلّ السكربتات window.currentUser = { name: "John" }; // وفي مكان آخر يريدها أحد alert(currentUser.name); // John // أو (لو كان هناك المتغير المحلي ذا الاسم «currentUser» // فنأخذها جهارةً من النافذة (وهذا آمن!) alert(window.currentUser.name); // John نختم هنا بأنّ استعمال المتغيرات العمومية غير محبّذ بالمرة ويجب أن يكون عددها بأقل ما يمكن. يُعدّ مبدأ تصميم الشيفرات حين تأخذ الدالة المتغيرات «الداخلة» وتُعطينا «نواتج» معيّنة - يُعدّ هذا المبدأ أفضل وأقلّ عُرضة للأخطاء وأسهل للاختبار موازنةً بالمتغيرات الخارجية أو العمومية. استعمالها للترقيع تعدديًا المجال الذي نستعمل الكائنات العمومية فيه هو اختبار لو كانت البيئة تدعم مزايا اللغة الحديثة. فمثلًا يمكننا اختبار لو كانت كائنات الوعود Promise المضمّنة في اللغة مضمّنة حقًا (لم تكن كذلك في المتصفحات العتيقة): if (!window.Promise) { alert("Your browser is really old!"); // تستعمل يا صاح متصفّحا من زمن الطيبين! } لو لم نجد هذه الكائنات (مثلًا نستعمل متصفّحًا قديمًا) فيمكننا «ترقيعه تعدّديًا»: أي إضافة الدوال التي لا تدعمها البيئة بينما هي موجودة في معيار اللغة الحديث. if (!window.Promise) { window.Promise = ... // شيفرة نكتبها بنفسنا تؤدّي الميزة الحديثة في اللغة هذه } ملخص يحمل الكائن العمومي تلك المتغيرات التي يلزم أن نصل إليها أينما كنّا في الشيفرة. تشمل المتغيرات هذه كل ما هو مضمّن في بنية لغة جافاسكربت مثل المصفوفات Array والقيم المخصّصة للبيئة مثل window.innerHeight (ارتفاع نافذة المتصفّح). للكائن العمومي اسم عام في المواصفة: globalThis. ولكن… دومًا ما نُشير إليه بالأسماء «الأثرية» حسب كل بيئة مثل window (في المتصفحات) وglobal (في Node.js)، إذ أنّ globalThis هو مُقترح جديد على اللغة وليس مدعومًا في المتصفّحات عدة Chromium Edge (ولكن يمكننا ترقيعه تعدّديًا). علينا ألا نخزّن القيم في الكائن العمومي إلّا لو كانت حقًا وفعلًا عمومية للمشروع الذي نعمل عليه. كما ويجب أن يبقى عددها بأقل ما يمكن. حين نطوّر لاستعمال الشيفرات في المتصفّحات (لو لم نستعمل الوحدات)، تصير الدوال العمومية والمتغيرات باستعمال var خاصيات للكائن العمومي. علينا استعمال خاصيات الكائن العمومي مباشرةً (مثل window.x) لتكون الشيفرة سهلة الصيانة مستقبلًا وأسهل فهمًا. ترجمة -وبتصرف- للفصل Global object من كتاب The JavaScript language
-
ذكرنا في أوائل الفصول حين تكلمنا عن المتغيرات - ذكرنا ثلاث طرائق للتصريح عنها: let const var تتصرّف كلا الإفادتين let وconst بذات الطريقة (بالمقايسة مع البيئات المُعجمية). بينما var فهو وحش آخر مختلف جذريًا ويعود في أصله إلى قرون سحيقة. لا نستعمله عادةً في السكربتات الحديثة ولكنّك ستجده حتمًا خلف إحدى صخور السكربتات القديمة. لو لم ترغب بالتعرّف على هذه السكربتات فيمكنك تخطّي هذا الفصل أو تأجيله لوقت لاحق. ولكن لا تنسَ احتمالية ندمك لاحقًا فيغدر بك هذا الوحش. من أول وهلة نرى بأنّ تصرّف var يشابه تصرّف let، أي أنّه يُصرّح (مثل الثاني) عن متغير: function sayHi() { // متغير محلي، استعملنا «var» بدل «let» var phrase = "Hello"; alert(phrase); // Hello } sayHi(); alert(phrase); // خطأ، phrase غير معرّف ولكن… ما خفي كان أعظم. إليك الفروق. ليس لإفادة var نطاقًا كتليًا حين نصرّح عن المتغيرات باستعمال var نكون جعلناها معروفة للدالة كاملةً (لو كانت في دالة) أو عمومية في السكربت. يمكنك أن ترى تلك المتغيرات إن اخترقت «جدران» الكُتل. مثال: if (true) { var test = true; // نستعمل «var» بدل «let» } alert(test); // الناتج true، أي أنّ المتغير «حيّ يُرزق» بعد إفادة if تجاهل var كتل الشيفرة، وبهذا صار متغير test عموميًا. لو استعملنا let test بدل var test فسيكون المتغير ظاهرًا لباقي الشيفرة داخل إفادة if فقط لا غير: if (true) { let test = true; // نستعمل «let» } alert(test); // خطأ: لم يُعرّف عن test يسري الأمر ذاته على الحلقات فلا يمكن أن يكون var محليًا حسب الكتلة أو حسب الحلقة: for (var i = 0; i < 10; i++) { // ... } alert(i); // 10، ظهر «i» بعد الحلقة فهو متغير عمومي لو كتبت كتلة شيفرة في دالة فسيصير var متغيرًا على مستوى الدالة كاملةً. function sayHi() { if (true) { var phrase = "Hello"; } alert(phrase); // يمكننا فعل هذا } sayHi(); alert(phrase); // خطأ: phrase غير معرّف (طالِع مِعراض المطوّر) كما نرى فإفادة var تخترق كُتل if وfor وغيرها من كُتل شيفرة. يعزو ذلك إلى أنّه في الزمن الماضي الجميل لم تكن لكُتل جافاسكربت بيئات مُعجمية. وvar إحدى آثار ذلك الزمن. تعالج التصريحات باستعمال var عند بدء الدالة تُعالج التصريحات باستعمال var متى ما بدأت الدالة (أو بدأ السكربت، للمتغيرات العمومية). أي أنّ متغيرات var تُعرّف من بداية الدالة مهما كان مكان تعريفها (هذا لو لم يكن التعريف في دالة متداخلة أخرى). يعني ذلك أنّ هذه الشيفرة: function sayHi() { phrase = "Hello"; alert(phrase); var phrase; } sayHi(); متطابقة تقنيًا مع هذه (بتحريك var phrase إلى أعلى): function sayHi() { var phrase; phrase = "Hello"; alert(phrase); } sayHi(); أو حتى هذه (لا تنسَ بأنّ كُتل الشيفرات مُهملة): function sayHi() { phrase = "Hello"; // (*) if (false) { var phrase; } alert(phrase); } sayHi(); يدعو الناس هذا السلوك بسلوك «الطفو» hoisting (أو الرفع) إذ أنّ متغيرات var «تطفو» إلى أعلى الدالة (أو ترتفع إلى أعلاها). أي أنّه في المثال أعلاه، الفرع if (false) من الإفادة لا يعمل قط ولكن هذا ليس بمهم، إذ أنّ var داخله سيُعالج في بداية الدالة، وحين تصل عملية التنفيذ إلى (*) سيكون المتغير موجودًا لا محالة. التصريحات تطفو صحيح، ولكنّ ليس عبارات الإسناد. الأفضل لو نمثّل ذلك في هذا المثال: function sayHi() { alert(phrase); var phrase = "Hello"; } sayHi(); في السطر var phrase = "Hello" إجراءان اثنان: التصريح عن المتغير باستعمال var إسناد قيمة للمتغير باستعمال =. يتعامل المحرّك مع التصريحات متى بدء تنفيذ الدالة (إذ التصريحات تطفو)، ولكنّ عبارة الإسناد لا تعمل إلّا حيثما ظهرت، فقط. إذًا فالشيفرة تعمل بهذا النحو فعليًا: function sayHi() { var phrase; // بادئ ذي بدء، يعمل التصريح... alert(phrase); // غير معرّف phrase = "Hello"; // ...هنا. } sayHi(); يُعالج المحرّك التصريحات var حين تبدأ الدوال، وبهذا يمكننا الإشارة إليها أينما أردنا في الشيفرة. ولكن انتبه فالمتغيرات غير معرّفة حتى تُسند إليها قيم. في الأمثلة أعلاه عمل التابِع alert دون أيّ أخطاء إذ أن المتغير phrase موجود. ولكن لم تُسند فيه قيمة بعد فعرض undefined. ملخص هناك فرقين جوهرين بين var موازنةً بِـ let/const: ليس لمتغيرات var نطاقًا كتليًا وأصغر نطاق لها هو في الدوال. تُعالج التصريحات باستعمال var عند بدء الدالة (أو بدء السكربت، للمتغيرات العمومية). هناك فرق آخر صغير يتعلّق بالكائن العمومي وسنشرحه في الفصل التالي. بهذا، غالبًا ما يكون استعمال var أسوأ بكثير من let بعدما عرفت الفروق بينها، فالمتغيرات على مستوى الكُتل أمر رائع جدًا ولهذا السبب تمامًا أُضيفت let إلى معيار اللغة منذ زمن وصارت الآن الطريقة الأساسية (هي وconst) للتصريح عن متغير. ترجمة -وبتصرف- للفصل The old "var" من كتاب The JavaScript language
-
لغة جافاسكربت هي لغة داليّة التوجّه إلى أقصى حدّ، فتعطينا أقصى ما يمكن من حريّة. يمكننا إنشاء الدوال ديناميكيًا ونسخها إلى متغيرات أخرى أو تمريرها كوسيط إلى دالة أخرى واستدعائها من مكان آخر تمامًا لاحقًا حين نريد. كما نعلم بأنّ الدوال تستطيع الوصول إلى المتغيرات خارجها. نستعمل هذه الميزة كثيرًا. ولكن، ماذا يحدث حين يتغيّر المتغيّر الخارجي؟ هل تستلم الدالة أحدث قيمة له أو تلك التي كانت موجودة لحظة إنشاء الدالة؟ كما وماذا يحدث حين تنتقل الدالة إلى مكان آخر في الشيفرة واستُدعت من ذلك المكان: هل يمكنها الوصول إلى المتغيرات الخارجية في المكان الجديد؟ يختلف سلوك اللغات عن بعضها من هذه الناحية. في هذا الفصل سنتحدّث عن سلوك جافاسكربت. أسئلة تحتاج أجوبة لنبدأ أولًا بحالتين اثنتين ندرس بهما الآلية الداخلية للغة خطوةً بخطوة، بهذا ستملك ما يكفي لتُجيب على الأسئلة الآتية وأخرى غيرها أكثر تعقيدًا في المستقبل. تستعمل الدالة sayHi المتغير الخارجي name. ما القيمة التي ستستعملها الدالة حين تعمل؟ let name = "John"; function sayHi() { alert("Hi, " + name); } name = "Pete"; sayHi(); // ماذا ستعرض؟ «John» أم «Pete»؟ يشيع وجود هذه الحالات في المتصفّحات كما وفي الخواديم عند التطوير. يمكن أن تعمل الدالة بعدما تُنشأ بفترة لو أراد المطوّر (مثلًا بعد أن يتفاعل المستخدم أو يستلم المتصفّح طلبًا من الشبكة). إذًا فالسؤال هو: هل تستعمل آخر التعديلات؟ تصنع الدالة makeWorker دالةً أخرى وتُعيدها، ويمكن أن نستعدي تلك الدالة الجديدة من أيّ مكان آخر نريد. السؤال هو: هل يمكنها الوصول إلى المتغيرات الخارجية تلك التي من مكان إنشائها الأصلي، أم تلك التي في المكان الجديد، أم من المكانين معًا؟ function makeWorker() { let name = "Pete"; return function() { alert(name); }; } let name = "John"; // نصنع الدالة let work = makeWorker(); // نستدعيها work(); ماذا ستعرض الدالة work()؟ «Pete» (الاسم الذي تراه عند الإنشاء) أم «John» (الاسم الذي تراه عند الاستدعاء)؟ البيئات المعجمية علينا أولًا أن نعرف ما هو «المتغير» هذا أصلًا لنُدرك ما يجري بالضبط. في لغة جافاسكربت، تملك كلّ دالة عاملة أو كتلة شفرات {...} أو حتّى السكربت كلّه - تملك كائنًا داخليًا مرتبطًا بها (ولكنّه مخفي) يُدعى بالبيئة المُعجمية Lexical Environment. تتألّف كائنات البيئات المُعجمية هذه من قسمين: سجلّ مُعجمي Environment Record: وهو كائن يخزّن كافة المتغيرات المحلية على أنّها خاصيات له (كما وغيرها من معلومات مثل قيمة this). إشارة إلى البيئة المُعجمية الخارجية - أي المرتبطة مع الشيفرة الخارجية للكائن المُعجمي. ليس «المتغير» إلا خاصية لإحدى الكائنات الداخلية الخاصة: السجل المُعجمي Environment Record. وحين نعني «بأخذ المتغير أو تغيير قيمته» فنعني «بأخذ خاصية ذلك الكائن أو تغيير قيمتها». إليك هذه الشيفرة البسيطة مثالًا (فيها بيئة مُعجمية واحدة فقط): هذا ما نسمّيه البيئة المُعجمية العمومية (global) وهي مرتبطة بالسكربت كاملًَا. نعني بالمستطيل (في الصورة أعلاه) السجل المُعجمي (أي مخزن المتغيرات)، ونعني بالسهم الإشارة الخارجية له. وطالما أنّ البيئة المُعجمية العمومية ليس لها إشارة خارجية، فذاك السهم يُشير إلى null. وهكذا تتغيّر البيئة حين تعرّف عن متغيّر وتُسند له قيمة: نرى في المستطيلات على اليمين كيف تتغيّر البيئة المُعجمية العمومية أثناء تنفيذ الشيفرة: حين تبدأ الشيفرة، تكون البيئة المُعجمية فارغة. بعدها يظهر التصريح let phrase، لكن لم تُسند للمتغيّر أيّ قيمة، لذا تخزّن البيئة undefined. تُسند للمتغير phrase قيمة. وهنا تتغيّر قيمة phrase. بسيط حتّى الآن، أم لا؟ نلخّص الموضوع: المتغير هو فعليًا خاصية لإحدى الكائنات الداخلية الخاصة، وهذا الكائن مرتبط بالكتلة أو الدالة أو السكربت الذي يجري تنفيذه حاليًا. حين نعمل مع المتغيرات نكون في الواقع نعمل مع خصائص ذلك الكائن. التصريح بالدوال لم نرى حتّى اللحظة إلا المتغيرات. حان وقت التصريحات بالدوال. الدوال على عكس متغيرات let، فليست تُهيّأ تمامًا حين تصلها عملية التنفيذ، لا، بل قبل ذلك حين تُنشأ البيئة المُعجمية. وحين نتكلم عن أعلى الدوال مستوًى، فنعني ذلك لحظة بدء السكربت. ولهذا السبب يمكننا استدعاء الدوال التي صرّحناها حتّى قبل أن نرى ذاك التعريف. نرى في الشيفرة أدناه كيف أنّ البيئة المُعجمية تحتوي شيئًا منذ بداية التنفيذ (وليست فارغة)، وما تحتويه هي say إذ أنّها تصريح عن دالة. وبعدها تسجّل phrase المُصرّح باستعمال let: البيئات المُعجمية الداخلية والخارجية الآن لنتعمّق ونرى ما يحدث حين تحاول الدالة الوصول إلى متغير خارجي. تستعمل say() أثناء الاستعداء المتغير الخارجي phrase. لنرى تفاصيل ما يجري بالضبط. تُنشأ بيئة مُعجمية تلقائيًا ما إن تعمل الدالة وتخزّن المتغيرات المحلية ومُعاملات ذلك الاستدعاء فمثلًا هكذا تبدو بيئة استدعاء say("John") (وصل التنفيذ السطر الذي عليه سهم): let phrase = "Hello"; function say(name) { alert( `${phrase}, ${name}` ); } say("John"); // Hello, John إذًا… حين نكون داخل استدعاءً لأحد الدوال نرى لدينا بيئتين مُعجميتين: الداخلية (الخاصة باستدعاء الدالة) والخارجية (العمومية): ترتبط البيئة المُعجمية الداخلية مع عملية التنفيذ الحالية للدالة say. تملك خاصية واحدة فقط: name (وسيط الدالة). ونحن استدعينا say("John") بهذا تكون قيمة name هي "John". البيئة المُعجمية الخارجية وهي هنا البيئة المُعجمية العمومية. تملك متغير phrase والدالة ذاتها. للبيئة المُعجمية الداخلية إشارة إلى تلك «الخارجية». حين تريد الشيفرة الوصول إلى متغير من المتغيرات، يجري البحث أولًا في البيئة المُعجمية الداخلية، وبعدها الخارجية، والخارجية أكثر وأكثر وكثر حتى نصل العمومية. لو لم يوجد المتغير في عملية البحث تلك فسترى خطأً (لو استعملت النمط الصارم Strict Mode). لو لم تستعمل use strict فسيُنشئ الإسناد إلى متغير غير موجود (مثل user = "John") متغيرًا عموميًا جديدًا باسم user. سبب ذلك هو التوافق مع الإصدارات السابقة. لنرى عملية البحث تلك في مثالنا: حين تحاول alert في دالة say الوصول إلى المتغير name تجده مباشرةً في البيئة المُعجمية للدالة. وحين تحاول الوصول إلى متغير phrase ولا تجده محليًا، تتبع الإشارة في البيئة المحلية وتصل البيئة المُعجمية خارجها، وتجد المتغير فيها. يمكننا أخيرًا تقديم إجابة على السؤال في أول الفصل. تأخذ الدالة المتغيرات الخارجية من مكانها الآن، أي أنها تستعمل أحدث القيم. القيم القديمة لا تُحفظ في أي مكان مهما بحثت. فحين تريد إحدى الدوال متغيرًا ما تأخذ قيمته الحالية من بيئتها المُعجمية هي أو الخارجية بالنسبة لها. إذًا، إجابة السؤال الأول هي Pete: let name = "John"; function sayHi() { alert("Hi, " + name); } name = "Pete"; // (*) sayHi(); // (*) Pete سير تنفيذ الشيفرة أعلاه: للبيئة المُعجمية العمومية name: "John". في السطر (*) يتغيّر المتغير العمومي ويصير الآن name: "Pete". تأخذ الدالة sayHi() حين تتنفّذ قيمة name من الخارج (أي البيئة المُعجمية العمومية) حيث صارت الآن "Pete". لكلّ استدعاء منك، بيئة مُعجمية من اللغة لاحظ بأنّ محرّك اللغة يُنشئ بيئة مُعجمية جديدة للدالة في كلّ مرة تعمل فيها الدالة. ولو استدعيت الدالة أكثر من مرة فلكلّ استدعاء منها بيئة مُعجمية خاصة بها مستقلة المتغيرات المحلية والمُعاملات، ومخصّصة فقط لذلك الاستدعاء. البيئات المُعجمية كائن في توصيف اللغة كائن «البيئة المُعجمية» (Lexical Environment) هو كائن في توصيف اللغة، أي أنّه موجود «نظريًا» فقط في توصيف اللغة لشرح طريقة عمل الأمور، ولا يمكننا أخذ هذا الكائن في الشيفرة ولا التعديل عليه مباشرةً. كما يمكن أن تُحسّن محرّكات جافاسكربت هذا الكائن أو تُهمل المتغيرات غير المستخدمة فتحفظ الذاكرة أو غيرها من خُدع داخلية، كلّ هذا بمنأًى عن السلوك الظاهر لنا فيظلّ كما هو. الدوال المتداخلة تكون الدالة «متداخلة» متى صنعتها داخل دالة أخرى. ويمكنك بسهولة بالغة فعل ذلك داخل جافاسكربت. يمكننا استعمال هذه الميزة لتنظيم الشيفرة الإسباغيتية، هكذا: function sayHiBye(firstName, lastName) { // دالة مساعدة متداخلة نستعملها أسفله function getFullName() { return firstName + " " + lastName; } alert( "Hello, " + getFullName() ); alert( "Bye, " + getFullName() ); } صنعنا هنا الدالة المتداخلة getFullName() لتسهّل حياتنا علينا، فيمكنها هي الوصول إلى المتغيرات الخارجية وإعادة اسم الشخص الكامل. كثيرًا ما نستعمل الدوال المتداخلة في جافاسكربت. والممتع أكثر هو أنّه يمكننا إعادة الدوال المتداخلة، إمّا باعتبارها خاصية لكائن جديد (لو كانت الدالة الخارجية تصنع كائنًا له توابع) أو أن تكون نتيجةً للدالة مستقلة بذاتها. ويمكننا لاحقًا استعمالها أينما أردنا، وأيّما كان مكانها الجديد فلديها الحقّ بالوصول إلى ذات المتغيرات الخارجية تلك. مثال على ذلك: أسندنا الدالة المتداخلة إلى كائن جديد باستعمال دالة مُنشئة: // تُعيد الدالة المُنشئة كائنًا جديدًا function User(name) { // نصنع تابِع الكائن على أنّه دالة متداخلة this.sayHi = function() { alert(name); }; } let user = new User("John"); // يمكن أن تصل شيفرة تابِع الكائن «sayHi» إلى «name» الخارجي user.sayHi(); وهنا أنشأنا دالة «عدّ» وأعدناها، لا أكثر: function makeCounter() { let count = 0; return function() { // يمكنها الوصول إلى متغير «count» الخارجي return count++; }; } let counter = makeCounter(); alert( counter() ); // 0 alert( counter() ); // 1 alert( counter() ); // 2 لنتفحّص مثال makeCounter. تصنع الشيفرة دالة «العدّ» وتُعيد العدد التالي كلّما استدعيناها. صحيح أنّ الدالة بسيطة لكن بتعديلها قليلًا يمكن استعمالها لأمور عديدة مفيدة مثل مولّدات الأعداد شبه العشوائية وغيرها. ولكن كيف يعمل هذا العدّاد داخليًا؟ متى ما عملت الدالة الداخلية، تبدأ بالبحث عن المتغير في count++ بدءًا منها وانطلاقًا إلى خارجها. فهكذا سيكون الترتيب في المثال أعلاه: المتغيرات المحلية للدالة المتداخلة… المتغيرات المحلية للدالة الخارجية… وهكذا حتى نصل المتغيرات العمومية. في هذا المثال وجدنا المتغير count في الخطوة الثانية. فلو عُدّلت قيمة المتغير الخارجي فيحدث هذا التعديل في المكان الذي وجدنا المتغير فيه. لهذا تجد count++ المتغير الخارجي وتزيد قيمته في البيئة المُعجمية التي ينتمي المتغير إليها، تمامًا كما لو استعملنا let count = 1. إليك سؤالين تفكّر بهما (أيضًا): هل يمكننا بطريقة أو بأخرى تصفير العدّاد count من الشيفرة التي لا تنتمي إلى makeCounter؟ مثلًا بعد استدعاءات alert في المثال أعلاه. حين نستعدي makeCounter() أكثر من مرة تُعيد لنا دوال counter كثيرة. فهل هي مستقلة بذاتها أم تتشارك ذات متغير count؟ حاول حلّ السؤالين قبل مواصلة القراءة. انتهيت؟ إذًا حان وقت الإجابات. ما من طريقة أبدًا: متغير count هو متغير محلي داخل إحدى الدوال ولا يمكننا الوصول إليه من الخارج. كلّ استدعاء من makeCounter() يصنع بيئة مُعجمية جديدة للدالة لها متغير count خاص بها. لذا فدوال counter الناتج مستقلة عن بعضها البعض. إليك شيئًا تجربّه بنفسك: function makeCounter() { let count = 0; return function() { return count++; }; } let counter1 = makeCounter(); let counter2 = makeCounter(); alert( counter1() ); // 0 alert( counter1() ); // 1 alert( counter2() ); // 0 (مستقل) آمل بأنّ الصورة الآن صارت أوضح أكثر. صحيح أنّا تكلمنا فقط عن المتغيرات الخارجية ولكن إدراك هذا القدر فقط يكفي أغلب الأحيان. هناك طبعًا تفاصيل أخرى في مواصفات اللغة لم نتحدّث عنها للإيجاز. في القسم التالي سنتكلم عن هذه التفاصيل أكثر. البيئات بالتفصيل الممل إليك ما يجري في مثال makeCounter خطوةً بخطوة. احرص على اتباعه لتحرص على فهم آلية عمل البيئات بالتفصيل. لاحظ أنّا شرحنا الخاصية الإضافية [[Environment]] هنا، ولم نشرحها سابقًا للتبسيط. حين يبدأ السكربت لا يكون هناك إلى بيئة مُعجمية عمومية: في تلك اللحظة ليس هناك إلا دالة makeCounter إذ أنها تصريح عن دالة، ولم يبدأ تشغيلها بعد. تستلم كافة الدوال «لحظة إفاقتها للحياة» خاصية مخفية باسم [[Environment]] فيها إشارة إلى البيئة المُعجمية حيث أُنشئت. لم نتحدّث عن هذه قبلًا، وهي الطريقة التي تعرف الدالة فيها مكان صناعتها الأولي. هنا أُنشأت makeCounter في البيئة المُعجمية العمومية، ولهذا فتُبقي [[Environment]] إشارة إليها. أي وبعبارة أخرى، «نطبع» على الدالة إشارةً للبيئة المُعجمية التي نشأت فيها، وخاصية [[Environment]] هي الخاصية الدالية المخفية التي تسجّل تلك الإشارة. تبدأ أخيرًا الشيفرة بالعمل، ويرى المحرّك متغيرا عموميًا جديدًا بالاسم counter صرّحنا عنه وقيمته هي ناتج استعداء makeCounter(). إليك نظرة على اللحظة التي تكون فيها عملية التنفيذ على أول سطر داخل makeCounter(): تُنشأ بيئة مُعجمية لحظة استدعاء makeCounter() لتحمل متغيراتها ومُعاملاتها. وكما الحال مع البيئات هذه فهي تخزّن أمرين: سجلّ بيئي فيه المتغيرات المحلية. في حالتنا هنا متغير count هو الوحيد المحلي (يظهر حين يُنفّذ سطر let count). الإشارة إلى البيئة المُعجمية الخارجية (وتُضبط قيمة لخاصية [[Environment]] للدالة). تُشير هنا [[Environment]] للدالة makeCounter إلى البيئة المُعجمية العمومية. إذًا لدينا بيئتين مُعجميتين اثنتين: الأولى عمومية والثانية مخصّصة لاستدعاء makeCounter الحالي، بينما الإشارة الخارجية لها هي البيئة العمومية. تُصنع -أثناء تنفيذ makeCounter()- دالة صغيرة متداخلة. لا يهمّنا إن كان التصريح عن الدالة أم تعبير الدالة هو من أنشأ… الدالة، فالخاصية [[Environment]] تُضاف لكل الدوال، وتُشير إلى البيئة المُعجمية التي صُنعت فيها تلك الدوال. وبطبيعة الحال فهذه الدالة الصغيرة المتداخلة لديها نصيب من الكعكة. قيمة الخاصية [[Environment]] للدالة المتداخلة هذه هي البيئة المُعجمية الحالية للدالة makeCounter() (مكان صناعة الدالة المتداخلة): لاحظ أنّ الدالة الداخلية (في هذه الخطوة) أُنشئت صحيح ولكن لم نستعدها بعد. الشيفرة في return count++; لا تعمل. تُواصل عملية التنفيذ العمل وينتهي استدعاء makeCounter() ويُسند ناتجها (وهو الدالة المتداخلة الصغيرة) إلى المتغير العمومي counter: ليس لتلك الدالة إلا سطرًا واحدًا: return count++ وسيُنفّذ ما إن نشغّل الدالة. وحين استدعاء counter() تُنشأ بيئة مُعجمية جديدة، لكنّها فارغة إذ أن ليس للدالة counter متغيرات محلية فيها، إلّا أنّ لخاصية الدالة counter [[Environment]] فائدة ففيها الإشارة «الخارجية» للدالة وهي التي تتيح لنا الوصول إلى متغيرات استدعاء makeCounter() السابق متى ما أنشأناه: أما الآن فحين يبحث الاستدعاء عن متغير count فهو يبحث أولًا في بيئته المُعجمية (الفارغة)، فلو لم يجدها بحث في البيئة المُعجمية لاستدعاء makeCounter() الخارجي، ويجد المتغير فيه. لاحظ آلية إدارة الذاكرة هنا. صحيح أنّ استدعاء makeCounter() انتهى قبل فترة إلا أن بيئته المُعجمية بقيت في الذاكرة لأنّ الدالة المتداخلة تحمل الخاصية [[Environment]] التي تُشير إلى تلك البيئة. يمكن القول بصفة عامة بأنّ البيئة المُعجمية لا تموت طالما يمكن لدالة من الدوال استعمالها. وحين لا توجد هكذا دالة - حينها تُمسح البيئة. لا يُعيد استدعاء counter() قيمة الخاصية count فحسب، بل أيضًا يزيدها واحدًا. لاحظ كيف أنّ التعديل حدث «في ذات مكانه» In place، فتعدّلت قيمة count في البيئة ذاتها التي وجدناه فيها. تعمل استدعاءات counter() التالية بنفس الطريقة. أفترض الآن بأنّ إجابة السؤال الثاني في أول الفصل ستكون جليّة. دالة work() في الشيفرة أدناه تأخذ الاسم name من مكانه الأصل عبر إشارة البيئة المُعجمية الخارجية إليه: إذًا، فالناتج هنا هو "Pete". ولكن لو لم نكتب let name في makeWorker() فسينتقل البحث إلى خارج الدالة تلك ويأخذ القيمة العمومية كما نرى من السلسلة أعلاه. في تلك الحالة سيكون الناتج "John". المنغلقات هناك مصطلح عام يُستعمل في البرمجة باسم «المُنغلِق» Clousure ويُفترض أن يعلم به المطوّرون. المُنغِلق هو دالة تتذكّر متغيراتها الخارجية كما ويمكنها أن تصل إليها. هذا الأمر -في بعض اللغات- مستحيل، أو أنّه يلزم كتابة الدالة بطريقة معيّنة ليحدث ذلك. ولكن كما شرحنا أعلاه ففي لغة جافاسكربت، كلّ الدوال مُنغلِقات بطبيعتها (وطبعًا ثمّة استثناء واحد أوحد نشرحه في فصل «صياغة الدالة الجديدة»). يعني ذلك بأنّ الدوال تتذكّر أين أُنشئت باستعمال خاصية [[Environment]] المخفية، كما ويمكن للدوال كافة الوصول إلى متغيراتها الخارجية. لو كنت عزيزي مطوّر الواجهات في مقابلةً وأتاك السؤال «ما هو المُنغلِق؟» فيمكنك أن تقدّم تعريفه شرحًا، كما وتُضيف بأنّ الدوال في جافاسكربت كلّها مُنغلِقات، وربما شيء من عندك تفاصيل تقنية مثل خاصية [[Environment]] وطريقة عمل البيئات المُعجمية. كُتل الشفرات والحلقات، تعابير الدوال الآنية ركّزتُ في الأمثلة أعلاه على الدوال، إلا أنّ البيئة المُعجمية موجودة لكلّ كتلة شيفرات {...}. تُنشأ البيئة المُعجمية حين تعمل أيّ كتلة شيفرات فيها متغيرات تُعدّ محلية لهذه الكتلة. إليك بعض الأمثلة. الجملة الشرطية If في المثال أسفله نرى المتغير user موجودًا فقط داخل كتلة if: let phrase = "Hello"; if (true) { let user = "John"; alert(`${phrase}, ${user}`); // Hello, John } alert(user); // خطأ! لا أرى هذا المتغير! حين تصل عملية التنفيذ داخل كتلة if يُنشئ المحرك البيئة المُعجمية «فقط وفقط إذا كذا…». لهذه البيئة إشارة إلى البيئة الخارجية، بهذا يمكن أن تجد المتغير phrase. ولكن على العكس فالمتغيرات وتعابير الدوال المصرَّح عنها داخل if في تلك البيئة المُعجمية لا يمكن أن نراها من الخارج. فمثلًا بعدما تنتهي إفادة if لن يرى التابِع alert أسفلها متغير user، وهذا سبب الخطأ. حلقة «كرّر طالما» لكلّ دورة في حلقة التكرار بيئة مُعجمية خاصة بها. وأيضًا لو صرّحت عن متغير في for(let ...) فسيكون موجودًا فيها: for (let i = 0; i < 10; i++) { // لكلّ دورة بيئة مُعجمية خاصة بها // {i: value} } alert(i); // خطأ، ما من متغير كهذا لاحظ كيف أنّ الإفادة let i خارج كتلة {...} بصريًا. مُنشئ حلقة for خاص نوعًا ما: لكلّ دورة من الحقة بيئة مُعجمية خاصة بها تحمل قيمة i الحالية فيها أيضًا. وكما مع if فبعد انتهاء الحلقة لا نرى i خارجها. كتل الشفرات يمكننا أيضًا استعمال كتلة شفرات{…} «مجرّدة» لنعزل المتغيرات في «نطاق محلي» خاص بها. فمثلًا في متصفّح الوب تتشارك كل السكربتات (عدا التي فيها type="module") نفس المساحة العمومية. لذا لو أنشأنا متغيرًا عموميًا في واحد من السكربتات يمكن أن تراه البقية. هذا الأمر يتسبب بمشكلة لو استعمل سكربتان اثنان نفس اسم المتغير وبدأ كلّ منهما بتعويض الذي عند الآخر. يمكن أن يحدث هذا لو كان اسم المتغير كلمة شائعة (مثلًا name) ولا يدري مطورو السكربتات ما يفعله الغير. يمكن أن نستعمل كتلة شيفرات لغول السكربت كاملًا أو جزءًا منه حتى لو أردنا تجنّب هذه المشكلة: { // نُجري أمرًا على المتغيرات المحلية يُمنع على ما خارجنا رؤيته let message = "Hello"; alert(message); // Hello } alert(message); // خطأ: message غير معرّف لا ترى الشيفرات خارج تلك الكتلة (أو حتى الموجودة في سكربت آخر) المتغيرات داخل الكتلة إذ أنّ لها بيئتها المُعجمية الخاصة بها. تعابير الدوال الآنية IIFE سابقًا لم تكن هناك بيئات مُعجمية للكُتل في جافاسكربت. وكما «الحاجة أمّ الاختراع»، فكان على المطوّرين حلّ ذلك، وهكذا صنعوا ما سمّوه «تعابير الدوال آنيّة الاستدعاء» Immediately-Invoked Function Expressions. لست تريد أن تكتب هذا النوع من الدوال في وقتنا الآن، ولكن يمكن أن تصادفك وأنت تطالع السكربتات القديمة فالأفضل لو تعرف كيف تعمل من الآن. إليك شكل الدوال الآنية هذه: (function() { let message = "Hello"; alert(message); // Hello })(); هنا أنشأنا تعبير دالة واستدعيناه مباشرةً/آنيًا. لذا فتعمل الشيفرة في لحظتها كما وفيها متغيراتها الخاصة بها. الطريقة هي أن نُحيط تعبير الدالة بأقواس (function {...}) إذ أنّ محرّك جافاسكربت حين يقابل "function" في الشيفرة الأساسية يفهمها وكأنّها تصريح عن دالة. ولكن، التصريح عن الدوال يحتاج اسمًا لها، بهذا فهذه الشيفرة ستَسبّب بخطأ: // نحاول التصريح عن الدالة واستدعائها آنيًا function() { // <-- Error: Unexpected token ( let message = "Hello"; alert(message); // Hello }(); حتّى لو قلنا «طيب لنضيف ذلك الاسم» فلن ينفع إذ أنّ محرّك جافاسكربت لا يسمح باستدعاء التصاريح عن الدوال آنيًا: // خطأ صياغي بسبب الأقواس أسفله function go() { }(); // <-- لا يمكن أن نستدعي التصريح عن الدوال آنيًا إذًا، فالأقواس حول الدالة ما هي إلا خدعة لنضحك على محرّك جافاسكربت ونُقنعه بأنّا أنشأنا الدالة في سياق تعبير آخر وبهذا فهي تعبير دالة… أي لا تحتاج اسمًا ويمكن استدعائها آنيًا. ثمّة طرق أخرى دون الأقواس لنُقنع المحرّك بأنّ ما نعني هو تعبير الدالة: // طرائق إنشاء هذه التعابير الآنية (function() { alert("أقواس تحيط بالدالة"); }) (); (function() { alert("أقواس تحيط بكامل الجملة"); }() ); ! function() { alert("عملية الأعداد الثنائية NOT أوّل التعبير"); }(); + function() { alert("عملية الجمع الأحادية أوّل التعبير"); }(); في كلّ الحالات أعلاه: صرّحنا عن تعبير دالة واستدعيناها آنيًا. لنوضّح ذلك ثانيةً: لم يعد هناك أيّ داع لنكتب هكذا شيفرات في وقتنا الحاضر. كنس المهملات عادةً ما تُمسح وتُحذف البيئة المُعجمية بعدما تعمل الدالة. مثال: function f() { let value1 = 123; let value2 = 456; } f(); هنا القيمتين (تقنيًا) خاصيتين للبيئة المُعجمية. ولكن حين تنتهي f() لا يمكن أن نصل إلى تلك البيئة بأيّ طريقة فتُحذف من الذاكرة. …ولكن لو كانت هناك دالة متداخلة يمكن أن نصل إليها بعدما تنتهي f (ولديها خاصية [[Environment]] التي تُشير إلى البيئة المُعجمية الخارجية)، لو كانت فيمكن أن نصل إليها: function f() { let value = 123; function g() { alert(value); } return g; // (*) } let func = f(); // يمكن أن تصل func الآن بإشارة إلى g // بذلك تبقى في الذاكرة، ومعها بيئتها المُعجمية الخارجية لاحظ بأنّه لو استدعينا f() أكثر من مرة، فسوف تُحفظ الدوال الناتجة منها وتبقى كائنات البيئة المُعجمية لكلّ واحدة منها في الذاكرة. إليك ثلاثة منها في الشيفرة أدناه: function f() { let value = Math.random(); return function() { alert(value); }; } // في المصفوفة ثلاث دوال تُشير كلّ منها إلى البيئة المُعجمية // في عملية التنفيذ f() المقابلة لكلّ واحدة let arr = [f(), f(), f()]; يموت كائن البيئة المُعجمية حين لا يمكن أن يصل إليه شيء (كما الحال مع أيّ كائن آخر). بعبارة أخرى فهو موجود طالما ثمّة دالة متداخلة واحدة (على الأقل) في الشيفرة تُشير إليه. في الشيفرة أسفله، بعدما تصير g محالة الوصول تُمسح بيئتها المُعجمية فيها (ومعها متغير value) من الذاكرة: function f() { let value = 123; function g() { alert(value); } return g; } // طالما يمكن أن تصل func بإشارة إلى g، ستظلّ تشغل حيّزًا في الذاكرة let func = f(); // ...والآن لم تعد كذلك ونكون قد نظّفنا الذاكرة func = null; التحسينات على أرض الواقع كما رأينا، فنظريًا طالما الدالة «حيّة تُرزق» تبقى معها كل متغيراتها الخارجية. ولكن عمليًا تُحاول محرّكات جافاسكربت تحسين أداء ذلك. فهي تحلّل استعمال المتغيرات فلو كان واضحًا لها في الشيفرة بأنّ المتغير الخارجي لم يعد مستعملًا، تحذفه. ثمّة -في محرّك V8 (كروم وأوبرا)- تأثير مهمّ ألا وهو أنّ هذا المتغير لن يكون مُتاحًا أثناء التنقيح. جرّب تشغيل المثال الآتي في «أدوات المطوّرين» داخل متصفّح كروم. ما إن يُلبث تنفيذ الشيفرة، اكتب alert(value) في الطرفية. function f() { let value = Math.random(); function g() { debugger; // اكتب في المِعراض: alert(value); ما من متغير كهذا! } return g; } let g = f(); g(); كما رأينا، ما من متغير كهذا! يُفترض نظريًا أن نصل إليه ولكنّ المحرّك حسّن أداء الشيفرة وحذفه. يؤدّي ذلك أحيانًا إلى مشاكل مضحكة (هذا إن لم تجلس عليها اليوم بطوله لحلّها) أثناء التنقيح. إحدى هذه المشاكل هي أن نرى المتغير الخارجي بدل الذي توقّعنا أن نراه (يحمل كلاهما نفس الاسم): let value = "Surprise!"; function f() { let value = "the closest value"; function g() { debugger; // اكتب في المِعراض: alert(value); إليك Surprise! } return g; } let g = f(); g(); إلى فصل آخر! من المفيد معرفة هذه الميزة في معيار V8. متى ما بدأت التنقيح في كروم أو أوبرا، فستراها شئت أم أبيت. هذه ليست علّة في المنقّح بل هي ميزة خاصة في معيار V8. ربما تتغير لاحقًا من يدري. يمكنك أن تتحقّق منها متى أردت بتجربة الأمثلة في هذه الصفحة. تمارين هل العدّادات مستقلة عن بعضها البعض؟ الأهمية: 5 صنعنا هنا عدّادين اثنين counter و counter2 باستعمال ذات الدالة makeCounter. هل هما مستقلان عن بعضهما البعض؟ ما الذي سيعرضه العدّاد الثاني؟ 0,1 أم 2,3 أم ماذا؟ function makeCounter() { let count = 0; return function() { return count++; }; } let counter = makeCounter(); let counter2 = makeCounter(); alert( counter() ); // 0 alert( counter() ); // 1 alert( counter2() ); // ؟ alert( counter2() ); // ؟ الحل الإجابة هي: 0,1. صنعنا الدالتين counter و counter2 باستدعاءين makeCounter مختلفين تمامًا. لذا فلكلّ منهما بيئات مُعجمية خارجية مستقلة عن بعضها، ولكلّ منهما متغير count مستقل عن الثاني. كائن عد الأهمية: 5 هنا صنعنا كائن عدّ بمساعدة دالة مُنشئة Constructor Function. هل ستعمل؟ ماذا سيظهر؟ function Counter() { let count = 0; this.up = function() { return ++count; }; this.down = function() { return --count; }; } let counter = new Counter(); alert( counter.up() ); // ؟ alert( counter.up() ); // ؟ alert( counter.down() ); // ؟ الحل طبعًا، ستعمل كما يجب. صُنعت الدالتين المتداخلتين في نفس البيئة المُعجمية الخارجية، بهذا تتشاركان نفس المتغير count وتصلان إليه: function Counter() { let count = 0; this.up = function() { return ++count; }; this.down = function() { return --count; }; } let counter = new Counter(); alert( counter.up() ); // 1 alert( counter.up() ); // 2 alert( counter.down() ); // 1 دالة في شرط if طالِع الشيفرة أسفله. ما ناتج الاستدعاء في آخر سطر؟ let phrase = "Hello"; if (true) { let user = "John"; function sayHi() { alert(`${phrase}, ${user}`); } } sayHi(); الحل الناتج هو: خطأ. صُرّح عن الدالة sayHi داخل الشرط if وتعيش فيه فقط لا غير. ما من دالة sayHi خارجية. المجموع باستعمال المُنغلِقات الأهمية: 4 اكتب الدالة sum لتعمل هكذا: sum(a)(b) = a+b. نعم عينك سليمة، هكذا تمامًا باستعمال قوسين اثنين (ليست خطأً مطبعيًا). مثال: sum(1)(2) = 3 sum(5)(-1) = 4 الحل ليعمل القوسين الثانيين، يجب أن يُعيد الأوليين دالة. هكذا: function sum(a) { return function(b) { return a + b; // تأخذ «a» من البيئة المُعجمية الخارجية }; } alert( sum(1)(2) ); // 3 alert( sum(5)(-1) ); // 4 الترشيح عبر دالة الأهمية: 5 نعلم بوجود التابِع arr.filter(f) للمصفوفات. ووظيفته هي ترشيح كلّ العناصر عبر الدالة f. لو أرجعت true فيُعيد التابِع العنصر في المصفوفة الناتجة. اصنع مجموعة مرشّحات «جاهزة لنستعملها مباشرة»: inBetween(a, b) -- بين a وbبما فيه الطرفين (أي باحتساب a وb). inArray([...]) -- في المصفوفة الممرّرة. هكذا يكون استعمالها: arr.filter(inBetween(3,6)) -- تحدّد القيم بين 3 و6 فقط. arr.filter(inArray([1,2,3])) -- تحدّد العناصر المتطابقة مع أحد عناصر [1,2,3] فقط. مثال: // .. شيفرة الدالتين inBetween وinArray let arr = [1, 2, 3, 4, 5, 6, 7]; alert( arr.filter(inBetween(3, 6)) ); // 3,4,5,6 alert( arr.filter(inArray([1, 2, 10])) ); // 1,2 الحل المرشّح inBetween function inBetween(a, b) { return function(x) { return x >= a && x <= b; }; } let arr = [1, 2, 3, 4, 5, 6, 7]; alert( arr.filter(inBetween(3, 6)) ); // 3,4,5,6 المرشّح inArray function inArray(arr) { return function(x) { return arr.includes(x); }; } let arr = [1, 2, 3, 4, 5, 6, 7]; alert( arr.filter(inArray([1, 2, 10])) ); // 1,2 الترشيح حسب حقل الاستمارة الأهمية: 5 أمامنا مصفوفة كائنات علينا ترتيبها: let users = [ { name: "John", age: 20, surname: "Johnson" }, { name: "Pete", age: 18, surname: "Peterson" }, { name: "Ann", age: 19, surname: "Hathaway" } ]; الطريقة الطبيعية هي الآتي: // حسب الاسم (Ann, John, Pete) users.sort((a, b) => a.name > b.name ? 1 : -1); // حسب العمر (Pete, Ann, John) users.sort((a, b) => a.age > b.age ? 1 : -1); هل يمكن أن تكون بحروف أقل، هكذا مثلًا؟ users.sort(byField('name')); users.sort(byField('age')); أي، بدل أن نكتب دالة، نضع byField(fieldName) فقط. اكتب الدالة byField لنستعملها هكذا. الحل let users = [ { name: "John", age: 20, surname: "Johnson" }, { name: "Pete", age: 18, surname: "Peterson" }, { name: "Ann", age: 19, surname: "Hathaway" } ]; function byField(field) { return (a, b) => a[field] > b[field] ? 1 : -1; } users.sort(byField('name')); users.forEach(user => alert(user.name)); // Ann, John, Pete users.sort(byField('age')); users.forEach(user => alert(user.name)); // Pete, Ann, John جيش عرمرم من الدوال الأهمية: 5 تصنع الشيفرة الآتية مصفوفة من مُطلقي النار shooters. يفترض أن تكتب لنا كلّ دالة رقم هويّتها، ولكن ثمّة خطب فيها… function makeArmy() { let shooters = []; let i = 0; while (i < 10) { let shooter = function() { // دالة مُطلق النار alert( i ); // المفترض أن ترينا رقمها }; shooters.push(shooter); i++; } return shooters; } let army = makeArmy(); army[0](); // مُطلق النار بالهويّة 0 يقول أنّه 10 army[5](); // مُطلق النار بالهويّة 5 يقول أنّه 10... // ... كلّ مُطلقي النار يقولون 10 بدل هويّاتهم 0 فَـ 1 فَـ 2 فَـ 3... لماذا هويّة كلّ مُطلق نار نفس البقية؟ أصلِح الشيفرة لتعمل كما ينبغي أن تعمل. الحل لنُجري مسحًا شاملًا على ما يجري في makeArmy، حينها يظهر لنا الحل جليًا. تُنشئ مصفوفة shooters فارغة: let shooters = []; تملأ المصفوفة في حلقة عبر shooters.push(function...). كلّ عنصر هو دالة، بهذا تكون المصفوفة الناتجة هكذا: shooters = [ function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); } ]; تُعيد الدالة المصفوفة. لاحقًا، يستلم استدعاء army[5]() العنصر army[5] من المصفوفة، وهي دالة فيستدعيها. الآن، لماذا تعرض كلّ هذه الدوال نفس الناتج؟ يعزو ذلك إلى عدم وجود أيّ متغير محلي باسم i في دوال shooter. فحين تُستدعى هذه الدالة تأخذ المتغير i من البيئة المُعجمية الخارجية. وماذا ستكون قيمة i؟ لو رأينا مصدر القيمة: function makeArmy() { ... let i = 0; while (i < 10) { let shooter = function() { // دالة مُطلق النار alert( i ); // المفترض أن ترينا رقمها }; ... } ... } كما نرى… «تعيش» القيمة في البيئة المُعجمية المرتبطة بدورة makeArmy() الحالية. ولكن متى استدعينا army[5]()، تكون دالة makeArmy قد أنهت مهمّتها فعلًا وقيمة i هي آخر قيمة، أي 10 (قيمة نهاية حلقة while). وبهذا تأخذ كلّ دوال shooter القيمة من البيئة المُعجمية الخارجية، ذات القيمة الأخيرة i=10. يمكن أن نُصلح ذلك بنقل تعريف المتغير إلى داخل الحلقة: function makeArmy() { let shooters = []; // (*) for(let i = 0; i < 10; i++) { let shooter = function() { // دالة مُطلق النار alert( i ); // المفترض أن ترينا رقمها }; shooters.push(shooter); } return shooters; } let army = makeArmy(); army[0](); // 0 army[5](); // 5 الآن صارت تعمل كما يجب إذ في كلّ مرة تُنفّذ كتلة الشيفرة في for (let i=0...) {...}، يُنشئ المحرّك بيئة مُعجمية جديدة لها فيها متغير i المناسب لتلك الكتلة. إذًا لنلخّص: قيمة i صارت «تعيش» أقرب للدالة من السابق. لم تعد في بيئة makeArmy() المُعجمية بل الآن في تلك البيئة المخصّصة لدورة الحلقة الحالية. هكذا صارت تعمل كما يجب. أعدنا كتابة الشيفرة هنا وعوّضنا while بحلقة for. يمكننا أيضًا تنفيذ حيلة أخرى. لنراها لنفهم الموضوع أكثر: function makeArmy() { let shooters = []; let i = 0; while (i < 10) { let j = i; // (*) let shooter = function() { // دالة مُطلق النار alert( j ); // (*) المفترض أن ترينا رقمها }; shooters.push(shooter); i++; } return shooters; } let army = makeArmy(); army[0](); // 0 army[5](); // 5 كما حلقة for، فحلقة while تصنع بيئة مُعجمية جديدة لكلّ دورة، وهكذا نتأكّد بأن تكون قيمة shooter صحيحة. باختصار ننسخ القيمة let j = i وهذا يصنع المتغير j المحلي داخل الحلقة وينسخ قيمة i إلى نفسه. تُنسخ الأنواع الأولية «حسب قيمتها» By value، لذا بهذا نأخذ نسخة كاملة مستقلة تمامًا عن i، ولكنّها مرتبطة بالدورة الحالية في الحلقة. ترجمة -وبتصرف- للفصل Closure من كتاب The JavaScript language
-
تتوقّع العديد من دوال جافاسكربت المضمّنة في اللغة عددًا من الوُسطاء لا ينتهي. مثال: Math.max(arg1, arg2, ..., argN) -- يُعيد أكبر وسيط من الوُسطاء. Object.assign(dest, src1, ..., srcN) -- ينسخ الخصائص من src1..N إلى dest. …وهكذا. سنتعلّم في هذا الفصل كيف نفعل ذلك أيضًا. كما وكيف نمرّر المصفوفات إلى هذه الدوال على أنّها مُعاملات. المعاملات «البقية» ... يمكن أن ننادي الدالة بأيّ عدد من الوُسطاء كيفما كانت معرّفة الدالة. هكذا: function sum(a, b) { return a + b; } alert( sum(1, 2, 3, 4, 5) ); لن ترى أيّ خطأ بسبب تلك الوُسطاء «الزائدة». ولكن طبعًا فالنتيجة لن تأخذ بالحسبان إلا أوّل اثنين. يمكن تضمين بقية المُعاملات في تعريف الدالة باستعمال الثلاث نقاط ... ثمّ اسم المصفوفة التي ستحتويهم. تعني تلك النقط حرفيًا «اجمع المُعاملات الباقية في مصفوفة». فمثلًا لجمع كلّ الوُسطاء في المصفوفة args: function sumAll(...args) { // اسم المصفوفة هو args let sum = 0; for (let arg of args) sum += arg; return sum; } alert( sumAll(1) ); // 1 alert( sumAll(1, 2) ); // 3 alert( sumAll(1, 2, 3) ); // 6 يمكن لو أردنا أن نأخذ المُعاملات الأولى في متغيّرات ونجمع البقية فقط. هنا نأخذ الوسيطين الأوليين في متغيرات والباقي نرميه في المصفوفة titles: function showName(firstName, lastName, ...titles) { alert( firstName + ' ' + lastName ); // Julius Caesar // الباقي نضعه في مصفوفة الأسماء titles // مثلًا titles = ["Consul", "Imperator"] alert( titles[0] ); // Consul alert( titles[1] ); // Imperator alert( titles.length ); // 2 } showName("Julius", "Caesar", "Consul", "Imperator"); يجب أن تُترك المُعاملات البقية إلى النهاية تجمع المُعاملات البقية كلّ الوُسطاء التي بقيت. وبهذا فالآتي ليس منطقيًا وسيتسبّب بخطأ: function f(arg1, ...rest, arg2) { // الوسيط arg2 بعد ...البقية؟! // خطأ } يجب أن يكون ...rest الأخير دومًا. متغير الوسطاء arguments هناك كائن آخر شبيه بالمصفوفات يُدعى arguments ويحتوي على كلّ الوُسطاء حسب ترتيب فهارسها. مثال: function showName() { alert( arguments.length ); alert( arguments[0] ); alert( arguments[1] ); // المصفوفة مُتعدَّدة // for(let arg of arguments) alert(arg); } // تعرض: 2, Julius, Caesar showName("Julius", "Caesar"); // تعرض: 1, Ilya, undefined (ما من مُعطى ثانٍ) showName("Ilya"); قديمًا لم تكن المُعاملات البقية موجودة في اللغة ولم يكن لدينا سوى استعمال arguments لنجلب كلّ مُعاملات الدالة. وما زالت تعمل الطريقة إلى يومنا هذا ويمكن أن تراها في الشيفرات القديمة. ولكن السلبية هنا هي أنّ arguments ليست مصفوفة (على الرغم من أنّها شبيهة بالمصفوفات ومُتعدّدة). بهذا لا تدعم توابِع المصفوفات فلا ينفع أن نستدعي عليها arguments.map(...) مثلًا. كما وأنّها تحتوي على كل الوُسطاء دومًا. لا يمكن أن نأخذ منها ما نريد كما نفعل مع المُعاملات البقية. لهذا متى ما احتجنا إلى ميزة كهذه، فالأفضل استعمال المُعاملات البقية بدلًا من arguments. ليس للدوال السهمية "arguments" لو حاولت الوصول إلى كائن الوُسطاء arguments من داخل الدالة السهمية، فستستلم الناتج من الدالة «الطبيعية» الخارجية. إليك مثالًا: function f() { let showArg = () => alert(arguments[0]); showArg(); } f(1); // 1 كما نذكر فليس للدوال السهمية قيمة this تخصّها، أمّا الآن صرنا نعلم بأنّ ليس لها كائن arguments أيضًا. مُعامل التوزيع رأينا كيف نأخذ مصفوفة من قائمة من المُعطيات. ولكن ماذا لو أردنا العكس من ذلك؟ فمثلًا لنقل أردنا استعمال الدالة المبنية في اللغة Math.max والتي تُعيد أكبر عدد من القائمة: alert( Math.max(3, 5, 1) ); // 5 لنقل أنّ لدينا المصفوفة [3, 5, 1]. كيف نستدعي Math.max عليها؟ لا ينفع تمريرها «كما هي» لأنّ Math.max يتوقّع قائمةً بالوُسطاء العددية لا مصفوفة واحدة: let arr = [3, 5, 1]; alert( Math.max(arr) ); // NaN وطبعًا لا يمكن أن نفكّ عناصر القائمة يدويًا في الشيفرة Math.max(arr[0], arr[1], arr[2]) لأنّنا في حالات لا نعرف كم من عنصر هناك أصلًا. وما إن يتنفّذ السكربت يمكن أن يكون فيه أكبر مما كتبناه أو حتّى لا شيء أصلًا، وسنحصد لاحقًا ما جنته هذه الشيفرة. عاش مُنقذنا مُعامل التوزيع! عاش عاش عاش! من بعيد نراه مشابهًا تمامًا للمُعاملات البقية، كما ويستعمل ...، إلّا أنّ وظيفته هي العكس تمامًا. فحين نستعمل ...arr في استدعاء الدالة، «يتوسّع» الكائن المُتعدَّد ...arr إلى قائمة من الوُسطاء. فمثلًا نعود إلى Math.max: let arr = [3, 5, 1]; // (يحوّل التوزيع المصفوفة إلى قائمة من الوُسطاء) alert( Math.max(...arr) ); // 5 يمكن أيضًا أن نمرّر أكثر من مُتعدَّد واحد بهذه الطريقة: let arr1 = [1, -2, 3, 4]; let arr2 = [8, 3, -8, 1]; alert( Math.max(...arr1, ...arr2) ); // 8 أو حتّى ندمج مُعامل التوزيع مع القيم العادية: let arr1 = [1, -2, 3, 4]; let arr2 = [8, 3, -8, 1]; alert( Math.max(1, ...arr1, 2, ...arr2, 25) ); // 25 كما يمكن أن نستعمل مُعامل التوزيعة لدمج المصفوفات: let arr = [3, 5, 1]; let arr2 = [8, 9, 15]; let merged = [0, ...arr, 2, ...arr2]; alert(merged); // 0,3,5,1,2,8,9,15 (0 ثمّ arr ثمّ 2 ثمّ arr2) استعملنا في الأمثلة أعلاه مصفوفة لنشرح مُعامل التوزيع، إلّا أنّ المُتعدَّدات أيًا كانت تنفع أيضًا. فمثلًا نستعمل هنا مُعامل التوزيع لنحوّل السلسلة النصية إلى مصفوفة محارف: let str = "Hello"; alert( [...str] ); // H,e,l,l,o يستعمل مُعامل التوزيع هذا داخليًا المُعدِّدات لجمع العناصر، كما تفعل حلقة for..of. لذا لو استلمت for..of سلسلةً نصيّة فتُعيد لنا المحارف وتصير ...str بالقيمة "H","e","l","l","o". وهكذا تُمرّر قائمة المحارف إلى مُهيّئ المصفوفة [...str]. يمكننا أيضًا لهذه المهمة استعمال Array.from إذ أنّه يحوّل المُتعدَّد (مثل السلاسل النصية) إلى مصفوفة: let str = "Hello"; // يُحوّل Array.from المُتعدَّد إلى مصفوفة alert( Array.from(str) ); // H,e,l,l,o ناتجه هو ذات ناتج [...str]. ولكن… هناك فرق ضئيل بين Array.from(obj) و[...obj]: يعمل Array.from على الشبيهات بالمصفوفات والمُتعدَّدات. ويعمل مُعامل التوزيع على المُتعدَّدات فقط لا غير. لذا لو أردت تحويل شيء إلى مصفوفة فالتابِع Array.from أكثر استعمالًا وشيوعًا. ملخص متى رأينا "..." في الشيفرة نعرف أنّه إمّا المُعاملات البقية وأمّا مُعامل التوزيع. إليك طريقة بسيطة للتفريق بينهما: حين ترى ... موجودة في نهاية مُعاملات الدالة فهي «المُعاملات البقية» وستجمع بقية قائمة الوُسطاء في مصفوفة. وحين ترى ... في استدعاء دالة أو ما شابهه فهو «مُعامل توزيع» يوسّع المصفوفة إلى قائمة. طُرق الاستعمال: تُستعمل المُعاملات البقية لإنشاء دوال تقبل أيّ عدد كان من الوُسطاء. يُستعمل مُعامل التوزيع لتمرير مصفوفة إلى دوال تطلب (عادةً) قائمة طويلة من الوُسطاء. كلا الميزتين تساعدك في التنقل بين القائمة ومصفوفة المُعاملات بسهولة ويُسر. يمكنك أيضًا أن ترى كل وُسطاء استدعاء الدالة «بالطريقة القديمة» arguments وهو كائن مُتعدَّد شبيه بالمصفوفات. ترجمة -وبتصرف- للفصل Rest parameters and spread syntax من كتاب The JavaScript language
-
فلنعد الآن إلى الدوال ونرى أمرها بتمعّن وتعمّق أكثر. سنتكلم أولًا عن التعاود (Rescursion). لو كنت ذا علم بالبرمجة فالأغلب أنّك تعرف ما هذا التعاود ويمكنك تخطّي هذا الفصل. يُعدّ التعاود (Rescursion) نمطًا برمجيًا نستعمله حين يمكن تقسيم المهمة الكبيرة جدًا إلى مهام أبسط منها متشابهة، أو حين يمكن تبسيط المهمة الواحدة إلى عملية بعضها بسيط وآخر يتشابه بين بعضه، أو نستعمله (كما سنرى قريبًا) للتعامل مع أنواعٍ محدّدة من بنى البيانات. يمكن للدالة حين تحاول إجراء مهمّة ما نداءَ دوال أخرى. أحيانًا يمكن أن تنادي تلك الدالة نفسها ثانيةً. هذا ما ندعوه بالتعاود. نهجان في التطوير لنبدأ بما هو أبسط. لنكتب دالة pow(x, n) ترفع x إلى الأسّ الطبيعي n. بعبارة أخرى، تضرب x بنفسه n مرّة. pow(2, 2) = 4 pow(2, 3) = 8 pow(2, 4) = 16 يمكننا تنفيذ هذه الدالة بطريقتين اثنتين. التفكير بالتكرار: حلقة for: function pow(x, n) { let result = 1; // نضرب الناتج في x - n مرّة داخل الحلقة for (let i = 0; i < n; i++) { result *= x; } return result; } alert( pow(2, 3) ); // 8 التفكير بالتعاود: تبسيط المهمة ونداء «الذات»: function pow(x, n) { if (n == 1) { return x; } else { return x * pow(x, n - 1); } } alert( pow(2, 3) ); // 8 لاحظ كيف أنّ تلك الشيفرة التعاودية مختلفة جذريًا عن سابقتها. حين تُستدعى pow(x, n) تنقسم عملية التنفيذ إلى فرعين: if n==1 = x / pow(x, n) = \ else = x * pow(x, n - 1) حين n == 1، نرى كل شيء كالعادة. نسمّي تلك الحالة بأساس التعاود، لأنها تُعطينا الناتج البديهي مباشرة: pow(x, 1) تساوي x. عدى تلك فيمكننا تمثيل pow(x, n) على أنها x * pow(x, n - 1). يمكنك رياضيًا كتابة x<sup>n</sup> = x * x<sup>n-1</sup>. نسمّي هذه خطوة تعاودية: فنعدّل مهمة الأس الكبيرة لتصير عملية أبسط (الضرب في x) ونستعمل استدعاءً أبسط لمهمة الأس (pow ولكن n أقل). في الخطوات اللاحقة تصير أبسط وأبسط إلى أن تصل n إلى 1. يمكن أيضًا أن نقول بأن pow تستدعي نفسها تعاوديًا حتى تكون n == 1. فمثلًا كي نحسب قيمة pow(2, 4) على التعاود إجراء هذه المهام: 1. pow(2, 4) = 2 * pow(2, 3) 2. pow(2, 3) = 2 * pow(2, 2) 3. pow(2, 2) = 2 * pow(2, 1) 4. pow(2, 1) = 2 للتلخيص، يبسّط التعاود استدعاء الدالة إلى استدعاءً آخر أبسط، وبعدها أبسط، وأبسط، وأبسط، حتّى يظهر الناتج ويصير معلومًا. غالبًا ما تكون شيفرة التعاود أقصر عادةً ما يكون الحل باستعمال التعاود أقصر من التكرار بالحلقات. يمكننا هنا مثلًا إعادة كتابة نفس الشيفرة ولكن باستعمال المُعامل الشرطي ? بدل if لتصير pow(x, n) أقصر أكثر وتبقى مقروءةً لنا: function pow(x, n) { return (n == 1) ? x : (x * pow(x, n - 1)); } يُسمّى أقصى عدد من الاستدعاءات المتداخلة (بما في ذلك أول استدعاء) بعمق التعاود (Rescursion Depth). في حالتنا هنا سيكون هذا العمق n. يحدّ محرّك جافاسكربت من أقصى عمق تعاودي ممكن. يمكن أن نقول بأنّ 10000 هو الحدّ الذي يمكننا الاعتماد عليه (ولو أنّ بعض المحرّكات ترفع هذا الحدّ أكثر). أجل، هناك تحسينات تلقائية تحاول رفع هذا الحدّ («تحسينات نهاية الاستدعاء») ولكنّها ليست مدعومة في كلّ مكان ولا تعمل إلّا على الحالات البسيطة. يقصّر هذا من تطبيقات استعمال التعاود، ولكنّه مع ذلك مستعمل بشدة، إذ هناك مهام كثيرة لو استعملت عليها التعاود لأعطتك شيفرة أقصر وأسهل للصيانة. سياق التنفيذ والمكدس لنرى الآن كيف يعمل التعاود أصلًا، ولذلك لا بدّ من أن نرى ما خلف كواليس الدوال هذه. تُخزّن المعلومات حول عملية تنفيذ الدالة (حين تعمل) في سياقها التنفيذي (execution context). يُعدّ سياق التنفيذ بنيةَ بيانات داخلية تحوي التفاصيل التي تخصّ عملية تنفيذ الدالة: إلى أين وصلت الآن؟ ما المتغيرات الحالية؟ ما قيمة this (لا نستعملها هنا) وتفاصيل أخرى داخلية. لكلّ استدعاء دالة سياق تنفيذي واحد مرتبط بها. حين تستدعي الدالة دوال أخرى متداخلة، يحدث: تتوقف الدالة الحالية مؤقتًا. يُحفظ سياق التنفيذ المرتبط بها في بنية بيانات خاصّة تسمى مكدس سياق التنفيذ execution context stack. يتنفّذ الاستدعاء المتداخل. ما إن ينتهي، يجلب المحرّك التنفيذ القديم ذاك من المكدس، وتواصل الدالة الخارجية عملها حيث توقفت. لنرى ما يحدث أثناء استدعاء pow(2, 3). pow(2, 3) يخزّن سياق التنفيذ (في بداية استدعاء pow(2, 3)) المتغيرات هذه: x = 2, n = 3 وأنّ سير التنفيذ هو في السطر رقم 1 من الدالة. يمكن أن نرسمه هكذا: السياق: { x: 2, n: 3, عند السطر 1 } pow(2, 3) .function-execution-context { border: 1px solid black; font-family: 'DejaVu Sans Mono', 'Lucida Console', 'Menlo', 'Monaco', monospace; padding: 4px 6px; margin: 0 4px; } .function-execution-context-call { color: gray; } هذا ما يجري حين يبدأ تنفيذ الدالة. بعد أن يصير الشرط n == 1 خطأً، ينتقل سير التنفيذ إلى الفرع الثاني من الإفادة if: function pow(x, n) { if (n == 1) { return x; } else { return x * pow(x, n - 1); } } alert( pow(2, 3) ); ما زالت المتغيرات كما هي، ولكن السطر تغيّر. بذلك يصير السياق الآن: السياق: { x: 2, n: 3, عند السطر 5 } pow(2, 3) علينا لحساب x * pow(x, n - 1) استدعاء pow فرعيًا بالوُسطاء الجديدة pow(2, 2). pow(2, 2) كي يحدث الاستدعاء المتداخل، يتذكّر محرّك جافاسكربت سياق التنفيذ الحالي داخل مكدس سياق التنفيذ. هنا نستدعي ذات الدالة pow، ولكن ذلك لا يهم إذ أنّ العملية هي ذاتها لكلّ الدوال: «يتذكّر المحرّك» السياقَ الحالي أعلى المكدس. يَصنع سياقًا جديدًا للاستدعاء الفرعي. متى انتهى الاستدعاء الفرعي يُطرح (pop) السياق السابق من المكدس ويتواصل التنفيذ. هذا مكدس السياق حين ندخل الاستدعاء الفرعي pow(2, 2): السياق: { x: 2, n: 2, عند السطر 1 } pow(2, 2) السياق: { x: 2, n: 3, عند السطر 5 } pow(2, 3) سياق التنفيذ الحالي والجديد هو الأعلى (بالخط الثخين) وأسفله السياقات التي تذكّرها المحرّك سابقًا. حين ننتهي من الاستدعاء الفرعي يمكننا بسهولة بالغة مواصلة السياق السابق، إذ أنّ متغيراته ومكان توقف الشيفرة محفوظان بالضبط في السياق. ملاحظة: نرى في الصورة أنّا استعملنا كلمة «سطر» إذ ليس في المثال إلّا استدعاءً فرعيًا واحدًا في السطر، ولكن يمكن أن تحتوي الشيفرات ذات السطر الواحد (بصفة عامة) على أكثر من استدعاءً فرعيًا، هكذا: pow(…) + pow(…) + somethingElse(…). لذا سنكون أدقّ لو قلنا بأن عملية التنفيذ تتواصل «بعد الاستدعاء الفرعي مباشرةً». pow(2, 1) تتكرّر العملية: يُصنع استدعاء فرعي جديد في السطر 5 بالوسطاء الجديدة x=2 و n=1. صنعنا سياقًا جديدًا، إذًا ندفع (push) الأخير أعلى المكدس: السياق: { x: 2, n: 1, عند السطر 1 } pow(2, 1) السياق: { x: 2, n: 2, عند السطر 5 } pow(2, 2) السياق: { x: 2, n: 3, عند السطر 5 } pow(2, 3) الآن هناك سياقين اثنين قديمين، وواحد يعمل للاستدعاء pow(2, 1). المخرج نرى الشرط n == 1 صحيحًا أثناء تنفيذ الاستدعاء pow(2, 1) (عكس ما سبقه من مرّات)، إذًا فالفرع الأول من إفادة if سيعمل هنا: function pow(x, n) { if (n == 1) { return x; } else { return x * pow(x, n - 1); } } لا استدعاءات متداخلة من هنا، بذلك تنتهي الدالة وتُعيد 2. وحين تنتهي الدالة لا يكون هناك حاجة لسياق التنفيذ فيُزال من الذاكرة، وبعدها يرجع السياق السابق أعلى المكدس: السياق: { x: 2, n: 2, عند السطر 5 } pow(2, 2) السياق: { x: 2, n: 3, عند السطر 5 } pow(2, 3) يتواصل تنفيذ الاستدعاء pow(2, 2)، وفيه ناتج الاستدعاء الفرعي pow(2, 1) لذا يُنهي أيضًا تنفيذ x * pow(x, n - 1) فيُعيد 4. بعدها يستعيد المحرّك السياقَ السابق: السياق: { x: 2, n: 3, عند السطر 5 } pow(2, 3) وحين ينتهي، يكون عندنا ناتج pow(2, 3) = 8. عمق التعاود في هذه الحالة هو: 3. كما نرى في الصور أعلاه فعمق التعاود يساوي أقصى عدد من السياقات في المكدس. لكن اعلم بأنّ السياقات تطلب الذاكرة. في حالتنا هنا نرفع العدد للأسّ n، وبذلك نحتاج ما يكفي من ذاكرة تسع لتخزين n سياق لكل القيم الأصغر من n. خوارزميات الحلقات والتكرار أفضل من حيث استعمال الذاكرة: function pow(x, n) { let result = 1; for (let i = 0; i < n; i++) { result *= x; } return result; } تستعمل دالة pow المتكرّرة سياقًا واحدًا تغيّر فيه المتغيران i وresult أثناء عملها، كما وأنّ احتياجاتها للذاكرة قليلة وثابتة ولا تعتمد على قيمة n. يمكن كتابة التعاودات أيًا كانت بصيغة الحلقات، وغالبًا ما تكون تلك الحلقات أفضل أداءً. …ولكن أحيانًا ما تكون إعادة الكتابة تلك تافهة وبلا قيمة حقيقية خصوصًا حين تستعمل الدالة استدعاءاتَ دوال تعاودية تختلف حسب شروط معيّنة، أو أن تدمج الدالة نتائج الاستدعاءات أو حين يصير تفريع الدالة أصعب أكثر فأكثر. حينها سيكون ذلك التحسين من المحرّك بلا داعٍ ولا حاجة لبذل كل ذلك المجهود له. هكذا يعطينا التعاود شيفرة أقصر سهلة الفهم ويمكننا دعمها بلا عناء. لا نحتاج إلى هذا «التحسين» في كلّ مكان؛ ما نريد هو فقط شيفرة جيدة، ولهذا نستعمل التعاود. مسح الأشجار تعاوديًا مسح الأشجار تعاوديًا Recursive Traversal هو تطبيق آخر عن روعة التعاود. لنقل بأنّ لدينا شركة ويمكن أن نمثّل بنية موظّفيها في هذا الكائن: let company = { sales: [{ name: 'John', salary: 1000 }, { name: 'Alice', salary: 600 }], development: { sites: [{ name: 'Peter', salary: 2000 }, { name: 'Alex', salary: 1800 }], internals: [{ name: 'Jack', salary: 1300 }] } }; أي أنّ في الشركة أقسام عدّة. يمكن أن يحتوي كل قسم على مصفوفة من العاملين. فمثلًا لقسم المبيعات sales عاملين اثنين: John وAlice. أو أن ينقسم القسم إلى أقسام فرعية، مثل قسم التطوير development له فرعان: تطوير المواقع sites والبرمجيات الداخلية internals. ولكلّ من الفرعين موظفين منفصلين. يمكن أيضًا أن يكبُر القسم الفرعي ويصير فروع من القسم الفرعي (أي «فِرَق»). مثلًا قسم المبيعات sites سيتطوّر ويتحسّن وينقسم مستقبلًا إلى فرعين siteA و siteB. وبعدها ربما (لو عمل فريق التسويق بجدّ) ينقسم أكثر أيضًا. طبعًا هذا تخيّل فقط وليس في الصورة تلك. الآن، ماذا لو أردنا دالة تعطينا مجموع كل الرواتب؟ كيف السبيل؟ لو جرّبنا بالتكرار فسيكون أمرًا عسيرًا إذ أنّ البنية ليست ببسيطة. أول فكرة على البال هي حلقة for تمرّ على الشركة company وداخلها حلقات فرعية على الأقسام بالمستوى الأول. ولكن هكذا سنحتاج حلقات فرعية متداخلة أيضًا لتمرّ على الموظفين في الأقسام بالمستوى الثاني مثل قسم sites… وبعدها حلقات أخرى داخل تلك فوقها للأقسام بالمستوى الثالث إن عمل فريق التسويق كما يجب… لو وضعنا 3-4 من هذه الحلقات الفرعية المتداخلة في شيفرة لتعمل جولة مسح على كائن واحد، فستنتج لنا شيفرة قبيحة حقًا. لنجرّب التعاود الآن. كما رأينا، حين تُلاقي الدالة قسمًا عليها جمع رواتبه، تواجه حالتين اثنتين: إمّا يكون قسمًا «بسيطًا» فيه مصفوفة من الناس، وهكذا تجمع رواتبهم في حلقة بسيطة. أو تجد كائنًا فيه N من الأقسام الفرعية، حينها تصنع N من الاستدعاءات المتعاودة لتحصي مجموع كلّ قسم فرعي وتدمج النتائج كلها. الحالة الأولى هي أساس التعاود، أي عملنا العادي حين نستلم مصفوفة. الحالة الثانية (حين نرى كائنًا) هي خطوة في التعاود. يمكن تقسيم تلك المهمة المعقّدة إلى مهام فرعية لكلّ قسم. ربّما تنقسم تلك المهام الفرعية ثانيةً، ولكنّها عاجلًا أم آجلًا ستنتهي بالحالة (1) لا محالة. ربّما… يكون أسهل لو قرأت الخوارزمية من الشيفرة ذاتها: // الكائن كما هو، ضغطناه لألا نُطيل القصة فقط let company = { sales: [{name: 'John', salary: 1000}, {name: 'Alice', salary: 600 }], development: { sites: [{name: 'Peter', salary: 2000}, {name: 'Alex', salary: 1800 }], internals: [{name: 'Jack', salary: 1300}] } }; // الدالة التي ستنفّذ هذا العمل function sumSalaries(department) { if (Array.isArray(department)) { // حالة (1) return department.reduce((prev, current) => prev + current.salary, 0); // نجمع عناصر المصفوفة } else { // حالة (2) let sum = 0; for (let subdep of Object.values(department)) { // نستدعي الأقسام الفرعية تعاوديًا، ونجمع النتائج sum += sumSalaries(subdep); } return sum; } } alert(sumSalaries(company)); // 6700 الشيفرة قصيرة وسهل فهمها (كما هو أملي). هنا تظهر قوّة التعاود، فسيعمل على أيّ مستوى من الأقسام الفرعية المتداخلة. إليك رسمة توضّح الاستدعاءات: الفكرة بسيطة للغاية: لو كان كائنًا {...}، نستعمل الاستدعاءات الفرعية، ولو كانت مصفوفات [...] هي آخر «أوراق» شجرة التعاود، فتعطينا الناتج مباشرةً. لاحظ كيف أنّ الشيفرة تستعمل مزايا ذكيّة ناقشناها سابقًا: التابِع arr.reduce في الفصل «توابِع المصفوفات» لنجمع الرواتب. الحلقة for(val of Object.values(obj)) للمرور على قيم الكائن إذ يُعيد التابِع Object.values مصفوفة بالقيم. بنى التعاود بنية البيانات التعاودية (أيّ التي يحدّد أساسها التعاود) هي بنية تكرّر نفسها على أجزاء منفصلة. رأينا لتوّنا مثالًا عن هذه البنية: بنية الشركة أعلاه. القسم في الشركة هو إمّا: مصفوفة من الناس. أو كائنًا فيه أقسام أخرى. لو كنت مطوّر وِب فالأمثلة التي تعرفها وتُدركها هي مستندات HTML وXML. ففي مستندات HTML، يمكن أن يحتوي وسم HTML على قائمة من: أجزاء من نصوص. تعليقات HTML. وسوم HTML أخرى (أي ما يمكن أن يحتوي على أجزاء من نصوص أو تعليقات أو وسوم أخرى وهكذا). وهذا ما نسمّيه بالبنى التعاوديّة. لنفهم التعاود أكثر سنشرح بنية تعاود أخرى تسمّى «القوائم المترابطة» (Linked List). يمكن أن تكون هذه القوائم أحيانًا بديلًا أفضل موازنةً بالمصفوفات. القوائم المترابطة لنقل بأنّا نريد تخزين قائمة كائنات مرتّبة. ستصنع مصفوفة كالعادة: let arr = [obj1, obj2, obj3]; ولكن… هناك مشكلة تخصّ المصفوفات. عمليات «حذف العنصر» و«إدراج العنصر» مُكلفة. فمثلًا على عملية arr.unshift(obj) إعادة ترقيم كلّ العناصر للكائن الجديد obj، ولو كانت المصفوفة كبيرة فستأخذ العملية وقتًا طويلًا. الأمر نفسه ينطبق لعملية arr.shift(). التعديلات على بنية البيانات (التي لا تحتاج إلى إعادة الترقيم بالجملة) هي تلك التي تؤثّر على نهاية المصفوفة: arr.push/pop. لذا يمكن أن تكون المصفوفة بطيئة حقًا لو كانت الطوابير طويلة حين نعمل مع المصفوفات من عناصرها الأولى. يمكننا عوض ذلك استعمال بنية بيانات أخرى لو أردنا إدخال البيانات وحذفها سريعًا. تُدعى هذه البنية بالقائمة المترابطة. يُعرّف عنصر القائمة المترابطة تعاوديًا على أنّه كائن فيه: قيمة value. خاصية «التالي» next تُشير إلى عنصر القائمة المترابطة التالي أو إلى null لو كانت هذه نهاية القائمة. مثال: let list = { value: 1, next: { value: 2, next: { value: 3, next: { value: 4, next: null } } } }; إليك التمثيل البصري لهذه القائمة: هذه شيفرة أخرى لنصنع القائمة: let list = { value: 1 }; list.next = { value: 2 }; list.next.next = { value: 3 }; list.next.next.next = { value: 4 }; list.next.next.next.next = null; هنا نرى بوضوح أكثر كيف أنّ هناك كائنات متعدّدة لكلّ منها خاصية value وأخرى next تُشير إلى العنصر بقرب «هذا». متغيّر list هو أول الكائنات في السلسلة وبهذا لو اتّبعنا إشارات next بدءًا منها سنصل إلى أيّ عنصر آخر نريد. يمكننا قسمة القائمة إلى أجزاء عدّة ودمجها لاحقًا لو أردنا: let secondList = list.next.next; list.next.next = null; للدمج: list.next.next = secondList; وطبعًا يمكننا إدخال العناصر إلى أي مكان وإزالتها من أي مكان. فمثلًا لو أردنا إضافة قيمة جديدة للبداية فعلينا تحديث رأس القائمة: let list = { value: 1 }; list.next = { value: 2 }; list.next.next = { value: 3 }; list.next.next.next = { value: 4 }; // نُضيف قيمة جديدة إلى بداية القائمة list = { value: "new item", next: list }; ولنُزيل قيمة من المنتصف نعدّل خاصية next للكائن الذي يسبق الجديد: list.next = list.next.next; هكذا أجبرنا list.next بأن «تقفز» فوق 1 لتصل 2، بهذا استثنينا القيمة 1 من السلسلة. وطالما أنّها ليست مخزّنة في أيّ مكان آخر فستُزال من الذاكرة تلقائيًا. وعلى عكس المصفوفات فلسنا هنا نُعيد الترقيم بالجملة، أي أنّ إعادة ترتيب العناصر أسهل. بالطبع فالقوائم ليست أفضل من المصفوفات دومًا وإلا فاستعملناها هي دومًا وما احتجنا المصفوفات أبدًا. السلبية الأساس هي أنّ الوصول إلى العنصر حسب رقمه ليس سهلًا كما في المصفوفات حيث نستعمل الإشارة المباشرة arr[n]. ولكن في القوائم علينا البدء من العنصر الأول والانتقال N مرّة عبر next لنصل إلى العنصر بالرقم N. …ولكننا لا نحتاج دومًا إلى هذه العمليات فمثلًا حين نريد طابورًا أو حتّى طابورًا متعدّد الطرفين فيجب أن نستعمل بنية مرتّبة تتيح بإضافة/إزالة العناصر من الجهتين بسرعة فائقة، وليس بالضروري أن نعرف ما في وسطها. يمكننا تحسين القوائم هكذا: إضافة الخاصية prev مع الخاصية next للإشارة إلى العنصر السابق، لننتقل وراءً بسهولة أكبر. إضافة متغيّر بالاسم tail يُشير إلى آخر عنصر من القائمة (وتحديثه متى أضفنا/أزلنا عناصر من النهاية). …يمكن أن تتغيّر بنية البيانات حسب متطلباتنا واحتياجاتنا. ملخص المصطلحات: التعاود* هو مصطلح برمجي يعني استدعاء دالة من داخلها. يمكن استعمال الدوال التعاودية لحلّ المهام المختلفة بطرق ذكية نظيفة. حين تستدعي الدالة نفسها نسمّي ذلك خطوة تعاود. تُعدّ وُسطاء الدالة التي تبسّط المهمّة إلى أقصى درجة بحيث لا تستدعي الدالة أيّ شيء بعدها - تُعدّ أساس التعاود. بنية البيانات التعاودية هي أيّة بنية بيانات تُحدّد نفسها بنفسها. فمثلًا يمكن تعريف القائمة المترابطة على أنّها بنية بيانات تحتوي على كائن يُشير إلى قائمة (أو يُشير إلى null). list = { value, next -> list } تُعدّ الأشجار مثل شجرة عناصر HTML أو شجرة الأقسام في هذا الفصل كائنات تعاودية بطبعها، فهي تتفرّع ولكلّ فرع فروع أخرى. يمكن استعمال الدوال التعاودية للمرور فيها كما رأينا في مثال sumSalary. يمكن إعادة كتابة أيّ دالة تعاودية لتصير دالة تستعمل التكرار، وغالبًا ما نفعل هذا لتحسين أداء الدوال. ولكن هناك مهام عديدة يكون الحلّ التعاودي سريعًا كفايةً وأسهل كتابةً ودعمًا. تمارين اجمع كلّ الأعداد إلى أن تصل للممرّر الأهمية: 5 اكتب الدالة sumTo(n) التي تجمع الأعداد 1 + 2 + ... + n. مثال: sumTo(1) = 1 sumTo(2) = 2 + 1 = 3 sumTo(3) = 3 + 2 + 1 = 6 sumTo(4) = 4 + 3 + 2 + 1 = 10 ... sumTo(100) = 100 + 99 + ... + 2 + 1 = 5050 اكتب 3 شيفرات: واحدة باستعمال حلقة for. واحدة باستعمال التعاود، إذ أنّ sumTo(n) = n + sumTo(n-1) طالما n > 1. واحدة باستعمال المتتاليات الحسابية. مثال عن الناتج: function sumTo(n) { /*... شيفرتك هنا ... */ } alert( sumTo(100) ); // 5050 ملاحظة: أيّ الشيفرات أسرع من الأخرى؟ وأيها أبطأ؟ ولماذا؟ ملاحظة أخرى: هل يمكن أن نستعمل التعاود لحساب sumTo(100000)؟ الحل الحلّ باستعمال الحلقة: function sumTo(n) { let sum = 0; for (let i = 1; i <= n; i++) { sum += i; } return sum; } alert( sumTo(100) ); الحلّ باستعمال التعاود: function sumTo(n) { if (n == 1) return 1; return n + sumTo(n - 1); } alert( sumTo(100) ); الحلّ باستعمال المعادلة sumTo(n) = n*(n+1)/2: function sumTo(n) { return n * (n + 1) / 2; } alert( sumTo(100) ); عن الملاحظة: بالطبع فالمعادلة هي أسرع الحلول فلا تستعمل إلا ثلاث عمليات لكلّ عدد n. الرياضيات إلى جانبنا هنا! الشيفرة باستعمال الحلقة تأتي في المرتبة الثانية من ناحية السرعة. لو تلاحظ فنحن هنا نجمع الأعداد نفسها في الشيفرتين التعاودية والحلقية، إلّا أنّ التعاودية فيها استدعاءات متداخلة أكثر وإدارةً لمكدس التنفيذ، هذا ما يستهلك موارد أكثر فتصير الشيفرة أبطأ. عن الملاحظة الأخرى: تدعم بعض المحرّكات «تحسين نهاية الاستدعاء» (tail call optimization)، ويعني أنّه لو كان الاستدعاء التعاودي هو آخر ما في الدالة (مثلما في الدالة sumTo أعلاه)، فلن تواصل الدالة الخارجية عملية التنفيذ كي لا يتذكّر المحرّك سياقها التنفيذي. يتيح هذا للمحرّك إزالة قضية الذاكرة بذلك يكون ممكنًا عدّ sumTo(100000). ولكن، لو لم يدعم محرّك جافاسكربت هذا النوع من التحسين (وأغلبها لا تدعم) فستواجه الخطأ: تخطّيت أكبر حجم في المكدس، إذ يُفرض -عادةً- حدّ على إجمالي حجم المكدس. احسب المضروب الأهمية: 4 المضروب هو عدد طبيعي مضروب بِ «العدد ناقصًا واحد» وثمّ بِ «العدد ناقصًا اثنين» وهكذا إلى أن نصل إلى 1. نكتب مضروب n بهذا الشكل: n! يمكننا كتابة تعريف المضروب هكذا: n! = n * (n - 1) * (n - 2) * ...*1 قيم المضاريب لأكثر من n: 1! = 1 2! = 2 * 1 = 2 3! = 3 * 2 * 1 = 6 4! = 4 * 3 * 2 * 1 = 24 5! = 5 * 4 * 3 * 2 * 1 = 120 مهمّتك هي كتابة الدالة factorial(n) لتحسب n! باستعمال الاستدعاءات التعاودية. alert( factorial(5) ); // 120 ملاحظة وفائدة: يمكنك كتابة n! هكذا n * (n-1)! مثلًا: 3! = 3*2! = 3*2*1! = 6 الحل حسب التعريف فيمكن كتابة المضروب n! هكذا n * (n-1)!. أي أنّه يمكننا حساب ناتج factorial(n) على أنّه n مضروبًا بناتج factorial(n-1). ويمكن أن ينخفض استدعاء n-1 أنزل وأنزل إلى أن يصل 1. function factorial(n) { return (n != 1) ? n * factorial(n - 1) : 1; } alert( factorial(5) ); // 120 القيمة الأساس للتعاود هي 1. يمكننا أن نجعل 0 هي الأساس ولكنّ ذلك لا يهم، ليست إلا خطوة تعاود أخرى: function factorial(n) { return n ? n * factorial(n - 1) : 1; } alert( factorial(5) ); // 120 أعداد فيبوناتشي الأهمية: 5 لمتتالية فيبوناتشي الصيغة F<sub>n</sub> = F<sub>n-1</sub> + F<sub>n-2</sub>. أي أنّ العدد التالي هو مجموع العددين الذين سبقاه. أوّل عددين هما 1، وبعدها 2(1+1) ثمّ 3(1+2) ثمّ 5(2+3) وهكذا: 1, 1, 2, 3, 5, 8, 13, 21.... ترتبط أعداد فيبوناتشي بالنسبة الذهبية وبظواهر طبيعية أخرى عديدة حولنا من كلّ مكان. اكتب الدالة fib(n) لتُعيد عدد فيبوناتش n-th. مثال لطريقة عملها: function fib(n) { /* شيفرتك هنا */ } alert(fib(3)); // 2 alert(fib(7)); // 13 alert(fib(77)); // 5527939700884757 ملاحظة: يجب أن تعمل الدالة بسرعة. يجب ألا يأخذ استعداء fib(77) أكثر من جزء من الثانية. الحل أوّل حلّ نفكّر به هو الحلّ بالتعاود. أعداد فيبوناتشي تعاودية حسب تعريفها: function fib(n) { return n <= 1 ? n : fib(n - 1) + fib(n - 2); } alert( fib(3) ); // 2 alert( fib(7) ); // 13 // fib(77); // سيكون استدعاءً أبطأ من السلحفاة …ولكن لو كانت قيمة n كبيرة فسيكون بطيئًا جدًا. يمكن أن يعلّق الاستدعاء fib(77) محرّك جافاسكربت لفترة من الوقت بينما يستهلك موارد المعالج كاملةً. يعزو ذلك إلى أنّ الدالة تؤدّي استدعاءات فرعية كثيرة، وتُعيد تقدير (evaluate) القيم ذاتها مرارًا وتكرارًا. لنرى مثلًا جزءًا من حسابات fib(5): ... fib(5) = fib(4) + fib(3) fib(4) = fib(3) + fib(2) ... يمكننا أن نرى هنا بأنّ قيمة fib(3) مفيدة للاستدعائين fib(5) وfib(4). لذا فستُستدعى fib(3) وتُقدّر قيمتها مرتين كاملتين منفصلتين عن بعضهما البعض. إليك شجرة التعاود كاملةً: نرى بوضوح كيف أنّ fib(3) تُقدّر مرتين اثنتين وfib(2) تُقدّر ثلاث مرات. إجمالي الحسابات يزداد أسرع مما تزداد قيمة n، ما يجعل الحسابات مهولة حين نصل n=77. يمكننا تحسين أداء الشيفرة بتذكّر القيم التي قدّرنا ناتجها قبل الآن: لو حسبنا قيمة fib(3) مثلًا، فيمكننا إعادة استعمالها في أيّ حسابات مستقبلية. أو، يمكن أن نترك التعاود كله ونحاول استعمال خوارزمية مختلفة جذريًا تعتمد على الحلقات. فبدلًا من أن نبدأ بِ n وننطلق نحو أسفل، يمكن أن نصنع حلقة تبدأ من 1 و2 ثمّ تسجّل ناتج ذلك على أنّه fib(3)، وناتج القيمتين السابقتين على أنّه fib(4) وهكذا دواليك إلى أن تصل إلى القيمة المطلوبة. هكذا لا نتذكّر في كلّ خطوة إلى قيمتين سابقتين فقط. إليك خطوات الخوارزمية الجديدة هذه بالتفصيل الممل. البداية: // a = fib(1)، b = fib(2)، هذه القيم حسب التعريف رقم 1 let a = 1, b = 1; // نأخذ c = fib(3) ليكون مجموعها let c = a + b; /* لدينا الآن fib(1) و fib(2) و fib(3) a b c 1, 1, 2 */ الآن نريد معرفة fib(4) = fib(2) + fib(3). لنحرّك ما في المتغيّرات إلى الجانب: a,b سيكونان fib(2),fib(3) وc سيكون مجموعهما: a = b; // now a = fib(2) b = c; // now b = fib(3) c = a + b; // c = fib(4) /* الآن لدينا المتتابعة: a b c 1, 1, 2, 3 */ الخطوة التالية تعطينا عددًا آخر في السلسلة: a = b; // الآن صار a = fib(3) b = c; // الآن صار b = fib(4) c = a + b; // c = fib(5) /* الآن لدينا المتتابعة (أضفنا عددًا آخر): a b c 1, 1, 2, 3, 5 */ …وهكذا إلى أن نصل إلى القيمة المطلوبة. وهذا أسرع بكثير من التعاود وليس فيه أيّة حسابات متكرّرة. الشيفرة كاملةً: function fib(n) { let a = 1; let b = 1; for (let i = 3; i <= n; i++) { let c = a + b; a = b; b = c; } return b; } alert( fib(3) ); // 2 alert( fib(7) ); // 13 alert( fib(77) ); // 5527939700884757 تبدأ الحلقة بالقيمة i=3 إذ أنّ قيمتا المتتابعة الأولى والثانية مكتوبتان داخل المتغيّران a=1 و b=1. يُدعى هذا الأسلوب بالبرمجة الديناميكية من أسفل إلى أعلى. طباعة قائمة مترابطة لنقل بأنّ أمامنا القائمة المترابطة هذه: let list = { value: 1, next: { value: 2, next: { value: 3, next: { value: 4, next: null } } } }; اكتب الدالة printList(list) لتطبع لنا عناصر القائمة واحدةً واحدة. اصنع نسختين من الحل: واحدة باستعمال الحلقات وواحدة باستعمال التعاود. أيّ الحلّين أفضل؟ بالتعاود أو بدون؟ الحل نسخة الحلقات هذا الحلّ باستعمال الحلقات: let list = { value: 1, next: { value: 2, next: { value: 3, next: { value: 4, next: null } } } }; function printList(list) { let tmp = list; while (tmp) { alert(tmp.value); tmp = tmp.next; } } printList(list); لاحظ كيف استعملنا المتغير المؤقت tmp للمرور على عناصر القائمة. يمكننا نظريًا استعمال مُعامل الدالة list بدل ذلك: function printList(list) { while(list) { // (*) alert(list.value); list = list.next; } } ولكن… سنندم على ذلك لاحقًا إذ قد نحتاج إلى توسيع عمل الدالة وإجراء عملية أخرى غير هذه على القائمة، ولو بدّلنا list فلن نقدر على ذلك حتمًا. وعلى سيرة الحديث عن تسمية المتغيرات «كما ينبغي»، فهنا تُعدّ القائمةُ list ذاتَ القائمة، أي العنصر الأوّل من تلك القائمة، ويجب أن يبقى الاسم كما هو هكذا، مقروءًا وواضحًا. بينما لا يعدو دور tmp إلّا أداةً لمسح القائمة، تمامًا مثل i في حلقات for. نسخة التعاود مفهوم النسخة التعاودية من الدالة printList(list) بسيط: علينا -كي نطبع قائمةً- طباعةَ العنصر الحالي list وتكرار ذلك على كلّ list.next: let list = { value: 1, next: { value: 2, next: { value: 3, next: { value: 4, next: null } } } }; function printList(list) { alert(list.value); // نطبع العنصر الحالي if (list.next) { printList(list.next); // ذات الحركة لكلّ عنصر باقٍ في القائمة } } printList(list); أمّا الآن، فأيّ النسختين أفضل؟ نظريًا تُعدّ نسخة الحلقات أفضل أداءً. صحيح أنّ الاثنتين عملهما واحد إلّا أن الحلقات لا تستهلك الموارد بتداخل استدعاءات الدوال. ولكن لو نظرنا للجهة الأخرى من الكأس فالنسخة التعاودية أقصر وأسهل فهمًا أحيانًا. طباعة قائمة مترابطة بالعكس الأهمية: 5 اطبع القائمة المترابطة من التمرين السابق، ولكن بعكس ترتيب العناصر. اصنع نسختين من الحل: واحدة باستعمال الحلقات وواحدة باستعمال التعاود. الحل نسخة التعاود هنا توجد خدعة في فكرة التعاود، إذ علينا أوّلًا طباعة الباقي من القائمة وبعدها طباعة القائمة الحالية: let list = { value: 1, next: { value: 2, next: { value: 3, next: { value: 4, next: null } } } }; function printReverseList(list) { if (list.next) { printReverseList(list.next); } alert(list.value); } printReverseList(list); نسخة الحلقات نسخة الحلقات هنا أكثر تعقيدًا (بقليل) عن سابقتها. ما من طريقة لنأخذ آخر قيمة في قائمتنا list، ولا يمكننا أن «نعود» فيها. لذا يمكننا أوّلًا المرور على العناصر بالترتيب المباشر وحِفظها في مصفوفة، بعدها طباعة ما حفظناه بالعكس: let list = { value: 1, next: { value: 2, next: { value: 3, next: { value: 4, next: null } } } }; function printReverseList(list) { let arr = []; let tmp = list; while (tmp) { arr.push(tmp.value); tmp = tmp.next; } for (let i = arr.length - 1; i >= 0; i--) { alert( arr[i] ); } } printReverseList(list); لاحظ كيف أنّ الحل باستعمال التعاود هو بالضبط كما باستعمال الحلقات، إذ يتبع القائمة ويحفظ العناصر في سلسلة من الاستدعاءات المتداخلة (في مكدس سياق التنفيذ)، وبعدها يطبع القيم. ترجمة -وبتصرف- للفصل Recursion and stack من كتاب The JavaScript language
-
لنقل بأنّ لدينا كائن معقّد البنية ونريد تحويله إلى سلسلة نصية؛ كي نُرسله عبر الشبكة أو نطبعه في الطرفية لتسجيل المخرجات. الطبيعي هو أن تكون في هذه السلسلة النصية كلّ الخاصيات المهمة. يمكننا إجراء هذا التحويل بهذه الطريقة: 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: تمثيل التاريخ والوقت
-
حان وقت الحديث عن كائن آخر مضمّن في اللغة: التاريخ 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
-
في جافاسكربت، الكائنات والمصفوفات هي أكثر بنى البيانات المستعملة. تُتيح لنا الكائنات إنشاء كيان واحد يُخزّن عناصر البيانات حسب مفاتيحها، وتُتيح لنا المصفوفات بجمع مختلف عناصر البيانات في تجميعة مرتّبة (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: تمثيل التاريخ والوقت المقال السابق: مفاتيح الكائنات وقيمها ومدخلاتها
-
لنأخذ راحة صغيرة بعيدًا عن بنى البيانات ولنتحدّث عن طريقة المرور على عناصرها. رأينا في الفصل السابق التوابِع 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: الخرائط والأطقم ضعيفة الإشارة
-
كما عرفنا من فصل «كنس المهملات»، فمُحرّك جافاسكربت يخُزّن القيمة في الذاكرة طالما يمكن أن يصل لها شيء (أي يمكن استعمالها لاحقًا). هكذا: 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 (الأطقم)
-
تعلّمنا حتّى الآن بنى البيانات المعقّدة هذه: الكائنات 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)
-
الكائنات المُكرَّرة (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 تقديم دالة لكلّ عنصر من عناصرها. ترجمة -وبتصرف- للفصل Iterables من كتاب 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)
-
تقدّم المصفوفات توابِع عديدة تُسهِّل التعامل معها. ولتبسيطها سنقسّمها إلى مجموعات بحسب الوظيفة في هذا الفصل ونشرح كل منها على حدة. إضافة العناصر وإزالتها عرفنا من الفصل الماضي بالتوابِع التي تُضيف العناصر وتُزيلها من بداية أو نهاية المصفوفة: 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)
-
تُتيح لك الكائنات تخزين القيم في مجموعات ذات مفاتيح، وهذا أمر طيّب. ولكنّك دومًا ما ستحتاج في عملك إلى مجموعة مرتّبة، أي أنّ العناصر مرتّبة: عنصر أوّل، عنصر ثانٍ، عنصر ثالث، وهكذا دواليك. تُفيدنا هذه الطريقة في تخزين أمور مثل: المستخدمين والبضائع وعناصر 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)
-
كثيرًا ما يتكرّر هذا السيناريو: يأتي مطوّر وِب بنيّة إضفاء ”العصريّة“ لأحد المواقع (أي ليحسّن تجربته على المحمول أو يعدّل لوحة الألوان المستعملة فيه أو الخطوط أو أيّ شيء آخر)، وينتهي الأمر بأن يهدم إحدى أكثر المشاريع التجارية ازدهارًا على الإنترنت – أن يقتل الموقع بأخطاء السيو 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
- 5 تعليقات
-
- 1
-
- ووردبريس
- تطوير المواقع
- (و 4 أكثر)
-
الهواتف الذكيّة والسيارات، الحواسيب الخارقة والأجهزة المنزلية، أجهزة سطح المكتب وخواديم الشركات - كلها تستعمل نظام التشغيل لينكس، النظام الذي ستراه حولك أينما أدرت وجهك. بدأ نظام التشغيل لينكس منذ منتصف التسعينات تقريبًا وقد حقّق منذئذ قاعدة مستخدمين ضخمة تجوب العالم. وكما مستخدميه، فلينكس في كل مكان: أجهزتك المحمولة، منظّمات الحرارة، السيارات، الثلاجات، أجهزة 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