這一課完成後:你會使用 addEventListener() 處理 click、input、submit,讀取表單值、使用 event object,並理解 preventDefault() 在阻止瀏覽器預設行為時的用途。
事件的核心:某件事發生了
瀏覽器裡一直都在發生事件。使用者點了一個按鈕、在輸入框打字、送出表單、按下鍵盤、移動滑鼠,甚至頁面本身載入完成,都可以形成事件。
事件讓網頁從「載入後自己跑完」變成「等使用者做事再回應」。
第一個 click event
HTML:
<button id="change-message" type="button">改變訊息</button>
<p id="message">還沒有被點擊。</p>
JavaScript:
const button = document.querySelector("#change-message");
const message = document.querySelector("#message");
button.addEventListener("click", () => {
message.textContent = "按鈕剛剛被點了。";
});
把它拆開:
"click" → 要監聽哪種事件
() => { ... } → 事件發生時執行什麼
addEventListener() 不會立刻執行函式。它只是先註冊:「如果之後發生 click,就執行這段。」
不要把函式直接呼叫掉
假設:
function changeMessage() {
message.textContent = "Changed";
}
正確註冊:
button.addEventListener("click", changeMessage);
初學者很常誤寫:
button.addEventListener("click", changeMessage());
後者會在程式跑到那一行時就先呼叫 changeMessage(),而不是等 click 才呼叫。
changeMessage() → 現在立刻執行函式
event object:瀏覽器會把事件資訊交給你
button.addEventListener("click", (event) => {
console.log(event);
});
event 是瀏覽器建立的物件,裡面描述這次事件。
例如:
button.addEventListener("click", (event) => {
console.log(event.type);
console.log(event.target);
});
event.type 可能是 click;event.target 則是實際觸發事件的元素。
變數名稱可以不是 event,很多人也會寫 e,但初學時寫完整一點比較容易讀。
input event:使用者每次改輸入內容都會觸發
HTML:
<label for="name-input">你的名字</label>
<input id="name-input" type="text" />
<p id="preview">你好。</p>
JavaScript:
const input = document.querySelector("#name-input");
const preview = document.querySelector("#preview");
input.addEventListener("input", () => {
preview.textContent = `你好,${input.value}`;
});
value 是表單元素目前的值。使用者每打或刪一個字,input 事件通常都會再觸發一次。
也可以從 event.target 讀值
input.addEventListener("input", (event) => {
preview.textContent = `你好,${event.target.value}`;
});
如果 handler 就是專門處理這個 input,這種寫法也很常見。
目前先知道兩種都可以,不用急著追求「唯一正確寫法」。
空字串也要處理
如果使用者把輸入框刪空,上面的程式會顯示:
你好,
可以加入條件:
input.addEventListener("input", () => {
const name = input.value.trim();
if (name === "") {
preview.textContent = "請輸入名字。";
return;
}
preview.textContent = `你好,${name}。`;
});
trim() 會去掉字串前後空白;return 則可以讓函式在這裡提早結束。
submit event:表單應該監聽 form,不是只監聽按鈕
HTML:
<form id="profile-form">
<label for="project-name">專案名稱</label>
<input id="project-name" name="projectName" type="text" required />
<button type="submit">送出</button>
</form>
<p id="result"></p>
JavaScript:
const form = document.querySelector("#profile-form");
const projectName = document.querySelector("#project-name");
const result = document.querySelector("#result");
form.addEventListener("submit", (event) => {
event.preventDefault();
result.textContent = `已收到:${projectName.value}`;
});
為什麼不是只監聽按鈕的 click?因為 form 可以透過不同方式送出,例如在某些情境按 Enter。監聽 submit 比監聽「某一個按鈕被點」更貼近真正的行為。
preventDefault() 在阻止瀏覽器原本要做的事
HTML form 預設送出時,瀏覽器通常會進行 navigation / request,頁面可能因此重新載入。
在純前端練習裡,我們不想讓頁面立刻跳走,所以:
event.preventDefault();
它的意思不是「停止所有 JavaScript」,而是:
狀態:畫面不是資料本身
做一個計數器:
<button id="increase" type="button">+1</button>
<p id="count">0</p>
JavaScript:
const increase = document.querySelector("#increase");
const countText = document.querySelector("#count");
let count = 0;
increase.addEventListener("click", () => {
count += 1;
countText.textContent = count;
});
這裡真正的資料是變數 count;#count 只是把目前狀態顯示出來。
這個思維以後進 React、Vue 或任何 UI framework 都會一直出現。
不要把 DOM 當成唯一資料來源
這種寫法可以動:
const current = Number(countText.textContent);
countText.textContent = current + 1;
但當應用開始變複雜,如果每次都回 DOM 把字讀出來當資料來源,會很難追。
更穩定的習慣通常是:
一個元素可以監聽很多事件
input.addEventListener("focus", () => {
console.log("開始輸入");
});
input.addEventListener("blur", () => {
console.log("離開輸入框");
});
你也可能看到 change、keydown、keyup、pointerdown 等等。
現在不要背完整事件表。先掌握:你要先問「我希望使用者做什麼時觸發邏輯?」再選事件。
事件處理函式不要塞成巨大一坨
例如:
function updatePreview(name) {
preview.textContent = name
? `你好,${name}。`
: "請輸入名字。";
}
input.addEventListener("input", () => {
const name = input.value.trim();
updatePreview(name);
});
事件 listener 可以很薄:取得事件資料,再交給有清楚名稱的函式處理。
這會比把所有判斷、DOM 操作、資料處理全部塞進一個匿名函式容易維護。
多個按鈕:event.target 會開始有用
HTML:
<button class="filter" data-filter="all">全部</button>
<button class="filter" data-filter="done">已完成</button>
<button class="filter" data-filter="todo">未完成</button>
JavaScript:
const filters = document.querySelectorAll(".filter");
for (const filter of filters) {
filter.addEventListener("click", (event) => {
console.log(event.target.dataset.filter);
});
}
data-* attribute 可以透過 dataset 讀取。這讓 DOM element 可以帶一點和操作相關的描述資訊。
事件也會傳遞,但現在先知道有這件事
如果一個按鈕放在一個卡片裡,click 事件可能會沿著 DOM 結構繼續傳遞。你之後會看到 bubbling、capturing、event delegation。
Web Basics 這一課不深入,但先記住:
等你開始處理大量動態列表時,event delegation 才會真正變得重要。
小挑戰:做一個互動式課程清單
沿用上一課的課程資料與 DOM rendering,再加入互動。
需求 1:畫面要有一個輸入框,可以新增課程名稱。
需求 2:用 form 的 submit 處理新增,不只監聽按鈕 click。
需求 3:空白內容不能被新增;需要顯示提示。
需求 4:新增後要更新 JavaScript 中的 lessons array,再重新把結果顯示到 DOM。
需求 5:每個 lesson 至少有一個「完成 / 未完成」按鈕。
需求 6:點擊後要改變該 lesson 的 done 狀態,再更新畫面。
需求 7:畫面上要顯示目前完成數,例如 3 / 5 completed。
需求 8:至少在一個 handler 裡使用 event object。
這次重點不是 UI 漂亮,而是完整跑通:
除錯時先問事件有沒有發生
如果按鈕完全沒反應,可以依序檢查:
最簡單的第一步常常是:
button.addEventListener("click", () => {
console.log("clicked");
});
先確認 event pipeline 有通,再處理後面的資料與 DOM。
最後收斂:互動網站的最小循環
到這裡,你已經不只是「會改 DOM」,而是開始理解前端應用真正的互動循環。
下一課會再多一層:資料不一定一開始就在你的 JavaScript 裡。網站可以用 Fetch 向外部 API 發 request,再根據 response 更新狀態和畫面。
完成條件:你能使用 addEventListener 處理 click、input 與 submit,讀取使用者輸入、使用 preventDefault() 控制表單流程,並用「event → state → DOM」的方式完成至少一個互動功能。