`#JavaScript` `#六角前端課程` `#學習筆記` `#骨力走傱` ## 什麼是 DOM? 文件物件模型(Document Object Model)。 > 是一個將 HTML 文件以樹狀的結構來表示的模型,而組合起來的樹狀圖,我們稱之為「DOM Tree」。 若將以下程式碼的關係以圖像呈現,會得到一張樹狀圖,**一個節點就是一個標籤,JavaScript 透過 DOM 提供的方法,可以操作這些節點,進而改變 HTML 的結構、樣式及內容。** ```html= <html> <head> <title>Shop</title> </head> <body> <h1>熱銷商品</h1> <p>歡迎選購</p> </body> </html> ``` ![Untitled](https://hackmd.io/_uploads/SkNzAo1kWg.jpg) ### 資料來源 * [重新認識 JavaScript: Day 11 前端工程師的主戰場:瀏覽器裡的 JavaScript](https://ithelp.ithome.com.tw/articles/10191666) * [JavaScript 從零開始 - Day25- 認識 DOM 文件物件模型](https://ithelp.ithome.com.tw/articles/10241371) ## 如何操作 DOM 節點? ### 選取節點 #### `querySelector()` ```html= <p id="text" class="primary-color">藍色泡泡槍</p> ``` ```css= .primary-color { color: lightskyblue; } ``` ```javascript= const tagNode = document.querySelector("p"); const tagId = document.querySelector("#text"); const classNode = document.querySelector(".primary-color"); console.log(tagNode); // <p class="primary-color">藍色泡泡槍</p> console.log(tagId); // <p class="primary-color">藍色泡泡槍</p> console.log(classNode); //<p class="primary-color">藍色泡泡槍</p> ``` * `document.querySelector(" HTML 標籤或 CSS 類別選擇器");` * Document 是一個物件,因此可以使用點記法 `.` 存取或呼叫其屬性與方法。 * `querySelector` 會返回選取對象的 HTML 結構,可以將結果賦值在變數上,以利後續操作。 * `("")` 內放入選取對象的 HTML 標籤、id 或 CSS 類別選擇器。 * id → 其前綴符號的 `#` * CSS → 其前綴符號的 `.` ### 選取多個節點 #### `querySelectorAll()` ```html= <a href="#">連結</a> <a href="#">連結</a> <a href="#">連結</a> ``` ```javascript= const links = document.querySelectorAll("a"); console.log(links); // NodeList [a, a, a] ``` * `document.querySelectorAll(" HTML 標籤或 CSS 類別選擇器");` * `querySelectorAll` 會以陣列的形式回傳節點列表,因此選取多個節點後,要對其中一個節點操作時,需要使用索引值選取。 * `links[0].textContent = "新增文字";` ### 新增、修改文字 #### `textContent` ```html= <p>藍色泡泡槍</p> ``` ```javascript= const tagNode = document.querySelector("p"); tagNode.textContent = "黃色泡泡槍"; console.log(tagNode); // <p>黃色泡泡槍</p> ``` * `DOM 節點.textContent = "要寫入的文字";` * 要先選取節點,才能使用 `textContent` 寫入文字。 * 寫入的文字會覆蓋原本的內容。 * `textContent` **無法將 HTML 標籤轉換為網頁結構**。若輸入 `tagNode.textContent = "<h1>黃色泡泡槍</h1>";`,也只會渲染 `<h1>黃色泡泡槍</h1>` 這段文字在網頁上而已。若希望標籤能轉換為網頁結構,需要使用 `innerHTML` 這個方法。 ### 新增、修改 HTML 標籤 #### `innerHTML` ```html= <div> <p>文字</p> </div> ``` ```javascript= const textNode = document.querySelector("div"); textNode.innerHTML = `<h1>標題</h1>`; console.log(textNode); // <div><h1>標題</h1></div> ``` * `DOM 節點.innerHTML = "要修改的 HTML 結構";` * `innerHTML` 會先清除節點的內容,再新增 HTML 結構,故我要在 `<p>` 標籤的位置新增 `<h1>` 標籤時,需要先選取 `<p>` 標籤的父層 `<div>`,再使用 `innerHTML` 新增結構。 * 可以使用**變數**替換內容: ```javascript= const textNode = document.querySelector("div"); let text = "虱目魚沒夢不應該"; textNode.innerHTML = `<h1>${text}</h1>`; console.log(textNode); // <div><h1>虱目魚沒夢不應該</h1></div> ``` ### 新增子節點 #### `appendChild()` ```javascript= // 取選要新增子節點的父容器 const appendChild = document.querySelector(".append-child"); // 使用 appendChild 前,要先使用 document.createElement() 建立新元素 const li = document.createElement("li"); li.textContent = "ㄉㄊㄋ"; // 可以將指定的 childNode 節點,加入到 Node 父層節點的末端 appendChild.appendChild(li); ``` * `document.createElement();` 新增新元素。 * `DOM 節點.appendChild(要加入的 HTML 結構);` * `appendChild` 不會清除原本的 HTML 結構,而是在父層節點容器的末端,新增子元素。 #### 資料來源 * [重新認識 JavaScript: Day 13 DOM Node 的建立、刪除與修改](https://ithelp.ithome.com.tw/articles/10191867) ### 新增、修改 HTML 屬性 #### `setAttribute()` ```html= <a href="#">連結</a> ``` ```javascript= const link = document.querySelector("a"); link.setAttribute("href","https://www.google.com/"); link.setAttribute("class","primary-color"); console.log(link); // <a href="https://www.google.com/" class="primary-color">連結</a> ``` * `DOM 節點.setAttribute("要修改的屬性名稱","要修改的內容");` * 若要新增 `class` 屬性,`""` 內輸入的類別名稱,不用加前綴符號 `.`。 ### 取出 HTML 結構、屬性及文字的方式 ```html= <div> <a href="https://www.google.com/" class="primary-color">google ㄉ 超連結</a> </div> ``` ```javascript= const divNode = document.querySelector("div"); const link = document.querySelector("a"); // 取出 HTML 結構 console.log(divNode.innerHTML); // <a href="https://www.google.com/" class="primary-color">google ㄉ 超連結</a> // 取出 HTML 屬性 console.log(link.getAttribute("href")); // https://www.google.com/ console.log(link.getAttribute("class")); // primary-color // 取出標籤內的文字 console.log(link.textContent); // google ㄉ 超連結 ``` * `DOM 節點.getAttribute("要取出其內容的屬性名稱");` * 以上述的程式碼為例,若要取得 `<a>` 標籤內的網址,可以使用 `getAttribute` ,接著 `("")` 輸入 href,就會回傳內容。 * `divNode.innerHTML`、`link.textContent` 則是會回傳賦值在變數上的資料,進而得到 HTML 結構及文字。 * 取值的方法也可以應用在取出表單內容。 * ```html= <input type="text" value="這是一個表格"> ``` ```javascript= const inputNode = document.querySelector("input"); console.log(inputNode.value); // 這是一個表格 ``` ## 什麼是 Event? 事件(Events),使用者在瀏覽網頁時,會觸發許多事件,例如點擊按鈕、滾動畫面及點選表單等等,JavaScript 會透過監聽(Listen)等等的方法處理這些事件。 ### 資料來源 * [JavaScript DOM Event(事件處理)](https://www.fooish.com/javascript/dom/event.html) ## 如何進行事件監聽? ### 註冊 #### `addeventListener()` ```html= <input type="button" value="按鈕"> <h1>標題</h1> ``` ```javascript= const btn = document.querySelector("input"); const text = document.querySelector("h1"); btn.addEventListener("click",function(e){ text.textContent = "按鈕已被點擊"; }); ``` * `DOM 節點.addEventListener("監聽事件","事件觸發時要執行的函式");` * `.` 前方為被監聽的 DOM 節點。 * `"click"` 點擊,表示監聽的事件種類,當 DOM 節點**被點擊時**,會執行後方的函式。 * [事件的種類](https://ithelp.ithome.com.tw/articles/10192175) #### 是否有註冊成功? ![螢幕擷取畫面 (1667)](https://hackmd.io/_uploads/SyA7DBM1-x.png) * 透過開發者工具的「事件監聽器」確認事件是否有註冊成功?以及註冊了哪些事件? #### 迷你加法器練習 ```html= <input type="button" value="按鈕"> <h1>0</h1> ``` ```javascript= const btn = document.querySelector("input"); const text = document.querySelector("h1"); let counts = 0; // 紀錄數字 btn.addEventListener("click",function(e){ counts++; text.textContent = counts; // 變數 counts 的值會覆蓋 <h1> 的內容 }); ``` ### 參數 `e` #### `function(e)` ![螢幕擷取畫面 (1669)](https://hackmd.io/_uploads/ryySCHMJbl.png) * `function(e)` 中的 `e` 為事件 `event` 的縮寫。若使用 `console.log` 查看,會回傳當前被觸發節點的**事件物件快照**,可利用這份資料進一步操作或應用。 ### 取得節點的 HTML 標籤名稱 #### `nodeName` ```html= <input type="button" value="按鈕"> ``` ```javascript= const btn = document.querySelector("input"); btn.addEventListener("click",function(e){ console.log(btn.nodeName); // INPUT }); ``` * `DOM 節點.nodeName` * 回傳節點的 HTML 標籤名稱。 ### 取得當前被觸發的元素位置 #### `e.target` ```html= <ul> <li>文字</li> <li>文字</li> <li><input type="button" value="按鈕"></li> </ul> ``` ```javascript= const ul = document.querySelector("ul"); const btn = document.querySelector("input"); ul.addEventListener("click",function(e){ console.log(e.target); // <li> ... </li> }); btn.addEventListener("click",function(e){ console.log(e.target); // <input type="button" value="按鈕"> }); ``` * `target` 是 DOM 的屬性,能告知開發者當前是哪一個元素被觸發了。 * 會根據註冊事件監聽的範圍大小,回傳 `<ul>` 或一個按鈕的結構。 ### 與 nodeName 的應用 ```html= <ul> <li>文字</li> <li>文字</li> <li><input type="button" value="按鈕"></li> </ul> ``` ```javascript= const ul = document.querySelector("ul"); const btn = document.querySelector("input"); // 取得 btn 的 nodeName // btn.addEventListener("click",function(e){ // console.log(e.target.nodeName); // INPUT // }); ul.addEventListener("click",function(e){ if(e.target.nodeName === "INPUT") { console.log("您點擊到按鈕了!"); } else { console.log("您點擊到其他地方了!"); } }); ``` 1. 希望使用者點擊到按鈕時,回傳「您點擊到按鈕了!」的資訊。 2. 在 `btn` 的 `e.target` 中找到 `nodeName`,取得節點名稱 `INPUT`。 3. 對 `ul` 註冊事件監聽,若點擊到按鈕(即 `e.target.nodeName === "INPUT"`),就回傳「您點擊到按鈕了!」的資訊;反之,則回傳「您點擊到其他地方了!」。 ### 取消預設的觸發行為 #### `preventDefault()` ```html= <a href="https://www.google.com/">連結</a> ``` ```javascript= const textNode = document.querySelector("a"); textNode.addEventListener("click",function(e) { e.preventDefault(); }); ``` * 未加入 `e.preventDefault();` 之前,點擊 `<a>` 標籤時,網頁會發生轉址;若想要取消預設的觸發行為,可以使用 `e.preventDefault();` 取消。 ## 陣列處理方法 ### 前言 面對大量資料時,若要一筆一筆計算加總,是效率極低的事,因此需要學習陣列處理的方法,如 `map`、`filter`、`forEach`,能快速完成資料篩選、轉換與彙整,提升作業效率。 這裡先筆記 `forEach()` 的使用方法。 ### forEach() ```javascript= let arr = [1, 2, 3]; arr.forEach(function(item,index,array){ console.log(item,index,array); }); // 1 0 [1, 2, 3] // 2 1 [1, 2, 3] // 3 2 [1, 2, 3] ``` * `變數.forEach(function(item,index,array){...};` * 陣列裡有幾筆資料,函式區塊內的程式碼就會執行幾次。 * 可自定義函式中的參數名稱,但參數**位置**所代表的意義是固定的: ```javascript= let arr = [1]; arr.forEach(function(num,i,arr){ console.log(num,i,arr); }); // 1 0 [1] ``` * 參數 1:目前正在處理的陣列元素。 * 參數 2:該元素的索引值。 * 參數 3:整個陣列本身。 * 可依需求決定使用哪些參數,例如不會用的 `array` 的話,可以省略。 ## forEach 的應用 ### 未使用 `forEach()` 前 ```javascript= // 加總陣列中的元素值。 let arr = [10, 20, 30]; let total = 0; total = arr[0] + arr[1] + arr[2]; console.log(total); // 60 ``` ### 使用 `forEach()` 後 ```javascript= // 加總陣列中的元素值。 let arr = [10, 20, 30]; let total = 0; arr.forEach(function(item,index){ total += item; console.log(total); }); // 執行第一次 // let total = 0; // total += item; ➡️ 0 + 10 = 10 // total = 10; // 執行第二次 // total = 10; // total += item; ➡️ 10 + 20 = 30 // total = 30; // 執行第三次 // total = 30; // total += item; ➡️ 30 + 30 = 60 // total = 60; console.log(total); // 60 ``` #### 若將 `let total = 0;` 放在函式區塊內 ```javascript= // 加總陣列中的元素值。 let arr = [10, 20, 30]; arr.forEach(function(item,index){ let total = 0; total += item; console.log(total); }); // 函式內發生了什麼事? // 執行第一次 // let total = 0; // total += item; ➡️ 0 + 10 = 10 // 執行第二次 // let total = 0; // total += item; ➡️ 0 + 20 = 20 // total = 20; // 執行第三次 // let total = 0; // total += item; ➡️ 0 + 30 = 30 // total = 30; console.log(total); // total is not defined ``` * 函式區塊內的變數只存在於區塊中,故每次都會執行 `let total = 0;` 時,都無法留下加總紀錄。 * 外層的 `console.log(total);` 無法往內層找到變數 `total`,故回傳 `total is not defined`。 ### 與 `if` 的應用 ```javascript= // 計算有陣列中有幾筆偶數,並紀錄結果。 // 偶數的條件 -> 餘數是 0 let data = [128415, 1893188, 10, 187391, 15400]; let evenCount = 0; let res = []; data.forEach(function(item){ if(item % 2 === 0){ evenCount ++; res.push(item); } }); console.log(evenCount); // 3 console.log(res); // [1893188, 10, 15400] ``` ### forEach 可以讀取物件資料 ```javascript= // 記錄學生的名字 let classMate = [ { name: "Tom", age: 10 }, { name: "Eva", age: 11 }, { name: "Lily", age: 10 } ]; let nameList = []; classMate.forEach(function(item){ console.log(item.name); nameList.push(item.name); }); console.log(nameList); // ['Tom', 'Eva', 'Lily'] ``` * `classMate.forEach(function(item){console.log(item);});` 回傳: * `{name: 'Tom', age: 10}` * `{name: 'Eva', age: 11}` * `{name: 'Lily', age: 10}` * 是物件資料,故使用點記法讀取 `name` 的值。 * 可以先使用 `console.log` 查看當前的資料是陣列還是物件?接著思考後續的讀取方式,以及可以使用的處理方法。 ## DOM 與 forEach 的整合 ### 整合 innerHTML 資料 ```javascript= let data = [ { Charge:"免費", name:"廖洧杰充電站" }, { Charge: "投幣式", name: "小花充電站" }, { Charge: "投幣式", name: "小明充電站" }, { Charge: "投幣式", name: "小天充電站" } ]; // 1. 整合 innerHTML 資料 // 1-1. 選取 DOM 節點 const list = document.querySelector("ul"); // 1-2. 組合結構用 let str = ""; // 1-3. 使用 forEach 逐筆讀取資料、字串相加,並賦值在 str data.forEach(function(item) { let content = `<li>${item.name}.${item.Charge}</li>` str += content; }); // 1-4. 使用 innerHTML 將組好的資料渲染至網頁 list.innerHTML = str; ``` #### `forEach` 的執行過程 ```javascript= data.forEach(function(item) { let content = `<li>${item.name}.${item.Charge}</li>` str += content; }); // 執行過程 // 第一次 // <li>廖洧杰充電站.免費</li> // 第二次 // <li>廖洧杰充電站.免費</li><li>小花充電站.投幣式</li> // 第三次 // <li>廖洧杰充電站.免費</li><li>小花充電站.投幣式</li><li>小明充電站.投幣式</li> // 第四次 // <li>廖洧杰充電站.免費</li><li>小花充電站.投幣式</li><li>小明充電站.投幣式</li><li>小天充電站.投幣式</li> ``` ### 設計初始化功能 ```javascript= // 2. 設計初始化功能 // 前一段程式碼的作用是將資料渲染到網頁上 // 可以將這整個流程封裝成一個函式 // 作為「資料初始化」的功能 function init() { const list = document.querySelector("ul"); let str = ""; data.forEach(function (item) { let content = `<li>${item.name}.${item.Charge}</li>`; str += content; }); list.innerHTML = str; }; init(); ``` ### 註冊事件監聽與篩選資料 ```javascript= // 3. 註冊事件監聽與篩選資料 // 3-1. 選取監聽範圍 const stationFilter = document.querySelector(".stationFilter"); // 3-2. 註冊監聽 stationFilter.addEventListener("click", function (e) { // 3-3 取出 value // console.log(e.target.value); // 根據點擊範圍會回傳免費、投幣式或 undefined // 3-4. if 流程 // 3-4-1. 先判斷點擊到哪個按鈕 if (e.target.value === undefined) { console.log("請點擊按鈕。"); } else { let str = ""; // 3-4-2. 篩選資料,若 e.target.value === item.Charge 就執行區塊內程式碼 data.forEach(function (item) { if (e.target.value === item.Charge) { str += `<li>${item.name}.${item.Charge}</li>`; } }); // 3-5. 渲染資料 const list = document.querySelector("ul"); list.innerHTML = str; } }); ``` ### 使用全域變數 ```javascript= const list = document.querySelector("ul"); const stationFilter = document.querySelector(".stationFilter"); function init() { let str = ""; data.forEach(function (item) { let content = `<li>${item.name}.${item.Charge}</li>`; str += content; }); list.innerHTML = str; } init(); stationFilter.addEventListener("click", function (e) { if (e.target.value === undefined) { console.log("請點擊按鈕。"); } else { let str = ""; data.forEach(function (item) { if (e.target.value === item.Charge) { str += `<li>${item.name}.${item.Charge}</li>`; } }); list.innerHTML = str; } }); ``` * 在兩組函式中,都有出現 `const list = document.querySelector("ul");`,此時可以使用「**若函式內層找不到變數時,會往外層找。**」的特性,將選取 DOM 節點的程式碼,拉到全域管理。 ### 新增篩選條件 ```javascript= // 5. 新增「全部」按鈕的篩選條件 data.forEach(function (item) { if (e.target.value === "全部") { str += `<li>${item.name}.${item.Charge}</li>`; } else if (e.target.value === item.Charge) { str += `<li>${item.name}.${item.Charge}</li>`; } }); ``` ### 新增資料 ```javascript= // 6. 新增資料 // 6-1. 選取 DOM 節點 const stationName = document.querySelector(".stationName"); const stationCharge = document.querySelector(".stationCharge"); const btn = document.querySelector(".btn"); // 6-2. 註冊監聽 btn.addEventListener("click",function(e){ // 6-2-1. 取出表格裡的 value 後組合資料 let obj = {}; obj.name = stationName.value; obj.Charge = stationCharge.value; data.push(obj); // 6-2-2. 重新將資料渲染至網頁 init(); // 6-2-3. 清除輸入過的文字與指定選項預設文字 stationName.value = ""; stationCharge.value = "免費"; }); ``` ### 總結 #### HTML ```html= <h2>新增資料</h2> <input type="text" class="stationName" placeholder="充電站名稱"> <select class="stationCharge"> <option value="免費">免費</option> <option value="投幣式">投幣式</option> </select> <input type="button" class="btn" value="儲存"> <h2>顯示資料</h2> <div class="stationFilter"> <input type="button" value="全部"> <input type="button" value="免費"> <input type="button" value="投幣式"> </div> <ul> </ul> ``` #### JavaScript ```javascript= // 資料 let data = [ { Charge: "免費", name: "廖洧杰充電站", }, { Charge: "投幣式", name: "小花充電站", }, { Charge: "投幣式", name: "小明充電站", }, { Charge: "投幣式", name: "小天充電站", }, ]; // 選取 DOM const list = document.querySelector("ul"); const stationFilter = document.querySelector(".stationFilter"); const stationName = document.querySelector(".stationName"); const stationCharge = document.querySelector(".stationCharge"); const btn = document.querySelector(".btn"); // 初始化 function init() { let str = ""; data.forEach(function (item) { let content = `<li>${item.name}.${item.Charge}</li>`; str += content; }); list.innerHTML = str; } init(); // 篩選功能 stationFilter.addEventListener("click", function (e) { if (e.target.value === undefined) { console.log("請點擊按鈕。"); } else { let str = ""; data.forEach(function (item) { if (e.target.value === "全部") { str += `<li>${item.name}.${item.Charge}</li>`; } else if (e.target.value === item.Charge) { str += `<li>${item.name}.${item.Charge}</li>`; } }); list.innerHTML = str; } }); // 新增功能 btn.addEventListener("click",function(e){ let obj = {}; obj.name = stationName.value; obj.Charge = stationCharge.value; data.push(obj); init(); stationName.value = ""; stationCharge.value = "免費"; }); ```