การปิด JavaScript ภายในลูป - ตัวอย่างการปฏิบัติง่ายๆ

Apr 15 2009

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

ผลลัพธ์นี้:

ค่าของฉัน: 3
ค่าของฉัน: 3
ค่าของฉัน: 3

ในขณะที่ฉันต้องการส่งออก:

ค่าของฉัน: 0
ค่าของฉัน: 1
ค่าของฉัน: 2


ปัญหาเดียวกันนี้เกิดขึ้นเมื่อความล่าช้าในการเรียกใช้ฟังก์ชันเกิดจากการใช้ตัวฟังเหตุการณ์:

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

…หรือรหัสอะซิงโครนัสเช่นการใช้สัญญา:

// Some async wait function
const wait = (ms) => new Promise((resolve, reject) => setTimeout(resolve, ms));

for (var i = 0; i < 3; i++) {
  // Log `i` as soon as each promise resolves.
  wait(i * 100).then(() => console.log(i));
}

นอกจากนี้ยังปรากฏในfor inและfor ofวนซ้ำ:

const arr = [1,2,3];
const fns = [];

for(var i in arr){
  fns.push(() => console.log(`index: ${i}`)); } for(var v of arr){ fns.push(() => console.log(`value: ${v}`));
}

for(var f of fns){
  f();
}

อะไรคือวิธีแก้ปัญหาพื้นฐานนี้?

คำตอบ

2214 harto Apr 15 2009 at 13:18

ปัญหาคือตัวแปรiภายในแต่ละฟังก์ชันที่ไม่ระบุตัวตนของคุณถูกผูกไว้กับตัวแปรเดียวกันนอกฟังก์ชัน

โซลูชัน ES6: let

ECMAScript 6 (ES6) แนะนำคำหลักใหม่letและconstคำหลักที่กำหนดขอบเขตแตกต่างจากvarตัวแปรตาม ตัวอย่างเช่นในลูปที่มีletดัชนีฐานการวนซ้ำแต่ละครั้งในลูปจะมีตัวแปรใหม่iพร้อมขอบเขตลูปดังนั้นโค้ดของคุณจะทำงานตามที่คุณคาดหวัง มีแหล่งข้อมูลมากมาย แต่ฉันขอแนะนำให้โพสต์ขอบเขตบล็อกของ 2alityเป็นแหล่งข้อมูลที่ดี

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

อย่างไรก็ตามระวัง IE9-IE11 และ Edge ก่อนที่จะรองรับ Edge 14 letแต่ได้รับข้อผิดพลาดข้างต้น (พวกเขาไม่ได้สร้างใหม่iทุกครั้งดังนั้นฟังก์ชันทั้งหมดข้างต้นจะบันทึก 3 เหมือนที่เราใช้var) Edge 14 ทำให้ถูกต้องในที่สุด


โซลูชัน ES5.1: forEach

ด้วยความพร้อมใช้งานของArray.prototype.forEachฟังก์ชั่นที่ค่อนข้างแพร่หลาย(ในปี 2015) จึงเป็นที่น่าสังเกตว่าในสถานการณ์เหล่านั้นที่เกี่ยวข้องกับการวนซ้ำเป็นหลักในอาร์เรย์ของค่าต่างๆ.forEach()เป็นวิธีที่เป็นธรรมชาติในการปิดการทำงานที่แตกต่างกันสำหรับการทำซ้ำ นั่นคือสมมติว่าคุณมีอาร์เรย์บางประเภทที่มีค่า (การอ้างอิง DOM วัตถุอะไรก็ตาม) และปัญหาเกิดจากการตั้งค่าการเรียกกลับเฉพาะสำหรับแต่ละองค์ประกอบคุณสามารถทำได้:

var someArray = [ /* whatever */ ];
// ...
someArray.forEach(function(arrayElement) {
  // ... code code code for this one element
  someAsynchronousFunction(arrayElement, function() {
    arrayElement.doSomething();
  });
});

แนวคิดก็คือการเรียกใช้ฟังก์ชันเรียกกลับแต่ละครั้งที่ใช้กับ.forEachลูปจะเป็นการปิดเอง พารามิเตอร์ที่ส่งผ่านไปยังตัวจัดการนั้นคือองค์ประกอบอาร์เรย์เฉพาะสำหรับขั้นตอนนั้น ๆ ของการวนซ้ำ หากใช้ในการโทรกลับแบบอะซิงโครนัสจะไม่ชนกับการเรียกกลับอื่นใดที่สร้างขึ้นในขั้นตอนอื่น ๆ ของการทำซ้ำ

หากคุณกำลังทำงานใน jQuery $.each()ฟังก์ชันนี้จะให้ความสามารถที่คล้ายกัน


วิธีแก้ปัญหาแบบคลาสสิก: การปิด

