`#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>
```

### 資料來源
* [重新認識 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)
#### 是否有註冊成功?

* 透過開發者工具的「事件監聽器」確認事件是否有註冊成功?以及註冊了哪些事件?
#### 迷你加法器練習
```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)`

* `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 = "免費";
});
```