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/3-terrarium/3-intro-to-DOM-and-closures/translations
Lee Stott 2daab5271b
Update Quiz Link
1 year ago
..
README.es.md Update Quiz Link 1 year ago
README.fr.md Update Quiz Link 1 year ago
README.hi.md Update Quiz Link 1 year ago
README.it.md Update Quiz Link 1 year ago
README.ja.md Update Quiz Link 1 year ago
README.ko.md Update Quiz Link 1 year ago
README.ms.md Update Quiz Link 1 year ago
README.pt.md Update Quiz Link 1 year ago
README.zh-tw.md Update Quiz Link 1 year ago
assignment.es.md Update assignment.es.md 5 years ago
assignment.fr.md Update assignment.fr.md 5 years ago
assignment.hi.md Update assignment.hi.md 5 years ago
assignment.it.md Italian translation of chapter 3 6 years ago
assignment.ja.md Update assignment.ja.md 5 years ago
assignment.ko.md Update assignment.ko.md 5 years ago
assignment.ms.md Update assignment.ms.md 5 years ago
assignment.nl.md Update assignment.nl.md 5 years ago
assignment.pt.md translate pt: intro dom closures assignment (#483) 5 years ago
assignment.zh-cn.md [zh-cn] 3/3/assignment.zh-cn.md 5 years ago
assignment.zh-tw.md removing references to en-US 5 years ago

README.zh-tw.md

盆栽盒專案 Part 3 - DOM 元素控制與閉包

DOM 元素與閉包

由 Tomomi Imura 繪製

課前測驗

課前測驗

大綱

操作 DOM (Document Object Model) 是網頁開發的一項關鍵。根據 MDN 文件, 「Document Object Model (DOM) 元素能根據網頁文件的結構與內容來呈現物件」。藉由使用 JavaScript 框架而非原始的 JavaScript 程式碼來管理 DOM,在網頁上操作 DOM 的挑戰已經不比以前困難了,但這裡我們要自己來管理它們!

此外,這堂課也會介紹有關JavaScript 閉包(Closure)的概念,你可以想像成一個函式被包在另一個函式中,以訪問外面函式範圍中的變數。

JavaScript 閉包是個廣闊且複雜的主題。本堂課只觸及建立盆栽盒需要的最基礎概念。你能得知一個閉包為:內部函式和外部函式建立一項關係,允許內部函式存取外部函式的變數等作用域。要得知更多關於閉包的原理,請造訪觀看額外的文件。

我們會使用閉包來操控 DOM。

想像 DOM 就像一棵樹,表現出所有操作網頁的方式。多樣的 APIs (Application Program Interfaces) 提供程式開發者,依照自己使用的程式語言,以存取、編輯、編排等方式管理 DOM 元素。

DOM 樹的表達

HTML 語法會參考 DOM 的呈現方式。出自 Olfa Nasraoui。

在這堂課中,我們會完成我們的盆栽盒專案,建立 JavaScript 來對網頁中的植物進行互動式操作。

開始之前

確保盆栽盒的 HTML 與 CSS 已經編輯完成。這堂課會新增拖曳植物進出盆栽罐的功能。

課題

在專案資料夾中,新增檔案 script.js。 匯入該檔案在 HTML 檔 <head> 的部分:

	<script src="./script.js" defer></script>

筆記:匯入外部 JavaScript 檔案到 HTML 檔案須使用 defer,讓 JavaScript 檔案只有在 HTML 被完全載入時才被執行。你也可以使用 async 的屬性,允許 JavaScript 在解析 HTML 檔時就被執行。這項專案中,我們必須確保 HTML 的元件被完整建立後才允許使用拖曳功能。


DOM 元素

我們要做的第一件事是建立 DOM 下,要被操控的物件的連結。在專案例子中,我們有罐子外的十四株植物等著被拖曳。

課題

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)

現在,你已經準備好要建立 dragElement 閉包,建立包在外部函式內的內部函式組,在我們的例子中,會用上三個函式。

閉包在一或多個以上函式要存取外部函式時非常好用。看看下面的例子:

function displayCandy(){
	let candy = ['jellybeans'];
	function addCandy(candyType) {
		candy.push(candyType)
	}
	addCandy('gumdrops');
}
displayCandy();
console.log(candy)

這項例子中,函式 displayCandy 包住另一個函式 addCandy,新增新的糖果樣式到已存在的矩陣當中。當執行這段程式時,矩陣 candy 會被認作是未定義,因為它是函式的本地變數。

✅ 你能讓矩陣 candy 被存取嗎?試著將它移到閉包外面。這時,矩陣會變成全域變數,取消閉包內的存取限制。

課題

在檔案 script.js 的元素宣告下方,新增函式:

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 的變數給函式內的物件使用。它們是本地變數,給每一個進到拖曳函式內的物件操控。盆栽盒會被這些拖曳物件填充,我們的網頁應用必須要持續追蹤這些物件的位置。

