٢٥ مارس ٢٠٢١

تحويل الكائنات إلى قيم مفرده

ماذا يحدث فى حالة جمع كائنين obj1 + obj2، أو طرحهما obj1 - obj2 أو طباعتهما باستخدام دالة التنبيه alert(obj) ؟

فى هذه الحالة، تتحول الكائنات إلى قيم فردية تلقائيًا، ثم يتم تنفيذ هذه العملية الحسابية.

فى قسم (تحويل الأنواع) رأينا كيف يمكن تحويل النصوص (strings) والأرقام والقيَم المنطقيه (booleans) إلى قيم فردية. ولكننا تركنا مساحة فارغة من أجل الكائنات. والآن بعد أن عرفنا الكثير عن الدوال (methods) والرموز (symbols)، أصبح الآن ممكنًا أن نملأ هذه المساحه.

  1. كل الكائنات عند تحويلها إلى قيمه منطقيه (boolean) فإن قيمتها تساوى true. وبالتالى فإن التحويلات المتاحة هي التحويل إلى نص أو رقم.

  2. يحدث التحويل إلى رقم عند طرح كائنين أو استخدام دالة حسابية. على سبيل المثال، الكائنات من نوع Date (سيتم شرحها فى قسم التاريخ) يمكن طرحها، ونتيجة طرح date1 - date2 هي الفرق بين التاريخين.

  3. وبالنسبه إلى التحويل إلى نص – فإنه يحدث عادة عند طباعة الكائن باستخدام دالة التنبيه alert(obj) والدوال المشابهة.

ToPrimitive

يمكننا التحكم فى التحويل إلى نص أو رقم، باستخدام بعض دوال الكائنات.

هناك ثلاث ملاحظات مختلفه على تحويل الأنواع ويطلق عليها “hints” وتم ذكرها فى المصدر:

"النص"

يحدث التحويل إلى نص عندما نقوم بعملية معينه على كائن تتوقع نصًا لا كائنًا مثل دالة التنبيه alert:

// output
alert(obj);

// using object as a property key
anotherObj[obj] = 123;
"الرقم"

يحدث التحويل إلى رقم عندما نقوم يعملية حسابيه على سبيل المثال:

// explicit conversion
let num = Number(obj);

// maths (except binary plus)
let n = +obj; // unary plus
let delta = date1 - date2;

// less/greater comparison
let greater = user1 > user2;

"التصرف الإفتراضي"

For instance, binary plus `+` can work both with strings (concatenates them) and numbers (adds them), so both strings and numbers would do. So if a binary plus gets an object as an argument, it uses the `"default"` hint to convert it.

على سبيل المثال، العلامه + يمكن أن تعمل مع النصوص (حيث تقوم بالإضافه) أو الأرقام (حيث تقوم بالجمع)، ولذلك فإنه يمكن التحويل إلى نصوص أو أرقام. ولذلك إذا استقبلت علامة ال + كائنا فإنها تستخدم "التصرف الإفتراضي".

وأيضًا فى حالة مقارنة كائن مع نص أو رقم أو رمز باستخدام == فإنه ليس واضح لأى نوع يمكن التحويل، ولذلك يتم استخدام "التصرف الإفتراضي".

// binary plus uses the "default" hint
let total = obj1 + obj2;

// obj == number uses the "default" hint
if (user == 1) { ... };

المقارنه باستخدام علامات الأكبر من أو الأصغر من مثل < >، يمكنها التعامل مع الأرقام والنصوص أيضا ولكنها مع ذلك تستخدم التحويل إلى رقم وليس الطريقه الافتراضيه، وهذا لأسباب متأصله historical reasons.

لا نحتاج إلى تذكر كل هذه التفاصيل الغريبه لأن كل الكائنات الموجوده عدا (Date والذي سيتم شرحه قريبا) يتم تحويلها باستخدام "الطريقه الإفتراضيه" مثل طريقة التحويل إلى رقم.

لا يوجد التحويل إلى "القيم المنطقيه"

لاحظ أن هناك ثلاث طرق (أو ملاحظات) فقط بكل بساطه.

