٢.٥ الإغلاقات (Closures)¶
التمرين 1: لنُنشئ دالة الغسالة التي تُرجع ملابس نظيفة.
function washingMachine(){
var startCleaning = true;
var cleanClothes = "The clothes are clean now!";
return cleanClothes;
}
var isClean = washingMachine();
console.log(isClean);
النتيجة هي: The clothes are clean now!
كيف تعمل الشيفرة السابقة؟
- يُعرّف محرّك JavaScript الدالة ويهيّئها
washingMachine. - يُسنِد
undefinedعلىisClean. - يبدأ تنفيذ الشيفرة من الأعلى إلى الأسفل.
- يُسنِد القيمة المُرجعة من
washingMachineعلىisClean. - الدالة
washingMachineتُرجع "The clothes are clean now!". - قيمة
isCleanتظهر في وحدة التحكم.
التمرين 2: لنُنشئ دالة التنظيف الإضافي.
function extraCleaning(){
var startExtraCleaning = true;
var cleanExtraClothes = "The clothes are extra clean now!";
return cleanExtraClothes;
}
var isClean = extraCleaning();
console.log(isClean);
النتيجة هي: The clothes are extra clean now!
التمرين 3: لنضع دالة التنظيف الإضافي داخل دالة الغسالة.
function washingMachine(){
var startCleaning = true;
var cleanClothes = "The clothes are clean now!";
function extraCleaning(){
var startExtraCleaning = true;
var cleanExtraClothes = "The clothes are extra clean now!";
return cleanExtraClothes;
}
return extraCleaning;
}
var isClean = washingMachine();
console.log(isClean());
النتيجة هي: The clothes are extra clean now!
وضعنا ببساطة شيفرة التمرين 2 داخل التمرين 1 . وبدلًا من إرجاع قيمة cleanClothes من washingMachine، نُرجع extraCleaning ؟
isClean قيمته هي extraCleaning الدالة. لذلك نحتاج إلى استدعاء isClean للحصول على قيمة extraCleaning .
أصبحت الخطوات واضحة:
- نشغّل الغسالة
- تبدأ عملية الغسيل العادي
var startCleaning = true. - تصبح الملابس نظيفة
var cleanClothes = "The clothes are clean now!" - لكننا نريد تنظيفًا إضافيًا، لذا تكون القيمة المُرجعة من
washingMachineهو العامل المنطقيextraCleaning؟ - بعد الغسيل العادي تبدأ عملية أخرى نُسمّيها التنظيف الإضافي
- القيمة المُرجعة من
extraCleaningهي "The clothes are extra clean now!".
التمرين 4: ارجع إلى التمرين 3؛ هل يمكن الوصول إلى المتغير startCleaning خارج الدالة washingMachine؟ أثبت إجابتك بشيفرة.
function washingMachine(){
var startCleaning = true;
var cleanClothes = "The clothes are clean now!";
function extraCleaning(){
var startExtraCleaning = true;
var cleanExtraClothes = "The clothes are extra clean now!";
return cleanExtraClothes;
}
return extraCleaning;
}
var isClean = washingMachine();
console.log(isClean());
console.log(startCleaning);
النتيجة كما يلي:
The clothes are extra clean now!
Uncaught ReferenceError: startCleaning is not defined
هذه نتيجة متوقّعة. startCleaning متغير محلي، فلا يمكن الوصول إليه مباشرةً من النطاق العام.
التمرين 5: ارجع إلى التمرين 3؛ هل يمكن الوصول إلى المتغير startCleaning داخل الدالة extraCleaning؟ أثبت إجابتك بشيفرة.
function washingMachine(){
var startCleaning = true;
var cleanClothes = "The clothes are clean now!";
function extraCleaning(){
var startExtraCleaning = true;
var cleanExtraClothes = "The clothes are extra clean now!";
console.log(startCleaning);
return cleanExtraClothes;
}
return extraCleaning;
}
var isClean = washingMachine();
console.log(isClean());
النتيجة كما يلي:
true
The clothes are extra clean now!
نعم! يمكننا الوصول إلى startCleaning داخل الدالة extraCleaning؛ فالدالة الداخلية تستطيع الوصول إلى نطاق الدالة الخارجية.
التمرين 6: ارجع إلى التمرين 3؛ هل يمكن الوصول إلى المتغير cleanClothes داخل الدالة extraCleaning؟ أثبت إجابتك بشيفرة.
function washingMachine(){
var startCleaning = true;
var cleanClothes = "The clothes are clean now!";
function extraCleaning(){
var startExtraCleaning = true;
var cleanExtraClothes = "The clothes are extra clean now!";
console.log(cleanClothes);
return cleanExtraClothes;
}
return extraCleaning;
}
var isClean = washingMachine();
console.log(isClean());
النتيجة كما يلي:
The clothes are clean now!
The clothes are extra clean now!
نعم! يمكننا الوصول إلى cleanClothes داخل الدالة extraCleaning. بالفعل، الدالة extraCleaning تحتاج إلى الوصول إلى cleanClothes .
التمرين 7: ارجع إلى التمرين 3؛ هل يمكن الوصول إلى المتغير startExtraCleaning داخل الدالة washingMachine؟ أثبت إجابتك بشيفرة.
function washingMachine(){
var startCleaning = true;
var cleanClothes = "The clothes are clean now!";
function extraCleaning(){
var startExtraCleaning = true;
var cleanExtraClothes = "The clothes are extra clean now!";
return cleanExtraClothes;
}
console.log(startExtraCleaning);
return extraCleaning;
}
var isClean = washingMachine();
console.log(isClean());
النتيجة كما يلي:
Uncaught ReferenceError: startExtraCleaning is not defined
الدالة الخارجية لا تحتاج إلى الوصول إلى متغيرات الدالة الداخلية. تذكّر أن المتغير المحلي لا يتاح مباشرةً خارج نطاقه. startExtraCleaning غير متاح خارج extraCleaning ؟
التمرين 8: ارجع إلى التمرين 3؛ هل يمكن الوصول إلى المتغير cleanExtraClothes داخل الدالة washingMachine؟ أثبت إجابتك بشيفرة.
function washingMachine(){
var startCleaning = true;
var cleanClothes = "The clothes are clean now!";
function extraCleaning(){
var startExtraCleaning = true;
var cleanExtraClothes = "The clothes are extra clean now!";
return cleanExtraClothes;
}
console.log(cleanExtraClothes);
return extraCleaning;
}
var isClean = washingMachine();
console.log(isClean());
النتيجة كما يلي:
Uncaught ReferenceError: cleanExtraClothes is not defined
هذه نتيجة متوقّعة؛ لا يمكن الوصول مباشرةً إلى المتغير المحلي خارج نطاق دالته.
لماذا لا تتاح المتغيرات المحلية خارج دوالها؟
لأن قواعد النطاق تحدّد مواضع الوصول إليها. وهناك أيضًا مفهوم عمر المتغير.
عمر المتغير هو المدة التي تظل فيها بياناته مطلوبة أثناء تشغيل البرنامج.
عادةً يمكن تحرير بيانات الدالة بعد انتهائها إذا لم تعد هناك مراجع إليها. لكن الإغلاق قد يُبقي متغيراتها متاحة، كما سنرى. وتبقى بيانات الصفحة العامة عادةً طوال عمر الصفحة.
التمرين 9: أنشئ دالة تستقبل قيمة وتضيف إليها 5، ثم تُرجع دالة أخرى تقسم الناتج على 2.
function processValue(value){
var process1Result = value + 5;
function processValueAgain(){
var process2Result = process1Result / 2;
return process2Result;
}
return processValueAgain;
}
var finalResult = processValue(7);
console.log(finalResult());
النتيجة هي: 6
نعالج القيمة على مرحلتين: نضيف 5 أولًا، ثم نقسم الناتج على 2 ونُرجعه.
التمرين 10: ارجع إلى التمرين 9؛ متى ينتهي عمر المتغير process1Result ؟
process1Result يمكن الاستغناء عن المتغير بعد أن تُنهي الدالة processValue تنفيذها، بشرط ألا تحتفظ به دالة داخلية أو مرجع آخر.
التمرين 11: ارجع إلى التمرين 9؛ هل يمكنك الوصول إلى process1Result داخل processValueAgain ؟
function processValue(value){
var process1Result = value + 5;
function processValueAgain(){
console.log(process1Result);
var process2Result = process1Result / 2;
return process2Result;
}
return processValueAgain;
}
var finalResult = processValue(7);
console.log(finalResult());
النتيجة كما يلي:
12
6
كما ترى، الدالة processValue أنهت تنفيذها، لكن المتغير process1Result لم تُفقد قيمته، إذ إن الدالة processValueAgain ما زالت تحتفظ بإمكانية الوصول إلى process1Result. كيف؟
الإغلاقات (Closures)
ما أنشأناه في التمرين 3 و التمرين 9 هو إغلاق (Closure).
الإغلاق دالة تحتفظ بإمكانية الوصول إلى متغيرات نطاقها الخارجي حتى بعد انتهاء تنفيذه.
يمكن لدالة الإغلاق الوصول إلى:
- متغيراتها المحلية.
- متغيرات الدالة الخارجية.
- متغيرات النطاق العام.
التمرين 12: ارجع إلى التمرين 9؛ أضف console.dir(finalResult)وتحقّق من النتيجة.
function processValue(value){
var process1Result = value + 5;
function processValueAgain(){
var process2Result = process1Result / 2;
return process2Result;
}
return processValueAgain;
}
var finalResult = processValue(7);
console.log(finalResult());
console.dir(finalResult);
النتيجة كما يلي:
6
ƒ processValueAgain()
انقر السهم بجانب ƒ processValueAgain()، فسترى:
ƒ processValueAgain()
arguments: null
caller: null
length: 0
name: "processValueAgain"
prototype: {constructor: ƒ}
__proto__: ƒ ()
[[Scopes]]: Scopes[2]
انقر السهم بجانب [[Scopes]]: Scopes[2]، وسترى:
[[Scopes]]: Scopes[2]
0: Closure (processValue) {process1Result: 12}
1: Global {parent: Window, opener: null, top: Window, length: 0, frames: Window, …}
رائع! الدالة الداخلية processValueAgain تصل هنا إلى نطاق الإغلاق الذي يحتوي على process1Result وإلى متغيرات النطاق العام.
console.dir()
console.dir(something) تعرض قائمة بخصائص كائن مُعطى..
يمكن كتابة console.dir(function) لاستكشاف النطاقات المتاحة للدالة في أدوات المطوّر.
التمرين 13: أنشئ دالة تزيد متغيرًا بمقدار 1 عند كل استدعاء. سمِّ المتغير numواجعل قيمته الأولية صفرًا.
var num = 0;
function counter(){
num++;
return num;
}
console.log(counter());
console.log(counter());
console.log(counter());
النتيجة كما يلي:
1
2
3
التمرين 14: ارجع إلى التمرين 13؛ أضف num = 15 إلى الشيفرة، ثم أعد استدعاء الدالة counter بعد ذلك.
var num = 0;
function counter(){
num++;
return num;
}
console.log(counter());
console.log(counter());
console.log(counter());
num = 15;
console.log(counter());
console.log(counter());
النتيجة كما يلي:
1
2
3
16
17
نريد تحديث قيمة num فقط عند استدعاء الدالة. الآن num تغيّر خارج الدالة، مما أثّر في العدّاد.
التمرين 15: أعد تنفيذ التمرين 13؛ تأكّد من أن num يتحدّث داخل الدالة فقط، ولا يتأثر بمتغيرات خارجها.
function counter(){
var num = 0;
num++;
return num;
}
console.log(counter());
console.log(counter());
console.log(counter());
النتيجة كما يلي:
1
1
1
المتغير num يساوي 1 دائمًا؛ لأن كل استدعاء يُعيد num إلى الصفر.
التمرين 16: عالج المشكلة في التمرين 15 باستخدام الإغلاقات.
function counter(){
var num = 0;
function updateNum(){
num++;
return num;
}
return updateNum;
}
console.log(counter()());
console.log(counter()());
console.log(counter()());
النتيجة كما يلي:
1
1
1
ما زالت المشكلة موجودة؛ كل استدعاء يعيد num إلى الصفر. نحتاج إلى تنفيذ الدالة الخارجية counter مرة واحدة فقط، حتى num يُهيَّأ بالصفر مرة واحدة.
التمرين 17: عالج المشكلة في التمرين 16 باستخدام IIFE.
var counter = (function(){
var num = 0;
function updateNum(){
num++;
return num;
}
return updateNum;
})();
console.log(counter());
console.log(counter());
console.log(counter());
النتيجة كما يلي:
1
2
3
ملاحظة
في التمرين 17; num متغير محلي لا يمكن تعديله مباشرةً من الخارج؛ تصل إليه الدالة الداخلية فقط. وهذه الدالة updateNum تستطيع أيضًا تعديل متغيرات الدالة الخارجية.
العامل التمارين 13–17 مستوحاة من هذا السؤال على Stack Overflow
التمرين 18: أنشئ دالة تستقبل عددًا وتُرجع دالة داخلية تستقبل عددًا آخر، وتُرجع حاصل ضرب العددين.
function func1(num1){
function func2(num2){
return num1 * num2;
}
return func2;
}
console.log(func1(2)(4));
console.log(func1(5)(10));
console.log(func1(1)(0));
console.log(func1(15)(8));
النتيجة كما يلي:
8
50
0
120
التمرين 19: ارجع إلى التمرين 18؛ إلى أي نطاقات تستطيع func2 الوصول؟ تلميح: استخدم console.dir(func1()).
function func1(num1){
function func2(num2){
return num1 * num2;
}
return func2;
}
console.dir(func1());
النتيجة كما يلي:
0: Closure (func1) {num1: undefined}
1: Global {parent: Window, opener: null, top: Window, length: 0, frames: Window, …}
num1 لم يُحدَّد بعد، لذا num1 قيمته undefined.
func2 تصل إلى نطاقين: نطاق الدالة الخارجية func1والنطاق العام.
النطاق المعجمي
يتحدّد النطاق المعجمي بحسب موضع تعريف الدالة في الشيفرة. يمكنها الوصول إلى متغيراتها وإلى متغيرات النطاقات المحيطة بها وصولًا إلى النطاق العام.
التمرين 20: ارجع إلى التمرين 18؛ أنشئ func3 داخل func2 بحيث تستقبل عددًا ثالثًا. func3 تُرجع num1 * num2 + num3. func2 تُرجع func3.
function func1(num1){
function func2(num2){
function func3(num3){
return num1 * num2 + num3;
}
return func3;
}
return func2;
}
console.log(func1(2)(4)(8));
console.log(func1(5)(10)(6));
console.log(func1(1)(0)(1));
console.log(func1(15)(8)(10));
النتيجة كما يلي:
16
56
1
130
التمرين 21: ارجع إلى التمرين 20؛ إلى أي نطاقات تستطيع func3 الوصول؟ تلميح: استخدم console.dir(func1()()).
function func1(num1){
function func2(num2){
function func3(num3){
return num1 * num2 + num3;
}
return func3;
}
return func2;
}
console.dir(func1()())
النتيجة كما يلي:
0: Closure (func2) {num2: undefined}
1: Closure (func1) {num1: undefined}
2: Global {parent: Window, opener: null, top: Window, length: 0, frames: Window, …}
func3 تستطيع الوصول إلى متغيرات func2 ومتغيرات func1 ومتغيرات النطاق العام.
الإغلاقات مرة أخرى
الإغلاق هو دالة مع بيئتها المعجمية التي عُرّفت فيها، بما تتضمّنه من متغيرات متاحة وقت إنشاء الدالة. انظر الإغلاقات (Closures)