دان أبراموف على JavaScript Closures

يصعب على المبرمجين عمليات الإغلاق لأنها بناء "غير مرئي".



عندما تستخدم كائنًا أو متغيرًا أو وظيفة ، فإنك تفعل ذلك عن قصد. تعتقد ، "هذا هو المكان الذي أحتاج فيه إلى متغير ،" وتضيفه إلى شفرتك. الإغلاق شيء آخر. بينما بدأ معظم المبرمجين في التعرف على عمليات الإغلاق ، فإن هؤلاء الأشخاص يستخدمون بالفعل عمليات الإغلاق دون معرفة ذلك. ربما نفس الشيء يحدث لك. لذا فإن تعلم الإغلاق لا يتعلق بتعلم فكرة جديدة بقدر ما يتعلق بتعلم كيفية التعرف على شيء واجهته عدة مرات من قبل. باختصار ، الإغلاق هو عندما تصل وظيفة إلى متغيرات تم الإعلان عنها خارجها. على سبيل المثال ، يوجد الإغلاق في هذا الجزء من الكود:















let users = ['Alice', 'Dan', 'Jessica'];
let query = 'A';
let user = users.filter(user => user.startsWith(query));


لاحظ أنها user => user.startsWith(query)وظيفة. إنها تستخدم متغيرًا query. ويتم الإعلان عن هذا المتغير خارج الدالة. هذا إغلاق.



يمكنك تخطي القراءة إذا أردت. تنظر بقية هذه المواد إلى الإغلاق في ضوء مختلف. بدلاً من الحديث عن ماهية عمليات الإغلاق ، سيتناول هذا الجزء من المقالة تفاصيل اكتشاف عمليات الإغلاق. هذا مشابه لكيفية عمل المبرمجين الأوائل في الستينيات.



الخطوة 1: يمكن للوظائف الوصول إلى المتغيرات المعلنة خارجها



لفهم عمليات الإغلاق ، يجب أن تكون على دراية بالمتغيرات والوظائف. في هذا المثال ، نعلن عن متغير foodداخل دالة eat:



function eat() {
  let food = 'cheese';
  console.log(food + ' is good');
}
eat(); //    'cheese is good'


ماذا لو أردت أن تكون قادرًا على تغيير قيمة متغير لاحقًا foodخارج الدالة eat؟ للقيام بذلك ، يمكننا إزالة المتغير نفسه من الوظيفة foodونقله إلى مستوى أعلى:



let food = 'cheese'; //     
function eat() {
  console.log(food + ' is good');
}


هذا يسمح لك بتغيير المتغير food"من الخارج" عند الحاجة:



eat(); //  'cheese is good'
food = 'pizza';
eat(); //  'pizza is good'
food = 'sushi';
eat(); //  'sushi is good'


بمعنى آخر ، foodلم يعد المتغير eatمحليًا بالنسبة للدالة . لكن الوظيفة eat، على الرغم من ذلك ، ليس لديها مشاكل عند العمل مع هذا المتغير. يمكن للوظائف الوصول إلى المتغيرات المعلنة خارجها. توقف لبعض الوقت وتحقق من نفسك ، وتأكد من عدم وجود مشاكل لديك مع هذه الفكرة. بمجرد أن يستقر هذا الفكر في عقلك ، انتقل إلى الخطوة الثانية.



الخطوة 2: وضع الكود في استدعاء الوظيفة



لنفترض أن لدينا بعض الكود:



/*   */


لا يهم أي رمز هو. لكن لنفترض أننا بحاجة إلى تشغيله مرتين.



الطريقة الأولى للقيام بذلك هي عمل نسخة من الكود:



/*   */
/*   */


هناك طريقة أخرى وهي وضع الكود في حلقة:



for (let i = 0; i < 2; i++) {
  /*   */
}


والطريقة الثالثة ، التي تهمنا بشكل خاص اليوم ، هي وضع هذا الرمز في دالة:



function doTheThing() {
  /*   */
}
doTheThing();
doTheThing();


يمنحنا استخدام الوظيفة أقصى قدر من المرونة ، حيث يسمح لنا بالاتصال بالرمز المحدد في أي عدد من المرات وفي أي وقت ومن أي مكان في البرنامج.



في الواقع ، إذا لزم الأمر ، يمكننا قصر أنفسنا على استدعاء واحد فقط للوظيفة الجديدة:



function doTheThing() {
  /*   */
}
doTheThing();


الرجاء ملاحظة أن الشفرة أعلاه تعادل مقتطف الشفرة الأصلي:



/*   */


