Menu
العربية
جرّب في Playground

الحقول الخاصة في JavaScript: صياغة # للخصوصية

كيف تجعل البادئة # حقول ومُعامِلات الكلاس خاصة فعلياً في JavaScript، والفرق بينها وبين اصطلاح الشرطة السفلية _ الذي لا يكفي لتحقيق خصوصية حقيقية.

مشكلة استخدام الشرطة السفلية _

لفترة طويلة، لم تكن لغة JavaScript تدعم الحقول الخاصة بشكل حقيقي. فكان المطورون يلجؤون إلى حيلة بسيطة: يضعون شرطة سفلية _ قبل اسم الخاصية، ويأملون أن يحترم الجميع هذا الاتفاق الضمني ولا يعبث بها:

index.js
Output
Click Run to see the output here.

_count يبدو خاصًا، لكنه في الحقيقة ليس كذلك. أي جهة مستدعية قادرة على قراءته أو الكتابة فيه أو حتى حذفه. الشرطة السفلية مجرد لافتة مؤدبة معلّقة على الباب، أما الباب نفسه فمفتوح على مصراعيه.

جافا سكريبت الحديثة عالجت هذه المشكلة عبر الحقول الخاصة الحقيقية، التي تُعلَّم بالرمز #.

الرمز # هو ما يجعل الحقل خاصًا فعلًا

ضع الرمز # قبل اسم الحقل عند التصريح وعند كل موضع وصول إليه:

index.js
Output
Click Run to see the output here.

من داخل الكلاس، يعمل this.#count بشكل طبيعي. أما من خارجه، فهو ببساطة غير موجود:

index.js
Output
Click Run to see the output here.

علامة # هي جزء أصيل من اسم الحقل نفسه، وليست كلمة مفتاحية معدِّلة مثل private في بقية اللغات — إنها رمز (sigil) يستخدمه المحلل النحوي (parser) للوصول إلى خانة منفصلة ومحمية داخل الكائن. ولهذا السبب يظهر الخطأ في مرحلة التحليل (parse time)، أي قبل أن يبدأ تنفيذ الكود أصلاً.

الدوال والـ getters الخاصة في javascript

الحقول ليست وحدها التي يمكن جعلها خاصة؛ فالدوال (methods) والـ getters والـ setters تقبل جميعها البادئة #:

index.js
Output
Click Run to see the output here.

assertPositive# هو مجرد دالة مساعدة داخلية، وليس جزءًا من الواجهة العامة للكلاس. عندما تجعله خاصًا بشكل حقيقي، فإنك تضمن ألا يستدعيه أحد من الخارج عن طريق الخطأ، كما أن أحدًا لن يعتمد عليه في كوده، وهذا يمنحك حرية كاملة في إعادة تسميته أو حذفه لاحقًا دون أن تكسر شيئًا.

الأعضاء الساكنة الخاصة (private static)

يمكن أيضًا جعل الأعضاء الساكنة (static) خاصة، وذلك بإضافة # قبل الاسم بنفس الطريقة:

index.js
Output
Click Run to see the output here.

الحقول الستاتيكية الخاصة مرتبطة بالكلاس نفسه وليس بنُسخه (instances)، وتنفع مثلاً لعدّادات داخلية، أو كاش، أو إعدادات لا تريد أن تتسرب خارج الكلاس.

الكلاسات الفرعية لا ترى الحقول الخاصة

هذه النقطة تُربك القادمين من Java أو C#. الحقول الخاصة في javascript خاصة بالكلاس نفسه (class-private) لا بالنسخة (instance-private)، ومعنى ذلك أن أي كلاس فرعي لا يستطيع الوصول إلى الحقول الخاصة للكلاس الأب:

index.js
Output
Click Run to see the output here.

لا يوجد protected في javascript. لو احتاج الكلاس الابن الوصول إلى البيانات، فلا بد أن يوفّر الكلاس الأب دالة أو getter أو — في حالات أقل شيوعًا — حقلًا غير خاص. هذا التصميم مقصود: الخاص يعني خاصًا فعلًا، والوراثة لا تفتح أي ثغرة فيه.

التحقق من وجود حقل خاص باستخدام in

أحيانًا تريد التأكد من أن الكائن ينتمي فعلًا إلى كلاسك — وهو ما يُعرف بـ brand check. عامل in يعمل مع أسماء الحقول الخاصة داخل الكلاس:

index.js
Output
Click Run to see the output here.

