การปิด JavaScript ภายในลูป - ตัวอย่างการปฏิบัติง่ายๆ
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();
}
อะไรคือวิธีแก้ปัญหาพื้นฐานนี้?
คำตอบ
ปัญหาคือตัวแปร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" จะยังคงอยู่ตามที่คุณต้องการ
ลอง:
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
อีกวิธีหนึ่งที่ยังไม่ได้กล่าวถึงคือการใช้ 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]();
}
การใช้นิพจน์ฟังก์ชันที่เรียกใช้ทันทีวิธีที่ง่ายที่สุดและอ่านง่ายที่สุดในการใส่ตัวแปรดัชนี:
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
ไปงานปาร์ตี้ช้าไปหน่อย แต่วันนี้ฉันกำลังสำรวจปัญหานี้และสังเกตเห็นว่าคำตอบจำนวนมากไม่ได้กล่าวถึงวิธีการที่ 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]();
}
ดูสิว่าตอนนี้มันง่ายแค่ไหน! สำหรับข้อมูลเพิ่มเติมโปรดดูคำตอบนี้ซึ่งข้อมูลของฉันอ้างอิงจาก
เมื่อ 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 ทำให้ถูกต้องในที่สุด
อีกวิธีหนึ่งในการพูดก็คือiในฟังก์ชันของคุณถูกผูกไว้ในขณะที่เรียกใช้ฟังก์ชันไม่ใช่เวลาของการสร้างฟังก์ชัน
เมื่อคุณสร้างการปิดiเป็นการอ้างอิงถึงตัวแปรที่กำหนดในขอบเขตภายนอกไม่ใช่สำเนาของตัวแปรเหมือนตอนที่คุณสร้างการปิด จะได้รับการประเมินในขณะดำเนินการ
คำตอบอื่น ๆ ส่วนใหญ่มีวิธีแก้ปัญหาโดยการสร้างตัวแปรอื่นที่จะไม่เปลี่ยนค่าให้คุณ
แค่คิดว่าฉันจะเพิ่มคำอธิบายเพื่อความชัดเจน สำหรับวิธีแก้ปัญหาโดยส่วนตัวฉันจะไปกับ Harto เนื่องจากเป็นวิธีที่อธิบายตนเองได้มากที่สุดจากคำตอบที่นี่ รหัสใด ๆ ที่โพสต์จะใช้งานได้ แต่ฉันเลือกที่จะปิดโรงงานโดยต้องเขียนความคิดเห็นจำนวนมากเพื่ออธิบายว่าเหตุใดฉันจึงประกาศตัวแปรใหม่ (Freddy และ 1800) หรือมีไวยากรณ์การปิดแบบฝังตัวแปลก ๆ (apphacker)
สิ่งที่คุณต้องเข้าใจคือขอบเขตของตัวแปรในจาวาสคริปต์จะขึ้นอยู่กับฟังก์ชัน นี่เป็นข้อแตกต่างที่สำคัญกว่าการพูด 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]();
}
นี่เป็นอีกรูปแบบหนึ่งของเทคนิคที่คล้ายกับ (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ตัวแปรจะกลายเป็นตัวแปรแบบคงที่โดยผูกไว้กับสำเนาที่ส่งคืนของฟังก์ชันภายใน กล่าวคือการเปลี่ยนแปลงค่าจะถูกเก็บรักษาไว้ระหว่างการโทร มันมีประโยชน์มาก
สิ่งนี้อธิบายถึงข้อผิดพลาดทั่วไปในการใช้การปิดใน 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);
}
}
สิ่งนี้ได้ผลเนื่องจากตัวแปรโลคัลในขอบเขตฟังก์ชันโดยตรงเช่นเดียวกับตัวแปรอาร์กิวเมนต์ของฟังก์ชันได้รับการจัดสรรสำเนาใหม่เมื่อรายการ
วิธีแก้ปัญหาที่ง่ายที่สุดคือ
แทนที่จะใช้:
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ค่าจึงแตกต่างกันสำหรับแต่ละฟังก์ชันที่กำหนดไว้ภายในฟังก์ชันที่ไม่ระบุชื่อ
โซลูชันนี้ดูเหมือนจะเหมาะกับปัญหาดังกล่าวเนื่องจากจะต้องมีการเปลี่ยนแปลงเล็กน้อยในโค้ดดั้งเดิมที่ประสบปัญหานี้ อันที่จริงนี่คือการออกแบบมันไม่ควรเป็นปัญหาเลย!
นี่เป็นวิธีง่ายๆที่ใช้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
ลองใช้อันที่สั้นกว่านี้
ไม่มีอาร์เรย์
ไม่มีพิเศษสำหรับการวนซ้ำ
for (var i = 0; i < 3; i++) {
createfunc(i)();
}
function createfunc(i) {
return function(){console.log("My value: " + i);};
}
http://jsfiddle.net/7P6EN/
ปัญหาหลักเกี่ยวกับรหัสที่ 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);
};
};
ฟังก์ชัน 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ในกรณีของเราจะถือว่าได้รับการประกาศในวงเล็บปีกกา)
หลังจากที่ได้อ่านผ่านโซลูชั่นต่างๆที่ผมอยากจะเพิ่มว่าเหตุผลที่ผู้ทำงานการแก้ปัญหาคือการพึ่งพาแนวคิดของห่วงโซ่ขอบเขต เป็นวิธีที่ 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เมื่อดำเนินการภายใน
สามารถดูรายละเอียดเพิ่มเติมได้ที่นี่
ซึ่งรวมถึงข้อผิดพลาดทั่วไปในการสร้างการปิดในลูปเหมือนกับสิ่งที่เรามีอยู่ที่นี่รวมถึงเหตุผลที่เราต้องปิดและพิจารณาประสิทธิภาพ
ด้วยคุณสมบัติใหม่ของการกำหนดขอบเขตระดับบล็อก 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
ฉันแปลกใจที่ยังไม่มีใครแนะนำให้ใช้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แทนแผนที่
เหตุผลที่ตัวอย่างเดิมของคุณใช้งานไม่ได้คือการปิดทั้งหมดที่คุณสร้างขึ้นในลูปอ้างอิงเฟรมเดียวกัน มีผลบังคับใช้ 3 วิธีในวัตถุเดียวโดยมีiตัวแปรเดียวเท่านั้น พวกเขาทั้งหมดพิมพ์ออกมาค่าเดียวกัน
คำถามนี้แสดงให้เห็นถึงประวัติของ 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>
ก่อนอื่นทำความเข้าใจกับสิ่งที่ผิดพลาดกับรหัสนี้:
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 แล้ว
ตอนนี้เพื่อแก้ปัญหานี้เรามีตัวเลือกมากมาย ด้านล่างนี้คือสองรายการ:
เราสามารถเริ่มต้น
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](); }ตัวเลือกอื่น ๆ สามารถแนะนำ
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](); }
ใช้โครงสร้างปิดซึ่งจะช่วยลดความพิเศษของคุณสำหรับห่วง คุณสามารถทำได้ในครั้งเดียวสำหรับการวนซ้ำ:
var funcs = [];
for (var i = 0; i < 3; i++) {
(funcs[i] = function() {
console.log("My value: " + i);
})(i);
}
เราจะตรวจสอบสิ่งที่เกิดขึ้นจริงเมื่อคุณประกาศ
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
สรุป:
- เมื่อคุณประกาศตัวแปรโดยใช้
'var'นอกฟังก์ชันตัวแปรนั้นจะกลายเป็นตัวแปรส่วนกลาง (คุณสามารถตรวจสอบได้โดยพิมพ์iหรือwindow.iในหน้าต่างคอนโซลมันจะส่งคืน 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
คุณสามารถใช้โมดูลประกาศสำหรับรายการข้อมูลเช่นเคียวรี -Js (*) ในสถานการณ์เหล่านี้ฉันเองพบวิธีการที่เปิดเผยไม่น่าแปลกใจ
var funcs = Query.range(0,3).each(function(i){
return function() {
console.log("My value: " + i);
};
});
จากนั้นคุณสามารถใช้ลูปที่สองของคุณและได้ผลลัพธ์ที่คาดหวังหรือคุณสามารถทำได้
funcs.iterate(function(f){ f(); });
(*) ฉันเป็นผู้เขียนแบบสอบถาม -js และด้วยเหตุนี้จึงมีความลำเอียงในการใช้ดังนั้นอย่าใช้คำของฉันเป็นคำแนะนำสำหรับไลบรารีดังกล่าวสำหรับแนวทางการประกาศเท่านั้น :)
ฉันชอบใช้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 มีความมหึมาน้อยกว่าโซลูชันอื่น ๆ
และอีกวิธีหนึ่ง: แทนที่จะสร้างลูปอื่นให้ผูกเข้า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
}
การผูกสิ่งนี้จะช่วยแก้ปัญหาได้เช่นกัน
จนถึง 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
}
วิธีแก้ปัญหาหลายอย่างดูเหมือนจะถูกต้อง แต่ไม่ได้กล่าวถึงมันเรียกว่า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);
}
}
ดูประสิทธิภาพที่เพิ่มขึ้นในเบราว์เซอร์ต่างๆ
รหัสของคุณใช้ไม่ได้เพราะสิ่งที่ทำคือ:
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การวนซ้ำแต่ละครั้ง
นี่เป็นเพียงหนึ่งในหลายวิธีในการแก้ปัญหานี้
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
}