الغوص العميق في الإغلاقات
الإغلاقات هي واحدة من أقوى وأساسيات المفاهيم في JavaScript. فهم الإغلاقات سيفتح أنماط برمجة متقدمة ويساعدك على كتابة كود أكثر أناقة وسهولة في الصيانة. في هذا الدرس، سنستكشف ما هي الإغلاقات، وكيف تعمل، وكيفية استخدامها بفعالية.
ما هي الإغلاقات؟
الإغلاق هو دالة لها حق الوصول إلى المتغيرات في نطاقها المعجمي الخارجي (المحيط)، حتى بعد أن تكون الدالة الخارجية قد عادت. بعبارة أبسط، يمنحك الإغلاق حق الوصول إلى نطاق الدالة الخارجية من دالة داخلية.
المفهوم الأساسي: يتم إنشاء الإغلاقات في كل مرة يتم فيها إنشاء دالة، في وقت إنشاء الدالة. إنها "تتذكر" البيئة التي تم إنشاؤها فيها.
مثال أساسي على الإغلاق
لنبدأ بمثال بسيط لفهم المفهوم:
function outerFunction() {
const outerVariable = "أنا من النطاق الخارجي";
function innerFunction() {
console.log(outerVariable); // يمكن الوصول إلى outerVariable
}
return innerFunction;
}
const myClosure = outerFunction();
myClosure(); // الإخراج: أنا من النطاق الخارجي
على الرغم من أن outerFunction قد انتهت من التنفيذ، لا يزال innerFunction يملك حق الوصول إلى outerVariable. هذا هو الإغلاق!
النطاق المعجمي والإغلاق
الإغلاقات مرتبطة ارتباطاً وثيقاً بالنطاق المعجمي - فكرة أن الدوال يتم تنفيذها باستخدام نطاق المتغير الذي كان ساري المفعول عندما تم تعريفها، وليس عندما يتم استدعائها.
function createCounter() {
let count = 0; // متغير خاص
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
getCount: function() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
console.log(counter.getCount()); // 1
// لا يمكن الوصول إلى count مباشرة
console.log(counter.count); // undefined
نصيحة: المتغير count خاص ولا يمكن الوصول إليه إلا من خلال الطرق المُعادة. هذا هو التغليف في JavaScript!
المتغيرات الخاصة مع الإغلاقات
تمكّننا الإغلاقات من إنشاء متغيرات خاصة لا يمكن الوصول إليها من خارج الدالة:
function createBankAccount(initialBalance) {
let balance = initialBalance; // متغير خاص
return {
deposit: function(amount) {
if (amount > 0) {
balance += amount;
return `تم الإيداع: $${amount}. الرصيد الجديد: $${balance}`;
}
return "مبلغ غير صالح";
},
withdraw: function(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return `تم السحب: $${amount}. الرصيد الجديد: $${balance}`;
}
return "مبلغ غير صالح أو رصيد غير كافٍ";
},
getBalance: function() {
return `الرصيد الحالي: $${balance}`;
}
};
}
const myAccount = createBankAccount(1000);
console.log(myAccount.deposit(500)); // تم الإيداع: $500. الرصيد الجديد: $1500
console.log(myAccount.withdraw(200)); // تم السحب: $200. الرصيد الجديد: $1300
console.log(myAccount.getBalance()); // الرصيد الحالي: $1300
// لا يمكن الوصول مباشرة إلى balance
console.log(myAccount.balance); // undefined
نمط الوحدة مع الإغلاقات
الإغلاقات هي أساس نمط الوحدة، الذي يسمح لك بإنشاء وحدات مع أعضاء خاصة وعامة:
const calculator = (function() {
// متغيرات ودوال خاصة
let history = [];
function addToHistory(operation) {
history.push(operation);
}
// واجهة عامة
return {
add: function(a, b) {
const result = a + b;
addToHistory(`${a} + ${b} = ${result}`);
return result;
},
subtract: function(a, b) {
const result = a - b;
addToHistory(`${a} - ${b} = ${result}`);
return result;
},
getHistory: function() {
return history.slice(); // إرجاع نسخة من history
},
clearHistory: function() {
history = [];
}
};
})();
console.log(calculator.add(5, 3)); // 8
console.log(calculator.subtract(10, 4)); // 6
console.log(calculator.getHistory()); // ["5 + 3 = 8", "10 - 4 = 6"]
معالجات الأحداث والإغلاقات
الإغلاقات مفيدة للغاية عند العمل مع معالجات الأحداث:
function setupButton(buttonId, message) {
const button = document.getElementById(buttonId);
let clickCount = 0;
button.addEventListener('click', function() {
clickCount++;
console.log(`${message} - تم النقر ${clickCount} مرات`);
});
}
setupButton('btn1', 'الزر 1');
setupButton('btn2', 'الزر 2');
// كل زر يحتفظ بـ clickCount الخاص به عبر الإغلاق
الإغلاقات في الحلقات
مشكلة شائعة مع الإغلاقات تحدث في الحلقات. إليك المشكلة والحل:
// ❌ المشكلة: كل الدوال تشير إلى نفس i
for (var i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i); // تطبع 4، 4، 4
}, i * 1000);
}
// ✅ الحل 1: استخدام let (نطاق الكتلة)
for (let i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i); // تطبع 1، 2، 3
}, i * 1000);
}
// ✅ الحل 2: استخدام IIFE لإنشاء إغلاق
for (var i = 1; i <= 3; i++) {
(function(num) {
setTimeout(function() {
console.log(num); // تطبع 1، 2، 3
}, num * 1000);
})(i);
}
تحذير: عند استخدام var في الحلقات مع العمليات غير المتزامنة، تذكر أن var لها نطاق دالة، وليس نطاق كتلة. استخدم let أو أنشئ إغلاقاً باستخدام IIFE.
نمط مصنع الدوال
تتيح لك الإغلاقات إنشاء مصانع دوال تولد دوال مخصصة:
function createMultiplier(multiplier) {
return function(number) {
return number * multiplier;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
const quadruple = createMultiplier(4);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(quadruple(5)); // 20
// مثال أكثر عملية
function createGreeting(greeting) {
return function(name) {
return `${greeting}، ${name}!`;
};
}
const sayHello = createGreeting("مرحباً");
const sayHi = createGreeting("أهلاً");
const sayWelcome = createGreeting("أهلاً وسهلاً");
console.log(sayHello("أحمد")); // مرحباً، أحمد!
console.log(sayHi("سارة")); // أهلاً، سارة!
console.log(sayWelcome("محمد")); // أهلاً وسهلاً، محمد!
اعتبارات الذاكرة
تحتفظ الإغلاقات بمراجع لنطاقها الخارجي، مما قد يؤثر على الذاكرة:
function createHeavyObject() {
const largeData = new Array(1000000).fill("data");
return {
// ❌ هذا يحتفظ بـ largeData في الذاكرة
getData: function() {
return largeData;
}
};
}
// ✅ أفضل: احتفظ فقط بما تحتاجه
function createOptimizedObject() {
const largeData = new Array(1000000).fill("data");
const summary = largeData.length;
return {
getSummary: function() {
return summary; // يحتفظ فقط بالرقم، وليس المصفوفة
}
};
}
أفضل ممارسة: كن على دراية بالمتغيرات التي تغلقها. إذا كنت لا تحتاج إلى الكائن أو المصفوفة بأكملها، فاستخرج فقط ما تحتاجه لتجنب استخدام الذاكرة غير الضروري.
أنماط الإغلاق الشائعة
إليك بعض الأنماط العملية باستخدام الإغلاقات:
// 1. التخزين المؤقت
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (key in cache) {
console.log('إرجاع النتيجة المخزنة مؤقتاً');
return cache[key];
}
const result = fn(...args);
cache[key] = result;
return result;
};
}
const expensiveCalculation = memoize((n) => {
console.log('جاري الحساب...');
return n * n;
});
console.log(expensiveCalculation(5)); // جاري الحساب... 25
console.log(expensiveCalculation(5)); // إرجاع النتيجة المخزنة مؤقتاً 25
// 2. دالة لمرة واحدة
function once(fn) {
let called = false;
let result;
return function(...args) {
if (!called) {
called = true;
result = fn(...args);
}
return result;
};
}
const initialize = once(() => {
console.log('جاري التهيئة...');
return "تم التهيئة";
});
console.log(initialize()); // جاري التهيئة... تم التهيئة
console.log(initialize()); // تم التهيئة (لا يتم تشغيلها مرة أخرى)
// 3. التخميد
function debounce(fn, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
}
const debouncedSearch = debounce((query) => {
console.log(`البحث عن: ${query}`);
}, 500);
// يبحث فقط بعد 500 مللي ثانية من عدم الكتابة
تمرين تطبيقي:
التحدي: أنشئ دالة createTimer() تُرجع كائناً بطرق لبدء وإيقاف والحصول على الوقت المنقضي. يجب أن يكون الوقت المنقضي خاصاً ويمكن الوصول إليه فقط من خلال الطرق.
الحل:
function createTimer() {
let startTime = null;
let elapsedTime = 0;
let isRunning = false;
return {
start: function() {
if (!isRunning) {
startTime = Date.now() - elapsedTime;
isRunning = true;
return "تم بدء المؤقت";
}
return "المؤقت يعمل بالفعل";
},
stop: function() {
if (isRunning) {
elapsedTime = Date.now() - startTime;
isRunning = false;
return "تم إيقاف المؤقت";
}
return "المؤقت لا يعمل";
},
getTime: function() {
if (isRunning) {
return Date.now() - startTime;
}
return elapsedTime;
},
reset: function() {
startTime = null;
elapsedTime = 0;
isRunning = false;
return "تم إعادة تعيين المؤقت";
}
};
}
const timer = createTimer();
console.log(timer.start()); // تم بدء المؤقت
setTimeout(() => {
console.log(timer.getTime()); // ~2000ms
console.log(timer.stop()); // تم إيقاف المؤقت
}, 2000);
الملخص
في هذا الدرس، تعلمت:
- الإغلاقات هي دوال تتذكر نطاقها المعجمي
- تمكّن الإغلاقات من المتغيرات الخاصة وتغليف البيانات
- يستخدم نمط الوحدة الإغلاقات لإنشاء أعضاء خاصة وعامة
- الإغلاقات ضرورية لمعالجات الأحداث والمستدعيات
- المشاكل الشائعة مع الإغلاقات في الحلقات وكيفية تجنبها
- مصانع الدوال تنشئ دوال مخصصة باستخدام الإغلاقات
- اعتبارات الذاكرة عند العمل مع الإغلاقات
- أنماط الإغلاق العملية: التخزين المؤقت، مرة واحدة، التخميد
التالي: في الدرس التالي، سنستكشف IIFE (تعبيرات الدوال المستدعاة فوراً) ونمط الوحدة بعمق!