function - كيف تعمل إغلاق JavaScript؟




variables scope closures (25)

كيف تشرح إغلاق JavaScript لشخص ما لديه معرفة بالمفاهيم التي يتكون منها (على سبيل المثال ، الوظائف والمتغيرات وما شابه) ، ولكن لا يفهم الإغلاق بأنفسهم؟

لقد رأيت مثال المخطط المعطى على ويكيبيديا ، ولكن للأسف لم يساعد.


Answers

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

ونعم ، أنا أوصي به لعمر 6 سنوات - إذا كان عمره 6 سنوات يتعلم عن الإغلاق ، فمن المنطقي انهم على استعداد لفهم التفسير الموجزة والبسيطة المنصوص عليها في المادة.


لا أفهم لماذا تكون الإجابات معقدة للغاية هنا.

هنا إغلاق:

var a = 42;

function b() { return a; }

نعم فعلا. ربما تستخدم ذلك عدة مرات في اليوم.


لا يوجد سبب للاعتقاد بأن عمليات الإغلاق هي اختراق تصميم معقد لمعالجة مشكلات معينة. لا ، إن الإغلاق يتعلق باستخدام متغير يأتي من نطاق أعلى من منظور المكان الذي تم فيه الإعلان عن الوظيفة (لم يتم تشغيلها) .

الآن ما يسمح لك أن تفعل يمكن أن يكون أكثر إثارة، انظر إجابات أخرى.


A إغلاق يشبه إلى حد كبير كائن. يتم إنشاء مثيل كلما قمت باستدعاء دالة.

إن نطاق الإغلاق في JavaScript هو معجم ، مما يعني أن كل ما هو موجود داخل الدالة التي ينتمي إليها الإغلاق ، لديه حق الوصول إلى أي متغير موجود فيه.

ويرد متغير في الإغلاق إذا كنت

  1. عينه مع var foo=1;أو
  2. اكتب فقط var foo;

إذا كانت وظيفة داخلية (دالة موجودة داخل وظيفة أخرى) تصل إلى مثل هذا المتغير دون تعريفه في نطاقه الخاص مع var ، فإنها تعدل محتوى المتغير في الإغلاق الخارجي .

A إغلاق outlives وقت التشغيل الدالة التي ولدت بها. إذا نجحت وظائف أخرى من الإغلاق / النطاق الذي تم تعريفها فيه (على سبيل المثال كقيم الإرجاع) ، فسيستمر ذلك في الإشارة إلى هذا الإغلاق .

مثال

function example(closure) {
  // define somevariable to live in the closure of example
  var somevariable = 'unchanged';

  return {
    change_to: function(value) {
      somevariable = value;
    },
    log: function(value) {
      console.log('somevariable of closure %s is: %s',
        closure, somevariable);
    }
  }
}

closure_one = example('one');
closure_two = example('two');

closure_one.log();
closure_two.log();
closure_one.change_to('some new value');
closure_one.log();
closure_two.log();

انتاج |

somevariable of closure one is: unchanged
somevariable of closure two is: unchanged
somevariable of closure one is: some new value
somevariable of closure two is: unchanged

إذا أخذنا هذه المسألة بجدية ، ينبغي أن نعرف ما هو القادر على بلوغ عمر 6 سنوات نموذجي ، على الرغم من الاعتراف ، بأن الشخص الذي يهتم بجافا سكريبت ليس نموذجيًا.

في مجال تنمية الطفولة: من 5 إلى 7 سنوات تقول:

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

يمكننا استخدام هذا المثال لشرح عمليات الإغلاق ، على النحو التالي:

المطبخ هو إغلاق يحتوي على متغير محلي يسمى trashBags . هناك وظيفة داخل المطبخ تسمى getTrashBag والتي تحصل على حقيبة قمامة واحدة getTrashBag .

يمكننا ترميز هذا في JavaScript مثل:

function makeKitchen() {
  var trashBags = ['A', 'B', 'C']; // only 3 at first

  return {
    getTrashBag: function() {
      return trashBags.pop();
    }
  };
}

var kitchen = makeKitchen();

console.log(kitchen.getTrashBag()); // returns trash bag C
console.log(kitchen.getTrashBag()); // returns trash bag B
console.log(kitchen.getTrashBag()); // returns trash bag A

مزيد من النقاط التي تشرح سبب الإغراءات المثيرة للاهتمام:

  • في كل مرة يتم استدعاء makeKitchen() ، يتم إنشاء إغلاق جديد باستخدام trashBags منفصلة خاصة به.
  • متغير trashBags محلي إلى داخل كل مطبخ ولا يمكن الوصول إليه بالخارج ، لكن الوظيفة الداخلية على الخاصية getTrashBag لها حق الوصول إليها.
  • تخلق كل مكالمة وظيفية إغلاقًا ، ولكن لن تكون هناك حاجة للحفاظ على الإغلاق ما لم يتم استدعاء وظيفة داخلية ، والتي يمكن الوصول إليها من داخل الإغلاق ، من خارج الإغلاق. إرجاع الكائن باستخدام الدالة getTrashBag هنا.

أنت تنام و تدع دان أنت تخبر دان بإحضار وحدة تحكم XBox واحدة.

دان يدعو بول. يطلب دان من بول إحضار جهاز تحكم واحد. كم عدد المتحكمين الذين تم إحضارهم إلى الحفلة؟

function sleepOver(howManyControllersToBring) {

    var numberOfDansControllers = howManyControllersToBring;

    return function danInvitedPaul(numberOfPaulsControllers) {
        var totalControllers = numberOfDansControllers + numberOfPaulsControllers;
        return totalControllers;
    }
}

var howManyControllersToBring = 1;

var inviteDan = sleepOver(howManyControllersToBring);

// The only reason Paul was invited is because Dan was invited. 
// So we set Paul's invitation = Dan's invitation.

var danInvitedPaul = inviteDan(howManyControllersToBring);

alert("There were " + danInvitedPaul + " controllers brought to the party.");

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


ويكيبيديا حول الإغلاق :

في علم الكمبيوتر ، يعتبر الإغلاق دالة مع بيئة مرجعية للأسماء غير المحلية (المتغيرات المجانية) لهذه الوظيفة.

من الناحية الفنية ، في JavaScript ، كل وظيفة هي إغلاق . لديه دائما الوصول إلى المتغيرات المحددة في النطاق المحيط.

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

غالبًا ما تستخدم عمليات الإغلاق لإنشاء وظائف مع بعض البيانات الخاصة المخفية (ولكنها ليست الحالة دائمًا).

