JavaScript / Control Flow

JS / 元素操作


簡介

為了進一步操作網頁內容,我們在這個章節要學習元素操作的相關函式。


DOM / 文件物件模型

Document Object Model

先前也談過,所有的標籤 ( 甚至包括文字與註解 ) 都會被解譯為物件,而物件之間的關係其實也保留了下來。我們可以透過 childNodes 欄位取得下層物件的陣列,比如說如下的 HTML 架構:

<ol id="ol"> <li> ... </li> <li> ... </li> <li> ... </li> </ol>

我們可以使用 JS 取得其子元素列表:

ol.childNodes[0] /* 代表第一個 li */ ol.childNodes[1] /* 代表第二個 li */ ol.childNodes[2] /* 代表第三個 li */

事實上,除了元素之間的關係,元素的屬性、各元素特殊的操作方法、建立 / 刪除 / 搬動 元素的方法也都有定義。這類關於元素的定義可以在 DOM ( Document Object Model, 文件物件模型 ) 中查到,我們不細談,只針對基本的方法與屬性搭配練習帶給大家。


元素屬性

除了先前學過的屬性以外,我們也可以自行定義元素的屬性。一般的通則是使用 data- 開頭的屬性名稱做為自行定義的屬性,以避免與 HTML 本身的屬性相衝突。例如這個 DIV 帶有 data-name 屬性:

<div id="mydiv" data-name="hello"> A tag </div>

在能使用 JS 的情況下,我們可以把自訂屬性想成是額外儲存在元素上的資訊,並且可以使用元素物件的 getAttribute 方法取得其內容:

mydiv.getAttribute("data-name")

舉例來說,我們直接讓元素在點擊時顯示他內藏的 data-name 值:

mydiv.onclick = function(evt) { alert(mydiv.getAttribute("data-name")); };

evt.target 屬性

上例直接在函式裡寫死了取 mydiv 的值,使得這個函式只能彈出 mydiv 對應的屬性值。如果我們有更多個元素要處理,這樣就要寫很多遍,很不方便。恰好 evt 事件物件帶有一個 target 欄位內存這觸發事件的標籤元素,誰觸發就存誰,因此我們可以將上例修改一下,就可以把處理函式重覆利用囉:

mydiv.onclick = function(evt) { alert(evt.target.getAttribute("data-name")); };
試試看吧!

試使用 getAttributeevt.target 搭配一個事件處理函式做出三個按鈕,點下去各會彈出不同的結果:

Click Me
Click Me
Click Me
給讀者的選擇題

搭配事件函式、條件判斷式、evt.targetgetAttribute,我們可以實作簡單的選擇題;如下例,我們對兩個按鈕加上事件處理函式,並依選擇結果顯示預先隱藏起來的兩個元素 answer1answer2

handler = function(evt) { if(evt.target.getAttribute("data-name") == "yes") { answer1.style.display = "block"; } else { answer2.style.display = "block"; } } btn1.onclick = btn2.onclick = handler;

此處為一個簡單的範例,你能夠重製這個例子嗎?

選選看你的星座吧?
我是處女座
我是雙子座

元素重製

除了應對事件控制元素的樣式以外,我們也可以使用程式重製並插入元素。比方說,點擊下例以獲得新點數:

點我得一元
你獲得了一元

使用 cloneNode(true) 進行元素的重製:

newdiv = mydiv.cloneNode(true)

重製的 div 跟舊元素有相同的 id 屬性,我們必須使用 removeAttribute 移除:

newdiv.removeAttribute("id")

或者,使用 setAttribute 寫入新的值:

newdiv.setAttribute("id", "newdiv")

最後,使用 appendChild()insertBefore() 函式插入目標物件:

document.body.appendChild(newdiv); /* 或者, 把 newdiv 插在 mydiv 的前面 */ document.body.insertBefore(newdiv, mydiv);
打地鼠練習

使用 3 個按鈕搭配 requestAnimationFrame 動態的變更按鈕的得分,實作一個打地鼠遊戲吧!

你可以使用這張圖:

網址: https://xinmeti.co/assets/img/sample/gopher.png
CC0 授權, 來源 https://commons.wikimedia.org/wiki/File:Go_gopher_favicon.svg

在每次擊中地鼠時,使用迷你地鼠頭代表得分,比方說下圖代表七分:

下面為實際範例:

分歧式敘事

我們這裡所謂的「分歧式敘事」,是指在敘事時,以角色扮演的形式,依讀者不同的選擇提供不同的回饋,影響敘事的走向。為了達到這樣的效果,必須在讀者進行選擇前先將內容隱藏,並且在必要時重覆利用同樣的內容。

由於這個概念複雜的是題目的設計,我們在這邊簡單使用一個密室逃脫的概念來示範,實際製作可參考類似 你是戒嚴時代的誰呢? 這個專案。

密室逃脫

為了製作密室逃脫,我們準備以下情境:

  • 你在一個空無一物的房間醒來,眼前只有一個門跟一扇窗。 ( 選擇開門 / 開窗 )
  • 走出門外,只看到一個漆黑的長廊。 ( 選擇向前奔跑 / 摸索前進 )
  • 忽然間眼前一亮,眼前出現了一個樓梯間。 ( 選擇往上 / 往下 )
  • 你走到了頂樓,看到了美麗的藍天。 ( 結束 )

