Learning Objectives
- 用 script 標籤在網頁中放入 JavaScript,分辨內嵌程式與外部 .js 檔
- 說明 DOM 是網頁的樹狀結構,JavaScript 透過 document 存取網頁
- 說明 script 放的位置為什麼會影響能不能取得元素,並用 defer 或 DOMContentLoaded 解決
- 用 createElement、setAttribute 與 getElementById 動態新增並取得網頁元素
script 標籤:把 JavaScript 放進網頁
瀏覽器讀到 HTML 裡的 <script> 標籤,就會執行裡面的 JavaScript。有兩種寫法:
<!-- 1. 內嵌:程式直接寫在標籤裡 -->
<script>
console.log('Hello');
</script>
<!-- 2. 外部檔案:程式寫在 app.js,用 src 指定路徑 -->
<script src="js/app.js"></script>
實際的專案大多用外部檔案:
- HTML 和 JavaScript 分開,各自比較好維護
- 很多頁面可以共用同一支
.js檔 - 瀏覽器會快取(cache)下載過的檔案,換頁時不用重新下載
兩個要注意的地方:有 src 的 <script>,標籤裡面寫的程式會被忽略,兩種寫法不要混用;
</script> 結束標籤不能省略,不能寫成 <script src="app.js" />。
你在舊教材可能會看到 document.write('…') 直接把文字寫進網頁。它在網頁載入完之後呼叫會把整頁內容清掉,現在已經不建議使用;
這一課後面會用 createElement 新增內容。
DOM:網頁的樹狀結構
瀏覽器讀進 HTML 之後,會建立一棵由節點組成的樹,叫做 DOM(Document Object Model,文件物件模型)。 DOM 是 HTML 文件的程式介面:JavaScript 不是去改 HTML 原始碼,而是讀取、修改這棵樹,樹一改,畫面就跟著改。
<html>
<head><title>我的網頁</title></head>
<body>
<h1>標題</h1>
<p id="intro">一段文字</p>
</body>
</html>
document
└── html
├── head
│ └── title ── 「我的網頁」
└── body
├── h1 ── 「標題」
└── p#intro ── 「一段文字」
document是整棵樹的根,JavaScript 存取網頁都從它開始,例如document.getElementById('intro')取得 id 為 intro 的元素- 找不到符合的元素時,
getElementById回傳null - DOM 是瀏覽器都要遵守的標準,和程式語言無關;JavaScript 是最常用來操作 DOM 的語言
DOM 的完整操作(各種選取方法、新增、刪除元素)在 j15 介紹,這一課只先看一個重要的問題:script 什麼時候執行。
script 放的位置會影響結果
瀏覽器由上而下解析 HTML,一邊讀一邊建立 DOM。遇到一般的 <script> 時,會停下來先執行它,執行完才繼續往下讀。
所以,如果 <script> 寫在某個元素的前面,執行的當下那個元素還沒被建立,getElementById 就會得到 null;
接著寫 null.textContent = '…' 就會出現 TypeError: Cannot set properties of null。這是初學者最常遇到的錯誤之一。
解決方法有三種:
- 把
<script>放在</body>的前面,也就是所有元素的後面 - 外部檔案加上
defer:<script src="app.js" defer></script>,可以放在<head>裡(下一段說明) - 等網頁讀完再執行:把程式包在
document.addEventListener('DOMContentLoaded', () => { … })裡面。DOMContentLoaded事件在整份 HTML 解析完、DOM 建立好時觸發(事件在 j19 詳細介紹)
下面的沙箱裡,HTML 分頁有兩段內嵌的 <script>,JavaScript 分頁的程式則會放在 body 的最後面執行。
defer 與 async
外部檔案的 <script src> 可以加上兩個屬性,改變下載與執行的時機:
| 寫法 | 下載 | 執行時機 | 多個檔案的執行順序 |
|---|---|---|---|
<script src="a.js"> |
停下解析 HTML,先下載 | 下載完立刻執行,執行完才繼續解析 | 依照在 HTML 裡的順序 |
<script src="a.js" defer> |
和解析 HTML 同時進行 | HTML 全部解析完之後、DOMContentLoaded 之前 |
依照在 HTML 裡的順序 |
<script src="a.js" async> |
和解析 HTML 同時進行 | 下載完就立刻執行(會暫停解析) | 不保證,誰先下載完誰先執行 |
- 需要操作 DOM、或檔案之間有先後依賴的程式(例如先載入函式庫再載入自己的程式),用
defer - 和頁面內容無關、獨立運作的程式(例如流量統計),可以用
async defer、async只對有src的外部檔案有效,寫在內嵌的<script>上沒有作用<script type="module" src="main.js">(ES 模組,j29 介紹)預設就是 defer 的行為
所以現在常見的寫法是把 <script src="app.js" defer></script> 放在 <head> 裡:瀏覽器可以早一點開始下載,又能等 DOM 建立好才執行。
想一想:script 的位置與載入方式
下面的 HTML,主控台會印出什麼?
<script>
console.log(document.getElementById('msg'));
</script>
<p id="msg">Hi</p>
寫了 <script src="app.js">console.log('A');</script>,app.js 裡是 console.log('B');,主控台會印出什麼?
<head> 裡依序有 <script src="lib.js" defer> 和 <script src="app.js" defer>,app.js 會用到 lib.js 的函式。下列哪一個描述正確?
下列哪一種寫法,defer 沒有任何作用?
第一次動手改網頁:新增標籤與屬性
用 JavaScript 新增一個標籤分成三步:建立元素 → 放入內容 → 放進 DOM 樹。
| 方法 | 作用 |
|---|---|
document.createElement('h1') |
建立一個 <h1> 元素(還不在網頁上) |
document.createTextNode('Hello') |
建立一個文字節點 |
父元素.appendChild(子節點) |
把子節點放到父元素的最後面,例如 document.body.appendChild(h1) |
元素.setAttribute('id', 'red') |
設定屬性,相當於 HTML 的 <h1 id="red"> |
元素.getAttribute('id') |
讀取屬性的值;沒有這個屬性時回傳 null |
元素.hasAttribute('id') |
有沒有這個屬性,回傳 true 或 false |
HTML 標籤常見的屬性:所有元素都能用的全域屬性,例如 id(同一頁唯一)、class(和 CSS 搭配)、style(行內樣式)、hidden(隱藏元素);
表單元素的 name、value;<form> 的 action、method。
取得元素之後,也可以直接讀寫一些屬性:el.id、el.className、el.tagName(大寫的標籤名稱)、el.style.color = 'red'。
投影片寫的 createElement("h1").setAttribute(…) 連在一起,得到的是 setAttribute 的回傳值 undefined,不是元素;要先把元素存進變數,再分開呼叫。
想一想:新增與取得元素
執行 const h = document.createElement('h2'); h.textContent = '新標題'; 之後,網頁上看得到「新標題」嗎?
<input id="name" type="text">,執行 document.getElementById('name').getAttribute('value') 會得到什麼?
頁面上有三個 id="h1" 的元素(id 重複了),document.getElementById('h1') 會得到什麼?
重點精華回顧
<script>可以內嵌程式,也可以用src載入外部.js檔;有src時標籤裡的程式會被忽略。- DOM 是網頁的樹狀結構,
document是根;JavaScript 透過 DOM 讀取與修改網頁。 - 瀏覽器由上而下解析 HTML,一般的 script 會立刻執行;寫在元素前面的 script 取得元素會得到
null。 - 解決方法:script 放在 body 最後、外部檔案加
defer、或在DOMContentLoaded之後再執行。 defer等 HTML 解析完依序執行;async下載完就執行、不保證順序;兩者都只對外部檔案有效。createElement建立元素、appendChild放進樹裡、setAttribute/getAttribute/hasAttribute處理屬性。