var db = (function() {
    // Create a hidden object, which will hold the data
    // it's inaccessible from the outside.
    var data = {};

    // Make a function, which will provide some access to the data.
    return function(key, val) {
        if (val === undefined) { return data[key] } // Get
        else { return data[key] = val } // Set
    }
    // We are calling the anonymous surrounding function,
    // returning the above inner function, which is a closure.
})();

db('x')    // -> undefined
db('x', 1) // Set x to 1
db('x')    // -> 1
// It's impossible to access the data object itself.
// We are able to get or set individual it.

نظم الإدارة البيئية

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


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

function sing(person) {

    var firstPart = "There was " + person + " who swallowed ";

    var fly = function() {
        var creature = "a fly";
        var result = "Perhaps she'll die";
        alert(firstPart + creature + "\n" + result);
    };

    var spider = function() {
        var creature = "a spider";
        var result = "that wiggled and jiggled and tickled inside her";
        alert(firstPart + creature + "\n" + result);
    };

    var bird = function() {
        var creature = "a bird";
        var result = "How absurd!";
        alert(firstPart + creature + "\n" + result);
    };

    var cat = function() {
        var creature = "a cat";
        var result = "Imagine That!";
        alert(firstPart + creature + "\n" + result);
    };

    fly();
    spider();
    bird();
    cat();
}

var person="an old lady";

sing(person);

تعليمات

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

CODE: كل الكتابة أعلاه تسمى الكود . مكتوب بلغة JavaScript.

JAVASCRIPT: JavaScript هي لغة. مثل الإنجليزية أو الفرنسية أو الصينية هي لغات. هناك الكثير من اللغات التي تفهمها أجهزة الكمبيوتر وغيرها من المعالجات الإلكترونية. لكي يفهم جافا سكريبت بواسطة الكمبيوتر ، يحتاج إلى مترجم. تخيل لو أن المعلم الذي يتحدث اللغة الروسية فقط يأتي لتعليم صفك في المدرسة. عندما يقول المعلم "все садятся" ، لن يفهم الفصل. ولكن لحسن الحظ لديك تلميذ روسي في صفك يخبر الجميع أن هذا يعني أن "الجميع يجلسون" - لذلك كل ما عليك فعله. يشبه الفصل الكمبيوتر ، والتلميذ الروسي هو المترجم. بالنسبة لجافا سكريبت ، يُعرف المترجم الأكثر شيوعًا بالمتصفح.

BROWSER: عند الاتصال بالإنترنت على جهاز كمبيوتر أو جهاز لوحي أو هاتف لزيارة موقع ويب ، فإنك تستخدم متصفحًا. من الأمثلة التي قد تعرفها Internet Explorer و Chrome و Firefox و Safari. يمكن للمتصفح فهم جافا سكريبت وإخبار الكمبيوتر بما يحتاج القيام به. تسمى تعليمات JavaScript بالوظائف.

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

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

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

عادة ما تحتوي الدالة على اسم وأقواس وقوس. مثله:

function cookMeal() {  /*  STUFF INSIDE THE FUNCTION  */  }

لاحظ أن /*...*/و //إيقاف رمز يتم قراءتها من قبل المتصفح.

NAME: يمكنك الاتصال بوظيفة حول أي كلمة تريدها. المثال "cookMeal" نموذجي في ضم كلمتين معًا وإعطاء الحرف الثاني حرفًا كبيرًا في البداية - لكن هذا ليس ضروريًا. لا يمكن أن يحتوي على مساحة فيه ، ولا يمكن أن يكون رقمًا من تلقاء نفسه.

PARENTHESES: "Parentheses" أو ()هي صندوق الرسائل الموجود على باب مصنع وظيفة JavaScript أو صندوق بريد في الشارع لإرسال حزم المعلومات إلى المصنع. في بعض الأحيان قد يتم وضع علامة على صندوق البريد على سبيل المثالcookMeal(you, me, yourFriend, myFriend, fridge, dinnerTime) ، وفي هذه الحالة تعرف ما هي البيانات التي لديك لإعطاءها .

السباق: "الأقواس" التي تشبه هذه {}هي النوافذ الملونة في مصنعنا. من داخل المصنع يمكنك رؤية ذلك ، ولكن من الخارج لا يمكنك رؤيته.

الكود الطويل المثال أعلاه

تبدأ الشفرة الخاصة بنا بوظيفة الكلمة ، لذلك نعرف أنها واحدة! ثم اسم الدالة يغني - هذا هو وصفي الخاص عن وظيفة ما. ثم أقواس () . الأقواس دائما هناك لوظيفة. في بعض الأحيان أنها فارغة، وأحيانا لديهم شيء في هذا واحد لديه كلمة في: (person). بعد هذا يوجد قوس مثل هذا {. هذا يدل على بداية وظيفة الغناء () . لديها شريك الذي يمثل نهاية الغناء () مثل هذا}

function sing(person) {  /* STUFF INSIDE THE FUNCTION */  }

لذلك قد يكون لهذه الوظيفة علاقة بالغناء وقد تحتاج إلى بعض البيانات عن شخص ما. لديه تعليمات داخل للقيام بشيء مع تلك البيانات.

الآن ، بعد وظيفة الغناء () ، بالقرب من نهاية الرمز هو الخط

var person="an old lady";

المتغير : الحروف var تعني "المتغير". المتغير يشبه المغلف. من الخارج هذا المظروف يحمل علامة "شخص". في داخله يحتوي على ورقة من الورقة مع المعلومات التي تحتاجها وظيفتنا ، وبعض الحروف والمسافات انضمت معا مثل قطعة من الخيط (يطلق عليه سلسلة) التي تجعل عبارة قراءة "سيدة تبلغ من العمر". يمكن أن يحتوي غلافنا على أنواع أخرى من الأشياء مثل الأرقام (تسمى الأعداد الصحيحة) ، والتعليمات (تسمى الوظائف) ، والقوائم (تسمى المصفوفات ). نظرًا لأن هذا المتغير مكتوب خارج كل الأقواس {}، ولأنك تستطيع أن ترى من خلال النوافذ الملوّنة عندما تكون داخل الأقواس ، يمكن رؤية هذا المتغير من أي مكان في الكود. نحن نسمي هذا "المتغير الشامل".