สิ่งที่คุณต้องการทำคือผูกตัวแปรภายในแต่ละฟังก์ชันเข้ากับค่าที่แยกจากกันโดยไม่เปลี่ยนแปลงนอกฟังก์ชัน:

var funcs = [];

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

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

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

เนื่องจากไม่มีขอบเขตการบล็อกใน JavaScript - ขอบเขตฟังก์ชันเท่านั้น - โดยการรวมการสร้างฟังก์ชันไว้ในฟังก์ชันใหม่คุณจึงมั่นใจได้ว่าค่าของ "i" จะยังคงอยู่ตามที่คุณต้องการ

390 Bjorn Apr 15 2009 at 13:10

ลอง:

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 เกี่ยวกับการใช้เป็นวิธีที่ดีที่สุดในการทำสิ่งนี้ในตอนนี้ นอกจากนี้ยังมีดูเถิดประ / ขีดเป็น_.partialเมื่อคุณไม่ต้องการหรือต้องการที่จะยุ่งกับ'sbindthisArg

357 Aust Oct 11 2013 at 23:41

อีกวิธีหนึ่งที่ยังไม่ได้กล่าวถึงคือการใช้ Function.prototype.bind

var funcs = {};
for (var i = 0; i < 3; i++) {
  funcs[i] = function(x) {
    console.log('My value: ' + x);
  }.bind(this, i);
}
for (var j = 0; j < 3; j++) {
  funcs[j]();
}

อัปเดต

ตามที่ @squint และ @mekdev ชี้ไว้คุณจะได้รับประสิทธิภาพที่ดีขึ้นโดยการสร้างฟังก์ชันนอกลูปก่อนจากนั้นจึงเชื่อมโยงผลลัพธ์ภายในลูป

function log(x) {
  console.log('My value: ' + x);
}

var funcs = [];

for (var i = 0; i < 3; i++) {
  funcs[i] = log.bind(this, i);
}

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

275 neurosnap Oct 12 2013 at 01:23

การใช้นิพจน์ฟังก์ชันที่เรียกใช้ทันทีวิธีที่ง่ายที่สุดและอ่านง่ายที่สุดในการใส่ตัวแปรดัชนี:

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จะได้รับการบันทึกเพื่อใช้ในภายหลังในฟังก์ชันการทำงานแบบอะซิงโครนัสภายใน IIFE

170 woojoo666 Apr 10 2015 at 16:57

ไปงานปาร์ตี้ช้าไปหน่อย แต่วันนี้ฉันกำลังสำรวจปัญหานี้และสังเกตเห็นว่าคำตอบจำนวนมากไม่ได้กล่าวถึงวิธีการที่ Javascript ปฏิบัติกับขอบเขตอย่างสมบูรณ์ซึ่งโดยพื้นฐานแล้วสิ่งนี้ทำให้เกิดปัญหา

ดังที่คนอื่น ๆ กล่าวถึงปัญหาคือฟังก์ชันภายในอ้างถึงiตัวแปรเดียวกัน แล้วทำไมเราไม่สร้างตัวแปรท้องถิ่นใหม่แต่ละการวนซ้ำและมีการอ้างอิงฟังก์ชันภายในแทน?

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
    var ilocal = i; //create a new local variable
    funcs[i] = function() {
        console.log("My value: " + ilocal); //each should reference its own local variable
    };
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}

เช่นเดียวกับก่อนที่ฟังก์ชั่นภายในแต่ละเอาท์พุทค่าสุดท้ายที่ได้รับมอบหมายiในขณะนี้ฟังก์ชั่นภายในแต่ละเพียง outputs ilocalค่าสุดท้ายที่ได้รับมอบหมาย แต่การทำซ้ำแต่ละครั้งไม่ควรมีของตัวเองilocal?

ปรากฎว่าเป็นปัญหา ilocalแต่ละซ้ำแชร์ขอบเขตเดียวกันดังนั้นการทำซ้ำหลังจากที่ครั้งแรกทุกคนเป็นเพียงการเขียนทับ จากMDN :

สำคัญ: JavaScript ไม่มีขอบเขตการบล็อก ตัวแปรที่นำมาใช้กับบล็อกจะถูกกำหนดขอบเขตไว้ที่ฟังก์ชันหรือสคริปต์ที่มีอยู่และผลของการตั้งค่ายังคงอยู่นอกเหนือจากตัวบล็อก กล่าวอีกนัยหนึ่งคำสั่งบล็อกไม่ได้นำเสนอขอบเขต แม้ว่าบล็อก "แบบสแตนด์อโลน" จะเป็นไวยากรณ์ที่ถูกต้อง แต่คุณไม่ต้องการใช้บล็อกแบบสแตนด์อโลนใน JavaScript เนื่องจากไม่ได้ทำในสิ่งที่คุณคิดหากคุณคิดว่าพวกเขาทำอะไรเช่นบล็อกดังกล่าวใน C หรือ Java

