الـ Symbol في جافاسكربت: قيمة فريدة مضمونة
النوع Symbol هو أحد الأنواع الأولية (primitive types) في جافاسكربت، إلى جانب string وnumber وboolean وnull وundefined وbigint. وما يميّزه ببساطة: كل رمز تُنشئه يختلف عن أي رمز آخر، إلى الأبد.
رمزان، أنشئ كلٌّ منهما دون أي وسائط، ومع ذلك فهما غير متساويين. هذه ليست صدفة، بل هذا هو جوهر Symbol في جافاسكربت تمامًا. فلا يمكنك تزوير رمز، ولا إعادة إنشائه بالمصادفة، ولا التصادم مع رمزٍ أنشأه شخصٌ آخر.
ويمكنك إرفاق وصف بالرمز لتسهيل تتبع الأخطاء، دون أن يؤثر ذلك على هويته:
الوصف نفسه، لكن الرموز مختلفة. فالوصف مجرد تسمية يستعين بها البشر عند قراءة السجلات.
لماذا وُجدت الرموز؟ مفاتيح بلا تعارض
السبب الرئيسي لوجود النوع Symbol في جافاسكربت هو تمكينك من إضافة خصائص إلى كائن دون القلق من تصادم مفتاحك مع مفتاح شخص آخر. فمفاتيح النصوص تتنافس جميعها داخل فضاء تسميات مسطّح؛ فلو قررت مكتبتان تخزين بياناتهما الوصفية في obj.meta، ستدوس إحداهما على الأخرى. أما مفاتيح Symbol فلا تتنافس، لأن لا أحد غيرك يملك الرمز الذي أنشأته.
الأقواس المربعة في [ID]: 42 هي صيغة الخاصية المحسوبة (computed property) — ومعناها: "استخدم قيمة ID مفتاحًا." لذلك لا يستطيع قراءة هذه الخاصية أو الكتابة فوقها إلا الكود الذي يحمل نفس رمز ID. أي وحدة أخرى تستعمل النص "id" أو رمزًا خاصًا بها من نوع Symbol("id") فهي في الحقيقة تتعامل مع خانة مختلفة تمامًا.
مفاتيح Symbol مخفية (في الغالب)
الخصائص التي مفتاحها من نوع Symbol لا تظهر في for...in ولا في Object.keys ولا في JSON.stringify. هي ليست سرية بالمعنى الأمني — فالكود المُصرّ على الوصول إليها يستطيع ذلك — لكنها تبقى بعيدة عن الطريق أثناء التكرار الاعتيادي على الكائن.
دالة Object.keys و JSON.stringify تتجاهلان مفاتيح الرموز تمامًا. أما إذا أردت الوصول إلى خصائص الرمز بشكل صريح، فالدالتان Object.getOwnPropertySymbols و Reflect.ownKeys تكشفان عنها. وهذا بالضبط السلوك المطلوب عند التعامل مع البيانات الوصفية (metadata) — تظهر عند البحث عنها، وتبقى خفيّة عن أي شيفرة تمرّ على المفاتيح النصية فقط.
مشاركة الرموز عبر Symbol.for
استدعاء Symbol() ينشئ لك رمزًا محليًا فريدًا لمرّة واحدة. لكن في بعض الأحيان تحتاج إلى العكس تمامًا — رمز يكون هو نفسه في كل مكان من البرنامج، وعبر الوحدات المختلفة، بحيث تتّفق أجزاء الشيفرة المتفرّقة على مفتاح موحّد. وهنا يأتي دور Symbol.for.
Symbol.for(key) يبحث في سجلّ عالمي (registry): فإذا كان هناك رمز مُسجَّل بهذا المفتاح فستحصل عليه كما هو، وإلا فسيُنشَأ رمز جديد ويُخزَّن. أما Symbol.keyFor فيعمل في الاتجاه المعاكس — يُعيد لك المفتاح الذي أُنشئ به الرمز المُسجَّل (أو undefined للرموز غير المُسجَّلة).
في معظم الحالات ستكتفي بـ Symbol() العادي. لا تلجأ إلى Symbol.for إلا حين يكون الرمز بحاجة فعلية للمشاركة بين وحدات (modules) مختلفة.
الرموز المعروفة في جافاسكربت: خطّافات داخل اللغة نفسها
هنا تظهر القيمة الحقيقية للرموز. توفّر جافاسكربت مجموعة من الرموز الجاهزة مسبقًا — وتُعرف بـ well-known symbols أو الرموز المعروفة — تبحث عنها اللغة نفسها في كائناتك. يكفي أن تُعرِّف دالة عند أحد هذه المفاتيح حتى يرتبط كائنك مباشرةً بميزة مدمجة في اللغة.
أكثر هذه الرموز استخدامًا هو Symbol.iterator. فأيّ كائن يملك دالة عند هذا المفتاح يصبح قابلًا للتكرار (iterable): يعمل مع for...of والـ spread والتفكيك (destructuring) و Array.from.
range ليس مصفوفة. إنه مجرد كائن عادي يحتوي على دالة واحدة فقط مفتاحها رمز خاص. لكن بما أن هذه الدالة تُعيد مُكرِّرًا (iterator)، فإن for...of وعامل النشر (spread) يعملان معه تمامًا كما يعملان مع المصفوفات والسلاسل النصية و Map. هذا هو جوهر الاتفاق: نفِّذ Symbol.iterator، وستتعامل معك اللغة ككائن قابل للتكرار.
رموز معروفة أخرى تستحق المعرفة
هناك عدد من الرموز المعروفة (well known symbols) الأخرى، وكل واحد منها يمثل نقطة ربط مع جزء مختلف من اللغة:
Symbol.iterator— يجعل الكائن قابلًا للتكرار.Symbol.asyncIterator— نفس الفكرة، لكن معfor await...of.Symbol.toPrimitive— يتحكم في كيفية تحويل الكائن إلى قيمة بدائية (رقم، نص، أو الوضع الافتراضي) عند الإكراه (coercion).Symbol.hasInstance— يخصّص ما يُرجعهinstanceofعند استخدامه مع الصنف (class) الخاص بك.Symbol.toStringTag— يحدّد الوسم الذي يظهر عند استدعاءObject.prototype.toString.call(obj).
لست مضطرًا لحفظها عن ظهر قلب؛ يكفي أن تعرف بوجودها. فعندما تواجه موقفًا تريد فيه أن يتصرف كائنك مثل نوع مدمج في اللغة، فغالبًا ستجد رمزًا معروفًا (well-known symbol) جاهزًا لهذا الغرض.
الفرق بين Symbol و string
من أكثر النقاط التي يتعثر فيها المبتدئون: الرموز لا تتحول تلقائيًا إلى نصوص. فمحاولة دمج رمز داخل سلسلة نصية ترمي خطأً، وكذلك تمريره إلى أي واجهة (API) تتوقع نصًا:
القوالب الحرفية (Template literals) ترمي نفس الخطأ TypeError. استخدم String(symbol) أو symbol.toString() عندما تريد فعلاً الحصول على نص. هذه الحدّة في التصميم مقصودة — فاللغة تحميك من التعامل عن طريق الخطأ مع قيمة هوية فريدة وكأنها مجرّد بيانات نصية عادية.
متى تستخدم Symbol في جافاسكربت؟ ومتى تتجنّبها؟
استخدم Symbol في الحالات التالية:
- عندما تُرفق بيانات وصفية (metadata) إلى كائنات لا تملكها، وتحتاج مفتاحاً مضموناً عدم تصادمه مع أي مفتاح آخر.
- عندما تصمّم بروتوكولاً ما — بمعنى: "أي كائن يريد العمل مع مكتبتي عليه أن يوفّر ميثود عند هذا الـ Symbol".
- عندما تريد خاصية لا تظهر في
JSON.stringifyولا فيfor...in. - عندما تُنفّذ
Symbol.iteratorأو أي رمز من الرموز المعروفة (well known symbols).
وتجنّب استخدامها في الحالات التالية:
- إذا كنت تحتاج فقط مفتاحاً لكائن ولا يوجد خطر تصادم. سلسلة نصية عادية تكون أبسط، وتظهر في سجلات الـ logs كما هي.
- إذا كنت تريد حقولاً "خاصة" فعلياً. فالخصائص ذات مفاتيح الـ Symbol ليست خاصة بالمعنى الحقيقي — إذ يكشفها
Object.getOwnPropertySymbols. للخصوصية الحقيقية استخدم حقول الأصناف الخاصة#private. - إذا كنت تخزّن بيانات يجب أن تبقى بعد
JSON.stringify، لأنها ببساطة لن تبقى.
معظم شيفرات جافاسكربت تمضي على خير دون أن تكتب Symbol(...) مباشرةً ولا مرة واحدة. لكن في اللحظة التي تريد فيها جعل كائنك الخاص قابلاً للتكرار مع for...of أو أن يتصرّف بشكل طبيعي في سياقات التحويل (coercion)، تصبح الرموز هي الباب الذي يدخلك إلى هذه الآلية.
ما بعد ذلك: تصريح الدوال
تعتمد الرموز والـ iterators والـ generators اعتماداً كبيراً على الدوال — فالميثودات المخزَّنة عند Symbol.iterator، والمصانع (factories) التي تُرجع iterators، والـ generators التي يخفي شكلها function* معظم التفاصيل المتكررة، كلها دوال في جوهرها. الفصل القادم مخصّص للدوال، ونبدأ فيه بالطرق المختلفة للتصريح عنها والفروقات بين كل شكل وآخر.
الأسئلة الشائعة
ما هو Symbol في جافاسكربت؟
الـ Symbol نوع أولي (primitive) قيمه مضمونة الفرادة. تنشئه عبر Symbol() أو Symbol('description')، ولن يتساوى أي استدعاءين مهما تكرّرا. يُستخدم غالبًا كمفتاح لخاصية داخل كائن دون خوف من التصادم مع مفاتيح أخرى، وكذلك للارتباط بميزات اللغة نفسها من خلال الرموز المعروفة مثل Symbol.iterator.
متى أستخدم Symbol بدلًا من مفتاح نصي (string)؟
استخدم Symbol حين تريد إضافة خاصية إلى كائن بدون خطر التعارض مع كود آخر — مثل مكتبة تضيف بيانات وصفية (metadata)، أو إطار عمل يُعلِّم الكائنات، أو عند تعريف مفتاح لا تريده جزءًا من الـ API العام. أما في الحالات الاعتيادية التي تهمّ فيها سهولة القراءة ولا يوجد قلق من التصادم، فالنصوص تبقى الخيار الأنسب.
ما فائدة Symbol.iterator؟
Symbol.iterator رمز معروف (well-known symbol) يُخبر for...of وعامل النشر (spread) والـ destructuring كيف يكرّرون الكائن. إذا عرّفت ميثود عند المفتاح Symbol.iterator تُعيد iterator، يصبح كائنك قابلًا للتكرار تلقائيًا. هكذا تعمل في الخلفية المصفوفات والسلاسل النصية و Map و Set.
ما الفرق بين Symbol() و Symbol.for()؟
Symbol('x') ينشئ رمزًا جديدًا تمامًا في كل مرة — استدعاءان بنفس الوصف يُنتجان رمزين مختلفين. أما Symbol.for('x') فيبحث في سجل عام (global registry) ويُعيد نفس الرمز للمفتاح ذاته في جميع أنحاء البرنامج. استعمل Symbol.for عندما تحتاج مشاركة الرمز بين وحدات (modules) أو realms مختلفة، واستعمل Symbol() عندما تكفيك الفرادة المحلية.