إغلاق جافاسكريبت داخل الحلقات - مثال عملي بسيط [javascript]


Answers

محاولة:

var funcs = [];

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

تعديل (2014):

شخصيا أعتقد أن @ أوست الإجابة الأكثر حداثة حول استخدام .bind هو أفضل وسيلة للقيام بهذا النوع من الشيء الآن. هناك أيضا لو اندفاعة / شرطة السفلية في _.partial عندما كنت لا تحتاج أو تريد أن الفوضى مع bind الصورة.

Question

var funcs = [];
for (var i = 0; i < 3; i++) {      // let's create 3 functions
  funcs[i] = function() {          // and store them in funcs
    console.log("My value: " + i); // each should log its value.
  };
}
for (var j = 0; j < 3; j++) {
  funcs[j]();                      // and now let's run each one to see
}

وينتج هذا:

القيمة: 3
القيمة: 3
القيمة: 3

في حين أود أن الإخراج:

القيمة: 0
القيمة: 1
القيمة: 2

تحدث نفس المشكلة عندما يحدث تأخير في تشغيل الدالة باستخدام مستمعي الحدث:

var buttons = document.getElementsByTagName("button");
for (var i = 0; i < buttons.length; i++) {          // let's create 3 functions
  buttons[i].addEventListener("click", function() { // as event listeners
    console.log("My value: " + i);                  // each should log its value.
  });
}
<button>0</button><br>
<button>1</button><br>
<button>2</button>

ما هو الحل لهذه المشكلة الأساسية؟




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

for (var i = 0; i < 3; i++) {

    (function(index) {
        console.log('iterator: ' + index);
        //now you can also loop an ajax call here 
        //without losing track of the iterator value: $.ajax({});
    })(i);

}

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




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

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

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

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

يتوفر الآن دعم المتصفح لأولئك الذين يستهدفون أحدث إصدارات المتصفحات. const / let معتمدة حاليا في أحدث فايرفوكس، سفاري، الحافة وكروم. كما أنها معتمدة في عقدة، ويمكنك استخدامه في أي مكان من خلال الاستفادة من أدوات البناء مثل بابل. يمكنك الاطلاع على مثال عملي هنا: http://jsfiddle.net/ben336/rbU4t/2/

محرر المستندات هنا:

حذار، على الرغم من أن IE9-IE11 و إدج قبل دعم الحافة 14 let ولكن الحصول على الخطأ أعلاه (أنها لا تخلق جديدة في كل مرة، لذلك كل الوظائف أعلاه سوف تسجل 3 كما لو أنهم لو استخدمنا var ). حافة 14 أخيرا يحصل على حق.




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

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

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

var funcs = {};
for (var i = 0; i < 3; i++) {
    let index = i;          //add this
    funcs[i] = function() {            
        console.log("My value: " + index); //change to the copy
    };
}
for (var j = 0; j < 3; j++) {
    funcs[j]();                        
}



يصف هذا الخطأ الشائع باستخدام عمليات الإغلاق في جافا سكريبت.

وتعرف الدالة بيئة جديدة

يعتبر:

function makeCounter()
{
  var obj = {counter: 0};
  return {
    inc: function(){obj.counter ++;},
    get: function(){return obj.counter;}
  };
}

counter1 = makeCounter();
counter2 = makeCounter();

counter1.inc();

alert(counter1.get()); // returns 1
alert(counter2.get()); // returns 0

في كل مرة يتم استدعاء makeCounter ، {counter: 0} النتائج في كائن جديد يتم إنشاؤه. أيضا، يتم إنشاء نسخة جديدة من obj كذلك للإشارة إلى الكائن الجديد. وهكذا، counter1 و counter2 مستقلة عن بعضها البعض.

الإغلاق في الحلقات

استخدام إغلاق في حلقة صعبة.

يعتبر:

var counters = [];