بما أنّ Wallet وحده القادر على إنشاء كائنات تحتوي على #balance، فإنّ الاختبار #balance in obj يُعدّ طريقة موثوقة للتأكّد من أنّ obj هو فعلاً نسخة أصلية من Wallet. هذا الأسلوب أسرع وأأمن من instanceof في بعض الحالات الطرفية، لأنّ الحقول الخاصة لا يمكن تزويرها من خارج الكلاس.

فخّ شائع: الكائنات العادية لا تملك هذه الحقول

الحقول الخاصة في javascript موجودة فقط على النسخ التي أنشأها كونستركتر الكلاس. فإذا حاولت استخدامها على كائن لم يُنشأ عبر new، فسيُرمى استثناء:

index.js
Output
Click Run to see the output here.

استدعاء الدالة بقيمة this لا تعود لنوع Point يُطلق خطأً وقت التشغيل. هذه بالضبط هي الآلية التي يقوم عليها الـ brand check المذكور سابقًا — فالحقول الخاصة مرتبطة بالكلاس المحدّد الذي صرّح بها، لا بأي كائن يصادف أن شكله مشابه.

متى تلجأ إلى #؟

اجعل الحقول الخاصة هي الخيار الافتراضي لأي حالة أو دالة مساعدة ليست جزءًا من الواجهة العامة للكلاس. والأسباب:

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

استخدم الخصائص العامة عندما يكون الشيء فعلًا جزءًا من الواجهة. واستخدم getter (مثل get name()) لو أردت وصول قراءة فقط إلى حقل خاص. وتخلَّ عن عادة استخدام الشرطة السفلية _ — فهي كانت مجرد حل التفافي لثغرة سدّتها اللغة اليوم.

index.js
Output
Click Run to see the output here.

celsius# هو مخزن البيانات المخفي، أما celsius وfahrenheit فمجرد واجهات للقراءة فقط. بهذا الشكل لا يستطيع المستخدم الخارجي العبث بالحالة الداخلية، ويبقى للكلاس حرية تغيير طريقة تخزين القيمة لاحقاً دون كسر أي شيء.

الخطوة التالية: الـ Prototypes

الكلاسات في JavaScript ليست سوى صياغة مبسّطة (syntactic sugar) فوق نظام الـ prototype — وهو النموذج الأقدم والأعمق الذي بُنيت عليه اللغة فعلياً. فهم الـ prototypes هو ما يوضّح لك سبب سلوك this بالطريقة التي تراها، وكيف يعمل التوريث حقاً، وماذا يفعل extends خلف الكواليس. هذا ما سنتناوله في الصفحة القادمة.

الأسئلة الشائعة

كيف نُعرِّف حقلاً خاصاً في JavaScript؟

تُضاف البادئة # قبل اسم الحقل، سواء عند التصريح به أو عند الوصول إليه في كل مرة. مثلاً: class Counter { #count = 0; increment() { this.#count++; } } يُنشئ حقلاً خاصاً فعلياً. لاحظ أن # جزء من الاسم نفسه وليست معامِلاً.

ما الفرق بين #field و _field في JavaScript؟

_field مجرد اصطلاح تسمية؛ الخاصية تبقى عامة ويستطيع أي كود قراءتها أو تعديلها. أما #field فاللغة نفسها تفرض خصوصيتها: أي كود خارج الكلاس لا يستطيع الوصول إليها أصلاً، ومحاولة ذلك تُطلق SyntaxError في مرحلة التحليل. استخدم # حين تحتاج خصوصية حقيقية وصارمة.

هل تستطيع الكلاسات المشتقة الوصول إلى الحقول الخاصة في الكلاس الأب؟

لا. نطاق الحقول الخاصة مقصور على الكلاس الذي صرّح بها، وحتى الكلاسات المشتقة لا تستطيع الوصول إليها. إن احتاج الكلاس المشتق إليها، فعلى الكلاس الأب أن يُتيحها عبر دالة أو getter. هذا السلوك أكثر صرامة من protected في لغات أخرى، وهو مقصود بهذا الشكل.

هل يمكن التحقق من وجود حقل خاص في كائن معيّن؟

نعم، باستخدام المعامل in داخل الكلاس: #field in obj يُعيد true إذا كان obj يحتوي ذلك الحقل الخاص. هذه طريقة مفيدة لما يُسمى brand check، أي التأكد من أن الكائن فعلاً نسخة من كلاسك قبل استدعاء دوال عليه.

تعلّم البرمجة مع Coddy

ابدأ الآن