الأعداد العشوائية في JavaScript: من الفوضى إلى النظام
\nتخيل: أنت تكتب لعبة تحتاج فيها إلى اختيار صندوق كنز بشكل عشوائي من 1 إلى 100. أو ربما تقوم بإنشاء اختبار A/B يختار خيارًا عشوائيًا للمستخدم. قد يبدو الأمر بسيطًا، أليس كذلك؟ لكن الشيطان، كما هو الحال دائمًا، يكمن في التفاصيل. مسألة توليد الأعداد الصحيحة العشوائية في JavaScript في نطاق محدد هي واحدة من أكثر الأسئلة شيوعًا على Stack Overflow، وليس من دون سبب. حتى المطورون ذوو الخبرة يتعثرون أحيانًا في مخاطر Math.random().
في هذه المقالة، سنقوم بتحليل ليس فقط الطرق الكلاسيكية، ولكن أيضًا أفضل الممارسات لعام 2025: كيفية جعل الكود ليس فقط فعالاً، بل أيضًا آمنًا، وقابلاً للتنبؤ (بالمعنى الجيد)، وفعالاً من حيث الأداء. هيا بنا!
\n\nكلاسيكيات النوع: Math.random() والمخاطر الخفية
\nلنبدأ بالأساسيات. الدالة المضمنة Math.random() تُرجع رقمًا عشريًا شبه عشوائي في النطاق [0, 1). النقطة الأساسية: يتم تضمين 0، ولكن لا يتم تضمين 1. هذا توزيع "غير عادل" إذا كنت ترغب في الحصول على أعداد صحيحة.
إليك الطريقة الأكثر شيوعًا (والأكثر خطأ) التي توجد غالبًا في الكتب الدراسية القديمة:
\n// مثال سيء: انحياز في الاحتمالية\nfunction getRandomBad(min, max) {\n return Math.floor(Math.random() * (max - min + 1)) + min;\n}\n\nللوهلة الأولى، يبدو كل شيء صحيحًا. لكن دعنا نتعمق أكثر. Math.floor() يُقرّب إلى الأسفل. إذا قام Math.random() بتوليد رقم قريب جدًا من 1 (ولكن ليس مساويًا له)، فإن الضرب في (max - min + 1) سيعطي قيمة أقل من max + 1، وسيقوم Math.floor() بقطعها. ومع ذلك، نظرًا للدقة المحدودة للأعداد العشرية (IEEE 754)، تظهر بعض القيم بشكل أقل من غيرها. بالنسبة للنطاق 0-1، هذا غير ملحوظ، ولكن بالنسبة للنطاقات الكبيرة (على سبيل المثال، من 1 إلى 1,000,000)، يصبح الانحياز ذا دلالة إحصائية.
الخلاصة: هذه الصيغة تعمل مع النطاقات الصغيرة، لكنها ليست "نقية تشفيريًا" وقد تؤدي إلى انحراف في التوزيع.
\n\nالصيغة الصحيحة من MDN
\nتوصي شبكة مطوري Mozilla (MDN) بنهج أكثر دقة يقلل من أخطاء التقريب:
\n// طريقة جيدة وعالمية\nfunction getRandomIntInclusive(min, max) {\n const minCeiled = Math.ceil(min);\n const maxFloored = Math.floor(max);\n return Math.floor(Math.random() * (maxFloored - minCeiled + 1) + minCeiled);\n}\n\nالفرق هو أننا نقوم بتحويل الحدود بشكل صريح إلى أعداد صحيحة باستخدام Math.ceil() و Math.floor(). هذا يضمن معالجة min و max بشكل صحيح، حتى إذا قمت بتمرير رقم عشري عن طريق الخطأ. استخدم هذه الطريقة كمعيار أساسي في عام 2025.
البدائل: عندما لا يكون Math.random() كافيًا
\nإن Math.random() الكلاسيكي هو مولد شبه عشوائي (PRNG). إنه حتمي: إذا كنت تعرف البذرة الأولية (seed)، يمكنك توقع التسلسل بأكمله. بالنسبة لمعظم تطبيقات الويب (الرسوم المتحركة، المحتوى العشوائي)، هذا كافٍ. ولكن هناك سيناريوهات تحتاج فيها إلى عشوائية حقيقية:
- \n
- الأمان: توليد الرموز المميزة (tokens)، كلمات المرور، رموز CSRF. \n
- التشفير: تشفير البيانات على جهة العميل. \n
- ألعاب القمار: توزيع عادل للمكاسب. \n
في هذه الحالات، يأتي crypto.getRandomValues() للإنقاذ - وهو مولد قوي تشفيريًا (CSPRNG). يستخدم مصادر إنتروبيا النظام ويكون أقل قابلية للتنبؤ بكثير.
توليد الأعداد الصحيحة باستخدام crypto.getRandomValues()
\nإليك كيف يبدو التنفيذ الحديث لعام 2025:
\n// توليد آمن لعدد صحيح عشوائي في النطاق [min, max]\nfunction getSecureRandomInt(min, max) {\n const range = max - min + 1;\n // القيمة القصوى التي هي من مضاعفات range (لتجنب الانحياز