You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Web-Dev-For-Beginners/translations/th/3-terrarium/3-intro-to-DOM-and-closures/README.md

230 lines
26 KiB

<!--
CO_OP_TRANSLATOR_METADATA:
{
"original_hash": "30f8903a1f290e3d438dc2c70fe60259",
"translation_date": "2025-08-26T21:32:47+00:00",
"source_file": "3-terrarium/3-intro-to-DOM-and-closures/README.md",
"language_code": "th"
}
-->
# โครงการ Terrarium ตอนที่ 3: การจัดการ DOM และ Closure
![DOM และ Closure](../../../../translated_images/webdev101-js.10280393044d7eaaec7e847574946add7ddae6be2b2194567d848b61d849334a.th.png)
> ภาพสเก็ตโน้ตโดย [Tomomi Imura](https://twitter.com/girlie_mac)
## แบบทดสอบก่อนเรียน
[แบบทดสอบก่อนเรียน](https://ff-quizzes.netlify.app/web/quiz/19)
### บทนำ
การจัดการ DOM หรือ "Document Object Model" เป็นส่วนสำคัญของการพัฒนาเว็บ ตามที่ [MDN](https://developer.mozilla.org/docs/Web/API/Document_Object_Model/Introduction) กล่าวไว้ว่า "Document Object Model (DOM) คือการแสดงข้อมูลของวัตถุที่ประกอบขึ้นเป็นโครงสร้างและเนื้อหาของเอกสารบนเว็บ" ความท้าทายในการจัดการ DOM บนเว็บมักเป็นแรงผลักดันให้ใช้เฟรมเวิร์ก JavaScript แทนการใช้ JavaScript แบบดั้งเดิมเพื่อจัดการ DOM แต่ในบทเรียนนี้เราจะจัดการด้วยตัวเอง!
นอกจากนี้ บทเรียนนี้จะนำเสนอแนวคิดของ [JavaScript closure](https://developer.mozilla.org/docs/Web/JavaScript/Closures) ซึ่งคุณสามารถคิดว่าเป็นฟังก์ชันที่ถูกล้อมรอบด้วยฟังก์ชันอื่น เพื่อให้ฟังก์ชันภายในสามารถเข้าถึงขอบเขตของฟังก์ชันภายนอกได้
> JavaScript closures เป็นหัวข้อที่กว้างและซับซ้อน บทเรียนนี้จะกล่าวถึงแนวคิดพื้นฐานที่สุดที่ในโค้ดของ terrarium คุณจะพบ closure: ฟังก์ชันภายในและฟังก์ชันภายนอกที่ถูกสร้างขึ้นในลักษณะที่อนุญาตให้ฟังก์ชันภายในเข้าถึงขอบเขตของฟังก์ชันภายนอก สำหรับข้อมูลเพิ่มเติมเกี่ยวกับวิธีการทำงานนี้ โปรดเยี่ยมชม [เอกสารที่ครอบคลุม](https://developer.mozilla.org/docs/Web/JavaScript/Closures)
เราจะใช้ closure เพื่อจัดการ DOM
ลองนึกถึง DOM ว่าเป็นต้นไม้ที่แสดงถึงวิธีการทั้งหมดที่เอกสารเว็บเพจสามารถถูกจัดการได้ มีการเขียน API (Application Program Interfaces) ต่าง ๆ เพื่อให้นักพัฒนาโปรแกรมสามารถเข้าถึง DOM และแก้ไข เปลี่ยนแปลง จัดเรียงใหม่ และจัดการได้ตามต้องการ
![การแสดง DOM tree](../../../../translated_images/dom-tree.7daf0e763cbbba9273f9a66fe04c98276d7d23932309b195cb273a9cf1819b42.th.png)
> การแสดง DOM และ HTML markup ที่อ้างถึงมัน จาก [Olfa Nasraoui](https://www.researchgate.net/publication/221417012_Profile-Based_Focused_Crawler_for_Social_Media-Sharing_Websites)
ในบทเรียนนี้ เราจะทำโครงการ terrarium แบบโต้ตอบให้เสร็จสมบูรณ์โดยสร้าง JavaScript ที่จะช่วยให้ผู้ใช้สามารถจัดการพืชบนหน้าเว็บได้
### ความต้องการเบื้องต้น
คุณควรมี HTML และ CSS สำหรับ terrarium ของคุณสร้างไว้แล้ว เมื่อจบบทเรียนนี้คุณจะสามารถย้ายพืชเข้าและออกจาก terrarium โดยการลากและวางได้
### งาน
ในโฟลเดอร์ terrarium ของคุณ สร้างไฟล์ใหม่ชื่อ `script.js` และนำเข้าไฟล์นั้นในส่วน `<head>`:
```html
<script src="./script.js" defer></script>
```
> หมายเหตุ: ใช้ `defer` เมื่อนำเข้าไฟล์ JavaScript ภายนอกในไฟล์ html เพื่อให้ JavaScript ทำงานหลังจากไฟล์ HTML ถูกโหลดเสร็จสมบูรณ์ คุณสามารถใช้ attribute `async` ได้เช่นกัน ซึ่งอนุญาตให้สคริปต์ทำงานในขณะที่ไฟล์ HTML กำลังถูกแปลง แต่ในกรณีของเรา สิ่งสำคัญคือต้องให้ element HTML พร้อมใช้งานสำหรับการลากก่อนที่เราจะอนุญาตให้สคริปต์ลากทำงาน
---
## องค์ประกอบ DOM
สิ่งแรกที่คุณต้องทำคือสร้างการอ้างอิงไปยังองค์ประกอบที่คุณต้องการจัดการใน DOM ในกรณีของเรา คือพืช 14 ชนิดที่รออยู่ในแถบด้านข้าง
### งาน
```html
dragElement(document.getElementById('plant1'));
dragElement(document.getElementById('plant2'));
dragElement(document.getElementById('plant3'));
dragElement(document.getElementById('plant4'));
dragElement(document.getElementById('plant5'));
dragElement(document.getElementById('plant6'));
dragElement(document.getElementById('plant7'));
dragElement(document.getElementById('plant8'));
dragElement(document.getElementById('plant9'));
dragElement(document.getElementById('plant10'));
dragElement(document.getElementById('plant11'));
dragElement(document.getElementById('plant12'));
dragElement(document.getElementById('plant13'));
dragElement(document.getElementById('plant14'));
```
เกิดอะไรขึ้นที่นี่? คุณกำลังอ้างอิงเอกสารและค้นหาใน DOM เพื่อหาองค์ประกอบที่มี Id เฉพาะ จำได้ไหมว่าในบทเรียนแรกเกี่ยวกับ HTML คุณได้ให้ Id เฉพาะกับแต่ละภาพพืช (`id="plant1"`)? ตอนนี้คุณจะใช้ประโยชน์จากความพยายามนั้น หลังจากระบุองค์ประกอบแต่ละรายการ คุณส่งรายการนั้นไปยังฟังก์ชันที่เรียกว่า `dragElement` ซึ่งคุณจะสร้างในอีกสักครู่ ดังนั้นองค์ประกอบใน HTML จะสามารถลากได้ หรือจะสามารถลากได้ในไม่ช้า
✅ ทำไมเราถึงอ้างอิงองค์ประกอบโดย Id? ทำไมไม่ใช้ CSS class? คุณอาจอ้างอิงบทเรียนก่อนหน้านี้เกี่ยวกับ CSS เพื่อหาคำตอบสำหรับคำถามนี้
---
## Closure
ตอนนี้คุณพร้อมที่จะสร้าง closure `dragElement` ซึ่งเป็นฟังก์ชันภายนอกที่ล้อมรอบฟังก์ชันภายในหนึ่งหรือหลายฟังก์ชัน (ในกรณีของเรา เราจะมีสามฟังก์ชัน)
Closure มีประโยชน์เมื่อฟังก์ชันหนึ่งหรือหลายฟังก์ชันต้องการเข้าถึงขอบเขตของฟังก์ชันภายนอก นี่คือตัวอย่าง:
```javascript
function displayCandy(){
let candy = ['jellybeans'];
function addCandy(candyType) {
candy.push(candyType)
}
addCandy('gumdrops');
}
displayCandy();
console.log(candy)
```
ในตัวอย่างนี้ ฟังก์ชัน `displayCandy` ล้อมรอบฟังก์ชันที่เพิ่มประเภทขนมใหม่ลงในอาร์เรย์ที่มีอยู่ในฟังก์ชัน หากคุณเรียกใช้โค้ดนี้ อาร์เรย์ `candy` จะไม่สามารถเข้าถึงได้ เนื่องจากมันเป็นตัวแปรท้องถิ่น (ท้องถิ่นต่อ closure)
✅ คุณจะทำให้อาร์เรย์ `candy` เข้าถึงได้อย่างไร? ลองย้ายมันออกไปนอก closure ด้วยวิธีนี้ อาร์เรย์จะกลายเป็น global แทนที่จะยังคงอยู่ในขอบเขตท้องถิ่นของ closure
### งาน
ภายใต้การประกาศองค์ประกอบใน `script.js` สร้างฟังก์ชัน:
```javascript
function dragElement(terrariumElement) {
//set 4 positions for positioning on the screen
let pos1 = 0,
pos2 = 0,
pos3 = 0,
pos4 = 0;
terrariumElement.onpointerdown = pointerDrag;
}
```
`dragElement` ได้รับวัตถุ `terrariumElement` จากการประกาศที่ด้านบนของสคริปต์ จากนั้นคุณตั้งค่าตำแหน่งท้องถิ่นที่ `0` สำหรับวัตถุที่ส่งผ่านไปยังฟังก์ชัน นี่คือตัวแปรท้องถิ่นที่จะถูกจัดการสำหรับแต่ละองค์ประกอบเมื่อคุณเพิ่มฟังก์ชันลากและวางภายใน closure ให้กับแต่ละองค์ประกอบ terrarium จะถูกเติมเต็มด้วยองค์ประกอบที่ถูกลากเหล่านี้ ดังนั้นแอปพลิเคชันจำเป็นต้องติดตามตำแหน่งที่วางไว้
นอกจากนี้ `terrariumElement` ที่ถูกส่งไปยังฟังก์ชันนี้จะถูกกำหนดเหตุการณ์ `pointerdown` ซึ่งเป็นส่วนหนึ่งของ [web APIs](https://developer.mozilla.org/docs/Web/API) ที่ออกแบบมาเพื่อช่วยในการจัดการ DOM `onpointerdown` จะทำงานเมื่อปุ่มถูกกด หรือในกรณีของเรา เมื่อองค์ประกอบที่สามารถลากได้ถูกสัมผัส เหตุการณ์นี้ทำงานได้ทั้ง [เว็บและเบราว์เซอร์มือถือ](https://caniuse.com/?search=onpointerdown) โดยมีข้อยกเว้นบางประการ
✅ [event handler `onclick`](https://developer.mozilla.org/docs/Web/API/GlobalEventHandlers/onclick) มีการสนับสนุนข้ามเบราว์เซอร์มากกว่า; ทำไมคุณถึงไม่ใช้มันที่นี่? ลองคิดถึงประเภทของการโต้ตอบหน้าจอที่คุณพยายามสร้างที่นี่
---
## ฟังก์ชัน Pointerdrag
`terrariumElement` พร้อมที่จะถูกลากไปรอบ ๆ เมื่อเหตุการณ์ `onpointerdown` ถูกเรียกใช้ ฟังก์ชัน `pointerDrag` จะถูกเรียกใช้ เพิ่มฟังก์ชันนั้นใต้บรรทัดนี้: `terrariumElement.onpointerdown = pointerDrag;`:
### งาน
```javascript
function pointerDrag(e) {
e.preventDefault();
console.log(e);
pos3 = e.clientX;
pos4 = e.clientY;
}
```
มีหลายสิ่งเกิดขึ้นที่นี่ อย่างแรก คุณป้องกันเหตุการณ์เริ่มต้นที่ปกติจะเกิดขึ้นเมื่อ pointerdown โดยใช้ `e.preventDefault();` ด้วยวิธีนี้คุณจะมีการควบคุมพฤติกรรมของอินเทอร์เฟซมากขึ้น
> กลับมาที่บรรทัดนี้เมื่อคุณสร้างไฟล์สคริปต์เสร็จสมบูรณ์และลองทำโดยไม่มี `e.preventDefault()` - จะเกิดอะไรขึ้น?
ถัดไป เปิด `index.html` ในหน้าต่างเบราว์เซอร์ และตรวจสอบอินเทอร์เฟซ เมื่อคุณคลิกที่พืช คุณจะเห็นว่าเหตุการณ์ 'e' ถูกจับ ลองเจาะลึกเข้าไปในเหตุการณ์เพื่อดูว่ามีข้อมูลมากแค่ไหนที่ถูกรวบรวมโดยเหตุการณ์ pointerdown หนึ่งครั้ง!
จากนั้น สังเกตว่าตัวแปรท้องถิ่น `pos3` และ `pos4` ถูกตั้งค่าเป็น e.clientX คุณสามารถค้นหาค่าของ `e` ในหน้าต่างการตรวจสอบ ค่าพวกนี้จับพิกัด x และ y ของพืชในขณะที่คุณคลิกหรือสัมผัสมัน คุณจะต้องการการควบคุมที่ละเอียดอ่อนต่อพฤติกรรมของพืชเมื่อคุณคลิกและลากมัน ดังนั้นคุณจึงติดตามพิกัดของมัน
✅ มันเริ่มชัดเจนขึ้นหรือยังว่าทำไมแอปนี้ถึงถูกสร้างด้วย closure ขนาดใหญ่? ถ้าไม่ใช่ closure คุณจะรักษาขอบเขตสำหรับพืชที่สามารถลากได้ทั้ง 14 ชนิดได้อย่างไร?
ทำฟังก์ชันเริ่มต้นให้เสร็จโดยเพิ่มการจัดการเหตุการณ์ pointer อีกสองรายการใต้ `pos4 = e.clientY`:
```html
document.onpointermove = elementDrag;
document.onpointerup = stopElementDrag;
```
ตอนนี้คุณกำลังระบุว่าคุณต้องการให้พืชถูกลากไปพร้อมกับ pointer เมื่อคุณเคลื่อนมัน และให้การลากหยุดเมื่อคุณยกเลิกการเลือกพืช `onpointermove` และ `onpointerup` เป็นส่วนหนึ่งของ API เดียวกันกับ `onpointerdown` อินเทอร์เฟซจะโยนข้อผิดพลาดในตอนนี้เนื่องจากคุณยังไม่ได้กำหนดฟังก์ชัน `elementDrag` และ `stopElementDrag` ดังนั้นสร้างฟังก์ชันเหล่านั้นต่อไป
## ฟังก์ชัน elementDrag และ stopElementDrag
คุณจะทำ closure ของคุณให้เสร็จโดยเพิ่มฟังก์ชันภายในอีกสองฟังก์ชันที่จะจัดการสิ่งที่เกิดขึ้นเมื่อคุณลากพืชและหยุดลากมัน พฤติกรรมที่คุณต้องการคือคุณสามารถลากพืชใด ๆ ได้ทุกเมื่อและวางมันไว้ที่ใดก็ได้บนหน้าจอ อินเทอร์เฟซนี้ค่อนข้างไม่จำกัด (ไม่มี drop zone ตัวอย่างเช่น) เพื่อให้คุณสามารถออกแบบ terrarium ของคุณได้ตามที่คุณต้องการโดยการเพิ่ม ลบ และจัดตำแหน่งพืชใหม่
### งาน
เพิ่มฟังก์ชัน `elementDrag` หลังจากปิดวงเล็บปีกกาของ `pointerDrag`:
```javascript
function elementDrag(e) {
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
console.log(pos1, pos2, pos3, pos4);
terrariumElement.style.top = terrariumElement.offsetTop - pos2 + 'px';
terrariumElement.style.left = terrariumElement.offsetLeft - pos1 + 'px';
}
```
ในฟังก์ชันนี้ คุณทำการแก้ไขตำแหน่งเริ่มต้น 1-4 ที่คุณตั้งค่าเป็นตัวแปรท้องถิ่นในฟังก์ชันภายนอก เกิดอะไรขึ้นที่นี่?
เมื่อคุณลาก คุณกำหนดค่าใหม่ให้กับ `pos1` โดยทำให้มันเท่ากับ `pos3` (ซึ่งคุณตั้งค่าไว้ก่อนหน้านี้เป็น `e.clientX`) ลบด้วยค่าปัจจุบันของ `e.clientX` คุณทำการดำเนินการที่คล้ายกันกับ `pos2` จากนั้นคุณตั้งค่าใหม่ให้กับ `pos3` และ `pos4` เป็นพิกัด X และ Y ใหม่ขององค์ประกอบ คุณสามารถดูการเปลี่ยนแปลงเหล่านี้ในคอนโซลขณะที่คุณลาก จากนั้นคุณจัดการสไตล์ css ของพืชเพื่อกำหนดตำแหน่งใหม่ของมันตามตำแหน่งใหม่ของ `pos1` และ `pos2` โดยคำนวณพิกัด X และ Y ด้านบนและด้านซ้ายของพืชโดยเปรียบเทียบ offset กับตำแหน่งใหม่เหล่านี้
> `offsetTop` และ `offsetLeft` เป็นคุณสมบัติ CSS ที่ตั้งค่าตำแหน่งขององค์ประกอบตามตำแหน่งของ parent ซึ่ง parent สามารถเป็นองค์ประกอบใด ๆ ที่ไม่ได้ถูกตั้งค่าเป็น `static`
การคำนวณตำแหน่งใหม่ทั้งหมดนี้ช่วยให้คุณปรับแต่งพฤติกรรมของ terrarium และพืชได้อย่างละเอียด
### งาน
งานสุดท้ายในการทำอินเทอร์เฟซให้เสร็จคือการเพิ่มฟังก์ชัน `stopElementDrag` หลังจากปิดวงเล็บปีกกาของ `elementDrag`:
```javascript
function stopElementDrag() {
document.onpointerup = null;
document.onpointermove = null;
}
```
ฟังก์ชันเล็ก ๆ นี้รีเซ็ตเหตุการณ์ `onpointerup` และ `onpointermove` เพื่อให้คุณสามารถเริ่มต้นความคืบหน้าของพืชใหม่ได้โดยเริ่มลากมันอีกครั้ง หรือเริ่มลากพืชใหม่
✅ จะเกิดอะไรขึ้นถ้าคุณไม่ตั้งค่าเหตุการณ์เหล่านี้เป็น null?
ตอนนี้คุณได้ทำโครงการของคุณเสร็จสมบูรณ์แล้ว!
🥇ขอแสดงความยินดี! คุณได้สร้าง terrarium ที่สวยงามเสร็จสมบูรณ์แล้ว! ![terrarium ที่เสร็จสมบูรณ์](../../../../translated_images/terrarium-final.0920f16e87c13a84cd2b553a5af9a3ad1cffbd41fbf8ce715d9e9c43809a5e2c.th.png)
---
## 🚀ความท้าทาย
เพิ่ม event handler ใหม่ใน closure ของคุณเพื่อทำสิ่งเพิ่มเติมกับพืช เช่น การดับเบิลคลิกที่พืชเพื่อทำให้มันอยู่ด้านหน้า ลองสร้างสรรค์ดู!
## แบบทดสอบหลังเรียน
[แบบทดสอบหลังเรียน](https://ff-quizzes.netlify.app/web/quiz/20)
## ทบทวนและศึกษาด้วยตนเอง
แม้ว่าการลากองค์ประกอบไปรอบ ๆ หน้าจอจะดูเหมือนเป็นเรื่องเล็กน้อย แต่มีหลายวิธีในการทำสิ่งนี้และมีข้อผิดพลาดมากมาย ขึ้นอยู่กับผลลัพธ์ที่คุณต้องการ ในความเป็นจริง มี [drag and drop API](https://developer.mozilla.org/docs/Web/API/HTML_Drag_and_Drop_API) ทั้งหมดที่คุณสามารถลองใช้ได้ เราไม่ได้ใช้มันในโมดูลนี้เพราะผลลัพธ์ที่เราต้องการแตกต่างออกไป แต่ลองใช้ API นี้ในโครงการของคุณเองและดูว่าคุณสามารถทำอะไรได้บ้าง
ค้นหาข้อมูลเพิ่มเติมเกี่ยวกับ pointer events ได้ที่ [เอกสาร W3C](https://www.w3.org/TR/pointerevents1/) และ [เอกสาร MDN](https://developer.mozilla.org/docs/Web/API/Pointer_events)
ตรวจสอบความสามารถของเบราว์เซอร์เสมอโดยใช้ [CanIUse.com](https://caniuse.com/)
## งานที่ได้รับมอบหมาย
[ทำงานเพิ่มเติมกับ DOM](assignment.md)
---
**ข้อจำกัดความรับผิดชอบ**:
เอกสารนี้ได้รับการแปลโดยใช้บริการแปลภาษา AI [Co-op Translator](https://github.com/Azure/co-op-translator) แม้ว่าเราจะพยายามให้การแปลมีความถูกต้อง แต่โปรดทราบว่าการแปลโดยอัตโนมัติอาจมีข้อผิดพลาดหรือความไม่ถูกต้อง เอกสารต้นฉบับในภาษาดั้งเดิมควรถือเป็นแหล่งข้อมูลที่เชื่อถือได้ สำหรับข้อมูลที่สำคัญ ขอแนะนำให้ใช้บริการแปลภาษามืออาชีพ เราไม่รับผิดชอบต่อความเข้าใจผิดหรือการตีความผิดที่เกิดจากการใช้การแปลนี้