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