الخطأRangeError: تم تجاوز الحد الأقصى لحجم مكدس الاستدعاءات (وتسمى أيضًا “تجاوز سعة المكدس” في البيئات غير المستعرضة) تعني وظيفة تسمى نفسها (بشكل مباشر أو غير مباشر) عدة مرات حتى نفدت مساحة مكدس الاستدعاءات في JavaScript. هنا كل الأسباب والإصلاح.
📋 Table of Contents
- لماذا يحدث هذا
- السبب 1: معالج الأحداث العودية اللانهائية
- السبب 2: مراجع الكائنات الدائرية في JSON.stringify
- السبب 3: استنساخ الكائنات العميقة مع العودية
- السبب 4: استدعاء دالة عن طريق الخطأ بدلاً من الرجوع إليها
- إصلاح: تحويل العودية إلى التكرار (الترامبولين)
- تصحيح تجاوزات المكدس
- الأسئلة المتداولة
- الخلاصة
لماذا يحدث هذا
يضيف كل استدعاء دالة إطارًا إلى مكدس الاستدعاءات. تحتوي محركات JavaScript على حجم مكدس محدود (حوالي 10000-15000 إطار اعتمادًا على إصدار المتصفح/العقدة). عندما تفتقر الوظائف العودية إلى حالة أساسية مناسبة، أو عندما تستدعي وظيفتان بعضهما البعض إلى أجل غير مسمى، فإن المكدس يمتلئ ويطرح RangeError.
// Simplest example: missing base case
function factorial(n) {
return n * factorial(n - 1); // never stops — factorial(0) calls factorial(-1) etc.
}
factorial(5); // RangeError: Maximum call stack size exceeded
// Fix: add base case
function factorial(n) {
if (n <= 1) return 1; // base case stops recursion
return n * factorial(n - 1);
}
السبب 1: معالج الأحداث العودية اللانهائية
// 🐛 Bug: React onChange updates state, triggers re-render, fires onChange again
function SearchInput() {
const [value, setValue] = useState('');
return <input
value={value}
onChange={(e) => {
setValue(e.target.value);
// If something here triggers another state update that causes re-render...
}}
/>;
}
// 🐛 Bug: useEffect calling function that updates state without dependency guards
useEffect(() => {
setData(processData(data)); // updates data → triggers effect → updates data...
}, [data]);
// ✅ Fix: use functional update or conditional guard
useEffect(() => {
if (data.needsProcessing) {
setData(prev => processData(prev)); // functional update avoids dependency
}
}, [data.id]); // depend on stable identifier, not the object
السبب 2: مراجع الكائنات الدائرية في JSON.stringify
// 🐛 Bug: circular reference crashes JSON.stringify
const obj = { name: "test" };
obj.self = obj; // circular reference
JSON.stringify(obj); // TypeError: Converting circular structure to JSON
// ✅ Fix 1: Replace circular references
function stringifySafe(obj) {
const seen = new WeakSet();
return JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) return '[Circular]';
seen.add(value);
}
return value;
});
}
// ✅ Fix 2: Use structuredClone for deep copying (handles circular refs)
const safeCopy = structuredClone(obj); // built-in, available Node 17+ / modern browsers
السبب 3: استنساخ الكائنات العميقة مع العودية
// 🐛 Bug: naive deep clone fails on deeply nested objects
function deepClone(obj) {
if (typeof obj !== 'object') return obj;
return Object.fromEntries(
Object.entries(obj).map(([k, v]) => [k, deepClone(v)]) // crashes at ~10k depth
);
}
// ✅ Fix 1: Use structuredClone (built-in, iterative internally)
const clone = structuredClone(deeplyNestedObj);
// ✅ Fix 2: Convert recursion to iteration for very deep objects
function deepCloneIterative(root) {
const stack = [{ source: root, target: Array.isArray(root) ? [] : {} }];
const result = stack[0].target;
const map = new WeakMap([[root, result]]);
while (stack.length) {
const { source, target } = stack.pop();
for (const [key, value] of Object.entries(source)) {
if (typeof value === 'object' && value !== null) {
if (map.has(value)) {
target[key] = map.get(value);
continue;
}
const newTarget = Array.isArray(value) ? [] : {};
map.set(value, newTarget);
target[key] = newTarget;
stack.push({ source: value, target: newTarget });
} else {
target[key] = value;
}
}
}
return result;
}
السبب 4: استدعاء دالة عن طريق الخطأ بدلاً من الرجوع إليها
// 🐛 Bug: function immediately calls itself
class Timer {
start() {
this.intervalId = setInterval(this.tick(), 1000); // tick() CALLED, not referenced
// this.tick() returns undefined (or calls tick immediately, which schedules tick...)
}
tick() {
console.log('tick');
this.start(); // calls start() which calls tick() again immediately
}
}
// ✅ Fix: pass function reference, not call result
class Timer {
start() {
this.intervalId = setInterval(() => this.tick(), 1000); // reference, not call
}
tick() {
console.log('tick');
}
}
إصلاح: تحويل العودية إلى التكرار (الترامبولين)
// For recursive algorithms that process large inputs:
// 🐛 Recursive sum — crashes for large n
function sum(n) {
if (n === 0) return 0;
return n + sum(n - 1); // crashes for n > ~10,000
}
// ✅ Iterative solution
function sumIterative(n) {
let total = 0;
for (let i = n; i > 0; i--) {
total += i;
}
return total;
}
// ✅ Trampolining (for complex recursive algorithms that are hard to iterate directly)
function trampoline(fn) {
return function trampolined(...args) {
let result = fn(...args);
while (typeof result === 'function') {
result = result();
}
return result;
};
}
const sumTrampoline = trampoline(function sum(n, acc = 0) {
if (n === 0) return acc;
return () => sum(n - 1, acc + n); // returns a thunk instead of recursing
});
sumTrampoline(100000); // works fine
تصحيح تجاوزات المكدس
// The error stack trace shows the recursive call
// RangeError: Maximum call stack size exceeded
// at factorial (/app/math.js:2:10)
// at factorial (/app/math.js:2:10) ← same line repeated = infinite recursion
// at factorial (/app/math.js:2:10)
// ...
// Step 1: Look for the repeated line in the stack trace
// Step 2: Add console.log with a counter to see how deep it goes
let callCount = 0;
function myFunction(data) {
callCount++;
if (callCount > 1000) {
console.trace('Deep recursion detected at:', callCount);
throw new Error('Stack overflow prevention');
}
// ...
}
// Step 3: Check for circular dependencies
function detectCircular(obj, path = '', visited = new WeakSet()) {
if (typeof obj !== 'object' || obj === null) return;
if (visited.has(obj)) { console.log('Circular at:', path); return; }
visited.add(obj);
for (const [key, val] of Object.entries(obj)) {
detectCircular(val, `${path}.${key}`, visited);
}
}
الأسئلة المتداولة
س: ما هو الحد الأقصى لعمق مكدس الاستدعاءات في JavaScript؟
ج: ~10000-15000 إطار في Chrome/Node.js؛ ~5000-10000 في فايرفوكس؛ يختلف حسب إصدار المحرك وحجم الإطار. الأرقام الدقيقة تتغير بين الإصدارات.
س: هل يمكنني زيادة حجم مكدس الاستدعاءات؟
ج: في Node.js:node --stack-size=65536 app.js (الحجم بالكيلو بايت). في المتصفحات: غير ممكن. الحل الأفضل: إصلاح التكرار بدلاً من زيادة الحد.
س: ما الفرق بين تجاوز سعة المكدس وكومة الذاكرة المؤقتة؟
ج: تجاوز سعة المكدس (RangeError) = عدد كبير جدًا من إطارات استدعاء الوظائف. نفاد الذاكرة (خطأ فادح) = تم تخصيص عدد كبير جدًا من الكائنات. تجاوز سعة المكدس هو من التكرار العميق؛ أخطاء الكومة هي من تخصيص الكثير من البيانات.
س: كيف يمكنني استنساخ كائن بشكل آمن في عام 2026؟
ج: استخدمstructuredClone(obj) – مدمج في جميع المتصفحات الحديثة وNode.js 17+. فهو يتعامل مع المراجع الدائرية وكائنات التاريخ والخرائط والمجموعات وArrayBuffers بشكل صحيح. استخدم JSON.parse(JSON.stringify(obj)) فقط للكائنات البسيطة القابلة للتسلسل JSON بدون مراجع دائرية.
س: هل يتوفر تحسين الاتصال الخلفي في JavaScript؟
ج: نعم من الناحية النظرية (تتضمن مواصفات ES6 التكلفة الإجمالية للملكية)، لكن Safari فقط هو الذي ينفذها بالكامل في عام 2026. قام V8 (Chrome/Node.js) بإزالة التكلفة الإجمالية للملكية. لا تعتمد على التكلفة الإجمالية للملكية من أجل سلامة المكدس — استخدم الحلول التكرارية أو الترامبولين بدلاً من ذلك.
الخلاصة
تأتي الأخطاء التي تم تجاوز الحد الأقصى لحجم مكدس الاستدعاءات من التكرار اللانهائي أو العميق للغاية. يعتمد الإصلاح على السبب:أضف حالة أساسية لإيقاف العودية، وقم بتحويل العودية إلى تكرار للمدخلات الكبيرة، واستخدم StructuredClone للنسخ العميقة، وراقب مراجع الكائنات الدائرية. يُظهر تتبع المكدس دائمًا السطر المتكرر الذي يتسبب في التكرار اللانهائي – اقرأه أولاً وعادةً ما يكون الإصلاح واضحًا.
🔗 Share this article
✍️ Leave a Comment