此外,進到函式的 terrariumElement 也被新增了 pointerdown 事件,它是管理 DOM 的其中一項網頁 APIs。當按鈕按下時,或是在我們案例中,一個拖曳物件被點擊時,onpointerdown 事件就會被觸發。這個事件處理器(event handler)皆運作在網頁與行動瀏覽器上,只有少部分的例外。

✅ 事件處理器 onclick支援更多的瀏覽器。為什麼我們不在這邊使用它? 想想看我們在這此建立的視窗互動類型。


函式 pointerDrag

terrariumElement 已經準備好被拖曳了。當觸發 onpointerdown 事件時,函式 pointerDrag 會參與其中。新增這項函式在程式碼 terrariumElement.onpointerdown = pointerDrag; 下方:

課題

function pointerDrag(e) {
	e.preventDefault();
	console.log(e);
	pos3 = e.clientX;
	pos4 = e.clientY;
}

許多事情會發生。首先,你使用 e.preventDefault(); 取消掉 pointerdown 原先的預設事件。這樣你可以操作更多的介面行為。

回到你建立的程式碼中,試著刪除 e.preventDefault() 並執行看看,發生了什麼事?

第二,用瀏覽器打開 index.html 並調查我們的介面。當你點擊植物時,你可以發現 'e' 事件被觸發了。專研一下,一個 pointerdown 事件會產生多少資訊!

接下來,紀錄本地變數 pos3 和 pos4 被設定為 e.clientX 和 e.clientY。你可以在觀察面板中,會發現 e 的數值。這項數值取得按下植物瞬間的 x 與 y 座標資訊。為了全面的控制植物行為,在拖曳植物時,我們會持續更新座標資訊。

✅ 將整個網頁應用建立在一個大閉包下,會讓程式碼變得比較清楚嗎?如果沒有,你有其他方法管理這十四株可拖曳的植物嗎?

增加初始化函式,在程式碼 pos4 = e.clientY 下方加上下列兩行事件處理:

document.onpointermove = elementDrag;
document.onpointerup = stopElementDrag;

現在,在游標拖曳時,你的植物能跟著你的游標走,而在你取消點擊時停下來。onpointermove 和 onpointerup 也是 onpointerdown 類型相同的 API。然而,現在介面會出現錯誤訊息,因為我們還沒建立函式 elementDrag 與 stopElementDrag。

函式 elementDrag 與 stopElementDrag

新增兩條內部函式在閉包中,它們會處理拖曳植物與停止拖曳的事件。你希望你可以拖曳任何一株植物且放在螢幕上的任一地方。介面並沒有強制你盆栽盒的配置格式,你可以自由地增加、移除與移動盆栽罐內的植物。

課題

新增函式 elementDrag 在函式閉包 pointerDrag 宣告列的正下方:

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

在這條函式之前,你編輯了四個本地變數位置的初始值在外部函式中。這邊又做了什麼事?

當你拖曳物件時,你更新數值 pos1 為 pos3 減去現在的 e.clientX,而 pos3 在之前被初始化為為 e.clientX。同樣的行為套用在 pos2上。之後,你更新 pos3 與 pos4 到新的 XY 座標點位置。你能在 console 下看到數值在拖曳下更新的情況。我們也更新植物的 CSS 造型中的定位點為 pos1 與 pos2,比較植物左上方座標點與新座標點的關係。

offsetTop 和 offsetLeft 是 CSS 的屬性,決定物件與它父關係物件的定位關係。父關係物件可以是任何元素,只要它的定位屬性不為 static。

這些座標點的計算式讓你成功校整了植物與盆栽盒之間的行為。

課題

最後的課題是在介面上新增 stopElementDrag 函式,我們將它加在函式閉包 elementDrag 的正下方:

function stopElementDrag() {
	document.onpointerup = null;
	document.onpointermove = null;
}

這條小函式重制 onpointerup 與 onpointermove 事件,這樣你可以重新開始該植物的拖曳事件,或是拖曳新的植物。

✅ 如果不將這些事件設為空值時,會發生什麼事?

我們終於完成了這項專案!

🥇 恭喜你!你建立了一個漂亮的盆栽盒。盆栽盒成果圖


🚀 挑戰

新增新的事件處理器到你的閉包中,讓你能對植物做更多的事情。舉例來說,雙擊植物讓它排列到最上層。發揮你的創意吧!

課後測驗

課後測驗

複習與自學

在螢幕上拖曳物件看似簡單,但依照不同的目的與實現方法會遭遇到不同的問題。事實上,這邊有一份關於你可以嘗試的拖曳 API。我們沒在專案中使用是為了建立不一樣的實現方法,試著使用這些 API 到專案中,看看你能完成什麼。

在 W3C 文件 和 MDN 網頁文件上取得更多關於 pointer 的事件。

記得習慣性用 CanIUse.com 檢查網頁的瀏覽器兼容性。

作業

用 DOM 做更多事