ย้ำเพื่อเน้น:

JavaScript ไม่มีขอบเขตการบล็อก ตัวแปรที่นำมาใช้กับบล็อกจะถูกกำหนดขอบเขตไว้ที่ฟังก์ชันหรือสคริปต์ที่มี

เราสามารถดูสิ่งนี้ได้โดยการตรวจสอบilocalก่อนที่เราจะประกาศในการทำซ้ำแต่ละครั้ง:

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
  console.log(ilocal);
  var ilocal = i;
}

นี่คือเหตุผลว่าทำไมข้อผิดพลาดนี้จึงยุ่งยากมาก แม้ว่าคุณจะประกาศตัวแปรซ้ำ แต่ Javascript จะไม่ส่งข้อผิดพลาดและ JSLint จะไม่ส่งคำเตือนด้วยซ้ำ นี่เป็นสาเหตุที่วิธีที่ดีที่สุดในการแก้ปัญหานี้คือการใช้ประโยชน์จากการปิดซึ่งโดยพื้นฐานแล้วแนวคิดที่ว่าใน Javascript ฟังก์ชันภายในสามารถเข้าถึงตัวแปรภายนอกได้เนื่องจากขอบเขตด้านใน "ล้อมรอบ" ขอบเขตด้านนอก

นอกจากนี้ยังหมายความว่าฟังก์ชันภายใน "ยึด" ตัวแปรด้านนอกและทำให้มันคงอยู่แม้ว่าฟังก์ชันภายนอกจะส่งกลับ ในการใช้สิ่งนี้เราสร้างและเรียกใช้ฟังก์ชัน wrapper เพื่อสร้างขอบเขตใหม่ประกาศilocalในขอบเขตใหม่และส่งคืนฟังก์ชันภายในที่ใช้ilocal(คำอธิบายเพิ่มเติมด้านล่าง):

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
    funcs[i] = (function() { //create a new scope using a wrapper function
        var ilocal = i; //capture i into a local var
        return function() { //return the inner function
            console.log("My value: " + ilocal);
        };
    })(); //remember to run the wrapper function
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}

การสร้างฟังก์ชันภายในภายในฟังก์ชัน Wrapper ทำให้ฟังก์ชันภายในมีสภาพแวดล้อมส่วนตัวที่มีเพียงการ "ปิด" เท่านั้นที่สามารถเข้าถึงได้ ดังนั้นทุกครั้งที่เราเรียกใช้ฟังก์ชัน wrapper เราจะสร้างฟังก์ชันภายในใหม่โดยมีสภาพแวดล้อมแยกต่างหากเพื่อให้แน่ใจว่าilocalตัวแปรจะไม่ชนกันและเขียนทับกัน การเพิ่มประสิทธิภาพเล็กน้อยให้คำตอบสุดท้ายที่ผู้ใช้ SO อื่น ๆ ให้:

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
    funcs[i] = wrapper(i);
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}
//creates a separate environment for the inner function
function wrapper(ilocal) {
    return function() { //return the inner function
        console.log("My value: " + ilocal);
    };
}

อัปเดต

ด้วย ES6 ที่เป็นกระแสหลักตอนนี้เราสามารถใช้letคำหลักใหม่เพื่อสร้างตัวแปรที่มีขอบเขตบล็อก:

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (let i = 0; i < 3; i++) { // use "let" to declare "i"
    funcs[i] = function() {
        console.log("My value: " + i); //each should reference its own local variable
    };
}
for (var j = 0; j < 3; j++) { // we can use "var" here without issue
    funcs[j]();
}

ดูสิว่าตอนนี้มันง่ายแค่ไหน! สำหรับข้อมูลเพิ่มเติมโปรดดูคำตอบนี้ซึ่งข้อมูลของฉันอ้างอิงจาก

157 BenMcCormick May 21 2013 at 10:04

เมื่อ ES6 ได้รับการสนับสนุนอย่างกว้างขวางคำตอบที่ดีที่สุดสำหรับคำถามนี้จึงเปลี่ยนไป ES6 ให้letและconstคำสำคัญสำหรับสถานการณ์นี้ แทนที่จะยุ่งกับการปิดเราสามารถใช้letเพื่อตั้งค่าตัวแปรขอบเขตการวนซ้ำเช่นนี้:

var funcs = [];

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

valจากนั้นจะชี้ไปที่วัตถุที่เฉพาะเจาะจงสำหรับการหมุนของลูปนั้น ๆ และจะส่งคืนค่าที่ถูกต้องโดยไม่มีสัญกรณ์ปิดเพิ่มเติม สิ่งนี้ทำให้ปัญหานี้ง่ายขึ้นอย่างเห็นได้ชัด