بمعنى آخر ، إذا أخذنا جزءًا من التعليمات البرمجية ولفناه في دالة ، ثم استدعينا هذه الوظيفة مرة واحدة تمامًا ، فلن نؤثر على ما تفعله هذه الشفرة بالضبط. هناك بعض الاستثناءات لهذه القاعدة ، والتي لن ننتبه لها ، ولكن بشكل عام يمكننا افتراض أن هذه القاعدة صحيحة. فكر في الأمر لفترة ، تعتاد على هذه الفكرة.



الخطوة 3: اكتشاف الإغلاق



توصلنا إلى فكرتين:



  • يمكن أن تعمل الوظائف مع المتغيرات المعلنة خارجها.
  • إذا قمت بوضع الكود في دالة واستدعيت هذه الوظيفة مرة واحدة ، فلن يؤثر ذلك على نتائج الكود.


الآن دعنا نتحدث عما سيحدث إذا تم الجمع بين هاتين الفكرتين.



لنأخذ عينة الكود التي نظرنا إليها في الخطوة الأولى:



let food = 'cheese';
function eat() {
  console.log(food + ' is good');
}
eat();


الآن دعنا نضع هذا المثال بالكامل في دالة نخطط لاستدعاءها مرة واحدة فقط:



function liveADay() {
  let food = 'cheese';
  function eat() {
    console.log(food + ' is good');
  }
  eat();
}
liveADay();


اقرأ أمثلة التعليمات البرمجية السابقة وتأكد من أنها متكافئة.



المثال الثاني يعمل! لكن دعونا نلقي نظرة فاحصة عليها. لاحظ أن الوظيفة eatموجودة داخل دالة liveADay. هل هذا مسموح به في JavaScript؟ هل من الممكن حقاً لف وظيفة داخل أخرى؟



هناك لغات يكون فيها الكود المنظم بهذه الطريقة غير صحيح. على سبيل المثال ، في لغة C ، سيكون هذا الرمز غير صحيح (لا توجد عمليات إغلاق في هذه اللغة). هذا يعني أنه عند استخدام لغة C ، فإن استنتاجنا الثاني سيكون خاطئًا - لا يمكنك فقط أن تأخذ جزءًا عشوائيًا من التعليمات البرمجية و "لفه" في دالة. لكن لا يوجد مثل هذا القيد في JavaScript.



دعنا نفكر في هذا الرمز مرة أخرى ، مع إيلاء اهتمام خاص للمكان الذي يتم فيه الإعلان عن المتغير ومكان استخدامه.food:



function liveADay() {
  let food = 'cheese'; //  `food`
  function eat() {
    console.log(food + ' is good'); //   `food`
  }
  eat();
}
liveADay();


دعنا ننتقل إلى هذا الرمز خطوة بخطوة معًا. أولاً ، نعلن ، في المستوى الأعلى ، عن وظيفة liveADay. نتصل بها على الفور. هذه الوظيفة لها متغير محلي food. يتم أيضًا الإعلان عن الوظيفة فيه eat. ثم liveADayيتم استدعاء الوظيفة داخليا eat. نظرًا لأن الوظيفة eatموجودة داخل دالة liveADay، فإنها eat"ترى" جميع المتغيرات المعلنة فيها liveADay. هذا هو السبب في eatأن الوظيفة يمكنها قراءة قيمة المتغير food.



هذا يسمى الإغلاق.



نتحدث عن وجود إغلاق عندما eatتقرأ دالة (مثل ) أو تكتب قيمة متغير (مثل food) ، يتم الإعلان عنه خارجها (على سبيل المثال ، في دالة liveADay).



فكر في هذه الكلمات وأعد قراءتها. اختبر نفسك من خلال إيجاد ما نتحدث عنه في نموذج الكود الخاص بنا.



هذا مثال تم تقديمه في بداية المقال:



let users = ['Alice', 'Dan', 'Jessica'];
let query = 'A';
let user = users.filter(user => user.startsWith(query));


قد يكون من الأسهل ملاحظة الإغلاق بإعادة كتابة هذا المثال باستخدام تعبير دالة:



let users = ['Alice', 'Dan', 'Jessica'];
// 1.  query    
let query = 'A';
let user = users.filter(function(user) {
  // 2.     
  // 3.      query (    !)
  return user.startsWith(query);
});


عندما تصل دالة إلى متغير تم الإعلان عنه خارجها ، فإننا نسميها إغلاقًا. المصطلح نفسه يستخدم بشكل فضفاض بدرجة كافية. سيطلق بعض الأشخاص على الوظيفة المتداخلة نفسها ، الموضحة في المثال ، "الإغلاق". قد يشير البعض الآخر إلى موصّل متغير خارجي من خلال تسميته "إغلاق". في الممارسة العملية ، هذا لا يهم.