function makeCounters(num)
{
  for (var i = 0; i < num; i++)
  {
    var obj = {counter: 0};
    counters[i] = {
      inc: function(){obj.counter++;},
      get: function(){return obj.counter;}
    }; 
  }
}

makeCounters(2);

counters[0].inc();

alert(counters[0].get()); // returns 1
alert(counters[1].get()); // returns 1

لاحظ أن counters[0] counters[1] ليست مستقلة. في الواقع، أنها تعمل على نفس obj !

وذلك لأن هناك نسخة واحدة فقط من obj المشتركة عبر جميع التكرارات من حلقة، ربما لأسباب تتعلق بالأداء. على الرغم من أن {counter: 0} يقوم بتكوين كائن جديد في كل تكرار، سيتم الحصول على نسخة نفس obj مع مرجع الى الكائن الأحدث.

الحل هو استخدام وظيفة مساعد آخر:

function makeHelper(obj)
{
  return {
    inc: function(){obj.counter++;},
    get: function(){return obj.counter;}
  }; 
}

function makeCounters(num)
{
  for (var i = 0; i < num; i++)
  {
    var obj = {counter: 0};
    counters[i] = makeHelper(obj);
  }
}

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

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




حاول هذا واحد أقصر

  • لا صفيف

  • لا إضافية للحلقة


for (var i = 0; i < 3; i++) {
    createfunc(i)();
}

function createfunc(i) {
    return function(){console.log("My value: " + i);};
}

http://jsfiddle.net/7P6EN/




وهنا حل بسيط يستخدم forEach (يعمل مرة أخرى إلى IE9):

var funcs = {};
[0,1,2].forEach(function(i) {          // let's create 3 functions
    funcs[i] = function() {            // and store them in funcs
        console.log("My value: " + i); // each should log its value.
    };
})
for (var j = 0; j < 3; j++) {
    funcs[j]();                        // and now let's run each one to see
}

مطبوعات:

My value: 0
My value: 1
My value: 2



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

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

في الكود الأولي:

funcs = {};
for (var i = 0; i < 3; i++) {         
  funcs[i] = function inner() {        // function inner's scope contains nothing
    console.log("My value: " + i);    
  };
}
console.log(window.i)                  // test value 'i', print 3

عندما يتم تنفيذ funcs ، فإن سلسلة نطاق تكون function inner -> global . وبما أن المتغير i لا يمكن العثور عليه في function inner (لم يعلن باستخدام var ولا يتم تمريره كوسيط)، فإنه يستمر في البحث حتى يتم العثور على قيمة i في النطاق العالمي وهو window.i .

من خلال لفه في وظيفة خارجية إما تعريف صريح وظيفة المساعد مثل هارتو فعلت أو استخدام وظيفة مجهولة مثل بيورن فعل:

funcs = {};
function outer(i) {              // function outer's scope contains 'i'
  return function inner() {      // function inner, closure created
   console.log("My value: " + i);
  };
}
for (var i = 0; i < 3; i++) {
  funcs[i] = outer(i);
}
console.log(window.i)          // print 3 still

عندما يتم تنفيذ funcs ، والآن سلسلة نطاق ستكون function inner -> function outer . هذه المرة i يمكن العثور عليها في نطاق الدالة الخارجية التي يتم تنفيذها 3 مرات في ل حلقة، في كل مرة لديها قيمة i ملزمة بشكل صحيح. انها لن تستخدم قيمة window.i عندما الداخلية تنفيذها.

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




مع ميزات جديدة من ES6 كتلة إدارة النطاق

var funcs = [];
for (let i = 0; i < 3; i++) {          // let's create 3 functions
    funcs[i] = function() {            // and store them in funcs
        console.log("My value: " + i); // each should log its value.
    };
}
for (let j = 0; j < 3; j++) {
    funcs[j]();                        // and now let's run each one to see
}

يتم استبدال التعليمات البرمجية في السؤال مع السماح بدلا من فار .

أكثر على دعونا




انا افضل لاستخدام forEachوظيفة، التي لديها إغلاق الخاص مع إنشاء مجموعة الزائفة:

var funcs = [];

new Array(3).fill(0).forEach(function (_, i) { // creating a range
    funcs[i] = function() {            
        // now i is safely incapsulated 
        console.log("My value: " + i);
    };
});

for (var j = 0; j < 3; j++) {
    funcs[j](); // 0, 1, 2
}

التي تبدو أقبح من النطاقات في لغات أخرى، ولكن IMHO أقل وحشية من الحلول الأخرى.




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




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

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



وبعد حل آخر: بدلا من إنشاء حلقة أخرى، مجرد ربط thisإلى وظيفة العودة.

var funcs = [];

function createFunc(i) {
  return function() {
    console.log('My value: ' + i); //log value of i.
  }.call(this);
}

for (var i = 1; i <= 5; i++) {  //5 functions
  funcs[i] = createFunc(i);     // call createFunc() i=5 times
}

عن طريق ربط هذا، لا يحل المشكلة أيضا.




لا تعمل كنت رمز، لأن ما تقوم به هو:

Create variable `funcs` and assign it an empty array;  
Loop from 0 up until it is less than 3 and assign it to variable `i`;
    Push to variable `funcs` next function:  
        // Only push (save), but don't execute
        **Write to console current value of variable `i`;**

// First loop has ended, i = 3;

Loop from 0 up until it is less than 3 and assign it to variable `j`;
    Call `j`-th function from variable `funcs`:  
        **Write to console current value of variable `i`;**  
        // Ask yourself NOW! What is the value of i?

والسؤال الآن هو، ما هي قيمة المتغير iعندما يتم استدعاء الدالة؟ لأنه يتم إنشاء حلقة الأولى مع حالة i < 3، فإنه يتوقف فورا عندما يكون الشرط غير صحيحة، لذلك هو i = 3.

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

لذلك، هدفك هو أولا حفظ قيمة iإلى وظيفة وفقط بعد ذلك حفظ وظيفة ل funcs. ويمكن أن يتم هذا على سبيل المثال على هذا النحو:

var funcs = [];
for (var i = 0; i < 3; i++) {          // let's create 3 functions
    funcs[i] = function(x) {            // and store them in funcs
        console.log("My value: " + x); // each should log its value.
    }.bind(null, i);
}
for (var j = 0; j < 3; j++) {
    funcs[j]();                        // and now let's run each one to see
}

بهذه الطريقة، سيكون كل وظيفة لها انها المتغير الخاص 'X' وضعناها هذه xالقيمة iفي كل تكرار.

هذا هو واحد فقط من عدة طرق وكيفية حل هذه المشكلة.




COUNTER كونها بدائية

يتيح تحديد مهام رد على النحو التالي:

// ****************************
// COUNTER BEING A PRIMITIVE
// ****************************
function test1() {
    for (var i=0; i<2; i++) {
        setTimeout(function() {
            console.log(i);
        });
    }
}
test1();
// 2
// 2

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

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

function test2() {
    function sendRequest(i) {
        setTimeout(function() {
            console.log(i);
        });
    }

    for (var i = 0; i < 2; i++) {
        sendRequest(i);
    }
}
test2();
// 1
// 2

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

COUNTER كونه OBJECT

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

// ****************************
// COUNTER BEING AN OBJECT
// ****************************
function test3() {
    var index = { i: 0 };
    for (index.i=0; index.i<2; index.i++) {
        setTimeout(function() {
            console.log('test3: ' + index.i);
        });
    }
}
test3();
// 2
// 2

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

function test4() {
    var index = { i: 0 };
    function sendRequest(index, i) {
        setTimeout(function() {
            console.log('index: ' + index);
            console.log('i: ' + i);
            console.log(index[i]);
        });
    }

    for (index.i=0; index.i<2; index.i++) {
        sendRequest(index, index.i);
    }
}
test4();
// index: { i: 2}
// 0
// undefined

// index: { i: 2}
// 1
// undefined