constคล้ายletกับข้อ จำกัด เพิ่มเติมที่ชื่อตัวแปรไม่สามารถย้อนกลับไปยังการอ้างอิงใหม่หลังจากการกำหนดครั้งแรก

ขณะนี้มีการรองรับเบราว์เซอร์สำหรับผู้ที่กำหนดเป้าหมายเบราว์เซอร์เวอร์ชันล่าสุด const/ letได้รับการสนับสนุนใน Firefox, Safari, Edge และ Chrome ล่าสุด นอกจากนี้ยังรองรับใน Node และคุณสามารถใช้งานได้ทุกที่โดยใช้ประโยชน์จากเครื่องมือสร้างเช่น Babel คุณสามารถดูตัวอย่างการทำงานได้ที่นี่:http://jsfiddle.net/ben336/rbU4t/2/

เอกสารที่นี่:

  • const
  • ปล่อย

อย่างไรก็ตามระวัง IE9-IE11 และ Edge ก่อนที่จะรองรับ Edge 14 letแต่ได้รับข้อผิดพลาดข้างต้น (พวกเขาไม่ได้สร้างใหม่iทุกครั้งดังนั้นฟังก์ชันทั้งหมดข้างต้นจะบันทึก 3 เหมือนที่เราใช้var) Edge 14 ทำให้ถูกต้องในที่สุด

91 DarrenClark Apr 15 2009 at 13:48

อีกวิธีหนึ่งในการพูดก็คือiในฟังก์ชันของคุณถูกผูกไว้ในขณะที่เรียกใช้ฟังก์ชันไม่ใช่เวลาของการสร้างฟังก์ชัน

เมื่อคุณสร้างการปิดiเป็นการอ้างอิงถึงตัวแปรที่กำหนดในขอบเขตภายนอกไม่ใช่สำเนาของตัวแปรเหมือนตอนที่คุณสร้างการปิด จะได้รับการประเมินในขณะดำเนินการ

คำตอบอื่น ๆ ส่วนใหญ่มีวิธีแก้ปัญหาโดยการสร้างตัวแปรอื่นที่จะไม่เปลี่ยนค่าให้คุณ

แค่คิดว่าฉันจะเพิ่มคำอธิบายเพื่อความชัดเจน สำหรับวิธีแก้ปัญหาโดยส่วนตัวฉันจะไปกับ Harto เนื่องจากเป็นวิธีที่อธิบายตนเองได้มากที่สุดจากคำตอบที่นี่ รหัสใด ๆ ที่โพสต์จะใช้งานได้ แต่ฉันเลือกที่จะปิดโรงงานโดยต้องเขียนความคิดเห็นจำนวนมากเพื่ออธิบายว่าเหตุใดฉันจึงประกาศตัวแปรใหม่ (Freddy และ 1800) หรือมีไวยากรณ์การปิดแบบฝังตัวแปลก ๆ (apphacker)

74 eglasius Apr 15 2009 at 13:25

สิ่งที่คุณต้องเข้าใจคือขอบเขตของตัวแปรในจาวาสคริปต์จะขึ้นอยู่กับฟังก์ชัน นี่เป็นข้อแตกต่างที่สำคัญกว่าการพูด c # ที่คุณมีขอบเขตการบล็อกและเพียงแค่คัดลอกตัวแปรไปยังตัวแปรหนึ่งภายใน for ก็จะใช้ได้

การห่อไว้ในฟังก์ชันที่ประเมินการส่งคืนฟังก์ชันเช่นคำตอบของ apphacker จะทำเคล็ดลับเนื่องจากตอนนี้ตัวแปรมีขอบเขตของฟังก์ชัน

นอกจากนี้ยังมีคำหลัก let แทน var ซึ่งอนุญาตให้ใช้กฎขอบเขตการบล็อก ในกรณีนั้นการกำหนดตัวแปรภายใน for จะทำเคล็ดลับ ที่กล่าวว่าคำหลัก let ไม่ใช่วิธีแก้ปัญหาที่ใช้ได้จริงเนื่องจากความเข้ากันได้

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

63 Boann Aug 07 2012 at 15:45

นี่เป็นอีกรูปแบบหนึ่งของเทคนิคที่คล้ายกับ (apphacker) ของ Bjorn ซึ่งช่วยให้คุณกำหนดค่าตัวแปรภายในฟังก์ชันแทนที่จะส่งเป็นพารามิเตอร์ซึ่งอาจชัดเจนกว่าในบางครั้ง:

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