وظيفة استدعاء شبح



قد تبدو عمليات الإغلاق كمفهوم بسيط مخادع لك الآن. لكن هذا لا يعني أنهم يفتقرون إلى بعض الميزات غير الواضحة. إذا فكرت مليًا في حقيقة أن الوظيفة يمكنها قراءة وكتابة قيم المتغيرات خارجها ، فقد اتضح أن لهذا عواقب وخيمة إلى حد ما.



على سبيل المثال ، هذا يعني أن مثل هذه المتغيرات "ستعيش" طالما يمكن استدعاء دالة متداخلة داخل دالة أخرى.



function liveADay() {
  let food = 'cheese';
  function eat() {
    console.log(food + ' is good');
  }
  //  eat   
  setTimeout(eat, 5000);
}
liveADay();


في هذا المثال ، foodهو متغير محلي داخل استدعاء دالة liveADay(). أريد فقط أن أقرر أن هذا المتغير "سيختفي" بعد الخروج من الوظيفة ، ولن يعود ليطاردنا مثل الشبح.



لكن في الوظيفة ، liveADayنطلب من المتصفح استدعاء الوظيفة eatبعد خمس ثوانٍ. وهذه الوظيفة تقرأ قيمة المتغير food. نتيجة لذلك ، اتضح أن محرك JavaScript يحتاج إلى إبقاء المتغير foodالمرتبط بهذه المكالمة على قيد الحياة liveADay()حتى يتم استدعاء الوظيفة eat.



بهذا المعنى ، يمكن اعتبار عمليات الإغلاق "أشباح" لاستدعاءات الوظائف السابقة ، أو "ذكريات" لمثل هذه المكالمات. على الرغم من تنفيذ الوظيفةliveADay()انتهت منذ فترة طويلة ، يجب أن تستمر المتغيرات المعلنة فيه في الوجود طالما eatيمكن استدعاء الوظيفة المتداخلة . لحسن الحظ ، تهتم JavaScript بهذه الآليات ، لذلك لا نحتاج إلى القيام بأي شيء خاص في هذه المواقف.



لماذا تسمى "عمليات الإغلاق" بهذه الطريقة؟



قد تتساءل لماذا تسمى عمليات الإغلاق بهذه الطريقة. السبب في ذلك تاريخي بشكل أساسي. قد يقول أي شخص على دراية بلغة الكمبيوتر أن تعبيرًا مثل هذا user => user.startsWith(query)له "ارتباط مفتوح". بمعنى آخر ، يتضح من هذا التعبير ما هي user(المعلمة) ، ولكن عند النظر إليها بمعزل عن غيرها ، لا يتضح ما هي query. عندما نقول أنه ، في الواقع ، queryمتغير تم الإعلان عنه خارج الوظيفة ، فإننا "نغلق" هذا الربط المفتوح. بعبارة أخرى ، نحصل على إغلاق.



لا يتم تنفيذ عمليات الإغلاق في جميع لغات البرمجة. على سبيل المثال ، في بعض اللغات ، مثل لغة C ، لا يمكنك استخدام الوظائف المتداخلة على الإطلاق. نتيجة لذلك ، يمكن أن تعمل الوظيفة فقط مع المتغيرات المحلية الخاصة بها أو مع المتغيرات العامة. ومع ذلك ، لا يوجد موقف يمكن فيه الوصول إلى المتغيرات المحلية للوظيفة الأم. هذا هو في الواقع قيد غير سارة للغاية.



هناك أيضًا لغات مثل Rust التي تنفذ عمليات الإغلاق. لكنهم يستخدمون صياغة مختلفة لوصف عمليات الإغلاق والوظائف العادية. نتيجة لذلك ، إذا كنت بحاجة إلى قراءة قيمة متغير خارج دالة ، فعند استخدام Rust ، فأنت بحاجة إلى استخدام بنية خاصة. والسبب في ذلك هو أن استخدام الإغلاق قد يتطلب الآليات الداخلية للغة لتخزين المتغيرات الخارجية (تسمى "البيئة") حتى بعد اكتمال استدعاء الوظيفة. هذا الحمل الإضافي على النظام مقبول في JavaScript ، ولكن يمكن أن يسبب مشاكل في الأداء عند استخدامه بلغات منخفضة المستوى.



الآن ، أتمنى أن تفهم مفهوم عمليات الإغلاق في JavaScript.



هل تواجه صعوبة في فهم مفاهيم JavaScript؟






All Articles