متغير عالمي: الشخص متغير عالمي ، بمعنى أنه إذا قمت بتغيير قيمته من "سيدة عجوز" إلى "شاب" ، فسيظل الشخص شابا حتى تقرر تغييره مرة أخرى وأي وظيفة أخرى في يمكن للرمز أن يرى أنه شاب. اضغط على F12الزر أو انظر إلى إعدادات الخيارات لفتح وحدة تحكم مطور المتصفح واكتب "الشخص" لمعرفة قيمة هذه القيمة. اكتب person="a young man"لتغييره ثم اكتب "الشخص" مرة أخرى لمعرفة أنه قد تغير.

بعد هذا لدينا الخط

sing(person);

هذا الخط يدعو إلى الوظيفة ، كما لو كان يدعو كلبًا

"هيا الغناء ، تعال واحصل على شخص !"

عندما يحمّل المتصفح شفرة جافا سكريبت بلغ هذا السطر ، سيبدأ في تشغيله. أضع السطر في النهاية للتأكد من أن المتصفح يحتوي على جميع المعلومات التي يحتاجها لتشغيلها.

وظائف تحديد الإجراءات - الوظيفة الرئيسية هي عن الغناء. يحتوي على متغير يسمى firstPart والذي ينطبق على الغناء عن الشخص الذي ينطبق على كل من آيات الأغنية: "كان هناك + + شخص +" الذي ابتلع ". إذا قمت بكتابة firstPart في وحدة التحكم ، فلن تحصل على إجابة لأنه يتم تأمين المتغير في وظيفة - لا يمكن للمتصفح أن يرى داخل النوافذ الملونة من الأقواس.

إغلاق: الإغلاق هي الوظائف الصغيرة الموجودة داخل الدالة sing () كبيرة . المصانع الصغيرة داخل المصنع الكبير. لديهم كل الأقواس الخاصة بهم مما يعني أن المتغيرات داخلها لا يمكن رؤيتها من الخارج. هذا هو السبب في أسماء المتغيرات ( مخلوق و نتيجة ) يمكن أن يتكرر في عمليات الإغلاق ولكن مع قيم مختلفة. إذا قمت بكتابة أسماء المتغيرات هذه في نافذة وحدة التحكم ، فلن تحصل على قيمتها لأنها مخفية بواسطة طبقتين من النوافذ الملونة.

جميع الإغلاق يعرف ما هو متغير الدالة الغناء () الذي يدعى firstPart ، لأنه يمكن أن يرى من النوافذ الملونة.

بعد الاغلاق تأتي الخطوط

fly();
spider();
bird();
cat();

ستقوم الدالة sing () باستدعاء كل من هذه الوظائف بالترتيب الذي تم منحها له. ثم سيتم تنفيذ عمل الغناء ().


هل يمكن أن توضح عمليات الإغلاق لعمر 5 سنوات؟ *

ما زلت أعتقد أن تفسير Google يعمل جيدًا وموجزًا:

/*
*    When a function is defined in another function and it
*    has access to the outer function's context even after
*    the outer function returns.
*
* An important concept to learn in JavaScript.
*/

function outerFunction(someNum) {
    var someString = 'Hey!';
    var content = document.getElementById('content');
    function innerFunction() {
        content.innerHTML = someNum + ': ' + someString;
        content = null; // Internet Explorer memory leak for DOM reference
    }
    innerFunction();
}

outerFunction(1);​

* سؤال # AC


أعلم أن هناك الكثير من الحلول بالفعل ، ولكن أعتقد أن هذا البرنامج النصي الصغير والبسيط يمكن أن يكون مفيدًا لإثبات المفهوم:

// makeSequencer will return a "sequencer" function
var makeSequencer = function() {
    var _count = 0; // not accessible outside this function
    var sequencer = function () {
        return _count++;
    }
    return sequencer;
}

var fnext = makeSequencer();
var v0 = fnext();     // v0 = 0;
var v1 = fnext();     // v1 = 1;
var vz = fnext._count // vz = undefined

سيتذكر الأطفال دائمًا الأسرار التي شاركوها مع والديهم ، حتى بعد ذهاب آبائهم. هذا هو ما يغلق للوظائف.

أسرار وظائف جافا سكريبت هي المتغيرات الخاصة

var parent = function() {
 var name = "Mary"; // secret
}

في كل مرة تسمونها ، يتم إنشاء "الاسم" المتغير المحلي ويعطى الاسم "ماري". وفي كل مرة تفقد الدالة المتغيّرة ، يتم نسيان الاسم.

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

ولكن ، في JavaScript هناك هذا الشيء المميز للغاية الذي يمكن أن تخلقه الوظائف التي يتم إنشاؤها داخل وظائف أخرى ، وكذلك معرفة المتغيرات المحلية لآبائهم والاحتفاظ بهم طالما أنهم يعيشون.

var parent = function() {
  var name = "Mary";
  var child = function(childName) {
    // I can also see that "name" is "Mary"
  }
}

لذلك ، طالما أننا في الوظيفة الأم ، فإنه يمكن إنشاء وظيفة واحدة أو أكثر من وظائف الطفل التي تشارك المتغيرات السرية من مكان سري.

لكن الشيء المحزن هو ، إذا كان الطفل هو أيضا متغير خاص من وظيفته الأم ، فإنه سيموت أيضا عندما ينتهي الوالد ، وتموت الأسرار معهم.

لكي يعيش الطفل ، يجب عليه المغادرة قبل فوات الأوان

var parent = function() {
  var name = "Mary";
  var child = function(childName) {
    return "My name is " + childName  +", child of " + name; 
  }
  return child; // child leaves the parent ->
}
var child = parent(); // < - and here it is outside 

والآن ، على الرغم من أن ماري "لم تعد تعمل" ، فإن ذكرياتها لا تضيع وسيتذكر طفلها دائمًا اسمها وأسرارها الأخرى التي شاركوها خلال وقتهم معًا.

لذا ، إذا كنت تتصل بالطفل "أليس" ، فستجيب

child("Alice") => "My name is Alice, child of Mary"

هذا كل ما هناك ليقول.


مثال للنقطة الأولى بواسطة dlaliberte:

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

var i;
function foo(x) {
    var tmp = 3;
    i = function (y) {
        console.log(x + y + (++tmp));
    }
}
foo(2);
i(3);

عمليات الإغلاق يصعب تفسيرها لأنها تستخدم في صنع بعض الأعمال السلوكية التي يتوقع الجميع أن يعملوا بها على أي حال. أجد أن أفضل طريقة لشرحهم (وطريقة تعلمهم ما يفعلونه) هي تخيل الوضع بدونهم:

    var bind = function(x) {
        return function(y) { return x + y; };
    }
    
    var plus5 = bind(5);
    console.log(plus5(3));