لا توجد طريقة التحويل إلى “قيمه منطقيه” (لأن كل الكائنات قيمتها true عن تحويلها إلى قيمه منطقيه). وإذا تعاملنا مع "الطريقه الإفتراضيه" و "الرقم" بطريقة مشابهة مثل كل الطرق الموجوده فسيكون هناك طريقتين فقط.

عند القيام بالتحويل، تقوم جافا سكريبت باستدعاء ثلاث دوال:

  1. استدعاء obj[Symbol.toPrimitive](hint) – وهو رمز موجود بالفعل (built-in)، وهذا فى حالة وجود هذه الدالة.
  2. فى حالة عدم وجودها وطانت الطريقه هى التحويل إلى نص
    • استخدام obj.toString() و obj.valueOf()، أيهم موجود.
  3. غير ذلك، إذا كانت الطريقه هي "الطريقه الإفتراضيه" أو "الرقم"
    • استخدام obj.valueOf() و obj.toString()، أيهم موجود.

Symbol.toPrimitive

لنبدأ بأول طريقه. يوجد رمز (symbol) موجود بالفعل يسمى Symbol.toPrimitive والذي يجب استخدامه لتسمية طريقة التحويل كالآتى:

obj[Symbol.toPrimitive] = function(hint) {
  // must return a primitive value
  // hint = one of "string", "number", "default"
};

على سبيل المثال, يطبق هذه الطريقه الكائن user:

let user = {
  name: 'John',
  money: 1000,

  [Symbol.toPrimitive](hint) {
    alert(`hint: ${hint}`);
    return hint == 'string' ? `{name: "${this.name}"}` : this.money;
  },
};

// conversions demo:
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.toPrimitive فإن جافا سكريبت تقوم بالبحث عنهمو استخدامهم بالترتيب الآتى:

  • toString -> valueOf فى الطريقه النصيه.
  • valueOf -> toString غير ذلك.

هذه الدوال لابد أن تقوم بإرجاع قيمه فردية. فإذا قامت هاتان الدالتان بإرجاع كائن فسيتم تجاهله.

أى كائن يمتلك افتراضيا الدالتين toString و valueOf:

  • الداله toString تقوم بإرجاع النص "[object Object]".
  • الداله valueOf تقوم بإرجاع الكائن نفسه.

كما فى المثال:

let user = { name: 'John' };

alert(user); // [object Object]
alert(user.valueOf() === user); // true

لذلك إذا حاولنا أن نستخدم الكائن كنص، كما فى حالة استخدام الداله النصيه alert سنرى بشكل افتراضي [object object].

الداله valueOf تم ذكرها هنا فقط لإكمال المعلومات ولتجنب أى التباس. فكما ترى فإن هذه الداله تقوم بإرجاع الكائن نفسه وبالتالى يتم تجاهله. لا تسأل لماذا فهذا لأسباب متأصله historical reasons. ولذلك يمكننا اعتبار أنها غير موجوده.

هيا نقوم باستخدام هذه الدوال.

على سبيل المثال، فإن الكائن user هنا يقوم بنفس التصرف أعلاه عند استخدام خليط من toString و valueOf بدلًا من Symbol.toPrimitive:

let user = {
  name: 'John',
  money: 1000,

  // for hint="string"
  toString() {
    return `{name: "${this.name}"}`;
  },

  // for hint="number" or "default"
  valueOf() {
    return this.money;
  },
};

alert(user); // toString -> {name: "John"}
alert(+user); // valueOf -> 1000
alert(user + 500); // valueOf -> 1500

كما نرى هنا فإن التصرف هو نفسه الموجود فى المثال السابق عند استخدام Symbol.toPrimitive.

ونحن عالبا مانحتاج إلى طريقه للتعامل مع كل حالات التحويل إلى قيم فرديه (primitive values). ففى هذه الحاله يمكننا استخدام toString فقط كالآتى:

let user = {
  name: 'John',

  toString() {
    return this.name;
  },
};

alert(user); // toString -> John
alert(user + 500); // toString -> John500

فى حالة غياب Symbol.toPrimitive و valueOf فإن toString ستقوم بالتعامل مع كل حالات التحويل إلى قيم فرديه.

أنواع القيم المسترجعه