โปรดทราบว่าไม่ว่าคุณจะใช้เทคนิคใดindexตัวแปรจะกลายเป็นตัวแปรแบบคงที่โดยผูกไว้กับสำเนาที่ส่งคืนของฟังก์ชันภายใน กล่าวคือการเปลี่ยนแปลงค่าจะถูกเก็บรักษาไว้ระหว่างการโทร มันมีประโยชน์มาก

58 Noname Apr 20 2013 at 16:59

สิ่งนี้อธิบายถึงข้อผิดพลาดทั่วไปในการใช้การปิดใน 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มีการเรียกใช้{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);
  }
}

สิ่งนี้ได้ผลเนื่องจากตัวแปรโลคัลในขอบเขตฟังก์ชันโดยตรงเช่นเดียวกับตัวแปรอาร์กิวเมนต์ของฟังก์ชันได้รับการจัดสรรสำเนาใหม่เมื่อรายการ

51 KemalDağ Jun 25 2013 at 21:21

วิธีแก้ปัญหาที่ง่ายที่สุดคือ

แทนที่จะใช้:

var funcs = [];
for(var i =0; i<3; i++){
    funcs[i] = function(){
        alert(i);
    }
}

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

ซึ่งจะแจ้งเตือน "2" เป็นเวลา 3 ครั้ง เนื่องจากฟังก์ชันที่ไม่ระบุชื่อที่สร้างขึ้นสำหรับลูปแชร์การปิดเดียวกันและในการปิดนั้นค่าของiจะเท่ากัน ใช้สิ่งนี้เพื่อป้องกันการปิดร่วมกัน:

var funcs = [];
for(var new_i =0; new_i<3; new_i++){
    (function(i){
        funcs[i] = function(){
            alert(i);
        }
    })(new_i);
}

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

ความคิดที่อยู่เบื้องหลังนี้คือห่อหุ้มร่างกายทั้งหมดของห่วงกับIIFE (Expression ฟังก์ชั่นทันที-เรียก) และส่งผ่านเป็นพารามิเตอร์และจับเป็นnew_i iเนื่องจากฟังก์ชันที่ไม่ระบุชื่อถูกเรียกใช้งานทันทีiค่าจึงแตกต่างกันสำหรับแต่ละฟังก์ชันที่กำหนดไว้ภายในฟังก์ชันที่ไม่ระบุชื่อ

โซลูชันนี้ดูเหมือนจะเหมาะกับปัญหาดังกล่าวเนื่องจากจะต้องมีการเปลี่ยนแปลงเล็กน้อยในโค้ดดั้งเดิมที่ประสบปัญหานี้ อันที่จริงนี่คือการออกแบบมันไม่ควรเป็นปัญหาเลย!

33 Daryl May 03 2014 at 10:42

นี่เป็นวิธีง่ายๆที่ใช้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
32 yilmazburk Sep 19 2013 at 21:20

ลองใช้อันที่สั้นกว่านี้

  • ไม่มีอาร์เรย์

  • ไม่มีพิเศษสำหรับการวนซ้ำ


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

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

http://jsfiddle.net/7P6EN/

29 TravisJ Mar 06 2014 at 06:03

ปัญหาหลักเกี่ยวกับรหัสที่ OP แสดงคือiจะไม่อ่านจนกว่าจะถึงรอบที่สอง เพื่อสาธิตให้นึกภาพว่ามีข้อผิดพลาดภายในรหัส

funcs[i] = function() {            // and store them in funcs
    throw new Error("test");
    console.log("My value: " + i); // each should log its value.
};

ข้อผิดพลาดจริงจะไม่เกิดขึ้นจนกว่าจะถูกดำเนินการfuncs[someIndex] ()การใช้ตรรกะเดียวกันนี้ควรเป็นที่ชัดเจนว่าค่าของiจะยังไม่ถูกรวบรวมจนกว่าจะถึงจุดนี้ เมื่อลูปเดิมเสร็จสิ้นให้i++นำiไปสู่ค่า3ที่ส่งผลให้เงื่อนไขi < 3ล้มเหลวและการสิ้นสุดลูป ณ จุดนี้iเป็น3และดังนั้นเมื่อfuncs[someIndex]()ถูกนำมาใช้และiได้รับการประเมินก็คือ 3 - ทุกครั้ง

เพื่อให้ผ่านพ้นไปได้คุณต้องประเมินiตามที่พบ โปรดทราบว่าสิ่งนี้เกิดขึ้นแล้วในรูปแบบของfuncs[i](ซึ่งมีดัชนีที่ไม่ซ้ำกัน 3 รายการ) มีหลายวิธีในการจับค่านี้ หนึ่งคือการส่งผ่านเป็นพารามิเตอร์ไปยังฟังก์ชันซึ่งแสดงอยู่หลายวิธีแล้วที่นี่