ما الذي يحدث هنا إذا لم تكن JavaScript تعرف الإغلاق؟ ما عليك سوى استبدال المكالمة الموجودة في السطر الأخير من خلال نص أسلوبها (وهو ما تقوم به الوظيفة بشكل أساسي) وتحصل على ما يلي:

console.log(x + 3);

الآن ، أين هو تعريف x ؟ لم نحدده في النطاق الحالي. الحل الوحيد هو السماح لـ plus5 بحمل نطاقه (أو بالأحرى نطاقه الأم). بهذه الطريقة ، يتم تعريف x بشكل جيد وهي مرتبطة بالقيمة 5.


كيف سأشرح ذلك لعمر ست سنوات:

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


إغلاق جافا سكريبت للمبتدئين

مقدم من موريس في الثلاثاء ، 2006-02-21 10:19. تم تحريره من قبل المجتمع.

الإغلاقات ليست سحرية

توضح هذه الصفحة عمليات الإغلاق حتى يتمكن المبرمج من فهمها - باستخدام شفرة JavaScript البرمجية العاملة. انها ليست للمعلمين أو المبرمجين الوظيفية.

ليس من الصعب فهم عمليات الإغلاق ما أن يتم تفكيك الفكرة الأساسية. ومع ذلك ، فمن المستحيل فهمها من خلال قراءة أي تفسيرات نظرية أو أكاديمية المنحى!

هذه المقالة مخصصة للمبرمجين الذين لديهم بعض الخبرة في البرمجة بلغة سائدة ، والذين يمكنهم قراءة وظيفة JavaScript التالية:

function sayHello(name) {
  var text = 'Hello ' + name;
  var say = function() { console.log(text); }
  say();
}
sayHello('Joe');

ملخصان موجزان

  • عندما تعلن دالة (foo) عن وظائف أخرى (bar و baz) ، فإن عائلة المتغيرات المحلية التي يتم إنشاؤها في foo لا يتم إتلافها عند خروج الدالة. المتغيرات تصبح مجرد غير مرئية للعالم الخارجي. وبالتالي يمكن لـ Foo إرجاع شريط المهام وباز ، ويمكنهم الاستمرار في القراءة والكتابة والتواصل مع بعضهم البعض من خلال عائلة المتغيبين ("الإغلاق") التي لا يستطيع أي شخص آخر التدخل بها ، ولا حتى شخص يدعو فو مرة أخرى في المستقبل.

  • الإغلاق هو أحد طرق دعم وظائف الدرجة الأولى ؛ هو تعبير يمكن أن يشير إلى المتغيرات ضمن نطاقه (عندما تم الإعلان عنه لأول مرة) ، أو أن يتم تعيينه لمتغير ، أو تمريره كوسيطة لوظيفة ، أو إعادته كنتيجة دالة.

مثال على الإغلاق

تقوم التعليمة البرمجية التالية بإرجاع مرجع إلى دالة:

function sayHello2(name) {
  var text = 'Hello ' + name; // Local variable
  var say = function() { console.log(text); }
  return say;
}
var say2 = sayHello2('Bob');
say2(); // logs "Hello Bob"

سوف يفهم معظم مبرمجي JavaScript كيف يتم إرجاع مرجع إلى دالة إلى متغير ( say2 ) في التعليمة البرمجية أعلاه. إذا لم تقم بذلك ، فأنت بحاجة إلى النظر في ذلك قبل أن تتعلم الإغلاق. say2 يستخدم C قد يفكر في الدالة على أنها say2 مؤشر إلى دالة ، وأن المتغيرات say و say2 كانت كل مؤشر إلى دالة.

يوجد اختلاف حرج بين مؤشر C إلى دالة ومرجع JavaScript إلى دالة. في JavaScript ، يمكنك التفكير في متغير مرجع دالة على أنه يحتوي على مؤشر إلى وظيفة بالإضافة إلى مؤشر مخفي إلى الإغلاق.

يحتوي التعليمة البرمجية الموجودة أعلاه على إغلاق لأن الدالة function() { console.log(text); } المجهولة function() { console.log(text); } function() { console.log(text); } أعلن داخل دالة أخرى ، sayHello2() في هذا المثال. في JavaScript ، إذا كنت تستخدم الكلمة الأساسية function داخل دالة أخرى ، فأنت تقوم بإنشاء إغلاق.

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

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

function() { console.log(text); } // Output of say2.toString();

بالنظر إلى ناتج say2.toString() ، يمكننا أن نرى أن الرمز يشير إلى text المتغير. يمكن أن تشير الدالة المجهولة إلى text يحمل قيمة 'Hello Bob' لأن المتغيرات المحلية لـ sayHello2() قد تم حفظها سراً في حالة إغلاق.

إن العبقرية هي أنه في جافا سكريبت ، يوجد مرجعية دالة لديها أيضًا مرجعًا سرىًا للإغلاق الذي تم إنشاؤه فيه - على غرار كيف يكون المفوضون مؤشر أسلوب بالإضافة إلى مرجع سري إلى كائن.

مزيد من الأمثلة

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

مثال 3

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

function say667() {
  // Local variable that ends up within closure
  var num = 42;
  var say = function() { console.log(num); }
  num++;
  return say;
}
var sayNumber = say667();
sayNumber(); // logs 43

مثال 4

جميع الوظائف العالمية الثلاث لها مرجع مشترك إلى نفس الإغلاق لأنه يتم الإعلان عنها جميعًا في مكالمة واحدة إلى setupSomeGlobals() .

var gLogNumber, gIncreaseNumber, gSetNumber;
function setupSomeGlobals() {
  // Local variable that ends up within closure
  var num = 42;
  // Store some references to functions as global variables
  gLogNumber = function() { console.log(num); }
  gIncreaseNumber = function() { num++; }
  gSetNumber = function(x) { num = x; }
}

setupSomeGlobals();
gIncreaseNumber();
gLogNumber(); // 43
gSetNumber(5);
gLogNumber(); // 5

var oldLog = gLogNumber;

setupSomeGlobals();
gLogNumber(); // 42

oldLog() // 5

وقد شاركت الوظائف الثلاث في الوصول إلى نفس الإغلاق - المتغيرات المحلية من setupSomeGlobals() عندما تم تعريف الوظائف الثلاث.

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

مثال 5

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

function sayAlice() {
    var say = function() { console.log(alice); }
    // Local variable that ends up within closure
    var alice = 'Hello Alice';
    return say;
}
sayAlice()();// logs "Hello Alice"