هناك شئ مهم يجب أن تعرفه وهو أن كل طرق التحويل إلى قيم مفرده لا يجب بالضروره أن تقوم بإرجاع نفس نوع القيمه المفرده المحوَّله إليه.

فلا يوجد ضمانه إذا كانت toString ستقوم بإرجاع نص بالتحديد أو حتى Symbol.toPrimitive ستقوم بإرجاع رقم فى طريقة "الرقم".

الأمر الوحيد الذى يمكن ضمانه والإلزامى هو أن هذه الدوال يجب أن تقوم بإرجاع يمة مفردة لا كائنًا.

ملاحظات متأصله

لأسباب قديمه historical reasons فإنه فى حالة أن الدوال toString or valueOf قامت بإرجاع كائن، فلا يوجد خطأ يظهر، بل يتم تجاه النتيجه فقط كأن شيئًا لم يكن. وذلك لأنه فى الماضي لم يكن هناك مفهوم جيد للخطأ فى جافا سكريبت.

على النقيض، فإن Symbol.toPrimitive يجب أن تقوم بإرجاع قيمة مفرده، وإلا سيكون هناك خطأ.

التحويلات الإضافيه

كما نعرف بالفعل أن الكثير من العلامات والدوال تقوم بتحويل الأنواع, مثال على ذلك علامة * تقوم بتحويل العاملين إلى أرقام.

إذا استخدمنا كائنين كعملين رياضيين فسيكون هناك مرحلتين:

  1. تحويل إلى الكائن إلى قيمه مفردة.
  2. إذا كانت نتيجة التحويل ليست من النوع الصحيح فسيتم تحويلها.

على سبيل المثال:

let obj = {
  // toString handles all conversions in the absence of other methods
  toString() {
    return '2';
  },
};

alert(obj * 2); // 4, object converted to primitive "2", then multiplication made it a number
  1. عملية الضرب obj * 2 تقوم أولا بتحويل الكائن إلى قيمة مفرده (والذي هو النص "2").
  2. ثم بعد ذلك فإن الجمله "2" * 2 تتحول إلى 2 * 2 (يتحول النص إلى رقم).

علامة الجمع + ستقوم بإضافة النصوص فى نفس هذا الموقف لأنها تعمل مع النصوص:

let obj = {
  toString() {
    return '2';
  },
};

alert(obj + 2); // 22 ("2" + 2), conversion to primitive returned a string => concatenation

الملخص

التحويل من كائن إلى قيمة مفردة يحدث تلقائيا عن طريق الكثير من الدوال الموجوده بالفعل والعمليات التى تُجرى والتى تعمل فقط على قيم مفردة وليس كائنات.

هناك 3 أنواع من طرق التحويل:

  • "النص" (ويحدث ذلك عند استخدام دالة التنبيه alert والتى تتوقع نصًا).
  • "الرقم" (فى العمليات الحسابيه).
  • "الطريقة الإفتراضيه" (فى بعض العمليات).

يوضح المصدر أى عملية تستخدم أى طريقه. وهناك القليل من العمليات التي “لا تعلم ما نوع العامل الذي ستستقبله” وتستخدم "الطريقه الإفتراضيه". وعادةً ما يتم استخدام "الطريقة الإفتراضيه" مع الكائنات الموجوده بالفعل كما يتم التعامل مع "الأرقام", ولذلك عمليا فإن الطريقتين الأخيرتين يمكن ضمهما معًا.

تتم طريقة التحويل كالآتى:

  1. استدعاء الداله obj[Symbol.toPrimitive](hint) فى حالة وجودها,
  2. غير ذلك إذا كانت الظريقه "نصًا"
    • استخدام obj.toString() و obj.valueOf() فى حالة وجود أي منهم.
  3. غير ذلك إذا كانت الطريقة "رقمًا" أو "الطريقة الإفتراضيه"
    • استخدام obj.valueOf() أو obj.toString() فى حالة وجود أى منهم.

ويكفى عمليًا استخدام obj.toString() لكل التحويلات والتى تقوم بإرجاع قيمة يمكن قرائتها من أجل الطباعة أو البحث عن الأخطاء.

خريطة الدورة التعليمية