อีกทางเลือกหนึ่งคือการสร้างวัตถุฟังก์ชันซึ่งจะสามารถปิดตัวแปรได้ ที่จะสำเร็จได้ด้วยประการฉะนี้

jsFiddle Demo

funcs[i] = new function() {   
    var closedVariable = i;
    return function(){
        console.log("My value: " + closedVariable); 
    };
};
24 Costa Dec 08 2016 at 00:33

ฟังก์ชัน JavaScript "ปิดทับ" ขอบเขตที่เข้าถึงได้เมื่อมีการประกาศและรักษาสิทธิ์การเข้าถึงขอบเขตนั้นไว้แม้ว่าตัวแปรในขอบเขตนั้นจะเปลี่ยนไปก็ตาม

var funcs = []

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

for (var k = 0; k < 3; k += 1) {
  funcs[k]()
}

แต่ละฟังก์ชั่นในอาร์เรย์ด้านบนปิดในขอบเขตส่วนกลาง (ส่วนกลางเพียงเพราะเป็นขอบเขตที่ประกาศไว้)

ในภายหลังฟังก์ชันเหล่านั้นจะเรียกใช้การบันทึกค่าปัจจุบันที่สุดของiขอบเขตส่วนกลาง นั่นคือความมหัศจรรย์และความยุ่งยากในการปิด

"ฟังก์ชั่น JavaScript ปิดเกินขอบเขตที่ประกาศไว้และยังคงเข้าถึงขอบเขตนั้นแม้ว่าค่าตัวแปรภายในขอบเขตนั้นจะเปลี่ยนไปก็ตาม"

การใช้letแทนการvarแก้ปัญหานี้โดยการสร้างขอบเขตใหม่ทุกครั้งที่forลูปทำงานสร้างขอบเขตแยกสำหรับแต่ละฟังก์ชันเพื่อปิดทับ เทคนิคอื่น ๆ อีกมากมายทำสิ่งเดียวกันกับฟังก์ชันพิเศษ

var funcs = []

for (let i = 0; i < 3; i += 1) {
  funcs[i] = function () {
    console.log(i)
  }
}

for (var k = 0; k < 3; k += 1) {
  funcs[k]()
}

( letทำให้ตัวแปรถูกบล็อกขอบเขตบล็อกจะแสดงด้วยวงเล็บปีกกา แต่ในกรณีของ for loop ตัวแปรการเริ่มต้นiในกรณีของเราจะถือว่าได้รับการประกาศในวงเล็บปีกกา)

15 wpding Jul 14 2014 at 21:42

หลังจากที่ได้อ่านผ่านโซลูชั่นต่างๆที่ผมอยากจะเพิ่มว่าเหตุผลที่ผู้ทำงานการแก้ปัญหาคือการพึ่งพาแนวคิดของห่วงโซ่ขอบเขต เป็นวิธีที่ JavaScript แก้ไขตัวแปรระหว่างการดำเนินการ

  • แต่ละรูปแบบฟังก์ชั่นความหมายขอบเขตประกอบด้วยตัวแปรท้องถิ่นประกาศโดยและvararguments
  • หากเรามีฟังก์ชันภายในที่กำหนดไว้ภายในฟังก์ชันอื่น (ภายนอก) สิ่งนี้จะรวมกันเป็นลูกโซ่และจะถูกใช้ในระหว่างการดำเนินการ
  • เมื่อมีฟังก์ชั่นได้รับการดำเนินรันไทม์ประเมินตัวแปรโดยการค้นหาห่วงโซ่ขอบเขต 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ที่สุด

โดยการรวมไว้ในฟังก์ชันภายนอกไม่ว่าจะกำหนดฟังก์ชันตัวช่วยอย่างชัดเจนเช่นhartoทำหรือใช้ฟังก์ชันที่ไม่ระบุตัวตนเช่นที่Bjornทำ:

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 ครั้งในลูป for แต่ละครั้งมีค่าที่iผูกไว้อย่างถูกต้อง จะไม่ใช้ค่าwindow.iเมื่อดำเนินการภายใน

สามารถดูรายละเอียดเพิ่มเติมได้ที่นี่
ซึ่งรวมถึงข้อผิดพลาดทั่วไปในการสร้างการปิดในลูปเหมือนกับสิ่งที่เรามีอยู่ที่นี่รวมถึงเหตุผลที่เราต้องปิดและพิจารณาประสิทธิภาพ

13 PrithviUppalapati Nov 07 2016 at 18:25

ด้วยคุณสมบัติใหม่ของการกำหนดขอบเขตระดับบล็อก 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
}

รหัสในคำถามของ OP จะถูกแทนที่ด้วยแทนletvar

10 ChristianLandgren Dec 10 2014 at 05:24