خادع: لاحظ أيضًا أن المتغير say هو أيضًا داخل الإغلاق ، ويمكن الوصول إليه عن طريق أي وظيفة أخرى قد يتم الإعلان عنها ضمن sayAlice() ، أو يمكن الوصول إليها بشكل متكرر داخل الوظيفة الداخلية.

مثال 6

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

تحتاج إلى فهم ميزة "الرفع المتغير" في Javascript لفهم هذا المثال.

function buildList(list) {
    var result = [];
    for (var i = 0; i < list.length; i++) {
        var item = 'item' + i;
        result.push( function() {console.log(item + ' ' + list[i])} );
    }
    return result;
}

function testList() {
    var fnlist = buildList([1,2,3]);
    // Using j only to help prevent confusion -- could use i.
    for (var j = 0; j < fnlist.length; j++) {
        fnlist[j]();
    }
}

 testList() //logs "item2 undefined" 3 times

result.push( function() {console.log(item + ' ' + list[i])} السطر result.push( function() {console.log(item + ' ' + list[i])} إشارة إلى وظيفة مجهولة ثلاث مرات إلى صفيف result.push( function() {console.log(item + ' ' + list[i])} لم تكن على دراية بالوظائف المجهولة ففكر في انها مثل:

pointer = function() {console.log(item + ' ' + list[i])};
result.push(pointer);

لاحظ أنه عند تشغيل المثال ، يتم تسجيل "item2 undefined" ثلاث مرات! هذا لأنه مثل الأمثلة السابقة فقط ، يوجد إغلاق واحد فقط للمتغيرات المحلية لـ buildList (وهي result ، i و item ). عندما يتم استدعاء الوظائف المجهولة على السطر fnlist[j]() ؛ جميعهم يستخدمون نفس الإغلاق الفردي ، ويستخدمون القيمة الحالية لـ i 'item2' ضمن ذلك الإغلاق الواحد (حيث يكون i قيمة 3 لأن الحلقة قد اكتملت ، item له قيمة 'item2' ). لاحظ أننا نقوم بفهرسة من 0 لذلك يحتوي item على قيمة item2 . و i ++ سيزيد i إلى القيمة 3 .

قد يكون من المفيد معرفة ما يحدث عند استخدام الإعلان على مستوى الكتلة item المتغير (عبر الكلمة المفتاحية) بدلاً من تعريف متغير متغير للوظيفة عبر الكلمة الرئيسية var . إذا تم إجراء هذا التغيير ، فإن كل دالة مجهولة في result المصفوفة لها إغلاق خاص بها ؛ عندما يتم تشغيل المثال الإخراج كما يلي:

item0 undefined
item1 undefined
item2 undefined

إذا تم تعريف المتغير i أيضًا باستخدام let بدلاً من var ، فإن الناتج يكون:

item0 1
item1 2
item2 3

مثال 7

في هذا المثال الأخير ، تؤدي كل مكالمة إلى الوظيفة الرئيسية إلى إغلاق منفصل.

function newClosure(someNum, someRef) {
    // Local variables that end up within closure
    var num = someNum;
    var anArray = [1,2,3];
    var ref = someRef;
    return function(x) {
        num += x;
        anArray.push(num);
        console.log('num: ' + num +
            '; anArray: ' + anArray.toString() +
            '; ref.someVar: ' + ref.someVar + ';');
      }
}
obj = {someVar: 4};
fn1 = newClosure(4, obj);
fn2 = newClosure(5, obj);
fn1(1); // num: 5; anArray: 1,2,3,5; ref.someVar: 4;
fn2(1); // num: 6; anArray: 1,2,3,6; ref.someVar: 4;
obj.someVar++;
fn1(2); // num: 7; anArray: 1,2,3,5,7; ref.someVar: 5;
fn2(2); // num: 8; anArray: 1,2,3,6,8; ref.someVar: 5;

ملخص

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

النقاط النهائية:

  • كلما استخدمت function داخل function أخرى ، يتم استخدام الإغلاق.
  • عندما تستخدم eval() داخل إحدى الوظائف ، يتم استخدام إغلاق. يمكن أن يشير النص الذي تقوم eval إلى المتغيرات المحلية للدالة ، وفي داخل eval يمكنك أيضًا إنشاء متغيرات محلية جديدة باستخدام eval('var foo = …')
  • عند استخدام new Function(…) ( مُنشئ الدالة ) داخل دالة ، لا يقوم بإنشاء إغلاق. (لا يمكن للوظيفة الجديدة الرجوع إلى المتغيرات المحلية للدالة الخارجية.)
  • يشبه الإغلاق في JavaScript الاحتفاظ بنسخة من جميع المتغيرات المحلية ، تمامًا كما كانت عند خروج إحدى الوظائف.
  • من الأفضل التفكير في أن الإغلاق يتم إنشاؤه دائمًا كمدخل لوظيفة فقط ، ويتم إضافة المتغيرات المحلية إلى هذا الإغلاق.
  • يتم الاحتفاظ بمجموعة جديدة من المتغيرات المحلية في كل مرة يتم فيها استدعاء دالة مع إغلاق (نظراً لأن الدالة تحتوي على تصريح دالة بداخلها ، ويتم إما إرجاع إشارة إلى تلك الدالة الداخلية أو الاحتفاظ بمرجع خارجي لها بطريقة ما ).
  • قد يبدو أن هناك وظيفتين لهما نفس النص الأصلي ، ولكنهما مختلفتان تمامًا بسبب إغلاقهما "المخفي". لا أظن أن شفرة جافا سكريبت يمكن أن تكتشف في الواقع ما إذا كان مرجع الوظيفة له إغلاق أم لا.
  • إذا كنت تحاول إجراء أي تعديلات ديناميكية على التعليمات البرمجية المصدر (على سبيل المثال: myFunction = Function(myFunction.toString().replace(/Hello/,'Hola')); ) ، لن يعمل إذا كان myFunction هو إغلاق ( بالطبع ، لن تفكر أبداً في إجراء استبدال سلسلة التعليمات البرمجية المصدر في وقت التشغيل ، ولكن ...).
  • من الممكن الحصول على إعلانات دالة ضمن إعلانات الوظائف ضمن الدالات & mdash ، ويمكنك الحصول على الإغلاق في أكثر من مستوى واحد.
  • أعتقد عادة أن الإغلاق هو مصطلح لكل من الوظيفة جنبا إلى جنب مع المتغيرات التي يتم التقاطها. لاحظ أنني لا أستخدم هذا التعريف في هذه المقالة!
  • أظن أن الإغلاق في JavaScript يختلف عن تلك الموجودة عادة في اللغات الوظيفية.

الروابط

شكر

إذا كنت قد علمت للتو عمليات الإغلاق (هنا أو في أي مكان آخر!) ، فأنا مهتمًا بأي تعليقات منك حول أي تغييرات قد تقترحها قد تجعل هذه المقالة أوضح. إرسال بريد إلكتروني إلى morrisjohns.com (morris_closure @). يرجى ملاحظة أنني لست غورو على جافا سكريبت - ولا على الإغلاق.

يمكن العثور على المشاركة الأصلية بواسطة Morris في أرشيف الإنترنت .


عندما ترى الكلمة الرئيسية للدالة ضمن وظيفة أخرى ، فإن الوظيفة الداخلية يمكنها الوصول إلى المتغيرات في الوظيفة الخارجية.

function foo(x) {
  var tmp = 3;

  function bar(y) {
    console.log(x + y + (++tmp)); // will log 16
  }

  bar(10);
}

foo(2);

سيُسجل هذا دائمًا 16 ، لأن bar يمكنه الوصول إلى x الذي تم تعريفه كوسيطة foo ، ويمكنه أيضًا الوصول إلى tmp من foo .

هذا هو الاغلاق. لا تحتاج الدالة إلى العودة لكي يتم تسميتها بإغلاق. ببساطة الوصول إلى المتغيرات خارج نطاق معجمك الفوري يخلق إغلاق .

function foo(x) {
  var tmp = 3;

  return function (y) {
    console.log(x + y + (++tmp)); // will also log 16
  }
}

var bar = foo(2); // bar is now a closure.
bar(10);

ستقوم الدالة أعلاه بتسجيل 16 ، لأن bar لا يزال يشير إلى x و tmp ، على الرغم من أنه لم يعد مباشرة داخل النطاق.

ومع ذلك ، بما أن tmp لا يزال معلقًا داخل إغلاق bar ، فإنه يتم أيضًا زيادته. سيتم زيادتها في كل مرة تقوم فيها بالاتصال bar .

أبسط مثال على الإغلاق هو:

var a = 10;

function test() {
  console.log(a); // will output 10
  console.log(b); // will output 6
}
var b = 6;
test();

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

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

هنا الرقم x هو رقم حرفي. كما هو الحال مع القيم الحرفية الأخرى في JavaScript ، عندما يتم استدعاء foo ، يتم نسخ الرقم x إلى foo كوسيطة x .

من ناحية أخرى ، تستخدم JavaScript دائمًا المراجع عند التعامل مع الكائنات. إذا قلنا ، اتصلت بـ foo مع كائن ، فإن الإغلاق الذي يعود إليه سيشير إلى ذلك الكائن الأصلي!

function foo(x) {
  var tmp = 3;

  return function (y) {
    console.log(x + y + tmp);
    x.memb = x.memb ? x.memb + 1 : 1;
    console.log(x.memb);
  }
}

var age = new Number(2);
var bar = foo(age); // bar is now a closure referencing age.
bar(10);

كما هو متوقع ، سيتم كل استدعاء bar(10) زيادة x.memb . ما قد لا يكون متوقعًا ، هو أن x يشير ببساطة إلى نفس الكائن مثل متغير age ! بعد بضعة مكالمات bar ، age.memb سيكون 2! هذا المرجع هو أساس تسرب الذاكرة مع كائنات HTML.


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

عمليات الإغلاق تمت بشكل صحيح:

console.log('CLOSURES DONE RIGHT');

var arr = [];

function createClosure(n) {
    return function () {
        return 'n = ' + n;
    }
}

for (var index = 0; index < 10; index++) {
    arr[index] = createClosure(index);
}

for (var index in arr) {
    console.log(arr[index]());
}
  • في التعليمة البرمجية المذكورة أعلاه createClosure(n)يتم استدعاء في كل تكرار للحلقة. لاحظ أني سمّيت المتغير nلإبراز أنه متغير جديد تم إنشاؤه في نطاق وظيفي جديد وليس المتغير نفسه indexالذي يرتبط بالنطاق الخارجي.

  • هذا يخلق نطاق جديد nوملزم لهذا النطاق ؛ هذا يعني أن لدينا 10 نطاقات منفصلة ، واحدة لكل تكرار.

  • createClosure(n) بإرجاع دالة تقوم بإرجاع n داخل هذا النطاق.

  • داخل كل نطاق nيرتبط بأي قيمة لديه عندما createClosure(n)تم استدعاؤه ، لذا فإن الدالة المتداخلة التي يتم إرجاعها ستعود دائمًا بقيمة nما createClosure(n)تم احتوائه عندما تم استدعاؤه.

عمليات الإغلاق تمت بشكل خاطئ:

console.log('CLOSURES DONE WRONG');

function createClosureArray() {
    var badArr = [];

    for (var index = 0; index < 10; index++) {
        badArr[index] = function () {
            return 'n = ' + index;
        };
    }
    return badArr;
}

var badArr = createClosureArray();

for (var index in badArr) {
    console.log(badArr[index]());
}
  • في التعليمة البرمجية المذكورة أعلاه ، تم نقل الحلقة داخل createClosureArray()الدالة والدالة الآن فقط بإرجاع مجموعة مكتملة ، والتي تبدو للوهلة الأولى أكثر بديهية.

  • ما قد لا يكون واضحًا هو أنه createClosureArray()ما لم يتم استدعاؤه إلا بعد إنشاء نطاق واحد فقط لهذه الوظيفة بدلاً من واحد لكل تكرار للحلقة.

  • ضمن هذه الوظيفة indexيتم تعريف متغير مسمى . تعمل الحلقة وتضيف وظائف إلى الصفيف الذي يرجع index. ملاحظة indexيتم تعريفها داخل createClosureArrayالوظيفة التي يتم استدعاؤها مرة واحدة فقط.

  • نظرًا لوجود نطاق واحد فقط داخل createClosureArray()الدالة ، indexيتم ربطه فقط بقيمة داخل هذا النطاق. وبعبارة أخرى ، في كل مرة تقوم فيها الحلقة بتغيير قيمة index، فإنها تغيرها لكل ما يشير إليها ضمن هذا النطاق.

  • ترجع جميع الدالات المضافة إلى الصفيف indexمتغير SAME من النطاق الأصلي حيث تم تعريفه بدلاً من 10 منها مختلفة من 10 نطاقات مختلفة مثل المثال الأول. النتيجة النهائية هي أن كل الدالات العشرة تقوم بإرجاع نفس المتغير من نفس النطاق.

  • بعد الانتهاء من الحلقة والانتهاء indexمن تعديلها ، كانت القيمة النهائية 10 ، وبالتالي فإن كل دالة تُضاف إلى الصفيف تُرجع قيمة indexالمتغير الوحيد الذي تم تعيينه الآن على 10.

نتيجة

CLOSURES DET RIGHT
n = 0
n = 1
n = 2
n = 3
n = 4
n = 5
n = 6
n = 7
n = 8
n = 9

CLOSURES Done WRONG
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10


حسنًا ، عندما أتحدث مع طفل في السادسة من العمر ، ربما أستخدم الرابط التالي.

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

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

لطفل متقدم أود أن أضع شيئا مثل ما يلي. إنها ليست مثالية ، ولكنها تجعلك تشعر بما هي عليه:

function playingInBrothersRoom (withToys) {
  // We closure toys which we played in the brother's room. When he come back and lock the door
  // your brother is supposed to be into the outer [[scope]] object now. Thanks god you could communicate with him.
  var closureToys = withToys || [],
      returnToy, countIt, toy; // Just another closure helpers, for brother's inner use.

  var brotherGivesToyBack = function (toy) {
    // New request. There is not yet closureToys on brother's hand yet. Give him a time.
    returnToy = null;
    if (toy && closureToys.length > 0) { // If we ask for a specific toy, the brother is going to search for it.

      for ( countIt = closureToys.length; countIt; countIt--) {
        if (closureToys[countIt - 1] == toy) {
          returnToy = 'Take your ' + closureToys.splice(countIt - 1, 1) + ', little boy!';
          break;
        }
      }
      returnToy = returnToy || 'Hey, I could not find any ' + toy + ' here. Look for it in another room.';
    }
    else if (closureToys.length > 0) { // Otherwise, just give back everything he has in the room.
      returnToy = 'Behold! ' + closureToys.join(', ') + '.';
      closureToys = [];
    }
    else {
      returnToy = 'Hey, lil shrimp, I gave you everything!';
    }
    console.log(returnToy);
  }
  return brotherGivesToyBack;
}
// You are playing in the house, including the brother's room.
var toys = ['teddybear', 'car', 'jumpingrope'],
    askBrotherForClosuredToy = playingInBrothersRoom(toys);

// The door is locked, and the brother came from the school. You could not cheat and take it out directly.
console.log(askBrotherForClosuredToy.closureToys); // Undefined

// But you could ask your brother politely, to give it back.
askBrotherForClosuredToy('teddybear'); // Hooray, here it is, teddybear
askBrotherForClosuredToy('ball'); // The brother would not be able to find it.
askBrotherForClosuredToy(); // The brother gives you all the rest
askBrotherForClosuredToy(); // Nothing left in there

كما ترون ، لا يزال يمكن الوصول إلى الألعاب التي تُترك في الغرفة عبر الأخ بغض النظر عما إذا كانت الغرفة مغلقة أم لا. هنا jsbin للتلاعب به.


عمليات الإغلاق بسيطة:

المثال البسيط التالي يغطي جميع النقاط الرئيسية لإغلاق JavaScript. *

هنا هو المصنع الذي ينتج الآلات الحاسبة التي يمكن أن تضيف وتتكاثر:

function make_calculator() {
  var n = 0; // this calculator stores a single number n
  return {
    add: function(a) {
      n += a;
      return n;
    },
    multiply: function(a) {
      n *= a;
      return n;
    }
  };
}

first_calculator = make_calculator();
second_calculator = make_calculator();

first_calculator.add(3); // returns 3
second_calculator.add(400); // returns 400

first_calculator.multiply(11); // returns 33
second_calculator.multiply(10); // returns 4000

والنقطة الأساسية: كل دعوة إلى make_calculatorإنشاء متغير محلي جديد n، التي لا تزال صالحة للاستعمال من قبل أن آلة حاسبة في addو multiplyظائف بعد فترة طويلة من make_calculatorالعوائد.

إذا كنت معتادًا على إطارات المكدس ، فإن هذه الآلات الحاسبة تبدو غريبة: كيف يمكنهم مواصلة الوصول nبعد make_calculatorالعودة؟ الإجابة هي تخيل أن جافا سكريبت لا تستخدم "إطارات مكدس" ، ولكن بدلاً من ذلك تستخدم "إطارات كومة الذاكرة المؤقتة" ، والتي يمكن أن تستمر بعد استدعاء الدالة الذي جعلها ترجع.

وظائف الداخلية مثل addو multiply، الذي الوصول إلى المتغيرات في وظيفة الخارجية ** ، وتسمى الإغلاق .

هذا إلى حد كبير كل ما هو هناك لإغلاق.


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

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


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

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

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

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

مثال:

function foo (initValue) {
   //This variable is not destroyed when the foo function exits.
   //It is 'captured' by the two nested functions returned below.
   var value = initValue;

   //Note that the two returned functions are created right now.
   //If the foo function is called again, it will return
   //new functions referencing a different 'value' variable.
   return {
       getValue: function () { return value; },
       setValue: function (newValue) { value = newValue; }
   }
}

function bar () {
    //foo sets its local variable 'value' to 5 and returns an object with
    //two functions still referencing that local variable
    var obj = foo(5);

    //Extracting functions just to show that no 'this' is involved here
    var getValue = obj.getValue;
    var setValue = obj.setValue;

    alert(getValue()); //Displays 5
    setValue(10);
    alert(getValue()); //Displays 10

    //At this point getValue and setValue functions are destroyed
    //(in reality they are destroyed at the next iteration of the garbage collector).
    //The local variable 'value' in the foo is no longer referenced by
    //anything and is destroyed too.
}

bar();

حسنا ، مروحة الإغلاق عمرها 6 سنوات. هل تريد أن تسمع أبسط مثال على الإغلاق؟

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

إليك كيف يمكنني تحويل قصة طائرتي إلى الشفرة.

var plane = function(defaultAirport) {

  var lastAirportLeft = defaultAirport;

  var car = {
    driver: {
      startAccessPlaneInfo: function() {
        setInterval(function() {
          console.log("Last airport was " + lastAirportLeft);
        }, 2000);
      }
    }
  };
  car.driver.startAccessPlaneInfo();

  return {
    leaveTheAirport: function(airPortName) {
      lastAirportLeft = airPortName;
    }
  }
}("Boryspil International Airport");

plane.leaveTheAirport("John F. Kennedy");


لقد كتبت تعليقًا مدونًا منذ فترة لشرح عمليات الإغلاق. إليك ما قلته عن الإغلاق من حيث لماذا تريد واحدًا.

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

وبهذا المعنى ، فإنهم يجعلون دالة تعمل قليلاً مثل كائن ذي سمات خاصة.

المشاركة الكاملة:

فما هي هذه الأشياء الختامية؟


يمكن لوظائف JavaScript الوصول إلى:

  1. الحجج
  2. السكان المحليون (أي ، المتغيرات المحلية والوظائف المحلية)
  3. البيئة ، والتي تشمل:
    • globals ، بما في ذلك DOM
    • أي شيء في الوظائف الخارجية

إذا وصلت إحدى الدوال إلى بيئتها ، فستكون الوظيفة عبارة عن إغلاق.

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

مثال على الإغلاق الذي يستخدم البيئة العالمية:

تخيل أن يتم تنفيذ أحداث زر Vote-Up و Vote-Down كـ closures ، voteUp_click and voteDown_click ، ​​التي تتمتع بإمكانية الوصول إلى المتغيرات الخارجية هي VotesU و VOWN Down ، والتي يتم تحديدها عالميًا. (من أجل التبسيط ، أنا أشير إلى أزرار تصويت الأسئلة الخاصة بـ ، وليس صف أزرار رد التصويت.)

عندما ينقر المستخدم على الزر VoteUp ، تقوم الدالة VoteUp_click بالتحقق مما إذا كان isVotedDown == true لتحديد ما إذا كنت ستصوت أو تلغي مجرد إجراء تصويت لأسفل. يعد function voteUp_click بمثابة الإغلاق لأنه يصل إلى بيئته.

var isVotedUp = false;
var isVotedDown = false;

function voteUp_click() {
  if (isVotedUp)
    return;
  else if (isVotedDown)
    SetDownVote(false);
  else
    SetUpVote(true);
}

function voteDown_click() {
  if (isVotedDown)
    return;
  else if (isVotedUp)
    SetUpVote(false);
  else
    SetDownVote(true);
}

function SetUpVote(status) {
  isVotedUp = status;
  // Do some CSS stuff to Vote-Up button
}

function SetDownVote(status) {
  isVotedDown = status;
  // Do some CSS stuff to Vote-Down button
}

جميع هذه الوظائف الأربع هي إغلاق لأنها جميع الوصول إلى بيئتهم.


ربما أكثر بقليل من الجميع ولكن أبكر من الأطفال في سن السادسة ، ولكن بعض الأمثلة التي ساعدت على جعل مفهوم الإغلاق في جافا سكريبت انقر لي.

الإغلاق هو وظيفة لديها الوصول إلى نطاق وظيفة أخرى (المتغيرات والوظائف). أسهل طريقة لإنشاء إغلاق هي وظيفة داخل وظيفة ؛ السبب في أنه في جافا سكريبت وظيفة دائما الوصول إلى نطاق وظيفة تحتوي على.

function outerFunction() {
    var outerVar = "monkey";
    
    function innerFunction() {
        alert(outerVar);
    }
    
    innerFunction();
}

outerFunction();

تنبيه: قرد

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

الآن فكر في ما يلي:

function outerFunction() {
    var outerVar = "monkey";
    
    function innerFunction() {
        return outerVar;
    }
    
    return innerFunction;
}

var referenceToInnerFunction = outerFunction();
alert(referenceToInnerFunction());

تنبيه: قرد

يتم تعيين referenceToInnerFunction إلى outerFunction () ، الذي يقوم ببساطة بإرجاع مرجع إلى innerFunction. عندما يتم استدعاء referenceToInnerFunction ، تقوم بإرجاع outerVar. مرة أخرى ، كما هو موضح أعلاه ، يوضح هذا أن الوظيفة الداخلية يمكنها الوصول إلى outerVar ، وهو متغير للوظيفة الخارجية. علاوة على ذلك ، من المثير للاهتمام ملاحظة أنه يحتفظ بهذا الوصول حتى بعد انتهاء العمل الخارجي.

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

function outerFunction() {
    var outerVar = "monkey";
    
    function innerFunction() {
        return outerVar;
    }
    
    return innerFunction;
}

var referenceToInnerFunction = outerFunction();
alert(referenceToInnerFunction());

outerFunction = null;
alert(referenceToInnerFunction());

تنبيه: قرد ALERT: قرد

لكن كيف هذا؟ كيف يمكن أن تشير الدالة TInInner إلى قيمة outerVar الآن بعد تعيين الدالة الخارجية على null؟

يرجع السبب في كون الإشارة ToInnerFile يمكن أن تستمر في الوصول إلى قيمة outerVar لأنه عندما تم إنشاء الإغلاق لأول مرة عن طريق وضع الداخلي الداخلي للوظيفة الخارجية ، أضاف InternalFunction إشارة إلى نطاق functionFunction (متغيراته ووظائفه) لسلسلة النطاق الخاصة به. ما يعنيه هذا هو أن الدالة الداخلي يحتوي على مؤشر أو مرجع لكل متغيرات العوامل الخارجية ، بما في ذلك outerVar. لذلك حتى عندما ينتهي العمل الخارجي ، أو حتى إذا تم حذفه أو تعيينه إلى قيمة فارغة ، فإن المتغيرات في نطاقه ، مثل outerVar ، تلتصق بالذاكرة بسبب الإشارة المعلقة إليها من جانب الوظيفة الداخلية التي تم إرجاعها إلى referenceToInnerFunction. لإطلاق outerVar وبقية المتغيرات الخارجية للذاكرة من الذاكرة فعليك التخلص من هذه الإشارة المعلقة إليهم ،اقول عن طريق تحديد referenceToInnerFunction إلى فارغة كذلك.

//////////

شيئين آخرين حول الإغلاق لملاحظة. أولاً ، سيحصل الإغلاق دائمًا على إمكانية الوصول إلى القيم الأخيرة لدالة احتوائه.

function outerFunction() {
    var outerVar = "monkey";
    
    function innerFunction() {
        alert(outerVar);
    }
    
    outerVar = "gorilla";

    innerFunction();
}

outerFunction();

تنبيه: الغوريلا

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


استخدام هيكل closure ، وهذا من شأنه أن يقلل من حلقة إضافية. يمكنك القيام بذلك في حلقة واحدة للحلقة:

var funcs = [];
for (var i = 0; i < 3; i++) {     
  (funcs[i] = function() {         
    console.log("My value: " + i); 
  })(i);
}




javascript function variables scope closures