同時準備一個失敗條件:

  • 你踩空跌落,掉到了無盡的深淵!忽然你驚醒了,剛剛的一切只是一場夢 ( 選擇 睜開眼睛 )

以下為實際運行的範例。你有辦法自己重製一個出來嗎?

你在一個空無一物的房間醒來,眼前只有一個門跟一扇窗。

開門走出去
開窗爬出去

你走出門外,只看到一個漆黑的長廊。

好恐怖,還是趕快跑吧!
太黑了,小心點慢慢前進吧!

忽然間你眼前一亮,眼前出現了一個樓梯間。

往上走比較有機會走出去吧。
高的地方很危險,還是向下吧。

終於你走到了頂樓,看到了美麗的藍天。

Good End

你踩空跌落,掉到了無盡的深淵!忽然你驚醒了,剛剛的一切只是一場夢...

再次睜開眼睛
進階版密室逃脫

試著在黑暗的長廊中加上一個秘密的房間,必須要慢慢走才能遇到。裡面存放了一支鑰匙,可以用來打開頂樓的門。做做看吧!以下是範例:

你在一個空無一物的房間醒來,眼前只有一個門跟一扇窗。

開門走出去
開窗爬出去

你走出門外,只看到一個漆黑的長廊。

好恐怖,還是趕快跑吧!
太黑了,小心點慢慢前進吧!

忽然間你眼前一亮,眼前出現了一個樓梯間。

往上走比較有機會走出去吧。
高的地方很危險,還是向下吧。

你走到了頂樓,卻發現門上鎖了。

沒辦法,只好往下走了。
如果手上有鑰匙的話,就可以試著開門了。

你踩空跌落,掉到了無盡的深淵!忽然你驚醒了,剛剛的一切只是一場夢...

再次睜開眼睛

你摸黑走著,意外撿到了一把鑰匙。

把鑰匙收進口袋繼續走。

「咖嚓」門開了!

走出去

終於你走到了頂樓,看到了美麗的藍天。

Good End

迴圈

在元素操作中我們可使用 childNodes 陣列取得子元素。在密室逃脫的例子中,如果我們想要清除所有元素重玩,可以使用 removeChild

node.removeChild(childNodes[0]); node.removeChild(childNodes[1]); node.removeChild(childNodes[2]); ...

但這樣做有兩個問題:

  1. 我們不知道有多少元素。
  2. 重覆的指令要寫很多次。

首先,陣列有一個特別的屬性 length 可以讓我們取得他的內容長度:

node.childNodes.length

接下來,我們可以使用一個特別的語法「迴圈」幫我們重覆執行指定的指令。下列為一個迴圈的實例:

for( var i=0; i < 10; i = i + 1) { .... }

其中 for() 括號中、以分號分隔有三組運算式,分別代表了:

  • 初始條件:var i = 0 指定 i 變數為 0
  • 繼續執行的條件: i < 10 的時候,才執行迴圈的內容
  • 執行完成後: i = i + 1i 遞增

因此我們可以透過以下程式清除遊戲的內容:

for(var i = 0; i < node.childNodes.length; i = i + 1) { node.removeChild(node.childNodes[0]); }
順序很重要

上例中我們永遠只移除第一個元素,那是因為當迴圈每執行一次,第一個元素被移除後,原本的第二個元素就變成了一下一次執行時的第一個元素。如果我們讓 i 從大到小遞減至 0 為止的話,寫法則會稍微不同:

for(var i = node.childNodes.length - 1; i >= 0; i = i - 1) { node.removeChild(node.childNodes[i]); }
超級打地鼠

隨機在畫面上跳出 20 隻地鼠,打完遊戲才會結束!試著做看看吧!

使用 parentNode

標籤物件除了有 childNodes 屬性可取得子元素,還有一個 parentNode 屬性可以取得上層元素。因此,一個將自己從文件樹中刪除的方式是:

node.parentNode.removeChild(node);

中斷迴圈

你可以在任何時候中斷迴圈。中斷迴圈有兩種形式:

  • 直接中止並跳出 ( 使用 break )
  • 中止此輪,從下輪開始 ( 使用 continue )

比方說,下例會略過第五次的迴圈:

for(i=1;i<10;i++) { if(i == 5) continue; }

而下例會隨機在 Math.random() 大於 0.5 的時候結束迴圈:

for(i=1;i<10;i++) { if(Math.random() > 0.5) break; }
橫向捲軸射擊遊戲

除了 cloneNode,我們也可以直接使用 document.createElement 憑空製造一個新元素。下例建立了一個 div 元素並存在 node 變數中,接著被插入到 body 元素之下:

var node = document.createElement("div") document.body.appendChild(node);

因為我們可以接著為新建立的元素設定各種屬性與樣式,只要使用 document.createElement 就代表我們完全不需要把要用到的元素寫在 HTML 之中了。

那麼,讓我們搭配 createElement 與剛學到的 break / continue 來試做一個簡單的橫向射擊遊戲吧!

範例圖片網址

網址: https://xinmeti.co/assets/img/sample/plane.png
CC0 授權, 來源 https://opengameart.org/content/smiling-spaceship-game-character
網址: https://xinmeti.co/assets/img/sample/bullet.gif
CC0 授權, 來源 https://opengameart.org/content/bullet-enemy-game-character
網址: https://xinmeti.co/assets/img/sample/enemy.png
CC0 授權, 來源 https://opengameart.org/content/blue-bat-sprites
新媒體內容技術與設計

New Media Techniques and Design