ماذا يحدث عند جمع كائنين بالشكل obj1 + obj2
، أو طرح كائنين obj1 - obj2
أو طباعة الكائنات باستخدام alert(obj)
؟ تحوَّل الكائنات في مثل هذه الحالات إلى أنواع أولية (primitives) ثم تُنَفَّذ العملية.
رأينا في فصل تحويل الأنواع قواعد التحويل بين الأنواع الأساسية مثل الارقام، النصوص أو القيم المنطقية، لكن بقيت فجوة متعلقة بالكائنات. الآن بعد أن تعرفنا على الرموز والدوال، يمكن ملء هذه الفجوة.
1- في السياق المنطقي (boolean)، جميع الكائنات تساوي القيمة true
، إذ يوجد تحوَّل الكائنات إلى أرقام ونصوص فقط.
2- يحدث التحويل العددي (numeric) عند طرح الكائنات أو تنفيذ العمليات الرياضية عليها. مثلًا، يمكن طرح كائنات Date
(سيتم شرحها في فصل التاريخ والوقت) ويكون ناتج طرح date1 - date2
هو الفارق الزمني بين التاريخين.
3- بالنسبة للتحويل إلى نص (string)، يحدث ذلك عادةً عند إخراج كائن بالطريقة alert(obj)
وأي سياق مشابه.
التحويل إلى أنواع أساسية عبر ToPrimitive
يمكننا ضبط التحويل إلى نص أو عدد باستخدام دوال خاصة بالكائنات. يوجد ثلاثة أنواع للتحويل بين الأنواع (يطلق عليها «النوع المخمَّن» [hint] الذي سيجري تحويل الكائن إليه)، مشروحة في هذه المواصفات:
إلى سلسلة نصية "string"
يُجرَى التحويل من كائن لنص عند القيام بعمليات على كائن تتوقع أن يكون الكائن نصًا، مثل alert
:
// المخرجات alert(obj); // استخدام كائن كمفتاح للخاصية anotherObj[obj] = 123;
إلى عدد "number"
يجري تحويل كائن لعدد عند القيام بعمليات حسابية:
// تحويل صريح let num = Number(obj); // ( عمليات رياضية (عدى عملية الجمع الأحادي let n = +obj; // عملية جمع أحادية let delta = date1 - date2; // عملية موازنة أصغر/أكبر let greater = user1 > user2;
إلى نوع افتراضي "default"
يحدث في حالات نادرة عندما لا يكون المُشَغِّل متأكدًا من نوع البيانات المُتوَقَّع. فمثلًا، يمكن لِمُعامِل الجمع الثنائي +
أن يتعامل مع كلًا من النصوص (دمج السلسلتين) ومع الأرقم (بجمعها)، لذا فإن كلًا من النصوص والأرقام تعمل بشكل صحيح. أيضًا، عند موازنة كائن بنص، أو رقم أو رمز، فلا يكون واضحًا أي نوع من التحويلات هو المطلوب.
// الجمع الثنائي let total = car1 + car2; // obj == string/number/symbol if (user == 1) { ... };
يمكن لِمعاملات الموازنة أكبر/أصغر <>
التعامل أيضًا مع كلً من النصوص والأعداد لكنها ترجح التحويل إلى عدد دون النظر إلى التحويل الافتراضي المرجح في هذه الحالة وذلك لأسباب تاريخية.
عمليًا، تُطبِّق جميع الكائنات المُدمجة في اللغة التحويل الافتراضي "default"
بنفس طريقة التحويل إلى عدد "number"
. وربما يجب أن نقوم بذلك أيضًا. (عدا الكائن Date
، والذي سَنتعلمه لاحقَا.)
لاحظ أن هناك ثلاثة أنواع للتحويل فقط، إذ الأمر بهذه البساطة. لا يوجد تحويل للنوع المنطقي (boolean، فجميع الكائنات تحمل القيمة true
في السياق المنطقي) أو أي شيء آخر. وإن عاملنا كلًا من التحويل الافتراضي "default"
والتحويل العددي "number"
بالطريقة ذاتها كما تقوم أغلب الدوال المدمجة، فسيكون هنالك تحويلين فقط.
تحاول JavaScript العثور على ثلاث دوال للكائن واستدعائها عند القيام بالتحويل
-
استدعاء
obj[Symbol.toPrimitive](hint)
- الدالة ذات المفتاح الرمزيSymbol.toPrimitive
(رمز نظام)، إن كانت هذه الدالة موجودة. -
أو إن كان النوع المخمَّن هو نص
"string"
-
جرب
obj.toString()
وobj.valueOf()
، أيًا كانت متواجدة.
-
جرب
-
إن كان hint هو عدد
"number"
أو"default"
-
جرب
obj.valueOf()
وobj.toString()
أيًا كانت متواجدة.
-
جرب
تابع التحويل Symbol.toPrimitive
لنبدأ من التابع الأول؛ يوجد رمز مُضَمَّن في JavaScript مُسَمَّى Symbol.toPrimitive
والذي يجب استخدامه لتسمية تابع التحويل، هكذا:
obj[Symbol.toPrimitive] = function(hint) { // يجب أن تُرجِع قيمة أولية // النوع المخمَّن هو إما نص أو عدد أو النوع الافتراضي };
مثلا، الكائن user
هنا يتضمنها:
let user = { name: "John", money: 1000, [Symbol.toPrimitive](hint) { alert(`hint: ${hint}`); return hint == "string" ? `{name: "${this.name}"}` : this.money; } }; // تجربة للتحويل: alert(user); // hint: string -> {name: "John"} alert(+user); // hint: number -> 1000 alert(user + 500); // hint: default -> 1500
كما يمكننا أن نرى في الشيفرة، أصبح user
نصًا يصف نفسه أو كمية من المال وفقًا للتحويل. تقوم الدالة user[Symbol.toPrimitive]
بجميع حالات التحويل.
التحويل إلى نص عبر toString
أو valueOf
ظهر التابعان toString
و valueOf
منذ وقت طويل، ولا يتبعان إلى نوع الرمز (symbol، إذ لم تكن الرموز موجودة في ذلك الحين)، لكنهما تابعين مسميان بأسماء توحي بارتباطهما بالنوع النصي، إذ كانت توفر آلية لعملية التحويل أصبحت قديمة النمط الآن.
إن لم يكن هناك تنفيذًا للتابع Symbol.toPrimitive
فتحاول JavaScript إيجاد هذه الدوال بالترتيب التالي:
-
toString -> valueOf
للنصوص. -
valueOf -> toString
لباقي الأنواع.
مثلا، يقوم الكائن user
بنفس الغرض السابق باستخدام toString
و valueOf
:
let user = { name: "John", money: 1000, // في حال النوع المخمَّن سلسلة نصية toString() { return `{name: "${this.name}"}`; }, // في حال النوع المخمَّن عدد أو النوع الافتراضي valueOf() { return this.money; } }; alert(user); // toString -> {name: "John"} alert(+user); // valueOf -> 1000 alert(user + 500); // valueOf -> 1500
كما رأينا، يتنفذ السلوك ذاته كما في المثال السابق الذي استخدم Symbol.toPrimitive
.
نحتاج غالبًا إلى مكان واحد يقوم بجميع التحويلات للأنواع الأولية (نص أو عدد). ففي هذه الحالة، يمكننا استخدام التابع toString
فقط لينفِّذ جميع عمليات التحويل، كما يلي:
let user = { name: "John", toString() { return this.name; } }; alert(user); // toString -> John alert(user + 500); // toString -> John500
سينفِّذ التابع toString
بجميع التحويلات الأولية في غياب كلًا من Symbol.toPrimitive
و valueOf
.
الأنواع المعادة
الأمر المهم الذي يجب معرفته عن جميع دوال التحويل بين الأنواع الأولية هو أنها لا تُرجِع بالضرورة النوع المخمَّن (hint) الأولي ذاته. لا يوجد تحكم في ما إن كانت toString()
ترجع نصًا، أو أن Symbol.toPrimitive
تُرجِع عددًا عند تحويل عدد. الأمر الوحيد المعروف والثابت هو أنَّ هذه الدوال تُرجِع نوعًا اوليًا وليس كائنًا.
ملاحظة تاريخية
لا يوجد خطأ إن أعاد التابع toString
أو valueOf
كائنًا، وذلك لأسباب تاريخية، لكن يتم تجاهل مثل هذه القيم (وكأن الدالة ليست موجودة). وذلك لعدم وجود مبدأ الخطأ الجيد (good error) في JavaScript حينها.
بالمقابل، يجب أن يعيد التابع Symbol.toPrimitive
قيمة أولية، وإلا فسيكون هناك خطأ.
عمليات تحويل إضافية
عرفنا مسبقًا أن جميع العوامل (operator) والدوال تجري عمليات تحويل على الأنواع التي تتعامل معها مثل معامل الضرب *
يحول جميع المُعامَلات (operands) إلى أعداد. فإن مرَّرنا كائنًا عبر وسيط إلى إحدى الدوال أو العمليات، فستمر عملية التحويل على مرحلة أو مرحلتين هما:
- يحوَّل الكائن إلى نوع أولي (باستعمال القواعد التي تحدثنا عنها آنفًا)
- إن لم يكن النوع الأولي الناتج مطابقًا للنوع المطلوب، فيحوَّل إلى النوع المطلوب وفق مبدأ التحويل بين الأنواع الأولية
إليك المثال التالي:
let obj = { // جميع التحويلات في غياب باقي الدوال toString يجري التابع toString() { return "2"; } }; // حُوِّل الكائن إلى القيمةا لأولية "2", ثم جعلته عملية الضرب عددًا alert(obj * 2); // 4
-
حوَّلت عملية الضرب
obj * 2
الكائن إلى نوع أولي، وكان النوع المعاد من عملية التحويل سلسلةً نصية، هي"2"
. -
جرى بعدئذٍ تحويل تلك السلسلة في العملية
"2" * 2
، إلى عدد ليصبح2 * 2
.
في المثال التالي، يقوم الجمع الثنائي بدمج النصوص في هذه الحالة والاكتفاء بعملية تحويل الكائن إلى سلسلة نصية:
let obj = { toString() { return "2"; } }; // (أعادت عملية التحويل إلى نوع أولي نصًا => دمج) alert(obj + 2); // 22
الخلاصة
تجرَى عملية التحويل من كائن لنوع أولي تلقائيًا بواسطة العديد من الدوال المدمجة في اللغة والمُعاملات التي تتوقع أن تتعامل مع قيم أولية.
يخمَّن النوع الأولي المراد تحويل الكائن إليه إلى:
-
سلسلة نصية
"string"
(للدالةalert
والعمليات الأخرى التي تتعامل مع نصوص) -
عدد
"number"
(للعمليات الرياضية) -النوع الافتراضي"default"
(لبعض العوامل)
تُحدِّد المواصفات النوع المخمَّن (hint) الذي يستخدمه كل مُعامل بوضوح. يوجد القليل من العوامل التي لا تعلم ما النوع المتوقع وتستخدم النوع الافتراضي "default"
. يُعامل النوع الافتراضي "default"
معامل العدد "number"
في الكائنات أغلب الأحيان؛ لذا، يتم دمج النوعين عمليًا.
خوارزمية التحويل كالتالي:
-
استدعاء
obj[Symbol.toPrimitive](hint)
إن وُجِدَت، -
أو إن كان النوع المخمَّن هو نص
"string"
-
جرب
obj.toString()
وobj.valueOf()
، أيًا كانت متواجدة.
-
جرب
-
إن كان النوع المُخمَّن هو عدد
"number"
أو"default"
-
جرب
obj.valueOf()
وobj.toString()
أيًا كانت متواجدة.
-
جرب
عمليًا، يكفي استخدام obj.toString()
فقط لإجراء جميع التحويلات، إذ تعيد "شيئًا مقروءًا" يمثِّل الكائن يستعمل هذا التمثيل لعملية التسجيل أو التنقيح.
ترجمة -وبتصرف- للفصل Object to primitive conversion من كتاب The JavaScript Language
اقرأ أيضًا
- المقال التالي: الباني والعامل "new"
- المقال السابق: الدوال في الكائنات واستعمالها this
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.