ماذا يحدث عند إضافة الكائنات obj1 + obj2
أو طرح obj1 - obj2
أو طباعتها باستخدام alert(obj)
؟
لا تسمح لك JavaScript بتخصيص كيفية عمل العوامل على الكائنات. على عكس بعض لغات البرمجة الأخرى، مثل Ruby أو C++، لا يمكننا تنفيذ طريقة كائن خاصة للتعامل مع عملية الإضافة (أو العوامل الأخرى).
في حالة مثل هذه العمليات، يتم تحويل الكائنات تلقائيًا إلى عناصر أولية، ومن ثم يتم تنفيذ العملية على هذه العناصر الأولية وينتج عنها قيمة أولية.
وهذا قيد مهم: لا يمكن أن تكون نتيجة obj1 + obj2
(أو أي عملية حسابية أخرى) كائنًا آخر!
على سبيل المثال، لا يمكننا إنشاء كائنات تمثل المتجهات أو المصفوفات (أو الإنجازات أو أي شيء آخر)، وقم بإضافتها ونتوقع كائنًا "مجمعًا" كنتيجة. مثل هذه الأعمال المعمارية الرائعة تصبح "خارج نطاق الموضوع" تلقائيًا.
لذا، نظرًا لأننا لا نستطيع فعل الكثير من الناحية الفنية هنا، فلا يوجد رياضيات مع الأشياء في المشاريع الحقيقية. عندما يحدث ذلك، مع استثناءات نادرة، يكون ذلك بسبب خطأ في الترميز.
سنغطي في هذا الفصل كيفية تحويل الكائن إلى كائن بدائي وكيفية تخصيصه.
لدينا غرضان:
Date
). سوف نلتقي بهم لاحقا.في الفصل تحويلات النوع، رأينا قواعد التحويلات الرقمية والسلسلة والمنطقية للأوليات. لكننا تركنا فجوة للأشياء. الآن، كما عرفنا عن الأساليب والرموز، أصبح من الممكن ملئها.
true
في سياق منطقي، بهذه البساطة. لا يوجد سوى تحويلات رقمية وسلسلة.Date
(التي سيتم تناولها في الفصل التاريخ والوقت)، وتكون نتيجة date1 - date2
هي الفرق الزمني بين تاريخين.alert(obj)
وفي سياقات مماثلة.يمكننا تنفيذ تحويل السلسلة والرقم بأنفسنا، باستخدام أساليب كائن خاصة.
والآن دعونا ندخل في التفاصيل الفنية، لأنها الطريقة الوحيدة لتغطية الموضوع بعمق.
كيف تحدد JavaScript التحويل المطلوب تطبيقه؟
هناك ثلاثة أنواع مختلفة من تحويل النوع، والتي تحدث في مواقف مختلفة. يطلق عليها "تلميحات"، كما هو موضح في المواصفات:
"string"
بالنسبة للتحويل من كائن إلى سلسلة، عندما نقوم بعملية على كائن يتوقع سلسلة، مثل alert
:
// output alert(obj); // using object as a property key anotherObj[obj] = 123;
"number"
بالنسبة للتحويل من كائن إلى رقم، كما هو الحال عندما نقوم بإجراء العمليات الحسابية:
// 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;
تتضمن معظم الوظائف الرياضية المضمنة أيضًا مثل هذا التحويل.
"default"
يحدث في حالات نادرة عندما يكون المشغل "غير متأكد" من النوع المتوقع.
على سبيل المثال، ثنائي زائد +
يمكن أن يعمل مع كل من السلاسل (يسلسلها) والأرقام (يجمعها). لذا، إذا حصلت علامة الجمع الثنائية على كائن كوسيطة، فإنها تستخدم التلميح "default"
لتحويله.
بالإضافة إلى ذلك، إذا تمت مقارنة كائن باستخدام ==
بسلسلة أو رقم أو رمز، فمن غير الواضح أيضًا أي تحويل يجب إجراؤه، لذلك يتم استخدام التلميح "default"
.
// binary plus uses the "default" hint let total = obj1 + obj2; // obj == number uses the "default" hint if (user == 1) { ... };
يمكن لعوامل المقارنة الأكبر والأصغر، مثل <
>
، العمل مع كل من السلاسل والأرقام أيضًا. ومع ذلك، فهم يستخدمون تلميح "number"
، وليس "default"
. وذلك لأسباب تاريخية.
لكن في الممارسة العملية، الأمور أبسط قليلاً.
جميع الكائنات المضمنة باستثناء حالة واحدة (كائن Date
، سنتعرف عليه لاحقًا) تنفذ التحويل "default"
بنفس طريقة التحويل "number"
. وربما ينبغي لنا أن نفعل الشيء نفسه.
ومع ذلك، من المهم أن نعرف كل التلميحات الثلاثة، وسنرى السبب قريبًا.
لإجراء التحويل، تحاول JavaScript البحث عن ثلاث طرق للكائنات واستدعاءها:
obj[Symbol.toPrimitive](hint)
- الطريقة ذات المفتاح الرمزي Symbol.toPrimitive
(رمز النظام)، إذا كانت هذه الطريقة موجودة،"string"
obj.toString()
أو obj.valueOf()
، مهما كان موجودًا."number"
أو "default"
obj.valueOf()
أو obj.toString()
، مهما كان موجودًا. لنبدأ من الطريقة الأولى. هناك رمز مضمن اسمه Symbol.toPrimitive
والذي ينبغي استخدامه لتسمية طريقة التحويل، مثل هذا:
obj[Symbol.toPrimitive] = function(hint) { // here goes the code to convert this object to a primitive // it must return a primitive value // hint = one of "string", "number", "default" };
في حالة وجود الأسلوب Symbol.toPrimitive
، فسيتم استخدامه لجميع التلميحات، ولن تكون هناك حاجة إلى المزيد من الأساليب.
على سبيل المثال، هنا يقوم كائن 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]
جميع حالات التحويل.
إذا لم يكن هناك Symbol.toPrimitive
، فستحاول JavaScript العثور على توابع toString
و valueOf
:
"string"
: قم باستدعاء الأسلوب toString
، وإذا لم يكن موجودًا أو إذا قام بإرجاع كائن بدلاً من قيمة أولية، فاستدعاء valueOf
(وبالتالي فإن toString
لها الأولوية لتحويلات السلسلة).valueOf
، وإذا لم تكن موجودة أو إذا أعادت كائنًا بدلاً من قيمة أولية، فاتصل toString
(لذا فإن valueOf
لها الأولوية للرياضيات). طرق toString
valueOf
تأتي من العصور القديمة. إنها ليست رموزًا (لم تكن الرموز موجودة منذ فترة طويلة)، ولكنها بالأحرى أساليب ذات أسماء سلسلة "عادية". أنها توفر طريقة بديلة "قديمة الطراز" لتنفيذ التحويل.
يجب أن تقوم هذه الأساليب بإرجاع قيمة أولية. إذا toString
أو valueOf
بإرجاع كائن، فسيتم تجاهله (تمامًا كما لو لم تكن هناك طريقة).
افتراضيًا، يتبع الكائن العادي توابع toString
و valueOf
:
toString
بإرجاع سلسلة "[object Object]"
.valueOf
بإرجاع الكائن نفسه.وهنا العرض التوضيحي:
let user = {name: "John"}; alert(user); // [object Object] alert(user.valueOf() === user); // true
لذا، إذا حاولنا استخدام كائن كسلسلة، كما هو الحال في alert
أو نحو ذلك، فسنرى افتراضيًا [object Object]
.
تم ذكر valueOf
الافتراضية هنا فقط من أجل الاكتمال، لتجنب أي لبس. كما ترون، فإنه يعيد الكائن نفسه، وبالتالي يتم تجاهله. لا تسألني لماذا، هذا لأسباب تاريخية. لذلك يمكننا أن نفترض أنه غير موجود.
دعونا ننفذ هذه الطرق لتخصيص التحويل.
على سبيل المثال، يقوم 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
.toPrimitive .
غالبًا ما نريد مكانًا واحدًا "شاملًا" للتعامل مع جميع التحويلات البدائية. في هذه الحالة، يمكننا تنفيذ toString
فقط، مثل هذا:
let user = { name: "John", toString() { return this.name; } }; alert(user); // toString -> John alert(user + 500); // toString -> John500
في حالة عدم وجود Symbol.toPrimitive
toPrimitive و valueOf
، سيتعامل toString
مع جميع التحويلات البدائية.
الشيء المهم الذي يجب معرفته عن جميع طرق التحويل البدائية هو أنها لا تُرجع بالضرورة البدائية "الملمحة".
لا يوجد أي تحكم فيما إذا كان toString
يُرجع سلسلة نصية بالضبط، أو ما إذا كان الأسلوب Symbol.toPrimitive
يُرجع رقمًا للتلميح "number"
.
الشيء الإلزامي الوحيد: يجب أن تُرجع هذه الأساليب كائنًا بدائيًا وليس كائنًا.
لأسباب تاريخية، إذا قام toString
أو valueOf
بإرجاع كائن، فلا يوجد خطأ، ولكن يتم تجاهل هذه القيمة (كما هو الحال في حالة عدم وجود الطريقة). وذلك لأنه في العصور القديمة لم يكن هناك مفهوم "خطأ" جيد في JavaScript.
في المقابل، فإن Symbol.toPrimitive
أكثر صرامة، ويجب أن يُرجع عنصرًا أوليًا، وإلا فسيكون هناك خطأ.
كما نعلم بالفعل، تقوم العديد من العوامل والوظائف بإجراء تحويلات للنوع، على سبيل المثال، يقوم الضرب *
بتحويل المعاملات إلى أرقام.
إذا مررنا كائنًا كوسيطة، فهناك مرحلتان من العمليات الحسابية:
على سبيل المثال:
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
obj * 2
أولاً بتحويل الكائن إلى كائن بدائي (وهذا عبارة عن سلسلة "2"
)."2" * 2
2 * 2
(يتم تحويل السلسلة إلى رقم).سوف يقوم Binary plus بتسلسل السلاسل في نفس الموقف، حيث أنه يقبل بكل سرور سلسلة:
let obj = { toString() { return "2"; } }; alert(obj + 2); // "22" ("2" + 2), conversion to primitive returned a string => concatenation
يتم استدعاء التحويل من كائن إلى بدائي تلقائيًا بواسطة العديد من الوظائف والمشغلات المضمنة التي تتوقع بدائية كقيمة.
هناك 3 أنواع (تلميحات) منها:
"string"
( alert
والعمليات الأخرى التي تحتاج إلى سلسلة)"number"
(للرياضيات)"default"
(عدد قليل من العوامل، عادةً ما تنفذه الكائنات بنفس طريقة تنفيذ "number"
)تصف المواصفات بوضوح أي عامل يستخدم أي تلميح.
خوارزمية التحويل هي:
obj[Symbol.toPrimitive](hint)
إذا كانت الطريقة موجودة،"string"
obj.toString()
أو obj.valueOf()
، مهما كان موجودًا."number"
أو "default"
obj.valueOf()
أو obj.toString()
، مهما كان موجودًا.يجب أن تقوم كل هذه الأساليب بإرجاع بدائية للعمل (إذا تم تعريفها).
من الناحية العملية، غالبًا ما يكفي تنفيذ obj.toString()
فقط كطريقة "التقاط الكل" لتحويلات السلسلة التي يجب أن تُرجع تمثيلًا "يمكن قراءته بواسطة الإنسان" لكائن ما، لأغراض التسجيل أو تصحيح الأخطاء.