ข้อผิดพลาดจากการซ้ำซ้อนของฟังก์ชันสำหรับแก้ไของค์ประกอบ DOM ใน JavaScript
สมมติว่าเรามีรายการบางอย่าง:
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
ให้เราได้รายการและรายการย่อยของมัน มาเก็บในตัวแปรแยก:
let ul = document.querySelector('ul');
let lis = document.querySelectorAll('li');
ทำให้รายการย่อยในรายการของเรา สามารถแก้ไขได้ด้วยอินพุตที่ปรากฏขึ้น:
for (let li of lis) {
li.addEventListener('click', function func() {
let input = document.createElement('input');
input.value = li.textContent;
li.textContent = '';
li.append(input);
input.addEventListener('blur', function() {
li.textContent = this.value;
li.addEventListener('click', func);
});
li.removeEventListener('click', func);
});
}
สมมติว่าตอนนี้เราต้องการให้สามารถเพิ่ม รายการใหม่เข้าไปในรายการได้ เพื่อจุดประสงค์นี้ ให้เรามีอินพุตที่สอดคล้องกัน อยู่ใต้รายการ:
<input id="adder">
ให้เราได้ลิงก์ไปยังอินพุตนี้มาเก็บในตัวแปร:
let adder = document.querySelector('#adder');
ทำให้เมื่ออินพุตเสียโฟกัส รายการใหม่ที่มีข้อความ ที่นำมาจากอินพุตของเราจะถูกเพิ่มเข้าไปในรายการ:
adder.addEventListener('blur', function() {
let li = document.createElement('li');
li.textContent = this.value;
ul.append(li);
});
สมมติว่าตอนนี้เราต้องการให้รายการที่เพิ่มเข้ามาใหม่ สามารถแก้ไขได้เช่นกัน การแก้ไขจะไม่ทำงานสำหรับพวกมัน โดยอัตโนมัติ เพราะเมื่อเราแนบตัวจัดการเหตุการณ์การคลิก ไปยังรายการในรายการ พวกนั้นยังไม่ปรากฏอยู่
ลองดูตัวเลือกที่เป็นไปได้ สำหรับการแก้ไขปัญหานี้
วิธีแก้ไขแรก
วิธีแก้ไขที่ง่ายที่สุดคือการคัดลอกโค้ด
ของฟังก์ชัน func โดยเชื่อมโยงมัน
สำหรับรายการที่สร้างขึ้นใหม่ด้วย:
adder.addEventListener('blur', function() {
let li = document.createElement('li');
li.textContent = this.value;
li.addEventListener('click', function func() {
// ที่นี่เราคัดลอกโค้ด
});
ul.append(li);
});
แน่นอนว่าในวิธีแก้ไขนี้เราเห็นข้อเสีย ทันที - การคัดลอกโค้ดไม่ถูกต้อง
วิธีแก้ไขที่สอง
เพื่อแก้ไขปัญหาการซ้ำซ้อน
เป็นตรรกะที่จะดึงฟังก์ชัน func
ออกมาข้างนอก ทำให้มันเป็น Function Declaration:
function func() {
let input = document.createElement('input');
input.value = li.textContent;
li.textContent = '';
li.append(input);
input.addEventListener('blur', function() {
li.textContent = this.value;
li.addEventListener('click', func);
});
li.removeEventListener('click', func);
}
และนี่คือปัญหาที่รอเราอยู่
ความจริงก็คือฟังก์ชันของเราใช้
ตัวแปร li ที่ได้
มาจากขอบเขตภายนอก
แต่หลังจากดึงฟังก์ชันออกมาแล้ว ตัวแปรนั้น
ตอนนี้ไม่เห็นแล้ว!
เพื่อแก้ไขปัญหา เราจะส่งผ่าน
li ของเราเป็นพารามิเตอร์:
function func(li) {
let input = document.createElement('input');
input.value = li.textContent;
li.textContent = '';
li.append(input);
input.addEventListener('blur', function() {
li.textContent = this.value;
li.addEventListener('click', func);
});
li.removeEventListener('click', func);
}
และที่นี่ วิธีแก้ไขของเราสร้าง ปัญหาอีกอย่างหนึ่งขึ้นมา ความจริงก็คือ คุณไม่สามารถส่งพารามิเตอร์ เข้าไปในตัวจัดการเหตุการณ์ได้ง่ายๆ:
for (let li of lis) {
li.addEventListener('click', func(li)); // ไม่ทำงาน!
}
เพื่อแก้ไขปัญหานี้ เพียงเรียกใช้ ฟังก์ชันของเราภายในตัวจัดการเหตุการณ์แบบไม่มีชื่อ:
for (let li of lis) {
li.addEventListener('click', function() {
func(li);
});
}
และทำในลักษณะเดียวกัน เมื่อสร้างรายการใหม่ในรายการ:
adder.addEventListener('blur', function() {
let li = document.createElement('li');
li.textContent = this.value;
li.addEventListener('click', function() {
func(li);
});
ul.append(li);
});
วิธีแก้ไขที่สาม
มีวิธีแก้ไขที่สง่างามกว่า คุณสามารถใช้ การมอบหมายเหตุการณ์ (Event Delegation) ในกรณีนี้ ปัญหากับรายการใหม่ในรายการ ก็จะไม่เกิดขึ้น:
ul.addEventListener('click', function(event) {
if (event.target.tagName === 'LI') { // จับการคลิกที่ li โดยเฉพาะ ไม่ใช่ที่อินพุต
let li = event.target;
let input = document.createElement('input');
input.value = li.textContent;
li.textContent = '';
li.append(input);
input.addEventListener('blur', function() {
li.textContent = this.value;
});
}
});
ในกรณีนี้ ลูปตามรายการในรายการ จะไม่จำเป็นสำหรับเราเลย และโค้ด สำหรับการสร้างรายการใหม่ในรายการ จะสั้นลงเหลือแค่นี้:
adder.addEventListener('blur', function() {
let li = document.createElement('li');
li.textContent = this.value;
ul.append(li);
});