ฉันแปลกใจที่ยังไม่มีใครแนะนำให้ใช้forEachฟังก์ชันเพื่อหลีกเลี่ยง (อีกครั้ง) โดยใช้ตัวแปรท้องถิ่น ในความเป็นจริงฉันไม่ได้ใช้for(var i ...)เลยอีกต่อไปด้วยเหตุผลนี้

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

// แก้ไขเพื่อใช้forEachแทนแผนที่

9 jottos Apr 15 2009 at 13:18

เหตุผลที่ตัวอย่างเดิมของคุณใช้งานไม่ได้คือการปิดทั้งหมดที่คุณสร้างขึ้นในลูปอ้างอิงเฟรมเดียวกัน มีผลบังคับใช้ 3 วิธีในวัตถุเดียวโดยมีiตัวแปรเดียวเท่านั้น พวกเขาทั้งหมดพิมพ์ออกมาค่าเดียวกัน

9 sidhuko Jan 13 2018 at 20:17

คำถามนี้แสดงให้เห็นถึงประวัติของ JavaScript จริงๆ! ตอนนี้เราสามารถหลีกเลี่ยงการกำหนดขอบเขตบล็อกด้วยฟังก์ชันลูกศรและจัดการลูปได้โดยตรงจากโหนด DOM โดยใช้เมธอด Object

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>

8 AliKahoot Nov 04 2016 at 14:46

ก่อนอื่นทำความเข้าใจกับสิ่งที่ผิดพลาดกับรหัสนี้:

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
}

ที่นี่เมื่อfuncs[]อาร์เรย์จะถูกเริ่มต้นiจะถูกเพิ่มขึ้นที่funcsอาร์เรย์จะเริ่มต้นและขนาดของfuncอาร์เรย์จะกลายเป็น 3 i = 3,ดังนั้น ตอนนี้เมื่อfuncs[j]()ถูกเรียกใช้ตัวแปรอีกครั้งiซึ่งเพิ่มขึ้นเป็น 3 แล้ว

ตอนนี้เพื่อแก้ปัญหานี้เรามีตัวเลือกมากมาย ด้านล่างนี้คือสองรายการ:

  1. เราสามารถเริ่มต้นiด้วยletหรือเริ่มต้นตัวแปรใหม่indexด้วยletและทำให้มันเท่ากับi. ดังนั้นเมื่อมีการโทรindexจะถูกใช้และขอบเขตจะสิ้นสุดหลังจากการเริ่มต้น และสำหรับการโทรindexจะเริ่มต้นอีกครั้ง:

    var funcs = [];
    for (var i = 0; i < 3; i++) {          
        let index = i;
        funcs[i] = function() {            
            console.log("My value: " + index); 
        };
    }
    for (var j = 0; j < 3; j++) {
        funcs[j]();                        
    }
    
  2. ตัวเลือกอื่น ๆ สามารถแนะนำtempFuncซึ่งส่งคืนฟังก์ชันจริง:

    var funcs = [];
    function tempFunc(i){
        return function(){
            console.log("My value: " + i);
        };
    }
    for (var i = 0; i < 3; i++) {  
        funcs[i] = tempFunc(i);                                     
    }
    for (var j = 0; j < 3; j++) {
        funcs[j]();                        
    }
    
8 VikashSingh Jan 20 2017 at 17:02

ใช้โครงสร้างปิดซึ่งจะช่วยลดความพิเศษของคุณสำหรับห่วง คุณสามารถทำได้ในครั้งเดียวสำหรับการวนซ้ำ:

var funcs = [];
for (var i = 0; i < 3; i++) {     
  (funcs[i] = function() {         
    console.log("My value: " + i); 
  })(i);
}
7 BimalDas Jan 16 2018 at 21:29

เราจะตรวจสอบสิ่งที่เกิดขึ้นจริงเมื่อคุณประกาศvarและletทีละคน

กรณีที่ 1 : การใช้var

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

ตอนนี้เปิดหน้าต่างคอนโซลโครเมี่ยมของคุณโดยกดF12และรีเฟรชหน้า ใช้ทุก 3 ฟังก์ชันภายในอาร์เรย์คุณจะเห็นคุณสมบัติที่เรียกว่า[[Scopes]]ขยายฟังก์ชันนั้น คุณจะเห็นออบเจ็กต์อาร์เรย์หนึ่งตัวเรียกว่า"Global"ขยายอันนั้น คุณจะพบคุณสมบัติที่'i'ประกาศลงในวัตถุที่มีค่า 3

