هذا المقال لمستوى: مبتدئ. لو لسه بتتعلم JavaScript وحَيّرك إن نفس القيمتين بيطلّعوا نتيجتين مختلفتين حسب العلامة اللي بينهم، ده المقال اللي يفك اللخبطة دي من جذورها.
ليه "5" + 3 بتطلّع "53" بينما "5" - 3 بتطلّع 2؟
لو كتبت "5" + 3 في JavaScript وانتظرت 8، هتتصدم لما تلاقي الناتج "53". وبعدها تكتب "5" - 3 وتلاقيه 2 بالظبط. نفس الرقمين، علامة مختلفة، عالمين مختلفين. ده مش عشوائي، وده مش bug. اسمه Type Coercion، يعني التحويل الضمني للأنواع، وفهمه هيوفّر عليك ساعات من مطاردة بوجات مش بتظهر في رسالة خطأ.
المشكلة باختصار
JavaScript لغة فيها أنواع بيانات: نص (string)، رقم (number)، وغيرهم. لكنها مش بتوقفك لما تخلط بينهم. بدل ما ترمي خطأ، بتحاول تخمّن قصدك وتحوّل نوع لنوع تاني من ورا ضهرك. المشكلة إن التخمين ده مفيد أوقات، وبيكسر حساباتك أوقات تانية. وأخطر حاجة إنه بيحصل بصمت، من غير أي تحذير.
يعني إيه Type Coercion؟ المثال الأول
تخيّل كاشير في سوبر ماركت. لو إديته صنفين وقلتله "اجمعهم"، هيحط واحد ورا التاني في كيس واحد. لكن لو إديته فلوس وقلتله "اطرح الخصم"، هيحسبها أرقام عادي. الكاشير بيقرأ نية العملية من العلامة اللي قلتهاله، مش من نوع اللي في إيدك.
JavaScript بتعمل نفس الحاجة. علامة الجمع + ليها معنيين: تجميع نصوص (concatenation) أو جمع أرقام. لمّا تلاقي طرف واحد على الأقل نص، بتقرّر إنك قصدك تجمّع نصوص، فبتحوّل الرقم 3 لنص "3" وتلزقه. لكن علامة الطرح - ليها معنى واحد بس: عملية حسابية. مفيش حاجة اسمها "طرح نصوص"، فبتحوّل النص "5" لرقم 5 وتطرح عادي.
دلوقتي الشرح العلمي بدقة: المحرّك بيطبّق عملية مجرّدة اسمها ToPrimitive على كل طرف، وبعدها العلامة بتفرض اتجاه التحويل. علامة + لو أي طرف طلع نص، بتستدعي ToString على الطرف التاني. أي علامة حسابية تانية (-، *، /، %) بتستدعي ToNumber على الطرفين. القاعدة دي مكتوبة حرفيًا في مواصفة ECMAScript تحت عمليات ToNumber وToString.
الكود اللي يثبتلك الكلام
افتح أي console (المتصفح أو Node) وجرّب ده بنفسك. كل سطر معاه ناتجه:
console.log("5" + 3); // "53" نص، لأن + لقى نص
console.log("5" - 3); // 2 رقم، لأن - حسابية
console.log("5" * "2"); // 10 الاتنين اتحولوا لأرقام
console.log(typeof ("5" + 3)); // "string"
console.log(typeof ("5" - 3)); // "number"
console.log(true + 1); // 2 true بتتحول لـ 1
console.log("" == 0); // true == بيعمل coercion كمان
الفخ الحقيقي: مجموع حقول الفورم
السيناريو ده بيحصل في أي مشروع فيه فورم. أي قيمة بتيجي من <input> بتكون نص دايمًا، حتى لو المستخدم كتب أرقام. تخيّل عندك 3 حقول سعر، كل واحد فيه "100"، والمفروض المجموع 300:
const prices = ["100", "100", "100"]; // زي اللي جاي من inputs
let total = 0;
for (const p of prices) {
total += p; // + لقى نص، فبيلزق بدل ما يجمع
}
console.log(total); // "0100100100" ← 9 خانات بدل 300
// الإصلاح: حوّل لرقم بصراحة
let fixed = 0;
for (const p of prices) {
fixed += Number(p); // ToNumber صريح
}
console.log(fixed); // 300 ✓ صح
لاحظ الفرق: النسخة الأولى طلّعت سلسلة من 9 خانات قيمتها صفر فعليًا في حساباتك، والمستخدم هيشوف رقم غريب في الفاتورة. النسخة التانية صح لأننا أجبرنا التحويل بـ Number() قبل الجمع. نفس النتيجة تتحقق بـ parseInt(p, 10) أو بعلامة الزائد الأحادية +p.
القاعدة الواحدة اللي تحفظها
ركّز في جملة واحدة: علامة + بس هي اللي بتفضّل النصوص. أي علامة حسابية تانية بتفضّل الأرقام. لو حفظت دي، 90% من مفاجآت الـ coercion هتبقى متوقّعة عندك. ولو عايز تتجنّب اللعبة كلها في المقارنات، استخدم === بدل ==، لأن === ما بيعملش تحويل أنواع خالص وبيقارن النوع والقيمة زي ما هما.
الـ trade-off
التحويل الضمني بيوفّرلك كتابة. تقدر تكتب "العمر: " + age من غير ما تحوّل age بإيدك، وده مريح في بناء النصوص. الثمن: نفس الراحة دي بتخبّي بوجات حسابية بتعدّي بصمت لحد ما تظهر في فاتورة أو تقرير. المكسب سطور أقل، الخسارة وضوح أقل وأخطاء مش بتطلع في رسالة. القاعدة العملية: استخدم التحويل الضمني في بناء النصوص فقط، وحوّل بصراحة بـ Number() في أي عملية حسابية.
متى لا تشغّل بالك بالموضوع ده
لو بتستخدم TypeScript وبتفعّل فحص الأنواع، الكومبايلر هيمسكلك خلط النص بالرقم قبل ما يشتغل أصلًا، فالخطر بيقل جدًا. وكمان لو بتعمل validation وتحويل لكل المدخلات عند حدود النظام (وقت ما تيجي من الفورم أو الـ API مباشرة)، فبقية الكود بيشتغل بأرقام نضيفة ومش هتقابل المشكلة. الموضوع ده بيوجع أكتر في JavaScript عادي بدون أنواع، ومع بيانات جاية من inputs أو من JSON.
الخطوة التالية
دلوقتي افتح أقرب مكان في كودك بتجمع فيه قيمة جاية من input أو req.body. لو لقيتها بتتجمع بـ + من غير Number() قبلها، ده مكان فيه bug مستني. لفّها بـ Number() أو parseInt، وجرّب تبعت "100" ثلاث مرات وتشوف المجموع طلع 300 ولا "0100100100".
المصادر
- MDN Web Docs — Type coercion:
developer.mozilla.org/en-US/docs/Glossary/Type_coercion - MDN Web Docs — Equality comparisons and sameness (== مقابل ===):
developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness - ECMAScript Language Specification (ECMA-262) — Abstract Operations: ToPrimitive، ToNumber، ToString:
tc39.es/ecma262 - MDN Web Docs — Addition (+) operator:
developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Addition