We are still cooking the magic in the way!
النطاق والاغلاقات
فهم النطاق في جافاسكريبت
النطاق هو احد اكثر المفاهيم اساسية في جافاسكريبت. يحدد اين يمكن الوصول الى المتغيرات والدوال في الكود. فهم النطاق ضروري لكتابة برامج خالية من الاخطاء وادارة الذاكرة بكفاءة وتنظيم الكود بطريقة نظيفة ومتوقعة. كل متغير تعلنه يعيش في نطاق محدد وتتبع جافاسكريبت قواعد دقيقة لتحديد المتغيرات المرئية من اي نقطة في الكود.
هناك ثلاثة انواع رئيسية من النطاق في جافاسكريبت: النطاق العام ونطاق الدالة ونطاق الكتلة. لكل نوع خصائص مميزة تؤثر على كيفية عمل متغيراتك. اتقان هذه المفاهيم سيساعدك على تجنب المزالق الشائعة مثل الكتابة فوق المتغيرات عن طريق الخطا وقيم undefined غير المتوقعة وتسريبات الذاكرة. في هذا الدرس سنستكشف كل نوع من النطاق بعمق ونفهم كيف تعمل سلسلة النطاق ثم نغوص في الاغلاقات -- واحد من اقوى الانماط في جافاسكريبت.
النطاق العام
النطاق العام هو النطاق الاكثر خارجية في برنامج جافاسكريبت. المتغيرات المعلنة خارج اي دالة او كتلة تكون في النطاق العام. هذه المتغيرات يمكن الوصول اليها من اي مكان في الكود -- داخل الدوال وداخل الحلقات وداخل الشروط ومن اي ملف يعمل في نفس سياق التنفيذ. في بيئة المتصفح تصبح المتغيرات العامة خصائص على كائن window. في Node.js تصبح خصائص على كائن global.
مثال: النطاق العام
// المتغيرات المعلنة في المستوى الاعلى في النطاق العام
var globalVar = 'انا عام';
let globalLet = 'انا ايضا عام';
const globalConst = 'انا عام ايضا';
function showGlobals() {
// جميع المتغيرات العامة يمكن الوصول اليها داخل الدوال
console.log(globalVar); // انا عام
console.log(globalLet); // انا ايضا عام
console.log(globalConst); // انا عام ايضا
}
showGlobals();
// في المتصفح، var تنشئ خاصية على window
var browserVar = 'مرحبا';
// console.log(window.browserVar); // 'مرحبا' (في المتصفح)
// let وconst لا تنشئان خصائص على window
let browserLet = 'العالم';
// console.log(window.browserLet); // undefined (في المتصفح)
مثال: تعارض اسماء المتغيرات العامة
// الملف 1: يعين متغيرا عاما
var config = { theme: 'dark', language: 'ar' };
// الملف 2: يكتب فوق نفس المتغير العام عن طريق الخطا
var config = { apiUrl: 'https://api.example.com' };
// الاعداد الاصلي ضاع تماما
console.log(config); // { apiUrl: 'https://api.example.com' }
// config.theme اصبح undefined -- هذا قد يعطل التطبيق
نطاق الدالة
نطاق الدالة يعني ان المتغيرات المعلنة داخل دالة يمكن الوصول اليها فقط داخل تلك الدالة. لا يمكن الوصول اليها من خارج الدالة. ينطبق هذا على المتغيرات المعلنة بـ var وlet وconst. نطاق الدالة ينشئ حدودا تغلف المتغيرات وتمنعها من التسرب الى النطاق المحيط. كل استدعاء دالة ينشئ نطاقا جديدا لذلك المتغيرات بنفس الاسم في دوال مختلفة لا تتداخل مع بعضها.
مثال: نطاق الدالة
function calculateTotal(price, taxRate) {
// هذه المتغيرات موجودة فقط داخل هذه الدالة
var subtotal = price;
let tax = price * taxRate;
const total = subtotal + tax;
console.log(total); // يمكن الوصول اليه هنا
return total;
}
calculateTotal(100, 0.1); // 110
// كل هذه ستسبب خطا مرجعي
// console.log(subtotal); // ReferenceError: subtotal is not defined
// console.log(tax); // ReferenceError: tax is not defined
// console.log(total); // ReferenceError: total is not defined
// كل استدعاء دالة ينشئ نطاقه الخاص
function counter() {
var count = 0;
count++;
console.log(count);
}
counter(); // 1
counter(); // 1 (وليس 2 -- يتم انشاء 'count' جديد في كل مرة)
counter(); // 1
مثال: نطاق الدوال المتداخلة
function outer() {
var outerVar = 'انا من الخارجية';
function inner() {
var innerVar = 'انا من الداخلية';
console.log(outerVar); // يمكن الوصول -- الداخلية ترى متغيرات الخارجية
console.log(innerVar); // يمكن الوصول -- متغير الداخلية الخاص
}
inner();
console.log(outerVar); // يمكن الوصول
// console.log(innerVar); // خطا مرجعي -- الخارجية لا ترى متغيرات الداخلية
}
outer();
نطاق الكتلة مع let وconst
تم تقديم نطاق الكتلة في ES6 مع الكلمتين المفتاحيتين let وconst. الكتلة هي اي كود محاط باقواس معقوصة {} مثل جسم عبارة if او حلقة for او حلقة while او حتى كتلة مستقلة. المتغيرات المعلنة بـ let او const داخل كتلة يمكن الوصول اليها فقط داخل تلك الكتلة. هذا مختلف عن var التي تكون محددة بنطاق الدالة وتتجاهل حدود الكتل.
مثال: نطاق الكتلة مقابل نطاق الدالة
// var محددة بنطاق الدالة -- تتجاهل حدود الكتل
if (true) {
var varVariable = 'اتسرب من الكتل!';
let letVariable = 'ابقى داخل الكتلة';
const constVariable = 'انا ايضا ابقى داخل';
}
console.log(varVariable); // 'اتسرب من الكتل!' (var تتجاهل الكتل)
// console.log(letVariable); // ReferenceError
// console.log(constVariable); // ReferenceError
// نطاق الكتلة في حلقات for
for (let i = 0; i < 3; i++) {
// 'i' موجود فقط داخل هذه الحلقة
console.log(i); // 0, 1, 2
}
// console.log(i); // ReferenceError: i is not defined
// قارن مع var في حلقة for
for (var j = 0; j < 3; j++) {
console.log(j); // 0, 1, 2
}
console.log(j); // 3 (var تتسرب من الحلقة!)
// الكتل المستقلة تنشئ نطاقا ايضا
{
let blockScoped = 'هنا فقط';
console.log(blockScoped); // 'هنا فقط'
}
// console.log(blockScoped); // ReferenceError
var وlet/const هو احد الاسباب الرئيسية التي تجعل جافاسكريبت الحديثة تفضل بشدة let وconst على var. نطاق الكتلة يجعل الكود اكثر قابلية للتنبؤ لان المتغيرات محصورة في اصغر نطاق ممكن. استخدم const افتراضيا وانتقل الى let فقط عندما تحتاج اعادة التعيين وتجنب var تماما في الكود الحديث.مثال: فوائد نطاق الكتلة العملية
// نطاق الكتلة يمنع تسرب المتغيرات في الشروط
function processUser(user) {
if (user.isAdmin) {
const adminPanel = 'وصول كامل';
let permissions = ['قراءة', 'كتابة', 'حذف'];
console.log(adminPanel, permissions);
}
if (user.isEditor) {
// هذا متغير 'permissions' مختلف -- لا تعارض
let permissions = ['قراءة', 'كتابة'];
console.log(permissions);
}
// لا adminPanel ولا permissions تتسرب هنا
// هذا يبقي جسم الدالة نظيفا ومتوقعا
}
// نطاق الكتلة في عبارات switch
function getDayType(day) {
switch (day) {
case 'السبت':
case 'الاحد': {
const type = 'عطلة نهاية الاسبوع';
return type;
}
default: {
const type = 'يوم عمل'; // نفس الاسم، كتلة مختلفة -- لا تعارض
return type;
}
}
}
النطاق المعجمي
تستخدم جافاسكريبت النطاق المعجمي (يسمى ايضا النطاق الثابت) وهذا يعني ان نطاق المتغير يتحدد بموقعه في الكود المصدري وليس بترتيب استدعاء الدوال اثناء التنفيذ. عندما يتم تعريف دالة فانها تتذكر بشكل دائم النطاق الذي انشئت فيه. هذا هو الاساس الذي تبنى عليه الاغلاقات.
في النطاق المعجمي تملك الدوال الداخلية وصولا الى المتغيرات المعرفة في دوالها الخارجية. يتحدد هذا الوصول في وقت كتابة الكود (الوقت المعجمي) وليس في وقت تنفيذ الكود. لهذا يسمى النطاق المعجمي -- لانه يتبع البنية المعجمية للكود.
مثال: النطاق المعجمي
const outerValue = 'خارجي';
function outerFunction() {
const middleValue = 'وسطي';
function middleFunction() {
const innerValue = 'داخلي';
function innerFunction() {
// هذه الدالة يمكنها الوصول لكل النطاقات الخارجية
console.log(outerValue); // 'خارجي' (النطاق العام)
console.log(middleValue); // 'وسطي' (نطاق outerFunction)
console.log(innerValue); // 'داخلي' (نطاق middleFunction)
}
innerFunction();
}
middleFunction();
}
outerFunction();
// النطاق المعجمي يتحدد باين يتم تعريف الدالة
// وليس باين او كيف يتم استدعاؤها
let greeting = 'مرحبا';
function greet() {
console.log(greeting); // تبحث عن greeting في نطاقها المعجمي
}
function changeAndGreet() {
let greeting = 'اهلا'; // هذا متغير مختلف في نطاق مختلف
greet(); // لا تزال تطبع 'مرحبا' -- النطاق المعجمي لـ greet يحتوي على greeting العام
}
changeAndGreet(); // مرحبا (وليس اهلا)
سلسلة النطاق
عندما تواجه جافاسكريبت متغيرا تبحث عنه بدءا من النطاق الحالي وتتحرك للخارج عبر كل نطاق محيط حتى تصل الى النطاق العام. هذه السلسلة من النطاقات تسمى سلسلة النطاق. اذا وجد المتغير في اي نطاق على طول السلسلة تستخدم تلك القيمة. اذا لم يوجد المتغير حتى في النطاق العام يتم رمي خطا مرجعي. سلسلة النطاق تتحرك دائما للخارج -- النطاقات الداخلية يمكنها الوصول للنطاقات الخارجية لكن ليس العكس.
مثال: سلسلة النطاق عمليا
const global = 'عام';
function levelOne() {
const one = 'المستوى الاول';
function levelTwo() {
const two = 'المستوى الثاني';
function levelThree() {
const three = 'المستوى الثالث';
// سلسلة النطاق: levelThree -> levelTwo -> levelOne -> عام
console.log(three); // وجد في النطاق الحالي
console.log(two); // وجد بمستوى واحد اعلى
console.log(one); // وجد بمستويين اعلى
console.log(global); // وجد في النطاق العام
}
levelThree();
}
levelTwo();
}
levelOne();
// تظليل المتغيرات -- متغير النطاق الداخلي يخفي المتغير الخارجي
let color = 'ازرق';
function paintHouse() {
let color = 'احمر'; // يظلل 'color' الخارجي
function paintRoom() {
let color = 'اخضر'; // يظلل color الخاص بـ paintHouse
console.log(color); // 'اخضر' (وجد في النطاق الحالي اولا)
}
paintRoom();
console.log(color); // 'احمر' (color الخاص بـ paintHouse)
}
paintHouse();
console.log(color); // 'ازرق' (color العام لم يتغير)
ما هي الاغلاقات؟
يتم انشاء اغلاق عندما تحتفظ دالة بالوصول الى متغيرات من نطاقها الخارجي (المحيط) حتى بعد انتهاء تنفيذ الدالة الخارجية. بمعنى اخر الاغلاق هو دالة مجمعة مع بيئتها المعجمية. كل دالة في جافاسكريبت تنشئ اغلاقا لكن المصطلح يستخدم بشكل اكثر شيوعا عندما يتم ارجاع دالة داخلية من دالة خارجية وتستمر في الاشارة الى متغيرات الدالة الخارجية.
الاغلاقات ليست صيغة خاصة او ميزة منفصلة تقوم بتفعيلها. انها نتيجة طبيعية للنطاق المعجمي مع حقيقة ان الدوال هي قيم من الدرجة الاولى في جافاسكريبت (مما يعني انه يمكن تمريرها وارجاعها وتخزينها مثل اي قيمة اخرى). فهم الاغلاقات يفتح انماط برمجة قوية تشمل خصوصية البيانات ومصانع الدوال والتطبيق الجزئي ونمط الوحدة.
مثال: اول اغلاق لك
function createGreeter(greeting) {
// 'greeting' في نطاق الدالة الخارجية
return function(name) {
// هذه الدالة الداخلية لها وصول الى 'greeting'
// حتى بعد انتهاء تنفيذ createGreeter
console.log(greeting + '، ' + name + '!');
};
}
const sayHello = createGreeter('مرحبا');
const sayHi = createGreeter('اهلا');
// createGreeter انتهت من الارجاع لكن الدوال الداخلية
// لا تزال تملك وصولا لقيم 'greeting' الخاصة بها
sayHello('علي'); // !مرحبا، علي
sayHello('احمد'); // !مرحبا، احمد
sayHi('سارة'); // !اهلا، سارة
// كل اغلاق يلتقط نسخته الخاصة من المتغيرات الخارجية
// greeting الخاص بـ sayHello هو 'مرحبا'، greeting الخاص بـ sayHi هو 'اهلا'
امثلة الاغلاق: العداد
احد الامثلة الكلاسيكية على الاغلاقات هو بناء عداد. متغير العداد محاط في نطاق الدالة الخارجية والدالة او الدوال المرجعة يمكنها الوصول اليه وتعديله. متغير العداد غير قابل للوصول من الخارج -- فهو خاص فعليا. كل استدعاء لدالة المصنع ينشئ عدادا جديدا ومستقلا بحالته المحاطة الخاصة.
مثال: العداد مع الاغلاقات
function createCounter(initialValue = 0) {
let count = initialValue; // هذا المتغير محاط
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
getCount: function() {
return count;
},
reset: function() {
count = initialValue;
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.increment()); // 3
console.log(counter.decrement()); // 2
console.log(counter.getCount()); // 2
console.log(counter.reset()); // 0
// كل عداد مستقل
const counterA = createCounter(10);
const counterB = createCounter(100);
console.log(counterA.increment()); // 11
console.log(counterB.increment()); // 101
console.log(counterA.getCount()); // 11 (غير متاثر بـ counterB)
// 'count' غير قابل للوصول من الخارج
// console.log(counter.count); // undefined -- هو خاص
امثلة الاغلاق: المتغيرات الخاصة
توفر الاغلاقات الطريقة الوحيدة لانشاء متغيرات خاصة حقا في جافاسكريبت (قبل الاضافة الحديثة لحقول الصف الخاصة بـ #). من خلال احاطة المتغيرات داخل دالة وكشف توابع محددة فقط للتفاعل معها يمكنك التحكم بالضبط في كيفية الوصول الى البيانات وتعديلها. هذا النمط اساسي لكتابة كود امن وقابل للصيانة.
مثال: المتغيرات الخاصة مع الاغلاقات
function createBankAccount(initialBalance) {
let balance = initialBalance; // متغير خاص
const transactions = []; // مصفوفة خاصة
return {
deposit: function(amount) {
if (amount <= 0) {
console.log('مبلغ الايداع يجب ان يكون موجبا');
return;
}
balance += amount;
transactions.push({ type: 'ايداع', amount, date: new Date() });
console.log('تم الايداع: $' + amount + '. الرصيد: $' + balance);
},
withdraw: function(amount) {
if (amount <= 0) {
console.log('مبلغ السحب يجب ان يكون موجبا');
return;
}
if (amount > balance) {
console.log('رصيد غير كاف. الرصيد: $' + balance);
return;
}
balance -= amount;
transactions.push({ type: 'سحب', amount, date: new Date() });
console.log('تم السحب: $' + amount + '. الرصيد: $' + balance);
},
getBalance: function() {
return balance;
},
getTransactionCount: function() {
return transactions.length;
}
};
}
const account = createBankAccount(1000);
account.deposit(500); // تم الايداع: $500. الرصيد: $1500
account.withdraw(200); // تم السحب: $200. الرصيد: $1300
console.log(account.getBalance()); // 1300
console.log(account.getTransactionCount()); // 2
// لا يمكن الوصول المباشر او تعديل البيانات الخاصة
// account.balance = 999999; // هذا ينشئ خاصية جديدة ولا يغير الرصيد المحاط
console.log(account.getBalance()); // لا يزال 1300
مثال: الحالة الخاصة للتحقق
function createValidator(rules) {
// rules ملتقطة بالاغلاق ولا يمكن تعديلها خارجيا
const errors = [];
return {
validate: function(data) {
errors.length = 0; // مسح الاخطاء السابقة
for (const rule of rules) {
if (!rule.test(data[rule.field])) {
errors.push(rule.message);
}
}
return errors.length === 0;
},
getErrors: function() {
return [...errors]; // ارجاع نسخة لمنع التعديل الخارجي
}
};
}
const userValidator = createValidator([
{ field: 'name', test: val => val && val.length >= 2, message: 'الاسم يجب ان يكون حرفين على الاقل' },
{ field: 'email', test: val => val && val.includes('@'), message: 'البريد يجب ان يحتوي @' },
{ field: 'age', test: val => val && val >= 18, message: 'يجب ان يكون العمر 18 او اكبر' }
]);
console.log(userValidator.validate({ name: 'ا', email: 'خطا', age: 15 })); // false
console.log(userValidator.getErrors());
// ['الاسم يجب ان يكون حرفين على الاقل', 'البريد يجب ان يحتوي @', 'يجب ان يكون العمر 18 او اكبر']
console.log(userValidator.validate({ name: 'علي', email: 'ali@mail.com', age: 25 })); // true
console.log(userValidator.getErrors()); // []
الاغلاقات في الحلقات: المزلق الكلاسيكي
واحد من اشهر اسئلة مقابلات جافاسكريبت وتحديات التنقيح يتضمن الاغلاقات داخل الحلقات. عندما تستخدم var في حلقة وتنشئ اغلاقات تشير الى متغير الحلقة فان جميع الاغلاقات تتشارك نفس المتغير لان var محددة بنطاق الدالة وليس نطاق الكتلة. بحلول الوقت الذي تنفذ فيه الاغلاقات تكون الحلقة قد انتهت ويحمل المتغير قيمته النهائية. هذا احد اهم الاسباب لاستخدام let بدلا من var.
مثال: مشكلة اغلاق الحلقة الكلاسيكية
// المشكلة: var محددة بنطاق الدالة
function createButtonHandlersVar() {
var handlers = [];
for (var i = 0; i < 5; i++) {
handlers.push(function() {
console.log('الزر ' + i + ' نقر');
});
}
return handlers;
}
var buttons = createButtonHandlersVar();
buttons[0](); // الزر 5 نقر (المتوقع: الزر 0 نقر)
buttons[1](); // الزر 5 نقر (المتوقع: الزر 1 نقر)
buttons[2](); // الزر 5 نقر (المتوقع: الزر 2 نقر)
// جميع المعالجات تطبع 5 لانها تتشارك نفس المتغير 'i'
// بحلول وقت تنفيذ اي معالج، الحلقة انتهت و i === 5
مثال: ثلاثة حلول لمشكلة اغلاق الحلقة
// الحل 1: استخدم let (حديث ومفضل)
function createButtonHandlersLet() {
const handlers = [];
for (let i = 0; i < 5; i++) {
// let تنشئ 'i' جديد لكل تكرار
handlers.push(function() {
console.log('الزر ' + i + ' نقر');
});
}
return handlers;
}
const buttonsLet = createButtonHandlersLet();
buttonsLet[0](); // الزر 0 نقر
buttonsLet[1](); // الزر 1 نقر
buttonsLet[2](); // الزر 2 نقر
// الحل 2: استخدم IIFE (نهج ما قبل ES6)
function createButtonHandlersIIFE() {
var handlers = [];
for (var i = 0; i < 5; i++) {
handlers.push((function(capturedI) {
return function() {
console.log('الزر ' + capturedI + ' نقر');
};
})(i)); // استدعاء فوري مع قيمة i الحالية
}
return handlers;
}
// الحل 3: استخدم forEach (يتجنب متغير الحلقة اليدوي)
function createButtonHandlersForEach() {
return [0, 1, 2, 3, 4].map(function(i) {
return function() {
console.log('الزر ' + i + ' نقر');
};
});
}
for. اي موقف تنشئ فيه اغلاقات داخل حلقة باستخدام var سيعاني من نفس المشكلة. يشمل ذلك حلقات while وحلقات do...while وحتى توابع المصفوفات اذا استخدمت var بطريقة ما داخلها. الحل دائما هو نفسه: استخدم let لانشاء ربط متغير جديد لكل تكرار.نمط الوحدة مع الاغلاقات
قبل وحدات ES6 كان نمط الوحدة هو الطريقة القياسية لتنظيم كود جافاسكريبت وانشاء مكونات مغلفة وقابلة لاعادة الاستخدام. يستخدم تعبير دالة يستدعى فورا (IIFE) مع الاغلاقات لانشاء نطاق خاص ثم يرجع كائنا يحتوي فقط على التوابع والخصائص التي يجب ان تكون متاحة للعامة. هذا النمط لا يزال يستخدم على نطاق واسع ومهم لفهمه.
مثال: نمط الوحدة
// نمط الوحدة باستخدام IIFE والاغلاقات
const UserModule = (function() {
// متغيرات ودوال خاصة
const users = [];
let nextId = 1;
function findUserIndex(id) {
return users.findIndex(user => user.id === id);
}
function validateUser(userData) {
if (!userData.name || userData.name.trim() === '') {
throw new Error('الاسم مطلوب');
}
if (!userData.email || !userData.email.includes('@')) {
throw new Error('بريد الكتروني صالح مطلوب');
}
return true;
}
// الواجهة العامة (الكائن المرجع)
return {
addUser: function(userData) {
validateUser(userData);
const user = {
id: nextId++,
name: userData.name,
email: userData.email,
createdAt: new Date()
};
users.push(user);
return user;
},
removeUser: function(id) {
const index = findUserIndex(id);
if (index === -1) {
throw new Error('المستخدم غير موجود');
}
return users.splice(index, 1)[0];
},
getUser: function(id) {
const index = findUserIndex(id);
if (index === -1) return null;
return { ...users[index] }; // ارجاع نسخة
},
getAllUsers: function() {
return users.map(user => ({ ...user })); // ارجاع نسخ
},
getUserCount: function() {
return users.length;
}
};
})();
// استخدام الوحدة
const ali = UserModule.addUser({ name: 'علي', email: 'ali@mail.com' });
const ahmed = UserModule.addUser({ name: 'احمد', email: 'ahmed@mail.com' });
console.log(UserModule.getUserCount()); // 2
console.log(UserModule.getUser(1)); // { id: 1, name: 'علي', ... }
console.log(UserModule.getAllUsers()); // [{ id: 1, ... }, { id: 2, ... }]
// العناصر الداخلية الخاصة مخفية تماما
// console.log(UserModule.users); // undefined
// console.log(UserModule.nextId); // undefined
// console.log(UserModule.findUserIndex); // undefined
// console.log(UserModule.validateUser); // undefined
مثال: نمط وحدة قابل للتكوين
// وحدة تقبل التكوين
const Logger = (function() {
let logLevel = 'info';
const levels = { debug: 0, info: 1, warn: 2, error: 3 };
const logs = [];
function shouldLog(level) {
return levels[level] >= levels[logLevel];
}
function formatMessage(level, message) {
const timestamp = new Date().toISOString();
return '[' + timestamp + '] [' + level.toUpperCase() + '] ' + message;
}
return {
setLevel: function(level) {
if (levels[level] === undefined) {
throw new Error('مستوى تسجيل غير صالح: ' + level);
}
logLevel = level;
},
debug: function(msg) {
if (shouldLog('debug')) {
const formatted = formatMessage('debug', msg);
logs.push(formatted);
console.log(formatted);
}
},
info: function(msg) {
if (shouldLog('info')) {
const formatted = formatMessage('info', msg);
logs.push(formatted);
console.log(formatted);
}
},
warn: function(msg) {
if (shouldLog('warn')) {
const formatted = formatMessage('warn', msg);
logs.push(formatted);
console.warn(formatted);
}
},
error: function(msg) {
if (shouldLog('error')) {
const formatted = formatMessage('error', msg);
logs.push(formatted);
console.error(formatted);
}
},
getLogHistory: function() {
return [...logs];
}
};
})();
Logger.setLevel('warn');
Logger.debug('هذا لن يظهر'); // تحت الحد
Logger.info('هذا لن يظهر'); // تحت الحد
Logger.warn('مساحة القرص منخفضة'); // سيظهر
Logger.error('فشل الاتصال'); // سيظهر
التاثيرات على الذاكرة للاغلاقات
لان الاغلاقات تحتفظ بمراجع لمتغيرات نطاقها الخارجي فان تلك المتغيرات لا يمكن تنظيفها من الذاكرة طالما الاغلاق موجود. هذا عادة ميزة وليس خطا -- فهو ما يسمح للاغلاقات بالعمل. ومع ذلك يمكن ان يصبح مشكلة اذا انشات اغلاقات تلتقط بنى بيانات كبيرة بغير قصد او اذا استمرت الاغلاقات لفترة اطول من اللازم. فهم التاثيرات على الذاكرة يساعدك على كتابة كود فعال وتجنب تسريبات الذاكرة.
مثال: اعتبارات الذاكرة
// مشكلة ذاكرة محتملة: التقاط بيانات كبيرة
function processLargeData() {
const hugeArray = new Array(1000000).fill('بيانات'); // بيانات كبيرة
return function getLength() {
// هذا الاغلاق يبقي hugeArray حيا في الذاكرة
// رغم انه يحتاج فقط الطول
return hugeArray.length;
};
}
const getLen = processLargeData();
// hugeArray لا يمكن تنظيفها لان getLen تشير اليها
// افضل: التقط فقط ما تحتاجه
function processLargeDataBetter() {
const hugeArray = new Array(1000000).fill('بيانات');
const length = hugeArray.length; // استخرج ما تحتاجه
return function getLength() {
return length; // تلتقط فقط الرقم وليس المصفوفة
};
// hugeArray يمكن تنظيفها بعد ارجاع processLargeDataBetter
}
// تنظيف الاغلاقات
function createEventHandler(element) {
let clickCount = 0;
function handler() {
clickCount++;
console.log('النقرات: ' + clickCount);
}
element.addEventListener('click', handler);
// ارجاع دالة تنظيف
return function cleanup() {
element.removeEventListener('click', handler);
// الان handler يمكن تنظيفها
// و clickCount معها
};
}
// الاستخدام:
// const cleanup = createEventHandler(myButton);
// لاحقا عندما لا تكون مطلوبة:
// cleanup();
حالات الاستخدام العملية
الاغلاقات ليست مجرد مفاهيم نظرية -- فهي تستخدم في كل مكان في تطوير جافاسكريبت الحقيقي. دعنا نستكشف حالات الاستخدام الاكثر شيوعا وعملية التي ستواجهها وتستخدمها في مشاريعك الخاصة.
معالجات الاحداث مع الحالة
الاغلاقات تسمح لمعالجات الاحداث بالحفاظ على الحالة بين الاستدعاءات بدون استخدام متغيرات عامة. كل معالج يمكن ان يملك حالته الخاصة التي تستمر عبر احداث متعددة.
مثال: معالجات الاحداث مع حالة الاغلاق
// دالة تاخير التنفيذ -- احد اكثر انماط الاغلاق شيوعا
function debounce(fn, delay) {
let timeoutId = null; // خاص بهذا الاغلاق
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// الاستخدام
const handleSearch = debounce(function(query) {
console.log('البحث عن: ' + query);
}, 300);
// استدعاءات سريعة -- فقط الاخير ينفذ
handleSearch('ج');
handleSearch('جا');
handleSearch('جاف');
handleSearch('جافا');
handleSearch('جافاسكريبت');
// يطبع فقط: 'البحث عن: جافاسكريبت' (بعد 300 مللي ثانية)
// دالة الخنق -- نمط اغلاق كلاسيكي اخر
function throttle(fn, limit) {
let inThrottle = false;
return function(...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
const handleScroll = throttle(function() {
console.log('موقع التمرير: ' + window.scrollY);
}, 200);
خصوصية البيانات والتغليف
الاغلاقات توفر خصوصية بيانات حقيقية وهو شيء لا تستطيع الكائنات العادية تقديمه. من خلال اخفاء الحالة الداخلية خلف حدود الاغلاق تنشئ واجهات برمجة تطبيقات لا يمكن استخدامها الا من خلال التوابع التي تكشفها صراحة.
مثال: انماط خصوصية البيانات
// محدد معدل مع حالة خاصة
function createRateLimiter(maxRequests, timeWindow) {
const requests = []; // طوابع زمنية خاصة للطلبات
return function isAllowed() {
const now = Date.now();
// ازالة الطوابع الزمنية المنتهية
while (requests.length > 0 && requests[0] <= now - timeWindow) {
requests.shift();
}
if (requests.length < maxRequests) {
requests.push(now);
return true;
}
return false;
};
}
const limiter = createRateLimiter(3, 1000); // 3 طلبات في الثانية
console.log(limiter()); // true
console.log(limiter()); // true
console.log(limiter()); // true
console.log(limiter()); // false (تم الوصول للحد)
// ذاكرة تخزين مؤقت مع بيانات خاصة
function createCache(maxSize = 100) {
const cache = new Map(); // خاصة
return {
get: function(key) {
return cache.get(key);
},
set: function(key, value) {
if (cache.size >= maxSize) {
// ازالة اقدم ادخال
const firstKey = cache.keys().next().value;
cache.delete(firstKey);
}
cache.set(key, value);
},
has: function(key) {
return cache.has(key);
},
size: function() {
return cache.size;
},
clear: function() {
cache.clear();
}
};
}
const myCache = createCache(3);
myCache.set('ا', 1);
myCache.set('ب', 2);
myCache.set('ج', 3);
myCache.set('د', 4); // 'ا' يتم اخراجها
console.log(myCache.has('ا')); // false
console.log(myCache.has('د')); // true
مصانع الدوال
تستخدم مصانع الدوال الاغلاقات لانشاء دوال متخصصة من قالب عام. دالة المصنع تاخذ معاملات التكوين وترجع دالة جديدة تحتوي على تلك المعاملات مخبوزة فيها بشكل دائم من خلال الاغلاق. هذا نمط قوي ونظيف بشكل لا يصدق لانشاء سلوك قابل لاعادة الاستخدام وقابل للتكوين.
مثال: مصانع الدوال
// مصنع المضاعف
function createMultiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
const toPercent = createMultiplier(100);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(toPercent(0.75)); // 75
// مصنع المنسق
function createFormatter(currency, locale) {
const formatter = new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency
});
return function(amount) {
return formatter.format(amount);
};
}
const formatUSD = createFormatter('USD', 'en-US');
const formatEUR = createFormatter('EUR', 'de-DE');
const formatSAR = createFormatter('SAR', 'ar-SA');
console.log(formatUSD(1234.56)); // $1,234.56
console.log(formatEUR(1234.56)); // 1.234,56 EUR
console.log(formatSAR(1234.56)); // ر.س. 1,234.56
// مصنع منشئ العناوين
function createApiClient(baseUrl) {
return {
get: function(endpoint) {
const url = baseUrl + endpoint;
console.log('GET ' + url);
return fetch(url);
},
post: function(endpoint, data) {
const url = baseUrl + endpoint;
console.log('POST ' + url);
return fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
}
};
}
const api = createApiClient('https://api.example.com');
// api.get('/users'); // GET https://api.example.com/users
// api.post('/users', data); // POST https://api.example.com/users
مثال: مصنع دوال متقدم -- الحفظ المؤقت
// الحفظ المؤقت: تخزين نتائج الدوال المكلفة مؤقتا باستخدام الاغلاقات
function memoize(fn) {
const cache = new Map(); // خاصة بهذا الاغلاق
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('اصابة الذاكرة المؤقتة لـ:', key);
return cache.get(key);
}
console.log('الحساب لـ:', key);
const result = fn(...args);
cache.set(key, result);
return result;
};
}
// مثال حساب مكلف
const fibonacci = memoize(function fib(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
console.log(fibonacci(10)); // حساب (اول مرة)... 55
console.log(fibonacci(10)); // اصابة الذاكرة المؤقتة... 55
console.log(fibonacci(8)); // اصابة الذاكرة المؤقتة... 21
// حفظ اي دالة نقية مؤقتا
const expensiveCalculation = memoize(function(x, y) {
console.log('حساب ثقيل...');
let result = 0;
for (let i = 0; i < x * y; i++) {
result += Math.sqrt(i);
}
return result;
});
expensiveCalculation(100, 200); // حساب ثقيل...
expensiveCalculation(100, 200); // اصابة الذاكرة المؤقتة (فوري)
الاغلاقات والكود غير المتزامن
تلعب الاغلاقات دورا حاسما في جافاسكريبت غير المتزامنة. عندما تمرر استدعاء راجع الى setTimeout او تقوم بطلب fetch او تنشئ مستمع احداث فان دالة الاستدعاء الراجع تشكل اغلاقا على متغيراتها المحيطة. هذا يسمح للكود غير المتزامن بالوصول الى البيانات التي كانت متاحة عند انشاء الاستدعاء الراجع حتى لو نفذ الاستدعاء الراجع في وقت لاحق بكثير.
مثال: الاغلاقات في الانماط غير المتزامنة
// الاغلاقات تحافظ على الحالة عبر العمليات غير المتزامنة
function fetchUserData(userId) {
const startTime = Date.now(); // ملتقط بالاغلاق
fetch('/api/users/' + userId)
.then(function(response) {
return response.json();
})
.then(function(user) {
const elapsed = Date.now() - startTime; // الاغلاق يملك وصولا لـ startTime
console.log('تم تحميل ' + user.name + ' في ' + elapsed + ' مللي ثانية');
});
}
// تسلسلي غير متزامن مع الاغلاقات
function processItemsSequentially(items) {
let index = 0; // حالة مشتركة عبر الاغلاق
function processNext() {
if (index >= items.length) {
console.log('تمت معالجة جميع العناصر');
return;
}
const currentItem = items[index]; // ملتقط لهذا التكرار
index++;
setTimeout(function() {
console.log('معالجة: ' + currentItem);
processNext(); // استدعاء تكراري
}, 1000);
}
processNext();
}
processItemsSequentially(['تفاح', 'موز', 'كرز']);
// بعد 1 ثانية: معالجة: تفاح
// بعد 2 ثانية: معالجة: موز
// بعد 3 ثانية: معالجة: كرز
// تمت معالجة جميع العناصر
ملخص المفاهيم الرئيسية
دعنا نراجع اهم المفاهيم من هذا الدرس:
- النطاق العام -- متغيرات يمكن الوصول اليها في كل مكان. قلل استخدامها لتجنب التعارضات والتعديلات غير المقصودة.
- نطاق الدالة -- المتغيرات المعلنة داخل دالة خاصة بتلك الدالة. تنشا جديدة في كل استدعاء دالة.
- نطاق الكتلة --
letوconstمحصورة في اقرب كتلة{}محيطة. فضل هذه دائما علىvar. - النطاق المعجمي -- يتحدد النطاق باين يكتب الكود وليس اين يستدعى. الدوال الداخلية يمكنها الوصول للمتغيرات الخارجية.
- سلسلة النطاق -- البحث عن المتغيرات يتحرك للخارج من النطاق الحالي عبر كل نطاق محيط الى النطاق العام.
- الاغلاقات -- دوال تحتفظ بالوصول لمتغيرات نطاقها الخارجي حتى بعد ارجاع الدالة الخارجية. تمكن خصوصية البيانات ومصانع الدوال والاستدعاءات الراجعة ذات الحالة.
- اغلاقات الحلقات -- استخدم
letفي الحلقات لانشاء ربط جديد لكل تكرار وتجنب مزلقvarالكلاسيكي. - نمط الوحدة -- يستخدم IIFEs والاغلاقات لانشاء حالة خاصة مع واجهة برمجة عامة.
- الذاكرة -- الاغلاقات تبقي المتغيرات المشار اليها حية. التقط فقط ما تحتاجه ونظف عند الانتهاء.
تمرين عملي
اكمل التحديات التالية لتعزيز فهمك للنطاق والاغلاقات:
- اكتب دالة تسمى
createMultiplierتاخذ رقما وترجع دالة تضرب اي رقم معطى بالرقم الاصلي. اختبرها بانشاء دالتيdoubleوtripleوتحقق من انهما تعملان بشكل مستقل. - نفذ دالة
createStackترجع كائنا بتوابعpushوpopوpeekوsize. المصفوفة الداخلية يجب ان تكون خاصة تماما وغير قابلة للوصول من الخارج. اختبر انك لا تستطيع الوصول للمصفوفة الداخلية مباشرة. - انشئ دالة
onceتاخذ دالة كوسيط وترجع دالة جديدة يمكن استدعاؤها مرة واحدة فقط. الاستدعاءات اللاحقة يجب ان ترجع النتيجة من الاستدعاء الاول بدون اعادة تنفيذ الدالة الاصلية. استخدم الاغلاق لتتبع ما اذا كانت الدالة قد استدعيت. - اكتب دالة
createSecretKeeperتاخذ سلسلة سرية اولية. يجب ان ترجع كائنا بتوابعgetSecret(يتطلب كلمة مرور) وsetSecret(يتطلب كلمة المرور الحالية) وchangePassword. جميع الحالة الداخلية يجب ان تكون خاصة ولا يمكن الوصول اليها الا من خلال التوابع المرجعة. - نفذ دالة
memoizeتخزن نتائج استدعاءات الدوال المكلفة مؤقتا. يجب ان تقبل اي دالة وترجع نسخة محفوظة مؤقتا. اختبرها مع دالة فيبوناتشي تكرارية وتحقق من ان الاستدعاءات اللاحقة بنفس الوسيط ترجع فورا من الذاكرة المؤقتة.