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



14 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 على @ Aust حول استخدام .bind هي أفضل طريقة للقيام بهذا النوع من الأشياء الآن. هناك أيضا lo-dash / الشرطة السفلية ' _.partial عندما لا تحتاج أو تريد الفوضى مع bind thisArg .

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>

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




This question really shows the history of JavaScript! Now we can avoid block scoping with arrow functions and handle loops directly from DOM nodes using Object methods.

const funcs = [1, 2, 3].map(i => () => console.log(i));
funcs.map(fn => fn())

const buttons = document.getElementsByTagName("button");
Object
  .keys(buttons)
  .map(i => buttons[i].addEventListener('click', () => console.log(i)));
<button>0</button><br>
<button>1</button><br>
<button>2</button>




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

تحدد وظيفة بيئة جديدة

يعتبر:

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 ، 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/




This is a problem often encountered with asynchronous code, the variable i is mutable and at the time at which the function call is made the code using i will be executed and i will have mutated to its last value, thus meaning all functions created within the loop will create a closure and i will be equal to 3 (the upper bound + 1 of the for loop.

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




I prefer to use forEach function, which has its own closure with creating a pseudo range:

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
}

That looks uglier than ranges in other languages, but IMHO less monstrous than other solutions.




You could use a declarative module for lists of data such as query-js (*). In these situations I personally find a declarative approach less surprising

var funcs = Query.range(0,3).each(function(i){
     return  function() {
        console.log("My value: " + i);
    };
});

You could then use your second loop and get the expected result or you could do

funcs.iterate(function(f){ f(); });

(*) I'm the author of query-js and therefor biased towards using it, so don't take my words as a recommendation for said library only for the declarative approach :)




Your code doesn't work, because what it does is:

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?

Now the question is, what is the value of variable i when the function is called? Because the first loop is created with the condition of i < 3 , it stops immediately when the condition is false, so it is i = 3 .

You need to understand that, in time when your functions are created, none of their code is executed, it is only saved for later. And so when they are called later, the interpreter executes them and asks: "What is the current value of i ?"

So, your goal is to first save the value of i to function and only after that save the function to funcs . This could be done for example this way:

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
}

This way, each function will have it's own variable x and we set this x to the value of i in each iteration.

This is only one of the multiple ways to solve this problem.




Many solutions seem correct but they don't mention it's called Currying which is a functional programming design pattern for situations like here. 3-10 times faster than bind depending on the browser.

var funcs = [];
for (var i = 0; i < 3; i++) {      // let's create 3 functions
  funcs[i] = curryShowValue(i);
}
for (var j = 0; j < 3; j++) {
  funcs[j]();                      // and now let's run each one to see
}

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

See the performance gain in different browsers .




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

إن التفافها في وظيفة تقيم إعادة الوظيفة مثل إجابة المتصفّح ستؤدي المهمة ، لأن المتغير لديه الآن نطاق الوظيفة.

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

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]();                        
}



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

  • يشكل كل تعريف دالة نطاقًا يتكون من جميع المتغيرات المحلية التي تم تعريفها بواسطة 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 .

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

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 . يمكن العثور على هذه المرة في نطاق الدالة الخارجي الذي يتم تنفيذه 3 مرات في الحلقة for ، وفي كل مرة يكون لها قيمة أكون ملزمة بشكل صحيح. لن تستخدم قيمة window.i عند تنفيذ الداخلية.

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




إليك حل بسيط يستخدم 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



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

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

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

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

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

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

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




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

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);

}

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




I'm surprised no one yet has suggested using the forEach function to better avoid (re)using local variables. In fact, I'm not using for(var i ...) at all anymore for this reason.

[0,2,3].forEach(function(i){ console.log('My value:', i); });
// My value: 0
// My value: 2
// My value: 3

// edited to use forEach instead of map.




Related