HTML 事件屬性直接在元素上附加 JavaScript 處理程式。
它們的名稱通常以 on 開始,例如 onclick、oninput 和 onsubmit。
<button type="button" onclick="console.log('按鈕已啟動');">
試試看
</button>相應事件發生時,瀏覽器會執行屬性中的 JavaScript 程式碼。
為了方便維護應用程式,通常應優先使用 addEventListener(),而不是行內事件屬性。 它將行為與標記分開,並支援多個監聽器。
39.1 附加處理程式的三種方式 #
HTML 事件屬性 #
<button type="button" onclick="showMessage()">顯示訊息</button>函式必須能從行內處理程式的執行環境存取。在 JavaScript 模組內宣告的函式,不會自動供行內處理程式使用。
JavaScript 事件處理屬性 #
const button = document.getElementById("message-button");
button.onclick = showMessage;事件處理屬性一次只保存一個處理程式。再次指派會取代先前的屬性處理程式。
行內 onclick 屬性及相應的 JavaScript onclick 屬性,不是兩個獨立的處理程式位置。
addEventListener() #
button.addEventListener("click", showMessage);它支援多個監聽器,不會取代事件處理屬性中的處理程式。
留意名稱差異:
| 使用情境 | 名稱 |
|---|---|
| HTML 屬性 | onclick |
| JavaScript 屬性 | onclick |
addEventListener() 事件類型 | "click" |
應傳入函式,而不是呼叫函式的結果:
// 正確。
button.addEventListener("click", showMessage);
// 通常不正確:會立即呼叫函式。
button.addEventListener("click", showMessage());39.2 常見事件屬性 #
以下介紹常見事件屬性,並非完整清單。
| 類別 | 屬性 | 常見用途 |
|---|---|---|
| 啟動 | onclick、ondblclick | 回應啟動或雙擊 |
| 輸入 | oninput、onchange | 回應值變更 |
| 表單 | onsubmit、onreset、oninvalid | 處理提交、重設或驗證失敗 |
| 鍵盤 | onkeydown、onkeyup | 回應按鍵操作 |
| 焦點 | onfocus、onblur | 回應取得或失去焦點 |
| 指標 | onpointerdown、onpointerup、onpointermove | 回應滑鼠、觸控筆或觸控指標操作 |
| 滑鼠 | onmousedown、onmouseup、onmousemove | 回應滑鼠事件 |
| 載入 | onload、onerror | 回應載入或資源錯誤 |
| 捲動 | onscroll、onwheel | 回應捲動或滾輪輸入 |
| 媒體 | onplay、onpause、onended | 回應媒體播放變更 |
| 拖放 | ondragstart、ondragover、ondrop | 支援原生拖放操作 |
許多事件屬性是全域屬性,但是否有用取決於元素。部分屬性具有特定放置位置或行為。
例如,<body> 上的某些事件屬性會作為視窗事件的處理程式。
39.3 事件物件 #
處理程式會收到事件物件,其中包含事件資訊:
const button = document.getElementById("message-button");
button.addEventListener("click", (event) => {
console.log(event.type);
console.log(event.target);
console.log(event.currentTarget);
});重要屬性包括:
| 屬性 | 意思 |
|---|---|
type | 事件名稱 |
target | 事件目標,受 Shadow DOM 等重新指定目標規則影響 |
currentTarget | 目前正在執行監聽器的物件 |
defaultPrevented | 事件的預設行為是否已取消 |
如果按鈕包含巢狀 <span>,點擊該 span 時,它可能成為 target,而按鈕仍然是 currentTarget。
行內處理程式也可以使用 event:
<button type="button" onclick="console.log(event.type)">
記錄事件
</button>39.4 事件處理程式中的 this #
在元素的行內處理程式中,this 通常指向該元素:
<button type="button" onclick="this.textContent = '已啟動'">
啟動
</button>使用一般函式作為 addEventListener() 回呼時,this 指向目前執行監聽器的事件目標:
button.addEventListener("click", function (event) {
console.log(this === event.currentTarget); // true
});箭頭函式不會取得新的 this 綁定:
button.addEventListener("click", (event) => {
event.currentTarget.textContent = "已啟動";
});使用 event.currentTarget 通常能更清晰地表達要操作的元素。
39.5 Input 與 Change 事件 #
input 和 change 具有不同用途。
<label for="display-name">顯示名稱</label>
<input id="display-name" name="displayName" type="text">const nameInput = document.getElementById("display-name");
nameInput.addEventListener("input", () => {
console.log("目前值:", nameInput.value);
});
nameInput.addEventListener("change", () => {
console.log("已確認的值:", nameInput.value);
});對文字輸入欄位而言:
input通常在使用者編輯值時觸發。change通常在值已改變,而且控制元素失去焦點後觸發。
其他控制元素具有不同的確認行為。例如,核取方塊通常在切換狀態時觸發 change。
透過 JavaScript 設定 .value,不會自動觸發這些事件。
不要用鍵盤事件取代輸入事件。值也可以透過貼上、語音輸入及其他非一般按鍵方式改變。
39.6 表單提交 #
應監聽表單的 submit,而不是只依靠提交按鈕的點擊:
<form id="search-form" action="/search" method="get">
<label for="search-query">搜尋</label>
<input id="search-query" name="q" type="search" required>
<button type="submit">搜尋</button>
</form>const form = document.getElementById("search-form");
form.addEventListener("submit", (event) => {
event.preventDefault();
const data = new FormData(form);
console.log(data.get("q"));
});表單可以透過鍵盤操作提交,不限於指標啟動。
重要區別包括:
- 內建驗證可以在
submit事件發生前阻止提交。 form.requestSubmit()遵循提交流程,包括驗證。form.submit()會略過約束驗證及submit事件。
只有在程式碼實作了預期替代行為時,才取消正常提交。
39.7 取消預設行為 #
使用 preventDefault() 取消可取消事件的預設行為:
form.addEventListener("submit", (event) => {
event.preventDefault();
// 在這裏實作替代提交行為。
});它不會停止事件傳播。
在行內處理程式中,返回 false 可以取消許多一般事件的預設行為:
<form onsubmit="return validateForm()">
<!-- 表單控制元素 -->
</form>不過,從 addEventListener() 回呼返回 false,不會取消事件:
form.addEventListener("submit", () => {
return false; // 不會阻止提交。
});應優先明確呼叫 event.preventDefault()。
視窗錯誤及離開頁面前的處理程式等具有特殊規則,不要將一般返回值行為套用至所有事件。
39.8 事件傳播 #
許多事件會經過以下階段:
- 捕獲:沿祖先元素朝目標傳播。
- 目標:到達事件目標。
- 冒泡:如果事件支援冒泡,沿祖先元素向外傳播。
傳入選項即可在捕獲階段監聽:
document.addEventListener("click", handleClick, {
capture: true
});兩個方法可以控制傳播:
| 方法 | 效果 |
|---|---|
stopPropagation() | 停止繼續傳播至其他目標 |
stopImmediatePropagation() | 也阻止目前目標上尚未執行的監聽器 |
兩者都不會自動取消預設行為。
避免不必要地停止傳播,否則可能干擾其他元件、分析工具或文件層級的互動處理。
並非所有事件都支援冒泡。例如,focus 和 blur 不會冒泡,而 focusin 和 focusout 會。
39.9 事件委派 #
事件委派在共用祖先上附加監聽器,處理來自後代元素的事件:
<div id="lesson-actions">
<button type="button" data-lesson="intro">入門</button>
<button type="button" data-lesson="forms">表單</button>
</div>const actions = document.getElementById("lesson-actions");
actions.addEventListener("click", (event) => {
if (!(event.target instanceof Element)) {
return;
}
const button = event.target.closest("button[data-lesson]");
if (!button || !actions.contains(button)) {
return;
}
console.log("已選課節:", button.dataset.lesson);
});closest() 可以處理按鈕內巢狀內容的點擊。
事件委派適合重複控制元素及動態新增元素。它依賴事件能傳播至祖先,而跨越 Shadow DOM 邊界時需要額外考慮。
39.10 監聽器選項與清理 #
addEventListener() 支援實用選項:
button.addEventListener("click", showMessage, {
once: true
});| 選項 | 用途 |
|---|---|
capture | 在捕獲階段監聽 |
once | 首次執行後移除監聽器 |
passive | 承諾監聽器不會取消預設行為 |
signal | 允許透過 AbortSignal 移除 |
明確清理監聽器:
function handleClick() {
console.log("已啟動");
}
button.addEventListener("click", handleClick);
button.removeEventListener("click", handleClick);移除時需要相同的事件類型、同一個回呼函式,以及相符的捕獲設定。
AbortController 可以一併管理多個監聽器:
const controller = new AbortController();
button.addEventListener("click", handleClick, {
signal: controller.signal
});
// 移除使用這個 signal 註冊的監聽器。
controller.abort();不要在 passive 監聽器內呼叫 preventDefault(),因為它不會取消預設行為。
39.11 鍵盤與指標無障礙功能 #
原生按鈕會透過一般鍵盤啟動及指標操作產生 click 事件:
<button type="button" id="save-button">儲存</button>document.getElementById("save-button")
.addEventListener("click", saveDraft);通常不需要為原生按鈕另外加入 Enter 及空白鍵處理程式。
相比之下:
<!-- 不是完整的無障礙控制元素。 -->
<div onclick="saveDraft()">儲存</div>點擊處理程式不會令 <div> 支援鍵盤操作,也不會提供按鈕語意。
也應避免:
- 只在指標停留時提供重要操作。
- 要求雙擊才能執行重要操作。
- 只在
pointerdown執行一般按鈕操作。 - 不必要地覆蓋標準鍵盤快捷鍵。
- 建立只能拖曳、沒有鍵盤替代方式的介面。
實作真正的鍵盤互動時,應使用 event.key,而不是已棄用的 keyCode 或 which。
39.12 安全與內容安全政策 #
行內事件屬性將可執行 JavaScript 與 HTML 混合:
<button onclick="showMessage()">顯示訊息</button>限制嚴格的內容安全政策(CSP)通常會阻止行內事件處理程式。
如果政策允許相關外部程式碼,使用 addEventListener() 的外部程式碼會較容易管理,但程式碼本身仍然必須符合政策。
不要只為保留事件屬性,而以寬鬆的行內程式碼權限削弱政策。
如果不安全地插入含有事件屬性的不可信 HTML,可能執行程式碼。例如,資源錯誤處理程式是常見的注入方式。
一般使用者文字應使用:
message.textContent = userProvidedText;不要使用 innerHTML 插入它。
將處理程式移出 HTML 可以改善結構,但本身不能防止跨網站指令碼攻擊(XSS)。仍然需要安全呈現及適當內容清理。
39.13 完整範例:輸入、提交與重設 #
以下獨立網頁預覽學員姓名,並處理示範表單提交。
它使用原生控制元素及 addEventListener(),不需要行內事件屬性。
id: hong-kong-html-event-attributes
name: HTML 事件 — 無障礙表單示範
type: html
content: |-
<!doctype html>
<html lang="zh-Hant-HK">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta
name="description"
content="使用原生 HTML 控制元素示範 input、submit 及 reset 事件。"
>
<title>HTML 事件 | 學員表單</title>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
color: #222;
background: #f5f7fa;
font-family: system-ui, sans-serif;
line-height: 1.8;
}
main {
max-width: 40rem;
margin-inline: auto;
padding: 2rem 1rem;
}
h1 {
color: #005ea8;
line-height: 1.4;
}
form {
padding: 1.25rem;
border: 1px solid #767676;
border-radius: 0.5rem;
background: white;
}
label {
display: block;
margin-block-end: 0.35rem;
font-weight: 700;
}
input {
width: 100%;
padding: 0.6rem;
border: 1px solid #767676;
border-radius: 0.3rem;
color: #222;
background: white;
font: inherit;
}
.help {
color: #444;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin-block-start: 1rem;
}
button {
padding: 0.6rem 1rem;
border: 1px solid #005ea8;
border-radius: 0.3rem;
font: inherit;
cursor: pointer;
}
button[type="submit"] {
color: white;
background: #005ea8;
}
button[type="reset"] {
color: #005ea8;
background: white;
}
:focus-visible {
outline: 3px solid #005ea8;
outline-offset: 3px;
}
#submission-status {
min-height: 1.8em;
}
</style>
</head>
<body>
<main>
<h1>學員表單</h1>
<p>
這個示範只會在目前網頁更新內容,
不會傳送或永久儲存你的姓名。
</p>
<noscript>
<p>
這個互動示範需要 JavaScript。
沒有 JavaScript 時,不會啟用提交控制元素。
</p>
</noscript>
<form id="learner-form">
<label for="learner-name">姓名</label>
<input
id="learner-name"
name="learnerName"
type="text"
autocomplete="name"
maxlength="80"
aria-describedby="name-help"
required
disabled
>
<p id="name-help" class="help">
輸入你想在這個示範使用的姓名。
</p>
<p>
預覽:<span id="name-preview">尚未輸入姓名。</span>
</p>
<div class="actions">
<button id="submit-button" type="submit" disabled>
確認姓名
</button>
<button id="reset-button" type="reset" disabled>
重設
</button>
</div>
</form>
<p id="submission-status" role="status"></p>
</main>
<script>
const form = document.getElementById("learner-form");
const nameInput = document.getElementById("learner-name");
const preview = document.getElementById("name-preview");
const status = document.getElementById("submission-status");
function updatePreview() {
preview.textContent =
nameInput.value.trim() || "尚未輸入姓名。";
}
nameInput.addEventListener("input", () => {
nameInput.setCustomValidity("");
status.textContent = "";
updatePreview();
});
form.addEventListener("submit", (event) => {
event.preventDefault();
const name = nameInput.value.trim();
if (!name) {
nameInput.setCustomValidity(
"請輸入不只是空白字元的姓名。"
);
nameInput.reportValidity();
return;
}
nameInput.setCustomValidity("");
status.textContent = `姓名已確認:${name}。`;
});
form.addEventListener("reset", () => {
nameInput.setCustomValidity("");
status.textContent = "";
// reset 事件在控制元素完成重設前執行。
// 在稍後的工作中讀取已還原的值。
setTimeout(updatePreview, 0);
});
nameInput.disabled = false;
document.getElementById("submit-button").disabled = false;
document.getElementById("reset-button").disabled = false;
updatePreview();
</script>
</body>
</html>在這個範例中:
input配合不同輸入方式更新預覽。submit處理按鈕及鍵盤提交。- 原生驗證檢查必填欄位。
- 自訂驗證拒絕完全由空白字元組成的姓名。
reset清除自訂驗證,並在控制元素重設後更新預覽。- 狀態區域讀出確認訊息,不需要移動焦點。
textContent顯示輸入文字,不會將它當成 HTML 剖析。- 只有在事件處理程式附加後,才啟用控制元素。
預覽不是即時通知區域,避免每次按鍵都產生通知。較少出現的確認結果使用 role="status"。
39.14 常見錯誤 #
| 錯誤 | 較好的做法 |
|---|---|
| 將大量程式碼混入 HTML 屬性 | 使用 addEventListener() |
向 addEventListener() 傳入 "onclick" | 使用 "click" |
| 註冊處理程式時呼叫它 | 傳入函式參照 |
| 以為多次指派事件處理屬性會保留所有處理程式 | 使用多個事件監聽器 |
從監聽器返回 false 以取消操作 | 呼叫 preventDefault() |
| 混淆取消行為與事件傳播 | 使用適當事件方法 |
| 只監聽提交按鈕的點擊 | 監聽表單的 submit 事件 |
| 使用鍵盤事件追蹤所有文字變更 | 使用 input |
| 以為程式碼改變值會觸發輸入事件 | 明確更新相關狀態 |
讓 <div> 可點擊,卻沒有完整互動支援 | 使用原生控制元素 |
| 在 passive 監聽器中取消捲動 | 適當選擇監聽器選項 |
使用 innerHTML 呈現使用者輸入 | 純文字使用 textContent |
| 使用不同函式實例移除監聽器 | 保留原本的回呼函式參照 |
本章總結: HTML 事件屬性提供行內 JavaScript 處理程式,但應用程式通常更適合使用 addEventListener()。應理解事件物件、預設行為、事件傳播及監聽器清理。使用原生控制元素、在表單層級處理提交,並保持內容呈現安全及鍵盤互動可預測。