สรุป:

  1. เมื่อคุณประกาศตัวแปรโดยใช้'var'นอกฟังก์ชันตัวแปรนั้นจะกลายเป็นตัวแปรส่วนกลาง (คุณสามารถตรวจสอบได้โดยพิมพ์iหรือwindow.iในหน้าต่างคอนโซลมันจะส่งคืน 3)
  2. ฟังก์ชันที่น่ากลัวที่คุณประกาศจะไม่เรียกและตรวจสอบค่าภายในฟังก์ชันเว้นแต่คุณจะเรียกใช้ฟังก์ชัน
  3. เมื่อคุณเรียกใช้ฟังก์ชันconsole.log("My value: " + i)รับค่าจากGlobalวัตถุและแสดงผลลัพธ์

CASE2: ใช้ let

ตอนนี้แทนที่'var'ด้วย'let'

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

ทำสิ่งเดียวกันไปที่ขอบเขต ตอนนี้คุณจะเห็นสองวัตถุ"Block"และ"Global". ตอนนี้ขยายBlockออบเจ็กต์คุณจะเห็น 'i' ถูกกำหนดไว้ที่นั่นและสิ่งที่แปลกก็คือสำหรับทุกฟังก์ชันค่าถ้าiแตกต่างกัน (0, 1, 2)

สรุป:

เมื่อคุณประกาศตัวแปรโดยใช้'let'แม้จะอยู่นอกฟังก์ชัน แต่อยู่ในลูปตัวแปรนี้จะไม่เป็นตัวแปรโกลบอลมันจะกลายเป็นBlockตัวแปรระดับที่ใช้ได้เฉพาะกับฟังก์ชันเดียวกันเท่านั้นนั่นคือเหตุผลที่เราได้รับค่าที่iแตกต่างกัน สำหรับแต่ละฟังก์ชันเมื่อเราเรียกใช้ฟังก์ชัน

สำหรับรายละเอียดเพิ่มเติมเกี่ยวกับวิธีการทำงานที่ใกล้ชิดยิ่งขึ้นโปรดอ่านวิดีโอบทแนะนำที่ยอดเยี่ยม https://youtu.be/71AtaJpJHw0

4 RuneFS Jun 17 2015 at 19:02

คุณสามารถใช้โมดูลประกาศสำหรับรายการข้อมูลเช่นเคียวรี -Js (*) ในสถานการณ์เหล่านี้ฉันเองพบวิธีการที่เปิดเผยไม่น่าแปลกใจ

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

จากนั้นคุณสามารถใช้ลูปที่สองของคุณและได้ผลลัพธ์ที่คาดหวังหรือคุณสามารถทำได้

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

(*) ฉันเป็นผู้เขียนแบบสอบถาม -js และด้วยเหตุนี้จึงมีความลำเอียงในการใช้ดังนั้นอย่าใช้คำของฉันเป็นคำแนะนำสำหรับไลบรารีดังกล่าวสำหรับแนวทางการประกาศเท่านั้น :)

4 RaxWunter Dec 17 2015 at 21:14

ฉันชอบใช้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 มีความมหึมาน้อยกว่าโซลูชันอื่น ๆ

4 pixel67 May 05 2016 at 18:48

และอีกวิธีหนึ่ง: แทนที่จะสร้างลูปอื่นให้ผูกเข้าthisกับฟังก์ชัน return

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
}

การผูกสิ่งนี้จะช่วยแก้ปัญหาได้เช่นกัน

4 ShivangGupta May 02 2019 at 18:57

จนถึง ES5 ปัญหานี้สามารถแก้ไขได้โดยใช้การปิดเท่านั้น

แต่ตอนนี้ใน ES6 เรามีตัวแปรขอบเขตระดับบล็อก การเปลี่ยนvarเป็นlet in ก่อนสำหรับ loopจะช่วยแก้ปัญหาได้

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 (var j = 0; j < 3; j++) {
  funcs[j]();                      // and now let's run each one to see
}

3 Pawel Feb 28 2017 at 22:09

วิธีแก้ปัญหาหลายอย่างดูเหมือนจะถูกต้อง แต่ไม่ได้กล่าวถึงมันเรียกว่าCurryingซึ่งเป็นรูปแบบการออกแบบโปรแกรมที่ใช้งานได้สำหรับสถานการณ์เช่นนี้ เร็วกว่าการผูก 3-10 เท่าขึ้นอยู่กับเบราว์เซอร์

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

ดูประสิทธิภาพที่เพิ่มขึ้นในเบราว์เซอร์ต่างๆ

3 Buksy Nov 04 2016 at 15:58

รหัสของคุณใช้ไม่ได้เพราะสิ่งที่ทำคือ:

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คืออะไร"

ดังนั้นเป้าหมายของคุณคือครั้งแรกที่บันทึกค่าของฟังก์ชั่นและหลังจากที่บันทึกการทำงานเพื่อ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การวนซ้ำแต่ละครั้ง

นี่เป็นเพียงหนึ่งในหลายวิธีในการแก้ปัญหานี้

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